webpack:webpack 使用笔记

webpack 的使用总结

目录

为什么要用构建工具

  • 转换 ES6 的语法
  • 转换 JSX
  • css 预处理器、前缀补全
  • 代码压缩混淆
  • 图片、字体等资源的处理

构建工具的演变

由于 requirejs/seajs 等模块化的概念不断催生,前端模块化编写方式越来越复杂

主要使用的构建工具

grunt => gulp => webpack / rollup

  • grunt 本质上是一个 task runder,将构建过程分为一个个任务,解析 html/css/js 等。但会将每个任务的结果存到本地磁盘,导致打包速度比较慢,磁盘 IO 的操作。

  • gulp 也是任务流,但是每一步构建的结果不会存放到本地磁盘,而是放在内存中,速度比较快。

  • webpack

  • rollup 适合纯 js 的比较小的库,速度快,配置方便。

主要配置项

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
module.exports = {
entry: './src/index.js', // 入口
output: './dist/main.js', // 输出
mode: 'production', // 环境
// loader
module: {
rules: [{ test: /\.txt$/, use: 'raw-loader' }],
},
// 插件
plugins: [
new HtmlwebpackPlugin({
template: './src/index.html',
}),
],
}
  • entry

    打包文件入口。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 单入口
    module.exports = {
    entry: './src/search.js',
    }
    // 多入口
    module.exports = {
    entry: {
    index: './src/index.js',
    search: './src/search.js',
    },
    }
  • output

    打包文件的输出。

    1
    2
    3
    4
    5
    6
    module.exports = {
    output: {
    path: path.join(__dirname, 'dist'), // 输出目录
    filename: '[name].js', // 输出文件名称(entry 中指定的key名称)
    },
    }
  • loader

    webpack 开箱即用只支持 JS 和 JSON 两种文件类型,通过 Loader 去支持其它文件类型并且把它们转化成有效的模块,并且可以添加到依赖图中。

    1
    2
    3
    4
    5
    6
    7
    8
    module: {
    rules: [
    {
    test: /.js$/,
    use: 'babel-loader',
    },
    ]
    }
  • 常用的 loader
    在这里插入图片描述

  • plugin

    插件⽤于 bundle ⽂文件的优化,资源管理理和环境变量量注⼊入,作⽤于整个构建过程。

    • 常用的插件
      在这里插入图片描述
  • mode

    mode ⽤用来指定当前的构建环境是:production、development 还是 none。

    在这里插入图片描述

一些常用配置

解析 ES6

借助 babel-loader + .babelrc,将 es6 语法转换成 es5 的语法,兼容旧的浏览器。

  • webpack
1
2
3
4
5
6
7
8
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader',
},
]
}
  • .babelrc
1
2
3
4
5
6
7
{
"presets": [
"@babel/preset-env", // preset 就是预设,是一系列 babel 插件的集合
"@babel/preset-react" //react 语法解析
],
"plugins": ["@babel/proposal-class-properties"] // babel 插件
}

解析 css

  • css-loader 用于加载 .css 文件,转换成 commonjs 对象,这样就可以在代码中 import 或者 require 文件了。
  • style-loader 将样式通过 <style> 标签插⼊入到 head 中。
  • less-loader / sass-loader 转换 less 或者 scss 语法。
1
2
3
4
5
6
7
8
module: {
rules: [
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader'], // 从右往左解析
},
]
}

解析图片、字体

  • file-loader 用于处理图片、字体等文件。
1
2
3
4
5
6
7
8
module: {
rules: [
{
test: /\.(png|svg|jpg|gif)$/,
use: 'file-loader',
},
]
}
  • url-loader 也可以用于处理图片、字体等文件,并且可以设置较小的资源自动转换成 base64,以减少 http 资源请求。
1
2
3
4
5
6
7
8
9
10
11
12
13
module: {
rules: [
{
test: /\.(png|svg|jpg|gif)$/,
use: [{
loader: 'url-loader',
options: [
limit: 10240 // 单位是字节。小于10k的图片,会转换成 base64
]
}],
},
]
}

解析 .vue

vue loader 会解析 vue 文件,将 <template> 的内容转换为字符串,插入到 vue 的 template 属性中,并将生成的 js、css 交给下一步 babel-loader 和 css-loader 等去处理。

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
module.exports = {
mode: 'development',
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
},
// 它会应用到普通的 `.js` 文件
// 以及 `.vue` 文件中的 `<script>` 块
{
test: /\.js$/,
loader: 'babel-loader',
},
// 它会应用到普通的 `.css` 文件
// 以及 `.vue` 文件中的 `<style>` 块
{
test: /\.css$/,
use: ['vue-style-loader', 'css-loader'],
},
],
},
plugins: [
// 请确保引入这个插件来施展魔法
new VueLoaderPlugin(),
],
}

