<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>nightire (Albert Yu)</title>
    <link>https://ruby-china.org/nightire</link>
    <description></description>
    <language>en-us</language>
    <item>
      <title> [上海 徐汇] 巧议科技招聘 Elixir/Ruby 开发工程师</title>
      <description>&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2017/b9fd25cd-db3a-4acc-aaf0-dd39262c0c65.png!large" title="" alt="logo"&gt;&lt;/p&gt;
&lt;h2 id="关于我们"&gt;关于我们&lt;/h2&gt;
&lt;p&gt;我们是巧议网络科技股份有限公司，位于上海市徐汇区。我们设计和构建基于互联网的应用程序，目前专注于一款产品：巧思（choiceform）。巧思是一个在线问卷调研平台，集专业问卷设计、多平台／设备问卷分发、数据实时收集／分析、以及商业智能呈现于一体，致力于帮助企业把传统的线下人工数据调研平滑过渡到互联网平台上。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2017/6012973a-866e-4ad4-a342-a650252fe3ee.png!large" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;我们借鉴了许多成功的桌面／互联网应用程序（比如 PhotoShop、Sketch、Webflow 等等），设计了一套完整、专业的图形设计客户端，用户能以实时可视化的方式设计专业的在线调研问卷。支持超过三十项的问卷元素，从常见的选择、填空、打分到高级用户需要的变量引用、循环、分支、配额甄别、3D（基于 WebGL）等等，充分满足了企业级用户的调研需求。&lt;/p&gt;

&lt;p&gt;通过巧思设计的问卷可以选择多种发布渠道，以丰富的手段直达终端用户，高度可交互的问卷客户端让问卷作答成为一种乐趣和享受，同时运用多种丰富的技术手段来帮助企业用户收集各种用户作答时产生的信息，让问卷能够收集和反馈真正有意义和价值的数据。&lt;/p&gt;
&lt;h2 id="产品愿景"&gt;产品愿景&lt;/h2&gt;
&lt;p&gt;设计制作和分发收集，这是巧思走出的第一步和第二步。我们看到的巧思的真正价值则在于对收集到的数据进行准确的专业分析，并结合企业的商业需求作出商业辅助决策。&lt;/p&gt;

&lt;p&gt;这对我们来说是一件很有趣的事情，为此我们已经做了很多的技术探索，比如说图表设计和制作工具（基于 d3 自主研发的工具库）、基于 WebGL 技术的三维视图呈现技术，基于 WebVR 的虚拟现实呈现技术等等。我们的技术团队是不设限的，一直在致力于融合多种互联网技术来提供商业价值。从移动互联网到云计算，从大数据分析到视觉呈现，我们的界限只存在于想象之中。&lt;/p&gt;
&lt;h2 id="团队需求"&gt;团队需求&lt;/h2&gt;
&lt;p&gt;巧议拥有一个小而精干的团队，我们的团队理念是充分挖掘每一位成员的技术潜力，给予足够的信任与空间让大家尽情发挥。我们的成员年龄结构非常合理，从老司机到小鲜肉总有一款适合你的“口味”；技术分布也非常均衡，从前到后都有经验丰富的 leader 总揽全局，所有成员都有专精的领域和全栈的能力，最有意思的是连我们 CEO 都是技术团队的成员，非技术背景出身却在前端设计和样式方面有着很高的造诣，真可谓全民皆”兵“。&lt;/p&gt;

&lt;p&gt;去年此时，我代表团队招才纳贤，最终收获了一枚 Ruby 功力深厚的后端大神。现如今，我们为了下一步的产品计划正在积极专注的改善我们的后端架构和程序设计。目前我们已经基本转型到了基于 Elixir／Erlang 的后端技术栈，所以今年我们正在寻找一位”希望之星“，为团队注入新鲜血液，和我们一起工作成长。&lt;/p&gt;
&lt;h2 id="职位描述"&gt;职位描述&lt;/h2&gt;
&lt;p&gt;目前来看 Elixir 固然很小众，不过没有关系，现在团队里的几位 Elixir 工程师也都是从 Ruby／JavaScript 转型过来的。作为一个学习型的团队，我们欢迎勇于挑战，拥抱变化的职业选手。无论你从前擅长哪种语言技术，无论你对 Elixir 是否了解，我们都有信心让你以最快的速度学会和适应新的技术栈。&lt;/p&gt;

&lt;p&gt;我们希望找到一起把想法用技术的方式变现的人。从短期看我们会用 Elixir 重建后端，从长期看我们希望所有人员都是全栈。当然这并不代表一个人要干 n 个人的活，工作量我们崇尚充分沟通并合理安排，但是工作质量则需要团队的每个人都有从整体入手进行考量和设计，然后深入具体领域完成漂亮实现的能力。&lt;/p&gt;

&lt;p&gt;下面是一些可量化的基础能力的要求，你不需要保证靠 100 分，我们更看重找对合适的人，而不是简单的 check list：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;以目标为驱动的思考方式（明确地说，技术应该是为产品或者说商业价值服务的，我们做的事情决定了这个前提，也希望共事者可以认同和理解）&lt;/li&gt;
&lt;li&gt;熟悉 Ruby 和 Rails（或者其他语言以及比较 modern 的 web 技术栈，我们不是要否定其他 web 技术的价值，只是认为这样共同语言会更多一些），如果熟悉 Elixir 和 Phoenix 更好&lt;/li&gt;
&lt;li&gt;熟悉关系型数据库，我们主要使用 PostgreSQL；另外如果你对分析型数据库有所涉猎那更棒了&lt;/li&gt;
&lt;li&gt;有编写测试的习惯，我们不喜欢拿测试当考核指标用，但是真正懂测试的人应该知道怎么做&lt;/li&gt;
&lt;li&gt;对技术有广泛的涉猎和热情，但不偏激&lt;/li&gt;
&lt;li&gt;良好的沟通技巧，能清晰的表达自己的想法&lt;/li&gt;
&lt;li&gt;更习惯看国外的技术资料，并自带相关技能&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;加分项&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;熟悉 JavaScript 并且有前端相关经验&lt;/li&gt;
&lt;li&gt;读过 Web 领域相关的规范，比如 HTTP, OAuth 2.0 等&lt;/li&gt;
&lt;li&gt;不错的算法功底，平时喜欢做一些 kata&lt;/li&gt;
&lt;li&gt;有自己的技术积累，比如说博客、开源项目等等&lt;/li&gt;
&lt;li&gt;有丰富的业余爱好，游戏、音乐、电影、运动等等，我们可以找到很多共同话题&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="福利待遇"&gt;福利待遇&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;每年都有配股的机会，目前技术团队拥有配股的比例是 100%，我们不需要很多人，我们希望每个人都能发挥自己的能力并获得自己的回报。我们每年的开发目标都和自身的收益有着关系，收益的权利是相等的，收益的额度就看你自己了。&lt;/li&gt;
&lt;li&gt;正规的福利保障，按照国家政策标准缴纳保险和公积金。&lt;/li&gt;
&lt;li&gt;弹性工作制，当然也需要你有足够的弹性适应力。&lt;/li&gt;
&lt;li&gt;开发设备（电脑、显示器）配给尊重个人的意愿和要求，标配 MBP 和 外接显示器。&lt;/li&gt;
&lt;li&gt;本次招聘的月薪范围是 10-15k，面向的是初级工程师，主要给予的是充分的成长空间，但同时你已经拥有了配股的机会。&lt;/li&gt;
&lt;li&gt;另外，公司鼓励工程师参加技术活动，比如技术会议等，并给予资金上的支持或奖励。简而言之，技术投资公司乐于出资。&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;其他方面的细节可以在面谈时具体商议。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="联系方式"&gt;联系方式&lt;/h2&gt;
&lt;p&gt;求职信息／简历请发至 albert#cform.io，24 小时内联系及回复，由于招聘信息发布渠道广泛，如果回复／跟帖没有得到回应请见谅，并请尽量发送电子邮件联系。谢谢！&lt;/p&gt;</description>
      <author>nightire</author>
      <pubDate>Mon, 05 Jun 2017 13:14:45 +0800</pubDate>
      <link>https://ruby-china.org/topics/33148</link>
      <guid>https://ruby-china.org/topics/33148</guid>
    </item>
    <item>
      <title>从恨到爱——如何使用 Ember 开发应用程序</title>
      <description>&lt;h2 id="前言"&gt;前言&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;前方预警：&lt;/strong&gt;这将是一篇长文，我尽可能把主题区分开并做到良好的排版，因此你可以根据标题来检索你感兴趣的部分。&lt;/p&gt;

&lt;p&gt;不过在我展开之前，先得丢几句不是那么“干”的货。从恨到爱，这个标题源自于&lt;a href="https://medium.com/front-end-hacking/am-i-wrong-in-hating-react-js-811ef963aa2d#.ndv07461y" rel="nofollow" target="_blank" title=""&gt;一位同行的文章&lt;/a&gt;，在这篇文章里 Ember 只是一个引子，但却因为作者对于 Ember 的态度所发生的转变而使得他怀疑自己“讨厌 React”的态度可能也是错的。&lt;/p&gt;

&lt;p&gt;我自己对 Ember 的态度倒没有这位老兄这样的极端化，至少我没有恨过它。对于技术性的东西，如果让你产生了恨意，那多半是因为自己太蠢：要么掌握不了它的用法，要么没搞清楚它真正的用武之地。可是人非圣贤，主观的好恶是免不了的，如果因为工作环境的需要你不得不使用一些你不喜欢的工具，那么你要么从容应对化腐朽为神奇，要么另谋高就，寻找更加适合自己的团队。恨，是解决不了问题的，它只会污染你和你身边同事的情绪；而作为工程师，我们是天生擅长解决问题的那一群人，何去何从就要问问自己的内心了。&lt;/p&gt;

&lt;p&gt;本文最大的受益群体应该是那些对 Ember 无感甚至有些讨厌 Ember 的人，并且你们因为工作或事其他什么原因不得不用这个框架（这么一说，真没多少人吧……），既不算喜欢又不能逃避，这的确挺煎熬的。那么我希望能够通过分享我们团队使用 Ember 的一些经验让你们对 Ember 的了解能更加完整一些深入一些，如果能让你们获得一些“啊哈”的瞬间那就更棒了，我相信用不了太久你们就能从恨它到爱上它，或者从路人转粉，我的一点心愿吧。&lt;/p&gt;
&lt;h2 id="现在（2016年末）的 Ember 足够成熟到值得推荐吗？"&gt;现在（2016 年末）的 Ember 足够成熟到值得推荐吗？&lt;/h2&gt;
&lt;p&gt;我认为是的。&lt;/p&gt;

&lt;p&gt;当然也有业界专家们也这么看：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2016/265808cd89bcb01cafd29334bc8a721e.png!large" title="" alt="ember-mature-enough"&gt;&lt;/p&gt;

&lt;p&gt;上图截取自 2016 年 11 月发布的 ThoughtWorks 技术雷达白皮书，详情可见：&lt;a href="https://www.thoughtworks.com/radar" rel="nofollow" target="_blank"&gt;https://www.thoughtworks.com/radar&lt;/a&gt;，有中文版的。&lt;/p&gt;

&lt;p&gt;可以看出，在新的或是有持续进展的框架中，ThoughtWorks 对 Ember 成熟度的认同是最高的，和 React + Redux 同属一个级别——值得注意的是，React 算不上真正意义上的框架，即使搭配了 Redux 要达到 Ember 的完整度也还要花很多功夫的。&lt;/p&gt;

&lt;p&gt;Angular 哪儿去了？v1 已经不再推荐了，v2 由于比较新，目前也很难下一个结论。我不否认 v2 有着一些很吸引人的特点，不过这不是什么“独占”的技术，需要的话我在 Ember 当中也可以容易的添加上它们。更重要的是非技术层面的观感，下面是一段引文：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You were part of the team at Angular, and you talk about “irreconcilable differences” between yourself and the team, could you talk a bit about that?&lt;/p&gt;

&lt;p&gt;We disagreed on various technical choices which I felt wouldn’t be tenable in the real world, wouldn’t be flexible enough. But coming back to the business side of things, what I began to see when I worked there was that the entire development process was completely disconnected from the community. There was a lot of speak that went out that was “Thank you, we love our community…” and that was genuine, they really do, but when they went back to build version 2, there were no use cases or case studies coming from anyone, not even from inside Google. There were groups inside of Google using things that they could have done an official case study with, and that didn’t happen there and it didn’t happen publicly.&lt;/p&gt;

&lt;p&gt;If you do a bit of research you’ll find that Angular is actually a subteam inside of something called GreenTea which is an internal app (specifically Google’s Adwords CRM). Really, that is the driver for Angular, and if Angular does anything at all, it’s going to be this one app. Because of how they’re set up there, Angular isn’t really this independent Google product that exists to do what people think it does. It does some of those things, but that whole business side is missing. It really exists because it’s funded internally by this other thing that has it’s own agenda.&lt;/p&gt;

&lt;p&gt;-- &lt;a href="https://www.cuttlesoft.com/interview-with-durandals-rob-eisenberg/" rel="nofollow" target="_blank" title=""&gt;Rob Eisenberg&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;捉虫：现在 Ruby China 的 markdown 无法正确处理引言的分段了&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Rob Eisenberg 是 Aurelia 框架的作者（Aurelia 也在上图中有），同时也是 Ember 社区的活跃份子之一，最近 Ember Engines 的新功能就是他主写的。之前他曾经受邀加入了 Angular 2 开发组，主要负责新的路由系统，但后来他主动离开了。上面的引言来自于一次访谈，大概讲述了离开的原因。在这里我也无意展开去说什么道理，只是我觉得有些事情还是值得去了解一下的，所谓“兼听则明”吧，反正我一直对 Angular 的社区无感，个人意见。&lt;/p&gt;

&lt;p&gt;这些事情对于一些人来说可能也无足轻重，但至少说明了部分业内人士对于 Ember 的评价是相当不错的，所以我认为它足够成熟，也足够让我向别人推荐。当然我们总是需要考虑实际的业务场景的，因此接着我就来说说：&lt;/p&gt;
&lt;h2 id="什么情况下你不需要 Ember"&gt;什么情况下你不需要 Ember&lt;/h2&gt;
&lt;p&gt;咱一上来先说 Ember 不适用的地方，这样可以节省一些读者的时间。如果你觉得你现在做的事情和下面的描述比较接近的话，Ember 大概是无法给你带来什么特别的好处的。但如果你已经是一个经验丰富的 Ember 工程师，那么你也应该知道即使在这些场合之下 Ember 依然可以发挥它的优势，善于利用的话也可以改变一些典型的思路并获得出其不意的效果——这全看你对你做的事情的理解，不必强求。&lt;/p&gt;

&lt;p&gt;而在你心里没什么谱的时候，下面这些情况是你&lt;em&gt;可以&lt;/em&gt;不需要 Ember 的。&lt;/p&gt;
&lt;h3 id="内容展示为主的网站"&gt;内容展示为主的网站&lt;/h3&gt;
&lt;p&gt;这个分类涵盖的范围极广，小到只有几个页面的官方网站，大到一个 CMS（比如现在遍地都是的网络小说网站）都算是以内容呈现和展示为主的网站。在过去十到二十年的时间里，互联网的主体内容就是这些，我们在日常获取绝大部份信息的渠道也正是这些。哪怕是今天，基于 Web 的应用程序这个概念炒得火热的 2016 年，我想至少八成以上的开发者做的事情也离不开这个范畴。这一类型的网站存在的时间足够长，大家对它的研究和探索也足够深入和广阔，因此能够把它做好的技术也是足够成熟和稳定的，用不用得着 Ember 真的是见仁见智的事情。在我看来，多数情况下不需要用到 Ember 也绰绰有余。有鉴于此，Ember 的用户基数很小在我看来一点儿也不奇怪。&lt;/p&gt;

&lt;p&gt;但是近年来，Angular／React／Vue 等前端框架或工具的崛起吸引了很多开发者的目光，所以大家也都纷纷试水开始使用它们来开发这一类型的网站。在这个过程当中，那些基础好、善于钻研、并且对 web 的理解比较到位的人并不会觉得有什么不便，并且还能够从中发掘出这些新兴工具的闪光点，让它们能够为自己的日常开发起到更多的助益；反之也会有另外一部分人会觉得非常不适应，一面增加了自己学习的负担，一面却体会不到新工具带来的好处（注：这是客观存在的现象，而不是说一定要分出个高下来，各位看官请自行体会），于是会放弃它们回归到自己的舒适区域，这在我看来也是很正常的事情。&lt;/p&gt;

&lt;p&gt;所以，你觉得新的工具在眼下帮不了你什么，那就不用它便是了，没必要产生一些额外的情绪，也没必要反感和排斥那些乐于探索新世界的人。想想前言里那位同行老司机吧，说不定有一天你也会产生同样的感受。另外，就算是停留在那些稳定舒适的技术区里，你就一定以为自己已经掌握的足够出色了吗？即使是那些今天看起来不那么 fancy 的工具和技术，它们所代表的基础技术素养也是极为重要的。我这里有几个例子，留着后面铺开了讲，下面再说说第二种：&lt;/p&gt;
&lt;h3 id="互动式炫酷 UI 为主的网站"&gt;互动式炫酷 UI 为主的网站&lt;/h3&gt;
&lt;p&gt;还有一些个网站本质上也属于内容呈现类的，但却不局限于传统的文本、图片等媒介，而是大量采用动画、音视频、绚丽的色彩、大胆的布局、前卫的交互技术，甚至包括 3D 特效等技术。这类网站可以看作是第一类的升级加强版，会给用户带来美妙到震撼的感官享受。擅长做这类网站的前端工程师也是值得敬佩的，因为他们往往也要花费大量的时间在学习和钻研前沿技术上面，并且他们还要擅长设计，从传统的平面设计一直跨越到 Web 设计，同时也要包括各种形式的媒介展示技术。他们或许并不擅长那些知名的应用程序框架，或许也很少制作大型应用程序的经验，但是他们所擅长的东西也足以让偏工程化为主的开发者们羡慕不已了。&lt;/p&gt;

&lt;p&gt;开发这样的网站不用 Ember 也是很正常的，因为这类网站的技术聚焦点不在于应用程序架构，而是更具体一些的技术，这个涉及的层面就很广阔了，比如说最近两年如火如荼的 HTML5 之中就有很多相关的技术点和 API 是为这类开发而生的。在过去很长一段时间里，这类网站主要被 Flash 的开发者们所垄断，后来鬼知道 Flash 都经历了些什么，反正这帮人基本上都转战 Web 了。可能像 Ember 这样的应用程序框架对他们而言是陌生的，或者是不对路数的，而没有 Ember 的话也不会让它们觉得缺少了些什么。&lt;/p&gt;

&lt;p&gt;在我看来，Ember 的组件化技术其实也是可以派的上用场的，不过仅仅考虑视图这一块的话，用 jQuery 操作 DOM 已经足够优秀，喜欢组件化的话也可以选择 React 之类的工具，所以 Ember 在这里就无关紧要的很了。&lt;/p&gt;
&lt;h3 id="信息管理为主的（后台）网站／应用"&gt;信息管理为主的（后台）网站／应用&lt;/h3&gt;
&lt;p&gt;这个类型其实算是一个临界点，拿 Ember 来说用或者不用当然也都可以，但个人认为用它带来的好处会更大一些。信息管理类的网站相比于内容展示类的网站最大的不同点在于：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;业务模型相对复杂一些。比如说一个小说网站，从前台的角度看没多少业务实体参与其中的，掰着指头数不会超过 10 个，一般 5 个左右就足够用了。但切换到后台这个数量级至少得 x2（其实 CMS 也在此例，只不过前面我们是单说它的前台罢了），再多一些也很寻常。&lt;/li&gt;
&lt;li&gt;要处理的用户交互更多。在前台用户主要就是看，收藏评论这一类的交互已经算是复杂的了；切到后台之后，用户虽然少了，但交互的频次和复杂度却直线上升，特别是各种批量操作、搜索／过滤／排序、数据呈现、角色／权限控制。所说这些东西最终也都必然会体现在 DOM 操作上，但很大一部分工作则是隐藏在视图之下的。&lt;/li&gt;
&lt;li&gt;数据交互也变得复杂了。这件事情要分两头说，有一种情况是整个网站无论前后台都是服务端渲染 + 静态化输出的，用的自然也就是大家耳熟能详的那些，这种情况下前端工具往往扮演的是“助攻者”的角色，很多动态化的数据更新其实还是后端做的，前端只能负责到渲染之后的用户交互上，也就是说其实大量的业务逻辑都是后端先做好了，只把结果丢给视图然后才有前端介入，那么此时 Ember 也会缺少自己的用武之地，倒是 jQuery／React／Vue 等等比较合适。
另外一种情况就是后端很轻，只有基本的数据模型／校验再加上 API 输出，而把更多的业务逻辑交给前端来完成，那么这时候 Ember 就能发挥自己的特长了。更具体的事情我后面再谈，这里只是一个概述。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;有很多现成的例子，比如博客引擎 Ghost 的后台，论坛 Discourse 全站，Linkedin 的管理后台和移动应用，Yahoo 内部使用的一些管理后台等等都是采用了 Ember 作为开发框架。在国内多数此类的应用都是基于 Angular 开发的，鉴于本人拥有长达四年的 Angular 开发经验，类似的企业级管理后台也做过几个，我可以负责任的说综合评比 Ember 绝对比 Angular 好用，至于 Angular 2 我不熟，暂时也没有要研究它的打算，就不予置评了。&lt;/p&gt;

&lt;p&gt;不过这里我们说的是可以不用 Ember 啊是不是？没错，因为这类网站大家也都是做的快要吐了的，不用 Ember 也没什么，只不过从这里开始使用 Ember 会带来更好的体验罢了——代价也有，比如说你得先学，而且在这类场景下想用的足够爽，你要学的还不能只是皮毛，否则发挥不出原有的十分之一也只会让你觉得束手束脚。&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;以上差不多涵盖了绝大部分基于 Web 的应用场景了。我说你&lt;strong&gt;可以不用&lt;/strong&gt;，但我并没有说&lt;strong&gt;你不能用&lt;/strong&gt;，也没有说&lt;strong&gt;用了不好&lt;/strong&gt;，比较准确的意思应该是&lt;strong&gt;你不用影响不大，其他的技术手段足够应对，并且 Ember 或其他框架也不是万能的，某些角度的考量下它（们）也有自己的劣势。&lt;/strong&gt;所以在这些场景下，Ember 可以不是第一选择。接下来我从自己的角度来说说什么时候 Ember 会是我的第一选择。&lt;/p&gt;
&lt;h2 id="框架的意义"&gt;框架的意义&lt;/h2&gt;&lt;h3 id="jQuery 的问题不是 jQuery 的问题，而是观念和使用者的问题"&gt;jQuery 的问题不是 jQuery 的问题，而是观念和使用者的问题&lt;/h3&gt;
&lt;p&gt;我们经常可以看到很多人在争论一个问题：&lt;strong&gt;jQuery 还是 xx 框架？&lt;/strong&gt;而这种争论的源起大概应该是多数的现代前端框架都不推荐使用 jQuery 了，或者换个说法，它们都认为可以不需要使用 jQuery 了，并且在绝大多数情况下效果只会更好。&lt;/p&gt;

&lt;p&gt;这样的意见倾向当然会引起争议，但人们在争论的时候却显然忽略了一个本质的问题：&lt;strong&gt;说可以不用 jQuery 的并不是框架，而是数据驱动这种新的思路&lt;/strong&gt;，而数据驱动这种思路不是孤立存在的，它需要有相对完整的架构链路来保障数据的通路——也就是说，当你用数据驱动的思路去开发应用程序的时候，DOM 仅仅是一整个链条中的一个部分，甚至都不是重心。jQuery 不是不能在数据驱动的思路中发挥作用，但只靠 jQuery 是肯定不够的。当你做的应用在应对数据处理时的规模不太大或者不复杂，又或者数据相关的业务逻辑都是服务端帮你完成的时候，你对此的感觉是非常微弱的。那么这些情况我都已经在前文中列举过，所以你觉得 jQuery 够了不需要框架那也是十分正常的事情。&lt;/p&gt;

&lt;p&gt;然而这些并不代表框架没有什么用处，或者框架在排斥 jQuery，我个人是很难理解为什么会有这样的想法，当我看到这些观点的时候我的感觉主要是两个原因：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;他们的确不怎么需要使用框架（就目前的需求来看），使用 jQuery 就足够了&lt;/li&gt;
&lt;li&gt;他们曾经学过／用过某种框架，但并没有学会或者没有领会到框架的实际意义&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;另外一方面也有一些人觉得框架&lt;strong&gt;可以&lt;/strong&gt;在任何场景下代替 jQuery 为代表的传统开发技术栈——注意，我又一次强调了&lt;strong&gt;可以&lt;/strong&gt;这两个字——不能说这是错的，因为的确可行，但也不代表就是正确的、明智的选择。这种说法的确会引起一些意见上的冲突，不过&lt;strong&gt;可以&lt;/strong&gt;不等于&lt;strong&gt;应该&lt;/strong&gt;，阅读这些观点的时候我们应该有自己的判断能力。&lt;/p&gt;

&lt;p&gt;如果遇到下列的场景，我也会毫不犹豫的使用 jQuery：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;后端 MVC 的架构，有完整的 View 层，不需要过度考虑前端组件化——当然，这个 View 层也得足够好用，像传统的 JSP 人工拼模版的方式，那我宁可在 jQuery 的基础上引入一个模板引擎 + 模块管理器，比如说 Handlebars + require.js。在这种场景下，后端框架可以提供给我足够的视图逻辑操作，比如说 Rails，我没必要硬塞一个前端框架进去。
不过对于我个人来说，我可能会倾向于逐渐改变这种架构，让后端去掉 View 层转变成纯粹的 API 服务，那么届时前端将会重构，框架就很有可能再次被引入了。至于为什么会有这种倾向，我放在后面单独说。&lt;/li&gt;
&lt;li&gt;制作一些单页面或少量页面即可满足需求的微型站点&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;所以如果你问我 jQuery 有什么问题，我会说它没有什么问题，真正的问题在于我们每一个人身处的业务环境不同，技术栈环境不同等等，很多时候是环境在约束你的技术栈选择，再接下来才是个人的倾向。你可以设想一下，接下来如果你只有 API 可用且不给你后端 View 层，在目前的应用发展趋势下你若是还能坚持 jQuery 去搞定一切的话，那你也算得上一条汉子了。&lt;/p&gt;

&lt;p&gt;那么有人就要问了：&lt;em&gt;凭什么不给我后端 View 层？这么多年我都用得好好的呀！&lt;/em&gt;&lt;/p&gt;
&lt;h3 id="你需要一个 API"&gt;你需要一个 API&lt;/h3&gt;
&lt;p&gt;你可以把前端能写的东西都混合在 MVC 的 View 层，这个时候 jQuery 似乎就足以应付了。&lt;/p&gt;

&lt;p&gt;如果现在要你做另一个应用呢？需要不同的 UI，但是数据和 Model 都是同一份。OK，可以设置新的路由，然后派发给新的控制器 + 视图来做，控制器或视图的重复可以抽取出来。&lt;/p&gt;

&lt;p&gt;如果现在要你做一个混合移动客户端呢？OK，我们可以判断 UserAgent 为移动端编写新的 View（且不论判断 UserAgent 是否准确，这里只是举例）&lt;/p&gt;

&lt;p&gt;如果现在要你做一个原生移动客户端呢？呃……写 API 吧&lt;/p&gt;

&lt;p&gt;如果你的应用程序的数据可以应用在更广泛的场合（而不仅仅是桌面浏览器里），那么为它编写一个 API（无论是内部使用的还是公共使用的）是迟早的事情。面对不断扩张的客户端需求，与其绞尽脑汁去修修补补现有的框架，还不如未雨绸缪的建立起 API 服务。&lt;/p&gt;

&lt;p&gt;如果你的应用程序才刚刚开始，你甚至都会考虑后端只提供 API，而不再使用臃肿的 MVC 架构——这也是一种趋势，在很多新兴的互联网公司、团队、项目中都有着广泛的实践。&lt;/p&gt;

&lt;p&gt;如果你现在所处的开发环境就是如此，那么有什么理由不去试试前端的框架呢？让我来描述一下最理性的思路：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;前后分离是必要前提条件，因为现在你没有后端视图层了。前后分离不是要逼迫你去分开，而是因为后端不再负责任何客户端的视图渲染了，于是分开就成了必然的选择。同时，前后分离并不等于 SPA，你依然可以开发各个页面独立的应用程序。再有，SPA 并不代表不支持 SEO 和首次加载慢，绝大多数的 SPA 框架都有服务端渲染技术的基础，这个我在后面单独会讲。&lt;/li&gt;
&lt;li&gt;API 会给予客户端比较完整且合理的数据映射（数据库 =&amp;gt; API），客户端需要有数据层来进行对接。绝大多数的框架都有适合自己的数据层来提供完整的解决方案。jQuery 只有底层的 Ajax 请求接口，数据怎么进来，怎么 mapping，怎么改变，怎么传递，怎么响应变化，怎么做有效性验证，怎么收集（用于提交）等等都需要你自己来设计。当你搞出一个相对通用且好用的方案来，你也就等于实现了一个其他框架里的等价机制，所以何去何从心里就会有个数了吧？&lt;/li&gt;
&lt;li&gt;如上所述，今后你很有可能做多个客户端（哪怕都是在一个平台上），所以通用逻辑或者 UI 组件如何抽取并且共享使用也是需要考虑的点。在这个方面，不同框架的表现各有不同，而你自己做也会面临同样的问题。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;即使你现在所处的开发环境还不是这样的，提供 API 这件事情从长远来看也是必要的。目前你可以依赖后端的 View，可要是遇到了我上面提到的一些场景，使用某种前端框架也是必要的了。&lt;/p&gt;

&lt;p&gt;刚巧前两天有一位国外同行写了一篇文章，文章的内容在这里不重要，重要的是里面几张图画的很好，我就借花献佛一下：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://d262ilb51hltx0.cloudfront.net/max/2000/1*k8ouvk608TxH5FaLM8XUxQ.png" title="" alt="back-to-2008"&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src="https://d262ilb51hltx0.cloudfront.net/max/2000/1*_kZH0yiuh9b7ypYpoPANTw.png" title="" alt="now-in-2006"&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src="https://d262ilb51hltx0.cloudfront.net/max/2000/1*m0iR2VK7iNt8flGqyhYx9A.png" title="" alt="the-divide"&gt;&lt;/p&gt;

&lt;p&gt;图的意思我就不浪费篇幅了，相信各位都能看明白。知乎上有人做了中文翻译，顺带分享一下地址：&lt;a href="https://zhuanlan.zhihu.com/p/23412169" rel="nofollow" target="_blank"&gt;https://zhuanlan.zhihu.com/p/23412169&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;（本章待续……）&lt;/p&gt;
&lt;h2 id="静态资源管理"&gt;静态资源管理&lt;/h2&gt;
&lt;p&gt;静态资源的管理其实是每一个前端工程师的必修课，尽管这件事情和 UI 的关系没有那么紧密，但是只有前端工程师才对如何管理和使用静态资源最有发言权（当然到了存储和伺服的环节就会偏向服务端了），这也是为什么自打  node.js 诞生以来，成堆的任务运行器和模块打包／构建机制疯狂滋生的主观原因——我们需要优秀的管理工具。&lt;/p&gt;

&lt;p&gt;当下最火的当属 webpack 派系了，不过由于 Ember CLI 诞生的较早并没有使用 webpack，而是借助了相对冷门的 broccoli 作为基础工具体系，然后在其基础上打造了专职服务于 Ember 体系的构建工具。也正是由于这个原因，大多数工程师对 Ember CLI 都不够熟悉，我在日常的交流中对此感触很深，因此我想单独撰写一章总结一下我所知道的一切。&lt;/p&gt;
&lt;h3 id="关于 Bower"&gt;关于 Bower&lt;/h3&gt;
&lt;p&gt;Bower 很烦人，一个需要修正的历史。实际上目前 Ember CLI 对 bower 的依赖只有两个了：&lt;code&gt;ember&lt;/code&gt; 和 &lt;code&gt;ember-cli-shims&lt;/code&gt;。前者已经重新发布到了 npm，目前正在集成测试中，预计下一个版本的 Ember CLI 就可以正式移除该依赖；而后者呢则可能会直接去除，新的 module resolution 方案将不再需要这些 shims。&lt;/p&gt;

&lt;p&gt;但是在很长一段时间之内 Ember CLI 还是会保留对 Bower 的支持，因为已有的项目可能会用 Bower 安装一些其他的依赖。在我的开发经验当中，几乎所有的依赖都可以找到对应的 npm modules 来取代 bower components，唯一的例外是 device.js——一个很小巧但是对移动端开发非常有用的库——不知道作者搞什么，npm modules 上发布的版本从来不更新，逼得人用 Bower。&lt;/p&gt;
&lt;h4 id="如果你需要用 Bower 安装模块，以下是正确的步骤："&gt;如果你需要用 Bower 安装模块，以下是正确的步骤：&lt;/h4&gt;
&lt;p&gt;&lt;u&gt;我用 &lt;code&gt;APP_NAME&lt;/code&gt;指代当前项目，&lt;code&gt;MODULE_NAME&lt;/code&gt; 指代目标模块，&lt;code&gt;EDITOR&lt;/code&gt; 指代使用编辑器编辑文件，&lt;code&gt;FILE_PATH&lt;/code&gt; 指代目标文件路径。另外如无特别说明，当前目录一概假定为项目的根路径。不再赘述。&lt;/u&gt;&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;bower &lt;span class="nb"&gt;install &lt;/span&gt;MODULE_NAME &lt;span class="nt"&gt;--save&lt;/span&gt;
&lt;span class="nv"&gt;$ &lt;/span&gt;EDITOR ember-cli-build.js
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// before `return app.toTree()&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./bower_component/MODULE_NAME/FILE_PATH&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;[,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;options&lt;/code&gt; 是一组可配置的参数，它的诸多用法可参照 &lt;a href="https://ember-cli.com/user-guide/#managing-dependencies" rel="nofollow" target="_blank" title=""&gt;Ember CLI 文档的 Managing Dependencies 一节&lt;/a&gt;。其中要重点说一下 &lt;code&gt;options.prepend: true&lt;/code&gt;，这个参数可以让被 &lt;u&gt;import&lt;/u&gt; 进来的依赖文件追加到 &lt;code&gt;vendor.{js,css}&lt;/code&gt; 的前面，这在很多时候都是必要的。比方说你要使用 &lt;code&gt;normalize.css&lt;/code&gt;，它就应该追加到 &lt;code&gt;vendor.css&lt;/code&gt; 的最前面才能发挥应有的作用。如果有多个文件都要 &lt;code&gt;options.prepend: true&lt;/code&gt;，那你就要小心安排 &lt;u&gt;import&lt;/u&gt; 它们的次序了。&lt;/p&gt;
&lt;h4 id="按需加载？"&gt;按需加载？&lt;/h4&gt;
&lt;p&gt;一般来说，一个 Ember App 只需要最终构建两个样式文件和两个脚本文件：&lt;code&gt;APP_NAME.{js,css}&lt;/code&gt; 和 &lt;code&gt;vendor.{js,css}&lt;/code&gt;，这些文件默认都已经引入在 &lt;code&gt;app/index.html&lt;/code&gt; 文件中了。然而很多人都喜欢按照一定的规则把样式／脚本拆分成多个子文件，然后希冀于“按需加载”并以此来获得预想中的“快速加载”体验，其实对于 SPA 应用来说这么做反而是得不偿失，理由有三：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;分成多个文件（但不按需加载，而是一次性加载全部）是有道理的，特别是在 HTTP/2 普及之后——目前只是实际尚未成熟，届时 Ember CLI 会做出改变来迎合新技术带来的好处；&lt;/li&gt;
&lt;li&gt;分成多个文件，借由路由驱动加上 &lt;code&gt;document.write&lt;/code&gt; “秘法”实现伪按需加载是没有道理的，且不说 &lt;code&gt;document.write&lt;/code&gt; 已经被打上了 &lt;u&gt;bad practise&lt;/u&gt; 的标签（现代浏览器会警告你），就是每次路由跳转都要等待零零碎碎的请求和加载就会影响用户的体验，你可能不得不定制 &lt;u&gt;loading state&lt;/u&gt; 来粉饰背后的“龌龊”，而且也使得离线应用特性变得名不符实；&lt;/li&gt;
&lt;li&gt;SPA 讲究的就是一次加载完毕，此后都是平滑／立刻响应，也就是说除了首次加载之外，之后的体验应该像桌面应用那样流畅（异步请求 + 局部状态更新是另外一个层面的问题，后面再说），而不是磕磕绊绊的。真正的按需加载不是这么玩的，如果确有其使用场景，你应该试试新的 Ember Engines（可以试试 LinkedIn 的移动端，感受一下无缝流畅的按需加载，这就是 Ember Engines 做到的效果）。&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;说到 Ember Engines，由于我还没机会去在正式产品中应用它，所以我估计这次是没法介绍太多了。明年 EmberConf 将会是 Ember Engines 和 Ember FastBoot 正式登场的时候，我希望届时再开专坑来填。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;因此我个人的建议是不要刻意去搞什么按需加载，为了按需加载你可能要在 UI 和体验上做很多妥协来弥补不成熟技术的缺陷，&lt;strong&gt;注意：不是所有的应用程序都需要追求极速的页面加载的，这种追求通常对于内容型网站会比较有意义，因此它们多采用服务端渲染，而工具／服务型应用的加载等待只要不是太过分，用户是完全可以接受的。&lt;/strong&gt;与其绕很多弯路，不如去追求更好的用户体验和引导，比如说我们可以学习一下 Slack 是怎么处理用户等待加载的。&lt;/p&gt;

