基于yeoman定制的交互式命令行腳手架

腳手架這個詞估計做前端的都很熟悉。在沒有實現前端工程化的年代,前端代碼的組織都是純手工維護的。比如我要做一個網站頁面,那么我需要手動創建一個文件夾來存放代碼文件,我把它命名為demo。然后在demo目錄下創建src文件夾,在src文件夾內創建css文件夾、js文件夾、image文件夾、lib文件夾等等...一切都是手工維護。自從node.js出現后,前端開發才慢慢開始告別刀耕火種,越來越多的自動化工具充斥我們的眼球。模板生成、代碼壓縮、構建打包、自動部署...這些已經成為構建前端工程項目的標配。那么,一個模板生成的命令行工具的原理是什么?怎樣開發一個屬于自己的命令行腳手架工具?希望我寫的這篇小文章會給大家帶來一點啟發。

原理

生成模板文件的方式可以是本地新建空白文件,然后進行文件內容讀寫;又或者是把本地已有的模板進行配置信息填充。然而我們知道,IO讀寫的速度非常慢,性能消耗大。但是一個模板生成器(Generator)如果是基于已有的模板文件進行配置填充,然后在copy到項目目錄對應的位置,那會比直接讀寫磁盤效率更高。所以一般來說,模板生成器會采用第二種工作原理。

Yeoman-generator

模板生成器的腳手架有很多,前端領域每天都會有很多類似的輪子源源不斷地從開源社區流出。這里我用來開發自己的generator的工具是Yeoman。Yeoman的Logo是一個戴著紅帽子的大胡子,它是一個通用的腳手架搭建系統,可以創建任何的類型的app。同時它又是"語言無感知"的,支持創建任何類型開發語言的項目,Web, Java, Python, C# 等等。Yeoman的通用性在于,它本身不做任何決定,所有的操作都是通過Yeoman環境里面的各種generator實現的。通過自定義generator,我們可以創建任何格式的項目目錄。這是Yeoman的最大魅力之處。另外,Yeoman通過提供promting這個方法實現輸入式命令行交互,可以讓用戶自由填寫配置信息,交互體驗也非常棒。下面說說怎樣基于Yeoman開發一個簡單的generator:

Simple-dir

simple-dir是我自己搗鼓的一個很簡單的Yeoman-generator,在這里我拿它來作為講解示例,大家也可以打開詳細代碼來看,歡迎star,也歡迎提issue。

第一步,package.json

開發一個Yeoman-generator,我們要做的第一步就是配置package.json。有幾個關鍵的地方,一個是,name的值的格式必須是"generator-"前綴 + Yeoman-generators官方源列表上的唯一值(如果你要共享你的generator到官方generator源的話);第二個就是,keywords屬性必須包括"yeoman-generator"這個值;第三,files屬性是命令自定義文件,app是默認的命令;第四,必須要安裝最新版本的yeoman-generator依賴,可以直接運行:npm install --save yeoman-generator 獲取最新的版本號。詳細的package.json可以看下面這份:

{
  "name": "generator-simple-dir",
  "version": "0.0.1",
  "description": "A very simple template generator",
  "files": [
    "generators/app",
    "generators/comp",
    "generators/page"
  ],
  "author": "橙鄉果汁",
  "license": "MIT",
  "keywords": [
    "yeoman-generator"
  ],
  "repository": {
    "type": "git",
    "url": "git@github.com:hugzh/generator-simple-dir.git"
  },
  "bugs": {
    "url": "https://github.com/hugzh/generator-simple-dir/issues"
  },
  "dependencies": {
    "glob": "^7.1.0",
    "mkdirp": "^0.5.1",
    "yeoman-generator": "^0.24.1"
  }
}

對應的src目錄格式應該是這樣的:

├───package.json
└───generators/
├───app/
│ └───index.js
├───comp/
│ └───index.js
└───page/
└───index.js

你也可以直接把files屬性直接寫成:

"files": [
  "app",
  "comp",
  "page"
]

但是這樣的話,你的代碼根目錄就必須直接包含app,comp和page文件夾。

第二步,拓展generator

這里我們有三個generator——app,comp和page。以page為例,我們來實現一個generator。

首先,需要繼承Yeoman提供的generator基類:

var generators = require('yeoman-generator');
module.exports = generators.Base.extend();

然后我們就可以在基類內部重寫generator的方法了。Yeoman提供了一系列的基類方法:

initializing - 初始化 (檢查當前項目狀態、獲取配置文件內容等等)
prompting - 獲取用戶輸入,實現與用戶的交互 (通過this.prompt()調用)
configuring - 保存配置并配置整個項目 (比如創建 .editorconfig 文件和其他媒介文件)
default - 當定義的方法沒有匹配任何基類方法的時候用到
writing - 根據自定義的規則寫入具體的generator文件 (routes, controllers, etc)
conflicts - 內部沖突處理
install - 安裝npm、bower等依賴的地方
end - 在最后調用, 實現cleanup, say good bye等功能。

在示例generator-simple-dir里,page這個generator的作用是創建頁面,需要生成html/css/js文件。在generators.Base.extend函數內部,page實現了 initializing、prompting、writing、end這幾個方法。對于prompting這樣的異步方法,需要在交互結束的時候調用this.async()來結束異步任務。Yeoman實現用戶交互的核心方法是prompting,它是一個異步的方法,并且返回一個promise。prompting方法通過一個數組參數,可以實現鏈式的用戶輸入。其中input類型的是用戶輸入自定義內容,confirm類型是作為True/False判斷的prompt,輸入Y/N。官方的示例如下:

module.exports = generators.Base.extend({
  prompting: function () {
    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(function (answers) {
      this.log('app name', answers.name);
      this.log('cool feature', answers.cool);
    }.bind(this));
  }
})

如果你想要記住用戶輸入的一個內容,用來做后面輸入的默認值的話,還可以通過增加store:true配置來實現。
在generator-simple-dir里面,page這個generator包含4個執行步驟:初始化、獲取用戶輸入、根據用戶輸入生產模板文件、結束返回,實現的代碼如下:

'use strict';
var generators = require('yeoman-generator');
var glob = require('glob');

module.exports = generators.Base.extend({
  // init
  initializing: function() {
    this.existedFile = [];
    this.pageName = '';
    // 遍歷./pages
    var pageFiles = glob.sync(this.destinationPath('./pages/*/'));
    var reg = /\/(\w+)(\/$)/;
    pageFiles.forEach(function(v) {
      if (v && v.lastIndexOf('/') > -1) {
        this.existedFile.push(reg.exec(v)[1]);
      }
    }.bind(this));
  },

  prompting: function() {
    var done = this.async();
    var promptConf = [{
      type: 'input',
      name: 'pageName',
      message: '請輸入頁面名稱:',
      default: 'page_demo',
      // 校驗page是否已存在
      validate: function(input) {
        if (this.existedFile && this.existedFile.indexOf(input) >
          -1) {
          this.log('頁面已存在,請換一個頁面名稱!');
          return false;
        } else {
          return true;
        }
      }.bind(this)
    }, {
      type: 'input',
      name: 'pageTitle',
      message: '頁面Title描述:',
      default: 'Title'
    }, {
      type: 'confirm',
      name: 'isNeedStyle',
      message: '是否需要樣式表?',
      default: true
    }, {
      type: 'confirm',
      name: 'isPc',
      message: '是否PC端的頁面?',
      default: false
    }];

    return this.prompt(promptConf)
      .then(function(props) {
        this.pageName = props.pageName;
        this.pageTitle = props.pageTitle;
        this.isNeedStyle = props.isNeedStyle;
        this.isPc = props.isPc;

        done();
      }.bind(this));
  },
  writing: function() {
    var tplArr = ['page.html', 'page.js', 'page.css'];
    var pageConf = {
      pageName: this.pageName,
      pageTitle: this.pageTitle,
      isNeedStyle: this.isNeedStyle,
    };
    if (this.isPc) {
      tplArr[0] = 'page.pc.html';
    }
    if (!this.isNeedStyle) {
      tplArr.pop();
    }

    tplArr.forEach(function(value, index) {
      // (from,to,content)
      this.fs.copyTpl(
        this.templatePath(value),
        this.destinationPath('pages/' + pageConf.pageName + '/' + pageConf.pageName +
          '.' + value.split(
            '.').pop()),
        pageConf
      );
    }.bind(this));

  },
  end: function() {
    this.log('新建頁面完成!')
  }
});

定制模板

