前言

将近一年前自己尝试阅读vite源码(2.x),虽然也有些收获但整体并没有到达我的预期,对于vite也是停留在一知半解的程度上。最近想重新开始学习vite,但回顾之前的学习历程,感觉不太想继续之前的方式,自己的水平有限,读起来太费劲,经常在不同的函数调用间迷失自己,最后草草收场。想起之前看文章很多人是看代码的最初实现版本的,于是也想尝试一下,选择阅读vite的最初版本分支1.x,效果是明显比之前好的,后续我觉得再阅读最新版本的代码的话是有很大帮助的。

阅读过程中发现关于热更新(HMR)这块逻辑略微复杂,想着记录下来,避免之后忘记。

HMR

之前对于HMR的了解大概是:webpack/vite会在启动后开启websocket服务用于浏览器端和服务端之间的通信,每当我们修改代码后服务端就会发送消息给浏览器端,浏览器端进行更新,对于具体过程是不太了解的

阅读分支

vite-v1

下文中的vite如无特殊说明均指的v1版本

前置了解

vite开发模式下会启动一个server以供开发者访问调试,具体实现中是启动了一个Koa服务,vite对被访问文件的处理都是以插件的形式进行的,HMR相关的主要有以下几个文件

vite
├─ src
│  ├─ client
│  │  ├─ client.ts
│  ├─ hmrPayload.ts
│  └─ node
│     ├─ server
│     │  ├─ index.ts
│     │  ├─ serverPluginClient.ts
│     │  ├─ serverPluginCss.ts
│     │  ├─ serverPluginHmr.ts
│     │  ├─ serverPluginHtml.ts
│     │  ├─ serverPluginModuleRewrite.ts
│     │  ├─ serverPluginVue.ts

整体流程

首先按照vite官网命令起一个demo,npm run dev之后打开开发者工具,可以看到请求的大概过程是:

浏览器端

入口文件index.html的处理

第一个请求是访问index.html的,与源文件不同的是这里多了一行代码,浏览器就会请求client.js

<script type="module" src="/@vite/client"></script>

这个处理是有htmlRewritePlugin插件完成的,代码如下(不过vite-v1中不是以src的方式引入的,而是import /vite/client)

export const clientPublicPath = `/vite/client`
const devInjectionCode = `\n<script type="module">import "${clientPublicPath}"
</script>\n`
...
injectScriptToHtml(html, devInjectionCode)
...

client.js

对于/vite/client的访问 是由clientPlugin插件处理的,主要是读取client/client.js文件并进行一些初始化后返回,具体实现如下:

