【譯】JavaScript 模組:從立即執行函式 ( IIFEs ) 到 CommonJS 再到 ES6 模組

FrankCheung發表於2019-01-26

原文地址:JavaScript Modules: From IIFEs to CommonJS to ES6 Modules
原文作者:Tyler McGinnis
譯者:FrankCheung

我教授 JavaScript 給很多不同的人很長一段時間了。這門語言普遍最難懂的概念就是模組系統。當然,這是有原因的,因為模組在 JavaScript 中有著一個奇怪的歷史。在這篇文章中,我們將重溫這段歷史,你將學習到過去的模組化方式,以更好地理解如今 JavaScript 模組的工作原理。

在我們學習怎麼在 JavaScript 中建立模組之前,我們首先必須明白什麼是模組以及它們存在的意義。現在請你環顧四周,所有你看到的稍微複雜一點的物體,都可能是使用能夠組合起來的、又相對獨立的小零件拼裝起來的。

下面我們以一塊手錶為例子。

【譯】JavaScript 模組:從立即執行函式 ( IIFEs ) 到 CommonJS 再到 ES6 模組

一塊簡單的腕錶由成千上萬個內部零件組成。對於如何和其他零件進行協同,每一個小零件都有一個特定的用途和清晰作用範圍。將所有的零件放到一起就可以組裝出一塊完整的手錶。我不是一個手錶工程師,但是上述方法的好處清晰可見。

可複用性 ( Reusability )

再看一下上面的圖表,留意一塊表上使用了多少相同的小零件。通過將十分聰明的設計思想融入到模組化中,在手錶設計的不同層面都可以複用相同的零件。這種可以複用零件的能力簡化了生產流程,與此同時,我猜想也增加了收益。

可組合性 ( Composability )

上述圖表是可組合性的一個很好的闡釋。通過劃分清楚每個內部零件的作用範圍,就可以將不同的微小的、功能單一的零件組合起來,製造出一隻功能完整的手錶。

槓桿作用 ( Leverage )

設想一下整個製造流程。這個公司並不是在製造手錶,而是在製造個別的手錶零件。他們既可以選擇由自己公司來生產,也可以選擇將這項工作外包出去,利用其他工廠進行生產,這都沒有問題。不管零件在哪裡生產,最關鍵的一點是每一個零件最後能夠組合起來形成一塊手錶即可。

獨立性 ( Isolation )

要明白整個系統是困難的,因為一塊手錶是由不同的功能單一的小零件組合而成的,每個小零件都可以被獨立地設計、製造或者修理。這種獨立性允許在製造或者修理手錶過程中,多人同時獨立工作,互不干擾。另外,如果手錶的其中一個零件損壞了,你需要做的僅僅是換掉那個損壞的零件,而不是換掉整塊手錶。

可組織性 ( Organization )

可組織性是每個零件具有清晰的作用範圍的副產品。在此基礎上,可組織性是自然而然產生的。


我們已經看到模組化應用在我們日常生活中的事物,比如手錶上的明顯的好處,如果將模組化應用到軟體上會怎麼樣呢?同樣的方法將得到同樣的好處,就像手錶的設計一樣,我們應該將軟體設計成由不同的功能單一的有著特定用途和清晰的作用範圍的小塊組成。在軟體中,這些小塊被稱為模組。在這一點上,一個模組聽上去可能和一個函式或者一個 React 元件沒有太大區別。那麼,一個模組究竟包含了什麼?

每個模組分為三個部分 —— 依賴(也稱為匯入 ( imports ) ) ( dependencies ), 程式碼 ( code ) , 匯出 ( exports )

imports code exports

依賴( 匯入 )

當一個模組需要另一個模組的時候,它可以 import 那個模組,將那個模組當作一個依賴。例如,當你想建立一個 React 元件時,你需要 import react 模組。如果你想使用一個庫,如 lodash ,你需要 import lodash 模組。

