web 前端模块化有哪些方式
- IIFE(立即执行函数表达式) 最早期的模块化方案,通过函数作用域隔离变量,避免全局污染。
js
// 模块定义
const moduleA = (function() {
const privateVar = '私有变量';
function privateFn() { /* ... */ }
return {
publicMethod: () => { /* ... */ },
publicVar: '公共变量'
};
})();
// 使用
moduleA.publicMethod();
缺点:依赖手动管理,无法动态加载。
- CommonJS(Node.js 模块系统)
用于 Node.js 环境,采用同步加载方式,通过 require
导入、module.exports
导出。
js
// 模块 a.js
module.exports = {
foo: () => console.log('foo'),
bar: 123
};
// 模块 b.js
const a = require('./a.js');
a.foo(); // 调用 a 模块的方法
缺点:同步加载不适合浏览器环境(会阻塞渲染)。
- AMD
专为浏览器设计,支持异步加载模块,代表库为 RequireJS。
js
// 定义模块
define('moduleA', ['dependency'], function(dep) {
return {
method: () => dep.doSomething()
};
});
// 加载模块
require(['moduleA'], function(moduleA) {
moduleA.method();
});
- CMD
结合 CommonJS 和 AMD 的特点,就近依赖、延迟执行,代表库为 SeaJS。
js
// 模块定义
define(function(require, exports, module) {
const dep = require('./dependency'); // 就近依赖
exports.method = () => dep.doSomething();
});
目前已逐渐被 ES Modules 替代。
- UMD
通用模块格式,兼容 CommonJS、AMD 和全局变量,用于跨环境模块(如工具库)。
js
(function(root, factory) {
if (typeof define === 'function' && define.amd) {
define(['dependency'], factory); // AMD
} else if (typeof module === 'object' && module.exports) {
module.exports = factory(require('dependency')); // CommonJS
} else {
root.moduleName = factory(root.dependency); // 全局变量
}
})(this, function(dep) {
return { /* 模块内容 */ };
});
- ES Modules
ES6 官方标准化的模块化方案,浏览器和 Node.js 均支持,采用静态分析(编译时加载)。
js
// 模块 a.js
export const name = 'moduleA';
export function func() { /* ... */ }
// 模块 b.js
import { name, func } from './a.js';
console.log(name);
func();
优点:静态检查、Tree-shaking 优化、原生支持;缺点:浏览器需通过 <script type="module">
启用。
pnpm 有什么优势
- 节省磁盘空间,减少重复存储
所有安装过的包会被存储在一个统一的全局存储目录 (如~/.pnpm-store
)中,不同项目引用同一版本的包时,只会保留一份副本,通过硬链接 或符号链接 指向项目的node_modules
。 - 安装速度更快
由于依赖包无需重复下载和存储,pnpm 安装依赖时,优先从全局存储中复用已有的包,减少网络请求和磁盘写入操作。 - 严格的依赖隔离,避免 幽灵依赖
npm/yarn 的node_modules
采用 "扁平结构",可能导致项目间接依赖的包被意外访问(即 "幽灵依赖",如import 'lodash'
但未在package.json
中声明)。 pnpm 的node_modules
结构为嵌套 + 链接 ,只有在package.json
中显式声明的依赖才会被暴露,避免隐式依赖导致的潜在问题,更符合规范。 - 支持 monorepo 项目的高效管理
对于多包管理的 monorepo 项目,pnpm 提供了原生支持(通过pnpm workspace
),能高效处理包之间的依赖关系,避免重复安装,提升构建效率。
webpack 有哪些常见的 Loader
js
raw-loader: 允许你将文件作为字符串导入到 JavaScript 模块中
file-loader: 把文件输出到文件夹中,在代码中通过相对 url 去引用输出的文件 (图片 字体)
url-loader: 与 file-loader 类似,区别 可以设置阈值 大于 交给file-loader 处理,小于则 base64 编码 处理图片
image-loader: 加载并压缩图片文件
json-loader: 加载 json 文件
babel-loader: 将 es6 转换成 es5
ts-loader: 将 ts 语言转换成 js
sass-loader 转换 css
less-loader 转换 css
css-loader: 加载 css
style-loader: 把 css 注入到 js 中,通过 dom 操作加载 css
postcss-loader: 扩展 css 语法,使用下一代 css
eslint-loader 代码检查
有哪些常见的 Plugin
define-plugin: 定义环境变量
js
ignore-plugin: 忽略部分文件
html-webpack-plugin: 简化 html 文件创建
uglifyjs-webpack-plugin: 压缩 js
mini-css-extract-plugin: 分离样式文件,css 提取为单独文件,支持按需加载
clean-webpack-plugin: 目录清理
webpack-bundle-analyzer: 可视化 webpack 输出文件的体积
speed-measure-webpack-plugin: 看到每个 loader plugin 执行耗时
Loader 和 Plugin 的区别
-
Loader(加载器):处理文件转换
核心功能 :将非 JavaScript 文件(如 CSS、图片、TypeScript 等)转换为 Webpack 可识别的模块(最终转为 JS 模块)。
工作时机 :在 Webpack 的模块解析阶段 工作,当遇到特定类型的文件时,触发对应的 Loader 进行转换。
使用方式 :在webpack.config.js
的module.rules
中配置,通过test
匹配文件类型,use
指定使用的 Loader。 -
Plugin(插件):扩展 Webpack 功能
核心功能 :解决 Loader 无法完成的其他任务,如优化打包结果、资源管理、环境变量注入等,扩展 Webpack 的整个构建流程。
工作时机 :贯穿 Webpack 的整个生命周期 (从初始化、编译、输出到结束),可在不同阶段介入处理。
使用方式 :在webpack.config.js
的plugins
数组中配置,通常需要实例化(通过new
关键字)。
维度 | Loader | Plugin |
---|---|---|
作用 | 转换特定类型的文件(如 CSS→JS) | 扩展 Webpack 功能(如优化、生成文件) |
处理对象 | 针对文件内容进行转换 | 针对整个构建流程进行扩展 |
工作阶段 | 模块解析阶段(加载时) | 整个生命周期(从初始化到输出) |
配置方式 | 在 module.rules 中配置 |
在 plugins 数组中实例化 |
本质 | 函数(接收文件内容,返回处理后内容) | 类(需实现 apply 方法,注册钩子) |
Webpack 构建流程
1. 初始化阶段
- 读取并合并配置参数(webpack.config.js、CLI 参数、默认配置)
- 创建 Compiler 实例,负责整个构建过程的统筹
- 加载所有配置的插件,执行插件的 apply 方法
2. 编译阶段
- 入口处理:从配置的 entry 出发,解析入口文件
- 模块解析:对每个模块进行递归解析(调用相应的 loader 对不同类型的文件进行转换,分析模块依赖关系,形成依赖图谱)
- 模块转换:将处理后的模块转换为可执行的代码块
3. 输出阶段
- Chunk 优化:对生成的代码块进行优化(如代码分割、Tree-shaking 等)
- 资源生成:根据输出配置(output)将 Chunk 写入到文件系统
- 插件介入:在输出过程中,插件可以对最终产物进行二次处理(如压缩、添加哈希等)
4. 完成阶段
- 完成所有文件的输出
- 执行构建完成后的回调函数

