完全搞清楚 ECMAScript 的模塊化

概述

模塊式是目前前端開發最重要的範式之一。javascript

隨着前端項目的日漸複雜,不得不花費大量時間去管理。html

模塊化就是最主流的代碼組織方式。前端

將複雜的代碼按照功能不一樣劃分爲不一樣的模塊,經過單獨維護的方式,提升開發效率,下降維護成本。java

「模塊化」只是思想,不包含具體實現。node

演變過程

早期的技術標準並無預料到現在前端項目的規模,因此不少設計上的遺留問題致使咱們如今去實現模塊化有不少問題。jquery

雖然這些問題都被如今的模塊化標準和工具所解決了,但它的演變過程值得去思考。webpack

整體來看,JavaScript 模塊化大體有 4 個階段。web

1. 文件劃分的形式

這種形式就是以每一個 js 文件爲一個模塊,在 html 文件中導入它們進行使用。ajax

這麼作有 3 個問題:npm

  1. 污染全局做用域,這樣一個模塊內的任何成員均可以被訪問和修改。

  2. 命名衝突,模塊多了事後,很容易產生命名衝突。

  3. 沒法管理模塊依賴關係

這種方式徹底依靠約定,項目一旦體量過大,就很難保證全部模塊徹底按照約定來使用。

var name = "module-a";
 function method1() {  console.log(name + "#method1"); }  function method2() {  console.log(name + "#method2"); } 複製代碼

2. 命名空間方式

基於文件劃分進行優化,每一個模塊只暴露一個對象。模塊的成員都只暴露在這個對象下面。

這種方式能夠減小命名衝突的可能。

但這種方式仍然沒有私有空間,內部成員仍然能夠被訪問和修改。模塊間的依賴關係也沒有獲得解決。

var moduleA = {
 name: "module-a",   method1: function () {  console.log(name + "#method1");  },   method2: function () {  console.log(name + "#method2");  }, }; 複製代碼

3. IIFE

IIFE 就是當即執行函數,將須要暴漏出來的成員掛載到 window 對象上,經過這種方式能夠保證內部成員沒法被訪問和修改。

(function () {
 var name = "module-a";   function method1() {  console.log(name + "#method1");  }   function method2() {  console.log(name + "#method2");  }   window.moduleA = {  method1,  method2,  }; })(); 複製代碼

4. IIFE 經過參數聲明依賴

在 IIFE 基礎上接收一個參數,這樣模塊的依賴關係也更加明確。

(function ($) {
 var name = "module-b";   function method1() {  console.log(name + "#method1");  $("body").animate({ margin: "200px" });  }   function method2() {  console.log(name + "#method2");  }   window.moduleB = {  method1,  method2,  }; })(jQuery); 複製代碼

這些就是早期在沒有工具和規範的狀況下對模塊化的落地方式。

規範的出現

上面介紹的幾種方式,在不一樣項目和不一樣開發者的實際使用中,會存在細微的差別。爲了統一差別,就須要一個標準來規範模塊加載模塊。

手動在 html 中引入 js 文件會有不少問題。當新增長模塊或者修改模塊名字時,須要手動修改。模塊的依賴關係發生改變時,須要手動修改。模塊多餘,不須要時,須要手動移除。總之就是須要人工維護模塊的加載。

因此咱們須要模塊化標準和模塊加載器,經過代碼的方式來幫咱們自動加載模塊。

CommonJS 規範

CommonJS 是 Nodejs 提出的一套模塊化規範,具備如下幾條約定。

  • 一個文件就是一個模塊
  • 每一個模塊都有單獨的做用域
  • 經過 module.exports 導出成員
  • 經過 require 函數載入模塊

可是這套規範在瀏覽器中使用會有問題。

CommonJS 是以同步的方式加載模塊。nodejs 的機制是在啓動時加載全部模塊,運行時不會再去加載,只會去使用模塊。

這種模式運行在瀏覽器中,會致使應用效率低下,每打開一個頁面都會致使大量的同步請求出現。

因此早起瀏覽器中並無使用 CommonJS 規範,而是結合瀏覽器的特色,從新設計了一套瀏覽器規範,AMD。

AMD(Asynchronous Module Definition)

AMD 是異步模塊定義規範。

Require.js 是一個實現了 AMD 規範的庫。

具體用法以下:

