0%

单独记录一下ES Module,听得太多用的也多,但是一直没深刻的认识一下它。

同样的节奏先Google找是否有大佬写这方面的文章。

幸运的是被我找到了Lin Clark(之前我学习浏览器方面的知识就是读的她的文章)写的,很喜欢她的文章,讲的很生动和具体。

开始

来来来,搬运一下这一篇ES modules: A cartoon deep-dive

模块如何提供帮助?

模块为您提供了更好的方式来组织这些变量和函数。使用模块,您可以将有意义的变量和函数组合在一起。

这会将这些函数和变量放入模块范围。模块作用域可用于在模块中的功能之间共享变量。

但是与函数作用域不同,模块作用域具有一种使其变量也可用于其他模块的方式。他们可以明确地说出模块中的哪些变量,类或函数应该可用。

当其他模块可以使用某些东西时,这称为导出。导出后,其他模块可以明确地说它们依赖于该变量,类或函数。

因为这是一种明确的关系,所以您可以知道如果删除另一个模块,哪个模块将中断。

一旦能够在模块之间导出和导入变量,就可以更轻松地将代码分解为可以相互独立工作的小块。然后,您可以组合并重组这些块(类似于积木),以从同一组模块创建所有不同种类的应用程序。

ES模块如何工作

在使用模块进行开发时,您将建立一个依赖关系图。不同依赖项之间的连接来自您使用的任何导入语句。

这些导入语句是浏览器或Node如何确切知道其需要加载哪些代码的方式。您给它一个文件,以用作图形的入口点。从那里开始,它紧随任何import语句以查找其余代码。

具有两个依赖关系的模块。 顶部模块是条目。 另外两个使用import语句关联

但是文件本身不是浏览器可以使用的东西。它需要解析所有这些文件,以将它们转换为称为模块记录的数据结构。这样,它实际上知道文件中正在发生什么。

具有各种字段的模块记录,包括RequestedModules和ImportEntries

之后,需要将模块记录转换为模块实例。实例结合了两件事:代码和状态。

该代码基本上是一组指令。这就像如何做某食物的食谱。但是就其本身而言,您不能使用该代码执行任何操作。您需要原材料才能与这些代码一起使用。

什么是状态?就像做食物的原材料。状态是变量在任何时间点的实际值。当然,这些变量只是内存中保存值的空间的昵称。

因此,模块实例将代码(指令列表)与状态(所有变量的值)组合在一起。

结合了代码和状态的模块实例

我们需要的是每个模块的模块实例。模块加载的过程将从此入口点文件变为具有模块实例的完整图。

对于ES模块,这分为三个步骤。

  1. 构造—查找,下载所有文件并将其解析为模块记录。
  2. 实例化—查找内存中的空间以放置所有导出的值(但尚未用值填充它们)。然后使导出和导入都指向内存中的那些空间的位置(地址)。这称为链接(引用)。
  3. 求值—运行代码以将变量的实际值填写在对应的内存空间。

三个阶段。 构建从单个JS文件到多个模块记录。 实例化链接那些记录。 评估执行代码。

人们谈论ES模块是异步的。您可以将其视为异步的,因为工作分为三个不同的阶段(加载,实例化和评估),并且这些阶段可以分别完成。

这意味着规范确实引入了CommonJS中不存在的一种异步。我将在后面解释,但是在CommonJS中,模块及其下面的依赖项一次全部被加载,实例化和求值,而中间没有任何中断。

但是,步骤本身不一定是异步的。它们可以以同步方式完成。这取决于正在执行的加载。这是因为并非所有内容都由ES模块规范控制。实际上有两部分工作,分别由不同的规范涵盖。

ES模块规范说,你应该如何解析文件到模块的记录,你应该如何实例化和评估模块。但是,它并没有说明如何首先获取文件。

加载程序将获取文件。加载程序在其他规范中指定。对于浏览器,该规范是HTML规范。但是您可以根据所使用的平台使用不同的装载程序。

两个卡通人物。 一个代表说明如何加载模块的规范(即HTML规范)。 另一个代表ES模块规范。

加载程序还精确控制模块的加载方式。它调用ES模块的方法- ParseModuleModule.InstantiateModule.Evaluate。有点像操纵JS引擎的字符串的p。

加载程序图形充当ES模块规范图形的伪造者。

现在,让我们详细介绍每个步骤。

构建

在构建阶段,每个模块发生三件事。

  1. 找出从哪里下载包含模块的文件
  2. 提取文件(通过从URL下载文件或从文件系统加载文件)
  3. 将文件解析为模块记录

查找并获取文件

加载程序将负责查找文件并下载。首先,它需要找到入口点文件。在HTML中,您可以通过脚本标记告诉加载程序在哪里找到它。

具有type = module属性和src URL的脚本标记。 src URL有一个文件,它是条目

但是,如何找到下一组模块- main.js直接依赖的模块呢?

这就是导入语句的来源。导入语句的一部分称为模块说明符。它告诉加载程序可以在哪里找到每个下一个模块。

有关模块说明符的一件事:在浏览器和Node之间有时需要对它们进行不同的处理。每个主机都有自己的解释模块说明符字符串的方式。为此,它使用一种称为模块解析算法的模块,该算法在平台之间有所不同。当前,某些可在Node中工作的模块说明符在浏览器中将无法工作,但仍在进行修复

在此问题修复之前,浏览器仅接受URL作为模块说明符。他们将从该URL加载模块文件。但这不会同时出现在整个图表上。在解析文件之前,您不知道模块需要获取哪些依赖项,并且在获取文件之前无法解析文件。

这意味着我们必须逐层遍历该树,解析一个文件,然后找出其依赖项,然后查找并加载这些依赖项。

如果主线程要等待这些文件中的每一个下载,则许多其他任务将堆积在其队列中。

这样阻塞主线程会使使用模块的应用程序使用起来太慢。这是ES模块规范将算法分为多个阶段的原因之一。将构造分为自己的阶段,使浏览器可以在开始实例化的同步工作之前获取文件并增强对模块图的理解。

这种方法(算法分为多个阶段)是ES模块和CommonJS模块之间的主要区别之一。

CommonJS可以做不同的事情,因为从文件系统加载文件比通过Internet下载花费的时间少得多。这意味着Node可以在加载文件时阻止主线程。并且由于文件已经加载,因此仅实例化和求值(在CommonJS中不是单独的阶段)是有意义的。这也意味着在返回模块实例之前,您要遍历整棵树,加载,实例化和评估任何依赖项。

CommonJS方法有一些含义,我将在后面详细解释。但是,这意味着一件事,就是在带有CommonJS模块的Node中,可以在模块说明符中使用变量。require在寻找下一个模块之前,您正在执行该模块中的所有代码(直到语句)。这意味着当您进行模块解析时,变量将具有一个值。

