<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
 
 <title>Peng Lyu</title>
 <link href="http://www.rebornix.com/atom.xml" rel="self"/>
 <link href="http://www.rebornix.com"/>
 <updated>2026-04-07T16:51:22+00:00</updated>
 <id>http://www.rebornix.com</id>
 <author>
   <name>Peng Lyu</name>
   <email>penn.lv@gmail.com</email>
 </author>

 
 <entry>
   <title>对不久的未来的一些展望</title>
   <link href="http://www.rebornix.com/ai/2023/12/27/unpredictable-2024/"/>
   <updated>2023-12-27T00:00:00+00:00</updated>
   <id>http://www.rebornix.com/ai/2023/12/27/unpredictable-2024</id>
   <content type="html">&lt;p&gt;作为AI/LLM行业的从业者（VS Code/Copilot），基于我对现有的公开产品、论文及过去两年的行业发展的感受，对未来（2024年至2025年中）进行了一些展望。总体而言，我的看法是偏悲观的。&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;以下内容很大概率听起来像废话，这也是为什么我特意加上了时间限定（2024年至2025年中）。21/22年我们在使用Tabnine的时候对未来有着美好的展望，当时我的感受是未来人人都会通过AI模型辅助写代码。结果LLM只花了半年就把“未来”变成了“现在”，实在是令人震惊。这篇文章的一大作用，就是在半年后打自己的脸。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&quot;可用的本地小模型将成为智能设备不可缺的核心组件&quot;&gt;可用的本地小模型将成为智能设备不可缺的核心组件&lt;/h2&gt;

&lt;p&gt;在移动平台上，苹果在iPhone上推出“Local First, Privacy First”的LLM体验是必然的，预计在2024年的WWDC上我们就能看到，它的效果也不会逊于Gemini Nano。虽然苹果的Siri像“智障”且有不少人怀疑苹果在这个领域行动缓慢，但老实说我觉得没什么好担心的，如果开源社区已经可以有数十个可用的模型，苹果没有理由做不出一个针对其芯片优化的高可用模型。&lt;/p&gt;

&lt;p&gt;这对苹果的应用市场会是颠覆性的，就像LLM将颠覆谷歌的搜索一样。我们这些iOS应用开发者以后可能都将成为agent developer，又或者我们的应用就根本没有存在的必要。由于LLM不是最终产品，苹果的情况可能要比谷歌好一些，毕竟搜索是自然语言交互，但是绝大部分iOS应用都不是。如何基于LLM提供合理的交互依然是未知的，这让我对接下来几年的应用开发感到些许兴奋。&lt;/p&gt;

&lt;h2 id=&quot;gpt-354-能力的模型使用成本会急剧下降&quot;&gt;GPT 3.5/4 能力的模型使用”成本”会急剧下降&lt;/h2&gt;

&lt;p&gt;这里主要指的是为LLM服务商支付的成本，LLM服务商会推出更高级的模型，而相对“低级”的模型要么降低使用价格，要么被开源的本地模型取代。最近层出不穷的Mixture of Experts模型在特定领域会让GPT3.5相形见绌（参考Mixtral 8x7）。任何试图通过prompt engineering优化gpt 3.5都是徒劳的，因为优化了幻觉就会让结果普遍更平庸，而显然对于openai来说，gpt3.5就是过去式，it’s what it is。&lt;/p&gt;

&lt;p&gt;绝大部分做些“中间商赚差价”生意的AI应用/服务会很快消亡，这其中打着 Build in Public 旗号的应用是最值得担忧的。这个行业发展过快了，追求low hanging fruits从长期来看是没有什么意义的。&lt;/p&gt;

&lt;p&gt;我自己判断一个AI应用是否有能力存活主要是看两个指标，一是这个产品本身在前AI时代是否也能提供价值（AI使得这个产品更可行，比如大幅降低了研发和运营成本），二是场景是不是足够复杂。小而美的应用有大概率会被AI以及的升级群碾压（所以请谨慎为它们付高额的subscriptions费）。&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;硬要找一个大家熟悉的应用作为标的的话，我觉得&lt;a href=&quot;https://podwise.xyz/&quot;&gt;Podwise&lt;/a&gt;是个足够好的产品。[The premier ___ app for podcast lovers] 是它提供的价值，而LLM是手段。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2 id=&quot;混沌的就业工作机会&quot;&gt;混沌的就业/工作机会&lt;/h2&gt;

&lt;p&gt;很多小众的需求因为ROI的提高而得到满足，从消费者的角度这是一件好事。另一方面很多工作会消失，尤其是白领岗位。&lt;/p&gt;

&lt;p&gt;很多工作的存在是因为个人的生产力没法进一步 scale up，所以只能 scale out。AI模型则打破了单个人生产力的玻璃天花板。我们早就认识到没有“人月神话”，所以探索了各种方法来解决软件工程中的问题。这其中很多问题又将随着单人生产力提高以及团队的精简而不复存在。再结合全球经济的趋势，老实说我很难不对此感到悲观。&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;如果苹果/谷歌有数据的话，他们可能能够看到新应用发布的速度变快了，而新应用的质量也相对更好了。过于二十年互联网服务发展的特别成熟，为一个已有的网络服务制作应用是现阶段LLM/Copilot非常擅长的。这对工作机会的影响是正面的。但与此同时，由于ROI的降低，复制一个产品的门槛也特别低，这可能最后让大家都没有生意可做。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;hr /&gt;

&lt;p&gt;我们要做的就是拥抱这个变化，good luck ;)&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>智能家居实践</title>
   <link href="http://www.rebornix.com/home/2021/01/21/SmartHome/"/>
   <updated>2021-01-21T00:00:00+00:00</updated>
   <id>http://www.rebornix.com/home/2021/01/21/SmartHome</id>
   <content type="html">&lt;p&gt;一直计划录一个视频详细讲解一下最近一年在家使用智能家居的心得感受，尤其是平台 &lt;a href=&quot;https://www.home-assistant.io&quot;&gt;Home Assistant&lt;/a&gt; 的优劣。没曾想最近几天我家的 Home Assistant 问题频出，在 troubleshooting 的过程中对智能家居平台的现状有了新的体会，原本录好一半的视频只能放弃，择日重录了。&lt;/p&gt;

&lt;p&gt;视频虽然鸽了，但不妨碍我先写一篇文章，介绍一下我家里智能家居配置的现状，给感兴趣的朋友一个参考。&lt;/p&gt;

&lt;h2 id=&quot;主控&quot;&gt;主控&lt;/h2&gt;

&lt;p&gt;我家里先后拥有了 Alexa 智能音箱（16年跟朋友一起团购的），Google Home Mini （Google 送的）以及 iOS 设备，所以这三个主流的智能主控我都能用。但因为它们各自支持的设备都较为有限，所以我更多是将它们和智能设备进行搭配组合，尽可能地保证我能够同时用手机应用以及语音来控制附近的设备。&lt;/p&gt;

&lt;p&gt;与此同时我也使用 Home Assistant 作为我的本地主控中心。Home Assistant 的优点是主动本地控制（不经过云端），而且是开源的，几乎能够支持所有的智能设备，甚至广义上的 IOT 设备，基本一个设备能够接入到局域网，就很有可能被 Home Assistant 支持。更多关于 Home Assistant 的优缺点，我会在之后的视频中详细分析。&lt;/p&gt;

&lt;p&gt;同时为了能够让家人也轻松地控制所有的设备，我使用 Home Assistant 的 Homekit integration 以及跑在 NAS 上的 Homebridge，将设备开放给 Homekit。家人可以通过他们的苹果设备和 Siri 来进行操控。&lt;/p&gt;

&lt;p&gt;具体来说：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;两个 Amazon Echo 用于语音控制厨房和车库的设备&lt;/li&gt;
  &lt;li&gt;Google Home Mini 用于管理书房的设备&lt;/li&gt;
  &lt;li&gt;Home Assistant 用于连接管理各种通用或者“狭义”的智能设备，以及处理自动化和设备联动&lt;/li&gt;
  &lt;li&gt;最后，家中所有人都可以通过苹果设备访问设备&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Home Assistant UI&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;img width=&quot;1648&quot; alt=&quot;image&quot; src=&quot;https://user-images.githubusercontent.com/876920/105220823-1d58e680-5b0d-11eb-82ac-3aa00a7263b5.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Homekit (macOS)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;img width=&quot;1516&quot; alt=&quot;image&quot; src=&quot;https://user-images.githubusercontent.com/876920/105220774-07e3bc80-5b0d-11eb-9940-a1ba61ecaf3e.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;就使用体验来说&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Home Assistant 相应速度最快，无论是操控还是联动，几乎实时。也支持把设备暴露给 Homekit，但就我的使用环境下，homekit integration 有一个 bug，经常不稳定。&lt;/li&gt;
  &lt;li&gt;Homebridge on NAS，这是为了解决 Home Assistant 无法连接 Homekit 而用的替代方案，优点是稳定，缺点是本身没有自动化&lt;/li&gt;
  &lt;li&gt;Homekit 通过手机操作时也是实时的，完美。一旦用到 Siri，那就是三四秒开外了，从手表上操控的话，再加两秒。&lt;/li&gt;
  &lt;li&gt;Google Home 和 Alexa 半斤八两，无法操控设备或者超时时有发生&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;协议&quot;&gt;协议&lt;/h2&gt;

&lt;p&gt;老实说，我在选择协议上犯了很大的错误。一开始由于对协议不算了解，我倾向于购买使用 Wifi 和 Bluetooth 的设备，因为它们的连接最方便，购买上的选择也足够多。&lt;/p&gt;

&lt;p&gt;如果让我再选一次，我有可能会选择 zwave，不仅是 mesh 网络，而且不在 2.4GHz频段上。不过也只是有可能，因为相比 zwave，我还是更喜欢 zigbee 这样开放的平台。&lt;/p&gt;

&lt;h2 id=&quot;设备&quot;&gt;设备&lt;/h2&gt;

&lt;p&gt;在挑选设备的时候，我的主要原则是稳定性和质量，价格和平台支持则是次要的。当然像黑五遇到五美金一个的 Amazon 私有的插座，买不了吃亏买不了上当，就另当别论了。&lt;/p&gt;

&lt;p&gt;我家里设备及具体用法是：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;三组宜家的智能灯。
    &lt;ul&gt;
      &lt;li&gt;每组智能灯有一个灯泡和一个小遥控器用于调控色温和亮度。如果家里有婴儿的话，很多时候语音和手机控制都不方便，遥控器要实用很多。此外，闺女两岁左右就可以自己用遥控器开关灯了，从此“不求人”。&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;TP Link Kasa 的开关，室内、室外插座若干
    &lt;ul&gt;
      &lt;li&gt;家里的绝大多数顶灯和落地灯都是它们控制的。如果没有调节灯的亮度或者颜色的需求，智能开关体验好非常多。&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;飞利浦 Hue 的智能灯
    &lt;ul&gt;
      &lt;li&gt;全部用在书房了。长时间工作用的灯，亮度和质量的标准都要高一些。&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;Hue 和 Aqara 的 Hub，可以将来用于管理 zigbee 设备。&lt;/li&gt;
  &lt;li&gt;Aqara 的 门窗传感器，Wyze 的门窗传感器、摄像头。
    &lt;ul&gt;
      &lt;li&gt;用来监控家里的门窗和车库门有没有关上，出门或者晚上忘了关车库门这种事发生了好几次了，只能说小区还算安全吧。&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;Aqara 的 Motion Sensor，用于晚上检测人的走动。&lt;/li&gt;
  &lt;li&gt;NFC 标签，用于搭配 Homekit 使用。&lt;/li&gt;
  &lt;li&gt;Airthings 的空气质量检测。&lt;/li&gt;
  &lt;li&gt;多个品牌的插座&lt;/li&gt;
  &lt;li&gt;其他“广义”的智能设备。因为使用了 Home Assistant，所以电视机、树莓派、NAS、打印机都能被接入到平台中。当然这也是前人栽树，后人乘凉，这些设备本身就使用了 UPnP，IPP 之类的。这是我愿意选择 Home Assistant 的原因，通用的平台才是好平台。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;自动化联动&quot;&gt;自动化、联动&lt;/h2&gt;

&lt;p&gt;在处理自动化脚本和设备间联动上，我优先使用 Home Assistant，在响应速度、稳定性上都完胜。在需要针对个人进行定制的地方，则主要使用 Homekit Automation 和 Siri Shortcut。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;最受家中领导赞赏的是：打开进入车库的门时自动把车库顶灯打开，两分钟后自动关闭。&lt;/li&gt;
  &lt;li&gt;我最喜欢的是每天天黑前半小时，自动打开 Driveway 和户外的装饰灯，天亮前关了。以往都是我们手动开关，自动化之后才发现每天都少一桩事太爽了。&lt;/li&gt;
  &lt;li&gt;当使用开关打开车库的顶灯时，同时打开其他所有的灯。车库的顶灯不够多，使用智能开关和插座的联动是最便宜的改造方式。&lt;/li&gt;
  &lt;li&gt;晚上定时检查所有门窗、车库门是否关闭，未关闭则发送通知。&lt;/li&gt;
  &lt;li&gt;晚上如果有人走下楼梯，自动打开楼下的灯。&lt;/li&gt;
  &lt;li&gt;有人回到家就打开车库的灯，五分钟后关闭。住在郊区，所以出门肯定是开车，前门基本就是用来收快递的。&lt;/li&gt;
  &lt;li&gt;当所有人都离开家后，打开 Alarm System，关闭其他所有的灯。&lt;/li&gt;
  &lt;li&gt;每当有门窗开关，手机发送通知
    &lt;ul&gt;
      &lt;li&gt;以上都是通过 Homekit 来实现的，手机与人是一对一关系，很好处理。&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;家里有小孩，语音有时候不方便，于是在顺手的位置贴上 NFC 标签，手机一扫就可以开关设备或者执行自动化脚本。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;跟很多智能家居爱好者相比，我用的自动化非常少。这不见得是坏事，它可能代表着家里的设计本身比较宜居。就拿开关灯来说，如果开关都在顺手的位置，动静检测触发开关有“多此一举”之嫌。不过使用智能设备进行联动，是一种价格低廉的 remodel 替代方案（仅限北美）。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/876920/105223778-146a1400-5b11-11eb-8cd4-17d985d49017.png&quot; alt=&quot;image&quot; /&gt;&lt;/p&gt;

&lt;h2 id=&quot;自定义场景&quot;&gt;自定义场景&lt;/h2&gt;

&lt;p&gt;除了自动化，我也设置了几个场景，一键控制多个不同的设备状态：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;工作，打开书房所有的灯，设置色温为 4000K。&lt;/li&gt;
  &lt;li&gt;阅读，同上，色温改为 3000K。&lt;/li&gt;
  &lt;li&gt;晚安，关掉除前院 driveway 上的所有的灯。&lt;/li&gt;
  &lt;li&gt;Youtube Time。打开电视并切换到 Youtube 应用，同时打开客厅的灯。&lt;/li&gt;
  &lt;li&gt;Apple TV。同上，电视源切换至连接到 Apple TV 的 HDMI。&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;意料之外的升级&quot;&gt;意料之外的升级&lt;/h2&gt;

&lt;p&gt;就像你上面看到的，我对智能家居的使用还是很初级的，主要是用自动化取代一些每天都要手动执行的工作。讲的好听点叫做“渐进式的升级”，但我觉得认真分析需求是有必要的，真正解决自己的痛点才有意义。&lt;/p&gt;

&lt;p&gt;虽然只是增加了一点点 Smartness，但这些设备却给我本身的生活带来了意想不到的麻烦，而这些在选购智能设备时很容易被忽视。当只有一两个设备时，大家都相安无事。但当我趁着 Prime Day 购买不同品牌、不同协议的设备，设备数量达到了两位数后，信号干扰开始成问题了。Wifi 类的智能设备都是使用 2.4GHz 频段，同时 Homekit 使用 mDNS/Bonjour，对路由器是个挑战。好一点的路由器都会对 mDNS/Bonjour 进行优化，但优化的同时又带来了更多的 bug，导致设备常常断开连接。更麻烦的是，2.4 GHz 上还有 Zigbee 和 Bluetooth，在 PC 上使用 MX Master 3 时就经常遇到蓝牙信号干扰的问题。&lt;/p&gt;

&lt;p&gt;我住的是 90 年代的老房子，屋内只有 Coax 而没有网线，但是我又不看传统的电视，所以家里的网络基础架构就是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Coax -&amp;gt; Modem -&amp;gt; Router&lt;/code&gt; ，一个路由器覆盖 300 平米。路由器放在客厅，从物理位置上来说很靠近家的正中心，所以客厅里的速度能够达到 250 Mbps (我们家选择的网络带宽理论上限)，而二楼角落里也能够有 100 Mbps，所以用了四年也从未考虑过升级，甚至去年一年都在家办公都没有大的需求要改善网络。直到有了这些智能设备，当我在书房办公室，如果 PC 使用 5 GHz 信道，因为信号穿了两道墙，衰减有点严重，而如果使用 2.4 GHz，又会显著增加对蓝牙信号的干扰。&lt;/p&gt;

&lt;p&gt;权衡利弊，最后我购买了 MoCA 来使用 Coax 传输网络数据，基本能够达到 1Gbps，搭配 gbe switch 就能够使用将支持 ethernet 的设备连入到有线网中，减少无线网络的压力。考虑到无论是 PC、电视还是 NAS，都只支持 gbe ，将来的很长一段时间都不需要升级网络设备了。&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;以上就是现阶段我对智能家居的使用，总体而言对全家的生活质量都有提高。不过也给我个人带来一些烦恼，主要在于让这些设备正常工作。即使是2021，智能家居设备们也不是即插即用，任重道远。&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Macbook Pro 16" 评测</title>
   <link href="http://www.rebornix.com/work/2019/12/31/rmbp16/"/>
   <updated>2019-12-31T00:00:00+00:00</updated>
   <id>http://www.rebornix.com/work/2019/12/31/rmbp16</id>
   <content type="html">&lt;p&gt;在多次决绝了老板升级笔记本的提议后，这个圣诞节苹果终于给了一个还算令人满意的选项 Macbook Pro 16”。虽然它依然带着 touchbar，但考虑到它在 ATP 上几乎被吹爆，我就选择了它作为今年圣诞节的礼物了。&lt;/p&gt;

&lt;p&gt;在这之前，我一直在使用 Macbook Pro 15” mid 2015。而为了应对性能不足，我同时在使用一台 PC （ 开发 VS Code 和使用 VS Code 都是跨平台的 ）。&lt;/p&gt;

&lt;p&gt;使用了一个礼拜后，我觉得可以对这台新电脑的情况做一些测评和分析了。Long story short，如果你和我一样，还在使用 mid 2015 款及之前的 Macbook 的话，新电脑&lt;strong&gt;值得&lt;/strong&gt;升级。&lt;/p&gt;

&lt;p&gt;下面具体聊一聊我的体会。&lt;/p&gt;

&lt;h2 id=&quot;配置&quot;&gt;配置&lt;/h2&gt;

&lt;p&gt;我的这台 Macbook Pro 是为了当前的工作略加定制的&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;处理器&lt;/strong&gt;：Intel Core i9 八核 2.4 GHz，Turbo Boost 最高可达 5.0 GHz，16MB L3 cache
&lt;strong&gt;内存&lt;/strong&gt;：32 GB 2666MHz DDR4
&lt;strong&gt;显卡&lt;/strong&gt; : AMD Radeon Pro 5500M with 8GB of GDDR6 memory
&lt;strong&gt;硬盘&lt;/strong&gt;: 512GB SSD&lt;/p&gt;

&lt;p&gt;除了内存和硬盘，其他配置升级到了最高的配置。开发 Electron 应用，硬盘不是大问题；内存略有遗憾，没有一步到位升级到 64G。&lt;/p&gt;

&lt;h2 id=&quot;硬件&quot;&gt;硬件&lt;/h2&gt;

&lt;p&gt;在使用这台新机器前，我很坚定的认为我手里的 Macbook Pro 15 mid 2015 是现存最好的 Macbook Pro，决绝反驳。所以我也从来没有试过其他同事正在使用的 2018 款。事实上在经历了四年的更新换代，不少的部件都已经发生了质变&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;扬声器。当我听到 &lt;a href=&quot;https://atp.fm&quot;&gt;ATP&lt;/a&gt; 上一顿狂吹的时候，我内心的台词是「得了吧，果粉也不是你们这么当的，不就是个扬声器么」。而且除非是在家办公，我也一直是使用耳机设备来听音乐和播客。但当我不经意间打开某个 Youtube 视频的时候，我很没出息的被震撼了。&lt;/li&gt;
  &lt;li&gt;16 寸屏幕。屏幕是越大越好的，加上有 True Tone，这块屏幕的素质比 mid 2015 高出不少。&lt;/li&gt;
  &lt;li&gt;Touch ID 很方便，比使用 Apple Watch 进行各种授权还方便。&lt;/li&gt;
  &lt;li&gt;新电脑，全新的续航能力，这一点我也很满意。&lt;/li&gt;
  &lt;li&gt;支持 Sidecar。原先 mid 2015 是不支持的。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;除了以上优点外，另外一个值得一提的就是这台机器没有&lt;strong&gt;明显的缺点&lt;/strong&gt;。上一周我在全力书写一个新的 iOS 应用以及对 Monaco Editor 进行新的调研，比较好的对我的使用习惯进行了覆盖，没有遇到什么特别的痛点。但是 low lights 还是有一些的：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;
    &lt;p&gt;屏幕最大展开角度比 mid 2015 款要小一些&lt;/p&gt;

    &lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/876920/71634295-a41fe600-2bcf-11ea-8a79-8500bcfc93a5.png&quot; alt=&quot;D8E3A93F-5CCA-430A-8A41-1BE7590FF133&quot; /&gt;&lt;/p&gt;
  &lt;/li&gt;
  &lt;li&gt;接口。虽然有 iPad Pro 这台 usbc 设备进行了缓冲， 但是我的键盘、麦克风都是 usba 口的，得接上转接头。&lt;/li&gt;
  &lt;li&gt;耳机口放到了右侧，简直莫名其妙。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;考虑到这几年 Macbook Pro 饱受批评，low lights 只有以上三点，我觉得已经非常难得了。与此同时，新键盘打起来还算舒服，有独立的 Escape 键所以作为 Vim 用户我也没啥抱怨；Touchbar 刚用时挺惊艳的，比五年前我使用的 X1 Carbon 的 Touchbar 好出了一个次元，但它是否真的实用，还需要更长时间的细心观察；Trackpad 足足大了一倍，但是似乎体验上跟以前差不多，更大的尺寸没有带来可见的更多的便利。&lt;/p&gt;

&lt;h2 id=&quot;跑分&quot;&gt;跑分&lt;/h2&gt;
&lt;p&gt;接下来就要看看在日常工作中，新的机器是否真的带来性能上的提升。&lt;/p&gt;

&lt;h3 id=&quot;vs-code&quot;&gt;VS Code&lt;/h3&gt;

&lt;p&gt;编译 VS Code 分为两步，第一步是从零编译所有的 npm 模块。VS Code 使用了不少 native modules，所以编译是需要一些时间的。&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; yarn &lt;span class=&quot;c&quot;&gt;# node dependencies&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# 15 (2015): 50 seconds&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# 16 (2019): 20 seconds&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;编译时间从 50 秒减到了 20 秒。&lt;/p&gt;

&lt;p&gt;第二步就是编译整个 VS Code 开发版，由于 VS Code 的 build pipeline 是基于 Nodejs 的，除去读取文件以外，剩下的工作没有任何并行和 multi-threading，我对这块的提升不抱太大的希望，不过结果还是令人高兴的。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&amp;gt; yarn run build # compile vscode
# 15 (2015): 120 seconds
# 16 (2019): 90 seconds
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;编译时间从两分钟减到了一分半。&lt;/p&gt;

&lt;h3 id=&quot;mcast&quot;&gt;mCast&lt;/h3&gt;

