摘要:在日常進行JS/TS專案開發的時候,經常會遇到require某個依賴和module.exports來定義某個函式的情況。就很好奇Modules都代表什麼和有什麼作用呢。
本文分享自華為雲社群《JS/TS專案裡的Module都是什麼?都有幾種形式?loaders和bundlers的區別是什麼?》,作者: gentle_zhou 。
在日常進行JS/TS專案開發的時候,經常會遇到require某個依賴和module.exports來定義某個函式的情況。再加上在日常審視程式碼的時候,發現tsconfig.json檔案裡有一個"compilerOptions",裡面關於module引入的是"commonjs",就很好奇Modules都代表什麼和有什麼作用呢。
什麼是Module?
一個Module(模組)顧名思義就是一段可以重複利用的程式碼(通常是一個特性,或則一些特性的集合;可以是一個檔案或則多個檔案/資料夾的集合),它封裝了內部程式碼實現的細節並曝露一個公開的API,讓其他程式碼可以輕易地載入和使用。
為什麼我們需要Modules?
技術上來說,其實完成一個JS/TS專案,我們並不需要模組,直接上手寫程式碼也是可以的。但就像在JAVA、Python軟體專案裡,不引入依賴一樣,會導致程式設計師們重複寫很多相同的程式碼。
引入Module,為的就是可以應對JS/TS專案的程式碼越來越龐大,越來越複雜的情形。我們需要使用軟體工程的方法,來管理JS/TS專案的業務邏輯。
在JS/TS專案中,模組應該允許我們實現以下功能:
- 抽象程式碼:將功能委託給專門的庫,這樣我們就不必瞭解它們內部實際如何實現的(無論多複雜)
- 封裝程式碼:如果我們不想再更改程式碼了,可以將程式碼隱藏在模組中
- 重用程式碼:避免反覆編寫相同的程式碼
- 管理依賴:在不重寫程式碼的情況下,輕鬆改變依賴關係
幾種常見的Module形式
在 ES6 Module 出現之前,在ES5時期,JS並沒有提供一個官方的定義模組的規則;因此JavaScript 社群裡的天才程式設計師們嘗試了各種形式來定義模組,以達到“在現有的執行環境下,可以實現模組效果”的目的。
一些非常有名的模組形式:
- CommonJS
CommonJS形式是用在Node.js環境裡的,我在文章開頭提到的require和module.exports就是CommonJS裡用來定義依賴和模組的:
var dep1 = require('./dep1'); module.exports = function(){ // ...}
- Asynchronous Module Definition (AMD)
AMD(官方github連結)則是用在瀏覽器中的,顧名思義這個形式是非同步的,其中用define函式來定義模組:
// 一個依賴陣列&一個工廠函式以引數的形式呼叫define函式 define(['dep1', 'dep2'], function (dep1, dep2) { //通過返回一個值來定義模組值 return function () {}; });
- Universal Module Definition (UMD)
UMD則是可以用在瀏覽器和Node.js中,是通用的:
(function (root, factory) { if (typeof define === 'function' && define.amd) { // AMD. 以同步模組的方式註冊. define(['b'], factory); } else if (typeof module === 'object' && module.exports) { // Node節點. 不能和嚴格意義上的CommonJS一起使用,但是類似CommonJS的環境裡是支援使用module.expoerts的,就像node. module.exports = factory(require('b')); } else { // 瀏覽器 globals (根節點是window) root.returnExports = factory(root.b); } }(this, function (b) { // 返回一個值來定義module export;這裡返回的是一個物件,但是模組其實可以返回一個函式作為exported value. return {}; }));
以及現在出現的官方ES6 模組形式,一種原生的模組形式。它用export來輸出模組的公開API:
// 輸出函式 export function sayHello(){ console.log('Hello'); }
我們可以使用import和as來引入部分程式碼到模組裡:
import { sayHello as say } from './lib'; say(); // 輸出Hello
或則直接在一開始引入整個模組:
import * as lib from './lib'; lib.sayHello(); // 輸出 Hello
Module loaders和Module bundlers的區別
兩者都是為了讓我們編寫模組化JS/TS應用的時候更方便快捷。
Module loaders
模組載入器用來解析並載入以特定模組格式編寫的模組,通常是一些庫;可以載入、解釋和執行使用特定模組格式/語法定義的JavaScript模組,比如AMD或Common JS。
在編寫模組化JS/TS應用程式時,通常每個模組都有一個檔案。因此,當編寫由數百個模組組成的應用程式時,要確保所有檔案都以正確的順序包含進去可能會非常痛苦。所以,如果有載入器會為你負責依賴管理,確保所有模組在應用程式執行時被載入,那會輕鬆容易很多。
模組載入器是在執行時(runtime)執行的:
- 在瀏覽器中載入模組載入器
- 告訴模組載入器載入哪個主應用檔案
- 模組載入器下載並解析主應用檔案
- 模組載入器根據需要去下載檔案
如果你試著在瀏覽器的開發人員控制檯中開啟network選項卡,將看到許多檔案是按需由模組載入器載入的:
一些流行的模組載入器的例子如下:
- Require JS: AMD格式的模組載入器
- System JS: AMD, Common JS, UMD或System.register格式的模組載入器
Module bundlers
模組繫結器相當於是模組載入器的替代品;基本上,它們做的事情是一樣的(管理和載入相互依賴的模組)。
但模組繫結器和載入器不同的地方是,它並非是在執行時執行的,而是作為應用程式構建的一部分執行(在build的時候執行);而且它是在瀏覽器中載入的。因此,繫結器在執行程式碼之前會將所有模組合併到一個檔案/bundle中(比如叫bundle.js),而不是在程式碼執行時再去載入出現的依賴項。比如現在流行的兩個bundlers:Webpack(AMD,Common JS, es6模組的bundler)和Browserify(Common JS模組的bundler)。
什麼時候更適合用哪個呢?
這個問題的答案取決於JS/TS應用程式的結構與大小。
使用bundler的主要優點是,它讓瀏覽器需要下載的檔案變少了很多,這可以給我們的應用程式帶來效能上的優勢(因為減少了載入所需的時間);但是取決於應用程式的模組數量,並不是說用bundler就一定是最好的。對於那種大型應用(有很多模組),模組載入器可以提供更好的效能,因為bundler在一開始載入一個巨大的單檔案會阻礙應用的啟動。
如何選取,其實只需要我們進行測試比較一下即可~
參考連結
- https://v8.dev/features/modules
- https://www.geeksforgeeks.org/node-js-modules/
- https://www.jvandemo.com/a-10-minute-primer-to-javascript-modules-module-formats-module-loaders-and-module-bundlers/
- https://stackoverflow.com/questions/38864933/what-is-difference-between-module-loader-and-module-bundler-in-javascript