腳手架這個詞估計做前端的都很熟悉。在沒有實現前端工程化的年代,前端代碼的組織都是純手工維護的。比如我要做一個網站頁面,那么我需要手動創建一個文件夾來存放代碼文件,我把它命名為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 page
$ atdir comp
結語
這只是Yeoman-generator的簡單用法,意圖在于學習搭建一個自己的命令行腳手架,其實還有很多可以完善的地方,比如目前的模板目錄是固定的,可以考慮實現更靈活的配置;還可以加上webpack等打包工具的config實現自動構建等等,這個就留到后面再去拓展。大家有什么想法也可以在github上提issue,歡迎指正!
原文出自:果汁的隨筆