寫一個自己的 Yeoman Generator
來自: http://leozdgao.me/write-yeoman-generator/
由于自己經常會寫一些 demo,或者學習新工具庫的使用,然后又比較依賴 npm 的模塊管理(這個是重點)和 webpack 的代碼打包功能,所以每次都要創建一個目錄結構,復制各種 .rc 文件,復制 webpack 的配置文件,復制一個應用了 webpack dev 中間件的 express server,每次都要這樣,讓我心里很煩。
我一直知道 yeoman 這個東西,不過找不到自己喜歡的 generator,簡單瀏覽過 generator 的文檔,感覺很麻煩,不易上手,就一直沒學。最近在新的項目組,我又定義了一套開發的目錄規范,為了給自己和團隊的其他人提供開發上的便利,于是決定好好學寫 Yeoman Generator。
本文將介紹一個基本的 Yeoman Generator 的寫法,并分享一些開發中的注意點。
Yeoman 是干什么的?
簡單介紹下 Yeoman,它是一個腳手架生成工具,比如在之前寫 ASP.NET MVC 的時候,Visual Studio 會給你選模板,然后生成一個項目的基本結構(腳手架),這對提升開發體驗是很有幫助的,節省了重復勞動。然而前端沒有什么 IDE(WebStorm?或許吧),沒有一個固定的開發模式,可能你喜歡 jshint,我想用 eslint,你覺得 angular 順手,我覺得 vue 更合適,這時就可以使用 Yeoman 這個工具,生成一個 適合自己技術棧 的腳手架,需要的一些文件都預先生成好,給自己省點事。
而 Yeoman Generator 則定義了一個腳手架應該如何生成,所以我們可以去 這個網站 找適合自己的 Generator,如果沒有的話,就自己動手吧。
然后這里是安裝和使用的命令,不具體介紹它的使用了,想學的話可以去 它的官網 看看。
> npm install -g yo > npm install -g generator-angular> yo angular</pre>
自己的需求
先說下自己的需求吧,我希望它可以:
- 滿足自己的技術棧:express、webpack、react、babel、eslint、travis
- 自動生成并安裝依賴
- 靈活性,即可以生成一個適合寫 demo 的小腳手架,也可以生成一個 WebApp 的復雜腳手架,同時,在需要的時候可以只生成一份 .babelrc
- 組合性,多個腳手架可以組合,可復用
很高興的是,Yeoman 完全可以實現我的需求。
開始寫 Yeoman Generator 了
Yeoman 給我們提供了一個用來寫腳手架的腳手架 generator-generator ,我們可以從它開始。
由于生成出來的項目依賴 nsp 服務,我在 npm prepublish 階段的時候發生了域名解析錯誤的問題,如果遇到了類似的問題,就把 package.json 里的 prepublish 刪掉吧。
假設我要寫一個 Generator 叫做 Butler(管家的意思),那么,根據 Yeoman 的規定,你需要將這個 node 模塊的名字命名為 generator-* ,所以我命名為 generator-butler ,如果你是通過 generator-generator 生成的目錄結構,那么可以進入到 generator-butler 目錄中,運行 npm link ,就可以開始使用你的 Generator 啦。
Yeoman Generator 高度依賴目錄結構,意思是它的行為由你的目錄結構決定,怎么說?比如:
yo butler yo butler:babel
第一條命令會找你代碼目錄中的 app 目錄,第二條命令會找你目錄中的 babel 目錄。這樣的一個個目錄稱為 sub-generator ,默認的 sub-generator 名字是 app。
為什么要這樣呢?我分享我的想法,我覺得這是出于對可組合性角度考慮的,我們可以定義多個 sub-generator ,比如我有多個 sub-generator 分別單獨管理:babel、eslint、webpack,同時 app 這個默認的 sub-generator 是這幾個 sub-generator 的組合,所以:
- 同時可以生成整個項目的結構,也可以(比如)只生成 babel 配置文件
- 各個模塊單獨管理,易于維護
非常符合自己比較認同的一句話:
perfer composition over inheritance
默認 sub-generator 是基于項目根目錄找的,也可以換一個目錄(比如 generators),就像例子中那樣統一管理,要實現這個,需要在 package.json 中加一個屬性:
{ ... "files": [ "generators" ], ... }
如何實現組合,下面會說到。
sub-generator 的加載似乎并不是直接應用 node 的模塊 resolve 機制,我本來以為是一個文件夾模塊加載方式,我試著直接創建文件模塊,它就不認了,看來是必須使用文件夾模塊的方式的。
基本結構
Yeoman 為我們提供了 Generator 的基類,于是:
var generators = require('yeoman-generator')module.exports = generators.Base.extend({
constructor: function () { generators.Base.apply(this, arguments)// your logic
} })</pre>
這邊用的 OOP 用的是 classical inheritance 的風格,使用了 class-extend 這個模塊,有興趣的可以看看。
</div>
我們需要做的就是定義它的方法就行了。那么要怎么定義呢?
運行周期
一個 Yeoman Generator 被創建后(構造函數必然是最先被調用的),會依次調用它原型上的方法,且每一個方法中的 this 都被綁定為 Generator 實例本身,調用的順序如下:
- initializing - 初始化一些狀態之類的,通常是和用戶輸入的 options 或者 arguments 打交道,這個后面說。
- prompting - 和用戶交互的時候(命令行問答之類的)調用。
- configuring - 保存配置文件(如 .babelrc 等)。
- default - 其他方法都會在這里按順序統一調用。
- writing - 在這里寫一些模板文件。
- conflicts - 處理文件沖突,比如當前目錄下已經有了同名文件。
- install - 開始安裝依賴。
- end - 擦屁股的部分... Say Goodbye maybe...
上面只是調用順序,后面的說明是建議,也就是說你完全可以在 install 的部分寫文件,在 configuring 的時候就開始安裝依賴,不過這樣的話,就不保證行為的正確性了,更不要說維護上的問題了,所以,別這樣,按照它的強制范式來吧。
這些運行周期方法,除了可以是函數外,還可以是對象,我以 babel 的 sub-generator 為例子:
writing: { files: function () { // 寫 `.babelrc` 文件 }, pkg: function () { // 給 package.json 文件上添加依賴項 } }對象里的每一個函數會被依次執行。是寫成一個函數,還是分成多個函數寫成一個對象,都可以,我個人傾向于后者。
關于依賴 Object 屬性的順序
偏一下題,注意 default 這個部分,【按順序執行】?
首先從 ECMAScript 標準來說,并不保證對象屬性的順序,之前開發遇到過坑:
4.3.3 Object An object is a member of the type Object. It is an unordered collection of properties each of which contains a primitive value, object, or function. A function stored in a property of an object is called a method.
自己在寫 Generator 的時候也沒怎么自定義方法(就是 default 這步是空的),都是依賴它的運行周期函數,而 Yeoman Generator 目前是依賴于對象屬性的插入順序的(相當于運行到 default 這步的時候),這里不多評價,如果平時開發希望在遍歷集合的時候,保證遍歷順序的話,應該使用數組或者是ECMAScript 2015 中新增的 Map 對象:
A Map iterates its elements in insertion order, whereas iteration order is not specified for Objects.
和用戶的交互
Yeoman 提供了多個方式來靈活定制你的腳手架:
Arguments 和 Options
比如:
yo butler MyProject --react --author leozdgao
其中, MyProject 就一個第一個定義的 argument,而 react 和 author 就是 options,值分別是 true 和 leozdgao。
對于 arguments 來說,不需要輸入 key,鍵值對的對應關系是根據定義順序來的。對于 options 來說,可以分別出入 key 和 value。
定義 arguments 和 options 的方式是類似的:
this.option('react', { type: Boolean, desc: "need to use React or not.", defaults: false }) this.arguments('name', { type: String, desc: "your project name", required: true })從參數名上就看的明白是什么意思了,不多說了。
定義 arguments 或者 options 寫在哪里都行,不過為了保證在任何地方都能正常訪問到,建議放在構造函數中。如果要訪問的話:
this.options['react'] // options 通過 options 屬性獲取 this.name // 是的,arguments 會直接作為 generator 的一個屬性arguments 和 options 的幫助信息會在定義后自動生成(如果它們不是在構造函數中被定義的話,幫助信息就無法自動生成):
> yo butler --help不要信你定義的 type ,其實這里并沒有根據你定義的 type 進行轉換,如果對數據類型有要求的話,這里要當心。
</div>
CLI 交互
使用與用戶問答交互的方式是比較有趣的,同時也不用記住要傳的參數,Yeoman 提供了 API 來讓我們快速實現 CLI 交互:
module.exports = generators.Base.extend({ prompting: function () { var done = this.async(); this.prompt({ type : 'input', name : 'name', message : 'Your project name', default : this.appname // Default to current folder name }, function (answers) { this.log(answers.name); done(); }.bind(this)); } });內部直接使用了 Inquirer.js ,API不變,這里就不多寫了,大家可以直接看 文檔 。
可以發現 Yeoman 處理異步的方式是聲明回調并顯示調用。
項目模板
生成腳手架就是拷貝模板文件,你可以定義你的模板文件。這里涉及到兩個文件夾,一個是你希望生成腳手架的目標文件夾,一個是模板所在的文件夾。Yeoman 提供了 API 來快速獲取它們,來看個例子,我希望根據 react 這個 option 來決定是否在 presets 中添加 react :
writing: function () { this.fs.copyTpl( this.templatePath('.babelrc'), this.destinationPath('.babelrc'), { needReact: this.options.react } ) }獲取目標文件夾目錄可以用 generator.destinationPath() ,傳入的參數和 path.join() 是一樣的。獲取模板文件夾目錄可以用 generator.sourceRoot() ,默認是 Generator 代碼目錄下的 ./templates ,也可以重寫: generator.sourceRoot('new/template/path') 。如果是拼模板文件路徑的話,就用 generator.templatePath('app/index.js') 。
Yeoman 給我們提供了方便的處理文件的工具,可以通過 fs 屬性調用,其實就是用了 mem-fs-editor 這個庫,可以直接看它的 API 說明,這里不多說了,要提一下的是模板引擎用的時 EJS。
這份是我對應上面例子的模板文件:
{ "presets": [ "es2015", "stage-0" <% if (needReact) { %> , "react" <% } %> ] }例子里調用了 copyTpl ,如果覺得不用經過模板引擎,可以直接用 copy 原樣拷貝。
組合
這里的組合只是概念,并不是按照函數式的方式實現的。
要實現組合,其實很簡單,在希望調用的地方調用 generator.composeWith 即可,直接上例子:
default: function () { // execute other sub-generators this.composeWith('butler:babel', { options: { react: this.options.react } }, { local: require.resolve('../babel') }) // select a License this.composeWith('license', { options: { name: this.props.authorName, email: this.props.authorEmail, website: this.props.authorUrl } }, { local: require.resolve('generator-license/app') }) }例子里分別是組合本地的一個 sub-generator ,和一個外部的 Generator ,我選擇在 default 這個運行周期調用組合。
composeWith 接受三個參數,第一個參數一個名字,寫什么都行,不過最好寫要被組合的 Generator 的名字。第二個參數是傳入 options 和 arguments 。第三個參數 settings,只用 local 和 link 兩個選項,local 是用來定位要組合的 Generator 的位置的,link 還不知道,沒怎么看懂它的 說明文檔 。
自動安裝依賴
恩,差點忘記這個,很簡單,就是函數調用:
install: function() { this.npmInstall([ 'lodash' ], { 'saveDev': true }); }在任何地方調用都是可以的,Yeoman 會統一在進入 install 階段的時候統一執行。如果還有在用 bower 的同學的話可以用這個: generator.bowerInstall() 。
最后
好了,基本上完了,如果什么地方寫錯了,還望指出。自己的 butler ,還在開發中,可以參考,另外我其實也是參考 generator-node 的,或者自己找些 Yeoman Generator 的源碼學學,個人認為使用 npm 作為包管理是趨勢(暫時也應該沒有終極方案,還是要依賴 bundle 工具),那么 bundle 工具就是不可或缺的了,寫個腳手架還是挺有幫助的,希望本文對大家有幫助。
然后是一些工具庫推薦:
- generator-license - 選擇 License 的 Generator
- inquirer - 提供命令行交互的工具
- inquirer-npm-name - 幫助查詢模塊名在 npm 上是否沖突,和 Yeoman 完美融合
- yosay - 在命令行輸出信息的時候,同時輸出 Yeoman 的卡通人物...
一些文檔的鏈接:
Yeoman 團隊目前在開發一個 Yeoman App ,就是一個 GUI 版的 yo 吧,總之還是期待的。
</div>本文由用戶 鄭友宏 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!相關經驗