一、 初始化一个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解析。
下面来实现一个用自定义命令输出参数的小案例:
- 新建/lib/echo.js,
1 2 3 4
| module.exports = function (message) { return message; };
|
- 新建/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(' ')));
|
- 配置bin命令
1 2 3 4 5 6
|
"bin": { "node-echo": "./bin/node-echo.js", },
|
此时在终端运行node-echo命令是无效的,还需要通过npm link
进行本地开发连接,当然,也可以在link后面指定模块名称,这时候指定的模块就会引入到当前项目的node_modules下面。
四、如何进行命令行交互
这里需要使用到相关工具包:命令行工具commander、交互工具inquirer、参数工具argv,借助这三个工具,我们可以实现输出项目版本信息,获取命令help提示,交互选择的功能。
- 编辑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');
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) })
program.parse(process.argv);
|
- 模板枚举可以根据需要自行定制json格式,
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| { "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
| async create(props) { 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
| 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