webpack学习笔记

webpack学习笔记

由于上篇文章 在 angular6 中自定义 webpack 配置 中,需要在 angular6 中自定义 webpack 设置,所以在这里重新学习一下 webpack,主要是写这个笔记,方便以后自己回顾。

本篇文章是我根据 B 站上的webpack 教学视频10天搞定webpack4,边看边写的,有兴趣的也可以看看。

注意:跟随视频敲代码的过程中,有很多包版本不对就会各种报错,我总结了一下,由于 webpack从4更新到 5 是两年前的事情,那么包就用三年前的问题应该不大

1. webpack的作用

webpack 可以实现代码转换,文件优化,代码分割,模块合并,自动刷新,代码校验,自动发布

2. webpack 安装

先全局安装 webpack,由于 webpack4 以上的版本要使用相关命令的话,必须安装 webpack-cli

npm i webpack@4.28.2 -g
npm i webpack-cli@3.1.2 -g

上面的版本我们就按照视频中的来,保持一致
先创建文件夹,在文件夹中初始化一下(生成 package.json),再本地安装 webpack webpack-cli,还需要本地安装是为了方便使用 require 引入

npm init -y
npm intall webpack@4.28.2 webpack-cli@3.1.2 -D

安装成功后,webpack 可以进行 0 配置,但这没有意义,功能很弱。
在根目录下创建 src 文件夹,里面添加 index.js 文件,写一句 console.log('webpack4-notes') 之后可以运行 webpack 来打包,生成 dist 文件,结果如下:
打包结果

其原理是,运行 webpack 命令时,会去找 node_modules/.bin/webpack.cmd 文件:
webpack.cmd
webpack.js

3. 自定义webpack

3.1 简单配置–入口和输出

默认配置文件是 webpack.config.js ,所以在根目录下创建 webpack.config.js 文件:

//webpack 是node 写出来的,是node 的写法
let path = require('path')
module.exports = {
	mode:'development',    //模式,默认两种:production--生产模式,development--开发模式
	entry:'./src/index.js',   //入口文件
	output:{
		filename:'bundle.js',    //打包后的文件名
		// path:'绝对路径',    //打包后的文件位置:路径必须是一个绝对路径
		path:path.resolve(__dirname,'dist'),    //用到了内置的 path 模块,需要导入;path.resolve() 可以把相对路径解析成绝对路径,意思是 以当前目录解析出一个 dist 目录,将打包后的 bundle.js 放到 dist 里面去;__dirname 总是指向被执行 js 文件的绝对路径
	}
}

完成后,就可以直接运行 webpack 进行打包了,此命令会去找 webpack.config.js 文件,打包结果如下:
使用 wenpack.config.js 配置打包
此时想要查看打包后的结果是否能运行,就在 dist 目录下新建 index.html,然后在浏览器中打开,如下图:
查看打包结果
浏览器中的查看结果:
浏览器中查看结果

3.2 修改自定义 webpack 文件名称

如果你的配置文件不想叫 webpack.config.js 这个默认的名字,还可以自己改,比如 webpack.config.my.js,如果还是运行 webpack的话,就还是得到最开始的默认最弱的那个配置,结果是 main.js,想要使用我们的 webpack.config.my.js,就要指定配置文件,webpack --config webpack.config.my.js

3.3 本地查看打包后的效果(webpack-dev-server,html-webpack-plugin)

像我们上面这样查看打包的结果太麻烦了,每次改动后都要重新打包,还有手动添加 index.html
太麻烦了,所以我们可以使用 webpack 内置的服务 webpack-dev-server,将打包后的结构丢到一个服务中,我们可以直接访问这个服务来查看结果

// 安装 webpack-dev-server 版本与视频一致
npm i webpack-dev-server@3.1.14 -D

视频中直接运行 webpack-dev-server 就可以了,但是我不行,就算重新安装包还是不行,在 package.json 中配置命令才行
在 package.json 中配置 build 和 dev 命令

之后运行 npm run dev,就可以了,结果如下:

npm run dev 运行结果
在浏览器中打开 http://localhost:8080/
浏览器查看 dev 的服务
这不是我们想要的结果,他是直接进到了当前目录,但是我们是想进入到 dist 文件夹中,可以直接访问里面的 index.html,所以还需要在 webpack.config.js 中加上 devServer 配置

//webpack 是node 写出来的,是node 的写法
let path = require('path')
module.exports = {
	devServer:{    //开发服务器的配置
		port:3000,    //指定端口
		progress:true,    //显示进度条
		contentBase:"./dist",    //以 dist 为静态服务,也就是访问 http://localhost:3000/时进入 dist 目录,就直接访问到了里面的 index.html 了
		compress:true    //启动压缩
	},
	mode:'development',    //模式,默认两种:production--生产模式,development--开发模式
	entry:'./src/index.js',   //入口文件
	output:{
		filename:'bundle.js',    //打包后的文件名
		// path:'绝对路径',    //打包后的文件位置:路径必须是一个绝对路径
		path:path.resolve(__dirname,'dist'),    //用到了内置的 path 模块,需要导入;path.resolve() 可以把相对路径解析成绝对路径,意思是 以当前目录解析出一个 dist 目录,将打包后的 bundle.js 放到 dist 里面去;__dirname 总是指向被执行 js 文件的绝对路径
	}
}

再来运行一次 npm run dev,结果如下:直接访问dist里面的 index.html
直接访问dist里面的 index.html

但是 dist 里面的 index.html 是我们手动加的,一般打包可能没有这个文件,不可能我们每次都手动加上这个文件然后再看效果吧,我们希望打包后能自动生成这个文件,此时我们需要一个插件(html-webpack-plugin),作用是能帮我们把打包后的文件通过 script 标签的方式引入到我们项目自带的 index.html 中,并且把改造后的 index.html 直接放到打包(contentBase)后文件目录下
安装插件:

npm i html-webpack-plugin -D

这里安装的是 “html-webpack-plugin”: “^5.3.2”,然后运行命令报错了:
html-webpack-plugin版本不对导致报错
将版本替换为 4.4.1的就好了

npm uninstall html-webpack-plugin -D
npm i html-webpack-plugin@4.4.1 -D

再添加插件配置:

//webpack 是node 写出来的,是node 的写法
let path = require('path')
let HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
	devServer:{    //开发服务器的配置
		port:3000,    //指定端口
		progress:true,    //显示进度条
		contentBase:"./dist",    //以 dist 为静态服务
		compress:true    //启动压缩
	},
	mode:'development',    //模式,默认两种:production--生产模式,development--开发模式
	entry:'./src/index.js',   //入口文件
	output:{
		filename:'bundle.js',    //打包后的文件名
		// path:'绝对路径',    //打包后的文件位置:路径必须是一个绝对路径
		path:path.resolve(__dirname,'dist'),    //用到了内置的 path 模块,需要导入;path.resolve() 可以把相对路径解析成绝对路径,意思是 以当前目录解析出一个 dist 目录,将打包后的 bundle.js 放到 dist 里面去;__dirname 总是指向被执行 js 文件的绝对路径
	},
	plugins:[   //插件--数组形式,放着所有的 webpack 插件
		new HtmlWebpackPlugin({
			template:'./src/index.html',   //以该文件为模板,所以要在 src 中新建 index.html 文件
			filename:'index.html',    //打包后要放在打包目录(dist)下的文件的名字
		})
	]
}

删除之前的 dist 文件夹后,再运行 npm run build,就可以看到,dist 中自动就有 index.html 文件,运行npm run dev 后,内容区修改的话会自动更新的,但配置文件修改了就得重新运行了

3.4 生产环境打包–压缩代码,hash

接下来我们想如果在生产环境打包,代码要压缩,而且 打包后的目录下的 index.html 文件也要压缩
build运行后有 index.html

打包后有 index.html
还可以添加其他的配置:压缩,hash

//webpack 是node 写出来的,是node 的写法
module.exports = {
	plugins:[   //插件--数组形式,放着所有的 webpack 插件
		new HtmlWebpackPlugin({
			template:'./src/index.html',   //以该文件为模板
			filename:'index.html',    //打包后要放在打包目录下的文件的名字
			minify:{
				removeAttributeQuotes:true,    //删除 index.html 中属性的双引号(部分属性值带逗号的删不掉)
				collapseWhitespace:true,   //折叠,代码变成一行
			},
			hash:true,    //index.html 中引入的 bundle.js 带 hash 戳,避免缓存(dist文件下的 bundle.js 名字不变,不带hash)
		})
	]
}

一行显示,去双引号,引入bundle.js时带hash

如果想所有打包后生成的文件都能带上 hash,就要直接在 output.filename 中配置:

output:{
		filename:'bundle.[hash].js',    //打包后的bundle.js,带有 hash,比如 bundle.5c5c0e9b436ca27579xxx.js,每次有更新后的打包都可以生成不一样的名字,避免别覆盖
		// filename:'bundle.[hash:8].js'  //只显示 8 位的 hash 值,比如 bundle.1cfb6be1.js
	},

