Node.js中的ES模塊現狀

做者:Tobias Nießen

翻譯:瘋狂的技術宅javascript

原文:https://jaxenter.com/es-modul...html

未經容許嚴禁轉載前端

幾乎每種編程語言都能將組成程序的代碼拆分爲多個文件。 在 C 和 C++ 中 #include 指令就用於這個目的,而 Java 和 Python 有 import 關鍵字。 JavaScript 是迄今爲止爲數很少的例外之一,但新的 JavaScript 標準(ECMAScript 6)經過引入所謂的 ECMAScript 模塊來改變這一點。全部主流瀏覽器都支持這個新標準 —— 只有 Node.js 彷佛落後了。這是爲何?

新的 ECMAScript(ES)模塊與之前的語言版本不徹底兼容,所以使用的 JavaScript 引擎須要知道每個文件是「舊」 JavaScript 代碼仍是「新」模塊。java

例如在 ECMAScript 5 中引入的許多程序員首選的嚴格模式曾經是可選的,必須明確啓用才行,同時它在 ES 模塊中始終處於活動狀態。所以,如下代碼段在語法上能夠解釋爲傳統的 JavaScript 代碼和 ES 模塊:node

a = 5;

做爲經典的 Node.js 模塊,這至關於 global.a = 5,由於未聲明變量 a 而且未明確激活嚴格模式,所以 a 被視爲全局變量。若是你嘗試加載與 ES 模塊相同的文件,則會收到錯誤 「ReferenceError:a is not defined」,由於未聲明的變量可能沒法在嚴格模式下使用。程序員

瀏覽器經過<script> 標記的擴展解決了區別問題:沒有 type 屬性或帶有 type="text/javascript" 屬性的腳本仍然在傳統模式下運行,而當腳本使用 type ="module" 屬性時則做爲模塊處理。因爲這種簡單的分離,如今全部流行的瀏覽器都支持新的模塊。 Node.js 中的實現要困可貴多:2009年發明的 JavaScript 應用程序框架使用 CommonJS 標準模塊,該標準基於 require 函數。此函數能夠隨時根據其相對於當前運行模塊的路徑加載另外一個模塊。新的 ES 模塊也是由它們的路徑定義的,可是 Node.js 是如何知道正在加載的模塊是遺留的 CommonJS 仍是 ES 模塊的呢?僅僅基於語法是不夠的,由於即便不使用新關鍵字的 ES 模塊也不兼容CommonJS模塊。面試

此外,ECMAScript 6 還提供了能夠從 URL 加載模塊,而 CommonJS 僅限於文件的相對和絕對路徑。這種創新不只使加載更復雜,並且可能更慢,由於 URL 不須要指向本地文件。特別是在瀏覽器中,腳本和模塊一般經過HTTP網絡協議加載。npm

CommonJS 容許經過 require 函數加載模塊,該函數返回加載的模塊。例如,CommonJS 模塊可能以下所示:編程

const { readFile } = require('fs');
const myModule = require('./my-module');

這不是 ECMAScript 6 中的一個選項,由於在 require() 調用期間,模塊在 HTTP 上加載時可能會長時間阻止整個程序的執行。相反,ES 模塊提供了兩種加載其餘模塊的方法。在大多數狀況下,使用 import 是有意義的:json

import { readFile } from 'fs';
import myModule from './my-module';

可是,這會不可避免地延遲模塊的執行,直到加載 fs./my-module,但它們不會阻止其餘模塊的執行。當模塊必須動態加載時,會變得更加複雜。 CommonJS 模塊中看起來微不足道的東西變得愈來愈難以異步:

if (condition) {
  myOtherModule = require('./my-other-module');
}

ECMAScript 但願經過功能性使用 import 關鍵字來解決這個問題,該關鍵字異步加載模塊並在每次調用時返回 Promise 對象。但缺點是程序員如今也負責錯誤處理,由於錯誤不會像在同步狀況下那樣自動傳給調用者。

if (condition) {
  import('./my-other-module.js')
  .then(myOtherModule => {
    // Module was loaded successfully and can
    // now be used here.
  })
  .catch(err => {
    // An error occurred that needs to be handled here.
    console.error(err);
  });
}

若是使用 async 關鍵字聲明瞭要加載模塊的函數,因爲 ECMAScript 6 中引入了 await 函數,import() 的使用更加清晰,而且錯誤處理被傳遞給同步執行中的調用者:

if (condition) {
  myOtherModule = await import('./my-other-module');
}

import 做爲一個函數使用,它不是 ECMAScript 6 的一個組件,而是一個所謂的 Stage 3 提案,有可能會在下一個 JavaScript 版本中標準化。此外 Firefox、Chrome 和 Safari 等許多瀏覽器以及 Node.js 都支持它。

在Node.js中使用

區分 CommonJS 和 ES 模塊的難度致使在 Node.js 下爲 ES 模塊引入了新的文件擴展名:若是已設置了 -experimental-modules 選項, Node.js 能夠把以 .mjs 結尾的文件做爲 ES 模塊進行加載。從 2017 年 9 月發佈的 Node.js 8.5.0 開始,若是將如下代碼保存爲 testmodule.mjs,則能夠用 node -experimental-modules testmodule.mjs 命令執行它:

