前言
? 在工程中,不僅是軟件工程,在建筑行業(yè),我們也經(jīng)常能看到腳手架的概念。腳手架(又稱(chēng)為CLI
,全稱(chēng)command-line interface
),我理解是一種快速構(gòu)建項(xiàng)目的工具,它主要提供了項(xiàng)目的基礎(chǔ)結(jié)構(gòu)和一些常用的配置,避免了從頭開(kāi)始搭建項(xiàng)目的繁瑣工作。通過(guò)使用腳手架,開(kāi)發(fā)者可以更加高效地創(chuàng)建和啟動(dòng)項(xiàng)目,并且保持項(xiàng)目結(jié)構(gòu)的一致性,同時(shí)還能提供一些常用的功能和工具,例如自動(dòng)化構(gòu)建、代碼生成、測(cè)試等。腳手架可以根據(jù)特定的需求和技術(shù)棧來(lái)定制。平時(shí)的學(xué)習(xí)和工作中我們經(jīng)常會(huì)用到各種各樣的腳手架,例如vue-cli
,Create React App
,create-vite
等等,使用這些工具可以大大提高我們創(chuàng)建項(xiàng)目的速度。
? 那當(dāng)我們遇到重復(fù)的構(gòu)建工作的時(shí)候是不是也可以考慮自己搭建一個(gè)腳手架呢。正好最近在使用vitepress
搭建自己的小冊(cè)網(wǎng)站記錄自己的學(xué)習(xí)過(guò)程。由于前端涉及到的工具很多,需要很多個(gè)小冊(cè),那么我是不是可以通過(guò)搭建自己的腳手架來(lái)完成這樣重復(fù)的小冊(cè)構(gòu)建工作呢?于是就有了這篇文章
? 學(xué)習(xí)本文,你能收獲:
- ?? 掌握開(kāi)發(fā)腳手架的全流程
- ?? 學(xué)會(huì)命令行開(kāi)發(fā)常用的多種第三方模塊
- ?? 擁有一個(gè)屬于自己的腳手架
前置知識(shí)
? 在看了常見(jiàn)的腳手架工具的源碼之后,發(fā)現(xiàn)大部分腳手架的構(gòu)建都離不開(kāi)以下工具,先對(duì)這些工具做個(gè)介紹,以便有初步的了解。這些庫(kù)不一定都會(huì)用到,可以按需選擇。
-
commander
:最常用的命令行工具,可以通過(guò)內(nèi)置的api很方便的讀取命令行的命令。-
minimist
: 命令行參數(shù)解析工具,比commander要簡(jiǎn)單
-
-
inquirer
: 交互式命令工具,給用戶提供一個(gè)提問(wèn)流方式,通過(guò)promise的方式返回用戶選擇。可以實(shí)現(xiàn)定制化功能 -
prompts
: 交互式命令工具,inquirer的輕量級(jí)版本 -
chalk
: 顏色插件,用來(lái)修改命令行輸出樣式,通過(guò)顏色區(qū)分info、error日志。對(duì)重要信息用不同顏色進(jìn)行區(qū)分 -
ora
: 用于顯示加載中的效果,類(lèi)似于前端頁(yè)面的loading效果,像下載模版這種耗時(shí)的操作,有了loading效果,可以提示用戶正在進(jìn)行中,請(qǐng)耐心等待。在我們平時(shí)使用命令行的時(shí)候經(jīng)??吹竭@種效果 -
fs-extra
: node fs文件系統(tǒng)模塊的增強(qiáng)版,可以方便我們操作本地文件。對(duì)所有的異步操作都提供了promise支持 -
cross-spawn
: 是一個(gè)用于跨平臺(tái)執(zhí)行命令的 Node.js 模塊。它解決了在不同操作系統(tǒng)上執(zhí)行命令時(shí)可能會(huì)遇到的一些兼容性問(wèn)題。 -
figlet
: 可以將text
文本轉(zhuǎn)化成生成基于ASCII
的藝術(shù)字 -
execa
: 執(zhí)行終端命令 -
handlebars
: 可以方便執(zhí)行模版替換,用用戶的輸入替換掉內(nèi)置模版
如何搭建一個(gè)腳手架
? 我們以vue-cli
為例,看下腳手架的一般功能
-
提供不同的指令,執(zhí)行不同的事情
例如 --version --help --create等等
-
交互式用戶選擇
我們的腳手架可能會(huì)有多種選擇,我們需要向用戶提供不同的選擇。
-
用戶選擇完畢后,根據(jù)用戶選擇生成用戶需求的項(xiàng)目文件
通過(guò)以上分析,我們可以看出腳手架的基本范式:通過(guò)命令行與用戶交互的選擇來(lái)生成對(duì)應(yīng)的文件。
搭建自己的腳手架
知道了大概流程之后我們就可以開(kāi)始搭建自己的腳手架了
項(xiàng)目使用 typescript + node
搭建,主要目錄結(jié)構(gòu)如下
zy-cli
├─ .gitignore
├─ README.md
├─ build // 打包后文件夾
├─ project-template // 初始化項(xiàng)目模版
├─ bin
| ├─ bin.js // 生產(chǎn)環(huán)境執(zhí)行文件入口
| ├─ bin-dev.js // 本底調(diào)試執(zhí)行文件入口,主要是能夠動(dòng)態(tài)編譯ts
├─ package.json // 配置文件,具體見(jiàn)下
├─ src
│ └─ const // 常量包
│ ├─ index.ts
│ ├─ commands // 命令文件夾
│ │ ├─ create.ts // create命令
│ │ ├─ config.ts // config命令
│ │ ├─ package.ts // package命令
│ │ └─ utils // 公共函數(shù)
│ ├─ index.ts // 入口文件
│ └─ helpers // 公共第三方包
│ ├─ index.ts
│ ├─ logger.ts // 控制臺(tái)顏色輸出
│ └─ spinner.ts // 控制臺(tái)loading
│ └─ utils // 工具類(lèi)
│ ├─ index.ts
├─ tsconfig.json // TypeScript配置文件
└─ tslint.json // tslint配置文件
初始化項(xiàng)目
安裝依賴(lài)
1、npm init
初始化package.json
2、npm i typescript ts-node eslint rimraf -D
安裝開(kāi)發(fā)依賴(lài)
3、npm i typescript chalk commander execa fs-extra globby handlebars inquirer ora pacote figlet
安裝生產(chǎn)依賴(lài)
packagejson 配置
scripts配置clear、build、publish、lint命令
。
完成的package.json
配置文件如下
{
"name": "xzy-cli",
"version": "1.0.0",
"description": "xzy-cli",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"clear": "rimraf build",
"build": "npm run clear && tsc",
"publish": "npm run build && npm publish",
"lint": "tslint ./src/**/*.ts --fix",
"lini-fix": "tslint ./src/**/*.ts --fix"
},
"repository": {
"type": "git",
"url": "https://github.com/xzy0625/xzy-cli.git"
},
"bin": {
"xzy": "./bin/bin.js",
"xzy-dev": "./bin/bin-dev.js"
},
"keywords": [
"cli",
"node",
"vitepress",
"typescript"
],
"author": "csuxxzy",
"license": "ISC",
"bugs": {
"url": "https://github.com/xzy0625/xzy-cli/issues"
},
"homepage": "https://github.com/xzy0625/xzy-cli#readme",
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^6.0.0",
"@typescript-eslint/parser": "^6.0.0",
"eslint": "^8.45.0",
"rimraf": "^5.0.1",
"ts-node": "^10.9.1",
"typescript": "^5.1.6"
},
"dependencies": {
"chalk": "^4.1.2",
"commander": "^11.0.0",
"execa": "^7.1.1",
"figlet": "^1.6.0",
"fs-extra": "^11.1.1",
"globby": "^11.0.4",
"handlebars": "^4.7.7",
"inquirer": "^9.2.8",
"ora": "^6.3.1",
"pacote": "^15.2.0"
}
}
需關(guān)注bin
字段和files
字段。
-
bin字段
: bin字段用于指定可執(zhí)行文件的路徑,是在全局或局部安裝模塊時(shí)可以直接運(yùn)行的命令。bin字段的值可以是一個(gè)字符串或一個(gè)對(duì)象。如果bin字段的值是一個(gè)字符串,那么它表示一個(gè)可執(zhí)行文件的路徑。例如:
{
"name": "my-package",
"version": "1.0.0",
"bin": "./dist/my-command.js"
}
? 在安裝該模塊后,可以直接在命令行中運(yùn)行my-command
來(lái)執(zhí)行./dist/my-command.js
指定的文件。
? 如果bin字段的值是一個(gè)對(duì)象,那么它的鍵是命令的名字,值是對(duì)應(yīng)的可執(zhí)行文件的路徑。例如:
{
"name": "my-package",
"version": "1.0.0",
"bin": {
"my-command": "./dist/my-command.js",
"another-command": "./dist/another-command.js"
}
}
? 在安裝該模塊后,可以直接在命令行中運(yùn)行my-command
和another-command
來(lái)分別執(zhí)行./dist/my-command.js和
./dist/another-command.js`指定的文件。
-
files字段
: 即npm的白名單,也就是說(shuō)發(fā)包后需要包括哪些文件,不配置的話默認(rèn)發(fā)布全部文件,這樣很容易暴漏我們的源碼,也會(huì)導(dǎo)致npm的包體積過(guò)大。所以這里我們配置了"files": [ "build", "bin.js" ]
,發(fā)布到npm的時(shí)候只會(huì)包含build目錄和bin.js文件
lint 和typescript配置
-
typescript配置
根目錄下執(zhí)行
tsc --init
,更改配置如下{ "compilerOptions": { "target": "es2017", "module": "commonjs", "moduleResolution": "node", "emitDecoratorMetadata": true, "experimentalDecorators": true, "allowSyntheticDefaultImports": true, "alwaysStrict": true, "sourceMap": false, "noEmit": false, "noEmitHelpers": false, "importHelpers": false, "strictNullChecks": false, "allowUnreachableCode": true, "lib": ["es6"], "typeRoots": ["./node_modules/@types"], "outDir": "./build", // 重定向輸出目錄 "rootDir": "./src" // 僅用來(lái)控制輸出的目錄結(jié)構(gòu) }, "exclude": [ // 不參與打包的目錄 "node_modules", "build", ], "esModuleInterop": true, "allowSyntheticDefaultImports": true, "compileOnSave": false, "buildOnSave": false }
-
eslint
根目錄下執(zhí)行
eslint --init --format json
,完整配置如下{ "env": { "browser": true, "es2021": true }, "extends": [ "eslint:recommended", "plugin:@typescript-eslint/recommended" ], "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaVersion": "latest", "sourceType": "module" }, "plugins": [ "@typescript-eslint" ], "rules": { "no-console": "warn" } }
加入bin字段調(diào)試
在
package.json
中加入如下bin
字段"bin": { "xzy": "./bin.js", "xzy-dev": "./bin-dev.js" }
新建
bin
目錄,加入bin.js
和bin-dev.js
(如有l(wèi)int報(bào)錯(cuò),在.eslintingnore文件中加入bin目錄就好)mkdir bin && cd ./bin && touch bin.js bin-dev.js
bin-dev.js
#!/usr/bin/env node require('ts-node/register') require('../src')
bin.js
#!/usr/bin/env node require('../build')
該字段是定義命令名(也就是你腳手架的名字)和關(guān)聯(lián)的執(zhí)行文件,行首加入一行
#!/usr/bin/env node
指定當(dāng)前腳本由node.js進(jìn)行解析由于我們使用的是
typescript
進(jìn)行開(kāi)發(fā),如果不用ts動(dòng)態(tài)編譯功能。所以在bin-dev
中使用了ts-node
。這樣不用每次改代碼都build一次。后續(xù)發(fā)包之后使用bin.js
,指向打包后的build
目錄
npm link調(diào)試
根目錄下執(zhí)行npm link
,將我們的包link到全局的node中,這樣就可以使用包中的bin命令了。
核心代碼實(shí)現(xiàn)
獲取所有命令
通過(guò)文件即功能的方式統(tǒng)一獲取commands目錄下的所有命令,同時(shí)注冊(cè)到commander中去
// 獲取所有命令
const commandsPath = await getPathList("./commands/*.*s");
// 注冊(cè)命令
commandsPath.forEach((commandPath) => {
const commandObj = require(`./${commandPath}`);
const { command, description, optionList, action } = commandObj.default;
const curp = program
.command(command)
.description(description)
.action(action);
optionList &&
optionList.map((option: [string]) => {
curp.option(...option);
});
});
create實(shí)現(xiàn)
-
檢查目錄是否存在
// 檢查是否已經(jīng)存在相同名字工程 export const checkProjectExist = async (targetDir) => { if (fs.existsSync(targetDir)) { const answer = await inquirer.prompt({ type: "list", name: "checkExist", message: `\n倉(cāng)庫(kù)路徑${targetDir}已存在同名文件,請(qǐng)選擇是否需要覆蓋原路徑(刪除原文件后新建)`, choices: ["是", "否"], }); if (answer.checkExist === "是") { logger.warn(`已刪除${targetDir}...`); fs.removeSync(targetDir); return false; } else { logger.info("您已取消創(chuàng)建"); return true; } } return false; };
-
獲取用戶輸入
// 獲取用戶輸入 export const getQuestions = async (projectName): Promise<IQuestion> => { return await inquirer.prompt([ { type: "input", name: "name", message: `package name: (${projectName})`, default: projectName, }, { type: "input", name: "description", message: "description", }, { type: "input", name: "author", message: "author", }, { type: "input", name: "git", message: "git倉(cāng)庫(kù)", }, ]); };
-
復(fù)制項(xiàng)目
export const cloneProject = async ( targetDir: string, projectName: string, template: ICmdArgs["template"], projectInfo: IQuestion ) => { spinner.start(`開(kāi)始創(chuàng)建目標(biāo)文件 ${chalk.cyan(targetDir)}`); // 復(fù)制'project-template'到目標(biāo)路徑下創(chuàng)建工程 await fs.copy( path.join(__dirname, "..", "..", `project_template/${template}`), targetDir ); // handlebars模版引擎解析用戶輸入的信息存在package.json const packagePath = `${targetDir}/package.json`; const configPath = `${targetDir}/docs/.vitepress/config.js`; // 讀取文件內(nèi)容 const packageContent = fs.readFileSync(packagePath, "utf-8"); const configContent = fs.readFileSync(configPath, "utf-8"); // 覆蓋模版內(nèi)容 const packageResult = handlebars.compile(packageContent)(projectInfo); const configResult = handlebars.compile(configContent)(projectInfo); // 寫(xiě)入新內(nèi)容 fs.writeFileSync(packagePath, packageResult); fs.writeFileSync(configPath, configResult); logger.info("開(kāi)始安裝項(xiàng)目所需依賴(lài)"); try { // 新建工程裝包 execa.commandSync("yarn", { stdio: "inherit", cwd: targetDir, }); } catch (error) { // 報(bào)錯(cuò)就用npm試下 execa.commandSync("npm install", { stdio: "inherit", cwd: targetDir, }); } if (projectInfo.git) { logger.info("開(kāi)始關(guān)聯(lián)項(xiàng)目到git"); // 關(guān)聯(lián)git gitCmds(projectInfo.git).forEach((cmd) => execa.commandSync(cmd, { stdio: "inherit", cwd: targetDir, }) ); } spinner.succeed( `目標(biāo)文件創(chuàng)建完成 ${chalk.yellow(projectName)}\n?? 輸入以下命令開(kāi)始創(chuàng)作吧!:` ); logger.info(`$ cd ${projectName}\n$ yarn dev\n`); };
美化項(xiàng)目
添加logo
當(dāng)使用--help
時(shí)在最后添加logo展示
console.log(
"\r\n" +
figlet.textSync("xzy-cli", {
font: "3D-ASCII",
horizontalLayout: "default",
verticalLayout: "default",
width: 80,
whitespaceBreak: true,
})
);
發(fā)包
到此,腳手架基本的搭建與開(kāi)發(fā)就完成了,發(fā)布到npm
-
1、npm run lint
校驗(yàn)代碼,畢竟都發(fā)包了,避免出現(xiàn)問(wèn)題 -
2、npm run build
typescript打包 -
3、npm publish
發(fā)布到npm 發(fā)包完成后,安裝檢查
具體可以參考我的另一片文章:npm發(fā)布包詳細(xì)流程+常見(jiàn)錯(cuò)誤
源碼倉(cāng)庫(kù)
github 地址: @csuxzy/xzy-cli文章來(lái)源:http://www.zghlxwxcb.cn/news/detail-576051.html
npm 倉(cāng)庫(kù)地址: @csuxzy/xzy-cli文章來(lái)源地址http://www.zghlxwxcb.cn/news/detail-576051.html
到了這里,關(guān)于設(shè)計(jì)自己的腳手架的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!