对Webpack的深度解析

一.对Webpack的理解以及解决了什么问题?

1.背景

Webpack的最初目标是实现前端项目的模块化,旨在高效地管理和维护项目中的每一个资源。

最早的时候,我们会通过文件划分的形式实现模块化,也就是将每个功能及其相关状态数据各自单独放到不同的JS文件中。

约定每个文件是一个独立的模块,然后再将这些JS文件引入到页面,一个script标签对应一个模块,然后调用模块化的成员。

javascript 复制代码
<script src="module-a.js"></script>
<script src="module-b.js"></script>

但这种模块化弊端十分明显,模块都是在全局中工作,大量模块成员污染了环境,模块与模块之间并没有依赖关系、维护困难、没有私有空等问题。项目一旦变大,上述问题会尤其明显。

随后就出现了命名空间方式,规定每个模块只暴漏一个全局对象,然后模块的内容都挂载到这个对象中

javascript 复制代码
window.moduleA = {
 method1: function () {
 console.log('moduleA#method1')
 }
}

这种方式也并没有解决第一种方式的依赖问题

再后来,我们使用立即执行函数为模块提供私有空间,通过参数的形式作为依赖说明,如下:

javascript 复制代码
// module-a.js
(function ($) {
 var name = 'module-a'
 function method1 () {
 console.log(name + '#method1')
 $('body').animate({ margin: '200px' })
 }
 window.moduleA = {
 method1: method1
 }
})(jQuery)

上述的方式都是早期解决模块的方式,但是任然存在一些没有解决的问题。例如,我们是通过script标签在页面引入这些模块的,这些模块的加载并不受代码的控制,时间一久维护起来也十分的麻烦。

理想的解决方式是,在页面中引入一个JS的入口文件,其余用到的模块可以通过代码控制,按需加载进来。除了模块加载的问题以外,还需要规定模块化的规范,如今流行的则是CommonJS、ES Modules。
2.问题

从后端渲染的JSP、PHP,到前端原生JavaScript,再到JQuery开发,再到目前的三大框架Vue、React、Angular开发方式,也从javascript到后面的es5、es6、7、8、9、10,再到typescript,包括编写CSS的预处理器Less、Sass等。

现代前端已变得十分的复杂,所以我们开发过程中会遇到如下问题:

  • 需要通过模块化的方式来开发。
  • 使用一些高级的特性来加快我们的开发效率或者安全性,比如通过ES6+、TypeScript开发脚本逻辑,通过sass、less等方式来编写css样式代码。
  • 监听文件的变化来并且反映到浏览器上,提高开发效率。
  • JavaScript代码需要模块化,HTML和CSS这些资源文件也会面临需要模块化的问题。
  • 开发完成后我们还需要将代码进行压缩、合并以及其他相关优化。

而Webpack恰巧可以解决以上问题
3.是什么

Webpack是一个用于现代JavaScript应用程序的静态模块打包工具

静态模块

这里的静态模块指的是开发阶段,可以被Webpack直接引用的资源(可以直接被获取打包进bundle.js的资源)

当Webpack处理应用程序时,它会在内部构建一个依赖图,此依赖图对应映射到项目所需的每个模块(不再局限js文件),并生成一个或多个bundle
4.Webpack的能力

  • 编译代码能力,提高效率,解决浏览器兼容问题。
  • 模块整合能力,提高性能,可维护性,解决浏览器频繁请求文件的问题。
  • 万物皆可模块能力,项目维护性增强,支持不同种类的前端模块类型,统一的模块化方案,所有资源文件的加载都可以通过代码控制。

二.Webpack的热更新是如何做到的,其原理是什么?

热模块更新(是什么,实现原理,总结)

是什么

HRM全称Hot Module Replacement,可以理解为模块热替换,指在应用程序运行过程中,替换、添加、删除模块,而无需重新刷新整个应用

例如,我们在应用运行过程中修改了某个模块,通过自动刷新会导致整个应用的整体刷新,那页面中的状态信息都会丢失。

如果使用的是HRM,就可以实现只将修改的模块实时替换至应用中,不必完全刷新整个应用

在Webpack中配置开启热模块也非常的简单,如下代码:

javascript 复制代码
const webpack = require('webpack')
module.exports = {
 // ...
 devServer: {
 // HMR 
 hot: true
 // hotOnly: true
 }
}

通过上述这种配置,如果我们修改并保存css文件,确实能够以不刷新的形式更新到页面中