程式碼

當引入了你的模組需要的依賴,接下來就是這個模組真正的程式碼。

匯出

匯出 ( exports ) 是一個模組的“介面”。不管你從這個模組中匯出什麼,對於該模組的匯入者來說都是可以訪問到的。


已經談論足夠多的上層概念了,下面讓我們來深入一些具體的例子。

首先,讓我們先看看 React Router 。十分方便,它們有一個模組資料夾,這個資料夾自然是充滿了......模組。如此,在 React Router 中,什麼是一個模組呢?大多數情況下,它們直接對映 React 元件到模組。這是可行的,並且模組化邏輯通常就是你在 React 專案中如何拆分元件的邏輯。這行得通,因為如果你重溫上面關於手錶的部分,並且用“元件 ( component ) ”替換所有“模組 ( module ) ”字眼,這個比喻仍然是成立的。

讓我們來看一下 MemoryModule 的程式碼。注意現在不要過分關注裡面的程式碼,而是更應該著眼於這個模組的結構。


// imports
import React from "react";
import { createMemoryHistory } from "history";
import Router from "./Router";

// code
class MemoryRouter extends React.Component {
  history = createMemoryHistory(this.props);
  render() {
    return (
      <Router
        history={this.history}
        children={this.props.children}
      />;
    )
  }
}

// exports
export default MemoryRouter;

複製程式碼

你會注意到模組的頂部定義了所需要的引入,或者是使 MemoryRouter 正確執行的其他模組。接下來是這個模組實際的程式碼。在這裡,它們建立了一個新的名叫 MemoryRouter 的 React 元件。然後在底部,它們定義了這個模組匯出的內容 MemoryRouter 。這意味著無論何時其他人引入 MemoryRouter 模組,都將得到該 MemoryRouter 元件。


現在我們明白了什麼是一個模組,讓我們回顧一下手錶設計的好處,並且看一下如何在遵循相似的模組化方法的情況下,讓軟體設計得到同樣的好處。

可複用性

模組最大化了可複用性,因為一個模組可以被其他任何需要的模組匯入並使用。除此以外,如果一個模組對其他應用程式有用,你還可以建立一個包 ( package )。一個包 ( package ) 可以包含一個或多個模組並且可以被上傳至 NPM 供其他人下載。 react, lodash, 以及 jquery 都是 NPM 包很好的例子,因為他們可以通過NPM地址進行安裝。

可組合性

因為模組明確定義了它們匯入和匯出的內容,所以它們可以很容易地被組合。不僅如此,優秀軟體的一個標誌就是可以輕鬆地被移除。模組化也提高了程式碼的“可移除性”( delete-ability )。

槓桿作用

NPM有著世界上最大的免費的可複用模組集合。這個優勢是如果你需要某個特定的包,NPM都會有。

獨立性

我們對手錶獨立性的描述在這裡同樣適用。“明白整個系統是困難的,因為(你的軟體)是由不同的功能單一的(模組)組合而成的,每個(模組)都可以被獨立地設計、建立或者修復。這種獨立性允許在建立或者修復(程式)過程中,多人同時獨立工作,互不干擾。另外,如果其中一個(模組)出問題了,你需要做的僅僅是換掉那個出問題的(模組),而不是換掉整個(程式)。”

可組織性

可能模組化對於軟體來說最大的好處就是可組織性。模組提供了一個自然的分割點。由此,正如我們即將看到的那樣,模組將能防止你汙染全域性名稱空間,並且幫助你避免命名衝突。


此刻你知道了模組的好處並且瞭解了模組的結構,是時候開始建立它們了。我們的方法是十分詳盡的,因為正如之前提到的,JavaScript 的模組有著奇怪的歷史。儘管現在 JavaScript 有“更新”的方法建立模組,但一些舊的方法仍然存在並且你還將會不時看到它們。如果我們一下子跳到2018年的模組化方式,對於你理解模組化來說是不利的。因此,我們將回到2010年末,AngularJS 剛剛釋出,jQuery 正盛行。公司最終還是使用 JavaScript 來建立複雜的網頁應用,正因如此,產生了通過模組來管理複雜網頁應用的需要。

