chenshenhai / blog

个人博客,没事写写玩玩~~~
145 stars 21 forks source link

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

Open chenshenhai opened 6 years ago

chenshenhai commented 6 years ago

前言

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使用简单须知

快速使用import/export

/* ./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();
    }
}

console.log(Mod1.num = ${Mod1.num}) Mod1.increase(); console.log(Mod1.num = ${Mod1.num}) Mod2.increase(); console.log(Mod1.num = ${Mod1.num})


- 执行代码

```sh
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 使用步骤

如果觉得以下文字太长,可以先去玩玩对应的demo3 https://github.com/chenshenhai/node-modules-demo/tree/master/demo3

自定义规则快速上手

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: ''
}

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');
});

自定义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 commented 6 years ago

我的天,各种url 绕来绕去

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

👍

kongling94 commented 5 years ago

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

ahuigo commented 5 years ago

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

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

xiaohuaxiangwoyiyang commented 3 years ago

这个运行之后不会报错吗 ≥ 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 commented 3 years ago

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

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