前言

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"
}

8abcc9f5b934568e54c0229c6663866c
  • 启动项目,没报错,嘴角疯狂上扬。

  • 浏览器访问后,空白页面,打开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写法支持的属性如下图所示:

image-20201009210815033

配置IDE

此处内容仅适用于webstorm,如果编辑器是其他的可跳过本部分。

我们在项目中集成了eslintprettier,默认情况下webstorm是没有启用这两个东西的,需要我们自己手动开启。

  • 打开webstorm的配置菜单,如下所示

    image-20201006153458084
  • 搜索eslint,按照下图所示进行配置,配置完成后点APPLYOK即可。

    image-20201006153031544
  • 搜索prettier,按照下图所示进行配置,配置完成后点APPLYOK即可。

    image-20201006153654226

配置完上面的内容后,还有一个问题,在组件上用v-if v-for等vue指令时没有提示,这是因为webstorm没法正确读取node_modules包,按照下述操作即可解决这一问题。

image-20201006154114315

执行上述操作后,等待时间根据cpu性能而定,届时电脑会发热。这都是正常现象

image-20201006154306682

成功后,我们发现编辑器已经可以正常识别v-指令了,并且给了相应的提示。

image-20201006154454592

项目目录对比

按照上述步骤,即可创建一个vue3的项目,接下来我们将需要重构的vue2项目的目录与上面创建的项目进行下目录对比。

  • 如下所示,为vue2.0项目的目录

    image-20201006162826706
  • 如下所示,为vue3.0项目的目录

    image-20201006162936370

仔细观察后,我们发现在目录上并没有什么大的区别,只是多了typescript的配置文件和项目内使用ts的时辅助文件。

项目重构

接下来,我们来一步步把vue2项目的文件迁移到vue3项目中,修改不合适的地方,让其适配vue3.0。

适配路由配置

我们先从路由配置文件开始适配,打开vue3项目的router/index.ts文件,发现有一个报错,报错如下。

image-20201006215331894

错误信息是类型没被推导出来,我看了下面路由的写法后,盲猜它需要用函数返回,于是试了下,还真就是这样,正确的路由写法如下。

  {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")。不然就会报黄色警告。

image-20201015223425458

image-20201015223525227

适配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没法推导出其类型,我们在使用时就会报错。

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-viewervue-native-websocketv-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组件开始重构,看看都做了哪些优化。

  1. 创建type文件夹,文件夹内创建ComponentDataType.ts,将组件中用到的类型指定放在其中。

  2. 创建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转换成fileconst imgFile = this.convertBase64UrlToImgFile(base64Img,fileName,"image/jpeg");}}}}
}

上面为一个发送消息的函数的部分代码,消息框中包含图片和文字,要对图片进行单独处理,我们需要要从target中拿到所有节点childNodes,然后遍历每个节点获取其类型,childNodes的类型为NodeList,那么他的每一个元素就是Node类型,如果当前遍历到的元素的nodeName属性是IMG时,它就是一个图片,我们就获取它的alt属性进一步判断,再获取src属性。

