我从 Vuejs 中学到了什么——框架设计学问
框架設(shè)計遠沒有大家想的那么簡單,并不是說只把功能開發(fā)完成,能用就算完事兒了,這里面還是有很多學問的。比如說,我們的框架應(yīng)該給用戶提供哪些構(gòu)建產(chǎn)物?產(chǎn)物的模塊格式如何?當用戶沒有以預(yù)期的方式使用框架時是否應(yīng)該打印合適的警告信息從而提升更好的開發(fā)體驗,讓用戶快速定位問題?開發(fā)版本的構(gòu)建和生產(chǎn)版本的構(gòu)建有何區(qū)別?熱跟新(HMR:Hot Module Replacement)需要框架層面的支持才行,我們是否也應(yīng)該考慮?再有就是當你的框架提供了多個功能,如果用戶只需要其中幾個功能,那么用戶是否可以選擇關(guān)閉其他功能從而減少資源的打包體積?所有以上這些問題我們都會在本節(jié)內(nèi)容進行討論。
本節(jié)內(nèi)容需要大家對常用的模塊打包工具有一定的使用經(jīng)驗,尤其是 rollup.js 以及 webpack。如果你只用過或了解過其中一個也沒關(guān)系,因為它們很多概念其實是類似的。如果你沒有使用任何模塊打包工具那么需要你自行去了解一下,至少有了初步認識之后再來看本節(jié)內(nèi)容會更好一些。
提升用戶的開發(fā)體驗
衡量一個框架是否足夠優(yōu)秀的指標之一就是看它的開發(fā)體驗如何,我們拿 Vue3 舉個例子:
createApp(App).mount('#not-exist')當我們創(chuàng)建一個 Vue 應(yīng)用并試圖將其掛載到一個不存在的 DOM 節(jié)點時就會得到一個警告信息:
warn
從這條信息中我們得知掛載失敗了,并說明了失敗的原因:Vue 根據(jù)我們提供的選擇器無法找到相應(yīng)的 DOM 元素(返回 null),正式因為這條信息的存在使得我們能夠清晰且快速的了解并定位問題,可以試想一下如果 Vue 內(nèi)部不做任何處理,那么很可能得到的是一個 JS 層面的錯誤信息,例如:Uncaught TypeError: Cannot read property 'xxx' of null,但是根據(jù)此信息我們很難知道問題出在哪里。
所以在框架設(shè)計和開發(fā)的過程中,提供友好的警告信息是至關(guān)重要的,如果這一點做得不好那么很可能經(jīng)常收到用戶的抱怨。始終提供友好的警告信息不僅能夠快速幫助用戶定位問題,節(jié)省用戶的時間,還能夠為框架收獲良好的口碑,讓用戶認為你是非常專業(yè)的。
在 Vue 的源碼中,你經(jīng)常能夠看到 warn() 函數(shù)的調(diào)用,例如上面圖片中的信息就是由這句 warn() 函數(shù)調(diào)用打印的:
warn(`Failed?to?mount?app:?mount?target?selector?"${container}"?returned?null.` )對于 warn() 函數(shù)來說,由于它需要盡可能的提供有用的信息,因此它需要收集當前發(fā)生錯誤的組件的組件棧信息,所以如果你去看源碼你會發(fā)現(xiàn)有些復(fù)雜,但其實最終就是調(diào)用了 console.warn() 函數(shù)。
對于開發(fā)體驗來說,除了提供必要的警告信息,還有很多其他方面可以作為切入口,可以進一步提升用戶的開發(fā)體驗。例如在 Vue3 中當我們在控制臺打印一個 Ref 數(shù)據(jù)時:
const?count?=?ref(0) console.log(count)打開控制臺查看輸出,如下圖所示:
沒有任何處理的輸出
可以發(fā)現(xiàn)非常的不直觀,當然我們可以直接打印 count.value ,這樣就只會輸出 0,但是有沒有辦法在打印 count 的時候讓輸出的信息更有好呢?當然可以,瀏覽允許我們編寫自定義的 formatter,從而自定義輸出的形式。在 Vue 的源碼中你可以搜索到名為 initCustomFormatter 的函數(shù),這個函數(shù)就是用來在開發(fā)環(huán)境下初始化自定義 formatter 的,以 chrome 為例我們可以打開 devtool 的設(shè)置,然后勾選 Console -> Enable custom formatters:
然后刷新瀏覽器后查看控制臺,會發(fā)現(xiàn)輸出的內(nèi)容變得非常直觀:
控制框架代碼的體積
框架的大小也是衡量框架的標準之一,在實現(xiàn)同樣功能的情況下當然是用越少的代碼越好,這樣體積就會越小,最后瀏覽器加載資源的時間也就越少。這時我們不禁會想,提供越完善的警告信息就意味著我們要編寫更多的代碼,這不是與控制代碼體積相駁嗎?沒錯,所以我們要想辦法解決這個問題。
如果我們?nèi)タ?Vue 的源碼會發(fā)現(xiàn),每一個 warn() 函數(shù)的調(diào)用都會配合 __DEV__ 常量的檢查,例如:
if?(__DEV__?&&?!res)?{warn(`Failed?to?mount?app:?mount?target?selector?"${container}"?returned?null.`) }可以看到,打印警告信息的前提是:__DEV__ 這個常量一定要為真,這里的 __DEV__ 常量就是達到目的的關(guān)鍵。
Vue 使用的是 rollup.js 對項目進行構(gòu)建的,這里的 __DEV__ 常量實際上是通過 rollup 的配置來預(yù)定義的,其功能類似于 webpack 中的 DefinePlugin 插件。
Vue 在輸出資源的時候,會輸出兩個版本的資源,其中一個資源用于開發(fā)環(huán)境,如 vue.global.js ;另一個與其對應(yīng)的用于生產(chǎn)環(huán)境,如:vue.global.prod.js ,通過文件名稱我們也能夠區(qū)分。
當 Vue 構(gòu)建用于開發(fā)環(huán)境的資源時,會把 __DEV__ 常量設(shè)置為 true,這時上面那段輸出警告信息的代碼就等價于:
if?(true?&&?!res)?{warn(`Failed?to?mount?app:?mount?target?selector?"${container}"?returned?null.`) }可以看到這里的 __DEV__ 被替換成了字面量 true ,所以這段代碼在開發(fā)環(huán)境是肯定存在的。
當 Vue 構(gòu)建用于生產(chǎn)環(huán)境的資源時,會把 __DEV__ 常量設(shè)置為 false,這時上面那段輸出警告信息的代碼就等價于:
if?(false?&&?!res)?{warn(`Failed?to?mount?app:?mount?target?selector?"${container}"?returned?null.`) }可以看到 __DEV__ 常量被替換為字面量 false ,這時我們發(fā)現(xiàn)這段分支代碼永遠都不會執(zhí)行,因為判斷條件始終為假,這段永遠不會執(zhí)行的代碼被稱為 Dead Code,它不會出現(xiàn)在最終的產(chǎn)物中,在構(gòu)建資源的時候就會被移除,因此在 vue.global.prod.js 中是不會存在這段代碼的。
這樣我們就做到了在開發(fā)環(huán)境為用戶提供友好的警告信息的同時,還不會增加生產(chǎn)環(huán)境代碼的體積。
框架要做到良好的 Tree-Shaking
上文中我們提到通過構(gòu)建工具設(shè)置預(yù)定義的常量 __DEV__ ,就能夠做到在生產(chǎn)環(huán)境使得框架不包含打印警告信息的代碼,從而使得框架自身的代碼量變少。但是從用戶的角度來看,這么做仍然不夠,還是拿 Vue 來舉個例子,我們知道 Vue 提供了內(nèi)置的組件例如 <Transition> ,如果我們的項目中根本就沒有使用到該組件,那么 <Transition> 組件的代碼需要包含在我們項目最終的構(gòu)建資源中嗎?答案是當然不需要,那如何做到這一點呢?這就不得不提到本節(jié)的主角 Tree-Shaking。
那什么是 Tree-Shaking 呢?在前端領(lǐng)域這個概念因 rollup 而普及,簡單的說所謂 **Tree-Shaking **指的就是消除哪些永遠不會執(zhí)行的代碼,也就是排除 dead-code,現(xiàn)在無論是 rollup 還是 webpack 都支持 Tree-Shaking。
想要實現(xiàn) Tree-Shaking 必須滿足一個條件,即模塊必須是 ES Module,因為 Tree-Shaking 依賴 ESM 的靜態(tài)結(jié)構(gòu)。我們使用 rollup 通過一個簡單的例子看看 Tree-Shaking 如何工作,我們 demo 的目錄結(jié)構(gòu)如下:
├──?demo │???└──?package.json │???└──?input.js │???└──?utils.js首先安裝 rollup:
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)?{obj?&&?obj.bar }代碼很簡單,我們在 utils.js 文件中定義并導(dǎo)出了兩個函數(shù),分別是 foo 和 bar,然后在 input.js 中導(dǎo)入了 foo 函數(shù)并執(zhí)行,注意我們并沒有導(dǎo)入 bar 函數(shù)。
接著我們執(zhí)行如下命令使用 rollup 構(gòu)建:
npx?rollup?input.js?-f?esm?-o?bundle.js這句命令的意思是以 input.js 文件問入口,輸出 ESM 模塊,輸出的文件名叫做 bundle.js 。命令執(zhí)行成功后,我們打開 bundle.js 來查看一下它的內(nèi)容:
//?bundle.js function?foo(obj)?{obj?&&?obj.foo } foo();可以看到,其中并不包含 bar 函數(shù),這說明 Tree-Shaking 起了作用,由于我們并沒有使用 bar 函數(shù),因此它作為 dead-code 被刪除了。但是如果我們仔細觀察會發(fā)現(xiàn),foo 函數(shù)的執(zhí)行也沒啥意義呀,就是讀取了對象的值,所以它執(zhí)行還是不執(zhí)行也沒有本質(zhì)的區(qū)別呀,所以即使把這段代碼刪了,也對我們的應(yīng)用沒啥影響,那為什么 rollup 不把這段代碼也作為 dead-code 移除呢?
這就涉及到 Tree-Shaking 中的第二個關(guān)鍵點,即副作用。如果一個函數(shù)調(diào)用會產(chǎn)生副作用,那么就不能將其移除。什么是副作用?簡單地說副作用的意思是當調(diào)用函數(shù)的時候,會對外部產(chǎn)生影響,例如修改了全局變量。這時你可能會說,上面的代碼明顯是讀取對象的值怎么會產(chǎn)生副作用呢?其實是有可能的,想想一下如果 obj 對象是一個通過 Proxy 創(chuàng)建的代理對象那么當我們讀取對象屬性時就會觸發(fā) Getter ,在 Getter 中是可能產(chǎn)生副作用的,例如我們在 Getter 中修改了某個全局變量。而到底會不會產(chǎn)生副作用,這個只有代碼真正運行的時候才能知道, JS 本身是動態(tài)語言,想要靜態(tài)的分析哪些代碼是 dead-code 是一件很有難度的事兒,上面只是舉了一個簡單的例子。
正因為靜態(tài)分析 JS 代碼很困難,所以諸如 rollup 等這類工具都會給我提供一個機制,讓我們有能力明確的告訴 rollup :”放心吧,這段代碼不會產(chǎn)生副作用,你可以放心移除它“,那具體怎么做呢?如下代碼所示,我們修改 input.js 文件:
import?{foo}?from?'./utils'/*#__PURE__*/?foo()注意這段注釋代碼 /*#__PURE_*_/,該注釋的作用就是用來告訴 rollup 對于 foo() 函數(shù)的調(diào)用不會產(chǎn)生副作用,你可以放心的對其進行 Tree-Shaking,此時再次執(zhí)行構(gòu)建命令并查看 bundle.js 文件你會發(fā)現(xiàn)它的內(nèi)容是空的,這說明 Tree-Shaking 生效了。
基于這個案例大家應(yīng)該明白的是,在編寫框架的時候我們需要合理的使用 /*#__PURE_*_/ 注釋,如果你去搜索 Vue 的源碼會發(fā)現(xiàn)它大量的使用了該注釋,例如下面這句:
export?const?isHTMLTag?=?/*#__PURE__*/?makeMap(HTML_TAGS)也許你會覺得這會不會對編寫代碼帶來很大的心智負擔?其實不會,這是因為通常產(chǎn)生副作用的代碼都是模塊內(nèi)函數(shù)的頂級調(diào)用,什么是頂級調(diào)用呢?如下代碼所示:
foo()?//?頂級調(diào)用function?bar()?{foo()?//?函數(shù)內(nèi)調(diào)用 }可以看到對于頂級調(diào)用來說是可能產(chǎn)生副作用的,但對于函數(shù)內(nèi)調(diào)用來說只要函數(shù) bar 沒有被調(diào)用,那么 foo 函數(shù)的調(diào)用當然不會產(chǎn)生副作用。因此你會發(fā)現(xiàn)在 Vue 的源碼中,基本都是在一些頂級調(diào)用的函數(shù)上使用 /*#__PURE__*/ 注釋的。當然該注釋不僅僅作用與函數(shù),它可以使用在任何語句上,這個注釋也不是只有 rollup 才能識別,webpack 以及壓縮工具如 terser 都能識別它。
框架應(yīng)該輸出怎樣的構(gòu)建產(chǎn)物
上文中我們提到 Vue 會為開發(fā)環(huán)境和生產(chǎn)環(huán)境輸出不同的包,例如 vue.global.js 用于開發(fā)環(huán)境,它包含了必要的警告信息,而 vue.global.prod.js 用于生產(chǎn)環(huán)境,不包含警告信息。實際上 Vue 的構(gòu)建產(chǎn)物除了有環(huán)境上的區(qū)分之外,還會根據(jù)使用場景的不同而輸出其他形式的產(chǎn)物,這一節(jié)我們將討論這些產(chǎn)物的用途以及在構(gòu)建階段如何輸出這些產(chǎn)物。
不同類型的產(chǎn)物一定是有對應(yīng)的需求背景的,因此我們從需求講起。首先我們希望用戶可以直接在 html 頁面中使用 <script> 標簽引入框架并使用:
<body><script?src="/path/to/vue.js"></script><script>const?{?createApp?}?=?Vue//?...</script> </body>為了能夠?qū)崿F(xiàn)這個需求,我們就需要輸出一種叫做 IIFE 格式的資源,IIFE 的全稱是 Immediately Invoked Function Expression ,即”立即調(diào)用的函數(shù)表達式“,可以很容易的用 JS 來表達:
(function?()?{//?... }())如上代碼所示,這就是一個立即執(zhí)行的函數(shù)表達式。實際上 vue.globale.js 文件就是 IIFE 形式的資源,大家可以看一下它的代碼結(jié)構(gòu):
var?Vue?=?(function(exports){//?...exports.createApp?=?createApp;//?...return?exports }({}))這樣當我們使用 <script> 標簽直接引入 vue.global.js 文件后,那么全局變量 Vue 就是可用的了。
在 rollup 中我們可以通過配置 format: 'iife' 來實現(xiàn)輸出這種形式的資源:
//?rollup.config.js const?config?=?{input:?'input.js',output:?{file:?'output.js',format:?'iife'?//?指定模塊形式} }export?default?config不過隨著技術(shù)的發(fā)展和瀏覽器的支持,現(xiàn)在主流瀏覽器對原生 ESM 模塊的支持都不錯,所以用戶除了能夠使用 <script> 標簽引用 IIFE 格式的資源外,還可以直接引如 ESM 格式的資源,例如 Vue3 會輸出 vue.esm-browser.js 文件,用戶可以直接用 <script> 標簽引入:
<script?type="module"?src="/path/to/vue.esm-browser.js"></script>為了輸出 ESM 格式的資源就需要我們配置 rollup 的輸出格式為:format: 'esm'。
你可能已經(jīng)注意到了,為什么 vue.esm-browser.js 文件中會有 -browser 字樣,其實對于 ESM 格式的資源來說,Vue 還會輸出一個 vue.esm-bundler.js 文件,其中 -browser 變成了 -bundler。為什么這么做呢?我們知道無論是 rollup 還是 webpack 在尋找資源時,如果 package.json 中存在 module 字段,那么會優(yōu)先使用 module 字段指向的資源來代替 main 字段所指向的資源。我們可以打開 Vue 源碼中的 packages/vue/package.json 文件看一下:
{"main":?"index.js","module":?"dist/vue.runtime.esm-bundler.js", }其中 module 字段指向的是 vue.runtime.esm-bundler.js 文件,意思就是說如果你的項目是使用 webpack 構(gòu)建的,那你使用的 Vue 資源就是 vue.runtime.esm-bundler.js ,也就是說帶有 -bundler 字樣的 ESM 資源是給 rollup 或 webpack 等打包工具使用的,而帶有 -browser 字樣的 ESM 資源是直接給 <script type="module"> 去使用的。
那他們之間的區(qū)別是什么呢?那這就不得不提到上文中的 __DEV__ 常量,當構(gòu)建用于 <script> 標簽的 ESM 資源時,如果是用于開發(fā)環(huán)境,那么 __DEV__ 會設(shè)置為 true;如果是用于生產(chǎn)環(huán)境,那么 __DEV__ 常量會被設(shè)置為 false ,從而被 Tree-Shaking 移除。但是當我們構(gòu)建提供給打包工具的 ESM 格式的資源時,我們不能直接把 __DEV__ 設(shè)置為 true 或 false,而是使用 (process.env.NODE_ENV !== 'production') 替換掉 __DEV__ 常量。例如下面的源碼:
if?(__DEV__)?{warn(`useCssModule()?is?not?supported?in?the?global?build.`) }在帶有 -bundler 字樣的資源中會變成:
if?((process.env.NODE_ENV?!==?'production'))?{warn(`useCssModule()?is?not?supported?in?the?global?build.`) }這樣用戶側(cè)的 webpack 配置可以自己決定構(gòu)建資源的目標環(huán)境,但是最終的效果其實是一樣的,這段代碼也只會出現(xiàn)在開發(fā)環(huán)境。
用戶除了可以直接使用 <script> 標簽引入資源,我們還希望用戶可以在 Node.js 中通過 require 語句引用資源,例如:
const?Vue?=?require('vue')為什么會有這種需求呢?答案是服務(wù)端渲染,當服務(wù)端渲染時 Vue 的代碼是運行在 Node.js 環(huán)境的,而非瀏覽器環(huán)境,在 Node.js 環(huán)境下資源的模塊格式應(yīng)該是 CommonJS ,簡稱 cjs。為了能夠輸出 cjs 模塊的資源,我們可以修改 rollup 的配置:format: 'cjs' 來實現(xiàn):
//?rollup.config.js const?config?=?{input:?'input.js',output:?{file:?'output.js',format:?'cjs'?//?指定模塊形式} }export?default?config特性開關(guān)
在設(shè)計框架時,框架會提供諸多特性(或功能)給用戶,例如我們提供 A、B、C 三個特性給用戶,同時呢我們還提供了 a、b、c 三個對應(yīng)的特性開關(guān),用戶可以通過設(shè)置 a、b、c 為 true 和 false 來代表開啟和關(guān)閉,那么將會帶來很多收益:
對于用戶關(guān)閉的特性,我們可以利用 Tree-Shaking 機制讓其不包含在最終的資源中。
該機制為框架設(shè)計帶來了靈活性,可以通過特性開關(guān)任意為框架添加新的特性而不用擔心用不到這些特性的用戶側(cè)資源體積變大,同時當框架升級時,我們也可以通過特性開關(guān)來支持遺留的 API,這樣新的用戶可以選擇不適用遺留的 API,從而做到用戶側(cè)資源最小化。
那怎么實現(xiàn)特性開關(guān)呢?其實很簡單,原理和上文提到的 __DEV__ 常量一樣,本質(zhì)是利用 rollup 的預(yù)定義常量插件來實現(xiàn),那一段 Vue3 的 rollup 配置來看:
{__FEATURE_OPTIONS_API__:?isBundlerESMBuild???`__VUE_OPTIONS_API__`?:?true, }其中 __FEATURE_OPTIONS_API__ 類似于 __DEV__,我們可以在 Vue3 的源碼中搜索,可以找到很多類似如下代碼這樣的判斷分支:
//?support?for?2.x?options if?(__FEATURE_OPTIONS_API__)?{currentInstance?=?instancepauseTracking()applyOptions(instance,?Component)resetTracking()currentInstance?=?null }當 Vue 構(gòu)建資源時,如果構(gòu)建的資源是用于給打包工具使用的話(即帶有 -bundler 字樣的資源),那么上面代碼在資源中會變成:
//?support?for?2.x?options if?(__VUE_OPTIONS_API__)?{?//?這一這里currentInstance?=?instancepauseTracking()applyOptions(instance,?Component)resetTracking()currentInstance?=?null }其中 __VUE_OPTIONS_API__ 就是一個特性開關(guān),用戶側(cè)就可以通過設(shè)置 __VUE_OPTIONS_API__ 來控制是否包含這段代碼。通常用戶可以使用 webpack.DefinePlugin 插件實現(xiàn):
//?webpack.DefinePlugin?插件配置 new?webpack.DefinePlugin({__VUE_OPTIONS_API__:?JSON.stringify(true)?//?開啟特性 })最后再來詳細解釋一下 __VUE_OPTIONS_API__ 開關(guān)是干嘛用的,在 Vue2 中我們編寫的組件叫做組件選項 API:
export?default?{data()?{},?//?data?選項computed:?{},?//?computed?選項//??其他選項... }但是在 Vue3 中,更推薦使用 Composition API 來編寫代碼,例如:
export?default?{setup()?{const?count?=?ref(0)const?doubleCount?=?computed(()?=>?count.value?*?2)?//?相當于?Vue2?中的?computed?選項} }但是為了兼容 Vue2,在 Vue3 中仍然可以使用選項 API 的方式編寫代碼,但是對于明確知道自己不會使用選項 API 的用戶來說,它們就可以選擇使用 __VUE_OPTIONS_API__ 開關(guān)來關(guān)閉該特性,這樣在打包的時候 Vue 的這部分代碼就不會包含在最終的資源中,從而減小資源體積。
錯誤處理
錯誤處理是開發(fā)框架的過程中非常重要的環(huán)節(jié),框架的錯誤處理做的好壞能夠直接決定用戶應(yīng)用程序的健壯性,同時還決定了用戶開發(fā)應(yīng)用時處理錯誤的心智負擔。
為了讓大家對錯誤處理的重要性有更加直觀的感受,我們從一個小例子說起。假設(shè)我們開發(fā)了一個工具模塊,代碼如下:
//?utils.js export?default?{foo(fn)?{fn?&&?fn()} }該模塊導(dǎo)出一個對象,其中 foo 屬性是一個函數(shù),接收一個回調(diào)函數(shù)作為參數(shù),調(diào)用 foo 函數(shù)時會執(zhí)行回調(diào)函數(shù),在用戶側(cè)使用時:
import?utils?from?'utils.js' utils.foo(()?=>?{//?... })大家思考一下如果用戶提供的回調(diào)函數(shù)在執(zhí)行的時候出錯了怎么辦?此時有兩個辦法,其一是讓用戶自行處理,這需要用戶自己去 try...catch:
import?utils?from?'utils.js' utils.foo(()?=>?{try?{//?...}?catch?(e)?{//?...} })但是這對用戶來說是增加了負擔,試想一下如果 utils.js 不是僅僅提供了一個 foo 函數(shù),而是提供了幾十上百個類似的函數(shù),那么用戶在使用的時候就需要逐一添加錯誤處理程序。
第二種辦法是我們代替用戶統(tǒng)一處理錯誤,如下代碼所示:
//?utils.js export?default?{foo(fn)?{try?{fn?&&?fn()?}?catch(e)?{/*?...?*/}},bar(fn)?{try?{fn?&&?fn()?}?catch(e)?{/*?...?*/}}, }這中辦法其實就是我們代替用戶編寫錯誤處理程序,實際上我們可以進一步封裝錯誤處理程序為一個函數(shù),假設(shè)叫它 callWithErrorHandling:
//?utils.js export?default?{foo(fn)?{callWithErrorHandling(fn)},bar(fn)?{callWithErrorHandling(fn)}, } function?callWithErrorHandling(fn)?{try?{fn?&&?fn()}?catch?(e)?{console.log(e)} }可以看到代碼變得簡潔多了,但簡潔不是目的,這么做真正的好處是,我們有機會為用戶提供統(tǒng)一的錯誤處理接口,如下代碼所示:
//?utils.js let?handleError?=?null export?default?{foo(fn)?{callWithErrorHandling(fn)},//?用戶可以調(diào)用該函數(shù)注冊統(tǒng)一的錯誤處理函數(shù)resigterErrorHandler(fn)?{handleError?=?fn} } function?callWithErrorHandling(fn)?{try?{fn?&&?fn()}?catch?(e)?{//?捕獲到的錯誤傳遞給用戶的錯誤處理程序handleError(e)} }我們提供了 resigterErrorHandler 函數(shù),用戶可以使用它注冊錯誤處理程序,然后在 callWithErrorHandling 函數(shù)內(nèi)部捕獲到錯誤時,把錯誤對象傳遞給用戶注冊的錯誤處理程序。
這樣在用戶側(cè)的代碼就會非常簡潔且健壯:
import?utils?from?'utils.js' //?注冊錯誤處理程序 utils.resigterErrorHandler((e)?=>?{console.log(e) }) utils.foo(()?=>?{/*...*/}) utils.bar(()?=>?{/*...*/})這時錯誤處理的能力完全由用戶控制,用戶既可以選擇忽略錯誤,也可以調(diào)用上報程序?qū)㈠e誤上報到監(jiān)控系統(tǒng)。
實際上這就是 Vue 錯誤處理的原理,你可以在源碼中搜索到 callWithErrorHandling 函數(shù),另外在 Vue 中我們也可以注冊統(tǒng)一的錯誤處理函數(shù):
import?App?from?'App.vue' const?app?=?createApp(App) app.config.errorHandler?=?()?=>?{//?錯誤處理程序 }良好的 Typescript 類型支持
Typescript 是微軟開源的編程語言,簡稱 TS,它是 JS 的超集能夠為 JS 提供類型支持。現(xiàn)在越來越多的人和團隊在他們的項目中使用 TS 語言,使用 TS 的好處很多,如代碼即文檔、編輯器的自動提示、一定程度上能夠避免低級 bug、讓代碼的可維護性更強等等。因此對 TS 類型支持的是否完善也成為評價一個框架的重要指標。
那如何衡量一個框架對 TS 類型支持的好壞呢?這里有一個常見的誤區(qū),很多同學以為只要是使用 TS 編寫就是對 TS 類型支持的友好,其實使用 TS 編寫框架和框架對 TS 類型支持的友好是兩件關(guān)系不大的事兒??紤]到有的同學可能沒有接觸過 TS,所以這里不會做深入討論,我們只舉一個簡單的例子,如下是使用 TS 編寫的函數(shù):
function?foo(val:?any)?{return?val }這個函數(shù)很簡單,它接受一個參數(shù) val 并且參數(shù)可以是任意類型(any),該函數(shù)直接將參數(shù)作為返回值,這說明返回值的類型是由參數(shù)決定的,參數(shù)如果是 number 類型那么返回值也是 number 類型,然后我們可以嘗試使用一下這個函數(shù),如下圖所示:
類型支持不友好
在調(diào)用 foo 函數(shù)時我們傳遞了一個字符串類型的參數(shù) 'str',按照之前的分析,我們得到的結(jié)果 res 的類型應(yīng)該也是字符串類型,然而當我們把鼠標 hover 到 res 常量上時可以看到其類型是 any,這并不是我們想要的結(jié)果,為了達到理想狀態(tài)我們只需要對 foo 函數(shù)做簡單的修改即可:
function?foo<T?extends?any>(val:?T):?T?{return?val }大家不需要理解這段代碼,我們直接來看一下現(xiàn)在的表現(xiàn):
類型友好
可以看到 res 的類型是字符字面量 'str' 而不是 any 了,這說明我們的代碼生效了。
通過這個簡單的例子我們認識到,使用 TS 編寫代碼與對 TS 類型支持友好是兩件事,在編寫大型框架時想要做到完美的 TS 類型支持是一件很不容易的事情,大家可以查看 Vue 源碼中的 runtime-core/src/apiDefineComponent.ts 文件,整個文件里真正會在瀏覽器運行的代碼其實只有 3 行,但是當你打開這個文件的時候你會發(fā)現(xiàn)它整整有接近 200 行的代碼,其實這些代碼都是在做類型支持方面的事情,由此可見框架想要做到完善的類型支持是需要付出相當大的努力的。
除了要花大力氣做類型推導(dǎo),從而做到更好的類型支持外,還要考慮對 TSX 的支持,我們會單獨一篇來詳細討論。
以上,歡迎分享、關(guān)注。
最近組建了一個江西人的前端交流群,如果你也是江西人可以加我微信ruochuan12 拉你進群。
常駐推薦閱讀
若川知乎高贊:有哪些必看的 JS庫?
我在阿里招前端,我該怎么幫你?(現(xiàn)在還可以加模擬面試群)
如何拿下阿里巴巴 P6 的前端 Offer
如何準備阿里P6/P7前端面試--項目經(jīng)歷準備篇
大廠面試官常問的亮點,該如何做出?
如何從初級到專家(P4-P7)打破成長瓶頸和有效突破
若川知乎問答:2年前端經(jīng)驗,做的項目沒什么技術(shù)含量,怎么辦?
常駐末尾
你好,我是若川,江西人~(點擊藍字了解我)歷時一年只寫了一個學習源碼整體架構(gòu)系列?有哪些必看的JS庫:jQuery、underscore、lodash、sentry、vuex、axios、koa、redux
關(guān)注若川視野,回復(fù)"pdf" 領(lǐng)取優(yōu)質(zhì)前端書籍pdf,回復(fù)"1",可加群長期交流學習
我的博客地址:https://lxchuan12.gitee.io?歡迎收藏
覺得文章不錯,可以?分享、點贊、在看?呀^_^另外歡迎留言交流~
小提醒:若川視野公眾號面試、源碼等文章合集在菜單欄中間【源碼精選】按鈕,歡迎點擊閱讀,也可以星標我的公眾號,便于查找
總結(jié)
以上是生活随笔為你收集整理的我从 Vuejs 中学到了什么——框架设计学问的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MaxDEA如何计算超效率DEA
- 下一篇: MobX 上手指南,写 Vue 的感觉?