原生JS实现一个简易版的热刷新

先来张美女镇楼
原生JS实现一个简易版的热刷新
文章图片

前言 刷新我们一般分为两种:

  • 一种是页面刷新,不保留状态,使用window.location.reload()
  • 另一种是基于WDS (Webpack-dev-server)的模块热替换Hot Module Replacement,只需要局部刷新页面上发生变化的模块,保留当前的数据状态,其实就是就是相当于不进行垃圾回收
本次只讲解第一种(因为第二种没写),各位如果对第二种有兴趣,可以自行查阅对应的文章 涉及到的技术栈
  • websocket
  • nodejs
  • httpServer
安装所需模块
yarn add ws // 如使用ts开发 yarn add -D @types/ws @types/node tsc --init

文件初始目录结构 @linux的tree命令
. ├── README.md ├── index.html ├── package.json ├── serve.ts<--主要文件 ├── tsconfig.json └── yarn.lock

I - 第一步先起个http服务 具体作用请往后看
import { createServer } from 'http' import { readFileSync } from 'fs'// TIP 为了方便理解,全部使用同步函数// # http部分 enum UrlList { ICON = '/favicon.ico', HOME = '/', } const htpServer = createServer((req, res) => { const url = req.url as UrlListswitch (url) { case UrlList.HOME: return res.end(readFileSync('./index.html', 'utf-8')) case UrlList.ICON: // # 懒得放图标,拿别人网站的吧 res.statusCode = 302 res.setHeader('Location', 'https://developer.mozilla.org/favicon-192x192.png') return res.end() default: } })htpServer.listen(3000, () => console.log('htp服务开启')) // # http部分结束

最简单的htp服务就起好了!
II - 再起个websocket服务 主要是用来通知客户端进行刷新或者其他操作
import { WebSocketServer, WebSocket } from 'ws'; // # socket部分 const wss = new WebSocketServer({ port: 8000 }); wss.on('connection', assignment); wss.on('listening', () => console.log('websocket正在监听'))function assignment(ws: WebSocket) { console.log('用户连接') ws.onmessage = ({ data }) => console.log(data) ws.onclose = () => console.log('用户离开') } // # socket部分结束... 代码

III - 编写模板文件测试ws响应
热更新demo 测试

控制台启动服务
原生JS实现一个简易版的热刷新
文章图片

访问 http://localhost:3000,可以看到服务socket已经成功链接
原生JS实现一个简易版的热刷新
文章图片

IIII - 思考热更新本质 热更新,本质上就是服务器监听客户端当前引用的文件,当文件被更改了,通过socket发送一个socketMessage给客户端,客户端来进行更新操作。明白了基础原理,我们下一步来做监听文件
IV - 监听客户端当前引用的文件 nodejs可以通过watch函数来监听文件的变更。http://nodejs.cn/api/fs.html#...
import { readFileSync, watch } from 'fs'// # 全局变量 // 将每个ws链接都保存起来 const wsServers: Map = new Map() const UPDATE = 'update' let i = 0 // # 全局变量结束// 函数更改 function assignment(ws: WebSocket & { id: number }) { console.log('用户连接') ws.id = i++ wsServers.set(ws, ws) ws.onmessage = ({ data }) => console.log(data) ws.onclose = () => ( wsServers.delete(ws), console.log(`id:${ws.id},用户退出`), ) }...代码// # 文件监听部分 let timer: NodeJS.Timeout | null = null function watchFile(target: string) { watch(target, (type) => { if (type === 'rename') return new Error('文件缺失') if (timer) return timer = setTimeout(() => { wsServers.forEach(item => item.send(UPDATE)) timer = null }, 100); }) } watchFile('index.html') // # 文件监听部分结束

现在,在模板文件修改即会触发客户端响应了
原生JS实现一个简易版的热刷新
文章图片

修改模板文件的socket代码,让其能响应更新
...代码

