<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>frontend on I Code It</title>
    <link>https://icodeit.org/categories/frontend/</link>
    <description>Recent content in frontend on I Code It</description>
    <generator>Hugo -- gohugo.io</generator>
    <lastBuildDate>Tue, 18 Jun 2019 00:00:00 +0000</lastBuildDate><atom:link href="https://icodeit.org/categories/frontend/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>修复缺陷的正确姿势</title>
      <link>https://icodeit.org/2019/06/how-to-fix-a-defect/</link>
      <pubDate>Tue, 18 Jun 2019 00:00:00 +0000</pubDate>
      
      <guid>https://icodeit.org/2019/06/how-to-fix-a-defect/</guid>
      <description>如何修复一个缺陷  如果给我一个小时来修复一个缺陷，我会花50分钟来写测试，用剩下的10分钟来改代码
&amp;ndash; 本来是一句模仿爱因斯塔的名言，结果发现爱因斯坦并没有说过……
 你确定这是个缺陷吗？ 下午2点，你喝下了一杯拿铁，它可以保证你在接下来的几个小时内保持清醒。突然，一位QA同事急匆匆的走了过来，从他的表情你就看出来事情不妙。果然，他告诉你SIT环境有个重大缺陷，如果不及时修复，好几个测试流程都不能进行。没错，你在还没有完全搞清楚发生了什么值钱，就莫名其妙的突然变成为了系统中一个“blocker”。
这位QA的神情和他对事情的描述让你不自觉的受到了一些感染，你也开始有点焦虑。你想要快速切换到IDE下，赶紧复现并修复他描述的问题。毕竟，你可不想成为一个阻塞别人工作的人。
我对你急切的心情表示理解，不过在你实际动手改代码之前，先冷静一下，稍微抑制一下赶紧修复问题的那种冲动。我们来看看在面对如此场景，如何表现的更为专业，以及更加卓有成效。
开始之前 事实上，在开始修复任何一个缺陷之前，你需要确认它确实是一个缺陷。这一点经常为很多新手忽略，从而导致修复缺陷从艺术变成了救火工作。
作为一名靠谱的开发，在真正动手修复之前，你可以做这样一些预先的check：
 缺陷是不是发生在不受支持的浏览器上？ 部署之后，有没有清理浏览器缓存？ 下游系统是不是有计划内更新？ 确定部署了最新版本吗？（部署之后，有没有机制可以确保SIT是最新版本）  或者你可以当QA宣称他找到了一个缺陷时，你可以反问：“你有没有试着重启浏览器/系统？”。很多情况下，在做了这些检查之后，你发现问题自己就解决了 &amp;ndash; 此谓不战而屈人之兵也。
如果事情仅仅到这一步就结束的话，你就可以接着看看medium上的文章或者刷刷知乎什么的。不过有时候，事情就不会这么顺利了。
遗漏掉的需求 不论前期分析的多么完善，在实际项目的行进中，还是会遇到一些遗漏掉的需求点。比如招聘系统中的对留学生通道的考虑，银行系统中的海外信用卡的货币汇率转换等等。有时候，当听完QA的描述后，如果你和团队里的其他人都表示这个功能点是第一次听说，那么不用慌张，这很可能是一个被遗漏掉的需求。
这时候只需要冷静下来，将其记录下来，然后作为正常的Story流程进行即可（排列优先级，kick-off，被拖入in-doing等等）
还真是个缺陷 如果它竟然还不是一个漏掉的需求，承认自己写的代码有缺陷也不是什么丢人的事儿。而且即使是缺陷，也并不意味着需要立即修复。和所有的其他需求那样，缺陷也应该被分级，并当成一个正常的Story卡流入Backlog。
在实践中，我发现这一点非常关键。很多团队在开发过程进入修复缺陷阶段之后变得各种混乱，其源头也正是来源于此。一个非常糟糕的实践是：某个人负责将测试团队中发现的缺陷分发给指定的人，并一天两次的常规Check是否有所进展。这个貌似高频率反馈的过程可以毁掉团队的敏捷氛围：工作从拉动的方式变成了指派。
正确的做法是:为缺陷建立卡片，并和其他需求卡一起排列优先级，并通过拉动的方式流入开发流程，并像任何一张卡片那样进行kick-off，in-dev，sign-off等。换言之，不要特殊对待缺陷，把它当成普通的需求变更即可。
如何重现 一旦你确定了一个缺陷，并且需要修复它，那么第一件要做的事情自然是重现它。很多时候，重现并不那么容易。在实际项目中，应用可能有各种各样的外部依赖，比如：
 依赖网络请求来获取数据 依赖特定的浏览器（可能是旧版本，也可能是特定浏览器） 依赖后端服务正常工作  所有的后端服务的异常都可能导致前端页面上的非预期行为：一个空白页面或者一个有着大红色叉的对话框，上书“找不着对象”等等。这些错误有时候可能会很难复现，或者至少需要一些特别的设置才可以使之发生。
