当前位置: 首页 > news >正文

前端梳理体系从常问问题去完善-工程篇(webpack,vite)

以前觉得入秋了,是风的温度降了,现在觉得入秋,是风吹入了万千思绪。报纸上说,生活是部压榨机,把人榨成了渣子,但人本身是压榨机中的头号零件。#人生海海

在这里插入图片描述

webpack实现原理

记忆点:

核心功能是将项目中的多个模块(JS、CSS、图片等)按照依赖关系打包成可在浏览器运行的静态资源。其实现原理可概括为:通过 “解析依赖→处理模块→合并代码→输出资源” 的流程,将分散的模块构建为最终产物,并支持通过 loader 和插件扩展功能。

三大概念:模块,chunk,bundble.

流程:

  1. 初始化:读取配置,创建 Compiler 实例,加载插件。

  2. 编译准备:创建 Compilation 实例,开始构建。

  3. 模块解析:

    • 解析入口 index.js,通过 babel-loader 转换 ES6 语法。
    • 解析 AST 发现依赖 utils.js,递归处理 utils.js
  4. Chunk 生成index.js + utils.js 组成一个入口 Chunk。

  5. 优化:删除未使用代码,压缩 JS。

  6. 输出:将 Chunk 渲染为 main.xxx.js,写入 dist 目录。

Webpack 是前端最主流的模块打包工具之一,核心功能是将项目中的多个模块(JS、CSS、图片等)按照依赖关系打包成可在浏览器运行的静态资源。其实现原理可概括为:通过 “解析依赖→处理模块→合并代码→输出资源” 的流程,将分散的模块构建为最终产物,并支持通过 loader 和插件扩展功能

一、核心概念铺垫

理解 Webpack 原理前,需先明确三个核心概念:

  • 模块(Module):项目中所有的源文件(JS、CSS、图片等)都被视为模块,Webpack 能处理各种类型的模块。
  • Chunk:模块处理过程中形成的 “中间代码块”,通常由多个关联模块组合而成(如入口模块及其依赖的所有模块)。
  • Bundle:最终输出到磁盘的文件(如 main.jsstyle.css),一个 Chunk 通常对应一个 Bundle(特殊场景下可能拆分)。

二、整体工作流程

Webpack 的构建过程是一个线性的生命周期流程,从读取配置到输出产物,可分为以下 6 个核心阶段:

初始化 → 编译准备 → 模块解析与依赖收集 → Chunk 生成 → 优化 → 输出

1. 初始化阶段:创建 Compiler 实例

  • 作用:读取并合并配置(默认配置 + 用户配置 webpack.config.js),初始化核心对象。

  • 细节

    • 解析命令行参数(如 --mode production)和配置文件,生成最终配置对象。
    • 创建 Compiler 实例(全局唯一,贯穿整个构建生命周期),包含配置、插件钩子、输出方法等核心信息。
    • 加载并执行所有插件的 apply 方法(插件通过此阶段注册生命周期钩子)。

2. 编译准备阶段:启动构建

  • 作用:准备编译环境,创建 Compilation 实例。

  • 细节

    • 触发 compiler.hooks.run 钩子(开始构建),准备内存文件系统(memfs)用于临时存储中间结果。
    • 创建 Compilation 实例(每次构建对应一个实例,如 watch 模式下的重新构建会创建新实例),负责具体的模块处理、依赖分析、Chunk 生成等工作。

3. 模块解析与依赖收集:核心阶段

从入口文件(entry)开始,递归解析所有模块及其依赖,形成 “依赖关系图”,是 Webpack 最核心的环节。

(1)入口模块处理

  • 根据配置的 entry(如 ./src/index.js),找到入口模块,开始解析。

(2)模块解析(Resolver)

  • 路径解析:根据 resolve 配置(如 extensionsalias),将模块引用路径(如 import './utils')解析为真实文件路径。
  • 类型判断:根据文件后缀(如 .js.css.png)判断模块类型,确定使用哪些 loader 处理。

(3)模块转换(Loader 链)

  • Webpack 本身只能处理 JS/JSON 模块,其他类型模块(CSS、TS 等)需通过 loader 转换为 JS 模块
  • 过程:按配置的 module.rules 顺序执行 loader(从后往前执行),例如处理 .tsx 文件:ts-loader(转 TS 为 JS)→ babel-loader(转 ES6+ 为 ES5)。
  • 结果:非 JS 模块被转换为 “可执行的 JS 模块”(例如 CSS 模块会被转换为通过 style 标签插入样式的 JS 代码)。

(4)依赖分析(AST 解析)

  • 对转换后的 JS 模块,通过 AST(抽象语法树)解析(使用 acorn 库),识别 importrequireexport 等依赖声明。
  • 递归处理:对解析出的依赖模块(如 ./utils),重复执行 “模块解析→转换→依赖分析” 流程,直到所有依赖都被处理。
  • 最终形成完整的依赖关系图compilation.modules 存储所有模块,module.dependencies 存储模块的依赖)。

4. Chunk 生成:组织模块为代码块

  • 作用:根据依赖关系,将模块分组为 Chunk(代码块)。

  • 规则:

    • 入口模块及其所有依赖默认组成一个 Chunk(入口 Chunk)。
    • 动态导入(import('./xxx'))会触发 “代码分割”,生成新的 Chunk(异步 Chunk)。
    • 通过 splitChunks 配置可自定义 Chunk 分割规则(如提取公共依赖为单独 Chunk)。
  • 结果:compilation.chunks 存储所有 Chunk,每个 Chunk 包含其对应的模块列表。

5. 优化阶段:优化 Chunk 内容

对生成的 Chunk 进行优化,减小体积、提升性能,核心优化手段包括:

  • Tree-shaking:基于 ESM 的静态分析,删除未被使用的代码(dead code)。
  • 代码压缩:通过 TerserPlugin 压缩 JS,CssMinimizerPlugin 压缩 CSS。
  • Chunk 合并 / 拆分:合并小 Chunk 减少请求数,拆分大 Chunk 实现并行加载。
  • 作用域提升(Scope Hoisting):通过 ModuleConcatenationPlugin 将多个模块的代码合并到一个函数作用域中,减少函数声明和闭包开销。

6. 输出阶段:生成最终产物

  • 作用:将优化后的 Chunk 渲染为物理文件,写入输出目录(output.path)。

  • 细节:

    • 每个 Chunk 被渲染为一个或多个 Bundle 文件(根据 output.filename 规则命名,如 [name].[contenthash].js)。
    • 处理资源路径:对图片、字体等静态资源,生成哈希文件名(避免缓存问题),并替换代码中对应的引用路径。
    • 触发 emit 钩子(输出前)和 done 钩子(输出完成),插件可在此阶段修改输出内容(如 HtmlWebpackPlugin 生成引用 Bundle 的 HTML)。

三、核心扩展机制:Loader 与 Plugin

Webpack 的强大之处在于其扩展性,而扩展性依赖两大机制:

1. Loader:处理非 JS 模块

  • 本质:是一个函数,接收源文件内容作为参数,返回转换后的内容。
  • 作用:将非 JS 模块转换为 Webpack 可处理的 JS 模块(例如 css-loader 将 CSS 转换为 JS 模块,url-loader 将图片转换为 Base64 或路径引用)。
  • 执行顺序:在 module.rules 中从后往前执行(如 use: ['style-loader', 'css-loader'] 中,css-loader 先执行,再传给 style-loader)。

2. Plugin:扩展构建流程

  • 本质:通过 Tapable 钩子系统介入 Webpack 生命周期,实现自定义逻辑(如修改配置、生成文件、优化输出等)。
  • 作用:处理 loader 无法完成的工作(如生成 HTML、清理输出目录、压缩代码等)。
  • 工作方式:通过 compilercompilation 的钩子注册回调,在特定阶段执行逻辑(如 HtmlWebpackPluginemit 阶段生成 HTML 文件)。

四、示例:简化的构建流程

以一个简单项目为例(入口 index.js 依赖 utils.js):

  1. 初始化:读取配置,创建 Compiler 实例,加载插件。

  2. 编译准备:创建 Compilation 实例,开始构建。

  3. 模块解析:

    • 解析入口 index.js,通过 babel-loader 转换 ES6 语法。
    • 解析 AST 发现依赖 utils.js,递归处理 utils.js
  4. Chunk 生成index.js + utils.js 组成一个入口 Chunk。

  5. 优化:删除未使用代码,压缩 JS。

  6. 输出:将 Chunk 渲染为 main.xxx.js,写入 dist 目录。

总结

Webpack 的核心原理是:以入口文件为起点,通过递归解析所有模块的依赖关系,利用 loader 处理非 JS 模块,通过插件扩展构建流程,最终将模块打包为优化后的静态资源。其设计思想是 “一切皆模块”+“插件化架构”,既保证了处理能力的全面性,又提供了极强的扩展性,使其成为前端工程化的核心工具之一。

webpack构建流程

记忆点:

简化流程总结

  1. 初始化:合并配置 → 创建 Compiler → 注册插件。
  2. 编译:从入口解析模块 → Loader 处理文件 → 递归收集依赖 → 生成依赖图谱。
  3. 输出:分割 Chunk → 优化代码 → 生成最终文件 → 完成构建。

理解 Webpack 构建流程有助于排查打包问题(如依赖解析错误、Loader 配置问题)和自定义优化策略

Webpack 的构建流程是一个从读取配置、处理文件到输出最终资源的完整过程,核心是将多个模块(文件)打包成一个或多个 bundle。以下是其核心流程的详细解析:

一、初始化阶段(Initialization)

  1. 读取配置
    • 解析 webpack.config.js(或通过 CLI 参数指定的配置),合并默认配置与用户配置,生成最终的配置对象。
    • 处理环境变量(如 mode: 'development''production'),应用对应模式的默认优化策略。
  2. 创建 Compiler 实例
    • 根据最终配置创建 Compiler 对象,它是 Webpack 构建的核心调度者,负责统筹整个构建流程。
    • 注册配置中定义的插件(Plugins),插件可通过钩子(hooks)介入构建的各个阶段。

二、编译阶段(Compilation)

此阶段是 Webpack 构建的核心,主要完成模块解析、依赖收集和代码转换。

  1. 入口处理(Entry)

    • 从配置的 entry 入口文件(如 ./src/index.js)开始,将其视为第一个待处理的模块。
  2. 模块解析(Module Resolution)

    • 对每个模块进行路径解析:根据 resolve 配置(如 extensions: ['.js', '.ts'])查找模块文件。
    • 处理别名(alias)、模块查找路径(modules)等配置,确定模块的真实路径。
  3. 模块加载(Module Loading)

    • 根据模块的文件类型(如

      .js
      
      .css
      
      .ts
      

      ),匹配对应的 Loader 进行处理:

      • 例如,.ts 文件通过 ts-loader 转换为 JavaScript;
      • .css 文件通过 css-loader 处理 @importurl(),再通过 style-loader 注入到 DOM。
    • Loader 是 “从右到左” 执行的(如 use: ['style-loader', 'css-loader'] 实际执行顺序为 css-loaderstyle-loader)。

  4. 依赖收集(Dependency Graph)

    • 解析处理后的模块代码,提取其中的依赖(如 importrequire 语句)。
    • 递归处理这些依赖模块,重复步骤 2-4,最终形成一个包含所有模块的依赖图谱(Dependency Graph)

三、输出阶段(Emission)

将处理后的模块按照依赖关系打包成最终的输出文件(bundle)。

  1. 模块分块(Chunking)

    • 根据

      optimization.splitChunks
      

      等配置,将依赖图谱中的模块分割为多个 Chunk(代码块):

      • 入口 Chunk:对应 entry 配置的入口文件;
      • 异步 Chunk:通过 import() 动态导入的模块会被单独打包;
      • 公共 Chunk:提取多个模块共享的代码(如第三方库 lodash)。
  2. 代码优化(Optimization)

    • 对每个 Chunk 进行优化处理,常见优化包括:
      • 代码压缩(如 TerserPlugin 压缩 JS,CssMinimizerPlugin 压缩 CSS);
      • Tree-shaking:删除未使用的代码(依赖 ES6 模块的静态分析);
      • 作用域提升(scope hoisting):减少函数嵌套,提升运行效率;
      • 哈希文件名(如 [contenthash]):实现缓存策略。
  3. 生成输出文件(Asset Generation)

    • 根据 output 配置(如 path 指定输出目录,filename 指定文件名),将 Chunk 转换为最终的静态资源文件(如 main.jsvendors.js)。
    • 处理非 JS 资源(如图片、字体),通过 file-loaderurl-loader 输出到指定目录,并在代码中替换为正确的引用路径。
  4. 完成构建

    • 所有资源输出完成后,触发 done 钩子,构建流程结束。

四、核心钩子与插件机制

Webpack 的每个阶段都暴露了钩子(由 tapable 库实现),插件可通过钩子介入流程,例如:

  • entryOption:入口配置处理后触发;
  • compile:开始编译时触发;
  • emit:输出文件前触发(可修改输出内容);
  • done:构建完成后触发。

示例HtmlWebpackPlugin 通过 emit 钩子生成包含 bundle 引用的 HTML 文件。

五、简化流程总结

  1. 初始化:合并配置 → 创建 Compiler → 注册插件。
  2. 编译:从入口解析模块 → Loader 处理文件 → 递归收集依赖 → 生成依赖图谱。
  3. 输出:分割 Chunk → 优化代码 → 生成最终文件 → 完成构建。

理解 Webpack 构建流程有助于排查打包问题(如依赖解析错误、Loader 配置问题)和自定义优化策略

webpack 如果不用插件跟loader处理js,那它默认是怎么处理的

Webpack 作为模块打包工具,即使不配置任何 loader 和插件,也会对 JavaScript 模块进行基础处理,其核心逻辑围绕「模块依赖解析」和「代码打包合并」展开,具体默认行为如下:

  1. 仅处理特定文件类型

Webpack 内置了对两种文件类型的解析能力:

  • JavaScript 文件.js.mjs 等)
  • JSON 文件.json

对于其他类型文件(如 .css.ts、图片等),如果没有配置对应的 loader,Webpack 会直接报错,因为它无法识别这些非默认类型。

  1. 解析模块依赖关系

Webpack 的核心功能是分析模块间的依赖关系,构建「依赖图」,无论是否配置 loader,它都会:

  • 识别 import/export(ES 模块语法)和 require()/module.exports(CommonJS 语法),追踪模块间的依赖。
  • 支持相对路径(./xxx)、绝对路径(/xxx)和 node_modules 中的第三方模块(如 import 'lodash')。
  • 自动解析文件扩展名:默认会尝试 .js.json.mjs 等扩展名(可通过 resolve.extensions 配置修改,默认值为 ['.js', '.json', '.mjs'])。
  1. 打包合并为单个 bundle

Webpack 会将所有依赖的 JavaScript 模块合并为一个或多个输出文件(默认是单个),默认行为包括:

  • 输出路径:默认打包到 dist/main.js(可通过 output.pathoutput.filename 配置修改)。

  • 模块封装

    :打包后的代码会用「自执行函数(IIFE)」包裹,通过模块 ID 管理不同模块,避免全局变量污染。例如,每个模块会被封装为一个函数,通过

    __webpack_require__
    

    方法加载,类似:

    (function(modules) {// 模块缓存var installedModules = {};// 模拟 require 函数function __webpack_require__(moduleId) {// ... 加载模块逻辑 ...}// 入口模块执行return __webpack_require__(0);
    })([// 模块数组(每个元素是一个封装后的模块函数)function(module, exports, __webpack_require__) {// 模块 0 的代码(入口模块)},// ... 其他模块 ...
    ]);
    
  1. 不处理语法转换和优化

这是关键区别:默认情况下,Webpack 不会对 JavaScript 代码进行语法转换或优化,具体表现为:

  • 不转换 ES6+ 语法:如箭头函数、const/letclass 等语法会原封不动保留在输出文件中。如果目标浏览器不支持这些语法,会直接报错(需要 babel-loader 等工具处理)。
  • 不压缩代码:输出的 main.js 是未压缩的,包含注释和空格(需要 terser-webpack-plugin 等插件处理)。
  • 不处理循环依赖、模块冗余等问题:仅做基础的依赖合并,不进行代码分割、tree-shaking 等优化(需要配置 mode: 'production' 或相关插件)。

