雷布斯他爹 發表於 2020-2-2 22:13:00

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

<p style="text-align: right">转自原文&nbsp;Node.js 中使用 ES6 中的 import / export 的方法大全, 2018.11</p>
<div class="article-title-box">
<p class="title-article" style="text-align: right">如何在 Node.js 中使用 import / export 的三种方法, 2018.8</p>
<p class="title-article" style="text-align: right">nodejs_es6_tutorials</p>
<p>因为一些历史原因,虽然 Node.js 已经实现了 99% 的 ES6 新特性,不过截止 2018.8.10,How To Enable ES6 Imports in Node.JS 仍然是老大难问题</p>
<p>下面我来介绍三种方法可以让我们在 Node.js 中使用 import/export 。</p>
<h1>一、三个方案</h1>
<h2>方案1 放弃用 ES6, 使用 Node中的 module 模块语法</h2>
<p>util_for_node.js</p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">function log(o) {
    console.log(o);
}

module.exports = log;
</pre>
</div>
<p>es6_const_let_node_demo.js</p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">// 在 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);</pre>
</div>
<p>测试</p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">$ node es6_const_let_node_demo.js
2
1
2
</pre>
</div>
<p>&nbsp;</p>
<h2>方案2&nbsp;&nbsp;使用万能变换器:babel <span style="background-color: rgba(153, 153, 153, 1); color: rgba(0, 255, 255, 1)">(不推荐)</span></h2>
<p>util_for_babel.js</p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">function log(o) {
    console.log(o);
}

export {log}
</pre>
</div>
<p>es6_const_let_babel_demo.js</p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">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);

</pre>
</div>
<p>上面的代码,直接 node 命令行运行是要报错的:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">$ 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:<span style="color: rgba(255, 0, 0, 1)"><strong> Unexpected token</strong></span> {
    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)

</pre>
</div>
<p>是的,这个时候,我们需要再加上一层 Babel 的映射逻辑。下面就是 Babel 出场了。</p>
<p>(1)安装依赖</p>
<p>npm <span class="hljs-keyword">install babel-<span class="hljs-keyword">register babel-preset-env <span class="hljs-comment">--D</span></span></span></p>
<p><span class="hljs-keyword">添加文件 package.json(别问我添加哪儿,这点儿还没有弄清楚,原始文章说的不详细)</span></p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">{
"name": "hell-node",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
    "test": "echo \"Error: no test specified\" &amp;&amp; exit 1"
},
"author": "",
"license": "ISC",
"devDependencies": {
    "babel-preset-env": "^1.7.0",
    "babel-register": "^6.26.0"
}
}
</pre>
</div>
<p>&nbsp;</p>
<p>(2)写处理启动脚本</p>
<p>es6_const_let_babel_demo_start.js</p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">require('babel-register') ({
    presets: [ 'env' ]
})

module.exports = require('./es6_const_let_babel_demo.js')
</pre>
</div>
<p>OK,多费了这么多事,终于可以跑了。</p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">$ node es6_const_let_babel_demo_start.js
2
1
2</pre>
</div>
<p>&nbsp;</p>
<h2>方案3&nbsp;&nbsp;使用 Node 中的实验特性(node --experimental-modules <span style="background-color: rgba(153, 153, 153, 1); color: rgba(0, 255, 255, 1)">| 推荐</span>)</h2>
<p>为了特意区分这是module JavaScript,文件后缀名必须改成 <span style="background-color: rgba(255, 255, 153, 1)">.mjs</span>。</p>
<p>util_for_node_exp.mjs</p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">/**
* 注意到这里的源码文件的后缀 .mjs
* @param o
*/

function log(o) {
    console.log(o);
}

export {log};
</pre>
</div>
<p>es6_const_let_node_exp_demo.mjs</p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">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
*/