你建立模組的第一直覺可能是通過建立不同檔案來拆分程式碼。


// users.js
var users = ["Tyler", "Sarah", "Dan"]

function getUsers() {
  return users
}

複製程式碼

// dom.js

function addUserToDOM(name) {
  const node = document.createElement("li")
  const text = document.createTextNode(name)
  node.appendChild(text)

  document.getElementById("users")
    .appendChild(node)
}

document.getElementById("submit")
  .addEventListener("click", function() {
    var input = document.getElementById("input")
    addUserToDOM(input.value)

    input.value = ""
})

var users = window.getUsers()
for (var i = 0; i < users.length; i++) {
  addUserToDOM(users[i])
}

複製程式碼

<!-- index.html -->
<!DOCTYPE html>
<html>
  <head>
    <title>Users</title>
  </head>

  <body>
    <h1>Users</h1>
    <ul id="users"></ul>
    <input
      id="input"
      type="text"
      placeholder="New User">
    </input>
    <button id="submit">Submit</button>

    <script src="users.js"></script>
    <script src="dom.js"></script>
  </body>
</html>

複製程式碼

完整程式碼見此處

好了,我們已經成功地將我們的應用程式碼拆分成不同的檔案,但這是否意味著我們已經成功地實現了模組化呢?不,這完全不是模組化。從字面上來說,我們所做的只是將程式碼所在的位置進行了拆分。在 JavaScript 中建立一個新的作用域的唯一方法是使用一個函式。我們宣告的所有不在函式體內的變數都是存在於全域性物件上的。你可以通過在控制檯上列印出 window 物件來驗證這一說法。你會意識到我們可以訪問,甚至更壞的情況是,改變 addUsers, users, getUsers, addUserToDOM 。這實質上就是整個應用程式。我們完全沒有將程式碼拆分到模組裡去,剛才所做的只是改變了程式碼物理上的存在位置。如果你是 JavaScript 初學者,這可能會讓你大吃一驚,但這可能是你對於如何在 JavaScript 中實現模組化的第一直覺。

如果拆分檔案並沒有實現模組化,那該怎麼做?還記得模組的優點 —— 可複用性、可組合性、槓桿作用、獨立性、可組織性。JavaScript 是否有一個天然的特性可供我們用於建立“模組”,並且帶來上述的好處?通過一個普通的函式如何?想一下函式的好處你會發現,它們可以很好地與模組的好處對應上。那麼該怎麼實現呢?與其讓整個應用存在於全域性名稱空間上,我們不如暴露一個單獨的物件,可以稱之為 APP 。可以將所有應用程式執行需要的方法放入這個 APP 物件中,防止汙染全域性名命空間。然後可以將所有東西用一個函式包裹,讓它們相對於應用程式的其他空間是封閉的。


// App.js
var APP = {}

複製程式碼

// users.js
function usersWrapper () {
  var users = ["Tyler", "Sarah", "Dan"]

  function getUsers() {
    return users
  }

  APP.getUsers = getUsers
}

usersWrapper()

複製程式碼

// dom.js

function domWrapper() {
  function addUserToDOM(name) {
    const node = document.createElement("li")
    const text = document.createTextNode(name)
    node.appendChild(text)

    document.getElementById("users")
      .appendChild(node)
  }

  document.getElementById("submit")
    .addEventListener("click", function() {
      var input = document.getElementById("input")
      addUserToDOM(input.value)

      input.value = ""
  })

  var users = APP.getUsers()
  for (var i = 0; i < users.length; i++) {
    addUserToDOM(users[i])
  }
}

domWrapper()

複製程式碼

