??如何快速開發(fā)一個自己的項目腳手架告组?

引言

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

image

隨著前端工程化的理念不斷深入怠噪,越來越多的人選擇使用腳手架來從零到一搭建自己的項目恐似。其中大家最熟悉的就是create-react-appvue-cli,它們可以幫助我們初始化配置傍念、生成項目結(jié)構(gòu)矫夷、自動安裝依賴,最后我們一行指令即可運行項目開始開發(fā)憋槐,或者進行項目構(gòu)建(build)双藕。

這些腳手架提供的都是普遍意義上的最佳實踐,但是我在開發(fā)中發(fā)現(xiàn)阳仔,隨著業(yè)務(wù)的不斷發(fā)展忧陪,必然會出現(xiàn)需要針對業(yè)務(wù)開發(fā)的實際情況來進行調(diào)整。例如:

  • 通過調(diào)整插件與配置實現(xiàn) Webpack 打包性能優(yōu)化后
  • 刪除腳手架構(gòu)建出來的部分功能
  • 項目架構(gòu)調(diào)整
  • 融合公司開發(fā)工具
  • ……

總而言之驳概,隨著業(yè)務(wù)發(fā)展赤嚼,我們往往會沉淀出一套更“個性化”的業(yè)務(wù)方案。這時候我們最直接的做法就是開發(fā)出一個該方案的腳手架來顺又,以便今后能復用這些最佳實踐與方案更卒。

1. 腳手架怎么工作?

功能豐富程度不同的腳手架稚照,復雜程度自然也不太一樣蹂空。但是總體來說,腳手架的工作大體都會包含幾個步驟:

  • 初始化果录,一般在這個時候會進行環(huán)境的初始化上枕,做一些前置的檢查
  • 用戶輸入,例如用 vue-cli 的時候弱恒,它會“問”你很多配置選項
  • 生成配置文件
  • 生成項目結(jié)構(gòu)辨萍,這是候可能會使用一個項目模版
  • 安裝依賴
  • 清理、校驗等收尾工作

此外返弹,你還需要處理命令行行為等锈玉。往往我們只是想輕量級、快速得創(chuàng)建一個特定場景的腳手架(不用想vue-cli那么完備)义起。而對于想要快速創(chuàng)建一個腳手架拉背,其實我們不用完全從零開始。Yeoman 就是一個可以幫我們快速創(chuàng)建腳手架的工具默终。

image

可能很多同學都不太了解椅棺,那么先簡單介紹一下 Yeoman 是什么犁罩,又是如何幫我們來簡化腳手架搭建的。

首先两疚,Yeoman 可以簡單理解為是一個腳手架的運行框架床估,它定義了一個腳手架在運行過程中所要經(jīng)歷的各個階段(例如我們上面說的,可能會先讀取用戶輸入鬼雀,然后生成項目文件顷窒,最后安裝依賴),我們所需要的就是在生命周期的對應(yīng)階段源哩,填充對應(yīng)的操作代碼即可。而我們填充代碼的地方鸦做,在 Yeoman 中叫做 generator励烦,物如其名,Yeoman 通過調(diào)用某個 generator 即可生成(generate)對應(yīng)的項目泼诱。

如果你還不是特別清楚它們之間的關(guān)系坛掠,那么可以舉個小例子:

將腳手架開發(fā)類比為前端組件開發(fā),Yeoman 的角色就像是 React治筒,是一個框架屉栓,尤其是定義了組件的生命周期函數(shù);而 generator 類似于你寫的一個 React 業(yè)務(wù)組件耸袜,根據(jù) React 的規(guī)則在各個生命周期中填代碼即可友多。

Yeoman 內(nèi)置的“生命周期”方法執(zhí)行順序如下:

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

其中 default 階段會執(zhí)行你自定義地各種方法。

同時堤框,Yeoman 還集成了腳手架開發(fā)中常用的各類工具域滥,像是文件操作、模版填充蜈抓、終端上的用戶交互功能启绰,命令行等,并且封裝成了簡單易用的方法沟使。

