博客 / 詳情

返回

通俗易懂地談談,前端工程化之自定義腳手架的理解,並附上一個實踐案例發佈到npm上

前言

  • 如果要開發一個新項目,傳統方式要敲不少命令
  • 如下:使用最新版的vite,創建一個項目,選擇對應的框架語言等

然後就是安裝各種依賴,安裝antd、安裝路由、安裝zustand等,如npm install axios react-router-dom antd ......

  • 若每次新開一個常規項目,都執行這樣的搭建操作,整體來説,效率略低,不優雅——(畢竟是手動配置)
  • 於是,在此基礎上,社區有作者提供了進一步的現成模板,比如針對於後台管理系統這類業務場景,有React-Admin、Ant-Design-Pro、或者ruoyi這樣後台模板框架,開發者根據自己情況適當修改增刪功能——(需根據自己公司業務適配修改)

如此這般,後續若再新開常規項目(假設需要新開發一個茶葉管理系統),直接複製一份先前已經沉澱好了的框架模板(假設原先沉澱好的就叫做基礎管理系統)修修改改,在先前的基礎上三次開發即可

自定義腳手架簡述

新項目手動複製粘貼的痛點

但是,這裏有一個麻煩的地方:

  • 首先,我們需要新建一個文件夾,然後把原本沉澱好的一套代碼複製過來(假設叫做base-admin)
  • 然後,執行npm i安裝依賴
  • 緊接着需要手動修改package.json裏面的name的值為新項目名、也要修改index.html文件裏面的title標籤裏面的名字等(當然還可能有其他要修改的),如下:
{
  "name": "base-admin", // 修改成:"name": "tea-admin",
  "version": "1.1.1",
  "type": "module",
  "scripts": { ... },
  "dependencies": { ... },
  "devDependencies": { ... }
}

然後

<!doctype html>
<html lang="en">

<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  <title>基礎管理系統</title>
  <!-- 修改成:茶葉管理系統 -->
</head>

<body>
  <div id="root"></div>
  <script type="module" src="/src/main.jsx"></script>
</body>

</html>
  • 所以,我們思考,能不能寫一個腳本,通過命令行交互的方式,交互執行一下
  • 就自動能夠把原本沉澱好的那套base-admin代碼拷貝過來

    • 並且也能自動夠修改package.json裏面的name的值為新項目名
    • 也能自動修改index.html文件裏面的title標籤裏面的名字
    • 包括自動執行npm i下載依賴
    • 等其他個性化操作

簡約來説,這件事,就是自定義腳手架,所做的事情

自定義腳手架——可定製內容

  • 實際上,自定義腳手架,不僅僅只是做 複製 base-admin 代碼→改 name→改 title這樣的 基礎功能
  • 還可以進階操作,比如base-admin有8個模塊、但是tea-admin只需要3個模塊,我們也可以通過命令行,使用自定義腳手架創建項目的時候,選擇保留那些模塊,或者丟棄那些模塊
  • 甚至,自定義腳手架,還可以幫我執行git倉庫初始化命令等

所以,自定義腳手架的收益就是:

減輕項目初始化的工作量、做到開發的規範和統一,當然也可以靈活的定製一些內容

自定義腳手架的大致步驟

  1. 把以往的沉澱好的base-admin發佈到github/gitlab上(這是前提,要有基礎項目框架模板代碼,便於後續開發項目的複用)
  2. 編輯自己的自定義腳手架(就是一個npm項目,帶有package.json和一堆js腳本文件)
  3. 把寫好的自定義腳手架(假設叫做self-cli)發佈到npm上(或者使用Verdaccio搭建自己的私服npm),然後所有同事可在自己電腦上全局安裝npm i self-cli -g

    1. 使用Verdaccio搭建自己的私服npm,可以參考筆者的這篇文章:《20張圖的保姆級教程,記錄使用Verdaccio在Ubuntu服務器上搭建Npm私服》
  4. 然後,就可以在命令行執行自定義腳手架提供的命令,比如self-cli -V(查看自定義版本號)、或self-cli create tea-admin(使用自定義腳手架self-cli創建新項目tea-admin)
  5. 這樣的話,就會自動拉取git倉庫上的base-admin代碼
  6. 緊接着,命令行會提供一些問詢交互,以便於創建新項目的時候,可以自定義一些東西(相當於執行npm create vite\@latest xxx的效果)
  7. 最後命令行回車,自動幫我們執行修改base-admin的一些基礎信息和其他自定義操作,最後自動執行npm i安裝依賴,並跑起來項目

自定義腳手架常用的包

常用包

強大命令包shelljs可以便捷地運行命令:https://www.npmjs.com/package/shelljs

