Guide to Webpack 5
webpack 使用指南。pnpm 安装的包没有类型提示,使用三斜线指令可以解决。
webpack-dev-server
clean-webpack-plugin
From webpack v5, you can remove the clean-webpack-plugin plugin and use the output.clean option in your webpack config:
1 output: {
2 filename: 'utils.min.js',
3 clean: true,
4 }
pnpm 没有 webpack 的类型声明
随便搞个 d.ts 然后加个三斜线指令
,就会有提示了。
1/// <reference path="/node_modules/webpack/types.d.ts"/>
Template strings
https://webpack.js.org/configuration/output/#template-strings
在 Compilation-level 可用的替换项:
[fullhash]
:此次编译过程中,所有模块内容生成的哈希值的总和。如果项目中的任何部分发生改变,这个值就会改变。[hash]
:与[fullhash]
相同,但已被弃用。
在 Chunk-level 可用的替换项:
[id]
:块(chunk)的唯一标识,通常是一个数字或字符串。[name]
:块的名称。在你的 webpack 配置中,你可以为每个入口起一个名字,如果设置了,[name]
就是这个名字。如果没有设置,那就是块的id
。[chunkhash]
:基于 chunk 内容的哈希值,如果块中包含的模块有任何更改,该值将更改。[contenthash]
:对于特定类型的块内容(例如,只包含 CSS 的块)计算的哈希值。只有当这个类型的内容改变时,这个值才会改变。
在 Module-level 可用的替换项:
[id]
:模块的唯一标识,通常是一个数字或字符串。[moduleid]
:与[id]
相同,但已被弃用。[hash]
:基于模块内容的哈希值。[modulehash]
:与[hash]
相同,但已被弃用。[contenthash]
:模块内容的哈希值。
在 File-level 可用的替换项:
[file]
:文件名和路径,不包括查询参数或片段标识符。[query]
:查询参数,以 '?' 开头。[fragment]
:片段标识符,以 '#' 开头。[base]
:只包括文件名和扩展名,不包括路径。[filebase]
:与[base]
相同,但已被弃用。[path]
:文件的路径,不包括文件名。[name]
:只有文件名,不包括扩展名和路径。[ext]
:文件扩展名,以 '.' 开头(在output.filename
中不可用)。
在 URL-level 可用的替换项:
[url]
:URL,包括协议、主机、端口、路径、查询参数、片段标识符。
以上这些替换项使我们能够根据不同的需求灵活定义文件名,如考虑缓存策略,做到只有当文件内容改变时,浏览器才会下载新的文件。
特殊的注释语法
在 Webpack 中,webpackChunkName
是一个特殊的注释语法,用于控制代码分割(code splitting)生成的 chunk 文件的名称。这个名字通常用于动态 import()
语句。例如:
1import(/* webpackChunkName: "my-chunk-name" */ './my-module');
在这个例子中,my-chunk-name
就是 chunk 文件的名称。Webpack 在生成 chunk 文件时会使用这个名称,生成的文件名将类似于 my-chunk-name.bundle.js
。
然而,webpackChunkName
不支持使用 hash。这是因为,webpackChunkName
的值在编译时需要是已知的静态字符串,而 hash 是在编译过程中生成的,依赖于模块的内容。因此,不能在 webpackChunkName
中使用 hash。
你可以使用 output.chunkFilename
配置项来控制 chunk 文件名的格式,包括 hash。例如:
1module.exports = {
2 //...
3 output: {
4 chunkFilename: '[name].[contenthash].js',
5 },
6};
在这个配置中,[name]
会被替换为 chunk 的名称(也就是 webpackChunkName
的值),[contenthash]
会被替换为 chunk 内容的 hash。所以,虽然不能直接在 webpackChunkName
中使用 hash,但你可以通过 output.chunkFilename
来控制 hash 在文件名中的使用。
在 webpack 中,通过魔法注释(magic comment)/* webpackChunkName: "name" */
,你可以为动态导入的代码块(chunk)设置一个特定的名字。这通常用于按需加载或代码分割。
如果你为多个代码块设置了相同的 webpackChunkName
,这些代码块会被打包到同一个文件中。这可以用作特性,比如当你想要多个异步代码块组合在一起时。
举个例子:
1// 如果你这样做:
2import(/* webpackChunkName: "group-a" */ './moduleA');
3import(/* webpackChunkName: "group-a" */ './moduleB');
4
5// 两者会被打包到同一个 chunk 文件中,如:group-a.js
6
7// 而如果这样做:
8import(/* webpackChunkName: "moduleA" */ './moduleA');
9import(/* webpackChunkName: "moduleB" */ './moduleB');
10
11// 则会生成两个不同的 chunk 文件,例如:moduleA.js 和 moduleB.js
所以,如果 webpackChunkName
重复,并不会导致错误,而是 webpack 故意这么设计的,以便你可以将多个模块组合到一个代码块中。
Magic Comments
Webpack 提供了一种称为"魔法注释"(Magic Comments)的机制,允许在源代码中插入特殊的注释,从而影响 webpack 的构建行为。下面列举了几种常用的魔法注释:
webpackChunkName
: 此注释用于指定分割出的块(chunk)的名称。例如:
1import(/* webpackChunkName: "my-chunk-name" */ './module');
以上代码会导致生成一个名为 "my-chunk-name" 的块,包含导入的模块。
webpackMode
: 此注释允许你修改模块的加载模式。可用的模式有lazy
(默认值,只有在模块需要时才请求)、lazy-once
(所有请求的模块都在一个网络请求中)、eager
(无需网络请求,但需要 promise 解析)和weak
(试图使用同步导入,如果不可用则拒绝)。例如:
1import(/* webpackMode: "eager" */ './module');
webpackPrefetch
/webpackPreload
: 这两个注释允许你控制浏览器的预获取/预加载行为。webpackPrefetch
会在浏览器闲置时加载资源,webpackPreload
则会和主代码并行加载。例如:
1import(/* webpackPrefetch: true */ 'LoginModal');
以上代码会在浏览器空闲时提前获取 "LoginModal" 模块。
webpackInclude
/webpackExclude
: 这两个注释可以限制 webpack 处理require.context()
时的模块路径。例如:
1function importAll(r) {
2 r.keys().forEach(r);
3}
4
5importAll(require.context('../components/', true, /* webpackInclude: /\.js$/ */));
以上代码将仅包含 ../components/
目录下所有 .js
文件。
webpackExports
: 这个注释可以用来在导入动态模块时,只导入某些输出,从而减少代码大小。例如:
1import(/* webpackExports: ["export1", "export2"] */ './module');
这些魔法注释都提供了对 webpack 构建行为的更细粒度的控制,可以在需要的时候选择使用。
组合使用
Webpack 的魔法注释可以组合使用。这将给你更大的灵活性来调整模块的行为。例如,你可能想要对一个被动态导入的模块进行预取(prefetching),并同时为这个模块指定一个特定的 chunk 名称。这可以通过在同一个注释中使用两个魔法注释来完成,如下所示:
1import(
2 /* webpackChunkName: "my-chunk-name", webpackPrefetch: true */
3 './module'
4);
在这个例子中,webpackChunkName
注释将为 chunk 指定名称 "my-chunk-name",webpackPrefetch
注释则会使得这个模块在浏览器空闲时被预取。
但需要注意的是,并非所有魔法注释都可以共同使用,例如 webpackInclude
和 webpackExclude
就不能同时使用。所以在使用时,需要结合实际需求和 webpack 文档来判断使用哪种组合。
filename 和 chunkFilename
在 Webpack 的配置中,filename
和 chunkFilename
是两个用于定义输出文件名称模板的重要选项。
filename
:这个选项用于指定入口文件的名称。在单入口场景中,filename
可以是一个静态的字符串,例如'bundle.js'
。在多入口场景中,filename
可以包含一些占位符(placeholders)来确保文件名称的唯一性,例如'js/[name].[contenthash].js'
。chunkFilename
:这个选项用于指定非入口的 chunk 文件的名称,比如通过动态导入(import()
)或者由于设置了optimization.splitChunks
而产生的新的 chunk。同样,chunkFilename
也可以包含占位符。
两者的主要区别在于,filename
用于主入口(entry point)的文件,而 chunkFilename
用于额外的、按需加载的代码块。
例如,假设我们的配置如下:
1output: {
2 filename: 'bundle.js',
3 chunkFilename: '[id].js'
4}
如果我们的项目有一个入口文件 index.js
,并且在这个文件中动态导入了 anotherModule.js
,那么在构建时,webpack 将会产生两个文件:bundle.js
(由 index.js
生成)和 0.js
(由 anotherModule.js
生成,假设它的 chunk id 是 0)。
Loader and Plugin
-
Loader
假设我们要编写一个 loader,它的功能是将 JS 文件中的所有
console.log
语句删除。这就需要用到abstract syntax tree (AST)
进行代码分析。1// remove-console-loader.js 2const { getOptions } = require('loader-utils'); 3const { parse, print, visit } = require('recast'); 4 5module.exports = function (source) { 6 const options = getOptions(this); 7 const { removeMethods = ['console.log'] } = options; 8 9 const ast = parse(source); 10 visit(ast, { 11 visitCallExpression(path) { 12 const node = path.node; 13 if ( 14 node.callee.type === 'MemberExpression' && 15 removeMethods.includes(print(node).code) 16 ) { 17 path.replace(); 18 } 19 return false; 20 }, 21 }); 22 23 return print(ast).code; 24};
在 webpack 的配置中使用这个 loader:
1// webpack.config.js 2module.exports = { 3 module: { 4 rules: [ 5 { 6 test: /\.js$/, 7 use: [ 8 { 9 loader: path.resolve('remove-console-loader.js'), 10 }, 11 ], 12 }, 13 ], 14 }, 15};
-
Plugin
假设我们要编写一个 plugin,其功能是将每次构建后的输出文件大小打印出来。
1// print-size-plugin.js 2const { RawSource } = require('webpack-sources'); 3const fs = require('fs'); 4const path = require('path'); 5 6class PrintSizePlugin { 7 apply(compiler) { 8 compiler.hooks.emit.tapAsync('PrintSizePlugin', (compilation, callback) => { 9 for (let filename in compilation.assets) { 10 let source = compilation.assets[filename].source(); 11 console.log(`The size of ${filename} is ${source.length} bytes.`); 12 } 13 callback(); 14 }); 15 } 16} 17 18module.exports = PrintSizePlugin;
在 webpack 配置中使用这个 plugin:
1// webpack.config.js 2const PrintSizePlugin = require('./print-size-plugin'); 3 4module.exports = { 5 plugins: [ 6 new PrintSizePlugin(), 7 ], 8};
这两个例子展示了如何通过编写 loader 和 plugin 来实现更复杂的功能。这些例子虽然更复杂,但仍然只是 webpack 提供的可扩展性的冰山一角。根据你的需求,你可以编写更复杂的 loader 和 plugin 来定制你的构建过程。
执行顺序
在 Webpack 中,loader 和 plugin 的应用顺序是有区别的。它们的执行顺序取决于它们在配置中的位置和 webpack 的执行流程。
Loader 的应用顺序:
- 对于一个模块(通常是一个文件),Webpack 在解析模块时,根据模块的文件类型(匹配
test
条件)选择相应的 loader 进行转换。 - 如果使用了多个 loader,Webpack 会根据
use
数组中的顺序,依次对模块进行转换。每个 loader 都会接收前一个 loader 处理后的结果。 - 最后一个 loader 将返回转换后的 JavaScript 代码,供 Webpack 继续处理。
例如:
1module: {
2 rules: [
3 {
4 test: /\.css$/,
5 use: ['style-loader', 'css-loader'],
6 },
7 ],
8},
在上面的配置中,首先 css-loader
将解析 CSS 文件,然后将结果传递给 style-loader
,style-loader
会将解析后的 CSS 样式添加到页面上。
Plugin 的应用顺序:
- Plugin 的执行顺序是在 loader 执行之后。
- 当所有模块的 loader 转换完成后,Webpack 开始执行插件。
- 插件通过 Webpack 的事件钩子机制来注入自定义行为,例如在文件输出、优化代码等阶段执行特定的任务。
例如:
1plugins: [
2 new HtmlWebpackPlugin(),
3 new CleanWebpackPlugin(),
4],
在上面的配置中,首先在所有模块转换完成后,Webpack 会执行 HtmlWebpackPlugin
和 CleanWebpackPlugin
的插件功能。
总结:Loader 主要用于模块文件的转换,根据文件类型按顺序执行,而 Plugin 则主要用于在整个构建过程的不同阶段执行特定的任务。因此,Loader 的执行顺序在每个模块的加载和转换过程中,而 Plugin 的执行顺序在整个构建过程的不同阶段。
在浏览器环境下使用Webpack或Next.js,如何处理依赖于Node.js的fs
模块的库?
在构建面向浏览器的Web项目时,开发者可能会遇到依赖于Node.js的fs
模块的库。由于浏览器不支持fs
模块,因此可能出现兼容性问题。以下是针对Webpack和Next.js的解决方案:
对于Webpack的解决方案:
-
设置
node
选项: 通过Webpack的配置文件,将fs
设置为"empty"
或者false
,从而忽略或替换fs
模块的引用。具体代码示例:
1// webpack.config.js 2module.exports = { 3 // 其他配置项 4 node: { 5 fs: 'empty' 6 } 7};
-
使用别名: 你可以使用别名将
fs
映射到一个自定义的文件。具体代码示例:
1// webpack.config.js 2module.exports = { 3 // 其他配置项 4 resolve: { 5 alias: { 6 fs: path.resolve(__dirname, 'path/to/customFs.js') 7 } 8 } 9};
对于Next.js的解决方案:
-
自定义Webpack配置: 在Next.js的
next.config.js
文件中,你可以自定义Webpack配置来处理fs
模块的引用。具体代码示例:
1// next.config.js 2module.exports = { 3 webpack: (config, { isServer }) => { 4 if (!isServer) { 5 config.resolve.fallback.fs = false; // 在客户端构建中替换fs 6 } 7 8 return config; 9 }, 10};
通过这些方案,开发者可以解决在Webpack或Next.js项目中引入了fs
模块的库的兼容性问题。这有助于确保项目的正确构建和运行,无论是在纯Webpack环境还是在Next.js框架中。