Compiler 和 Compilation 区别
1. Compiler
- 本质:全局唯一的编译器实例,贯穿整个 Webpack 生命周期。
- 职责 :
- 管理 Webpack 从启动到退出的全过程,是构建流程的总控制器。
- 保存着 Webpack 的全局配置(如
entry
、output
、plugins
等)。 - 提供生命周期钩子(如
run
、compile
、done
等),供插件在不同阶段介入。
- 生命周期:从 Webpack 启动时创建,直到构建完全结束才被销毁。
2. Compilation
- 本质:代表一次具体的编译过程(可能有多次),是构建过程中的 "工作单元"。
- 职责 :
- 负责模块的解析、依赖分析、代码转换和优化等具体编译工作。
- 维护当前编译的所有模块(
modules
)、代码块(chunks
)、资源(assets
)等信息。 - 提供编译阶段的钩子(如
buildModule
、seal
、optimize
等),用于干预模块处理和代码优化。
- 生命周期:每次触发编译时创建(如首次构建、watch 模式下文件变化时),编译完成后销毁。
维度 | Compiler | Compilation |
---|---|---|
生命周期 | 全局唯一,贯穿整个构建流程 | 每次编译创建,编译结束后销毁 |
核心作用 | 控制整体流程,管理全局配置 | 执行具体编译工作,处理模块和依赖 |
关联关系 | 可以创建多个 Compilation 实例 | 由 Compiler 实例创建和管理 |
钩子类型 | 全局流程钩子(如启动、完成) | 编译阶段钩子(如模块解析、优化) |
文件指纹
在 Webpack 中配置文件指纹(即给输出文件添加唯一标识),主要用于解决浏览器缓存问题,确保用户能获取到最新的文件。配置方式因文件类型(JS、CSS、图片等)略有不同,下面是具体实现方法:
- 核心配置:
output.filename
Webpack 通过output.filename
配置出口文件的命名规则,通过内置的 占位符(placeholders) 实现文件指纹:
常用占位符:
[hash]
:整个项目的哈希值,每次构建(内容有变化)都会改变[chunkhash]
:基于入口 chunk 的哈希值,不同入口的哈希独立[contenthash]
:基于文件内容的哈希值,内容不变则哈希不变(推荐)[name]
:文件名[id]
:chunk 的 id[ext]
:文件扩展名
(1)JS 文件指纹
直接在 output
中配置,推荐使用 [contenthash]
:
js
module.exports = {
output: {
// 格式:文件名.内容哈希.扩展名
filename: 'js/[name].[contenthash:8].js',
// 非入口的 chunk(如异步加载的模块)
chunkFilename: 'js/[name].[contenthash:8].chunk.js'
}
};
(2)CSS 文件指纹
CSS 通常通过 mini-css-extract-plugin
提取为单独文件,需在该插件中配置:
配置
js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
module: {
rules: [
{
test: /.css$/,
// 用 MiniCssExtractPlugin.loader 替代 style-loader
use: [MiniCssExtractPlugin.loader, 'css-loader']
}
]
},
plugins: [
new MiniCssExtractPlugin({
// CSS 文件指纹配置
filename: 'css/[name].[contenthash:8].css',
chunkFilename: 'css/[name].[contenthash:8].chunk.css'
})
]
};
(3)图片 / 字体等资源文件
js
module.exports = {
module: {
rules: [
{
test: /.(png|jpe?g|gif|svg)$/,
use: [
{
loader: 'url-loader',
options: {
limit: 10240, // 小于 10KB 的图片转 base64
// 资源文件指纹配置
name: 'img/[name].[contenthash:8].[ext]'
}
}
]
}
]
}
};
最佳实践
- 优先使用
[contenthash]
:只有文件内容变化时,哈希才会改变,最大化利用缓存 - 按类型分目录 :如
js/
、css/
、img/
,便于管理 - 哈希长度 :通常取 8 位(如
[contenthash:8]
),既保证唯一性又避免文件名过长 - 配合 HtmlWebpackPlugin:自动引入带指纹的文件,无需手动修改 HTML 引用
Babel 原理
Babel 是一个广泛使用的 JavaScript 编译器,主要用于将 ES6+(ES2015 及以上)的代码转换为向后兼容的 JavaScript 语法,以便在旧版本浏览器或环境中运行。其核心原理可以概括为「解析 - 转换 - 生成」三个阶段的工作流程。
- 解析(Parse)
将原始代码字符串转换为抽象语法树(AST,Abstract Syntax Tree),分为两个步骤:
- 词法分析
将代码字符串拆分为为不可再分的最小单元(称为 Token,如关键字、标识符、运算符等)。例如,const a = 1
会被拆分为const
、a
、=
、1
等 Token。 - 语法分析
根据 Token 序列和语法规则,构建出描述代码结构的 AST。AST 是一种树形结构,每个节点代表代码中的一个语法结构(如变量声明、函数调用等)。
Babel 使用@babel/parser
(基于 Acorn 实现)完成解析过程。
- 转换(Transform)
对生成的 AST 进行遍历和修改,将高版本语法转换为低版本语法。这是 Babel 最核心的步骤: 通过@babel/traverse
工具遍历 AST 的每个节点。例如,将 ES6 的箭头函数() => {}
转换为 ES5 的function() {}
,将class
转换为原型链语法等。经过插件处理后,得到符合目标环境语法的新 AST。 - 生成(Generate)
将转换后的 AST 重新转换为代码字符串:
递归生成代码 :@babel/generator
会递归遍历新 AST 的每个节点,将其转换为对应的代码字符串,并自动添加分号、空格等格式。
保留源码信息:在生成过程中,会尽量保留原始代码的注释、换行等格式(如果配置了相关选项)。
webpack 热更新
在开发时,修改代码后可以保留当前页面状态(如表单输入、页面滚动位置等),同时看到更新效果。
- 启动阶段:
- Webpack 构建时会在输出文件中注入 HMR 运行时代码(
webpack/hot/dev-server
) - 开发服务器(如
webpack-dev-server
或webpack-dev-middleware
)启动一个 WebSocket 连接,用于客户端和服务器的实时通信
- 文件变化时:
- Webpack 监听到文件修改,重新编译受影响的模块,生成 更新后的 chunk 和 更新清单(manifest)
- 服务器通过 WebSocket 将更新清单发送给客户端
- 客户端更新:
- 客户端根据清单请求获取更新的 chunk
- HMR 运行时替换旧模块,执行模块的
module.hot.accept
回调(如果有) - 如果模块不支持热更新,会触发页面刷新

