Skip to content

vite-plugin-fake-server 导致的 require 函数问题深度分析报告

日期: 2025-10-10
分析人员: Claude Code
项目: 01s-11comm 智慧社区管理系统


1. 问题概述

1.1 问题现象

在使用 vite-plugin-fake-server 插件并设置 enableProd: true 时,构建生产环境代码后出现以下问题:

  1. 浏览器控制台报错:require is not defined
  2. 构建产物中出现 Node.js 的 require 函数调用
  3. 具体表现为类似代码出现在浏览器端:
    javascript
    const { message: Mzt } = require("D:/code/github-desktop-store/01s-11comm/apps/admin/src/utils/message.ts");

1.2 问题影响

  • 应用在生产环境无法正常运行
  • Mock 数据功能失效
  • 浏览器不支持 CommonJS 的 require 函数,导致运行时错误

2. 技术背景

2.1 项目配置

vite.config.ts 配置(apps/admin/vite.config.ts:101-104):

typescript
build: {
  commonjsOptions: {
    transformMixedEsModules: true,
    strictRequires: true,  // 关键配置
  },
}

vite-plugin-fake-server 配置(apps/admin/build/plugins/index.ts:189-195):

typescript
vitePluginFakeServer({
  logger: false,
  include: "mock",
  infixName: false,
  enableProd: true,  // 生产环境启用
}),

2.2 Vite 6 的 strictRequires 变更

Vite 6 将 strictRequires 的默认值从 "auto" 改为 true,这是一个破坏性变更:

  • strictRequires: "auto": 宽松模式,允许动态 require
  • strictRequires: true: 严格模式,禁止动态 require,要求所有 require 必须是静态可分析的

3. 问题根源深度分析

3.1 vite-plugin-fake-server 的工作原理

3.1.1 开发环境

在开发环境下,插件通过以下方式工作:

  1. 使用 chokidar 监听 mock 文件变化
  2. 使用 bundle-import 动态加载 mock 文件
  3. 创建 middleware 拦截 HTTP 请求
  4. 返回 mock 数据

关键代码(index.mjs:440-461):

javascript
async loadFakeData(filepath) {
  const fakeCodeData = [];
  let fakeFileDependencies = {};
  try {
    const { mod, dependencies } = await bundleImport({ filepath, cwd: this.options.root });
    fakeFileDependencies = dependencies;
    const resolvedModule = mod.default || mod;
    if (Array.isArray(resolvedModule)) {
      fakeCodeData.push(...resolvedModule);
    } else {
      fakeCodeData.push(resolvedModule);
    }
  } catch (error) {
    this.options.loggerOutput.error(colors.red(`failed to load module from ${filepath}`), {
      error,
      timestamp: true
    });
  }
  // ...
}

3.1.2 生产环境(enableProd: true)

enableProd: true 时,插件在 transformIndexHtml 钩子中注入代码:

关键代码(index.mjs:1996-2079):

javascript
transformIndexHtml: {
  order: "pre",
  handler: async (htmlString) => {
    if (isDevServer || !opts.enableProd) {
      return htmlString;
    }

    // 1. 注入全局对象
    scriptTagList.push({
      ...scriptTagOptions,
      children: [
        "window.__VITE__PLUGIN__FAKE__SERVER__",
        "=",
        JSON.stringify({ meta: pkg, vitePluginFakeServerOptions: opts }, null, 2),
        ";"
      ].join("")
    });

    // 2. 使用 import.meta.glob 导入所有 mock 文件
    const fakeFilePath = getFakeFilePath(
      {
        include: opts.include,
        exclude: opts.exclude,
        extensions: opts.extensions,
        infixName: opts.infixName
      },
      config.root
    );
    const relativeFakeFilePath = fakeFilePath.map((filePath) => `/${filePath}`);
    const fakeTemplate = `
      const modules = import.meta.glob(${JSON.stringify(relativeFakeFilePath, null, 2)}, { eager: true });
      const fakeModuleList = Object.keys(modules).reduce((list, key) => {
        const module = modules[key] ?? {};
        if (module.default) {
          for (const moduleKey of Object.keys(module)) {
            const mod = modules[key][moduleKey] ?? [];
            const modList = Array.isArray(mod) ? [...mod] : [mod];
            return [...list, ...modList];
          }
        } else {
          return list;
        }
      }, []);
      window.__VITE__PLUGIN__FAKE__SERVER__.fakeModuleList = fakeModuleList;
    `;
    scriptTagList.push({
      ...scriptTagOptions,
      children: fakeTemplate
    });

    // 3. 注入 xhook 拦截器
    scriptTagList.push({
      ...scriptTagOptions,
      children: `${xhook.toString()};window.__VITE__PLUGIN__FAKE__SERVER__.xhook=xhook();`
    });

    // 4. 构建并注入 path-to-regexp
    const pathToRegexpContent = await buildPackage("path-to-regexp");
    scriptTagList.push({
      ...scriptTagOptions,
      children: `${pathToRegexpContent}`
    });

    // 5. 注入钩子函数
    scriptTagList.push({
      ...scriptTagOptions,
      children: `const fakeModuleList = window.__VITE__PLUGIN__FAKE__SERVER__.fakeModuleList;
        const pathToRegexp = window.__VITE__PLUGIN__FAKE__SERVER__.pathToRegexp;
        const match = pathToRegexp.match ?? pathToRegexp.default.match;
        window.__VITE__PLUGIN__FAKE__SERVER__.xhook.before(${createHookTemplate(false, opts)});
        window.__VITE__PLUGIN__FAKE__SERVER__.xhook.before(${createHookTemplate(true, opts)});`
    });

    return scriptTagList;
  }
}

3.2 问题的核心原因

3.2.1 Mock 文件的依赖链

在项目中发现:mock 文件引用了项目源代码

asyncRoutes.ts(apps/admin/mock/asyncRoutes.ts:3):

typescript
import { defineFakeRoute } from "vite-plugin-fake-server/client";
import { RouterOrderEnums } from "@/router/enums"; // ← 关键:引用了项目源码

这导致:

  1. Mock 文件不是独立的
  2. 构建时会包含项目源码的依赖
  3. 依赖链可能包含复杂的模块依赖关系

3.2.2 import.meta.glob 的构建行为

当使用 import.meta.glob 时:

javascript
const modules = import.meta.glob(["/mock/**/*.ts"], { eager: true });

Vite 会:

  1. 静态分析这些文件
  2. 构建这些文件及其所有依赖
  3. 将它们打包到最终产物中

3.2.3 strictRequires: true 的影响

strictRequires: true 模式下:

  1. 严格的 CommonJS 处理

    • Rollup 要求所有 require 调用必须是静态可分析的
    • 动态 require 会被保留为原始代码
    • 不会转换为 ES 模块导入
  2. 依赖分析失败
    当 mock 文件的依赖链中包含:

    • 动态导入
    • 条件导入
    • 循环依赖

    Rollup 可能无法正确分析和转换这些模块

  3. 结果
    无法转换的 require 调用被保留在浏览器端代码中

3.3 buildPackage 函数的配置差异

插件内部使用 buildPackage 函数构建 path-to-regexp 时,使用了不同的配置:

buildPackage 函数(index.mjs:637-677):

javascript
const require = createRequire(import.meta.url);
async function buildPackage(packageName) {
	const result = await build({
		configFile: false,
		build: {
			commonjsOptions: {
				strictRequires: "auto", // ← 注意:使用 "auto" 模式
			},
			write: false,
			lib: {
				entry: require.resolve(packageName),
				name: camelCasePackageName,
				formats: ["iife"],
				fileName: packageName,
			},
			rollupOptions: {
				output: {
					exports: "named",
					extend: true,
				},
			},
			minify: false,
		},
	});
	// ...
}

配置差异对比

配置项项目 vite.config.tsbuildPackage 函数
strictRequirestrue"auto"
影响主应用构建path-to-regexp 构建

这说明:

  • 插件作者意识到 strictRequires: true 会导致问题
  • 在构建 path-to-regexp 时使用了宽松模式
  • 但主应用的 mock 文件仍然使用项目配置的严格模式

4. 为什么会 require message.ts?

4.1 可能的场景

虽然在当前检查中未直接发现 mock 文件引用 message.ts,但问题可能出现在:

  1. 间接依赖

    plain
    asyncRoutes.ts → RouterOrderEnums → (其他模块) → message.ts
  2. 动态导入
    某些模块可能使用动态导入:

    typescript
    const module = await import("@/utils/message");
  3. Rollup 的 CommonJS 转换
    当遇到无法静态分析的模块时,Rollup 可能生成如下代码:

    javascript
    // 转换失败,保留 require
    const { message: Mzt } = require("D:/code/github-desktop-store/01s-11comm/apps/admin/src/utils/message.ts");