文件监听

  • webpack –watch => 当文件有更新时,会自动构建,但不会刷新浏览器,需要手动。

webpack 会轮询判断文件的最后编辑时间是否变化,某个文件发⽣了变化,并不会⽴刻告诉监听者,而是先缓存起来,等 aggregateTimeout 时间后再执行。

1
2
3
4
5
6
7
8
9
10
11
12
module.export = {
//默认 false,也就是不开启
watch: true, // 只有开启监听模式时,watchOptions才有意义
wathcOptions: {
// 默认为空,不监听的文件或者文件夹,支持正则匹配
ignored: /node_modules/,
// 监听到变化发生后会等300ms再去执行,默认300ms
aggregateTimeout: 300,
//判断文件是否发生变化,是通过不停询问系统指定文件有没有变化实现的,默认每秒问1000次
poll: 1000,
},
}

热更新

  • 方法一:webpack-dev-server 插件,有三个优势:
  1. 不刷新浏览器
  2. 不输出文件,放在内存中,没有磁盘 IO

package.json

1
2
3
"scripts": {
"dev": "webpack-dev-server --config webpack.dev.js --open"
},

webpack.config

1
2
3
4
5
6
7
module.exports = {
plugins: [new webpack.HotModuleReplacementPlugin()],
devServer: {
contentBase: './dist',
hot: true,
},
}
  • 方法二:webpack-dev-middleware 中间件。适⽤于灵活的定制场景,需要自己实现服务器(WDM 将 webpack 输出的⽂件传输给服务器)。

使用 express

1
2
3
4
5
6
7
8
9
10
11
12
const express = require('express')
const webpack = require('webpack')
const webpackDevMiddleware = require('webpack-dev- middleware')
const app = express()
const config = require('./webpack.config.js')
const compiler = webpack(config)
app.use(
webpackDevMiddleware(compiler, { publicPath: config.output.publicPath })
)
app.listen(3000, function () {
console.log('Example app listening on port 3000!\n')
})
  • 热更新原理
    在这里插入图片描述

启动阶段:

  1. 经过 webpack compiler 打包文件;
  2. 打包好后,将编译好的 bundle 传输给 bundle sever,bundle sever 是一个服务,让浏览器可以正常访问;

更新阶段:

  1. 当文件发生更新时,webpack compiler 打包文件;
  2. 代码发送到 HMR server,得知那些模块发生了改变;
  3. HMR server 通知 HMR runtime,通常以 json 数据传输,HMR runtime 更新浏览器代码,不需要刷新浏览器。

文件指纹

文件指纹一般用来做版本管理,对于没有修改的文件,可以继续取浏览器缓存。例如, 让 index.html 入口的 response header 设置为 cache-control: no-cache, 不允许缓存, 其他 js 文件设置缓存 cache-control: xxx。

  • Hash:和整个项目的构建相关,只要项⽬文件有修改,整个项⽬构建的 hash 值就会更更改。
  • Chunkhash:和 webpack 打包的 chunk 有关,不同的 entry 会生成不同的 chunkhash 值。
  • Contenthash:根据文件内容来定义 hash,文件内容不变,则 contenthash 不变。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
module.exports = {
output: {
// 文件
path: path.join(__dirname, 'dist'),
filename: '[name]_[chunkhash:8].js',
},
module: {
rules: [
{
test: /.(png|jpg|gif|jpeg)$/,
use: [
{
loader: 'file-loader',
options: {
// 图片
name: '[name]_[hash:8].[ext]',
},
},
],
},
],
},
}

常用占位符:
在这里插入图片描述

