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
插件也极其简陋,若有建议或者意见,欢迎与笔者联系。现实中我惟惟诺诺,键盘上我重拳出击!
本人首发于我的博客