<!-- index.html -->
<!DOCTYPE html>
<html>
  <head>
    <title>Users</title>
  </head>

  <body>
    <h1>Users</h1>
    <ul id="users"></ul>
    <input
      id="input"
      type="text"
      placeholder="New User">
    </input>
    <button id="submit">Submit</button>

    <script src="app.js"></script>
    <script src="users.js"></script>
    <script src="dom.js"></script>
  </body>
</html>

複製程式碼

完整程式碼見此處

現在如果你檢視 window 物件,之前它擁有應用程式所有重要的部分,現在它只擁有 APP 以及包裹函式 ( wrapper functions ),usersWrapperdomWrapper 。更重要的是,我們重要的程式碼(例如 users )不能被隨意修改了,因為它們並不存在於全域性名稱空間上。

讓我們看看是否能夠更進一步,是否有方法可以避免使用包裹函式?注意我們定義了包裹函式並且馬上呼叫了它們,我們賦予包裹函式一個名字的原因只是為了能呼叫它們。是否有方法可以立即呼叫一個匿名函式,這樣我們就不需要賦予它們名字了?確實有這樣的方法,並且這個方法還有一個很好的名字 —— 立即執行函式表示式 ( Immediately Invoked Function Expression ) 或者縮寫為 IIFE

IIFE

下面是 IIFE 的大概樣式


(function () {
  console.log('Pronounced IF-EE')
})()

複製程式碼

注意下面只是一個包裹在小括號中的匿名函式表示式。


(function () {
  console.log('Pronounced IF-EE')
})

複製程式碼

然後,就像其他函式一樣,為了呼叫它,我們在其後面新增了一對小括號。


(function () {
  console.log('Pronounced IF-EE')
})()

複製程式碼

現在為了避免使用包裹函式,並且讓全域性名命空間變乾淨,讓我們使用 IIFEs 的相關知識改造我們的程式碼。


// users.js

(function () {
  var users = ["Tyler", "Sarah", "Dan"]

  function getUsers() {
    return users
  }

  APP.getUsers = getUsers
})()

複製程式碼

// dom.js

(function () {
  function addUserToDOM(name) {
    const node = document.createElement("li")
    const text = document.createTextNode(name)
    node.appendChild(text)

    document.getElementById("users")
      .appendChild(node)
  }

  document.getElementById("submit")
    .addEventListener("click", function() {
      var input = document.getElementById("input")
      addUserToDOM(input.value)

      input.value = ""
  })

  var users = APP.getUsers()
  for (var i = 0; i < users.length; i++) {
    addUserToDOM(users[i])
  }
})()

複製程式碼

完整程式碼見此處

完美!現在如果你檢視 window 物件,你將會看到只新增了 APP 到其上,作為該應用程式執行所需的所有方法的名稱空間。

我們可以稱這個模式為 IIFE Module Pattern

IIFE Module Pattern 的好處是什麼呢?首先並且最重要的是,避免了將所有東西都放置到全域性名稱空間上,這將有助於減少變數衝突以及讓程式碼更私有化。這種模式是否有不足之處?當然,我們仍在全域性名稱空間上建立了一個變數, APP 。如果碰巧另一個庫也使用相同的名稱空間就會很麻煩。其二,index.html<script> 標籤的順序影響程式碼執行,如果不保持現有順序,那麼整個應用程式將會崩潰。

儘管上述解決方法並非完美,但仍然是進步的。現在我們明白了 IIFE module pattern 的優缺點,如果讓我們制定建立和管理模組的標準,會需要什麼樣的功能呢?

之前我們將程式碼拆分為模組的第一直覺,是每個檔案都是一個新的模組。儘管在 JavaScript 中這並不起作用,但我認為這是一個明顯的模組分割點。每個檔案就是一個獨立的模組。基於此,還需要一個功能,就是讓每個檔案定義明確的匯入(或者說是依賴),以及對於匯入模組可用的明確的匯出


Our Module Standard