&lt;p&gt;另外在现阶段，我们也可以在工程方面多注重一下控制依赖的体积，尽量避免盲目的使用大堆的第三方模块。说到体积，我估计多数人都认为 Ember 在物理体积上很庞大吧？给大家看两张截图对比：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2016/13b19cd5c2f75556aed00a9c2c397fed.png!large" title="" alt="ember-production-build"&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2016/44796cfcb2c0bbad0e8f63d343aa642c.png!large" title="" alt="webpack-production-build"&gt;&lt;/p&gt;

&lt;p&gt;上图是一个新建 Ember 项目的构建结果，下图是我自己的 Univera 脚手架（基于 webpack 的 React+Redux+SSR）的构建结果，单从数字比较 Ember 多了 1/3，但需要说明的是：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Univera 里写了一个简单的 demo，不多，业务代码撑死一百来行几乎可以忽略不计；而 Ember 因为许多 convensions over configurations，实际写业务代码时能省略的东西也不少，综合比较：平手。&lt;/li&gt;
&lt;li&gt;图中这俩架构，Univera 多了 HMR（模块热替换）和 SSR（服务端渲染）以及按需加载（不是静态资源，而是模块）的支持，总的代码量会有一些但也不是大头；上面这三个特性在干净的 Ember 架构中是没有的，加上 Ember Engines 和 Ember FastBoot 会再大一些些，但是 Ember 有更多东西是 Univera 里没有的，综合比较：平手&lt;/li&gt;
&lt;li&gt;Ember 架构里有个“癞皮狗”依赖暂时去不掉——我说的是 jQuery，其实它有时候也是很有用的；而在 Univera 里由于是我自主搭建的，压根儿就没有考虑过 jQuery 的存在，这货的体积占了 1/6 左右（使用代码行数粗略估算），所以单考虑构建后体积 Ember 吃了个亏，综合比较：Univera 胜之“不武”&lt;/li&gt;
&lt;li&gt;最重要的！Ember 是社区维护，开箱即用，持续更新，不断优化；Univera 能负责的人只有我一个，而我也是站在一堆靠谱或者不靠谱的开源软件的身上，搭建 Univera 的过程……鬼知道我都经历了些什么，而那些依赖的后续可靠性……鬼知道它们都在经历着什么，所以技术风险和稳定性保障方面，诸位自己掂量着办，综合比较：Ember 完胜&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;问题是：为什么 Ember 会这么大呢？原因是 Ember 提供了非常非常多的“宝石”，看看它的 API 文档就知道了，即使你使用 Ember 框架好几年，你依然会觉得很多东西都没有学会怎么去用；但是另外一个角度来看，有很多潜在的问题也因为 Ember 而解决在无形之中。举个例子，如果你要在 IE 9 使用 Function.prototype.bind 你必须要自己添加 es5-shims，而 Ember 内置了增强版的 Ember.run.bind 等等。&lt;/p&gt;

&lt;p&gt;不过也有很多时候我们并不需要默认提供给我们这么多东西，也许我们更希望能够自己挑选需要的部分，这是一种非常合理的诉求，只是因为从前“模块化”这件事情是 JavaScript 的痛，Ember CLI 开发的时候也没有 webpack 这样的模块处理机制，所以一直没能成为现实。&lt;/p&gt;

&lt;p&gt;Ember 社区当然不会视而不见，最新的一个 RFC 已经将此事提上了日程（实际上已经开始做了），Ember 框架将被会以独立模块的方式用 npm 提供，于是用户可以选择用什么或者不用什么，这将从根本上解决体积相对较大的问题。有兴趣的不妨读读这个 RFC，特别是末尾的附录——你可以看到体积的对比之下，Univera 缺少了太多太多东西。&lt;/p&gt;

&lt;p&gt;我还是一样的观点：体积不是最重要的，毕竟我是冲着媲美桌面应用的 SPA 去的，缩减体积并不是最重要的优化因素，而我已经做过大多数人都没有做过的尝试，我深知这里面水的深浅。这个问题在不远的将来将不会再是问题，就此搁笔，何去何从且悉听尊便。&lt;/p&gt;

&lt;p&gt;（本章待续……）&lt;/p&gt;
&lt;h2 id="Static assets management (npm / bower / x-processors / others)"&gt;Static assets management (npm / bower / x-processors / others)&lt;/h2&gt;&lt;h3 id="Plus output multiple files"&gt;Plus output multiple files&lt;/h3&gt;&lt;h2 id="Environment management"&gt;Environment management&lt;/h2&gt;&lt;h2 id="Module resolution and shims (how it helps for future, e.g. tree-shaking)"&gt;Module resolution and shims (how it helps for future, e.g. tree-shaking)&lt;/h2&gt;&lt;h2 id="Prototype extensions and why add-ons disable it by default"&gt;Prototype extensions and why add-ons disable it by default&lt;/h2&gt;&lt;h2 id="When (not) to use arrow function in CP and why should always use CP for property declarations"&gt;When (not) to use arrow function in CP and why should always use CP for property declarations&lt;/h2&gt;&lt;h2 id="CPs are lazy evaluated"&gt;CPs are lazy evaluated&lt;/h2&gt;&lt;h2 id="Why observers are not to recommended to use"&gt;Why observers are not to recommended to use&lt;/h2&gt;&lt;h2 id="Understanding data binding (When 1-way / 2-way bindings are useful)"&gt;Understanding data binding (When 1-way / 2-way bindings are useful)&lt;/h2&gt;&lt;h2 id="The art of components organization"&gt;The art of components organization&lt;/h2&gt;&lt;h3 id="Try best to not to embed children components deeply in parent component"&gt;Try best to not to embed children components deeply in parent component&lt;/h3&gt;&lt;h2 id="What is ember-runloop and why it is so important?"&gt;What is &lt;code&gt;ember-runloop&lt;/code&gt; and why it is so important?&lt;/h2&gt;&lt;h2 id="extend means put into prototype which might fool you on testing"&gt;
&lt;code&gt;extend&lt;/code&gt; means put into &lt;code&gt;prototype&lt;/code&gt; which might fool you on testing&lt;/h2&gt;</description>
      <author>nightire</author>
      <pubDate>Thu, 27 Oct 2016 23:51:53 +0800</pubDate>
      <link>https://ruby-china.org/topics/31451</link>
      <guid>https://ruby-china.org/topics/31451</guid>
    </item>
    <item>
      <title>[上海] 巧思科技诚聘后端服务工程师／架构师</title>
      <description>&lt;p&gt;我们是&lt;strong&gt;巧思科技&lt;/strong&gt;，一家正在快速成长中的互联网应用软件设计与开发公司。&lt;/p&gt;

&lt;p&gt;我们专注于一个产品和一件事情，她的名字叫“巧思”，是 &lt;em&gt;Choice&lt;/em&gt; 的谐音。&lt;/p&gt;

&lt;p&gt;简单的说，巧思是一款基于互联网的问卷调查软件。&lt;/p&gt;

&lt;p&gt;她有一个针对 Web 端的，精心设计的交互式问卷设计系统，每一位体验过的用户都交口称赞（&lt;a href="https://www.cform.io" rel="nofollow" target="_blank" title=""&gt;cform.io&lt;/a&gt; 可以把玩一下）：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2016/03a6cfe9fc4a794943b41524aa61e4c3.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;她还有一个主要针对移动设备的问卷客户端（用于回答和提交问卷）：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2016/a7b516c55e5c0978de01a24643a20242.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;此外，她还有丰富的问卷发布渠道和问卷收集与数据分析系统。巧思的目标是成为最好的问卷调查解决方案。&lt;/p&gt;

&lt;p&gt;如你所见，我们的设计能力和客户端研发能力（即前端）还是很强的，Angular／Ember／React 我们都很擅长，前端也行有余力的一直工作在前沿技术栈之下。&lt;strong&gt;但是我们在服务端还不够强大，这就是我们迫切的需要你（们）的原因！&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;巧思从一个概念发展到今天已近两年，我们是那种“认准一件事情就要把它往极致去做”的团队，然而我们还很年轻，特别是在服务端开发这块缺少老司机引路，一路摸爬滚打栽了无数坑。现在我们遇到了很多阻碍与瓶颈，比如应对高并发的问卷答题／提交／奖励领取、分布式容器部署（我们正在和 DaoCloud 亲密合作）、更高效率／集成度／自动化的研发流程体系等等……&lt;/p&gt;

&lt;p&gt;我们秉承“与合适的人做合适的事”的理念，渴望寻找能指引我们走向新高度的可靠伙伴。我们不以年龄、资历、技术背景论英雄，因此也不希望设定具体的“门槛”。以下是我们对心目中合适的伙伴的抽象描述：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;一位了解 Web 开发，互联网产品形态的工程师／架构师，精湛的技艺是你的专长，而同时你自己有这样的意识：技术只是实现目标的手段，我的价值远不止于技艺。&lt;/li&gt;
&lt;li&gt;对自己的定位是架构师，但不一定非要要求现在有这个职位或是丰富的从业经验，我们知道你只是在等待一个舞台；同时也能认同架构师首先是一个称职的工程师。&lt;/li&gt;
&lt;li&gt;擅长什么语言其实不要紧，我们认为语言只是工具，而我们真正缺少的是阅历和眼界以及动手折腾的能力。你擅长的我们乐意跟着你学，你不擅长的我们乐意和你一起学。&lt;/li&gt;
&lt;li&gt;这里是 Ruby China，所以如果你是一位 Awesome Rubist 那么我们就更开心了。我们之前的后端服务都是基于 .Net 技术栈的，坦率地说大家对外面的世界了解不够——但我们并不狭隘的认为 xxx 好／不好，我们只是希望跟时代的发展贴合的更紧密，跟我们产品的定位和需求更合拍罢了。&lt;/li&gt;
&lt;li&gt;当然除了 Ruby 之外，其他的技术栈我们也很欢迎的，比如说 Elixir（我正在学，可以陪你折腾）、Go、Rust、Scala……Whatever，只要你认为这是正确的选择，我们就支持你！&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;所谓合适的人其定义当然远不止上述那些，只不过它们相对客观且容易描述罢了。共事是一种双向选择，合适不合适其实终究是要面对面沟通才能做出判断的，我们是一个很有爱的团队（全部算上也就 20 来个人），每个人都很有趣，所以无论如何请来坐坐聊聊看吧。&lt;/p&gt;

&lt;p&gt;如果我们有缘在一起共事，我们会尽全力满足你的个人要求。坦率地说作为一个创业型的公司，很多条件其实都是可以谈的，我们至少可以保证：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;舒适的工作条件，我们刚刚买下了隔壁的区域（因为你要来呀！），如果这会儿你来的话，想怎么布置悉听尊便；硬件设备没有标配，我们相信你自己选择的才是最适合你的！（顺手拍一张我的桌子，就是想告诉你，你可以自己定制自己的舒适区）&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2016/2b2b70e337d0b066dacfd69594a7f6ff.png" title="" alt=""&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;尽全力满足你的薪酬条件及其他物质方面的需求，实实在在地说，我们现在寻找的是“定海神针”角色的伙伴，所以怎么可能在钱方面草率抠门呢，对不对？具体多少我们尊重你的意愿。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;其它的都是虚的，还是那句话，来我们这里坐坐聊聊有什么要求你尽管提。不才在下也是 Ruby China 的老人了，怎么说也不会糊弄谁或者坑谁，只不过我是擅长前端那块儿的，后端这边上了一定层次我就力不从心了。我们现在发展的态势和机遇都非常好，但是时间紧迫，没有另外一个五年给我独立摸索后端去了……从我个人角度来说，我们为你提供的是一个能让你尽情施展手脚的舞台（同时满足你的物质需求），学习型团队、没有等级牵绊、很少技术负债（我们已经打算抛弃以前不成熟的技术架构了），你可以按照你的见识和期望来构建和带领你的技术团队与环境，只要共同的目标都是为了巧思这个产品，那么你想怎么施展你的抱负我们都可以一起来。&lt;/p&gt;

&lt;p&gt;不要犹豫啦，赶紧来我们这儿玩玩吧，看看是不是符合你的心意。我们的地址是：&lt;strong&gt;上海市徐汇区龙华中路 600 号绿地中心 B 座 701 室&lt;/strong&gt;，如果你想进一步了解你个人关注的方面或是你有能充分展示自己的简历／求职信等等，可以邮件：developer@cform.io；再图省事一点的话可以在这里 &lt;a href="/nightire" class="user-mention" title="@nightire"&gt;&lt;i&gt;@&lt;/i&gt;nightire&lt;/a&gt; ，告诉我你的想法和打算。我不是 HR，我和大家是同行，我会站在你的立场理解你的所言所想，因为我们期待和你一起共事去创造我们的价值。&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;搭个顺风车，我们还需要一到两名前端工程师，我们前端这边做的东西都还蛮酷的，具体的事宜可直接联系我：dev.yufan@gmail.com，我个人希望有空直接来我们公司见见吧。&lt;/p&gt;

&lt;p&gt;另外我收到咨询的邮件，说是招聘帖里没有说清楚对于职位的具体职责要求，那我感到很抱歉！但是事出有因，为了让其他人更明确一些，我把我回答的内容也贴出来作为补充：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;招聘合适的人是一个很复杂的事情，我们的期望是找到一个能独当一面的架构师，如果问我具体的工作职责是什么我没办法说，简单粗暴的说那就是一切都是你的工作职责，因为我们只有一款产品，所有的人力物力都投入到这一件事情上，很难分出哪些事情是你的或是我的。&lt;/p&gt;

&lt;p&gt;从另外一个角度来说，也可以是“你可以改变任何事情”，我们需要的不是那种“你告诉我做什么我就照做”的人，而是“我知道如何做会更好，我们来这样做吧”的人。所以我在招聘帖里不会说我们“要求”你做什么，唯一的要求就是为我们的产品而努力。&lt;/p&gt;

&lt;p&gt;至于说非架构师以外的职位，我们当然也需要。这里要特别说明的是我们会先把重点放在架构师上，因为他的到来将决定我们未来的技术走向，有了这个前提之后我们才更清楚其它职位的具体招聘要求。&lt;/p&gt;

&lt;p&gt;总之呢，如果你也对我们做的事情感兴趣的话，我还是建议直接来我们公司大家一起聊一下，和我们的 CEO 等核心人员彼此交流一下会更利于你明确你的问题。其实连我自己都是这样进入公司的，最开始我是因为朋友介绍我来为巧思做一个前端技术选型的咨询，一来二去接触的多了之后大家发现志同道合，于是一起干就成了十分自然的选择。&lt;/p&gt;
&lt;/blockquote&gt;</description>
      <author>nightire</author>
      <pubDate>Thu, 26 May 2016 21:13:01 +0800</pubDate>
      <link>https://ruby-china.org/topics/30136</link>
      <guid>https://ruby-china.org/topics/30136</guid>
    </item>
    <item>
      <title>(番外篇) 同构化的 React + Redux 服务端渲染</title>
      <description>&lt;p&gt;前一篇写于一个月前，那时候我开始准备同构化的应用程序架构也就一两个星期的时间，经验并不足，所以尽管也写了挺长一篇但是留下不少坑，很多细节也没有讲得很清楚。&lt;/p&gt;

&lt;p&gt;当时我承诺以后会补充，并且把完整的架构开源分享出来（上一篇公开的那个 repo 是实验性质的，有很多瑕疵），经过一个多月的实际产品开发，我修补和完善了许多细节并重新整理一个 repo，那么这一篇就是来兑现承诺的。当然我不会把冷饭再炒一遍，上次讲过的代码细节就不再重复了，这次主要是总结一些遗漏的细节以及针对实际场景的一些考量与决策分析，会把重点放在 Redux 以及 Data &amp;lt;-&amp;gt; UI 的交互上面。相比于上一篇纯粹是理论性质的阐述，这一篇的内容应该对现实开发会更有意义。&lt;/p&gt;

&lt;p&gt;先把 repo 地址放在前面，需要参考代码的地方都在这上面：&lt;a href="https://github.com/very-geek/univera" rel="nofollow" target="_blank"&gt;https://github.com/very-geek/univera&lt;/a&gt; 克隆下来，运行 &lt;code&gt;npm install&lt;/code&gt; 然后 &lt;code&gt;npm start&lt;/code&gt; 就行；如果需要针对服务端调试就 &lt;code&gt;npm run debug&lt;/code&gt;；如果要看发行版本效果就 &lt;code&gt;npm run stage&lt;/code&gt;……具体的请看 &lt;code&gt;package.json&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;值得一提的是这个 repo 的提交历史我整理的非常小心仔细，如果你关注所有细节的演进过程，不妨顺着历史记录看。在过程中我针对一些技术选型写了一些 demo，然而为了保持这个 repo 的“干净”，在继续演进之后我把它们都删掉了（其实应该把分支都留下来的，以后会注意这一点）。&lt;/p&gt;

&lt;p&gt;也有人问我：“你不是鼓吹前后分离 SPA＋API 的吗，怎么又玩儿回去了？”其实问出这种问题恰恰说明他既不了解“分”也不了解“合”，不解释，直接看：&lt;a href="http://tomdale.net/2015/02/youre-missing-the-point-of-server-side-rendered-javascript-apps/" rel="nofollow" target="_blank"&gt;http://tomdale.net/2015/02/youre-missing-the-point-of-server-side-rendered-javascript-apps/&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="从 Express 到 Koa"&gt;从 Express 到 Koa&lt;/h2&gt;
&lt;p&gt;上一篇的实验是用 Express 做 server 的，这一次则改用了 Koa 并且直接走了 v2 的路子。对我来说唯一的原因就是更喜欢 Koa v2 对于 middleware 的设计，详细的阐述我在不久前发过一篇文章：&lt;a href="https://segmentfault.com/a/1190000004883199" rel="nofollow" target="_blank"&gt;https://segmentfault.com/a/1190000004883199&lt;/a&gt; ，对 Koa 感兴趣的不妨先转道一观。&lt;/p&gt;

&lt;p&gt;不过这么一转也给我带来了一些麻烦，主要是在 Express 下非常通用的 middleware 放在 Koa 下就需要针对 async/await 式的写法重新封装了，难度倒是不大，在 &lt;a href="https://github.com/very-geek/univera/tree/develop/server/modules" rel="nofollow" target="_blank" title=""&gt;server/modules&lt;/a&gt; 有许多例子可以参考。&lt;/p&gt;

&lt;p&gt;那么用下来的感受呢？优势在于 Koa 的中间件设计更加灵活，async/await 的语法也会让处理异步的过程变得更“干净”，虽说相应的又要适应一种异步模型，但这也是迟早的事情啦。说到异步，以前我也是对 Promise／Generator／Async Function 这些花样感到无比头大，后来看了这个视频（N 遍）之后才算是打通了这条经脉：&lt;a href="https://www.youtube.com/watch?v=lil4YCCXRYc" rel="nofollow" target="_blank"&gt;https://www.youtube.com/watch?v=lil4YCCXRYc&lt;/a&gt; 。&lt;/p&gt;

&lt;p&gt;我推荐这个视频不只是因为 JavaScript 或者 Async Function，最重要的是 Jafar Husain 讲得是真好，把异步的概念解释的既生动又透彻，值得一看。&lt;/p&gt;

&lt;p&gt;Koa 的缺点就是周边的支持了，尽管常用的中间件也不少，但是稍微偏门点的轮子要么只有 Express 牌的，要么就是型号不兼容 v2。所以真要在生产中使用 Koa，那得先把它的 Middleware 机制玩儿熟了，这样就算找不到合适的也可以拿别家的过来改造一下，基本指导思想很简单：&lt;u&gt;把 callback 改成 promise，然后在 async function 里 await 它了。&lt;/u&gt;相比 Generator 来说还是 Async Function 更具实用价值，因为 Generator 并非就是为了“异步”而生的（尽管它能做到），Generator 需要对应的 runner 配合才适合处理异步场景，而 Async Function 就直接了当多了（这俩的对比建议还是看上面那个视频，图文并茂附带动效比我这干嚼文字更直观）。&lt;/p&gt;
&lt;h2 id="关于 Webpack 的二三事"&gt;关于 Webpack 的二三事&lt;/h2&gt;
&lt;p&gt;如果你认为 Webpack 是一个类似 Rake 的 task runner 或者类似于 Assets Pipeline 的 assets building tool 那你就错了，这些功能其实都是 Webpack 的“副作用”，它的真正核心 feature 是一个模块解析器，而且它可以是一个“万能的”模块解析器。通俗地讲就是可以让你用各种可能的机制来加载各种可能的资源：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;各种可能的机制&lt;/strong&gt;，即在特定的 context 下用合法的引用方式来导入资源。比如说在 JavaScript 文件里使用 &lt;code&gt;require()&lt;/code&gt; 或是 &lt;code&gt;import ... from ...&lt;/code&gt; 就是可以的；或者在 CSS 文件里用 &lt;code&gt;@import ...&lt;/code&gt; 或者 &lt;code&gt;composes: selector from ...&lt;/code&gt; 或者 &lt;code&gt;url(...)&lt;/code&gt; 等等也是可以的；&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;各种可能的资源&lt;/strong&gt;，HTML、CSS／Less／Sass／PostCSS、JavaScript、TXT／JSON／YAML／XML、字体、图片、音频／视频……反正你做 web 产品能用上的资源它都能给你加载了，加载的方法是上一条中的某个／些或全部——这取决于你声明使用的 loader 如何；&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;以上两点配合使用可以带来无穷的可能性，你可能因此而改变很多传统的开发手段，但另一方面也对同构与服务端渲染带来的许多棘手的挑战。做这个架构的过程中其实我花在 Webpack 身上的时间非常多，至少三成吧。倒不是因为 Webpack 不好（当然这货的文档是需要大力吐槽的 🙄），反而是很多时候 Webpack 能为客户端开发做到的事情太多了，一旦同构就会导致一些机制没法在服务端渲染的过程中“即插即用”，因此你得找出在服务端的解决方案。在这方面我至今做得都不够完美，这里面也有一些原因是受到客观的现实因素所制约的，下面我就说一说这些细节：&lt;/p&gt;
&lt;h3 id="JavaScript 模块的解析"&gt;JavaScript 模块的解析&lt;/h3&gt;
&lt;p&gt;99% 的 JavaScript 模块解析在这个架构里都没什么好讲的，该怎么写怎么写。经验分享如下：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;完全的 ES2015 模块语法支持&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;我属于“极端派”，要么不用要么全用。在客户端，只要 Webpack 用 babel-loader 就可以做到，配置文件写成 &lt;code&gt;webpack.config.babel.js&lt;/code&gt; 就可以直接用 ES2015 模块语法（重点是 &lt;code&gt;.babel&lt;/code&gt; 部分，其他随意）。针对 &lt;code&gt;babel-loader&lt;/code&gt; 的配置建议写成 &lt;code&gt;.babelrc&lt;/code&gt; 方便前后通用（或者写进 &lt;code&gt;package.json&lt;/code&gt;）。&lt;/p&gt;

&lt;p&gt;在服务端，你需要一个入口文件（见：&lt;a href="https://github.com/very-geek/univera/blob/develop/server%2Findex.js" rel="nofollow" target="_blank" title=""&gt;server/index.js&lt;/a&gt;）预先导入 &lt;code&gt;babel-register&lt;/code&gt;；如果要使用更“前卫”的语法，如 &lt;u&gt;async/await&lt;/u&gt; 还需要导入 &lt;code&gt;babel-polyfill&lt;/code&gt;，然后才能导入真正的服务端代码，此后就可以放心写 ES2015 了。&lt;/p&gt;

&lt;p&gt;极个别情况下还需要 &lt;code&gt;require&lt;/code&gt; 语法，主要是因为 Webpack 的 &lt;code&gt;require&lt;/code&gt; 扩展了 CommonJS 的同名函数，它有一些特别的用处（比如说用来按需加载的 &lt;code&gt;require.ensure&lt;/code&gt;）；另外就是 &lt;code&gt;require&lt;/code&gt; 是每次执行都会重新导入模块而 &lt;code&gt;import&lt;/code&gt; 则是会 cache 的，所以在特殊场合下——比如代码热加载／替换时还得用。Webpack v2 会原生支持 &lt;code&gt;import&lt;/code&gt; 并提供相应的异步加载机制，这样会使得写法趋近统一，但由于目前还在 RC 阶段所以我并没有用（实在是怕了它的文档大坑 😰）。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;小坑一个：Babel v6 之后为了完全靠拢 ES 标准，&lt;a href="http://stackoverflow.com/a/33705077/1582747" rel="nofollow" target="_blank" title=""&gt;去除了 &lt;code&gt;export default ...&lt;/code&gt; 语法转换时针对 CommonJS 的兼容性处理&lt;/a&gt;，因此不得不用 &lt;code&gt;require(...).default&lt;/code&gt; 的方式来导入模块。解决办法见：&lt;a href="https://github.com/very-geek/univera/blob/develop/.babelrc#L4" rel="nofollow" target="_blank" title=""&gt;&lt;u&gt;Line 4 @ .babelrc&lt;/u&gt;&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;一些特殊的模块解析场景&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;不是所有的模块都需要／值得 &lt;code&gt;import&lt;/code&gt; 或 &lt;code&gt;require&lt;/code&gt;，以下是几种很常见的情形：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;全局通用／替代原生同名对象：比如说我倾向用 Bluebird 来代替原生的 &lt;code&gt;Promise&lt;/code&gt;（绝对值得），那么在客户端可以使用 &lt;a href="https://github.com/very-geek/univera/blob/develop/config%2Fwebpack.babel.js#L50" rel="nofollow" target="_blank" title=""&gt;&lt;u&gt;webpack.ProvidePlugin&lt;/u&gt;&lt;/a&gt; 来解决；服务端的话可以 &lt;code&gt;global.Promise = require('bluebird')&lt;/code&gt;，不过我觉得没必要，善用 Bluebird 的 &lt;code&gt;promisify&lt;/code&gt; 方法就够了。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;适合无需显式导入的模块：比如说我们做 motion 用的 Velocity，这东西到处用，如果每次都 &lt;code&gt;import&lt;/code&gt; 很烦人呐（而且它还有一个额外的 ui-pack）。大多数 repo 都手动把它绑定在 &lt;code&gt;window&lt;/code&gt; 下面全局调用，那其实 Webpack 也可以让你&lt;a href="https://github.com/very-geek/univera/blob/develop/config%2Fwebpack.babel.js#L14" rel="nofollow" target="_blank" title=""&gt;这样来导入&lt;/a&gt;。那么只要是那种加载时自动会往特定对象身上绑定的（比如 &lt;code&gt;window&lt;/code&gt; 或 &lt;code&gt;jQuery&lt;/code&gt;）都可以这么做的。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;为便于调试暴露至全局的模块（可以直接在 &lt;u&gt;console&lt;/u&gt; 里访问到）：和 2. 类似的，除了人工绑定这种笨办法之外，还有一个 &lt;a href="https://github.com/very-geek/univera/blob/develop/config%2Fwebpack.babel.js#L45" rel="nofollow" target="_blank" title=""&gt;expose-loader 更适合这项任务&lt;/a&gt;。和 &lt;u&gt;webpack.ProvidePlugin&lt;/u&gt; 的区别就是它是暴露在浏览器全局的，便于调试用。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;值得一试：&lt;a href="https://github.com/very-geek/univera/blob/develop/.babelrc#L6-L9" rel="nofollow" target="_blank" title=""&gt;这几行的 babel plugins&lt;/a&gt; 以及&lt;a href="https://github.com/very-geek/univera/blob/develop/config%2Fwebpack.babel.js#L26-L28" rel="nofollow" target="_blank" title=""&gt;这几行的 Webpack 配置&lt;/a&gt;都有助于优化导入模块时的写法，或缩减相对路径，或简化模块命名；协调团队代码约定时很有用处。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="样式（及相关资源）模块的解析"&gt;样式（及相关资源）模块的解析&lt;/h3&gt;
&lt;p&gt;样式是同构 SSR 里的一个重头戏。在过去数年中，我们已经接受了外联样式文件的主流处理方式，然而若想获得极速的初始加载体验，内联样式其实是更佳的方案。过去我们讨厌内联样式的主要原因是不好写也不好维护，但假如这方面有技术手段可以解决还会难以接受吗？Google 的首页到今天还是以内联样式为主的，减少请求数固然是最直接的原因，同时 SSR 处理的响应也是方便做 cache 的。&lt;/p&gt;

&lt;p&gt;当然了，不是说所有的样式都内联最好。SSR 的应用场景是：如果有页面请求（如浏览器刷新）-&amp;gt; 判断当前 path -&amp;gt; 生成需要的内容 -&amp;gt; 返回响应；一旦客户端接手，由于客户端路由的存在（假设你是有客户端路由的，否则就纯粹是传统的 SSR 了），接下来的 URLs 变化就不会产生页面请求了（只有可能的 APIs 请求或按需加载的静态资源）。因此，只有直接的页面请求需要内联样式（为了渲染时避免 FOUC），而且只需要那些必不可缺的（这个概念叫做 &lt;a href="https://medium.com/@luisvieira_gmr/understanding-the-critical-rendering-path-rendering-pages-in-1-second-735c6e45b47a#.aq17nljzh" rel="nofollow" target="_blank" title=""&gt;Critical Rendering Path&lt;/a&gt;，CSS 是其中的重点处理部分）为最佳。&lt;/p&gt;

&lt;p&gt;直到大约一周以前我们在产品开发中一直使用一个叫做 Radium 的工具，这个东西允许你用 JavaScript 来编写 CSS（写法极其类似，但最终导出的是 JavaScript Object），因为 Radium 的样式终究是由 JavaScript 对象转换而来的，所以内联化非常简单，维护也不是问题，同时对 SSR 的支持非常好。我们一直很满意……直到我们发现它对动画性能有非常大的影响。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;动画的触发和流向依赖状态，状态变更会导致组件重新渲染，而应用了 Radium 的组件刷新会重新计算样式，新的样式（即使规则未改）应用会操作 DOM——这是连锁反应。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;所以我们推倒了样式方案（我必须承认现在做前端的确很折腾，有人会问：值得吗？其实这个答案也是连锁反应，有需求才会有折腾，后面我会谈到这些方面），转而采用更贴近未来趋势的 cssnext，也就是下一代的 css4。cssnext 现在是 PostCSS 的组成部分，PostCSS 你可以把它看作是样式圈里的 Babel，也就是一个语法转换器。和 Radium 即时计算样式不同，PostCSS 是集成在 Webpack 环境中的，它只在 building 时转换并生成最终样式，并且不会因为组件渲染或 DOM 操作而改变（除非你改的就是样式）。缺点在于由于是标准的样式体系，所以它不能解决内联样式不好写和不好维护的问题。我注意到目前有一些工具可以用于在 SSR 过程中抽取必要的样式规则进行内联化，但看起来都不够成熟，暂时没敢碰……看来内联部分说不得还得靠自己写。目前诸位看到的这个 repo 没有处理内联样式的机制，因为我还在踌躇之中，也因此，我还没有使用 Webpack 进行样式的抽取打包并最终转成外联样式表文件，因为这是解决内联样式之后的事情（而且也很简单）。&lt;/p&gt;

&lt;p&gt;关于 PostCSS 的所有配置都在 Webpack 配置文件里，这里唯一要说的就是 CSS Modules 的部分。CSS Modules 使得样式可以针对具体的组件编写而不用考虑命名冲突或复杂的选择符设计问题，本质上会由 &lt;u&gt;css-loader&lt;/u&gt; 内置的组件化支持把样式命名转化为随机名称，然后生成一个 map 供你引用。于是你可以在 JavaScript 中 &lt;code&gt;import&lt;/code&gt; 样式表然后动态使用。为了在 SSR 过程中也能得到一模一样的名称映射，需要在服务端加载 &lt;em&gt;css_module_require_hook&lt;/em&gt; 模块，这是因为 CommonJS 并不懂如何处理 &lt;code&gt;.css&lt;/code&gt; 文件的模块。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;传统的 SSR 并不能直接用原生的模块导入来引用样式表文件，你只能读取文件内容然后解析再生成最终的文本内容拼接到 response body 中。所以本质上 &lt;em&gt;css_module_require_hook&lt;/em&gt; 就像是 Webpack css-loader 那样，理解 &lt;code&gt;require('*.css')&lt;/code&gt;，解析它然后把它当作 JavaScript 代码来处理——这就是同构得以实现的基本要求。&lt;/p&gt;

&lt;p&gt;因此，抽取影响 Critical Rendering Path 的样式部分然后内联它们也是完全可能的，但是只有选择符名称映射的对象还不够，还得要解析所有的规则声明然后匹配 CRP 里对应的 HTML 结构再插入……这个过程相当繁琐，如果硬生生的塞进 SSR 的处理过程中我不确定值不值得或者是否正确。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;CSS 内部引用的其它资源，诸如字体、图片等都由 Webpack 负责解析了。在开发过程中，这些解析的内容会由 &lt;em&gt;webpack_dev_middleware&lt;/em&gt; 处理成内存式的文件系统映射，拦截实际的路径请求然后把对应的内容返回给你；最终构建时 Webpack 会把它们按照指定的规则写入硬盘的路径，或者把对应的路径替换为指定的规则（如：CDN），因此你不必担心如何处理它们的问题。&lt;/p&gt;

&lt;p&gt;留存的一个问题是，由于 Webpack 也允许你在 JavaScript 模块中引用各种静态资源，所以 SSR 在渲染这些组件时遇到这些引入会无能为力（除了样式我们已经解决了）。好比说你想在 SSR 时就预先计算一个 &lt;code&gt;&amp;lt;img/&amp;gt;&lt;/code&gt; 的宽高尺寸就会遇到麻烦了。不过解决的思路其实也和 CSS Modules 那个部分是类似的，拦截 &lt;code&gt;require&lt;/code&gt;，解析，然后当成 JavaScript 来使用。比如说图片，拦截之后可以进行某种编码，或者用 Headless Browser“伪加载”了，获得需要的信息后拼装成 Object 传递给组件，组件内部自然有动态修改 &lt;code&gt;&amp;lt;img/&amp;gt;&lt;/code&gt; 的逻辑（别忘了它们是要同构的），于是服务端和客户端的行为就一致了。&lt;/p&gt;

