Vue3:JSX/TSX的应用

发布于 2022-11-03  1379 次阅读


tsx支持

首先需要安装官方维护的vite插件@vitejs/plugin-vue-jsx,这个插件其实核心还是@vue/babel-plugin-jsx,只是在这个插件上封装了一层供vite插件调用。所以关于vue的jsx语法规范可以直接参看@vue/babel-plugin-jsx,文档链接如下,建议大家可以先读一遍语法规范。官方写得比较详细,后续我也会结合实际讲解一下大部分规范的用法,vue jsx语法规范

$ npm install @vitejs/plugin-vue-jsx -D
# or
$ yarn add @vitejs/plugin-vue-jsx -D

安装完之后在vite.config.ts进行插件使用,代码如下:

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import vueJsx from "@vitejs/plugin-vue-jsx";

export default defineConfig({
  plugins: [
    vue(),
    vueJsx() //插件使用
  ],
});

后面就可以把目录中的app.vueHelloWorld.vue以及shims.vue.d.ts这三个文件删除了,因为后面我们就只需要写tsx文件了。

然后src目录下新增App.tsx文件,写入如下代码:

import { defineComponent } from 'vue'

export default defineComponent({
    setup() {
        return () => <div>hello world</div> //写一个 hello world祭天
    }
})

小tip

每次书写vue3模式的tsx模板也比较麻烦,这里建议大家如果使用vscode可以添加一个自定义代码片段,这是本人日常使用的模板:

{
	"Print to console": {
      "prefix": "vuetsx",
      "body": [
			"import { defineComponent } from 'vue'\n",
      "export default defineComponent({",
      "    props: {},",
			"    emits: [],",
			"    components: {},",
			"    setup(props, ctx) {",
			"        return () => <div></div>",
			"    }",
      "})",
      ],
      "description": "Create vue template"
    }
}

jsx/tsx语法规范

如果有过react的开发经验,可以发现除了vue中独有的几个新概念:slotdirectiveemit等以外,大部分支持vue的jsx语法规范和react的都是一样的,相同的部分我就不多说了,大家不了解的可以翻下文档很快就能理解,不同的接下来我就一个个的结合代码进行举例示范:

Fragment

在vue3的模版语法中是支持解析多根节点的语法结构的,比如这样:

<template>
  <div></div>
  <div></div>
  <div></div>
</template>
复制代码

但是使用jsx的方式是不支持这种写法的,还是必须只有一个根结点,这个时候我们可以和react一样通过添加一个虚拟节点来完成同样的需求:

const App = () => (
  <>
    <span>I'm</span>
    <span>Fragment</span>
  </>
);
复制代码
指令

@vue/babel-plugin-jsx帮我们解析了几个常见的vue指令,比如v-showv-model,这两个的用法和功能与vue中一摸一样,就不多赘述了,接下来说几个常见但是需要自己实现的指令功能:

  • v-bind
import { defineComponent, ref } from "vue";
const App = defineComponent({
  setup(){
    const size = ref<"large" | "medium" | "small" | "mini">("mini")
    return () => 
      <Button size={size.value}></Button> //此处直接换成jsx的模版语法 效果和v-bind是一致的
  }
});
复制代码
  • v-if

使用条件判断语句来实现v-if的功能,与react中一致。

const App = () => (
  <>
   {
     condition ?  <span>A</span> : <span>B</span>
   }
  </>
);
复制代码
  • v-for

和react中一样,采用map循环的方式

import { defineComponent, ref } from "vue";
const App = defineComponent({
  setup(){
    const list = ref<string[]>([])
    return () => {
      list.value.map((data,index) => <p key={index}>{data}</p>)
    }
  }
});
复制代码
  • 自定义指令

首先创建自定义指令

import { ObjectDirective } from "vue";

const foucsDirective: ObjectDirective<HTMLElement, any> = {
  mounted(el) {
    switch (el.tagName) {
      case "INPUT":
        el.focus();
        break;
      default:
        const input = el.querySelector("input");
        input?.focus();
        break;
    }
  },
};

export default foucsDirective;
复制代码

全局引入

import App from "./App";
import store from "./store";
import router from "./router";
import { createApp } from "vue";
import foucsDirective from "@/directive/focus";
import "element-plus/lib/theme-chalk/index.css";

const app = createApp(App);

// 全局挂载指令
app.directive("focus",foucsDirective);

app.use(router).use(store).mount("#app");
复制代码

局部引入

import { defineComponent, ref } from "vue";
import foucsDirective from "@/directive/focus";

const App = defineComponent({
  directives: { focus: foucsDirective },
  setup(){
    const value = ref<string>("")
    return () => <input type="text" v-focus v-model={value.value}/>
  }
});
复制代码
插槽

不像 react,component 自带一个 children 的 props,vue 的自定义组件嵌套全得靠 slot,所以在jsx中想要实现vue中的插槽写法也有很大不同。

import { defineComponent } from "vue";

// 子组件
const Child = defineComponent({
  setup(props, { slots }) {
    return () => (
      <>
        默认插槽: {slots.default && slots.default()}
        <br />
        具名插槽: {slots.prefix && slots.prefix()}
        <br />
        作用域插槽:{slots.suffix && slots.suffix({ name: "这是作用域插槽的示范" })}
      </>
    );
  },
});

// 父组件
const Father = defineComponent({
  setup() {
    return () => (
      <Child
        v-slots={{
          prefix: <i class="el-icon-star-on"></i>, // 具名插槽
          suffix: (props: Record<"name", string>) => <span>{props.name}</span>, // props可作插槽作用域的作用
        }}
      >
        这是默认插槽的示范
      </Child>
    );
  },
});

