搭一個腳手架,考驗了你的 nodejs 水平、工程化能力、以及工具服務的設計能力,是前端進階不可或缺的過程前端
筆者在開發 cli 的過程當中,調研流行的 cli 並造成最佳實踐,本文旨在用最短的篇幅實現主要功能,揭露核心原理,同時提供 demo 倉庫與你們學習探討。vue
通篇閱讀大約須要 10 分鐘,基於本教程本身擼一個 cli 大約須要花費 15 分鐘node
其實腳手架的初衷,就是提供一個最佳實踐的基礎模板,所以模板拷貝是其核心功能webpack
幾年前我曾寫過一個極簡的腳手架,大該幹了這麼一件事兒git
一個命令,就能夠把我預設的完整的工程目錄建立好,特別方便效率。github
我想,這應該算是一個雛形腳手架吧web
上面雛形腳手架能夠很好的服務於我的需求,可是畢竟過於乾癟和簡陋,要想成爲被你們普遍接受的工具,還須要完善。chrome
你們熟知的 vue-cli
create-react-app
@tarojs/cli
umi
最基本功能:首先提出一些列問題選項,而後爲你的新建項目提供一份模板並安裝依賴,再提供調試構建命令vue-cli
沒錯,最核心的部分就是這個思路;但若是要作成一個可伸縮的、用戶友好的,還需考慮這些需求:
看起來信息量有點大,但其實都並不晦澀,咱們一一說明一下意圖
好比用戶使用 v1.0.0 的模板建立了項目,半年後,已經迭代升級到了 v2.0.0。咱們須要依舊可以找到 v1.0.0 版本,由於老用戶不想或者不方便升級。
像我以前的雛形腳手架,將模板打一個壓縮包放在雲服務器上是不可行的,一旦更新就全量替換了
npm 倉庫自然支持版本管理,所以將模板發佈到 npm 上天然解決了這個問題 (非開源項目,可考慮自建倉庫或者私有的倉庫)
好比咱們一開始咱們的腳手架支持 H5 的模板。
半年後,隨着業務發展,需支持微信小程序的模板。
此時,咱們無需額外再開發一個 cli,而是讓 cli 一開始設計的就支持擴展,這符合了開放封閉的設計原則
npm 提供了一些命令來檢測包的版本,好比你 npm view react version
返回 16.9.0
,告知你最新版本
藉此,能夠判斷用戶目前安裝的是否最新版本,並提示用戶更新
模板雖然說是爲了統一,但也要在統一中支持差別,可經過問詢用戶,來提供差別化支持,好比:
這些問詢的結果,將影響咱們最終的模板,好比咱們根據是否 TypeScript 會在兩套預設的模板中選一個套,將用戶輸入的「項目介紹」插入 package.json 的 description 字段等等
合適的格式、顏色、字體、進圖條等,給與用戶良好的信息反饋
下文會介紹一些經常使用的庫,來提供這些功能
咱們一般使用 webpack 來構建/調試,對於不一樣的模板,構建流程存在較大差別,咱們須要支持爲不一樣的模板配置不一樣的構建
所以構建能力也被抽離成單獨的 npm 包,模板中可指定其構建包
由於存在多版本,咱們須要約束,讓全部項目的貢獻者的產出是一致的
其核心原則就是:針對那些可能致使差別的因素,咱們都收錄到工程中,讓 git 倉庫記錄,從而實現一樣,所以,如今流行的腳手架,如 umi
taro
,都將 構建能力 local 化到本地工程中,後續會作詳細闡明
一個被實踐檢驗,可以符合上述需求的腳手架架構,其實很是簡單,首先咱們拆分紅三類 npm 包:
包 | 功能 | 安裝位置 | 備註 |
---|---|---|---|
全局命令包 | 就像一個大腦,負責響應全局命令,並進行調度 | 全局包路徑 | global 安裝,提供全局命令 |
模板插件包 | 初始化工程所拷貝的模板 | 某個約定路徑,如 ~/.maoda |
模板可隨業務擴展 |
構建插件包 | 提供構建(webpack)能力 | 工程內 (目前主流腳手架都改用此方案) | 不一樣模板可以使用同一構建包,也可不一樣 |
注:構建插件包,早期不少腳手架都把它放在工程外,好比放在全局,優點是多工程可複用一套 webpack 能力,但弊端也暴露出來,即在多人協同開發的項目中,因爲構建插件包不在工程裏沒能被 git 倉庫收錄,致使一些不可預期的差別結果。
其調度關係以下:
前面說了一通理論,下面開始正式搭建
全局命令包的功能:負責接收全局命令,並調度。
好比我作的 cli 的模板 demo cli-tpl
npm i cli-tpl -g # 或 yarn global add cli-tpl 複製代碼
全局安裝後,暴露出一個 dcli
命令 (本身隨便取的名字),該命令有如下典型功能:
暴露全局命令經過 package.json 中 bin 來指定,可參考個人 demo
命令 | 效果 |
---|---|
dcli install [pkgName] |
安裝一個「模板插件包」到 ~/.maoda 路徑,若是已經安裝再執行,則詢問更新到最新版,如安裝 dcli install gen-tpl |
dcli init |
以某個模板初始化一個新工程,執行後會讓你從已裝模板裏選擇 |
dcli build |
在工程根目錄執行 (或寫進工程的 scripts 裏),嘗試讀取工程依賴的「構建插件包」並執行構建 |
dcli dev |
與 dcli build 相似,只不過是執行調試 |
重要性 | 包名稱 | 功能 |
---|---|---|
必要 | minimist | 解析用戶命令,將 process.argv 解析成對象 |
必要 | fs-extra | 對 fs 庫的擴展,支持 promise |
必要 | chalk | 讓你 console.log 出來的字帶顏色,好比成功時的綠色字 |
必要 | import-from | 相似 require,但支持指定目錄,讓你能夠跨工程目錄進行 require,好比全局包想引用工程路徑下的內容 |
必要 | resolve-from | 同上,只不過是 require.resolve |
必要 | inquirer | 詢問用戶並記錄反饋結果,界面互動的神器 |
必要 | yeoman-environment | 【核心】用於執行一個「模板插件包」,後文詳細描述 |
錦上添花 | easy-table | 相似 console.table,輸出漂亮的表格 |
錦上添花 | ora | 提供 loading 菊花 |
錦上添花 | semver | 提供版本比較 |
錦上添花 | figlet | console.log出一個漂亮的大logo |
錦上添花 | cross-spawn | 跨平臺的child_process (跨 Windows/Mac) |
錦上添花 | osenv | 跨平臺的系統信息 |
錦上添花 | open | 跨平臺打開 app,好比調試的時候開打 chrome |
命令的解析與分發,是「全局命令包」的核心功能,其過程比較簡單。你們也能夠直接看倉庫 cli-tpl (所有功能壓縮到大約300行代碼)
npm view cli-tpl version
命令查詢當前 npm 庫最新版本dcli install
可拿到 install
(正式版推薦使用 minimist 解析參數)install.js
文件來處理該邏輯require('./scripts/' + command)
這樣,若是 command 是 install
則映射執行 script/install.js
文件接下來咱們看下 4 個核心命令,主要是:
命令 | 效果 |
---|---|
install | 幫用戶安裝/升級一個「模板插件包」 |
init | 幫用戶初始化一個工程,並拷貝模板 |
build | 調用工程中的「構建插件包」,幫用戶webpack構建 |
dev | 幫用戶啓動 devServer 進行調試 |
下面逐一闡述每一個命令的實現過程以及效果:
install 意思就是把這個模板插件包下載到硬盤;此處我作了一個最小功能的 demo 包 gen-tpl (後文詳細分解) 來輔助講解
dcli install gen-tpl
複製代碼
核心處理流程以下:
~/.maoda
下是否已經有安裝過 gen-tpl
包
~/.maoda
目錄下執行 npm install)execSync('npm i gen-tpl@latest -S', { cwd: '~/.maoda' })
咱們能夠爲「模板插件包」的名稱作一個約定,即具有固定的前綴,諸如
gen-xxx
這是一個腳手架高頻而核心的功能
dcli init
複製代碼
此時會分發去執行 script/init.js
文件,咱們看看其邏輯
~/.maoda
下的 package.json
文件,讀取其中 dependacies
字段,拿到已安裝的「模板插件包」
inquery
庫發起對話,羅列出已裝模板,讓用戶選擇,好比上圖的 gen-pc
gen-h5
gen-tpl
gen-tpl
這個模板,則用 yeoman-environment
這個庫去執行緩存目錄裏的這個包 ~/.maoda/gen-tpl/index.js
import-from
這個庫這裏直接用包名稱作選項,爲了演示更直觀,實際一般用包的 description 作選項,更友好一些,好比
gen-pc
包可能描述爲生成PC模板
dcli build
複製代碼
maoda.js
(採用約定式的配置,相似 webpack.config.js
.babelrc
.prettierrc
)maoda.js
中 builder
配置項 (即指定的構建插件包),好比本 demo 中指定爲 build-tpl
build-tpl
npm install
(或 yarn add,此處有個小技巧,可根據用戶工程中 lock 文件的類型,判斷用戶使用的 npm 仍是 yarn)build-tpl
一般,咱們用配置文件指明「構建插件包」,也能夠直接在命令裏指明,好比 dcli build --builder=build-h5;後者每每適用於一套代碼打包出多種結果,如京東的 Taro cli
平時你們用慣了 npm run build
yarn build
,只需在咱們的模板中的 package.json
添加一行:
{ "script": { ++ "build": "dcli build" } } 複製代碼
相似 build 只不過 webpack 配置不一樣,此處略
核心功能:提供模板文件夾 + 文件夾的拷貝。這裏一樣提供了一個樣例工程 gen-tpl (僅 50 行代碼)
處理流程以下:
name: <%= packageName %>
填充成 name: 個人工程
【重點來了】看似流程蠻多,其實只用一個現成的輪子便可搞定,即 yeoman-generator,它幫咱們把這些過程都封裝好了,咱們只需繼承基類,並寫幾個預設的生命週期函數便可,無腦到使人髮指 (細節處理,可參考模板倉庫)
module.exports = class extends Generator { // 【問詢環節】 prompting() { return this.prompt([ { type: 'input', name: 'appName', message: '請輸入項目名稱:', }, { type: 'list', choices: ['Javascript', 'TypeScript'], name: 'language', message: '請選擇項目語言', default: 'TypeScript', }, ]).then(answers => { this.answers = answers }) } // 【模板拷貝】 writing() { // 從模板路徑拷貝到工程路徑 this.fs.copy(this.templatePath(), this.destinationPath()) } // 【安裝依賴】 install() { this.installDependencies() } end() { this.log('happy coding!') } } 複製代碼
很明顯,「模板插件包」導出的是一個 class,咱們須要經過上文提到的「全局命令包」裏的 yeoman-environment
來啓動:
// 【節選自 全局命令包 init 命令,略修改以增長可讀性】 yoemanEnv.register(resolveFrom('./maoda', 'gen-tpl'), 'gen-tpl') yoemanEnv.run('gen-tpl', (e, d) => { d && this.console('happy coding', 'green') }) 複製代碼
這裏一樣用到前文提到的 resolve-from
包,進行跨目錄的引用解析
yeoman 是一個比較完善的生態,模板插件包可用 yeoman 提供的全局命令 yo 來建立,但並不是必要,此處就不展開說了
一樣咱們提供了一個構建插件包的模板 build-tpl (20行代碼,啓動 webpack),webpack 配置都是空的,你們在開發過程當中可自行定製
構建插件包其實核心就是 webpack 能力,webpack 能力這裏就不展開說了,這裏只描述一下調用關係
以 dcli build
爲例,「全局命令包」在收到 build 命令後,啓動「構建插件包」
importFrom(process.cwd(), 'build-tpl') 複製代碼
沒錯,就是這麼簡單,import-from 庫能跨文件目錄,指定使用特定目錄的文件;使得全局包能夠直接去執行工程目錄的包 效果與同工程下 require('build-tpl')
同樣
此處也可使用 import-cwd 庫
而 build-tpl 這個構建插件包,負責將內置的 webpack.config.js 與用戶工程下自定義的 webpackCustom 進行 merge,而後執行 webpack 流程
固然,構建工具不必定非要使用 webpack,好比能夠選擇 rollup 或者像 Taro 在構建小程序代碼時候,本身建立一套工具
筆者認爲,只有夠精簡,才能下降入門門檻,才能強化記憶;所以,本文的案例,在成熟的腳手架上進行不斷刪減,剔除掉哪些徒增記憶負擔的部分,只保留精髓和核心,旨在快速在腦海裏建模出一個企業級腳手架
同時提供了腳手架 3 個組成部分的 倉庫/npm 包,以增長可操做性
如需引用與實際開發中,咱們須要繼續豐滿其血肉,包括但不限於:
文章博客地址:github.com/imaoda/js-f… 歡迎批評指正