html / css / js 压缩

  • js 压缩: 内置了 uglifyjs-webpack-plugin 插件,不需要额外处理(当然也可以手动加一些配置)。
  • html 压缩:html-webpack-plugin,可以把空格、换行符、注释等都处理掉。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    plugins: [
    //多页多入口时,一个 html 页面加一次
    new HtmlWebpackPlugin({
    template: path.join(__dirname, 'src/index.html'),
    filename: 'index.html', // 打包出来的文件名
    chunks: ['index'], // 指定打包出来的 html 要写入哪些 chunks
    inject: true, // css 等会注入到 html 里面去
    minify: {
    html5: true,
    collapseWhitespace: true,
    preserveLineBreaks: false,
    minifyCSS: true,
    minifyJS: true,
    removeComments: false,
    },
    }),
    ]
  • css 压缩:optimize-css-assets-webpack-plugin,同时使⽤用 cssnano
    1
    2
    3
    4
    5
    6
    plugins: [
    new OptimizeCSSAssetsPlugin({
    assetNameRegExp: /\.css$/g,
    cssProcessor: require('cssnano'),
    }),
    ]

一些进阶配置

自动清理构建目录

每次构建的时候不会清理⽬录,造成构建的输出⽬录 output 文件越来越多,使用 clean-webpack-plugin 清理,默认会清理 output 指定的输出目录。

1
plugins: [new CleanWebpackPlugin()]

css 前缀

由于市面上存在不同的浏览器渲染内核,需要为一些 css 属性增加前缀做兼容。如:

1
2
3
4
5
6
.box {
-moz-border-radius: 10px;
-webkit-border-radius: 10px;
-o-border-radius: 10px;
border-radius: 10px;
}

postCss 是一个后处理器,结合 autoprefixer,生成需要的浏览器 css 前缀。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
module.exports = {
module: {
rules: [
{
test: /.less$/,
use: [
'css-loader',
'less-loader',
{
loader: 'postcss-loader',
options: {
plugins: () => [
require('autoprefixer')({
// 支持最新的两个版本,1% 的使用比例,ios7以上,参考 caniuse.com
browsers: ['last 2 version', '>1%', 'ios 7'],
}),
],
},
},
],
},
],
},
}

移动端 px 转换成 rem

移动设备尺寸太多了,如果使用媒体查询实现布局会编写很多套,会比较麻烦。
使用 rem ,参考标准是根元素的 font-size。 root element font-size。
使用 px2rem-loader 自动转换成 rem。

  1. 使用 px2rem-loader 自动转换成 rem
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
module.exports = {
module: {
rules: [
{
test: /.less$/,
use: [
'css-loader',
'less-loader',
{
loader: 'px2rem-loader',
options: {
remUnit: 75,
remPrecision: 8, // 8 位小数
},
},
],
},
],
},
}
  1. 利用手机淘宝的库:lib-flexible, 自动设置根元素的 font-size

资源内联

将 css 和 js 代码内联到 html,意义:

  • 代码层⾯:

    • 页面框架的初始化脚本
    • 上报相关打点
    • css 内联,避免页面闪动
  • 请求层⾯面:

    • 减少 HTTP ⽹络请求数
    • 小图⽚或者字体内联 (url-loader)

  • html / js 内联:

    1
    2
    3
    4
    5
    raw-loader 内联 html
    // 如 meta 信息
    <script>${require('raw-loader!babel-loader!./meta.html')}</script>
    raw-loader 内联 JS
    <script>${require('raw-loader!babel-loader!../node_modules/lib-flexible')}</script>
  • css 内联:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
module.exports = {
module: {
rules: [
{
test: /\.scss$/,
use: [
{
loader: 'style-loader',
options: {
insertAt: 'top', // 样式插入到 <head>
singleton: true, //将所有的style标签合并成一个 }
},
},
'css-loader',
'sass-loader',
],
},
],
},
}

多页面打包方案

  1. 每个⻚面对应一个 entry,⼀个写 html-webpack-plugin(缺点:每次新增或删除⻚面需要改 webpack 配置)
  2. 动态获取 entry 和设置 html-webpack-plugin 数量。利用 glob.sync。
    1
    entry: glob.sync(path.join(__dirname, './src/*/index.js'))

source-map

source-map 的作用是通过 source map 定位到源代码,开发环境开启,线上环境关闭,线上排查问题的时候可以将 sourcemap 上传到错误监控系

提取公共资源

每个页面(多页非单页)可能会使用相同的模块,不需要重复打包了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module.exports = {
optimization: {
splitChunks: {
// webpack4
chunks: 'async', // async 只会处理 import() 异步引入的库, initial 是正常同步引入的库, all是所有的库
minSize: 30000, // 抽离的包最小大小,小于这个大小将不会抽离
maxSize: 0, // 抽离的包最大大小
minChunks: 1, // 最小引用的次数,比如包被引用了两次
automaticNameDelimiter: '~',
name: true,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
},
},
},
},
}

