从基础到实战 手把手带你掌握新版Webpack4.0-1

webpack 是什么?

webpack 是模块打包工具。它不能理解成 es6 的翻译器,因为它只认识 import 语法,其他的高级语法不认识。因为它是模块打包工具,所以各种模块的语法它都认识。

url-loader 和 file-loader

url-loader 可以完全替代 file-loader;

css-loader 的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 {
text:/\.css|less$/,
use:[
'style-loader',
{
loader:'css-loader',
options:{
// 指定通过import引入的样式也走后面的2个loader
importLoaders:2,
// css模块化打包
modules:true
}
},
'less-loader',
'postcss-loader'
}
  1. importLoaders 的含义
    样式在解析时,通常是从下到上或从右到左的依次执行loader,但是在写样式时,有时会出现@import ../list/index.less的语法,这时在打包的时候import的样式可能就不会走css-loader后面的 loader,直接走css-loader,所以通过设置importLoadders,让通过import引入的样式,在引入之前也都走css-loader后的 2 个loader
  2. css 模块化
    在引入 css 时通常有如下两种方式
1
2
3
4
5
// 全局的方式
import './index.less';

// 模块的方式
import styles from './index.less'

两种方式的区别

  • 全局的方式引入,只要有元素使用的样式在index.less中有定义,样式就会起作用。如果多个css文件中有相同名字的样式,则会出现样式污染的情况。
  • 模块式引入,要想使用模块式样式,需要在css-loader中配置modules:true,这样在使用时,
    • 只有引入的模块,
    • 并且指定了样式的地方才会起作用。
1
2
3
4
5
6
import styles from './index.less'

// 有效(非react语法)
<div class={styles.name}></div>
// 无效,虽然上面已经引入了样式,即使是单页面应用,由于下面的使用方式不是模块调用,所以即使样式名在index.less中有也没用
<div class="name"></div>

打包字体文件

使用 file-loader 即可。

代码映射

devtool 构建速度 重新构建速度 生产环境 品质(quality)
(none) +++ +++ yes 打包后的代码
eval +++ +++ no 生成后的代码
cheap-eval-source-map + ++ no 转换过的代码(仅显示行);eval是最快的,另外这个不包含第三方模块,所以感觉开发中使用这个最合适。
cheap-module-eval-source-map o ++ no 原始源代码(仅显示行);但是视频推荐使用这个
eval-source-map + no 原始源代码
cheap-source-map + o no 转换过的代码(仅显行)
cheap-module-source-map o - no 原始源代码(仅显行)
inline-cheap-source-map + o no 转换过的代码(仅显示行)
inline-cheap-module-source-map o - no 原始源代码(仅显示行)
source-map yes 原始源代码
inline-source-map no 原始源代码
hidden-source-map yes 原始源代码
nosources-source-map yes 无源代码内容

+++ 非常快速, ++ 快速, + 比较快, o 中等, - 比较慢, – 慢

  • cheap的含义:只针对业务代码进行映射,不包括安装的第三方包。另外 cheap 是只映射到行,不会映射到列,即行中的第几个字符。
  • module的含义:就是包含安装的第三方模块。比如cheap-module-source-map
  • eval的含义,eval就是把源代码打包成字符串,放到eval中。eval的打包速度是最快的。

    视频中推荐开发模式使用cheap-module-eval-source-map,生产环境使用cheap-module-source-map

自我理解

evalinline关键字的,会把源代码和打包后的文件打包在一起,不包含的会对源代码进行单独打包,生成一个单独的文件。

dev-server

dev-server就是先通过webpack打包,然后启动一个node服务,把打包后的静态资源启动起来,但是dev-server是把静态资源放到内存中了,并没有放到磁盘上。
所以可以在dev-serverbefore钩子中写自己的mock

就是启动了一个 node 服务

手写一个简单的 dev-server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const express = require("express");
const webpack = require("webpack");
const webpackDevMiddleware = require("webpack-dev-middleware");
const config = require("./webpack.config.js");
// 使用webpack进行打包
const complier = webpack(config);

const app = expres();

// 使用express中间件,每当文件发生变化,webpackDevMiddleware使用complier进行打包。
app.use(
webpackDevMiddleware(complier, {
publickPath: config.output.publicPath,
}),
);

app.listen(3000, () => {
console.log("server is runing.");
});

自己写需要配置的东西比较多,推荐使用dev-server.

热更新 HMR

不配置 HMR,当有改动时,也会自动更新,但是这时是重新加载,如果页面上有数据或者状态值发生改变,是不会保留的。
而 HMR 是只更新变化的部分,并且保留页面的状态和数据。

1
2
3
4
5
6
7
devServer:{
contentBase:'./dist/,
open:true,
port:3000,
hot:true,
hotOnly:true
}
  • hot 是开启 hmr 模式
  • hotOnly 表示,启用热模块替换,在构建失败时不刷新页面作为回退。配置了这个有时候需要手动刷新,还是不要配置了。

记得配置new webpack.HotModuleReplacementPlugin()

上面讲解了热更新及它的配置,但是这还没完,配置后,如果想让它起作用,需要在代码中编写相关的代码才行,如下:

1
2
3
4
5
6
7
8
9
10
11
counter();
number();
// 如果开启了HMR
if (module.hot) {
module.hot.accept("./number", () => {
// 删除老的元素
document.body.removeChild(document.getElementById("number"));
// 重新执行变更的元素
number();
});
}

很多人奇怪,不对啊,在开发过程中没写过这样的代码啊,只要上面的配置完,HMR就已经起作用了啊,这是为什么呢?

这是因为在开发过程中,我们使用的loader都已经实现了 HMR 的接口,也就是做了上面类似的操作。所以我们不需要手动再写。比如style-loadervue-loader、react 的bable-preset等,都已经实现了 HMR 接口。可参考官网“概念”模块里面的讲解。

Babel 解决 ES6 语法

由于现在很多浏览器还不支持 ES6 语法,所以需要使用 babel 把 es6 语法转换成所有浏览器都支持的 ES5 语法。
babel-loader并不会直接把ES6语法转换成ES5,它只是搭建babelwebpack的桥梁,它需要使用babel/preset-env插件对 ES6 进行转换。
babel/preset-env只能转换 ES6 语法,比如箭头函数、let 等,它不能转换 ES6 中的函数,比如 Promise、Array.map()等,如果需要支持这些 ES6 的函数,需要使用babel/polyfill

1
import "@babel/polyfill";

在使用时,我们需要引入;
由于 polyfill 文件很大,所以在打包时如果把没有用到的方法也打包进来,那打包后的文件会很大,如果只打包用到的方法,这样就比较符合我们的用意。通过设置useBuiltIns:'usage'可以达到我们的目的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
module:{
rules:[
rules:[
{
test:/\.jsx?$/,
exclude:/node_modules/,
loader:'babel-loader',
options:{
presets:[
['@babel/preset-env',{
// 指定需要适应的浏览器版本,如果浏览器版本已经支持了ES6,那么代码就不会被转换了
targets:{chrome:'67'}
// 指定polyfill只打包用到的函数,比如只用了promise,那只会打包polyfill中的promise。当配置了这个,就不需要手动import ‘@babel/polyfill’了
useBuiltIns:'usage'
}]
],
}
}
]
]
}

开发第三方库时 polyfill 的配置

由于上面的引入是全局的,所以如果开发第三方包时也那样做,就会污染全局变量,应该使用下面的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
module:{
rules:[
rules:[
{
test:/\.jsx?$/,
exclude:/node_modules/,
loader:'babel-loader',
options:{
"plugins":[["@babel/plugin-transform-runtime",{
"corejs":2,
"helpers":true,
"regenerator":true,
"useESModules":false,
}]]
}
}
]
]
}

另外 impor 的地方需要删除掉。同时需要安装npm install --save @babel/runtime-corejs2

不想在 options 中配置,可以创建 babel 的配置文件.babelrc来专门配置 babel

Tree Shaking

官网解释其实已经很明确了。
就是移除没用到的代码,打包的时候只把用到的代码进行打包。它依赖于 ES2015 模块系统中的静态结构特性,例如 importexport。也就是说 Tree Shaking 只支持ES6module,对于commonjs的模块是不支持的,因为它是动态结构。

Tree Shaking 形象的描述了这一行为,摇动树,把没用的树叶摇掉,只留下有用的。

在开发环境下,这个功能默认是没开启的,所以在development环境下,需要设置

1
2
3
4
plugins:[],
optimization:{
usedExports:true
}

这样在开发模式下,就只会对使用到的代码进行打包,但是这样有个问题,比如@babel/polyfill这类文件,需要引入,但是不需要专门导出其中的某个方法,这时打包时就会认为这个包没用到,打包时就会删除它,这样就会有问题。
我们可以通过设置package.json中的sideEffects来指定哪些包不需要tree shaking的包。

1
2
// 这样就不会对@babel/polyfill进行tree shaking了
"sideEffects":["@babel/polyfill"]

但是由于@babel/polyfill不需要手动引入,所以我们需要对所有的文件进行tree shaking,这时把siideEffects设置成false即可。

1
2
// 对所有的文件进行tree shaking
"sideEffects":false

但是如果 css 没有使用 module 的模式,则会认为 css 文件没有导出任何东西,也会被移除,所以需要对 css 进行配置

1
2
// 不对所有的css进行tree shaking
"sideEffects":["*.css"]

上面配置完,开发模式下的 tree shaking 就配置完了,但是通过查看打包后的文件发现,没用到的代码还是被打包了 😂,是不是没起作用,其实起作用了,在注释里面标记了文件有哪些方法,真正用到的是哪些方法,只是标记了一下,为什么不移除掉呢,因为如果移除掉,在调试的时候,代码的行数可能就对不上了,所以开发模式下配置这个就是脱裤放屁,妈了个巴子

所以 tree shaking 真正有意义的是在生产环境,但是生产环境不需要手动做 tree shaking 配置,webpack 自动都配置好了,所以这功能了解就行,不需要做什么。sideEffects 需要设置下。

webpack 和 code spliting

code spliting 就是把代码打包成多个包,这样在浏览器加载的时候就可以并行加载,提高性能和效率。
实现的方式有很多种,下面列举一些常用的。

  1. 把全局使用的包单独打包
    比如lodash,这个包封装了一些封装了一些常用的方法,完全可以挂在在 window 全局上,而不需要在每个使用的地方 import。
    新建一个js文件lodash.js
1
2
import _ from "lodash";
window._ = _;

然后打包的时候,就可以把这个文件单独打包,这样就不会在每个使用的地方都打包一次(也可以通过 webpack 提取模板的方式解决)

1
2
3
4
entry:{
main:'./index.js',
lodash:'./lodash.js'
}

这样打包的时候,lodash就被单独打包了。

使用 webpack 自带的代码分割

我们不需要专门的对 lodash 这种第三方模块进行处理,通过配置 webpack,它自动做代码分割。

1
2
3
4
5
6
7
8
9
10
11
// 业务代码中
import _ from 'lodash'
// ... 业务代码

// webpack 配置
plugins:[],
optimization:{
splitChunks:{
chuks:'all'
}
}

上面的配置是对同步代码的;
如果是异步加载,那么上面的配置就不需要了,webpack 会把异步加载的包自动单独进行打包。
默认打出的 chunk 包名是用包的 id 来命名的,也就是 0,1,2 等等,可以手动指定 chunk 的包名。
异步加载组件支持一种语法叫做“魔法注释”,可以通过这种方式设置生成 chunk 的名字。
比如下面是一段异步加载的代码(注意两端的空格)

1
2
3
4
5
6
7
8
9
10
11
function getComponent(){
return import('/* webpackChunkName:"lodash" */ lodash').then({default:_}=>{
var element=document.createElement('div');
element.innnerHTML=_.join(['Hello','Every','One'],'-');
return element;
})
}

getComponent().then(element=>{
document.body.appendChild(element);
})

上面打包后lodash的包就会被命名成vendors~lodash.js; 是不是奇怪为什么会多一个vendors~,如果不想让带着前缀,可以通过配置插件SplitChunksPlugin来实现。

1
2
3
4
5
6
7
8
9
10
11
12
module.exports = {
//...
optimization: {
splitChunks: {
chunks: "all",
cacheGroups: {
vendors: false, // 这个设置成false
default: false, // 这个设置成false
},
},
},
};

这样再打包,包名就成lodash.js了。

其实代码分割就是通过配置SplitChunksPlugin插件来实现的。虽然上面是通过注释的方式,其实也是走的SplitChunksPlugin插件。

SplitChunksPlugin 详细讲解

SplitChunksPlugin有一个默认配置,参见官网
,手动设置成空对象或不配置,走的都是这个默认配置。

  • chunks
chunks 参数值 含义
all 把动态和非动态模块同时进行优化打包;所有模块都扔到 vendors.bundle.js 里面。依赖cacheGroup中的配置,如果cacheGroup中的配置设置成 false,则不会分割打包。
initial 把非动态模块打包进 vendor,动态模块优化打包
async 把动态模块打包进 vendor,非动态模块保持原样(不优化)
  • minSize
    需要进行代码分割的最小尺寸。只有大于这个值的包才会进行代码分割。单位是字节,(1024 字节=1kb)
  • maxSize
    配置打包后 chunk 的包的大小,比如配置 50000,50kb,那么lodash包的大小超过了这个,打包的时候lodash会被拆分多个 50kb 的包。一般不配置这个选项。
  • minChunks
    设置模块被引用多少次才进行代码拆分。

    这里说的引用是指被打包后生成的 chunk 所引用的次数,不是开发代码中引用的次数(是这样吗?)

  • maxAsyncRequests
    设置页面同时加载的模块数量。比如设置 5,当打包前 5 个库的时候,会分别打包成 5 个包文件,超过 5 个的模块就不再进行代码分割。
  • maxInitialRequests
    设置整个网站首页或入口文件在进行加载时,可能也会引入其他代码库,这个就是设置入口文件引入的库最多拆分几个包。比如设置成 3,即使入口文件引入了 5 个文件,那也会只拆分成三个包。
  • automaticNameDelimiter
    设置生成文件时名称之间的连接符。
  • name
    设置cacheGroup中的名字是否有效,这个参数一般不动。
  • cacheGroups
    配置拆分的规则。为什么叫“缓存组”,比如有两个第三方包,lodash 和 jquery,当遇到 lodash 时,看下是否满足拆规则,如果满足则先缓存下来,然后再分析 jquery,等把包都分析完了,把都满足规则的包一起打包到对应组指定的文件中。
    • reuseExistingChunk
      指定打过包的文件不会再被打包。比如有 a,b 两个包,在 c 文件中引入了 a,b,但是 a 中也引入了 b,这样在打包的时候,由于在对 a 打包的时候已经打过 b 的包,所以在对 c 打包的时候,不会再对 b 进行打包。
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 = {
//...
optimization: {
splitChunks: {
chunks: "async",
minSize: 30000,
maxSize: 0,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
automaticNameDelimiter: "~",
automaticNameMaxLength: 30,
name: true,
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10,
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true, //指定打过包的模块不会被重复打包。
},
},
},
},
};
文章作者: wenmu
文章链接: http://blog.wangpengpeng.site/2020/01/09/%E4%BB%8E%E5%9F%BA%E7%A1%80%E5%88%B0%E5%AE%9E%E6%88%98%20%E6%89%8B%E6%8A%8A%E6%89%8B%E5%B8%A6%E4%BD%A0%E6%8E%8C%E6%8F%A1%E6%96%B0%E7%89%88Webpack4.0-1/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 温木的博客
微信打赏