【我要做開源】Vue DevUI開源指南06:手把手帶你開發一個腳手架

語言: CN / TW / HK

本文作者:iel

原文連結:https://juejin.cn/post/7021870182855344142

最近在與村長老師一起做直播,給大家分享vue devui開源元件庫的建設,1-3期以 tree 元件為栗子🌰,介紹瞭如何設計和開發Vue元件: 1. Vue DevUI開源指南01:提交我的第一次pr 1. Vue DevUI開源指南02:實現一個能渲染多層節點的Tree元件 1. Vue DevUI開源指南03:如何給 tree 元件增加展開/收起功能

從第4期開始給大家分享元件庫工程化相關的內容: 1. 【我要做開源】Vue DevUI開源指南04:使用Vite搭建一個支援TypeScript/JSX的Vue3元件庫工程 1. 【我要做開源】Vue DevUI開源指南05:給Vue3元件庫新增VitePress文件系統

後續的直播也會分成兩條線: 1. 一條是元件的設計和實現 1. 另一條是元件庫的工程化

歡迎大家持續關注~

上一期內容回顧

上一期主要給大家分享了兩部分內容: - 使用vitepress搭建元件庫的文件系統 - 給文件系統增加demo展開/收起功能

本期邀請了Vue DevUI團隊的雷同學給大家分享DevUI CLI工具的實現原理。

雷同學是我們Vue DevUI的早期貢獻者,也是Toast元件的田主,早期Vue DevUI元件庫還很不完善,元件的檔案和元件庫的入口檔案都是手動新增的,每次新增一個元件,都非常麻煩,需要 1. 先在devui目錄下建立一堆檔案index.ts/src/xx.tsx等 2. 在devui/vue-devui.ts元件庫入口檔案中新增相應的元件匯出 3. 在sidebar.ts左側元件選單中新增相應的元件配置

而且像vue-devui.ts/sidebar.ts等公共檔案,經常多個田主一起修改,經常導致衝突。

雷同學敏銳得發現了這個問題,並主動提出建立一個DevUI CLI工具來解決這個問題,本期直播雷同學就給大家分享了DevUI CLI的實現原理。

這只是DevUI CLI工具系列的第一期,後續還會繼續深入如何使用DevUI CLI: - 為元件庫生成入口檔案 - 建立元件目錄結構 - 自動生成左側元件選單檔案

以下是原文:

腳手架是為了保證各施工過程順利進行而搭設的工作平臺。按搭設的位置分為外腳手架、裡腳手架;按材料不同可分為木腳手架、竹腳手架、鋼管腳手架;按構造形式分為立杆式腳手架、橋式腳手架、門式腳手架、懸吊式腳手架、掛式腳手架、挑式腳手架、爬式腳手架。 ——百度百科

本質上就是一個便利工具,為一些比較特殊或繁瑣的工作提供輔助,我們這裡需要開發的是一個基於命令列的工具,後文以 cli 代替。

為什麼需要開發一個腳手架?

以下為初期元件庫協同開發時遇到的問題:

  • 元件目錄結構不一致
  • 扁平化目錄
  • src 型目錄
  • 元件產出命名不一致
  • 字首 D 命名
  • 無字首 D 命名
  • 小寫駝峰命名
  • 大寫駝峰命名
  • xxxService 命名
  • useXxxService 命名
  • 元件入口檔案經常衝突

為了解決上述問題,本著為開源社群做貢獻,發光發熱的時候到了,和專案組織人 kagol 溝通了腳手架方案順利通過,Prefect

TODO

  • [x] 建立統一元件結構
  • [x] 建立元件庫入口檔案

技術選型

腳手架 = 命令 + 互動 + 邏輯處理

  • 命令
  • commander 外掛提供命令註冊、引數解析、執行回撥
  • 互動
  • inquirer 外掛用於命令列的互動(問答)
  • 邏輯處理
  • fs-extra 外掛是對 nodejs 檔案 Api 的進一步封裝,便於使用
  • kolorist 外掛用於輸出顏色資訊進行友好提示

初始化 cli

step1 建立 cli 目錄

shell mkdir devui-cli // 建立腳手架目錄 cd devui-cli // 進入腳手架目錄 // 初始化一個 node 專案 npm init // or yarn init

