1
0
mirror of https://github.com/ruanyf/es6tutorial.git synced 2025-05-24 10:22:23 +00:00

docs(module-loader): edit Node 的 ES6 模块加载

This commit is contained in:
ruanyf 2017-09-10 13:27:04 +08:00
parent 4dbd9b0f48
commit f05dcbe4c5
2 changed files with 86 additions and 42 deletions

View File

@ -266,40 +266,76 @@ $ babel-node main.js
Node 对 ES6 模块的处理比较麻烦,因为它有自己的 CommonJS 模块格式,与 ES6 模块格式是不兼容的。目前的解决方案是将两者分开ES6 模块和 CommonJS 采用各自的加载方案。
在静态分析阶段,一个模块脚本只要有一行`import``export`语句Node 就会认为该脚本为 ES6 模块,否则就为 CommonJS 模块。如果不输出任何接口,但是希望被 Node 认为是 ES6 模块,可以在脚本中加一行语句
Node 要求 ES6 模块采用`.mjs`后缀文件名。也就是说,只要脚本文件里面使用`import`或者`export`命令,那么就必须采用`.mjs`后缀名。`require`命令不能加载`.mjs`文件,会报错,只有`import`命令才可以加载`.mjs`文件。反过来,`.mjs`文件里面也不能使用`require`命令,必须使用`import`
```javascript
export {};
目前,这项功能还在试验阶段。安装 Node 9.0 以上版本,要用`--experimental-modules`参数才能打开该功能。
```bash
$ node --experimental-modules my-app.mjs
```
上面的命令并不是输出一个空对象,而是不输出任何接口的 ES6 标准写法。
如果不指定绝对路径Node 加载 ES6 模块会依次寻找以下脚本,与`require()`的规则一致。
为了与浏览器的`import`加载规则相同Node 的`.mjs`文件支持 URL 路径。
```javascript
import './foo';
// 依次寻找
// ./foo.js
// ./foo/package.json
// ./foo/index.js
import './foo?query=1'; // 加载 ./foo 传入参数 ?query=1
```
上面代码中,脚本路径带有参数`?query=1`Node 会按 URL 规则解读。同一个脚本只要参数不同,就会被加载多次,并且保存成不同的缓存。由于这个原因,只要文件名中含有`:``%``#``?`等特殊字符,就必须转义,不如`foo#bar.js`不能写成`import './foo#bar'`,而要写成`import './foo\#bar'`
目前Node 的`import`命令只支持加载本地模块(`file:`协议),不支持加载远程模块。
如果模块名不含路径,那么`import`命令会去`node_modules`目录寻找这个模块。
```javascript
import 'baz';
// 依次寻找
// ./node_modules/baz.js
// ./node_modules/baz/package.json
// ./node_modules/baz/index.js
// 寻找上一级目录
// ../node_modules/baz.js
// ../node_modules/baz/package.json
// ../node_modules/baz/index.js
// 再上一级目录
import 'abc/123';
```
ES6 模块之中,顶层的`this`指向`undefined`CommonJS 模块的顶层`this`指向当前模块,这是两者的一个重大差异
如果模块名包含路径,那么`import`命令会按照路径去寻找这个名字的脚本文件。
### import 命令加载 CommonJS 模块
```javascript
import 'file:///etc/config/app.json';
import './foo';
import './foo?search';
import '../bar';
import '/baz';
```
Node 采用 CommonJS 模块格式,模块的输出都定义在`module.exports`这个属性上面。在 Node 环境中,使用`import`命令加载 CommonJS 模块Node 会自动将`module.exports`属性,当作模块的默认输出,即等同于`export default`
如果脚本文件省略了后缀名,比如`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`命令是异步加载,这一点与浏览器的处理方法相同。
### 内部变量
ES6 模块应该是通用的同一个模块不用修改就可以用在浏览器环境和服务器环境。为了达到这个目标Node 规定 ES6 模块之中不能使用 CommonJS 模块的特有的一些内部变量。
首先,就是`this`关键字。ES6 模块之中,顶层的`this`指向`undefined`CommonJS 模块的顶层`this`指向当前模块,这是两者的一个重大差异。
其次,以下这些顶层变量在 ES6 模块之中都是不存在的。
- `arguments`
- `require`
- `module`
- `exports`
- `__filename`
- `__dirname`
如果你一定要使用这些变量,有一个变通方法,就是写一个 CommonJS 模块输出这些变量,然后再用 ES6 模块加载这个 CommonJS 模块。但是这样一来,该 ES6 模块就不能直接用于浏览器环境了,所以不推荐这样做。
```javascript
// expose.js
module.exports = {__dirname};
// use.mjs
import expose from './expose.js';
const {__dirname} = expose;
```
上面代码中,`expose.js`是一个 CommonJS 模块,输出变量`__dirname`,该变量在 ES6 模块之中不存在。ES6 模块加载`expose.js`,就可以得到`__dirname`
### ES6 模块加载 CommonJS 模块
CommonJS 模块的输出都定义在`module.exports`这个属性上面。Node 的`import`命令加载 CommonJS 模块Node 会自动将`module.exports`属性,当作模块的默认输出,即等同于`export default xxx`
下面是一个 CommonJS 模块。
@ -317,7 +353,9 @@ export default {
};
```
`import`命令加载上面的模块,`module.exports`会被视为默认输出。
`import`命令加载上面的模块,`module.exports`会被视为默认输出,即`import`命令实际上输入的是这样一个对象`{ default: module.exports }`
所以,一共有三种写法,可以拿到 CommonJS 模块的`module.exports`
```javascript
// 写法一
@ -327,11 +365,8 @@ import baz from './a';
// 写法二
import {default as baz} from './a';
// baz = {foo: 'hello', bar: 'world'};
```
如果采用整体输入的写法(`import * as xxx from someModule``default`会取代`module.exports`,作为输入的接口。
```javascript
// 写法三
import * as baz from './a';
// baz = {
// get default() {return module.exports;},
@ -340,7 +375,9 @@ import * as baz from './a';
// }
```
上面代码中,`this.default`取代了`module.exports`。需要注意的是Node 会自动为`baz`添加`default`属性,通过`baz.default`拿到`module.exports`
上面代码的第三种写法,可以通过`baz.default`拿到`module.exports``foo`属性和`bar`属性就是可以通过这种方法拿到了`module.exports`
下面是一些例子。
```javascript
// b.js
@ -351,13 +388,11 @@ import foo from './b';
// foo = null;
import * as bar from './b';
// bar = {default:null};
// bar = { default:null };
```
上面代码中,`es.js`采用第二种写法时,要通过`bar.default`这样的写法,才能拿到`module.exports`
下面是另一个例子。
```javascript
// c.js
module.exports = function two() {
@ -388,33 +423,41 @@ setTimeout(_ => module.exports = null);
由于 ES6 模块是编译时确定输出接口CommonJS 模块是运行时确定输出接口,所以采用`import`命令加载 CommonJS 模块时,不允许采用下面的写法。
```javascript
import {readfile} from 'fs';
// 不正确
import { readfile } from 'fs';
```
上面的写法不正确,因为`fs`是 CommonJS 格式,只有在运行时才能确定`readfile`接口,而`import`命令要求编译时就确定这个接口。解决方法就是改为整体输入。
```javascript
// 正确的写法一
import * as express from 'express';
const app = express.default();
// 正确的写法二
import express from 'express';
const app = express();
```
### require 命令加载 ES6 模块
### CommonJS 模块加载 ES6 模块
采用`require`命令加载 ES6 模块时,ES6 模块的所有输出接口,会成为输入对象的属性。
CommonJS 模块加载 ES6 模块,不能使用`require`命令,而要使用`import()`函数。ES6 模块的所有输出接口,会成为输入对象的属性。
```javascript
// es.js
let foo = {bar:'my-default'};
// es.mjs
let foo = { bar:'my-default' };
export default foo;
foo = null;
// cjs.js
const es_namespace = require('./es');
const es_namespace = await import('./es');
// es_namespace = {
// get default() {
// ...
// }
// }
console.log(es_namespace.default);
// {bar:'my-default'}
// { bar:'my-default' }
```
上面代码中,`default`接口变成了`es_namespace.default`属性。另外,由于存在缓存机制,`es.js``foo`的重新赋值没有在模块外部反映出来。
@ -423,13 +466,13 @@ console.log(es_namespace.default);
```javascript
// es.js
export let foo = {bar:'my-default'};
export {foo as bar};
export let foo = { bar:'my-default' };
export { foo as bar };
export function f() {};
export class c {};
// cjs.js
const es_namespace = require('./es');
const es_namespace = await import('./es');
// es_namespace = {
// get foo() {return foo;}
// get bar() {return foo;}

View File

@ -215,6 +215,7 @@
- Bradley Meck, [ES6 Module Interoperability](https://github.com/nodejs/node-eps/blob/master/002-es6-modules.md): 介绍 Node 如何处理 ES6 语法加载 CommonJS 模块
- Axel Rauschmayer, [Making transpiled ES modules more spec-compliant](http://www.2ality.com/2017/01/babel-esm-spec-mode.html): ES6 模块编译成 CommonJS 模块的详细介绍
- Axel Rauschmayer, [ES proposal: import() dynamically importing ES modules](http://www.2ality.com/2017/01/import-operator.html): import() 的用法
- Node EPS, [ES Module Interoperability](https://github.com/nodejs/node-eps/blob/master/002-es-modules.md): Node 对 ES6 模块的处理规格
## 二进制数组