手把手15分鐘搭一個(gè)企業(yè)級(jí)腳手架
1 寫在前面的話
搭一個(gè)腳手架,考驗(yàn)了你的 nodejs 水平、工程化能力、以及工具服務(wù)的設(shè)計(jì)能力,是前端進(jìn)階不可或缺的過(guò)程
筆者在開(kāi)發(fā) cli 的過(guò)程中,調(diào)研流行的 cli 并形成最佳實(shí)踐,本文旨在用最短的篇幅實(shí)現(xiàn)主要功能,揭露核心原理,同時(shí)提供 demo 倉(cāng)庫(kù)與大家學(xué)習(xí)探討。
通篇閱讀大約需要 10 分鐘,基于本教程自己擼一個(gè) cli 大約需要花費(fèi) 15 分鐘
2 腳手架的雛形
其實(shí)腳手架的初衷,就是提供一個(gè)最佳實(shí)踐的基礎(chǔ)模板,因此模板拷貝是其核心功能
幾年前我曾寫過(guò)一個(gè)極簡(jiǎn)的腳手架,大該干了這么一件事兒
- npm publish 一個(gè)全局安裝的包
- 執(zhí)行命令時(shí),wget 我云服務(wù)上的一個(gè)壓縮包,并在當(dāng)前文件夾下解壓
一個(gè)命令,就可以把我預(yù)設(shè)的完整的工程目錄創(chuàng)建好,特別方便效率。
我想,這應(yīng)該算是一個(gè)雛形腳手架吧
3 腳手架需要考慮的
上面雛形腳手架可以很好的服務(wù)于個(gè)人需求,但是畢竟過(guò)于干癟和簡(jiǎn)陋,要想成為被大家廣泛接受的工具,還需要完善。
大家熟知的 vue-cli create-react-app @tarojs/cli umi 最基本功能:首先提出一些列問(wèn)題選項(xiàng),然后為你的新建項(xiàng)目提供一份模板并安裝依賴,再提供調(diào)試構(gòu)建命令
沒(méi)錯(cuò),最核心的部分就是這個(gè)思路;但如果要做成一個(gè)可伸縮的、用戶友好的,還需考慮這些需求:
- 模板支持版本管理
- 支持?jǐn)U展新模板
- 自動(dòng)檢測(cè)版本更新
- 根據(jù)用戶選擇,生成個(gè)性化模板
- 友好的UI界面
- 構(gòu)建功能獨(dú)立,可因模板而異 (如區(qū)分H5/PC/weapp/RN)
- 多人合作項(xiàng)目,能確保構(gòu)建結(jié)果一致
看起來(lái)信息量有點(diǎn)大,但其實(shí)都并不晦澀,我們一一說(shuō)明一下意圖
3.1 模板支持版本管理
比如用戶使用 v1.0.0 的模板創(chuàng)建了項(xiàng)目,半年后,已經(jīng)迭代升級(jí)到了 v2.0.0。我們需要依舊能夠找到 v1.0.0 版本,因?yàn)槔嫌脩舨幌牖蛘卟环奖闵?jí)。
像我之前的雛形腳手架,將模板打一個(gè)壓縮包放在云服務(wù)器上是不可行的,一旦更新就全量替換了
npm 倉(cāng)庫(kù)天然支持版本管理,因此將模板發(fā)布到 npm 上自然解決了這個(gè)問(wèn)題 (非開(kāi)源項(xiàng)目,可考慮自建倉(cāng)庫(kù)或者私有的倉(cāng)庫(kù))
3.2 支持?jǐn)U展新模板
比如我們一開(kāi)始我們的腳手架支持 H5 的模板。
半年后,隨著業(yè)務(wù)發(fā)展,需支持微信小程序的模板。
此時(shí),我們無(wú)需額外再開(kāi)發(fā)一個(gè) cli,而是讓 cli 一開(kāi)始設(shè)計(jì)的就支持?jǐn)U展,這符合了開(kāi)放封閉的設(shè)計(jì)原則
3.3 自動(dòng)檢測(cè)版本更新
npm 提供了一些命令來(lái)檢測(cè)包的版本,比如你 npm view react version 返回 16.9.0,告知你最新版本
借此,可以判斷用戶目前安裝的是否最新版本,并提示用戶更新
3.4 根據(jù)用戶選擇,生成個(gè)性化模板
模板雖說(shuō)是為了統(tǒng)一,但也要在統(tǒng)一中支持差異,可通過(guò)問(wèn)詢用戶,來(lái)提供差異化支持,比如:
這些問(wèn)詢的結(jié)果,將影響我們最終的模板,比如我們根據(jù)是否 TypeScript 會(huì)在兩套預(yù)設(shè)的模板中選一個(gè)套,將用戶輸入的「項(xiàng)目介紹」插入 package.json 的 description 字段等等
3.5 友好的UI界面
合適的格式、顏色、字體、進(jìn)圖條等,給與用戶良好的信息反饋
下文會(huì)介紹一些常用的庫(kù),來(lái)提供這些功能
3.6 構(gòu)建功能獨(dú)立,可因模板而異
我們通常使用 webpack 來(lái)構(gòu)建/調(diào)試,對(duì)于不同的模板,構(gòu)建流程存在較大差異,我們需要支持為不同的模板配置不同的構(gòu)建
因此構(gòu)建能力也被抽離成單獨(dú)的 npm 包,模板中可指定其構(gòu)建包
3.7 多人合作項(xiàng)目,能確保構(gòu)建結(jié)果一致
因?yàn)榇嬖诙喟姹?,我們需要約束,讓所有項(xiàng)目的貢獻(xiàn)者的產(chǎn)出是一致的
其核心原則就是:針對(duì)那些可能導(dǎo)致差異的因素,我們都收錄到工程中,讓 git 倉(cāng)庫(kù)記錄,從而實(shí)現(xiàn)同樣,因此,現(xiàn)在流行的腳手架,如 umi taro,都將 構(gòu)建能力 local 化到本地工程中,后續(xù)會(huì)做詳細(xì)闡明
4 腳手架的三類包
一個(gè)被實(shí)踐檢驗(yàn),能夠符合上述需求的腳手架架構(gòu),其實(shí)非常簡(jiǎn)單,首先我們拆分成三類 npm 包:
包 | 功能 | 安裝位置 | 備注 |
---|---|---|---|
全局命令包 | 就像一個(gè)大腦,負(fù)責(zé)響應(yīng)全局命令,并進(jìn)行調(diào)度 | 全局包路徑 | global 安裝,提供全局命令 |
模板插件包 | 初始化工程所拷貝的模板 | 某個(gè)約定路徑,如 ~/.maoda |
模板可隨業(yè)務(wù)擴(kuò)展 |
構(gòu)建插件包 | 提供構(gòu)建(webpack)能力 | 工程內(nèi) (目前主流腳手架都改用此方案) | 不同模板可使用同一構(gòu)建包,也可不同 |
注:構(gòu)建插件包,早期很多腳手架都把它放在工程外,比如放在全局,優(yōu)勢(shì)是多工程可復(fù)用一套 webpack 能力,但弊端也暴露出來(lái),即在多人協(xié)同開(kāi)發(fā)的項(xiàng)目中,由于構(gòu)建插件包不在工程里沒(méi)能被 git 倉(cāng)庫(kù)收錄,導(dǎo)致一些不可預(yù)期的差異結(jié)果。
其調(diào)度關(guān)系如下:
5 全局命令包
前面說(shuō)了一通理論,下面開(kāi)始正式搭建
全局命令包的功能:負(fù)責(zé)接收全局命令,并調(diào)度。
比如我做的 cli 的模板 demo cli-tpl
npm i cli-tpl -g # 或 yarn global add cli-tpl
全局安裝后,暴露出一個(gè) dcli 命令 (自己隨便取的名字),該命令有以下典型功能:
暴露全局命令通過(guò) package.json 中 bin 來(lái)指定,可參考我的 demo
命令 | 效果 |
---|---|
dcli install [pkgName] |
安裝一個(gè)「模板插件包」到 ~/.maoda 路徑,如果已經(jīng)安裝再執(zhí)行,則詢問(wèn)更新到最新版,如安裝 dcli install gen-tpl |
dcli init |
以某個(gè)模板初始化一個(gè)新工程,執(zhí)行后會(huì)讓你從已裝模板里選擇 |
dcli build |
在工程根目錄執(zhí)行 (或?qū)戇M(jìn)工程的 scripts 里),嘗試讀取工程依賴的「構(gòu)建插件包」并執(zhí)行構(gòu)建 |
dcli dev |
與 dcli build 類似,只不過(guò)是執(zhí)行調(diào)試 |
5.1 cli 開(kāi)發(fā)中值得收藏的一些第三方調(diào)料包
重要性 | 包名稱 | 功能 |
---|---|---|
必要 | minimist | 解析用戶命令,將 process.argv 解析成對(duì)象 |
必要 | fs-extra | 對(duì) fs 庫(kù)的擴(kuò)展,支持 promise |
必要 | chalk | 讓你 console.log 出來(lái)的字帶顏色,比如成功時(shí)的綠色字 |
必要 | import-from | 類似 require,但支持指定目錄,讓你可以跨工程目錄進(jìn)行 require,比如全局包想引用工程路徑下的內(nèi)容 |
必要 | resolve-from | 同上,只不過(guò)是 require.resolve |
必要 | inquirer | 詢問(wèn)用戶并記錄反饋結(jié)果,界面互動(dòng)的神器 |
必要 | yeoman-environment | 【核心】用于執(zhí)行一個(gè)「模板插件包」,后文詳細(xì)描述 |
錦上添花 | easy-table | 類似 console.table,輸出漂亮的表格 |
錦上添花 | ora | 提供 loading 菊花 |
錦上添花 | semver | 提供版本比較 |
錦上添花 | figlet | console.log出一個(gè)漂亮的大logo |
錦上添花 | cross-spawn | 跨平臺(tái)的child_process (跨 Windows/Mac) |
錦上添花 | osenv | 跨平臺(tái)的系統(tǒng)信息 |
錦上添花 | open | 跨平臺(tái)打開(kāi) app,比如調(diào)試的時(shí)候開(kāi)打 chrome |
5.2 命令解析與分發(fā)
命令的解析與分發(fā),是「全局命令包」的核心功能,其過(guò)程比較簡(jiǎn)單。大家也可以直接看倉(cāng)庫(kù)cli-tpl (全部功能壓縮到大約300行代碼)
cli 版本更新判斷:
- 先獲取本 package.json 中的 version
- 再通過(guò) npm view cli-tpl version 命令查詢當(dāng)前 npm 庫(kù)最新版本
- 兩者比較得出結(jié)論,提醒用戶更新
解析用戶命令
- 通過(guò) process.argv[2] 獲取到用戶執(zhí)行的實(shí)際命令,比如 dcli install 可拿到 install (正式版推薦使用 minimist 解析參數(shù))
處理命令
- 比如 install 命令,則通過(guò) require 動(dòng)態(tài)映射 install.js 文件來(lái)處理該邏輯
- 注:require 支持動(dòng)態(tài)名稱,如 require('./scripts/' + command) 這樣,如果 command 是 install 則映射執(zhí)行 script/install.js 文件
接下來(lái)我們看下 4 個(gè)核心命令,主要是:
命令 | 效果 |
---|---|
install | 幫用戶安裝/升級(jí)一個(gè)「模板插件包」 |
init | 幫用戶初始化一個(gè)工程,并拷貝模板 |
build | 調(diào)用工程中的「構(gòu)建插件包」,幫用戶webpack構(gòu)建 |
dev | 幫用戶啟動(dòng) devServer 進(jìn)行調(diào)試 |
下面逐一闡述每個(gè)命令的實(shí)現(xiàn)過(guò)程以及效果:
5.3 install命令:安裝一個(gè)「模板插件包」
install 意思就是把這個(gè)模板插件包下載到硬盤;此處我做了一個(gè)最小功能的 demo 包gen-tpl (后文詳細(xì)分解) 來(lái)輔助講解
dcli install gen-tpl
核心處理流程如下:
先判斷是否硬盤緩存目錄 ~/.maoda 下是否已經(jīng)有安裝過(guò) gen-tpl 包
- 如果沒(méi)有,則接下來(lái)進(jìn)行安裝 (相當(dāng)于在 ~/.maoda 目錄下執(zhí)行 npm install)
- 如果有,且版本低,則提示升級(jí)
- 如果有,且版本最新,則不作為
安裝過(guò)程即 execSync('npm i gen-tpl@latest -S', { cwd: '~/.maoda' })
我們可以為「模板插件包」的名稱做一個(gè)約定,即具備固定的前綴,諸如 gen-xxx
5.4 init命令: 選一個(gè)「模板插件包」來(lái)初始化一個(gè)新工程
這是一個(gè)腳手架高頻而核心的功能
dcli init
此時(shí)會(huì)分發(fā)去執(zhí)行 script/init.js 文件,我們看看其邏輯
查詢硬盤緩存目錄 ~/.maoda 下的 package.json 文件,讀取其中 dependacies 字段,拿到已安裝的「模板插件包」
- 如果一個(gè)都沒(méi)安裝,則提示用戶要先 install
讓用戶選擇一套模板
- 利用 inquery 庫(kù)發(fā)起對(duì)話,羅列出已裝模板,讓用戶選擇,比如上圖的 gen-pc gen-h5 gen-tpl
觸發(fā)模板初始化流程
- 比如用戶選擇了 gen-tpl 這個(gè)模板,則用 yeoman-environment 這個(gè)庫(kù)去執(zhí)行緩存目錄里的這個(gè)包 ~/.maoda/gen-tpl/index.js
- 注:這里相當(dāng)于跨目錄的兩個(gè) js 文件引用執(zhí)行,用到了之前說(shuō)的 import-from 這個(gè)庫(kù)
「模板插件包」被執(zhí)行,則啟動(dòng)了常規(guī)的模板拷貝過(guò)程 (后面展開(kāi)細(xì)說(shuō))
這里直接用包名稱做選項(xiàng),為了演示更直觀,實(shí)際通常用包的 description 做選項(xiàng),更友好一些,比如 gen-pc 包可能描述為 生成PC模板
5.5 build命令:在工程里執(zhí)行構(gòu)建
dcli build
確定工程目錄
- 工程目錄即執(zhí)行目錄,通過(guò) process.cwd() 獲取
讀取該工程所用的構(gòu)建插件
- 讀取工程中約定的配置文件,本demo中為 maoda.js (采用約定式的配置,類似 webpack.config.js .babelrc .prettierrc)
- 讀取 maoda.js 中 builder 配置項(xiàng) (即指定的構(gòu)建插件包),比如本 demo 中指定為 build-tpl
- 如果有的話,讀取自定義 webpack 配置 (約定為 webpackCustom 字段,后續(xù)會(huì)被合并/覆蓋到默認(rèn) webpack 配置上)
使用制定的構(gòu)建插件包來(lái)進(jìn)行 webpack 打包
- 判斷工程中是否已經(jīng)安裝 build-tpl
- 未安裝,則在工程中路徑中執(zhí)行 npm install (或 yarn add,此處有個(gè)小技巧,可根據(jù)用戶工程中 lock 文件的類型,判斷用戶使用的 npm 還是 yarn)
- 已安裝,則直接執(zhí)行 build-tpl
通常,我們用配置文件指明「構(gòu)建插件包」,也可以直接在命令里指明,比如 dcli build --builder=build-h5;后者往往適用于一套代碼打包出多種結(jié)果,如京東的 Taro cli
平時(shí)大家用慣了 npm run build yarn build,只需在我們的模板中的 package.json 添加一行:
{ "script": { ++ "build": "dcli build" } }
5.6 dev命令:?jiǎn)?dòng) devServer 進(jìn)行調(diào)試
類似 build 只不過(guò) webpack 配置不同,此處略
6 模板插件包
核心功能:提供模板文件夾 + 文件夾的拷貝。這里同樣提供了一個(gè)樣例工程 gen-tpl (僅 50 行代碼)
處理流程如下:
詢問(wèn)用戶,并獲取反饋的答案
- 比如工程名是什么,描述一下你的工程,是否使用 TypeScript,是否使用 Sass/Less/Stylus 等
根據(jù)用戶的答案,拷貝對(duì)應(yīng)的模板,細(xì)分兩種拷貝
- 直接拷貝,直接把模板插件包里的文件夾/文件,拷貝到用戶工程目錄
- 填充模板拷貝,將用戶答案,填充到文檔的對(duì)應(yīng)位置,類似 WebpackHTMLPlugin、ejs,如將 name: <%= packageName %> 填充成 name: 我的工程
在工程中執(zhí)行 npm 依賴的安裝
【重點(diǎn)來(lái)了】看似流程蠻多,其實(shí)只用一個(gè)現(xiàn)成的輪子即可搞定,即yeoman-generator,它幫我們把這些過(guò)程都封裝好了,我們只需繼承基類,并寫幾個(gè)預(yù)設(shè)的生命周期函數(shù)即可,無(wú)腦到令人發(fā)指 (細(xì)節(jié)處理,可參考模板倉(cāng)庫(kù))
module.exports = class extends Generator { // 【問(wèn)詢環(huán)節(jié)】 prompting() { return this.prompt([ { type: 'input', name: 'appName', message: '請(qǐng)輸入項(xiàng)目名稱:', }, { type: 'list', choices: ['Javascript', 'TypeScript'], name: 'language', message: '請(qǐng)選擇項(xiàng)目語(yǔ)言', default: 'TypeScript', }, ]).then(answers => { this.answers = answers }) } // 【模板拷貝】 writing() { // 從模板路徑拷貝到工程路徑 this.fs.copy(this.templatePath(), this.destinationPath()) } // 【安裝依賴】 install() { this.installDependencies() } end() { this.log('happy coding!') } }
很明顯,「模板插件包」導(dǎo)出的是一個(gè) class,我們需要通過(guò)上文提到的「全局命令包」里的 yeoman-environment 來(lái)啟動(dòng):
// 【節(jié)選自 全局命令包 init 命令,略修改以增加可讀性】 yoemanEnv.register(resolveFrom('./maoda', 'gen-tpl'), 'gen-tpl') yoemanEnv.run('gen-tpl', (e, d) => { d && this.console('happy coding', 'green') })
這里同樣用到前文提到的 resolve-from 包,進(jìn)行跨目錄的引用解析
yeoman 是一個(gè)比較完善的生態(tài),模板插件包可用 yeoman 提供的全局命令 yo 來(lái)創(chuàng)建,但并非必要,此處就不展開(kāi)說(shuō)了
7 構(gòu)建插件包
同樣我們提供了一個(gè)構(gòu)建插件包的模板build-tpl (20行代碼,啟動(dòng) webpack),webpack 配置都是空的,大家在開(kāi)發(fā)過(guò)程中可自行定制
構(gòu)建插件包其實(shí)核心就是 webpack 能力,webpack 能力這里就不展開(kāi)說(shuō)了,這里只描述一下調(diào)用關(guān)系
以 dcli build 為例,「全局命令包」在收到 build 命令后,啟動(dòng)「構(gòu)建插件包」
importFrom(process.cwd(), 'build-tpl')
沒(méi)錯(cuò),就是這么簡(jiǎn)單,import-from 庫(kù)能跨文件目錄,指定使用特定目錄的文件;使得全局包可以直接去執(zhí)行工程目錄的包 效果與同工程下 require('build-tpl') 一樣
此處也可以使用 import-cwd 庫(kù)
而 build-tpl 這個(gè)構(gòu)建插件包,負(fù)責(zé)將內(nèi)置的 webpack.config.js 與用戶工程下自定義的 webpackCustom 進(jìn)行 merge,然后執(zhí)行 webpack 流程
當(dāng)然,構(gòu)建工具不一定非要使用 webpack,比如可以選擇 rollup 或者像 Taro 在構(gòu)建小程序代碼時(shí)候,自己創(chuàng)建一套工具
8 寫在最后的話
筆者認(rèn)為,只有夠精簡(jiǎn),才能降低入門門檻,才能強(qiáng)化記憶;因此,本文的案例,在成熟的腳手架上進(jìn)行不斷刪減,剔除掉哪些徒增記憶負(fù)擔(dān)的部分,只保留精髓和核心,旨在快速在腦海里建模出一個(gè)企業(yè)級(jí)腳手架
同時(shí)提供了腳手架 3 個(gè)組成部分的 倉(cāng)庫(kù)/npm 包,以增加可操作性
如需引用與實(shí)際開(kāi)發(fā)中,我們需要繼續(xù)豐滿其血肉,包括但不限于:
- 異常處理 (如一些邊界情況)
- webpack 配置部分需完善 (本 demo 中 webpack.config 是空的)
- UI 和提升語(yǔ)可更友好
- 根據(jù)業(yè)務(wù)需求,擴(kuò)展額外的命令,比如卸載包,發(fā)布cdn等
文章博客地址:https://github.com/imaoda/js-front-end-practice 歡迎批評(píng)指正
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
- 詳解使用vue腳手架工具搭建vue-webpack項(xiàng)目
- vue-cli3.0 腳手架搭建項(xiàng)目的過(guò)程詳解
- 從零開(kāi)始學(xué)習(xí)搭建React腳手架項(xiàng)目
- vue腳手架搭建過(guò)程圖解
- vue腳手架搭建項(xiàng)目的兼容性配置詳解
- 詳解nodejs解壓版安裝和配置(帶有搭建前端項(xiàng)目腳手架)
- 搭建一個(gè)nodejs腳手架的方法步驟
- 使用vue-cli腳手架工具搭建vue-webpack項(xiàng)目
- vue-cli腳手架搭建的項(xiàng)目去除eslint驗(yàn)證的方法
- 使用vue腳手架(vue-cli)搭建一個(gè)項(xiàng)目詳解
相關(guān)文章
javascript this指向相關(guān)問(wèn)題及改變方法
這篇文章主要介紹了javascript this指向相關(guān)問(wèn)題及改變方法,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下2020-11-11javascript 實(shí)現(xiàn)劃詞標(biāo)記劃詞搜索功能
在頁(yè)面中加上這串代碼就行了,同時(shí)還有搜索功能。2009-10-10性能優(yōu)化篇之Webpack構(gòu)建速度優(yōu)化的建議
這篇文章主要介紹了性能優(yōu)化篇之Webpack構(gòu)建速度優(yōu)化的建議,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04Javascript將字符串日期格式化為yyyy-mm-dd的方法
日期格式化相信對(duì)于大家來(lái)說(shuō)再熟悉不過(guò),最近工作中自己利用Javascript就寫了一個(gè),現(xiàn)在將實(shí)現(xiàn)的代碼分享給大家,希望對(duì)有需要的朋友們能有所幫助,感興趣的朋友們下面來(lái)一起看看吧。2016-10-10一些常用的JavaScript函數(shù)(json)附詳細(xì)說(shuō)明
一些常用的JavaScript函數(shù)(json)附詳細(xì)說(shuō)明,學(xué)習(xí)js的朋友可以參考下。2011-05-05bootstrap fileinput插件實(shí)現(xiàn)預(yù)覽上傳照片功能
這篇文章主要介紹了bootstrap fileinput插件實(shí)現(xiàn)預(yù)覽上傳照片功能,代碼簡(jiǎn)單易懂,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2018-01-01js實(shí)現(xiàn)鼠標(biāo)點(diǎn)擊左上角滑動(dòng)菜單效果代碼
這篇文章主要介紹了js實(shí)現(xiàn)鼠標(biāo)點(diǎn)擊左上角滑動(dòng)菜單效果代碼,涉及JavaScript基于鼠標(biāo)事件動(dòng)態(tài)變換頁(yè)面元素樣式的技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-09-09