Webpack4打包多页面工程基本教程——小白的填坑之旅

1. 背景

Webpack是当下使用人数最多的网页打包工具。当前其github主页的star数量为49.2k,比另一个流行的打包工具parcel(31.9k)要高不少。本人(小白一枚)由于项目需要的原因,最近几个月断断续续学习和使用webpack进行多页面工程的打包,可以说是一路挖坑一路填坑,最终实现了令人满意的效果。本文将回顾一路以来遇到的主要问题,简单分析并着重给出解决方案,主要包括:

  1. html、js、css和图片的打包、压缩方法
  2. 多页面入口的正确打包结构及注意事项
  3. 多页面入口babel/polyfill引用方式
  4. 开发与调试配置文件差异

我希望给遇到同样问题的读者一些技术上的建议和帮助,从而少走弯路。文末会给出配置文件的完整代码,请读者耐心阅读,也希望指出笔者存在的问题,提出宝贵的建议。下面我将循序渐进地分享我的经验。😉

2. 什么是Webpack?

随着ECMAScript的版本不断迭代,node.js的不断推广,大页面被切分为小模块,一个js文件引用了多个js文件,很多的开源js库也可以通过下载,被引用到自己的工程中。这样的作法是有利于开发者的:将功能一致的代码提取到独立的文件中,既减少了每个文件的体积,降低开发难度,提高代码组织清晰度,又提高了代码的复用性。但是这么深的文件结构以及无数的小文件并不利于浏览器加载,因此,将多个模块和依赖合并为一个文件,简化网页工程的结构,这是webpack的基本能力。

在此基础之上,结合编译和压缩相关的库,webpack还可以编译js文件以及压缩网页、脚本、样式以及图片文件。从而得到浏览器可以直接使用的打包后的工程。
webpack官网首页

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:需要注意的地方有两点

  1. path的当前目录是webpack.config.js的当前目录,即/config,而/dist与/config是平行关系,因此写的是’…/dist’。
  2. 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:关键的几个属性

  1. template:选择要打包的html路径,以根目录为当前目录
  2. chunks:写entry中的入口名称,即对应的js
  3. minify:压缩配置

根据上述配置,可以进行多个页面(html)的匹配(与js)和压缩。

6.2. 压缩图片

图片压缩分为两类,一类是在html中的图片,另一类是在js或者css中的图片。

需要下载两个库url-loaderhtml-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-pluginstyle-loadercss-loader,后者用到optimize-css-assets-webpack-plugincssnanopostcss-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. 结束语

好久没有写这么长的博文了,写完后只有两个字“舒服”。本文是笔者个人学习和实践所得,如需转载,请注明出处,谢谢!🌱

  • 12
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 10
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

HouGISer

HouGiser需要你的鼓励~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值