2019年末,you大的 vue3.0 正式 release 了一個 alpha 版本。全新的 api,更強大的速度和 typescript 的支持,讓人充滿期待;同時,它結合了 hooks 的一系列優勢,使其生態更容易從 React 等別的框架進行遷移。做爲 React 和 Vue 雙重粉絲,鼓掌就完事了!本文受使用Vue 3.0作JSX(TSX)風格的組件開發啓發,因爲原做大神並無給出 demo ,因此只能本身嘗試複製大神的思路,先寫一個極其簡陋的 babel-plugin 來實現 tsx + Vue。javascript
首先咱們先把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
是一個語法糖,在 React
和 Vue
裏會被轉爲 createElement/h
,這也是babel-transform-jsx
工做的重點部分。爲了更好地知道jsx
被轉碼後的樣子,咱們先用 Vue
的 h
函數手寫一下他原本的樣子。java
Vue3
中的 h
函數和以前的不太同樣,請務必參考閱讀render-RFC和composition-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
插件也極其簡陋,若有建議或者意見,歡迎與筆者聯繫。現實中我惟惟諾諾,鍵盤上我重拳出擊!
本人首發於我的博客