打包后的文件名中带 hash

3.5 支持 css,less,sass,及优化等

3.5.1 解析 css

我们现在 src 下创建一个 index.css,里面写一点样式,然后在 js 中引入此样式。有人问为什么不在 src\index.html 中引入样式呢,因为 index.html 是打包时的模板,这个模板是要原封不动的输出到 dist 中去的:
在 index.html中引入css
在 index.html中引入css,打包后的结果
但是我们也可以看到,在 dist 文件里面是没有 index.css的,在 npm run dev 后,会报错:
报错
所以不能在 html 中进行引入,而在 js 中通过 require 来,比如 require(’./index.css’):
index.js中引入css
但是由于 webpack 默认只能识别 javaScript,不识别css,所以又报错了:
解析css失败
提示需要一个合适的 loader 去解析这个模块,安装

npm i css-loader@2.1.0 style-loader@0.23.1 -D  //照旧根据视频中的版本来,自己尝试随意安装就是报错

loader的作用:就是将我们的源代码进行转化,变成 webpack 能识别的代码

//webpack 是node 写出来的,是node 的写法
let path = require('path')
let HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
	mode:'development',    //模式,默认两种:production--生产模式,development--开发模式
	entry:'./src/index.js',   //入口文件
	output:{
		filename:'bundle.js',    //打包后的文件名
		path:path.resolve(__dirname,'dist'),    
	},
	plugins:[  //省略,后面会有个配置的小结
	],
	module:{    //模块
		rules:[   //规则
			{   
				test:/\.css$/,     //找到以 css 为后缀的文件
				use:['style-loader','css-loader']  //先执行 css-loader,再执行 style-loader
			}
		]
	}
}
  • loader的特点:作用单一;
  • loader的用法(也就是 use 的写法):
    • 1.可以是字符串,那就只能用一个 loader ,比如 use:'css-loader'
    • 2.使用多个loader时, 只能是一个数组,比如 use:['style-loader','css-loader']
    • 3.如果有其他配置的话,loader还可以是个对象,比如 use:[{loader:'style-loader',options:{}},'css-loader']
  • loader的顺序:从右向左执行,从下往上执行
    为了体现上述部分特点,我们创建一个 a.css 将之引入到 index.css 中去:
    css的引入

npm run dev 运行结果:
css生效

上述我们打包后的样式时直接插在 head 标签的最底部,如果我们想要我们在 index.html 中自己写的样式生效,可以改变我们打包后 css 样式插入的位置

//webpack 是node 写出来的,是node 的写法
let path = require('path')
let HtmlWebpackPlugin = require('html-webpack-plugin')
module.exports = {
	module:{    //模块
		rules:[   //规则
			{   
				test:/\.css$/,     //找到以 css 为后缀的文件
				use:[
					{
						loader:'style-loader',
						options:{
							insertAt:'top'
						}
					},
					'css-loader'
				]  //先执行 css-loader,再执行 style-loader
			}
		]
	}
}

npm run dev 运行结果:
改变解析后css的插入位置

3.5.2 解析 less,sass,stylus

我们可以在 src 中新建一个 index.less 文件,写一点样式:
less文件样式
然后也在 index.js 中引入:

require('./index.less')

安装处理 lessloader

npm i less@3.9.0 less-loader@4.1.0 -D

webpack.config.js 中进行配置


	module:{    //模块
		rules:[   //规则
			{     
				
				test:/\.css$/,     //找到以 css 为后缀的文件
				use:[
					{
						loader:'style-loader',
						options:{
							insertAt:'top'  //将解析后的 css 插入到 head 标签的最上方
						}
					},
					'css-loader'
				]  //先执行 css-loader,再执行 style-loader
			},
			{    // 解析 less 文件
				test:/\.less$/,     //找到以 less 为后缀的文件
				use:[
					{
						loader:'style-loader', 
						options:{
							insertAt:'top'  //将解析后的 css 插入到 head 标签的最上方
						}
					},
					'css-loader',  //解析 css 各种语法
					'less-loader'  //将 less 转化为 css 
				]  //先执行 less-loader,再执行 css-loader,最后执行 style-loader
			}
		]
	}

结果如下:
less效果
同理,sass 和 stylus 预处理器的解析都是一样的,需要 node-sass,sass-loader 和 stylus,stylus-loader

3.5.3 抽离 css (mini-css-extract-plugin)

我们上面解析后的样式文件全都放在了 head 标签里面,多了之后容易阻塞加载,所以想把解析后的样式文件单独处理出来,不放在 head 标签中了,为此有个插件可以帮助我们实现

安装抽离 css 的插件

npm i mini-css-extract-plugin@0.5.0 -D
let path = require('path')  //绝对路径需要
let HtmlWebpackPlugin = require('html-webpack-plugin')  //模板插件
let MiniCssExtractPlugin = require('mini-css-extract-plugin')  //抽离 css 的插件
module.exports = {
	plugins:[  
		new MiniCssExtractPlugin({    //作用:抽离 css
			filename:'main.css'  //抽离出来的 css 文件放在 这个文件中;
			//那么我们还要决定哪些 css 文件,要放到这里来,
			//所以 MiniCssExtractPlugin 提供了一个 loader,作用就跟 style-loader 是类似的
			//可以在解析样式文件时,不使用 style-loader,而使用这个 MiniCssExtractPlugin.loader
		})
	],
	module:{    //模块
		rules:[   //规则
			{   
				test:/\.css$/,     //找到以 css 为后缀的文件
				use:[
					MiniCssExtractPlugin.loader,  //解析css之后将文件放在 MiniCssExtractPlugin 配置的文件中去
					/* {  //这里我们不再把样式放在 head 标签中,而是放到 main.css 中
						loader:'style-loader',
						options:{
							insertAt:'top'  //将解析后的 css 插入到 head 标签的最上方
						}
					}, */
					'css-loader'
				]  //先执行 css-loader,再执行 style-loader
			},
			{    // 解析 less 文件
				test:/\.less$/,     //找到以 less 为后缀的文件
				use:[
					{
						loader:'style-loader',  //将解析后的文件插入到打包后的 index.html 中的 head 标签中
						options:{
							insertAt:'top'  //将解析后的 css 插入到 head 标签的最上方
						}
					},
					'css-loader',  //解析 css 各种语法
					'less-loader'  //将 less 转化为 css 
				]  //先执行 less-loader,再执行 css-loader,最后执行 style-loader
			}
		]
	}
}

从上面的配置可以看到,我们把 css 抽离到 main.css 中去了,但是,less 文件没有抽离,还是放在了 head 中,npm run dev 运行结果如下:
抽离css成功
打包结果如下:
抽离 css 后的打包结果
那么再扩展一下,我们想抽离到不同的文件中去,css文件抽离到 main.css 中,less文件抽离到 home.css 中,那么可以再加一个插件配置:

let MiniCssExtractPlugin = require('mini-css-extract-plugin')  //抽离 css 的插件
let MiniCssExtractPlugin2 = require('mini-css-extract-plugin')  //将 less 抽离到另一个文件中
module.exports = {
	plugins:[  
		new MiniCssExtractPlugin({    //作用:抽离 css
			filename:'main.css'  //抽离出来的 css 文件放在 这个文件中;
		}),
		new MiniCssExtractPlugin2({    //作用:抽离 css
			filename:'home.css'  //抽离出来的 css 文件放在 这个文件中;
		})
	],
	module:{    //模块
		rules:[   //规则
			{   
				test:/\.css$/,     //找到以 css 为后缀的文件
				use:[
					MiniCssExtractPlugin.loader,  //解析css之后将文件放在 main.css 中去
					'css-loader'
				]
			},
			{    // 解析 less 文件
				test:/\.less$/,     //找到以 less 为后缀的文件
				use:[
					MiniCssExtractPlugin2.loader,   //将解析的 less 文件抽离到 home.css 中
					'css-loader',  //解析 css 各种语法
					'less-loader'  //将 less 转化为 css 
				] 
			}
		]
	}
}

效果如下:
less文件抽离到另一个文件中去

3.5.4 解析后的样式自动加上浏览器前缀(postcss-loader,autoprefixer)

我们写一点有兼容性的样式
兼容性样式代码
效果:
样式生效
为此要安装插件:

npm i postcss-loader@3.0.0 autoprefixer@9.5.0 -D  //这两个版本是我自己找的,不知道视频是啥

然后在解析 css 之前就上这个前缀就行了

rules:[   //规则
			{   
				test:/\.css$/,     //找到以 css 为后缀的文件
				use:[
					MiniCssExtractPlugin.loader,  //解析css之后将文件放在 main.css 中去
					'css-loader',
					'postcss-loader'  //给css代码加上各种浏览器前缀
				]
			},
			{    // 解析 less 文件
				test:/\.less$/,     //找到以 less 为后缀的文件
				use:[
					MiniCssExtractPlugin2.loader,   //将解析的 less 文件抽离到 home.css 中
					'css-loader',  //解析 css 各种语法
					'postcss-loader',  //给css代码加上各种浏览器前缀
					'less-loader'  //将 less 转化为 css 
				] 
			}
		]