&lt;p&gt;我们的产品没有这方面的诉求所以我没有做相应的实现，不过未来我们有计划要做对中文字体的子集抽取，大概也会经历类似上面的一个过程吧。这一段的内容略微有些生涩，大概在现实中不会有很多团队需要自己去做这些事情吧，毕竟太繁琐。面向现实考虑，我推荐一个轮子吧：&lt;a href="https://github.com/halt-hammerzeit/webpack-isomorphic-tools" rel="nofollow" target="_blank" title=""&gt;webpack-isomorphic-tools&lt;/a&gt;，这个东西的作用就是帮你把 Webpack 为客户端处理的各种模块解析对应在服务端的实现都做好了，你按照它的说明写点代码整合到自己的架构中即可，比较快捷实用，不过也是蛮新的一个项目里面有坑几何我就没法儿说了。&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;(Redux 的坑先留着，对一些场景的解决方案我有了一些新的想法需要实践检验一下，有了结果我再来填）&lt;/p&gt;</description>
      <author>nightire</author>
      <pubDate>Sat, 23 Apr 2016 04:53:53 +0800</pubDate>
      <link>https://ruby-china.org/topics/29835</link>
      <guid>https://ruby-china.org/topics/29835</guid>
    </item>
    <item>
      <title>同构化的 React + Redux 服务端渲染</title>
      <description>&lt;p&gt;我们的产品有一个面向 C 端且主要运行于移动设备的部分，在我接手之前是 Meteor＋Blaze＋.Net API 的架构。除了 .Net API 是历史遗留问题（主服务）姑且不谈，Meteor 其实也算满前卫的东西了。然而之前的实现并没有利用到 Meteor 的精髓部分：数据获取主要是通过 API，虽然传递给 Blaze 的过程借用了 Meteor 的同构机制，但由于 Meteor 没有负责数据模型层，所以纯粹的通信仅仅是利用了 WebSocket 而已。但是这个部分的应用对即时通信的需求并不多，在上线运行一段时间后发现真正的瓶颈还在于客户端动态渲染上（有些时候会需要渲染相当大的页面），而且移动端首次访问需要加载一堆未加充分利用的 Meteor 代码……总之效果并没有想象中那么好。&lt;/p&gt;

&lt;p&gt;之后我受命重构这个应用，并且上头希望用 React，本来我个人觉得 React 应对客户端动态渲染已经足够好了，但是首次访问总是需要加载 React 的，于是上头的意思是越快越好……咳咳，大家懂得～所以就得试一试 SSR（服务端渲染）。我还是第一次搞这个东西，过程着实艰辛，不过苦水就免吐了，直接进入正题。&lt;/p&gt;
&lt;h2 id="SSR 是如何工作的?"&gt;SSR 是如何工作的？&lt;/h2&gt;
&lt;p&gt;要点如下：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;你有一套组件——不管你写多少个组件，其相互应用关系最终就是一棵组件树，在这里组件的具体数量并不重要，也不需要关心，所以我就叫它（们）一套组件了；&lt;/li&gt;
&lt;li&gt;当用户请求一个 URL，首先由 Node 端响应然后进过一系列步骤（详见后文）最终返回一段 HTML 文本给浏览器渲染；&lt;/li&gt;
&lt;li&gt;这段 HTML 和你用相同的组件在客户端渲染出来的结果是一模一样的（必须保证这一点，否则会报错），当然其中也包含了客户端所需要的 JavaScript 文件；&lt;/li&gt;
&lt;li&gt;当客户端的 JavaScript 加载完毕会执行和之前相同的渲染过程（区别在于渲染用的函数不同，见后文），然而用户不会察觉到这一点，因为他们早已经看到了静态渲染的页面；&lt;/li&gt;
&lt;li&gt;之后的一切过程由客户端接管，不再有 SSR 什么事儿了。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="同构在其中如何体现？"&gt;同构在其中如何体现？&lt;/h2&gt;
&lt;p&gt;我在这里讲的同构其实是狭义的，和真正的 Universal Application 还有不小的差距。不过从概念上来说，同构就是相同的代码可以同时运行于服务端与客户端。只不过我们没有去做 100% 的同构，只有部分而已。&lt;/p&gt;

&lt;p&gt;首先，组件是可以同构的。前提是组件不要有独立的私有状态，因为 SSR 很难保证重现这些私有状态。试想，如果某组件定义了一个私有状态，其值是依赖于浏览器环境的（比如：&lt;code&gt;window.navigator.language&lt;/code&gt;）那么服务端执行它是会有问题的——当然你可以尝试模拟客户端环境，但是极其困难甚至某些情形是不可能或不可控的。这个大前提就意味着一些东西：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;用于 SSR 的组件树必须能通过服务端获取到初始渲染时需要的一切状态，如果确实有不可避免的依赖客户端的私有状态，你得想办法让它顺利渡过 SSR 的过程并在客户端渲染时重新初始化它——这是可行的。&lt;/li&gt;
&lt;li&gt;为了便于服务端为 SSR 准备初始化的状态，很显然，这里用单一状态树是最合理的，因此 Redux 就成了上上之选。Flux 当然也不是不行，就是得考虑一下如何规划数据传递。实际上用了 Redux 就可以避免一切私有状态所带来的影响。这里并不是说你不能为组件定义私有状态，只是说 SSR 为这些私有状态准备初始化状态会更方便更安全。（比方说 SSR 初始化一个默认值，客户端接手后用实际值替换——如果你没有全局状态树的话，你需要深入到相应的组件层级为其初始化默认值；反之私有状态可以通过检查上层传递下来的数据进行初始化或者使用 Context／Container 等等方式——总之 SSR 不需要深入只需要准备全局初始状态）。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Redux 里的 store 也是可以同构的而且在这里必须同构，因为 SSR 和 CSR（客户端渲染）需要同样的初始状态。SSR 可以直接把初始状态写入在返回的 HTML 文本中，客户端接手之后可以清理它或者干脆 let it be。&lt;/p&gt;

&lt;p&gt;既然 store 可以同构，那么相应的 reducers 还有 actions 也是可以同构的。在这个层面上同事们有着不小的困惑，在经过一系列讨论之后我意识到根本的原因在于大家对于 Redux 的理解上，大家表达的共同疑惑在于：&lt;strong&gt;业务逻辑的处理怎么可能前后通用&lt;/strong&gt;？&lt;/p&gt;

&lt;p&gt;我不打算在此时详细解释，就先说明一个关键点：Redux 负责的是数据结构的表征而不是具体的业务逻辑。通常我们会认为数据结构的表征就是业务逻辑的一部分，那是因为常规的处理模式的确是把业务逻辑的处理与数据结构的变迁混合在一起的。&lt;/p&gt;

&lt;p&gt;如果你把应用程序的数据库看作是一个单一状态树（实际上是完全可行的，只需要结构上的转换，但实际中我们并不需要整个数据库作为应用程序的状态树，因为状态是可以变迁的，状态树只要满足当前状态下 UI 的需要即可，而且现实中的状态树还可能会包含数据库里没有的数据，比如说不需要持久化的 UI 特定的状态），那么某个业务逻辑的执行无非就是要更新这颗树，只不过这颗树有其特定的形态，而我们的 raw data 往往不能满足这种形态，因此才需要代码逻辑来进行必要的前置处理。这部分的代码逻辑是前后无法通用的，但是之后的数据形态前后却是一致的。&lt;/p&gt;

&lt;p&gt;Redux 所体现的就是纯粹的数据形态及其变更（这是前后可以一致的地方），至于你怎么变更（这是前后无法一致的地方）并不是 Redux 关心的事情，只是由于我们习惯了无论前后都把这两件事情一起做，所以才会觉得“怎么可能？”。&lt;/p&gt;

&lt;p&gt;之后我可能会单独就此写一些东西，但本篇不涉及具体的业务逻辑层面，手头也没有现成的代码示例，所以先就此打住——你只需要知道这是完全可能的，也就是你写一份 reducers + actions 就可以同时应用于前后两端。&lt;/p&gt;

&lt;p&gt;另外，路由也是可以同构并且最好同构，能做，何乐而不为呢，对吧？&lt;/p&gt;
&lt;h2 id="Show me the code"&gt;Show me the code&lt;/h2&gt;
&lt;p&gt;OK，下面说一说重点的代码部分。前提是你有一套组件了，我不关心它们具体如何，只假设入口是 &lt;code&gt;&amp;lt;Home/&amp;gt;&lt;/code&gt;。&lt;/p&gt;
&lt;h3 id="HTTP Server"&gt;HTTP Server&lt;/h3&gt;
&lt;p&gt;既然是 SSR 那就没有什么分离一说了，只是我们想用 webpack，所以开发环境下是 &lt;code&gt;Node HTTP Server + WebpackDevMiddleware&lt;/code&gt; 的组合，部署时候去掉 webpack 的部分即可：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// server/server.js&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Router&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;webpack&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;webpack&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../webpack.config.babel.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;bootup&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./bootup&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Main&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../common/components/Main&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ssr&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Router&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODE_ENV&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;compiler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;webpack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;webpack-dev-middleware&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="nx"&gt;compiler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;publicPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;publicPath&lt;/span&gt;&lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;ssr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;bootup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Main&lt;/span&gt;&lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;ssr&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;PORT&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;1337&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;server&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;address&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Listening on -&amp;gt; http://localhost:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;port&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上面的代码基本都是大路货，只是我们匹配了所有的请求，统统转入 &lt;code&gt;bootup&lt;/code&gt; 做最后的渲染处理。此时你看到的 &lt;code&gt;bootup&lt;/code&gt; 只是最初级形态，随着架构的升级它也会逐渐跟着适应，由于此处是 SSR 的核心逻辑，所以我们从最初的形态开始逐步理解。&lt;/p&gt;
&lt;h3 id="Basic SSR"&gt;Basic SSR&lt;/h3&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// server/bootup.js&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;renderToString&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-dom/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;generatePage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SSR Demo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/title&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;div id="root"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/div&amp;gt;
    &amp;lt;script src="/assets/client.js"&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script src="/assets/vendor.js"&amp;gt;&amp;lt;/script&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;components&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;renderToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;components&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;generatePage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;renderToString&lt;/code&gt; 把组件渲染为字符串（HTML），等价于编译模版，然后我们用 &lt;code&gt;generatePage&lt;/code&gt; 函数把它插入到 &lt;code&gt;div#root&lt;/code&gt; 里。这个过程和以下的客户端代码干的是类似的事情：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// client/index.js&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-dom&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Main&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../common/components/Main&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Main&lt;/span&gt;&lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;root&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;实际上 &lt;code&gt;client/index.js&lt;/code&gt; 就是 &lt;code&gt;&amp;lt;script src="/assets/client.js"&amp;gt;&amp;lt;/script&amp;gt;&lt;/code&gt; 内容，webpack 用在了这里来生成最终的客户端脚本文件。所以一次完整的 SSR 会有两次组件的渲染过程，先是服务端 --&amp;gt; 生成静态 HTML --&amp;gt; 浏览器快速呈现 --&amp;gt; 加载客户端脚本 --&amp;gt; 执行再次渲染一遍——但其实第二次渲染几乎等于没做，因为 SSR 的时候生成的标签里已经有了 &lt;code&gt;data-react-checksum&lt;/code&gt;，由于同构的关系，CSR 渲染后的 &lt;code&gt;data-react-checksum&lt;/code&gt; 是一模一样的（如果不一样会报错的，时刻留意开发者工具控制台），因此不会产生任何 DOM 的修改（除了一件事：CSR 会追加必要的事件处理函数）。我观测了 Recalculate Style 事件，客户端渲染花费的时间恒定在 &lt;code&gt;0ms&lt;/code&gt;，完美～&lt;/p&gt;

&lt;p&gt;即使是如此简单的一个 Demo，你已经可以获得极速的渲染体验并可以无缝过渡到客户端应用程序，plus，毫无障碍的 SEO 适应性。唯一的瓶颈可能只剩下客户端的网速了，我用 Chrome 模拟了 3G／4G 的网络加载速度，有 SSR 的时候几乎可以忽略不计（当然也是因为渲染出来的 HTML 并不大，但就算大也大不到哪儿去的），没有 SSR 的时候就得取决于客户端最终的脚本文件有多大了。&lt;/p&gt;

&lt;p&gt;这样看来 React 的服务端渲染似乎也蛮简单的，不过一个 "Hello World" 的 Demo 顶个屁用，接下来我们要看看整合了一切需要用上的技术之后会是何种情形吧。&lt;/p&gt;
&lt;h3 id="Give me some color to see see?"&gt;Give me some color to see see?&lt;/h3&gt;
&lt;p&gt;样式方面，SSR 是不能用外链样式表文件的，因为会有短暂的“裸屏”。所以你需要生成样式文本，就和 HTML 文本一样（哪怕是指满足首屏需要也可以）一起填充到 SSR 的返回内容中。原理是很简单的，具体做法就多了去了。比如说你用了 Sass/Less 之类的，那就要在服务端先编译了然后插入进去；又或者你可以用 CSS Module，配合 &lt;a href="https://github.com/JedWatson/classnames" rel="nofollow" target="_blank" title=""&gt;classnames&lt;/a&gt; 直接在组件里面编写（于是我也释怀了在 JavaScript 里编写样式这件事情）。代码范例我就省略了，不过留意前面的 &lt;code&gt;generatePage&lt;/code&gt; 函数，它的第二个参数就是用来传递所有需要 SSR 填充的东西的，你当然也可以用模版引擎等东西简化这个函数的实现，不过我觉得 ES2015 的 String Template 已经足够好用了。&lt;/p&gt;
&lt;h3 id="React Hot Module Replacement (optional)"&gt;React Hot Module Replacement (optional)&lt;/h3&gt;
&lt;p&gt;热代码替换也是现在很火热的话题，但是牵涉到 React 以后，这里面水就深了……在我动手加上它之前，我特意边跑边读了好几个 Google 到的 demo projects，各式各样的写法五花八门，真是越看越糊涂。&lt;/p&gt;

&lt;p&gt;后来看到了“始作俑者”Dan Abramov &lt;a href="https://medium.com/@dan_abramov/hot-reloading-in-react-1140438583bf#.r1w679vng" rel="nofollow" target="_blank" title=""&gt;最近的一篇文章&lt;/a&gt;才闹明白：原来之前几乎都是瞎折腾（指的是他最早开始的实践 react-hot-loader 以及后续基于此的各种实验性项目）。当然，这个项目并非没有价值，只不过它的价值是发现“此路不灵”，需要另觅他途。我对他未来打算使用的基于 ES2015 Proxy 的方案非常感兴趣，但是目前就不要趟这浑水了，老老实实的用更通用的 webpackHotMiddleware 吧。&lt;/p&gt;

&lt;p&gt;增加模块热替换的方法如下（原理就不讲了，webpack 官网有，Google 也能找到不少资料）：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;添加中间件&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// server/server.js&lt;/span&gt;

&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODE_ENV&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;production&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;compiler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;webpack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;webpack-dev-middleware&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="nx"&gt;compiler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;publicPath&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;publicPath&lt;/span&gt;&lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;webpack-hot-middleware&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="nx"&gt;compiler&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;配置 webpack。要注意，如果你有多个 entries，每一个都需要追加一样的入口，如下所示：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// webpack.config.babel.js&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;webpack&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;webpack&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./client/index.jsx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;webpack-hot-middleware/client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;vendor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-dom&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;webpack-hot-middleware/client&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;plugins&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;webpack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;optimize&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;CommonsChunkPlugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vendor&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;vendor.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;webpack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;optimize&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;OccurrenceOrderPlugin&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;webpack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;HotModuleReplacementPlugin&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;webpack&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;NoErrorsPlugin&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我没把配置写全，以上只有必须要有和推荐要有的部分，其他的部分按自己的要求随便写就是了。&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;在需要的地方描述如何进行热替换，之前的 Demo 需要这样：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// client/index.js&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ReactDOM&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-dom&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;render&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Main&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../common/components/Main&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;ReactDOM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Main&lt;/span&gt;&lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;root&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../common/components/Main&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;Main/&amp;gt;&lt;/code&gt; 组件没再用 ES2015 模块是因为我们需要每次加载“新鲜”的模块，Babel Loader 把 &lt;code&gt;import&lt;/code&gt;  转换成了 &lt;code&gt;require&lt;/code&gt; 于是你就没机会了……这个问题将在 webpack v2 得到解决，届时我们禁用 Babel 的模块转换，webpack 会对所有的 &lt;code&gt;import&lt;/code&gt; 做即时绑定处理；另外一个原因是 ES2015 模块必须得在文件顶端声明，将来有了支持异步的 System Loader，那么这部分则可以变为：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;render&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../common/components/Main&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;Main&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;ReactDOM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Main&lt;/span&gt;&lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rootElement&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;code&gt;module&lt;/code&gt; 对象是 webpackHotMiddleware 自动暴露出来的，由于我们只在开发环境才用所以无需在这里做环境判断。将来要想在生产环境也用那就连改都不要改了&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;setTimeout(render)&lt;/code&gt; 是为了避免 &lt;code&gt;render&lt;/code&gt; 抛出异常会打断热替换的正常工作，为了得到更好的反馈，我们可以用 redbox-react 一类的工具重新封装一下 &lt;code&gt;render&lt;/code&gt; 方法&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// client/index.js&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ReactDOM&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-dom&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rootElement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;root&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;render&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Main&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../common/components/Main&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;ReactDOM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Main&lt;/span&gt;&lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rootElement&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;renderNormally&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;renderException&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RedBox&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;redbox-react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;ReactDOM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;RedBox&lt;/span&gt; &lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="sr"&gt;/&amp;gt;, rootElement&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="err"&gt;;
&lt;/span&gt;  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nx"&gt;render&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;renderNormally&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nf"&gt;renderException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../common/components/Main&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;render&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们做了一些细节调整去捕获每次渲染可能会抛出的异常，然后用 redbox-react 来对错误进行更加友好的反馈（看起来和 react native 类似的红色示警），这样应该不会影响热替换的正常工作——我直觉是这样，需要时间验证——所以我们可以去掉 &lt;code&gt;setTimeout&lt;/code&gt; 了。&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;热替换就是这样子，等到 Redux 加入之后，需要单独为 reducers 做一下热替换；而 react-router 加入之后，则有一个小坑要填，我们后面再回到这个话题。&lt;/p&gt;
&lt;h3 id="Bring the Redux in..."&gt;Bring the Redux in...&lt;/h3&gt;
&lt;p&gt;如同之前谈过的，SSR 需要为应用提供最初始的数据形态，也就是最开始的全局状态树；我们也谈到过，在后端你要如何生成组装这个状态树完全是你的自由（这是业务逻辑的部分），SSR 唯一关心的就是当我们有了 initial state 之后，如何写到 response HTML 里去；我们还谈到过，Redux 里的 store 是可以且必须要同构的，所以我们需要有一个同构的创建 store 的逻辑。&lt;/p&gt;

&lt;p&gt;不过我打算倒过来描述这个过程，先假设我们已经可以生成前后统一的 store 了，那么 SSR 和 CSR 分别是什么样子的呢？&lt;/p&gt;

&lt;p&gt;以下是一个典型的应用了 Redux 的 CSR：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// client/index.js&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ReactDOM&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-dom&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-redux&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;createStore&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../common/store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__INITIAL_STATE__&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rootElement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;root&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;render&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Main&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../common/components/Main&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;ReactDOM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Provider&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Main&lt;/span&gt;&lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Provider&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;,
&lt;/span&gt;    &lt;span class="nx"&gt;rootElement&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里的 &lt;code&gt;createStore&lt;/code&gt; 是我们自己的定制版同构化的方法，不同于 &lt;code&gt;Redux.createStore&lt;/code&gt;，它只需要接收 initial state 一个参数就够了；细节我们稍后再讲。&lt;/p&gt;

&lt;p&gt;对应到 SSR，让我犹豫了许久的事情是创建 store 的过程应该放在 &lt;code&gt;bootup&lt;/code&gt; 内部还是外部？一开始我没有把同构这件事情想透彻，天真的认为：“当多个路由的场景介入的时候，可能需要针对 store 做不同的处理，因此放在 &lt;code&gt;bootup&lt;/code&gt; 外面会比较灵活”，而且我看到的 demo 里面有一些也是这么做的。但实践到后面我才意识到：“store 的结构应该是恒定的（一旦设计完成），对应不同的路由场景，真正可能变化的只是 initial state 而已——否则 store 就不好同构了“。&lt;/p&gt;

&lt;p&gt;因此 &lt;code&gt;bootup&lt;/code&gt; 需要做以下调整：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// server/bootup.js&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;renderToString&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-dom/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-redux&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;createStore&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../common/store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;generatePage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SSR Demo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/title&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;div id="root"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/div&amp;gt;
    &amp;lt;script&amp;gt;window.__INITIAL_STATE__ = &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;;&amp;lt;/script&amp;gt;
    &amp;lt;script src="/assets/client.js"&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script src="/assets/vendor.js"&amp;gt;&amp;lt;/script&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;components&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;initialState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;initialState&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;renderToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Provider&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;components&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Provider&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;generatePage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还有服务端匹配路由的部分：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// server/index.js&lt;/span&gt;

&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="nx"&gt;ssr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;initialState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{};&lt;/span&gt;

  &lt;span class="nf"&gt;bootup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;initialState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Main&lt;/span&gt;&lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;很显然，在服务端的 &lt;code&gt;initialState&lt;/code&gt; 就是在创建 store 前你需要使用你的业务逻辑去准备好的数据结构，如果在服务端你也完全使用了 Redux（而不仅仅是为了 SSR 准备一个 store 而已），那么这个 &lt;code&gt;initialState&lt;/code&gt; 的准备会简单很多，否则视应用程序的复杂度它的准备工作也会同比增加——这和我们在客户端用还是不用 Redux 的感受是一模一样的。&lt;/p&gt;

&lt;p&gt;而在 &lt;code&gt;bootup&lt;/code&gt; 处理过后，&lt;code&gt;store.getState()&lt;/code&gt; 帮我们吐出最终的 &lt;code&gt;state&lt;/code&gt;，SSR 需要把它传递给 CSR 来接手。在上面的代码中我直接写进了全局变量 &lt;code&gt;window.__INITIAL_STATE__&lt;/code&gt;，当然你可以用任何办法，只要别影响 CSR 的效率就好。CSR 接手后重新 &lt;code&gt;createStore&lt;/code&gt;，但这一次用的是 SSR 吐出来的最终形态的 state，所以得到的 store 自然也是一样一样的。&lt;/p&gt;

&lt;p&gt;所以拼图剩下的部分就是我们自己的 &lt;code&gt;createStore&lt;/code&gt; 了：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// common/store/index.js&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;applyMiddleware&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;createStore&lt;/span&gt; &lt;span class="nx"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;_createStore&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;redux&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;middlewares&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../middlewares&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;reducers&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../reducers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;initialState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createStore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;applyMiddleware&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;middlewares&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="nx"&gt;_createStore&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reducers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;initialState&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hot&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;module&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hot&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../reducers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replaceReducer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../reducers&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;reducer&lt;/code&gt; 还有 &lt;code&gt;middleware&lt;/code&gt; 都是 Redux 相关的东西，没有特殊性。另外我们针对 &lt;code&gt;reducers&lt;/code&gt; 实施了模块热替换，这是 Redux 唯一需要指定热替换的地方。在 &lt;code&gt;client/index.js&lt;/code&gt; 创建的 &lt;code&gt;store&lt;/code&gt; 和这里的 &lt;code&gt;store&lt;/code&gt; 是同一个对象，当这里 &lt;code&gt;store.replaceReducer()&lt;/code&gt; 之后会让应用程序得到更新的 store。&lt;/p&gt;
&lt;h3 id="...as well the React Router"&gt;...as well the React Router&lt;/h3&gt;
&lt;p&gt;当路由牵涉到同构化和 SSR 的时候，解决方案就不是那么显而易见了。主要是因为客户端路由需要依赖 history / location 等浏览器专属的 API，因此你需要谨慎考虑很多细节。&lt;/p&gt;

&lt;p&gt;比方说，由于组件是有生命周期的，你不能指望 &lt;code&gt;componentDidMount&lt;/code&gt; 内部的代码会在 SSR 时得到执行（请温习 React Component LifeCycle 的文档，里面有说明哪些生命周期不会在 SSR 时执行），所以如果你有在 &lt;code&gt;componentDidMount&lt;/code&gt; 内部异步更改数据的习惯，这会造成 SSR 与 CSR 无法同步的问题。这一点让我开始纠结了好久，不知如何处理更加妥当。在没有 Redux 的时候，我们可以把这种逻辑抽象以下，利用回调机制让它在服务／客户两端分别执行不同的逻辑（可参考&lt;a href="http://imweb.io/topic/5636466d09e01a534b461ec3" rel="nofollow" target="_blank" title=""&gt;这篇文章&lt;/a&gt;，里面的一些细节分析对本文有更深入的补充），这在客户端编程相对比较容易一些，但在 SSR 的时候，如果这些组件不在顶层，或者这些回调不是从顶层逐层向下传递的，你就不得不想办法深入进入单独渲染这些组件然后再回填到整棵组件树里——太折磨人了！&lt;/p&gt;

&lt;p&gt;有了 Redux 会简单很多（配合一些第三方的工具库，会增加理解成本，但非常舒服），特别是考虑到同构化之后，我们 &lt;code&gt;mapDispatchToProps&lt;/code&gt; 以及 &lt;code&gt;bindActionCreators&lt;/code&gt; 的动作都是放在顶层处理的，也就意味着我们有机会使用同一套 &lt;code&gt;actions&lt;/code&gt;，但在服务／客户两端 &lt;code&gt;dispatch&lt;/code&gt; 时执行不同的逻辑。我还没有做足够多的实验，而且这是 more Redux specific 的话题，以后有机会再结合实例讲解吧。总而言之，在施行了 SSR 之后，你在写组件的时候会不由自主提前考虑这个组件在 SSR 时能否正常渲染，我可以预期到这对我们整个团队来讲都是一个考验，未来的一段时间内一定会有很多坑等着我去填的……&lt;/p&gt;

&lt;p&gt;好，让我们先简单点：假设我们手中的一套组件都是可以顺利 SSR 的，那么路由怎么做？我们还是用分析 Redux 的顺序来一遍好了。&lt;/p&gt;

&lt;p&gt;先是 CSR 的部分：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// client/index.js&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ReactDOM&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-dom&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-redux&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;syncHistoryWithStore&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-router-redux&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Router&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;browserHistory&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;createStore&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../common/store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__INITIAL_STATE__&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;history&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;syncHistoryWithStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;browserHistory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rootElement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;root&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;render&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;Main&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../common/components/Main&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;ReactDOM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Provider&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Router&lt;/span&gt; &lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Route&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Main&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Router&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Provider&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;,
&lt;/span&gt;    &lt;span class="nx"&gt;rootElement&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这里我用了 &lt;a href="https://github.com/reactjs/react-router-redux" rel="nofollow" target="_blank" title=""&gt;react-router-redux&lt;/a&gt; 目的是为了得到一个“增强版”的 &lt;code&gt;history&lt;/code&gt; 提供给 react-router，用以增强 router 与 redux store 的同步性。以上只完成了一半，另外一半需要扩展一下 reducers 以获得更佳的整合性：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// common/reducers/index.js&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;combineReducers&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;redux&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;routerReducer&lt;/span&gt; &lt;span class="nx"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;routing&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-router-redux&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;combineReducers&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;otherReducers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;routing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不管你自己的 reducers 是什么样的，追加一个 key 为 &lt;code&gt;routing&lt;/code&gt; 的 reducer，它是 react-router-redux 提供的。&lt;/p&gt;

&lt;p&gt;至此我们还不能直接动手去实现等价的 SSR 端，还记得最开始我们在服务端匹配了所有的路由吗？我想我们可以使用服务端路由（虽然我没这么做）：在每一个路由处理中 1）准备 &lt;code&gt;initialState&lt;/code&gt;；2）调用 &lt;code&gt;bootup&lt;/code&gt; 渲染。但在现实场景中，我们通常都会把客户端的路由定义抽取成单独的模块，由于 react-router 提供了 SSR 的实现，所以我们可以同构使用路由定义。&lt;/p&gt;

&lt;p&gt;SSR 在这部分的处理有别于 CSR 的部分在于：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;对于错误需要响应 &lt;code&gt;500&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;对于跳转需要响应 &lt;code&gt;30x&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;对于资源定位错误需要响应 &lt;code&gt;404&lt;/code&gt;；&lt;/li&gt;
&lt;li&gt;maybe more...&lt;/li&gt;
&lt;li&gt;最后，在渲染前获取必要的初始数据，特别是路由相关的。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;为了实现这些需求，我们要利用 react-router 提供的几个机制：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/reactjs/react-router/blob/master/docs/API.md#match-routes-location-history-options--cb" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;match&lt;/code&gt; 函数&lt;/a&gt;：这个方法用于 SSR，传入路由定义和一个 location，它会对齐进行匹配。匹配到了给你一个签名为 &lt;code&gt;function(error, redirectLocation, renderProps)&lt;/code&gt; 的 callback，由你来定义其中的逻辑。&lt;code&gt;error&lt;/code&gt; 和 &lt;code&gt;redirectLocation&lt;/code&gt; 可用于错误和跳转的处理，而 &lt;code&gt;match&lt;/code&gt; 本身并不会渲染路由定义的组件，它只会给你准备就绪的 &lt;code&gt;renderProps&lt;/code&gt;，于是接下来我们可以——&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://github.com/reactjs/react-router/blob/master/docs/API.md#routercontext" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;&amp;lt;RouterContext/&amp;gt;&lt;/code&gt; 组件&lt;/a&gt;：当你拿到了 &lt;code&gt;renderProps&lt;/code&gt; 之后，可以利用 &lt;code&gt;&amp;lt;RouterContext/&amp;gt;&lt;/code&gt; 渲染所有的组件，它比客户端使用的 &lt;code&gt;Router&lt;/code&gt; 更底层一些，在 CSR 阶段，&lt;code&gt;Router&lt;/code&gt; 就是调用它的，而在 SSR 阶段则需要我们来主动调用之。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;此外，我还使用了一个库叫做 history，用于在服务端创建可用的 history 及 location，这些是 &lt;code&gt;match&lt;/code&gt; 函数的前置依赖。&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// server/server.js&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;createHistory&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;history/lib/createMemoryHistory&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;createRoutes&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../common/routes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nx"&gt;ssr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;*&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;history&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createHistory&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;routes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createRoutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createLocation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nx"&gt;routes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;redirectLocation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;renderProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;redirectLocation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;302&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;redirectLocation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;redirectLocation&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;search&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;renderProps&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;initialState&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

      &lt;span class="k"&gt;switch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;renderProps&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="nx"&gt;initialState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;mainData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{...}};&lt;/span&gt;
          &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nl"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="nx"&gt;initialState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="na"&gt;mainData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}};&lt;/span&gt;
          &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;

      &lt;span class="nf"&gt;bootup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;initialState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;renderProps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Not Found&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;上述代码是所有变化的部分。&lt;/p&gt;

&lt;p&gt;在 SSR 这边，没有必要去创建“增强版”的 &lt;code&gt;history&lt;/code&gt;；另外我们需要匹配当前 &lt;code&gt;req.url&lt;/code&gt; 的 &lt;code&gt;location&lt;/code&gt;；&lt;code&gt;createRoutes&lt;/code&gt; 见后文。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;switch&lt;/code&gt; 那端代码演示了我们可以获得判定不同路由的机会，这里的主要目的就是为不同路由准备 &lt;code&gt;initialState&lt;/code&gt; 了（在写本文的时候我已经意识到这里应该叫做 &lt;code&gt;initialData&lt;/code&gt;，在 &lt;code&gt;bootup&lt;/code&gt; 里经过 &lt;code&gt;store.getState()&lt;/code&gt; 取出的才应该叫 &lt;code&gt;initialState&lt;/code&gt;，这是命名上的瑕疵）。在这里丢一段 &lt;code&gt;switch&lt;/code&gt; 固然不好看，不过我手头要做的事情比较简单，还没有到需要再封装一层的必要，各位可根据情况自便。我想我应该对 Redux 还不太了解的看官们稍微解释一下。&lt;/p&gt;

&lt;p&gt;假设在进入某一路由时，UI 需要这样的一个状态树：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;mainData&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{...},&lt;/span&gt;
  &lt;span class="nx"&gt;routing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{...},&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;routing&lt;/code&gt; 在 SSR 阶段是不需要的，你可以忽略它（但你不能提供一个和 CSR 阶段不一样的 &lt;code&gt;routing&lt;/code&gt;），到了 CSR 接手之后会有 &lt;code&gt;routerReducer&lt;/code&gt; 补充它（见前文）；&lt;/p&gt;

&lt;p&gt;&lt;code&gt;mainData&lt;/code&gt; 是 SSR 阶段需要提供的，因为 UI 的首次渲染需要它，你必须确保 CSR 接收后保持 &lt;code&gt;mainData&lt;/code&gt; 不变——然而现实中你有可能需要变（在没有用户交互的情况下自动做一些客户端特定的事情来改变 &lt;code&gt;mainData&lt;/code&gt;），那么你一定要把这类变化用 Redux 的方式来执行（类似于上面的 &lt;code&gt;routerReducer&lt;/code&gt;），这样 store 才会认为这是正确的状态迁移，而不是 SSR 与 CSR 不同步。&lt;/p&gt;

&lt;p&gt;以上理论目前我不确定是否完全正确或是没有漏洞，因为我也还在逐步深入体会中，所以不必尽信，权当抛砖引玉就好。&lt;/p&gt;

&lt;p&gt;Now, 让我们看看 &lt;code&gt;createRoutes&lt;/code&gt; 吧：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// common/routes/index.js&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Router&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Main&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../components/Main&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;history&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Router&lt;/span&gt; &lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Route&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="nx"&gt;component&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Main&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Router&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;相应的，以下变更是必要的：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// client/index.js&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ReactDOM&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-dom&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-redux&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;syncHistoryWithStore&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-router-redux&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Router&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;browserHistory&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;createStore&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../common/store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__INITIAL_STATE__&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;history&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;syncHistoryWithStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;browserHistory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rootElement&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;root&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;render&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;createRoutes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../common/routes&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;ReactDOM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;unmountComponentAtNode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rootElement&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;ReactDOM&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Provider&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;createRoutes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Provider&amp;gt;&lt;/span&gt;&lt;span class="err"&gt;,
&lt;/span&gt;    &lt;span class="nx"&gt;rootElement&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// server/bootup.js&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;React&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;renderToString&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-dom/server&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;Provider&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-redux&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;RouterContext&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react-router&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;createStore&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../common/store&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;generatePage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SSR Demo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`
&amp;lt;!DOCTYPE html&amp;gt;
&amp;lt;html&amp;gt;
  &amp;lt;head&amp;gt;
    &amp;lt;title&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/title&amp;gt;
  &amp;lt;/head&amp;gt;
  &amp;lt;body&amp;gt;
    &amp;lt;div id="root"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/div&amp;gt;
    &amp;lt;script&amp;gt;window.__INITIAL_STATE__ = &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;;&amp;lt;/script&amp;gt;
    &amp;lt;script src="/assets/client.js"&amp;gt;&amp;lt;/script&amp;gt;
    &amp;lt;script src="/assets/vendor.js"&amp;gt;&amp;lt;/script&amp;gt;
  &amp;lt;/body&amp;gt;
