前言
webpack 是目前最火的项目构建工具,只需要简单的配置,就能够完成对模块的加载和打包。这篇文章意在让我从繁杂的文档中解脱出来。
不跟你多bb,先看 Webpack 配置
1 | const path = require('path'); |
在实际开发当中,是要区分开发环境和生产环境。大致目录:
1 | + build |
在 build 目录下有三个 webpack 配置,分别是:
- webpack.base.conf.js
- webpack.dev.conf.js
- webpack.prod.conf.js
这分别对应开发、生产和测试环境的配置。其中 webpack.base.conf.js 是一些公共的配置项。我们使用 webpack-merge 把这些公共配置项和环境特定的配置项 merge 起来,成为一个完整的配置项。比如 webpack.dev.conf.js 中:
1 | const webpack = require('webpack'); |
从上面 webpack 配置来看,再配置 .babelrc 文件,使其能够使用新的 ES 语法
1 | { |
再加一个 postcss.config.js 文件
1 | module.exports = { |
这些基本满足日常开发需求,若是用 vue 或者 react 开发,再添加相关 loader 就好。
Webpack 高级概念
Tree Shaking
tree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)。它依赖于 ES2015 模块语法的 静态结构 特性,例如 import 和 export。
不过需要注意的是, tree shaking 能移除无用代码的同时,也有一定的副作用(错误识别无用代码)。比如你可能会遇到 UI 组件库没有样式的问题,这个问题原因在于 tree shaking 不仅对 JS 生效,也对 CSS 生效 。我们通常在导入 CSS 时使用 import ‘xxx.min.css’ , ES6 的静态导入 + 生产环境满足了 tree shaking 的生效条件,并且 Webpack 无法判断 CSS 有效,所以它被当做了 dead-code 然后被删除。为了解决这个问题,你可以在 package.json 中添加一个 sideEffects 选项,告知 Webpack 那些文件是可以直接引入而不用 tree shaking 检查的,使用如下:
package.json
1 | { |
示例:创建一个 math.js
1 | export const add = (a, b) => { |
在 index.js 中引用它
1 | // Tree Shaking |
然后利用之前的 webpack 配置打包,因为其中 optimization 配置了usedExports。看打包后的 main.js 中是否有多余的 minus 方法。
Code Splitting
它一般做什么:
- 为 vendor 单独打包 (vendor 指第三方的库或者公共的基础组件,因为 Vendor 的变化比较少,单独打包利于缓存)
- 为 Manifest (Webpack 的 Runtime 代码)单独打包
- 为不同入口的公共业务代码打包(同理,也是为了缓存和加载速度)
- 为异步加载的代码打一个公共的包
在 webpack3 及以前我们都利用 CommonsChunkPlugin 将一些公共代码分割成一个 chunk,实现单独加载。在 webpack4 中 CommonsChunkPlugin 被废弃,使用 SplitChunksPlugin,其配置如上 webpack 所示。
有一点要知道其实 Code Splitting 与 webpack 是无关的,因为不需要 webpack 依然可以做代码分割。之所以用到它是因为更加方便。
示例:index.js1
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
34// 我现在想引入一个 lodash 工具库,第一种方式是直接 import 它(静态引入)。
import _ from 'lodash'; 1mb
... 业务逻辑 1mb
console.log(_.join(['a','b'], '*'))
// 这种方式,首次访问页面时,加载 main.js 假设打包后是 2mb,当业务逻辑发生改变时,又要加载 2mb 的内容
第二种方式
那我不用 splitChunksPlugin 配置呢,那就先创建一个新的文件 lodash.js,引入 lodash ,再付给全局变量 window,再到 webpack 配置中配置一下入口文件加一个入口。如此 main.js 就被拆成两个文件 lodash.js、main.js,当业务逻辑发生改变时,只需加载 main.js 即可 (1mb)
第三种方式
在 webpack 中的 optimization 加上 splitChunksPlugin。
optimization: {
splitChunks: {
chunks: 'all'
}
}
这里打包后 dist 目录就会增加一个 vendors~main.js 文件。
第四种方式
当然除了设置 webpack 之外,还有一种是异步引入 lodash,这种方式打包后也会生成一个单独的文件类似 0.js
例如: index.js 中是这样
function getComponent() {
return import('lodash').then(({ default: _ }) => {
var element = document.createElement('div');
element.innerHTML = _.join(['hello', 'world'], ',');
return element;
})
}
getComponent().then(element => {
document.body.appendChild(element);
})
当然也可以用 async / await 代替 promise。
Lazy Loading 懒加载
按需加载又名懒加载,是指当需要依赖的页面被打开采取加载这个依赖,这样就减少了主页的负担,提升首屏渲染速度。而要做到按需加载,你只需在导入依赖的时候用 import 或 require.ensure 这两种动态加载方式。
在这一点上可以根据上面的代码稍微改动下如下所示:
1 | async function getComponent() { |
打包分析,Preloading,Prefetching
打包分析
推荐使用 webpack-bundle-analyzer ,它会启动一个服务,在浏览器中很清楚地展现生成物和源文件的映射关系和层级,也可以在 package.json 中加上
1 | webpack --profile --json > stats.json // 意思是把配置信息存进 stats.json 中 |
安装
1 | npm install --save-dev webpack-bundle-analyzer |
配置:在 webpack.prod.conf.js 中增加以下配置
1 | const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin; |
在 项目的 package.json 文件中注入如下命令,以方便运行她(npm run analyz),默认会打开http://127.0.0.1:8888作为展示。
1 | “analyz”: “NODE_ENV=production npm_config_report=true npm run build” |
运行 npm run build,如图所示
Prefetch
在声明 import 时,使用下面这些内置指令,可以让 webpack 输出 “resource hint(资源提示)”,来告知浏览器
- prefetch(预取):将来某些导航下可能需要的资源
- preload(预加载):当前导航下可能需要资源
下面这个 prefetch 的简单示例中,有一个 HomePage 组件,其内部渲染一个 LoginButton 组件,然后在点击后按需加载 LoginModal 组件。
LoginButton.js
1 | import(/* webpackPrefetch: true */ 'LoginModal'); |
这会生成 并追加到页面头部,指示着浏览器在闲置时间预取 login-modal-chunk.js 文件。
与 prefetch 指令相比,preload 指令有许多不同之处:
- preload chunk 会在父 chunk 加载时,以并行方式开始加载。prefetch chunk 会在父 chunk 加载结束后开始加载。
- preload chunk 具有中等优先级,并立即下载。prefetch chunk 在浏览器闲置时下载。
- preload chunk 会在父 chunk 中立即请求,用于当下时刻。prefetch chunk 会用于未来的某个时刻。
- 浏览器支持程度不同。
CSS 文件的代码分割
首先安装 MiniCssExtractPlugin
1 | npm install --save-dev mini-css-extract-plugin // 打包 css |
webpack.prod.config.js
1 | const MiniCssExtractPlugin = require("mini-css-extract-plugin"); |
Webpack 与浏览器缓存(Caching)
当我们每次打包的时候,这里有个问题。在改变源代码的情况下打包后,用户浏览器上有我们之前代码包的缓存,这里我们需要配置一下来更新下代码。
webpack.pord.conf.js1
2
3
4
5
6module.exports = {
output: {
filename: '[name].[contenthash].js',
chunkFilename: '[name].[contenthash].js'
}
}
显而易见,这是根据代码内容变化所产生一个 hash 值,代码不变,文件包名字也不改变,这样就解决了上面的问题。
当然还有可能会遇到一个问题,当我们没有改变这源代码,但是打包后的 hash 值依然改变了,这即是 webpack 的版本太低了,在新版本中会默认有个这样的配置如下:
1 | module.exports = { |
首先为什么 hash 值依然改变了,那是因为在我们打包后的 main.js 和 chunk 之间有着关联关系的存在,即 manifest,老版本在打包后 manifest 可能会发生改变。所以造成 hash 跟着变了的结果。上面配置解决的即是把关连关系单独提出一个文件出来为 runtime.[hash].js 这样每次打包的时候主文件和库文件的 hash 问题就解决了。
Shimming 的作用
它的作用意在解决 webpack 打包过程当中的兼容问题,类似 @babel/polyfill 解决的是兼容低版本浏览器中没有 promise 等全局变量的问题。这就是所谓的垫片。
先看个🌰:
index.js1
2
3
4
5
6
7
8
9import $ from 'jquery';
import _ from 'lodash';
import { ui } from './jquery.ui'
ui();
const dom = $('div');
dom.html(_.join(['dell', 'lee'], '---'));
$('body').append(dom);
jquery.ui.js
1 | export function ui() { |
在此 index.js 中要在外部引入 jquery.ui.js 这个库文件,但是这里会有个小问题,jquery.ui.js 在打包后会报一个错 $ is undefined. 显而易见,在ES module 中变量是不能跨文件的,也即是安全变量。这种形式解决了模块与模块之间的耦合。
当然你不可能修改库文件代码,或者去 node_modules 里面去修改代码,在里面引入一个 $ 变量。所以这时候就要用到 shimming。如下所示:
webpack.base.conf.js1
2
3
4
5
6
7
8
9module.exports = {
plugins: [
new webpack.ProvidePlugin({
$: 'jquery', // 作用是在发现模块中有 $ 这个字符串的时候,就表示引入了 jquery
_join: ['lodash', 'join'] // shimming 的细粒化,即只用lodash中的 join 方法
})
]
}
shimming 就是帮助修改或者解决 webpack 一些不能解决的事情。又比如,我想在模块中使用 this,同时要使它指向 window。我们知道 模块中的 this 都是指向模块自身。为了得到你想要的 this。可以这样:
1 | module.exports = { |