define("moduleName", ["jQuery", "./module2"], function ($, module2) {
 return {  start: function () {  $("body").animate({ margin: "200px" });  module2();  },  }; }); 複製代碼

define 是定義模塊的函數。它接收兩個或三個參數。

第一個參數是模塊名,第二個參數是依賴數組,第三個是模塊函數。

模塊函數提供了獨立的做用域,並能夠按照依賴參數數組的順序接收參數列表。返回的對象就是暴露的成員。

第二個參數是可選的,若是沒有依賴的模塊,能夠省略。

除了 define,AMD 還有一個 require 函數,用法以下:

require(["./module1"], function (module1) {
 module1.start(); }); 複製代碼

require 用法和 define 相似,不一樣的是 require 只是會導入模塊和執行代碼,而不會去定義模塊。導入模塊的方式是建立一個 script 標籤,而後去請求模塊代碼。

目前絕大多數第三方庫都支持 AMD 規範。

因此 AMD 規範生態是很好的,可是使用相對複雜,並且若是模塊劃分過細的話,js 文件請求會很頻繁,致使頁面效率低下。

Sea.js + CMD

同時期的淘寶推出了 sea.js,和 require.js 相似。CMD 規範相似於 AMD,目的是想簡化 AMD 的寫法,儘可能和 CMD 保持一致,從而減小開發者學習成本,可是後來被 AMD 兼容了。算是一個重複的輪子。

define(function (require, exports, module) {
 var $ = require("jquery");  module.exports = function () {  console.log("module 2~");  $("body").append("<p>module2</p>");  }; }); 複製代碼

標準規範

上面的幾種規範雖然都解決了模塊化,但或多或少存在一些問題。

如今的前端模塊化已經很是成熟了,並且你們對目前的前端模塊化方式已經基本統一。

在瀏覽器中,使用 ES Modules 規範;在 nodejs 中,使用 CommonJS 規範。

因爲 CommonJS 是 nodejs 內置支持的模塊化規範,因此不存在兼容性問題。

ES Modules 是 ECMAScript2015 才被定義的標準,會存在各類環境下的兼容性問題。不過隨着 Webpack 等打包工具的流行,這個問題也逐漸被解決。

目前來講,ES Modules 是最流行的模塊化規範。相較於社區提出的 AMD 規範,ES Modules 在語言層面實現了模塊化,更加完善。

由於 ES Modules 是官方提出的規範,因此早晚全部瀏覽器都會原生實現這個特性。將來有着很是好的發展。

並且,短時間內應該也不會再有新的模塊化標準輪子出現。

ES Modules

基本特性

在瀏覽器中直接使用模塊的方式是給 script 標籤設置 type=module。

首先來建立一個 index.html 文件體驗一下。

<!DOCTYPE html>
<html lang="en">  <head>  <meta charset="UTF-8" />  <meta name="viewport" content="width=device-width, initial-scale=1.0" />  <title>Document</title>  </head>  <body>  <script type="module">  var hi = "hello, world!";  console.log(hi);  </script>  </body> </html> 複製代碼

經過 serve 工具啓動它,會發現這個模塊的內容和普通腳本同樣正常執行,沒有什麼區別。

接下來看一下模塊和腳本的幾個具體區別。

區別 1 模塊自動採用嚴格模式

在普通的腳本文件中,默認採用寬鬆模式,若是須要啓用嚴格模式,須要使用 "use strict"聲明。

拿 this 舉例。

<!DOCTYPE html>
<html lang="en">  <head>  <meta charset="UTF-8" />  <meta name="viewport" content="width=device-width, initial-scale=1.0" />  <title>Document</title>  </head>  <body>  <script>  console.log(`腳本:${this}`);  </script>  <script type="module">  console.log(`模塊:${this}`);  </script>  </body> </html> 複製代碼

這裏會有兩句日誌輸出。第一句會輸出 window,第二句會輸出 undefined。

區別 2 每一個模塊都會擁有私有做用域

看下面的例子。

<!DOCTYPE html>
<html lang="en">  <head>  <meta charset="UTF-8" />  <meta name="viewport" content="width=device-width, initial-scale=1.0" />  <title>Document</title>  </head>  <body>  <script>  var a = 1;  </script>  <script type="module">  var b = 2;  console.log("模塊1: ", a, b);  </script>  <script type="module">  console.log("模塊2: ", a, b);  </script>  </body> </html> 複製代碼

它也會打印 2 句日誌。

模塊1:  1 2
ReferenceError: b is not defined 複製代碼

能夠看到,腳本中用 var 建立的變量被掛載到 window 對象上,因此全部腳本和模塊均可以訪問到變量 a。

可是模塊中使用 var 建立的變量並不會被掛載到 window 對象上,因此接下來的模塊或者腳本訪問 b 時都會獲得 b is not defined 的錯誤。

這樣就能夠放心的在模塊中建立變量,而不須要擔憂全局做用域命名空間污染問題。

區別 3 模塊是經過 CORS 的方式請求外部模塊的

若是經過 src 請求模塊文件,同源的狀況下沒有問題。非同源的話好比服務端開啓 CORS 響應頭信息才能夠。

查看下面百度 jquery 的例子,這個 js 文件是不支持 CORS 的。

<!DOCTYPE html>
<html lang="en">  <head>  <meta charset="UTF-8" />  <meta name="viewport" content="width=device-width, initial-scale=1.0" />  <title>Document</title>  </head>  <body>  <script src="https://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>  <script>  console.log("get success");  </script>  <script  type="module"  src="https://libs.baidu.com/jquery/2.0.0/jquery.min.js"  ></script>  </body> </html> 複製代碼

能夠看到控制檯先打印 get success,因爲 script 默認是同步執行的,因此意味着經過腳本的模式加載文件成功了。接下來會獲得一個跨域的錯誤,意味着以模塊的方式加載文件失敗了。

並且模塊只支持經過 http 協議加載,不支持本地 file 協議加載。

區別 4 模塊會延遲執行腳本

腳本默認會當即執行,腳本的執行過程當中會中斷頁面渲染。

而模塊會延遲執行,至關於給 script 標籤添加了 defer 屬性。

看下面的例子。

<!DOCTYPE html>
<html lang="en">  <head>  <meta charset="UTF-8" />  <meta name="viewport" content="width=device-width, initial-scale=1.0" />  <title>Document</title>  </head>  <body>  <script src="./alert.js"></script>  <div>world</div>  </body> </html> 複製代碼

要在 index.html 同級目錄下建立一個 alert.js 文件。

alert("hello");
複製代碼

alert 的執行會阻塞頁面渲染,因此在 alert 存在的時候,是看不到 world 的。

若是加上 type=module,就能夠實現腳本的延遲執行。

導入/導出

模塊自己具備私有做用域,因此內部成員都沒法被外部所訪問到。若是要把模塊內的成員暴露出去,須要使用 export 關鍵詞去修飾要暴露的變量,而若是須要導入其餘模塊的成員,則須要 import 關鍵詞。

下面演示一下導入導出。

建立 module.js,其中暴露一個變量。

export var msg = "hello world";
複製代碼

建立 app.js 文件,導入這個變量並在控制檯打印。

import { msg } from "./module.js";
 console.log(msg); 複製代碼

建立 index.html,導入 app.js,用來測試。

<!DOCTYPE html>
<html lang="en">  <head>  <meta charset="UTF-8" />  <meta name="viewport" content="width=device-width, initial-scale=1.0" />  <title>Document</title>  </head>  <body>  <script type="module" src="./app.js"></script>  </body> </html> 複製代碼

在瀏覽器中打開 index.html,就能夠看到正常的輸出結果。

這就是最簡單的導入導出用法。

export 除了能夠導出變量之外,還能夠導出 function 和 class。

export var msg = "hello world";
 export function log(...args) {  console.log(args); }  export class Clazz {} 複製代碼

除了能夠直接在變量聲明前面使用 export,也能夠在模塊底部統一導出須要導出的成員。

var msg = "hello world";
 function log(...args) {  console.log(args); }  class Clazz {}  export { msg, log, Clazz }; 複製代碼

這種寫法更爲經常使用,由於能夠很直觀的知道這個模塊導出了哪些成員,並且經過這種方式導出的成員,能夠被重命名。

export { msg as myMsg, log, Clazz };
複製代碼

若是導出的成員被重命名了,導入的地方就須要使用重命名後的名字。

import { myMsg } from "./module.js";
複製代碼

若是導出成員的名字爲 default,那麼 default 的值就會做爲這個模塊的默認導出。因爲 default 是關鍵字,因此導入的地方必須重命名。

export { msg as default, log, Clazz };
複製代碼
import { default as msg } from "./module.js";
複製代碼

