簡介
腳手架CLI(command-line-interface)是一類快速形成工程化目錄的工具。
開發過程中,如果需要新建前端項目,我們經常都會用到腳手架來創建工程,通過命令行式的交互,可快速選擇選項並完成初始項目的搭建。而CV大法往往會帶來很多重複的刪減工作,且會導致項目分散、架構不統一等等弊端。
常見的主流框架都有自己的腳手架:
- create-vite
- @vue/cli
- create-react-app
通過本文內容實踐,可以開發出一套較基礎的腳手架工具,滿足日常工作中搭建項目的同時,持續對腳手架和模版內容做優化。
搭建
按照慣例,先附上代碼 thjjames/th-cli 並整理下知識點:
- 如何聲明全局命令
- 如何實現命令行交互
- 如何實現用户選項交互
- 如何創建項目(預設和遠程)
依賴包
在此之前,還需要先訪問一下 package.json 文件,看下需要安裝的依賴並簡單瞭解這些依賴對應的功能:
-
核心工具庫
commanderTJ大神寫的Nodejs命令行交互工具inquirer選項命令行交互工具,也可以用promptsfs-extrafs的擴展包,添加了部分方法和promise支持mem-fs/mem-fs-editor基於ejs的文件編輯助手download-git-repo遠程模板下載工具
-
輔助工具庫
minimist輕量級的命令行參數解析引擎semver語義化版本號規則validate-npm-package-name驗證項目名稱chalk美化終端輸出ora終端加載動畫figlet輸出終端藝術字
聲明全局命令
為何安裝完腳手架包後,就能在任意地方執行腳手架命令呢?
// 安裝
npm i -g thjjames/th-cli
// 使用
th-cli create <project-name>
先看 package.json 文件:
// 當只有一個可執行文件且命令名就是包名稱,例如這裏可以簡寫為
// "bin": "bin/th-cli.js"
{
"name": "th-cli",
"version": "1.0.0",
"description": "Scaffold for Creating Projects.",
"author": "田豪峻 James<thjjames@163.com>",
"bin": {
"th-cli": "bin/th-cli.js"
},
...
}
bin 字段指定了命令名到本地文件名的映射,安裝包時會在 node_modules 文件夾下面的 .bin 目錄中複製可執行文件,這樣就可以在安裝的目錄下執行自定義腳手架命令。當全局安裝時,會映射到全局存儲的位置(Linux 和 macOS 系統默認目錄 /usr/local/lib/)。
如果想直接執行 th-cli 命令,會直接從全局目錄查找,必須全局安裝。
npm run[-script]執行命令時的查找順序為: 先從當前項目目錄的node_modules文件夾下查找,然後是全局安裝目錄,最後再是Node根目錄。
如何實現命令行交互
理解完上述的命令查找關係鏈,我們看下命令名 th-cli 對應的 bin/th-cli.js 文件,是開始執行腳手架內容的關鍵,先看第一行:
#! /usr/bin/env node
#! 是一種在 Linux 系統中使用的特殊註釋,通常用於指定腳本文件的解釋器。/usr/bin/env 代表解釋器目錄, 用 node 執行。更多相關內容可以通過 Shebang 瞭解。
執行前可以做些校驗內容,用 semver 判斷當前環境的 node 版本是否滿足腳手架需要的最低版本、validate-npm-package-name 判斷新建的包名是否符合命名規範,這些都是錦上添花的功能,不花篇幅去講了,請自行擼代碼。
接下去就是核心內容 commander 部分,這裏看下我們使用到的一些功能:
option定義選項-
command創建 create 和 list 等自定義命令alias給命令添加別名description給命令添加描述argument給命令聲明參數option給命令定義選項action給命令添加執行函數
version提供版本號
具體用法文檔上已經寫的很詳細了,照着文檔創建一個自定義的create命令:
program
.command('create <project-name>')
.description('create a new project by th-cli')
.option('-d, --default', 'skip prompts and use default preset')
.option('-f, --force', 'overwrite target directory if it exists')
.option('-c, --clone', 'use git clone when fetching remote preset')
.action((name, options) => {
if (minimist(process.argv.slice(3))._.length > 1) {
console.warn(chalk.yellow('You provided more than one argument.'))
console.warn(chalk.yellow('The first one will be used as the project name, the rest are ignored.'))
}
require('../lib/create')(name, options);
});
通過上述代碼完成了需要通過命令行交互來搭建項目的主體,裏面的參數例如 -f 作用是在我們創建的項目目錄有衝突時提供的強制覆蓋選項:
if (fse.existsSync(targetDir)) {
if (options.force) {
fse.removeSync(targetDir);
} else {
console.error(chalk.red(`Folder ${targetDir} is already in use, please rename or overwrite it by using option -f.`));
process.exit(1);
}
}
執行自定義命令並通過驗證後可以看到直接進入用户選項交互:
th-cli create <project-name>
如何實現用户選項交互
inquirer 是交互式命令行美化工具,提供了一系列常用組件(如input、list、checkbox等),解析輸入並收集、驗證答案。經過一系列的交互操作後,在命令的執行目錄生成了新的項目。
const inquirer = require('inquirer');
const prompts = [
{
name: 'type',
type: 'list',
message: '請選擇獲取模板方式',
default: 'preset',
choices: [
{ value: 'preset', name: '預設模板' },
{ value: 'remote', name: '遠程模板' }
]
},
...
];
const answers = await inquirer.prompt(prompts);
這裏交互是幫助我們可以通過一系列選項來確認最終想要的項目,是本地預設還是遠程模板、是vue還是react項目、是babel還是esbuild打包、是否需要ts等等,這些都可以通過交互得到答案並體現在最終的項目上。
如何創建項目
如果選擇遠程模板,需要藉助 download-git-repo 從遠程 git 倉庫上下載,上述的 -c 參數對應此包的clone參數用來區分 git clone 和 http下載,下載後腳手架需要幫助執行 git init 以初始化現有倉庫;而選擇預設模板的話,將會從腳手架代碼中的template文件夾複製到本地,當然了,此處並不是簡單的 fs.copy 複製,會將模板中的文件基於 ejs 模板引擎和用户交互選項的答案anwsers生成新的模板文件。
const templateDir = answers.type === 'preset' ? path.resolve(TEMPLATE_DIR, answers.preset) : path.resolve(os.tmpdir(), `cli-tmp-${name}`);
const spinner = ora(`Creating a new project in ${targetDir}, please wait...\n`).start();
if (answers.type === 'preset') {
await copy(name, answers, targetDir, templateDir);
} else {
await downloadGitRepoAsync(answers.repository, templateDir, {
clone: options.clone
});
await copy(name, answers, targetDir, templateDir);
fse.removeSync(templateDir);
}
execSync('git init', {
cwd: targetDir
});
spinner.succeed('Create successfully');
由於上述過程需要消耗點時間,在過程中我們通過 ora 來展現加載效果,完善整個交互過程。
看到這裏思考下一個問題,遠程模板的下載速度要比本地預設慢很多,那為何還需要這個選項呢?原因有以下,一是當自己開發的腳手架需要給其他部門使用時,不太適合將別人的定製模板放到本地預設中,這種情況下通過 git clone 的方式是最合適的,二是當本地預設模板還不夠完善需要頻繁改動時,會頻繁的更新版本號導致開發者頻繁的更新版本,這種情況下也可以考慮使用遠程模板來規避。
最後想下,整個用户交互的過程還是略微有一些些成本的,如果我們團隊的項目風格比較固定即大部分情況下的選項答案都是相同的,有沒有更快速便捷的創建方式呢?往上看選項 -d 就是起這個作用,帶上這個參數時,所有的交互選項都會選擇默認答案並跳過用户交互環節,直接生成默認項目模板!
結語
到這裏,我們實現了一個簡易的腳手架,可以選擇模板,也有自定義的快捷命令,但回看簡介裏主流的腳手架代碼,可以看出還有很多功能點可以完善,例如:
- 模板中如何根據選項(例如是否移動端、是否需要pinia、是否需要ts)動態定製成多套?
- 腳手架中的模板更新後如何同步更新到以前已經生成的項目中?
就賣個關子🤪留給大家思考吧,前端腳手架並沒有一成不變的最佳實踐,一切還得根據團隊實際情況定製。但不管怎麼説,完成了從0到1的MVP版本後,再到MDP版本就簡單多了。