在项目中,我们经常需要一个自动刷新的功能。无论你使用 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 的顺序是正确的。
这就是简单的自刷新中间件了。