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