但是,使用ES模块,您可以在进行任何计算(求值)之前预先建立整个模块图。这意味着您不能在模块说明符中包含变量,因为这些变量尚无值。

但是有时将变量用于模块路径确实很有用。例如,您可能想根据代码在做什么或在什么环境中运行来切换加载的模块。

为了使ES模块成为可能,有一个建议叫做动态导入。有了它,您可以使用类似的导入语句import(${path}/foo.js)

动态导入的工作原理是,任何使用import()来导入的文件,都会作为一个入口文件从而创建一棵单独的依赖树,被单独处理。

但是要注意一件事–这两棵树中的任何模块都将共享一个模块实例。这是因为加载程序会缓存模块实例。对于特定全局范围内的每个模块,将只有一个模块实例。

这意味着浏览器的工作量更少。例如,这意味着即使多个模块依赖该模块文件,该模块文件也只会被提取一次。(这是缓存模块的一个原因。我们将在评估部分中看到另一个原因。)

加载程序使用称为模块映射的内容来管理此缓存。每个全局变量在单独的模块图中跟踪其模块。

当加载程序获取一个URL时,它将把该URL放入模块映射中,并记下它当前正在获取文件。然后它将发出请求并继续以开始获取下一个文件。

如果另一个模块依赖于同一文件会怎样?加载程序将在模块映射中查找每个URL。如果在其中看到fetching,它将继续前进到下一个URL。

但是模块图不仅跟踪正在获取的文件。模块映射还充当模块的缓存,如下所示。

解析

现在我们已经获取了该文件,我们需要将其解析为模块记录。这有助于浏览器了解模块的不同部分。

该图显示了被解析成模块记录的main.js文件

创建模块记录后,将其放置在模块映射中。这意味着无论何时从此处请求,加载程序都可以将其从该映射中拉出。

模块映射图中的“获取”占位符被模块记录填充

解析有一个细节看似微不足道,但实际上有很大的含义。所有模块都像严格模式来解析的。也还有其他的小细节,比如,关键字 await 在模块的最顶层是保留字, this 的值为 undefinded

这种不同的解析方式称为“解析目标”。如果您解析相同的文件但使用不同的目标,那么最终将得到不同的结果。因此,在开始解析之前就需要知道要解析的文件类型,不管是否是模块。

在浏览器中,这非常简单。您只需放入type="module"script标签。这告诉浏览器应将此文件解析为模块。并且由于只能导入模块,因此浏览器知道任何导入也是模块。

加载程序确定main.js是一个模块,因为script标签上的type属性表明是这样,而counter.js必须是一个模块,因为它已导入

但是在Node中,您不使用HTML标记,因此无法选择使用type属性。社区尝试解决此问题的一种方法是使用 .mjs扩展。使用该扩展名告诉Node,“此文件是一个模块”。您会看到人们将其视为解析目标的信号。目前讨论仍在进行中,因此尚不清楚Node社区最终决定使用什么信号。

无论哪种方式,加载程序都将确定是否将文件解析为模块。如果它是一个模块并且有imports,它将重新开始该过程,直到提取并解析了所有文件。

在加载过程的最后,您已经从只有一个入口点文件变成了拥有许多模块记录。

建设阶段的结果,左侧为JS文件,右侧为3个已解析的模块记录

下一步是实例化此模块并将所有实例连接在一起。

实例化

就像我之前提到的,实例化是将代码与状态结合在一起。该状态存在于内存中,因此实例化步骤就是将所有状态链接到内存。

首先,JS引擎创建一个模块环境记录(Module Environment Record)。它是管理所有模块记录的变量。然后,它会在内存中找到所有export对应的的地址。模块环境记录将跟踪内存中与每个export相关联的地址。

内存中的这些地址对应的空间尚无法获取其值。只有在运行之后,它们的实际值才会被填写。需要注意的一点是:在此阶段中将初始化所有导出的函数声明。这将使后面的执行阶段变得更加容易。

为了实例化模块关系图,引擎会采用深度优先的后序遍历方式。这意味着它将到达关系图的最底部(底部不依赖于其他任何东西),并设置其导出。

最终,引擎会把模块下的所有依赖导出连接到当前模块。接着回到上一层把模块的导入连接起来。

请注意,导出和导入均指向内存中的同一位置。

这不同于CommonJS模块。在CommonJS中,整个导出对象在导出时被复制。这意味着导出的任何值(如数字)都是副本,所以在CommonJS如果导出模块以后更改了该值,则导入模块将看不到该更改。

这意味着,如果导出模块以后更改了该值,则导入模块将看不到该更改。

相反,ES模块使用实时绑定(Live Binding)。两个模块都指向内存中的相同位置(引用)。这意味着,当导出模块更改值时,该更改将显示在导入模块中。

导出值的模块可以随时更改这些值,但是导入模块不能更改其导入的值,因为是导入的是只读引用。不过如果模块导入了一个对象,则它可以更改该对象上的属性值。

之所以ESM采用实时绑定,是因为可以在不运行任何代码的情况下链接到所有模块。这有助于解决循环依赖的问题,在后面的运行(evaluation)阶段会细说。

OK,当实例化结束时,我们得到了所有模块实例,并知道了已完成链接的导出/导入变量的内存地址。

现在我们可以开始评估代码,并使用它们的值填充这些内存位置。

运行

最后一步是往解析阶段获取的内存地址所在的空间里填充值。JS 引擎通过运行顶层代码(函数外的代码)来完成填充。

除了填充值以外,运行代码还可能引发副作用。例如,一个模块可能会请求服务器。

模块将在功能之外进行编码,标记为顶级代码

因为这些潜在副作用的存在,所以模块代码只能运行一次
前面我们看到,实例化阶段中发生的链接过程可以多次进行,并且每次的结果都一样。但是,如果运行阶段进行多次的话,则可能会每次都得到不一样的结果。

这正是为什么需要有模块映射的原因之一。模块映射通过规范URL,缓存模块,因此每个模块只有一个模块记录。这样可以确保每个模块仅执行一次。与实例化一样,这是深度优先的后遍历。

那我们之前谈到的循环依赖怎么处理呢?

在循环依赖关系中,您最终会在模块关系图中出现循环。你依赖我我依赖你,通常,这会变成很大的循环。

为了解释这个问题,我举个例子。

首先让我们看一下如果时CommonJS模块会时什么样的。首先,main模块将执行到require语句。然后它将去加载counter模块。

然后,counter模块将尝试从访问导出的对象message。但是由于尚未在main模块中执行,因此它将返回undefined。JS引擎将在内存中为局部变量分配空间,并将该值设置为undefined。

中间的内存,main.js和内存之间没有连接,但是从counter.js到未定义的内存位置的导入链接

