Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Node 9下import/export的丝般顺滑使用 #24

Open
chenshenhai opened this issue Nov 17, 2017 · 7 comments
Open

Node 9下import/export的丝般顺滑使用 #24

chenshenhai opened this issue Nov 17, 2017 · 7 comments
Labels

Comments

@chenshenhai
Copy link
Owner

chenshenhai commented Nov 17, 2017

前言

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机制不一样

快速使用import/export

  • 新建mod-1.mjsmod-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()区别

能力 描述 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

如果觉得以下文字太长,可以先去玩玩对应的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这四种模块/文件格式.

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

@plh97
Copy link

plh97 commented Nov 17, 2017

我的天,各种url 绕来绕去

@ahuigo
Copy link

ahuigo commented Aug 18, 2018

  1. 最后那段通用loader.mjs 少了一个默认参数: const baseURL = new URL('file://'); parentModuleURL=baseURL;
  2. node 交互模式/chrome 交互模式还不支持 import, 如果支持了 调试会很方便,imort 推广起来更容易
  3. 希望node 能默认支持这个loader.mjs, 这样就能无通升级、无痛使用了

@zy445566
Copy link

👍

@kongling94
Copy link

我记得koa2现阶段是不支持import引入的又或者是在inexjs启动文件中不支持。

@ahuigo
Copy link

ahuigo commented Jun 29, 2019

我记得koa2现阶段是不支持import引入的又或者是在inexjs启动文件中不支持。

不是koa2 不支持import,而是你的node 需要开启这个支持才行。

@xiaohuaxiangwoyiyang
Copy link

这个运行之后不会报错吗
≥ node --experimental-modules --loader ./custom-loader.mjs server/index.js 14:45
(node:73405) ExperimentalWarning: --experimental-loader is an experimental feature. This feature could change at any time
(Use node --trace-warnings ... to show where the warning was created)
internal/process/esm_loader.js:74
internalBinding('errors').triggerUncaughtException(
^

TypeError [ERR_INVALID_URL]: Invalid URL: [object Object]
at onParseError (internal/url.js:257:9)
at new URL (internal/url.js:333:5)
at new URL (internal/url.js:330:22)
at resolve (file:///Users/huahua/Desktop/scripts/custom-loader.mjs:49:20)
at Loader.resolve (internal/modules/esm/loader.js:85:40)
at Loader.getModuleJob (internal/modules/esm/loader.js:229:28)
at Loader.import (internal/modules/esm/loader.js:164:28)
at internal/modules/run_main.js:46:28
at Object.loadESM (internal/process/esm_loader.js:68:11) {
input: '[object Object]',
code: 'ERR_INVALID_URL'

@chenshenhai
Copy link
Owner Author

@xiaohuaxiangwoyiyang 您好,这个文章是3年前写的,Node已经支持原生ES module使用了,不需要原来实验中的方法哈。
您提供的这个报错里就有这个提示哈

ExperimentalWarning: --experimental-loader is an experimental feature. This feature could change at any time

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

6 participants