简单代码热更新

背景

  1. 开发静态页面修改样式时需要手动刷新页面才会在页面上看到效果比较麻烦
  2. 了解了热更新原理需要实践下
  3. 手动实现了websocket 的解析,趁热打铁把 websocket 应用在实际项目中

原理

  1. 服务端监听文件改动
  2. 当文件有改动之后服务端发送事件通知客户端
  3. 客户端接收到事件之后做出对应的处理

实现

服务端监听文件改动

nodejs 在 fs 模块中提供了两个监听文件改动的方法 fs.watchfs.watchFile 两个方法。

fs.watch 参考

监听文件及文件夹下的改动,通过如下方式调用:

1
fs.watch(filename[, options], listener)
  • filename: 文件或目录
  • options: 可选的,如果 options 传入字符串,则它指定 encoding。 否则,options 应传入对象,属性如下:
    • persistent: <boolean> 指示如果文件已正被监视,进程是否应继续运行。默认值: true
    • recursive: <boolean> 指示应该监视所有子目录,还是仅监视当前目录。这适用于监视目录时,并且仅适用于受支持的平台(参见注意事项)。默认值: false。
    • encoding: <string> 指定用于传给监听器的文件名的字符编码。默认值: utf8
  • listener: 监听器回调有两个参数 (eventType, filename)eventTyperenamechangefilename 是触发事件的文件的名称。
fs.watchFile 参考

监听特定文件的改动,通过如下方式调用:

1
fs.watchFile(filename[, options], listener)
  • filename: 文件
  • options: 可选的,options 应传入对象,属性如下:
    • persistent: <boolean> 指示如果文件已正被监视,进程是否应继续运行。默认值: true
    • interval : 指示轮询目标的频率(以毫秒为单位)。
    • bigint: 回调函数的参数对象是否为BigInts类型。
  • listener: 监听器回调有两个参数 (curr, prev)curr 是文件修改后的 fs.Stat 实例, prev 是文件修改前的 fs.Stat 实例。

通知客户端

通过建立一个 websocket 服务的方式,当文件有变更的时候发送更新消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
server.on('upgrade', function (req, socket, head) {
const read = new WebsocketRead(req, socket)
const write = new WebsocketWrite(socket)

console.log('websocket connect success')

read.on('text', function (data) {
console.log('接收到响应', data)
})

read.on('ping', function () {
write.pong()
})

watch.on('update', (data) => {
write.send(data)
})
})

客户端处理事件

在客户端插入建立 websocket 链接的代码,接收到更新之后重新刷新页面

1
2
3
4
5
6
7
<script>
const socket = new WebSocket(`ws://\${location.host}`)
socket.onmessage = function (e) {
const data = e.data
location.reload()
}
</script>

其它

自动注入建立 websocket 链接的代码

返回 HTML 页面时通过正则的方式在页面中插入代码

1
2
3
4
5
6
let data = fs.readFileSync(filePath)
if (path.extname(filePath) === '.html') {
data = data
.toString()
.replace(/\<\/\s*body>/, `${websocketTemplateStr}</body>`)
}

对文件修改监听的封装

通过封装成对应的方式,利用事件总线的模式实现事件的派发

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
const fs = require('fs')
const { EventEmitter } = require('events')
const path = require('path')

const { resolveRootPath } = require('./utils')

class Watch extends EventEmitter {
constructor(dir) {
super()
this.watchPath = resolveRootPath(dir)
fs.watch(this.watchPath, { recursive: true }, this.handleCallback)
}

handleCallback = (eventType, filename) => {
try {
const content = fs
.readFileSync(path.resolve(this.watchPath, filename), {
encoding: 'utf-8',
})
.toString()
this.emit('update', {
filename,
content,
})
} catch (e) {
this.emit('remove', {
filename,
content: null,
})
}
}
}

module.exports = Watch

优化

  1. fs.watchfs.watchFile 兼容性考虑,在不兼容的平台上可以通过定时检查文件的方式判断文件是否有改动
  2. websocket 兼容性考虑,不支持的平台上降级为短轮询或长轮询的方式
  3. 文件修改时可以在抛出的事件中提供更多的信息,事件局部文件的热更新而不用刷新整个页面

参考文献