普通视图

发现新文章,点击刷新页面。
昨天以前首页

Vite开发环境按需编译是怎么实现的?

作者 sorryhc
2025年12月11日 14:55

前言

Vite的快,我们并不陌生,主要体现在开发环境时的体验。

而相较于其他构建工具,Vite核心是依靠了现代浏览器对于原生esm模块的支持+按需实时编译将性能达到了极致。

我们基于源码来看看esbuild编译的完整过程。

核心流程图

Browser Request
    ↓
Vite DevServer (Connect 中间件)
    ↓
请求路由判断
    ├─ /.vite/client → 注入客户端代码
    ├─ /@modules/* → node_modules 导入
    ├─ /src/* → 源代码文件
    └─ *.json, *.css 等 → 特殊处理
    ↓
ModuleGraph 缓存检查
    ├─ 命中缓存 → 返回
    └─ 未命中 → esbuild 编译
    ↓
TransformPlugin 流程
    ├─ pre plugins
    ├─ esbuild transform
    └─ post plugins
    ↓
发送给浏览器 (ES Modules)

DevServer入口代码

这里初始化了开发服务器、模块图(缓存系统)、很多中间件(用于拦截实时编译)。

// packages/vite/src/node/server/index.ts

import connect from 'connect'
import { createPluginContainer } from './pluginContainer'

export async function createServer(inlineConfig: InlineConfig = {}) {
  const config = await resolveConfig(inlineConfig, 'serve')
  
  // 创建 Express-like 应用
  const middlewares = connect()
  const httpServer = createHttpServer(middlewares)
  
  // 创建模块图(缓存系统)
  const moduleGraph = new ModuleGraph((url) =>
    pluginContainer.resolveId(url)
  )
  
  // 创建插件容器(执行插件)
  const pluginContainer = await createPluginContainer(config)
  
  // 核心中间件们
  middlewares.use(timeMiddleware)
  middlewares.use(cors)
  middlewares.use(transformMiddleware(server))  // ⭐ 重点
  middlewares.use(servePublicDir)
  middlewares.use(serveRawFs)
  
  const server = {
    middlewares,
    httpServer,
    moduleGraph,
    pluginContainer,
    ws: createWebSocketServer(httpServer),
    // ... 其他属性
  }
  
  return server
}

Transform中间件(请求拦截)

这里是一个很经典的例子,从浏览器发起第一次main.ts请求开始,Vite做了ts文件的转换。

而后续的请求会从main.ts中发起。

// packages/vite/src/node/server/middlewares/transform.ts

export function transformMiddleware(server: ViteDevServer) {
  return async (req: IncomingMessage, res: ServerResponse, next: NextFunction) => {
    if (req.method !== 'GET' || isSkipped(req.url)) {
      return next()
    }

    let url = req.url
    const { pathname, search, hash } = new URL(url, `http://${req.headers.host}`)
    
    // 示例:/src/main.ts?t=123 → /src/main.ts
    url = pathname + search + hash

    try {
      // ⭐ 核心:调用加载和转换
      const result = await transformRequest(url, server, {
        raw: req.headers['accept']?.includes('application/octet-stream'),
      })

      if (result) {
        const type = isDirectCSSRequest(url) ? 'text/css' : 'application/javascript'
        res.setHeader('Content-Type', type)
        res.setHeader('Cache-Control', 'no-cache')
        res.setHeader('ETag', getEtag(result.code))
        
        return res.end(result.code)
      }
    } catch (e) {
      // 错误处理
      if (e.code === 'ENOENT') {
        return next()
      }
      // HMR 错误通知浏览器
      server.ws.send({
        type: 'error',
        event: 'vite:error',
        err: e,
      })
    }

    next()
  }
}

请求转换核心逻辑

这里是核心的源码转换逻辑,基于源码优先从模块缓存表中取,如果没有才走该模块的首次转换,最后会落到缓存中。

// packages/vite/src/node/server/transformRequest.ts

export async function transformRequest(
  url: string,
  server: ViteDevServer,
  options?: TransformOptions,
) {
  // 1️⃣ 获取文件内容 + 元数据
  const { code: raw, map } = await loadRawRequest(url, server)
  
  let code = raw
  const inMap = map

  // 2️⃣ 检查缓存
  const cached = server.moduleGraph.getModuleByUrl(url)
  if (!server.config.command === 'serve' && cached?.transformedCode) {
    return {
      code: cached.transformedCode,
      map: cached.map,
    }
  }

  // 3️⃣ 执行插件转换
  const result = await pluginContainer.transform(code, url)
  if (result) {
    code = result.code
  }

  // 4️⃣ 特殊处理:自动导入注入
  if (!options?.raw) {
    code = injectHelper(code, url)
  }

  // 5️⃣ 缓存结果
  server.moduleGraph.updateModuleInfo(url, {
    transformedCode: code,
    map: result?.map,
  })

  return { code, map: result?.map }
}

加载原始请求(磁盘读写)

而加载和编译源码则是直接通过esbuild能力来实现。

// packages/vite/src/node/server/transformRequest.ts

async function loadRawRequest(url: string, server: ViteDevServer) {
  let id = decodeURIComponent(parseUrl(url).pathname)
  
  // ⭐ 调用插件的 resolveId hook
  const resolveResult = await server.pluginContainer.resolveId(id)
  
  if (resolveResult?.id) {
    id = resolveResult.id
  }

  // 从文件系统读取
  let code = await fs.promises.readFile(id, 'utf-8')
  let map: SourceMap | null = null

  // 如果是 TypeScript,用 esbuild 转译
  if (id.endsWith('.ts') || id.endsWith('.tsx')) {
    const result = await esbuildService.transform(code, {
      loader: 'ts',
      target: 'esnext',
      sourcemap: true,
    })
    code = result.code
    map = result.map
  }

  return { code, map }
}

因此一次完整的编译流程如下:

// 实际请求处理过程

// 浏览器请求:GET /src/main.ts
// ↓
// transformMiddleware 拦截
// ↓
// transformRequest('/src/main.ts', server)
// ↓
// loadRawRequest: 从磁盘读取 main.ts
// ├─ 如果是 .ts,用 esbuild 转译为 .js
// └─ 返回 { code, map }
// ↓
// pluginContainer.transform(code, '/src/main.ts')
// ├─ vue plugin: .vue 转换为 { script, template, style }
// ├─ css-in-js plugin: 处理 styled-components 等
// ├─ import-analysis plugin: 分析依赖,重写为 /@modules/xxx
// └─ ...其他插件
// ↓
// 返回转换后的代码给浏览器
// ↓
// 浏览器 import './main.ts' 
// → 收到 ESM 代码,正常执行

依赖解析重写

Vite如果这样设计,会面临一个问题:请求的数量特别大,导致浏览器首屏时间反而更久。

Vite做了一层设计,将多个模块合并到一个模块,即依赖解析重写,如vue -> @modules/vue?v=xxx

// packages/vite/src/node/plugins/importAnalysis.ts

export function importAnalysisPlugin(): Plugin {
  return {
    name: 'vite:import-analysis',
    
    async transform(code: string, id: string) {
      // 匹配 import/export 语句
      const imports = parse(code) // 用 es-module-lexer 解析
      
      let s = new MagicString(code)
      
      for (const imp of imports) {
        // 例如:import { ref } from 'vue'
        const source = imp.source
        
        if (isRelative(source)) {
          // 相对路径,保持不变
          // import Foo from './foo.ts'
        } else if (isBuiltin(source)) {
          // Node 内置模块,忽略
        } else {
          // ⭐ NPM 包,重写为 /@modules/xxx
          // import { ref } from 'vue'
          // ↓
          // import { ref } from '/@modules/vue?v=xxx'
          
          const resolved = await resolveImport(source)
          const rewritten = `/@modules/${resolved.id}`
          
          s.overwrite(imp.startPos, imp.endPos, 
            `import {...} from '${rewritten}'`
          )
        }
      }
      
      return {
        code: s.toString(),
        map: s.generateMap(),
      }
    }
  }
}

处理node_modules三方库请求

既然将三方库依赖路径重写,那处理对应的请求也需要进行一次路径转换。

// 当浏览器请求 /@modules/vue?v=xxx 时

middlewares.use('/@modules/', async (req, res, next) => {
  const moduleName = req.url.split('/')[2]?.split('?')[0]
  
  // /@modules/vue → node_modules/vue/dist/vue.esm.js
  const modulePath = require.resolve(moduleName, {
    paths: [config.root],
  })
  
  const code = await fs.promises.readFile(modulePath, 'utf-8')
  
  // 继续执行 transform 中间件处理
  // 确保 node_modules 中的代码也被正确处理
  res.end(code)
})

HMR热更新

那按照这样的设计,所有模块只要经过一次编译,就会保存在模块缓存表中,热更新如何处理呢?

Vite做的也比较通俗易懂,当文件系统监听到文件变化,则清除该模块相关缓存信息,然后websocket通知浏览器,Vite client runtime会重新发起相关改动模块的请求。

// packages/vite/src/node/server/hmr.ts

// 当文件变更时
watcher.on('change', async (file) => {
  const url = urlFromFile(file, config.root)
  
  // 1️⃣ 清除模块缓存
  server.moduleGraph.invalidateModule(url)
  
  // 2️⃣ 收集受影响的模块
  const affectedModules = server.moduleGraph.getImporters(url)
  
  // 3️⃣ 通过 WebSocket 通知浏览器
  server.ws.send({
    type: 'update',
    event: 'vite:beforeUpdate',
    updates: affectedModules.map(m => ({
      type: m.isSelfAccepting ? 'js-update' : 'full-reload',
      event: 'vite:beforeUpdate',
      path: m.url,
      acceptedPath: url,
      timestamp: Date.now(),
    }))
  })
})

HMR客户端脚本注入

这就是客户端热更新的核心代码。

// packages/vite/src/client/client.ts

// 注入到每个 HTML 的脚本
const hotModule = import.meta.hot

if (hotModule) {
  hotModule.accept(({ default: newModule }) => {
    // 接收模块更新
    // 执行自定义 HMR 逻辑或完整重载
  })
  
  // 监听服务器推送
  hotModule.on('vite:beforeUpdate', async (event) => {
    if (event.type === 'js-update') {
      // 动态 import 新版本模块
      await import(event.path + `?t=${event.timestamp}`)
    } else {
      // 完整页面刷新
      window.location.reload()
    }
  })
}

因此热更新的流程总结如下:

用户编辑文件保存
    ↓
文件系统监听器检测变化
    ↓
清除 ModuleGraph 缓存
    ↓
WebSocket 通知浏览器
    ↓
浏览器发起新请求(带时间戳)
    ↓
transformMiddleware 拦截
    ↓
loadRawRequest (esbuild 编译 TS/JSX)
    ↓
pluginContainer.transform (执行插件 Vue/CSS 等)
    ↓
返回最新的 ESM 代码
    ↓
浏览器执行 HMR 回调更新页面

结尾

这就是Vite开发环境的核心机制!按需编译+缓存+HMR推送,相比于Webpack,少了最早的整个bundle的构建,自然而然会快非常多,因为Vite在初始化根本就没有build的过程,甚至连main.ts入口文件都是实时编译的。

一次 React 项目 lock 文件冲突修复:从 Hook 报错到 Vite 配置优化

作者 eason_fan
2025年12月12日 11:37

一次 React 项目 lock 文件冲突修复:从 Hook 报错到 Vite 配置优化

在日常开发中,分支合并是高频操作,但稍有不慎就可能引发依赖相关的“连锁反应”。本文记录了一次 rebase main 后因 lock 文件冲突,导致 React Hook 报错的完整排查与解决过程,希望能为遇到类似问题的开发者提供参考。

一、背景:rebase main 引发的“意外”

最近在开发一个基于 React + Vite + Mobx 的项目,为了同步主分支的最新代码,我执行了 git rebase main 操作。过程中遇到了 package-lock.json 冲突,由于当时急于推进开发,我直接手动编辑了冲突文件,保留了双方的依赖配置后提交了代码。

本以为只是简单的文件合并,没想到启动项目后,浏览器控制台直接抛出了一连串报错:

img_v3_02sr_d84a55a3-57fb-4edb-b768-04c950fdd4hu.jpg

报错堆栈指向 mobx-react-lite 中的 useObserver 方法,提示 useRef 无法读取 null 属性。更奇怪的是,这些代码在 rebase 前完全正常,没有任何语法或逻辑修改。

二、问题分析:锁定核心矛盾

1. 排除代码逻辑问题

首先排查业务代码:近期未修改 Hook 调用逻辑,所有 useRefuseState 等 Hooks 均符合“顶层调用”规则,且未在条件、循环或事件处理函数中调用。排除代码本身的问题后,将目光聚焦到依赖和构建配置上。

2. 定位依赖层面问题

根据 React 官方文档提示,Hook 调用异常的三大常见原因:

  1. 违反 Hooks 使用规则(已排除);
  2. React 与渲染器(如 React DOM)版本不匹配;
  3. 项目中存在多个 React 实例。

结合“仅 lock 文件冲突后出现问题”的场景,重点排查后两点:

  • 执行 npm ls react react-dom 查看依赖树,
    • 发现输出中,Terminal#1-14 显示面板同时存在两版 mobx-react-lite :直接依赖 4.1.0 ,通过 mobx-react@9.2.1 间接带入 4.1.1 。这会让它们各自沿着不同的依赖解析路径去找 react ,在多入口/预打包的情况下,很容易把两份 React 打到同一页面。
  • 进一步验证:在打包文件中搜索package.json中的react版本号18.3.1,或者搜索react源码中的ReactCurrentDispatch。可以发现合了代码之后,构建产物两个chunk中都有react。
image.pngimg_v3_02sr_9d69706b-b957-48ec-8144-06036dc021hu.jpg

代码修改前的打包资源

img_v3_02sr_8a12fa19-d116-403a-9e4d-74a9914ce9hu.jpg

img_v3_02sr_9d2d7185-de0b-474a-8b81-b6169247b3hu.jpg

代码修改后的打包资源

3. 追溯问题根源

lock 文件的核心作用是锁定依赖的安装路径和版本。手动合并冲突时,错误保留了不同分支的依赖配置,导致 npm install 时出现依赖嵌套安装:

  • 项目和项目依赖的包都依赖了mobx-react-lite并且版本不同。
  • 打包产物中,两个chunk中各自有一个react
  • 运行时,就产生了两个react实例

React Hooks 的运行依赖单一的调度器实例,当 mobx-react-lite 中的 useObserver 调用嵌套依赖的 React 实例时,会因调度器不匹配导致 Hook 调用失效,进而抛出 useRef 读取 null 的错误。

三、尝试修改:从依赖到配置的逐步排查

1. 重置依赖(首次尝试失败)

首先想到的是修复依赖树,执行以下操作:

# 清除本地依赖和缓存
rm -rf node_modules package-lock.json
npm cache clean --force
# 重新安装依赖
npm install

但重新安装后,npm ls react 仍显示存在嵌套版本。推测是 mobx-react-lite 的依赖声明中未将 React 设为 peerDependency,导致 npm 自动安装兼容版本的嵌套依赖。

2. 强制统一依赖版本(部分缓解)

通过 npm install react@18.2.0 react-dom@18.2.0 --force 强制指定 React 版本,重新安装后嵌套依赖消失。但启动项目后,仍偶尔出现 Hook 报错,排查发现是 Vite 开发环境预构建时未正确识别依赖,导致部分代码仍引用旧版本缓存。

3. 优化 Vite 配置(最终突破)

结合之前对 Vite dedupeoptimizeDeps 的了解,意识到需要从构建层面确保依赖的唯一性和预构建的完整性:

  • resolve.dedupe:强制 Vite 将所有 React 相关依赖解析为根目录版本,杜绝多实例;
  • optimizeDeps.include:强制预构建核心依赖,避免预构建漏检导致的缓存问题。

四、解决问题:最终生效的配置方案

1. 固化 Vite 配置

修改 vite.config.js,添加依赖去重和预构建配置:

import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: [react()],
  resolve: {
    // 去重 React 相关核心依赖,确保单一实例
    dedupe: ['react', 'react-dom', 'mobx-react-lite'],
  },
  optimizeDeps: {
    // 强制预构建核心依赖,避免漏检
    include: ['react', 'react-dom', 'mobx-react-lite'],
    // 预构建阶段再次去重,双重保障
    dedupe: ['react', 'react-dom'],
  },
})

2. 清理缓存并验证

执行 vite --force 强制清除预构建缓存,重新启动项目后:

  • 浏览器控制台无任何 Hook 相关报错;
  • 执行 npm ls react react-dom 仅显示根目录单一版本;
  • 打印 React 实例对比结果为 true,确认多实例问题彻底解决。

五、总结与反思

这次问题的核心是“lock 文件冲突处理不当”,但背后暴露了对依赖管理和构建工具配置的认知缺口。总结几点关键经验:

  1. lock 文件冲突切勿手动修改:遇到 lock 文件冲突时,优先执行 git checkout -- package-lock.json 回滚,再通过 rm -rf node_modules && npm install 重新安装,避免依赖树混乱;
  2. 依赖声明需规范:第三方库应将 React 等核心依赖设为 peerDependency,而非直接依赖,避免嵌套安装;
  3. Vite 配置的“防护作用” :对于 React、Vue 等核心依赖,建议在 Vite 配置中提前设置 dedupeoptimizeDeps.include,从构建层面规避多实例和预构建问题;
  4. 报错排查要结合官方文档:React 官方明确列出了 Hook 调用异常的三大原因,排查时应先对照文档缩小范围,避免盲目尝试。

此次排查过程虽曲折,但也加深了对依赖管理、Vite 构建原理和 React Hooks 运行机制的理解。希望这篇记录能帮助大家在遇到类似问题时少走弯路~

🚀 “踩坑日记”:shadcn + Vite 在 Monorepo 中配置报错

2025年12月11日 20:20

问题介绍

ui.shadcn.com/docs/instal…

按照这个官方文档配置 shadcn + vite 项目后,遇到个错误:

image.png

按照官方文档配置,理应是没有错误的,但是我的项目特殊点就在于是一个 Monorepo 项目。

所以,当你在一个 Monorepo 里使用 TypeScript + ESLint(Flat Config,eslint.config.js)时,常会遇到下面这个解析错误:

Parsing error: No tsconfigRootDir was set, and multiple candidate TSConfigRootDirs are present:
  - /Users/.../packages/SearchChat 
  - /Users/.../packages/SearchChatUI
You'll need to explicitly set tsconfigRootDir in your parser options.
See: https://typescript-eslint.io/packages/parser/#tsconfigrootdireslint

项目背景

  • Monorepo 项目结构示例:
/Users/.../ui-common
├── apps/
│   └── react-jsx/
├── packages/
│   ├── ChatMessage/
│   ├── CustomIcons/
│   ├── DatePicker/
│   ├── DropdownList/
│   ├── EntityUI/
│   └── SearchChatUI/
└── pnpm-workspace.yaml
  • 每个包(例如 packages/SearchChatUI)通常都有自己的 tsconfig.json(含 referencestsconfig.app.json / tsconfig.node.json)、eslint.config.jspackage.json
  • ESLint 在启用类型感知规则(或需要类型信息的配置)时,会通过 @typescript-eslint/parser 加载 TypeScript Program,这需要明确告诉它:以哪个目录为根去解析 projecttsconfig*.json)。

错误现象

  • 在 Monorepo 根或任一包里运行 eslint,报错显示发现多个候选 TSConfigRootDir
  • 这是因为解析器试图自动探测 tsconfig 根目录,但同时看到了多个包的 tsconfig,于是拒绝继续。

原因分析

  • TypeScript-ESLint 的解析器需要一个“根目录”(tsconfigRootDir)来解释你提供的 parserOptions.project(即哪些 tsconfig*.json 参与构建类型信息)。
  • 在 Monorepo 中,如果没有明确为每个包设定独立的 tsconfigRootDir 与对应的 project,解析器会在工作区内“看见”多个包的 tsconfig,从而无法确定到底应该用哪个根,最终报错。

快速修复(针对单个包)

packages/SearchChatUI 为例,给它的 eslint.config.js 增加明确的 parserOptions.tsconfigRootDirparserOptions.project 即可。

// packages/SearchChatUI/eslint.config.js
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
import { fileURLToPath } from 'node:url'
import path from 'node:path'

const __dirname = path.dirname(fileURLToPath(import.meta.url))

export default defineConfig([
  globalIgnores(['dist']),
  {
    files: ['**/*.{ts,tsx}'],
    extends: [
      js.configs.recommended,
      tseslint.configs.recommended,
      reactHooks.configs.flat.recommended,
      reactRefresh.configs.vite,
    ],
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser,
      parserOptions: {
        // 关键设置:指向当前包目录,避免 Monorepo 下的多 tsconfig 混淆
        tsconfigRootDir: __dirname,
        // 指定当前包使用的 tsconfig 列表(路径相对于 tsconfigRootDir)
        project: [
          './tsconfig.json',
          './tsconfig.app.json',
          './tsconfig.node.json',
        ],
        sourceType: 'module',
      },
    },
  },
])