此时会一直运行持续到counter模块顶级代码的末尾。我们想看看是否最终将获得正确的message(在执行main.js之后),因此我们设置了超时时间。然后继续运行到main.js

message变量将被初始化并添加到内存中。但是由于两者之间没有连接,因此message在counter模块中仍然是时undefined。

如果使用实时绑定处理导出,则counter模块最终将看到正确的值。到超时运行时,main.js的执行就已经完成并填充了值。

支持循环依赖是 ESM 设计之初就考虑到的一大因素。也正是这种分(三)阶段设计使其成为可能。

原文地址:

模块话,模块化说了那么多次,以为了解个大概就行了,然而在一次面试经历中被问到AMD和ES Module,所以特此来一篇,汇总一下吸收的模块方面的内容。

又是一篇搬运文章,谁让我放荡不羁爱打野呢。作者博客地址Preethi Kasireddy

为什么模块很重要?

如果没有模块,你能想象在复杂场景下你得js代码是个什么鬼模样吗?模块解决了名称空间和可维护性等变得越来越难以处理的问题。

好的模块是高度独立的,具有独特的功能,可以根据需要对它们进行改组,删除或添加,而不会破坏整个系统。

优势:

1)可维护性:根据定义,模块是独立的。精心设计的模块旨在尽可能减少对代码库各部分的依赖,从而使其能够独立增长和改进。当模块与其他代码解耦时,更新单个模块要容易得多。

2)命名空间:在JavaScript中,顶级函数范围之外的变量是全局变量(意味着每个人都可以访问它们)。因此,普遍存在“命名空间污染”,其中完全不相关的代码共享全局变量。

在不相关的代码之间共享全局变量在开发中是一个很大的禁忌。模块允许我们通过为变量创建私有空间来避免名称空间污染。

3)可重用性:抽取通用部分,哪里需要就拿去,不用重复写,当然也对应的第一点,当有修改时只需要该一份。

早期的时候为了达到“模块模式”,也有很多方式,不过我看了下基本上都是基于匿名闭包的基础上而来的。

这些方式有一个共同点:使用单个全局变量将其代码包装在函数中,从而使用闭包作用域为其自身创建私有名称空间,同时自定义公开哪些方法、变量

大家可以看下jQuery的源码,就是这样的实现方式,如下面的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
var myGradesCalculate = (function () {

// Keep this variable private inside this closure scope
var myGrades = [93, 95, 88, 0, 55, 91];

var average = function() {
var total = myGrades.reduce(function(accumulator, item) {
return accumulator + item;
}, 0);

return'Your average grade is ' + total / myGrades.length + '.';
};

var failing = function() {
var failingGrades = myGrades.filter(function(item) {
return item < 70;
});

return 'You failed ' + failingGrades.length + ' times.';
};

// Explicitly reveal public pointers to the private functions
// that we want to reveal publicly

return {
average: average,
failing: failing
}
})();

myGradesCalculate.failing(); // 'You failed 2 times.'
myGradesCalculate.average(); // 'Your average grade is 70.33333333333333.'

如您所见,这种方法使我们可以决定将哪些变量/方法设为私有(例如*myGrades*),以及通过将它们放在return语句中(例如*average**failing***)来公开哪些变量/方法。

所以大家可以看出,早期关于模块的写法,尽管每种方法都以其自己的方式有效,但它们也有缺点:

  1. 大家各自发挥的,这样有个最大的问题就是到最后就是乱的,就像为什么需要TC39一样,让大家有一套标准,才能从而写出更友好通用的模块。
  2. 依赖管理是个问题,需要我们开发人员自己管理依赖,记得刚开始用jQuery这种第三方库的时候,如果没注意到顺序就会报错,所以想象一下如果引用较多时,管理依赖关系并正确解决这些问题会让人头疼。
  3. 全局作用域被污染,上述的方式创建的变量(比如myGradesCalculate)都在全局范围内,所以该全局范围内的代码的每个部分都可以更改该变量。恶意代码可以有意更改该变量,以使您的代码执行您不希望这样做的事情,或者非恶意代码可能会无意间破坏了您的变量。

所以基于上述原因,这个时候我们就需要一套规范了来解决这些个问题:我们能否设计一种无需遍历全局范围即可请求模块接口的方法

规范

现在比较通行得规范有两种:CommonJSAMD

CommonJS

CommonJS是一个旨在定义一系列规范以帮助开发服务器端JavaScript应用程序的项目。CommonJS团队尝试解决的领域之一就是模块,负责设计和实现用于声明模块的JavaScript API。我听说CommonJS,最早是在15年写Node应用的时候接触的,Node.js最开始就是遵循这套规范弄得模块化,但是据说后来不用该规范了。

一个CommonJS的模块本质上是一种可重复使用的一段JavaScript代码其中出口特定对象,使它们可用于其他模块需要在他们的计划。

使用CommonJS,每个JavaScript文件都将模块存储在其自己的唯一模块上下文中(就像将其包装在闭包中一样)。在此范围内,我们使用module.exports对象公开模块,并要求将其导入。

当您定义CommonJS模块时,它可能看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
function myModule() {
this.hello = function() {
return 'hello!';
}

this.goodbye = function() {
return 'goodbye!';
}
}

module.exports = myModule;

我们使用特殊对象模块,并将我们函数的引用放入module.exports中。这使CommonJS模块系统知道我们要公开的内容,以便其他文件可以使用它。

然后,当某人想要使用myModule时,他们可以在其文件中要求它,如下所示:

1
2
3
4
5
var myModule = require('myModule');

var myModuleInstance = new myModule();
myModuleInstance.hello(); // 'hello!'
myModuleInstance.goodbye(); // 'goodbye!'

与我们之前讨论的模块模式相比,这种方法有两个明显的好处:

  1. 避免全局命名空间污染
  2. 明确我们的依赖关系

要注意的另一件事是,CommonJS采用服务器优先的方法并同步加载模块。这很重要,因为如果我们有我们需要的其他三个模块需要,它就会加载它们一个接一个。

现在,它可以在服务器上很好地工作,但是不幸的是,这使得为浏览器编写JavaScript时更难使用,因为服务器端通常是从磁盘读取,而浏览器需要网络请求,所以只要加载模块的脚本一直在运行(JavaScript线程将停止直到代码被加载),它就会阻止浏览器运行其他任何东西,直到加载完成。

AMD

从上面我们知道CommonJS是同步的,所以很显然不适用浏览器端,那我们就需要异步模块定义的规范,即AMD

使用AMD加载模块如下所示:

1
2
3
define(['myModule', 'myOtherModule'], function(myModule, myOtherModule) {
console.log(myModule.hello());
});

这里发生的是,define函数将每个模块依赖项的数组作为第一个参数。这些依赖项在后台加载(以非阻塞方式),并且一旦加载了define,便调用回调函数。

