眾所周知,js在前端開發中的地位。學好它,真的很重要。
下面這篇文章,介紹一下模組化。
到底什麼是模組化、模組化開發呢?
事實上模組化開發最終的目的是將程式劃分成一個個小的結構。
這個結構中編寫屬於自己的邏輯程式碼,有自己的作用域,不會影響到其他的結構。
這個結構可以將自己希望暴露的變數、函數、物件等匯出給其結構使用。
也可以通過某種方式,匯入另外結構中的變數、函數、物件等。
上面說提到的結構,就是模組;按照這種結構劃分開發程式的過程,就是模組化開發的過程。
在網頁開發的早期,Brendan Eich開發JavaScript僅僅作為一種指令碼語言,做一些簡單的表單驗證或動畫實現等,那個時候程式碼還是很少的:
這個時候我們只需要講JavaScript程式碼寫到
並沒有必要放到多個檔案中來編寫。
但是隨著前端和JavaScript的快速發展,JavaScript程式碼變得越來越複雜了:
ajax的出現,前後端開發分離,意味著後端返回資料後,我們需要通過JavaScript進行前端頁面的渲染。
SPA的出現,前端頁面變得更加複雜:包括前端路由、狀態管理等等一系列複雜的需求需要通過JavaScript來實現。
包括Node的實現,JavaScript編寫複雜的後端程式,沒有模組化是致命的硬傷。
所以,模組化已經是JavaScript一個非常迫切的需求。所以ES6(2015)才推出了自己的模組化方案。
在此之前,為了讓JavaScript支援模組化,湧現出了很多不同的模組化規範:AMD、CMD、CommonJS等。
比如命名衝突的問題。
通過立即函數呼叫表示式(IIFE)來解決上面的問題。因為函數有自己的作用域,不會造成不同檔案命名衝突。
// a.js var moduleA = (function() { var name = "llm" var age = 22 var isFlag = true return { name: name, isFlag: isFlag } })()
// b.js var moduleB = (function() { var name = "zh" var isFlag = false return { name: name, isFlag: isFlag } })()
// 使用 moduleA.name moduleB.name
但是,我們其實帶來了新的問題:
我必須記得每一個模組中返回物件的命名,才能在其他模組使用過程中正確的使用。
程式碼寫起來混亂不堪,每個檔案中的程式碼都需要包裹在一個匿名函數中來編寫。
在沒有合適的規範情況下,每個人、每個公司都可能會任意命名、甚至出現模組名稱相同的情況。
所以,我們會發現,雖然實現了模組化,但是我們的實現過於簡單,並且是沒有規範的。
我們需要制定一定的規範來約束每個人都按照這個規範去編寫模組化的程式碼。這個規範中應該包括核心功能:模組本身可以匯出暴露的屬性,模組又可以匯入自己需要的屬性。JavaScript社群為了解決上面的問題,湧現出一系列好用的規範,接下來我們就學習具有代表性的一些規範。
我們需要知道CommonJS是一個規範,最初提出來是在瀏覽器以外的地方使用,並且當時被命名為ServerJS,後來為了體現它的廣泛性,修改為CommonJS,平時我們也會簡稱為CJS。
Node是CommonJS在伺服器端一個具有代表性的實現。
Browserify是CommonJS在瀏覽器中的一種實現。
webpack打包工具具備對CommonJS的支援和轉換。
所以,Node中對CommonJS進行了支援和實現,讓我們在開發node的過程中可以方便的進行模組化開發。
在Node中每一個js檔案都是一個單獨的模組。
這個模組中包括CommonJS規範的核心變數:exports、module.exports、require。
我們可以使用這些變數來方便的進行模組化開發。
前面我們提到過模組化的核心是匯出和匯入,Node中對其進行了實現:
exports和module.exports可以負責對模組中的內容進行匯出。
require函數可以幫助我們匯入其他模組(自定義模組、系統模組、第三方庫模組)中的內容。
Node中對CommonJS進行了支援和實現,讓我們在開發node的過程中可以方便的進行模組化開發:
在Node中每一個js檔案都是一個單獨的模組。
這個模組中包括CommonJS規範的核心變數:exports、module.exports、require。
exports和module.exports可以負責對模組中的內容進行匯出。
require函數可以幫助我們匯入其他模組(自定義模組、系統模組、第三方庫模組)中的內容。
下面我們將來介紹exports、module.exports、require的使用。
exports是一個物件,我們可以在這個物件中新增很多個屬性,新增的屬性會匯出。
我們也可以通過module.exports直接匯出一個物件。
我們通過require()函數匯入一個檔案。並且該檔案匯出的變數。
下面來詳細介紹一個module.exports。
CommonJS中是沒有module.exports的概念的。
但是為了實現模組的匯出,Node中使用的是Module的類,每一個模組都是Module的一個範例,也就是module。
所以在Node中真正用於匯出的其實根本不是exports,而是module.exports。
因為module才是匯出的真正實現者。
並且內部將exports賦值給module.exports。
該方式的匯入匯出有以下特點:
Node中的檔案都執行在一個函數中。可以通過列印console.log(arguments.callee + "")來驗證。
匯入匯出是值的參照,如果匯出的是一個基本資料型別值,那麼匯出檔案改變該值,然後匯入檔案該變數的值也不會變。
// a.js const obj = require("./b.js") console.log(obj) setTimeout(() => { obj.name = "llm" }, 1000)
// b.js const info = { name: "zh", age: 22, foo: function() { console.log("foo函數~") } } setTimeout(() => { console.log(info.name) // llm }, 2000) module.exports = info
他是通過require 函數來匯入的,只有在執行js程式碼才會知道模組的依賴關係。
程式碼是同步執行的。
模組多次引入,只會載入一次。每個module內部會存在一個loaded來確定是否被載入過。
程式碼迴圈引入的時候,深度優先來載入模組。然後再廣度優先。
我們現在已經知道,require是一個函數,可以幫助我們引入一個檔案(模組)中匯出的物件。
那麼,require的查詢規則是怎麼樣的呢?
詳細查詢規則,請存取這裡
這裡我總結比較常見的查詢規則:匯入格式如下:require(X)
模組在被第一次引入時,模組中的js程式碼會被執行一次
模組被多次引入時,會快取,最終只載入(執行)一次
為什麼只會載入執行一次呢?
這是因為每個模組物件module都有一個屬性:loaded。為false表示還沒有載入,為true表示已經載入。
如果有迴圈引入,那麼載入順序是什麼?
如上圖,Node採用的是深度優先演演算法:main -> aaa -> ccc -> ddd -> eee ->bbb
CommonJS載入模組是同步的:
同步的意味著只有等到對應的模組載入完畢,當前模組中的內容才能被執行。
這個在伺服器不會有什麼問題,因為伺服器載入的js檔案都是本地檔案,載入速度非常快。
如果將它應用於瀏覽器呢?
瀏覽器載入js檔案需要先從伺服器將檔案下載下來,之後再載入執行。
那麼採用同步的就意味著後續的js程式碼都無法正常執行,即使是一些簡單的DOM操作。所以在瀏覽器中,我們通常不使用CommonJS規範。當然在webpack中使用CommonJS是另外一回事。因為它會將我們的程式碼轉成瀏覽器可以直接執行的程式碼。
在早期為了可以在瀏覽器中使用模組化,通常會採用AMD或CMD。但是目前一方面現代的瀏覽器已經支援ES Modules,另一方面藉助於webpack等工具可以實現對CommonJS或者ES Module程式碼的轉換。AMD和CMD已經使用非常少了,所以這裡我們進行簡單的演練。
AMD主要是應用於瀏覽器的一種模組化規範:
AMD是Asynchronous Module Definition(非同步模組定義)的縮寫。它採用的是非同步載入模組。
我們提到過,規範只是定義程式碼的應該如何去編寫,只有有了具體的實現才能被應用。
AMD實現的比較常用的庫是require.js和curl.js。
require.js的使用
定義HTML的script標籤引入require.js和定義入口檔案。data-main屬性的作用是在載入完src的檔案後會載入執行該檔案
// index.html <script src="./require.js" data-main="./index.js"></script>
//main.js require.config({ baseUrl: '', // 預設是main.js的資料夾路徑 paths: { foo: "./foo" } }) require(["foo"], function(foo) { console.log("main:", foo) })
// foo.js define(function() { const name = "zh" const age = 22 function sum(num1, num2) { return num1 + num2 } return { name, age, sum } })
CMD規範也是應用於瀏覽器的一種模組化規範:
CMD 是Common Module Definition(通用模組定義)的縮寫。它也採用了非同步載入模組,但是它將CommonJS的優點吸收了過來。
AMD實現的比較常用的庫是SeaJS。
SeaJS的使用
引入sea.js和使用主入口檔案。
// index.html <script src="./sea.js"></script> <script> seajs.use("./main.js") </script>
//main.js define(function(require, exports, module) { const foo = require("./foo") console.log("main:", foo) })
// foo.js define(function(require, exports, module) { const name = "zh" const age = 22 function sum(num1, num2) { return num1 + num2 } // exports.name = name // exports.age = age module.exports = { name, age, sum } });
ES Module和CommonJS的模組化有一些不同之處:
一方面它使用了import和export關鍵字來實現模組化。
另一方面它採用編譯期的靜態分析,並且也加入了動態參照的方式。
export負責將模組內的內容匯出。
import負責從其他模組匯入內容。
採用ES Module將自動採用嚴格模式:use strict。
基本使用
// index.html <script src="./main.js" type="module"></script>
// foo.js let obj = { name: "zh", age: 22 } export default sum
// main.js import foo from './foo.js' console.log(foo)
在html檔案載入入口檔案的時候,需要指定type為module。
在開啟html檔案時,需要開啟本地服務,而不能直接開啟執行在瀏覽器上。
這個在MDN上面有給出解釋:
你需要注意本地測試 — 如果你通過本地載入Html 檔案 (比如一個 file:// 路徑的檔案), 你將會遇到 CORS 錯誤,因為Javascript 模組安全性需要。
你需要通過一個伺服器來測試。
export關鍵字將一個模組中的變數、函數、類等匯出。
我們希望將其他中內容全部匯出,它可以有如下的方式:
方式一:在語句宣告的前面直接加上export關鍵字。
export const name = "zh" export const age = 22
方式二:將所有需要匯出的識別符號,放到export後面的 {} 中。注意:這裡的 {}裡面不是ES6的物件字面量的增強寫法,{}也不是表示一個物件的。所以: export {name: name},是錯誤的寫法。
const name = "zh" const age = 22 function foo() { console.log("foo function") } export { name, age, foo }
方式三:匯出時給識別符號起一個別名。(基本沒用,一般在匯入檔案中起別名)。然後在匯入檔案中就只能使用別名來獲取。
export { name as fName, age as fAge, foo as fFoo }
import關鍵字負責從另外一個模組中匯入內容。
匯入內容的方式也有多種:
方式一:import {識別符號列表} from '模組'。注意:這裡的{}也不是一個物件,裡面只是存放匯入的識別符號列表內容。
import { name, age } from "./foo.js"
方式二:匯入時給識別符號起別名。
import { name as fName, age as fAge } from './foo.js'
方式三:通過 * 將模組功能放到一個模組功能物件(a module object)上。然後通過起別名來使用。
import * as foo from './foo.js'
表示匯入匯出。
import { add, sub } from './math.js' import {otherProperty} from './other.js' export { add, sub, otherProperty }
等價於
// 匯入的所有檔案會統一被匯出 export { add, sub } from './math.js' export {otherProperty} from './other.js'
等價於
export * from './math.js' export * from './other.js'
為什麼要這樣做呢?
在開發和封裝一個功能庫時,通常我們希望將暴露的所有介面放到一個檔案中。 這樣方便指定統一的介面規範,也方便閱讀。這個時候,我們就可以使用export和import結合使用。
前面我們學習的匯出功能都是有名字的匯出(named exports):
在匯出export時指定了名字。
在匯入import時需要知道具體的名字。
還有一種匯出叫做預設匯出(default export)
// foo.js const name = "zh" cconst age = 22 export { name, // 或者這樣的預設匯出 // age as default } export default age
// 匯入語句: 匯入的預設的匯出 import foo, {name} from './foo.js' console.log(foo, name) // 22 zh
預設匯出export時可以不需要指定名字。
在匯入時不需要使用 {},並且可以自己來指定名字。
它也方便我們和現有的CommonJS等規範相互操作。
注意:在一個模組中,只能有一個預設匯出(default export)。
通過import載入一個模組,是不可以在其放到邏輯程式碼中的,比如:
if(true) { import foo from './foo.js' }
為什麼會出現這個情況呢?
這是因為ES Module在被JS引擎解析時,就必須知道它的依賴關係。
由於這個時候js程式碼沒有任何的執行,所以無法在進行類似於if判斷中根據程式碼的執行情況。
但是某些情況下,我們確確實實希望動態的來載入某一個模組:
如果根據不同的條件,動態來選擇載入模組的路徑。
這個時候我們需要使用 import() 函數來動態載入。import函數返回的結果是一個Promise。
import("./foo.js").then(res => { console.log("res:", res.name) })
es11新增了一個屬性。meta屬性本身也是一個物件: { url: "當前模組所在的路徑" }
console.log(import.meta)
ES Module是如何被瀏覽器解析並且讓模組之間可以相互參照的呢?
ES Module的解析過程可以劃分為三個階段:
階段一:構建(Construction),根據地址查詢js檔案,並且下載,將其解析成模組記錄(Module Record)。
階段二:範例化(Instantiation),對模組記錄進行範例化,並且分配記憶體空間,解析模組的匯入和匯出語句,把模組指向對應的記憶體地址。
階段三:執行(Evaluation),執行程式碼,計算值,並且將值填充到記憶體地址中。
階段一:
階段二和三:
所以,從上面可以看出在匯出檔案中,修改變數的值會影響到匯入檔案中的值。而且匯入檔案被限制修改匯出檔案的值。
相關推薦:
以上就是JavaScript高階語法之模組化(建議收藏)的詳細內容,更多請關注TW511.COM其它相關文章!