框架設(shè)計(jì)并非僅僅實(shí)現(xiàn)功能那么簡(jiǎn)單,里面有很多學(xué)問(wèn)。例如:
-
框架應(yīng)該給用戶(hù)提供哪些構(gòu)建產(chǎn)物?產(chǎn)物的模塊格式如何?
-
當(dāng)用戶(hù)沒(méi)有以預(yù)期的方式使用框架時(shí),是否應(yīng)該打印合適的警告信息從而提供更好的開(kāi)發(fā)體驗(yàn),讓用戶(hù)快速定位問(wèn)題?
-
開(kāi)發(fā)版本和生產(chǎn)版本的構(gòu)建有何區(qū)別?
-
熱更新(hot module replacement,HMR)需要框架層面的支持,是否也應(yīng)該考慮?
-
當(dāng)框架提供了多個(gè)功能,而用戶(hù)只需要其中幾個(gè)功能時(shí),用戶(hù)能否選擇關(guān)閉其他功能從而減少最終資源的打包體積?
-
…
1. 提升開(kāi)發(fā)體驗(yàn)
衡量一個(gè)框架是否足夠優(yōu)秀的指標(biāo)之一就是看它的開(kāi)發(fā)體驗(yàn)如何,以 Vue.js 3 為例:
createApp(App).mount('#not-exist')
當(dāng)創(chuàng)建一個(gè)組件并試圖將其掛載到一個(gè)不存在的 DOM 節(jié)點(diǎn)時(shí),就會(huì)收到一條警告信息:
[Vue warn]: Failed to mount app: mount target selector "#not-exist" returned null.
這條信息讓我們能夠清晰且快速地定位問(wèn)題。如果 Vue.js 內(nèi)部不做任何處理,那么很可能得到的是 JavaScript 層面的錯(cuò)誤信息,如 Uncaught TypeError: Cannot read property 'xxx' of null
,而根據(jù)此信息很難知道問(wèn)題所在。
因此,在框架設(shè)計(jì)和開(kāi)發(fā)過(guò)程中,提供友好的警告信息至關(guān)重要。始終提供友好的警告信息不僅能夠幫助用戶(hù)快速定位問(wèn)題,節(jié)省用戶(hù)的時(shí)間,還能夠讓框架收獲良好的口碑,讓用戶(hù)認(rèn)可框架的專(zhuān)業(yè)性。
在 Vue.js 的源碼中,經(jīng)常能夠看到 warn
函數(shù)的調(diào)用,例如:
warn(`Failed to mount app: mount target selector "${container}" returned null.`)
除了提供必要的警告信息外,還有很多其他方面可以作為切入口,進(jìn)一步提升用戶(hù)的開(kāi)發(fā)體驗(yàn)。例如,在 Vue.js 3 中,當(dāng)在控制臺(tái)打印一個(gè) ref 數(shù)據(jù)時(shí):
const count = ref(0)
console.log(count)
打印結(jié)果是一個(gè) ref 對(duì)象,很不直觀,但調(diào)用 count.value
后,得到的就是響應(yīng)式對(duì)象的值,變得非常直觀。
那么有沒(méi)有辦法在直接打印 count
時(shí)讓輸出的信息更友好呢?當(dāng)然可以,瀏覽器允許我們編寫(xiě)自定義的 formatter,從而自定義輸出形式。
在 Vue.js 3 的源碼中,有一個(gè)名為 initCustomFormatter
的函數(shù),用來(lái)在開(kāi)發(fā)環(huán)境下初始化自定義 formatter。
以 Chrome 為例,打開(kāi) DevTools 的設(shè)置,然后勾選 “Console” -> “Enable custom formatters” 選項(xiàng),如下:
然后刷新瀏覽器并查看控制臺(tái),會(huì)發(fā)現(xiàn)輸出內(nèi)容變得非常直觀,如下:
2. 控制代碼體積
框架的大小也是衡量框架的標(biāo)準(zhǔn)之一。在實(shí)現(xiàn)同樣功能的情況下,代碼越少越好,這樣體積就會(huì)越小,最后瀏覽器加載資源的時(shí)間也就越少。前面說(shuō)到,框架提供越完善的警告信息越好,但這意味著要編寫(xiě)更多的代碼,那么如何在這個(gè)基礎(chǔ)上實(shí)現(xiàn)代碼體積的控制呢?
如果去看 Vue.js 3 的源碼,就會(huì)發(fā)現(xiàn)每一個(gè) warn
函數(shù)的調(diào)用都會(huì)配合 __DEV__
常量的檢查,例如:
if (__DEV__ && !res) { // 打印警告信息的前提時(shí) __DEV__ 為 true
warn(
`Failed to mount app: mount target selector "${container}" returned null.`
)
}
Vue.js 使用 rollup.js 對(duì)項(xiàng)目進(jìn)行構(gòu)建,這里的 __DEV__
常量實(shí)際上是通過(guò) rollup.js 的插件配置來(lái)預(yù)定義的,其功能類(lèi)似于 webpack 中的 DefinePlugin 插件。
Vue.js 在輸出資源的時(shí)候,會(huì)輸出兩個(gè)版本,其中一個(gè)用于開(kāi)發(fā)環(huán)境,如 vue.global.js,另一個(gè)用于生產(chǎn)環(huán)境,如 vue.global.prod.js。
當(dāng) Vue.js 構(gòu)建用于開(kāi)發(fā)環(huán)境的資源時(shí),會(huì)把 __DEV__
常量設(shè)置為 true
;當(dāng)構(gòu)建生產(chǎn)環(huán)境的資源時(shí),會(huì)把 __DEV__
常量設(shè)置為 false
。
因?yàn)樯a(chǎn)環(huán)境下判斷條件始終為假,這段永遠(yuǎn)不會(huì)執(zhí)行的代碼稱(chēng)為 dead code,它不會(huì)出現(xiàn)在最終產(chǎn)物中,在構(gòu)建資源時(shí)就會(huì)被移除。這樣就做到了在開(kāi)發(fā)環(huán)境中為用戶(hù)提供友好的警告信息的同時(shí),不會(huì)增加生產(chǎn)環(huán)境代碼的體積。
3. 良好的 Tree-Shaking
僅僅通過(guò) __dev__
變量控制代碼量是遠(yuǎn)遠(yuǎn)不夠的。還以 Vue.js 為例,其內(nèi)建了很多組件,例如 <Transition>
組件,如果項(xiàng)目中沒(méi)有用到該組件,其對(duì)應(yīng)的代碼就不需要也不應(yīng)該包含在最終的構(gòu)建資源中。那么如何做到這一點(diǎn)呢?答案就是 Tree-Shaking。
在前端領(lǐng)域,這個(gè)概念是由 rollup.js 普及的。簡(jiǎn)單地說(shuō),Tree-Shaking 指的就是消除那些永遠(yuǎn)不會(huì)被執(zhí)行的代碼,也就是排除 dead code,現(xiàn)在無(wú)論是 rollup.js 還是webpack,都支持 Tree-Shaking。
想要實(shí)現(xiàn) Tree-Shaking,必須滿(mǎn)足一個(gè)條件,即模塊必須是 ESM(ES Module),因?yàn)?Tree-Shaking 依賴(lài) ESM 的靜態(tài)結(jié)構(gòu)。以 rollup.js 為例看看 Tree-Shaking 如何工作,其目錄結(jié)構(gòu)如下:
- demo
- package.json
- input.js
- utils.js
首先安裝 rollup.js:
yarn add rollup -D # 或 npm install rollup -D
input.js 和 utils.js 文件的內(nèi)容如下:
// input.js
import { foo } from './utils.js'
foo()
// utils.js
export function foo(obj) {
obj && obj.foo
}
export function bar(obj) { // bar 函數(shù)未被使用
obj && obj.bar
}
接著,執(zhí)行如下命令進(jìn)行構(gòu)建:
npx rollup input.js -f esm -o bundle.js
構(gòu)建后,輸出的 bundle.js 的內(nèi)容為:
export function foo(obj) {
obj && obj.foo
}
foo()
這說(shuō)明 Tree-Shaking 起了作用,我們并沒(méi)有使用 bar
函數(shù),因此它作為 dead code 被刪除了。但是仔細(xì)觀察會(huì)發(fā)現(xiàn),foo
函數(shù)的執(zhí)行也沒(méi)有什么意義,僅僅是讀取了對(duì)象的值,所以它的執(zhí)行似乎沒(méi)什么必要。既然把這段代碼刪了也不會(huì)對(duì)程序產(chǎn)生影響,那為什么 rollup.js 不把這段代碼也作為 dead code 移除呢?
這涉及 Tree-Shaking 中的第二個(gè)關(guān)鍵點(diǎn) —— 副作用。如果一個(gè)函數(shù)調(diào)用會(huì)產(chǎn)生副作用,那么就不能將其移除。
簡(jiǎn)單地說(shuō),副作用就是,當(dāng)調(diào)用函數(shù)的時(shí)候會(huì)對(duì)外部產(chǎn)生影響,例如修改了全局變量。
但是,上面的代碼只讀取對(duì)象的值,怎么會(huì)產(chǎn)生副作用呢?其實(shí)是有可能的,如果 obj
對(duì)象是一個(gè)通過(guò) Proxy 創(chuàng)建的代理對(duì)象,那么讀取對(duì)象屬性時(shí),就會(huì)觸發(fā)代理對(duì)象的 get 夾子(trap),在 get 夾子中是可能產(chǎn)生副作用的。至于到底會(huì)不會(huì)產(chǎn)生副作用,只有代碼真正運(yùn)行的時(shí)候才能知道,JavaScript 本身是動(dòng)態(tài)語(yǔ)言,因此想要靜態(tài)地分析哪些代碼是 dead code 很有難度。
即然靜態(tài)地分析代碼很困難,所以像 rollup.js 這類(lèi)工具都會(huì)提供一個(gè)機(jī)制,讓我們手動(dòng)明確地告訴 rollup.js 該段代碼是一個(gè)純函數(shù),不會(huì)產(chǎn)生副作用,可以移除它。具體實(shí)現(xiàn)如下:
import {foo} from './utils'
/*#__PURE__*/ foo() // 前面的 __PURE__ 告知是一個(gè)純函數(shù),不會(huì)產(chǎn)生副作用,可以移除
此時(shí)再次執(zhí)行構(gòu)建命令并查看 bundle.js 文件,就會(huì)發(fā)現(xiàn)它的內(nèi)容是空的。
因此,我們?cè)诰帉?xiě)框架的時(shí)候需要合理使用 /*#__PURE__*
注釋。Vue.js 3 的源碼里面大量使用了該注釋。
那么,這會(huì)不會(huì)對(duì)編寫(xiě)代碼造成很大的心智負(fù)擔(dān)呢?其實(shí)不會(huì),因?yàn)橥ǔ.a(chǎn)生副作用的代碼都是模塊內(nèi)函數(shù)的頂級(jí)調(diào)用。
什么是頂級(jí)調(diào)用呢?如下:
foo() // 頂級(jí)調(diào)用
function bar() {
foo() // 函數(shù)內(nèi)調(diào)用 -- 沒(méi)有副作用,除非 bar() 頂級(jí)調(diào)用
}
/*#__PURE__*/
注釋不僅僅作用于函數(shù),它可以應(yīng)用于任何語(yǔ)句上。該注釋也不是只有 rollup.js 才能識(shí)別,webpack 以及壓縮工具(如 terser)都能識(shí)別它。
4. 輸出構(gòu)建產(chǎn)物
前面說(shuō)到 Vue.js 會(huì)為開(kāi)發(fā)環(huán)境和生產(chǎn)環(huán)境輸出不同的包,如 vue.global.js 用于開(kāi)發(fā)環(huán)境,它包含必要的警告信息,而 vue.global.prod.js 用于生產(chǎn)環(huán)境,不包含警告信息。實(shí)際上,Vue.js 的構(gòu)建產(chǎn)物除了有環(huán)境上的區(qū)分之外,還會(huì)根據(jù)使用場(chǎng)景的不同而輸出其他形式的產(chǎn)物。
不同類(lèi)型的產(chǎn)物一定有對(duì)應(yīng)的需求背景,因此需要從需求講起。首先我們希望用戶(hù)可以直接在 HTML 頁(yè)面中使用 <script>
標(biāo)簽引入框架并使用:
<body>
<script src="/path/to/vue.js"></script>
<script>
const { createApp } = Vue
// ...
</script>
</body>
為了實(shí)現(xiàn)這個(gè)需求,需要輸出一種叫作 IIFE 格式的資源。IIFE 的全稱(chēng)是 Immediately Invoked Function Expression,即“立即調(diào)用的函數(shù)表達(dá)式”,易于用 JavaScript 來(lái)表達(dá):
(function () {
// ...
})
如以上代碼所示,這是一個(gè)立即執(zhí)行的函數(shù)表達(dá)式。實(shí)際上,vue.global.js 文件就是 IIFE 形式的資源,它的代碼結(jié)構(gòu)如下所示:
var Vue = (function(exports){
// ...
exports.createApp = createApp;
// ...
return exports
}({}))
這樣當(dāng)我們使用 <script>
標(biāo)簽直接引入 vue.global.js 文件后,全局變量 Vue 就是可用的了。在 rollup.js 中,可以通過(guò)配置 format: 'iife'
來(lái)輸出這種形式的資源:
// rollup.config.js
const config = {
input: 'input.js',
output: {
file: 'output.js',
format: 'iife' // 指定模塊形式
}
}
export default config
隨著技術(shù)的發(fā)展和瀏覽器的支持,現(xiàn)在主流瀏覽器對(duì)原生 ESM 的支持都不錯(cuò),所以用戶(hù)除了能夠使用 <script>
標(biāo)簽引用 IIFE 格式的資源外,還可以直接引入 ESM 格式的資源,例如 Vue.js 3 還會(huì)輸出 vue.esm-browser.js 文件,用戶(hù)可以直接用 <script type="module">
標(biāo)簽引入:
<script type="module" src="/path/to/vue.esm-browser.js"></script>
為了輸出 ESM 格式的資源,rollup.js 的輸出格式需要配置為:format: 'esm'
。
為什么 vue.esm-browser.js 文件中會(huì)有 -browser 字樣?其實(shí)對(duì)于 ESM 格式的資源來(lái)說(shuō),Vue.js 還會(huì)輸出一個(gè) vue.esm-bundler.js 文件,其中 -browser 變成了 -bundler。為什么這么做呢?我們知道,無(wú)論是 rollup.js 還是 webpack,在尋找資源時(shí),如果 package.json 中存在 module
字段,那么會(huì)優(yōu)先使用 module
字段指向的資源來(lái)代替 main
字段指向的資源。
可以打開(kāi) Vue.js 源碼中的 packages/vue/package.json 文件看一下:
{
"main": "index.js",
"module": "dist/vue.runtime.esm-bundler.js",
}
其中 module
字段指向的是 vue.runtime.esm-bundler.js 文件,意思是說(shuō),如果項(xiàng)目是使用webpack 構(gòu)建的,那么你使用的 Vue.js 資源就是 vue.runtime.esm-bundler.js,也就是說(shuō),帶有 -bundler 字樣的 ESM 資源是給 rollup.js 或 webpack 等打包工具使用的,而帶有 -browser 字樣的 ESM 資源是直接給 <script type="module">
使用的。它們之間有何區(qū)別?這就不得不提到上文中的 __DEV__
常量。當(dāng)構(gòu)建用于 <script>
標(biāo)簽的 ESM 資源時(shí),如果是用于開(kāi)發(fā)環(huán)境,那么 __DEV__
會(huì)設(shè)置為 true
;如果是用于生產(chǎn)環(huán)境,那么 __DEV__
常量會(huì)設(shè)置為 false
,從而被 Tree-Shaking 移除。但是當(dāng)我們構(gòu)建提供給打包工具的 ESM 格式的資源時(shí),不能直接把 __DEV__
設(shè)置為 true
或 false
,而要使用(process.env.NODE_ENV !== 'production'
)替換 __DEV__
常量。例如下面的源碼:
if (__DEV__) {
warn(`useCssModule() is not supported in the global build.`)
}
在帶有 -bundler 字樣的資源中會(huì)變成:
if ((process.env.NODE_ENV !== 'production')) {
warn(`useCssModule() is not supported in the global build.`)
}
這樣做的好處是,用戶(hù)可以通過(guò) webpack 配置自行決定構(gòu)建資源的目標(biāo)環(huán)境,但是最終效果其實(shí)一樣,這段代碼也只會(huì)出現(xiàn)在開(kāi)發(fā)環(huán)境中。用戶(hù)除了可以直接使用 <script>
標(biāo)簽引入資源外,我們還希望用戶(hù)可以在 Node.js 中通過(guò) require
語(yǔ)句引用資源,例如:
const Vue = require('vue')
為什么會(huì)有這種需求呢?因?yàn)楫?dāng)進(jìn)行服務(wù)端渲染時(shí),Vue.js 的代碼是在 Node.js 環(huán)境中運(yùn)行的。在 Node.js 環(huán)境中,資源的模塊格式應(yīng)該是 CommonJS,簡(jiǎn)稱(chēng) cjs。為了能夠輸出 cjs 模塊的資源,可以通過(guò)修改 rollup.config.js 的配置 format: 'cjs'
來(lái)實(shí)現(xiàn):
5. 特征開(kāi)關(guān)
在設(shè)計(jì)框架時(shí),框架會(huì)給用戶(hù)提供諸多特性(或功能),例如提供 A、B、C 三個(gè)特性給用戶(hù),同時(shí)還提供了 a、b、c 三個(gè)對(duì)應(yīng)的特性開(kāi)關(guān),用戶(hù)可以通過(guò)設(shè)置 a、b、c 為 true
或 false
來(lái)代表開(kāi)啟或關(guān)閉對(duì)應(yīng)的特性,這將會(huì)帶來(lái)很多益處,如:
- 對(duì)于用戶(hù)關(guān)閉的特性,可以利用 Tree-Shaking 機(jī)制讓其不打包在最終的資源中。
- 該機(jī)制為框架設(shè)計(jì)帶來(lái)了靈活性,可以通過(guò)特性開(kāi)關(guān)任意為框架添加新的特性,而不用擔(dān)心資源體積變大。
- 當(dāng)框架升級(jí)時(shí),也可以通過(guò)特性開(kāi)關(guān)來(lái)支持遺留 API,這樣新用戶(hù)可以選擇不使用遺留 API,從而使最終打包的資源體積最小化。
那如何實(shí)現(xiàn)特性開(kāi)關(guān)呢?其原理和前面提到的 __DEV__
常量一樣,本質(zhì)上是利用 rollup.js 的預(yù)定義常量插件來(lái)實(shí)現(xiàn)。拿 Vue.js 3 源碼中的一段 rollup.js 配置來(lái)說(shuō):
{
__FEATURE_OPTIONS_API__: isBundlerESMBuild ? `__VUE_OPTIONS_API__` : true,
}
其中 __FEATURE_OPTIONS_API__
類(lèi)似于 __DEV__
。在 Vue.js 3 的源碼中搜索,可以找到很多類(lèi)似于如下代碼的判斷分支:
// support for 2.x options
if (__FEATURE_OPTIONS_API__) {
currentInstance = instance
pauseTracking()
applyOptions(instance, Component)
resetTracking()
currentInstance = null
}
當(dāng) Vue.js 構(gòu)建資源時(shí),如果構(gòu)建的資源是供打包工具使用的(即帶有 -bundler 字樣的資源),那么上面的代碼在資源中會(huì)變成:
// support for 2.x options
if (__VUE_OPTIONS_API__) { // 這里不一樣
currentInstance = instance
pauseTracking()
applyOptions(instance, Component)
resetTracking()
currentInstance = null
}
其中 __VUE_OPTIONS_API__
是一個(gè)特性開(kāi)關(guān),用戶(hù)可以通過(guò)設(shè)置 __VUE_OPTIONS_API__
預(yù)定義常量的值來(lái)控制是否要包含這段代碼。通常用戶(hù)可以使用 webpack.DefinePlugin 插件來(lái)實(shí)現(xiàn):
// webpack.DefinePlugin 插件配置
new webpack.DefinePlugin({
__VUE_OPTIONS_API__: JSON.stringify(true) // 開(kāi)啟特性
})
最后解釋一下 __VUE_OPTIONS_API__
開(kāi)關(guān)有什么用。在 Vue.js 2 中,我們編寫(xiě)的組件叫作Options API;在 Vue.js 3 中,推薦使用 Composition API 來(lái)編寫(xiě)代碼。為了兼容 Vue.js 2,在 Vue.js 3 中仍然可以使用 Options API 的方式編寫(xiě)代碼。但是如果明確知道自己不會(huì)使用選項(xiàng) API,用戶(hù)就可以使用 __VUE_OPTIONS_API__
開(kāi)關(guān)來(lái)關(guān)閉該特性,這樣在打包的時(shí)候 Vue.js 的這部分代碼就不會(huì)包含在最終的資源中,從而減小資源體積。
6. 錯(cuò)誤處理
錯(cuò)誤處理是框架開(kāi)發(fā)過(guò)程中非常重要的環(huán)節(jié)??蚣苠e(cuò)誤處理機(jī)制的好壞直接決定了用戶(hù)應(yīng)用程序的健壯性,還決定了用戶(hù)開(kāi)發(fā)時(shí)處理錯(cuò)誤的心智負(fù)擔(dān)。
假設(shè)我們開(kāi)發(fā)了一個(gè)工具模塊,代碼如下:
// utils.js
export default {
foo(fn) {
fn && fn()
}
}
該模塊導(dǎo)出一個(gè)對(duì)象,其中 foo
屬性是一個(gè)函數(shù),接收一個(gè)回調(diào)函數(shù)作為參數(shù),調(diào)用 foo
函數(shù)時(shí)會(huì)執(zhí)行該回調(diào)函數(shù),在用戶(hù)側(cè)使用時(shí):
import utils from 'utils.js'
utils.foo( () => {
// ...
})
如果用戶(hù)提供的回調(diào)函數(shù)在執(zhí)行的時(shí)候出錯(cuò)了,怎么辦?有兩個(gè)辦法,第一個(gè)辦法是讓用戶(hù)自行處理,這需要用戶(hù)自己執(zhí)行 try ... catch
:
import utils from 'utils.js'
utils.foo( () => {
try {
// ...
} catch(e) {
// ...
}
})
但是這會(huì)增加用戶(hù)的負(fù)擔(dān)。如果 utils.js 提供了幾十上百個(gè)類(lèi)似的函數(shù),那么用戶(hù)在使用的時(shí)候就需要逐一添加錯(cuò)誤處理程序。
第二個(gè)辦法是我們代替用戶(hù)統(tǒng)一處理錯(cuò)誤,如以下代碼所示:
// utils.js
export default {
foo(fn) {
try {
fn && fn()
} catch(e) {
/* ... */
}
},
bar(fn) {
try {
fn && fn()
} catch(e) {
/* ... */
}
},
// ...
}
事實(shí)上,可以進(jìn)一步將錯(cuò)誤處理程序封裝在一個(gè)函數(shù)上,假設(shè)稱(chēng)為 callWithErrorHandling
:
// utils.js
export default {
foo(fn) {
callWithErrorHandling(fn)
},
bar(fn) {
callWithErrorHandling(fn)
},
// ...
}
function callWithErrorHandling(fn) {
try {
fn && fn()
} catch(e) {
/* ... */
}
}
簡(jiǎn)潔還不是封裝函數(shù)的主要目的,我們能為用戶(hù)提供統(tǒng)一的錯(cuò)誤處理接口,如:
// utils.js
let handleError = null
export default {
foo(fn) {
callWithErrorHandling(fn)
},
// 用戶(hù)可以調(diào)用該函數(shù)注冊(cè)統(tǒng)一的錯(cuò)誤處理函數(shù)
registerErrorHandler(fn) {
handleError = fn
}
}
function callWithErrorHandling(fn) {
try {
fn && fn()
} catch (e) {
// 將捕獲到的錯(cuò)誤傳遞給用戶(hù)的錯(cuò)誤處理程序
handleError(e)
}
}
這樣用戶(hù)側(cè)的代碼就會(huì)非常簡(jiǎn)潔且健壯:
import utils from 'utils.js'
// 注冊(cè)錯(cuò)誤處理程序
utils.registerErrorHandler((e) => {
console.log(e)
})
utils.foo(() => {/*...*/})
utils.bar(() => {/*...*/})
這時(shí)錯(cuò)誤處理的能力完全由用戶(hù)控制,用戶(hù)既可以選擇忽略錯(cuò)誤,也可以調(diào)用上報(bào)程序?qū)㈠e(cuò)誤上報(bào)給監(jiān)控系統(tǒng)。實(shí)際上,這就是 Vue.js 錯(cuò)誤處理的原理,可以在源碼中搜索到 callWithErrorHandling
函數(shù)。另外,在 Vue.js 中,也可以注冊(cè)統(tǒng)一的錯(cuò)誤處理函數(shù):
import App from 'App.vue'
const app = createApp(App)
app.config.errorHandler = () => {
// 錯(cuò)誤處理程序
}
7. 良好的 TS 支持
TypeScript 是由微軟開(kāi)源的編程語(yǔ)言,簡(jiǎn)稱(chēng) TS,它是 JavaScript 的超集,能夠?yàn)?JavaScript 提供類(lèi)型支持。使用 TS 的好處有很多,如代碼即文檔、編輯器自動(dòng)提示、一定程度上能夠避免低級(jí) bug、代碼的可維護(hù)性更強(qiáng)等。因此對(duì) TS 類(lèi)型的支持是否完善也成為評(píng)價(jià)一個(gè)框架的重要指標(biāo)。
如何衡量一個(gè)框架對(duì) TS 類(lèi)型支持的水平呢?這里有一個(gè)常見(jiàn)的誤區(qū),很多人以為只要是使用 TS 編寫(xiě)框架,就等價(jià)于對(duì) TS 類(lèi)型支持友好,其實(shí)這兩種完全不同。
舉例來(lái)說(shuō)。下面是使用 TS 編寫(xiě)的函數(shù):
function foo(val: any) {
return val
}
這個(gè)函數(shù)直接將參數(shù)作為返回值,這說(shuō)明返回值的類(lèi)型是由參數(shù)決定的,如果參數(shù)是 number 類(lèi)型,那么返回值也是 number 類(lèi)型。但是,假設(shè)有下面的代碼:
const res = foo('str') // 參數(shù)為字符串類(lèi)型,理論上 res 也為字符串類(lèi)型,但是卻推斷成了 any 類(lèi)型
為了達(dá)到理想狀態(tài),只需要對(duì) foo
函數(shù)做簡(jiǎn)單的修改即可:
function foo<T extends any>(val: T): T {
return val
}
const res = foo('str') // 這時(shí)就會(huì)將 res 推斷為 "str" 字符串字面量了
通過(guò)這個(gè)例子可以認(rèn)識(shí)到,使用 TS 編寫(xiě)代碼與對(duì) TS 類(lèi)型支持友好是兩件事。在編寫(xiě)大型框架時(shí),想要做到完善的 TS 類(lèi)型支持很不容易,可以查看 Vue.js 源碼中的 runtime-core/src/apiDefineComponent.ts 文件,整個(gè)文件里真正會(huì)在瀏覽器中運(yùn)行的代碼其實(shí)只有 3 行,但是全部的代碼接近 200 行,其實(shí)這些代碼都是在為類(lèi)型支持服務(wù)。由此可見(jiàn),框架想要做到完善的類(lèi)型支持,需要付出相當(dāng)大的努力。文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-438051.html
更多文章可關(guān)注:GopherBlog、GopherBlog副站文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-438051.html
到了這里,關(guān)于《Vue.js 設(shè)計(jì)與實(shí)現(xiàn)》—— 02 框架設(shè)計(jì)核心要素的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!