接下来,回调函数将加载的依赖项作为参数(在本例中为myModule*myOtherModule),以允许函数使用这些依赖项。最后,还必须使用**define***关键字定义依赖项本身。

例如,***myModule***可能看起来像这样:

1
2
3
4
5
6
7
8
9
10
11
define([], function() {

return {
hello: function() {
console.log('hello');
},
goodbye: function() {
console.log('goodbye');
}
};
});

与CommonJS不同,AMD采用了浏览器优先的方法以及异步行为来完成工作。

除了异步之外,AMD的另一个好处是您的模块可以是对象,函数,构造函数,字符串,JSON和许多其他类型,而CommonJS仅支持将对象作为模块。

AMD与CommonJS相比,其提供的io,文件系统和其他面向服务器的功能不兼容。

UMD

对于需要同时支持AMD和CommonJS功能的项目,还有另一种格式:通用模块定义(UMD)。

UMD本质上创建了一种使用这两种方法之一的方式,同时还支持全局变量定义。结果,UMD模块能够在客户端和服务器上工作。

以下是UMD如何开展业务的快速体验:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['myModule', 'myOtherModule'], factory);
} else if (typeof exports === 'object') {
// CommonJS
module.exports = factory(require('myModule'), require('myOtherModule'));
} else {
// Browser globals (Note: root is window)
root.returnExports = factory(root.myModule, root.myOtherModule);
}
}(this, function (myModule, myOtherModule) {
// Methods
function notHelloOrGoodbye(){}; // A private method
function hello(){}; // A public method because it's returned (see below)
function goodbye(){}; // A public method because it's returned (see below)

// Exposed public methods
return {
hello: hello,
goodbye: goodbye
}
}));

ES Module

是我做前端开始听的最多的了,当然也因为无时无刻都在用它。

上面咱们所说的,都不是JavaScript固有的。不过幸运的是,TC39(定义ECMAScript语法和语义的标准机构)引入了ECMAScript 6(ES6)内置模块。

ES6提供了多种导入和导出模块的可能性,其他人则做了很好的解释-以下是其中的一些资源:

与CommonJS或AMD相比,ES6模块最大的优点是它能够提供两全其美的优势:紧凑和声明性语法以及异步加载,以及诸如更好地支持依赖项等附加优点。

ES6模块最让人兴奋的应该是导入是导出的实时只读视图,即是只读引用,不过却可以改写属性。所以你猜到了当你模块里的值属性发生变化时,导入的地方获取的值是一样的。ES Module具体的后面还有一篇文档单讲,不然这文章就太长了。

最后我们对比一下两种方式吧

ES Module与CommonJS:

  • CommonJS规范通常适用于Node这类服务器端的
  • CommonJS模块是对象,是运行时加载,运行时才把模块挂载在exports之上(加载整个模块的所有),加载模块其实就是查找对象属性。
  • ES Module不是对象,是使用export显示指定输出(函数、对象、变量等),再通过import导入。为编译时加载,编译时遇到import就会生成一个只读引用。等到运行时就会根据此引用去被加载的模块取值。所以不会加载模块所有方法,仅取所需。
  • CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
  • CommonJS 模块是运行时加载,ES6 模块是编译时输出接口

说明:

全局范围(作用域)

  1. 写在script标签中的JS代码,都在全局作用域 。
  2. 全局作用域在页面打开时创建,在页面关闭时销毁。
  3. 在全局作用域中有一个全局对象window,它代表的是一个浏览器的窗口,它由浏览器创建我们可以直接使用 。
  4. 全局作用域中,创建变量都会作为window对象的属性保存
  5. 创建的函数都会作为window对象的方法保存
  6. 全局作用域中的变量都是全局变量,在页面的任何部分都可以访问的到并可以修改它

来源:

话说从接触Linux那一天起,就有一种独领风骚的傲气,命令走天下的这种霸气,那时候感觉精通Linux的连女朋友都带光环。

我最开始接触Linux是从Ubuntu开始,当时笔记本装了双系统,我就鼓捣了一个Ubuntu,现在用的较多的CentOS,首先入门都是先从了解root开始,我其实一直对root很好奇,刚好今天看到twitter上ruanyf大佬推了一篇文章Root User in Ubuntu: Important Things You Should Know讲基于Ubuntu的root的文章,基础好用,我们现在做的产品的安装包刚好也是基于Ubuntu做的,所以更有必要在此做个中文版记录。

开始

文章主要讲了以下四块:

  1. 为什么在Ubuntu中禁用root用户
  2. 以root身份使用命令
  3. 切换到root用户
  4. 解锁root用户

什么是root用户?为什么将其锁定在Ubuntu中?

根用户Ubuntu

稍微了解Linux的都知道,在Linux中,有一个称为root的超级用户。这是超级管理员帐户,可以使用系统执行任何操作。它可以访问任何文件并在Linux系统上运行任何命令。

拥有权利的同时也被赋予了重大的责任。超级用户可以为您提供对系统的完全控制权,因此也格外谨慎。超级用户可以访问系统文件并运行命令来更改系统配置。因此,错误的命令可能会造成无法挽回的损失,比如网上老说的一个梗:当rm -rf 之后我跑路了。所以一般情况下都不会给root权限,管理员会分配部分权限建对应的用户供对应的人使用。

这也说明了为什么Ubuntu默认情况下锁定了root用户,就是为了避免意外灾难。

您无需具有root特权即可执行日常任务,例如将文件移动到主目录中,从Internet下载文件,创建文档等。

*以此类比更好地理解它。如果必须切水果,可以使用菜刀。如果必须砍伐树木,则必须使用锯。现在,您可以使用锯切水果,但这不明智,是吗?*

这是否意味着您不能成为Ubuntu的root用户或无法使用具有root用户特权的系统?不,您仍然可以在“ sudo”的帮助下获得root用户访问权限(在下一节中说明)。

重点:

用户功能强大,无法用于常规任务。这就是为什么不建议始终使用root的原因。您仍然可以使用root运行特定命令。

如何在Ubuntu中以root用户身份运行命令?

须藤三明治xkcd图片来源:xkcd

当你需要某些系统特定任务的root特权。例如,如果要通过命令行更新Ubuntu,则不能以常规用户身份运行该命令。会有以下类似的错误。

1
2
3
4
5
6
apt update
Reading package lists... Done
E: Could not open lock file /var/lib/apt/lists/lock - open (13: Permission denied)
E: Unable to lock directory /var/lib/apt/lists/
W: Problem unlinking the file /var/cache/apt/pkgcache.bin - RemoveCaches (13: Permission denied)
W: Problem unlinking the file /var/cache/apt/srcpkgcache.bin - RemoveCaches (13: Permission denied)