网络异常 网络异常非常常见，而且可以导致各种各样的异常行为。开发中，localhost或者办公室的千兆宽带往往很难看到一些仅仅会在4G或者更慢速网络中会出现的问题。而当多个页面请求中的某一个失败时才会出现的缺陷则更难以复现。
不过，Chrome提供的DevTools可以在很大程度上帮助你（不过你可能需要每隔一个月重新学习下这些Tab的布局和新功能，Chrome的开发团队非常乐于偷偷给DevTools上线一些新功能，并完全破坏掉之前的布局）。
假设你需要调试/复现一个关于loading控件的问题，这个缺陷仅仅在网络极差的情况下才可以复现。你可以使用Chrome自带的throttling来模拟这种极端的网络场景。而有时候，你需要模拟网络完全不可用的状况，可以勾选Offline.
除了这些常规操作之外，你可以以定义黑名单的方式来屏蔽掉一些指定的URL：
这样，发往黑名单中的URL被屏蔽，这样可以很方便的模拟出改服务不可用的场景。有时候，如果你在开发海外的站点，也可以通过屏蔽特定的URL，比如facebook或者twitter链接来查看该站点在国内的表现。
消除重现缺陷的其他blocker 现实中，大部分实际应用都有多个页面。如果缺陷在第N个页面上，而你每次都需要从第一步导航到第N步。而如果每一步都有一些必填字段要填写，那么要重现这个缺陷，特别是debug的时候就会变成一场噩梦：你修改了一行代码，live-reload自动刷新并将你重定向到第一页，然后你完成所有填写并到达要调试的组件，然后发现本应该是字符串的地方显示的是[Object Object]。
不过，如果你恰好使用React+Redux组合的话，Redux Devtools Extension可以节省你很多时间。通过使用这个插件，你可以将应用的状态导入/导出。
Setup非常容易，只需要在创建redux store的时候加上一行代码即可：
const store = createStore( reducer, /* preloadedState, */ window.__REDUX_DEVTOOLS_EXTENSION__ &amp;amp;&amp;amp; window.__REDUX_DEVTOOLS_EXTENSION__() ); 这样你可以使用该插件将应用在某一时刻的状态导出到本地文件。在导出的文件中，redux-dev-tools会将应用的初始状态和所有发生过的事件（以及事件相关的数据）记录下来，以便重新播放。
当然了，如果你并没有使用React+Redux的组合，很多表单的auto-filler也是可以完成类似动作的。你可能需要花费几分钟来学习如何定义selector以及如何用快捷键来自动填写，不过一旦学会，它就可以节省你很多时间。
寻找根因 根据我的经验，很多缺陷都并非发生在逻辑错误上（比如倒置的if-else，没有跳出的while等）。相反，很多时候错误会发生在错误的mapping，空值的保护不足等场景。
非法数据 实际应用中，每个集成点都潜在的会有这样的问题。比如UI到BFF，BFF到下游的系统等。代码中的很多配置，或者不一致的惯例也会导致类似的问题。</description>
      <content:encoded><![CDATA[<h1 id="如何修复一个缺陷">如何修复一个缺陷</h1>
<blockquote>
<p>如果给我一个小时来修复一个缺陷，我会花50分钟来写测试，用剩下的10分钟来改代码</p>
<p>&ndash; 本来是一句模仿爱因斯塔的名言，结果发现爱因斯坦并没有说过……</p>
</blockquote>
<h2 id="你确定这是个缺陷吗">你确定这是个缺陷吗？</h2>
<p>下午2点，你喝下了一杯拿铁，它可以保证你在接下来的几个小时内保持清醒。突然，一位QA同事急匆匆的走了过来，从他的表情你就看出来事情不妙。果然，他告诉你<code>SIT</code>环境有个重大缺陷，如果不及时修复，好几个测试流程都不能进行。没错，你在还没有完全搞清楚发生了什么值钱，就莫名其妙的突然变成为了系统中一个“blocker”。</p>
<p>这位QA的神情和他对事情的描述让你不自觉的受到了一些感染，你也开始有点焦虑。你想要快速切换到IDE下，赶紧复现并修复他描述的问题。毕竟，你可不想成为一个阻塞别人工作的人。</p>
<p>我对你急切的心情表示理解，不过在你实际动手改代码之前，先冷静一下，稍微抑制一下<strong>赶紧修复问题</strong>的那种冲动。我们来看看在面对如此场景，如何表现的更为专业，以及更加卓有成效。</p>
<h3 id="开始之前">开始之前</h3>
<p>事实上，在开始修复任何一个缺陷之前，你需要确认它<strong>确实是</strong>一个缺陷。这一点经常为很多新手忽略，从而导致修复缺陷从艺术变成了救火工作。</p>
<p>作为一名靠谱的开发，在真正动手修复之前，你可以做这样一些预先的<code>check</code>：</p>
<ul>
<li>缺陷是不是发生在不受支持的浏览器上？</li>
<li>部署之后，有没有清理浏览器缓存？</li>
<li>下游系统是不是有计划内更新？</li>
<li>确定部署了最新版本吗？（部署之后，有没有机制可以确保SIT是最新版本）</li>
</ul>
<p>或者你可以当<code>QA</code>宣称他找到了一个缺陷时，你可以反问：“你有没有试着重启浏览器/系统？”。很多情况下，在做了这些检查之后，你发现问题自己就解决了 &ndash; 此谓<strong>不战而屈人之兵</strong>也。</p>
<p>如果事情仅仅到这一步就结束的话，你就可以接着看看<code>medium</code>上的文章或者刷刷知乎什么的。不过有时候，事情就不会这么顺利了。</p>
<h3 id="遗漏掉的需求">遗漏掉的需求</h3>
<p>不论前期分析的多么完善，在实际项目的行进中，还是会遇到一些遗漏掉的需求点。比如招聘系统中的对留学生通道的考虑，银行系统中的海外信用卡的货币汇率转换等等。有时候，当听完QA的描述后，如果你和团队里的其他人都表示这个功能点是第一次听说，那么不用慌张，这很可能是一个被遗漏掉的需求。</p>
<p>这时候只需要冷静下来，将其记录下来，然后作为正常的Story流程进行即可（排列优先级，<code>kick-off</code>，被拖入<code>in-doing</code>等等）</p>
<h3 id="还真是个缺陷">还真是个缺陷</h3>
<p>如果它竟然还不是一个漏掉的需求，承认自己写的代码有缺陷也不是什么丢人的事儿。而且即使是缺陷，也并不意味着需要立即修复。和所有的其他需求那样，缺陷也应该被<strong>分级</strong>，并当成一个正常的Story卡流入Backlog。</p>
<p>在实践中，我发现这一点非常关键。很多团队在开发过程进入修复缺陷阶段之后变得各种混乱，其源头也正是来源于此。一个非常糟糕的实践是：某个人负责将测试团队中发现的缺陷分发给指定的人，并一天两次的常规<code>Check</code>是否有所进展。这个貌似高频率反馈的过程可以毁掉团队的敏捷氛围：工作从拉动的方式变成了指派。</p>
<p>正确的做法是:为缺陷建立卡片，并和其他需求卡一起排列优先级，并通过拉动的方式流入开发流程，并像任何一张卡片那样进行<code>kick-off</code>，<code>in-dev</code>，<code>sign-off</code>等。换言之，<strong>不要特殊对待缺陷</strong>，把它当成普通的需求变更即可。</p>
<h2 id="如何重现">如何重现</h2>
<p>一旦你确定了一个缺陷，并且需要修复它，那么第一件要做的事情自然是<strong>重现</strong>它。很多时候，重现并不那么容易。在实际项目中，应用可能有各种各样的外部依赖，比如：</p>
<ul>
<li>依赖网络请求来获取数据</li>
<li>依赖特定的浏览器（可能是旧版本，也可能是特定浏览器）</li>
<li>依赖后端服务正常工作</li>
</ul>
<p>所有的后端服务的异常都可能导致前端页面上的非预期行为：一个空白页面或者一个有着大红色叉的对话框，上书“找不着对象”等等。这些错误有时候可能会很难复现，或者至少需要一些特别的设置才可以使之发生。</p>
<h3 id="网络异常">网络异常</h3>
<p>网络异常非常常见，而且可以导致各种各样的异常行为。开发中，<code>localhost</code>或者办公室的千兆宽带往往很难看到一些仅仅会在4G或者更慢速网络中会出现的问题。而当多个页面请求中的某一个失败时才会出现的缺陷则更难以复现。</p>
<p>不过，Chrome提供的<code>DevTools</code>可以在很大程度上帮助你（不过你可能需要每隔一个月重新学习下这些Tab的布局和新功能，Chrome的开发团队非常乐于偷偷给<code>DevTools</code>上线一些新功能，并完全破坏掉之前的布局）。</p>
<p>假设你需要调试/复现一个关于<code>loading</code>控件的问题，这个缺陷仅仅在网络极差的情况下才可以复现。你可以使用Chrome自带的<code>throttling</code>来模拟这种极端的网络场景。而有时候，你需要模拟网络完全不可用的状况，可以勾选<code>Offline</code>.</p>
<p><img loading="lazy" src="/images/2019/06/throttling.png" type="" alt=""  /></p>
<p>除了这些常规操作之外，你可以以定义黑名单的方式来<strong>屏蔽</strong>掉一些指定的URL：</p>
<p><img loading="lazy" src="/images/2019/06/block-url.png" type="" alt=""  /></p>
<p>这样，发往黑名单中的<code>URL</code>被屏蔽，这样可以很方便的模拟出改服务<em>不可用</em>的场景。有时候，如果你在开发海外的站点，也可以通过屏蔽特定的URL，比如facebook或者twitter链接来查看该站点在国内的表现。</p>
<h3 id="消除重现缺陷的其他blocker">消除重现缺陷的其他blocker</h3>
<p>现实中，大部分实际应用都有多个页面。如果缺陷在第N个页面上，而你每次都需要从第一步导航到第N步。而如果每一步都有一些必填字段要填写，那么要重现这个缺陷，特别是<code>debug</code>的时候就会变成一场噩梦：你修改了一行代码，<code>live-reload</code>自动刷新并将你重定向到第一页，然后你完成所有填写并到达要调试的组件，然后发现本应该是字符串的地方显示的是<code>[Object Object]</code>。</p>
<p>不过，如果你恰好使用<code>React+Redux</code>组合的话，<a href="https://github.com/zalmoxisus/redux-devtools-extension">Redux Devtools Extension</a>可以节省你很多时间。通过使用这个插件，你可以将应用的状态导入/导出。</p>
<p><img loading="lazy" src="/images/2019/06/redux-import-export.png" type="" alt=""  /></p>
<p>Setup非常容易，只需要在创建<code>redux</code> <code>store</code>的时候加上一行代码即可：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="kr">const</span> <span class="nx">store</span> <span class="o">=</span> <span class="nx">createStore</span><span class="p">(</span>
    <span class="nx">reducer</span><span class="p">,</span> <span class="cm">/* preloadedState, */</span>
    <span class="nb">window</span><span class="p">.</span><span class="mi">__</span><span class="nx">REDUX_DEVTOOLS_EXTENSION__</span> <span class="o">&amp;&amp;</span> <span class="nb">window</span><span class="p">.</span><span class="mi">__</span><span class="nx">REDUX_DEVTOOLS_EXTENSION__</span><span class="p">()</span>
<span class="p">);</span>
</code></pre></div><p>这样你可以使用该插件将应用在某一时刻的状态导出到本地文件。在导出的文件中，<code>redux-dev-tools</code>会将应用的初始状态和所有发生过的事件（以及事件相关的数据）记录下来，以便重新播放。</p>
<p><img loading="lazy" src="/images/2019/06/state-file.png" type="" alt=""  /></p>
<p>当然了，如果你并没有使用<code>React+Redux</code>的组合，很多表单的<code>auto-filler</code>也是可以完成类似动作的。你可能需要花费几分钟来学习如何定义<code>selector</code>以及如何用快捷键来自动填写，不过一旦学会，它就可以节省你很多时间。</p>
<h2 id="寻找根因">寻找根因</h2>
<p>根据我的经验，很多缺陷都并非发生在逻辑错误上（比如倒置的<code>if-else</code>，没有跳出的<code>while</code>等）。相反，很多时候错误会发生在错误的mapping，空值的保护不足等场景。</p>
<h3 id="非法数据">非法数据</h3>
<p>实际应用中，每个集成点都潜在的会有这样的问题。比如<code>UI</code>到<code>BFF</code>，<code>BFF</code>到下游的系统等。代码中的很多配置，或者不一致的惯例也会导致类似的问题。</p>
<p>比如，<code>toggle</code>的定义本来是<code>TOGGLE_...</code>形式的，不过后来变成了这样：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="nx">features</span><span class="o">:</span> <span class="p">{</span>
    <span class="s2">&#34;ADVANCED_SEARCH&#34;</span><span class="o">:</span> <span class="kc">false</span>
<span class="p">}</span>
</code></pre></div><p>但是代码中有些地方还在使用旧的形式：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="p">{</span>
    <span class="nx">features</span><span class="p">.</span><span class="nx">TOGGLE_ADVANCED_SEARCH</span> <span class="o">&amp;&amp;</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">renderAdvancedSearchPanel</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div><p>另外一个很常见的问题是同一个实体在不同系统中有着不同的名字，或者同一个名字在不同的系统中有着不同的含义。比如<code>Product</code>，<code>Item</code>和<code>OrderItem</code>可能都指向了<code>ShoppingCart</code>，而<code>User</code>，<code>Customer</code>以及<code>Account</code>可能在数据库中是同一条记录等等。然后在集成的时候，前端需要展示一个<code>Customer</code>的列表，但是从后台获取的是一个<code>Account</code>的列表等。</p>
<h3 id="外部依赖">外部依赖</h3>
<p>另一个经常会导致问题的是应用外部的依赖。一个应用可能有各式各样的依赖，这些依赖或现或隐，显式的比如系统间调用，浏览器兼容性等，隐式的比如日期/时间等。</p>
<p>多年前，我遇到过一个印象深刻的关于日期的缺陷:</p>
<p><img loading="lazy" src="/images/2019/06/invalid-date-resized.png" type="" alt=""  /></p>
<p>经过好几个小时的分析，最终发现是由夏令时导致的，细节可以参考<a href="http://icodeit.org/2014/09/a-bug-about-time/">这篇文章</a>。不过所幸这种依赖可以通过各种各样的工程方式来模拟，比如修改操作系统中的时区来模拟应用所需要运行在的国家/地区，或者将时间调整到某个历史时期来重现特定的问题。</p>
<p>通过上述的各种工具和设置，你终于通过手工操作重现了该缺陷，现在我们来看看如何修复它。</p>
<h2 id="修复">修复</h2>
<p><strong>修复缺陷</strong>乍看起来好像就是改代码，这也是很多人常犯的一个<strong>错误</strong>。事实上，修复一个缺陷是实施<code>TDD</code>的一个绝佳机会，它甚至比从零开始开发更容易实施<code>TDD</code>。很多时候，在开发新功能的时候，人们会有各式各样的借口来拒绝实施<code>TDD</code>：诸如降低开发效率，团队能力不匹配等等。</p>
<p>而在修复工作中，通常对输入和输出的定义往往都非常完整：期望某个页面元素的值是<code>$1,200</code>，实际显示的是<code>$1000</code> &ndash; 这天然的就是一个测试用例!</p>
<p>所以修复缺陷的第一步是写一个测试来重现上述的手工重现步骤。当然了，你无须从网络异常开始模拟，而通常可以从<strong>当网络异常后，某些数据为空</strong>这样的setup来编写测试。</p>
<p>这样做的好处有很多：</p>
<ul>
<li>防止这个缺陷重新混入代码（比如某位同事不小心改坏了代码）</li>
<li>对本次修复更有信心</li>
<li>便于未来对代码的重构</li>
<li>重塑测试套件，使之与测试金字塔更为契合</li>
</ul>
<h3 id="编写自动化测试">编写自动化测试</h3>
<p>如果只是数据<code>mapping</code>的问题，添加一个用例会非常简单：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="nx">it</span><span class="p">(</span><span class="s1">&#39;renders label&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span>
  <span class="kr">const</span> <span class="nx">props</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">label</span><span class="o">:</span> <span class="s1">&#39;Name&#39;</span>
  <span class="p">}</span>
  
  <span class="kr">const</span> <span class="nx">wrapper</span> <span class="o">=</span> <span class="nx">shallow</span><span class="p">(</span><span class="o">&lt;</span><span class="nx">InputField</span> <span class="p">{...</span><span class="nx">props</span><span class="p">}</span> <span class="o">/&gt;</span><span class="p">);</span>
  <span class="nx">expect</span><span class="p">(</span><span class="nx">wrapper</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="s1">&#39;h6&#39;</span><span class="p">).</span><span class="nx">text</span><span class="p">()).</span><span class="nx">toBe</span><span class="p">(</span><span class="nx">props</span><span class="p">.</span><span class="nx">label</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div><p>或者测试一个下拉框中正确的消费了预定义的数据结构：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="nx">it</span><span class="p">(</span><span class="s1">&#39;renders dropdown&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span>
  <span class="kr">const</span> <span class="nx">props</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">label</span><span class="o">:</span> <span class="s1">&#39;State&#39;</span><span class="p">,</span>
    <span class="nx">options</span><span class="o">:</span> <span class="p">[</span>
      <span class="p">{</span> <span class="nx">label</span><span class="o">:</span> <span class="s1">&#39;VIC&#39;</span><span class="p">,</span> <span class="nx">value</span><span class="o">:</span> <span class="s1">&#39;Victoria&#39;</span> <span class="p">},</span>
      <span class="p">{</span> <span class="nx">label</span><span class="o">:</span> <span class="s1">&#39;WA&#39;</span><span class="p">,</span> <span class="nx">value</span><span class="o">:</span> <span class="s1">&#39;Western Australia&#39;</span> <span class="p">},</span>
      <span class="p">{</span> <span class="nx">label</span><span class="o">:</span> <span class="s1">&#39;SA&#39;</span><span class="p">,</span> <span class="nx">value</span><span class="o">:</span> <span class="s1">&#39;Southern Australia&#39;</span> <span class="p">},</span>
      <span class="p">{</span> <span class="nx">label</span><span class="o">:</span> <span class="s1">&#39;QLD&#39;</span><span class="p">,</span> <span class="nx">value</span><span class="o">:</span> <span class="s1">&#39;Queesland&#39;</span> <span class="p">},</span>
      <span class="p">{</span> <span class="nx">label</span><span class="o">:</span> <span class="s1">&#39;NSW&#39;</span><span class="p">,</span> <span class="nx">value</span><span class="o">:</span> <span class="s1">&#39;New South Wales&#39;</span> <span class="p">}</span>
    <span class="p">]</span>
  <span class="p">};</span>

  <span class="kr">const</span> <span class="nx">wrapper</span> <span class="o">=</span> <span class="nx">shallow</span><span class="p">(</span><span class="o">&lt;</span><span class="nx">Dropdown</span> <span class="p">{...</span><span class="nx">props</span><span class="p">}</span> <span class="o">/&gt;</span><span class="p">);</span>
  <span class="nx">expect</span><span class="p">(</span><span class="nx">wrapper</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="s1">&#39;&lt;Option&gt;&#39;</span><span class="p">).</span><span class="nx">length</span><span class="p">).</span><span class="nx">toBe</span><span class="p">(</span><span class="nx">props</span><span class="p">.</span><span class="nx">options</span><span class="p">.</span><span class="nx">length</span><span class="p">);</span>
<span class="p">});</span>
</code></pre></div><p>当然，并非所有缺陷都是这么简单。很多时候，组件间具有某些依赖，或者组件依赖于网络数据等等。比如，容器组件需往往要和后端API进行数据交换，而经常发生问题的是：当后端API更新了<code>schema</code>之后，忘记通知前端。</p>
<p>这时候，独立的测试就没有太大用途了，我们需要某个层次的集成测试。有时候我们甚至需要更高层次的端到端测试（如<code>selenium</code>或者<code>cypress</code>测试）或者契约测试等来确保集成的正确性。</p>
<p>不过通常来说，单元测试和集成测试可以覆盖大部分的场景，端到端级别保留尽可能少而精的测试即可。在单元测试中，你可以通过<code>mock</code>/<code>stub</code>的方式来模拟网络请求/响应。</p>
<p>比如：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="nx">it</span><span class="p">(</span><span class="s1">&#39;fetch data from remote&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span>
  <span class="nx">axios</span><span class="p">.</span><span class="nx">get</span> <span class="o">=</span> <span class="nx">jest</span><span class="p">.</span><span class="nx">fn</span><span class="p">().</span><span class="nx">mockImplementation</span><span class="p">(()</span> <span class="p">=&gt;</span> <span class="nb">Promise</span><span class="p">.</span><span class="nx">resolve</span><span class="p">({</span><span class="nx">data</span><span class="o">:</span> <span class="nx">books</span><span class="p">}));</span>
<span class="p">});</span>
</code></pre></div><p>当然，通常你需要模拟失败和成功两种场景（如果你需要对不同错误码进行不同响应的话，则需要更多的cases）。</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="nx">it</span><span class="p">(</span><span class="s1">&#39;Fetch data with error&#39;</span><span class="p">,</span> <span class="p">()</span> <span class="p">=&gt;</span> <span class="p">{</span>
  <span class="nx">axios</span><span class="p">.</span><span class="nx">get</span> <span class="o">=</span> <span class="nx">jest</span><span class="p">.</span><span class="nx">fn</span><span class="p">().</span><span class="nx">mockImplementation</span><span class="p">(()</span> <span class="p">=&gt;</span> <span class="nb">Promise</span><span class="p">.</span><span class="nx">reject</span><span class="p">({</span><span class="nx">message</span><span class="o">:</span> <span class="s1">&#39;Something went wrong&#39;</span><span class="p">}))</span>
<span class="p">});</span>
</code></pre></div><p>如果你需要以端到端的方式来验证某个关键路径，可以使用<a href="https://github.com/GoogleChrome/puppeteer">puppeteer</a> 或者 <a href="https://www.cypress.io/">cypress</a>。</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="nx">describe</span><span class="p">(</span><span class="s1">&#39;Fancy Application&#39;</span><span class="p">,</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
  <span class="nx">it</span><span class="p">(</span><span class="s1">&#39;Add new experience&#39;</span><span class="p">,</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
    <span class="nx">cy</span><span class="p">.</span><span class="nx">visit</span><span class="p">(</span><span class="s1">&#39;https://localhost:1234/experience&#39;</span><span class="p">);</span>

    <span class="nx">cy</span><span class="p">.</span><span class="nx">contains</span><span class="p">(</span><span class="s1">&#39;Add&#39;</span><span class="p">).</span><span class="nx">click</span><span class="p">();</span>
    
    <span class="nx">cy</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s1">&#39;.project&#39;</span><span class="p">).</span><span class="nx">type</span><span class="p">(</span><span class="s1">&#39;super&#39;</span><span class="p">)</span>
    <span class="nx">cy</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s1">&#39;.period&#39;</span><span class="p">).</span><span class="nx">type</span><span class="p">(</span><span class="s1">&#39;10 months&#39;</span><span class="p">)</span>
    <span class="nx">cy</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s1">&#39;.tech-stack&#39;</span><span class="p">).</span><span class="nx">type</span><span class="p">(</span><span class="s1">&#39;JavaScript&#39;</span><span class="p">)</span>

    <span class="nx">cy</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s1">&#39;h2&#39;</span><span class="p">).</span><span class="nx">should</span><span class="p">(</span><span class="s1">&#39;contain&#39;</span><span class="p">,</span> <span class="s1">&#39;Worked on project super for 10 months&#39;</span><span class="p">);</span>
  <span class="p">});</span>
<span class="p">});</span>
</code></pre></div><p>请注意，此处你在<strong>通过自动化测试来重现缺陷</strong>。如果有了一个失败的测试用例，即你可以通过自动化的方式来重现这个缺陷，那么剩下的事情就会变得简单：修改代码使得测试通过即可。</p>
<h3 id="考虑测试金字塔">考虑测试金字塔</h3>
<p>虽然端到端的测试非常具有诱惑力 &ndash; 它会将整个系统串联起来，并告诉你<strong>真实</strong>的结果。不过它很容易被滥用，太多的端到端测试一方面会导致构建时间过长，另一方面由于真实环境变量太多，大量的端到端测试相比于底层测试往往会比较脆弱。此外，端到端测试非常昂贵，不论是运行时间还是指导开发的debug都不那么开发者友好。</p>
<p>在添加任何额外的端到端测试之前，请优先考虑底层的测试 &ndash; 它们运行的更快，更容易帮助开发调试。至于端到端测试，有限的关键路径覆盖对于大部分应用来说就已经足够。</p>
<p>此外，在重构业务代码的同时，也要保持对测试代码的重构：删除重复的、废弃的测试，移动测试到上一层或者下一层，重新分组测试<code>suite</code>等等。每次修复，都尽量让测试套件更符合测试金字塔的原则。</p>
<p><img loading="lazy" src="/images/2019/06/test-pyramid.png" type="" alt=""  /></p>
<h3 id="防御式编程">防御式编程</h3>
<p>此外，一个在集成中频繁使用的<code>防御式编程</code>可以避免很多潜在的问题，即在系统中，对于输入往往采取<strong>不信任</strong>的假设。很多场景下，对嵌套数据的存取往往会导致问题，比如<code>shoppingCart[0].item.name.toLowerCase()</code>潜在可能产生若干个<code>can not access xxx of undefined</code>异常。</p>
<p>这时候，你可能不得不编写很多保护逻辑来组织代码：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="k">if</span><span class="p">(</span><span class="nx">shoppingCart</span> <span class="o">&amp;&amp;</span> <span class="nx">shoppingCart</span><span class="p">.</span><span class="nx">length</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span><span class="p">(</span><span class="nx">shoppingCart</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">item</span> <span class="o">&amp;&amp;</span> <span class="nx">shoppingCart</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">item</span><span class="p">.</span><span class="nx">name</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="nx">shoppingCart</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">item</span><span class="p">.</span><span class="nx">name</span><span class="p">.</span><span class="nx">toLowerCase</span><span class="p">();</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div><p><code>lodash</code>中的<code>_.get</code>可以大大简化类似的场景：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="mi">_</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="nx">cart</span><span class="p">,</span> <span class="s1">&#39;shoppingCart[0].item.name&#39;</span><span class="p">);</span>
</code></pre></div><p>另一个常见的技巧是提供<code>in-line</code>的保护机制如：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="nx">axios</span><span class="p">.</span><span class="nx">get</span><span class="p">().</span><span class="nx">then</span><span class="p">(</span><span class="nx">response</span> <span class="p">=&gt;</span> <span class="nx">response</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">products</span> <span class="o">||</span> <span class="p">[]);</span>
<span class="nx">axios</span><span class="p">.</span><span class="nx">get</span><span class="p">().</span><span class="nx">then</span><span class="p">(</span><span class="nx">response</span> <span class="p">=&gt;</span> <span class="nx">response</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">products</span> <span class="o">||</span> <span class="p">{});</span>
</code></pre></div><p>这种微小的技巧可以节省你花在<code>catch</code>各种运行时异常上的很多时间。</p>
<h2 id="修复之后">修复之后</h2>
<p>通常来说，在修复过程中，你可以通过频繁的<code>mini-showcase</code>(desk check)从QA或者BA那里得到反馈，确保你始终在正确的方向上。</p>
<p>另一个我经常会使用的实践是：在修复完一个大的缺陷后，你可以和团队分享一下修复的过程，比如如何<code>debug</code>，如何复现，根因分析，如何编写测试等等。这样可以帮助别人快速学习到你是如何处理问题的，反过来，你也可能通过团队讨论发现解决问题的新思路。</p>
<p>另外，比修复单一的缺陷更重要的是，这个实践可以帮助团队建立一个良好的、健康的氛围：对于缺陷而言，我们选择<strong>直面它，并从中学习</strong> &ndash; 而不是指责或者将其分配到指定的人员头上。</p>
<h2 id="小结">小结</h2>
<p>当有人告诉你，你的代码有缺陷时，不要慌张。首先确保这确实是一个缺陷（排除测试的打开方式错误，遗漏掉的需求等场景）。通过使用Chrome的<code>DevTools</code>，和一些其他插件，你可以非常高效的模拟一些场景，从而在本地手工重现缺陷。</p>
<p>一旦可以手工重现，你需要编写自动化测试来自动重现。有了测试之后，就可以按照常规的<code>TDD</code>流程来修复。在修复过程中，保持对测试金字塔的关注，必要时还需要重构测试套件，以确保测试和产品代码都处于一个良好的状态。</p>
<p>修复之后，通过<strong>对缺陷的分析和修复的过程</strong>的分享，让团队从中学习，并鼓励团队其他成员也这么做，使得团队可以在一个安全，健康的氛围中工作。</p>
<hr>
<p>关于缺陷（defect）和臭虫（bug）的名字，通常来说它们指的是同一个东西。甚至在很多上下文中，你都可以将这两个名词互换而不影响句子整体的意思。不过这里我倾向于使用缺陷(defect)，因为<code>bug</code>听着像是<strong>问题</strong>自己跑到代码中并藏了起来，而缺陷（是指和需求的<strong>偏差</strong>）则比较中立一些，也比较客观。</p>
<hr>
<p>P.S. 这篇文章翻译自我自己的同名<a href="https://itnext.io/how-to-fix-a-defect-like-a-boss-7f5e3390c51d">英文版博客</a>，重写了一部分，另外对缺陷修复的流程上有所修改。</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>前端页面性能调优</title>
      <link>https://icodeit.org/2017/02/frontend-page-performance-tuning/</link>
      <pubDate>Wed, 08 Feb 2017 00:00:00 +0000</pubDate>
      
      <guid>https://icodeit.org/2017/02/frontend-page-performance-tuning/</guid>
      <description>Web页面的性能 我们每天都会浏览很多的Web页面，使用很多基于Web的应用。这些站点看起来既不一样，用途也都各有不同，有在线视频，Social Media，新闻，邮件客户端，在线存储，甚至图形编辑，地理信息系统等等。虽然有着各种各样的不同，但是相同的是，他们背后的工作原理都是一样的：
 用户输入网址 浏览器加载HTML/CSS/JS，图片资源等 浏览器将结果绘制成图形 用户通过鼠标，键盘等与页面交互  这些种类繁多的页面，在用户体验方面也有很大差异：有的响应很快，用户很容易就可以完成自己想要做的事情；有的则慢慢吞吞，让焦躁的用户在受挫之后拂袖而去。毫无疑问，性能是影响用户体验的一个非常重要的因素，而影响性能的因素非常非常多，从用户输入网址，到用户最终看到结果，需要有很多的参与方共同努力。这些参与方中任何一个环节的性能都会影响到用户体验。
 宽带网速 DNS服务器的响应速度 服务器的处理能力 数据库性能 路由转发 浏览器处理能力  早在2006年，雅虎就发布了提升站点性能的指南，Google也发布了类似的指南。而且有很多工具可以和浏览器一起工作，对你的Web页面的加载速度进行评估：分析页面中资源的数量，传输是否采用了压缩，JS、CSS是否进行了精简，有没有合理的使用缓存等等。
如果你需要将这个过程与CI集成在一起，来对应用的性能进行监控，我去年写过一篇相关的博客。
本文打算从另一个角度来尝试加速页面的渲染：浏览器是如何工作的，要将一个页面渲染成用户可以看到的图形，浏览器都需要做什么，哪些过程比较耗时，以及如何避免这些过程（或者至少以更高效的方式）。
页面是如何被渲染的 说到性能优化，规则一就是：
 If you can&amp;rsquo;t measure it, you can&amp;rsquo;t improve it. - Peter Drucker
 根据浏览器的工作原理，我们可以分别对各个阶段进行度量。
图片来源：http://dietjs.com/tutorials/host#backend
像素渲染流水线  下载HTML文档 解析HTML文档，生成DOM 下载文档中引用的CSS、JS 解析CSS样式表，生成CSSOM 将JS代码交给JS引擎执行 合并DOM和CSSOM，生成Render Tree 根据Render Tree进行布局layout（为每个元素计算尺寸和位置信息） 绘制（Paint）每个层中的元素（绘制每个瓦片，瓦片这个词与GIS中的瓦片含义相同） 执行图层合并（Composite Layers）  使用Chrome的DevTools - Timing，可以很容易的获取一个页面的渲染情况，比如在Event Log页签上，我们可以看到每个阶段的耗时细节（清晰起见，我没有显示Loading和Scripting的耗时）：
上图中的Activity中，Recalculate Style就是上面的构建CSSOM的过程，其余Activity都分别于上述的过程匹配。
应该注意的是，浏览器可能会将Render Tree分成好几个层来分别绘制，最后再合并起来形成最终的结果，这个过程一般发生在GPU中。
Devtools中有一个选项：Rendering - Layers Borders，打开这个选项之后，你可以看到每个层，每个瓦片的边界。浏览器可能会启动多个线程来绘制不同的层/瓦片。
Chrome还提供一个Paint Profiler的高级功能，在Event Log中选择一个Paint，然后点击右侧的Paint Profiler就可以看到其中绘制的全过程：</description>
      <content:encoded><![CDATA[<h2 id="web页面的性能">Web页面的性能</h2>
<p>我们每天都会浏览很多的Web页面，使用很多基于Web的应用。这些站点看起来既不一样，用途也都各有不同，有在线视频，<code>Social Media</code>，新闻，邮件客户端，在线存储，甚至图形编辑，地理信息系统等等。虽然有着各种各样的不同，但是相同的是，他们背后的工作原理都是一样的：</p>
<ul>
<li>用户输入网址</li>
<li>浏览器加载<code>HTML/CSS/JS</code>，图片资源等</li>
<li>浏览器将结果绘制成图形</li>
<li>用户通过鼠标，键盘等与页面交互</li>
</ul>
<p><img loading="lazy" src="/images/2017/02/internet-resized.png" type="" alt=""  /></p>
<p>这些种类繁多的页面，在用户体验方面也有很大差异：有的响应很快，用户很容易就可以完成自己想要做的事情；有的则慢慢吞吞，让焦躁的用户在受挫之后拂袖而去。毫无疑问，性能是影响用户体验的一个非常重要的因素，而影响性能的因素非常非常多，从用户输入网址，到用户最终看到结果，需要有很多的参与方共同努力。这些参与方中任何一个环节的性能都会影响到用户体验。</p>
<ul>
<li>宽带网速</li>
<li>DNS服务器的响应速度</li>
<li>服务器的处理能力</li>
<li>数据库性能</li>
<li>路由转发</li>
<li>浏览器处理能力</li>
</ul>
<p>早在2006年，雅虎就发布了提升站点性能的<a href="https://developer.yahoo.com/performance/rules.html">指南</a>，Google也发布了类似的<a href="https://developers.google.com/speed/docs/insights/rules">指南</a>。而且有很多工具可以和浏览器一起工作，对你的Web页面的加载速度进行评估：分析页面中资源的数量，传输是否采用了压缩，JS、CSS是否进行了精简，有没有合理的使用缓存等等。</p>
<p>如果你需要将这个过程与CI集成在一起，来对应用的性能进行监控，我去年写过一篇<a href="http://icodeit.org/2016/02/performance-testing-in-ci/">相关的博客</a>。</p>
<p>本文打算从另一个角度来尝试加速页面的渲染：浏览器是如何工作的，要将一个页面渲染成用户可以看到的图形，浏览器都需要做什么，哪些过程比较耗时，以及如何避免这些过程（或者至少以更高效的方式）。</p>
<h2 id="页面是如何被渲染的">页面是如何被渲染的</h2>
<p>说到性能优化，<strong>规则一</strong>就是：</p>
<blockquote>
<p>If you can&rsquo;t measure it, you can&rsquo;t improve it. - Peter Drucker</p>
</blockquote>
<p>根据浏览器的工作原理，我们可以分别对各个阶段进行度量。</p>
<p><img loading="lazy" src="/images/2017/02/how-browser-works-frame-resized.png" type="" alt=""  /></p>
<p><em>图片来源：http://dietjs.com/tutorials/host#backend</em></p>
<h3 id="像素渲染流水线">像素渲染流水线</h3>
<ol>
<li>下载HTML文档</li>
<li>解析HTML文档，生成<code>DOM</code></li>
<li>下载文档中引用的CSS、JS</li>
<li>解析CSS样式表，生成<code>CSSOM</code></li>
<li>将JS代码交给JS引擎执行</li>
<li>合并DOM和CSSOM，生成<code>Render Tree</code></li>
<li>根据<code>Render Tree</code>进行布局<code>layout</code>（为每个元素计算尺寸和位置信息）</li>
<li>绘制（Paint）每个层中的元素（绘制每个瓦片，瓦片这个词与GIS中的瓦片含义相同）</li>
<li>执行图层合并（Composite Layers）</li>
</ol>
<p>使用Chrome的DevTools - Timing，可以很容易的获取一个页面的渲染情况，比如在<code>Event Log</code>页签上，我们可以看到每个阶段的耗时细节（清晰起见，我没有显示<code>Loading</code>和<code>Scripting</code>的耗时）：</p>
<p><img loading="lazy" src="/images/2017/02/timeline-resized.png" type="" alt="Timeline"  /></p>
<p>上图中的Activity中，<code>Recalculate Style</code>就是上面的构建<code>CSSOM</code>的过程，其余Activity都分别于上述的过程匹配。</p>
<p>应该注意的是，浏览器可能会将Render Tree分成好几个层来分别绘制，最后再合并起来形成最终的结果，这个过程一般发生在<code>GPU</code>中。</p>
<p>Devtools中有一个选项：<code>Rendering - Layers Borders</code>，打开这个选项之后，你可以看到每个层，每个瓦片的边界。浏览器可能会启动多个线程来绘制不同的层/瓦片。</p>
<p><img loading="lazy" src="/images/2017/02/layer-and-tile-resized.png" type="" alt="Layers and Tiles"  /></p>
<p>Chrome还提供一个<code>Paint Profiler</code>的高级功能，在<code>Event Log</code>中选择一个<code>Paint</code>，然后点击右侧的<code>Paint Profiler</code>就可以看到其中绘制的全过程：</p>
<p><img loading="lazy" src="/images/2017/02/paint-in-detial-resized.png" type="" alt="Paint in detail"  /></p>
<p>你可以拖动滑块来看到随着时间的前进，页面上元素被逐步绘制出来了。我录制了一个我的知乎活动页面的视频，不过需要翻墙。</p>
<iframe width="560" height="315" src="https://www.youtube.com/embed/gley7VZFx_I" frameborder="0" allowfullscreen></iframe>
<h3 id="常规策略">常规策略</h3>
<p>为了尽快的让用户看到页面内容，我们需要快速的完成<code>DOM+CSSOM - Layout - Paint - Composite Layers</code>的整个过程。一切会阻塞DOM生成，阻塞CSSOM生成的动作都应该尽可能消除，或者延迟。</p>
<p>在这个前提下，常见的做法有两种：</p>
<h4 id="分割css">分割CSS</h4>
<p>对于不同的浏览终端，同一终端的不同模式，我们可能会提供不同的规则集：</p>
<div class="highlight"><pre class="chroma"><code class="language-css" data-lang="css"><span class="p">@</span><span class="k">media</span> <span class="nt">print</span> <span class="p">{</span>
	<span class="nt">html</span> <span class="p">{</span>
		<span class="k">font-family</span><span class="p">:</span> <span class="s1">&#39;Open Sans&#39;</span><span class="p">;</span>
		<span class="k">font-size</span><span class="p">:</span> <span class="mi">12</span><span class="kt">px</span><span class="p">;</span>
	<span class="p">}</span>
<span class="p">}</span>

<span class="p">@</span><span class="k">media</span> <span class="nt">orientation</span><span class="p">:</span><span class="nd">landscape</span> <span class="p">{</span>
	<span class="o">//</span>
<span class="p">}</span>
</code></pre></div><p>如果将这些内容写到统一个文件中，浏览器需要下载并解析这些内容（虽然不会实际应用这些规则）。更好的做法是，将这些内容通过对<code>link</code>元素的<code>media</code>属性来指定：</p>
<div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html"><span class="p">&lt;</span><span class="nt">link</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;print.css&#34;</span> <span class="na">rel</span><span class="o">=</span><span class="s">&#34;stylesheet&#34;</span> <span class="na">media</span><span class="o">=</span><span class="s">&#34;print&#34;</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">link</span> <span class="na">href</span><span class="o">=</span><span class="s">&#34;landscape.css&#34;</span> <span class="na">rel</span><span class="o">=</span><span class="s">&#34;stylesheet&#34;</span> <span class="na">media</span><span class="o">=</span><span class="s">&#34;orientation:landscape&#34;</span><span class="p">&gt;</span>
</code></pre></div><p>这样，<code>print.css</code>和<code>landscape.css</code>的内容不会阻塞Render Tree的建立，用户可以更快的看到页面，从而获得更好的体验。</p>
<h4 id="高效的css规则">高效的CSS规则</h4>
<h5 id="css规则的优先级">CSS规则的优先级</h5>
<p>很多使用<code>SASS/LESS</code>的开发人员，太过分的喜爱嵌套规则的特性，这可能会导致复杂的、无必要深层次的规则，比如：</p>
<div class="highlight"><pre class="chroma"><code class="language-css" data-lang="css"><span class="p">#</span><span class="nn">container</span> <span class="p">{</span>
	<span class="err">p</span> <span class="err">{</span>
		<span class="err">.title</span> <span class="err">{</span>
			<span class="err">span</span> <span class="err">{</span>
				<span class="k">color</span><span class="p">:</span> <span class="mh">#f3f3f3</span><span class="p">;</span>
			<span class="p">}</span>
		<span class="err">}</span>
	<span class="err">}</span>
<span class="err">}</span>
</code></pre></div><p>在生成的CSS中，可以看到：</p>
<div class="highlight"><pre class="chroma"><code class="language-css" data-lang="css"><span class="p">#</span><span class="nn">container</span> <span class="nt">p</span> <span class="p">.</span><span class="nc">title</span> <span class="nt">span</span> <span class="p">{</span>
  <span class="k">color</span><span class="p">:</span> <span class="mh">#f3f3f3</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div><p>而这个层次可能并非必要。CSS规则越复杂，在构建Render Tree时，浏览器花费的时间越长。CSS规则有自己的优先级，不同的写法对效率也会有影响，特别是当规则很多的时候。这里有一篇关于<a href="https://css-tricks.com/specifics-on-css-specificity/">CSS规则优先级</a>的文章可供参考。</p>
<h5 id="使用gpu加速">使用GPU加速</h5>
<p>很多动画都会定时执行，每次执行都可能会导致浏览器的重新布局，比如：</p>
<div class="highlight"><pre class="chroma"><code class="language-css" data-lang="css"><span class="p">@</span><span class="k">keyframes</span> <span class="nt">my</span> <span class="p">{</span>
	<span class="nt">20</span><span class="o">%</span> <span class="p">{</span>
		<span class="k">top</span><span class="p">:</span> <span class="mi">10</span><span class="kt">px</span><span class="p">;</span>
	<span class="p">}</span>
	
	<span class="nt">50</span><span class="o">%</span> <span class="p">{</span>
		<span class="k">top</span><span class="p">:</span> <span class="mi">120</span><span class="kt">px</span><span class="p">;</span>
	<span class="p">}</span>
	
	<span class="nt">80</span><span class="o">%</span> <span class="p">{</span>
		<span class="k">top</span><span class="p">:</span> <span class="mi">10</span><span class="kt">px</span><span class="p">;</span>
	<span class="p">}</span>
<span class="p">}</span>
</code></pre></div><p>这些内容可以放到GPU加速执行（GPU是专门设计来进行图形处理的，在图形处理上，比CPU要高效很多）。可以通过使用<code>transform</code>来启动这一特性：</p>
<div class="highlight"><pre class="chroma"><code class="language-css" data-lang="css"><span class="p">@</span><span class="k">keyframes</span> <span class="nt">my</span> <span class="p">{</span>
	<span class="nt">20</span><span class="o">%</span> <span class="p">{</span>
		<span class="k">transform</span><span class="p">:</span> <span class="nb">translateY</span><span class="p">(</span><span class="mi">10</span><span class="kt">px</span><span class="p">);</span>
	<span class="p">}</span>

	<span class="nt">50</span><span class="o">%</span> <span class="p">{</span>
		<span class="k">transform</span><span class="p">:</span> <span class="nb">translateY</span><span class="p">(</span><span class="mi">120</span><span class="kt">px</span><span class="p">);</span>
	<span class="p">}</span>
		
	<span class="nt">80</span><span class="o">%</span> <span class="p">{</span>
		<span class="k">transform</span><span class="p">:</span> <span class="nb">translateY</span><span class="p">(</span><span class="mi">10</span><span class="kt">px</span><span class="p">);</span>
	<span class="p">}</span>
<span class="p">}</span>
</code></pre></div><h4 id="异步javascript">异步JavaScript</h4>
<p>我们知道，JavaScript的执行会阻塞DOM的构建过程，这是因为JavaScript中可能会有DOM操作：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="kd">var</span> <span class="nx">element</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">createElement</span><span class="p">(</span><span class="s1">&#39;div&#39;</span><span class="p">);</span>
<span class="nx">element</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">width</span> <span class="o">=</span> <span class="s1">&#39;200px&#39;</span><span class="p">;</span>
<span class="nx">element</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">color</span> <span class="o">=</span> <span class="s1">&#39;blue&#39;</span><span class="p">;</span>

<span class="nx">body</span><span class="p">.</span><span class="nx">appendChild</span><span class="p">(</span><span class="nx">element</span><span class="p">);</span>
</code></pre></div><p>因此浏览器会等等待JS引擎的执行，执行结束之后，再恢复DOM的构建。但是并不是所有的JavaScript都会设计DOM操作，比如审计信息，WebWorker等，对于这些脚本，我们可以显式地指定该脚本是<strong>不阻塞DOM渲染</strong>的。</p>
<div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html"><span class="p">&lt;</span><span class="nt">script</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;worker.js&#34;</span> <span class="na">async</span><span class="p">&gt;&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
</code></pre></div><p>带有<code>async</code>标记的脚本，浏览器仍然会下载它，并在合适的时机执行，但是不会影响DOM树的构建过程。</p>
<h3 id="首次渲染之后">首次渲染之后</h3>
<p>在首次渲染之后，页面上的元素还可能被不断的重新布局，重新绘制。如果处理不当，这些动作可能会产生性能问题，产生不好的用户体验。</p>
<ul>
<li>访问元素的某些属性</li>
<li>通过JavaScript修改元素的CSS属性</li>
<li>在<code>onScroll</code>中做耗时任务</li>
<li>图片的预处理（事先裁剪图片，而不是依赖浏览器在布局时的缩放）</li>
<li>在其他Event Handler中做耗时任务</li>
<li>过多的动画</li>
<li>过多的数据处理（可以考虑放入<code>WebWorker</code>内执行）</li>
</ul>
<h4 id="强制同步布局回流">强制同步布局/回流</h4>
<p>元素的一些属性和方法，当在被访问或者被调用的时候，会触发浏览器的布局动作（以及后续的Paint动作），而布局基本上都会波及页面上的所有元素。当页面元素比较多的时候，布局和绘制都会花费比较大。</p>
<p>通过Timeline，有时候你会看到这样的警告：</p>
<p><img loading="lazy" src="/images/2017/02/forced-reflow-resized.png" type="" alt=""  /></p>
<p>比如访问一个元素的<code>offsetWidth</code>（布局宽度）属性时，浏览器需要重新计算（重新布局），然后才能返回最新的值。如果这个动作发生在一个很大的循环中，那么浏览器就不得不进行多次的重新布局，这可能会产生严重的性能问题：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="k">for</span><span class="p">(</span><span class="kd">var</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">list</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
	<span class="nx">list</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">style</span><span class="p">.</span><span class="nx">width</span> <span class="o">=</span> <span class="nx">parent</span><span class="p">.</span><span class="nx">offsetWidth</span> <span class="o">+</span> <span class="s1">&#39;px&#39;</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div><p>正确的做法是，先将这个值读出来，然后缓存在一个变量上（触发一次重新布局），以便后续使用：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="kd">var</span> <span class="nx">parentWidth</span> <span class="o">=</span> <span class="nx">parent</span><span class="p">.</span><span class="nx">offsetWidth</span><span class="p">;</span>
<span class="k">for</span><span class="p">(</span><span class="kd">var</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">list</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
	<span class="nx">list</span><span class="p">[</span><span class="nx">i</span><span class="p">].</span><span class="nx">style</span><span class="p">.</span><span class="nx">width</span> <span class="o">=</span> <span class="nx">parentWidth</span> <span class="o">+</span> <span class="s1">&#39;px&#39;</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div><p>这里有一个完整的列表<a href="https://gist.github.com/paulirish/5d52fb081b3570c81e3a">触发布局</a>。</p>
<h4 id="css样式修改">CSS样式修改</h4>
<h5 id="布局相关属性修改">布局相关属性修改</h5>
<p>修改布局相关属性，会触发<code>Layout - Paint - Composite Layers</code>，比如对位置，尺寸信息的修改：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="kd">var</span> <span class="nx">element</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;#id&#39;</span><span class="p">);</span>
<span class="nx">element</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">width</span> <span class="o">=</span> <span class="s1">&#39;100px&#39;</span><span class="p">;</span>
<span class="nx">element</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">height</span> <span class="o">=</span> <span class="s1">&#39;100px&#39;</span><span class="p">;</span>

<span class="nx">element</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">top</span> <span class="o">=</span> <span class="s1">&#39;20px&#39;</span><span class="p">;</span>
<span class="nx">element</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">left</span> <span class="o">=</span> <span class="s1">&#39;20px&#39;</span><span class="p">;</span>
</code></pre></div><h5 id="绘制相关属性修改">绘制相关属性修改</h5>
<p>修改绘制相关属性，不会触发<code>Layout</code>，但是会触发后续的<code>Paint - Composite Layers</code>，比如对背景色，前景色的修改：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="kd">var</span> <span class="nx">element</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="s1">&#39;#id&#39;</span><span class="p">);</span>
<span class="nx">element</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">backgroundColor</span> <span class="o">=</span> <span class="s1">&#39;red&#39;</span><span class="p">;</span>
</code></pre></div><h5 id="其他属性">其他属性</h5>
<p>除了上边的两种之外，有一些特别的属性可以在不同的层中单独绘制，然后再合并图层。对这种属性的访问（如果正确使用了CSS）不会触发<code>Layout - Paint</code>，而是直接进行<code>Compsite Layers</code>:</p>
<ul>
<li>transform</li>
<li>opacity</li>
</ul>
<p><code>transform</code>展开的话又分为: <code>translate</code>, <code>scale</code>, <code>rotate</code>等，这些层应该放入单独的渲染层中，为了对这个元素创建一个独立的渲染层，你必须提升该元素。</p>
<p>可以通过这样的方式来提升该元素：</p>
<div class="highlight"><pre class="chroma"><code class="language-css" data-lang="css"><span class="p">.</span><span class="nc">element</span> <span class="p">{</span>
  <span class="k">will-change</span><span class="p">:</span> <span class="k">transform</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div><blockquote>
<p>CSS 属性 will-change 为web开发者提供了一种告知浏览器该元素会有哪些变化的方法，这样浏览器可以在元素属性真正发生变化之前提前做好对应的优化准备工作。</p>
</blockquote>
<p>当然，额外的层次并不是没有代价的。太多的独立渲染层，虽然缩减了<code>Paint</code>的时间，但是增加了<code>Composite Layers</code>的时间，因此需要仔细权衡。在作调整之前，需要<code>Timeline</code>的运行结果来做支持。</p>
<p>还记得性能优化的规则一吗？</p>
<blockquote>
<p>If you can&rsquo;t measure it, you can&rsquo;t improve it. - Peter Drucker</p>
</blockquote>
<p>下面这个视频里可以看到，当鼠标挪动到特定元素时，由于CSS样式的变化，元素会被重新绘制：</p>
<iframe width="560" height="315" src="https://www.youtube.com/embed/c6wKfDpbwu8" frameborder="0" allowfullscreen></iframe>
<p><a href="https://csstriggers.com/">CSS Triggers</a>是一个完整的CSS属性列表，其中包含了会影响布局或者绘制的CSS属性，以及在不同的浏览器上的不同表现。</p>
<h3 id="总结">总结</h3>
<p>了解浏览器的工作方式，对我们做前端页面渲染性能的分析和优化都非常有帮助。为了高效而智能的完成渲染，浏览器也在不断的进行优化，比如资源的预加载，更好的利用GPU（启用更多的线程来渲染）等等。</p>
<p>另一方面，我们在编写前端的HTML、JS、CSS时，也需要考虑浏览器的现状：如何减少DOM、CSSOM的构建时间，如何将耗时任务放在单独的线程中（通过<code>WebWorker</code>）。</p>
<h3 id="参考资料">参考资料</h3>
<ul>
<li>Google出品的<a href="https://developers.google.com/web/fundamentals/performance/?hl=zh-cn">Web基础</a></li>
<li>一篇关于如何<a href="https://varvy.com/pagespeed/optimize-css-delivery.html">优化CSS的文章</a></li>
<li><a href="https://varvy.com/performance/cssom.html">CSSOM</a>的介绍</li>
</ul>
]]></content:encoded>
    </item>
    
    <item>
      <title>Reflux 101</title>
      <link>https://icodeit.org/2015/11/get-started-with-reflux/</link>
      <pubDate>Mon, 09 Nov 2015 00:00:00 +0000</pubDate>
      
      <guid>https://icodeit.org/2015/11/get-started-with-reflux/</guid>
      <description>一点背景 React在设计之初就只关注在View本身上，其余部分如数据的获取，事件处理等，全然不在考虑之内。不过构建大型的Web前端应用，这些点又实在不可避免。所以Facebook的工程师提出了前端的Flux架构，这个架构的最大特点是单向数据流（后面详述）。但是Flux本身的实现有很多不合理的地方，比如单例的Dispatcher会在系统中有多种事件时导致臃肿的switch-cases等。
这里是Facebook官方提供的提供Flux的结构图。
其实整个Flux背后的思想也不是什么新东西。在很久之前，Win32的消息机制（以及很多的GUI系统）就在使用这个模型，而且这也是一种被证实可以用来构建大型软件的模型。
鉴于Flux本身只是一个架构，而且Facebook提供的参考实现又有一些问题，所以社区有了很多版本的Flux实现。比如我们这里会用到的Reflux。
Reflux简介 简而言之，Reflux里有两个组件：Store和Action。Store负责和数据相关的内容：从服务器上获取数据，并更新与其绑定的React组件（view controller）;Action是一个事件的集合。Action和Store通过convention来连接起来。
具体来说，一个典型的过程是：
 用户的动作(或者定时器)在组件上触发一个Action Reflux会调用对应的Store上的callback（自动触发） 这个callback在执行结束之后，会显式的触发（trigger）一个数据 对应的组件（可能是多个）的state会被更新 React组件检测到state的变化后，会自动重绘自身  一个例子 我们这里将使用React/Reflux开发一个实际的例子，从最简单的功能开始，逐步将其构建为一个较为复杂的应用。
这个应用是一个书签展示应用（数据来源于我的Google Bookmarks）。第一个版本的界面是这样的：
要构建这样一个列表应用，我们需要这样几个部分：
 一个用来fetch数据，存储数据的store （BookmarkStore） 一个用来表达事件的Action（BookmarkActions） 一个列表组件（BookmarkList） 一个组件条目组件（Bookmark）  定义Actions var Reflux = require(&amp;#39;reflux&amp;#39;); var BookmarkActions = Reflux.createActions([ &amp;#39;fetch&amp;#39; ]); module.exports = BookmarkActions; 第一个版本，我们只需要定义一个fetch事件即可。然后在Store中编写这个Action的回调：
定义Store var $ = require(&amp;#39;jquery&amp;#39;); var Reflux = require(&amp;#39;reflux&amp;#39;); var BookmarkActions = require(&amp;#39;../actions/bookmark-actions&amp;#39;); var Utils = require(&amp;#39;../utils/fetch-client&amp;#39;); var BookmarkStore = Reflux.createStore({ listenables: [BookmarkActions], init: function() { this.onFetch(); }, onFetch: function() { var self = this; Utils.</description>
      <content:encoded><![CDATA[<h3 id="一点背景">一点背景</h3>
<p>React在设计之初就只关注在View本身上，其余部分如<code>数据的获取</code>，<code>事件处理</code>等，全然不在考虑之内。不过构建大型的Web前端应用，这些点又实在不可避免。所以Facebook的工程师提出了前端的<code>Flux</code>架构，这个架构的最大特点是<code>单向数据流</code>（后面详述）。但是<code>Flux</code>本身的实现有很多不合理的地方，比如单例的Dispatcher会在系统中有多种事件时导致臃肿的<code>switch-cases</code>等。</p>
<p>这里是Facebook官方提供的提供<a href="https://github.com/facebook/flux/tree/master/examples/flux-todomvc#structure-and-data-flow">Flux的结构图</a>。</p>
<p><img loading="lazy" src="/images/2015/11/flux-diagram-resized.png" type="" alt="Flux Diagram"  /></p>
<p>其实整个Flux背后的思想也<a href="http://bitquabit.com/post/the-more-things-change/">不是什么新东西</a>。在很久之前，Win32的消息机制（以及很多的GUI系统）就在使用这个模型，而且这也是一种被证实可以用来构建大型软件的模型。</p>
<p>鉴于Flux本身只是一个架构，而且Facebook提供的参考实现又有一些问题，所以社区有了很多版本的Flux实现。比如我们这里会用到的<a href="http://spoike.ghost.io/deconstructing-reactjss-flux/">Reflux</a>。</p>
<h3 id="reflux简介">Reflux简介</h3>
<p>简而言之，<a href="https://github.com/reflux/refluxjs">Reflux</a>里有两个组件：Store和Action。Store负责和数据相关的内容：从服务器上获取数据，并更新与其绑定的React组件（view controller）;Action是一个事件的集合。Action和Store通过convention来连接起来。</p>
<p>具体来说，一个典型的过程是：</p>
<ol>
<li>用户的动作(或者定时器)在组件上触发一个Action</li>
<li>Reflux会调用对应的Store上的callback（自动触发）</li>
<li>这个callback在执行结束之后，会显式的触发（trigger）一个数据</li>
<li>对应的组件（可能是多个）的state会被更新</li>
<li>React组件检测到state的变化后，会自动重绘自身</li>
</ol>
<p><img loading="lazy" src="/images/2015/11/reflux-data-flow.png" type="" alt="reflux data flow"  /></p>
<h3 id="一个例子">一个例子</h3>
<p>我们这里将使用React/Reflux开发一个实际的例子，从最简单的功能开始，逐步将其构建为一个较为复杂的应用。</p>
<p>这个应用是一个书签展示应用（数据来源于我的Google Bookmarks）。第一个版本的界面是这样的：</p>
<p><img loading="lazy" src="/images/2015/11/bookmarks-list-resized.png" type="" alt="bookmarks list"  /></p>
<p>要构建这样一个列表应用，我们需要这样几个部分：</p>
<ol>
<li>一个用来<code>fetch</code>数据，存储数据的store （BookmarkStore）</li>
<li>一个用来表达事件的<code>Action</code>（BookmarkActions）</li>
<li>一个列表组件（BookmarkList）</li>
<li>一个组件条目组件（Bookmark）</li>
</ol>
<h4 id="定义actions">定义Actions</h4>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="kd">var</span> <span class="nx">Reflux</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;reflux&#39;</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">BookmarkActions</span> <span class="o">=</span> <span class="nx">Reflux</span><span class="p">.</span><span class="nx">createActions</span><span class="p">([</span>
	<span class="s1">&#39;fetch&#39;</span>
<span class="p">]);</span>

<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="nx">BookmarkActions</span><span class="p">;</span>
</code></pre></div><p>第一个版本，我们只需要定义一个<code>fetch</code>事件即可。然后在<code>Store</code>中编写这个Action的回调：</p>
<h4 id="定义store">定义Store</h4>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="kd">var</span> <span class="nx">$</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;jquery&#39;</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">Reflux</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;reflux&#39;</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">BookmarkActions</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;../actions/bookmark-actions&#39;</span><span class="p">);</span>

<span class="kd">var</span> <span class="nx">Utils</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;../utils/fetch-client&#39;</span><span class="p">);</span>

<span class="kd">var</span> <span class="nx">BookmarkStore</span> <span class="o">=</span> <span class="nx">Reflux</span><span class="p">.</span><span class="nx">createStore</span><span class="p">({</span>
	<span class="nx">listenables</span><span class="o">:</span> <span class="p">[</span><span class="nx">BookmarkActions</span><span class="p">],</span>

	<span class="nx">init</span><span class="o">:</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
		<span class="k">this</span><span class="p">.</span><span class="nx">onFetch</span><span class="p">();</span>
	<span class="p">},</span>

	<span class="nx">onFetch</span><span class="o">:</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
		<span class="kd">var</span> <span class="nx">self</span> <span class="o">=</span> <span class="k">this</span><span class="p">;</span>
		<span class="nx">Utils</span><span class="p">.</span><span class="nx">fetch</span><span class="p">(</span><span class="s1">&#39;/bookmarks&#39;</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">bookmarks</span><span class="p">)</span> <span class="p">{</span>
			<span class="nx">self</span><span class="p">.</span><span class="nx">trigger</span><span class="p">({</span>
				<span class="nx">data</span><span class="o">:</span> <span class="nx">bookmarks</span><span class="p">,</span>
				<span class="nx">match</span><span class="o">:</span> <span class="s1">&#39;&#39;</span>
			<span class="p">});</span>
		<span class="p">});</span>
	<span class="p">}</span>
<span class="p">});</span>

<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="nx">BookmarkStore</span><span class="p">;</span>
</code></pre></div><p>此处，我们使用<code>listenables: [BookmarkActions]</code>来将Store和Action关联起来，根据<code>convention</code>，<code>on</code>+事件名称就是回调函数的名称。这样当<code>Action</code>被触发的时候，<code>onFetch</code>会被调用。当获取到数据之后，这里会显式的<code>trigger</code>一个数据。</p>
<h4 id="list组件">List组件</h4>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="kd">var</span> <span class="nx">React</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;react&#39;</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">Reflux</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;reflux&#39;</span><span class="p">);</span>

<span class="kd">var</span> <span class="nx">BookmarkStore</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;../stores/bookmark-store.js&#39;</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">Bookmark</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;./bookmark.js&#39;</span><span class="p">);</span>

<span class="kd">var</span> <span class="nx">BookmarkList</span> <span class="o">=</span> <span class="nx">React</span><span class="p">.</span><span class="nx">createClass</span><span class="p">({</span>
	<span class="nx">mixins</span><span class="o">:</span> <span class="p">[</span><span class="nx">Reflux</span><span class="p">.</span><span class="nx">connect</span><span class="p">(</span><span class="nx">BookmarkStore</span><span class="p">,</span> <span class="s1">&#39;bookmarks&#39;</span><span class="p">)],</span>

	<span class="nx">getInitialState</span><span class="o">:</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
		<span class="k">return</span> <span class="p">{</span>
			<span class="nx">bookmarks</span><span class="o">:</span> <span class="p">{</span><span class="nx">data</span><span class="o">:</span> <span class="p">[]}</span>
		<span class="p">}</span>
	<span class="p">},</span>

	<span class="nx">render</span><span class="o">:</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
		<span class="kd">var</span> <span class="nx">list</span> <span class="o">=</span> <span class="p">[];</span>
		<span class="k">this</span><span class="p">.</span><span class="nx">state</span><span class="p">.</span><span class="nx">bookmarks</span><span class="p">.</span><span class="nx">data</span><span class="p">.</span><span class="nx">forEach</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">item</span><span class="p">)</span> <span class="p">{</span>
	      <span class="nx">list</span><span class="p">.</span><span class="nx">push</span><span class="p">(</span><span class="o">&lt;</span><span class="nx">Bookmark</span> <span class="nx">title</span><span class="o">=</span><span class="p">{</span><span class="nx">item</span><span class="p">.</span><span class="nx">title</span><span class="p">}</span> <span class="nx">created</span><span class="o">=</span><span class="p">{</span><span class="nx">item</span><span class="p">.</span><span class="nx">created</span><span class="p">}</span><span class="o">/&gt;</span><span class="p">)</span>
	    <span class="p">});</span>
	    
		<span class="k">return</span> <span class="o">&lt;</span><span class="nx">ul</span><span class="o">&gt;</span>
			<span class="p">{</span><span class="nx">list</span><span class="p">}</span>
		<span class="o">&lt;</span><span class="err">/ul&gt;</span>
	<span class="p">}</span>
<span class="p">});</span>

<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="nx">BookmarkList</span><span class="p">;</span>
</code></pre></div><p>在组件中，我们通过<code>mixins: [Reflux.connect(BookmarkStore, 'bookmarks')]</code>将Store和组件关联起来，这样List组件<code>state</code>上的<code>bookmarks</code>就和<code>BookmarkStore</code>连接起来了。当<code>state.bookmarks</code>变化之后，<code>render</code>方法就会被自动调用。</p>
<p>对于每一个书签，就只是简单的展示内容即可：</p>
<h4 id="bookmark组件">Bookmark组件</h4>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="kd">var</span> <span class="nx">React</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;react&#39;</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">Reflux</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;reflux&#39;</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">moment</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;moment&#39;</span><span class="p">);</span>

<span class="kd">var</span> <span class="nx">Bookmark</span> <span class="o">=</span> <span class="nx">React</span><span class="p">.</span><span class="nx">createClass</span><span class="p">({</span>

	<span class="nx">render</span><span class="o">:</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
		<span class="kd">var</span> <span class="nx">created</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Date</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">.</span><span class="nx">created</span> <span class="o">*</span> <span class="mi">1000</span><span class="p">);</span>
		<span class="kd">var</span> <span class="nx">date</span> <span class="o">=</span> <span class="nx">moment</span><span class="p">(</span><span class="nx">created</span><span class="p">).</span><span class="nx">format</span><span class="p">(</span><span class="s1">&#39;YYYY-MM-DD&#39;</span><span class="p">);</span>

		<span class="k">return</span> <span class="o">&lt;</span><span class="nx">li</span><span class="o">&gt;</span>
			<span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="s1">&#39;bookmark&#39;</span><span class="o">&gt;</span>
				<span class="o">&lt;</span><span class="nx">h5</span> <span class="nx">className</span><span class="o">=</span><span class="s1">&#39;title&#39;</span><span class="o">&gt;</span><span class="p">{</span><span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">.</span><span class="nx">title</span><span class="p">}</span><span class="o">&lt;</span><span class="err">/h5&gt;</span>
				<span class="o">&lt;</span><span class="nx">span</span> <span class="nx">className</span><span class="o">=</span><span class="s1">&#39;date&#39;</span><span class="o">&gt;</span><span class="nx">Created</span> <span class="err">@</span> <span class="p">{</span><span class="nx">date</span><span class="p">}</span><span class="o">&lt;</span><span class="err">/span&gt;</span>
			<span class="o">&lt;</span><span class="err">/div&gt;</span>
		<span class="o">&lt;</span><span class="err">/li&gt;;</span>
	<span class="p">}</span>
<span class="p">});</span>

<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="nx">Bookmark</span><span class="p">;</span>
</code></pre></div><p>这里我使用了<code>moment</code>库将<code>unix timestamp</code>转换为日期字符串，然后写在页面上。</p>
<p>最后，<code>Utils</code>只是一个对<code>jQuery</code>的包装：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="kd">var</span> <span class="nx">$</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;jquery&#39;</span><span class="p">);</span>
<span class="kd">var</span> <span class="nb">Promise</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;promise&#39;</span><span class="p">);</span>

<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="p">{</span>
    <span class="nx">fetch</span><span class="o">:</span> <span class="kd">function</span><span class="p">(</span><span class="nx">url</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">var</span> <span class="nx">promise</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Promise</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">resolve</span><span class="p">,</span> <span class="nx">reject</span><span class="p">)</span> <span class="p">{</span>
            <span class="nx">$</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="nx">url</span><span class="p">).</span><span class="nx">done</span><span class="p">(</span><span class="nx">resolve</span><span class="p">).</span><span class="nx">fail</span><span class="p">(</span><span class="nx">reject</span><span class="p">);</span>
        <span class="p">});</span>

        <span class="k">return</span> <span class="nx">promise</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}</span>
</code></pre></div><p>我们再来总结一下，<code>BookmarkStore</code>在初始化的时候，显式地调用了<code>onFetch</code>，这个动作会导致<code>BookmarkList</code>组件的<code>state</code>的更新，这个更新会导致<code>BookmarkList</code>的重绘，<code>BookmarkList</code>会依次迭代所有的<code>Bookmark</code>。</p>
<h3 id="更复杂一些">更复杂一些</h3>
<p>当然，Reflux的好处不仅仅是上面描述的这种单向数据流。当<code>Store</code>，<code>Actions</code>以及具体的<code>组件</code>被解耦之后，构建大型的应用才能成为可能。我们来对上面的应用做一下扩展：我们为列表添加一个搜索功能。</p>
<p>随着用户的键入，我们发送请求到服务器，将过滤后的数据渲染为新的列表。我们需要这样几个东西</p>
<ol>
<li>一个SearchBox组件</li>
<li>一个新的<code>Action</code>：<code>search</code></li>
<li><code>BookmarkStore</code>上的一个新方法<code>onSearch</code></li>
<li>组件<code>SearchBox</code>需要和<code>BookmarkActions</code>关联起来</li>
</ol>
<p>为了让用户看到匹配的效果，我们需要将匹配到的关键字高亮起来，这样我们需要在<code>Bookmark</code>组件中监听<code>BookmarkStore</code>，当<code>BookmarkStore</code>发生变化之后，我们就可以即时修改书签的<code>title</code>了。</p>
<h4 id="搜索框组件">搜索框组件</h4>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="kd">var</span> <span class="nx">React</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;react&#39;</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">BookmarkActions</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;../actions/bookmark-actions&#39;</span><span class="p">);</span>

<span class="kd">var</span> <span class="nx">SearchBox</span> <span class="o">=</span> <span class="nx">React</span><span class="p">.</span><span class="nx">createClass</span><span class="p">({</span>
	<span class="nx">performSearch</span><span class="o">:</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
		<span class="kd">var</span> <span class="nx">keyword</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">refs</span><span class="p">.</span><span class="nx">keyword</span><span class="p">.</span><span class="nx">value</span><span class="p">;</span>
		<span class="nx">BookmarkActions</span><span class="p">.</span><span class="nx">search</span><span class="p">(</span><span class="nx">keyword</span><span class="p">);</span>
	<span class="p">},</span>

	<span class="nx">render</span><span class="o">:</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
		<span class="k">return</span> <span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="s2">&#34;search&#34;</span><span class="o">&gt;</span>
			<span class="o">&lt;</span><span class="nx">input</span> <span class="nx">type</span><span class="o">=</span><span class="s1">&#39;text&#39;</span> 
				<span class="nx">placeholder</span><span class="o">=</span><span class="s1">&#39;type to search...&#39;</span> 
				<span class="nx">ref</span><span class="o">=</span><span class="s2">&#34;keyword&#34;</span>
				<span class="nx">onChange</span><span class="o">=</span><span class="p">{</span><span class="k">this</span><span class="p">.</span><span class="nx">performSearch</span><span class="p">}</span> <span class="o">/&gt;</span>	
		<span class="o">&lt;</span><span class="err">/div&gt;</span>
	<span class="p">}</span>
<span class="p">});</span>

<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="nx">SearchBox</span><span class="p">;</span>
</code></pre></div><h4 id="bookmarkstore">BookmarkStore</h4>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="kd">var</span> <span class="nx">$</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;jquery&#39;</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">Reflux</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;reflux&#39;</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">BookmarkActions</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;../actions/bookmark-actions&#39;</span><span class="p">);</span>

<span class="kd">var</span> <span class="nx">Utils</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;../utils/fetch-client&#39;</span><span class="p">);</span>

<span class="kd">var</span> <span class="nx">BookmarkStore</span> <span class="o">=</span> <span class="nx">Reflux</span><span class="p">.</span><span class="nx">createStore</span><span class="p">({</span>
	<span class="nx">listenables</span><span class="o">:</span> <span class="p">[</span><span class="nx">BookmarkActions</span><span class="p">],</span>

	<span class="nx">init</span><span class="o">:</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
		<span class="k">this</span><span class="p">.</span><span class="nx">onFetch</span><span class="p">();</span>
	<span class="p">},</span>

	<span class="nx">onFetch</span><span class="o">:</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
		<span class="kd">var</span> <span class="nx">self</span> <span class="o">=</span> <span class="k">this</span><span class="p">;</span>
		<span class="nx">Utils</span><span class="p">.</span><span class="nx">fetch</span><span class="p">(</span><span class="s1">&#39;/bookmarks&#39;</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">bookmarks</span><span class="p">)</span> <span class="p">{</span>
			<span class="nx">self</span><span class="p">.</span><span class="nx">trigger</span><span class="p">({</span>
				<span class="nx">data</span><span class="o">:</span> <span class="nx">bookmarks</span><span class="p">,</span>
				<span class="nx">match</span><span class="o">:</span> <span class="s1">&#39;&#39;</span>
			<span class="p">});</span>
		<span class="p">});</span>
	<span class="p">},</span>

	<span class="nx">onSearch</span><span class="o">:</span> <span class="kd">function</span><span class="p">(</span><span class="nx">keyword</span><span class="p">)</span> <span class="p">{</span>
		<span class="kd">var</span> <span class="nx">self</span> <span class="o">=</span> <span class="k">this</span><span class="p">;</span>

		<span class="nx">Utils</span><span class="p">.</span><span class="nx">fetch</span><span class="p">(</span><span class="s1">&#39;/bookmarks?keyword=&#39;</span><span class="o">+</span><span class="nx">keyword</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">bookmarks</span><span class="p">)</span> <span class="p">{</span>
			<span class="nx">self</span><span class="p">.</span><span class="nx">trigger</span><span class="p">({</span>
				<span class="nx">data</span><span class="o">:</span> <span class="nx">bookmarks</span><span class="p">,</span>
				<span class="nx">match</span><span class="o">:</span> <span class="nx">keyword</span>
			<span class="p">});</span>
		<span class="p">});</span>
	<span class="p">}</span>
<span class="p">});</span>

<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="nx">BookmarkStore</span><span class="p">;</span>
</code></pre></div><p>我们在<code>BookmarkStore</code>中添加了<code>onSearch</code>方法，它会根据关键字来调用后台API进行搜索，并将结果<code>trigger</code>出去。由于数据本身的结构并没有变化（只是数量会由于过滤而变少），因此<code>BookmarkList</code>是无需修改的。</p>
<h4 id="书签高亮">书签高亮</h4>
<p>当搜索匹配之后，我们可以将对应的关键字高亮起来，这时候我们需要修改<code>Bookmark</code>组件：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="kd">var</span> <span class="nx">React</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;react&#39;</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">Reflux</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;reflux&#39;</span><span class="p">);</span>
<span class="kd">var</span> <span class="nx">moment</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;moment&#39;</span><span class="p">);</span>

<span class="kd">var</span> <span class="nx">BookmarkStore</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="s1">&#39;../stores/bookmark-store.js&#39;</span><span class="p">);</span>

<span class="kd">var</span> <span class="nx">Bookmark</span> <span class="o">=</span> <span class="nx">React</span><span class="p">.</span><span class="nx">createClass</span><span class="p">({</span>
	<span class="nx">mixins</span><span class="o">:</span> <span class="p">[</span><span class="nx">Reflux</span><span class="p">.</span><span class="nx">listenTo</span><span class="p">(</span><span class="nx">BookmarkStore</span><span class="p">,</span> <span class="s1">&#39;onMatch&#39;</span><span class="p">)],</span>

    <span class="nx">onMatch</span><span class="o">:</span> <span class="kd">function</span><span class="p">(</span><span class="nx">data</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">this</span><span class="p">.</span><span class="nx">setState</span><span class="p">({</span>
            <span class="nx">match</span><span class="o">:</span> <span class="nx">data</span><span class="p">.</span><span class="nx">match</span>
        <span class="p">});</span>
    <span class="p">},</span>

	<span class="nx">getInitialState</span><span class="o">:</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
		<span class="k">return</span> <span class="p">{</span>
			<span class="nx">match</span><span class="o">:</span> <span class="s1">&#39;&#39;</span>
		<span class="p">}</span>
	<span class="p">},</span>

	<span class="nx">render</span><span class="o">:</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
		<span class="kd">var</span> <span class="nx">created</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Date</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">.</span><span class="nx">created</span> <span class="o">*</span> <span class="mi">1000</span><span class="p">);</span>
		<span class="kd">var</span> <span class="nx">date</span> <span class="o">=</span> <span class="nx">moment</span><span class="p">(</span><span class="nx">created</span><span class="p">).</span><span class="nx">format</span><span class="p">(</span><span class="s1">&#39;YYYY-MM-DD&#39;</span><span class="p">);</span>

		<span class="kd">var</span> <span class="nx">title</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">.</span><span class="nx">title</span><span class="p">;</span>
		<span class="k">if</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">state</span><span class="p">.</span><span class="nx">match</span><span class="p">.</span><span class="nx">length</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
			<span class="nx">title</span> <span class="o">=</span> <span class="o">&lt;</span><span class="nx">span</span>
		        <span class="nx">dangerouslySetInnerHTML</span><span class="o">=</span><span class="p">{{</span>
		          <span class="mi">__</span><span class="nx">html</span> <span class="o">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">props</span><span class="p">.</span><span class="nx">title</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="k">new</span> <span class="nb">RegExp</span><span class="p">(</span><span class="s1">&#39;(&#39;</span><span class="o">+</span><span class="k">this</span><span class="p">.</span><span class="nx">state</span><span class="p">.</span><span class="nx">match</span><span class="o">+</span><span class="s1">&#39;)&#39;</span><span class="p">,</span> <span class="s2">&#34;gi&#34;</span><span class="p">),</span> <span class="s1">&#39;&lt;span class=&#34;highlight&#34;&gt;$1&lt;/span&gt;&#39;</span><span class="p">)</span>
		        <span class="p">}}</span> <span class="o">/&gt;</span>
		<span class="p">}</span>

		<span class="k">return</span> <span class="o">&lt;</span><span class="nx">li</span><span class="o">&gt;</span>
			<span class="o">&lt;</span><span class="nx">div</span> <span class="nx">className</span><span class="o">=</span><span class="s1">&#39;bookmark&#39;</span><span class="o">&gt;</span>
				<span class="o">&lt;</span><span class="nx">h5</span> <span class="nx">className</span><span class="o">=</span><span class="s1">&#39;title&#39;</span><span class="o">&gt;</span><span class="p">{</span><span class="nx">title</span><span class="p">}</span><span class="o">&lt;</span><span class="err">/h5&gt;</span>
				<span class="o">&lt;</span><span class="nx">span</span> <span class="nx">className</span><span class="o">=</span><span class="s1">&#39;date&#39;</span><span class="o">&gt;</span><span class="nx">Created</span> <span class="err">@</span> <span class="p">{</span><span class="nx">date</span><span class="p">}</span><span class="o">&lt;</span><span class="err">/span&gt;</span>
			<span class="o">&lt;</span><span class="err">/div&gt;</span>
		<span class="o">&lt;</span><span class="err">/li&gt;;</span>
	<span class="p">}</span>
<span class="p">});</span>

<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="nx">Bookmark</span><span class="p">;</span>
</code></pre></div><p><code>mixins: [Reflux.listenTo(BookmarkStore, 'onMatch')]</code>表示，我们需要监听<code>BookmarkStore</code>的变化，当变化发生时，调用<code>OnMatch</code>方法。<code>OnMatch</code>会修改组件的<code>match</code>属性，从而触发<code>render</code>。</p>
<p>在<code>render</code>中，我们替换关键字为<code>&lt;span class=&quot;highlight&quot;&gt;$keyword&lt;/span&gt;</code>，这样就可以达到预期的效果了：</p>
<p><img loading="lazy" src="/images/2015/11/bookmark-search-resized.png" type="" alt="bookmark search"  /></p>
<h3 id="结论">结论</h3>
<p>从上面的例子可以看到，我们从一开始就引入了Reflux。虽然第一个版本和React原生的写法差异并不是很大，但是当加入<code>SearchBox</code>功能之后，需要修改的地方非常清晰：添加<code>Actions</code>，在对应的<code>Store</code>中添加callback，然后在组件中使用。这种方法不仅可以最大程度的使用<code>React</code>的长处（diff render），而且使得代码逻辑变得较为清晰。</p>
<p>随着业务代码的不断增加，Reflux提供的方式确实可以在一定程度上控制代码的复杂性和可读性。</p>
<p>完整的<a href="https://github.com/abruzzi/react-reflux-demo">代码地址在这里</a>。</p>
<h3 id="其他参考">其他参考</h3>
<ul>
<li><a href="http://www.bunniesandbeatings.com/RefluxManifesto/">Reflux“宣言”</a></li>
<li><a href="https://news.ycombinator.com/item?id=10381015">Flux is the new WndProc</a></li>
<li><a href="https://ochronus.com/react-reflux-example/">React Reflux Example</a></li>
</ul>
]]></content:encoded>
    </item>
    
    <item>
      <title>前后端分离了，然后呢</title>
      <link>https://icodeit.org/2015/06/whats-next-after-separate-frontend-and-backend/</link>
      <pubDate>Mon, 22 Jun 2015 00:00:00 +0000</pubDate>
      
      <guid>https://icodeit.org/2015/06/whats-next-after-separate-frontend-and-backend/</guid>
      <description>前言 前后端分离已经是业界所共识的一种开发/部署模式了。所谓的前后端分离，并不是传统行业中的按部门划分，一部分人纯做前端（HTML/CSS/JavaScript/Flex），另一部分人纯做后端，因为这种方式是不工作的：比如很多团队采取了后端的模板技术（JSP, FreeMarker, ERB等等），前端的开发和调试需要一个后台Web容器的支持，从而无法做到真正的分离（更不用提在部署的时候，由于动态内容和静态内容混在一起，当设计动态静态分流的时候，处理起来非常麻烦）。关于前后端开发的另一个讨论可以参考这里。