1) File based
2) Explicit imports
3) Explicit exports

複製程式碼

現在知道了我們制定的模組標準需要的功能,下面可以來看一下 API 。唯一真實的我們需要定義的 API 是匯入和匯出的實現。從匯出開始,儘量保持簡單,任何關於模組的資訊可以放置於在 module 物件中。然後我們可以將想匯出的內容新增到 module.exports 。跟下面的程式碼類似:


var users = ["Tyler", "Sarah", "Dan"]

function getUsers() {
  return users
}

module.exports.getUsers = getUsers

複製程式碼

這意味著我們也可以用下面的寫法:


var users = ["Tyler", "Sarah", "Dan"]

function getUsers() {
  return users
}

module.exports = {
  getUsers: getUsers
}

複製程式碼

無論有多少方法,都可以將他們新增到 exports 物件上。


// users.js

var users = ["Tyler", "Sarah", "Dan"]

module.exports = {
  getUsers: function () {
    return users
  },
  sortUsers: function () {
    return users.sort()
  },
  firstUser: function () {
    return users[0]
  }
}

複製程式碼

現在我們弄清楚了從一個模組匯出內容是怎樣的,下面需要弄清楚從模組匯入內容的 API 是怎樣的。簡單而言,我們假設有一個名叫 require 的函式。它將接收字串路徑作為第一引數,然後將會返回從該路徑匯出的內容。繼續使用 users.js 作為例子,引入模組將會類似下面的方式:


var users = require('./users')

users.getUsers() // ["Tyler", "Sarah", "Dan"]
users.sortUsers() // ["Dan", "Sarah", "Tyler"]
users.firstUser() // ["Tyler"]

複製程式碼

非常順手。使用我們假想的 module.exportsrequire 語法,我們保留了模組的所有好處並且避免了使用 IIFE Modules pattern 的兩個缺點。

正如你目前為止所猜測的,這並不是一個虛構的標準。這個標準是真實存在的,它叫做 CommonJS 。

CommonJS 定義了一個模組格式,通過保證每個模組在其獨自的名稱空間內執行, 來解決 JavaScript 的作用域問題。這需要強制模組清晰地匯出需要暴露給外界的變數,並且定義好程式碼正常工作需要引入的其他模組。 —— Webpack 文件

如果你之前使用過 Node ,CommonJS 看起來是相似的。這是因為為了實現模組化,Node (大多數情況下)使用了 CommonJS 的規範。因此,在 Node 中你使用之前看到過的,CommonJS 的 requiremodule.exports 語法來使用模組。然而,瀏覽器並不像 Node ,其並不支援 CommonJS 。事實上,不僅僅是瀏覽器不支援 CommonJS 的問題,而且對於瀏覽器來說, CommonJS 並不是一個好的模組化解決方案,因為它對於模組的載入是同步的。在瀏覽器環境中,非同步載入才是王道。

總體而言,CommonJS 有兩個問題。第一個問題是瀏覽器並不支援,第二個問題是它的模組載入是同步的, 這樣在瀏覽器端的使用者體驗是極差的。如果能夠解決上述兩個問題,情況將會大為不同。那麼如果CommonJS對於瀏覽器並不友好,我們花時間討論它的意義何在?下面將介紹一種解決方案,它被稱為 模組打包器 ( module bundler ) 。

模組打包器 ( Module Bundlers )

JavaScript 模組打包器會檢查你整個程式碼庫,找到所有的匯入和匯出,然後智慧地將所有模組打包成一個瀏覽器能識別的單獨的檔案。不需要像以前一樣在 index.html 中按順序引入所有的 scripts ,現在你需要做的是引入那個打包好的檔案 bundle.js 即可。


app.js ---> |         |
users.js -> | Bundler | -> bundle.js
dom.js ---> |         |

複製程式碼

