2019年底,you大的 vue3.0 正式 release 了一个 alpha 版本。全新的 api,更强大的速度和 typescript 的支持,让人充满期待;同时,它结合了 hooks 的一系列优点,使其生态更容易从 React 等别的框架进行迁移。作为 React 和 Vue 双重粉丝,鼓掌就完事了!本文受使用Vue 3.0做JSX(TSX)风格的组件开发启发,由于原作大神并没有给出 demo ,所以只能自己尝试复制大神的思路,先写一个极其简陋的 babel-plugin 来实现 tsx + Vue。
搭建 vue3 + Typescript 项目工程
首先我们先把vue-next-webpack-preview先 clone 到本地,把它改造成一个 typescript 的工程。
- 把
main.js 改为main.ts ,这一步仅需要改一个文件后缀名即可。 - 新建
tsconfig.json ,最基本的配置即可,如下 - 改造一下
webpack.config.js ,主要添加对typescript 的处理,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | { 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 ,如下:
1 2 3 4 5 | declare module '*.vue' { import { Component } from 'vue' const component: Component export default component } |
- 安装相关需要的依赖,顺便说一句
[email protected] 以上,支持option chain ,好用,点赞!
1 2 | npm i @babel/core @babel/preset-env babel-loader ts-loader -D npm i typescript -S |
经过改进后工程的目录结构大致如下
1 2 3 4 5 6 7 8 9 10 11 12 | |-- .gitignore |-- package.json |-- babel.config.js |-- tsconfig.json |-- webpack.config.js |-- plulic |-- index.html |-- src |-- main.ts |-- logo.png |-- App.vue |-- globals.d.ts |
这个时候,项目应该还是能正常启动的,如果无法启动请自己解决>_<
编写 render 函数形式的组件
总所周知,
先写一个简单的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | <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 转换为h 函数
开发babel 插件前的知识准备
在开始编写之前,请补习一下
- Babel 插件手册
- 从零开始编写一个babel插件
代码参考如下:
- plugin-transform-react-jsx
- babel-plugin-transform-vue-jsx
- 在线
AST parser
可参考上述代码及教程开始你的
编写 babel 插件
开始之前,我们先观察一下
分析这个组件:
- 首先一个代码块是一个大的
Program 节点,我们通过path 这个对象能拿到节点的所有属性。对这个简单组件,我们先要引入h 函数。就是把现在的import { defineComponent } from 'vue' 转换为import { h, defineComponent } from 'vue' ,所以我们可以修改Program.body 的第一个ImportDeclaration 节点,达到一个自动注入的效果。 - 对于
jsx 的部分,节点如下图:
我们处理JSXElement 节点即可,整体都是比较清晰的,把JSXElement 节点替换为callExpression 节点即可。知道结构了,让我们开始吧。
自动注入 h 函数
简单来看,就是在代码顶部插入一个节点即可:
1 | import { h } from 'vue' |
所以,处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | // 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | 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 插件也极其简陋,如有建议或者意见,欢迎与笔者联系。现实中我唯唯诺诺,键盘上我重拳出击!
本人首发于个人博客