View on GitHub

Yinjie - GitHub.io

Welcome to the Yinjie's notes

通过 respawn 加强需要即时生效的开发环境构建系统

便利的开发环境是一个开发团队非常重要的基础建设之一,好的构建环境能大幅的提高业务开发人员的效率,顺畅的开发过程也能维持一个好心情。

其中构建环境的持续响应程度是良好构建系统一个要点,我们不仅需要构建系统的功能完善,也需要它能及时的将结果呈现。

一般的面向浏览器的静态资源文件构建不会存在这个方面的问题,构建生成新的结果文件只是坐等被引用加载而已。

但是如果是面向 server 的 nodejs 项目构建,你就得考虑在构建完成时,新文件能应用在已经启动的服务中。 这就是本文要优化的问题。

child_process 的 exec/spawn

nodejs 原生提供的 api 中,有 exec/spawn 两种语法,都是启动子进程相关。不同的是,spawn 可以支持挂起的服务性应用,比如一个服务器,我们可以通过此 api,在构建结束后,将服务重新启动,这也是笔者之前用的方法。

不过存在几个不方便的问题:

  • 停止进程,原生没有提供进程停止相关的方法,只能是通过系统杀死进程的命令,把 pid 给 kill 掉;
  • 多次启动,如要我们的场景需要在服务启动失败后再次尝试多次,用 child_process 就比较麻烦了,在 onerror 和启动之前要求一段比较高质量的代码

除以上两点外,其它的一些小不便就不再说了,虽然都可以用代码解决,但在之前笔者完成后发现这一部分写得脚本真有不少了,实际我只需要一个友好的启动及监控方法而已。

respawn

respawn 就是一个 spawn 的封装方法工具,除原生 spawn 的功能之外,自身还提供了几个非常好用的常规方法。

  • monitor.start() Starts the monitor

  • monitor.stop(cb) Stops the monitor (kills the process if its running with SIGTERM)

  • monitor.status Get the current monitor status. Available values are running, stopping, stopped, crashed and sleeping

通过这几个“简单”的方法,我们再来实现上述提到的需求就非常便捷了。

代码示例

下面分享一个 gulp 构建环境的 watch 重启代码,用 respawn 来进行服务的及时重启。


/**
 * 主构建方法
 */
function transpile () {...}

/**
 * 启动服务
 */
function startServe(done) {
	console.log(gutil.colors.green('Start server ...'));
    // 服务启动命令
	const command = [ 'node', '--harmony' ];
	command.push('index.js');
    // 启动进程
	const monitor = respawn(command, {
		env,
		cwd: PATH_DIST,
		maxRestarts: 10,
		sleep: 300,
		stdio: 'inherit',
	});
	monitor
		.on('stdout', (data) => console.log(data.toString()))
		.on('stderr', (err) => console.error(err.toString()));
    // 重启
	function restartMonitor () {
		monitor.stop(() => monitor.start());
	}
	monitor.start();

    /**
     * watch 子方法
     * 文件改动后
     */
	function watchHandle (evt) {
		if (evt.endsWith('.html')) {
			moveTpl()
				.resume()
				.on('end', () => {
                    restartMonitor();
				});
		}
		else {
			let isLintError = false;
			lint()
				.resume()
				.on('error', () => {
					isLintError = true;
				})
				.on('end', () => {
					if (isLintError) {
                        // 未通过 lint,不进行启动
						return;
					}
					transpile().on('end', restartMonitor);
				});
		}
	}

	/**
	 * 开始监听任务
	 */
	const watchIns = gulp.watch([ PATH_JS, PATH_TPL, '../dist' ], (watchDone) => {
		gutil.log(`Watch project is doing ...`);
		watchDone();
	});
	watchIns.on('change', (evt) => {
		gutil.log(`File change : ${evt}`);
		// 如果是 server 端的变化,重启 server
		if (evt.indexOf('src/') === 0) {
			watchHandle(evt);
		}
	});
	watchIns.on('add', (evt) => {
		gutil.log(`File add : ${evt}`);
		watchHandle(evt);
	});
	watchIns.on('unlink', (evt) => {
		gutil.log(`File delete : ${evt}`);
		watchHandle(evt);
	});
	done();
}

/**
 * 默认任务
 * @module gulp#4.0
 */
gulp.task('default', gulp.series(
	// 第一步:clean + lint
	gulp.parallel(clean, lint),
	// 第二步:编译 + 移动模板
	gulp.parallel(transpile, moveTpl),
	// 第三步:启动服务
	startServe
));