热更新探究

829 阅读2分钟

我们经常使用通过webpack搭建的脚手架,比如vue-cli来开发应用,当代码修改时页面直接更新,还有我经常在用的npm包:live-server静态服务器,代码修改后也直接更新视图,感觉很爽,所以想来探究一下里面是什么原理?是怎么工作的?

live-server

通过对live-server的探究,其热更新策略如下

live-server的源码较少,我就不贴了,通过对其的探究自己实现了一个热更新

核心

  1. 监听文件变化,通知ws透传客户端更新
  2. 更新策略

用到的npm包

faye-websocket

其用于在Node中轻松构建WebSocket服务器和客户端的类,因为node自己本身没有实现一个scoket模块

chokidar

封装 Node.js监控文件系统文件变化功能的库,为什么不用使用node本身的fs.watch和fs.watchFile?可以看这里

实现还是比较简单浅显的,直接贴出来吧!

服务端代码:

  1. 实现了一个简单静态服务器
  2. ws实例注册
  3. 文件监听执行ws发送客户端

npm包引入

const http = require('http')
const fs = require('fs')
const path = require('path')
const WebSocket = require('faye-websocket')
const chokidar = require('chokidar');
const mime = require('mime-types')
const networkInterfaces = require('os').networkInterfaces()

简单静态服务器

// 获取注入脚本片段
const INJECTED_CODE = fs.readFileSync(path.resolve(__dirname, "injected.html"), "utf8");

const resolvePublic = (fileName) => path.resolve(`${__dirname}/public/${fileName}`);

// 静态服务器
const app = http.createServer((req, res) => {
  if (req.url === '/') {
    res.setHeader('content-type', 'text/html')
    
    // 合并html入口文件和注入脚本返回
    res.end(fs.readFileSync(resolvePublic('index.html')) + INJECTED_CODE)

  } else {
    try {
      let file = req.url.split('?')[0]
      res.setHeader('content-type', mime.lookup(file))
      res.end(fs.readFileSync(resolvePublic(file)))
    } catch (e) {
      res.end()
    }
  }
})

监听http upgrade事件,注入webscoket实例

// 注册WebSocket 实例
let clients = []

app.on('upgrade', (request, socket, head) => {
  const ws = new WebSocket(request, socket, head)
  ws.onopen = function() {
    ws.send('connected');
  }
  ws.onclose = function() {
    // 过滤掉当前关闭ws实例
    clients = clients.filter(function (x) {
      return x !== ws;
    });
  }
  clients.push(ws)
})

监听文件变化,向客户端发送信息

// 监听静态文件变化
const watcher = chokidar.watch(resolvePublic('/'))
watcher
	.on("change", staticFileChange)
	.on("add", staticFileChange)
	.on("unlink", staticFileChange)
	.on("addDir", staticFileChange)
	.on("unlinkDir", staticFileChange)
	.on("ready", function () {
		console.log("Ready for changes");	
	})
	.on("error", function (err) {
		console.log("ERROR:".red, err);
});
    
function staticFileChange (changePath) {
  const isCssFile = path.extname(changePath) === '.css'
  console.log(`change file:${changePath}`)
  clients.forEach(wx => {
    wx.send(isCssFile ? 'refreshcss' : 'reload')
  })
}

启动

app.listen(1111, () => {
  const address = networkInterfaces.en0[1].address || networkInterfaces.eth0[0].address // 获取内网ip
  const notice = `
    http://localhost:1111,
    http://${address}:1111
  `
  console.log(notice)
})

injected.html 注入文件

更新策略

  • 非css文件直接reload
  • css文件通过原链接后缀加时间戳重新请求css文件更新视图
<script type="text/javascript">
	// <![CDATA[  <-- For SVG support
	if ('WebSocket' in window) {
		(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.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 + '/ws';
			var socket = new WebSocket(address);
			socket.onmessage = function(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>

未完...待续...