长久以来我的博客就遇到一个很蛋疼的问题:在更新一个页面的时候,有一定的概率 Hexo 会渲染出来一个空白的页面,尽管源文件显然不是空的。这时候重新尝试保存文件,多试几次就能让渲染恢复正常。不过这个问题非常让人烦躁。今天在写一篇长文过程中无数次遭遇这个问题之后我决心秉承“磨刀不误砍柴工”的信念,决定抽时间调试一下这个问题。
尽管艰难的基于命令行输出的调试之后,最后发现,在读取文件阶段 data.content
,即文章内容就已经是空的了。我编辑的文件是一个 page,而非 post (即我的 Markdown 文件并不位于 _posts
目录下),因此,执行文件读取的代码位于 /lib/plugins/processor/assset.js
中,
1 | // ... |
此处的 content
就已经是空的了。注意我的意思是,这里的 content
是一个空的字符串,而不是 null
或者 undefined
。读取一个非空文件读到空的内容,这是一个典型的读写竞争的问题。即另一端写入文件尚未完成的时候,Hexo 对应文件的 change
时间就已经发生,此时对目标文件读取就会导致的读到空的内容。
StackOverflow 的这个帖子 chokidar: onchange event for a file is possibly triggered to fast 和 Getting empty string from fs.readFile inside chokidar.watch(path_file).on('change', …) 都讨论了这个问题。具体而言,要解决上面的问题,我们相遇让 chokidar 库在目标文件的写入完成以后再触发 change
事件。这可以通过 awaitWriteFinish
参数进行。在使用这个参数时,如果目标文件发生了变化,chokidar 会检测目标文件的大小,当其大小超过一段时间没有继续变化后才会触发 change
事件。使用例子如下:
1 | const watcher = chokidar.watch(configPathString, { |
其中 stabilityThreshold
这个参数决定了 chokidar 需要在等待多长的时间最终确定目标文件的大小已经不在变化,单位是毫秒。
Hexo 同这两个问题中的场景一样,在监视文件变化的时候都是使用了 chokidar 这个库。我们来看 lib/box/index.js
这个文件,约 175 行前后
1 | return this.process().then(() => watch(base, this.options)).then(watcher => { |
其中这个 this.options
最终会传递给 chokidar。所以我们来到 Box
类的开头,修改器 options
成员定义如下:
1 | this.options = Object.assign({ |
这里我们的阈值时间设定的是 200ms。基本上现代硬盘,200ms的间隔足够了。