前端工程记录:Vue2 typescript项目升级Vue3

由于typescript飞速发展,某些vue2项目也在vue3出现之前集成了typescript开发,例如我的个人网站,当时花费了不少时间。而vue3我使用一段时间后,在2022年左右开始投入生产,但是这个个站就没怎么维护了。若是想继续,升级是无法避开,毕竟vue2也不怎么熟悉了

1 依赖升级

1.1 老项目依赖 - vuex-module-decorators+vue-property-decorator

老项目使用装饰器包vue-property-decorator实现vue组件的ts支持,使用vuex-module-decorators实现vuex状态管理的支持。

  "scripts": {
    "dev": "vue-cli-service serve",
    "build": "vue-cli-service build",
    "lint": "vue-cli-service lint"
  },
  "dependencies": {
    "lz-string": "~1.4.4",
    "tslib": "~2.1.0",
    "vue": "2.6.10",
    "vue-router": "~3.5.1",
    "vuex": "~3.6.2"
  },
  "devDependencies": {
    "@types/js-cookie": "3.0.6",
    "@types/lz-string": "^1.3.33",
    "@vue/cli-plugin-typescript": "~3.11.0",
    "@vue/cli-service": "~3.11.0",
    "babylonjs": "~4.1.0",
    "js-cookie": "~2.2.1",
    "sass": "~1.18.0",
    "sass-loader": "~7.1.0",
    "ts-loader": "~6.2.1",
    "typescript": "~3.7.2",
    "vue-meta": "~2.3.3",
    "vue-property-decorator": "8.5.1",
    "vue-template-compiler": "2.6.10",
    "vuex-module-decorators": "^0.11.0",
    "webpack": "4.47.0"
  },

1.2 新项目ts依赖

  "scripts": {
    "dev": "vite --port 5174",
    "build": "vue-tsc && vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "babylonjs": "~4.1.0",
    "dayjs": "^1.11.11",
    "lz-string": "^1.5.0",
    "vue": "^3.3.4",
    "vue-router": "^4.2.5"
  },
  "devDependencies": {
    "@types/node": "^20.14.9",
    "@vitejs/plugin-vue": "^4.2.3",
    "@vitest/ui": "^1.6.0",
    "@vue/test-utils": "^2.4.6",
    "autoprefixer": "^10.4.19",
    "jsdom": "^24.1.0",
    "postcss": "^8.4.39",
    "sass": "^1.69.5",
    "sass-loader": "^14.2.1",
    "tailwindcss": "^3.4.4",
    "typescript": "^5.0.2",
    "vite": "^4.4.5",
    "vitest": "^1.6.0",
    "vue-tsc": "^1.8.5"
  }

1.3 升级思路

1.3.1 vue3直接支持typescript,所以去掉了装饰器式的组件定义
1.3.2 去掉vuex依赖

实际上,因为状态比较简单,直接使用vue3的reactive定义响应式状态属性state,其它的方法从actions里面提取出来,这样改动很小,每个compute属性返回一个vue3 computed的计算属性

如果是比较复杂的项目,可以考虑前期这样,后面替换成pinia,实际上我个人是不推荐使用pinia的,除非有kpi需求等

1.3.3 修改配置

这个是必须的,主要是webpack和vite配置的升级

1.3.4 修改组件代码

这个是工作量最大的,下面会讲一些注意点

1.3.5 创建vue3项目将老代码和依赖移过去(推荐)

这是我实践且推荐的方法,注意目录结构不要变,一点点的移过去会更稳

2 webpack升级到vite的配置

比较详细的官方会有,这里只是讲一些关键点

2.1 新增vite配置文件

新增vite.config.ts,配置如下(如果是采用1.3.5的推荐方法可跳过)

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";
import path from "path";
// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  build: {
    rollupOptions: {
      //
    },
  },
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "./src"),
    },
  },
  server: {
    proxy: {
     
    },
  },
});

2.2 更新外部依赖配置externals

很多时候,我们会把比较大的包或常用的包通过url引入,这时候就需要修改配置,例如vue.config.js的配置如下

const path = require('path');

module.exports = {
  chainWebpack: (config) => {
    config.externals({
      vue: 'Vue',
    });
  },
};