4.2 绝对路径的出现

require 中出现完整的文件系统路径是因为:

  1. Rollup 的 externalize 处理
    当模块无法被打包时,Rollup 会将其外部化

  2. 路径解析失败
    无法正确解析别名路径(如 @/)时,会使用绝对路径

  3. strictRequires: true 的副作用
    在严格模式下,Rollup 倾向于保留原始路径


5. Vite 构建时保留 require 的条件

5.1 直接原因

Vite/Rollup 在以下情况下会保留 require 调用:

  1. 动态 require

    javascript
    const moduleName = getModuleName();
    const module = require(moduleName); // 动态,无法静态分析
  2. 条件 require

    javascript
    if (condition) {
    	require("./module-a");
    } else {
    	require("./module-b");
    }
  3. 循环依赖

    javascript
    // a.js
    const b = require("./b");
    // b.js
    const a = require("./a"); // 循环依赖
  4. 外部化模块

    javascript
    // rollup.config.js
    external: ["some-module"];
    // 构建结果
    const someModule = require("some-module");

5.2 strictRequires: true 的影响机制

Rollup 的 CommonJS 插件行为

javascript
// strictRequires: "auto" (宽松模式)
// 输入
const message = require("./message");
// 输出(转换为 ESM)
import message from "./message";

// strictRequires: true (严格模式)
// 输入
const message = require("./message"); // 如果无法静态分析
// 输出(保留原样)
const message = require("./message"); // ← 导致浏览器报错

静态分析失败的情况

  1. 模块路径包含变量
  2. require 在条件语句中
  3. 模块有副作用导入
  4. TypeScript 装饰器
  5. 复杂的导入导出结构

6. 解决方案建议

6.1 方案一:降级 strictRequires(最简单)⭐

修改 vite.config.ts

typescript
build: {
  commonjsOptions: {
    transformMixedEsModules: true,
    strictRequires: "auto",  // 从 true 改为 "auto"
  },
}

优点

  • 修改简单,只需一行配置
  • 立即解决问题
  • 插件作者也在 buildPackage 中使用此配置

缺点

  • 可能隐藏一些模块兼容性问题
  • 不是最佳实践

6.2 方案二:隔离 Mock 文件依赖(推荐)✅

原则:Mock 文件不应该引用项目源代码

修改 asyncRoutes.ts

typescript
// ❌ 错误:引用项目源码
import { RouterOrderEnums } from "@/router/enums";

// ✅ 正确:在 mock 文件中定义常量
const RouterOrderEnums = {
	home: 0,
	chatai: 1,
	vueflow: 2,
	// ... 其他值
	system: 14,
	monitor: 15,
	tabs: 16,
	// ...
};

// 或者使用内联值
const systemManagementRouter = {
	path: "/system",
	meta: {
		icon: "ri:settings-3-line",
		title: "common.menus.pureSysManagement",
		rank: 14, // 直接使用数字
	},
	// ...
};

优点

  • 彻底解决依赖问题
  • Mock 文件完全独立
  • 避免循环依赖风险
  • 符合最佳实践

缺点

  • 需要重构 mock 文件
  • 可能需要同步维护常量值

6.3 方案三:关闭生产环境 Mock(最彻底)

修改插件配置

typescript
vitePluginFakeServer({
  logger: false,
  include: "mock",
  infixName: false,
  enableProd: false,  // 改为 false
}),

优点

  • 完全避免生产环境的 mock 问题
  • 生产包更小
  • 性能更好

缺点

  • 失去生产环境 mock 功能
  • 如果需要演示环境需要其他方案

6.4 方案四:使用 Vite 的 build.rollupOptions.external

修改 vite.config.ts

typescript
build: {
  commonjsOptions: {
    transformMixedEsModules: true,
    strictRequires: true,
  },
  rollupOptions: {
    external: (id) => {
      // 排除 mock 文件的依赖
      if (id.includes('/mock/')) {
        return false;
      }
      // 排除项目源码在 mock 构建中被引用
      if (id.includes('/src/router/') && id.includes('mock')) {
        return true;
      }
      return false;
    },
  },
}

优点

  • 精确控制外部依赖
  • 保持 strictRequires: true

缺点

  • 配置复杂
  • 需要精确了解依赖关系
  • 维护成本高

6.5 方案五:升级或替换 vite-plugin-fake-server

检查更新

bash
pnpm update vite-plugin-fake-server

