├── 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 |
<%= 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 |欢迎来到<%= 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 | <%= name %>
10 |<%= message %>
11 |<%= stack %>
12 | -------------------------------------------------------------------------------- /app/layout/template-bigpipe.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |两个方案,static与static-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 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app/pages/doc/config.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 |3 |
4 |
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我们已经在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 | -------------------------------------------------------------------------------- /app/layout/template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |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 | 它在其他语言也叫做View Helpers。 由于是使用ejs, 因此下面的定义或用法,都是符合ejs filters的规格。
3 |在ejs里面默认提供了如下过滤器, 详见这里
4 |但光是这个是不够用的,因此config目录下有一个filters文件,专门用于定义全局都使用的视图过滤器。 29 | 现在只添加了一个我们去哪儿网用到的qzzUrl过滤器
-------------------------------------------------------------------------------- /app/pages/doc/ejs.html: -------------------------------------------------------------------------------- 1 |我们通常用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 |使用了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
版本号是fekit在发布系统中生成的,执行fekit min对静态资源进行打包压缩之后,生成versions.mapping文件,内容如下所示:
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 |扼要地说, 在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 |扼要地说, 在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 |该接口用于前端把收集好的数据记录到后端服务器上,并最后收集绘制出CACTI的监控图表。
6 | 7 |http://m.ued.qunar.com/monitor/log
10 | 11 || 参数 | 17 |类型 | 18 |必填 | 19 |备注 | 20 |
|---|---|---|---|
| code | 23 |String | 24 |是 | 25 |监控名,参考规范 | 26 |
| time | 29 |int | 30 |-- | 31 |时间(监控图的纵轴值),不传时记录只记录次数 | 32 |
| logbrowser |
35 | 1/0 | 36 |-- | 37 |是否区分浏览器做记录,默认0 | 38 |
| ignoreSpider |
41 | 1/0 | 42 |-- | 43 |是否忽略爬虫 的请求,1表示不记录爬虫的访问,默认0 | 44 |
| count | 47 |int | 48 |-- | 49 |默认是1,表示该次请求记录N次count | 50 |
接口返回一个1*1像素的gif,大约43字节。
58 | 59 || 代码 | 65 |含义 | 66 |
|---|---|
| IE6 | 69 |MSIE 6 | 70 |
| IE7 | 73 |MSIE 7 | 74 |
| IE8 | 77 |MSIE 8 | 78 |
| IE9 | 81 |MSIE 9 | 82 |
| IE10 | 85 |MSIE 10 | 86 |
| IE11 | 89 |MSIE 11 | 90 |
| Chrome | 93 |Chrome | 94 |
| Waterfox | 97 |Waterfox | 98 |
| Firefox | 101 |Firefox | 102 |
| Safari | 105 |Safari | 106 |
| Android | 109 |Android | 110 |
| iOS | 113 |iOS | 114 |
| Winphone | 117 |Windows Phone | 118 |
| UnknownTouch | 121 |其他移动设备 | 122 |
| Unknown | 125 |未知浏览器 | 126 |
http://m.ued.qunar.com/monitor/log?code=hotel_detail_bktool_render&time=1024
133 |
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 | 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 | //原文链接:http://davidwalsh.name/es6-generators
2 | 3 |generator 即生成器,是 ES6 中众多特性中的一种,是一个新的函数类型。
4 | 5 |这篇文章旨在介绍 generator 的基础知识,以及告诉你在 JS 的未来,他们为何如此重要。
6 | 7 |为了理清这个新的函数类型和其他函数类型有何区别,我们首先需要了解 『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 循环将花费超长的时间才能完成。
只有等待这个漫长的循环结束后,setTimeout 中的 console.log('Hello World') 才能执行。
如果 foo() 函数能够被中断会怎样呢?
这是多线程编程语言的挑战,但我们并不需要考虑这个,因为 JS 是单线程的。
38 | 39 |使用 ES6 的生成器特性,我们有了一种新的函数类型:
42 | 43 |允许这个函数的执行被中断一次或多次,在中断的期间我们可以去做其他操作,完成后再回来恢复这个函数的执行。
44 | 45 |如果你了解过其他并发型或多线程的语言的话,你可能知道『协作(cooperative)』:
46 | 47 |在一个函数执行期间,允许执行中断,在中断期间与其他代码进行协作。
48 | 49 |ES6 生成器函数在并发行为中体现了这种『协作』的特性。
50 | 51 |在生成器函数体中,我们可以使用一个新的 yield 关键字在内部来中断函数的执行。
需要注意的是,生成器并不能恢复自己中断的执行,我们需要一个额外的控制来恢复函数的执行。
54 | 55 |所以,一个生成器函数能够被中断和重启。那生成器函数中断自己的执行后,怎么才知道何时恢复执行呢?
56 | 57 |我们可以使用 yield 来对外发送中断的信号,当外部返回信号时再恢复函数的执行。
我们可以这样声明一个生成器函数:
62 | 63 |function *foo() {
64 | // ...
65 | }
66 |
67 | 注意这里的星号(*)即声明了这个函数是属于生成器类型的函数。
68 | 69 |生成器函数大多数功能与普通函数没有区别,只有一部分新颖的语法需要学习。
70 | 71 |先介绍一个 yield 关键字:
yield ___ 也叫做 『yield 表达式』,当我们重启生成器时,会向函数内部传值,这个值为对应的 yield ___ 表达式的计算结果。
举个栗子:
76 | 77 |function *foo() {
78 | var x = 1 + (yield "foo");
79 | console.log(x);
80 | }
81 |
82 | 在这段代码中, yield "foo" 表达式将在函数中断时,向外部发送 “foo” 这个值,且当这个生成器重启时,外部传入的值将作为这个表达式的结果:
在这里,外部传入的值将会与 1 进行相加操作,然后赋值给 x。
看到双向通信的特点了么?我们在生成器内部向外发送 “foo” 然后中断函数执行,然后当生成器接收到外部传入一个值时,生成器将重启,函数将恢复执行。
87 | 88 |如果我们只是向中止函数而不对外传值时,只使用 yield 即可:
// 注意: `foo(..)` 在这里并不是一个生成器
91 | function foo(x) {
92 | console.log("x: " + x);
93 | }
94 |
95 | function *bar() {
96 | yield; // 只是中断,而不向外传值
97 | foo( yield ); // 当外部传回一个值时,将执行 foo() 操作
98 | }
99 |
100 | 迭代器是一种设计模式,定义了一种特殊的行为:
103 | 104 |我们通过 next() 来获取一组有序的值。
举个栗子:我们有个数组为 [1, 2, 3, 4, 5],第一次调用 next() 将返回 1,第二次调用 next() 将返回 2,以此类推,当数组内的值都返回完毕时,继续调用 next()将返回 null 或 false。
为了从外部控制生成器函数,我们使用生成器迭代器(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(),接着我们调用它一次来生成一个迭代器:
var it = foo();121 | 122 |
你可能会疑问为啥我们不是使用 new 关键字即 var it = new foo() 来生成迭代器?好吧,这语法背后比较复杂已经超出了我们的讨论范围了。
接下来我们就可以使用这个迭代器了:
125 | 126 |console.log( it.next() ); // { value: 1, done: false }
127 |
128 | 这里的 it.next() 返回 { value: 1, done: false },其中的 value: 1 是 yield 1 返回的值,而 done: false 表示生成器函数还没有迭代完成。
继续调用 it.next() 进行迭代:
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,是因为这时候生成器函数并未处于完成状态,我们再调用一次看看:
console.log( it.next() ); // { value:undefined, done:true }
140 |
141 | 这时候我们已经执行完了所有的 yield ___ 表达式,所以 done 已经为 true。
你可能会好奇的是:如果我们在一个生成器函数中使用了 return,我们在外部还能获取到 yield 的值么?
答案可以是:能
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 先初始化了一个迭代器。
第一个 next() 中没有传递参数进去,因为这个生成器函数中没有对应的 yield 来接收参数,所以如果我们在第一个 next() 强制传参进去的话,什么都不会发生。
175 | 第一个 yield (x+1) 将返回 value: 6 到外部,此时生成器未迭代完毕,所以同时返回 done: false 。
第二个 next(12) 中我们传递了参数 12 进去,则表达式 yield(x+1) 会被赋值为 12,相当于:
var x = 5; 180 | var y = 2 * 12; // => 24181 | 182 |
第二个 yield (y/3) 将返回 value: 8 到外部,此时生成器未迭代完毕,所以同时返回 done: false 。
同理,在第三个 next(13) 中我们传递了参数 13 进去,则表达式 yield(y/3) 会被赋值为 13,相当于:
var x = 5 187 | var y = 24; 188 | var z = 13;189 | 190 |
第三个 yield并不存在,所以会 return (x + y + z) 即返回 value: 42 到外部,此时生成器已迭代完毕,所以同时返回 done: true 。
答案也可以是:不能!
193 | 194 |依赖 return 从生成器中返回一个值并不好,因为当生成器遇见了 for..of 循环的时候,被返回的值将会被丢弃,举个栗子:
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 的值。
所以,for..of 循环会忽略被返回的 6,同时因为没有暴露出 next() 方法,for..of 循环就不能用于我们在中断生成器的期间,对生成器进行传值的场景。
看了以上 ES6 Generators 的基础知识,很自然地就会想我们在什么场景下会用到这个新颖的生成器呢?
219 | 220 |当然有很多的场景能发挥生成器的这些特性了,这篇文章只是抛砖引玉,我们将继续深入挖掘生成器的魔力!
221 | 222 |当你在最新的 Chrome nightly 或 canary 版,或 Firefox nightly版,甚至在 v0.11+ 版本的 node (带 –harmony 开启 ES6 功能)中运行了以上这些代码片段
223 | 224 |ES6 中生成器的其中一个强大的特点就是:函数内部的代码编写风格是同步的,即使外部的迭代控制过程可能是异步的。
227 | 228 |也就是说,我们可以简单地对错误进行处理,类似我们熟悉的 try..catch 语法,举个栗子:
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 将会捕获到。
具体一个错误是怎样传入生成器的呢,举个栗子:
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 捕获。
注意:如果我们使用 throw() 方法产生一个错误传进生成器中,但没有对应的 try..catch 对错误进行捕获的话,这个错误将会被传出去,外部如果不对错误进行捕获的话,则会抛出异常:
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 | 另一个我们想做的可能是在一个生成器中调用另一个生成器。
289 | 290 |我并不是指在一个生成器中初始化另一个生成器,而是说我们可以将一个生成器的迭代器控制交给另一个生成器。
291 | 292 |为了实现委托,我们需要用到 yield 关键字的另一种形式:yield *,举个栗子:
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()迭代完后再将控制权收回,继续进行迭代。
这里使用了 for..of 循环进行示例,正如在基础篇我们知道 for..of 循环中没有暴露出 next() 方法来传递值到生成器中,所以我们可以用手动的方式:
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()) 获取到返回值,举个栗子:
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() 再继续迭代下去。
yield 和 yield * 表达式的一个有趣的区别是:在 yield 中,返回值在 next() 中传入的,而在 yield * 中,返回值是在 return 中传入的。
此外,我们也可以在委托的生成器中进行双向的错误绑定,举个栗子:
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() 中被捕获。
同理,throw "Oops!" 在 foo() 内部抛出的错误,将会传回给 bar() 后,被 bar() 中的 try..catch 捕获到。
生成器有着同步方式的编写语法,意味着我么可以使用 try..catch 在 yield 表达式中进行错误处理。
生成器迭代器中也有一个 throw() 方法用于在中断期间向生成器内部传入一个错误,这个错误能被生成器内部的 try..catch 捕获。
yield * 允许我们将迭代器的控制权从当前的生成器中委托给另一个生成器。好处是 yield * 扮演了在生成器间传递消息和错误的角色。
了解了这么多,还有一个很重要的问题没有解决:
421 | 422 |怎么异步地使用生成器呢?
423 | 424 |关键是要实现这么一个机制:在异步环境中,当迭代器的 next() 方法被调用,我们需要定位到生成器中断的地方重新启动。
生成器提供了同步方式编写的代码风格,这就允许我们隐藏异步的实现细节。
427 | 428 |我们就可以用一种非常自然的方式来表达程序的执行流程,避免了同时处理异步代码的语法和陷阱。
429 | 430 |换句话说,我们利用生成器从内到外、从外到内双向传值的特点,将不同的值的处理交给了不同的生成器逻辑,只需要关心获取到特定的值进行某种操作,而无需关心特定的值如何产生(通过netx() 将值的产生逻辑委托出去)。
这么一来,异步处理的优点以及易读的代码结合到一起,就加强了我们程序的可维护性。
433 | 434 |举个栗子,假定我们已经有了以下代码:
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() 方法。
当我们使用 var it = main(); 创建了一个迭代器后,紧接着使用 it.next(); 开始迭代,这时候遇到第一个 yield 中断了生成器,转而执行 request( "http://some.url.1" )
当 request( "http://some.url.1" ) 异步获取到数据后,在回调函数中调用 it.next(response) 将 response 传回给生成器刚刚中断的地方,生成器将继续迭代。
这里的亮点就是,我们在生成器中无需关心异步请求的数据如何获取,我们只知道调用了 request() 后,当需要的数据获取到了,就会通知生成器继续迭代。
这么一来在生成器中我们使用同步方式的编写风格,其实我们获取到了异步数据!
485 | 486 |同理,当我们继续调用 it.next() 时,会遇到第二个 yield 中断迭代,发出第二个请求 yield request( "http://some.url.2?id=" + data.id ) 异步获取到数据后再恢复迭代,我们依旧不用关心异步获取数据的细节了,多爽!
以上这段代码中,request() 请求的是异步 AJAX 请求,但如果我们后续改变程序给 AJAX 设置了缓存了,获取数据会先从缓存中获取,这时候没有执行真正的 AJAX 请求就不能在回调函数中调用 it.next(response) 来恢复生成器的中断了啊!
没关系,我们可以使用一个小技巧来解决这个问题,举个栗子:
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() 这个工具函数的逻辑,而无需改动调用这个工具函数获取数据的生成器:
var result1 = yield request( "http://some.url.1" ); 515 | var data = JSON.parse( result1 ); 516 | ..517 | 518 |
在生成器中,我们还是像以前一样调用 request() 就能获取到需要的异步数据,无需关心获取数据的细节实现!
这就是将异步操作当做一个细节实现抽象出来后展现出的魔力了!
521 | 522 |上面介绍的异步方案对于简单的异步生成器来说工作良好,但用途有限,我们需要一个更强大的异步方案:使用 Promises.
525 | 526 |如果你对 ES6 Promises 有迷惑的话,我建议你先读 我写的介绍 Promises 的文章
527 | 528 |我们的代码目前有个严重的问题:回调多了会产生多重嵌套(即回调地狱)。
529 | 530 |此外,我们目前还缺乏的东西有:
531 | 532 |清晰的错误处理逻辑。我们使用 AJAX 的回调可能会检测到一个错误,然后使用 it.throw() 将错误传回给生成器,在生成器中则使用 try..catch 来捕获错误。
534 | 一来我们需要猜测我们可能发生错误且手动添加对应的错误处理函数,二来我们的错误处理代码没法重复使用。
如果 makeAjaxCall() 函数不受我们控制,调用了多次回调的话,也会多次触发回调中的 it.next() ,生成器就会变得非常混乱。
处理和阻止这种问题需要大量的手动工作,也非常不方便。
有时候我们需要 『并行地』执行不只一个任务(比如同时触发两个 AJAX 请求)。而生成器中的 yield 并不支持两个或多个同时进行。
以上这些问题都可以用手动编写代码的方式来解决,但谁会想每次都重新编写类似的重复的代码呢?
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。
我们接着将这个实例返回,这样它就能够被 yield 了。
接下来我们需要一个工具来控制我们生成器的迭代器,接收返回的 promise 实例,然后再通过 next() 来恢复生成器的中断:
// 执行异步的生成器
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)。
接着我们就可以使用这个 runGenerator() 了:
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 后再执行对应的操作。
这么一来,我们只需要同时生成不同的 promise 实例,就可以『并行地』执行不只一个任务(比如同时触发两个 AJAX 请求)了。
606 | 607 |既然我们使用了 promises 来管理生成器中处理异步的代码,我们就解决了只有在回调中才能实现的功能,这就避免了回调嵌套了。
608 | 609 |使用 Generotos + Promises 的优点是:
610 | 611 |我们可以使用内建的错误处理机制。虽然这没有在上面的代码片段中展示出来,但其实很简单:
613 | 614 |监听 promise 中的错误,使用 it.throw() 把错误抛出,然后在生成器中使用 try..catch 进行捕获和处理即可。
我们可以使用到 Promises 提供的 control/trustability 特性。
Promises 提供了大量处理多并行且复杂的任务的特性。
617 | 618 |举个栗子:yield Promise.all([ .. ]) 方法接收一组 promise 组成的数组作为参数,然后 yield 一个 promise 提供给生成器处理,这个 promise 会等待数组里所有 promise 完成。当我们得到 yield 后的 promise 时,说明传进去的数组中的所有 promise 都已经完成,且是按照他们被传入的顺序完成的。
首先,我们体验一下错误处理:
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 捕获。
再举个使用 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 会被 yield 给 runGenerator() 生成器,然后这个生成器就可以恢复迭代。
在上面的代码片段中,我们自己编写了 runGenerator() 函数来提供 Generators + Promises 的功能,其实我们也可以使用社区里优秀的类库,举几个栗子: Q 、Co、 asynquence 等
接下来我会简要地介绍下 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 函数类型。
使用这种函数,我们可以向外部发出 promises,然后使用 async 函数自动地将这些 promises 连接起来,当 promises 完成的时候,就会恢复 async 函数自己的中断(不需要在繁杂的迭代器中手动恢复)。
这个提议如果被采纳的话,可能会像这样:
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() 一样进行包装。
此外,我们没有使用 yield 关键字,而是使用了新的 await 关键字来声明等待 await 后面的 promise 的完成。
一言以蔽之:Generators + Promises 的组合,强大且优雅地用同步编码风格实现了复杂的异步控制操作。
778 | 779 |使用一些简单的工具类库,比如上面提到的 Q 、Co、 asynquence 等,我们可以更方便地实现这些操作。
780 | 781 |可以预见在不久的将来,当 ES7+ 发布的时候,我们使用 async 函数甚至可以无需使用一些类库支撑就可以实现原生的异步生成器了!
(译注:本文是第三篇文章,其实还有最后一篇是讲述并发式生成器的实现思路,涉及到 CSP 的相关概念,原文中引用了比较多的东西,读起来比较晦涩难懂,怕翻译出来与原文作者想要表达的东西相差太远,就先放一边了,感兴趣的可以直接查看原文。 784 | 欢迎大牛接力)
785 | 786 | --------------------------------------------------------------------------------