那么,这个时候怎么做呢?简单的答案是在需要以root身份运行的命令之前添加sudo。

1
sudo apt update

Ubuntu和许多其他Linux发行版使用一种称为sudo的特殊机制。Sudo是一个程序,用于以root(或其他用户)身份控制对运行命令的访问。

Sudo实际上是一个多功能的工具。可以将其配置为允许用户以root用户身份运行所有命令。您可以配置它仅以root身份运行选定的几个命令。您也可以配置为不带密码运行sudo

在安装Ubuntu时,必须创建一个用户帐户。该用户帐户在您的系统上以管理员身份运行,并且按照Ubuntu中的默认sudo策略,它可以使用root特权在系统上运行任何命令。

运行sudo不需要root密码,但需要用户自己的password

这就是为什么当使用sudo运行命令时,总会一开始就询问密码:

1
2
gamehu@nuc:~$ sudo apt update
[sudo] password for gamehu:

如您在上面的示例中看到的,用户gamehu试图使用sudo运行’apt update’命令,系统要求输入gamehu的密码。

要注意当您开始在终端中输入密码时,屏幕上什么都没有发生不会有任何显示,也没有所谓的删除键、退格键…这里会记录你按下的所有键。因为作为默认安全功能,屏幕上不显示任何内容。甚至没有星号(*)。您输入密码,然后按Enter。*

划重点:

要在Ubuntu中以root身份运行命令,请在命令前添加sudo。
当要求输入密码时,输入您的帐户密码。
在屏幕上键入密码时,看不到任何内容。只需继续正确的输入密码,然后按Enter。

如何在Ubuntu中成为root用户?

你可以使用sudo以root身份运行命令。但是,在某些情况下,您必须以root用户身份运行多个命令,则可以临时切换为root用户。

sudo命令允许您使用以下命令模拟root登录shell:

1
2
3
4
5
6
sudo -i
gamehu@nuc:~$ sudo -i
[sudo] password for gamehu:
root@nuc:~# whoami
root
root@nuc:~#

您会注意到,切换到root用户时,shell命令提示符将从$(美元键符号)更改为#(磅键符号)。

*尽管已向您展示了如何成为root用户,但我必须警告你,应避免将系统用作root用户。毕竟出于某种原因,我们不建议这样做。*

临时切换到root用户的另一种方法是使用su命令:

1
sudo su

如果您尝试在不使用sudo的情况下使用su命令,则会遇到“ su身份验证失败”错误。

您可以使用exit命令恢复为普通用户。

1
exit

如何在Ubuntu中启用root用户?

到目前为止,您已经知道默认情况下,root用户在基于Ubuntu的发行版中被锁定。

Linux使您可以自由地对系统进行任何操作。解锁root用户是这些自由之一。

如果出于某种原因决定启用root用户,则可以通过为它设置密码来启用它:

1
sudo passwd root

同样,不建议这样做,我也不鼓励您在桌面上执行此操作。如果忘记了密码,将无法再次在Ubuntu中更改root密码

您可以通过删除密码来再次锁定root用户:

1
sudo passwd -dl root

记录近两年各种机会下与事业部头部三剑客交流收获的一些真知灼见。

关于合作

目标一致

看上去的完美搭档其实是一个很磨人的过程,一开始恨不得一见面就呼巴掌作为打招呼的方式,最终能达成默契或者说合作,目标一致最重要的前提。

不管你怎么看不惯一个人,三观有多么不合,但是当你发现大家目标都是一致的时候,静下来的时候大家总会想一想,是不是我有问题,慢慢的就变得能够互相包容,所以要做成事首要条件不是考虑要找多要好的人多默契的人,关键在于你们的目标是否一致,只要目标一致你会发现你们很难分割彼此。

所以有目标是一件重要紧急的事。

关于做事

干一行,爱一行,不然干不了大事。

最深刻的一句话,我记得当时是我谈起对我现在做的事好像也没到喜欢的程度的时候,领导对我说了这句话,听后我记得当时醍醐灌顶,然后手脚冰凉,内心很奔溃,至此以后我逐渐爱上了我现在做的事,虽然还是有很厌烦的时候,不过总能爱回去。

勇于释放自己,学会利用资源,学会影响上级。

这句话是我最亲近的老大对我说的,当我在抱怨做事的一些困惑时。这是最有挑战的不过收获也是最大的,这句话不是教我怎么在领导面前表现,而是告诉我领导在意的是你的结果,当你结果好的时候,才会在意过程,从而改变对你的一些印象。当你做出结果时,他们会关注到的。

关于自信

当你进入了公司就不要再因为自己的出身、学历等感到自卑,因为能进来表示公司已经认可了你,接下来你要想的是,怎么做到比别人牛逼。

否定自己是很耗能量的一件事,积极一点。

OK,正式说明了

SSO的说明网上有很多我就不在这儿丢人了。找了张小图SSO的作用一目了然。

以下主要记录一下我在产品中SSO的实践案例。

案例1

案例1 是比较标准的基于OpenID方式的SSO,用Node.js写的。

案例1没什么说的,网上样例很多,如果有兴趣可以看下我之前写的,不过比较老了,也是第一次写nodejs。

案例2

则是非标的SSO,用Java+javascript写的。

案例2虽然不是非标的,不过整体流程是具备的,比较适用特定编码场景(Spring Security+OpenID),可能有需要的同学,反正我是没在网上找到这类案例。

客户现场的系统A需要登入到我们提供的系统B,没有单独用户中心即也不存在用户同步,客户要求的是能无缝登入,所以解决办法有用户则直接登入无用户则创建后再登入,登录效果与从登录页面发起的登录一样,所以token解析后用Security的方式执行登录。

前端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
/**
* sso出现在路径末尾 react router方式 目前采用这种方式 http://.../frame/#/module/xxx?sso=xxx 避免sso一直保留
* @param key 需要获取url参数key
* @returns {string|null}
*/
export function getSsoString(key) {
const str = location.hash;
if (str == null || str.length < 2) {
return null;
}
const arr = str.split('?');
if (arr != null && arr.length === 2) {
const query = arr[1];
if (query != null && query.length > 0) {
const words = query.split('&');
// 将每一个数组元素以=分隔并赋给obj对象
for (let i = 0; i < words.length; i++) {
const tmp_arr = words[i].split('=');
const k = decodeURIComponent(tmp_arr[0]);
const v = decodeURIComponent(tmp_arr[1]);
if (k === key) {
return v;
}
}
}
}
return null;
}

/**
* 单点登录逻辑 在页面token发送到后端进行验证
* @param callback
*/
export function sso(callback) {
const token = getSsoString('sso');
if (token != null) {
req(BASE_WEB_API.SSO, { token }, null, { validateError: true })
.then(response => {
// do something....
if (callback != null) {
callback();
}
})
.catch(e => {
console.error('failed sso --> ', e);
if (callback != null) {
callback();
}
});
} else if (callback != null) {
callback();
}
}

