Vite 开发环境中路由切换导致页面刷新问题

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 的开发服务器工作机制:

  1. 按需编译模式:Vite 在开发环境中采用无打包模式,使用浏览器原生 ES 模块导入功能。

  2. 依赖预构建:当首次遇到未预构建的依赖时,Vite 会暂停当前请求,对依赖进行预构建,然后刷新页面。

  3. 组件按需加载:当路由切换到使用新组件的页面时,如果该组件尚未被预构建,就会触发上述过程。

即使 @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 的内置配置项 作为插件添加

生产环境中的情况

在生产环境中不会出现这个问题,原因是:

  1. 完全打包:所有代码在构建时就已经被处理和打包
  2. 代码分割:应用被分割成多个块,按需加载
  3. 没有运行时预构建:不存在运行时的依赖预构建过程
  4. 优化的加载策略:代码块通过异步加载,不需要刷新页面

生产环境的构建配置示例:

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 的设计理念:"极速的开发体验和优化的构建输出"------它在开发和生产环境采用了不同的策略,以获得最佳的开发体验和生产性能。

相关推荐
赵大仁1 分钟前
深入解析前后端分离架构:原理、实践与最佳方案
前端·架构
学不动学不明白4 分钟前
PC端项目兼容手机端
前端
无名之逆5 分钟前
Hyperlane:轻量、高效、安全的 Rust Web 框架新选择
开发语言·前端·后端·安全·rust·github·ssl
wkj00111 分钟前
js给后端发送请求的方式有哪些
开发语言·前端·javascript
最新资讯动态18 分钟前
“RdbStore”上线开源鸿蒙社区 助力鸿蒙应用数据访问效率大幅提升
前端
magic 24519 分钟前
JavaScript运算符与流程控制详解
开发语言·前端·javascript
xulihang44 分钟前
在手机浏览器上扫描文档并打印
前端·javascript·图像识别
RR91 小时前
【Vue3 进阶👍】:如何批量导出子组件的属性和方法?从手动代理到Proxy的完整指南
前端·vue.js
加个鸡腿儿1 小时前
01实战案例:「新手向」如何将原始数据转换为前端可用选项?
前端·程序员
java1234_小锋1 小时前
一周学会Flask3 Python Web开发-SQLAlchemy更新数据操作-班级模块
前端·数据库·python·flask·flask3