当前位置:   article > 正文

Vite原理学习之HMR_vite hmr

vite hmr

前言

Vite是基于ESM的构建工具,预编译和按需编译机制是其在项目启动以及快速更新的核心。除了这两个机制外,还存在一个主流构建工具都存在一个机制即HMR。HMR是Hot Module Replacement的简写,意思是模块热替换,即允许在运行时更新各种模块,而无需进行完全刷新。HMR大大提高了开发阶段的更新的响应速度,避免全量更新,在提高效率的同时大大提高了开发体验。

Live Reload

在正式去学习HMR机制之前,先聊聊另外一个概念Live Reload。Live Reload表示实时重加载,实际上这个概念跟HMR很相近。HMR是在运行时更新存在改变的模块,Live Reload也是在运行时更新模块,但是它全量更新的。Live Reload和HMR都是前端工程化时代下的产物,不同于原始阶段手动刷新更改后的页面这种方式,Live Reload和HMR都实现页面的自动更新。

事物的发展总是循序渐进的,技术也是如此,从原始的手动刷新 、Live Reload机制的全量自动更新、HMR机制的局部自动更新,整个发展主要脉络是清晰的。

Vite中HMR机制

不论是否了解Vite背后原理,你应该知道其背后是基于WebSocket的,实际上目前主流构建工具的HMR的实现都是基于WebSocket的。WebSocket协议是服务器端与客户端通信的协议之一,不同于HTTP协议的半双工,WebSocket协议是全双工的,它允许客户端和服务器端可以同时通信,这服务器端可以主动向客户端发送请求。现代浏览器基于WebSocket协议实现了相关功能,提供了WebSocket对象。在Node端也有相关的WebSocket库, 例如ws等。

构建工具的核心都是在本地基于Node的http模块创建一个本地开发服务器,提供开发阶段所有资源的相关处理等工作。HMR机制是基于WebSocket的,必然也是需要客户端和服务端的。通过源码梳理,Vite中HMR机制对应的WebSocket的服务端是在预编译期间,准确的说实在创建开发服务器时创建的,而WebSocket客户端则是在按需编译期间,准确的说是在开发服务器启动之后Vite注入到HTML页面中的@vite/client文件中执行的。

WebSocket Server

在之前Vite预编译文章中,知道创建开发服务器逻辑是调用createServer方法来实现的,而其中涉及到WebSocket Server的创建逻辑如下:

 const ws = createWebSocketServer(httpServer, config, httpsOptions)
  • 1

而createWebSocketServer的逻辑也比较清晰,具体逻辑如下:

