一、babel
什么是babel?浏览器对ES6的语法支持的并不好,一些ES6的语法,浏览器并不能识别;React的JSX语法,浏览器也是不能识别的,都需要转换为浏览器能识别的语法,才可以被浏览器解释执行。我们需要babel来做这件事, 把babel当做翻译器。
1.1 babel使用
babel的配置文件是.bablerc,同时需要babel-loader。
babel的使用很简单,test对文件格式进行匹配,比如.js结尾的文件/.js$/,同时使用bable-loader
module: {
rules: [
{
test: /\.js$/,
use: 'bable-loader'
}
]
}
1.2 babel的构成
babel是由presets和plugins两部分构成,bable的plugins中,每个plugin能解决一个问题。
比如:解释箭头函数需要 transform-es2015-arrow-functions 这个插件;
比如:解释模板字符串需要 transform-es2015-template-literals 这个插件;
这就是插件的概念
但是,babel也有全家桶,比如你要解释ES6,babel全家桶就一次性把ES6需要的plugins集合起来,叫做一个presets,这样你就不用一个一个的去安装了,如果项目中有需要一些插件的,就写在plugins中
1.3 babel解析ES6
{
"presets": [
"@babel/preset-env" // ES6的babel preset配置
],
"plugins": [
"@babel/proposal-class-properties"
]
}
npm i @babel/core @babel/preset-env babel-loader -D
1.4 babel解析JSX
npm install react react-dom @babel/plugin-proposal-class-properties @babel/preset-react -D
在.babelrc添加react的preset
import React from 'react'
import ReactDOM from 'react-dom'
class Search extends React.Component {
render = () => {
return (
<div>我是 React Search 组件</div>
)
}
}
ReactDOM.render(
<Search />,
document.getElementById('root')
)
编译之后
二、解析样式(CSS、Less、Sass)
2.1 解析CSS
css-loader用于加载.css 文件,并且转换为commonjs对象
style-loader将样式通过
npm i style-loader css-loader -D
webpack.config.js中的module下的rules数组中添加一项:
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
注意:
也就是如下:loader的调用是链式的从右向左调用,所以,我们从左向右依次写入style-loader、css-loader,那么实际执行的时候先用css-loader去解析css,再将解析好的css传递给style-loader(链式调用)
const path = require('path')
module.exports = {
entry: {
index: './src/index.js',
search: './src/search.js'
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js'
},
mode: 'production',
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader'
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
}
]
}
}
执行完成后运行 npm run build,打开网页:
2.2 解析Less
less-loader将less转换为css,然后接着上一小节继续
所以,我们可以猜到,less的rules规则一定是在use的最后面再加上 less-loader
npm install less less-loader -D
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader']
}
const path = require('path')
module.exports = {
entry: {
index: './src/index.js',
search: './src/search.js'
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js'
},
mode: 'production',
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader'
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader']
}
]
}
}
sass和less用法一样,可以类比,这里不再赘述
三、解析图片和字体
图片和字体都属于文件,file-loader用于处理文件,使用上简单。
3.1 解析图片
npm install file-loader -D
const path = require('path')
module.exports = {
entry: {
index: './src/index.js',
search: './src/search.js'
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js'
},
mode: 'production',
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader'
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader']
},
{
test: /\.(png|jpg|gif|jpeg)$/,
use: ['file-loader']
}
]
}
}
3.2 解析字体
图片和文件都是文件,使用上类似。
我们在项目中加一个otf字体文件
less文件:
@font-face {
font-family: 'SourceHanSerifSC-Heavy';
src: url('./images/SourceHanSerifSC-Heavy.otf') format('truetype');
}
.search-text {
font-size: 40px;
color: #FF0000;
font-family: 'SourceHanSerifSC-Heavy'
}
webpack 配置如下:
const path = require('path')
module.exports = {
entry: {
index: './src/index.js',
search: './src/search.js'
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js'
},
mode: 'production',
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader'
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader']
},
{
test: /\.(png|jpg|gif|jpeg)$/,
use: ['file-loader']
}, {
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: ['file-loader']
}
]
}
}
字体确实变了
3.3 解析文件的其他方式
url-loader 也可以处理图片和字体,可以设置较小资源自动base64
其实,url-loader,内核就是file-loader,但是在文件大小(单位byte)低于指定的限制时,可以返回一个DataURL
我们把之前file-loader的配置改成url-loader.
npm i url-loader -D
const path = require('path')
module.exports = {
entry: {
index: './src/index.js',
search: './src/search.js'
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js'
},
mode: 'production',
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader'
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader']
},
// {
// test: /\.(png|jpg|gif|jpeg)$/,
// use: ['file-loader']
// },
{
test: /\.(png|jpg|gif|jpeg)$/,
use: [{
loader: 'url-loader', options: {
limit: 102400}}]
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: ['file-loader']
}
]
}
}
试着比较一下base64之前和之后search.js的大小,没有转换成base64之前,search.js大小是134k,转换成base64之后,图片成了js的一部分,search.js大小是210k,之前的图片是57.4k,图片转换成base64,体积扩大1/3, 57.4k变为76k,之前的134+76 = 210k,很符合。
注:转为base64,扩大1/3的 原理如下:base64扩大三分之一
四、webpack文件监听
文件监听是在发现源码发生变化时,自动重新构建出新的输出文件
4.1 用法
webpack开启监听模式,有两种方法:
- 启动webpack命令时,带上 --watch 参数;
- 在配置webpack.config.js中设置 watch: true;
比如在package.json中,添加一项,但是有个缺陷:每次要手动刷新浏览器
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "webpack",
"watch": "webpack --watch"
}
npm run watch 之后
修改文件,保存,可以看到终端上出现了自动构建的打印,然后手动刷新浏览器,发现界面变化,终端始终处于监听状态。
4.2 监听原理
轮询机制,轮询判断文件的最后编辑时间是否发生变化,一开始,文件会有修改时间,这个时间会被存储起来,下次再有修改的时候,就会和上次存储的时间进行对比,如果不一样,它并不会立即告诉监听者,而是先把这个变化缓存起来,等待一个叫做 aggregateTimeout 的时间(默认是300ms),这个等待的时间,如果其他的文件也有变化,它会把这个变化的文件列表一起构建,最后把这个一起构建的结果生成到bundle文件里。
配置如下:
module.exports = {
//默认 false,也就是不开启
watch: true,
//只有开启监听模式时,watchOptions才有意义
wathcOptions: {
//默认为空,不监听的文件或者文件夹,支持正则匹配
ignored: /node_modules/,
//监听到变化发生后会等300ms再去执行,默认300ms
aggregateTimeout: 300,
//判断文件是否发生变化是通过不停询问系统指定文件有没有变化实现的,默认每秒问1000次
poll: 1000
}
}
五、webpack热更新及原理
前面我们已经看到,watch只能自动构建,却不能刷新,每次还要手动刷新。本节就是为了解决这个问题的。
热更新: webpack-dev-server , 又叫WDS
WDS 不刷新浏览器;
WDS 不输出文件,而是放在内存中;
使用 HotModuleReplacementPlugin插件;
注意,WDS不像watch有本地磁盘的I/O操作,WDS是放在内存中,所以构建速度很有优势。
5.1 使用
"scripts": {
"build": "webpack",
"watch": "webpack --watch",
"dev": "webpack-dev-server --open" // --open表示自动打开浏览器
}
因为这肯定是开发阶段才用的到,所以,mode要设置为"development",同时要使用一个webpack内置的插件。
npm run dev
自动打开浏览器
然后,修改文件,会发现浏览器自动刷新。
另外,热更新还可以不使用webpack-dev-sever,使用中间件:webapck-dev-middleware.
webpack-dev-middleware将webpack输出的文件传输给服务器,用于更灵活的定制场景,不过,要结合express框架一起使用。
5.2 原理
Webpack Compiler:编译器,将源代码编译成bundle.js,也就是最后打包好后的文件;
HMR Server:将热更新的文件输出给HMR Runtime;
Bundle Server:以服务器的形式提供文件再浏览器中访问,不然,你只能以文件的形式【file:///Users/guoyu/study/webpack/webpack-demo/dist/index.html】,但是BundleSever可以以localhost:8080的方式让你去访问。
HMR Runtime:开发过程打包的时候,被注入到浏览器bundle.js里成为bundle.js的一部分,这个HMR Runtime可以和HMR Server建立一个连接,是websocket实现的。
5.3 过程
分为两个过程:
**1,启动阶段:**文件系统中将你的源码通过webpack compiler编译成bundle.js, 把bundle.js传输到BundleServer(服务器),BundleSever以服务器的形式让浏览器能够访问到bundle.js。如下所示
1 -> 2 -> A -> B
**2,文件更新:**源码发生变化,还是通过 Webpack Compiler 编译,再将编译好的代码发送给 HMR Server, HMR Server可以知道哪些模块发生了改变,HMR Server(服务端)会通知 客户端(浏览器)的HMR Runtime,告知哪些文件发生了变化(以json形式通知),然后HMR Runtime就会更新代码,无须刷新浏览器。
六、文件指纹
6.1 什么是文件指纹
打包输出的文件名后缀!
上图中6位字符就是文件指纹。
文件指纹做版本的管理,文件发布的时候,有的文件修改了,有的文件没修改,只发布修改的文件即可,未修改的文件,指纹不变。没有修改的文件,可以持续用浏览器的缓存。
6.2 文件指纹有哪几种
**Hash:**和整个项目构建有关,同一次构建过程中生成的hash都是一样的,只要项目文件有改动,整个项目构建的hash值就会改变,如果出口output的是hash,那么一旦针对项目中任何一个文件的修改,都会构建整个项目,重新获取hash值,缓存就会失效。
**Chunkhash:**采用hash是跟整个项目构建都有关系,每次构建后生成的hash不一样,即使文件内容没变化,这样是无法实现缓存效果,我们需要另外一种chunkhash。和webpack打包的chunk有关,不同的entry会生成不同的chunkhash值;chunk其实就是webpack打包的模块,A,B页面,A页面的JS变化了,打包之后,B页面JS也会发生变化,这其实是没必要的。这里就有chunkhash的概念,chunk就是模块,不同的entry生成不同的chunk,不同的entry保持他们的chunk独立就可以了,这样使用的chunkhash,每个页面,有一个文件发生变化,不会影响其他页面。chunkhash和hash不一样,它根据不同的文件入口entry进行依赖文件解析,构建对应的chunk,生成对应的hash,我们在生产环境里把一些公共库和程序入口文件区分开,单独打包构建,接着我们采用chunkhash的方式生成hash值,只要我们不改动公共库的代码,就可以保证hash不收影响。比如,采用chunkhash,项目主入口文件main.js及其对应的依赖文件main.css由于被打包在同一个模块,所以共用相同的chunkhash,但是公共库由于是不同的模块,所以有单独的chunkhash,这就保证线上构建的时候只要文件内容没改变就不会重复构建。
Contenthash:根据文件内容来定义hash,文件内容不变,则contenthash不变,如果对css使用了chunkhash之后,它与依赖它的chunk共用chunkhash,测试后会发现,css与js文件名的chunkhash值是一样的,如果我修改了js文件,js的hash值会变化,css的文件名的hash还是和变化后的js文件的hash值一样,如果我修改了css文件,也会导致重新构建,css的hash值和js的hash值还是一样的,即使js文件没有被修改。这样会导致缓存作用失效,所以css文件最好使用contenthash。
总结:JS文件用chunkhash、CSS文件用contenthash、字体图片等文件用hash即可(这里的hash不是上面讲的全局hash)
6.3 JS文件设置文件指纹
设置output的filename,使用[chunkhash],对于js而言,很简单
6.4 CSS文件设置文件指纹
要使用contenthash,正常情况下,如果我们使用了css-loader和style-loader,css会由style-loader插入到style里,并且放到head头部,这个时候并没有独立出CSS文件,这时,我们通常采用MiniCSSExtractPlugin这个插件将CSS提取成一个独立的文件,所以,文件指纹设置在MiniCssExtractPlugin里面的filename字段下,并且使用contenthash。
6.5 图片文件指纹
设置file-loader的那么,使用hash,这里hash是md5生成的,和前面讲的hash不是同一个概念
6.6 举例
比如我们要创建hash,我们需要在生产环境中,因为webpack的chunkhash是没法和热更新一起使用的,没办法和HotModuleReplacementPlugin一起使用的,所以,我们创建一个新的配置webpack.prod.js,生产环境不需要代码热更新,需要把热更新那部分去掉。
"scripts": {
"build": "webpack --config webpack.prod.js",
"watch": "webpack --watch",
"dev": "webpack-dev-server --config webpack.dev.js --open"
}
npm run build
上面并没有设置CSS文件指纹,下面做一个css文件指纹。要CSS文件指纹,肯定需要css独立出来,但是之前都是通过style-loader将CSS文件插入到head中,并没有形成独立的css 文件。所以要通过插件将CSS提取出来。
npm install mini-css-extract-plugin -D
npm run build
附webpack.prod.js 和 webpack.dev.js
const path = require('path')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
module.exports = {
entry: {
index: './src/index.js',
search: './src/search.js'
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name]_[chunkhash:8].js'
},
mode: 'production',
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader'
},
{
test: /\.css$/,
use: [
// 'style-loader',
MiniCssExtractPlugin.loader,
'css-loader'
]
},
{
test: /\.less$/,
use: [
// 'style-loader',
MiniCssExtractPlugin.loader,
'css-loader',
'less-loader'
]
},
{
test: /\.(png|jpg|gif|jpeg)$/,
use: [{
loader: 'file-loader',
options: {
name: '[name]_[hash:8].[ext]'
}
}]
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: [{
loader: 'file-loader',
options: {
name: '[name]_[hash:8].[ext]'
}
}]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name]_[contenthash:8].css'
})
]
}
const path = require('path')
const webpack = require('webpack')
module.exports = {
entry: {
index: './src/index.js',
search: './src/search.js'
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name].js'
},
mode: 'development',
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader'
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.less$/,
use: ['style-loader', 'css-loader', 'less-loader']
},
{
test: /\.(png|jpg|gif|jpeg)$/,
use: [{
loader: 'url-loader',
options: {
limit: 102400}
}]
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: ['file-loader']
}
]
},
plugins: [
new webpack.HotModuleReplacementPlugin(),
],
devServer: {
contentBase: './dist',
hot: true
}
}
七、代码压缩
7.1 JS压缩
webpack4.x内置了uglifyjs-webpack-plugin插件,我们基本不用管了,当然你也可以使用其他压缩插件,然后自行配置。只要你的 mode 设置为 production,默认是开启uglifyjs-webpack-plugin的,js默认就压缩了。
7.2 CSS压缩
在webpack3.x里,可以在css-loader里面对CSS进行压缩,但是到了webpack4.x里面,css-loader不再支持css的压缩功能,我们通过插件来填补这个功能。
使用 optimize-css-assets-webpack-plugin同时配合 cssnano(css的一个预处理器)
plugins: [
new OptimizeCSSAssetsPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require('cssnano')
})
]
npm install optimize-css-assets-webpack-plugin cssnano -D
const path = require('path')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
// 这里是重点
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
module.exports = {
entry: {
index: './src/index.js',
search: './src/search.js'
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name]_[chunkhash:8].js'
},
mode: 'production',
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader'
},
{
test: /\.css$/,
use: [
// 'style-loader',
MiniCssExtractPlugin.loader,
'css-loader'
]
},
{
test: /\.less$/,
use: [
// 'style-loader',
MiniCssExtractPlugin.loader,
'css-loader',
'less-loader'
]
},
{
test: /\.(png|jpg|gif|jpeg)$/,
use: [{
loader: 'file-loader',
options: {
name: '[name]_[hash:8].[ext]'
}
}]
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: [{
loader: 'file-loader',
options: {
name: '[name]_[hash:8].[ext]'
}
}]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name]_[contenthash:8].css'
}),
// 这里是重点
new OptimizeCSSAssetsPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require('cssnano')
})
]
}
7.3 HTML压缩
不得不提的一个插件:html-webpack-plugin,设置压缩参数。这个插件在webpack的世界是王者般的存在。
plugins: [
new HtmlWebpackPlugin({
// html模板所在的文件路径
// 根据自己的指定的模板文件来生成特定的 html 文件。
// 这里的模板类型可以是任意你喜欢的模板,可以是 html, jade, ejs, hbs, 等等,
// 但是要注意的是,使用自定义的模板文件时,需要提前安装对应的 loader
// 否则webpack不能正确解析
template: path.join(__dirname, 'src/search.html'),
// 打包出来的文件名称
filename: 'search.html',
// 指定生成的html要用哪些chunk
chunks: ['search'],
//
inject: true,
minify: {
html5: true,
collapseWhitespace: true,
preserveLineBreaks: false,
minifyCSS: true,
minifyJS: true,
removeComments: false
}
})
]
npm install html-webpack-plugin -D
全文配置:
const path = require('path')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
const HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
entry: {
index: './src/index.js',
search: './src/search.js'
},
output: {
path: path.join(__dirname, 'dist'),
filename: '[name]_[chunkhash:8].js'
},
mode: 'production',
module: {
rules: [
{
test: /\.js$/,
use: 'babel-loader'
},
{
test: /\.css$/,
use: [
// 'style-loader',
MiniCssExtractPlugin.loader,
'css-loader'
]
},
{
test: /\.less$/,
use: [
// 'style-loader',
MiniCssExtractPlugin.loader,
'css-loader',
'less-loader'
]
},
{
test: /\.(png|jpg|gif|jpeg)$/,
use: [{
loader: 'file-loader',
options: {
name: '[name]_[hash:8].[ext]'
}
}]
},
{
test: /\.(woff|woff2|eot|ttf|otf)$/,
use: [{
loader: 'file-loader',
options: {
name: '[name]_[hash:8].[ext]'
}
}]
}
]
},
plugins: [
new MiniCssExtractPlugin({
filename: '[name]_[contenthash:8].css'
}),
new OptimizeCSSAssetsPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require('cssnano')
}),
// 这里多页面,需要写两个
new HtmlWebpackPlugin({
template: path.join(__dirname, 'src/index.html'),
filename: 'index.html',
chunks: ['index'],
inject: true,
minify: {
html5: true,
collapseWhitespace: true,
preserveLineBreaks: false,
minifyCSS: true,
minifyJS: true,
removeComments: false
}
}),
new HtmlWebpackPlugin({
template: path.join(__dirname, 'src/search.html'),
filename: 'search.html',
chunks: ['search'],
inject: true,
minify: {
html5: true,
collapseWhitespace: true,
preserveLineBreaks: false,
minifyCSS: true,
minifyJS: true,
removeComments: false
}
})
]
}
所谓模板,就是你写个样板html,生成的HTML文件就是按照你的样板生成,只不过插件会把生成的CSS文件、js文件插入到样板中。
样板如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="root"></div>
</body>
</html>
打包生成的html文件如下:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<link href="search_a3f6adeb.css" rel="stylesheet">
</head>
<body>
<div id="root"></div>
<script src="search_5b056385.js">
</script>
</body>
</html>
八、自动清理构建目录产物
每次构建的时候不会清理上次的dist目录,造成构建的输出目录output文件越来越多。
你是不是想过
rm -rf ./dist && webpack
rimraf ./dist && webpack
但这样不优雅,我们可以使用一个插件:clean-webpack-plugin,该插件默认删除output指定的输出目录
plugins: [new CleanWebpackPlugin()]
npm install clean-webpack-plugin -D
const {
CleanWebpackPlugin } = require('clean-webpack-plugin')
plugins: [new CleanWebpackPlugin()]
如果不使用这个插件,dist目录里的输出文件越来越多,并不会删除之前打包出来的构建文件,使用后,都是先删除,再生成。