1. 程式人生 > 其它 >我從 Vuejs 中學到了什麼

我從 Vuejs 中學到了什麼

技術標籤:vuejavascript

框架設計遠沒有大家想的那麼簡單,並不是說只把功能開發完成,能用就算完事兒了,這裡面還是有很多學問的。比如說,我們的框架應該給使用者提供哪些構建產物?產物的模組格式如何?當用戶沒有以預期的方式使用框架時是否應該打印合適的警告資訊從而提升更好的開發體驗,讓使用者快速定位問題?開發版本的構建和生產版本的構建有何區別?熱跟新(HMR:Hot Module Replacement)需要框架層面的支援才行,我們是否也應該考慮?再有就是當你的框架提供了多個功能,如果使用者只需要其中幾個功能,那麼使用者是否可以選擇關閉其他功能從而減少資源的打包體積?所有以上這些問題我們都會在本節內容進行討論。

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

提升使用者的開發體驗

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

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

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

從這條資訊中我們得知掛載失敗了,並說明了失敗的原因: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 選項
 }
}

但是在 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 的支援。

以上來源前端森林