通過這兩點委可,Yeoman 可以幫我們大大規(guī)范與簡化腳手架的開發(fā)。

2. 開發(fā)一個自己的腳手架

了解了一些腳手架的工作方式與 Yeoman 的基本概念腊嗡,咱們就可以來創(chuàng)建一個屬于自己的腳手架着倾。作為例子,這個腳手架的功能很簡單叽唱,它會為我們創(chuàng)建一個最簡版的基于 Webpack 的前端項目屈呕。最終腳手架使用效果如下:

image

2.1. 準備一個項目模版

腳手架是幫助我們快速生成一套既定的項目架構(gòu)、文件棺亭、配置虎眨,而最常見的做法的就是先寫好一套項目框架模版,等到腳手架要生成項目時,則將這套模版拷貝到目標目錄下嗽桩。這里其實會有兩個小點需要關(guān)注岳守。

第一個是模版內(nèi)變量的填充。

在模版中的某些文件內(nèi)容可能會需要生成時動態(tài)替換碌冶,例如根據(jù)用戶在終端中輸入的內(nèi)容湿痢,動態(tài)填充package.json中的name值。而 Yeoman 內(nèi)置了 ejs 作為模版引擎扑庞,可以直接使用譬重。

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

一種是直接放在本地罐氨,也就是直接放到 generator 中臀规,跟隨 generator 一起下載,每次安裝都是本地拷貝栅隐,速度很快塔嬉,但是項目模版自身的更新升級比較困難,需要提示用戶升級 generator租悄。

另一種則是將模版文件放到某個服務(wù)器上谨究,每次使用腳手架初始化時通過某個地址動態(tài)下載,想要更新升級模版會很方便泣棋,通常會選擇托管在 github 上胶哲。

關(guān)于第二個模版放置究竟是選擇在本地好,還是遠端好外傅,其實還是依據(jù)你個人的業(yè)務(wù)場景而定纪吮,在不同的場景的限制的需求不同,我之前既寫過模版放在本地的腳手架(即和腳手架一起通過 npm 安裝)萎胰,也寫過托管在 git 倉庫上的這種方式碾盟。

回到我們「創(chuàng)建一個最簡版的基于 Webpack 的前端項目」的目標,我準備了一個項目模版技竟,之后就會用它來作為腳手架生成的項目內(nèi)容冰肴。

2.2. 創(chuàng)建 generator(yeoman-generator)

創(chuàng)建 Yeoman 的 generator 需要遵循它的規(guī)則。

首先是 generator 命名規(guī)則榔组。需要以generator打頭熙尉,橫線連接。例如你想創(chuàng)建一個名為 webpack-kickoff 的 generator搓扯,包名需要取成 generator-webpack-kickoff检痰。

這樣,當你通過

npm i -g yo

安裝完 Yeoman 的 CLI 后锨推,就可以通過yo命令來使用 generator 來啟動腳手架:

yo webpack-kickoff

這里的 webpack-kickoff 就是包名里generator-后面的內(nèi)容铅歼,Yeoman 會按這個規(guī)則去全局找相匹配的包公壤。

其次,依據(jù) Yeoman 的規(guī)范椎椰,默認情況下你需要在項目(即 generator)的generators/app/目錄下創(chuàng)建index.js厦幅,在其中寫入你的腳手架工作流程。當然慨飘,也可以通過修改配置來擴展或改變這個規(guī)則确憨。

此外,你創(chuàng)建的 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 中的一個同名方法慎皱,你需要的就是在子類中重寫后所需的對應(yīng)方法。default階段則會執(zhí)行用戶定義的類方法叶骨。

例如茫多,你想在初始化時打印下版本信息,可以這么做:

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 類中填充各種方法的實現(xiàn)細節(jié)了天揖。

2.3. 處理用戶交互

