├── app ├── pages │ ├── bigpipe │ │ ├── middle.html │ │ ├── right.html │ │ ├── left.html │ │ ├── index.html │ │ └── controller.js │ ├── doc │ │ ├── about.html │ │ ├── scheme.html │ │ ├── routes.html │ │ ├── momeryleak.html │ │ ├── package.json.html │ │ ├── static.html │ │ ├── etag.html │ │ ├── config.html │ │ ├── favicon.html │ │ ├── cookie.html │ │ ├── filters.html │ │ ├── ejs.html │ │ ├── controller.js │ │ ├── logger.html │ │ ├── fekitVersion.html │ │ ├── index.html │ │ ├── cacti.html │ │ └── es6-generators.html │ ├── doc-session │ │ ├── other.html │ │ ├── user.html │ │ ├── controller.js │ │ └── index.html │ └── error.html └── layout │ ├── template-bigpipe.html │ ├── template.html │ └── template_fekit.html ├── version.png ├── public ├── favicon.ico ├── favicon.jpg └── favicon.png ├── config ├── version.json ├── filters.js ├── routes.json ├── log4js.json └── fekitVersion.js ├── ref └── ver │ └── versions.mapping ├── test.js ├── compose.js ├── package.json ├── core ├── lru.js └── pm2-cacti.js ├── master.js ├── README.md ├── co.js ├── worker.js ├── bin └── agate.js └── LICENSE /app/pages/bigpipe/middle.html: -------------------------------------------------------------------------------- 1 |
中间
-------------------------------------------------------------------------------- /app/pages/bigpipe/right.html: -------------------------------------------------------------------------------- 1 |
右边
-------------------------------------------------------------------------------- /app/pages/doc/about.html: -------------------------------------------------------------------------------- 1 |

<%= body %>

-------------------------------------------------------------------------------- /app/pages/doc/scheme.html: -------------------------------------------------------------------------------- 1 | https://github.com/MangroveTech/koa-scheme -------------------------------------------------------------------------------- /version.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RubyLouvre/agate/HEAD/version.png -------------------------------------------------------------------------------- /app/pages/bigpipe/left.html: -------------------------------------------------------------------------------- 1 |
2 | 左边 3 |
4 | -------------------------------------------------------------------------------- /app/pages/doc/routes.html: -------------------------------------------------------------------------------- 1 | express的路由规则 2 | 3 | http://nodeclass.com/articles/410667 -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RubyLouvre/agate/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/favicon.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RubyLouvre/agate/HEAD/public/favicon.jpg -------------------------------------------------------------------------------- /public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RubyLouvre/agate/HEAD/public/favicon.png -------------------------------------------------------------------------------- /app/pages/doc-session/other.html: -------------------------------------------------------------------------------- 1 |

欢迎来到<%= username %>

2 |

退出登陆

-------------------------------------------------------------------------------- /app/pages/doc-session/user.html: -------------------------------------------------------------------------------- 1 |

<%= username %> 已经登陆

2 |

跳到其他页面

3 |

退出登陆

-------------------------------------------------------------------------------- /config/version.json: -------------------------------------------------------------------------------- 1 | { 2 | "path": "ref/ver/versions.mapping", 3 | 4 | "qzzHost": "http://qunarzz.com", 5 | 6 | "qzz": "test" 7 | } -------------------------------------------------------------------------------- /app/pages/doc/momeryleak.html: -------------------------------------------------------------------------------- 1 | http://www.w3ctech.com/topic/842 2 | 3 | http://www.oschina.net/translate/tracking-down-memory-leaks-in-node-js-a-node-js-holiday-season -------------------------------------------------------------------------------- /app/pages/doc/package.json.html: -------------------------------------------------------------------------------- 1 | 建议一个空目录,然后在里面建package.json文件,里面只有 2 |
3 | {
4 |   "dependencies": {
5 |   }
6 | }
7 | 
-------------------------------------------------------------------------------- /config/filters.js: -------------------------------------------------------------------------------- 1 | //在这里集中添加各种全局过滤器 2 | var fekitVersion = require("./fekitVersion") 3 | module.exports = { 4 | //处理fekit前端资源版本号 5 | qzzUrl: fekitVersion.getBase64Path 6 | } -------------------------------------------------------------------------------- /app/pages/bigpipe/index.html: -------------------------------------------------------------------------------- 1 |
2 | left 3 |
4 |
5 | middle 6 |
7 |
8 | right 9 |
10 | -------------------------------------------------------------------------------- /app/pages/error.html: -------------------------------------------------------------------------------- 1 | 9 |

<%= name %>

10 |

<%= message %>

11 |

<%= stack %>

12 | -------------------------------------------------------------------------------- /app/layout/template-bigpipe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 用于显示BigPipe的 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /ref/ver/versions.mapping: -------------------------------------------------------------------------------- 1 | common.js#a4797f0cdce349992637b076a96f7e9b 2 | index.js#d9db02c3dabdb26bdbd7f8c0a4e05cbd 3 | request.js#47ea34ed8b3ca91cd81ff829c6cb6972 4 | oniui.js#96b68ebb8be798cdd126ad9d82484134 5 | renderTicker.js#6e413840fa9f9854ee6cc2a9dbfad54f 6 | common.css#e1d4defaabe9855a5ac608dd53c09d81 7 | oniui.css#b8686c3645bc0b4582df6ba23b3e76fa -------------------------------------------------------------------------------- /test.js: -------------------------------------------------------------------------------- 1 | var thunkify = require('thunkify'); 2 | var co = require('./co'); 3 | var fs = require('fs'); 4 | 5 | 6 | var readFile = thunkify(fs.readFile); 7 | 8 | co(function *() { 9 | var now = new Date - 0 10 | var a = yield readFile('./readme.md'); 11 | var b = yield readFile('./fwp_core.zip'); 12 | var c = yield readFile('./package.json'); 13 | console.log(a.length, b.length, c.length) 14 | console.log(new Date - now) 15 | }) -------------------------------------------------------------------------------- /app/pages/doc/static.html: -------------------------------------------------------------------------------- 1 |

静态资源处理

2 | 3 |

两个方案,staticstatic-cache

4 |

目前使用static-cache。

5 | 6 |
 7 | var path = require('path')
 8 | var staticCache = require('koa-static-cache')
 9 | 
10 | app.use(staticCache(path.join(__dirname, 'public'), {
11 |   maxAge: 365 * 24 * 60 * 60
12 | }))
13 | 
14 |

以后静态资源分门别类放到public目录下就行。

-------------------------------------------------------------------------------- /app/pages/doc/etag.html: -------------------------------------------------------------------------------- 1 |

HTTP 提供了许多页面缓存的方案,其中属 Etag 和 Last-Modified 应用最广。 2 | 如果想拿到 Etag,就必须先拿到要输出的数据,所以 Etag 只能减少带宽的占用,并不能降低服务器的消耗。 3 | 如果是静态页面,可以判断文件最近一次的修改时间(Last-Modified), 4 | 获取文件上次修改时间的消耗比拿到整个数据的消耗要小的多。所以很多时候 Etag 都是配合这 Last-Modified 一起使用的。

5 |

需要配合koa-conditional-get使用

6 |

etag session保存http会话

7 |

etag 介绍

