由于typescript飞速发展,某些vue2项目也在vue3出现之前集成了typescript开发,例如我的个人网站,当时花费了不少时间。而vue3我使用一段时间后,在2022年左右开始投入生产,但是这个个站就没怎么维护了。若是想继续,升级是无法避开,毕竟vue2也不怎么熟悉了
如果vue2项目是js编写的,升级参考:跟随通义灵码一步步升级vue2(js)项目到vue3版本,本文主要讲如何将vue2的typescript项目升级成vue3,并提供一个prompt提示词,教你如何利用通义灵码快速完成组件转写
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>
推荐使用通义灵码来转写,提示词:将次vue2组件,转化成 vue3 SFC + typescript + setup 组件
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; } } }
推荐使用通义灵码来辅助完成,提示词:将此vue2-vuex store,转化成typescript服务,状态部分使用vue3的reactive进行管理
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不可以了。
5.3 用好通义灵码,节省90%转写时间
如果不适应代码辅助,手动一行行的还是很累的事情,尤其是类型定义等。使用工具,绝对提速10倍,除了部分配置需要手动,以及对生成代码的微调。