webpack常见问题
webpack是什么?
webpack是一个用于现代JS应用程序的静态模块打包工具。
静态模块是指开发阶段可以被webpack直接引用的资源(能够直接被获取打包进bundle.js的资源)。当webpack处理应用程序时,它会在内部构建一个依赖图,此依赖图对应映射到项目所需的每个模块(不再局限JS文件),并生成一个或多个bundle。
webpack解决的问题
开发中会使用框架(React、Vue),ES6 模块化语法,Less/Sass 等 css 预处理器等语法。这样的代码要想在浏览器运行必须经过编译成浏览器能识别的 JS、css 等语法。webpack能进行转换。
编译代码:提高效率,解决浏览器兼容问题
模块整合:提高性能,可维护性,解决浏览器频繁请求文件的问题
万物皆可模块:项目可维护性增强,支持不同种类的前端模块类型,统一的模块化方案,所有资源文件的加载都可以通过代码控制
webpack的构建流程
总体运行流程
webpack的运行流程是一个串行的过程,它的工作流程就是将各个插件串联起来。在运行过程中会广播事件,插件只需监听它所关心的事件,就能加入到webpack机制中,去改变webpack的运作,使得整个系统扩展性良好。
从启动到结束会依次执行以下三大步骤:
- 初始化流程:从配置文件和shell语句中读取与合并参数,并初始化需要使用的插件和配置插件等执行环境所需要的参数。命令行接口参数的优先级高于配置文件。
- 编译构建流程:从entry出发,针对每个module串行调用相应的loader去翻译文件内容,再找该module依赖的module,递归地进行编译处理
- 输出流程:对编译后的module组合成chunk,把chunk转换成文件,输出到文件系统
初始化流程
从配置文件和 Shell
语句中读取与合并参数,得出最终的参数。配置文件默认下为webpack.config.js
,也或者通过命令的形式指定配置文件,主要作用是用于激活webpack
的加载项和插件。webpack
将 webpack.config.js
中的各个配置项拷贝到 options
对象中,并加载用户配置的 plugins
。
完成上述步骤之后,则开始初始化Compiler
编译对象,该对象掌控着webpack
生命周期,不执行具体的任务,只是进行一些调度工作。Compiler
对象继承自 Tapable
,初始化时定义了很多钩子函数。
编译构建流程
根据配置中的 entry
找出所有的入口文件。初始化完成后会调用Compiler
的run
来真正启动webpack
编译构建流程,主要流程如下:
compile
开始编译make
从入口点分析模块及其依赖的模块,创建这些模块对象build-module
构建模块seal
封装构建结果emit
把各个chunk输出到结果文件
compile 编译
执行了run
方法后,首先会触发compile
,主要是构建一个Compilation
对象。该对象是编译阶段的主要执行者,主要会依次执行下述流程:模块创建、依赖收集、分块、打包等主要任务的对象。
make 编译模块
当完成了上述的compilation
对象后,就开始从Entry
入口文件开始读取,主要执行_addModuleChain()
函数。_addModuleChain
中接收参数dependency
传入的入口依赖,使用对应的工厂函数NormalModuleFactory.create
方法生成一个空的module
对象。回调中会把此module
存入compilation.modules
对象和dependencies.module
对象中,由于是入口文件,也会存入compilation.entries
中。随后执行buildModule
进入真正的构建模块module
内容的过程。
build module 完成模块编译
这里主要调用配置的loaders
,将我们的模块转成标准的JS
模块。在用Loader
对一个模块转换完后,使用 acorn
解析转换后的内容,输出对应的抽象语法树(AST
),以方便 Webpack
后面对代码的分析。从配置的入口模块开始,分析其 AST
,当遇到require
等导入其它模块语句时,便将其加入到依赖的模块列表,同时对新找出的依赖模块递归分析,最终搞清所有模块的依赖关系。
输出流程
seal 输出资源
seal
方法主要是要生成chunks
,对chunks
进行一系列的优化操作,并生成要输出的代码
webpack
中的 chunk
,可以理解为配置在 entry
中的模块,或者是动态引入的模块
根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk
,再把每个 Chunk
转换成一个单独的文件加入到输出列表
emit 输出完成
在确定好输出内容后,根据配置确定输出的路径和文件名。在 Compiler
开始生成文件前,钩子 emit
会被执行,这是我们修改最终文件的最后一个机会。从而webpack
整个打包过程则结束了。
Loader
是什么?
用于对模块的源代码进行转换,在import或“加载”模块时预处理文件
三种配置方式
- 配置方式:在webpack.config.js文件中指定loader
- 内联方式:在每个import语句中显式指定loader
- CLI方式:在shell命令中指定
配置方式
loader
的配置写在module.rules
属性中,属性介绍如下:
rules
是一个数组的形式,因此我们可以配置很多个loader
- 每一个
loader
对应一个对象的形式,对象属性test
为匹配的规则,一般情况为正则表达式 use
针对匹配到文件类型,调用对应的loader
进行处理
特性
- loader支持链式调用,执行顺序与指定顺序相反
- loader 可以是同步的,也可以是异步的
- loader 运行在 Node.js 中,并且能够执行任何操作
- 除了常见的通过
package.json
的main
来将一个 npm 模块导出为 loader,还可以在 module.rules 中使用loader
字段直接引用一个模块 - 插件(plugin)可以为 loader 带来更多特性
- loader 能够产生额外的任意文件
常见的loader
- style-loader: 将css添加到DOM的内联样式标签style里
- css-loader :允许将css文件通过require的方式引入,并返回css代码
- less-loader: 处理less
- sass-loader: 处理sass
- postcss-loader: 用postcss来处理CSS
- autoprefixer-loader: 处理CSS3属性前缀,已被弃用,建议直接使用postcss
- file-loader: 分发文件到output目录并返回相对路径
- url-loader: 和file-loader类似,但是当文件小于设定的limit时可以返回一个Data Url
- html-minify-loader: 压缩HTML
- babel-loader :对JS进行兼容性处理
plugin
是什么?
扩展webpack功能,赋予其各种灵活的功能,例如打包优化、资源管理、环境变量注入等,它们会运行在 webpack
的不同阶段(钩子 / 生命周期),贯穿了webpack
整个编译周期。解决loader
无法实现的其他功能。
配置方法
通过配置文件导出对象中plugins
属性传入new
实例对象
特性
其本质是一个具有apply
方法javascript
对象。apply
方法会被 webpack compiler
调用,并且在整个编译生命周期都可以访问 compiler
对象。
常见的plugin
- ExtractTextWebpackPlugin:从bundle中提取文本(CSS)到单独的文件
- HotModuleReplacementPlugin:启用模块热替换
- HtmlWebpackPlugin:简单创建HTML文件,用于服务器访问
- IgnorePlugin:从bundle中排除某些模块
- LimitChunkCountPlugin:设置chunk的最小/最大限制,以微调和控制chunk
- MinChunkSizePlugin:确保chunk大小超过指定限制
loader与plugin的区别
概念区别
- loader 是文件加载器,能够加载资源文件,并对这些文件进行一些处理,诸如编译、压缩等,最终一起打包到指定的文件中。实质是一个转换器,将A文件进行编译形成B文件,操作的是文件,比如将
A.scss
或A.less
转变为B.css
,单纯的文件转换过程。 - plugin 赋予了 webpack 各种灵活的功能,例如打包优化、资源管理、环境变量注入等,目的是解决 loader 无法实现的其他事件。在
Webpack
运行的生命周期中会广播出许多事件,Plugin
可以监听这些事件,在合适的时机通过Webpack
提供的API
改变输出结果。
运行时机
- loader 运行在打包文件之前
- plugins 在整个编译周期都起作用
编写loader
loader的本质是函数,函数中的this作为上下文会被webpack填充,因此不能将loader设为一个箭头函数。loader函数接受一个参数,为webpack传递给loader的文件源内容,函数中this是由webpack提供的对象,能够获取当前loader所需要的各种信息。函数中有异步操作或同步操作,异步操作通过this.callback返回,返回值要求为string或Buffer。
一般在编写loader的过程中,保持功能单一,避免做多种功能。如less文件转换为css文件也不是一步到位,而是经过less-loader、css-loader、style-loader几个loader的链式调用才能完成转换。
1 | // 导出一个函数,source为webpack传递给loader的文件源内容 |
编写plugin
由于webpack基于发布订阅模式,在运行的生命周期中会广播很多事件,plugin通过监听这些事件,就可以在特定的阶段执行自己的任务。
webpack中的两个核心对象:
- compiler:包含webpack环境的所有配置信息,包括options,loader,plugin和webpack整个生命周期相关的钩子
- compilation:作为plugin内置事件回调函数的参数,包含了当前的模块资源、编译生成资源、变化的文件以及被跟踪依赖的状态信息。当检测到一个文件变化,一次新的compilation将被创建。
实现plugin需要遵循的规范:
- 插件必须是一个函数或者是一个包含
apply
方法的对象,这样才能访问compiler
实例 - 传给每个插件的
compiler
和compilation
对象都是同一个引用,因此不建议修改 - 异步的事件需要在插件处理完任务时调用回调函数通知
Webpack
进入下一个流程,不然会卡住
1 | class MyPlugin{ |
在 emit
事件发生时,代表源文件的转换和组装已经完成,可以读取到最终将输出的资源、代码块、模块及其依赖,并且可以修改输出资源的内容
webpack热更新的原理
是什么?
HMR(Hot Module Replacement),模块热替换,指在程序运行过程中,替换、添加、删除模块,而无需重新刷新整个应用。
1 | // webpack.config.js |
上述操作在修改CSS后,能达到不刷新的形式更新页面。但是修改JS后,页面依旧自动刷新了。这是因为还需要额外配置,需要去指定哪些模块发生更新时进行HMR。
1 | if(module.hot){ |
原理
- webpack与浏览器通过socket进行长连接
- 当某一个文件或者模块发生变化时,
webpack
监听到文件变化对文件重新编译打包,编译生成唯一的hash
值,这个hash
值用来作为下一次热更新的标识。根据变化的内容生成两个补丁文件:manifest
(包含了hash
和chundId
,用来说明变化的内容)和chunk.js
模块。 - 当文件发生改动的时候,服务端会向浏览器推送一条消息,消息包含文件改动后生成的
hash
值,作为下一次热更细的标识 - 在浏览器接受到这条消息之前,浏览器已经在上一次
socket
消息中已经记住了此时的hash
标识,这时候会创建一个ajax
去服务端请求获取到变化内容的manifest
文件。mainfest
文件包含重新build
生成的hash
值,以及变化的模块。 - 浏览器根据
manifest
文件获取模块变化的内容,从而触发render
流程,实现局部模块更新
webpack proxy
是什么?
接收客户端发送的请求后转发给其他服务器。其目的是为了便于开发者在开发模式下解决跨域问题(浏览器安全策略限制)
1 | //webpack.config.js |
原理
利用http-proxy-middleware这个中间件。通过设置webpack proxy
实现代理请求后,相当于浏览器与服务端中添加一个代理者。当本地发送请求的时候,代理服务器响应该请求,并将请求转发到目标服务器,目标服务器响应数据后再将数据返回给代理服务器,最终再由代理服务器将数据响应给本地。
服务器与服务器之间请求数据并不会存在跨域行为,跨域行为是浏览器安全策略限制
webpack优化
提升开发体验:sourceMap
提高打包构建速度:thread、oneOf、Cache
减少代码体积:tree shaking、image minimizer、@babel/plugin-transform-runtime
优化代码运行性能:code split、preload和prefetch、network cache、core-js
webpack前端优化手段
js代码压缩:
terser
是一个JavaScript
的解释、绞肉机、压缩机的工具集,可以帮助我们压缩、丑化我们的代码,让bundle
更小。在production
模式下,webpack
默认就是使用TerserPlugin
来处理我们的代码的。CSS压缩:css-minimizer-webpack-plugin。通常是去除无用的空格等。
HTML压缩:HtmlWebpackPlugin
文件压缩:compression-webpack-plugin
图片压缩:image minimizer
tree shaking
code split
module、chunk、bundle区别
module是当前项目编写的文件或引入的资源,例如a.js、b.jpg;bundle是webpack最终输出的文件,由浏览器直接运行;chunk是webpack内部根据模块之间的引用关系生成的文件。
output的path、publicPath的区别
- path。仅仅告诉webpack结果存储在哪里,必须是绝对路径。
- publicPath。不会对生成文件的路径造成影响。主要是用于生产模式,对页面引入的资源的路径进行补全。常见的就是css文件里面引入的图片、html文件里的url值,功能和file-loader的publicPath一致。
webpack externals
作用
防止将某个import的包打包到bundle中,而是在运行时再去从外部获取这些扩展依赖。
为什么需要它?(以JQuery为例)
使用Webpack打包发布这个库:
1 | // 入口文件 |
这样打包出来的bundle.js文件会把jquery的代码完整地注入进去,因为你的test中使用到了它。
但是这往往并不符合我们的预期,因为jquery是很通用的模块,在一个项目中,很可能其它的文件也会用到它,如果每一个文件模块的发布版本都将jquery原封不动地打包进了自己的bundle,最后拼到一起,在最终的发布代码里就会有很多份jquery的复制,当然这可能并不会影响它的正常功能,但是会占据很大的代码体积,显然不符合我们的预期。
所以通常情况下当你的库需要依赖到例如jquery,bootstrap这样的通用JS模块时,我们可以不将它打包进bundle,而是在Webpack的配置中声明external:
1 | externals: { |