🛠如何快速開發一個本身的項目腳手架?

喜歡的朋友歡迎關注個人博客RSS 訂閱html

引言

下面是一個使用腳手架來初始化項目的典型例子。前端

隨着前端工程化的理念不斷深刻,愈來愈多的人選擇使用腳手架來從零到一搭建本身的項目。其中你們最熟悉的就是create-react-appvue-cli,它們能夠幫助咱們初始化配置、生成項目結構、自動安裝依賴,最後咱們一行指令便可運行項目開始開發,或者進行項目構建(build)。vue

這些腳手架提供的都是廣泛意義上的最佳實踐,可是我在開發中發現,隨着業務的不斷髮展,必然會出現須要針對業務開發的實際狀況來進行調整。例如:node

  • 經過調整插件與配置實現 Webpack 打包性能優化後
  • 刪除腳手架構建出來的部分功能
  • 項目架構調整
  • 融合公司開發工具
  • ……

總而言之,隨着業務發展,咱們每每會沉澱出一套更「個性化」的業務方案。這時候咱們最直接的作法就是開發出一個該方案的腳手架來,以便從此能複用這些最佳實踐與方案。react

1. 腳手架怎麼工做?

功能豐富程度不一樣的腳手架,複雜程度天然也不太同樣。可是整體來講,腳手架的工做大致都會包含幾個步驟:webpack

  • 初始化,通常在這個時候會進行環境的初始化,作一些前置的檢查
  • 用戶輸入,例如用 vue-cli 的時候,它會「問」你不少配置選項
  • 生成配置文件
  • 生成項目結構,這是候可能會使用一個項目模版
  • 安裝依賴
  • 清理、校驗等收尾工做

此外,你還須要處理命令行行爲等。每每咱們只是想輕量級、快速得建立一個特定場景的腳手架(不用想vue-cli那麼完備)。而對於想要快速建立一個腳手架,其實咱們不用徹底從零開始。Yeoman 就是一個能夠幫咱們快速建立腳手架的工具。git

可能不少同窗都不太瞭解,那麼先簡單介紹一下 Yeoman 是什麼,又是如何幫咱們來簡化腳手架搭建的。github

首先,Yeoman 能夠簡單理解爲是一個腳手架的運行框架,它定義了一個腳手架在運行過程當中所要經歷的各個階段(例如咱們上面說的,可能會先讀取用戶輸入,而後生成項目文件,最後安裝依賴),咱們所須要的就是在生命週期的對應階段,填充對應的操做代碼便可。而咱們填充代碼的地方,在 Yeoman 中叫作 generator,物如其名,Yeoman 經過調用某個 generator 便可生成(generate)對應的項目。web

若是你還不是特別清楚它們之間的關係,那麼能夠舉個小例子:vue-cli

將腳手架開發類比爲前端組件開發,Yeoman 的角色就像是 React,是一個框架,尤爲是定義了組件的生命週期函數;而 generator 相似於你寫的一個 React 業務組件,根據 React 的規則在各個生命週期中填代碼便可。

Yeoman 內置的「生命週期」方法執行順序以下:

  1. initializing
  2. prompting
  3. default
  4. writing
  5. conflicts
  6. install
  7. end

其中 default 階段會執行你自定義地各類方法。

同時,Yeoman 還集成了腳手架開發中經常使用的各種工具,像是文件操做、模版填充、終端上的用戶交互功能,命令行等,而且封裝成了簡單易用的方法。

經過這兩點,Yeoman 能夠幫咱們大大規範與簡化腳手架的開發。

2. 開發一個本身的腳手架

瞭解了一些腳手架的工做方式與 Yeoman 的基本概念,我們就能夠來建立一個屬於本身的腳手架。做爲例子,這個腳手架的功能很簡單,它會爲咱們建立一個最簡版的基於 Webpack 的前端項目。最終腳手架使用效果以下:

2.1. 準備一個項目模版

腳手架是幫助咱們快速生成一套既定的項目架構、文件、配置,而最多見的作法的就是先寫好一套項目框架模版,等到腳手架要生成項目時,則將這套模版拷貝到目標目錄下。這裏其實會有兩個小點須要關注。

第一個是模版內變量的填充。

在模版中的某些文件內容可能會須要生成時動態替換,例如根據用戶在終端中輸入的內容,動態填充package.json中的name值。而 Yeoman 內置了 ejs 做爲模版引擎,能夠直接使用。

第二個就是模版的放置位置。

一種是直接放在本地,也就是直接放到 generator 中,跟隨 generator 一塊兒下載,每次安裝都是本地拷貝,速度很快,可是項目模版自身的更新升級比較困難,須要提示用戶升級 generator。

另外一種則是將模版文件放到某個服務器上,每次使用腳手架初始化時經過某個地址動態下載,想要更新升級模版會很方便,一般會選擇託管在 github 上。

