【譯】JavaScript 模塊:從當即執行函數 ( IIFEs ) 到 CommonJS 再到 ES6 模塊

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

我教授 JavaScript 給不少不一樣的人很長一段時間了。這門語言廣泛最難懂的概念就是模塊系統。固然,這是有緣由的,由於模塊在 JavaScript 中有着一個奇怪的歷史。在這篇文章中,咱們將重溫這段歷史,你將學習到過去的模塊化方式,以更好地理解現在 JavaScript 模塊的工做原理。html

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

下面咱們以一塊手錶爲例子。node

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

可複用性 ( Reusability )

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

可組合性 ( Composability )

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

槓桿做用 ( Leverage )

設想一下整個製造流程。這個公司並非在製造手錶,而是在製造個別的手錶零件。他們既能夠選擇由本身公司來生產,也能夠選擇將這項工做外包出去,利用其餘工廠進行生產,這都沒有問題。無論零件在哪裏生產,最關鍵的一點是每個零件最後可以組合起來造成一塊手錶便可。git

獨立性 ( Isolation )

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

可組織性 ( Organization )

可組織性是每一個零件具備清晰的做用範圍的副產品。在此基礎上,可組織性是天然而然產生的。github


咱們已經看到模塊化應用在咱們平常生活中的事物,好比手錶上的明顯的好處,若是將模塊化應用到軟件上會怎麼樣呢?一樣的方法將獲得一樣的好處,就像手錶的設計同樣,咱們應該將軟件設計成由不一樣的功能單一的有着特定用途和清晰的做用範圍的小塊組成。在軟件中,這些小塊被稱爲模塊。在這一點上,一個模塊聽上去可能和一個函數或者一個 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 ,還能夠幫你更好理解它們的設計思路。

相關文章
相關標籤/搜索