export default Father
复制代码

由上述的简单例子很容易就能总结出vue中默认插槽、具名插槽以及作用域插槽的用法,它渲染的结果如下:

这里有一个坑,v-slots中直接传入defineComponent包裹的组件将不会执行渲染

const Test1 = defineComponent({
  setup() {
    return <i class="el-icon-star-on"></i>;
  },
}); // 错误 此组件作为slot传入子组件不会被成功渲染

const Test2 = () => <i class="el-icon-star-on"></i> // 正确 此组件作为slot传入子组件会被成功渲染
复制代码
emit

vue中子向父传值一般都是emit的方式,这个在vue3中大致写法相似,只是多了一个定义emit的步骤,这也是为了后续的类型推倒做准备。

import { defineComponent } from "vue";

// 子组件
const Child = defineComponent({
  emits: ["click"],
  setup(props ,{ emit }) {
    return () => (
      <button onClick={() => {emit("click")}}>点我触发emit</button>
    );
  },
});

// 父组件
const Father = defineComponent({
  setup() {
    return () => (
      <Child onClick={() => {
          console.log("emit 触发了")
      }}/>
    );
  },
});
复制代码

这种方式本没有问题,但是在tsx中由于子组件props中没有相关emit事件的类型声明,就会报错

但是实际功能是能够触发的,这里只是类型检测出现了异常。有时候遇到了没能兼容tsx写法形式(比如element-plus = =)的库,又不想有红色报错,这个时候其实可以这么处理:

import { defineComponent } from "vue";

// 子组件
const Child = defineComponent({
  emits: ["click"],
  setup(props ,{ emit }) {
    return () => (
      <button onClick={() => {emit("click")}}>点我触发emit</button>
    );
  },
});

// 父组件
const Father = defineComponent({
  setup() {
    return () => (
      <Child {...{  // 避免出现因为类型检测导致的报错 此方法可适用于任何不存在props类型声明的参数
        onClick:() => {
          console.log("emit 触发了")
        }
      }}/>
    );
  },
});
复制代码

但是这个其实也是一个曲线救国的方案,所以如果大家有开发库的打算或者平时打算用vue3结合tsx写项目,最好还是使用下面的方式,做个兼容处理:

import { defineComponent, PropType } from "vue";

// 子组件
const Child = defineComponent({
  emits: ["click"], // 传统template写法
  props: {
    onClick: Function as PropType<(event:MouseEvent) => void> // 兼容tsx写法,让事件有类型声明
  },
  setup(props ,{ emit }) {
    return () => (
      <button onClick={(event:MouseEvent) => {emit("click",event)}}>点我触发emit</button>
    );
  },
});

// 父组件
const Father = defineComponent({
  setup() {
    return () => (
      <Child onClick={(event:MouseEvent) => { // 此处便不会出现类型报错 并且有好的类型提示
          console.log("emit 触发了")
      }}/>
    );
  },
});
复制代码
tsx Render方式

tsx目前还支持render方式的写法,这种写法目前也是大多数开源UI库的写法,个人比较推荐这种写法,它将逻辑层和模板层分开后期更易维护

import { ref, renderSlot, onUnmounted, defineComponent } from "vue";

// 带render函数的组件 优点:可将逻辑区与模版区分开
export const RenderComponent = defineComponent({
  props: {
    title: String,
  },
  // 逻辑层
  setup() {
    const count = ref<number>(1);

    const timer = setInterval(() => {
      count.value++;
    }, 2000);

    onUnmounted(() => {
      clearInterval(timer);
    });

    return {
      count,
    };
  },
  // 渲染层
  render() {
    // render函数在响应式数据发生更改时会自动触发(与react类似)
    const { count, $slots, title } = this;
    return (
      <div class="render-component">
        {renderSlot($slots, "prefix")} {count}
        <br />
        这是props:{title}
        <br />
        {renderSlot($slots, "default")}
      </div>
    );
  },
});
复制代码

router和vuex项目中使用

router项目中的使用
import { defineComponent } from "vue";
import { useRouter, useRoute, RouterView } from "vue-router";

const App = defineComponent({
  setup(){
    const router = useRouter();
    const route = useRoute();
    
    function go(pathName:string){
      // 跳转路由
      router.push({
        name: pathName,
        query: {
          value: "路由传参"
        }
      })
      
      // 取路由传递的参数 params的同理
      const { query } = route;
      console.log(query)
    }
    
    return () => <>
      <button onClick={() => {go('home')}}>跳转home</button>
      <button onClick={() => {go('login')}}>跳转login</button>
      <RouterView />
    </>
  }
});
复制代码
vuex项目中的使用
import { useStore } from "vuex";
import { SET_USER } from "@/store/login/actionType";
import { defineComponent, computed, readonly } from "vue";

const App = defineComponent({
  setup(){
    // 暴露state以及dispatch
    const { state, dispatch } = useStore();
    // 此处最好用readonly包裹暴露出的state,让其成为只读属性 避免直接修改
    const loginState = computed(() => readonly(store.state.login));
    
    function modifyUserInfo(){
      // 直接调用dispatch 用法和vue2中一致
      dispatch(`login/${SET_USER}`,{})
    }
    
    return () => <>
      <button onClick={modifyUserInfo}>修改state</button>
      <div>{loginState.user} {loginState.password}</div>
    </>
  }
});

欢迎欢迎~热烈欢迎~