// src/node/server/serverPluginClient.ts
export const clientFilePath = path.resolve(__dirname, '../../client/client.js')
export const clientPublicPath = `/vite/client`
export const clientPlugin: ServerPlugin = ({ app, config }) => {const clientCode = fs.readFileSync(clientFilePath, 'utf-8').replace(`__MODE__`, JSON.stringify(config.mode || 'development'))app.use(async (ctx, next) => {if (ctx.path === clientPublicPath) {// ...ctx.type = 'js'ctx.status = 200ctx.body = clientCode}})
}

client.js中主要做了以下三件事

  1. 启动websocket建立与服务端之前的连接
  2. 接受websocket信息并进行相应处理(处理细节在后面)
  3. 暴露出一个HMR Context,以供其他模块(文件)调用
const socket = new WebSocket(`${socketProtocol}://${socketHost}`, 'vite-hmr')socket.addEventListener('message', async ({ data }) => {const payload = JSON.parse(data) as HMRPayload | MultiUpdatePayloadif (payload.type === 'multi') {payload.updates.forEach(handleMessage)} else {handleMessage(payload)}
})export const createHotContext = (id: string) => {...const hot = {accept(callback: HotCallback['fn'] = () => {}) {hot.acceptDeps(id, callback)},acceptDeps(deps: HotCallback['deps'],callback: HotCallback['fn'] = () => {}) {...},...}return hot
}

其他模块(文件)HMR能力的注入

随便打开几个文件可以发现在某些文件中是由HMR相关代码注入的,比如App.vue

可以看到除了业务代码外在最开始引入了client.js并创建了一个App.vue的HMR模块,在结束的地方调用了一些HMR的方法,有了这些就可以完成App.vue的热更新了

服务端

服务端的处理主要是hmrPlugin moduleRewritePlugin插件和一些专门处理某类文件HMR的cssPlugin vuePlugin插件完成的。

hmrPlugin主要做了以下几件事

  1. 启动服务端的websocket
  2. 每当文件有变化的时候会向浏览器端发送信息
import chokidar from 'chokidar'  // `chokidar`是用来监听文件变化的
const watcher = chokidar.watch(root, {ignored: ['**/node_modules/**', '**/.git/**'],ignoreInitial: true,...chokidarWatchOptions
}) as HMRWatcher
const wss = new WebSocket.Server({ noServer: true })
watcher.on('change', (file) => {if (!(file.endsWith('.vue') || isCSSRequest(file))) {//  vue文件和plain css文件在serverPluginVue 和 serverPluginCss文件中处理handleJSReload(file)}
})// 这里把send方法直接放到watcher实例上了,便于有文件变化的话可以直接send消息
const send = (watcher.send = (payload: HMRPayload) => {wss.clients.forEach((client) => {if (client.readyState === WebSocket.OPEN) {client.send(stringified)}})
})

cssPluginvuePlugin是分别用来处理.css文件和.vue文件的,里面包含了HMR相关的部分,比如,App.vue最下方的HMR逻辑的注入就是从vuePlugin写入的(这里其实我没找到import.meta.hot.accept相关的逻辑,只有hmrId注入,但在最新版plugin-vue插件中找到了相关逻辑,这里我就认为是在vue插件中注入的了)

// src/node/server/serverPluginVue.ts
...code += `\n__script.__hmrId = ${JSON.stringify(publicPath)}`code += `\ntypeof __VUE_HMR_RUNTIME__ !== 'undefined' && __VUE_HMR_RUNTIME__.createRecord(__script.__hmrId, __script)`code += `\n__script.__file = ${JSON.stringify(filePath)}`code += `\nexport default __script`
...
// https://github.com/vitejs/vite/blob/7a6d4bc0d7fa614d3ac469ca35352a23aaef8232/packages/plugin-vue/src/main.ts#L115
// HMR
if (devServer &&devServer.config.server.hmr !== false &&!ssr &&!isProduction
) {...output.push(`import.meta.hot.accept(mod => {`,`  if (!mod) return`,`  const { default: updated, _rerender_only } = mod`,`  if (_rerender_only) {`,`    __VUE_HMR_RUNTIME__.rerender(updated.__hmrId, updated.render)`,`  } else {`,`    __VUE_HMR_RUNTIME__.reload(updated.__hmrId, updated)`,`  }`,`})`)
}

moduleRewritePlugin主要并不是来处理HMR的,只是在对请求的模块(文件)进行重写处理的过程中进行了文件依赖关系的分析和HMR逻辑的相关重写,支撑了HMR功能。App.vue模块上方HMR Context的注入就是在此插件中完成的。

const hasHMR = source.includes('import.meta.hot')
if (hasHMR) {rewriteFileWithHMR(root, source, importer, resolver, s)
}function rewriteFileWithHMR() {...s.prepend(`import { createHotContext } from "${clientPublicPath}"; ` +`import.meta.hot = createHotContext(${JSON.stringify(importer)}); `)...
}

具体流程

通过以上插件的执行,浏览器端和服务端通信的websocket就有了,各个文件要进行HMR的预先处理也完成了,只要文件发生变化,服务端就通知客户端进行热更新,那么文件变化=>进行热更新的具体流程是啥呢?

前置了解-HMR设计的思路

hmrPlugin中对于HMR的设计思路进行了注释(如下图)

大意是指对文件进行HMR graph analysis, 只要是走到dead end,就发送full page reload,否则找到相应的hmr boundary(指的是本次hmr受到影响的文件),把要进行hmr的所有模块hmr Boundaries都发送给浏览器端。对应的代码实现就是上面提到的hmrPlugin里的handleJSReload(file)法,这里留个大概印象就行,先不用关心具体细节比如是dead end,什么是hmr boundary,下面都有涉及

初次访问某个文件

moduleRewritePlugin是最后执行的koa插件,接收到的文件已经全部被处理为了js文件,moduleRewritePlugin的主要作用就是

  1. 路径的重写,比如把对某些第三方包的请求路径改为预购建后的路径
  2. 记录模块之前的依赖关系
  3. 加入HMR逻辑并track HMR boundary accept whitelists(这一段不知道该怎么翻译)

这里的2、3都是使用es-module-lexer将文件转化为ast然后再进行解析得到的。
其中模块之间的依赖关系被放在了以下两个变量中

// moduleRewritePlugin / function rewriteImports
export const importerMap: HMRStateMap = new Map()
export const importeeMap: HMRStateMap = new Map()
// 例如,a模块有有一句`import {x} from 'b'`
// 那么importeeMap里就会加一条  {key: a, value:['b']}
// importerMap里就会加一条{ key: b, value: ['a']},两者的key 、value是相反的关系
// 每个文件都这么记录下来就能获取到所有文件的依赖关系

加入HMR逻辑是指上面提到的检测到vuePlugin注入了一段import.meta.hot.accept后在文件头部注入的HMR Context,track HMR boundary accept whitelists(这一段不知道该怎么翻译)是指下面这两个变量

// fucntion rewriteFileWithHMR
export const hmrAcceptanceMap: HMRStateMap = new Map()
export const hmrDeclineSet = new Set<string>() // 当发现文件中有调用Hmr方法,比如 import.hot.meta.accept 或者其他方法时,就会开始记录
// 比如 a 模块中有  import.meta.hot.accept(['./b','./c'],callback)
// hmrAcceptanceMap里就会加一个  {key: a, value: ['./b', './c']}
// 这里简单说下accept是单个模块,如`import.meta.hot.accept('./foo', () => {})`
// `import.meta.hot.accept() OR import.meta.hot.accept(() => {})`会把当前模块加进去
// accepts是接受多个模块
// decline的话会加到hmrDeclineSet里

有了以上这些变量后就能够支持HMR graph analysis

监听文件变化

// src/node/server/serverPluginHmr.tswatcher.on('change', (file) => {if (!(file.endsWith('.vue') || isCSSRequest(file))) {// everything except plain .css are considered HMR dependencies.// plain css has its own HMR logic in ./serverPluginCss.ts.handleJSReload(file)}})

可以看到当监听到文件变化后,会有两大类的处理

  1. 一般性的处理 ,执行handleJSReload
  2. 特殊处理,对于.vue .css文件需要在其对应的插件中处理

这里我们先看对一般性文件handleJSReload的处理

HMR graph analysis

这里的实现也就对应着上面提到的HMR设计思路

fn handleJSReload里表明 HMR graph analysis分析的结果就两种,要么是dead end,发送

send({ type: 'full-reload', path: publicPath })

要么就是把找到的多个hmr boundary发送出去

fn walkImportChain是来判断到底是dead end还是存在hmr boundary的,

  1. 当前文件调用了import.meta.hot.decline(),那么一定是dead end
  2. 当前文件自己就存在自己的hmrAcceptanceMap里, 那么自己就是hmr boundary
  3. 如果(被当前文件使用的文件)是.vue文件或者(被当前文件使用的文件)的hmrAcceptanceMap里包括当前文件或者自己,那么被(当前文件使用的文件)就是hmr boundary
  4. 如果非以上情况,那么就递归的判断 被(被当前文件使用的文件)使用的文件,一直往上,直到结束
//  在importer 的hmrAcceptanceMap里
function isHmrAccepted(importer: string, dep: string): boolean {const deps = hmrAcceptanceMap.get(importer)return deps ? deps.has(dep) : false
}
const handleJSReload = (watcher.handleJSReload = (filePath: string,timestamp: number = Date.now()
) => {const publicPath = resolver.fileToRequest(filePath)const importers = importerMap.get(publicPath) //  获取被publicPath使用的模块if (importers || isHmrAccepted(publicPath, publicPath)) {const hmrBoundaries = new Set<string>()const dirtyFiles = new Set<string>() //  记录被影响了的文件dirtyFiles.add(publicPath)const hasDeadEnd = walkImportChain(publicPath,importers || new Set(),hmrBoundaries,dirtyFiles)// record dirty files - this is used when HMR requests coming in with// timestamp to determine what files need to be force re-fetchedhmrDirtyFilesMap.set(String(timestamp), dirtyFiles)const relativeFile = '/' + slash(path.relative(root, filePath))if (hasDeadEnd) {send({type: 'full-reload',path: publicPath})console.log(chalk.green(`[vite] `) + `page reloaded.`)} else {const boundaries = [...hmrBoundaries]send({type: 'multi',updates: boundaries.map((boundary) => {return {type: boundary.endsWith('vue') ? 'vue-reload' : 'js-update',path: boundary,changeSrcPath: publicPath,timestamp}})})}} else {debugHmr(`no importers for ${publicPath}.`)}
})
function walkImportChain(importee: string,importers: Set<string>,hmrBoundaries: Set<string>,dirtyFiles: Set<string>,currentChain: string[] = []
): boolean {if (hmrDeclineSet.has(importee)) {// 文件调用了import.meta.hot.declinereturn true}if (isHmrAccepted(importee, importee)) {//  自己就自己的hmrAcceptanceMap里的话,直接返回了hmrBoundaries.add(importee)dirtyFiles.add(importee)return false}for (const importer of importers) {if (importer.endsWith('.vue') ||// explicitly accepted by this importerisHmrAccepted(importer, importee) ||// importer is a self accepting moduleisHmrAccepted(importer, importer)) {// vue boundaries are considered dirty for the reloadif (importer.endsWith('.vue')) {dirtyFiles.add(importer)}hmrBoundaries.add(importer)currentChain.forEach((file) => dirtyFiles.add(file))} else {const parentImpoters = importerMap.get(importer) //  获取被importer(被当前importee使用的模块)使用的模块if (!parentImpoters) {//  dead endreturn true} else if (!currentChain.includes(importer)) {if (walkImportChain(importer,parentImpoters,hmrBoundaries,dirtyFiles,currentChain.concat(importer))) {return true}}}}return false
}

消息类型

send的消息类型定义在 src/hmrPayload.ts里,针对每种type,浏览器端都会有不同的相应,在src/client/client.ts

export type HMRPayload =| ConnectedPayload| UpdatePayload| FullReloadPayload| StyleRemovePayload| SWBustCachePayload| CustomPayload| MultiUpdatePayloadinterface ConnectedPayload {type: 'connected'
}export interface UpdatePayload {type: 'js-update' | 'vue-reload' | 'vue-rerender' | 'style-update'path: stringchangeSrcPath: stringtimestamp: number
}interface StyleRemovePayload {type: 'style-remove'path: stringid: string
}interface FullReloadPayload {type: 'full-reload'path: string
}interface SWBustCachePayload {type: 'sw-bust-cache'path: string
}interface CustomPayload {type: 'custom'id: stringcustomData: any
}export interface MultiUpdatePayload {type: 'multi'updates: UpdatePayload[]
}

浏览器端响应

clients.js除了前面描述的一些功能外,还定义了一些变量和方法用于处理HMR相关的逻辑

const hotModulesMap = new Map<string, HotModule>() // 记录HMR模块相关的信息

初次访问

比如App.vue中增加了如下的HMR逻辑

import {createHotContext as __vite__createHotContext} from "/@vite/client";
import.meta.hot = __vite__createHotContext("/src/App.vue"); // 注册一个HMR模块
_sfc_main.__hmrId = "7a7a37b1";
import.meta.hot.accept((mod)=>{if (!mod)return;const {default: updated, _rerender_only} = mod;if (_rerender_only) {__VUE_HMR_RUNTIME__.rerender(updated.__hmrId, updated.render);} else {__VUE_HMR_RUNTIME__.reload(updated.__hmrId, updated);}
}
);

import.meta.hot.accept执行后,hotModulesMap里会增加一条记录

key: '/src/App.vue'
values:  {id: '/src/App.vue',callbacks: [{deps: '/src/App.vue',fn: callback},{deps: 'xxxxxx/xxxx.vue',fn: callback}]
}

响应HMR

服务端发送的消息类型有很多,每种类型都有对应的方法,比如full-reload会触发页面刷新等等,这里我们主要看下js-update的时候

switch (payload.type) {...case 'vue-rerender':const templatePath = `${path}?type=template`import(`${templatePath}&t=${timestamp}`).then((m) => {__VUE_HMR_RUNTIME__.rerender(path, m.render)console.log(`[vite] ${path} template updated.`)})breakcase 'style-remove':removeStyle(payload.id)breakcase 'js-update':queueUpdate(updateModule(path, changeSrcPath, timestamp))breakcase 'full-reload':if (path.endsWith('.html')) {const pagePath = location.pathnameif (pagePath === path ||(pagePath.endsWith('/') && pagePath + 'index.html' === path)) {location.reload()}return} else {location.reload()}
}

服务端把hmr有变化的文件目录都发送了过来,fn updateModule里就是把hotModulesMap里这些文件里注册的所有callback(deps,callback )都拿出来,并重新请求各个depsimport deps, fn queueUpdate就是在这么deps重新加载后执行之前对应的callback,到这里一次HMR就完成了

总结

HMR消息类型有多种,以下是多个hmr boundary 类型为js-update时的一次更新流程图

End

  1. 本次源码阅读的结论主要是通过阅读源码和百度一些资料得到的,没有经过断点一步一步调试,所以可能会存在理解有偏差的地方,有任何问题都欢迎大家一起讨论
  2. 阅读过程中建了一个分支,有随手加的一些注释,有需要可以看下
  3. 自己阅读源码的记录会统一放在这里,包括single-spa rollup qiankun ...
  4. 感谢阅读!

vite1.x 热更新(HMR)的实现原理相关推荐

  1. Webpack 热更新HMR 原理全解析

    这是 Webpack 原理分析系列第十篇文章,前文可到公众号[Tecvan]查阅. 一.什么是 HMR HMR 全称 Hot Module Replacement,中文语境通常翻译为模块热更新,它能够 ...

  2. webpack的热重载/热更新HMR是如何实现的

    概念 关键词:热更新/热重载/HMR(Hot Module Replacement) 效果:浏览器的无刷新更新,允许在运行时替换,添加,删除各种模块,而无需进行完全刷新重新加载整个页面 目的:加快开发 ...

  3. IOS热更新-JSPatch实现原理+Patch现场恢复

    关于HotfixPatch 在IOS开发领域,由于Apple严格的审核标准和低效率,IOS应用的发版速度极慢,稍微大型的app发版基本上都在一个月以上,所以代码热更新(HotfixPatch)对于IO ...

  4. 修改html时webpack热更新,webpack热更新(HMR)

    8种机械键盘轴体对比 本人程序员,要买一个写代码的键盘,请问红轴和茶轴怎么选? 一.HMR介绍 在我们开发react应用的时候,在配置了webpack-dev-server的前提下每一次的组件内容修改 ...

  5. Unity 热更新技术 | (一) 热更新的基本概念原理及主流热更新方案介绍

  6. webpack 热更新原理解析

    一.什么是 HMR HMR 全称 Hot Module Replacement,中文语境通常翻译为模块热更新,它能够在保持页面状态的情况下动态替换资源模块,提供丝滑顺畅的 Web 页面开发体验. 1. ...

  7. webpack热更新

    什么是模热更新?有什么优点 模块热更新是webpack的一个功能,它可以使得代码修改之后,不用刷新浏览器就可以更新. 在应用过程中替换添加删出模块,无需重新加载整个页面,是高级版的自动刷新浏览器. 优 ...

  8. webpack4.x热更新,自动刷新

    模块热替换(Hot Module Replacement) 模块热替换功能会在应用程序运行过程中替换.添加或删除模块,无需重新加载整个页面.主要是通过以下几种方式,来显著加快开发速度: 保留在完全重新 ...

  9. python flask热更新_客户端python热更新

    介绍: 热更新,就是在维持服务不间断的情况下,对软件代码逻辑或配置数据进行更新修复.随着游戏项目引入了脚本语言以后,热更新技术逐渐成为了标配,在我经历过的游戏项目中,无论是服务端还是客户端,版本的更新 ...

最新文章

  1. php 编译mcrypt,centos 6下编译安装php时安装mcrypt支持库
  2. Python将彩色图转换为灰度图
  3. MaxCompute的任务状态和多任务执行
  4. 《剑指offer》答案整理
  5. python 3.8.0安卓_Python 3.8.0稳定版正式发布
  6. Exchange 企业邮件与Windows安全应用 — Exchange 2007 收件人管理
  7. python之父名言_Python之父:为什么操作符很有用?
  8. android 滑动tabhost,tabhost左右滑动按钮
  9. 计算机发展的英语介绍ppt模板,计算机发展跟应用-锐得ppt模板资料.ppt
  10. 金蝶kis商贸采购单商品代码_金蝶KIS云商贸版(采购模块)常见问题汇总
  11. 教你一个小技巧给latex表格添加脚注 (非footnote)
  12. 贝叶斯公式的理解【转】
  13. 我非英雄,广目无双,我本坏蛋,无限嚣张
  14. C#.NET发EMAIL的几种方法 MailMessage/SmtpClient/CDO.Message
  15. Matlab符号运算(符号的创建和简单运算、函数求导、不定积分和定积分、解方程组)代码和解释
  16. 【SCSS】1300- 这些 SCSS 使用技巧真好用~
  17. 【转】下一代密码模块安全标准探讨
  18. 计算机软考网络工程师 查询,计算机软考网络工程师考试成绩查询指南
  19. nodejs+express搭建小程序后台服务器
  20. 哈里波特与魔法石pdf_哈里·罗伯茨(CSS)CSS框架的命运与失败

热门文章

  1. 一个更加牛逼的科技媒体将如何崛起
  2. 放电式消除BIOS密码(有图)
  3. (待补充)单细胞测序的基础知识
  4. Pdf转doc的感受
  5. 金蝶云星空报错信息及解决办法
  6. ZYNQ PL与PS交互的最大带宽
  7. 一个招聘网站的详细部件设计
  8. PageRank算法及Python实现
  9. 美国登月时寻找嫦娥录音走红:注意带大兔子姑娘
  10. 大小端设备对程序的影响