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

相关推荐
sunbyte3 小时前
Tailwind CSS 初学者入门指南:项目集成,主要变更内容!
前端·css
可爱的秋秋啊3 小时前
vue3,element ui框架中为el-table表格实现自动滚动,并实现表头汇总数据
前端·vue.js·笔记·elementui
一夜枫林4 小时前
uniapp自定义拖拽排列
前端·javascript·uni-app
IT瘾君5 小时前
JavaWeb:Html&Css
前端·html
264玫瑰资源库6 小时前
问道数码兽 怀旧剧情回合手游源码搭建教程(反查重优化版)
java·开发语言·前端·游戏
喝拿铁写前端6 小时前
从圣经Babel到现代编译器:没开玩笑,普通程序员也能写出自己的编译器!
前端·架构·前端框架
HED6 小时前
VUE项目发版后用户访问的仍然是旧页面?原因和解决方案都在这啦!
前端·vue.js
拉不动的猪7 小时前
前端自做埋点,我们应该要注意的几个问题
前端·javascript·面试
王景程7 小时前
如何测试短信接口
java·服务器·前端
安冬的码畜日常7 小时前
【AI 加持下的 Python 编程实战 2_10】DIY 拓展:从扫雷小游戏开发再探问题分解与 AI 代码调试能力(中)
开发语言·前端·人工智能·ai·扫雷游戏·ai辅助编程·辅助编程