基本配置

1
2
# 安装
npm install webpack webpack-cli --save-dev
1
2
3
# 命令行打包
webpack ./path/to/src/file -o ./path/to/dist/file --mode=development
# mode可选development或production
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// webpack.config.js
module.exports = {
// 1. 输入
entry: "./path/to/index.js", // entry可以使用相对路径
// 2. 输出
output: {
filename: "/yourOutputJsFolder/yourBundleName.js",
path: __dirname + "/dist", // 输出一定要使用绝对路径
},
// 3. loader
module: {
rules: [

]
},
// 4. plugins
plugins: [

],
// 5. mode
mode: "development", // 或production
// 6. devServer
// 需要npm i -D webpack-dev-server
// package.json需要添加script "start": "webpack serve --open",
devServer: {
contentBase: '/path/to/output/folder',
compress: true, // 使用gzip压缩,可选
port: 3000// 端口号,可选
open: true // 自动打开浏览器,也可以在script添加--open
hot: true, //开启HRM
},
devtool: 'source-map'
}

补充:绝对路径拼接

引入nodejs的path.resolve方法,借助__dirname拼接路径

__dirname为当前文件所在目录的绝对路径(e.g. 即webpack.config.js所在路径)

注意使用__dirname 无需声明或引号

path.resolve:ref

  1. 从右往左构建路径,直到遇到绝对路径片段则停止
  2. 若没有遇到绝对路径片段,则前面自动添加当前文件所在目录的绝对路径(即等同于__dirname
  3. 若参数为空,则等同于__dirname

Entry

entry的值有以下三种类型 ref

  1. string:打包后形成一个chunk(默认名为main),输出一个bundle文件(即单入口)

  2. array:多入口,同string只生成一个chunk,输出一个bundle

    (即多入口的js文件,即使没有引用关系,也会打包到同一个文件中。通常用于解决hrm中html热更新问题)

  3. object:多入口,形成多个chunk和输出多个bundle,chunk名称即为object中的key

    (object里面的value可以为string或array,为array时即类似于2,将多个不相关的js打包到同一文件,如下面例子将react相关库打包到同一文件)

    1
    2
    3
    entry: {
    react: ['react', 'react-dom', 'react-router'],
    }

webpack5默认值:entry: './src/index.js'

Output

  1. filename:文件路径(optional)+ 名称

    命名过程可以使用如下特殊变量(用中括号[]表示)

    1. [name]: (仅针对object类型的多入口entry):entry object中的key名
    2. hash相关(包括[hash], [chunkhash], [contenthash], [contenthash:10]等)
  2. path:输出文件目录(后续所有资源都会输出到该目录,需要是绝对路径)

  3. publicPath:(适用于生产环境,为所有资源添加一个公共路径前缀)

  4. chunkFilename:区别于filename,只对非入口chunk生成的bundle命名,同理可以使用1中的特殊变量

    1. 通过动态import的模块会生成单独的chunk/bundle

      (注意:默认[name]为根据id自动生成,也可以按如下方式在import中指定:

      import(/* webpackChunkName: 'myModule */'./path/to/myModule.js').then()

    2. 通过optimization.splitChunks对node_module模块进行分割后的chunk

  5. library相关:library和libraryTarget:将模块/库暴露出来供后续调用

    library: 设置模块对应的变量名(对于多入口可以使用[name]即chunk名)

    libraryTarget:设置挂载到的目标对象,可选'window' | 'global' | 'commonjs' | 'umd'

    (一般结合dll使用)

补充:webpack5默认值

1
2
3
4
output: {
path: path.resolve(__dirname, "dist"),
filename: "[name].js"
}

Resolve

可以给路径添加一个别名前缀(e.g. ~ , $css 等)

后续import时可以使用这个别名,而无需通过相对路径的方式引入文件(e.g 多重../../

1
2
3
4
5
6
7
8
9
resolve: {
alias: {
$css: resolve(__dirname, 'src/css'), // 配置需要使用绝对路径
},
extensions: ['.js', '.json', 'jsx', '.css'], // 配置省略文件路径的后缀名
// 默认js和json,css为后续添加
// 使用时可以直接import("./somePath/style")
// 会依次查找style.js, style.json, style.css
}

Loaders

css相关loader

1
2
# 安装
npm i css-loader style-loader -D
1
2
3
4
5
6
rules: [
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]

补充: outputPath

在loader的options中可设置outputPath: 'yourOutputFolderName',可以将对应的文件输出到对应output文件夹的不同路径(e.g. img, media, data, etc)

处理图片

1
2
3
4
# 安装 (url-loader依赖于file-loader所以需要一起安装,使用只需要url-loader)
npm i -D url-loader file-loader
# 处理html图片的loader (2.0以上版本对路径处理存在问题)
npm i -D html-loader@1.3.2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
rules: [
{
// 该loader仅能处理js或者css内引用的图片,无法对html template内引用的图片进行处理
test: /\.(jpg|png|gif)$/,
loader: 'url-loder',
options: { // 可选配置对象
limit: 8 * 1024, // 优化(optional): 对于小于8kb的图片使用base64处理
name: '[hash:10].[ext]', // 优化(optional): 图片重命名取hash前10位,避免文件名过长
}
},
{ // 可选,如果需要在html内引用图片
//
test: /\.html$/,
loader: 'html-loader',
}
]

补充:base64将图片编码为字符串,可以减少文件请求数量但增大传送数据体积,一般折中选择小于8-12kb的图片转为base64

eslint

以使用airbnb eslint为例

  1. 配置airbnb eslint:

    1
    npm i -D eslint eslint-plugin-import eslint-config-airbnb-base
    1
    2
    3
    4
    5
    6
    7
    // package.json对象添加如下属性:
    "eslintConfig": {
    "extends": "airbnb-base",
    "env": {
    "browser": true
    }
    }
  2. 配置eslint loader

    1
    npm i -D eslint-loader eslint # eslint-loader依赖eslint,如果前面安装了eslint则此处无需安装
    1
    2
    3
    4
    5
    6
    7
    8
    { // 添加loader
    test: /\.js$/,
    loader: 'eslint-loader',
    exclude: /node_modules/, // 排除node_modules文件夹
    options: {
    fix: true, // 自动修复
    },
    },

babel

1
npm i -D babel-loader @babel/core
  1. 基本js兼容性处理:使用@babel/preset-env配置文件

    1
    npm i -D @babel/preset-env
    1
    2
    3
    4
    5
    6
    7
    8
    { // 注意:需要放在eslint等loader的下面,或者设置enfore: "pre"优先执行
    test: /\.js$/,
    exclude: /node_modules/,
    loader: 'babel-loader',
    options: {
    presets: ['@babel/preset-env'], // 预设:指示babel做怎样的兼容性处理
    }
    }

    存在问题:只能转换基本语法,promise等高级语法无法转换

  2. 全部兼容性处理:使用@babel/polyfill

    1
    npm i -D @babel/polyfill
    1
    2
    // 在入口文件index.js中引入
    import '@babel/polyfill';
  3. 按需加载兼容性处理:使用core-js

    1
    npm i -D core-js
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    {
    test: /\.js$/,
    exclude: /node_modules/,
    loader: 'babel-loader',
    options: {
    cacheDirectory: true, // optional: 开启babel缓存,用于性能优化
    presets: [
    ['@babel/preset-env',
    { // 按需加载
    useBuiltIns: 'usage',
    corejs: {
    version: 3, //指定core-js版本
    },
    targets: { // 兼容目标浏览器版本
    chrome: '60',
    firefox: '60',
    ie: '9',
    safari: '10',
    edge: '17'
    }
    }]
    ],
    }
    },

Plugins

html-webpack-plugin

复制一个模板html文件(或默认创建一个空html文件),并引入打包好的资源

1
2
# 安装
npm i -D html-webpack-plugin
1
2
3
4
5
6
7
8
9
10
11
const HtmlWebpackPlugin = require('html-webpack-plugin')

plugins: [
new HtmlWebpackPlugin({
template: "./path/to/src/template/file.html",
minify: { // 可选html压缩(新版webpack可省略)
collapseWhitespace: true,
removeComments: true
}
}); //可以不传入对象,则默认生成空html文件并引入webpack打包后的资源
]

mini-css-extract-plugin

将css提取出成单独文件(而不是集合到js中再添加到style标签)以优化性能

1
2
# 安装
npm i -D mini-css-extract-plugin
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
const MiniCssExtractPlugin = require("mini-css-extract-plugin")

module.export = {
// ... 省略其他配置
module: {
rules: [
{
test: /\.css$/,
// 使用插件上附带的loader,且不使用style-loader
use: [
{
loader: MiniCssExtractPlugin.loader.
options: {
publicPath: "/"; // 设置publicPath以解决css内url图片引用问题
}
},
’css-loader',
],
}
]
},
plugins: [
// ... other plugins (e.g. HtmlWebpackPlugin)
new MiniCssExtractPlugin({ // 除了使用loader外仍需在此加载插件
filename: 'css/yourFileName.css', // 可选配置:设置输出css文件名和路径
});
]
}

optimize-css-assets-webpack-plugin

压缩css

1
npm i -D optimize-css-assets-webpack-plugin
1
2
3
4
5
const OptimizeCssAssetsWebpackPlugin = require("optimize-css-assets-webpack-plugin");
// ... 其他module配置
plugins: [
new OptimizeCssAssetsWebpackPlugin(),
]

devServer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
devServer: { 
contentBase: '/path/to/output/folder',
watchContentBase: true, // 监视contentBase目录变化,并自动在浏览器重reload
watchOptions: { // 忽略监视某些文件
ignore: /node_modules/
},
compress: true, // 使用gzip压缩,可选
port: 3000// 端口号,可选
host: 'localhost', // 域名,可选
open: true // 自动打开浏览器,也可以在script添加--open
hot: true, //开启HRM
clientLogLevel: 'none' // 关闭服务器日志,可选
quiet: true, // 仅显示基本启动信息,可选
overlay: false, // 不全屏提示出错信息,可选
proxy: { // 代理,解决跨域问题
'/api': {
target: 'your_target_server',
pathRewrite: { // 路径重写,请求时去掉api前缀
'^/api': ''
}
}
}
},

Optimization

参考基本配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const TerserWebpackPlugin = require("terser-webpack-plugin");
module.exports = {
optimization: {
splitChunks: {
chunks: 'all'
},
runtimeChunk: {
// 假设index.js引入外部库,webpack编译后会通过一个hash值在bundle.js中查找该外部库
// 外部库发生变化,其对应hash也发生变化,导致bundle.js内部引用的hash发生变化,以至于bundle.js的缓存失效
// 加入以下配置,将单独记录其他模块的hash值到一个runtime文件,这样引用的外部库发生变化后不影响bundle.js的hash值
name: entrypoint => `runtime-${entrypoint.name}`
},
minimizer: { // 针对生产环境的压缩方案,需要安装和引入terser-webpack-plugin
new TerserWebpackPlugin({ // 以下配置可选
cache: true, // 开启缓存
parallel: true, // 多进程打包
sourceMap: true // 不压缩souceMap
})
}
}
}

性能优化总结

开发环境优化:

HRM

hot module replacement 热替换: 在devServer中,当一个模块变化是,只重新打包该模块(提升构建速度)

  1. devServer里面配置hot: true
  2. 对于style文件,style-loader默认实现了HRM
  3. 对于html文件:需要在webpack.config.js添加entry: ['./src/js/index.js', './src/index.html'],否则热更新会失效

SourceMap

添加从源码到编译后代码的映射,方便从console中找出源码出错位置

需要在webpack.config中添加devtool: ‘source-map’

缓存

babel缓存:配置loader的option,添加cacheDirectory: true

文件资源缓存:配置output.filename: ‘path/to/bundle.[hash:10].js’,其中hash值替换为以下选项(即contenthash)

  1. hash: webpack构建时生成的文件hash值,每次构建俊辉变化
  2. chunkhash:根据不同入口文件生成的打包文件具有不同hash值
  3. contenthash:根据文件内容生成的hash值,文件发生变化则hash值发生变化

设置为contenthash后,不变化的文件生成的打包文件,其文件名中的hash值不变,因此可以有效利用web缓存

生产环境优化

Tree Shaking

去掉无用代码(开启production mode自动启用)

Code Split

  1. 使用多入口进行代码分割,适用于多页面应用(较少使用)
1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
entry: {
// 会分开输出打包文件
index: './path/to/index.js',
somePage: './path/to/somePage.js',
},
output: {
// 多入口可以使用[name]取得在entry中的命名
filename: 'js/[name].[contenthash:10].js',
path: __dirname + "/build",
}
}
  1. 使用split chunks分割打包node_modules:
    1. 对于单入口打包,使用splitChunks会将所有引用的node_modules打包到一个vendor文件
    2. 对于多入口打包,如果多个入口使用共同的模块,会单独抽出来进行打包
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
module.exports = {
optimization: {
splitChunks: {
chunks: 'all'
},
runtimeChunk: {
// 假设index.js引入外部库,webpack编译后会通过一个hash值在bundle.js中查找该外部库
// 外部库发生变化,其对应hash也发生变化,导致bundle.js内部引用的hash发生变化,以至于bundle.js的缓存失效
// 加入以下配置,将单独记录其他模块的hash值到一个runtime文件,这样引用的外部库发生变化后不影响bundle.js的hash值
name: entrypoint => `runtime-${entrypoint.name}`
},
minimizer: { // 针对生产环境的压缩方案,需要安装和引入terser-webpack-plugin
new TerserWebpackPlugin({
cache: true, // 开启缓存
parallel: true, // 多进程打包
sourceMap: true // 不压缩souceMap
})
}
}
}

其他optimization配置参考 ref

  1. 对于引用本地自行创建的模块,可以通过动态加载的方式将引入的模块单独打包

(即在使用时候动态import()模块,该import返回一个promise。注意该功能基于ES6,需要使用babel转换)

1
2
3
4
5
// 使用特殊的注释语法给单独打包的模块文件重命名
import(/* webpackChunkName: 'test', webpackPrefetch: true */'./test')
.then((yourModuleExport) => {
// ...
})

webpackPrefetch为true时即预加载,会在触发异步事件前先将模块加载到本地,默认false(即懒加载)则会等到触发异步事件后才加载模块

预加载会等其他资源加载完毕,等浏览器空闲了才开始加载

PWA

使用workbox (workbox-webpack-plugin 插件) 实现离线加载访问 (在生产环境下)

1
2
# 安装
npm i -D workbox-webpack-plugin
1
2
3
4
5
6
7
8
9
10
11
// webpack.config.js 注册插件
const WorkboxWebpackPlugin = require('workbox-webpack-plugin');

module.exports = {
plugins:[
new WorkboxWebpackPlugin.GenerateSW({ // 该插件会在输出目录下生成一个serviceworker配置文件
clientsClaim: true,
skipWaiting: true
})
]
}
1
2
3
4
5
6
7
8
9
10
// index.js 入口文件注册插件
if ('serviceWorker' in navigator) { // 注意:eslint需要配置为browser才能使用navigator和window等对象
window.addEventListener('load', () => {
navigator.serviceWorker
.register('/service-worker.js') // workbox插件自动生成的配置文件
.then(() => {
console.log("register success!");
});
})
}

thread-loader

实现多进程打包,加快打包速度

(注意:因为进程启动需要一定时间(大约600ms),因此仅大型项目打包才能体现优化效果)

1
npm i -D thread-loader
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.js$/,
use: [
'thread-loader', // 可以加载到任意loader处
// ... 其他loader
]
}
]
}
}

externals

对于一些外部引入的全局js库 (e.g. jquery通过cdn引入),可以使用externals配置,webpack不对其进行打包

1
2
3
4
5
6
7
// webpack.config.js
module.exports = {
externals: {
// key为npm包名,value为window全局变量中的jQuery变量
jquery: 'jQuery'
}
}

dll

ref

和externals的区别:同样可以减少外部库的打包,dll提供了外部引用的库只需打包一次(创建一个单独的打包文件),在本地进行serve;而externals直接忽略外部的库,需要通过cdn进行引入

总结

开发环境优化:

  1. 打包速度:HMR
  2. 代码调试:source-map

生产环境优化

  1. 打包速度:oneOf,babel缓存,多进程打包(thread-loader),externals,ddl
  2. 运行性能(重点):缓存(hash/chunkhash/contenthash)/ tree-shaking / code split / 动态加载(懒加载和预加载) / pwa