轉自原文 Node.js 中使用 ES6 中的 import / export 的方法大全, 2018.11
如何在 Node.js 中使用 import / export 的三種方法, 2018.8
因為一些歷史原因,雖然 Node.js 已經實現了 99% 的 ES6 新特性,不過截止 2018.8.10,How To Enable ES6 Imports in Node.JS 仍然是老大難問題
下面我來介紹三種方法可以讓我們在 Node.js 中使用 import/export 。
一、三個方案
方案1 放棄用 ES6, 使用 Node中的 module 模塊語法
util_for_node.js
function log(o) {
console.log(o);
}
module.exports = log;
es6_const_let_node_demo.js
// 在 Node 中使用模塊的正確姿勢:
const log = require("./lib/util_for_node");
// ES5
var a = 1;
a = a + 1;
log(a); // 2
// ES6
const b = 1;
// b = b + 1; // error : TypeError: Assignment to constant variable.
log(b);
// ES6
let c = 1;
c = c + 1;
log(c);
測試
$ node es6_const_let_node_demo.js 2 1 2
方案2 使用萬能變換器:babel (不推薦)
util_for_babel.js
function log(o) {
console.log(o);
}
export {log}
es6_const_let_babel_demo.js
import {log} from "./lib/util_for_babel";
/**
node: module.exports和require
es6:export和import
nodejs仍未支持import/export語法,需要安裝必要的npm包–babel,使用babel將js文件編譯成node.js支持的commonjs格式的代碼。
因為一些歷史原因,雖然 Node.js 已經實現了 99% 的 ES6 新特性,不過截止 2018.8.10,How To Enable ES6 Imports in Node.JS 仍然是老大難問題
借助 Babel
1.下載必須的包
npm install babel-register babel-preset-env --D
命令行執行:
babel-node es6_const_let_babel_demo.js
*
* @type {number}
*/
// ES5
var a = 1;
a = a + 1;
log(a); // 2
// ES6
const b = 1;
// b = b + 1; // error : TypeError: Assignment to constant variable.
log(b);
// ES6
let c = 1;
c = c + 1;
log(c);
上面的代碼,直接 node 命令行運行是要報錯的:
$ node es6_const_let_babel_demo.js
/Users/jack/WebstormProject/node-tutorials/hello-node/es6_const_let_babel_demo.js:1
(function (exports, require, module, __filename, __dirname) { import {log} from "./lib/util_for_babel";
^
SyntaxError: Unexpected token {
at new Script (vm.js:79:7)
at createScript (vm.js:251:10)
at Object.runInThisContext (vm.js:303:10)
at Module._compile (internal/modules/cjs/loader.js:656:28)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:699:10)
at Module.load (internal/modules/cjs/loader.js:598:32)
at tryModuleLoad (internal/modules/cjs/loader.js:537:12)
at Function.Module._load (internal/modules/cjs/loader.js:529:3)
at Function.Module.runMain (internal/modules/cjs/loader.js:741:12)
at startup (internal/bootstrap/node.js:285:19)
是的,這個時候,我們需要再加上一層 Babel 的映射邏輯。下面就是 Babel 出場了。
(1)安裝依賴
npm install babel-register babel-preset-env --D
添加文件 package.json(別問我添加哪兒,這點兒還沒有弄清楚,原始文章說的不詳細)
{
"name": "hell-node",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
"babel-preset-env": "^1.7.0",
"babel-register": "^6.26.0"
}
}
(2)寫處理啟動腳本
es6_const_let_babel_demo_start.js
require('babel-register') ({
presets: [ 'env' ]
})
module.exports = require('./es6_const_let_babel_demo.js')
OK,多費了這么多事,終於可以跑了。
$ node es6_const_let_babel_demo_start.js 2 1 2
方案3 使用 Node 中的實驗特性(node --experimental-modules | 推薦)
為了特意區分這是module JavaScript,文件后綴名必須改成 .mjs。
util_for_node_exp.mjs
/**
* 注意到這里的源碼文件的后綴 .mjs
* @param o
*/
function log(o) {
console.log(o);
}
export {log};
es6_const_let_node_exp_demo.mjs
import {log} from "./lib/util_for_node_exp";
// ES5
var a = 1;
a = a + 1;
log(a); // 2
// ES6
const b = 1;
// b = b + 1; // error : TypeError: Assignment to constant variable.
log(b);
// ES6
let c = 1;
c = c + 1;
log(c);
/**
* 源碼后綴 .mjs
*/
(node:1402) ExperimentalWarning: The ESM module loader is experimental.
2
1
2
二、其他
2.1 Node 9下import/export的絲般順滑使用
Node 9最激動人心的是提供了在flag模式下使用ECMAScript Modules,雖然現在還是Stability: 1 - Experimental階段,但是可以讓Noder拋掉babel等工具的束縛,直接在Node環境下愉快地去玩耍import/export
如果覺得文字太多,看不下去,可以直接去玩玩demo,地址是
https://github.com/chenshenhai/node-modules-demo
Node 9下import/export使用簡單須知
Node 環境必須在 9.0以上
不加loader時候,使用import/export的文件后綴名必須為*.mjs(下面會講利用Loader Hooks兼容*.js后綴文件)
啟動必須加上flag --experimental-modules
文件的import和export必須嚴格按照ECMAScript Modules語法
ECMAScript Modules和require()的cache機制不一樣
快速使用import/export
新建mod-1.mjs,mod-2.mjs文件
/* ./mod-1.mjs */
export default {
num: 0,
increase() {
this.num++;
},
decrease() {
this.num--;
}
}
/* ./mod-2.mjs */
import Mod1 from './mod-1';
export default {
increase() {
Mod1.increase();
},
decrease() {
Mod1.decrease();
}
}
建立啟動文件 index.mjs
import Mod1 from './mod-1';
import Mod2 from './mod-2';
console.log(`Mod1.num = ${Mod1.num}`)
Mod1.increase();
console.log(`Mod1.num = ${Mod1.num}`)
Mod2.increase();
console.log(`Mod1.num = ${Mod1.num}`)
執行代碼
node --experimental-modules ./index.mjs
使用簡述
執行了上述demo后,快速體驗了Node的原生import/export能力,那我們來講講目前的支持狀況,Node 9.x官方文檔 https://nodejs.org/dist/latest-v9.x/docs/api/esm.html
與require()區別

2.2 Loader Hooks模式使用
由於歷史原因,在ES6的Modules還沒確定之前,JavaScript的模塊化處理方案都是八仙過海,各顯神通,例如前端的AMD、CMD模塊方案,Node的CommonJS方案也在這個“亂世”誕生。
當到了ES6規范確定后,Node的CommonJS方案已經是JavaScript中比較成熟的模塊化方案,但ES6怎么說都是正統的規范,“法理”上是需要兼容的,所以*.mjs這個針對ECMAScript Modules規范的Node文件方案在一片討論聲中應運而生。
當然如果import/export只能對*.mjs文件起作用,意味着Node原生模塊和npm所有第三方模塊都不能。所以這時候Node 9就提供了 Loader Hooks,開發者可自定義配置Resolve Hook規則去利用import/export加載使用Node原生模塊,*.js文件,npm模塊,C/C++的Node編譯模塊等Node生態圈的模塊。
Loader Hooks 使用步驟
自定義loader規則
啟動的flag要加載loader規則文件
例如:node --experimental-modules --loader ./custom-loader.mjs ./index.js
如果覺得以下文字太長,可以先去玩玩對應的demo3 https://github.com/chenshenhai/node-modules-demo/tree/master/demo3
自定義規則快速上手
文件目錄
├── demo3 │ ├── es │ │ ├── custom-loader.mjs │ │ ├── index.js │ │ ├── mod-1.js │ │ └── mod-2.js │ └── package.json
加載自定義loader,執行import/export的*.js文件
node --experimental-modules --loader ./es/custom-loader.mjs ./es/index.js
自定義loader規則解析
以下是Node 9.2官方文檔提供的一個自定義loader文件
import url from 'url';
import path from 'path';
import process from 'process';
// 獲取所有Node原生模塊名稱
const builtins = new Set(
Object.keys(process.binding('natives')).filter((str) =>
/^(?!(?:internal|node|v8)\/)/.test(str))
);
// 配置import/export兼容的文件后綴名
const JS_EXTENSIONS = new Set(['.js', '.mjs']);
// flag執行的resolve規則
export function resolve(specifier, parentModuleURL /*, defaultResolve */) {
// 判斷是否為Node原生模塊
if (builtins.has(specifier)) {
return {
url: specifier,
format: 'builtin'
};
}
// 判斷是否為*.js, *.mjs文件
// 如果不是則,拋出錯誤
if (/^\.{0,2}[/]/.test(specifier) !== true && !specifier.startsWith('file:')) {
// For node_modules support:
// return defaultResolve(specifier, parentModuleURL);
throw new Error(
`imports must begin with '/', './', or '../'; '${specifier}' does not`);
}
const resolved = new url.URL(specifier, parentModuleURL);
const ext = path.extname(resolved.pathname);
if (!JS_EXTENSIONS.has(ext)) {
throw new Error(
`Cannot load file with non-JavaScript file extension ${ext}.`);
}
// 如果是*.js, *.mjs文件,封裝成ES6 Modules格式
return {
url: resolved.href,
format: 'esm'
};
}
規則總結
在自定義loader中,export的resolve規則最核心的代碼是
return {
url: '',
format: ''
}
- url 是模塊名稱或者文件URL格式路徑
- format 是模塊格式有
esm,cjs,json,builtin,addon這四種模塊/文件格式.
2.3 Koa2 直接使用import/export
看看demo4,https://github.com/chenshenhai/node-modules-demo/tree/master/demo4
文件目錄
├── demo4 │ ├── README.md │ ├── custom-loader.mjs │ ├── index.js │ ├── lib │ │ ├── data.json │ │ ├── path.js │ │ └── render.js │ ├── package-lock.json │ ├── package.json │ └── view │ ├── index.html │ └── todo.html
代碼片段太多,不一一貼出來,只顯示主文件
import Koa from 'koa';
import { render } from './lib/render.js';
import data from './lib/data.json';
let app = new Koa();
app.use((ctx, next) => {
let view = ctx.url.substr(1);
let content;
if ( view === 'data' ) {
content = data;
} else {
content = render(view);
}
ctx.body = content;
})
app.listen(3000, ()=>{
console.log('the modules test server is starting');
});
執行代碼
node --experimental-modules --loader ./custom-loader.mjs ./index.js
自定義loader規則優化
從上面官方提供的自定義loader例子看出,只是對*.js文件做import/export做loader兼容,然而我們在實際開發中需要對npm模塊,*.json文件也使用import/export
loader規則優化解析
import url from 'url';
import path from 'path';
import process from 'process';
import fs from 'fs';
// 從package.json中
// 的dependencies、devDependencies獲取項目所需npm模塊信息
const ROOT_PATH = process.cwd();
const PKG_JSON_PATH = path.join( ROOT_PATH, 'package.json' );
const PKG_JSON_STR = fs.readFileSync(PKG_JSON_PATH, 'binary');
const PKG_JSON = JSON.parse(PKG_JSON_STR);
// 項目所需npm模塊信息
const allDependencies = {
...PKG_JSON.dependencies || {},
...PKG_JSON.devDependencies || {}
}
//Node原生模信息
const builtins = new Set(
Object.keys(process.binding('natives')).filter((str) =>
/^(?!(?:internal|node|v8)\/)/.test(str))
);
// 文件引用兼容后綴名
const JS_EXTENSIONS = new Set(['.js', '.mjs']);
const JSON_EXTENSIONS = new Set(['.json']);
export function resolve(specifier, parentModuleURL, defaultResolve) {
// 判斷是否為Node原生模塊
if (builtins.has(specifier)) {
return {
url: specifier,
format: 'builtin'
};
}
// 判斷是否為npm模塊
if ( allDependencies && typeof allDependencies[specifier] === 'string' ) {
return defaultResolve(specifier, parentModuleURL);
}
// 如果是文件引用,判斷是否路徑格式正確
if (/^\.{0,2}[/]/.test(specifier) !== true && !specifier.startsWith('file:')) {
throw new Error(
`imports must begin with '/', './', or '../'; '${specifier}' does not`);
}
// 判斷是否為*.js、*.mjs、*.json文件
const resolved = new url.URL(specifier, parentModuleURL);
const ext = path.extname(resolved.pathname);
if (!JS_EXTENSIONS.has(ext) && !JSON_EXTENSIONS.has(ext)) {
throw new Error(
`Cannot load file with non-JavaScript file extension ${ext}.`);
}
// 如果是*.js、*.mjs文件
if (JS_EXTENSIONS.has(ext)) {
return {
url: resolved.href,
format: 'esm'
};
}
// 如果是*.json文件
if (JSON_EXTENSIONS.has(ext)) {
return {
url: resolved.href,
format: 'json'
};
}
}
三、說明
目前Node對import/export的支持現在還是Stability: 1 - Experimental階段,后續的發展還有很多不確定因素,自己練手玩玩還可以,但是在還沒去flag使用之前,盡量不要在生產環境中使用。
