Webpack 自动刷新的问题
在此之前呢我们已经简单了解了webpack-dev-server的一些基本用法和特性,那他主要就是为我们使用webpack构建的项目提供了一个比较友好的开发环境,和一个可以用来调试的开发服务器。那使用webpack-dev-server就可以让我们的开发过程更加专注于编码。
因为他可以监视到我们代码变化,然后自动进行打包,最后再通过自动刷新的方式,同步到浏览器,以便于我们可以及时的预览。
但是当你实际去使用这样的特性去弯沉具体的开发任务时那你会发现,这里还是会有一些不舒服的地方。
那例如这里是一个编辑器的应用,我想要在这里能够及时去调试编辑器中文本内容的样式,正常的操作肯定是先尝试在编辑器中添加一些文本作为展示样例。
然后我们回到开发工具当中找到控制这个编辑器样式的css文件。在这个编辑器样式文件中我们简单添加一些样式。那这个时候我们就能发现问题了,当我们修改完样式过后呢,原本想着可以及时看到最新的界面效果,但是这个时候我们编辑器中给的内容确没有了,那这里我们不得不再来编辑器中再去添加一些文本,如果说此时你对我们的样式还是不满意的话,那我们还需要继续来去调整样式,而且调整完了过后又会面临刚刚文本内容丢失这样一个问题。
那久而久之的话你就会发现,自动刷新这样一个功能还是很鸡肋,他并没有我们想象中那么好用。那这是因为我们每次修改完代码,webpack监视到文件变化就会自动打包,然后自动刷新到浏览器。
那一旦页面整体刷新,那页面中之前的任何操作状态都会丢失,所以说就会出现,刚刚我们所看到的这样一个情况。但是呢,聪明的人一般都会有一些小办法,例如我们可以在代码当中先去写死一个文本到我们的编辑器当中。那这样的话,即便是我们页面刷新也不会有丢失的这种情况出现。
那又或是我们通过一些额外的代码,把我们的内容先保存到临时存储中,然后刷新过后我们再去取回来。总之就是你有问题,我有办法。
那确实这些都是好办法,但是又都不是特别的好,因为这些都是典型的有洞补洞的操作,并不能根治我们页面刷新过后导致的页面数据丢失的这样一个问题。
而且这些方法都需要我们去编写一些跟我们业务本身无关的一些代码,那更好的办法自然是能够在页面不刷新的这种情况下我们代码也可以及时的更新进去。
那针对于这样的需求,webpack同样也可以满足,那接下来我们就一起去了解一下webpack当中如果去在页面不刷新的情况下及时的去更新我们的代码模块。
Webpack HMR 体验
HRM全称是Hot Module Replacement那翻译过来叫做,模块热替换或者叫做模块热更新。
那计算机行业我们经常听到一个叫做热拔插的名词,指的就是我们可以在一个正在运行的机器上随时去插拔设备,而我们机器的运行状态,是不会受插拔设备的影响。而且我们插上的设备可以立即开始工作,例如我们在电脑设备上的USB端口就是可以热拔插的。
那模块热替换的这个热,和我们刚刚提到的热拔插实际上是一个道理,他们都是在运行过程中的即时变化,那webpack中的模块热替换指的就是可以在应用程序运行的过程中实时的去替换掉我们应用中的某个模块。而我们应用的运行状态不会因此而改变。
例如我们在应用程序的运行过程中,我们修改了某个模块,那通过自动刷新就会导致我们应用整体的刷新,那应用中的状态信息呢,都会丢失掉。
而如果我们这个地方使用的是热替换的话,我们就可以实现,只将刚刚修改的这个模块,实时的去替换到应用当中,不必去完全刷新应用。
那这里我们可以先来对比一下使用热更新和使用自动刷新这两种方式的体验差异。
那屏幕显示的这个项目我们已经开启了HMR这个特性,那这里我们同样先在我们的页面当中随意去添加一些内容,也就是为我们的页面去制造一些运行的状态。
然后我们回到开发工具当中,这里我们尝试去修改我们文本的样式,我们先将它的颜色修改为红色,那保存过后呢,我们就可以立即看到新的样式结果。而我们的页面并没有整体的刷新。
那这种体验是非常友好的。对于项目中其他代码文件的修改,也可以有相同的热替换的这样一种体验。那我们这里再来尝试修改一下js文件。我们随意去修改一行js代码,然后保存。那此时呢我们浏览器当中,也没有刷新页面,而是直接执行了刚刚我们修改的这个模块。
那不仅如此,对于项目当中那些非文本文件,同样也可以使用热更新,那例如我们所显示的背景图片,我们通过简单的画图板来去修改一下这个图片。保存过后呢,我们浏览器当中同样也可以及时更新过来我们最新的这张图片,而我们整个应用的运行状态呢,也没有因此而发生变化。
那这就是HMR的作用和他的一个体验。
HMR可以算是webpack中最强大的特性之一,同时他也是最受欢迎的特性。因为他确实极大程度的提高了开发者的工作效率,所以说我们接下来要重点来去看,如何去使用HMR
Webpack 开启 HMR
热更新这么强大的功能而言,他的使用并不算特别的复杂,接下来我们就一起来了解下具体如何去使用HMR。
HMR已经集成在了webpack-dev-server工具当中,所以说我们就不需要再去单独安装什么模块了。
那使用这个特性呢,我们需要去运行webpack-dev-server这个命令时通过–hot这样一个参数去开启这样一个特性,或者也可以在配置文件当中去添加对应的配置来去开启这样一个功能。
我们打开配置文件。这里我们需要配置的地方有两个,第一个我们要将dev-server中的hot属性设置为true。
然后我们需要载入一个插件,那这个插件是webpack内置的一个插件,所以我们这里先去导入webpack模块,那有了这个模块过后呢,我们这里使用的是这个模块当中的hot-module-replacement-plugin这样一个插件。
我们把这个插件配置进来。
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development'
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
// publicPath: 'dist/'
},
devtool: 'source-map',
devServer: {
hot: true
}
module: {
rules: [
{
test: /.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /.png$/,
use: 'file-loader'
}
]
},
plugins: [
new HtmlWebpackPlugin({
title: 'Webpack Plugin Sample',
template: './src/index.html'
}),
new webpack.HotModuleReplacementPlugin()
]
}
配置完成过后我们打开命令行终端,因为我们已经开启了HMR,所以我们这里直接去运行webpack-dev-server 然后去启动weboack开发的服务器。
yarn webpack-dev-server
接下来我们就可以在浏览器去体验HMR带来的优势了,我们回到开发工具当中,这里我们先尝试修改一下样式文件,那保存过后这个样式模块就可以以热更新的方式直接去作用到我们的页面当中了。
然后呢我们再来尝试修改一下js模块文件,我们在js中随便console.log一句话,此时我们就会发现,这里的页面却自动刷新了。并没有刚刚我们所看到的那种热更新的体验。
我们再在页面的编辑器当中去添加一些文字,这样的话我们可以更容易去确认是否有刷新,完成过后我们再回到开发工具当中。修改文件,我们发现页面真的刷新了。
我们可以发现样式文件有变动可以实现热更新,但是js文件的修改就失去了作用,那具体我们该如何去实现所有模块资源的热替换,我们接着往下看。
Webpack HMR 的疑问
我们发现了模块热替换确实提供了非常友好的开发体验,但是当我们自己尝试开启HMR过后呢,我们发现效果确不尽如人意,那这里是因为HMR他并不像webpack其他的特性一样可以开箱即用。
也就是说我们的HMR还需要我们做一些额外的操作才能够可以正常工作,那webpack中的HMR需要我们手动通过代码去处理当模块更新过后我们需要如何把更新过后的模块去替换到我们运行的页面当中。
那可能会有人问,为什么当我开启了HMR过后我们的样式文件就可以直接去热更新,我们好像也没有手动的去处理样式模块的更新。
这是因为样式文件是经过loader处理的,在style-loader里面就已经自动处理了样式文件的热更新,所以说我们就不需要我们自己去额外做手动的操作。
可能你会想,凭什么样式文件就可以自动处理,而我的脚本文件就需要我们自己手动处理呢,这个原因也很简单,因为样式模块更新过后呢,他只需要把更新过后的css及时的替换到页面当中,他就可以覆盖掉之前的样式,从而实现样式文件的更新。
而我们所编写的javaSciript的模块他是没有任何规律的,因为你可能在一个模块中导出的是一个对象,也有可能是一个字符串,还有可能导出的是一个函数,那我们对导出的这个成员我们的使用也是各不相同的。
所以说webpack面对这些毫无规律的js模块,他就根本不知道,如何去处理更新过后的模块,那也就没有办法帮你实现一个通用所有模块的替换方案。
这就是为什么样式文件可以直接热更新,而js文件更新过后我们页面还是自动刷新的原因。
那可能有一些使用过vue-cli或者是create-react-app的一些脚手架工具的人来说,他会觉得,我的项目当中并没有手动的去处理我们js模块的更新,我的代码照样可以做热替换,没有我们刚刚说的这么麻烦。
那这是因为你使用的是框架,那我使用框架开发时,我们项目中每个文件他自然就有了规律,因为框架他提供的就是一些规则,例如我们再react模块中要求每一个模块必须要去导出一个函数,或者是导出一个类。
那有了这样一个规律那就可能会有通用的替换办法,例如每一个文件导出的都是一个函数的话那他就自动的把这个函数再拿回来再去执行一下。
而且这些工具内部都已经帮你提供好了这种通用的HMR替换模块,所以说我们就不需要自己手动处理了,如果你之前没有接触过这样的工具,也无所谓,你可以忽略掉这一条。这并不影响我们后面的理解。
综上所述,我们还需要自己手动处理当JS模块更新过后我们需要去做的事情。
Webpack 使用 HMR API
Hot-module-replacement-plugin为我们js提供了一套用于去处理HMR的api,我们需要在自己的代码中去使用这套api来去处理当某一个模块更新过后应该如何替换掉当前正在运行的页面当中。
接下来我们一起回到代码当中,尝试通过HMR的api来去手动处理模块更新过后的热替换,我们打开js。
这是我们打包的入口文件。也就是在这个文件当中才开始去加载其他的模块,那就是因为这个模块当中使用了这些导入的模块,那一但当这些模块更新了过后,我们就必须要去重新使用这些模块。
所以说我们要在这个模块中去处理他所依赖的这些模块更新过后的热替换。
在这套API当中,他为我们的module对象提供了一个hot属性,那这个属性也是一个对象,他就是我们HMR API的核心对象,那他提供了一个accept方法,用于去注册,当我们某一个模块更新过后的处理函数。
那这个方法的第一个参数接收的就是我们依赖模块的路径,第二个参数就是依赖路径更新过后的处理函数。
我们这里先来尝试注册一下当我们的editor模块更新过后的处理函数。那这里第一个参数就是editor模块的路径,然后第二个参数呢,我们传入一个函数。然后在这个函数中我们去打印一个消息,表示一个editor模块更新了。
import createEditor from './editor';
import background from './better.png';
import './global.css';
const editor = createEditor();
document.body.appendChild(editor);
const img = new Image();
img.src = background;
document.body.appendChild(img);
module.hot.accept('./editor', () => {
console.log('editor 模块更新了,需要这里手动处理');
});
完成过后我们打开命令行启动webpak-dev-server回到浏览器打开开发人员工具。
那这个时候我们就可以尝试回到开发工具中去修改editor这个模块中的代码。那此时浏览器的控制台中就会打印我们刚刚所打印的那个消息,而且也就不会再去触发自动刷新了。
那也就是说一旦这个模块的更新被我们这样手动的处理了,那他就不会去触发自动刷新,反之如果我们没有手动处理这个模块的热替换。那HMR就会自动forback到自动刷新,从而导致我们页面刷新。
Webpack 处理 JS 模块热替换
那了解了这个API的作用过后我们就需要去考虑,具体该怎样实现editor模块这个热替换的逻辑,那这个模块导出的是一个函数,我们这里先直接打印到控制台当中,然后在模块更新过后我们再去打印一次。
import createEditor from './editor';
import background from './better.png';
import './global.css';
const editor = createEditor();
document.body.appendChild(editor);
const img = new Image();
img.src = background;
document.body.appendChild(img);
console.log(createEditor);
module.hot.accept('./editor', () => {
// console.log('editor 模块更新了,需要这里手动处理');
console.log(createEditor);
});
那这个时候我们回到我们的editor.js当中,我们尝试去修改这个模块,然后保存,此时你会发现,当我们的模块更新过后我们这里拿到的函数也就更新为了最新的结果。
那知道这样一个特点就好办了,因为我们这里是使用了这个函数去创建一个界面的元素,那一但当这个函数更新了,我们界面上这个元素也应该被重新创建。所以说我们这里先直接去移除原来的元素,然后我们再去调用createEditor这个函数去创建一个新的元素,然后追加到页面中。
那这样的话就相当我们的界面重新工作了。
并且我们这里还需要去记录下来我们新创建的这个元素,把他放在变量当中,否则的话我们下一次热替换的时候就找不到这一次所创建的这个元素了。
import createEditor from './editor';
import background from './better.png';
import './global.css';
const editor = createEditor();
document.body.appendChild(editor);
const img = new Image();
img.src = background;
document.body.appendChild(img);
let lastEditor = editor;
module.hot.accept('./editor', () => {
// console.log('editor 模块更新了,需要这里手动处理');
// console.log(createEditor);
document.body.removeChild(lastEditor);
const newEditor = createEditor();
document.body.appendChild(newEditor);
lastEditor = newEditor;
});
完成以后我们再来尝试修改editor模块。保存过后我们回到浏览器当中来查看一下这里呢,界面当中的元素确实立即更新为了我们最新的结果。
我们再尝试在界面上输入一些内容,再修改editor模块,保存,此时你就会发现和我们之前同样的问题。
那由于热替换时我们把界面上之前的编辑器元素已经移除掉了,那我们之前所输入的状态自然也就丢失掉了,然后替换为了一个新的元素,所以说我们界面上的这些状态都会丢失。
这也就证明我们的热替换操作还需要去改进,那我们必须在要替换原来的元素之前先把他的状态保留下来。
那想要保留我们编辑器中的状态也非常简单,我们就是把编辑器当中之前的内容给他存下来,然后在替换过后我们再把它放回去就好了。
因为这里我们用的是一个可编辑元素,并不是一个文本框,所以我需要通过innerHTML拿到我们之前所添加的内容,然后我们在创建新元素过后再把它设置到新元素当中,那这样的话就可以解决我们这个文本框状态的丢失问题。
let lastEditor = editor;
module.hot.accept('./editor', () => {
// console.log('editor 模块更新了,需要这里手动处理');
// console.log(createEditor);
const value = lastEditor.innerHTML;
document.body.removeChild(lastEditor);
const newEditor = createEditor();
newEditor.innerHTML = value;
document.body.appendChild(newEditor);
lastEditor = newEditor;
});
我们再次回到浏览器输入一些内容,再来修改editor模块,此时这个模块就可以以热替换的方式工作,而不用担心我们状态丢失问题,因为我们模块重新工作之后我们已经把上一次的状态记录下来。
那这就是我们针对于js模块热替换的一个处理过程,注意这不是一个通用的方式,这只适用于我们当前的editor.js模块。
那通过这样一个过程我们就应该能够发现为什么webpack的HMR需要我们自己去处理js模块的热更新,因为不同的模块有不同的逻辑,不同的业务逻辑又导致我们在这他的处理过程肯定也是不同的。
那我们这里是一个文本编辑器,所以我们需要去保留状态,那如果说这里不是这种类型的,那就不需要这么做。
所以说webpack根本没有办法去提供一个通用的替换方案。
Webpack 处理图片模块热替换
图片模块的热替换逻辑就会简单的多,我们快速来看一下。
我们同样通过module.hot.accept这个方法去注册一下这个图片模块的热替换处理函数,那在这个函数当中我们只需要将图片元素的src设置为新的图片路径就可以了。
因为在图片修改过后我们的图片文件名呢是会发生变化的,而我们这里拿到的就是更新之后的文件名。所以说我们直接重新设置图片元素的src就可以实现图片的热替换。
那以上就是我们针对两种不同类型资源的热替换的处理过程,可能你会觉得比较麻烦因为我们需要去写一些额外的代码,甚至有人觉得我们不如不用,那我个人的想法是利大于弊。
这个道理就像是为什么现在的开发者都愿意去写一些单元测试一样,对于一个长期开发的项目,这一点额外的工作并不算什么,而且如果说你能为自己的代码设计一些规律的话,那你也可以去实现一些通用的替换方案。
那当然如果说你使用的是框架去开发的话,那使用HMR将十分简单,因为大部分框架当中都有成熟的HMR方案,那你只需要去使用就可以了,但是我们这里使用的是纯原声的方式去做的开发。所以说HMR使用起来相对会麻烦一点,那这也正是为什么大部分人都喜欢选择集成式框架的原因,因为足够简单。
Webpack HMR 注意事项
刚开始去使用HMR肯定会遇到一些问题,下面我们来看一下最有可能发生的问题和大家容易产生疑惑的地方。
首先大家容易出问题的地方就是,如果说我们处理热替换的代码有错误,那就不容易发现,结果会导致页面自动刷新,而自动刷新过后,页面中的错误信息已经被清除了,这样一来我们就不容易发现到底是哪里出错了。
这种情况推荐大家使用hot only的方式来去解决,因为我们默认使用的hot方式如果说热替换失败,那他就自动会回退去使用自动刷新这样一个功能,而hot only他就不会去使用自动刷新。
配置文件当中,这里我们将dev-server当中的hot: true修改为hotOnly: true。
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development'
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
// publicPath: 'dist/'
},
devtool: 'source-map',
devServer: {
hotOnly: true
}
module: {
rules: [
{
test: /.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /.png$/,
use: 'file-loader'
}
]
},
plugins: [
new HtmlWebpackPlugin({
title: 'Webpack Plugin Sample',
template: './src/index.html'
}),
new webpack.HotModuleReplacementPlugin()
]
}
重启服务,此时再去修改代码,无论代码是否被处理了模块的热替换浏览器都不会去自动刷新了。
那这样的话我们这些错误信息就可以很容易的看到了。
第二个问题就是如果我们在代码中使用了HMR提供的API,但是我们在启动dev-server的时候没有开启HMR的选项,那此时我们再运行环境中就会报出一个accept undefined的错误。
原因是因为module.hot对象是HMR插件所提供的,我们没有开启这个插件,所以也就没有这个对象。
解决的办法也非常简单,就跟我们在业务代码中去判断API兼容一样,我们应该先去判断是否存在module.hot这个对象然后再去使用它。这样的话就可以解决我们这样的一个问题。
可能还会有一个疑问,那就是我们再我们的代码当中写了很多与业务功能本身无关的代码,那这会不会有影响。
那这个答案也很简单,我们通过一个简单的尝试来验证下,我们回到配置文件当中,那这里我们确保我们已经将热替换功能关闭了。并且我们已经移除了热替换的插件(plugin)。
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
mode: 'development'
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
// publicPath: 'dist/'
},
devtool: 'source-map',
devServer: {
// hotOnly: true
}
module: {
rules: [
{
test: /.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /.png$/,
use: 'file-loader'
}
]
},
plugins: [
new HtmlWebpackPlugin({
title: 'Webpack Plugin Sample',
template: './src/index.html'
}),
// new webpack.HotModuleReplacementPlugin()
]
}
然后我们打开命令行终端,运行一下webpack打包。
yarn webpack
打包完成过后我们找到生成的bundle.js文件,然后我们找到对应的那个模块,找到过后你就会发现其实我们代码当中编辑的那些处理热替换的代码都已经被移除掉了,只剩下一个if(false) {} 的空判断。
这种没有意义的判断在我们代码压缩过后也会自动去掉,所以说他根本不会影响我们生产环境当中的运行状态。
至此我们对HMR的一个使用基本上就完全了解了。
Webpack 生产环境优化
前面所了解到的一些用法和特性都是为了可以让我们在开发阶段拥有更好的开发体验,而这些体验提高的同事我们的打包结果也会随之变得越来越臃肿。
那这是因为在这个过程中webpack为了实现这些特性他会自动往打包结果中添加一些额外的内容,例如我们之前所使用到的source-map和HMR, 他们都会往输出结果中添加额外的代码来去实现各自的功能。
但是这些额外的代码对于生产环境来讲是冗余的,因为生产环境和开发环境是有很大的差异。
在生产环境中我们强调的是以更少量,更高效的代码去完成业务功能。也就是我们会更注重运行效率,而在开发环境中我们会只注重开发效率。
那针对于这个问题,webpack4+当中就推出了mode用法,那他为我们提供了不同模式下的一些预设的配置,那其中生产模式中就已经包括了很多我们在生产环境中所需要的优化配置。
那同时webpack也建议我们为不同的工作环境去创建不同的配置,以便于让我们的打包结果可以适用于不同的环境。
那接下来我们一起来探索一下生产环境中有哪些值得我们优化的地方,以及一些注意事项。
Webpack 不同环境下的配置
下面我们先来尝试为不同的工作环境去创建不同的webpack配置,那创建不同的环境配置的方式主要有两种。
第一种就是在我们的配置文件当中去添加相应的判断条件,然后根据环境的判断条件不同导出不同的配置。
第二种就是为我们不同的对应一个配置文件。那这种就确保我们每一个环境下面都会有一个对应的环境配置文件。
那我们分别来尝试下这两种方式下如何为我们开发环境和生产环境去创建不同的配置。
- 首先来看我们在配置文件当中来添加判断的这种方式。
我们回到配置文件当中,webpack的配置文件还支持导出一个函数,然后在这个函数当中去返回我们所需要的配置对象。
那这个函数可以接受到两个参数,第一个是env也就是我们通过cli传递的环境名参数
第二个是argv,那这个是指我们运行cli过程中所传递的所有参数,那我们就可以借助这样一个特点来去实现为我们的开发环境和生产环境去分别返回不同的配置。
我们先将这里的开发模式配置定义在config这样一个变量当中。
然后我们再去判断一下env是不是等于production,这里我们约定的生产环境的env就是production。
如果说是生产环境的话我们这里就将mode属性的字段设置为production,然后我们再将devtool设置为false,也就是禁用掉source-map,最后我们再来添加cleanWebpackPlugin和CopyWebpackPlugin这两个插件。
那这两个插件我们之前介绍的时候也说到了他实际上在开发阶段可以省略的插件,他是在上线打包之前才有他实际的价值。
这里我们使用的是ES6扩展运算符的方式把这两个插件和之前所有的插件放在一起去创建一个新的数组。
const webpack = require('webpack');
const {
CleanWebpackPlugin } = require('clean-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = (env, argv) => {
const config = {
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
publicPath: 'dist/'
},
devtool: 'cheap-eval-module-source-map',
devServer: {
hot: true,
contentBase: 'public'
},
module: {
rules: [
{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
{
test: /.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /.png$/,
use: 'url-loader'
},
{
test: /.html$/,
use: {
loader: 'html-loader',
options: {
attrs: ['img:src', 'a:href']
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
title: 'webpack',
template: './src/index.html'
}),
new webpack.HotModuleReplacementPlugin(),
]
}
if (env === 'production') {
config.mode = 'production';
config.devtool = false;
config.plugins = [
...config.plugins,
new CleanWebpackPlugin(),
new CopyWebpackPlugin(['public'])
]
}
return config;
}
完成以后我们打开命令行终端,我们先尝试直接去运行webpack
yarn webpack
此时我们并没有传递任何参数,这里我们的webpack就会以开发模式运行打包,打包完成过后我们可以展开dist目录,此时目录中并不会有public目录copy过来的文件。
然后我们再回到命令行,我们这里运行一下webpack --env production。那这个时候就相当于给webpack传递了一个env参数, 这个参数的值是production。
yarn webpack --env producton
那我们的配置文件接收到这样一个参数他就会返回生产模式下的配置,那也就意味着此时我们webpack会以生产模式运行打包。
那我们这些额外的插件也就会工作起来,这里我们就能看到public下的文件已经被copy到dist目录了。
那这就是我们通过在导出函数中对环境进行判断从而实现为不同的环境导出不同的配置,当然你也可以在全局去判断环境变量直接导出不同的配置,这样也是可以的。
Webpack 不同环境的配置文件
通过判断环境名参数去返回不同的配置对象这种方式只适用于中小型项目,因为一旦项目变得复杂那我们的配置文件也会一起变得复杂起来。
所以说对于大型的项目我们还是建议大家使用不同环境去对应不同配置文件的方式来实现,一般在这种方式下面我们项目当中至少会有三个webpack配置文件。
其中两个是用来适配不同的环境的,另外一个是一个公共的配置,因为我们的开发环境和生产环境并不是所有的配置都完全不同,说一说我们需要一个公共的文件来去抽象两者之间相同的配置。
我们具体来看,首先我们在项目的跟目录下去新建一个webpack.common.js, 那在这个文件当中我们把刚刚复制的公共配置粘贴进来。
module.exports = {
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.join(__dirname, 'dist'),
publicPath: 'dist/'
},
devtool: 'cheap-eval-module-source-map',
devServer: {
hot: true,
contentBase: 'public'
},
module: {
rules: [
{
test: /.js$/,
use: {
loader: 'babel-loader',
options: {
presets: ['@babel/preset-env']
}
}
}
{
test: /.css$/,
use: [
'style-loader',
'css-loader'
]
},
{
test: /.png$/,
use: 'url-loader'
},
{
test: /.html$/,
use: {
loader: 'html-loader',
options: {
attrs: ['img:src', 'a:href']
}
}
}
]
},
plugins: [
new HtmlWebpackPlugin({
title: 'webpack',
template: './src/index.html'
}),
new webpack.HotModuleReplacementPlugin(),
]
}
然后我们再去新建一个webpack.dev.js和一个webpack.prod.js分别去用来为我们的开发和生产环境去定义特殊的配置。
在生产环境的配置当中(webpack.prod.js)我们先去导入公共的配置对象,这里我们可以使用Object.assign方法把我们公共配置对象复制到我们这里的配置对象当中,并且我们可以通过最后一个对象去覆盖掉这个公共配置当中的一些配置。
但是熟悉Object.assign这个方法的人都应该知道,这个方法是全完覆盖掉前一个对象当中的同名属性,那这样一个特点对应我们普通的值类型属性覆盖都没有什么问题,但是像我们配置当中的plugins这种数组,那我们是希望是可以在公共配置的原有基础之上我们去添加一两个插件。
const common = require('./webpack.common');
const {
CleanWebpackPlugin } = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = Object.assign({
}, common, {
mode: 'production',
plugins: [
new CleanWebpackPlugin(),
new CopyWebpackPlugin()
]
})
而Object.assgin这个方法呢,他会导致我们这里的特殊配置会覆盖掉公共配置,所以说Object.assign是不合适的,那这里我们就需要一个更合适的方法去合并这里的配置和公共的配置。
你可以使用loadash所提供的merge方法来去实现,不过社区当中提供了更为专业的webpack-merge这样一个模块。
那这个模块呢他可以专门用来满足这里合并webpack配置的这样一个需求。我们需要安装这样一个模块。
yarn webpack-merge --dev
那安装完成过后我们回到配置文件当中,我们先去载入这样一个模块,这个模块导出的就是一个merge函数。
我们这里使用这个函数来去合并我们这里的配置和公共的配置,使用webpack-merge这个模块过后呢我们这里所配置的这个对象他就可以跟普通的webpack配置一样,需要什么就配置什么。
merge函数的内部会自动去处理合并的逻辑。
const common = require('./webpack.common');
const merge = require('webpack-merge');
const {
CleanWebpackPlugin } = require('clean-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
module.exports = merge(common, {
mode: 'production',
plugins: [
new CleanWebpackPlugin(),
new CopyWebpackPlugin()
]
})
同理webpack.dev.js这样一个js文件当中也可以通过这样一个方式来去实现一些额外的配置,这里我们就不重复尝试了。
完成过后我们再次回到命令行终端然后尝试运行webpack打包。
不过这里因为我们已经没有了默认的配置文件,所以这里我们运行webpack时需要通过–config这样一个参数来去指定我们所使用的配置文件也就是我们刚刚的webpack.prod.js。
yarn webpack --config webpack.prod.js
那此时我们就可以以生产环境这种模式的配置去打包我们的应用了。
那当然如果你觉得这样去使用的话我们的命令变得复杂了,那你同样可以把这个构建的命令定义到package.json的script当中,方便我们的使用。
Webpack DefinePlugin
在webpack4x中新增的production模式下面内部就自动开启了很多通用的优化功能。
对于使用者而言,这种开箱即用的体验是非常方便的,但是对于学习者而言这种开箱即用他会导致我们忽略掉很多需要了解的东西。以致于我们出现问题之后无从下手。
如果说我们需要深入了解webpack的使用那我建议你可以去单独研究一下每一个配置背后的作用,那我们这里先一起来学习一下其中几个主要的优化配置。顺便去了解一下webpack是如何优化我们的打包结果的。
首先第一个是一个插件叫做define-plugin, 那define-plugin是用来为我们的代码去注入全局成员的。
在production模式下,默认这个插件就会启用起来并且往我们的代码当中注入了一个process.env.NODE_ENV这样一个常量。
很多第三方的模块都是通过这个成员去判断当前的运行环境,从而去决定是否去执行例如打印日志这样一些操作。
那这里我们先来单独使用一下这个插件。我们回到配置文件当中,那define-plugin是一个内置的插件所以说我们先要导入webpack模块。
然后我们再到plugins这个数组当中去添加一下这个插件,那这个插件他的构造函数接收的是一个对象,这个对象中每一个键值都会被注入到我们的代码当中。
例如我们这里在这个对象当中去定义一个API_BASE_URL的一个值,用来为我们的代码去注入我们的api服务地址,那他的值是一个字符串我们这里使用https://api.github.com。
const webpack = require('webpack');
module.exports = {
mode: 'node',
entry: './src/main.js',
output: {
filename: 'bundle.js'
},
plugins: [
new webpack.DefinePlugin({
API_BASE_URL: 'https://api.github.com'
})
]
}
然后我们回到我们的代码当中, 简单的来吧这个API_BASE_URL打印出来,完成以后我们打开命令行终端。然后运行webpack打包,找到打包结果,找到刚刚打印的位置。
这里我们发现,define-plugin其实就是把我们注入成员的值直接替换到了代码当中,而我们刚刚设置的值呢,内容就是https://api.github.com,字符串中并没有包含引号,所以说我们这里替换进来是没有引号的。
其实define-plugin的设计并不是只是用来帮我们替换一个数据进来,我们这所传递的字符串内容他要求的实际上是一个代码片段,也就是一段符合js语法的代码,所以说我们这样去传的话是不对的。
那正确的做法是传入一个字符串,这个字符串的内容呢就是一个我们js代码中的字符串字面量语句。
当然了,如果说你需要注入其他的代码也是可以的。
const webpack = require('webpack');
module.exports = {
mode: 'node',
entry: './src/main.js',
output: {
filename: 'bundle.js'
},
plugins: [
new webpack.DefinePlugin({
API_BASE_URL: '"https://api.github.com"'
})
]
}
完成以后我们再来查看打包结果,就会发现变成我们想要的样子了。另外呢这里还有一个非常常用的小技巧。
就是如果说我们需要注入的是一个值的话,那我们可以先通过JSON.stringfiy的方式来去将这个值去转换成一个表示这个值的代码片段,那这样的话就不会错了。
const webpack = require('webpack');
module.exports = {
mode: 'node',
entry: './src/main.js',
output: {
filename: 'bundle.js'
},
plugins: [
new webpack.DefinePlugin({
API_BASE_URL: JSON.stringify('https://api.github.com')
})
]
}
那这个插件的作用非常简单, 但是他确非常有用,那我们可以用它为我们的代码去注入一些可能会发生变化的值,例如我们刚刚使用的河中API的根路径,那我们的开发环境和生产环境他们的路径肯定是不一样的,我们就可以借助于define-plugin去注入我们想要的api路径。
转载:https://blog.csdn.net/xuzhijia1991/article/details/112238129