vue3则需要安装依赖

yarn add vite-plugin-external -D

vite.config.ts中需要使用插件:

import { defineConfig } from 'vite';
import createExternal from 'vite-plugin-external';
export default defineConfig({
  plugins: [vue(),
            createExternal({
              externals: {
                 vue: 'Vue'
              }
            })
 ],
});

2.3 更新代理服务器配置

vue.config.js的配置如下:

module.exports = {
  
  devServer: {
    port: 8080,
    proxy: {
      '^/api': {
        target: 'https://api.xxx.fun',
        // ws: true,
        changeOrigin: true,
        pathRewrite: {
          '^/api': 'aaa',
        },
        onProxyReq: function (request) {
          request.setHeader('origin', 'https://www.xxx.fun');
        },
      },
    },
  },
};

在vite.config.ts中则对应:

import { defineConfig } from "vite";
import vue from "@vitejs/plugin-vue";

export default defineConfig({
  server: {
    proxy: {
      "^/api": {
        target: "https://api.xxx.fun",
        // ws: true,
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, "/xxx"),
        headers: {
          Origin: "https://www.xxx.fun",
        },
      },
    },
  },
});

可以较大的变化就是头的修改和pathrewrite了,更多详细信息见:开发服务器选项 | Vite 官方中文文档

2.3 入口html文件调整

2.3.1 将index.html从public移出至更目录
2.3.2 在body最下面新增es入口

即 <script type="module" src="/src/main.ts"></script>

2.3.3 去掉以前的baseurl配置

vue2项目支持的html模板语法<%= BASE_URL %>vite下不再默认支持,去掉即可,然后修改index.html文件即可

3 常用优化策略迁移

3.1 摇树优化treeshake默认支持

由于rollup默认支持treeshake,所以可以去掉vue2的相关配置,也就是package.json中的sideEffects字段

3.2 分包优化的调整-指定分包

在vue2中分包方式和vue3没变化,都是使用import函数,但是有一点区别:

  • vue2中未命名分包会进入chunk,vue3会是一个单独的文件

  • vue2命名分包可使用下面的方式,vue3不生效需要删除注释

    import(/* webpackChunkName: "aaa" */ './AAA.vue')

  • vue3中在vite.config.ts中配置:

    export default defineConfig({
    plugins: [vue({})],
    build: {
    rollupOptions: {
    output:{
    manualChunks(id) {
    if (id.includes("AAA.vue")) {
    return 'aaa'
    }
    }
    }
    },
    },
    });

3.3 小文件引入

vue2支持用require引入文件,vue3也支持使用file-loader,所以变动不大。

4 组件和状态迁移

4.1 组件代码迁移

关键步骤就是:

  • script 新增 setup
  • 去掉class和decorator
  • Prop定义使用defineProps
  • state定义使用reactive
  • compute使用computed
  • slot使用v-slot,子组件使用时用<template></template>包括实现slot的插入使用

例如一个多语言组件

<script lang="ts">
import { Component, Vue, Prop } from 'vue-property-decorator';
import { ClientModule } from '@/store/client';
import { parseLangText } from '@/utils/basic';
@Component({ name: 'lang' })
export default class extends Vue {
  @Prop({
    required: true
  })
  private val: any;
  protected render(h: any) {
    const lang = ClientModule.lang;
    const [txt, fl] = parseLangText(this.val, lang);
    return h(
      'span',
      { class: fl === 'en' ? 'english' : 'chinese', ...this.$props },
      txt
    );
  }
}
</script>

可以转化为

<script lang="ts" setup>
import { computed } from "vue";
import { langModule } from "../../modules";

const mod = langModule();
const props = defineProps<{
  val: any;
}>();

const text = computed(() => {
  return mod.parseText(props.val);
});

const classList = computed(() => {
  return mod.lang === "en" ? "english" : "chinese";
});
</script>
<template>
  <span :class="classList"> {{ text }} </span>
</template>

4.2 vuex store迁移

关键点就是:

  • 用类或者一个js对象代替store,state用reactive定义实现响应式
  • 计算属性使用computed替代
  • action/mutation都定义为一个对象方法

