【手把手】15分鐘搭一個企業級腳手架

1 寫在前面的話

搭一個腳手架,考驗了你的 nodejs 水平、工程化能力、以及工具服務的設計能力,是前端進階不可或缺的過程前端

筆者在開發 cli 的過程當中,調研流行的 cli 並造成最佳實踐,本文旨在用最短的篇幅實現主要功能,揭露核心原理,同時提供 demo 倉庫與你們學習探討。vue

通篇閱讀大約須要 10 分鐘,基於本教程本身擼一個 cli 大約須要花費 15 分鐘node

倉庫清單: 文章博客 | 腳手架-全局命令包 | 腳手架-模板插件包 | 腳手架-構建插件包react

2 腳手架的雛形

其實腳手架的初衷,就是提供一個最佳實踐的基礎模板,所以模板拷貝是其核心功能webpack

幾年前我曾寫過一個極簡的腳手架,大該幹了這麼一件事兒git

  1. npm publish 一個全局安裝的包
  2. 執行命令時,wget 我雲服務上的一個壓縮包,並在當前文件夾下解壓

一個命令,就能夠把我預設的完整的工程目錄建立好,特別方便效率。github

我想,這應該算是一個雛形腳手架吧web

3 腳手架須要考慮的

上面雛形腳手架能夠很好的服務於我的需求,可是畢竟過於乾癟和簡陋,要想成爲被你們普遍接受的工具,還須要完善。chrome

你們熟知的 vue-cli create-react-app @tarojs/cli umi 最基本功能:首先提出一些列問題選項,而後爲你的新建項目提供一份模板並安裝依賴,再提供調試構建命令vue-cli

沒錯,最核心的部分就是這個思路;但若是要作成一個可伸縮的、用戶友好的,還需考慮這些需求:

  • 模板支持版本管理
  • 支持擴展新模板
  • 自動檢測版本更新
  • 根據用戶選擇,生成個性化模板
  • 友好的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行代碼)

  1. cli 版本更新判斷:
    • 先獲取本 package.json 中的 version
    • 再經過 npm view cli-tpl version 命令查詢當前 npm 庫最新版本
    • 二者比較得出結論,提醒用戶更新
  2. 解析用戶命令
    • 經過 process.argv[2] 獲取到用戶執行的實際命令,好比 dcli install 可拿到 install (正式版推薦使用 minimist 解析參數)
  3. 處理命令
    • 好比 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
複製代碼

核心處理流程以下:

  1. 先判斷是否硬盤緩存目錄 ~/.maoda 下是否已經有安裝過 gen-tpl
    • 若是沒有,則接下來進行安裝 (至關於在 ~/.maoda 目錄下執行 npm install)
    • 若是有,且版本低,則提示升級
    • 若是有,且版本最新,則不做爲
  2. 安裝過程即 execSync('npm i gen-tpl@latest -S', { cwd: '~/.maoda' })

咱們能夠爲「模板插件包」的名稱作一個約定,即具有固定的前綴,諸如 gen-xxx

5.4 init命令: 選一個「模板插件包」來初始化一個新工程

這是一個腳手架高頻而核心的功能

dcli init
複製代碼

此時會分發去執行 script/init.js 文件,咱們看看其邏輯

  1. 查詢硬盤緩存目錄 ~/.maoda 下的 package.json 文件,讀取其中 dependacies 字段,拿到已安裝的「模板插件包」
    • 若是一個都沒安裝,則提示用戶要先 install
  2. 讓用戶選擇一套模板
    • 利用 inquery 庫發起對話,羅列出已裝模板,讓用戶選擇,好比上圖的 gen-pc gen-h5 gen-tpl
  3. 觸發模板初始化流程
    • 好比用戶選擇了 gen-tpl 這個模板,則用 yeoman-environment 這個庫去執行緩存目錄裏的這個包 ~/.maoda/gen-tpl/index.js
    • 注:這裏至關於跨目錄的兩個 js 文件引用執行,用到了以前說的 import-from 這個庫
  4. 「模板插件包」被執行,則啓動了常規的模板拷貝過程 (後面展開細說)

這裏直接用包名稱作選項,爲了演示更直觀,實際一般用包的 description 作選項,更友好一些,好比 gen-pc 包可能描述爲 生成PC模板

5.5 build命令:在工程裏執行構建

dcli build
複製代碼

  1. 肯定工程目錄
    • 工程目錄即執行目錄,經過 process.cwd() 獲取
  2. 讀取該工程所用的構建插件
    • 讀取工程中約定的配置文件,本demo中爲 maoda.js (採用約定式的配置,相似 webpack.config.js .babelrc .prettierrc)
    • 讀取 maoda.jsbuilder 配置項 (即指定的構建插件包),好比本 demo 中指定爲 build-tpl
    • 若是有的話,讀取自定義 webpack 配置 (約定爲 webpackCustom 字段,後續會被合併/覆蓋到默認 webpack 配置上)
  3. 使用制定的構建插件包來進行 webpack 打包
    • 判斷工程中是否已經安裝 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"
    }
}
複製代碼

5.6 dev命令:啓動 devServer 進行調試

相似 build 只不過 webpack 配置不一樣,此處略

6 模板插件包

核心功能:提供模板文件夾 + 文件夾的拷貝。這裏一樣提供了一個樣例工程 gen-tpl (僅 50 行代碼)

處理流程以下:

  1. 詢問用戶,並獲取反饋的答案
    • 好比工程名是什麼,描述一下你的工程,是否使用 TypeScript,是否使用 Sass/Less/Stylus 等
  2. 根據用戶的答案,拷貝對應的模板,細分兩種拷貝
    • 直接拷貝,直接把模板插件包裏的文件夾/文件,拷貝到用戶工程目錄
    • 填充模板拷貝,將用戶答案,填充到文檔的對應位置,相似 WebpackHTMLPlugin、ejs,如將 name: <%= packageName %> 填充成 name: 個人工程
  3. 在工程中執行 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等

文章博客地址:github.com/imaoda/js-f… 歡迎批評指正

相關文章
相關標籤/搜索