• Node.js 中使用 ES6 中的 import / export 的方法大全


    转自原文 Node.js 中使用 ES6 中的 import / export 的方法大全, 2018.11

    如何在 Node.js 中使用 import / export 的三种方法, 2018.8

    nodejs_es6_tutorials

    因为一些历史原因,虽然 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 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 是模块格式有esmcjsjsonbuiltinaddon这四种模块/文件格式.

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

  • 相关阅读:
    妙用sql的统计进行集合的比较
    kissy学习笔记(1)上手kissy
    Ext与现有项目的结合(一)完全封装已有的HTML元素使之成为Ext的组件
    Fix IE6 剪贴板撤销机制CtrlZ,CtrlY功能会在由于Js动态改变页面元素的value后失效的Problem
    Ajax个人开发心得(一)先从一个最简单的ajax功能模块说起,Ajax技术其实很简单
    【转】仅用 []()+! 就足以实现几乎任意Javascript代码
    为什么不用13px字体
    Ajax个人开发心得(二)页面逻辑与页面表现的分离,让你的Ajax编程更加模块化
    纯JS转换Select为Combox,完美实现原select所有功能
    关于window.parent的奇怪问题
  • 原文地址:https://www.cnblogs.com/arxive/p/12254008.html
Copyright © 2020-2023  润新知