然而,ts会报错altsrc属性不存在,报错如下:

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,将groupArrowbuddyList放进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 项目(长文)相关推荐

  1. Vue3教程:Vue3 开源商城项目重构计划正式启动!

    我打算用 Vue3 写一个商城项目,目前已经开始着手开发,测试完成后正式开源到 GitHub,让大家也可以用现成的 Vue3 大型商城项目源码来练练手. 1 Vue3 来了 今年上半年,我用 Vue ...

  2. 在已有项目中集成mars3d注意事项(vue3和vue2技术栈下)

    目前cesium1.96改变了代码打包方式,在vue2项目下会有这个兼容问题.目前解决方式有(三选一) 1. 参考 https://gitee.com/marsgis/mars3d-vue-templ ...

  3. 在 Vue3 成为默认版本后,盘点了 Vue3 与 Vue2 的区别

    目录 前言 正文 一.Vue3 与 Vue2 区别概览 二.Vue3 与 Vue2 区别详述 生命周期 多根节点 Composition API 异步组件(Suspense) Teleport 响应式 ...

  4. vue3和vue2的区别并且如何升级 (对于vue2有基础的)

    目录 一.vue3值得注意的新特性 1.1.Teleport 1.2.Vue 3 现在正式支持了多根节点的组件 1.3.自定义事件 emits 1.4.v-model 1.4.1. v-model参数 ...

  5. 关于 vue3.0 实战项目 setup、 props、 reactive、ref

    关于 vue3.0 实战项目中遇到的问题 介绍vue3.0的特性: 亿点小知识 1.diff算法的优化 增加了静态标记PatchFlag 2.按需编译,体积比Vue2.x更小(Tree shaking ...

  6. 一个 Java 猿眼中 Vue3 和 Vue2 的差异

    随着 TienChin 项目视频的录制,松哥终于也要静下心来,认真捋一捋 Vue3 中的各种新特性了,然后再和小伙伴们进行分享,其实 Vue3 中还是带来了很多新鲜的玩意,今天我们就不卷 Java 了 ...

  7. Vue2项目总结-电商后台管理系统

    Vue2项目总结-电商后台管理系统 去年做的项目,拖了很久,总算是打起精力去做这个项目的总结,并对Vue2的相关知识进行回顾与复习 各个功能模块如果有过多重复冗杂的部分,将会抽取部分值得记录复习的地方 ...

  8. 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 ...

  9. vue - vue3与vue2.x的区别(一) :目录结构不一致

      今天总结一下vue3与vue2.x的区别 -- 目录结构不一致.由于目录结构的不一致相对的也造成了一些问题的存在,比如打包项目之后打开出现白屏现象. 一.目录结构不一致   通过上图可以发现 vu ...

最新文章

  1. java快速查找算法_Java实现的快速查找算法示例
  2. 将中文日期转换成自己想要的格式如:2018年09月29日转换成2018-09-29
  3. android圆形菜单
  4. 在c语言中scanf什么时候用,scanf什么时候用??c语言?
  5. RabbitMQ报错NOT_ALLOWED - access to vhost ‘/‘ refused for user ‘zq‘(10, 40)
  6. Java加密解密快速入门上篇【包括MD5、BASE64、DES、RSA等算法】
  7. python - 2 8 16进制/颜色/字符编码
  8. Hadoop 源码目录树
  9. 微信公众号 获取推送消息
  10. python if else_菜鸟笔记015 Python If ... Else
  11. 唯品会关键词搜索API接口(item_search-按关键字搜索唯品会商品API接口),唯品会API接口
  12. Cramer`s Rule 克莱姆法则(克拉默法则)
  13. c语言设计通讯录设计报告,C语言通讯录课程设计报告--设计一个通讯录管理系统...
  14. 自媒体全套教程+全套工具(带教程)+原创实操教程
  15. mysql左链sql去重,MySQL数据库去重 SQL解决
  16. c++工程师历年企业笔试真题汇总
  17. mysql 主从1236_mysql主从复制1236错误
  18. ui设计学习路线图分享送给初学者
  19. 个人网站如何转型为商业网站
  20. A `Concatenate` layer requires inputs with matching shapes except for the concat axis.

热门文章

  1. 电视服务器媒体流中断是什么意思,视频流媒体服务器稳定吗?出现播放中断问题怎么办?...
  2. ubuntu20.4安装ROS2 Noetic Ninjem
  3. anaconda3安装注意事项以及pytorch环境配置
  4. arm linux 时钟源 信息,Linux学习——ARM芯片时钟体系
  5. Andorid实例,淘宝评分条,星级评分条应用
  6. 华硕天选 关闭CPU睿频降低待机温度
  7. 其实,男人也需要被疼爱
  8. go的beego搭建
  9. React.FC详细解说
  10. C++题解:蜗牛旅行