很多人只是會用 Vue.js,而我卻學到了這幾點

框架設計遠沒有大家想的那麼簡單,並不是說只把功能開發完成,能用就算完事兒了,這裏面還是有很多學問的。

比如說,我們的框架應該給用戶提供哪些構建產物?產物的模塊格式如何?當用戶沒有以預期的方式使用框架時是否應該打印合適的警告信息從而提升更好的開發體驗,讓用戶快速定位問題?開發版本的構建和生產版本的構建有何區別?熱跟新(HMR:Hot Module Replacement)需要框架層面的支持纔行,我們是否也應該考慮?

再有就是當你的框架提供了多個功能,如果用戶只需要其中幾個功能,那麼用戶是否可以選擇關閉其他功能從而減少資源的打包體積?所有以上這些問題我們都會在本節內容進行討論。

本節內容需要大家對常用的模塊打包工具有一定的使用經驗,尤其是 rollup.js 以及 webpack。如果你只用過或瞭解過其中一個也沒關係,因爲它們很多概念其實是類似的。如果你沒有使用任何模塊打包工具那麼需要你自行去了解一下,至少有了初步認識之後再來看本節內容會更好一些。


提升用戶的開發體驗

衡量一個框架是否足夠優秀的指標之一就是看它的開發體驗如何,我們拿 Vue3 舉個例子:

createApp(App).mount('#not-exist')

當我們創建一個 Vue 應用並試圖將其掛載到一個不存在的 DOM 節點時就會得到一個警告信息:

warn

從這條信息中我們得知掛載失敗了,並說明了失敗的原因:Vue 根據我們提供的選擇器無法找到相應的 DOM 元素(返回 null),正式因爲這條信息的存在使得我們能夠清晰且快速的瞭解並定位問題,可以試想一下如果 Vue 內部不做任何處理,那麼很可能得到的是一個 JS 層面的錯誤信息,例如:Uncaught TypeError: Cannot read property 'xxx' of null,但是根據此信息我們很難知道問題出在哪裏。

所以在框架設計和開發的過程中,提供友好的警告信息是至關重要的,如果這一點做得不好那麼很可能經常收到用戶的抱怨。始終提供友好的警告信息不僅能夠快速幫助用戶定位問題,節省用戶的時間,還能夠爲框架收穫良好的口碑,讓用戶認爲你是非常專業的。

在 Vue 的源碼中,你經常能夠看到 warn() 函數的調用,例如上面圖片中的信息就是由這句 warn() 函數調用打印的:

warn(
  `Failed to mount app: mount target selector "${container}" returned null.`
)

對於 warn() 函數來說,由於它需要儘可能的提供有用的信息,因此它需要收集當前發生錯誤的組件的組件棧信息,所以如果你去看源碼你會發現有些複雜,但其實最終就是調用了 console.warn() 函數。

對於開發體驗來說,除了提供必要的警告信息,還有很多其他方面可以作爲切入口,可以進一步提升用戶的開發體驗。例如在 Vue3 中當我們在控制檯打印一個 Ref 數據時:

const count = ref(0)
console.log(count)

打開控制檯查看輸出,如下圖所示:

沒有任何處理的輸出

可以發現非常的不直觀,當然我們可以直接打印 count.value ,這樣就只會輸出 0,但是有沒有辦法在打印 count 的時候讓輸出的信息更有好呢?當然可以,瀏覽允許我們編寫自定義的 formatter,從而自定義輸出的形式。在 Vue 的源碼中你可以搜索到名爲 initCustomFormatter 的函數,這個函數就是用來在開發環境下初始化自定義 formatter 的,以 chrome 爲例我們可以打開 devtool 的設置,然後勾選 Console -> Enable custom formatters

然後刷新瀏覽器後查看控制檯,會發現輸出的內容變得非常直觀:

控制框架代碼的體積

框架的大小也是衡量框架的標準之一,在實現同樣功能的情況下當然是用越少的代碼越好,這樣體積就會越小,最後瀏覽器加載資源的時間也就越少。這時我們不禁會想,提供越完善的警告信息就意味着我們要編寫更多的代碼,這不是與控制代碼體積相駁嗎?沒錯,所以我們要想辦法解決這個問題。