關於第二個模版放置到底是選擇在本地好,仍是遠端好,其實仍是依據你我的的業務場景而定,在不一樣的場景的限制的需求不一樣,我以前既寫過模版放在本地的腳手架(即和腳手架一塊兒經過 npm 安裝),也寫過託管在 git 倉庫上的這種方式。

回到咱們「建立一個最簡版的基於 Webpack 的前端項目」的目標,我準備了一個項目模版,以後就會用它來做爲腳手架生成的項目內容。

2.2. 建立 generator(yeoman-generator)

建立 Yeoman 的 generator 須要遵循它的規則。

首先是 generator 命名規則。須要以generator打頭,橫線鏈接。例如你想建立一個名爲 webpack-kickoff 的 generator,包名須要取成 generator-webpack-kickoff

這樣,當你經過

npm i -g yo
複製代碼

安裝完 Yeoman 的 CLI 後,就能夠經過yo命令來使用 generator 來啓動腳手架:

yo webpack-kickoff
複製代碼

這裏的 webpack-kickoff 就是包名裏generator-後面的內容,Yeoman 會按這個規則去全局找相匹配的包。

其次,依據 Yeoman 的規範,默認狀況下你須要在項目(即 generator)的generators/app/目錄下建立index.js,在其中寫入你的腳手架工做流程。固然,也能夠經過修改配置來擴展或改變這個規則

此外,你建立的 generator 類須要繼承 yeoman-generator。因此咱們會在generators/app/index.js中寫以下代碼:

const Generator = require('yeoman-generator');
class WebpackKickoffGenerator extends Generator {
    constructor(params, opts) {
        super(params, opts);
    }
}
module.exports = WebpackKickoffGenerator;
複製代碼

還記得以前提到的「生命週期」方法麼?包括 initializing、prompting、default、writing、conflicts、install 和 end。除了default,其餘都表明了 Generator 中的一個同名方法,你須要的就是在子類中重寫後所需的對應方法。default階段則會執行用戶定義的類方法。

例如,你想在初始化時打印下版本信息,能夠這麼作:

const Generator = require('yeoman-generator');
class WebpackKickoffGenerator extends Generator {
    constructor(params, opts) {
        super(params, opts);
    }
    
    initializing() {
        const version = require('../../package.json').version;
        this.log(version);
    }
}
module.exports = WebpackKickoffGenerator;
複製代碼

可見,剩下的工做就是在 WebpackKickoffGenerator 類中填充各類方法的實現細節了。

2.3. 處理用戶交互

腳手架工做中通常都會有一些用戶自定義的內容,例如建立的項目目錄名,或者是否啓用某個配置等。這些交互通常都是經過交互式的終端來實現的,例以下面這個功能。

可使用 Inquirer.js 來實現。而 Yeoman 已經幫咱們集成好了,直接在 generator 裏調用 this.prompt 便可。

在用戶交互部分的需求也比較簡單,只須要詢問用戶所需建立的項目目錄名便可,隨後也會做爲項目名。按照 Yeoman 的流程規範,咱們將該部分代碼寫在 prompting 方法中:

class WebpackKickoffGenerator extends Generator {
    // ……
    prompting() {
        const done = this.async();

        const opts = [{
            type: 'input',
            name: 'dirName',
            message: 'Please enter the directory name for your project:',
            default: 'webpack-app',
            validate: dirName => {
                if (dirName.length < 1) {
                    return '⚠️ directory name must not be null!';
                }
                return true;
            }
        }];

        return this.prompt(opts).then(({dirName}) => {
            this.dirName = dirName;
            done();
        });
    }
    // ……
}
複製代碼

注意,因爲用戶交互是一個「異步」的行爲,爲了讓後續生命週期方法在「異步」完成後再繼續執行,須要調用this.async()方法來通知方法爲異步方法,避免順序執行完同步代碼後直接調用下一階段的生命週期方法。調用後會返回一個函數,執行函數代表該階段完成。

2.4. 下載模版

正如2.1.中所述,咱們選擇將模版託管在 github 上,所以在生成具體項目代碼前,須要將相應的文件下載下來。可使用 download-git-repo 來快速實現。

class WebpackKickoffGenerator extends Generator {
    // ……
    _downloadTemplate() {
        return new Promise((resolve, reject) => {
            const dirPath = this.destinationPath(this.dirName, '.tmp');
            download('alienzhou/webpack-kickoff-template', dirPath, err => {
                if (err) {
                    reject(err);
                    return;
                }
                resolve();
            });
        });
    }
    // ……
}
複製代碼

這裏咱們使用了this.destinationPath()方法,該方法主要用於獲取路徑。不傳參時返回當前命令行運行的目錄;若是收到多個參數,則會進行路徑的拼接。

此外,若是你細心的話,會發現_downloadTemplate()方法帶了一個下劃線前綴。這是 Yeoman 中的一個約定:Yeoman 執行順序中有個default階段,該階段包含了全部用戶自定義的類方法。可是,若是某些方法你不但願被 Yeoman 的腳手架流程直接調用,而是做爲工具方法提供給其餘類方法,則能夠添加一個下劃線前綴。對於這種命名的方法,則會在default階段被忽略。

