Webpack概述
浏览器只能忍受普通的html、css、js文件,我们在前端开发过程中使用新特性、新语法写出的vue、jsx、ts等代码浏览器都是不认识的,这时候就需要像Webpack这样的打包工具进行打包,将这些代码转为浏览器认识的代码。Webpack使用的是js代码进行开发,基于Node平台运行。
与Node相关的前置知识
Webpack常常用到path模块来做绝对路径的生成,因此需要了解path.join()和path.resolve(),后者非常重要。
-
path.join()
path.join()用于两个路径的拼接,会根据不同的操作系统适配出不同的分隔符(如windows一般是\,linux是/),并不一定会返回绝对路径。
jsconst path=require("path") const path1="./lin/linlin" const path2="../wang/wangwang" console.log(path.join(path1,path2)) //结果:./lin/wang/wangwang
-
path.resolve()
path.resolve(...paths: String[])将多个路径按从右到左的顺序进行拼接,无论传入的参数如何,最终都会返回绝对路径。
返回的绝对路径有两种情况,第一是如果在拼接过程中产生了绝对路径,则不会再往前进行拼接,立刻直接返回一个绝对路径。此时的绝对路径又有两种结果:如果拼接结果没有盘符,则会返回
[项目所在盘符]:\[该绝对路径]
;如果拼接结果是一个带盘符的完整的绝对路径,如C:/lin/linlin,则直接返回该绝对路径。js//该文件位于C盘某项目中 const path=require("path") const path1="/lin" const path2="/linlin" console.log(path.resolve(path1,path2)) //结果:C:\lin\linlin path1="F:" path2="/lin" console.log(path.resolve(path1,path2)) //结果:F:\lin
第二种情况是拼接的结果为相对路径,则最终返回的结果是
[项目所在文件夹的绝对路径]:\[该相对路径]
。jsconst path=require("path") const path1="./lin" const path2="./wang" console.log(path.resolve(path1,path2)) //结果:C:\Users\HP\Desktop\前端学习\node\day3\path\lin\wang
如果不传参,该函数则返回当前工作目录
js//该文件位于当前项目path1根目录下的src文件夹的main.js const path=require("path") console.log(path.resolve()) //在当前项目根目录path1下打开cmd敲node ./src/main.js //结果:C:\Users\HP\Desktop\前端学习\node\day3\path1
Webpack的安装
目前需要安装webpack和webpack-cli两个包,前者用于源代码中使用,后者用于识别命令行中的命令,比如直接在命令行输入npx webpack
的话需要webpack-cli识别。实际工程中都是分项目安装Webpack,即每个项目都要有自己的Webpack。安装方式如下。
js
npm i webpack webpack-cli -D
Webpack的基本使用
Webpack在默认情况下对项目进行打包,会以/src/index.js
作为入口文件,并以/dist/main.js
作为出口,以/webpack.config.js
作为配置文件,命令行中输入npx webpack
可以令webpack运行(运行方式后面一般需要使用scripts优化)。
使用webpack仍然需要有一个index.html作为页面,该页面会引入打包后的js,index.html引入打包后的js代码如下。
html
<!--index.html-->
<script src="./build/bundle.js"></script><!--这里将bundle.js作为打包出口-->
webpack的入口、出口以及出口所在目录都可以通过默认的项目根目录下的webpack.config.js进行配置,如下:
js
const path=require("path")
module.exports={
entry:"./src/main.js", //入口更改为main.js
output:{ //配置出口,是一个对象
filename:"bundle.js", //配置出口文件名为bundle.js
path:path.resolve(__dirname,"./build")
//配置出口文件所在目录为[当前项目绝对路径]/build,实际出口文件则会在[当前项目绝对路径]/build/bundle.js
// 注意这里的path对应的值一定要是绝对路径,否则会报错
// path的值如果指定不正确,比如只传入一个/build,那么webpack最终的打包文件夹会在C:/build/bundle.js
}
}
注意,这里的入口用的是相对路径,但是是相对于另一个配置项context(默认值为项目根路径)来说的,而不是相对于配置文件所在目录。
webpack的默认配置文件也是可以修改的,比如想要修改为yh.config.js
,那么可以在命令行输入 npx webpack --config yh.config.js
,实际上每次在命令行中输入如此长的命令会显得复杂,因此往往通过配置package.json中的scripts来进行运行优化,scripts中的配置如下。
js
"scirpt":{
"build":"webpack --config yh.config.js" //这里配置的话不需要加npx
}
//配置完毕后,每次在命令行中输入npm run build即可进行指定配置的打包。
Webpack的打包过程
webpack在确定了入口文件后,会根据各个模块之间的依赖关系形成一个依赖图,然后遍历图依次根据不同文件进行打包,webpack默认只支持js文件的打包,less,sass等文件是不认识的,该类型文件需要使用相应的loader进行打包。从上述描述可以得知任何想要被打包的文件都要被添加到依赖图中,任何一个文件都是一个模块,在编写代码的时候需要注意这点,正确使用好import与export。
Webpack的loader
Webpack需要各种各样的loader来识别它不认识的文件。
css-loader
当我们需要模块化开发将css文件与js文件分隔开时,js文件中需要依赖到css文件中的类,如下。
js
//main.js
import "./css/content.css"
document.body.classList.add("content") //为body添加content.css中写的content类
该content类生效需要有两个步骤,首先是使用css-loader进行依赖图中css文件的识别,将css文件进行解析。然后是使用style-loader将css链接到index.html文件中,让css文件真正发挥效果。
因此,需要先安装css-loader与style-loader,注意这两个包仅用于打包,因此只需要在开发环境安装。安装过程如下。
cmd
npm install css-loader style-loader -D
然后,在webpack的配置文件中进行配置相应的loader。使用相应loader解析css文件并添加至页面的配置如下。
js
module.exports={
module:{
rules:[
{
test:/\.css$/, //解释见[1]
use:[ //记录需使用的loader,注意这里是从后往前使用,即先使用css-loader再使用style---loader
{loader:style-loader}, //将css文件链接至index.html
{loader:css-loader} //解析css文件
]
}
]
}
}
//[1]这里使用的是正则表达式,/ /代表正则表达式的开始与结束,\.代表.(.在正则中有特定含义,需要用\转义),$表示需要以.css结尾。
less-loader
如果是想解析less文件的话,首先需要使用less-loader将less文件转为css文件(注意,less-loader需要借助less库完成转化,因此需要先用npm安装less库),再使用css-loader解析css文件,最后需要将解析完毕的css文件添加至页面中。安装less和less-loader如下。
js
npm i less less-loader -D
webpack配置如下。
js
module.exports={
module:{
rules:[
{
test:"/\.less$/",
use:[
{loader:"style-loader"},
{laoder:"css-loader"},
{loader:"less-loader"}
]
}
]
}
}
postcss-loader
postcss是一个样式转换工具,可以对css进行转化和适配,比如自动加浏览器前缀以适配不同浏览器、将一些新特性转为浏览器都认识的语法、将px转为rem或vh。当然,完成上述的功能需要postcss对应的不同插件的支持,比如autoprefixer、postcss-preset-env。postcss安装如下。
js
npm i postcss-loader -D
-
autoprefixer
autoprefixer是一个postcss中自动为有兼容性问题的属性自动加上不同浏览器前缀以适配的插件,安装过程如下。
jsnpm i autoprefixer -D
配置过程如下。
jsmodule.export={ module:{ rules:[ { test:"/\.css$/", use:[ "style-loader", "css-loader", { loader:"postcss", options:{ postcssOptions:{ plugins:["autoprefixer"] //使用autoprefixer插件自动添加浏览器前缀 } } } ] } ] } }
显然直接在webpack配置文件中配置过程过于复杂,实际中postcss的相关配置会被单独抽取为一个项目根目录下的postcss.config.js配置文件(重点)。因此webpack中配置文件只需要指定loader为postcss即可。webpack的配置以及postcss.config.js中的配置分别如下。
js//webpack.config.js module.export={ module:{ rules:[ { test:"/\.css$/", use:[ "style-loader", "css-loader", "postcss-loader" ] } ] } }
js//postcss.config.js module.export={ plugins:["autoprefixer"] }
效果如下。
-
postcss-preset-env
实际开发中已经很少使用autoprefixer,因为postcss-present-env的功能更加强大,它不仅可以自动添加前缀(内置了autoprefixer),还可以将一些新特性转为浏览器认识的属性,如将#666666这样直接通过16进制设置颜色+透明度的操作转为rgba。postcss-preset-env安装如下。
jsnpm i postcss-preset-env -D
配置过程和上述相同。
babel-loader
babel用于将一些带有新特性的js代码转为es5,比如将es6中的箭头函数转为普通函数、将es6中的const转为var。注意babel本身是无法完成这样的转换的,他需要各种各样的插件的支持,比如使用@babel/plugin-transform-arrow-functions将箭头函数转为普通函数,使用@babel/plugin-transform-block-scoping将const转为var。babel-loader以及这两个插件的安装如下。
js
npm i babel-loader -D
npm i @babel/plugin-transform-arrow-functions -D //箭头函数转为普通函数的插件
npm i @babel/plugin-transform-block-scoping -D //const转为var的插件
babel-loader以及上述两个插件的配置如下。
js
module.exports={
test:/\.js$/,
use:[
{
loader:"babel-loader",
options:{
plugins:[ //babel使用的插件
"@babel/plugin-transform-arrow-functions",
"@babel/plugin-transform-block-scoping"
]
}
}
],
}
显然,这样直接在webpack中配置插件略麻烦,因此一般将babel的配置进行抽取并命名为babel.config.js。babel.config.js的配置如下。
js
module.exports={
plugins: [
"@babel/plugin-transform-arrow-functions",
"@babel/plugin-transform-block-scoping"
]
}
然而,上述的插件又多又难记,因此实际开发中一般给Webpack提供一个预设preset,Webpack根据预设来加载对应的插件列表并传递给babel。
babel的预设有常见的三种类型,一是用于js代码转换的env,另外两个分别是用于转换react代码的react以及用于转换TS的Typescript。env的安装如下。
js
npm i @babel/preset-env -D
在Webpack中的配置过程如下:
js
module.exports={
test:/\.js$/,
use:[
{
loader:"babel-loader",
options:{
presets:["@babel/preset-env"] //注意这里是预设preset配置项,已不再是plugins
}
}
],
}
将预设@babel/preset-env的配置抽取到babel.config.js中的配置如下。
js
module.exports={
presets:["@babel/preset-env"]
}
asset module type资源模块类型
在Webpack5之前,加载静态资源(如txt文件、png、jpg等图片文件)需要使用url-loader,file-loader等loader完成。在Webpack5之后,Webpack内置了加载这些资源的工具,静态资源的加载只需使用asset module type资源模块类型,一共有三种常用类型(还有一种先不管),如下。
asset/resource
指定该类型后,Webpack会将资源重命名后直接放至打包后的文件夹中,并在页面中以url的方式引用。配置如下。
js
module.exports={
module:{
rules:[
{
test:/\.(png|jpe?g|svg|gif)$/, //(a|b)表示可a也可c,?表示可选
type:"asset/resource" //注意这里的key是type而不是use
}
]
}
}
打包后的文件夹如下图。
index.html中引用到图片的地方及其方式如下图。
这种方式的优点在于打包后的js文件小,缺点在于每次获取资源时都需要发起网络请求,消耗服务器网络资源。
asset/inline
指定该类型后,Webpack会将资源文件进行Base64编码,然后嵌入至js文件中(这样会使得js文件十分庞大),再由浏览器进行解码呈现。打包后的文件夹非常简单只有一个js文件,如下图。
打包后的部分js代码如下。
浏览器中引用到图片的地方及其方式如下图,该编码十分长。
这种方式的优点在于不需要消耗服务器网络请求资源,缺点在于会使得打包后的js文件十分庞大,浏览器加载js的负担过重。
asset
上述两种类型都有各自的优缺点,但都显得过于极端,而指定类型为asset后,Webpack会根据情况自动选择类型为asset/resource或者asset/inline,比如文件大于阈值则使用url,小于阈值则使用编码。当然,这需要做一定的配置。根据阈值进行自动选择的配置如下。
JS
//配置1
module.exports={
module:{
rules:[
{
test:"/\.(png|jpe?g|svg|gif)$/",
type:"asset",
parser:{
dataUrlCondition:{
maxSize:6*1024 //这里即阈值,单位需要是字节数(byte),6*1024代表6kb
}
}
}
]
}
}
同时,也可以进行打包后出口的配置,包括配置出口文件所在的目录、文件名(默认是一个hash值),配置如下。
js
//配置2
rules:[
{
test:/\.(je?pg|png|svg|gif)$/,
type:"asset",
generator:{
filename:"img/[name]_[hash][ext]" //在img文件夹下以原图片名+hash值+后缀的格式输出,这里的[]表示占位符
}
}
]
//hash值若太长也可以进行字符串截取,比如截取前8位,generator的配置如下
generator:{
filename:"img/[name]_[hash:8][ext]" //在img文件夹下以原图片名+hash值+后缀的格式输出,这里的[]表示占位符
}
配置1和2后,打包后的文件夹如下图。
效果如下图。
Webpack解析模块的流程
Webpack中使用revolve模块来进行模块的解析,从每个import/require语句中找到需要引入的模块,其中会使用enhanced_resolve来解析文件路径。
Webpack解析路径的流程
路径共分三种情况,一是绝对路径,此时不需要进行解析。二是相对路径,则会根据上下文拼接出绝对路径。三是模块路径,会默认从node_modules中(不建议改)查找文件。
在使用相对路径的时候,往往会遇见多层目录嵌套的情况,比如utils目录中深层的mul模块需要引用utils目录下的getTime模块,如下图所示。
需要使用import "../../getTime"
,这样写太多../显然过于复杂,因此可以使用resolve.alias来对某个字段起别名,Webpack中具体配置如下。
js
module.export={
resolve:{
alias:{
utils:path.resolve(__dirname,"./src/utils") //使用绝对路径,__dirname此时为项目根目录
}
}
}
Webpack解析文件/文件夹的流程
解析路径后会确定该路径是文件还是文件夹,如果是文件,若有扩展名则不进行后缀拼接。若没有扩展名,则按照resolve.extensions的顺序对其进行后缀拼接,拼接的顺序为[".wasm",".mjs",".js",".json"]
。显然.ts、.vue等后缀是默认不拼接的,因此使用TS或是Vue开发时往往为其添加,具体配置如下。
js
module.exports={
resolve:{
extensions:[".wasm",".mjs",".js",".json",".ts",".vue"]
}
}
如果是文件夹,则默认寻找index文件(不建议改),再对其按上述的规则进行后缀解析。
Webpack中的plugin
loader代表加载器,只能完成不同模块类型的解析,plugin代表插件,可以做更加广泛的任务,贯穿于Webpack整个生命周期。
plugin的作用
- 打包优化,将样式单独抽取为一个文件并在index.html中用link方式引用,而不是用style标签直接嵌入到html页面中。
- 资源管理,二次打包前先删除上次打包后的文件夹,可以使得二次打包不需要的文件(如图片)自动删除。
- 环境变量注入,使得变量一处定义,处处可用。
资源管理clean插件
clean-webpack-plugin插件可以使得二次打包前先删除上次打包后的文件夹,可以使得二次打包不需要的文件(如图片)自动删除。安装如下。
js
npm i clean-webpack-plugin -D
Webpack配置如下。
js
import {CleanWebpackPlugin} from "clean-webpack-plugin"
module.exports={
plugin:[new CleanWebpackPlugin()]
}
效果如下,上次打包后残留rubbish.js文件(左图),使用该插件后先删除整个打包后的文件夹再重新生成打包后的新文件夹(右图)。
实际上,也可以不通过配置该插件达到该效果,只需配置webpack.output.clean为true即可,两者执行原理相同,配置如下。
js
module.exports={
output:{
clean:true
}
}
Html插件
前面所述的打包操作中打包后仅有一个js文件,需要自己手动在项目中新生成一个html文件引入打包后的js。而html-webpack-plugin插件则可以在打包后的目录中自动生成一个html文件。安装如下。
js
npm i html-webpack-plugin -D
Webpack配置如下。
js
const HtmlWebpackPlugin=require("html-webpack-plugin")
module.exports={
plugin:[new HtmlWebpackPlugin()]
}
效果如下。
该插件生成的html实际上是根据该库中的default_index.ejs中的模板进行生成的,所在位置(左图)和文件内容(右图)分别如下。
右图中可见title标签读取了插件中的options.title属性,该属性可以选择在Webpack配置文件中将插件实例化时传入,Webpack配置如下图。
js
module.exports={
plugins:[
new HtmlWebpackPlugin({
title:"电商app"
})
]
}
也可以自己指定模板,配置如下。
js
module.exports={
plugins:[
new HtmlWebpackPlugin({
template:"./index.html"
})
]
}
DefinePlugin插件
define-plugin插件用于环境变量注入,使得变量一处定义,处处可用。该插件是webpack内置插件,不需要安装,从webpack库中引入。Webpack配置如下。
js
const {DefinePlugin} from "webpack"
module.exports={
plugins:[
new DefinePlugin({
"BASE_URL":"./" //配置BASE_URL变量的值为"./"
})
]
}
在html中的使用该变量的过程如下。
html
<html>
<link ref="icon" href="<%= BASE_URL %>favicon.ico"></link>
</html>
在js中使用该变量的过程如下。
js
console.log(BASE_URL)
webpack在默认情况下会注入一个process.env.NODE_ENV来区分生产环境和开发环境(后面常用),默认是production生产环境。
process.env.NODE_ENV的值实际上是根据webpack配置项中的mode来进行配置的,取值有development|production|none,根据不同的值Webpack会进行不同的优化处理。
Webpack搭建本地服务器
上述学习过程都是使用WebStorm自带的插件完成服务器的搭建,该开发模式首先需要经过打包build,然后再重新刷新浏览器页面才可以看到最新的结果,因此存在开发效率过低的问题。
Webpack提供了webpack-dev-server库,使得开发中更改源代码后可以自动打包以及自动刷新浏览器页面,提升开发效率。webpack-dev-server安装如下。
js
npm i webpack-dev-server -D
进行使用时,只需带上serve参数即可,注意实际使用时往往配置为serve脚本以方便运行,package.json中的配置如下。
js
scripts:{
"serve":"webpack serve --config wk.config.js"
}
注意,使用该库自动打包后是在项目中看不到打包后的目录以及文件的,这里因为webpack将其打包至内存中并直接使用,而不是先写入磁盘,再从磁盘中加载至内存,这样做大大提升了开发效率。
可以通过dev-server配置项对服务器进行配置,如端口、IP地址、是否自动打开浏览器等。配置如下。
js
module.exports={
dev-server:{
host:"127.0.0.1", //主机号
port:"8080", //端口
open:true //自动打开浏览器
}
}
模块热更新HMR
在搭建本地服务器后可以自动打包并且自动刷新页面,当修改了某个模块后浏览器会刷新,仍然存在开发效率低的问题。因此可以使用模块热更新配置,原理是开启后webpack会与浏览器端建立一个websocket长连接,修改源码后重新打包,并推送到浏览器,浏览器通过devtools直接修改源码实现高效更新。
这里有两个需要注意的点:
- 开启模块热更新后,修改入口文件仍然会导致整个浏览器刷新。
- 如果入口文件依赖了更新的模块中的某些变量或函数等,那么修改该模块也会导致整个浏览器刷新。
热更新默认是开启的,可通过webpack配置文件进行修改,如下。
js
module.exports={
devServer:{
hot:true //开启热更新
}
}
虽然是开启的,但是默认不生效,需要在编写代码时候用module.hot.accept()函数指定需要热更新的模块,如下。
js
//main.js
import "./mul"
if(module.hot){
module.hot.accept("./mul",()=>{"回调函数"}) //指定mul模块修改时触发热更新,同时传入回调函数
}
js
//mul.js
console.log("mul") //修改模块内容会自动触发热更新
使用Vue或React时,框架都会完成HMR。
区分开发环境和生产环境
实际开发中会将开发环境与生产环境进行区分,并使用不同的配置。因此常将两个不同的配置放到同个文件夹下的两个不同的文件,如下图。
不同的配置文件记得配置不同mode,配置如下。
分离后两个不同的配置之间存在共性,因此可以将公共部分配置抽取为webpack.comm.config.js。然后使用webpack-merge库中的merge()函数进行配置项的合并。此时,Webpack配置项中的对应路径可能也需要作出修改,如alias中的utils值原先为 path.resolve(__dirname,"./src/utils")
,经过修改后,应更改为path.resolve(__dirname,"../src/utils")
。上述文件如下。
js
//webpack.comm.config.js
const path=require("path")
module.exports={
entry:"./src/index.js", //公共入口
output:{
path:path.resolve(__dirname,"../build"), //公共出口,注意这里要用"../build"而不是"./build"
}
}
js
//webpack.dev.config.js
const {merge} = require("webpack-merge")
const commConfig=require("./webpack.comm.config")
module.exports=merge(commConfig,{ //merge()函数合并
mode:"development"
})
js
//webpack.prod.config.js
const {merge} = require("webpack-merge");
const commConfig = require("./webpack.comm.config");
module.exports=merge(commConfig,{
mode:"production",
output:{
clean:true
}
})
然后,需要顺便修改package.json中的scripts,使其在build和serve时使用两套不同的配置,如下。
js
scripts:{
"build":"webpack ./config/webpack.dev.config.js", //使用生产环境配置
"serve":"webpack serve ./config/webpack.prod.config.js" //开启本地服务器并使用开发环境配置
}