摘要:在日常進行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