8 | 9 | -------------------------------------------------------------------------------- /app/pages/doc/config.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | TODO supply a title 10 | 11 | 12 | 13 | 14 |
TODO write content
15 | 16 | 17 | -------------------------------------------------------------------------------- /compose.js: -------------------------------------------------------------------------------- 1 | module.exports = compose; 2 | 3 | /** 4 | * Compose `middleware` returning 5 | * a fully valid middleware comprised 6 | * of all those which are passed. 7 | * 8 | * @param {Array} middleware 9 | * @return {Function} 10 | * @api public 11 | */ 12 | 13 | function compose(middleware){ 14 | return function *(next){ 15 | if (!next) next = noop(); 16 | 17 | var i = middleware.length; 18 | 19 | while (i--) { 20 | next = middleware[i].call(this, next); 21 | } 22 | 23 | yield *next; 24 | } 25 | } 26 | 27 | /** 28 | * Noop. 29 | * 30 | * @api private 31 | */ 32 | 33 | function *noop(){} -------------------------------------------------------------------------------- /app/pages/bigpipe/controller.js: -------------------------------------------------------------------------------- 1 | var path = require("path") 2 | exports.index = function * (next) { 3 | var BigPipe = require("bigpipe") 4 | var b = new BigPipe(4) 5 | var layout = app.getLayout("template-bigpipe.html") 6 | this.body = b.stream 7 | this.type = "html" 8 | 9 | b.begin(layout) 10 | 11 | b.flush(path.join(__dirname, "./index.html"), 0) 12 | 13 | setTimeout(function () { 14 | b.flush(path.join(__dirname, "./left.html"), 1) 15 | }, 2000) 16 | setTimeout(function () { 17 | b.flush(path.join(__dirname, "./middle.html"), 2) 18 | }, 1000) 19 | b.flush(path.join(__dirname, "./right.html"), 3) 20 | //https://www.ibm.com/developerworks/cn/java/j-lo-bigpipe/ 21 | } -------------------------------------------------------------------------------- /app/pages/doc-session/controller.js: -------------------------------------------------------------------------------- 1 | exports.login = function*(next) { 2 | var username = this.session.username 3 | 4 | if (username) { 5 | this.redirect("/user") 6 | } else if (this.method == 'POST') { 7 | var body = this.request.body 8 | this.session.username = body.username 9 | this.redirect("/user") 10 | } else { 11 | yield this.render("doc-session/index") 12 | } 13 | } 14 | 15 | 16 | exports.user = function*(next) { 17 | yield this.render("doc-session/user", { 18 | username: this.session.username 19 | }) 20 | 21 | } 22 | 23 | exports.other = function*(next) { 24 | yield exports.user 25 | } 26 | 27 | 28 | exports.logout = function*(next) { 29 | delete this.session.username 30 | this.redirect("/login") 31 | } -------------------------------------------------------------------------------- /config/routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "get /": "doc index", 3 | "get /bigpipe": "bigpipe index", 4 | "get /cacti": "doc cacti", 5 | "get /cookie": "doc cookie", 6 | "get /doc": "doc index", 7 | "get /ejs": "doc ejs", 8 | "get /es6-generators": "doc es6-generators", 9 | "get /favicon": "doc favicon", 10 | "get /fekitVersion": "doc fekitVersion", 11 | "get /filters": "doc filters", 12 | "get /logger": "doc logger", 13 | "get /login": "doc-session login", 14 | "get /logout": "doc-session logout", 15 | "get /momeryleak": "doc momeryleak", 16 | "get /node": "node index", 17 | "get /sessionother": "doc-session other", 18 | "get /static": "doc static", 19 | "get /user": "doc-session user", 20 | "get /xxx": "home index", 21 | "post /login": "doc-session login" 22 | } -------------------------------------------------------------------------------- /app/pages/doc/favicon.html: -------------------------------------------------------------------------------- 1 |

网站ICON

2 |

生成 favicon.ico图标

3 |

设置 favicon.ico图标

4 |

5 |

两个步骤:

6 |

1,把做好的favicon.ico图标文件上传到网站根目录;

7 |

2,把以下代码放到网页Html代码中的<head>部分:

8 |
 9 |         <link rel="icon" href="/favicon.ico" mce_href="/favicon.ico" type="image/x-icon">
10 |         <link rel="shortcut icon" href="/favicon.ico" mce_href="/favicon.ico" type="image/x-icon">
11 |     
12 |
13 |

对苹果移动设备的支持

14 | 15 | 16 | -------------------------------------------------------------------------------- /config/log4js.json: -------------------------------------------------------------------------------- 1 | { 2 | "appenders": [ 3 | { "type" : "console" }, 4 | { 5 | "type": "dateFile", 6 | "filename": "logs/access.log", 7 | "pattern": "-yyyy-MM-dd", 8 | "category": "normal", 9 | "level": "LOG" 10 | }, 11 | { 12 | "type": "file", 13 | "filename": "logs/error.log", 14 | "maxLogSize": 20480, 15 | "backups": 3, 16 | "category": "error" 17 | }, 18 | { 19 | "type": "file", 20 | "filename": "logs/record.log", 21 | "maxLogSize": 20480, 22 | "backups": 3, 23 | "category": "record" 24 | } 25 | ], 26 | "replaceConsole": false, 27 | "levels": { 28 | "error": "error", 29 | "record": "trace" 30 | } 31 | } -------------------------------------------------------------------------------- /app/pages/doc-session/index.html: -------------------------------------------------------------------------------- 1 |

session的使用

2 |

session我们已经在app.js中配置好了,在action方法中使用时,就是简单的对this.session添加或删除键值对。

3 |
 4 | exports.login = function*(next) {
 5 |   var username = this.session.username
 6 |   if (username) {
 7 |     this.redirect("/user")
 8 |   } else if (this.method == 'POST') {
 9 |     var body = this.request.body
10 |     this.session.username = body.username
11 |     this.redirect("/user")
12 |   } else {
13 |     yield this.render("doc-session/index")
14 |   }
15 | }
16 | 
17 |

具体例子可见pages/doc-session的例子:

18 |
19 |
20 |

名字:

21 |

密码:

22 |

23 |
24 |
-------------------------------------------------------------------------------- /app/layout/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | agate 5 | 6 | 7 | 8 | 9 | 10 | 11 | 19 | 20 | 21 | 22 | 23 | 24 | 25 |
<%- body %>
26 | 27 | 28 | -------------------------------------------------------------------------------- /config/fekitVersion.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs') 2 | var path = require('path') 3 | //得到同一目录下的version.json配置文件 4 | var versionJson = require("./version.json") 5 | 6 | var qzzPath = [versionJson.qzzHost, '/', versionJson.qzz, '/prd/'].join('') //网络路径 7 | var qzzLocalPath = path.join(path.resolve(__dirname + '/..'), versionJson.path) //硬盘路径 8 | var mapping = fs.readFileSync(qzzLocalPath, 'utf-8') //这是一个字符串 9 | 10 | var versions = {} //转换为一个以文件名:版本号形式存放的对象 11 | mapping.split('\n').forEach(function (val) { 12 | var kv = val.split('#') 13 | versions[kv[0].replace(/^\.[\/\\]/, '')] = kv[1] 14 | }) 15 | 16 | exports.getBase64Path = function (key) { 17 | key = key.replace(/^[\.\/\\]+/, '') 18 | var ver 19 | if (versions.hasOwnProperty(key)) { 20 | ver = versions[key] || '' 21 | } else { 22 | ver = 'dev' 23 | } 24 | key = key.replace(/(\.js|\.css)$/, '@' + ver + '$1') 25 | return qzzPath + key //返回一个完整的路径,并且最后添加上 @ + base64 26 | } -------------------------------------------------------------------------------- /app/pages/doc/cookie.html: -------------------------------------------------------------------------------- 1 |

cookie

2 |

koa已经整合了cookie功能,是基于https://github.com/pillarjs/cookies这个模块实现的

3 |

我们可以在控制器上这样访问的所有cookies

4 |
 5 | //pages/doc/controller.js
 6 | exports.cookie = function *(next) {
 7 |    var cookie = this.request.header.cookie
 8 |    yield this.render("doc/index", {
 9 |        cookie: cookie
10 |    })
11 | }
12 | 
13 |

设置cookie

14 |
15 | exports.cookie = function *(next) {
16 |     this.cookies.set('aaa', 'bbb' ); 
17 |     this.cookies.set('xxx','yyy' );
18 |    yield this.render("doc/index")
19 | }
20 | 
21 |

获取cookie

22 |
23 | exports.cookie = function *(next) {
24 |    var a = this.cookies.get('aaa')
25 |    yield this.render("doc/index",{
26 |       aaa: a
27 |    })
28 | }
29 | 
30 | <%= cookie %> -------------------------------------------------------------------------------- /app/pages/doc/filters.html: -------------------------------------------------------------------------------- 1 |

过滤器

2 |

它在其他语言也叫做View Helpers。 由于是使用ejs, 因此下面的定义或用法,都是符合ejs filters的规格。

3 |

在ejs里面默认提供了如下过滤器, 详见这里

4 | 28 |

但光是这个是不够用的,因此config目录下有一个filters文件,专门用于定义全局都使用的视图过滤器。 29 | 现在只添加了一个我们去哪儿网用到的qzzUrl过滤器

-------------------------------------------------------------------------------- /app/pages/doc/ejs.html: -------------------------------------------------------------------------------- 1 |

ejs模板

2 | 3 | 全局配置 4 | 5 | ```javascript 6 | var koa = require('koa'); 7 | var render = require('koa-ejs'); 8 | var path = require('path'); 9 | var app = koa(); 10 | render(app, { 11 | root: path.join(__dirname, 'view'), //这是所有模板的目录 12 | layout: 'template', //这是默认的layout 13 | viewExt: 'html', //这是模板的后缀名,只有这类文件才能被匹配当成模板使用 14 | cache: false, //是否缓存,开发时为false,上线应当设为flase 15 | debug: true 16 | // locals: locals, //全局函数 17 | // filters: filters //全局过滤器 18 | }); 19 | ``` 20 | render方法使用 21 | ```javascript 22 | ejs.render(str, options); //模板名, 配置对象,里面可以重设上面参数 23 | ``` 24 | 一个简单例子,某个action的代码 25 | ```javascript 26 | router.get('/users', function *(next) { 27 | var users = ["司徒正美", "清风火羽", "古道瘦马"] 28 | console.log(users) 29 | yield this.render('list', { 30 | layout: "template2", 31 | h2: "这是用户列表页", 32 | users: users 33 | }) 34 | }) 35 | ``` 36 | 详见 http://blog.csdn.net/zhangxin09/article/details/18409119 37 | 38 | 如何使用agate 39 | -------------------------------------------------------------------------------- /app/layout/template_fekit.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | agate 5 | 6 | 7 | 8 | 9 | 10 | <%links.forEach(function(link) {%> 11 | 12 | <%})%> 13 | 18 | 19 | 20 | 21 |

这是模块1

22 |
<%- body %>
23 | <%scripts.forEach(function(script) {%> 24 | 25 | <%})%> 26 | 27 | 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "agatejs", 3 | "description": "qunar hotel node framework.", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/RubyLouvre/agate.git" 7 | }, 8 | "main": "master.js", 9 | "author": "RubyLouvre <1669866773@qq.com>", 10 | "version": "0.0.1", 11 | "keywords": [ 12 | "koa", 13 | "middleware", 14 | "router" 15 | ], 16 | "dependencies": { 17 | "cli-color": "^0.3.3", 18 | "commander": "^2.7.1", 19 | "koa": "^0.18", 20 | "koa-bodyparser": "^1.4.1", 21 | "koa-conditional-get": "^1.0.2", 22 | "koa-ejs": "^1.1.3", 23 | "koa-etag": "^2.0.0", 24 | "koa-flash": "^1.0.0", 25 | "koa-router": "^4.3.0", 26 | "koa-session": "^3.1.0", 27 | "koa-static-cache": "^3.1.0", 28 | "log4js": "^0.6.0", 29 | "merge": "^1.2.0", 30 | "mkdirp": "^0.5.0", 31 | "nodemon": "^1.3.7", 32 | "open": "0.0.5", 33 | "parse5": "^1.4.2", 34 | "request": "^2.55.0" 35 | }, 36 | "bin": { 37 | "agate": "bin/agate.js" 38 | }, 39 | "devDependencies": {}, 40 | "scripts": {}, 41 | "engines": { 42 | "node": "> 0.11.4" 43 | }, 44 | "license": "MIT" 45 | } 46 | -------------------------------------------------------------------------------- /app/pages/doc/controller.js: -------------------------------------------------------------------------------- 1 | 2 | exports.index = function *(next) { 3 | yield this.render("doc/index", { 4 | body: "这是文档首页" 5 | }) 6 | } 7 | 8 | exports.logger = function *(next) { 9 | yield this.render("doc/logger") 10 | } 11 | 12 | exports.cookie = function *(next) { 13 | this.cookies.set('aaa', 'bbb' ); 14 | this.cookies.set('xxx','yyy' ); 15 | yield this.render("doc/cookie",{ 16 | cookie: "author=司徒正美" 17 | }) 18 | } 19 | 20 | exports["es6-generators"] = function *(next) { 21 | yield this.render("doc/es6-generators") 22 | } 23 | 24 | exports["favicon"] = function *(next) { 25 | yield this.render("doc/favicon") 26 | } 27 | exports["ejs"] = function *(next) { 28 | yield this.render("doc/ejs") 29 | } 30 | 31 | exports["momeryleak"] = function *(next) { 32 | yield this.render("doc/momeryleak") 33 | } 34 | 35 | exports["static"] = function *(next) { 36 | yield this.render("doc/static") 37 | } 38 | 39 | exports["filters"] = function *(next) { 40 | yield this.render("doc/filters") 41 | } 42 | 43 | exports["fekitVersion"] = function *(next) { 44 | yield this.render("doc/fekitVersion", { 45 | links: ["common.css"], 46 | scripts: ["common.js"], 47 | layout: "../layout/template_fekit" 48 | }) 49 | } 50 | 51 | exports["cacti"] = function *(next) { 52 | yield this.render("doc/cacti") 53 | } -------------------------------------------------------------------------------- /core/lru.js: -------------------------------------------------------------------------------- 1 | 2 | // https://github.com/rsms/js-lru 3 | // jshint ignore:line 4 | function LRU(maxLength) { 5 | this.size = 0 6 | this.limit = maxLength 7 | this.head = this.tail = void 0 8 | this._keymap = {} 9 | } 10 | 11 | var p = LRU.prototype 12 | 13 | p.put = function (key, value) { 14 | var entry = { 15 | key: key, 16 | value: value 17 | } 18 | this._keymap[key] = entry 19 | if (this.tail) { 20 | this.tail.newer = entry 21 | entry.older = this.tail 22 | } else { 23 | this.head = entry 24 | } 25 | this.tail = entry 26 | if (this.size === this.limit) { 27 | this.shift() 28 | } else { 29 | this.size++ 30 | } 31 | return value 32 | } 33 | 34 | p.shift = function () { 35 | var entry = this.head 36 | if (entry) { 37 | this.head = this.head.newer 38 | this.head.older = 39 | entry.newer = 40 | entry.older = 41 | this._keymap[entry.key] = void 0 42 | } 43 | } 44 | p.get = function (key) { 45 | var entry = this._keymap[key] 46 | if (entry === void 0) 47 | return 48 | if (entry === this.tail) { 49 | return entry.value 50 | } 51 | // HEAD--------------TAIL 52 | // <.older .newer> 53 | // <--- add direction -- 54 | // A B C E 55 | if (entry.newer) { 56 | if (entry === this.head) { 57 | this.head = entry.newer 58 | } 59 | entry.newer.older = entry.older // C <-- E. 60 | } 61 | if (entry.older) { 62 | entry.older.newer = entry.newer // C. --> E 63 | } 64 | entry.newer = void 0 // D --x 65 | entry.older = this.tail // D. --> E 66 | if (this.tail) { 67 | this.tail.newer = entry // E. <-- D 68 | } 69 | this.tail = entry 70 | return entry.value 71 | } 72 | 73 | 74 | module.exports = LRU -------------------------------------------------------------------------------- /app/pages/doc/logger.html: -------------------------------------------------------------------------------- 1 |

日志输出log4js

2 |

我们通常用console.log来打印各种调试消息,但它受限了控制台那个小小的显示区, 3 | 并且服务器一挂或一关就什么都没有,为了事后进行跟踪,我们需要将日志输出到硬盘上.于是有了各种各样的日志系统.

4 |

在JAVA界里,有一个久负盛名的log4j, 不是一般的好用,因此我们需要重复造轮子,只需将语言改一改,于是就有了log4js.

5 |

6 | log4js 有三个主要组件:loggers(记录点), appenders(挂载点)和layouts(布局)。 7 | 这三类组件一起应用,可以让开发人员能够根据日志的类型和级别进行记录,并且能在程序运行时控制log信息输出的格式和往什么地方输出信息。 8 |

9 |

在agate框架中, log4js的配置是放于config/log4js.json中。

10 | 11 |
12 | { 
13 |   "appenders": [  
14 |   // 下面一行应该是用于跟express配合输出web请求url日志的
15 |   {"type": "console", "category": "console"}, 
16 |   // 定义一个日志记录器
17 |   {      
18 |     "type": "dateFile",                 // 日志文件类型,可以使用日期作为文件名的占位符
19 |     "filename": "e:/weblogs/logs/",     // 日志文件名,可以设置相对路径或绝对路径
20 |     "pattern": "debug/yyyyMMddhh.txt",  // 占位符,紧跟在filename后面
21 |     "absolute": true,                   // filename是否绝对路径
22 |     "alwaysIncludePattern": true,       // 文件名是否始终包含占位符
23 |     "category": "logInfo"               // 记录器名
24 |   } ],
25 |   "levels":{ "logInfo": "DEBUG"}        // 设置记录器的默认显示级别,低于这个级别的日志,不会输出
26 | }
27 | 
28 | 29 |

log4js的输出级别6个: trace, debug, info, warn, error, fatal

30 |
31 | logger.trace("Entering cheese testing");
32 | logger.debug("Got cheese.");
33 | logger.info("Cheese is Gouda.");
34 | logger.warn("Cheese is quite smelly.");
35 | logger.error("Cheese is too ripe!");
36 | logger.fatal("Cheese was breeding ground for listeria.");
37 | 
38 |

项目中使用

39 |
40 |  log4js.getLogger("error").error(controller + " 控制器没有定义" )
41 | 
42 |

更多请参考

43 | -------------------------------------------------------------------------------- /app/pages/doc/fekitVersion.html: -------------------------------------------------------------------------------- 1 |

fekit ver文件部署

2 | 3 |

使用了fekit的前端项目,在页面中引用资源的地址如下所示:

4 | 5 |
<script src="http://qunarzz.com/hotel_fekit/prd/scripts/base@d7dadc627df2c525fc695a60bcba9f18.js"></script>
 6 | 
7 | 8 |

该链接由两部分构成,地址部分http://qunarzz.com/hotel_fekit/prd/scripts/base.js以及版本号@d7dadc627df2c525fc695a60bcba9f18

9 | 10 |

版本号是fekit在发布系统中生成的,执行fekit min对静态资源进行打包压缩之后,生成versions.mapping文件,内容如下所示:

11 | 12 |
common.js#a4797f0cdce349992637b076a96f7e9b
13 |     index.js#d9db02c3dabdb26bdbd7f8c0a4e05cbd
14 |     request.js#47ea34ed8b3ca91cd81ff829c6cb6972
15 |     oniui.js#96b68ebb8be798cdd126ad9d82484134
16 |     renderTicker.js#6e413840fa9f9854ee6cc2a9dbfad54f
17 |     common.css#e1d4defaabe9855a5ac608dd53c09d81
18 |     oniui.css#b8686c3645bc0b4582df6ba23b3e76fa
19 | 
20 | 21 |

形式为:<文件名>#<版本号>

22 | 23 |

在ejs模板template_fekit.html中,使用:

24 | 25 |

26 | <%links.forEach(function(link) {%>
27 |     <link rel="stylesheet" href="<%=: link|qzzUrl %>">
28 | <%})%>
29 | 
30 | <%scripts.forEach(function(script) {%>
31 |     <script type="text/javascript" src="<%=: script|qzzUrl %>"></script>
32 | <%})%>
33 | 
34 | 35 |

引入静态资源。

36 | 37 |

在app.js中配置ejs的地方增加filters:

38 | 39 |

40 | render(app, {
41 |     root: path.join(__dirname, 'app', "pages"),
42 |     layout: '../layout/template',
43 |     viewExt: 'html',
44 |     cache: false,
45 |     debug: true,
46 |     filters: {
47 |         //处理fekit前端资源版本号
48 |         qzzUrl: fekitVersion.version
49 |     }
50 |     // locals: locals,
51 |     // filters: filters
52 | });
53 | 
54 | 55 |

config目录下,增加配置version.json:

56 | 57 |

58 | {
59 |     "path": "ref/ver/versions.mapping", //versions.mapping路径
60 | 
61 |     "qzzHost": "http://qunarzz.com",        //qzz域名
62 | 
63 |     "qzz": "test"                           //工程名
64 | }
65 | 
66 | 67 |

渲染页面时,使用:

68 | 69 |

70 | exports["fekitVersion"] =  function *(next) {
71 |     yield this.render("doc/fekitVersion", {
72 |         links: ["common.css"],
73 |         scripts: ["common.js"],
74 |         layout: "../layout/template_fekit"
75 |     })
76 | }
77 | 
78 | 79 |

即可。

-------------------------------------------------------------------------------- /master.js: -------------------------------------------------------------------------------- 1 | /* 2 | nodemon 适合开发使用, 正式环境使用forever 3 | 4 | http://snoopyxdy.blog.163.com/blog/static/601174402013520103319858/ 5 | http://www.infoq.com/cn/articles/nodejs-cluster-round-robin-load-balancing 6 | */ 7 | 8 | var cluster = require('cluster') 9 | var os = require('os') 10 | var http = require('http') 11 | var workers = {}; 12 | process.on('uncautchException', function (err) { 13 | console.error(err.stack) 14 | //do nothing 15 | }); 16 | 17 | var numCPUs = os.cpus().length * 2 18 | console.log("CPU " + numCPUs) 19 | 20 | if (cluster.isMaster) { 21 | // Master: 22 | // Let's fork as many workers as you have CPU cores 23 | 24 | for (var i = 0; i < numCPUs; i++) { 25 | var w = cluster.fork() 26 | workers[w.id] = { 27 | crashCount: 0, 28 | worker: w, 29 | lastTime: new Date() 30 | } 31 | 32 | } 33 | 34 | http.createServer(function (req, res) { 35 | res.writeHead(200); 36 | if ('check your bussiness' === 'check your bussiness') { 37 | res.end("重启成功\n"); 38 | setTimeout(function () { 39 | for(var i in workers){ 40 | workers[i].worker.disconnect() 41 | } 42 | 43 | process.exit(0); 44 | }, 0); 45 | } else { 46 | res.end("重启失败,因为wulawulaWula\n"); 47 | } 48 | }).listen(7543); //这个端口是个后门,不应该被他人访问 49 | 50 | cluster.on('exit', function (worker, code, signal) { 51 | console.log('worker ' + worker.process.pid + ' died'); 52 | }); 53 | 54 | cluster.on('disconnect', function (worker) { 55 | var w = workers[worker.id] 56 | w.crashCount++ 57 | var now = new Date() 58 | if (now - w.lastTime > 10 * 60 * 1000) { 59 | w.crashCount = 0 60 | w.lastTime = now 61 | } 62 | if (w.crashCount > 60) { 63 | console.error('panic') 64 | return process.exit(1) 65 | } 66 | delete workers[worker.id] 67 | var newWorker = cluster.fork() //respawn 68 | workers[newWorker.id] = { 69 | crashCount: 0, 70 | worker: newWorker, 71 | lastTime: new Date() 72 | } 73 | }); 74 | } else { 75 | // Worker: 76 | // Let's spawn a HTTP server 77 | // (Workers can share any TCP connection. 78 | // In this case its a HTTP server) 79 | require("./worker.js") 80 | 81 | } 82 | 83 | 84 | -------------------------------------------------------------------------------- /app/pages/doc/index.html: -------------------------------------------------------------------------------- 1 |

agate使用教程

2 |

扼要地说, 在app里面写业务代码, 在config里写各种配置,在public里放静态页面。

3 |

当你下载好本框架后,直接npm install就能安装好各种依赖, 4 | 然后进入config目录,使用 pm2 start processes.json, 于是服务器就起来了。

5 |

app是写业务代码, 里面有两个目录pages与layout, pages下面应该是一个个目录,每个目录代表一个页面。 6 | 每个目录有一个controller.js(它就是MVC中的C),C里面以这样的形式组织代码: 7 |

8 |
  9 | exports.index  = function*(next) {
 10 |     yield this.render("doc-session/index",{xxx: "111"})
 11 | }
 12 | 
 13 | exports.list  = function*(next) {
 14 |     yield this.render("doc-session/list")
 15 | }
 16 | 
 17 | exports.create  = function*(next) {
 18 |     yield this.render("doc-session/create")
 19 | }
 20 | 
 21 | exports.delete  = function*(next) {
 22 |     yield this.render("doc-session/delete")
 23 | }
 24 | 
 25 | 
26 |

这些index, list, create, delete方法就是controller中的action,一个action应该对应页面上的一个HTTP请求,它用于响应请求返回数据或页面, 27 | 或者转交其他action进来处理(用术语来说就是重定向)。如果是返回页面,就使用this.render方法,它有两个参数, 28 | 一个是指定当前的子页面(子页面要结合layout才能变成一个完整的页面),第一个参数是各种数据及这个页面的其他配置。

29 |

现在我们有4个action,那么理应在该目录下建4个页面(MVC中的V),我们现在是使用ejs模块。详见下面模板引擎这一节

30 |

31 | 页面配置已经写app.js中了,没有把握请不要改动它 32 |

33 |
 34 | var render = require('koa-ejs');
 35 | render(app, {
 36 |     root: path.join(__dirname, 'app', "pages"), //所有页面模板所在的位置
 37 |     layout: '../layout/template',    //默认所有页面都使用这个layout
 38 |     viewExt: 'html',             
 39 |     cache: app.env !== "development" ,//开发环境不进行缓存
 40 |     debug: true
 41 |         // locals: locals,
 42 |         // filters: filters
 43 | });
 44 | 
45 |

如果我们有许多业务逻辑,那么请在对应目录下添加对应JS文件(MVC中的C),然后在action中调用

46 |

有了MVC了, 那么我们的请求如何才能到达这里呢,那就需要路由系统了。路由系统会根据config下的routes.js定义的路由规则进行定义 47 | 定义到对应的controller下的某action下。一个完整的路由规则如下: 48 |

49 |
 50 | routes["get /xxxx"] = {
 51 |     controller: "xxx",
 52 |     action: "index"
 53 | }
 54 | 
55 | 56 |

我们写代码时,其操作过程是反过来的,首先是在routes.js下添加路由规则,然后在pages目录下建议你的页面目录,下面建立controller.js 57 | 然后里面有多少个请求,就建议多少个action,然后再建立对应的页面及模型JS, 如果不满意原layout页面,可以再建新的layout, 58 | 然后通过this.render方法的第2个参数指定。 59 |

60 |

这样就over了。什么日志, session, cookie, 多线程并发都为你准备好了。

61 | 62 |
    63 |
  1. es6 Generator学习
  2. 64 |
  3. 日志处理log4js
  4. 65 |
  5. 静态资源处理
  6. 66 |
  7. cookie
  8. 67 |
  9. session
  10. 68 |
  11. ejs模板
  12. 69 |
  13. 模板过滤器
  14. 70 |
  15. fekitVersion
  16. 71 |
  17. 内存泄漏检测
  18. 72 |
73 | 74 | 75 | 76 | 77 |
 78 | package.json使用
 79 | http://javascript.ruanyifeng.com/nodejs/packagejson.html
 80 | http://www.infoq.com/cn/articles/msh-using-npm-manage-node.js-dependence/
 81 | 
 82 | 监控
 83 | http://se77en.cc/2013/06/27/goodbye-node-forever-hello-pm2-translation/
 84 | 
 85 | http://segmentfault.com/a/1190000002394571
 86 | 
 87 | 当我们修改了业务代码,让node服务器自动重启,只监听某一两个文件夹的文件改动或新增删除
 88 | https://www.npmjs.com/package/nodemon
 89 | https://www.npmjs.com/package/pm2
 90 | 
 91 | 
 92 | 
 93 | 
 94 | 
 95 | 对model的字段进行格式化,验证
 96 | https://github.com/Textalk/angular-schema-form
 97 | 详看 https://github.com/gcanti/tcomb-form-native https://github.com/joshfire/jsonform
 98 | 
 99 | 
100 | 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | agate是去哪儿网酒店前端架构组推出一个nodejs框架,能帮你迅速搭好架子。 2 | 3 | 主要依赖技术 4 | __________________________ 5 | koa + nodemon + log4js + ... 6 | 7 | 为什么使用koa ? nodejs三大框架express, koa, hapi, 目前国内最流行的是前两者。 8 | 9 | express需要历史悠久,但版本众多,许多模块可能只运行于某一版本中,查起来非常麻烦,并且许多依赖都独立出去, 一盘散沙似的,不方便学习。 10 | 11 | 12 | koa是基于generator与co之上的新一代的中间件框架, 代表着历史的前进方向。虽然受限于generator的实现程度。但是它的优势却不容小觑。 13 | 14 | 1. 有了koa,我们可以很好的解决回调的问题。只要yield就行,还可以直接用try来捕获异常 15 | 2. koa会自动帮你改造node的req,res对象,省去你很多工作。再也不需要每个res.end都要写一大堆返回状态了, 16 | 也不需要各种检测错误了,也不需要每次都用finish来确保程序正常关闭了。 17 | 3. 内置了很多以前express的第三方基础库,更加方便。这样你写中间件的时候没必要到处安装依赖库。 18 | 19 | 目录结构 20 | ``` 21 | │ 22 | ├──agate.js 23 | ├──server.js 24 | ├──app 25 | │  ├──layouts 26 | │ │ └──layout1.html 27 | │  └──pages 28 | │ ├──home 29 | │ │ ├──controller.js 30 | │ │ └──index.html 31 | │ └──xxx 32 | │ ├──controller.js 33 | │ └──index.html 34 | ├──config 35 | │   ├──filters.js 36 | │   ├──log4js.js 37 | │   └──routes.json 38 | ├──core 39 | │   └──lur.js 40 | ├──bin 41 | │   └──agate.js 42 | ├──public 43 | │ └──favicon.ico 44 | ├──logs 45 | │ └──favicon.ico 46 | ├──node_modules 47 | └──package.json 48 | ``` 49 | http://www.veryhuo.com/a/view/39755.html 50 | 51 | 52 | ## 启动命令 53 | ``` 54 | agate start 3000 '' prod 55 | agate start 3000 '' test 56 | agate start 3000 '' dev 57 | ``` 58 | 59 | ## 脚手架命令 60 | ```javascript 61 | node --harmony agate 62 | ``` 63 | 64 | ##直接启动命令 65 | ```javascript 66 | agate agate scaffold /test2 test2 index post#create 67 | ``` 68 | 69 | 70 | 在开发环境使用 nodemon, 在生产环境使用pm2 71 | //http://ourjs.com/detail/52456ae04cd0e14503000009 72 | 73 | 74 | 75 | 为什么使用log4js ? 其前身是log4j, 历史悠久, 质量有保证, 并且提供各种日志打印方式及保存方案。 76 | 77 |

扼要地说, 在app里面写业务代码, 在config里写各种配置,在public里放静态页面。

78 |

当你下载好本框架后,直接npm install就能安装好各种依赖, 79 | 然后进入config目录,使用 pm2 start processes.json, 于是服务器就起来了。

80 |

app是写业务代码, 里面有两个目录pages与layout, pages下面应该是一个个目录,每个目录代表一个页面。 81 | 每个目录有一个controller.js(它就是MVC中的C),C里面以这样的形式组织代码: 82 |

83 | ```javascript 84 | exports.index = function*(next) { 85 | yield this.render("doc-session/index",{xxx: "111"}) 86 | } 87 | 88 | exports.list = function*(next) { 89 | yield this.render("doc-session/list") 90 | } 91 | 92 | exports.create = function*(next) { 93 | yield this.render("doc-session/create") 94 | } 95 | 96 | exports.delete = function*(next) { 97 | yield this.render("doc-session/delete") 98 | } 99 | 100 | ``` 101 |

这些index, list, create, delete方法就是controller中的action,一个action应该对应页面上的一个HTTP请求,它用于响应请求返回数据或页面, 102 | 或者转交其他action进来处理(用术语来说就是重定向)。如果是返回页面,就使用this.render方法,它有两个参数, 103 | 一个是指定当前的子页面(子页面要结合layout才能变成一个完整的页面),第一个参数是各种数据及这个页面的其他配置。

104 |

现在我们有4个action,那么理应在该目录下建4个页面(MVC中的V),我们现在是使用ejs模块。详见下面模板引擎这一节

105 |

106 | 页面配置已经写app.js中了,没有把握请不要改动它 107 |

108 | ```javascript 109 | var render = require('koa-ejs'); 110 | render(app, { 111 | root: path.join(__dirname, 'app', "pages"), //所有页面模板所在的位置 112 | layout: '../layout/template', //默认所有页面都使用这个layout 113 | viewExt: 'html', 114 | cache: app.env !== "development" ,//开发环境不进行缓存 115 | debug: true 116 | // locals: locals, 117 | // filters: filters 118 | }); 119 | ``` 120 |

如果我们有许多业务逻辑,那么请在对应目录下添加对应JS文件(MVC中的C),然后在action中调用

121 |

有了MVC了, 那么我们的请求如何才能到达这里呢,那就需要路由系统了。路由系统会根据config下的routes.js定义的路由规则进行定义 122 | 定义到对应的controller下的某action下。一个完整的路由规则如下: 123 |

124 | ```javascript 125 | routes["get /xxxx"] = { 126 | controller: "xxx", 127 | action: "index" 128 | } 129 | ``` 130 | 131 |

我们写代码时,其操作过程是反过来的,首先是在routes.js下添加路由规则,然后在pages目录下建议你的页面目录,下面建立controller.js 132 | 然后里面有多少个请求,就建议多少个action,然后再建立对应的页面及模型JS, 如果不满意原layout页面,可以再建新的layout, 133 | 然后通过this.render方法的第2个参数指定。 134 |

135 |

这样就over了。什么日志, session, cookie, 多线程并发都为你准备好了。

136 | 137 | 更多教程,当你启动本工程后,首页就是教程首页。然后你再将routes中的路由规则重设首页,添加你自己的页面! 138 | 139 | 140 | -------------------------------------------------------------------------------- /app/pages/doc/cacti.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |

用途

4 | 5 |

该接口用于前端把收集好的数据记录到后端服务器上,并最后收集绘制出CACTI的监控图表。

6 | 7 |

接口地址:

8 | 9 |

http://m.ued.qunar.com/monitor/log

10 | 11 |

参数:

12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |
参数 类型 必填 备注
code String 监控名,参考规范
time int -- 时间(监控图的纵轴值),不传时记录只记录次数
logbrowser
1/0 -- 是否区分浏览器做记录,默认0
ignoreSpider
1/0 -- 是否忽略爬虫 的请求,1表示不记录爬虫的访问,默认0
count int -- 默认是1,表示该次请求记录N次count
52 |
53 | 54 | 55 |

返回:

56 | 57 |

接口返回一个1*1像素的gif,大约43字节。

58 | 59 |

能够区分出的浏览器:

60 | 61 |
62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 |
代码 含义
IE6 MSIE 6
IE7 MSIE 7
IE8 MSIE 8
IE9 MSIE 9
IE10 MSIE 10
IE11 MSIE 11
Chrome Chrome
Waterfox Waterfox
Firefox Firefox
Safari Safari
Android Android
iOS iOS
Winphone Windows Phone
UnknownTouch 其他移动设备
Unknown 未知浏览器
128 |
129 | 130 |

例子:

131 | 132 |

http://m.ued.qunar.com/monitor/log?code=hotel_detail_bktool_render&time=1024

133 |
134 |
135 | var img = new Image();
136 |     img.onload=function(){
137 |     //防止IE7 GC时在请求未发出去之前abort掉该请求。
138 |     img.onload=null;
139 |     img=null;
140 | };
141 | img.src="http://m.ued.qunar.com/monitor/log?code=hotel_detail_bktool_render&time=1024";
142 |         
143 |
144 |
-------------------------------------------------------------------------------- /core/pm2-cacti.js: -------------------------------------------------------------------------------- 1 | var logger = require('log4js').getLogger('pm2-cacti'), 2 | pm2 = require('pm2'), 3 | path = require('path'), 4 | merge = require('merge'), 5 | url = require('url'), 6 | request = require('request'); 7 | 8 | var config = merge({ 9 | INTERVAL: 60 * 1000, 10 | prefix: "", 11 | //for url format 12 | urlObj: { 13 | host: "", 14 | protocol: "http", 15 | pathname: "" 16 | }, 17 | //names which send to cacti monitor 18 | qsName: [ 19 | "RESTART_TIME", //restart counts 20 | "UNSTABLE_RESTART_TIME", //unstable restart counts 21 | "ONLINE", //online workers counts 22 | "LAST_UPDATE", 23 | "MEMORY" 24 | ] 25 | }, require(path.join(__dirname, "../config", "pm2-cacti.json"))), 26 | isReady = false, 27 | _reconnectingHandler, 28 | keyMap = {}, 29 | //L Latest 30 | //S Current 31 | VALUES = { 32 | L_RESTART_TIME : -1, 33 | L_UNSTABLE_RESTART_TIME : -1, 34 | 35 | S_RESTART_TIME : -1, 36 | S_UNSTABLE_RESTART_TIME : -1, 37 | S_ONLINE : -1, 38 | S_LAST_UPDATE : -1, 39 | S_MEMORY : -1 40 | }; 41 | 42 | 43 | //============启动监控============== 44 | //bus 45 | busLaunch(); 46 | 47 | //monitor 48 | setInterval(function() { 49 | pm2.connect(check) 50 | }, config.INTERVAL) 51 | 52 | /** 53 | * bus launch 54 | * use pm2 bus insteadof pm2-interface 55 | * https://github.com/Unitech/PM2/blob/master/doc/PROGRAMMATIC.md 56 | */ 57 | 58 | function busLaunch() { 59 | 60 | logger.info('Connecting to pm2'); 61 | 62 | pm2.launchBus(function(err, bus) { 63 | 64 | logger.info('Connected to pm2'); 65 | 66 | bus.on('*', function(event, data) { 67 | 68 | var name = event, 69 | processData = data.data, 70 | processPid = data.process && data.process.pm_id; 71 | 72 | if( name === 'log:out') 73 | return; 74 | 75 | if(processPid){ 76 | if( processData && typeof processData === 'number'){ 77 | keyMap[name] = 1; 78 | logger.info('Receive from PM2ID:%s[%s]+%s',processPid,name,processData); 79 | }else if( name === 'process:event' ){ 80 | logger.info('Receive from PM2ID:%s[process:event:%s]',processPid,data.event); 81 | }else{ 82 | logger.info('Receive from PM2ID:%s[%s]',processPid,name); 83 | } 84 | }else{ 85 | try{ 86 | logger.info('Received : [%s]%s',name,JSON.stringify(data||{}).substr(0,20) + "..."); 87 | }catch(e){ 88 | logger.info('Received : [%s]',name); 89 | } 90 | } 91 | 92 | }); 93 | 94 | bus.sock.on('reconnect attempt', function() { 95 | isReady = false; 96 | 97 | if( !_reconnectingHandler ){ 98 | _reconnectingHandler = setTimeout(function(){ 99 | _reconnectingHandler = null; 100 | },1000); 101 | logger.info("Reconnecting"); 102 | } 103 | }).on('connect', function() { 104 | logger.info('Connected to pm2'); 105 | }).on('closed',function(){ 106 | logger.info('closed'); 107 | }).on('close',function(){ 108 | logger.info('close'); 109 | }); 110 | 111 | }); 112 | } 113 | 114 | /** 115 | * get pm2 monitorData && send a request to cacti monitor 116 | */ 117 | function check() { 118 | 119 | pm2.list(function(err, list) { 120 | if(!err) { 121 | var online = 0, 122 | restart_time = 0, 123 | unstable_restarts = 0, 124 | memory = 0; 125 | 126 | list.forEach(function(proc){ 127 | //I_RESTART_TIME 128 | 129 | if(proc.pm2_env.status === 'online') 130 | online++; 131 | 132 | unstable_restarts += proc.pm2_env.unstable_restarts; 133 | restart_time += proc.pm2_env.restart_time; 134 | memory += proc.monit.memory; 135 | 136 | }); 137 | 138 | 139 | VALUES.S_ONLINE = online; 140 | 141 | VALUES.S_MEMORY = memory; 142 | 143 | if( VALUES.L_RESTART_TIME === -1 ){ 144 | VALUES.S_RESTART_TIME = 0; 145 | }else{ 146 | VALUES.S_RESTART_TIME = restart_time - VALUES.L_RESTART_TIME; 147 | } 148 | 149 | VALUES.L_RESTART_TIME = restart_time; 150 | 151 | 152 | if( VALUES.L_UNSTABLE_RESTART_TIME === -1 ){ 153 | VALUES.S_UNSTABLE_RESTART_TIME = 0; 154 | }else{ 155 | VALUES.S_UNSTABLE_RESTART_TIME = unstable_restarts - VALUES.L_UNSTABLE_RESTART_TIME; 156 | } 157 | 158 | VALUES.L_UNSTABLE_RESTART_TIME = unstable_restarts; 159 | 160 | VALUES.S_LAST_UPDATE = new Date().getTime(); 161 | 162 | 163 | for( var x in VALUES ){ 164 | if( VALUES[x] < 0 ) 165 | VALUES[x] = 0; 166 | 167 | var m = x.match(/^([S])_(.+)$/); 168 | 169 | if(m && config.qsName.indexOf(m[2]) > -1 ){ 170 | makeRequest(m[2], VALUES[x]); 171 | } 172 | } 173 | 174 | } else { 175 | //pm2 CLI 176 | //return cb ? cb({msg:err}) : exitCli(cst.ERROR_EXIT); 177 | logger.error(err.msg); 178 | } 179 | }) 180 | } 181 | 182 | //send monitor request 183 | //format likes http://m.ued.qunar.com/monitor/log?code=hotel_detail_bktool_render&time=1024 184 | function makeRequest(name, value) { 185 | var qsObj = {}; 186 | qsObj[config.prefix + "-" + name] = value; 187 | 188 | request({ 189 | url: url.format(config.urlObj), 190 | qs: qsObj 191 | }); 192 | } 193 | 194 | 195 | 196 | 197 | -------------------------------------------------------------------------------- /co.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * slice() reference. 4 | */ 5 | 6 | var slice = Array.prototype.slice; 7 | 8 | /** 9 | * Expose `co`. 10 | */ 11 | 12 | module.exports = co['default'] = co.co = co; 13 | 14 | /** 15 | * Wrap the given generator `fn` into a 16 | * function that returns a promise. 17 | * This is a separate function so that 18 | * every `co()` call doesn't create a new, 19 | * unnecessary closure. 20 | * 21 | * @param {GeneratorFunction} fn 22 | * @return {Function} 23 | * @api public 24 | */ 25 | 26 | co.wrap = function (fn) { 27 | createPromise.__generatorFunction__ = fn; 28 | return createPromise; 29 | function createPromise() { 30 | return co.call(this, fn.apply(this, arguments)); 31 | } 32 | }; 33 | 34 | /** 35 | * Execute the generator function or a generator 36 | * and return a promise. 37 | * 38 | * @param {Function} fn 39 | * @return {Promise} 40 | * @api public 41 | */ 42 | 43 | function co(gen) { 44 | var ctx = this; 45 | 46 | // we wrap everything in a promise to avoid promise chaining, 47 | // which leads to memory leak errors. 48 | // see https://github.com/tj/co/issues/180 49 | return new Promise(function(resolve, reject) { 50 | if (typeof gen === 'function') gen = gen.call(ctx); 51 | if (!gen || typeof gen.next !== 'function') return resolve(gen); 52 | 53 | onFulfilled(); 54 | 55 | /** 56 | * @param {Mixed} res 57 | * @return {Promise} 58 | * @api private 59 | */ 60 | 61 | function onFulfilled(res) { 62 | var ret; 63 | try { 64 | ret = gen.next(res); 65 | } catch (e) { 66 | return reject(e); 67 | } 68 | next(ret); 69 | } 70 | 71 | /** 72 | * @param {Error} err 73 | * @return {Promise} 74 | * @api private 75 | */ 76 | 77 | function onRejected(err) { 78 | var ret; 79 | try { 80 | ret = gen.throw(err); 81 | } catch (e) { 82 | return reject(e); 83 | } 84 | next(ret); 85 | } 86 | 87 | /** 88 | * Get the next value in the generator, 89 | * return a promise. 90 | * 91 | * @param {Object} ret 92 | * @return {Promise} 93 | * @api private 94 | */ 95 | 96 | function next(ret) { 97 | if (ret.done) return resolve(ret.value); 98 | var value = toPromise.call(ctx, ret.value); 99 | if (value && isPromise(value)) return value.then(onFulfilled, onRejected); 100 | return onRejected(new TypeError('You may only yield a function, promise, generator, array, or object, ' 101 | + 'but the following object was passed: "' + String(ret.value) + '"')); 102 | } 103 | }); 104 | } 105 | 106 | /** 107 | * Convert a `yield`ed value into a promise. 108 | * 109 | * @param {Mixed} obj 110 | * @return {Promise} 111 | * @api private 112 | */ 113 | 114 | function toPromise(obj) { 115 | if (!obj) return obj; 116 | if (isPromise(obj)) return obj; 117 | if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj); 118 | if ('function' == typeof obj) return thunkToPromise.call(this, obj); 119 | if (Array.isArray(obj)) return arrayToPromise.call(this, obj); 120 | if (isObject(obj)) return objectToPromise.call(this, obj); 121 | return obj; 122 | } 123 | 124 | /** 125 | * Convert a thunk to a promise. 126 | * 127 | * @param {Function} 128 | * @return {Promise} 129 | * @api private 130 | */ 131 | 132 | function thunkToPromise(fn) { 133 | var ctx = this; 134 | return new Promise(function (resolve, reject) { 135 | fn.call(ctx, function (err, res) { 136 | if (err) return reject(err); 137 | if (arguments.length > 2) res = slice.call(arguments, 1); 138 | resolve(res); 139 | }); 140 | }); 141 | } 142 | 143 | /** 144 | * Convert an array of "yieldables" to a promise. 145 | * Uses `Promise.all()` internally. 146 | * 147 | * @param {Array} obj 148 | * @return {Promise} 149 | * @api private 150 | */ 151 | 152 | function arrayToPromise(obj) { 153 | return Promise.all(obj.map(toPromise, this)); 154 | } 155 | 156 | /** 157 | * Convert an object of "yieldables" to a promise. 158 | * Uses `Promise.all()` internally. 159 | * 160 | * @param {Object} obj 161 | * @return {Promise} 162 | * @api private 163 | */ 164 | 165 | function objectToPromise(obj){ 166 | var results = new obj.constructor(); 167 | var keys = Object.keys(obj); 168 | var promises = []; 169 | for (var i = 0; i < keys.length; i++) { 170 | var key = keys[i]; 171 | var promise = toPromise.call(this, obj[key]); 172 | if (promise && isPromise(promise)) defer(promise, key); 173 | else results[key] = obj[key]; 174 | } 175 | return Promise.all(promises).then(function () { 176 | return results; 177 | }); 178 | 179 | function defer(promise, key) { 180 | // predefine the key in the result 181 | results[key] = undefined; 182 | promises.push(promise.then(function (res) { 183 | results[key] = res; 184 | })); 185 | } 186 | } 187 | 188 | /** 189 | * Check if `obj` is a promise. 190 | * 191 | * @param {Object} obj 192 | * @return {Boolean} 193 | * @api private 194 | */ 195 | 196 | function isPromise(obj) { 197 | return 'function' == typeof obj.then; 198 | } 199 | 200 | /** 201 | * Check if `obj` is a generator. 202 | * 203 | * @param {Mixed} obj 204 | * @return {Boolean} 205 | * @api private 206 | */ 207 | 208 | function isGenerator(obj) { 209 | return 'function' == typeof obj.next && 'function' == typeof obj.throw; 210 | } 211 | 212 | /** 213 | * Check if `obj` is a generator function. 214 | * 215 | * @param {Mixed} obj 216 | * @return {Boolean} 217 | * @api private 218 | */ 219 | function isGeneratorFunction(obj) { 220 | var constructor = obj.constructor; 221 | if (!constructor) return false; 222 | if ('GeneratorFunction' === constructor.name || 'GeneratorFunction' === constructor.displayName) return true; 223 | return isGenerator(constructor.prototype); 224 | } 225 | 226 | /** 227 | * Check for plain object. 228 | * 229 | * @param {Mixed} val 230 | * @return {Boolean} 231 | * @api private 232 | */ 233 | 234 | function isObject(val) { 235 | return Object == val.constructor; 236 | } -------------------------------------------------------------------------------- /worker.js: -------------------------------------------------------------------------------- 1 | var koa = require('koa'); 2 | var path = require('path'); 3 | var app = koa(); 4 | var http = require("http") 5 | var mkdirp = require('mkdirp') 6 | //============设置session============== 7 | app.keys = ['secret', 'key']; //https://github.com/koajs/koa/issues/203 8 | var session = require('koa-session') 9 | app.use(session(app)) 10 | global.app = app 11 | //============设置静态资源缓存============== 12 | //处理public目录下的js, css, jpg, png , ttf, woff, eot, otf, svg文件 13 | var staticCache = require('koa-static-cache') 14 | app.use(staticCache(path.join(__dirname, 'public'), { 15 | maxAge: 365 * 24 * 60 * 60 16 | })) 17 | 18 | //============设置etag============== 19 | var conditional = require('koa-conditional-get'); 20 | var etag = require('koa-etag'); 21 | app.use(conditional()); 22 | app.use(etag()); 23 | //============req.body============== 24 | //req.body为一个对象,以键值对的形式存放POST请求中的数据 25 | //使用 https://github.com/koajs/bodyparser 模块 26 | //受 https://github.com/stream-utils/raw-body https://github.com/Raynos/body 所启发 27 | //http://codeforgeek.com/2014/09/handle-get-post-request-express-4/ 28 | var bodyParser = require('koa-bodyparser'); 29 | app.use(bodyParser()); 30 | //============设置日志============= 31 | var log4js = require('log4js'); 32 | var loggerName = 'normal'; 33 | mkdirp.sync(path.join(__dirname, "logs")) 34 | var logjson = require(path.join(__dirname, "config", "log4js.json")) 35 | app.logger = log4js.configure(logjson); 36 | //============设置视图引擎============= 37 | 38 | app.layoutPath = path.join(__dirname, "app", "layout") 39 | app.getLayout = function(name){ 40 | return require("fs").readFileSync(path.join(app.layoutPath, name), "utf8") 41 | } 42 | 43 | 44 | var render = require('koa-ejs'); 45 | var filters = require(path.join(__dirname, "config", "filters")) 46 | render(app, { 47 | root: path.join(__dirname, 'app', "pages"), 48 | layout: '../layout/template', 49 | viewExt: 'html', 50 | cache: app.env !== "development", //开发环境不进行缓存 51 | debug: true, 52 | filters: filters 53 | // locals: locals, 54 | // filters: filters 55 | }); 56 | 57 | 58 | app.use(function*(next) { 59 | var req = this.request, 60 | header = req.header 61 | browser = header["user-agent"].replace(/\([^)]+\)/g, " ").replace(/\s+/g, " ") //去掉着小括号里面的内容 62 | log4js.getLogger("normal").info([req.method, req.url, browser].join(" ")) 63 | yield next; 64 | }) 65 | 66 | 67 | 68 | //============转为各种请求到对controller#action中去============== 69 | var router = require('koa-router')(); 70 | var routes = require(path.join(__dirname, "config", "routes.json")) 71 | 72 | var Cache = require(path.join(__dirname, "core", "lru.js")) 73 | //https://cnodejs.org/topic/4fa94df3b92b05485007fd87 防止撑爆内存,必须限制键值对数量,因此不用普通JS对象 74 | var controllers = new Cache(1024) //缓存所有控制器 75 | Object.keys(routes).forEach(function(key) { 76 | 77 | var methodRule = key.split(" ") 78 | var method = methodRule[0].toLowerCase() 79 | var rule = methodRule[1] 80 | var controllerAction = routes[key].split(" ") 81 | var scontroller = controllerAction[0] 82 | var saction = controllerAction[1] 83 | 84 | var controller 85 | var _controller = controllers.get(scontroller) 86 | if (typeof _controller === "object") { 87 | controller = _controller 88 | } else { 89 | try { 90 | var controllerPath = path.join(__dirname, "app", "pages", scontroller, "controller.js") 91 | controller = require(controllerPath) 92 | controllers.set(scontroller, controller) 93 | } catch (e) { 94 | // log4js.getLogger("error").error(scontroller + " 控制器没有定义") 95 | } 96 | 97 | } 98 | if (!controller) 99 | return 100 | var action = controller[saction] 101 | if (typeof action === "function") { 102 | if (typeof router[method] === "function") { 103 | router[method](rule, action) 104 | } else { 105 | log4js.getLogger("error").error(scontroller + "#" + saction + " 对应的路由规则【" + key + "】存在问题") 106 | } 107 | 108 | } else { 109 | log4js.getLogger("error").error(scontroller + " 控制器没有定义" + saction + " 方法") 110 | } 111 | 112 | }) 113 | 114 | 115 | 116 | app.use(router.routes()) 117 | app.use(router.allowedMethods()); 118 | //============设置错误处理============= 119 | app.on("error", function(err, ctx) { 120 | //https://github.com/koajs/examples/issues/20 121 | console.log("捕获到错误"+err) 122 | console.log(err.stack) 123 | }) 124 | 125 | 126 | app.use(function* pageNotFound(next) { 127 | yield next; 128 | if (404 != this.status) return; 129 | // we need to explicitly set 404 here 130 | // so that koa doesn't assign 200 on body= 131 | this.status = 404; 132 | switch (this.accepts('html', 'json')) { 133 | case 'html': 134 | this.type = 'html'; 135 | this.body = '

Page Not Found

'; 136 | break; 137 | case 'json': 138 | this.body = { 139 | message: 'Page Not Found' 140 | }; 141 | break 142 | default: 143 | this.type = 'text'; 144 | this.body = 'Page Not Found'; 145 | } 146 | }); 147 | 148 | void function(){ 149 | var port, url 150 | process.argv.slice(2).forEach(function(el) { 151 | if (!el.indexOf("port")) { 152 | port = parseFloat(el.split("=")[1]) 153 | } else if (!el.indexOf("url")) { 154 | url = el.split("=")[1] 155 | } 156 | }) 157 | port = port || 3000 158 | url = url || "http://localhost:" 159 | app.listen(port) 160 | console.log("已经启动" + url + port) 161 | }() 162 | /* 163 | 我们自己写了一个WEB管理控制台,deamon用的是cluster模式实现的,还写了一点IPC通信, 164 | 这样登录后台可以看到各子进程的PID、工作状态、内存占用、数据库连接占用、连接排队、 165 | HTTP会话数、TCP会话数、Cache使用率、Cache命中率等,管理控制台还记录了 166 | 每个业务模块的调用次数及调用耗时用于性能调优,deamon进程使用node-schedule 167 | 每10分钟记录一次子进程服务状态同时生成监控页面再通过shell汇总和压缩。 168 | https://cnodejs.org/topic/51cc49e973c638f37042f7b4 169 | */ -------------------------------------------------------------------------------- /bin/agate.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var program = require('commander'); 3 | var path = require('path'); 4 | var fs = require('fs'); 5 | var mkdirp = require('mkdirp') 6 | var color = require('cli-color') 7 | var spawn = require('child_process').spawn 8 | //var rootPath = __dirname.split(path.sep).slice(0, -1).join(path.sep) 9 | var rootPath = path.resolve(__dirname + '/..') 10 | program 11 | .version(JSON.parse(fs.readFileSync(path.join(__dirname, '../package.json'), 'utf8')).version) 12 | console.log(path.join(__dirname, '../package.json') + "!") 13 | //process.chdir(rootPath) 14 | //表示这是一个必填参数 15 | //[xxx]表示这是一个可选参数 16 | //[xxx...]表示这是一个可选数组参数 17 | program 18 | .command('scaffold [actions...]') 19 | .description('创建一个路由规则, 控制器(controller.js), action方法及页面') 20 | .action(function (rule, controller, actions) { 21 | //agate scaffold /test2 test2 get#index post#create 22 | //相当于agate scaffold /test2 test2 index post#create 23 | var jsonPath = path.join(rootPath, 'config', 'routes.json') 24 | var json = require(jsonPath) 25 | var hasNewKey = false 26 | var newActions = [] 27 | for (var i = 0, action; action = actions[i++]; ) { 28 | var arr = action.split("#") 29 | if (arr.length === 1) { 30 | arr = ["get", action] 31 | } else { 32 | arr = [arr[0].toLowerCase(), arr[1]] 33 | } 34 | var key = arr[0] + " " + rule 35 | var val = controller + " " + arr[1] 36 | if (json.hasOwnProperty(key)) { 37 | console.error(key + " 已经定义") 38 | } else { 39 | json[key] = val 40 | newActions.push(arr[1]) 41 | hasNewKey = true 42 | } 43 | } 44 | if (hasNewKey) { 45 | var ret = {} 46 | //对路由规则进行排序,方便查工 47 | Object.keys(json).sort().forEach(function (el) { 48 | ret[el] = json[el] 49 | }) 50 | //重写routes.json的内容 51 | fs.writeFile(jsonPath, JSON.stringify(ret, null, '\t'), function (err) { 52 | if (err) 53 | throw err 54 | console.log('添加新的路由规则成功') 55 | console.log(JSON.stringify(ret, null, '\t')) 56 | }) 57 | //准备要添加action函数 58 | var scontroller = newActions.map(function (action) { 59 | return '\r\nexports.' + action + ' = function *(next) {\r\n' + 60 | '\tyield this.render("' + controller + '/' + action + '")\r\n' + 61 | '}\r\n' 62 | }).join("") 63 | var controllerPath = path.join(rootPath, "app", "pages", controller, "controller.js") 64 | //确保此目录存在 65 | mkdirp(path.dirname(controllerPath), function (err) { 66 | //在controller.js中添加新action函数 67 | fs.writeFile(controllerPath, 68 | scontroller, { 69 | encoding: "utf8", 70 | flag: "a+" 71 | }, 72 | function (err) { 73 | if (err) 74 | throw err 75 | console.log('添加新action成功') 76 | }) 77 | //添加action对应的空页面 78 | newActions.forEach(function (action) { 79 | fs.writeFile(path.join(rootPath, "app", "pages", controller, action + ".html"), 80 | action, { 81 | encoding: "utf8", 82 | flag: "a+" 83 | }, 84 | function () { 85 | }) 86 | }) 87 | 88 | }) 89 | 90 | 91 | } 92 | 93 | }).on('--help', function () { 94 | console.log('参数:'); 95 | console.log(); 96 | console.log('%s\t 后跟路由规则,如%s,它会添加在config/routes.json下', 97 | color.bold('rule'), color.cyan('page\\:pageId')); 98 | console.log('%s后跟控制器的名字,不能有非法字符, 如topic', color.bold('controller')) 99 | console.log('它会在app/pages目录下建topic目录,再建一个controller.js'); 100 | console.log('%s\t 后面重复跟N个%s ,如%s', color.bold('actions'), color.green('请求名#action名'), 101 | color.cyan('get#index get#about post#create')); 102 | console.log('此外get请求名默认可省略,相当于%s 有多少action就会建多少个相名空页面', 103 | color.cyan('index about post#create')); 104 | console.log('一个完整的命令如下'); 105 | console.log(color.cyan('agate scaffold page\\:pageId topic index about post#create')); 106 | console.log(); 107 | }); 108 | 109 | 110 | 111 | program 112 | .command('start [port] [url] [env]') 113 | .description('启动服务器, 并通过默认浏览器打开该面') 114 | .action(function (port, url, env) { 115 | port = isFinite(port) ? parseFloat(port) : 3000 116 | url = /http|localhost/.test(url) ? url : "http://localhost:" 117 | var map = { 118 | "prod": "production", 119 | "production": "production", 120 | "dev": "development", 121 | "development": "development", 122 | "test": "test" 123 | } 124 | env = map[env] || "development" 125 | //process.execPath 相当于 "C:\\Program Files\\nodejs\\node.exe" 126 | //http://www.cnblogs.com/xiziyin/p/3578905.html 127 | switch (env) { 128 | case "development": //热启动 129 | spawn(process.execPath, 130 | [path.join(rootPath, "node_modules/nodemon/bin/nodemon"), 131 | "--harmony", path.join(rootPath, 'worker.js'), 132 | "localhost", port, "port=" + port, "url=" + url], { 133 | stdio: 'inherit', 134 | cwd: rootPath 135 | }) 136 | break 137 | case "test": 138 | spawn(process.execPath, 139 | ["--harmony", path.join(rootPath, 'master.js'), "port=" + port, "url=" + url], { 140 | stdio: 'inherit', 141 | cwd: rootPath 142 | }) 143 | break 144 | case "production": //多线程 145 | spawn(process.execPath, 146 | ["--harmony", path.join(rootPath, 'master.js'), "port=" + port, "url=" + url], { 147 | stdio: 'inherit', 148 | cwd: rootPath 149 | }) 150 | break 151 | } 152 | setTimeout(function(){ 153 | var open = require("open") 154 | open(url + port) 155 | }, 0) 156 | 157 | }).on('--help', function () { 158 | console.log('参数:') 159 | console.log() 160 | console.log('%s\t 端口号, 默认是%f', color.bold('port'), color.cyan('4000')); 161 | console.log('%s\t URL地址, 必须是http/https开头或者是localhost, 默认是http://localhost', color.bold('url')) 162 | console.log('%s\t 工作环境, 可以是test, dev, prod, development, production, 默认是development', color.bold('env')) 163 | console.log('一个完整的命令如下'); 164 | console.log(color.cyan('agate start 8888 "" prod')) 165 | console.log() 166 | console.log(" 在开发环境中,是通过nodemon进行文件监控,如果文件发动改动,会自动重启服务器") 167 | console.log(" 在测试与生产环境中,是cluster模块启动多个线程(线程数视CPU核心决定),如果服务器挂掉,会自动重启服务器"); 168 | console.log("但在10秒内连续重启60次,就认为是严重故障,不再重启了"); 169 | }) 170 | 171 | 172 | program 173 | .command('stop') 174 | .description('关掉') 175 | .action(function (port) { 176 | var port = 7543; 177 | var options = { 178 | host: 'localhost', 179 | port: port, 180 | path: '/shutdown', 181 | method: 'GET' 182 | }; 183 | var http = require('http') 184 | http.request(options, function (res) { 185 | console.log('STATUS: ' + res.statusCode); 186 | var buf = []; 187 | res.on('data', buf.push.bind(buf)).on('end', function () { 188 | console.log(Buffer.concat(buf).toString()); 189 | }); 190 | }).end(); 191 | 192 | 193 | }) 194 | 195 | program.parse(process.argv) 196 | 197 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | 341 | -------------------------------------------------------------------------------- /app/pages/doc/es6-generators.html: -------------------------------------------------------------------------------- 1 |

原文链接:http://davidwalsh.name/es6-generators

2 | 3 |

generator 即生成器,是 ES6 中众多特性中的一种,是一个新的函数类型。

4 | 5 |

这篇文章旨在介绍 generator 的基础知识,以及告诉你在 JS 的未来,他们为何如此重要。

6 | 7 |

运行直到完成 (Run-To-Completion)

8 | 9 |

为了理清这个新的函数类型和其他函数类型有何区别,我们首先需要了解 『run to completion』 的概念。

10 | 11 |

我们知道 JS 是单线程的,所以一旦一个函数开始执行,排在队列后边的函数就必须等待这个函数执行完毕。

12 | 13 |

举个栗子:

14 | 15 |
setTimeout(function(){
 16 |     console.log("Hello World");
 17 | },1);
 18 | 
 19 | function foo() {
 20 |     // 注意: 永远不要使用这种超长的循环,这里只是为了演示方便
 21 |     for (var i=0; i<=1E10; i++) {
 22 |         console.log(i);
 23 |     }
 24 | }
 25 | 
 26 | foo();
 27 | // 0..1E10
 28 | // "Hello World"
 29 | 
30 | 31 |

在这段代码中,我们先执行了 foo() 然后执行 setTimeout,而 foo() 中的 for 循环将花费超长的时间才能完成。

32 | 33 |

只有等待这个漫长的循环结束后,setTimeout 中的 console.log('Hello World') 才能执行。

34 | 35 |

如果 foo() 函数能够被中断会怎样呢?

36 | 37 |

这是多线程编程语言的挑战,但我们并不需要考虑这个,因为 JS 是单线程的。

38 | 39 |

运行可被中止 (Run..Stop..Run)

40 | 41 |

使用 ES6 的生成器特性,我们有了一种新的函数类型:

42 | 43 |

允许这个函数的执行被中断一次或多次,在中断的期间我们可以去做其他操作,完成后再回来恢复这个函数的执行。

44 | 45 |

如果你了解过其他并发型或多线程的语言的话,你可能知道『协作(cooperative)』:

46 | 47 |

在一个函数执行期间,允许执行中断,在中断期间与其他代码进行协作。

48 | 49 |

ES6 生成器函数在并发行为中体现了这种『协作』的特性。

50 | 51 |

在生成器函数体中,我们可以使用一个新的 yield 关键字在内部来中断函数的执行。

52 | 53 |

需要注意的是,生成器并不能恢复自己中断的执行,我们需要一个额外的控制来恢复函数的执行。

54 | 55 |

所以,一个生成器函数能够被中断和重启。那生成器函数中断自己的执行后,怎么才知道何时恢复执行呢?

56 | 57 |

我们可以使用 yield 来对外发送中断的信号,当外部返回信号时再恢复函数的执行。

58 | 59 |

生成器的语法

60 | 61 |

我们可以这样声明一个生成器函数:

62 | 63 |
function *foo() {
 64 |     // ...
 65 | }
66 | 67 |

注意这里的星号(*)即声明了这个函数是属于生成器类型的函数。

68 | 69 |

生成器函数大多数功能与普通函数没有区别,只有一部分新颖的语法需要学习。

70 | 71 |

先介绍一个 yield 关键字:

72 | 73 |

yield ___ 也叫做 『yield 表达式』,当我们重启生成器时,会向函数内部传值,这个值为对应的 yield ___ 表达式的计算结果。

74 | 75 |

举个栗子:

76 | 77 |
function *foo() {
 78 |     var x = 1 + (yield "foo");
 79 |     console.log(x);
 80 | }
81 | 82 |

在这段代码中, yield "foo" 表达式将在函数中断时,向外部发送 “foo” 这个值,且当这个生成器重启时,外部传入的值将作为这个表达式的结果:

83 | 84 |

在这里,外部传入的值将会与 1 进行相加操作,然后赋值给 x

85 | 86 |

看到双向通信的特点了么?我们在生成器内部向外发送 “foo” 然后中断函数执行,然后当生成器接收到外部传入一个值时,生成器将重启,函数将恢复执行。

87 | 88 |

如果我们只是向中止函数而不对外传值时,只使用 yield 即可:

89 | 90 |
// 注意: `foo(..)` 在这里并不是一个生成器
 91 | function foo(x) {
 92 |     console.log("x: " + x);
 93 | }
 94 | 
 95 | function *bar() {
 96 |     yield; // 只是中断,而不向外传值
 97 |     foo( yield ); // 当外部传回一个值时,将执行 foo() 操作
 98 | }
99 | 100 |

生成器迭代器(Generator Iterator)

101 | 102 |

迭代器是一种设计模式,定义了一种特殊的行为:

103 | 104 |

我们通过 next() 来获取一组有序的值。

105 | 106 |

举个栗子:我们有个数组为 [1, 2, 3, 4, 5],第一次调用 next() 将返回 1,第二次调用 next() 将返回 2,以此类推,当数组内的值都返回完毕时,继续调用 next()将返回 null 或 false。

107 | 108 |

为了从外部控制生成器函数,我们使用生成器迭代器(generator iterator)来实现,举个栗子:

109 | 110 |
function *foo() {
111 |     yield 1;
112 |     yield 2;
113 |     yield 3;
114 |     yield 4;
115 |     yield 5;
116 | }
117 | 118 |

我们先定义了一个生成器函数 foo(),接着我们调用它一次来生成一个迭代器:

119 | 120 |
var it = foo();
121 | 122 |

你可能会疑问为啥我们不是使用 new 关键字即 var it = new foo() 来生成迭代器?好吧,这语法背后比较复杂已经超出了我们的讨论范围了。

123 | 124 |

接下来我们就可以使用这个迭代器了:

125 | 126 |
console.log( it.next() ); // { value: 1, done: false }
127 | 128 |

这里的 it.next() 返回 { value: 1, done: false },其中的 value: 1yield 1 返回的值,而 done: false 表示生成器函数还没有迭代完成。

129 | 130 |

继续调用 it.next() 进行迭代:

131 | 132 |
console.log( it.next() ); // { value:2, done:false }
133 | console.log( it.next() ); // { value:3, done:false }
134 | console.log( it.next() ); // { value:4, done:false }
135 | console.log( it.next() ); // { value:5, done:false }
136 | 137 |

注意我们迭代到值为 5时,done 还是为 false,是因为这时候生成器函数并未处于完成状态,我们再调用一次看看:

138 | 139 |
console.log( it.next() ); // { value:undefined, done:true }
140 | 141 |

这时候我们已经执行完了所有的 yield ___ 表达式,所以 done 已经为 true

142 | 143 |

你可能会好奇的是:如果我们在一个生成器函数中使用了 return,我们在外部还能获取到 yield 的值么?

144 | 145 |

答案可以是:能

146 | 147 |
function *foo() {
148 |     yield 1;
149 |     return 2;
150 | }
151 | 
152 | var it = foo();
153 | 
154 | console.log( it.next() ); // { value:1, done:false }
155 | console.log( it.next() ); // { value:2, done:true }
156 | 157 |

让我们看看当我们使用迭代器时,生成器怎么对外传值,以及怎么接收外部传入的值:

158 | 159 |
function *foo(x) {
160 |     var y = 2 * (yield (x + 1));
161 |     var z = yield (y / 3);
162 |     return (x + y + z);
163 | }
164 | 
165 | var it = foo( 5 );
166 | 
167 | // 注意:这里没有给 `it.next()` 传值
168 | console.log( it.next() );       // { value:6, done:false }
169 | console.log( it.next( 12 ) );   // { value:8, done:false }
170 | console.log( it.next( 13 ) );   // { value:42, done:true }
171 | 172 |

我们传入参数 5 先初始化了一个迭代器。

173 | 174 |

第一个 next() 中没有传递参数进去,因为这个生成器函数中没有对应的 yield 来接收参数,所以如果我们在第一个 next() 强制传参进去的话,什么都不会发生。 175 | 第一个 yield (x+1) 将返回 value: 6 到外部,此时生成器未迭代完毕,所以同时返回 done: false

176 | 177 |

第二个 next(12) 中我们传递了参数 12 进去,则表达式 yield(x+1) 会被赋值为 12,相当于:

178 | 179 |
var x = 5;
180 | var y = 2 * 12; // => 24
181 | 182 |

第二个 yield (y/3) 将返回 value: 8 到外部,此时生成器未迭代完毕,所以同时返回 done: false

183 | 184 |

同理,在第三个 next(13) 中我们传递了参数 13 进去,则表达式 yield(y/3) 会被赋值为 13,相当于:

185 | 186 |
var x = 5
187 | var y = 24;
188 | var z = 13;
189 | 190 |

第三个 yield并不存在,所以会 return (x + y + z) 即返回 value: 42 到外部,此时生成器已迭代完毕,所以同时返回 done: true

191 | 192 |

答案也可以是:不能!

193 | 194 |

依赖 return 从生成器中返回一个值并不好,因为当生成器遇见了 for..of 循环的时候,被返回的值将会被丢弃,举个栗子:

195 | 196 |
function *foo() {
197 |     yield 1;
198 |     yield 2;
199 |     yield 3;
200 |     yield 4;
201 |     yield 5;
202 |     return 6;
203 | }
204 | 
205 | for (var v of foo()) {
206 |     console.log( v );
207 | }
208 | // 1 2 3 4 5
209 | 
210 | console.log( v ); // 仍然是 `5`, 而不是 `6` 
211 | 212 |

看到了吧?由 foo() 创建的迭代器会被 foo..of 循环自动捕获,且会自动进行一个接一个的迭代,直到遇到 done: true,就结束了,并没有处理 return 的值。

213 | 214 |

所以,for..of 循环会忽略被返回的 6,同时因为没有暴露出 next() 方法,for..of 循环就不能用于我们在中断生成器的期间,对生成器进行传值的场景。

215 | 216 |

总结

217 | 218 |

看了以上 ES6 Generators 的基础知识,很自然地就会想我们在什么场景下会用到这个新颖的生成器呢?

219 | 220 |

当然有很多的场景能发挥生成器的这些特性了,这篇文章只是抛砖引玉,我们将继续深入挖掘生成器的魔力!

221 | 222 |

当你在最新的 Chrome nightly 或 canary 版,或 Firefox nightly版,甚至在 v0.11+ 版本的 node (带 –harmony 开启 ES6 功能)中运行了以上这些代码片段

223 | 224 |

错误处理

225 | 226 |

ES6 中生成器的其中一个强大的特点就是:函数内部的代码编写风格是同步的,即使外部的迭代控制过程可能是异步的。

227 | 228 |

也就是说,我们可以简单地对错误进行处理,类似我们熟悉的 try..catch 语法,举个栗子:

229 | 230 |
function *foo() {
231 |     try {
232 |         var x = yield 3;
233 |         console.log( "x: " + x ); // 如果出错,这里可能永远不会执行
234 |     }
235 |     catch (err) {
236 |         console.log( "Error: " + err );
237 |     }
238 | }
239 | 240 |

即使这个生成器可能会在 yield 3 处中断,当接收到外部传入的错误时,try..catch 将会捕获到。

241 | 242 |

具体一个错误是怎样传入生成器的呢,举个栗子:

243 | 244 |
var it = foo();
245 | 
246 | var res = it.next(); // { value:3, done:false }
247 | 
248 | // 我们在这里不调用 it.next() 传值进去,而是触发一个错误
249 | it.throw( "Oops!" ); // Error: Oops!
250 | 251 |

我们可以使用 throw() 方法产生错误传进生成器中,那么在生成器中断的地方,即 yield 3 处会产生错误,然后被 try..catch 捕获。

252 | 253 |

注意:如果我们使用 throw() 方法产生一个错误传进生成器中,但没有对应的 try..catch 对错误进行捕获的话,这个错误将会被传出去,外部如果不对错误进行捕获的话,则会抛出异常:

254 | 255 |
256 | function *foo() { }
257 | 
258 | var it = foo();
259 | // 在外部进行捕获
260 | try {
261 |     it.throw( "Oops!" );
262 | }
263 | catch (err) {
264 |     console.log( "Error: " + err ); // Error: Oops!
265 | }
266 | 267 |

当然,我们也可以进行反方向的错误捕获:

268 | 269 |
function *foo() {
270 |     var x = yield 3;
271 |     var y = x.toUpperCase(); // 若 x 不是字符串的话,将抛出TypeError 错误
272 |     yield y;
273 | }
274 | 
275 | var it = foo();
276 | 
277 | it.next(); // { value:3, done:false }
278 | 
279 | try {
280 |     it.next( 42 ); // `42` 是数字没有 `toUpperCase()` 方法,所以会出错
281 | }
282 | catch (err) {
283 |     console.log( err ); // 捕获到 TypeError 错误
284 | }
285 | 286 |

生成器委托

287 | 288 |

另一个我们想做的可能是在一个生成器中调用另一个生成器。

289 | 290 |

我并不是指在一个生成器中初始化另一个生成器,而是说我们可以将一个生成器的迭代器控制交给另一个生成器。

291 | 292 |

为了实现委托,我们需要用到 yield 关键字的另一种形式:yield *,举个栗子:

293 | 294 |
function *foo() {
295 |     yield 3;
296 |     yield 4;
297 | }
298 | 
299 | function *bar() {
300 |     yield 1;
301 |     yield 2;
302 |     yield *foo(); // `yield *` 将迭代器控制委托给了 `foo()`
303 |     yield 5;
304 | }
305 | 
306 | for (var v of bar()) {
307 |     console.log( v );
308 | }
309 | // 1 2 3 4 5
310 | 311 |

以上这段代码应该通俗易懂:当生成器 bar() 迭代到 yield 2 时,先将控制权交给了另一个生成器 foo()迭代完后再将控制权收回,继续进行迭代。

312 | 313 |

这里使用了 for..of 循环进行示例,正如在基础篇我们知道 for..of 循环中没有暴露出 next() 方法来传递值到生成器中,所以我们可以用手动的方式:

314 | 315 |
316 | function *foo() {
317 |     var z = yield 3;
318 |     var w = yield 4;
319 |     console.log( "z: " + z + ", w: " + w );
320 | }
321 | 
322 | function *bar() {
323 |     var x = yield 1;
324 |     var y = yield 2;
325 |     yield *foo(); // `yield *` 将迭代器控制委托给了 `foo()`
326 |     var v = yield 5;
327 |     console.log( "x: " + x + ", y: " + y + ", v: " + v );
328 | }
329 | 
330 | var it = bar();
331 | 
332 | it.next();      // { value:1, done:false }
333 | it.next( "X" ); // { value:2, done:false }
334 | it.next( "Y" ); // { value:3, done:false }
335 | it.next( "Z" ); // { value:4, done:false }
336 | it.next( "W" ); // { value:5, done:false }
337 | // z: Z, w: W
338 | 
339 | it.next( "V" ); // { value:undefined, done:true }
340 | // x: X, y: Y, v: V
341 | 342 |

尽管我们在这里只展示了一层的委托关系,但具体场景中我们当然可以使用多层的嵌套。

343 | 344 |

一个 yield * 技巧是,我们可以从被委托的生成器(比如示例中的 foo()) 获取到返回值,举个栗子:

345 | 346 |
function *foo() {
347 |     yield 2;
348 |     yield 3;
349 |     return "foo"; // 返回一个值给 `yield*` 表达式
350 | }
351 | 
352 | function *bar() {
353 |     yield 1;
354 |     var v = yield *foo();
355 |     console.log( "v: " + v );
356 |     yield 4;
357 | }
358 | 
359 | var it = bar();
360 | 
361 | it.next(); // { value:1, done:false }
362 | it.next(); // { value:2, done:false }
363 | it.next(); // { value:3, done:false }
364 | it.next(); // "v: foo"   { value:4, done:false } 注意:在这里获取到了返回的值
365 | it.next(); // { value:undefined, done:true }
366 | 367 |

yield *foo() 得到了 bar() 的控制权,完成了自己的迭代操作后,返回了一个 v: foo 值 给bar() ,然后 bar() 再继续迭代下去。

368 | 369 |

yieldyield * 表达式的一个有趣的区别是:在 yield 中,返回值在 next() 中传入的,而在 yield * 中,返回值是在 return 中传入的。

370 | 371 |

此外,我们也可以在委托的生成器中进行双向的错误绑定,举个栗子:

372 | 373 |
function *foo() {
374 |     try {
375 |         yield 2;
376 |     }
377 |     catch (err) {
378 |         console.log( "foo caught: " + err );
379 |     }
380 | 
381 |     yield; // 中断
382 | 
383 |     // 现在抛出另一个错误
384 |     throw "Oops!";
385 | }
386 | 
387 | function *bar() {
388 |     yield 1;
389 |     try {
390 |         yield *foo();
391 |     }
392 |     catch (err) {
393 |         console.log( "bar caught: " + err );
394 |     }
395 | }
396 | 
397 | var it = bar();
398 | 
399 | it.next(); // { value:1, done:false }
400 | it.next(); // { value:2, done:false }
401 | 
402 | it.throw( "Uh oh!" ); // 将会在 `foo()` 内部捕获
403 | // foo caught: Uh oh!
404 | 
405 | it.next(); // { value:undefined, done:true }  --> 这里没有错误
406 | // bar caught: Oops!
407 | 408 |

throw( "Uh oh!" ) 在代理给 foo() 的过程中,抛了个错误进去,所以错误在 foo() 中被捕获。

409 | 410 |

同理,throw "Oops!"foo() 内部抛出的错误,将会传回给 bar() 后,被 bar() 中的 try..catch 捕获到。

411 | 412 |

总结

413 | 414 |

生成器有着同步方式的编写语法,意味着我么可以使用 try..catchyield 表达式中进行错误处理。

415 | 416 |

生成器迭代器中也有一个 throw() 方法用于在中断期间向生成器内部传入一个错误,这个错误能被生成器内部的 try..catch 捕获。

417 | 418 |

yield * 允许我们将迭代器的控制权从当前的生成器中委托给另一个生成器。好处是 yield * 扮演了在生成器间传递消息和错误的角色。

419 | 420 |

了解了这么多,还有一个很重要的问题没有解决:

421 | 422 |

怎么异步地使用生成器呢?

423 | 424 |

关键是要实现这么一个机制:在异步环境中,当迭代器的 next() 方法被调用,我们需要定位到生成器中断的地方重新启动。

425 | 426 |

生成器提供了同步方式编写的代码风格,这就允许我们隐藏异步的实现细节。

427 | 428 |

我们就可以用一种非常自然的方式来表达程序的执行流程,避免了同时处理异步代码的语法和陷阱。

429 | 430 |

换句话说,我们利用生成器从内到外、从外到内双向传值的特点,将不同的值的处理交给了不同的生成器逻辑,只需要关心获取到特定的值进行某种操作,而无需关心特定的值如何产生(通过netx() 将值的产生逻辑委托出去)。

431 | 432 |

这么一来,异步处理的优点以及易读的代码结合到一起,就加强了我们程序的可维护性。

433 | 434 |

最简单的异步

435 | 436 |

举个栗子,假定我们已经有了以下代码:

437 | 438 |
function makeAjaxCall(url,cb) {
439 |     // 执行一个 ajax 请求
440 |     // 请求完成后执行 `cb(result)` 
441 | }
442 | 
443 | makeAjaxCall( "http://some.url.1", function(result1){
444 |     var data = JSON.parse( result1 );
445 | 
446 |     makeAjaxCall( "http://some.url.2/?id=" + data.id, function(result2){
447 |         var resp = JSON.parse( result2 );
448 |         console.log( "我们请求到的数据是: " + resp.value );
449 |     });
450 | } );
451 | 452 |

使用简单的生成器来表达的话,就像这样:

453 | 454 |
function request(url) {
455 |    // 调用这个普通函数来隐藏异步处理的细节
456 |    // 使用 `it.next()` 来恢复调用这个普通函数的生成器函数的迭代器
457 |     makeAjaxCall( url, function(response){
458 |         // 异步获取到数据后,给生成器发送 `response` 信号
459 |         it.next( response );
460 |     } );
461 |     // 注意: 这里没有返回值
462 | }
463 | 
464 | function *main() {
465 |     var result1 = yield request( "http://some.url.1" );
466 |     var data = JSON.parse( result1 );
467 | 
468 |     var result2 = yield request( "http://some.url.2?id=" + data.id );
469 |     var resp = JSON.parse( result2 );
470 |     console.log( "The value you asked for: " + resp.value );
471 | }
472 | 
473 | var it = main();
474 | it.next(); // 开始迭代
475 | 476 |

request() 这个工具函数只是将我们的异步请求数据的代码进行了封装,需要注意的是在回调函数中调用了生成器的 next() 方法。

477 | 478 |

当我们使用 var it = main(); 创建了一个迭代器后,紧接着使用 it.next(); 开始迭代,这时候遇到第一个 yield 中断了生成器,转而执行 request( "http://some.url.1" )

479 | 480 |

request( "http://some.url.1" ) 异步获取到数据后,在回调函数中调用 it.next(response)response 传回给生成器刚刚中断的地方,生成器将继续迭代。

481 | 482 |

这里的亮点就是,我们在生成器中无需关心异步请求的数据如何获取,我们只知道调用了 request() 后,当需要的数据获取到了,就会通知生成器继续迭代。

483 | 484 |

这么一来在生成器中我们使用同步方式的编写风格,其实我们获取到了异步数据!

485 | 486 |

同理,当我们继续调用 it.next() 时,会遇到第二个 yield 中断迭代,发出第二个请求 yield request( "http://some.url.2?id=" + data.id ) 异步获取到数据后再恢复迭代,我们依旧不用关心异步获取数据的细节了,多爽!

487 | 488 |

以上这段代码中,request() 请求的是异步 AJAX 请求,但如果我们后续改变程序给 AJAX 设置了缓存了,获取数据会先从缓存中获取,这时候没有执行真正的 AJAX 请求就不能在回调函数中调用 it.next(response) 来恢复生成器的中断了啊!

489 | 490 |

没关系,我们可以使用一个小技巧来解决这个问题,举个栗子:

491 | 492 |
// 给 AJAX 设置缓存
493 | var cache = {};
494 | 
495 | function request(url) {
496 |     // 请求已被缓存
497 |     if (cache[url]) {
498 |         // 使用 setTimeout 来模拟异步操作
499 |         setTimeout( function(){
500 |             it.next( cache[url] );
501 |         }, 0 );
502 |     }
503 |     // 请求未被缓存,发出真正的请求
504 |     else {
505 |         makeAjaxCall( url, function(resp){
506 |             cache[url] = resp;
507 |             it.next( resp );
508 |         } );
509 |     }
510 | }
511 | 512 |

看,当我们给我们的程序添加了 AJAX 缓存机制甚至其他异步操作的优化时,我们只改变了 request() 这个工具函数的逻辑,而无需改动调用这个工具函数获取数据的生成器:

513 | 514 |
var result1 = yield request( "http://some.url.1" );
515 | var data = JSON.parse( result1 );
516 | ..
517 | 518 |

在生成器中,我们还是像以前一样调用 request() 就能获取到需要的异步数据,无需关心获取数据的细节实现!

519 | 520 |

这就是将异步操作当做一个细节实现抽象出来后展现出的魔力了!

521 | 522 |

更好的异步

523 | 524 |

上面介绍的异步方案对于简单的异步生成器来说工作良好,但用途有限,我们需要一个更强大的异步方案:使用 Promises.

525 | 526 |

如果你对 ES6 Promises 有迷惑的话,我建议你先读 我写的介绍 Promises 的文章

527 | 528 |

我们的代码目前有个严重的问题:回调多了会产生多重嵌套(即回调地狱)。

529 | 530 |

此外,我们目前还缺乏的东西有:

531 | 532 |
    533 |
  1. 清晰的错误处理逻辑。我们使用 AJAX 的回调可能会检测到一个错误,然后使用 it.throw() 将错误传回给生成器,在生成器中则使用 try..catch 来捕获错误。 534 | 一来我们需要猜测我们可能发生错误且手动添加对应的错误处理函数,二来我们的错误处理代码没法重复使用。

  2. 535 |
  3. 如果 makeAjaxCall() 函数不受我们控制,调用了多次回调的话,也会多次触发回调中的 it.next() ,生成器就会变得非常混乱。

    536 | 537 |

    处理和阻止这种问题需要大量的手动工作,也非常不方便。

  4. 538 |
  5. 有时候我们需要 『并行地』执行不只一个任务(比如同时触发两个 AJAX 请求)。而生成器中的 yield 并不支持两个或多个同时进行。

  6. 539 |
540 | 541 |

以上这些问题都可以用手动编写代码的方式来解决,但谁会想每次都重新编写类似的重复的代码呢?

542 | 543 |

我们需要一个更好的可信任、可重复使用的方案来支持我们基于生成器编写异步的代码。

544 | 545 |

怎么实现?使用 Promises !

546 | 547 |

我们将原来的代码加入 Promises 的特性:

548 | 549 |
function request(url) {
550 |     // 注意: 这里返回的是一个 promise
551 |     return new Promise( function(resolve,reject){
552 |         makeAjaxCall( url, resolve );
553 |     } );
554 | }
555 | 556 |

request() 函数中创建了一个 promise 实例,一旦 AJAX 请求完成,这个实例将会被 resolved

557 | 558 |

我们接着将这个实例返回,这样它就能够被 yield 了。

559 | 560 |

接下来我们需要一个工具来控制我们生成器的迭代器,接收返回的 promise 实例,然后再通过 next() 来恢复生成器的中断:

561 | 562 |
// 执行异步的生成器
563 | // 注意: 这是简化的版本,没有处理错误
564 | function runGenerator(g) {
565 |     // 注意:我们使用 `g()` 自动初始化了迭代器
566 |     var it = g(), ret;
567 | 
568 |     // 异步地迭代
569 |     (function iterate(val){
570 |         ret = it.next( val );
571 | 
572 |         // 迭代未完成
573 |         if (!ret.done) {
574 |             // 判断是否为 promise 对象,如果没有 `then()` 方法则不是
575 |             if ("then" in ret.value) {
576 |                 // 等待 promise 返回
577 |                 ret.value.then( iterate );
578 |             }
579 |             // 如果不是 promise 实例,则说明直接返回了一个值
580 |             else {
581 |                 // 使用 `setTimeout` 模拟异步操作
582 |                 setTimeout( function(){
583 |                     iterate( ret.value );
584 |                 }, 0 );
585 |             }
586 |         }
587 |     })();
588 | }
589 | 590 |

注意:我们在 runGenerator() 中先生成了一个迭代器 var it = g(),然后我们会执行这个迭代器直到它完成(done: true)。

591 | 592 |

接着我们就可以使用这个 runGenerator() 了:

593 | 594 |
runGenerator( function *main(){
595 |     var result1 = yield request( "http://some.url.1" );
596 |     var data = JSON.parse( result1 );
597 | 
598 |     var result2 = yield request( "http://some.url.2?id=" + data.id );
599 |     var resp = JSON.parse( result2 );
600 |     console.log( "你请求的数据是: " + resp.value );
601 | } );
602 | 603 |

我们通过生成不同的 promise 实例,分别对这些实例进行 yield,不同的实例等待自己的 promise 被 resolve 后再执行对应的操作。

604 | 605 |

这么一来,我们只需要同时生成不同的 promise 实例,就可以『并行地』执行不只一个任务(比如同时触发两个 AJAX 请求)了。

606 | 607 |

既然我们使用了 promises 来管理生成器中处理异步的代码,我们就解决了只有在回调中才能实现的功能,这就避免了回调嵌套了。

608 | 609 |

使用 Generotos + Promises 的优点是:

610 | 611 |
    612 |
  1. 我们可以使用内建的错误处理机制。虽然这没有在上面的代码片段中展示出来,但其实很简单:

    613 | 614 |

    监听 promise 中的错误,使用 it.throw() 把错误抛出,然后在生成器中使用 try..catch 进行捕获和处理即可。

  2. 615 |
  3. 我们可以使用到 Promises 提供的 control/trustability 特性。

  4. 616 |
  5. Promises 提供了大量处理多并行且复杂的任务的特性。

    617 | 618 |

    举个栗子:yield Promise.all([ .. ]) 方法接收一组 promise 组成的数组作为参数,然后 yield 一个 promise 提供给生成器处理,这个 promise 会等待数组里所有 promise 完成。当我们得到 yield 后的 promise 时,说明传进去的数组中的所有 promise 都已经完成,且是按照他们被传入的顺序完成的。

  6. 619 |
620 | 621 |

首先,我们体验一下错误处理:

622 | 623 |
// 假设1: `makeAjaxCall(..)` 第一个参数判断是否有错误产生
624 | // 假设2: `runGenerator(..)` 能捕获并处理错误
625 | 
626 | function request(url) {
627 |     return new Promise( function(resolve,reject){
628 |         makeAjaxCall( url, function(err,text){
629 |             // 如果出错,则 reject 这个 promise
630 |             if (err) reject( err );
631 |             // 否则,resolve 这个 promise
632 |             else resolve( text );
633 |         } );
634 |     } );
635 | }
636 | 
637 | runGenerator( function *main(){
638 |     // 捕获第一个请求的错误
639 |     try {
640 |         var result1 = yield request( "http://some.url.1" );
641 |     }
642 |     catch (err) {
643 |         console.log( "Error: " + err );
644 |         return;
645 |     }
646 |     var data = JSON.parse( result1 );
647 | 
648 |     // 捕获第二个请求的错误
649 |     try {
650 |         var result2 = yield request( "http://some.url.2?id=" + data.id );
651 |     } catch (err) {
652 |         console.log( "Error: " + err );
653 |         return;
654 |     }
655 |     var resp = JSON.parse( result2 );
656 |     console.log( "你请求的数据是: " + resp.value );
657 | } );
658 | 659 |

如果一个 promise 被 reject 或遇到其他错误的话,将使用 it.throw() (代码片段中没有展示出来)抛出一个生成器的错误,这个错误能被 try..catch 捕获。

660 | 661 |

再举个使用 Promises 管理更复杂的异步操作的栗子:

662 | 663 |
function request(url) {
664 |     return new Promise( function(resolve,reject){
665 |         makeAjaxCall( url, resolve );
666 |     } )
667 |     // 对 promise 返回的字符串进行后处理操作
668 |     .then( function(text){
669 |         // 是否为一个重定向链接
670 |         if (/^https?:\/\/.+/.test( text )) {
671 |             // 是的话对向新链接发送请求
672 |             return request( text );
673 |         }
674 |         // 否则,返回字符串
675 |         else {
676 |             return text;
677 |         }
678 |     } );
679 | }
680 | 
681 | runGenerator( function *main(){
682 |     var search_terms = yield Promise.all( [
683 |         request( "http://some.url.1" ),
684 |         request( "http://some.url.2" ),
685 |         request( "http://some.url.3" )
686 |     ] );
687 | 
688 |     var search_results = yield request(
689 |         "http://some.url.4?search=" + search_terms.join( "+" )
690 |     );
691 |     var resp = JSON.parse( search_results );
692 | 
693 |     console.log( "Search results: " + resp.value );
694 | } );
695 | 
696 | 697 |

Promise.all([ .. ]) 构造了一个 promise ,等待数组中三个 promise 的完成,这个 promise 会被 yieldrunGenerator() 生成器,然后这个生成器就可以恢复迭代。

698 | 699 |

使用其他的 Promise 类库

700 | 701 |

在上面的代码片段中,我们自己编写了 runGenerator() 函数来提供 Generators + Promises 的功能,其实我们也可以使用社区里优秀的类库,举几个栗子: QCoasynquence

702 | 703 |

接下来我会简要地介绍下 asynquence 中的 runner插件 。如果你感兴趣的话,可以阅读我写的两篇深入理解 asynquence 的博文

704 | 705 |

首先,asynquence 提供了回调函数中错误为第一参数的编写风格(error-first style),举个栗子:

706 | 707 |
function request(url) {
708 |     return ASQ( function(done){
709 |         // 传进一个以错误为第一参数的回调函数
710 |         makeAjaxCall( url, done.errfcb );
711 |     } );
712 | }
713 | 714 |

接着,asynquence 的 runner 插件会接收一个生成器作为参数,这个生成器可以处理传入的数据处理后再传出来,而所有的的错误会自动地传递:

715 | 716 |
// 我们使用 `getSomeValues()` 来产生一组 promise,并链式地进行异步操作
717 | getSomeValues()
718 | 
719 | // 现在使用一个生成器来处理接收到的数据
720 | .runner( function*(token){
721 |     var value1 = token.messages[0];
722 |     var value2 = token.messages[1];
723 |     var value3 = token.messages[2];
724 | 
725 |     // 并行地执行三个 AJAX 请求
726 |     // 注意: `ASQ().all(..)` 就像之前提过的 `Promise.all(..)`
727 |     var msgs = yield ASQ().all(
728 |         request( "http://some.url.1?v=" + value1 ),
729 |         request( "http://some.url.2?v=" + value2 ),
730 |         request( "http://some.url.3?v=" + value3 )
731 |     );
732 | 
733 |     // 当三个请求都执行完毕后,进入下一步
734 |     yield (msgs[0] + msgs[1] + msgs[2]);
735 | } )
736 | 
737 | // 现在使用前面的生成器返回的值作为参数继续发送 AJAX 请求
738 | .seq( function(msg){
739 |     return request( "http://some.url.4?msg=" + msg );
740 | } )
741 | 
742 | // 完成了一系列请求后,我们就获取到了想要的数据
743 | .val( function(result){
744 |     console.log( result ); // 获取数据成功!
745 | } )
746 | 
747 | // 如果产生错误,则抛出
748 | .or( function(err) {
749 |     console.log( "Error: " + err );
750 | } );
751 | 752 |

ES7 async

753 | 754 |

在 ES7 草案中有一个提议,建议采用另一种新的 async 函数类型。

755 | 756 |

使用这种函数,我们可以向外部发出 promises,然后使用 async 函数自动地将这些 promises 连接起来,当 promises 完成的时候,就会恢复 async 函数自己的中断(不需要在繁杂的迭代器中手动恢复)。

757 | 758 |

这个提议如果被采纳的话,可能会像这样:

759 | 760 |
async function main() {
761 |     var result1 = await request( "http://some.url.1" );
762 |     var data = JSON.parse( result1 );
763 | 
764 |     var result2 = await request( "http://some.url.2?id=" + data.id );
765 |     var resp = JSON.parse( result2 );
766 |     console.log( "The value you asked for: " + resp.value );
767 | }
768 | 
769 | main();
770 | 771 |

我们使用 async 声明了这种异步函数类型,然后使用 main() 直接调用这个函数,而不用像使用 runGenerator()ASQ().runner() 一样进行包装。

772 | 773 |

此外,我们没有使用 yield 关键字,而是使用了新的 await 关键字来声明等待 await 后面的 promise 的完成。

774 | 775 |

总结

776 | 777 |

一言以蔽之:Generators + Promises 的组合,强大且优雅地用同步编码风格实现了复杂的异步控制操作。

778 | 779 |

使用一些简单的工具类库,比如上面提到的 QCoasynquence 等,我们可以更方便地实现这些操作。

780 | 781 |

可以预见在不久的将来,当 ES7+ 发布的时候,我们使用 async 函数甚至可以无需使用一些类库支撑就可以实现原生的异步生成器了!

782 | 783 |

(译注:本文是第三篇文章,其实还有最后一篇是讲述并发式生成器的实现思路,涉及到 CSP 的相关概念,原文中引用了比较多的东西,读起来比较晦涩难懂,怕翻译出来与原文作者想要表达的东西相差太远,就先放一边了,感兴趣的可以直接查看原文。 784 | 欢迎大牛接力)

785 | 786 | --------------------------------------------------------------------------------