再打包还是报错:
报错
缺少文件,那就创建一个 postcss.config.js
创建 postcss.config.js
之后再运行,倒是不报错了,但是打包后的样式文件中没有生成前缀啊,视频中倒是没出现这个问题,在网上找到的解决方法:解决webpack4.x使用autoprefixer 无效
在package.json中加 browserlist
再试一次:
前缀生成了

3.5.5 将解析出来的 css 文件进行压缩(optimize-css-assets-webpack-plugin)

上面我们打包出来的只有 js 文件压缩了,但是 css 文件没有,接下来,就将css文件也压缩一下

安装插件

npm i optimize-css-assets-webpack-plugin@3.2.1 -D

然后新增配置:

let OptimizeCss = require('optimize-css-assets-webpack-plugin')  //压缩 css
module.exports = {
	mode:'production',    //模式,默认两种:production--生产模式,development--开发模式
	optimization: {    //优化--压缩 css 文件
		minimizer:[
			new OptimizeCss()
		]
	},
}

打包之后,css确实成一行了:
css压缩了

3.6 压缩 js

3.6.1 压缩 js(uglifyjs-webpack-plugin)

根据 3.5.5 的效果,css优化后, js 反而没有压缩了,变回去了:
js没有压缩了
所以说,如果压缩了 css ,那么就必须要写上压缩 js 的,插件是 uglifyjs-webpack-plugin

npm i uglifyjs-webpack-plugin@2.2.0 -D
let UglifyjsWebpackPlugin = require('uglifyjs-webpack-plugin')  //压缩 js
module.exports = {
	mode:'production',    //模式,默认两种:production--生产模式,development--开发模式
	optimization: {    //优化
		minimizer:[
			new UglifyjsWebpackPlugin({    //压缩 js
				cache:true,    //有缓存
				parallel:true,    //是并发打包
				sourceMap:true    //
			}),
			new OptimizeCss()    //压缩 css 文件
		]
	},
}

打包结果:
js也压缩了

3.6.2 转化 js
3.6.2.1 babel-loader, @babel/core, @babel/preset-env

我们之前都是直接写的 es5 的代码,现在我们来写 es6 的语法,比如箭头函数:
es6语法
开发模块下打包依旧是 es6的语法:
打包后依旧是 es6
那么我们想把 es6 转化为 es5,这个时候,我们需要用到 babel
安装:
babel-loader:转化的加载器;@babel/core:核心模块,可以调用方法实现转化;@babel/preset-env:可以把高级的语法转化成低级的语法;

npm i babel-loader@8.0.4 @babel/core@7.2.2 @babel/preset-env -D

增加 rules:

rules:[   //规则
			{    // 解析 js 文件
				test:/\.js$/,     //找到以 js 为后缀的文件
				use:[
					{
						loader:'babel-loader',  
						options:{    //用 babel-loader 把 es6 转化为 es5
							presets:[    //预设
								'@babel/preset-env'    //调用此模块来进行转化
							]
						}
					}
				]
			}
		]

执行打包操作:
es6转化成es5

3.6.2.1 @babel/plugin-proposal-decorators

我们还可以写个更高级的语法试试:(下面代码报错因为只要在vscode首选项中设置一下就行)
装饰器
然后就报错了:
dev报错
可以去babel官网去找这个插件,看怎么用的,这还是提案中的语法,官网直接有代码:
babel官网代码

npm i @babel/plugin-proposal-decorators@7.2.3 -D
rules:[   //规则
			{    // 解析 js 文件
				test:/\.js$/,     //找到以 js 为后缀的文件
				use:[
					{
						loader:'babel-loader',  
						options:{    //用 babel-loader 把 es6 转化为 es5
							presets:[    //预设
								'@babel/preset-env'    //调用此模块来进行转化
							],
							plugins: [
								["@babel/plugin-proposal-decorators", { "legacy": true }],
								["@babel/plugin-proposal-class-properties"]
							]
						}
					}
				]
			}
		]

之后就可以了:
成功转化了装饰器

3.6.2.3 @babel/plugin-transform-runtime,@babel/runtime

然后再高级一点:
a.js
index.js中引入a.js
打包及运行后:
打包后代码好像是转化了
还是报错
这里我们还需要另一个包来进行转化
babel关于这个包的信息

npm i @babel/plugin-transform-runtime@7.2.0 -D
npm i @babel/runtime@7.2.0 -S    //这个包生产模式也需要
rules:[   //规则
			{    // 解析 js 文件
				test:/\.js$/,     //找到以 js 为后缀的文件
				use:[
					{
						loader:'babel-loader',  
						options:{    //用 babel-loader 把 es6 转化为 es5
							presets:[    //预设
								'@babel/preset-env'    //调用此模块来进行转化
							],
							plugins: [
								["@babel/plugin-proposal-decorators", { "legacy": true }],
								["@babel/plugin-proposal-class-properties"],
								"@babel/plugin-transform-runtime"
							]
						}
					}
				],
				include:path.resolve(__dirname,'src'),  //只解析 src 中的 js 文件
				exclude:/node_modules/  //排除掉 node_modules 中的js文件
			}
		]
3.6.2.4 @babel/polyfill

还有一个问题,实例上的方法都不会去解析,比如:
实例中的方法
打包后没有解析成 es5
没有解析
这里要用到 @babel/polyfill

npm i @babel/polyfill@7.2.5 -S  //也需要到生产环境

引入此模块
打包后就解析了
解析了
另外,视频中没有报错的但是我报错的地方,就是 a.js 中的导出报错了:

Uncaught TypeError: Cannot assign to read only property’exports‘ of object’#[Object]
// module.exports = 'abcd'  //不这样写,改成下面的
const a = 'abcd'
export default a
3.6.3 js 校验

写代码的时候加个校验器,来校验代码是否规范,使用 ESLint,可以进入官网看 demo
eslint 官网demo
官网下载的 eslintrc.json 文件,但是也说明
.eslintrc.json
所以自己修改下文件名,加个 .,然后复制到根目录下:
.eslintrc.json复制到根目录下
接下来再去改配置:

npm i eslint@5.12.0 eslint-loader -D
rules:[   //规则
			//js 一般应该是先校验,再解析转化 js,所以这个 eslint-loader 应该写在 babel-loader 的下面,或者可以用 enforce 来配置
			{    // 校验 js 语法
				test:/\.js$/,     //找到以 js 为后缀的文件,校验语法规范
				use:[
					{
						loader:'eslint-loader',
						options:{
							enforce:'pre'  //默认值: 'normal';还可以是 'post':在nornal之后;或者 'pre':执行强制在 normal 的loader 之前执行 
						} 
					}
				]
			},
			{    // 解析 js 文件
				test:/\.js$/,     //找到以 js 为后缀的文件
				use:[
					{
						loader:'babel-loader',  
						options:{    //用 babel-loader 把 es6 转化为 es5
							presets:[    //预设
								'@babel/preset-env'    //调用此模块来进行转化
							],
							plugins: [
								["@babel/plugin-proposal-decorators", { "legacy": true }],
								["@babel/plugin-proposal-class-properties",{"loose":true}],
								"@babel/plugin-transform-runtime"
							]
						}
					}
				],
				include:path.resolve(__dirname,'src'),  //只解析 src 中的 js 文件
				exclude:/node_modules/  //排除掉 node_modules 中的js文件
			},
		]

之后再打包的时候,就会看到各种报错:

eslint校验错误
当然了,写代码的时候我们不会去开这个校验,所以注释掉就行,所以才会把这个 eslint-loader 单独写,而不是和 babel-loader 放在一起

4. 全局变量引入问题

4.1 内联 loader(expose-loader)暴露到 window 上

比如说,项目中要用到 jquery 等第三方包

npm i jquery -S

使用 jquery
$并没有暴露给 window

我们希望把 $ 暴露给 window ,所以我们可以使用另一个 loaderexpose-loader–暴露全局的loader,这个可以直接在代码中使用,是内联的loader

上一节我们提到过 enforce 参数中的三种 loader,分别是 前面执行的(pre),普通的(normal),后置的(post),还有一种是内联的,写法如下:

npm i expose-loader@0.7.5 -D
import $ from 'expose-loader?$!jquery';
//上面的意思是,将 jquery 作为 $ 暴露给全局
console.log('[ $ ] >', $)
console.log('[ window.$ ] >', window.$)

运行 npm run dev 之后结果如下:
将 $ 暴露给去全局

4.2 不使用内联 loader

如果不想使用内联loader,那就还是写到 webpack.config.js 中去:
代码中引入还是以前的写法:import $ from 'jquery';

rules:[   //规则
			{    // 校验 js 语法,一般我们开发过程中不会去校验,注释掉
				test:require.resolve('jquery'),     //当代码中引入了 jquery
				use:[
					{
						loader:'expose-loader?$',
					}
				]
			},
		]

打印结果如上图,我这就不贴了

4.3 ProvidePlugin 给每个模块提供 $