&amp;lt;/html&amp;gt;
`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;renderProps&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;initialState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createStore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;initialState&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;renderToString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Provider&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;RouterContext&lt;/span&gt; &lt;span class="p"&gt;{...&lt;/span&gt;&lt;span class="nx"&gt;renderProps&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/Provider&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;store&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getState&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nf"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;generatePage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;That's it～&lt;/p&gt;

&lt;p&gt;当你定义了完整的 routes 之后，尽管 SSR 这边匹配的是 &lt;code&gt;*&lt;/code&gt;，但 &lt;code&gt;match&lt;/code&gt; 只会匹配到 routes 里定义的路由，其他的会转到 &lt;code&gt;404&lt;/code&gt;（或者你追加 middleware 来处理）。再者，你可以在匹配 &lt;code&gt;*&lt;/code&gt; 之前定义完全和 react-router 无关的路由，这样就可以和 React App 混搭合作了。而 Redux 是完全可以前后通用的，尽管我现在还对一些细节存有疑惑，但并非“能不能”的疑惑，而是“好不好”或“怎样好”的疑惑。&lt;/p&gt;

&lt;p&gt;希望在这个架构里正式开发一段时间之后能回来跟大家分享更多的东西，现在我要去整理一下这个 demo 了，尽快给大家提供一个完整的 repo 来测试。&lt;/p&gt;
&lt;h3 id="Oh, one more thing"&gt;Oh, one more thing&lt;/h3&gt;
&lt;p&gt;如果想要连接 Redux DevTools 并且保持同构化的 store，你需要判定服务端／客户端，并且如果是后者还要检查有没有这个扩展然后重写 &lt;code&gt;createStore&lt;/code&gt; 方法。这是可选的，等 repo 准备好了之后参见 &lt;code&gt;common/store/index.js&lt;/code&gt; 文件。&lt;/p&gt;
&lt;h3 id="可以运行的演示项目"&gt;可以运行的演示项目&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://github.com/nightire/isomorphic" rel="nofollow" target="_blank"&gt;https://github.com/nightire/isomorphic&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;这只是一个演示用的 repo，只需要 &lt;code&gt;npm install &amp;amp; npm start&lt;/code&gt; 就可以运行。比起网络上能够搜到的类似项目，完成度方面只能算中上，但是代码逻辑和结构会更简单和清晰。在此基础之上我还完成了一些东西（不在此 repo 中）：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;常见应用场景里的 css modules／actions／reducers／async middleware 的支持和实例&lt;/li&gt;
&lt;li&gt;服务端重启时的缓存处理（性能优化）&lt;/li&gt;
&lt;li&gt;测试／I18n／API 文档&lt;/li&gt;
&lt;li&gt;部署阶段的配置（webpack／node server）及运行脚本&lt;/li&gt;
&lt;li&gt;docker 部署／分发支持等&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这些方面的代码目前还处在私有状态，因为会随着对业务的改造不断改善所以还处于不稳定状态。我想等这个项目基本完成之后再做一次分享，除了上述几点之外也许还可以多说些对于 Redux 的感想。&lt;/p&gt;

&lt;p&gt;Thank you all &amp;amp; Happy Hackin'&lt;/p&gt;</description>
      <author>nightire</author>
      <pubDate>Tue, 15 Mar 2016 20:28:29 +0800</pubDate>
      <link>https://ruby-china.org/topics/29352</link>
      <guid>https://ruby-china.org/topics/29352</guid>
    </item>
    <item>
      <title>CodeSchool 本周末全免，推荐一看</title>
      <description>&lt;p&gt;CodeSchool 是一家很不错的在线视频教育服务商，我自己入门的时候就是看他家的视频教程“长大的”，本周末 CodeSchool 是为庆祝超过二百万用户而全免两天，注册就能看，所以推荐一下。&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.codeschool.com/free-weekend/" rel="nofollow" target="_blank"&gt;https://www.codeschool.com/free-weekend/&lt;/a&gt;&lt;/p&gt;</description>
      <author>nightire</author>
      <pubDate>Thu, 03 Mar 2016 13:50:34 +0800</pubDate>
      <link>https://ruby-china.org/topics/29193</link>
      <guid>https://ruby-china.org/topics/29193</guid>
    </item>
    <item>
      <title>有了解 Ultimate Hacking Keyboard 的来说说</title>
      <description>&lt;p&gt;&lt;a href="https://www.crowdsupply.com/ugl/ultimate-hacking-keyboard" rel="nofollow" target="_blank"&gt;https://www.crowdsupply.com/ugl/ultimate-hacking-keyboard&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;刚看到的时候并没想太多，今天忽然又翻到看到众筹截止快到了，心里开始纠结，大家觉得怎么样啊？有没有入手一个的想法？&lt;/p&gt;</description>
      <author>nightire</author>
      <pubDate>Sat, 12 Dec 2015 21:57:52 +0800</pubDate>
      <link>https://ruby-china.org/topics/28365</link>
      <guid>https://ruby-china.org/topics/28365</guid>
    </item>
    <item>
      <title>Ember Data 概述</title>
      <description>&lt;p&gt;呃……前面那个坑还没填完，因为我又改变了一些想法还在试验中。&lt;/p&gt;

&lt;p&gt;这是一个小坑，是两个月前给新团队的小伙伴们入门 Ember Data 时写的大纲，这两个月下来我发现还是挺有用的，先放出来以备后面挖大坑用……注意，这份大纲编写的时候 Ember Data 才刚刚 2.0 beta，到今天已经有了一些变化，而且官方的 API 文档做了很多补全或修正，所以实际应用还是要多看文档，以它为准。&lt;/p&gt;
&lt;h2 id="基本概念"&gt;基本概念&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Data Store：前端的数据仓库，类似于后端的数据库，但数据库是物理存储的实体且没有对象关系（需要语言提供 ORM 来实现对象关系映射），而前端的数据仓库则是随着应用程序的启动而实例化在内存中的动态仓库。它是有时效性的（浏览器关闭就没有了，因此它要实现与后端 API 的通信来存储数据），而且它很快（运行在内存中），另外它是可以管理对象关系的。简而言之，数据仓库就是把后端 API 返回的数据（如：JSON）转换为 JavaScript 对象（Ember Object），并映射和管理对象之间的关系（类似后端的 ORM），同时它也负责保存自前端创建的数据对象，并完成向后台发起请求保存数据的工作&lt;/li&gt;
&lt;li&gt;Ember Data records：记录，即数据实体；可类比为后端数据库里的一条记录，只不过前端没有数据库实体，而是运行在内存中的 Store&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="主要组成部分"&gt;主要组成部分&lt;/h2&gt;&lt;h2 id="DS.Store"&gt;&lt;a href="http://emberjs.com/api/data/classes/DS.Store.html" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;DS.Store&lt;/code&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Store 保存了自服务器取回的所有数据（保存在客户端）&lt;/li&gt;
&lt;li&gt;Store 同时负责在客户端创建定义为 &lt;code&gt;DS.Model&lt;/code&gt; 的数据实体，为的是让这样的数据实体可以和 Handlebars 模版进行数据绑定&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="DS.Model"&gt;&lt;a href="http://emberjs.com/api/data/classes/DS.Model.html" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;DS.Model&lt;/code&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;所有的 Ember Data records 都是 Model 的后代，当使用 Ember Data 配合 Ember 的时候，应该使用它来定义所有的 models&lt;/li&gt;
&lt;li&gt;除此之外还有一个 &lt;code&gt;DS.InternalModel&lt;/code&gt;，专门用来处理 Ember Data 内部的数据模型，但不应用来开发 Ember 应用里的数据模型。这是因为 &lt;code&gt;DS.Model&lt;/code&gt; 产生的数据实体是有数据绑定等能力的，适合于 UI 编程（与模版视图进行数据绑定）；而 &lt;code&gt;DS.InternalModel&lt;/code&gt; 处理的数据实体都是纯 JavaScript 对象，只适合 Ember Data 内部（不需要和模版视图进行数据绑定）。也因此，&lt;code&gt;DS.InternalModel&lt;/code&gt; 要比 &lt;code&gt;DS.Model&lt;/code&gt; 快些&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="DS.Snapshot"&gt;&lt;a href="http://emberjs.com/api/data/classes/DS.Snapshot.html" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;DS.Snapshot&lt;/code&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Snapshot：快照，即数据实体在 Ember 应用程序内的载体，除了数据本身之外快照保存了数据实体当前的状态（如是否有更改等）以及数据实体的关系等。&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="DS.RootState"&gt;&lt;a href="http://emberjs.com/api/data/classes/DS.RootState.html" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;DS.RootState&lt;/code&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;每一条数据实体都有一个 &lt;code&gt;currentState&lt;/code&gt; 属性，它显式的追踪着数据实体在任意时间点下的状态。比如说一条刚创建并且没有保存（即提交至后端）的数据实体，其状态是 &lt;code&gt;root.loaded.created.uncommitted&lt;/code&gt; 等等&lt;/li&gt;
&lt;li&gt;一些事件会通过数据实体自己或者数据仓库发送给 &lt;code&gt;currentState&lt;/code&gt; 属性，数据实体对这些事件如何反应取决于当前所处的状态。在不同的状态下，一些事件是无效的并且会抛出相应的异常（可用来追踪非法操作，比如说标注为 &lt;code&gt;删除状态&lt;/code&gt; 的数据实体不能不访问或修改等等）&lt;/li&gt;
&lt;li&gt;状态是分级管理的，并且所有的状态都是 &lt;code&gt;RootState&lt;/code&gt; 的子状态。比方说，一条数据实体可以从 &lt;code&gt;root.deleted.uncommitted&lt;/code&gt; 状态转换至 &lt;code&gt;root.deleted.inFlight&lt;/code&gt; 状态等等。如果一个子状态没有响应的事件回调函数，状态管理会尝试去调用它所有的父级状态的事件回调函数来处理它，直到根级状态。&lt;/li&gt;
&lt;li&gt;状态层级使用路径式的字符串来表示，你可以访问数据实体的 &lt;code&gt;stateName&lt;/code&gt; 属性来获取当前状态的完整路径。例如：&lt;code&gt;record.get('currentState.stateName')&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Ember Data 提供的默认状态（分级）如下表所示：

&lt;ul&gt;
&lt;li&gt;root

&lt;ul&gt;
&lt;li&gt;deleted

&lt;ul&gt;
&lt;li&gt;saved&lt;/li&gt;
&lt;li&gt;uncommitted&lt;/li&gt;
&lt;li&gt;inFlight&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;empty&lt;/li&gt;
&lt;li&gt;loaded

&lt;ul&gt;
&lt;li&gt;created

&lt;ul&gt;
&lt;li&gt;uncommitted&lt;/li&gt;
&lt;li&gt;inFlight&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;saved&lt;/li&gt;
&lt;li&gt;updated

&lt;ul&gt;
&lt;li&gt;uncommitted&lt;/li&gt;
&lt;li&gt;inFlight&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;loading&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DS.Model&lt;/code&gt; 的状态其本身是“无状态的”，这个意思是说：分层式的状态数据结构在全局中只有一份（类似单例对象），每一个状态所指向的都是这个不可改变且全局共享的数据结构（immutable and global shared data structure）&lt;/li&gt;
&lt;li&gt;这种设计是出于提高性能的考量，可是状态管理如何知道当前是哪一个数据实体处在哪一个状态中呢？Ember Data 会在状态事件回调函数中把当前数据实体作为第一个参数传递给状态管理机制&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="事件和旗标（Events and Flags）"&gt;事件和旗标（Events and Flags）&lt;/h3&gt;
&lt;p&gt;一个状态可能实现零或多个事件和旗标&lt;/p&gt;
&lt;h4 id="事件"&gt;事件&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;事件是具名的回调函数，当数据实体收到事件时会被调用&lt;/li&gt;
&lt;li&gt;数据实体先寻找自身对应的事件回调函数，如果没找到就向上（沿着状态层级）寻找父级的事件回调函数，依此类推直到根级状态（root state）&lt;/li&gt;
&lt;li&gt;若是根级状态也没有对应的事件回调函数，就会抛出一个异常&lt;/li&gt;
&lt;li&gt;这样的机制有助于调试新的功能特性&lt;/li&gt;
&lt;li&gt; 没有意外的情况下，事件回调函数应该转换当前的状态到一个新的状态去，这可以通过调用数据实体的 &lt;code&gt;transitionTo&lt;/code&gt; 方法来完成&lt;/li&gt;
&lt;li&gt;状态转换发生时，状态管理机制会尝试循着当前状态的路径向上查找，类似于寻找事件回调函数的处理方式&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;为了便于理解，举一个例子来说明，想象当前有这样一个状态结构：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;created

&lt;ul&gt;
&lt;li&gt;uncommitted &amp;lt;- currentState&lt;/li&gt;
&lt;li&gt;inFlight&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;updated

&lt;ul&gt;
&lt;li&gt;inFlight&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;接着，如果调用 &lt;code&gt;record.transitionTo('inFlight')&lt;/code&gt;，那么状态将会转换至 &lt;code&gt;created.inFlight&lt;/code&gt;；如果调用 &lt;code&gt;record.transitionTo('updated.inFlight')&lt;/code&gt;，那么状态会转换至 &lt;code&gt;updated.inFlight&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;切记！状态转换必须发生在事件回调函数内，永远不要在事件回调函数外调用 &lt;code&gt;transitionTo&lt;/code&gt; 方法；如果你确实需要，就创建一个新的自定义事件并发送给状态管理机制（于是这个事件的回调函数就能用来调用 &lt;code&gt;transitionTo&lt;/code&gt; 方法了）&lt;/strong&gt;&lt;/p&gt;
&lt;h4 id="旗标"&gt;旗标&lt;/h4&gt;
&lt;p&gt;旗标就是一系列的布尔类型的属性，为了让你方便的查询数据实体当前的状态（而不是解析路径字符串）。比方说，虽然你可以这样做：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;statePath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;stateManager.currentPath&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;statePath&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;created.inFlight&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;doSomething&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是，这样做会更方便：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;isNew&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;isSaving&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;doSomething&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以上例子也暗示了一件事：旗标与状态并非一一对应的（虽然有时候是），一个状态可能是由多个旗标共同定义的。&lt;/p&gt;

&lt;p&gt;如果一个状态没有设置对应的旗标，那么旗标的值会继承父级状态对应的旗标，或者是之前的状态设置的值。&lt;/p&gt;

&lt;p&gt;以下是默认的旗标，它们的说明可以在 &lt;code&gt;DS.Model&lt;/code&gt; 的文档里找到。如果你需要定义新的旗标，需要在 &lt;code&gt;DS.Model&lt;/code&gt; 里定义新的属性。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;isEmpty&lt;/li&gt;
&lt;li&gt;isLoading&lt;/li&gt;
&lt;li&gt;isLoaded&lt;/li&gt;
&lt;li&gt;isDirty&lt;/li&gt;
&lt;li&gt;isSaving&lt;/li&gt;
&lt;li&gt;isDeleted&lt;/li&gt;
&lt;li&gt;isNew&lt;/li&gt;
&lt;li&gt;isValid&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="DS.Transform"&gt;&lt;a href="http://emberjs.com/api/data/classes/DS.Transform.html" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;DS.Transform&lt;/code&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;根据&lt;a href="http://guides.emberjs.com/v2.1.0/models/defining-models/#toc_defining-attributes" rel="nofollow" target="_blank" title=""&gt;官方文档的描述&lt;/a&gt;，Ember Data 默认允许我们定义四种数据类型的数据属性，它们分别是：&lt;code&gt;string&lt;/code&gt; &lt;code&gt;number&lt;/code&gt; &lt;code&gt;boolean&lt;/code&gt; 和 &lt;code&gt;date&lt;/code&gt;。有时候我们需要自定义类型的数据属性，我们可以利用 &lt;code&gt;DS.Transform&lt;/code&gt; 接口来进行扩展。它的简单用法如下：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/transforms/temperature.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;DS&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ember-data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="c1"&gt;// 转换 JSON 里的摄氏度数，变成应用里的华氏度数&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;DS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Transform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;deserialize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;serialized&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;serialized&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;  &lt;span class="mf"&gt;1.8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;deserialized&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;deserialized&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;1.8&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/models/requirement.js&lt;/span&gt;

&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;DS&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ember-data&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;DS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;temperature&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;temperature&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// 实际应用&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="DS.Adapter"&gt;&lt;a href="http://emberjs.com/api/data/classes/DS.Adapter.html" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;DS.Adapter&lt;/code&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Adapter（适配器）用于接收来自数据仓库发出的请求（requests）并将其转换成对应数据持久层（通常是 API Service）的 HTTP 请求或其他合适的请求（比如说用 LocalStorage 做持久层的时候，就不需要 HTTP 请求了）&lt;/li&gt;
&lt;li&gt;通常我们不会直接调用 Adapter，而是通过 Store 提供的高层 API&lt;/li&gt;
&lt;li&gt;Adapter 是一个抽象的基础类，当你定义新的 Adapter 的时候，目标就是让其适应你的后端服务。一套最小的实现应该包含如下方法：

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;findRecord()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;createRecord()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;updateRecord()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;deleteRecord()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;findAll()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;query()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;有时为了提高网络通信的性能，你也可以重载 &lt;code&gt;findMany()&lt;/code&gt; 方法来优化你的 Adapter&lt;/li&gt;
&lt;li&gt;Ember Data 默认提供了通用的 &lt;a href="http://emberjs.com/api/data/classes/DS.RESTAdapter.html" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;DS.RESTAdapter&lt;/code&gt;&lt;/a&gt;，适用于标准的 REST API Service，你可以重载其中的方法来适应你的（非标准的）REST API Service&lt;/li&gt;
&lt;li&gt;Ember Data v2.0 后，默认（并且推荐）的 Adapter 将会是 &lt;a href="http://emberjs.com/api/data/classes/DS.JSONAPIAdapter.html" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;DS.JSONAPIAdapter&lt;/code&gt;&lt;/a&gt;，&lt;a href="http://jsonapi.org/" rel="nofollow" target="_blank" title=""&gt;JSON API 规范&lt;/a&gt; 是基于 REST 架构的新一代的标准，它定义和规范了开发基于 JSON 的 REST API 的一系列标准和实践准则&lt;/li&gt;
&lt;li&gt;此外，还有一个 &lt;a href="http://emberjs.com/api/data/classes/DS.DebugAdapter.html" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;DS.DebugAdapter&lt;/code&gt;&lt;/a&gt;，这是 Ember Data 内部用于调试的适配器，通常不需要考虑它&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="DS.BuildURLMixin 和 DS.EmbeddedRecordsMixin"&gt;
&lt;a href="http://emberjs.com/api/data/classes/DS.BuildURLMixin.html" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;DS.BuildURLMixin&lt;/code&gt;&lt;/a&gt; 和 &lt;a href="http://emberjs.com/api/data/classes/DS.EmbeddedRecordsMixin.html" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;DS.EmbeddedRecordsMixin&lt;/code&gt;&lt;/a&gt;
&lt;/h2&gt;
&lt;p&gt;这两个 Mixins 用于为 Adapter 实现两种功能：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;构造合适的 URL（为 API Service）&lt;/li&gt;
&lt;li&gt;处理嵌套式数据资源&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;具体用法可以参考 &lt;code&gt;DS.RESTAdapter&lt;/code&gt; 或 &lt;code&gt;DS.JSONAPIAdapter&lt;/code&gt;。&lt;/p&gt;
&lt;h2 id="DS.Serializer"&gt;&lt;a href="http://emberjs.com/api/data/classes/DS.Serializer.html" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;DS.Serializer&lt;/code&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Serializer（序列器）是和 Adapter（适配器）类似的抽象基础类，不过它用来转换请求或响应的数据格式，而不是匹配请求的方法&lt;/li&gt;
&lt;li&gt;一套最下的实现应该包含如下方法：

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;normalizeResponse()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;serialize()&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;normalize()&lt;/code&gt;（可选）&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Ember Data 内置了通用的 &lt;a href="http://emberjs.com/api/data/classes/DS.JSONSerializer.html" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;DS.JSONSerializer&lt;/code&gt;&lt;/a&gt;，以及对应 REST 和 JSON API 适配器的 &lt;a href="http://emberjs.com/api/data/classes/DS.RESTSerializer.html" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;DS.RESTSerializer&lt;/code&gt;&lt;/a&gt; 和 &lt;a href="http://emberjs.com/api/data/classes/DS.JSONAPISerializer.html" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;DS.JSONAPISerializer&lt;/code&gt;&lt;/a&gt;。需要定义适合自己 API Service 的序列器就可以参考以上的例子或者直接继承 &lt;code&gt;DS.JSONSerializer&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;举一个常见的例子，假设你有这样的 data model：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;User&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="na"&gt;friends&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;hasMany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;house&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;DS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;belongsTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;location&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时如果直接使用 &lt;code&gt;DS.JSONSerializer&lt;/code&gt;，那么请求或响应获得的 JSON 数据应该是这样的：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Sebastian&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;friends&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="nx"&gt;links&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nl"&gt;house&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/houses/lefkada&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是很常规的 JSON 数据格式，如果你的服务器与此不符，那么你可以扩展 &lt;code&gt;DS.JSONSerializer&lt;/code&gt; 来重新定义。&lt;/p&gt;
&lt;h2 id="DS.Errors"&gt;&lt;a href="http://emberjs.com/api/data/classes/DS.Errors.html" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;DS.Errors&lt;/code&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;DS.Errors&lt;/code&gt; 是 Ember Data 提供的异常和错误处理的基础类&lt;/li&gt;
&lt;li&gt;它主要用于保存模型的有效性验证错误（validation errors），使用模型的属性作为组织结构&lt;/li&gt;
&lt;li&gt;每一个 &lt;code&gt;DS.Model&lt;/code&gt; 都有一个 &lt;code&gt;errors&lt;/code&gt; 属性，其值就是一个 &lt;code&gt;DS.Errors&lt;/code&gt; 的实例对象，可以用来显示服务端返回的验证错误信息&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;DS.ActiveModelAdapter&lt;/code&gt;（以前版本里自带的适配器，用于适配 Rails 框架，现已被抽取出去变成了独立的 Addon）实现了这一套机制，可以自动处理 Rails 返回的错误信息。如果你要定义适合自己 API Service 的错误信息，可以参考它的实现&lt;/li&gt;
&lt;li&gt;目前，Ember Data 的新版本正在扩展 &lt;code&gt;DS.Errors&lt;/code&gt; 以期提供更多的默认类，文档尚未补全&lt;/li&gt;
&lt;/ul&gt;</description>
      <author>nightire</author>
      <pubDate>Sun, 06 Dec 2015 22:42:45 +0800</pubDate>
      <link>https://ruby-china.org/topics/28295</link>
      <guid>https://ruby-china.org/topics/28295</guid>
    </item>
    <item>
      <title>[Thinking in Ember 2] 组件化的探索——路漫漫其修远兮，还得 Up &amp; Down 啊～</title>
      <description>&lt;h2 id="引子（未完，持续更新中——2015／11／30 00:00）"&gt;引子（未完，持续更新中——2015／11／30 00:00）&lt;/h2&gt;
&lt;p&gt;标题中的 Up &amp;amp; Down 对应了原句里的“上下而求索”，在这里实际上是在说 Data Down Actions Up，简称 DDAU。&lt;/p&gt;

&lt;p&gt;当你开始践行组件化这件事情久了你就会有这样的感受：组件本身没什么难的，单独拎出来还不见得比写一个 jQuery Plugin 难，可是当组件开始和周围产生关系的时候复杂性就会逐渐展现出来了。这种关系往细了说既可以是组件与组件之间的关系，也可以是组件和其他层次的关系；而前者再往细了说也可以是先祖与后代组件之间的关系，同辈组件之间的关系，以及无关联（指的是 DOM 结构上的关联）组件之间的关系。&lt;/p&gt;

&lt;p&gt;很少会有机会在编写一个组件时遇到上述所有的关系一起朝你汹涌而来的情形，然而一旦你遇到了，那么高层次的预先设计就会尤其重要。像 DDAU 这样的理念，在简单结构下并不难实现，也不会让你有太特别的感受；反之，若场景很复杂，它的好处会体现的很直观（尽管更多时候体现在眼睛看不到的地方，比如在渲染性能和数据的可维护性上），然而你想要始终坚持它也会变得困难起来。&lt;/p&gt;

&lt;p&gt;最近我就对此深有感触，一开始就想写写这方面的事情了，但越做发现水越深，同时自身的理解和感悟也在不断增加着，到了后面反而感觉难以下笔了……不过还是要写的，不总结一下就没法真正把知识消化吸收掉。&lt;/p&gt;

&lt;p&gt;接下来要写的东西其实可以不和 Ember 紧紧耦合在一起，只不过演示的代码都是用 Ember 写的，所以还是归类在 Ember 系列的文章里吧。此乃前序。&lt;/p&gt;
&lt;h2 id="Immutable Data Structure"&gt;Immutable Data Structure&lt;/h2&gt;
&lt;p&gt;一个月以前我自己都想象不到会在写这篇文章的时候首先去讲 Immutable Data Structure（以下简称 IDS），因为在那时候我根本就是“只知其名，不见其形”的状态。当社区里大讲特讲 DDAU 的时候，IDS 也是其中被提及最多的概念之一。我能理解 DDAU 的“形状”，也能照猫画虎的去做，但老实讲我并不懂我在做什么以及为何要这样做。直到开始接触 Elixir 的时候（其实并非第一次接触了，一年前就试过水但没形成什么概念）才开始理解为啥要 DDAU。&lt;/p&gt;

&lt;p&gt;然而现在我其实也不具备来“传道解惑”的能力，之所以先提提 IDS，是因为对此入门之后我就有意识的去重构之前刚写好的组件了，虽然到现在还没有重构完不过接下来的例子里我会有要解释为什么这么做的地方。总而言之，在当前的前端架构之下（不管你用 Ember 还是其他什么）理解和使用 IDS 是趋势，我现在的感受是它的确会让一些事情变得更简单可控，并且我相信它能让我感受的好处会越来越多。&lt;/p&gt;

&lt;p&gt;我的理解是这样的，IDS 在纯函数式语言里是天然的特性，没有任何“人”去直接改写一个数据，如果你需要让数据变化，你做的是：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;复制原始的数据（通常这一步不需要显式的去写代码，语言本身帮你做了）；&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;计算出新的数据 －&amp;gt; 通过函数，函数就是数据的转换器；&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;复杂的计算用多个单一行为的函数组合运算，不同的语言提供不同机制来完成这种组合；&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;以上的概念只是表象化的描述，即使我这样去做了却还是不知道其中的道理，而现在我稍微有些入门了。对于以上描述：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;改写数据，意味着数据在内存中的位置不变（长度可能变化），通过直接操作内存地址来达到目的&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;如果谁都可以改写数据，那么这样做是危险的。

&lt;ul&gt;
&lt;li&gt;面向对象把数据封装在一个个对象内部，约定只有对象才能改变自身的数据，其他的对象需要通过消息去告诉它改变数据——&lt;strong&gt;这是我们原本惯常熟悉去做的事情，对吧？&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;如果数据可以改写，那么这样做是没有效率的。

&lt;ul&gt;
&lt;li&gt;即便用对象来封装数据，但是数据本身依然可以被改变，这就牵扯到许多问题啦，比如说可变地址长度，不连续地址的引用，垃圾回收的算法和效率，并发编程的安全性、可控性、一致性……等等等等。在这些方面我是菜鸟，就不扯远了，只是知道会有这些问题，大概能理解它们的缺陷以及人们为了解决它们需要花费的努力在哪里而已。&lt;/li&gt;
&lt;li&gt;另外，面向对象需要“对象”这样的机制来实现对数据的封装（我现在对封装的理解也更深了一些），那么由此就有了类型化的需求——你需要归类对象。由此能否反证非 OO 语言是不是就不需要类型化我并不清楚，但至少更加理解了类型化系统存在的必然性。现在在我的观念里，OO 里的类型不是原始数据类型（因为它的实例是用来封装原始数据类型的，并且能够改写原始数据类型），像 Struct 这样的东东算不算原始数据类型我不确认，但至少它要比 OO 的类型更“轻薄”，更“单纯”。&lt;/li&gt;
&lt;li&gt;对应到 JavaScript，尽管对象不是原始数据类型，但它依然要比类型系统里的 Class 要“轻薄”（不过 JavaScript 并没有 IDS 的支持，所以它的对象不“单纯”）；它用原型来实现继承确有它的好处，自此我也更能理解大神们为何强调“不要勉强用构造器＋原型去模仿类机制”了，这是语言的天性啊。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;复制数据，然后把计算之后的数据写入新的内存地址形成一份拷贝，这样做和改写数据相比自然是有很大的不同了。细节我就不谈了，和上面对照一下也能想明白，但有一点让我“观念性颠覆”的是：&lt;strong&gt;用对象来封装数据然后让对象自己去改写数据是没有必要的了。&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;尽管在 JavaScript 中想完全“模拟”这样的形态很困难（因为语言原生不支持），但：1）可能性还是存在的，只是要自己或第三方实现的底层支持；2）你可以控制对象机制的“存在感”来最大化减少对数据的直接改写。&lt;/li&gt;
&lt;li&gt;由此，我对函数式编程的本质也有了更深刻的理解，不是说函数式编程与面向对象是非要互斥的，而是它们各自生存的“世界”大不一样，天性相左；而像 JavaScript 这样揉合二者在一个世界里也是很有趣的事情，真的是你对它的理解会决定你对它的态度和使用方式，关键是把握一个度，掌握住这个平衡你就可以获得来自二者的好处，往往也会让你有多一种的选择。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;而对于函数的使用和理解，暂时还没有太多升华之处。拿 Elixir 来说，它对函数的使用和我在 JavaScript 里使用函数的方式非常类似（除了语言无法支持的部分），唯一的一个是对 ES6 里尾递归优化在原理上的理解。Elixir 让我用 &lt;code&gt;[ head | tail ]&lt;/code&gt; 重新认识了一遍递归（我的算法很差很差），在项目里也的确重写了一处用了递归的逻辑。不过我忽然想起我是来写组件的，函数就到此为止好了。&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;以上讲这么多，貌似和组件化没啥直接关系啊？嗯，我想说的是，当你去设计复杂组件的时候往往要考虑很多数据流转的问题，比如说谁传递谁接收，哪里要获得新数据，哪里要改变数据等等。IDS 会直接改变你的很多思路，理解和不理解 IDS 在对待很多问题的时候在观念上是有非常大的差别的。我在做手头上项目的时候很多次要给同事们解释在组件里我为什么要这样做或者那样做，有时候根本无法让对方理解背后的原因。那么在本文里涉及到可以这样做也可以那样做的时候，我可能就会直接讲：因为 IDS，所以……你懂的。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;有人已经编写了&lt;a href="https://github.com/sebmarkbage/ecmascript-immutable-data-structures" rel="nofollow" target="_blank" title=""&gt;向 ECMAScript 增加 Immutable Data Structure 的提案&lt;/a&gt;，不过目前还没有看到这份提案正式列入 TC39 的 &lt;a href="https://github.com/tc39/ecma262/blob/master/stage0.md" rel="nofollow" target="_blank" title=""&gt;Stage 0 提案列表里&lt;/a&gt;。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="场景"&gt;场景&lt;/h2&gt;
&lt;p&gt;原本这篇文章里讲述的“模特”是这么个东东：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2015/0a435d6656556bd9cb21049801961d7c.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;但是我现在得把它改一下，因为：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;开始计划写本文的时候我没想到这个东西底下的业务逻辑是如此复杂，所以也比原计划耽搁了一月有余；如果照实际情况讲还得解释一些业务逻辑，这和 UI 编程没啥关系&lt;/li&gt;
&lt;li&gt;前面也说了，最近本事看涨，这个组件在上线之后我就直接开始重构了，目前还在进行中；比起重构前的很多写法，我更想在本文里写更好的例子&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;所以呢，以下我还是以它为原型，但在此基础上简化一下层次只把一些通用的问题和思考描述清楚就足够了。&lt;/p&gt;

&lt;p&gt;以下是这个组件在模板內的结构图：&lt;/p&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;v-calendar&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
  &lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;v-calendar-grid&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
    &lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;v-calendar-timeline&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
       &lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;v-calendar-now&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
    &lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;v-calendar-timeline&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;

    &lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;v-calendar-arrangements&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
      &lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;v-calendar-operators&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;

      &lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;v-calendar-events&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
        &lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;v-calendar-event&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
          &lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;v-calendar-detail&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
        &lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;v-calendar-event&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
      &lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;v-calendar-events&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
    &lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;v-calendar-arrangements&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
  &lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;v-calendar-grid&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;

  &lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;v-calendar-status&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;v-calendar&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;简单描述：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;v-calendar&lt;/code&gt;：最外层组件，主要负责接收数据以及确定组件整体在 DOM 上的位置&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;v-calendar-grid&lt;/code&gt; 和 &lt;code&gt;v-calendar-status&lt;/code&gt;：之所以这里一分为二主要是 &lt;code&gt;v-calendar-status&lt;/code&gt; 是置底不动的，而 &lt;code&gt;v-calendar-grid&lt;/code&gt; 是内部垂直滚动的（静态截图没体现滚动），纯粹是 UI 上交互的需要。&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;v-calendar-timeline&lt;/code&gt; 和 &lt;code&gt;v-calendar-arrangements&lt;/code&gt;：同样这里一分为二是因为 &lt;code&gt;v-calendar-timeline&lt;/code&gt; 是置左不动的，而 &lt;code&gt;v-calendar-arrangements&lt;/code&gt; 是内部水平滚动的。

&lt;ul&gt;
&lt;li&gt;重构的时候我也在想：其实我可以让 &lt;code&gt;v-calendar-grid&lt;/code&gt; 同时负责水平和垂直滚动，这样可以去掉一层。当时为啥没这样做呢？有可能是里面有坑我现在忘了，也有可能是我犯傻。那么在写这篇文章同时也在重构的时候我会尝试去掉 &lt;code&gt;v-calendar-arrangements&lt;/code&gt;，看看行不行得通（九成是可行的）。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;v-calendar-operators&lt;/code&gt;：顶部那一排人名，它要跟随水平滚动，同时在垂直滚动时要置顶。&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;v-calendar-events&lt;/code&gt; 和 &lt;code&gt;v-calendar-event&lt;/code&gt;：是若干个垂直列，和 &lt;code&gt;v-calendar-operators&lt;/code&gt; 数量相等，每一列包含属于对应 &lt;u&gt;operator&lt;/u&gt; 的 &lt;u&gt;events&lt;/u&gt;。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;其实这个组件也还没算完工，还有一些 UI 交互方面的工作要做，比如说时间段冲突的 &lt;code&gt;events&lt;/code&gt; 要自动并排（这比我想象的要难，以前没做过不知道最优的做法是怎样的思路，，有做过的或是知道开源实现的请不吝赐教，我需要一些先期调研。）等等，业务逻辑那边没确定暂时没有提上日程。&lt;/p&gt;

&lt;p&gt;OK，大的需求就这样了。不管看官用的是不是 Ember，模板语法如何不同，思维方式有何差异……反正组件化都是这么搞了，也会遵循一些通用设计原则比如单一职责之类的。你可以分得更细／粗，然而 UI 的复杂性会让你无法避免父子嵌套／同辈并排等组件关系。咱们就先将就着用我这个方案吧，有什么建议请提出我正好采纳重构去。&lt;/p&gt;

&lt;p&gt;另外，以上只是一个结构示意图，并不是说在真实的代码里就是这样把组件写进模板的哦。组件自身可以是行内或块级形式的，你甚至可以把内层的所有结构都隐藏在 &lt;code&gt;v-calendar&lt;/code&gt; 里面，最终调用的时候只是写一个 &lt;code&gt;{{v-calendar}}&lt;/code&gt; 完事，事实上这将是我们开始探讨的第一个话题——&lt;/p&gt;
&lt;h2 id="Inline？or Block？"&gt;Inline? or Block?&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;因为组件终究要有使用它的地方，本文就假定它用在 &lt;code&gt;events/template.hbs&lt;/code&gt; 里，对应的路由和控制器分别是 &lt;code&gt;events/route.js&lt;/code&gt;，&lt;code&gt;events/controller.js&lt;/code&gt;，先予以说明，在后面的代码顶部都会声明是哪个文件，以免看糊涂了。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;组件可以是行内的，用来隐藏模版的细节。&lt;/p&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="c"&gt;{{! 在你使用组件的地方 }}&lt;/span&gt;
&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;a-inline-component&lt;/span&gt; &lt;span class="nv"&gt;with-attribute&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;and-value&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当然也可以是块级的，用来让用户编写包裹于内的内容。&lt;/p&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="c"&gt;{{! 在你使用组件的地方 }}&lt;/span&gt;
&lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;a-block-component&lt;/span&gt; &lt;span class="nv"&gt;with-attribute&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;and-value&lt;/span&gt; &lt;span class="nv"&gt;as&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="nv"&gt;exposed-property&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;

  &lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;user-define-component&lt;/span&gt; &lt;span class="nv"&gt;can-accept&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;exposed-property&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;or, plain html code&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;

&lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;a-block-component&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Ember 也允许你同时兼备二者，使得组件既可以行内调用也可以块级调用。&lt;/p&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="c"&gt;{{! 在你定义组件模版的地方 }}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;h1&amp;gt;&lt;/span&gt;这是组件标题，组件的使用者无法接触到这里&lt;span class="nt"&gt;&amp;lt;/h1&amp;gt;&lt;/span&gt;

&lt;span class="k"&gt;{{#if&lt;/span&gt; &lt;span class="nv"&gt;hasBlock&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;yield&lt;/span&gt; &lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;{{else}}&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;{{/if}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="c"&gt;{{! 在你使用组件的地方 }}&lt;/span&gt;
&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;use-as-inline-component&lt;/span&gt; &lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"用户定义的输出内容"&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;

&lt;span class="c"&gt;{{! 或者 }}&lt;/span&gt;

&lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;use-as-block-component&lt;/span&gt; &lt;span class="nv"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"作为参数输出的内容"&lt;/span&gt;
  &lt;span class="nv"&gt;as&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="nv"&gt;exposed-property&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
  &lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;exposed-property&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt; 用户定义的输出内容
&lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;use-as-block-component&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;像 &lt;code&gt;{{v-calender}}&lt;/code&gt; 这样的组件，它自身及其内部众多组成部分，哪些该用行内，哪些该用行外，哪些该二者并用呢？&lt;/p&gt;

&lt;p&gt;在一开始我以为这是一个很简单的问题，如果你的组件只有一到两层结构也会很容易做出判断，可 &lt;code&gt;{{v-calendar}}&lt;/code&gt; 的复杂度着实让我纠结了好久，最终我也是做了所有可能的尝试才确定了一个方案。到底在纠结哪些东西呢？下面我分别加以说明。&lt;/p&gt;
&lt;h3 id="在简单与灵活之间的平衡和取舍"&gt;在简单与灵活之间的平衡和取舍&lt;/h3&gt;
&lt;p&gt;简单、灵活，是所有组件开发者追求的共同目标，简单意味着隐藏实现的细节让用户专注于使用，灵活意味着提供丰富的接口和用户自定义能力。可惜有些时候此二者难以两全。&lt;/p&gt;

&lt;p&gt;我对 &lt;code&gt;{{v-calendar}}&lt;/code&gt; 做出的第一个决定就是让它非常容易调用，最好就是这样：&lt;/p&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="c"&gt;{{! events/template.hbs }}&lt;/span&gt;
&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;v-calendar&lt;/span&gt; &lt;span class="nv"&gt;property&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在我很惊讶于我的第一直觉，然而在开始阶段我却因此而饱受折磨……&lt;/p&gt;

&lt;p&gt;第一直觉使用行内形式（并且不提供块级形式）的原因是：这个组件光是看看完整的组件结构都会让人头晕，更不要说展开成 DOM 结构了。因为它结构非常复杂，HTML 的安排和 CSS 的编写都是经过精心编排的，留给用户自定义的余地实在是太小了，块级结构没什么实用价值。&lt;/p&gt;

&lt;p&gt;第二个原因是，&lt;code&gt;{{v-calendar}}&lt;/code&gt; 不是那种通用化的组件，更没有要开源出去供更多项目使用的计划（业务逻辑耦合性高，在自家应用里也只有一处需求，就算给别人用也得二次改造不是），所以要提供多大的灵活性不是我要考虑的目标。&lt;/p&gt;

&lt;p&gt;既然灵活性的需求很低，那就追求简单直白吧，这就是第一直觉的驱动原力。&lt;/p&gt;

&lt;p&gt;OK，写着写着问题来了。第一个问题是让数据向下传递太辛苦了！&lt;/p&gt;

&lt;p&gt;&lt;code&gt;{{v-calendar}}&lt;/code&gt; 需要的数据很多，由于 API 设计上的一些问题，实际中的数据还无法拿来就用，在 &lt;code&gt;events/route.js&lt;/code&gt; 里我做了大量的数据重组的工作，然后把它们一一传递给最外层的 &lt;code&gt;{{v-calendar}}&lt;/code&gt;，主要的数据罗列如下：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;markers：时间标记，用于&lt;code&gt;{{v-calendar-timeline}}&lt;/code&gt;。其实在我看来有 &lt;u&gt;start&lt;/u&gt; &lt;u&gt;ending&lt;/u&gt; &lt;u&gt;interval&lt;/u&gt; 就可以在客户端算出对应的时间刻度，不过 API 直接给返回了一个 &lt;code&gt;['10:00', '10:30', '11:00', ...]&lt;/code&gt; 数组，所以就沿用了。&lt;/li&gt;
&lt;li&gt;operators：操作者，用于 &lt;code&gt;{{v-calendar-operators}}&lt;/code&gt;。这是我虚构的实体名称，原本的业务逻辑里其实分好多种，什么 &lt;u&gt;consultants&lt;/u&gt; &lt;u&gt;artificers&lt;/u&gt; &lt;u&gt;technicians&lt;/u&gt;，简直要命！仔细一看其实全是 &lt;u&gt;employees&lt;/u&gt;，无非就是性质不同罢了，但由于 &lt;u&gt;employees&lt;/u&gt; 有用，所以我给统一抽象成了 &lt;u&gt;operators&lt;/u&gt;。&lt;/li&gt;
&lt;li&gt;events：预约事件，用于 &lt;code&gt;{{v-calendar-events}}&lt;/code&gt; &lt;code&gt;{{v-calendar-event}}&lt;/code&gt; 和 &lt;code&gt;{{v-calendar-detail}}&lt;/code&gt;。这是最主要的数据，但也是最坑爹的！不过这里关于 API 就不去吐槽了，反正在我坚持之下后端小伙伴们去重构了，以后会专门写关于 Data Store 的文章，到时再议。&lt;/li&gt;
&lt;/ol&gt;

&lt;blockquote&gt;
&lt;p&gt;关于 &lt;code&gt;events&lt;/code&gt;，理想情况下在最外层原本不应该有单独的 &lt;code&gt;events&lt;/code&gt;，它们应该是各自 &lt;code&gt;operator&lt;/code&gt; 的关系数据，通过 &lt;code&gt;operator.events&lt;/code&gt; 就可以直接获取。但由于 API 并没能做到这一点，所以事实上前端这边是做了处理的。后面会有一个谈及这个事情的段落。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;这些就是组件需要的主体数据，其实并不太多对吧？仔细观察一下你会发现，使用这些数据的都是组件结构中最里层的几个，第三或第四层。我后来才顿悟，复杂的组件之所以复杂，主要就是因为 UI 结构造成的。UI 和交互上的需求使得组件设计必须分层，逐层处理这些需求的依赖关系，但是往往只有最里层才真正需要使用到业务逻辑需求的数据。换个角度说，如果你不需要很多层级就直接处理业务了，那说明你的 UI／交互并不复杂；而如果你觉得 UI／交互很复杂，但还是没怎么分层的话，老兄你的代码会不会全堆在一起了呢？&lt;/p&gt;

&lt;p&gt;我看了一下 &lt;code&gt;{{v-calendar}}&lt;/code&gt; 全部的代码，最长的组件也就 167 行，在代码的可读性和维护性方面做得还是可以的（要不然自己都不想重构啊）。&lt;/p&gt;

&lt;p&gt;我可以这样调用模版：&lt;/p&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="c"&gt;{{!-- events/template.hbs --}}&lt;/span&gt;

&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;v-calendar&lt;/span&gt;
  &lt;span class="nv"&gt;markers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;markers&lt;/span&gt;
  &lt;span class="nv"&gt;operators&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;operators&lt;/span&gt;
  &lt;span class="nv"&gt;events&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;events&lt;/span&gt;
  &lt;span class="nv"&gt;others&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;others&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;此时，只有 &lt;code&gt;{{v-calendar}}&lt;/code&gt; 能够接收到 &lt;u&gt;context&lt;/u&gt;（注：即组件所处的上下文环境，在这里是 &lt;code&gt;events/controller&lt;/code&gt;；在未来则应是 Routable Component）里面的数据，也就是 &lt;code&gt;model&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;如果每一层的组件都是这样把内部的细节隐藏起来，那么所有的数据都需要这样一层接一层的传下去，可以想象会有多么麻烦的吧？麻烦也就罢了，数据在到达真正的使用者那里之前需要一层层的“过滤”，这样留下很多重复之处，也不利于可读性和可维护性，到处散发着 DRY 的味道。&lt;/p&gt;

&lt;p&gt;当然，我们也可以这样调用模版：&lt;/p&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="c"&gt;{{!-- events/template.hbs --}}&lt;/span&gt;

&lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;v-calendar&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
  &lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;v-calendar-grid&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
    &lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;v-calendar-timeline&lt;/span&gt; &lt;span class="nv"&gt;markers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;markers&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
       &lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;v-calendar-now&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
    &lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;v-calendar-timeline&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;

    &lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;v-calendar-arrangements&lt;/span&gt;
      &lt;span class="nv"&gt;operators&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;operators&lt;/span&gt;
      &lt;span class="nv"&gt;events&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;events&lt;/span&gt;
      &lt;span class="nv"&gt;as&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="nv"&gt;operators&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;

      &lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;v-calendar-operators&lt;/span&gt; &lt;span class="nv"&gt;operators&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;operators&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;

      &lt;span class="c"&gt;{{!-- 下面两个 each 都可以隐藏在直接父级组件内部，这里列出是为了明示 --}}&lt;/span&gt;
      &lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;each&lt;/span&gt; &lt;span class="nv"&gt;operators&lt;/span&gt; &lt;span class="nv"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt; &lt;span class="nv"&gt;as&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="nv"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
        &lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;v-calendar-events&lt;/span&gt; &lt;span class="nv"&gt;events&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;events&lt;/span&gt; &lt;span class="nv"&gt;as&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="nv"&gt;events&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
          &lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;each&lt;/span&gt; &lt;span class="nv"&gt;events&lt;/span&gt; &lt;span class="nv"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt; &lt;span class="nv"&gt;as&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="nv"&gt;event&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
            &lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;v-calendar-event&lt;/span&gt; &lt;span class="nv"&gt;event&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;event&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
          &lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;each&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
        &lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;v-calendar-events&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
      &lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;each&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
    &lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;v-calendar-arrangements&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
  &lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;v-calendar-grid&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;

  &lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;v-calendar-status&lt;/span&gt; &lt;span class="nv"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;data&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;v-calendar&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样一来，我们把组件的内部细节全部暴露在环境上下文里（这里指的是 &lt;code&gt;events/controller&lt;/code&gt;），于是每一个组件都可以直接获得来自上下文的数据了。我们省去了一些向下传递数据的中间环节，但是增加了调用时的复杂性，这在我们的项目中是可以接受的，如我之前所说的，只用这一次而已。但是对于开源组件的开发者来说，这肯定是不能忍受的做法，也是我要重构的一个原因。&lt;/p&gt;

&lt;p&gt;不过在&lt;/p&gt;
&lt;h3 id="DD 不是形式主义，并非只要能“向下”就完事了"&gt;DD 不是形式主义，并非只要能“向下”就完事了&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;{{v-calendar-arrangements}}&lt;/code&gt; 同时接收了 &lt;code&gt;model.operators&lt;/code&gt; 和 &lt;code&gt;model.events&lt;/code&gt; 这两个数据，而实际上它也用不着这俩数据。之所以这么做，是因为 API 是分开返回的：&lt;/p&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"operators"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"events"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而它们本应该是关系化的数据，这样在后面遍历 &lt;code&gt;operators&lt;/code&gt; 的时候可以直接把所属的 &lt;code&gt;events&lt;/code&gt; 分组了。可由于现在不是这样，所以我在这里接收了二者，然后在里面处理成了关系化的数据，这个处理类似这样：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// v-calendar-arrangements/component.js&lt;/span&gt;

&lt;span class="nx"&gt;newOperators&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;operators&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;operators&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;operator&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;events&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;pushObjects&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;events&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filterBy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;employeeId&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="nx"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;id&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}),&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后在它的模版內将重组后的数据暴露出来：&lt;/p&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="c"&gt;{{!-- v-calendar-arrangements/template.hbs --}}&lt;/span&gt;

&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;yield&lt;/span&gt; &lt;span class="nv"&gt;newOperators&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;当组件內的属性被 &lt;code&gt;yield&lt;/code&gt; 之后，调用时就可以用 &lt;code&gt;as&lt;/code&gt; 按顺序暴露出来，也就是我们在前面看到的：&lt;/p&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;v-calendar-arrangements&lt;/span&gt;
  &lt;span class="nv"&gt;operators&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;operators&lt;/span&gt;
  &lt;span class="nv"&gt;events&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;events&lt;/span&gt;
  &lt;span class="nv"&gt;as&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="nv"&gt;operators&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;

&lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;v-calendar-arrangements&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而我做错的地方就在于，&lt;strong&gt;数据的重组不应该发生在组件的内部&lt;/strong&gt;，我也不知道当时是怎么想的。尽管在这里组件并没有去改变数据而是 &lt;code&gt;operators&lt;/code&gt; 自己作出的改变，可地方错了。因为选错了地方，所以才做出了让 &lt;code&gt;{{v-calendar-arrangements}}&lt;/code&gt; 去接收 &lt;code&gt;model.operators&lt;/code&gt; 和 &lt;code&gt;model.events&lt;/code&gt; 的决策，并且加重了这个组件的负担。&lt;/p&gt;

&lt;p&gt;DDAU 没有挑明的一点是：&lt;strong&gt;一旦数据向下传递了，数据就不应该被下面的任意一层去改变（这是为了性能），数据的拥有者（即封装了该数据的对象）也不应该在下面的任意一层里去做改变数据的动作（这是为了组件的单一职责）。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;尽管 JavaScript 没有原生的 IDS，该去改数据的时候就可以改，但我们开头也说过你就当它是 IDS 好了，只是把改变数据这件事情留到最上层去做。这里面最直接的原因是提高 UI 的性能，如果你不理解这怎么就提高的，去看看 Glimmer 引擎的原理（Youtube 上搜），我就少说点。&lt;/p&gt;

&lt;p&gt;那么哪里做这件事情是对的？最理想的状态是 API 的返回棒棒的，于是 Ember Data 已经帮你把数据准备好了，传下来就行。不过总还是有需要你改变的时候，这时可以在路由里做这些事情（推荐的），或者是控制器里（不推荐）。记住一点：MV* 放在前端其实挺牵强的，过去几年大家都没太想明白所以控制器像标配一样到处都是，可现在迹象越来越明显，控制器将死，别再习惯于把代码堆积在那里。&lt;/p&gt;

&lt;p&gt;总结一点：向下不是目的，不在下层改写数据才是真正的内涵。&lt;/p&gt;

&lt;p&gt;然后我们回到重构模版调用的话题，最新的想法如下所述：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;最终的调用还是要保持简单，哪怕只用一次&lt;/p&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="c"&gt;{{!-- events/template.hbs --}}&lt;/span&gt;

&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;v-calendar&lt;/span&gt;
  &lt;span class="nv"&gt;data-status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;status&lt;/span&gt;
  &lt;span class="nv"&gt;data-markers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;markers&lt;/span&gt;
  &lt;span class="nv"&gt;data-operators&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;operators&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;内部结构不必层层隐藏，可以在第一层的模版内适度展开&lt;/p&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="c"&gt;{{!-- v-calendar/template.hbs --}}&lt;/span&gt;

&lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;v-calendar-grid&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
  &lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;v-calendar-timeline&lt;/span&gt; &lt;span class="nv"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;markers&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
  &lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;v-calendar-arrangements&lt;/span&gt; &lt;span class="nv"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;operators&lt;/span&gt; &lt;span class="nv"&gt;as&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="nv"&gt;operators&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
    &lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;v-calendar-holders&lt;/span&gt; &lt;span class="nv"&gt;operators&lt;/span&gt; &lt;span class="nv"&gt;as&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="nv"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;v-calendar-holders&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;

    &lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;each&lt;/span&gt; &lt;span class="nv"&gt;operators&lt;/span&gt; &lt;span class="nv"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt; &lt;span class="nv"&gt;as&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="nv"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
      &lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;v-calendar-events&lt;/span&gt; &lt;span class="nv"&gt;operator&lt;/span&gt; &lt;span class="nv"&gt;as&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="nv"&gt;event&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
        &lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;v-calendar-event&lt;/span&gt; &lt;span class="nv"&gt;event&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
      &lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;v-calendar-events&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
    &lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;each&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
  &lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;v-valendar-arrangements&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;v-calendar-grid&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;

&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;v-calendar-status&lt;/span&gt; &lt;span class="nv"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这样比之前好看太多了有没有？不过 &lt;code&gt;data-*&lt;/code&gt; 和 &lt;code&gt;data.*&lt;/code&gt; 什么鬼？为啥要这么写？实际上这个是我新想出来的“花招”，源自于一个新的业务需求，下面单独来说一下这个玩法：&lt;/p&gt;
&lt;h3 id="接收不定数量属性的组件（非最佳实践，而且是少见需求，可以跳过无视）"&gt;接收不定数量属性的组件（非最佳实践，而且是少见需求，可以跳过无视）&lt;/h3&gt;
&lt;p&gt;新的需求是这样的：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;event&lt;/code&gt; 可以不仅仅属于 &lt;code&gt;operator&lt;/code&gt;，它还可以属于 &lt;code&gt;room&lt;/code&gt; 和／或 &lt;code&gt;equipment&lt;/code&gt;，也就是说预约事件可以占用操作人，也可以占用房间、设备&lt;/li&gt;
&lt;li&gt;应用默认展示在这里的是 &lt;code&gt;operator&lt;/code&gt; 的维度，但是可以在 UI 上切换 &lt;code&gt;room&lt;/code&gt; 或 &lt;code&gt;equipment&lt;/code&gt;，具体能切换几个取决于用户的付费级别。也就是说 &lt;code&gt;room&lt;/code&gt; 和 &lt;code&gt;equipment&lt;/code&gt; 是付费开启的维度&lt;/li&gt;
&lt;li&gt;基于此，传给 &lt;code&gt;v-calendar&lt;/code&gt; 的数据是可变的，这一层维度会有一到三种数据不等&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;我们可以总是把三类数据都传给 &lt;code&gt;v-calendar&lt;/code&gt;，然后根据当前用户级别决定他／她能切换哪些。但是最理想的状况是 API 服务那里已经判断和筛选过了，我们从路由那里得到的 &lt;code&gt;model&lt;/code&gt; 就是不定数量的对象，所以组件自身得拥有接收不定数量属性的能力，于是调用时可能会是这样的：&lt;/p&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="c"&gt;{{!-- events/template.hbs --}}&lt;/span&gt;

&lt;span class="k"&gt;{{#if&lt;/span&gt; &lt;span class="nv"&gt;freeUser&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
  &lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;v-calendar&lt;/span&gt;
    &lt;span class="nv"&gt;data-status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;status&lt;/span&gt;
    &lt;span class="nv"&gt;data-markers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;markers&lt;/span&gt;
    &lt;span class="nv"&gt;data-operators&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;operators&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;span class="k"&gt;{{/if}}&lt;/span&gt;

&lt;span class="k"&gt;{{#if&lt;/span&gt; &lt;span class="nv"&gt;paidUser&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
  &lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;v-calendar&lt;/span&gt;
    &lt;span class="nv"&gt;data-status&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;status&lt;/span&gt;
    &lt;span class="nv"&gt;data-markers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;markers&lt;/span&gt;
    &lt;span class="nv"&gt;data-rooms&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;rooms&lt;/span&gt;
    &lt;span class="nv"&gt;data-operators&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;operators&lt;/span&gt;
    &lt;span class="nv"&gt;data-equipments&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;equipments&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;span class="k"&gt;{{/if}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么组件要如何处理不定数量属性？上代码：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// example-component.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Ember&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ember&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;computed&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Ember&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;Ember&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;computed&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prop&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;prop&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;data-&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;curr&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="nx"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;curr&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;substr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;curr&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;prev&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;戏法戳穿了毫不稀奇，就是约定 &lt;code&gt;data-&lt;/code&gt; 开头的属性都是可变数量属性的一员，又因为所有的属性都会在组件的 &lt;code&gt;attrs&lt;/code&gt; 对象里，因此在里面把所有 &lt;code&gt;data-&lt;/code&gt; 开头的属性取出来构造成新的对象就是了。&lt;/p&gt;

&lt;p&gt;很显然目前的写法是有些局限性的，比如说 &lt;code&gt;data-&lt;/code&gt; 的前缀太死板了，不过这可以通过把它也变成可变属性的方式来解决。然而真正的问题是上面的代码只能在组件初次渲染时生效，当任意一个 &lt;code&gt;data-&lt;/code&gt; 属性改变之后，组件是不会知道的，所以也不会重新渲染，原因是 &lt;code&gt;computed&lt;/code&gt; 没有可以监视的属性，你不能事先把所有 &lt;code&gt;data-&lt;/code&gt; 的属性名写上去，因为你无法在组件定义的时候就知道有多少个。你也不能想当然的去监视 &lt;code&gt;attrs&lt;/code&gt; 属性，因为它本身不是 CP（即：Computed Property，计算属性）。&lt;/p&gt;

&lt;p&gt;在我们这个特定的需求里，我可以很简单的通过监视 &lt;code&gt;['data-rooms.[]', 'data-operators.[]', 'data-equipments.[]']&lt;/code&gt; 来解决，尽管它们是可变的却也就这么多了，即使以后增加也无法改一处即可。可对于那些目标是通用型的组件来说可咋办呢？&lt;a href="https://ember-twiddle.com/65bfb2c9b2154b1b7aef" rel="nofollow" target="_blank" title=""&gt;目前为止，我只想出了一个很傻的办法&lt;/a&gt;，希望有人能想个天才的主意出来。&lt;/p&gt;
&lt;h3 id="接收数据却不用写属性名的组件"&gt;接收数据却不用写属性名的组件&lt;/h3&gt;
&lt;p&gt;之后我们使用 &lt;code&gt;{{yield data}}&lt;/code&gt; 暴露给内层组件，紧接着就看到了这样的接收方式：&lt;/p&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;v-calendar-timeline&lt;/span&gt; &lt;span class="nv"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;markers&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;v-calendar-arrangements&lt;/span&gt; &lt;span class="nv"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;operators&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;对的，接收数据的时候是可以不用写属性名称的，如果你尚不知道的话，这个特性叫做按位参数（即：Positional Params）。&lt;a href="http://guides.emberjs.com/v2.2.0/components/passing-properties-to-a-component/#toc_positional-params" rel="nofollow" target="_blank" title=""&gt;官方文档已经更新了这个特性&lt;/a&gt;，我就略过不谈了。不过它给了我一个灵感，或许按位参数能更好的解决不定数量属性的遗留问题？先不整了，这只是一个无关大局的事情。&lt;/p&gt;

&lt;p&gt;关于按位参数，刚学会它的时候我恨不得把所有属性都变成这种形式的，多方便呀！但是写多以后发现你很难记住它们的顺序和含义。那现在会这样做：只把最重要的属性作为按位参数来写，甚至完全不用按位参数也没有什么关系。对于组件的使用者来说最重要的就是接口清楚，在一目了然不产生误解或歧义的前提下去用按位参数是好的，否则不如不用。&lt;/p&gt;
&lt;h3 id="可遍历的 {{yield}}"&gt;可遍历的 &lt;code&gt;{{yield}}&lt;/code&gt;
&lt;/h3&gt;
&lt;p&gt;前面说过行内模式的组件和块级模式的组件是可以混用的，这个重构的新想法就是结合了二者的优点之后做出的。观察在 &lt;code&gt;{{v-calendar}}&lt;/code&gt; 模版内部的一部分：&lt;/p&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;v-calendar-arrangements&lt;/span&gt; &lt;span class="nv"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;operators&lt;/span&gt; &lt;span class="nv"&gt;as&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="nv"&gt;operators&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
  &lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;v-calendar-holders&lt;/span&gt; &lt;span class="nv"&gt;operators&lt;/span&gt; &lt;span class="nv"&gt;as&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="nv"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;span&amp;gt;&lt;/span&gt;&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/span&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;v-calendar-holders&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;

  &lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;each&lt;/span&gt; &lt;span class="nv"&gt;operators&lt;/span&gt; &lt;span class="nv"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt; &lt;span class="nv"&gt;as&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="nv"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
    &lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;v-calendar-events&lt;/span&gt; &lt;span class="nv"&gt;operator&lt;/span&gt; &lt;span class="nv"&gt;as&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="nv"&gt;event&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
      &lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;v-calendar-event&lt;/span&gt; &lt;span class="nv"&gt;event&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
    &lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;v-calendar-events&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
  &lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;each&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;v-valendar-arrangements&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在 &lt;code&gt;{{v-calendar-arrangements}}&lt;/code&gt; 的内部其实是有两次对 &lt;code&gt;operators&lt;/code&gt; 数组的遍历的，这是因为 &lt;code&gt;{{v-calendar-holders}}&lt;/code&gt; 和 &lt;code&gt;{{v-calendar-events}}&lt;/code&gt; 在 DOM 结构上是同辈关系。这里我其实会有很多种选择，比如说可以把 &lt;code&gt;{{v-calendar-arrrangements}}&lt;/code&gt; 的内部结构完全隐藏起来，也就是使用行内组件，如此一来，两次遍历都在更内层，上面的代码会更干净一些。之所以没有这么做有一部分原因是为了更舒服的绑定动作（即 Actions），这个我们会在后面讲到。另外一部分原因是我希望在观察整体的结构时可以更直观一些——我们已经在使用 &lt;code&gt;{{v-calendar}}&lt;/code&gt; 的时候做到足够简洁了，而对于组件的维护者而言继续深入下去也没必要一层一层的去打开不同的文件，这样会很累。&lt;/p&gt;

&lt;p&gt;我知道 &lt;code&gt;{{v-calendar-holders}}&lt;/code&gt; 的实现比较简单，以后也不会有太多的修改，所以把遍历隐藏起来省点地方；我也知道 &lt;code&gt;{{v-calendar-events}}&lt;/code&gt; 以下的部分则是会频繁修改的地方，所以及早展开也无妨，更何况里面的 &lt;code&gt;{{v-calendar-event}}&lt;/code&gt; 也是需要去直接绑定很多动作的，所以就暴露在这里了。那么块级组件形态下的遍历是如何被隐藏在组件内部的呢？看一下 &lt;code&gt;{{v-calendar-holders}}&lt;/code&gt; 的模版：&lt;/p&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="c"&gt;{{!-- v-calendar-holder/template.hbs --}}&lt;/span&gt;

&lt;span class="k"&gt;{{#if&lt;/span&gt; &lt;span class="nv"&gt;hasBlock&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
  &lt;span class="k"&gt;{{#&lt;/span&gt;&lt;span class="nn"&gt;each&lt;/span&gt; &lt;span class="nv"&gt;operators&lt;/span&gt; &lt;span class="nv"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt; &lt;span class="nv"&gt;as&lt;/span&gt; &lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="nv"&gt;operator&lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"v-calendar-holder"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;yield&lt;/span&gt; &lt;span class="nv"&gt;operator&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;{{/&lt;/span&gt;&lt;span class="nn"&gt;each&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;span class="k"&gt;{{/if}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就这样很简单的。&lt;/p&gt;
&lt;h3 id="小结"&gt;小结&lt;/h3&gt;
&lt;p&gt;在设计复杂组件的结构时，我感觉到 DDAU 里的 Data Down 原则其实会带来很大的影响。早期的设计几乎完全就是参照 UI 的需要，或者更确切地说是以 HTML＋CSS 结构为中心来安排组件的结构，其实这也没有错，只是同样的 HTML＋CSS 结构使用行内或块级组件都可以做到的时候，就少了一个指标来引导你去做出合适的设计决策。当你实现过一遍，体会过里面数据流转的各种情况之后，对组件结构设计的感受就升华了，忽然之间就对这件事情特别有底气了。然而影响组件设计的重要指标还有一个，那就是 DDAU 里的 Actions Up。接下来我们就把流程反转，来探讨一下复杂组件设计里的动作。&lt;/p&gt;
&lt;h2 id="Bubble Up? or Closure?"&gt;Bubble Up? or Closure?&lt;/h2&gt;
&lt;p&gt;虽然此标题也是俩问号，但最终的结论却不像 &lt;u&gt;Inline? or Block?&lt;/u&gt; 那样任君选择。我先把结论摆前头：&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;即便 Closure Actions 还未能发挥出它 100% 的能力（在本文写作时的当前版本，即 v2.2.0 下），但是也应当坚持使用 Closure Actions 而不是 Bubble Up Actions&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;有时候可以有选择的使用 Event Bus 模式，不过这和 Bubble Up Actions 还是有区别的（尽管原理上相通）。后面会讲到这一点，但是不影响这个结论。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;目前这个时期的 Ember 在 Actions 这个话题上有些尴尬，因为有两种 Actions 系统并存着，而且它们用的 Template Helper 是一样的，只是写法不同。这个设计在当下会让开发者们感到非常混乱，如果不了解来龙去脉很容易就把自己搞得晕头转向。我先帮大家把这个事情理理头绪：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Bubble Up Actions&lt;/strong&gt; 是旧的 Actions 系统，原理基于浏览器的事件委托，利用事件冒泡机制自底向上发送动作；&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Closure Actions&lt;/strong&gt; 是新的 Actions 系统，原理基于函数闭包，利用函数是“一等公民”的特性将函数作为值在应用中传递，使得基于 Actions 的通讯变得更灵活、更纯粹（所谓纯粹，意指 Action 通讯是纯函数式的，不依赖事件委托——当然还是靠事件触发的，注意两者的区别）；&lt;/li&gt;
&lt;li&gt;Bubble Up Actions 是传统的浏览器事件模型的实现，在过去是所有 JavaScript 应用程序离不开的东西。以前我们以 DOM 为中心进行 UI 编程，事件系统和我们开发的关系非常紧密，而基于选择符的 DOM 操作也和事件系统很合拍，所以大家都觉得顺理成章；&lt;/li&gt;
&lt;li&gt;进入组件化时代之后，因为 DOM 自身存在的缺陷难以克服，再加上以 React 为代表的众多视图引擎的推动，我们不再以 DOM 为中心进行 UI 编程了，最显著的特征就是基于选择符的 DOM 操作不再必要了；&lt;/li&gt;
&lt;li&gt;然而，组件最终还是渲染成为 DOM 对象的，它们终究还是树状结构的文档对象模型，当事件向上传递的时候，难免会涉及到指定事件委托节点这样的事情；当我们不再用选择符去操作 DOM 了，怎么去指定事件委托节点呢？&lt;/li&gt;
&lt;li&gt;多数的现代前端框架都会将应用程序的根节点（比如 &lt;code&gt;document.body&lt;/code&gt; 或是就近的某个 DOM）做为整个应用程序的事件委托节点，如果你注册一个 Action 在根节点，那么调用这个 Action 的请求（事件）无论距离此节点多远都要逐级向上冒泡才能到达这里；&lt;/li&gt;
&lt;li&gt;如果应用程序是组件化的，那么可以预见随着应用程序变得复杂，组件的层次也会越来越深，对吧？然而很多 Actions 都是要去修改数据／发起请求的，按照 DDAU 的原则，数据不应该在下游被改变，那么相应的 Actions 也应该保持位于上游；&lt;/li&gt;
&lt;li&gt;OK，矛盾就此产生——我想不需要再解释了吧？&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;事件机制在现代 Web 应用的开发里还有别的问题（当然了，也许是因为现在的开发框架们都还不够成熟导致的），我在旁观一些社区动态的时候看到很多相关的讨论，有些东西我也是懵懵懂懂……不过有一点可以肯定，这不是 Ember 一家面临的问题，家家都有遇到，家家也都有自己的一套。&lt;/p&gt;

&lt;p&gt;比如说事件对象吧（Event Object），每个框架都要想办法处理这个事情（跨浏览器啊）。Ember 从早期开始就在底层依赖 jQuery 搞定这个，所以 Bubble Up Actions 传递的事件对象其实不是原生的，而是 &lt;code&gt;jQuery.Event&lt;/code&gt;。现在随着大家都逐渐学会了使用 &lt;u&gt;state&lt;/u&gt;，对这个的依赖也越来越少了，那么 Ember 也在逐渐的做着让 jQuery 变成可选项的事情；React 呢？从开始就彻底一点了，自己做了一个 Event 系统叫 &lt;code&gt;SyntheticEvent&lt;/code&gt;，如果你要使用原生事件对象，就得单独用 &lt;code&gt;nativeEvent&lt;/code&gt; 属性去接收（兼容性问题自理），而它的 &lt;code&gt;SynthticEvent&lt;/code&gt; 对象是很轻很轻的，就十来个属性方法（本来人家就希望你少依赖 DOM 里的东西）。再说一个 Vue.js，它搞了一个和 Angular 一样的 Event 系统：dispatch all the way up to &lt;code&gt;$root&lt;/code&gt;，broadcast all the way down to any listener 这样子，和前面说的别无二致。&lt;/p&gt;

&lt;p&gt;其实自定义的事件对象 Ember 也有，就是 &lt;code&gt;Ember.Application.customEvents&lt;/code&gt;，以前用在 &lt;code&gt;Ember.View&lt;/code&gt; 里的，后来搞起了 Glimmer，View 就废弃了。&lt;/p&gt;

&lt;p&gt;其实 Event Bus 这样的机制 Ember 也可以搞，最简单的方式就是 &lt;a href="http://emberjs.com/api/classes/Ember.Evented.html" rel="nofollow" target="_blank" title=""&gt;&lt;code&gt;Ember.Evented&lt;/code&gt; mixin&lt;/a&gt;，也可以基于此写一个全局的事件总线，这样就可以跨越重重组件结构，想怎么整就怎么整。如果你对这样的 UI 编程方式很有爱的话，后面我会单独写一节实现一个 Global Event Bus Service。&lt;/p&gt;

&lt;p&gt;但是这些在 Ember 里都不是主要的通讯方式，我们先来说说 Bubble Up Actions 吧。&lt;/p&gt;
&lt;h3 id="捉摸不定的泡泡"&gt;捉摸不定的泡泡&lt;/h3&gt;
&lt;p&gt;鉴于目前是一个 helper 同时用于两套 Actions 系统，我认为非常有必要从形势上分清楚两者的差异。下面我将以尽可能简单的方式列举 Bubble Up Actions 的用法和特性：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;提醒：前面已经说过 Bubble Up Actions 是不推荐使用的 Actions 系统，然而由于向后兼容和历史遗留的一些原因，这个 Actions 系统在 Ember 3 之前都不会消失，而且大量的教程，包括现存的一些官方文档都还有它的痕迹残留着。我在这里之所以要花时间和篇幅讲这个系统，一是为了总结，二十为了在你混淆的时候有一个能明辨真相的地方。如果你真的完全不想搭理这个旧的 Actions 系统，我很赞成的，往后跳吧！&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;要理解 Bubble Up Actions 是如何被接收（或者说绑定）以及如何被执行的，首先得特别清楚两个概念，一个是执行环境（context），另外一个是冒泡链（bubble chain）。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;执行环境：action 在哪个模版被接收的，这个模版所对应的作用域对象就是 action 的执行环境。一般模版对应的作用域对象就是控制器，而组件模版的作用域对象自然就是这个组件了。等到 Routable Components 一统江山以后，这事就简单了，没有控制器什么事儿了。&lt;/li&gt;
&lt;li&gt;冒泡链很简单，就是执行环境一层层向上追溯，直到 &lt;code&gt;ApplicationRoute&lt;/code&gt; 为止。

&lt;ul&gt;
&lt;li&gt;这里面有路由什么事儿呢？比方说一个 action 是在 &lt;code&gt;events/edit/template.hbs&lt;/code&gt; 被绑定的，那么当它被执行的时候，它的寻找顺序是：
&lt;code&gt;EventsEditController&lt;/code&gt; -&amp;gt; &lt;code&gt;EventsEditRoute&lt;/code&gt; -&amp;gt; &lt;code&gt;EventsRoute&lt;/code&gt; -&amp;gt; &lt;code&gt;ApplicationRoute&lt;/code&gt;
在此四层之中，第一个被发现的 &lt;code&gt;events.action&lt;/code&gt; 会被执行，如果这个函数返回 &lt;code&gt;true&lt;/code&gt;，那么冒泡继续，一直到找不到下一个 &lt;code&gt;events.action&lt;/code&gt; 或者是一直走到了 &lt;code&gt;ApplicationRoute&lt;/code&gt; 为止。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;其实 Bubble Up Actions 就这么简单，以下是几个展示其用法和特性的例子：&lt;/p&gt;

&lt;p&gt;首先我们假设有这样的 &lt;code&gt;ApplicationController&lt;/code&gt; 和 &lt;code&gt;ApplicationRoute&lt;/code&gt;（我就不搞太深的 Bubble Chain 了）：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// application/controller.js&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;Ember&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;bubbleAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`#bubbleAction in controller...`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// application/route.js&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;bubbleAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`#bubbleAction in route...`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我在两个 &lt;code&gt;bubbleAction&lt;/code&gt; 里都打印了 &lt;code&gt;event&lt;/code&gt; 对象，然而在后面的例子里，不是每种情况都有办法获得 &lt;code&gt;event&lt;/code&gt; 对象。我会把打印的结果写在注释里，没有的话就是 &lt;code&gt;undefined&lt;/code&gt;，有就是 &lt;code&gt;jQuery.Event&lt;/code&gt; 或者 &lt;code&gt;Event&lt;/code&gt;，这样你明了了。&lt;/p&gt;
&lt;h4 id="1. Default Bubble Up Action"&gt;1. Default Bubble Up Action&lt;/h4&gt;&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="c"&gt;{{! application/handlebars.hbs }}&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;action&lt;/span&gt; &lt;span class="s2"&gt;"bubbleAction"&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Bubble Action&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// #bubbleAction in controller... undefined&lt;/span&gt;
&lt;span class="c1"&gt;// #bubbleAction in route... undefined&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;打印了两次，这就是冒泡的效果。&lt;/p&gt;

