JavaScript 中的模組化

墨紙瀚雲發表於2020-07-28

JavaScript 中的模組化

最早的基於立即執行函式,閉包的模組化

const MountClickModule = function(){
  let num = 0;
  const handleClick = ()=>{
    console.log(++num);
  }

  return {
    countClick:()=>{
      document.addEventListener('click',handleClick)
    }
  }
}();

MountClickModule.countClick();

(function(module{
module.say = ()=>{
  console.log(num)  //undefined
  //do something
}
})(MountClickModule);

MountClickModule.say();

這種閉包的壞處:

  • 擴充套件模組間無法訪問私有變數。
  • 強依賴模組匯入的順序,專案變大後不好維護。

AMD 和 CommonJS 模組化JavaScript 應用

AMD 和 CommonJS 是兩個互相競爭的標準,均可定義 JavaScript 模組。除了語法和原理的區別之外,主要區別是 AMD 的設計理念是明確基於瀏覽器,而 CommonJS 的設計是面向通用 JavaScript 環境

使用 AMD 定義模組依賴

AMD 非同步模組定義規範制定了定義模組的規則,這樣模組和模組的依賴可以被非同步載入。這和瀏覽器的非同步載入模組的環境剛好適應(瀏覽器同步載入模組會導致效能、可用性、除錯和跨域訪問等問題)。

目前,AMD 最流行的實現是 RequireJS

  define("alpha", ["require""exports""beta"], function (require, exports, beta{
       exports.verb = function() {
           return beta.verb();
           //Or:
           return require("beta").verb();
       }
   });

AMD 提供名為 aplha 的函式,它接收一下引數:

  • 第一個引數,id,是個字串。它指的是定義中模組的名字,這個引數是可選的。如果沒有提供該引數,模組的名字應該預設為模組載入器請求的指定指令碼的名字。
  • 第二個引數,dependencies,是個定義中模組所依賴模組的陣列。
  • 第三個引數是初始化模組的工廠函式,該函式接收dependencies作為引數 AMD非同步獲取依賴,以避免阻塞,如果依賴來自伺服器,那麼這個過程將花費一些時間,知道依賴全部載入完成後,呼叫模組的工廠函式,並傳入所有的依賴。

可以看出,AMD 有一下幾項有點:

  • 非同步載入模組,避免阻塞。
  • 自動處理依賴,我們無需考慮模組的引入順序。
  • 在同一個檔案中可以定義多個模組。

CMD

CMD 是 SeaJS 在推廣過程中對模組定義的規範化產出,在 CMD 規範中,一個模組就是一個檔案。程式碼的書寫格式如下:

define(function (require, exports, module{
  const foo = require('./foo')
  a.doSomething()
  // ...
  const bar = require('./bar'// 依賴可以就近書寫
  b.doSomething()
  // do something else
})

CommonJS

AMD 的設計明確基於瀏覽器,而 CommonJS 的設計是面向通用 JavaScript 環境。CommonJS 目前在 Nodejs 社群中具有最多的使用者。CommonJS 使用基於檔案的模組,所以每個檔案中都只能定義一個模組,CommonJs 提供變數 module,該變數具有屬性 exports,通過 exports 很容易擴充套件屬性。最後,module.exports 作為模組的公共介面。

const beta = require('beta');
function alpha(){
    return beta.verb();
    //Or:
    return require("beta").verb();
}

module.exports = alpha;

CommonJS 要求一個檔案就是一個模組,檔案中的程式碼就是模組的一部分,所以不需要使用立即執行函式來包裝變數,在模組中定義的變數都是安全的再模組中,不會洩露到全域性作用域。只有通過 module.exports 物件暴露的物件或函式才可以在函式外部訪問 CommonJS 具有以下特點:

  • 基於檔案系統。
  • 引入模組時檔案同步載入,可以訪問模組的公共介面。
  • 模組載入相對更快 這是 CommonJS 在服務端更流行的原因。

ES6 模組的匯入匯出

ES6 模組結合了CommonJS 和 AMD 的有點,具體如下:

  • 與 CommonJS 類似,ES6模組語法相對簡單,並且基於檔案(每一個檔案就是一個模組)
  • 與 AMD 類似,ES6 模組支援非同步載入模組。

既 ES6 結合了兩種模組化的有點,基於檔案系統,既支援非同步也支援同步,因為瀏覽器並沒有實現 ES6 的模組化 API 所以具體是非同步還是同步取決於loader api

ES6 模組的主要思想是必須顯示的使用標誌符匯出模組,才能從外部訪問模組。其他標誌符,甚至在最頂級作用域中定義的識別符號,只能在模組中使用。 ES6 引入兩個關鍵字:

  • export ---- 從模組外部指定識別符號。
  • import ---- 匯入模組識別符號。

從index.js模組中匯出:

  const hello = 'hello';
  export const name = 'yunfly'

  export function sayHi(){
    return `${hello} ${name}!`
  }

也可以在模組最後一起匯出:

  // foo.js
  const hello = 'hello';
  export const name = 'yunfly'

  export function sayHi(){
    return `${hello} ${name}!`
  }

  export { name, sayHi }

  // export { name as firstName, sayHi }
  

  // bar.js

  // 使用 as 設定導如別名
  import { name as firstName, sayHi } from 'foo'

  console.log(name)
  sayHi()

  //bar2.js
  // 匯出全部識別符號:
  import * as sayModule from 'foo';

  console.log(sayModule.name)
  sayModule.sayHi()

預設匯出 export default 被認為是有害的

// foo.js
class Foo {}

export default Foo


// bar.js
import Foo from './foo';

這存在一些可維護性的問題:

  • 如果你在 foo.ts 裡重構 Foo,在 bar.ts 檔案中,它將不會被重新命名;
  • 如果你最終需要從 foo.ts 檔案中匯出更多有用的資訊(在你的很多檔案中都存在這種情景),那麼你必須兼顧匯入語法。
  • 在 ts 中預設匯出的可發現性非常差,你不能智慧的辨別一個模組它是否有預設匯出。

文章參考:《JavaScript忍者祕籍》

相關文章