針對這種狀況,還有一種更簡單的寫法。經過 export 和 default 關鍵字的組合來導出默認的模塊。

export { log, Clazz };
export default msg; 複製代碼

這樣在導入模塊的默認導出時,就能夠隨便起一個名字。

import iMsg from "./module.js";
複製代碼

導入/導出注意事項

語法上和字面量對象的區別

export 的語法和對象字面量語法徹底一致,這就會產生迷惑行爲。

修改 module.js 內容。

var name = "jack";
var age = 18;  const obj = { name, age }; export { name, age }; 複製代碼

這就致使不少人認爲 export 導出的就是一個對象,而導入就是對這個對象的解構。實際上這是錯誤的。

雖然寫法同樣,但意義卻沒有任何關係。

export 單獨使用時,就必須用花括號包裹要導出的成員。

當 export 和 default 同時使用時,後面的花括號就表示一個對象。

這一點要區分開。

var name = "jack";
var age = 18;  const obj = { name, age }; // export { name, age };// 這是導出語法 export default { name, age }; // 這是默認導出一個對象 複製代碼

這時經過 import 來獲取 module.js 的默認導出。

修改 app.js 的內容。

import { name, age } from "./module";
 console.log(name, age); 複製代碼

會獲得一個錯誤。

Uncaught SyntaxError: The requested module './module.js' does not provide an export named 'age'
複製代碼

這說明 import 和 export 同樣,導出的不是一個對象的解構。花括號一樣是固定寫法。

成員的引用

模塊中導出的成員都不是值傳遞,而是引用傳遞。即便是基礎類型也是這樣。

能夠經過下面的實例來觀察。

修改 module.js。

var name = "jack";
var age = 18;  const obj = { name, age }; export { name, age };  setTimeout(function () {  age = 20; }, 1000); 複製代碼

修改 app.js。

import { name, age } from "./module.js";
 console.log(name, age);  setTimeout(function () {  console.log(age); }, 1500); 複製代碼

這樣在 1 秒後,age 的值被修改成 20,1.5 秒後打印 age 的值,若是是 20,就意味着是引用傳遞。

1.5 秒後打印的結果是 20。

這和 nodejs 的 CommonJS 規範是徹底不一樣的。

暴露出來的成員都是隻讀的

咱們沒有辦法在模塊外部修改模塊內部暴露的成員。

好比嘗試在 app.js 中修改 module.js 中暴露的成員 name。

import { name, age } from "./module.js";
 name = "tom"; 複製代碼

會獲得一個錯誤。

Uncaught TypeError: Assignment to constant variable.
複製代碼

這和 const 有點像。

導入

引用路徑

import 導入模塊時,from 後面的字符串其實是模塊的路徑。這個路徑必須是完整的文件名,不能夠省略擴展名。

可是在一些打包工具中,好比 Webpack,能夠省略文件擴展名,或者只填寫目錄名,省略 index.js。

import utils from "./utils/index.js"; // 標準語法
import utils from "./utils/index"; // 打包工具的語法,省略擴展名 import utils from "./utils"; // 打包工具的語法,省略 index.js 複製代碼

這一點和 CommonJS 是不一樣的。

在網頁開發中,引用網絡資源時,若是使用的是相對路徑,能夠省略掉./,可是 import 不能夠省略。

<img src="cat.png" />
<!-- 等價於 --> <!-- <img src='./cat.png'/> --> 複製代碼

可是 import 不能夠省略。

import utils from "./utils/index.js"; // 正確語法
import utils from "utils/index.js"; // 錯誤語法 複製代碼

若是省略了/ ./ 或者 ../,那就會以字母開頭,會被認爲是加載第三方模塊,這點和 CommonJS 是相同的。

除了相對路徑,也能夠經過項目的絕對路徑或者 http 的 URL 來導入模塊。

只去執行某個模塊代碼

好比模塊 A 只是輸出一串文字,並無導出任何成員。

// moduleA.js
console.log("hello, world"); 複製代碼

在模塊 B 中想要執行模塊 A 的內容,只須要導入這個模塊,不須要導入成員,就能夠執行裏面的腳本。

// moduleB.js
import {} from "./moduleA.js"; 複製代碼

除此以外,還有一種簡單寫法。就是省略成員列表和 from 關鍵詞。

import "./moduleA.js";
複製代碼
導出成員較多