&lt;p&gt;我现在维护的最大 macOS/iOS 项目就是 mCast ( macOS 播客客户端 &lt;a href=&quot;https://apps.apple.com/us/app/mcast/id1462802606?mt=12&quot;&gt;‎mCast on the Mac App Store&lt;/a&gt; ) 。构建项目本身只需 10 秒，所以新老机器区别不大。但是 mCast 使用了  &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Kingfisher&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Alamofire&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Realm&lt;/code&gt;，构建这些原来需要 134 秒，而新机器上需要 105 秒，略有提升。&lt;/p&gt;

&lt;div class=&quot;language-bash highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; carthage update —platform macOS 
&lt;span class=&quot;c&quot;&gt;# 15 (2015): 134 seconds&lt;/span&gt;
&lt;span class=&quot;c&quot;&gt;# 16 (2019): 105 seconds&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;h2 id=&quot;稳定性&quot;&gt;稳定性&lt;/h2&gt;

&lt;p&gt;如果单看 CPU 的话，你应该会觉得性能提升应该很大才对，但实际的测试结果显示，虽然有明显提升，但跟 CPU 本身的提升差距较大。&lt;/p&gt;

&lt;p&gt;不久前图拉鼎对他的 Macbook Pro 2013 和 2018 进行了&lt;a href=&quot;https://imtx.me/archives/2834.html&quot;&gt;一次稳定性比对&lt;/a&gt;，发现散热性能对实际的 CPU 运行效率是有较大影响的。这里我依葫芦画瓢，拿 2015 和 2019 做一个类似的对比。使用工具是 &lt;a href=&quot;https://software.intel.com/en-us/articles/intel-power-gadget&quot;&gt;Intel Power Gadge&lt;/a&gt; 和 &lt;a href=&quot;https://handbrake.fr/&quot;&gt;HandBrake&lt;/a&gt;。&lt;/p&gt;

&lt;h3 id=&quot;all-thead-frequency&quot;&gt;All Thead Frequency&lt;/h3&gt;

&lt;p&gt;首先直接运行 Intel Power Gadge 的 All Thead Frequency 测试&lt;/p&gt;

&lt;p&gt;&lt;em&gt;左侧 2015, 右侧 2019&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/876920/71634304-b6018900-2bcf-11ea-9cf3-a9240dddd9ff.png&quot; alt=&quot;All Thread Tests&quot; /&gt;&lt;/p&gt;

&lt;p&gt;2015 CPU Turbo Boost 到了 3.17GHz，接近满分；而 2019 只能跑到 4.05 GHz，与理论最大值 5 GHz 差距甚远。&lt;/p&gt;

&lt;h3 id=&quot;视频编码&quot;&gt;视频编码&lt;/h3&gt;

&lt;p&gt;第二个测试则是和图拉鼎的测试过程一样，使用 Handshake 进行视频编码。而这个测试则充分说明了问题&lt;/p&gt;

&lt;p&gt;&lt;em&gt;左侧 2015, 右侧 2019&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/876920/71634299-ab46f400-2bcf-11ea-97b6-a870347d7668.png&quot; alt=&quot;Video Temp&quot; /&gt;&lt;/p&gt;

&lt;p&gt;在视频编码测试中，2015 CPU 先是 Turbo Boost 到 3， 随后随着温度上升降到了 2.8 左右；而 2019 就惨不忍睹了，先是快速 Turbo Boost 到 5，温度上去后，直接就降到了 3.2.&lt;/p&gt;

&lt;p&gt;高开低走，高下立判。&lt;/p&gt;

&lt;h2 id=&quot;小结&quot;&gt;小结&lt;/h2&gt;

&lt;p&gt;总的来说，Macbook Pro 16“ 2019 是个非常不错的升级。屏幕、扬声器、Touch ID 让我惊喜，依然符合苹果一贯的标准，有 Escape 键的键盘中规中矩；硬件的提升带来了工作中更少的编译时间，而且更重要的是，现阶段很多应用都是&lt;strong&gt;资源消耗型&lt;/strong&gt;的（最底层基于 Chromium 的各种应用），哪怕是 20% 的提升，体验上都是全方位的进步。更新 GPU 对于使用此类应用当然也大有裨益。&lt;/p&gt;

&lt;p&gt;如果你还在使用 mid 2015 款及以前的 Macbook Pro 的话， 是时候进行升级了。但还请&lt;strong&gt;注意&lt;/strong&gt;，性能的提升跟参数的关系不大，在选购时大可不必选择最高配置的 CPU，选择能够最大发挥其功效的配置更合适。&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>做一名合格的会议分享者</title>
   <link href="http://www.rebornix.com/work/2019/11/12/BetterScreenSharing/"/>
   <updated>2019-11-12T00:00:00+00:00</updated>
   <id>http://www.rebornix.com/work/2019/11/12/BetterScreenSharing</id>
   <content type="html">&lt;p&gt;从我工作的第一天开始，所在的团队就在践行 Scrum 的工作模式。 由于每天都有 scrum meeting，我已经在会议上做过几百次内容分享，听过的分享更是不计其数。&lt;/p&gt;

&lt;p&gt;如果说我只能从这些分享中总结出一个有用的经验的话，那就是：“首先，我们要努力做一个合格的分享者”。&lt;/p&gt;

&lt;p&gt;一个好的分享，需要优质的内容，需要为听众准备好一个 setup，抛出要解决的问题，提出方案，层层推进，引发他人的思考，鼓励参与讨论。做一个合格的分享，就简单得多了。你只要保证与会人员能够看得懂你的分享，并且能够流畅地看完你的分享。&lt;/p&gt;

&lt;p&gt;但遗憾的是，越简单的事情越难做好。很多人因为反复在几个小细节上犯错误，导致自己的分享总是状况不断。今天这篇文章就是要讲讲在会议上进行分享前，
你应该花&lt;strong&gt;一分钟时间&lt;/strong&gt;做好的几个准备工作。&lt;/p&gt;

&lt;p&gt;当你进入到会议（会议室或者在线会议）时，你通常会遇到以下问题：&lt;/p&gt;

&lt;h2 id=&quot;accessibility&quot;&gt;Accessibility&lt;/h2&gt;
&lt;p&gt;看到这个词的时候，估计你已经在思考残障人士了，但是请先等一下。很多分享如此不 accessible，甚至连身体健全的人都没法参与其中。在你开始分享桌面，或者把 HDMI 接口插入到你的电脑上时，就应该开始考虑 Accessibility 了。&lt;/p&gt;

&lt;p&gt;首先看看投影仪投射出来的效果，&lt;strong&gt;分辨率&lt;/strong&gt;是否合适。很多人滔滔不绝讲了半天，其他人才找到机会插一句嘴 “同学能把字调大点吗？”。如果你是远程参与会议，请务必&lt;strong&gt;问一问&lt;/strong&gt;会议室里的其他人员。&lt;/p&gt;

&lt;p&gt;如果你的分享不只是幻灯片，那么其他人有大概率看到你的&lt;strong&gt;电脑桌面&lt;/strong&gt;。开始分享前，看看桌面上是否有不合时宜的文件。最佳实践当然是桌面上啥都不要有。&lt;/p&gt;

&lt;p&gt;最后，即使所有与会人员都是 Dark Mode 的重度爱好者，在使用投影仪投射内容时，Dark Mode 也几乎是灾难，极大降低了文字的易读性。建议切换至 &lt;strong&gt;Light Mode&lt;/strong&gt;，操作系统和编辑器均是如此。&lt;/p&gt;

&lt;h2 id=&quot;勿扰&quot;&gt;勿扰&lt;/h2&gt;
&lt;p&gt;开会过程中手机响起来这种事情，我觉得应该是小概率事件了。但除了手机，也请大家记得把电脑调节到&lt;strong&gt;勿扰模式&lt;/strong&gt;。&lt;strong&gt;勿扰模式&lt;/strong&gt;需要能够避免以下干扰：&lt;/p&gt;

&lt;p&gt;笔记本电脑断电。屏幕分享、尤其是在视频通话中分享屏幕，耗电量是非常大的，这个过程中电脑没电了，可就没救了。&lt;/p&gt;

&lt;p&gt;锁屏。分享过程中如果夹杂着讨论，时间一长，屏幕可能就锁住了。&lt;/p&gt;

&lt;p&gt;退出音乐、播客播放器。如果不小心误触播放键的话，希望你有个好的音乐品味。&lt;/p&gt;

&lt;p&gt;关闭系统通知。我以前就看到过同事分享的过程中，收到了猎头的邮件回复，场面一度很尴尬。&lt;/p&gt;

&lt;h2 id=&quot;语音清晰&quot;&gt;语音清晰&lt;/h2&gt;
&lt;p&gt;由于人总是能够听到自己说话，我们常常忽视了别人接受到我们的声音的最终效果。这一点在远程会议时尤为明显。&lt;/p&gt;

&lt;p&gt;如果你不能在安静的环境加入到在线会议中，那么一个专业的收声效果良好的麦克风就是必不可少的。否则其他人听到的背景音可能比你的声音还大。&lt;/p&gt;

&lt;p&gt;如果你的讲话结束了，或者暂时停止了，记得关闭麦克风。很多人刚结束分享的时候都是一下子放松下来，然后开始干一些跟当前会议没什么太大关系的事情，这个时候麦克风还开着的话，那就只能祝福你没有自言自语的习惯了。&lt;/p&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;小结&quot;&gt;小结&lt;/h2&gt;

&lt;p&gt;怎么样，上面的这些建议是不是都非常简单？但你摸着良心告诉博主，有没有哪次你也遇到过上面说的问题？&lt;/p&gt;

&lt;p&gt;文章开头的时候，我强调一个好的分享，是要为听众准备好一个 setup 的。这个 setup 的目的，就是帮助听众，直达你要分享的核心。无论是杂乱无中的电脑桌面、漆黑一片的编辑器、突然跳出的微信通知，都是在不停地分散听众的注意力。而听众的思绪一旦被打断，他们就可能忘记自己刚刚想问的问题，好不容易回想起来了，却发现你的幻灯片已经又往下走了两页了。&lt;/p&gt;

&lt;p&gt;如果与会人员因为这些小细节的疏忽，没能及时准确地理解你分享的内容，他们会很容易对你产生一种不好的印象，那就是你的分享好像比较难懂，或者比较难以获得什么价值。一个流畅的，不被打扰的，“一镜到底”的分享，则会让与会者感到过瘾，这种印象一旦建立，就会让大家更愿意去倾听你的分享，从而提供有价值的反馈。&lt;/p&gt;

&lt;p&gt;授人以渔，当然也要授人以鱼。这里介绍一下我在使用 macOS 时，是如何使用第三方工具，来规避上面列出的这些问题的。我主要使用的工具暂时有两个：&lt;/p&gt;

&lt;p&gt;第一个是 &lt;a href=&quot;https://fireball.studio/oneswitch/&quot;&gt;One Switch&lt;/a&gt;，来自知名独立开发者图拉鼎。One Switch 是一个 macOS menubar 应用，能够一键切换各种系统功能。在下图中，左侧是我平时使用电脑时的状态，而在进入到一个会议前，我会切换到右侧&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;隐藏全部桌面图标&lt;/li&gt;
  &lt;li&gt;关闭 Dark Mode&lt;/li&gt;
  &lt;li&gt;禁止系统睡眠&lt;/li&gt;
  &lt;li&gt;打开系统勿扰模式&lt;/li&gt;
  &lt;li&gt;快速调整分辨率&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/876920/68721510-71f6ee00-0567-11ea-83c8-72d84ce448ea.png&quot; alt=&quot;One Switch Settings&quot; /&gt;&lt;/p&gt;

&lt;p&gt;另一个应用就是 &lt;a href=&quot;https://mizage.com/shush/&quot;&gt;Shush&lt;/a&gt;。这个应用是我以前玩播客的时候买的，它能够帮助你一键控制麦克风，主要包含两个模式&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Push to talk。默认情况下麦克风关闭，按下某个快捷键时，麦克风打开。适合参与到人比较多的会议时，别人提问时才打开麦克风。&lt;/li&gt;
  &lt;li&gt;Push to mute。默认情况下麦克风打开，按下快捷键暂时关闭麦克风，释放快捷键则恢复麦克风。录播客的时候因为一直讲话，所以这个模式更适合，需要咳嗽了就按下键静音一会儿。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;img width=&quot;190&quot; alt=&quot;image&quot; src=&quot;https://user-images.githubusercontent.com/876920/68721527-83d89100-0567-11ea-95d2-627bb2893d63.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;这个应用我主要在远程工作时使用。如果 One Switch 能够支持这个功能，那我不必再多开一个应用了。&lt;/p&gt;

&lt;p&gt;文中提到的其他几点，比如避免笔记本断电、切换编辑器 Dark Mode、使用专业麦克风，并没有什么通用的解决方案，还望见谅 ;)&lt;/p&gt;

</content>
 </entry>
 
 <entry>
   <title>Web 性能优化、文档及代码编辑器相关的新提案</title>
   <link href="http://www.rebornix.com/work/2019/10/29/Web-Proposals/"/>
   <updated>2019-10-29T00:00:00+00:00</updated>
   <id>http://www.rebornix.com/work/2019/10/29/Web-Proposals</id>
   <content type="html">&lt;p&gt;作为使用 Web 技术的代码编辑器从业者，关注 Web 平台的各种新提案是工作的一部分。随着 Monaco Editor 被很多服务使用，我也开始收到各个新提案的发起者的邀请，对他们的提案提供反馈。其中一些提案跟编辑器开发效率和性能优化息息相关，但还处于非常早期的阶段，十分有潜力，所以我觉得值得分享出来，这样大家有机会可以参与到这些提案的设计讨论中。&lt;/p&gt;

&lt;p&gt;首先，这篇文章的适用人群&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Web 文档和开发工具从业者。这里我还是举几个例子帮助理解
    &lt;ul&gt;
      &lt;li&gt;你们的产品类似于 Office Online, Google Docs, 石墨, 语雀, Notion 等等&lt;/li&gt;
      &lt;li&gt;你们的产品类似于 Monaco Editor, CodeMirror, Ace 等等&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;对 Web 平台&lt;strong&gt;性能优化&lt;/strong&gt;感兴趣或者有需求的同&lt;/li&gt;
  &lt;li&gt;对 Web 平台技术和标准非常感兴趣的同学&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;好了，废话少说。本文要介绍的提案包含以下几个：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Keyboard Map&lt;/li&gt;
  &lt;li&gt;Edit Context&lt;/li&gt;
  &lt;li&gt;Highlight API&lt;/li&gt;
  &lt;li&gt;Virtual Scroller&lt;/li&gt;
  &lt;li&gt;Display Locking&lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;keyboard-map&quot;&gt;Keyboard Map&lt;/h2&gt;

&lt;p&gt;提案：&lt;a href=&quot;https://github.com/WICG/keyboard-map&quot;&gt;Keyboard Map&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;首先要介绍的提案 Keyboard Map，其实在我前面的几篇博客详细介绍过了 。如果你的产品的用户，使用非 US 标准键盘的话，请务必详细阅读下面这个系列的文章。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Web 应用快捷键支持
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;https://rebornix.com/vscode/2019/08/11/web-keyboard-support/&quot;&gt;（一）正确处理 Keyboard Event&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://rebornix.com/vscode/2019/08/25/web-keyboard-support-2/&quot;&gt;（二）code/key 的缺点和 Node Native Keymap&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;简言之，在不同的标准的键盘上，按下同一个物理键时输入到系统中的字符可能是不同的。比如在 US 标准键盘上，Shift 旁边的键是 Z，而在德语键盘上则是 Y。&lt;/p&gt;

&lt;p&gt;这个提案，就是为大家提供一个物理键和字符之间的映射关系，至于它能解决什么问题嘛，还请阅读上面的系列文章，抱歉，五百字可能都解释不完。&lt;/p&gt;

&lt;h2 id=&quot;edit-context&quot;&gt;Edit Context&lt;/h2&gt;

&lt;p&gt;提案：&lt;a href=&quot;https://discourse.wicg.io/t/proposal-editcontext-api/3656&quot;&gt;EditContext API - WICG&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;第二个要介绍的提案，来自微软的 Edge 团队。当我看到这个提案的时候，我是非常激动的，因为它就是为 Web 平台文档和代码编辑器设计的。&lt;/p&gt;

&lt;p&gt;Web 平台要做一个文档或者代码编辑器，光靠 Textarea 和 Input 是肯定不行的，要实现文字多样式、快捷键支持、文档虚拟化渲染，大家一般会使用下面两种方案&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;一个隐形的 textarea&lt;/li&gt;
  &lt;li&gt;ContentEditable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;第一个方案，通过创建一个隐形的 textaea，监听发生在这个上面的所有事件，比如 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Input&lt;/code&gt;， &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keyDown&lt;/code&gt;等等，然后再将输入的内容按照格式渲染在页面中。也就是说，textarea 监听事件，而用户看到的内容都是虚拟渲染出来的。&lt;/p&gt;

&lt;p&gt;这种方案的工作量其实是非常大的，因为连 cursor 和 selection 都是自己渲染的。而且这也意味着，在触摸屏上，基本上没法通过手指选择文字。&lt;/p&gt;

&lt;p&gt;而 ContentEditable 这个 API 的推出就是为了更方便的实现 WYSIWYG 编辑器的 API，但是 ContentEditable 早年的 bug 实在是太多了，大家在 textarea 这个方案上走的非常远了。现在 ContentEditable 成熟度好多了，但它有几个局限&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;ContentEditable 本质上是把 View 当作 single source of truth，它接受用户输入，直接更新渲染。接着我们需要从 View 中检测出变动的内容和格式，最后更新 Model。
    &lt;ul&gt;
      &lt;li&gt;当然也是可以在 ContentEditable 截断各种 event 的，但那就跟 textaea 没区别了。&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;用户跟 ContentEditable 的交互不能被打断。如果用户在输入时（比如说有 IME），这时候有其他对象更新了 ContentEditable 对象，那么用户的输入就会打断。对于强调 Collobration 的文档系统，这是不可以接受的。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;EditContext 这个提案，解决方案是将用户输入这系列 API 独立出来，应用只需通过 EditContext 这个新的接口来接收用户的输入。换成大白话来说&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;你可以把 EditContext 当成一个隐藏的 textarea 那样去监听各种事件&lt;/li&gt;
  &lt;li&gt;你可以往 EditContext 中塞入当前光标附近的上下文，这样系统能够提供 Spell Check，Suggestion，Accessibility 等功能。&lt;/li&gt;
  &lt;li&gt;你可以利用 EditContext，告诉系统，你觉得当前光标应该在哪里，这样当用户使用输入法、调用 Emoji 输入框、调用字典时，系统就知道在哪里渲染对话框了。&lt;/li&gt;
  &lt;li&gt;EditContext 是直接跟底层的输入 API 通讯的，不受页面增量渲染的影响。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;这个提案看起来还是很有潜力的，如果最终的实现能够提升触屏上的使用体验，以及提升 Accessibility 的话，那是很值得一试的。&lt;/p&gt;

&lt;h2 id=&quot;highlight-api&quot;&gt;Highlight API&lt;/h2&gt;

&lt;p&gt;提案：&lt;a href=&quot;https://github.com/MicrosoftEdge/MSEdgeExplainers/blob/master/highlight/explainer.md&quot;&gt;MSEdgeExplainers/highlight&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Edge 团队另外一个有趣的提案就是 Highlight API。这个更好理解了，你可以异步地通知浏览器调整某段 DOM 节点的样式。&lt;/p&gt;

&lt;p&gt;现在大家的做法都是在生成 HTML 的时候，就直接带上了样式信息（inline 或者通过 CSS）。Highlight API 将这个过程异步化，既简化了结构，又让渲染变的更加可控。理论上，Highlight API 的性能能更好些，也更干净。&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;如果说上面的提案跟编辑器强相关，下面的这两个提案就相对更通用了，对于大部分 Web 应用的性能优化都有潜在价值。&lt;/p&gt;

&lt;p&gt;通常来说，页面中 DOM 结构越复杂、节点越多，给浏览器渲染带来的挑战就越大。解决这个问题的一个主要方法，就是把用户暂时看不到的内容暂时不塞入 DOM 中。Problem Solved。&lt;/p&gt;

&lt;p&gt;这么做优化了性能，但是带来的体验上的问题却不小：&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;当用户需要滚动页面时，将要看到的内容还未加载到页面中。我们需要在此处截断，提前把用户将要看到的内容塞入页面中。&lt;/li&gt;
  &lt;li&gt;滚动条比例不正确。我们需要自行渲染滚动条，以展示当前页面在全部内容中所占的比重。&lt;/li&gt;
  &lt;li&gt;浏览器默认的搜索框无法搜索全部内容。我们需要实现额外的搜索组件。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;前两个难题，一般来说，只要你是使用主流的框架来开发项目的，基本坑都已经被填好了，按照 “框架名 virtual list ” 进行搜索就能找到答案。解决方案一般都是&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;在 model 和 view 之间，增加一个 view model，根据 container 和 list item 的大小，决定 view 中需要熏染的 list item 的数量。&lt;/li&gt;
  &lt;li&gt;监听 scroll event，在 scroll 发生前，将下一个 page 塞入到 view 中。&lt;/li&gt;
  &lt;li&gt;最后，非常重要，通过 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;will-change&lt;/code&gt; 或者 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;translateZ&lt;/code&gt;通知浏览器将 view 在新的 layer 中进行渲染。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;完成上面的方案，对算法功底和浏览器渲染机制都是有一定的要求，而且每个框架，都可能需要一个自己的 virtual list。&lt;/p&gt;

&lt;p&gt;而下面这个提案，就是要救大家于水火。&lt;/p&gt;

&lt;h2 id=&quot;virtual-scroller&quot;&gt;Virtual Scroller&lt;/h2&gt;

&lt;p&gt;提案：&lt;a href=&quot;https://github.com/WICG/virtual-scroller&quot;&gt;virtual scroller&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;顾名思义，就是提供一个实现 virtual list 的内置方案，你负责塞数据，我负责渲染，而且尽可能保证只渲染用户需要看到的。语法也很直观：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&amp;lt;virtual-scroller id=&apos;scroller&apos;&amp;gt;
  &amp;lt;div&amp;gt;Item 1&amp;lt;/div&amp;gt;
  &amp;lt;div&amp;gt;Item 2&amp;lt;/div&amp;gt;
  ...
  &amp;lt;div&amp;gt;Item 1000&amp;lt;/div&amp;gt;
&amp;lt;/virtual-scroller&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;一个新语法，解决三大问题，不需要用户根据滚动加载数据，滚动条比例正确，浏览器也能正确搜索。不要问有什么潜在问题，问就是银弹。&lt;/p&gt;

&lt;p&gt;当然我们知道，哪怕 virtual scroller 真的可以做到按需渲染，但我们也不能真的把内容全部都塞到 DOM 中。比如你打开一个文件，一百万行代码，如果全塞进去，再牛的引擎也不行。因此 virtual scroller 也是允许你自行做 paging 的，它会根据滚动的情况释放 rangechange 事件，让你有机会往里面塞入更多的内容。&lt;/p&gt;

&lt;p&gt;到这里，哪怕你对实现一个虚拟化的 list 组件没什么概念，你肯定也意识到了，无论是土方法还是 virtual scroller ，万变不离其宗，都是减少渲染开销嘛。既然目标这么明确，那么干脆就对阵下药好了，Chrome 团队又提出了一个相对更底层的 API：Display Locking，让你直接控制渲染。&lt;/p&gt;

&lt;h2 id=&quot;display-locking&quot;&gt;Display Locking&lt;/h2&gt;
&lt;p&gt;提案：&lt;a href=&quot;https://github.com/WICG/display-locking&quot;&gt;Display Locking&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;一句话，将部分 DOM 节点锁定，让它跳过 rendering 这个环节，等到需要的时候再进行 render。&lt;/p&gt;

&lt;p&gt;有了这个 API，上面的 Virtual Scoller 是不是实现起来也很简单？我们把所有 list item 都加载到 DOM 中，但是 lock 住用户看不到的部分。然后在 idle 或者用户滚动的时候，增量地渲染。&lt;/p&gt;

&lt;p&gt;Virtual Scoller 抽象程度更高，解决的是 list view 这一类型的问题，而 Display Locking 这样底层的 API，你几乎可以用在任何地方。&lt;/p&gt;

&lt;hr /&gt;

&lt;h1 id=&quot;小结&quot;&gt;小结&lt;/h1&gt;

&lt;p&gt;以上这五个提案，只是抛砖引玉，大家可以在 WICG 这个 github organization 里查看各种新提案，每个都有自己要解决问题。如果它们刚好跟你们的业务相关，不妨参与到提案的讨论中。&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>「Shape Up」 适合中小团队的一种工作方式</title>
   <link href="http://www.rebornix.com/work/2019/10/18/Shape-Up/"/>
   <updated>2019-10-18T00:00:00+00:00</updated>
   <id>http://www.rebornix.com/work/2019/10/18/Shape-Up</id>
   <content type="html">&lt;p&gt;VS Code 这三年，用户量增长二三十倍，但是团队大小几乎没有变化，依然保持了稳定的产出。为了同时达成高速增长与稳定产出这两个目标，我们是有一套团队运作方法的。虽然我也多次受邀去分享我们的工作经验，但我心中一直有个疑惑，就是这套工作方式是否能真正在其他团队中落地。&lt;/p&gt;

&lt;p&gt;直到最近我读了 Basecamp 的产品方法论 &lt;a href=&quot;https://basecamp.com/shapeup&quot;&gt;Shape Up: Stop Running in Circles and Ship Work that Matters&lt;/a&gt; 。这份文档介绍了 Basecamp 是如何保证，一个想法通过层层把关，在一个自由的工作环境中，最终准时变成产品发布出去的。&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;你可能对 Basecamp 这家公司不太了解，但你一定听说过他们的产出：Ruby on Rails、Rework 和 Remote。&lt;/p&gt;

  &lt;p&gt;Shape Up 现在只有在线版，可以免费阅读，估计之后 Basecamp 会将其出版。到时候可能会像 Remote 和 Rework 一样获得广泛关注。&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;虽然 Shape Up 是在讲如何推进产品，但我更愿意称它为一种工作方式，因为产品最终是由人来推动的，而它本身也就是在讲对团队和成员应该进行哪些约束，又该创造哪些自由空间。&lt;/p&gt;

&lt;p&gt;当我阅读这份文档时，过去这三年的工作经历在脑海中快速闪过。我们的工作方式跟 Basecamp 有非常多的共同点，而且团队大小也在一个数量级上（20 - 50）人。但同时，我们的产品形态、公司规模、团队组成又有着很大的不同。这意味着，这一种工作方式是具备一定的普适性的。&lt;/p&gt;

&lt;p&gt;这篇文章里我会尝试对 Shape Up 做一些梳理，同时结合我的工作经验做一些解读。&lt;/p&gt;

&lt;h2 id=&quot;发布&quot;&gt;发布&lt;/h2&gt;
&lt;p&gt;重要的事情最先说，整个文档的核心目标就是发布。这些年大环境的熏陶下，大家都明白了 “Ship it”，重要的是把产品发布出来，因为一个产品没有问世见到客户，一切都是空谈。“Ship it” 很难，努努力总是可以达到的，没达到的团队就死了。&lt;/p&gt;

&lt;p&gt;相比之下，更难的则是产品发布后，持续的发布迭代。为了确保每个 release 计划的新功能都能够发布出去，Basecamp 的第一个准则就是小步快跑，六个礼拜一个 release，把 deadline 作为严格标准，其他事情都得为 deadline 做让步。&lt;/p&gt;

&lt;p&gt;六个礼拜的时间是 Basecamp 实践后找到的最合适他们的产品发布周期。VS Code 也差不多，通常一个 release 持续四个礼拜或者五个礼拜。&lt;/p&gt;

&lt;h2 id=&quot;shaping&quot;&gt;Shaping&lt;/h2&gt;
&lt;p&gt;既然我们确定了六个礼拜后会发生什么，那么在一个 release 周期最开始做计划的时候就要格外小心了，任何错误的估计都可能导致无法按时交付。&lt;/p&gt;

&lt;p&gt;回想一下，你有没有什么时候跟老板说这个任务两天就能搞定，结果搞了一个礼拜加班加点才勉强完成的？&lt;/p&gt;

&lt;p&gt;对一个存在很多未知数的需求进行工程量的分析是非常困难的。Basecamp 决定在这个上面下功夫，在开始工作前，对需求进行探讨、分析，给出解决方案的雏形，预估风险。Shaping 完成之后，再执行这套方案。他们称这个过程为 Shaping。&lt;/p&gt;

&lt;p&gt;Shaping 结果则要求满足三个条件&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Rough 这个计划必须是简单粗浅的，只设计大框架，而不深入细节。这里 Shape Up 提了个观点非常有意思，计划越详细，最后要花的时间越多，因为一旦你把细节都设计好了，团队在执行的时候，可以做的 tradeoff 越少，反而会投入更多的时间&lt;/li&gt;
  &lt;li&gt;Solved 这个计划必须是可以执行最后产出结果的，这要求了计划必须考虑清楚潜在的坑（rabbit hole）&lt;/li&gt;
  &lt;li&gt;Bounded 这个计划必须是有边界的，既要知道什么可以做，也要知道什么不能做，到哪里止步。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;问题来了，谁能保证 Shaping 的质量呢？Basecamp 用了一个巧妙的方法，Shaping 由专门的资深团队来做，这个团队做完 Shaping 把方案雏形交给其他团队来执行。&lt;/p&gt;

&lt;p&gt;也就是说公司实际上分成了两种团队&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;第一种团队进行需求分析，制定方案框架。这个团队人数很少，但是对每个成员的要求都非常高，分析能力、设计能力、表达能力都得是一流的。&lt;/li&gt;
  &lt;li&gt;第二种团队负责执行。这种团队相对较小，由个别设计师加工程师组成。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;VS Code 团队是每个人自己做 Shaping，如果成员比较 junior，一般老板也会参与。不过 Basecamp 的双轨制更适合大部分团队，团队种不一定人人都是顶尖好手，但几个资深工程师还是有的，让他们做 Shaping 的工作。&lt;/p&gt;