Webpack 5 显著提升和改进
- 持久化缓存 :Webpack 5 引入了更强大的持久化缓存机制,可在多次构建间有效利用缓存,大幅减少重复计算和构建时间。
webpack 会缓存生成的 webpack 模块和 chunk, 来改善构建速度 缓存在 webpack5 中默认开启,缓存默认是在内存里,但可以对 cache 进行设置 webpack 追踪了每个模块的依赖,并创建了文件系统快照。此快照会与真实文件系统进行比较,当检测到差异时,将触发对应模块的重新构建
- 模块联邦
实现跨应用之间的模块共享,使用 npm 的缺点:npm 包升级要通知所有的项目升级,每个项目的升级都要重新打包构建。案例:
js
// 在 HOME 的入口文件或其他需要使用的文件中
// 动态导入远程模块中的组件
async function loadRemoteComponent() {
const { Header } = await import('nav/./Header');
// 这里假设 Header 是一个 React 组件,使用方式如下(以 React 为例)
ReactDOM.render(<Header />, document.getElementById('root'));
}
loadRemoteComponent();
- tree-shaking
在 Webpack5 打包之后,只会有function1 了,
- 自动清除打包目录
代码分割
- 多入口起点配置
js
module.exports = {
entry: {
main: './src/main.js',
vendor: './src/vendor.js'
}
}
- 动态导入
Dynamic import
js
// main.js
import('./moduleA').then((module) => {
// 使用moduleA
})
- splitChunks 插件
js
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all'
}
}
}
vite 中怎么修改 plugin 的执行顺序
● 1. enforce: 'pre'
设置此值的插件会在 Vite 核心插件之前执行。
● 2. 不设置 enforce
默认情况下,插件会在 Vite 核心插件之后、其他构建插件之前执行。
● 3. enforce: 'post'
设置此值的插件会在 Vite 构建插件之后执行。
1.首先执行带有 enforce: 'pre' 的用户插件
2.接着是 Vite 的核心插件
3.然后是没有设置 enforce 属性的用户插件和其他 Vite 构建插件
4.紧接着是带有 enforce: 'post' 的用户插件
5.最后是 Vite 的后置构建插件(如代码最小化、生成 manifest 文件等)
js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import myPlugin from 'my-vite-plugin'
export default defineConfig({
plugins: [
// 在核心插件前执行的插件
{ ...myPlugin(), enforce: 'pre' },
// 核心插件
vue(),
// 默认顺序的插件
anotherPlugin(),
// 在构建插件后执行的插件
{ ...postPlugin(), enforce: 'post' }
]
})
Vite为什么更快?
- webpack 需要先打包,然后启动本地服务器。由于浏览器支持 ES module,vite 是先启动了服务器,然后按需编译构建。
- vite 编译使用 esbuild ,编译速度比 babel 等工具快
- 生产打包过程使用rollup,对 treeshaking 和代码拆分的支持度好,减少打包体积。
- HRM:Vite 只会重新加载发生变化的模块,而不是重新打包整个应用
Vite 的依赖预加载机制
主要针对项目中不会频繁变动的第三方依赖(如node_modules中的库)进行优化
devServer启动 -> Vite 会先对依赖进行预构建 -> 通过esbuild将依赖转换为ES 模块(ESM) 格式(将 CommonJS 或 UMD 格式转换为ESM)
合并依赖:将多个关联的依赖模块合并为单个文件(如将lodash的多个子模块合并),减少浏览器请求次数。
预构建结果(转换后的 ESM 文件、依赖关系图谱等)会缓存到node_modules/.vite目录,后续启动时若依赖未变则直接复用,大幅提升启动速度。避免了重复执行耗时的依赖处理工作。
vite 中的插件
生命周期:
- config:用于修改 Vite 配置,通常在构建或开发过程中使用。
- configureServer:用于修改开发服务器的行为,如自定义请求处理。
- transform:对文件内容进行转换,适用于文件类型转换或代码处理。
- buildStart 和 buildEnd:在构建过程开始和结束时触发,适用于日志记录或优化操作。
webpack 的 bundle 和 chunk
chunk:在内存中处理的代码块 | Webpack 内部构建过程中的中间产物 | 用于 Webpack 内部的模块依赖管理和代码分割,帮助 Webpack 跟踪模块间的关系
bundle:输出到磁盘的文件 | 是最终供浏览器加载执行的文件,包含了经过处理(如压缩、转换)的代码。