《九》Webpack开发环境调优

徐丰茂
2023-12-01

Webpack开发效率插件:

webpack-dashboard:

Webpack每一次构建结束后都会在控制台输出一些打包相关的信息,但是这些信息是以列表的形式展示的,有时会显得不够直观。webpack-dashboard就是用来更好地展示这些信息。

webpack-dashboard的控制台分为几个面板来展示不同方面的信息。比如:左上角的Log面板就是Webpack本身的日志;下面的Modules面板则是此次参与打包的模块,从中可以看出哪些模块资源占用比较多;而从右下方的Problems面板中可以看到构建过程中的警告和错误等。

  1. 安装命令:npm install webpack-dashboard
  2. 需要把webpack-dashboard作为插件添加到webpack配置中:
    const DashboardPlugin = require('webpack-dashboard/plugin');
    module.exports = {
    	entry: './app.js',
    	output: {
    		filename: '[name].js'
    	},
    	mode: 'development',
    	plugins: [
    		new DashboardPlugin ();
    	]
    }
    
  3. 为了使webpack-dashboard生效,还要更改一下webpack的启动方式,就是用户webpack-hashboard模块命令替代原本的webpack或者webpack-dev-server的命令,并将原有的启动命令作为参数传给它。
    // package.json
    //  假设原本的启动命令如下:
    {
    	"scripts": {
    		"dev": "webpack-dev-server"
    	}
    }
    //加上webpack-dashboard后则变为:
    {
    	"scripts": {
    		"dev": "webpack-dashboard -- webpack-dev-server"
    	}
    }
    
webpack-merge:

对于需要配置多种打包环境的项目来说,webpack-merge是一个非常实用的工具。

假设项目有3种不同的配置,分别对应本地环境、测试环境和生产环境。每一个环境对应的配置都不同,但也有一些公共的部分,那么就可以将这些公共的部分提取出来。

假设创建一个webpack.common.js来存放所有这些配置。

//webpack.common.js
module.exports = {
	entry: './app.js',
	output: {
		filename: '[name].js'
	},
	module: {
		rules: [
			{
				test: /\.(png | jpg | gif)$/,
				use: 'file-loader'
			},
			{
				test: /\.css$/,
				use: [
					'style-loader',
					'css-loader'
				]
			}
		]
	}
}

每一个环境又都有一个相应的配置文件。如:对于生产环境可以专门创建一个webpack.pros.js,假如不借助任何工具从webpack.commmon.js引入公共配置:

//webpack.prod.js
const commonConfig = require('./webpack.common.js');
module.exports = Object.assign(commonConfig ,{
	mode: 'production'
})

这样看起来很简单,但问题是,假如想修改一下CSS的打包规则,例如用extract-tet-webpack-plugin将样式单独打包出来,这时就需要添加一些代码:

//webpack.prod.js
const commonConfig = require('./webpack.common.js');
const EctractTextPlugin = require('extract-tet-webpack-plugin');
module.exports = Object.assign(commonConfig ,{
	mode: 'production',
	module:{
		rules: [
			{
				test: /\.(png | jpg | gif)$/,
				use: 'file-loader'
			},
			{
				test: /\.css$/,
				use: EctractTextPlugin .extract({
					fallback: 'style-loader',
					use: 'css-loader'
				})
			}
		]
	}
})

一下子感觉冗余了,这是因为通过Object.assign没有办法准确找到CSS的规则并进行替换,所以必须替换掉整个module的配置。可以用webpack-merge来解决这个问题。

  1. 安装命令:npm install webpack-merge
  2. 更改webpack.prod.js:用merge.smart替换了Object.assign,它在合并module.rules的过程中会以test属性作为标识符,当发现有相同项出现的时候会以后面饿规则覆盖前面的规则,这样就不必添加冗余代码了。
    //webpack.prod.js
    const merge = require('webpack-merge');
    const commonConfig = require('./webpack.common.js');
    const EctractTextPlugin = require('extract-tet-webpack-plugin');
    module.exports = merge.smart(commonConfig ,{
    	mode: 'production',
    	module:{
    		rules: [
    			{
    				test: /\.css$/,
    				use: EctractTextPlugin .extract({
    					fallback: 'style-loader',
    					use: 'css-loader'
    				})
    			}
    		]
    	}
    })
    
speed-measure-webpack-plugin(SMP):

SMP可以分析出Webpack整个打包过程中在各个loader和plugin上耗费的时间,这有助于找出构建过程中的瓶颈,分析出哪些构建步骤耗时较长,以便于优化和反复测试。

  1. 安装命令:npm install speed-measure-webpack-plugin
  2. 只要用SMP的wrap方法包裹在Webpack的配置对象外面即可。
    //webpack.config.js
    const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
    const smp = new SpeedMeasurePlugin();
    module.exports = smp.wrap({
    	entry: './app.js'
    })
    
size-plugin:

一般而言,随着项目的开发,产出的资源会越来越大,最终生成的资源会逐渐变得臃肿起来。size-plugin这个插件可以帮助我们监控资源体积的变化,尽早地发现问题。在每次执行Webpack打包命令后,size-plugin都会输出本次构建的资源体积,以及与上次构建相比体积变化了多少。

  1. 安装命令:npm install size-plugin
  2. 修改webpack.config.js的配置:
    const path = rquire('path');
    const SizePlugin = require('size-plugin');
    module.exports = {
    	entry: './app.js',
    	output: {
    		path: path.join(__dirname,'dist');
    		filename: '[name].js'
    	},
    	mode: 'production',
    	plugins: [
    		new SizePlugin();
    	]
    }
    