export function createWebSocketServer(
  server: Server | null,
  config: ResolvedConfig,
  httpsOptions?: HttpsServerOptions
): WebSocketServer {
  let wss: WebSocket
  let httpsServer: Server | undefined = undefined

  const hmr = isObject(config.server.hmr) && config.server.hmr
  const wsServer = (hmr && hmr.server) || server

  if (wsServer) {
    wss = new WebSocket({ noServer: true })
    // 协议更改支持
    wsServer.on('upgrade', (req, socket, head) => {
      if (req.headers['sec-websocket-protocol'] === HMR_HEADER) {
        wss.handleUpgrade(req, socket as Socket, head, (ws) => {
          wss.emit('connection', ws, req)
        })
      }
    })
  } else {
  	// 创建开发服务器
    // vite dev server in middleware mode
    wss = new WebSocket(websocketServerOptions)
  }
  wss.on('connection', (socket) => {
   // 建立连接
  })
  wss.on('error', (e: Error & { code: string }) => {
    // 错误处理
  })

  return {
    on: wss.on.bind(wss),
    off: wss.off.bind(wss),
    send(payload: HMRPayload) {
    	// 相关逻辑
    },
    close() {
    	// 相关逻辑
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44

上面的逻辑实际上总结下来就是基于开发服务器创建WebSocket Server,然后监听相关事件并返回相关实例方法。实际上Vite中是基于ws第三库来建立WebSocket Server,ws的具体使用可以查看ws使用文档

WebSocket Client

在之前 Vite按需编译文章中,知道当请求index.html时Vite会在页面中插入@vite/client文件请求,而该文件的主要逻辑之一就是创建WebSocket客户端。

// 根据开发服务器地址使用WebSocket对象创建客户端
const socketProtocol =
  __HMR_PROTOCOL__ || (location.protocol === 'https:' ? 'wss' : 'ws')
const socketHost = `${__HMR_HOSTNAME__ || location.hostname}:${__HMR_PORT__}`
const socket = new WebSocket(`${socketProtocol}://${socketHost}`, 'vite-hmr')

socket.addEventListener('message', async ({ data }) => {
  handleMessage(JSON.parse(data))
})

socket.addEventListener('close', async ({ wasClean }) => {
	// 相关逻辑
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
HMR运行机制

当创建了对应的WebSocket服务器和客户端之后,当页面加载之后就会建立连接。当模块被修改WebSocket服务器端就会通过WebSocket连接通知浏览器热更新,这部分逻辑是在开发服务器定义的。

Vite内部使用chokidar库来实现文件的监听,当文件被修改就会被感知从而来执行相关逻辑,具体逻辑如下:

const watcher = chokidar.watch(path.resolve(root), {
    ignored: [
      '**/node_modules/**',
      '**/.git/**',
      ...(Array.isArray(ignored) ? ignored : [ignored])
    ],
    ignoreInitial: true,
    ignorePermissionErrors: true,
    disableGlobbing: true,
    ...watchOptions
  }) as FSWatcher

watcher.on('change', async (file) => {})
watcher.on('add', (file) => {
	handleFileAddUnlink(normalizePath(file), server)
})
watcher.on('unlink', (file) => {
	handleFileAddUnlink(normalizePath(file), server, true)
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

使用chokidar创建监听器对象,然后监听change、add、unlink事件对不同动作执行不同逻辑:

  • change:文件内容改变时触发
  • add:新增文件时触发
  • unlink:删除文件时触发
文件内容修改操作的回调处理

文件内容修改后回调处理逻辑主要有两点:

// 依赖图谱逻辑处理
moduleGraph.onFileChange(file)
// 处理更新
handleHMRUpdate(file, server)
  • 1
  • 2
  • 3
  • 4

在开发服务器启动过程中的会创建一个moduleGraph对象,该对象内部定义多个moduleMap,例如:

  • urlToModuleMap
  • idToModuleMap
  • fileToModulesMap

在对应map对象中对保存项目中所有模块相关的图谱信息,而这个图谱内容是在模块按需编译时创建。这里暂不关心依赖图谱的具体处理逻辑。

handleHMRUpdate负责处理具体的更新逻辑,实际上该方法的主要逻辑如下几点:

  • 对应配置文件的更改,会重启服务器
  • 对于client目录下文件、html文件等无法热更新的,只能全部加载
  • 创建HMR上下文对象,调用对应插件的handleHotUpdate方法,得到需要更新模块

所有需要更新的操作都是通过WebSocket连接发送相关类型和数据到浏览器端:

ws.send({ type: 'full-reload' })
ws.send({ type: 'update', updates })
  • 1
  • 2

然后由浏览器端根据对应的type类型做相关处理。相关类型有:

  • full-reload
  • update:该类型下还有不同的子类型,而子类型主要是跟文件类型有关,例如js-update、css-update

在客户端@vite/client文件中,对于full-reload的逻辑主要就是调用location.reload来实现整体重新加载。而对于update的处理逻辑:

  • 对于JS模块更新会先构建特殊的url使用import动态请求需要更新的模块文件(?import&t=时间戳&其他参数),然后将加载好的模块以及模块依赖结果存放到队列中,然后批量更新。
文件新增和文件删除操作的回调处理

文件新增和文件删除都是调用handleFileAddUnlink方法来实现的,而该方法的逻辑主要是操作服务器对象的_globImporters属性:

// 从依赖图谱中获取新增或删除文件地址对应的相关模块
const modules = [...(server.moduleGraph.getModulesByFile(file) ?? [])]
// 删除操作
if (isUnlink && file in server._globImporters) {
	delete server._globImporters[file];
} else {
	// 新增操作
	for (const i in server._globImporters) {
    	const { module, importGlobs } = server._globImporters[i];
        for (const { base, pattern } of importGlobs) {
        	if (micromatch_1.isMatch(file, pattern) ||
            	micromatch_1.isMatch(path__default.relative(base, file), pattern)) {
            	modules.push(module);
                server.moduleGraph.onFileChange(module.file);
                break;
            }
        }
    }
}

// 使用WebSocket连接发送给客户端相关数据,这边逻辑与文件修改时相同
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

对于新增或删除的文件要判断是否是通过import.meta.glob方式来加载的,_globImporters属性实际上就是存储了import.meta.glob方式来加载的相关模块信息,具体相关信息暂不关心。

结语

本文梳理了Vite中HMR主要的处理逻辑,可以知道下面流程:

  • 在预编译阶段即开发服务器创建时
    • 使用ws库来基于开发服务器(HTTP服务)创建WebSocket服务端
    • 使用chokidar创建监听器,监听相关文件变动,针对新增、删除、修改做对应处理
  • 在按需编译阶段即开发服务器运行时,在客户端即@vite/client文件中使用WebSocket对象按照开发服务器地址创建WebScoket客户端从而建立连接

当修改文件后,监听器监听到对应文件改变就会触发change事件,其回调函数会被执行:

  • 首先会处理依赖图谱相关的逻辑
  • 对于client目录、html、vite配置文件等不同文件的更改会通知浏览器做相关操作,通知的机制就是通过WebSocket连接来实现的
    • vite配置文件的更改会重启服务器
    • client目录下文件和html文件会触发full-load类型的操作,该操作浏览器直接是location.reload, 即重新加载
    • 其他文件更改触发update类型操作,而update类型下又细分js-update和css-update子类型,其中对于js模块的更新,会构建?import&t=时间戳形式的地址使用import动态加载模块,加载后的内容会存入队列中批量更新

当新增或删除文件后,监听器就会触发add、unlink事件,之后通知客户端更新的逻辑与新增操作时并没有任何区别,只是要处理模块不同而已。

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号