如何快速開發 CLI,Oclif 了解一下
- 2020 年 2 月 18 日
- 筆記
一、CLI 簡介
CLI(Command Line Interface)命令行介面是在圖形用戶介面得到普及之前使用最為廣泛的用戶介面,它通常不支援滑鼠,用戶通過鍵盤輸入指令,電腦接收到指令後,予以執行。也有人稱之為字元用戶介面(character user interface,CUI)。
為了便於大家的理解,我們來舉一個實際的例子,比如 Angular 開發者都熟悉的 Angular CLI:

(圖片來源 —— https://cli.angular.io/)
除了 Angular CLI 之外,一些主流的框架也有提供相應的 CLI,比如 Vue CLI 和 Ionic CLI 等。在日常工作中,為了提高開發效率或統一開發方式,我們通常會開發團隊內專屬的 CLI 工具。那麼如何開發 CLI 工具呢,對於前端開發者來說,我們可以基於 Node.js 來開發,因為目前 NPM 上已經有很多成熟的第三方庫,如 chalk、Inquirer.js、commander.js 和 configstore 等。基於這些成熟的第三方庫,我們就可以方便、快捷地開發 Node.js CLI 工具。
二、Oclif 簡介
This is a framework for building CLIs in Node.js. This framework was built out of the Heroku CLI but generalized to build any custom CLI. It』s designed both for single-file CLIs with a few flag options, or for very complex CLIs that have subcommands (like git or heroku).
Oclif 是由 Heroku(一個支援多種程式語言的雲應用平台,在 2010 年被 Salesforce.com 收購)開發的 Node.js Open CLI 開發框架,它可以用來開發 single-command CLI 或 multi-command CLI,同時還提供了可擴展的插件機制和鉤子機制。
2.1 CLI 類型
使用 Oclif 你可以創建兩種不同類型的 CLI,即 Single CLIs 和 Multi CLIs。Single CLIs 類似於 Linux 或 MacOS 平台中常見的 ls 或 cat 命令。而 Multi CLIs 類似於前面提到的 Angular CLI 或 Vue CLI,它們包含子命令,這些子命令本身也是 Single CLI。在 package.json 文件中有一個 oclif.commands 欄位,該欄位指向一個目錄,該目錄包含了當前 CLI 的所有子命令。舉個例子,假設你擁有一個名為 mycli 的 CLI,該 CLI 含有 mycli create 和 mycli destroy 兩個子命令,那麼你將擁有一個與下面類似的項目結構:
package.json src/ └── commands/ ├── create.ts └── destroy.ts
2.2 用法
創建一個 single-command CLI:
$ npx oclif single mynewcli ? npm package name (mynewcli): mynewcli $ cd mynewcli $ ./bin/run hello world from ./src/index.js!
創建一個 multi-command CLI:
$ npx oclif multi mynewcli ? npm package name (mynewcli): mynewcli $ cd mynewcli $ ./bin/run --version mynewcli/0.0.0 darwin-x64 node-v9.5.0 $ ./bin/run --help USAGE $ mynewcli [COMMAND] COMMANDS hello help display help for mynewcli $ ./bin/run hello hello world from ./src/hello.js!
三、Todocli 實戰
下面我們將創建一個 Todo CLI,它可以執行以下 4 個操作:
- 添加一個新任務
- 查看所有任務
- 更新任務
- 移除任務
3.1 初始化項目
使用 Oclif 你可以創建兩種不同類型的 CLI,即 Single CLIs 和 Multi CLIs。這裡我們來創建一個 Multi CLIs 項目:
$ npx oclif multi todocli
以上命令執行後,我們需要設置 todocli 項目的一些配置資訊,具體如下圖所示:

當上述的命令成功執行後,會在當前的命令的執行目錄下創建一個 todocli 項目。接著我們進入該項目,然後運行 help 命令:
$ cd todocli && ./bin/run --help
以上命令運行後,控制台將輸出以下結果:
a todo list cli VERSION todocli/1.0.0 darwin-x64 node-v10.12.0 USAGE $ todocli [COMMAND] COMMANDS hello describe the command here help display help for todocli
3.2 項目結構
在 src 目錄中,我們可以發現一個名為 commands 子目錄,該目錄包含所有與文件名相關的所有命令。比如,我們有一個名為 hello 的命令,那麼在 commands 目錄中將會包含一個 hello.js 或 hello.ts 文件。這裡我們無需進行任何設置,即可運行該命令。
$ ./bin/run hello hello world from ./src/commands/hello.ts
現在讓我們刪除 hello.ts,因為我們不需要它了。
3.3 設置資料庫
為了存儲我們的任務,我們需要一個存儲系統。為簡單起見,我們將使用 lowdb,這是一個非常簡單的 JSON 文件存儲系統。
讓我們來安裝它:
$ npm install -S lowdb $ npm install -D @types/lowdb
待成功安裝 lowdb 依賴後,在我們項目的根目錄下創建一個 db.json 文件,這個文件用來保存數據。然後我們繼續在 src 目錄中創建一個 db.ts 文件並輸入以下內容:
import * as lowdb from "lowdb"; import * as FileSync from "lowdb/adapters/FileSync"; type Todo = { id: number; task: string; done: boolean; }; type TodoSchema = { todos: Todo[]; }; const adapter = new FileSync<TodoSchema>("db.json"); const db = lowdb(adapter); db.defaults({ todos: [] }).write(); const Todos = db.get("todos"); export { db, Todos };
3.4 添加任務
設置完資料庫,讓我們先來實現添加 Todo 任務的功能。這裡我們將使用 Oclif CLI 提供的命令,來快速創建 command。下面我們先來創建 add 命令:
$ npx oclif command add
以上命令運行後,在 src/command 目錄下會生成一個 add.ts 文件,打開該文件並輸入以下程式碼:
import { Command, flags } from "@oclif/command"; import { Todos } from "../db"; export default class Add extends Command { static description = `Adds a new todo ... Adds a new todo to the existing list `; static flags = { task: flags.string({ char: "n", description: "task" }) }; async run() { const { flags: { task } } = this.parse(Add); if (!task) return; const todo = await Todos.push({ task, id: Todos.value().length, done: false }).write(); this.log(JSON.stringify(todo)); } }
上述程式碼中包含一些關鍵組件:
description屬性,用於描述命令的用途;flags屬性,用於描述傳遞給命令的標識;- 一個
run方法用於執行當前命令的主要功能;
創建完 add 命令後,我們可以在命令行中運行它:
$ ./bin/run add --task="Learn Oclif"
以上命令成功執行後,會輸出以下資訊:
[{"task":"Learn Oclif","id":0,"done":false}]
同時在項目根目錄的 db.json 文件中也會保存相應的資訊:
{ "todos": [ { "task": "Learn Oclif", "id": 0, "done": false } ] }
3.5 查看任務
下面我們繼續使用 Oclif CLI 提供的命令,來創建一個新的 show 命令:
$ npx oclif command show
以上命令運行後,在 src/command 目錄下會生成一個 show.ts 文件,打開該文件並輸入以下程式碼:
import { Command, flags } from "@oclif/command"; import chalk from "chalk"; import { Todos } from "../db"; export default class Show extends Command { static description = `Shows existing tasks ... Show all the tasks sorted by their ids `; async run() { const todos = await Todos.sortBy("id").value(); todos.forEach(({ id, task, done }) => { this.log( `${chalk.magenta(id.toString())} ${ done ? chalk.green("DONE") : chalk.grey("NOT DONE") } : ${task}` ); }); } }
創建完 show 命令,我們馬上來測試一下,即在命令行輸入以下命令:
$ ./bin/run show
以上命令成功執行後,會輸出以下資訊:
0 NOT DONE : Learn Oclif
此外,在運行 show 命令時,我們還可以添加 --help 標識,輸出該命令的幫助資訊:
$ ./bin/run show --help Shows existing tasks USAGE $ todocli show DESCRIPTION ... Show all the tasks sorted by their ids
3.6 更新任務
目前我們已經創建了 add 和 show 兩個命令,接下來我們再來創建一個 update 命令,該命令用於更新已創建的 Todo 任務,簡單起見,我們只實現更新任務是否完成的狀態。與前兩個命令一樣,我們首先創建 update 命令:
$ npx oclif command update
以上命令運行後,在 src/command 目錄下會生成一個 update.ts 文件,打開該文件並輸入以下程式碼:
import { Command, flags } from "@oclif/command"; import { Todos } from "../db"; export default class Update extends Command { static description = `Marks a task as done ... Marks a task as done `; static flags = { id: flags.string({ char: "n", description: "task id" }) }; async run() { const { flags: { id } } = this.parse(Update); if (!id) return; const todo = await Todos.find({ id: parseInt(id, 10) }) .assign({ done: true }) .write(); this.log(JSON.stringify(todo)); } }
創建完 update 命令,我們來嘗試更新一下前面通過 add 命令創建的 Learn Oclif 待辦任務的狀態:
$ ./bin/run update --id=0
以上命令成功執行後,會輸出以下資訊:
{"task":"Learn Oclif","id":0,"done":true}
3.7 移除任務
現在我們來創建最後一個命令,該命令用於移除 Todo 任務。與前面一樣,我們首先創建 remove 命令:
$ npx oclif command remove
以上命令運行後,在 src/command 目錄下會生成一個 remove.ts 文件,打開該文件並輸入以下程式碼:
import { Command, flags } from "@oclif/command"; const { Todos } = require("../db"); export default class Remove extends Command { static description = `Removes a task by id ... Removes a task permanently from database by id `; static flags = { id: flags.string({ char: "n", description: "task id", required: true }) }; async run() { const { flags: { id } } = this.parse(Remove); const todo = await Todos.remove({ id: parseInt(id, 10) }).write(); this.log(JSON.stringify(todo)); } }
創建完 remove 命令,我們來實際測試一下:
$ ./bin/run remove --id=0
如果不出意外的話,當以上命令成功運行後,項目根目錄下 db.json 文件的內容將發生變化,具體如下:
{ "todos": [] }
很明顯前面我們通過 add 命令創建的 Todo 任務,已經被移除了。此時,Todo CLI 包含的 4 個命令都已經創建完成了,最後我們來介紹一下如何把 Todo CLI 項目發布到 NPM。
3.8 構建與發布
發布到 NPM 前,你需要確保擁有一個 NPM 賬戶,然後使用以下命令進行登錄:
$ npm login
接著在項目的根目錄中運行以下 NPM 腳本:
$ npm run prepack
執行該命令後會對項目進行自動構建並更新項目中的 README.md 說明文檔。項目構建成功後,就可以發布到 NPM 了,具體操作如下:
$ npm version (major|minor|patch) # bumps version, updates README, adds git tag $ npm publish
四、參考資源
歡迎小夥伴們訂閱前端全棧修仙之路,及時閱讀 Angular、TypeScript、Node.js/Java和Spring技術棧最新文章。