&lt;p&gt;在 &lt;code&gt;ApplicationController.actions.bubbleAction&lt;/code&gt;  方法里：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;如果返回除了 &lt;code&gt;true&lt;/code&gt; 以外的任何值，都不会冒泡&lt;/li&gt;
&lt;li&gt;如果有 &lt;code&gt;this.send('anotherAction'[, args])&lt;/code&gt;，那么 &lt;code&gt;ApplicationController.actions.anotherAction&lt;/code&gt; 会被调用

&lt;ul&gt;
&lt;li&gt;同时，如果 &lt;code&gt;ApplicationController.actions.anotherAction&lt;/code&gt; 里面也有 &lt;code&gt;return true&lt;/code&gt;，那么此方法也会冒泡&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="2. actions 命名空间"&gt;2. &lt;code&gt;actions&lt;/code&gt; 命名空间&lt;/h4&gt;
&lt;p&gt;为什么所有的 Actions 都要放在 &lt;code&gt;actions&lt;/code&gt;  对象里？其实就是为了提供一个命名空间，一是方便 Ember 查找方法名，二是便于开发者给方法命名。&lt;/p&gt;

&lt;p&gt;不过默认的命名空间其实是可以改变的，假设我在 &lt;code&gt;ApplicationController&lt;/code&gt; 里加了这样的代码：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// application/controller.js&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;Ember&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;bubbleAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`#bubbleAction in controller...`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="na"&gt;customActions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nf"&gt;bubbleAction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`customized #bubbleAction in controller...`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后测试以下两种写法：&lt;/p&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="c"&gt;{{! application/handlebars.hbs }}&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;action&lt;/span&gt; &lt;span class="s2"&gt;"bubbleAction"&lt;/span&gt; &lt;span class="nv"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"actions"&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Bubble Action&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;action&lt;/span&gt; &lt;span class="s2"&gt;"bubbleAction"&lt;/span&gt; &lt;span class="nv"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"customActions"&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Custom Action&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// #bubbleAction in controller... undefined&lt;/span&gt;
&lt;span class="c1"&gt;// customized #bubbleAction in controller... undefined&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;target&lt;/code&gt; 这个属性的默认值是 &lt;code&gt;Ember.Router&lt;/code&gt; 的实例，它是一个单例对象，它有能力遍寻整个应用去找到正确的 action，当然是在遵循默认规则的前提下。如果你不关心规则，你可以手动指明它。但是你要小心！改变 &lt;code&gt;target&lt;/code&gt; 会改变 Bubble Up 的规则，这就是为什么上面的例子里都没有冒泡的缘故。那么 &lt;code&gt;target&lt;/code&gt; 能指明哪些东西呢？只要是当前执行环境能访问到的东西都可以！&lt;/p&gt;