Tree shaking

webpack 借鉴了 rollup。用于擦除无用代码:

  • 代码不会被执行,不可到达
  • 代码执⾏的结果不会被用到
  • 代码只会影响死变量(只写不读)

例如:

1
2
3
if (false) {
console.log('这段代码永远不会执行')
}

必须是 es6 import 语法。原理利用了 ES6 模块的特点:

  • 只能作为模块顶层的语句句出现
  • import 的模块名只能是字符串串常量量
  • import binding 是 immutable 的

像 import() 动态引入的代码,只能在执行的时候才知道有没有用到,不能运用。

代码擦除: uglify 阶段删除⽆用代码

scope hoisting

打包后的 es5 代码,为了模拟模块作用域,会存在大量的闭包。

在这里插入图片描述

存在问题:

  • ⼤量作⽤域包裹代码,导致体积增大(模块越多越明显)
  • 运行代码时创建的函数作⽤域变多,内存开销变大

解决原理:
将所有模块的代码按照引用顺序放在一个函数作⽤域里,然后适当的重命名一些变量以防⽌变量名冲突。
通过 scope hoisting 可以减少函数声明代码和内存开销。

webpack mode 为 production 默认开启。

动态引入

脚本懒加载,使得初始下载的代码更小。

ES6: 动态 import() (⽬前还没有原⽣支持,需要 babel 转换)

  • .babelrc:
    1
    2
    3
    {
    "plugins": ["@babel/plugin-syntax-dynamic-import"],
    }

优化构建时的命令显示

  • 设置 stat
    在这里插入图片描述

  • 使用 friendly-errors-webpack-plugin (stats 设置成 errors-only)

1
2
3
4
module.exports = {
plugins: [new FriendlyErrorsWebpackPlugin()],
stats: 'errors-only',
}

echo$ 可以输出是否打包出错。

compiler 在每次构建结束后,会在 plugin 触发 done 的钩子,所以可以监听 done 捕获:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
module.exports = {
plugins: [
function () {
this.hooks.done.tap('done', (stats) => {
if (
stats.compilation.errors &&
stats.compilation.errors.length &&
process.argv.indexOf('- -watch') == -1
) {
console.log('build error')
process.exit(1)
}
})
},
],
}

案例:使用 webpack 打包基础库

实现⼀个⼤整数加法库的打包,效果:

  • 压缩版本和非压缩版本(分别用于开发和产线)
  • ⽀持 AMD/CJS/ESM 模块引⼊

  • 目录结构
    v

  • 模块语法
    在这里插入图片描述

  • 暴露
    在这里插入图片描述


webpack 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const TerserPlugin = require('terser-webpack-plugin')

module.exports = {
entry: {
'large-number': './src/index.js',
'large-number.min': './src/index.js',
},
output: {
filename: '[name].js',
library: 'largeNumber',
libraryTarget: 'umd',
libraryExport: 'default',
},
mode: 'none', // 去掉默认压缩,改用 terser-webpack-plugin 压缩
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
include: /\.min\.js$/,
}),
],
},
}

新增入口文件,判断不同环境使用的入口:

1
2
3
4
5
if (process.env.NODE_ENV === 'production') {
module.exports = require('./dist/large-number.min.js')
} else {
module.exports = require('./dist/large-number.js')
}

package.json 配置,将 index 设置为 入口, prepublish 钩子设置打包。

1
2
3
4
5
6
7
8
9
10
{
"name": "large-number",
"version": "1.0.1",
"description": "大整数加法打包",
"main": "index.js",
"scripts": {
"build": "webpack",
"prepublish": "webpack"
}
}

webpack 配置设计

在这里插入图片描述

webpack 构建速度和体积优化

  1. 在 node 中使用 webpack 回调查看打包结果
    在这里插入图片描述

  2. 使用 speed-measure-webpack-plugin 分析打包速度
    在这里插入图片描述

  3. 使用 webpack-bundle-analyzer 查看打包结果
    在这里插入图片描述

  4. 多进程/多实例构建 => thread-loader / HappyPack
    在这里插入图片描述

  5. 多进程并行压缩代码 => parallel-uglify-plugin / uglifyjs-webpack-plugin 开启 parallel 参数 / terser-webpack-plugin 开启 parallel 参数

  6. 增加构建缓存(提升二次构建速度)

    • babel-loader 开启缓存
    • terser-webpack-plugin 开启缓存
    • 使用 cache-loader 或者 hard-source-webpack-plugin