Webpack
Workflow

Configuration
Enable webpack configuration types intellisense:
npm i -D webpack webpack-cli webpack-dev-server
Enable devServer type intellisense:
# Add `devServer` type to `webpack.Configuration`
npm i -D @types/webpack-dev-server
/** @type {import('webpack').Configuration} */
module.exports = {
entry: {
main: './src/index.ts',
},
output: {
filename: devMode ? '[name].js' : '[name].[contenthash].js',
path: path.resolve(__dirname, 'build'),
},
mode: devMode ? 'development' : 'production',
devServer: {
hot: true,
open: true,
port: 2333,
},
}
Hot Module Replacement
HMR:
- 使用 WDS 托管静态资源, 同时以 Runtime 方式注入 HMR 客户端代码 (HMR Runtime).
- 浏览器加载页面后, 与 WDS 建立 WebSocket 连接.
- Webpack 监听到文件变化后, 增量构建发生变更的模块, 并通过 WebSocket 发送 hash 事件.
- 浏览器接收到 hash 事件后, 请求 manifest (
[hash].hot-update.json) 资源文件, 确认增量变更范围. - 浏览器加载发生变更的增量模块.
- 浏览器中注入的 HMR Runtime 触发变更模块的
module.hot.accept回调, 执行代码变更逻辑.