&lt;p&gt;你可能会问，如果计划都订好了，那执行团队的工作会不会太“无脑”了？执行团队是否永远无法变得“资深”？Shaping 规则要求了要 Rough，是给执行团队提供了一定的空间的。如果你仍然比较担心，我觉得有几个方案可以解决这个问题&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;资深工程师也可以参与到执行中，让执行团队有机会跟资深工程师一起工作。&lt;/li&gt;
  &lt;li&gt;Shaping 团队可以预留个把位置给初级工程师，每个 release 轮换，给他们参与 Shaping 的机会锻炼锻炼。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;在 Shaping 完了后，Shape Up 还要求要计划写成文案（pitch），这个文案要介绍&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;想解决的问题&lt;/li&gt;
  &lt;li&gt;项目的大小，small batch 还是 big batch。Small batch 需要在一两周内完成，Big batch 则是六周内完成&lt;/li&gt;
  &lt;li&gt;解决方案&lt;/li&gt;
  &lt;li&gt;潜在的坑&lt;/li&gt;
  &lt;li&gt;有哪些功能是不要做的&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;文案出了之后，接下来就是拿着他们去说服老板，通过评审后，交给执行团队。这个挺像微软以前开发项目前必须做的 Design Spec 的，只不过想在全面投向敏捷，已经不做了。没想到 Basecamp 六个礼拜一个发布周期依然能够继续做这个重的计划工作（当然，这个代价就是得有一个专门的团队做 Shaping）。&lt;/p&gt;

&lt;p&gt;更多的关于 Shaping 怎么做，有哪些发现坑的技巧，如果确定项目的时间，如何砍功能，还请阅读原文，原文有实战的例子。https://basecamp.com/shapeup/1.1-chapter-02&lt;/p&gt;

&lt;p&gt;而在 VS Code ，我们制定好计划后，都会发布一个 iteration plan https://github.com/microsoft/vscode/issues/8059，这个 plan 也就是类似于 Shaping 的结果，分配到人，然后大家各自按照这个计划来执行就行了。&lt;/p&gt;

&lt;h2 id=&quot;执行&quot;&gt;执行&lt;/h2&gt;

&lt;p&gt;交给执行团队后，接下来就是按照计划来完成了。由于计划其实是非常 Rough 的，设计师跟开发该怎么协作，第一步做什么，怎么集成，这些完全都是交给执行团队的。&lt;/p&gt;

&lt;p&gt;Shape Up 对此的定义也很有意思，他们认为交给执行团队的是一个项目 Project，而不是任务 Tasks。该怎么完成项目，是这个团队自己决定的。&lt;/p&gt;

&lt;p&gt;Shape Up 也提供了一套工作执行方案，没有太多新鲜的知识。比如开发者不一定非要等设计出设计稿了再动手，可以先用一个 placeholder ，样式也可以不计较，等等。我相信大部分团队都有自己的开发节奏，不必完全照搬。只要 deadline 和方案定了，怎么适合自己怎么来。&lt;/p&gt;

&lt;p&gt;不过这个章节提到了一非常重要的规则，能不能保证计划最终发布，很大程度上靠它：&lt;strong&gt;拒绝干扰&lt;/strong&gt;。一个执行团队如果开始这个项目了，那么任何事情都得靠边站。无论你突然有了一个什么爆炸的想法，还是用户找你吐槽一个 bug，都得靠边站。&lt;/p&gt;

&lt;p&gt;这一条要想在国内的团队落地，难度相当大，如果没有大老板拍板决定此套工作方式，很难想象在六个礼拜内 PM 不会跑出来加需求。哪怕在 VS Code，我们也是相对松散的，如果有比较紧急的事情，手里的活是可以放一放的，当然出现的次数相对较少。&lt;/p&gt;

&lt;h2 id=&quot;debtbug-fixing&quot;&gt;Debt/Bug Fixing&lt;/h2&gt;
&lt;p&gt;细心的话你会发现，上面的工作计划，完全没有涉及到修 bug。难道说 Basecamp 人人有如神助，写出来的代码没 bug？这当然是不可能的。为了保证 bug 有时间修，技术债有时间还，Basecamp 会在六个礼拜的 iteration 之间，增加两个礼拜的自由时间，在这段时间团队可以修 bug，优化架构等等。&lt;/p&gt;

&lt;p&gt;这么算来，其实 Basecamp 的一个 iteration 是八个礼拜。而 VS Code 就紧凑的多&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;第一个礼拜主要负责 debt&lt;/li&gt;
  &lt;li&gt;一到三个礼拜做功能&lt;/li&gt;
  &lt;li&gt;一个礼拜测试、发布&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;VS Code 也会做类似的 housekeeping，不过我们现在的节奏是一年做一次，一般都是在年底，花一整月大幅度削减 issue 数量。&lt;/p&gt;

&lt;hr /&gt;

&lt;p&gt;以上就是我阅读全文后的总结和感受。这个文档中，除了从 Shaping 到执行这条明线外，还有一条暗线，那就是想法到底重不重要？全文一直在强调一下几点&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;想法不重要&lt;/li&gt;
  &lt;li&gt;一个让人激动万分的想法，不重要&lt;/li&gt;
  &lt;li&gt;不需要把想法记录下来&lt;/li&gt;
  &lt;li&gt;对一个新想法的默认答案是，不&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;你可能会问，如果是这样对想法视而不见的话，产品如何创新呢？Basecamp 的解释说白了就两条&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;首先，好的想法一定会不断地出现在你的脑海里的，根本不用记&lt;/li&gt;
  &lt;li&gt;不管多好的想法，我们等这个 release 结束，自由时间里我们再进行讨论&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;只有这样，才能保证项目开发不受任何的干扰，保证最终的发布。对此我的感受是，这个团队还得有一个特质，那就是冷静，极度的冷静。&lt;/p&gt;

&lt;h2 id=&quot;想法和界限&quot;&gt;想法和界限&lt;/h2&gt;

&lt;p&gt;我们中的大部分人，应该都会有过那么几次感觉自己发现了个碉堡了的想法，然后立刻动手开始尝试，随着坑越开越大，最后要么弃坑了，要么交付出去个半成品。只有极少数把那个碉堡了的点子，快速变成了碉堡了的产品。&lt;/p&gt;

&lt;p&gt;这里不是说你的 “碉堡了” 的想法不好，相反，越好的想法问题越麻烦。因为好想法通常是不收敛的，你可能从一个简单的点出发，最后爆发出成百上千的好点子。&lt;/p&gt;

&lt;p&gt;问题是，我们的 release 是有时间限制的：六个礼拜。如果 Follow Your Heart，那么六个礼拜后我们能看到什么结果是不可预期的。遗憾的是，就经验而言，这种做法最后带给我们的产品，往往都是达不到发布的标准的。&lt;/p&gt;

&lt;p&gt;知道了这点后，我们在计划的时候，就要时刻保持冷静，尽可能的不要对想法感到过于激动。这听起来挺难的，而且很反人性。我们常说，最加法简单，做减法很难。&lt;/p&gt;

&lt;p&gt;其实加法越挺难的，Basecamp 把这种风险规避在了 Shaping 的阶段。由头脑冷静，经验丰富的工程师，对想法进行推敲，最终变成可执行的方案。&lt;/p&gt;

&lt;h2 id=&quot;局限&quot;&gt;局限&lt;/h2&gt;

&lt;p&gt;这种工作方式，在 Basecamp 和 VS Code 团队的效果都很好，但也有很大的局限性。这种局限性来自于 Basecamp 和 VS Code 团队的特质，那就是高度自治。Basecamp 不用说，四五十个的团队，全公司都执行此套方案。而 VS Code 的开发，则几乎是由团队自己推动的，极少收到外界的干扰，这在大公司实属罕见。&lt;/p&gt;

&lt;p&gt;要执行此套方案，要求推动者能够获得较大的产品控制权，这个 20 - 50 个人的团队，要高度自治。如果达不到这点的话，无论是需求分析阶段（Shaping）还是执行阶段都无法按照上面的方案进行。&lt;/p&gt;

&lt;p&gt;这套方案的核心就在于&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;资深团队、成员分析需求，将想法变成可执行的方案&lt;/li&gt;
  &lt;li&gt;掌舵者和资深团队一起审议，挑选优先级最高的方案&lt;/li&gt;
  &lt;li&gt;执行团队无干扰推动进展&lt;/li&gt;
  &lt;li&gt;一切以发布为首要任务&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;说服大团队落地这个工作方式应该是比较困难的，没法一口吃成大胖子。建议可以在小团队内先尝试，一个 tech lead，加几个执行能力强的工程师就足够了，作为 Manager，剩下的就是给他们足够的空间了。&lt;/p&gt;

</content>
 </entry>
 
 <entry>
   <title>从 mCast 聊聊声明式 UI（Vue.js 与 SwiftUI）和原型构建效率</title>
   <link href="http://www.rebornix.com/swift/2019/09/28/prototype-with-swiftui/"/>
   <updated>2019-09-28T00:00:00+00:00</updated>
   <id>http://www.rebornix.com/swift/2019/09/28/prototype-with-swiftui</id>
   <content type="html">&lt;p&gt;作为玩票性质的 Vue.js 用户和 iOS/macOS 开发，这个标题对于我来说，写起来本来是没有多少底气的。不过半年前我花了点业余时间和朋友写了个 macOS 的播客客户端 &lt;a href=&quot;https://apps.apple.com/us/app/mcast/id1462802606?ls=1&amp;amp;mt=12&quot;&gt;‎mCast&lt;/a&gt; ，获得了一些第一手的感悟，故此写篇文章聊聊我的想法。&lt;/p&gt;

&lt;p&gt;先说结论，声明式 UI 及配套的现代工具链（Hot Reload，Live Editing）能够大幅提高原型构建的效率，而且对经验不丰富的开发者尤为明显。&lt;/p&gt;

&lt;p&gt;我们先不妨看看 mCast 的设计、原型和最终实现的过程。&lt;/p&gt;

&lt;h2 id=&quot;设计&quot;&gt;设计&lt;/h2&gt;

&lt;p&gt;&lt;a href=&quot;https://apps.apple.com/us/app/mcast/id1462802606?ls=1&amp;amp;mt=12&quot;&gt;‎mCast&lt;/a&gt;  并不是要做一个全尺寸的桌面端播客客户端，它只是来自我几个简单的想法&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;一个 macOS menubar 应用，永远可以快速通过鼠标访问&lt;/li&gt;
  &lt;li&gt;类似于 iOS 的应用设计&lt;/li&gt;
  &lt;li&gt;核心功能就是：搜索、浏览热门，然后收听&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;第一点对于一个开了十几个应用窗口的人来说是刚需，第二点意味着这个应用就是个 Single Page App，第三点代表着这个应用不会去做播客下载等功能，因为在桌面平台上，我们假设网络是永远可达的。&lt;/p&gt;

&lt;p&gt;有了这几个点子，这个应用该长什么样在脑海里就有个基本的概念了。接下来只需花个两分钟，把一个 iOS 播客应用的截图，放到 macOS menubar 上就行了&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/876920/65824642-373c2f00-e221-11e9-89dd-46afcb8e0c51.png&quot; alt=&quot;PH3@1x&quot; /&gt;&lt;/p&gt;

&lt;p&gt;对于首页而言，这个可以说就是设计稿了。我们觉得它看起来还不错，像那么一回事。接下来就是写个原型，看看如果这个应用跑起来的话，是不是能够解决我们前面提到的那些痛点。&lt;/p&gt;

&lt;h2 id=&quot;原型&quot;&gt;原型&lt;/h2&gt;
&lt;p&gt;既然是要写原型的话，一定是尽可能的使用廉价的方式。这里就不得不提我的情况，我并没有太多的设计经验，不具备专业的设计能力。&lt;/p&gt;

&lt;p&gt;也就是说，这个页面里面，字体是多大，部件的大小、间距、阴影该是多少，我还没有一套科学的方案。这种情况下，要想知道怎么进行布局才最“悦目”，唯一的办法就是不断的试错。所以，在构建原型时，代码/UI 修改一定要尽可能的轻松和快速。&lt;/p&gt;

&lt;p&gt;此外，原型设计工具如果能够尽可能接近我的 mental model，那就再好过了。&lt;/p&gt;

&lt;p&gt;基于以上两点，我决定在 codesandbox 上，使用 Vue.js 来书写第一个原型。完成以下的效果，花了一小时不到。&lt;/p&gt;

&lt;p&gt;&lt;img width=&quot;887&quot; alt=&quot;image&quot; src=&quot;https://user-images.githubusercontent.com/876920/65824657-75395300-e221-11e9-8197-1677fc179acf.png&quot; /&gt;&lt;/p&gt;

&lt;p&gt;核心模板代码也很简单&lt;/p&gt;

&lt;div class=&quot;language-html highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nt&quot;&gt;&amp;lt;template&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;id=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;home&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;input&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;v-model=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;query&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;type=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;search&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;placeholder=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;Search or enter url&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;v-on:change=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;updateQuery&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;spellcheck=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;false&quot;&lt;/span&gt; &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;filters&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;filter&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
        ...
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;filter&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
        ...
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;setting&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;results&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;div&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;result&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;v-for=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;user in podcasts&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;v-bind:key=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;user.id&quot;&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;&amp;gt;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;&amp;lt;router-link&lt;/span&gt;
          &lt;span class=&quot;na&quot;&gt;:to=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;{ name: &apos;Podcast&apos;, params: { id: user.id, podcast: user } }&quot;&lt;/span&gt;
          &lt;span class=&quot;nt&quot;&gt;&amp;gt;&amp;lt;img&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;class=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;artwork&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;:src=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;user.artwork&quot;&lt;/span&gt; &lt;span class=&quot;na&quot;&gt;:title=&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;user.title&quot;&lt;/span&gt;
        &lt;span class=&quot;nt&quot;&gt;/&amp;gt;&amp;lt;/router-link&amp;gt;&lt;/span&gt;
      &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
    &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
  &lt;span class=&quot;nt&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Vue.js 的模版是非常直观的，一个 input，两个 filter，一个 button，和一个 list。借助 Hot Reload，我可以通过修改 CSS，实时地修改最终的渲染效果，找到最佳的图片大小和间距。&lt;/p&gt;

&lt;p&gt;之后我只需创建一个 menubar app，在窗口中放一个 wkwebview，加载这个 codesandbox 页面即可。一个以假乱真的 mCast 原型就实现出来了。从想法，到这个原型的实现，一共也就个把小时。&lt;/p&gt;

&lt;h2 id=&quot;实现&quot;&gt;实现&lt;/h2&gt;
&lt;p&gt;接下来要做的就是产品化了。这里我有两个不同的技术选型，第一个是继续使用 Web 技术，第二个是使用 Native 方案重写。尽管我对 Web 技术的掌握程度比较高，我最终还是选择了后者。因为 Web 方案潜在会带来体验上的损失（好比说毛玻璃背景效果），我情愿在开发过程中多花一些时间。&lt;/p&gt;

&lt;p&gt;但事实上我低估了使用 Native 方案开发的时间成本，投入的大部分时间基本归于以下几类&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;没有 Hot Reload。我的 iOS/macOS 开发经验还不足以支持我光看代码就能够想象出它最终的呈现效果，导致的结果是我频繁的需要修改各类参数，然后等待编译部署然后在 Simulator 中看到结果。&lt;/li&gt;
  &lt;li&gt;Layout。无论是在 Storyboard 中使用 Auto Layout，还是编程的方式调整布局，都远不如 CSS 书写来的直观。&lt;/li&gt;
  &lt;li&gt;代码过于冗余。本来就是非常简单的 List/Collection View，你依然得实现一套 UICollectionView data source 和 delegate，更别说如果要自定义 layout 了。（哪怕可以借助 IDE 的 code snippet 和 auto complete）&lt;/li&gt;
  &lt;li&gt;自定义 View 过于繁琐，无论是 xib 还是完全代码来实现。某次我忘记调整 xib 里 custom view 的 file owner，花了很长的时间才明白为啥 custom view 无法被创建。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;mCast 一共就三个主要页面，但是使用 Web 技术实现一遍的时间，可能是最终 Native 方案的 1/10。当然，这一部分要归功于我对 Web 技术的熟练，但是 Vue/HTML 模版的表达能力，CSS 的布局，以及 Hot Reload 起了决定性的作用。&lt;/p&gt;

&lt;p&gt;当然，能够使用 iPad Pro 随时随地书写 Vue，可能也是一个不大不小的原因。&lt;/p&gt;

&lt;h2 id=&quot;swiftui&quot;&gt;SwiftUI&lt;/h2&gt;
&lt;p&gt;苹果在 19 年 WWDC 上宣布了 SwiftUI，很快就成为了热门话题。虽然有不少类似于 “这不是 xyz 吗” 或者 “声明式 UI xyz 老早以前就支持了” 的论调，但作为成熟的工程师，我们应该关注的是 SwiftUI 实际解决的问题，以及苹果提供的一整套工具类和框架类库等基础设施对于现阶段的开发效率能有怎么样的影响。对此我的观察是&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;声明式 UI 加上界面预览，优于 Storyboard 和现有的纯代码构建方式。&lt;/li&gt;
  &lt;li&gt;Hot Reload 保证代码更改可以被实时预览。大大利好 UI 的调优。&lt;/li&gt;
  &lt;li&gt;可以直接修改 UI ，SwiftUI 代码自动更新。类似于浏览器里的 Web Inspector。&lt;/li&gt;
  &lt;li&gt;SwiftUI 是 platform agnostic ，不用关心底层是 AppKit 还是 UIKit&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;以上几点很好的解决了我在把 Vue.js 原型使用 Native 的痛点。于是我迫不及待地装上 Catalina Beta 和 Xcode 11 尝尝鲜，先看看实现首页的列表复杂度有多少。&lt;/p&gt;

&lt;p&gt;不幸的是，SwiftUI 没有 Collection View。万幸的是我们可以用 List 来间接地实现同样的效果。代码大致如下&lt;/p&gt;

&lt;div class=&quot;language-swift highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;ContentView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
	  &lt;span class=&quot;o&quot;&gt;...&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;some&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kt&quot;&gt;NavigationView&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;kt&quot;&gt;ScrollView&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;vertical&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;showsIndicators&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;kt&quot;&gt;VStack&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                    &lt;span class=&quot;kt&quot;&gt;ForEach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;row&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt;
                        &lt;span class=&quot;kt&quot;&gt;HStack&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                            &lt;span class=&quot;kt&quot;&gt;Spacer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
                            &lt;span class=&quot;kt&quot;&gt;ForEach&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;\&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;col&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;in&lt;/span&gt;
                                &lt;span class=&quot;kt&quot;&gt;BoxView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;box&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;k&quot;&gt;self&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;boxes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;row&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;*&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;3&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;+&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;col&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;padding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
                            &lt;span class=&quot;kt&quot;&gt;Spacer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
                        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
                    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
                &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
                &lt;span class=&quot;kt&quot;&gt;Spacer&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;padding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;navigationBarTitle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;mCast&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;

&lt;span class=&quot;kd&quot;&gt;struct&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;BoxView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;let&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;box&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;Box&lt;/span&gt;

    &lt;span class=&quot;k&quot;&gt;var&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;body&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kd&quot;&gt;some&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;View&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
        &lt;span class=&quot;kt&quot;&gt;ZStack&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
            &lt;span class=&quot;kt&quot;&gt;NavigationLink&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;destination&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;kt&quot;&gt;DetailedPodcastView&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;box&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;box&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
                &lt;span class=&quot;kt&quot;&gt;Image&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;box&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;imageUrl&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;s&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;resizable&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
                    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;cornerRadius&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
                    &lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;frame&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;width&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;80&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nv&quot;&gt;height&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;80&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
            &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;buttonStyle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;PlainButtonStyle&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
        &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
    &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;写起来真的是太简单了，也很容易对 View 进行抽象封装。而实时预览和操作惊为天人&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/876920/65824694-eaa52380-e221-11e9-8e43-322f6973417e.gif&quot; alt=&quot;mcast-swiftui&quot; /&gt;&lt;/p&gt;

&lt;p&gt;如果 SwiftUI 早出来一年的话，我一定会用它来完成原型到产品的全部书写。因为用 SwiftUI 进行想法的尝试，已经足够高效了。&lt;/p&gt;

&lt;h2 id=&quot;小结&quot;&gt;小结&lt;/h2&gt;

&lt;p&gt;从时代的角度来说，苹果推出 SwiftUI 一点也不奇怪，甚至是必然的结果（当然这里我有“马后炮”的嫌疑）&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;声明式 UI 已经有了广泛的群众基础。WPF、React、Flutter 已经花了足够多的时间教育用户，而前端社区的爆发式增长也降低了推广难度。&lt;/li&gt;
  &lt;li&gt;为 iOS/macOS 跨平台提供基础。&lt;/li&gt;
  &lt;li&gt;无论是近些年流行的 Full Stack，还是苹果社区推崇的独立开发，再或是降低设计和开发之间协作鸿沟的需求，都在要求开发者能够完成一定的设计或者原型构建的工作。SwiftUI 无疑是一枚“银弹”。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;现在是学习声明式 UI 的最佳时机，不能再晚了。不过如果要真的在生产环境使用 SwiftUI 的话，建议还是再等个半年。苹果向来是一流的设计（UI、语言、框架均是如此），二流的实现和工具链，现在就强行推 SwiftUI 的话，要踩的坑会比较多。在等待 SwiftUI 的同时，不妨学习一下 Swift 和响应式编程。&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Web 应用快捷键支持（三）：VS Code 快捷键服务的实现</title>
   <link href="http://www.rebornix.com/vscode/2019/09/11/web-keyboard-support-3/"/>
   <updated>2019-09-11T00:00:00+00:00</updated>
   <id>http://www.rebornix.com/vscode/2019/09/11/web-keyboard-support-3</id>
   <content type="html">&lt;p&gt;这篇文章是系列文章《Web 应用快捷键支持》的第三篇。前两篇文章中介绍了如何正确处理浏览器 Keyboard Event。今天我们一起来看看，如何把正确解析的键盘事件，映射到对应的命令上。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Web 应用快捷键支持
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;https://rebornix.com/vscode/2019/08/11/web-keyboard-support/&quot;&gt;（一）正确处理 Keyboard Event&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://rebornix.com/vscode/2019/08/25/web-keyboard-support-2/&quot;&gt;（二）code/key 的缺点和 Node Native Keymap&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://rebornix.com/vscode/2019/09/11/web-keyboard-support-3/&quot;&gt;（三）VS Code 快捷键服务的实现&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;在 VS Code 中，用户有两种方式自定义快捷键。第一种是使用快捷键编辑器，也就是以图形化的方式修改快捷键到命令的绑定。第二种，也是 VS Code 从第一个版本就支持的方式，直接修改 JSON 格式的快捷键定义。一个快捷键到命令的定义，以 JSON 形式的来表示是&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;key&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;cmd+/&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;command&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;editor.action.commentLine&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;when&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;editorTextFocus &amp;amp;&amp;amp; !editorReadonly&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;when&lt;/code&gt;语句里可以组合使用各种 context key ，来决定这个绑定什么时候生效。比如上面的例子里，当用户把光标放到编辑器里，同时编辑器里的文件不是只读的（readonly），那么按下 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cmd+/&lt;/code&gt;就是把当前行的代码注释掉。&lt;/p&gt;

&lt;p&gt;你可以简单的把 VS Code 的快捷键服务存储着上面这种 JSON 格式的快捷键绑定定义。同时，VS Code 在整个 container 上监听 keyboard event，对于每个 keyboard event，在快捷键服务里进行查询，如果能够找到对应的命令，则执行命令，然后 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;preventDefault()&lt;/code&gt;。相信你对此已经比较有经验了，不多加赘述。&lt;/p&gt;

&lt;h2 id=&quot;keycommand-lookup&quot;&gt;Key/Command lookup&lt;/h2&gt;
&lt;p&gt;一个应用程序里的快捷键最多也就上百个，所以数据结构基本不是问题，你只需要两个 Map .&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nx&quot;&gt;_keyToKeybindings&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Keybinding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;;&lt;/span&gt;
&lt;span class=&quot;nl&quot;&gt;_commandTokeybindings&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;Map&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;string&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;nx&quot;&gt;Keybinding&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[]&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;第一个 Map 是从 Keybinding stringify 的结果到 Keybinding 本身的映射。比如&lt;/p&gt;

&lt;p&gt;this&lt;/p&gt;

&lt;p&gt;is&lt;/p&gt;

&lt;p&gt;typewriter&lt;/p&gt;

&lt;p&gt;mode&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;err&quot;&gt;“cmd+/”:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
	&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
		&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;command:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;“editor.action.commentLine”&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
		&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;when:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;“editorTextFocus&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;!editorReadonly”&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
		&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
	&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
	&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
		&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;command:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
	&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
	&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;第二个 Map 则是从 Command Id 到 Keybinding 本身&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;err&quot;&gt;“editor.action.commentLine”:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
	&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;command:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;“editor.action.commentLine”&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
	&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;when:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;“editorTextFocus&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;!editorReadonly”&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
	&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;简单的 Map 就能够避免每次都进行暴力搜索了，而且在这几百个快捷键的 scale下，Map 足够好了。&lt;/p&gt;

&lt;p&gt;同时你也注意到了，第一个 Map 的值是一个 Array。这是因为真实环境下，是会出现 conflicts。比如应用已经使用了一个快捷键组合，然后用户将这个快捷键组合绑定到了另一个命令上。而我们并不希望出现 data loss，所以我们需要将它们都储存起来，然后执行合适的命令。&lt;/p&gt;

&lt;p&gt;那么当快捷键组合出现 conflict 的时候，我们怎么决定哪条命令呢？有两个方法，我们先看简单的一个。&lt;/p&gt;

&lt;h2 id=&quot;ordering&quot;&gt;Ordering&lt;/h2&gt;
&lt;p&gt;最简单的方法就是给每个快捷键添加一个新的属性 order，用于排序。Order 值更高的就拥有更高的优先级。&lt;/p&gt;

&lt;p&gt;VS Code 在使用了这个方法，不过是却是隐式的。VS Code 在启动时，会先注册默认的快捷键，接着是加载插件注册的快捷键，最后加载用户修改的快捷键。然后遵循后注册优先级更高的方式进行覆盖。所以，如果用户将 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cmd+/&lt;/code&gt;绑定给了其他的命令，那么 Toggle Line Comment 就不会被执行了。&lt;/p&gt;

&lt;p&gt;要注意的是，如果要显式的使用 order 属性，只能在内部进行使用，但是不能面向用户，否则就会像 CSS 中的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;z-index&lt;/code&gt; 一样，仍然可能存在 conflict。&lt;/p&gt;

&lt;h2 id=&quot;when&quot;&gt;When&lt;/h2&gt;
&lt;p&gt;第二种方法，就是给快捷键绑定加上限定条件。Toggle Line Comment 只能在编辑器中使用，那么当用户在使用其他模块时，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;cmd+/&lt;/code&gt;理当不被占用才对。应用可以定义好足够多同时通用的 context key，用于描述应用所处的状态，比如&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;editorTextFocus&lt;/code&gt; 当前焦点在编辑器文本上&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;terminalFocus&lt;/code&gt;当前焦点在终端上&lt;/li&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;listFocus&lt;/code&gt;当前焦点在某个 list 上，比如 File Explorer，Search Result View&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;上面的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;listFocus&lt;/code&gt;是一个通用的 context key，VS Code 中有非常多的 list，如果为每个 list 都指定一个 context key，然后绑定上同一套快捷键，那就太麻烦了。&lt;/p&gt;

