Webpack 学习笔记
入口起点(entry point)
用法
语法:entry: string | [string] | {
// webpack.config.js
module.exports = {
entry: {
app: './src/app.js',
file: ['./src/file_1.js', './src/file_2.js'],
a1: {
dependOn: 'app', // 当前入口所依赖的入口。它们必须在该入口被加载前被加载。
filename: '', // 指定要输出的文件名称
import: '', // 启动时需加载的模块
library: '', // 指定 library 选项,为当前 entry 构建一个 library
runtime: '', // 运行时 chunk 的名字。如果设置了,就会创建一个新的运行时 chunk。在 webpack 5.43.0 之后可将其设为 false 以避免一个新的运行时 chunk
publicPath: '', // 当该入口的输出文件在浏览器中被引用时,为它们指定一个公共 URL 地址
},
}
}
注意事项
- runtime 和 dependOn 不应在同一个入口上同时使用
- 确保 runtime 不能指向已存在的入口名称
- dependOn 不能是循环引用的
经验之谈
- 在webpack4.x及以上版本,不要为 vendor 或其他不是执行起点创建 entry,而是使用 optimization.splitChunks 选项,将 vendor 和 app(应用程序) 模块分开,并为其创建一个单独的文件
- 每个 HTML 文档只使用一个入口起点
loader
定义
loader用于转换某些类型的模块,webpack 只能理解 JavaScript 和 JSON 文件,这是 webpack 开箱可用的自带能力。loader 让 webpack 能够去处理其他类型的文件,并将它们转换为有效模块,以供应用程序使用,以及被添加到依赖图中。
使用方式
配置方式:在 webpack.config.js 文件中指定 loader
内联方式:在每个 import 语句中显式指定 loader,通过为内联 import 语句添加前缀,可以覆盖配置文件中的所有 loader, preLoader 和 postLoader
// 传递参数(类似 webpack 配置中的 options) import 'style-loader!css-loader?modules=true!./styles.css' // 多个参数用 & 分隔 import 'url-loader?limit=1024&name=images/[hash].[ext]!./image.png' // 使用 ! 前缀,将禁用所有已配置的 normal loader(普通 loader) import Styles from '!style-loader!css-loader?modules!./styles.css' // 使用 !! 前缀,将禁用所有已配置的 loader(preLoader, loader, postLoader) import Styles from '!!style-loader!css-loader?modules!./styles.css' // 使用 -! 前缀,将禁用所有已配置的 preLoader 和 loader,但是不禁用 postLoaders import Styles from '-!style-loader!css-loader?modules!./styles.css'
知识点
- loader本质上是导出为函数的JavaScript模块,webpack内部的loader runner会调用该函数,并将上一个loader的输出结果作为参数传给该函数
- loader的执行顺序是从右到左,从下到上,可以通过enforce配置项或loader的pitch方法改变执行顺序,最后一个loader的输出结果应该为String或Buffer类型
- loader可以是同步的,也可以是异步的
- loader运行在Node.js中
- loader内部的this上可访问的属性和方法都通过loader context上下文绑定
编写一个loader
// my-loader.js
// 同步loader
module.exports = function(source, map, meta) {
// 单个处理结果可以直接return
// return doSomething(source)
// 多个处理结果则必须调用this.callback()
this.callback(null, doSomething(source), map, meta)
return
}
// 异步loader,必须调用this.async()来告知loader runner等待异步输出结果
// 它会返回 this.callback() 回调函数。随后 loader 必须返回 undefined 并且调用该回调函数
module.exports = function(source, map, meta) {
var callback = this.async()
doSomeAsyncthing(source, function(err, res) {
if (err) {
return callback(err)
}
callback(null, res, map, meta)
})
}
plugin
定义
webpack插件是一个具有 apply 方法的 JavaScript 类。apply 方法会被 webpack compiler 调用,并且在 整个 编译生命周期都可以访问 compiler 对象,插件目的在于解决 loader 无法实现的其他事,包括:打包优化,资源管理,注入环境变量。
使用方式
- 配置方式:在webpack.config.js文件中,向 plugins 属性传入一个 new 实例
- Node API方式:
const webpack = require('webpack') // 访问 webpack 运行时(runtime) const configuration = require('./webpack.config.js') let compiler = webpack(configuration) new webpack.ProgressPlugin().apply(compiler) compiler.run(function (err, stats) { // ... })
编写一个plugin
一个生成构建文件清单的插件:
// fileListPlugin.js
class FileListPlugin {
constructor(options) {
this.options = Object.assign({
filename: '',
title: '',
}, options)
}
apply(compiler) {
const { filename, title } = this.options
compiler.hooks.emit.tapAsync('FileListPlugin', function (compilation, callback) {
// 创建文件内容
let fileList = title
// 遍历所有编译资源
for (const asset in compilation.assets) {
const size = compilation.assets[asset].size()
fileList += `- ${asset} (${size} bytes)\n`
}
// 将清单作为新资源添加到编译中
compilation.assets[filename] = {
source: () => fileList,
size: () => fileList.length
}
callback()
})
}
}
module.exports = FileListPlugin
模块解析
webpack 使用 enhanced-resolve 来解析文件路径,支持三种文件路径的解析:
- 相对路径:
import '../src/file1' - 绝对路径:
import '/home/me/file' - 模块路径:
import 'module/lib/file'
解析规则
当文件路径类型为模块路径时,路径解析遵循以下规则:
基于webpack.config.js中配置的resolve.modules中指定的目录(默认是
['node_modules'])进行检索,如果添加一个目录到模块搜索目录,此目录优先于 node_modules/ 搜索// webpack.config.js const path = require('path') module.exports = { //... resolve: { modules: [path.resolve(__dirname, 'src'), 'node_modules'], }, }// src/index.js // 将优先从src目录下开始检索 import { Tabs } from 'antd'如果模块(package)中有package.json文件,则在resolve.exportsFields配置项中指定的字段会被依次查找,优先级高于package.json中的main/module字段
// webpack.config.js module.exports = { //... resolve: { exportsFields: ['exports', 'myCompanyExports'], }, }// myPkg中的package.json { name: 'myPkg', scripts: {}, exports: { '.': { "require": "./index.cjs", "import": "./index.mjs", }, }, myCompanyExports: { '.': { "require": "./index.cjs", "import": "./index.mjs", }, }, }// src/index.js // ES模块导入 import myPkg from 'myPkg' // CommonJS模块导入 const myPkg = require('myPkg')
一旦根据上述规则解析路径后,enhanced-resolve 将会检查路径是指向文件还是文件夹。
如果路径指向文件:
- 如果文件具有扩展名,则直接将文件打包
- 否则,将使用 resolve.extensions 选项作为文件扩展名来解析,此选项会告诉解析器在解析中能够接受那些扩展名(例如 .js,.jsx)
如果路径指向一个文件夹,则进行如下步骤寻找具有正确扩展名的文件:
- 如果文件夹中包含 package.json 文件,则会根据 resolve.mainFields 配置中的字段顺序查找,并根据 package.json 中的符合配置要求的第一个字段来确定文件路径。
// webpack.config.js module.exports = { //... resolve: { mainFields: ['browser', 'module', 'main'], }, }// myPkg中的package.json { "name": 'myPkg', "browser": "build/myPkg.js", "module": "index" }// src/index.js // 从build/myPkg.js中查找 import * as myPkg from 'myPkg' - 如果不存在 package.json 文件或 resolve.mainFields 没有返回有效路径,则会根据 resolve.mainFiles 配置选项中指定的文件名顺序查找,看是否能在 import/require 的目录下匹配到一个存在的文件名
- 然后使用 resolve.extensions 选项,以类似的方式解析文件扩展名
runtime和mainfest
在使用 webpack 构建的典型应用程序或站点中,有三种主要的代码类型:
- 你或你的团队编写的源码。
- 你的源码会依赖的任何第三方的 library 或 “vendor” 代码。
- webpack 的 runtime 和 manifest,管理所有模块的交互。
runtime
主要指在浏览器运行过程中,webpack用来连接各个模块化代码的代码,runtime代码使模块化代码能加载和解析其他的模块化代码,从而与其他模块化代码交互。可以使用 optimization.runtimeChunk: 'single' 选项将 runtime 代码拆分为一个单独的 chunk。
mainfest
mainfest是一张映射表,记录了模块打包后的关键元数据,用来在运行时管理模块的加载和依赖关系。理解 Manifest 是掌握 Webpack 运行机制的关键,尤其在 代码分割 和 缓存策略 优化中至关重要!
mainfest的主要作用有以下三点:
- 模块标识:将打包前的模块路径映射为打包后的模块ID
- 动态加载:管理异步加载的模块(如import()拆分的代码块)
- 缓存优化:通过内容哈希实现持久缓存
典型的mainfest内容如下:
{
"modules": {
"./src/index.js": 0, // 模块路径 → 模块ID
"./src/utils.js": 1
},
"chunks": {
"main": { // 入口代码块
"js": "main.abc123.js", // 生成的文件名
"contains": [0, 1] // 包含的模块ID
},
"async-chunk": { // 异步代码块
"js": "async.def456.js",
"loaded": false // 标记是否已加载
}
},
"runtime": "runtime.xyz789.js" // Webpack 运行时代码
}
代码分离
代码分离是webpack比较重要的特性,它能够将代码分离到不同的bundle,然后按需加载或并行加载,控制资源加载的优先级,提升页面加载速度,常见的代码分离方式有三种:
- 入口分离:通过entry配置将三方库(如react、antd等)配置成单独的entry,实现vendor分离
- 防止重复:使用入口依赖(entry dependOn)或splitChunksPlugin去重和分离chunk
- 动态导入:将顶部的
import _ from lodash改为在实际需要lodash的地方引入import(lodash).then(({ default: _ }) => {})
Webpack v4.6.0+ 增加了对预获取和预加载的支持:
- 预获取:
import(/* webpackPrefetch: true */ './path/to/LoginModal.js') - 预加载:
import(/* webpackPreload: true */ 'ChartingLibrary')
可以使用webpack官方分析工具或者其他三方工具(webpack-bundle-analyzer、webpack-visualizer等)分析分离后的bundle,进一步优化代码
缓存/性能优化
缓存
- 基于浏览器缓存规则,对output.filename使用contenthash可使文件名根据文件内容变化而变化
- 使用
optimization.runtimeChunk: 'single'选项将 runtime 代码拆分为一个单独的 chunk - 使用 splitChunksPlugin插件的cacheGroup选项来缓存很少修改的三方库
- 通过模块标识符(
optimization.moduleIds: 'deterministic')保证多次构建后三方vendor的文件名hash值不变
性能优化
- 使用最新版本的webpack、node、npm
- 精准使用loader,通过include/exclude排查不需要转换的资源
- 尽可能少使用loader/plugin,它们会额外的消耗性能
- 精确的匹配文件资源,减少资源解析的时间,减少resolve.modules, resolve.extensions, resolve.mainFiles, resolve.descriptionFiles 中条目数量,因为他们会增加文件系统调用的次数:
import './src/demo'改为import './src/demo.js',resolve.extensions: [‘.js’, ‘.jsx’] - 使用Dllplugin为更改不频繁的代码单独编译
- 使用更少/更小的三方库,删除未使用的代码,更合理的分离chunk,减少大体积资源的生成
- 使用thread-loader将耗资源的loader交给worker池
- 使用webpack的cache并配合package.json中的postinstall清除缓存目录实现持久化缓存
- 移除webpack ProgressPlugin,该插件没多大实际用途,且耗费资源
- 分环境使用loader/plugin,不要紧生产环境、开发环境的loader、plugin混用
- 使用webpack压缩工具压缩生产环境代码
- source-map非常消耗资源,生产环境应禁用
- 合理的使用webpack三方工具(babel、typescript、sass等)
- webpack5.x支持模块联邦,可以在微前端架构中提取公共模块
- 合理使用tree shaking:通过webpack的optimization.usedExports: true配合package.json的sideEffects: false将文本标记为无副作用。tree shaking只适用于ES2015以上的import/export语法
- 真正意义上的懒加载:比如button click事件触发时再动态import lodash
兼容性
webpack运行环境和webpack构建出来的项目的运行环境不是一个概念,webpack基于Node.js运行,webpack构建出来的项目一般基于浏览器环境运行
浏览器兼容性
webpack支持所有符合ES5标准的浏览器(不支持IE8及以下版本),如果想兼容旧版本浏览器,需要加载polyfill:
// src/index.js
import 'babel-polyfill'
function Component() {}
配置兼容性
webpack.config.js配置文件是webpack运行过程中本身需要的文件,应避免ES6语法编写代码:
- webpack基于Node.js运行,遵循CommonJS规范
- Node.js v12.x及以上版本才开始支持原生ES6模块(.mjs),v14.x及以上版本才稳定支持ES6
- webpack原生支持ES6模块是指支持项目代码中的import/export语句,但仅限于模块化依赖分析,不包含语法转换。Webpack 会将 import/export 转换为自己的模块系统(如__webpack_require__),实现代码拆分和依赖管理。
零碎知识
- webpack通过mode配置项设置模式,mode取值范围:string = ‘production’: ‘none’ | ‘development’ | ‘production’
- webpack通过target配置项设置构建目标,默认值为 “browserslist”,target: string | [string] | false,多target通过多个独立的配置来实现:module.exports = [serverConfig, clientConfig]
- 开发环境通过source-map跟踪堆栈信息(
devtool: 'inline-source-map'),生产环境不建议使用source-map - webpack 提供了几种可选方式帮助在代码发生变化后自动编译代码:–watch、webpack-dev-server、webpack-dev-middleware,watch模式需要手动刷新浏览器,middleware是server的内部实现核心,middleware+express -> server
- publicPath 配置选项在各种场景中都非常有用。可以通过它来指定应用程序中所有资源的基础路径
- webpack5.x版本新增了资源模块,可以不再需要像之前的版本配置各种loader来转换静态资源(图片、字体等)
- 命令行接口参数的优先级高于配置文件中的参数,例如:
build: webpack --mode="production"会优于webpack.config.js中的mode: development - 可以结合mainfest和stats data分析数据,优化构建性能,mainfest存在于运行时(浏览器环境),stats data存在于构建后(本地json,不影响运行时)