第一步先建立一個目錄來存放我們即將開發的腳手架,作為一個 nodejs 包,需要我們通過 npm 或者 yarn 初始化包的資訊,一律回車即可通過,生成後的目錄結構如下圖。

image.png

step2 建立入口檔案

shell mkdir src echo 'console.log("hello devui-cli")' > src/index.js

step3 安裝所需依賴

shell npm i -D commander inquirer fs-extra kolorist // or yarn add -D commander inquirer fs-extra kolorist

image.png

開發命令指令碼

這裡先給大家梳理下 cli 的執行流程:命令列輸入 devui-cli --> 命令列互動 --> 根據不同引數進行不同操作。

這裡大家可能要問了,命令列如何識別 devui-cli 的?又是如何執行互動操作的?

這裡簡單給大家解答一下,命令列裡面輸入 devui-cli 本質上是執行某一個可執行指令碼,那麼對應我們 node 包來說就是入口檔案 src/index.js 了,所以可以看成是 node src/index.js ,效果是一樣的,只不過第一種更為方便與友好一點。那麼我們直接執行是否就可以了呢?答案肯定不是的,需要在 package.json 裡面配置 bin 屬性來標明指令碼的一個入口。

準備工作結束,接下來開始正式的 cli 指令碼編寫。

配置環境直譯器

```shell

!/usr/bin/env node

```

image.png

部分看官可能會疑惑這句話有什麼用呢?

答案在這裡,若是有使用過 Linux 或者 Unix 的小夥伴們,對於 Shebang 應該不陌生,它是一個符號的名稱 #! 。這個符號通常在 Unix 系統的基本中第一行開頭中出現,用於指明這個指令碼檔案的解釋程式, #!/usr/bin/env node 目的就是告訴作業系統執行這個指令碼的時候,在 /usr/bin 的環配置裡找到 node 直譯器並執行。

註冊命令

配置好環境直譯器之後就可以編寫我們的命令邏輯了。

首先,先註冊下我們需要執行的一些命令以及一些命令引數。

```js

!/usr/bin/env node

import { Command } from 'commander' import { onCreate } from './commands/create'

// 建立命令物件 const program = new Command()

// 註冊命令、引數、回撥 program // 註冊 create 命令 .command('create') // 新增命令描述 .description('建立一個元件模板或配置檔案') // 新增命令引數 -t | --type 表示該引數必填,[type] 表示選填 .option('-t --type ', 建立型別,可選值:component, lib-entry) // 註冊命令回撥 .action(onCreate)

// 執行命令列引數解析 program.parse()

```

建立具體命令目錄,方便統一管理。

shell mkdir src/commands // 命令存放目錄 echo 'export function onCreate() { }' > src/commands/create.js // 建立 create 命令檔案並匯出回撥函式

image.png

image.png

測試指令碼命令

我們可以先在 onCreate 裡面列印一下我們接受到的引數。

js export function onCreate (cmd) { console.log(cmd) }

執行一下指令碼。

shell node src/index.js

image.png

報錯了!!!我們才剛開始就報錯了,是否已經開始崩潰?

穩住別慌,一切在我們的意料之中。這是因為我們編寫的是 node 程式,本應該使用 commonjs 簡稱 CJS 格式,也就是用 requireexports 等語法才能正常使用 node xxx.js 進行啟動,但是我們使用了新一代的 esmodule 簡稱 ESM 格式,所以 node 臉盲了!那麼有什麼辦法呢?

解決辦法一:將 .js 改成 .mjs 。why? 很明顯因為 ESM 和 CJS 的載入方式不同,為了更好區分這兩種不同的載入方式,所以建立了 .mjs 的檔案型別,旨在 Module javascript.mjs 就是表示當前檔案用 ESM 的方式進行載入,如果是普通的 .js 檔案,則採用 CJS 的方式載入。

解決辦法二:通過一些模組打包器進行轉換為 node 熟悉的 cjs 格式,然後再進行開發。

這裡選擇第二種方式,原因是採用打包器我們可以對程式碼進行其他操作,例如:壓縮、轉換等。

模組打包器的話這裡採用 esbuild ,理由就是:快捷、方便。

shell npm i -D esbuild // or yarn add -D esbuild

