🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
### 1. 加载规则 浏览器加载 ES6 模块,也使用`<script>`标签,但是要加入`type="module"`属性。 ~~~ <script type="module" src="./foo.js"></script> ~~~ 上面代码在网页中插入一个模块`foo.js`,由于`type`属性设为`module`,所以浏览器知道这是一个 ES6 模块。 浏览器对于带有`type="module"`的`<script>`,都是异步加载,不会造成堵塞浏览器,即等到整个页面渲染完,再执行模块脚本,等同于打开了`<script>`标签的`defer`属性。 ~~~ <script type="module" src="./foo.js"></script> <!-- 等同于 --> <script type="module" src="./foo.js" defer></script> ~~~ 如果网页有多个`<script type="module">`,它们会按照在页面出现的顺序依次执行。 `<script>`标签的`async`属性也可以打开,这时只要加载完成,渲染引擎就会中断渲染立即执行。执行完成后,再恢复渲染。 ~~~ <script type="module" src="./foo.js" async></script> ~~~ 一旦使用了`async`属性,`<script type="module">`就不会按照在页面出现的顺序执行,而是只要该模块加载完成,就执行该模块。 ES6 模块也允许内嵌在网页中,语法行为与加载外部脚本完全一致。 ~~~ <script type="module"> import utils from "./utils.js"; // other code </script> ~~~ 举例来说,jQuery 就支持模块加载。 ~~~ <script type="module"> import $ from "./jquery/src/jquery.js"; $('#message').text('Hi from jQuery!'); </script> ~~~ * 模块之中,顶层的`this`关键字返回`undefined`,而不是指向`window`。也就是说,在模块顶层使用`this`关键字,是无意义的。 ### 2. ES6模块与CommonJS中模块的差异 它们有两个重大差异。 * CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。 * CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。 第二个差异是因为 CommonJS 加载的是一个对象(即`module.exports`属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。 下面重点解释第一个差异。 CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。请看下面这个模块文件`lib.js`的例子。 ~~~javascript // lib.js var counter = 3; function incCounter() { counter++; } module.exports = { counter: counter, incCounter: incCounter, }; ~~~ 上面代码输出内部变量`counter`和改写这个变量的内部方法`incCounter`。然后,在`main.js`里面加载这个模块。 ~~~javascript // main.js var mod = require('./lib'); console.log(mod.counter); // 3 mod.incCounter(); console.log(mod.counter); // 3 ~~~ 上面代码说明,`lib.js`模块加载以后,它的内部变化就影响不到输出的`mod.counter`了。这是因为`mod.counter`是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值。 ~~~javascript // lib.js var counter = 3; function incCounter() { counter++; } module.exports = { get counter() { return counter }, incCounter: incCounter, }; ~~~ 上面代码中,输出的`counter`属性实际上是一个取值器函数。现在再执行`main.js`,就可以正确读取内部变量`counter`的变动了。 ~~~bash $ node main.js 3 4 ~~~ ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令`import`,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的`import`有点像 Unix 系统的“符号连接”,原始值变了,`import`加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。 还是举上面的例子。 ~~~javascript // lib.js export let counter = 3; export function incCounter() { counter++; } // main.js import { counter, incCounter } from './lib'; console.log(counter); // 3 incCounter(); console.log(counter); // 4 ~~~ 上面代码说明,ES6 模块输入的变量`counter`是活的,完全反应其所在模块`lib.js`内部的变化。 ### 3. Node加载 Node 对 ES6 模块的处理比较麻烦,因为它有自己的 CommonJS 模块格式,与 ES6 模块格式是不兼容的。目前的解决方案是,将两者分开,ES6 模块和 CommonJS 采用各自的加载方案。 Node 要求 ES6 模块采用`.mjs`后缀文件名。也就是说,只要脚本文件里面使用`import`或者`export`命令,那么就必须采用`.mjs`后缀名。`require`命令不能加载`.mjs`文件,会报错,只有`import`命令才可以加载`.mjs`文件。反过来,`.mjs`文件里面也不能使用`require`命令,必须使用`import`。 目前,这项功能还在试验阶段。安装 Node v8.5.0 或以上版本,要用`--experimental-modules`参数才能打开该功能。 ~~~bash $ node --experimental-modules my-app.mjs ~~~ 为了与浏览器的`import`加载规则相同,Node 的`.mjs`文件支持 URL 路径。 ~~~javascript import './foo?query=1'; // 加载 ./foo 传入参数 ?query=1 ~~~ 上面代码中,脚本路径带有参数`?query=1`,Node 会按 URL 规则解读。同一个脚本只要参数不同,就会被加载多次,并且保存成不同的缓存。由于这个原因,只要文件名中含有`:`、`%`、`#`、`?`等特殊字符,最好对这些字符进行转义。 目前,Node 的`import`命令只支持加载本地模块(`file:`协议),不支持加载远程模块。 如果模块名不含路径,那么`import`命令会去`node_modules`目录寻找这个模块。 ~~~javascript import 'baz'; import 'abc/123'; ~~~ 如果模块名包含路径,那么`import`命令会按照路径去寻找这个名字的脚本文件。 ~~~javascript import 'file:///etc/config/app.json'; import './foo'; import './foo?search'; import '../bar'; import '/baz'; ~~~ 如果脚本文件省略了后缀名,比如`import './foo'`,Node 会依次尝试四个后缀名:`./foo.mjs`、`./foo.js`、`./foo.json`、`./foo.node`。如果这些脚本文件都不存在,Node 就会去加载`./foo/package.json`的`main`字段指定的脚本。如果`./foo/package.json`不存在或者没有`main`字段,那么就会依次加载`./foo/index.mjs`、`./foo/index.js`、`./foo/index.json`、`./foo/index.node`。如果以上四个文件还是都不存在,就会抛出错误。 最后,Node 的`import`命令是异步加载,这一点与浏览器的处理方法相同。