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.vue
、HelloWorld.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中独有的几个新概念:slot
、directive
、emit
等以外,大部分支持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-show
、v-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>
</>
}
});
Comments | NOTHING