前言
截止发文时间,vite正式版已经发布快2年时间了,vue3也发布到3.2版本了,它的周边设施基本上已经齐活了。也是时候再次重构下我那个vue3.0的开源项目了。
本篇文章就记录下我的重构过程,欢迎各位感兴趣的开发者阅读本文。
环境搭建
1年多前,我用Vue Cli 4.5构建的此项目,有关此项目的更多细节请移步我的另一篇文章使用Vue3重构Vue2项目。同样的,从CLI迁移到Vite仍然是在package.json中添加vite的依赖项,在项目中添加它的配置文件。
此次项目构建还加入了volta的相关配置,对此感兴趣的开发者请移步:强大的JavaScript工具管理器Volta
新增vite相关依赖项
我们打开package.json,找到devDependencies字段,移除CLI相关的依赖,添加vite相关的依赖,如下所示:
- +绿色标识代表新增
- -红色标识代表移除
{
"dependencies": {
- "compression-webpack-plugin": "^5.0.1",
},
"devDependencies": {
+ "@vitejs/plugin-vue": "^3.0.0",
+ "vite": "^3.0.0",
+ "vue-tsc": "^0.38.4",
+ "@types/node": "^18.6.3",
- "sass-loader": "^8.0.2",
- "@vue/cli-plugin-babel": "~4.5.0",
- "@vue/cli-plugin-eslint": "~4.5.0",
- "@vue/cli-plugin-router": "~4.5.0",
- "@vue/cli-plugin-typescript": "~4.5.0",
- "@vue/cli-plugin-vuex": "~4.5.0",
- "@vue/cli-service": "~4.5.0",
- "@vue/compiler-sfc": "^3.0.0-0"
-
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
随后,我们找到scripts字段,修改项目的运行与构建命令。
{
"scripts": {
"serve": "vite --open",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview"
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
vite3.x版本要求node版本必须大于14.18.0,因此我们需要在engines字段中做一下提示,如下所示:
{
"engines": {
"npm": "please-use-yarn",
"yarn": ">= 1.0.0",
"node": ">= 14.18.0"
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
除了上述配置外,我们还需要在项目的根目录创建.npmrc文件,写入下述内容:
engine-strict = true
- 1.
配置完成后,我们执行在终端执行yarn install安装依赖即可。
在上述配置中,我们还强制设置了yarn作为项目的包管理工具,如果项目开发成员使用了npm install则不会开始安装依赖并提示其使用yarn来安装依赖。
添加vite配置文件
在vite中,index.html已经从public文件夹迁移到项目的根目录下了,官方文档对此的解释为:在开发期间 Vite 是一个服务器,而 index.html 是该 Vite 项目的入口文件。
有关此变更的详细解释请移步:index.html 与项目根目录
接下来,我们在项目的根目录创建index.html文件(将public目录下的文件删除)
- 引入静态文件时不需要使用%PUBLIC_URL%作为占位符,可以直接写/来访问,vite会将其解析到public根目录下
- 通过<script type="module" src="...">标签直接指向Vue的入口文件(文件后缀可以为js或者ts)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>chat-system</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
注意:如果你的项目比较复杂,有多个入口,那么就将index.html文件放到对应入口的根目录下。
最后,我们创建vite.config.ts文件,配置代码如下所示:
- 设置开发环境的端口号
- 设置路径别名
- 设置打包后base地址以及打包输出目录
import { defineConfig } from "vite";
import { resolve } from "path";
import vue from "@vitejs/plugin-vue";
const IS_PRODUCTION = process.env.NODE_ENV === "production";
export default defineConfig({
plugins: [vue()],
server: {
host: true,
port: 8020,
proxy: {}
},
resolve: {
// 设置路径别名
alias: {
"@": resolve(__dirname, "./src"),
"*": resolve("")
}
},
base: IS_PRODUCTION ? "/chat-system" : "./",
define: {
"process.env": {}
},
build: {
outDir: resolve(__dirname, "dist")
}
});
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
- 27.
注意:我的项目配置比较简单,它只有一个入口,打包后只会部署到生产环境。如果你的项目较为复杂,也不必太过担心,你的应用场景vite也是支持的,按照文档进行相关的配置就好,如下所示:
- 自定义构建
- 多页面应用模式
- 环境变量和模式
当你的项目有多个入口时,期望通过不同命令来启动不同项目时,你可以使用yarn的--cwd指令来指定其运行时的工作目录。
例如:你有两个入口,那么就在src目录下创建两个文件夹:**A、B **。A和B中分别有自己的index.html、main.ts以及package.json文件(配置start、build命令,传入不同的参数来启动/构建不同入口的项目)
根目录的package.json中你就可以配置启动/构建命令为:
{
"scripts": {
"dev:A": "yarn --cwd ./src/A run start",
"dev:B": "yarn --cwd ./src/B run start",
"build:A": "yarn --cwd ./src/A run build",
"build:B": "yarn --cwd ./src/B run build",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview"
},
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
最后,我们以A入口为例,列举下package.json文件中的配置:
{
"name": "A",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"start": "vite serve --config ../../vite.config-A.ts --mode development",
"build": "vue-tsc --noEmit && vite build --config ../../vite.config-A.ts --mode production"
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
升级Vue周边依赖项
vue3.2的单文件组件引入了setup规范,它可以让代码变得更简洁,可以使用纯 TypeScript 声明 props 和抛出事件,有着更好的运行时性能。这些优点让我有了升级vue版本的动力,之前的3.0版本写起来很臃肿,需要return一大堆东西,甚是麻烦。
打开package.json文件作出下述变动:
- 更新了vue、router、vuex的版本号
- 新增了vueuse包,这是一个基于 Composition API 的实用函数集合,封装了一些常用的功能(实时获取鼠标位置、防抖、节流、获取客户端系统主题等),可以避免一些重复性的工作内容,大大提升开发效率。
{
"dependencies": {
- "vue": "^3.0.0-0",
- "vue-class-component": "^8.0.0-0"
- "vue-router": "^4.0.0-0",
- "vuex": "^4.0.0-0",
+ "vue": "^3.2.37",
+ "vue-router": "^4.1.3",
+ "vuex": "^4.0.2",
+ "@vueuse/components": "^8.9.2",
+ "@vueuse/core": "^8.9.2"
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
最后执行yarn install即可完成整个环境的搭建,本章节重构完成后的完整文件请移步:
- .npmrc
- index.html
- package.json
- vite.config.ts
经验分享
本章节就跟大家分享下,我切到新环境后做的一些优化点以及遇到的问题和解决方案。
本章节修改到的文件,完整文件代码如下:
- package.json
- tsconfig.json
require不存在
一切准备就绪后,按下了项目启动按钮,很快啊,651ms项目就启动了,不愧是vite速度就是快,嘴角疯狂上扬。
浏览器加载完项目后,我傻眼了,我的登陆界面呢🌚?顺势打开控制台,发现报错require is not defined。
解决方案
打开Login.vue文件后,发现我用require导入了一些图片文件,在VueCLI环境下的require会交给webpack处理。在vite中是不存在的,那么我们就需要查看vite是怎么处理静态文件了。
翻了下文档后,在静态资源处理章节发现他有两种处理方法:
- 通过import语句直接导入图片
- 通过new URL来导入图片
我打算将所有组件都重构为setup形式,因此直接使用import方式来导入图片可以保持组件的一致性,可以大大提升可读性。
我们写个简单的demo来尝试下,如下所示:
<template>
<img :src="loginUndo" alt="" />
</template>
<script lang="ts" setup>
import loginUndo from "@/assets/img/login/LoginWindow_BigDefaultHeadImage@2x.png";
</script>
<style scoped></style>
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
已经可以正确解析出图片的路径了。
注意:本文不会过多讲解setup的语法,对此不了解的开发者请移步:单文件组件 - script setup
new URL方式可以用来引入一个动态资源,例如:你有一份json配置文件,里面描述了图片的文件名,这些图片是放在项目中的,他们的访问前缀都一样,此时你就可以通过遍历json文件通过此方式来引入这些图片。
vue相关模块不存在
我试图从vue的包中导入shallowRef时,编辑器报错: TS2305: Module 'xxx' has no exported member 'shallowRef'. 。
解决方案
经过一番排查后,是因为项目typescript版本是3.x,跟3.2版本的vue不兼容,需要将其升级至4.x版本。
打开package.json文件,作出如下所示的修改,重新执行yarn install命令即可。
{
"devDependencies": {
- "typescript": "~3.9.3",
+ "typescript": "~4.7.4",
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
setup中的变量警告未被使用
当我在setup中声明了一个函数或者导入了一个文件,在template中已经使用了,但是他却报错ESLint: 'xx' is assigned a value but never used.(@typescript-eslint/no-unused-vars)
解决方案
在 eslint-plugin-vue 插件的Issues中看到有人遇到了跟我同样的问题,在v9.0.0: regression in unused variables in script setup中我找到了解决方案。
我们需要升级下@vue/eslint-config-typescript和eslint-plugin-vue的版本号,如下所示:
{
"devDependencies": {
"@vue/eslint-config-typescript": "^11.0.0",
"eslint-plugin-vue": "^9.0.0"
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
随后在eslint的配置文件中,添加parser属性,重新执行yarn install命令即可。
module.exports = {
+ parser: 'vue-eslint-parser'
}
- 1.
- 2.
- 3.
模块隔离
Vite 使用 esbuild 来转译 TypeScript,并受限于单文件转译的限制,因此需要在ts的配置文件中将isolatedModules属性设置为true。
{
"compilerOptions": {
"isolatedModules": true
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
process不存在
在路由配置文件中,我们需要从process中获取BASE_URL,此时编辑器报错: TS2591: Cannot find name 'process'. Do you need to install type definitions for node? Try npm i --save-dev @types/node and then add 'node' to the types field in your tsconfig.
解决方案
由于vite中已经没有process了,需要用import.meta来代替,那么上述的路由配置文件就应该改为:
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL), // 地址栏不带#
routes
});
- 1.
- 2.
- 3.
- 4.
无法导入json文件
在表情面板模块,我将每个表情都放入了json文件中。在vite中引入文件需要使用import,改了写法后,发现它报错:Cannot find module 'xx.json'. Consider using '--resolveJsonModule' to import module with '.json' extension.
解决方案
我们需要在ts的配置文件中添加resolveJsonModule属性,如下所示:
{
"compilerOptions": {
+ "resolveJsonModule": true
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
使用vite提供的对象
当我想使用vite所提供的glob属性时,发现编辑器报错: TS2339: Property 'glob' does not exist on type 'ImportMeta'.
解决方案也很简单,我们只需要在ts的配置文件中添加vite/client即可,如下所示:
{
"compilerOptions": {
"types": [
+ "vite/client"
]
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
获取全局属性
当我们使用一些第三方库的时候它会在globalProperties挂载一些方法,当在ts+setup环境下使用时,会出现类型无法推导问题,如下所示:
第三方库提供了一个$connect方法
我们通过proxy来访问
<script lang="ts" setup>
import {
getCurrentInstance,
onMounted,
ComponentInternalInstance
} from "vue";
onMounted(() => {
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
proxy.$connect();
})
</script>
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
他会出现报错: TS2339: Property 'xx' does not exist on type 'ComponentPublicInstance{}, {}, {}, {}, {}, {}, {}, {}, false, ComponentOptionsBase >'.
解决方案
我们可以在type目录下新建一个global文件夹,在这里存放一些我们扩展出来的全局方法。
如下所示,我们:
- 创建了一个useCurrentInstance方法
- 将globalProperties属性暴露出去
import { ComponentInternalInstance, getCurrentInstance } from "vue";
export default function useCurrentInstance() {
const { appContext } = getCurrentInstance() as ComponentInternalInstance;
const proxy = appContext.config.globalProperties;
return {
proxy
};
}
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
我们在组件中使用暴露出来的proxy即可,如下所示:
<script lang="ts" setup>
import useCurrentInstance from "@/type/global/UseCurrentInstance";
onMounted(() => {
const { proxy } = useCurrentInstance();
proxy.$connect();
})
</script>
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
无法识别NodeJS类型
我们在给setinterval和setTimeout指定类型时,会用到NodeJS模块,会出现报错:ESLint: 'NodeJS' is not defined.(no-undef)。
这个问题的解决方案是:打开eslint的配置文件在globals对象中添加NodeJS选项,如下所示:
{
globals: {
NodeJS: true
}
}
- 1.
- 2.
- 3.
- 4.
- 5.
除了将类型声明为NodeJS.Timeout外,我们还可以将其声明为number类型,但是需要携带window前缀(window.setinterval/window.setTimeout)
管理静态资源
当我们在组件中使用import导入很多静态资源时,组件看起来会很杂乱。此时我们可以将其按照功能类型进行拆分。我的做法如下:
- 在src下创建resource文件夹
- 根据功能类型创建ts文件,将其导出
import defaultAvatar from "@/assets/img/login/LoginWindow_BigDefaultHeadImage@2x.png";
import defaultLoginBtnIcon from "@/assets/img/login/icon-enter-undo@2x.png";
import loginUndo from "@/assets/img/login/icon-enter-undo@2x.png";
import loginBtnHover from "@/assets/img/login/icon-enter-hover@2x.png";
import loginBtnDown from "@/assets/img/login/icon-enter-down@2x.png";
export {
defaultAvatar,
defaultLoginBtnIcon,
loginUndo,
loginBtnHover,
loginBtnDown
};
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
分离模版与逻辑代码
我的项目中有一个很复杂的组件,有上千行代码,去年我用CompositionAPI优化了一版,将组件中所有的方法都拆分成了一个个独立的ts文件,做到了逻辑代码与模版代码分离,模版需要什么方法我就通过import导入进来,最后return给模版。
在拆分出来的文件中,是没有办法访问vue提供的一些内置属性的,比如:defineProps、defineEmits、getCurrentInstance。因此我想了一个奇妙的方法:将这些无法访问的属性都存起来。具体的做法请移步我另一篇文章:使用Vue3的CompositionAPI来优化代码量-创建InitData.ts文件
适配方案
vue3.2的setup语法糖支持import进来的方法都能在模版中直接使用,那我们的组件又可以精简下了,我花了亿点点时间对其进行了适配。
之前我们想获取组件的emit需要从context中拿,props声明并从setup函数的参数中获取,如下所示:
<script>
export default defineComponent({
name: "message-display",
props: {
listId: String, // 消息id
messageStatus: Number, // 消息类型
buddyId: String, // 好友id
buddyName: String, // 好友昵称
serverTime: String // 服务器时间
},
setup(props, context) {
// 访问emit
context.emit
}
})
</script>
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
现在我们就不用这么麻烦了,直接通过defineProps、defineEmits获取即可,如下所示:
<script lang="ts" setup>
// 获取父组件传递值
const props = defineProps<{
listId: string; // 消息id
messageStatus: number; // 消息类型
buddyId: string; // 好友id
buddyName: string; // 好友昵称
serverTime: string; // 服务器时间
}>();
const emit = defineEmits<{
(
e: "update-last-message",
msgObj: {
text: string;
id: string;
time: string;
}
): void;
}>();
// 事件监听函数,传入props和emit将其存储到initData中
const { userID, onlineUsers } = eventMonitoring(props, emit) as {
userID: ComputedRef<string>;
onlineUsers: ComputedRef<number>;
};
</script>
- 1.
- 2.
- 3.
- 4.
- 5.
- 6.
- 7.
- 8.
- 9.
- 10.
- 11.
- 12.
- 13.
- 14.
- 15.
- 16.
- 17.
- 18.
- 19.
- 20.
- 21.
- 22.
- 23.
- 24.
- 25.
- 26.
此组件重构后的完整代码请移步:
- message-display.vue
- EventMonitoring.ts
项目地址
QQw">至此,项目的重构工作就结束了。本文重构好的项目代码地址: