前端工程记录: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

相关推荐
xiao-xiang3 分钟前
jenkins-通过api获取所有job及最新build信息
前端·servlet·jenkins
C语言魔术师20 分钟前
【小游戏篇】三子棋游戏
前端·算法·游戏
匹马夕阳2 小时前
Vue 3中导航守卫(Navigation Guard)结合Axios实现token认证机制
前端·javascript·vue.js
你熬夜了吗?2 小时前
日历热力图,月度数据可视化图表(日活跃图、格子图)vue组件
前端·vue.js·信息可视化
桂月二二8 小时前
探索前端开发中的 Web Vitals —— 提升用户体验的关键技术
前端·ux
沈梦研9 小时前
【Vscode】Vscode不能执行vue脚本的原因及解决方法
ide·vue.js·vscode
hunter2062069 小时前
ubuntu向一个pc主机通过web发送数据,pc端通过工具直接查看收到的数据
linux·前端·ubuntu
qzhqbb9 小时前
web服务器 网站部署的架构
服务器·前端·架构
刻刻帝的海角9 小时前
CSS 颜色
前端·css
轻口味9 小时前
Vue.js 组件之间的通信模式
vue.js