长久以来我的博客就遇到一个很蛋疼的问题:在更新一个页面的时候,有一定的概率 Hexo 会渲染出来一个空白的页面,尽管源文件显然不是空的。这时候重新尝试保存文件,多试几次就能让渲染恢复正常。不过这个问题非常让人烦躁。今天在写一篇长文过程中无数次遭遇这个问题之后我决心秉承“磨刀不误砍柴工”的信念,决定抽时间调试一下这个问题。

尽管艰难的基于命令行输出的调试之后,最后发现,在读取文件阶段 data.content,即文章内容就已经是空的了。我编辑的文件是一个 page,而非 post (即我的 Markdown 文件并不位于 _posts 目录下),因此,执行文件读取的代码位于 /lib/plugins/processor/assset.js 中,

1
2
3
4
5
6
7
8
// ...
return Promise.all([
file.stat(),
file.read()
]).spread((stats, content) => {
const data = yfm(content);
const output = ctx.render.getOutput(path);
// ...

此处的 content 就已经是空的了。注意我的意思是,这里的 content 是一个空的字符串,而不是 null 或者 undefined。读取一个非空文件读到空的内容,这是一个典型的读写竞争的问题。即另一端写入文件尚未完成的时候,Hexo 对应文件的 change 时间就已经发生,此时对目标文件读取就会导致的读到空的内容。

StackOverflow 的这个帖子 chokidar: onchange event for a file is possibly triggered to fastGetting empty string from fs.readFile inside chokidar.watch(path_file).on('change', …) 都讨论了这个问题。具体而言,要解决上面的问题,我们相遇让 chokidar 库在目标文件的写入完成以后再触发 change 事件。这可以通过 awaitWriteFinish 参数进行。在使用这个参数时,如果目标文件发生了变化,chokidar 会检测目标文件的大小,当其大小超过一段时间没有继续变化后才会触发 change 事件。使用例子如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const watcher = chokidar.watch(configPathString, { 
persistent: true,
awaitWriteFinish: {
stabilityThreshold: 500
}
});
watcher.on('change', (path, stats) => {

fs.readFile(configPathString,(err, data)=>{
if (err) throw err;

//console.log('data',data);

let receivedData = JSON.parse(data);

//Do whatever you like
})
});

其中 stabilityThreshold 这个参数决定了 chokidar 需要在等待多长的时间最终确定目标文件的大小已经不在变化,单位是毫秒。

Hexo 同这两个问题中的场景一样,在监视文件变化的时候都是使用了 chokidar 这个库。我们来看 lib/box/index.js 这个文件,约 175 行前后

1
2
3
4
return this.process().then(() => watch(base, this.options)).then(watcher => {
this.watcher = watcher;

watcher.on('add', path => {

其中这个 this.options 最终会传递给 chokidar。所以我们来到 Box 类的开头,修改器 options 成员定义如下:

1
2
3
4
5
6
this.options = Object.assign({
persistent: true,
awaitWriteFinish: {
stabilityThreshold: 200
}
}, options);

这里我们的阈值时间设定的是 200ms。基本上现代硬盘,200ms的间隔足够了。


当天我在 Hexo 的 Github 上提交了 Issue (#4632),目前这个问题已经修复 (!4633)。