Vue3各种API——结合typescript

# Vue3各种API——结合typescript

# 1. defineComponent定义组件

defineComponent类型声明:

runtime-core.d.ts

export declare function defineComponent<
    // props 只读的,props对象必须是只读类型才会触发类型声明
    PropsOptions extends Readonly<ComponentPropsOptions>, 
    // 在setup()中返回的内容可以通过this访问,RawBindings就是绑定后的this类型声明
    RawBindings, 
    D, 
    // computed
    C extends ComputedOptions = {}, 
    M extends MethodOptions = {}, 
    Mixin extends ComponentOptionsMixin = ComponentOptionsMixin, 
    Extends extends ComponentOptionsMixin = ComponentOptionsMixin, 
    E extends EmitsOptions = Record<string, any>, 
    EE extends string = string
>(
options: ComponentOptionsWithObjectProps<PropsOptions, RawBindings, D, C, M, Mixin, Extends, E, EE>): DefineComponent<PropsOptions, RawBindings, D, C, M, Mixin, Extends, E, EE>;

声明props,使用PropType定义类型

import { defineComponent, PropType } from 'vue'

interface Profile {
  username: string
  age: number
}
export default defineComponent({
  name: 'HelloWorld',
  props: {
    msg: {
      type: String as PropType<string>,
    },
    profile: {
      type: Object as PropType<Profile>,
      required: true,
    },
  },
})

将props对象抽离出来

import { defineComponent, PropType } from 'vue'

const obj = {
  msg: {
    type: String as PropType<string>,
  },
  age: {
    type: Number,
    required: true,    // 设置required:true,那么age的类型只能为number,不可能为undefined,但
  },
}

export default defineComponent({
  name: 'HelloWorld',
  props: obj,

  mounted() {
    this.age    // 此时类型为number或undefined,required没有生效?
  },
})

原因就是最开始defineComponent中关于PropsOptions是只读的,因此必须将props对象声明为只读类型,required: true才会被视作是只读的,而不是视作一个boolean,但抽离出去之后就不是只读的了,因此需要添加声明:

const obj = {
  msg: {
    type: String as PropType<string>,
  },
  age: {
    type: Number,
    required: true,    // 设置required:true,那么age的类型只能为number,不可能为undefined,但
  },
} as const

加上as const后age就是number了。

# 2. h函数详解

.vue文件的作用其实是用来降低入门门槛的,因为在里面可以写html js css,但事实上vue文件是不能直接运行在浏览器中的,必须经过compiler等的编译,最终还是会编译成js,通过js去渲染视图。而template中写的对于我们来说好像是html,但是对于vue来说它们只是一些字符串,经过编译以后会生成一个h()函数的调用,h函数会去生成对应的virtual DOM最终渲染视图。所以写template中的html其实就是在写h函数。

下面以App.vue为例,用h函数的形式复现App.vue的结构。

首先来看看App.vuetemplate

<div id="app">
    <img alt="Vue logo" src="./assets/logo.png" />
    <HelloWorld msg="Welcome to Your Vue.js + TypeScript App" :age="10" />
  </div>

main.ts

import { createApp } from 'vue'
import App from './App.vue'

createApp(App).mount('#app')

# 编写h函数

import { createApp, h } from 'vue'
// import App from './App.vue'
import HelloWorldVue from './components/HelloWorld.vue'

// vue文件中的路径会通过url-loader、file-loader、webpack等的编译转换,所以在h函数中不能直接写路径,而是要导入。
// 由于使用import 会触发typescript类型校验,所以使用require,但是又会触发eslint,所以用注释的方式禁用下面这行的校验
const img = require('./assets/logo.png') // eslint-disable-line

const App = h('div', { id: 'app' }, [
  h('img', { alt: 'Vue logo', src: img }),
  h(HelloWorldVue, { msg: 'Welcome to Your Vue.js + TypeScript App', age: 10 }),
])

createApp(App).mount('#app')

ok,除了样式以外,现在通过h函数渲染出来的内容和App.vue渲染出来的内容就是一样的了。

可以看到,一个h函数第一个参数可以接收字符串(元素html标签)或一个组件实例如HelloWorldVue,第二个参数和第三个参数可选,第二个参数传入对应的属性,第三个参数传入子节点内容。

# h函数源码

runtime-core/h.ts

