🤾♀️序言
上一篇文章中我们讲到了 webpack
的一些基础特性,但是呢,单单会基础特性还是远远不够的。因此,在今天的文章中,将给大家带来 webpack
的高级特性,包括但不限于 dev
环境和 prod
环境的区分打包,以及使用 webpack
对项目进行代码分割等等技巧。
废话不多说,下面开始进入今天的学习吧~🎳
🏓一、Tree Shaking
1. 引例阐述
假设现在我们有一个需求,写一段程序来对两个数做加法和减法。现在,我们来实现这个功能。具体代码如下:
export const add = (a, b) => { console.log(a + b); } export const minus = (a, b) => { console.log(a - b); } 复制代码
接下来,我们在入口文件引入它。具体代码如下:
import { add } from './math.js'; add(1, 2); 复制代码
在这种状态下,我们用 npx webpack
命令来对项目进行打包。查看打包后的文件代码:
/***/ "./src/math.js": /*!*********************!*\ !*** ./src/math.js ***! \*********************/ /*! exports provided: add, minus */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; __webpack_require__.r(__webpack_exports__); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "add", function() { return add; }); /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "minus", function() { return minus; }); const add = (a, b) => { console.log(a + b); }; const minus = (a, b) => { console.log(a - b); }; /***/ }) 复制代码
我们可以发现,在入口文件我们只引入了加法的操作,因为我们当下只想用到加法功能,而暂时还不需要用减法。但是呢,打包后的文件,把减法部分的内容,也一起,给打包进去了。这无形之中,多多少少给我们添加了不少麻烦。
因此呢,我们就需要引入 webpack
中的 Tree Shaking
,来解决这个问题。
2. Tree Shaking配置
首先我们需要在 webpack.common.js
里面进行配置。如果是在 Development
的模式下,那么默认是不拥有 Tree Shaking
的。因此需要进行以下配置,具体代码如下:
//node的核心模块 const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const CleanWebpackPlugin = require('clean-webpack-plugin'); const webpack = require('webpack'); module.exports = { mode:'development', optimization: { usedExports: true } } 复制代码
在开发环境下,我们需要加上 optimization
模块来对项目开启 Tree Shaking
。
接下来,我们继续对 package.json
进行配置。在这个文件下,我们需要添加以下代码:
{ "sideEffects": false } 复制代码
sideEffects:false
指的是什么意思呢?当设置为 false
时,表明对所有的 ES Module
开启 Tree Shaking
操作。
值得注意的是, Tree Shakig
只支持 ES Module
这种类型的引入,并不支持 commonJS
等类型的引入。这是因为, ES Module
的底层实现是静态的,而 commonJS
的实现则是动态的。
还有另外一种情况就是,如果你想要使得某些模块不开启 Tree Shaking
,那么可以将 sideEffects
进行以下配置。具体代码如下:
{ "sideEffects": [ "*.css", "@babel/poly-fill" ] } 复制代码
以上代码的意思为,对所有的 css
文件以及 @babel/poly-fill
不开启 Tree-shaking
功能。
接下来我们来看一下配置完成之后,在开发环境下的效果。具体代码如下:
/***/ "./src/math.js": /*!*********************!*\ !*** ./src/math.js ***! \*********************/ /*! exports provided: add, minus */ /*! exports used: add */ /***/ (function(module, __webpack_exports__, __webpack_require__) { "use strict"; /* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "a", function() { return add; }); /* unused harmony export minus */ const add = (a, b) => { console.log(a + b); }; const minus = (a, b) => { console.log(a - b); }; /***/ }) /******/ }); //# sourceMappingURL=main.js.map 复制代码
大家可以看到,如果是在开发模式下,减法功能依然是存在的,只是多个一句 /*! exports used: add */
来表明只使用 add
这个功能。
这是为什么呢?因为在开发模式下, webpack
怕这个地方如果引入了其他模块,那么删除就很容易导致报错,所以它没有进行删除。
但如果 mode
是在生产环境下时, Tree Shaking
的作用就比较明显了。我们修改完 mode
后来看下打包后的结果。具体代码如下:
function (e, n, r) { "use strict"; r.r(n); var t, o; t = 1, o = 2, console.log(t + o) } 复制代码
生产环境下打包后只有一行,小编摘取了最终要的部分出来。
大家可以看到,当处于生产环境下时,打包后的结果只显示了加法功能。而我们没有使用到减法,所以这个时候就不会连带着打包出来了。
顺带着这个话题,接下来我们就来讲一下, webpack
在 development
和 Production
模式下的区分打包。
🏸二、Development和Prodiction模式的区分打包
1. 项目打包结构
通常情况下,我们的项目会有三个 webpack
的配置文件。一个是 webpack.common.js
,一个是 webpack.dev.js
,另外一个是webpack.prod.js
。第一个文件用来放开发环境和生产环境下共同的配置,第二个文件用来放开发环境下的配置,第三个文件用来放生产环境下的配置。
接下来我们来了解下,这三个配置文件的代码。
2. 共有配置webpack.common.js
如果我们不编写 common
文件的话,那么 dev
和 prod
这两个文件的代码重合度就会比较高,所以我们把相同的部分抽离出来。具体代码如下:
//node核心模块 const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const CleanWebpackPlugin = require('clean-webpack-plugin'); module.exports = { // 放置入口文件,明确怎么打包 entry:{ main: './src/index.js' }, module:{ rules:[{ test: /\.m?js$/, //exclude,顾名思义即排除在外。如果js文件在node_modules文件夹下面,那么我们就排除在外 // 因为node_module一般都是来自于第三方库,它已经自动的处理好此部分工作,所以我们没必要再去做重复操作 exclude: /node_modules/, use: { loader: "babel-loader", } },{ test:/\.(jpg|png|gif)$/, use:{ loader:'file-loader', options: { //placeholder 占位符 name: '[name]_[hash].[ext]', outputPath: 'images/', limit: 10240 } } },{ test:/\.scss$/, use:[ 'style-loader', { loader: 'css-loader', options: { //表明前面要先走sass-loader和postcss-loader importLoaders: 2, modules: true } }, 'sass-loader', 'postcss-loader' ] },{ test:/\.css$/, use:[ 'style-loader', 'css-loader', 'postcss-loader' ] },{ test: /\.(eot|ttf|svg)$/, use: { loader: 'file-loader', } }] }, plugins: [new HtmlWebpackPlugin({ //表明要引用哪一个模板 template: 'src/index.html' }),new CleanWebpackPlugin(['dist']) ], // 输出,表明webpack应该怎么输出 output: { publicPath: '/', //用[]可以生成多个文件 filename: '[name].js', // 指打包后的文件要放在哪个文件下 path: path.resolve(__dirname, 'dist') } } 复制代码
3. 开发环境webpack.dev.js
抽离完 common
代码之后,现在我们来编写 webpack.dev.js
文件。具体代码如下:
const webpack = require('webpack'); const merge = require('webpack-merge'); const commonConfig = require('./webpack.common.js'); const devConfig = { mode:'production', devtool: 'cheap-module-eval-source-map', devServer: { contentBase: './dist', // 当运行完npm run start时,会自动的帮我们打开浏览器 open: true, port: 8080, // 让我们的webpackDevServer开启hotModuleReplacement这样子的功能 hot: true, // 即便HMR没有生效,也不让浏览器自动刷新 hotOnly: true }, plugins: [ //热模块更新 new webpack.HotModuleReplacementPlugin() ], optimization: { usedExports: true } } module.exports = merge(commonConfig, devConfig) 复制代码
4. 生产环境webpack.prod.js
继续,我们来编写抽离后的 prod
代码。具体代码如下:
const merge = require('webpack-merge'); const commonConfig = require('./webpack.common.js'); const prodConfig = { mode:'development', devtool: 'cheap-module-source-map' } module.exports = merge(commonConfig, prodConfig) 复制代码
到这里,大家对比上面三个文件的代码可以发现,这样的代码抽离使得我们的项目结构变得更为清晰了。
5. 运行项目package.json
上面配置完成之后,我们现在要来想一下,想要运行开发环境和生产环境不用的配置,是不是应该把运行命令也给区分开来。因此,我们对 package.json
文件做出以下配置:
{ "scripts": { "dev": "webpack-dev-server --config webpack.dev.js", "build": "webpack --config webpack.prod.js" } } 复制代码
通过以上配置,那么我们就可以通过命令 npm run dev
和 npm run build
来运行项目,以此来区分项目是开发环境还是生产环境。
同时,如果我们想要在控制台更直观的看到报错信息,那么在开发环境的情况下,我们可以不用 webpack-dev-server
来运行,可以直接用用 webpack
来运行。具体代码如下:
{ "scripts": { "dev-server": "webpack --config webpac.dev.js", "dev": "webpack-dev-server --config webpack.dev.js", "build": "webpack --config webpack.prod.js" } } 复制代码
这样,我们就可以通过 npm run dev-build
,来使用 webpack
来运行项目。
⚽三、Webpack和Code Splitting、SplitChunksPlugin
1. 代码分割-Code Splitting
有时候,我们可能会遇到一个业务逻辑里面有上万行代码,那么这上万行代码打包后,全部就丢到 main.js
文件里面了,这样大的文件,会使得整个项目的加载速度会变得很是缓慢。因此,我们就需要用到代码分割Code Splitting来解决这件事情。
我们在 webpack.common.js
中进行配置。具体代码如下:
module.exports = { optimization: { splitChunks: { chunks: 'all' } } } 复制代码
通过以上代码我们可以得知,通过使用 optimization
中的 splitChunks
,达到了代码分割的效果。
那使用这个配置之后, webpack
想要做的事情是什么呢?
事实上,使用 splitChunks
之后,那么当 webpack
遇到公用的类库时,会帮我们自动地打包生成一个新的文件,之后再把其余的业务逻辑拆分到另外一个文件中去。
值得注意的是,公用类库不管是以同步的方式还是以异步的方式进行加载, webpack
都能够帮我们进行代码分割。
2. 引例阐述-SplitChunksPlugin
上面我们讲到了 webpack
的代码分割,那么实际上, webpack
的代码分割,其底层实现原理所使用的是 splitChunksPlugin
这个插件。接下来我们来讲一下这个插件。
在没有用 SplitChunksPlugin
这个插件之前,如果我们异步引入一个库,那么 webpack
给其打包后的文件名将会命名为 0.js
、 1.js
和 ......
。
我们现在希望在做代码分割时, webpack
能给我们的第三方库进行自定义命名,这又该怎么处理呢?
3. 实现方式-SplitChunksPlugin
首先我们对引入的库前面,添加 webpackChunkName
配置。具体代码如下:
function getComponent() { return import(/*webpackChunkName:"lodash"*/'lodash').then(({ default: _ }) => { var element = document.createElement('div'); element.innerHTML = _.join(['Monday', 'Tuesday'], '_'); return element; }) } getComponent().then(element => { document.body.appendChild(element); }) 复制代码
/*webpackChunkName:"lodash"*/
这句话想要表明的意思为:当我们异步的引入 lodash
这个库并且想要做代码分割时,即当我们给 webpack
进行打包时,给其起名为 lodash
。
上面这个配置知识第一步,接下来我们要来安装并使用一个动态引入的插件。具体代码如下:
安装插件:
npm install --save-dev @babel/plugin-syntax-dynamic-import 复制代码
在 .babelrc
下引入:
{ // plugins: ["dynamic-import-webpack"] 非官方支持插件 plugins: ["@babel/plugin-syntax-dynamic-import"] } 复制代码
配置 webpack.common.js
:
module.exports = { optimization: { splitChunks: { chunks: 'all', cacheGroups: { vendors: false, default: false } } }, } 复制代码
4. SplitChunksPlugin配置参数
接下来我们再来看关于 SplitChunksPlugin
的一些常用配置。具体代码如下:
module.exports = { optimization: { splitChunks: { /*使用async时,只对异步代码进行代码分割; 使用all时,对同步和异步代码同时进行代码分割; 使用initial时,表示对同步代码进行代码分割*/ chunks: 'all', //大于30kb时,做代码分割 minSize: 30000, //表明代码分割后文件最大的大小,如果超过了则会继续进行拆分;有一些文件如果拆分不了则此配置想基本就没啥用了 maxSize: 0, minRemainingSize: 0, //至少有两个块被使用了才会被提取 minChunks: 2, //表示同时加载的模块数,最多是5个 /*比如我们引入了10个类库,那么我们会做10次代码分割。 而这个时候我们将此参数填为5,那么webpack在打包时会将前10个库给我们生成5个js文件, 之后的就不会再做代码分割了,全部丢到一个文件里面去*/ maxAsyncRequests: 5, //入口文件做代码分割最多只能分割成3个js文件,超过3个就不会再做代码分割了 maxInitialRequests: 3, //文件生成时的中间符号 automaticNameDelimiter: '~', //让defaultVendors和default中的文件名有效 name: true, enforceSizeThreshold: 50000, //当打包同步代码时,cacheGroups cacheGroups: { defaultVendors: { //检测你引入的库,查询是否是在node_module下的 test: /[\\/]node_modules[\\/]/, priority: -10, //确定是在node_modules下后,将其进行打包,并命名为vendors.js filename: 'vendors.js' }, //对非第三方库的代码做代码分割 default: { priority: -20, reuseExistingChunk: true, filename: 'common.js' } }, } } } 复制代码
🏐四、打包分析,Preloading,Prefetching
1. 打包分析
打包分析指的是,当我们使用 webpack
来进行代码打包之后,我们可以借助打包分析的一些工具,来对我们打包生成的文件进行一定的分析,之后来看其打包的是否合理。那么如何进行打包分析呢?
我们要用到一个 github
的第三方仓库,戳此链接进入~
了解完文档该库的内容后,接下来我们要对 package.json
进行配置。具体代码如下:
{ "scripts": { "dev-build": "webpack --profile --json > stats.json --config ./build/webpack.dev.js", "dev": "webpack-dev-server --config ./build/webpack.dev.js", "build": "webpack --config ./build/webpack.prod.js" } } 复制代码
通过上面的代码,我们可以分析:在 --config
前面加上配置 --profile --json > stats.json
,意思为 webpack
打包后会生成打包分析文件,这个文件叫做 stats.json
,同时, --json
表示的意思为 stats.json
文件的格式是一个 json
格式的。
在生成完 stats.json
文件以后,我们就可以把它放到打包工具里进行分析。大家可以定位到官方文档中的 bundle-analysis ,里面提供了 webpack-chart
、 webpack-visualizer
等可视化工具供我们使用,这个可以依据个人需求,选择对应的工具,对 stats.json
文件进行分析。
2. Preloading、Prefetching
(1)引例阐述
事实上,当我们配置 splitChunks
时,里面的 chunks
默认值是 async
。也就是说,如果我们不对它进行配置,那 webpack
默认只对异步代码进行代码分割。 webpack
为什么要这么做呢?
webpack认为把同步代码打包在一个文件就好了,同时呢,它希望我们要多写点异步加载代码,这样才能让我们的网站性能得到真正的提升。
现在,我们来说一个生活中一个非常常见的场景。假设现在我们在登录知乎网站,那么刚进去的时候我们是还没有登录的。我们现在希望的是点击登录这个按钮,登录对应的模态框就能显示出来,而不是点击完还要等到它加载,这无形这种就可以使得页面加载速度变快了许多。
那这该怎么处理呢?这就要谈到 webpack
中的 preloading
和 prefetching
了。
(2)preloading和prefetching
假设我们现在要引入一个 click.js
文件,那么我们可以这么处理。具体代码如下:
document.addEventListener('click', () => { // 这句话的意思为:当主要的js文件都加载完成之后,之后就是网络带宽有空闲的时候,它就会偷偷的把 ./click.js 文件给加载好 import(/* webpackPrefetch: true */ './click.js').then(({default: func}) => { func(); }) }); 复制代码
通过以上代码我们可以知道,在所引入的文件前面,即 ./click.js
前面,加入 /* webpackPrefetch: true */
即可达到我们想要的效果。这句话的意思为,当主要的 js
文件都加载完成之后,这时就是网络带宽有空闲的时候,那么 webpack
机会偷偷的在这个时间段,把 ./click.js
文件给加载好。
这里说到的是 prefetch
,但其实我们也可以把 webpackPrefetch
改成 webpackPreload
, preload
与 prefetch
的区别在于: preload
是跟主文件同时进行加载,而不是在主文件加载完才加载的。一般来说,我们都用 prefetch
, 只有等主文件把活干完了,再来加载剩余的我们想要的文件,这样的逻辑和对页面的优化才是比较完美的。
综上,原先我们打包像 jQuery
、 lodash
之类的库时,只需要在第一次访问的时候加载,而等到第二次访问的时候,我们就可以借助缓存,提高访问的速度。但这种方式也只是提高第二次访问的速度,而我们想要实现的是,当第一次访问的时候, webpack
就可以使得页面的加载速度是最快的。
所以最终我们使用了 preload
和 prefetch
这两种实现方式来解决这个问题。
🏏五、CSS文件的代码分割
1. css文件代码分割
上面我们讲到了 js
文件如何进行代码分割,现在,我们来讲 css
文件,如何进行代码分割。对于 css
文件的代码分割来说,我们要引用到官方文档中提到的一个插件: MiniCssExtractPlugin
。接下来我们来看一下这个插件如何使用。
第一步: 安装插件。具体命令如下:
npm install --save-dev mini-css-extract-plugin 复制代码
第二步: 在开发环境和线上环境中使用。我们打开 webpack.common.js
文件,引入该插件并使用。具体代码如下:
const MiniCssExtractPlugin = require("mini-css-extract-plugin"); module.exports = { entry:{ main: './src/index.js' }, module:{ rules:[{ test:/\.scss$/, use:[ MiniCssExtractPlugin.loader, { loader: 'css-loader', options: { //表明前面要先走sass-loader和postcss-loader importLoaders: 2, modules: true } }, 'sass-loader', 'postcss-loader' ] },{ test:/\.css$/, use:[ MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader' ] }] }, plugins: [ new MiniCssExtractPlugin({ // 如果文件被直接引用,走filename filename: '[name].css', // 如果文件被间接的引用,那么走chunkFilename chunkFilename: '[name].chunk.js' }) ], optimization: { //使用treeShaking usedExports: true, splitChunks: { chunks: 'all', cacheGroups: { vendors: false, default: false } } } } 复制代码
第三步: 配置 package.json
文件。具体代码如下:
{ "sideEffects": [ //表示不对css文件开启treeShaking "*.css" ] } 复制代码
2. 压缩css文件
对于打包后的css文件,其大小还是比较大的,所以我们需要对文件大小进行压缩。这该怎么处理呢?
**第一步:**安装插件。具体命令如下:
npm install optimize-css-assets-webpack-plugin -D 复制代码
第二步: 在开发环境和线上环境中使用。我们打开 webpack.common.js
文件,引入该插件并使用。具体代码如下:
const CssMinimizerPlugin = require("optimize-css-assets-webpack-plugin"); module.exports = { optimization: { minimizer: [new OptimizeCSSAssetsPlugin({})] } } 复制代码
3. 合并打包css文件
有时候,我们可能会有很多个入口文件,每个入口文件又都有其对应的若干个 css
文件。那么这个时候我们想把所有的 css
文件,都打包到同一个文件下,该怎么处理呢?
我们需要在 webpack.common.js
进行配置,具体代码如下:
module.exports = { optimization: { splitChunks: { cacheGroups: { styles: { name: "styles", test: /\.css$/, chunks: "all", enforce: true, }, }, }, } } 复制代码
通过以上代码,我们可以了解到,我们需要在 splitChunks
中额外配置一个 styles
的 cacheGroups
,将所有的 css
文件打包到一个 命名为 styles
的文件夹下。
🏑六、webpack与浏览器缓存
1. 浏览器缓存配置
当我们刚访问网站时,第一次加载总是需要从零开始加载各种文件的,而假设我们的代码没有更新时,我们希望再次加载时可以从浏览器中直接拉取缓存,而不是重新进行加载。而等到我们的代码发生更新时,再重新加载网页。那这该怎么处理呢?
第一步: 配置开发环境 webpack.dev.js
文件。具体代码如下:
const devConfig = { output: { filename: '[name].js', chunkFilename: '[name].chunk.js', } } 复制代码
第二步: 配置生产环境 webpack.prod.js
文件。具体代码如下:
const prodConfig = { output: { filename: '[name].[contenthash].js', chunkFilename: '[name].[contenthash].js', } } 复制代码
以上的代码旨在解决的问题是,当在线上环境时,对输出的文件加一个哈希值。这个时候,如果我们的代码有发生改变时,webpack就会生成一个新的哈希值,网页就会进行更新。如果我们的代码没有发生改变,那么这个哈希值还是一样的不会改变,网页就会从浏览器中拉取内存信息,进行加载。
2. 解决旧版本问题
以上内容呢,如果发生在一些比较低的 webpack
版本,是不能生效的。所以我们需要进行一个配置,来兼容低版本问题。我们在 webpack.common.js
下进行配置,具体代码如下:
module.exports = { optimazation: { runtimeChunk: { name: 'runtime' } } } 复制代码
⚾七、Shimming的作用
1. Shimming垫片
继续,现在我们来了解一下在 webpack
中, shimming
,即垫片的概念。
在 webpack
的打包过程中呢,我们往往要做一些代码上或者打包过程上的兼容。
比如,对于两个 js
文件来说,模块和模块之间是相互独立的,他们之间是没有耦合度的。假设我们现在有两个文件,具体代码如下:
jquery.ui.js:
export function ui(){ $('body').css('background', 'red'); } 复制代码
index.js:
import _ from 'lodash'; import $ from 'jquery'; import { ui } from './jquery.ui'; ui(); const dom = $('<div>'); dom.html(_.join(['Mondady', 'Tuesday']), '~'); $('body').append(dom); 复制代码
大家可以看到,现在我们想要在 index.ui.js
文件中引入 $
,但是在这个文件中,它并没有引入 jquery
这个库。所以,如果就这样子直接运行,是肯定的会报错的。
因此,如果我们想要解决这个问题,该怎么进行配置呢?
下面我们将对 webpack.common.js
进行配置。具体代码如下:
const webpack = require('webpack'); module.exports = { plugins: [ new webpack.ProvidePlugin({ $: 'jquery', //'_': 'lodash', //_join: ['lodash', 'join'] }) ] } 复制代码
所以,这间接的可以理解为,拿点东西把 $
垫上,也就有了我们标题说的 shimming
,垫片的概念。
2. this指向
对于项目中的文件来说,其 this
的指向都是指向模块本身,而不是指向全局。那我们如果想让项目中的所有 js
文件都指向全局,该怎么处理呢?
第一步: 安装 loader
。具体命令如下:
npm install imports-loader --save-dev 复制代码
第二步: 配置 webpack.common.js
。具体代码如下:
module.exports = { module: { rules: [{ test: /\.m?js$/, exclude: /node_modules/, use: [{ loader: "babel-loader", },{ loader: 'imports-loader?this=>window' }] }] } } 复制代码
通过配置 imports-loader
以后, webpack
表明,它将会把 js
文件中的 this
指向全部指向 window
全局中。
🎖️八、结束语
在今天的文章中,我们学习了使用 Tree Shaking
来优化代码的打包大小,同时,学习了 Dev
和 Prod
环境下的区分打包,需要明确在这两个模式下各自相同的配置以及不同的配置,分清楚他们之间的关系。
除此之外呢,我们还学会了使用 webpack
来对 js
文件和 css
文件进行代码分割。以及使用 webpack
来对代码进行打包分析和提前加载 preloading
。
最后,我们还学会了关于 webpack
如何开启浏览器缓存,以及垫片 Shimming
的作用。
关于 webpack
的基础特性和高级特性讲到这里就结束啦!希望对大家有帮助~
如有疑问或文章有误欢迎小伙伴们评论区留言呀~💬