模块热替换:

在早期开发工具还比较简单和匮乏的年代,调试代码的方法基本都是改代码–>刷新网页查看结果–>再改代码,这样反复地修改和测试;后来,一些Web开发框架和工具提供了更便捷的方式,只要检测到代码改动就会自动重新构建,然后触发网页刷新,这种一般被称为live reload;Webpack则在live reload的基础上又进了一步,可以让代码在网页不刷新的前提下得到最新的改动,甚至不需要重新发起请求就能看到更新后的效果,这就是模块热替换。

HMR对于大型应用于尤其适用。试想一个复杂的系统每改动一个地方都要经历资源重构建、网络请求、浏览器渲染等过程,怎么也要几秒甚至几十秒的时间才能完成;况且调试的页面可能位于很深的层级,每次还要通过一些人为操作才能验证结果,其效率是非常低下的。而HMR则可以在保留页面当前状态的前提下呈现出最新的改动,可以节省开发者大量的时间成本。

开启HMR:

HMR是需要手动开启的,并且有一些必要条件:首先要确保项目是基于webpack-dev-server或者webpack-dev-middle进行开发的,Webpack本身的命令行并不支持HMR。

下面是一个使用webpack-dev-server开启HMR的例子。

const webpack = require('webpack');
module.exports = {
	plugins: [
		new webpack.HotModuleReplacementPlugin()
	],
	devServer:{
		hot: true
	}
}

上面配置产生的结果是Webpack会为每个模板绑定一个module.hot对象,这个对象包含了HMR的API。调用HMR API有两种方式:一种是手动地添加这部分代码;另一种是借助一些现成的工具,比如react-hot-loader、vue-loader等。

  1. 如果应用的逻辑比较简单,可以直接手动添加代码来开启HMR。比如下面的例子:
    // index.js
    import {add} from 'util.js';
    add(2,3);
    
    if(module.hot){
    	module.hot.accept();
    }
    
    假设Index.js是应用的入口,那么我们就可以把调用HMR API的代码放在该入口中,这样HMR对于index.js和其依赖的所有模块都会生效。当发现有模块发生变动时,HMR会使应用在当前浏览器环境下重新执行一遍Index.js,包括其依赖的内容,但是页面本身不会刷新。
  2. 大多数时候,还是建议使用第三方提供的HMR解决方案,因为HMR触发过程中可能会有很多预想不到的问题,导致模块更新后应用的表现和正常加载的表现不一致。为了解决这类问题,Webpakc社区中已经有许多相应的工具提供了解决方案:比如react组件的热更新由react-hot-loader来处理,直接拿来用就行。
HMR原理:

在开启HMR的状态下进行开发,会发现资源的体积会比原来的大很多,这是因为Webpack为了实现HMR而注入了很多相关代码。

在本地开发环境下,浏览器是客户端,webpack-dev-server(WDS)相当于是服务端。HMR的核心就是客户端从服务端拉取更新后的资源(准确地说,HMR拉取的不是整个资源文件,而是chunk diff,即chunk需要更新的部分)。

第一步就是浏览器什么时候去拉取这些更新,这需要WDS对本地源文件进行监听。实际上WDS与浏览器之间维护了一个websocket,当本地资源发生变化时WDS会向浏览器推送更新事件,并带上这次构建的hash,让客户端与上一次资源进行比对。通过hash的比对可以防止冗余更新的出现,因为很多时候源文件的更改并不一定代表构建结果的更改(比如添加了一个文件末尾空行等)。

现在客户端已经获取到了chunk的更新,到这里又遇到了一个非常重要的问题,即客户端获取到这些增量更新之后如何处理?哪些状态需要保留?哪些状态又需要更新?这些就不属于Webpack的工作了,但是它提供了相关的API,开发者可以使用这些API针对自身场景进行处理,像react-hot-loader和vue-loader也都是借助这些API来实现的HMR。

HMR API示例:
// index.js
import {logToScreen} from './util.js'
let counter = 0;
console.log('setInterval starts');
setInterval(()=>{
	counter+=1;
	logToScreen(counter);
},1000)

//util.js
export function logToScreen(content){
	documnet.body.innerHTML = 'content:${content}';
}

这个例子实现的是在屏幕上输出一个整数并没秒加1。现在以最简单的方式对它添加HMR:

if(module.hot){
	module.hot.accept();
}

这段代码的意思是让Index.js及其依赖只要发生改变就在当前环境下全部重新执行一遍。但是发现它会带来一个问题:在当前的运行时已经有了一个setInterval,而每次HMR过后又会添加新的setInterval,并没有对之前的进行清除,所以最后会看到屏幕上有不用的数字闪来闪去。

为了避免这个问题,可以让HMR不对Index.js生效。也就是说,当Index.js发生改变时,直接让整个页面刷新,以防止逻辑出现问题,但对于其他模块来说还想让HMR继续生效,那么可以将上面的代码修改如下:

if(module.hot){
	module.hot.decline();
	module.hot.accept(['./util.js']);
}

module.hot.decline()是将当前Index.js的HMR关掉,当Index.js自身发生改变时禁止使用HMR进行更新,只能刷新整个页面。module.hot.accept([’./util.js’])的意思是当util.js改变时依然可以启用HMR更新。

 类似资料: