一、为什么要学 Vite 插件开发?

在使用 Vite 的过程中,你可能会遇到这些场景:

  • 需要在构建时自动生成某些文件
  • 想要自定义模块解析逻辑
  • 需要在开发服务器中添加特定的 API 路由
  • 想要集成特定的代码检查或转换工具

Vite 插件就是解决这些问题的钥匙!

Vite 插件 vs Rollup 插件

Vite 的插件系统基于 Rollup,但做了大量扩展:

特性 Rollup 插件 Vite 插件
构建阶段 ✅ 支持 ✅ 支持
开发服务器 ❌ 不支持 ✅ 支持
HMR 热更新 ❌ 不支持 ✅ 支持
配置解析 ❌ 不支持 ✅ 支持

二、插件基础:Hello World

Vite 插件本质上是一个对象(或返回对象的函数),包含 name 属性和各种钩子函数:

2.1 最简单的插件

// my-first-plugin.js
export default function myFirstPlugin() {
  return {
    name: 'my-first-plugin',
    
    // 配置解析时调用
    config(config, { command }) {
      console.log('🚀 Vite 模式:', command); // 'serve' 或 'build'
      return {
        // 可以返回部分配置,会与用户配置合并
        base: command === 'build' ? '/app/' : '/'
      };
    },
    
    // 构建开始时调用
    buildStart() {
      console.log('📦 开始构建...');
    },
    
    // 构建结束时调用
    buildEnd() {
      console.log('✅ 构建完成!');
    }
  };
}

使用插件:

// vite.config.js
import { defineConfig } from 'vite';
import myFirstPlugin from './my-first-plugin.js';

export default defineConfig({
  plugins: [myFirstPlugin()]
});

2.2 运行效果

$ npm run dev
🚀 Vite 模式: serve
📦 开始构建...
✅ 构建完成!
  VITE v5.0.0  ready in 320 ms

三、核心钩子详解

Vite 提供了丰富的钩子函数,覆盖了从配置解析到构建完成的整个生命周期。

3.1 配置相关钩子

export default function configPlugin() {
  return {
    name: 'config-plugin',
    
    // 1. config - 修改或扩展配置
    config(userConfig, { command, mode }) {
      // command: 'serve' | 'build'
      // mode: 'development' | 'production' | 自定义
      return {
        resolve: {
          alias: {
            '@': '/src'
          }
        }
      };
    },
    
    // 2. configResolved - 配置解析完成后
    configResolved(resolvedConfig) {
      // 可以获取最终解析后的配置
      console.log('📁 项目根目录:', resolvedConfig.root);
      console.log('🔧 运行模式:', resolvedConfig.mode);
    }
  };
}

3.2 开发服务器钩子

这是 Vite 独有的,Rollup 插件无法使用:

export default function serverPlugin() {
  return {
    name: 'server-plugin',
    
    // 配置开发服务器
    configureServer(server) {
      // server 是 ViteDevServer 实例
      
      // 添加自定义路由
      server.middlewares.use('/api/health', (req, res, next) => {
        res.end(JSON.stringify({ status: 'ok', time: Date.now() }));
      });
      
      // 监听文件变化
      server.watcher.on('change', (file) => {
        console.log('📝 文件变化:', file);
      });
    },
    
    // 配置预览服务器(vite preview)
    configurePreviewServer(server) {
      server.middlewares.use('/api/version', (req, res) => {
        res.end(JSON.stringify({ version: '1.0.0' }));
      });
    }
  };
}

3.3 构建钩子(Rollup 兼容)

export default function buildPlugin() {
  return {
    name: 'build-plugin',
    
    // 解析模块ID
    resolveId(source, importer) {
      if (source === 'virtual-module') {
        return source; // 返回解析后的ID
      }
    },
    
    // 加载模块内容
    load(id) {
      if (id === 'virtual-module') {
        return 'export const msg = "Hello from virtual module!"';
      }
    },
    
    // 转换代码
    transform(code, id) {
      // 只对 .js 文件进行处理
      if (id.endsWith('.js')) {
        // 简单的代码转换示例
        return {
          code: code.replace(/console\\.log/g, 'console.debug'),
          map: null // 可以返回 source map
        };
      }
    }
  };
}

