模块

CommonJS

Node.js在2009年推出时候,就是一个基于V8引擎,事件驱动I/O的服务端JS运行环境,在09年推出的时候他就实现了一套名为CommonJS的模块化规范。

特点:

  • 在CommonJS规范里,每个JS文件就是一个 模块(module),每个文件内部可以使用 require 函数和 module.exports 对象来对模块进行导入和导出,最终实现作用域的隔离和模块化。
  • module.exports 就是一个很普通的js语法,实际上这个内容是在Node.js在运行的时候,解析JS文件的过程中加载进去的。就是说CommonJS的模块化规范只能在Node环境下运行,如果直接把这部分内容放到浏览器当中就会报错。
  • 在CommonJS规范中,每个模块都是单例的,当多次加载某个模块的时候,它实际上只会被加载一次,真正运行时候只有一次。 ``` // index.js require(‘./moduleA’); var m = require(‘./moduleB’); console.log(m);

// moduleA.js var m = require(‘./moduleB’); setTimeout(() => console.log(m),2000);

// moduleB.js var m = new Date().getTime(); module.exports = m;

  1. > index.js 代表的模块通过执行 require 函数,分别加载了相对路径为 ./moduleA ./moduleB 的两个模块,同时输出 moduleB 模块的结果。
  2. >
  3. > moduleA.js 文件内也通过 require 函数加载了 moduleB 模块,在2s后也输出了加载进来的结果。
  4. >
  5. > moduleB.js 文件内部仅定义了一个时间戳,然后直接通过 module.exports 导出。
  6. CommonJS 的一个模块,就是一个脚本文件。`require`命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象。

{ id: ‘…’, exports: { … }, loaded: true, … }

  1. 上面代码就是 Node 内部加载模块后生成的一个对象。该对象的`id`属性是模块名,`exports`属性是模块输出的各个接口,`loaded`属性是一个布尔值,表示该模块的脚本是否执行完毕。其他还有很多属性,这里都省略了。<br />以后需要用到这个模块的时候,就会到`exports`属性上面取值。即使再次执行`require`命令,也不会再次执行该模块,而是到缓存之中取值。也就是说,CommonJS 模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。
  2. **总结CommonJS模块化规范**
  3. 1. JS本身是没有模块化规范的,当09Node.js出来的时候带有CommonJS规范,给JS里面加载了require函数和module.exports对象,能够让我们通过这两种形式让我们对一个模块进行加载和导出,这是他模块导入导出的方式。
  4. 1. **所有模块都是单例的,一个模块在加载完成之后,无论加载它多少次,它的结果都和第一次加载的结果是相同的**。
  5. CommonJSNode.js原生实现的,就是说在node.js运行时实现了这么一套CommonJS的规范,所以我们只能在Node.js里面用。那么浏览器里面如果要用我们的模块化规范应该怎么做呢?为了应对这种情况,浏览器端实现了另一种模块化的规范AMD
  6. <a name="zIL37"></a>
  7. ### AMD
  8. 另一个为WEB开发者所熟悉的JS运行环境就是浏览器了。浏览器并没有提供像Node.js里一样的require 方法。不过受到 CommonJS 模块化规范的启发,WEB端还是逐渐发展起来了AMD, SystemJS 规范等适合浏览器端运行的JS模块化开发规范。
  9. - AMD全称 **Asynchronous module definition** 意为异步的模块定义,**不同于 CommonJS 规范的同步加载 AMD 正如其名所有模块默认都是异步加载**,这也是早期为了满足 Web 开发者的需要,因为如果在 Web 端也使用同步加载,那么页面在解析脚本文件的过程中可能使页面暂停响应。CommonJS规范是一个require函数,每当require函数加载的时候它才会通过文件系统读取该文件路径下模块的代码,然后再进行解析。 如果浏览器也这么做并且模块非常多的话,那么同步的进行解析文件浏览器的JS的就会被阻碍进行,会影响页面的渲染还有其他JS逻辑的执行。所以在浏览器中就不能通过CommonJS这种同步的方式来解析我们的模块,浏览器端就实现了这么一套异步的模块加载规范。
  10. - 其实比较简单,但大部分同学可能并不熟悉AMD模块化加载的规范,这块可以先给大家简单的讲解一下:AMD CommonJS 规范还是有自己特定要求的,AMD规范对于模块的依赖是基于ID的。AMD有一个 define 函数,他的ID就是他的第一个参数(或者他的文件名),如果我们不手动指定模块的ID的话那么默认模块ID就是当前的文件;第二个参数是定义一个函数作为AMD的模块(是不是和我们现在的微前端比较像)。同时,如果我们想加载的话,require函数提供两个参数,第一个参数是我们所有依赖的目标模块,就是我们所有依赖的模块的ID,它的回调里面就是所有模块的结果。
  11. - AMD模块化规范中,需要再加载一个符合AMD模块化规范的库,比较经典的就是require.js,它的作用就是提供上面说的一些definerequire函数。
  12. **require.js原来其实就是通过jsonP对模块进行了异步加载,听到最后一定会清楚他怎么做的,后面会讲到这部分的内容。**<br />python -m SimpleHTTPServer 8080 启动一个本地server。然后把网络速度调整到slow 3G,可以看到先加载了require.jsindex.js 因为在html里面写死了,然后AMD通过分析 index.js 发现需要异步加载 moduleA moduleB 两个模块,然后就发请求加载这两个模块。

// index.js require([‘moduleA’,’moduleB’],function(moduleA, moduleB){ console.log(‘B:’,moduleB); });

// moduleA.js define(function (require) { const m = require(‘moduleB’); setTimeout(() => console.log(‘A:’, m), 1000); });

// moduleB.js define(function () { var m = new Date().getTime(); return m; });

  1. **总结AMD模块化规范:**
  2. 1. **首先它不同于 CommonJS 规范,AMD 它是异步加载的**。
  3. 1. 它也是定义了一种模块化导入和导出的方式,但是和CommonJS不同,它是通过define函数来定义一个模块,通过require函数来加载一个模块,同时它是通过参数和回调的形式来确定这个模块已经被完全加载了。因为它本身就是异步的不像CommonJS require进来那就一定是加载成功了。
  4. 1. **和CommonJS一样,一个模块无论加载多少次,他的结果只会执行一次。后面加载的都是他的缓存。**
  5. Node里面用CommonJS,浏览器还用AMD比较好,当然这些都是在没有打包的情况下。没有进行编译的情况下就可以在直接这么使用。
  6. 不管是AMD还是CommonJS规范,都有个两个特点:
  7. 1. 就是在语言上层的运行环境中实现的模块化规范,也就是说**模块化规范由环境自己来定义**。(CommonJSnode.js实现的,node发现了这个文件里面有requiremodule.exports的时候,就会在解析的过程中增加require函数和module.exports对象,所以CommonJS只能运用在node环境中,因为其他环境没有实现这个东西;AMD同理,如果要使用AMD规范必须要使用一个AMD的加载器 require.js 核心原理就是使用了jsonP 来对模块进行异步加载,然后再执行一个回调,这两个都有个缺点就是必须在特定的环境中使用,或者必须要加载特定的库)
  8. 1. 相互之间不能共用模块,例如不能在Node.js运行AMD模块,不能直接在浏览器运行CommonJS模块。 (如果不借助任何外界工具webpackbrowserify等打包工具的话,是不行的);有时候我们需要在浏览器用一个模块,但他是CommonJS实现的,就比较难受了需要做一个兼容或者重写一下它。有时候我们这个模块可能既要运行在浏览器环境、又要运行在Node环境,这个时候可能需要写多份模块化规范的文件,其实就比较麻烦了。**通用的解决方案UMD是一个,另一个就是用ESmodule。**
  9. EcmaScript 2015 也就是我们常说的ES6之后,JS 有了 **语言层面 **的模块话导入导出关键词与语法以及与之配置的 ESModule 规范。使用ESModule规范我们可以通过 import export 两个关键词来对模块进行导入与导出。不同与其他的,现在大多数环境都还不支持es6的规范,所以我们需要对它进行编译。
  10. <a name="ZvWWw"></a>
  11. ### ESModule
  12. ESModule最大特点就是不是运行环境实现的,是语言层进行规定的。最大的好处就是兼容性非常的好。 <br />如果说Node.js需要支持ES6模块化规范,我们什么都不需要做,只需要升级我们的解析引擎就可以了。这个时候解析引擎就可以把这个语法识别出来,从而达到了模块化的意义。<br />浏览器里面通过升级V8引擎来达到实现模块化相关的内容。<br />引擎可以解释这个JS代码,那么这个运行环境就可以实现ESModule的导入导出。
  13. **ESModule就属于JS Core层面的规范,而AMDCommonJS是运行环境的规范**。所以,想要使运行环境支持ESModule其实是比较简单的,只需要升级自己环境中的JS Core解释引擎到足够的版本,引擎层面就能认识这种语法,从而不认为这是个 语法错误(syntax error)运行环境中只需要做一些兼容工作即可。
  14. 这也就是说,如果想在Node.js环境中使用ESModule,就需要升级Node.js到高版本,这相对来说比较容易,毕竟服务端Node.js版本控制在开发人员自己手里。但浏览器端具有分布式的特点,是否能使用这种高版本特性取决于用户访问时的版本,而且这种解释器语法层面的内容无法像AMD那样在运行时进行兼容,所以想要直接使用会比较麻烦。<br />所以最佳实践是,使用ESModule写的模块化,经过打包之后,处理成浏览器可以识别的模块。
  15. 通过前面的分析我们可以看出,使用ESModule的模块明显更符合JS开发的历史进程,因为任何一个支持JS的环境,随着对应解释器的升级,最终一定会支持ESModule的标准。但是,WEB端受制于用户使用的浏览器版本,我们并不能随心所欲的随时使用JS的最新特性。为了让我们的新代码也能运行在用户的老浏览器中,社区涌现了越来越多的工具,他们能静态将高版本规范的代码编译为低版本规范的代码,最为大家所熟知的就是babel。涉及到webpack等打包工具

const str = require('./moduleA'); const str = require('./moduleB'); console.log(str);;

const functionWrapper = [ ‘function(require, module, exports) {‘, ‘}’ ]; // 将我们的文件进行包裹,成为一个字符串函数 const result = functionWrapper[0] + str + functionWrapper[1]; const vm = require(‘vm’);

vm.runInNewContext

  1. 我们可以参考Node.js对于CommonJS模块的处理方式来处理一个CommonJS模块。在Node.js中,所有的CommonJS模块都会被包裹在一个函数中,然后在node.js中使用vm来运行它,最终达到一个模块化导入和导出的目的。<br />好比我们执行了 node index.js 执行的时候,node会通过文件系统读取index.js里面的内容,这时候是一个字符串,同时对这个字符串进行一个包裹。通过一个函数字符串的形式,将这个文件的内容包裹进去。把它变成了一个字符串的函数。

(function(modules) {

// 模拟 require 语句 function webpack_require() { }

// 执行存放所有模块数组中的第0个模块,然后根据依赖图依次往下找 webpack_require(0);

})([/存放所有模块的数组 依赖图/])

  1. 我们在浏览器中也可以用相同的思路进行处理:
  2. - 我们在打包阶段将每个模块包裹上一层函数字符串,然后放置到浏览器中去执行它。
  3. - 同时我们实现一个简单版本的require函数和module对象来处理运行时加载的问题,这样一个基本流程就好了。
  4. - 接下来我们要处理运行时模块之间的依赖关系,所以我们需要自己维护一个。
  5. <a name="FFFmG"></a>
  6. ### ES6Module和CommonJS差异
  7. - 静态化,编译时就能确定模块的依赖关系,以及输入和输出的变量,可以编译时做“静态优化”。CommonJS运行时确定这些东西,CommonJS 模块就是对象,输入时必须查找对象属性
  8. - CommonJS 模块输出的是值的缓存,CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用
  9. - CommonJS 模块的`require()`是同步加载模块,ES6 模块的`import`命令是异步加载,有一个独立的模块依赖的解析阶段
  10. <br />CommonJS 加载的是一个对象(即`module.exports`属性),该对象只有在脚本运行完才会生成<br />CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值
  11. ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令`import`,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。

export var foo = ‘bar’; setTimeout(() => foo = ‘baz’, 500);

上面代码输出变量foo,值为bar,500 毫秒之后变成baz。 export语句输出的接口,与其对应的值是动态绑定关系,即通过该接口,可以取到模块内部实时的值

  1. ```
  2. // m1.js
  3. export var foo = 'bar';
  4. setTimeout(() => foo = 'baz', 500);
  5. // m2.js
  6. import {foo} from './m1.js';
  7. console.log(foo);
  8. setTimeout(() => console.log(foo), 500);
  9. m1.js的变量foo,在刚加载时等于bar,过了 500 毫秒,又变为等于baz。
  10. 上面代码表明,ES6 模块不会缓存运行结果,而是动态地去被加载的模块取值,并且变量总是绑定其所在的模块。

