前端工程02 创建自己的脚手架工具

前一阵子用Egg做了一个Node服务端项目的重构,发现Egg相比于Koa和Express来说,感觉更适合于现阶段团队的Node开发,因为现在团队Node的代码根本没有任何规范和约定,乱的一坨,所以引入了Node,遵循它的规范,使用它预置的功能,对代码的可维护性提升还是比较大的。

开发完成后,在Egg的基础上增加了一点中间件,配置了一些插件和代码风格控制的配置,将这个模板抽离出来作为团队以后开发Node服务的基准模板,放在了Gitlab上。

原来的时候方法都是直接Clone这个项目,再去更改Git的相关信息等等,不是很方便。所以想试着做一个类似于简易的Vue-Cli这样的工具,能够直接拉取模板,并且直接生成对应的目录,配置一些简单的信息。

因为要实现的脚手架工具比较简单,没有那么多的配置,实现起来还是比较简单的。下面分几个部分记录自己学习的过程。

前言

前一阵子用Egg做了一个Node服务端项目的重构,发现Egg相比于Koa和Express来说,感觉更适合于现阶段团队的Node开发,因为现在团队Node的代码根本没有任何规范和约定,乱的一坨,所以引入了Node,遵循它的规范,使用它预置的功能,对代码的可维护性提升还是比较大的。

开发完成后,在Egg的基础上增加了一点中间件,配置了一些插件和代码风格控制的配置,将这个模板抽离出来作为团队以后开发Node服务的基准模板,放在了Gitlab上。

原来的时候方法都是直接Clone这个项目,再去更改Git的相关信息等等,不是很方便。所以想试着做一个类似于简易的Vue-Cli这样的工具,能够直接拉取模板,并且直接生成对应的目录,配置一些简单的信息。

因为要实现的脚手架工具比较简单,没有那么多的配置,实现起来还是比较简单的。下面分几个部分记录自己学习的过程。

控制台交互

使用Cli工具,必定要在控制台输入、输出一些相关的信息,可以使用Commander这个包,它可以接受并解析在控制台输入的参数process.argv,它基本的使用方法如下:

1
2
3
program.version('0.1.0')
.option('-i, init [name]', '初始化Egg项目')
.parse(process.argv);

通过上面的配置,我们就可以通过program.init拿到用户在控制台输入init a时跟在init后面的输入a,我们将a作为模板的目录名

然后在开始拉取模板之前,我们需要有一个Loading的提示,会更加友好,可以使用ora这个包,它用来在终端展示loading的图标。我们这里使用的也很简单,开始时展示图标和提示语:

1
const spinner = ora('正在拉取Create-Egg-App模板...').start();

拉取成功或者失败后展示对应的图标和文字:

1
2
3
4
5
// 成功
spinner.succeed('拉取成功');

// 失败
spinner.fail('拉取失败');

提示的文字我们还希望能带一些颜色,比如成功我们希望是绿色,失败时红色,这样会更加美观,我们就可以使用chalk这个包,它可以用来在终端中展示各种颜色:

1
2
3
4
5
6
7
const chalk = require('chalk');

const success = chalk.blueBright;
const error = chalk.bold.red;

spinner.succeed(success('拉取成功'));
console.error(error('请在init后输入目录名'));

准备工作都就绪了,就差最关键的一步了,就是拉取模板了。

拉取模板

拉取模板我们也可以借助第三方的包来实现,要使用的就是download-git-repo这个包了,它可以帮助我们模板代码从各种仓库中拉取下来,它既可以通过SSH协议拉取Github的代码:

1
2
github:owner/name 
owner/name

也可以直接通过HTTPS拉取代码:

1
direct:url

它接受四个参数:

1
dowanload(url, pathName, opts, callback)

详细的配置还是参考它的文档吧。整个拉取代码的逻辑就基本完成了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (program.init && typeof program.init === "string") {
const spinner = ora('正在拉取Create-Egg-App模板...').start();
download(templateUrl, program.init, { clone: true }, function (err) {
if (!err) {
spinner.succeed(success('拉取成功'));
// 更改 package.json 中的 name 和版本号
changePackage()
} else {
spinner.fail('拉取成功');
}
});
} else {
console.error(error('请在init后输入目录名'));
}

优化

还有一点优化在changePackage这个函数中完成了,这个函数会将模板中的package.jsonnameversion,这个功能使用了原生的Node来实现

1
2
3
4
5
6
7
8
9
10
11
12
const changePackage = () => {
fs.readFile(`${process.cwd()}/${program.init}/package.json`, (err, data) => {
if (err) throw err;
let _data = JSON.parse(data.toString());
_data.name = program.init;
_data.version = '1.0.0';
let str = JSON.stringify(_data, null, 4);
fs.writeFile(`${process.cwd()}/${program.init}/package.json`, str, function (err) {
if (err) throw err;
})
});
};

这段代码基本是Copy第一篇参考文章的,自己的Node并不灵,把这个段代码学习一下。

首先通过fs.readFile来读取package.json文件,查找文件路径时使用了process.cwd,它表示当前执行Node命令时的文件夹路径名

Node中海油几个获取路径的方法,可以来看一下:

1
2
3
4
__dirname:    获得当前执行文件所在目录的完整目录名
__filename: 获得当前执行文件的带有完整绝对路径的文件名
process.cwd():获得当前执行node命令时候的文件夹目录名
./: 不使用require时候与process.cwd()一样,使用require时候与__dirname一样

我们这里使用的是process.cwd(),就是在执行当前的Node命令时的目录下,因为通过program.init新建的目录是在执行Node命令的目录下

因为package.json文件时JSON格式的,所以可以首先通过data.toString将二进制文件转换为字符串,然后通过JSON.parse()进行解析。

解析完成后修改相关字段,修改完成后再通过JSON.stringify转换为字符串,它额外接受了两个参数,第二个参数用来在序列化过程中替换序列化的内容,第三个参数用来指定缩进空白字符串。

序列化完成后,再将内容通过writeFile写会原来的package.json文件。

使用

使用时为了避免全局安装,使用npx来拉取模板是很好的解决防范。具体使用时分为两种情况,一种是模板在内网的Gitlab上,一种是模板在公网的Github上。

先来看在Github上,模板的地址是https://github.com/duola8789/create-egg-app-zh,我们先在NPM上注册账户,并通过npm login登陆,然后将egg-cli-zh发布到NPM上。

1
NPM publish

发布成功后,就可以直接使用npx拉取模板了:

1
npx egg-cli-zh init [project_name]

但是如果模板在内网的Gitlab上,egg-cli-zh也不能发布到NPM上,那就使用ssh+git来使用即可:

1
npx git+ssh://git@gitlab/xxx/template.git init [project_name]

至此,一个简易的脚手架工具就完成了。Egg-Cli-zh脚手架的源代码在这里,Create-Egg-App-zh的源码在这里。

参考