上一篇博客起了个头,介绍了为何要用webpack,用webpack能够作什么,以及最简单地配置。这篇博客将结合实际需求,一步一步的引入一些配置,全部内容基于实际需求出发。javascript
上一篇博客说到,entry是webpack打包的入口文件,webpack会从这个入口文件开始,寻找该入口文件的依赖模块,以及递归的寻找依赖模块的依赖模块,直到将全部的依赖模块打包成单个js文件输出到output配置的路径。根据不一样的使用场景,主要有一对1、多对1、多对多等不一样状况。html
若是没有设置entry,那么其默认值是./src/index.js
,即webpack会自动寻找根目录下的src文件夹中的index.js当作入口文件vue
经过字符串值能够直接指定入口文件的路径,好比上一篇博客中的例子:java
const path = require('path');
module.exports = {
entry: './src/index.js',
output: {
path: path.join(__dirname, 'dist'),
filename: 'bundle.js'
}
}
复制代码
这种状况就是将一个输入文件打包到一个输出文件。webpack
有时为了业务逻辑更加清晰,咱们的入口文件可能不止一个,这时能够将多个入口文件放在一个数组中,指定到entry:git
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
两个文件打包成一个文件输出。github
这种场景其实更常见,好比咱们想作一个SPA,一共有5个page,那么每一个page应该都是一个单独的入口文件,最后每一个page都应该打包输出一个单独的文件,也就是5个输入、5个输出的状况。这时就要用到对象形式的entry:web
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
。shell
在一个SPA项目中,有可能后面还会增长新的page,按照前面的配置,每次新加一个page,就要把这个page的路径放到entry里面去,告诉webpack把新增的这个page打包。显然这种方式并不完美,能够有更懒的办法。为了便于管理,通常会将全部的页面都放到pages目录下,那么在webpack构建时,只须要读取这个目录下的全部js文件,而后动态生成entry对象就能够了。npm
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'
}
}
复制代码
这样,就能够一劳永逸,之后添加的全部页面均可以自动打包了。
你们都知道浏览器是有缓存机制的,若是是同一个js文件,即便服务器上已经存在更新的版本,浏览器仍有可能从本地缓存读取,从而不能拿到最新的结果。要解决这个问题,就须要让每次打包生成的js文件名不同,而后让HTML去引用这个新名称的js文件,从而绕过浏览器的缓存。修改文件名最简单的方法就是在文件名后面加上md5戳,在webpack中这很容易实现
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基于每一个入口文件及其依赖进行计算,每一个输出文件的md5戳只跟本身的依赖文件有关,因此每一个输出文件的md5戳都是不一样的,而且只有本身的入口文件或依赖文件发生改变时才会变化。
modules.exports = {
entry: getEntries(),
output: {
path: path.join(__dirname, '/dist'),
filename: '[name].[chunkhash:5].js'
}
}
复制代码
最后输出的结果:
index1.f85c4.js
index2.dd9e3.js
复制代码
显然,chunkhash的方式更加理想。
每次打包生成的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的主要配置有:
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
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目录下的全部文件。
到目前为止,每次修改js文件后,都须要手动的执行npm run build
来启动webpack进行编译。一样的,这种机械性的重复工做很烦,应该交给webpack来作才对。只须要在执行webpack时加上--watch
启动监听模式就能够了,直接在package.json
中修改:
"scripts": {
"build": "webpack --watch"
}
复制代码
这样只须要执行一遍npm run build
,后面只要咱们修改了入口文件或其依赖文件中的任意一个,都会自动从新执行webpack打包。注意,若是是修改了webpack的配置文件webpack.config.js
,并不会自动从新执行,仍是要手动执行如下。
虽然修改文件后能够自动打包了,但仍是要手动刷新一下浏览器才能看到效果,这步操做也仍是有点烦,一样的交给webpack来作吧。
咱们这里先考虑只有一个输出的状况,即html-webpack-plugin为默认配置:
plugins: [new HtmlWebpackPlugin()]
复制代码
webpack-dev-sever
插件npm i webpack-dev-server -D
复制代码
module.exports = {
devServer: {
// 指定dist文件夹做为服务器的目录
contentBase: './dist'
}
}
复制代码
"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中的文件时,在不刷新浏览器的状况下就能够更新。
到目前为止,都是介绍如何用webpack打包从而进行开发,却忽视了一个问题于webpack将多个文件打包成了单个文件的。最后在浏览器中,咱们能看到的也就是打包后的这个文件。若是是打断点调试,咱们固然但愿是能在打包以前原始模块中,这样定位问题和修改也比较方便。为了实现这个功能,webpack中提供了devtool配置,只须要设置以下:
module.exports = {
devtool: 'source-map'
}
复制代码
这样,在浏览器的控制台中就能够看到打包以前的原始模块文件。
一般,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
。