即使通过API来解耦前端和后端开发过程，前后端通过RESTFul的接口来通信，前端的静态内容和后端的动态计算分别开发，分别部署，集成仍然是一个绕不开的问题 &amp;mdash; 前端/后端的应用都可以独立的运行，但是集成起来却不工作。我们需要花费大量的精力来调试，直到上线前仍然没有人有信心所有的接口都是工作的。
一点背景 一个典型的Web应用的布局看起来是这样的：
前后端都各自有自己的开发流程，构建工具，测试集合等等。前后端仅仅通过接口来编程，这个接口可能是JSON格式的RESTFul的接口，也可能是XML的，重点是后台只负责数据的提供和计算，而完全不处理展现。而前端则负责拿到数据，组织数据并展现的工作。这样结构清晰，关注点分离，前后端会变得相对独立并松耦合。
上述的场景还是比较理想，我们事实上在实际环境中会有非常复杂的场景，比如异构的网络，异构的操作系统等等:
在实际的场景中，后端可能还会更复杂，比如用C语言做数据采集，然后通过Java整合到一个数据仓库，然后该数据仓库又有一层Web Service，最后若干个这样的Web Service又被一个Ruby的聚合Service整合在一起返回给前端。在这样一个复杂的系统中，后台任意端点的失败都可能阻塞前端的开发流程，因此我们会采用mock的方式来解决这个问题：
这个mock服务器可以启动一个简单的HTTP服务器，然后将一些静态的内容serve出来，以供前端代码使用。这样的好处很多:
 前后端开发相对独立 后端的进度不会影响前端开发 启动速度更快 前后端都可以使用自己熟悉的技术栈（让前端的学maven，让后端的用gulp都会很不顺手）  但是当集成依然是一个令人头疼的难题。我们往往在集成的时候才发现，本来协商的数据结构变了：deliveryAddress字段本来是一个字符串，现在变成数组了（业务发生了变更，系统现在可以支持多个快递地址）；price字段变成字符串，协商的时候是number；用户邮箱地址多了一个层级等等。这些变动在所难免，而且时有发生，这会花费大量的调试时间和集成时间，更别提修改之后的回归测试了。