&lt;h3 id=&quot;context-key-expression&quot;&gt;Context Key Expression&lt;/h3&gt;
&lt;p&gt;有了 context key，接下来就是看如何让用户自组合使用 context key 来精确的指定快捷键绑定。我们依然是由简入繁。&lt;/p&gt;

&lt;p&gt;第一步就是只支持单个 boolean 类型的 context key。那么只需直接拿 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;when&lt;/code&gt;的值进行查询就行了。&lt;/p&gt;

&lt;p&gt;第二步支持单个 string 或者 number 类型的 context key。这一步就需要对 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;when&lt;/code&gt;语句进行解析了，因为我们得在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;when&lt;/code&gt;语句中使用诸如 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;==&lt;/code&gt;， &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;!=&lt;/code&gt;， &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;gt;&lt;/code&gt;， &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;&lt;/code&gt;等等操作符。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&quot;editorLangId == typescript&quot;
&quot;resourceExtname != .js&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;第三步，支持与操作 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;amp;&amp;amp;&lt;/code&gt;和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;||&lt;/code&gt;。在解析这个两个操作符的同时，要注意优先级，通识是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;amp;&amp;amp;&lt;/code&gt;要优先于 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;||&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;有了上面这三步，我们就可以非常准确的描述各种使用场景了。比如 VS Code 的内置终端有一个搜索框，我们想改变&lt;em&gt;跳转到下一个搜索结果&lt;/em&gt;命令的快捷键时，我们就可以使用如下的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;when&lt;/code&gt;条件语句&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&quot;when&quot;: &quot;terminalFocus &amp;amp;&amp;amp; terminalFindWidgetFocus&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;借助描述精确地 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;when&lt;/code&gt; 条件语句，已经可以大大避免 conflict。但是有的时候我们会找到多个快捷键绑定，它们的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;when&lt;/code&gt;都为真，比如下面两个快捷键定义&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;command&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;findNext&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;key&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;enter&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;when&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;terminalFocus &amp;amp;&amp;amp; terminalFindWidgetFocus&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;},&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;command&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;insertNewLine&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;key&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;enter&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;when&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;terminalFocus&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;当用户在内置终端的搜索框里按下 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;enter&lt;/code&gt;时，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;terminalFocus&lt;/code&gt;和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;terminalFocus &amp;amp;&amp;amp; terminalFindWidgetFocus&lt;/code&gt;都为真，那么如何判断该执行哪个命令呢？&lt;/p&gt;

&lt;p&gt;我们应该找出 scope 更小的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;when&lt;/code&gt;条件。由于 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;terminalFocus &amp;amp;&amp;amp; terminalFindWidgetFocus&lt;/code&gt;能够推导出 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;terminalFocus&lt;/code&gt;，但是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;terminalFocus&lt;/code&gt;并不能推出&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;terminalFocus &amp;amp;&amp;amp; terminalFindWidgetFocus&lt;/code&gt;，所以 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;enter&lt;/code&gt;应该为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;findNext&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;完成这样的推导，你可能需要一点数理逻辑的&lt;a href=&quot;https://en.wikipedia.org/wiki/De_Morgan%27s_laws&quot;&gt;知识&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;如果完成以上的推导后，如果匹配的快捷键绑定仍然超过一个，那么这就是正儿八经的 conflict 里，我们可以按照上文提到的 Ordering 方式，选择后注册的快捷键绑定执行对应的命令。&lt;/p&gt;

&lt;h2 id=&quot;小结&quot;&gt;小结&lt;/h2&gt;
&lt;p&gt;这三篇文章介绍了浏览器 keyboard event 的正确处理和如何实现一套快捷键系统，并且支持用户自由地修改快捷键绑定。&lt;/p&gt;

&lt;p&gt;文章里没有讨论的是，当用户按下快捷键，我们查询到对应的命令后，如何在正确的上下文中执行命令。比如上面的例子里，Keybinding Service 在接受到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;enter&lt;/code&gt;事件后，找到了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;findNext&lt;/code&gt;命令并且执行。那么 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;findNext&lt;/code&gt;命令是如何被初始化的，如何访问到当前的 terminal 里的内容，我们没有深入讨论。在 VS Code 中，每个命令都是 stateless 的，然后命令本身通过控制反转（注入和查找都有使用）来获取当前的上下文。&lt;/p&gt;

&lt;p&gt;关于在 Web 应用中实现快捷键服务，你还有别的感兴趣的知识点但是本文没有提及的，欢迎给我留言 ;)&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Web 应用快捷键支持（二）：code/key 的缺点和 Node native keymap</title>
   <link href="http://www.rebornix.com/vscode/2019/08/25/web-keyboard-support-2/"/>
   <updated>2019-08-25T00:00:00+00:00</updated>
   <id>http://www.rebornix.com/vscode/2019/08/25/web-keyboard-support-2</id>
   <content type="html">&lt;p&gt;这篇文章是系列文章《Web 应用快捷键支持》的第二篇。上一篇文章中介绍了处理快捷键常见的方式，以及 keyCode 潜在的问题。而 keyCode 已经被新的标准 code 和 key 取代了，这个新的标准能够解决问题吗？&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Web 应用快捷键支持
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;https://rebornix.com/vscode/2019/08/11/web-keyboard-support/&quot;&gt;（一）正确处理 Keyboard Event&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://rebornix.com/vscode/2019/08/25/web-keyboard-support-2/&quot;&gt;（二）code/key 的缺点和 Node Native Keymap&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://rebornix.com/vscode/2019/09/11/web-keyboard-support-3/&quot;&gt;（三）VS Code 快捷键服务的实现&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;h2 id=&quot;code--key&quot;&gt;code / key&lt;/h2&gt;

&lt;p&gt;还是先看看来 code 和 key 的定义吧。&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;code holds a string that identifies the physical key being pressed. The value is not affected by the current keyboard layout or modifier state, so a particular key will always return the same value.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;code 代表着用户按下的物理键，这个值不会因为用户所使用的键盘布局或者是否按下了 Modifier 影响。挺好。&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;key holds a  &lt;a href=&quot;https://www.w3.org/TR/uievents-key/#key-attribute-value&quot;&gt;key attribute value&lt;/a&gt;  corresponding to the key pressed.&lt;/p&gt;

  &lt;p&gt;A &lt;em&gt;key attribute value&lt;/em&gt; is defined as being a string that contains one of the following:&lt;/p&gt;
  &lt;ul&gt;
    &lt;li&gt;A key string that corresponds to the character typed by the user, taking into account the user’s current locale setting, modifier state, and any system-level keyboard mapping overrides that are in effect.&lt;/li&gt;
    &lt;li&gt;A named key attribute value, as defined by the tables in this document …&lt;/li&gt;
  &lt;/ul&gt;
&lt;/blockquote&gt;

&lt;p&gt;大部分情况下，key 代表着用户按下某个键后生成的字符，这个字符会受用户的键盘布局、语言、是否按下了 Modifier 以及系统级别的键盘映射等等。除此之外还有一些附加条件。&lt;/p&gt;

&lt;p&gt;code 和 key 这两个属性，一个只关心物理按键，一个只关心最终的按键输出结果，比 keyCode 这样模棱两可的属性容易理解多了。不过它们能解决前一篇文章里的问题吗？&lt;/p&gt;

&lt;p&gt;我们再回顾一下我们在德语键盘上遇到的快捷键处理的问题。在 macOS 德语键盘上按下 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ctrl+shift+7&lt;/code&gt; 时，我们得到的 keyboard event 如下&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;{
  keyCode: 191,
  shift: true,
  ctrl: true,
  alt: false,
  meta: false,
  key: ‘/’,
  code: ‘Digit7’
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;用户按下 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ctrl+shift+7&lt;/code&gt; 的目的是为了实现 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ctrl+/&lt;/code&gt;。在不知道用户键盘布局类型的情况下，这个 keyboard event 能告诉我们的是&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;用户按下这几个键能够生成的字符是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;在用户的键盘上，用户实际按下的物理键是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ctrl&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shift&lt;/code&gt;和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;7&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;没了。而如果要推断出用户的目标是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ctrl+/&lt;/code&gt;，缺失的关键信息是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shift+7&lt;/code&gt;能够生成 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;天下果然没有免费的午餐。不过问题还是要解决的，既然浏览器没有给我们提供充足的信息，我们就自己动手吧。&lt;/p&gt;

&lt;h2 id=&quot;node-native-keymap&quot;&gt;Node native keymap&lt;/h2&gt;
&lt;p&gt;VS Code 基于 Electron ，它是一个 Web 应用没错，但同时它也是一个 Native 应用，我们是可以访问操作系统 API 的。我们写了一个 Node native module &lt;a href=&quot;https://github.com/microsoft/node-native-keymap&quot;&gt;GitHub - microsoft/node-native-keymap: Provide OS keyboard layout functionality as a nodejs module&lt;/a&gt;，完成以下工作：&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;检测当前的键盘布局类型&lt;/li&gt;
  &lt;li&gt;监听键盘布局变化&lt;/li&gt;
  &lt;li&gt;获取当前键盘布局下，按下每个键、以及不同的 Modifier 时，能够产生的字符。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;通过使用这个模块，我们就能知道当前键盘是德语键盘，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shift+7&lt;/code&gt;生成 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/&lt;/code&gt;，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ctrl+7&lt;/code&gt;生成的字符是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;7&lt;/code&gt;。因此 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ctrl+shift+7&lt;/code&gt;是可以被简化为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ctrl+/&lt;/code&gt;的。问题解决了。&lt;/p&gt;

&lt;h2 id=&quot;web-环境&quot;&gt;Web 环境&lt;/h2&gt;
&lt;p&gt;VS Code 快捷键的问题算是解决了，但这个方案的缺点就是必须要能够访问操作系统 API。对于纯粹的 Web 环境，这个是不可能的，我们只能想想办法 workaround 了。&lt;/p&gt;

&lt;p&gt;首先，虽然不能够通过访问操作系统获取 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;code + modifier &amp;lt;—&amp;gt; key&lt;/code&gt;的映射关系，但是这个世界上流行的键盘布局毕竟还是有限的。我们可以在主流的键盘布局上运行 node-native-keymap，获取这些映射关系，将它们提前缓存。&lt;/p&gt;

&lt;p&gt;而检测当前的键盘布局类型以及键盘布局变化，可以通过检测用户出发的 keyboard event 来实现。比如当用户在德语键盘里按下 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Y&lt;/code&gt;，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;code&lt;/code&gt; 是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;KeyZ&lt;/code&gt;而 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;key&lt;/code&gt;是&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Y&lt;/code&gt;，我们可以判断这不是标准键盘，然后我们可以通过第一步提前缓存的 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;code + modifier &amp;lt;—&amp;gt; key&lt;/code&gt; 映射里进行一一比对，找出最接近的键盘布局即可。&lt;/p&gt;

&lt;h2 id=&quot;keyboard-map-提案&quot;&gt;Keyboard Map 提案&lt;/h2&gt;
&lt;p&gt;除了上面的这种穷人版键盘布局检测和监听方案以外，我们还可以借用 Chrome 提交的 Keyboard Map 提案，来优化这个步骤。Keyboard Map https://github.com/WICG/keyboard-map 提案的目标，可以理解为简化版的 Node native keymap，且看它的说明：&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Draft specification for an API that returns a mapping table from KeyboardEvent.code values into strings that can be shown to the user to identify that physical key.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Keyboard Map 将提供当前键盘布局下 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;code&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;key&lt;/code&gt; 之间的映射关系。这个 API 提供的信息如下&lt;/p&gt;

&lt;div class=&quot;language-js highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;nb&quot;&gt;navigator&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;keyboard&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;getLayoutMap&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;().&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;then&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&amp;gt;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;
  &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;letkeyofe&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;){&lt;/span&gt;
    &lt;span class=&quot;nx&quot;&gt;console&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;log&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;nx&quot;&gt;key&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]);&lt;/span&gt;
  &lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;});&lt;/span&gt;

&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;KeyE&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;e&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;KeyD&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;d&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Minus&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;-&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;KeyH&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;h&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;KeyZ&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;z&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Equal&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;KeyN&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;n&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;KeyP&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;p&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;BracketRight&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;]&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;BracketLeft&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Digit8&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;8&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Digit9&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;9&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;KeyS&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Semicolon&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;;&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Digit5&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;5&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;KeyQ&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;q&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;KeyO&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;o&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Period&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;Digit6&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;6&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;KeyV&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;v&lt;/span&gt;&lt;span class=&quot;dl&quot;&gt;&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;与此同时，它还会在键盘布局发生变化时提供事件通知。在阅读完它的文档后，我发现使用它有两个大问题和两个小问题&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;首先，这个提案的出发点是游戏。
    &lt;ul&gt;
      &lt;li&gt;它的目标是能够让游戏开发者，能够正确的显示快捷键，比如说，在标准键盘上你可以使用 WASD 来操作方向。在其他键盘布局上，你可能还是会使用这四个物理键，但是它们对应的字符变了。游戏开发者可以通过这个 API 获取到物理键与字符的映射。&lt;/li&gt;
      &lt;li&gt;基于此，这个提案提供 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;code&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;key&lt;/code&gt; 之间的映射，不会考虑 Modifier。无论你是否按下 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shift/ctrl/alt&lt;/code&gt;，都不会对结果有任何的影响。也就是说我们仍然需要使用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;node-native-keymap&lt;/code&gt; 生成各种流行键盘布局的缓存。只不过 Keyboard Map API 能够提高我们匹配键盘布局的准确性，因为我们不再只使用 Keyboard Event 这样单一的信息了。&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;因为还只是提案，所以只能在 Chromium 上使用。&lt;/li&gt;
  &lt;li&gt;这个 API 提供的映射关系，还有一些 bug，也就是映射关系不准确。不过在我们提供 bug report 后，有些已经修复了。&lt;/li&gt;
  &lt;li&gt;监听键盘布局变化的这个 API 还没有实现。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;简言之，由于有上面的局限，Keymap API 能够提供的只是帮助我们确认当前的键盘布局类型。不过这比只能检测 Keyboard Event 前进了一大步了。&lt;/p&gt;

&lt;p&gt;接下来我们希望 Keymap API 能够考虑 Web 开发工具（不限于 Web IDE 和文档工具）的体验，提供&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;code + modifier &amp;lt;—&amp;gt; key&lt;/code&gt;的映射关系。具体的 tracking issue 是 &lt;a href=&quot;https://github.com/WICG/keyboard-map/issues/26&quot;&gt;Broaden the scope of the specification to support web IDEs · Issue #26 · WICG/keyboard-map · GitHub&lt;/a&gt; ，有兴趣的同学可以关注一下。&lt;/p&gt;

&lt;h2 id=&quot;小结&quot;&gt;小结&lt;/h2&gt;

&lt;p&gt;到这里这篇文章介绍了&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;code/key 的用途和局限性&lt;/li&gt;
  &lt;li&gt;Electron 应用如何通过 node-native-keymap 实现准确的快捷键映射&lt;/li&gt;
  &lt;li&gt;纯 Web 应用支持不同键盘布局时可以使用的 API 和其局限&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;当然，知道用户按下了什么键只是快捷键服务的第一步，接下来我们还要找到正确的命令。而这一步的难点则是在于快捷键到命令的映射往往不是一对一的。拿 VS Code 的快捷键定义来举例（JSON 输出）&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;key&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;cmd+/&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;command&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;editor.action.commentLine&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;nl&quot;&gt;&quot;when&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;editorTextFocus &amp;amp;&amp;amp; !editorReadonly&quot;&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;除了快捷键、命令以外，另一个重要的属性就是执行条件 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;when&lt;/code&gt;，只有当 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;when&lt;/code&gt;条件为真时，这个快捷键才会被映射到这个命令上。如何解析 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;when&lt;/code&gt; 条件语句，如何支持 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;amp;&amp;amp;&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;||&lt;/code&gt;和基本的正则表达式语法，咱们下一篇文章继续探讨。&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Web 应用快捷键支持（一）：正确处理 Keyboard Event</title>
   <link href="http://www.rebornix.com/vscode/2019/08/11/web-keyboard-support/"/>
   <updated>2019-08-11T00:00:00+00:00</updated>
   <id>http://www.rebornix.com/vscode/2019/08/11/web-keyboard-support</id>
   <content type="html">&lt;p&gt;这篇文章是系列文章《Web 应用快捷键支持》的第一篇。《Web 应用快捷键支持》将介绍 Web 应用中实现快捷键支持面临的 Web 标准的问题，以及 VS Code 中如何处理快捷键国际化和快捷键服务的实现方法。&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Web 应用快捷键支持
    &lt;ul&gt;
      &lt;li&gt;&lt;a href=&quot;https://rebornix.com/vscode/2019/08/11/web-keyboard-support/&quot;&gt;（一）正确处理 Keyboard Event&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://rebornix.com/vscode/2019/08/25/web-keyboard-support-2/&quot;&gt;（二）code/key 的缺点和 Node Native Keymap&lt;/a&gt;&lt;/li&gt;
      &lt;li&gt;&lt;a href=&quot;https://rebornix.com/vscode/2019/09/11/web-keyboard-support-3/&quot;&gt;（三）VS Code 快捷键服务的实现&lt;/a&gt;&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
&lt;/ul&gt;

&lt;hr /&gt;

&lt;p&gt;转眼 2019 年已经过去了一半，Electron 和 PWA 之间的竞争可以说是逐渐激烈起来了。一方面随着微软的大力投资，以及和社区的每半年一次的 private meetup/conference， Electron 在稳定上已经上了好几个台阶了。而发布周期上，Electron 团队更是在 Chrome 76 发布的同一天，发布了携带了与 Chrome 76 相同的版本 Chromium 的 Electron 6。而说到 PWA （Progressive Web App），谷歌和微软一直以来都是最大的推动者，随着 Edge 转投了 Chromium 阵营，将 PWA 落地替代传统桌面应用则更具有实际意义。&lt;/p&gt;

&lt;p&gt;不管哪个技术最后能成为赢家，我们在桌面端见到更多的基于 Web 技术的应用几乎是板上钉钉了。但是，要想在 Web 应用里实现一套跟传统桌面应用一致的快捷键体验，是比较有挑战的，几乎没有什么应用能够实现完美的快捷键支持（包括 VS Code 😢 ）。这个锅并不能由应用开发者来背，Web 标准的不完整以及不完善的更新才是始作俑者。&lt;/p&gt;

&lt;h2 id=&quot;keyboard-event&quot;&gt;Keyboard Event&lt;/h2&gt;

&lt;p&gt;要实现快捷键支持，第一步要做的就是监听键盘事件并且合适地进行截断。当用户按下某几个键之后，浏览器会发送 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keydown&lt;/code&gt;, &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keypress&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keyup&lt;/code&gt; 等事件。&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keypress&lt;/code&gt;只有在按下的键能够生成字符时才会触发，比如你在键盘上按下 Shift 键时，并不会触发 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keypress&lt;/code&gt; 事件。而且 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keypress&lt;/code&gt; 已经被从标准里移除了，所以正确的做法就是只处理 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keydown&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keyup&lt;/code&gt;  事件。至于这两个事件该使用哪一个，完全取决于应用决定什么时候触发快捷键的执行。VS Code 中使用的是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keydown&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keydown&lt;/code&gt; 事件会携带以下主要信息（例子中我在 US 标准键盘上按下 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ctrl+/&lt;/code&gt;）：&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;keyCode:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;191&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;shift:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;ctrl:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;alt:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;meta:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;key:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;‘/’&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;code:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;‘Slash’&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Keyboard Event 中还包含了 charCode、which、keyIdentifier 等等，它们都已经被从标准移除，而且问题不少。Monaco Editor（VS Code 中的核心编辑器）在就是通过 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keyCode&lt;/code&gt; 和 modifiers (shift, alt, ctrl 和 meta) 来进行快捷键的设置和分发，这也是现在比较流行/主流的做法。&lt;/p&gt;

&lt;p&gt;比如在上面的例子里，Monaco 在启动时，注册了快捷键命令 Toggle Line Comment ，并且指定了快捷键为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Ctrl+/&lt;/code&gt;。在 US 标准键盘上，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/&lt;/code&gt;对应的 keyCode 就是 191，那么这个命令的快捷键可以被简单的记录为&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;keyCode:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;191&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;ctrl:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;alt:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;shift:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;meta:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;那么当用户按下 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Ctrl+/&lt;/code&gt;时，我们只要拿着这个事件对着所有的命令一一比对，就可以找到 Toggle Line Comment 这个命令了。接下来就可以使用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;event.preventDefault()&lt;/code&gt;来截断这个事件，并且处理命令本身了。&lt;/p&gt;

&lt;h2 id=&quot;keycode-有什么问题吗-&quot;&gt;keyCode 有什么问题吗 ？&lt;/h2&gt;

&lt;p&gt;如果 keyCode 很完美的话，这篇文章就可以到此为止了，就像打平即可出线一样，当然是不可能的。&lt;/p&gt;

&lt;p&gt;首先我们来了解 keyCode 的定义 &lt;a href=&quot;https://lists.w3.org/Archives/Public/www-dom/2010JulSep/att-0182/keyCode-spec.html&quot;&gt;w3c&lt;/a&gt; 。如果对看定义没兴趣的话，可以直接看后面的例子。&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;The keyCode for keydown / keyup events is calculated as follows:&lt;/p&gt;

  &lt;p&gt;1	Read the virtual key code from the operating system’s event information, if such information is available.
2	If an Input Method Editor is processing key input and the event is keydown, return 229.&lt;/p&gt;

  &lt;p&gt;3	If input key when pressed without modifiers would insert a numerical character (0-9), return the ASCII code of that numerical character.&lt;/p&gt;

  &lt;p&gt;4	If input key when pressed without modifiers would insert a a lower case character in the a-z alphabetical range, return the ASCII code of the upper case equivalent.&lt;/p&gt;

  &lt;p&gt;5	If the implementation supports a key code conversion table for the operating system and platform, look up the value. If the conversion table specifies an alternate virtual key value for the given input, return the specified value.&lt;/p&gt;

  &lt;p&gt;6	If the key’s function, as determined in an implementation-specific way, corresponds to one of the keys in the ~table of fixed virtual key codes~, return the corresponding key code.&lt;/p&gt;

  &lt;p&gt;7	Return the virtual key code from the operating system.&lt;/p&gt;

  &lt;p&gt;8	If no key code was found, return 0.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;如果你拿上面这八条定义跟 Google 上搜索 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;keyCode&lt;/code&gt;得到的各种结果进行比对的话，你会发现，大部分内容都只描述了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;3&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;4&lt;/code&gt;&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;如果你按下的键能够生成 0-9 数字，或者 a-zA-Z 字母的话，返回数字或者字母所对应的 ASCII 值&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;就这两个规则而言，无论你使用的什么键盘、什么操作系统，都应该是没有歧义的。但是剩下的几条问题可就多了&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;2 如果输入法正在处理键盘输入时，返回 229&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;不是每个浏览器都这么做，不过这个问题到是不大，因为当输入法正在处理输入时，我们一般会把控制权完全交给浏览器&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;5 如果浏览器有一个系统事件到 keyCode 的查询表的话，就从表里面读取
6 如果按下的键的功能 （由浏览器具体实现决定） 刚好与固定 Virtual Key Code 表格中的某条相同的话，就使用这一条所对应的 keyCode&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;已经开始鸡同鸭讲了，keyCode 怎么生成，这两条完全由浏览器实现决定。&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;7 返回操作系统提供的 virtual key code&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;关于操作系统提供的 virtual key code ，Windows 是有非常明确的&lt;a href=&quot;https://docs.microsoft.com/en-us/windows/win32/inputdev/about-keyboard-input&quot;&gt;定义&lt;/a&gt;的。 可惜的是，macOS 和 Linux 上我没有找到具体的文档。&lt;/p&gt;

&lt;p&gt;看到这里你可能已经感觉到问题出在哪里了，以及为什么 keyCode 已经被&lt;strong&gt;从标准里移除&lt;/strong&gt;了。它的值仅在有限情况下是 OS independent 且 Browser independent 的（ASCII 值），其他情况下全看系统或者浏览器是怎么实现的。&lt;/p&gt;

&lt;p&gt;来个实际的例子来解释一下 keyCode 的不可预知性。我们将分别在 Windows / macOS，US / 德语键盘上按下 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Y&lt;/code&gt;，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Z&lt;/code&gt;，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;7&lt;/code&gt;，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Shift+7&lt;/code&gt; 。选这几个键的原因是，US 和德语键盘上这几个键的位置和作用是不同的。&lt;/p&gt;

&lt;p&gt;当我们在操作系统上添加德语输入法，切换到德语输入后，可以查看当前的键盘布局：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://user-images.githubusercontent.com/876920/62841557-807af400-bc5e-11e9-98d3-39624ef0066f.png&quot; alt=&quot;German keyboard layout&quot; /&gt;&lt;/p&gt;

&lt;p&gt;你可以拿上图和你电脑上的物理键盘布局比较。你会发现&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Z&lt;/code&gt; 和 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Y&lt;/code&gt; 的位置变了&lt;/li&gt;
  &lt;li&gt;德语键盘上没有 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/&lt;/code&gt;。而要打出这个键的话，需要按下 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shift+7&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;按下这几个键的结果如下&lt;/p&gt;

&lt;table&gt;
  &lt;tr&gt;
    &lt;td&gt;物理按键&lt;/td&gt;
    &lt;td&gt;US 键盘布局 Windows&lt;/td&gt;
    &lt;td&gt;US 键盘布局 macOS&lt;/td&gt;
    &lt;td&gt;德语键盘布局 Windows&lt;/td&gt;
    &lt;td&gt;德语键盘布局 macOS&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;&lt;kbd&gt;Y&lt;/kbd&gt;&lt;/td&gt;
    &lt;td&gt;&lt;pre&gt;shift: false&lt;br /&gt;keyCode: 89&lt;/pre&gt;&lt;/td&gt;
    &lt;td&gt;&lt;pre&gt;shift: false&lt;br /&gt;keyCode: 89&lt;/pre&gt;&lt;/td&gt;
    &lt;td&gt;&lt;pre&gt;shift: false&lt;br /&gt;keyCode: 90&lt;/pre&gt;&lt;/td&gt;
    &lt;td&gt;&lt;pre&gt;shift: false&lt;br /&gt;keyCode: 90&lt;/pre&gt;&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;&lt;kbd&gt;Z&lt;/kbd&gt;&lt;/td&gt;
    &lt;td&gt;&lt;pre&gt;shift: false&lt;br /&gt;keyCode: 90&lt;/pre&gt;&lt;/td&gt;
    &lt;td&gt;&lt;pre&gt;shift: false&lt;br /&gt;keyCode: 90&lt;/pre&gt;&lt;/td&gt;
    &lt;td&gt;&lt;pre&gt;shift: false&lt;br /&gt;keyCode: 89&lt;/pre&gt;&lt;/td&gt;
    &lt;td&gt;&lt;pre&gt;shift: false&lt;br /&gt;keyCode: 89&lt;/pre&gt;&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;&lt;kbd&gt;7&lt;/kbd&gt;&lt;/td&gt;
    &lt;td&gt;&lt;pre&gt;shift: false&lt;br /&gt;keyCode: 55&lt;/pre&gt;&lt;/td&gt;
    &lt;td&gt;&lt;pre&gt;shift: false&lt;br /&gt;keyCode: 55&lt;/pre&gt;&lt;/td&gt;
    &lt;td&gt;&lt;pre&gt;shift: false&lt;br /&gt;keyCode: 55&lt;/pre&gt;&lt;/td&gt;
    &lt;td&gt;&lt;pre&gt;shift: false&lt;br /&gt;keyCode: 55&lt;/pre&gt;&lt;/td&gt;
  &lt;/tr&gt;
  &lt;tr&gt;
    &lt;td&gt;&lt;kbd&gt;Shift&lt;/kbd&gt;+&lt;kbd&gt;7&lt;/kbd&gt;&lt;/td&gt;
    &lt;td&gt;&lt;pre&gt;shift: true&lt;br /&gt;keyCode: 55&lt;/pre&gt;&lt;/td&gt;
    &lt;td&gt;&lt;pre&gt;shift: true&lt;br /&gt;keyCode: 55&lt;/pre&gt;&lt;/td&gt;
    &lt;td&gt;&lt;pre&gt;shift: true&lt;br /&gt;keyCode: 55&lt;/pre&gt;&lt;/td&gt;
    &lt;td&gt;&lt;pre&gt;shift: true&lt;br /&gt;keyCode: &lt;b&gt;191&lt;/b&gt;&lt;/pre&gt;&lt;/td&gt;
  &lt;/tr&gt;