&lt;p&gt;这其实很强大很强大！比方说你把一些特定的 actions 封装到了一个 Service 里面，你可以把这个 Service 注入到 Controller 里然后做为 &lt;code&gt;target&lt;/code&gt; 来直接调用：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// application/controller.js&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;Ember&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;calendarActions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Ember&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;inject&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;service&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;calendar-actions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="c"&gt;{{! application/handlebars.hbs }}&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;action&lt;/span&gt; &lt;span class="s2"&gt;"addEvent"&lt;/span&gt; &lt;span class="nv"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"calendarActions"&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Add Event&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;再比如说你用 Ember Data 的，你的 Model 有实现自身相关的方法，那你也可以把 &lt;code&gt;target&lt;/code&gt; 指向 Model 实例，嗯……你可以试试。&lt;/p&gt;
&lt;h4 id="3. Bubble Up Actions on Components"&gt;3. Bubble Up Actions on Components&lt;/h4&gt;</description>
      <author>nightire</author>
      <pubDate>Sat, 28 Nov 2015 17:08:20 +0800</pubDate>
      <link>https://ruby-china.org/topics/28230</link>
      <guid>https://ruby-china.org/topics/28230</guid>
    </item>
    <item>
      <title>[上海外包项目] Redmine 二次开发</title>
      <description>&lt;p&gt;现在有一客户使用 Redmine 做项目管理，需要根据业务需求进行一些定制开发，客户很靠谱，技术型大公司只不过不是 IT，地理位置在上海。&lt;/p&gt;

&lt;p&gt;由于我不是甲方，只是帮忙找人，所以具体需求我不是很了解，大体听了一下不是很复杂，唯一要注意的是他们的 Redmine 配置了单点登录，我不清楚对二次开发有没有影响，这个你心里有数就好。&lt;/p&gt;

&lt;p&gt;有空闲想赚点外快的，发邮件到 dev.yufan # gmail，留下你的电话联系方式，我好介绍。最好是上海本地的，客户那边有意向长期合作把这块的业务承包下去。&lt;/p&gt;</description>
      <author>nightire</author>
      <pubDate>Fri, 18 Sep 2015 04:32:21 +0800</pubDate>
      <link>https://ruby-china.org/topics/27380</link>
      <guid>https://ruby-china.org/topics/27380</guid>
    </item>
    <item>
      <title>[Tips on Ember 2] How components work when reaching out the DOM boundary of App?</title>
      <description>&lt;p&gt;这一篇还是一个简单的例子所引发的思考。&lt;/p&gt;

&lt;p&gt;你看，如今的框架和库，无论规模大小功能多少，它们在本质上都朝着“组件化”的思路快速演进着。Angular 有 directives，Angular 2 应该也还是这个叫法；Ember 从 View 过渡到了 Component，并且接下来的迭代会朝向 WebComponent 的标准来设计生命周期及其 API；React 自身就是一个组件化的范式；还有 Polymer，那就是 Google 为 WebComponent 搞得一套 polyfills……大家都这么玩。&lt;/p&gt;

&lt;p&gt;我们都无法完整精确的预测 WebComponent 能让 Web 进化到什么程度，但是现在已经有了这么多可用的工具了，大家自然是跃跃欲试的搞起。我也没有例外，在之前使用 Angular 的时候就在尽力做组件的抽象，把 directives 那一套算是玩儿转了——那个时候并没有意识到一件事情，直到最近返回 Ember 之后才开始有所体会。几天前我在 Ember Community 提了个&lt;a href="http://discuss.emberjs.com/t/is-there-an-attribute-level-component/8763" rel="nofollow" target="_blank" title=""&gt;问题&lt;/a&gt;来讨论此事，虽然也得到很多建议却还是模模糊糊的；后来又和 &lt;a href="/darkbaby123" class="user-mention" title="@darkbaby123"&gt;&lt;i&gt;@&lt;/i&gt;darkbaby123&lt;/a&gt; &lt;a href="https://github.com/wppurking/ember-cli-todos/issues/1#issuecomment-139703491" rel="nofollow" target="_blank" title=""&gt;询问了一番&lt;/a&gt;，感谢他给了我很多启迪。然而始终没有想到一个确切的应用场景来验证一番。&lt;/p&gt;

&lt;p&gt;说了半天估计你们都看糊涂了：到底什么事情啊？&lt;/p&gt;

&lt;p&gt;Components 用来封装可重用的 HTML＋CSS＋JavaScript 片段，这很好，然而当下的框架们都要处理事件委托的问题，这就意味着框架会给你一个范围来开发你的应用，这个范围的边界就是框架用于事件委托的临界线，出了这条线就是浏览器自身的事件处理机制在作用了。那么，当你不得不“迈过”这条线的时候，框架（以及它提供的组件化机制）该如何帮助你呢？&lt;/p&gt;

&lt;p&gt;举例来说，Angular 的边界在你声明 &lt;code&gt;ng-app&lt;/code&gt; 的地方（所以我见过不少人把它放在 &lt;code&gt;&amp;lt;html&amp;gt;&amp;lt;/html&amp;gt;&lt;/code&gt; 上来扩大这个界限）；Ember 默认在 &lt;code&gt;&amp;lt;body&amp;gt;&amp;lt;/body&amp;gt;&lt;/code&gt; 上，当然你可以改；React 也是一样，你总是要把你的第一个 component 渲染到 &lt;code&gt;&amp;lt;body&amp;gt;&amp;lt;/body&amp;gt;&lt;/code&gt; 下面。那么，当你要做的事情超出 &lt;code&gt;body&lt;/code&gt; 之外，它们应该如何处理呢？&lt;/p&gt;

&lt;p&gt;对于像 jQuery 这样以 DOM 为中心（当然还有 BOM）的工具来说，这个问题很简单——以 DOM 为中心就意味着浏览器有什么你就用什么，无非就是它原生的 API 不好用或不够用，你拿过来用 jQuery 封装一下就好了，换汤不换药。所以当你要操作 &lt;code&gt;body&lt;/code&gt; 以外的东西，原生的东西随便你用，比如 &lt;code&gt;document&lt;/code&gt;（DOM），比如 &lt;code&gt;window&lt;/code&gt;（BOM）等等，你唯一要做的就是外面套一个 &lt;code&gt;$()&lt;/code&gt; 壳子。&lt;/p&gt;

&lt;p&gt;在我们的大脑模型里已经习惯了把 HTML 和 DOM 视为一体，但除此之外 DOM 和 BOM 还提供了丰富的接口来处理很多额外的事情，每一个框架或库都会多多少少提供这些额外接口的封装，比如 Angular 里的 &lt;code&gt;$cookie&lt;/code&gt;，&lt;code&gt;$location&lt;/code&gt;，甚至干脆彻底的 &lt;code&gt;$document&lt;/code&gt; 和 &lt;code&gt;$window&lt;/code&gt;，然而当这些东西和 component 关联在一起的时候，事情会变得微妙起来：什么可以做不可以做？何时／何处来做？这些问题的界线变得摇摆不定。&lt;/p&gt;

&lt;p&gt;Angular 有 DI（依赖注入）的机制，在 directives 的层面上，它巧妙的设计了一个 Attribute Level 的 directive 定义，通过 DI 你可以把超出 &lt;code&gt;body&lt;/code&gt; 以外的操作通过 HTML 的属性绑定给其他的 Tag Level 的 directives。因为组件的存在范围被限制在 &lt;code&gt;body&lt;/code&gt; 以内，这就是这种机制（目前）存在的意义所在。我们还不知道当 WebComponents 尘嚣落定之时会给出我们怎样的答案，当然届时 JavaScript 已经有了 modules，所以全局污染的问题已经不复存在，现在唯一不明朗的就是如何与组件的生命周期关联起来。&lt;/p&gt;

&lt;p&gt;让我们来看一个例子。现在有很多应用都有这样的设计：Header 与 Main Content 没有明显的界限，看起来像一个整体。但如果 Main Content 的内容超出了浏览器一屏的高度，那么当用户向下滚动的时候，Header 会“浮”起来（通过下放的阴影）并固定在窗口顶部，很不错的视觉效果。&lt;/p&gt;

&lt;p&gt;问题就在于监听用户滚动事件的动作应该是发生在 BOM 范围内的，如果你的应用是基于以组件为中心的思想开发的，这个动作到底应该在哪里做？&lt;/p&gt;

&lt;p&gt;这个问题其实会有很多变数，比如说你可以设想这个动作和任何具体的组件无关，而是在应用程序初始化的时候直接执行。很好，但是有两个问题：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;固定 Header 并为它添加阴影是需要 Header 已经存在于 DOM 之中的，通常在应用程序初始化的时候这个条件尚未达成&lt;/li&gt;
&lt;li&gt;这个动作并不是发生在全局范围之内的，比如说某个路由进入之后或某种组件渲染之后才发生&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;以上任意一点都可以否决初始化执行这个方案，如果你考虑长远和周全一些的话就必须另寻出路。&lt;/p&gt;

&lt;p&gt;好，我不废话了，先把最近用 Ember 完成的这个例子代码写出来，最后我再说一点对此的想法吧。&lt;/p&gt;
&lt;h3 id="第一步，把 Header 抽象为组件"&gt;第一步，把 Header 抽象为组件&lt;/h3&gt;
&lt;p&gt;这个很简单，直接 &lt;code&gt;ember generate component app-header&lt;/code&gt; 就好了，代码略过。&lt;/p&gt;
&lt;h3 id="第二步，在组件渲染之后执行监听用户向下滚动的事件并为组件添加 class，这个 class 完成了阴影等效果。"&gt;第二步，在组件渲染之后执行监听用户向下滚动的事件并为组件添加 class，这个 class 完成了阴影等效果。&lt;/h3&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SCROLL_THRESHOLD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;  &lt;span class="c1"&gt;// header' height is 50px&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;Ember&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;classNameBindings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sticky&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="nf"&gt;didInsertElement&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scroll&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scrollY&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;SCROLL_THRESHOLD&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sticky&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sticky&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="第三步，当组件销毁后，注销监听回调"&gt;第三步，当组件销毁后，注销监听回调&lt;/h3&gt;
&lt;p&gt;这就可以发生在 &lt;code&gt;body&lt;/code&gt; 以外的操作能和组件的生命周期紧密联系在一起。&lt;strong&gt;这一点很重要，不管你用 Ember 还是 Angular／React，一定要注意组件的生命周期，特别是组件销毁时这些框架都会提供对应的 hook，要注意清理“垃圾”，移除绑定，释放内存等等，避免内存泄漏&lt;/strong&gt;。&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;SCROLL_THRESHOLD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;  &lt;span class="c1"&gt;// header' height is 50px&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;_stickHeaderHandler&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scrollY&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nx"&gt;SCROLL_THRESHOLD&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sticky&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sticky&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;Ember&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;classNameBindings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;sticky&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="nf"&gt;didInsertElement&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scroll&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_stickHeaderHandler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;  &lt;span class="c1"&gt;// remember to bind!!!&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="nf"&gt;willDestroyElement&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;removeEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;scroll&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_stickHeaderHandler&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="DONE"&gt;DONE&lt;/h3&gt;
&lt;p&gt;我们还可以怎样改进它呢？问题有二：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;组件不应该固化特殊的行为，如果这个组件是跨应用共享的（比如你发布成 Addon），那么其他应用可能是不需要置顶的使用者期望的是如下的可选项：&lt;/p&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;app-header&lt;/span&gt; &lt;span class="nv"&gt;stickyOnScroll&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;50&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;监听滚动那一套行为如果不是组件特有的（这就派出了发步成 Addon 的条件）而是应用内共享的，则应该想办法抽象出去——监听滚动这个事情很典型&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;对于问题一，答案已经揭示在那里了。组件都是可以传递参数或外部作用域的，利用此机制进行判断来执行可选行为，这是对用户友好的举措。&lt;/p&gt;

&lt;p&gt;对于问题二，在 Ember 里你至少有三个选项：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;抽象成 Mixin。这个很直观，缺点是 Mixin 提供的属性不是 default value，它不能由你主动去覆盖，不够灵活；&lt;/li&gt;
&lt;li&gt;定义成新的 Component。需要继承的其他组件可以 extend 它，解决 Mixin 不够灵活的问题，局限是只能给组件用——不过对于处理浏览器事件和操作 DOM／BOM 已够用了；&lt;/li&gt;
&lt;li&gt;抽象成 Service。这个等价于 Angular 的 DI，可以由你自己定义丰富的接口来配置和调用，最灵活，适合封装需要的外部接口等等。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;关于 Service，具体的代码先 hold，以后我会专门讲 Service 在 Ember 里的用法。今天这个例子不适合抽象 Service，原因就是上面的第二点。&lt;/p&gt;

&lt;p&gt;当我在几天前对此还很困惑时，我一度认为像 Angular 的 Attribute Level Directives 才是处理此类问题的最佳方案，然而 WebComponent 并没有 Attribute Level Component 这种设计，这也是我困惑的最初原因。现在想一想，Attribute Level Directives 等于无视组件的生命周期（当然它有自己的生命周期，但是和要附着的目标组件无关，你得管理两份），它把可选行为附着于目标组件的过程等同于你创建一个新的特殊的 Service（特殊之处就在于它可以放在模版里），然后利用这个 Service 去写实现代码并且还可以再 DI 其他的 Services，以此来实现可选性和可复用性。这种设计乍看讨巧但也有很多缺点，比如说多个 directives 共存的时候要考虑优先级和行为覆盖的问题，比如说和未来的 WebComponents 不兼容改造起来很费事，等等。&lt;/p&gt;

&lt;p&gt;现在我们看到，React 一开始做得就很不错（后起之秀借鉴了很多前辈们的经验教训），不过它只是一个渲染引擎，做大型应用还需要你在整体架构上下功夫；Ember 的架构很完整，以前的问题很多但现在都在一一完善，设计思路没有什么错误，拿来做 UI 交互复杂的 web 应用的确是很不错的选择。&lt;/p&gt;</description>
      <author>nightire</author>
      <pubDate>Wed, 16 Sep 2015 13:28:31 +0800</pubDate>
      <link>https://ruby-china.org/topics/27359</link>
      <guid>https://ruby-china.org/topics/27359</guid>
    </item>
    <item>
      <title>前后分离架构的探索之路</title>
      <description>&lt;p&gt;大约五年前，那时候我还是一个小小讲师（苹果 AATC 培训认证），完全不懂编程为何物的菜鸟，一个偶然的机会让我进入了公司的开发部门，任职什么呢？用户体验设计师，原因很操蛋——我以前干过广告设计，做过餐饮服务行业，因而我有两个优势：能聆听和揣摩客户的需求，然后能做一些图。&lt;/p&gt;

&lt;p&gt;那时候很多像我们公司一样的中小 IT 企业（200 人左右，组成成分主要是大大小小的项目团队）都有要做自主产品的诉求，这是市场决定的：出门找生意越来越难了。于是很多野路子出家的产品研发团队就这样诞生了……&lt;/p&gt;

&lt;p&gt;说是产品研发团队，其实都只是一群习惯了听命于人去按照 RFP 实现功能的码农罢了，和其他项目组相比唯一的差别大概就是“尚有梦想的咸鱼”而已。所以研发过程中的种种幼稚和操蛋，你用脚趾头都能猜想得到。&lt;/p&gt;

&lt;p&gt;一开始我就是把各位老大的设想整理成人人看得懂的需求，然后把它们串起来画成草图（mockup）再交给各种工程师去实现好了，这个角色类似于如今很时髦的“产品经理”。然而很快我发现大家老是加班，为什么呢？调 CSS 样式！&lt;/p&gt;

&lt;p&gt;做惯了平面设计的我并不懂得把画出来的东西变成浏览器里的东西会有多麻烦。（今天，我在面试一些切图页面仔时，听他们大谈特谈像素级还原尚觉得好笑，但想想五年前的自己还是很有些羞愧的……然而更令我无语的是：到了如今初出茅庐的小前端们还把像素级还原的切页面当成是至高无上的本事，这件事情本身是不是很令人“沮丧”呢？）当写页面的同事一再告诉我我画的东西不实际之后，我憋不住了——我就不信我画的东西实现不出来！&lt;/p&gt;

&lt;p&gt;抱着一口“怨气”，我义无反顾的踏上了 HTML＋CSS 这条路，其中过程不用多讲，唯一的金玉良言只有一条：别看国内的教程，别信 w3school 之类的拼凑资源站。总之这事儿的结果是半年以后整个项目组几乎所有的页面都是我来写了。（今天，已然成为前端架构师的我，所有页面的自定义样式还是得我亲自写，我不怪任何人因为我知道在很多工程师内心里还是瞧不上写 HTML＋CSS 的技术的，你们不愿意学我不勉强，我来。我还要感谢你们，为了能把饭喂到诸位的嘴里，我花费大量的时间学习 CSS 框架的开发，从而精通了整个生态链，从 pre-processing 一直到 post-processing）。&lt;/p&gt;

&lt;p&gt;这个世界就是这样：一旦你专精了一项技能，你会很容易看出相关的技能在目前的水准如何。古人说：水涨船高。诚不欺我也。&lt;/p&gt;

&lt;p&gt;所以成天耳濡目染 HTML＋CSS 的我，经受着各种国外大神的视频＋教程耳提面命的我，很快就明白了一件事：我做的这个叫前端开发，HTML＋CSS 只是起了一个头，后面还有一座叫 JavaScript 的大山等着我。而我们做的前端开发还很嫩，活该你成天加班改样式，修 bug，因为你一开始就没走对路数。&lt;/p&gt;

&lt;p&gt;幸好还不晚，不过这篇的主题是前后分离，所以我得按下快进按钮直奔主题而去。&lt;/p&gt;

&lt;p&gt;JavaScript 对我来讲太难太难了，但是 jQuery 尚可，因为它有非常棒的 API 设计和兼容性处理，很适合我这样的菜鸟入门。那时候有一个叫 Jeffery Way 的家伙录制了一套 30 天学会 jQuery 的教程让我受益匪浅，我认为他讲得好，主要是两个原因：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;他不主要讲各种 API 如何用，他从一开始就给我贯彻了一个重要的思想：学会看文档；&lt;/li&gt;
&lt;li&gt;他主要讲如何分析一个功能的实现，如何组织以 jQuery 为核心的代码逻辑；&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;在这里先插一段旁述。各位能在工作中使用 Rails 的同行们，你们是无比幸运的！因为 Rails 已经把 View－Template 这一环节梳理的足够简单，哪怕完全不懂 Rails 的页面仔，你稍微提点提点，他也能很快学会如何把静态页面套进 Rails 的模版里去。&lt;/p&gt;

&lt;p&gt;而我则是很不幸的，在那时我碰上了非常讨厌的 JSP！你们千万别笑，随便去问那些写页面出身的前端们对 JSP 是什么感受，绝对不会有好脸色的。对，我承认自己很菜。写静态页面我行，但转成 JSP 模版这件事在那时真的能把我难死！更要命的是，如果你把写好的页面交给后端工程师去套模版，最终的结果就是一塌糊涂！没错，他们根本不会细心周到的照顾你精心设计的每一个标签，他们会做出各种各样奇葩的事情来破坏原本完美的页面结构，逼迫你不停的修改样式和脚本来适应这些“补丁”。&lt;/p&gt;

&lt;p&gt;更要命的是调试！原本写 HTML＋CSS 一个轻量级编辑器就搞定了，但等他们转成 JSP 之后你再想去调试就没那么简单了。你需要：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;运行环境，比如 Java＋Tomcat，不懂吧？没事，学！&lt;/li&gt;
&lt;li&gt;生态链，比如 Maven 或 Gradel，不懂吧？没事，学！&lt;/li&gt;
&lt;li&gt;IDE，比如 eclipse 或 IDEA Intellij，不懂吧？没事，学！顺便一提，知道没接触过 Java 的人想跑起一个应用来有多难吗？我就是为此才爱上 Rails 的！&lt;/li&gt;
&lt;li&gt;……&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;就这样，为了调试 HTML＋CSS，你最终变成除了不会写 Java 代码外其他全都会的 Java 开发工程师。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;你们这些从后端出身的家伙们能体会到前端页面仔们迈出这一步需要多大的勇气和毅力吗？&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;你们能想象他们之所以不得不学做这些，就是因为你们无法认真对待 HTML＋CSS＋JavaScript 吗？&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;为什么要在后端的环境下做前端的事情？其实就为了三个字：擦屁股！&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;你可以说我们这一群人都很菜，我也承认，可是你要知道：环境不是时时处处都可以给你各种选择的，有时候你唯一能做的选择就是改变自己。那么作为一个只懂 HTML＋CSS＋皮毛 JavaScript 的我，能做出什么？不知从何时开始，“如果可以不再依赖任何环境就可以做好我们的份内之事就好了”这样幼稚的念头开始萦绕在我的脑袋里……&lt;/p&gt;

&lt;p&gt;回到 Jeffery 的视频教程，在其中的一节他演示了 Ajax 获取远程数据然后动态修改 DOM 的例子，当时的例子里用的是 Twitter 的 API，然后每隔一段时间拉取几条新数据让页面即时刷新这样子……&lt;/p&gt;

&lt;p&gt;不要笑，知道我当时有多震惊吗？我觉得我们就他妈的是一群傻逼好吗？&lt;/p&gt;

&lt;p&gt;第二天我慌不择路的把这段视频拿给后端架构师看，问他实现这样的东西，可行？他憋了半天：我们都是直接去数据渲染到 JSP 的，API 没做过……&lt;/p&gt;

&lt;p&gt;操！没做过难道不能做？我赶紧抛出了诱饵：如果搞的出来，以后你们再也不用套模版了！&lt;/p&gt;

&lt;p&gt;这货立马答应了……&lt;/p&gt;

&lt;p&gt;此后就是翻天覆地的折腾，我搜遍了所有能找到的资料，把它们翻成中文或者直接当面讲给后端听，有些东西我们都无法理解就先记下来，晚上回去我上 SO 问，上 Youtube 搜会议等资料看。&lt;/p&gt;

&lt;p&gt;然后他们告诉我，如果换 Spring 的话可能会比较简单，因为他们能百度到用 Spring 开发 API 的例子。于是我们就开始改造了。&lt;/p&gt;

&lt;p&gt;改造的第一步是不用写 JSP（或者少量的写），但是静态资源其实还是放在 Tomcat 容器里的，因为我们经过尝试发现跨域问题解决不了（是的，当时就是菜，连反向代理都不懂），不过没关系，反正我已经学会了本地跑 Tomcat 了，至少我们可以不用写 JSP 了嘛。现在回想一下当初搞前后分离的原始动机竟然就是为了不再去写 JSP，多么滑稽啊！然而反过来想想，这也映衬了一个事实：前端工程师们的生态环境是有多糟糕！&lt;/p&gt;

&lt;p&gt;再然后就是把 jQuery 修炼到满级开始无脑刷副本的无聊过程，当然在这个过程中也体验了一些新东西，比如前端的模版引擎（Jade／Handlebars／Art 等），模块系统（SeaJS／RequireJS）等等，JavaScript 的水平和理解有了长足的进步，终于开始有一个工程师的样子了。&lt;/p&gt;

&lt;p&gt;再之后就是大家都知道的剧本，node.js 横空出世，一下子 HTTP Server，API Service，Shell Scripting……等等这些统统都可以用 JavaScript 来搞了，npm bower grunt gulp……等等这些应运而生，忽然间前端开始有了自己的生态系统！尽管它还很弱小还很混乱，但是它给了我们这些野路子出身摸爬滚打浑身泥水的家伙们一道希望之光，它让我们看到：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;我们可以不依赖后端的运行环境：node.js&lt;/li&gt;
&lt;li&gt;我们可以有自己的生态圈：npm&lt;/li&gt;
&lt;li&gt;我们可以随心所欲使用各种方便的开发工具：所以我后来成了 vim 党&lt;/li&gt;
&lt;li&gt;……&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;我们可以有很多可能，我们可以把我们擅长的事情做得更棒而不需要后端哥哥们操心，我们可以省去很多后端要 cover 的工作让他们专心写好自己的代码，我们设想中的分离是有搞头的，不仅仅是为了分离而分离，而是为了更好的专精、多能、协作、管理而分离！&lt;/p&gt;

&lt;p&gt;何以如此狭隘的看待前后分离？时至今日我也不懂为什么有那么多人抱持着种种怀疑与偏见。&lt;/p&gt;

&lt;p&gt;你们不用担心数据层逻辑会有冗余，因为把 Model 的逻辑分摊到前端身上可以省去后端的部分代码和处理工作，而前端也可以更容易地按照业务来组合自己需要的 Model
你们不用担心视图层的缓存，因为分离后前端只存在静态资源，我们可以利用 CDN，利用 负载均衡，利用很多很多技术分摊过去必须让后端来承担的工作
你们不用担心页面渲染速度，首页怕慢我们可以交给服务端来渲染，或者在中间加一个很简单的 node server 来做首页静态化渲染，后面的事情交给前端就是，只快不慢
你们不用担心要为多个客户端做不同的资源调度，只要 API 规划得到，一套 Service 可以支持多个客户端的业务体系，而前端行有余力甚至可以写出多个版本来做 A／B 测试
你们不用担心 ……&lt;/p&gt;

&lt;p&gt;优点多了去了。&lt;/p&gt;

&lt;p&gt;不是说这些优点目前都很成熟，也不是说实现它们没有代价，但是你不去做就不可能成熟，代价也不是不可以有但关键是要看长远的收益。&lt;/p&gt;

&lt;p&gt;很现实的例子就是我们有一套系统本来是为自己做的，后来让客户知道了觉得很感兴趣希望为自己定制一份。我们分析了一下，发现现有的 API 已经可以满足用户的需求，只需要针对几个具体的业务逻辑再扩充几个接口让数据负载更合理便可，于是我们只用了三天就给客户出了一个完全可用且相当稳定的 demo。客户觉得满意，开始按照他们的 VI 重新设计一套 UI，然后剩下的事就是找几个页面仔把页面写出来，现成的 Angular 逻辑往上一套小改几处即可。&lt;/p&gt;

&lt;p&gt;你会觉得不值得？&lt;/p&gt;

&lt;p&gt;当然了，我在这里不是鼓吹前后分离信仰，不是强求所有的事情都需要分离来做。一个产品的轨迹是需要产品研发团队自己把控的，如果人云亦云流行什么用什么那也就和无脑儿没啥区别了。有些场景也的确不需要分离，比如说门户网站，CMS，Mini Site 这类的需求就可以沿用成熟的开发体系。不过我之前谈到过，探索和实践分离体系还有一个重要的好处，就是能够让你现有的前端开发团队摸索和整理出一套单兵作战的环境体系，即便是不用分离架构，我单纯用 node.js 写一套门户网站，CMS，Mini Site 这样的东西也不会比 Rails 慢啊！这样一来，我还是可以把后端的资源用在更重要的底层服务或业务逻辑去，把那些和页面 UI 交互相关，但又和数据层有着小小关联的业务交给前端独立完成，又有什么不好呢？&lt;/p&gt;
&lt;h4 id="前后分离＝SPA？SPA＝臃肿框架？"&gt;前后分离＝SPA？SPA＝臃肿框架？&lt;/h4&gt;
&lt;p&gt;这一点我觉得有必要分析清楚，标题里的两个问号是我见到过最多的误解。&lt;/p&gt;

&lt;p&gt;首先，前后分离是架构上的事情，第一次做肯定很痛苦，但做一遍之后好处还是很多的。举实例说明：&lt;/p&gt;

&lt;p&gt;我们做过一个会议的应用，这个应用一开始设计是没有 web 端的前台的，只有一个管理后台，前台都是移动端。基于这个原因，我们还是分离的（因为你得提供 API 给移动端，不分离还能怎么搞呢？），后台用成熟的 Angular 很快就做好了。&lt;/p&gt;

&lt;p&gt;没想到后来有一个额外的要求，用户要在创建会议的时候生成一套在线的会议手册，这个会议手册就是一个简单的多页面 CMS 系统，当用户创建新会议的时候在后台填写手册相关的内容，我们就要为它生成一系列的页面来显示（类似于 Mini Site），它有两个特定要求：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;不需要登录，公开访问。然而最初的设计是没有账号就不能参加会议，需要报名，所以我们后台和移动 App 都是直接先要求登录或注册的，相应的 API 请求也是如此，有鉴权控制的。&lt;/li&gt;
&lt;li&gt;要能多端访问，还要能嵌套在原生应用的 webview 里，因为加功能来不及了，只有一天时间。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;传统的架构你的写页面然后套模版去调试，虽然只有不到 10 页，但也是很费时间的。但我们已经分离了，现在为了这么一个额外的需求也不值得再倒退回去，那么怎么做的呢？&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;单独建一个会议手册的项目；&lt;/li&gt;
&lt;li&gt;模版用 Sass＋Handlebars 很快搞定；&lt;/li&gt;
&lt;li&gt;里面的接口请求为每一个会议服务商绑定一个 token（后来还在后台允许管理员重新生成和绑定 token），渲染页面时写死在  标签里（就好像 CSRF 的处理），以此绕过鉴权&lt;/li&gt;
&lt;li&gt;写一个简单的 node service，就干一件事：渲染 handlebars 模版&lt;/li&gt;
&lt;li&gt;创建会议的时候，Java API 传会议 ID 给 node service，把渲染好后的页面单独保存在静态资源服务器下（用 ID 创建独立目录），然后返回调用地址&lt;/li&gt;
&lt;li&gt;后台收到会议地址，嵌入一个 iframe 做手册预览&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;一天搞定，完事。值得一提的是，整个手册用了许多 HTML5 的新特性，比如 History API，SessionStorage，OfflineCache，GeoLocation，DesktopNotification，没有用 Polyfill，因为这些都是可选特性，不支持就不作用，关键是：从头到尾就是没用 jQuery——不是我跟 jQuery 过不去，的确用不着，还嫌大。更不要提 SPA 框架了，完全没有。&lt;/p&gt;

&lt;p&gt;所以你看，分离架构可以让我们很快完成这样的小任务，并且可以单独维护管理，也可以直接共享现有的 API 资源，它不一定只是为了 SPA 才分离，而且也没有什么技术难度。能用很短的时间完成还能保证质量，是因为我们有成熟的构建和 CI，如果换成是当初 JSP 那一套，光配置个本地环境就够够的了，其他我都不敢想象。&lt;/p&gt;

&lt;p&gt;分离是架构选择，决定了你如何管理、分配与协调现有的资源，至于你分离后要做 SPA 还是其他模式的应用那完全是你的自由，并不是捆绑一加一的强制性决策。去构建一个分离体系当然会有挫折有代价，没有人否认这个，然而一）这是可选的；二）你能否看到和利用它的好处。&lt;/p&gt;

&lt;p&gt;至于 SPA 一定是臃肿的吗？保持这种思想的我只能说你目光所见过浅。相比十年前的 web 开发，我能说现在 Rails 很臃肿吗？别说十年前了，就是今天一样也有人说 Rails too heavy！你觉得呢？那又怎样呢？还不是该用就用？水平高的自然知道拆分和减肥，连 Rails 自己都知道瘦身一个 Rails API 出来，你以为所谓“臃肿”是 SPA 框架的专利吗？SPA 之所以臃肿是有两个主要的现阶段环境因素决定的：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;非常多的新特性层出不穷，为我们开发更丰富强大的应用程序提供了武器和弹药。但是浏览器（及其他运行环境）和设备碎片化的问题导致这些新特性无法提供始终一致的表现或性能，于是各种框架就要在底层做兼容性的补充与改良，顺便还要为尚未形成标准的新特性重新封装 API 接口。比如说 Ember 干嘛要造一个 Object 接口出来？不就是因为 Observable 接口没有吗？有什么大不了的？ES2016 就有了（非常可能），或者你可以不用 Ember 自己的，用第三方的 Observable 组件来代替也行。
jQuery 做的事情和这有多大区别？没错，jQuery 是相对轻了，可是它负责的面儿也少啊，哪位用 jQuery 的不都得附带十个八个插件的？合在一起就轻了？&lt;/li&gt;
&lt;li&gt;相对的，前端工程这块业界整体的水平差距很大，牛的特牛，菜的特菜；但是菜的也希望用牛的工具，可又没那个底蕴解决牛的能解决的问题，于是牛的就把一个一个特性统统封装好联系在一起，让你尽可能快速简单的就能用到。
如果大部分的工程师都成长起来了，也就没有必要非得搞大而全的方案了，React 及其生态体系不就是一个很好的例子吗？不给你搞大而全，只给你搞小而专，你以为你把那一堆连起来用就不叫 SPA 了？幼稚！&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;再说一遍，SPA 是一种产品的技术形态，而不是特定某（几）种框架下的产物，满足这种技术形态的工具链可以臃肿也可以简洁，这是因为环境和人决定的。&lt;/p&gt;

&lt;p&gt;Single Page Application, not Some Particular Application&lt;/p&gt;

&lt;p&gt;前后分离还有一方面的作用。前端工程师都有一个普遍的特点：你让他们写个页面信手拈来，但是你让他们负责一个完整的业务多半就得抓瞎。为什么？因为他们太偏门。最近一两年我开始大量的面试和储备新人，十有八九都是这样的：HTTP？不懂！Ajax？懂！（你觉得合理吗？）jQuery 请求 API？会！Promise 用过？……没。换个说法，deferred 对象？哦哦，见到过！（你觉得合理吗？）&lt;/p&gt;

&lt;p&gt;诸如此类的问题屡见不鲜，让我对前端这个行业的未来充满忧虑。当初我也是从一窍不通的菜鸟开始，若那时没有“一定要摆脱 JSP”的幼稚理想，我怎么可能通过摸索前后分离让自己拥有今天这样相对全面的见识和理解？&lt;strong&gt;我走过的路让我明白，探索前后分离并不是像很多旁观者说的“为了分离而分离”，反而是“为了更好的理解 web 开发这回事而分离”。&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;因为当你开始摸索这条路，你就不得不面对许多根本性的问题，拿跨域资源共享来说吧，以前的架构前端工程师是极少需要面对这种问题的，但你只要一分离就必然会碰到，然后你就要去学诸如 JSONP，CORS，HTTP 协议，浏览器安全机制，PreFlight Request，反向代理等等技术细节。看似加重了学习成本（要我说，这些原本应该是学校的责任！），但作为同事，你希望你身边做的是个只会“追求像素级还原”的页面仔呢？还是对上述知识点有着扎实的理解和实践经验的工程师呢？&lt;/p&gt;

&lt;p&gt;说到这，就昨天有人在 SF 上问了个问题，大致是问：JavaScript 怎样才算学好了？总觉得需要自己能写一个库或框架出来才算学好了，大家怎么看？&lt;/p&gt;

&lt;p&gt;我刚好和人吵完了架，静下心想了想之后作出了如下回答：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;少年苦练 10 年拳术欲下山扬名立万，路遇一使刀汉子，数招后不敌惨败而归……回山后找师傅问话

“师傅，为何我苦练十年还会输？”

“因为你不知道打架不止可以用拳头。”

“可你也没告诉我啊！”

“你只说要学拳法，又没说学打架！”

“那我不学拳法了，我要学打架！”

“那就不只是要学拳法了，打架想要赢就得十八般武艺都学，你未必要门门精通，但你最起码得有这些见识。除此之外，还得学挨打，学疗伤，学逃跑，学追踪，学暗器，学使毒……想赢？哪有那么简单的！”

“那我还能成拳法宗师吗？”

“呵呵，如果你打架再也不会输，谁敢说你不是宗师？”
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个寓言想表达的意思是不言而寓的，我很赞同这里一位朋友说的：我们不应该有前端后端之分，我们可以有专精之处，但是对于 web 开发这回事该懂的都应该要懂，否则你怎么可能打得赢？同理，如果说后端工程师需要靠写页面来了解前端的话，那么前端也应该有类似的方式来了解后端做的一些事情。在这里探索前后分离就是一个很好的教学与实践相结合的手段。没有哪个页面仔会甘于永远切图写页面，他们也很羡慕后端哥哥们大神般的风骚，只是他们所处的环境造成了他们只知道数十年如一日的就懂切页面了，如果能多给他们一些提携与帮助，谁敢说他们以后不会成为江湖高手？&lt;/p&gt;