// Actual implementation
export function h(type: any, propsOrChildren?: any, children?: any): VNode {
  const l = arguments.length    // 参数长度
  if (l === 2) { 
    if (isObject(propsOrChildren) && !isArray(propsOrChildren)) {    // 第二个参数是对象且不是数组
      // single vnode without props
      if (isVNode(propsOrChildren)) {    // 如果第二个参数是一个VNode,说明第二个参数传入的是子节点,调用createVNode
        return createVNode(type, null, [propsOrChildren])
      }
      // props without children
      return createVNode(type, propsOrChildren)
    } else {
      // omit props
      return createVNode(type, null, propsOrChildren)
    }
  } else {
    if (l > 3) {
      children = Array.prototype.slice.call(arguments, 2)
    } else if (l === 3 && isVNode(children)) {
      children = [children]
    }
    return createVNode(type, propsOrChildren, children)
  }
}

可以看到h函数其实就是对createVNode函数的一层包装,通过h函数我们可以写出更松散的写法,可以传一个参数,两个参数,三个参数,第二个参数可以传属性也可也传子节点内容。

# 3. setup的返回值

setup函数的返回值可以有两种形式:

  • 一个对象

  • 一个函数(该函数返回一个h函数)

返回对象的写法我们都很熟悉,就是把各种变量和方法暴露出来,这样就可以在模板或this上使用。

返回函数的写法:

import { defineComponent, h, ref } from 'vue'
import HelloWorld from './components/HelloWorld.vue'
const img = require('./assets/logo.png') // eslint-disable-line

export default defineComponent({
  name: 'App',
  setup() {
    const num = ref(1)
    setInterval(() => {
      num.value += 1
    }, 1000)

    return () => {
      return h('div', { id: 'app' }, [
        h('img', { alt: 'Vue Logo', src: img }),
        h(HelloWorld, {
          msg: 'Welcome to Your Vue.js + TypeScript App by h',
          age: 10,
        }),
        h('h3', num.value),
      ])
    }
  },
})

此时页面依然正常渲染,且template里的内容被忽略了,而是渲染了返回的h函数的内容,但文件里的样式也保留了。num的值每过1秒加一。

需要注意的是:

setup函数只会执行一次,而返回的函数会在每次数据变化时都调用。来看一个例子:

export default defineComponent({
  name: 'App',
  setup() {
    const num = ref(1)
    setInterval(() => {
      num.value += 1
    }, 1000)

    const numValue = num.value    // 把num.value取出来

    return () => {
      return h('div', { id: 'app' }, [
        h('img', { alt: 'Vue Logo', src: img }),
        h(HelloWorld, {
          msg: 'Welcome to Your Vue.js + TypeScript App by h',
          age: 10,
        }),
        h('h3', numValue),    // 渲染numValue,但是页面上始终为1
      ])
    }
  },
})

结果是页面上始终为1而不会每隔一秒加一,因为setup只执行一次,const numValue = num.value中numValue被赋值一次之后就再也不变了。解决方法是把numValue放到返回的函数里就可以了。

return () => {
      const numValue = num.value      // 放到函数里,此时就每隔一秒加一了。
      return h('div', { id: 'app' }, [
        h('img', { alt: 'Vue Logo', src: img }),
        h(HelloWorld, {
          msg: 'Welcome to Your Vue.js + TypeScript App by h',
          age: 10,
        }),
        h('h3', numValue),
      ])
    }

其实可以发现,返回的这个函数就是一个闭包的应用,它引用了setup里的变量。

# 4. 使用JSX开发组件

实时查看JSX编译成h函数结果[jsx playground](Vue JSX Explorer (opens new window))

首先需要安装JSX的babel插件

yarn add @vue/babel-plugin-jsx -D

在babel配置中:babel.config.js

module.exports = {
  presets: ['@vue/cli-plugin-babel/preset'],
  plugins: ['@vue/babel-plugin-jsx'],
}

写一个简单的JSX,同样是在setup中返回一个函数,但是该函数返回的是jsx片段。

App.tsx

import { defineComponent, h, ref } from 'vue'
import HelloWorld from './HelloWorld.vue'
const img = require('../assets/logo.png') // eslint-disable-line

export default defineComponent({
  name: 'App',
  setup() {
    const num = ref(1)
    return () => {
      return <h1>hello jsx</h1>
    }
  },
})

使用JSX的方式进行开发有许多好处:

  • 首先,在jsx中可以使用模板语法中的指令,jsx对模板的功能是没有降级的,模板的功能都支持使用。

  • 可以获得编辑器的错误提示等信息,比如有一个组件有一个必传的props,当你忘记传递时编译就会错误并且编辑器会有提示。

  • 可以方便地进行函数抽离,逻辑的复用,可以将jsx片段抽离出来,封装成函数返回。