webpack是什么?

  • webpack是一个用于现代JS应用程序的静态模块打包工具。

  • 静态模块是指开发阶段可以被webpack直接引用的资源(能够直接被获取打包进bundle.js的资源)。当webpack处理应用程序时,它会在内部构建一个依赖图,此依赖图对应映射到项目所需的每个模块(不再局限JS文件),并生成一个或多个bundle。

webpack解决的问题

  • 开发中会使用框架(React、Vue),ES6 模块化语法,Less/Sass 等 css 预处理器等语法。这样的代码要想在浏览器运行必须经过编译成浏览器能识别的 JS、css 等语法。webpack能进行转换。

  • 编译代码:提高效率,解决浏览器兼容问题

  • 模块整合:提高性能,可维护性,解决浏览器频繁请求文件的问题

  • 万物皆可模块:项目可维护性增强,支持不同种类的前端模块类型,统一的模块化方案,所有资源文件的加载都可以通过代码控制

webpack的构建流程

pic1

总体运行流程

​ webpack的运行流程是一个串行的过程,它的工作流程就是将各个插件串联起来。在运行过程中会广播事件,插件只需监听它所关心的事件,就能加入到webpack机制中,去改变webpack的运作,使得整个系统扩展性良好。

从启动到结束会依次执行以下三大步骤:

  • 初始化流程:从配置文件和shell语句中读取与合并参数,并初始化需要使用的插件和配置插件等执行环境所需要的参数。命令行接口参数的优先级高于配置文件。
  • 编译构建流程:从entry出发,针对每个module串行调用相应的loader去翻译文件内容,再找该module依赖的module,递归地进行编译处理
  • 输出流程:对编译后的module组合成chunk,把chunk转换成文件,输出到文件系统

初始化流程

​ 从配置文件和 Shell 语句中读取与合并参数,得出最终的参数。配置文件默认下为webpack.config.js,也或者通过命令的形式指定配置文件,主要作用是用于激活webpack的加载项和插件。webpackwebpack.config.js 中的各个配置项拷贝到 options 对象中,并加载用户配置的 plugins

​ 完成上述步骤之后,则开始初始化Compiler编译对象,该对象掌控着webpack生命周期,不执行具体的任务,只是进行一些调度工作。Compiler 对象继承自 Tapable,初始化时定义了很多钩子函数。

编译构建流程

​ 根据配置中的 entry 找出所有的入口文件。初始化完成后会调用Compilerrun来真正启动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.jsonmain 来将一个 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.scssA.less转变为B.css,单纯的文件转换过程。
  • plugin 赋予了 webpack 各种灵活的功能,例如打包优化、资源管理、环境变量注入等,目的是解决 loader 无法实现的其他事件。在Webpack 运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过Webpack提供的 API改变输出结果。

运行时机

  • loader 运行在打包文件之前
  • plugins 在整个编译周期都起作用
webpack

编写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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 导出一个函数,source为webpack传递给loader的文件源内容
module.exports = function(source){
const content = doSomeThing2JsString(source);
// 如果 loader 配置了 options 对象,那么this.query将指向 options
const options = this.query;
/*
* this.callback 参数:
* error:Error | null,当 loader 出错时向外抛出一个 error
* content:String | Buffer,经过 loader 编译后需要导出的内容
* sourceMap:为方便调试生成的编译后内容的 source map
* ast:本次编译生成的 AST 静态语法树,之后执行的 loader 可以直接使用这个 AST,进而省去重复生成 AST 的过程
*/
this.callback(null, content);//异步
return content;//同步
}

编写plugin

​ 由于webpack基于发布订阅模式,在运行的生命周期中会广播很多事件,plugin通过监听这些事件,就可以在特定的阶段执行自己的任务。

​ webpack中的两个核心对象:

  • compiler:包含webpack环境的所有配置信息,包括options,loader,plugin和webpack整个生命周期相关的钩子
  • compilation:作为plugin内置事件回调函数的参数,包含了当前的模块资源、编译生成资源、变化的文件以及被跟踪依赖的状态信息。当检测到一个文件变化,一次新的compilation将被创建。

