重拳出擊:打造 Vue3.0 + Typescript + TSX 開(乞)發(丐)模式

2019年末,you大的 vue3.0 正式 release 了一個 alpha 版本。全新的 api,更強大的速度和 typescript 的支持,讓人充滿期待;同時,它結合了 hooks 的一系列優勢,使其生態更容易從 React 等別的框架進行遷移。做爲 React 和 Vue 雙重粉絲,鼓掌就完事了!本文受使用Vue 3.0作JSX(TSX)風格的組件開發啓發,因爲原做大神並無給出 demo ,因此只能本身嘗試複製大神的思路,先寫一個極其簡陋的 babel-plugin 來實現 tsx + Vue。javascript

搭建 vue3 + Typescript 項目工程

首先咱們先把vue-next-webpack-preview先 clone 到本地,把它改形成一個 typescript 的工程。css

  • main.js 改成 main.ts,這一步僅須要改一個文件後綴名便可。
  • 新建 tsconfig.json,最基本的配置便可,以下
  • 改造一下 webpack.config.js,主要添加對 typescript 的處理,以下:
{
    test: /\.ts|\.tsx$/,
    exclude: /node_modules/,
    use: [
        'babel-loader',
        {
            loader: 'ts-loader',
            options: {
                appendTsxSuffixTo: [/\.vue$/],
                transpileOnly: true
            }
        }
    ]
}
// 剩餘部分,咱們把 index.html 移動到 public 裏邊,使其像 vuecli4 工程 🐶 
plugins: [
    new VueLoaderPlugin(),
    new MiniCssExtractPlugin({
      filename: '[name].css'
    }),
    new HtmlWebpackPlugin({
      title: 'vue-next-test',
      template: path.join(__dirname, '/public/index.html')
    })
],
devServer: {
    historyApiFallback: true,
    inline: true,
    hot: true,
    stats: 'minimal',
    contentBase: path.join(__dirname, 'public'),
    overlay: true
}
複製代碼
  • Vue 單文件寫一個聲明文件 src/globals.d.ts,以下:
declare module '*.vue' {
    import { Component } from 'vue'
    const component: Component
    export default component
}
複製代碼
  • 安裝相關須要的依賴,順便說一句 typescript@3.7.2 以上,支持 option chain,好用,點贊!
npm i @babel/core @babel/preset-env babel-loader ts-loader -D
npm i typescript -S
複製代碼

通過改進後工程的目錄結構大體以下html

|-- .gitignore
|-- package.json
|-- babel.config.js
|-- tsconfig.json
|-- webpack.config.js
|-- plulic
 |-- index.html
|-- src
 |-- main.ts
 |-- logo.png
 |-- App.vue
 |-- globals.d.ts
複製代碼

這個時候,項目應該仍是能正常啓動的,若是沒法啓動請本身解決>_<vue

編寫 render 函數形式的組件

總所周知,jsx/tsx 是一個語法糖,在 ReactVue 裏會被轉爲 createElement/h,這也是babel-transform-jsx 工做的重點部分。爲了更好地知道jsx 被轉碼後的樣子,咱們先用 Vueh 函數手寫一下他原本的樣子。java

Vue3 中的 h 函數和以前的不太同樣,請務必參考閱讀render-RFCcomposition-api-RFC,主要變更是更扁平化了,對 babel 來講更好處理了。node

先寫一個簡單的 input.vue,以下react

<script lang="tsx">
import { defineComponent, h, computed } from 'vue'

interface InputProps {
  value: string,
  onChange?: (value: string) => void,
}

const Input =  defineComponent({
  setup(props: InputProps, { emit }) {
    const handleChange = (e: KeyboardEvent) => {
      emit('update:value', (e.target as any)!.value)
    }

    const id = computed(() => props.value + 1)

    return () => h('input', {
      class: ['test'],
      style: { 
        display: 'block',
      },
      id: id.value,
      onInput: handleChange,
      value: props.value,
    })
  },
})

export default Input
</script>
複製代碼

顯然直接寫 h 函數式可行、可靠的。可是就是麻煩,因此才須要 jsx,一是便於理解,二是提升開發效率。但既然是乞丐版,咱們的插件就只作兩件事:webpack

  • 自動注入 h 函數
  • jsx 轉換爲 h函數

