前端模組化淺析

position_柚子發表於2017-08-31

平時寫程式碼的時候,知道如何匯出變數,如何引入變數。可見模組化就在我們的身邊,可是為什麼前端會引入模組化的概念,以及為什麼有同步載入和非同步載入呢?

為什麼要模組化

在之前的專案中,如果沒有模組化的概念,很多變數都有重名或者不小心重新賦值的危險。而且用 script 有可能阻塞 HTML 的下載或者渲染,影響使用者體驗。

在平時編碼中,我們都習慣把一些通用的方法提出來放在一個檔案裡,哪個地方需要用到就引用,這樣能夠很好的梳理頁面的邏輯,維護程式碼的成本也降低了不少。所以模組化給我們帶來的好處是顯而易見的。

  • 分離: 程式碼需要分離成小塊,以便能為人所理解。
  • 可組合性: 在一個檔案中編碼,被許多其他檔案重複使用。這提升了程式碼庫的靈活性。
  • 解決全域性變數重名問題
  • 提高複用性

現有的一些模組化方案有以下幾種:

  • ES 6 模組
  • Commonjs
  • AMD
  • CMD

下面我就自身的理解對這幾種方案做一個對比和總結:

ES6 Module

  • 編譯時就能確定模組的依賴關係

ES6 模組遇到 import 命令時,不會去執行模組,而是生成一個引用,等用到的時候,才去模組中取值。因為是動態引用,所以不存在快取的問題。可以看一下下面的例子:

// util.js
export let env = 'qa';
setTimeout(() => env = 'local', 1000);

// main.js
import {env} from './util';
console.log('env:', env);

setTimeout(() => console.log('new env:', env), 1500);複製程式碼

執行 main.js,會輸出下面的結果:

// env: qa
// new env: local複製程式碼

可以看出 ES6 模組是動態的取值,不會快取執行的結果。

目前瀏覽器尚未支援 ES6 模組 ,所以需要使用 babel 轉換,大家可以在 Babel 提供的 REPL 線上編譯器 中檢視編譯後的結果。

// es 6
import {add} from './config';

// es 5
'use strict';
var _config = require('./config');複製程式碼

可以看出,最後轉換成 require 的方式了。ES6 模組在瀏覽器和伺服器端都可以用,ES6 模組的設計思想是儘量的靜態化,使得編譯時就能確定模組的依賴關係,以及輸入和輸出的變數。

import 命令具有提升效果,會提升到整個模組的頭部,首先執行,所以不能把 import 寫在表示式裡面。這和 ES 6 模組的概念不符合。

CommonJS

  • 用於伺服器端
  • 只能在執行時確定
  • 同步載入
  • 模組載入的順序,按照其在程式碼中出現的順序。

node 的模組遵循 CommonJS 規範。在伺服器端,依賴是儲存在本地硬碟的,所以讀取的速度非常快,使用同步載入不會有什麼影響。

看一下 CommonJS 的語法:

// header.js
module.exports = {
    title: '我是柚子'
};

// main.js
var header = require('./header');複製程式碼

module

這裡的 module 代表的是當前模組,它是一個物件,把它列印出來是下面的結果:

{
Module {
  id: '/Users/yanmeng/2017FE/css-animation/js/b.js',
  exports: { item: 'item' },
  parent:
   Module {
     id: '.',
     exports: {},
     parent: null,
     filename: '/Users/yanmeng/2017FE/css-animation/js/main.js',
     loaded: false,
     children: [ [Circular] ],
     paths:
      [ '/Users/yanmeng/2017FE/css-animation/js/node_modules',
        '/Users/yanmeng/2017FE/css-animation/node_modules',
        '/Users/yanmeng/2017FE/node_modules',
        '/Users/yanmeng/node_modules',
        '/Users/node_modules',
        '/node_modules' ] },
  filename: '/Users/yanmeng/2017FE/css-animation/js/b.js',
  loaded: false,
  children: [],
  paths:
   [ '/Users/yanmeng/2017FE/css-animation/js/node_modules',
     '/Users/yanmeng/2017FE/css-animation/node_modules',
     '/Users/yanmeng/2017FE/node_modules',
     '/Users/yanmeng/node_modules',
     '/Users/node_modules',
     '/node_modules' 
    ] 
}複製程式碼
  • id 是該模組的 id
  • loaded 代表改模組是否載入完畢
  • exports 是一個物件,裡面有模組輸出的各個介面。

之後呼叫這個模組的時候,就會從 exports 中取值,即使再執行,也不會再執行改模組,而是從快取中取值,返回的是第一次執行的結果,除非手動清除快取。

// 刪除指定模組的快取
delete require.cache[moduleName];

// 刪除所有模組的快取
Object.keys(require.cache).forEach(function(key) {
  delete require.cache[key];
})複製程式碼

快取是根據絕對路徑識別模組的,如果同一個模組放在不同的路徑下,還是會重新載入這個模組。

require

require 命令第一次執行的時候,會載入並執行整個指令碼,然後在記憶體中生成此指令碼返回的 exports 物件。

ES6 模組與 CommonJS 模組的差異

  • CommonJS 模組輸出的是一個值的拷貝,ES6 模組輸出的是值的引用。
  • CommonJS 模組是執行時載入,ES6 模組是編譯時輸出介面。

ES6 模組是動態引用,並且不會快取值。

ES6 模組在對指令碼靜態分析的時候,遇到 import 就會生成一個只讀引用,等到指令碼真正執行的時候,再根據這個只讀引用,到被載入的那個模組裡取值,所以說 ES6 模組是動態引用。
從依賴中引入的模組變數是一個地址引用,是隻讀的,可以為它新增屬性,可是不能重新賦值。

// lib.js
export let obj = {};

// main.js
import { obj } from './lib';

obj.prop = 123; // OK
obj = {}; // TypeError複製程式碼

AMD

又稱非同步載入模組(Asynchronous Module Definition)

  • 依賴前置
  • 比較適合瀏覽器環境
  • 實現 js 檔案的非同步載入,避免網頁失去響應
  • 管理模組之間的依賴性,便於程式碼的編寫和維護
  • 代表庫: RequireJS

如果在瀏覽器環境,就需要在服務端載入模組,那麼採用同步載入的方法就會影響使用者體驗,所以瀏覽器端一般採用 AMD 規範。

它採用非同步方式載入模組,模組的載入不影響它後面語句的執行。所有依賴這個模組的語句,都定義在一個回撥函式中,等到載入完成之後,這個回撥函式才會執行。

 // lib.js
  define('[./util.js]', function(util){
      function bar() {
          util.log('it is sunshine');
      };
      return {
          bar: bar
      };
  });
  
// main.js
require(['./lib.js'], function(lib){
    console.log(lib.bar());
})複製程式碼

CMD

  • 依賴就近
  • 需要用到依賴的時候才申明
  • 代表庫: Sea.js

Sea.js 實現了這個規範,Sea.js 遇到依賴後只會去下載 JS 檔案,並不會執行,而是等到所有被依賴的 JS 指令碼都下載完以後,才從頭開始執行主邏輯。因此被依賴模組的執行順序和書寫順序完全一致。

define(function(require, exports, module) {
    var a = require('./a')
    a.doSomething()
    // ...
    var b = require('./b') 
    b.doSomething()
    // ...
})複製程式碼

本文只是淺顯的介紹了一些模組的概念和用法,關於 ES6 模組、 CommonJs 的迴圈載入和 ES 6 模組和 CommonJs的互相引用,大家可以動手實踐一下,會受益匪淺。

參考:

相關文章