那麼打包器實際上是如何工作的呢?這真是一個大問題,並且這個問題我也沒有完全弄明白。但下面給出通過 Webpack,一個流行的模組打包器,打包後的我們的程式碼。

完整程式碼見此處 你需要下載這些程式碼,執行 "npm install" 指令,然後執行 "webpack" 指令。


(function(modules) { // webpackBootstrap
  // The module cache
  var installedModules = {};
  // The require function
  function __webpack_require__(moduleId) {
    // Check if module is in cache
    if(installedModules[moduleId]) {
      return installedModules[moduleId].exports;
    }
    // Create a new module (and put it into the cache)
    var module = installedModules[moduleId] = {
      i: moduleId,
      l: false,
      exports: {}
    };
    // Execute the module function
    modules[moduleId].call(
      module.exports,
      module,
      module.exports,
      __webpack_require__
    );
    // Flag the module as loaded
    module.l = true;
    // Return the exports of the module
    return module.exports;
  }
  // expose the modules object (__webpack_modules__)
  __webpack_require__.m = modules;
  // expose the module cache
  __webpack_require__.c = installedModules;
  // define getter function for harmony exports
  __webpack_require__.d = function(exports, name, getter) {
    if(!__webpack_require__.o(exports, name)) {
      Object.defineProperty(
        exports,
        name,
        { enumerable: true, get: getter }
      );
    }
  };
  // define __esModule on exports
  __webpack_require__.r = function(exports) {
    if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
      Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
    }
    Object.defineProperty(exports, '__esModule', { value: true });
  };
  // create a fake namespace object
  // mode & 1: value is a module id, require it
  // mode & 2: merge all properties of value into the ns
  // mode & 4: return value when already ns object
  // mode & 8|1: behave like require
  __webpack_require__.t = function(value, mode) {
    if(mode & 1) value = __webpack_require__(value);
    if(mode & 8) return value;
    if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
    var ns = Object.create(null);
    __webpack_require__.r(ns);
    Object.defineProperty(ns, 'default', { enumerable: true, value: value });
    if(mode & 2 && typeof value != 'string')
      for(var key in value)
        __webpack_require__.d(ns, key, function(key) {
          return value[key];
        }.bind(null, key));
    return ns;
  };
  // getDefaultExport function for compatibility with non-harmony modules
  __webpack_require__.n = function(module) {
    var getter = module && module.__esModule ?
      function getDefault() { return module['default']; } :
      function getModuleExports() { return module; };
    __webpack_require__.d(getter, 'a', getter);
    return getter;
  };
  // Object.prototype.hasOwnProperty.call
  __webpack_require__.o = function(object, property) {
      return Object.prototype.hasOwnProperty.call(object, property);
  };
  // __webpack_public_path__
  __webpack_require__.p = "";
  // Load entry module and return exports
  return __webpack_require__(__webpack_require__.s = "./dom.js");
})
/************************************************************************/
({

/***/ "./dom.js":
/*!****************!*\
  !*** ./dom.js ***!
  \****************/
/*! no static exports found */
/***/ (function(module, exports, __webpack_require__) {

eval(`
  var getUsers = __webpack_require__(/*! ./users */ \"./users.js\").getUsers\n\n
  function addUserToDOM(name) {\n
    const node = document.createElement(\"li\")\n
    const text = document.createTextNode(name)\n
    node.appendChild(text)\n\n
    document.getElementById(\"users\")\n
      .appendChild(node)\n}\n\n
    document.getElementById(\"submit\")\n
      .addEventListener(\"click\", function() {\n
        var input = document.getElementById(\"input\")\n
        addUserToDOM(input.value)\n\n
        input.value = \"\"\n})\n\n
        var users = getUsers()\n
        for (var i = 0; i < users.length; i++) {\n
          addUserToDOM(users[i])\n
        }\n\n\n//# sourceURL=webpack:///./dom.js?`
);}),

/***/ "./users.js":
/*!******************!*\
  !*** ./users.js ***!
  \******************/
/*! no static exports found */
/***/ (function(module, exports) {

eval(`
  var users = [\"Tyler\", \"Sarah\", \"Dan\"]\n\n
  function getUsers() {\n
    return users\n}\n\nmodule.exports = {\n
      getUsers: getUsers\n
    }\n\n//# sourceURL=webpack:///./users.js?`);})
});

