目录
什么是webpack?
简要介绍
webpack中的模块
打包(bundle)的概念
webpack的工作
webpack如何分析模块依赖关系
webpack五大核心概念
entry
output
mode
loaders
plugins
安装webpack
安装前准备
全局安装 or 局部安装
开发依赖 or 生产依赖
安装步骤
webpack的基本使用(entry、output、mode)
webpack命令
带参数webpack命令
带参数webpack命令注册为package.json的scripts
webpack.config.js配置文件
CSS文件打包
css-loader
style-loader
less-loader、sass-loader
mini-css-extract-plugin
browserslist
postcss的基本使用:postcss、postcss-cli、autoprefixer、postcss-preset-env
postcss-loader
css-loader的importLoaders属性
css-minimizer-webpack-plugin
HTML文件打包
单页面应用
html-webpack-plugin
处理单页面的favicon.ico图片:copy-webpack-plugin
HTML压缩
JS文件打包
babel的基本使用:@babel/core、@babel/cli、@babel/preset-env
babel-loader
@babel/polyfill
JS混淆压缩:terser-webpack-plugin
资源模块
资源模块类型
asset/resource
asset
webpack自动打包
自动删除旧的打包目录
watch模式
webpack-dev-server
HotModuleReplace 热模替换
proxy代理
SourceMap
区分打包环境
webpack是一个前端模块打包器。
webpack是基于nodejs开发,因此webpack支持commonjs模块化,而commonjs规范中,js和json文件会被当成模块,因此webpack可以直接识别js和json文件为模块。
在webpack中,打包指的是将具有依赖关系的多个模块合并为一个文件的过程。
分析模块之间的依赖关系生成依赖图,并按照用户指定的打包规则,将依赖图中的多个模块打包为一个或多个静态资源文件。
如上图所示,
这里我们需要注意的是,webpack分析模块依赖关系时,不单单只依靠commonjs的require语法,还包括不限于如下模块化语法:
更多关于webpack模块请参考Modules | webpack
webpack有五大核心概念,也可以称为五大核心配置:
entry配置用于告知webpack从哪个模块开始分析模块依赖,默认值是项目根目录下./src/index.js
output配置用于告知webpack打包后的静态资源文件输出到哪,默认值是项目根目录下./dist/main.js
mode配置用于告知webpack用哪种打包模式打包,有两个取值:development和production,默认值是production
webpack只能识别js和json文件为模块,其他类型的文件无法被webpack识别为模块,如html、css、图片等,因此非js、json文件无法被webpack打包。
而loader的工作就是匹配特定类型的文件,然后将其翻译为可以被webpack识别的模块。
webpack的打包过程就像是一条流水线,在特定时机会暴露特定的钩子,我们可以通过plugin去调用钩子,对正在被打包的文件做一些优化处理,比如浏览器兼容性处理、文件压缩、位置变动等。
相比loader只能匹配特定类型的文件做翻译工作,plugin的自由度更高和工作内容更丰富。
由于webpack是基于nodejs开发的,并发布在了npm上,因此我们需要在本地先安装好node环境,然后才能安装webpack。
如果本地只有一个项目需要webpack打包,或者本地有多个项目但是使用同一个webpack版本,则可以选择全局安装webpack,全局安装的好处是,webpack命令会自动注册为系统变量,我们可以在任意目录下使用webpack命令。
如果本地有多个项目,并且多个项目使用的webpack版本不一样,则不建议全局安装webpack,因为全局安装的webpack只能选择一个版本,而webpack不同版本之间的差异较大,会发生严重的兼容性问题,因此我们建议针对每个项目安装一个适配版本的,局部的webpack。但是局部安装的webpack的命令无法自动注册为系统变量,也就是说,局部webpack的命令无法命令文件所在目录以外的目录中使用,此时我们可以使用npx命令间接调用。
webpack只是对项目源码进行打包,并不参与项目需求功能点开发,因此webpack应该安装为开发依赖,而不是生产依赖。
1、生成node项目
npm init -y
2、安装webpack和webpack-cli
npm i webpack webpack-cli -D
webpack-cli是webpack面向用户的命令行工具,用户在命令行中键入的webpack打包命令,将被webpack-cli解析,并调用webpack包底层完成打包行为。
3、验证安装结果
npx webpack -v
我们需要注意的是,这里我们是局部安装的webpack以及webpack-cli,因此webpack和webpack-cli命令不会自动注册为系统变量,即无法在任意目录下使用webpack命令,但是我们可以使用npx来调用局部安装的node包中的命令。
webpack的基本使用指的是:单纯靠webpack实现的打包行为。
我们知道,webpack自身只能识别js和json文件为模块,并进行模块依赖关系分析,然后打包,对于其他类型文件无法识别为模块,也就无法打包。
如果我们不配置webpack的entry、output、mode配置,则它们会使用默认值:
因此,我们按照如上默认值建立一个项目
demo
└─ src
├─ index.js
├─ js
│ └─ utils.js
└─ json
└─ data.json
我们在./src/index.js中引入了./src/js/utils.js,在./src/js/utils.js中引入了./src/json/data.json
下面我们使用npx webpack来打包,打包文件被默认输出在了./dist/main.js
我们将打包文件main.js进行格式化,发现./src/js/utils.js和./src/json/data.json的内容都被封装进了一个立即执行函数中
我们的项目中可能并不想使用webpack的entry,output,mode配置的默认值,因此webpack命令可以使用命令参数--entry、--output、--mode来自定义配置
如上面例子,我们通过--entry参数来指定webpack打包入口模块为./src/app.js,通过--mode参数来指定webpack的打包模式,通过--output-path来指定webpack打包出口目录,通过--output-filename来指定webpack打包出口文件的名字
npx webpack --entry ./src/app.js --output-path ./dist --output-filename bundle.js --mode development
由于带参数的webpack命令过长,因此,我们可以考虑将它注册为node项目的短命令
此时,我们只需要 npm run build 即可执行注册在package.json的scripts.build中的命令
我们可以将webpack命令参数对应的打包配置,如mode、entry、output,提取到一个独立的配置文件webpack.config.js中,此时我们执行不带参数的webpack打包命令时,webpack就会自动去命令执行目录中查找webpack.config.js的导出对象中的打包配置,如下例所示
一般情况下,我们会为项目定制两个webpack打包配置文件,一个用于开发过程打包,如webpack.dev.js,一个用于生产过程打包,如webpack.prod.js。
为了使用方便,我们需要在package.json的scripts中配置两个短命令,并通过--config参数指向对应的配置文件。
webpack只能识别js和json文件为模块,并进行打包,其他类型的文件无法被webpack当成模块处理,也就无法打包。
如下面例子,webpack打包入口模块./src/app.js引入的文件./src/css/common.css时,然后执行webpack打包,结果报错
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file.
你可能需要一个合适的loader去处理这种文件类型,但是当前没有配置任何loader来处理这个文件。
首先,我们需要解释下为什么webpack直接打包CSS文件会报错,下面是webpack打包流程:
webpack解析文件时,都是将文件内容当成JS代码进行解析的,因此webpack解析CSS代码时会报错,如果我们将CSS文件的内容改成JS代码,则可以解析成功,如下面例子:
因此,如果我们想让webpack打包CSS文件,则必须将CSS文件的内容先变为JS代码,而将非JS文件的内容变为JS代码就是webpack的loader的工作。
或者我们可以简单理解,webpack的loader的作用就是:将非JS文件翻译为为webpack可识别的模块。
而翻译CSS文件为模块的loader经常使用:css-loader
由于webpack是基于nodejs开发,因此loader作为webpack的辅助工具,最终是要被整合到webpack的工作流中,因此loader也是基于nodejs开发的npm包,我们可以去npm上下载loader。并且由于loader只是参与代码的打包,而不参与实际业务需求开发,因此安装时选择开发依赖。
安装好css-loader后,我们还需要告知webpack,当遇到css文件时,需要先使用css-loader将css文件翻译为webpack可识别的模块,然后再交给webpack打包,具体配置方式如下
webpack.config.js导出对象的module配置是和entry、output、mode平级的配置,module.rules是一个数组,module.rules数组的元素是一个对象,对象有两个属性:
当我们配置好css-loader后,再次重新打包,就成功了,并且common.css的内容也成功合入了打包文件bundle.js中。
但是,我们需要注意的是,当我们在HTML中引入该打包文件bundle.js时,bundle.js并不会自动将样式引入HTML文件中,如下面例子中,虽然index.html引入了bundle.js,但是bundle.js运行完成后,并没有将打包的样式插入到style标签中,或者通过link标签引入
因此,css-loader仅仅只做了翻译CSS文件为模块的工作,而并不会管打包进bundle.js的样式后续如何使用。
为了让被打包的css文件中样式可以生效,我们需要借助style-loader,该loader的作用是为打包的css样式附加一段逻辑:在打包文件bundle.js被html引入执行后,实时创建一个style标签到html的head中,并将被打包的css样式插入到这个新创建的style标签中。这样的话,被打包的css样式就可以生效了。
我们需要下载style-loader
安装好style-loader后,我们将其配置进webpack.config.js中,此时需要注意的是,style-loader只能处理css-loader处理后的css模块,因此配置如下:
此时use属性需要写成一个数组,webpack在加载loader时,是从use数组的尾部开始,到头部结束,即webpack模块解析遇到css文件时,先调用css-loader处理,css-loader处理完后,将处理结果css模块交给后续的style-loader继续处理。
再次重写打包,我们可以发现打包文件bundle.js中有创建style标签的行为
将打包文件bundle.js引入到HTML文件中,可以证明,打包样式已经被插入到style标签中了
less和scss是css的预处理器语言,可以弥补css语言的编程能力不足的缺点,因此目前被广泛用于前端项目中,因此我们也应该需要webpack打包less和scss文件。而less和scss最终都需要经过预编译变为css,因此我们只需要将less和scss转为css,然后打包css即可。
而将less预处理为css,需要使用less-loader,将scss预处理为css,需要使用sass-loader。
配置时,注意less-loader和sass-loader加载顺序应该在css-loader之前,因为less文件或scss文件需要先经过预处理变为css,然后交给css-loader处理
我们分别定义了./src/css/a.less和./src/css/b.scss,并在./src/app.js中引入了它们
然后执行打包命令,成功打包,并在打包文件bundle.js中找到了a.less和b.scss预编译后的css样式代码
我们将打包文件bundle.js引入到HTML中验证
基于style-loader,我们可以将css文件中的内容插入新建的style标签中,但是通常而言,我们期望将源码中所有css文件独立打包为一个文件后被link引入,此时我们需要借助插件mini-css-extract-plugin。
安装好插件后,我们需要配置插件到webpack的配置文件中
此时,执行打包命令,webpack打包时就会将css模块打包默认输出到./dist/main.css中。
如果我们想更改css模块的打包位置和名字,则可以修改插件配置
然后重新打包
不同浏览器对于CSS样式的兼容性不同,比如有的CSS样式需要添加浏览器前缀后才能被某些浏览器识别,比如有的新特性的CSS样式某些浏览器根本无法识别,此时我们就需要对CSS样式做兼容性处理,来保证它可以被大部分主流浏览器识别。
这里我们需要注意的是,我们只能保证CSS做兼容性处理后,可以被大部分主流浏览器识别,而不是被所有浏览器识别,那么我们如何划定主流浏览器和大部分这个范围呢?
我们可以通过webpack5自带browserslist包来查询
而browserslist底层其实是去caniuse网站查询的主流浏览器范围
"Can I use" usage table
默认情况下,browserslist的查询条件是:
我们可以将browserslist的查询条件配置在package.json的browserslist配置项中,当我们执行browserslist命令时,browserslist底层就会优先去package.json中获取查询条件,而不是使用默认查询条件。
另外,我们也可以将browserslist命令的查询条件设置在独立的配置文件.browserslistrc中,该配置文件需要建立在package.json平级目录上,一般就是项目根目录
但是,如果同时存在package.json的browserslist配置项和.browserslistrc配置文件,则browserslist命令将不知道以哪个为标准去查询,因此我们只能选择其中一种方式设置browserslist查询条件
目前,市面上最好用的处理CSS兼容性的工具就是postcss,并且postcss是可以独立使用的,如果想单独使用postcss,则还需要安装postcss-cli命令行工具。
但是我们需要了解的是,postcss本身并不能直接处理CSS兼容性问题,postcss更像是一种微内核,用于为处理具体CSS兼容性问题的插件提供运行环境
autoprefixer是专门用于处理CSS兼容性前缀问题的postcss插件,我们可以通过autoprefixer官网体验下它的工作成果Autoprefixer CSS online
下面我们安装下autoprefixer
我们需要注意的是autoprefixer的依赖包中也有browserslist,因为autoprefixer不会做所有浏览器的CSS兼容性处理,而只会为从browserslist查询出来的主流浏览器做CSS兼容性处理,因此我们在package.json中配置的browserslist条件或者.browserslistrc中条件最终会被autoprefixer使用。
安装好autoprefixer后,我们需要在postcss命令中集成它,具体命令如下
比如下例中,我们创建了一个包含CSS兼容性问题样式的./src/css/common.css,然后使用postcss处理它
如果我们改变.browserslistrc的查询条件,则postcss处理结果会发生变化
CSS的兼容性问题不仅仅只有浏览器兼容性前缀,还有一些新特性的样式无法被低版本浏览器识别的问题,比如CSS样式16进制颜色值通常只有6位,比如background-color:#123456,但是最新CSS特性中,颜色16进制支持8位,即background-color:#12345678,新增的后两位会作为透明度值。
此时,postcss单纯依靠autoprefixer插件是无法处理这个问题的
此时,我们需要使用postcss推荐的另一个插件postcss-preset-env,这个插件中集成了autoprefixer,因此也可以处理兼容性前缀问题,另外该插件还具备了处理大部分CSS兼容性问题的能力,包括CSS新特性兼容性处理,因此我们完全可以使用postcss-preset-env来替代autoprefixer
postcss目前需要通过postcss-cli命令行工具手动输入命令运行,这是不符合自动化构建要求,因此我们应该将postcss交给webpack管理,而postcss也开发了postcss-loader与webpack对接。
postcss处理的css的兼容性,因此postcss-loader应该在css-loader之前工作,在less-loader、scss-loader之后工作。
另外,postcss-loader上面这种带配置的写法非常不美观,我们可以将它提取到postcss.config.js,此时当webpack解析模块,调用到postcss-loader时,就会去找postcss.config.js加载对应插件postcss-preset-env。
重新打包,发现一切OK
postcss-loader需要在css-loader前面工作,即postcss-loader先对原始CSS文件进行兼容性处理,然后再将处理后的CSS文件交给css-loader翻译为模块。
上面工作流程处理具有@import的CSS文件时存在问题,如下面例子:
在index.css中@import另一个common.css,并且common.css中的样式存在兼容性问题,此时如果按照上面webpack.config.js对css文件的loader处理,则流程如下:
实际打包后结果如下:common.css中样式并没有做兼容性处理,因为postcss-loader没有处理过它,或者说错过了它
我们期望的是postcss-loader会对@import进行模块分析,并将引入的CSS文件也做兼容性处理,但是这显然超出了postcss-loader业务范围。
因此,我们只能期望css-loader进行@import模块分析发现新的css文件时,可以回头调用postcss-loader进行兼容性处理,而这就需要借助css-loader的importLoaders属性了,该属性的值是数字,该属性的含义是:当css-loader翻译过程遇到@import引入其他CSS文件时,可以回退到css-loader之前的多少个loader开始重新处理
比如,我们期望css-loader遇到@import引入新CSS文件时,可以重新从postcss-loader开始处理新CSS文件,即回退1步
然后重新打包,此时被@import的common.css就可以被postcss-loader处理兼容性了,然后在被css-loader翻译合并打包了。
前面已经完成了对CSS的打包、兼容性处理,已经满足了开发环境的使用,但是在生产环境中,我们还需要对打包后的CSS文件进行压缩,来减小文件体积。
CSS压缩使用到了插件:css-minimizer-webpack-plugin
配置插件
我们需要注意的是该插件的配置与其他插件不同,是单独配置在optimization配置项中的,另外上面配置只对mode=production有效,对于mode=development无效。
另外压缩打包后的CSS代码的和源码之间的sourcemap设置受到webpack.config.js导出对象的devtool配置影响,并且CSS的sourcemap只支持:source-map
, inline-source-map
, hidden-source-map
, nosources-source-map。
更多关于sourcemap和devtool的知识请看后续章节。
单页面应用,也叫单页面网站,指的是一个网站只有一个html网页。
虽然单页面网站只有一个网页,但是却能通过改变URL的hash值,来触发hashchange事件,进而通过javascript操作DOM实现网页内容的局部刷新,来模拟多页面网站页面跳转的功能。
更多关于单页面应用实现,请看Vue2.x - Vue Router_伏城之外的博客-CSDN博客
目前,Vue和React标准化项目都是单页面应用。而单页面应用也更适合webpack打包。
因为,webpack打包时默认会将模块依赖图中所有js文件合并打包为一个js文件,所有css文件合并打包为一个css文件,如果打包后的js、css服务于多页面,则对于每个页面而言,必然会引入不属于本页面的行为和样式。又或者,我们打包前针对多页面设定多个入口模块,这样就会打包多组js和css,但是这样会很麻烦。
对于单页面而言,就没有这些烦恼,webpack打包后的单个js和css文件可以直接引入到唯一的单页面中。
由于单页面最终会引入打包后的js和css文件,因此源码中的单页面不需要也不应该引入未经打包的js和css文件,如Vue标准化单页面项目源码中 /public/index.html 中就没有引入任何外部js和css文件
webpack默认只能识别js和json文件为模块,无法识别html文件为模块,也就无法将html文件打包,因此我们可能需要找一些loader来将html文件翻译为webpack可以识别的模块。
但是我们还需要注意的是,打包后的js和css应该被引入到打包后的html文件中,因此html文件的打包并不只有单纯的模块翻译工作。
也就说,打包html文件,仅仅依靠loader是不够,我们应该依靠plugin做更多的事情。
目前市场主流的html打包使用的是插件:html-webpack-plugin
安装插件
配置插件
如上配置下,html-webpack-plugin在打包过程中会自动创建一个index.html,并且会在index.html文件中自动引入打包合并后的js文件以及css文件,如下面例子所示
这里自动创建的index.html文件其实是参照的html-webpack-plugin内置的default_index.ejs模板生成的,如下图
这里ejs模板中htmlWebpackPlugin.options.title默认取值"Webpack App",我们可以在插件配置中修改它
然后重新打包,发现打包目录中单页面的title确实改变了
但是靠这html-webpack-plugin的默认模板创建的index.html并不能满足我们的项目要求,因为默认模板中没有任何实际内容,此时我们可以参考Vue标准化单页面项目的 /public/index.html创建对应单页面,Vue的单页面中并没有实际内容,只有一个
比如,我们在./src/app.js中定义一段插入span标签到
此时,我们让还需要让html-webpack-plugin更改单页面模板为/public/index.html
然后重新打包,却发现报错,提示解析/public/index.html时,无法找到BASE_URL变量定义
这里BASE_URL是定义在webpack自带插件DefinePlugin配置中的,因此我们需要在webpack.config.js中引入DefinePlugin,并配置BASE_URL为当前目录,因为打包后的页签icon和单页面在一级目录下,需要注意的是BASE_URL的配置值需要包裹两层引号
然后重新打包。即可成功
在浏览器中打开打包后HTML文件
发现网页内容展示正常,但是页签icon没有展示出来,因为我们在打包html时并没有将引用的favicon.ico文件一起带过来
html-webpack-plugin只是根据指定的模板来创建一个index.html到打包目录下,所以该插件本质上并没有将源码中html文件翻译为模块,因此webpack不会对源码中html文件进行模块解析,也就不会衍生出对的模块依赖分析和进一步的对favicon.ico的打包。
当html-webpack-plugin创建一个新的index.html到打包目录后,效果如下
我们可以发现,其实对于favicon.ico的打包操作本质就是将favicon.ico从/public转移到/dist下。
而拷贝文件到指定目录,也有专门的插件,比较常用是copy-webpack-plugin。
安装插件
配置插件
如上例,CopyPlugin的配置patterns数组元素对象有两个属性from(源目录文件地址),to(目的目录地址),但是我们可以省略to的配置,因为不配置to属性,CopyPlugin默认获取webpack五大核心配置里面output.path作为to属性的值,省略to属性的写法不仅能简化配置,还能避免设置多处打包目录。
然后重新打包,发现/public/favicon.ico已经转移到了dist目录下,如下图所示
html打包后文件压缩,只需要在html-webpack-plugin配置中新增一个minify:true即可
再次执行打包命令
另外需要注意的是,webpack命令打包时如果设置打包模式mode为production,则html文件打包时也会被压缩,此时我们最好设置HtmlWebpackPlugin的配置minify为false
由于webpack可以识别js为模块,因此js可以直接被webpack打包,所以我们打包js文件时主要是处理js的浏览器兼容性。
而js的兼容性处理主要就是ES6转ES5,而市面上最好用的ES6转ES5工具就是babel,并且babel是可以单独使用的,下面我们介绍如何单独使用babel来处理js兼容性。
babel和postcss很像,独立工作使用时都需要安装cli工具,比如postcss需要安装postcss-cli,而babel需要安装@babel/cli,来处理用户输入的命令。
并且,babel和postcss都是多包架构,即将复杂的兼容性问题拆分为一个一个小问题,每个小问题的都由一个包来处理,如postcss处理浏览器兼容性前缀使用的包是autoprefixer,postcss包本身更像是一个微内核,本身不处理具体兼容性问题,而是提供autoprefixer的运行环境,babel也是如此。
babel的微内核是@babel/core,@babel/core本身不能处理具体的兼容性问题,只是为其他包提供运行环境。
如上例中,我们只安因为只装了@babel/core,所以无法处理任何JS兼容性问题,比如src/index.js中的let,const声明,以及箭头函数。
如果想要将ES6的let,const声明转为ES5的var,则还需要安装使用@babel/plugin-transform-block-scoping
此时,我们执行babel命令时集成 @babel/plugin-transform-block-scoping插件,就可以处理let,const转var了
而将ES6箭头函数转为ES5普通函数,则需要安装使用@babel/plugin-transform-arrow-functions
如上面例子,执行 babel命令时带上@babel/plugin-transform-arrow-functions,就可以将ES6箭头函数转为普通函数了。
但是ES6语法有很多,我们很难一一下载对应的babel包,因此babel提供一个预设包:@babel/preset-env,该预设包中集成了很多常用的ES6转ES5的处理包
注意此时,命令参数使用--presets来接收@babel/preset-env,而不是--plugins
默认情况下,babel会将ES6转ES5,但是现在越来越多的浏览器开始支持ES6语法,因此对于部分浏览器来说没有必要做ES6转ES5。
而babel支持根据browserslist查询出来的浏览器范围,来判断是否需要做ES6转ES5的兼容性处理,如果浏览器范围中有一个浏览器不支持ES6,则babel就会进行ES6转ES5。
因此,我们可以配置.browserslistrc来设置处理哪些浏览器的兼容性
通过上面例子,我们可以发现,市场占有率>1%,最近两个版本,24个月内有过更新的浏览器已经全部支持ES6语法了,因此babel就没必要对这些浏览器做ES6转ES5的工作了。
如果将市场占有率缩小,即扩大支持的浏览器范围,此时有的浏览器就不支持ES6了,因此babel会将ES6转ES5。
单独使用babel的话,我们都需要手动输入babel命令进行ES6语法转换,很麻烦,因此我们一般将babel交给webpack管理,而babel也开发了babel-loader来对接webpack。
如果我们嫌babel-loader配置插件规则放在webpack.config.js中不好看,则可以将其配置提取到babel.config.js中
此时,babel执行时就会去查询babel.config.js中的配置的插件。
执行打包
可以发现打包后文件中的ES6语法成功转为了ES5语法。
@babel/preset-env其实并不能处理所有的ES6语法兼容性,比如Promise、Generator等,如下例所示:打包后bundle.js中的Promise并没有转为ES5语法
而Promise、Generator这些ES6语法无法通过ES5语法简单地实现,而是需要polyfill源码重新定义,比如IE10无法识别Promise,因此我们需要重新定义function Promise的实现,这就是polyfill。而babel将这些需要polyfill源码重新实现的ES6兼容性处理都整理到了@babel/polyfill。
由于@babel/polyfill最终需要注入到项目代码中,因此是生产依赖,注意安装时带上-S。
安装完后,我们只需要在我们入口模块顶部引入@babel/polyfill即可,这样就完成了polyfill注入
然后执行打包命令,由于此时最终打包文件中引入了大量polyfill,因此打包文件无法直观看到转换后的Promise,我们直接将打包文件引入html中,并在IE10中展示
发现不支持Promise的IE10也可以使用Promise语法了。
但是这种导入整个@babel/polyfill,会造成打包文件的体积非常大,影响网页的加载速度
因此我们不建议直接引入@babel/polyfill。
在babel官网中,提示@babel/polyfill已经过时了,建议我们现在下载core-js和regenerator-runtime来代替
@babel/polyfill · Babel 中文文档 (docschina.org)
当我们安装好core-js和regenrator-runtime后,我们可以按照如下规则配置
为了性能最优,我们肯定选择useBuiltsIns:'usage'按需引入
需要注意的是:
此时重新打包,然后在IE10上运行,同样可以成功,但是,polyfill引入的体积却小了很多
一般情况下,只有在生产环境下使用的JS文件才需要混淆压缩,开发环境下使用的JS不需要混淆压缩。
而webpack打包时,如果mode=production,则默认自动混淆压缩打包后的JS文件,如果mode=development则不会混淆压缩打包后的JS文件。
但是需要注意的是,如果我们在webpack.config.js的导出对象中配置了optimization配置项,则mode=production模式下的webpack打包则不会进行JS的混淆压缩。
此时,我们可以使用webpack官方推荐的JS混淆压缩插件 terser-webpack-plugin
此时再执行打包,就可以完成打包JS文件的压缩混淆了
webpack4打包过程中遇到HTML中引用的图片,如img标签的src属性引用的图片,或者遇到CSS中引用的图片,如background样式url函数引用的图片,是无法直接当成模块打包的,因此需要借助第三方loader来翻译,通常使用file-loader和url-loader,其中url-loader可以设定文件大小阈值,大小低于阈值的图片会被转为Data URI字符串(base64编码),这样就可以将小体积的图片合入HTML或CSS文件中,减少请求服务器资源的次数,同时也不会太增大单次请求的带宽压力,起到文件打包优化的作用。
webpack5开始,webpack支持通过内置的资源模块asset modules来直接打包模块依赖中资源文件,webpack5内置的资源模块有四种类型:
webpack5打包过程中,如果模块分析时遇到样式中通过url函数引入图片,则会默认隐式地使用asset/resource处理,而不需要显示地配置它
如上例中,webpack打包时,首先从入口模块./src/app.js进行模块解析,解析第一行时发现了模块依赖./src/css/index.css,而index.css后缀是css类型,则会被css-loader进行模块解析,在解析过程中又遇到了background:url引入了./src/img/bg.jpg图片,因此触发新的模块依赖,但是bg.jpg的文件类型并没有配置对应的loader处理,因此webpack5会将bg.jpg交给默认的asset/resource处理,而asset/resource会默认拷贝./src/img/bg.jpg到打包目录dist下,并返回新的URL作为backgroud:url函数的入参。
在HTML中通过img标签src属性引入的图片同样会默认使用asset/resource来打包,但是我们目前HTML文件的打包依赖的是html-webpack-plugin,该插件不会将源码中HTML文件翻译为模块,因此HTML文件中的img标签的src引用也不会被模块依赖分析到,因此无法被asset/resource拦截处理。
另外,在单页面应用中,单页面只作为模板,其中不会包含实际内容,单页面中的内容(包括img标签)基本上都是通过JS的DOM操作实时插入的,因此对于单页面应用而言,我们更多地需要关注如何打包JS文件中动态创建地img标签src属性引用地模块,比如下面例子:
动态创建了一个img标签,并设置img.src为一个图片
下面我们执行打包操作,看看是否可以触发asset/resource来打包图片
通过打包结果和实际浏览器运行结果可以发现,JS中动态创建的img标签的src引用的图片并没有被asset/resource打包。
这是因为JS中动态创建的img标签的src属性被赋值为一个字符串,而不是一个图片文件,因此webpack对app.js进行模块解析时,并没有把img的src引用当成模块依赖。所以,我们需要使用require模块化语法导入图片给img.src属性
可能此时会有人发出疑问,这里require('./img/vue.png')的结果是啥呢?可以赋值给img.src吗?
我们知道webpack可以识别require,并会尝试对require的图片文件进行模块解析,而当前没有配置任何loader来处理图片文件,因此webpack可能会找默认的asset/resource来处理图片文件的打包,而asset/resource除了会将源码中图片文件拷贝到打包目录中,还会返回对应URL作为require函数的返回值,因此这里将require的结果赋值给img.src是合理的。
但是实际打包却报错了,提示模块解析失败,即require导入的图片并不会触发默认的asset/resource进行打包处理,因此我们需要配置asset/resource去处理图片
资源模块类型的配置和loader的配置类似,都是配置在module.rules中,但是资源模块类型使用type引入,而loader使用use引入。
配置完成后,重新打包
通过打包结果和实际浏览器运行结果可以发现,图片被成功打包了。
如果我们嫌弃图片打包后的名字和位置不合适,我们可以通过generator.filename配置修改
generator.filename可以使用占位符来定义动态文件名,常用的占位符有:
另外,对于图片文件来说,如果图片文件体积小于8KB,我们通常将图片文件转为data url字符串合并到引用它的代码中,这样会减少一次服务器资源请求,因此我们通常使用另一个资源模块类型asset来处理图片的打包
上面配置,会让小于8KB的图片转为data url合入到代码中,而大于8K的图片直接拷贝到打包目录,如下面例子中bg.jpg是92KB,vue.png是7KB
因此,img标签src引入的vue.png会被转为data url合入代码,而background:url样式引入bg.jpg会被拷贝到打包目录
执行打包
通过打包结果和浏览器运行结果来看,确实如此。
进行新的打包操作前,我们都需要删除旧的打包目录。这是因为如果新的打包结果只会覆盖旧的打包目录,因此如果新的打包结果中删除了几个文件,则无法完全覆盖旧的打包目录,而产生冗余文件。因此为了保险起见,我们每次打包前都需要删除旧的打包目录。
但是目前,我们都是手动删除旧的打包目录,这是不符合自动化构建要求的,而目前最常用的自动删包插件是:clean-webpack-plugin
开发调试阶段,我们通常经历如下几个过程
以上过程中,有几个手动,其中手动打开浏览器和手动部署出包文件到服务器,我们可以借助VSCode的插件liveServer一站式完成。因此最浪费时间的手动操作就是执行webpack打包命令了。
那么有没有一种可能,我们修改保存完源码后,webpack自动进行打包呢?
webpack针对这个需求,提供了watch模式打包,即如下命令
webpack --watch
当我们首次执行webpack --watch命令进行打包,webpack打包完成后并不会自动退出,而是处于监听模式,一旦源码发生变动,则会自动触发打包操作。
这样,我们在开发过程就不用每次修改完源码后重新手动执行打包命令了。
需要注意的是:webpack --watch命令打包和webpack命令打包是不一样的,webpack命令打包是对项目所有文件进行重新打包,而webpack --watch只会对发生修改的文件进行重新打包,可能打包过程中也会引起模块依赖的其他文件重新打包。
因此webpack --watch命令打包的所需时间要少于webpack命令打包。
前面说了开发调试过程中的三个手动操作:
其中第3个,我们可以通过webpack --watch来解放双手,并且可以花费更少的时间来打包。而第1、2我们是借助VSCode插件liveServer来完成的,liveServer可以自动开启一个服务器,将当前项目所有文件都部署到服务器上,这其中当然也包括我们打包后的文件
但是liveServer只能部署磁盘上的文件到服务器,这意味着,我们webpack打包的结果只能写入到磁盘文件上,而磁盘上读写数据是非常浪费时间,如果我们直接将打包结果缓存在内存中,然后有一台服务器可以直接读取缓存在内存中的打包文件,那样的话将大大降低我们出包的时间(节省了读写磁盘的时间)。
而webpack官方推出的webpack-dev-server工具,就可以完成上面两个关键任务:
下面来安装webpack-dev-server
在webpack.config.js的devServer属性下配置webpack-dev-server开启的服务器所占用的端口号,
执行新的打包命令:webpack-dev-server
我们需要注意的是webpack-dev-server打包命令不会产生dist目录,因为webpack-dev-server会将打包文件缓存在内存中,而不是磁盘上。
此时我们只需要访问http://localhost:8080/index.html即可
当然,我们也可以通过该服务器访问项目源码
如果我们想在webpack-dev-server打包完成后自动打开浏览器访问打包结果,则可以在webpack.config.js导出对象的devServer中设置open:true
另外,我们还可以将webpack-dev-server命令配置为短命令
在开发调试过程中,我们经常会遇到这样一种情况:
我们需要经过极其繁琐的业务步骤才能进入到某个测试用例的界面,但是在我们测试该用例的过程中发现了问题,于是需要修改源码重新打包部署,而一旦服务器上的代码被重新部署了,则我们需要刷新浏览器重新访问,这也意味着我们要再次重复繁琐的业务步骤进入对应测试用例的界面,非常浪费时间。
有没有一种可能,服务器支持进行热部署,即只会重新部署发生变动的代码,其余代码部署状态保持不变呢?
而这就是webpack-dev-server的热模替换功能,我们可以设置webpack.config.js导出对象的devServer配置下的hot:true,此时就会开启热模替换
下面是未开启热模替换的情况:
一旦服务器的代码重新部署,则浏览器会自动刷新,导致网页内所有用户操作数据丢失
下面是开启了热模替换的情况,
即使服务器的代码重新部署,浏览器也不会自动刷新,而是局部更新发生改变的组件,这样的话用户操作的数据不会丢失
当我们使用webpack-dev-server打包前端项目后,前端项目会被部署在webpack-dev-server开启的本地服务器上,而该服务器和后端服务器肯定不是同一个(协议、域名、端口存在一个不同),因此,前端网页通过浏览器发送请求到后端服务器,必然会产生跨域问题,为了解决跨域,我们通常采用的方式:
当然,如果后端服务器彻底放开跨域请求,则前端不需要做任何处理,如果后端服务器没有开放跨域,或者只针对特定服务器开放跨域,则此时我们只能使用代理服务器转发前端请求,而对于webpack-dev-server而言,它本身就是一个服务器,因此我们不需要在借助额外的服务器,我们只需要将网页中发送到后端服务器的请求,改为发送到webpack-dev-server开启的服务器即可,然后让webpack-dev-server开启的服务器转发请求到后端服务器,这样就可以避开浏览器的跨域拦截
更多关于跨域的了解,请参考前端网络基础 - 跨域xhr/fetch_伏城之外的博客-CSDN博客_xhr跨域请求
下面我们利用nodejs+express创建一个后端服务器,端口80,注意此时后端服务器接口响应没有设置CORS头,因此浏览器端会进行跨域校验
const express = require("express");
const app = express();
/* 接入所有请求 */
app.use((req, res, next) => {
console.log(req.url);
next();
});
/* 接入http://127.0.0.1:80/api/users的请求 */
app.get("/api/users", (req, res) => {
res.status(200).json([
{
name: "qfc",
age: 18,
},
]);
});
app.listen(80, () => {
console.log("服务器启动成功");
});
然后前端部署在webpack-dev-server开启的服务器上,端口号8080
前端网页中会通过fetch发起一个AJAX请求,请求地址就是后端服务器的/api/users接口。
启动webpack-dev-server后,就会触发打包并自动部署打包文件到前端服务器上,以及自动打开浏览器访问前端首页,然后触发fetch请求。最终结果如下,由于前端网页所在服务器和fetch请求的服务器的域名和端口不同,因此被浏览器CORS校验失败,服务器的响应被拦截了。
此时,我们可以通过webpack-dev-server开启的服务器,即前端项目部署的服务器,来转发前端网页的发起的fetch请求,因此我们需要改变:
此时fetch请求地址不需要再写具体的协议、域名和端口,不写的话,会默认使用当前webpack-dev-server开启的服务器的协议、域名、端口,即fetch('/proxy/api/users')本质上请求的是fetch('http://localhost:8080/proxy/api/users')。
当fetch('/proxy/api/users')请求被触发时,会先去本地服务器查找有没有对应接口,如果没有则会去找webpack.config.js的devServer.proxy配置,看有没有对应的转发配置
devServer: {
proxy: {
"/proxy": {
target: "http://127.0.0.1:80",
pathRewrite: { "^/proxy": "" },
changeOrigin: true,
},
},
}
devServer.proxy配置对象的属性是一个请求接口地址前缀,比如我们fetch('/proxy/api/users')的请求接口地址前缀就是"/proxy",此时就会匹配到 devServer.proxy."/proxy"属性,然后获取的转发逻辑,target表示转发到哪个服务器(需要提供协议、域名、端口),pathRewrite用于重写请求接口地址,比如{”^/proxy“:”“}表示将请求接口地址'/proxy/api/users'中开头的”/proxy“替换为”“,则最终转发出去的接口地址为 ”/api/users“,结合前面的服务器地址,最终请求地址是”http://127.0.0.1:80/api/users“。
changeOrigin:true,表示将代理服务器转发的请求头中的origin值也改为http://127.0.0.1:80,防止后端服务器也做了请求来源的校验。
为什么devServer.proxy需要匹配请求前缀来处理转发呢?
这是为了防止前端对接多个后端服务器,此时为了区分请求发往哪个后端服务器,只能添加请求前缀,比如发往server1的就加server1前缀,发往server2的就加server2前缀,这些前缀并不属于实际后端接口地址,因此需要pathRewrte去除掉。
在开发调试过程中,不可避免地会产生报错,但是我们该如何定位报错信息呢?
如下面例子中,源码中,我们访问了一个未定义的变量aa
源码被打包部署后,通过浏览器访问必然会产生报错
通过浏览器的报错信息,我们可以定位在box.js的76行产生了错误
但是我们回到源码中查看时,却并非76行,而是第6行。
这是因为,浏览器中报错信息的位置是基于打包后的文件定位的,而不是源码中的文件定位的。
为了可以准确定位到源码中的报错位置,我们需要让webpack打包时为打包文件生产SourceMap,所谓SourceMap即建立打包文件和源码之间联系,比如打包文件中某段代码对应源码中第几行。
而webpack中,是否生成SourceMap,由webpack.config.js中的devtool配置控制。
如果我们未配置devtool,则根据mode配置默认生成devtool。比如mode=development,则默认devtool是"eval",即不生成sourcemap文件。
如果我们想要生成sourcemap,则需要修改devtool:'source-map'
此时,webpack打包后就会为打包文件和源码之间建立联系,并生成对应的sourcemap来保存这种联系,如上面的bundle.js.map。此时我们再次回到浏览器定位报错,就可以找到源码报错位置
需要注意的是,这里的报错信息是精确定位,即明确给出报错的信息的行列位置,当然代价就是生成sourcemap所需要的时间更多,体积更大。
而报错定位有时候,不需要精确到列,只需要精确到哪一行即可,因此我们还可以设置devtool:'cheap-module-source-map',来减少生成sourcemap的代价
当然上面sourcemap的配置,我们只能在开发阶段设置,在项目上线我们不应该设置sourcemap,因为这样会暴露源码。
而当mode=production时,则默认设置devtool:'none',即不生产sourcemap文件。
针对开发环境和生产环境,我们应该使用不同的打包配置。
对于开发环境而言,我们追求快速出包,快速部署,热模替换,sourceMap,其余最好和生产环境保持一致。
因此,开发环境和生产环境具有一些公共配置,我们应该将这些公共配置提取出来进行复用,因此打包配置最终设计如下
其中,webpack.common.js是公共配置,webpack.prod.js是生产环境独有配置,webpack.dev.js是开发环境独有配置。
我们这里将三个打包配置放到了一个专门的config文件夹下,因此打包配置中使用到相对路径的配置项就会产生路径查找错误,为了解决路径问题,我们自定义了一个rootPath.js
const { resolve } = require("path");
module.exports = (path) => {
const root = process.cwd();
return resolve(root, path);
};
rootPath.js返回一个函数,该函数接收一个相对路径,然后使用process.cwd()获取到打包命令执行目录,即项目根目录,然后将项目根目录与入参相对路径拼接,就得到了一个绝对路径。
我们可以将上面三个打包配置文件中 webpack.?.js 使用到的相对路径替换为绝对路径。
其次,就是打包配置的合并问题?到底是common合入到dev、prod中,还是dev、prod合入common中?其实都可以,但是这里我们推荐使用dev、prod合入common中。
原因是:如果是common合入到dev、prod中,则需要在dev、prod中分别写一次合入逻辑,如果是dev、prod合入common中,则只需要在common中写一次合入逻辑。
但是,使用dev、prod合入common中,我们需要知道何时合入dev到common,何时合入prod到common。例如:如果我执行npm run build,我期望的是prod 合入 common,产生一个新配置作为打包配置,但是我们如何在common中得知本次打包是生产打包呢?
如下面短命令设置,我们可以给打包命令传入一个新参数--env,该参数的值会传递给common配置文件的导出函数的入参。
我们需要注意的是,webpack.common.js配置文件导出的是一个函数,函数的返回值是最终打包配置对象 。
如果--env production,则webpack.common.js配置文件导出函数入参env.production的值就是true
如果--env development,则webpack.common.js配置文件导出函数入参env.development的值就是true
接下来就是如何合并comm和prod或dev的打包配置了,此时合并操作并不能简单使用{...comm, ...prod}这种对象解构重组的方式,因为这种方式只能合并comm对象和prod对象的一级属性,无法合并一级属性下子孙属性。因此这种合并操作的逻辑非常复杂,好在webpack为我们提供了工具方法,我们需要安装 webpack-merge 插件,该插件导出对象下有一个方法merge,可以合并打包配置对象。
最终案例请见
qwx427219/webpack5_config_reference: webpack5基础配置参考 (github.com)https://github.com/qwx427219/webpack5_config_reference