验证:

  • 进入包目录运行 npm run lint(保证命令在包内执行)
  • 预期不再出现 Parsing error

在 Monorepo 根统一配置的做法(推荐)

如果你倾向于在根目录放一个统一的 eslint.config.js,可以使用 “按包 override” 的方式,让每个包都明确自己的 tsconfigRootDirproject

示例(伪代码,按需调整包路径):

// eslint.config.js at workspace root
import { defineConfig } from 'eslint/config'
import globals from 'globals'
import tseslint from 'typescript-eslint'
import js from '@eslint/js'
import { fileURLToPath } from 'node:url'
import path from 'node:path'

const rootDir = path.dirname(fileURLToPath(import.meta.url))
const pkg = (dir) => path.join(rootDir, 'packages', dir)

export default defineConfig([
  {
    files: ['packages/SearchChatUI/**/*.{ts,tsx}'],
    extends: [js.configs.recommended, tseslint.configs.recommended],
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser,
      parserOptions: {
        tsconfigRootDir: pkg('SearchChatUI'),
        project: [
          path.join(pkg('SearchChatUI'), 'tsconfig.json'),
          path.join(pkg('SearchChatUI'), 'tsconfig.app.json'),
          path.join(pkg('SearchChatUI'), 'tsconfig.node.json'),
        ],
        sourceType: 'module',
      },
    },
  },
  {
    files: ['packages/SearchChat/**/*.{ts,tsx}'],
    extends: [js.configs.recommended, tseslint.configs.recommended],
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser,
      parserOptions: {
        tsconfigRootDir: pkg('SearchChat'),
        project: [
          path.join(pkg('SearchChat'), 'tsconfig.json'),
          path.join(pkg('SearchChat'), 'tsconfig.app.json'),
          path.join(pkg('SearchChat'), 'tsconfig.node.json'),
        ],
        sourceType: 'module',
      },
    },
  },
  // 为其他包继续添加 overrides...
])