&lt;p&gt;很多人拿工作忙，缺人手，创业公司求效率等借口来回避在技术道路上的探索和进取，说真的我个人非常非常可以理解，我当初所做的事情其实和创业什么的也没多大区别，我们人手也很紧缺——今天我们只有三个人维护着四款前后分离架构的中大规模产品，这些产品有 Saas 版本的，还有大大小小十几个在客户那里独立部署的，你没看错就三个人！一个 Java 工程师，一个懂 Java 的前端工程师，再加上我这个什么都懂一点但什么都不专精的万金油。&lt;/p&gt;

&lt;p&gt;我们做的还不够好，但我们已尽力做到自己能做的最好，与我们这五年来碰到的风风雨雨相比较，探索前后分离这真的不算个大事儿好吗？&lt;/p&gt;

&lt;p&gt;作为前端工程师（并且是懂得和尊重后端开发的），我很欣慰能活跃在这个时代，就像有人说的：这是前端最好的时代，也是前端最坏的时代。然而历史无数次证明：真金不怕火炼，英雄应运而生。那些后端语言环境和框架体系难道没有经历过同样的革新与变迁？就因为我们过去是只会写 jQuery 的页面仔，所以我们就应该永远这样停滞不前？&lt;/p&gt;

&lt;p&gt;这就是我探索前后分离的过程和心得感想，主要是在离职前为过去五年做一个总结。写得比较凌乱也没什么技术含量，根本的意思还是要鼓励众多的前端同行们：学校没有我们的专业课，社会对我们的工作没有准确的认知和评价，这都不要紧！重要的是我们自己不能看轻自己的能力，不能放弃自己的价值。在学习和工作尚有余力的时候勇于探索吧，别管别人说什么，本事学到手才是最重要的，要记住：你是一个工程师，你不是一个页面仔！&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;题外话，不吐不快：&lt;/p&gt;

&lt;p&gt;我不是要在这里分出一个孰优孰劣，好坏是应该留给历史评判，留给后人做茶余饭后的谈资的，而作为从业者的我们应该保持向前看，永不放弃好奇的心理和探索的欲求。对于一群探索者，你不尊重也就罢了，就算不认同不看好，顶多坐在一边看戏还不够吗，如果非要给人扣上“xxxx 信仰”和“xxxx 神教”的帽子，那我就要对您的人品致以深深的同情了，哪怕你是大神亦不例外。&lt;/p&gt;</description>
      <author>nightire</author>
      <pubDate>Sat, 12 Sep 2015 01:24:28 +0800</pubDate>
      <link>https://ruby-china.org/topics/27293</link>
      <guid>https://ruby-china.org/topics/27293</guid>
    </item>
    <item>
      <title>2015 最新调查：现在的前端工程师都用什么？</title>
      <description>&lt;p&gt;推荐大家去扫一眼：&lt;/p&gt;

&lt;p&gt;&lt;a href="http://ashleynolan.co.uk/blog/frontend-tooling-survey-2015-results" rel="nofollow" target="_blank"&gt;http://ashleynolan.co.uk/blog/frontend-tooling-survey-2015-results&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;即使你完全不做前端开发，但身边也离不开前端伙伴们，看看这个调查可以知道你身边的前端工程师靠谱不靠谱。&lt;/p&gt;

&lt;p&gt;话说回来，看到 jQuery 和测试工具的结果还是令人挺沮丧的啊，尽管这两年前端发展日新月异，可是总体水平提升还是有点赶不上。&lt;/p&gt;</description>
      <author>nightire</author>
      <pubDate>Thu, 10 Sep 2015 04:26:41 +0800</pubDate>
      <link>https://ruby-china.org/topics/27265</link>
      <guid>https://ruby-china.org/topics/27265</guid>
    </item>
    <item>
      <title>[Tips on Ember 2] UI 布局与应用状态的关系处理</title>
      <description>&lt;h2 id="引子"&gt;引子&lt;/h2&gt;
&lt;p&gt;SPA（单页面应用）的核心是什么？&lt;/p&gt;

&lt;p&gt;自该类型应用诞生以来我最多思考的问题就是这个。现在前端 SPA 框架满天飞，许多不是框架的也被称作框架，究竟有什么代表性的层（layer）能让一个系统称得上是框架？&lt;/p&gt;

&lt;p&gt;我的答案是路由，而路由的本质就是一个状态管理器。没有路由机制的系统不能称之为框架，而路由机制做得不好的框架也算不上好框架（但可以算是好的工具集合，比如 Angular——详见&lt;a href="https://ruby-china.org/topics/24646?page=1#reply29" title=""&gt;我在 Ruby China 上曾经吐过的槽&lt;/a&gt;）。&lt;/p&gt;

&lt;p&gt;为什么这么说呢？我们都知道 HTML 是无状态的（stateless），做一堆 HTML 页面拼在一起那不叫“应用”，顶多称之为“内容系统”；在以前，HTML 网站上的状态管理是由后端的 Session 加前端的 Cookies 协作完成的，到了 SPA 的时代 Session 不是必须的了（尽管传统的 Session 机制也是可用的），UI 上的状态转移到了前端由 JavaScript 完全管控（由于 SPA 前后分离的特点），所以前端工程师担负起了更多的业务逻辑职责，相应的整个技术链上也必须有一个可靠的环节来帮助他们做状态管理这件事情。&lt;/p&gt;

&lt;p&gt;在前端框架的发展过程中路由的诞生是水到渠成的（基于一些新技术的成熟，比如 &lt;a href="http://diveintohtml5.info/history.html" rel="nofollow" target="_blank" title=""&gt;HTML5 的 History API&lt;/a&gt; 等等），但是应用开发工程师对于路由的理解和重视却还远远不够。如果说传统的前端开发是以页面为中心来入手的话，那么现代的 SPA 应用开发就是以状态为中心来着手设计和开发的。&lt;/p&gt;

&lt;p&gt;Ember 就是一款非常重视路由组件的 SPA 框架，本文借由一个实现 UI 布局的例子来谈谈 UI 编程与路由的关系，尽管这只是涉及到路由特性的一部分却也足够说明一些问题了。希望这个例子能让更多前端工程师认识和理解路由的重要性，从而更好的设计与实现 SPA 应用的各种功能场景。&lt;/p&gt;
&lt;h2 id="场景描述"&gt;场景描述&lt;/h2&gt;
&lt;p&gt;多数应用都有如下所述的 UI 设计：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;多数视图在一个通用的布局内呈现，比如典型的 Header + Main 的布局&lt;/li&gt;
&lt;li&gt;个别视图需要一个特定的布局，比如登录和注册页面不需要 Header 等等&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;对于这些场景来说，那些重复的 HTML 结构（如 Header 和 Footer）肯定需要某种方式的抽象使得它们可以复用或者指定渲染还是不渲染。后端渲染技术使用了一些机制（如 helpers 等）来帮助开发者在视图层实现这些逻辑，等到返回给浏览器的时候已经是完整的 HTML 了（当然也有 Turbolinks 这样融合了部分前端路由特性的新技术，本文不做进一步描述），这显然是不适合前端应用的场景的，因为对于 SPA 应用来说用户更换 URLs 时需要在浏览器端即时拼装最终的完整视图，并不存在“预先渲染好的页面一起交付过来”这么一说。我们需要先思考一下高层设计，看看有什么机制可以利用的。&lt;/p&gt;
&lt;h3 id="初步分析"&gt;初步分析&lt;/h3&gt;
&lt;p&gt;路由是怎么管理状态的？复杂的话题简单说：&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;In Ember.js, each of the possible states in your application is represented by a URL.
在 Ember.js 中，应用的每一个可能的状态都是通过 URL 体现的。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;这是&lt;a href="http://guides.emberjs.com/v2.0.0/routing/" rel="nofollow" target="_blank" title=""&gt;官方文档里所总结的&lt;/a&gt;，我来试着举例表述一下：&lt;/p&gt;

&lt;p&gt;假设当前有如下路由定义：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;Router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Ember&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="nx"&gt;Router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dashboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/dashboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;signin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/signin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;于是，当用户——&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;进入 &lt;code&gt;/dashboard&lt;/code&gt; URL 的时候，对应的 &lt;code&gt;dashboard&lt;/code&gt; 路由开始接管应用的当前状态&lt;/li&gt;
&lt;li&gt;进入 &lt;code&gt;/signin&lt;/code&gt; URL 的时候，对应的 &lt;code&gt;signin&lt;/code&gt; 路由开始接管应用的当前状态&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;但更重要的是：所有的路由都有一个共有的顶级路由——&lt;code&gt;application&lt;/code&gt; 路由&lt;/strong&gt;，其重要性主要体现在:

&lt;ol&gt;
&lt;li&gt;它是唯一一个靠谱的可以用来管理全局范围状态的路由&lt;/li&gt;
&lt;li&gt;它为所有子路由的视图渲染提供了模板的入口（outlet）&lt;/li&gt;
&lt;/ol&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;接着问题来了：如果说状态通过 URL 来体现，那么 UI 布局的不同如何体现呢？比如：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;进入 &lt;code&gt;/dashboard&lt;/code&gt; URL 的时候，我们需要 Header + Main 的布局 &lt;/li&gt;
&lt;li&gt;进入 &lt;code&gt;/signin&lt;/code&gt; URL 的时候，我们不需要 Header&lt;/li&gt;
&lt;li&gt;无论何种情形，&lt;code&gt;application&lt;/code&gt; 路由在其中的作用……？&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="第一次尝试"&gt;第一次尝试&lt;/h2&gt;
&lt;p&gt;因为每一个路由都会渲染自己的模版，我们可以做一个最简单的尝试：&lt;/p&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="c"&gt;{{!app/pods/application/template.hbs}}&lt;/span&gt;
&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;outlet&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="c"&gt;{{!app/pods/dashboard/template.hbs}}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;header&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/header&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;main&amp;gt;&lt;/span&gt;
    ...
    &lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;outlet&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/main&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="c"&gt;{{!app/pods/signin/template.hbs}}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;main&amp;gt;&lt;/span&gt;
    ...
    &lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;outlet&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/main&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;虽然这么做可以奏效，然而问题也是显而易见的：如果出现多个和 &lt;code&gt;dashboard&lt;/code&gt; 一样的布局结构，我们将不得不多次重复 &lt;code&gt;&amp;lt;header&amp;gt;&amp;lt;/header&amp;gt;&lt;/code&gt;；曾经 Ember 有 &lt;code&gt;{{partial}}&lt;/code&gt; 这样的 helper 来做模版片段复用，但是第一，以后没有 &lt;code&gt;{{partial}}&lt;/code&gt; 了，二来用 &lt;code&gt;{{partial}}&lt;/code&gt; 做布局是错误的选择。&lt;/p&gt;