所以仅仅使用一个静态服务器，然后提供mock数据是远远不够的。我们需要的mock应该还能做到：
 前端依赖指定格式的mock数据来进行UI开发 前端的开发和测试都基于这些mock数据 后端产生指定格式的mock数据 后端需要测试来确保生成的mock数据正是前端需要的  简而言之，我们需要商定一些契约，并将这些契约作为可以被测试的中间格式。然后前后端都需要有测试来使用这些契约。一旦契约发生变化，则另一方的测试会失败，这样就会驱动双方协商，并降低集成时的浪费。
一个实际的场景是：前端发现已有的某个契约中，缺少了一个address的字段，于是就在契约中添加了该字段。然后在UI上将这个字段正确的展现了（当然还设置了字体，字号，颜色等等）。但是后台生成该契约的服务并没有感知到这一变化，当运行生成契约部分测试（后台）时，测试会失败了 &amp;mdash; 因为它并没有生成这个字段。于是后端工程师就找前端来商量，了解业务逻辑之后，他会修改代码，并保证测试通过。这样，当集成的时候，就不会出现UI上少了一个字段，但是谁也不知道是前端问题，后端问题，还是数据库问题等。
而且实际的项目中，往往都是多个页面，多个API，多个版本，多个团队同时进行开发，这样的契约会降低非常多的调试时间，使得集成相对平滑。
在实践中，契约可以定义为一个JSON文件，或者一个XML的payload。只需要保证前后端共享同一个契约集合来做测试，那么集成工作就会从中受益。一个最简单的形式是：提供一些静态的mock文件，而前端所有发往后台的请求都被某种机制拦截，并转换成对该静态资源的请求。
 moco，基于Java wiremock，基于Java sinatra，基于Ruby  看到sinatra被列在这里，可能熟悉Ruby的人会反对：它可是一个后端全功能的的程序库啊。之所以列它在这里，是因为sinatra提供了一套简洁优美的DSL，这个DSL非常契合Web语言，我找不到更漂亮的方式来使得这个mock server更加易读，所以就采用了它。