但是,当我们修改并保存js文件之后,页面依旧自动刷新了,这里并没有触发热模块,所以,HRM并不像Webpack的其他属性一样可以开箱即用,需要有一些额外的操作,我们需要指定哪些模块发生更新时进行HRM,如下代码:

javascript 复制代码
if(module.hot){
 module.hot.accept('./util.js',()=>{
 console.log("util.js ")
 })
}

实现原理

首先看一张图,如下

  • Webpack Compile:将JS源代码编译成bundle.js
  • HMR Server:用来将热更新的文件输出给HMR Runtime
  • Bundle Server:静态资源文件服务器,提供文件访问路径
  • HMR Runtime:socket服务器,会被注入到浏览器,更新文件的变化
  • bundle.js:构建输出的文件
  • 在HMR Runtime和HMR Server之间建立websocket,即图上4号线,用于实时更新文件变化
    上图中,可以分成2个阶段:
    启动阶段为上图1-2-A-B
    在编写未经过webpack打包的源代码后,Webpack Compile将源代码和HRM Runtime一起编译成bundle文件,传输给Bundle Server静态资源服务器。
    更新阶段为上图1-2-3-4
    当某一个文件或者模块发生变化时,Webpack监听到文件变化对文件重新编译打包,编译生成唯一的hash值,这个hash值用来作为下一次热更新的标识
    根据变化的内容生成两个补丁文件:mainfest(包含了hash和chundId,用来说明变化的内容)和chunk.js模块
    由于socket服务器在HRM Runtime和HRM Server之间建立websocket链接,当文件发生改动的时候,服务端会向浏览器推送一条消息,消息包含文件改动后生成的hash值,如下图的h属性,作为下一次热更新的标识

在浏览器接收到这条消息之前,浏览器已经在上一次socket消息中已经记住了此时的hash标识,这时我们会创建一个ajax去服务端请求获取到变化内容的manifest文件

manifest文件包含重新build生成的hash值,以及变化的模块,对应上图的c属性浏览器根据manifest文件获取模块变化的内容,从而触发render流程,实现局部模块更新

总结

关于Webpack热模块更新的总结如下:

  • 通过webpack-dev-server创建两个服务器:提供静态资源服务(express)和Socket服务
  • express server负责直接提供静态资源的服务(打包后的资源直接被浏览器请求和解析)
  • socket server是一个websocket的长链接,双方可以通信
  • 当socket server监听到对应的模块发生变化时,会生成2个文件.json(manifest文件)和.js文件(undate chunk)
  • 通过长连接,socket server 可以直接将这两个文件主动发生给客户端(浏览器)
  • 浏览器拿到两个新的文件后,通过HRM Runtime机制,加载这两个文件,并且针对修改的模块进行更新。

三.Webpack的构建流程

webpack的构建流程(包括初始化流程、编译构建流程、输出流程)
1.运行流程

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

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

  • 初始化流程:从配置文件和Shell语句中读取与合并参数,并初始化需要使用的插件和配置插件等执行环境所需的参数。
  • 编译构建流程:从Entry发出,针对每个Moudle串行调用对应的Loader去翻译文件内容,再找到该Moudle依赖的Moudle,递归地进行编译处理。
  • 输出流程:对编译后的Moudle组合成Chunk,把Chunk转换成文件,输出到文件系统
    2.初始化流程
    从配置文件和Shell语句中读取与合并参数,得出最终的参数
    配置文件默认下为webpack.config.js,也或者通过命令的形式指定配置文件,主要作用是用于激活webpack的加载项和插件
    关于文件配置内容分析,如下注释:
javascript 复制代码
var path = require('path');
var node_modules = path.resolve(__dirname, 'node_modules');
var pathToReact = path.resolve(node_modules, 'react/dist/react.min.js');
module.exports = {
 // chunk
 entry: './path/to/my/entry/file.js'
 // ( )
 resolve: {
 alias: {
 'react': pathToReact
 }
 },
 // 
 output: {
 path: path.resolve(__dirname, 'build'),
 filename: '[name].js'
 },
 // loader css loader es6 loader
 loader
 module: {
 loaders: [
 {
 test: /\.js$/,
 loader: 'babel',
 query: {
 presets: ['es2015', 'react']
 }
 }
 ],
 noParse: [pathToReact]
 },
 // webpack webpack 
 plugins: [
 new webpack.HotModuleReplacementPlugin()
 ]
};