module.hot.accept 有两种调用模式:
- 无参调用模式
module.hot.accept(): 当前文件修改后, 重头执行当前文件代码. - 回调调用模式
module.hot.accept(path, callback): 常用模式, 监听模块变更, 执行代码变更逻辑.
// 该模块修改后, `console.log('bar')` 会重新执行
console.log('bar')
module.hot.accept()
import component from './component'
let demoComponent = component()
document.body.appendChild(demoComponent)
if (module.hot) {
module.hot.accept('./component', () => {
const nextComponent = component()
document.body.replaceChild(nextComponent, demoComponent)
demoComponent = nextComponent
})
}
react-refresh-webpack-plugin/vue-loader/style-loader
利用 module.hot.accept 实现了 HMR (forceUpdate),
无需开发者编写热模块更新逻辑.
Watch
echo fs.notify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf
sudo sysctl -p
Resolve Path
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin')
module.exports = {
resolve: {
alias: {
'#': path.resolve(__dirname, '/'),
'~': path.resolve(__dirname, 'src'),
'@': path.resolve(__dirname, 'src'),
'~@': path.resolve(__dirname, 'src'),
'vendor': path.resolve(__dirname, 'src/vendor'),
'~component': path.resolve(__dirname, 'src/components'),
'~config': path.resolve(__dirname, 'config'),
},
extensions: ['.tsx', '.ts', '.jsx', '.js'],
plugins: [new TsconfigPathsPlugin({ configFile: './tsconfig.json' })],
},
}
get baseUrland paths from tsconfig.json:
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin')
module.exports = {
resolve: {
plugins: [new TsconfigPathsPlugin({ configFile: './tsconfig.json' })],
},
}
jsconfig.json for vscode resolve path:
{
"compilerOptions": {
// This must be specified if "paths" is set
"baseUrl": ".",
// Relative to "baseUrl"
"paths": {
"*": ["*", "src/*"]
}
}
}
{
"compilerOptions": {
"target": "es2017",
"allowSyntheticDefaultImports": false,
"baseUrl": "./",
"paths": {
"Config/*": ["src/config/*"],
"Components/*": ["src/components/*"],
"Ducks/*": ["src/ducks/*"],
"Shared/*": ["src/shared/*"],
"App/*": ["src/*"]
}
},
"exclude": ["node_modules", "dist"]
}
Flags
- --progress
- --colors
- -p
Source Map
| Devtool | Build | Rebuild | Production | Quality |
|---|---|---|---|---|
| (none) / false | fastest | fastest | yes | bundle |
| eval | fast | fastest | no | generated |
| eval-cheap-source-map | ok | fast | no | transformed |
| eval-cheap-module-source-map | slow | fast | no | lines only |
| eval-source-map | slowest | ok | no | lines + rows |
Output
const path = require('node:path')
module.exports = {
entry: {
'bod-cli.min': path.join(__dirname, './src/index.js'),
'bod-cli': path.join(__dirname, './src/index.js'),
},
output: {
path: path.join(__dirname, './dist'),
filename: '[name].[contenthash].js',
library: 'bod',
libraryExport: 'default',
libraryTarget: 'esm',
globalObject: 'this',
},
}
Babel
const config = {
test: /\.(js|mjs|jsx|ts|tsx)$/,
include: path.resolve('src'),
use: [
'thread-loader',
{
loader: require.resolve('babel-loader'),
},
],
options: {
customize: require.resolve('babel-preset-react-app/webpack-overrides'),
plugins: [
[
require.resolve('babel-plugin-named-asset-import'),
{
loaderMap: {
svg: {
ReactComponent: '@svgr/webpack?-svgo,+titleProp,+ref![path]',
},
},
},
],
['lodash'],
],
cacheDirectory: true,
cacheCompression: false,
compact: isEnvProduction,
},
}
CSS
style-loader将 CSS 动态注入到 DOM 中 (document.createElement('style')), 导致 DOM 重新渲染.production下需利用Webpack将 CSS 提前打包 (mini-css-extract-plugin):- 优先加载 critical CSS in
<head>. - Lazy loading non-critical CSS.
- Split up non-initial page CSS.
- 优先加载 critical CSS in
Next.js不允许:global(.global-class):modules.mode设置为pure.
const CssMinimizerPlugin = require('css-minimizer-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const devMode = process.env.NODE_ENV !== 'production'
module.exports = {
module: {
rules: [
{
test: /.s?css$/,
exclude: /node_modules$/,
use: [
devMode ? 'style-loader' : MiniCssExtractPlugin.loader,
{
loader: 'css-loader',
options: {
modules: {
compileType: 'module',
localIdentName: '[local]__[hash:base64:5]',
},
},
},
'sass-loader',
{
loader: 'postcss-loader',
options: {
postcssOptions: {
plugins: [['autoprefixer']],
},
},
},
],
},
],
},
optimization: {
minimizer: [
// `...`,
new CssMinimizerPlugin(),
],
},
}
Static Assets
- ImageMin Loader
asset/resourceemits separate file and exports the URL (file-loader).asset/inlineexports data URI of the asset (url-loader).asset/sourceexports source code of the asset (raw-loader).assetautomatically chooses between exporting data URI and separate file (url-loaderwith asset size limit, default8kb).
const config = {
rules: [
{
test: /\.(png|jpg|gif|jpeg|webp|svg|eot|ttf|woff|woff2)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 4 * 1024, // 4kb
},
},
},
],
}
Resource
const path = require('node:path')
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
assetModuleFilename: 'images/[hash][ext][query]',
},
module: {
rules: [
{
test: /\.png/,
type: 'asset/resource',
},
{
test: /\.html/,
type: 'asset/resource',
generator: {
filename: 'static/[hash][ext][query]',
},
},
],
},
}
import mainImage from './images/main.png'
img.src = mainImage // '/dist/151cfcfa1bd74779aadb.png'
Inline
const path = require('node:path')
const svgToMiniDataURI = require('mini-svg-data-uri')
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.svg/,
type: 'asset/inline',
generator: {
dataUrl: (content) => {
content = content.toString()
return svgToMiniDataURI(content)
},
},
},
],
},
}
import metroMap from './images/metro.svg'
block.style.background = `url(${metroMap})`
// => url(data:image/svg+xml;base64,PHN2ZyB4bW0iaHR0cDo...vc3ZnPgo=)
Source
const path = require('node:path')
module.exports = {
entry: './src/index.js',
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist'),
},
module: {
rules: [
{
test: /\.txt/,
type: 'asset/source',
},
],
},
}
import exampleText from './example.txt'
block.textContent = exampleText // 'Hello world'
Thread
const config = {
rules: [
{
loader: 'thread-loader',
// loaders with equal options will share worker pools
options: {
// the number of spawned workers, defaults to (number of cpus - 1) or
// fallback to 1 when require('os').cpus() is undefined
workers: 2,
// number of jobs a worker processes in parallel
// defaults to 20
workerParallelJobs: 50,
// additional node.js arguments
workerNodeArgs: ['--max-old-space-size=1024'],
// Allow to respawn a dead worker pool
// respawning slows down the entire compilation
// and should be set to false for development
poolRespawn: false,
// timeout for killing the worker processes when idle
// defaults to 500 (ms)
// can be set to Infinity for watching builds to keep workers alive
poolTimeout: 2000,
// number of jobs the poll distributes to the workers
// defaults to 200
// decrease of less efficient but more fair distribution
poolParallelJobs: 50,
// name of the pool
// can be used to create different pools with elseWise identical options
name: 'my-pool',
},
},
// your expensive loader (e.g. babel-loader)
],
}
const threadLoader = require('thread-loader')
threadLoader.warmup(
{
// pool options, like passed to loader options
// must match loader options to boot the correct pool
},
[
// modules to load
// can be any module, i. e.
'babel-loader',
'babel-preset-es2015',
'sass-loader',
]
)
Web Worker
npm i -D worker-loader
module.exports = {
module: {
rules: [
{
test: /\.worker\.js$/,
use: { loader: 'worker-loader' },
},
],
},
}
Plugins
module.exports = {
plugins: [
function () {
this.hooks.done.tap('done', (stats) => {
if (
stats.compilation.errors
&& stats.compilation.errors.length
&& !process.argv.includes('--watch')
) {
// Process build errors.
process.exit(1)
}
})
},
],
}
const childProcess = require('node:child_process')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const branch = childProcess
.execSync('git rev-parse --abbrev-ref HEAD')
.toString()
.replace(/\s+/, '')
const version = branch.split('/')[1]
const scripts = [
'https://cdn.bootcss.com/react-dom/16.9.0-rc.0/umd/react-dom.production.min.js',
'https://cdn.bootcss.com/react/16.9.0/umd/react.production.min.js',
]
class HotLoad {
apply(compiler) {
compiler.hooks.beforeRun.tap('UpdateVersion', (compilation) => {
compilation.options.output.publicPath = `./${version}/`
})
compiler.hooks.compilation.tap('HotLoadPlugin', (compilation) => {
HtmlWebpackPlugin.getHooks(compilation).alterAssetTags.tapAsync(
'HotLoadPlugin',
(data, cb) => {
scripts.forEach(src => [
data.assetTags.scripts.unshift({
tagName: 'script',
voidTag: false,
attributes: { src },
}),
])
cb(null, data)
}
)
})
}
}
module.exports = HotLoad
Code
- HTML Plugin
- UglifyJS Terser Plugin
- JavaScript Obfuscator
- Circular Dependency Plugin
- TypeScript React Components Properties Parser
- Mini CSS Extract Plugin
- CSS Minimizer Plugin
- CSS Tree Shaking
Image
Build
GUI
Migration
Make sure there's no webpack deprecation warnings:
node --trace-deprecation node_modules/webpack/bin/webpack.js