如果我們去看 Vue 的源碼會發現,每一個 warn() 函數的調用都會配合 __DEV__ 常量的檢查,例如:

if (__DEV__ && !res) {
  warn(
    `Failed to mount app: mount target selector "${container}" returned null.`
  )
}

可以看到,打印警告信息的前提是:__DEV__ 這個常量一定要爲真,這裏的 __DEV__ 常量就是達到目的的關鍵。

Vue 使用的是 rollup.js 對項目進行構建的,這裏的 __DEV__ 常量實際上是通過 rollup 的配置來預定義的,其功能類似於 webpack 中的 DefinePlugin 插件。

Vue 在輸出資源的時候,會輸出兩個版本的資源,其中一個資源用於開發環境,如 vue.global.js ;另一個與其對應的用於生產環境,如:vue.global.prod.js ,通過文件名稱我們也能夠區分。

當 Vue 構建用於開發環境的資源時,會把 __DEV__ 常量設置爲 true,這時上面那段輸出警告信息的代碼就等價於:

if (true && !res) {
  warn(
    `Failed to mount app: mount target selector "${container}" returned null.`
  )
}

可以看到這裏的 __DEV__ 被替換成了字面量 true ,所以這段代碼在開發環境是肯定存在的。

當 Vue 構建用於生產環境的資源時,會把 __DEV__ 常量設置爲 false,這時上面那段輸出警告信息的代碼就等價於:

if (false && !res) {
  warn(
    `Failed to mount app: mount target selector "${container}" returned null.`
  )
}

可以看到 __DEV__ 常量被替換爲字面量 false ,這時我們發現這段分支代碼永遠都不會執行,因爲判斷條件始終爲假,這段永遠不會執行的代碼被稱爲 Dead Code,它不會出現在最終的產物中,在構建資源的時候就會被移除,因此在 vue.global.prod.js 中是不會存在這段代碼的。

這樣我們就做到了在開發環境爲用戶提供友好的警告信息的同時,還不會增加生產環境代碼的體積


框架要做到良好的 Tree-Shaking

上文中我們提到通過構建工具設置預定義的常量 __DEV__ ,就能夠做到在生產環境使得框架不包含打印警告信息的代碼,從而使得框架自身的代碼量變少。但是從用戶的角度來看,這麼做仍然不夠,還是拿 Vue 來舉個例子,我們知道 Vue 提供了內置的組件例如 <Transition> ,如果我們的項目中根本就沒有使用到該組件,那麼 <Transition> 組件的代碼需要包含在我們項目最終的構建資源中嗎?答案是當然不需要,那如何做到這一點呢?這就不得不提到本節的主角 Tree-Shaking

那什麼是 Tree-Shaking 呢?在前端領域這個概念因 rollup 而普及,簡單的說所謂 **Tree-Shaking **指的就是消除哪些永遠不會執行的代碼,也就是排除 dead-code,現在無論是 rollup 還是 webpack 都支持 Tree-Shaking

想要實現 Tree-Shaking 必須滿足一個條件,即模塊必須是 ES Module,因爲 Tree-Shaking 依賴 ESM 的靜態結構。我們使用 rollup 通過一個簡單的例子看看 Tree-Shaking 如何工作,我們 demo 的目錄結構如下:

├── demo
│   └── package.json
│   └── input.js
│   └── utils.js

首先安裝 rollup

yarn add rollup -D # 或者 npm install rollup -D

下面是 input.jsutils.js 文件的內容:

// 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 文件中定義並導出了兩個函數,分別是 foobar,然後在 input.js 中導入了 foo 函數並執行,注意我們並沒有導入 bar 函數。

接着我們執行如下命令使用 rollup 構建:

npx rollup input.js -f esm -o bundle.js

這句命令的意思是以 input.js 文件問入口,輸出 ESM 模塊,輸出的文件名叫做 bundle.js 。命令執行成功後,我們打開 bundle.js 來查看一下它的內容:

// bundle.js
function foo(obj{
  obj && obj.foo
}
foo();

可以看到,其中並不包含 bar 函數,這說明 Tree-Shaking 起了作用,由於我們並沒有使用 bar 函數,因此它作爲 dead-code 被刪除了。但是如果我們仔細觀察會發現,foo 函數的執行也沒啥意義呀,就是讀取了對象的值,所以它執行還是不執行也沒有本質的區別呀,所以即使把這段代碼刪了,也對我們的應用沒啥影響,那爲什麼 rollup 不把這段代碼也作爲 dead-code 移除呢?

這就涉及到 Tree-Shaking 中的第二個關鍵點,即副作用。如果一個函數調用會產生副作用,那麼就不能將其移除。什麼是副作用?簡單地說副作用的意思是當調用函數的時候,會對外部產生影響,例如修改了全局變量。這時你可能會說,上面的代碼明顯是讀取對象的值怎麼會產生副作用呢?其實是有可能的,想想一下如果 obj 對象是一個通過 Proxy 創建的代理對象那麼當我們讀取對象屬性時就會觸發 Getter ,在 Getter 中是可能產生副作用的,例如我們在 Getter 中修改了某個全局變量。而到底會不會產生副作用,這個只有代碼真正運行的時候才能知道, JS 本身是動態語言,想要靜態的分析哪些代碼是 dead-code 是一件很有難度的事兒,上面只是舉了一個簡單的例子。

正因爲靜態分析 JS 代碼很困難,所以諸如 rollup 等這類工具都會給我提供一個機制,讓我們有能力明確的告訴 rollup :”放心吧,這段代碼不會產生副作用,你可以放心移除它“,那具體怎麼做呢?如下代碼所示,我們修改 input.js 文件:

import {foo} from './utils'

/*#__PURE__*/ foo()

注意這段註釋代碼 /*#__PURE_*_/,該註釋的作用就是用來告訴 rollup 對於 foo() 函數的調用不會產生副作用,你可以放心的對其進行 Tree-Shaking,此時再次執行構建命令並查看 bundle.js 文件你會發現它的內容是空的,這說明 Tree-Shaking 生效了。

基於這個案例大家應該明白的是,在編寫框架的時候我們需要合理的使用 /*#__PURE_*_/ 註釋,如果你去搜索 Vue 的源碼會發現它大量的使用了該註釋,例如下面這句:

export const isHTMLTag = /*#__PURE__*/ makeMap(HTML_TAGS)

也許你會覺得這會不會對編寫代碼帶來很大的心智負擔?其實不會,這是因爲通常產生副作用的代碼都是模塊內函數的頂級調用,什麼是頂級調用呢?如下代碼所示:

foo() // 頂級調用

function bar({
  foo() // 函數內調用
}

可以看到對於頂級調用來說是可能產生副作用的,但對於函數內調用來說只要函數 bar 沒有被調用,那麼 foo 函數的調用當然不會產生副作用。因此你會發現在 Vue 的源碼中,基本都是在一些頂級調用的函數上使用 /*#__PURE__*/ 註釋的。當然該註釋不僅僅作用與函數,它可以使用在任何語句上,這個註釋也不是隻有 rollup 才能識別,webpack 以及壓縮工具如 terser 都能識別它。


框架應該輸出怎樣的構建產物

上文中我們提到 Vue 會爲開發環境和生產環境輸出不同的包,例如 vue.global.js 用於開發環境,它包含了必要的警告信息,而 vue.global.prod.js 用於生產環境,不包含警告信息。實際上 Vue 的構建產物除了有環境上的區分之外,還會根據使用場景的不同而輸出其他形式的產物,這一節我們將討論這些產物的用途以及在構建階段如何輸出這些產物。

不同類型的產物一定是有對應的需求背景的,因此我們從需求講起。首先我們希望用戶可以直接在 html 頁面中使用 <script> 標籤引入框架並使用:

<body>
  <script src="/path/to/vue.js"></script>
  <script>
  const { createApp } = Vue
  // ...
  
</script>
</body>

爲了能夠實現這個需求,我們就需要輸出一種叫做 IIFE 格式的資源,IIFE 的全稱是 Immediately Invoked Function Expression ,即”立即調用的函數表達式“,可以很容易的用 JS 來表達:

(function ({
  // ...
}())

如上代碼所示,這就是一個立即執行的函數表達式。實際上 vue.globale.js 文件就是 IIFE 形式的資源,大家可以看一下它的代碼結構:

var Vue = (function(exports){
  // ...
 exports.createApp = createApp;
  // ...
  return exports
}({}))

這樣當我們使用 <script> 標籤直接引入 vue.global.js 文件後,那麼全局變量 Vue 就是可用的了。

rollup 中我們可以通過配置 format: 'iife' 來實現輸出這種形式的資源:

// rollup.config.js
const config = {
  input'input.js',
  output: {
    file'output.js',
    format'iife' // 指定模塊形式
  }
}

export default config

不過隨着技術的發展和瀏覽器的支持,現在主流瀏覽器對原生 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'

你可能已經注意到了,爲什麼 vue.esm-browser.js 文件中會有 -browser 字樣,其實對於 ESM 格式的資源來說,Vue 還會輸出一個 vue.esm-bundler.js 文件,其中 -browser 變成了 -bundler。爲什麼這麼做呢?我們知道無論是 rollup 還是 webpack 在尋找資源時,如果 package.json 中存在 module 字段,那麼會優先使用 module 字段指向的資源來代替 main 字段所指向的資源。我們可以打開 Vue 源碼中的 packages/vue/package.json 文件看一下:

{
 "main""index.js",
  "module""dist/vue.runtime.esm-bundler.js",
}

其中 module 字段指向的是 vue.runtime.esm-bundler.js 文件,意思就是說如果你的項目是使用 webpack 構建的,那你使用的 Vue 資源就是 vue.runtime.esm-bundler.js ,也就是說帶有 -bundler 字樣的 ESM 資源是給 rollup 或 webpack 等打包工具使用的,而帶有 -browser 字樣的 ESM 資源是直接給 <script type="module"> 去使用的。

那他們之間的區別是什麼呢?那這就不得不提到上文中的 __DEV__ 常量,當構建用於 <script> 標籤的 ESM 資源時,如果是用於開發環境,那麼 __DEV__ 會設置爲 true;如果是用於生產環境,那麼 __DEV__ 常量會被設置爲 false ,從而被 Tree-Shaking 移除。但是當我們構建提供給打包工具的 ESM 格式的資源時,我們不能直接把 __DEV__ 設置爲 truefalse,而是使用 (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.`)
}

這樣用戶側的 webpack 配置可以自己決定構建資源的目標環境,但是最終的效果其實是一樣的,這段代碼也只會出現在開發環境。

用戶除了可以直接使用 <script> 標籤引入資源,我們還希望用戶可以在 Node.js 中通過 require 語句引用資源,例如:

const Vue = require('vue')

爲什麼會有這種需求呢?答案是服務端渲染,當服務端渲染時 Vue 的代碼是運行在 Node.js 環境的,而非瀏覽器環境,在 Node.js 環境下資源的模塊格式應該是 CommonJS ,簡稱 cjs。爲了能夠輸出 cjs 模塊的資源,我們可以修改 rollup 的配置:format: 'cjs' 來實現:

// rollup.config.js
const config = {
  input'input.js',
  output: {
    file'output.js',
    format'cjs' // 指定模塊形式
  }
}

export default config


特性開關

在設計框架時,框架會提供諸多特性(或功能)給用戶,例如我們提供 A、B、C 三個特性給用戶,同時呢我們還提供了 a、b、c 三個對應的特性開關,用戶可以通過設置 a、b、c 爲 truefalse 來代表開啓和關閉,那麼將會帶來很多收益:

  1. 對於用戶關閉的特性,我們可以利用 Tree-Shaking 機制讓其不包含在最終的資源中。
  2. 該機制爲框架設計帶來了靈活性,可以通過特性開關任意爲框架添加新的特性而不用擔心用不到這些特性的用戶側資源體積變大,同時當框架升級時,我們也可以通過特性開關來支持遺留的 API,這樣新的用戶可以選擇不適用遺留的 API,從而做到用戶側資源最小化。

那怎麼實現特性開關呢?其實很簡單,原理和上文提到的 __DEV__ 常量一樣,本質是利用 rollup 的預定義常量插件來實現,那一段 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 = instance
  pauseTracking()
  applyOptions(instance, Component)
  resetTracking()
  currentInstance = null
}

當 Vue 構建資源時,如果構建的資源是用於給打包工具使用的話(即帶有 -bundler 字樣的資源),那麼上面代碼在資源中會變成:

// support for 2.x options
if (__VUE_OPTIONS_API__) { // 這一這裏
  currentInstance = instance
  pauseTracking()
  applyOptions(instance, Component)
  resetTracking()
  currentInstance = null
}

其中 __VUE_OPTIONS_API__ 就是一個特性開關,用戶側就可以通過設置 __VUE_OPTIONS_API__ 來控制是否包含這段代碼。通常用戶可以使用 webpack.DefinePlugin 插件實現:

// webpack.DefinePlugin 插件配置
new webpack.DefinePlugin({
  __VUE_OPTIONS_API__JSON.stringify(true// 開啓特性
})

最後再來詳細解釋一下 __VUE_OPTIONS_API__ 開關是幹嘛用的,在 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__ 開關來關閉該特性,這樣在打包的時候 Vue 的這部分代碼就不會包含在最終的資源中,從而減小資源體積。


錯誤處理

錯誤處理是開發框架的過程中非常重要的環節,框架的錯誤處理做的好壞能夠直接決定用戶應用程序的健壯性,同時還決定了用戶開發應用時處理錯誤的心智負擔。

爲了讓大家對錯誤處理的重要性有更加直觀的感受,我們從一個小例子說起。假設我們開發了一個工具模塊,代碼如下:

// utils.js
export default {
  foo(fn) {
    fn && fn()
  }
}

該模塊導出一個對象,其中 foo 屬性是一個函數,接收一個回調函數作爲參數,調用 foo 函數時會執行回調函數,在用戶側使用時:

import utils from 'utils.js'
utils.foo(() => {
  // ...
})

大家思考一下如果用戶提供的回調函數在執行的時候出錯了怎麼辦?此時有兩個辦法,其一是讓用戶自行處理,這需要用戶自己去 try...catch

import utils from 'utils.js'
utils.foo(() => {
  try {
   // ...
  } catch (e) {
   // ...
 }
})

但是這對用戶來說是增加了負擔,試想一下如果 utils.js 不是僅僅提供了一個 foo 函數,而是提供了幾十上百個類似的函數,那麼用戶在使用的時候就需要逐一添加錯誤處理程序。

第二種辦法是我們代替用戶統一處理錯誤,如下代碼所示:

// utils.js
export default {
  foo(fn) {
    try {
      fn && fn() 
    } catch(e) {/* ... */}
  },
  bar(fn) {
    try {
      fn && fn() 
    } catch(e) {/* ... */}
  },
}

這中辦法其實就是我們代替用戶編寫錯誤處理程序,實際上我們可以進一步封裝錯誤處理程序爲一個函數,假設叫它 callWithErrorHandling

// utils.js
export default {
  foo(fn) {
    callWithErrorHandling(fn)
  },
  bar(fn) {
    callWithErrorHandling(fn)
  },
}
function callWithErrorHandling(fn{
  try {
    fn && fn()
  } catch (e) {
    console.log(e)
  }
}

可以看到代碼變得簡潔多了,但簡潔不是目的,這麼做真正的好處是,我們有機會爲用戶提供統一的錯誤處理接口,如下代碼所示:

// utils.js
let handleError = null
export default {
  foo(fn) {
    callWithErrorHandling(fn)
  },
  // 用戶可以調用該函數註冊統一的錯誤處理函數
  resigterErrorHandler(fn) {
    handleError = fn
  }
}
function callWithErrorHandling(fn{
  try {
    fn && fn()
  } catch (e) {
    // 捕獲到的錯誤傳遞給用戶的錯誤處理程序
    handleError(e)
  }
}

我們提供了 resigterErrorHandler 函數,用戶可以使用它註冊錯誤處理程序,然後在 callWithErrorHandling 函數內部捕獲到錯誤時,把錯誤對象傳遞給用戶註冊的錯誤處理程序。

這樣在用戶側的代碼就會非常簡潔且健壯:

import utils from 'utils.js'
// 註冊錯誤處理程序
utils.resigterErrorHandler((e) => {
  console.log(e)
})
utils.foo(() => {/*...*/})
utils.bar(() => {/*...*/})

這時錯誤處理的能力完全由用戶控制,用戶既可以選擇忽略錯誤,也可以調用上報程序將錯誤上報到監控系統。

實際上這就是 Vue 錯誤處理的原理,你可以在源碼中搜索到 callWithErrorHandling 函數,另外在 Vue 中我們也可以註冊統一的錯誤處理函數:

import App from 'App.vue'
const app = createApp(App)
app.config.errorHandler = () => {
  // 錯誤處理程序
}


良好的 Typescript 類型支持

Typescript 是微軟開源的編程語言,簡稱 TS,它是 JS 的超集能夠爲 JS 提供類型支持。現在越來越多的人和團隊在他們的項目中使用 TS 語言,使用 TS 的好處很多,如代碼即文檔、編輯器的自動提示、一定程度上能夠避免低級 bug、讓代碼的可維護性更強等等。因此對 TS 類型支持的是否完善也成爲評價一個框架的重要指標。

那如何衡量一個框架對 TS 類型支持的好壞呢?這裏有一個常見的誤區,很多同學以爲只要是使用 TS 編寫就是對 TS 類型支持的友好,其實使用 TS 編寫框架和框架對 TS 類型支持的友好是兩件關係不大的事兒。考慮到有的同學可能沒有接觸過 TS,所以這裏不會做深入討論,我們只舉一個簡單的例子,如下是使用 TS 編寫的函數:

function foo(val: any{
  return val
}

這個函數很簡單,它接受一個參數 val 並且參數可以是任意類型(any),該函數直接將參數作爲返回值,這說明返回值的類型是由參數決定的,參數如果是 number 類型那麼返回值也是 number 類型,然後我們可以嘗試使用一下這個函數,如下圖所示:

類型支持不友好

在調用 foo 函數時我們傳遞了一個字符串類型的參數 'str',按照之前的分析,我們得到的結果 res 的類型應該也是字符串類型,然而當我們把鼠標 hover 到 res 常量上時可以看到其類型是 any,這並不是我們想要的結果,爲了達到理想狀態我們只需要對 foo 函數做簡單的修改即可:

function foo<T extends any>(val: T): T {
  return val
}

大家不需要理解這段代碼,我們直接來看一下現在的表現:

類型友好

可以看到 res 的類型是字符字面量 'str' 而不是 any 了,這說明我們的代碼生效了。

通過這個簡單的例子我們認識到,使用 TS 編寫代碼與對 TS 類型支持友好是兩件事,在編寫大型框架時想要做到完美的 TS 類型支持是一件很不容易的事情,大家可以查看 Vue 源碼中的 runtime-core/src/apiDefineComponent.ts 文件,整個文件裏真正會在瀏覽器運行的代碼其實只有 3 行,但是當你打開這個文件的時候你會發現它整整有接近 200 行的代碼,其實這些代碼都是在做類型支持方面的事情,由此可見框架想要做到完善的類型支持是需要付出相當大的努力的。

除了要花大力氣做類型推導,從而做到更好的類型支持外,還要考慮對 TSX 的支持,我們會單獨一篇來詳細討論。

以上,歡迎分享、關注。


順手點“在看”,每天早下班;轉發加關注,共奔小康路~

加站長好友可進微信羣,跟衆多大佬一起交流技術!


本文分享自微信公衆號 - 1024譯站(trans1024)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章