基本的包

  • 自定義腳手架要允許在命令行執行命令,可使用 commander
  • 執行完命令以後,要允許用户輸入選擇等交互操作,可使用 inquirer
  • 要能夠拉取git倉庫代碼,可使用 download-git-repo
  • 在拉取代碼的過程中,需要有加載loading效果,可使用 ora
  • 在拉取代碼的過程中,需要有進度條百分比加載的效果,可使用progress

如果美化一下命令行,可使用如下包

  • 如果想讓終端的輸出文字五顏六色,可以使用 chalk
  • 如果想讓終端輸出的文字有字符畫效果,可以使用 figlet
  • 如果想讓終端輸出的文字呈現表格形式,可以使用 table
  • 如果想讓終端輸出帶有emoji,可以使用node-emoji

常用包做的有意思的效果

上述效果對應package.json包如下

{
  "name": "some-npm-pkg",
  "version": "1.0.0",
  "type": "module",
  "main": "index.js",
  "scripts": {
    "start": "node index.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": "",
  "dependencies": {
    "chalk": "^5.6.2",
    "commander": "^14.0.2",
    "figlet": "^1.9.4",
    "inquirer": "^12.11.1",
    "node-emoji": "^2.2.0",
    "ora": "^9.0.0",
    "progress": "^2.0.3",
    "table": "^6.9.0"
  }
}

其他的包

  • fs-extra,增強版的fs文件操作,更好用
  • ejs模板引擎,可用來替換模板中的變量
  • semver,語義化版本號解析 / 對比(判斷版本高低、是否符合規則)
  • which,查找系統中可執行文件的路徑(如找到 node/npm 等命令的安裝位置)
  • chokidar,高性能文件監聽(監控文件 / 目錄變化,如文件修改後自動觸發操作)——nodemon核心依賴
  • portfinder,自動查找可用端口(避免端口占用,如本地服務自動選端口)
  • opener,跨平台打開文件 / 瀏覽器(如自動打開本地服務頁面)
  • mime,MIME 類型解析(判斷文件 / 請求的內容類型,如 json、html、jpg 等)
  • giturl,解析 / 轉換 Git 倉庫 URL(如把 HTTPS 轉 SSH,或提取倉庫信息)
  • npm-request,簡化 HTTP 請求的工具(聚焦 npm 相關接口請求,如查詢包、下載包)
  • clipanion,Node.js 命令行參數解析(更優雅的 CLI 構建)——對標Conmand
  • diff,文本差異對比(測試時驗證輸出 / 文件變化)
  • is-windows,判斷是否 Windows 系統

自己寫一個簡單的self-cli

首先要有一個基礎模板項目 base-admin

  • 筆者已經上傳到github上了,地址: https://github.com/shuirongshuifu/base-admin
  • base-admin是一個演示的項目,沒有太多東西,實際開發中,這裏基礎模板會有很多東西,比如eslint、prettier等
  • 同時,也可能會有多個模板,比如react技術棧基礎模板、vue技術棧基礎模板、後台基礎模板、前台基礎模板等

如下圖:

需求:

  • 當新項目開啓的時候,我們使用自定義腳手架在命令行執行self-ci create,
  • 會從git倉庫拉取這個base-admin
  • 然後,在命令行中我們可以輸入新項目名
  • 輸入的新項目名,會自動替換模板引擎和修改package.json文件裏面的name
  • 同時,也會自動幫我們執行npm install命令
  • 最後,可以讓我們選擇,是否啓動這個項目(是否npm run dev)

就是把原先需要手動複製粘貼項目,修改項目裏面的內容的步驟,換成了命令行腳本自動化執行了...

自定義腳手架完成效果圖

我們先看一下,完成後的效果圖

對應拉取的創建並修改的新項目

  • 在動態圖中,我們可以看到,左上角拉取了項目,名字叫做 pro-new ,這個明顯也就是我們輸入的新項目的名字
  • 這個是簡單案例,實際項目中,控制枱的交互會多一些

接下來,我們來快速過一下這個腳手架做了那些事情

commander包定製命令行輸入命令

index.js

#!/usr/bin/env node

import { program } from 'commander';
import fs from 'fs-extra';
import app from './app.js';

const pkg = fs.readJsonSync(new URL('./package.json', import.meta.url));

program
  .version(pkg.version, '-v, --version')
  .name('self-cli')
  .description('自定義腳手架工具');

program
  .command('create [app-name]')
  .description('創建一個新的項目')
  .action(app);

program.parse(process.argv);

shelljs包去判斷是否安裝了git、執行git clone命令等

**shelljs 很強大,強的可怕 **

import shell from 'shelljs';

// 檢查 git
if (!shell.which('git')) {
    console.log(chalk.red('❌ 請先安裝 git'));
    shell.exit(1);
}

// 拉取 Git 倉庫 - 使用 git clone
const TEMPLATE_REPO = 'shuirongshuifu/base-admin';
const spinner = ora('正在拉取項目...').start();

// 使用 SSH URL
const repoUrl = `git@github.com:${TEMPLATE_REPO}.git`;
const cloneResult = shell.exec(`git clone ${repoUrl} ${projectName}`, { silent: false });

if (cloneResult.code !== 0) {
    spinner.fail(chalk.red('拉取失敗'));
    console.log(chalk.red('錯誤信息:' + cloneResult.stderr));
    console.log(chalk.yellow('\n提示:請確保已配置 SSH key,或檢查網絡連接'));
    shell.exit(1);
}

進入對應目錄,並安裝項目依賴
// 安裝依賴
console.log(chalk.cyan('正在安裝依賴...'));
const installResult = shell.exec(`cd ${projectName} && npm install`);
if (installResult.code !== 0) {
    console.log(chalk.red('❌ 依賴安裝失敗'));
    console.log(chalk.red('錯誤信息:' + installResult.stderr));
    console.log(chalk.yellow('請手動進入項目目錄執行:npm install'));
    shell.exit(1);
}

console.log(chalk.green('✅ 項目創建完成!'));

ejs包處理模板文件

  // 處理 ejs 模板文件
  console.log(chalk.cyan('正在處理模板文件...'));
  const processEjsFiles = (dir) => {
    const files = fs.readdirSync(dir);
    files.forEach(file => {
      const filePath = path.join(dir, file);
      const stat = fs.statSync(filePath);

      if (stat.isDirectory() && file !== 'node_modules' && file !== '.git') {
        processEjsFiles(filePath);
      } else if (file.endsWith('.ejs')) {
        const template = fs.readFileSync(filePath, 'utf-8');
        const rendered = ejs.render(template, { projectName });
        const destPath = filePath.replace(/\.ejs$/, '');
        fs.writeFileSync(destPath, rendered, 'utf-8');
        fs.unlinkSync(filePath);
      }
    });
  };
  processEjsFiles(projectPath);
  console.log(chalk.green('✅ 模板文件處理完成'));

fs模塊直接修改package.json文件

// 修改 package.json
console.log(chalk.cyan('正在修改 package.json...'));
const pkgPath = path.join(projectPath, 'package.json');
const pkg = await fs.readJson(pkgPath);
pkg.name = projectName;
await fs.writeJson(pkgPath, pkg, { spaces: 2 });
console.log(chalk.green('✅ package.json 修改完成'));

等,不贅述...

重點:self-cli為何能夠被命令行識別?

  • 上述案例的自定義腳手架,代碼並不難,我們思考,為何在命令行執行self-cli命令能夠被識別呢
  • 畢竟self-cli並不是操作系統自帶的命令

命令行識別邏輯順序

  • 當我們在命令行中,輸入xxx的時候,操作系統會進行如下的查詢執行邏輯
  • 比如,先看看這個xxx是不是自帶的內部命令,如 ls dir cd 等(是自帶的就按照自帶的邏輯執行)
  • 不是自帶的,就會去環境變量Path裏面遍歷查找,比如執行了git -v
  • 那麼,發現,環境變量真有,找到對應的Path裏面對應的路徑的值對應的文件夾,再看看文件夾裏面是否有對應的exe或cmd或bat,再交給其執行
  • 先遍歷環境變量裏面有那些文件夾,再到對應文件夾裏面,再次遍歷找對應可執行文件批處理命令(系統先按Path的目錄順序 “逛文件夾”,在每個文件夾裏只看直接文件,找 “命令名 + 可執行後綴” 的文件,找到就用,找不到就換下一個文件夾,全逛完都沒有就報錯)

報錯命令:

C:\Users\lss13>hello
'hello' 不是內部或外部命令,也不是可運行的程序
或批處理文件。

C:\Users\lss13>

對應路徑的確有exe可執行文件

比如,查看java、python、git、node版本也是上述同樣類似的道理

C:\Users\lss13>java -version
java version "1.8.0_201"
Java(TM) SE Runtime Environment (build 1.8.0_201-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.201-b09, mixed mode)

C:\Users\lss13>python --version
Python 3.12.8

C:\Users\lss13>git -v
git version 2.45.2.windows.1

C:\Users\lss13>node -v
v22.12.0

self-cli識別命令,則是當找到node的環境變量path後,在對應文件夾找到了self-cli,如下圖

注意,這裏有三個,分別是

self-cli       # Linux/Mac風格腳本
self-cli.cmd   # Windows CMD批處理腳本(核心)
self-cli.ps1   # PowerShell腳本

所以,就是如下圖的箭頭所示

  • 所以,這裏的本質就是通過 npm link給某個包

npm link介紹

npm link是 npm 專為本地開發 npm 包(比如筆者的self-cli 腳手架)設計的調試工具,核心是通過軟鏈接(類似 Windows 快捷方式)  關聯本地代碼和全局 npm 環境,避免反覆安裝的麻煩

  • 比如這個self-cli 這類自定義 CLI 工具,開發時需要頻繁修改代碼並測試命令效果,npm link 能讓全局執行的 self-cli 命令直接指向本地開發目錄的代碼
  • 改完代碼無需重新全局安裝,直接執行命令就能看到最新效果,大幅提升調試效率

也就是説,npm link在node的環境變量文件夾裏面創建了一個鏈接,讓我們在命令行執行對應的命令的時候,系統能夠識別,能夠找到對應的腳本js文件,做對應的執行處理

在對應的自定義腳手架裏面執行npm link會自動生成可執行文件和軟連接,這樣就能達到全局掛載可使用的效果了

不過,我們還需要在package.json文件裏面,加上bin規則,告知self-cli要執行那個js文件,同時,對應js文件,要加格式固定:#!/usr/bin/env node

即:

package.json

{
  "name": "self-cli",
  "version": "1.2.3",
  "description": "自定義腳手架工具",
  "main": "index.js",
  "type": "module",
  "bin": {
    "self-cli": "./index.js"  // 這個
  },
  "scripts": { ... },
  "license": "ISC",
  "dependencies": { ... }
}

index.js

// 這個
#!/usr/bin/env node 

import { program } from 'commander';
import fs from 'fs-extra';
import app from './app.js';

......

npm link 與 npm install xyz -g的差異

方式 本質 代碼修改後效果 適用場景
npm link 創建軟鏈接(快捷方式) 實時生效,無需額外操作 本地開發、頻繁改代碼
npm install xyz -g 複製文件到全局目錄 需重新安裝才生效 開發完成後安裝最終版本
  • 當然,我們本地開發腳手架,使用npm link去全局鏈接上,方便調試
  • 當這個腳手架self-cli開發完畢後,就可以發到npm上
  • 或者發到公司裏面自己搭建的私服npm
  • 搭建私服npm可以參見筆者的這篇文章:20張圖的保姆級教程,記錄使用Verdaccio在Ubuntu服務器上搭建Npm私服
  • 這樣的話,團隊成員就可以全局下載self-cli
  • 就可以在命令行使用對應命令,拉取gitlab倉庫代碼,自定義創建新項目了

一句話總結前端自定義腳手架

前端自定義腳手架(比如self-cli)是基於nodejs語法的全局CLI工具,核心價值是基於模板一鍵生成標準化且可自定義配置的前端項目,從而達到提效的目的

CLI = 命令行(Command Line)+ 界面(Interface),核心指的是:基於命令行操作的工具或交互方式

筆者的這個演示腳手架,也發佈到npm上面了,不過因為包名不能類似原因,筆者做了修改,現在叫做:s-cli-srsf

地址:https://www.npmjs.com/package/s-cli-srsf?activeTab=readme

大家可以嘗試着

npm install -g s-cli-srsf

然後,就會發現,這次生成的就不是npm link那種鏈接了,是直接安裝的文件夾內容

全局安裝以後,就可以愉快地創建項目了

回顧package.json常用的配置鍵值對

回顧一下知識點

鍵名 類型 描述 示例
name String 包的名稱,必須唯一 "my-package"
version String 版本號,遵循語義化版本控制 "1.0.0"
description String 包的簡短描述 "A sample package"
keywords Array 關鍵詞數組,用於搜索 ["web", "framework"]
homepage String 項目主頁URL "https://example.com"
bugs Object Bug報告地址 {"url": "https://github.com/.../issues"}
license String 許可證類型 "MIT"
author String/Object 作者信息 "Name email@domain.com"
contributors Array 貢獻者列表 [{"name": "John", "email": "..."}]
files Array 發佈時包含的文件 ["dist/", "lib/"]
main String 主入口文件 "index.js"
browser String 瀏覽器端入口文件 "./browser.js"
bin Object 命令行工具配置 {"cli": "./bin/cli.js"}
repository Object 倉庫信息 {"type": "git", "url": "..."}
scripts Object 腳本命令集合 {"start": "node index.js"}
dependencies Object 生產環境依賴 {"express": "^4.17.1"}
devDependencies Object 開發環境依賴 {"jest": "^27.0.0"}
peerDependencies Object 同級依賴 {"react": "^17.0.0"}
optionalDependencies Object 可選依賴 {"fsevents": "^2.3.2"}
engines Object 支持的引擎版本 {"node": "\>=14.0.0"}
os Array 支持的操作系統 ["darwin", "linux"]
cpu Array 支持的CPU架構 ["x64", "arm64"]
private Boolean 是否私有包 true
workspaces Array 工作區配置 ["packages/*"]
user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.