</pre>
</div>
<p>&nbsp;</p>
</div>
<div class="article-info-box">&nbsp;node --<span class="hljs-keyword">experimental-modules es6_const_let_node_exp_demo.mjs</span></div>
<div class="article-info-box">输出</div>
<div class="article-info-box">
<div class="cnblogs_code">
<pre> (node:1402<span style="color: rgba(0, 0, 0, 1)">) ExperimentalWarning: The ESM module loader is experimental.
</span>2
1
2</pre>
</div>
<p>&nbsp;</p>
</div>
<h1 class="article-info-box">二、其他</h1>
<h2 class="article-info-box"><strong>2.1 Node 9下import/export的丝般顺滑使用</strong></h2>
<p>Node 9最激动人心的是提供了在flag模式下使用ECMAScript Modules,虽然现在还是Stability: 1 - Experimental阶段,但是可以让Noder抛掉babel等工具的束缚,直接在Node环境下愉快地去玩耍import/export</p>
<p>如果觉得文字太多,看不下去,可以直接去玩玩demo,地址是</p>
<p>https://github.com/chenshenhai/node-modules-demo</p>
<p>&nbsp;</p>
<p>Node 9下import/export使用简单须知<br>Node 环境必须在 9.0以上<br>不加loader时候,使用import/export的文件后缀名必须为*.mjs(下面会讲利用Loader Hooks兼容*.js后缀文件)<br>启动必须加上flag --experimental-modules<br>文件的import和export必须严格按照ECMAScript Modules语法<br>ECMAScript Modules和require()的cache机制不一样</p>
<p><br><strong>快速使用import/export</strong><br>新建mod-1.mjs,mod-2.mjs文件</p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">/* ./mod-1.mjs */
export default {
    num: 0,
    increase() {
      this.num++;
    },
    decrease() {
      this.num--;
    }
}
</pre>
</div>
<p>&nbsp;</p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">/*./mod-2.mjs */
import Mod1 from './mod-1';

export default {
    increase() {
      Mod1.increase();
    },
    decrease() {
      Mod1.decrease();
    }
}
</pre>
</div>
<p>建立启动文件&nbsp;<code>index.mjs</code></p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">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}`)
</pre>
</div>
<p>&nbsp;</p>
<p>执行代码</p>
<p><em id="__mceDel" style="font-family: &quot;Courier New&quot;; font-size: 12px"><code class="source-shell hljs delphi">node --<span class="hljs-keyword">experimental-modules ./<span class="hljs-keyword">index.mjs</span></span></code></em></p>
<p><strong>使用简述</strong><br>执行了上述demo后,快速体验了Node的原生import/export能力,那我们来讲讲目前的支持状况,Node 9.x官方文档 https://nodejs.org/dist/latest-v9.x/docs/api/esm.html</p>
<p><strong>与require()区别</strong></p>
<p><strong><img src="https://img2018.cnblogs.com/i-beta/527375/202002/527375-20200202220745052-1709660796.png" alt="" width="696" height="284"></strong></p>
<p>&nbsp;</p>
<p>&nbsp;</p>
<h2>2.2 Loader Hooks模式使用</h2>
<p>由于历史原因,在ES6的Modules还没确定之前,JavaScript的模块化处理方案都是八仙过海,各显神通,例如前端的AMD、CMD模块方案,Node的CommonJS方案也在这个“乱世”诞生。<br>当到了ES6规范确定后,Node的CommonJS方案已经是JavaScript中比较成熟的模块化方案,但ES6怎么说都是正统的规范,“法理”上是需要兼容的,所以*.mjs这个针对ECMAScript Modules规范的Node文件方案在一片讨论声中应运而生。</p>
<p>当然如果import/export只能对*.mjs文件起作用,意味着Node原生模块和npm所有第三方模块都不能。所以这时候Node 9就提供了 Loader Hooks,开发者可自定义配置Resolve Hook规则去利用import/export加载使用Node原生模块,*.js文件,npm模块,C/C++的Node编译模块等Node生态圈的模块。</p>
<p>&nbsp;</p>
<h3><strong>Loader Hooks 使用步骤</strong></h3>
<p>自定义loader规则<br>启动的flag要加载loader规则文件<br>例如:node --experimental-modules --loader ./custom-loader.mjs ./index.js<br>如果觉得以下文字太长,可以先去玩玩对应的demo3 https://github.com/chenshenhai/node-modules-demo/tree/master/demo3<br><br></p>
<h3>自定义规则快速上手</h3>
<p>文件目录</p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">├── demo3
│   ├── es
│   │   ├── custom-loader.mjs
│   │   ├── index.js
│   │   ├── mod-1.js
│   │   └── mod-2.js
│   └── package.json

