1. 什么是 CLI
CLI 是 command line interface 的简称,也就是命令行界面。用户可在提示符下键入可执行指令,然后计算机执行,它通常不支持鼠标。前端开发中也会用到很多 CLI,最典型的就是 react 对应的 create-react-app,vue 对应的 vue-cli。以 vue-cli 为例,我们是这么创建 vue 项目的
vue create my-app
然后再命令行中回答一些项目相关的问题,最终根据问题的答案来生成相应的项目文件。所以,我们要构建自己的 CLI,就是自定义这些操作,来帮助完成一些工作,比如生成项目,代码检查。
2. 相关概念
2.1 命令
命令就是操作的名称,比如上面的create就是命令,代表创建项目的操作。我们可以执行vue --help来看有哪些命令:
Usage: vue <command> [options]
Options:
-V, --version output the version number
-h, --help output usage information
Commands:
create [options] <app-name> create a new project powered by vue-cli-service
add [options] <plugin> [pluginOptions] install a plugin and invoke its generator in an already created project
invoke [options] <plugin> [pluginOptions] invoke the generator of a plugin in an already created project
inspect [options] [paths...] inspect the webpack config in a project with vue-cli-service
serve [options] [entry] serve a .js or .vue file in development mode with zero config
build [options] [entry] build a .js or .vue file in production mode with zero config
ui [options] start and open the vue-cli ui
init [options] <template> <app-name> generate a project from a remote template (legacy API, requires @vue/cli-init)
config [options] [value] inspect and modify the config
outdated [options] (experimental) check for outdated vue cli service / plugins
upgrade [options] [plugin-name] (experimental) upgrade vue cli service / plugins
migrate [options] [plugin-name] (experimental) run migrator for an already-installed cli plugin
info print debugging information about your environment
Run vue <command> --help for detailed usage of given command.
2.1 选项
选项就是命令的配置,比如执行vue create --help,可以看到:
Usage: create [options] <app-name>
create a new project powered by vue-cli-service
Options:
-p, --preset <presetName> Skip prompts and use saved or remote preset
-d, --default Skip prompts and use default preset
-i, --inlinePreset <json> Skip prompts and use inline JSON string as preset
-m, --packageManager <command> Use specified npm client when installing dependencies
-r, --registry <url> Use specified npm registry when installing dependencies (only for npm)
-g, --git [message] Force git initialization with initial commit message
-n, --no-git Skip git initialization
-f, --force Overwrite target directory if it exists
--merge Merge target directory if it exists
-c, --clone Use git clone when fetching remote preset
-x, --proxy <proxyUrl> Use specified proxy when creating project
-b, --bare Scaffold project without beginner instructions
--skipGetStarted Skip displaying "Get started" instructions
-h, --help output usage information
- 其中以-或--开头就是配置选项的名称
- 参数
- 传递给命令的值,就好比命令就是函数,该值就是函数执行时的实参。
- vue create my-app中my-app就是参数,表示项目名称
3. 开始
3.1 初始化项目
首先,新建一个空白的文件夹,初始化项目
$ npm init
3.2 写JavaScript 脚本
写一个 JavaScript 脚本index.js:
#!/usr/bin/env node
console.log('hello cli');
#!/usr/bin/env node ,简单的理解,就是输入命令后,会有在一个新建的 shell 中执行指定的脚本,在执行这个脚本的时候,我们需要来指定这个脚本的解释程序是 node。
3.3 修改package.json
{
"bin": {
"youzan": "./index.js"
}
}
3.4 npm link
npm link用来在本地项目和本地npm模块之间建立连接,可以在本地进行模块测试
在执行npm link,然后就能执行youzan命令了
4. 配置命令行选项
这里推荐使用commander.js
$ yarn add commander --save
program.option('-ig,--initgit', 'init git');
console.log('Options: ', program.opts()); // 可以得到选项值
4.1 第一个命令
就像vue create一样,我们要完成一个创建项目的命令,并且可以配置模版,以及是否初始化 git
const { program } = require('commander');
const handleCreate = (params, options) => {
console.log(params, options);
};
program
.command('create <name> [destination]')
.description('create a project')
.action((name, destination) => {
handleCreate({ name, destination }, program.opts());
});
program.option('-ig,--initgit', 'init git');
program.parse(process.argv);
-
.command()用于配置命令及参数,其中<>表示参数是必须的,[]表示参数是可选的; -
.description()添加命令描述 -
.action()用于添加操作函数,入参就是配置命令时候的参数 -
program.parse(process.argv);处理命令行参数
5. 用户交互问题
这里用到Inquirer.js,点击查看文档 github.com/SBoudrias/I… 用法可以参考这篇文章 blog.csdn.net/qq_26733915…
$ yarn add inquirer --save
const handleCreate = (params, options) => {
console.log(params, options);
inquirer
// 用户交互
.prompt([
{
type: 'input',
name: 'author',
message: 'author name?'
},
{
type: 'list',
name: 'template',
message: 'choose a template',
choices: ['tpl-1', 'tpl-2']
}
])
.then((answers) => {
//根据回答以及选项,参数来生成项目文件
genFiles({ ...answers, ...params, ...options });
})
.catch((error) => {
console.error(error);
});
};
6. 按需生成项目文件
在项目中创建templates目录用于存放模版文件
+-- templates
| +-- tpl-1
| +-- package.json
| +-- tpl-2
| +-- package.json
然后,就是复制文件到指定目录.
这里用到Metalsmith,可以很方便地复制文件到指定目录,指定目录若不存在,则创建新目录
//获得命令运行时的路径
const getCwd = () => process.cwd();
const genFiles = (options) => {
//模版的目录
const templateSrc = path.resolve(__dirname, `./templates/${options.template}`);
//项目指定生成目录,如果命令中没有有配置目录,则在当前命令运行的目录下生成以项目名称为名字的新目录
const destination = options.destination
? path.resolve(options.destination)
: path.resolve(getCwd(), options.name);
Metalsmith(__dirname)
.source(templateSrc)
.destination(destination)
.build((err) => {
if (err) {
console.error(err);
}
});
};
.source()和.destination()分别配置复制源目录和目标目录,最好使用绝对路径
7. 动态渲染目标文件
生成的package.json中的name,author等值是固定的,应当是随着项目名称而变化才对。 所以package.json必须是一个模版文件,在生成的同时要根据实际情况渲染成目标文件;
7.1 模版采用ejs
yarn add ejs --save
7.2 修改模版文件
在.destination()和.build()之间加入处理程序
const ejs = require('ejs');
// 需要动态生成的文件
const renderPathList = [ 'package.json', // 'src/main.js',]
Metalsmith(__dirname)
.source(templateSrc)
.destination(destination)
.use((files) => {
Object.keys(files).forEach((key) => {
// 指定的文件动态生成
if (renderPathList.includes(key)) {
const file = files[key];
// 原内容
const str = file.contents.toString();
// 新内容
const newContents = ejs.render(str, options);
// 将新内容写到文件中
file.contents = Buffer.from(newContents);
}
});
})
.build((err) => {
if (err) {
console.error(err);
}
});
这样,一个简单 CLI 完成,更多命令可以自己添加。
8.命令行等待优化
遇到命令执行比较耗时的情况,友好地提示等待也是必须的,ora可以帮助你
const genFiles = ()=>{
// todo 上面生成文件的操作...
}
const ora = require('ora')
const processGenFiles = ora('Create project……')
processGenFiles.start() // 进度条开始
await genFiles(answers);
processGenFiles.succeed(`Create project complete: i18n-b-${name}`)
9. git模版下载
大部分 CLI 工具的模版并不在本地,而是从网上下载。可以用download-git-repo这个库,以及github API
const { promisify } = require('util')
const clone = async function (repo, desc) {
const download = promisify(require('download-git-repo')) // download-git-repo: Download and extract a git repository (GitHub, GitLab, Bitbucket)
const ora = require('ora')
const process = ora(`下载......${repo}`)
process.start() // 进度条开始
await download(repo, desc)
// download-git-repo导出的download方法,第一个参数repo是仓库地址,格式有三种:
// GitHub - github:owner/name or simply owner/name
// GitLab - gitlab:owner/name
// Bitbucket - bitbucket:owner/name
process.succeed()
}
await clone('git@gitlab.qima-inc.com:sz-web/i18n-b-dashboard.git', name)
10. 在node.js中执行shell(npm install 为例)
在node.js中执行shell一般用child_process 的spawn,实现从主进程的输出流连通到子进程的输出流
const spawn = async (...args) => {
const { spawn } = require('child_process')
return new Promise(resolve => {
const proc = spawn(...args) // 在node.js中执行shell一般用spawn,实现从主进程的输出流连通到子进程的输出流
proc.stdout.pipe(process.stdout) // 子进程正常流搭到主进程的正常流
proc.stderr.pipe(process.stderr) // 子进程错误流插到主进程的错误流
proc.on('close', () => {
resolve()
})
})
}
await spawn('npm', ['install'], { cwd: `./` }) // cwd 执行命令的目录
11. 包的发布
11.1 npm init
上面执行过就不用再执行了
11.2 npm login
- www.npmjs.com 注册一个账号
- 进入你的项目根目录,运行 npm login
- 会输入你的用户名、密码和邮箱
11.2 npm publish
登录成功后,执行 npm publish,就发布成功啦,我们可以在官网看到
12.使用自己的npm包
举例
- 全局安装
npm install -g <包的名称>
# or
yarn global add <包的名称>
- 命令
youzan --version
youzan --help
youzan init <project-name>
扩展阅读
- 构建自己的 CLI zhuanlan.zhihu.com/p/242656395
最后
- 多多点赞会变好看
- 多多留言会变有钱~