寫一個自己的 Yeoman Generator

鄭友宏 8年前發布 | 19K 次閱讀 ESLint 前端技術 webpack

來自: 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 實例本身,調用的順序如下:

  1. initializing - 初始化一些狀態之類的,通常是和用戶輸入的 options 或者 arguments 打交道,這個后面說。
  2. prompting - 和用戶交互的時候(命令行問答之類的)調用。
  3. configuring - 保存配置文件(如 .babelrc 等)。
  4. default - 其他方法都會在這里按順序統一調用。
  5. writing - 在這里寫一些模板文件。
  6. conflicts - 處理文件沖突,比如當前目錄下已經有了同名文件。
  7. install - 開始安裝依賴。
  8. 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>

 本文由用戶 鄭友宏 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!