3.4 完整生命周期图

config → configResolved → configureServer
                              ↓
                        (开发服务器运行中)
                              ↓
                        buildStart → resolveId → load → transform
                                              ↓
                                        (HMR 触发时重复)
                                              ↓
                                        buildEnd → closeBundle

四、实战案例

案例 1:自动生成环境信息文件

开发中经常需要知道当前构建的时间、版本号等信息:

// plugins/build-info-plugin.js
import { writeFileSync } from 'fs';
import { resolve } from 'path';

export default function buildInfoPlugin() {
  return {
    name: 'build-info',
    
    buildStart() {
      const info = {
        version: process.env.npm_package_version || '1.0.0',
        buildTime: new Date().toISOString(),
        nodeEnv: process.env.NODE_ENV,
        platform: process.platform
      };
      
      // 将信息写入 JSON 文件
      const outputPath = resolve(process.cwd(), 'public', 'build-info.json');
      writeFileSync(outputPath, JSON.stringify(info, null, 2));
      
      console.log('📝 构建信息已生成');
    }
  };
}

使用:

// vite.config.js
import buildInfoPlugin from './plugins/build-info-plugin.js';

export default {
  plugins: [buildInfoPlugin()]
};

前端获取:

// App.vue 或任意 JS 文件
fetch('/build-info.json')
  .then(r => r.json())
  .then(info => {
    console.log('📦 版本:', info.version);
    console.log('🕐 构建时间:', info.buildTime);
  });

案例 2:Mock API 服务器

开发时常用的 Mock 数据插件:

// plugins/mock-plugin.js
import { readFileSync } from 'fs';
import { resolve } from 'path';

const mockData = {
  '/api/users': [
    { id: 1, name: '张三', role: 'admin' },
    { id: 2, name: '李四', role: 'user' }
  ],
  '/api/posts': [
    { id: 1, title: 'Hello Vite', content: 'Vite 真快!' }
  ]
};

export default function mockPlugin(options = {}) {
  const { prefix = '/api', delay = 500 } = options;
  
  return {
    name: 'mock-server',
    
    configureServer(server) {
      server.middlewares.use((req, res, next) => {
        // 只处理 API 请求
        if (!req.url.startsWith(prefix)) {
          return next();
        }
        
        // 模拟网络延迟
        setTimeout(() => {
          const data = mockData[req.url];
          
          if (data) {
            res.setHeader('Content-Type', 'application/json');
            res.end(JSON.stringify({ code: 0, data }));
          } else {
            res.statusCode = 404;
            res.end(JSON.stringify({ code: 404, msg: 'Not Found' }));
          }
        }, delay);
      });
    }
  };
}

使用:

// vite.config.js
import mockPlugin from './plugins/mock-plugin.js';

export default {
  plugins: [
    mockPlugin({
      prefix: '/api',
      delay: 300  // 模拟 300ms 延迟
    })
  ]
};

