前言:在開始一個新項目得時候,如果我們已經有存在得項目,大多數都會借鑑這個項目,包括他的基礎配置,以及一些公共的組件。而往往採用的方式都是複製粘貼,如果項目比較複雜,這樣做有幾個很明顯的弊端,一是比較耗時,二是容易出錯。即使我們重新構建一個新的腳手架,也需要根據項目的需求做不少的添加更改等等,其實經過簡單的分析,在我們的許多項目當中都有很多共性的東西,哪我們有沒有一個方法,在每次需開發新項目的時候,迅速生成這些基礎的配置,就像我們下載一個npm包一樣簡單。答案當然是可以的,接下來我們我們就介紹這樣一個腳手架yeoman-generator。
簡介
yeoman是一個可以幫助開發者快速開啓一個新項目的工具集。yoeman提出一個yeoman工作流的概念,通過腳手架工具(yo),構建工具(grunt gulp等)和包管理器(npm bower等)的配合使用讓開發者專注於業務的邏輯。在yeoman的官網中可以搜索到用於初始化項目的generator,可以用於快速開啓項目。同時yeoman也提供給開發者如何定義自己的generator,所有我們自己開發的generator都作爲一個插件可以通過yo工具創建出我們需要的結構。
自己創建的generator可以是很簡單的創建幾個模板頁面,也可以通過和用戶交互構建一套量身定製的項目,取決於項目初始化的策略。可以利用yeoman的generator-generator工具來開始構建自己的generator。
一.準備:
下載安裝yo和generator-generator
注:在已經有裝好node和npm的前提下,需要全局安裝yo
和generator-generator
npm install -g yo
npm install -g generator-generator
之後運行generator-generator
來創建我們自己需要的generator的基礎框架
yo generator
在一系列設置問題之後
最終得到的generator的目錄:
.
├── generators/
│ └── app/
│ ├── index.js
│ └── templates/
│ └── dummyfile.txt
├── .editorconfig
├── .gitattributes
├── .gitignore
├── .eslintrc
├── .travis.yml
├── .yo-rc.json
├── package.json
├── gulpfile.js
├── README.md
├── LICENSE
└── test/
└── app.js
二.從一個簡單的例子開始
我們的generator是一個插件,所以首先需要創建成一個node module包,在yeoman中這個包的名字必須是generator開頭的,那麼我們這個generator就叫做generator-xxx。每一個package.json的keyword中必須包含yeoman-generator
。files屬性要指向項目的模板目錄。
三.通過npm init
或是自己手動創建generator的package.json,項目依賴yeoman-generator。建議用yo generator來初始化
{
"name": "generator-demo",
"version": "0.1.0",
"description": "",
"files": [
"generators"
],
"keywords": [
"yeoman-generator"
],
"main": "generators/app/index.js",
"dependencies": {
"yeoman-generator": "^1.0.0"
}
}
四.往template中填充內容,也就是demo項目的三個基本文件的內容。這裏簡單提供一個例子: template/index.html
<!DOCTYPE html>
<html lang="zh_CN">
<head>
<title>generator-demo</title>
<link rel="stylesheet" href="styles/style.css">
</head>
<body>
<h1>helle <%= name %></h1>
<script src="scripts/main.js"></script>
</body>
</html>
yeoman採用ejs模板語法,可以在模板文件中傳入參數。
template/styles/style.css
* {
margin: 0;
padding: 0;
}
template/sctipts/main.js
'use strict';
window.onload = function() {
console.log('generator success');
};
五.到這一步後就是擴展generator。yeoman提供了一個基礎的generator模板,它有自己的生命週期和事件,功能強大。可以通過擴展這個基礎generator來實現我們項目的初始化需求。接下來就是編輯app/index.js來擴展它:
const Generator = require('yeoman-generator');
const path = require('path');
const fs = require('fs');
const mkdirp = require('mkdirp');
const utils = require('../utils');
const log = utils.log;
module.exports = class extends Generator {
constructor(args, opts) {
super(args, opts);
this.props = {
projectName: 'demo',
name: 'world'
};
}
writing() {
const { projectName, name } = this.props;
const temps = {
'index.html': { name: this.props.name }
};
fs.readdir(this.sourceRoot(), (err, items) => {
for(let item of items) {
if(temps[item]) {
this.fs.copyTpl(
this.templatePath(item),
this.destinationPath(projectName, item),
temps[item]
);
} else {
this.fs.copy(
this.templatePath(item),
this.destinationPath(projectName, item)
);
}
}
});
}
end() {
log.info('generator success');
}
};
六.就是運行generator。yoeaman的generator是一個全局npm module,我們在本地開發的generator可以通過軟連接的方式在本地生成它的全局npm包。在工程的根目錄下運行npm link
,它會在本地的全局npm目錄下安裝我們新建的generator。
在確定本地已經安裝yo工具(npm install -g yo
)後,在你需要初始化項目的地方運行yo demo
,等命令執行完畢,就可以看到新建的項目了。
七.在擴展基礎generator時,我們給實例添加自定義的方法,每一個添加進去的方法都會在generator調用的時候被調用,而且通常來講,這些方法是按照順序調用的。除非是已下劃線_開頭的私有方法,或是定義在實例上的方法。
module.exports = class extends Generator {
constructor(args, opts) {
super(args, opts);
this.task = () => {
this.log('instance task');
}
}
method1() {
this.log('method 1');
}
method2() {
this.log('method 2');
}
_task() {
this.log('private task');
}
};
// 輸出:
// 'method 1'
// 'method 2'
每一個方法在yeoman中都被認爲是一個任務,這些任務都會被run loop調用。yeoman的run loop是一個有優先級的隊列系統。採用Grouped-queue來維護yeoman的事件隊列。除了自定義的方法外,yeoman有很多特殊的事件方法,按照優先級排序:
- initializing - 初始化開始
- prompting - 調用this.prompt()與用戶產生交互
- configuring - 創建配置文件(package.json,config.js等)
- default - 方法都不匹配這些優先級時,就會是default優先級(自定義方法會被劃入default)
- writing - 創建項目文件
- conflicts - 文件創建中產生衝突的處理
- install - 調用(npm, bower)包install
end - 結束項目初始化 其他自定義方法在configuring和writing按順序優先級調用。
八.現在我們來給generator增加用戶交互和package.json,讓它能構建出一個更復雜的項目。還是修改app/index.js,首先增加prompting:
prompting() {
return this.prompt([{
type: 'input',
name: 'projectName',
message: '請輸入項目名字',
default: 'default-name'
}, {
type: 'confirm',
name: 'package',
message: '需要package.json文件',
default: true
}, {
type: 'input',
name: 'name',
message: '請輸入你的名字',
default: 'world'
}]).then((answers) => {
this.log('create project: ', answers.projectName);
this.log('by: ', answers.name);
this.props = answers;
});
}
增加configuring:
configuring() {
const { projectName, name } = this.props;
let packageSettings = {
name: projectName,
version: '0.0.1',
description: 'YOUR DESCRIPTION - Generated by generator-demo',
main: '',
scripts: {},
repository: '',
keywords: [],
author: name,
devDependencies: {},
dependencies: {}
};
this.fs.writeJSON(this.destinationPath(projectName, 'package.json'), packageSettings);
}
九. 重寫constructor方法
有些generator方法只有定義在構造方法內才能被調用到.這些特殊的方法可以做的一些重要的操作等,而這些操作可能在構造之外無法正常運行。
module.exports = generators.Base.extend({ // The name `constructor` is important here constructor: function () { // Calling the super constructor is important so our generator is correctly set up generators.Base.apply(this, arguments); // Next, add your custom code this.option('coffee'); // This method adds support for a `--coffee` flag } });
十.找到工程根目錄
當使用yo命令來運行generator的生活,yeoman會把 .yo-rc.json文件所在的目錄作爲工程的根目錄,之後Yeoman將當前文件目錄跳轉到根目錄下運行請求的生成器。當我們使用this.config.save()的時候,storage模塊會創建它。如果.yo-rc.json 不在當前的工作目錄,請確保他也不在其他的項目目錄裏。
十一. 運行上下文
在generator內,所有的靜態方法都會被作爲action而自定執行,當然generator也提供了可以聲明不自動執行的輔助函數,generator提供了三種可以創建輔助函數的方法.
通過下劃線開頭定義函數,如:CopyFiles 2 使用實例函數聲明:
generators.Base.extend({ constructor: function () { this.helperMethod = function () { console.log('won\'t be called automatically'); }; } });
繼承一個父級generator:
var MyBase = generators.Base.extend({ helper: function () { console.log('methods on the parent generator won\'t be called automatically'); } }); module.exports = MyBase.extend({ exec: function () { this.helper(); } });
十二.generator Arguments
Arguments是在命令行中直接傳遞的。 如:yo webapp my-project,接受鍵值對的條件。
desc:描述argument
required:定義是否必須
optional:是否可選擇的
type:參數類型,支持的類型有String Number Array Object
defaults: argument默認值
banner:字符串顯示的使用說明(這是默認提供)
注意:參數必須的定義在construct函數內,否則當你使用generator調用命令(如:yo webapp --help)的時候,不能夠輸出相關的幫助信息。
示例:
var _ = require('lodash'); module.exports = generators.Base.extend({ // note: arguments and options should be defined in the constructor. constructor: function () { generators.Base.apply(this, arguments); // This makes `appname` a required argument. this.argument('appname', { type: String, required: true }); // And you can then access it later on this way; e.g. CamelCased this.appname = _.camelCase(this.appname); } });
十三. Options
option和argument很相似,但是option是作爲命令行標識使用的,如yo webapp --coffee。
我們可可以通過generator.option()添加option。
示例:
module.exports = generators.Base.extend({ // note: arguments and options should be defined in the constructor. constructor: function () { generators.Base.apply(this, arguments); // This method adds support for a `--coffee` flag this.option('coffee'); // And you can then access it later on this way; e.g. this.scriptSuffix = (this.options.coffee ? ".coffee": ".js"); } });
十四. 輸出消息
輸出消息是通過generator.log模塊來處理實現的。不建議使用console.log輸出命令。
示例:
module.exports = generators.Base.extend({ myAction: function () { this.log('Something has gone wrong!'); } });
十五. 處理依賴關係
一般當你運行你的generator的時候,你經常需要通過 npm 和 Bower來安裝一些generator用到的依賴模塊。而這些任務是非常繁瑣的,爲了方便,yeoman將這部分任務抽離了出來。
npm
你只需要調用generator.npmInstall() 命令就可以執行npm安裝命令,yeoman確保了npm install只執行了一次,即使他被多個generator調用。
例如你想安裝lodash作爲dev dependency:
generators.Base.extend({ installingLodash: function() { this.npmInstall(['lodash'], { 'saveDev': true }); } });
上面代碼等同於調用了npm install lodash --save-dev命令。
Bower
你只需要調用generator.bowerInstall()即可啓動安裝命令。yeoman確保了bower install只執行了一次,即使他被多個generator調用。
npm && Bower
調用enerator.installDependencies()即可同時運行npm 和 bower。
其他tools
yeoman抽離了spawn命令,這個抽離保證了我們可以在Linux ,mac 以及windows系統上可以很好的運行。
假如你是一個PHP狂熱愛好者,你想運行composer命令,你可以這樣做:
generators.Base.extend({ install: function () { this.spawnCommand('composer', ['install']); } });
請確保面spawn命令在install隊列裏。因爲您的用戶不願意等待在那兒直到安裝命令完成。
十六. 上下文路徑
爲了方便文件流的輸入輸出,Yeoman使用兩種位置環境。
1. 目標上下文
目標上下文定義爲當前工作目錄或含.yo-rc.json文件最接近的父文件夾。該.yo-rc.json文件定義了一個generator項目的根目錄。該文件允許用戶在子目錄中運行命令,並讓他們在項目中可以運行。這確保了用戶行爲的一致。
你可以通過generator.destinationRoot()命令獲取目標路徑,也可以通過generator.destinationPath('sub/path')來拼一個路徑:
// Given destination root is ~/projects generators.Base.extend({ paths: function () { this.destinationRoot(); // returns '~/projects' this.destinationPath('index.js'); // returns '~/projects/index.js' } });
2. 模板上下文
模板上下文是你保存模板文件的目錄,他一般是你要讀取和複製的目錄。模板上下文一般是默認是定義在./templates/目錄的.你可以通過generator.sourceRoot('new/template/path')命令來重寫。你可以通過generator.sourceRoot()或者generator.templatePath('app/index.js').來獲取路徑。
generators.Base.extend({ paths: function () { this.sourceRoot(); // returns './templates' this.templatePath('index.js'); // returns './templates/index.js' } });
3. “內存”文件系統
當涉及到覆蓋用戶的文件的時候,yeoman非常的謹慎,基本上,每一個write動作都是一個爲已經存在的文件解決衝突的過程。幫助用戶嚴重需要覆蓋的內容。
4. 文件工具
generator的this.fs暴露出所有的文件方法,通過mem-fs editor .
十七.完整示例
'use strict'; var generators = require('yeoman-generator'); var mkdirp = require('mkdirp'); var yosay = require('yosay'); var chalk = require('chalk'); module.exports = generators.Base.extend({ constructor: function() { generators.Base.apply(this, arguments); this.option('coffee'); this.scriptSuffix = (this.options.coffee ? ".coffee": ".js"); }, initializing: function() { var message = chalk.bgBlack.bold('\nWelcome to webApp\n') + chalk.underline('webApp.github.io\n'); this.log(yosay(message)); }, prompting: function() { var prompts = [{ type:'input', name: 'appName', message: 'input app name .', default: 'webApp' }]; this.prompt(prompts, function (answers) { this.log(answers); }.bind(this)); }, configuring: function() { this.config.save(); }, selfFunction: function () { this.log("執行了自定義方法"); }, writing: function() { this.fs.copyTpl( this.templatePath('index.html'), this.destinationPath('public/index.html'), { title: 'Templating with Yeoman' } ); }, });
實列:
注:使用
_.template
(lodash的template功能)和this.fs.write
將模版中的關鍵字替換爲用戶的輸入項。
this.fs.readJSON
和this.fs.writeJSON
,則是將package.json模版中的數據讀取出來,作出一定修改寫成新的文件。最後使用
mkdirp
和this.fs.copy
構建工程目錄結構和將一些不要修改的配置文件copy到指定目錄。
發佈
首先npm官網註冊一個npm賬號,如果有則運行npm login
登陸。然後到工程根目錄下,運行npm publish
進行發佈。
官網api:https://yeoman.github.io/generator/Generator.html#destinationRoot