擁抱 Vue 3 系列之 JSX 語法

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

  • 全面擁抱 TypeScript
  • 重構 complier
  • 重構 Virtual DOM
  • ......

寫在前面

這是該系列文章的第一篇,後續會持續更新,覆蓋 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

Vue JSX 簡介

對於使用 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 中對 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-modelv-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 2 到 Vue 3 的過渡

Vue 3 雖然引入了一部分破壞性的更新,但對於絕大多數 Vue 2 的 API 仍是兼容的。那麼一樣的,咱們也要儘量讓使用 JSX 的用戶經過最小的成本升級到 Vue 3,這是一個核心的目標。寫這篇文章的時候,antdv 已經使用 @ant-design-vue/babel-plugin-jsx 重構了大約 70% 的功能,預計會在 Vue 3 正式版以前發佈測試版,大機率會是東半球最快兼容 Vue 3 的企業級組件庫。

Vue 3 JSX 的 API 設計

  • 函數式組件
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>
  )
})
  • Fragment
const App = () => (
  <>
    <span>I'm</span>
    <span>Fragment</span>
  </>
)

Fragment 參考 React 的寫法,儘量寫起來更加方便

  • Attributes/Props
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

Vue 2 的 JSX 寫法如何快速遷移到 Vue 3

因爲 antdv 的底層基本上都是基於 JSX 來寫的,想要快速遷移到 Vue 3,就必須有一個比較好的插件來支持,這也是爲何會有這個插件的緣由。固然在實現過程當中也踩了不少坑。

目前用法和 Vue 2 的語法大多數是一致的,爲了幫助更快遷移,在插件中作了針對舊 VNode 格式的兼容層,這裏只能兼容一部分寫法,以及部分語法的兼容會增長運行時的性能開銷,因此咱們但願可以將咱們的經驗分享給你們,讓你們少走彎路!

{
  "plugins": ["@ant-design-vue/babel-plugin-jsx", { "transformOn": true, "compatibleProps": true }]
}
  • transformOn

針對 Vue 2 中 on: { click: xx } 寫法的兼容,在運行時中會轉爲 onClick: xxx

  • compatibleProps

上文提到 Vue 3 對屬性的傳遞作了變動,propsattrs 這些都不存在了,所以若是設置了這個屬性爲 true,在運行時也會被解構到第一層的屬性中。

須要注意的一點,目前一旦開啓這兩個屬性,在 createVNode 的第二個參數,都會包一個 compatibleProps transformOn 方法,因此酌情開啓這兩個參數。對於使用 Vue 2 的 JSX 同窗,若是沒有使用到比較」鮮爲人知「 的 API的狀況下,均可以快速得遷移。

那麼 antdv 又是如何作遷移的呢?考慮到 antdv 是個組件庫,都包一層 compatibleProps 勢必不太優雅,所以沒有選擇開啓這個兩個開關。這裏插一句,目前 antdv 的遷移還在進行中,相關的進度都在這個 issue 裏面(Vue 3 支持),有興趣的同窗能夠關注下,提一些 PR 過去。

對於 props 的遷移工做比較簡單,若是你是直接經過標籤的屬性來傳遞,那麼無須作更改。

<Modal visible={visible} />

若是是經過對象來傳遞的屬性,只須要把原有分散在 propsonattrs 中的值直接鋪開便可。

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 中,這個選項不影響 classstyle 綁定,可是在 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

相關文章
相關標籤/搜索