複製程式碼

你將留意到這裡有大量魔術般的程式碼(如果你想弄明白究竟發生了什麼,可以閱讀註釋),但有趣的是所有的程式碼被包裹在一個大的 IIFE 中了。這樣一來,通過簡單地利用原來的 IIFE Module Pattern,他們找到了一個可以得到一個優秀模組系統所有優點的同時,避免上文提到的缺點的方法。


未來真正證明了 JavaScript 是一門活的語言。 TC-39 ,JavaScript 標準委員會,一年討論幾次關於這門語言的潛在優化可能性。與此同時,可以清晰看到,模組化對於建立可擴充套件、可維護的 JavaScript 程式碼來說是一個重要的功能。在2013年以前(也可能很久以前),JavaScript 很明顯需要一個標準化的、內建的解決方案來處理模組。這開始了原生 JavaScript 實現模組化的程式。

基於你現在所知道的,如果你被賦予一項任務是為 JavaScript 創設一個模組系統,你設想會是怎樣的? CommonJS 大部分實現是正確的。就像 CommonJS ,每個檔案是一個新的模組並且能清晰定義匯入和匯出的內容。—— 很明顯,這是最重要的一點。CommonJS 的一個問題是它載入模組是同步的,這對伺服器來說是好的,但是對於瀏覽器來說則恰恰相反。其中一個可以做出的改變是支援模組非同步載入,另一個可以做出的改變是定義新的關鍵字,而不是使用一個 require 函式呼叫,因為我們需要的是讓這門語言原生支援該功能。下面讓我們從 importexport 開始。

沒有與上述我們“假設的標準”相距太遠,當TC-39 委員會創造出 "ES Modules"(目前在 JavaScript 中建立模組的標準方法)的時候,他們想到了這個完全相同的設計思路。讓我們來看一下這個語法。

ES Modules

正如上文提到的,為了指定需要從模組匯出的內容,需要使用 export 關鍵字。


// utils.js

// Not exported
function once(fn, context) {
	var result
	return function() {
		if(fn) {
			result = fn.apply(context || this, arguments)
			fn = null
		}
		return result
	}
}

// Exported
export function first (arr) {
  return arr[0]
}

// Exported
export function last (arr) {
  return arr[arr.length - 1]
}

複製程式碼

現在要匯入 firstlast ,你有幾個不同的選擇。其中一個是匯入所有從 utils.js 中匯出的東西。


import * as utils from './utils'

utils.first([1,2,3]) // 1
utils.last([1,2,3]) // 3

複製程式碼

但如果我們並不想匯入所有該模組匯出的東西呢?具體到這個例子而言,如果我們僅僅想匯入 first 但不想匯入 last 呢?這裡可以使用 名命匯入 ( named imports ) (看起來像解構但其實並不是)


import { first } from './utils'

first([1,2,3]) // 1

複製程式碼

ES Modules 很酷的地方不僅僅是可以指定多個普通匯出,而且也可以指定一個預設匯出 ( default export )


// leftpad.js

export default function leftpad (str, len, ch) {
  var pad = '';
  while (true) {
    if (len & 1) pad += ch;
    len >>= 1;
    else break;
  }
  return pad + str;
}

複製程式碼

當你使用預設匯出時,這將改變你引入該模組的方式。不用像之前一樣使用 * 語法或者名命匯入,預設匯出只需要使用 import name from './path 進行匯入。


import leftpad from './leftpad'

複製程式碼

如果有一個模組既有預設匯出,也有其他常規匯出呢?沒錯,你可以用你期待的方式來進行匯入。