要点:

  • 每个包的 override 都拥有自己的 tsconfigRootDir
  • project 数组中的路径要基于该包目录。
  • 保持按包运行 eslint .(或通过 workspace 脚本定位到包)能减少路径解析混乱。

常见坑位与提示

  • project 路径必须相对于 tsconfigRootDir,不要写相对于工作区根的路径。
  • 若你使用的是 TypeScript-ESLint 的“类型感知”配置(例如 tseslint.configs.recommendedTypeChecked 或启用了需要类型信息的规则),一定要提供 tsconfigRootDirproject
  • 如果你不需要类型感知规则(为了更快的性能),可以只用非 type-checked 的推荐集,省略 project(但要权衡规则能力):
    extends: [tseslint.configs.recommended] // 非类型感知
    // 不设置 parserOptions.project
    
  • 在包内运行 npm run linteslint .)比在根随意运行更可控。
  • ESM 环境下,要用 fileURLToPath(import.meta.url) 获取当前文件路径来计算 __dirname

验证步骤

  1. 在目标包目录执行:
    • npm run lint
  2. 确认不再出现 “No tsconfigRootDir was set … multiple candidate TSConfigRootDirs …” 的错误。
  3. 如果还有包报同样错误,逐个为它们的配置添加 tsconfigRootDirproject

