如何使用vue2+vite+tsx+composition-api开发组件

832 阅读3分钟

前言

随着vue3与vue2.7的发布,composition-api已经得到越来越多的关注,其重要性不言而喻。composition-api带来了首要好处就是更好的逻辑复用,同时相较于optioins-apicomposition-api对于ts的支持度也得到了很大的提升。尽管如此,vue在很多地方对ts的支持还是不像react那样丝滑。本文就来探讨下如何使用vue2+tsx开发组件,来看看如何在vue中使用ts以及template语法对应的jsx应该怎么写。

依赖

首先来看看需要哪些关键的依赖,如下:

"devDependencies": {
    "@babel/plugin-transform-typescript": "^7.18.4",
    "@vue/babel-helper-vue-jsx-merge-props": "^1.2.1",
    "@vue/babel-preset-jsx": "^1.2.4",
    "@vue/composition-api": "^1.6.3",
    "typescript": "^4.7.4",
    "vite": "^2.9.12",
    "vite-plugin-vue2": "^2.0.1",
    "vue": "2.6.14",
    "vue-template-compiler": "^2.6.14"
 }

配置

接下来是vite及babel的配置

// vite.config.ts
import { defineConfig } from 'vite'
import { createVuePlugin } from "vite-plugin-vue2";
import path from 'path'

const cwd = process.cwd()
const entryDir = path.resolve(cwd, './src')
const outDir = path.resolve(cwd, './lib')
const rollupOptions = {
  external: [
    'vue',
    '@vue/composition-api'
  ]
}

export default defineConfig({
  build: {
    rollupOptions,
    lib: {
      entry: path.resolve(entryDir, 'index.tsx'),
      fileName: (format) => `index.${format}.js`,
      formats: ['es']
    },
    outDir
  },
  plugins: [
    createVuePlugin({
      jsx: true
    })
  ],
  css: {
    preprocessorOptions: {
      less: {
        javascriptEnabled: true
      }
    }
  }
})
// babel.config.js
module.exports = {
  presets: [
    '@vue/cli-plugin-babel/preset',
    ['@vue/babel-preset-jsx', { compositionAPI: true }]
  ],
  plugins: [
    ['@babel/plugin-transform-typescript', { isTSX: true }]
  ]
}

如何在vue中使用ts

1. Props类型推断

我们都知道,在vue中声明props的类型都是用Constructor的(如String/Number), 而要使得ts能够根据给定的构造器推导出props的类型就需要用到defineComponent():

import { defineComponent } from '@vue/composition-api'

defineComponent({
  props: {
    name: String,
    msg: { type: String, required: true }
  },
  mounted() {
    this.name // (property) name?: string | undefined
    this.msg  // (property) msg: string
  },
  setup(props) {
    props.msg // (property) name?: string | undefined
  },
  render() {
    return (
      <div></div>
    )
  }
})

那么对于比较复杂的对象或者对象数组类型该如何处理呢?请看下面代码:

import { defineComponent } from '@vue/composition-api'
import type { PropType } from '@vue/composition-api'

interface Person {
  name: string;
  age: number;
}

defineComponent({
  props: {
    persons: Array as PropType<Person[]>
  },
  mounted() {
    this.persons // (property) persons?: Person[] | undefined
  },
  setup(props) {
    props.persons // (property) persons?: Person[] | undefined
  },
  render() {
    return (
      <div></div>
    )
  }
})

但遗憾的是这个类型推断并不能在render函数中起作用。说到这里可能会有人会说,直接在setup函数中返回函数不就行了,为什么要写render函数呢?这就涉及到vue2 jsx的babel插件问题了,请看下面代码:

import { defineComponent, h } from '@vue/composition-api'

defineComponent({
  name: 'Test',
  setup() {
    return () => <div></div>    
  }
})

这代码似乎没什么毛病,但是看编译后的代码就知道问题出在哪里:

import { defineComponent } from "@vue/composition-api";
defineComponent({
  name: "Test",
  setup() {
    const h = this.$createElement;
    return () => h("div");
  }
});