例如 一个简单的store

import {
  VuexModule,
  Module,
  Mutation,
  Action,
  getModule
} from 'vuex-module-decorators';
import store from '../';
import { IMedia } from '@/types/media';
import Vue from 'vue';

export interface IPlayerState {
  audioList: IMedia[];
}

@Module({ dynamic: true, store, namespaced: true, name: 'player' })
class Player extends VuexModule implements IPlayerState {
  public audioList: IMedia[] = [];
  public target: IMedia | null = null;
  public get audioPlay() {
    return this.audioList.find(ele => ele.playing);
  }
  @Mutation
  public play() {
    if (this.target) {
      Vue.set(this.target, 'playing', true);
    }
  }
  @Mutation
  public setTarget(e: IMedia | null) {
    if (this.target && e && this.target.src === e.src) {
      return;
    }
    if (e !== null) {
      e.playing = false;
    }
    this.target = e;
  }
  @Mutation
  public stop() {
    if (this.target) {
      Vue.set(this.target, 'playing', false);
    }
  }
}

export const PlayerModule = getModule(Player);

可以转化成

import { IMedia } from "../../types";
import { reactive,computed} from "vue";

type MediaPlayerInfo = {
  audioList: IMedia[];
  target: IMedia | null;
};

export class PlayerManager {
  static initialState() {
    return {
      audioList: [],
      target: null,
    };
  }
  static build(
    stateBuilder: (s: MediaPlayerInfo) => ObjectState<MediaPlayerInfo>
  ) {
    return new PlayerManager(this.initialState());
  }
public readonly state:MediaPlayerInfo
  constructor(state: MediaPlayerInfo) {
this.state=reactive(state)
}
  audioPlaying() {
    return computed(()=>this.state["audioList"].find((ele) => ele.playing));
  }
  target() {
    return computed(()=>this.state.target);
  }
  public play() {
    const target = this.state.target;
    if (target) {
      target.playing = true;
      target.playing = true;
    }
  }
  public setTarget(e: IMedia | null) {
    const target = this.state.target;
    if (target && e && target.src === e.src) {
      return;
    }
    if (e !== null) {
      e.playing = false;
    }
    this.state.target= e;
  }
  public stop() {
    const target = this.state.target;
    if (target) {
      target.playing = false;
    }
  }
}

4.3 迁移策略

4.3.1 从页面组件或store模块作为一次任务

避免漏掉,一个个的完成

4.3.2 从最细粒度的开始迁移

也就是页面所用到的最小组件开始,这样可以避免过多的报错导致代码工具或者提示不可以

4.3.3 多commit代码

哪怕没完成,也不要在未commit的时候撤销等待,避免浪费工作量

5 版本管理

5.1 可在新分支新目录下存放全部的代码

这样的好处是merge等不会出现冲突

5.2 老版本核心依赖版本,用~而不是^

例如vue2/vue-router/vuex,锁定小版本,写固定版本最好。这样的好处就是不用担心老项目出现大的变化,vue2有些版本还是会出现breaking change的,这也是我对vue比较揪心的。例如vue2.7就让一些slot不可以了。

希望看完这篇文章对你有所帮助,写了一个小时,也该休息了。看完的你也是,如果有什么好的心得和补充,欢迎留言~

2cy

YU.H

相关推荐
小马哥编程24 分钟前
原型链(Prototype Chain)入门
css·vue.js·chrome·node.js·原型模式·chrome devtools
cwj&xyp1 小时前
Python(二)str、list、tuple、dict、set
前端·python·算法
dlnu20152506221 小时前
ssr实现方案
前端·javascript·ssr
古木20191 小时前
前端面试宝典
前端·面试·职场和发展
轻口味3 小时前
命名空间与模块化概述
开发语言·前端·javascript
前端小小王3 小时前
React Hooks
前端·javascript·react.js
迷途小码农零零发4 小时前
react中使用ResizeObserver来观察元素的size变化
前端·javascript·react.js
娃哈哈哈哈呀4 小时前
vue中的css深度选择器v-deep 配合!important
前端·css·vue.js
旭东怪4 小时前
EasyPoi 使用$fe:模板语法生成Word动态行
java·前端·word