当前位置: 首页 > article >正文

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


http://www.kler.cn/a/324997.html

相关文章:

  • 生成式语言模型 三范式 预训练、微调、强化反馈学习
  • Vue3中使用Axios构建高效的请求处理机制
  • 【Vue笔记】基于vue3 + element-plus + el-dialog封装一个自定义的dialog弹出窗口组件
  • 第十六届蓝桥杯模拟赛(第一期)-c++/c
  • 百度智能云 VectorDB 优势数量 TOP 1
  • 网页作业9
  • ppt压缩有什么简单方法?压缩PPT文件的几种方法
  • Qt_对话框QDialog的介绍
  • Docker搭建 RabbitMQ 最新版
  • 作业报告┭┮﹏┭┮(Android反调试)
  • Linux安装go-fastdfs
  • 编译安装的 Nginx 设置为服务启动
  • 基于mediapipe深度学习算法的手势数字0-9识别系统python源码+精美GUI界面
  • 9-pg内核之锁管理器(四)常规锁
  • [前端]DOM+CSS+HTML实现水波进度效果
  • SpringBoot 流式输出时,正常输出后为何突然报错?
  • 使用Prometheus进行系统监控,包括Mysql、Redis,并使用Grafana图形化表示
  • JVM和GC监控技术
  • 前端html+css+js 基础总结
  • Ubuntu24.04 yum安装
  • Android问题笔记五十:构建错误-AAPT2 aapt2-7.0.2-7396180-windows Daemon
  • “滑动窗口”思想在算法里面的应用
  • llama3.1 8b instruct的function calling的template解析
  • C++第五讲(1):STL--string--各个函数的使用方法
  • 在线支付系统
  • 无人机之模拟图传篇