腳手架工作中一般都會有一些用戶自定義的內(nèi)容,例如創(chuàng)建的項目目錄名跪帝,或者是否啟用某個配置等今膊。這些交互一般都是通過交互式的終端來實現(xiàn)的,例如下面這個功能伞剑。

image

可以使用 Inquirer.js 來實現(xiàn)斑唬。而 Yeoman 已經(jīng)幫我們集成好了,直接在 generator 里調(diào)用 this.prompt 即可黎泣。

在用戶交互部分的需求也比較簡單恕刘,只需要詢問用戶所需創(chuàng)建的項目目錄名即可,隨后也會作為項目名抒倚。按照 Yeoman 的流程規(guī)范褐着,我們將該部分代碼寫在 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();
        });
    }
    // ……
}

注意托呕,由于用戶交互是一個“異步”的行為含蓉,為了讓后續(xù)生命周期方法在“異步”完成后再繼續(xù)執(zhí)行频敛,需要調(diào)用this.async()方法來通知方法為異步方法,避免順序執(zhí)行完同步代碼后直接調(diào)用下一階段的生命周期方法谴餐。調(diào)用后會返回一個函數(shù)姻政,執(zhí)行函數(shù)表明該階段完成。

2.4. 下載模版

正如2.1.中所述岂嗓,我們選擇將模版托管在 github 上汁展,因此在生成具體項目代碼前,需要將相應(yīng)的文件下載下來厌殉∈陈蹋可以使用 download-git-repo 來快速實現(xiàn)。

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()方法公罕,該方法主要用于獲取路徑器紧。不傳參時返回當前命令行運行的目錄;如果收到多個參數(shù)楼眷,則會進行路徑的拼接铲汪。

此外,如果你細心的話罐柳,會發(fā)現(xiàn)_downloadTemplate()方法帶了一個下劃線前綴掌腰。這是 Yeoman 中的一個約定:Yeoman 執(zhí)行順序中有個default階段,該階段包含了所有用戶自定義的類方法张吉。但是齿梁,如果某些方法你不希望被 Yeoman 的腳手架流程直接調(diào)用,而是作為工具方法提供給其他類方法肮蛹,則可以添加一個下劃線前綴勺择。對于這種命名的方法,則會在default階段被忽略伦忠。

2.5. 模版文件拷貝

項目模版下載完畢后省核,下面就可以將相關(guān)的目錄、文件拷貝到目標文件夾中缓苛。這些都可以在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()方法笔刹,它支持文件拷貝,同時還可以指定相應(yīng)的模版參數(shù)冬耿,此外舌菜,如果出現(xiàn)重名覆蓋情況會在控制臺自動輸出相應(yīng)信息。

最后亦镶,把下載與拷貝整合起來即可完成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. 依賴安裝

到目前袱瓮,腳手架已經(jīng)可以幫我們把項目開發(fā)所需的配置、目錄結(jié)構(gòu)爱咬、依賴清單都準備好了尺借。這時候可以進一步幫開發(fā)人員將依賴安裝完畢,這樣腳手架創(chuàng)建項目完成后精拟,開發(fā)人員就可以直接開發(fā)了燎斩。

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

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

到這里,腳手架的核心功能就完成了蜂绎。已經(jīng)可以使用咱們的這個 generator 來快速創(chuàng)建項目了栅表。很簡單吧~

完整的代碼可以參考 generator-webpack-kickoff

3. 使用腳手架 ??

使用該腳手架會同時需要 Yeoman 與上述咱們剛創(chuàng)建的 yeoman-generator师枣。當然怪瓶,有一個前提,Yeoman 與這個 generator 都需要全局安裝践美。全局安裝 Yeoman 沒啥有問題(npm install -g yo)洗贰,處理 generator-webpack-kickoff 的話可能有幾種方式:

  1. 直接發(fā)布到 npm,然后正常全局安裝
  2. 直接手動拷貝到全局 node_modules
  3. 使用npm link將某個目錄鏈接到全局