webpack将webpack.config.js中的各个配置拷贝到options对象中,并加载用户配置的plugins

完成上述步凑之后,则开始初始化Compiler编译对象,该对象掌握者webpack声明周期,不执行具体的任务,只是进行一些调度工作

javascript 复制代码
class Compiler extends Tapable {
 constructor(context) {
 super();
 this.hooks = {
 beforeCompile: new AsyncSeriesHook(["params"]),
 compile: new SyncHook(["params"]),
 afterCompile: new AsyncSeriesHook(["compilation"]),
 make: new AsyncParallelHook(["compilation"]),
 entryOption: new SyncBailHook(["context", "entry"])
 // 
 };
 // ...
 }
}
function webpack(options) {
 var compiler = new Compiler();
 ...// options, watch true, watch
 return compiler;
}
...

Compiler对象继承自Tapable,初始化时定义了很多钩子函数
3.编译构建流程

根据配置中的entry找出所有的入口文件

javascript 复制代码
module.exports = {
 entry: './src/file.js'
}

初始化完成后会调用Compiler的run来真正启动webpack编译构建流程,主要流程如下:

  • compile开始编译
  • make从入口点分析模块及其依赖的模块,创建这些模块对象
  • build-module构建模块
  • seal封装构建结果
  • emit把各个chunk输出到结果文件
    compile编译
    执行了run方法后,首先会触发compile,主要是构建一个Compilation对象
    该对象是编译阶段的主要执行者,主要会依次下述流程:执行模块创建、依赖收集、分块、打包等主要任务对象。
    make编译模块
    当完成了上述的Compilation对象后,就开始从Entry入口文件开始读取,主要执行_addModuleChain()函数,如下:
javascript 复制代码
_addModuleChain(context, dependency, onModule, callback) {
 ...
 // 
 const Dep = /** @type {DepConstructor} */ (dependency.constructor);
 const moduleFactory = this.dependencyFactories.get(Dep);
 
 // NormalModuleFactory create NormalModule
 moduleFactory.create({
 dependencies: [dependency]
 ...
 }, (err, module) => {
 ...
 const afterBuild = () => {
 this.processModuleDependencies(module, err => {
 if (err) return callback(err);
 callback(null, module);
 });
 };
 
 this.buildModule(module, false, null, null, err => {
 ...
 afterBuild();
 })
 })
}

过程如下:

_addModuleChain中接收参数dependency传入的入口依赖,使用对应的工厂函数NormalModuleFactory.create方法生成一个空的module对象,回调中会把此module存入Compilation.modules对象和dependencies.module对象中,由于是入口文件,也会存入Compilation.entries中,随后执行buildModule进入真正的构建模块module内容的过程

build module完成模块编译

这里主要调用配置的loaders,将我们的模块转成标准的JS模块,在用Loader对一个模块转化完后,使用acorn解析转化后的内容,输出对应的抽象语法树(AST),以方便webpack后面对代码的解析。从配置的入口模块开始,分析其AST,当遇到require等导入其他模块语句时,便将其加入到依赖的模块列表,同时对新出的依赖模块递归分析,最终搞清所有模块的依赖关系
4.输出流程

seal输出资源

seal方法主要是要生成chunks,对chunks进行一系列的优化操作,并生成要输出的代码webpack中的chunk,可以理解为配置在entry中的模块,或者是动态引入的模块,根据入口和模块之间的依赖关系,组装成一个个包含多个模块的Chunk,再把每个Chunk转换成一个单独的文件加入到输出列表

emit输出完成

在确定好输出内容后,根据配置确定输出的路径和文件名

javascript 复制代码
output: {
 path: path.resolve(__dirname, 'build'),
 filename: '[name].js'
}

在Compiler开始生成文件前,钩子emit会被执行,这是我们修改最终文件的最后一个机会从而webpack整个打包过程则结束了。

小结

四.Webpack proxy工作原理,为什么能解决跨域?

Webpack proxy(是什么?实现原理,跨域)

Webpack proxy,即Webpack提供的代理服务。

基本行为就是接收客户端发送的请求后转发给其他服务器

其目的是为了便于开发者在开发模式下解决跨域问题(浏览器安全策略限制)

想要实现代理首先需要一个中间服务器,webpack中提供服务器的工具为webpack-dev-server

