一、Vite的核心设计理念

Vite(法语"快速")的诞生是为了解决Webpack在开发环境下的两大顽疾:缓慢的启动速度(HMR)和冗余的打包过程。与Webpack在Dev模式也走完整Bundle流程不同,Vite做了根本性的架构切割。

1.1 两种开发模式的本质差异

// Webpack Dev模式的痛点:
// $ webpack --mode development
// [==] 编译中... ███████████████████████░░░░░░░ 85%
// 实际行为:即使只改了一个文件,也要重建整个依赖图
// 模块数量达到1000+时,首次启动 >30秒

// Vite的设计哲学:按需编译 + esm原生支持
// ┌──────────────────────────────────────────────┐
// │                Vite Dev Server               │
// │                                              │
// │  ┌─────────────────┐   ┌────────────────┐  │
// │  │  Dev Middleware  │   │  Pre-bundling  │  │
// │  │  (请求拦截)      │   │  (依赖预构建)   │  │
// │  └────────┬────────┘   └───────┬────────┘  │
// │           │                     │            │
// │           ▼                     ▼            │
// │  ┌─────────────────────────────────────────┐ │
// │  │        Rollup + esbuild (CJS转ESM)      │ │
// │  │        单文件按需编译                   │ │
// │  └─────────────────────────────────────────┘ │
// └──────────────────────────────────────────────┘

// Vite生产模式的构建:
// vite build → Rollup(完整Tree-shaking + 代码分割)
// 这与开发模式是两个完全不同的构建引擎

// 核心指标对比
//                      Webpack     Vite
// 冷启动(500模块)      32秒        0.8秒  ⚡ 40x
// HMR(单文件)         1.5秒       50ms   ⚡ 30x
// 生产构建             45秒        12秒   ⚡ 3.7x
// 产出大小(gzip)      420KB       380KB  (Rollup优化)

二、esbuild依赖预构建(Pre-bundling)

2.1 为什么需要预构建

// 依赖预构建解决的问题:
// ① CJS → ESM 转换(CommonJS依赖太多)
// ② 大量小文件合并(减少HTTP请求数)
// ③ 路径规范化(处理exports/exportsField)

// Vue3的依赖结构(仅计算dependencies):
// node_modules/vue/
// ├── package.json
// ├── dist/vue.runtime.esm-bundler.js    ← esm bundler版
// ├── dist/vue.esm-bundler.js           ← esm bundler完整版
// └── dist/vue.cjs.js                   ← CommonJS版
// 浏览器直接import时,exportsField可能导致加载错误版本

// 预构建后的产物(vite.config.js: optimizeDeps)
// .vite/deps/vue.js     ← 预打包后的单文件
// .vite/deps/_metadata.json  ← 缓存元信息

// 触发预构建的条件:
// 1. 首次运行(.vite目录不存在)
// 2. package.json变化(dependencies增减)
// 3. lock文件变化(npm install更新了版本)

// esbuild的配置(源码位于 vite/src/node/optimizer)
// vite optimizeDeps.include = ['vue', 'react', 'lodash']
// 强制将某些包加入预构建(避免动态import问题)

2.2 预构建的缓存机制

// 预构建缓存路径:node_modules/.vite/deps/
// 缓存失效条件(immutable cache):
// .vite/deps/_metadata.json 记录:
{
  "hash": "a3f2c1b8",     // 根据 node_modules 内容计算
  "browserHash": "d4e5f6a7", // 根据 package.json deps 计算
  "optimized": {
    "vue": {
      "file": "vue.js",
      "src": "vue/dist/vue.esm-bundler.js",
      "needsInterop": false
    }
  }
}

// ⚠️ 常见问题:预构建失效导致"cannot import from"
import _ from 'lodash-es';  // lodash-es vs lodash
// 解决:预构建时指定正确的入口
// vite.config.js
export default defineConfig({
  optimizeDeps: {
    include: ['lodash-es/lodash.min.js']
  }
});

三、Rollup插件机制与Vite插件链

3.1 Rollup插件的数据流

// Rollup插件是"转换函数"的组合,数据流如下:
//
// Source Code
//      │
//      ▼
//   ① load(id)         ← 按ID加载模块内容
//      │ (返回null则进入下一插件)
//      ▼
//   ② transform(code, id)  ← AST转换(babel/terser/typescript)
//      │ (返回null则跳过)
//      ▼
//   ③ parse(id)       ← 生成AST(供后续分析)
//      │
//      ▼
//   ④ resolveId(importPath) ← 路径解析
//      │ (返回null则继续)
//      ▼
//   ⑤ transform(...)    ← 再次transform(可链式)
//      │
//      ▼
//   ⑥ generate/bundle  ← 输出最终产物