安裝好後先看下命令列幫助文件。

shell npx esbuild -h // or yarn esbuild -h

執行完後會看到以下幫助資訊。

image.png

看過幫助資訊後我們加入如下命令:

js { // --bundle 標識打包的入口檔案 // --format 轉換為目標格式程式碼 // --platform 目標平臺,預設 browser // --outdir 輸出目錄 // 開發時實時編譯 "dev": "esbuild --bundle ./src/index.js --format=cjs --platform=node --outdir=./lib --watch", // 打包命令 "build": "esbuild --bundle ./src/index.js --format=cjs --platform=node --outdir=./lib", // 執行 create 命令,如果有多個命令,可以去掉 create ,使用時再傳入 "cli": "node ./lib/index.js create" }

image.png

執行下 dev 命令,然後重新開一個 shell 再執行 cli 命令。

image.png

shell yarn cli // or npm run cli

image.png

輸出了一個 {} ,這是我們列印的 cmd 入參,我們並沒有填入任何引數,所以解析後是一個空物件,接下來傳入 type 引數再看看。

shell yarn cli -t component // -t 是 --type 的別名 // or npm run cli -- -t component // -- 是 npm run 指令碼傳參時需要加的,類似於引數透傳給指令碼

image.png

image.png

現在已經能夠正常獲取到命令引數了,證明命令註冊成功,後面可以繼續實現我們的互動邏輯。

完善 create 命令

接下來就是進一步完善我們的命令互動了,以 component 為例,程式碼如下:

```js import inquirer from 'inquirer' import { red } from 'kolorist'

// create type 支援項 const CREATE_TYPES = ['component', 'lib-entry'] // 文件分類 const DOCS_CATEGORIES = ['通用', '導航', '反饋', '資料錄入', '資料展示', '佈局']

export async function onCreate(cmd = {}) { let { type } = cmd

// 如果沒有在命令引數裡帶入 type 那麼就詢問一次 if (!type) { const result = await inquirer.prompt([ { // 用於獲取後的屬性名 name: 'type', // 互動方式為列表單選 type: 'list', // 提示資訊 message: '(必填)請選擇建立型別:', // 選項列表 choices: CREATE_TYPES, // 預設值,這裡是索引下標 default: 0 } ]) // 賦值 type type = result.type }

// 如果獲取的型別不在我們支援範圍內,那麼輸出錯誤提示並重新選擇 if (CREATE_TYPES.every((t) => type !== t)) { console.log( red(當前型別僅支援:${CREATE_TYPES.join(', ')},收到不在支援範圍內的 "${type}",請重新選擇!) ) return onCreate() }

try { switch (type) { case 'component': // 如果是元件,我們還需要收集一些資訊 const info = await inquirer.prompt([ { name: 'name', type: 'input', message: '(必填)請輸入元件 name ,將用作目錄及檔名:', validate: (value) => { if (value.trim() === '') { return '元件 name 是必填項!' } return true } }, { name: 'title', type: 'input', message: '(必填)請輸入元件中文名稱,將用作文件列表顯示:', validate: (value) => { if (value.trim() === '') { return '元件名稱是必填項!' } return true } }, { name: 'category', type: 'list', message: '(必填)請選擇元件分類,將用作文件列表分類:', choices: DOCS_CATEGORIES, default: 0 } ])

    createComponent(info)
    break
  case 'lib-entry':
    createLibEntry()
    break
  default:
    break
}

} catch (e) { console.log(red('✖') + e.toString()) process.exit(1) } }

function createComponent(info) { // 輸出收集到的元件資訊 console.log(info) }

function createLibEntry() { console.log('create lib-entry file.') } ```

ok,接下來嘗試執行一下我們的指令碼。

先嚐試錯誤的型別:

shell yarn cli -t error // or npm run cli -- -t error

image.png

按照我們的預想提示了錯誤資訊並讓我們重新選擇型別。

接下來嘗試正確的型別:

shell yarn cli -t component // or npm run cli -- -t component

image.png

因為指定了型別為元件,所以現在需要收集一下即將建立的元件資訊。

image.png

按照提示資訊一步一步完成輸入最終獲取到了我們需要的資料,接下來就是模板的生成了。

未完待續

盡情期待後續更精彩的分享!