likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 64

先来张美女镇楼

原生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 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响应

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>热更新demo</title>
</head>

<body>
    <h2>测试</h2>
</body>
<script>
    const clientWS = new WebSocket('ws://localhost:8000')

    function onOpen() {
        clientWS.send('我上线了')
    }

    function onMessage({ data }) { console.log(data) }

    function onError() { console.error('socket连接失败') }

    clientWS.onopen = onOpen
    clientWS.onmessage = onMessage
    clientWS.onerror = onError
</script>

</html>

控制台启动服务原生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<WebSocket, WebSocket> = 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代码,让其能响应更新

...代码
<script>
    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
</script>

现在可以去试修改文件了,热更新已经初步完成!

接下来还有一个要考虑的点,实现热更新还需要自己手动添加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<script type="module">
      ${main}
      </script>`)
      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 === 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服务器来处理,当模板文件添加了代码类似于<script src='xxx.js'></script>的时候,我们希望能把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部分结束

PS:如果觉得有收获,点个赞吧!

源码地址 https://github.com/1596944197...