若是一個模塊導出的成員很是多,並且其中不少成員都會被使用到,那麼可使用星號(*)的方式導入。

這種導入須要使用 as 關鍵詞給導出的全部成員重命名,而後導出的成員都會被掛載到這個對象身上。

好比在 module.js 中導出了兩個成員。

var name = "jack";
var age = 18;  export { name, age }; 複製代碼

那麼在 app.js 中用星號的方式導入。

import * as mod from "./module.js";
 console.log(mod); 複製代碼

就能夠在控制檯看到這個對象。

{
 "age": 18,  "name": "jack" } 複製代碼
import 不能夠導入變量

當一個模塊路徑是在代碼運行階段獲得的,那麼沒法使用 import 導入。

import 關鍵詞只能夠導入在代碼運行前已知的模塊路徑。

因此 import 關鍵詞只能夠出如今模塊文件的最頂部。

錯誤示例。

var modulePath = "./module.js"
import * as mod from modulePath console.log(mod) 複製代碼

當咱們遇到須要動態導入的機制時,就須要使用 import 函數。

由於這裏的 import 是一個函數,而不是關鍵詞,因此能夠在代碼的任何位置去執行。

import 函數返回一個 Promise 對象。模塊暴露的對象會經過 Promise 中 then 參數函數的參數拿到。

var modulePath = "./module.js";
 import(modulePath).then((module) => {  console.log(module); }); 複製代碼
同時導出默認成員和命名成員

若是 module.js 同時暴露了默認成員和命名成員,而在 app.js 想同時導入它們,能夠用下面這種寫法。

module.js

var name = "jack";
var age = 18;  export { name, age }; export default "hi"; 複製代碼

app.js

import { name, age, default as title } from "./module.js";
 console.log(name, age, title); 複製代碼

除了重命名這種寫法,還能夠直接在花括號外前面直接導出默認成員,更加簡潔。

這種寫法必須讓 default 成員在命名成員的前面,保證花括號和 from 關鍵詞是在一塊兒的。

import title, { name, age } from "./module.js";
 console.log(name, age, title); 複製代碼

直接導出導入成員

import 能夠和 export 組合使用,便於咱們直接導出。

export { name, age, default as title } from "./module.js";
 // console.log(name, age, title); 複製代碼

可是這種導出須要注意兩個點。

第一是 default 要寫到花括號中。

第二是直接 export 的話,模塊內部沒法訪問到這些成員。

這種語法一般在一個模塊文件夾中的 index.js 上使用。

好比有一個 components 文件夾,裏面存放了不少組件。

每一個組件都以一個模塊的形式維護。這樣會有 button.js、table.js、avatar.js 等等。

使用的時候就須要挨個導入,很是麻煩。

import { Button } from "./components/button.js";
import { Table } from "./components/table.js"; import { Avatar } from "./components/avatar.js"; 複製代碼

優化這種導入導出體驗的辦法是在 components 文件夾下建立一個 index.js,由它負責導出全部的組件。

index.js

export { Button } from "./button.js";
export { Table } from "./table.js"; export { Avatar } from "./avatar.js"; 複製代碼

這樣在導入時會很是簡單。

import { Button, Table, Avatar } from "./components/index.js";
複製代碼

ES Modules in Browser

因爲 ES Module 是 2014 年才提出的,到如今仍是有不少瀏覽器原生不支持這種用法。

因此使用 ES Module 時,還須要考慮兼容性的問題。

Polyfill 兼容方案

通常來講,咱們使用 ES Module 編寫的源代碼都會通過編譯工具的編譯轉換爲 ES5,再拿去瀏覽器中工做。

可是也有辦法讓瀏覽器直接支持 ES Module。

社區提供了一個叫作 browser-es-module-loader 的模塊,其實就是兩個 js 文件。在 html 中先導入這兩個模塊,就可讓 ES Module 工做了。

<!DOCTYPE html>
<html lang="en">  <head>  <meta charset="UTF-8" />  <meta name="viewport" content="width=device-width, initial-scale=1.0" />  <title>Document</title>  </head>  <body>  <script src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/babel-browser-build.js"></script>  <script src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js"></script>  <script type="module" src="./app.js"></script>  </body> </html> 複製代碼

上面用到的兩個包,第一個是 babel 運行在瀏覽器的版本,第二個是用來加載 es module 的。