如果我还是嫌麻烦,不想要 import $ from 'jquery'; ,那么可以在每个模块中注入 $ 对象

// import $ from 'jquery';
// import $ from 'expose-loader?$!jquery';
console.log('[ $ ] >', $)
console.log('[ window.$ ] >', window.$)
let Webpack = require('webpack')    //在每个模块中注入全局变量
plugins:[   
		new Webpack.ProvidePlugin({    //webpack 提供插件,作用是:在每个模块中都注入 $
			$:'jquery'
		})

	],
rules:[   //规则
			//注释掉 expose-loader ,直接使用 new Webpack.ProvidePlugin 实现
			/* {    // 校验 js 语法,一般我们开发过程中不会去校验,注释掉
				test:require.resolve('jquery'),     //当代码中引入了 jquery
				use:[
					{
						loader:'expose-loader?$',
					}
				]
			}, */
		]

运行结果如下:
每个模块都注入 $

可以看到,每个模块都注入了 $,但是 window 中是没有的

4.4 直接引入 CDN 链接,但是不打包

引入CDN链接
那就有 window.$ 了:
那就有 window.$ 了
如果你既在 index.html 中使用 CDN 引入了,又在 index.js 中引入了,打包之后:
打包文件中有 jquery
那么再配置一下:

let Webpack = require('webpack')    //在每个模块中注入全局变量
plugins:[   
		// new Webpack.ProvidePlugin({    //webpack 提供插件,作用是:在每个模块中都注入 $
		// 	$:'jquery'
		// })

	],
externals:{
		jquery:"$"  //如果模块中引入了 jquery ,打包不要打进去
	},

再打包看看:
打包不加jquery

5. 图片处理

5.1 在js中创建图片来引入

import logo from './1.png';  //把图片引入,返回的结果是一个新的图片地址
let image = new Image();
image.src = logo
document.body.appendChild(image)

npm run dev 之后报错:
需要合适的 loader 来解析
这里要使用 file-loader,作用是:默认会在内部生成一张图片到 dist目录下,还会把生成的图片的名字返回回来

npm i file-loader@4.3.0 -D
{    
				test:/\.(png|jpg|gif|jpeg|bmp)/,     //当遇到图片,就使用 file-loader 解析,作用是:默认会在内部生成一张图片到 `dist `目录下,还会把生成的图片的名字返回回来
				use:[
					{
						loader:'file-loader',
					}
				]
			},

图片解析
图片打包进去了

5.2 在css 中引入

在css中使用背景图
可以看见背景图

5.3 在 html 中直接写死

直接在 html 中引入图片
这里就看不到图片效果了,只有这个元素
看不到图片
这里也需要一个loader 来解析

