寫一個本身的 Yeoman Generator

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 是幹什麼的?

簡單介紹下 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 了

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 實例自己,調用的順序以下:

  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,而 reactauthor 就是 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 的名字。第二個參數是傳入 optionsarguments。第三個參數 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 吧,總之仍是期待的。

相關文章
相關標籤/搜索