原理比較簡單,首先 browser-es-module-loader 會經過 ajax 方式或者直接提取標籤中代碼的方式加載 module 文件,而後交給 babel 轉換,獲得瀏覽器能夠直接運行的代碼。

須要注意,這只是解決了模塊導入的問題,若是代碼中存在 ES6 的語法,還須要額外的 polyfill。

可是這種作法會有一個小問題。

在直接支持 es module 的瀏覽器中,模塊的代碼會被執行兩次。緣由很簡單,瀏覽器自己就會執行一次模塊中的代碼,polyfill 也會執行一次模塊中的代碼。

解決這個問題的方法就是給 script 添加 nomodule 屬性。

被添加 nomodule 屬性的腳本,只會在不支持 es module 的瀏覽器中去執行,而支持 es module 的瀏覽器則不執行。

<script  nomodule  src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/babel-browser-build.js" ></script> <script  nomodule  src="https://unpkg.com/browser-es-module-loader@0.4.1/dist/browser-es-module-loader.js" ></script> 複製代碼

polyfill 方案僅適合開發測試階段使用,而不適合生產階段去使用。由於全部的模塊代碼都是實時編譯的,性能很是差。

ES Modules in Node.js

ES Modules 做爲 JavaScript 語言層面的標準,它會逐漸取代 JavaScript 領域內全部模塊化的需求。

Node.js 做爲 JavaScript 一個重要運行環境,它已經開始逐漸支持這種特性了。

支持狀況

在 node.js8.5 版本後,內部就開始以實現特性的方式支持 ES Module 了。因此如今能夠直接在 node.js 中編寫 ES Module 的代碼。

在 node.js 中使用 ES Module,首先須要將文件的擴展名設置爲 mjs。

建立一個 module.mjs 文件。

export foo = 'foo'
export bar = 'bar' 複製代碼

再建立一個 index.mjs 文件。

import { foo, bar } from "./module.mjs";
 console.log(foo, bar); 複製代碼

在命令行運行 index.mjs 時須要加上 --experimental-modules 參數,表示啓用 ES Modules 實驗特性。

node --experimental-modules index.mjs
複製代碼

雖然這兩個成員會被正常打印,但同時也會有一句警告。

(node:7586) ExperimentalWarning: The ESM module loader is experimental.
複製代碼

意思就是告訴咱們 ESM module 屬於實驗特性,儘可能不要在生產環境去使用。

上面就是在 node.js 直接使用 ES Module 須要作的兩件事情。

導入內置模塊和第三方模塊

node.js 內置模塊能夠正常導入,沒有任何問題。

import fs from "fs";
 fs.writeFileSync("./foo.txt", "es module working"); 複製代碼

第三方模塊也能夠這樣導入,好比 lodash。

首先安裝一下 lodash。

npm i lodash
複製代碼

編寫測試代碼。

import _ from "lodash";
 console.log(_.upperCase("es module")); 複製代碼

運行代碼,發現它是能夠正常工做的。

再嘗試一下導出 upperCase。

import { upperCase } from "lodash";
 console.log(upperCase("es module")); 複製代碼

會獲得一個錯誤。

SyntaxError: The requested module 'lodash' does not provide an export named 'upperCase'
複製代碼

緣由也很簡單。

lodash 只提供了一個 default 的默認導出,而沒有將全部的成員所有單獨導出。

因此咱們也只能經過 import default 的方式導入默認成員。

不少第三方模塊都是這樣,由於它們都只提供了 CommonJS 標準的模塊。

可是 node.js 內置模塊對導出作了兼容,可使用命名成員的方式單獨導出。

import { writeFileSync } from "fs";
 writeFileSync("./foo.txt", "es module working"); 複製代碼

上面的代碼是能夠正常工做的。

ES Module 與 CommonJS 模塊交互

由於不少第三方模塊提供的都是 CommonJS 標準的模塊,因此就會面臨 ES Module 和 CommonJS 交互的狀況。

ES Module 導入 CommonJS

在 ES Module 中導入 CommonJS 和導入 ES Module 沒有太大區別。

編寫 commonjs.js。

module.exports = {
 foo: "hello world", }; 複製代碼

編寫 esm.js,導入 commons.js 中導出的模塊。

import mod from "./commonjs.js";
 console.log(mod); 複製代碼

運行 esm.js,能夠正常拿到 commonjs.js 中導出的對象。

