上一篇博客起了个头,介绍了为什么要用webpack,用webpack可以做什么,以及最简单地配置。这篇博客将结合实际需求,一步一步的引入一些配置,所有内容基于实际需求出发。
entry和output
上一篇博客说到,entry是webpack打包的入口文件,webpack会从这个入口文件开始,寻找该入口文件的依赖模块,以及递归的寻找依赖模块的依赖模块,直到将所有的依赖模块打包成单个js文件输出到output配置的路径。根据不同的使用场景,主要有一对一、多对一、多对多等不同情况。
默认值
如果没有设置entry,那么其默认值是./src/index.js
,即webpack会自动寻找根目录下的src文件夹中的index.js当做入口文件
entry为字符串:一对一
通过字符串值可以直接指定入口文件的路径,比如上一篇博客中的例子:
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.join(__dirname, 'dist'),
filename: 'bundle.js'
}
}
这种情况就是将一个输入文件打包到一个输出文件。
entry为数组:多对一
有时为了业务逻辑更加清晰,我们的入口文件可能不止一个,这时可以将多个入口文件放在一个数组中,指定到entry:
const path = require('path');
module.exports = {
entry: ['./src/index1.js', './src/index2.js'],
output: {
path: path.join(__dirname, 'dist'),
filename: 'bundle.js'
}
}
这时会将./src/index1.js
和./src/index2.js
两个文件打包成一个文件输出。
entry为对象:多对多
这种场景其实更常见,比如我们想做一个SPA,一共有5个page,那么每个page应该都是一个单独的入口文件,最后每个page都应该打包输出一个单独的文件,也就是5个输入、5个输出的情况。这时就要用到对象形式的entry:
const path = require('path');
module.exports = {
entry: {
index1: './src/index1.js',
index2: './src/index2.js'
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js'
}
}
在output中的filename中,我们没有直接指定文件名,而是用[name].js
,这里的name就是entry中的key值index1和index2,也就是告诉webpack,对entry中的两个入口文件分别打包,最后分别输出到dist下的index1.js
和index2.js
。
更完美一点的用法
在一个SPA项目中,有可能后面还会增加新的page,按照前面的配置,每次新加一个page,就要把这个page的路径放到entry里面去,告诉webpack把新增的这个page打包。显然这种方式并不完美,可以有更懒的办法。为了便于管理,一般会将所有的页面都放到pages目录下,那么在webpack构建时,只需要读取这个目录下的所有js文件,然后动态生成entry对象就可以了。
const path = require('path');
const glob = require('glob');
function getEntries() {
const pagePath = path.resolve(__dirname, 'src/pages');
let entries = {};
// 读取pages目录下的所有js文件
let files = glob.sync(pagePath + '/*.js');
// 根据文件名,生成entry对象
files.forEach(item => {
let filename = item.split(path.sep).pop().split('.')[0];
entries[filename] = item;
})
return entries
}
module.exports = {
entry: getEntries(),
output: {
path: path.join(__dirname, '/dist'),
filename: '[name].js'
}
}
这样,就可以一劳永逸,以后添加的所有页面都可以自动打包了。
输出添加hash
大家都知道浏览器是有缓存机制的,如果是同一个js文件,即使服务器上已经存在更新的版本,浏览器仍有可能从本地缓存读取,从而不能拿到最新的结果。要解决这个问题,就需要让每次打包生成的js文件名不一样,然后让HTML去引用这个新名称的js文件,从而绕过浏览器的缓存。修改文件名最简单的方法就是在文件名后面加上md5戳,在webpack中这很容易实现
hash
modules.exports = {
entry: getEntries(),
output: {
path: path.join(__dirname, '/dist'),
filename: '[name].[hash].js'
}
}
只用在filename后面加上[hash]
,最后打包生成的js文件名就会带上md5戳。比如:
index1.20e52ce145885ab03243.js
index2.20e52ce145885ab03243.js
如果不希望后面的md5戳那么长,也可以指定md5戳的长度:
modules.exports = {
entry: getEntries(),
output: {
path: path.join(__dirname, '/dist'),
filename: '[name].[hash:5].js'
}
}
这样最后打包生成的js文件的md5长度只有5位:
index1.20e52.js
index2.20e52.js
hash的计算方法是基于整个项目的,只要整个项目中的任何一个文件发生变化,生成的md5戳就会改变,即使真正的入口js及其依赖并没有发生变化。此外,所有的输出文件都共用这一个md5戳的值。这种方式有些过于粗暴,对浏览器的缓存机制很不友好。我们希望的是,如果文件有改动,文件名后面的hash值改变,如果没有改动,则保持不变,浏览器直接从缓存读取。这样既保证了及时更新,又保证了性能。
chunkhash
chunkhash基于每个入口文件及其依赖进行计算,每个输出文件的md5戳只跟自己的依赖文件有关,所以每个输出文件的md5戳都是不同的,并且只有自己的入口文件或依赖文件发生改变时才会变化。
modules.exports = {
entry: getEntries(),
output: {
path: path.join(__dirname, '/dist'),
filename: '[name].[chunkhash:5].js'
}
}
最后输出的结果:
index1.f85c4.js
index2.dd9e3.js
显然,chunkhash的方式更加理想。
动态生成HTML
每次打包生成的js文件名都会不一样,那么index.html
中的script标签每次都要去改引入的文件名,这也太麻烦了。同样的,webpack可以帮我们解决这个问题。前面的博客中提到,webpack的强大之处在于强大的生态系统,提供了很多有用的插件。在这里,我们就可以使用插件来帮我们实现。
首先,安装这个插件:
npm i html-webpack-plugin -D
然后,在webpack配置文件中使用这个插件:
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
...
plugins: [
new HtmlWebpackPlugin({
// options配置
})
]
}
这时候再执行npm run build
,就会看到dist文件下会自动生成一个index.html
,并且这个Html文件会自动引入文件名变化的index.[md5].js
文件。直接将这个文件拖到浏览器中就可以显示。
如果不做任何配置,html-webpack-plugin会创建一个新的空白HTML(官方文档说会默认寻找src/index.ejs
,不过我在实测中未实现,有可能是我哪个地方配置不对),只是在这个HTML中引入了打包后的js文件,没有设定任何的DOM。这种情况是合理的,因为当我们用webpack时,很多时候HTML本身就是空白的,所有的DOM都是通过js渲染出来然后挂载上去的,比如常见的vue。可以通过title去设定这个HTML的title标签的内容。当然也可以指定一个模板HTML,html-webpack-plugin会在这个HTML中自动引入打包后的js文件。options的主要配置有:
- template:一个字符串路径,指定HTML的模板,将在该HTML中引入打包后的js
- filename:指定输出文件的文件名
- Chunks:默认情况下,html-webpack-plugin会引入打包后的所有的js文件,可以在chunk中指定引入哪些文件
plugins: [
new HtmlWebpackPlugin({
// 指定index.html作为模板,即让index.html自动引用打包后的js文件
template: 'src/index.html',
// 输出到dist目录下的index1.html
filename: 'index1.html',
// 让index.html只引用dist目录下的page1.[hash].js文件,注意这里只用写文件名的前缀,如果写成page1.js会直接在dist文件夹下找page1.js文件,而不能找到带hash的结果
chunks: ['page1']
})
]
如果希望有多个HTML,那么就可以设置多个html-webpack-plugin:
plugins: [
// 第一个页面,index1.html,引用page1.js打包后的文件
new HtmlWebpackPlugin({
template: 'src/index.html',
filename: 'index1.html',
chunks: ['page1']
}),
// 第二个页面,index2.html,引用page2.js打包后的文件
new HtmlWebpackPlugin({
template: 'src/index.html',
filename: 'index2.html',
chunks: ['page2']
})
]
当然,如果页面个数是动态变化的,也可以用多entry类型的形式读取文件夹下的所有文件。更多的配置可以Github
清除dist
webpack每执行一次,就会在dist目录下生成一些文件。如果这次生成的文件和上次是同名的,则会直接覆盖,问题不大。但如果是不同名的,则会在dist文件中累加,最后导致dist文件夹中存在很多不必要的文件。例如上面我们加上md5戳之后,每次打包生成的输出文件名并不相同,这就会造成dist文件夹的累积。即使不是用md5戳,也会有类似场景。比如开始我在pages下有一个page3.js
,打包后在dist目录下生成了page3.js
,后来我不需要这个页面了,把pages下的page3.js
删除了,执行webpack,但此时dist目录下的page3.js
依然存在。显然这些不是我们希望的。一种解决方法是每次执行webpack前手动清空dist,不过这也太麻烦了,这种麻烦的事情当然需要webpack来替我们做。
首先,需要安装这个插件
npm i clean-webpack-plugin
然后,在配置文件中使用这个插件
const {CleanWebpackPlugin} = require('clean-webpack-plugin');
modules.exports = {
...,
plugins: [
new CleanWebpackPlugin()
]
}
这样,每次执行webpack时就会自动清除dist目录下的所有文件。
watch模式
到目前为止,每次修改js文件后,都需要手动的执行npm run build
来启动webpack进行编译。同样的,这种机械性的重复工作很烦,应该交给webpack来做才对。只需要在执行webpack时加上--watch
启动监听模式就可以了,直接在package.json
中修改:
"scripts": {
"build": "webpack --watch"
}
这样只需要执行一遍npm run build
,后面只要我们修改了入口文件或其依赖文件中的任意一个,都会自动重新执行webpack打包。注意,如果是修改了webpack的配置文件webpack.config.js
,并不会自动重新执行,还是要手动执行以下。
webpack-dev-server自动刷新
虽然修改文件后可以自动打包了,但还是要手动刷新一下浏览器才能看到效果,这步操作也还是有点烦,同样的交给webpack来做吧。
单个输出
我们这里先考虑只有一个输出的情况,即html-webpack-plugin为默认配置:
plugins: [new HtmlWebpackPlugin()]
- 安装一下
webpack-dev-sever
插件
npm i webpack-dev-server -D
- 在配置文件webpack.config.js中启用:
module.exports = {
devServer: {
// 指定dist文件夹作为服务器的目录
contentBase: './dist'
}
}
- 这时候,在package.json中直接通过webpack-dev-server启动
"scripts": {
"build": "webpack --watch",
"start": "webpack-dev-server --opem"
}
然后执行npm run start
,浏览器窗口就会自动打开,显示index.html引用所有js文件的结果。这是只要改变index.html
或者任意一个引用的js文件,浏览器都会自动刷新以显示最新的结果。
多个输出
单个输出的情况比较简单,dist目录下只有一个index.html
文件,浏览器一打开就会显示这个文件。但是,如果是上面“动态生成HTML”一节描述的多个html文件,DevSever肯定无法知道显示哪一个了,这时有以下2种办法:
- Webpack-dev-server一般默认打开的是
localhost:8080
,在contentBase中我们配置的就是dist文件夹,所以这个地址等同于dist文件夹,如果输入localhost:8080/index1.html
就会显示index1.html
,输入localhost:8080/index2.html
就会显示index2.html
-
直接配置openPage,告诉webpack-dev-server默认打开哪个:
devServer: { contentBase: "./dist", // 告诉webpack-dev-server默认打开index2.html openPage: './index2.html' }
热更新
在已经使用了webpack-dev-server的情况下,修改文件已经不需要手动刷新了,但是,浏览器自动刷新也会有一些问题:比如在调试一个表单验证问题,已经填写了一些信息,如果浏览器自动刷新,会直接将之前填写的表单内容清空。而webpack的热更新(HMR, Hot Module Replacement),就可以让浏览器在不刷新的情况下直接更新浏览器页面。
-
使用HotModuleReplacementPlugin,它包含在webpack中。在配置文件中添加:
const webpack = require('webpack'); module.exports = { plugins: [new webpack.HotModuleReplacementPlugin()] }
-
在之前devServer中设置hot为true
devSever: { contentBase: './dist', hot: true }
-
在入口文件index.js底部添加对子模块的监听
if (module.hot) { module.hot.accept(); }
这样,如果再修改子模块a.js中的文件时,在不刷新浏览器的情况下就可以更新。
Devtool调试
到目前为止,都是介绍如何用webpack打包从而进行开发,却忽视了一个问题于webpack将多个文件打包成了单个文件的。最后在浏览器中,我们能看到的也就是打包后的这个文件。如果是打断点调试,我们当然希望是能在打包之前原始模块中,这样定位问题和修改也比较方便。为了实现这个功能,webpack中提供了devtool配置,只需要设置如下:
module.exports = {
devtool: 'source-map'
}
这样,在浏览器的控制台中就可以看到打包之前的原始模块文件。
mode
通常,js代码都要分开发模式和生产模式。比如,在开发模式中,不用太纠结代码的性能,更加重视代码的可读性,而在生产模式中,代码一般需要进行优化和压缩等。在webpack4中,可以通过mode来配置development模式和production模式,主要有两种配置方法:
-
直接在配置文件中指定mode
module.exports = { mode: 'development' }
-
在运行webpack时增加
--mode
,可以在package.json
中添加"scripts": { "dev": "webpack --mode development", "prod": "webpack --mode production" }
这时,运行
npm run dev
就是开发模式,npm run prod
就是生产模式
分别看一下dist目录下输出的文件,可以发现production模式下的代码体积更小,dev模式下的代码可读性更强。
多配置文件
在webpack 4中,已经可以通过mode来指定不同生产环境下代码的编译模式了,但这还是不够的。比如,在开发环境下,我们希望开启devtool调试,而在生产模式下就不需要。像这样的区分配置还有很多,如果每次都是去修改webpack.config.js
,显然非常麻烦。一种简单的方法是,我们建一个build文件夹,在这个文件下下放三个配置文件。为了整合,先安装一个叫webpack-merge
的插件,其主要作用是合并两个webpack配置文件:
npm i webpack-merge -D
然后分别写三个配置文件:
-
webpack.base.config.js
:这个文件用来存放公共的配置,也就是在开发环境和生产环境都要用到的module.exports = { // 公共配置,如entry/output }
-
webpack.dev.config.js
:开发环境下的独有配置const merge = require('webpack-merge'); const baseConfig = require('./webpack.base.config') module.exports = merge(baseConfig, { mode: 'development', devtool: 'source-map', devServer: { contentBase: './dist', // 在开发模式下,默认打开index1 openPage: './index1.html', hot: true }, })
-
webpack.prod.config.js
:生产环境下的独有配置const merge = require('webpack-merge'); const baseConfig = require('./webpack.base.config'); module.exports = merge(baseConfig, { mode: 'production', devServer: { contentBase: './dist', // 在生产模式下,默认打开index2 openPage: './index2.html', hot: true }, })
可以看到,base只是一个公共文件,真正传到webpack使用的是dev和prod。那么如何传到webpack呢?在webpack或者webpack-dev-server运行时,可以通过--config
指定配置文件。所以,我们直接修改package.json
"scripts": {
"dev": "webpack-dev-server --open --config ./build/webpack.dev.config.js",
"prod": "webpack-dev-server --open --config ./build/webpack.prod.config.js"
}
此时,执行npm run dev
,就会按照webpack.dev.config.js
的配置,打开index1.html
,并且开启debug;执行npm run prod
,就会按照webpack.prod.config.js
的配置,打开index2.html
。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。