性能与类型感知

  • 类型感知规则需要构建 TypeScript Program,解析器会加载并分析 project 指定的 tsconfig;在大 Monorepo 中这可能较慢。
  • 推荐做法:
    • 只有在确实需要类型规则的包上开启 project
    • 使用按包 override 控制范围。
    • 结合 CI 分层执行(先非类型感知快速检查,再在关键包跑类型感知规则)。

小结

这个报错本质是 Monorepo 环境下 “类型规则需要明确上下文” 的提醒。只要为每个包设定清晰的 tsconfigRootDirproject,ESLint 就能准确地获取类型信息并稳定工作。按包划分 override 是根级统一配置的好方式;而在包内独立配置则更为直觉。


参考链接

webpack和vite区别及原理实现

作者 光影少年
2025年12月9日 17:52

WebpackVite 都是用于构建现代前端应用的构建工具,它们在原理和实现上有显著的区别。下面我将详细比较它们的异同,帮助你了解两者的工作原理以及各自的优势。


一、Webpack 和 Vite 的核心区别

特性 Webpack Vite
构建速度 较慢,特别是大型项目 快,几乎是即时的
构建原理 通过打包所有资源,生成最终的 bundle 采用按需编译,利用浏览器原生支持 ES 模块
开发模式 一开始就进行全部的打包,编译速度较慢 通过浏览器原生支持 ES Modules,只有请求的模块才会被处理
构建产物 生成一个或多个 bundle 文件 基于 ES Module 按需加载,不同于 Webpack 完整的打包
支持类型 支持所有 JavaScript,CSS,图片,字体等 主要支持 ES 模块,针对现代浏览器优化
使用体验 配置复杂,适用于各种需求和优化 配置简单,适合快速开发,但功能不如 Webpack 灵活