&lt;/table&gt;

&lt;p&gt;首先前两排，你可以看出，无论是是是用 US 还是德语虚拟键盘，按下的键如果生成 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Y&lt;/code&gt;，那么 keyCode 就是 89，如果生成的是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Z&lt;/code&gt;，那么 keyCode 就是 90。这个结果对应上面 keyCode 生成规则的 3。操作系统对结果也没影响。&lt;/p&gt;

&lt;p&gt;如果按下了 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;7&lt;/code&gt;，无论是哪个键盘上，keyCode 都是 55。&lt;/p&gt;

&lt;p&gt;如果按下 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shift+7&lt;/code&gt;，结果就开始变得奇怪了。首先，在 US 键盘上，keyCode 总是 55，也就是&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;7&lt;/code&gt;的所对应的 ASCII 值，同时 shift 是 true。虽然 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shift+7&lt;/code&gt; 能够生成 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;amp;&lt;/code&gt;，但不满足条件 2 和 3，所以并不能使用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;amp;&lt;/code&gt;的 ASCII 值。&lt;/p&gt;

&lt;p&gt;而在德语键盘上，Windows 系统上，keyCode 55，shift 为 true，跟 US 保持一致。同样的，虽然德语键盘上，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shift+7&lt;/code&gt; 生成 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/&lt;/code&gt;，但是不满足 2 和 3，所以 keyCode 依然按照 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;7&lt;/code&gt; 返回了 55。&lt;/p&gt;

&lt;p&gt;可是 macOS 德语键盘给出的结果居然是&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;keyCode:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;191&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
    &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;shift:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这是上面的例子里，唯一一个不符合预期的。同时 191 是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/&lt;/code&gt; 所对应的 keyCode，由于 shift 仍然是 true，这个结果跟 US 键盘上，按下 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shift+/&lt;/code&gt; 生成的事件是一样的。&lt;/p&gt;

&lt;p&gt;这样的害群之马还有很多，导致如果你只看 keyCode 和 modifiers，你根本不知道用户真正按下的是什么键，也就别说进行快捷键的处理了。&lt;/p&gt;

&lt;p&gt;文章最开始提到的 Toggle Line Comment 命令，在 VS Code / Monaco 中，我们给 macOS 预设的快捷键是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ctrl+/&lt;/code&gt; 。在 US 键盘上按下 Ctrl 和&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/&lt;/code&gt;就行了。但是在德语键盘上，因为没有 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/&lt;/code&gt; 键，用户的直觉会是用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shift+7&lt;/code&gt; 代替 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/&lt;/code&gt;。也就是说用户会按下 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ctrl+shift+7&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;但是我们在 macOS 得到的 Keyboard Event 是&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;keyCode:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;191&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;shift:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;ctrl:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;alt:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;meta:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;key:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;‘/’&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;code:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;‘Digit&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;’&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;我们既可以把它理解为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ctrl+shift+/&lt;/code&gt;，也可以理解为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ctrl+/&lt;/code&gt;。请允许我说一句 what the …&lt;/p&gt;

&lt;h2 id=&quot;keycode-的替代方案&quot;&gt;keyCode 的替代方案&lt;/h2&gt;

&lt;p&gt;keyCode 已经被从标准移除了，这个我们要举双手赞成，因为 keyCode 是有歧义的。用来替代 keyCode 则是两个属性 key 和 code。key 是用于表示按下这几个键将会生成的字符，而 code 则是代表用户在键盘上按下了哪个物理键。前者只关心用户看到的什么，后者只与物理键盘有关，相当于将 keyCode 拆分开了。&lt;/p&gt;

&lt;p&gt;比如在 macOS 德语键盘上按下 Ctrl+Shift+7 时&lt;/p&gt;

&lt;div class=&quot;language-json highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&lt;span class=&quot;p&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;keyCode:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;191&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;shift:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;ctrl:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;true&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;alt:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;meta:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;false&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;key:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;‘/’&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
  &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;code:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;err&quot;&gt;‘Digit&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;7&lt;/span&gt;&lt;span class=&quot;err&quot;&gt;’&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;w&quot;&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;key 是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/&lt;/code&gt;，因为 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shift+7&lt;/code&gt; 能够打出&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;/&lt;/code&gt;。而 code 则是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Digit7&lt;/code&gt;，说明用户按下了&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;7&lt;/code&gt;这个键，结合&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;shift&lt;/code&gt;和&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ctrl&lt;/code&gt;，我们就知道用户同时按下了&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ctrl+shift+7&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;那么通过使用 key 和 code，能够完美实现一套快捷键服务吗？答案是不能。而 key/code 属性存在的问题，以及 VS Code 中我们是如何解决这些问题的，我们下一篇文章继续聊。&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Notion 勉强合格的数字化手账</title>
   <link href="http://www.rebornix.com/vscode/2019/02/25/Notion/"/>
   <updated>2019-02-25T00:00:00+00:00</updated>
   <id>http://www.rebornix.com/vscode/2019/02/25/Notion</id>
   <content type="html">&lt;p&gt;在克服了种种困难后，我终于开始流畅使用 Notion 了，并且成功说服了我们组（VS Code）的 PM 也迁移到了 Notion 上。&lt;/p&gt;

&lt;p&gt;&lt;em&gt;本文假想你已经听说过或者尝试过 Notion （一个目标将 Todo、Calendar 和 Notes 集于一体的应用）&lt;/em&gt;&lt;/p&gt;

&lt;h2 id=&quot;困难重重&quot;&gt;困难重重&lt;/h2&gt;
&lt;p&gt;Notion 已经上线好几年了，作为一个小团队，没有拿风投而是自负盈亏，可以说是非常值得赞赏的。但是小团队的问题是，如果你的使用习惯跟他们的 top priority 不能重合，那么要想用的顺手，往往需要等待较长的时间，甚至可能需要改变自己。&lt;/p&gt;

&lt;p&gt;比如我于去年 11 月购入最新款 iPad Pro 11 寸，由于是全新的屏幕尺寸，Notion 并没有成功地进行 Auto Layout。我等待了将近三个月才等到了他们的更新，而在这之前我无法使用 Notion，因为 iPad Pro 是我在非工作之余的主要输入设备。&lt;/p&gt;

&lt;p&gt;其他几个我使用频繁的应用对 iPad Pro 11 做出支持的时间如下&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Bear 从第一天开始就完美支持&lt;/li&gt;
  &lt;li&gt;Telegram 从第一天开始就完美支持&lt;/li&gt;
  &lt;li&gt;Tweetbot 从第一天开始就完美支持&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://itunes.apple.com/cn/app/qi-dian/id947792507&quot;&gt;‎奇点 - 轻轻松松刷微博”&lt;/a&gt; 等待一周。在我拿到机器后一周，图拉鼎就发布了&lt;a href=&quot;https://www.weibo.com/1846569133/H4hABfiEt?filter=hot&amp;amp;root_comment_id=0&amp;amp;type=comment#_rnd1551123038413&quot;&gt;新版本以支持新的分辨率&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;NBC Sports 等待一个月&lt;/li&gt;
  &lt;li&gt;Google Map 等待三个月&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;对新 iPad Pro 的适配的速度跟公司大小或者团队大小没有什么直接的关系，但即使适配速度比较慢，也没什么好指责的，只不过当打开 Bear 的时候发现没有任何问题，我感慨这 1.5 刀一个月真值。&lt;/p&gt;

&lt;p&gt;除了上面这个非常小众的困难以外，另一个阻挠我上车的原因是 Notion 真的太难用了。这本身不是 Notion 的锅，一个支持模板且可选元素丰富的 WISWIG 编辑器，是不可能好用到哪里去的。Notion 在移动设备上（包括 iPad Pro）定制模板的体验也是灾难级别的，这导致我几乎只愿意在桌面端进行复杂的操作。&lt;/p&gt;

&lt;h2 id=&quot;为什么要迎难而上&quot;&gt;为什么要迎难而上&lt;/h2&gt;
&lt;p&gt;Notion 吸引我的地方是可以自己创建模板。作为 Todolist、Bear 和 Calendars 5 的用户，我的第一想法就是找到这三个独立工具所缺失的功能&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;对我的整个工作、任务安排和知识储备并没有一个清晰的概览。我没办法快速的翻看我最近的工作进展，完成了哪些内容等等。&lt;/li&gt;
  &lt;li&gt;很多时候我需要快速记录内容，然后再将它们重新组织分配到几个应用中。现阶段充当缓冲区的是 Onenote。&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;对于工作和生活概览，Notion 的 “Personal Home” 模板已经能够达成了目的了，我可以将它当做全年计划和工作记录的 landing page&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/rebornix/rebornix.github.io/master/_posts/images/personal-home-template.png&quot; style=&quot;width: 100%;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;而 Notion 的另一个模板 “Weekly Agenda” 则很好的完成了周记的功能。我们组每天都会交流今天各自做了什么，每周还会跟全组汇报一周工作进展，周记能够很好地完成整理工作记录的功能。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/rebornix/rebornix.github.io/master/_posts/images/weekly-agenda.png&quot; style=&quot;width: 100%;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;在将一个个模板和命令（command）尝试过去后，我暂时在 Personal Home 中创建了以下几个页面&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;Issue Grooming 记录 VSCode 中指派给我的各种 issue&lt;/li&gt;
  &lt;li&gt;Standup 也就是我的周记&lt;/li&gt;
  &lt;li&gt;Better Naming 是我在 Telegram 上的一个 &lt;a href=&quot;https://t.me/rebornix&quot;&gt;Channel&lt;/a&gt;。今年我会坚持每周分享一则我自己学习到的知识和感悟。&lt;/li&gt;
  &lt;li&gt;最后就是年度计划和旅行计划&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;呈现的效果如下&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Personal Home&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/rebornix/rebornix.github.io/master/_posts/images/rebornix-personal-home.png&quot; style=&quot;width: 100%;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Weekly Report 列表&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/rebornix/rebornix.github.io/master/_posts/images/rebornix-standup.png&quot; style=&quot;width: 100%;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Weekly Report 明细&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/rebornix/rebornix.github.io/master/_posts/images/rebornix-standup-details.png&quot; style=&quot;width: 100%;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Better Naming backlog&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/rebornix/rebornix.github.io/master/_posts/images/better-naming-details.png&quot; style=&quot;width: 100%;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;自此，我每天工作的第一件事情，就是打开 Notion 的 Personal Home 界面 。&lt;/p&gt;

&lt;p&gt;而针对 “快速记录内容” 这个痛点，就轮到 Notion 的模板转换功能发挥功效了。简单来说，你可以将一个内容的类型，在不同的模板之间转换，比如 Bullet List 转 Todo List，Text 转 Sub Page。这样一来，当我完成速记之后，就可以直接在 Notion 中，将确定下来的工作，转换为 Todo List。在工作的工程中，又可以将之前简单以文字记录的想法，转换成一个单独的页面进行深度拓展。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/rebornix/rebornix.github.io/master/_posts/images/notion-transformation.png&quot; style=&quot;width: 100%;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;举个切实的例子。在计划每个月的工作时，我会自己准备好一个列表，包含以下几种内容&lt;/p&gt;

&lt;ul&gt;
  &lt;li&gt;VS Code
    &lt;ul&gt;
      &lt;li&gt;Feature Request list&lt;/li&gt;
      &lt;li&gt;User feedback&lt;/li&gt;
    &lt;/ul&gt;
  &lt;/li&gt;
  &lt;li&gt;GitHub for VS Code&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://github.com/Microsoft/vscode/wiki/Issue-Tracking&quot;&gt;Inbox Tracking&lt;/a&gt; duty&lt;/li&gt;
  &lt;li&gt;Vacation Plan&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;然后跟老板交流，看看哪些项目应该先做，哪些的 priority 低一些，同时也会对一些任务做一些技术上的讨论。在讨论结束后，我就直接在 Notion 中对文案进行相应的模板转换。&lt;/p&gt;

&lt;h2 id=&quot;为什么-notion-可能会适合我&quot;&gt;为什么 Notion 可能会适合我&lt;/h2&gt;
&lt;p&gt;我本人对 &lt;strong&gt;All-in-one workspace&lt;/strong&gt; 是很不感冒的，像 Bear 这种简单粗暴的应用反而更受我青睐，拿起来就能写，要记住的语法也就那么几个，任何时候 Bear 都不会给你意外和阻挠。而 Notion 这种功能过于强大，单纯的拿来记笔记时非常容易分心（此处我是不是该用 todo list？这里是不是可以插入一个图片？再或者我是不是应该自定义一个 table？）。&lt;/p&gt;

&lt;p&gt;但人总是很矛盾的。Bear 和 Todoist 都是约束性非常强的应用，我往往没办法做到每天坚持使用下去。比如我今年打算每周都在 &lt;a href=&quot;https://t.me/rebornix&quot;&gt;Better Naming&lt;/a&gt; 上做分享，我会一有想法就先记录下来，反复思考和修订，然后再分享出来。听起来我可以将这个列表放在 Todoist 里面管理，但是当我要加一些备注（比如链接、图片和文字），Todoist 就差那么点意思。以前我往往会弃 Todoist 而转而使用 Onenote 或者实体笔记本，而使用后者的后果就是到最后我也不知道东西记到哪里去了 …&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://raw.githubusercontent.com/rebornix/rebornix.github.io/master/_posts/images/better-naming-details.png&quot; style=&quot;width: 100%;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;而像上图这种，通过自己定义模板加强了一定的约束、但是又不失灵活性的记录方式，应该算是对手账的一种不错的数字化实践了。&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>One Dev Minute - VSCode</title>
   <link href="http://www.rebornix.com/vscode/2018/12/21/One-Dev-Minute-VSCode/"/>
   <updated>2018-12-21T00:00:00+00:00</updated>
   <id>http://www.rebornix.com/vscode/2018/12/21/One-Dev-Minute-VSCode</id>
   <content type="html">&lt;p&gt;如果谈到微软的技术，尤其是 Windows 相关的知识，相信大家的第一反应依然是 &lt;em&gt;未知&lt;/em&gt;/&lt;em&gt;神秘&lt;/em&gt;/&lt;em&gt;封闭&lt;/em&gt; 等等，不过 Windows 的朋友搞了个 Studio ，专门制作了一系列的视频，解释 Windows 内部的技术。&lt;/p&gt;

&lt;p&gt;这个系列叫做 One Dev Minute https://channel9.msdn.com/Blogs/One-Dev-Minute ，所有参与录制的开发人员中，我最喜欢的当属 Raymend Chen ，不仅能把技术讲的非常明白，而且有着独特的幽默感，还顺带附送当年 Windows 组的各种八卦，值得一看。&lt;/p&gt;

&lt;p&gt;如果你看到这里还没有关掉页面，那你就好运了 ;) 他们最近邀请我们团队在 One Dev Minute 做了一系列关于 VS Code 的短视频，主要讲了讲 VS Code 的历史、技术框架、插件等相关的故事。其中三个短视频是我去录的&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;a href=&quot;https://channel9.msdn.com/Blogs/One-Dev-Minute/One-Dev-Question-How-did-VSCode-come-together&quot;&gt;How did VSCode come together?&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://channel9.msdn.com/Blogs/One-Dev-Minute/One-Dev-Question-What-makes-a-great-extension-for-VSCode&quot;&gt;What makes a great extension for VSCode?&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;https://channel9.msdn.com/Blogs/One-Dev-Minute/One-Dev-Question-How-does-an-external-debugger-work-with-VSCode&quot;&gt;How does an external debugger work with VSCode?&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;第一次在正儿八经的 Producer 指导下录视频，还是挺紧张的，偶尔口齿不清，不过好在有字幕组 ;)&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>知乎专栏 Better Naming: 基于 VSCode/Monaco 的 iOS 编辑器（一）</title>
   <link href="http://www.rebornix.com/editor/2017/06/25/Monaco-iOS/"/>
   <updated>2017-06-25T00:00:00+00:00</updated>
   <id>http://www.rebornix.com/editor/2017/06/25/Monaco-iOS</id>
   <content type="html">
</content>
 </entry>
 
 <entry>
   <title>维护一个大型开源项目 Visual Studio Code 是怎样的体验</title>
   <link href="http://www.rebornix.com/vscode/2017/04/24/Maintaining-Large-OSS-Project/"/>
   <updated>2017-04-24T00:00:00+00:00</updated>
   <id>http://www.rebornix.com/vscode/2017/04/24/Maintaining-Large-OSS-Project</id>
   <content type="html">
</content>
 </entry>
 
 <entry>
   <title>Toggle any setting in VS Code using keyboard shortcut arguments</title>
   <link href="http://www.rebornix.com/vscode/2017/04/11/ToggleSettings/"/>
   <updated>2017-04-11T00:00:00+00:00</updated>
   <id>http://www.rebornix.com/vscode/2017/04/11/ToggleSettings</id>
   <content type="html">
</content>
 </entry>
 
 <entry>
   <title>Share snippets with your team in VS Code</title>
   <link href="http://www.rebornix.com/vscode/2017/03/17/ProjectLevelSnippet/"/>
   <updated>2017-03-17T00:00:00+00:00</updated>
   <id>http://www.rebornix.com/vscode/2017/03/17/ProjectLevelSnippet</id>
   <content type="html">
</content>
 </entry>
 
 <entry>
   <title>Visual Studio Code 1.7 发布为何会影响 NPM 服务</title>
   <link href="http://www.rebornix.com/vscode/2016/11/12/CodeLoveNPM/"/>
   <updated>2016-11-12T00:00:00+00:00</updated>
   <id>http://www.rebornix.com/vscode/2016/11/12/CodeLoveNPM</id>
   <content type="html">&lt;p&gt;十天之前（11 月 2 日）Visual Studio Code 发布十月 Iteration （1.7）的更新，但三个小时后之后 1.7.0 便被从服务器上拿下，用户收到 roll back 回 1.6.1 的更新通知。第二天（11 月 3 日）我们便发布了 VS Code 1.7.1，并且发表了一个&lt;a href=&quot;http://code.visualstudio.com/blogs/2016/11/3/rollback&quot;&gt;申明&lt;/a&gt;，详细解释了此次事故的来龙去脉，以及为何此次事故会牵扯到 NPM。&lt;/p&gt;