后端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
/**
* 跳转到猎豹系统
*
* @param response
* @throws Exception
*/
@PostMapping(value = "/cheetah", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public String cheetah(@RequestBody SSOVO ssovo,
HttpServletRequest request,
HttpServletResponse response) throws Exception {
try {
// 验证license
if (!licenseService.isValid()) {
LOGGER.error("license is invalid");
return validateTokenError(request, LICENSE_ERROR_MSG);
}
//解析token
Context.Token userToken = Context.getUserInfoFromToken(ssovo.getToken());
if (isNullOrEmpty(userToken.getUserName()) || isNullOrEmpty(userToken.getPassword())) {
LOGGER.warn("token is invalid:{}", ssovo.getToken());
return validateTokenError(request);
}
LOGGER.info("当前单点登录的用户信息为:{}", JSON.toJSONString(userToken));
//验证内置用户是否存在,不存在则创建
SSOUserVO user = ssoService.checkUser(userToken.getUserName(), Context.getCmsContext());
if (user != null) {
// 执行登录
user.setPassword(userToken.getPassword());
return ssoLogin(request, response, user);
}
//异常时跳转到登录页
return validateTokenError(request);
} catch (Exception e) {
LOGGER.error("sso登录失败:{}", e.getMessage());
return validateTokenError(request);
}
}

private String validateTokenError(HttpServletRequest request) {
return validateError(request, SSO_VERIFICATION_ERROR_MSG);
}

private String validateTokenError(HttpServletRequest request, String msg) {
return validateError(request, msg);
}

private String validateError(HttpServletRequest request, String msg) {
HttpSession session = request.getSession();
if (session != null) {
//使session失效
session.invalidate();
}
SSOErrorVO errorVo = new SSOErrorVO(SSO_VERIFICATION_ERROR, msg);
return JSON.toJSONString(errorVo);
}
/**
* 执行登录
*
* @param request
* @param response
* @param userToken
* @return
* @throws IOException
* @throws ServletException
*/
private String ssoLogin(HttpServletRequest request, HttpServletResponse response, SSOUserVO userToken) throws IOException, ServletException {
try {
//登录
UsernamePasswordAuthenticationToken authReq
= new UsernamePasswordAuthenticationToken(userToken.getUserName(), userToken.getPassword());
authReq.setDetails(new WebAuthenticationDetails(request));
Authentication auth = authenticationManagerBean.authenticate(authReq);
SecurityContextHolder.getContext().setAuthentication(auth);
HttpSession session = request.getSession(true);
// 永不超时
session.setMaxInactiveInterval(-1);
//TODO 静态导入
session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, SecurityContextHolder.getContext());
baymaxLoginSuccessHandler.onAuthenticationSuccess(request, response, auth);
} catch (AuthenticationException failed) {
LOGGER.warn(
"sso: InternalAuthenticationServiceException occurred while trying to authenticate the user.",
failed);
SecurityContextHolder.clearContext();
baymaxAuthenticationFailureHandler.onAuthenticationFailure(request, response, failed);
validateTokenError(request);
}

return null;
}

