从构建自定义 cli(脚手架) 到 npm 包的发布使用

3,477 阅读5分钟

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

  1. www.npmjs.com 注册一个账号
  2. 进入你的项目根目录,运行 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>

扩展阅读

最后

  • 多多点赞会变好看
  • 多多留言会变有钱~