</pre>
</div>
<p>加载自定义loader,执行<code>import/export</code>的<code>*.js</code>文件</p>
<p>node --<span class="hljs-keyword">experimental-modules --loader ./es/custom-loader.mjs ./es/<span class="hljs-keyword">index.js</span></span></p>
<h3>自定义loader规则解析</h3>
<p>以下是Node 9.2官方文档提供的一个自定义loader文件</p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">import url from 'url';
import path from 'path';
import process from 'process';

// 获取所有Node原生模块名称
const builtins = new Set(
Object.keys(process.binding('natives')).filter((str) =&gt;
    /^(?!(?: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 &amp;&amp; !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'
};
}</pre>
</div>
<h3>规则总结</h3>
<p>&nbsp;在自定义loader中,export的resolve规则最核心的代码是</p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">return {
url: '',
format: ''
}
</pre>
</div>
<ul>
<li>url 是模块名称或者文件URL格式路径</li>
<li>format 是模块格式有<code>esm</code>,&nbsp;<code>cjs</code>,&nbsp;<code>json</code>,&nbsp;<code>builtin</code>,&nbsp;<code>addon</code>这四种模块/文件格式.</li>
</ul>
<p>&nbsp;</p>
<h2>2.3 Koa2 直接使用import/export</h2>
<p>看看demo4,https://github.com/chenshenhai/node-modules-demo/tree/master/demo4</p>
<p>文件目录</p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">├── 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
</pre>
</div>
<p>代码片段太多,不一一贴出来,只显示主文件</p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">import Koa from 'koa';
import { render } from './lib/render.js';
import data from './lib/data.json';

let app = new Koa();
app.use((ctx, next) =&gt; {
    let view = ctx.url.substr(1);
    let content;
    if ( view === 'data' ) {
      content = data;
    } else {
      content = render(view);
    }
    ctx.body = content;
})
app.listen(3000, ()=&gt;{
    console.log('the modules test server is starting');
});
</pre>
</div>
<p>执行代码</p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">node --experimental-modules--loader ./custom-loader.mjs ./index.js
</pre>
</div>
<p>&nbsp;</p>
<h3>自定义loader规则优化</h3>
<p>从上面官方提供的自定义loader例子看出,只是对<code>*.js</code>文件做<code>import/export</code>做loader兼容,然而我们在实际开发中需要对npm模块,<code>*.json</code>文件也使用<code>import/export</code></p>
<h3>loader规则优化解析</h3>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">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) =&gt;
    /^(?!(?: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 &amp;&amp; typeof allDependencies === 'string' ) {
    return defaultResolve(specifier, parentModuleURL);
}

// 如果是文件引用,判断是否路径格式正确
if (/^\.{0,2}[/]/.test(specifier) !== true &amp;&amp; !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) &amp;&amp; !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'
    };
}

}
</pre>
</div>
<p>  </p>
<p>&nbsp;</p>
<h1>三、说明</h1>
<p>目前Node对<code>import/export</code>的支持现在还是<code>Stability: 1 - Experimental</code>阶段,后续的发展还有很多不确定因素,自己练手玩玩还可以,但是在还没去flag使用之前,尽量不要在生产环境中使用。</p>
<div class="article-info-box">&nbsp;</div>
<div class="article-info-box">&nbsp;</div>
<div class="article-info-box">&nbsp;</div>
<div class="article-info-box">&nbsp;</div>
<p>&nbsp;</p>

</div>
<div id="MySignature" role="contentinfo">
    <span style="color: #999999">

你们的评论、反馈,及对你们有所用,是我整理材料和博文写作的最大的鼓励和唯一动力。欢迎讨论和关注!
<br>

没有整理与归纳的知识,一文不值!高度概括与梳理的知识,才是自己真正的知识与技能。

永远不要让自己的自由、好奇、充满创造力的想法被现实的框架所束缚,让创造力自由成长吧!

多花时间,关心他(她)人,正如别人所关心你的。理想的腾飞与实现,没有别人的支持与帮助,是万万不能的。

</span><br><br>
来源:https://www.cnblogs.com/arxive/p/12254008.html
頁: [1]
查看完整版本: Node.js 中使用 ES6 中的 import / export 的方法大全