/**
* 根据用户名,获取用户的token
*
* @param userName
* @param response
* @return
*/
@RequestMapping(value = "/getToken/{userName}", produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
public String getToken(@PathVariable(value = "userName", required = false) String userName, HttpServletResponse response) {

try {
return Context.createToken(userName, PasswordUtil.getPlaintextPwd());
} catch (Exception e) {
LOGGER.error("获取token失败:{}", e.getMessage());
formatErrorResponse(response, HttpServletResponse.SC_BAD_REQUEST, e.getMessage());
return null;
}
}

private void formatErrorResponse(HttpServletResponse response, int httpCode, String errorMsg) {
response.setStatus(httpCode);
response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
try (PrintWriter out = response.getWriter();) {
String errorMsgVo = JSON.toJSONString(ImmutableMap.of("code", SSO_GET_TOKEN_ERROR, "message", errorMsg));
out.write(errorMsgVo);
out.flush();
} catch (IOException ex) {
LOGGER.warn("get token :{}", ex.getMessage());
}
}

处理400异常避免出现白页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @author Gamehu
* @description 接管400异常,个性化错误提示
* @date 2019/12/19
*/
@RestControllerAdvice(assignableTypes = SSOController.class)
@Order(Ordered.HIGHEST_PRECEDENCE)
@Slf4j
@Component
public class SSO400ExceptionHandler {
@ExceptionHandler(value = Exception.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public Object defaultErrorHandler(Exception e) {
log.warn("---SSO验证异常--- ERROR: {}", e.getMessage());
return ImmutableMap.of("code", SSO_VERIFICATION_ERROR, "message", e.getMessage());
}
}

引伸阅读:

最近产品提了一个需求,要做菜单优化。

菜单优化:

  1. 重写菜单数据初始化sql脚本,数据结构两层变为三层
  2. 权限数据调整
  3. 兼容原有菜单权限数据

这里面第三点是最麻烦的,兼容原有的菜单数据,我给需求说了两个方案。

PlanA:从时间方面(因为是个小迭代,整个迭代的功能只有一周时间开发)考虑管理员菜单权限保留,其它普通角色菜单权限一律置空,需求也接受(因为客户现场大多数情况都是用管理员账号)。

PlanB:保留原有数据,但是需要1-2天预研一下升级方案是否可行,我提出的方案是直接用sql脚本做,需求也认可,PlanA为兜底方案。

我就提个解决方案,结果这事最后让我支援一下给做了…,所以在这简单记录一下过程产物。

PLV8

简单地说就算PostgreSQL里加个扩展,这个扩展就是V8引擎,是的,你理解的没错,就是Google开源的JavaScript引擎,有了这个扩展那就能在sql里写js代码了,这对于在脚本里写逻辑那可是爽歪歪了。

安装过程我就不多说了,网上有很多,大体流程就是

  1. 从GitHub wget 下来
  2. make install
1
2
3
4
5
#添加扩展
CREATE EXTENSION plv8;

#验证plv8的版本,出来版本号就证明装上了
SELECT plv8_version();

然后你就可以写JS代码了,ES6、coffeeScript等都可以,只要最终是v8能解析的就成。

好现在开始写了,一开始不太了解,打算用存储过程做,但是后来老前辈提醒我,我这个是升级脚本,只需要执行一遍就成,所以没必要做存储过程,最后还得删掉,因为留着没意义。

然后建议我用DO $$的写法,此方式执行完不会留下其它痕迹就跟执行一条长sql一样,贴个代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
DO
$$
plv8.elog(INFO, '------------------------------------update user menus start------------------------------------');
buildTree = function(list) {
try {
let temp = {};
let tree = {};
for (let menu of list) {
temp[menu.id] = menu;
}
for (let i in temp) {
if (temp[i].parent) {
if (!temp[temp[i].parent].children) {
temp[temp[i].parent].children = {};
}
temp[temp[i].parent].children[temp[i].id] = temp[i];
} else {

tree[temp[i].id] = temp[i];
}
}
return tree;

} catch (error) {
plv8.elog(ERROR, 'buildTree ' + error);
}
};

getTreeOfMenus = function() {
let cmdb = require('xxx');
let result = cmdb.service.query('default', '{xxx{id name}}', {});
return buildTree(result);
};
let allMenus = getTreeOfMenus();

delete allMenus['xxx'];

let ids = [];
idsOfflatten = function(data) {
try {
for (let id in data) {
ids.push(id);
if (data[id].children) {
idsOfflatten(data[id].children);
}
}
return ids;
} catch (error) {
plv8.elog(ERROR, 'idsOfflatten ' + error);
}
};
let flatIds = idsOfflatten(allMenus);
plv8.elog(INFO, 'new menu ids:' + flatIds);
getNormalMenus = function(menus) {
try {
plv8.elog(INFO, 'old menus : ' + JSON.stringify(menus));
let old_keys = Object.keys(menus);
old_keys.forEach(id => {
if (!flatIds.includes(id)) {
plv8.elog(INFO, 'delete menu id: ' + id);
delete menus[id];
}
});
return menus;
} catch (error) {
plv8.elog(ERROR, 'getNormalMenus ' + error);
}
};

updateMenus = function() {
try {
let ROLE_ADMIN = 'admin',
ROLE_DOMAIN = 'domain';
let query = 'SELECT name, menus, role_type FROM xxx';
let updateAdmin =
'UPDATE xxx SET menus=NULL, last_modified=CURRENT_TIMESTAMP where name = $1';
let updateNormal = 'UPDATE xxx SET menus=$1, last_modified=CURRENT_TIMESTAMP where name = $2';
let execCount=0;
plv8.execute(query).forEach(row => {
let roleType = row.role_type;
if (roleType === ROLE_DOMAIN || roleType === ROLE_ADMIN) {
let adminCount = plv8.execute(updateAdmin, [row.name]);
plv8.elog(INFO, 'update admin user menus is null ,count: '+adminCount );
adminCount>0 ? execCount+=1:null;
return;
}

let newMenus = getNormalMenus(row.menus);
let normalCount = plv8.execute(updateNormal, [newMenus, row.name]);
plv8.elog(INFO, 'update normal user menus , ' + JSON.stringify(newMenus)+',count:'+normalCount);
normalCount>0 ? execCount+=1:null;
});

return execCount;
} catch (error) {
plv8.elog(ERROR, 'updateMenus ' + error);
}
};

let updateCount=updateMenus();
plv8.elog(INFO,'total of successes :'+updateCount);
plv8.elog(INFO,'------------------------------------update user menus end------------------------------------');
$$ LANGUAGE plv8;

小结:

PostgreSQL很强大,这是我初试水,后续有机会会再写写工作中的一些PostgreSQL的实践例子。

其实PostgreSQL几乎可扩展主流的所有编程语言比如C++、Java、nodejs等。

参考文档:

背景

由于我们产品是基于docker做的部署,所以不管在开发过程中还是在处理客户现场问题时,多多少少都要用到一些docker命令,此篇做个简单的记录,把我用到的命令记录下来。

先看图

说命令之前先看图了解下便于更有代入感。

架构图(不包含Dokcer Engine等细节)

命令

各种查看

docker COMMAND --help

查看docker相关命令的信息,里面有每个命令的说明。

docker ps、docker ps -a

docker ps 这是最常用的,查看容器的运行状态,查问题时不ps一下心里都没底,该命令会列出所有正在运行的容器,当然 等同于docker container ls。

另外docker ps -a,可用于显示所有正在运行和退出的容器。

docker info、docker version

docker info 该命令用于获取当前安装的docker版本以及有关操作系统的几条信息。

docker version 列出有关Docker客户端和服务器版本的信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Client:
Version: 18.09.7
API version: 1.39
Go version: go1.10.1
Git commit: 2d0083d
Built: Fri Aug 16 14:20:06 2019
OS/Arch: linux/amd64
Experimental: false

Server:
Engine:
Version: 18.09.7
API version: 1.39 (minimum version 1.12)
Go version: go1.10.1
Git commit: 2d0083d
Built: Wed Aug 14 19:41:23 2019
OS/Arch: linux/amd64
Experimental: false
docker search xxx

该命令只有在我自己玩得时候用过(不想重复造轮子),搜索registry上得镜像。

docker images

列出所有的镜像,通常只需要关注REPOSITORY、TAG两列就行。

1
2
3
4
5
6
7
8
9
10
11
root@feature1_dev:~# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
172.17.162.141:5000/baymax-nginx 1.17.6 231d40e811cd 6 months ago 126MB
172.17.162.141:5000/pg10-cmdb 1.3.0 284de991364f 10 months ago 370MB
172.17.162.141:5000/myflink 1.8.1 96c4d2af10fc 10 months ago 449MB
172.17.162.141:5000/yandex/clickhouse-server 19 58006c9044b7 13 months ago 514MB
172.17.162.141:5000/zookeeper latest f336949ce7a1 19 months ago 148MB
172.17.162.141:5000/redis latest 1babb1dde7e1 20 months ago 94.9MB
172.17.162.141:5000/kafka latest 568143d73a6b 20 months ago 339MB
172.17.162.141:5000/dubbo-admin latest 954bf5f29e96 2 years ago 492MB

docker logs -f container_name

查看容器的日志,我用的也较少。

docker commit -a "gamehu" -m "what f" container_id IMAGE_REPOSITORY:TAG`

通过容器id创建一个新的镜像,

Container

docker start 、stop、restart、rm、kill

高频使用了,后接 container_id/container_name,依次分别为:启动(已存在)容器、停止容器(会进行正常时间等待其停止)、重启重启、删除(已停止)容器、立即停止容器

docker exec -it container_id

使用的较多,通常是为了测试而替换容器内的内容,命令用于访问正在运行的容器,并启用交互模式,可用一些基本的命令。

1
2
3
4
root@feature1_dev:~# docker exec -it 58f5d79c10a3 /bin/bash
root@58f5d79c10a3:/# ls
bin boot dev etc home lib lib64 media mnt opt proc root run sbin srv sys tmp usr var
root@58f5d79c10a3:/#
docker inspect container_id

查看容器的相关信息,用的也较少。

Image

docker run、create

基于镜像创建一个新的容器,run是创建并启动,create是创建但不启动。

示例:docker run -i -t -p 1000:8000 image_name/image_id:TAG,使用镜像,以后台模式启动一个容器,将容器的 8000 端口映射到主机的 1000 端口

docker build <path to docker file>

此命令用于从指定的dockerfile构建镜像。

docker push IMAGE_NAME:TAG

做完镜像推送到镜像仓库。

docker rmi image_id/image_name

删除镜像,通常是处理现场问题,要替换镜像的时候用一用。

docker inspect image_id

查看镜像相关信息,我制作镜像的时候会用一用,用的很少。

docker save image_id> xx.tar

导出镜像,通常是修复现场问题时做该操作,导出已修复的镜像。

docker load < xx.tar

导入镜像,通常是修复现场问题时做该操作,载入已修复后的镜像。

docker tag image_id tag_name

修改镜像的TAG,通常是修复现场问题时做该操作。

感谢

本文引用的内容,如有侵权请联系我删除,给您带来的不便我很抱歉。


最近看了李笑来先生写的《区块链小白书》对其中的理解一个新事物正确的姿势在此做一个备忘。

1. 不要滥用类比

你将要尝试着学习并理解的,是一个前所未有的重大创新,其中有很多你一下子难以理解透彻的概念,很少有人能一下子全都弄明白……

在这样的时候,人们会不由自主地抄近路走捷径 —— 滥用类比:

“哦! 我明白了,这就好像是……”

比如,你经常会看到人们挣扎着理解了半天,突然冒出一句:“哦,我懂了,比特币就是电子黄金!” 类比是约等号(≈),而“这就是”是等号(=),约等号和等号之间的差别有时甚至超过十万八千里。 比特币与电子黄金之间的关系甚至干脆就谈不上是约等于…… 全然不是一个东西。

在理解全新事物的时候,滥用类比的危害非常大,因为你压根就找不到什么过往已经存在的东西真的和这个创新竟然一模一样 —— 否则,它也不可能被称为创新了,是不是?

这种不恰当的类比被滥用多次之后,就再也没办法形成正确的理解了 —— 因为理解一个创新需要理解多个前所未有的概念,每个都做了不恰当的类比之后,多次非常不恰当的约等于拼接起来之后,无论如何都没办法达到一个与正确理解相近的效果。

请务必注意,每次你的脑子里不由自主地冒出 “这就好像……” 这个念头的时候,你都要把它强压回去。

2. 重复重复再重复

遇到暂时无法理解的概念,不要担心、不要纠结、不要停顿,你要做的事情很简单:

  • 继续读下去;
  • 读完之后再重复读很多次……

这是学习任何新知识或者在任何新领域探索的 “必杀技”。这背后有一个重要的原理:

绝大多数难以理解的知识,是因为它内部有很多 “前置引用”。

所谓的前置引用,就是一个在后面才能深入理解的概念竟然在此之前已经被引用了,导致的结果是学习者总是处于懵懂的状态。学校里的知识却不是这样的,学校里的知识总是线性层层递进的,理解了前面,就能理解后面…… 关于 “前置引用”。

“硬着头皮读完,而后重复读很多次” 这个策略,就是可以轻松突破 “前置引用” 所设置的障碍。这个技巧,事实上可以用在任何领域。

3.借助群智的力量

快速掌握新知识,快速适应新领域,还有个重要的技巧,就是借助群智的力量。事实上,在学校里,你早就应该发现这个技巧了 —— 如果你能跟那些学霸经常聊天,经常玩耍,你就会发现总是在不经意之间,很多重点难点就那样轻而易举地被解决掉了……

这首先因为人是社交动物,然而更为重要的是,交流这个东西,随意的交流总是比刻意的交流更为有效 —— 因为随意的交流总是刻意解决那些连你自己都没意识到的问题…… 可偏偏,这些你自己意识不到的问题恰恰是最重要甚至最关键的问题。如果不借助这种群智的力量,很难想象还有什么办法可以解决这种隐秘的关键问题。

本文引用的内容,如有侵权请联系我删除,给您带来的不便我很抱歉。

背景

过去一年多的时间,整个产品是从0到1的,整个前端团队也几乎是从0到1(大多都是后端转的前端),被产品迭代的车路推动,滚滚向前,处于野蛮生长的阶段。最近产品大体进入了一个思考过去未来的阶段,相对较平缓。可以有喘息的时间干点想干的事情了。

你知道的,干一行爱一行,既然在前端了就想着咋个把它做的更好,因为以前做后端,后端的工程化的工具箱(CI、CD、监控…有一大堆实践)是很成熟的也较容易搭建起来,不过前端相对就很陌生了,据说也没那么容易。

记录

刚好关注了【前端早早聊】,发现里面有这部分的内容,盗了两张图,让我对前端工程化有了很宽的认识,做个记录,后续慢慢实践。

本文引用的图片,如有侵权请联系我删除。

最近看《圆桌派》聊的一个话题斜杠:理想还是欲望?一专还是多能?。想在这儿随便说说。

这个词其实前两年说的比较多,当时另一个说法是一个人的标签很多。为什么出现的频率高,也流行,更多的原因是在于那其实是大大多数的梦想。

里面窦先生说了一下,欲望和理想的区别:一定要结果的就是欲望,热爱过程的是理想。所以对于追求斜杠,你是出于欲望还是理想?从我个人来说,我对斜杠的追求我归为欲望,因为很显然我是需要结果的,不管是物质上的还是精神上的我都是有所求的。

我一直认为,斜杠青年:其实是一个很奢侈的一个愿望。

我比较俗啊,我对斜杠的定义是,每个斜杠都应该是一份事业且他们是相互独立(跨界)的,就像《圆桌派》里说的一样你虽然干了很多份事业但是其实他们都是有关联性或者有递进性的这其实不算是斜杠,因为你始终还是在那个圈里。

既然是事业那就肯定会有付出和回报当然咱们谈的不仅限于钱,最起码你要花时间和精力去做,且会收到反馈。人的时间和精力都是有限的,你把有限的时间和精力花在了多个不同的行业,且都得到反馈,这才算是斜杠。

比如你上午卖炒河粉,下午coding,晚上酒吧驻唱。这我觉得可以说是:餐饮/互联网/演艺,三个斜杠。

所以为什么说斜杠是很奢侈的,首先你的精力要跟上,其次你的时间能自己做主,然后你还得技术过硬。当然跟现实生活中一样奢侈品再贵,也总有些人是能消费的。所以不是说斜杠就没有,只是那是很小的一部分。

大家别把斜杠玩坏了,也别一味的飘飘然的追求斜杠,很多时候你自己要先静一静想一想,别听风就是雨的,不仅飞不高还摔得疼。

就算你有这个欲望,不得现有实力和条件吗,先让自己有这些条件然后再想吧。