// utils.js

function once(fn, context) {
	var result
	return function() {
		if(fn) {
			result = fn.apply(context || this, arguments)
			fn = null
		}
		return result
	}
}

// regular export
export function first (arr) {
  return arr[0]
}

// regular export
export function last (arr) {
  return arr[arr.length - 1]
}

// default export
export default function leftpad (str, len, ch) {
  var pad = '';
  while (true) {
    if (len & 1) pad += ch;
    len >>= 1;
    else break;
  }
  return pad + str;
}

複製程式碼

這樣的話,匯入語法是什麼樣的呢?在這個例子中,同樣,可以以你期望的方式匯入。


import leftpad, { first, last } from './utils'

複製程式碼

非常順手,對吧? leftpad 是預設匯出, firstlast 是常規匯出。

ES Modules 有趣的地方在於,因為是 JavaScript 的原生語法,現代瀏覽器不需要使用打包器就可以支援。看看教程一開始的簡單的 Users 的例子使用 ES Modules後會是什麼樣子的。

完整程式碼見此處


// users.js

var users = ["Tyler", "Sarah", "Dan"]

export default function getUsers() {
  return users
}

複製程式碼

// dom.js

import getUsers from './users.js'

function addUserToDOM(name) {
  const node = document.createElement("li")
  const text = document.createTextNode(name)
  node.appendChild(text)

  document.getElementById("users")
    .appendChild(node)
}

document.getElementById("submit")
  .addEventListener("click", function() {
    var input = document.getElementById("input")
    addUserToDOM(input.value)

    input.value = ""
})

var users = getUsers()
for (var i = 0; i < users.length; i++) {
  addUserToDOM(users[i])
}

複製程式碼

這就是 ES Modules 神奇的地方。 使用 IIFE pattern, 仍然需要通過 script 標籤引入每一個 JS 檔案(並且要按順序)。使用 CommonJS 需要一個打包器,如 Webpack ,然後通過一個 script 標籤引入 bundle.js 檔案。使用 ES Modules, 在現代瀏覽器中,需要做的只是引入主檔案(這個例子中的 dom.js ) 並且在 script 標籤上新增 type='module' 屬性即可。


!DOCTYPE html>
<html>
  <head>
    <title>Users</title>
  </head>

  <body>
    <h1>Users</h1>
    <ul id="users">
    </ul>
    <input id="input" type="text" placeholder="New User"></input>
    <button id="submit">Submit</button>

    <script type=module src='dom.js'></script>
  </body>
</html>

複製程式碼

Tree Shaking

CommonJS 和 ES Modules 之間還有一個不同點上文沒有提及。

使用 CommonJS ,你可以在任何地方 require 一個模組,甚至是有條件地引入。


if (pastTheFold === true) {
  require('./parallax')
}

複製程式碼

因為 ES Modules 是靜態的,匯入宣告必須位於模組的頂層。有條件地引入是不可以的。


if (pastTheFold === true) {
  import './parallax' // "import' and 'export' may only appear at the top level"
}

複製程式碼

這樣一個設計思路的原因是,通過強制為靜態模組,載入器可以靜態分析模組樹,找出實際被使用的程式碼,並且從程式碼束中丟棄沒有被使用的程式碼。這是一個很大的話題,用另外一種說法就是,因為 ES Modules 強制在模組頂層寫匯入宣告,打包器可以快速瞭解程式碼的依賴樹,據此檢查哪些程式碼沒有被使用並將他們從程式碼束中移除。這就叫做 Tree Shaking or Dead Code Elimination

這是一個 stage 3 proposal 關於 動態匯入 ( dynamic imports ) 的介紹,這個語法允許有條件地使用 import() 匯入模組。


我希望通過深入 JavaScript 模組的歷史,不僅可以幫助你更好地理解 ES Modules ,還可以幫你更好理解它們的設計思路。

相關文章