依據(jù)2.2.節(jié)的內(nèi)容陨倡,咱們的 generator 名稱為 generator-webpack-kickoff哆姻。由于我的包已經(jīng)發(fā)到 npm 上了,所以要使用該腳手架可以運行如下指令:

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

# 啟動腳手架
yo webpack-kickoff

4. 優(yōu)化

從上文這個例子可以看出玫膀,實現(xiàn)一個腳手架非常簡單。例子雖小爹脾,但也包含了腳手架開發(fā)的主要部分帖旨。當然,這篇文章為了簡化灵妨,省略了一些“優(yōu)化”功能解阅。例如

  • 項目目錄的重名檢測,生成項目時泌霍,檢查是否目錄已存在货抄,并提示警告
  • 項目模版的緩存。雖然我們使用 github 托管方式朱转,但也可以考慮不必每次都重新下載蟹地,可以放一份本地緩存,然后每天或每周更新藤为;
  • CLI 的優(yōu)化怪与。完整版里還會包含一些更豐富的 CLI 使用,例如我們在動圖中看到的 loading 效果缅疟、頭尾顯示的信息面板等分别。這些工具包括
    • ora遍愿,用于創(chuàng)建 spinner,也就是上面所說的 loading 效果
    • chalk耘斩,用于打印彩色的信息
    • update-notifier沼填,用于檢查包的線上版本與本地版本
    • beeper,可以“嗶”一下你括授,例如出錯的時候
    • boxen坞笙,創(chuàng)建頭尾的那個小“面板”
  • 版本檢查。上面提到可以用 update-notifier 來檢查版本刽脖。所以可以在 initializing 階段進行版本檢查羞海,提示用戶更新腳手架。

最后

本文通過一個簡單的例子來告訴大家如何使用 Yeoman 快速創(chuàng)建腳手架曲管。要了解更多 yeoman-generator 的開發(fā)與使用却邓,可以參考社區(qū)里大家寫的各類 generator。目前在 npm 上有超過 8000 個 yeoman-generator院水,也許就會有你的菜腊徙。

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末檬某,一起剝皮案震驚了整個濱河市撬腾,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌恢恼,老刑警劉巖民傻,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異场斑,居然都是意外死亡漓踢,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進店門漏隐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來喧半,“玉大人,你說我怎么就攤上這事青责⊥荩” “怎么了?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵脖隶,是天一觀的道長扁耐。 經(jīng)常有香客問我,道長浩村,這世上最難降的妖魔是什么萌业? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任缺谴,我火速辦了婚禮坑鱼,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘榨乎。我一直安慰自己,他們只是感情好瘫筐,可當我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布蜜暑。 她就那樣靜靜地躺著,像睡著了一般策肝。 火紅的嫁衣襯著肌膚如雪肛捍。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天之众,我揣著相機與錄音拙毫,去河邊找鬼。 笑死棺禾,一個胖子當著我的面吹牛缀蹄,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播膘婶,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼缺前,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了悬襟?” 一聲冷哼從身側(cè)響起衅码,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎脊岳,沒想到半個月后逝段,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡割捅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年惹恃,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片棺牧。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖朗儒,靈堂內(nèi)的尸體忽然破棺而出颊乘,到底是詐尸還是另有隱情,我是刑警寧澤醉锄,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布乏悄,位于F島的核電站,受9級特大地震影響恳不,放射性物質(zhì)發(fā)生泄漏檩小。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一烟勋、第九天 我趴在偏房一處隱蔽的房頂上張望规求。 院中可真熱鬧筐付,春花似錦、人聲如沸阻肿。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽丛塌。三九已至较解,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間赴邻,已是汗流浹背印衔。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留姥敛,地道東北人奸焙。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓,卻偏偏與公主長得像徒溪,于是被迫代替她去往敵國和親忿偷。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,675評論 2 359

推薦閱讀更多精彩內(nèi)容