2020年2月3日 · developer · 3分钟阅读

编写一个自动推送 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 使用示例:

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

'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

'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: 'name@email.com',
  },
  // 需要 push 的目录,此处推送的是 webpack 默认的打包路径
  baseDir: pathFn.resolve('./', 'dist'),
  repo: {
    url: 'git@github.com: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

{
  ...
  "scripts": {
    ...
    "deploy": "node ./script/deploy"
  },
  ...
}

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

Hozen@HOZEN MINGW64 /c/Workspace/localstatic/lei (dev)
$ npm run deploy

> lei@1.0.0 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 'git@github.com:****/****.github.io.git'.

注意

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

评论

登录 — 登录后参与讨论。