基于Vue Cli4搭建Vue3 TSX移动端项目(四)
至上一篇,基本上这个项目的框架的大致完成了。本篇不再针对框架层面进行改动,主要讲述基于Vue3 Components API 封装一些在移动端中我们经常会使用到的Hooks及Vue3中TSX和单Vue文件写法的一些对比。
封装常用Hooks
在我们公司的业务中,日常移动端开发必不可少的4个Hooks分别是:
- 接口请求
- 微信分享
- 文件上传
- 数据埋点上报
这里微信分享、文件上传、接口请求比较具有通用性,而用户操作记录的埋点上报太过业务,就不赘述了。
useRequest 接口请求
这里我借鉴了umi的useRequest,将其中一些在我这边没有应用场景的功能移除。这个Hooks 的主要实现如下
- 返回响应变量
data,loading方便页面展示 - 返回
run方法,可以让业务页面手动触发请求。同时run是一个Promise,当请求成功时会触发这个Promise,方便一些需要做链式请求的业务。 - 可配置自动/手动触发请求
- 请求防抖
swr数据缓存- 封装后端接口异常提示
这里我新建了src/hooks文件夹作为hooks的存放目录,新建useRequest.ts。具体代码如下,这里我做了详细的注释,代码逻辑也很简单。
import { ref } from '@vue/reactivity';
import { AxiosPromise } from 'axios';
import { Toast } from 'vant';
import { errorParse } from '@/utils/errorParse';
interface IRequestConfig {
// 是否手动请求
manual?:boolean;
// 防抖毫秒数
debounce?:number;
// swr的缓存key
cacheKey?:string;
// 是否关闭Toast报错
closeToast?:boolean;
}
// 缓存
const cache:Record<string, any> = {};
// 防抖
let timer:number | undefined;
/**
* @description 执行防抖
* @author Wynne
* @date 2021-03-16
* @param {IRequestConfig} [config]
*/
function runDebounce(config?:IRequestConfig) {
return new Promise((resolve, reject) => {
clearTimeout(timer);
// 是否配置了防抖
if (config?.debounce && config.debounce > 0) {
timer = setTimeout(() => {
resolve(true);
}, config.debounce);
} else {
resolve(true);
}
});
}
/**
* @description useRequest Hooks
* @author Wynne
* @date 2021-03-16
* @param {() => AxiosPromise<T>} request
* @param {IRequestConfig} [config]
*/
export function useRequest<T>(request:() => AxiosPromise<T>, config?:IRequestConfig) {
// 是否加载中
const loading = ref(false);
// 返回数据
const data = ref<T>();
// 执行请求方法
const run = ():AxiosPromise<T> => new Promise((resolve, reject) => {
// 如果配置了缓存且命中时,先返回缓存再做请求
if (config?.cacheKey && cache) {
data.value = cache[config.cacheKey];
}
loading.value = true;
runDebounce(config).then(() => {
/** 这里有点争议
* request参数是具体的请求方法
* 这里我想到的方法是可以采用两种形式:
* 1. 将请求方法和参数分开传入
* 2. 将请求方法再包一层,形成一个闭包,类似于: (param) => () => AxiosPromise<T>
* 我采用了第二种形式,主要的考虑是请求方法中不止有传参,还会有一些headers之类的
* 如果全部传进来会导致参数过长,我又不太喜欢超过3个参数的方法。
* 至于是否还有更好的方法,目前我还没有想到,也望有大佬能够指点下
*/
request().then((res) => {
loading.value = false;
if (config?.cacheKey) {
cache[config?.cacheKey] = res.data;
}
if (/^2\d/.test(res.status.toString())) {
data.value = res.data;
} else if (!config?.closeToast) {
// 如果非200开头,则展示通用errorCode提示
Toast(errorParse((res.data as any).error_code));
}
resolve(res);
}).catch((err) => {
reject(err);
loading.value = false;
// 如果不希望展示报错弹框,需要业务代码自己做处理时,可以关闭报错弹框
if (!config?.closeToast) {
// 这里errorParse(err.error_code)是根据后端返回错误码去换取文案,就不再展示详细代码了
Toast(errorParse(err.error_code));
}
});
}).catch((e) => console.log(e));
});
// 如果不需要立即执行
if (!config?.manual) {
run();
}
return { loading, data, run };
}
然后我这里大概的展示下如何使用这个Hooks
// src/apis/wechat.ts
import Http from 'axios';
const config = window.CUSTOMCONFIG; // 项目不同环境的配置文件
export const Api = new Http(config.api);
// 获取微信Token的API接口
export const fetchWechatToken = ():AxiosPromise<IWechatToken> => Api.get('/getWechatToken');
// src/views/Test.tsx
const { data, loading, run } = useRequest(fetchAccountLogin({
openId: 'xxxxxxxxxx',
}),
{
manual: true, // 开启手动触发
debounce: 500, // 开启500毫秒防抖
cacheKey: 'WechatToken', // 设置缓存key
closeToast: false, // 开启默认提示
});
// 手动触发请求
run().then((res) => {
console.log('请求成功', res);
}).catch((err) => {
console.log('请求失败', err);
});
useWechatShare 微信分享
微信JSSDK的坑踩过太多回了,所以在封装这个Hooks的时候,我把一些正则参数校验也加入到了里面,防止后续使用的人不注意传了不规范的参数,导致设置失败。
首先我们在public/index.html中添加微信jssdk
<script src="//res.wx.qq.com/open/js/jweixin-1.6.0.js" async></script>
然后我将正则校验的规则统一放在了utils/regex.ts中,将微信JSSDK相关方法放在utils/wechat.ts中。这样能够统一后续项目中的正则,方便以后规则修改。
utils/regex.ts
// 手机号正则
export const mobileReg = /^1[3-9]\d{9}/;
// 整数正则
export const integerReg = /^\d{6}/;
// url正则
export const urlReg = /(https?):\/\/[-A-Za-z0-9+&@#/%?=~_|!:,.;]+[-A-Za-z0-9+&@#/%=~_|]/;
// 是否是微信浏览器
export const isWechat = !!navigator.userAgent.toLocaleLowerCase().match(/micromessenger/i);
// 是否是IOS
export const isIOS = !!navigator.userAgent.match(/ipad|iphone/i);
utils/wechat.ts
interface IWechatConfig {
debug:boolean; // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
appId:string; // 必填,公众号的唯一标识
timestamp:number; // 必填,生成签名的时间戳
nonceStr:string; // 必填,生成签名的随机串
signature:string; // 必填,签名
jsApiList:string[]; // 必填,需要使用的JS接口列表
}
export interface IWechatSharedConfig {
title:string; // 分享标题
desc:string; // 分享描述
link:string; // 分享链接,该链接域名或路径必须与当前页面对应的公众号JS安全域名一致
imgUrl:string; // 分享图标
}
/**
* @description 微信分享给朋友配置
* @author Wynne
* @date 2021-03-16
* @param {IWechatSharedConfig} [config]
*/
const wechatAppMessageShared = (config:IWechatSharedConfig) => new Promise((resolve, reject) => {
window.wx.updateAppMessageShareData({
title: config.title,
desc: config.desc,
link: config.link,
imgUrl: config.imgUrl,
success: function() {
resolve(true);
},
});
});
/**
* @description 微信分享朋友圈配置
* @author Wynne
* @date 2021-03-16
* @param {IWechatSharedConfig} [config]
*/
const wechatTimelineShared = (config:IWechatSharedConfig) => new Promise((resolve, reject) => {
window.wx.updateTimelineShareData({
title: config.title,
desc: config.desc,
link: config.link,
imgUrl: config.imgUrl,
success: function() {
resolve(true);
},
});
});
/**
* @description 微信初始化
* @author Wynne
* @date 2021-03-16
* @param {IWechatConfig} [config]
*/
export const wechatInit = (config:IWechatConfig) => new Promise((resolve, reject) => {
window.wx.config(config);
window.wx.ready(() => {
resolve(true);
});
window.wx.error((res:any) => {
reject(res);
});
});
/**
* @description 微信分享初始化
* @author Wynne
* @date 2021-03-16
* @param {IWechatSharedConfig} [config]
*/
export const wechatSharedInit = async(config:IWechatSharedConfig) => Promise.all([wechatAppMessageShared(config), wechatTimelineShared(config)])
之后继续在hooks文件夹中添加useWechatShare.ts文件
import { fetchWechatJSSDK } from '@/apis/wechat'
import { isWechat, urlReg } from '@/utils/regex'
import { IWechatSharedConfig, wechatInit, wechatSharedInit } from '@/utils/wechat'
const config = window.CUSTOMCONFIG
/**
* @description 微信分享配置Hooks
* @author Wynne
* @date 2021-03-16
* @param {IWechatSharedConfig} [config]
*/
export const useWechatShare = (wechatSharedConfig:IWechatSharedConfig, isDebug = false) => {
// 校验分享链接是否不符合url规则
if (!urlReg.test(wechatSharedConfig.link)) {
throw new Error('link 格式不正确!!')
}
// 校验分享图片是否不符合url规则
if (!urlReg.test(wechatSharedConfig.imgUrl)) {
throw new Error('imgUrl 格式不正确!!')
}
// 如果非微信浏览器则不再做后续操作
if (!isWechat) return
// 这里需要向后端获取签名,具体方法可以根据自己业务编写
fetchWechatJSSDK({
url: location.href.split('#')[0],
js_api_list: ['updateAppMessageShareData', 'updateTimelineShareData'],
appid: config.wechat.appId
}).then((res) => {
const { data } = res
// 进行微信jssdk初始化
wechatInit({
debug: isDebug,
appId: data.appId,
nonceStr: data.nonceStr,
signature: data.signature,
timestamp: data.timestamp,
jsApiList: data.jsApiList
}).then((res) => {
// 设置微信分享文案
wechatSharedInit(wechatSharedConfig)
}).catch((err) => {
console.log(err)
})
}).catch((err) => console.log(err))
}
当我们需要做微信的分享样式设置时,只需要这样使用
useWechat({
title: '开启时光机,惊喜好礼等你赢',
desc: '参与活动可得100%抽奖机会,赢小米笔记本、ipad、扫地机器人…',
link: location.origin,
imgUrl: 'https://source-static.xxxxx.cn/wx_shared.png',
});
useUpload 文件上传
这里import { CdnUpload } from '@/utils/upload';是封装的阿里云OSS的上传SDK,这个Hooks主要是针对上传文件的数量、文件类型、文件大小之类等做了一些便于使用的功能业务。具体的阿里云OSS上传方法就不再赘述了,可以百度查一下或自己使用的云服务商文档看。
import { ref } from '@vue/reactivity';
import { Toast } from 'vant';
import { CdnUpload } from '@/utils/upload';
// 上传状态
export enum UploadStatusEnum {
WAIT, // 未开始
UPLOADING, // 上传中
SUCCESS, // 成功
FAIL, // 失败
}
// 上传配置
export interface IUploadConfig {
// 允许上传的类型,例: image/*
accept?:string;
// 最大大小限制,单位K
maxSize?:number;
// 最大上传数量
maxCount?:number;
// 自定义文件名,如果不传会自动生成一个guid标识
fileName?:string;
// 上传路径,不传默认存放在根目录
path?:string;
}
// 文件信息
export interface IFileInfo {
// 文件原名
fileOriginName:string;
// 文件自定义后的名字
fileName:string;
// 文件大小,单位K
fileSize:number;
// 文件上传后的URL
fileUrl?:string;
// 文件上传进度
progress:number;
// bucket名,只针对海报上传
bucket?:string;
}
/**
* @description 生成GUID
* @author Wynne
* @date 2021-03-24
* @return {*}
*/
function generateGUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
// 上传input实例
let fileInputEle:HTMLInputElement | undefined;
/**
* @description 生成GUID
* @author Wynne
* @date 2021-03-24
* @param {function} fileUrl 成功回调
* @param {IUploadConfig} config 上传配置
*/
export const useUpload = (config?:IUploadConfig) => {
if (config?.path && !/[0-9|a-zA-Z|\-|_]*\/$/.test(config.path)) {
throw new Error('path只支持中英文/数字/中划线/下划线');
}
// 上传进度,百分制
const files = ref<IFileInfo[]>([]);
// 上传状态
const status = ref<UploadStatusEnum>(UploadStatusEnum.WAIT);
// 开始上传
const start = (uploadFiles?:File[]):Promise<IFileInfo[]> => new Promise((resolve, reject) => {
// 进度条信息变更
const handleProgressChange = (fileName:string, pg:any) => {
const item = files.value.find((e) => e.fileName === fileName);
if (item) {
item.progress = pg.total.percent;
}
};
// 验证是否上传完成
const checkUploadComplete = () => {
if (fileInputEle?.files && fileInputEle.files.length === files.value.filter((e) => e.fileUrl).length) {
fileInputEle.value = '';
status.value = UploadStatusEnum.SUCCESS;
resolve(files.value);
}
};
// 文件上传
const fileUpload = (file:File) => {
// 文件重命名
const fileExt = file.name.split('.').pop();
const fileName = `${(config && config.path) || ""}${generateGUID()}.${fileExt}`;
const newFile = new File([file], fileName, {
type: file.type,
});
// 添加到上传文件记录汇总
files.value.push({
fileName: fileName,
fileOriginName: file.name,
fileSize: Math.floor(file.size / 1024),
progress: 0,
});
// 常规上传
CdnUpload(newFile, (progress) => {
handleProgressChange(fileName, progress);
}).then((url) => {
const item = files.value.find((e) => e.fileName === fileName);
if (item) {
item.fileUrl = url;
}
// 校验是否上传完成
checkUploadComplete();
}).catch((error) => {
status.value = UploadStatusEnum.FAIL;
if (error.code === 'ECONNABORTED') {
Toast('网络环境较差,上传失败,请稍后重试一下吧~');
} else {
Toast('上传失败,请重试一下吧~');
}
console.log('上传失败:', error);
reject(error);
});
};
// input变更事件
const handleInputChange = () => {
const uploadFileList = uploadFiles || fileInputEle?.files;
console.log(uploadFileList);
// 判断文件为空
if (!uploadFileList || uploadFileList.length === 0) {
Toast('请先选择上传文件哦');
return;
}
// 判断超出最大上传数
if (config?.maxCount && uploadFileList.length > config?.maxCount) {
Toast(`超出最大上传文件数哦,最多上传${config.maxCount}个文件`);
return;
}
// 判断超出最大文件大小
if (config?.maxSize) {
for (let index = 0; index < uploadFileList.length; index++) {
const file = uploadFileList[index];
if (file.size / 1024 > config.maxSize) {
Toast(`超出上传过大哦,最大上传${config.maxSize}K的文件`);
return;
}
}
}
// 判断是否符合文件类型
if (config?.accept) {
let acceptArr = config.accept.split(',');
acceptArr = acceptArr.map((e) => {
e = e.replaceAll('/', '\/');
e = e.replaceAll('*', '.*');
return `(${e})`;
});
const reg = new RegExp(acceptArr.join('|'));
for (let index = 0; index < uploadFileList.length; index++) {
const file = uploadFileList[index];
if (!reg.test(file.type)) {
Toast(`文件格式错误,仅支持上传${config.accept}文件`);
return;
}
}
}
status.value = UploadStatusEnum.UPLOADING;
for (const file of uploadFileList) {
fileUpload(file);
}
};
// 如果没有input实例,则创建input file实例
if (!fileInputEle) {
fileInputEle = document.createElement('input');
fileInputEle.type = 'file';
fileInputEle.multiple = config?.maxCount !== 1;
if (config?.accept) {
fileInputEle.accept = config.accept;
}
fileInputEle.addEventListener('change', handleInputChange);
}
files.value = [];
// 如果自定义了上传文件,则不触发input上传
if (uploadFiles && uploadFiles.length > 0) {
handleInputChange();
} else {
fileInputEle?.click();
}
});
return {
start,
files,
status,
};
};
下面再附带一个该HOOKS的使用示例:
const { start, files, status } = useUpload({
accept: 'image/*,video/mp4',
maxCount: 2,
maxSize: 50 * 1024,
path: 'test/'
});
start().then((res) => {
console.log(res[0]);
}).catch((err) => { console.log(err); });
TSX vs Vue
Vue3 静态提升
Vue3中针对静态的DOM做了静态提升,但是这个优化目前只能在template写法才能享受到。TSX的写法就无法享受到了。下面是我测试的一个例子:
Vue文件template写法
这里我用了最简单的页面展示来做静态提升测试
<template>
<h1>h1</h1>
<h1>h2</h1>
<h1>{{ msg }}</h1>
</template>
<script>
import { defineComponent, ref } from "@vue/runtime-core";
export default defineComponent({
name: "App",
setup() {
const msg = ref("hello world");
return {
msg,
};
},
});
</script>
然后我们进行打包看下转换后的源码
从上面的代码中我们可以看到上面的两个静态DOM已经做了静态提升优化。接下来我们再尝试下TSX的写法。
TSX文件写法
这里我用TSX的写法再重新写一下上面的DOM结构
import { defineComponent, ref } from "@vue/runtime-core";
export default defineComponent({
name: "App",
setup() {
const msg = ref("hello world");
return ()=>(
<div>
<h1>h1</h1>
<h1>h2</h1>
<h1>{msg.value}</h1>
</div>
);
},
});
然后我们再来看下TSX写法打包后的源码
通过上面两组代码的测试,我们可以看到
Vue3的TSX写法并不享受静态提升。毕竟Vue从一开始就不是为TSX而生,所以官方推荐的写法是用template而不是tsx。所以在日常业务开发中,还是建议使用template进行开发,除此优化之外,支持度也是template更优。所以我个人倾向于主要使用template写法,当开发灵活度要求高的组件时再使用tsx,两种方式混合去开发。
Vue中常用语法在TSX中的写法
v-show 隐藏/展示
template写法
<template>
<h1 v-show="visible">Hello world</h1>
</template>
<script lang="ts">
import { defineComponent, ref } from '@vue/runtime-core'
export default defineComponent({
setup () {
const visible = ref(true)
return { visible }
}
})
</script>
tsx写法
import { defineComponent, ref } from 'vue'
export default defineComponent({
name: 'Home',
setup () {
const visible = ref(false)
return () => (
<div>
<h1 v-show={visible.value}>Hello world1</h1>
<h1 vShow={visible.value}>Hello world2</h1>
</div>
)
}
})
其实这里TSX的两种写法都是能够正常使用的,但是原生H5标签中并没有vShow、vHtml、vModel等声明,所以这样写TS会报错。暂时没有找到更好的办法去解决这个问题。
v-if 是否渲染
template写法
<template>
<h1 v-if="visible">Hello world</h1>
</template>
<script lang="ts">
import { defineComponent, ref } from '@vue/runtime-core'
export default defineComponent({
setup () {
const visible = ref(true)
return { visible }
}
})
</script>
tsx写法
import { defineComponent, ref } from 'vue'
export default defineComponent({
name: 'Home',
setup () {
const visible = ref(false)
return () => (
visible.value
? <div>
<h1>Hello world</h1>
</div>
: null
)
}
})
v-if在TSX中我们直接使用javascript的三元表达式来判断即可
v-for 循环
template写法
<template>
<h1 v-for="i in list" :key="i">
Hello world
</h1>
</template>
<script lang="ts">
import { defineComponent, ref } from '@vue/runtime-core'
export default defineComponent({
setup () {
const list = ref([1, 2, 3, 4, 5])
return { list }
}
})
</script>
tsx写法
import { defineComponent, ref } from 'vue'
export default defineComponent({
name: 'Home',
setup () {
const list = ref([1, 2, 3, 4, 5])
return () => (
<div>
{
list.value.map((e) => <h1>Hello world</h1>)
}
</div>
)
}
})
v-for在TSX中我们直接使用javascript的循环即可
ref 原始DOM引用
template写法
<template>
<h1 ref="h1Ref">Hello world</h1>
</template>
<script lang="ts">
import { defineComponent, onMounted, ref } from '@vue/runtime-core'
export default defineComponent({
setup () {
const h1Ref = ref<HTMLElement>()
onMounted(() => {
console.log(h1Ref.value)
})
return { h1Ref }
}
})
</script>
tsx写法
import { defineComponent, onMounted, ref } from 'vue'
export default defineComponent({
name: 'Home',
setup () {
const h1Ref = ref<HTMLElement>()
onMounted(() => {
console.log(h1Ref.value)
})
return () => (
<div>
<h1 ref={h1Ref}>Hello world</h1>
</div>
)
}
})
slots 插槽
template写法
// 子组件
<template>
<div>
<p><slot /></p>
<h1><slot name="strong" /></h1>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
name: 'HelloWorld'
})
</script>
// 父组件
<template>
<div>
<HelloWorld>
<span>default</span>
<template #strong>Strong</template>
</HelloWorld>
</div>
</template>
<script lang="ts">
import { defineComponent } from '@vue/runtime-core'
import { HelloWorld } from '@/components/HelloWorld'
export default defineComponent({
components: { HelloWorld },
setup () {
return {}
}
})
</script>
tsx写法
// 子组件
import { defineComponent, renderSlot } from 'vue'
export const HelloWorld = defineComponent({
name: 'HelloWorld',
setup (props, ctx) {
console.log(ctx.slots)
return () =>
<div>
<p>{renderSlot(ctx.slots, 'default')}</p>
<h1>{renderSlot(ctx.slots, 'strong')}</h1>
</div>
}
})
// 父组件
import { defineComponent } from 'vue'
import { HelloWorld } from '@/components/HelloWorld'
export default defineComponent({
name: 'Home',
setup () {
return () => (
<HelloWorld vSlots={{
default: () => [<span>default</span>],
strong: () => [<span>Strong</span>]
}}>
</HelloWorld>
)
}
})
v-model 双向绑定
template写法
<template>
<div>
<input v-model="form" type="text"/>
</div>
</template>
<script lang="ts">
import { defineComponent, ref, watch } from '@vue/runtime-core'
export default defineComponent({
setup () {
const form = ref('')
watch(
() => form.value,
() => console.log(form.value)
)
return {
form
}
}
})
</script>
tsx写法
import { defineComponent, ref, watch } from 'vue'
export default defineComponent({
name: 'Home',
setup () {
const form = ref('')
watch(
() => form.value,
() => console.log(form.value)
)
return () => (
<div>
<input v-model={form.value} type="text" />
<input vModel={form.value} type="text" />
</div>
)
}
})
这里其实跟
vShow有一样的问题,就是在原生DOM上使用vModelts 会报错
.sync 修饰符
子组件
import { defineComponent } from 'vue'
export const HelloWorld = defineComponent({
name: 'HelloWorld',
props: ['visible'],
setup (props, ctx) {
const updateStatus = () => {
ctx.emit('update:visible', !props.visible)
}
return () =>
<button onClick={updateStatus}>点击变更</button>
}
})
template写法
<template>
<HelloWorld v-model:visible="visible" />
</template>
<script lang="ts">
import { defineComponent, ref, watch } from '@vue/runtime-core'
import { HelloWorld } from '@/components/HelloWorld'
export default defineComponent({
components: { HelloWorld },
setup () {
const visible = ref(false)
watch(
() => visible.value,
() => console.log(visible.value)
)
return {
visible
}
}
})
</script>
tsx写法
import { defineComponent, ref, watch } from 'vue'
import { HelloWorld } from '@/components/HelloWorld'
export default defineComponent({
name: 'Home',
setup () {
const visible = ref(false)
watch(
() => visible.value,
() => console.log(visible.value)
)
return () => (
<div>
<HelloWorld vModel={[visible.value, 'visible']} />
</div>
)
}
})
可以感觉到
Vue3现在在TSX方面的支持比起React还是差得比较多的。所以还是建议日常业务开发时使用template写法,当写灵活度高的组件时再使用tsx的写法。这样也能让你的项目更大范围的去享受静态提升
TSX样式模块化
我们知道在vue文件中可以使用
scoped来实现样式模块化,那么在tsx中vue cli4也帮我们内置了css modules,只需要我们在引入样式时,将样式文件名改为类似于*.module.scss即可,下面附带一个demo
首先我们需要在shims-vue.d.ts文件中添加一下声明,避免引入scss报错
//解决scss文件报错问题
declare module '*.scss'{
const sass:any
export default sass
}
在tsx文件中引入scss并使用style来赋值class
import { defineComponent, ref, watch } from 'vue'
import style from './home.module.scss'
export default defineComponent({
name: 'Home',
setup () {
return () => (
<div class={style.blue}>
<div class={style.red}></div>
</div>
)
}
})
这样就可以实现tsx中的css模块化了
自定义组件onClick、vModel、vSlots等报错
在引入第三方组件库或者我们自己编写的组件时,会发现当我们监听onClick事件时ts会提示属性未定义,
这里应该也算是Vue3在tsx支持中的不完善之处,大概翻阅了下资料,发现在ElementUI-Plus中有提到一个解决方案,就是我们自己在typings文件夹下新建ele.d.ts声明文件扩展@vue/runtime-core这个module,具体代码如下
import { VNodeChild } from '@vue/runtime-core';
import { HTMLAttributes } from '@vue/runtime-dom';
export type JsxNode = VNodeChild | JSX.Element;
export interface SlotDirective {
[name: string]: () => JsxNode;
}
type JsxComponentCustomProps = {
vModel?: unknown;
vModels?: unknown[];
vCustom?: unknown[];
vShow?: boolean;
vHtml?: JsxNode;
vSlots?: SlotDirective;
} & Omit<HTMLAttributes, 'innerHTML'> & {
innerHTML?: JsxNode;
};
declare module '@vue/runtime-core' {
interface ComponentCustomProps extends JsxComponentCustomProps {
onClick?: () => any;
vSlots?: {
[eleName: string]: JSX.Element;
};
[eleName: string]: any;
}
}