现在可以去试修改文件了,热更新已经初步完成!
接下来还有一个要考虑的点,实现热更新还需要自己手动添加socket代码,这很明显是不可能的!所以我们下一步需要抽离script内容
抽离JS代码
目前我们是用htp服务来返回页面的,既然是htp服务,那我们就可以用nodejs在请求响应之前做点“手脚”,比如给模板文件嵌入内容
// # 全局变量 ...代码 const template = 'index.html' const cacheTempPath = '_' + template// * 将main.js(script内容)文件内容提取出来,如果需要,可以拷贝内容到根目录的main.js const main = ` const clientWS = new WebSocket('ws://localhost:8000')const UPDATE = 'update' const CLEAR = 'clear'function onOpen() {}function onMessage({ data }) { if (data =https://www.it610.com/article/== UPDATE) location.reload() }function onError() {console.error('socket连接失败')}clientWS.onopen = onOpen clientWS.onmessage = onMessage clientWS.onerror = onError `// # http部分 ...代码 case UrlList.HOME: const temp = readFileSync(template, 'utf-8') // 建立一个模板文件的缓存副本,用来进行处理,不然如果对模板文件直接进行处理,那编写体验就非常不良好了 appendFileSync(cacheTempPath, ` ${temp.toString()} \n`) return res.end(readFileSync(cacheTempPath))

运行一下,发现已经成功处理并响应
原生JS实现一个简易版的热刷新
文章图片

但是,再次更改模板文件的时候,发现出现这种情况
原生JS实现一个简易版的热刷新
文章图片

出现这种情况,是因为没有清理缓存文件,我们都知道每次客户端reload后都会重新请求htp服务器获取html文件,然鹅我们的代码只是简单的添加内容到缓存文件上,并没有清除原先的内容。所以,接下来就是...
在每次刷新时清除缓存文件内容
分析一下,发现很简单,有一个生命周期是dom页面销毁之前触发的window.onbeforeunload,我们可以给客户端添加一下代码,在这个生命周期触发时,给服务器发送一个清除指令。又或者在服务器的监听函数那里添加代码,每次监听到变更时触发清理
...代码 // 清理tag const CLEAR = 'clear' // 清理函数 const clearTemp = () => writeFileSync(cacheTempPath, '')const main = ` const clientWS = new WebSocket('ws://localhost:8000')const UPDATE = 'update' const CLEAR = 'clear'function onOpen() {}function onMessage({ data }) { if (data =https://www.it610.com/article/== UPDATE) location.reload() }function onError() {console.error('socket连接失败')}clientWS.onopen = onOpen clientWS.onmessage = onMessage clientWS.onerror = onErrorwindow.onbeforeunload = () => clientWS.send(CLEAR) ` // # 全局变量结束// # socket部分 ...代码 ws.onmessage = ({ data }) => data =https://www.it610.com/article/== CLEAR ? clearTemp() : null ws.onclose = () => ( wsServers.delete(ws), console.log(`id:${ws.id},用户退出`), clearTemp() ) // # socket部分结束or// # 文件监听部分 ...代码 timer = setTimeout(() => { wsServers.forEach(item => item.send(UPDATE)) timer = null clearTemp() }, 100); // # 文件监听部分结束

好了,代码基本完成,现在更改模板文件就可以正常触发热更新了...
但是,我们还会发现,现在只是监听了一个文件,当模板文件引用其他文件的时候,其他文件发现变化并不触发更新,这并不是我们想要的,所以,我们还需要做一个处理...
监听与模板文件关联的文件
【原生JS实现一个简易版的热刷新】还是通过htp服务器来处理,当模板文件添加了代码类似于的时候,我们希望能把xxx.js文件也监听了,添加如下代码
// # 全局变量 ...代码 // 保存除模板文件外的文件列表 const dynamicResourceList = new Proxy([] as string[], { set(target, p, value, receiver) { if (typeof value =https://www.it610.com/article/=='string') watchFile(value) return Reflect.set(target, p, value, receiver) } }) // # 全局变量结束// # http部分 // 添加default的处理 ...代码 default: try { const path = '.' + url if (dynamicResourceList.find(item => item === path)) { return res.end() } else { dynamicResourceList.push(path) const file = readFileSync(path, 'utf-8') return res.end(file) } } catch (error) { console.error(error); return res.end() } } // # http部分结束

PS:如果觉得有收获,点个赞吧! 源码地址 https://github.com/1596944197...

    推荐阅读