基于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基類:
vargenerators =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(){ returnthis.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'; vargenerators =require('yeoman-generator'); varglob =require('glob'); module.exports = generators.Base.extend({ // init initializing: function(){ this.existedFile = []; this.pageName =''; // 遍歷./pages varpageFiles = glob.sync(this.destinationPath('./pages/*/')); varreg =/\/(\w+)(\/$)/; pageFiles.forEach(function(v){ if(v && v.lastIndexOf('/') >-1) { this.existedFile.push(reg.exec(v)[1]); } }.bind(this)); }, prompting: function(){ vardone =this.async(); varpromptConf = [{ type: 'input', name: 'pageName', message: '請輸入頁面名稱:', default:'page_demo', // 校驗page是否已存在 validate: function(input){ if(this.existedFile &&this.existedFile.indexOf(input) > -1) { this.log('頁面已存在,請換一個頁面名稱!'); returnfalse; } else{ returntrue; } }.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 }]; returnthis.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(){ vartplArr = ['page.html','page.js','page.css']; varpageConf = { 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 );
commander
上面我們講解了Yeoman-generator的定制,也展示了一個簡單的generator——“simple-dir”。為了把simple-dir很優雅地跑起來,我們需要搞一個命令行工具。基于Nodejs開發自己的命令行工具是很簡單的事情,因為TJ大神已經為我們貢獻了屌炸天的工具——commander.js。關于commander的使用教程有很多,也比較容易上手。
有了commander的基礎之后,我們將Yeoman-generator封裝到自定義好的命令中。比如我已經封裝好了自己的命令行工具,它的名字叫做atdir(取自auto director),我們想要實現只需要運行 “atdir page” 就會自動生成需要的 html/css/js。然后我們只需要在atdir里面定義page.js:
module.exports =function(){ varyeoman =require('yeoman-environment'); varenv = yeoman.createEnv(); env.lookup(function(){ env.run('simple-dir:page', { 'skip-install':true }, function(err){ if(err) { throwerr; } }); }); }
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,歡迎指正!
來自:http://blog.hugzh.com/2016/09/25/基于yeoman定制的交互式命令行腳手架/