如何開發自己的 yeoman 腳手架

korbinzhao發表於2019-03-04

腳手架能夠幫我們自動生成包含本地除錯、編譯、打包、釋出等工具的專案目錄,使我們能夠減少大量重複勞動的同時,遵循一定的開發規範,大大提升我們的開發、協同效率。

腳手架所做的事情主要有:

  • 生成規範化目錄結構
  • 根據使用者輸入配置專案資訊,如專案名稱、開發人員等
  • 生成專案本地server、編譯、單元測試、打包、釋出等配置

開發自己的腳手架

下文中出現的 generator 統一代指 yeoman 腳手架生成器。

建立一個 node 模組專案

yeoman 腳手架生成器(generator) 本質上是一個 node 模組。建立一個 yeoman 腳手架生成器,首先我們要先建立一個資料夾,資料夾名稱必須為 generator-name (name 為我們自定義腳手架生成器的名稱)。這點非常重要,因為 yeoman 通過檔案系統去找可用的腳手架生成器。

在剛剛建立的資料夾內,建立一個 package.json檔案,手動輸入如下內容,也可以通過在命令列執行 npm init 生成該檔案。

{
  "name": "generator-name",
  "version": "0.1.0",
  "description": "",
  "files": [
    "generators"
  ],
  "keywords": ["yeoman-generator"],
  "dependencies": {
    "yeoman-generator": "^1.0.0"
  }
}
複製程式碼

name 屬性必須帶有字首 generator- ;keywords 屬性必須含有 “yeoman-generator” , 說明欄位最好有對該腳手架生成器的簡約清晰的說明。此外,務必引入最新版 yeoman-generator 作為專案依賴,可以通過在命令列中執行 npm install –save yeoman-generator 來安裝此依賴。

目錄結構

yeoman 會根據腳手架生成器目錄結構來完成相應操作,每個子腳手架生成器必須包含在自己的資料夾內。

當在命令列執行 yo name 命令時,yeoman 會預設生成 app 資料夾內的腳手生成器架內容。

子腳手架生成器可以通過在命令列中執行 yo name:subcommand 來執行。

專案目錄示例如下:

|---- package.json
|---- generators/
    |---- app/
        |---- index.js
    |---- router/
        |---- index.js
複製程式碼

yeoman 支援兩種不同的目錄結構,即 ./ 和 ./generators 兩種形式。

上述示例也可以這麼寫:

|---- package.json
|---- app/
    |---- index.js
|---- router/
    |---- index.js
複製程式碼

下一步

目錄結構就位,就可以完整的腳手架生成器了。

yeoman 提供了一個 base generator 基礎類 ,通過繼承 base generator 可大幅減少我們生成一個腳手架生成器的工作量。

可以在 index.js 檔案中,整合該基礎類:

var Generator = require(`yeoman-generator`);

module.exports = class extends Generator {};
複製程式碼

如果想支援 ES5 環境,靜態方法 Generator.extend() 可以用來擴充基類,並且允許你提供一個新的 prototype。該方法參照 class-extend

重寫建構函式

有些生成器函式只能在建構函式 constructor 內部呼叫,這些特殊的方法可能用於重要狀態的控制或者不對建構函式外部造成影響。

重寫生成器建構函式的方法如下:

module.exports = class extends Generator {
  // The name `constructor` is important here
  constructor(args, opts) {
    // Calling the super constructor is important so our generator is correctly set up
    super(args, opts);

    // Next, add your custom code
    this.option(`babel`); // This method adds support for a `--babel` flag
  }
};
複製程式碼

增加自定義功能

增加到 property 上的每個方法都會按照某種順序執行,但是某些特定的方法也會有特殊的觸發邏輯。

如下,增加幾個方法:

module.exports = class extends Generator {
  method1() {
    this.log(`method 1 just ran`);
  }

  method2() {
    this.log(`method 2 just ran`);
  }
};
複製程式碼

執行腳手架

到這裡,我們的腳手架就處於可執行狀態了。剛才我們本地開發了 一個腳手架,但是它還不能全域性執行。我們應該使用 npm 建立一個node全域性模組,並且連線到本地。

在腳手架專案根目錄下(generator-name/),輸入如下

npm link
複製程式碼

這樣會安裝依賴,並且連線全域性模組到本地檔案。然後,可通過在命令列中輸入 yo name ,即可執行該腳手架生成器。

腳手架執行時環境

腳手架生成器方法是如何執行的、執行環境是怎樣的是對於開發腳手架最重要的概念之一。

