記一次nodejs開發CLI的過程

STMU同學發表於2019-03-04

大家新年好!
年前在工作中接到任務要開發一個自己的CLI,便去了解了一下。發現並不難,只需運用nodejs的相關api即可。

目前實現的功能為:

  1. 輸入 new 命令從github下載一個腳手架模版,然後建立對應的app。
  2. 輸入 create 命令可以快速的建立一些樣板檔案。

下面將分步去解析一個CLI的製作過程,也算是一次記錄回憶的過程。

1.建立CLI專案

用 npm init 生成專案的package.json檔案。然後編輯該檔案主要加上

"bin": {
    "jsm": "./bin/jsm.js"
  },
複製程式碼

然後在當前目錄建立你自己的指令碼檔案,對應上述配置為 mkdir bin && touch bin/jsm.js
編輯建立好的檔案,加上

#!/usr/bin/env node
console.log(`Hello CLI`)
複製程式碼

接下來在專案的根目錄執行一下 npm i -g, 現在就可以在命令列使用jsm命令了。
注意: 一定要在開頭加上#!/usr/bin/env node, 否則無法執行。

詳解:package.json檔案只有加上了bin欄位,才能在控制檯使用你的命令,對應的這裡的命令就是jsm,對應的執行檔案為bin/jsm.js。 其實"jsm"命令就是 "node bin/jsm.js" 的別稱,只有你用npm i -g全域性安裝後才可以用,開發的過程中直接用node bin/jsm.js即可。

2.解析命令引數

一個CLI需要通過命令列輸入各種引數,可以直接用nodejs的process相關api進行解析,但是更推薦使用commander這個npm包可以大大簡化解析的過程。
npm i commander安裝, 然後更改之前的指令碼檔案新增

const program = require(`commander`);

 program
  .command(`create <type> [name] [otherParams...]`)
  .alias(`c`)
  .description(`Generates new code`)
  .action(function (type, name, otherParams) {
    console.log(`type`, type);
    console.log(`name`, name);
    console.log(`other`, otherParams);
    // 在這裡執行具體的操作
  });

program.parse(process.argv);
複製程式碼

現在在終端執行一下 node bin/jsm.js c component myComponent state=1 title=HelloCLI 應該就能看到輸入的各種資訊資訊了。至此命令解析部分就基本ok了,其他更多的用法可以參考官方例子

詳解:command第一個引數為命令名稱,alias為命令的別稱, 其中<>包裹的為必選引數 []為選填引數 帶有...的引數為剩餘引數的集合。

3.下載模版建立檔案

接下來需要根據上一步輸入的命令去做一些事情。具體到一個腳手架CLI一般主要做兩件事,快速的生成一個新專案和快速的建立對應的樣板檔案。
既然需要建立檔案就少不了對nodejs的fs模組的運用,這裡用的一個增強版的fs-extra

下面封裝兩個常用的檔案處理函式

//寫入檔案
function write(path, str) {
  fs.writeFileSync(path, str);
}
//拷貝檔案
function copyTemplate(from, to) {
  from = path.join(__dirname, from);
  write(to, fs.readFileSync(from, `utf-8`));
}
複製程式碼

3.1 生成一個新專案

命令如下

program
  .command(`new [name]`)
  .alias(`n`)
  .description(`Creates a new project`)
  .action(function (name) {
    const projectName = name || `myApp`;
    init({ app: projectName })
  });
複製程式碼

init函式主要做了兩件事:

  • 從github下載一個腳手架模版。(如用本地的腳手架模版可省略此步驟)
  • 拷貝腳手架檔案到命令指定的目錄並安裝相應的依賴包。
const fs = require(`fs-extra`);
const chalk = require(`chalk`);
const {basename, join} = require(`path`);
const readline = require(`readline`);
const download = require(`download-git-repo`);
const ora = require(`ora`);
const vfs = require(`vinyl-fs`);
const map = require(`map-stream`);
const template = `stmu1320/Jsm-boilerplate`;

// 建立函式
function createProject(dest) {
  const spinner = ora(`downloading template`)
  spinner.start()
  if (fs.existsSync(boilerplatePath)) fs.emptyDirSync(boilerplatePath)
  download(template, `boilerplate`, function (err) {
    spinner.stop()
    if (err) {
      console.log(err)
      process.exit()
    }

    fs
    .ensureDir(dest)
    .then(() => {
      vfs
        .src([`**/*`, `!node_modules/**/*`], {
          cwd: boilerplatePath,
          cwdbase: true,
          dot: true,
        })
        .pipe(map(copyLog))
        .pipe(vfs.dest(dest))
        .on(`end`, function() {
          const app = basename(dest);
          const configPath = `${dest}/config.json`;
          const configFile = JSON.parse(fs.readFileSync(configPath, `utf-8`));
          configFile.dist = `../build/${app}`;
          configFile.title = app;
          configFile.description = `${app}-project`;
          write(configPath, JSON.stringify(configFile, null, 2));
          // 這一部分執行依賴包安裝,具體程式碼請檢視文末連結
          message.info(`run install packages`);
          require(`./install`)({
            success: initComplete.bind(null, app),
            cwd: dest,
          });
        })
        .resume();
    })
    .catch(err => {
      console.log(err);
      process.exit();
    });
})
}

function init({app}) {
  const dest = process.cwd();
  const appDir = join(dest, `./${app}`);
  createProject(appDir);
}
複製程式碼

3.2 快速生成樣板檔案

生成樣板檔案這一部分,其實就是拷貝一個檔案到指定的地方而已,當然還應該根據引數改變檔案的具體內容。

program
  .command(`create <type> [name] [otherParams...]`)
  .alias(`c`)
  .description(`Generates new code`)
  .action(function (type, name, otherParams) {
    const acceptList = [`component`, `route`]
    if (!acceptList.find(item => item === type)) {
      message.light(`create type must one of [component | route]`)
      process.exit()
    }
    const params = paramsToObj(otherParams)
    params.name = name || `example`
    generate({
      type,
      params
    })
  });

//生成檔案入口函式
function generate({type, params}) {
  const pkgPath = findPkgPath(process.cwd())
  if (!pkgPath) {
    message.error(`No `package.json` file was found for the project.`)
    process.exit()
  }
  const dist = path.join(pkgPath, `./src/${type}s`);
  fs
    .ensureDir(dist)
    .then(() => {
      switch (type) {
        case `component`:
          // 具體程式碼請檢視文末連結
          createComponent(dist, params);
          break;

        case `route`:
          createRoute(dist, params);
          break;

        default:
          break;
      }
    })
    .catch(err => {
      console.log(err);
      process.exit(1);
    });
}
複製程式碼

到這裡一個基本的腳手架CLI就差不多了,剩下的是幫助資訊等友好提示的東西了。文章的所有原始碼點選這裡
也歡迎大家安裝試用一下 npm i -g jsm-cli

相關文章