二、Webpack 原理和实现

1. 传统的打包工具

Webpack 是一个 模块打包器,它将所有的静态资源(JavaScript、CSS、图片等)当作模块处理,并生成一个或多个 bundle 文件,最终这些文件将被浏览器加载。

2. 打包过程:

Webpack 的打包过程主要包含以下几个阶段:

  1. 解析阶段(Parsing)

    • Webpack 从入口文件(entry)开始,递归地解析每一个依赖,生成依赖图。
    • 在解析时,Webpack 会调用 loader 对不同类型的文件进行预处理(如 Babel 转译、Sass 编译等)。
  2. 构建阶段(Building)

    • Webpack 会通过 loaderplugin 处理所有模块,生成最终的 AST(抽象语法树)
    • 使用 module bundling 将所有模块合并成一个或多个文件(bundle)。
  3. 优化阶段(Optimization)

    • Webpack 会对生成的 bundle 进行优化,如:分割代码(Code Splitting)、压缩(Terser)等。
  4. 输出阶段(Output)

    • 最终将 bundle 输出到指定的目录,并生成相应的文件供浏览器使用。

3. Webpack 需要时间打包所有资源

由于 Webpack 会将所有资源都打包成一个或多个文件,所以当你做 webpack --mode development 命令时,它必须编译所有文件,这就导致开发过程中启动时间较长。