webpack-dev-server是webpack官方推出的一款开发工具,将自动编译和自动刷新浏览器等一系列对开发友好的功能全部集成在了一起。目的是为了提高开发者日常开发的效率,只适用于开发阶段

关于配置方面,在webpack配置对象属性中通过devServer属性提供,如下:

javascript 复制代码
// ./webpack.config.js
const path = require('path')
module.exports = {
 // ...
 devServer: {
 contentBase: path.join(__dirname, 'dist'),
 compress: true,
 port: 9000,
 proxy: {
 '/api': {
 target: 'https://api.github.com'
 }
 }
 // ...
 }
}

devServer里面的proxy则是关于代理的配置,该属性为对象的形式,对象中每一个属性就是一个代理的规则匹配。

属性的名称是需要被代理的请求路径前缀,一般为了辨别都会设置前缀为/api,值为对应的代理匹配规则,对应如下:

  • target:表示的是代理到的目标地址。
  • pathRewrite:默认情况下,我们的/api-hy也会被写入到URL中,如果希望删除,可以适用pathRewrite。
  • secure:默认情况下不接收转发到https的服务器上,如果希望支持,可以设置为false。
  • changeOrigin:它表示是否更新代理后请求的headers中host地址。
    工作原理
    proxy工作原理实质上是利用http-proxy-middleware这个http代理中间件,实现请求转发给其他服务器。
    例如在开发阶段,本地地址为http://localhost:3000,该浏览器发送一个前缀带有/api标识的请求到服务端获取数据,但响应这个请求的服务器只是将请求转发到另一台服务器中
javascript 复制代码
const express = require('express');
const proxy = require('http-proxy-middleware');
const app = express();
app.use('/api', proxy({target: 'http://www.example.org', changeOrigin: true
}));
app.listen(3000);
// http://localhost:3000/api/foo/bar -> http://www.example.org/api/foo/bar

跨域

在开发阶段,webpack-dev-server会启动一个本地开发服务器,所以我们的应用在开发阶段是独立运行在localhost的一个端口上,而后端服务又是运行在另外一个地址上,所以在开发阶段中,由于浏览器同源策略的原因,当本地访问后端就会出现跨域请求的问题,通过设置webpack proxy实现代理请求后,相当于浏览器与服务端中添加一个代理者,当本地发送请求的时候,代理服务器响应请求,并将请求转发到目标服务器,目标服务器响应数据后再将数据返回给代理服务器,最终再由代理服务器将数据响应给本地。

在代理服务器传递数据给本地浏览器的过程中,两者同源,并不存在跨域行为,这时候浏览器就能正常接收数据。
注意:服务器与服务器之间请求数据并不会存在跨域行为,跨域行为是浏览器安全策略限制。

五.Webpack中常见的Loader以及解决了什么问题?

Webpack中常见的Loader(是什么、特性、常见的)

loader用于对模块的"源代码"进行转换,在import或"加载"模块时预处理文件

webpack做的事情,仅仅是分析各种模块的依赖关系,然后形成资源列表,最终打包生成到指定的文件中。如下图:

在webpack内部中,任何文件都是模块,不仅仅只是JS文件。

默认情况下,在遇到import或者require加载模块的时候,webpack只支持对js和json文件打包。像css、sass、png等这些类型的文件的时候,webpack则无能为力,这时候就需要配置对应的loader进行文件内容解析。在加载模块的时候,执行顺序如下:

当webpack碰到不识别的模块的时候,webpack会在配置中查找该文件解析规则,关于配置loader的方式有三种:

  • 配置方式(推荐):在webpack.config.js文件中指定loader
  • 内联方式:在每个import语句中显示指定loader
  • CLI方式:在shell命令中指定它们
    配置方式
    关于loader的配置,我们是写在module.rules属性中,属性介绍如下:
  • rules是一个数组的形式,因此我们可以配置很多个loader
  • 每一个loader对应一个对象的形式,对象属性test为匹配的规则,一般情况为正则表达式
  • 属性use针对匹配到文件类型,调用对应的loader处理
    代码编写如下:
javascript 复制代码
module.exports = {
 module: {
 rules: [
 {
 test: /\.css$/,
 use: [
 { loader: 'style-loader' },
 {
 loader: 'css-loader',
 options: {
 modules: true
 }
 },
 { loader: 'sass-loader' }
 ]
 }
 ]
 }
};

特性

