https://leozdgao.me/write-yeoman-generator/html
因爲本身常常會寫一些 demo,或者學習新工具庫的使用,而後又比較依賴 npm 的模塊管理(這個是重點)和 webpack 的代碼打包功能,因此每次都要建立一個目錄結構,複製各類 .rc
文件,複製 webpack 的配置文件,複製一個應用了 webpack dev 中間件的 express server,每次都要這樣,讓我內心很煩。前端
我一直知道 yeoman 這個東西,不過找不到本身喜歡的 generator,簡單瀏覽過 generator 的文檔,感受很麻煩,不易上手,就一直沒學。最近在新的項目組,我又定義了一套開發的目錄規範,爲了給本身和團隊的其餘人提供開發上的便利,因而決定好好學寫 Yeoman Generator。vue
本文將介紹一個基本的 Yeoman Generator 的寫法,並分享一些開發中的注意點。node
簡單介紹下 Yeoman,它是一個腳手架生成工具,好比在以前寫 ASP.NET MVC 的時候,Visual Studio 會給你選模板,而後生成一個項目的基本結構(腳手架),這對提高開發體驗是頗有幫助的,節省了重複勞動。然而前端沒有什麼 IDE(WebStorm?或許吧),沒有一個固定的開發模式,可能你喜歡 jshint,我想用 eslint,你以爲 angular 順手,我以爲 vue 更合適,這時就可使用 Yeoman 這個工具,生成一個 適合本身技術棧 的腳手架,須要的一些文件都預先生成好,給本身省點事。react
而 Yeoman Generator 則定義了一個腳手架應該如何生成,因此咱們能夠去 這個網站 找適合本身的 Generator,若是沒有的話,就本身動手吧。webpack
而後這裏是安裝和使用的命令,不具體介紹它的使用了,想學的話能夠去 它的官網 看看。git
> npm install -g yo > npm install -g generator-angular > yo angular
先說下本身的需求吧,我但願它能夠:github
知足本身的技術棧:express、webpack、react、babel、eslint、travisweb
自動生成並安裝依賴shell
靈活性,便可以生成一個適合寫 demo 的小腳手架,也能夠生成一個 WebApp 的複雜腳手架,同時,在須要的時候能夠只生成一份 .babelrc
組合性,多個腳手架能夠組合,可複用
很高興的是,Yeoman 徹底能夠實現個人需求。
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 } })
這邊用的 OOP 用的是
classical inheritance
的風格,使用了 class-extend 這個模塊,有興趣的能夠看看。
咱們須要作的就是定義它的方法就好了。那麼要怎麼定義呢?
一個 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
進行轉換,若是對數據類型有要求的話,這裏要小心。
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 吧,總之仍是期待的。