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

