如果需要在vue的原型上挂载东西,就不能使用以前的原型挂载方法,需要使用新方法config.globalProperties
,详细用法请查阅官方文档。
我的项目中用到了一个websocket的插件,他需要在vuex中往Vue原型上挂载方法,下面是我的做法。
- 将
main.ts
中的createApp
方法导出。
import { createApp } from "vue"; const app = createApp(App); export default app;
- 在
store/index.ts
中导入main.ts
,然后调用方法挂载即可。
mutations: { // 连接打开 SOCKET_ONOPEN(state, event) { main.config.globalProperties.$socket = event.currentTarget; state.socket.isConnected = true; // 连接成功时启动定时发送心跳消息,避免被服务器断开连接 state.socket.heartBeatTimer = setInterval(() => { const message = "心跳消息"; state.socket.isConnected && main.config.globalProperties.$socket.sendObj({ code: 200, msg: message }); }, state.socket.heartBeatInterval); } }
适配axios
axios在封装成插件时与之前的差别对比如下:
- 暴露
install
方法由原来的Plugin.install
改为了install
- 增加了ts的类型声明
Object.defineProperties
舍弃了,现在直接使用app.config.globalProperties
挂载即可
适配完成的代码如下:
import { App } from "vue"; import axiosObj, { AxiosInstance, AxiosRequestConfig } from "axios"; import store from "../store/index"; const defaultConfig = { // baseURL在此处省略配置,考虑到项目可能由多人协作完成开发,域名也各不相同,此处通过对api的抽离,域名单独配置在base.js中 // 请求超时时间 timeout: 60 * 1000, // 跨域请求时是否需要凭证 // withCredentials: true, // Check cross-site Access-Control heards: { get: { "Content-Type": "application/x-www-form-urlencoded;charset=utf-8" // 将普适性的请求头作为基础配置。当需要特殊请求头时,将特殊请求头作为参数传入,覆盖基础配置 }, post: { "Content-Type": "application/json;charset=utf-8" // 将普适性的请求头作为基础配置。当需要特殊请求头时,将特殊请求头作为参数传入,覆盖基础配置 } } }; /** * 请求失败后的错误统一处理,当然还有更多状态码判断,根据自己业务需求去扩展即可 * @param status 请求失败的状态码 * @param msg 错误信息 */ const errorHandle = (status: number, msg: string) => { // 状态码判断 switch (status) { // 401: 未登录状态,跳转登录页 case 401: // 跳转登录页 break; // 403 token过期 case 403: // 如果不需要自动刷新token,可以在这里移除本地存储中的token,跳转登录页 break; // 404请求不存在 case 404: // 提示资源不存在 break; default: console.log(msg); } }; export default { // 暴露安装方法 install(app: App, config: AxiosRequestConfig = defaultConfig) { let _axios: AxiosInstance; // 创建实例 _axios = axiosObj.create(config); // 请求拦截器 _axios.interceptors.request.use( function(config) { // 从vuex里获取token const token = store.state.token; // 如果token存在就在请求头里添加 token && (config.headers.token = token); return config; }, function(error) { // Do something with request error error.data = {}; error.data.msg = "服务器异常"; return Promise.reject(error); } ); // 响应拦截器 _axios.interceptors.response.use( function(response) { // 清除本地存储中的token,如果需要刷新token,在这里通过旧的token跟服务器换新token,将新的token设置的vuex中 if (response.data.code === 401) { localStorage.removeItem("token"); // 页面刷新 parent.location.reload(); } // 只返回response中的data数据 return response.data; }, function(error) { if (error) { // 请求已发出,但不在2xx范围内 errorHandle(error.status, error.data.msg); return Promise.reject(error); } else { // 断网 return Promise.reject(error); } } ); // 将axios挂载到vue的全局属性中 app.config.globalProperties.$axios = _axios; } };
然后将其在main.js中use
,就可以在代码中通过this.$axios.xx
来使用了。
不过上述将axios挂载到vue上是多此一举的,因为我已经将api进行了抽离,在每个单独的api文件中都是通过导入我们封装好的axios的配置文件,然后用导入进来的axios实例来进行的接口封装。(ps: 之前由于自己太菜没注意到这个,傻傻的将其封装成了插件😂)
那么,不需要将其封装成插件的话,那它就属于对axios进行配置封装了,我们将它放在config目录下,将上述代码稍作修改即可,修改好的代码地址:config/axios.ts。
最后在main.ts
中将api挂载到全局属性。
import { createApp } from "vue"; import api from "./api/index"; const app = createApp(App); app.config.globalProperties.$api = api;
随后就就可以在业务代码中通过this.$api.xx
按模块来调用我们抛出来的接口了。
shims-vue.d.ts类型声明文件
shims-vue.d.ts是一个Typescript
的声明文件,当项目启用ts后,有些文件是我们自己封装的,类型较为复杂,ts不能推导出其具体类型,此时就需要我们进行手动声明。
例如上面我们挂载到原型上的$api
,它导出了一个类文件,此时类型就较为复杂了,ts
没法推导出其类型,我们在使用时就会报错。
image-20201010100416381
要解决这个错误,我们就需要在shims-vue.d.ts
中声明api
的的类型
// 声明全局属性类型 declare module "@vue/runtime-core" { interface ComponentCustomProperties<T> { $api: T; } }
注意:在
shims-vue.d.ts
文件中,类型声明超过1个时,组件内需要import包就不能在其内部进行,需要将其写在最外层,否则会报错。
image-20201010101906448
适配入口文件
由于启用了typescript
,入口文件由main.js
变成了main.ts
,文件中的写法与之前相比其不同点如下:
- 初始化挂载vue由原先的
new Vue(App)
改为了按需导入写法的createApp(App)
- 使用插件时,也由原先的
Vue.use()
改成了,createApp(App).use()
在我的项目中引用了几个插件,需要在入口文件中做一些初始化的操作,插件还是2.x版本,没有ts的类型声明文件,因此导入时ts没法推导出它的类型,就得用// @ts-ignore
让ts忽略它。
完整的入口文件地址:main.ts
适配组件
基础设施完善后,接下来我们来适配组件,我们先来试试把2.x项目的所有组件搬过来看看,能不能直接启动。
结果可想而知,无法运行。因为我用了2.x的插件,vue3.0有关插件的封装,一些写法变了。我项目中总共引用了2个插件v-viewer
、vue-native-websocket
,v-viewer
这个插件无解,他底层使用用到的2.x
语法太多了,所以我选择放弃这个插件。vue-native-websocket
这个插件就是使用的Vue.prototype.xx
写法被舍弃了,用新的写法Vue.config.globalProperties.xx
将其替换即可。
image-20201009174402912
替换完成后,重新编译即可,随后启动项目,如下所示,错误解决,项目成功启动。
image-20201009175415170
正如上图中所看到的,控制台有黄色警告,因为我们组件的代码还是使用的vue2.x的语法,我们要重新整理组件中的方法从而适配vue3.0
。
注意:组件script标签声明lang="ts"后,就必须按照Vue官方文档所说使用
defineComponent
全局方法来定义组件。
组件优化
接下来,我们从login.vue
组件开始重构,看看都做了哪些优化。
- 创建
type
文件夹,文件夹内创建ComponentDataType.ts
,将组件中用到的类型指定放在其中。 - 创建
enum
文件夹,将组件中用到的枚举放在其中。
我们先来看看第一点,将组件内用到的类型进行统一管理,我们以登录组件为例,我们需要为data
返回的对象指定其每个属性的类型,因此我们ComponentDataType.ts
中创建一个名为loginDataType
的类型,其代码如下。
export type loginDataType<T> = { loginUndo: T; // 禁止登录时的图标 loginBtnNormal: T; // 登录时的按钮图标 loginBtnHover: T; // 鼠标悬浮时的登录图标 loginBtnDown: T; // 鼠标按下时的登录图标 userName: string; // 用户名 password: string; // 密码 confirmPassword: string; // 注册时的确认登录密码 isLoginStatus: number; // 登录状态:0.未登录 1.登录中 2.注册 loginStatusEnum: Object; // 登录状态枚举 isDefaultAvatar: boolean; // 头像是否为默认头像 avatarSrc: T; // 头像地址 loadText: string; // 加载层的文字 };
声明好类型后,就可以在组件中使用了,代码如下:
import { loginDataType } from "@/type/ComponentDataType"; export default defineComponent({ data<T>(): loginDataType<T> { return { loginUndo: require("../assets/img/login/icon-enter-undo@2x.png"), loginBtnNormal: require("../assets/img/login/icon-enter-undo@2x.png"), loginBtnHover: require("../assets/img/login/icon-enter-hover@2x.png"), loginBtnDown: require("../assets/img/login/icon-enter-down@2x.png"), userName: "", password: "", confirmPassword: "", isLoginStatus: 0, loginStatusEnum: loginStatusEnum, isDefaultAvatar: true, avatarSrc: require("../assets/img/login/LoginWindow_BigDefaultHeadImage@2x.png"), loadText: "上传中" }; } })
上述代码完整地址:
- type/ComponentDataType.ts
- login.vue
再然后,我们看看第二点,使用enum来优化组件内部的条件判断,例如上面data中的isLoginStatus就有3种状态,我们要根据这三种状态来做不同的事情,如果直接用数字来代表三种状态直接赋值数字,后期维护时将是一件很痛苦的事情,如果用enum来定义的话,根据语意一眼就能看出它的状态是什么。
我们在enum文件夹中创建ComponentEnum.ts
文件,组件内用到的所有枚举都会在此文件内定义,接下来在组件内创建loginStatusEnum
,代码如下:
export enum loginStatusEnum { NOT_LOGGED_IN = 0, // 未登录 LOGGING_IN = 1, // 登录中 REGISTERED = 2 // 注册 }
声明好后,我们就可以在组件中使用了,代码如下:
import { loginStatusEnum } from "@/enum/ComponentEnum"; export default defineComponent({ methods: { stateSwitching: function(status) { case "条件1": this.isLoginStatus = loginStatusEnum.LOGGING_IN; break; case "条件2": this.isLoginStatus = loginStatusEnum.NOT_LOGGED_IN; break; } } })
上述代码完整地址:
- enum/ComponentEnum.ts
- login.vue
this指向
在适配组件过程中,方法内部的this不能很好的识别,无奈就用了很笨的方法解决。
如下所示:
const _img = new Image(); _img.src = base64; _img.onload = function() { const _canvas = document.createElement("canvas"); const w = this.width / scale; const h = this.height / scale; _canvas.setAttribute("width", w + ""); _canvas.setAttribute("height", h + ""); _canvas.getContext("2d")?.drawImage(this, 0, 0, w, h); const base64 = _canvas.toDataURL("image/jpeg"); }
onload
方法内部的this
应该是指向_img
的,但是ts
并不这么认为,报错如下所示。
image-20201013171520088
this对象中不包含width
属性,解决方案就是讲this换成_img
,问题解决。
image-20201013171712449
Dom对象类型定义
当操作dom对象时,层级过时ts就无法推断出具体类型了,如下所示:
sendMessage: function(event: KeyboardEvent) { if (event.key === "Enter") { // 阻止编辑框默认生成div事件 event.preventDefault(); let msgText = ""; // 获取输入框下的所有子元素 const allNodes = event.target.childNodes; for (const item of allNodes) { // 判断当前元素是否为img元素 if (item.nodeName === "IMG") { if (item.alt === "") { // 是图片 let base64Img = item.src; // 删除base64图片的前缀 base64Img = base64Img.replace(/^data:image\/\w+;base64,/, ""); //随机文件名 const fileName = new Date().getTime() + "chatImg" + ".jpeg"; //将base64转换成file const imgFile = this.convertBase64UrlToImgFile( base64Img, fileName, "image/jpeg" ); } } } } }
上面为一个发送消息的函数的部分代码,消息框中包含图片和文字,要对图片进行单独处理,我们需要要从target
中拿到所有节点childNodes
,然后遍历每个节点获取其类型,childNodes的类型为NodeList
,那么他的每一个元素就是Node
类型,如果当前遍历到的元素的nodeName
属性是IMG
时,它就是一个图片,我们就获取它的alt属性进一步判断,再获取src属性。
然而,ts会报错alt
和src
属性不存在,报错如下:
image-20201013172815950
此时,我们就需要把item
断言成HTMLImageElement
类型。
image-20201019110053258
复杂类型定义
在适配组件过程中,遇到一个比较复杂的数据类型定义,数据如下:
data(){ return { friendsList: [ { groupName: "我", totalPeople: 2, onlineUsers: 2, friendsData: [ { username: "神奇的程序员", avatarSrc: "https://www.kaisir.cn/uploads/1ece3749801d4d45933ba8b31403c685touxiang.jpeg", signature: "今天的努力只为未来", onlineStatus: true, userId: "c04618bab36146e3a9d3b411e7f9eb8f" }, { username: "admin", avatarSrc: "https://www.kaisir.cn/uploads/40ba319f75964c25a7370e3909d347c5admin.jpg", signature: "", onlineStatus: true, userId: "32ee06c8380e479b9cd4097e170a6193" } ] }, { groupName: "我的朋友", totalPeople: 0, onlineUsers: 0, friendsData: [] }, { groupName: "我的家人", totalPeople: 0, onlineUsers: 0, friendsData: [] }, { groupName: "我的同事", totalPeople: 0, onlineUsers: 0, friendsData: [] } ] }; },
一开始我是这样定义的。
image-20201014214430066
嵌套到一起,自认为没问题,放进代码后,报错长度不匹配,这样写知识给第一个对象定义了类型。
image-20201014214529652
经过一番求助后,他们说应该分开写,不能这样嵌套定义,正确写法如下:
- 类型分开定义
// 联系人面板Data属性定义 export type contactListDataType<V> = { friendsList: Array<V>; }; // 联系人列表类型定义 export type friendsListType<V> = { groupName: string; // 分组名称 totalPeople: number; // 总人数 onlineUsers: number; // 在线人数 friendsData: Array<V>; // 好友列表 }; // 联系人类型定义 export type friendsDataType = { username: string; // 昵称 avatarSrc: string; // 头像地址 signature: string; // 个性签名 onlineStatus: boolean; // 在线状态 userId: string; // 用户id };
- 组件中使用
import { contactListDataType, friendsListType, friendsDataType } from "@/type/ComponentDataType"; data(): contactListDataType<friendsListType<friendsDataType>> { return { friendsList: [ { groupName: "我", totalPeople: 2, onlineUsers: 2, friendsData: [ { username: "神奇的程序员", avatarSrc: "https://www.kaisir.cn/uploads/1ece3749801d4d45933ba8b31403c685touxiang.jpeg", signature: "今天的努力只为未来", onlineStatus: true, userId: "c04618bab36146e3a9d3b411e7f9eb8f" }, { username: "admin", avatarSrc: "https://www.kaisir.cn/uploads/40ba319f75964c25a7370e3909d347c5admin.jpg", signature: "", onlineStatus: true, userId: "32ee06c8380e479b9cd4097e170a6193" } ] }, { groupName: "我的朋友", totalPeople: 0, onlineUsers: 0, friendsData: [] }, { groupName: "我的家人", totalPeople: 0, onlineUsers: 0, friendsData: [] }, { groupName: "我的同事", totalPeople: 0, onlineUsers: 0, friendsData: [] } ] }; }
深刻的理解到了typescript泛型的使用,经验++😄
tag属性被移除
我们在使用router-link
时,它默认会渲染成a标签,如果想让他渲染成其它自定义标签,可以通过tag
属性来修改,如下所示:
<router-link :to="{ name: 'list' }" tag="div">
然而,在vue-router
的新版本中,官方将event和tag属性移除了,因此我们就不能这么使用了,当然官方文档中也给了解决方案使用v-solt
来作为替代方案,上述代码中我们希望将其渲染成div,用v-solt
的写法如下所示:
<router-link :to="{ name: 'list' }" custom v-slot="{ navigate }"> <div @click="navigate" @keypress.enter="navigate" role="link" > </div> </router-link>
有关这一块的更多讲解,请移步官方文档:removal-of-event-and-tag-props-in-router-link
组件无法外链文件
当我把页面当组件进行引入声明时,发现vue3不支持将逻辑代码外链,像下面这样,通过src外链。
<script lang="ts" src="../assets/ts/message-display.ts"></script>
在组件中引用。
<template> <message-display message-status="0" list-id="1892144211" /> </template> <script> import messageDisplay from "@/components/message-display.vue"; export default defineComponent({ name: "msg-list", components: { messageDisplay }, }) </script>
然后,他就报错了,类型无法推断。
image-20201018224619607
尝试了很多方法,最后发现是不能通过src外链的问题,于是我把ts文件中的代码写在vue模版中报错就没了。
必须使用as进行断言
当我把代码搬到vue模版中后,它报了一些很奇怪的错误,如下所示imgContent
变量可能存在多个类型,ts无法推断出具体类型,此时就需要我们自己进行断言给他指定类型,我用了尖括号的写法,他报错了,webstorm可能对vue3的适配不是很好,他的报错很奇怪,如下所示
image-20201018225114933
一开始,我看到这个错误我是一脸懵逼的,一个朋友告诉我用排除法,注释下距离它最近的代码,看看是否会报错,于是找到了问题根源,就是上面的类型断言的锅,将它修改后,问题解决。
image-20201018225618020
问题是解决了,但是我很是想不通为何一定要用as,尖括号跟他是同等的才对,于是我翻了官方文档。
image-20201018225919664
正如官方文档所说,启用jsx
后就只能使用as语法了。可能vue3的模版语法默认是启用jsx
的吧。
ref数组不会自动创建数组
在vue2中,在v-for
里使用ref属性时会用ref数组填充相应的$refs
属性,如下所示为好友列表的部分代码,它通过循环friendsList
,将groupArrow
和buddyList
放进ref数组中。
<template> <div class="group-panel"> <div class="title-panel"> <p class="title">好友</p> </div> <div class="row-panel" v-for="(item,index) in friendsList" :key="index"> <div class="main-content" @click="groupingStatus(index)"> <div class="icon-panel"> <img ref="groupArrow" src="../assets/img/list/tchat_his_arrow_right@2x.png" alt="左箭头"/> </div> <div class="name-panel"> <p>{{item.groupName}}</p> </div> <div class="quantity-panel"> <p>{{item.onlineUsers}}/{{item.totalPeople}}</p> </div> </div> <!--好友列表--> <div class="buddy-panel" ref="buddyList" style="display:none"> <div class="item-panel" v-for="(list,index) in item.friendsData" :key="index" tabindex="0"> <div class="main-panel" @click="getBuddyInfo(list.userId)"> <div class="head-img-panel"> <img :src="list.avatarSrc" alt="用户头像"> </div> <div class="nickname-panel"> <!--昵称--> <div class="name-panel"> {{list.username}} </div> <!--签名--> <div class="signature-panel"> [{{list.onlineStatus?"在线":"离线"}}]{{list.signature}} </div> </div> </div> </div> </div> </div> </div> </template>
我们通过$refs
可以访问到相应的节点,如下所示。
import lodash from 'lodash'; export default { name: "contact-list", methods:{ // 分组状态切换 groupingStatus:function (index) { if(lodash.isEmpty(this.$route.params.userId)===false){ this.$router.push({name: "list"}).then(); } // 获取transform的值 let transformVal = this.$refs.groupArrow[index].style.transform; if(lodash.isEmpty(transformVal)===false){ // 截取rotate的值 transformVal = transformVal.substring(7,9); // 判断是否展开 if (parseInt(transformVal)===90){ this.$refs.groupArrow[index].style.transform = "rotate(0deg)"; this.$refs.buddyList[index].style.display = "none"; }else{ this.$refs.groupArrow[index].style.transform = "rotate(90deg)"; this.$refs.buddyList[index].style.display = "block"; } }else{ // 第一次点击添加transform属性,旋转90度 this.$refs.groupArrow[index].style.transform = "rotate(90deg)"; this.$refs.buddyList[index].style.display = "block"; } }, // 获取列表好友信息 getBuddyInfo:function (userId) { // 判断当前路由params与当前点击项的userId是否相等 if(!lodash.isEqual(this.$route.params.userId,userId)){ this.$router.push({name: "dataPanel", params: {userId: userId}}).then(); } } } }
上述写法在vue2没问题,但是在vue3中你得到的结果是报错,官方认为这种行为会变得不明确且效率低下,采用了新的语法来解决这个问题,通过ref来绑定一个函数去处理,如下所示。
<template> <!---其它代码省略---> <img :ref="setGroupArrow" src="../assets/img/list/tchat_his_arrow_right@2x.png" alt="左箭头" /> <!---其它代码省略---> <div class="buddy-panel" :ref="setGroupList" style="display:none"> </div> </template> <script lang="ts"> import _ from "lodash"; import { defineComponent } from "vue"; import { contactListDataType, friendsListType, friendsDataType } from "@/type/ComponentDataType"; export default defineComponent({ name: "contact-list", data(): contactListDataType<friendsListType<friendsDataType>> { return { groupArrow: [], groupList: [] } }, // 设置分组箭头Dom setGroupArrow: function(el: Element) { this.groupArrow.push(el); }, // 设置分组列表dom setGroupList: function(el: Element) { this.groupList.push(el); }, // 列表状态切换 groupingStatus: function(index: number) { if (!_.isEmpty(this.$route.params.userId)) { this.$router.push({ name: "list" }).then(); } // 获取transform的值 let transformVal = this.groupArrow[index].style.transform; if (!_.isEmpty(transformVal)) { // 截取rotate的值 transformVal = transformVal.substring(7, 9); // 判断分组列表是否展开 if (parseInt(transformVal) === 90) { this.groupArrow[index].style.transform = "rotate(0deg)"; this.groupList[index].style.display = "none"; } else { this.groupArrow[index].style.transform = "rotate(90deg)"; this.groupList[index].style.display = "block"; } } else { // 第一次点击添加transform属性,旋转90度 this.groupArrow[index].style.transform = "rotate(90deg)"; this.groupList[index].style.display = "block"; } } )}
完整代码请移步:contact-list.vue
ref更多描述请移步官方文档: v-for 中的 Ref 数组
项目地址
至此,项目已经可以正常启动了,重构工作也结束了,接下来要解决的问题就是vue-native-websocket
这个插件无法在vue3中工作的问题了。一开始我以为把它在原型行挂载的写法改动下就可以了,然而是我想的太简单了,改动后编辑器是不报错了,但是在运行时会报很多错。无奈只好先把与服务端交互这部分代码移除掉了。
接下来我会尝试重构vue-native-websocket
这个插件,让其支持vue3。
最后放上本文重构好的项目代码地址:chat-system
写在最后
- 公众号无法外链,文中链接可点击下方阅读原文进行查看。