或考虑替代方案:

  • vite-plugin-mock-server
  • vite-plugin-mock-dev-server
  • msw (Mock Service Worker)

优点

  • 可能已有官方修复
  • 更好的 Vite 6 兼容性

缺点

  • 需要测试新版本
  • 可能需要重构 mock 文件

7. 推荐实施步骤

7.1 短期快速修复(1 小时内)

  1. 应用方案一:修改 strictRequires"auto"
  2. 验证构建:运行 pnpm build 确认构建成功
  3. 测试应用:验证生产环境功能正常

7.2 长期最佳实践(1-2 天)

  1. 重构 mock 文件(方案二):

    bash
    # 检查所有 mock 文件的依赖
    grep -r "from '@/" apps/admin/mock/
    grep -r "from '@/router" apps/admin/mock/
  2. 隔离依赖

    • 将共享常量复制到 mock 文件中
    • 或创建 mock/constants.ts 存放 mock 专用常量
  3. 恢复 strictRequires

    typescript
    strictRequires: true,  // 恢复严格模式
  4. 验证构建:确保所有依赖都正确处理

7.3 最佳实践建议

  1. Mock 文件规范

    • Mock 文件应该自包含
    • 不引用项目源码
    • 使用 faker 等库生成测试数据
  2. 依赖管理

    • 定期检查 mock 文件的依赖
    • 使用 lint 规则禁止 mock 引用 src
  3. 构建验证

    • CI/CD 中添加生产构建检查
    • 检测浏览器不兼容的 Node.js API

8. 技术细节补充

8.1 import.meta.glob 的工作原理

javascript
// Vite 编译前
const modules = import.meta.glob("/mock/**/*.ts", { eager: true });

// Vite 编译后(简化版)
const modules = {
	"/mock/login.ts": () => import("/mock/login.ts"),
	"/mock/asyncRoutes.ts": () => import("/mock/asyncRoutes.ts"),
	// ...
};

// eager: true 时
const modules = {
	"/mock/login.ts": __import_0,
	"/mock/asyncRoutes.ts": __import_1,
	// ...
};

8.2 Rollup CommonJS 插件的转换逻辑

转换流程

plain
1. 解析 require 调用

2. 检查 strictRequires 配置

3a. strictRequires: "auto"     3b. strictRequires: true
    ↓                              ↓
    尝试转换为 ESM                 严格检查
    ↓                              ↓
    成功 → ESM import              静态分析通过 → ESM import
    失败 → 保留 require            静态分析失败 → 保留 require ❌

8.3 浏览器环境的限制

浏览器不支持:

  • require() 函数
  • module.exports
  • __dirname, __filename
  • Node.js 内置模块(fs, path, 等)

Vite 的处理:

  • 自动将 ES 模块转换为浏览器兼容代码
  • 使用 Rollup 打包 CommonJS 模块
  • Polyfill 部分 Node.js API

9. 相关资源

9.1 官方文档

9.2 相关 Issue


10. 结论

10.1 问题本质

vite-plugin-fake-server 在生产环境使用 import.meta.glob 导入 mock 文件时,如果 mock 文件引用了项目源码(如本项目的 asyncRoutes.ts 引用 @/router/enums),在 Vite 6 的 strictRequires: true 模式下,Rollup 无法正确转换所有依赖,导致浏览器端代码中保留了 Node.js 的 require 函数调用。

10.2 根本原因

  1. 配置冲突:Vite 6 默认 strictRequires: true 与插件的生产环境实现不兼容
  2. 依赖污染:Mock 文件引用了项目源码,破坏了依赖隔离
  3. 构建行为差异:开发环境使用 bundle-import,生产环境使用 import.meta.glob,行为不一致

10.3 推荐方案

立即实施(紧急修复):

  • 方案一:strictRequires: "auto"

长期优化(推荐):

  • 方案二:重构 mock 文件,隔离依赖

根本解决(如果可以):

  • 方案三:enableProd: false

10.4 预防措施

  1. 代码审查:确保 mock 文件不引用项目源码
  2. 自动化检查:添加 lint 规则
  3. CI/CD:生产构建检查
  4. 文档规范:制定 mock 文件编写规范

报告完成日期: 2025-10-10
建议优先级: 高
预估修复时间:

  • 临时方案:10 分钟
  • 长期方案:2-4 小时

联系方式: 如有疑问请查阅 Vite 官方文档或提交 Issue

贡献者

The avatar of contributor named as ruan-cat ruan-cat

页面历史

最近更新