【JS基礎】一文看懂前端模組化規範

諾頓發表於2019-05-05

前言

前端的模組化之路經歷了漫長的過程,這裡根據大佬們寫的文章,將模組化規範部分做了彙總和整理,想詳細瞭解的小夥伴可以看浪裡行舟大神寫的前端模組化詳解(完整版),希望讀完的小夥伴能有些收穫,也希望覺得有用的小夥伴可以點個贊,筆芯。

什麼是模組

  • 將一個複雜的程式依據一定的規則(規範)封裝成幾個塊(檔案), 並進行組合在一起
  • 塊的內部資料與實現是私有的,只是向外部暴露一些介面(方法)與外部其它模組通訊

CommonJS

Node 應用由模組組成,採用 CommonJS 模組規範。每個檔案就是一個模組,有自己的作用域。在一個檔案裡面定義的變數、函式、類,都是私有的,對其他檔案不可見。在伺服器端,模組的載入是執行時同步載入的;在瀏覽器端,模組需要提前編譯打包處理。

CommonJS規範載入模組是同步的,也就是說,只有載入完成,才能執行後面的操作。

基本語法:

  • 暴露模組:module.exports = valueexports.xxx = value
  • 引入模組:require(xxx),如果是第三方模組,xxx為模組名;如果是自定義模組,xxx為模組檔案路徑

但是,CommonJs有一個重大的侷限使得它不適用於瀏覽器環境,那就是require操作是同步的。這對伺服器端不是一個問題,因為所有的模組都存放在本地硬碟,可以同步載入完成,等待時間就是硬碟的讀取時間。但是,對於瀏覽器,這卻是一個大問題,因為模組都放在伺服器端,等待時間取決於網速的快慢,可能要等很長時間,瀏覽器處於”假死”狀態。

因此,瀏覽器端的模組,不能採用”同步載入”(synchronous),只能採用”非同步載入”(asynchronous),這就是AMD規範誕生的背景。

AMD

特點:非同步載入模組,允許指定回撥函式,瀏覽器端一般採用AMD規範

代表作:require.js

用法:

    //定義沒有依賴的模組
    define(function(){
       return 模組
    })

    //定義有依賴的模組
    define(['module1', 'module2'], function(m1, m2){
       return 模組
    })
    
    //引入使用模組
    require(['module1', 'module2'], function(m1, m2){
       //使用m1/m2
    })
複製程式碼

CMD

特點:專門用於瀏覽器端,模組的載入是非同步的,模組使用時才會載入執行

代表作:Sea.js

用法:

    //定義沒有依賴的模組
    define(function(require, exports, module){
      exports.xxx = value
      module.exports = value
    })
    
    //定義有依賴的模組
    define(function(require, exports, module){
      //引入依賴模組(同步)
      var module2 = require('./module2')
        //引入依賴模組(非同步)
        require.async('./module3', function (m3) {
        })
      //暴露模組
      exports.xxx = value
    })
    
    //引入使用模組
    define(function (require) {
      var m1 = require('./module1')
      var m4 = require('./module4')
      m1.show()
      m4.show()
    })
複製程式碼

CMD與AMD區別

AMD和CMD最大的區別是對依賴模組的執行時機處理不同,而不是載入的時機或者方式不同,二者皆為非同步載入模組。

AMD依賴前置,js可以方便知道依賴模組是誰,立即載入;

而CMD就近依賴,需要使用把模組變為字串解析一遍才知道依賴了那些模組,這也是很多人詬病CMD的一點,犧牲效能來帶來開發的便利性,實際上解析模組用的時間短到可以忽略。

一句話總結: 兩者都是非同步載入,只是執行時機不一樣。AMD是依賴前置,提前執行,CMD是依賴就近,延遲執行。

UMD

UMD是AMD和CommonJS的糅合:

AMD模組以瀏覽器第一的原則發展,非同步載入模組。

CommonJS模組以伺服器第一原則發展,選擇同步載入,它的模組無需包裝(unwrapped modules)。

這迫使人們又想出另一個更通用的模式UMD (Universal Module Definition),希望解決跨平臺的解決方案。

UMD先判斷是否支援Node.js的模組(exports)是否存在,存在則使用Node.js模組模式。

在判斷是否支援AMD(define是否存在),存在則使用AMD方式載入模組。

    (function (window, factory) {
        if (typeof exports === 'object') {
         
            module.exports = factory();
        } else if (typeof define === 'function' && define.amd) {
         
            define(factory);
        } else {
         
            window.eventUtil = factory();
        }
    })(this, function () {
        //module ...
    });
複製程式碼

ES6模組化

ES6 模組的設計思想是儘量的靜態化,使得編譯時就能確定模組的依賴關係,以及輸入和輸出的變數。CommonJS 和 AMD 模組,都只能在執行時確定這些東西。比如,CommonJS 模組就是物件,輸入時必須查詢物件屬性。

ES6 Module預設目前還沒有被瀏覽器支援,需要使用babel,在日常寫demo的時候經常會顯示這個錯誤:

圖片描述

