先来张美女镇楼
前言
刷新我们一般分为两种:
- 一种是页面刷新,不保留状态,使用
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 UrlList
switch (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
测试
控制台启动服务
访问 http://localhost:3000
,可以看到服务socket已经成功链接
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')
// # 文件监听部分结束
现在,在模板文件修改即会触发客户端响应了
修改模板文件的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 === 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))
运行一下,发现已经成功处理并响应
但是,再次更改模板文件的时候,发现出现这种情况
出现这种情况,是因为没有清理
缓存文件,我们都知道每次客户端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 === UPDATE) location.reload()
}
function onError() {console.error('socket连接失败')}
clientWS.onopen = onOpen
clientWS.onmessage = onMessage
clientWS.onerror = onError
window.onbeforeunload = () => clientWS.send(CLEAR)
`
// # 全局变量结束
// # socket部分
...代码
ws.onmessage = ({ data }) => data === 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);
// # 文件监听部分结束
好了,代码基本完成,现在更改模板文件就可以正常触发热更新了...
但是,我们还会发现,现在只是监听了一个文件,当模板文件引用其他文件的时候,其他文件发现变化并不触发更新
,这并不是我们想要的,所以,我们还需要做一个处理...
监听与模板文件关联的文件
还是通过htp服务器来处理,当模板文件添加了代码类似于的时候,我们希望能把xxx.js文件也监听了,添加如下代码
// # 全局变量
...代码
// 保存除模板文件外的文件列表
const dynamicResourceList = new Proxy([] as string[], {
set(target, p, value, receiver) {
if (typeof value === '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部分结束