實戰教學使用 Vue3 重構 Vue2 項目(萬字好文推薦)

2021-02-15 圖雀社區

本文來自於 神奇的程式設計師 的分享好文,點擊閱讀原文查看作者的掘金鍊接。感謝作者的文章✌️

前言

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,如果編輯器是其他的可跳過本部分。

我們在項目中集成了eslint和prettier,默認情況下webstorm是沒有啟用這兩個東西的,需要我們自己手動開啟。

打開webstorm的配置菜單,如下所示

image-20201006153458084

搜索eslint,按照下圖所示進行配置,配置完成後點APPLY、OK即可。

image-20201006153031544

搜索prettier,按照下圖所示進行配置,配置完成後點APPLY、OK即可。

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-20201015223425458image-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'導出時丟棄之前的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改為了installObject.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,將組件中用到的類型指定放在其中。

我們先來看看第一點,將組件內用到的類型進行統一管理,我們以登錄組件為例,我們需要為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

再然後,我們看看第二點,使用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;
    }
  }
})

上述代碼完整地址:

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-20201013171712449Dom對象類型定義

當操作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>
<div>
<p>好友</p>
</div>
<div v-for="(item,index) in friendsList" :key="index">
<div @click="groupingStatus(index)">
<div>
<img ref="groupArrow" src="../assets/img/list/tchat_his_arrow_right@2x.png" alt="左箭頭"/>
</div>
<div>
<p>{{item.groupName}}</p>
</div>
<div>
<p>{{item.onlineUsers}}/{{item.totalPeople}}</p>
</div>
</div>
<!--好友列表-->
<div ref="buddyList" style="display:none">
<div v-for="(list,index) in item.friendsData" :key="index" tabindex="0">
<div @click="getBuddyInfo(list.userId)">
<div>
<img :src="list.avatarSrc" alt="用戶頭像">
</div>
<div>
<!--暱稱-->
<div>
{{list.username}}
</div>
<!--籤名-->
<div>
[{{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 :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(https://github.com/likaia/chat-system/)

匯聚精彩的免費實戰教程

關注公眾號回復 z 拉學習交流群

相關焦點

  • Vue.js 實戰高清 pdf
    本書以Vue.js 2為基礎,以項目實戰的方式來引導讀者漸進式學習Vue.js。本書分為基礎篇、進階篇和實戰篇三部分。通過閱讀本書,讀者能夠掌握Vue.js框架主要API的使用方法、自定義指令、組件開發、單文件組件、Render函數、使用webpack開發可復用的單頁面富應用等。
  • 【Vue.js入門到實戰教程】11-Vue Loader(下)| 編寫一個單文件 Vue 組件
    編寫 ModalExample 組件我們將 vue_learning/component/slot.html 中的 modal-example 組件拆分出來,在 vue_learning/demo-project/src/components 目錄下新建一個單文件組件 ModalExample.vue,將 modal-example 組件代碼按照 Vue Loader 指定的格式填充到對應位置
  • Vue進階篇: vue-loader
    基礎篇學習請參照:從「零」開始玩轉Vue2.x+綜合大項目實戰筆記請直接進入公眾號菜單欄查看即可。一、你必須先認識下Webpack?在前面學習的NodeJS中,我們可以通過require、exports去加載各種資源和模塊,但是在前端,我們並不能直接去做,所以我們得藉助模塊加載器。
  • Vue入門10 vue+elementUI
    十二、實戰快速上手我們採用實戰教學模式並結合ElementUI組件庫,將所需知識點應用到實際中,以最快速度帶領大家掌握Vue的使用;
  • 【項目推薦】Vue.js
    作者是尤雨溪,寫下這篇文章時 vue.js版本為 1.0.7 。我推薦使用 sublime text 作為編輯器,關於這個編輯器可以看我這篇文章。在 package control中安裝Vuejs SnippetsVue Syntax Highlight推薦使用 npm 管理,新建兩個文件 app.html,app.js,為了美觀使用 bootstrap,我們的頁面模板看起來是這樣:<!
  • Vue項目實戰(八)渲染一個列表
    vue渲染一個列表,公眾號已經準備了vue實戰教程,如果您有需要,可以在公眾號回復「vue」獲取。在第三篇博文中,我們規劃了我們的項目文件結構,當時保留了一個 components 的空文件夾。這裡,就是準備放我們的自定義組件的。首先,我們去創建兩個空文本文件,分別是 header.vue 文件和 footer.vue 文件。
  • Vue入門-實戰教程
    p=285Vue2+VueRouter2+Webpack+Axios 構建項目實戰(二)配置環境及構建初始項目:http://www.javazhiyin.com/?p=287Vue2+VueRouter2+Webpack+Axios 構建項目實戰(三)認識項目所有文件:http://www.javazhiyin.com/?
  • 你知道vue項目怎麼使用TypeScript嗎?
    ③ 代碼提示:ts 搭配 vscode,代碼提示非常友好④代碼重構:例如全項目更改某個變量名(也可以是類名、方法名,甚至是文件名[重命名文件自動修改的是整個項目的import]),在JS中是不可能的,而TS可以輕鬆做到。
  • 不要再用Vue 2的思維寫Vue 3了
    我剛從Vue2轉到Vue3時,代碼都嚴格的遵循Compostion API寫法,但是發現比Option API寫法維護性更差。踩過的坑1. 按技術類型劃分代碼在日常開發中,前端一般會收到交互稿或設計稿後開始布局,然後編寫邏輯代碼。
  • Vue-使用vue-video-player組件
    在實際開發過程中會有添加視頻的需求在vue項目中添加視頻可以使用vue-video-player組件來實現實現步驟:1.安裝在控制臺輸入: npm install vue-video-player –s
  • 前端開源實戰項目推薦
    66套java從入門到精通實戰課程分享前言這段時間一直有學員和一些正在從事前端開發工作的朋友詢問「有沒有推薦的前端開源項目?」,因為一直忙於工作沒有時間去整理,今天應各位的請求,我整理了一些開源項目 。推薦順序與項目的好壞無關,框架的推薦順序就大家詢問的比例來分,跟當前市場框架的佔有率無關,所以大家不要先入為主的認為我列在前面的可能就是好的。話不多說,我們進入正題。
  • 在VUE2.0中使用PostCSS
    當我們使用 vue-cli (最新版)創建好 vue 項目時,在 build/webpack.base.conf.js 中可以看到 vue-loader 的中已經默認加入了 autoprefixer 的配置,如下圖所示:
  • 如何在vue框架項目中使用echarts並製作柱狀圖
    cnpm install --global vue-cli2、接著使用命令:vue init webpack wanm,創建一個基於webpack模板的新項目vue init webpack wanm3、切換工作目錄cd wanm/,然後運行項目:npm run dev
  • 12 種使用 Vue 的優秀做法
    2.在事件中使用短橫線命名在發出定製事件時,最好使用短橫線命名,這是因為在父組件中,我們使用相同的語法來偵聽該事件。3.使用駝峰式聲明 props,並在模板中使用短橫線命名來訪問 props優秀的做法只是遵循每種語言的約定。在 JS 中,駝峰式聲明是標準,在HTML中,是短橫線命名。
  • 基於Vue實現一個有點意思的拼拼樂小遊戲
    本文轉載自【微信公眾號:趣談前端,ID:beautifulFront】經微信公眾號授權轉載,如需轉載與原文作者聯繫前言為了加深大家對vue的了解和vue項目實戰,筆者採用vue生態來重構此項目,方便大家學習和探索。
  • 什麼是Vue? 如何安裝和使用Vue?
    2.框架和庫的區別?框架:是一套完整的解決方案;對項目的`侵入性`較大,項目如果需要更換框架,則需要重構整個項目。庫(插件):提供某一個小功能,對項目的`侵入性`較小,如果某個庫無法完成某些需求,可以很容易切換到其它庫實現需求。
  • 什麼是vue?在項目開發中為什麼要用vue?
    在近兩年的web及項目開發中,vue技術的使用越來越普遍,其各種資料、介紹以及使用攻略內容資料非常多,那麼vue到底什麼?在項目開發中,vue起到什麼作用?它與傳統的html+css+js+lamp開發網站項目又有什麼區別呢?
  • Vue的安裝及使用快速入門
    vue是一個JavaMVVM庫,是一套用於構建用戶界面的漸進式框架,是初創項目的首選前端框架。它是以數據驅動和組件化的思想構建的,採用自底向上增量開發的設計。它是輕量級的,它有很多獨立的功能或庫,我們會根據我們的項目來選用vue的一些功能。它提供了更加簡潔、更易於理解的API,使得我們能夠快速地上手並使用Vue.js。
  • Vue.js系列之vue-router(上)(3)
    說明:我們項目現在用的是:vue2.0 + vue-cli + webpack + vue-router2.0 + vue-resource1.0.3
  • Vue知識點總結(24)——使用VueCli創建一個項目(超級詳細)
    今天我們來試著使用VueCli3完整的創建一個項目。首先我們打開終端,在合適的目錄下運行以下命令:(前提是你已經完整了上篇文章對於Vue-Cli3的基本配置)vue create hellovuecli3這個hellovuecli3是文件名,文件名大家可以自擬,但是注意,不要有大寫字母的出現,這裡是不支持大寫的。