学习视频
webpack是前端资源构建工具,是一个静态模块打包器(构建工具就是将前端需要进行的操作整合到一起)。简而言之就是打包工具。
打包过程
webpack.config.js配置文件分为5个部分。
- entry:入口文件
- output:输出文件
- loader:翻译
- plugins:执行功能更强大的事,优化,压缩…
- mode:模式
loader:webpack本身只能处理js,json资源。如果需要处理css,jpg等其他类型的资源就需要不同的loader对资源进行处理,比如css资源通过css-loader翻译为js代码等
plugin:配置一些功能更为强大的插件
mode分为2种模式:
mode:'development'//开发模式,在本地能开发运行就行
mode:'production' //生产模式,让代码优化上线运行的环境
不同模式下的终端命令打包方法:
//开发环境:
webpack ./src/index.js -o ./build/built --mode=development
//生产环境:
webpack ./src/index.js -o ./build/built --mode=production
整个打包过程就是:
从entry入口文件开始,根据入口文件中引入的其他文件形成关系依赖图(并对资源进行翻译),最后形成一个chunk代码块。
如:上图中入口文件index.js中引入了index.css
/index.less
/iconfont.js
/print.js
四个文件,webpack会先引入这四个文件,然后对index.css
/index.less
/这两个文件使用相应的loader进行翻译,而iconfont.js
/print.js
文件里面如果还引入了其他文件,则会继续引入相关的文件并使用loader翻译形成关系依赖图,最终形成chunk代码块输出。
其他知识点:
- 浏览器并不能认识ES6模块化。webpack的开发模式打包,生产模式打包都能将】将ES6模块化编译成浏览器能识别的模块化
- 生产环境打包 比 开发模式打包 多一个压缩代码的功能
- webpack不会重复打包同一个文件,只会打包一次
打包css,less文件(开发模式)
打包css文件
注:loader的加载顺序是从右到左
module:{
rules:[
// 会通过script标签添加到页面上
{
test:/\.css$/,
use:[
'style-loader', //创建style标签,将js中的样式资源添加到head中生效
'css-loader' //将css文件变成commonjs模块加载到js中,里面内容是样式字符串
]
}
]
},
打包less文件
module:{
rules:[
// 会通过script标签添加到页面上
{
test:/\.css$/,
use:[
'style-loader', //创建style标签,将js中的样式资源添加到head中生效
'css-loader' //将css文件变成commonjs模块加载到js中,里面内容是样式字符串
]
},
{
test:/\.less$/,
use:[
'style-loader', //创建style标签,将js中的样式资源添加到head中生效
'css-loader', //将css文件变成commonjs模块加载到js中,里面内容是样式字符串
'less-loader' //将less文件翻译为css文件
]
}
]
},
自动将输出资源引入html文件中
指定html的模板为'./src/index.html'
,将打包后的资源引入到html模板中,并打包输出相应的html文件。
const HtmlWebpackPlugin = require('html-webpack-plugin')
plugins:[
// 默认会创建一个空的html,自动引入打包输出的所有资源
new HtmlWebpackPlugin({
// 复制'./src/index.html'文件,自动引入打包输出的所有资源
template:'./src/index.html'
})
],
打包图片(开发模式)
打包css文件中引入的图片
//需要下载url-loader和file-loader,因为url-loader是依赖于file-loader的
loader:'url-loader',
options:{
//图片大小小于8kb,就会被base64字符串处理
// 优点:减少请求数量,减轻服务器压力
// 缺点:图片体积更大,文件请求速度慢
limit:8 * 1024,
// 4.给图片重命名
// [hash:10]:取图片的hash的前10位
// [ext]取文件原来的扩展名
name:'[hash:10].[ext]'
}
}
打包html文件中引入的图片
//需要下载url-loader和file-loader,因为url-loader是依赖于file-loader的
loader:'url-loader',
options:{
//图片大小小于8kb,就会被base64字符串处理
// 优点:减少请求数量,减轻服务器压力
// 缺点:图片体积更大,文件请求速度慢
limit:8 * 1024,
esModule:false,
// 4.给图片重命名
// [hash:10]:取图片的hash的前10位
// [ext]取文件原来的扩展名
name:'[hash:10].[ext]'
}
},
{
// 2.处理html文件的img图片(负责引入img,从而能被url-loader处理)
test:/\.html$/,
loader:'html-loader'
}
问题:图片不显示
[object Module]问题
url-loader默认使用es6模块化解析,而html-loader引入图片是commonjs,解析时会出现[object Module]的问题,通过esModule:false
来关闭url-loader中的es6模块解析
//需要下载url-loader和file-loader,因为url-loader是依赖于file-loader的
loader:'url-loader',
options:{
//图片大小小于8kb,就会被base64字符串处理
// 优点:减少请求数量,减轻服务器压力
// 缺点:图片体积更大,文件请求速度慢
limit:8 * 1024,
//3.存在问题:
// 因为url-loader默认使用es6模块化解析,而html-loader引入图片是commonjs
// 解析时会出现问题[object Module]
// 解决:
// 关闭url-loader中的es6模块
esModule:false,
// 4.给图片重命名
// [hash:10]:取图片的hash的前10位
// [ext]取文件原来的扩展名
name:'[hash:10].[ext]'
}
},
{
// 2.处理html文件的img图片(负责引入img,从而能被url-loader处理)
test:/\.html$/,
loader:'html-loader'
}
webpack版本问题
- 查看webpack版本,我使用的是webpack5,所以webpack4的配置不生效= =
webpack -v //查看
webpack 5.55.0
webpack-cli 4.8.0
webpack-dev-server 4.3.0
- 解决方法1:
任然使用webpack4的配置方法,需要添加type:'javascript/auto'
实现功能
{
test:/\.(jpg|png|gif)$/,
use:[
{
loader:'url-loader',
options:{
limit:8 * 1024,
name:'[hash:10].[ext]',
esModule:false,
outputPath:'imgs'
},
}
],
type:'javascript/auto',
},
- 解决方法2:
直接使用webpack5的配置方法
{
test:/\.(jpg|png|gif)$/,
type: 'asset/resource',
generator:{
filename:'img/[hash:10][ext]',
},
},
使用devServe(开发模式)
devServe属于开发配置,修改代码不需要重新打包(只在内存中打包),界面会自动更新。
- 在
webpack.config.js
中添加如下配置:
//只会在内存中编译打包,不会有任何输出
devServer:{
contentBase:resolve(__dirname,'build'),
compress:true, //启动压缩
port:3000, //指定端口号
open:true //自动打开浏览器
}
- 在
package.json
中配置指令
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"dev": "webpack serve"
},
- 通过
npm run dev
启动
生产模式的打包
生产模式的打包需要让代码优化上线,让代码运行速度更快,解决兼容性问题等。
- css的代码存放在js文件中,会导致js文件特别大
- 对代码进行压缩
- …
注:如果一个文件要被多个loader处理,那么一定要指定loader的执行顺序,比如对js的处理:既使用了eslint-loader
,又使用了babel-loader
,那么可以通过设置enforce:'pre'
来规定该loader先执行。
{
test:/\.js$/,
exclude:/node_modules/,
enforce:'pre', //优先执行
loader:'eslint-loader',
options:{
fix:true
}
}
css文件打包优化(生产模式)
提取css为单独文件
使用插件mini-css-extract-plugin
第一步:引入
const miniCssExtractPlugin = require('mini-css-extract-plugin')
第二步:配置
//2.1先在plugin中配置插件
plugins:[
new miniCssExtractPlugin({
filename:'css/built.css'
})
],
//2.2然后使用miniCssExtractPlugin.loader取代style-loader
{
test:/\.css$/,
use:[
// 'style-loader', //创建style标签,将js中的样式资源添加到head中生效
// 使用这个loader取代style-loader,作用是提取js中的css为单独的文件
miniCssExtractPlugin.loader,
'css-loader' //将css文件变成commonjs模块加载到js中,里面内容是样式字符串
]
},
css兼容性处理
使用插件postcss-loader
,postcss-preset-env
postcss会找到package.json
文件中browerslist
里面的配置(这个配置可以根据需求自行修改),加载配置指定的css兼容性样式.
postcss-loader
第一步:在package.json
文件中配置browerslist
"browerslist":{
//开发环境--->设置node环境变量:
//在webpack.config.js文件中添加语句process.env.NODE_ENV = "development"
"development":[
"last 1 chrom version",
"last 1 firefox version",
"last 1 safari version"
],
//生产环境:默认是看生产环境
"production":[
">0.2%",
"not dead",
"not op_mini all"
]
}
第二步:在webpack.config.js
中配置插件
{
test:/\.css$/,
use:[
miniCssExtractPlugin.loader,
'css-loader',
{
loader:'postcss-loader',
options:{
postcssOptions:{
plugins:[
'postcss-preset-env'
]
}
}
}
]
},
实现效果:
打包前css文件:
#boc{
display:flex;
backface-visibility: hidden;
}
打包后css文件:
#boc{
display:flex;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
}
css代码压缩
使用插件css-minimizer-webpack-plugin
//1.引入
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
//2.配置
plugins:[
new CssMinimizerPlugin()
],
js文件打包优化(生产模式)
js兼容性处理
把es6的语法转化为es5的语法。比如源文件中使用了箭头函数,打包之后箭头函数会变为正常的函数。
打包前:
var add = (x, y) => { return x + y }
打包后:
var add = function add(x, y) {\n return x + y;\n};\n\nconsole.log(add(1, 2));
- 兼容配置方法1
使用插件babel-loader
,@babel/core
, @babel/preset-env
rules:[
{
test:/\.js$/,
loader:'babel-loader',
exclude:/node_modules/,
options:{
//预设,指示babel做怎么样的兼容性处理
presets:['@babel/preset-env']
}
},
]
方法1存在的问题是:只能转化简单的语法,像Promise这样的语法不能转化。
- 兼容配置方法2
使用插件@babel/polyfill
,在需要兼容的文件中引入即可,如:
import '@babel/polyfill'
const promise = new Promise((resolve) => {
setTimeout(() => {
console.log('定时器执行了!')
resolve()
}, 1000)
})
console.log(promise)
方法2存在的问题是:将所有兼容性代码全部引入,代码体积太大
- 兼容配置方法3
使用插件cnpm i core-js -D
{
test:/\.js$/,
loader:'babel-loader',
exclude:/node_modules/,
options:{
//预设,指示babel做怎么样的兼容性处理
presets:[[
'@babel/preset-env',
{
useBuiltIns:'usage',
corejs:{
version:3
},
targets:{
chrome:'60',
firefox:'60',
ie:'9',
safari:'10',
edge:'17'
}
}
]]
}
},
压缩js和html(生产模式)
- js
生产环境下会自动压缩js代码
- html
//在plugins中进行配置:压缩html代码
new HtmlWebpackPlugin({
// 复制'./src/index.html'文件,自动引入打包输出的所有资源
template:'./src/index.html',
minify:{
collapseWhitespace:true, //移除空格
removeComments:true, //移除注释
}
}),
优化配置
开发环境性能优化
- 优化打包构建速度
- 优化代码调试
生产环境性能优化
- 优化打包构建速度
- 优化代码性能
HMR(开发模式)
前面配置过的devServe存在的问题是:修改一个文件中的代码,其他文件都会重新执行
解决:开启HMR功能(热模块替换),这样一个模块发生变化,就只会打包这一个模块,而不是打包所有模块,极大提升打包速度。开启的方式是添加hot:true
配置
devServer:{
contentBase:resolve(__dirname,'build'),
compress:true, //启动压缩
port:3000, //指定端口号
open:true, //自动打开浏览器
hot:true //开启HMR功能
}
注:修改ebpack.configf.js中的配置,一定要重启服务
-
样式文件可以使用HMR,因为style-loader内部实现了(即修改css文件中的内容,其他文件不会重新打包)
-
js文件默认没有HMR(即修改js文件,其他文件也会重新打包)
- 解决:修改index.js入口文件,添加以下代码
if(module.hot){ //一旦module.hot为true,说明开启了HMR功能。 ----> 让HMR功能代码生效 module.hot.accept('./print.js', function(){ //方法会监听 print.js 文件的变化,一旦发生变化,其他默认不会重新打包构建 //会执行后面的回调函数 }) }
-
html文件默认没有HMR,同时会导致问题:html文件没有热更新了
- 解决:修改entry,添加html文件
entry:['./src/js/index.js','./src/index.html'],
备注:入口文件index.js是不能做HMR功能的,修改index.js文件就会重新打包其他资源。
source-map
一种提供源代码到构建后代码 映射 的技术(如果构建后代码出错了,通过映射可以追踪源代码错误)
在webpack.config.js中添加下面的语句
devtool:'source-map'
重新打包webpack
,在build/js文件夹下会新增一个文件built.js.map文件。
有以下写法:
[inline-|hidden-|eval-|nosources-|cheap-]source-map
//内联:构建速度更快,不会新增文件,map文件中的内容内联在built.js文件中
devtool:'inline-source-map'
//生成外部文件
devtool:'hidden-source-map'
//内联,每个文件都生成source-map,都在eval
devtool:'eval-source-map'
oneOf:[]
oneof内配置loader,一个文件只会匹配一个loader
不能有两个配置处理同一类型文件,可以把处理同一类型文件的配置提到oneof外面去。
缓存 (生产模式)
babel缓存
babel对代码进行编译,比如编译了100个js文件,修改其中一个文件,只需要编译这一个文件,其他文件应该不需要编译。开启babel缓存只需要配置cacheDirectory:true
{
test:/\.js$/,
loader:'babel-loader',
exclude:/node_modules/,
options:{
//预设,指示babel做怎么样的兼容性处理
presets:[[
'@babel/preset-env', //只能做简单的兼容性处理
{
useBuiltIns:'usage',
corejs:{
version:3
},
targets:{
chrome:'60',
firefox:'60',
ie:'9',
safari:'10',
edge:'17'
}
}
]],
//开启babel缓存,第二次构建时会读取之前的缓存
cacheDirectory:true
}
},
文件资源缓存
先写一个服务器来测试代码的实际运行情况,在文件根目录下新建文件夹:server.js
// 服务器代码
// 启动:
// node server.js
const express = require('express')
const app = express()
app.use(express.static('build', {maxAge: 1000 * 3600}))
app.listen(3000)
可以在浏览器中输入http://localhost:3000/
进行访问,可以发现存在问题:强缓存,资源文件发生改变,页面显示不会更新
解决:使用hash来获取最新的数据,即对输出的文件进行hash命名。webpack每次打包都会生成一个此次打包的hash值,每次打包hash值都会发生变化,即文件名发生变化,所以虽然会有缓存,但是因为文件名变了,所以页面的内容会更新。
output:{
filename:'js/built.[hash:10].js',
path:resolve(__dirname, 'build'), //__dirname代表当前文件的目录绝对路径
},
new miniCssExtractPlugin({
filename:'css/built.[hash:10].css'
}),
但是上面的做法由于js和css同时使用一个hash值,重新打包会导致所有缓存失效(改动一个文件,导致所有缓存失效),即改动一个文件,其他文件名也会变化,所以不会从缓存中获取数据,而是重新获取所有的文件。
chunkhash
:根据chunk生成hash值,如果打包来源于同一个chunk,那么hash值就一样。但是css是在js中被引进来的,所以同属于一个chunk,不能解决上面的问题。
-contenthash
:根据文件的内容生成hash值,不同文件的hash值不一样。即通过维护hash值的变化,控制缓存是否失效。
output:{
filename:'js/built.[contenthash:10].js',
path:resolve(__dirname, 'build'), //__dirname代表当前文件的目录绝对路径
},
new miniCssExtractPlugin({
filename:'css/built.[contenthash:10].css'
}),
tree-shaking
去除无用代码
前提:1.必须使用ES6模块; 2.开启production模式
好处:减少代码体积,请求速度更快
测试:
- 在src文件夹下新建.js
export function mul(x, y){
return x * y
}
export function count(x, y){
return x - y
}
- 在index.js中引入其中的一个方法
//tree-shaking
import {mul} from './test.js'
console.log(mul(3,4))
- 可以看到打包后的结果中只打包了mul方法,并没有打包count方法。即tree-shaking。
注意:
在package.json文件中配置
"sideEffects":false
代表所有代码都没有副作用,都可以进行tree-shaking
,如果这样配置会导致index.js
中引入的css文件失效,被tree-shaking
剔除出去了
code split
配置多路口:下面这样配置index.js文件中就不需要引入test.js了,把index.js文件中test.js的内容删除。之前的写法是单入口。
entry:{
//每个入口都输出一个bundle
main:'./src/js/index.js',
test:'./src/js/test.js'
},
output:{
//输出文件名也需要修改相应的配置,防止文件名重复报错
// [name]对应entry中的文件名
filename:'js/[name].built.[contenthash:10].js',
path:resolve(__dirname, 'build'), //__dirname代表当前文件的目录绝对路径
},
上面这种做法麻烦,可以通过下面的配置
optimization:{
splitChunks:{
chunks:'all'
}
}
- 多入口, 单入口都会将node_modules中代码单独打包一个chunk输出
- 自动分析多入口chunk中,有没有公共的文件。如果有会打包成单独一个chunk
如果向让某个文件单独打包成一个chunk,可以通过js代码来写
//import动态导入语法,能将某个文件单独打包
import(/* webpackChunkName:'test' */ './test.js')
.then(({mul, count}) => {
console.log(mul(3, 8))
})
.catch(() => {
console.log('文件加载失败')
})
懒加载和预加载
懒加载
js文件的懒加载。触发了某些条件才加载。
在index.js文件中写下面的代码
console.log('index.js文件被加载了')
import {mul} from './test.js'
document.getElementById('btn').onclick = function(){
console.log(mul(4, 5))
}
可以看到index.js文件和test.js文件都被加载了,但是test.js文件初始应该不需要使用,点击按钮才会加载(懒加载)
修改如下:
console.log('index.js文件被加载了')
document.getElementById('btn').onclick = function(){
//分割成了一个名为test的单独js文件,并且实现了懒加载
import(/* webpackChunkName: 'test' */'./test').then(({mul}) => {
console.log(mul(4, 5))
})
}
这样初始test.js文化不会被加载,只有点击了按钮,才会加载test.js文件
预加载
设置webpackPrefetch: true
console.log('index.js文件被加载了')
document.getElementById('btn').onclick = function(){
import(/* webpackChunkName: 'test', webpackPrefetch: true*/'./test').then(({mul}) => {
console.log(mul(4, 5))
})
}
刷新页面,f12查看网络
可以看到test文件已经被加载了,在点击按钮时,其实加载的是之前的缓存。
总结:
懒加载:当文件需要使用时才加载
预加载:会在使用前,提前加载js文件
正常加载:并行加载(同一时间加载多个文件),预加载是等其他资源加载完毕,浏览器空闲了,在偷偷加载资源。
PWA
离线也能访问,渐进式网络开发应用程序
workbox --> workbox-webpack-plugin
//1.引入
const WorkboxWebpackPlugin = require('workbox-webpack-plugin')
//2.在 plugins 中配置
new WorkboxWebpackPlugin.GenerateSW({
// 1.帮助servicework快速启动
// 2.删除旧的servicework
// 生成一个servicework配置文件
clientsClaim:true,
skipWaiting:true
})
//3.在index.js文件中注册 servicework
// 注册servicework
// 处理兼容性问题
if('serviceWorker' in navigator){
window.addEventListener('load', () => {
navigator.serviceWorker
.register('./service-work.js')
.then(() => {
console.log('sw注册成功')
})
.catch(() => {
console.log('sw注册失败')
})
})
}
servicework必须运行在服务器上
—> node.js
—> npm i serve -g
serve -s build
启动服务器,将build目录下所有资源作为静态资源暴露出去
可以在应用程序的服务工作进程中查看注册的serviceworker信息,在缓存存储中查看缓存的资源。
此时将网络置为offline,并刷新页面,可以看到下面的资源任能被访问,是从serviceworker中获取的。
多进程打包
安装 thread-loader
{
test:/\.js$/,
exclude:/node_modules/,
use:[
//开启多进程打包
// 进程开启大概为600ms,进程通信也有开销
// 只有工作消耗时间比较长,才需要多进程打包
'thread-loader',
{
loader:'babel-loader',
options:{
//预设,指示babel做怎么样的兼容性处理
presets:[[
'@babel/preset-env', //只能做简单的兼容性处理
{
useBuiltIns:'usage',
corejs:{
version:3
},
targets:{
chrome:'60',
firefox:'60',
ie:'9',
safari:'10',
edge:'17'
}
}
]],
//开启babel缓存,第二次构建时会读取之前的缓存
cacheDirectory:true
}
}
]
},
可以自己配置thread-loader
{
loader:'thread-loader',
options:{
workers:2
}
},
externals
externals:{
// 忽略库名 --- npm包名
// 拒绝jQuery被打包进来
jquery:'jQuery'
}
在https://www.bootcdn.cn/
中找jquery的cdn地址
添加到index.html文件中
DLL
对代码进行单独打包
// 使用dll技术,对某些库(第三方库:jquery,react, vue...)单独打包
// 当使用webpack时,默认查找的是webpack.config.js配置文件
// 现在需要运行webpack.dll.js配置文件
// 所以运行指令需要改为webpack --config webpack.dll.js
const {resolve} = require('path')
const webpack = require('webpack')
module.exports = {
entry:{
//要打包的库是第二个jquery
jquery:['jquery']
},
output:{
//打包生成文件的[name]为上面的jquery
filename:'[name].js',
path:resolve(__dirname,'dll'),
library:'[name]_[hash]', //打包的库里面向外暴露出去的内容叫什么名字
},
plugins:[
//打包生成一个manifest.json ---> 提供jquery映射
new webpack.DllPlugin({
name:'[name]_[hash]', //映射库的暴露的内容名称
path:resolve(__dirname, 'dll/manifest.json') //输出文件路径
})
]
}
在webpack.config.js中进行配置,不需要再打包jquery了
const webpack = require('webpack')
在plugins中添加
//告诉webpack哪些库不参与打包,同时使用时的名称也得变
new webpack.DllReferencePlugin({
manifest:resolve(__dirname,'dll/manifest.json')
})
插件 add-asset-html-webpack-plugin
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin')
//将某个文件打包输出出去,并在html中自动引入该资源
new AddAssetHtmlWebpackPlugin({
filepath:resolve(__dirname,'dll/jquery.js')
})
正在学习中= =,有错误的地方欢迎批评指出~谢谢。