&lt;p&gt;写不下去了，以上字正腔圆的“官方”解释是我的极限了 :( 我还是来通俗地简单地解释一下当时发生了什么。&lt;/p&gt;

&lt;h2 id=&quot;automatic-type-acquisition&quot;&gt;Automatic Type Acquisition&lt;/h2&gt;
&lt;p&gt;在 VS Code 中，我们借助于 TypeScript 的语言支持来实现 JavaScript 的 Intellisense，而 TypeScript 是使用类型申明文件（typings）来达到这个功能的。在 1.7 之前，用户需要手动（当然是肯定可以用脚本代劳的）为自己引用的 npm package 下载 typings 文件到本地从而激活 VS Code 里的 Intellisense。&lt;/p&gt;

&lt;p&gt;在 1.7 中，我们试图把这个过程自动化，即 Automatic Type Acquisition ，自动地为用户写在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;package.json&lt;/code&gt; 的 npm package 下载 typings 文件。由于现在 typings 文件都存放在 npm 的 &lt;a href=&quot;https://www.npmjs.com/~types&quot;&gt;typigns scope&lt;/a&gt; 下，所以当用户用 1.7 版本的 VS Code 打开某个 node project，这个 feature 将会发送若干个请求给到 npm registry 来查询 typings 文件并下载。&lt;/p&gt;

&lt;h2 id=&quot;问题来了&quot;&gt;问题来了&lt;/h2&gt;
&lt;p&gt;当时每个得到更新的 VS Code 打开 node project 时都会给 npm registry 发送 query 请求，以确定是否有 typings 供本地下载。这里我们得承认的是，并不是所有 JavaScript 的 npm package 都是有 typings 文件的（否则年底工资涨一万倍）。因此 npm 的 CDN 满地打滚无数次命中 404 ，进而间接影响了 npm 的 availability。&lt;/p&gt;

&lt;h2 id=&quot;修复&quot;&gt;修复&lt;/h2&gt;
&lt;p&gt;我们在十一月三号发布了 1.7.1 ，暂时把 ATA 这个功能禁用。这只是一个暂时的 roll back，ATA 是一定会有的，请大家耐心等待，1.7.2 即将到来, stay Connected.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>了解 Angular/React/Vue.js 必听的六个播客单集</title>
   <link href="http://www.rebornix.com/frontend/2016/01/13/episodes-4-ng-react-vuejs/"/>
   <updated>2016-01-13T00:00:00+00:00</updated>
   <id>http://www.rebornix.com/frontend/2016/01/13/episodes-4-ng-react-vuejs</id>
   <content type="html">&lt;p&gt;如果说2014年 Angular 几乎一枝独秀，2015年就真的算是百花齐放了。Angular 2 基本成型，React 红遍大江南北，Vue.js 异军突起。如果你还没来得及前端如此深度的脚步，不妨先听一听以下几个中英文混杂的播客单季，准保你对时下前端届的流行趋势和发展有足够的了解。&lt;/p&gt;

&lt;h2 id=&quot;teahour-51-期-interview-with-ari-lerner-about-angularjs&quot;&gt;Teahour 51 期 &lt;a href=&quot;http://teahour.fm/2014/04/21/interview-with-ari-lerner.html&quot;&gt;Interview with Ari Lerner about AngularJS&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;国内最有干货的中文技术博客 Teahour 在 14 年邀请到了 ngbook (中文译本为 《Angular权威教程》) 的作者 Ari Lerner 。从 Backbone，Ember，Knockout，最后聊到 Angular。可以说是全方位覆盖，了解 Angular 发展史以及包袱的上乘之作。&lt;/p&gt;

&lt;audio class=&quot;episode-player&quot; controls=&quot;&quot; preload=&quot;meta&quot;&gt;
  &lt;source src=&quot;http://screencasts.b0.upaiyun.com/podcasts/teahour_episode_51.m4a&quot; /&gt;
&lt;/audio&gt;

&lt;h2 id=&quot;内核恐慌-10-期-reactjs-研讨会专题&quot;&gt;内核恐慌 10 期 &lt;a href=&quot;http://ipn.li/kernelpanic/10/&quot;&gt;React.js 研讨会专题&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;国内另一档技术播客内核恐慌，在Willow（柳成荫）参加完 React.js Conf 2015 后邀请他一起聊React.js, Flux, JSX等相关的话题。&lt;/p&gt;

&lt;audio class=&quot;episode-player&quot; controls=&quot;&quot; preload=&quot;meta&quot;&gt;
  &lt;source src=&quot;https://ipn.c.zgslb.net/kernelpanic/kernelpanic-ep10.mp3&quot; /&gt;
&lt;/audio&gt;

&lt;h2 id=&quot;changelog-140-期-aurelia-durandal-and-leaving-angularjs-with-rob-eisenberg&quot;&gt;Changelog 140 期 &lt;a href=&quot;http://5by5.tv/changelog/140&quot;&gt;Aurelia, Durandal, and leaving AngularJS with Rob Eisenberg&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;5by5 知名技术播客 Changelog 采访了原 AngularJS 组的成员 Rob Eisenberg。Rob 大神离开 Angular 组单飞去做了 Aurelia，这里面有什么恩怨情仇或者技术分歧，值得一听。&lt;/p&gt;

&lt;audio class=&quot;episode-player&quot; controls=&quot;&quot; preload=&quot;meta&quot;&gt;
  &lt;source src=&quot;http://fdlyr.co/d/changelog/cdn.5by5.tv/audio/broadcasts/changelog/2015/changelog-140.mp3&quot; /&gt;
&lt;/audio&gt;

&lt;h2 id=&quot;teahour-78-期-和-vuejs-框架的作者聊聊前端框架开发背后的故事&quot;&gt;Teahour 78 期 &lt;a href=&quot;http://teahour.fm/2015/08/16/vuejs-creator-evan-you.html&quot;&gt;和 Vue.js 框架的作者聊聊前端框架开发背后的故事&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;15年很多人听说了 Vue.js ，更多人听说的是 Vue.js 是一个中国工程师开发的独立项目。Teahour 在最佳的时间邀请到了 Vue.js 的作者尤小右，从读大学到 Google 工作时创造出 Vue.js，让你一窥 10x 程序员的战斗力。同时作为足以匹敌 Angular 和 React 框架的 Vuejs 作者，尤小右同学对各种前端框架和技术都有深入了解，帮助你进行下一次的前端技术选型。&lt;/p&gt;

&lt;audio class=&quot;episode-player&quot; controls=&quot;&quot; preload=&quot;meta&quot;&gt;
  &lt;source src=&quot;http://screencasts.b0.upaiyun.com/podcasts/teahour_episode_78.m4a&quot; /&gt;
&lt;/audio&gt;

&lt;h2 id=&quot;changelog-184-期-discussing-vuejs-and-personal-projects-with-evan-you&quot;&gt;Changelog 184 期 &lt;a href=&quot;https://changelog.com/184/&quot;&gt;Discussing Vue.js and Personal Projects With Evan You&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;尤小右同学 Changelog 录的一期，全程英文。内容上和在 Teahour 上分享的不会有太大的出入，不过足以带你感受 Evan You 的“高大上”。听一听这期，你会对去年年底微博前端届的撕x事件心领神会。&lt;/p&gt;

&lt;audio class=&quot;episode-player&quot; controls=&quot;&quot; preload=&quot;meta&quot;&gt;
  &lt;source src=&quot;http://fdlyr.co/d/changelog/cdn.5by5.tv/audio/broadcasts/changelog/2015/changelog-184.mp3&quot; /&gt;
&lt;/audio&gt;

&lt;hr /&gt;

&lt;p&gt;以上六集播客，中英文各三个，以 Evan You 的最为出彩和接地气（个人评价），而且小右谈了很多 Angular 和 React 的利弊，属于不可多得的干货。16 年一开始，中文技术播客界就新添了一名新丁 &lt;a href=&quot;http://jsnext.fm/&quot;&gt;JSNext&lt;/a&gt;，想进一步深入了解前端的同学可以试着听听看。&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>有点软文：Anders Hejlsberg 和 Erich Gamma</title>
   <link href="http://www.rebornix.com/snippets/2015/11/20/minisoft-anders-erich/"/>
   <updated>2015-11-20T00:00:00+00:00</updated>
   <id>http://www.rebornix.com/snippets/2015/11/20/minisoft-anders-erich</id>
   <content type="html">&lt;p&gt;大概半年前，我写了一篇名叫&lt;a href=&quot;/snippets/2015/04/14/minisoft/&quot;&gt;《有点软文》&lt;/a&gt;的文章，深情并茂地告诉大家，我司其实隐藏着很多牛人巨擘。有些人是身怀屠龙技，但是大家不认识；有些朋友则是声名远播，但可惜的是大家根本不知道他在微软。在看完 &lt;a href=&quot;https://connect2015.visualstudio.com&quot;&gt;Connect&lt;/a&gt; ( Visual Studio 大会 )之后，我决定克服拖延症，来讲一讲大会上除了 Scott Gu （Asp.Net 之父）以外的两个天王巨星。&lt;/p&gt;

&lt;p&gt;那就是 Anders Hejlsberg 和 Erich Gamma&lt;/p&gt;

&lt;h1 id=&quot;anders-hejlsberg&quot;&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Anders_Hejlsberg&quot;&gt;Anders Hejlsberg&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://upload.wikimedia.org/wikipedia/commons/thumb/e/ef/Anders_Hejlsberg.jpg/440px-Anders_Hejlsberg.jpg&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;Anders 出席本次 Connect 大会，主要还是介绍他设计的 TypeScript 。那么谁是 Anders？&lt;/p&gt;

&lt;p&gt;Anders 早年写了Compass Pascal编译器的 core，后来卖给了 Borland，也就开始为 Borland 打工。之后一直是 Turbo Pascal 的架构师。离开 Borland 之前，Anders 设计了 Delphi 语言。&lt;/p&gt;

&lt;p&gt;96年，Anders 被 Bill Gates 亲自挖到了微软，给予了丰厚的薪水（百万美金，那还是20世纪哎我去）以及极大的权力。后果就是 Borland 和微软要死要活地打官司，控诉微软不正当拐骗，最后还真的赢了官司。之后 Anders 在微软主持开发 Visual J++，结果没几年就与 Sun 在 Java 的问题上发生了末日之战，不幸的是 Visual J++ 最终停止了开发（这哥们真是走哪儿官司打到哪儿）。随后 Anders 开始主持 .NET 的设计与开发，并且担任 C# 语言的首席架构师。&lt;/p&gt;

&lt;p&gt;2012 年，Anders 宣布了他设计的新语言 TypeScript，这门语言是 JavaScript 的超集同时具备静态类型。TypeScript 虽然很年幼且有着浓浓的&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;软&lt;/code&gt;味，但是社区已经开始展现对它的兴趣，比如 Angular 2 就是用 TypeScript 写的。&lt;/p&gt;

&lt;p&gt;简言之，如果你是微软平台上的开发者，你几乎无法和 Anders 脱离关系。而如果你是 Angular 的开发者，不久的将来，你也会感受到 Anders 的荣光。这哥们就是语言设计专业户。&lt;/p&gt;

&lt;h1 id=&quot;erich-gamma&quot;&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Erich_Gamma&quot;&gt;Erich Gamma&lt;/a&gt;&lt;/h1&gt;
&lt;p&gt;&lt;img src=&quot;https://pbs.twimg.com/profile_images/66432812/eg2.gif&quot; alt=&quot;&quot; /&gt;&lt;/p&gt;

&lt;p&gt;我们知道 Anders 醉心于语言设计，而 Erich 就是软件开发的实践派。每每谈到软件开发，大家就不可避免地提及设计模式。而将设计模式从建筑行业引入到软件领域的，就是 Erich Gamma（当然，还有另外三个小伙伴，他们的统称十分霸气，Gang of Four，四人 帮）。Erich 91年博士毕业后就来到美国，与三个小伙伴一起布道设计模式。&lt;/p&gt;

&lt;p&gt;&lt;em&gt;注解&lt;/em&gt; ：经网友提醒，这里多讲点背景故事。Erich Gamma 在加入微软之前，和 Kent Beck 一起合作开发了单元测试框架 JUnit ，之后又领导了 Eclipse Java Development Tools 项目，真的是 IBM 的一个瑰宝啊。&lt;/p&gt;

&lt;p&gt;有些工程师朋友会觉得设计模式不过是纸上谈兵，这里我们按下不表。Erich Gamma 在11年加入了 Visual Studio Team，开始在瑞士苏黎世独自带领团队（Erich 是瑞士人，我司为了大牛也真是大手笔）。加入 Visual Studio Team 之后，Erich 开始亲自操刀，用 JavaScript 实现的编辑器 Monaco。在 Visual Studio Online，Onedrive（线上），Office 365 中广泛应用。&lt;/p&gt;

&lt;p&gt;我刚加入微软的时候，做的项目中就用了 Monaco，刚开始不知天高地厚，发现 Monaco 的 bug 后还嚷嚷着要去爆他们。有一次内部 Training，我们组也不知道怎么就请到了 Erich Gamma，他跟我们介绍如何利用 TypeScript （和 Anders 老兄产生了交集）重构 Monaco，把代码量从十万行硬是降到了两万以下。当时我都没好好听，觉得这又是一个爱忽悠的哥们。后来某次翻看设计模式的书，突然发现我天天想着要去爆的人，居然就是大名鼎鼎的 Erich。&lt;/p&gt;

&lt;p&gt;不过凭良心讲，在没有意识到 Monaco 是 Erich 写的时候，我就已经深感 Monaco 比 Ace或者 CodeMirror 性能好的多。只不过由于只在内部使用且未开源，Monaco 并未声名远播。&lt;/p&gt;

&lt;p&gt;直到，Erich Gamma 把 Monaco 用 Electron （跨平台包装工）包装了一下，瞬间变为多平台的编辑器 Visual Studio Code。刚发布的时候，很多朋友都以为这和 GitHub Atom 没区别，但下载使用后却发现性能比 Atom 好出很多。都是使用 Electron 实现跨平台，而编辑器本身，Monaco 暂时略甚一筹。希望切身实际地学习和领悟设计模式的最佳实践，快去看 Visual Studio Code 的&lt;a href=&quot;https://github.com/Microsoft/vscode&quot;&gt;源码&lt;/a&gt;吧！&lt;/p&gt;

&lt;p&gt;#看到这里的朋友都是真爱，我决定透露一个我的惊人发现
从软文的角度，本文到这里其实就可以结束了。但是为了我的忠实听众们，我决定来讲一个 Google 的项目：Angular 2。&lt;/p&gt;

&lt;p&gt;AngularJS 在经历了大火之后，Google 的朋友们发现它的缺陷过于明显（暂时不展开，同学们可以参考我以前的几篇博客），于是决定重构。而在重构的过程中，Google 的 Angular team 和微软一起合作，研究新版本 Angular 的架构的各种可行性，最终选择了使用 TypeScript 作为开发语言。而他们使用的开发工具则是 Visual Studio Code。&lt;/p&gt;

&lt;p&gt;你们想想，一群 Google 的天才，有 Anders 设计的 TypeScript 和 Erich Gamma 开发的 VS Code 保证代码质量和开发效率，Angular 2 要好成什么样你说！&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>升级 Jekyll 3 之前你一定要注意的一件事情</title>
   <link href="http://www.rebornix.com/engineering/2015/11/16/Jekyll3Breaks/"/>
   <updated>2015-11-16T00:00:00+00:00</updated>
   <id>http://www.rebornix.com/engineering/2015/11/16/Jekyll3Breaks</id>
   <content type="html">&lt;p&gt;Jekyll 在不久之前发布了 &lt;a href=&quot;http://jekyllrb.com/news/2015/10/26/jekyll-3-0-released/&quot;&gt;3.0&lt;/a&gt;，读完整个 release notes后，我并未发现任何潜在的风险，于是我果断跑到我的服务器上（没错，我自己托管了 Jekyll 服务而不是使用 GitHub Page，为了逼格，为了 &lt;a href=&quot;https://rebornix.com/ssl/2015/03/25/enablehttps/&quot;&gt;https&lt;/a&gt;）升级了 Jekyll。&lt;/p&gt;

&lt;p&gt;##接着惨绝人寰的悲剧发生了
升级后，我访问了我的博客，首页一切完美如初。然后我试着打开某篇我自认为写的还不错的文章，拉到页面底端，打算再次回味一下粉丝们的评论。wat，怎么评论都没了！！！辛辛苦苦写软文骗来的粉丝和眼球，就这么消失了！！！&lt;/p&gt;

&lt;p&gt;我第一时间看了下浏览器地址栏，holly f**k，我的精心设计的大小写交错的 url 已经全部变成了小写。我迅速滚回了2.5.3，坐和放宽了一会会儿，小站又恢复了平静。&lt;/p&gt;

&lt;p&gt;##变成小写究竟意味着什么
首先，即使你不是处女座，你的内心也会因为这样一个不小的变化感到内心煎熬，不是吗？为什么要改变我的 url 呢。&lt;/p&gt;

&lt;p&gt;然后是正经的，我们曾经发布的博客的 url，已经出现在各种 RSS、Twitter、Google 的索引等等之中，一旦 url 发生改变，之前的所有链接都失效了。我不确定谷歌的索引是否大小写敏感，但是我确定是，feedly 中我的文章的性感度（hot！）已经被清零了，twitter上 ifttt 自动发布的更新消息也无法成功的打开。&lt;/p&gt;

&lt;p&gt;也就是说，这是一个 &lt;strong&gt;breaking change&lt;/strong&gt;，然而这并没有出现在 Jekyll 3.0 的 &lt;a href=&quot;http://jekyllrb.com/news/2015/10/26/jekyll-3-0-released/&quot;&gt;release notes&lt;/a&gt; 中。&lt;/p&gt;

&lt;p&gt;##那个第五分钟出现的红裙子小妹妹就是凶手！
为了找出问题的真凶，我立刻到 GitHub 上查看 Jekyll 的源码。经过一段时间的比对，root cause 出现了。在 2.x 中，Jekyll 使用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;post.rb&lt;/code&gt; 来处理文章，同时存在一个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.rb&lt;/code&gt; 来应对别的类型的文件。下面摘录一小段代码&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;#post.rb
:title  =&amp;gt; slug,

def process(name)
  m, cats, date, slug, ext = *name.match(MATCHER)
  self.date = Utils.parse_date(date, &quot;Post &apos;#{relative_path}&apos; does not have a valid date in the filename.&quot;)
  self.slug = slug
  self.ext = ext
end

#document.rb
title: Utils.slugify(data[&apos;slug&apos;]) || Utils.slugify(basename_without_ext)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;简单来说，post 中的 title，就是你文件中的名字，比如&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;2015-11-16-IAmAwesome.md&lt;/code&gt;的 title 就是 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;IAmAwesome&lt;/code&gt;。但是在 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;document.rb&lt;/code&gt; 中，Jekyll 额外进行了一步处理 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Utils.slugify&lt;/code&gt;，我们看看这个函数会做什么：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;SLUGIFY_RAW_REGEXP = Regexp.new(&apos;\\s+&apos;).freeze
SLUGIFY_DEFAULT_REGEXP = Regexp.new(&apos;[^[:alnum:]]+&apos;).freeze
SLUGIFY_PRETTY_REGEXP = Regexp.new(&quot;[^[:alnum:]._~!$&amp;amp;&apos;()+,;=@]+&quot;).freeze

def slugify(string, mode=nil)
  mode ||= &apos;default&apos;
  return nil if string.nil?
  return string.downcase unless SLUGIFY_MODES.include?(mode)

  # Replace each character sequence with a hyphen
  re = case mode
  when &apos;raw&apos;
    SLUGIFY_RAW_REGEXP
  when &apos;default&apos;
    SLUGIFY_DEFAULT_REGEXP
  when &apos;pretty&apos;
    # &quot;._~!$&amp;amp;&apos;()+,;=@&quot; is human readable (not URI-escaped) in URL
    # and is allowed in both extN and NTFS.
    SLUGIFY_PRETTY_REGEXP
  end

  string.
    # Strip according to the mode
    gsub(re, &apos;-&apos;).
    # Remove leading/trailing hyphen
    gsub(/^\-|\-$/i, &apos;&apos;).
    # Downcase
    downcase
end
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Jekyll 把它认为不正常的字符都替换成&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-&lt;/code&gt;，并且把字符都换成小写的。&lt;/p&gt;

&lt;p&gt;为了修复我博客的小问题，我立刻发了个 &lt;a href=&quot;https://github.com/jekyll/jekyll/pull/4100&quot;&gt;Pull request&lt;/a&gt;，然而这个 pr 是有私心的，我当时只考虑了大小写的问题。后来有哥们&lt;a href=&quot;https://github.com/jekyll/jekyll/issues/4135&quot;&gt;表示&lt;/a&gt;他写在文件名里的&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_&lt;/code&gt;都被换成了&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-&lt;/code&gt;，不开心。&lt;/p&gt;

&lt;p&gt;可以想见，以后会不断有人跑过来说，把我的什么什么符号还给我…&lt;/p&gt;

&lt;p&gt;##终极解决方案
我的 pr 之后只会保留大小写和&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;-&lt;/code&gt;、&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;_&lt;/code&gt;，其他的符号依然会被替换掉，这不是一个一劳永逸的修复。再加上 Jekyll 目测进入了 maintain mode，feature change 和 bug fix 现在都需要多人 review 才能进入主分支。我的 pr 写了两周了，一个 maintainer 表示可以 merge，但要等别人再 review。结果我这一等就是两周，这期间我给 Jekyll 提了四个 issue，三个 pr，通通 pending。所以，要想等 Jekyll 发布新的版本，可能不是一个明智的做法。&lt;/p&gt;

&lt;p&gt;为此，我写了一个插件，叫做 &lt;a href=&quot;https://github.com/rebornix/jekyll-post-unslugify&quot;&gt;jekyll-post-unslugify&lt;/a&gt;，大家可以参考&lt;a href=&quot;https://github.com/rebornix/jekyll-post-unslugify/blob/master/README.md&quot;&gt;文档&lt;/a&gt;进行安装，安装配置完毕后，你的文章的 title 就会像 Jekyll 2.x 里面一样随心所欲。只有 title 回到了过去，你依然可以享受 Jekyll 3.0 的新功能。&lt;/p&gt;

&lt;p&gt;最后，May the title be with you.&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>理解Angular Nested Scope 的关键：Prototype Chain</title>
   <link href="http://www.rebornix.com/frontend/2015/07/03/nestedscope/"/>
   <updated>2015-07-03T00:00:00+00:00</updated>
   <id>http://www.rebornix.com/frontend/2015/07/03/nestedscope</id>
   <content type="html">&lt;p&gt;#一个人人都要踩的坑
Angular容易上手的一个重要原因就是data binding非常简单，当你在controller里面给scope绑定上一个object，立刻就能在view中show出来，而且也能够非常轻松地实现two way binding。生活十分愉快。&lt;/p&gt;

&lt;p&gt;但突然有一天，不知道从加了哪一行代码开始，two way binding不工作了。你翻箱倒柜把书从头翻到尾，到SO上求爷爷告奶奶，最终你发现，你遇到了一个名叫&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;nested scope&lt;/code&gt;的问题。&lt;/p&gt;

&lt;p&gt;#这个坑长什么样
我们来举一个不能再简单的栗子，童鞋们可以到&lt;a href=&quot;http://plnkr.co/edit/zOtVDpNPP8XpaDhYkNnE&quot;&gt;这里&lt;/a&gt;看demo。首先我们有个html页面充当view&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&amp;lt;body ng-controller=&quot;MainCtrl&quot;&amp;gt;
  &amp;lt;p&amp;gt;Hello !&amp;lt;/p&amp;gt;
  &amp;lt;input ng-model=&quot;name&quot;&amp;gt;
  &amp;lt;div ng-if=&quot;includeForm&quot;&amp;gt;
    &amp;lt;input ng-model=&quot;name&quot;&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;接着咱们有段javascript&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;var app = angular.module(&apos;plunker&apos;, []);
app.controller(&apos;MainCtrl&apos;, function($scope) {
  $scope.name = &apos;World&apos;;
  $scope.includeForm = true;
});
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;很容易看出，我们这个 angular app，其实就是把 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$scope.name&lt;/code&gt; 绑定到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;p&lt;/code&gt; 和两个 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;input&lt;/code&gt; element 上。初始化后，页面是这样的，初始值都是 world&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://dn-rebornix.qbox.me/nestedscope-1.png&quot; alt=&quot;一切都在掌控之中&quot; /&gt;&lt;/p&gt;

&lt;p&gt;然后我们在第一个 input box 里面将文字改成 kitty
&lt;img src=&quot;https://dn-rebornix.qbox.me/nestedscope-2.png&quot; alt=&quot;完美！&quot; /&gt;&lt;/p&gt;

&lt;p&gt;接下来属于高危动作，睁大你的双眼：修改第二个 input box 里的 text，把它改成 peng 。惊人的是，Title 和 第一个 input 里的 kitty 并未随之改变。
&lt;img src=&quot;https://dn-rebornix.qbox.me/nestedscope-3.png&quot; alt=&quot;发生了什么...&quot; /&gt;&lt;/p&gt;

&lt;p&gt;最后就是见证奇迹的时刻，修改第一个 input box 的值，改回成 world，Title 立刻随着一起改变，但第二个 input box 像是与这个世界失去了联系。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://dn-rebornix.qbox.me/nestedscope-4.png&quot; alt=&quot;已经彻底被玩坏&quot; /&gt;&lt;/p&gt;

&lt;p&gt;在分析上面的 case 中 到底发生了什么之前，我们一起回顾一下 JavaScript 的基础知识 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Inheritance_and_the_prototype_chain&quot;&gt;Inheritance and the prototype chain&lt;/a&gt;。JS 高玩自行跳过这个章节 :)&lt;/p&gt;

&lt;p&gt;#什么是prototype 
我们知道在 C++/Java/C# 这样的面向对象编程语言中，我们可以使用继承（inheritance）来实现属性和方法的共享，减少冗余代码的书写。&lt;/p&gt;

&lt;p&gt;JavaScript 也支持继承，但是它并没有类的概念，而是使用 prototype 来实现这一目标。JavaScript 中的每个对象都有一个内部私有的链接指向另一个对象，这个对象就是原对象的原型(prototype)。这个原型对象也有自己的原型，直到对象的原型为 null 为止（也就是没有原型）。这种一级一级的链结构就称为原型链。&lt;/p&gt;

&lt;p&gt;拥有了继承之后，JavaScript 的 Object 就拥有了两种属性，一种是对象自身的属性，另外一种是继承于原型链上的属性。当我们去读取 Object 的某个属性时，首先查看当前 Object 是否拥有该属性，有的话返回值，如果没有的话，找到它的 prototype，看看这个对象上是否有有该属性。JavaScript 会顺着 prototype chain 一路上去，直到找到这个属性活着 prototype chain 到头为止。&lt;/p&gt;

&lt;p&gt;关于 prototype 更加详细和通透的解释，大家可以参考 &lt;a href=&quot;https://github.com/norfish/blog/blob/master/prototype.md&quot;&gt;这篇文章&lt;/a&gt; 和 ruanyf 老师的&lt;a href=&quot;http://www.ruanyifeng.com/blog/2011/06/designing_ideas_of_inheritance_mechanism_in_javascript.html&quot;&gt;大作&lt;/a&gt;，我高中语文老是不及格，就不给大家添麻烦了。我就带大家来看个小小的栗子。第一步，我们创建一个 object，就叫它爹吧。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&amp;gt; parent = { &quot;first_name&quot;: &quot;Peng&quot;}
&amp;lt; Object {first_name: &quot;Peng&quot;}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;爹有一个属性叫做 first_name，值为 Peng。接着我们生一个儿子，&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&amp;gt; child = Object.create(parent)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create&quot;&gt;Object.create()&lt;/a&gt; 这个函数会创建一个新的 Object 并将新Object 的 prototype 指定为 传入的参数。比如这里，我们传入的参数是 parent，那么 child 的prototype 就是 parent，child 会从 parent 这里继承属性。比如：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&amp;gt; child.first_name
&amp;lt; &quot;Peng&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;child 上本身并没有 first_name 这个属性，但是他爹有，于是依然得到了 Peng 这个值。到这里为止，我们展示了如何从 prototype 上继承一个 primitive value 。继承 object property 也是一样的。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&amp;gt; parent = { &quot;name&quot; : { &quot;first&quot;: &quot;peng&quot;, &quot;last&quot;: &quot;lv&quot;}}
&amp;gt; child = Object.create(parent)
&amp;gt; child.name.first
&amp;lt; &quot;peng&quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;童鞋们可以在浏览器的 console 里面玩一下
&lt;img src=&quot;https://dn-rebornix.qbox.me/nestedscope-5.png&quot; alt=&quot;没图我说个球啊&quot; /&gt;&lt;/p&gt;

&lt;p&gt;到这里，即使是从没听说过 prototype 的朋友肯定也明白了，这不就是老鼠的儿子会打洞么。但是关于 prototype 的继承，我想把 MDN 文档里的一句话高亮出来&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;Setting a property to an object creates an own property. The only exception to the getting and setting behavior rules is when there is an inherited property with a &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Working_with_Objects#Defining_getters_and_setters&quot;&gt;getter or a setter&lt;/a&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;最重要的就是第一句了，&lt;strong&gt;Setting a property to an object creates an own property&lt;/strong&gt;。当我们去 get property 的时候，会顺着 prototype chain 一直往上找，但是 set property 并不会这样，而是为当前对象生成一个新的 property 。比如这样：&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://dn-rebornix.qbox.me/nestedscope-6.png&quot; alt=&quot;右手 左手 不是慢动作重播！&quot; /&gt;&lt;/p&gt;

&lt;p&gt;自此 child 和 parent 就失联了。下面我们可以来看看 Angular 的 nested scope 是怎么一回事。&lt;/p&gt;

&lt;p&gt;#Angular 如何创建child scope
在使用 Angular 的一些 built-in directive 时，比如 ng-if/ng-include/ng-repeat/ng-switch/etc 时，需要注意的一点是，Angular 会为其生成一个新的 scope，这个 scope 继承自 外层的 scope。回到我们最上面提到的 demo&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&amp;lt;body ng-controller=&quot;MainCtrl&quot;&amp;gt;
  &amp;lt;p&amp;gt;Hello !&amp;lt;/p&amp;gt;
  &amp;lt;input ng-model=&quot;name&quot;&amp;gt;
  &amp;lt;div ng-if=&quot;includeForm&quot;&amp;gt;
    &amp;lt;input ng-model=&quot;name&quot;&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;MainCtrl&lt;/code&gt; 上有一个 scope 作为胶水来粘合 controller 和 view，而&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ng-if&lt;/code&gt; 又会生成一个 scope，这个 scope 向上继承 controller 的 scope。这个继承 Angular 是如何实现的呢，我们来看&lt;a href=&quot;https://github.com/angular/angular.js/blob/291d7c467fba51a9cb89cbeee62202d51fe64b09/src/ng/rootScope.js#L83-L95&quot;&gt;源码&lt;/a&gt;&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;function createChildScopeClass(parent) {
  function ChildScope() {
    this.$$watchers = this.$$nextSibling =
        this.$$childHead = this.$$childTail = null;
    this.$$listeners = {};
    this.$$listenerCount = {};
    this.$$watchersCount = 0;
    this.$id = nextUid();
    this.$$ChildScope = null;
  }
  ChildScope.prototype = parent;
  return ChildScope;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;我们看到，创建 child scope 的时候，会把 child scope 的 prototype (原型) 设置为 parent 。根据我们上面刚刚温习的 prototype 继承机制，当在第二个 input box 里访问 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ng-model=&quot;name&quot;&lt;/code&gt; 时，会先到 ng-if 上的 child scope 寻找 name 这个属性，如果没有，沿着 prototype 找到 parent scope，最终找到 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;name&lt;/code&gt; 这个属性。&lt;/p&gt;

&lt;p&gt;而当我们往第二个 input box 里面输入新的值（见 第三张图片），则触发了 prototype 的另一个规则 &lt;strong&gt;Setting a property to an object creates an own property&lt;/strong&gt; , ng-if 上的 child scope 增加了一个新的属性 name ，parent scope 上的 name 和 child scope 的 name 从此再无瓜葛。当我们再次修改 第一个 input box 里的值时，实际上我们修改的 parent scope 上的 name ，对于 第二个 input box 来说，并没有什么卵用。&lt;/p&gt;

&lt;p&gt;&lt;img src=&quot;https://dn-rebornix.qbox.me/nestedscope-4.png&quot; alt=&quot;鸟都不鸟你啊&quot; /&gt;&lt;/p&gt;

&lt;p&gt;问题到这里已经清楚了，Angular 的 nested scope 使用了 prototype 这个机制来实现 child scope 对 parent scope 的继承，当我们修改 child scope 上的属性时，会导致无法更新 parent scope 的属性。那么该如何解救它们呢？&lt;/p&gt;

&lt;p&gt;有两招&lt;/p&gt;

&lt;h1 id=&quot;dot-notation-和-parent&quot;&gt;Dot Notation 和 $parent&lt;/h1&gt;

&lt;h2 id=&quot;第一招江湖人称-dot-notation&quot;&gt;第一招，江湖人称 Dot Notation&lt;/h2&gt;

&lt;p&gt;换做人能够听懂的语言就是，避免给 child scope 上的属性赋值。还记得上文我们讲解 prototype chain 的时候说过，属性是 object 也可以继承&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&amp;gt; parent = { &quot;name&quot; : { &quot;first&quot;: &quot;peng&quot;, &quot;last&quot;: &quot;lv&quot;}}
&amp;gt; child = Object.create(parent)
&amp;gt; child.name.first ＝ &quot;hulk&quot;
&amp;lt; &quot;hulk&quot;
&amp;gt; parent.name
&amp;lt; Object {first: &quot;hulk&quot;, last: &quot;lv&quot;}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;parent 有个属性叫 name，name 有个属性叫 first 。如果我们修改 child.name.first ，第一步是查找 child 上的 name 属性，没有找到，根据 prototype chain 找到了 parent 上的 name 属性，然后修改了它的 property &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;first&lt;/code&gt; 。整个过程并没有给 child 的 property &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;name&lt;/code&gt; 赋值。&lt;/p&gt;

&lt;p&gt;当然，如果你直接修改 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;child.name&lt;/code&gt;，name 的继承就消失了。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&amp;gt; child.name = {&quot;first&quot;: &quot;captain&quot;, last: &quot;america&quot;}
&amp;lt; Object {first: &quot;captain&quot;, last: &quot;america&quot;}
&amp;gt; parent.name
&amp;lt; Object {first: &quot;hulk&quot;, last: &quot;lv&quot;}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;Dot Notation，这个名字真的是传神啊，修改和访问 $scope上属性的属性（$scope.name.first），而不是直接操作 $scope的属性（$scope.name），多一个 Dot ，就解决了 two way binding 的坑。&lt;/p&gt;

&lt;p&gt;##第二招，见招拆招，使用$parent。
不是担心修改 child scope 上的属性么，直接访问 parent scope 上的属性不就行了么。我们再来看 Angular 的&lt;a href=&quot;https://github.com/angular/angular.js/blob/291d7c467fba51a9cb89cbeee62202d51fe64b09/src/ng/rootScope.js#L231&quot;&gt;代码&lt;/a&gt;&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;child.$parent = parent;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;child scope 直接有个 $parent 属性来 reference parent scope。&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&amp;lt;body ng-controller=&quot;MainCtrl&quot;&amp;gt;
  &amp;lt;p&amp;gt;Hello !&amp;lt;/p&amp;gt;
  &amp;lt;input ng-model=&quot;name&quot;&amp;gt;
  &amp;lt;div ng-if=&quot;includeForm&quot;&amp;gt;
    &amp;lt;input ng-model=&quot;$parent.name&quot;&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/body&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这个方法过于暴力，博主不推荐使用，被同事爆的风险太高了。&lt;/p&gt;

&lt;hr /&gt;
&lt;p&gt;每篇文章的最后总该总结点什么，不能虎头蛇尾….&lt;/p&gt;

&lt;p&gt;想到了！以上问题只在 Angular 1.x 出现，因为2.0开始就没有 scope 咯～&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>从StackOverflow看Angular 1.x</title>
   <link href="http://www.rebornix.com/frontend/2015/06/10/NGSummary/"/>
   <updated>2015-06-10T00:00:00+00:00</updated>
   <id>http://www.rebornix.com/frontend/2015/06/10/NGSummary</id>
   <content type="html">&lt;p&gt;去年 IE Team 把 issue tracker 放到 StackOverflow 之后，我一度以为 SO 会革了 MSDN Library 和 Forum 的命（也就是让我丢掉饭碗），就像 GitHub 干掉 Codeplex 和 Google Code 一样。刚好当时我学习和实践 AngularJS 有一段时间了，我就想到 SO 上试试身手，看看自己的水平，是否能够帮助到别人。&lt;/p&gt;

&lt;p&gt;于是我每天大概花半个小时不到在 SO 的 &lt;a href=&quot;http://stackoverflow.com/questions/tagged/angularjs&quot;&gt;Angular JS&lt;/a&gt; tag 下面解答问题。玩了半个月左右，拿到一千多分。在 SO 上刷分当然不是目的，但是分数比较好的体现了你回答的问题的准确度和热门度，所以我比较关注这个指标，但这个过程只能用艰难来形容。之所以称之为艰难，主要原因有两个。&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;如果题目比较基础，解答相对容易。但是你会遇到很多三哥朋友们第一时间冲出来回答。甚至很多时候，你已经做出解答了，他们还源源不断地 post 和你一模一样的 answer 。&lt;/li&gt;
  &lt;li&gt;如果题目比较深入，确实没有三哥和你抢了，但很多时候这也意味着，没有多少人会关注这个问题，为你的答案点赞。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;这两个现象其实都来源于一个 root cause ：基础的问题，回答起来更容易，花的时间更少，得到的分数更多。而这样的问题，受到所有人的喜爱。为什么呢？对于抢分狂魔而言，这种题目分数更好得；对于热心答题不计较分数的好心人而言，回答这种问题，能够把时间省下来帮助更多的人，何乐而不为？当整个社区都呈现这种状态是，你就不难得到当年老赵对 SO 的评论：&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;如果你经过深思熟虑也无法解决的问题，丢到 SO 上，也基本上不会得到你想要的答案&lt;/code&gt;。这是个悲伤的结论，但 it’s true 。&lt;/p&gt;

&lt;p&gt;SO 其实也意识到这个问题了我估计，于是他们搞了个 Golden Badge 叫做&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Unsung Hero&lt;/code&gt;。获得这个 Badge 的条件是，你超过一半的答案被选为正确答案，但是没人给你 upvote（也就意味着关注度小）。真的是名副其实，无名英雄。最开始拿到这个 Badge 的时候，我并没有特别高兴。但当我得到上面的结论时，我就释然了。高兴了无聊了工作累了，我就爬上去回答一些我知道的问题。&lt;/p&gt;

&lt;p&gt;这几天突然看到自己又一次上了 AngularJS 这个 Tag 的&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Last 30 days top answerers&lt;/code&gt;，我想，既然在 SO 上最热门最抢手的问题都是比较基础的、简单的，这也间接说明，对于新手而言，这些知识点具备一定学习曲线，并没有那么容易上手和熟悉。如果把这些知识点和问题做一个整理，这一定是一个不错的文档，也能从中看出这个语言或框架在设计方面的不足。于是我从前到后阅读了一遍我回答过的 Angular 的问题，很快发现不少反复出现或者同类型的提问。&lt;/p&gt;

&lt;p&gt;下面让我们一起，从我的角度，看看大家学习 Angular 1.x 的时候通常会遇到哪些困难。&lt;/p&gt;

&lt;p&gt;##一：Angular Principles&lt;/p&gt;

&lt;p&gt;第一类问题是关于 Angular 的运行原理。像 ASP.NET 这种框架，你大部分时候都不需要理解它的机制，而当你不得不去理解 ASP.NET 是怎么运行，那基本说明你遇到了非常坑爹的问题了。但是 Angular 并不是这样的，即使你是上手才半个礼拜的新手，当你需要完成一些特定的功能，你就不得不理解 Angular 的 principles，否则你就会掉入坑中爬不出来。这也就是业界对 Angular 最大的批评，学习曲线比较高。&lt;/p&gt;

&lt;p&gt;###Digest Loop&lt;/p&gt;

&lt;p&gt;这是我心目中的 Angular 第一大杀手。很多前端工程师从纯 JS 或者别的框架迁移到 Angular 的时候，最经常问的就是，为什么我更新了某某 object 的值，页面上没有更新呢？ Angular 使用 Digest cycle 来实现 two way binding，而他们的操作并没有进入到这个 cycle 中，比如 &lt;a href=&quot;http://stackoverflow.com/questions/30613139/ace-editor-replace-text/30613945#30613945&quot;&gt;ace update text&lt;/a&gt;，&lt;a href=&quot;http://stackoverflow.com/questions/28252432/angularjs-doesnt-show-json-data/28252476#28252476&quot;&gt;show json&lt;/a&gt;, &lt;a href=&quot;http://stackoverflow.com/questions/28226525/angularjs-watch-for-image-change/28227729#28227729&quot;&gt;image change&lt;/a&gt;。 GitHub 上有这样一群 repo ，他们专门负责把第三方的 library 进行包装，使得能够在 Angular 中使用。老实讲， Angular 养活了一批人呢。等2.0上市了，大家又要一股脑儿把这些 library 重新包装，有兴趣的同学不妨试试，是个不错的学习 Angular 和 Contribute to Open Source 的机会。&lt;/p&gt;

&lt;p&gt;还有就是在写 directive 的时候，更新了 model 却不能&lt;a href=&quot;http://stackoverflow.com/questions/28207826/modifying-scope-variables-in-an-event-in-a-directives-link-function/28208216#28208216&quot;&gt;刷新&lt;/a&gt; &lt;a href=&quot;http://stackoverflow.com/questions/28213976/ngmodel-value-is-not-updating-in-directive/28214184#28214184&quot;&gt;页面&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;好不容易 developer 知道可以使用 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$apply&lt;/code&gt; &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$digest&lt;/code&gt; 去手动 trigger digest loop，他们也会经常遇到 &lt;a href=&quot;http://stackoverflow.com/questions/28310486/running-into-infinite-digest-cycle-while-binding-to-function-that-has-http-insi/28311493#28311493&quot;&gt;infinite digest cycle&lt;/a&gt; 的问题，简直要疯了。&lt;/p&gt;

&lt;p&gt;###Expressions
Angular 中有你可以这么使用expression&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;在 built-in 的 directive 中，比如 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;input type=&quot;text&quot; name=&quot;userName&quot; ng-model=&quot;user.name&quot;&lt;/code&gt;&lt;/li&gt;
  &lt;li&gt;另一种就是使用 double bracket  &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;p&amp;gt;My first expression: {{ value }}&amp;lt;/p&amp;gt;&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;可是奇怪的事情，当你使用&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;ng-src&lt;/code&gt;的时候，你需要这样写 &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;&amp;lt;img ng-src=&quot;http://www.gravatar.com/avatar/{{hash}}&quot; alt=&quot;Description&quot;/&amp;gt;&lt;/code&gt; … 我已经不知道该说什么好了 …你可以再看看这里 &lt;a href=&quot;http://stackoverflow.com/questions/28232073/creating-a-directive-for-bootstrap-menuitems/28232195#28232195&quot;&gt;1&lt;/a&gt;, &lt;a href=&quot;http://stackoverflow.com/questions/28379139/angularjs-ng-click-function-with-angular-expression-parameter-returning-a-syntax/28379163#28379163&quot;&gt;2&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;###$scope&lt;/p&gt;

&lt;p&gt;很多人搞不对expression也可能是因为没有理解 scope 。scope 既是连接 controller 和 view 的胶水，也是 expression 的运行环境。很多朋友在这里栽了，带来的结果往往是搞不定 &lt;a href=&quot;http://stackoverflow.com/questions/28207136/angularjs-injector-error-uncaught-error-injectormodulerr/28208023#28208023&quot;&gt;data&lt;/a&gt; &lt;a href=&quot;http://stackoverflow.com/questions/28525717/angular-js-ng-include-binding-issues/28525878#28525878&quot;&gt;binding&lt;/a&gt; 。&lt;/p&gt;

&lt;p&gt;###Module definition&lt;/p&gt;

&lt;p&gt;如果你只是通过官方的demo来学习Angular，你可能会搞不清楚&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;angular.module(&apos;app&apos;, [])&lt;/code&gt;和&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;angular.module(&apos;app&apos;)&lt;/code&gt;的区别，并且由于调用的错误，导致 module 不断地被反复 &lt;a href=&quot;http://stackoverflow.com/questions/28258655/angularjs-directive-definition-using-angular-module/28258670#28258670&quot;&gt;initialize&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;###Angular config/run phase&lt;/p&gt;

&lt;p&gt;在 module 的 bootstrap 过程中，你可以通过指定自定义的 Configuration blocks 来操作 provider/constants，或者使用 Run blocks 来配置 instances／constants ，千万不要&lt;a href=&quot;http://stackoverflow.com/questions/28541179/global-functions-in-angularjs/28541333#28541333&quot;&gt;错用了&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;###DI
一般 dependency injection 出现问题，基本都是用户粗心&lt;a href=&quot;http://stackoverflow.com/questions/28585695/angularjs-1-0-7-locationprovider-undefined/28585749#28585749&quot;&gt;漏掉了某个 factory &lt;/a&gt;，或者是使用了 minification 。&lt;/p&gt;

&lt;p&gt;##二：Directive
什么是 directive ，这里我只简单贴一下官方的说法&lt;/p&gt;

&lt;blockquote&gt;
  &lt;p&gt;At a high level, directives are markers on a DOM element (such as an attribute, element name, comment or CSS class) that tell AngularJS’s HTML compiler ($compile) to attach a specified behavior to that DOM element or even transform the DOM element and its children.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;directive 能够很好滴帮助实现模块化，在 Angular 在自己的实现过程中，就把不少功能以 directive 的方式提供给开发者。用户也可以自己书写 directive 对功能进行抽象。在我看来，directive 往往是初学者比较难以掌握的。&lt;/p&gt;

&lt;p&gt;###Built-in Directives
首先是 Angular 提供的 directive 。有的时候，即使是认真研习文档，你也不一定能够成功理解它们的运行机制，很多朋友在这里就吃了苦头了。印象里 built-in directive 这个方面我拿分相对容易，因为我比较勤劳，如果文档不能够帮助解决问题的话，我会直接跑去看源码。如果问我泡 StackOverflow 最大的收获是什么，那一定是读了不少 Angular 的 source code。&lt;/p&gt;

&lt;p&gt;SO 上经常会提及的 built-in directive 有&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;&lt;a href=&quot;http://stackoverflow.com/questions/28575962/ng-disabled-of-image-in-angularjs/28576023#28576023&quot;&gt;ng-disabled&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;http://stackoverflow.com/questions/28575500/how-to-set-default-value-in-an-angular-select-menu-based-on-data-binding-instead/28575607#28575607&quot;&gt;ng-change&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;http://stackoverflow.com/questions/28533755/how-to-show-specific-element-in-table-cells-using-angularjs/28533798#28533798&quot;&gt;ng-repeat&lt;/a&gt;, &lt;a href=&quot;http://stackoverflow.com/questions/28409338/preprocess-ng-repeat-variables/28409606#28409606&quot;&gt;ng-repeat-start/ng-repeat-end&lt;/a&gt; 。尤其是后者，满多人上来问，我该怎么用 ng-repeat 实现 xxx 功能，基本都是因为他们不知道 ng-repeat-start/end .&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;http://stackoverflow.com/questions/28537190/is-it-possible-to-show-template-in-angular-when-data-is-ready/28537209#28537209&quot;&gt;ng-cloak&lt;/a&gt; 玩过 Angular 的同学都知道，如果页面 loading 比较慢，就会看到页面上充满着各种花括号，然后才会变成相应的文字。为了解决这种问题， Angular 给我们提供了 ng-cloak （ng-bind也能实现一样的效果）。这是我给&lt;a href=&quot;http://microsoft.github.io&quot;&gt;microsoft.github.io&lt;/a&gt; 交的第一个 pr :)&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;http://stackoverflow.com/questions/28252017/how-to-assign-a-width-to-span-dynamically/28252247#28252247&quot;&gt;ng&lt;/a&gt;-&lt;a href=&quot;http://stackoverflow.com/questions/30430443/how-can-i-use-ng-if-to-check-if-the-string-contains-others-substrings/30430469#30430469&quot;&gt;style&lt;/a&gt;/&lt;a href=&quot;http://stackoverflow.com/questions/30552822/why-cant-i-set-the-height-of-an-element-with-ng-style/30552904#30552904&quot;&gt;ng-class&lt;/a&gt; 在使用 JQuery 的时候，可以直接操作 DOM 来添加样式，但这并不是 Angular 推荐的方式。很多人意识到了这点，他们只是刚好没听说 ngStyle/ngClass 。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;###Write your own directive
除了使用 built-in 的 directive ，Angular 也鼓励大家来写自己的 directive。但是这里大家经常会遇到 &lt;a href=&quot;http://stackoverflow.com/questions/28214948/angularjs-send-image-to-directive-and-show-directive/28216033#28216033&quot;&gt;model binding&lt;/a&gt; 或者是如何给 &lt;a href=&quot;http://stackoverflow.com/questions/28394118/angularjs-directive-scope-not-resolved-attr-name-is-not-defined-error/28394192#28394192&quot;&gt;attr 传值&lt;/a&gt;，再或者从 directive 中&lt;a href=&quot;http://stackoverflow.com/questions/28425711/angularjs-pass-scope-variable-as-directive-attribute/28425856#28425856&quot;&gt;访问 controller 的 scope&lt;/a&gt; 。&lt;/p&gt;

&lt;p&gt;##三：$http
$http 只是一个 built-in 的 service ，但我要把它单独拿出来讲一讲，一方面是因为这是一个非常基础的 service （访问后台 Rest API ），另一方面是 developer 在和 $http 总会不约而同的想静静。&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;Encoding 真的是所有程序员的噩梦。到底是 form 还是 json 还是 byte array ？如何设置 header ？如何配置 callback ？&lt;a href=&quot;http://stackoverflow.com/questions/28384174/paypal-api-with-angular-400-bad-request/28389396#28389396&quot;&gt;问题&lt;/a&gt; &lt;a href=&quot;http://stackoverflow.com/questions/30495962/angularjs-consume-asp-net-web-service/30496036#30496036&quot;&gt;多&lt;/a&gt; &lt;a href=&quot;http://stackoverflow.com/questions/30519568/what-data-attribute-to-use-when-returning-application-pdf/30520094#30520094&quot;&gt;多&lt;/a&gt;。&lt;/li&gt;
  &lt;li&gt;Cache 为了减少 call 后台的 cost，$http 提供了 &lt;a href=&quot;http://stackoverflow.com/questions/28472395/how-to-cache-http-in-angular-until-parameters-changed/28472485#28472485&quot;&gt;cache 的功能&lt;/a&gt;。&lt;/li&gt;
  &lt;li&gt;Headers 论如何为所有的 $http request 配置 &lt;a href=&quot;http://stackoverflow.com/questions/30557457/how-can-i-added-default-header-for-delete-in-angular-js/30557502#30557502&quot;&gt;header&lt;/a&gt;。&lt;/li&gt;
  &lt;li&gt;&lt;a href=&quot;http://stackoverflow.com/questions/30634093/error-on-get-request-to-steam-market/30634248#30634248&quot;&gt;JSONP&lt;/a&gt;&lt;/li&gt;
  &lt;li&gt;CORS. Angular 把 rendering engine 搬到了浏览器里，我们需要通过 ajax 向 backend server 抓取数据然后在页面上渲染，这就不可避免的遇到了 cross domain 的各种 failure ，算得上前端届又一个&lt;a href=&quot;http://stackoverflow.com/questions/28140859/angularjs-refused-to-set-unsafe-header-access-control-request-headers/28162228#28162228&quot;&gt;老&lt;/a&gt; &lt;a href=&quot;http://stackoverflow.com/questions/28269240/how-to-properly-separate-frontend-and-backend/28269626#28269626&quot;&gt;大&lt;/a&gt; &lt;a href=&quot;http://stackoverflow.com/questions/28515863/cross-origin-request-not-working-in-cordova-with-angularjs/28516044#28516044&quot;&gt;难&lt;/a&gt; &lt;a href=&quot;http://stackoverflow.com/questions/28447391/http-how-to-get-filename-of-headers-from-webapi-with-cors/28447415#28447415&quot;&gt;问题&lt;/a&gt;。&lt;/li&gt;
  &lt;li&gt;Promise. ASP.NET 有 async/wait，你猜 Angular 里面&lt;a href=&quot;http://stackoverflow.com/questions/28523621/returning-response-to-controller-from-factory-method-in-angular/28523666#28523666&quot;&gt;都&lt;/a&gt; &lt;a href=&quot;http://stackoverflow.com/questions/28207899/how-to-execute-an-action-only-after-success-angularjs/28208117#28208117&quot;&gt;有&lt;/a&gt; &lt;a href=&quot;http://stackoverflow.com/questions/28469774/using-angular-how-can-we-make-a-call-after-completing-bunch-of-asynchronous-call/28469983#28469983&quot;&gt;啥&lt;/a&gt;。&lt;/li&gt;
  &lt;li&gt;interceptor. Angular 允许你 customize $http 的 &lt;a href=&quot;http://stackoverflow.com/questions/28375505/angularjs-successful-401-intercept-still-throws-401/28377697#28377697&quot;&gt;error handling&lt;/a&gt;。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;最后，读者朋友如何跑到我博客首页看一看，你还能看到去年我对 Angular 的吐槽 &lt;a href=&quot;https://rebornix.com/frontend/2014/12/07/AngularJSFuckMeUp/&quot;&gt;Angular 一个值得当心的bug&lt;/a&gt; 。是的，我喷的就是 $http ，虽然没有什么卵用。&lt;/p&gt;

&lt;p&gt;##四：Best Practice
还有一类问题，来自有理想有追求的朋友。他们想知道如何 think in Angular，如何操作能够有比较好的 performance等等。&lt;/p&gt;

&lt;p&gt;###Communication/Data Sharing between Controllers/Factories
当你想在不同的 context 之间共享 data 的时候，全局变量堪称 fast and dirty 。但不幸的是，从你上大学的第一天开始，就有人不断告诉你这么做是不负责任滴。于是为了避免被同事 code review 的时候 challenge，不断有人来问该&lt;a href=&quot;http://stackoverflow.com/questions/28208523/how-to-change-the-scope-in-angular-in-a-view-which-you-are-not-yet-in/28208585#28208585&quot;&gt;如何&lt;/a&gt; 在 &lt;a href=&quot;http://stackoverflow.com/questions/28235351/parent-view-does-not-get-updated/28235844#28235844&quot;&gt;controller/factory/directive/etc&lt;/a&gt; &lt;a href=&quot;http://stackoverflow.com/questions/30524583/is-it-possible-to-share-value-between-controllers-without-ng-model-in-angularjs/30524689#30524689&quot;&gt;之间&lt;/a&gt; &lt;a href=&quot;http://stackoverflow.com/questions/28337048/communicating-between-controllers-in-angularjs/28337130#28337130&quot;&gt;share&lt;/a&gt; &lt;a href=&quot;http://stackoverflow.com/questions/30507754/angular-js-api-using-a-factory/30507914#30507914&quot;&gt;data&lt;/a&gt; 。俗称，月经帖。&lt;/p&gt;

&lt;p&gt;###Model binding/Performance Enhancement
其实这个和上文提到的 &lt;a href=&quot;#digest-loop&quot;&gt;digest loop&lt;/a&gt; 可以放在一起谈。Angular 使用了 dirty check 去实现 two-way binding，也就是很傻瓜地反复地遍历 watch list，看看大家有没有什么更新。当页面上的绑定的 element 开始增多（大概2000+），dirty check 就会严重拖累整个页面的 performance。&lt;/p&gt;

&lt;p&gt;dirty check 简直是 Angular 1.x 的阿克琉斯之踵，然后谷歌的大大们在2.0中，把它无情地抛下了。听闻这个消息，我等屁民“弹冠相庆”，谁让它&lt;a href=&quot;http://stackoverflow.com/questions/28289529/ng-model-is-overkill-for-me-any-alternative-which-will-update-scope-only-on-but/28290509#28290509&quot;&gt;老是&lt;/a&gt; &lt;a href=&quot;http://stackoverflow.com/questions/28291569/elements-in-scope-missing-from-form/28291758#28291758&quot;&gt;折磨&lt;/a&gt; &lt;a href=&quot;http://stackoverflow.com/questions/28378575/how-to-get-previous-value-and-compare-with-new-value-before-bind-value-in-angula/28379119#28379119&quot;&gt;我们&lt;/a&gt; 呢。&lt;/p&gt;

&lt;hr /&gt;
&lt;p&gt;去年四月我第一次接触 Angular，写了点自嗨的&lt;a href=&quot;https://rebornix.com/海上日志/2014/04/20/SinglePageBlogWithAngularJS/&quot;&gt;小玩具&lt;/a&gt;，转眼已经过去十三月了。短短的一年光阴，Angular 1.x 已经确立了不可撼动的一哥地位，而2.0 也已经“洗心革面”马上要出来重新做人。至于我，在尝试完各种姿势之后，也算是找到了一点方向。希望2015年剩下的时间里，做好一个产品，来一点 breaking change。&lt;/p&gt;

&lt;p&gt;Breaking Change!! Hell Yeah!!&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>如何优雅地维护开源软件 一个小小tip</title>
   <link href="http://www.rebornix.com/frontend/2015/01/24/OSSTip1/"/>
   <updated>2015-01-24T00:00:00+00:00</updated>
   <id>http://www.rebornix.com/frontend/2015/01/24/OSSTip1</id>
   <content type="html">&lt;p&gt;##每一篇博客的开始，都是一个悲剧
从五月份开始写JS，到现在已经有九个月了，前半年因为是刚接触到前端这个领域，成长的速度特别快。到近期仿佛是遇到了一些瓶颈，代码质量一直上不去。为了不让我的代码祸害我们的线上产品，我决定封码几日，换了几个prototype来做做，毕竟proof of concept对有没有八阿哥不太讲究。&lt;/p&gt;

&lt;p&gt;不料前几日同事火急火燎来找我，说有一个我负责的功能突然不work了，完完全全100%不work，而且是live site issue，要立马修好。&lt;/p&gt;

&lt;p&gt;当时我整个人都不好了，这都什么世界啊，惹不起也就算了，躲我都躲不起啊，不checkin代码都能把production搞挂了这种灵异事件我特么还是第一次见。不行，我一定要还自己清白。&lt;/p&gt;

&lt;p&gt;##Trouble shooting
首先，要确定是不是别人改动了我的代码造成的regression，我绝不能错过任何爆别人的机会。查阅commit history一番之后发现，这段代码已经三个月没有改过了，希望破灭了。接着我打开编辑器查看这段代码，我发现当时为了实现一些two way binding，我使用了一个第三方的library叫做&lt;a href=&quot;https://github.com/angular-ui/ui-ace&quot;&gt;ui-ace&lt;/a&gt;。这引起了我的注意，会不会这个包发生了什么问题呢？&lt;/p&gt;

&lt;p&gt;我到github上看了下ui-ace的release notes，果不其然，ui-ace在这几日刚刚发布了新版本。把最近几次的commit都阅读了一遍之后，我发现了问题所在。ui-ace中有一段旧代码逻辑存在一点问题，由于掩盖在层层API之下，我在使用这个功能的时候，把它所有的行为都当成了feature（没错，一个原本默默无闻的bug成功地把自己伪装成了feature，一次精彩的cosplay）。而最近ui-ace的维护者发现了这个问题，然后火速把它修掉了。也就是说，原来的feature，没了。我的代码应声倒地。&lt;/p&gt;

&lt;p&gt;故事到这里水落石出。不过我估计肯定有朋友会疑问，人家ui-ace升级，跟你们有几毛钱关系啊，您们引用了人家的代码还不让人家升级，什么人啊这是？&lt;/p&gt;

&lt;p&gt;其实这个问题，根本错误确实是在我们。我们在引用第三方library时，使用了&lt;a href=&quot;http://bower.io&quot;&gt;bower&lt;/a&gt;来管理library的版本和升级。而我们的配置文件&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;bower.json&lt;/code&gt;是长酱紫的：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&quot;dependencies&quot;: {
    &quot;angular&quot;: &quot;~1.3.0&quot;,
    &quot;angular-bootstrap&quot;: &quot;~0.10.0&quot;,
    &quot;angular-cookies&quot;: &quot;~1.3.0&quot;,
    &quot;angular-file-upload&quot;: &quot;~1.1.5&quot;,
    &quot;angular-mocks&quot;: &quot;~1.3.0&quot;,
    &quot;angular-sanitize&quot;: &quot;~1.3.0&quot;,
    &quot;angular-ui-ace&quot;: &quot;~0.1.1&quot;
    ...
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;大家看，我们对Angular的版本要求是&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~1.3.0&lt;/code&gt;，翻译成人话就是&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;使用1.3.x的Angular代码，并且保持自动更新&lt;/code&gt;。所以每次我们开发、测试、部署时，bower都会帮我们check一下Angular有没有&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1.3&lt;/code&gt;下的小版本更新，有的话，自动更新Angular的代码。多么美妙的自动化啊。&lt;/p&gt;

&lt;p&gt;同样的，我们对ui-ace的期待是&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~0.1.1&lt;/code&gt;，任何&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;0.1.x&lt;/code&gt;的版本我们都能接受。这几天刚好遇上我们重新铺production，铺的过程中，bower帮我们更新了ui-ace的代码，从而导致我的代码break了。&lt;/p&gt;

&lt;p&gt;##如何优雅地维护开源软件
如果我更加小心地书写我的bower配置文件，显然这次live site issue是能够避免的（使用固定的版本号，而不是让bower自动更新）。但是，这次的问题，也让我对ui-ace产生了一些不信任感。如果我以后自己来维护开源项目，我一定会努力避免给我的用户们造成这样的困扰。&lt;/p&gt;

&lt;p&gt;方法就是：&lt;/p&gt;

&lt;h3 id=&quot;版本升级有规则&quot;&gt;版本升级有规则&lt;/h3&gt;
&lt;p&gt;Angular在版本更新的过程中，保证&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;major.minor.build.revision&lt;/code&gt;中，&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;build&lt;/code&gt;和&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;revision&lt;/code&gt;的升级不会存在任何的breaking change。所有功能性的大变化都会在&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;major.minor&lt;/code&gt;中出现。这也就是为什么我们放心地使用&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;~1.3.0&lt;/code&gt;来标志Angular的版本。&lt;/p&gt;

&lt;p&gt;我在上一篇文章&lt;a href=&quot;http://rebornix.com/frontend/2014/12/07/AngularJSFuckMeUp/&quot;&gt;Angular 一个值得当心的bug&lt;/a&gt;中提到的&lt;a href=&quot;https://github.com/angular/angular.js/issues/10349&quot;&gt;bug&lt;/a&gt;，在被确诊之后，第一时间放到了&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;angular 1.4 candidate&lt;/code&gt;中，就是因为修掉这个bug，是一个breaking change。插句题外话，在接近30个1.4 candidate中，这个issue将会随1.4的正式release一起fix掉，详情请见&lt;a href=&quot;https://www.youtube.com/watch?v=nBptZTfmjhE&quot;&gt;Angular 1.4 Planning&lt;/a&gt;）。&lt;/p&gt;

&lt;p&gt;如果你在维护一个开源软件，不妨告诉你的用户和其他contributor，什么样的版本升级会存在breaking change；什么样的版本升级，只是增加新的feature或者修掉一些无关痛痒的bug。这样的姿势更优雅。&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>Angular 一个值得当心的bug</title>
   <link href="http://www.rebornix.com/frontend/2014/12/07/AngularJSFuckMeUp/"/>
   <updated>2014-12-07T00:00:00+00:00</updated>
   <id>http://www.rebornix.com/frontend/2014/12/07/AngularJSFuckMeUp</id>
   <content type="html">&lt;p&gt;上周四早上收到美帝客户的邮件爆我们，说咱们的编辑器不能编辑markdown也不能preview。美帝同志还是很体贴的，虽然这周我on call，他们也没有惨无人道地半夜把我喊起来，只是静静地等我们都来了公司才呼唤我们。这里给你们点赞！&lt;/p&gt;

&lt;p&gt;##Live Site&lt;/p&gt;

&lt;p&gt;上线一看，只有部分文章的内容load不出来，导致无法编辑和预览。抓包看了下，HTTP request call全部都是成功了，也就是说问题没有出在后台，至少浏览器已经成功获取content。那问题很显然出在JS代码上，而获取markdown文档代码其实只有一行：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$http.get(url).then(function(response) { ... } ;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;不幸的是&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;$http.get&lt;/code&gt;函数都没执行完就挂了，抛出了这样的exception &lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Syntax Error: undefined token D&lt;/code&gt;。看到这里同学们肯定都笑了，这不就是JSON没处理好嘛，不怨别人，backend server返回的JSON格式不对。&lt;/p&gt;

&lt;p&gt;可剧情在这里发生了反转，我们在后台使用Azure Blob作为storage，无论是markdown文档，还是图片，视频，都以Blob (binary large object)进行存储，也就是说后台把数据传送给前台就是一串byte。而且，会在response headers里面加上这样一条&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;content-type: applicaiton/octet-stream&lt;/code&gt;，而不是常见的&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;applicaiton/json&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;问题到这里变得比较清晰了，backend server显式地申明response的content-type为任意二进制数据，为何AngularJS坚持要把数据当JSON解析呢？&lt;/p&gt;

&lt;p&gt;##打开AngularJS $http 瞧一瞧
带着疑惑我打开anguarjs关于$http的代码，看看究竟是什么样的逻辑。&lt;a href=&quot;https://github.com/angular/angular.js/blob/master/src/ng/http.js&quot;&gt;AngularJS/src/ng/http.js&lt;/a&gt;代码的第一段就把故事交代了，我们看&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;var APPLICATION_JSON = &apos;application/json&apos;;
var CONTENT_TYPE_APPLICATION_JSON = {&apos;Content-Type&apos;:APPLICATION_JSON + &apos;;charset=utf-8&apos;};
var JSON_START = /^\s*(\[|\{[^\{])/;
var JSON_END = /[\}\]]\s*$/;
var JSON_PROTECTION_PREFIX = /^\)\]\}&apos;,?\n/;

