深度閱讀<Javascript Modules 從IIFEs 到CommonJS 到 ES6 Modules>

skinner發表於2019-01-20

原文:tylermcginnis.com/javascript-…

深度閱讀<Javascript Modules 從IIFEs 到CommonJS 到 ES6 Modules>

本文通過現代社會工廠生產一塊手錶的過程,引申出如何構建一個物理邏輯都隔離的模組,論述了其包含的思想原則。另外從js發展過程中為實現這些原則而不斷做出的努力和嘗試,通過了解這些歷史,我們能更深入瞭解ES Modules的設計原則,希望能夠對我們平常編寫程式碼提供一些啟發。

一塊手錶由成千上萬個零部件構成,每一個零部件都有其自身的作用,並且如何與其它零部件搭配都有比較清晰的規定,把它們組裝在一起就是一塊手錶,那這其中能給我們帶來哪些啟示呢?

  • 可複用性
  • 可組合型
  • 中心化
  • 獨立性

延伸到實際js開發中,對每個檔案或者程式碼塊的要求就是能夠被重複使用,具有相對獨立性(自己負責自己的一塊),能夠和相關模組進行組合,且整個模組有一個統一的排程中心負責去組合這些獨立的模組。

IIFE

我們先看下原始時代,即Jquery還是巔峰的時代,那個時候我們是如何分割程式碼的,以下就是一個簡單的增加使用者,列舉使用者的一個curd例子

// 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>
複製程式碼

看著程式碼好像我們是把檔案分割開了,但實際上並沒有,這種方式只是物理上看起來把專案分成多個模組,而其實他們都是掛靠在window物件上的,執行程式碼檢視即可發現。那容易帶來的問題就是,第三方可以隨意去修改它們,回想下,是不是不符合模組獨立性原則。同時這樣也容易對window物件造成汙染。

然後緊接著,我們想到既然不能放在window物件上,我們就自己定義一個變數,比如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>
複製程式碼

我們首先不討論名稱空間也容易被汙染的問題,這種方式,我們的使用者列表現在不容易被外部篡改以及增加使用者的邏輯都放在App物件下,獨立性有了保證,唯一多了usersWrapperdomWrapper兩個包裹函式需要主動去呼叫下。相比之前有了很大改進。但這兩個函式還是暴露在window物件上,後面就有了立即執行函式-IIFE。

// App.js
var APP = {}
複製程式碼
// 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])
  }
})()
複製程式碼

現在除了App變數還暴露在window物件上之外,另外兩個函式都有了自己的獨立的作用域,外部不能修改它們。雖然這種方式不是很完美,但是還是邁進了一大步。

CommonJS

後面Node.js出來了,有個CommonJS規範,能夠匯出一個方法或變數,在需要的檔案中能夠匯入一個方法或變數,但它在現代瀏覽器中無法執行,且它是同步的,無法滿足現代瀏覽器對效能的要求。基於此社群也出現了很多方案,最火的莫過於webpack,通過webpack你能將基於CommonJS規範編寫的程式碼打包成一個bundle,在入口index.html檔案中直接引用這個bundle即可。然而通過檢視webpack編譯後的程式碼你會發現本質上運用的還是IIFE模式,且最關鍵的還是CommonJS是同步的,不支援非同步載入,另外就是它是執行時載入,無法做靜態分析導致類如tree shaking等特性無法被滿足。

(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?`);})
});
複製程式碼

ES Modules

為了解決以上種種問題,TC-39釋出了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])
}
複製程式碼
<!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 modules 和 ES Modules有一個最大的不同,通過CommonJS你能匯入任何模組在任何地點

if (pastTheFold === true) {
  require('./parallax')
}
複製程式碼

而ES Modules因為是靜態的,只能在檔案最開頭匯入

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

為什麼這麼設計呢,原因是靜態分析,我們能夠分析出匯入的模組,如果有些模組沒有被使用,我們通過tree shaking去除這些無用的程式碼,從而減少程式碼體積,進而提升執行效能,而CommonJS是動態分析的,就無法做到這一點,這也是為啥webpack後面版本才只是tree skaking特性的原因,因為它必須依賴於ES6 Modules靜態編譯特性。

Export Default的問題

Es Modules匯出有export 和 export default兩種方式,它們區別如下:

  • export與export default均可用於匯出常量、函式、檔案、模組等
  • 在一個檔案或模組中,export、import可以有多個,export default僅有一個
  • 通過export方式匯出,在匯入時要加{ },export default則不需要
  • export能直接匯出變數表示式,export default不行。

我這裡主要想講的是儘量減少export default的使用,理由如下:

  1. export default因為是整體匯出,tree shaking無法分析哪些使用哪些沒使用,從而無法減少無效程式碼
  2. 個人覺得程式碼應該符合一致性原則,由於export default匯出在引入的時候可以隨意命名使用變數,在團隊分工從事的情況下,容易造成引入同一個模組命名不一樣帶來的程式碼前後不一致的問題。

以上就是我對整篇文章的深度閱讀,希望這邊文章對您在認識模組系統上有一定的幫助,如果喜歡我的文章,歡迎您的點贊!

相關文章