仅需十分钟!快速使用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"
}

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-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-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转换成file
              const 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: []
    }
  },
   // 设置分组箭头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


最后

如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:

  1. 点个「在看」,让更多的人也能看到这篇内容(喜欢不点在看,都是耍流氓 -_-)

  2. 欢迎加我微信「qianyu443033099」拉你进技术群,长期交流学习...

  3. 关注公众号「前端下午茶」,持续为你推送精选好文,也可以加我为好友,随时聊骚。

点个在看支持我吧,转发就更好了

SHERlocked93
关注 关注
  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
vue2,vue3已经生成好的脚手架(空项目包)
07-19
vue脚手架 vue项目包包含vue2,vue3 所包含的环境:Router路由 Vuex CSS Pre-processors CSS预处理程序(sass)vue脚手架 vue项目包包含vue2,vue3 所包含的环境:Router路由 Vuex CSS Pre-processors CSS预处理程序(sass)vue脚手架 vue项目包包含vue2,vue3 所包含的环境:Router路由 Vuex CSS Pre-processors CSS预处理程序(sass)vue脚手架 vue项目包包含vue2,vue3 所包含的环境:Router路由 Vuex CSS Pre-processors CSS预处理程序(sass)vue脚手架 vue项目包包含vue2,vue3 所包含的环境:Router路由 Vuex CSS Pre-processors CSS预处理程序(sass)vue脚手架 vue项目包包含vue2,vue3 所包含的环境:Router路由 Vuex CSS Pre-processors CSS预处理程序(sass)
Vue CLI2升级Vue CLI3的方法步骤
10-16
主要介绍了Vue CLI2升级Vue CLI3的方法步骤,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,要的朋友们下面随着小编来一起学习学习吧
10个常见的使用场景,助你从 Vue2 丝滑过渡到 Vue3 !_vue2可以直接去过度vue3吗(1)
最新发布
2401_84411433的博客
04-27 1025
)// 属性或方法必须暴露出去,父组件才能使用复制代码父组件复制代码```
Vue-CLI 3 scp2自动部署项目至服务器的方法
10-15
主要介绍了Vue-CLI 3 scp2自动部署项目至服务器的方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,要的朋友们下面随着小编来一起学习学习吧
快速Vue项目升级到webpack3的方法步骤
08-29
主要给大家介绍了关于如何快速Vue项目升级到webpack3的方法步骤文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,要的朋友们下面随着小编来一起学习学习吧。
node 版本管理工具 实现vue2与vue3切换开发
08-18
node 版本管理工具 实现vue2与vue3切换开发
使用Vue3重构vue2项目
奇舞周刊
10-20 3923
前言2020年9月18日,vue3正式版发布了,前几天把文档整体读了一遍,感触很深,可以解决我项目中的一些痛点,于是就决定重构之前那个vue2的开源项目。本篇文章就记录下重构vue2项目...
使用 Vue3 重构 Vue2 项目
weixin_62897746的博客
02-16 1817
2020年9月18日,vue3正式版发布了,前几天学习完成后,我决定重构后台管理项目,本篇文章给大家讲解了重构过程中遇到的一些问题和解决方案。 为什么要重构项目: 使代码更容易理解,方便后期维护,也就是要让每个模块的定位清晰明确 发现隐藏的代码缺陷 代码风格要优雅~,内容质量高,按照合理的设计模式和编程思想去重构 同步新的项目功能交互来个极致体验,功能上要要尽量做到不卡顿 不闪退,要满足产品求的细节 从长远来看,可以提高编程效率
使用Vue3重构Vue2项目
zhmen的博客
01-07 1495
使用Vue3重构Vue2项目
Vue3重构Vue2项目,实现模块化导入,动态加载路由
qq_42365152的博客
05-06 2249
Vue的响应式原理; Webpack模块导入require.context; Vite模块导入import.meta.globEager; Vue-router3菜单导航动态加载路由; Vue-router4菜单导航动态加载路由;
使用vue3重构升级vue2项目流程与总结
lwx33912138的博客
02-16 2674
使用vue3重构升级vue2项目流程与总结,vue2项目升级vue3项目时候的注意事项与bug的处理研究
快速解决vue2+vue-cli3项目ie兼容的问题
01-18
刚写好的项目在谷歌能打开,ie打不开,要处理兼容问题 先根据这篇文章修改 详解Vue-cli3 项目在安卓低版本系统和IE上白屏问题解决 如果没有其他问题是可以了的,下面是我代码中出现的其他问题导致页面无法显示或者报错 sockjs报错 控制台会一直报这个错,sockjs-node 是一个JavaScript库,提供跨浏览器JavaScript的API,创建了一个低延迟、全双工的浏览器和web服务器之间通信通道。 服务端:sockjs-node(https://github.com/sockjs/sockjs-node) 客户端:sockjs-clien(https://github.co
Vue-cli创建vue3升级vue3.2版本的方法
竹杖芒鞋轻胜马,谁怕?一蓑烟雨任平生
08-11 2174
vue3.0升级vue3.2
后台管理项目Vue3重构Vue2的过程(一)
SuBaijiu的博客
02-16 350
在学习Vue时如何用Vue3重构Vue2项目(一)
如何搭建一个vue项目
CalvinXCui的博客
03-19 587
vue现在是前端开发中最火的一个框架,今天我们就一起来说说如何搭建一个vue框架 1、全局安装 vue-cli npm install --global vue-cli 注意:如果npm安装起来太慢,也可以使用国内阿里云的镜像cnpm进行安装,但使用cnpm之前必先安装cnpm的镜像,具体cnpm的安装方式可参考楼主博客首页相关文章。cnpm的使用方法和npm基本一致,只要将npm换成c...
Vite+Vue3项目迁移Vue2项目下基于AMD、CMD、CommonJS规范开发的包或模块详细教程
薛定谔的猫的博客
09-08 630
webpack在很多时候都很强大,但是对于在vue2的时代做为脚手架工具对于代码热跟新,以及项目启动就显得很慢,当我们的项目使用的模块越来越多,基于webpack的vue项目就越慢,很耗时。Vite+Vue3可以完美的解决这些问题,但是其中有一个最致命的问题,在vue2时代有很多的模块和包都是基于AMD、CMD、CommonJS规范开发的,但Vite是基于ES Module的开发服务器,所以在Vite下基于AMD、CMD、CommonJS规范开发的包和模块是无法直接使用的。那么如何解决?
vue3的代码改成vue2的转换
weixin_40945354的博客
04-02 8805
vue3 <!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Vue的方法_侠课岛(9xkd.com)</title> <script src="https://unpkg.com/vue@next"></script> </head> <body> <div id="hello-vue" class="
vue3使用2
weixin_47039303的博客
11-18 879
1.context 可以拿到属性 插槽 发射的自定义事件 <div id="app"> <lk-box style="width: 200px; height: 200px; background-color: red;" > </lk-box> </div> const app=Vue.createApp({ }); app.component('lk-
vue2和vue3之间的相互改造【区别】
m0_37755267的博客
07-14 1489
vue2和vue3之间的相互改造【区别】
vue3重构vue2项目
09-02
重构Vue2项目Vue3可以按照以下步骤进行: 1. 首先,创建一个新的Vue3项目。可以使用命令行或者Vue CLI来创建一个空的Vue3项目。 2. 接下来,将Vue2项目的目录与创建的Vue3项目的目录进行对比。可以查看两个项目的目录结构和文件,找出要迁移的文件和目录。 3. 逐步将Vue2项目的文件迁移到Vue3项目中。根据对比结果,修改不合适的地方,使其适配Vue3。可能要更改的地方包括路由配置、组件引用、语法等。 4. 如果原先的Vue2项目使用了一些Vue周边的库,要确保这些库在Vue3项目中也能正常使用。可以查看这些库的官方文档或社区讨论,了解它们是否已经适配了Vue3,并按照相应的指导进行配置和迁移。 总之,重构Vue2项目Vue3要创建一个新的Vue3项目,对比和迁移文件,并根据要修改适配的部分。同时,还确保原先使用Vue周边库在Vue3项目中能够正常工作。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* *3* [使用Vue3重构vue2项目](https://blog.csdn.net/qiwoo_weekly/article/details/109192667)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_1"}}] [.reference_item style="max-width: 100%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
写文章

热门文章

  • 都在推介 TS,但 TS 真的有必要上吗?来看看老司机怎么说 24102
  • 好用不卡,这些插件和配置让你的 Webstorm 更牛逼! 23202
  • React Hooks 原理与最佳实践 14404
  • 手把手教你看懂Chrome火焰图!(调试性能必备) 11724
  • 可视化大屏自适应:autofit.js 一行搞定布局 10728

分类专栏

  • git 1篇
  • webstorm 1篇
  • webpack 1篇
  • 前端下午茶 21篇
  • javascript 7篇
  • vue 3篇
  • css 1篇
  • GraphQL 2篇
  • 读书笔记 7篇
  • 浏览器 1篇
  • 年终总结 1篇
  • 好用的工具 3篇
  • 前端工具栈 3篇

最新评论

  • 都在推介 TS,但 TS 真的有必要上吗?来看看老司机怎么说

    我太有感觉了: 🤣时隔两年的回复,两年前我还是个小白

  • 都在推介 TS,但 TS 真的有必要上吗?来看看老司机怎么说

    今天也要努力写代码: 应该是>被转译成了>,不知道他文章怎么弄的

  • 我放弃 antd 的理由

    努力奋斗的小高: 可以看看antd vue的啊 我用下来 感觉还不错,但是存在pro版本功能 普通版没有的情况

  • 可视化大屏自适应:autofit.js 一行搞定布局

    就是不睡觉bug: 请问解决了吗? 我也是有相同疑问

  • 都在推介 TS,但 TS 真的有必要上吗?来看看老司机怎么说

    sinvon: 说的好有道理!!

您愿意向朋友推荐“博客详情页”吗?

  • 强烈不推荐
  • 不推荐
  • 一般般
  • 推荐
  • 强烈推荐
提交

最新文章

  • 单页面首屏优化,打包后大小减少64M,加载速度快了13.6秒
  • 苦尽甘来,下一站,上岸(船)
  • 面试官:sessionStorage 能在多个标签页之间共享数据吗?
2024年27篇
2023年88篇
2022年2篇
2021年43篇
2020年107篇
2019年70篇

目录

目录

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值

聚圣源韩国直播awesomenauts李治廷王力宏初中补习费用乔起名字公司名称变更还用原公司起诉纯真岁月传颂之物下载三个女生可以起什么名字火和木组成的字起名字女孩进击的巨人第四季16宝贝起名宝宝起名大全2016洋气大灌篮高清下载特朗普打算恢复对伊朗的制裁跑路彭姓胎儿起名大全金石圭漆黑的魅影二周目女孩起名有含义有字女生脱衣服起名网测名大全八字测名男孩练打字李宝宝起名称企鹅tvgoldwin官网装修起的名字好乐器店起名查起名称大全公司起名 的姓名测试淀粉肠小王子日销售额涨超10倍罗斯否认插足凯特王妃婚姻让美丽中国“从细节出发”清明节放假3天调休1天男孩疑遭霸凌 家长讨说法被踢出群国产伟哥去年销售近13亿网友建议重庆地铁不准乘客携带菜筐雅江山火三名扑火人员牺牲系谣言代拍被何赛飞拿着魔杖追着打月嫂回应掌掴婴儿是在赶虫子山西高速一大巴发生事故 已致13死高中生被打伤下体休学 邯郸通报李梦为奥运任务婉拒WNBA邀请19岁小伙救下5人后溺亡 多方发声王树国3次鞠躬告别西交大师生单亲妈妈陷入热恋 14岁儿子报警315晚会后胖东来又人满为患了倪萍分享减重40斤方法王楚钦登顶三项第一今日春分两大学生合买彩票中奖一人不认账张家界的山上“长”满了韩国人?周杰伦一审败诉网易房客欠租失踪 房东直发愁男子持台球杆殴打2名女店员被抓男子被猫抓伤后确诊“猫抓病”“重生之我在北大当嫡校长”槽头肉企业被曝光前生意红火男孩8年未见母亲被告知被遗忘恒大被罚41.75亿到底怎么缴网友洛杉矶偶遇贾玲杨倩无缘巴黎奥运张立群任西安交通大学校长黑马情侣提车了西双版纳热带植物园回应蜉蝣大爆发妈妈回应孩子在校撞护栏坠楼考生莫言也上北大硕士复试名单了韩国首次吊销离岗医生执照奥巴马现身唐宁街 黑色着装引猜测沈阳一轿车冲入人行道致3死2伤阿根廷将发行1万与2万面值的纸币外国人感慨凌晨的中国很安全男子被流浪猫绊倒 投喂者赔24万手机成瘾是影响睡眠质量重要因素春分“立蛋”成功率更高?胖东来员工每周单休无小长假“开封王婆”爆火:促成四五十对专家建议不必谈骨泥色变浙江一高校内汽车冲撞行人 多人受伤许家印被限制高消费

聚圣源 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化