编写一个自动推送 Git Page 的 nodejs 脚本

背景

写了一个简单的个人主页,由于使用了一些 CSS3 的属性,考虑到浏览器兼容性问题,就使用了 webpack 的一些 loader 给这些属性加前缀。所以简单的静态单页面用了 webpack,又因为这个页面要放在 Github Pages 上,每次打包后还得手动切换分支推送到 Github 上,干脆趁这个机会写一个自动推送的脚本。

该脚本主要参考了 hexo-deployer-git,可以查看该项目获取功能更完善的源码。

预备知识

脚本比较简单,主要使用 NodeJs child_process 的 spawn 方法调用子进程执行 git 语句。如果你对 NodeJs 不熟悉可能需要了解以下内容才能完全弄懂脚本中的每一行代码:(如果你只是想大致了解一下代码原理,可不必查看以下知识,本文将做简单的解释)

spawn

用法

NodeJs 的 child_process 模块提供了 spawn 方法,该方法将调用子进程,并执行传入的命令行及参数。该脚本主要利用该方法执行需要的 git 命令。下面是一个简单的 spawn 使用示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const { spawn } = require('child_process');
const ls = spawn('ls', ['-lh', '/usr']);

ls.stdout.on('data', (data) => {
console.log(`stdout: ${data}`);
});

ls.stderr.on('data', (data) => {
console.error(`stderr: ${data}`);
});

ls.on('close', (code) => {
console.log(`子进程退出,使用退出码 ${code}`);
});

实例中向 spawn 传入了 ls 命令,以及 -lh/usr 参数,并通过事件监听回调处理命令执行的结果。spawn 方法返回一个 ChildProcess实例,其中 stdoutstderr 是命令执行后的输出流和错误流。我们监听这两个流的事件来打印命令执行后的结果或错误信息。同时监听实例的 close 事件,打印命令执行后的退出码。退出码为 0 时代表命令正常执行完毕,非 0 时代表发生了异常。(如 debug 需要具体代码含义,可查看退出码)。

封装 spawn

如上所述,spawn 通过回调来处理响应,我们希望使用更方便易读的 Promise 来简单封装一下 spawn。(对于简单的代码来说,这一步不是必要的,但我还是参照 hexo 的做法封装了 spawn。)

spawn.js

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
'use strict'

const { spawn } = require('child_process')

function promiseSpawn(command, args, options) {
if (!command) throw new TypeError('command is reuqired');

if (!options && args && !Array.isArray(args)) {
options = args;
args = [];
}

// note-1 以上是对参数的容错处理

args = args || [];
options = options || {};

// note-2 接下来将其封装成一个 Promise 对象
return new Promise((resolve, reject) => {
const task = spawn(command, args, options);
const encoding = options.hasOwnProperty('encoding') ? options.encoding : 'utf8';

/*-- note-3 start --*/
if (task.stdout) {
task.stdout.pipe(process.stdout);
}

if (task.stderr) {
task.stderr.pipe(process.stdout)
}
/*
这里调用子进程输出流(stdout/stderr)的 pipe 方法,
将其输入到当前进程的输出流,这样就能在调用该脚本的
进程中看到子进程中执行的命令的输入了

如果你希望子进程的命令静默执行,这些代码同样不是必要的
或者可以传入一个参数来决定是否在主进程中展示这些信息
*/
/*-- note-3 end --*/

// note-4 监听命令结束事件,根据 code 来决定是否 resolve
task.on('close', code => {
if (code) {
const e = new Error('command execute failed');
e.code = code;

return reject(e);
}

resolve();
});


// note-5 命令执行错误时 reject
task.on('error', reject)

// note-6 处理 `exit` 事件
if (!task.stderr && !task.stdout) {
task.on('exit', code => {
if (code) {
const e = new Error('Spawn failed');
e.code = code;

return reject(e);
}
});
}
});
}

module.exports = promiseSpawn;

ChildProcess 的 closeerrorexit 事件的触发是有区别的,并且也不是简单的包含关系,所以代码中对其分别进行了处理,具体可查看官方文档

执行 git 命令

利用上面封装的 spawn 方法,可以很方便的执行 git 的语句。

deployer.js

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
'use strict'

const spawn = require('./lib/spawn'); // note-1 导入封装好的 spawn
const pathFn = require('path');
const fs = require('fs');

// note-2 设置好执行 git 命令时需要用到的参数,也可以单独分离出配置文件
const args = {
user: {
name: 'name',
email: '[email protected]',
},
// 需要 push 的目录,此处推送的是 webpack 默认的打包路径
baseDir: pathFn.resolve('./', 'dist'),
repo: {
url: '[email protected]:username/username.github.io.git',
branch: 'master',
},
};

deployToGit(args); // 执行推送函数

function deployToGit(args) {
const message = args.message || `Site updated: ${(new Date()).toDateString()}`
const baseDir = args.baseDir;
const gitDir = pathFn.join(baseDir, '.git');

if (!args.repo) {
return console.log('Please check configs of repository!')
}

// 检查要推送的目录是否存在
if (!fs.existsSync(baseDir)) {
throw new Error('Please build before deploy')
}
// 检查该目录下是否已经存在 git 仓库
if (!fs.existsSync(gitDir)) {
setup()
.then(() => push(args.repo));
} else {
push(args.repo);
}

function git(...args) {
return spawn('git', args, {
cwd: baseDir,
stdio: 'inherit'
});
}

// 初始化 git 仓库
function setup() {
const userName = args.user && args.user.name || '';
const userEmail = args.user && args.user.emial || '';

return git('init').
then(() => git('config', 'user.name', userName)).
then(() => git('config', 'user.email', userEmail)).
then(() => git('add', '-A')).
then(() => git('commit', '-m', message));
}

// 提交并推送指定目录
function push(repo) {
return git('add', '-A').
then(() => git('commit', '-m', message).catch(() => '')).
then(() => git('push', '-u', repo.url, 'HEAD:' + repo.branch, '--force'));
}
}

执行 git 命令的代码相对来说就比较简单了,但是要关注一些错误情况的检查,可能需要多 debug 几遍才能确保命令的执行达到预期的效果。

执行脚本

以上 spawn.jsdeployer.js 就是推送脚本的全部内容,最后把这些脚本胶乳 package.json 中的 script 中,就能在打包后,一键推送部署到 github pages 上了。

package.json

1
2
3
4
5
6
7
8
{
...
"scripts": {
...
"deploy": "node ./script/deploy"
},
...
}

在 shell 中执行 npm run deploy 即可执行脚本一键部署,下面是我执行的示,(显然我在这次部署前没有进行任何修改)

1
2
3
4
5
6
7
8
9
10
Hozen@HOZEN MINGW64 /c/Workspace/localstatic/lei (dev)
$ npm run deploy

> [email protected] deploy C:\Workspace\localstatic\lei
> node ./script/deploy

On branch master
nothing to commit, working tree clean
Everything up-to-date
Branch 'master' set up to track remote branch 'master' from '[email protected]:****/****.github.io.git'.

注意

由于不同操作系统和不同 shell 客户端中的命令行和格式可能会有不同,导致同一套代码可能不能在不同环境中执行,要解决这个问题可以使用 cross-spawn 工具替代 NodeJs 中的 spawn。