2.5. 模版文件拷貝

項目模版下載完畢後,下面就能夠將相關的目錄、文件拷貝到目標文件夾中。這些均可以在writing階段操做。此時須要遍歷模版中的全部目錄,將全部文件進行模版填充與拷貝。遍歷方式以下:

class WebpackKickoffGenerator extends Generator {
    // ……
    _walk(filePath, templateRoot) {
        if (fs.statSync(filePath).isDirectory()) {
            fs.readdirSync(filePath).forEach(name => {
                this._walk(path.resolve(filePath, name), templateRoot);
            });
            return;
        }

        const relativePath = path.relative(templateRoot, filePath);
        const destination = this.destinationPath(this.dirName, relativePath);
        this.fs.copyTpl(filePath, destination, {
            dirName: this.dirName
        });
    }
    // ……
}
複製代碼

這裏使用了this.fs.copyTpl()方法,它支持文件拷貝,同時還能夠指定相應的模版參數,此外,若是出現重名覆蓋狀況會在控制檯自動輸出相應信息。

最後,把下載與拷貝整合起來便可完成writing階段。

class WebpackKickoffGenerator extends Generator {
    // ……
    writing() {
        const done = this.async();
        this._downloadTemplate()
            .then(() => {
                const templateRoot = this.destinationPath(this.dirName, '.tmp');
                this._walk(templateRoot, templateRoot);
                fs.removeSync(templateRoot);
                done();
            })
            .catch(err => {
                this.env.error(err);
            });
    }
    // ……
}
複製代碼

2.6. 依賴安裝

到目前,腳手架已經能夠幫咱們把項目開發所需的配置、目錄結構、依賴清單都準備好了。這時候能夠進一步幫開發人員將依賴安裝完畢,這樣腳手架建立項目完成後,開發人員就能夠直接開發了。

Yeoman 也提供了this.npmInstall()來方法來實現 npm 包的安裝:

class WebpackKickoffGenerator extends Generator {
    // ……
    install() {
        this.npmInstall('', {}, {
            cwd: this.destinationPath(this.dirName)
        });
    }
    // ……
}
複製代碼

到這裏,腳手架的核心功能就完成了。已經可使用我們的這個 generator 來快速建立項目了。很簡單吧~

完整的代碼能夠參考 generator-webpack-kickoff

3. 使用腳手架 🚀

使用該腳手架會同時須要 Yeoman 與上述我們剛建立的 yeoman-generator。固然,有一個前提,Yeoman 與這個 generator 都須要全局安裝。全局安裝 Yeoman 沒啥有問題(npm install -g yo),處理 generator-webpack-kickoff 的話可能有幾種方式:

  1. 直接發佈到 npm,而後正常全局安裝
  2. 直接手動拷貝到全局 node_modules
  3. 使用npm link將某個目錄連接到全局

依據2.2.節的內容,我們的 generator 名稱爲 generator-webpack-kickoff。因爲個人包已經發到 npm 上了,因此要使用該腳手架能夠運行以下指令:

# 安裝一次便可
npm i -g yo
npm i -g generator-webpack-kickoff

# 啓動腳手架
yo webpack-kickoff
複製代碼

4. 優化

從上文這個例子能夠看出,實現一個腳手架很是簡單。例子雖小,但也包含了腳手架開發的主要部分。固然,這篇文章爲了簡化,省略了一些「優化」功能。例如

  • 項目目錄的重名檢測,生成項目時,檢查是否目錄已存在,並提示警告
  • 項目模版的緩存。雖然咱們使用 github 託管方式,但也能夠考慮沒必要每次都從新下載,能夠放一份本地緩存,而後天天或每週更新;
  • CLI 的優化。完整版裏還會包含一些更豐富的 CLI 使用,例如咱們在動圖中看到的 loading 效果、頭尾顯示的信息面板等。這些工具包括
    • ora,用於建立 spinner,也就是上面所說的 loading 效果
    • chalk,用於打印彩色的信息
    • update-notifier,用於檢查包的線上版本與本地版本
    • beeper,能夠「嗶」一下你,例如出錯的時候
    • boxen,建立頭尾的那個小「面板」
  • 版本檢查。上面提到能夠用 update-notifier 來檢查版本。因此能夠在 initializing 階段進行版本檢查,提示用戶更新腳手架。

最後

本文經過一個簡單的例子來告訴你們如何使用 Yeoman 快速建立腳手架。要了解更多 yeoman-generator 的開發與使用,能夠參考社區裏你們寫的各種 generator。目前在 npm 上有超過 8000 個 yeoman-generator,也許就會有你的菜。

文中完成的代碼請查看 generator-webpack-kickoff

相關文章
相關標籤/搜索