转自原文 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使用之前,尽量不要在生产环境中使用。