可以看到,编译后的h函数并不会取@vue/composition-api中的,而是取了this.$createElement,我们都知道,setup作用域下this指向的是undefined,所以这个表达式并不成立,因此实际运行时代码会报错,那么我们只能退而求其次——使用render函数了。 虽然render函数没法拿到props的类型推断,但是可以通过手动指定this类型来解决。

import { defineComponent } from "@vue/composition-api";
import Vue from 'vue'

defineComponent({
  name: "Test",
  props: {
    name: String
  },
  render(this: Vue & { name: string }) {
      // ...
  }
});

2. 自定义事件

在vue的开发中肯定是少不了使用自定义事件,我们来看下使用ts的情况下,使用自定义事件需要注意什么:

import { defineComponent } from '@vue/composition-api'

const Child = defineComponent({
  name: 'Child',
  setup(props, { emit }) {
    const handleClick = () => {
      emit('customEvent')
    }
    
    return {
      handleClick
    }
  },
  render() {
    return (
      <div onClick={this.handleClick}>child</div>
    )
  }
})

defineComponent({
  name: 'Parent',
  setup() {},
  render() {
    return (
      // Type '{ onCustomEvent: () => void; }' is not assignable to type 'Readonly<Partial<{}> & Omit<{} & {}, never>>'.
      <Child onCustomEvent={() => {}}>parent</Child>
    )
  }
})

这代码看起来似乎没什么问题,但是ts却会抛出异常令人不知所措,其实解决方法也很简单,只需要在Child组件的ComponentOptions中增加emits选项就行了,如下:

const Child = defineComponent({
  name: 'Child',
  emits: ['customEvent'],
  setup(props, { emit }) {
    const handleClick = () => {
      emit('customEvent')
    }
    
    return {
      handleClick
    }
  },
  render() {
    return (
      <div onClick={this.handleClick}>child</div>
    )
  }
})

如何在vue中使用jsx

相信大部分仅使用vue的小伙伴对jsx相对是比较陌生的,那么我们就来讨论下在vue中使用jsx需要注意哪些。

1. jsx的优势

我们先来说说jsx的优势是什么

  • 灵活。我们都知道使用templete时其作用域是组件本身。它的优点就在于不需要通过this.xxx的形式访问组件上声明的变量,但同时它也失去了灵活性,比如一些需要在templete中使用一些常量也必须注册到data()中,比如没法直接在templete中调用console.log等等。而jsx则没有这些问题,任意的变量、数据都能在其中使用,也不必全都注册都组件实例上。
  • 支持度高。jsx得到了babel、ts的原生支持,不必在为它写额外的plugin去支持。也许不就的将来jsx会成为类似于html一样的标准。

2. 内置指令

  • v-model: 使用手动绑定valuechange事件代替。当然,也是用插件可以实现在jsx中使用modalValue来代替手动绑定,但我个人觉得没必要。
  • v-show:添加动态class来切换display的值
  • v-if: condition && <div></div> condition ? <div></div> : <span></span>
  • v-for: list.map(item => <div key={item}>{item}</div>)
  • v-bind: <div type={someValue}></div>
  • v-html: <div domPropsInnerHTML='<span></span>'></div>
  • v-text: <div domPropsInnerText='123456'></div>
  • v-on: <div onClick={handleClick}></div>
  • v-slot: <div scopedSlots={{ default: () => {}, other: () => {} }}></div>

3. 自定义指令

自定义指令也是vue的一大特色,他可以封装一些原生dom操作,给日常开发带来了极大的便利。下面我们来看下如何在jsx中使用自定义指令

defineComponent({
  name: 'Child',
  render() {
    // 相当于 v-custom:foo.bar="() => 1 + 1"
    const directives = [
      {
          name: 'custom',
          value: () => 1 + 1,
          arg: 'foo',
          modifiers: { bar: true }
      }
    ]
  
    return (
      <div {...{directives}}>child</div>
    )
  }
})

结语

以上就是本文的所有内容了。本文主要介绍了vue中使用ts及使用jsx的一些注意点,目前我个人遇到的就是这些了。本人三年多的react,刚转vue,有哪些说的不好的还望多多指教。