使用 Vue3 重构 Vue2 项目(长文)
前言
2020年9月18日,vue3正式版发布了,前几天把文档整体读了一遍,感触很深,可以解决我项目中的一些痛点,于是就决定重构之前那个vue2的开源项目。
本篇文章就记录下重构vue2项目的过程,欢迎各位感兴趣的开发者阅读本文。
环境搭建
本来打算使用vite + vue3 + VueRouter + vuex + typescript
来构架项目的,但是经过一番折腾后发现vite
目前只对vue支持,对于vue周边的一些库还没做到支持,没法在项目中使用。
最后,还是决定使用Vue Cli 4.5
来构建了。
虽然vite目前还无法正常在项目中使用,但是我也折腾了一回,就记录下在折腾时的过程以及一些报错。
使用vite构建项目
本文采用的包管理工具为yarn
,将其升级至最新版本就可以正常创建vite项目了。
初始化项目
接下来,我们来看看具体步骤。
打开终端,进入你的项目目录,运行命令:
yarn crete vite-app vite-project
,该命令用于创建一个名为vite-project
的项目。创建完成后,会得到如下所示的文件。
进入创建好的项目,运行命令:
yarn install
,该命令会安装package.json
中声明的依赖。我们使用
IDE
打开刚才创建的项目,整体项目如下所示,vite官方为我们提供了一个简单的demo。打开
package.json
查看启动命令在终端运行命令:yarn run dev
或者点击ide
的运行图标来启动项目。大功告成,浏览器访问
http://localhost:3000/
,如下所示。
集成Vue周边库
我们将Vue CLI
初始化的项目文件替换到用vite
初始化的项目中去,然后修改packge.json
中的相关依赖,然后重新安装依赖即可。
具体过程如下:
替换文件,替换后的项目目录如下所示。
从
package.json
中提取我们需要的依赖,提取后的文件下。
{"name": "vite-project","version": "0.1.0","scripts": {"dev": "vite","build": "vite build"},"dependencies": {"core-js": "^3.6.5","vue": "^3.0.0-0","vue-class-component": "^8.0.0-0","vue-router": "^4.0.0-0","vuex": "^4.0.0-0"},"devDependencies": {"vite": "^1.0.0-rc.1","@typescript-eslint/eslint-plugin": "^2.33.0","@typescript-eslint/parser": "^2.33.0","@vue/compiler-sfc": "^3.0.0-0","@vue/eslint-config-prettier": "^6.0.0","@vue/eslint-config-typescript": "^5.0.2","eslint": "^6.7.2","eslint-plugin-prettier": "^3.1.3","eslint-plugin-vue": "^7.0.0-0","node-sass": "^4.12.0","prettier": "^1.19.1","sass-loader": "^8.0.2","typescript": "~3.9.3"},"license": "MIT"
}
启动项目,没报错,嘴角疯狂上扬。
浏览器访问后,空白页面,打开
console
后,发现main.js 404
难搞,找不到main.js
,那我把main.ts
后缀改一下试试。将后缀改成js
后,文件是不报错404了,但是又有了新的错误。
vite服务500和@别名无法识别,于是我打开ide的控制台看了错误,大概是scss的错,vite还没支持scss。
scss不支持,别名不识别,网上找了一圈也没找到解决方案,这些最基础的东西都无法被vite支持,那它就不能用在项目中了,于是我放弃了。
综合上述,vite
要走的路还有很多,等它在社区成熟了,再将它应用到项目中吧。
使用Vue Cli构建项目
由于vite
的不合适,我们还是继续选择用webpack
,此处我们选择用Vue CLI 4.5
来创建项目。
初始化项目
在终端进入项目目录,执行命令:
vue create chat-system-vue3
该命令用于创建一个名为chat-system-vue3
的项目。创建完成后,如下所示。
用
IDE
打开项目,打开package.json
文件,查看项目启动命令或者直接点编译器的运行按钮。OK,大功告成,打开浏览器,访问终端的内网地址。
解决报错问题
在浏览CLI
默认创建的demo时,打开main.js
文件发现其中App.vue
文件报类型错误,无法推导出具体的类型。
一开始,我也懵逼,想起了Vue文档所说的,启用TypeScript必须要让 TypeScript 正确推断 Vue 组件选项中的类型,需要使用 defineComponent
。
App.vue文件代码如下:
<template><div id="nav"><router-link to="/">Home</router-link> |<router-link to="/about">About</router-link></div><router-view />
</template><style lang="scss">
#app {font-family: Avenir, Helvetica, Arial, sans-serif;-webkit-font-smoothing: antialiased;-moz-osx-font-smoothing: grayscale;text-align: center;color: #2c3e50;
}#nav {padding: 30px;a {font-weight: bold;color: #2c3e50;&.router-link-exact-active {color: #42b983;}}
}
</style>
观察代码后我们发现CLI
生成的代码没有包含文档中所描述的代码,因此我们将其补充上,然后导出即可。
import { defineComponent } from "vue";
const Component = defineComponent({// 已启用类型推断
});
export default Component;
加入上述代码后,我们的代码就不报错了。
根据官网描述,我们可以在defineComponent
的包裹中写组件的逻辑代码,但是我看了CIL提供的demo的Home
组件后发现,他的写法如下。
export default class Home extends Vue {}
在项目的src
目录下有一个名为shims-vue.d.ts
的文件,它声明了所有vue文件的返回类型,因此我们可以按照上述方法来写。该声明文件代码如下。
declare module "*.vue" {import { defineComponent } from "vue";const component: ReturnType<typeof defineComponent>;export default component;
}
这样的写法看起来更符合TypeScript,不过这种写法写法只支持部分属性,同样的我们组件的逻辑代码写在类内部即可,那么将刚才App.vue
文件中做的更改也应用到此处,如下所示。
<script lang="ts">
import { Vue } from "vue-class-component";
export default class App extends Vue {}
</script>
class
写法支持的属性如下图所示:
配置IDE
此处内容仅适用于webstorm
,如果编辑器是其他的可跳过本部分。
我们在项目中集成了eslint
和prettier
,默认情况下webstorm
是没有启用这两个东西的,需要我们自己手动开启。
打开webstorm的配置菜单,如下所示
image-20201006153458084
搜索
eslint
,按照下图所示进行配置,配置完成后点APPLY
、OK
即可。image-20201006153031544
搜索
prettier
,按照下图所示进行配置,配置完成后点APPLY
、OK
即可。image-20201006153654226
配置完上面的内容后,还有一个问题,在组件上用v-if
v-for
等vue指令时没有提示,这是因为webstorm没法正确读取node_modules
包,按照下述操作即可解决这一问题。
执行上述操作后,等待时间根据cpu
性能而定,届时电脑会发热。这都是正常现象
成功后,我们发现编辑器已经可以正常识别v-
指令了,并且给了相应的提示。
项目目录对比
按照上述步骤,即可创建一个vue3
的项目,接下来我们将需要重构的vue2
项目的目录与上面创建的项目进行下目录对比。
如下所示,为
vue2.0
项目的目录image-20201006162826706
如下所示,为
vue3.0
项目的目录image-20201006162936370
仔细观察后,我们发现在目录上并没有什么大的区别,只是多了typescript
的配置文件和项目内使用ts的时辅助文件。
项目重构
接下来,我们来一步步把vue2项目的文件迁移到vue3项目中,修改不合适的地方,让其适配vue3.0。
适配路由配置
我们先从路由配置文件开始适配,打开vue3项目的router/index.ts
文件,发现有一个报错,报错如下。
错误信息是类型没被推导出来,我看了下面路由的写法后,盲猜它需要用函数返回,于是试了下,还真就是这样,正确的路由写法如下。
{path: "/",name: "Home",component: () => Home}
整体的路由配置文件代码如下:
import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router";
import Home from "../views/Home.vue";const routes: Array<RouteRecordRaw> = [{path: "/",name: "Home",component: () => Home},{path: "/about",name: "About",// route level code-splitting// this generates a separate chunk (about.[hash].js) for this route// which is lazy-loaded when the route is visited.component: () =>import(/* webpackChunkName: "about" */ "../views/About.vue")}
];const router = createRouter({history: createWebHashHistory(),routes
});export default router;
我们再来看看vue2
项目中的路由配置,为了简单起见我摘抄了部分代码过来,如下所示。
import Vue from 'vue'
import VueRouter from 'vue-router'
import MsgList from '../views/msg-list'
import Login from "../views/login"
import MainBody from '../components/main-body'
Vue.use(VueRouter);const routes = [{path: '/',redirect: '/contents/message/message',},{name: 'contents',path: '/contents/:thisStatus',// 重定向到嵌套路由redirect: '/contents/:thisStatus/:thisStatus/',components: {mainArea: MainBody},props: {mainArea: true},children: [{path: 'message',components: {msgList: MsgList}}],},{name: 'login',path: "/login",components: {login:Login}}
];const router = new VueRouter({// mode: 'history',routes,
});export default router
经过观察后,它们的不同点如下:
Vue.use(VueRouter)
这种写法被移除new VueRouter({})
写法改为了createRouter({})
hash模式和history模式声明由原先的
mode
选项变更为了createWebHashHistory()
和createWebHistory()
更加语义化了声明路由时多了ts的类型注解
Array<RouteRecordRaw>
知道它们的区别后,我们就可以对路由进行适配和迁移了,迁移完成的路由配置文件:router/index.ts
这里有个小坑,路由懒加载的时候必须给他返回一个函数。例如:
component: () => import("../views/msg-list.vue")
。不然就会报黄色警告。
适配Vuex配置
接下来我们来看看两个版本在vuex
使用上的区别,如下所示为vue3
的vuex配置。
import { createStore } from "vuex";export default createStore({state: {},mutations: {},actions: {},modules: {}
});
我们再来看看vue2
项目中的vuex配置,为了简洁起见,我只列出了大体代码。
import Vue from 'vue'
import Vuex from 'vuex'Vue.use(Vuex);export default new Vuex.Store({state: {},mutations: {},actions: {},modules: {}
})
经过对比后,我们发现的不同点如下所示:
按需导入
import { createStore } from "vuex"
,移除了之前的整个导入import Vuex from 'vuex'
移除了
Vue.use(Vuex)
的写法导出时丢弃之前的
new Vuex.Store
写法,改用了createStore
写法。
知道上述不同点后,我们就可以对代码进行适配和迁移了,迁移完成的vuex配置文件:store/index.ts
如果需要在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-Controlheards: {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里获取tokenconst token = store.state.token;// 如果token存在就在请求头里添加token && (config.headers.token = token);return config;},function(error) {// Do something with request errorerror.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
没法推导出其类型,我们在使用时就会报错。
要解决这个错误,我们就需要在shims-vue.d.ts
中声明api
的的类型
// 声明全局属性类型
declare module "@vue/runtime-core" {interface ComponentCustomProperties<T> {$api: T;}
}
注意:在
shims-vue.d.ts
文件中,类型声明超过1个时,组件内需要import包就不能在其内部进行,需要将其写在最外层,否则会报错。
适配入口文件
由于启用了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
将其替换即可。
替换完成后,重新编译即可,随后启动项目,如下所示,错误解决,项目成功启动。
正如上图中所看到的,控制台有黄色警告,因为我们组件的代码还是使用的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
并不这么认为,报错如下所示。
this对象中不包含width
属性,解决方案就是讲this换成_img
,问题解决。
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转换成fileconst imgFile = this.convertBase64UrlToImgFile(base64Img,fileName,"image/jpeg");}}}}
}
上面为一个发送消息的函数的部分代码,消息框中包含图片和文字,要对图片进行单独处理,我们需要要从target
中拿到所有节点childNodes
,然后遍历每个节点获取其类型,childNodes的类型为NodeList
,那么他的每一个元素就是Node
类型,如果当前遍历到的元素的nodeName
属性是IMG
时,它就是一个图片,我们就获取它的alt属性进一步判断,再获取src属性。
然而,ts会报错alt
和src
属性不存在,报错如下:
此时,我们就需要把item
断言成HTMLImageElement
类型。
复杂类型定义
在适配组件过程中,遇到一个比较复杂的数据类型定义,数据如下:
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: []}]};},
一开始我是这样定义的。
嵌套到一起,自认为没问题,放进代码后,报错长度不匹配,这样写知识给第一个对象定义了类型。
经过一番求助后,他们说应该分开写,不能这样嵌套定义,正确写法如下:
类型分开定义
// 联系人面板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>
然后,他就报错了,类型无法推断。
尝试了很多方法,最后发现是不能通过src外链的问题,于是我把ts文件中的代码写在vue模版中报错就没了。
必须使用as进行断言
当我把代码搬到vue模版中后,它报了一些很奇怪的错误,如下所示imgContent
变量可能存在多个类型,ts无法推断出具体类型,此时就需要我们自己进行断言给他指定类型,我用了尖括号的写法,他报错了,webstorm可能对vue3的适配不是很好,他的报错很奇怪,如下所示
一开始,我看到这个错误我是一脸懵逼的,一个朋友告诉我用排除法,注释下距离它最近的代码,看看是否会报错,于是找到了问题根源,就是上面的类型断言的锅,将它修改后,问题解决。
问题是解决了,但是我很是想不通为何一定要用as,尖括号跟他是同等的才对,于是我翻了官方文档。
正如官方文档所说,启用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: []}},// 设置分组箭头DomsetGroupArrow: function(el: Element) {this.groupArrow.push(el);},// 设置分组列表domsetGroupList: 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
写在最后
公众号无法外链,文中链接可点击下方阅读原文进行查看。
❤️爱心三连击1.看到这里了就点个在看支持下吧,你的「点赞,在看」是我创作的动力。
2.关注公众号程序员成长指北,回复「1」加入Node进阶交流群!「在这里有好多 Node 开发者,会讨论 Node 知识,互相学习」!
3.也可添加微信【ikoala520】,一起成长。
“在看转发”是最大的支持
使用 Vue3 重构 Vue2 项目(长文)相关推荐
- Vue3教程:Vue3 开源商城项目重构计划正式启动!
我打算用 Vue3 写一个商城项目,目前已经开始着手开发,测试完成后正式开源到 GitHub,让大家也可以用现成的 Vue3 大型商城项目源码来练练手. 1 Vue3 来了 今年上半年,我用 Vue ...
- 在已有项目中集成mars3d注意事项(vue3和vue2技术栈下)
目前cesium1.96改变了代码打包方式,在vue2项目下会有这个兼容问题.目前解决方式有(三选一) 1. 参考 https://gitee.com/marsgis/mars3d-vue-templ ...
- 在 Vue3 成为默认版本后,盘点了 Vue3 与 Vue2 的区别
目录 前言 正文 一.Vue3 与 Vue2 区别概览 二.Vue3 与 Vue2 区别详述 生命周期 多根节点 Composition API 异步组件(Suspense) Teleport 响应式 ...
- vue3和vue2的区别并且如何升级 (对于vue2有基础的)
目录 一.vue3值得注意的新特性 1.1.Teleport 1.2.Vue 3 现在正式支持了多根节点的组件 1.3.自定义事件 emits 1.4.v-model 1.4.1. v-model参数 ...
- 关于 vue3.0 实战项目 setup、 props、 reactive、ref
关于 vue3.0 实战项目中遇到的问题 介绍vue3.0的特性: 亿点小知识 1.diff算法的优化 增加了静态标记PatchFlag 2.按需编译,体积比Vue2.x更小(Tree shaking ...
- 一个 Java 猿眼中 Vue3 和 Vue2 的差异
随着 TienChin 项目视频的录制,松哥终于也要静下心来,认真捋一捋 Vue3 中的各种新特性了,然后再和小伙伴们进行分享,其实 Vue3 中还是带来了很多新鲜的玩意,今天我们就不卷 Java 了 ...
- Vue2项目总结-电商后台管理系统
Vue2项目总结-电商后台管理系统 去年做的项目,拖了很久,总算是打起精力去做这个项目的总结,并对Vue2的相关知识进行回顾与复习 各个功能模块如果有过多重复冗杂的部分,将会抽取部分值得记录复习的地方 ...
- vue3+cli4运行项目报错export ‘default‘ (imported as ‘VueRouter‘) was not found in ‘vue-router‘
vue3+cli4运行项目报错export 'default' (imported as 'VueRouter') was not found in 'vue-router' 1.解决方案: cli3 ...
- vue - vue3与vue2.x的区别(一) :目录结构不一致
今天总结一下vue3与vue2.x的区别 -- 目录结构不一致.由于目录结构的不一致相对的也造成了一些问题的存在,比如打包项目之后打开出现白屏现象. 一.目录结构不一致 通过上图可以发现 vu ...
最新文章
- java快速查找算法_Java实现的快速查找算法示例
- 将中文日期转换成自己想要的格式如:2018年09月29日转换成2018-09-29
- android圆形菜单
- 在c语言中scanf什么时候用,scanf什么时候用??c语言?
- RabbitMQ报错NOT_ALLOWED - access to vhost ‘/‘ refused for user ‘zq‘(10, 40)
- Java加密解密快速入门上篇【包括MD5、BASE64、DES、RSA等算法】
- python - 2 8 16进制/颜色/字符编码
- Hadoop 源码目录树
- 微信公众号 获取推送消息
- python if else_菜鸟笔记015 Python If ... Else
- 唯品会关键词搜索API接口(item_search-按关键字搜索唯品会商品API接口),唯品会API接口
- Cramer`s Rule 克莱姆法则(克拉默法则)
- c语言设计通讯录设计报告,C语言通讯录课程设计报告--设计一个通讯录管理系统...
- 自媒体全套教程+全套工具(带教程)+原创实操教程
- mysql左链sql去重,MySQL数据库去重 SQL解决
- c++工程师历年企业笔试真题汇总
- mysql 主从1236_mysql主从复制1236错误
- ui设计学习路线图分享送给初学者
- 个人网站如何转型为商业网站
- A `Concatenate` layer requires inputs with matching shapes except for the concat axis.