自己写一个 livereoload 中间件

广告位招租
扫码页面底部二维码联系

在项目中,我们经常需要一个自动刷新的功能。无论你使用 webpack-dev-server 还是其他工具,不够,市面上的工具都稍微有点复杂,会另外起一个端口,用于服务端和客户端相互通信。这在某些条件下反而不符合要求,有些情况下,我们只有一个端口可以使用,只能在当前服务基础上进行处理。我专门写了一个 express 的中间件来实现。

const chokidar = require('chokidar'); // 除了其他依赖,还需要依赖这个,用于监听文件变动

// 开启保存自动刷新功能
if (livereload) {
	app.use(createLiveReload({
		matchUrls: [/^\/some/, '/index.html'],
		renderFile: path.join(__dirname, 'www/index.html'),
		watchFiles: path.join(__dirname, 'www/**/*'),
                isReady: () => true,
	}))
}

/**
 * 创建一个 livereload 中间件
 * @param {*} options
 */
function createLiveReload(options) {
	const { matchUrls, renderFile, watchFiles, intervalTime = 1000, isReady } = options

	let latest = Date.now()
	let modified = latest

	chokidar.watch(watchFiles, {
		ignored: /(.*\.(map)$)|(\/vendor\/)/,
	}).on('all', () => {
		if (typeof isReady === 'function' && !isReady()) {
			return
		}
		modified = Date.now()
	})

	return function(req, res, next) {
		const { method, originalUrl } = req

		if (method.toLowerCase() !== 'get') {
			next()
			return
		}

		const scriptUrl = '/_live-reload.js'
		const url = URL.parse(originalUrl)
		const { pathname } = url

		if (pathname === scriptUrl) {
			if (modified > latest) {
				const content = `window.location.reload(true)`
				latest = modified
				res.end(content)
			}
			else {
				const content = `
					var currentScript = document.currentScript;
					setTimeout(function() {
						// 移除老的脚本
						currentScript.parentNode.removeChild(currentScript)
						// 插入新的脚本
						const script = document.createElement('script')
						script.src = '/_live-reload.js?v=' + Date.now()
						document.body.appendChild(script)
					}, ${intervalTime})
				`
				res.setHeader('content-type', 'application/json; charset=utf-8')
				res.end(content)
			}
		}
		else if (matchUrls.some(item => item instanceof RegExp ? item.test(pathname) : item === pathname)) {
			fs.readFile(renderFile, (err, data) => {
				if (err) {
					res.sendStatus(404);
				}
				else {
					const html = data.toString()
					const body = html.indexOf('</body>')
					const script = `<script src="${scriptUrl}"></script>`
					if (body > -1) {
						const content = html.replace('</body>', script + '</body>')
						res.end(content)
					}
					else {
						const content = html + script
						res.end(content)
					}
				}
			})
		}
		else {
			next()
		}
	}
}

实现原理,就是在前端界面不断的移除,新增脚本,脚本的内容由服务端输出,在服务端文件变化时,脚本内容为 window.location.reload(true) 从而刷新页面。

需要注意两个点:

  • 由于它会直接读取文件后立即渲染,所以,在 express 的路由列表中,要注意其顺序,放在 app.use(express.static(...)) 后面,这样可以保证静态文件都可以被访问到,放在其他所有动态路由的前面,这样能保证通过 matchUrls 匹配到的 url 能够使用 renderFile 进行渲染
  • 你可能需要根据不同的 url 渲染不同的文件,此时,你要多次调用 createLiveReload 来实现多个中间件实例,当然,这个时候你要保证 matchUrls 的顺序是正确的。

这就是简单的自刷新中间件了。