前言

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
  • 文件的importexport必须严格按照ECMAScript Modules语法
  • ECMAScript Modulesrequire()的cache机制不一样

使用简述

Node 9.x官方文档 https://nodejs.org/dist/latest-v9.x/docs/api/esm.html

与require()区别

能力 描述 require() import
NODE_PATH 从NODE_PATH加载依赖模块 Y N
cache 缓存机制 可以通过require的API操作缓存 自己独立的缓存机制,目前不可访问
path 引用路径 文件路径 URL格式文件路径,例如import A from './a?v=2017'
extensions 扩展名机制 require.extensions Loader Hooks
natives 原生模块引用 直接支持 直接支持
npm npm模块引用 直接支持 需要Loader Hooks
file 文件(引用) *.js,*.json等直接支持 默认只能是*.mjs,通过Loader Hooks可以自定义配置规则支持*.js,*.json等Node原有支持文件

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

Koa2 直接使用import/export

看看demo4,https://github.com/chenshenhai/node-modules-demo/tree/master/demo4

  • 文件目录
  1. ├── esm
  2. ├── README.md
  3. ├── custom-loader.mjs
  4. ├── index.js
  5. ├── lib
  6. ├── data.json
  7. ├── path.js
  8. └── render.js
  9. ├── package.json
  10. └── view
  11. ├── index.html
  12. ├── index.html
  13. └── todo.html

代码片段太多,不一一贴出来,只显示主文件

  1. import Koa from 'koa';
  2. import { render } from './lib/render.js';
  3. import data from './lib/data.json';
  4. let app = new Koa();
  5. app.use((ctx, next) => {
  6. let view = ctx.url.substr(1);
  7. let content;
  8. if ( view === '' ) {
  9. content = render('index');
  10. } else if ( view === 'data' ) {
  11. content = data;
  12. } else {
  13. content = render(view);
  14. }
  15. ctx.body = content;
  16. })
  17. app.listen(3000, ()=>{
  18. console.log('the modules test server is starting');
  19. })

自定义loader规则优化

从上面官方提供的自定义loader例子看出,只是对*.js文件做import/export做loader兼容,然而我们在实际开发中需要对npm模块,*.json文件也使用import/export

loader规则优化解析

  1. import url from 'url';
  2. import path from 'path';
  3. import process from 'process';
  4. import fs from 'fs';
  5. // 从package.json中
  6. // 的dependencies、devDependencies获取项目所需npm模块信息
  7. const ROOT_PATH = process.cwd();
  8. const PKG_JSON_PATH = path.join( ROOT_PATH, 'package.json' );
  9. const PKG_JSON_STR = fs.readFileSync(PKG_JSON_PATH, 'binary');
  10. const PKG_JSON = JSON.parse(PKG_JSON_STR);
  11. // 项目所需npm模块信息
  12. const allDependencies = {
  13. ...PKG_JSON.dependencies || {},
  14. ...PKG_JSON.devDependencies || {}
  15. }
  16. //Node原生模信息
  17. const builtins = new Set(
  18. Object.keys(process.binding('natives')).filter((str) =>
  19. /^(?!(?:internal|node|v8)\/)/.test(str))
  20. );
  21. // 文件引用兼容后缀名
  22. const JS_EXTENSIONS = new Set(['.js', '.mjs']);
  23. const JSON_EXTENSIONS = new Set(['.json']);
  24. export function resolve(specifier, parentModuleURL, defaultResolve) {
  25. // 判断是否为Node原生模块
  26. if (builtins.has(specifier)) {
  27. return {
  28. url: specifier,
  29. format: 'builtin'
  30. };
  31. }
  32. // 判断是否为npm模块
  33. if ( allDependencies && typeof allDependencies[specifier] === 'string' ) {
  34. return defaultResolve(specifier, parentModuleURL);
  35. }
  36. // 如果是文件引用,判断是否路径格式正确
  37. if (/^\.{0,2}[/]/.test(specifier) !== true && !specifier.startsWith('file:')) {
  38. throw new Error(
  39. `imports must begin with '/', './', or '../'; '${specifier}' does not`);
  40. }
  41. // 判断是否为*.js、*.mjs、*.json文件
  42. const resolved = new url.URL(specifier, parentModuleURL);
  43. const ext = path.extname(resolved.pathname);
  44. if (!JS_EXTENSIONS.has(ext) && !JSON_EXTENSIONS.has(ext)) {
  45. throw new Error(
  46. `Cannot load file with non-JavaScript file extension ${ext}.`);
  47. }
  48. // 如果是*.js、*.mjs文件
  49. if (JS_EXTENSIONS.has(ext)) {
  50. return {
  51. url: resolved.href,
  52. format: 'esm'
  53. };
  54. }
  55. // 如果是*.json文件
  56. if (JSON_EXTENSIONS.has(ext)) {
  57. return {
  58. url: resolved.href,
  59. format: 'json'
  60. };
  61. }
  62. }

规则总结

在自定义loader中,export的resolve规则最核心的代码是

  1. return {
  2. url: '',
  3. format: ''
  4. }
  • url 是模块名称或者文件URL格式路径
  • format 是模块格式有esm, cjs, json, builtin, addon这四种模块/文件格式.

注意:
目前Node对import/export的支持现在还是Stability: 1 - Experimental阶段,后续的发展还有很多不确定因素,自己练手玩玩还可以,但是在还没去flag使用之前,尽量不要在生产环境中使用。Node 9.x 更详细import/export的使用,可参考 https://github.com/ChenShenhai/blog/issues/24