[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"list-\u002Farticles\u002F8":3},{"items":4,"total":144},[5,23,34,43,52,63,78,88,101,113,120,133],{"id":6,"type":7,"slug":8,"title":9,"date":10,"category":11,"tags":12,"body_markdown":16,"permalink":17,"excerpt_src":18,"media_type":18,"media_title":18,"media_author":18,"media_url":18,"rating":18,"layout":18,"pv":19,"admin_only":19,"created_at":20,"updated_at":20,"deleted_at":18,"status":21,"cover":22},164,"article","alert_in_node","Node.js中低成本实现错误告警","2016-01-27 20:18","web",[13,14,15],"Node","错误","告警","\n相比其他语言（特指PHP）而言，Node.js应用更需要关注出错信息，因为一旦处理不慎，就会导致应用crash。\n\n一种偷懒的方法是使用[PM2](https:\u002F\u002Fgithub.com\u002FUnitech\u002Fpm2)之类的进程管理软件来启动Node.js进程，从而达到出错crash后自动重新启动应用的目的。\n\n当然更好的办法则是手工捕获错误，然后进行适当的处理，防止应用产生未被接住的错误导致crash。\n\n在捕获到Node.js产生的错误后，下一步自然是记录到错误日志中，以便日后可以进行分析，并针对性地排查修改。本文要说的，即是对错误日志的处理方式之一——告警。\n\n告警是运维工作中非常重要的一个环节，它能让开发者（维护者）及时获知应用出错状态和详情，及早介入处理，将线上故障的影响降低到最低。而要实现告警功能，则需要从两方面入手，一方面是对错误信息进行集中处理（分类、分级、合并、限流等），另一方面需要将这些错误信息及时推送出去。\n\n\u003C!-- more -->\n\n## 推送\n\n推送渠道可以有很多种，常见的包括邮件、短信、微信等，Geek一点的还可以考虑用slack、GTalk(死了吧)、Telegram机器人推送什么的。\n\n为了降低开发成本，这里选用了[Server Chan](http:\u002F\u002Fsc.ftqq.com\u002F1.version)作为推送服务，它的使用极其简单，只要登录之后就会获得一个key，然后访问带key的URL `http:\u002F\u002Fsc.ftqq.com\u002F{KEY}.send` 即可完成消息推送。而推送的渠道则有两种，一种是手机客户端，另一种是微信。想要哪种就使用哪种，在网站绑定即可，推送时是不分渠道的。\n\n## 日志\n\n接下来是应用的错误日志收集，在打听了很多方案之后先用了[bunyan](https:\u002F\u002Fgithub.com\u002Ftrentm\u002Fnode-bunyan)这个模块作为日志记录工具。bunyan的优势在于：\n\n- 结构化日志数据，方便后续整理分析\n- 完善的错误分级 `fatal` \u002F `error` \u002F `warn` \u002F `info` \u002F `debug` \u002F `trace` 一应俱全\n- 多种错误处理方式：文件、控制台、流\n- 可扩展：可以通过扩展流的方式自定义错误处理逻辑\n- 日志文件自动滚动\n\n这里我们主要用到bunyan的扩展性，自定义一个流来获取错误，然后在自定义的逻辑中调用推送逻辑完成告警。\n\n大概的代码：\n\n```javascript\nvar ServerChan = require('bunyan-serverchan');\n\nvar logger = bunyan.createLogger({\n\tname: 'myapp',\n\tstreams: [{\n\t\tlevel: 'error',\n\t\tstream: new ServerChan({key:'MY_KEY'})\n\t}]\n});\n```\n\n首先我们定义了一个`logger`用来记录日志，记录到的`error`级别以上的日志会送给`ServerChan`的实例（一个“stream”）。关于`ServerChan`，稍后解释。\n\n接下来，在出错的地方调用`logger`记录错误：\n\n```javascript\nxxx.on('error',function(err){\n\tlogger.error(err, 'Something went wrong:%s',err.message);\n});\n```\n\n此时错误的记录部分就算完成了，bunyan会负责将错误信息传递给`ServerChan`的实例。\n\n## ServerChan模块\n\n在上面的代码中，我们通过`require('bunyan-serverchan')`引入了`ServerChan`，这个模块负责接受错误信息，并调用Server Chan的URL完成推送。\n\n那这个模块到底是什么呢？\n\n没错，是我写的，欢迎到\u003Chttps:\u002F\u002Fgithub.com\u002FTooBug\u002Fbunyan-serverchan>围观。\n\n事实上这个模块的逻辑极其简单，调用构造函数之后会生成一个对象，只要保证这个对向的`write`方法是存在的，即可以用于bunyan的自定义stream。也就是说，虽然bunyan的概念中是一个自定义stream，但我们并不需要真的实现一个stream，只需要一个有`write`方向的对象即可。\n\n`write`方向负责接受错误信息，并完成自定义逻辑（推送）。\n\n## 结\n\nOver，就这么简单。\n","\u002Farticle\u002Falert_in_node.html",null,0,"2026-08-28 04:37:17","published","",{"id":24,"type":7,"slug":25,"title":26,"date":27,"category":28,"tags":29,"body_markdown":31,"permalink":32,"excerpt_src":18,"media_type":18,"media_title":18,"media_author":18,"media_url":18,"rating":18,"layout":18,"pv":19,"admin_only":19,"created_at":20,"updated_at":20,"deleted_at":18,"status":21,"cover":33},119,"git_revert_merge","[译]Git回滚合并","2015-05-26 12:50","tech",[30],"Git","\n如果工作流严重依赖分支合并的话，也难免会碰到需要回滚合并的情况。而且回滚还分为两种情况：永久回滚，或者回滚后在稍后重新合并。\n\n假设有如下分支图：\n\n![分支图](\u002Fassets\u002Fgit_revert_merge\u002Funmerge1.png)\n\n> 注：Git分支图中的箭头表示依赖关系，并不是分支发展路线。发展路线和箭头是相反的。也就是图中是从C1开始一直发展到C12的。\n\n假设要回滚C10。\n\n第一种解决方案是将`master`回退到C8，然后将两个特性分支`jk\u002Fpost-checkout`和`db\u002Fpush-cleanup`合并过来。\n\n```sh\ngit checkout master\ngit reset --hard [sha_of_C8]\ngit merge jk\u002Fpost-checkout\ngit merge db\u002Fpush-cleanup\n```\n\n完成之后，分支图如下：\n\n![分支图](\u002Fassets\u002Fgit_revert_merge\u002Funmerge2.png)\n\n接下来就可以继续在新的`master`上工作，然后在适当的时候将`tv\u002Frebase-stat`合并回来。\n\n\u003C!-- more -->\n\n## 回滚合并\n\n如果在很久之后才发现要回滚，或者其它人已经在合并之后提交了代码，分支图会是这样：\n\n![分支图](\u002Fassets\u002Fgit_revert_merge\u002Funmerge3.png)\n\n这种情况下要么回退一次合并，要么退回去，再重新合并，然后将新的变更（C9和C10）cherry-pick过来。后者容易让人迷惑，做起来也不容易，尤其是在合并之后提交很多的情况下。\n\n`git revert`能很好地处理合并的回退。你需要指定需要回退的那次合并提交记录，并指定保留合并中的哪一条线（parent）。假设我们需要回退合并`jk\u002Fpost-checkout`的记录，则这样做：\n\n```sh\ngit revert -m 1 [sha_of_C8]\nFinished one revert.\n[master 88edd6d] Revert \"Merge branch 'jk\u002Fpost-checkout'\"\n 1 files changed, 0 insertions(+), 2 deletions(-)\n```\n\n完成后会产生一个新提交，这个提交回滚了合并过来的内容。其结果和一个包含被合并过来分支改动的cherry-pick（反向操作，即回滚）差不多。\n\n![分支图](\u002Fassets\u002Fgit_revert_merge\u002Funmerge4.png)\n\n## 回滚“回滚”\n\n假设在回滚之后，我们需要再次合并这个分支。如果你直接合并的话，什么都不会发生。\n\n```sh\ngit merge jk\u002Fpost-checkout\nAlready up-to-date.\n```\n\n更令人困惑的是，如果你回到分支，再做一些修改后再合并，则只有新产生的修改会被合并过来。\n\n![分支图](\u002Fassets\u002Fgit_revert_merge\u002Funmerge5.png)\n\n这种状态是一种非常奇怪的状态，有可能导致冲突或者难以理解的错误。此时你真正想做的事情应该是回滚“上一次对合并的回滚操作”。\n\n```sh\ngit revert 88edd6d\nFinished one revert.\n[master 268e243] Revert \"Revert \"Merge branch 'jk\u002Fpost-checkout'\"\"\n 1 files changed, 2 insertions(+), 0 deletions(-)\n```\n\n![分支图](\u002Fassets\u002Fgit_revert_merge\u002Funmerge6.png)\n\n现在我们将分支恢复到了合并之后的情况，如果分支上有新的改动，就可以直接合并了。\n\n```sh\ngit merge jk\u002Fpost-checkout\nAuto-merging test.txt\nMerge made by recursive.\n test.txt |    1 +\n 1 files changed, 1 insertions(+), 0 deletions(-)\n ```\n\n![分支图](\u002Fassets\u002Fgit_revert_merge\u002Funmerge7.png)\n\n最后，建议使用`git merge --no-ff`合并分支，这样可以保持 分支不是快进的，使它可以回滚。\n\n> 原文地址\u003Chttps:\u002F\u002Fgit-scm.com\u002Fblog\u002F2010\u002F03\u002F02\u002Fundoing-merges.html>，本文并未逐字逐句翻译，仅根据理解摘录了重要部分。\n","\u002Farticle\u002Fgit_revert_merge.html","https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Fgit_revert_merge\u002Funmerge1.png",{"id":35,"type":7,"slug":36,"title":37,"date":38,"category":28,"tags":39,"body_markdown":41,"permalink":42,"excerpt_src":18,"media_type":18,"media_title":18,"media_author":18,"media_url":18,"rating":18,"layout":18,"pv":19,"admin_only":19,"created_at":20,"updated_at":20,"deleted_at":18,"status":21,"cover":22},118,"git_and_gitflow","团队使用Git和Git-Flow手记","2015-05-11 14:23",[30,40],"Git-Flow","\n去年10月份，在我们被产品节奏逼到墙角无路可走的时候，我们在几乎没有准备的情况下，在团队中引入了Git。目前时间已经过去半年，回顾这半年的时间，基本还是运作得比较顺利。当然过程中也少不了踩坑，因此记录一些心得。\n\n## Why Git?\n\n如果用一句话来说的话，我们是冲“分支”而来的。背景如下：\n\n团队的固定版本节奏为两周一个版本，一周半的时间开发，半周时间测试发布。如果使用软件工程中的概念来说的话，这是一个比较典型的瀑布式流程，即“需求->设计->开发->测试->发布”，然后周而复始，过程中几乎没有重叠。\n\n伴随着瀑布式的流程，代码也只有一份，“开发->改bug->发布”，周而复始。\n\n直到去年10月，公司做了一场声势浩大的营销活动，灾难开始了：一方面需要以并不确定的研发周期支持各种运营活动（运营时间不等人，必须快速写快速发），一方面是运营活动带来大量客户，与客户相关的流程也陷入频繁修改发布的过程。此外支持各种终端需求也集中爆发，需要web侧快速跟进上线。当然，还有一个东西，就是上面瀑布流程中两周发布一次的“主版本”。当这么多版本交叠在一起时，我们发现必须要引入分支来管理研发过程。于是果断切Git。\n\n\u003C!-- more -->\n\n## 困扰\n\n引入Git的过程并非一帆风顺，中间也有不少困扰。\n\n### 概念和特性\n\n首先是Git的概念繁多，而且很多命令并不直观。尤其是有一些和svn很像的命令，需要一段时间去理解。比如svn的`commit`会推送到服务器，而Git不会，这会导致解释Git的`push`命令并不是那么容易。\n\n另外Git会在操作可能导致修改丢失时拒绝操作。\n\n比如当前分支上有个文件A，当前状态为A1，我们将它做一点修改，并没提交，此时状态为A2。切换到另一分支的状态B1，如果A1和B1不一致，就需要覆盖当前文件，从A2切到B1，此时就会导致状态A1到A2的改动丢失，Git会拒绝操作。\n\n类似的场景很多，只要Git发现有改动会丢失就会拒绝操作，而如果改动不会丢失，才允许操作。这种是否允许操作的不一致性也会困扰团队成员。解决的方案是引入stash操作，或者鼓励成员多提交。\n\n### 分支思维\n\n然后是从单线开发转入多线开发的思维转变。\n\n在切Git之前，我们的代码都是只有一个主线的（其实有另外一份代码，但是是以Copy文件夹的形式存在，更多的意义在于“备份”）。而在切换Git之后，一方面概念繁多，一方面还要时刻去关注代码所处的分支状态。甚至会有很多成员在一开始很怀疑分支的有效性，总会担心自己辛苦写的代码一不小心切完就再也找不回来。这个问题也同样需要一段时间来适应。\n\n### 日常操作\n\n另外就是对于日常操作的困扰。\n\n对比svn，Git的日常代码提交增加了add和push的过程，略显繁琐。但繁琐并不是很大的问题。真正的问题在于Git对版本和文件完整性的要求导致不允许对未提交的文件进行合并。直观的表现就是本机修改了文件无法直接与远程合并，必须要先提交，再合并远程，最后再推送。如果本机有部分文件无法提交的话，还需要增加stash相关的动作，整个流程就变为“add->commit->stash->pull(merge或rebase)->push->stash pop”，而svn的则是“update->commit”，光看看路径的长度就懂了。\n\n这个问题并没有什么好的解决办法，只能是让成员不断练习、练习、实践、实践，然后习惯。\n\n当然也有部分Git客户端会简化这个操作，比如Github客户端（[Mac](https:\u002F\u002Fmac.github.com) [Windows](https:\u002F\u002Fwindows.github.com)）和[SmartGit](http:\u002F\u002Fwww.syntevo.com\u002Fsmartgit\u002F)还有命令行工具[LeGit](https:\u002F\u002Fgithub.com\u002Fkennethreitz\u002Flegit)都提供了一个操作叫`sync`，就是把上面列的这一长串流程放到一起了。\n\n### 二进制文件\n\n在从单线开发转到多线并行的开发后，一般情况下可以在各个分支独立处理自己的事情，最后由Git来进行合并。但是如果碰到二进制文件，事情就变得完全不一样，比较典型的案例就是雪碧图。\n\n当多个分支都要修改雪碧图的时候，如果独立在各分支中修改，最后再合并，场景一定很壮观。因为Git没法处理二进制文件的合并。\n\n对这个问题，短期的解决方案是，涉及到二进制文件改动时，在各个分支同步修改。\n\n是的，一点都不优雅，所以长期的方案是，干掉二进制文件合并的场景。比如对于雪碧图来说，如果将图都拆开，在开发阶段不作合并，则代码合并也不会出问题。因为改动都是“增加了icon1.png”“删除了icon2.png”，只有两个分支同时增加或者修改同名文件才会出问题，如果不合并雪碧图，则几乎不存在这样的场景。因此这个问题较好的解决办法是将雪碧图放到构建阶段，由自动化打包工具来完成。\n\n## 经验\n\n### 使用Git-Flow\n\n在没有分支管理经验的时候，全盘引入别人的成功经验是可取的。我们一开始也是全盘引入了[Git-Flow](http:\u002F\u002Fwww.oschina.net\u002Ftranslate\u002Fa-successful-git-branching-model)，虽然并不是100%完美，但很大程度上避免了前期刚切入Git时的混乱期。\n\n期间我们发现Git-Flow并没有对测试介绍做出指导（何时在哪个分支做测试），导致 我们唯一的测试服务器上经常出现版本混乱，于是我们尝试加入了tests分支，但事实证明这个分支并不能很好地与其它分支协作，于是作罢，仍然基本采用Git-Flow的流程来做。（回想起来，当时测试服务器上出的问题并不是Git-Flow带来的。测试应该是在release分支拉出后再做，而如果是单独测试特性，则直接在feature分支做。如果需要同时测多个东西，则需要多台测试服务器，于是后来我们也增加了很多测试服务器来解决这个问题。）\n\n公司内也有其它团队使用Git，但是不采用Git-Flow，结果就会经常为拉分支和合并分支的事情而困扰。\n\n### 有关release分支\n\n在一开始看到Git-Flow的时候，我以为release分支是一个存活期非常短的分支，只是拉出来打打版本号，然后立马就消失了。但是后来逐渐认识到，release分支其实相当于对develop分支的一个冻结副本，release拉出来的时候就意味着上面的需求都已经冻结，在release上唯一可以继续做的改动只有这些需求的bug修复。而release一旦拉出后，develop上就可以继续执行新功能开发，这样新功能开发和版本测试发布可以并行，所以测试的介入的理想节点是release版本。\n\n如果是瀑布式模型，测试和新功能开发不重合，则可以不需要release版本。目前我们团队内还没有使用到release分支。\n\n### 有关develop分支\n\n在理想情况下，一个版本开始的时候develop和master是完全一样的。此时开始在develop上做开发，相当于这是一个大版本，上面拉出来的各种feature分支都是在做这个大版本开发的一部分（它们最后也合回develop，和直接在develop上开发的效果是一样的）。而如果一个大版本正在开发，此时需要再来一个并行的大版本（比如碰到了长线需求），也即正在开发的独立大版本不止一个，则理论上需要并行的多个develop分支。这是Git-Flow的各种介绍文章中均没有提到的问题。\n\n所以，当碰到有多个并行大版本的需求时，如果要准备开发第二个大版本的需求（也可以简化为“独立发布的需求”），则必须**不能**使用Git-Flow从develop拉出feature分支。此时应该使用hotfix分支（相当于另一个develop分支）。\n\n### 有关hotfix\n\n在看过上面一段后，应该能明白，hotfix的地位和develop其实是一样的，只是develop存在的时间更久一些（“大”版本嘛），还会拉出功能分支，接受功能分支合并。简单说，他们的区别就是develop更“重”一些，hotfix更轻量一些，本质上都是从master来，回master去。\n\n### 有关master\n\n在有紧急bug的时候，Git-Flow要求拉出一个hotfix分支来处理，而不能直接改动master。但从我们实践的经验来看，master上并不是不能做修改，有时候为简单起见，也可以在master上直接改，只是做完修改需要合并回develop，合并完之后和拉hotfix分支再完成等效。\n\n## 一些习惯\n\n### rebase和merge\n\n建议本机和远程分支（相同分支）同步必须用rebase，因为不用rebase就会使用merge，这样会导致（逻辑上的）同一分支在版本记录中变成分叉的多个分支（然后这些分支合并的消息全是`merge xxx branch of http:\u002F\u002Fremote.server\u002Fxxx.git`），不利于追踪版本记录。\n\n但是，从实践的经验来看，rebase在发生冲突时解决方案并不是那么友好。首先是此时的“my version”和“theirs version”并不和想象的一样。（详情可参考rebase文档，很可能“theirs”其实是自己的代码。）然后，在极端情况下rebase会导致代码丢失（目前原因未知）。\n\n上面说的是本机和远程的相同分支使用rebase。如果是不同分支的合并则必须用merge，否则会导致历史记录不可读，因为再也找不到合并之前各个分支到底是在哪里。\n\n另外还有一个需要注意的点，假设有这样的场景：现在有两个分支A和B，本机A的状态为A1，远程的比A1新，为A2，此时需要将B合并到A。按照上方所说，本机和远程的A分支同步使用rebase，B合并到A使用merge。\n\n如果先将B merge到A，则本机的状态为A1，然后是B。此时再拉远程的A2，会导致B合并过来的记录丢失（在分支图中看不到合并的痕迹。）因此碰到这种情况，既需要和远程同步，又需要合并其它分支的情况，一定要先rebase再merge，否则会丢失merge记录。\n\n### commit和stash\n\n如前文所述，svn的习惯（update->commit）行不通，因此需要先stash或者commit。鼓励团队成员多commit，即使没写完，也可以在下次提交时使用`--amend`将两次提交进行合并。\n\n之所以鼓励commit而不是stash，是因为stash并不在版本记录中，理解起来并不那么容易，对比和合并也不是特别方便。\n\n此外就是前文说的，Git sync是个好功能，但是从实践的情况来看，sync时并不一定会选择rebase，而merge会导致版本记录可读性变差。\n\n### 有关tags\n\n如果有需要从Git中提取代码的情况，则通过tags来操作是非常好的，因为可以很方便地追踪代码版本情况。如果不打tags，也可以使用提交的hash值，也可以追踪，只是人眼看起来没有那么直观了。而如果使用分支名，技术上也可行，但是会导致追踪困难。（试想，你能很轻易地找到前天晚上master在哪里么？）\n\n而如果有发布系统来记录每一次发布的情况，则可以不用tags，因为发布系统可以记录当时提交的hash值，追踪起来也很方便。\n\n## 小结\n\n坑都是要自己踩的，认真踩下来，结果还不错。谨以此文献给正被Git团队协作困扰的团队，希望能提供一些参考。\n","\u002Farticle\u002Fgit_and_gitflow.html",{"id":44,"type":7,"slug":45,"title":46,"date":47,"category":48,"tags":49,"body_markdown":51,"permalink":18,"excerpt_src":18,"media_type":18,"media_title":18,"media_author":18,"media_url":18,"rating":18,"layout":18,"pv":19,"admin_only":19,"created_at":20,"updated_at":20,"deleted_at":18,"status":21,"cover":22},79,"2014-summary","2014小结","2015-03-11 19:05","life",[50],"年度总结","\n一晃已经到2015年的3月份了。Todo list中的“总结”俩字还一直刺眼地呆在那，再不写下这篇小结的话估计又得欠下一年了。\n\n## 选择\n\n2013年的总结文章以“勿忘初心”结尾，并且以预言的口吻交代了一些即将发生的事情。回头看来，这些事情都一一发生了。其中对我影响最大的当数跳槽了。\n\n可能对所有人来说，对职场都会有一个从生涩到熟练的过程，或者说得更通俗一点，混得时间长了，总能找到混日子的办法。我也是这样。在工作步入第三年之后，日子突然就闲起来了。但从骨子里说，我并不是一个愿意心安理得混日子的人。于是在2014年6月的最后一天，我离开了正式工作整整三年的腾讯。\n\n做出选择并不轻松，在打算离开之后，那段日子多少会显得有点沉重，毕竟多少还是会怀念曾经战斗过的日子，还有一起战斗过的战友。我以为我会走得很轻松，但是真的走出办公室的时候，所有人都望着我，望得人很心疼，也是在那一刻，泪如泉涌。\n\n## 忙\n\n在北京瞎晃了一个星期之后，7月7日，正式来到现在的公司。虽然已经有心理准备，但工作强度仍然非常大。入职大半年，一直保持非常忙碌的状态，而且预计这种状态还将持续很长时间。\n\n## 收获\n\n放弃轻闲的日子，加入一个如此忙碌的团队，必有所图。于我而言，重要的是有事情可做。我看到了我之前收藏的牛刀有了用武之地，我看到了我听说的一些方法有了更具体的形象。虽然忙碌，但是心安。\n\n## 愧疚和感恩\n\n曾经引用过一句话，说“心在哪，时间就在哪”。但于我现在的状态而言，对于家人，确实心有余而力不足。有时候想起来好久没给家里电话，有时候想起亲人落寞的样子，忍不住会愧疚。我很感谢他们支持我，更感谢他们受了委屈还不说的那份心。\n\n也要感谢很多朋友关心和支持，有的人没时间一一当面致谢，都会记在心里。\n\n## 改变\n\n随着时间越来越不够用，这一年也做了很多改变。\n\n首先改变了作息时间，晚上的时间不再拿来学习、输出，而是用来聊天和休息。早上早起一两个小时，把早上的时间拿来学习和输出。这样的改变效果非常好，晚上轻松的氛围很适合增进家人之间的交流，而早上的时间则学习输出效率更高。同时早睡还能带来更饱满的精神。\n\n然后戒掉了新闻。半年多下来，似乎并没有错过什么，反而因为看不到很多文笔不通的文章而觉得心情平静了很多。再回来来看，有时候看新闻也只是一种无意识的行为，只是因为内心想给自己找点事做，或者是生怕自己错过了什么，但其实对大部分人来说，只是在浪费时间。\n\n## 小而美的事\n\n比如一些美的事：参与翻译的书出版了，看着自己的名字印在书上，感觉还是美美的。只不过稿费着实太低，也只能当作练练手了。\n\n比如一些小的事：买股票了，虽然一直亏着，但说起来还是高大上的。\n\n比如一些小而美的事：听到了《蒋勋说红楼》，相见恨晚。\n\n当然还有些趣事，比如去看房子，一个新手售楼小姐面对两个没钱的年青人大谈豪宅，还差点谈成了一千万的生意，回头想起来也忍不住会微微一笑。\n\n## 结\n\n似乎还有很多想说的没说，随风去吧。只留一句，希望明年的总结是带图的。\n",{"id":53,"type":7,"slug":54,"title":55,"date":56,"category":11,"tags":57,"body_markdown":61,"permalink":62,"excerpt_src":18,"media_type":18,"media_title":18,"media_author":18,"media_url":18,"rating":18,"layout":18,"pv":19,"admin_only":19,"created_at":20,"updated_at":20,"deleted_at":18,"status":21,"cover":22},163,"using_pm2_deploy","使用PM2 Deploy部署基于Git版本管理的网站应用","2014-11-19 13:29",[58,59,60],"Node.js","PM2","部署","\n按照官方介绍，PM2是一款用于生产环境Node.js应用进程管理的工具。按照民间介绍，它主要有这样几个功能：保证Node.js应用永远在线（挂掉自动重启）、自动负载均衡、零中断重启应用等。\n\n鉴于它是如此优秀，这里还是简要介绍一下前两个功能。\n\n## 安装\n\n首先，它是一个Node.js写的工具，使用npm即可安装使用：\n\n\tnpm install -g pm2\n\n## 运行Node.js程序\n\n如果不使用pm2，运行Node.js程序是这样：\n\n\tnode xxx.js\n\n使用pm2，是这样：\n\n\tpm2 start xxx.js\n\n### 监视模式\n\n如果你正在开发Node.js应用，需要在代码变更后自动重启应用，只需要在pm2的参数中加上`--watch`即可：\n\n\tpm2 start xxx.js --watch\n\n\u003C!-- more -->\n\n### cluster模式\n\n默认情况下pm2是以fork模式启动应用的，如果以cluster模式启动的话，则可以使用pm2自带的负载均衡、零间断重启等功能。\n\n\tpm2 start xxx.js -i 4\n\n上面的命令会以cluster模式启动4个应用进程，并自动为它们提供负载均衡，并且可以使用gracefulReload达到更新应用时不中断服务的效果。\n\n> 关于cluster模式，可参见朴灵《深入浅出Node.js》一书。\n>\n> pm2低版本默认是以cluster模式启动的。\n\n## 部署应用\n\n这才是本文的重点。PM2的部署功能可以实现网站应用的半自动部署功能。注意本文标题没有加“Node.js”，意味着这个功能并不只适用于Node.js网站应用，事实上它部署功能是用shell写的，跟网站使用什么语言没什么关系。\n\nPM2的部署功能与版本管理工具（Git，不确定是否支持SVN，下文以Git为例）结合比较紧，因此需要保证网站项目使用版本管理工具管理代码，并且服务器可以访问到版本管理服务器。\n\n部署功能是在新版本（0.12？）中才添加进来的。如果你使用的是旧版本的，需要先升级：\n\n```sh\nnpm install -g pm2@latest\npm2 updatePM2\n```\n\n接下来需要建立一个部署的配置文件，这个文件在本机（操作发布的机器）和服务器上都需要有，因此最好放入Git版本管理中，并且推送到远程代码库（Git服务器）。\n\n切换到项目目录下，然后执行\n\n```sh\npm2 ecosystem\n```\n\n即可得到一个示例json文件（例如我得到的是`ecosystem.json5`），将它做对应的修改，大致如下：\n\n```json\n{\n\t\"apps\" : [{\n\t\t\"name\" : \"xxx\", \u002F\u002F项目的名字\n\t\t\"script\" : \"xxx.js\",  \u002F\u002F项目主入口（Node.js）\n\t\t\"env\": {\n\t\t\t\"COMMON_VARIABLE\": \"true\"\n\t\t},\n\t\t\"env_production\" : {\n\t\t\t\"NODE_ENV\": \"production\"\n\t\t}\n\t}],\n\t\"deploy\" : {\n\t\t\"production\" : {\n\t\t\t\"user\" : \"toobug\",\n\t\t\t\"host\" : \"server.toobug.net\",\n\t\t\t\"ref\"  : \"origin\u002Fmaster\", \u002F\u002F需要部署的分支\n\t\t\t\"repo\" : \"git@github.com:TooBug\u002Fxxx.git\",\n\t\t\t\"path\" : \"\u002Fvar\u002Fwww\u002Fxxx\", \u002F\u002Fweb目录\n\t\t\t\"post-deploy\" : \"npm install && pm2 startOrRestart ecosystem.json --env production\"\n\t\t}\n\t}\n}\n```\n\n需要注意：\n\n1. `apps.name`和`apps.script`应该与PM2识别应用有关，后续执行`pm2 restart`的时候可以对应到进程（未证实）\n2. `deploy`中可以含有多个环境，需要能够通过SSH（公钥认证）登录服务器\n3. web目录并不是真正的放版本库文件的目录，PM2会再建立一个`source`子目录，这个才是真正放代码的目录\n4. `post-deploy`是指代码部署完之后执行的命令，这里以Node.js为例子，执行依赖安装，然后重启PM2中的进程\n\n然后就可以使用\n\n```sh\npm2 deploy ecosystem.json production\n```\n\n自动发布网站项目了，非常方便。\n\n```sh\n$>pm2 deploy dev\n--> Deploying to production environment\n--> on host server.toobug.net\n  ○ deploying\n  ○ hook pre-deploy\n  ○ fetching updates\nFetching origin\n  ○ resetting HEAD to origin\u002Fmaster\nHEAD is now at eda2cdd xxx\n  ○ executing post-deploy npm install && pm2 startOrRestart ecosystem.json --env production\nmanpath: can't set the locale; make sure $LC_* and $LANG are correct\nNow using node v0.11.13\n[PM2] restartProcessId process id 0\n┌──────────┬────┬──────┬──────┬────────┬───────────┬────────┬─────────────┬──────────┐\n│ App name │ id │ mode │ PID  │ status │ restarted │ uptime │      memory │ watching │\n├──────────┼────┼──────┼──────┼────────┼───────────┼────────┼─────────────┼──────────┤\n│ xxx      │ 0  │ fork │ 7384 │ online │        33 │ 0s     │ 12.438 MB   │ disabled │\n└──────────┴────┴──────┴──────┴────────┴───────────┴────────┴─────────────┴──────────┘\n Use pm2 info \u003Cid|name> to get more details about an app\n  ○ hook test\n  ○ successfully deployed origin\u002Fmaster\n--> Success\n```\n\n在使用过程中还有几个值得注意的点：\n\n- 在部署过程中，PM2会执行一次`git reset --hard`，意味着如果你修改了配置文件之类的，会被还原，因此最好使用环境变量或者新建文件（不在管理库中）的方式来指定服务器专用的配置项（比如数据库连接信息等）\n- 执行服务器命令时需要关注环境变量，比如使用nvm来管理node版本的话，有可能导致PM2连接后找不到node（以及npm\u002Fpm2）所在路径，解决办法是在脚本最前面加上指定环境变量的脚本，例如`source ~\u002F.bashrc`\n\n## End\n\n就是这样，水文一篇，你要是当成PM2的广告读也行，主要是这个功能真的是很方便。尤其是有多个环境的话，几条命令就能搞定，再也不用登录服务器手工做一堆事情了。\n","\u002Farticle\u002Fusing_pm2_deploy.html",{"id":64,"type":7,"slug":65,"title":66,"date":67,"category":68,"tags":69,"body_markdown":72,"permalink":18,"excerpt_src":18,"media_type":73,"media_title":74,"media_author":75,"media_url":76,"rating":18,"layout":18,"pv":19,"admin_only":19,"created_at":20,"updated_at":20,"deleted_at":18,"status":21,"cover":77},15,"recommend-for-jiangxun-hongloumeng","推荐《蒋勋细说红楼梦》","2014-07-25 20:11","bmm",[70,71],"红楼梦","音频","\n![蒋勋细说红楼梦](\u002Fassets\u002Fbmm\u002F2014\u002Frecommend-for-jiangxun-hongloumeng\u002F01.jpeg)\n\n第一次读到《红楼梦》已是10年前，彼时刚上高中，抱着“要当一名好文学青年”的想法捧起了这本名著。\n\n必须得承认，红楼的前5回是很难读的，场景切换极多，人物极多，关系极复杂，即使沉下心来细看也得费很多心思。但当时抱着一定要看完的心理，竟也草草读下去了。\n\n如果你读过红楼，不知道你是否还记得那一僧一道，不知道你是否还知道有一个有一位后面常出现的女子小时候叫英莲，不知道你是否还能想得起来賈雨村是个什么样的性子。\n\n如果你有点模糊了，那么来听听这一版的解读吧。如果你还记得，那么你更该听听，因为听完你也可能会发现，原来你对前5回一无所知。\n\n\u003C!-- more -->\n\n还是说一说这版解读的来由吧。作者蒋勋，台湾著名画家、诗人、作家，在维基百科上略略一瞄，也能看得出算是位著作等身的人物了。这一系列的音频来自他在台湾某大学开设的课程录音，对红楼解读非常详尽，每一回都有上下两部分，加起来大约2小时左右。\n\n借用作者的话，红楼的写法是不着痕迹的，曹雪芹笔下从来不会说这个人物如何，那个人物又如何，他的笔下只有人物的生活。但是，作者对人物是有偏爱的，对事物是有态度的，这些偏爱和态度往往就在那么不经意的一两笔中透露出来，一不留神就会错过，因此，有人会觉得红楼读起来无味，而能悟出那一两笔中味道的人则会爱得无以复加。\n\n我属于前者，十年前初读红楼的时候，只觉得热闹，而十年之后再听到这个解读，才发现原来自己读过的只是故事，而非红楼。\n\n向你推荐这一套音频是一件思考了很久的事情。一方面想着在一个如此快节奏的社会去推荐一件慢慢品的东西是否不合时宜，一方面也在细细品味着音频中的话语，掂量着是否值得向大家推荐。直到我听完了第七回的解读，才发现这个解读是如此精彩，而更让我震惊的是，最最平淡的第七回竟然比其它的回目更有味道。如果你也有兴趣，去听听吧，不会失望的。再次借用作者的话，把第七回单独拿出来品也是件非常值得的事情。\n\n好了，不多说了，有时间的话，不妨点击下方链接一听，便知分晓。如果你使用手机上网，下载“荔枝FM”即可找到。\n\n[蒋勋细说红楼梦](https:\u002F\u002Fwww.lizhi.fm\u002Fuser\u002F1114221)\n","book","蒋勋细说红楼梦","蒋勋","https:\u002F\u002Fbook.douban.com\u002Fsubject\u002F4173156\u002F","https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Fbmm\u002F2014\u002Frecommend-for-jiangxun-hongloumeng\u002F01.jpeg",{"id":79,"type":7,"slug":80,"title":81,"date":82,"category":28,"tags":83,"body_markdown":85,"permalink":86,"excerpt_src":18,"media_type":18,"media_title":18,"media_author":18,"media_url":18,"rating":18,"layout":18,"pv":19,"admin_only":19,"created_at":20,"updated_at":20,"deleted_at":18,"status":21,"cover":87},117,"how_to_debug_efficiently","如何高效debug","2014-06-29 12:19",[84],"调试","\n> 这是一个公司内网上的问题，原意是题主觉得debug非常费时，影响了项目的效率，问如何改进。当时也在内网随手码了几点，回头又看了一遍觉得很有共性，可以再扩展一下单独写写，于是诞生此文。因为专业所限，本文的部分细节也只限于web前端，但思路和其它语言是相通的。\n\n首先，程序员要调整好心态。\n\n事实上，debug是程序员工作的重要组成部分，甚至在产品的某些阶段是唯一的组成部分，所以不用把调bug看成是拖累产品进度、降低效率的凶手，这是一件必须要花时间认真去做的事情，它就是你的工作，也是整个项目进度过程中必不可少的组成部分。所以在debug的时候也可以更加平心静气一些，不用那么急躁。\n\n当然，这个心态的改变也会涉及到项目进度安排上的一些调整，比如在排期的时候就要预估debug的时间，而不能仅仅只安排编码的时间。此外，还需要给自己做好时间管理，尽量能排比较大段的不被打扰的时间来debug。\n\n把心态放平之后，有的bug可能解决起来真的就没那么耗时了。在心态平和、思路清晰、无人打扰的情况下，一般修起bug来都有如神助，效率高得连自己都不太敢相信。相反，往往越是着急的时候越是难定位到bug的真正原因。\n\n\u003C!-- more -->\n\n然后说提高调试效率的一些办法：\n\n## 练好基础\n\n这个不论对开发还是调试效率都有决定性影响，一旦基础扎实了，在编写代码的过程中就能避免非常多的bug，在调试过程中也很敏锐地察觉到异常之处。\n\n这里所指的基础其实包含了两个部分：一部分是编程语言的基础知识，只有掌握好才能知道一个问题如何用最低成本、最正统、最能扛住需求变更、最不容易出问题的方式写出来。另一个部分则是良好的编码习惯，基本上各个语言都有各家总结的一些编码规范和习惯，并且也会解释为什么建议这么写，如果掌握好的编码习惯，一般而言可以避免非常多编码细节导致的bug。\n\n比如，关于数组去重，如果你去网上随便找一段代码，很有可能找到利用对象的key来去重的代码：\n\n```javascript\nArray.prototype.delRepeat=function(){\n\tvar newArray=[];\n\tvar provisionalTable = {};\n\tfor (var i = 0, item; (item= this[i]) != null; i++) {\n\t\tif (!provisionalTable[item]) {\n\t\t\tnewArray.push(item);\n\t\t\tprovisionalTable[item] = true;\n\t\t}\n\t}\n\treturn newArray;\n};\n```\n\n如果你直接去用的话一般不会有问题，但如果你的数组是`[1,'1']`就会发现这个方法秀逗了，根本原因是数组的key必须是字符串，`1`在被当作key时会被转成`'1'`，导致二者无法区分。如果你不了解对象的相关机制，就很难发现这个bug。\n\n再比如，如果你习惯花括号另起一行，那么在函数中返回一个对象就很容易写成这样：\n\n```javascript\nfunction A()\n{\n\treturn\n\t{};\n}\n```\n\n但事实上由于JavaScript的分号补全机制，这里最后返回的是`undefined`，而如果将花括号跟在`return`后面则不会存在问题。这就是典型的编码习惯可以避免的bug。\n\n练好基础的工夫必然是在工作之外的时间，因为基本上工作时间是满负荷输出，不太有时间去接触新东西，如果你每行代码都需要先Google一下再写，那想必你的老板也会不乐意。所以给自己充电的时间就只能是在工作之外，这是件非常考验人的事情。\n\n## 用好工具\n\n工具包括三个方面：\n\n### 质量工具\n\n质量工具对于前端来说是特别必要的，甚至比其它语言更必要，因为HTML、CSS、JS都是没有语法检查没有编译过程的，基本上编写完就直接丢上线了。所以为了保证代码质量，我们可以引入一些检查工具来辅助控制代码质量。\n\n至于具体的工具，可能是IDE，可能是命令行下的各种lint，也可能是集成构建过程中的语法检查过程，还可能是在线的语法检查器等，方法不一而足，但做的事情都是差不多的。\n\nHTML检查的话主要是W3C的标准验证（\u003Chttp:\u002F\u002Fvalidator.w3.org\u002F>），也有各种牛人编写的HTML Lint之类的工具。\n\nCSS检查的话主要是CSSLint，此外不规范（无效）的属性在Chrome开发者工具的console中会显示警告。\n\nJS检查的话主要是JSLint\u002FJSHint。\n\n此外还有一些针对特殊文件、语法而做的工具，比如用于检查json语法的jsonlint之类，种类繁多，不一一列出。\n\n以上这些工具基本都有对应的grunt、gulp插件，也有IDE插件，可以直接集成到编码过程或者构建过程中。\n\n质量工具除了语法检查类的之外，还有一类是属于“规范”类的，即用来统一项目的编码规范，比如editorconfig，建议每个项目都能引入，它可以更好地控制项目的缩进、换行符、编码之类的规范。\n\n### 调试工具\n\n其实调试工具不仅仅是一个工具，要用好的话还得掌握调试方法。这一节的核心思想就只有一句话“代码调试真的要靠调试，而不能靠猜”。见过非常多的前端同学，在某个逻辑与预期不一致的时候基本都是对着源代码猜，是不是这个地方单词写错了？是不是这里循环少了一次？是不是后台返回的数据是空的？然后用各种奇葩的方法一遍遍地改代码，直到终于让结果符合预期了，才算“调试”完毕了。\n\n怎么样才算是“调试”呢？那就是你可以看到你写的每一句代码最终跑起来是什么样的。具体而言，HTML主要检查生成的DOM树结构，CSS主要看样式的层叠关系及最后应用的样式来自哪里，JS主要看代码每一行的结果和预期是否相符。\n\n以Chrome dev tool为例，看HTML的问题主要在Elements面板，看CSS的问题主要也在Elements面板（有时候也需要在Resource面板），看JS的问题则主要在Source面板。\n\n比如两句简单的代码：\n\n```javascript\nvar words = location.hash;\n$('#test').append(words);\n```\n\n如果页面上没有出现你想要的文字，该怎么样？很多人这个时候就开始猜了，是不是`location.hash`兼容有问题？是不是jQuery没引入？是不是`$`被覆盖了？是不是不支持取ID？是不是`append`用法不对？\n\n很有可能你拿调试工具断一下点，看看每个表达式的结果，你就会发现，其实原因是`$('#test')`取不到。然后恍然大悟，原来没有把DOM操作放在DOM Ready中！\n\n因为不是调试工具教程，所以细节不多说，放个截图，我说的就是这一堆。\n\n![调试工具截图](\u002Fassets\u002Fhow_to_debug_efficiently\u002F1.png)\n\n另外强烈建议将画圈的按钮点一下让它高亮，这样可以在代码报错之前立即自动断点下来，避免因为报错而导致页面刷新、表单提交之类的动作，使得调试变得困难。\n\n> 如果看不懂上面一堆在说什么，那么这一段的目标读者就是你了，你需要好好学习一下前端调试方法和工具的使用了。\n\n与上面说的类似，移动端的调试也有很多方案，比如weinre、MIH Tools之类，iOS和Android也有各自的USB调试方案。\n\n多多掌握这些调试方法和工具能够真正在你想找问题的时候事半功倍。\n\n### 生成工具\n\n有时候我们的代码中还会有一些假数据或者写死的数据，或者是一些根据数据生成的代码（比如拼接HTML），或者是一些模板化的代码，此时如果借助工具来生成数据或者代码，会比手工编写靠谱得多，也可以避免很多错误产生。\n\n比如\u003Chttp:\u002F\u002Fwww.json-generator.com\u002F>、\u003Chttp:\u002F\u002Fshancarter.github.io\u002Fmr-data-converter\u002F>、\u003Chttp:\u002F\u002Fwww.colorzilla.com\u002Fgradient-editor\u002F>、\u003Chttp:\u002F\u002Fmatthewlein.com\u002Fceaser\u002F>都是非常好的生成工具。\n\n## 用好搜索\n\n如果在调试之后定位到某个很诡异的地方，实在无法解释或者找不到原因的话，不妨搜索一下看看，一般这种诡异的问题都已经有前人碰到了，在stackoverflow或者是开源代码的官方讨论区基本上都能找到答案，绝大部分也可以找到规避方法或者替代方案。\n\n这里值得注意的是，你仍然需要先调试再搜索，因为在你调试之前，你对问题的描述只能是“现象”，但是调试之后一般可以定位到某个具体的代码或者对象或者API，去搜索对象或者API的行为比搜索“现象”得到的答案往往会更接近真相。\n\n## 多总结\n\n每调完一个重大bug就会有比较大的成就感，一般这也是进行总结的最佳时机。\n\n每个程序员都应该有总结bug和原因的习惯，比如为什么会造成这个bug，如果是编码习惯的问题能否规避，能否利用工具在编码阶段就解决掉？如果是逻辑上的问题，能否推给产品、交互去重新梳理，避免类似问题？\n\n总结之后最好还要有提炼和分享，比如bug是由于不为人知的特性或者API导致的，能否对与之相关的任务进行封装，或者能否将这个坑埋掉，然后共享给组员让大家避开这个坑，甚至回馈给组件作者让更多人避免这些坑？如果能将你的成果分享给大家，就避免其它人再掉入同一个坑，而如果将这个坑埋掉，就可以让地球上的程序员永远不再掉进这个坑，想想都是一件很幸福的事情。\n","\u002Farticle\u002Fhow_to_debug_efficiently.html","https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Fhow_to_debug_efficiently\u002F1.png",{"id":89,"type":7,"slug":90,"title":91,"date":92,"category":68,"tags":93,"body_markdown":96,"permalink":18,"excerpt_src":18,"media_type":73,"media_title":97,"media_author":98,"media_url":99,"rating":18,"layout":18,"pv":19,"admin_only":19,"created_at":20,"updated_at":20,"deleted_at":18,"status":21,"cover":100},14,"comments-on-are-you-happy","《幸福了吗？》简评","2014-05-14 12:47",[94,95],"观后感","书","\n![幸福了吗？](\u002Fassets\u002Flife\u002F2014\u002Fcomments-on-are-you-happy\u002F01.jpg)\n\n和十年前的《痛并快乐着》完全是同一种风格，那些不为人知的故事可以满足人猎奇的需要，但除此之外，能给人启示的地方实在太少。\n\n总体来看，和前一本一样，文字仍然显得浮夸，很多类似中学生一叶知秋的感慨，基本是一段不太搭调的引子开头，一段三言两语的故事，生发出一大 堆深度并不太够的感慨。于读者而言，无文字美感可看，无精彩故事可读，无深刻见地可感。\n\n在选材上，无意中仍然会流露出作为名人的盲目自信，以至于对一些自己并不太了解的领域大加评论，并以一句你可以有不同见解的自由加以开脱。而在最见真情的感谢部分，诗人大头，浮夸的老师随后，再一段音乐家，戛然而止，没有同事没有家人没有朋友，足见诚意不足。\n\n为这本书打三星，为那些不为人知的内幕，仅此而已。\n\n\u003C!-- more -->\n\nP.S.\n\n看完这本书的时候飞机延误，正在深圳机场候机，看完后就随手用手机打下了上面的文字。\n\n总体来说，我还是欣赏白岩松的，能力自不必说，除此之外有见地，有坚持也是颇为难得可贵的。在这本《幸福了吗？》以及前作《痛并快乐着》中，可以见到一个媒体人的坚持、妥协和成长，这便是这类书的意义所在。至于其它，倒真没有多少可圈可点之处。\n\n最后，同为媒体人，同为央视评论部成员，柴静在去年也发布了一本书，叫《看见》，相较之下，柴静的书可读性就要高很多。之前也有推送过一篇读后感，有兴趣的朋友可以回复“WN”获取阅读。\n\n最后的最后，最近在看孟非的《随遇而安》，还没看完，感觉不错，推荐。后面有机会再为大家细细介绍。\n","幸福了吗？","白岩松","https:\u002F\u002Fbook.douban.com\u002Fsubject\u002F5252677\u002F","https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Flife\u002F2014\u002Fcomments-on-are-you-happy\u002F01.jpg",{"id":102,"type":7,"slug":103,"title":104,"date":105,"category":11,"tags":106,"body_markdown":110,"permalink":111,"excerpt_src":18,"media_type":18,"media_title":18,"media_author":18,"media_url":18,"rating":18,"layout":18,"pv":19,"admin_only":19,"created_at":20,"updated_at":20,"deleted_at":18,"status":21,"cover":112},162,"css_image_sprites_on_retina_screen","Retina屏下的CSS雪碧图","2014-03-19 13:39",[107,108,109],"CSS","雪碧图","背景图","\nCSS雪碧图早已经成为前端知识体系中一个必备知识了，时至今日，可能很多人都觉得这一块已经没有什么东西可以再讲了的。但事实上雪碧图一直都可以引出新的话题，比如从最早的连接数和体积的平衡到格式之争到图像摆放位置的策略，再到合并图像的颗粒度，再到内存占用、CPU占用等性能问题……\n\n没错，今天还要在这一古老的话题上展开，引入一个新的问题，那就是雪碧图在retina屏下存在的问题及应对方案。（值得注意的是，retina屏一般指分辨率为普通屏幕两倍的屏，这样按照普通尺寸开发出来的网站相当于被放大了2倍，会导致图像模糊之类的现象产生，理想的解决方案是为retina专门适配一套皮肤，但本文关注的问题是未适配retina屏幕的网站所出现的问题。）\n\n> 雪碧图本身不是浏览器或者web标准中的技术，因此它的不少细节取决于浏览器的实现，本文中的讨论的内容正是如此，为避免争议，本文所有结论的得出场景限定为Mac OSX 10.9.1、Chrome浏览器V33。是否适用iPhone、iPad等场景未做相应测试。\n\n## 无处不在的白边\n\n如前文所述，在retina屏上浏览未做专门适配的网站，会出现图像模糊等问题，无法达到最佳效果，但一般情况下，仍然处于可以接受的范围。不过，在某些网站上，却出现了比图像模糊更糟糕的情况：\n\n![WebQQ在retina屏下出现白边](\u002Fassets\u002Fcss_image_sprites_on_retina_screen\u002F1.png)\n\n\u003C!-- more -->\n\n![财付通首页菜单在retina屏下出现白边](\u002Fassets\u002Fcss_image_sprites_on_retina_screen\u002F2.png)\n\n![支付宝的按钮在retina屏下出现白边](\u002Fassets\u002Fcss_image_sprites_on_retina_screen\u002F3.png)\n\n从上面三张图看到，不少的互联网产品在retina屏下都出现了白边。那么这些白边出现的原因是什么呢？通过查看这些出问题的页面，发现存在一个共同点，那就是这些白边所在的地方都使用了背景图，而且都是使用雪碧图合并的。于是问题就浮现出来了，正是由于雪碧图在retina屏上的放大导致了白边的产生。更为技术化的表达则是，**图片放大过程中进行了插值运算，导致原来整齐的图片边界混入了插值后模糊的像素**，从而导致原来整齐的边界处出现“白边”。\n\n打开WebQQ的雪碧图（\u003Chttp:\u002F\u002F0.web.qstatic.com\u002Fwebqqpic\u002Fpubapps\u002F0\u002F50\u002Fimages\u002Feqq_sprite.gif?t=20111011001>），就可以验证这一结论。\n\n![WebQQ雪碧图](\u002Fassets\u002Fcss_image_sprites_on_retina_screen\u002F4.gif)\n\n如果您在阅读本文时刚好使用的retina屏幕，可能已经能看到图标边上的白边了，为了统一说明，特放上图像编辑软件中局部放大的图片。\n\n![WebQQ雪碧图局部放大](\u002Fassets\u002Fcss_image_sprites_on_retina_screen\u002F5.png)\n\n图中可以看到，边缘是非常整齐的，但我们在retina屏上截到的图却是这样：\n\n![WebQQ雪碧图局部放大](\u002Fassets\u002Fcss_image_sprites_on_retina_screen\u002F6.png)\n\n可以看到，由于retina屏下，图片被强制放大，导致了原本整齐的边界不再整齐，从而使得页面上出现“白边”。\n\n## 真的是因为雪碧图吗\n\n至此，我们已经推断出白边是因为图片被放大而导致，那么这跟雪碧图有关系吗？如果不用雪碧图会出现这样的现象么？为了验证这个结论，本文曾一度中断，最终还是拿到了比较令人信服的结果。\n\n首先，我们准备一张如下的图片：\n\n![实验用图1](\u002Fassets\u002Fcss_image_sprites_on_retina_screen\u002F7.png)\n\n这张图放了四个色块，其中左上和右下的色块有留白。接下来我们在浏览器中打开它，结果如下：\n\n![实验用图1](\u002Fassets\u002Fcss_image_sprites_on_retina_screen\u002F8.png)\n\n可以看到，图片中有颜色交界的地方都有插值运算而导致模糊，但边缘却是清晰的！也就是说，如果没有拼图的话，浏览器是可以处理好图片的边缘的。为了保险起见，接下来又做了一个实验，准备了一张20*20的纯红色图片，并与CSS写的红色背景进行混合，看看是否有“白边”出现。\n\n![实验用图2](\u002Fassets\u002Fcss_image_sprites_on_retina_screen\u002F9.gif)\n\n可以看到，在CSS背景色不断变化的过程中，图片与背景可以完全融合，没有任何奇怪的现象出现。\n\n至此，我们终于判定，导致“白边”的原因就是因为雪碧图中不同图像之间在拼合后产生了插值而导致边缘部分模糊。\n\n## 解决之道\n\n知道了原因就好解决了，既然白边的出现是因为插值，并且这个插值行为不可控，那就只好将插值的部分移出视野之外了。讲人话就是：切图的时候多留点“出血”。\n\n继续拿WebQQ为例，如上面所说，原文中聊天气泡图标的背景宽度是20px，两边各加1px，总共22px，效果如下：\n\n![WebQQ图标改进1](\u002Fassets\u002Fcss_image_sprites_on_retina_screen\u002F10.png)\n\n![WebQQ图标改进1效果](\u002Fassets\u002Fcss_image_sprites_on_retina_screen\u002F11.png)\n\n可以看到白边已经减少了不少，但仍然存在。继续在两边各加1px，总共24px，效果如下：\n\n![WebQQ图标改进2](\u002Fassets\u002Fcss_image_sprites_on_retina_screen\u002F12.png)\n\n![WebQQ图标改进2效果](\u002Fassets\u002Fcss_image_sprites_on_retina_screen\u002F13.png)\n\n至此，问题完美解决，结论是：\n\n**切图时请为图标在各个方向上多留2px空间**，即可保证retina屏下不出现意料之外的毛边（白边）。\n\n## The End?\n\n这就完了？当然没有，还有另外一类案例解决不了，就是开头提到的支付宝的按钮。如果你不记得了，没关系，我再放一次图：\n\n![支付宝的按钮在retina屏下出现白边](\u002Fassets\u002Fcss_image_sprites_on_retina_screen\u002F3.png)\n\n看一下它的雪碧图(\u003Chttps:\u002F\u002Fi.alipayobjects.com\u002Fe\u002F201204\u002F2vCVR5Bh4d.png>)和结构：\n\n![支付宝按钮雪碧图](\u002Fassets\u002Fcss_image_sprites_on_retina_screen\u002F14.png)\n\n![支付宝按钮结构](\u002Fassets\u002Fcss_image_sprites_on_retina_screen\u002F15.png)\n\n可以看出，这个按钮其实是由左边两边拼合而成（分别由内外两层元素组成），白边来自右边的结构。如果把左边的背景屏蔽掉，会看得更清楚：\n\n![支付宝按钮结构](\u002Fassets\u002Fcss_image_sprites_on_retina_screen\u002F16.png)\n\n这种情况下，背景图小于容器本身，因此无法将插值部分排除到视野外，也即上面说的多留2px也无法解决。（事实上左边已经留有N像素了……）那就只好再利用上面在验证是否是雪碧图才有问题时说的另外一个结论了：边缘部分是不会被插值的。\n\n于是，将这个雪碧图需要插值的部分改到边缘去，如下图（只改了上面的几个）：\n\n![支付宝雪碧图修改版](\u002Fassets\u002Fcss_image_sprites_on_retina_screen\u002F17.png)\n\n效果如下：\n\n![支付宝雪碧图修改版效果](\u002Fassets\u002Fcss_image_sprites_on_retina_screen\u002F18.png)\n\n至此问题解决。\n\n> 注：之所以会采用这样一种结构的按钮，是因为它可以根据文字长度进行自适应，`background-position`的`x`值取`right`即可保证雪碧图是始终靠按钮右边对齐的。而修改版中改变雪碧图结构后，则需要手工指定`background-position`的`x`值。\n\n> 一种更好的解决方案则是直接使用CSS3来写按钮，在IE下进行降级。\n\n## 结\n\n这应该是博客中图片最多的一篇了，关注的也是一个非常非常非常小的点，起因只是因为支付宝的按钮在我发现这个问题一年后仍然没有改过，于是忍不住研究了一下这问题到底有多难解决。\n\n最后，根据上述实验和推断过程小结一下在应用雪碧图的过程中值得注意的点：\n\n1. 雪碧图中请给背景留出足够的空间（出血），否则可能导致retina屏下产生毛边（白边）\n2. 雪碧图如果图标排得太过密集，可能导致retina屏下出现“窜色”（与毛边一样，是由于插值导致）\n3. 如果插值区域无法避免，请将它放在图片边缘位置\n\n最后的最后，一句题外话，以上所有现象均可在部分浏览器放大页面时出现（我忘记是什么浏览器了，曾有项目因此被产品经理报bug，最终将所有图标周围留了2px空白解决）。\n","\u002Farticle\u002Fcss_image_sprites_on_retina_screen.html","https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Fcss_image_sprites_on_retina_screen\u002F1.png",{"id":114,"type":7,"slug":115,"title":116,"date":117,"category":48,"tags":118,"body_markdown":119,"permalink":18,"excerpt_src":18,"media_type":18,"media_title":18,"media_author":18,"media_url":18,"rating":18,"layout":18,"pv":19,"admin_only":19,"created_at":20,"updated_at":20,"deleted_at":18,"status":21,"cover":22},78,"my-2013","我的2013","2014-01-01 20:52:18",[50],"\n今年年末的日子，跟去年比起来，算是悠闲得多了，所以其实这篇总结文章已经在我的心里打过无数遍腹稿了，每当我回想起这一年的一些片段，就总想着把它写进总结里去。\n\n## 生活篇\n\n往年的总结，都会先写技术篇，因为总觉得技术对于我而言，是一个直接反映过去一年状态的东西。但今年，我想先写生活，因为慢慢地开始觉得，技术和工作不能成为生活的全部，有生活才是真的在生活着。\n\n这一年，总体上比较平淡，尤其是心态上，几乎没有什么事情能让我在回想的时候立刻跳上心头来。但仔细一点一点去搜寻的时候，发现这一年可说的事情还实在是不少。\n\n2013年的第一大关键字是压力，而压力的来源，不能免俗——结婚和买房。这种压力在下半年得到了比较集中的爆发，在某一个阶段，甚至让我手足无措。在我决定不读研的时候，就打定了“将公司前三年工作当成读研”的心态，事实上，算上实习期的话，工作也已有三年，我也确实是一直抱着学习的心态，并且自认为学习得还算不错，至少可以给这三年的经历一个圆满的毕业。但当我按着计划，一脚一脚地往前迈的时候，却猛然发现似乎身边的人都已经不再等我了，我觉得我才刚毕业，他们觉得我可以顶天立地了。好在，我已经养成了独自做决定的习惯，不管怎样，该顶的还是得顶一阵，也好在，我有一个和我抱有同样理想的女朋友，我们可以一起规划未来的生活，而不必特别被他人牵制。不过，可以预见，2014会更难，压力会更大，事情会更多，必须得有更详细的规划和更大的信心。\n\n在过去的这一年，我也在努力地做些改变，不断尝试新的事物和生活方式。\n\n首先值得一提的是理财。对于这个话题，我以前从来都是不屑一顾的，总觉得这件事情离我好远。后来跟同事细致地交流了一下，发现了解一下总还是有好处的，因此过去的一年里，我也有不系统地了解过基金、股票（港股）、短期理财、P2P网贷等各种不同的理财渠道。不过，最终的最终，还是选择将不多的余钱丢入了余额宝，跟短期理财不相上下的收益率以及T+0的资金流动周期让人无法拒绝。不过，话说回来，我也真的只是接触了下理财而已，回头看下账本，一贫如洗，这其中有各种各样的原因，但收获的理财知识让我觉得还是大有好处。\n\n第二件值得一提的事情就是为自己更换了笔记本电脑，虽然相对其它电脑来说，MacbookPro不能算便宜，但从综合素质考虑，这台笔记本仍然是非常有吸引力的。通过这一年的实际使用来看，完全不为当时的冲动而后悔。作为一款生产工具，它真的是好用到家了，再也不用花时间去陪电脑开机关机以及没电和死机，我只需要关注我的生产过程就好了。即便不做生产工具，作为日常使用，也常常带给我不少惊喜。（所以各位亲们不要再问我推荐什么电脑了，我的回答一定是Macbook的。）\n\n除了电脑之外，剩下的一件大家电就是电视啦。曾经我并不主张在家里放电视，一来觉得电视的观感太差，看电影什么的很鸡肋，二来觉得工作外的时间应该多拿来为自己充电，太过娱乐化并不是一件很有益处的事情。不过，在女朋友的妈妈在我们房子里试过了一个多月没有电视的生活后，我终于开始觉得没有电视并不是一件值得自豪的事情了。在买了电视之后，我们两个人下班后的时间基本上都会坐在电视前，虽然有的时候是看电视，有的时候是当广播听，还有的时候只是为了显得不那么冷清，但至少感觉并没有那么差，以至于我都已经回想不起来在没有电视的时候，我们下班后是怎样的一种状态了。今天看了一篇电视的评测，结尾处有一个案例，说是问老爸老妈为什么不愿意用电视盒看电视剧，而是要蹲点看，结果老妈的回答大出所料：“我和你爸只是为了在吃完饭刷完碗筷后，能坐下来，一起聊聊电视剧，至于放的什么，都没关系。”一瞬间，我就明白了我妈说的“没有电视不像个家”的含义了，也就觉得，因为有了这台电视，我们下班后还挺像个家的。\n\n2013年，我们之前定下的旅游计划真正开始启动。\n\n4月份，我们抱着了解一下这个从来默默无闻的地方的想法，去了澳门。结果既兴奋又失望，兴奋是因为终于到了这个小地方，看到了它与众不同的样子，并且也见识了它真的很小，而失望则是因为它的与众不同的样子并不是我们所向往的。给我的感觉，澳门无比依赖以赌博为首的娱乐业，以至于连公共交通都是由各个酒店来承载，俨然一个被娱乐业包场的城市，而除了娱乐业之外，澳门几乎没有任何产业，甚至连文化都没有，很难让人再次产生欲望。\n\n8月底9月初，我们来到了长三角，五天四夜的行程，奔走了上海、苏州、乌镇、杭州四个地方。行程很赶，也很累，但很享受。上海很有底蕴，也很发达；苏州很别致，很有小家碧玉的气质；乌镇则有些过度开发、名不副实；杭州西湖很美，但也开始遭遇空气质量、道路拥堵等问题。总体而言，这次旅行还是惊喜颇多，但除了城市之外，我觉得最美好的部分其实在于我们两个人第一次携手走那么远那么久，每天都要安排好行程并抓紧时间赶路，每时每刻都要看方向看地图，每分每秒都要防车防人防小偷，那种相互依赖相互信任的感觉，如果不出去走走真的是很难体验得这么深刻。\n\n在去年的总结中，我说希望对音乐好一点，于是在2013年，我找到了虾米音乐。在听了半年多之后，不得不说，这个选择是正确的。虾米的乐库大得惊人，这一点仅凭它的音乐分类之专业就可见一斑，事实也是如此，虾米能把我的个人喜好猜得八九不离十，时不时推荐一些不错的音乐过来，让我觉得很满足。虽然有些音乐也不是特别满意，但相对豆瓣到最后只会放《喜洋洋》、QQ音乐猜来猜去永远十来首，虾米已经给我足够多的惊喜了。2014，希望能听到更多的好音乐，更希望自己能对音乐玩得更深入一些。\n\n去年的总结中，我也写过希望对文字好一点，于是在2013年，努力在博客中留下了4篇文章，并通过蝶恋花的微信（qiudielianhua）推送了20多篇文章。于我而言，我觉得这是一个不错的结果，虽然在年末的时候有些懒散，但至少回忆起来，总还是有些抓得住的东西。2014，希望更进一步。\n\n读书自然是不能少的一个总结项目，这一年，买回家里书架的书不多，但读的书并不少，因为发现了豆瓣阅读这个平台，有些书就直接在豆瓣上购买了电子版。有不少的书让我心生感慨，尤其是年末读到的《迟到的间隙年》，真的勾起了我心底的欲望，很想出去走走，看看外面的世界。\n\n也还有些值得纪念的事情可以一带而过，比如尝试更多在家做饭，比如买了自行车，比如偶尔去体育公园散散步等等，总之这些都是在过去的一年里对生活模式的一些探索。生活方式总是没有对错的，但多一些尝试可能会发现多一些的精彩，总体而言，2013的尝试我觉得还是挺出彩的，希望2014年有更好的表现！\n\n最后必须提一下的就是十一又回到了武汉这件事，感触也是两方面的：\n\n一方面是关于武汉。在毕业的时候，我的信念是很坚定的，一定不要留在武汉，因为我真的受不了这个城市的天气和交通。但这次十一回去之后，发现改观还是不小，公交更规范了，地铁也有了，街道也显得更干净了，堵点也在慢慢梳理改造。虽不尽满意，但至少改变是看得见的，这短短两年多时间的变化也让我对武汉有了新的认知，也许未来的某一天，我也会喜欢上武汉，谁知道呢？\n\n另一方面则是关于民乐团。虽然毕业已近三年，但对民乐团的牵挂却一丝一毫没有减少。这次回武汉自然也少不了去团里转转，却遗憾地发现除了指挥和极少的研究生外，已然全是生面孔。我就像个过客一样，突兀地出现在他们的排练厅，偶尔还非常关注地望一望这个音出自谁之手，最后一言不发，莫名其妙地走掉。虽然已有心理准备，但真的碰到，还是不免会心生寒意，属于我们的时代已经一去不返了，甚至连跟我们有关系的时代都已经不在了。偶尔在群里冒下泡，我觉得孩子们应该跟看外星人一样没有概念吧，07级的学长突然出现在13级的群里，差了6年，而民乐团一共才12年的历史。我终于意识到，这个团体其实已经跟我无关了，他们有新的团干，甚至执行力比我们还强，他们有新的团员，朝气蓬勃，他们有新的乐队，意气风发。而我的那些心结，除了释然，别无他法。唯一欣慰的是，团委的老师还在，一起吃的那顿晚饭让我倍感温暖。\n\n## 技术篇\n\n这句话在每一次的腹稿中都会出现，现在终于可以写出来了：“2013年，我对时间的利用可以说是分秒必争，没浪费过哪怕一小时的时间。”虽略显夸张，但除去年末的一点时间外，基本状态就是这样。\n\n在去年总结的时候，我为自己定了一个感性的目标——爆发，意指结束前两年韬光养晦、厚积却不薄发的状态，在新一年输出一些东西。现在回头来看过去的一年，也的确是收获颇丰：\n\n- 翻译完《JavaScript Patterns》，虽有狗尾续貂之嫌，但仍然无法阻止我对它的喜爱，这个项目差不多经历了整整一年，也是我人生中参与翻译的第一本书\n- TooSolo，基于Node.js的静态博客系统，虽然还未整理完发布，但已经在不同的场合下开始使用了\n- TooDiff，文本文件对比工具，初衷是为了挑战下自己，看下这一块的水到底有多深\n- Shadow DatePicker，使用shadow dom实现的日历选择器，为掌握演示shadow dom我而做\n- TooZip，Node.js压缩zip文件用的库\n- JLint，windows下不依赖Node.js运行jsLint的sublime text插件\n- CompactExpandCSS，CSS书写格式化sublime text插件\n- Kapok，Grunt.js可视化工具，虽然还在alpha，但已经可以运行\n- 113.im系列，包括todo.113.im待办事项管理、url.113.im短链接、note.113.im便签等\n- lesscss.net，全面接管了LESSCSS中国官网的维护更新\n- gruntjs.org，对Grunt.js中国社区提供指导\n- 博客，一共输出6篇文章（内网还有一篇）\n\n除此之外，在技术方面，虽然没有专门去系统学习，但仍然掌握了不少新的技术，值得一提。\n\n首先是对git有了更深的了解，掌握了gitflow的用法并摸索出在公司SVN环境下适合个人的版本管理方式。这让我对开源界的版本管理方式有了更深入的了解，也显著提升了个人的工作效率。\n\n其次是对Node.js这个大红大紫的平台有了更深入的了解，并开始真实运营一些基于Node的网站。同时Node生态圈中相关的东西比如node-webkit也有了一些了解，非常有助于扩大技术的适用场景，能更合理地选用不同技术去实现需求。\n\n接下来有一个重头戏，是基于Node的构建工具——Grunt.js。Grunt的出现可以说是前端工程化的一个标志性事件，在可预见的未来，前端工程化会越来越专业，而Grunt所发挥的作用也会越来越重要。所幸，我算是走在了时代的较前列的位置，较早地在团队中引入了Grunt，并鼓励同事设立了Grunt中文社区，现在回想起来，这些事情显得非常有意义。\n\n除了本职工作以外，还有两个小小的不务正业的东西。\n\n一个是以arduino作为代表的开源硬件。这一年接触了非常多的开源硬件相关的东西，刚开始觉得非常有意思，但随着时间的推移，热情也有些减退，尤其是在可穿戴设备大热，而我们做的实验性的装备因为没有办法自己开模或者做外观设计而体积臃肿时，就会觉得这样小打小闹总还是没有前途。或者未来有一天，我会自己部署一套智能家居，但用这个做可穿戴设备，总还是显得太悬乎了。\n\n另一个则是设计。下半年因为时间相对较多，开始慢慢尝试自己为产品做些设计稿，后来发现其实用心琢磨的话，做一个不那么“程序员风格”的设计并不是一件特别难的事情。这件事情让我非常惊喜，也让我对以后独立做产品有了更多的信心，\n\n最后，特别想说一下的是，我在做民乐团的新版网站的时候，才深切地体会到，移动互联网的大潮已势不可挡，拥抱移动端真的是大势所趋。希望2014，能有所突破。\n\n坦白说，在年末这一个时刻，去回想的话会发现2013的确是碌碌，但却不能说是有为，有非常多的输出离理想的状态差得很远。同时，2013这种状态也多少会让人觉得有些疲惫，很希望在2014年聚焦一下方向，再交出一份漂亮的答卷。\n\n## 工作\n\n是的，最后总还是免不了要谈谈工作的。\n\n这一年，也许只有两件事比较大，一件是我所在的部门被拆掉了，另一件是则是第一次经历了需要答辩的职级晋升。\n\n部门被拆掉的同时原来的老大也出走了，这一系列变化带来的直接后果就是我变得悠闲了。很难说这于我是好是坏，可以说我品味了半年仍然还没回过神来。未来会怎样，很难说，未来我会怎样，也很难说。\n\n答辩这件事于我而言并不新鲜，但这次职级晋升的答辩给我的感觉并不算好，原因主要在于我虽挂着设计师的名，但做的是工程师的实，答辩时让一个工程师从设计师的角度来讲，多少有些奇怪。关于未来究竟要如何走，我也没有看清楚。\n\n正是这样两件事情，让我觉得很迷茫，所以把工作的总结也留到了最后，因为不知道说什么。\n\n## 展望\n\n好了，就到这里吧，2014年注定会发生很多事情，会经历很多状况，会面临很多选择，我现在无法预知怎样才是最好的选择，怎样才有最好的结果，唯有告诫自己：勿忘初心。\n\n2014，加油！\n",{"id":121,"type":7,"slug":122,"title":123,"date":124,"category":11,"tags":125,"body_markdown":131,"permalink":132,"excerpt_src":18,"media_type":18,"media_title":18,"media_author":18,"media_url":18,"rating":18,"layout":18,"pv":19,"admin_only":19,"created_at":20,"updated_at":20,"deleted_at":18,"status":21,"cover":22},161,"learning_es6_generator","学习ES6生成器（Generator）","2013-12-29 13:35:00",[126,127,128,129,130],"ES2015","Generator","生成器","回调","异步","\n这几天，TJ大神的koa框架突然在国内火起来了，随之而来的，则是其使用的ES6生成器（Generator）引起了广大码农的强烈兴趣，各种文章也如雨后春笋般拔地而起，比如[这篇](https:\u002F\u002Fwww.imququ.com\u002Fpost\u002Fgenerator-function-in-es6.html)、[这篇](http:\u002F\u002Fbg.biedalian.com\u002F2013\u002F12\u002F21\u002Fharmony-generator.html)、还有[这篇](https:\u002F\u002Fdeveloper.mozilla.org\u002Fzh-CN\u002Fdocs\u002FJavaScript\u002FGuide\u002FIterators_and_Generators)。这个神奇的生成器被视为解决JS“回调恶魔金字塔”的利器。在动手实践之后，发现介绍ES6生成器的文章仍然有些疏漏，因此有了这篇文章，权当是对各位大大们的补充好了。\n\n## 背景\n\n在JS的使用场景中，异步操作的处理是一个不可回避的问题，如果不做任何抽象、组织，只是“跟着感觉走”，那么面对“按顺序发起3个ajax请求”的需求，很容易就能写出如下代码（假设已引入jQuery）：\n\n```javascript\n\u002F\u002F 第1个ajax请求\n$.ajax({\n  url:'http:\u002F\u002Fecho.113.im',\n  dateType:'json',\n  type:'get',\n  data:{\n    data:JSON.stringify({status:1,data:'hello world'}),\n    type:'json',\n    timeout:1000\n  },\n  success:function(data){\n    if(data.status === 1){\n      \u002F\u002F 第2个ajax请求\n      $.ajax({\n        ......此处省略500字\n        success:function(data){\n          if(data.status === 1){\n            \u002F\u002F 第3个ajax请求\n            $.ajax({\n              ......此处省略500字\n              success:function(data){\n                if(data.status === 1){\n\n                }\n              }\n            });\n          }\n        }\n      });\n    }\n  }\n});\n```\n\n当顺序执行的异步操作越来越多的时候，回调层级也就越多，这也就是传说中的“回调恶魔金字塔”。\n\n\u003C!-- more -->\n\n## 生成器的卢山真面目\n\n所谓“生成器”，其实是一个函数，但是这个函数的行为会比较特殊：\n\n1. 它并不直接执行逻辑，而是用来生成另一个对象（这也正是“生成器”的含义）\n2. 它所生成的对象中的函数可以把逻辑拆开来，一片一片调用执行，而不是像普通的函数，只能从头到尾一次执行完毕\n\n生成器的语法和普通函数类似，特殊之处在于：\n\n1. 字面量（函数声明\u002F函数表达式）的关键字`function`后面多了一个`*`，而且这个`*`前后允许有空白字符\n2. 函数体中多了`yield`运算符\n\n举个粟子：\n\n```javascript\nfunction * GenA(){\n  console.log('from GenA, first.');\n  yield 1;\n  console.log('from GenA, second.');\n  var value3 = yield 2;\n  console.log('from GenA, third.',value3);\n  return 3;\n}\n\nvar a = GenA();\n```\n\n接下来依次执行：\n\n```javascript\na.next();\n\u002F\u002F from GenA, first.\n\u002F\u002F Object {value:1,done:false}\n\na.next();\n\u002F\u002F from GenA, second.\n\u002F\u002F Object {value:2,done:false}\n\na.next(333);\n\u002F\u002F from GenA, third.\n\u002F\u002F 333\n\u002F\u002F Object {value:3,done:true}\n\na.next();\n\u002F\u002F Object {value:undefined,done:true}\n```\n\n这个例子反映了生成器的基本用法，有以下几点值得注意：\n\n1. 在调用`GenA()`时，函数体中的逻辑并不会执行（控制台没有输出），直接调用`a.next()`时才会执行\n2. `a`是一个对象，它由生成器`GenA()`调用而来，注意`GenA()`并没有返回`a`对象，这非常像构造函数的执行形式，但是不允许添加`new`\n3. 调用`a.next()`时，函数体中的逻辑才开始真正执行，每次调用时会到`yield`语句结束，并将`yield`的运算数作为结果返回\n4. `a.next()`返回的结果是一个对象，对`yield`的运算数做了包装，并带上了`done`属性\n5. 当`done`属性为`false`时，表示该函数逻辑还未执行完，可以调用`a.next()`继续执行\n6. 最后一次返回的结果为`return`语句返回的结果，且`done`值为`true`。如果不写`return`，则值为`undefined`\n7. `value3 = yield 2`这句是指，这一段逻辑返回2，在下一次调用`a.next()`时，将参数赋给value3。换句话说，这句只执行了后面半段就暂停了，等到再次调用`a.next()`时才会将参数赋给value3并继续执行下面的逻辑\n8. 返回值中`done`为`true`时，仍然可以继续调用，返回的值为`undefined`\n\n## 同步场景下生成器的使用\n\n来看看同步场景下，如何使用生成器：\n\n```javascript\nfunction * Square(){\n  for(var i=1;;i++){\n    yield i*i;\n  }\n}\n\nvar square = Square();\n\nsquare.next(); \u002F\u002F 1\nsquare.next(); \u002F\u002F 4\nsquare.next(); \u002F\u002F 9\n......\n```\n\n同步场景下大概就是这么用的，很无趣是吧？我也这么觉得，其实和直接函数调用差别不大。不过值得注意的是，我们在循环中并没有设中止条件，因为调用一个`square.next()`方法，它才会执行一次，不调用则不执行，所以不用担心死循环的问题。\n\n## 异步场景下的生成器使用\n\n如何用生成器解决异步场景下的“回调恶魔金字塔”呢？满心期待对吧，很遗憾，它并不能那么简单地解决……\n\n从前面的例子中，其实已经可以体会出来了，生成器的用法中并不包含对异步的处理，所以其实没有办法帮助我们对异步回调进行封闭。那么为什么大家将它视为解决回调嵌套的神器呢？在翻阅了不少资料后找到[这篇文章](http:\u002F\u002Fblog.stevensanderson.com\u002F2013\u002F12\u002F21\u002Fexperiments-with-koa-and-javascript-generators\u002F)，文章作者一开始也认为生成器并不能解决回调嵌套的问题，但下面自己做了解释，如果生成器的返回的是一系列的Promise对象的话，情况就会不一样了，举个粟子：\n\n```javascript\nfunction myAjax(){\n  return fetch('http:\u002F\u002Fecho.113.im?data=1');\n}\n```\n\n我们使用`window.fetch`方法来处理ajax请求，这个方法会返回一个Promise对象。然后，我们使用一个生成器来包装这个操作：\n\n```javascript\nfunction * MyLogic(){\n  var serverData = yield myAjax();\n  console.log('MyLogic after myAjax');\n  console.log('serverStatus:%s',serverData.status);\n}\n```\n\n使用的时候这样用：\n\n```javascript\nvar myLogic = MyLogic();\nvar promise = myLogic.next().value;\npromise.then(function(serverData){\n  myLogic.next(serverData);\n});\n```\n\n可以看到，我们这里的`myAjax1()`以及`MyLogic()`函数中，并没有使用回调，就完成了异步操作。\n\n这里有几个值得注意的点：\n\n1. `myAjax()`函数返回的是一个Promise对象\n2. `myLogic`中的第一个语句，返回给外界的是`myAjax()`返回的Promise对象，等外界再次调用`next()`方法时将数据传进来，赋值给`serverDate`\n3. `promise`的状态是由第三段代码，在外部进行处理，完成的时候调用`myLogic.next()`方法并将`serverData`再传回`MyLogic()`中\n\n你一定会问，下面这个`promise.done`不就是回调操作么？Bingo！这正是精华所在！我们来看一下这段代码做了什么：\n\n首先，`myLogic.next()`返回了一个Promise对象（`promise`），然后，`promise.then`中的回调函数所做的事情就是调用`myLogic.next()`方法就行了，除了调用`next()`方法，其它的什么事情都没有。此时，我们就会想到一个程序员特别喜欢的词，叫“封装”！既然这个回调函数只是调用`myLogic.next()`方法，那为什么不把它封装起来？\n\n## 异步封装\n\n首先，我们保持`myAjax()`和`MyLogic`定义不变，而将`myLogic.next()`放到一个函数来调用，这个函数专门负责调用`myLogic.next()`，得到返回的Promise对象，然后在Promise被resolve的时候再次调用`myLogic.next()`：\n\n```javascript\nvar myLogic = MyLogic();\n\nfunction genRunner(){\n\n  \u002F\u002F 调用next()获取promise\n  var yieldValue = myLogic.next();\n  var promise = yieldValue.value;\n\n  if(promise){\n    promise.then(function(data){\n      \u002F\u002F promise被resolve的时候再次调用genRunner\n      \u002F\u002F 以继续执行MyLogic中后面的逻辑\n      genRunner();\n    });\n  }\n}\n```\n\n这样我们就把不停地调用`myLogic.next()`和不停地`promise.then()`的过程进行了封装。运行`genRunner()`跑一下：\n\n```\nMyLogic after myAjax1\nUncaught (in promise) TypeError: Cannot read property 'status' of undefined(…)\n```\n\n可见`MyLogic`在`yield`后的语句的确被执行了，但是`serverData`却没有值，这是因为我们在调用`myLogic.next()`的时候没有把值传回去。稍微修改下代码：\n\n```javascript\n\u002F\u002F diff1: genRunner接受参数val\nfunction genRunner(val){\n\n  \u002F\u002F diff2: .next调用时把参数传过去，yield左边可以被赋值\n  var yieldValue = myLogic.next(val);\n  var promise = yieldValue.value;\n\n  if(promise){\n    promise.then(function(data){\n      \u002F\u002F diff3: 调用genRunner时传递参数\n      genRunner(data);\n    });\n  }\n}\n```\n\n这次一切都对了：\n\n```\nMyLogic after myAjax1\nserverStatus:200\n```\n\n至此我们已经把封装最核心的部分抽离出来了，我们的业务代码`MyLogic()`已经是“异步操作，同步写法”，而我们亲眼见证了这一切是怎么办到的。那么接下来？为什么不再封装得更通用一些呢？\n\n```javascript\nvar genRunner = function(GenFunc){\n\n  return new Promise(function(resolve, reject){\n\n    var gen = GenFunc();\n\n    var innerRun = function(val){\n\n      var val = gen.next(val);\n\n      \u002F\u002F 如果已经跑完了，则resolve\n      if(val.done){\n        resolve(val.value);\n        return;\n      }\n      \u002F\u002F 如果有返回值，则调用`.then`\n      \u002F\u002F 否则直接调用下一次innerRun()\n      \u002F\u002F 为简单起见，假设有值的时候永远是promise\n      if(val.value){\n        val.value.then(function(data){\n          innerRun(data);\n        });\n      }else{\n        innerRun(val.value);\n      }\n\n    }\n    innerRun();\n\n  });\n\n};\n```\n\n这里我们将刚刚看过的封装改成了`innerRun()`，并加上了自动调用。外面再封装了一层`genRunner()`，返回一个Promise。在`genFunc`全程调用完之后，Promise被resolve。\n\n用起来大约是这样：\n\n```javascript\ngenRunner(function*(){\n\n  var serverData = yield myAjax();\n  console.log('MyLogic after myAjax');\n  console.log('serverStatus:%s',serverData.status);\n\n}).then(function(message){\n\n  console.log(message);\n\n});\n```\n\n生活真美好！\n\n最后，以别人文章中的一段koa框架使用代码收尾吧：\n\n```javascript\nvar koa = require('koa'),\n  app = koa();\n\napp.use(function *() {\n\n  \u002F\u002F 这是这个例子中最重要的部分，我们进行了一系列异步操作，却没有回调\n  var city = yield geolocation.getCityAsync(this.req.ip);\n  var forecast = yield weather.getForecastAsync(city);\n\n  this.body = 'Today, ' + city + ' will be ' + forecast.temperature + ' degrees.';\n\n});\n\napp.listen(8080);\n```\n\n眼熟吗？koa就是像我们刚刚做的这样，封装了对生成器返回值的处理和调用`next()`方法的细节（这里的`app.use()`就像前面的`genRunner()`函数），使得我们的逻辑代码看起来是如此简单，这正是koa的伟大之处，也是ES6生成器这一特性能迅速引起如此多轰动的真正原因。\n","\u002Farticle\u002Flearning_es6_generator.html",{"id":134,"type":7,"slug":135,"title":136,"date":137,"category":68,"tags":138,"body_markdown":139,"permalink":18,"excerpt_src":18,"media_type":73,"media_title":140,"media_author":141,"media_url":142,"rating":18,"layout":18,"pv":19,"admin_only":19,"created_at":20,"updated_at":20,"deleted_at":18,"status":21,"cover":143},10,"comments-on-the-late-gap-year","《迟到的间隔年》","2013-12-02 12:00",[94,95],"\n![迟到的间隔年](\u002Fassets\u002Fbmm\u002F2013\u002Fcomments-on-the-late-gap-year\u002F01.jpg)\n\n今天看完了《迟到的间隔年》，这是一本比较短小的书，不过我也陆续看了一个来月。\n\n书中的内容是说作者在辞掉工作之后行走东南亚的经历。从缅甸到老挝到越南到印度再到巴基斯坦，作者在东南亚游历了大约1年的时间，然后从巴基斯坦回到国内西藏，最后以尼泊尔作为旅行的最后一站。\n\n坦白讲，文字其实很一般，这也不是这类作品的价值所在。这本书打动我的地方在于你可以见识到各种不同的人，各种不同的生活方式。你会惊叹原来印度的地痞流氓是如此之多，老挝的人是如此之悠闲，2美元竟然可以开一个小吃店，义工竟然是件如此美好的事情，没有宗教信仰竟是如此不可理喻……\n\n合上书本，看看周围的世界，赶公交赶项目追房子追物价似乎就是生活的全部。这种强烈的对比让人很深刻地意识到，原来自己的生活如此单调，原来我们看到的世界如此之小，进而对这个世界再次生出敬畏，对未来充满希望…… 如果你也想去看看世界而又不能立刻上路，那么，不妨看看这本吧。\n","迟到的间隔年","孙东纯","https:\u002F\u002Fbook.douban.com\u002Fsubject\u002F3905366\u002F","https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Fbmm\u002F2013\u002Fcomments-on-the-late-gap-year\u002F01.jpg",190]