ES6模組使用import關鍵字匯入模組,export關鍵字匯出模組:

    /** 匯出模組的方式 **/
    
    var a = 0;
    export { a }; //第一種
       
    export const b = 1; //第二種 
      
    let c = 2;
    export default { c }//第三種 
    
    let d = 2;
    export default { d as e }//第四種,別名
    
    /** 匯入模組的方式 **/
    
    import { a } from './a.js' //針對export匯出方式,.js字尾可省略
    
    import main from './c' //針對export default匯出方式,使用時用 main.c
    
    import 'lodash' //僅僅執行lodash模組,但是不輸入任何值
複製程式碼

命名式匯出與預設匯出

export {<變數>}這種方式一般稱為 命名式匯出 或者 具名匯出,匯出的是一個變數的引用

export default這種方式稱為 預設匯出 或者 匿名匯出,匯出的是一個

舉例:

    // a.js
    let x = 10
    let y = 20
    setTimeout(()=>{
        x = 100
        y = 200
    },100)
    export { x }
    export default y

    // b.js
    import { x } from './a.js'
    import y from './a.js'
    setTimeout(()=>{
        console.log(x,y) // 100,20
    },100)
複製程式碼

ES6 模組與 CommonJS 模組的差異

① CommonJS 模組輸出的是一個值的拷貝,ES6 模組輸出的是值的引用。

CommonJS 模組輸出的是值的拷貝,也就是說,一旦輸出一個值,模組內部的變化就影響不到這個值。而且,CommonJS 模組無論載入多少次,都只會在第一次載入時執行一次,以後再載入,返回的都是第一次執行結果的快取,除非手動清除系統快取。

ES6 模組的執行機制與 CommonJS 不一樣,JS 引擎對指令碼靜態分析的時候,遇到模組載入命令import,就會生成一個只讀引用,等到指令碼真正執行時,再根據這個只讀引用,到被載入的那個模組裡面去取值。換句話說,ES6 的import有點像 Unix 系統的“符號連線”,原始值變了,import載入的值也會跟著變。因此,ES6 模組是動態引用,並且不會快取值,模組裡面的變數繫結其所在的模組。

② CommonJS 模組是執行時載入,ES6 模組是編譯時輸出介面。

CommonJS 載入的是一個物件(即module.exports屬性),該物件只有在指令碼執行完才會生成。即在輸入時是先載入整個模組,生成一個物件,然後再從這個物件上面讀取方法,這種載入稱為“執行時載入”。

例如:

    // CommonJS模組
    let { stat, exists, readFile } = require('fs');
    
    // 等同於
    let _fs = require('fs');
    let stat = _fs.stat;
    let exists = _fs.exists;
    let readfile = _fs.readfile;
複製程式碼

上面程式碼的實質是整體載入fs模組(即載入fs的所有方法),生成一個物件(_fs),然後再從這個物件上面讀取 3 個方法。因為只有執行時才能得到這個物件,導致完全沒辦法在編譯時做“靜態優化”。

ES6 模組不是物件,它的對外介面只是一種靜態定義,在程式碼靜態解析階段就會生成。通過export命令顯式指定輸出的程式碼,import時採用靜態命令的形式。即在import時可以指定載入某個輸出值,而不是載入整個模組,這種載入稱為“編譯時載入”或者“靜態載入”。

    // ES6模組
    import { stat, exists, readFile } from 'fs';
複製程式碼

上面程式碼的實質是從fs模組載入 3 個方法,其他方法不載入。即 ES6 可以在編譯時就完成模組載入,效率要比 CommonJS 模組的載入方式高。當然,這也導致了沒法引用 ES6 模組本身,因為它不是物件

由於 ES6 模組是編譯時載入,使得靜態分析成為可能。有了它,就能進一步拓寬 JavaScript 的語法,比如引入巨集(macro)和型別檢驗(type system)這些只能靠靜態分析實現的功能。

除了靜態載入帶來的各種好處,ES6 模組還有以下好處:

  • 不再需要UMD模組格式了,將來伺服器和瀏覽器都會支援 ES6 模組格式。目前,通過各種工具庫,其實已經做到了這一點。
  • 將來瀏覽器的新API 就能用模組格式提供,不再必須做成全域性變數或者navigator物件的屬性。
  • 不再需要物件作為名稱空間(比如Math物件),未來這些功能可以通過模組提供。

總結

  1. CommonJS規範主要用於服務端程式設計,載入模組是同步的,這並不適合在瀏覽器環境,因為同步意味著阻塞載入,瀏覽器資源是非同步載入的,因此有了AMD、CMD解決方案。
  2. AMD規範在瀏覽器環境中非同步載入模組,而且可以並行載入多個模組。不過,AMD規範開發成本高,程式碼的閱讀和書寫比較困難,模組定義方式的語義不順暢。
  3. CMD規範與AMD規範很相似,都用於瀏覽器程式設計,依賴就近,延遲執行,可以很容易在Node.js中執行。不過,依賴SPM打包,模組的載入邏輯偏重。
  4. ES6 在語言標準的層面上,實現了模組功能,而且實現得相當簡單,完全可以取代 CommonJS 和 AMD 規範,成為瀏覽器和伺服器通用的模組解決方案。

以上是本篇文章的內容,歡迎大家提出自己的想法,我們一起學習進步,與君共勉。

參考資料

相關文章