总结

Webpack 的默认处理逻辑是「基础的模块依赖解析 + 合并打包」,仅负责将多个 JavaScript/JSON 模块按依赖关系合并为可执行的 bundle,不涉及语法转换、代码优化或非 JS 类型文件的处理。这些增强功能需要通过 loader(如处理语法的 babel-loader)和插件(如压缩代码的 terser-webpack-plugin)来实现。

热更新原理

记忆点:

  1. 编译阶段:生成热更新相关资源(通过webpack-dev-serverwebpack-dev-middleware配合HotModuleReplacementPlugin
  • 为每个模块添加额外代码,用于后续模块替换逻辑。
  • 生成 “热更新清单文件”,记录本次更新的模块信息和哈希值。
  • 生成 “热更新 chunk”,包含变化模块的最新代码。
  1. 服务端与客户端的通信建立
  • Webpack 的watch 模式会检测到文件变化,触发增量编译。
  • 编译完成后,Webpack 生成新的 “热更新清单” 和 “热更新 chunk”,并通知 WDS 有更新。
  • WDS 通过 WebSocket 向客户端发送更新通知(包含更新清单的 URL)。
  1. 文件变化检测与更新触发
  • Webpack 的watch 模式会检测到文件变化,触发增量编译。
  • 编译完成后,Webpack 生成新的 “热更新清单” 和 “热更新 chunk”,并通知 WDS 有更新。
  • WDS 通过 WebSocket 向客户端发送更新通知。

4. 客户端处理更新

  1. 请求更新资源:通过 HTTP 请求获取 “热更新清单” 和 “热更新 chunk”。
  2. 验证模块依赖:HMR runtime 根据清单分析哪些模块需要更新,并检查依赖链
  3. 替换旧模块
    • 调用旧模块的module.hot.dispose(如有)清理资源。
    • 执行新模块代码,替换模块缓存中的旧模块。
    • 调用新模块的module.hot.accept(如有),通知应用模块已更新,触发业务逻辑刷新(如重新渲染组件)。
  4. 完成更新:若所有模块替换成功,应用在不刷新的情况下呈现新状态;若失败,则降级为全页面刷新。

Webpack 的热更新(Hot Module Replacement,简称 HMR)是开发环境中提升效率的核心特性,它允许在应用运行时更新模块而无需完全刷新页面,从而保留应用状态并加快开发速度。其原理可分为以下几个关键环节:

1. 编译阶段:生成热更新相关资源

当开启 HMR 时(通过webpack-dev-serverwebpack-dev-middleware配合HotModuleReplacementPlugin),Webpack 在编译过程中会:

  • 为每个模块添加额外代码(HMR runtime),用于后续模块替换逻辑。
  • 生成 “热更新清单文件”(manifest,如[hash].hot-update.json),记录本次更新的模块信息和哈希值。
  • 生成 “热更新 chunk”(如[id].[hash].hot-update.js),包含变化模块的最新代码。

2. 服务端与客户端的通信建立

  • 服务端webpack-dev-server(WDS)启动一个 HTTP 服务器提供静态资源,并通过WebSocket与客户端建立长连接,用于实时推送更新通知。
  • 客户端:页面加载时,会注入 WDS 客户端脚本(包含 HMR runtime),通过 WebSocket 监听服务端的更新消息。

3. 文件变化检测与更新触发

当开发者修改文件时:

  • Webpack 的watch 模式会检测到文件变化,触发增量编译(仅重新编译变化的模块,而非全量打包)。
  • 编译完成后,Webpack 生成新的 “热更新清单” 和 “热更新 chunk”,并通知 WDS 有更新。
  • WDS 通过 WebSocket 向客户端发送更新通知(包含更新清单的 URL)。

4. 客户端处理更新

客户端收到更新通知后,执行以下步骤:

  1. 请求更新资源:通过 HTTP 请求获取 “热更新清单” 和 “热更新 chunk”。
  2. 验证模块依赖:HMR runtime 根据清单分析哪些模块需要更新,并检查依赖链(例如,若 A 依赖 B,B 更新则 A 可能也需要更新)。
  3. 替换旧模块
    • 调用旧模块的module.hot.dispose(如有)清理资源。
    • 执行新模块代码,替换模块缓存中的旧模块。
    • 调用新模块的module.hot.accept(如有),通知应用模块已更新,触发业务逻辑刷新(如重新渲染组件)。
  4. 完成更新:若所有模块替换成功,应用在不刷新的情况下呈现新状态;若失败,则降级为全页面刷新。

关键技术点

  • 模块缓存:Webpack 通过module.hot对象管理模块缓存,更新时仅替换变化的模块,避免全量重新加载。
  • 哈希标识:每个编译产物包含唯一哈希值,用于快速识别模块是否变化(清单文件记录变化模块的哈希)。
  • HMR API:开发者可通过module.hot.acceptmodule.hot.dispose等 API 自定义模块更新逻辑(如 React 的react-refresh就是基于此实现组件热更新)。

与自动刷新的区别

  • 自动刷新(Live Reload):修改文件后刷新整个页面,所有状态丢失。
  • 热更新(HMR):仅更新变化的模块,保留应用状态,更新速度更快。

综上,HMR 通过 “增量编译 - WebSocket 通信 - 模块精确替换” 的流程,实现了开发过程中的无刷新更新,大幅提升了前端开发效率。

webpack与rollup与vite的区别是什么?

Webpack、Rollup 和 Vite 都是前端构建工具,但它们的设计理念、适用场景和工作方式有显著区别:

1. Webpack

  • 定位:全能型模块打包工具,适用于复杂应用

  • 核心特点

    • 支持所有模块系统(CommonJS、AMD、ES Modules)
    • 强大的 loader 和 plugin 生态,可处理各种资源(JS、CSS、图片等)
    • 基于依赖图的打包方式,能处理复杂依赖关系
    • 支持代码分割、懒加载等高级特性
  • 适用场景:大型应用、多页面应用、需要处理多种资源类型的项目

  • 缺点:配置复杂,启动和热更新速度相对较慢

2. Rollup

  • 定位:专注于 JavaScript 库打包的工具

  • 核心特点

    • 基于 ES Modules,支持 Tree-shaking(消除未使用代码)
    • 输出代码简洁、体积小,更接近原生 ES 模块
    • 配置相对简单,专注于 JavaScript 打包
  • 适用场景:JavaScript 库、框架的开发(如 Vue、React 等)

  • 缺点:处理非 JS 资源需要额外配置,对 CommonJS 支持不够友好

3. Vite

  • 定位:新一代前端构建工具,专注于开发体验

  • 核心特点

    • 开发环境使用原生 ES Modules,无需打包,启动速度极快
    • 热更新性能优异,基于模块依赖图的精确更新
    • 生产环境使用 Rollup 打包,兼顾开发体验和生产性能
    • 内置对 TypeScript、JSX、CSS 等的支持
  • 适用场景:现代前端应用开发,特别是单页应用

  • 缺点:对传统浏览器兼容性较差,生态相对较新

总结选择建议:

  • 开发库 / 框架:优先选择 Rollup
  • 开发大型应用:Webpack 或 Vite(Vite 开发体验更好)
  • 追求极致开发体验:选择 Vite
  • 需要处理复杂资源和场景:选择 Webpack

近年来,Vite 凭借其出色的开发体验获得了广泛关注,而 Webpack 依然是最成熟稳定的选择,Rollup 则在库开发领域保持优势。

如何使用webpack做优化产出代码?

使用 Webpack 优化产出代码(通常指生产环境构建产物)的核心目标是:减小包体积、提高加载速度、优化缓存效率。以下是常用的优化手段及具体配置方案:

1. 代码分割(Code Splitting)

将代码拆分为多个小块(chunk),实现按需加载或并行加载,减少初始加载体积。

(1)自动分割公共代码

通过 splitChunks 配置提取重复依赖(如第三方库、公共组件):

// webpack.config.js
module.exports = {optimization: {splitChunks: {chunks: 'all', // 对所有类型的 chunk(初始、异步)生效cacheGroups: {vendor: { // 提取 node_modules 中的第三方库test: /[\\/]node_modules[\\/]/,name: 'vendors', // 输出文件名:vendors.jschunks: 'all',priority: 10 // 优先级高于默认分组},common: { // 提取公共业务代码(被多次引用)minChunks: 2, // 至少被引用 2 次才会被提取name: 'common',chunks: 'all',priority: 5}}}}
};

(2)动态导入(懒加载)

通过 import() 语法手动分割代码,实现按需加载(如路由懒加载):

// 路由懒加载示例(React/Vue 通用)
const Home = () => import(/* webpackChunkName: "home" */ './pages/Home');
const About = () => import(/* webpackChunkName: "about" */ './pages/About');// 打包后会生成 home.js、about.js,仅在访问对应路由时加载

2. Tree-shaking(消除未使用代码)

移除代码中未被引用的部分(“死代码”),减小包体积。

配置要求:

  • 必须使用 ES Modulesimport/export),不支持 CommonJS(require)。
  • 生产环境(mode: 'production')下自动启用(Webpack 会默认集成 TerserPlugin 进行摇树)。

补充配置:

// package.json(确保不会被 babel 转译为 CommonJS)
{"sideEffects": false, // 标记项目无副作用代码(可选,进一步优化)// 若有副作用文件(如全局样式),需指定:"sideEffects": ["*.css", "*.less"]
}// webpack.config.js
module.exports = {mode: 'production', // 生产模式自动启用 tree-shakingoptimization: {usedExports: true // 标记未使用的导出,供 Terser 删除}
};

3. 压缩代码(JS/CSS/HTML)

减小文件体积,加速传输。

(1)JS 压缩

Webpack 5 内置 TerserPlugin(生产环境默认启用),可自定义配置:

const TerserPlugin = require('terser-webpack-plugin');module.exports = {optimization: {minimizer: [new TerserPlugin({parallel: true, // 多进程压缩,加速构建terserOptions: {compress: {drop_console: true, // 移除 console.logdrop_debugger: true // 移除 debugger}}})]}
};

(2)CSS 压缩

需配合 MiniCssExtractPlugin(提取 CSS 到单独文件)和 CssMinimizerPlugin

const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin');module.exports = {module: {rules: [{test: /\.css$/i,use: [MiniCssExtractPlugin.loader, 'css-loader'] // 提取 CSS 而非内嵌到 JS}]},plugins: [new MiniCssExtractPlugin({filename: 'css/[name].[contenthash:8].css' // 输出路径+文件名(带哈希)})],optimization: {minimizer: ['...', // 保留默认的 JS 压缩器new CssMinimizerPlugin() // 压缩 CSS]}
};

(3)HTML 压缩

若使用 HtmlWebpackPlugin,可开启压缩:

const HtmlWebpackPlugin = require('html-webpack-plugin');module.exports = {plugins: [new HtmlWebpackPlugin({template: './src/index.html',minify: {collapseWhitespace: true, // 折叠空白removeComments: true, // 移除注释removeRedundantAttributes: true // 移除冗余属性}})]
};

4. 优化资源(图片、字体等)

(1)小资源内联

通过 Webpack 5 的 asset 模块,将小图片 / 字体转为 Base64 内嵌到代码,减少 HTTP 请求:

module.exports = {module: {rules: [{test: /\.(png|jpe?g|gif|svg)$/i,type: 'asset',parser: {dataUrlCondition: {maxSize: 10 * 1024 // 小于 10KB 的资源转为 Base64}},generator: {filename: 'images/[name].[hash:8][ext]' // 大资源输出路径}}]}
};

(2)图片压缩

使用 image-webpack-loader 压缩图片:

module.exports = {module: {rules: [{test: /\.(png|jpe?g|gif)$/i,use: [{ loader: 'file-loader' }, // 或 'url-loader'{loader: 'image-webpack-loader',options: {mozjpeg: { quality: 80 }, // JPEG 压缩optipng: { enabled: false }, // 禁用 PNG 压缩(可选)pngquant: { quality: [0.6, 0.8] } // PNG 压缩质量范围}}]}]}
};

5. 缓存策略优化

通过文件名哈希(hash)让浏览器缓存有效资源,仅在内容变化时更新缓存。

module.exports = {output: {filename: 'js/[name].[contenthash:8].js', // 内容哈希(仅内容变化时更新)chunkFilename: 'js/[name].[contenthash:8].chunk.js'},plugins: [new MiniCssExtractPlugin({filename: 'css/[name].[contenthash:8].css' // CSS 也使用内容哈希})]
};
  • contenthash:基于文件内容生成哈希(推荐,仅内容变化时哈希改变)。
  • hash:基于整个项目构建生成哈希(不推荐,任何文件变化都会改变)。

6. 排除不必要的依赖

通过 externals 配置排除无需打包的第三方库(如通过 CDN 引入):

module.exports = {externals: {react: 'React', // 全局变量 React 对应依赖 react'react-dom': 'ReactDOM'}
};// HTML 中通过 CDN 引入:
// <script src="https://cdn.jsdelivr.net/npm/react@18/umd/react.production.min.js"></script>

7. 分析打包结果

使用 webpack-bundle-analyzer 可视化分析包体积,定位大文件或冗余依赖:

const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;module.exports = {plugins: [new BundleAnalyzerPlugin() // 构建后自动打开分析页面]
};

8. 生产环境专属配置

最终,建议将开发环境与生产环境配置分离,生产环境启用以下核心优化:

// webpack.prod.js
module.exports = {mode: 'production', // 自动启用 tree-shaking、压缩等devtool: 'source-map', // 生成高质量 source-map(可选,用于调试)optimization: {splitChunks: { /* 代码分割配置 */ },minimizer: [ /* 压缩配置 */ ]},// 其他资源、缓存配置...
};

总结

Webpack 优化的核心思路是:“拆包(减小单文件体积)→ 减码(删除无用代码)→ 压缩(减小文件大小)→ 缓存(减少重复加载)”。通过结合代码分割、Tree-shaking、资源优化和缓存策略,可显著提升生产环境代码的加载性能。

webpack插件底层的实现的原理是什么?

记忆:事件流机制和Tapable库

基于 Tapable 的钩子系统,通过 apply 方法将自定义逻辑注册到 Webpack 构建的生命周期钩子中,在特定阶段介入并修改构建过程,从而实现功能扩展。

Webpack 插件的底层实现原理,核心是基于 事件流机制(Event-driven Architecture)Tapable 库的钩子系统,通过介入 Webpack 构建的生命周期,实现对构建过程的扩展和自定义。

一、核心基础:Tapable 库

Webpack 内部通过 Tapable(Webpack 团队开发的一个事件触发与监听库)来管理所有构建过程中的 “钩子(Hook)”。这些钩子本质上是不同生命周期阶段的事件触发点,插件通过 “注册” 这些钩子的回调函数,实现对构建过程的介入。

Tapable 提供了多种钩子类型(如同步、异步、并行、串行等),适配不同的场景:

  • SyncHook:同步钩子,按顺序执行所有回调,不关心返回值。
  • AsyncSeriesHook:异步串行钩子,前一个回调完成后才执行下一个。
  • AsyncParallelHook:异步并行钩子,所有回调同时执行。
  • 其他如 SyncBailHook(同步熔断)、AsyncSeriesWaterfallHook(异步串行传值)等。

插件的核心工作,就是通过 Tapable 提供的 tap(注册同步回调)、tapAsync(注册异步回调)、tapPromise(注册 Promise 回调)等方法,将自定义逻辑 “挂载” 到 Webpack 的特定钩子上。

二、插件的基本结构

一个 Webpack 插件通常是一个带有 apply 方法的类(或函数),其基本结构如下:

class MyPlugin {// 构造函数可选,用于接收插件配置constructor(options) {this.options = options;}// Webpack 初始化时会调用 apply 方法,并传入 compiler 对象apply(compiler) {// 在特定钩子上注册回调compiler.hooks.someHook.tap('MyPlugin', (params) => {// 自定义逻辑:如修改参数、添加资源、打印日志等});}
}// 使用时需实例化
module.exports = MyPlugin;

核心是 apply 方法:Webpack 启动时会自动调用插件的 apply 方法,并传入 compiler 对象(Webpack 全局构建实例),插件通过 compiler 访问所有钩子。

三、核心对象:compiler 与 compilation

插件的逻辑主要围绕两个核心对象展开:

1. compiler 对象

  • 含义:代表 Webpack 编译的 “全局上下文”,整个 Webpack 生命周期中只有一个 compiler 实例

  • 作用:包含 Webpack 配置(compiler.options)、全局钩子(如 entryOptionrundone 等),以及启动构建、输出资源的核心方法。

  • 常用钩子:

    • entryOption:入口配置处理完成后触发。
    • run:开始执行构建时触发。
    • emit:即将输出资源到文件系统前触发(可在此阶段修改输出内容)。
    • done:构建完成后触发。

2. compilation 对象

  • 含义:代表一次具体的构建过程(如首次构建、watch 模式下的重新构建),每次构建都会创建一个新的 compilation 实例。

  • 作用:包含当前构建的所有模块(modules)、代码块(chunks)、资源(assets)等信息,是处理模块转换、代码生成的核心对象。

  • 常用钩子:

    • compile:开始编译时触发(compilation 实例创建前)。
    • compilation:compilation 实例创建后触发(可通过此钩子访问 compilation 对象)。
    • optimize:优化阶段触发(如 Tree-shaking、代码压缩)。
    • assetEmitted:资源输出到文件系统后触发。

四、插件工作流程(以 “构建完成后打印日志” 为例)

  1. 插件实例化:在 Webpack 配置的 plugins 数组中,插件被实例化(如 new MyPlugin(options))。
  2. 调用 apply 方法:Webpack 启动时,遍历 plugins 数组,调用每个插件的 apply 方法,并传入 compiler 对象。
  3. 注册钩子回调:插件在 apply 方法中,通过 compiler.hooks.done.tap(...) 注册一个回调到 done 钩子(构建完成后触发)。
  4. 触发钩子执行:当 Webpack 构建流程执行到 “完成” 阶段时,done 钩子被触发,插件的回调函数执行(打印日志)。
// 示例:构建完成后打印成功日志的插件
class SuccessLogPlugin {apply(compiler) {// 注册到 "done" 钩子(构建完成后触发)compiler.hooks.done.tap('SuccessLogPlugin', (stats) => {console.log('构建成功!耗时:', stats.endTime - stats.startTime, 'ms');});}
}// 使用:在 webpack.config.js 中
module.exports = {plugins: [new SuccessLogPlugin()]
};

五、插件的核心能力

通过介入不同阶段的钩子,插件可以实现几乎所有自定义需求:

  • 修改配置:在 entryOption 钩子中调整入口配置。
  • 处理模块:在 compilation.hooks.module.tap 中修改模块内容(如添加注释、转换语法)。
  • 生成资源:在 emit 钩子中添加自定义文件(如 dist/version.txt)。
  • 优化输出:在 optimize 阶段修改代码块(chunks),实现自定义压缩、分割逻辑。
  • 错误处理:在 failed 钩子中捕获构建错误并自定义提示。

总结

Webpack 插件的底层原理可概括为:
基于 Tapable 的钩子系统,通过 apply 方法将自定义逻辑注册到 Webpack 构建的生命周期钩子中,在特定阶段介入并修改构建过程,从而实现功能扩展

这种事件驱动的设计,让 Webpack 具备了极强的灵活性 —— 从简单的日志输出到复杂的代码转换(如 html-webpack-plugin 生成 HTML、mini-css-extract-plugin 提取 CSS),都可以通过插件实现。

如何使用 Webpack 持久化缓存大幅提升构建性能?

Dependency Graph:如何管理模块间依赖

https://www.yuque.com/ergz/web/rit6w0zu8fsgferd

Chunk 三种产物的打包逻辑

https://www.yuque.com/ergz/web/dsq2n0c4qidw2m9s

Runtime:模块编译打包及运行时逻辑

https://www.yuque.com/ergz/web/ch172prqvva0eu85

Sourcemap:源码映射原理与应用技巧

https://www.yuque.com/ergz/web/yxotydxk61vndqf2

loader 与pluggin的区别,执行时机是什么

一、Webpack 中 Plugin 和 Loader 的核心区别

对比维度LoaderPlugin
作用处理特定类型的文件(如转换、编译)扩展 Webpack 功能(如打包优化、资源管理)
工作阶段模块解析阶段(处理单个文件)整个构建周期(从开始到结束)
执行顺序从右到左(或从下到上)执行按注册顺序执行(plugins 数组中的顺序)
输入输出输入文件内容,输出转换后的内容不直接处理文件,而是通过钩子介入构建流程
典型场景Babel 编译 JSX、CSS 加载器处理样式文件代码压缩、HTML 文件生成、环境变量注入
使用方式module.rules 中配置plugins 数组中注册

示例对比:

在 Webpack 构建流程中,Loader 和 Plugin 的执行时机有明显区别:

Loader 的执行时机

Loader 是在 模块解析阶段 执行的,具体来说:

  1. 当 Webpack 开始处理某个模块(如 import './style.css')时
  2. 会根据模块的文件类型(通过扩展名判断)匹配对应的 Loader 规则
  3. 按照配置中的 use 数组顺序 从后往前 执行 Loader 链
  4. 每个 Loader 处理完后将结果传递给下一个 Loader,最终返回 JavaScript 代码给 Webpack 进一步处理

简单说,Loader 是在 Webpack 读取和转换文件内容时被调用的,专注于 处理特定类型的文件(如将 CSS、TypeScript 等转为 JS)。

Plugin 的执行时机

Plugin 可以在 整个 Webpack 构建生命周期的任意阶段 执行,具体取决于插件的实现:

  1. Webpack 构建过程会触发一系列 钩子事件(如 entryOptioncompileemitdone 等)
  2. 插件通过注册这些钩子,在对应的阶段执行自定义逻辑
  3. 例如:
    • HtmlWebpackPluginemit 阶段生成 HTML 文件
    • CleanWebpackPluginbeforeEmit 阶段清理输出目录
    • 可以在构建开始前、模块解析中、资源输出前、构建完成后等任意时机介入

Plugin 功能更强大,能覆盖从构建开始到结束的整个流程,用于解决 Loader 无法处理的复杂任务(如打包优化、资源管理、环境变量注入等)。

总结:Loader 是在处理具体模块时按顺序执行,Plugin 则是通过钩子在整个构建流程的特定阶段执行。

Tree-shaking 原理是什么

Tree-shaking 是 Webpack、Rollup 等打包工具中用于删除未使用代码(Dead Code) 的优化技术,得名于将代码想象成一棵树,摇晃后抖落无用的叶子(未使用的代码)。其核心原理基于 ES6 模块的静态分析特性,具体工作流程如下:

一、核心前提:ES6 模块的静态性

Tree-shaking 依赖 ES6 模块(import/export)的静态分析能力,这是它与 CommonJS 模块(require/module.exports)的关键区别:

  • ES6 模块:导入导出语句是 “静态” 的,在编译阶段(代码执行前)就能确定模块依赖关系,无法在运行时动态修改(如 import('./' + var) 是不允许的)。
  • CommonJS 模块:依赖关系是 “动态” 的(如 require(someVariable)),只能在运行时确定,因此无法被 Tree-shaking 优化。

结论:Tree-shaking 仅对 ES6 模块有效,需确保代码中使用 import/export,而非 require

二、工作流程:标记 → 删除

Tree-shaking 的过程可分为两个核心步骤:

1. 标记未使用的代码(标记阶段)

打包工具(如 Webpack)会遍历整个模块依赖树,分析每个导出(export)是否被其他模块导入(import)并使用:

  • 被使用的导出:如 import { func } from './utils'func() 被调用,则 func 会被标记为 “有用”。
  • 未被使用的导出:如仅 import { func } from './utils' 但从未调用 func,或根本没有导入,则 func 会被标记为 “无用”。

示例

// utils.js(导出两个函数)
export const add = (a, b) => a + b;
export const minus = (a, b) => a - b;// index.js(仅使用 add)
import { add } from './utils';
console.log(add(1, 2)); // 仅使用 add

分析后,minus 会被标记为未使用的代码。

2. 删除未使用的代码(删除阶段)

标记完成后,打包工具会通过代码压缩工具(如 Terser、UglifyJS)将标记为 “无用” 的代码从最终打包产物中删除。

在 Webpack 中,这一步通常由 TerserPlugin(生产模式默认启用)完成。压缩工具会:

  • 移除未被引用的变量、函数或类;
  • 清理空作用域、注释等冗余内容;
  • 最终输出只包含被使用的代码。

上述示例的打包结果(简化):

// 只保留 add 函数及其调用
const add = (a, b) => a + b;
console.log(add(1, 2));
// minus 函数被删除

三、关键配置(以 Webpack 为例)

要确保 Tree-shaking 生效,需正确配置以下项:

  1. 使用 ES6 模块
    确保代码未被 Babel 等工具转译为 CommonJS(否则会破坏静态分析)。
    若使用 Babel,需在 babel.config.json 中禁用模块转换:

    {"presets": [["@babel/preset-env", { "modules": false }] // 不转换 ES6 模块]
    }
    
  2. 设置 mode: 'production'
    Webpack 在生产模式下会自动启用:

    • 模块 concatenation(作用域提升);
    • TerserPlugin 压缩工具(负责删除未使用代码)。
  3. 通过 package.json 标记副作用
    “副作用” 指模块执行时会产生外部影响(如修改全局变量、样式注入等),这类代码即使未被导入也不应删除。
    可在 package.json 中通过 sideEffects 字段声明:

    {"sideEffects": ["./src/style.css", // 有副作用(样式注入),不删除"*.css" // 所有 CSS 文件均有副作用]
    }
    

    sideEffects: false,表示所有模块均无副作用,可安全删除未使用的模块。

四、局限性与注意事项

  1. 仅对顶层导出有效
    Tree-shaking 主要处理模块的顶层 export,无法删除对象内部未使用的属性:

    // utils.js
    export const obj = {a: 1,b: 2 // 即使未被使用,也不会被删除(因属于对象属性)
    };// index.js
    import { obj } from './utils';
    console.log(obj.a); // obj.b 不会被 Tree-shaking 删除
    
  2. 动态导入的处理
    对于 import('./module') 等动态导入,Tree-shaking 仍可生效,但需确保动态导入的模块内部未使用的代码能被正确标记。

  3. 类与副作用代码
    若类的方法未被调用,但类本身被实例化,Tree-shaking 可能无法删除(因构造函数可能有副作用)。

总结

Tree-shaking 的核心原理是:利用 ES6 模块的静态分析能力,标记并删除未被导入使用的代码,最终通过压缩工具移除冗余内容
它显著减少了打包产物的体积,是现代前端构建优化的重要手段。实际使用中需注意模块格式、生产模式配置及副作用声明,以确保其正常工作。

理解 Tree Shaking:原理与那些“甩不掉”的代码陷阱本文深入解析 Tree Shaking 原理,列举常见失效 - 掘金

Tree-shaking分析的流程是什么,如何进行从入口到整个页面的分析

Tree-shaking 的核心是通过静态分析识别 “未被使用的代码(Dead Code)”,其分析流程紧密依赖于 ES6 模块(ESM)的静态特性,从入口文件开始递归遍历整个依赖图谱,最终标记并剔除无用代码。以下是具体流程及从入口到整个页面的分析逻辑:

一、Tree-shaking 分析的核心流程

Tree-shaking 的分析过程可分为 “依赖图谱构建”→“导出使用标记”→“副作用过滤” 三个关键阶段,最终为代码删除(压缩阶段)提供依据。

  1. 阶段一:构建完整的依赖图谱(Dependency Graph)

从入口文件(如 index.js)开始,递归解析所有模块的依赖关系,形成一个包含整个应用的 “模块依赖树”。
具体步骤

  • 解析入口模块:以配置的 entry(如 src/index.js)为起点,解析该模块的代码,提取其 import 语句(确定依赖的模块)。
  • 递归解析依赖:对每个被导入的模块(如 src/utils.jsnode_modules/lodash-es 等)重复解析过程,提取它们的 import 语句,直到所有间接依赖的模块都被纳入图谱。
  • 记录模块关系:在图谱中记录 “模块 A 依赖模块 B”“模块 B 导出了哪些成员(如 export function add)” 等信息。

示例
若入口 index.js 导入 utils.jsutils.js 又导入 math.js,则依赖图谱为:index.js → utils.js → math.js

2. 阶段二:标记 “被使用的导出”(Used Exports)

遍历依赖图谱,跟踪每个模块的 export 成员是否被其他模块 “实际使用”,未被使用的导出会被标记为 “可删除”。
具体规则

  • 直接使用:若模块 B 的 export const add 被模块 A 通过 import { add } from './B' 导入,且 add() 在模块 A 中被调用,则 add 被标记为 “使用”。
  • 间接使用:若模块 C 导出 { a: 1, b: 2 },模块 D 导入 { a } 并使用 a,则 b 被标记为 “未使用”(即使 a 被使用,同模块的其他导出也可能被单独标记)。
  • 默认导出特殊性:默认导出(export default)若被导入(import obj from './module'),即使只使用其部分属性(如 obj.a),整个默认导出对象也会被标记为 “使用”(因静态分析无法确定对象内部未使用的属性)。

示例

// math.js(导出两个函数)
export const add = (a, b) => a + b;
export const minus = (a, b) => a - b;// utils.js(仅使用 add)
import { add } from './math';
export const calculate = (x, y) => add(x, y) * 2;// index.js(使用 calculate)
import { calculate } from './utils';
console.log(calculate(1, 2));

分析后:addutils.js 使用 → 标记为 “使用”;minus 未被任何模块导入使用 → 标记为 “未使用”。

3. 阶段三:过滤 “有副作用的代码”(Side Effects)

部分代码即使未被直接使用,也可能产生 “副作用”(如修改全局变量、注册事件、注入样式等),这类代码不能被删除。分析时需排除这些有副作用的模块或代码块。
识别方式

  • 显式声明:通过 package.jsonsideEffects 字段标记有副作用的文件(如 ["*.css", "./src/polyfill.js"]),工具会跳过这些文件的 Tree-shaking。
  • 隐式推断:对于未声明副作用的代码,工具会尝试分析是否包含副作用(如 window.xxx = 1 属于副作用,纯函数 const f = () => {} 无副作用)。若无法确定,可能会保守保留(避免误删)。

二、从入口到整个页面的分析逻辑

Tree-shaking 对 “整个页面” 的分析本质是以入口为根,递归覆盖所有可达模块的依赖链,确保没有遗漏任何可能被使用的代码。具体过程如下:

1. 以入口为起点,建立 “可达性” 判断

整个应用的代码可视为一个 “有向图”(入口是起点,模块是节点,import 是边)。Tree-shaking 只分析从入口出发 “可达” 的模块(即被直接或间接导入的模块),完全未被导入的模块(不可达)会被直接排除(无需标记,打包时直接忽略)。

2. 递归遍历所有可达模块,跟踪导出使用

从入口模块开始,逐层深入分析每个依赖模块:

  • 对于每个模块,先解析其 export 列表(如 export { a, b, c })。
  • 检查这些导出是否被 “上游模块”(即导入当前模块的模块)使用:
    • 若导出 a 被上游模块导入并使用,则标记 a 为 “使用”,并继续分析 a 内部依赖的其他代码(如 a 调用了同模块的 d 函数,则 d 也需被标记为 “使用”)。
    • 若导出 b 未被任何上游模块使用,则标记 b 为 “未使用”。
  • 对模块内部的嵌套依赖(如 a 函数调用了另一个模块的 e 函数),重复上述过程,直到所有相关代码都被分析。

3. 处理特殊场景(动态导入、循环依赖等)

  • 动态导入(import():动态导入的模块会被视为 “条件可达”,Tree-shaking 仍会分析其内部导出(若导出未被该模块内使用,仍可被删除),但模块本身不会被完全排除(因运行时可能被加载)。
  • 循环依赖:如 A → B → A,工具会通过依赖图谱的环检测机制,确保循环中的导出使用情况被正确标记(不会因循环而遗漏分析)。

4. 生成 “可删除代码” 清单

分析完成后,工具会生成一份清单,包含所有 “未被使用且无副作用” 的代码(如未使用的导出函数、变量、类等),供后续压缩阶段(如 Terser、esbuild)删除。

三、工具实现差异(以 Webpack 和 Rollup 为例)

不同打包工具的分析细节略有差异,但核心流程一致:

  • Webpack:通过 ModuleGraph 跟踪模块依赖和导出使用情况,在 optimization.usedExports: true 时标记未使用导出,最终由 Terser 删除。
  • Rollup:原生支持 Tree-shaking,在解析阶段直接分析导出使用,打包时默认剔除未使用代码,无需额外配置。

总结

Tree-shaking 的分析流程是:

  1. 从入口构建全量依赖图谱(覆盖整个页面的所有可达模块);
  2. 递归跟踪每个模块的导出是否被使用,标记未使用的代码;
  3. 排除有副作用的代码,生成可删除清单。

其核心依赖 ES6 模块的静态性,确保在编译时即可确定代码的 “使用状态”,最终实现 “只保留被使用的代码” 的优化目标。

如果A文件引用了B文件,B文件又引用了A文件,C引用了AB文件哪Tree Shaking怎么做的标记呢

ree Shaking 的核心是通过静态分析 ES 模块(ESM)的导入导出关系,标记出 “未被使用的导出(dead code)”,最终在优化阶段将其删除。对于存在循环依赖(A 引用 B,B 引用 A)且被 C 引用的场景,其标记逻辑依然遵循 “是否被实际使用” 的核心原则,具体过程如下:

一、前提:Tree Shaking 生效的基础

Tree Shaking 仅对 ES 模块(import/export 有效,因为 ESM 是 “静态的”—— 导入导出语句只能出现在模块顶层,且导入的模块路径和导出的成员是明确的,可在编译阶段(而非运行时)被解析。

对于 CommonJS 模块(require/module.exports),由于其导入导出是动态的(如 require(someVariable)),无法在编译阶段确定依赖关系,因此 Tree Shaking 不适用。

二、循环依赖(A↔B)+ C 引用的场景分析

假设模块结构如下:

// A.js
import { b } from './B.js';
export const a = 'a';
export const aUsedByB = b + ' from A'; // A 引用 B 的 b// B.js
import { a } from './A.js';
export const b = 'b';
export const bUsedByA = a + ' from B'; // B 引用 A 的 a// C.js
import { a, aUsedByB } from './A.js';
import { b } from './B.js';
console.log(a, b); // C 只使用 A 的 a 和 B 的 b

Tree Shaking 会按以下逻辑标记 “使用 / 未使用” 的导出:

1. 静态分析依赖关系,构建 “导出 - 引用” 图谱

工具(如 Webpack、Rollup)会遍历所有模块的导入导出语句,忽略代码执行顺序(循环依赖不影响静态分析),仅记录 “谁导出了什么” 以及 “谁导入了什么”:

  • A 导出aaUsedByB
  • A 导入:B 的 b
  • B 导出bbUsedByA
  • B 导入:A 的 a
  • C 导入:A 的 aaUsedByB;B 的 b
  • C 使用:仅 abaUsedByB 被导入但未使用)

2. 标记 “被使用的导出”

从 “根引用”(通常是入口模块或明确被使用的模块)出发,递归标记所有被直接或间接使用的导出:

  • C 直接使用:A 的 a、B 的 b → 标记为 “使用”。
  • A 中 aUsedByB 的生成依赖 B 的 b:但 aUsedByB 仅被 C 导入却未使用 → 标记为 “未使用”。
  • B 中 bUsedByA 的生成依赖 A 的 a:但 bUsedByA 从未被任何模块导入(包括 A、B、C)→ 标记为 “未使用”。
  • 循环依赖中的引用不影响标记:A 用了 B 的 b,B 用了 A 的 a,但这两个成员已被 C 直接使用,因此它们的 “被使用状态” 已被确认,循环依赖仅影响运行时初始化顺序,不影响静态标记。

3. 特殊情况:副作用(side effect)的影响

如果模块中存在 “副作用代码”(即代码执行会影响外部环境,如修改全局变量、注册事件等),即使模块的导出未被使用,Tree Shaking 也不会删除该模块(否则可能破坏程序运行)。

例如,若 A.js 中存在副作用:

// A.js(有副作用)
import { b } from './B.js';
export const a = 'a';
window.globalVar = 'side effect'; // 副作用:修改全局变量

即使 A 的所有导出都未被使用,由于存在副作用,Tree Shaking 不会删除 A.js 的代码。

为解决此问题,可通过 package.jsonsideEffects 字段声明模块是否有副作用,帮助工具更精准地标记:

// package.json
{"sideEffects": false // 所有模块均无副作用,可安全删除未使用模块// 或指定有副作用的文件:"sideEffects": ["./src/A.js"]
}

三、总结:Tree Shaking 的标记逻辑

在循环依赖场景中,Tree Shaking 的标记核心是:
忽略依赖的 “循环关系”,仅通过静态分析确定 “每个导出成员是否被任何模块(直接或间接)使用”。被使用的成员会被保留,未被使用且无副作用的成员 / 模块会被标记为 “可删除”。

这一过程不依赖代码的执行顺序,仅基于 ESM 明确的导入导出语法,因此循环依赖不会干扰 Tree Shaking 的标记结果。

循环依赖是如何影响Tree Shaking的效率的?

循环依赖(模块间相互引用,如 A 依赖 B 且 B 依赖 A)对 Tree Shaking 效率的影响,主要体现在增加静态分析的复杂度引入额外的计算开销,但通常不会直接破坏 Tree Shaking 的正确性(前提是使用 ES 模块)。具体影响机制如下:

一、Tree Shaking 的效率核心:静态依赖图的遍历成本

Tree Shaking 的基础是静态分析 ES 模块的依赖关系,构建 “导出 - 引用” 图谱,再从入口出发标记 “被使用的导出”。这一过程的效率取决于:

  1. 依赖图的复杂度(节点数、边数);
  2. 遍历算法的执行成本(是否需要重复处理模块、是否需要特殊逻辑处理闭环)。

二、循环依赖对效率的具体影响

循环依赖会通过以下方式增加分析成本,降低 Tree Shaking 效率:

1. 依赖图遍历需额外处理 “闭环”,避免无限递归

正常的线性依赖(A→B→C)中,遍历可按 “深度优先” 或 “广度优先” 顺序一次性完成,每个模块仅被处理一次。

但循环依赖会形成闭环(如 A↔B 或 A→B→C→A),若不特殊处理,遍历算法可能陷入无限循环(反复处理 A→B→A→B…)。因此,工具(如 Webpack、Rollup)必须:

  • 维护 “已访问模块” 的集合(如哈希表),每次处理模块前先检查是否已访问;
  • 若模块已在处理中(处于 “遍历栈” 中),则终止当前分支的递归,避免重复处理。

这一额外的 “闭环检测” 逻辑会增加遍历过程的计算开销(如哈希表查询、栈状态维护),且循环依赖越复杂(如多模块形成的大型闭环),开销越大。

2. 导出依赖关系更复杂,增加 “使用标记” 的计算成本

Tree Shaking 需要标记 “被使用的导出”,而循环依赖会导致模块间的导出引用关系更复杂:

  • 模块 A 的导出可能被 B 使用,而 B 的导出又被 A 使用(如 A.foo 依赖 B.bar,B.bar 依赖 A.baz);
  • 工具需要多次交叉验证 “导出是否被间接使用”,甚至可能需要对闭环内的模块进行 “联合分析”,而非单独处理。

例如,在 A↔B 的闭环中,若 C 仅使用 A.foo,工具需要确认:A.foo 是否依赖 B 的某个导出?该 B 的导出是否又依赖 A 的其他导出?这些导出是否被 C 间接使用?

这种 “交叉验证” 会增加标记过程的计算步骤,尤其是在大型闭环(如 5 个以上模块相互依赖)中,可能导致标记效率显著下降。

3. 副作用判断难度增加,可能导致过度保留代码(间接影响效率)

Tree Shaking 需结合 “副作用(side effect)” 判断:若模块存在副作用(如修改全局变量),即使其导出未被使用,也不能被删除。

循环依赖会增加副作用判断的复杂度:

  • 闭环中的模块可能共享副作用(如 A 初始化全局状态,B 读取该状态),工具难以单独判断某个模块是否 “无副作用”;
  • 为避免误删有副作用的代码,工具可能保守地保留整个闭环中的模块,即使其中部分导出未被使用。

这种 “过度保留” 虽不直接降低分析效率,但会导致 Tree Shaking 的 “优化效果” 下降(需要处理的代码量未减少),间接增加后续压缩、打包等步骤的成本。

三、不同工具的处理差异(影响效率的实际表现)

不同打包工具对循环依赖的处理策略不同,效率影响也有差异:

  • Rollup:对 ESM 的静态分析更彻底,会将循环依赖视为 “共享作用域” 处理,闭环内的模块分析成本较低,但大型闭环仍会明显增加遍历时间;
  • Webpack:依赖图更复杂(包含更多 runtime 逻辑),处理循环依赖时需维护更多模块状态(如 module 对象、依赖缓存),效率损耗相对更大;
  • Terser(代码压缩工具):作为 Tree Shaking 的后续步骤,若循环依赖导致代码中出现更多闭包或交叉引用,Terser 的死代码删除效率也会下降。

四、总结

循环依赖对 Tree Shaking 效率的影响,本质是增加了静态分析的复杂度和计算开销

  1. 需额外逻辑处理闭环,避免无限递归;
  2. 导出引用关系更复杂,增加 “使用标记” 的验证成本;
  3. 副作用判断难度上升,可能导致过度保留代码,间接增加后续步骤的负担。

但需注意:循环依赖不会让 Tree Shaking 完全失效,只是会降低其优化效率(速度变慢或效果打折扣)。实际开发中,应尽量减少不必要的循环依赖(如通过拆分模块职责),以提升 Tree Shaking 的效率和效果。

webpack与Vite的区别是什么?

Webpack 和 Vite 是前端工程化中最常用的两种构建工具,两者在设计理念、核心原理和使用场景上有显著差异。以下从核心原理、开发体验、构建流程等维度详细对比:

一、核心原理:“打包” vs “原生 ESM”

这是两者最根本的区别,直接决定了其他所有差异。

  • Webpack
    基于 “全量打包” 理念,无论开发还是生产环境,都会将所有模块(JS、CSS、图片等)递归解析、处理后,打包成一个或多个 bundle 文件。
    即使是开发环境,也需要先完成打包才能启动服务,浏览器加载的是打包后的 bundle
  • Vite
    基于 “原生 ES 模块(ESM)” 理念,开发环境不打包,直接利用浏览器原生支持的 <script type="module"> 加载源码。
    浏览器请求某个模块时,Vite 的 Dev Server 才会即时编译该模块(如 TS 转 JS、Sass 转 CSS),实现 “按需编译”;生产环境则基于 Rollup 进行打包优化。

二、开发体验:启动速度与热更新

开发环境的效率是两者差异最直观的体现。

特性WebpackVite
启动速度较慢(秒级甚至分钟级)。 需递归解析所有模块并打包成 bundle,项目越大启动越慢。极快(毫秒级)。 无需打包,仅启动 Dev Server,模块按需编译,启动时间与项目规模无关。
热更新(HMR)较慢。 修改代码后,需重新打包变化的模块及其依赖,再推送更新,大型项目可能有明显延迟。极速。 利用原生 ESM 缓存机制,仅更新变化的模块,无需重新打包,毫秒级反馈。
调试体验依赖 SourceMap 映射到源码,复杂项目可能存在映射偏差。直接映射源码(因未打包),调试更直观,SourceMap 更准确。

三、构建流程:复杂打包 vs 按需处理

1. Webpack 构建流程(开发 / 生产通用核心步骤):

  1. 读取配置,确定入口、Loader、Plugin 等;
  2. 从入口文件开始,递归解析所有模块的依赖关系,生成 “依赖图谱”;
  3. 通过 Loader 处理非 JS 模块(如 CSS、图片),将其转为可打包的模块;
  4. 执行 Plugin 钩子,介入构建的各个阶段(如代码分割、压缩等);
  5. 将所有模块打包成 bundle(开发环境未压缩,生产环境经过优化)。

整个流程是 “先全量处理,再输出结果”,耗时随项目规模线性增长。

2. Vite 构建流程:

  • 开发环境
    1. 启动 Dev Server,预构建 node_modules 中的依赖(将 CommonJS 转 ESM,合并小依赖);
    2. 浏览器请求入口 HTML,Vite 返回包含 <script type="module" src="/src/main.js"> 的页面;
    3. 浏览器请求 main.js,Vite 即时编译并返回;
    4. 递归处理 main.js 中的 import,每次请求模块时才编译,实现 “按需处理”。
  • 生产环境
    基于 Rollup 进行打包(与 Webpack 生产流程类似,但 Rollup 对 ESM 的处理更高效),包括 Tree-shaking、代码分割、压缩等优化。

四、依赖处理:全量打包 vs 预构建缓存

  • Webpack
    源码和 node_modules 中的依赖会被一起解析、打包到 bundle 中。即使依赖未修改,每次构建(尤其是开发环境)都需要重新处理,耗时较长。
  • Vite
    • 开发环境:对 node_modules 中的依赖进行预构建(首次启动时),将 CommonJS 转为 ESM(避免浏览器兼容问题),并合并小依赖(减少 HTTP 请求),结果缓存到 node_modules/.vite,后续启动直接复用。
    • 生产环境:依赖会被 Rollup 打包,但 Rollup 对 ESM 的 Tree-shaking 更高效,产物体积通常更小。

五、生态与灵活性

  • Webpack
    • 生态极其成熟,支持几乎所有前端场景(如多页面应用、SSR、PWA 等);
    • 配置灵活但复杂,需要手动配置 Loader(如 babel-loadercss-loader)和 Plugin(如 HtmlWebpackPlugin);
    • 适合复杂项目(如大型企业级应用),但学习成本高。
  • Vite
    • 生态较新但增长迅速,原生支持 Vue、React、Svelte 等框架,通过插件扩展功能;
    • 配置简洁,内置常用功能(如 TS、JSX、CSS 预处理器支持),无需手动配置大量 Loader;
    • 适合现代前端项目(尤其是单页应用),学习成本低,但复杂场景(如特殊的模块转换)可能需要自定义插件。

六、适用场景总结

工具适用场景核心优势潜在劣势
Webpack大型复杂项目、多页面应用、需要深度定制构建流程生态成熟、灵活性高、支持所有场景开发启动慢、配置复杂、热更新延迟
Vite现代单页应用(Vue/React 等)、追求开发效率开发启动快、热更新极速、配置简洁复杂场景生态略弱、生产构建依赖 Rollup

总结

Webpack 是 “全能型打包工具”,通过全量打包支持各种复杂场景,但开发体验受限于打包流程;Vite 是 “现代构建工具”,利用原生 ESM 实现极速开发体验,生产环境借助 Rollup 生成高效产物,更适合追求开发效率的现代前端项目。选择时需根据项目复杂度、团队熟悉度和开发效率需求综合判断。

webpack多进程如何开启,是怎样的一个执行机制

在 Webpack 中,可以通过配置多进程来并行处理任务(如代码转译、压缩等),从而提升构建速度。核心是利用 Node.js 的 child_process 模块开启子进程,将耗时任务分配到多个 CPU 核心上执行。

一、如何开启多进程

Webpack 本身不直接管理多进程,需通过以下工具实现:

1. 处理 loader 阶段:thread-loader

thread-loader 可将 loader 执行过程放入独立进程,适用于耗时的 loader(如 babel-loaderts-loader)。

使用步骤

  1. 安装:npm install thread-loader --save-dev
  2. 配置(webpack.config.js):

使用 thread-loader 开启多进程处理 loader

2. 处理代码压缩:terser-webpack-plugin(Webpack 5 内置)

Webpack 5 中,terser-webpack-plugin 默认支持多进程压缩 JS 代码,无需额外安装。

配置示例

使用 terser-webpack-plugin 开启多进程压缩

3. 其他场景:happypack(较旧,推荐用 thread-loader

happypack 是早期多进程方案,原理与 thread-loader 类似,但配置更复杂,目前已逐渐被 thread-loader 替代。

二、多进程执行机制

Webpack 多进程的核心逻辑是 “主进程调度 + 子进程执行 + 结果汇总”

  1. 主进程(Main Process)
    • 负责解析配置、管理依赖图、协调子进程。
    • 当遇到配置了多进程的 loader 或插件时,将任务分配给子进程。
  2. 子进程(Worker Process)
    • 由主进程通过 child_process.fork() 启动,数量通常为 CPU 核心数 - 1(避免占用全部资源)。
    • 独立执行具体任务(如转译 JS、压缩代码),不干扰主进程。
    • 任务执行完毕后,将结果通过 IPC(进程间通信)发送给主进程。
  3. 任务分配原则
    • 按 “任务单元” 拆分工作(如每个模块文件作为一个任务)。
    • 子进程空闲时主动向主进程请求任务,避免负载不均。
  4. 缓存机制
    • 子进程执行结果会被缓存(如 thread-loader 可配合 cache-loader),二次构建时直接复用,减少重复计算。

三、注意事项

  1. 并非所有场景都适合多进程
    • 优势:适用于 CPU 密集型任务(如 Babel 转译、代码压缩)。
    • 劣势:进程启动和 IPC 通信有开销,对于简单任务(如 url-loader),多进程可能反而变慢。
  2. 进程数不宜过多
    • 超过 CPU 核心数的进程会导致频繁上下文切换,降低效率,建议设置为 os.cpus().length - 1
  3. 环境限制
    • Node.js 单进程内存有限(32 位约 1.4GB,64 位约 1.7GB),多进程可突破此限制,适合大型项目。

总结

Webpack 多进程通过 thread-loader(处理 loader)和 terser-webpack-plugin(处理压缩)实现,核心机制是将耗时任务分配到多个子进程并行执行,从而利用多核 CPU 提升构建效率。实际使用中需根据任务类型和项目规模合理配置,避免过度启用导致性能反降。

为什么Vite冷启动那么快?

Vite 冷启动速度快的核心原因是它采用了 “按需编译”“原生 ESM 支持” 的设计,彻底抛弃了 Webpack 等工具的 “先打包再启动” 模式,从根本上优化了启动流程。

1. 基于原生 ESM,无需预打包

传统工具(如 Webpack)冷启动时需要:

  1. 递归解析所有依赖,构建完整的依赖图
  2. 将所有模块打包成一个或多个 bundle(即使很多模块暂时用不到)

而 Vite 的处理方式完全不同:

  • 开发时,Vite 直接将源码以原生 ESM 格式提供给浏览器
  • 浏览器会根据 import 语句按需请求模块,Vite 只在浏览器请求时才编译该模块
  • 启动阶段无需打包整个项目,只需启动一个开发服务器,因此启动速度极快

举例来说,一个包含 1000 个模块的项目,冷启动时:

  • Webpack 需要处理所有 1000 个模块并打包
  • Vite 只需启动服务器,等待浏览器请求,第一个请求到的模块才会被编译

2. 依赖预构建:只处理第三方库

Vite 并非完全不处理依赖,而是将依赖分为两类区别对待:

  • 源码(src/):开发者编写的代码,可能频繁变更,采用 “按需编译”
  • 依赖(node_modules/):第三方库(如 React、Vue),通常很少变更

对于依赖,Vite 在首次启动时会做一次预构建:

  1. 将 CommonJS 格式的依赖转换为 ESM(避免浏览器不支持)
  2. 将零散的小模块合并成少数几个文件(减少 HTTP 请求次数)

但预构建有两个优化点:

  • 只在依赖变动时重新执行(通过 node_modules/.vite 缓存结果)
  • 预构建过程非常快(使用 esbuild,基于 Go 语言编写,比 JS 工具快 10-100 倍)

3. 极致简洁的启动流程

Vite 冷启动的核心步骤:

  1. 启动一个 Koa 服务器(毫秒级)
  2. 预构建依赖(首次慢,后续读缓存)
  3. 等待浏览器请求,按需编译源码

相比之下,Webpack 需要:

  1. 解析配置文件
  2. 构建完整依赖图
  3. 执行各种 loader 和 plugin
  4. 打包所有模块
  5. 启动服务器

流程复杂度的差异直接导致了启动速度的差距。

4. 工具链效率优势

Vite 在关键步骤使用了更高效的工具:

  • esbuild:用于依赖预构建和 TS/JSX 转译,Go 语言编写,速度远超 Babel 等 JS 工具
  • 原生 ESM 解析:利用浏览器原生能力处理模块依赖,无需手动维护依赖图

而传统工具依赖 JS 编写的打包器和转译器,在处理大量文件时性能瓶颈明显。

总结

Vite 冷启动快的本质是 “颠覆了传统打包思路”

  • 用 “按需编译” 替代 “全量打包”
  • 用 “浏览器原生 ESM” 替代 “自定义模块系统”
  • 用 “高效工具链(esbuild)” 处理固定工作(依赖预构建)

这种设计让 Vite 冷启动速度不受项目规模影响(即使是大型项目,启动也只需几百毫秒),而传统工具的启动时间会随项目体积增长显著变慢。

vite生产环境下跟开发下的区别?

Vite 在开发环境和生产环境下的行为和实现有显著差异,这些差异是为了分别优化 “开发体验” 和 “生产性能”。以下是核心区别:

一、核心目标不同

  • 开发环境(vite dev
    优先保证开发效率快速反馈,目标是让开发者修改代码后能瞬间看到效果(毫秒级更新)。
  • 生产环境(vite build
    优先保证产物性能,目标是生成体积小、加载快、运行高效的最终代码。

二、底层实现差异

1. 模块处理方式

  • 开发环境
    • 基于 原生 ES 模块(ESM) 运行,无需打包。
    • 浏览器直接通过 <script type="module"> 加载源码,Vite 作为开发服务器(Dev Server)在中间做 “即时转换”(如编译 TypeScript、JSX、CSS 等)。
    • 只在浏览器请求某个模块时才对其进行处理,实现 “按需编译”,启动速度极快。
  • 生产环境
    • 基于 Rollup 进行打包(Vite 2.0+ 采用 Rollup 作为生产打包工具),将所有模块合并为优化后的静态资源。
    • 会对代码进行全量处理(如 Tree-shaking、代码分割、压缩等),生成适合生产的紧凑代码。

2. 热更新机制

  • 开发环境
    • 支持 极速热模块替换(HMR):修改代码后,只更新变化的模块,不刷新页面,保留应用状态。
    • 利用浏览器原生 ESM 的模块缓存机制,结合 WebSocket 推送更新,实现毫秒级更新反馈。
  • 生产环境
    • 不支持热更新(生产环境无需开发时的动态更新能力)。
    • 产物是静态文件,需通过部署服务器(如 Nginx)运行,更新需重新构建并部署。

3. 代码优化策略

  • 开发环境

    • 几乎不做代码优化,甚至会故意保留代码格式(如不压缩、不混淆),方便调试。
    • 可能会注入一些开发辅助代码(如 HMR 运行时、源码映射 SourceMap)。
  • 生产环境

    • 启用

      全量优化

      ,包括:

      • Tree-shaking:删除未使用的代码;
      • 代码压缩(JS 用 Terser,CSS 用 CSSMinimizer);
      • 代码分割(按路由、公共库拆分 chunk);
      • 懒加载(动态导入的模块单独打包);
      • 预构建依赖(将 node_modules 中的 CommonJS 转为 ESM 并缓存);
      • 自动注入 polyfill(根据浏览器兼容性配置);
      • 生成优化的 SourceMap(默认只包含错误定位信息,减小体积)。

4. 依赖处理

  • 开发环境

    • node_modules
      

      中的依赖进行

      预构建

      (首次启动时):

      • 将 CommonJS 模块转为 ESM(避免浏览器无法识别);
      • 将多个小依赖合并为一个 “依赖模块”(减少 HTTP 请求)。
    • 预构建结果缓存到 node_modules/.vite,后续启动直接复用。

  • 生产环境

    • 依赖会被 Rollup 打包到最终产物中,根据配置进行拆分(如 vendor chunk 单独打包第三方库)。
    • 不依赖缓存,每次构建都是基于源码重新处理。

5. 环境变量与配置

  • 开发环境
    • 读取 .env.development 中的环境变量(默认 NODE_ENV=development)。
    • 配置中 server 相关选项(如端口、代理、HMR)生效。
  • 生产环境
    • 读取 .env.production 中的环境变量(默认 NODE_ENV=production)。
    • 配置中 build 相关选项(如输出目录、资源哈希、目标浏览器)生效。

三、实际表现对比

特性开发环境(vite dev生产环境(vite build
启动速度极快(毫秒级,按需编译)较慢(秒级,全量打包优化)
产物体积不优化,体积大(含源码和辅助代码)极小(压缩、Tree-shaking 等优化)
运行时性能一般(未优化,可能有调试代码)极佳(代码经过深度优化)
调试体验友好(完整 SourceMap,支持 HMR)有限(SourceMap 简化,无 HMR)
网络请求多(浏览器直接请求单个模块)少(合并为少量 chunk)

总结

Vite 的核心设计是 “开发时利用原生 ESM 实现极速体验,生产时用 Rollup 进行高效打包”,通过差异化的策略兼顾了开发效率和生产性能。这种 “快启动、热更新、优产物” 的特点,使其成为现代前端项目的主流构建工具之.

vite构建原理

Vite 是一款由尤雨溪团队开发的前端构建工具,核心目标是提升前端开发体验,其构建原理围绕 “快速启动、即时热更新、优化生产构建” 三大核心设计,与传统构建工具(如 Webpack)有本质区别。以下从核心原理、开发环境、生产环境三个维度详细解析:

一、核心设计理念:颠覆传统 “预打包” 模式

传统构建工具(如 Webpack)在开发环境中会先将所有模块预打包成 bundle(即使是未使用的模块也会被处理),再启动开发服务器。这种模式的问题是:项目越大,打包时间越长,冷启动和热更新效率越低。

Vite 则基于 浏览器原生 ES 模块(ESM) 设计,彻底抛弃 “预打包” 思路:

  • 开发时,Vite 不提前打包代码,而是将模块直接交给浏览器处理(通过 <script type="module"> 加载)。
  • 仅在浏览器请求某个模块时,Vite 才对其进行实时编译(如 TypeScript 转 JS、JSX 转换等),实现 “按需处理”。

二、开发环境核心原理

开发环境中,Vite 启动一个基于 Koa 的开发服务器,核心流程包括依赖预构建模块请求实时处理两部分。

1. 依赖预构建(首次启动时执行)

第三方依赖(如 node_modules 中的库)通常存在两类问题:

  • 部分依赖使用 CommonJS/UMD 格式,浏览器无法直接通过 ESM 加载。
  • 依赖内部可能包含大量小模块(如 Lodash),直接请求会导致浏览器发送数百次 HTTP 请求,性能极差。

Vite 解决方式:

  • 格式转换:使用 esbuild(Go 编写的超快打包工具)将 CommonJS/UMD 依赖转换为 ESM 格式。
  • 依赖合并:将多个小模块合并为少数几个 “预构建产物”,减少浏览器请求次数。

预构建产物默认缓存于 node_modules/.vite 目录,后续启动时若依赖未变化,会直接复用缓存,避免重复处理。

2. 模块请求实时处理

当浏览器通过 ESM 语法(import)请求模块时,开发服务器会拦截请求并实时处理,核心步骤:

  • 路径解析:将相对路径(如 ./src/main.js)解析为真实文件路径,处理别名、条件导入等。

  • 模块编译

    :根据文件类型(JS/TS/JSX/CSS 等)调用对应处理器:

    • TypeScript/JSX:通过 esbuild 实时转译为 JS(比 Babel 快 10-100 倍)。
    • CSS:将 CSS 转换为 JS 模块,注入 style 标签(支持 CSS Modules、PostCSS)。
    • 静态资源(图片、字体等):直接返回资源内容,或生成路径引用。
  • 依赖注入:对预构建后的依赖,自动替换为缓存产物的路径(如 node_modules/.vite/deps/react.js)。

3. 高效热模块替换(HMR)

当文件修改时,Vite 实现精准的热更新,而非全量刷新:

  • 开发服务器通过 WebSocket 与浏览器建立连接,实时推送文件变化事件。
  • 仅重新编译修改的模块,并根据模块依赖关系(通过 ES 模块的 import 语法分析)计算 “最小更新范围”。
  • 对于 CSS 模块,直接更新 <style> 标签内容,无需重新加载 JS;对于 JS 模块,通过 import.meta.hot API 触发局部更新(如 Vue 组件、React 组件的热替换)。

三、生产环境构建原理

生产环境中,Vite 不再依赖浏览器 ESM,而是使用 Rollup 进行打包优化(Rollup 对 ESM tree-shaking 更彻底,输出产物更小)。核心流程:

  1. 入口分析:从项目入口(如 index.html)解析所有依赖,生成模块依赖树。
  2. 代码转换:对 TypeScript、JSX、CSS 等进行编译(与开发环境一致,但为一次性处理)。
  3. 优化打包:
    • Tree-shaking:删除未使用的代码(依赖 ESM 的静态分析能力)。
    • 代码分割:按路由、组件拆分代码,实现按需加载(dynamic import)。
    • 压缩混淆:使用 esbuildterser 压缩 JS/CSS,生成 SourceMap。
    • 资源处理:对图片、字体等进行优化(如压缩、Base64 内联小资源)。
  4. 生成产物:输出优化后的静态文件(JS/CSS/HTML/ 资源),可直接部署到服务器。

四、与传统工具对比的核心优势

特性传统工具(Webpack)Vite
开发启动速度慢(预打包所有模块)快(按需处理,依赖预构建缓存)
热更新效率随项目增大变慢(需重新打包 bundle)快(仅更新修改模块,依赖树精准更新)
生产构建工具内置 Webpack基于 Rollup(更优的 tree-shaking)
底层工具主要依赖 Babel(JS 转译)依赖 esbuild(Go 编写,转译速度极快)

总结

Vite 的核心原理是开发环境利用浏览器原生 ESM 实现按需编译,通过 esbuild 预构建依赖优化性能;生产环境基于 Rollup 进行高效打包。这种 “开发与生产分离优化” 的思路,既解决了传统工具冷启动慢、热更新效率低的问题,又保证了生产产物的优化质量,从而大幅提升前端开发体验。

Vite7更新了那些

  1. Node.js支持变更
  • 要求使用Node.js 20.19+或22.12+
  • 不再支持Node.js 18(已于2025年4月底达到EOL)
  • 新的Node.js版本要求使Vite能够以纯ESM格式发布,同时保持对CJS模块通过require调用的兼容性
  1. 浏览器兼容性目标调整
  • 默认浏览器目标从'modules'更改为'baseline-widely-available'
  • 支持的最低浏览器版本更新:
    • Chrome: 87 → 107
    • Edge: 88 → 107
    • Firefox: 78 → 104
    • Safari: 14.0 → 16.0
  • 这一变化使浏览器兼容性更具可预测性
  1. Environment API增强
  • 保留了Vite 6中引入的实验性Environment API
  • 新增buildApp钩子,使插件能够协调环境的构建过程
  • 为框架提供了更强大的环境API
  1. Rolldown集成
  • 引入基于Rust的下一代打包工具Rolldown
  • 通过rolldown-vite包可替代默认的vite包
  • 未来Rolldown将成为Vite的默认打包工具
  • 能显著减少构建时间,尤其对大型项目
  1. Vite DevTools增强
  • 通过VoidZero与NuxtLabs合作开发
  • 为所有基于Vite的项目和框架提供更深入的调试与分析功能
  1. 废弃功能移除
  • 移除了Sass的旧版API支持
  • 移除了splitVendorChunkPlugin
  1. Vitest支持
  • Vitest 3.2开始支持Vite 7.0

这些更改使Vite在性能、开发体验和浏览器兼容性方面都有了显著提升。 下面是Vite博客原文

为什么import要写在顶部?

记忆点:模块的依赖关系和导入 / 导出行为在编译时是完全可预测的

  • import 声明只能出现在模块的最顶层作用域(不能在函数、条件语句等代码块内部);
  • 导入的模块路径和成员名称必须是静态字符串(不能是变量或表达式)。

Tree-shaking(树摇)能够有效消除代码中未被使用的部分(“死代码”),核心依赖于 ES6 模块(ESM)的静态分析能力。而 import 声明需要写在模块顶部,正是由这种 “静态特性” 决定的 —— 只有保证导入行为在编译时可确定,Tree-shaking 才能安全、准确地识别并删除未使用的代码。

核心原因:Tree-shaking 依赖 “静态模块分析”

Tree-shaking 的工作原理是:在代码打包阶段(编译时),通过分析模块的导入(import)和导出(export)关系,识别出哪些导出成员从未被使用,然后将其从最终产物中剔除。这一过程的前提是:模块的依赖关系和导入 / 导出行为在编译时是完全可预测的

ES6 模块(ESM)的 import 声明设计为 “静态语法”,必须满足:

  • import 声明只能出现在模块的最顶层作用域(不能在函数、条件语句等代码块内部);
  • 导入的模块路径和成员名称必须是静态字符串(不能是变量或表达式)。

这种设计让打包工具(如 Webpack、Rollup)在编译时就能清晰地知道:

  • 某个模块依赖了哪些其他模块;
  • 从依赖模块中具体导入了哪些成员(如 import { a } from './module' 明确导入了 a)。

基于这些静态信息,工具才能准确判断:“模块 A 导出的 b 从未被任何地方导入使用”,进而安全地将 b 从打包结果中删除(即 “摇掉” 这部分代码)。

为什么动态导入会破坏 Tree-shaking?

如果 import 不写在顶部,而是出现在动态逻辑中(如条件语句、函数内),就会变成 “动态导入”,例如:

// 错误示例:import 不在顶部,而是在条件语句中
if (someCondition) {import { a } from './module'; // 动态导入(非标准 ESM 语法,实际需用 import())
}

这种情况下,模块的依赖关系只有在运行时才能确定(取决于 someCondition 的值)。打包工具在编译时无法预知:

  • 这个 import 是否会被执行;
  • 如果执行,导入的成员是否会被使用。

为了避免误删可能被用到的代码,Tree-shaking 工具只能放弃对这类动态导入的分析,导致未使用的代码无法被摇掉。

(注:ES6 标准中,动态导入需用 import() 函数,返回 Promise,这种方式同样无法被 Tree-shaking 优化,因为其行为是运行时动态的。)

总结

import 必须写在模块顶部,是为了保证 ES6 模块的静态可分析性—— 让打包工具在编译时就能明确模块间的依赖关系和导入 / 导出成员,从而为 Tree-shaking 提供准确的判断依据。

如果 import 出现在动态逻辑中,会导致依赖关系不可预测,Tree-shaking 就失去了工作的基础,无法安全地剔除未使用的代码。这也是为什么 Tree-shaking 仅支持 ES6 模块(静态),而不支持 CommonJS 模块(动态 require)的根本原因。

webpack 是怎么做这个分包得?

Webpack 的分包(Code Splitting) 是优化前端性能的核心手段之一,其核心思想是将代码分割成多个独立的 chunk(代码块),实现按需加载(减少初始加载体积)或复用(减少重复代码)。Webpack 主要通过以下几种方式实现分包,配合灵活的配置满足不同场景需求。

一、自动分包:splitChunks 配置(核心机制)

Webpack 4+ 引入了 splitChunks 配置,默认会对满足条件的代码自动分包,主要针对第三方库公共模块异步加载模块

1. 默认行为(无需手动配置)

Webpack 默认会对以下情况自动分包:

  • 异步加载的模块:通过 import() 动态导入的模块,会被单独打包成一个 chunk。
  • 共享的公共模块:被至少 2 个模块共享,且体积超过一定阈值(默认 30KB)。
  • node_modules 中的依赖:第三方库(如 React、Lodash)会被单独打包成 vendors 开头的 chunk。

2. 自定义 splitChunks 配置

通过 webpack.config.js 中的 optimization.splitChunks 可精细化控制分包规则,常用配置项如下:

// webpack.config.js
module.exports = {optimization: {splitChunks: {chunks: 'all', // 对哪些类型的 chunk 生效:'async'(默认,仅异步)、'initial'(同步)、'all'(全部)minSize: 30000, // 最小体积(字节),超过此值才会被分包minChunks: 1, // 最少被引用次数,超过此值才会被分包maxAsyncRequests: 5, // 异步加载时,最多允许的并行请求数maxInitialRequests: 3, // 初始加载时,最多允许的并行请求数cacheGroups: {// 缓存组:可以将不同类型的代码分到不同的 chunk 中vendors: {test: /[\\/]node_modules[\\/]/, // 匹配 node_modules 中的模块priority: -10, // 优先级(数值越大越优先)name: 'vendors', // 生成的 chunk 名称},common: {minChunks: 2, // 被至少 2 个模块引用priority: -20,reuseExistingChunk: true, // 如果已有包含该模块的 chunk,直接复用name: 'common', // 公共业务模块的 chunk 名称},},},},
};

关键参数说明

  • chunks: 'all':最常用配置,同时处理同步和异步模块的分包。
  • cacheGroups:核心配置,通过不同规则将代码分到不同的缓存组(如第三方库单独分一个包,业务公共代码分一个包)。
  • reuseExistingChunk:避免重复打包,提高复用率(例如 A 和 B 都引用了 C,若 C 已被打包到某个 chunk,则直接复用)。

二、手动分包:动态导入(import()

通过 ES 提案的 import() 语法(动态导入),可手动指定需要分包的模块,Webpack 会将其单独打包成一个 chunk,并在需要时异步加载。

1. 基础用法

// 同步导入(会被打包到当前 chunk)
import utils from './utils';// 动态导入(会被单独打包成一个 chunk,路径支持变量)
button.addEventListener('click', () => {// 加载时返回 Promiseimport('./dialog').then((module) => {module.openDialog(); // 使用动态加载的模块});
});

2. 配合 React.lazy 实现路由懒加载(常见场景)

在 React 中,可结合 React.lazySuspense 实现路由级别的分包

import { Suspense, lazy } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';// 动态导入 Home 和 About 组件,各自生成独立 chunk
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));function App() {return (<BrowserRouter><Suspense fallback={<div>Loading...</div>}><Routes><Route path="/" element={<Home />} /><Route path="/about" element={<About />} /></Routes></Suspense></BrowserRouter>);
}

三、多入口分包:entry 配置

当项目有多个入口文件时,Webpack 会为每个入口生成一个 chunk,同时通过 splitChunks 提取入口间的公共代码。

配置示例

// webpack.config.js
module.exports = {entry: {page1: './src/page1.js', // 入口 1page2: './src/page2.js', // 入口 2},output: {filename: '[name].bundle.js', // 生成 page1.bundle.js 和 page2.bundle.jspath: path.resolve(__dirname, 'dist'),},optimization: {splitChunks: {chunks: 'all', // 提取 page1 和 page2 共享的代码到 common chunk},},
};
  • page1page2 都引用了 utils.js,则 utils.js 会被提取到 common.bundle.js 中,避免重复打包。

四、分包后的 chunk 加载机制

Webpack 分包后会生成:

  • 多个 chunk 文件(如 vendors.jscommon.js0.js 等,数字为自动生成的 chunkId)。
  • 一个 runtime chunk(默认包含在入口 chunk 中,可通过 runtimeChunk: 'single' 单独提取),负责管理 chunk 的加载逻辑(如判断哪些 chunk 已加载、动态请求未加载的 chunk 等)。

加载流程

  1. 初始加载时,浏览器下载入口 chunk 和 runtime chunk。
  2. 当需要动态加载模块(如点击按钮)时,runtime 会通过 JSONP 方式请求对应的 chunk。
  3. 加载完成后,runtime 执行 chunk 代码并注入到当前环境中。

五、最佳实践

  1. 第三方库单独分包:通过 cacheGroups.vendorsnode_modules 中的依赖单独打包(如 vendors.js),利用浏览器缓存(第三方库变动少)。

  2. 路由级别分包:通过 import() 对路由组件分包,减少首屏加载体积。

  3. 控制 chunk 数量:避免 maxAsyncRequestsmaxInitialRequests 过大导致请求数过多(一般建议不超过 6 个,受浏览器并发限制)。

  4. 命名 chunk:通过

    /* webpackChunkName: "xxx" */
    

    给动态导入的 chunk 命名,便于调试:

    import(/* webpackChunkName: "dialog" */ './dialog'); 
    // 生成的 chunk 名称为 dialog.js(而非默认数字)
    

总结

Webpack 分包的核心是通过 splitChunks 自动提取公共代码和第三方库,结合 import() 手动控制按需加载的模块,最终实现 “减少初始加载体积、提高代码复用率、利用缓存优化性能” 的目标。配置时需根据项目规模(如单页 / 多页)和需求(如首屏速度、交互体验)灵活调整规则。

vite的构建流程

Vite 的构建流程在开发环境生产环境下有显著差异,核心设计是 “开发时利用原生 ESM 实现极速体验,生产时通过打包优化产物”。以下是详细流程解析:

一、开发环境构建流程(vite dev

开发环境的核心是不打包直接运行,依赖浏览器原生 ES 模块(ESM)和 Vite 开发服务器(Dev Server)的即时编译,流程如下:

1. 初始化与配置解析

  • 读取项目根目录的 vite.config.js(或其他格式配置文件),合并默认配置与用户配置(如 rootserverplugins 等)。
  • 解析环境变量(.env.development 等),注入 import.meta.env
  • 初始化插件系统,加载用户配置的插件(如 @vitejs/plugin-vue@vitejs/plugin-react)。

2. 依赖预构建(Pre-bundling)

  • 目标:解决两个问题:① 将 CommonJS 依赖转为 ESM(避免浏览器不兼容);② 合并零散的小依赖(减少浏览器请求次数)。

  • 流程

    • 扫描项目依赖(package.jsondependencies),识别需要预构建的模块(如 lodashreact 等)。
    • 使用 esbuild(极速 JS 编译器)将依赖从 CommonJS 转为 ESM,并合并多个关联小模块为单个文件(如将 lodash 的多个工具函数合并为 node_modules/.vite/deps/lodash.js)。
    • 生成预构建缓存(存于 node_modules/.vite),后续启动时若依赖未变化,直接复用缓存,跳过预构建。

3. 启动开发服务器(Dev Server)

  • 基于 connect 框架启动 HTTP 服务器(默认端口 5173),并创建 WebSocket 连接(用于热更新通知)。
  • 服务器监听文件变化(通过 chokidar 库),为热更新做准备。

4. 处理浏览器请求(核心流程)

  • 当浏览器访问 http://localhost:5173 时,服务器返回入口 HTML 文件(默认 index.html)。

  • HTML 中包含 <script type="module" src="/src/main.js"></script>,浏览器会据此请求 main.js(源码文件)。

  • 模块请求处理:

    • 服务器接收模块请求(如

      /src/main.js
      

      ),根据文件类型调用对应插件 / 转换器处理:

      • .vue 文件:通过 vue-plugin 解析为 JS 模块(拆分模板、脚本、样式)。
      • .ts 文件:通过 esbuild 即时编译为 JS。
      • .scss 文件:先编译为 CSS,再通过 style-loader 转为 JS 模块(注入 <style> 标签)。
    • 处理完成后,将转换后的代码返回给浏览器,同时在代码中注入热更新相关逻辑(如 import.meta.hot)。

  • 依赖请求处理

    • 若请求的是预构建后的依赖(如 /node_modules/.vite/deps/lodash.js),直接返回缓存的预构建结果。

5. 热模块替换(HMR)

  • 当开发者修改文件时,文件监听器触发事件,服务器通过以下步骤处理:
    1. 重新编译变化的模块(仅处理修改的文件,无需全量编译)。
    2. 通过 WebSocket 向浏览器发送更新通知(包含变化模块的信息)。
    3. 浏览器的 HMR 客户端(注入的 runtime 代码)接收通知,加载新模块代码,替换旧模块,并执行插件定义的更新逻辑(如 Vue 组件重新渲染)。
    4. 若模块不支持热更新,则降级为全页面刷新。

二、生产环境构建流程(vite build

生产环境的核心是打包优化,基于 Rollup(未来可能替换为 Rolldown)生成高效的静态资源,流程如下:

1. 配置解析与环境准备

  • 读取 vite.config.js 中的生产环境配置(build 相关选项),合并默认生产配置(如 minify: 'esbuild'sourcemap: false)。
  • 解析 .env.production 环境变量,注入生产环境的 import.meta.env

2. 依赖预构建(可选,按需执行)

  • 若预构建缓存不存在或依赖有变化,重新执行开发环境中的预构建步骤(确保依赖为 ESM 格式)。

3. 基于 Rollup 打包

  • Vite 调用 Rollup 核心 API,传入转换后的配置(将 Vite 配置映射为 Rollup 配置)。

  • 核心步骤

    1. 入口解析:从 build.rollupOptions.input 定义的入口文件(默认 index.html)开始,解析所有模块依赖,生成依赖图谱。

    2. 插件处理:执行 Vite 插件和 Rollup 插件(如 @vitejs/plugin-vue 处理 Vue 单文件组件,rollup-plugin-terser 压缩代码)。

    3. 代码转换:将非 JS 模块(CSS、图片等)转为可打包资源(如 CSS 提取为单独文件,图片转为 base64 或生成资源链接)。

    4. 优化处理

      • Tree-shaking:删除未使用的代码(依赖 Rollup 原生支持)。
      • 代码分割:按路由、公共库等规则拆分 chunk(如 vendor.js 存放第三方依赖)。
      • 压缩混淆:使用 esbuildterser 压缩 JS,cssnano 压缩 CSS。
      • 生成哈希文件名(如 app.[hash].js):用于浏览器缓存控制。

4. 生成输出文件

  • 将打包后的资源(JS、CSS、图片等)输出到 build.outDir 配置的目录(默认 dist)。
  • 生成 index.html 并自动注入打包后的资源链接(如 <script src="/assets/app.123.js"></script>)。

5. 构建完成

  • 输出构建统计信息(如产物体积、构建耗时),可选生成 build.manifest.json(记录资源映射关系,用于后端集成)。

三、核心差异总结

阶段开发环境(vite dev生产环境(vite build
核心工具开发服务器(Dev Server) + esbuild 即时编译Rollup(或未来的 Rolldown)全量打包
处理方式按需编译(浏览器请求时才处理模块)全量预编译(一次性处理所有模块)
产物目标供开发调试的未优化代码(保留源码结构)供生产部署的优化代码(压缩、分割、Tree-shaking)
热更新支持支持(毫秒级更新)不支持(静态产物)
启动速度极快(依赖缓存,与项目规模无关)较慢(全量处理,随项目规模增长)

总结

Vite 的构建流程通过开发环境的原生 ESM 即时处理生产环境的 Rollup 优化打包,兼顾了开发效率和生产性能。开发时的 “按需编译” 和生产时的 “全量优化” 是其核心设计,也是区别于传统打包工具(如 Webpack)的关键。

vite发展方向

你提到的 Rolldown 是 Vite 团队正在开发的新一代打包工具,旨在其定位是作为 Rollup 的替代方案,未来可能成为 Vite 生产环境的默认打包器。不过目前(2025 年),Vite 的生产环境打包仍以 Rollup 为主,Rolldown 尚处于逐步集成和迭代阶段。

Rolldown 与 Vite 的关系

  • 开发背景:Rolldown 由 Vite 核心团队研发,目标是解决 Rollup 在大型项目下的性能瓶颈,同时保持 Rollup 简洁的 API 和优秀的 Tree-shaking 能力。它基于 Rust 编写(类似 esbuild、swc),主打 “极速打包” 和 “兼容 Rollup 生态”。
  • 当前状态:Rolldown 已进入 alpha/beta 阶段,Vite 官方通过 @vitejs/plugin-rolldown 插件提供实验性支持,可在生产环境替换 Rollup 进行打包。但由于生态兼容性(如部分 Rollup 插件尚未适配)和稳定性问题,尚未成为默认选项。
  • 未来规划:按照官方路线图,Rolldown 最终将成为 Vite 生产环境的默认打包器,彻底替代 Rollup,进一步提升生产构建速度(尤其是大型项目)。

为什么 Vite 要转向 Rolldown?

  • 性能优势:Rust 编写的 Rolldown 比 JavaScript 编写的 Rollup 在模块解析、代码生成等环节快数倍,能显著缩短大型项目的生产构建时间。
  • 生态兼容:Rolldown 设计为兼容 Rollup 的配置和插件 API,降低迁移成本,同时支持 Vite 现有的优化策略(如 Tree-shaking、代码分割)。
  • 协同优化:作为 Vite 团队自研工具,Rolldown 可与 Vite 的开发服务器、依赖预构建等模块深度协同,减少工具链之间的适配成本。

综上,Rolldown 是 Vite 未来打包方案的核心,但目前仍处于过渡阶段,Vite 主版本尚未完全切换到 Rolldown,生产环境默认仍使用 Rollup。随着 Rolldown 生态的成熟,这一情况会逐步改变。

Rollup有什么用,优势是什么?

Rollup 是一款专注于 JavaScript 模块打包 的构建工具,核心目标是将多个 ES6 模块(ESM)合并为少数几个优化后的文件(如库、应用程序)。它以 “简洁、高效、专注于代码优化” 著称,广泛用于 JavaScript 库(如 Vue、React 部分工具链)和中小型应用的打包。

一、Rollup 的核心用途

  1. 库打包
    最典型的场景是构建 JavaScript 库(如工具函数库、UI 组件库),输出多种模块格式(ES6 模块、CommonJS、UMD 等),方便不同环境使用。
    例如:lodash-esvue 等库的打包均使用 Rollup 或类似理念的工具。
  2. 应用程序打包
    可用于构建中小型前端应用,将源码(JS、CSS、静态资源等)打包为优化后的静态资源,供浏览器加载。
    (Vite 的生产环境打包默认使用 Rollup,正是利用其优秀的优化能力)
  3. 代码优化与转换
    支持通过插件处理非 JS 资源(如 CSS、图片)、转换语法(如 TypeScript 转 JS、JSX 转 JS)、压缩代码等。

二、Rollup 的核心优势

  1. 极致的 Tree-shaking 能力

Rollup 原生支持对 ES6 模块的静态分析,能精准识别并删除未使用的代码(Dead Code),生成的产物体积更小。
相比 Webpack,Rollup 的 Tree-shaking 更彻底(因专注于 ESM,无过多运行时代码干扰),尤其适合库打包(避免冗余代码)。

示例
若一个模块导出 addminus 两个函数,但仅 add 被使用,Rollup 会直接剔除 minus,产物中仅保留 add 相关代码。

2. 简洁的输出代码(无冗余运行时)

Rollup 打包后的代码更接近源码结构,几乎没有多余的 “运行时(runtime)” 代码(如 Webpack 中的模块加载器逻辑)。
这使得产物更易读、体积更小,尤其适合库开发者(避免给用户引入额外代码)。

对比

  • Webpack 产物:包含 __webpack_require__ 等模块加载逻辑(增加体积)。
  • Rollup 产物:直接将模块内容合并,保留原始导入导出逻辑(仅在必要时转换)。

3. 原生支持多种模块格式输出

可一次性输出多种模块格式,满足不同场景需求:

  • es:ES6 模块格式(供现代浏览器或构建工具使用)。
  • cjs:CommonJS 格式(供 Node.js 环境使用)。
  • umd:通用模块格式(兼容浏览器全局变量、AMD、CommonJS)。

配置示例

// rollup.config.js
export default {input: 'src/index.js',output: [{ file: 'dist/index.es.js', format: 'es' },{ file: 'dist/index.cjs.js', format: 'cjs' },{ file: 'dist/index.umd.js', format: 'umd', name: 'MyLibrary' }]
};

4. 插件生态简洁高效

Rollup 插件 API 设计简洁,专注于 “转换代码” 和 “处理资源”,插件通常体积小、功能单一,易于组合使用。
常用插件:

  • @rollup/plugin-node-resolve:解析 node_modules 中的依赖。
  • @rollup/plugin-commonjs:将 CommonJS 模块转为 ESM(以便 Tree-shaking)。
  • @rollup/plugin-typescript:处理 TypeScript 代码。

5. 适合库开发的细节优化

  • 保留原始代码注释:可通过配置保留 JSDoc 注释,方便生成 API 文档。
  • 作用域提升(Scope Hoisting):默认将多个模块的代码合并到同一作用域,减少函数嵌套,提升运行效率。
  • 精确的 sourcemap:生成的 sourcemap 更清晰,便于调试源码。

三、与其他工具的对比(Webpack、Vite)

工具核心优势适用场景
RollupTree-shaking 强、输出简洁、适合库JavaScript 库、中小型应用
Webpack生态全、支持复杂场景(如多页面、SSR)大型复杂应用、需要丰富 loader/plugin
Vite开发环境极速(基于原生 ESM)现代前端应用(生产依赖 Rollup)

总结

Rollup 的核心价值在于 “以最小的体积和最简洁的形式输出高质量代码”,尤其适合 JavaScript 库的打包。其极致的 Tree-shaking、无冗余运行时和多模块格式支持,使其成为库开发者的首选工具。对于中小型应用,Rollup 也能提供高效的打包体验,这也是 Vite 选择 Rollup 作为生产环境打包器的核心原因。

多进程构建模式适用于哪些场景?

多进程构建模式通过利用多核 CPU 并行处理任务提升构建效率,但并非所有场景都适用。其核心价值在于解决CPU 密集型任务的性能瓶颈,以下是其最适合的场景及适用逻辑:

一、核心适用场景:CPU 密集型构建任务

多进程的优势在于将耗时的计算任务分摊到多个 CPU 核心,避免单进程阻塞。以下场景尤其能体现其价值:

  1. 大型项目(模块数量多)
  • 特点:项目包含数千甚至数万个模块(如大型 React/Vue 应用、企业级中台系统),单进程处理时模块转译、依赖解析耗时极长。
  • 多进程作用:将模块按批次分配给不同子进程并行处理,显著减少总构建时间。例如,1000 个模块由 4 个进程并行处理,理论上可缩短至 1/4 时间(忽略进程通信开销)。
  1. 使用耗时的转译工具

当项目中使用对 CPU 消耗大的转译工具时,多进程能有效分摊压力:

  • Babel/TypeScript 转译babel-loader 处理 ES6+ 转 ES5、ts-loader 类型检查 + 转译 JS 是典型的 CPU 密集型任务,尤其是开启装饰器、复杂类型推断时。
  • CSS 预处理器高级特性:如 sass-loader 处理嵌套语法、变量计算,或 postcss-loader 配合 autoprefixer 进行复杂样式转换。
  • 示例:一个包含大量 class 组件和装饰器的 TypeScript 项目,单进程构建需 60 秒,启用 4 进程后可缩短至 20-30 秒。
  1. 代码压缩优化阶段
  • JS/CSS 压缩:代码压缩(如 Terser 压缩 JS、CSSNano 压缩 CSS)需要大量语法分析和字符处理,是构建后期的性能瓶颈。
  • 多进程作用:Webpack 5 的 terser-webpack-plugin 默认支持多进程并行压缩,将多个 chunk 分配给不同进程处理,避免单进程压缩耗时过长。例如,10 个大型 JS chunk 由 3 个进程并行压缩,总耗时可减少 50% 以上。
  1. 图片 / 资源优化(特定场景)
  • 当使用 image-webpack-loader 等工具对大量图片进行无损压缩(如 PNG 优化、WebP 转换)时,多进程可并行处理多张图片,减少资源处理耗时。

二、次要适用场景:内存受限的大型项目

Node.js 单进程内存存在限制(64 位系统约 1.7GB),大型项目构建时可能因内存不足导致崩溃(如 JavaScript heap out of memory 错误)。多进程可:

  • 将内存消耗分摊到多个子进程,每个进程独立管理内存,避免单进程内存溢出。
  • 例如,包含大量依赖库(如 node_modules 体积超过 500MB)的项目,单进程构建易内存不足,多进程可通过分散内存占用解决。

三、不适用或效果有限的场景

多进程并非 “银弹”,以下场景启用后可能无效甚至降低效率:

  1. 小型项目(模块少、依赖简单)
  • 特点:项目仅包含几十个模块,依赖少且无复杂转译(如小型静态网站、简单工具类项目)。
  • 问题:多进程启动(创建子进程、初始化环境)和进程间通信存在额外开销,可能导致总耗时增加。例如,单进程构建需 5 秒的项目,多进程可能因启动开销增至 7 秒。
  1. I/O 密集型任务为主的场景
  • 特点:构建瓶颈在于磁盘读写(如频繁读取 node_modules、大量静态资源复制)而非 CPU 计算。
  • 问题:多进程无法加速 I/O 操作(受限于磁盘读写速度),反而可能因进程竞争磁盘资源导致效率下降。例如,使用 copy-webpack-plugin 复制大量图片时,多进程对提速无帮助。
  1. 简单 loader 链处理
  • 若项目仅使用轻量 loader(如 url-loader 处理小图片、raw-loader 加载文本),或转译逻辑简单(如仅用 babel-loader 处理箭头函数),单进程已足够高效,多进程的开销会抵消收益。
  1. 开发环境的 “快速热更新” 场景
  • 开发环境中,Webpack Dev Server 依赖快速增量更新,而多进程的缓存共享和状态同步较复杂,可能导致热更新响应变慢。因此,开发环境通常建议仅在全量构建时启用多进程,增量更新阶段关闭。

四、判断是否适用的核心原则

可通过以下步骤判断项目是否需要多进程构建:

  1. 分析构建瓶颈:使用 webpack-bundle-analyzerspeed-measure-webpack-plugin 定位耗时环节,若 CPU 密集型任务(转译、压缩)占总耗时 60% 以上,则适合多进程。
  2. 测试对比:在项目中分别测试单进程和多进程的全量构建时间,若多进程耗时减少 30% 以上,则保留配置;否则关闭。
  3. 结合环境:生产环境全量构建适合多进程(追求最终效率),开发环境优先保证热更新速度(可选择性关闭)。

总结

多进程构建模式的核心适用场景是 “CPU 密集型的大型项目构建”,尤其是包含大量模块转译、复杂代码压缩的场景。对于小型项目、I/O 密集型任务或开发环境的快速迭代,多进程可能无法发挥价值甚至适得其反。实际使用中需结合项目规模、构建瓶颈和测试数据动态调整。

为什么Vite生产环境下那么慢?

Vite 在生产环境下的构建速度(vite build)相比开发环境(vite dev)确实会慢很多,但这并非 “性能问题”,而是由生产环境的核心目标和必要优化步骤决定的。这种 “慢” 是为了生成高质量、高性能的最终产物而进行的合理权衡。

一、生产环境 “慢” 的核心原因

Vite 生产环境的构建流程与开发环境有本质区别,以下是导致其耗时更长的关键因素:

  1. 从 “按需处理” 变为 “全量打包”
  • 开发环境:基于原生 ES 模块(ESM),浏览器请求某个模块时,Vite 才会即时编译该模块(如 TypeScript 转 JS、Sass 转 CSS 等),属于 “按需处理”,无需遍历所有文件,启动和更新速度极快。

  • 生产环境

    :需要将

    所有模块(包括依赖)打包为优化后的静态资源

    (如

    chunk
    

    文件)。这意味着 Vite(基于 Rollup)必须:

    • 遍历整个项目的依赖图谱(所有 import/export 关系);
    • 处理每一个模块(包括源码、node_modules 依赖);
    • 合并、拆分模块为最终的 chunk
      全量处理的工作量远大于开发环境的 “按需编译”,自然更耗时。
  1. 大量必要的代码优化步骤

生产环境的核心目标是生成体积小、加载快、运行高效的代码,因此必须执行一系列耗时的优化操作,例如:

  • Tree-shaking:分析并删除未使用的代码(需遍历模块依赖,标记无用代码);
  • 代码压缩:对 JS(Terser 或 esbuild)、CSS(cssnano)进行压缩混淆(压缩过程需解析 AST 并优化);
  • 代码分割:按路由、公共库等规则拆分 chunk(需分析模块复用率,计算最优分割策略);
  • 依赖预构建:将 node_modules 中的 CommonJS 模块转为 ESM(避免浏览器兼容问题),并合并小依赖(减少请求数);
  • 语法降级与 polyfill:根据目标浏览器(browserslist)将高版本 JS/CSS 语法转为兼容版本(如 ES6 转 ES5),并自动注入必要的 polyfill
  • SourceMap 生成:即使是生产环境的简化版 SourceMap,也需要消耗计算资源生成映射关系。

这些优化步骤本质上是 “以时间换性能”—— 构建时多花时间,用户使用时就能获得更快的加载和运行体验。

  1. Rollup 打包的特性

Vite 生产环境依赖 Rollup 进行打包(而非开发环境的原生 ESM)。Rollup 作为专注于 “构建优化产物” 的工具,其内部流程(如模块解析、依赖分析、代码生成)设计上更注重产物质量而非构建速度,因此相比开发环境的 “即时响应”,自然会更耗时。

虽然 Rollup 也在不断优化速度(如支持并行处理),但相比开发环境 “不打包直接运行” 的模式,仍有本质差距。

  1. 项目规模与依赖复杂度

生产环境构建时间与项目复杂度正相关:

  • 模块数量越多(如大型项目的 hundreds/thousands 个组件),依赖图谱越复杂,遍历和处理耗时越长;
  • 第三方依赖越多(尤其是体积大、格式复杂的库,如 lodashecharts 等),转换和打包的工作量越大;
  • 配置的插件越多(如 vite-plugin-vuevite-plugin-reactvite-plugin-svg 等),每个插件对模块的额外处理(如 JSX 转换、SVG 优化)都会累积耗时。

二、“慢” 是合理的权衡

Vite 生产环境的 “慢” 并非技术缺陷,而是前端工程化的必然选择:

  • 开发环境追求 “快反馈”(开发者体验),因此可以牺牲产物质量(不优化、不压缩);
  • 生产环境追求 “高性能产物”(用户体验),因此必须牺牲构建时间,执行全套优化。

事实上,相比 Webpack 等传统工具,Vite 的生产构建速度已经通过 Rollup 的高效设计和 esbuild 的预构建支持(如依赖转换)得到了优化,只是开发环境的 “极速” 显得生产环境相对较慢。

三、优化生产构建速度的建议

如果生产构建耗时过长,可以通过以下方式优化:

  1. 简化 SourceMap 配置:生产环境默认 sourcemap: false(或 'hidden'),避免生成完整 SourceMap(耗时且增大产物体积)。

  2. 使用 esbuild 压缩

    :在

    vite.config.js
    

    中配置

    esbuild
    

    作为压缩工具(比 Terser 快得多):

    // vite.config.js
    export default {build: {minify: 'esbuild' // 替代默认的 'terser'}
    }
    
  3. 减少不必要的插件:移除生产环境不需要的开发插件(如 vite-plugin-inspect)。

  4. 拆分大型项目:通过 build.rollupOptions.output.manualChunks 合理拆分 chunk,避免单一大 chunk 处理耗时。

  5. 利用缓存:Vite 会缓存预构建的依赖(node_modules/.vite),避免重复处理,首次构建后再次构建会更快。

总结

Vite 生产环境构建 “慢” 的核心原因是:为了生成优化后的产物,必须执行全量模块处理和一系列耗时的代码优化步骤。这种 “慢” 是开发体验与生产性能之间的合理权衡,而非技术缺陷。通过合理配置,可在保证产物质量的前提下尽可能提升构建速度。

webpack5 如何开启缓存

module.exports = {// 开启缓存cache: {type: 'filesystem', // 启用磁盘缓存(默认是 'memory' 内存缓存)// 可选配置:自定义缓存目录(默认是 node_modules/.cache/webpack)cacheDirectory: path.resolve(__dirname, '.webpack-cache'),// 可选:缓存失效条件(当这些内容变化时,缓存会失效)buildDependencies: {// 当配置文件变化时,缓存失效config: [__filename]// 可以添加其他依赖文件,如:// src: path.resolve(__dirname, 'src/**/*')},// 可选:缓存版本(当需要强制清空缓存时,修改版本号即可)version: '1.0.0'},// 其他配置...mode: 'development',entry: './src/index.js',output: {path: path.resolve(__dirname, 'dist'),filename: 'bundle.js'}
};

DLLplugin 的好处是什么?

DLLPlugin 是 Webpack 提供的一种优化插件,全称为 Dynamic Link Library Plugin,其核心作用是将不常变化的第三方库(如 React、Vue、lodash 等)与业务代码分离打包,从而显著提升构建速度和开发体验。以下是其主要优势及工作原理:

一、DLLPlugin 的核心好处

  1. 大幅缩短构建时间
  • 传统打包方式:每次构建时,Webpack 需重新编译所有代码(包括第三方库和业务代码),耗时较长。

  • DLLPlugin 优化

    • 首次构建时,将第三方库单独打包为一个或多个 DLL 文件(如 vendor.dll.js),并生成映射文件(如 manifest.json)。
    • 后续构建时,Webpack 直接引用已生成的 DLL 文件,无需重新编译第三方库,仅需处理业务代码,构建速度可提升 70% 以上。
  1. 减少开发环境的热更新时间
  • 在开发模式下,每次修改业务代码后,Webpack 只需重新打包业务代码,而无需重新处理第三方库。
  • 对于大型项目,这能将热更新时间从数秒缩短至数百毫秒,显著提升开发效率。
  1. 缓存优化,降低 CI/CD 时间
  • DLL 文件可被长期缓存(如发布到 CDN 或本地缓存),CI/CD 流程中无需重复构建相同的依赖。
  • 特别适合多团队协作的大型项目,每个团队只需维护自己的业务代码,共享公共依赖。
  1. 更小的 bundle 体积
  • DLL 文件可单独进行压缩和优化,避免与业务代码混合导致的重复打包问题。
  • 通过合理拆分 DLL,可实现更细粒度的缓存策略(如将常用库与不常用库分离)。

二、DLLPlugin 的工作原理

  1. 预编译阶段
  • 创建一个独立的 Webpack 配置文件(如

    webpack.dll.config.js
    

    ),专门用于打包第三方库:

    // webpack.dll.config.js
    const path = require('path');
    const webpack = require('webpack');module.exports = {entry: {vendor: ['react', 'react-dom', 'lodash'], // 指定需要打包的第三方库},output: {path: path.join(__dirname, 'dist'),filename: '[name].dll.js',library: '[name]_[hash]', // 暴露为全局变量},plugins: [new webpack.DllPlugin({name: '[name]_[hash]',path: path.join(__dirname, 'dist', '[name]-manifest.json'),}),],
    };
    
  • 执行 webpack --config webpack.dll.config.js,生成 vendor.dll.jsvendor-manifest.json

  1. 主项目构建阶段
  • 在主项目的 Webpack 配置中引入

    DllReferencePlugin
    

    ,告知 Webpack 哪些模块已被预编译:

    // webpack.config.js
    const webpack = require('webpack');module.exports = {// 其他配置...plugins: [new webpack.DllReferencePlugin({manifest: require('./dist/vendor-manifest.json'),}),],
    };
    
  • 主项目构建时,Webpack 遇到第三方库(如 import React from 'react')会直接从 DLL 文件中引用,无需重新编译。

三、使用场景与注意事项

  1. 适用场景
  • 大型项目:依赖众多,构建时间长。
  • 开发环境:频繁修改代码,热更新需求高。
  • CI/CD 流水线:需要快速构建和部署。
  1. 注意事项
  • DLL 文件更新:当第三方库版本升级或新增依赖时,需重新执行 DLL 构建。
  • 生产环境优化:DLL 文件可配合 CommonsChunkPluginSplitChunksPlugin 进一步优化。
  • 与 Tree Shaking 结合:需确保 DLL 中的模块支持 Tree Shaking(如使用 ES6 模块语法)。

四、与其他优化方式的对比

优化方式适用场景优势局限性
DLLPlugin第三方库稳定,构建频繁显著提升构建速度需要额外维护 DLL 配置
CachePlugin多次构建间依赖变化小自动缓存模块,配置简单缓存粒度较粗
HMR(热更新)开发环境实时反馈仅更新修改的模块对大型依赖处理效果有限
Code Splitting优化运行时加载性能减小首屏加载体积与构建速度无关

五、总结

DLLPlugin 通过预编译第三方库的方式,将构建过程中的 “不变部分” 与 “变化部分” 分离,从而大幅提升构建效率。对于中大型项目,尤其是依赖较多且更新频繁的场景,这是一种性价比极高的优化手段。

React版本更换如何检测DLL

在 Webpack 的 DLLPlugin 工作流程中,预编译第三方库(如 React)时并不会主动 “检测版本变化”,但可以通过一系列间接机制感知依赖变化并触发重新编译。以下从原理、关键机制和实践方案三个层面详细说明:

一、DLLPlugin 预编译的核心原理

DLLPlugin 的核心作用是将第三方库(如 React、Lodash 等)提前编译为独立的动态链接库(DLL),并生成 manifest.json 清单文件。主项目构建时通过 DLLReferencePlugin 读取清单,直接复用预编译的 DLL 代码,避免重复编译,从而提升构建速度。

  • 预编译产物:包含第三方库代码的 *.dll.js 文件(如 react.dll.js)和记录模块映射关系的 manifest.json
  • 核心依赖manifest.json 中记录了第三方库的模块路径、ID 与 DLL 代码的映射关系(例如 React 模块在 DLL 中的具体位置)。

二、感知 React 版本变化的关键机制

当 React 版本变化时,其代码内容、模块结构或路径可能发生改变,导致预编译的 DLL 失效。DLLPlugin 虽不直接检测 “版本号”,但可通过以下机制感知这种变化:

  1. 文件内容哈希变化(最核心机制)

React 版本变化会直接导致其源码文件内容改变(例如函数实现、导出方式调整)。在预编译时,Webpack 会通过 内容哈希(contenthash) 感知这种变化:

  • 预编译阶段:Webpack 对第三方库的源码文件(如 node_modules/react/index.js)计算哈希(基于文件内容),并将哈希嵌入 DLL 文件名或 manifest.json 中。

  • 版本变化影响

    :React 版本更新后,源码文件内容改变,哈希值会发生变化。此时若仍使用旧的 DLL 文件,会导致:

    • DLL 代码与源码不匹配,manifest.json 中的模块映射关系失效;
    • 主项目构建时 DLLReferencePlugin 读取清单后,发现模块路径或内容不匹配,可能触发报错(如 “模块未找到”)或运行时异常(如 React 语法错误)。
  1. manifest.json 清单的映射失效

manifest.json 是连接预编译 DLL 和主项目的关键。它记录了第三方库中每个模块的 “原始路径” 与 “DLL 中的内部 ID” 的映射关系(例如 react 模块对应 DLL 中的 ID 1)。

当 React 版本变化时:

  • 若模块结构改变(如新增 / 删除导出、内部文件拆分),manifest.json 中的映射关系会与实际源码不匹配;
  • 主项目构建时,DLLReferencePlugin 会根据清单查找模块,若发现模块不存在或路径不匹配,会提示 “模块无法解析”,间接反映依赖已变化。
  1. 依赖版本的 “被动校验”

DLLPlugin 本身不直接读取 package.json 中的版本号,但可通过工具或脚本间接校验版本是否与预编译时一致:

  • 版本记录:预编译时可记录当前 React 的实际版本(如从 node_modules/react/package.jsonversion 字段读取),并存储在临时文件(如 .dll-versions.json)中。
  • 校验触发:主项目构建前,通过脚本对比当前 React 版本与记录版本,若不一致则强制重新生成 DLL。

三、实践:确保 React 版本变化后自动更新 DLL

为避免 React 版本变化导致的 DLL 失效问题,需在构建流程中添加 “变化检测” 逻辑。以下是具体方案:

1. 基于文件哈希的自动感知

利用 Webpack 的哈希机制,让 DLL 文件名包含内容哈希(如 react.[contenthash].dll.js)。当 React 版本变化导致源码内容改变时,哈希值会更新,旧的 DLL 文件会被新文件替代,主项目自然引用新 DLL。

配置示例

// dll.config.js(DLL 预编译配置)
module.exports = {entry: {react: ['react', 'react-dom'] // 预编译 React 相关库},output: {path: path.join(__dirname, 'dll'),filename: '[name].[contenthash].dll.js', // 文件名包含内容哈希library: '[name]_dll'},plugins: [new webpack.DllPlugin({name: '[name]_dll',path: path.join(__dirname, 'dll', '[name].manifest.json') // 生成带哈希的清单})]
};
  • 效果:React 版本变化 → 源码内容变化 → contenthash 变化 → 生成新的 DLL 文件和清单 → 主项目自动引用新文件。

2. 版本校验脚本(主动检测)

通过构建脚本主动校验 React 版本是否与上次预编译时一致,若不一致则触发 DLL 重新编译。

实现步骤

  1. 记录版本:预编译 DLL 时,从 node_modules/react/package.json 中读取版本号,存储到日志文件(如 .dll-versions.json):

    // .dll-versions.json
    {"react": "18.2.0","react-dom": "18.2.0"
    }
    
  2. 构建前校验:在主项目构建脚本(如 package.jsonscripts)中添加版本对比逻辑:

    // 检查版本是否变化的脚本 check-dll-version.js
    const fs = require('fs');
    const currentReactVersion = require('react/package.json').version;
    const lastVersions = require('./.dll-versions.json');if (currentReactVersion !== lastVersions.react) {console.log('React 版本变化,重新生成 DLL...');// 执行 DLL 预编译命令(如 webpack --config dll.config.js)require('child_process').execSync('npm run build:dll');// 更新版本记录fs.writeFileSync('./.dll-versions.json', JSON.stringify({...lastVersions,react: currentReactVersion}));
    }
    
  3. 集成到构建流程:在主构建命令前执行校验脚本:

    // package.json
    {"scripts": {"prebuild": "node check-dll-version.js", // 构建前先校验"build": "webpack","build:dll": "webpack --config dll.config.js"}
    }
    

3. 基于锁文件的依赖变化检测

通过监控 package-lock.jsonyarn.lock 的变化,间接感知第三方库版本更新。这些锁文件记录了依赖的精确版本和安装路径,当 React 版本变化时,锁文件内容会更新。

  • 实践方案

    :在构建工具(如 Webpack、Gulp)中添加对锁文件的监听,若锁文件变化,则触发 DLL 重新编译:

    javascript

    // webpack.config.js
    const { watch } = require('fs');watch('./package-lock.json', (event, filename) => {console.log('依赖锁文件变化,重新生成 DLL...');require('child_process').execSync('npm run build:dll');
    });
    

四、为什么版本变化会导致问题?

若 React 版本变化后未重新生成 DLL,会导致以下问题:

  1. 运行时错误:旧 DLL 中的 React 代码与新版本源码不兼容(如 React 18 新增的 createRoot 与旧 DLL 中的 render 冲突)。
  2. 构建报错manifest.json 中记录的模块映射失效,主项目构建时 DLLReferencePlugin 无法找到对应模块,提示 “Module not found”。
  3. 逻辑异常:即使无报错,也可能因代码不匹配导致功能异常(如 Hooks 规则失效、状态更新逻辑错误)。

总结

DLLPlugin 本身不主动 “检测版本变化”,但可通过以下方式确保 React 版本变化后 DLL 及时更新:

  1. 文件哈希机制:利用 contenthash 让 DLL 文件名随内容变化,自动区分新旧版本。
  2. 版本校验脚本:对比当前依赖版本与上次预编译版本,差异时触发重新编译。
  3. 锁文件监听:监控 package-lock.json 变化,间接感知依赖更新。

通过以上机制,可确保预编译的 DLL 始终与当前 React 版本匹配,避免因版本变化导致的构建或运行时问题。

引用

小时候觉得忘带作业是天大的事,高中的时候觉得考不上大学是天大的事,恋爱的时候觉得和喜欢的人分开是天大的事,但现在回头看看,那些难以跨过的山,其实都已经跨过了,以为不能接受的也都接受了。生活充满了选择,遗憾也不过是常态,其实,人通常无论习惯做什么选择,都会后悔,大家总是习惯美化当时自己没有选择的那条路,可是大家都心知肚明,就算时间重来一次,以当时的心智和阅历,还是会作出同样的选择,那么故事的结局还重要吗。我想,人生就是一场享受过程的修行,失之东隅,收之桑榆,回头看,轻舟已过万重山,向前看,前路漫漫亦灿灿。

相关内容

写下自己求职记录也给正在求职得一些建议-CSDN博客

从初中级如何迈入中高级-其实技术只是“入门卷”-CSDN博客

前端梳理体系从常问问题去完善-基础篇(html,css,js,ts)-CSDN博客

http://www.dtcms.com/a/393771.html

相关文章:

  • Go语言在K8s中的核心优势
  • 旅游门票预订系统支持微信小程序+H5
  • Requests 网络请求:Python API 交互与数据获取
  • 基于Dify实现简历自动筛选过滤
  • PHP中常见数组操作函数
  • 避坑指南:鸿蒙(harmony next)APP获取公钥和证书指纹的方法
  • Java 大视界 -- Java 大数据在智能教育学习效果评估与教学质量改进中的深度应用(414)
  • 【场景题】如何解决大文件上传问题
  • 云原生复杂多变的环境中的安全防护方案
  • Python10-逻辑回归-决策树
  • 如何生成一个不会重复随机数?
  • 【精品资料鉴赏】155页WORD大型制造企业MES制造执行系统建设方案
  • 定时计划任务
  • 【脑电分析系列】第23篇:癫痫检测案例:从频谱特征到深度学习模型的CHB-MIT数据集实战
  • `CookieStore` API
  • 数据可视化的中间表方案
  • 编译运行duckdb rust插件模板extension-template-rs
  • 接口测试流程+jmeter并发+面试题(总结)
  • JMeter下载安装及入门教程
  • Oracle体系结构-Java Pool详解
  • ​​Service Worker 缓存 与 HTTP 缓存 是什么关系?
  • c++ 之三/五法则
  • 传输层协议 UDP
  • 关于类和对象(一)
  • 多人协作下的游戏程序架构 —— 分层方案
  • 机器学习中三个是基础的指标:​准确率 (Accuracy)​、精确率 (Precision)​​ 和 ​召回率 (Recall)​
  • 《Web端图像剪辑方案:Canvas API与JavaScript实现》
  • DeepSeek 登《自然》封面,OpenAI 推出 GPT-5-Codex,Notion Agent 首亮相!| AI Weekly 9.15-9.21
  • 多线程-初阶
  • 在 R 语言中,%>% 是 管道操作符 (Pipe Operator),它来自 magrittr 包(后被 dplyr 等 tidyverse 包广泛采用)