注意点

  • 顶部thisundefined ,利用顶层的this等于undefined这个语法点,可以侦测当前代码是否在 ES6 模块之中。 const isNotModuleScript = this !== undefined;
  • ES6 的模块自动采用严格模式
  • ESM 异步加载 CJM 同步加载
  • ESM 编译的时候加载 CJM运行的时候
  • ESM 值的引用 CJM值的拷贝
  • ESM 不会从缓存中取值 CJM从缓存中取值

    动态加载

    通过import()来进行动态加载,按需加载,返回promise。加载模块成功以后,这个模块会作为一个对象,当作then方法的参数。因此,可以使用对象解构赋值的语法,获取输出接口。
    多个模块加载的时候,可以用promise.all await来实现 ``` export var foo = “bar”; export var name = “guanqingchao”;

import(“./test1”).then((data) => console.log(data));//{foo: “baz”, name: “guanqingchao”} ```

循环加载

CommonJS 模块遇到循环加载时,返回的是当前已经执行的部分的值,而不是代码全部执行后的值。
ES6 模块是动态引用那些变量不会被缓存,而是成为一个指向被加载模块的引用【接口】,需要开发者自己保证,真正取值的时候能够取到值。
循环引用示例

References

https://mp.weixin.qq.com/s/6CpzQXkZd1AcX51A3CNWgA