一个例子 我们以这个应用为示例，来说明如何在前后端分离之后，保证代码的质量，并降低集成的成本。这个应用场景很简单：所有人都可以看到一个条目列表，每个登陆用户都可以选择自己喜欢的条目，并为之加星。加星之后的条目会保存到用户自己的个人中心中。用户界面看起来是这样的：
不过为了专注在我们的中心上，我去掉了诸如登陆，个人中心之类的页面，假设你是一个已登录用户，然后我们来看看如何编写测试。
前端开发 根据通常的做法，前后端分离之后，我们很容易mock一些数据来自己测试：
[ { &amp;#34;id&amp;#34;: 1, &amp;#34;url&amp;#34;: &amp;#34;http://abruzzi.github.com/2015/03/list-comprehension-in-python/&amp;#34;, &amp;#34;title&amp;#34;: &amp;#34;Python中的 list comprehension 以及 generator&amp;#34;, &amp;#34;publicDate&amp;#34;: &amp;#34;2015年3月20日&amp;#34; }, { &amp;#34;id&amp;#34;: 2, &amp;#34;url&amp;#34;: &amp;#34;http://abruzzi.github.com/2015/03/build-monitor-script-based-on-inotify/&amp;#34;, &amp;#34;title&amp;#34;: &amp;#34;使用inotify/fswatch构建自动监控脚本&amp;#34;, &amp;#34;publicDate&amp;#34;: &amp;#34;2015年2月1日&amp;#34; }, { &amp;#34;id&amp;#34;: 3, &amp;#34;url&amp;#34;: &amp;#34;http://abruzzi.</description>
      <content:encoded><![CDATA[<h3 id="前言">前言</h3>
<p>前后端分离已经是业界所共识的一种开发/部署模式了。所谓的前后端分离，并<strong>不是</strong>传统行业中的按部门划分，一部分人纯做前端（HTML/CSS/JavaScript/Flex），另一部分人纯做后端，因为这种方式是<strong>不工作</strong>的：比如很多团队采取了后端的模板技术（JSP, FreeMarker, ERB等等），前端的开发和调试需要一个后台Web容器的支持，从而无法做到真正的分离（更不用提在部署的时候，由于动态内容和静态内容混在一起，当设计动态静态分流的时候，处理起来非常麻烦）。关于前后端开发的另一个讨论可以<a href="http://icodeit.org/2015/06/do-we-really-short-for-front-end-developer/">参考这里</a>。</p>
<p>即使通过API来解耦前端和后端开发过程，前后端通过<code>RESTFul</code>的接口来通信，前端的静态内容和后端的动态计算分别开发，分别部署，<strong>集成</strong>仍然是一个绕不开的问题 &mdash; 前端/后端的应用都可以独立的运行，但是集成起来却不工作。我们需要花费大量的精力来调试，直到上线前仍然没有人有信心所有的接口都是工作的。</p>
<h3 id="一点背景">一点背景</h3>
<p>一个典型的Web应用的布局看起来是这样的：</p>
<p><img loading="lazy" src="/images/2015/06/single-backend-resized.png" type="" alt="typical web application"  /></p>
<p>前后端都各自有自己的开发流程，构建工具，测试集合等等。前后端仅仅通过接口来编程，这个接口可能是JSON格式的RESTFul的接口，也可能是XML的，重点是后台只负责数据的提供和计算，而完全不处理展现。而前端则负责拿到数据，组织数据并展现的工作。这样结构清晰，关注点分离，前后端会变得相对独立并松耦合。</p>
<p>上述的场景还是比较理想，我们事实上在实际环境中会有非常复杂的场景，比如异构的网络，异构的操作系统等等:</p>
<p><img loading="lazy" src="/images/2015/06/multiple-backend-resized.png" type="" alt="real word application"  /></p>
<p>在实际的场景中，后端可能还会更复杂，比如用C语言做数据采集，然后通过Java整合到一个数据仓库，然后该数据仓库又有一层Web Service，最后若干个这样的Web Service又被一个Ruby的聚合Service整合在一起返回给前端。在这样一个复杂的系统中，后台任意端点的失败都可能阻塞前端的开发流程，因此我们会采用mock的方式来解决这个问题：</p>
<p><img loading="lazy" src="/images/2015/06/mock-server-resized.png" type="" alt="mock application"  /></p>
<p>这个<code>mock</code>服务器可以启动一个简单的HTTP服务器，然后将一些静态的内容serve出来，以供前端代码使用。这样的好处很多:</p>
<ol>
<li>前后端开发相对独立</li>
<li>后端的进度不会影响前端开发</li>
<li>启动速度更快</li>
<li>前后端都可以使用自己熟悉的技术栈（让前端的学maven，让后端的用gulp都会很不顺手）</li>
</ol>
<p>但是当<strong>集成</strong>依然是一个令人头疼的难题。我们往往在集成的时候才发现，本来协商的数据结构变了：<code>deliveryAddress</code>字段本来是一个字符串，现在变成数组了（业务发生了变更，系统现在可以支持多个快递地址）；<code>price</code>字段变成字符串，协商的时候是<code>number</code>；用户邮箱地址多了一个层级等等。这些变动在所难免，而且时有发生，这会花费大量的调试时间和集成时间，更别提修改之后的回归测试了。</p>
<p>所以仅仅使用一个静态服务器，然后提供<code>mock</code>数据是远远不够的。我们需要的<code>mock</code>应该还能做到：</p>
<ol>
<li>前端依赖指定格式的mock数据来进行UI开发</li>
<li>前端的开发和<strong>测试</strong>都基于这些mock数据</li>
<li>后端产生指定格式的mock数据</li>
<li>后端需要测试来确保生成的mock数据正是前端需要的</li>
</ol>
<p>简而言之，我们需要商定一些契约，并将这些契约作为<strong>可以被测试</strong>的中间格式。然后前后端都需要有测试来使用这些契约。一旦契约发生变化，则另一方的测试会失败，这样就会驱动双方协商，并降低集成时的浪费。</p>
<p>一个实际的场景是：前端发现已有的某个契约中，缺少了一个<code>address</code>的字段，于是就在契约中添加了该字段。然后在UI上将这个字段正确的展现了（当然还设置了字体，字号，颜色等等）。但是后台生成该契约的服务并没有感知到这一变化，当运行生成契约部分测试（后台）时，测试会失败了 &mdash; 因为它并没有生成这个字段。于是后端工程师就找前端来商量，了解业务逻辑之后，他会修改代码，并保证测试通过。这样，当集成的时候，就不会出现UI上少了一个字段，但是谁也不知道是前端问题，后端问题，还是数据库问题等。</p>
<p>而且实际的项目中，往往都是多个页面，多个API，多个版本，多个团队同时进行开发，这样的契约会降低非常多的调试时间，使得集成相对平滑。</p>
<p>在实践中，契约可以定义为一个JSON文件，或者一个XML的payload。只需要保证前后端<strong>共享同一个契约集合</strong>来做测试，那么集成工作就会从中受益。一个最简单的形式是：提供一些静态的<code>mock</code>文件，而前端所有发往后台的请求都被某种机制拦截，并转换成对该静态资源的请求。</p>
<ol>
<li><a href="https://github.com/dreamhead/moco">moco</a>，基于Java</li>
<li><a href="http://wiremock.org/index.html">wiremock</a>，基于Java</li>
<li><a href="http://www.sinatrarb.com/">sinatra</a>，基于Ruby</li>
</ol>
<p>看到<code>sinatra</code>被列在这里，可能熟悉<code>Ruby</code>的人会反对：它可是一个<code>后端</code>全功能的的程序库啊。之所以列它在这里，是因为<code>sinatra</code>提供了一套简洁优美的<code>DSL</code>，这个<code>DSL</code>非常契合<code>Web</code>语言，我找不到更漂亮的方式来使得这个<code>mock server</code>更加易读，所以就采用了它。</p>
<h3 id="一个例子">一个例子</h3>
<p>我们以这个应用为示例，来说明如何在前后端分离之后，保证代码的质量，并降低集成的成本。这个应用场景很简单：所有人都可以看到一个条目列表，每个登陆用户都可以选择自己喜欢的条目，并为之加星。加星之后的条目会保存到用户自己的<code>个人中心</code>中。用户界面看起来是这样的：</p>
<p><img loading="lazy" src="/images/2015/06/bookmarks-resized.png" type="" alt="bookmarks"  /></p>
<p>不过为了专注在我们的中心上，我去掉了诸如登陆，个人中心之类的页面，假设你是一个已登录用户，然后我们来看看如何编写测试。</p>
<h4 id="前端开发">前端开发</h4>
<p>根据通常的做法，前后端分离之后，我们很容易<code>mock</code>一些数据来自己测试：</p>
<div class="highlight"><pre class="chroma"><code class="language-json" data-lang="json"><span class="p">[</span>
    <span class="p">{</span>
        <span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
        <span class="nt">&#34;url&#34;</span><span class="p">:</span> <span class="s2">&#34;http://abruzzi.github.com/2015/03/list-comprehension-in-python/&#34;</span><span class="p">,</span>
        <span class="nt">&#34;title&#34;</span><span class="p">:</span> <span class="s2">&#34;Python中的 list comprehension 以及 generator&#34;</span><span class="p">,</span>
        <span class="nt">&#34;publicDate&#34;</span><span class="p">:</span> <span class="s2">&#34;2015年3月20日&#34;</span>
    <span class="p">},</span>
    <span class="p">{</span>
        <span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span>
        <span class="nt">&#34;url&#34;</span><span class="p">:</span> <span class="s2">&#34;http://abruzzi.github.com/2015/03/build-monitor-script-based-on-inotify/&#34;</span><span class="p">,</span>
        <span class="nt">&#34;title&#34;</span><span class="p">:</span> <span class="s2">&#34;使用inotify/fswatch构建自动监控脚本&#34;</span><span class="p">,</span>
        <span class="nt">&#34;publicDate&#34;</span><span class="p">:</span> <span class="s2">&#34;2015年2月1日&#34;</span>
    <span class="p">},</span>
    <span class="p">{</span>
        <span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
        <span class="nt">&#34;url&#34;</span><span class="p">:</span> <span class="s2">&#34;http://abruzzi.github.com/2015/02/build-sample-application-by-using-underscore-and-jquery/&#34;</span><span class="p">,</span>
        <span class="nt">&#34;title&#34;</span><span class="p">:</span> <span class="s2">&#34;使用underscore.js构建前端应用&#34;</span><span class="p">,</span>
        <span class="nt">&#34;publicDate&#34;</span><span class="p">:</span> <span class="s2">&#34;2015年1月20日&#34;</span>
    <span class="p">}</span>
<span class="p">]</span>
</code></pre></div><p>然后，一个可能的方式是通过请求这个json来测试前台：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="nx">$</span><span class="p">(</span><span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
	<span class="nx">$</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s1">&#39;/mocks/feeds.json&#39;</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">feeds</span><span class="p">)</span> <span class="p">{</span>
		<span class="kd">var</span> <span class="nx">feedList</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Backbone</span><span class="p">.</span><span class="nx">Collection</span><span class="p">(</span><span class="nx">extended</span><span class="p">);</span>
		<span class="kd">var</span> <span class="nx">feedListView</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">FeedListView</span><span class="p">(</span><span class="nx">feedList</span><span class="p">);</span>

		<span class="nx">$</span><span class="p">(</span><span class="s1">&#39;.container&#39;</span><span class="p">).</span><span class="nx">append</span><span class="p">(</span><span class="nx">feedListView</span><span class="p">.</span><span class="nx">render</span><span class="p">());</span>
	<span class="p">});</span>
<span class="p">});</span>
</code></pre></div><p>这样当然是可以工作的，但是这里发送请求的<code>url</code>并不是最终的，当集成的时候我们又需要修改为真实的<code>url</code>。一个简单的做法是使用<code>Sinatra</code>来做一次url的转换：</p>
<div class="highlight"><pre class="chroma"><code class="language-rb" data-lang="rb"><span class="n">get</span> <span class="s1">&#39;/api/feeds&#39;</span> <span class="k">do</span>
	<span class="n">content_type</span> <span class="s1">&#39;application/json&#39;</span>
	<span class="no">File</span><span class="o">.</span><span class="n">open</span><span class="p">(</span><span class="s1">&#39;mocks/feeds.json&#39;</span><span class="p">)</span><span class="o">.</span><span class="n">read</span>
<span class="k">end</span>
</code></pre></div><p>这样，当我们和实际的服务进行集成时，只需要连接到那个服务器就可以了。</p>
<p>注意，我们现在的核心是<code>mocks/feeds.json</code>这个文件。这个文件现在的角色就是一个契约，至少对于前端来说是这样的。紧接着，我们的应用需要渲染<code>加星</code>的功能，这就需要另外一个契约：找出当前用户加星过的所有条目，因此我们加入了一个新的契约：</p>
<div class="highlight"><pre class="chroma"><code class="language-json" data-lang="json"><span class="p">[</span>
    <span class="p">{</span>
        <span class="nt">&#34;id&#34;</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
        <span class="nt">&#34;url&#34;</span><span class="p">:</span> <span class="s2">&#34;http://abruzzi.github.com/2015/02/build-sample-application-by-using-underscore-and-jquery/&#34;</span><span class="p">,</span>
        <span class="nt">&#34;title&#34;</span><span class="p">:</span> <span class="s2">&#34;使用underscore.js构建前端应用&#34;</span><span class="p">,</span>
        <span class="nt">&#34;publicDate&#34;</span><span class="p">:</span> <span class="s2">&#34;2015年1月20日&#34;</span>
    <span class="p">}</span>
<span class="p">]</span>
</code></pre></div><p>然后在<code>sinatra</code>中加入一个新的映射：</p>
<div class="highlight"><pre class="chroma"><code class="language-rb" data-lang="rb"><span class="n">get</span> <span class="s1">&#39;/api/fav-feeds/:id&#39;</span> <span class="k">do</span>
	<span class="n">content_type</span> <span class="s1">&#39;application/json&#39;</span>
	<span class="no">File</span><span class="o">.</span><span class="n">open</span><span class="p">(</span><span class="s1">&#39;mocks/fav-feeds.json&#39;</span><span class="p">)</span><span class="o">.</span><span class="n">read</span>
<span class="k">end</span>
</code></pre></div><p>通过这两个请求，我们会得到两个列表，然后根据这两个列表的交集来绘制出所有的星号的状态（有的是空心，有的是实心）：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="nx">$</span><span class="p">.</span><span class="nx">when</span><span class="p">(</span><span class="nx">feeds</span><span class="p">,</span> <span class="nx">favorite</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">feeds</span><span class="p">,</span> <span class="nx">favorite</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="nx">ids</span> <span class="o">=</span> <span class="mi">_</span><span class="p">.</span><span class="nx">pluck</span><span class="p">(</span><span class="nx">favorite</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="s1">&#39;id&#39;</span><span class="p">);</span>
    <span class="kd">var</span> <span class="nx">extended</span> <span class="o">=</span> <span class="mi">_</span><span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="nx">feeds</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span> <span class="kd">function</span><span class="p">(</span><span class="nx">feed</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="mi">_</span><span class="p">.</span><span class="nx">extend</span><span class="p">(</span><span class="nx">feed</span><span class="p">,</span> <span class="p">{</span><span class="nx">status</span><span class="o">:</span> <span class="mi">_</span><span class="p">.</span><span class="nx">includes</span><span class="p">(</span><span class="nx">ids</span><span class="p">,</span> <span class="nx">feed</span><span class="p">.</span><span class="nx">id</span><span class="p">)});</span>
    <span class="p">});</span>

    <span class="kd">var</span> <span class="nx">feedList</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Backbone</span><span class="p">.</span><span class="nx">Collection</span><span class="p">(</span><span class="nx">extended</span><span class="p">);</span>
    <span class="kd">var</span> <span class="nx">feedListView</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">FeedListView</span><span class="p">(</span><span class="nx">feedList</span><span class="p">);</span>

    <span class="nx">$</span><span class="p">(</span><span class="s1">&#39;.container&#39;</span><span class="p">).</span><span class="nx">append</span><span class="p">(</span><span class="nx">feedListView</span><span class="p">.</span><span class="nx">render</span><span class="p">());</span>
<span class="p">});</span>
</code></pre></div><p>剩下的一个问题是当点击红心时，我们需要发请求给后端，然后更新红心的状态：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="nx">toggleFavorite</span><span class="o">:</span> <span class="kd">function</span><span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">event</span><span class="p">.</span><span class="nx">preventDefault</span><span class="p">();</span>
    <span class="kd">var</span> <span class="nx">that</span> <span class="o">=</span> <span class="k">this</span><span class="p">;</span>
    <span class="nx">$</span><span class="p">.</span><span class="nx">post</span><span class="p">(</span><span class="s1">&#39;/api/feeds/&#39;</span><span class="o">+</span><span class="k">this</span><span class="p">.</span><span class="nx">model</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s1">&#39;id&#39;</span><span class="p">)).</span><span class="nx">done</span><span class="p">(</span><span class="kd">function</span><span class="p">(){</span>
        <span class="kd">var</span> <span class="nx">status</span> <span class="o">=</span> <span class="nx">that</span><span class="p">.</span><span class="nx">model</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s1">&#39;status&#39;</span><span class="p">);</span>
        <span class="nx">that</span><span class="p">.</span><span class="nx">model</span><span class="p">.</span><span class="nx">set</span><span class="p">(</span><span class="s1">&#39;status&#39;</span><span class="p">,</span> <span class="o">!</span><span class="nx">status</span><span class="p">);</span>
    <span class="p">});</span>
<span class="p">}</span>
</code></pre></div><p>这里又多出来一个请求，不过使用Sinatra我们还是可以很容易的支持它：</p>
<div class="highlight"><pre class="chroma"><code class="language-rb" data-lang="rb"><span class="n">post</span> <span class="s1">&#39;/api/feeds/:id&#39;</span> <span class="k">do</span>
<span class="k">end</span>
</code></pre></div><p>可以看到，在没有后端的情况下，我们一切都进展顺利 &mdash; 后端甚至还没有开始做，或者正在由一个进度比我们慢的团队在开发，不过无所谓，他们不会影响我们的。</p>
<p>不仅如此，当我们写完前端的代码之后，可以做一个<code>End2End</code>的测试。由于使用了mock数据，免去了数据库和网络的耗时，这个<code>End2End</code>的测试会运行的非常快，并且它确实起到了端到端的作用。这些测试在最后的集成时，还可以用来当UI测试来运行。所谓一举多得。</p>
<div class="highlight"><pre class="chroma"><code class="language-rb" data-lang="rb"><span class="c1">#encoding: utf-8</span>
<span class="nb">require</span> <span class="s1">&#39;spec_helper&#39;</span>

<span class="n">describe</span> <span class="s1">&#39;Feeds List Page&#39;</span> <span class="k">do</span>
	<span class="n">let</span><span class="p">(</span><span class="ss">:list_page</span><span class="p">)</span> <span class="p">{</span><span class="no">FeedListPage</span><span class="o">.</span><span class="n">new</span><span class="p">}</span>

	<span class="n">before</span> <span class="k">do</span>
		<span class="n">list_page</span><span class="o">.</span><span class="n">load</span>
	<span class="k">end</span>

	<span class="n">it</span> <span class="s1">&#39;user can see a banner and some feeds&#39;</span> <span class="k">do</span>
		<span class="n">expect</span><span class="p">(</span><span class="n">list_page</span><span class="p">)</span><span class="o">.</span><span class="n">to</span> <span class="n">have_banner</span>
		<span class="n">expect</span><span class="p">(</span><span class="n">list_page</span><span class="p">)</span><span class="o">.</span><span class="n">to</span> <span class="n">have_feeds</span>
	<span class="k">end</span>

	<span class="n">it</span> <span class="s1">&#39;user can see 3 feeds in the list&#39;</span> <span class="k">do</span>
		<span class="n">expect</span><span class="p">(</span><span class="n">list_page</span><span class="o">.</span><span class="n">all_feeds</span><span class="p">)</span><span class="o">.</span><span class="n">to</span> <span class="n">have_feed_items</span> <span class="ss">count</span><span class="p">:</span> <span class="mi">3</span>
	<span class="k">end</span>

	<span class="n">it</span> <span class="s1">&#39;feed has some detail information&#39;</span> <span class="k">do</span>
		<span class="n">first</span> <span class="o">=</span> <span class="n">list_page</span><span class="o">.</span><span class="n">all_feeds</span><span class="o">.</span><span class="n">feed_items</span><span class="o">.</span><span class="n">first</span>
		<span class="n">expect</span><span class="p">(</span><span class="n">first</span><span class="o">.</span><span class="n">title</span><span class="p">)</span><span class="o">.</span><span class="n">to</span> <span class="n">eql</span><span class="p">(</span><span class="s2">&#34;Python中的 list comprehension 以及 generator&#34;</span><span class="p">)</span>
	<span class="k">end</span>
<span class="k">end</span>
</code></pre></div><p><img loading="lazy" src="/images/2015/06/e2e-resized.png" type="" alt="end 2 end"  /></p>
<p>关于如何编写这样的测试，可以参考之前写的<a href="http://icodeit.org/2015/01/page-object-with-site-prism/">这篇文章</a>。</p>
<h4 id="后端开发">后端开发</h4>
<p>我在这个示例中，后端采用了<code>spring-boot</code>作为示例，你应该可以很容易将类似的思路应用到Ruby或者其他语言上。</p>
<p>首先是请求的入口，<code>FeedsController</code>会负责解析请求路径，查数据库，最后返回JSON格式的数据。</p>
<div class="highlight"><pre class="chroma"><code class="language-java" data-lang="java"><span class="nd">@Controller</span>
<span class="nd">@RequestMapping</span><span class="o">(</span><span class="s">&#34;/api&#34;</span><span class="o">)</span>
<span class="kd">public</span> <span class="kd">class</span> <span class="nc">FeedsController</span> <span class="o">{</span>

    <span class="nd">@Autowired</span>
    <span class="kd">private</span> <span class="n">FeedsService</span> <span class="n">feedsService</span><span class="o">;</span>

    <span class="nd">@Autowired</span>
    <span class="kd">private</span> <span class="n">UserService</span> <span class="n">userService</span><span class="o">;</span>

    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">setFeedsService</span><span class="o">(</span><span class="n">FeedsService</span> <span class="n">feedsService</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">feedsService</span> <span class="o">=</span> <span class="n">feedsService</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="kd">public</span> <span class="kt">void</span> <span class="nf">setUserService</span><span class="o">(</span><span class="n">UserService</span> <span class="n">userService</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">this</span><span class="o">.</span><span class="na">userService</span> <span class="o">=</span> <span class="n">userService</span><span class="o">;</span>
    <span class="o">}</span>

    <span class="nd">@RequestMapping</span><span class="o">(</span><span class="n">value</span><span class="o">=</span><span class="s">&#34;/feeds&#34;</span><span class="o">,</span> <span class="n">method</span> <span class="o">=</span> <span class="n">RequestMethod</span><span class="o">.</span><span class="na">GET</span><span class="o">)</span>
    <span class="nd">@ResponseBody</span>
    <span class="kd">public</span> <span class="n">Iterable</span><span class="o">&lt;</span><span class="n">Feed</span><span class="o">&gt;</span> <span class="nf">allFeeds</span><span class="o">()</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">feedsService</span><span class="o">.</span><span class="na">allFeeds</span><span class="o">();</span>
    <span class="o">}</span>


    <span class="nd">@RequestMapping</span><span class="o">(</span><span class="n">value</span><span class="o">=</span><span class="s">&#34;/fav-feeds/{userId}&#34;</span><span class="o">,</span> <span class="n">method</span> <span class="o">=</span> <span class="n">RequestMethod</span><span class="o">.</span><span class="na">GET</span><span class="o">)</span>
    <span class="nd">@ResponseBody</span>
    <span class="kd">public</span> <span class="n">Iterable</span><span class="o">&lt;</span><span class="n">Feed</span><span class="o">&gt;</span> <span class="nf">favFeeds</span><span class="o">(</span><span class="nd">@PathVariable</span><span class="o">(</span><span class="s">&#34;userId&#34;</span><span class="o">)</span> <span class="n">Long</span> <span class="n">userId</span><span class="o">)</span> <span class="o">{</span>
        <span class="k">return</span> <span class="n">userService</span><span class="o">.</span><span class="na">favoriteFeeds</span><span class="o">(</span><span class="n">userId</span><span class="o">);</span>
    <span class="o">}</span>
<span class="o">}</span>
</code></pre></div><p>具体查询的细节我们就不做讨论了，感兴趣的可以在文章结尾处找到代码库的链接。那么有了这个Controller之后，我们如何测试它呢？或者说，如何让契约变得实际可用呢？</p>
<p><code>spring-test</code>提供了非常优美的DSL来编写测试，我们仅需要一点代码就可以将契约用起来，并实际的<strong>监督</strong>接口的修改：</p>
<div class="highlight"><pre class="chroma"><code class="language-java" data-lang="java"><span class="kd">private</span> <span class="n">MockMvc</span> <span class="n">mockMvc</span><span class="o">;</span>
<span class="kd">private</span> <span class="n">FeedsService</span> <span class="n">feedsService</span><span class="o">;</span>
<span class="kd">private</span> <span class="n">UserService</span> <span class="n">userService</span><span class="o">;</span>

<span class="nd">@Before</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">setup</span><span class="o">()</span> <span class="o">{</span>
    <span class="n">feedsService</span> <span class="o">=</span> <span class="n">mock</span><span class="o">(</span><span class="n">FeedsService</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>
    <span class="n">userService</span> <span class="o">=</span> <span class="n">mock</span><span class="o">(</span><span class="n">UserService</span><span class="o">.</span><span class="na">class</span><span class="o">);</span>

    <span class="n">FeedsController</span> <span class="n">feedsController</span> <span class="o">=</span> <span class="k">new</span> <span class="n">FeedsController</span><span class="o">();</span>
    <span class="n">feedsController</span><span class="o">.</span><span class="na">setFeedsService</span><span class="o">(</span><span class="n">feedsService</span><span class="o">);</span>
    <span class="n">feedsController</span><span class="o">.</span><span class="na">setUserService</span><span class="o">(</span><span class="n">userService</span><span class="o">);</span>

    <span class="n">mockMvc</span> <span class="o">=</span> <span class="n">standaloneSetup</span><span class="o">(</span><span class="n">feedsController</span><span class="o">).</span><span class="na">build</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div><p>建立了mockmvc之后，我们就可以编写Controller的单元测试了：</p>
<div class="highlight"><pre class="chroma"><code class="language-java" data-lang="java"><span class="nd">@Test</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">shouldResponseWithAllFeeds</span><span class="o">()</span> <span class="kd">throws</span> <span class="n">Exception</span> <span class="o">{</span>
    <span class="n">when</span><span class="o">(</span><span class="n">feedsService</span><span class="o">.</span><span class="na">allFeeds</span><span class="o">()).</span><span class="na">thenReturn</span><span class="o">(</span><span class="n">Arrays</span><span class="o">.</span><span class="na">asList</span><span class="o">(</span><span class="n">prepareFeeds</span><span class="o">()));</span>

    <span class="n">mockMvc</span><span class="o">.</span><span class="na">perform</span><span class="o">(</span><span class="n">get</span><span class="o">(</span><span class="s">&#34;/api/feeds&#34;</span><span class="o">))</span>
            <span class="o">.</span><span class="na">andExpect</span><span class="o">(</span><span class="n">status</span><span class="o">().</span><span class="na">isOk</span><span class="o">())</span>
            <span class="o">.</span><span class="na">andExpect</span><span class="o">(</span><span class="n">content</span><span class="o">().</span><span class="na">contentType</span><span class="o">(</span><span class="s">&#34;application/json;charset=UTF-8&#34;</span><span class="o">))</span>
            <span class="o">.</span><span class="na">andExpect</span><span class="o">(</span><span class="n">jsonPath</span><span class="o">(</span><span class="s">&#34;$&#34;</span><span class="o">,</span> <span class="n">hasSize</span><span class="o">(</span><span class="n">3</span><span class="o">)))</span>
            <span class="o">.</span><span class="na">andExpect</span><span class="o">(</span><span class="n">jsonPath</span><span class="o">(</span><span class="s">&#34;$[0].publishDate&#34;</span><span class="o">,</span> <span class="n">is</span><span class="o">(</span><span class="n">notNullValue</span><span class="o">())));</span>
<span class="o">}</span>
</code></pre></div><p>当发送<code>GET</code>请求到<code>/api/feeds</code>上之后，我们期望返回状态是200，然后内容是<code>application/json</code>。然后我们预期返回的结果是一个长度为3的数组，然后数组中的第一个元素的<code>publishDate</code>字段不为空。</p>
<p>注意此处的<code>prepareFeeds</code>方法，事实上它会去加载<code>mocks/feeds.json</code>文件 &mdash; 也就是前端用来测试的mock文件：</p>
<div class="highlight"><pre class="chroma"><code class="language-java" data-lang="java"><span class="kd">private</span> <span class="n">Feed</span><span class="o">[]</span> <span class="nf">prepareFeeds</span><span class="o">()</span> <span class="kd">throws</span> <span class="n">IOException</span> <span class="o">{</span>
    <span class="n">URL</span> <span class="n">resource</span> <span class="o">=</span> <span class="n">getClass</span><span class="o">().</span><span class="na">getResource</span><span class="o">(</span><span class="s">&#34;/mocks/feeds.json&#34;</span><span class="o">);</span>
    <span class="n">ObjectMapper</span> <span class="n">mapper</span> <span class="o">=</span> <span class="k">new</span> <span class="n">ObjectMapper</span><span class="o">();</span>
    <span class="k">return</span> <span class="n">mapper</span><span class="o">.</span><span class="na">readValue</span><span class="o">(</span><span class="n">resource</span><span class="o">,</span> <span class="n">Feed</span><span class="o">[].</span><span class="na">class</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div><p>这样，当后端修改<code>Feed</code>定义（添加/删除/修改字段），或者修改了mock数据等，都会导致测试失败；而前端修改mock之后，也会导致测试失败 &mdash; 不要惧怕失败 &mdash; 这样的失败会促进一次协商，并驱动出最终的service的契约。</p>
<p>对应的，测试<code>/api/fav-feeds/{userId}</code>的方式类似：</p>
<div class="highlight"><pre class="chroma"><code class="language-java" data-lang="java">
<span class="nd">@Test</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">shouldResponseWithUsersFavoriteFeeds</span><span class="o">()</span> <span class="kd">throws</span> <span class="n">Exception</span> <span class="o">{</span>
    <span class="n">when</span><span class="o">(</span><span class="n">userService</span><span class="o">.</span><span class="na">favoriteFeeds</span><span class="o">(</span><span class="n">any</span><span class="o">(</span><span class="n">Long</span><span class="o">.</span><span class="na">class</span><span class="o">)))</span>
        <span class="o">.</span><span class="na">thenReturn</span><span class="o">(</span><span class="n">Arrays</span><span class="o">.</span><span class="na">asList</span><span class="o">(</span><span class="n">prepareFavoriteFeeds</span><span class="o">()));</span>

    <span class="n">mockMvc</span><span class="o">.</span><span class="na">perform</span><span class="o">(</span><span class="n">get</span><span class="o">(</span><span class="s">&#34;/api/fav-feeds/1&#34;</span><span class="o">))</span>
            <span class="o">.</span><span class="na">andExpect</span><span class="o">(</span><span class="n">status</span><span class="o">().</span><span class="na">isOk</span><span class="o">())</span>
            <span class="o">.</span><span class="na">andExpect</span><span class="o">(</span><span class="n">content</span><span class="o">().</span><span class="na">contentType</span><span class="o">(</span><span class="s">&#34;application/json;charset=UTF-8&#34;</span><span class="o">))</span>
            <span class="o">.</span><span class="na">andExpect</span><span class="o">(</span><span class="n">jsonPath</span><span class="o">(</span><span class="s">&#34;$&#34;</span><span class="o">,</span> <span class="n">hasSize</span><span class="o">(</span><span class="n">1</span><span class="o">)))</span>
            <span class="o">.</span><span class="na">andExpect</span><span class="o">(</span><span class="n">jsonPath</span><span class="o">(</span><span class="s">&#34;$[0].title&#34;</span><span class="o">,</span> <span class="n">is</span><span class="o">(</span><span class="s">&#34;使用underscore.js构建前端应用&#34;</span><span class="o">)))</span>
            <span class="o">.</span><span class="na">andExpect</span><span class="o">(</span><span class="n">jsonPath</span><span class="o">(</span><span class="s">&#34;$[0].publishDate&#34;</span><span class="o">,</span> <span class="n">is</span><span class="o">(</span><span class="n">notNullValue</span><span class="o">())));</span>
<span class="o">}</span>
</code></pre></div><h3 id="总结">总结</h3>
<p>前后端分离是一件容易的事情，而且团队可能在短期可以看到很多好处，但是如果不认真处理集成的问题，分离反而可能会带来更长的集成时间。通过面向契约的方式来组织各自的测试，可以带来很多的好处：更快速的<code>End2End</code>测试，更平滑的集成，更安全的分离开发等等。</p>
<h3 id="代码">代码</h3>
<p>前后端的代码我都放到了Gitbub上，感兴趣的可以clone下来自行研究：</p>
<ol>
<li><a href="https://github.com/abruzzi/bookmarks-frontend">bookmarks-frontend</a></li>
<li><a href="https://github.com/abruzzi/bookmarks-server">bookmarks-server</a></li>
</ol>
]]></content:encoded>
    </item>
    
    <item>
      <title>我们真的缺前端工程师吗</title>
      <link>https://icodeit.org/2015/06/do-we-really-short-for-front-end-developer/</link>
      <pubDate>Sun, 14 Jun 2015 00:00:00 +0000</pubDate>
      
      <guid>https://icodeit.org/2015/06/do-we-really-short-for-front-end-developer/</guid>
      <description>前言 这两天在好几个地方都看到了一篇关于为什么整个互联网行业都缺前端工程师？的文章，文章本身是去年的，中心思想是：其实我们并不缺前端工程师，我们缺的是优秀的前端工程师。我还是比较同意作者观点的，不过略有意犹未尽的感觉。于是我结合自己的经验，也来聊一下这个话题：我们真的缺前端工程师吗？
These walls are kind of funny like that. First you hate them, then you get used to them.Enough time passed, get so you depend on them. That’s institutionalising.
传统软件公司划分开发者的方式下，在前端部门的程序员永远不会去读缓存数据部分的代码，设计师也不太可能去和开发坐在一起，开发也不知道软件最终软件会以何种方式部署在服务器上。
什么是“前端”工程师 我在招聘广告和办公室的一些对话中，听到了一个新的角色：UI Dev，事实上我在知乎上还回答过一个关于ThoughtWorks的UI Dev的问题。简而言之，UI Dev可以快速的把设计师的作品实现为HTML/CSS/JavaScript代码。
如果按照这个标准，我觉得UI Dev对自己的要求太低了。毕竟要学会HTML/CSS实现mockup并不困难，但是成为一名前端工程师则需要掌握更多的知识：
 会用PS来进行图片的处理（比如切图，微调等） 用HTML/CSS实现mockup（可能还有SASS/LESS等工具） 熟悉JavaScript（比如前端的MVVM框架，客户端模板） 前端开发的工作流程（代码检查，精简化，模块化CSS，LiveReload，调试） 编写测试（静态检查，单元测试） 跨浏览器、跨设备的解决方法（不同分辨率，不同厂商） 会根据项目的特点选择不同的前端技术栈（移动端，Web站点，响应式设计等）  在有了基础的HTML/CSS/JS技能之后，你会尝试做的更好：
 如何更高效的操作DOM 如何将CSS写的更加清晰易懂 如何编写更加易于维护的代码（更有意义的单元测试） 如何组织大型的项目结构，模块化，组件化等等  这些要求事实上已经不那么容易做到了。它可能会花费你2到3年时间来完全掌握。但是2到3年之后，即便你已经成为了一个“合格的”前端工程师，这也还远远不够。在现实世界中，一个软件产品除了前端，还有非常广阔的空间，还有很多有趣的东西值得学习：
 HTTP协议本身（缓存，鉴权） Web容器/HTTP服务器如何工作 无状态的Web应用的工作原理（如何让网站正确地运行在集群上） 动态，静态内容如何分离部署（反向代理配置） 安全机制如何配置 监控机制如何配置  有了这些，也算是有点端到端的意思了。这时你也已经不是一个“纯前端”工程师了，系统中的大部分问题你都可以搞定，不过日常工作中可能更多的职责还是做前端的开发。但是这些还不够，软件除了交付之外，还有一些非功能性的需求：
 端到端测试（UI测试，比如selenium server/web driver） devops（比如数据库环境，测试服务器，CI服务器的自动化provision） 基本的UI设计原则（在某些页面确实的情况下，根据系统的已有UI做设计） 数据库性能优化 性能测试  不过这些还只是我对于Web开发这个领域的总结。其他领域，比如大数据，机器学习，GIS，图像/视频处理等等。</description>
      <content:encoded><![CDATA[<h3 id="前言">前言</h3>
<p>这两天在好几个地方都看到了一篇关于<a href="http://chuansong.me/n/1369941">为什么整个互联网行业都缺前端工程师？</a>的文章，文章本身是去年的，中心思想是：其实我们并不缺前端工程师，我们缺的是优秀的前端工程师。我还是比较同意作者观点的，不过略有意犹未尽的感觉。于是我结合自己的经验，也来聊一下这个话题：<strong>我们真的缺前端工程师吗？</strong></p>
<p><code>These walls are kind of funny like that. First you hate them, then you get used to them.Enough time passed, get so you depend on them. That’s institutionalising.</code></p>
<p>传统软件公司划分开发者的方式下，在前端部门的程序员永远不会去读缓存数据部分的代码，设计师也不太可能去和开发坐在一起，开发也不知道软件最终软件会以何种方式部署在服务器上。</p>
<h3 id="什么是前端工程师">什么是“前端”工程师</h3>
<p>我在招聘广告和办公室的一些对话中，听到了一个新的角色：UI Dev，事实上我在知乎上还回答过一个<a href="http://www.zhihu.com/question/30170650">关于ThoughtWorks的UI Dev的问题</a>。简而言之，UI Dev可以快速的把设计师的作品实现为HTML/CSS/JavaScript代码。</p>
<p><img loading="lazy" src="/images/2015/06/website-development-resized.png" type="" alt="front-end development"  /></p>
<p>如果按照这个标准，我觉得UI Dev对自己的要求太低了。毕竟要学会HTML/CSS实现mockup并不困难，但是成为一名前端工程师则需要掌握更多的知识：</p>
<ul>
<li>会用PS来进行图片的处理（比如切图，微调等）</li>
<li>用HTML/CSS实现mockup（可能还有<a href="http://sass-lang.com/">SASS</a>/<a href="http://lesscss.org/">LESS</a>等工具）</li>
<li>熟悉JavaScript（比如前端的MVVM框架，客户端模板）</li>
<li>前端开发的工作流程（代码检查，精简化，模块化CSS，LiveReload，调试）</li>
<li>编写测试（静态检查，单元测试）</li>
<li>跨浏览器、跨设备的解决方法（不同分辨率，不同厂商）</li>
<li>会根据项目的特点选择不同的前端技术栈（移动端，Web站点，响应式设计等）</li>
</ul>
<p>在有了基础的HTML/CSS/JS技能之后，你会尝试做的更好：</p>
<ul>
<li>如何更高效的操作DOM</li>
<li>如何将CSS写的更加清晰易懂</li>
<li>如何编写更加易于维护的代码（更有意义的单元测试）</li>
<li>如何组织大型的项目结构，模块化，组件化等等</li>
</ul>
<p>这些要求事实上已经不那么容易做到了。它可能会花费你2到3年时间来完全掌握。但是2到3年之后，即便你已经成为了一个“合格的”前端工程师，这也还远远不够。在现实世界中，一个软件产品除了前端，还有非常广阔的空间，还有很多有趣的东西值得学习：</p>
<ul>
<li>HTTP协议本身（缓存，鉴权）</li>
<li>Web容器/HTTP服务器如何工作</li>
<li>无状态的Web应用的工作原理（如何让网站正确地运行在集群上）</li>
<li>动态，静态内容如何分离部署（反向代理配置）</li>
<li>安全机制如何配置</li>
<li>监控机制如何配置</li>
</ul>
<p>有了这些，也算是有点端到端的意思了。这时你也已经不是一个“纯前端”工程师了，系统中的大部分问题你都可以搞定，不过日常工作中可能更多的职责还是做前端的开发。但是这些还不够，软件除了交付之外，还有一些非功能性的需求：</p>
<ul>
<li>端到端测试（UI测试，比如<a href="http://www.seleniumhq.org/">selenium server</a>/web driver）</li>
<li>devops（比如数据库环境，测试服务器，CI服务器的自动化provision）</li>
<li>基本的UI设计原则（在某些页面确实的情况下，根据系统的已有UI做设计）</li>
<li>数据库性能优化</li>
<li>性能测试</li>
</ul>
<p><del>不过这些还只是我对于Web开发这个领域的总结。其他领域，比如大数据，机器学习，GIS，图像/视频处理等等。</del></p>
<p>这时候，你才能算是一个严格意义上的“前端”工程师。不从系统的角度来思考，不真正做一些后端开发/配置，并不能算是前端工程师，或者可以被称为<em>偏前端</em>工程师（partial frontend developer）。但是即使称为上边这样的“前端工程师”，我想这离一个优秀的工程师还是有<strong>很大差距</strong>的。</p>
<p>我跟一位<a href="http://www.caicaiwan.com/">设计师同事</a>聊过这个问题：</p>
<p><code>Dev眼中的世界是这样的，从墙上（物理的或者电子的）上找到一些卡片（story卡或者需求文档说明书），然后撸袖子开干，干的过程中有很多自以为是的理解，同样有一些自以为是的牛逼实践（TDD啊，自动化啊），最后功能做完，大功告成，然后接着做下一个卡片。传统的Dev，或者苦逼屌丝程序员的世界就是这样的：需求从哪儿来，不知道；做完之后谁来负责质量，不知道；最终上线的时候怎么发布，不知道；线上有问题了怎么办，不知道。</code></p>
<p>以及</p>
<p><code>在ThoughtWorks，Dev的工作有了很大的变化，一个最明显的变化是边界的模糊。比如很多项目都不设QA角色，所有人都对质量负责，都做测试，也有OPs角色，但是大部分非生产环境都是Dev自己发布。也就是说，软件/项目生命周期中的大部分实践我们都能涉足，而且可以带来改进，提升效率。但是这只是往下游（从开发，到测试，到部署，到运维），反过来看上游，比如需求从哪儿来，Dev还是不知道。这毫无疑问是一个令人沮丧的事实，因为这需求的产生才是核心，也就是我昨天跟你聊的：一个idea如何变成一个可视化的原型，然后进一步演进为项目原型？</code></p>
<p>开发工作不应该仅仅局限在编码上，作为开发者/工程师，应该尽可能的多了解一些上下文：比如我们的项目最终是给谁用的，需求从何而来，项目是如何部署在线上的等等。</p>
<p><img loading="lazy" src="/images/2015/06/software-life-cycle-resized.png" type="" alt="software life cycle"  /></p>
<p>简而言之，开发者视野应该放开开阔一些。不要将自己局限在某种角色上，不但不要局限在前端/后端开发上，压根就不要局限在开发这种角色本身上，你在系统中，可以是设计师，还可以是业务分析师。虽然不一定最终要你去转行做BA，或者UX，但是更广阔的视野可以使你更加高效的发挥自己的作用，也可以在和别的角色互动式，快速的了解上下文。</p>
<p>我所理解的，前端不一定要熟知所有这些知识和技能，但是<strong>一定不要认为自己做好了前端的一亩三分地就足够了</strong>，不要给自己设限。跨界会给你带来难以估量的好处，一个角色做久了，难免会产生一些盲点。这时候，换个视角，从其他角色的角度来看待你的工作，又会有很多新的发现。而且不仅如此，很可能你会发现之前很麻烦，很难搞定的事情，在新的方法/视角下变得很容易。</p>
<h3 id="我的故事">我的故事</h3>
<h4 id="其实我是一名后端开发">其实，我是一名后端开发</h4>
<p>工作之后，我在很长一段时间是专注于“非前端”的领域。和很多刚入行的新人一样，我对计算机能触及的几乎一切领域都感兴趣：语言解释器，人工智能（遗传算法，隐式马尔科夫模型，自动纠错，模式识别），嵌入式开发，图形处理，操作系统的进程调度，进程间通信，多线程模型，各种脚本语言（python，ruby，JavaScript等等），另外，日常开发流程中的一些工具的定制化也会花去我很多的时间，比如如何配置vim，写几个小脚本来和编辑器做集成等等。更别说那些令人一听就觉得激动的编程范式：面向对象，基于消息总线，函数式编程等等。如果你感兴趣，可以看看我<a href="http://abruzzi.iteye.com/">几年前的博客</a>。</p>
<p>我的上一家公司的产品是一个省级电网的收费/计费系统（电其实和我们在超市里购买的其他生活用品一样，也是一种商品）。我在那里工作了差不多两年，日常的开发方式就是ssh登陆到RHEL（Redhat Enterprise Linux）服务器上，用vim（当然有一堆的vim插件）开发C代码，调试器是gdb（对，就是那个很牛逼，但是对新手特别不友好的gdb）。</p>
<p>我们用C语言给Apache的httpd写了一个扩展module，大约相当于现在rack里的中间件，这个module要和后端的一个要复杂的多的模块通信，其中不但涉及网络通信，还有*nix管道，缓冲，并发等等考虑。在这两年里，我几乎没有碰过任何的Web界面上的东西（除了用php写了一两百行的页面之外）。</p>
<p>在加入这家公司之前，我在一家用Java做报表的公司工作，技术栈为J2EE。其中有一些前端的工作，但是并不很多，而且说实话，我当时有些看不太上这些技术。HTML/CSS在我心目中的地位比线程池，语言解析等差远了，所以我也没有认真地去系统学习。</p>
<p>在加入ThoughtWorks之前，在“前端”方面，唯一算是比较擅长的也不过是写JavaScript，而且对于前端的MVVM框架，双向绑定，模块化等高级货都没听过。且不能论HTML/CSS的最佳实践，连根据设计稿做出一个静态页面的的能力也不具备。我之前有一点JSP/HTML经验，而CSS经验也并没有超出如何画一个细线表格的范畴。换句话说，我的前端（特别是HTML/CSS）是最近才学会的。</p>
<h4 id="thoughtworks的开发">ThoughtWorks的开发</h4>
<p>在ThoughtWorks，很多团队是按照feature团队来组建的。相对于传统的component团队（按部门划分，比如研发组，测试组，设计组等，每个组还有可能会再细分成如用户调研，流程设计，视觉设计等等），feature团队里配备了软件开发过程中需要的几乎所有角色：业务分析，测试工程师，开发工程师，设计师（设计师一般不会常驻），有的团队还有项目经理的角色。</p>
<p>在feature团队里，你可以很容易看到不同的角色是如何工作的，很多时候，开发会和设计师一起来调整颜色，排版，布局，也可能和测试一起编写自动化测试用例，showcase等。也就是说，角色之间的藩篱在淡化，而就开发这一种角色而言，对于前端/后端的区分也会显得非常模糊，因为需求划分之后的story（敏捷开发中的一个术语，其实就是需求的一种展现方式）是端到端的，比如一个商品列表展示的story，会包括</p>
<ul>
<li>数据库的表结构</li>
<li>访问数据库的<a href="https://en.wikipedia.org/wiki/Object-relational_mapping">ORM</a>部分，</li>
<li>使用ORM的业务逻辑service</li>
<li>响应客户端的controller（消费JSON或者XML的HTTP接口）</li>
<li>发送请求，处理响应的JavaScript代码</li>
<li>和设计稿一致的CSS样式</li>
</ul>
<p>而且在这个过程中还会涉及到一些外围的工具</p>
<ul>
<li>虚拟机环境准备</li>
<li>数据库连接</li>
<li>自动化测试（单元测试，集成测试，可能还有UI测试）</li>
<li>数据库迁移脚本</li>
</ul>
<p>在这个过程中，开发者需要掌握和开发过程相关的一切实践中的一切工具.</p>
<p>在我的<a href="http://www.thoughtworks.com/">ThoughtWorks</a>的第一个项目中，我是以Java开发工程师角色加入的，下项目的时候，我学会了自动化provision，<a href="https://cucumber.io/">cucumber</a>测试工具，Rails，<a href="https://gradle.org/">gradle</a>（没错，我之前用Java都是用IDE构建的，在Linux世界我用make），<a href="http://jasmine.github.io/2.0/introduction.html">jasmine</a>测试工具，<a href="http://backbonejs.org/">Backbone.js</a>，haml.js。</p>
<p>第二个项目的时候，我是以前端工程师角色加入，下项目的时候，我学会了nginx配置缓存、负载均衡服务器，<a href="http://gatling.io/">gatling</a>测试工具，Hadoop/Spark等的集群配置，还有一些和项目相关的<a href="http://icodeit.org/2014/04/intro-map-gis/">GIS（地理信息系统）</a>的技术栈，前后端分离策略等。</p>
<p>第三个项目我是以Java开发工程师角色加入的，下项目的时候，我学会了如何做性能测试，如何建立<a href="http://dashing.io/#setup">一个漂亮的Dashboard</a>（可以用来展现CI等），而且在业余时间系统的学习了CSS3和HTML5，将之前零敲碎打的那些知识串起来，这些总结做了几次内部培训后，还整理成了<a href="https://selfstore.io/products/348">一本电子书</a>。</p>
<p>第四个项目我又变成了一个前端工程师，但是这个项目有意思的地方是跟mobile相关，于是页面性能，体验又变成了一个重点，下项目的时候，我对无状态的Web应用，session的持久化，<a href="https://www.gitbook.com/book/juntao/3-web-designs-in-3-weeks/details">CSS3的动画</a>，用Backbone.js组织多页面的方式等等又有了新的理解。</p>
<p>如果这些经历造成了你觉得我很牛的错觉，那我应该道歉。我觉得自己勉强可以算是个合格的程序员：对学习保持着热情，对解决问题保持着热情，仅此而已。在项目上，如果我发现了问题，我就想办法解决，如果属于知识欠缺，那我就会去学习。我还远远没有到达精通这些技术的地步，但是在工程实践领域，根据<a href="https://en.wikipedia.org/wiki/Pareto_principle">80/20原则</a>，这些粗浅的知识足以解决80%的问题，而另外的20%，我们才真正需要一个专家来帮忙。也就是说，团队里需要有一个能解决20%的问题的前端工程师，而其他的80%的前端工作，应该可以被其他所有的开发完成，对于后端开发也是一样。</p>
<p><del>尝试从系统级别去解决一个问题，而不是将问题抛给另外一个角色（后端工程师，UX或者QA）</del></p>
<p>我是一个Dev，但是花了一些时间来学习界面设计，这里是我从设计到实现的两个小页面：</p>
<p><img loading="lazy" src="/images/2015/06/ilearn-resized.png" type="" alt="ilearn"  /></p>
<p><img loading="lazy" src="/images/2015/06/lightweight-resized.png" type="" alt="lightweight"  /></p>
<h3 id="总结">总结</h3>
<p>我们缺的从来都不是前端/后端工程师，而是<strong>工程师</strong>(或者那些会系统思考，并总是想着解决问题的人)。角色划分在大的机构内可能是有意义的，就像历史上工厂里，工人被分为车工，钳工，木工，电工。但是这种模式在软件开发中未必好用，<strong>具体而微</strong>的小团队可能更具竞争力。而在一个个的小团队中，再细分前端后端就显得比较滑稽了。团队中的每个成员都应该具备基本的端到端能力（不仅仅是开发，更应该是具有业务上下文，即每个人都清楚我们要交付的最终产品是什么，以及这个产品是如何帮助最终用户的），每个成员也都需要为最终的交付物负责，而不是为自己的职责负责。</p>
]]></content:encoded>
    </item>
    
    <item>
      <title>基于underscore和jQuery的微型告警系统</title>
      <link>https://icodeit.org/2015/02/build-sample-application-by-using-underscore-and-jquery/</link>
      <pubDate>Sat, 21 Feb 2015 00:00:00 +0000</pubDate>
      
      <guid>https://icodeit.org/2015/02/build-sample-application-by-using-underscore-and-jquery/</guid>
      <description>一个监控系统 我们今天要使用underscore.js和jQuery来构建一个客户端的应用，这个应用是一个监控系统的前端，设计师已经给出了界面设计：
而对应的服务器端的API也已经就绪了：
$ curl http://localhost:9527/alarms.json -s | jq . 会看到诸如这样的返回值：
[ { &amp;#34;proiority&amp;#34;: &amp;#34;critical&amp;#34;, &amp;#34;occurrence&amp;#34;: &amp;#34;2/12/2015 01:23 AM&amp;#34;, &amp;#34;summary&amp;#34;: &amp;#34;heartbeat failure&amp;#34;, &amp;#34;node&amp;#34;: &amp;#34;VIQ002&amp;#34; }, { &amp;#34;proiority&amp;#34;: &amp;#34;major&amp;#34;, &amp;#34;occurrence&amp;#34;: &amp;#34;2/12/2015 01:22 AM&amp;#34;, &amp;#34;summary&amp;#34;: &amp;#34;packages are rejected&amp;#34;, &amp;#34;node&amp;#34;: &amp;#34;VIQ002&amp;#34; }, { &amp;#34;proiority&amp;#34;: &amp;#34;medium&amp;#34;, &amp;#34;occurrence&amp;#34;: &amp;#34;2/11/2015 01:23 AM&amp;#34;, &amp;#34;summary&amp;#34;: &amp;#34;connection cannot be established&amp;#34;, &amp;#34;node&amp;#34;: &amp;#34;VIQ002&amp;#34; } ] 每条告警信息都包含：优先级，发生时间，描述信息，以及发生告警的节点名称。
我们要将这些信息整合，并展示在页面上。
mockup 我在《3周3页面》中讨论过现代前端开发的方式，你也可以参考这篇文章以及这篇文章。我们这里还是采用相同的方式来实现这个mockup，也就是静态页面。
首先我们在index.html中编写HTML：
&amp;lt;div class=&amp;#34;container&amp;#34;&amp;gt; &amp;lt;h1&amp;gt;Active Event List in transmission&amp;lt;/h1&amp;gt; &amp;lt;ul class=&amp;#34;events&amp;#34;&amp;gt; &amp;lt;li&amp;gt; &amp;lt;div class=&amp;#34;event critical&amp;#34;&amp;gt; &amp;lt;h3&amp;gt;heartbeat failure @ VIQ002&amp;lt;/h3&amp;gt; &amp;lt;span class=&amp;#34;date&amp;#34;&amp;gt;2/12/2015 01:23 AM&amp;lt;/span&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;/li&amp;gt; &amp;lt;li&amp;gt; &amp;lt;div class=&amp;#34;event major&amp;#34;&amp;gt; &amp;lt;h3&amp;gt;packages are rejected&amp;lt;/h3&amp;gt; &amp;lt;span class=&amp;#34;date&amp;#34;&amp;gt;2/12/2015 01:23 AM&amp;lt;/span&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;/li&amp;gt; &amp;lt;li&amp;gt; &amp;lt;div class=&amp;#34;event medium&amp;#34;&amp;gt; &amp;lt;h3&amp;gt;connection cannot be established&amp;lt;/h3&amp;gt; &amp;lt;span class=&amp;#34;date&amp;#34;&amp;gt;2/12/2015 01:23 AM&amp;lt;/span&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;/li&amp;gt; &amp;lt;!</description>
      <content:encoded><![CDATA[<h2 id="一个监控系统">一个监控系统</h2>
<p>我们今天要使用<code>underscore.js</code>和<code>jQuery</code>来构建一个客户端的应用，这个应用是一个监控系统的前端，设计师已经给出了界面设计：</p>
<p><img loading="lazy" src="/images/2015/02/alarms-design-resized.png" type="" alt="alarms design"  /></p>
<p>而对应的服务器端的API也已经就绪了：</p>
<div class="highlight"><pre class="chroma"><code class="language-sh" data-lang="sh">$ curl http://localhost:9527/alarms.json -s <span class="p">|</span> jq .
</code></pre></div><p>会看到诸如这样的返回值：</p>
<div class="highlight"><pre class="chroma"><code class="language-json" data-lang="json"><span class="p">[</span>
  <span class="p">{</span>
    <span class="nt">&#34;proiority&#34;</span><span class="p">:</span> <span class="s2">&#34;critical&#34;</span><span class="p">,</span>
    <span class="nt">&#34;occurrence&#34;</span><span class="p">:</span> <span class="s2">&#34;2/12/2015 01:23 AM&#34;</span><span class="p">,</span>
    <span class="nt">&#34;summary&#34;</span><span class="p">:</span> <span class="s2">&#34;heartbeat failure&#34;</span><span class="p">,</span>
    <span class="nt">&#34;node&#34;</span><span class="p">:</span> <span class="s2">&#34;VIQ002&#34;</span>
  <span class="p">},</span>
  <span class="p">{</span>
    <span class="nt">&#34;proiority&#34;</span><span class="p">:</span> <span class="s2">&#34;major&#34;</span><span class="p">,</span>
    <span class="nt">&#34;occurrence&#34;</span><span class="p">:</span> <span class="s2">&#34;2/12/2015 01:22 AM&#34;</span><span class="p">,</span>
    <span class="nt">&#34;summary&#34;</span><span class="p">:</span> <span class="s2">&#34;packages are rejected&#34;</span><span class="p">,</span>
    <span class="nt">&#34;node&#34;</span><span class="p">:</span> <span class="s2">&#34;VIQ002&#34;</span>
  <span class="p">},</span>
  <span class="p">{</span>
    <span class="nt">&#34;proiority&#34;</span><span class="p">:</span> <span class="s2">&#34;medium&#34;</span><span class="p">,</span>
    <span class="nt">&#34;occurrence&#34;</span><span class="p">:</span> <span class="s2">&#34;2/11/2015 01:23 AM&#34;</span><span class="p">,</span>
    <span class="nt">&#34;summary&#34;</span><span class="p">:</span> <span class="s2">&#34;connection cannot be established&#34;</span><span class="p">,</span>
    <span class="nt">&#34;node&#34;</span><span class="p">:</span> <span class="s2">&#34;VIQ002&#34;</span>
  <span class="p">}</span>
<span class="p">]</span>

</code></pre></div><p>每条告警信息都包含：优先级，发生时间，描述信息，以及发生告警的节点名称。</p>
<p>我们要将这些信息整合，并展示在页面上。</p>
<h3 id="mockup">mockup</h3>
<p>我在<a href="juntao.gitbooks.io/3-web-designs-in-3-weeks/">《3周3页面》</a>中讨论过现代前端开发的方式，你也可以参考<a href="http://icodeit.org/2014/11/modern-ui-development-workflow/">这篇文章</a>以及<a href="http://">这篇文章</a>。我们这里还是采用相同的方式来实现这个<code>mockup</code>，也就是静态页面。</p>
<p>首先我们在<code>index.html</code>中编写HTML：</p>
<div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;container&#34;</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">h1</span><span class="p">&gt;</span>Active Event List in transmission<span class="p">&lt;/</span><span class="nt">h1</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">ul</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;events&#34;</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">li</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;event critical&#34;</span><span class="p">&gt;</span>
                <span class="p">&lt;</span><span class="nt">h3</span><span class="p">&gt;</span>heartbeat failure @ VIQ002<span class="p">&lt;/</span><span class="nt">h3</span><span class="p">&gt;</span>
                <span class="p">&lt;</span><span class="nt">span</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;date&#34;</span><span class="p">&gt;</span>2/12/2015 01:23 AM<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span>
            <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
        <span class="p">&lt;/</span><span class="nt">li</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">li</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;event major&#34;</span><span class="p">&gt;</span>
                <span class="p">&lt;</span><span class="nt">h3</span><span class="p">&gt;</span>packages are rejected<span class="p">&lt;/</span><span class="nt">h3</span><span class="p">&gt;</span>
                <span class="p">&lt;</span><span class="nt">span</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;date&#34;</span><span class="p">&gt;</span>2/12/2015 01:23 AM<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span>
            <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
        <span class="p">&lt;/</span><span class="nt">li</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">li</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;event medium&#34;</span><span class="p">&gt;</span>
                <span class="p">&lt;</span><span class="nt">h3</span><span class="p">&gt;</span>connection cannot be established<span class="p">&lt;/</span><span class="nt">h3</span><span class="p">&gt;</span>
                <span class="p">&lt;</span><span class="nt">span</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;date&#34;</span><span class="p">&gt;</span>2/12/2015 01:23 AM<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span>
            <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
        <span class="p">&lt;/</span><span class="nt">li</span><span class="p">&gt;</span>
        <span class="c">&lt;!-- ... --&gt;</span>
    <span class="p">&lt;/</span><span class="nt">ul</span><span class="p">&gt;</span>

    <span class="p">&lt;</span><span class="nt">ul</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;legend&#34;</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">li</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;count critical&#34;</span><span class="p">&gt;</span>
                <span class="p">&lt;</span><span class="nt">i</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;alarm&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">i</span><span class="p">&gt;</span>
                <span class="p">&lt;</span><span class="nt">span</span><span class="p">&gt;</span>critical: 20<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span>
            <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
        <span class="p">&lt;/</span><span class="nt">li</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">li</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;count major&#34;</span><span class="p">&gt;</span>
                <span class="p">&lt;</span><span class="nt">i</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;alarm&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">i</span><span class="p">&gt;</span>
                <span class="p">&lt;</span><span class="nt">span</span><span class="p">&gt;</span>major: 13<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span>
            <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
        <span class="p">&lt;/</span><span class="nt">li</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">li</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;count medium&#34;</span><span class="p">&gt;</span>
                <span class="p">&lt;</span><span class="nt">i</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;alarm&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">i</span><span class="p">&gt;</span>
                <span class="p">&lt;</span><span class="nt">span</span><span class="p">&gt;</span>medium: 20<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span>
            <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
        <span class="p">&lt;/</span><span class="nt">li</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">li</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;count warning&#34;</span><span class="p">&gt;</span>
                <span class="p">&lt;</span><span class="nt">i</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;alarm&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">i</span><span class="p">&gt;</span>
                <span class="p">&lt;</span><span class="nt">span</span><span class="p">&gt;</span>warning: 30<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span>
            <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
        <span class="p">&lt;/</span><span class="nt">li</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">li</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;count indeterminate&#34;</span><span class="p">&gt;</span>
                <span class="p">&lt;</span><span class="nt">i</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;alarm&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">i</span><span class="p">&gt;</span>
                <span class="p">&lt;</span><span class="nt">span</span><span class="p">&gt;</span>indeterminate: 6<span class="p">&lt;/</span><span class="nt">span</span><span class="p">&gt;</span>
            <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
        <span class="p">&lt;/</span><span class="nt">li</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">ul</span><span class="p">&gt;</span>
<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>

</code></pre></div><p><code>events</code>中包含了所有告警信息，每一条告警根据等级不同显示了不同的颜色：红色表示严重，橘红表示主要，橘色表示一般等。告警信息包含了一个描述信息，并且包含了一个时间戳，表示告警发生的时间。</p>
<p><code>legend</code>部分是一个图例，其中包含了各个颜色的说明，并且包含了不同级别的告警信息的数目，比如截止目前为止，共有<strong>20</strong>个严重的告警，13个主要告警等。</p>
<p>对应的<code>scss</code>内容如下，首先定义了一些通用的CSS类：</p>
<div class="highlight"><pre class="chroma"><code class="language-scss" data-lang="scss"><span class="k">@import</span> <span class="s2">&#34;compass/reset&#34;</span><span class="p">;</span>
<span class="k">@import</span> <span class="s2">&#34;compass/css3&#34;</span><span class="p">;</span>

<span class="nt">body</span> <span class="p">{</span>
	<span class="na">font-size</span><span class="o">:</span> <span class="mi">62</span><span class="mf">.5</span><span class="kt">%</span><span class="p">;</span>
	<span class="na">font-family</span><span class="o">:</span> <span class="s2">&#34;Open Sans&#34;</span><span class="o">,</span> <span class="ni">serif</span><span class="p">;</span>
	<span class="na">text-align</span><span class="o">:</span> <span class="ni">center</span><span class="p">;</span>
<span class="p">}</span>

<span class="nc">.critical</span> <span class="p">{</span>
	<span class="na">background-color</span><span class="o">:</span> <span class="no">red</span><span class="p">;</span>
	<span class="na">color</span><span class="o">:</span> <span class="ni">white</span><span class="p">;</span>
<span class="p">}</span>

<span class="nc">.major</span> <span class="p">{</span>
	<span class="na">background-color</span><span class="o">:</span> <span class="no">orangered</span><span class="p">;</span>
	<span class="na">color</span><span class="o">:</span> <span class="ni">white</span><span class="p">;</span>
<span class="p">}</span>

<span class="nc">.medium</span> <span class="p">{</span>
	<span class="na">background-color</span><span class="o">:</span> <span class="no">orange</span><span class="p">;</span>
	<span class="na">color</span><span class="o">:</span> <span class="ni">white</span><span class="p">;</span>
<span class="p">}</span>

<span class="nc">.warning</span> <span class="p">{</span>
	<span class="na">background-color</span><span class="o">:</span> <span class="mh">#2C75DB</span><span class="p">;</span>
	<span class="na">color</span><span class="o">:</span> <span class="ni">white</span><span class="p">;</span>
<span class="p">}</span>

<span class="nc">.indeterminate</span> <span class="p">{</span>
	<span class="na">background-color</span><span class="o">:</span> <span class="mh">#29D4BA</span><span class="p">;</span>
	<span class="na">color</span><span class="o">:</span> <span class="ni">white</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div><p>然后定义<code>container</code>中的各个元素的样式：</p>
<div class="highlight"><pre class="chroma"><code class="language-scss" data-lang="scss"><span class="nc">.container</span> <span class="p">{</span>
	<span class="na">padding</span><span class="o">:</span> <span class="mi">1</span><span class="kt">em</span> <span class="mi">0</span><span class="p">;</span>
	<span class="na">width</span><span class="o">:</span> <span class="mi">800</span><span class="kt">px</span><span class="p">;</span>
	<span class="na">margin</span><span class="o">:</span> <span class="mi">0</span> <span class="ni">auto</span><span class="p">;</span>

	<span class="nt">h1</span> <span class="p">{</span>
		<span class="na">font-size</span><span class="o">:</span> <span class="mi">3</span><span class="kt">em</span><span class="p">;</span>
		<span class="na">text-transform</span><span class="o">:</span> <span class="ni">uppercase</span><span class="p">;</span>
		<span class="na">margin</span><span class="o">:</span> <span class="mi">1</span><span class="kt">em</span> <span class="mi">0</span><span class="p">;</span>
	<span class="p">}</span>

	<span class="nc">.event</span> <span class="p">{</span>
		<span class="na">position</span><span class="o">:</span> <span class="ni">relative</span><span class="p">;</span>

		<span class="nt">h3</span> <span class="p">{</span>
			<span class="na">text-align</span><span class="o">:</span> <span class="ni">left</span><span class="p">;</span>
			<span class="na">font-size</span><span class="o">:</span> <span class="mi">1</span><span class="mf">.5</span><span class="kt">em</span><span class="p">;</span>
			<span class="na">padding</span><span class="o">:</span> <span class="mf">.6</span><span class="kt">em</span> <span class="mf">.5</span><span class="kt">em</span><span class="p">;</span>
			<span class="na">text-transform</span><span class="o">:</span> <span class="ni">capitalize</span><span class="p">;</span>
		<span class="p">}</span>

		<span class="nc">.date</span> <span class="p">{</span>
			<span class="na">position</span><span class="o">:</span> <span class="ni">absolute</span><span class="p">;</span>
			<span class="na">top</span><span class="o">:</span> <span class="mi">50</span><span class="kt">%</span><span class="p">;</span>
			<span class="na">right</span><span class="o">:</span> <span class="mi">1</span><span class="kt">em</span><span class="p">;</span>
			<span class="na">font-size</span><span class="o">:</span> <span class="mi">1</span><span class="kt">em</span><span class="p">;</span>
			<span class="na">font-style</span><span class="o">:</span> <span class="ni">italic</span><span class="p">;</span>
			<span class="na">color</span><span class="o">:</span> <span class="mh">#eeeeee</span><span class="p">;</span>
		<span class="p">}</span>
	<span class="p">}</span>

	<span class="nc">.legend</span> <span class="p">{</span>
		<span class="na">margin</span><span class="o">:</span> <span class="mi">1</span><span class="kt">em</span> <span class="mi">0</span><span class="p">;</span>
		<span class="nt">li</span> <span class="p">{</span>
			<span class="na">width</span><span class="o">:</span> <span class="mi">20</span><span class="kt">%</span><span class="p">;</span>
			<span class="na">float</span><span class="o">:</span> <span class="ni">left</span><span class="p">;</span>

			<span class="nc">.count</span> <span class="p">{</span>
				<span class="na">padding</span><span class="o">:</span> <span class="mf">.6</span><span class="kt">em</span><span class="p">;</span>
				<span class="na">text-transform</span><span class="o">:</span> <span class="ni">capitalize</span><span class="p">;</span>
			<span class="p">}</span>
		<span class="p">}</span>
	<span class="p">}</span>
<span class="p">}</span>
</code></pre></div><h3 id="应用程序">应用程序</h3>
<p>首先我们下载<code>underscore.js</code>和<code>jquery.js</code>，并将它们放在当前目录下的<code>scripts/libs</code>下，并在<code>scripts</code>下创建一个<code>app.js</code>文件。</p>
<p>这时候的目录结构如下：</p>
<pre><code>├── index.html
├── sass
│   └── style.scss
├── scripts
│   ├── app.js
│   └── libs
│       ├── jquery.min.js
│       └── underscore.js
└── stylesheets
    └── style.css
</code></pre><p>然后我们在<code>index.html</code>中引入上列的文件：</p>
<div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html"><span class="p">&lt;</span><span class="nt">script</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;scripts/libs/jquery.min.js&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">script</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;scripts/libs/underscore.js&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
<span class="p">&lt;</span><span class="nt">script</span> <span class="na">src</span><span class="o">=</span><span class="s">&#34;scripts/app.js&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
</code></pre></div><p>我们的应用程序需要做的事情很简单：</p>
<ol>
<li>请求服务器获得数据</li>
<li>处理数据（如果需要的话）</li>
<li>将加工过的数据与模板结合，渲染在页面上</li>
</ol>
<p><code>underscore.js</code>中有一个用来处理模板的函数，叫做<code>template</code>：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="kd">var</span> <span class="nx">compiled</span> <span class="o">=</span> <span class="mi">_</span><span class="p">.</span><span class="nx">template</span><span class="p">(</span><span class="s2">&#34;&lt;h1&gt;&lt;%= title %&gt;&lt;/h1&gt;&#34;</span><span class="p">);</span>
<span class="nx">compiled</span><span class="p">({</span><span class="s2">&#34;title&#34;</span><span class="o">:</span> <span class="s2">&#34;Heartbeat Failure @ VIQ002&#34;</span><span class="p">});</span>

<span class="c1">//&lt;h1&gt;Heartbeat Failure @ VIQ002&lt;/h1&gt;
</span></code></pre></div><p>注意上式中的<code>&lt;%= variable %&gt;</code>，这个表达式表示打印<code>variable</code>的值。而如果只是执行JavaScript代码，表达式则为<code>&lt;% expression; %&gt;</code>。</p>
<p>比如我们可以使用<code>for</code>循环：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="kd">var</span> <span class="nx">template</span> <span class="o">=</span>
	<span class="s2">&#34;&lt;% _.each(alarms, function(alarm){ %&gt;&#34;</span> <span class="o">+</span>
		<span class="s2">&#34;&lt;h3&gt;&lt;%= alarm.summary %&gt;&lt;/h3&gt;&#34;</span> <span class="o">+</span>
	<span class="s2">&#34;&lt;% }); %&gt;&#34;</span><span class="p">;</span>

<span class="kd">var</span> <span class="nx">compilded</span> <span class="o">=</span> <span class="mi">_</span><span class="p">.</span><span class="nx">template</span><span class="p">(</span><span class="nx">template</span><span class="p">);</span>
<span class="nx">compilded</span><span class="p">({</span><span class="s2">&#34;alarms&#34;</span><span class="o">:</span> <span class="p">[</span>
  <span class="p">{</span>
    <span class="s2">&#34;proiority&#34;</span><span class="o">:</span> <span class="s2">&#34;critical&#34;</span><span class="p">,</span>
    <span class="s2">&#34;occurrence&#34;</span><span class="o">:</span> <span class="s2">&#34;2/12/2015 01:23 AM&#34;</span><span class="p">,</span>
    <span class="s2">&#34;summary&#34;</span><span class="o">:</span> <span class="s2">&#34;heartbeat failure&#34;</span><span class="p">,</span>
    <span class="s2">&#34;node&#34;</span><span class="o">:</span> <span class="s2">&#34;VIQ002&#34;</span>
  <span class="p">},</span>
  <span class="p">{</span>
    <span class="s2">&#34;proiority&#34;</span><span class="o">:</span> <span class="s2">&#34;major&#34;</span><span class="p">,</span>
    <span class="s2">&#34;occurrence&#34;</span><span class="o">:</span> <span class="s2">&#34;2/12/2015 01:22 AM&#34;</span><span class="p">,</span>
    <span class="s2">&#34;summary&#34;</span><span class="o">:</span> <span class="s2">&#34;packages are rejected&#34;</span><span class="p">,</span>
    <span class="s2">&#34;node&#34;</span><span class="o">:</span> <span class="s2">&#34;VIQ002&#34;</span>
  <span class="p">},</span>
  <span class="p">{</span>
    <span class="s2">&#34;proiority&#34;</span><span class="o">:</span> <span class="s2">&#34;medium&#34;</span><span class="p">,</span>
    <span class="s2">&#34;occurrence&#34;</span><span class="o">:</span> <span class="s2">&#34;2/11/2015 01:23 AM&#34;</span><span class="p">,</span>
    <span class="s2">&#34;summary&#34;</span><span class="o">:</span> <span class="s2">&#34;connection cannot be established&#34;</span><span class="p">,</span>
    <span class="s2">&#34;node&#34;</span><span class="o">:</span> <span class="s2">&#34;VIQ002&#34;</span>
  <span class="p">}</span>
<span class="p">]});</span>

<span class="c1">//&lt;h3&gt;heartbeat failure&lt;/h3&gt;&lt;h3&gt;packages are rejected&lt;/h3&gt;&lt;h3&gt;connection cannot be established&lt;/h3&gt;
</span></code></pre></div><p>注意上边代码中的<code>_.each</code>语句。</p>
<h3 id="实现">实现</h3>
<p>首先我们在页面上定义一个模板，定义在id为<code>events</code>的<code>script</code>标签中，注意这个script的type为<code>template</code>，这样既可以避免浏览器解释它，又可以为我们临时保存一段文本。</p>
<div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html"><span class="p">&lt;</span><span class="nt">script</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;template&#34;</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;events&#34;</span><span class="p">&gt;</span>
    <span class="o">&lt;%</span> <span class="mi">_</span><span class="p">.</span><span class="nx">each</span><span class="p">(</span><span class="nx">alarms</span><span class="p">,</span> <span class="kd">function</span><span class="p">(</span><span class="nx">alarm</span><span class="p">)</span> <span class="p">{</span> <span class="o">%&gt;</span>
        <span class="o">&lt;</span><span class="nx">li</span><span class="o">&gt;</span>
            <span class="o">&lt;</span><span class="nx">div</span> <span class="kr">class</span><span class="o">=</span><span class="s2">&#34;event &lt;%= alarm.proiority%&gt;&#34;</span><span class="o">&gt;</span>
                <span class="o">&lt;</span><span class="nx">h3</span><span class="o">&gt;&lt;%=</span> <span class="nx">alarm</span><span class="p">.</span><span class="nx">summary</span><span class="o">%&gt;</span> <span class="err">@</span> <span class="o">&lt;%=</span> <span class="nx">alarm</span><span class="p">.</span><span class="nx">node</span> <span class="o">%&gt;&lt;</span><span class="err">/h3&gt;</span>
                <span class="o">&lt;</span><span class="nx">span</span> <span class="kr">class</span><span class="o">=</span><span class="s2">&#34;date&#34;</span><span class="o">&gt;&lt;%=</span> <span class="nx">alarm</span><span class="p">.</span><span class="nx">occurrence</span> <span class="o">%&gt;&lt;</span><span class="err">/span&gt;</span>
            <span class="o">&lt;</span><span class="err">/div&gt;</span>
        <span class="o">&lt;</span><span class="err">/li&gt;</span>
    <span class="o">&lt;%</span> <span class="p">});</span> <span class="o">%&gt;</span>
<span class="p">&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
</code></pre></div><p>然后在<code>app.js</code>中只需要请求<code>alarms.json</code>，然后编译模板，并绑定数据：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="nx">$</span><span class="p">(</span><span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
	<span class="nx">$</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s2">&#34;/alarms.json&#34;</span><span class="p">).</span><span class="nx">done</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">alarms</span><span class="p">)</span> <span class="p">{</span>
		<span class="kd">var</span> <span class="nx">compiled</span> <span class="o">=</span> <span class="mi">_</span><span class="p">.</span><span class="nx">template</span><span class="p">(</span><span class="nx">$</span><span class="p">(</span><span class="s2">&#34;#events&#34;</span><span class="p">).</span><span class="nx">html</span><span class="p">());</span>
		<span class="kd">var</span> <span class="nx">html</span> <span class="o">=</span> <span class="nx">compiled</span><span class="p">({</span><span class="s2">&#34;alarms&#34;</span><span class="o">:</span> <span class="nx">alarms</span><span class="p">});</span>

		<span class="nx">$</span><span class="p">(</span><span class="s2">&#34;.events&#34;</span><span class="p">).</span><span class="nx">html</span><span class="p">(</span><span class="nx">html</span><span class="p">);</span>
	<span class="p">});</span>
<span class="p">});</span>
</code></pre></div><p>刷新页面，就可以看到来自于后台的实际数据了（当然，我们这里使用了一个静态的<code>alarms.json</code>来模拟后台的API）：</p>
<p><img loading="lazy" src="/images/2015/02/fetch-data-resized.png" type="" alt="fetch data from server"  /></p>
<h4 id="处理数据">处理数据</h4>
<p>你可能已经看到了，由于后台数据采集的问题，前端请求到的数据不一定每次都是按照日期排好序的，因此我们需要在拿到数据之后先排序在展示。好在我们之前已经学习了如何做到这一点：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="nx">$</span><span class="p">(</span><span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
	<span class="nx">$</span><span class="p">.</span><span class="nx">get</span><span class="p">(</span><span class="s2">&#34;/alarms.json&#34;</span><span class="p">).</span><span class="nx">done</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">alarms</span><span class="p">)</span> <span class="p">{</span>
		<span class="kd">var</span> <span class="nx">eventCompiled</span> <span class="o">=</span> <span class="mi">_</span><span class="p">.</span><span class="nx">template</span><span class="p">(</span><span class="nx">$</span><span class="p">(</span><span class="s2">&#34;#events&#34;</span><span class="p">).</span><span class="nx">html</span><span class="p">());</span>

		<span class="kd">var</span> <span class="nx">events</span> <span class="o">=</span> <span class="mi">_</span><span class="p">(</span><span class="nx">alarms</span><span class="p">).</span><span class="nx">sortBy</span><span class="p">(</span><span class="s2">&#34;occurrence&#34;</span><span class="p">).</span><span class="nx">reverse</span><span class="p">();</span>
		<span class="kd">var</span> <span class="nx">eventHTML</span> <span class="o">=</span> <span class="nx">eventCompiled</span><span class="p">({</span><span class="s2">&#34;alarms&#34;</span><span class="o">:</span> <span class="nx">events</span><span class="p">});</span>

		<span class="nx">$</span><span class="p">(</span><span class="s2">&#34;.events&#34;</span><span class="p">).</span><span class="nx">html</span><span class="p">(</span><span class="nx">eventHTML</span><span class="p">);</span>
	<span class="p">});</span>
<span class="p">});</span>
</code></pre></div><p>剩下的就是页面最底部的图例部分了，这部分会统计各种类型告警的合计信息，这些信息需要进一步的汇总。首先我们将<code>legend</code>封装成模板：</p>
<div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html"><span class="p">&lt;</span><span class="nt">script</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;template&#34;</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;legend&#34;</span><span class="p">&gt;</span>
    <span class="o">&lt;%</span> <span class="mi">_</span><span class="p">.</span><span class="nx">each</span><span class="p">(</span><span class="nx">legends</span><span class="p">,</span> <span class="kd">function</span><span class="p">(</span><span class="nx">legend</span><span class="p">)</span> <span class="p">{</span> <span class="o">%&gt;</span>
        <span class="o">&lt;</span><span class="nx">li</span><span class="o">&gt;</span>
            <span class="o">&lt;</span><span class="nx">div</span> <span class="kr">class</span><span class="o">=</span><span class="s2">&#34;count &lt;%= legend.proiority %&gt;&#34;</span><span class="o">&gt;</span>
                <span class="o">&lt;</span><span class="nx">i</span> <span class="kr">class</span><span class="o">=</span><span class="s2">&#34;alarm&#34;</span><span class="o">&gt;&lt;</span><span class="err">/i&gt;</span>
                <span class="o">&lt;</span><span class="nx">span</span><span class="o">&gt;&lt;%=</span> <span class="nx">legend</span><span class="p">.</span><span class="nx">proiority</span> <span class="o">%&gt;:</span> <span class="o">&lt;%=</span> <span class="nx">legend</span><span class="p">.</span><span class="nx">count</span> <span class="o">%&gt;&lt;</span><span class="err">/span&gt;</span>
            <span class="o">&lt;</span><span class="err">/div&gt;</span>
        <span class="o">&lt;</span><span class="err">/li&gt;</span>
    <span class="o">&lt;%</span> <span class="p">});</span> <span class="o">%&gt;</span>
<span class="p">&lt;/</span><span class="nt">script</span><span class="p">&gt;</span>
</code></pre></div><p>也即，我们需要这样一个结果集：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js">
<span class="p">[</span>
	<span class="p">{</span>
		<span class="s2">&#34;proiority&#34;</span><span class="o">:</span> <span class="s2">&#34;critical&#34;</span><span class="p">,</span>
		<span class="s2">&#34;count&#34;</span><span class="o">:</span> <span class="mi">3</span>
	<span class="p">},</span>
	<span class="p">{</span>
		<span class="s2">&#34;proiority&#34;</span><span class="o">:</span> <span class="s2">&#34;major&#34;</span><span class="p">,</span>
		<span class="s2">&#34;count&#34;</span><span class="o">:</span> <span class="mi">2</span>
	<span class="p">}</span>
<span class="p">]</span>
</code></pre></div><p>不过使用<code>underscore.js</code>，我们可以很容易的得到这个格式的数据：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="kd">var</span> <span class="nx">legends</span> <span class="o">=</span> <span class="mi">_</span><span class="p">.</span><span class="nx">chain</span><span class="p">(</span><span class="nx">alarms</span><span class="p">)</span>
	<span class="p">.</span><span class="nx">groupBy</span><span class="p">(</span><span class="s2">&#34;proiority&#34;</span><span class="p">)</span>
    <span class="p">.</span><span class="nx">map</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">value</span><span class="p">,</span> <span class="nx">key</span><span class="p">)</span> <span class="p">{</span>
    	<span class="k">return</span> <span class="p">{</span><span class="nx">proiority</span><span class="o">:</span> <span class="nx">key</span><span class="p">,</span> <span class="nx">count</span><span class="o">:</span> <span class="nx">value</span><span class="p">.</span><span class="nx">length</span><span class="p">};</span>
	<span class="p">}).</span><span class="nx">value</span><span class="p">();</span>
</code></pre></div><p>其中，<code>groupBy</code>是一个新的API，和<code>SQL</code>中的<code>group by</code>子句一样，它可以将符合条件的项目合并为不同的组：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="kd">var</span> <span class="nx">contacts</span> <span class="o">=</span> <span class="p">[</span>
  <span class="p">{</span>
      <span class="s2">&#34;name&#34;</span><span class="o">:</span> <span class="s2">&#34;Juntao&#34;</span><span class="p">,</span>
      <span class="s2">&#34;age&#34;</span><span class="o">:</span> <span class="mi">29</span>
  <span class="p">},</span>
  <span class="p">{</span>
      <span class="s2">&#34;name&#34;</span><span class="o">:</span> <span class="s2">&#34;Abruzzi&#34;</span><span class="p">,</span>
      <span class="s2">&#34;age&#34;</span><span class="o">:</span> <span class="mi">30</span>
  <span class="p">},</span>
  <span class="p">{</span>
      <span class="s2">&#34;name&#34;</span><span class="o">:</span> <span class="s2">&#34;Sara&#34;</span><span class="p">,</span>
      <span class="s2">&#34;age&#34;</span><span class="o">:</span> <span class="mi">29</span>
  <span class="p">}</span>
<span class="p">];</span>

<span class="kd">var</span> <span class="nx">result</span> <span class="o">=</span> <span class="mi">_</span><span class="p">(</span><span class="nx">contacts</span><span class="p">).</span><span class="nx">groupBy</span><span class="p">(</span><span class="s2">&#34;age&#34;</span><span class="p">);</span>
</code></pre></div><p>会得到这样的结果：</p>
<div class="highlight"><pre class="chroma"><code class="language-json" data-lang="json"><span class="p">{</span>
    <span class="nt">&#34;29&#34;</span><span class="p">:</span> <span class="p">[</span>
        <span class="p">{</span>
            <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;Juntao&#34;</span><span class="p">,</span>
            <span class="nt">&#34;age&#34;</span><span class="p">:</span> <span class="mi">29</span>
        <span class="p">},</span>
        <span class="p">{</span>
            <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;Sara&#34;</span><span class="p">,</span>
            <span class="nt">&#34;age&#34;</span><span class="p">:</span> <span class="mi">29</span>
        <span class="p">}</span>
    <span class="p">],</span>
    <span class="nt">&#34;30&#34;</span><span class="p">:</span> <span class="p">[</span>
        <span class="p">{</span>
            <span class="nt">&#34;name&#34;</span><span class="p">:</span> <span class="s2">&#34;Abruzzi&#34;</span><span class="p">,</span>
            <span class="nt">&#34;age&#34;</span><span class="p">:</span> <span class="mi">30</span>
        <span class="p">}</span>
    <span class="p">]</span>
<span class="p">}</span>
</code></pre></div><p>因此我们通过上面的计算就可以得到需要的结果了：</p>
<p><img loading="lazy" src="/images/2015/02/with-legend-resized.png" type="" alt="with legend"  /></p>
]]></content:encoded>
    </item>
    
    <item>
      <title>现代Web页面开发流程</title>
      <link>https://icodeit.org/2014/11/modern-ui-development-workflow/</link>
      <pubDate>Tue, 25 Nov 2014 00:00:00 +0000</pubDate>
      
      <guid>https://icodeit.org/2014/11/modern-ui-development-workflow/</guid>
      <description>现代Web页面开发流程 通常来说，Web页面开发的流程大致是这样的：设计师（设计师不是美工，就像程序员不是码农一样）提供设计稿，通常是图片格式。然后前端的开发人员（在ThoughtWorks我们称之为UI Dev）来手工的将图片转换为对应的HTML+CSS，往往还需要在各个浏览器中调试等。
大多数时候，设计师会提供色卡，或者至少前景色/背景色/高亮色的值给开发人员。如果没有的话，开发人员会用到一些工具如colorpicker, ruler之类来确保最终的效果和设计稿是一致的。
如果你观察过UI Dev的工作流程的话，你会发现基本的上是这样的：使用编辑器（或者IDE）编写HTML代码，CSS代码，保存修改内容，切换到浏览器窗口，按F5或者Ctrl-R刷新，然后对比设计稿和实现，如果发现不一致的地方，再切换到编辑器中修改代码，如是往复。
避免手工劳动 纯手工的方式来编辑HTML/CSS会非常耗时，特别是作为标记语言的HTML，开发者需要时刻关注关闭已经打开的标签。比如一个标题元素，你需要：
&amp;lt;h1&amp;gt;This is the page title&amp;lt;/h1&amp;gt; 几乎从一开始，人们就想到了各种办法来避免自己重复的键入，比如Vim的SuperTab以及Snipmate插件，可以通过输入标签名+Tab来补全所有的标签等，又或者DreamWaver提供的代码生成的方式来简化这一流程。
Sublime的编辑器上的著名插件Emmet可以帮助开发人员飞速的开发HTML/CSS，这里有一个小例子。假设我们需要实现的页面是这样的：
那么对应的HTML结构可能会是：
&amp;lt;ul&amp;gt; &amp;lt;li&amp;gt; &amp;lt;div class=&amp;#34;feature&amp;#34;&amp;gt; &amp;lt;span class=&amp;#34;number&amp;#34;&amp;gt;&amp;lt;/span&amp;gt; &amp;lt;i&amp;gt;&amp;lt;/i&amp;gt; &amp;lt;h4&amp;gt;&amp;lt;/h4&amp;gt; &amp;lt;p&amp;gt;&amp;lt;/p&amp;gt; &amp;lt;/div&amp;gt; &amp;lt;/li&amp;gt; ... &amp;lt;/ul&amp;gt; 使用Emmet，则只需要给出表达式，然后按一下Tab键就可以补全为上述的结构了：
ul&amp;gt;li*3&amp;gt;.feature&amp;gt;span.number+i+h4+p 上边的这条命令可以读作：&amp;ldquo;创建一个UL，该UL下有3个LI，每个LI下有一个class为feature的DIV（不指定元素名称的话，默认生成div），每个DIV内，有一个类为.number的SPAN，一个i元素，一个H4元素和一个P元素&amp;rdquo;
完整的技巧可以参看官方文档。
避免重复劳动 上边提到的频繁的F5刷新，可以通过LiveReload+Guard两个工具的组合来解决。LiveReload是一个浏览器的插件，通过协议与后台的服务器进行通信。当后台文件发生变化时，LiveReload会自动刷新页面。
Guard会使用操作系统的API来感知本地文件的变化，当文件变化后，它可以通知LiveReload进行刷新，当然Guard可以做其他一些事情，比如等SCSS发生变化时，自动编译CSS等。
两者结合之后，就可以节省我们大量的时间，而把精力主要投放在开发这件事情本身上。
样板工程 我在Github上公开了一个样板工程，这是一个开箱即用的工程，其中提供了这样一些配置：
 SCSS的编译环境（使用compass） Guard配置（当你的SCSS文件或者HTML文件修改之后，自动通知LiveReload来刷新浏览器） 一个标准的HTML5样板文档 一个基本的style.scss  Guardfile的配置中，如果index.html发生变化，或者stylesheets中的css文件发生变化，或者scripts目录中的js文件发生变化，都会触发livereload任务（通知浏览器）。
guard &amp;#39;livereload&amp;#39; do watch(&amp;#39;index.html&amp;#39;) watch(%r{stylesheets/.+\.(css)}) watch(%r{scripts/.+\.(js)}) end guard :compass 你只需要简单的将这个工程克隆到本地：
$ git clone git@github.com:abruzzi/design-boilerplate.git mydesign 然后在该目录中执行bundle install即可：
$ cd mydesign $ bundle install 这里有两点假设：</description>
      <content:encoded><![CDATA[<h3 id="现代web页面开发流程">现代Web页面开发流程</h3>
<p>通常来说，Web页面开发的流程大致是这样的：设计师（设计师不是美工，就像程序员不是码农一样）提供设计稿，通常是图片格式。然后前端的开发人员（在ThoughtWorks我们称之为UI Dev）来手工的将图片转换为对应的HTML+CSS，往往还需要在各个浏览器中调试等。</p>
<p>大多数时候，设计师会提供色卡，或者至少前景色/背景色/高亮色的值给开发人员。如果没有的话，开发人员会用到一些工具如<code>colorpicker</code>, <code>ruler</code>之类来确保最终的效果和设计稿是一致的。</p>
<p>如果你观察过UI Dev的工作流程的话，你会发现基本的上是这样的：使用编辑器（或者IDE）编写HTML代码，CSS代码，保存修改内容，切换到浏览器窗口，按<code>F5</code>或者<code>Ctrl-R</code>刷新，然后对比设计稿和实现，如果发现不一致的地方，再切换到编辑器中修改代码，如是往复。</p>
<h4 id="避免手工劳动">避免手工劳动</h4>
<p>纯手工的方式来编辑HTML/CSS会非常耗时，特别是作为标记语言的HTML，开发者需要时刻关注关闭已经打开的标签。比如一个标题元素，你需要：</p>
<div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html"><span class="p">&lt;</span><span class="nt">h1</span><span class="p">&gt;</span>This is the page title<span class="p">&lt;/</span><span class="nt">h1</span><span class="p">&gt;</span>
</code></pre></div><p>几乎从一开始，人们就想到了各种办法来避免自己重复的键入，比如Vim的<a href="https://github.com/ervandew/supertab">SuperTab</a>以及<a href="https://github.com/garbas/vim-snipmate">Snipmate</a>插件，可以通过输入<code>标签名</code>+<code>Tab</code>来补全所有的标签等，又或者DreamWaver提供的<code>代码生成</code>的方式来简化这一流程。</p>
<p>Sublime的编辑器上的著名插件<a href="https://sublime.wbond.net/packages/Emmet">Emmet</a>可以帮助开发人员飞速的开发HTML/CSS，这里有一个小例子。假设我们需要实现的页面是这样的：</p>
<p><img loading="lazy" src="/images/2014/11/web-design-resized.png" type="" alt="web design"  /></p>
<p>那么对应的HTML结构可能会是：</p>
<div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html"><span class="p">&lt;</span><span class="nt">ul</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">li</span><span class="p">&gt;</span>
        <span class="p">&lt;</span><span class="nt">div</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;feature&#34;</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">span</span> <span class="na">class</span><span class="o">=</span><span class="s">&#34;number&#34;</span><span class="p">&gt;&lt;/</span><span class="nt">span</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">i</span><span class="p">&gt;&lt;/</span><span class="nt">i</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">h4</span><span class="p">&gt;&lt;/</span><span class="nt">h4</span><span class="p">&gt;</span>
            <span class="p">&lt;</span><span class="nt">p</span><span class="p">&gt;&lt;/</span><span class="nt">p</span><span class="p">&gt;</span>
        <span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
    <span class="p">&lt;/</span><span class="nt">li</span><span class="p">&gt;</span>
    ...
<span class="p">&lt;/</span><span class="nt">ul</span><span class="p">&gt;</span>
</code></pre></div><p>使用Emmet，则只需要给出表达式，然后按一下<code>Tab</code>键就可以补全为上述的结构了：</p>
<pre><code>ul&gt;li*3&gt;.feature&gt;span.number+i+h4+p
</code></pre><p>上边的这条命令可以读作：&ldquo;创建一个UL，该UL下有3个LI，每个LI下有一个class为feature的DIV（不指定元素名称的话，默认生成div），每个DIV内，有一个类为.number的SPAN，一个i元素，一个H4元素和一个P元素&rdquo;</p>
<p>完整的技巧可以参看<a href="http://docs.emmet.io/cheat-sheet/">官方文档</a>。</p>
<h4 id="避免重复劳动">避免重复劳动</h4>
<p>上边提到的频繁的F5刷新，可以通过<code>LiveReload+Guard</code>两个工具的组合来解决。<a href="https://chrome.google.com/webstore/detail/livereload/jnihajbhpnppcggbcgedagnkighmdlei">LiveReload</a>是一个浏览器的插件，通过协议与后台的服务器进行通信。当后台文件发生变化时，LiveReload会自动刷新页面。</p>
<p><a href="https://github.com/guard/guard">Guard</a>会使用操作系统的API来感知本地文件的变化，当文件变化后，它可以通知LiveReload进行刷新，当然Guard可以做其他一些事情，比如等SCSS发生变化时，自动编译CSS等。</p>
<p>两者结合之后，就可以节省我们大量的时间，而把精力主要投放在开发这件事情本身上。</p>
<h4 id="样板工程">样板工程</h4>
<p>我在Github上公开了一个样板工程，这是一个开箱即用的工程，其中提供了这样一些配置：</p>
<ol>
<li>SCSS的编译环境（使用compass）</li>
<li>Guard配置（当你的SCSS文件或者HTML文件修改之后，自动通知LiveReload来刷新浏览器）</li>
<li>一个标准的HTML5样板文档</li>
<li>一个基本的style.scss</li>
</ol>
<p>Guardfile的配置中，如果<code>index.html</code>发生变化，或者<code>stylesheets</code>中的css文件发生变化，或者<code>scripts</code>目录中的js文件发生变化，都会触发<code>livereload</code>任务（通知浏览器）。</p>
<div class="highlight"><pre class="chroma"><code class="language-rb" data-lang="rb"><span class="n">guard</span> <span class="s1">&#39;livereload&#39;</span> <span class="k">do</span>
  <span class="n">watch</span><span class="p">(</span><span class="s1">&#39;index.html&#39;</span><span class="p">)</span>
  <span class="n">watch</span><span class="p">(</span><span class="sr">%r{stylesheets/.+\.(css)}</span><span class="p">)</span>
  <span class="n">watch</span><span class="p">(</span><span class="sr">%r{scripts/.+\.(js)}</span><span class="p">)</span>
<span class="k">end</span>

<span class="n">guard</span> <span class="ss">:compass</span>
</code></pre></div><p>你只需要简单的将这个工程克隆到本地：</p>
<div class="highlight"><pre class="chroma"><code class="language-sh" data-lang="sh">$ git clone git@github.com:abruzzi/design-boilerplate.git mydesign
</code></pre></div><p>然后在该目录中执行<code>bundle install</code>即可：</p>
<div class="highlight"><pre class="chroma"><code class="language-sh" data-lang="sh">$ <span class="nb">cd</span> mydesign
$ bundle install
</code></pre></div><p>这里有两点假设：</p>
<ol>
<li>你已经安装了<a href="http://rvm.io/">rvm</a></li>
<li>你已经使用rvm安装了某个版本的ruby，即<code>bundler</code>这个gem</li>
</ol>
<h4 id="开发流程">开发流程</h4>
<p>我通常会启动两个终端，一个用来运行<code>Guard</code>，另一个用来运行<code>HTTP Server</code>，然后是一个浏览器：</p>
<p><img loading="lazy" src="/images/2014/11/workflow-resized.png" type="" alt="workflow"  /></p>
<p>当在编辑器中进行编辑之后，保存文件，浏览器会自动刷新，这样的快速反馈可以告诉我下一步应该如何修改：将背景色调整的再淡一点，还是把会h2的字体变得更大，或者图片和文字的上边缘没有对齐等等。</p>
<p>这种开发流程和后台开发人员进行TDD的方式非常类似：<code>实时反馈，小步前进</code>！如果你的桌子上有两个显示器的话，那就更好了，你可以在一台显示器上显示设计稿，另一台上分屏显示编辑器和浏览器，这样就可以非常舒服的进行UI开发了：</p>
<p><img loading="lazy" src="/images/2014/11/two-displays-resized.png" type="" alt="two displays"  /></p>
]]></content:encoded>
    </item>
    
    <item>
      <title>jQuery插件101</title>
      <link>https://icodeit.org/2013/07/write-a-jquery-plugin-step-by-step/</link>
      <pubDate>Sat, 20 Jul 2013 00:00:00 +0000</pubDate>
      
      <guid>https://icodeit.org/2013/07/write-a-jquery-plugin-step-by-step/</guid>
      <description>这篇文章将讨论如何编写一个简单的jQuery插件的基本步骤和实践，最后完成的时候，我们会得到一个管理todo的插件（而且还是一个比较灵活，易于定制的todo插件）</description>
      <content:encoded><![CDATA[<h3 id="最终结果">最终结果</h3>
<p>这篇文章将讨论如何编写一个简单的jQuery插件的基本步骤和实践，最后完成的时候，我们会得到一个管理todo的插件（而且还是一个比较灵活，易于定制的todo插件）。事实上，这个插件可以工作在所有与管理todo类似的应用场景中，比如gmail的搜索框中的token等，豆瓣读书里的tags管理等等。</p>
<p><img loading="lazy" src="/images/2013/07/todo-origin.resized.png" type="" alt="默认设置"  /></p>
<p>上例中HTML结构如下：</p>
<div class="highlight"><pre class="chroma"><code class="language-html" data-lang="html"><span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;container&#34;</span><span class="p">&gt;</span>
    <span class="p">&lt;</span><span class="nt">input</span> <span class="na">type</span><span class="o">=</span><span class="s">&#34;text&#34;</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;input&#34;</span> <span class="p">/&gt;</span>
    <span class="p">&lt;</span><span class="nt">div</span> <span class="na">id</span><span class="o">=</span><span class="s">&#34;todos&#34;</span> <span class="p">/&gt;</span>
<span class="p">&lt;/</span><span class="nt">div</span><span class="p">&gt;</span>
</code></pre></div><p>下面的JavaScript代码将会找到id为<em>input</em>的输入框，并将它<strong>变为</strong>一个todolist的控制器，并将新加入的内容添加到id为<em>todos</em>的容器中：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="nx">$</span><span class="p">(</span><span class="s2">&#34;#input&#34;</span><span class="p">).</span><span class="nx">todoify</span><span class="p">({</span>
	<span class="nx">container</span><span class="o">:</span> <span class="s2">&#34;#todos&#34;</span>
<span class="p">});</span>
</code></pre></div><p>如果需要定制item的外观，可以定义模板并自定义渲染函数：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="nx">$</span><span class="p">(</span><span class="s2">&#34;#thing-input&#34;</span><span class="p">).</span><span class="nx">todoify</span><span class="p">({</span>
    <span class="nx">container</span><span class="o">:</span> <span class="s2">&#34;#thing-todos&#34;</span><span class="p">,</span>
    <span class="nx">template</span><span class="o">:</span> <span class="s2">&#34;&lt;section class=&#39;todoItem&#39;&gt;&lt;header&gt;&lt;%= todo %&gt;&lt;/header&gt;&lt;a&gt;remove&lt;/a&gt;&lt;/section&gt;&#34;</span><span class="p">,</span>
    <span class="nx">renderItem</span><span class="o">:</span> <span class="kd">function</span><span class="p">(</span><span class="nx">item</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">var</span> <span class="nx">cont</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">renderTemplate</span><span class="p">(</span><span class="nx">item</span><span class="p">);</span>

        <span class="nx">cont</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="s2">&#34;a&#34;</span><span class="p">).</span><span class="nx">click</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">event</span><span class="p">){</span>
            <span class="nx">cont</span><span class="p">.</span><span class="nx">remove</span><span class="p">();</span>
        <span class="p">});</span>

        <span class="k">return</span> <span class="nx">cont</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">});</span>
</code></pre></div><p><img loading="lazy" src="/images/2013/07/todo-customized.resized.png" type="" alt="Item定制"  /></p>
<h3 id="jquery插件基础知识">jQuery插件基础知识</h3>
<h4 id="简单流程">简单流程</h4>
<p>通常使用jQuery的流程是这样的：通过选择器选择出一个jQuery对象（集合），然后为这个对象应用一些预定义的函数，如：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="nx">$</span><span class="p">(</span><span class="s2">&#34;.artile .title&#34;</span><span class="p">).</span><span class="nx">mouseover</span><span class="p">(</span><span class="kd">function</span><span class="p">(){</span>
	<span class="nx">$</span><span class="p">(</span><span class="k">this</span><span class="p">).</span><span class="nx">css</span><span class="p">({</span>
		<span class="s2">&#34;background-color&#34;</span><span class="o">:</span> <span class="s2">&#34;red&#34;</span><span class="p">,</span>
		<span class="s2">&#34;color&#34;</span><span class="o">:</span> <span class="s2">&#34;white&#34;</span>
	<span class="p">});</span>
<span class="p">});</span>
</code></pre></div><p>我们如果要定义自己的插件，预期其被调用的方式和此处的<em>mouseover</em>并无二致。这需要将我们定义的函数attach到jQuery对象的fn属性上：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="nx">$</span><span class="p">.</span><span class="nx">fn</span><span class="p">.</span><span class="nx">hltitle</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
	<span class="k">this</span><span class="p">.</span><span class="nx">mouseover</span><span class="p">(</span><span class="kd">function</span><span class="p">(){</span>
		<span class="nx">$</span><span class="p">(</span><span class="k">this</span><span class="p">).</span><span class="nx">css</span><span class="p">({</span>
			<span class="s2">&#34;background-color&#34;</span><span class="o">:</span> <span class="s2">&#34;red&#34;</span><span class="p">,</span>
		 	<span class="s2">&#34;color&#34;</span><span class="o">:</span> <span class="s2">&#34;white&#34;</span>
		<span class="p">})</span>
	<span class="p">})</span>
<span class="p">}</span>

<span class="nx">$</span><span class="p">(</span><span class="s1">&#39;.article .title&#39;</span><span class="p">).</span><span class="nx">hltitle</span><span class="p">();</span>
</code></pre></div><p>jQuery的一个很明显的特点是其链式操作，即每次调用完成一个函数/插件之后仍然会返回jQuery对象本身，这个需要我们在插件函数的最后一行返回<em>this</em>。这样插件的使用者会像使用其他函数/插件一样很方便的将调用连起来。</p>
<p>另外一个问题是注意命名冲突（$是一个合法的标示符，而且被众多的JavaScript库在使用），所以可以通过匿名执行函数来避免：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">$</span><span class="p">){</span>
	<span class="nx">$</span><span class="p">.</span><span class="nx">fn</span><span class="p">.</span><span class="nx">hltitle</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
		<span class="c1">//...
</span><span class="c1"></span>	<span class="p">}</span>
<span class="p">}(</span><span class="nx">jQuery</span><span class="p">));</span>
</code></pre></div><h4 id="需要注意的问题">需要注意的问题</h4>
<p>上面是一个最简单的插件定义，为了插件更加灵活，我们需要尽可能多的将配置项暴露给插件的用户，比如提供一些默认选项，如果用户不提供配置，则插件按照默认配置来工作，但是用户可以通过修改配置来定制插件的行为：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">$</span><span class="p">){</span>
	<span class="nx">$</span><span class="p">.</span><span class="nx">fn</span><span class="p">.</span><span class="nx">hltitle</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">options</span><span class="p">)</span> <span class="p">{</span>
		<span class="kd">var</span> <span class="nx">defaults</span> <span class="o">=</span> <span class="p">{</span>
			<span class="s2">&#34;background-color&#34;</span><span class="o">:</span> <span class="s2">&#34;red&#34;</span><span class="p">,</span>
		 	<span class="s2">&#34;color&#34;</span><span class="o">:</span> <span class="s2">&#34;white&#34;</span>				
		<span class="p">};</span>
		
		<span class="kd">var</span> <span class="nx">settings</span> <span class="o">=</span> <span class="nx">$</span><span class="p">.</span><span class="nx">extend</span><span class="p">(</span><span class="nx">defaults</span><span class="p">,</span> <span class="nx">options</span><span class="p">);</span>
		
		<span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">mouseover</span><span class="p">(...);</span>
	<span class="p">}</span>
<span class="p">}(</span><span class="nx">jQuery</span><span class="p">));</span>

</code></pre></div><h3 id="todoify">Todoify</h3>
<p>我们的插件是一个遵循上述原则的简单插件，基本的步骤如下：</p>
<ul>
<li>将给定的input包装成一个jQuery对象</li>
<li>需要一个默认的放置todolist的容器元素</li>
<li>为input注册keypress事件（如果用户按Enter，则触发add事件，添加一个新条目到容器）</li>
</ul>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">$</span><span class="p">){</span>
    <span class="nx">$</span><span class="p">.</span><span class="nx">fn</span><span class="p">.</span><span class="nx">todoify</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(</span><span class="nx">options</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">var</span> <span class="nx">settings</span> <span class="o">=</span> <span class="nx">$</span><span class="p">.</span><span class="nx">extend</span><span class="p">({</span>
            <span class="nx">container</span><span class="o">:</span> <span class="s2">&#34;body&#34;</span><span class="p">,</span>
            <span class="nx">template</span><span class="o">:</span> <span class="s2">&#34;&lt;span class=&#39;todo-item&#39;&gt;&lt;%= todo %&gt;&lt;/span&gt;&#34;</span><span class="p">,</span>
            <span class="nx">renderItem</span><span class="o">:</span> <span class="kd">function</span><span class="p">(</span><span class="nx">item</span><span class="p">)</span> <span class="p">{</span>
	            <span class="k">return</span> <span class="nx">$</span><span class="p">(</span><span class="mi">_</span><span class="p">.</span><span class="nx">template</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">template</span><span class="p">,</span> <span class="p">{</span><span class="nx">todo</span><span class="o">:</span> <span class="nx">item</span><span class="p">}));</span>
            <span class="p">}</span>
        <span class="p">},</span> <span class="nx">options</span><span class="p">);</span>

        <span class="nx">$</span><span class="p">(</span><span class="k">this</span><span class="p">).</span><span class="nx">keypress</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">event</span><span class="p">){</span>
            <span class="k">if</span><span class="p">(</span><span class="nx">event</span><span class="p">.</span><span class="nx">keyCode</span> <span class="o">==</span> <span class="mi">13</span><span class="p">)</span> <span class="p">{</span>
                <span class="kd">var</span> <span class="nx">item</span> <span class="o">=</span> <span class="nx">$</span><span class="p">(</span><span class="k">this</span><span class="p">).</span><span class="nx">val</span><span class="p">();</span>
                <span class="nx">$</span><span class="p">(</span><span class="nx">settings</span><span class="p">.</span><span class="nx">container</span><span class="p">).</span><span class="nx">append</span><span class="p">(</span><span class="nx">settings</span><span class="p">.</span><span class="nx">renderItem</span><span class="p">(</span><span class="nx">item</span><span class="p">));</span>
                <span class="nx">$</span><span class="p">(</span><span class="k">this</span><span class="p">).</span><span class="nx">val</span><span class="p">(</span><span class="s2">&#34;&#34;</span><span class="p">).</span><span class="nx">focus</span><span class="p">();</span>
            <span class="p">}</span>
        <span class="p">});</span>

        <span class="k">return</span> <span class="k">this</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">}(</span><span class="nx">jQuery</span><span class="p">));</span>
</code></pre></div><p>此处为了防止创建众多的DOM元素，然后依次插入到正确地节点上，我使用了underscore.js的template，不过此处并非重点，略微一提。</p>
<p>如果用户想要更好地定制性，比如用户想要apply自己的class，定义自己的模板，或者注册新的事件（删除一条todo），显然我们需要更多的options：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="kd">var</span> <span class="nx">settings</span> <span class="o">=</span> <span class="nx">$</span><span class="p">.</span><span class="nx">extend</span><span class="p">({</span>
    <span class="nx">data</span><span class="o">:</span> <span class="p">[],</span>
    <span class="nx">template</span><span class="o">:</span> <span class="s2">&#34;&lt;div class=&#39;todo&#39;&gt;&lt;h3&gt;&lt;%= todo %&gt;&lt;/h3&gt;&lt;span&gt;X&lt;/span&gt;&lt;/div&gt;&#34;</span><span class="p">,</span>
    <span class="nx">container</span><span class="o">:</span> <span class="s2">&#34;body&#34;</span><span class="p">,</span>
    <span class="nx">renderTemplate</span><span class="o">:</span> <span class="kd">function</span><span class="p">(</span><span class="nx">item</span><span class="p">)</span> <span class="p">{</span>
        <span class="k">return</span> <span class="nx">$</span><span class="p">(</span><span class="mi">_</span><span class="p">.</span><span class="nx">template</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">template</span><span class="p">,</span> <span class="p">{</span><span class="nx">todo</span><span class="o">:</span> <span class="nx">item</span><span class="p">}));</span>
    <span class="p">},</span>
    <span class="nx">renderItem</span><span class="o">:</span> <span class="kd">function</span><span class="p">(</span><span class="nx">item</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">var</span> <span class="nx">cont</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">renderTemplate</span><span class="p">(</span><span class="nx">item</span><span class="p">);</span>
        <span class="nx">cont</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="s2">&#34;span&#34;</span><span class="p">).</span><span class="nx">click</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="p">{</span>
            <span class="nx">cont</span><span class="p">.</span><span class="nx">remove</span><span class="p">();</span>
        <span class="p">});</span>
        <span class="k">return</span> <span class="nx">cont</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">},</span> <span class="nx">options</span><span class="p">);</span>
</code></pre></div><p>这里定义了默认的close事件需要attach到span（定义在模板中）上，如果插件的用户需要自己绘制模板，并且注册事件，那么会像文章开头的那个实例一样：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="nx">$</span><span class="p">(</span><span class="s2">&#34;#thing-input&#34;</span><span class="p">).</span><span class="nx">todoify</span><span class="p">({</span>
    <span class="nx">container</span><span class="o">:</span> <span class="s2">&#34;#thing-todos&#34;</span><span class="p">,</span>
    <span class="nx">template</span><span class="o">:</span> <span class="s2">&#34;&lt;section class=&#39;todoItem&#39;&gt;&lt;header&gt;&lt;%= todo %&gt;&lt;/header&gt;&lt;a&gt;remove&lt;/a&gt;&lt;/section&gt;&#34;</span><span class="p">,</span>
    <span class="nx">renderItem</span><span class="o">:</span> <span class="kd">function</span><span class="p">(</span><span class="nx">item</span><span class="p">)</span> <span class="p">{</span>
        <span class="kd">var</span> <span class="nx">cont</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">renderTemplate</span><span class="p">(</span><span class="nx">item</span><span class="p">);</span>

        <span class="nx">cont</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="s2">&#34;a&#34;</span><span class="p">).</span><span class="nx">click</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">event</span><span class="p">){</span>
            <span class="nx">cont</span><span class="p">.</span><span class="nx">remove</span><span class="p">();</span>
        <span class="p">});</span>

        <span class="k">return</span> <span class="nx">cont</span><span class="p">;</span>
    <span class="p">}</span>
<span class="p">});</span>
</code></pre></div><h3 id="进一步改进">进一步改进</h3>
<p>目前，todoify还没有与后台进行任何的通信，如果可以和后台的RESTFul的API集成的话，这个插件将会有更多的使用场景。</p>
<p>简单来讲，只需要为插件提供更多选项，并提供回调函数即可，比如：</p>
<div class="highlight"><pre class="chroma"><code class="language-js" data-lang="js"><span class="nx">$</span><span class="p">(</span><span class="s2">&#34;#input&#34;</span><span class="p">).</span><span class="nx">todoify</span><span class="p">({</span>
	<span class="nx">restful</span><span class="o">:</span> <span class="s1">&#39;http://app/todos&#39;</span><span class="p">,</span>
	<span class="nx">onadd</span><span class="o">:</span> <span class="kd">function</span><span class="p">(</span><span class="nx">item</span><span class="p">){</span>
		<span class="c1">//...
</span><span class="c1"></span>	<span class="p">},</span>
	<span class="nx">ondelete</span><span class="o">:</span> <span class="kd">function</span><span class="p">(</span><span class="nx">item</span><span class="p">){</span>
		<span class="c1">//...
</span><span class="c1"></span>	<span class="p">}</span>
<span class="p">})</span>
</code></pre></div><p>然后加入一些ajax的调用即可。</p>
]]></content:encoded>
    </item>
    
  </channel>
</rss>