三、Vite 原理和实现

1. 基于浏览器原生支持的 ES Modules

Vite 的核心原理是利用浏览器原生支持 ES Modules,它并不像 Webpack 那样进行完整的打包,而是通过 按需加载 来提高构建速度。

2. Vite 开发流程:

Vite 的开发过程分为两个阶段:

开发阶段:
  1. 按需编译

    • 当你启动 Vite 时,它不会一次性打包整个项目,而是仅对 首次请求的模块 进行编译和服务。比如,只有用户第一次访问某个页面时,Vite 才会编译该页面依赖的 JavaScript 和 CSS。
  2. 热模块替换(HMR)

    • Vite 提供了 即时的热模块替换,当你在开发过程中修改了某个模块,Vite 会只编译并替换该模块,而不是重新打包整个项目。这大大提高了开发体验。
构建阶段:
  1. 生产构建(build)

    • 在生产环境下,Vite 使用 Rollup(一个现代的 JavaScript 打包工具)进行最终的打包,将所有模块合并成一个优化过的 bundle,进行代码拆分,压缩等优化,生成最终的静态文件。

3. 不需要一直打包全部资源

Vite 的按需编译和快速响应机制,使得开发过程非常迅速。只有在页面访问时,才会处理该页面的依赖,避免了 Webpack 那种完全打包的性能消耗。


