Webpack4打包多页面工程基本教程——小白的填坑之旅
1. 背景
Webpack是当下使用人数最多的网页打包工具。当前其github主页的star数量为49.2k,比另一个流行的打包工具parcel(31.9k)要高不少。本人(小白一枚)由于项目需要的原因,最近几个月断断续续学习和使用webpack进行多页面工程的打包,可以说是一路挖坑一路填坑,最终实现了令人满意的效果。本文将回顾一路以来遇到的主要问题,简单分析并着重给出解决方案,主要包括:
html、js、css和图片的打包、压缩方法
多页面入口的正确打包结构及注意事项
多页面入口babel/polyfill引用方式
开发与调试配置文件差异
我希望给遇到同样问题的读者一些技术上的建议和帮助,从而少走弯路。文末会给出配置文件的完整代码,请读者耐心阅读,也希望指出笔者存在的问题,提出宝贵的建议。下面我将循序渐进地分享我的经验。😉
2. 什么是Webpack?
随着ECMAScript的版本不断迭代,node.js的不断推广,大页面被切分为小模块,一个js文件引用了多个js文件,很多的开源js库也可以通过下载,被引用到自己的工程中。这样的作法是有利于开发者的:将功能一致的代码提取到独立的文件中,既减少了每个文件的体积,降低开发难度,提高代码组织清晰度,又提高了代码的复用性。但是这么深的文件结构以及无数的小文件并不利于浏览器加载,因此,将多个模块和依赖合并为一个文件,简化网页工程的结构,这是webpack的基本能力。
在此基础之上,结合编译和压缩相关的库,webpack还可以编译js文件以及压缩网页、脚本、样式以及图片文件。从而得到浏览器可以直接使用的打包后的工程。
3. 基本安装
在确保node.js安装好的前提下,首先创建项目目录文件夹webpack-demo
mkdir webpack-demo && cd webpack-demo
安装webpack4.0以上版本,安装完成后,当前目录下会出现node_modules文件夹和package-lock.json。
npm install --save-dev webpack webpack-cli
创建和初始化package.json文件
npm init -y
初始化后的package.json文件结构如下:
{
"name": "webpack-demo",
"version": "1.0.0",
"description": "",
"main": "index.js",
"dependencies": {
"webpack-cli": "^3.3.2",
"webpack": "^4.33.0"
},
"devDependencies": {},
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}
这里我给这个json添加以下代码,防止意外发布私有库:
{
...
"private":true,
...
}
对于package.json的详细介绍可以参考这篇文章mujiang.info。
这样,webpack环境就基本配好。接下来我直接介绍多文件打包的流程。👽
4. 多文件基本结构
|–node_modules
package.json
package-lock.json
|–dist
|–img
|–css
|–js
index.html
second.html
|–src
|–img
|–css
|–js
index.html
second.html
PS:
/dist:打包后的文件夹; /src:打包前的文件夹
该文件目录只是多文件结构的一种,当然也可以一个页面做一个文件夹,然后下面包含/img、/css等资源文件。本文以该结构进行讲解。
5. 打包配置文件
5.1. 创建配置文件
打包配置文件是webpack的特点之一,webpack根据配置文件的设定对文件夹进行打包。通常我们会在根目录直接创建一个名为webpack.config.js的配置文件,这也是webpack默认的配置文件位置。但是其实是可以灵活调整的。这里我直接创建两个配置文件,一个用于调试(webpack.config.dev.js),一个用于发布(webpack.config.build.js)。
我在根目录创建一个名为config的文件夹,然后创建上述两个配置文件,文件结构变为:
|–config
webpack.config.dev.js
webpack.config.build.js
…
我先对用于开发的配置文件进行介绍。
5.2. webpack.config.dev.js配置文件
基本内容如下:
const webpack = require('webpack');
const path = require('path');
module.exports = {
mode:"development",
entry:{...},
output:{...},
module:{...},
plugins:[...],
...
}
各参数含义如下表:
参数 | 含义 |
---|---|
mode | 可以选"development"或"production",前者表示开发模式,不压缩;后者表示生产模式,会对js文件进行压缩。 |
entry | 打包入口,通常为某个js文件 |
output | 打包输出路径 |
module | 打包规则,不同后缀的文件用不同的包来处理 |
plugins | 实现一些功能用到的插件 |
下面对各参数详细讲解。
5.2.1. entry
entry可以定义多个入口,格式如下:
entry:{
name1:'./src/js/name1.js',
name2:'./src/js/name2.js',
...
}
PS:
name1为别名,‘src/js/name1.js’为入口js文件的路径,该路径是以根目录为当前目录,注意’src’前面不能少掉’./’,否则打包会报错找不到该文件。
对于上述文件结构,每个html都会对应一个js入口文件,比如index.html引用了index.js,first.js作为入口引用了其他的文件。同理,second.html引用了second.js作为入口文件,那么entry可以写为:
entry:{
index:'./src/js/first.js',
second:'./src/js/second.js',
}
这样就会打包这两个js。
5.2.2. output
output基本设置如下:
output: {
filename: 'js/[name].js',
path: path.resolve(__dirname, '../dist'),
}
PS:
需要注意的地方有两点
- path的当前目录是webpack.config.js的当前目录,即/config,而/dist与/config是平行关系,因此写的是’…/dist’。
- filename是打包后的文件名,是相对于path。name是entry中的别名,'js/[name].js’表明打包后的位置是/dist/js/[name].js。
5.2.3. module
module基本结构如下:
module:{
rules:[
{
test:正则表达式,
use:[对应的loader]
}
]
}
module的作用是匹配不同类型的文件,用不同的包进行处理和解析。这里就涉及到文件的压缩问题。html、js、css和图片的打包策略都不同,我将在后面的章节中详细介绍。
5.2.4. plugins
plugins是为了更方便打包,实现了某些功能的接口。我将在后面实现具体功能时穿插plugins的配置方法。
5.3. webpack.config.build.js配置文件
发布的版本和调试用的版本有很大的区别,主要差异在于文件的体积小很多。这里就引出第六章压缩文件的话题。
6. 压缩文件
网页是由许许多多资源文件组成的,包括网页文件(.html)、脚本文件(.js)、样式文件(.css)、以及图片文件(.jpg、.png)等等。当浏览器打开这些网页时,本质上是从服务器上下载这些资源文件到浏览器上,再通过浏览器对这些文件进行解析,渲染成我们所看到的各式各样的网页。既然是从服务器上下载,在不影响效果的前提下,我们都希望文件越小越好,因为这样下载的时间更短,用户体验就会提高。压缩文件的方式最简单的就是将文件中的换行和空格去掉,而压缩不同类型的文件用到的库页不同,下面介绍对于四种资源文件的压缩打包配置方式。
6.1. 压缩html
通常打包的js,与html是没有对应关系的,即独立打包,之后在html中对打包后的js进行引用。但其实通过插件,html和js是可以联系起来的。这里用到了html-webpack-plugin
包,安装方式如下:
npm install --save-dev html-webpack-plugin
安装后,在plugins中进行配置:
const htmlPlugin = require('html-webpack-plugin');
...
plugins:[
new htmlPlugin({
filename:'index.html',//打包后的文件名
minify:{//对html文件进行压缩
removeAttributeQuotes:true, //去掉属性的双引号
removeComments: true,//去掉注释
collapseWhitespace: true,//去掉空白
},
chunks:['index'],//每个html只引入对应的js和css
inject:true,
hash:true, //避免缓存js。
template:'./src/index.html' //打包html模版的路径和文件名称
}),
new htmlPlugin({
filename:'second.html',
minify:{...},
chunks:['second'],
inject:true,
hash:true,
template:'./src/second.html'
}),
...
]
PS:
关键的几个属性
- template:选择要打包的html路径,以根目录为当前目录
- chunks:写entry中的入口名称,即对应的js
- minify:压缩配置
根据上述配置,可以进行多个页面(html)的匹配(与js)和压缩。
6.2. 压缩图片
图片压缩分为两类,一类是在html中的图片,另一类是在js或者css中的图片。
需要下载两个库url-loader
和html-withimg-loader
,安装方式如下:
npm install --save-dev url-loader html-withimg-loader
在module中的配置如下:
module:{
rules:[
{//压缩css和js中的图片
test:/\.(png|jpg|gif|jpeg)/,//匹配图片文件后缀名
use:[{
loader:'url-loader',//指定使用的loader和loader的配置参数
options:{
limit:5*1024,//是把小于5KB的文件打成Base64的格式,写入JS
outputPath:'./img/',//打包后的图片放到img文件夹下
}
}]
},
{//html配置
test: /\.(htm|html)$/i,
use:[ 'html-withimg-loader']
},
...
]
}
6.3. 提取和压缩css
css的提取和压缩实际上用到了不同的包,前者用到extract-text-webpack-plugin
、style-loader
、css-loader
,后者用到optimize-css-assets-webpack-plugin
、cssnano
和postcss-safe-parser
,安装方法如下:
npm install --save-dev extract-text-webpack-plugin style-loader css-loader optimize-css-assets-webpack-plugin cssnano postcss-safe-parser
配置方法如下:
const extractTextPlugin = require('extract-text-webpack-plugin');
const indexExtractCss = new extractTextPlugin('css/[name].css');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
...
module:{
rules:[
{
test:/src(\\|\/)css(\\|\/).*\.(css)$/,
use:indexExtractCss.extract({
fallback:"style-loader",
use:[
{
loader:"css-loader",
options:{
// importLoaders:1
// minimize:true
}
},
]
})
},
...
]
},
plugins:[
indexExtractCss,
new OptimizeCSSAssetsPlugin({//压缩css
assetNameRegExp: /(?:first|second)\.css/g, //需要根据自己打包出来的文件名来写正则匹配,这个配置是我自己的
cssProcessor: require('cssnano'),
cssProcessorOptions: {
discardComments: { removeAll: true },
parser: require('postcss-safe-parser'),
autoprefixer: false
},
canPrint: true
}),
]
6.4. 压缩和编译js
webpack高版本对于js的压缩其实非常简单,只需要将配置文件中的mode改为"production"即可。
js的编译则需要引入babel,为了使用ecmascript最新的标准,需要引入babel/polyfill。安装如下:
npm install --save-dev @babel/core @babel/polyfill @babel/preset-env babel-loader
使用也很容易,配置如下:
entry:{
index:['@babel/polyfill','./src/js/first.js'],
second:['@babel/polyfill','./src/js/second.js'],
},
module:{
rules:[
{
test:/\.js$/,
exclude: /node_modules/,//排除node_modules文件夹下的js
loader:'babel-loader',
}
]
},
...
另外,在每个入口文件的文件头,引用polyfill。以first.js为例,如下所示:
import'@babel/polyfill';
...
这样就可以使用最新的API来写js代码了。👍
7. webpack打包路径问题
css中引用的img,打包后的图像路径是相对于css文件,而js或html中引用的img,打包后是相对于html的路径。为了使得打包后的图像统一放在文件夹dist/img/下,使用publicPath参数。
配置方法:
output:{
...
publicPath:'/'
},
...
这样的效果是将所有打包后的相对路径都替换为绝对路径/,这样无论之后发布的ip和端口怎么变,只要dist是根目录,引用就没问题。当然,如果已知ip和端口不变,也可以直接设置publicPath为’http://ip:port/’,只是这样不够灵活。
8. 网页调试
webpack可以很方便的实时打包和调试,需要安装webpack-dev-server
,安装方式如下:
npm install --save-dev webpack-dev-server
在package.json中添加:
"scripts":{
"server": "webpack-dev-server --open --hot --config=config/webpack.config.dev.js"
}
然后在webpack.config.dev.js中配置:
module.exports = {
...
devServer:{
contentBase:path.resolve(__dirname,'../dist'),//设置基本目录结构,相对当前文件的路径
host:'localhost',//服务器的IP地址,这里先使用loaclhost地址
compress:true,//服务端压缩是否开启
port:'8888', //配置服务端口号
},
}
运行
npm run server
就会自动在浏览器中打开网页,并且修改js文件会自动刷新网页。💚
9. 附件
9.1. webpack.config.dev.js
const webpack = require('webpack');
const path = require('path');
const htmlPlugin = require('html-webpack-plugin');
const extractTextPlugin = require('extract-text-webpack-plugin');
const indexExtractCss = new extractTextPlugin('css/[name].css');
var website = {
publicPath:"http://localhost:8888/" //调试
}
module.exports = {
mode: "development",//development or production
entry:{
index:['@babel/polyfill','./src/js/first.js'],
second:['@babel/polyfill','./src/js/second.js'],
},
output: {
filename: 'js/[name].js',
path: path.resolve(__dirname, '../dist'),
publicPath:website.publicPath
},
module:{
rules:[
{
test:/\.js$/,
exclude: /node_modules/,
loader:'babel-loader',
},
{
test:/\.(css)$/,
use:indexExtractCss.extract({
fallback:"style-loader",
use:[
{
loader:"css-loader",
},
]
})
},
{
test:/\.(png|jpg|gif|jpeg)/, //是匹配图片文件后缀名
use:[{
loader:'url-loader', //指定使用的loader和loader的配置参数
options:{
limit:5*1024, //是把小于5KB的文件打成Base64的格式,写入JS
outputPath:'./img/' //打包后的图片放到img文件夹下
}
}]
},
{
test: /\.(htm|html)$/i,
use:[ 'html-withimg-loader']
}
]
},
plugins:[
new htmlPlugin({
filename:'index.html',
minify:{//对html文件进行压缩
removeAttributeQuotes:true, //removeAttrubuteQuotes是去掉属性的双引号。
},
chunks:['index'],//每个html只引入对应的js和css
inject:true,
hash:true, //为了开发中js有缓存效果,所以加入hash,这样可以有效避免缓存JS。
template:'./src/index.html' //打包html模版的路径和文件名称。
}),
new htmlPlugin({
filename:'second.html',
minify:{//对html文件进行压缩
removeAttributeQuotes:true, //removeAttrubuteQuotes是去掉属性的双引号。
},
chunks:['second'],
inject:true,
hash:true, //为了开发中js有缓存效果,所以加入hash,这样可以有效避免缓存JS。
template:'./src/second.html' //打包html模版的路径和文件名称。
}),
indexExtractCss,//提取css
],
devServer:{
contentBase:path.resolve(__dirname,'../dist'),//设置基本目录结构
host:'localhost',//服务器的IP地址,这里先使用loaclhost地址
compress:true,//服务端压缩是否开启
port:'8888', //配置服务端口号
},
};
9.2. webpack.config.build.js
const webpack = require('webpack');
const path = require('path');
const htmlPlugin = require('html-webpack-plugin');
const extractTextPlugin = require('extract-text-webpack-plugin');
const indexExtractCss = new extractTextPlugin('css/[name].css');
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
var website = {
publicPath:"/"
}
module.exports = {
mode: "production",//development or production
entry:{
index:['@babel/polyfill','./src/js/first.js'],
second:['@babel/polyfill','./src/js/second.js'],
},
output: {
filename: 'js/[name].js',
path: path.resolve(__dirname, '../dist'),
publicPath:website.publicPath
},
module:{
rules:[
{
test:/\.js$/,
exclude: /node_modules/,
loader:'babel-loader',
},
{
test:/\.(css)$/,
use:indexExtractCss.extract({
fallback:"style-loader",
use:[
{
loader:"css-loader",
},
]
})
},
{
test:/\.(png|jpg|gif|jpeg)/, //是匹配图片文件后缀名
use:[{
loader:'url-loader', //指定使用的loader和loader的配置参数
options:{
limit:5*1024, //是把小于5KB的文件打成Base64的格式,写入JS
outputPath:'./img/' //打包后的图片放到img文件夹下
}
}]
},
{
test: /\.(htm|html)$/i,
use:[ 'html-withimg-loader']
}
]
},
plugins:[
new htmlPlugin({
filename:'index.html',
minify:{//对html文件进行压缩
removeAttributeQuotes:true, //removeAttrubuteQuotes是去掉属性的双引号。
},
chunks:['index'],//每个html只引入对应的js和css
inject:true,
hash:true, //为了开发中js有缓存效果,所以加入hash,这样可以有效避免缓存JS。
template:'./src/index.html' //打包html模版的路径和文件名称。
}),
new htmlPlugin({
filename:'second.html',
minify:{//对html文件进行压缩
removeAttributeQuotes:true, //removeAttrubuteQuotes是去掉属性的双引号。
},
chunks:['second'],
inject:true,
hash:true, //为了开发中js有缓存效果,所以加入hash,这样可以有效避免缓存JS。
template:'./src/second.html' //打包html模版的路径和文件名称。
}),
indexExtractCss,//提取css
new OptimizeCSSAssetsPlugin({//压缩css
assetNameRegExp: /(?:index|second)\.css/g, //需要根据自己打包出来的文件名来写正则匹配这个配置是我自己的
cssProcessor: require('cssnano'),
cssProcessorOptions: {
discardComments: { removeAll: true },
parser: require('postcss-safe-parser'),
autoprefixer: false
},
canPrint: true
}),
],
};
9.3. package.json
{
"name": "webpack-demo",
"version": "1.0.0",
"description": "",
"private": true,
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"watch": "webpack --watch",
"build": "webpack --config=config/webpack.config.build.js",
"server": "webpack-dev-server --open --hot --config=config/webpack.config.dev.js"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.4.3",
"@babel/polyfill": "^7.4.3",
"@babel/preset-env": "^7.4.3",
"babel-loader": "^8.0.5",
"css-loader": "^2.1.1",
"cssnano": "^4.1.10",
"extract-text-webpack-plugin": "^4.0.0-beta.0",
"file-loader": "^3.0.1",
"html-webpack-plugin": "^3.2.0",
"html-withimg-loader": "^0.1.16",
"optimize-css-assets-webpack-plugin": "^5.0.0",
"postcss-safe-parser": "^4.0.1",
"style-loader": "^0.23.1",
"url-loader": "^1.1.2",
"vue-multiselect": "^2.1.4",
"webpack": "^4.30.0",
"webpack-cli": "^3.3.1",
"webpack-dev-server": "^3.3.1"
},
"dependencies": {}
}
9.4. 打包和调试命令
打包运行
npm run build
调试运行
npm run server
10. 结束语
好久没有写这么长的博文了,写完后只有两个字“舒服”。本文是笔者个人学习和实践所得,如需转载,请注明出处,谢谢!🌱