function defaultHttpResponseTransform(data, headers) {
  if (isString(data)) {
    // strip json vulnerability protection prefix
    data = data.replace(JSON_PROTECTION_PREFIX, &apos;&apos;);
    var contentType = headers(&apos;Content-Type&apos;);
    if ((contentType &amp;amp;&amp;amp; contentType.indexOf(APPLICATION_JSON) === 0 &amp;amp;&amp;amp; data.trim()) || (JSON_START.test(data) &amp;amp;&amp;amp; JSON_END.test(data))) 
    {
      data = fromJson(data);
    }
  }
  return data;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;我们看到，在浏览器收到HTTP response后，AngularJS会试图对response body的数据进行一些transform，而默认的transform方法就是判断数据是否为JSON。判断的方式有两种&lt;/p&gt;

&lt;ol&gt;
  &lt;li&gt;看Response Headers里面是否有&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;Content-Type&lt;/code&gt;，如果有，判断一下是否为&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;application/json&lt;/code&gt;。&lt;/li&gt;
  &lt;li&gt;使用两个正则表达式&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;JSON_START = /^\s*(\[|\{[^\{])/;&lt;/code&gt;和&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;JSON_END = /[\}\]]\s*$/;&lt;/code&gt;。&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;我们的这条HTTP Response肯定不符合第一条，那么就到了第二条check。只要&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;JSON_START.test(data) &amp;amp;&amp;amp; JSON_END.test(data)&lt;/code&gt;为&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;true&lt;/code&gt;，那么AngularJS就会试图把response data当作JSON来parse。可是这两条正则表达式弱到你可以分分钟想到一个满足条件的值：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;&amp;gt; JSON_START.test(&apos;[a}&apos;) &amp;amp;&amp;amp; JSON_END.test(&apos;[a}&apos;)
&amp;lt; true
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;老师们，你确定这两条正则表达式不是在逗我？&lt;/p&gt;

&lt;p&gt;找到root cause，我们再来看下导火索是啥。提示，我们的response data是markdown文档，你能构造出一个让AngularJS误以为是JSON的文档吗？&lt;/p&gt;

&lt;p&gt;这里我就不卖关子了。我们的用户写的文档，是类似于这样的：&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;[link](some links...)

Function bar definition:

function bar() {
  if (a &amp;gt; b) {
    foo();
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;这是一种典型的API文档。这年头无论是technical writer都开始追求文档书写的轻便化，无论是开源项目（比如AngularJS自己）还是MSDN的一些API文档都开始使用Markdown来书写。&lt;/p&gt;

&lt;p&gt;于是我被爆了，华丽丽地，连肥皂都没捡。&lt;/p&gt;

&lt;p&gt;##那什么时候这个Bug会被修掉呢？
发现问题后我给AngularJS开了一个&lt;a href=&quot;https://github.com/angular/angular.js/issues/10349&quot;&gt;issue&lt;/a&gt;，他们很快确认了这个bug。我建议能不能把这条非常弱的RegEx去掉，或者是实现像Jquery一样的Intelligence。&lt;/p&gt;

&lt;p&gt;他们表示，remove掉是肯定不可能的，由于他们从一开始就有这段代码，一些&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;non-well-behaving&lt;/code&gt;的backend可能偷懒不添加&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;content-type&lt;/code&gt;，把这段remove掉，这些用户一更新AngularJS就挂掉了，这种属于breaking change，只能等到1.4这样的大版本更新。而现在的code base里这个黑科技出现在很多地方，没法简单地修改，所以一时半会儿也没法引入类似Jquery的智能监测。&lt;/p&gt;

&lt;p&gt;现在这个&lt;a href=&quot;https://github.com/angular/angular.js/issues/10349&quot;&gt;issue&lt;/a&gt;被放入了&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;1.4 candidate&lt;/code&gt;中，最终能不能在1.4修复，大家可以继续关注这个issue。NG team的不同成员对这个问题怎么修好像有不同的看法，截至作者发博客，他们已经快要打起来了。&lt;/p&gt;

&lt;p&gt;当然，他们对于&lt;code class=&quot;language-plaintext highlighter-rouge&quot;&gt;[a}&lt;/code&gt;这样的string都能被认为是JSON自己都受不了，于是他们发了一个&lt;a href=&quot;https://github.com/angular/angular.js/blob/master/src/ng/http.js&quot;&gt;pull request&lt;/a&gt;，保证opening/closing brackets是一样的。我觉得吧，这是条不归路。&lt;/p&gt;

&lt;p&gt;##如果AngularJS还没修，我们该怎么workaround呢
AngularJS 1.2正式release和1.3release之间隔了11一个月，如果我们傻傻分不清再等个11个月，早就被老板爆了。不过还好还是有一个比较clean的方式避开这个bug，你只需要在$http的request中显式地申明response content-type，就像这样&lt;/p&gt;

&lt;div class=&quot;language-plaintext highlighter-rouge&quot;&gt;&lt;div class=&quot;highlight&quot;&gt;&lt;pre class=&quot;highlight&quot;&gt;&lt;code&gt;$http.get(url,{responseType:&apos;byteArray&apos;}).then(function(response) { ... } ;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/div&gt;

&lt;p&gt;当然，你是没法从官方文档看到这个奇淫巧计的，他们只是告诉你这里可以填responseType，类型是string，具体参考MDN，&lt;a href=&quot;https://developer.mozilla.org/en-US/docs/DOM/XMLHttpRequest#responseType&quot;&gt;点这里&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;看来我只能把他们吵架的故事告诉我的老板，希望他不要嫌我这个bug fix的太dirty。&lt;/p&gt;

&lt;hr /&gt;
&lt;p&gt;分割线。周四live site被爆了一天，没想到礼拜五我又被爆了，这一次，我被Angular-ui某个库的一个特别stupid的bug玩地死去活来。不过我现在要去打扫了，下次再来和大家讲故事。当然，你可以Donate(ripple: &lt;a href=&quot;https://rippletrade.com/#/send?to=rf25Qv2Y3wbBYBgkXTmHMFeq1kGc6hVaXc&quot;&gt;rf25Qv2Y3wbBYBgkXTmHMFeq1kGc6hVaXc&lt;/a&gt;, btc: &lt;a href=&quot;bitcoin:19yAgh8uDZJNaeVQmh7iiuzE8Y7amkx8St&quot;&gt;19yAgh8uDZJNaeVQmh7iiuzE8Y7amkx8St&lt;/a&gt;)助我一臂之力，早日买到扫地机器人，把省下的时间写博客(陪妹子)。&lt;/p&gt;
</content>
 </entry>
 
 <entry>
   <title>基于触发器-动作框架的桌面分拣器</title>
   <link href="http://www.rebornix.com/2011/10/12/%E5%9F%BA%E4%BA%8E%E8%A7%A6%E5%8F%91%E5%99%A8-%E5%8A%A8%E4%BD%9C%E6%A1%86%E6%9E%B6%E7%9A%84%E6%A1%8C%E9%9D%A2%E5%88%86%E6%8B%A3%E5%99%A8.md/"/>
   <updated>2011-10-12T00:00:00+00:00</updated>
   <id>http://www.rebornix.com/2011/10/12/基于触发器-动作框架的桌面分拣器.md</id>
   <content type="html">&lt;span style=&quot;font-family: 微软雅黑;&quot;&gt;     昨晚尝试给Blog中的代码添加高亮，修改了style.css，虽然成功了但是发布文章出现了异常，经常打着打着上面的一段文字就missing了，非常蛋疼。而且正好在纠结Linux kernel中关于task_struct一些字段，索性后来把文章删了。&lt;/span&gt;
&lt;div&gt;&lt;span style=&quot;font-family: 微软雅黑;&quot;&gt;     早上近九点爬起来上Java课，布置了第一个实验（小作业 sigh～），名为“基于触发器-动作框架的桌面分拣器”，听起来十分的拗口。不过对于触发器-动作这个框架倒是不陌生，前些日子很火的&lt;a href=&quot;http://www.ifttt.com&quot; target=&quot;_blank&quot;&gt;ifttt&lt;/a&gt;就是这个原理（要邀请码的留言即可），if this then that。今年最后的大作业也是写一个类似于ifttt的应用，故前些日子学习了下sina的api结果被大神们见笑了。&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;font-family: 微软雅黑;&quot;&gt;     不再扯蛋，实验要完成的要求是：&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;font-family: 微软雅黑;&quot;&gt;     “很多人喜欢把东西随便放在桌面上，导致桌面文件很乱。针对这一问题，我们要求编写一个分拣器，来对不同类型的文件进行归类。”&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;font-family: 微软雅黑;&quot;&gt;   实验要求的触发动作是如果桌面有文件，就根据后缀名把它放入到指定的目录下。&lt;!--more--&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;font-family: 微软雅黑;&quot;&gt;     我花了一点时间，实现了个简单的分拣器，把.txt文件放到txt文件夹下。&lt;/span&gt;&lt;/div&gt;
&lt;div&gt;&lt;span style=&quot;font-family: 微软雅黑;&quot;&gt;     定义一个Task()接口，所有任务都得实现Task接口&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;dp-highlighter&quot;&gt;
&lt;ol class=&quot;dp-j&quot;&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;&lt;span class=&quot;keyword&quot;&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;interface&lt;/span&gt;&lt;span&gt; Task {   &lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;&lt;span&gt;          &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;boolean&lt;/span&gt;&lt;span&gt; THIS();   &lt;/span&gt;&lt;/li&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;          &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;void&lt;/span&gt;&lt;span&gt; THAT();   &lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;&lt;span&gt;    }   &lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div align=&quot;left&quot;&gt;&lt;span style=&quot;font-family: 微软雅黑;&quot;&gt;   新建一个任务，就叫它FileClassify吧。override This()和That()，遍历desktop中的文件夹，若文件名中存在.txt，触发条件，新建txt文件夹将其存入。&lt;/span&gt;&lt;/div&gt;
&lt;div align=&quot;left&quot;&gt;&lt;span style=&quot;font-family: 微软雅黑;&quot;&gt;   我检索后缀只是用了String.contains(t)，显然这个是不合理的。对文件进行移动时修改了文件的目录，如果更换所在盘，这个方法就失效了。&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;dp-highlighter&quot;&gt;
&lt;ol class=&quot;dp-j&quot;&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;&lt;span class=&quot;keyword&quot;&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;class&lt;/span&gt;&lt;span&gt; FileClassify &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;implements&lt;/span&gt;&lt;span&gt; Task{   &lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;&lt;span&gt;      &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;private&lt;/span&gt;&lt;span&gt; String[] fileNameList;   &lt;/span&gt;&lt;/li&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;      &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;private&lt;/span&gt;&lt;span&gt; String rootDir;   &lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;&lt;span&gt;      &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;public&lt;/span&gt;&lt;span&gt; FileClassify(){   &lt;/span&gt;&lt;/li&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;            rootDir = &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt;&lt;span&gt; String(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;C:/Users/Rebornix/Desktop&quot;&lt;/span&gt;&lt;span&gt;);   &lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;&lt;span&gt;            nameListCreate();   &lt;/span&gt;&lt;/li&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;      }   &lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;&lt;span&gt;      &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;void&lt;/span&gt;&lt;span&gt; nameListCreate(){   &lt;/span&gt;&lt;/li&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;            File desktop = &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt;&lt;span&gt; File(rootDir);   &lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;&lt;span&gt;            fileNameList = desktop.list();   &lt;/span&gt;&lt;/li&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;      }   &lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;&lt;span&gt;      &lt;/span&gt;&lt;span class=&quot;annotation&quot;&gt;@Override&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/li&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;      &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;void&lt;/span&gt;&lt;span&gt; THAT() {   &lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;&lt;span&gt;            &lt;/span&gt;&lt;span class=&quot;comment&quot;&gt;// TODO Auto-generated method stub &lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/li&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;            File txtDir = &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt;&lt;span&gt; File(rootDir+&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;/txt&quot;&lt;/span&gt;&lt;span&gt;);   &lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;&lt;span&gt;            &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;if&lt;/span&gt;&lt;span&gt;(!txtDir.exists())   &lt;/span&gt;&lt;/li&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;                  txtDir.mkdir();   &lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;&lt;span&gt;            File tempFile,fileNew;   &lt;/span&gt;&lt;/li&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;            &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;for&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;int&lt;/span&gt;&lt;span&gt; i = &lt;/span&gt;&lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;&lt;span&gt;; i &amp;lt; fileNameList.length; i++){   &lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;&lt;span&gt;                  &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;if&lt;/span&gt;&lt;span&gt;(fileNameList[i].contains(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;.txt&quot;&lt;/span&gt;&lt;span&gt;)){   &lt;/span&gt;&lt;/li&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;                        tempFile = &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt;&lt;span&gt; File(rootDir+&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;/&quot;&lt;/span&gt;&lt;span&gt;+fileNameList[i]);   &lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;&lt;span&gt;                        fileNew = &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt;&lt;span&gt; File(rootDir+&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;/txt/&quot;&lt;/span&gt;&lt;span&gt;+fileNameList[i]);   &lt;/span&gt;&lt;/li&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;                        tempFile.renameTo(fileNew);   &lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;&lt;span&gt;                  }   &lt;/span&gt;&lt;/li&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;            }   &lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;&lt;span&gt;               &lt;/span&gt;&lt;/li&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;      }   &lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;&lt;span&gt;      &lt;/span&gt;&lt;span class=&quot;annotation&quot;&gt;@Override&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/li&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;      &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;boolean&lt;/span&gt;&lt;span&gt; THIS() {   &lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;&lt;span&gt;            &lt;/span&gt;&lt;span class=&quot;comment&quot;&gt;// TODO Auto-generated method stub &lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/li&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;            &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;for&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;int&lt;/span&gt;&lt;span&gt; i = &lt;/span&gt;&lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;&lt;span&gt;; i &amp;lt; fileNameList.length; i++){   &lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;&lt;span&gt;                  &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;if&lt;/span&gt;&lt;span&gt;(fileNameList[i].contains(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;.txt&quot;&lt;/span&gt;&lt;span&gt;))   &lt;/span&gt;&lt;/li&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;                        &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;true&lt;/span&gt;&lt;span&gt;;   &lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;&lt;span&gt;            &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;false&lt;/span&gt;&lt;span&gt;;   &lt;/span&gt;&lt;/li&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;      }   &lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;&lt;span&gt;}   &lt;/span&gt;&lt;/li&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;  &lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div align=&quot;left&quot;&gt;
&lt;div align=&quot;left&quot;&gt; &lt;span style=&quot;font-family: 微软雅黑;&quot;&gt;    最后实现一个启动器&lt;/span&gt;&lt;/div&gt;
&lt;div class=&quot;dp-highlighter&quot;&gt;
&lt;ol class=&quot;dp-j&quot;&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;&lt;span class=&quot;keyword&quot;&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;class&lt;/span&gt;&lt;span&gt; Activator {   &lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;&lt;span&gt;      &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;static&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;void&lt;/span&gt;&lt;span&gt; main(String[] args){   &lt;/span&gt;&lt;/li&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;            Activator act = &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt;&lt;span&gt; Activator(&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt;&lt;span&gt; FileClassify());   &lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;&lt;span&gt;      }   &lt;/span&gt;&lt;/li&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;      &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;public&lt;/span&gt;&lt;span&gt; Activator(Object basicTask){   &lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;&lt;span&gt;            &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;if&lt;/span&gt;&lt;span&gt;(((Task)basicTask).THIS()){   &lt;/span&gt;&lt;/li&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;                  ((Task)basicTask).THAT();   &lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;&lt;span&gt;            }   &lt;/span&gt;&lt;/li&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;      }   &lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;&lt;span&gt;}  &lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div align=&quot;left&quot;&gt;
&lt;div align=&quot;left&quot;&gt;&lt;span style=&quot;color: #7f0055;&quot;&gt;&lt;strong&gt;      实现结果： &lt;/strong&gt;&lt;/span&gt;&lt;/div&gt;
&lt;div align=&quot;left&quot;&gt;

&lt;span&gt;&lt;a href=&quot;http://rebornix.com/wp-content/uploads/rebornix.com/2011/10/filter.jpg&quot;&gt;&lt;img class=&quot;aligncenter size-full wp-image-104&quot; title=&quot;filter&quot; src=&quot;http://rebornix.com/wp-content/uploads/rebornix.com/2011/10/filter.jpg&quot; alt=&quot;&quot; width=&quot;473&quot; height=&quot;148&quot; /&gt;&lt;/a&gt;&lt;/span&gt;

&amp;nbsp;

&lt;/div&gt;
&lt;div align=&quot;left&quot;&gt;&lt;span style=&quot;font-family: 微软雅黑;&quot;&gt;     功能成功的实现，方法很弱，体验很蹩脚，尤其是和正在用的Fences一比。Fences需要把文件手动调整到fence下，这里是自动调整到目录里。要是可以调用fences的api就好了。&lt;/span&gt;&lt;/div&gt;
&lt;div align=&quot;left&quot;&gt;&lt;span class=&quot;Apple-style-span&quot; style=&quot;font-family: 微软雅黑;&quot;&gt;     去吃饭了，回去再优化代码添加功能吧～&lt;/span&gt;&lt;/div&gt;

&lt;hr /&gt;

&lt;div style=&quot;font-family: 微软雅黑;&quot; align=&quot;left&quot;&gt;      仓促之下写的THAT()实在是弱爆了！今天听了IBM的CTO Linux Paul的讲座，关于Code Style的一段，自觉这个代码确实不精简，而且功能不靠谱，纯属人工手段。对于后缀的判断其实可以找出最后的一个“.”并将其后面的字符串取出，即为suffix。对每个文件查询其suffix，若已有此文件夹，置入，反之先新建此文件夹。如下：&lt;/div&gt;
&lt;div class=&quot;dp-highlighter&quot;&gt;
&lt;ol class=&quot;dp-j&quot;&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;&lt;span class=&quot;keyword&quot;&gt;public&lt;/span&gt;&lt;span&gt; String suffixDefine(String name){   &lt;/span&gt;&lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;&lt;span&gt;        &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;int&lt;/span&gt;&lt;span&gt; dotPos = name.lastIndexOf(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&apos;.&apos;&lt;/span&gt;&lt;span&gt;);    &lt;/span&gt;&lt;/li&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;        String suffix = &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt;&lt;span&gt; String();   &lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;&lt;span&gt;           &lt;/span&gt;&lt;/li&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;        &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;if&lt;/span&gt;&lt;span&gt;(dotPos &amp;lt; name.length()&amp;amp;&amp;amp; dotPos &amp;gt; &lt;/span&gt;&lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;&lt;span&gt;){   &lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;&lt;span&gt;            suffix = name.substring(dotPos + &lt;/span&gt;&lt;span class=&quot;number&quot;&gt;1&lt;/span&gt;&lt;span&gt;); &lt;/span&gt;&lt;span class=&quot;comment&quot;&gt;// not dotPos !  &lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/li&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;            &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;return&lt;/span&gt;&lt;span&gt; suffix;   &lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;&lt;span&gt;        }   &lt;/span&gt;&lt;/li&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;        &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;else&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;&lt;span&gt;                      &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;return&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;;   &lt;/span&gt;&lt;/li&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;    }   &lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;&lt;span&gt;    &lt;/span&gt;&lt;span class=&quot;annotation&quot;&gt;@Override&lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/li&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;    &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;public&lt;/span&gt;&lt;span&gt; &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;void&lt;/span&gt;&lt;span&gt; THAT() {   &lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;&lt;span&gt;        &lt;/span&gt;&lt;span class=&quot;comment&quot;&gt;// TODO Auto-generated method stub &lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/li&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;        System.out.println(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;Yes!&quot;&lt;/span&gt;&lt;span&gt;);   &lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;&lt;span&gt;        File tempFile;    &lt;/span&gt;&lt;/li&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;        File fileNew;   &lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;&lt;span&gt;        String suffix = &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt;&lt;span&gt; String(&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;);   &lt;/span&gt;&lt;/li&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;        &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;for&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;int&lt;/span&gt;&lt;span&gt; i = &lt;/span&gt;&lt;span class=&quot;number&quot;&gt;0&lt;/span&gt;&lt;span&gt;; i &amp;lt; fileNameList.length; i++){   &lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;&lt;span&gt;            &lt;/span&gt;&lt;span class=&quot;comment&quot;&gt;// define the suffix &lt;/span&gt;&lt;span&gt;  &lt;/span&gt;&lt;/li&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;            suffix = suffixDefine(fileNameList[i]);   &lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;&lt;span&gt;            &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;if&lt;/span&gt;&lt;span&gt;(suffix != &lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;&quot;&lt;/span&gt;&lt;span&gt;){   &lt;/span&gt;&lt;/li&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;                File subDir = &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt;&lt;span&gt; File(rootDir + &apos;/&apos; + suffix);   &lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;&lt;span&gt;                &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;if&lt;/span&gt;&lt;span&gt;(!subDir.exists())   &lt;/span&gt;&lt;/li&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;                    subDir.mkdir();   &lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;&lt;span&gt;                tempFile = &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt;&lt;span&gt; File(rootDir+&lt;/span&gt;&lt;span class=&quot;string&quot;&gt;&quot;/&quot;&lt;/span&gt;&lt;span&gt;+fileNameList[i]);   &lt;/span&gt;&lt;/li&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;                fileNew = &lt;/span&gt;&lt;span class=&quot;keyword&quot;&gt;new&lt;/span&gt;&lt;span&gt; File(rootDir+&apos;/&apos;+suffix+&apos;/&apos;+fileNameList[i]);   &lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;&lt;span&gt;                tempFile.renameTo(fileNew);   &lt;/span&gt;&lt;/li&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;            }   &lt;/span&gt;&lt;/li&gt;
	&lt;li&gt;&lt;span&gt;        }   &lt;/span&gt;&lt;/li&gt;
	&lt;li class=&quot;alt&quot;&gt;&lt;span&gt;    }  &lt;/span&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div  style=&quot;font-family: 微软雅黑;&gt;      当然，这个仅仅是Java的小作业，实现难度几乎没有，关键在于一个触发的思想。除了触发的算法要写的精妙，必不可少的就是各个应用的API，否则老是分拣器必然没意思。我比较喜欢的是Sina Weibo 和 Evernote的API，后者调用的机制可能略显复杂，但原理相同。用户授权都是使用OAuth 2.0 .我只是先弱弱的使用weibo的SDK，把分拣之后的文件名发条微博，其他的以后再试。包括JavaScript版本。&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
</content>
 </entry>
 
 
</feed>