// Rollup插件结构(标准五件套)
export default function myPlugin() {
  return {
    name: 'my-plugin',           // 插件名(用于日志)
    enforce: 'pre' | 'post',    // 执行顺序:pre在Vite内置插件前

    // 钩子签名
    resolveId(source, importer) {  // 路径解析
      if (source === 'virtual-module') {
        return source;  // 返回ID表示已处理
      }
      return null;  // 返回null表示交给下一个插件处理
    },

    load(id) {                  // 加载模块
      if (id === 'virtual-module') {
        return 'export default "from virtual module"';
      }
      return null;
    },

    transform(code, id) {       // 转换代码
      if (!id.endsWith('.special')) return null;

      return {
        code: code.replace(/old/g, 'new'),
        map: null,              // 源码映射
        deps: [],               // 新增依赖
        assets: []              // 静态资源
      };
    },

    generateBundle(options, bundle) {  // 输出前最后修改
      // 在这里可以修改输出文件
      const chunk = bundle['main.js'];
      chunk.code = '/* injected by plugin */\n' + chunk.code;
    }
  };
}

3.2 Vite插件与Rollup插件的差异

// Vite独有钩子(开发服务器专用)
// 这些钩子在 rollupOptions.plugins 中无法使用
export default function viteOnlyPlugin() {
  return {
    name: 'vite-only',
    // HTTP请求拦截(配置服务器中间件)
    configureServer(server) {
      server.middlewares.use('/custom-api', (req, res) => {
        res.end(JSON.stringify({ from: 'vite-plugin' }));
      });
    },

    // 热更新自定义(ViteHMRClient通信)
    handleHotUpdate(ctx) {
      // ctx = { file, modules, server, timestamp, read }
      if (ctx.file.includes('shared')) {
        // 通知所有相关模块热更新
        ctx.server.ws.send({ type: 'full-reload' });
      }
    },

    // 开发服务器启动前
    configurePreviewServer(server) { },

    // 构建前执行
    apply: 'build' | 'serve' | undefined
  };
}

// Vite插件扩展了Rollup钩子的三个场景
// ① SSR构建:transformIndexHtml → 注入SSR bundle标签
// ② 模块预构建:resolveDependencies → 自定义依赖解析
// ③ 路径规范化:normalizePath → 跨平台路径处理

四、生产构建与代码分割

4.1 Rollup的代码分割策略

// Rollup支持三种代码分割策略:
// ① 动态import() 自动分割
// ② manualChunks 函数精确控制
// ③ facade chunks(入口与代码分离)

// 策略一:动态import自动分割
// index.js
import('./heavy-module.js').then(m => m.run());

// 编译后自动生成独立chunk:
// dist/
// ├── index-[hash].js      ← 主入口
// ├── heavy-module-[hash].js ← 自动分割
// └── index-[hash].html

// 策略二:manualChunks精细控制
// vite.config.js
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks(id) {
          // 按目录分组
          if (id.includes('node_modules/vue')) return 'vendor-vue';
          if (id.includes('node_modules/react')) return 'vendor-react';
          if (id.includes('/components/')) return 'ui-components';
          if (id.includes('/utils/')) return 'utils';
        }
      }
    }
  }
});

// 策略三:facade chunk(按入口分割)
// 每个HTML入口对应一个facade + 共享chunk
// 适用:多页应用(MPA)
// vite.config.js
export default defineConfig({
  build: {
    rollupOptions: {
      input: {
        main: 'index.html',
        admin: 'admin.html',
        docs: 'docs.html'
      },
      output: {
        // 产出结构:
        // main-[hash].js        ← 主入口facade
        // admin-[hash].js       ← 管理后台facade
        // shared-[hash].js      ← 公共库(自动提取)
      }
    }
  }
});

五、自定义插件实战:从设计到实现

// 实战:创建一个"SVG组件化"插件
// 功能:import './icon.svg?component' → 返回React/Vue组件
// 使用:import Icon from './icon.svg?component'

import { readFileSync } from 'fs';
import { basename } from 'path';

// 方案一:纯Vite插件(推荐)
export function svgComponentPlugin(preamble = '') {
  const svgRegex = /\.svg\?component$/;

  return {
    name: 'vite-plugin-svg-component',

    // 解析:识别特殊后缀
    resolveId(source) {
      if (svgRegex.test(source)) {
        // 返回处理后的ID(去掉?component后缀)
        return source.replace('?component', '');
      }
      return null;
    },

    // 加载:将SVG转为React/Vue组件代码
    load(id) {
      if (!svgRegex.test(id) && !id.endsWith('.svg')) return null;

      const raw = readFileSync(id, 'utf-8');

      // 提取SVG属性(用于保留原始样式)
      const attrs = raw.match(/]*)>/)?.[1] || '';
      // 清理SVG(移除width/height以支持CSS控制)
      const content = raw
        .replace(/]*>/, '')
        .replace('', '')
        .trim();

      const componentName = basename(id, '.svg')
        .replace(/-([a-z])/g, (_, c) => c.toUpperCase())
        .replace(/^\w/, c => c.toUpperCase());

      // 生成React组件代码
      const code = [
        preamble,
        `const ${componentName} = (props) => ${'`'}<svg ${attrs} {...props}>${content}</svg>${'`'};`,
        `export default ${componentName};`
      ].join('\n');

      return { code, map: null };
    }
  };
}

// vite.config.js 使用
// import { svgComponentPlugin } from './plugins/svg-component';
// export default defineConfig({
//   plugins: [
//     vue(),
//     svgComponentPlugin('/* Auto-generated SVG Component */')
//   ]
// });