在 commons.js 中,是沒法實現 ES Module 命名導出的。

exports.foo = "hello world";
複製代碼

上面這種導出方式仍然是一個 default 導出。

在 ES Module 中仍然沒法命名導入。

import { foo } from "./commonjs.js";
複製代碼

這會獲得一個錯誤信息,提示咱們沒有導出一個名爲 foo 的成員。

CommonJS 導入 ES Module

在 node.js 環境中,不容許 CommonJS 直接導入 ES Module。

修改 esm.js。

export const foo = "hello world";
複製代碼

修改 common.js

const mod = require("./esm.mjs");
 console.log(mod); 複製代碼

運行 common.js,會獲得一個錯誤。

Must use import to load ES Module
複製代碼

若是要在 node.js 中使用 CommonJS 導入 ES Module,須要使用 webpack 之類的打包工具。

ES Module 與 CommonJS 的差別

CommonJS 提供了幾個模塊內置的全局成員,相似於全局變量。

建立 cjs.js 文件,並輸出這幾個全局成員。

// 加載模塊函數
console.log(require);  // 模塊對象 console.log(module);  // 導出對象別名 console.log(exports);  // 當前文件絕對路徑 console.log(__filename);  // 當前文件所在目錄 console.log(__dirname); 複製代碼

運行以後,沒有任何問題。

再建立一個 esm.mjs 文件,把 cjs.js 中的內容複製過去,經過 node --experimental-modules esm.mjs 來運行。

會獲得一個錯誤。

ReferenceError: require is not defined
複製代碼

使用了 ES Module 載入模塊後,這些全局成員所有都沒法獲取了。

require、module 和 exports 在 ES Module 中沒有意義,但 __filename 和 __dirname 仍是須要的。

經過 import.meta.url 拿到當前的文件絕對路徑,再經過 nodejs 內置的 fileURLToPath 和 dirname 函數,就能夠獲取到 __filename 和 __dirname 了。

import { fileURLToPath } from "url";
import { dirname } from "path";  const __filename = fileURLToPath(import.meta.url); console.log(__filename); const __dirname = dirname(__filename); console.log(__dirname); 複製代碼
新版本的進一步支持

在 nodejs 最新的版本(v12+)中,對 ES Module 支持度更高了。

能夠經過在 package.json 中添加 type:module 屬性來提升對 ES Module 的支持。

{
 "type": "module" } 複製代碼

這樣項目下全部的 js 文件都會按照 ES Module 的方式執行,可是 --experimental-modules 參數不能夠省略。

若是開啓了這個特性,那麼以 Commonjs 編寫的模塊也會以 ES Modules 的模式來運行,這樣並不會識別 require 等函數,因此會報錯。

const fs = require("fs");
 fs.writeFileSync("./foo.txt", "es module working"); 複製代碼

這種狀況下,就須要將 CommonJS 標準規範的模塊後綴名改成.cjs。

Babel 兼容方案

在早期的 nodejs 版本中,並不支持 ES Module。這時就須要藉助 babel 來讓 ES Module 轉換爲 CommonJS 代碼。

能夠安裝一個低版本的 node,或者使用相似 nvm 或 n 的工具來設置一個低版本的 node。

在項目中安裝 babel 和 babel 相關的插件。

npm i -D @babel/node @babel/core @babel/preset-env
複製代碼

只是安裝了 babel 是不夠的,由於 babel 並不會自動幫助咱們轉換代碼,因此還須要本身編寫 babel 的配置,在項目根目錄下建立.babelrc 文件。

{
 "presets": ["@babel/preset-env"] } 複製代碼

再去運行 ES Module 的代碼,就能夠正常工做了。

npx babel-node index.js
複製代碼

@babel/preset-env 是一個 ES6 轉 ES5 的插件集合,咱們也能夠經過單個的插件去轉換。

好比使用@babel/plugin-transform-modules-commonjs。

安裝。

npm i -D @babel/plugin-transform-modules-commonjs
複製代碼

修改配置。

{
 "presets": ["@babel/plugin-transform-modules-commonjs"] } 複製代碼

這樣的話,編譯速度可能會更快一些。

寫在最後

前端模塊化是前端工程化的第三塊內容,到此講解結束,接下來會發布另外幾篇關於前端工程化的文章,敬請期待。

本文使用 mdnice 排版

相關文章
相關標籤/搜索