一、 初始化一个npm项目

手动创建 package.json,自行键入"name、version、description、main"等字段。

npm init自动生成package.json。

这里介绍下main字段的作用,看到很多文章都说“指定主入口文件”,却很少看到有详细说明的。

学过c语言的应该了解,程序的执行有个main函数,这个main函数是程序的唯一执行入口,而package.json中的main含义类似,当这个项目作为npm包给被他人安装使用时,他人引用你的模块全部从main指引的模块导出,这相对于他人即为主入口,但相对于自己项目即为主出口。

二、模块查找

在node中以require(m)的方式引入模块,那么这种引入方式是如何查找模块的呢?

如果 m 是node的核心模块名,直接返回核心模块。

如果 m 以/ ./ ../方式开头的:
计算 m 的绝对路径 p;
如果 p 是文件,尝试以该文件的类型加载,成功则返回;
如果 p.js 是文件,当作 CommonJS 脚本加载,成功则返回;
如果 p.json 是文件,当作 json 文件加载,成功则返回;
如果 p.node 是文件,当作原生扩展模块加载,成功则返回;
以上都不成功,把 p 当作目录:
如果 p/package.json 是文件,找里面的 main 字段;
如果没有 main 字段,尝试加载 p/index.js p/index.json p/index.node;
如果有 main 字段;
计算 main 绝对路径 q,按上面的规则当作文件尝试加载
如果都失败,尝试 q/index.js q/index.json q/index.node
如果 q/package.json 不是文件,尝试加载 q/index.js q/index.json q/index.node
以上都不成功,throw not found

三、如何运行自定义命令

如何运行自定义命令,而不是使用node xxx,这时候就需要用到第一小节package.json中的bin字段了。

在node项目中,可以把Js文件当做shell脚本来执行,只需要在文件开头标明#! /usr/bin/env node表示使用nodejs解析。

下面来实现一个用自定义命令输出参数的小案例:

  1. 新建/lib/echo.js,
1
2
3
4
// echo.js
module.exports = function (message) {
return message;
};
  1. 新建/bin/node-echo.js,
1
2
3
4
5
6
#! /usr/bin/env node

const argv = require('argv');
const echo = require('../lib/echo');

console.log(echo(argv.run().targets.join(' ')));
  1. 配置bin命令
1
2
3
4
5
6
// package.json
// ...
"bin": {
"node-echo": "./bin/node-echo.js",
},
// ...

此时在终端运行node-echo命令是无效的,还需要通过npm link进行本地开发连接,当然,也可以在link后面指定模块名称,这时候指定的模块就会引入到当前项目的node_modules下面。

四、如何进行命令行交互

这里需要使用到相关工具包:命令行工具commander、交互工具inquirer、参数工具argv,借助这三个工具,我们可以实现输出项目版本信息,获取命令help提示,交互选择的功能。

  1. 编辑node-echo.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
#! /usr/bin/env node
// 命令行工具
const program = require('commander');
// 命令交互工具
const inquirer = require('inquirer');
const TEMPLATE_ENUM = require('../lib/template/templateEnum.json');
const { version } = require('../package.json');
// 输出版本信息和help提示
program.version(version, '-v, --version')
.usage('<command> [options]')

program.command('create')
.description('创建一个项目模板')
.action(async (cmd) => {
const { template } = await inquirer.prompt([
{
type: 'list',
name: 'template',
message: '选择要使用的模板',
choices: Object.keys(TEMPLATE_ENUM)
}
])
console.log('你选择了:',template)
})
// 把命令行参数传给commander解析
program.parse(process.argv);
  1. 模板枚举可以根据需要自行定制json格式,
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// templateEnum.json
{
"pc": {
"download": "xxxxxx",
"gitUrl": "xxxxxx",
"repoName": "template_PC"
},
"nativeApp": {
"download": ""
},
"miniApp": {
"download": ""
}
}

五、拉取远程仓库代码到本地

这节拉取远程代码,涉及比较多的边界处理,仅列举部分伪代码以做说明。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// createTemplate.js
async create(props) {
// 项目name、创建路径和git仓库一些信息
const { name, mkdir, gitRepo } = props;

try {
console.log(`⠋`, `下载模板中, 请稍候...`);
// 返回临时数据目录
tmpdir = await fetchRemotePreset(gitRepo['url']);
} catch (e) {
console.error(` 下载失败`);
throw e;
}
// 将接收的模板文件 传入到目标目录中
try {
fs.copySync(tmpdir, context, {
filter: (src, dest) => {
return path.basename(src, '.git') !== gitRepo.repoName
}
});
} catch (error) {
return console.error(`Error: ${error}`);
}
}

在正式执远程下载前还有很多校验处理,比如: node版本是否符合要求,项目名称是否符合命名要求,当前创建目录是否为空等等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// loadRemoteProject.js
const fs = require('fs-extra');

module.exports = async function fetchRemotePreset(name, clone = false) {
const os = require('os');
const path = require('path');
const download = require('./gitDownloadRepo');
// 生成临时目录, 方便后续中间件对其抓取下来的模板进行处理
const tmpdir = path.resolve(os.tmpdir(), 'node-echo');

// 将临时目录的内容先清空, 否则无法写入正常的拉取后的文件
await fs.remove(tmpdir);

return new Promise((resolve, reject) => {
download(name, tmpdir, { clone }, err => {
if (err) {
return reject(err);
}
return resolve(tmpdir);
});
});
};

借助一些工具包,对远程项目下载,由于安全机制问题,先下载到对应的系统临时文件下,再通过fs模块复制到对应的项目目录中去。

此外,还需要一些终端字符输出美化,终端清空,版本升级提示等,才能让这个脚手架工具更加完美。

详细代码参考:https://github.com/joydezhong/simpleCli-template.git