四、Webpack 与 Vite 优缺点对比

特性 Webpack Vite
构建速度 较慢(尤其是大型项目时) 极快,尤其是冷启动和热更新
配置复杂性 配置较为复杂,需要处理许多细节 配置简单,开箱即用,少配置即可
开发体验 开发中每次更改都会触发完整编译 热更新速度快,修改后的内容即时反应
支持的功能 功能强大,支持的插件丰富,几乎无所不包 适合现代前端开发,特性较为简洁和聚焦
构建产物 生成一个或多个较大的 bundle 生成多个按需加载的小文件
适用场景 适合中大型复杂项目,支持更多自定义需求 适合中小型项目、现代前端框架(如 React/Vue)

五、总结

  1. Webpack:

    • 适用于复杂的前端项目,支持插件和加载器的灵活扩展。
    • 在开发时,启动和热更新较慢,尤其是大型项目。
    • 配置复杂,需要更多的手动配置来实现项目定制。
  2. Vite:

    • 更适合现代前端开发,特别是对开发速度和用户体验有高要求的场景。
    • 使用浏览器原生的 ES Modules 来实现按需编译和即时热更新,开发体验极佳。
    • 适用于现代前端框架(如 Vue、React),并在生产环境中使用 Rollup 进行高效构建。

📌 推荐场景:

  • Webpack 适合 大型、复杂的前端项目,尤其是有多种技术栈、框架,或者需要更多自定义构建的项目。
  • Vite 更适合 快速开发、现代化前端应用,尤其是小型或中型项目,或者想要享受极速开发体验的团队。
❌
❌