npm i html-withimg-loader@0.1.16 -D
	{    
				{    
				test:/\.html/,     //当遇到图片,就使用 file-loader 解析,作用是:默认会在内部生成一张图片到 `dist `目录下,还会把生成的图片的名字返回回来
				use:[
					{
						loader:'html-withimg-loader',
					}
				]
			},

图片显示出来了

5.4 将小图转化为 base64

一般情况下,我们不会直接用 file-loader,而是使用 url-loader,作用是:当图片小于多少 kb 时,转化base64 ,减少 http 请求;如果大于这个限制,就用 file-loader 将这个图片产出

npm i url-loader@1.1.2 -D
	{    
				test:/\.(png|jpg|gif|jpeg|bmp)/,     //当遇到图片
				use:[
					{
						loader:'url-loader',
						options:{
							limit:200*1024,    //当 图片小于 200K 时,转化为 base64 ,否则直接用 file-loader 解析产出
						}
					}
				]
			},
			// {    
			// 	test:/\.(png|jpg|gif|jpeg|bmp)/,     //当遇到图片,就使用 file-loader 解析,作用是:默认会在内部生成一张图片到 `dist `目录下,还会把生成的图片的名字返回回来
			// 	use:[
			// 		{
			// 			loader:'file-loader',
			// 		}
			// 	]
			// },

打包后可以看到,小图片确实转化为 base64 了,但是要注意,base64 要比原图片大 1/3
小图片转化为 base64
url-loader 也有 file-loader 的功能,所以不需要用 file-loader 了,要验证可以将 limit 的值 改为 1
打包之后:
url-loader也有产出图片的功能

6 打包文件分类

6.1 图片打包分类

将打包后的图片单独放在一个文件夹里面

	{    
				test:/\.(png|jpg|gif|jpeg|bmp)/,     //当遇到图片
				use:[
					{
						loader:'url-loader',
						options:{
							limit:1,    //当 图片小于 200K 时,转化为 base64 ,否则直接用 file-loader 解析产出
							outputPath:'img/'
						}
					}
				]
			},

打包后:
图片单独放置

6.2 样式文件分类

在抽离 css 文件时,可以直接指定目录

new MiniCssExtractPlugin({    //作用:抽离 css
	filename:'css/main.css'  //抽离出来的 css 文件放在 这个文件中;
}),

打包后:
样式打包分类

6.3 给打包后的引入资源加域名

我们还希望在引用图片的时候,加个域名在前面,我们可以在 output 中加个公共路径配置

output:{
		filename:'bundle.js',    //打包后的文件名
		path:path.resolve(__dirname,'dist'),  
		publicPath:'http://www.baidu.com',    //打包后引入的资源的路径都会加这个域名
	},

打包看看:
打包后资源路径加域名
但是可以看到,cssbundle.js 的路径都没问题,但是 图片的路径 img 前面少了个 /
所以在图片分类打包的配置里改一下:

{    
				test:/\.(png|jpg|gif|jpeg|bmp)/, 
				use:[
					{
						loader:'url-loader',
						options:{
							limit:1,  
							outputPath:'/img/'  //这里的 img 前面加了个 /
						}
					}
				]
			},

如果我只想给图片加上这个域名,其他类型的资源不加,那要怎么办呢?好办,首先 output 里面的 publicPath 就不要了,然后在 解析图片时单独加上:


	{    
		test:/\.(png|jpg|gif|jpeg|bmp)/,
		use:[
			{
				loader:'url-loader',
				options:{
					limit:1,   
					outputPath:'/img/',
					publicPath:'http://www.baidu.com',    //打包后引入的图片的路径都会加这个域名
				}
			}
		]
	},

打包后:
只有图片加了域名

7. 打包多页应用

构建多页应用,这里我们重新建一个文件夹,跟之前的区分开,还是之前的步骤:

npm init -y
npm intall webpack@4.28.2 webpack-cli@3.1.2 -D
npm i html-webpack-plugin@4.4.1 -D

多页应用
可以看到我们的打包配置也是两个入口,但是只输出了一个 bundle.js 文件,看打包结果:
打包报错
报错了,两个入口的输出文件名都叫 bundle.js ,这是不允许的;所以需要产出两个出口
修改配置文件:
多个输出
打包成功后的结果:
打包成功多个输出
仅仅是 js 还不能满足需求,肯定也有多个 html:
打包后有多个 html
打包后:
两个html
但是上面两个 html 文件内的内容是一样的,这不是我们预期的效果,我们希望 home.html 中引入 home.jsother.html 中引入 other.js,那么再添加 chunks 配置:

let path = require('path')
let HtmlWebpackPlugin = require('html-webpack-plugin')  //模板插件

module.exports = {
    //多入口
    mode:"development",
    entry:{
        home:'./src/index.js',
        other:'./src/other.js'
    },
    output:{
        filename:'[name].js',  //打包后多个出口, home.js  other.js
        path: path.resolve(__dirname,'dist')
    },
    plugins:[
        new HtmlWebpackPlugin({    //作用:根据模板生成打包后的入口
			template:'./index.html',   //以该文件为模板
			filename:'home.html',    //打包后要放在打包目录下的 html 文件的名字,第1个
            chunks:['home']    //代码块,引入 home.js
		}),
        new HtmlWebpackPlugin({    //作用:根据模板生成打包后的入口
			template:'./index.html',   //以该文件为模板
			filename:'other.html',    //打包后要放在打包目录下的 html 文件的名字,第2个
            chunks:['other']    //代码块,引入 other.js
		}),
    ]

}

重新打包:
home.html中只引入 home.js
other.html只引入 other.js

8. 配置devtool,调试源代码

我们之前在优化项中用到了这个:

optimization: {    //优化项--只有生产环境才会执行这个,开发环境不会执行这个
	minimizer:[
		new UglifyjsWebpackPlugin({    //压缩 js
			cache:true,    //有缓存
			parallel:true,    //是并发打包
			sourceMap:true    //
		}),
		new OptimizeCss()    //压缩 css 文件
	]
},

但是并没有详细说,这里我还是闲安装包:

npm i webpack-dev-server@3.1.14 -D
npm i babel-loader@8.0.4 @babel/core@7.2.2 @babel/preset-env -D

index.js 中写一点 es6 语法的代码,故意写错:
代码写错了
然后看一下配置,注意是生产环境:

let path = require('path')
let HtmlWebpackPlugin = require('html-webpack-plugin')  //模板插件

module.exports = {
    mode:"production",    //development  || production
    entry:{     //入口
        home:'./src/index.js'
    },
    output:{    //输出
        filename:'[name].js',  
        path: path.resolve(__dirname,'dist')
    },
    module:{    //规则
        rules:[
            {
                test:/.js$/,  //匹配以 js 结尾的文件
                use:{
                    loader:'babel-loader',  //使用 babel-loader 加载器
                    options:{
                        presets:['@babel/preset-env']    //将高级语法转化为es5
                    }
                }
            }
        ]
    },
    plugins:[    //插件
        new HtmlWebpackPlugin({    //作用:根据模板生成打包后的入口
			template:'./index.html',   //以该文件为模板
			filename:'index.html',    //打包后要放在打包目录下的 html 文件的名字,第1个
		}),
    ]
}

运行 npm run dev 之后,果然报错了:
果然报错了
找不到出错代码
这样就不好调试代码了,我希望点击错误的时候,出现的是源码,而不是打包之后的代码,所以需要一个源码映射

8.1 source-map

在配置文件中新增 devtool 配置:
新增 devtool配置
然后打包,可以看到新增了 home.js.map 文件:
home.js.map
直接将打包后的 index.html 放在浏览器中,可以看到:
报错处
上面可以看到报错文件也不一样了,以前是 home.js:2,这是打包后的文件,现在这个变成了 index.js:5 ,这个是源文件,点击之后可以看到源代码了:
可以看到源代码了

8.2 eval-source-map

devtool 还有另一种值:eval-source-map,不会产生单独的文件,但是会显示行和列:
eval-source-map
然后重新打包,果然没有了 home.js.map
没有了 home.js.map
还是将 打包后的 index.html 放到浏览器中:
报错位置又不一样了
依旧看到了源代码:
依旧看到了源代码

8.3 cheap-module-source-map

devtool 的第三种值:cheap-module-source-map,不会产生列,但是会产生单独的映射文件,产生后不会跟我们的代码关联起来:
cheap-module-source-map
重新打包:
有映射文件
再来一次,查看 index.html
报错处
看不到源代码了:
看不到源代码了

8.4 cheap-module-eval-source-map

devtool 的第四种值:cheap-module-eval-source-map,不会产生映射文件,也不会产生列,但是会集成在打包后的文件中:
修改 devtool
打包后:
集成
查看结果:
报错
看到了源代码

9. watch 监听代码变化打包

我们每次改完代码都需要手动再打包一遍,台麻烦了,虽然说 npm run dev 可以看到效果,但是它不会产生实体文件啊,所以我们可以使用 watch 配置,以及它对应的选项 watchOptions

let path = require('path')
let HtmlWebpackPlugin = require('html-webpack-plugin')  //模板插件

module.exports = {
    mode:"production",    //development  || production
    entry:{     //入口
        home:'./src/index.js'
    },

    watch:true,    //监控,作用是:监控代码的变化,代码一变化,就帮我们打包
    watchOptions:{  //监控的选项
        poll:1000,    //每秒问1000次:需要更新吗,值越小精度越低
        aggregateTimeout:500,   //防抖: 我一直输入代码,但不能我写一个字母就打包一次吧,500表示:我一直输入内容,直到500毫秒内都没有输入了,就打包
        ignored: /node_modules/  //忽略:不需要监控 node_modules 文件
    },

    output:{    //输出
        filename:'[name].js',  
        path: path.resolve(__dirname,'dist')
    },
}

当有以上设置后,每当代码变化或保存的时候,就会自动打包

10. webpack小插件

10.1 cleanWebpackPlugin

我们每次打包之前,都要手动把上一次的 dist 目录删掉,避免缓存的问题,那么这个插件会帮我们做这件事

npm i clean-webpack-plugin@3.0.0 -D

配置如下:

let CleanWebpackPlugin = require('clean-webpack-plugin')  //打包后自动删除之前的 dist 文件
module.exports = {
    plugins:[    //插件
        new CleanWebpackPlugin('./dist'),  //告诉它要清空的目录,或者是一个数组,要清空多个文件夹
    ]
}

验证过程如下:先打包一次,然后修改输出文件名称,从 index.html 改为 home.html ,再次打包,看 dist 中有没有 index.html ,没有就表示成功
修改之前:
修改之前打包结果
修改之后再次打包,报错了,视频上是没报错的,可能是版本不一样,我这里是 @3.0.0,视频就不知道了:
打包报错
然后在 这里 找到解决方法,修改之后:

let { CleanWebpackPlugin } = require('clean-webpack-plugin')  //打包后自动删除之前的 dist 文件,由clean-webpack-plugin.d.ts文件的内容可知,导出的是以一个对象属性的形式,所以我们在引入的时候需要以解构的方式来获取
module.exports = {
    plugins:[    //插件
        new CleanWebpackPlugin(),  //参数是可选的,如果什么都不配置默认删除未使用的资源,我们采用默认的即可
    ]
}

再次尝试打包,发现已经是 home.html 了,而且没有之前的 index.html 的缓存:
修改成功

10.2 copyWebpackPlugin

我现在有些 src 之外的文件也想打包到 dist 文件里,比如:
其他的文档
还是先安装

npm i copy-webpack-plugin@4.6.0 -D

配置:

let CopyWebpackPlugin = require('copy-webpack-plugin')  //将某些文件打包时直接复制到 dist 中去
module.exports = {
    plugins:[    //插件
        new CopyWebpackPlugin(    //打包时复制文件
            [
                {from: './doc' , to: './doc'}    //从相对于 src 的某个文件 copy 到 相对于dist中的某个路径下去,如果还有其他的文件需要copy,就再写一个对象
            ]
        )
    ]
}

打包后:
复制成功

10.3 bannerPlugin(内置的)

这个插件的作用是:在每个打包文件的头部都插入一些版权信息

let Webpack = require('webpack')    //使用 webpack 内置的插件
module.exports = {
    plugins:[    //插件
        new Webpack.BannerPlugin('make 2021 by me')    //在每个打包js文件的头部都有这样的版权信息
    ]
}

打包后:
插入版权信息

11. webpack 跨域

11.1 使用 express 搭建简单的服务器

在根目录下创建 server.js 文件:

// 使用 express 来 启动一个 服务端
let express = require('express')

let app = express()

/** 
 * 当请求 /api/use 接口时 ,req:表示请求参数, res:表示响应
 */
app.get('/api/use',(req,res)=>{
    res.json({name:'张三'})
})

app.listen(3000)    //服务端监听 3000 的端口

然后修改 package.json 文件:
加入服务端启动命令
运行 npm run start ,然后浏览器访问:http://localhost:3000/api/use,就能看到服务启动成功了:
服务启动成功

11.2 解决跨域的3种方式

11.2.1 前端向 express 服务器请求数据

index.js 中进行请求:

//使用 ajax 请求刚刚我们写的接口
let xhr = new XMLHttpRequest()

//请求(请求类型,接口,是否异步),注意:这样默认访问的路径就是 http://localhost:8080/ ,是 webpack-dev-serve 的服务,但是 express 服务端默认端口是 3000
xhr.open('GET','/api/use',true)  

xhr.onload = function(){
    console.log(xhr.response)  //打印请求结果
}

xhr.send()  //将请求结果发送出去

然后运行 npm run dev,可以看到报错了:
404
报错404了,我们刚刚请求,默认是请求 webpack-dev-server 的地址,所以找不到;

这里我们需要用到 反向代理 http-proxy 来解决跨域的问题,在 webpack 中可以直接配置一下就行:

module.exports = {
    devServer:{
        proxy:{
            '/api' : 'http://localhost:3000/'  //配置了一个代理,当访问了 http://localhost:8080/api 的时候,就直接转到 http://localhost:3000/ 去找里面的 api
        }
    },
]

再次 npm run dev ,就请求成功了:
跨域请求成功了
上述是跨域成功了,但是一般后台在写 api 的时候,不会直接写 '/api/use',而是 '/use',这样的话,我们不可能写代理的时候,有一个接口就写一个代理吧,太麻烦了;

所以,当后台接口没有一个统一的路径时,比如:'/use1''/use2',我们前端请求的时候还是可以自己加上一个统一的路径,比如:'/api/use1''/api/use2',然后在代理的时候,将前端的路径中的 /api 删除 一下,再发给服务器就行了

server.js 中修改一下,然后重新启动一下服务器:npm run start
修改接口
前端的 ajax 不变,然后在 webpack.config.js 中修改一下:

module.exports = {
    devServer:{
        proxy:{
            '/api' : {
                target:'http://localhost:3000/',  //配置了一个代理,当访问了 http://localhost:8080/api 的时候,就直接转到 http://localhost:3000/ 去找里面的 api
                pathRewrite:{  //代理的时候,先将 '/api' 重写,再发送到 target 去
                    '/api':''    //将 '/api' 重写成 ''
                }
            }
        }
    },
]

npm run dev ,看一下结果,还是请求成功了,这里就不截图了,还是和上面一样

11.2.2 前端不向服务器请求,只想 mock 一些数据

由于 devServer 内部本来就是 express ,所以我们可以在 devServer 里面写一些接口:

module.exports = {
    devServer:{
    before(app){    //提供的方法,钩子,启动之前调用此方法,这个参数 app,就跟我们刚刚在 express 中写的 app 是一样的
            app.get('/use',(req,res)=>{
                res.json({name:'张三-before'})
            })
        },

        // proxy:{    //将请求重写的方式,把请求 代理 到 express 服务器
        //     '/api' : {
        //         target:'http://localhost:3000/',  //配置了一个代理,当访问了 http://localhost:8080/api 的时候,就直接转到 http://localhost:3000/ 去找里面的 api
        //         pathRewrite:{  //代理的时候,先将 '/api' 重写,再发送到 target 去
        //             '/api':''    //将 '/api' 重写成 ''
        //         }
        //     }
        // }
    },
]

然后将前端请求的路径也直接改为 /use
修改请求路径
这里就没有 express 服务器 什么事了,就是代码向 devServer 启动的服务请求了数据,npm run dev 一下:
mock数据成功

11.2.3 前端和服务端用同一个端口

有服务端,但是不用代理来处理,在服务端中启动 webpack 的端口,前端和服务端用同一个端口,这样的话,也不会有跨域的问题了

npm i webpack-dev-middleware@3.4.0 -D    //中间件,作用是可以在服务端启动 webpack

在 服务端 启动 webapck,下面是 server.js

// 使用 express 来 启动一个 服务端
let express = require('express')

let app = express()

//在服务端启动 webpack:要使用 express 的中间件
// 大致流程: 先拿到 webpack.config.js 配置对象 --> 交给这里 服务端的 webpack 来处理,会产生一个编译对象 --> 将编译对象扔给中间件

let webpack = require('webpack')

//express的中间件
let middle = require('webpack-dev-middleware')

// 1. 拿到配置文件
let config = require('./webpack.config.js')

// 2. webpack 来处理这个 配置对象,返回的是一个编译的结果
let compiler = webpack(config)

// 3. 使用中间件,这样的话,就不需要 webpack-dev-server 了,直接启动服务端,也会自动启动 webpack 配置
app.use(middle(compiler))


/** 
 * 当请求 /api/use 接口时 ,req:表示请求参数, res:表示响应
 */
// app.get('/api/use',(req,res)=>{
//     res.json({name:'张三'})
// })

app.get('/use',(req,res)=>{
    res.json({name:'张三'})
})

app.listen(3000)    //服务端监听 3000 的端口

然后直接启动服务端 npm run start

启动服务端的时候打包了
然后我们直接访问 http://localhost:3000/use ,也就是服务端,也可以看到服务端确实启动了:
服务端确实启动了
我们再访问 http://localhost:3000 ,也就是 webpack 运行的地址,也可以看到我们的请求成功了:
请求成功

12 resolve 配置

12.2 resolve.modules

commonJS 规范中,我们知道,会从当前目录下的 node_modules 中去找,找不到就一层一层往上去找 node_modules 中的包

resolve :解析 第三方包,这个配置可以限定我们查找包的路径:

module.exports = {
    resolve:{  //解析 第三方包
        modules:[ path.resolve('node_modules') ]    //只在当前目录下的 node_modules 中去找包,不要往上找
    },
]

12.2 resolve.alias

我们试试能不能引用 bootstrap 中的样式,先安装

npm i bootstrap -S
//这里直接安装 bootstrap@4的就行,4以上的安装时,没有 font 文件夹,webpack 就不用处理字体图标类型的文,不然会报错

报错
上面的报错可以在 webpack 中加一个匹配规则,我实践过了,但应该有用:

npm i file-loader@4.3.0 -D
npm i url-loader@1.1.2 -D
{test:/\.(ttf|eot|svg|woff|woff2)$/,use:'url-loader'},

然后在 index.html 中写上按钮:

    <button class="btn btn-danger"></button>

然后在 index.js 中引入样式:

// import 'bootstrap'  //这个会先去 bootstrap 中找它的 package.json 中的 main: "main": "./dist/js/npm", 所以引用的是js 文件,而不是 css 文件,而且运行后还会报缺少 jquery 的错误
//所以我们直接引入 css 文件
import 'bootstrap/dist/css/bootstrap.css'

结果:
按钮样式生效

上面这样直接引入 css 文件写的也太长了,可以在 webpack 中配置别名:

resolve:{  //解析 第三方包
        modules:[ path.resolve('node_modules') ],    //只在当前目录下的 node_modules 中去找包,不要往上找
        alias:{    //别名,比如你引入 vue 也是 别名,实际叫 vue.runtime
            bootstrap: 'bootstrap/dist/css/bootstrap.css'    //当你引入了 'bootstrap' 的时候,实际上是引入了 'bootstrap/dist/css/bootstrap.css'
        }
    },

这样配置了之后,也能够 直接在 index.js 中引入 bootstrap 了,这实际上就是引入了 css 文件

 import 'bootstrap'  

12.3 resolve.mainFields

或者另一个角度,我们在引入包的时候,会默认去找包里面的 package.json 里面的 main ,这样是会找到 js 文件的,那能不能设置去找 style 呢,这样就会找 css 文件了啊:
bootstrap中的 package.json

 import 'bootstrap'  
resolve:{  //解析 第三方包
        modules:[ path.resolve('node_modules') ],    //只在当前目录下的 node_modules 中去找包,不要往上找
        // alias:{    //别名,比如你引入 vue 也是 别名,实际叫 vue.runtime
        //     bootstrap: 'bootstrap/dist/css/bootstrap.css'    //当你引入了 'bootstrap' 的时候,实际上是引入了 'bootstrap/dist/css/bootstrap.css'
        // },
        mainFields:['style','main']  //先找包里面的 package.json 中的 style,style 找不到的话再去找 main
    },

还有一种需求,当你写了样式文件,引入到 js 中去的时候,不想写 后缀:
引入样式:
引入样式

结果:
样式生效

12.4 resolve.extensions

但是我想引入样式文件的时候,不写 import './style.css' ,而写 import './style',这里可以配置扩展名:

resolve:{  //解析 第三方包
    modules:[ path.resolve('node_modules') ],    //只在当前目录下的 node_modules 中去找包,不要往上找
    // alias:{    //别名,比如你引入 vue 也是 别名,实际叫 vue.runtime
    //     bootstrap: 'bootstrap/dist/css/bootstrap.css'    //当你引入了 'bootstrap' 的时候,实际上是引入了 'bootstrap/dist/css/bootstrap.css'
    // },
    // mainFiles:[],    //入口文件的名字 index.js
    mainFields:['style','main'],  //先找包里面的 package.json 中的 style,style 找不到的话再去找 main
    extensions:['.js' , '.css' , '.json']    //引入的包如果没有写后缀,就先去找同名的 js 文件,没有就找 css 文件,再没有就找 json 文件
},

13. 区分生产环境和开发环境的配置

13.1 定义环境变量

区分开发环境和生产环境分别是什么配置,首先要用到 配置环境中的变量,先来看怎么使用吧:
我们在 index.js 中进行判断:
在 js 中使用 webpack 定义的变量
然后在 webpack.config.js 中定义该变量 `DEV``:
在 webpack 中定义变量的写法

上图中要注意:变量的值要在 引号 内部,如果是字符串的话就要两层引号了,比如: DEV: "'development'" ,但是不推荐这样的写法,我们可以写成:DEV: JSON.stringify('development')

然后 npm run dev 的结果:
使用了 webpack 的变量

13.2 区分环境

我们可以建两个配置文件来对应两种环境,然后将我们之前写的 webpack.config.js 改名为 webpack.base.js

  • webpack.prod.js :对应的是 production 生产环境
  • webpack.dev.js :对应的是 development 开发环境
  • webpack.base.js :对应的是基础的,公共的配置

通过上面三种配置文件,可以在生产环境下,是 webpack.base.js + webpack.prod.js
然后 开发环境下是: webpack.base.js + webpack.dev.js
这样合并不同文件中的配置的操作需要用到 一个插件 :webpack-merge

npm i webpack-merge@4.2.1 -D

然后是 webpack.prod.js配置:
生产环境配置
以及 webpack.dev.js 开发环境的配置:
开发环境的配置
然后将 webpack.base.js 中的 mode 直接删除就行了,然后将 package.json 中的 "build": "webpack --config webpack.config.js" 改为 "build": "webpack"
以后,要在生产环境或者开发环境下打包,就直接在打包指令后直接指定对应的配置文件就行:

npm run build -- --config webpack.prod.js    //生产模式打包
npm run build -- --config webpack.dev.js     //开发模式打包

生产环境打包:
生产环境打包
开发环境打包:
开发环境打包
发现确实生效后,我们之前的开发和生产的配置就可以分开了:

  • webpack.prod.js中,现在可以直接把优化项放在这里:
let {smart} = require('webpack-merge')    //合并 webpack 配置的插件
let base = require('./webpack.base.js')    //基础,公共的 webpack 配置

//然后把两个配置变成一个:
module.exports = smart(base,{
    mode:'production',    //生产环境

    optimization: {    //优化项--只有生产环境才会执行这个,开发环境不会执行这个
        minimizer:[
            new UglifyjsWebpackPlugin({    //压缩 js
                cache:true,    //有缓存
                parallel:true,    //是并发打包
                sourceMap:true    
            }),
            new OptimizeCss()    //压缩 css 文件
        ]
    },
})
  • webpack.dev.js中,现在可以直接把开发时的配置放在这里:
let {smart} = require('webpack-merge')    //合并 webpack 配置的插件
let base = require('./webpack.base.js')    //基础,公共的 webpack 配置

//然后把两个配置变成一个:
 module.exports = smart(base,{
     mode:'development',    //生产环境

     /** 
      * devtool 的值:
      * 1. 'source-map':源码映射,会单独生成一个 sourcemap 文件,出错了会标识当前报错的列和行,大 和 全
      * 2. 'eval-source-map':不会产生单独的文件,但是会显示行和列
      * 3. 'cheap-module-source-map':不会产生列,但是会产生单独的映射文件,产生后不会跟我们的代码关联起来,但可以保留起来,用于调试
      * 4. 'cheap-module-eval-source-map':不会产生映射文件,也不会产生列,但是会集成在打包后的文件中
      */
     devtool:'cheap-module-eval-source-map',    //增加映射文件,可以帮我们调试源代码; 
     
     devServer:{
         before(app){    //提供的方法,钩子,启动之前调用此方法,这个参数 app,就跟我们刚刚在 express 中写的 app 是一样的
             app.get('/use',(req,res)=>{
                 res.json({name:'张三-before'})
             })
         },
 
         // proxy:{    //将请求重写的方式,把请求 代理 到 express 服务器
         //     '/api' : {
         //         target:'http://localhost:3000/',  //配置了一个代理,当访问了 http://localhost:8080/api 的时候,就直接转到 http://localhost:3000/ 去找里面的 api
         //         pathRewrite:{  //代理的时候,先将 '/api' 重写,再发送到 target 去
         //             '/api':''    //将 '/api' 重写成 ''
         //         }
         //     }
         // }
     },
 })

以上就是 webpack 的基础配置了,下面再讲到一些 插件,优化之类的

14. webpack 的优化

我们还是建立一个基础的 demo:
在 index.js 中引入 jquery

14.1 noParse 打包解析时忽略某些包的依赖关系

如上所示:在 index.js 中引入了 jquery ,那么再打包的时候,会自动去找这个包,解析其中的依赖关系,要是还有其他的依赖就一起打包,但是我们可以确定 jquery 中没有其他依赖,想跳过这一步解析,提高打包效率,这时可以使用 noParse
配置之前的打包:
配置之前的打包时间
配置后:
配置 noParse
打包:
配置后的打包时间
对比一下,配置后的打包时间明显缩短了

14.2 排除(exclude)和包含(include)

这一项之前我们用到过,这里就贴下代码:

    module:{    //规则
        noParse: /jquery/,    //不去解析 jquery 中的依赖库
        rules:[
            {
                test:/\.js$/,  //匹配以 js 结尾的文件
                use:{
                    loader:'babel-loader',  //使用 babel-loader 加载器
                    options:{
                        presets:['@babel/preset-env']    //将高级语法转化为es5
                    }
                },
				include:path.resolve(__dirname,'src'),  //只解析 src 中的 js 文件
				exclude:/node_modules/  //排除掉 node_modules 中的js文件
            }
        ]
    },

14.3 IgnorePlugin 忽略包的某些无用引入

安装 moment

npm i moment@2.22.2 -S

使用 moment
使用 moment
运行 npm run dev 之后,打印成功了 :
打印结果

打包后的体积
上图中,虽然我们只用了这个包里的几个方法,但实际上是把包里面的所有内容都打包进来了,包括所有的语言:
moment.js

方便的是,可以直接设置语言:
设置语言
结果是:
中文版
但是,我只想用到中文包的情况下,把所有包都引进来就很冗余,打包体积很大,可以用一个插件实现:忽略包里面的引用的所有的本地文件
配置:

let Webpack = require('webpack')
module.exports = {
    plugins:[
        new Webpack.IgnorePlugin(/\.\/locale/,/moment/),    //如果从 moment 中引入了 './locale',就忽略掉  
    ],
}

再来运行:
只加载中文包
配置前和配置后,减少了 500 多 KB,但是看看打印结果,又恢复到英文了,因为中文包没有被引入,所以我们需要手动引入所需要的语言:
手动引入中文包
再运行就是打印的中文了

14.4 动态链接库

由于视频中使用的是 react ,只能跟着来了,第一步还是安装包:

npm i react@16.7.0 react-dom@16.7.0 -S
npm i @babel/preset-react@7.0.0 -D    //解析 angular 语法的包,只有一个版本 6.0.15

使用 react 写点代码:
index.js
index.html
配置文件中,要翻译 react 语法:
翻译react语法
然后运行看看:
react 运行正常
运行结果
我们可以看到:体积也很大了,1.2M 左右,我们不会去更改 react 和 react-dom ,我们希望打包的时候,不要把这两个包打进去:

14.4.1 单独打包 react react-dom

新增 webpack.config.react.js 配置文件,里面是单独打包 react react-dom 的配置:

//单独去打包 react react-dom,在开发的时候,引用我们打包好的文件,这样的话 react react-dom 就不会重新打包了

let path = require('path')
let Webpack = require('webpack')
module.exports = {
    mode:"development",
    entry:{
        react:['react','react-dom'],
    },
    output:{
        filename:'_dll_[name].js',  // 产生的文件名
        path: path.resolve(__dirname,'dist'),
        library:'_dll_[name]',    //产生的文件导出的变量叫这个名字,_dll_react
        libraryTarget:'var'    //var 是默认值,还可以是 commonjs , var , this , umd ......
    },
    plugins:[
        new Webpack.DllPlugin({
            name:'_dll_[name]',    // name == library  这是规定好的,容易去找对应关系
            path:path.resolve(__dirname,'dist','mainfest.json')    //一个路径,清单,让人能找到这个文件
        })
    ]

}

然后 运行 webpack --config webpack.config.react.js 进行打包:
_dll_react.js
manifest.json
打包成功后,就改引入到我们的项目中去了:
然后修改我们的打包配置 wbpack.config.js

plugins:[
        new Webpack.DllReferencePlugin({    //引用第三方动态链接库
            manifest: path.resolve(__dirname,'dist','manifest.json')  //先去 dist/manifest.json 中去找引用的模块,没有的话再按照顺序从 node_modules 中去找
        }),
    ],

再打包看看:
只有6k了
打包的 index.js 只有 6k 了,因为我们已经把 react 相关的包都打包好了,放在了 _dll_react.js 中且在 index.html 中引用了,这样的动态链接库提升了我们的打包效率。

14.5 多线程打包

使用 模块 happypack 可以实现多线程打包

npm i happypack@5.0.1 -D

我们以前解析 js 的时候用的 babel-loader ,但是现在可以使用 happypack/loader,通过指定的 id 去找 插件配置中对应的 id 来解析相关文件:

let Happypack = require('happypack')    //多线程打包
module.exports = {
    plugins:[
        new Happypack({
            id:'js',
            use:[    // use 必须是 数组
                {
                    loader:'babel-loader',  //使用 babel-loader 加载器
                    options:{
                        presets:['@babel/preset-env','@babel/preset-react']    //将高级语法转化为es5
                    }
                }
            ]
        }),
    ],
    module:{    //规则
        noParse: /jquery/,    //不去解析 jquery 中的依赖库
        rules:[
            {
                test:/\.js$/,  //匹配以 js 结尾的文件
                // use:{
                //     loader:'babel-loader',  //使用 babel-loader 加载器
                //     options:{
                //         presets:['@babel/preset-env','@babel/preset-react']    //将高级语法转化为es5
                //     }
                // },
                use:'Happypack/loader?id=js',    //使用 Happypack 中的 loader 来解析,其中 id 是 js ,这时就会去 plugins 中找对应的插件(id:'js')来解析
				include:path.resolve(__dirname,'src'),  //只解析 src 中的 js 文件
				exclude:/node_modules/  //排除掉 node_modules 中的js文件
            }
        ]
    },

}

上面是 解析 js 使用 多线程,同理也可以在解析 css 或其他类型文件时也可以使用多线程,差不多的配置
然后将配置前和配置后的 npm run build 打包来对比:
单线程
多线程
我们可以看到多线程怎么反而是 825ms 比单线程的 420ms 还多呢,这是因为我们现在的项目太小了,在分配线程的时候也会消耗一些性能

14.6 webpack 自带的优化

14.6.1 tree-shaking

先写点代码:
test.js
index.js
运行后是正常的:
运行正常
在开发模式下打包:

开发模式
开发模式下,我们可以看到, summinus 都有,但是我们使用的时候只用到了 sum 啊,所以打包的时候不想要 minus,再看看生产模式下打包的结果:

生产模式下没有 minus
生产环境下打包没有 minus

可以看到: 使用 import 引入时,会在生产环境下,自动去除没用的代码,这叫做 tree-shaking :把没用到的代码自动删除掉
那么如果是使用 require 导入的呢:
使用 require 导入
结果运行就报错了:
报错了
直接打印 calc 看看:
calc
可以看见 require 的是 es6 的模块,模块里面还有个 default 对象,所以使用的时候得去:
console.log(calc.default.sum(1,2))
对了
解决报错后,再来对比生产和开发两种模式下打包的结果:
先是开发模式:
开发模式
生产模块:
生产模块
可见:在使用 require 导入的时候,生产模式下还是有 minus 这些没用到的代码

14.6.2 scope hosting 作用域提升

index.js 中:

let a = 1;
let b = 2;
let c = 3;
let d = a + b + c;
console.log(d);
//上面这些代码其实可以直接写成  console.log(1+2+3);

先在开发模式下打包:
开发模式
打包结果
生产模式下打包:
在这里插入图片描述
打包结果
可以看见:开发模式下只是翻译成了 es5,但是变量都在,但在 生产模式下时, a,b,c,d 这四个变量都不见了,表达式也不见了,只有 console.log(6)

以上就是 scope hosting --> 在 webpack 中自动省略一些可以简化的代码

15 抽离公共代码(splitChunks)

15.1 抽离公共文件

在多个页面中有公用的部分,我们需要抽离出来,我们来创建一个情景,多入口应用中,两个入口都需要用到 a.jsb.js
结构

//a.js
console.log('a~~~~~~~~');
//b.js
console.log('b~~~~~~~~');
//index.js
import './a'
import './b'
console.log('index.js');
//other.js
import './a'
import './b'
console.log('other.js');

打包一下:
index.js
other.js
打包结果可以看到 ,打包后的 index.jsother.js 都引用了 a.jsb.js,下面就开始抽离的操作了:

let path = require('path')
module.exports = {
    optimization: {    //优化项
        splitChunks:{   //分割代码块
            cacheGroups:{    //缓存组
                common:{    //公共模块
                    chunks:'initial',    //抽离的时机:刚开始就进行抽离
                    minSize:0,    //只要大于 0 字节的就抽离出来
                    minChunks:1,    //这个代码块需要引用多少次,才用抽离出来

                }
            }
        }
    },
    devServer:{
        port:3000,
        open:true,  //自动打开浏览器
        contentBase:'./dist',  //结果在 ./dist 目录中
    },
    mode:"production",    // production  || development
    entry:{    //多入口应用
        index:'./src/index.js',
        other:'./src/other.js'
    },
    output:{
        filename:'[name].js',  //打包后多个出口, index.js  other.js
        path: path.resolve(__dirname,'dist')
    },
}

打包:
抽离了

15.2 抽离第三方包

other.js 里面和 index.js 中都加上以下代码:

import $ from 'jquery'
console.log($);

配置:

let path = require('path')
module.exports = {
    optimization: {    //优化项
        splitChunks:{   //分割代码块
            cacheGroups:{    //缓存组
                common:{    //公共模块
                    chunks:'initial',    //抽离的时机:刚开始就进行抽离
                    minSize:0,    //只要大于 0 字节的就抽离出来
                    minChunks:1,    //这个代码块需要引用多少次,才用抽离出来

                },
                vendor:{    //第三方包
                    priority:1,    //优先级,因为是从上往下执行的,上面的 common 就直接把多次引用的 juqery 也一起抽离出来了,都不会到这里了,所以要设置优先级,先抽离第三方模块,再抽离公共文件
                    test:/node_modules/,    //把你抽离出来
                    chunks:'initial',    //抽离的时机:刚开始就进行抽离
                    minSize:0,    //只要大于 0 字节的就抽离出来
                    minChunks:2,    //这个代码块需要引用多少次,才用抽离出来
                }
            }
        }
    },
}

注意以上的 priority 在抽离第三方包的时候一定要设置,不然按照从上往下执行的顺序, jquerycommon 中就直接被抽离到 common~index~other.js 中了
打包:
抽离第三方模块

16 懒加载(es6 的 import)

我们在代码中引入其他资源的时候,之前都是直接导入的,就是资源跟代码一起加载出来的,现在懒加载就是实现,当我们需要资源的时候,才去加载资源文件,这里用到的是 es6 草案中的 动态导入文件 的做法:
index.js 中,创建按钮,点击按钮时加载 source.js 中的内容:

// index.js
let button = document.createElement('button')
button.innerHTML = 'hello'
button.addEventListener('click',function() {
    console.log('click');
    //加载资源:es6 草案中的语法:jsonp实现动态加载文件,返回的是一个 promise
    //视频中这里的导入解析报错了,需要一个 @babel/plugin-syntax-dynamic-import@7.2.0 来解析语法
    import ('./source').then(data=>{
        console.log(data);
    })
    //实际上 vue懒加载,react 的懒加载都是根据上述的原理实现的
})
document.body.appendChild(button)

es6 草案中的 动态导入文件:import ('./source').then(data=>{console.log(data);}),原理是 jsonp实现动态加载文件,返回的是一个 promise;
然后在 source.js 中导出一点内容:

// source.js
export default 'abcd'

然后 npm run dev 运行,这时,视频中出现报错,是需要插件来解析上面的 import 语法,但是我本地没有报错,不过还是按照视频中的安装了下:

npm i @babel/plugin-syntax-dynamic-import@7.2.0 -D

然后配置一下:

let path = require('path')
let HtmlWebpackPlugin = require('html-webpack-plugin')  //模板插件
let Webpack = require('webpack')
// let Happypack = require('happypack')    //多线程打包

module.exports = {
    devServer:{
        port:3000,
        open:true,  //自动打开浏览器
        contentBase:'./dist',  //结果在 ./dist 目录中
    },
    mode:"production",    // production  || development
    entry:{   
        index:'./src/index.js',
    },
    output:{
        filename:'[name].js',  //打包后多个出口, home.js  other.js
        path: path.resolve(__dirname,'dist')
    },
    plugins:[
        new HtmlWebpackPlugin({    //作用:根据模板生成打包后的入口
			template:'./public/index.html',   //以该文件为模板
			filename:'index.html',    //打包后要放在打包目录下的 html 文件的名字,第1个
		})
    ],
    module:{    //规则
        noParse: /jquery/,    //不去解析 jquery 中的依赖库
        rules:[
            {
                test:/\.js$/,  //匹配以 js 结尾的文件
                use:{
                    loader:'babel-loader',  //使用 babel-loader 加载器
                    options:{
                        presets:['@babel/preset-env','@babel/preset-react'],    //将高级语法转化为es5
                        plugins:['@babel/plugin-syntax-dynamic-import']    //用来解析 es6草案中 的动态导入文件的语法
                    }
                },
				include:path.resolve(__dirname,'src'),  //只解析 src 中的 js 文件
				exclude:/node_modules/  //排除掉 node_modules 中的js文件
            }
        ]
    },

}

运行后:
点击按钮后导出文件
点击之后才加载  1.js
打包后:
打包

17 热更新

我们以前每次更新代码,都会导致整个项目全都刷新,我们希望只更新某个部分,比如我只更改了某个组件,完成后希望也只更新这个组件,这就是热更新
这里就不使用懒加载了,直接导入 source.js

//index.js
import str from './source'
console.log(str);

然后其实我们每次改变 source.js 的时候,dev ` 运行都可以看到浏览器左上角的刷新图标实现了刷新,
下面配置热更新:

let Webpack = require('webpack')

module.exports = {
    optimization: {    //优化项 
        moduleIds:'named'    // NamedModulesPlugin 弃用后,替换为 moduleIds 配置,在 webpack 官网可以查到
    },
    devServer:{
        hot:true,    //启用热更新:当我只修改了某个文件时,也只更新这个文件,而不是全部代码
        port:3000,
        open:true,  //自动打开浏览器
        contentBase:'./dist',  //结果在 ./dist 目录中
    },
    plugins:[
        // new Webpack.NamedModulesPlugin(),    //已弃用--打印更新的模块路径:告诉我们那个模块更新了-- NamedModulesPlugin → optimization.moduleIds: 'named'
        new Webpack.HotModuleReplacementPlugin,   //热更新插件:用这个插件来支持热更新
    ],
}

然后每次修改 source.js 后再看浏览器的这个页面是否刷新,发现还是刷新了,结果还要在 index.js 中做修改:

//直接导入 source.js
import str from './source'
console.log(str);
if(module.hot){    //如果当前模块支持热更新
    module.hot.accept('./source',()=>{  //当 './source' 模块更新后,就重新加载 source.js
        console.log('文件更新了');
    })
}

之后每次修改 source.js 之后,页面的刷新图标都没刷新过,但是打印内容一直在更新

18 Tapable

安装:

npm i tapable@1.1.1 -D
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值