案例 3:条件编译(类似 #ifdef)

实现类似 C 语言的条件编译功能:

// plugins/conditional-compile-plugin.js
export default function conditionalCompilePlugin(options = {}) {
  const { env = {} } = options;
  
  return {
    name: 'conditional-compile',
    
    transform(code, id) {
      // 只对 JS/TS/Vue 文件处理
      if (!/\\.(js|ts|vue)$/.test(id)) return;
      
      // 匹配 //#ifdef KEY ... //#endif
      const regex = /\\/\\/\\s*#ifdef\\s+(\\w+)\\s*([\\s\\S]*?)\\/\\/\\s*#endif/g;
      
      return {
        code: code.replace(regex, (match, key, content) => {
          // 如果环境变量存在且为真,保留内容,否则删除
          return env[key] ? content : '';
        }),
        map: null
      };
    }
  };
}

使用:

// vite.config.js
import conditionalCompile from './plugins/conditional-compile-plugin.js';

export default {
  plugins: [
    conditionalCompile({
      env: {
        DEBUG: process.env.NODE_ENV === 'development',
        PRO_FEATURE: false
      }
    })
  ]
};

源代码中:

function initApp() {
  //#ifdef DEBUG
  console.log('调试模式启动');
  console.log('配置信息:', config);
  //#endif
  
  //#ifdef PRO_FEATURE
  loadProModules();
  //#endif
  
  startApp();
}

构建后(DEBUG=true, PRO_FEATURE=false):

function initApp() {
  console.log('调试模式启动');
  console.log('配置信息:', config);
  
  startApp();
}

五、进阶技巧

5.1 虚拟模块

创建不对应真实文件的模块:

// plugins/virtual-module-plugin.js
const virtualModuleId = 'virtual:app-config';
const resolvedVirtualModuleId = '\\0' + virtualModuleId;

export default function virtualModulePlugin() {
  return {
    name: 'virtual-module',
    
    resolveId(id) {
      if (id === virtualModuleId) {
        return resolvedVirtualModuleId;
      }
    },
    
    load(id) {
      if (id === resolvedVirtualModuleId) {
        return `
          export const appName = 'My Awesome App';
          export const version = '${process.env.npm_package_version}';
          export const features = {
            darkMode: true,
            i18n: true
          };
        `;
      }
    }
  };
}

使用:

import { appName, version, features } from 'virtual:app-config';

console.log(appName); // 'My Awesome App'

5.2 HMR 热更新支持

让插件支持热更新:

export default function hmrPlugin() {
  return {
    name: 'hmr-plugin',
    
    handleHotUpdate({ server, modules, timestamp }) {
      // 自定义 HMR 处理逻辑
      
      // 过滤特定模块
      const filtered = modules.filter(m => !m.id.includes('node_modules'));
      
      console.log('🔄 热更新模块:', filtered.map(m => m.id));
      
      // 返回模块列表,Vite 会继续处理
      return filtered;
    }
  };
}

5.3 插件排序

控制插件执行顺序:

export default {
  plugins: [
    {
      ...myPlugin(),
      enforce: 'pre'  // 'pre' | 'post',默认 normal
    }
  ]
};

六、发布你的插件

6.1 插件项目结构

vite-plugin-awesome/
├── src/
│   └── index.js          # 插件入口
├── package.json
├── README.md
└── LICENSE

6.2 package.json 配置

{
  "name": "vite-plugin-awesome",
  "version": "1.0.0",
  "description": "An awesome Vite plugin",
  "main": "dist/index.js",
  "module": "dist/index.mjs",
  "types": "dist/index.d.ts",
  "files": ["dist"],
  "scripts": {
    "build": "rollup -c",
    "prepublishOnly": "npm run build"
  },
  "peerDependencies": {
    "vite": "^4.0.0 || ^5.0.0"
  },
  "keywords": ["vite", "plugin", "vite-plugin"],
  "license": "MIT"
}
6.3 发布到 npm

6.3 发布到 npm

npm login
npm publish

七、常用插件推荐

插件 功能
@vitejs/plugin-vue Vue 单文件组件支持
@vitejs/plugin-react React Fast Refresh
vite-plugin-pwa PWA 支持
unplugin-auto-import 自动导入 API
vite-plugin-svg-icons SVG 图标雪碧图
vite-plugin-mock Mock 数据服务

八、总结

Vite 插件开发并不复杂,核心要点:

✅ 必须掌握

  • 插件基本结构(name + hooks)
  • 常用钩子:configtransformconfigureServer
  • 虚拟模块的使用

✅ 进阶技能

  • HMR 热更新处理
  • 开发服务器中间件
  • 构建流程控制

✅ 最佳实践

  • 插件选项设计要灵活
  • 提供详细的文档和示例
  • 处理好错误边界
Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