Node.js 中使用 ES6 中的 import / export 的方法大全
三種方法。 先上圖。
源代碼文件目錄(https://github.com/AK-47-D/nodejs_es6_tutorials):
image.png
方法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
跑的時候,你會明顯發現,真 TMD 慢了許多!!! 慢就慢在 Babel 這層處理映射邏輯上。
方法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 --experimental-modules es6_const_let_node_exp_demo.mjs
輸出:
(node:1402) ExperimentalWarning: The ESM module loader is experimental. 2 1 2
OK,上面就是 Node.js 中使用 ES6 中的 import / export 方法。
Node.js 中使用 import / export
因爲一些歷史原因,雖然 Node.js 已經實現了 99% 的 ES6 新特性,不過截止 2018.8.10,How To Enable ES6 Imports in Node.JS 仍然是老大難問題
下面我來介紹兩種方法可以讓我們在 Node.js 中使用 import/export 。
藉助 Babel 1.下載必須的包
npm install babel-register babel-preset-env --D
- 修改你的 server.js 下面是一個 server.js 的例子:
const Koa = require('koa') const app = new Koa() app.listen(3000, console.log("application is start at port 3000"))
用 import 替換 require
import Koa from 'koa' const app = new Koa() app.listen(3000, console.log("application is start at port 3000"))
如果你現在用 node server.js 跑這個文件,你會收到像這樣的錯誤提示:
/Users/zyf/myStudy/demo/chatroom/server/app.js:1 (function (exports, require, module, __filename, __dirname) { import Koa from 'koa' ^^^ SyntaxError: Unexpected identifier
下面讓我們用 babel 來解決這個問題
3.新增一個 start.js 文件 這個文件將成爲我們的入口文件,裏面是一些 babel 的代碼
require('babel-register') ({ presets: [ 'env' ] }) module.exports = require('./server.js')
注意,接下來不是 node server.js,而是用 node start.js 來啓動這個文件
來自 Node.js 官方的力量 Node 9提供了一個尚處於 Experimental 階段的模塊,讓我們可以拋棄 babel 等一類工具的束縛,直接在 Node 環境下使用 import/export。
官方手冊:ECMAScript Modules 一個不錯的教程:Node 9下import/export的絲般順滑使用 有興趣的可以去了解一下,嫌字多的可以繼續往下看看我總結的使用方法
用前須知 Node 版本需在 9.0 及以上 不加 loader 時候,使用 import/export 的文件後綴名必須爲 .mjs 舉個栗子 還是用上面的例子,請將代碼回退到 Babel 中第一步的樣子
1.改寫 server.js
import Koa from 'koa' const app = new Koa() app.listen(3000, console.log("application is start at port 3000"))
和前面一樣,不過將文件名改一下,從 server.js 改爲 server.mjs
2.啓動文件 執行下面代碼,來啓動文件
node --experimental-modules ./server.mjs
注意這是引用 koa 第三方模塊不用做其他變化,如果要 import 自己的文件,那麼那個待引入的文件也要改後綴。
比如
import example from './example'
那麼原來應該是 example.js 要改爲 example.mjs
目前這個模塊還處於實驗階段,還是不要放到生產環境,自己拿出來玩玩還是可以的。
原文:https://blog.csdn.net/zwkkkk1/article/details/81564971
Node 9下import/export的絲般順滑使用
前言
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()區別
能力 | 描述 | 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使用之前,儘量不要在生產環境中使用。