如何開發本身的 yeoman 腳手架

腳手架可以幫咱們自動生成包含本地調試、編譯、打包、發佈等工具的項目目錄,使咱們可以減小大量重複勞動的同時,遵循必定的開發規範,大大提高咱們的開發、協同效率。html

腳手架所作的事情主要有:vue

  • 生成規範化目錄結構
  • 根據用戶輸入配置項目信息,如項目名稱、開發人員等
  • 生成項目本地server、編譯、單元測試、打包、發佈等配置

開發本身的腳手架

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

建立一個 node 模塊項目

yeoman 腳手架生成器(generator) 本質上是一個 node 模塊。建立一個 yeoman 腳手架生成器,首先咱們要先建立一個文件夾,文件夾名稱必須爲 generator-name (name 爲咱們自定義腳手架生成器的名稱)。這點很是重要,由於 yeoman 經過文件系統去找可用的腳手架生成器。git

在剛剛建立的文件夾內,建立一個 package.json文件,手動輸入以下內容,也能夠經過在命令行運行 npm init 生成該文件。github

{
  "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 來安裝此依賴。web

目錄結構

yeoman 會根據腳手架生成器目錄結構來完成相應操做,每一個子腳手架生成器必須包含在本身的文件夾內。npm

當在命令行運行 yo name 命令時,yeoman 會默認生成 app 文件夾內的腳手生成器架內容。json

子腳手架生成器能夠經過在命令行中運行 yo name:subcommand 來執行。gulp

項目目錄示例以下:promise

|---- 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

複製代碼
相關文章
相關標籤/搜索