您好,登錄后才能下訂單哦!
1 寫在前面的話
搭一個腳手架,考驗了你的 nodejs 水平、工程化能力、以及工具服務的設計能力,是前端進階不可或缺的過程
筆者在開發 cli 的過程中,調研流行的 cli 并形成最佳實踐,本文旨在用最短的篇幅實現主要功能,揭露核心原理,同時提供 demo 倉庫與大家學習探討。
通篇閱讀大約需要 10 分鐘,基于本教程自己擼一個 cli 大約需要花費 15 分鐘
2 腳手架的雛形
其實腳手架的初衷,就是提供一個最佳實踐的基礎模板,因此模板拷貝是其核心功能
幾年前我曾寫過一個極簡的腳手架,大該干了這么一件事兒
npm publish 一個全局安裝的包
執行命令時,wget 我云服務上的一個壓縮包,并在當前文件夾下解壓
一個命令,就可以把我預設的完整的工程目錄創建好,特別方便效率。
我想,這應該算是一個雛形腳手架吧
3 腳手架需要考慮的
上面雛形腳手架可以很好的服務于個人需求,但是畢竟過于干癟和簡陋,要想成為被大家廣泛接受的工具,還需要完善。
大家熟知的 vue-cli create-react-app @tarojs/cli umi 最基本功能:首先提出一些列問題選項,然后為你的新建項目提供一份模板并安裝依賴,再提供調試構建命令
沒錯,最核心的部分就是這個思路;但如果要做成一個可伸縮的、用戶友好的,還需考慮這些需求:
模板支持版本管理
支持擴展新模板
自動檢測版本更新
根據用戶選擇,生成個性化模板
友好的UI界面
構建功能獨立,可因模板而異 (如區分H5/PC/weapp/RN)
多人合作項目,能確保構建結果一致
看起來信息量有點大,但其實都并不晦澀,我們一一說明一下意圖
3.1 模板支持版本管理
比如用戶使用 v1.0.0 的模板創建了項目,半年后,已經迭代升級到了 v2.0.0。我們需要依舊能夠找到 v1.0.0 版本,因為老用戶不想或者不方便升級。
像我之前的雛形腳手架,將模板打一個壓縮包放在云服務器上是不可行的,一旦更新就全量替換了
npm 倉庫天然支持版本管理,因此將模板發布到 npm 上自然解決了這個問題?(非開源項目,可考慮自建倉庫或者私有的倉庫)
3.2 支持擴展新模板
比如我們一開始我們的腳手架支持 H5 的模板。
半年后,隨著業務發展,需支持微信小程序的模板。
此時,我們無需額外再開發一個 cli,而是讓 cli 一開始設計的就支持擴展,這符合了開放封閉的設計原則
3.3 自動檢測版本更新
npm 提供了一些命令來檢測包的版本,比如你 npm view react version 返回 16.9.0,告知你最新版本
借此,可以判斷用戶目前安裝的是否最新版本,并提示用戶更新
3.4 根據用戶選擇,生成個性化模板
模板雖說是為了統一,但也要在統一中支持差異,可通過問詢用戶,來提供差異化支持,比如:
這些問詢的結果,將影響我們最終的模板,比如我們根據是否 TypeScript 會在兩套預設的模板中選一個套,將用戶輸入的「項目介紹」插入 package.json 的 description 字段等等
3.5 友好的UI界面
合適的格式、顏色、字體、進圖條等,給與用戶良好的信息反饋
下文會介紹一些常用的庫,來提供這些功能
3.6 構建功能獨立,可因模板而異
我們通常使用 webpack 來構建/調試,對于不同的模板,構建流程存在較大差異,我們需要支持為不同的模板配置不同的構建
因此構建能力也被抽離成單獨的 npm 包,模板中可指定其構建包
3.7 多人合作項目,能確保構建結果一致
因為存在多版本,我們需要約束,讓所有項目的貢獻者的產出是一致的
其核心原則就是:針對那些可能導致差異的因素,我們都收錄到工程中,讓 git 倉庫記錄,從而實現同樣,因此,現在流行的腳手架,如 umi taro,都將?構建能力 local 化到本地工程中,后續會做詳細闡明
4 腳手架的三類包
一個被實踐檢驗,能夠符合上述需求的腳手架架構,其實非常簡單,首先我們拆分成三類 npm 包:
包 功能 安裝位置 備注 全局命令包 就像一個大腦,負責響應全局命令,并進行調度 全局包路徑 global 安裝,提供全局命令 模板插件包 初始化工程所拷貝的模板 某個約定路徑,如 ~/.maoda 模板可隨業務擴展 構建插件包 提供構建(webpack)能力 工程內?(目前主流腳手架都改用此方案)?不同模板可使用同一構建包,也可不同 注:構建插件包,早期很多腳手架都把它放在工程外,比如放在全局,優勢是多工程可復用一套 webpack 能力,但弊端也暴露出來,即在多人協同開發的項目中,由于構建插件包不在工程里沒能被 git 倉庫收錄,導致一些不可預期的差異結果。
其調度關系如下:
5 全局命令包
前面說了一通理論,下面開始正式搭建
全局命令包的功能:負責接收全局命令,并調度。
比如我做的 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 類似,只不過是執行調試 5.1 cli 開發中值得收藏的一些第三方調料包
重要性 包名稱 功能 必要 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 5.2 命令解析與分發
命令的解析與分發,是「全局命令包」的核心功能,其過程比較簡單。大家也可以直接看倉庫 cli-tpl?(全部功能壓縮到大約300行代碼)
cli 版本更新判斷:
先獲取本 package.json 中的 version
再通過 npm view cli-tpl version 命令查詢當前 npm 庫最新版本
兩者比較得出結論,提醒用戶更新
解析用戶命令
通過 process.argv[2] 獲取到用戶執行的實際命令,比如 dcli install 可拿到 install?(正式版推薦使用 minimist 解析參數)
處理命令
比如 install 命令,則通過 require 動態映射 install.js 文件來處理該邏輯
注:require 支持動態名稱,如 require('./scripts/' + command) 這樣,如果 command 是 install 則映射執行 script/install.js 文件
接下來我們看下 4 個核心命令,主要是:
命令 效果 install 幫用戶安裝/升級一個「模板插件包」 init 幫用戶初始化一個工程,并拷貝模板 build 調用工程中的「構建插件包」,幫用戶webpack構建 dev 幫用戶啟動 devServer 進行調試 下面逐一闡述每個命令的實現過程以及效果:
5.3?install命令:安裝一個「模板插件包」
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
5.4?init命令: 選一個「模板插件包」來初始化一個新工程
這是一個腳手架高頻而核心的功能
dcli?init 復制代碼
此時會分發去執行 script/init.js 文件,我們看看其邏輯
查詢硬盤緩存目錄 ~/.maoda 下的 package.json 文件,讀取其中 dependacies 字段,拿到已安裝的「模板插件包」
如果一個都沒安裝,則提示用戶要先 install
讓用戶選擇一套模板
利用 inquery 庫發起對話,羅列出已裝模板,讓用戶選擇,比如上圖的 gen-pc gen-h6 gen-tpl
觸發模板初始化流程
比如用戶選擇了 gen-tpl 這個模板,則用 yeoman-environment 這個庫去執行緩存目錄里的這個包 ~/.maoda/gen-tpl/index.js
注:這里相當于跨目錄的兩個 js 文件引用執行,用到了之前說的 import-from 這個庫
「模板插件包」被執行,則啟動了常規的模板拷貝過程?(后面展開細說)
這里直接用包名稱做選項,為了演示更直觀,實際通常用包的 description 做選項,更友好一些,比如 gen-pc 包可能描述為 生成PC模板
5.5?build命令:在工程里執行構建
dcli?build復制代碼
確定工程目錄
工程目錄即執行目錄,通過 process.cwd() 獲取
讀取該工程所用的構建插件
讀取工程中約定的配置文件,本demo中為 maoda.js?(采用約定式的配置,類似 webpack.config.js .babelrc .prettierrc)
讀取 maoda.js 中 builder 配置項?(即指定的構建插件包),比如本 demo 中指定為 build-tpl
如果有的話,讀取自定義 webpack 配置?(約定為 webpackCustom 字段,后續會被合并/覆蓋到默認 webpack 配置上)
使用制定的構建插件包來進行 webpack 打包
判斷工程中是否已經安裝 build-tpl
未安裝,則在工程中路徑中執行 npm install?(或 yarn add,此處有個小技巧,可根據用戶工程中 lock 文件的類型,判斷用戶使用的 npm 還是 yarn)
已安裝,則直接執行 build-tpl
通常,我們用配置文件指明「構建插件包」,也可以直接在命令里指明,比如 dcli build --builder=build-h6;后者往往適用于一套代碼打包出多種結果,如京東的 Taro cli
平時大家用慣了 npm run build yarn build,只需在我們的模板中的 package.json 添加一行:
{?"script":?{ ++?"build":?"dcli?build" ?} } 復制代碼
5.6?dev命令:啟動 devServer 進行調試
類似 build 只不過 webpack 配置不同,此處略
6 模板插件包
核心功能:提供模板文件夾 + 文件夾的拷貝。這里同樣提供了一個樣例工程 gen-tpl?(僅 50 行代碼)
處理流程如下:
詢問用戶,并獲取反饋的答案
比如工程名是什么,描述一下你的工程,是否使用 TypeScript,是否使用 Sass/Less/Stylus 等
根據用戶的答案,拷貝對應的模板,細分兩種拷貝
直接拷貝,直接把模板插件包里的文件夾/文件,拷貝到用戶工程目錄
填充模板拷貝,將用戶答案,填充到文檔的對應位置,類似 WebpackHTMLPlugin、ejs,如將 name: <%= packageName %> 填充成 name: 我的工程
在工程中執行 npm 依賴的安裝
【重點來了】看似流程蠻多,其實只用一個現成的輪子即可搞定,即 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 來創建,但并非必要,此處就不展開說了
7 構建插件包
同樣我們提供了一個構建插件包的模板 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 在構建小程序代碼時候,自己創建一套工具
8 寫在最后的話
筆者認為,只有夠精簡,才能降低入門門檻,才能強化記憶;因此,本文的案例,在成熟的腳手架上進行不斷刪減,剔除掉哪些徒增記憶負擔的部分,只保留精髓和核心,旨在快速在腦海里建模出一個企業級腳手架
同時提供了腳手架 3 個組成部分的 倉庫/npm 包,以增加可操作性
如需引用與實際開發中,我們需要繼續豐滿其血肉,包括但不限于:
異常處理 (如一些邊界情況)
webpack 配置部分需完善?(本 demo 中 webpack.config 是空的)
UI 和提升語可更友好
根據業務需求,擴展額外的命令,比如卸載包,發布cdn等
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。