&lt;h3 id="问题分析"&gt;问题分析&lt;/h3&gt;
&lt;p&gt;如果我们可以把问题场景简化为只有一种可能，例如“所有的视图都用 Header + Main 的布局”，那么解决方案可以简化为：&lt;/p&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="c"&gt;{{!app/pods/application/template.hbs}}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;header&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/header&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;main&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;outlet&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/main&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;footer&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/footer&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="c"&gt;{{!app/pods/dashboard/template.hbs}}&lt;/span&gt;
...
&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;outlet&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="c"&gt;{{!app/pods/signin/template.hbs}}&lt;/span&gt;
...
&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;outlet&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;那么再次恢复原来的场景要求，问题变成了：“进入 &lt;code&gt;/signin&lt;/code&gt; 之后，如何隐藏 &lt;code&gt;application&lt;/code&gt; 模版里的 &lt;code&gt;&amp;lt;header&amp;gt;&amp;lt;/header&amp;gt;&lt;/code&gt;？&lt;/p&gt;
&lt;h2 id="第二次尝试"&gt;第二次尝试&lt;/h2&gt;
&lt;p&gt;隐藏模版里的片段，最简单的方法可以这么做：&lt;/p&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="c"&gt;{{!app/pods/application/template.hbs}}&lt;/span&gt;
&lt;span class="k"&gt;{{#if&lt;/span&gt; &lt;span class="nv"&gt;showNavbar&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;header&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/header&amp;gt;&lt;/span&gt;
&lt;span class="k"&gt;{{/if}}&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;main&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;outlet&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/main&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们知道模版内可访问的变量可以通过控制器来设置，但此时我不打算创建 &lt;code&gt;ApplicationController&lt;/code&gt;，因为路由里有一个 &lt;code&gt;setupController&lt;/code&gt; 的钩子方法能帮我们设置控制器的（更重要的原因是很快 Routable Components 将取代现在的 route + controller + template 的分层体系，所以从现在开始最好尽可能少的依赖 controller），试试看：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/pods/application/route.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;Ember&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nf"&gt;setupController&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_super&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nx"&gt;controller&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;showNavbar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;现在所有的状态都会显示 &lt;em&gt;header&lt;/em&gt; 部分了，那怎么让 &lt;code&gt;/signin&lt;/code&gt; 不显示呢？或许这样……？&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/pods/signin/route.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;Ember&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nf"&gt;setupController&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_super&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;controllerFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;showNavbar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;以下是测试结果（这里建议先写 Acceptance Test，省时间且不易错漏），在每次刷新页面后：&lt;/p&gt;
&lt;table class="table table-bordered table-striped"&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;th&gt;从...&lt;/th&gt;
&lt;th&gt;到...&lt;/th&gt;
&lt;th&gt;结果&lt;/th&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/dashboard&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;成功&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/dashboard&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;成功&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/signin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;成功&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/signin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;失败&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/dashboard&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/signin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;成功&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/signin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/dashboard&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;失败&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/signin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/dashboard&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;失败&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/dashboard&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;/signin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;失败&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;我们在测试中增加了 &lt;code&gt;/dashboard&lt;/code&gt; 的访问，但是我们并没有定义位于 &lt;code&gt;DashboardRoute&lt;/code&gt; 里的 &lt;code&gt;setupController&lt;/code&gt; 钩子，这是因为我们期望 &lt;code&gt;/dashboard&lt;/code&gt; 能够继承 &lt;code&gt;/&lt;/code&gt; 的状态，否则所有的路由都要设置类似的 &lt;code&gt;setupController&lt;/code&gt; 会把人累死，然而测试结果可能会让初学者觉得摸不着头脑，我们试着分析一下好了：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;/&lt;/code&gt; 和 &lt;code&gt;/dashboard&lt;/code&gt; 都需要 &lt;code&gt;showNavbar === true&lt;/code&gt;，所以正反都可以；&lt;/li&gt;
&lt;li&gt;当自 &lt;code&gt;/signin&lt;/code&gt; 刷新页面的时候，先执行了 &lt;code&gt;ApplicationRoute&lt;/code&gt; 然后才是 &lt;code&gt;SigninRoute&lt;/code&gt;，等到进入 &lt;code&gt;/&lt;/code&gt; 的时候，&lt;code&gt;setupController&lt;/code&gt; 不会再次执行的；&lt;/li&gt;
&lt;li&gt;同上;&lt;/li&gt;
&lt;li&gt;同上。&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id="问题分析"&gt;问题分析&lt;/h3&gt;
&lt;p&gt;这里最明显的问题就是 &lt;code&gt;ApplicationRoute#setupController&lt;/code&gt; 这个钩子方法是不可靠的，你只能保证它的第一次运行，一旦变成了在路由之间来回跳转就无效了。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;实际上，&lt;code&gt;setupController&lt;/code&gt; 的作用是将 &lt;code&gt;model&lt;/code&gt; 钩子返回的结果绑定在对应的控制器上的，你可以扩展这个逻辑但也仅限于数据层面的设置。只有当调用了 &lt;code&gt;route#render()&lt;/code&gt; 且返回了与之前不同的 model 时 &lt;code&gt;setupController&lt;/code&gt; 才会再次被调用。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;于是问题又变成了：有哪一个钩子方法能保证在路由发生变化的时候都可用？&lt;/p&gt;
&lt;h4 id="路由的生命周期"&gt;路由的生命周期&lt;/h4&gt;
&lt;p&gt;这是一个非常重要但又很无趣的主题，我不想在这里重复那些可以通过阅读文档和亲测就可以得出的答案，不过我可以给出一份测试路由生命周期的完整代码片段：&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gist.github.com/nightire/f766850fd225a9ec4aa2" rel="nofollow" target="_blank"&gt;https://gist.github.com/nightire/f766850fd225a9ec4aa2&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;把它们放进你的路由当中然后仔细观察吧。顺便给你一些经验之谈：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;这个测试不要错过 &lt;code&gt;ApplicationRoute&lt;/code&gt;，因为它是最特殊的一个&lt;/li&gt;
&lt;li&gt;其他的路由至少要同时测试两个，比如 &lt;code&gt;IndexRoute&lt;/code&gt; 和 &lt;code&gt;TestRoute&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;不要只测试页面刷新后的生命周期，还要尝试各种路由之间的相互过渡&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;测试完之后，你就会对整个路由系统有一个非常全面的了解了，这些体验会带给你一个重要的技能，即是在将来你可以很容易的决断出实现一个功能应该从哪里入手。对于我们这个例子来说，比较重要的结论如下：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;ApplicationRoute&lt;/code&gt; 是所有路由的共同先祖，当你第一次进入应用程序——无论是从 &lt;code&gt;/&lt;/code&gt; 进入还是从 &lt;code&gt;/some/complicated/state&lt;/code&gt; 进入——&lt;code&gt;ApplicationRoute&lt;/code&gt; 都是第一个实例化的路由，并且它 &lt;code&gt;activated&lt;/code&gt; 就不会 &lt;code&gt;deactivated&lt;/code&gt; 了（除非你手动刷新浏览器）。因此我们可以把 &lt;code&gt;ApplicationRoute&lt;/code&gt; 作为一个特殊的永远激活的路由&lt;/li&gt;
&lt;li&gt;如果你有应用逻辑依存于 &lt;code&gt;ApplicationRoute#setupController&lt;/code&gt;，那么第一次进入就是唯一靠谱的机会——你不能指望这个钩子会在路由来回切换的时候触发&lt;/li&gt;
&lt;li&gt;但是其他路由上的 &lt;code&gt;#setupController&lt;/code&gt; 钩子是会在每次过渡进来的时候重新执行的&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="第三次尝试"&gt;第三次尝试&lt;/h2&gt;
&lt;p&gt;基于以上分析，我们可以调整我们的代码了：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/pods/application/route.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;Ember&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/pods/index/route.js and app/pods/dashboard/route.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;Ember&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nf"&gt;setupController&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_super&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;controllerFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;showNavbar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/pods/signin/route.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;Ember&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nf"&gt;setupController&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_super&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;controllerFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;showNavbar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们把 &lt;code&gt;ApplicationRoute#setupController&lt;/code&gt; 里的逻辑转移到了 &lt;code&gt;IndexRoute#setupController&lt;/code&gt; 里去，就是因为当你访问 &lt;code&gt;/&lt;/code&gt; 的时候，&lt;code&gt;ApplicationRoute#setupController&lt;/code&gt; 只会触发一次（第一次刷新的时候），而 &lt;code&gt;IndexRoute#setupController&lt;/code&gt; 则可以保证每次都触发。现在，我们设想的场景可以实现了。&lt;/p&gt;

&lt;p&gt;这个设定一开始看起来非常古怪，很多初学者都在这里被搞晕掉：“为什么要有 &lt;code&gt;IndexRoute&lt;/code&gt;？为什么不直接用 &lt;code&gt;ApplicationRoute&lt;/code&gt;？”&lt;/p&gt;
&lt;h3 id="抽象路由"&gt;抽象路由&lt;/h3&gt;
&lt;p&gt;当我们刚开始接触前端的路由机制时，我们很容易把 &lt;code&gt;ApplicationRoute&lt;/code&gt; 和 &lt;code&gt;/&lt;/code&gt; 关联起来，可实际上真正和 &lt;code&gt;/&lt;/code&gt; 关联的是 &lt;code&gt;IndexRoute&lt;/code&gt;。如果你没有自行创建 &lt;code&gt;IndexRoute&lt;/code&gt;，Ember 会帮你创建一个，但不管怎样 &lt;code&gt;IndexRoute&lt;/code&gt; 都是必不可少的。&lt;/p&gt;

&lt;p&gt;那么 &lt;code&gt;ApplicationRoute&lt;/code&gt; 到底扮演着一个什么样的角色呢？&lt;/p&gt;

&lt;p&gt;先记住这个结论：&lt;strong&gt;在路由系统中，路由树中任何一个当前激活的路径都会至少包括两个路由节点，并且其中一个必然是 &lt;code&gt;ApplicationRoute&lt;/code&gt;，&lt;/strong&gt;这也正是 &lt;code&gt;ApplicationRoute&lt;/code&gt; 永远处于 &lt;code&gt;activated&lt;/code&gt; 而永远不会 &lt;code&gt;deactivate&lt;/code&gt; 的原因所在。&lt;/p&gt;

&lt;p&gt;举几个例子：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;当访问 '/' 时，路由树中当前激活的路径为：&lt;code&gt;application =&amp;gt; index&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;当访问 '/users/new' 时，路由树中当前激活的路径为：&lt;code&gt;application =&amp;gt; users =&amp;gt; new&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;当访问 '/posts/1/comments/1' 时，路由树中当前激活的路径为：&lt;code&gt;application =&amp;gt; post =&amp;gt; index =&amp;gt; comment =&amp;gt; index&lt;/code&gt;，也可能是：&lt;code&gt;application =&amp;gt; posts =&amp;gt; show =&amp;gt; comments =&amp;gt; show&lt;/code&gt; ——取决于你的路由规则的写法&lt;/li&gt;
&lt;li&gt;等等……&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Ember 并没有为这个特殊的 &lt;code&gt;ApplicationRoute&lt;/code&gt; 做一个明确的定义（但是&lt;a href="http://guides.emberjs.com/v2.0.0/routing/defining-your-routes/#toc_the-application-route" rel="nofollow" target="_blank" title=""&gt;简要描述了其特点&lt;/a&gt;），不过在其他类似的路由系统里我们可以找到等价物——比如来自 &lt;a href="http://angular-ui.github.io/ui-router/site/#/api/ui.router" rel="nofollow" target="_blank" title=""&gt;ui.router&lt;/a&gt;（Angular 生态圈里最优秀的路由系统）里的&lt;a href="https://github.com/angular-ui/ui-router/wiki/Nested-States-and-Nested-Views#abstract-states" rel="nofollow" target="_blank" title=""&gt;抽象路由（Abstract Route）&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;Ember 的 &lt;code&gt;ApplicationRoute&lt;/code&gt; 和 ui.router 的抽象路由非常相似，它们的共性包括：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;都能够拥有子路由&lt;/li&gt;
&lt;li&gt;自身都不能被直接激活（不能位于路由树中当前激活路径的顶点）&lt;/li&gt;
&lt;li&gt;不能直接过渡，也就是 transition to；Ember 里会等价于过渡到 &lt;code&gt;IndexRoute&lt;/code&gt;，ui.router 则会抛出异常&lt;/li&gt;
&lt;li&gt;都有对应的模版、控制器、数据入口、生命周期钩子等等&lt;/li&gt;
&lt;li&gt;当其下的任意子路由被激活，作为父节点的抽象路由都会被激活&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;当然，它们也有不同，比如说：你可以在 ui.router 的路由树中任意定义抽象路由，不受数量和节点深度的限制，只要保证抽象路由不会位于某条路径的顶点就是了；而 Ember Router 只有一个抽象路由（而且并没有明确的定义语法，只是行为类似——典型的鸭子类型设计嘛）且只能是 &lt;code&gt;ApplicationRoute&lt;/code&gt;，你可以手动创建别的路由来模拟，但是 Ember Router 不会阻止你过渡到这些路由，不像 ui.router 会抛出异常（这一点很容易让初学者碰壁）&lt;/p&gt;

&lt;p&gt;实际上当你对 Ember Router 的理解日渐深入之后你会发现&lt;a href="http://guides.emberjs.com/v2.0.0/routing/defining-your-routes/#toc_the-index-route" rel="nofollow" target="_blank" title=""&gt;所有的嵌套路由（包括顶层路由）都是抽象路由&lt;/a&gt;，因为它们都会隐式的创建对应的 &lt;code&gt;IndexRoute&lt;/code&gt; 作为该路径的顶节点，访问它们就等于访问它们的 &lt;code&gt;IndexRoute&lt;/code&gt;。我认为 Ember Router 的这个设计与 ui.router 相比有利有弊：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;利：设计精巧简单，可以避免大量的 boilerplate 代码，路由的定义相对清晰简洁&lt;/li&gt;
&lt;li&gt;弊：对于初学者来说，由于不存在抽象路由的概念，很难深刻理解父子节点，特别是隐式 &lt;code&gt;IndexRoute&lt;/code&gt; 的存在价值&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="这个方案足够完美了吗？"&gt;这个方案足够完美了吗？&lt;/h3&gt;
&lt;p&gt;不，还差一些。试想：当我们需要很多路由来组织应用程序的结构时，类似的 &lt;code&gt;#setupController&lt;/code&gt; 岂不是要重复定义很多次？如何抽象这一逻辑让其变得易于复用和维护？&lt;/p&gt;
&lt;h4 id="Thinking in Angular way(w/ ui.router)"&gt;Thinking in Angular way(w/ ui.router)&lt;/h4&gt;
&lt;p&gt;在开发 Angular 应用的时候，类似场景的路由定义一般是这样的：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                   +----&amp;gt; layoutOne(with header) +----&amp;gt; childrenRoutes(like dashboard, etc.)       
                   |
                   |
application(root) -|
                   |
                   |
                   +----&amp;gt; layoutTwo(without header) +----&amp;gt; childrenRoutes(like signin, etc.)

&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我们用 Ember Router 也可以模拟这样的路由定义，实现同样的结果，代码类似：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/router.js&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;Router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Ember&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;locationType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="nx"&gt;Router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// provide layout w/ &amp;lt;header&amp;gt;&amp;lt;/header&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;layoutOne&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dashboard&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;resetNamespace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

    &lt;span class="c1"&gt;// provide layout w/o &amp;lt;header&amp;gt;&amp;lt;/header&amp;gt;&lt;/span&gt;
    &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;layoutTwo&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;signin&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;resetNamespace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是个人非常不喜欢也不推崇这么做，原因是：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;这样的路由定义写多了会很恶心&lt;/li&gt;
&lt;li&gt;为了避免类似 &lt;code&gt;/layoutOne/dashboard&lt;/code&gt; 这样的 URLs，不得不重复设定 &lt;code&gt;path: '/'&lt;/code&gt; 来覆盖

&lt;ul&gt;
&lt;li&gt;ui.router 解决此问题依靠的是 url pattern inheritence，由于每一个路由的定义都必须指明 url 属性，所以也就习惯了&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;为了避免类似 &lt;code&gt;layoutTwo.signin&lt;/code&gt; 这样的路由名字，不得不重复设定 &lt;code&gt;resetNamespace: true&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;ui.router 解决此问题依靠的是路由定义里的 parent 属性，所以子路由是可以分开定义的，不用嵌套也就无需 resetNamespace&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;对比两家的路由定义语法，各有优缺点吧，但是 Ember Router 向来都是以简明扼要著称的，真心不喜欢为了这个小小需求而把路由定义写得一塌糊涂&lt;/p&gt;

&lt;p&gt;另外这样的路由设计还会导致 &lt;code&gt;application&lt;/code&gt; 这个模版变成一个废物，除了 &lt;code&gt;{{outlet}}&lt;/code&gt; 它啥也做不成，生成的 DOM Tree 里平白多一个标签看的人直恶心～&lt;/p&gt;
&lt;h4 id="Thinking in Ember way"&gt;Thinking in Ember way&lt;/h4&gt;
&lt;p&gt;既然问题的本质是 &lt;code&gt;#setupController&lt;/code&gt; 钩子需要重复定义，那么有没有 Ember 风格办法来解决这一问题呢？&lt;/p&gt;

&lt;p&gt;首先我们来考量一下 Mixin，你可以这么做：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/mixins/show-navbar.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;Ember&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Mixin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nf"&gt;setupController&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_super&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;controllerFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;showNavbar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// app/mixins/hide-navbar.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;Ember&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Mixin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="nf"&gt;setupController&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_super&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;controllerFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;showNavbar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/pods/index/route.js and app/pods/dashboard/route.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;ShowNavbarMixin&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../../mixins/show-navbar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;Ember&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ShowNavbarMixin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// app/pods/signin/route.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;HideNavbarMixin&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../../mixins/hide-navbar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;Ember&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;HideNavbarMixin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这么做倒也不是不行，但是——明显很蠢嘛——这和抽取两个方法然后到处调用没有什么本质的区别，看起来我们需要的是某种程度上的继承与重写才对：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// somewhere in app/app.js&lt;/span&gt;
&lt;span class="nx"&gt;Ember&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reopen&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="c1"&gt;// show navbar by default, can be overwriten when define a specific route&lt;/span&gt;
    &lt;span class="na"&gt;withLayout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="nf"&gt;setupController&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_super&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;controllerFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;showNavbar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;withLayout&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/pods/index/route.js and app/pods/dashboard/route.js&lt;/span&gt;
&lt;span class="c1"&gt;// Do nothing if showNavbar: true is expected&lt;/span&gt;

&lt;span class="c1"&gt;// app/pods/signin/route.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;Ember&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;withLayout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样就行了，不需要额外的路由体系设计，就用 Ember 的对象系统便足够完美。本文所描述的这个例子其实非常简单，我相信略有 Ember 经验的开发者都能做出来，但是我的重点不在于这个例子，而在于对路由系统的一些阐述和理解。这个例子来源自真实的工作，为了给同事解释清楚最初的方案为什么不行着实费了我好大功夫，于是我把整个梳理过程记录下来，希望对初学者——特别是对 SPA 的核心尚未了解的初学者能有所助益吧。&lt;/p&gt;
&lt;h3 id="基于事件的解决方案"&gt;基于事件的解决方案&lt;/h3&gt;
&lt;p&gt;这个问题其实还有多种解法，基于事件响应的解法我就在现实里演示了两种，不过相比于上面的最终方案，它们还是略微糙了些。在这里我写其中一种比较少见的，里面涉及到一些 Ember 的内部机制，权当是一个借鉴吧，思路我就不多解释了。&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/mixins/hide-navbar.js&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;Ember&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Mixin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;hideNavbar&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;showNavbar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;init&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/router.js&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;Router&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Ember&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Router&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;locationType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;

    &lt;span class="nf"&gt;didTransition&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_super&lt;/span&gt;&lt;span class="p"&gt;(...&lt;/span&gt;&lt;span class="nx"&gt;arguments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;currentRoute&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;container&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lookup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`route:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;currentRouteName&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;container&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;lookup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;controller:application&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;showNavbar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;_&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isUndefined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;currentRoute&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;showNavbar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/pods/signin/route.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;HideNavbarMixin&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../../mixins/hide-navbar&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;Ember&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;HideNavbarMixin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// only use this mixin when you need to hide the Header&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;</description>
      <author>nightire</author>
      <pubDate>Wed, 09 Sep 2015 18:11:10 +0800</pubDate>
      <link>https://ruby-china.org/topics/27258</link>
      <guid>https://ruby-china.org/topics/27258</guid>
    </item>
    <item>
      <title>[Tips on Ember 2] Ember CLI 和 Sass (及其周边) 的协同工作</title>
      <description>&lt;p&gt;今天这篇主要讲讲 Ember CLI 里关于样式开发的一些前期准备工作，主要是 Sass 和 Bootstrap。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;a href="http://www.emberaddons.com/" rel="nofollow" target="_blank" title=""&gt;Ember Addons&lt;/a&gt; 是寻找各种组件的绝佳场所，下文将要介绍的一些都可以在这里找到，没事的时候多探索一下会有很多惊喜。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="关于 Sass"&gt;关于 Sass&lt;/h2&gt;
&lt;p&gt;Sass 的演变和使用在前端开发领域真是个又臭又长的话题，如果你是自行搭建构建系统你就明白我说的意思了。还好 Ember CLI 的生态系统比较完备，也有一个广大的社区做后盾可以为我们省去很多功夫。&lt;/p&gt;

&lt;p&gt;对于 Sass 的基础使用，我们只需要安装 &lt;a href="https://www.npmjs.com/package/ember-cli-sass" rel="nofollow" target="_blank" title=""&gt;ember-cli-sass&lt;/a&gt; 就好了，它默认使用 &lt;a href="https://www.npmjs.com/package/node-sass" rel="nofollow" target="_blank" title=""&gt;node-sass&lt;/a&gt;，支持 SourceMaps 和 IncludePaths 等功能选项，比较省心。较新的 Ember CLI 应该是直接内置了 ember-cli-sass 的，推荐升级哦。&lt;/p&gt;

&lt;p&gt;对于不太熟悉 Sass 的程序员，IncludePaths 值得一讲，我见到有些人啊为了方便的 import，把许多第三方的 sass 文件拷过来拷过去的，其实大可不必哦～就拿 &lt;a href="https://github.com/twbs/bootstrap-sass" rel="nofollow" target="_blank" title=""&gt;bootstrap-sass&lt;/a&gt; 为例好了：&lt;/p&gt;

&lt;p&gt;安装 bootstrap-sass：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ npm install bootstrap-sass --save
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;完后呢，入口文件的路径在 &lt;code&gt;node_modules/bootstrap-sass/assets/stylesheets&lt;/code&gt; 这里，因为通常 &lt;code&gt;node_modules/&lt;/code&gt; 和 &lt;code&gt;bower_components/&lt;/code&gt; 这些目录是不会被包含在项目里的（包含在 Git 或 HTTP Server Root 下），所以才会有手工拷贝到别处的做法。在 Ember CLI 里，你可以这样设置一下：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ember-cli-build.js or Brocfile.js&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;EmberApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;defaults&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;sassOptions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;includePaths&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node_modules/bootstrap-sass/assets/stylesheets&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后在项目的 sass 文件内直接 &lt;code&gt;@import "bootstrap";&lt;/code&gt; 就好了，那是一个数组所以你懂的，你可以设置很多路径，sass 在编译的时候会挨个儿去找。&lt;/p&gt;
&lt;h3 id="关于 POD"&gt;关于 POD&lt;/h3&gt;
&lt;p&gt;如果你跟我一样喜欢 POD 文件结构，那么还有一个 &lt;a href="https://www.npmjs.com/package/ember-cli-sass-pods" rel="nofollow" target="_blank" title=""&gt;ember-cli-sass-pods&lt;/a&gt; 也可以用用，这个东西能让你这样生成 sass 文件：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;ember generate style &lt;span class="o"&gt;[&lt;/span&gt;name] &lt;span class="nt"&gt;-p&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;生成的文件会保存在同名的 POD 目录下，不过我一向都是手动创建文件的，所以并没有实际测试它。对于样式文件在 POD 架构下的导入我是这样做的：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;创建 &lt;code&gt;app/styles/_pods.scss&lt;/code&gt; 文件&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;app/styles/app.scss&lt;/code&gt; 文件里添加一句 &lt;code&gt;@import "pods";&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;includePaths&lt;/code&gt; 那里添加 &lt;code&gt;app/pods&lt;/code&gt; 这一项&lt;/li&gt;
&lt;li&gt;新增加的 PODs 样式在 &lt;code&gt;app/styles/_pods.scss&lt;/code&gt; 内这样引用：`&lt;a href="/import" class="user-mention" title="@import"&gt;&lt;i&gt;@&lt;/i&gt;import&lt;/a&gt; "name/style;"&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;后来我注意到 ember-cli-sass-pods 也是这么做的，英雄所见略同嘛～&lt;/p&gt;
&lt;h2 id="关于 Bootstrap w/ sass"&gt;关于 Bootstrap w/ sass&lt;/h2&gt;
&lt;p&gt;前面提到了用 &lt;code&gt;includePaths&lt;/code&gt; 来引用 Bootstrap 的方法，不过在 Ember CLI 项目里，我还是推荐你用 &lt;a href="https://www.npmjs.com/package/ember-cli-bootstrap-sassy" rel="nofollow" target="_blank" title=""&gt;ember-cli-bootstrap-sassy&lt;/a&gt; 来辅助你做这件事，因为这个 addon 额外做了几件好事：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;a href="https://github.com/lifegadget/ember-cli-bootstrap-sassy/blob/master/blueprints%2Fember-cli-bootstrap-sassy%2Findex.js?ts=240px#L11" rel="nofollow" target="_blank" title=""&gt;添加了 bower 版的 bootstrap-sass&lt;/a&gt;，省去了你人工寻找和安装的过程&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/lifegadget/ember-cli-bootstrap-sassy/blob/master/index.js?ts=240px#L54" rel="nofollow" target="_blank" title=""&gt;完成了 &lt;code&gt;includePaths&lt;/code&gt; 的设置&lt;/a&gt;，免得你忘记了&lt;/li&gt;
&lt;li&gt;完成了 &lt;a href="https://github.com/lifegadget/ember-cli-bootstrap-sassy/blob/master/index.js?ts=240px#L38" rel="nofollow" target="_blank" title=""&gt;字体文件的导入&lt;/a&gt; 和 &lt;a href="https://github.com/lifegadget/ember-cli-bootstrap-sassy/blob/master/index.js?ts=240px#L25" rel="nofollow" target="_blank" title=""&gt;脚本文件的导入&lt;/a&gt;，好省心呐&lt;/li&gt;
&lt;li&gt;Bootstrap 自带的字体图标可以选择不导入，JavaScript 的模块可以选择性的导入或者完全不要，具体设置如下所示：&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;EmberApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;defaults&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ember-cli-bootstrap-sassy&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;glyphicons&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;js&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;transition&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;collapse&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="使用／定制 Bootstrap 的正确姿势"&gt;使用／定制 Bootstrap 的正确姿势&lt;/h3&gt;
&lt;p&gt;关于这个话题我简直可以写本小说出来了，在我带实习生的过程里被问到和发现最多问题的就是 Bootstrap 的用法，限于篇幅我在这里只将一些前期的要点：&lt;/p&gt;
&lt;h4 id="别直接用 _bootstrap.scss"&gt;别直接用 &lt;code&gt;_bootstrap.scss&lt;/code&gt;
&lt;/h4&gt;
&lt;p&gt;大多数人是这样用的：直接在主样式文件里 &lt;code&gt;@import "bootstrap";&lt;/code&gt;，然后遇到需要定制的就开始覆盖覆盖覆盖……别这么搞！&lt;/p&gt;

&lt;p&gt;看一下 &lt;a href="https://github.com/twbs/bootstrap-sass/tree/master/assets/stylesheets/bootstrap" rel="nofollow" target="_blank" title=""&gt;Bootstrap Sass 的目录结构&lt;/a&gt; 以及 &lt;a href="https://github.com/twbs/bootstrap-sass/blob/master/assets%2Fstylesheets%2F_bootstrap.scss?ts=240px" rel="nofollow" target="_blank" title=""&gt;源码&lt;/a&gt; 便知道人家本来就是模块化开发的，既然用了 sass 咱就应该把它当成级别高点的编程语言来对待。我是这么做的：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;创建 &lt;code&gt;app/styles/_custom-bootstrap.scss&lt;/code&gt; 文件&lt;/li&gt;
&lt;li&gt;在 &lt;code&gt;app/styles/app.scss&lt;/code&gt; 里 &lt;code&gt;@import "custom-bootstrap";&lt;/code&gt;，一般来说这个应该是第一个导入的模块&lt;/li&gt;
&lt;/ol&gt;
&lt;h5 id="_custom-bootstrap.scss 怎么用？"&gt;
&lt;code&gt;_custom-bootstrap.scss&lt;/code&gt; 怎么用？&lt;/h5&gt;
&lt;p&gt;一开始你可以把原来的 &lt;code&gt;_bootstrap.scss&lt;/code&gt; 内容原封不动拷贝进来，由于 &lt;code&gt;includePaths&lt;/code&gt; 的作用，所有导入的路径都可以不变。&lt;/p&gt;

&lt;p&gt;然后把所有的模块导入都注释掉，此时你的项目里等于完全没有 Bootstrap。&lt;/p&gt;

&lt;p&gt;之后一般会分两种情况来定制：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;需要用到且可以直接沿用的模块&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这个简单，把注释的部分去掉就好了嘛。曾经有徒弟质疑我：“师傅，人家官网上有自定义模块构建的功能，咱为啥不用那个？”&lt;/p&gt;

&lt;p&gt;图样图森破，那个功能就是拿来秀的，一点实用性都没有。有多少人自定义构建之后从头用到尾刚刚好既不多又不少的？神都预测不到你会用到哪些组件的，难道你一遍又一遍的去构建定制版本啊？那是菜鸟的用法。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;需要用到但得修改／定制的模块&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这里又可以大致分出两种情形，比较简单的改动——比如变量，你可以把其定义写在 &lt;code&gt;@import "bootstrap/variables";&lt;/code&gt; 的前面（特别是覆盖默认变量的，一定注意顺序）；我会做的比较彻底，直接创建一个 &lt;code&gt;app/styles/_custom-variables.scss&lt;/code&gt; 并且作为第一个模块导入进去。另外，自定义的变量不需要写尾部的 &lt;code&gt;!default&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;第二种情形就比较进阶一些了，我举个例子，以前经常看见这样的写法：&lt;/p&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn btn-default btn-block btn-purple"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;我说你写这么多 class 不累啊？人家 Bootstrap 是为了可重用性才定义了这种粒度很细的 helper classes，如果你是做一个 rapid prototype 那我没意见，但是正式的产品这样写问题就大了：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;像 &lt;code&gt;btn-purple&lt;/code&gt; 这样的命名是很糟糕的，完全没有语义性，万一将来要换个色彩主题怎么办？可维护性也很差，万一将来维护的是个色盲怎么办？（开个玩笑）&lt;/li&gt;
&lt;li&gt;重复的写一串 class 可读性也很差，如果将来要进行微调，不熟悉这些 class 的人会被折腾死&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;该怎么写？&lt;/p&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"button-main button-block"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight scss"&gt;&lt;code&gt;&lt;span class="c1"&gt;/// app/styles/_custom-buttons.scss&lt;/span&gt;

&lt;span class="c1"&gt;// Overwrite for more semantic button class names&lt;/span&gt;
&lt;span class="nc"&gt;.button&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;@extend&lt;/span&gt; &lt;span class="nc"&gt;.btn&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="c1"&gt;// Bootstrap doesn't give buttons transition effects by default,&lt;/span&gt;
    &lt;span class="c1"&gt;// so we simply extend it here. You can use some mixin instead.&lt;/span&gt;
    &lt;span class="nl"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;all&lt;/span&gt; &lt;span class="mi"&gt;.2s&lt;/span&gt; &lt;span class="n"&gt;ease-out&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;@each&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nt"&gt;name&lt;/span&gt; &lt;span class="nt"&gt;in&lt;/span&gt; &lt;span class="nt"&gt;default&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;primary&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;success&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;warning&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;danger&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;info&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="nt"&gt;block&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nc"&gt;.button-&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;@extend&lt;/span&gt; &lt;span class="nc"&gt;.btn-&lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="nv"&gt;$name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Define site-wide main button colors&lt;/span&gt;
&lt;span class="nv"&gt;$button-main-color&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="mh"&gt;#fff&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$button-main-bg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="nv"&gt;$violet&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nv"&gt;$button-main-border&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;darken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$violet&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="m"&gt;5%&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nc"&gt;.button-main&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;@extend&lt;/span&gt; &lt;span class="nc"&gt;.button&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;@include&lt;/span&gt; &lt;span class="nt"&gt;button-variant&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;
        &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nt"&gt;button-main-color&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nt"&gt;button-main-bg&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt; &lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="nt"&gt;button-main-border&lt;/span&gt;
    &lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这是个例子，我从最近的一个项目里扒出来的，仅就这一例子而言或许有点小题大做，但如果考虑一个大型的项目，这样的改造绝对是有必要的。好的习惯要从小养成，正确的姿势得贯彻始终。&lt;/p&gt;

&lt;p&gt;类似的技巧还有好多，鉴于这里的主题是 Ember CLI 呢便就此打住了，我也是想：既然选择了 Ember 这么靠谱的前端框架，相应的前端技术也应该靠谱起来吧，所以抛砖引玉一下。&lt;/p&gt;
&lt;h3 id="还有什么值得一用？"&gt;还有什么值得一用？&lt;/h3&gt;
&lt;p&gt;Bootstrap 绝对不完美，只会用它的前端工程师绝对不是合格的前端工程师，针对 Bootstrap 不完善的地方 sass 社区还有非常多的组件值得一用。在这里我先推荐几个，以后还可以再补充。&lt;/p&gt;
&lt;h4 id="Susy"&gt;Susy&lt;/h4&gt;
&lt;p&gt;Bootstrap 的 Grid 系统很一般（虽说对它的定位而言也够用），定死的 12 栅格并非时时够用；嵌套时的 gutter 无法灵活调整；需要手动覆盖 row 两端 cols 的 padding（当你需要边缘与 container 对齐的时候，如 gallery 布局）……等等槽点都被喷了好几年了。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Bootstrap v4 将使用 flex 做 Grid 系统，这是好事&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;所以我推荐你试一下 &lt;a href="http://susy.oddbird.net/" rel="nofollow" target="_blank" title=""&gt;Susy&lt;/a&gt;，做布局——专业的！用在 Ember CLI 里也很简单，&lt;code&gt;npm install susy --save&lt;/code&gt;，然后设定一下 &lt;code&gt;includePaths&lt;/code&gt; 就好，非常轻量，非常好用&lt;/p&gt;
&lt;h4 id="Bourbon"&gt;Bourbon&lt;/h4&gt;
&lt;p&gt;Bootstrap 自己定义了一些 &lt;a href="https://github.com/twbs/bootstrap-sass/tree/master/assets/stylesheets/bootstrap/mixins" rel="nofollow" target="_blank" title=""&gt;mixins&lt;/a&gt; 善用它们会令你事半功倍，然而习惯了 compass 的开发者大概还是会觉得不够用吧？因此我向你推荐 &lt;a href="http://bourbon.io/" rel="nofollow" target="_blank" title=""&gt;Bourbon&lt;/a&gt;，ThoughtBot 出品，Ruby 社区应该不陌生的，品质一流。&lt;/p&gt;

&lt;p&gt;总的来说 Compass 就不要再用了，又大又笨而且连亲爹都准备放弃它了，未来将是小快灵组件协同工作的大趋势，Bourbon 就是可以用来替代 &lt;code&gt;compass/css3&lt;/code&gt; 的组件库。另外它的兄弟 &lt;a href="http://neat.bourbon.io/" rel="nofollow" target="_blank" title=""&gt;Neat&lt;/a&gt; 也不错，只是功能上和我们上述的工具集合有冲突了，不是很有必要。&lt;/p&gt;
&lt;h4 id="Breakpoint"&gt;Breakpoint&lt;/h4&gt;
&lt;p&gt;这个推荐一下，可以选用，主要是用来辅助响应式设计开发的，比 Bootstrap 自带的那点特性强大多了。&lt;a href="http://breakpoint-sass.com/" rel="nofollow" target="_blank"&gt;http://breakpoint-sass.com/&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="关于后期处理"&gt;关于后期处理&lt;/h2&gt;
&lt;p&gt;前面说的一大堆综合起来都是做 CSS 的前期处理的（也就是 pre-processing），现在前端也很重视后期处理（post-processing），关于这个话题呢推荐看一下 &lt;a href="http://pleeease.io/" rel="nofollow" target="_blank" title=""&gt;pleeease&lt;/a&gt; 也就差不多了。&lt;/p&gt;

&lt;p&gt;样式的后期处理有很多范畴，综合考虑我认为目前唯一称得上必须要做的那就是 &lt;a href="https://github.com/postcss/autoprefixer" rel="nofollow" target="_blank" title=""&gt;Autoprefixer&lt;/a&gt;，这个东西的特点及用法概括如下：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;有了它，你再也不用去写 &lt;a href="http://webdesign.about.com/od/css/a/css-vendor-prefixes.htm" rel="nofollow" target="_blank" title=""&gt;vendor prefixes&lt;/a&gt;，连想都不要去想（如果你用到的第三方组件越俎代庖了也没关系，Autoprefixer 会自动筛选一遍）&lt;/li&gt;
&lt;li&gt;当你构建的时候，它会自动分析你的样式，然后添加必要的 vendor prefixes&lt;/li&gt;
&lt;li&gt;你可以指定针对的浏览器品牌，版本，受众地区等等参量，从而让它知道哪些 vendor prefixes 是需要加的&lt;/li&gt;
&lt;li&gt;它通过 &lt;a href="http://caniuse.com/" rel="nofollow" target="_blank" title=""&gt;Can I Use&lt;/a&gt; 提供的技术数据来完成最终的工作&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://github.com/kimroen/ember-cli-autoprefixer" rel="nofollow" target="_blank" title=""&gt;ember-cli-autoprefixer&lt;/a&gt; 可以帮助你把它集成到 Ember CLI 项目中，简单好用。以下是一个配置的范例代码：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;EmberApp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;defaults&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// ...&lt;/span&gt;
    &lt;span class="na"&gt;autoprefixer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;browsers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;&amp;gt; 5% in CN&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;last 2 versions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;仔细阅读一下 Autoprefixer 的文档，你会发现配置它所用到的 &lt;a href="https://github.com/ai/browserslist" rel="nofollow" target="_blank" title=""&gt;DSL（BrowserList）&lt;/a&gt; 还蛮有趣的。&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;得，今天就说到这里，本来这篇早就写得差不多了，只是这两天一直在挖／填 Ember2 的一些坑没顾上整理，耽误了。到此前期的周边打造就差不多了，下篇开始我打算重点写一些和 Ember 的特性密切相关的东东，maybe 先从路由开始。&lt;/p&gt;</description>
      <author>nightire</author>
      <pubDate>Wed, 09 Sep 2015 02:53:04 +0800</pubDate>
      <link>https://ruby-china.org/topics/27246</link>
      <guid>https://ruby-china.org/topics/27246</guid>
    </item>
    <item>
      <title>[Tips on Ember 2] Ember CLI with Webstorm &amp; top level component/view</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;Tips on Ember 2 对我来说是没什么计划性的写作，我只是把它当做是每天工作的总结日志，一个很重要的目的是为团队做一些技术事务的整理，以帮助一些新人快速成长起来。如果有些内容不能满足各位看官的胃口，提前说声抱歉并且请不要担心，随着项目的逐渐开展，好戏会在后头。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h5 id="补充上篇的内容"&gt;补充上篇的内容&lt;/h5&gt;
&lt;p&gt;&lt;a href="https://ruby-china.org/topics/27155#%E9%87%8D%E5%86%99%E6%97%A7%E7%9A%84%20components" title=""&gt;上一篇我提到了暂时使用 &lt;code&gt;Ember.GlimmerComponent&lt;/code&gt; 取代 &lt;code&gt;Ember.Component&lt;/code&gt;&lt;/a&gt; 的事情，虽然有效但是却不得不改变编写 Component 的接口，着实挺烦的。其实我们可以直接替换掉旧的 &lt;code&gt;Ember.Component&lt;/code&gt; 便好，于是我加了很简单的一句：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// app/app.js&lt;/span&gt;
&lt;span class="c1"&gt;// ...&lt;/span&gt;

&lt;span class="nx"&gt;Ember&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MODEL_FACTORY_INJECTIONS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

&lt;span class="nx"&gt;Ember&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GlimmerComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reopenClass&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;isComponentFactory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nx"&gt;Ember&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Component&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Ember&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GlimmerComponent&lt;/span&gt;

&lt;span class="nx"&gt;App&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;Ember&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Application&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;({...})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这样你 Component 该怎么写就怎么写，不用再改了。&lt;/p&gt;
&lt;h3 id="不喜欢 Ember 和 Ember CLI 的 Webstorm"&gt;不喜欢 Ember 和 Ember CLI 的 Webstorm&lt;/h3&gt;
&lt;p&gt;我是 Vim 党，很少折腾复杂笨重的 IDEs，然而小伙伴们不高兴了，纷纷表示 Vim 太难学还是要用 Webstorm（也有选择 Sublime／Atom 的，由于配置比较简单，略过）。好，你用就用吧，各种问题自己也不会看文档问谷歌，成天怨声载道的（不得不吐槽一下现在的年轻人……）。没办法，我自己来一遍配置，填掉所有的坑！&lt;/p&gt;

&lt;p&gt;Webstorm 以前用 Angular 的时候也试过，总体上还行，代码补全比较优秀，就是稍微有点慢；然而我是那种不依赖自动补全，就是喜欢手打的类型，所以还是轻便迅捷的 Editor 合我的胃口。这次换成搭配 Ember 和 Ember CLI 了，好家伙～各种小问题层出不穷，官方就是不支持你也没有办法，最终我只能整理一下力所能及的配置过程了——&lt;/p&gt;
&lt;h3 id="项目特定"&gt;项目特定&lt;/h3&gt;&lt;h4 id="文件夹标记"&gt;文件夹标记&lt;/h4&gt;
&lt;p&gt;在 Webstorm 导入 Ember CLI 创建好的项目之后，打开［Preferences -&amp;gt; Project -&amp;gt; Directories］，然后针对项目里的各种目录打一些必要的标记；如图：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2015/444d69f57bd9b870917974cb18b35939.png" title="" alt="directories"&gt;&lt;/p&gt;

&lt;p&gt;简要解释一下这三个标记的作用：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Tests：&lt;/strong&gt;标记测试文件所在的根路径；对于很多测试框架来说这是 Webstorm 给它们指示测试文件位置的标志，然而由于并不支持 Ember CLI 所以没什么鸟用——当然你可以尝试绕过 Ember CLI 直接配置基于 QUnit 的测试环境，难～&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Excluded：&lt;/strong&gt;该标记作用下的文件夹会被 Webstorm 内部的各种机制排除在外，比如说代码补全、状态监视（版本控制）、项目结构解析（常用于重构等）等等；主要的作用就是提速——你把 &lt;code&gt;tmp/&lt;/code&gt; 去掉这个标记试试看！&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource Root:&lt;/strong&gt;标记静态资源的位置；之后在代码内但凡出现相对路径的资源索引，都会从这里面来找，比如说 HTML 里面的 &lt;code&gt;src&lt;/code&gt; 属性和 CSS 里面的 &lt;code&gt;url()&lt;/code&gt; 等等……&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;OK，后面两个其实是调整 Webstorm 性能与功能的关键平衡点，标记的过少会导致很多智能特性发挥不了用处，反之则会严重降低 IDE 的运行性能。我 09 年的老爷机通过合理配置（和上图那个 Demo 不完全一样，需要自己摸索）之后跑得比最新的 MBP 还顺畅我会随便乱讲？&lt;/p&gt;
&lt;h3 id="语言与框架"&gt;语言与框架&lt;/h3&gt;&lt;h4 id="Javascript"&gt;Javascript&lt;/h4&gt;
&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2015/60f405736e99f0ab4261a3fa2305e0f3.png" title="" alt="javascript"&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;目前版本的 Ember CLI 已经全面支持 ES6 了，所以这里的选择是理所当然的；&lt;/li&gt;
&lt;li&gt;严格模式不用选，因为 Ember CLI 创建的代码都是基于 ES6 Modules 的，默认都是严格模式，不需要 IDE 检查；&lt;/li&gt;
&lt;li&gt;最后一个选项决定了自动代码补全结果的丰富程度，不选会给你找出更多的补全项，但也意味着杂乱和性能损耗。&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="Node.js and NPM"&gt;Node.js and NPM&lt;/h4&gt;
&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2015/2fc6ea1d1876b8f24d95f72432f637e3.png" title="" alt="node-and-npm"&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;先跳到 node.js 和 npm 的配置，由于我们统一使用的是 io.js，所以如上图所示对应的路径。&lt;/li&gt;
&lt;li&gt;以前呢，io.js 的 sources 是不能在这里获取到的，最近的版本应该是修正了这个问题。不过下载下来的 sources 被命名为 &lt;em&gt;Node.js v3.2.0 Core Modules&lt;/em&gt; 由于我手动改了它的名字（后面会讲到），所以上图里看起来还是未下载和配置 sources 的样子。

&lt;ul&gt;
&lt;li&gt;这个 sources 有什么用？当你开发 node.js/io.js 模块时，如果能有对核心库的代码补全、分析、调试、文档等功能那自然是很爽。但是 node.js/io.js 把核心库都封装在了二进制运行命令中，IDE 无法直接获取到，因此这些 sources 就是用来做上述功能的。&lt;/li&gt;
&lt;li&gt;对于 Ember 应用程序开发来说，以上是非必需的，没有配置也没什么影响。只不过 Ember CLI 是工作于 node.js/io.js 环境下的，如果你经常需要看相关模块的代码，或是 debugging 它们，这就派上用场了。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;为什么使用 io.js？&lt;/strong&gt;有关 node.js 和 io.js 的纠结历史可以去谷歌一下，此处不再啰嗦；最根本的原因就是 io.js 对 ES6 的支持更好，更新和维护也更勤快。好消息是不久以后当 io.js 进入 v4 的时候，node.js 和 io.js 将再次合并，从此以后将只有一个 node.js v4 了。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id="Libraries"&gt;Libraries&lt;/h4&gt;
&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2015/cfeefeb013df9f156041e28d92fa55c5.png" title="" alt="libraries"&gt;&lt;/p&gt;

&lt;p&gt;node.js 和 npm 那边配置好以后呢，在这里会出现 &lt;em&gt;Node.js v3.2.0 Core Modules&lt;/em&gt;，如上图所示我已经把它更名为 &lt;em&gt;io.js v3.2.0 Core Modules&lt;/em&gt; 了。最上面那个 &lt;em&gt;ember-DefinitelyTyped&lt;/em&gt; 是 TypeScript 社区提供的 API Stubs，有助于快速的代码补全提示。TS Community 还提供了大量的 JS 库或框架的 API Stubs，非常便利。这些都可以在这个界面里搜索和安装。&lt;/p&gt;
&lt;h4 id="JSHint"&gt;JSHint&lt;/h4&gt;
&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2015/a297a0d4f182d324979995e009e5ffb3.png" title="" alt="jshint"&gt;&lt;/p&gt;

&lt;p&gt;Ember CLI 集成了代码质量控制工具 JSHint，Webstorm 也有很棒的内部支持，不过配置要跟着上图来，否则是没有用的。&lt;/p&gt;
&lt;h4 id="Templates"&gt;Templates&lt;/h4&gt;
&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2015/78ac3351c18394f64c69d9e3991f159b.png" title="" alt="templates"&gt;&lt;/p&gt;

&lt;p&gt;这些是关于模版引擎的支持，现在 Ember 已经使用了全新的模版引擎：HTMLBars，目前没什么 IDE 有完整支持的，最贴近的还是 Handlebars 插件（可能需要自行安装或开启，见［Preferences -&amp;gt; Plugins］）。&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;前两个选项完全是看个人偏好&lt;/li&gt;
&lt;li&gt;第三个不要选！选了之后会使用内置的格式控制，但实际上并没有针对 Handlebars 的格式控制调整，因此据我观察还是复用了 HTML 那一套，然而并不好用；Ember CLI 继承了 EditorConfig（Webstorm 有对应的插件支持），因此还是交给插件自己去控制，这样可以获得相对漂亮的代码格式控制&lt;/li&gt;
&lt;li&gt;第四个选项其实没啥用处（在 Ember 项目里全是 &lt;code&gt;.hbs&lt;/code&gt;，没有 &lt;code&gt;.html&lt;/code&gt; 什么事情），选不选都一样&lt;/li&gt;
&lt;li&gt;最后的注释还是选择 Handlebars 风格比较好&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id="Bower"&gt;Bower&lt;/h4&gt;
&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2015/d37f7563220cdbe3bf0daa925e19a0b3.png" title="" alt="bower"&gt;&lt;/p&gt;

&lt;p&gt;Bower 的支持配置很简单，路径选对就是了。&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;就这些，Webstorm 对 Ember 和 Ember CLI 的支持也就这样了，真心很有限，感觉有点对不起它的名声呀，我觉得你们大家还是投入到 Vim 的怀抱来吧！&lt;/p&gt;

&lt;p&gt;最后奉送一个 tip，在 v2.3 的 Routable Components 到来之前有用的：&lt;/p&gt;
&lt;h3 id="修改 toplevel component/view 的方法"&gt;修改 toplevel component/view 的方法&lt;/h3&gt;
&lt;p&gt;什么是 toplevel component/view 呢？就是应用初始化后在 &lt;code&gt;&amp;lt;body&amp;gt;&lt;/code&gt; 标签里插入的第一个 DOM 元素，它通常是这样的：&lt;/p&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;body&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"ember-xxx"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"ember-view"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;...&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/body&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;它看起来和其他的 components 差不多，唯一的问题是如果你想改它的 &lt;em&gt;tagName&lt;/em&gt;/&lt;em&gt;elementId&lt;/em&gt;/&lt;em&gt;classNames&lt;/em&gt; 等属性的时候该怎么办？&lt;/p&gt;

&lt;p&gt;创建一个 ApplicationComponent？没用……创建一个 ApplicationView？对不起，View 已经没了……&lt;/p&gt;

&lt;p&gt;在 Routable Components 出来之前（其实这就是一个典型的 Routable Component），唯一修改它的办法其实是创建一个叫 ApplicationView 的 Component——啥意思？看下面：&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;ember generate view application
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后编辑 &lt;code&gt;app/views/application.js&lt;/code&gt;&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Ember&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ember&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;Ember&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Component&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;tagName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;main&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;classNames&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如上，它是一个 Component，但要保存在 &lt;code&gt;app/views&lt;/code&gt; 下面，否则是找不到的。&lt;/p&gt;</description>
      <author>nightire</author>
      <pubDate>Wed, 02 Sep 2015 22:02:33 +0800</pubDate>
      <link>https://ruby-china.org/topics/27174</link>
      <guid>https://ruby-china.org/topics/27174</guid>
    </item>
    <item>
      <title>[Tips on Ember 2] 如何尝试 angle-bracket component</title>
      <description>&lt;blockquote&gt;
&lt;p&gt;Ruby China 的朋友大概都知道我很喜欢 Ember，然而我用 Ember 的经历其实远比不上 Angular 那么丰富（Ember 业余爱好，Angular 做正儿八经的项目）。最近我换工作了，终于可以在新的项目里主导使用 Ember 来开发 Web App，恰逢 Ember 进入了 2.0 时代，许多东西和当初自己瞎玩的时候相比变化都很大。于是我就想把接下来在实际工作中的一些经验技巧都记录下来发在 Ruby China，希望对喜欢 Ember，关注前端开发的朋友们有所帮助。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;来到新的公司新的团队，终于可以彻彻底底的使用 Ember 了，由于接下来有了发挥的空间和自由，所以我特别想先尝试尝试那些“传说中了好久”的新特性，第一个想到的就是 angle-bracket component（也就是尖括号形式的 component，写起来如同 HTML 一样，这也是 Angular／React 等框架创造 component 的形式，一度是 Angular 的主推卖点）。&lt;/p&gt;

&lt;p&gt;虽然 Ember 2.0 已经发布了，但是 angle-bracket component 还要等到 ~2.1 才能在正式版本里出现，如果现在就想尝鲜的话就得使用 canary 版本了，这主要是因为 Ember 的新特性需要手动开启 [Feature Flags][feature-flags] 才能尝试，而目前只有 canary 版本允许你开启 Feature Flags（当前可以使用的 feature flags [有一份列表][feature-flags-list] 可查）。&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;⚠警告：&lt;/strong&gt;canary 版本是很不稳定的，并不推荐使用于要上线的应用。如果你要尝试新的特性，要么是新建一个测试用的 Ember App，要么是你的应用离正式上线还早并且你（和你的团队）折腾得起。就个人经验来说 canary 版本本身还算稳定（毕竟有测试），但问题主要出在：1）API 的变化没有文档，你需要自己去跟踪 issues；2）周边工具会收到影响（比如我在尝试 angle-bracket component 的时候，ember inspector 就有 bug 了，会影响正常的开发）&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;下面简要列举开启 angle-bracket component 相关的 feature flags 的步骤：&lt;/p&gt;
&lt;h3 id="升级 ember 和 ember-data（可选）至 canary 版本"&gt;升级 ember 和 ember-data（可选）至 canary 版本&lt;/h3&gt;
&lt;p&gt;更改 &lt;code&gt;bower.json&lt;/code&gt; 文件内相关的部分为：&lt;/p&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ember"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"components/ember#canary"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"ember-data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"components/ember-data#canary"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后执行 &lt;code&gt;$ bower uninstall ember &amp;amp;&amp;amp; bower uninstall ember-data &amp;amp;&amp;amp; bower install&lt;/code&gt;，或者你也可以不去 &lt;code&gt;uninstall&lt;/code&gt; 直接尝试 &lt;code&gt;bower install&lt;/code&gt;，但是有时候会需要解决烦人的依赖问题。&lt;/p&gt;
&lt;h3 id="开启相关的 feature flags"&gt;开启相关的 feature flags&lt;/h3&gt;
&lt;p&gt;编辑 &lt;code&gt;config/environment.js&lt;/code&gt;，在 &lt;code&gt;ENV.EmberENV.FEATURE&lt;/code&gt; 下添加下面的代码：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;ENV&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="na"&gt;EmberENV&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;FEATURES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ember-htmlbars-attribute-syntax&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ember-htmlbars-inline-if-helper&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ember-htmlbars-component-generation&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="重写旧的 components"&gt;重写旧的 components&lt;/h3&gt;
&lt;p&gt;旧的 components 都是 &lt;code&gt;Ember.Component&lt;/code&gt; 的子类，而 angle-bracket component 则是 &lt;code&gt;Ember.GlimmerComponent&lt;/code&gt; 的子类，所以你只需要保证这一点就可以完成转换了。一个新创建的 angle-bracket component 的代码看起来是这样的：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Ember&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ember&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nx"&gt;Ember&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GlimmerComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;不出意外的话我认为当正式版本发布后，现在的 &lt;code&gt;Ember.Component&lt;/code&gt; 将被 &lt;code&gt;Ember.GlimmerComponent&lt;/code&gt; 取代，所以以后可能还得改回来（这应该还有段日子的）。内部其他的 API 目前还是以 &lt;code&gt;Ember.Component&lt;/code&gt; 的文档为准，未来有什么变化以后再看吧。&lt;/p&gt;

&lt;p&gt;现在重要的是 component template 的写法，我在测试的时候把一个登录表单封装成了 component，以下是其 template 的写法和使用的方法：&lt;/p&gt;
&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- components/signin-form/template.hbs --&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;form&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-group"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"username"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;账号&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-control"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"username"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"username"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;username&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-group"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;label&lt;/span&gt; &lt;span class="na"&gt;for=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;密码&lt;span class="nt"&gt;&amp;lt;/label&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;input&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-control"&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"password"&lt;/span&gt; &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;attrs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;credential&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nv"&gt;password&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"form-group"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"submit"&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"button-submit"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;登录&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;pre class="highlight handlebars"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;signin-form&lt;/span&gt; &lt;span class="na"&gt;credential=&lt;/span&gt;&lt;span class="k"&gt;{{&lt;/span&gt;&lt;span class="nv"&gt;model&lt;/span&gt;&lt;span class="k"&gt;}}&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/signin-form&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如上，可以看到新的模版语法里给 component 传递数据和访问数据的一些写法上的变化，这些变化其实是依赖于 &lt;code&gt;'ember-htmlbars-attribute-syntax'&lt;/code&gt; 这个 feature flag 的。&lt;/p&gt;
&lt;h3 id="修复一个 deprecation warning"&gt;修复一个 deprecation warning&lt;/h3&gt;
&lt;p&gt;观察修改后的应用，可以看到这样的警告：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2015/3cc6bdf347318a5bbaa157670170613d.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;修补这个问题的代码很简单：&lt;/p&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;Ember&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;GlimmerComponent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reopenClass&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;isComponentFactory&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这段代码可以插入到 &lt;code&gt;app/app.js&lt;/code&gt; 文件里，在应用初始化的时候即时生效。在此功能正式发布之后应该是不需要这段补丁代码的，目前来说也不会影响使用。&lt;/p&gt;
&lt;h3 id="结束"&gt;结束&lt;/h3&gt;
&lt;p&gt;就是这样了，从现在开始你可以使用新的语法来创建 components，除此之外还有新的 htmlbars-attribute-syntax（上例所示）和 htmlbars-inline-if-helpers 特性。HTMLBars 对于模版语法带来的改变，可以参考这篇总结性的文章：&lt;a href="http://colintoh.com/blog/htmlbars" rel="nofollow" target="_blank"&gt;http://colintoh.com/blog/htmlbars&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;下一篇我打算讲讲 Sass 在 Ember CLI 里面的一些最佳方案，其中包括如何整合 Bootstrap 的 Sass 版（而不是直接 import 它的 css 版本），以及如何在此基础上使用其他的 Sass 库／框架等等。这个选题是因为新团队里的伙伴们对这件事情有些争执，所以我就确定了一个最佳方案，宗旨是：1）保证最大的灵活性和定制性；2）在此基础上让设置步骤足够简单。&lt;/p&gt;

&lt;p&gt;我也乐意听听大家的反馈，如果有什么事情是你觉得很想了解的，或是你已经做到了但是觉得还不足够好的，可以回复我，等我有了答案之后我也会如此整理出来和大家分享。&lt;/p&gt;
&lt;h3 id="关于 Ember Inspector 的 bug"&gt;关于 Ember Inspector 的 bug&lt;/h3&gt;
&lt;p&gt;目前使用 canary 版本的时候，ember inspector 会有一个隐蔽的 bug，其表现是：当你在 ember inspector 开启的状态下（开发者工具打开并且当前的 tab 是 Ember）刷新当前应用时页面会变空白，此时你可以在 console 下看到这样的警告：&lt;/p&gt;

&lt;p&gt;&lt;img src="https://l.ruby-china.com/photo/2015/31559723b708655aaa52ec7153d42057.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;这个问题其实和你的应用无关，是 ember inspector 在刷新后重新初始化的时候造成的，经个人测试只在最近的 canary 版本里存在，估计 ember inspector 更新以后会修复吧。有一个简单的临时解决办法就是关闭开发者工具然后再刷新就好了，之后再开启开发者工具之后 ember inspector 还可以用。其实你只要保证刷新的时候 ember inspector 不处于当前激活的 tab 就好了，开发者工具可以不关的。&lt;/p&gt;

&lt;p&gt;[feature-flags]: &lt;a href="http://guides.emberjs.com/v2.0.0/configuring-ember/feature-flags/" rel="nofollow" target="_blank"&gt;http://guides.emberjs.com/v2.0.0/configuring-ember/feature-flags/&lt;/a&gt;
[feature-flags-list]: &lt;a href="https://github.com/emberjs/ember.js/blob/master/FEATURES.md" rel="nofollow" target="_blank"&gt;https://github.com/emberjs/ember.js/blob/master/FEATURES.md&lt;/a&gt;&lt;/p&gt;</description>
      <author>nightire</author>
      <pubDate>Tue, 01 Sep 2015 16:47:17 +0800</pubDate>
      <link>https://ruby-china.org/topics/27155</link>
      <guid>https://ruby-china.org/topics/27155</guid>
    </item>
    <item>
      <title>分享一个价值超过 4K 美刀的创业大礼包 (价格不是我说的，自己算……)</title>
      <description>&lt;p&gt;开春以后大礼包到处都是啊，这个不管你们知不知道先分享出来吧：&lt;a href="http://startups.paddle.com/" rel="nofollow" target="_blank"&gt;http://startups.paddle.com/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;全是各种 Paas／Saas 的付费服务免费送，用得上的或者想体验一下的可以试试，早上我已成功拿到。注意我上云梯的时候拿不到，回墙内反而成功了…… 😕&lt;/p&gt;</description>
      <author>nightire</author>
      <pubDate>Thu, 07 May 2015 08:23:53 +0800</pubDate>
      <link>https://ruby-china.org/topics/25467</link>
      <guid>https://ruby-china.org/topics/25467</guid>
    </item>
    <item>
      <title>Apple Watch 开发的基础教程（免费视频）</title>
      <description>&lt;p&gt;&lt;a href="http://www.pluralsight.com/courses/apple-watch-fundamentals" rel="nofollow" target="_blank"&gt;http://www.pluralsight.com/courses/apple-watch-fundamentals&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;刚收到，分享出来，有对 Apple Watch Apps 开发感兴趣或有需求的不妨一看。&lt;/p&gt;</description>
      <author>nightire</author>
      <pubDate>Thu, 16 Apr 2015 16:52:57 +0800</pubDate>
      <link>https://ruby-china.org/topics/25173</link>
      <guid>https://ruby-china.org/topics/25173</guid>
    </item>
    <item>
      <title>CSS A to Z：一个非常好的涨 CSS 技能的视频系列 (可 iTunes 免费订阅)</title>
      <description>&lt;p&gt;我知道这里做 web 开发的朋友很多，但是大部分都受困于前端知识匮乏，特别是 CSS；大家都想把它掌握到随心所欲的地步但实在不容易。所以特意给各位推荐一个非常好的，关于 CSS 的视频系列：CSS A to Z。一共 26 集，每集关注一个知识点，涵盖了 CSS 绝大部分的理论知识和实操技能，尤其是把许多“坑点”都分析的清晰透彻，实在是居家旅行，杀人灭口的最佳选择……废话少说直接去订阅吧：&lt;a href="http://www.atozcss.com/" rel="nofollow" target="_blank"&gt;http://www.atozcss.com/&lt;/a&gt;&lt;/p&gt;</description>
      <author>nightire</author>
      <pubDate>Sat, 11 Apr 2015 14:49:42 +0800</pubDate>
      <link>https://ruby-china.org/topics/25098</link>
      <guid>https://ruby-china.org/topics/25098</guid>
    </item>
  </channel>
</rss>