loader特性如下

  • loader可以是同步的,也可以是异步的
  • loader运行在Node.js中,并且能够执行任何操作
  • 除了常见的通过package.json的main来将一个npm模块导出loader,还可以在module.rules中适用loader字段直接引用一个模块
  • 插件(plugin)可以为loader带来更多特性
  • loader可以产生额外的任意文件
    可以通过loader的预处理函数,为javascript生态系统提供更多能力。用户现在可以更加灵活地引入细粒度逻辑。例如:压缩、打包、语言翻译和更多其他特性。

常见的loader

在页面开发过程中,我们经常性加载除了js文件以外的内容,这时候我们需要配置响应的loader进行加载。

常见的loader如下:

  • style-loader:将css添加到DOM的内联样式标签style里
  • css-loader:允许将css文件通过requre的方式引入,并返回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:用babel来转换ES6文件
    css-loader
    分析css模块之间的关系,并合成一个css
javascript 复制代码
npm install --save-dev css-loader
javascript 复制代码
rules: [
 ...,
{
 test: /\.css$/,
 use: {
 loader: "css-loader",
 options: {
 // / url() 
 url: true,
 // / @import 
 import: true,
 // / Sourcemap
 sourceMap: false
 }
 }
}
]

style-loader

把css-loader生成的内容,用style标签挂载到页面的head中

javascript 复制代码
npm install --save-dev style-loader
javascript 复制代码
rules: [
 ...,
{
 test: /\.css$/,
 use: ["style-loader", "css-loader"]
}
]

less-loader

开发中,我们也常常会使用less、sass、stylus预处理器编写css样式,使开发效率提高,这里需要使用less-loader。

javascript 复制代码
npm install less-loader -D
javascript 复制代码
rules: [
 ...,
{
 test: /\.css$/,
 use: ["style-loader", "css-loader","less-loader"]
}
]

raw-loader

在webpack中通过import方式导入文件内容,该loader并不是内置的,所以首先要安装。

javascript 复制代码
npm install --save-dev raw-loader

然后在webpack.config.js中进行配置

javascript 复制代码
module.exports = {
 ...,
 module: {
 rules: [
 {
 test: /\.(txt|md)$/,
 use: 'raw-loader'
 }
 ]
}
}

file-loader

把识别出的资源模块,移动到指定的输出目录,并且返回这个资源在输出目录的地址(字符串)

javascript 复制代码
npm install --save-dev file-loader
javascript 复制代码
rules: [
 ...,
{
 test: /\.(png|jpe?g|gif)$/,
 use: {
 loader: "file-loader",
 options: {
 // placeholder [name] 
 // [ext] 
 name: "[name]_[hash].[ext]",
 //
 outputPath: "./images",
 // url
 publicPath: './images',
 }
 }
}
]

url-loader

可以处理file-loader所有的事情,但遇到图片格式的模块,可以选择性的把图片转成base64格式的字符串,并打包到JS中,对小体积的图片比较合适,大图片不合适

javascript 复制代码
npm install --save-dev url-loader
javascript 复制代码
rules: [
 ...,
{
 test: /\.(png|jpe?g|gif)$/,
 use: {
 loader: "url-loader",
 options: {
 // placeholder [name] 
 // [ext] 
 name: "[name]_[hash].[ext]",
 //
 outputPath: "./images"
 // url
 publicPath: './images',
 // 100 base64 
 limit: 100
 }
 }
}
]
相关推荐
拜无忧8 小时前
【案例】可视化模板,驾驶舱模板,3x3,兼容移动
前端·echarts·数据可视化
濮水大叔8 小时前
AOP编程有三大场景:控制器切面,内部切面,外部切面,你get到了吗?
typescript·node.js·nestjs
向葭奔赴♡8 小时前
前端框架学习指南:提升开发效率
前端·javascript·vue.js
小高0078 小时前
🔥🔥🔥Vue 3.5 核弹级小补丁:useTemplateRef 让 ref 一夜失业?
前端·javascript·vue.js
小许哥8 小时前
如何把微信小程序转换成支付宝小程序
前端
汤姆Tom8 小时前
CSS 最佳实践与规范
前端·css
golang学习记8 小时前
VS Code 2025 最顺眼又顺手的插件,效率Max,太上头了!
前端
三十_8 小时前
团队踩坑实录:用 patch-package 临时修复第三方依赖的最佳实践
前端·npm
一个不爱写代码的瘦子8 小时前
Map、weakMap和Set、weakSet
前端·javascript