使用koa简单实现前端js热更新测试服务示例

很小伙伴或许和我一样,在平时写前端项目的时候都是使用网上现成的脚手架吧。在编写js,或者vue代码的时候,在浏览器端显示的界面,会及时刷新,也是处于好奇,想着自己是不是可以简单实现一下呢?说干就干,于是有了这篇文章

先来讲一下实现的原理

[“实际使用的脚手架框架中不仅仅有热更新,还有代码的压缩打包等功能,这里就仅仅实现热跟新的功能”]

运行测试服务器的时候会开启http服务与webSockit服务以及通过插件监听文件变化
http服务是用来访问测试页面的,
通过开启目录变化的监听,当目录文件发生变化的时候通过webSockit控制是否全局或是局部的刷新。

1. 首先来创建静态html文件

html这里用到了es6的对外部包的引用,需要type="module"标识,可以使用import命令加载其他模块。

<!DOCTYPE html>
<html>

<head>
    <meta charset="UTF-8"> 
    <title>Vite App</title>
</head>

<body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
</body>

</html>

2. 使用chokidar插件来实现对文件变化监听

nodejs 里面有个fs.watch api。它可以监听文件变动,
使用示例:fs.watch(filename[, options][, listener])

// WebSocket 连接客户端实例
const wsClients=[];
/*
.............
*/
function fileChange(event, changePath) {
    const refreshcss = path.extname(changePath) === '.css'&&event!='add'//判断刷新类型
    console.log(`event: ${event}    change file:${changePath}`)
    wsClients.forEach(ws => {//遍历已连接的webSockit客户端执行更新
        ws.send(refreshcss ? 'refreshcss' : 'reload')
    })
}

chokidar.watch('src', {
    persistent: true,
    ignored: /(^|[\/\\])\../, // 忽略点文件
    cwd: '.', // 表示当前目录
    depth: 0 // 只监听当前目录不包括子目录
}).on('all', fileChange);//监听除了ready, raw, and error之外所有的事件

3. 使用Koa快速创建http服务

const fs = require('fs')
const path = require('path')
const Koa = require('koa')
const mime = require('mime-types')
const networkInterfaces = require('os').networkInterfaces()

const app = new Koa();


app.use(async ctx => {
    const { request: { url } } = ctx
    // 首页
    if (url == '/') {
        ctx.type = "text/html"
        ctx.body = fs.readFileSync('./index.html', 'utf-8')
    } else if (url.endsWith('.js')) {
        try {
            let file = ctx.req.url.split('?')[0]
            ctx.type = mime.lookup(file)
            ctx.body = fs.readFileSync(resolvePublic(file), 'utf-8')
            //........
        } catch (e) {

        }
    }
})

/*
...
*/

const PROT=3001;
server.listen(PROT, () => {
    const ens = Object.keys(networkInterfaces)[0];
    const address = networkInterfaces[ens][1].address || networkInterfaces[ens][0].address // 获取内网ip
    const notice = `open:
          http://localhost:${PROT},
          http://${address}:${PROT}
        `
    console.log(notice)
});

4. 添加WebSocket服务

添加WebSocket服务有很多方法和相关的插件,这里就使用ws库来实现吧。

//...
const WebSocket = require('ws');
//...
const wss = new WebSocket.Server({);
//...
wss.on('connection', function connection(ws) {
    ws.send('connected');
    wsClients.push(ws);//连接成功自动添加
    ws.onclose = function () {
        // 过滤掉当前关闭ws实例
        wsClients = wsClients.filter(function (x) {
            return x !== ws;
        });
    }
});

//...

5. http与WebSocket端口共用

为了实现能够让http与WebSocket公用端口,使用http库进行结合

//...
const http = require('http')
//...
//...
const server = http.createServer(app.callback())
const wss = new WebSocket.Server({// 公用端口
    server
});
//...
//...
//...
const PROT=3001;
server.listen(PROT, () => {
    const ens = Object.keys(networkInterfaces)[0];
    const address = networkInterfaces[ens][1].address || networkInterfaces[ens][0].address // 获取内网ip
    const notice = `open:
          http://localhost:${PROT},
          http://${address}:${PROT}
        `
    console.log(notice)
});

6. 注入浏览器端的WebSocket交互客户端代码

虽然开启了WebSocket服务,但是前端需要能够连接上服务才能,所以需要往html页面或者是js中注入相关的脚本代码。

我就用简单粗暴的方式来演示一下:

//...

app.use(async ctx => {
    const { request: { url } } = ctx
    // 首页
    if (url == '/') {
        ctx.type = "text/html"
        ctx.body = fs.readFileSync('./index.html', 'utf-8') + `
        <script type="text/javascript">
            (function () {
                function refreshCSS() {
                    var sheets = [].slice.call(document.getElementsByTagName("link"));
                    var head = document.getElementsByTagName("head")[0];
                    for (var i = 0; i < sheets.length; ++i) {
                        var elem = sheets[i];
                        head.removeChild(elem);
                        var rel = elem.rel;
                        if (elem.href && typeof rel != "string" || rel.length == 0 || rel.toLowerCase() == "stylesheet") {
                            var url = elem.href.split("?")[0]
                            elem.href = url + '?_=' + (+new Date());
                            // var url = elem.href.replace(/(&|\?)_cacheOverride=\d+/, '');
                            // elem.href = url + (url.indexOf('?') >= 0 ? '&' : '?') + '_cacheOverride=' + (new Date().valueOf());
                        }
                        head.appendChild(elem);
                    }
                }
                var protocol = window.location.protocol === 'http:' ? 'ws://' : 'wss://';
                var address = 'ws://' + window.location.host + window.location.pathname;
                var socket = new WebSocket(address);
                socket.onmessage = function (msg) {
                    console.log(msg);
                    if (msg.data == 'reload') window.location.reload();
                    else if (msg.data == 'refreshcss') refreshCSS();
                    else { console.log(msg.data); }
                };
                console.log('Live reload enabled.');
            })();
    
        </script>`
    } else {
        try {
            let file = ctx.req.url.split('?')[0]
            ctx.type = mime.lookup(file)
            ctx.body = fs.readFileSync(resolvePublic(file), 'utf-8')
        } catch (e) {

        }
    }
})
//...
最后完整代码放在:

https://gitee.com/baojuhua/hot_refresh_server_simple/tree/master

推荐阅读更多精彩内容