prompting方法是用來獲取用戶輸入,writing方法是根據用戶輸入內容生成模板文件。之前說到,模板生成器的一般原理是用獲取的配置信息渲染好模板,再拷貝到項目目錄對應的位置。所以,在writing方法里面,需要實現模板渲染和拷貝。在Yeoman-generator里,需要的模板文件默認放在templates文件夾里,所有文件相關的操作通過this.fs對象來實現。this.fs.copyTpl就是我們用來拷貝渲染好的模板文件的方法,需要輸入三個參數:模板源路徑、需要拷貝到的項目路徑、模板渲染內容對象。模板的渲染是基于ejs模板引擎的語法。根據我們定義的項目結構,page的實現如下:

this.fs.copyTpl(
  this.templatePath(value),
  this.destinationPath('pages/' + pageConf.pageName + '/' + pageConf.pageName +
    '.' + value.split(
      '.').pop()),
  pageConf
);

更詳細的Yeoman-generator文件操作文檔請點擊這里

commander

上面我們講解了Yeoman-generator的定制,也展示了一個簡單的generator——"simple-dir"。為了把simple-dir很優雅地跑起來,我們需要搞一個命令行工具。基于Nodejs開發自己的命令行工具是很簡單的事情,因為TJ大神已經為我們貢獻了屌炸天的工具——commander.js。關于commander的使用教程有很多,也比較容易上手,如果你還沒有了解過commander.js,推薦閱讀這兩篇文章:《Commander:node.js命令行接口的完全解決方案》《Commander寫自己的Nodejs命令》

有了commander的基礎之后,我們將Yeoman-generator封裝到自定義好的命令中。比如我已經封裝好了自己的命令行工具,它的名字叫做atdir(取自auto director),我們想要實現只需要運行 "atdir page" 就會自動生成需要的 html/css/js。然后我們只需要在atdir里面定義page.js:

module.exports = function() {
  var yeoman = require('yeoman-environment');
  var env = yeoman.createEnv();
  env.lookup(function() {
    env.run('simple-dir:page', {
      'skip-install': true
    }, function(err) {
      if (err) {
        throw err;
      }
    });
  });
}

env.lookup()的作用是遍歷用戶機器上安裝好的generator,接入到Yeoman-environment,比如我們simple-dir的init、page或者comp命令。然后運行env.run()。由于我已經將simple-dir發布到npm包了,所以可以直接調用env.run('simple-dir:page',function(){})。如果你不想將generator發布到npm,然后又想在本地使用generator的話也可以,直接進入generator的根目錄,執行npm link,simple-dir 指令就會關聯到本地的npm里面,Yeoman就能找到 "simple-dir:page" 這個指令啦!

小工具——atdir

atdir就是上面說的命令行小工具,想要了解命令行的詳細封裝方法可以戳這里。由于atdir沒有發布到npm源,不能直接npm i。如果想要運行起來的話,請先把atdir源碼clone到本地,進入到atdir根目錄,執行npm link,npm install 之后就可以愉快的執行atdir命令啦~~~
附上幾張運行界面截圖:
$ atdir init

atdir init

$ atdir page


atdir page

$ atdir comp


atdir comp

結語

這只是Yeoman-generator的簡單用法,意圖在于學習搭建一個自己的命令行腳手架,其實還有很多可以完善的地方,比如目前的模板目錄是固定的,可以考慮實現更靈活的配置;還可以加上webpack等打包工具的config實現自動構建等等,這個就留到后面再去拓展。大家有什么想法也可以在github上提issue,歡迎指正!

原文出自:果汁的隨筆

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,578評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,701評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,691評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,974評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,694評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,026評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,015評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,193評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,719評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,442評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,668評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,151評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,846評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,255評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,592評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,394評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容

  • yeoman-generator yeoman-generator是一個構建腳手架的工具,如果你還不了解你自行go...
    稻草人_02dd閱讀 1,527評論 0 1
  • Yeoman是什么? Yeoman為何方神圣,說他有三頭六臂一點都不夸張。 因為他由三部分組成:yo(腳手架工具)...
    GrowthCoder閱讀 1,429評論 0 2
  • 接著上篇的《 利用Node.js搭建前端自動化平臺 》我們開始搭建自己的前端工作流吧!要啟動一個項目,最先要做什么...
    Max_Law閱讀 3,155評論 0 7
  • 在日常的設計工作中,難免會遇到一些制作可交互原型的工作。可交互的原型對比于靜態原型來說,直觀是最大的優點。其次,在...
    妖葉秋閱讀 14,443評論 4 37
  • 16周歲,我們在社團相識,那會兒給你發消息聊天時,你還總是湊和湊和的回復我。 17周歲,我們在同一個地方工作,而那...
    L小白閱讀 235評論 0 1