Webpack每一次构建结束后都会在控制台输出一些打包相关的信息,但是这些信息是以列表的形式展示的,有时会显得不够直观。webpack-dashboard就是用来更好地展示这些信息。
webpack-dashboard的控制台分为几个面板来展示不同方面的信息。比如:左上角的Log面板就是Webpack本身的日志;下面的Modules面板则是此次参与打包的模块,从中可以看出哪些模块资源占用比较多;而从右下方的Problems面板中可以看到构建过程中的警告和错误等。
npm install webpack-dashboard
;const DashboardPlugin = require('webpack-dashboard/plugin');
module.exports = {
entry: './app.js',
output: {
filename: '[name].js'
},
mode: 'development',
plugins: [
new DashboardPlugin ();
]
}
// package.json
// 假设原本的启动命令如下:
{
"scripts": {
"dev": "webpack-dev-server"
}
}
//加上webpack-dashboard后则变为:
{
"scripts": {
"dev": "webpack-dashboard -- webpack-dev-server"
}
}
对于需要配置多种打包环境的项目来说,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来解决这个问题。
npm install webpack-merge
;//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'
})
}
]
}
})
SMP可以分析出Webpack整个打包过程中在各个loader和plugin上耗费的时间,这有助于找出构建过程中的瓶颈,分析出哪些构建步骤耗时较长,以便于优化和反复测试。
npm install speed-measure-webpack-plugin
;//webpack.config.js
const SpeedMeasurePlugin = require('speed-measure-webpack-plugin');
const smp = new SpeedMeasurePlugin();
module.exports = smp.wrap({
entry: './app.js'
})
一般而言,随着项目的开发,产出的资源会越来越大,最终生成的资源会逐渐变得臃肿起来。size-plugin这个插件可以帮助我们监控资源体积的变化,尽早地发现问题。在每次执行Webpack打包命令后,size-plugin都会输出本次构建的资源体积,以及与上次构建相比体积变化了多少。
npm install size-plugin
;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是需要手动开启的,并且有一些必要条件:首先要确保项目是基于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等。
// 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,包括其依赖的内容,但是页面本身不会刷新。在开启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。
// 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更新。