Vite 开发环境中路由切换导致页面刷新问题
问题描述
在使用 Vite 开发环境时,当点击菜单切换路由时,页面会意外刷新。控制台日志显示如下信息:
09:26:57 [vite] ✨ new dependencies optimized: @yto/custom/es/components/water-mark/index.mjs 09:26:57 [vite] ✨ optimized dependencies changed. reloading 09:27:06 [vite] ✨ new dependencies optimized: @yto/custom/es/components/sticky-container/index.mjs 09:27:06 [vite] ✨ optimized dependencies changed. reloading 09:27:15 [vite] ✨ new dependencies optimized: @yto/custom/es/components/tabs/index.mjs, @yto/custom/es/components/empty/index.mjs 09:27:15 [vite] ✨ optimized dependencies changed. reloading
这表明每次访问使用新组件的页面时,Vite 都会进行依赖预构建并刷新页面,严重影响开发体验。
问题原因
这个问题的根本原因在于 Vite 的开发服务器工作机制:
-
按需编译模式:Vite 在开发环境中采用无打包模式,使用浏览器原生 ES 模块导入功能。
-
依赖预构建:当首次遇到未预构建的依赖时,Vite 会暂停当前请求,对依赖进行预构建,然后刷新页面。
-
组件按需加载:当路由切换到使用新组件的页面时,如果该组件尚未被预构建,就会触发上述过程。
即使 @yto/custom/es/components/
下的组件已经是 .mjs
格式(ES 模块格式),Vite 仍然需要对它们进行预构建,原因包括:
- 依赖扁平化和请求合并:将多个小模块合并,减少 HTTP 请求
- 浏览器缓存优化:生成稳定的内容哈希,便于缓存
- 兼容性转换:确保代码在目标浏览器中正常运行
- 路径重写和别名解析:处理模块路径和别名
- 依赖图分析:分析整个依赖图以确保所有必要的模块都被正确加载
- 一致性保证:确保所有依赖以一致的方式被处理和加载
解决方案
方案一:使用 optimizeDeps.include 预构建所有可能用到的组件
在 vite.config.ts
中配置:
ts
optimizeDeps: {
include: [
'@yto/custom/es/components/**/*', // 这将包含所有子组件
'vue',
'@vue/shared',
],
},
这告诉 Vite 在开发服务器启动时就预构建所有这些组件,而不是等到它们被实际使用时才进行预构建。
方案二:全局注册常用组件
对于频繁使用的组件,可以考虑在应用启动时全局注册:
ts
// main.ts
import YtoWaterMark from '@yto/custom/es/components/water-mark'
import YtoLayoutRouter from '@yto/custom/es/components/layout-router'
const app = createApp(App)
app.component('YtoWaterMark', YtoWaterMark)
app.component('YtoLayoutRouter', YtoLayoutRouter)
方案三:创建自定义插件扫描并添加依赖
ts
import fs from 'fs';
import path from 'path';
// 创建一个插件来扫描并添加所有 @yto/custom 组件
function ytoCustomPreloadPlugin() {
return {
name: 'yto-custom-preload',
config(config) {
const componentsDir = path.resolve('./node_modules/@yto/custom/es/components');
if (fs.existsSync(componentsDir)) {
const components = fs
.readdirSync(componentsDir)
.filter((dir) => fs.statSync(path.join(componentsDir, dir)).isDirectory())
.map((dir) => @yto/custom/es/components/${dir});
// 合并现有的 include 配置
const existingIncludes = config.optimizeDeps?.include || [];
return {
optimizeDeps: {
include: [...existingIncludes, ...components],
},
};
}
return {};
},
};
}
optimizeDeps 与 warmup 的区别
两者都用于优化开发体验,但有明显区别:
特性 | optimizeDeps | warmup |
---|---|---|
关注点 | node_modules 中的依赖 | 项目源代码文件 |
处理范围 | 依赖的预构建和打包 | 文件的预编译和缓存 |
执行时机 | 开发服务器启动时 | 服务器启动后或空闲时 |
配置方式 | Vite 的内置配置项 | 作为插件添加 |
生产环境中的情况
在生产环境中不会出现这个问题,原因是:
- 完全打包:所有代码在构建时就已经被处理和打包
- 代码分割:应用被分割成多个块,按需加载
- 没有运行时预构建:不存在运行时的依赖预构建过程
- 优化的加载策略:代码块通过异步加载,不需要刷新页面
生产环境的构建配置示例:
ts
// vite.config.ts
import { defineConfig, ConfigEnv, UserConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import Icons from 'unplugin-icons/vite'
import IconsResolver from 'unplugin-icons/resolver'
import UnoCSS from 'unocss/vite'
import vueJsx from '@vitejs/plugin-vue-jsx'
import { visualizer } from 'rollup-plugin-visualizer'
import { DynamicComponentsResolver } from './src/components/DynamicComponentsResolver'
import { YtoCustomResolver } from '@yto/custom/resolvers'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import { warmup } from 'vite-plugin-warmup'
// https://vitejs.dev/config/
export default defineConfig(({ mode }: ConfigEnv): UserConfig => {
const isDev = process.env.NODE_ENV === 'development'
return {
resolve: {
dedupe: ['lodash', 'axios'], // 将 'lodash' 添加到 dedupe 数组中
alias: {
vue: 'vue/dist/vue.esm-bundler.js',
'~/': ''.concat(path.resolve(__dirname, 'src'), '/'),
'@': path.resolve(__dirname, 'src'), // 别名路径
lodash: path.resolve(__dirname, 'node_modules/lodash-es'), // 将 'lodash' 别名指向 'lodash-es'
},
},
optimizeDeps: {
include: [
'@yto/custom/es/components/**/*', // 这将包含所有子组件
'vue',
'@vue/shared',
],
},
plugins: [
vue(),
vueJsx({
transformOn: true,
mergeProps: true,
}),
warmup({
clientFiles: ['./*.html', './src/cusComponents/*.vue', './src/components/**/*.vue'],
}),
AutoImport({
// 插件预设支持导入的api
imports: ['vue', 'vue-router', 'pinia'],
resolvers: [
ElementPlusResolver(),
// 自动导入图标组件
IconsResolver({
prefix: 'Icon',
}),
],
dts: 'types/auto-imports.d.ts',
eslintrc: {
enabled: true,
filepath: 'types/.eslintrc-auto-import.json',
globalsPropValue: true,
},
}),
Components({
// ui库解析器,也可以自定义
resolvers: [
// 使用动态组件解析器
ElementPlusResolver({
importStyle: isDev ? false : 'sass',
directives: true,
}),
DynamicComponentsResolver(),
YtoCustomResolver(),
// 自动注册图标组件
IconsResolver({
enabledCollections: ['ep'],
}),
],
include: [/\.vue$/, /\.vue\?vue/, /\.tsx$/],
dts: 'types/components.d.ts',
// 添加这个配置来确保组件被正确注册
dirs: ['src/components'],
}),
Icons({
autoInstall: true,
}),
UnoCSS({
layers: {
component: 10,
},
}),
visualizer(),
// ytoCustomPreloadPlugin(),
],
css: {
preprocessorOptions: {
scss: {
javascriptEnabled: true,
// additionalData 的内容会在每个 scss 文件的开头自动注入
additionalData: !isDev ? `@use "~/assets/styles/variables.scss" as *;` : '', // 配置全局 scss`, // 配置全局 scss
},
},
},
build: {
sourcemap: mode !== 'development',
reportCompressedSize: mode !== 'development',
target: ['es2015', 'chrome63'],
rollupOptions: {
// 将大块分解成更小的块
output: {
manualChunks: {
state: ['pinia', 'pinia-plugin-persistedstate'],
core: ['vue', 'vue-router', '@vueuse/core'],
ytoSecurity: ['@yto-security/vue3-sdk'],
charts: ['echarts'],
immutable: ['immutable'],
utils: ['axios', 'lodash-es', 'dayjs'],
},
// 优化文件命名
chunkFileNames: 'assets/js/[name]-[hash].js',
entryFileNames: 'assets/js/[name]-[hash].js',
assetFileNames: 'assets/[ext]/[name]-[hash].[ext]',
},
},
},
server: {
port: 8082,
host: true,
proxy: {
'/api': {
target: 'http://10.130.17.89:8080', // uat
// target: 'http://10.130.136.73:8080', // sit
// target: "http://192.168.201.27:8080",
changeOrigin: true,
},
'/service-api': {
target: 'http://10.130.16.149:8082',
changeOrigin: true,
},
'/yto-collect': {
target: 'http://10.130.14.191:9999',
changeOrigin: true,
},
},
},
}
})
总结
这个问题是 Vite 开发服务器的工作机制导致的,通过正确配置 optimizeDeps.include
可以有效解决。在生产环境中,由于采用了完全不同的加载机制,不会出现这个问题。
这也体现了 Vite 的设计理念:"极速的开发体验和优化的构建输出"------它在开发和生产环境采用了不同的策略,以获得最佳的开发体验和生产性能。