第 59 篇原創好文~
本文首發於政採雲前端團隊博客: 擁抱 Vue 3 系列之 JSX 語法
![]()
「別再更新了,學不動了」。這句話不知道出了多少開發者的辛酸。在過去的一年中,Vue 團隊一直都在開發 Vue.js 的下一個主要版本,就在 6 月底,尤大更新同步了 Vue 3 及其周邊生態的狀態(Vue 3: mid 2020 status update)。html
if (isTrue("I am planning to use Vue 3 for a new project")) { if (isTrue("I need IE11 support")) { await IE11CompatBuild() // July 2020 } if (isTrue("RFCs are too dense, I need an easy-to-read guide")) { await migrationGuide() // July 2020 } if (isTrue("I'd rather wait until it's really ready") { await finalRelease() // Targeting early August 2020 }) run(`npm init vite-app hello-vue3`) return }
咱們能夠看到,若是一切順利的話,預計在 8 月份,Vue 3 的正式版本就能夠和咱們見面了,目前距離發佈正式版還有必定的差距,還要作一些兼容性的工做。同時還會提供對 IE11 的支持。前端
Vue 3 爲了達到更快、更小、更易於維護、更貼近原生、對開發者更友好的目的,在不少方面進行了重構:vue
這是該系列文章的第一篇,後續會持續更新,覆蓋 Vue 3 生態經常使用庫。JSX 是一個小衆羣體使用開發方式,第一篇以 JSX 爲切入點,目標是讓大多數開發 Vue 的同窗也對 JSX 有必定的認知,在用 Vue 開發複雜應用時,也能有更加靈活的方式。git
好比當開始寫一個只能經過 level
prop 動態生成標題 (heading) 的組件時,你可能很快想到這樣實現:github
<script type="text/x-template" id="anchored-heading-template"> <h1 v-if="level === 1"> <slot></slot> </h1> <h2 v-else-if="level === 2"> <slot></slot> </h2> <h3 v-else-if="level === 3"> <slot></slot> </h3> </script>
這裏用模板並非最好的選擇,在每個級別的標題中重複書寫了 <slot></slot>
,不夠優雅。express
若是嘗試用 JSX 來寫,代碼就會變得簡單不少。npm
const App = { render() { const tag = `h${this.level}` return <tag>{this.$slots.default}</tag> } }
看過 Ant Design Vue 源碼 (下面簡稱爲 antdv) 的同窗應該知道, antdv 的底層是基於 JSX 來實現的,也是 Vue 生態中使用 JSX 的深度用戶。antd 爲了儘快的兼容 Vue 3,和 Vue 官方展開合做,因而一塊兒開發了 @ant-design-vue/babel-plugin-jsxapi
對於使用 React 的開發者來講,JSX 再熟悉不過了,可是若是你是一個 Vue 的重度用戶,可能對 JSX 不是特別熟悉,甚至聽到有同窗說沒有 template 的 Vue 項目沒有靈魂。babel
先來看下面一段代碼:antd
const el = <div>Vue 3</div>;
這段代碼既不是 HTML 也不是字符串,被稱之爲 JSX,是 JavaScript 的擴展語法。JSX 可能會令人聯想到模板語法,可是它具有 Javascript 的徹底變成能力。
看到這裏可能會有疑問,很多同窗可能會覺得 JSX 是 React 中特有的,其實否則。大多數同窗都知道,咱們日常在 .vue 文件中開發的代碼,實際上會被 vue-loader 處理,但可能少數同窗去看過咱們手把手寫出的代碼,會變編譯成啥樣。有興趣的同窗能夠戳這個地址來看下。vue-template-explorer (由於衆所周知的緣由,可能訪問略慢)
<div id="app">{{ msg }}</div>
function render() { with(this) { return _c('div', { attrs: { "id": "app" } }, [_v(_s(msg))]) } }
觀察上述代碼咱們發現,到運行階段實際上都是 render 函數在執行。Vue 推薦在絕大多數狀況下使用 template 來建立你的 HTML。然而在一些場景中,就須要使用 render 函數,它比 template 更加靈活。
寫過 render 函數的同窗可能深有體會,書寫複雜的 render 函數異常痛苦,並且難以維護,很是容易被稱之爲 「祖傳代碼」。好在 2.0 的官方提供了一個 Babel 插件,能夠將更接近於模板語法的 JSX 轉譯成 JavaScript。
使用過 React 的同窗對於如何寫 JSX 語法必定很是熟悉了,然而,Vue 2 中 的 JSX 寫法和 React 仍是有一些略微的區別。React 中全部傳遞的數據都掛在頂層。
const App = <A className="x" style={style} onChange={onChange} />
Vue 2 中,僅僅屬性就有三種:組件屬性 props
,普通 html 屬性 attrs
,DOM 屬性 domProps
。想要更多瞭解如何在 Vue 2 中寫 JSX 語法,能夠看這篇,在 Vue 中使用 JSX 的正確姿式。
Vue 3 中,屬性這塊的傳遞和 React 相似,意味這不須要再傳遞 props,attrs 這些屬性。
// before { class: ['foo', 'bar'], style: { color: 'red' }, attrs: { id: 'foo' }, domProps: { innerHTML: '' }, on: { click: foo }, key: 'foo' } // after { class: ['foo', 'bar'], style: { color: 'red' }, id: 'foo', innerHTML: '', onClick: foo, key: 'foo' }
Vue 3 把大多數全局 API 和 內部 helper 移到了 ES 模塊中導出(譬如 v-model、transition、teleport),從而使得 Vue 3 在增長了不少新特性以後,基線的體積反而小了。
v-model
、v-show
這些 API 所有經過模塊導出的方式來引入
基線體積: 沒法捨棄的代碼體積
咱們來看一段很是簡單的代碼 <input v-model="x" />
,在 Vue 2 和 Vue 3 中的編譯結果有何不一樣
// before function render() { with(this) { return _c('input', { directives: [{ name: "model", rawName: "v-model", value: (x), expression: "x" }], domProps: { "value": (x) }, on: { "input": function ($event) { if ($event.target.composing) return; x = $event.target.value } } }) } }
// after import { vModelText as _vModelText, createVNode as _createVNode, withDirectives as _withDirectives, openBlock as _openBlock, createBlock as _createBlock } from "vue" export function render(_ctx, _cache) { return _withDirectives((_openBlock(), _createBlock("input", { "onUpdate:modelValue": $event => (_ctx.x = $event) }, null, 8 /* PROPS */, ["onUpdate:modelValue"])), [ [_vModelText, _ctx.x] ]) }
能夠看到在 Vue 3 中,對各個 API 作了更加細緻的拆分,理想狀態下,用戶能夠在構建時利用搖樹優化 (tree-shaking) 去掉框架中不須要的特性,只保留本身用到的特性。模版編譯器會生成適合作 tree-shaking 的代碼,不須要使用者去關心如何去作,這部分的改動一樣須要在 JSX 寫法中實現。
模板編譯器中增長了 PatchFlag
,在 JSX 的編譯過程一樣也作了處理,性能會有提高,可是考慮到 JSX 的靈活性,作了一些兼容處理,該功能還在測試階段。
Vue 3 雖然引入了一部分破壞性的更新,但對於絕大多數 Vue 2 的 API 仍是兼容的。那麼一樣的,咱們也要儘量讓使用 JSX 的用戶經過最小的成本升級到 Vue 3,這是一個核心的目標。寫這篇文章的時候,antdv 已經使用 @ant-design-vue/babel-plugin-jsx 重構了大約 70% 的功能,預計會在 Vue 3 正式版以前發佈測試版,大機率會是東半球最快兼容 Vue 3 的企業級組件庫。
const App = () => <div>Vue 3 JSX</div>
const App = { render() { return <div>Vue 3.0</div> } }
const App = defineComponent(() => { const count = ref(0); const inc = () => { count.value++; }; return () => ( <div onClick={inc}> {count.value} </div> ) })
const App = () => ( <> <span>I'm</span> <span>Fragment</span> </> )
Fragment 參考 React 的寫法,儘量寫起來更加方便
const App = () => <input type="email" />
const placeholderText = 'email' const App = () => ( <input type="email" placeholder={placeholderText} /> )
建議在 JSX 中使用駝峯 (vModel
),可是v-model
也能用
v-show
const App = { data() { return { visible: true }; }, render() { return <input vShow={this.visible} />; }, };
v-model
修飾符:使用 (_
) 代替 (.
) (vModel_trim={this.test}
)
export default { data: () => ({ test: 'Hello World', }), render() { return ( <> <input type="text" vModel_trim={this.test} /> {this.test} </> ) }, }
自定義指令
const App = { directives: { antRef }, setup() { return () => ( <a vAntRef={(ref) => { this.ref = ref; }} /> ); }, }
關於指令、插槽最終的 API 還在討論中,有想法的能夠去留言。Vue 3 JSX Design
因爲 antdv 的底層基本上都是基於 JSX 來寫的,想要快速遷移到 Vue 3,就必須有一個比較好的插件來支持,這也是爲何會有這個插件的緣由。固然在實現過程當中也踩了不少坑。
目前用法和 Vue 2 的語法大多數是一致的,爲了幫助更快遷移,在插件中作了針對舊 VNode 格式的兼容層,這裏只能兼容一部分寫法,以及部分語法的兼容會增長運行時的性能開銷,因此咱們但願可以將咱們的經驗分享給你們,讓你們少走彎路!
{ "plugins": ["@ant-design-vue/babel-plugin-jsx", { "transformOn": true, "compatibleProps": true }] }
針對 Vue 2 中 on: { click: xx }
寫法的兼容,在運行時中會轉爲 onClick: xxx
上文提到 Vue 3 對屬性的傳遞作了變動,props
、attrs
這些都不存在了,所以若是設置了這個屬性爲 true
,在運行時也會被解構到第一層的屬性中。
須要注意的一點,目前一旦開啓這兩個屬性,在 createVNode
的第二個參數,都會包一個 compatibleProps
和 transformOn
方法,因此酌情開啓這兩個參數。對於使用 Vue 2 的 JSX 同窗,若是沒有使用到比較」鮮爲人知「 的 API的狀況下,均可以快速得遷移。
那麼 antdv 又是如何作遷移的呢?考慮到 antdv 是個組件庫,都包一層 compatibleProps
勢必不太優雅,所以沒有選擇開啓這個兩個開關。這裏插一句,目前 antdv 的遷移還在進行中,相關的進度都在這個 issue 裏面(Vue 3 支持),有興趣的同窗能夠關注下,提一些 PR 過去。
對於 props 的遷移工做比較簡單,若是你是直接經過標籤的屬性來傳遞,那麼無須作更改。
<Modal visible={visible} />
若是是經過對象來傳遞的屬性,只須要把原有分散在 props
、on
、attrs
中的值直接鋪開便可。
const vcUploadProps = { - props: { - ...this.$props, - prefixCls, - beforeUpload: this.reBeforeUpload, - }, - on: { - start: this.onStart, - error: this.onError, - progress: this.onProgress, - success: this.onSuccess, - reject: this.onReject, - }, + ...this.$props, + prefixCls, + beforeUpload: this.reBeforeUpload, + onStart: this.onStart, + onError: this.onError, + onProgress: this.onProgress, + onSuccess: this.onSuccess, + onReject: this.onReject, + ref: 'uploadRef', + attrs: this.$attrs, + ...this.$attrs, };
可是關於 inheritAttrs
有個較爲底層的變更,須要開發者根據實際狀況去修改。什麼是inheritAttrs? 在 Vue 2 中,這個選項不影響 class
和 style
綁定,可是在 Vue 3 中會影響到。所以可能在屬性的傳遞上,須要額外對這兩個參數作處理。
在事件的處理上,咱們建議在 props 中聲明,這樣對後續的開發更加易維護,能夠很直觀地從 props 看出我這個組件到底會傳遞哪些事件。值得一提的是,在 props 中聲明的事件,也能夠經過 emit
來觸發。例如聲明瞭 onClick
事件,仍然可使用 emit('click')
。
Vue 3 對 context 的 API 也作了改動,通常若是不是複雜的組件,不會涉及到這個 API。這部分的改動能夠看原先 Vue Compositon API 的相關文檔,Dependency Injection,注意一點,在 setup 中取不到 this
。
現在有超過百萬的開發人員使用 Vue,還有超百萬的 React 開發者正在去使用 Vue 的路上。
雖說 Vue 中 JSX 的開發方式是一個少數羣裏,可是 antdv 的使用用戶也不是少數。爲了讓這部分用戶能夠快速體驗到兼容 Vue 3 版本的組件庫,所以在設計這個插件的時候,第一原則就是要最小的遷移和認知成本。
對於常年使用 template 的開發者來講,JSX 又未嘗不是一片新的天空呢?開發者要與使用者共情,站在使用者的角度出發,設計出的工具大機率可能知足其需求。
距離 JSX 發佈正式版本,還有一部分路要走。
最後要感謝 Vue.js 官方團隊,尤爲是 @sodatea 大佬的信任。
文中出現的倉庫地址:
擁抱 Vue 3 系列之如何開發設計一個 Vue 3 JSX 插件
政採雲前端團隊(ZooTeam),一個年輕富有激情和創造力的前端團隊,隸屬於政採雲產品研發部,Base 在風景如畫的杭州。團隊現有 50 餘個前端小夥伴,平均年齡 27 歲,近 3 成是全棧工程師,妥妥的青年風暴團。成員構成既有來自於阿里、網易的「老」兵,也有浙大、中科大、杭電等校的應屆新人。團隊在平常的業務對接以外,還在物料體系、工程平臺、搭建平臺、性能體驗、雲端應用、數據分析及可視化等方向進行技術探索和實戰,推進並落地了一系列的內部技術產品,持續探索前端技術體系的新邊界。
若是你想改變一直被事折騰,但願開始能折騰事;若是你想改變一直被告誡須要多些想法,卻無從破局;若是你想改變你有能力去作成那個結果,卻不須要你;若是你想改變你想作成的事須要一個團隊去支撐,但沒你帶人的位置;若是你想改變既定的節奏,將會是「5 年工做時間 3 年工做經驗」;若是你想改變原本悟性不錯,但老是有那一層窗戶紙的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但願參與到隨着業務騰飛的過程,親手推進一個有着深刻的業務理解、完善的技術體系、技術創造價值、影響力外溢的前端團隊的成長曆程,我以爲咱們該聊聊。任什麼時候間,等着你寫點什麼,發給 ZooTeam@cai-inc.com