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

本人首发于我的博客

相关文章
相关标签/搜索