方法即行為

每一個繫結在腳手架生成器 prototype 上的方法都可以被當做一個任務。每個任務都是按順序在 yeoman 執行環境中執行的。

助手和私有方法

既然每個 prototype 方法都被當做一個任務,你可能會疑惑如何去定義一個不會被自動呼叫的助手或私有方法。以下有3種方式達到該目的:

  1. 為方法名新增下劃線字首
  class extends Generator {
    method1() {
      console.log(`hey 1`);
    }

    _private_method() {
      console.log(`private hey`);
    }
  }
複製程式碼
  1. 使用例項方法
  class extends Generator {
    constructor(args, opts) {
      // Calling the super constructor is important so our generator is correctly set up
      super(args, opts)

      this.helperMethod = function () {
        console.log(`won`t be called automatically`);
      };
    }
  }
複製程式碼
  1. 繼承一個父生成器 (parent generator)
  class MyBase extends Generator {
    helper() {
      console.log(`methods on the parent generator won`t be called automatically`);
    }
  }

  module.exports = class extends MyBase {
    exec() {
      this.helper();
    }
  };
複製程式碼

生成器生命週期

按順序執行任務對於單生成器來說已經夠了,但是當你把多個生成器合在一起時就有問題了。這就是 yeoman 使用生命週期的原因。

生命週期是一個帶有優先權的佇列系統。

生命週期執行順序如下:

  1. initializing – 初始化函式
  2. prompting – 接收使用者輸入階段
  3. configuring – 儲存配置資訊和檔案
  4. default – 自定義功能函式名稱,如 method1
  5. writing – 生成專案目錄結構階段
  6. conflicts – 統一處理衝突,如要生成的檔案已經存在是否覆蓋等處理
  7. install – 安裝依賴階段
  8. end – 生成器結束階段

非同步任務

有幾種暫停生命週期知道一個非同步任務結束的方法。

最簡單的方式就是 return 一個 promise。

如果你依賴的非同步 API 不支援 promise,我們可以使用 this.async() 方法。呼叫 this.async() 方法會在任務結束時返回一個函式。

asyncTask() {
  var done = this.async();

  getUserEmail(function (err, name) {
    done(err);
  });
}
複製程式碼

如果 done 函式被呼叫,並且傳參為一個 error 引數,那麼生命週期會結束,並且異常會丟擲。

使用者互動

系統提示

系統提示是腳手架生成器和使用者互動的主要方式。系統提示模組使用Inquirer.js

系統提示命令時非同步的,會返回一個 promise。你需要在你的任務中返回一個 promise ,來達到執行完當前任務,再執行下一個任務的目的。

示例如下:

module.exports = class extends Generator {
  prompting() {
    return this.prompt([{
      type    : `input`,
      name    : `name`,
      message : `Your project name`,
      default : this.appname // Default to current folder name
    }, {
      type    : `confirm`,
      name    : `cool`,
      message : `Would you like to enable the Cool feature?`
    }]).then((answers) => {
      this.log(`app name`, answers.name);
      this.log(`cool feature`, answers.cool);
    });
  }
};
複製程式碼

記錄使用者引數

對於特定問題,使用者往往會給出相同的答案,對於這些問題,最好記住使用者的歷史輸入。

yeoman 擴充了 Inquirer.js,新增 store 屬性,用於允許記錄使用者歷史輸入。示例如下:

this.prompt({
  type    : `input`,
  name    : `username`,
  message : `What`s your GitHub username`,
  store   : true
});
複製程式碼

引數 arguments

引數直接從命令列中獲取,如:

yo webapp my-project
複製程式碼

上述例子中,my-project 就是一個引數。

定義引數的方法如下:

使用 this.argument(name, options) 方法,其中 options 是一個多鍵值對物件:

  • desc String – 引數說明
  • required Boolean – 是否必填
  • type String, Number, Array (也可以是返回字串的自定義函式) – 型別
  • default – 預設值

this.argument(name, options) 只能在建構函式內呼叫。另外 help 引數不可用於傳入值,如 yo webapp –help。

使用示例如下:

module.exports = class extends Generator {
  // note: arguments and options should be defined in the constructor.
  constructor(args, opts) {
    super(args, opts);

    // This makes `appname` a required argument.
    this.argument(`appname`, { type: String, required: true });

    // And you can then access it later; e.g.
    this.log(this.options.appname);
  }
};
複製程式碼

選項 options

選項和引數非常相似。通過 generator.option(name, options) 呼叫。options 為一個多鍵值對物件,屬性如下:

  • desc – 選項說明
  • alias – 別名
  • type Either Boolean, String or Number (也可以是返回純字串的函式) – 型別
  • default – 預設值
  • hide Boolean – 是否從 help 中隱藏

使用示例:

module.exports = class extends Generator {
  // note: arguments and options should be defined in the constructor.
  constructor(args, opts) {
    super(args, opts);

    // This method adds support for a `--coffee` flag
    this.option(`coffee`);

    // And you can then access it later; e.g.
    this.scriptSuffix = (this.options.coffee ? ".coffee": ".js");
  }
};
複製程式碼

輸出資訊

通過 generator.log 模組輸出資訊。使用示例:

module.exports = class extends Generator {
  myAction() {
    this.log(`Something has gone wrong!`);
  }
};
複製程式碼

和檔案系統互動

位置環境和路徑

yeoman 檔案功能是基於硬碟上的兩個地址環境的,他們分別是生成器最長操作的讀取和寫入資料夾。

目標環境

第一個環境是目標環境。這個目標環境是 yeoman 建立腳手架應用的資料夾,也就是你的專案資料夾。

目的地環境通常是當前目錄或者包含 .yo-rc.json 檔案的最近父資料夾。.yo-rc.json 檔案定義了 yeoman 工程的根目錄。這個檔案允許你的使用者在子目錄中執行命令。

可以通過 generator.destinationRoot() 或者 generator.destinationPath(`sub/path`) 獲取目標路徑。

// Given destination root is ~/projects
class extends Generator {
  paths() {
    this.destinationRoot();
    // returns `~/projects`

    this.destinationPath(`index.js`);
    // returns `~/projects/index.js`
  }
}
複製程式碼

你可以通過 generator.destinationRoot(`new/path`) 手動修改目標路徑。但是為了便於維護,最好不要修改預設路徑。

如果你想知道使用者是在哪裡執行 yo 命令的,你可以通過 this.contextRoot 獲取該路徑。

模板環境

模板環境是你儲存模板檔案的資料夾,這通常是你將要讀取和拷貝的資料夾。

模板環境預設為 ./templates/ ,可以通過 generator.sourceRoot(`new/template/path`) 進行重寫。

可以通過 generator.sourceRoot() 或 generator.templatePath(`app/index.js`) 獲取模板路徑。

class extends Generator {
 paths() {
   this.sourceRoot();
   // returns `./templates`

   this.templatePath(`index.js`);
   // returns `./templates/index.js`
 }
};
複製程式碼

檔案功能

生成器將所有方法都暴露於 this.fs , this.fs 是 mem-fs editor的例項,可自行學習其 API 。

不過 commit 方法沒任何價值,不要在你的生成器中使用。yeoman 會在生命週期的 confilcts 階段自行呼叫。

例項:拷貝一個模板檔案

模板檔案如下:

<html>
  <head>
    <title><%= title %></title>
  </head>
</html>
複製程式碼

我們通過 copyTpl 方法拷貝檔案。

class extends Generator {
  writing() {
    this.fs.copyTpl(
      this.templatePath(`index.html`),
      this.destinationPath(`public/index.html`),
      { title: `Templating with Yeoman` }
    );
  }
}
複製程式碼

執行生成器, public/index.html 將包含如下內容:

<html>
  <head>
    <title>Templating with Yeoman</title>
  </head>
</html>
複製程式碼

通過流轉換輸出檔案

生成器系統允許你對每個檔案進行自定義過濾處理。自動美化檔案、格式化空白等也都是可以的。

yeoman 執行過程中,檔案將被一個 vinyl 物件流(像 gulp 一樣)處理,任何生成器的作者都可以註冊一個流轉換來修改檔案路徑和內容。

示例如下:

var beautify = require(`gulp-beautify`);
this.registerTransformStream(beautify({indent_size: 2 }));
複製程式碼

注意每個任何型別的檔案都會被流處理。要確保不被支援的檔案會被流轉換器過濾掉。像 gulp-if 或 gulp-filter 之類的工具會幫我們過濾非法型別的檔案。

在生成檔案的 writing 階段,你可以在 yeoman 流轉換中使用任何 gulp 外掛。

demo

根據以上教程,開發了一個基於 yoeman 的 vue ui 元件腳手架生成器 generator-vueui

使用方法

npm install -g yo
npm install -g generator-vueui

mkdir vueui-example
cd vueui-example

yo vueui

複製程式碼

相關文章