開發babel 插件前的知識準備

在開始編寫以前,請補習一下 babel 的相關知識,筆者主要參考以下:git

代碼參考以下:github

可參考上述代碼及教程開始你的 babel 之旅。

編寫 babel 插件

開始以前,咱們先觀察一下 AST

分析這個組件:

  • 首先一個代碼塊是一個大的 Program 節點,咱們經過 path 這個對象能拿到節點的全部屬性。對這個簡單組件,咱們先要引入 h 函數。就是把如今的 import { defineComponent } from 'vue' 轉換爲 import { h, defineComponent } from 'vue',因此咱們能夠修改 Program.body 的第一個 ImportDeclaration 節點,達到一個自動注入的效果。
  • 對於 jsx 的部分,節點以下圖:
    咱們處理 JSXElement 節點便可,總體都是比較清晰的,把 JSXElement 節點替換爲 callExpression 節點便可。知道結構了,讓咱們開始吧。

自動注入 h 函數

簡單來看,就是在代碼頂部插入一個節點便可:

import { h } from 'vue'
複製代碼

因此,處理 Program 節點便可,須要判斷是否代碼已經引入了 Vue,同時判斷,是否已經引入了 h函數。代碼參考以下:

// t 就是 babel.types
Program: {
    exit(path, state) {
        // 判斷是否引入了 Vue
        const hasImportedVue = (path) => {
          return path.node.body.filter(p => p.type === 'ImportDeclaration').some(p => p.source.value == 'vue')
        }

        // 注入 h 函數
        if (path.node.start === 0) {
            // 這裏簡單的判斷了起始位置,不是很嚴謹
          if (!hasImportedVue(path)) {
              // 若是沒有 import vue , 直接插入一個 importDeclaration 類型的節點
            path.node.body.unshift(
              t.importDeclaration(
                // 插入 importDeclaration 節點後,插入 ImportSpecifier 節點,命名爲 h
                [t.ImportSpecifier(t.identifier('h'), t.identifier('h'))],
                t.stringLiteral('vue')
              )
            )
          } else {
              // 若是已經 import vue,找到這個節點,判斷它是否引入了 h
            const vueSource = path.node.body
              .filter(p => p.type === 'ImportDeclaration')
              .find(p => p.source.value == 'vue')
            const key = vueSource.specifiers.map(s => s.imported.name)
            if (key.includes('h')) {
                // 若是引入了,就無論了
            } else {
                // 沒有引入就直接插入 ImportSpecifier 節點,引入 h
              vueSource.specifiers.unshift(t.ImportSpecifier(t.identifier('h'), t.identifier('h')))
            }
          }
        }
    }
}
複製代碼

轉換jsx

babel 轉換 jsx 須要對 JSXElement 類型的節點,進行替換;把 JSXElement 替換爲 callExpression 既函數調用表達式,具體代碼以下

JSXElement: {
      exit(path, state) {      
        // 獲取 jsx 
        const openingPath = path.get("openingElement")
        const children = t.react.buildChildren(openingPath.parent)
        // 這裏暫時只處理了普通的 html 節點,組件節點須要 t.identifier 類型節點及其餘節點等,待完善
        const tagNode = t.stringLiteral(openingPath.node.name.name)
  
        // 建立 Vue h
        const createElement = t.identifier('h')
        // 處理屬性
        const attrs = buildAttrsCall(openingPath.node.attributes, t)
        // 建立 h(tag,{...attrs}, [chidren])
        const callExpr = t.callExpression(createElement, [tagNode, attrs, t.arrayExpression(children)])
        path.replaceWith(t.inherits(callExpr, path.node))
      }
    },
複製代碼

自此,基本的代碼已經完成,完整代碼及工程請參考 vue3-tsx

代碼受限於筆者能力,可能存在若干問題,babel 插件也極其簡陋,若有建議或者意見,歡迎與筆者聯繫。現實中我惟惟諾諾,鍵盤上我重拳出擊!

本人首發於我的博客

相關文章
相關標籤/搜索