使用Node.js脚本自动监听发布文章到Hexo

部署好博客之后,我们就可以发布博文了。由于Hexo是静态博客,每次发布文章后均需要手动输入命令来部署博客。博主觉得十分麻烦,能不能用什么方法实现上传文章后自动部署?简单搜索后,发现了一些现成的工具。但博主喜欢自己动手,何况这个功能的原理应该很简单。所以决定自己造轮子了。本文将介绍博主写的自动部署工具。有需要的朋友可以参考修改也可以直接使用。

Node.js

首先就是选择使用的语言。由于博客部署在Ubuntu,是Linux环境,能方便使用的语言是Shell本身和Python,外加安装Hexo之前必须安装的Node.js,一共是三种语言可以选择。博主Shell命令用的多,但是很少编写Shell程序,排除。Python博主也很喜欢,但是由于最近实习需要每天使用Nodej.js,所以更倾向于使用Node.js。之后如果有时间会更新Python和Shell版本的自动发布工具。 不打算更新了,有更好的轮子:Hexo-Auto-Deployer

原理

再来看看手动发布的流程。

  1. 上传.md文件到_post文件夹下(hexo new命令本质就是使用模板创建.md文件)
  2. 使用命令hexo d -g生成并发布文章

第一步的上传文件目前还是采取sftp上传。下一步考虑自动从OneDrive同步。

第二步是本篇的重点。这一步可以分为两部分。

  • 自动发布工具需要监听:
    • 添加文章
    • 删除文章
    • 修改文章
  • 运行发布命令

那么边看代码边讲解吧。

监听文件夹

如何监听_post文件夹内是否有新文件呢?首先想到的是使用setInterval函数轮询文件夹内的所有文件,然后比对上一次查询到的最后修改时间mTime 。如果不一致则代表修改过;新增和删除可以通过是否存在旧/新纪录来判断。但这个方法有个主要的缺点是轮询的间隔不好设置,过短对服务器有压力,过长则不能及时更新。谁也不想刚写完的文章要等好久才能看到效果吧,那还不如直接手动发布。

所以翻看fs模块的文档,找到了一个叫fs.watch的函数。这个函数刚好符合我的需求:通过事件监听机制及时获取文件变动状态。以下是第一步的监听文件夹的代码:

1
2
3
4
5
6
7
8
9
const fs = require('fs');		// 处理文件的模块
const path = require('path'); // 处理路径的模块

fs.watch(path.resolve(__dirname, 'blog', 'source', '_posts'), (event, filename) => {
if (filename) {
console.log(`Changes may happen on file ${filename}`);
// 发布文章的代码
}
});

非常简单明了。watch接受一个参数一个回调函数。参数是监听的目录,这里通过path.resolve来拼接目录地址,其中__dirname是当前工作目录。回调函数提供两个参数,event表示事件的类型,本工具暂时不做区分,所以不需要使用;filename是触发事件的文件名。

关键点

运行上面这段代码做测试,添加或修改文件,每次操作都会有输出,但是次数却不止一次。这是因为有些操作,比如写入,有些编辑器会先清空旧文件再写入,因此多次触发监听事件。其次根据平台的不同,有些操作会有中间态,持续几十到几百毫秒不等,因此也会多次触发监听事件。

这个问题不细想可能觉得没问题,但是仔细思考会发现这样做有些隐患。显而易见的是浪费资源,发布一个文章可能反复部署两三次。因此,需要对以上代码进行修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const fs = require('fs');		// 处理文件的模块
const path = require('path'); // 处理路径的模块

let blogWait = false;
fs.watch(path.resolve(__dirname, 'blog', 'source', '_posts'), (event, filename) => {
if (filename) {
if (blogWait) return;
blogWait = setTimeout(() => {
blogWait = false;
console.log(`Changes may happen on file ${filename}`);
// 发布文章的代码
}, 200);
}
});

我们可以加入一个信号量wait,默认为false,首次触发监听事件的时候设置一个延迟操作,在200ms后再次将wait信号量设置为false。同时把wait设置为setTimeout的返回值,由于返回值是一个大于0的数字,在判断时会被当做true来处理。整个操作实现的是:在首次监听到变化时,改变信号量,即防止后续监听到变化时运行核心代码,并在200ms后恢复正常。由于200ms是一个相对于手动操作来说比较短的时间,一般也不会出现负面效果。

发布文章

发布文章需要用的子进程模块,导入包之后可以直接运行Shell命令,比如运行ll命令:

1
2
3
4
5
6
7
8
9
10
const { exec } = require('child_process');

exec('ls -l', (error, stdout, stderr) => {
if (error) {
console.error(error);
return;
}
console.log(stdout);
console.error(stderr);
});

其中error是报错,stdoutstderr正常输出和错误输出,自动发布工具不涉及,所以不展开。

最后把部署文章的代码加到监听的代码中,直接来看总效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const fs = require('fs');
const path = require('path');
const { exec } = require('child_process');

let blogWait = false;
fs.watch(path.resolve(__dirname, 'blog', 'source', '_posts'), (event, filename) => {
if (filename) {
if (blogWait) return;
blogWait = setTimeout(() => {
blogWait = false;
console.log(`${filename} file Changed`);
exec('hexo d -g', {
cwd: path.resolve(__dirname, 'blog'), // 切换工作目录
}, (err) => {
if(err) {
console.log('Cannot deploy Hexo');
} else {
console.log('Blog Deployed');
}
})
}, 200);
}
});

exec接受的第二个参数是选项,由于Hexo需要在博客根目录下运行部署命令,因此添加一个cwd选项切换工作目录。不直接把部署工具放在博客根目录下是因为本站是双语站,需要监听两个目录。如果你要需要部署双语站,可以参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const fs = require('fs');
const path = require('path');
const { exec } = require('child_process');

let blogWait = false;
fs.watch(path.resolve(__dirname, 'blog', 'source', '_posts'), (event, filename) => {
if (filename) {
if (blogWait) return;
blogWait = setTimeout(() => {
blogWait = false;
console.log(`${filename} file Changed`);
exec('hexo d -g', {
cwd: path.resolve(__dirname, 'blog'), // 切换工作目录
}, (err) => {
if(err) {
console.log('Cannot deploy Hexo');
} else {
console.log('Blog Deployed');
}
})
}, 200);
}
});

let zhBlogWait = false;
fs.watch(path.resolve(__dirname, 'zhblog', 'source', '_posts'), (event, filename) => {
if (filename) {
if (zhBlogWait) return;
zhBlogWait = setTimeout(() => {
zhBlogWait = false;
console.log(`${filename} file Changed`);
exec('hexo d -g', {
cwd: path.resolve(__dirname, 'zhblog'), // 切换工作目录
}, (err) => {
if(err) {
console.log('Cannot deploy Hexo');
} else {
console.log('ZhBlog Deployed');
}
})
}, 200);
}
});

评论