export function helloWorld(name) {
  console.log(`Hallo, ${name}!`);
}
  
helloWorld('javascript-conference.com');

Node.js 12 擴展了對 ES 模塊的支持。重要的是,如今能夠用 package.json 文件,它包含了諸如包的惟一名稱之類的信息。如今使用的 JSON 格式擴展了一個名爲 type 的新屬性。能夠選擇將其更改成 commonjsmodule 以肯定默認狀況下應加載的包中所包含的 JavaScript 文件的模式。如下配置指定了一個包 example-package,它至少必須包含 ES 模塊 index.js

{
  "name": "example-package",
  "type": "module",
  "main": "index.js"
}

像往常同樣,「main」 字段指定哪一個文件應該做爲入口點。例如 index.js 模塊可能以下所示:

import { userInfo } from 'os';
  
export function greet() {
  return `Hello ${userInfo().username}!`;
}

如今能夠從其餘文件加載此模塊。包一般位於 node_modules 目錄中各自的文件夾中。要加載剛建立的包,咱們能夠用如下目錄結構和一個名爲 main.js 的新文件:

- main.js
+ node_modules
  + example-package
    - package.json
    - index.js

main.js 文件能夠引用傳統的 CommonJS 或新的 ECMAScript 模塊。在這兩種狀況下,example-package 都不能使用一般的 require() 調用加載,由於 ECMAScript 模塊必須始終異步加載。所以 CommonJS 模塊必須使用 import 加載 ES 模塊:

import('example-package')
.then(package => {
  console.log(package.greet());
})
.catch(err => {
  console.error(err);
});

這樣作的缺點是 CommonJS 模塊不能像往常那樣在開始時訪問其餘模塊或軟件包,但只能在事實和異步以後才能訪問。執行如上所述腳本:node -experimental-modules main.js,若是入口點自己也是 ES 模塊,則更容易。若是將 main.js 重命名爲 main.mjs,則能夠用 import

import { greet } from 'example-package';
console.log(greet());

所以,能夠在一個應用程序中同時使用 CommonJS 和 ECMAScript 模塊,但它有可能會引起混亂。由於 CommonJS 模塊須要知道正在加載的模塊是 CommonJS 仍是 ES 模塊,而且只能異步加載 ES 模塊。這也適用於經過 npm 安裝的軟件包的加載。 fscrypto 等內置模塊能夠經過兩種方式加載。

Node.js 中的差別

除了異步加載依賴項的問題以外,Node.js 中的舊模塊和新模塊之間還存在進一步的差別。特別是 ES 模塊中再也不提供 Node.js 的特定功能,如變量 __dirname__filenameexportmodule__dirname__filename 能夠根據須要重新的 import.meta 對象重建:

import { fileURLToPath } from 'url';
import { dirname} from 'path';
  
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

變量 moduleexports 已被刪除而無需替換;這一樣適用於 module.filenamemodule.idmodule.parent 等屬性。一樣 require()require.main 再也不可用。

雖然 CommonJS 中的循環依賴關係已經經過緩存各個模塊的 module.exports 對象來解決,但 ECMAScript 6 用了所謂的綁定。簡而言之,ES 模塊不會導出和導入值,只是對值的引用。導入此類引用的模塊能夠訪問該值,但沒法修改它。已導出引用的模塊能夠爲引用分配新值,該值將由從該點導入引用的其餘模塊使用。與以前的概念相比,這有着本質的區別,後者容許在任什麼時候間點將屬性分配給 CommonJS 模塊的 module.exports 對象,從而使這些更改僅部分反映在其餘模塊中。

根據 ECMAScript 規範,import 默認狀況下不會用文件擴展名完成文件路徑,由於 Node.js 以前已經爲 CommonJS 模塊完成了,所以必須明確說明。一樣當指定的路徑是目錄時,行爲會發生變化:import'./directory' 不會在指定的文件夾中查找 index.js 文件,而是拋出一個錯誤,這是 Node.js 中的標準狀況。二者均可以經過傳遞實驗選項 -es-module-specifier-resolution = node 來改變。

結論

在最近發佈的 Node.js 12.1.0 中,仍然須要經過 -experimental-modules 選項顯式激活 ECMAScript 模塊的使用,由於它是一個實驗性功能。可是,開發人員的目標是在 Node.js 12 成爲新的長期支持版本以前,在沒有明確激活的狀況下完成此功能並支持 ES 模塊,預計將會在2019年10月完成。

現有的各類 CommonJS 模塊使從 CommonJS 到 ECMAScript 模塊的轉換變得複雜。單個程序包沒法切換到 ES 模塊,從而不會發生與使用 require() 加載相應程序包的現有程序和程序包不兼容的狀況。像 Babel 這樣的工具能夠將較新的語法轉換爲與舊環境兼容的代碼,這使轉換更容易。像 Deno 這樣的新框架背棄了近年來多樣化的模塊化系統,徹底依賴於 ECMAScript 模塊,這對於把 JavaScript 做爲編程語言的開發,標準化模塊的引入是重要的一步,爲將來的改進鋪平了道路。


本文首發微信公衆號:前端先鋒

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章

歡迎掃描二維碼關注公衆號,天天都給你推送新鮮的前端技術文章

歡迎繼續閱讀本專欄其它高贊文章:


相關文章
相關標籤/搜索