实现plugin需要遵循的规范:

  • 插件必须是一个函数或者是一个包含 apply 方法的对象,这样才能访问compiler实例
  • 传给每个插件的 compilercompilation 对象都是同一个引用,因此不建议修改
  • 异步的事件需要在插件处理完任务时调用回调函数通知 Webpack 进入下一个流程,不然会卡住
1
2
3
4
5
6
7
8
9
10
class MyPlugin{
// Webpack 会调用 MyPlugin 实例的 apply 方法给插件实例传入 compiler 对象
apply(compiler){
// 找到合适的事件钩子,实现自己的插件功能
compiler.hooks.emit.tap('MyPlugin', compilation=>{
// compilation: 当前打包构建流程的上下文
console.log(compilation);
})
}
}

​ 在 emit 事件发生时,代表源文件的转换和组装已经完成,可以读取到最终将输出的资源、代码块、模块及其依赖,并且可以修改输出资源的内容

webpack热更新的原理

是什么?

HMR(Hot Module Replacement),模块热替换,指在程序运行过程中,替换、添加、删除模块,而无需重新刷新整个应用。

1
2
3
4
// webpack.config.js
devServer: {
hot: true
}

上述操作在修改CSS后,能达到不刷新的形式更新页面。但是修改JS后,页面依旧自动刷新了。这是因为还需要额外配置,需要去指定哪些模块发生更新时进行HMR。

1
2
3
4
5
if(module.hot){
module.hot.accept('./util.js', ()=>{
console.log('util.js更新了');
})
}

原理

原理
  • webpack与浏览器通过socket进行长连接
  • 当某一个文件或者模块发生变化时,webpack监听到文件变化对文件重新编译打包,编译生成唯一的hash值,这个hash值用来作为下一次热更新的标识。根据变化的内容生成两个补丁文件:manifest(包含了 hashchundId,用来说明变化的内容)和chunk.js 模块。
  • 当文件发生改动的时候,服务端会向浏览器推送一条消息,消息包含文件改动后生成的hash值,作为下一次热更细的标识
  • 在浏览器接受到这条消息之前,浏览器已经在上一次socket 消息中已经记住了此时的hash 标识,这时候会创建一个 ajax 去服务端请求获取到变化内容的 manifest 文件。mainfest文件包含重新build生成的hash值,以及变化的模块。
  • 浏览器根据 manifest 文件获取模块变化的内容,从而触发render流程,实现局部模块更新

webpack proxy

是什么?

接收客户端发送的请求后转发给其他服务器。其目的是为了便于开发者在开发模式下解决跨域问题(浏览器安全策略限制)

1
2
3
4
5
6
7
8
9
10
11
12
//webpack.config.js
devServer: {
contentBase: path.join(__dirname, 'dist'),
compress: true,
port: 9000,
proxy: {
'/api': {
target: 'https://api.github.com'
}
}
// ...
}

原理

​ 利用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
2
3
4
5
6
7
8
9
// 入口文件
entry: {
test: './test.js',
}
// 输出文件
output: {
path: './dist',
filename: 'bundle.js'
}

​ 这样打包出来的bundle.js文件会把jquery的代码完整地注入进去,因为你的test中使用到了它。

​ 但是这往往并不符合我们的预期,因为jquery是很通用的模块,在一个项目中,很可能其它的文件也会用到它,如果每一个文件模块的发布版本都将jquery原封不动地打包进了自己的bundle,最后拼到一起,在最终的发布代码里就会有很多份jquery的复制,当然这可能并不会影响它的正常功能,但是会占据很大的代码体积,显然不符合我们的预期。

​ 所以通常情况下当你的库需要依赖到例如jquery,bootstrap这样的通用JS模块时,我们可以不将它打包进bundle,而是在Webpack的配置中声明external:

1
2
3
4
5
6
7
8
externals: {
jquery: {
root: 'jquery',
commonjs: 'jquery',
commonjs2: 'jquery',
amd: 'jquery',
},
},