<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>WangJie&apos;s blog</title><description>分享生活中的乐趣</description><link>https://iwj.moe/</link><item><title>Arc 初体验</title><link>https://iwj.moe/blog/post/early-review-of-arc/</link><guid isPermaLink="true">https://iwj.moe/blog/post/early-review-of-arc/</guid><pubDate>Sat, 16 Jul 2022 15:16:48 GMT</pubDate><content:encoded>&lt;p&gt;Arc 是 &lt;a href=&quot;https://thebrowser.company/&quot;&gt;the browser company&lt;/a&gt; 推出的浏览器。很早之前就有过宣传，号称是划时代的新型浏览器。而在六月底，终于开放了测试。我在收到邮件后第一时间下载测试。并且重度使用至今，已经两个多礼拜了。感觉真的很棒很炫酷。这里写一下评价和使用感受。&lt;/p&gt;
&lt;blockquote class=&quot;twitter-tweet&quot;&gt;&lt;p lang=&quot;en&quot; dir=&quot;ltr&quot;&gt;it&amp;#39;s called Arc &lt;a href=&quot;https://t.co/1dl2H2Ca4e&quot;&gt;pic.twitter.com/1dl2H2Ca4e&lt;/a&gt;&lt;/p&gt;&amp;mdash; The Browser Company (@browsercompany) &lt;a href=&quot;https://twitter.com/browsercompany/status/1516432026668257294?ref_src=twsrc%5Etfw&quot;&gt;April 19, 2022&lt;/a&gt;&lt;/blockquote&gt; &lt;script async src=&quot;https://platform.twitter.com/widgets.js&quot; charset=&quot;utf-8&quot;&gt;&lt;/script&gt;&lt;!-- more --&gt;&lt;h1&gt;做出的改变&lt;/h1&gt;
&lt;h3&gt;标签页 &amp;amp; 书签&lt;/h3&gt;
&lt;p&gt;Arc 最大的特点就是对于传统浏览器标签页 (tab) 的改变。传统浏览器会将标签页平铺。很容易导致打开一大堆标签页无法管理的情况。虽然 Chrome 和 Firefox 都相继支持了分组和固定的功能。但是对于标签页的管理仍然困难重重。因此也有诸多浏览器扩展试图弥补这一点。但是终究有所局限。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/images/early-review-of-arc/LGh3USj15Oy27IR.png&quot; alt=&quot;Arc Sidebar&quot;&gt;&lt;/p&gt;
&lt;p&gt;而 Arc 将标签页和书签的管理相互结合。最明显的就是左上方只有 icon 的 favorite 列表。对于很多常用的基于浏览器的应用，比如邮箱、IM 之类的应用，常驻一个标签页使得切换起来非常方便。我此前的主力浏览器一直是 Chrome，在使用 Chrome 的过程中我也经常会把 Gmail、telegram 这些始终会开一个的应用 pin 在最前面。可是 Chrome 在 pin 了之后的标签页非常小，点起来很不方便。而且每次因为重启关闭浏览器之后再打开，标签页也不会保留。而 Arc 直接将其固定，可以说是完美解决了这一问题。不过 favorite 列表目前也被限制为了最多 8 个。&lt;/p&gt;
&lt;p&gt;另外就是标签页就等于书签。其实在日常使用过程中就有这样的感受。每次使用过程中，在一段时间内频繁打开的始终就那些页面。可是一直开着又会很占资源，关了之后再打开又很麻烦。而 Arc 直接将书签和标签页组合。会有一根线将标签页进行区分。下面的页面在关闭后，tab 也会直接消失。而上面的则关闭后 tab 还会保留。需要时就只要点一下，就像没有关闭一样，切换起来非常自然。而下面的临时标签页也可以很方便的一键关闭。可以通过这种方式很快的清理资源，整理思绪。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/images/early-review-of-arc/MLcfsYOupIHad6m.png&quot; alt=&quot;Chrome Screenshot&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;这是我的 Chrome 浏览器的画风：一大堆常用的书签。我每次想要使用都要点一下打开一个新的页面，要不然就是从一大堆打开的标签页中寻找。相比 Arc 使用起来很麻烦。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;Space &amp;amp; Folder&lt;/h3&gt;
&lt;p&gt;Arc 将标签管理作为核心能力，提供了 space 功能。这也是当你打开 Arc 时非常明显告诉你的一个功能。浏览器作为目前最为重要，不可或缺的软件。不管是在工作、学习还是娱乐都会用到。而在通常的浏览器当中。对于此类状态的切换十分不顺畅。虽然大部分浏览器也可以多窗口（window）。而且近些年，也提供了直接「重新打开关闭的窗口内的所有标签页」之类的功能。但是问题在于每次重新打开的过程都会非常的卡顿。而且也不是每次都需要打开所有的标签页。如果所有窗口的所有页面都保持打开，那么会非常吃资源。而不一直打开，需要切换时又会非常麻烦。&lt;/p&gt;
&lt;p&gt;而 Arc 除了直接将标签页和书签结合之外，同时提供了 space 的功能。你可以通过点击图标或者快捷键快速切换。而因为书签就是标签页。你也不需要一次性打开所有相关的页面，只要直接点击标签页切换就行了。而因为标签页就是书签。你也不需要为了区分各种状态，去费力的分类或是整理自己的书签。&lt;/p&gt;
&lt;p&gt;这点真的是非常好用，也非常实用。&lt;/p&gt;
&lt;h3&gt;地址栏 = command bar&lt;/h3&gt;
&lt;p&gt;现在很多重大的产品都提供了聚合众多功能的 command bar。诸如 Mac 系统自带的 Spotlight，最为著名的 Alfred，以及最近大伙的 Raycast。然后 GitHub、Vercel 等等很多知名的网站。毋庸置疑这确实是非常高效易用的方案。传统浏览器在这方面也有很多尝试：有非常多类似 Alfred 的浏览器扩展，Chrome 的地址栏也一直可供扩展使用，并且也逐渐内建支持了搜索以外诸如标签页切换之类的功能。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/images/early-review-of-arc/DtrlsKOxRfU1jL9.png&quot; alt=&quot;Arc command bar&quot;&gt;&lt;/p&gt;
&lt;p&gt;而 Arc 则直接将浏览器最主要的地址栏缩小到左上角的一块，并且只显示域名部分。而主力推荐使用 &lt;code&gt;Command + T&lt;/code&gt; 快捷键唤起地址栏，这也是传统浏览器打开新标签页的快捷键。其实就在传统浏览器当中，在需要访问的各种站点越来越多的时候，当要打开一个页面通常也是在地址栏直接搜索或者输域名，而不是在书签栏里寻找。Arc 对此做成的结合和改变无疑在现代使用中非常方便快捷。&lt;/p&gt;
&lt;h3&gt;Note &amp;amp; Easel&lt;/h3&gt;
&lt;p&gt;这应该算是比较无关紧要的功能了。但是也是 Arc 精心设计和大力宣传的一点。我这两个功能使用的不多。因为我已经有稳定信息汇聚和整理工作流了。但是这里也简单介绍一下。&lt;/p&gt;
&lt;p&gt;Arc 的 Note 类似 Notion，是一个基于 Block 的富文本编辑功能。而 Easel 类似 Figma 新出的 FigJam 功能。区别在于 Arc 提供的功能较少，但是因为基于原生实现。有着非常好的性能。并且和 Arc 自带的网页元素截图功能，标签页功能结合的非常密切。如果整体作为一个工作流，使用起来会非常方便。&lt;/p&gt;
&lt;h3&gt;其他一些锦上添花的功能&lt;/h3&gt;
&lt;h6&gt;Little Arc&lt;/h6&gt;
&lt;p&gt;开启这个功能之后在其他所有软件中点击链接都会在当前桌面（Desktop）开启一个单独的窗口显示。相比传统浏览器有了两点极大的改善：&lt;/p&gt;
&lt;p&gt;首先在以往浏览器会直接在浏览器打开，如果浏览器不在同一个桌面，那么会感觉工作流程被突然打断，会很不爽。而 Little Arc 直接在当前桌面大概，非常平滑自然，感觉就像所有软件都有了链接预览功能一样。非常舒服。&lt;/p&gt;
&lt;p&gt;其次，他的窗口和传统浏览器的窗口不一样，传统浏览器有两种窗口，一种是正常的 window，另一种是没有书签栏之类其他功能的 frame。前者会切换桌面，性能很差，而后者则窗口默认很小，切无法切换成正常 window 里的 tab。相反，Arc 的独立窗口性能非常好，而且可以一键切换成正常的 tab。在其他软件和浏览器结合的工作场景中非常流畅自然，体验非常好。&lt;/p&gt;
&lt;h6&gt;Previews&lt;/h6&gt;
&lt;p&gt;Arc 为个别常用网站定制了预览功能。比如 Gmail、Figma 等。这些网站只要鼠标移动到标签页上，就可以弹出一个小窗，以定制的原生 UI 显示网站的内容。因为是原生实现，而且是定制的 UI，并非是原本网页的缩小预览，性能和体验都会非常好。然而这个纯粹是锦上添花，支持的网站非常少，而且也无法满足所有人的偏好，开发起来成本也会比较高。&lt;/p&gt;
&lt;p&gt;如果之后能将这个能力提供给第三方开发者来开发可能会有所作为。目前真的纯粹是锦上添花。&lt;/p&gt;
&lt;h6&gt;Library&lt;/h6&gt;
&lt;p&gt;Arc 在左下角隐藏了一个 Library 功能，可以浏览电脑里的文件，在拖拽到网页的场景时会非常方便。但是 mac 本身就可以将 folder 固定在 dock。并且交互已经挺好了。所以这个功能也是纯粹的锦上添花。&lt;/p&gt;
&lt;p&gt;但是值得一提的是，在 Arc 中，文件、包括下载的文件，都可以放在 tab 栏中，也就是说你可以把文件，比如 PDF 之类的，也和书签之类的放在一起管理。在经常需要把下载的 PDF 和网页一起管理的场景来说是非常方便了。&lt;/p&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;其他可能还有一些功能，这里就不完全介绍了。以上说的都是一些比较重要，核心的改变和功能。其他的如果想了解更多可以看官方的 Notion Page：&lt;a href=&quot;https://browserinc.notion.site/Arc-Resources-e5dfd3226b2544b0816f6778b32fc6a4&quot;&gt;Arc Resources&lt;/a&gt;。&lt;/p&gt;
&lt;h1&gt;个人感受和看法&lt;/h1&gt;
&lt;p&gt;上面主要是对于 Arc 功能的介绍，也包含了我感觉比较好的一些感受。这里在整体介绍一些和功能无关的。&lt;/p&gt;
&lt;p&gt;其实前几年看到 the browser company 的宣传，对他们号称的「未来的」浏览器，我的期望非常高，而我第一次打开 Arc，除了开屏动画，感觉并没有非常的惊艳。之前从宣传的视频或者媒体的信息来看，可能是一种全新的浏览器工作方式。比如所有网页都在一个类似锤子的无限屏的无限扩展空间内展示。然而现在还是基于传统的标签页的方式。但是目前的 Arc，可以说在创意、设计、未来，这些点之间达成了一个目前最佳的平衡。目前的 Arc 应该不只是这家公司最终的愿景。但是也带来了足够的革新。&lt;/p&gt;
&lt;p&gt;目前来说，没有必要，也没有可能，完全从零开发一款浏览器。Arc 完全是基于 Chromium 开发的。而 Arc 相比其他的一些新兴浏览器产品体验要更好。&lt;/p&gt;
&lt;h3&gt;基于 Chromium，确保了下限&lt;/h3&gt;
&lt;p&gt;其他的很多浏览器也是在 Chromium 之上包壳制作的，但是很多浏览器会把 chromium 作为一个 fork，在其之上大作修改。砍掉很多 chromium 自带的功能，然后有加入一些鸡肋的功能。定制一套和原本类似的 UI。强行按上一个自己的品牌。甚至很多年前国内的一些浏览器还会愚蠢的占很大一个空间展示一个自己的 LOGO。我不知道那样的意义何在。而且过多的修改导致没法跟随上游的更新。其实就是一个垃圾的 fork。并没有任何意义。&lt;/p&gt;
&lt;p&gt;而 Arc 基于 Chromium 之上对他来说则是确保了这款浏览器的下限。Arc 是一个功能上完全没有阉割的 Chromium。所有内置的页面都有（可以通过 &lt;code&gt;chrome://chrome-urls/&lt;/code&gt; 查看并打开）。这就决定了用户从 Chrome 或是 Chromium 切换过来没有任何的体验下降。我之前尝试很多各种各样的产品，又最终放弃，很大程度都是因为那些产品做了一部分额外的功能，但是又丢失了原有的一些功能。而 Arc 则对 Chrome 所有的功能都有。我从 Chrome 切换到 Arc 的过程非常平滑和自然。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/images/early-review-of-arc/kWVZILNd6Yv1tPM.png&quot; alt=&quot;Arc chromium page with devtool&quot;&gt;&lt;/p&gt;
&lt;p&gt;在初次打开时，可以直接登录 Google 账号，之前的所有浏览记录和扩展都会被同步过来。我此前在 Chrome 会有一大堆扩展，完全无法离开。而即使切换到 Arc 也不会丧失这些能力。&lt;/p&gt;
&lt;p&gt;但是另一个视角来看，这也可以说是 Arc 的妥协。如果没有这些能力，可能我在切换的过程会很痛苦。以至于我最终无法完全切换过来。不过还是更启动更多更高程度的创新。我相信足够的创新可以重新定义使用习惯和交互。&lt;/p&gt;
&lt;h3&gt;极高的性能和额外的优化&lt;/h3&gt;
&lt;p&gt;Arc 最大的特点就是他优秀的 UI 和交互。而在这样炫酷的 UI 和交互之上，他的体验和流畅程度相比 Chrome 完全没有下降，反而还有所提升。除了他完全原生的 UI。而且正是因为他把标签和书签结合的设计。他可以更方便自然的优化闲置标签页。从而带来比 Chrome 更好的性能体验。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/images/early-review-of-arc/P5kDIabiJN3TmXO.png&quot; alt=&quot;Arc Task Manager&quot;&gt;&lt;/p&gt;
&lt;p&gt;性能是我刚切换来时最大的顾虑，因为我在使用 Chrome 的过程饱受糟糕的性能和侵占大量电脑资源的困扰。而切换到 Arc 在性能方面反而更好，这点是着实惊艳到我的。光是这点就给我足够一直使用 Arc 的理由。&lt;/p&gt;
&lt;h1&gt;总结&lt;/h1&gt;
&lt;p&gt;Arc 目前还在积极的开发当中。甚至我后来在和朋友的推荐中才知道这次开放下载还不是完全可供所有人使用。现在似乎还得先在官网加入 wait list。不过我这些天对 Arc 的尝试着实久违地收到来自这样优秀和创新的产品的触动。&lt;/p&gt;
</content:encoded></item><item><title>SPA 应用切换页面如何保留滚动位置及可能遇到的问题</title><link>https://iwj.moe/blog/post/Scroll-Restoration-in-SPA/</link><guid isPermaLink="true">https://iwj.moe/blog/post/Scroll-Restoration-in-SPA/</guid><pubDate>Wed, 10 Nov 2021 22:53:49 GMT</pubDate><content:encoded>&lt;p&gt;在应用中通常会有在页面切换时保留滚动位置的需求。一种方式是自行控制滚动状态。但是其实浏览器很早之前就可以自动保存页面的滚动位置。&lt;/p&gt;
&lt;p&gt;可以参考 &lt;a href=&quot;https://developers.google.com/web/updates/2015/09/history-api-scroll-restoration&quot;&gt;History API: Scroll Restoration&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;浏览器的滚动位置保存在 history 中。在使用 history API 手动控制 url 时也可以保留滚动的位置。&lt;/p&gt;
&lt;p&gt;但是如果在 SPA 应用中，页面切换时，如果首屏渲染的页面高度不够原本的滚动位置，就会出现滚动位置不对的问题。&lt;/p&gt;
&lt;!-- more --&gt;&lt;h3&gt;示例&lt;/h3&gt;
&lt;p&gt;下面以 React + React Router 为例，演示一下这个问题。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;export default function BasicExample() {
  return (
    &amp;lt;Router&amp;gt;
      &amp;lt;Switch&amp;gt;
        &amp;lt;Route exact path=&amp;quot;/&amp;quot;&amp;gt;
          &amp;lt;ItemList /&amp;gt;
        &amp;lt;/Route&amp;gt;
        &amp;lt;Route path=&amp;quot;/item/:id&amp;quot;&amp;gt;
          &amp;lt;Item /&amp;gt;
        &amp;lt;/Route&amp;gt;
      &amp;lt;/Switch&amp;gt;
    &amp;lt;/Router&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;有两个页面，分别是一个 item list 和 item 的 detail 页。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;function ItemList() {
  const list = useMemo(() =&amp;gt; {
    return Array.from({ length: 50 }, (_, i) =&amp;gt; {
      return (
        &amp;lt;Link to={`/item/${i}`} key={i}&amp;gt;
          &amp;lt;div
            style={{
              width: 200,
              height: 100
            }}
          &amp;gt;
            {i}
          &amp;lt;/div&amp;gt;
        &amp;lt;/Link&amp;gt;
      );
    });
  }, []);

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h2&amp;gt;ItemList&amp;lt;/h2&amp;gt;
      {list}
    &amp;lt;/div&amp;gt;
  );
}

function Item() {
  const history = useHistory();

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h2&amp;gt;Item&amp;lt;/h2&amp;gt;
      &amp;lt;button onClick={() =&amp;gt; history.goBack()}&amp;gt;back&amp;lt;/button&amp;gt;
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Item Detail 页有按钮可以返回列表页。也可以直接用浏览器的返回按钮。&lt;/p&gt;
&lt;p&gt;可用代码的完整内容可以在 &lt;a href=&quot;https://codesandbox.io/s/save-scroll-position-example-9lndo?file=/example.js&quot;&gt;CodeSandbox&lt;/a&gt; 查看。（但是在 iframe 的预览框里没法保留位置，需要在新页面中打开预览地址）&lt;/p&gt;
&lt;h3&gt;无法保留滚动位置的情况&lt;/h3&gt;
&lt;p&gt;但是一些开发的时候，可以会默认进行一个 loading 的操作。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;
function ItemList() {
  const list = useMemo(() =&amp;gt; {
    return Array.from({ length: 50 }, (_, i) =&amp;gt; {
      return (
        &amp;lt;Link to=&amp;quot;/about&amp;quot;&amp;gt;
          &amp;lt;div
            style={{
              width: 200,
              height: 100
            }}
          &amp;gt;
            {i}
          &amp;lt;/div&amp;gt;
        &amp;lt;/Link&amp;gt;
      );
    });
  }, []);

  const [loading, setLoading] = useState(true);

  useEffect(() =&amp;gt; {
    setLoading(false);
  }, []);

  return (
    &amp;lt;div&amp;gt;
      &amp;lt;h2&amp;gt;ItemList&amp;lt;/h2&amp;gt;
      {loading ? &amp;quot;loading&amp;quot; : list}
    &amp;lt;/div&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这时在第一次 render 时。页面上只有 loading，而没有渲染整个列表（虽然这个列表之前被渲染过了。他的数据可以被缓存下来）。&lt;/p&gt;
&lt;h3&gt;推荐的最佳实践&lt;/h3&gt;
&lt;p&gt;所以如果需要保留滚动位置，必须缓存之前的页面的数据。这有很多种办法实现。缓存或者全局状态都可以。&lt;/p&gt;
&lt;p&gt;而最简单便捷的一种方式是使用 &lt;a href=&quot;https://swr.vercel.app/&quot;&gt;SWR&lt;/a&gt; 这个库。这是 Vercel 的 data fetching 库。自带了缓存的功能。使用起来非常方便。&lt;/p&gt;
&lt;p&gt;下面是他的官方示例。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;import useSWR from &amp;#39;swr&amp;#39;

function Profile () {
  const { data, error } = useSWR(&amp;#39;/api/user/123&amp;#39;, fetcher)

  if (error) return &amp;lt;div&amp;gt;failed to load&amp;lt;/div&amp;gt;
  if (!data) return &amp;lt;div&amp;gt;loading...&amp;lt;/div&amp;gt;

  // render data
  return &amp;lt;div&amp;gt;hello {data.name}!&amp;lt;/div&amp;gt;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这也就是为什么 &lt;a href=&quot;https://swr.vercel.app/docs/getting-started&quot;&gt;SWR 的 demo&lt;/a&gt; 中是在没有 data 时 return loading，而不是在 isValidating 的时候 return loading。&lt;/p&gt;
&lt;h3&gt;如何排查这样的问题&lt;/h3&gt;
&lt;p&gt;如果代码比较复杂的时候很难确定是在什么地方导致列表没有渲染。这种情况如果要排查的话可以用到 React Devtool 的 Profiler 功能。&lt;/p&gt;
&lt;p&gt;先从列表页进入内容详情页。然后点 profiler 页的 Record 按钮。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/images/Scroll-Restoration-in-SPA/iEvmgrcj2F687Py.png&quot; alt=&quot;Screen Shot 2021-11-11 at 00.15.57.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后返回列表页。然后 Stop Profiling。&lt;/p&gt;
&lt;p&gt;之后就可以看到这期间的页面 rerender 的过程。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/images/Scroll-Restoration-in-SPA/Uni9O5wjs2aELqr.png&quot; alt=&quot;Screen Shot 2021-11-11 at 00.12.02.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;这里可以看到在第二个 frame 中页面切换到了列表页。但是这时并没有渲染列表。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/images/Scroll-Restoration-in-SPA/lizS1kPfmCWKIdu.png&quot; alt=&quot;Screen Shot 2021-11-11 at 00.12.30.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;在第三个 frame 中列表元素才渲染出来。&lt;/p&gt;
&lt;p&gt;所以就可以知道没有渲染列表元素的组件层级。然后就可以比较好的缩小排查范围了。&lt;/p&gt;
&lt;p&gt;（但是很蛋疼的是组件全是 Anonymous。又可以写一篇这方面的最佳实践了_(:з」∠)_&lt;/p&gt;
</content:encoded></item><item><title>Git 工作流程的演化</title><link>https://iwj.moe/blog/post/progressive-of-git-workflow/</link><guid isPermaLink="true">https://iwj.moe/blog/post/progressive-of-git-workflow/</guid><pubDate>Sat, 30 Oct 2021 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;很多人协作的 git 仓库经常会变的非常混乱。但是在有一定规范的情况下还是很好避免的。&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;抖音有一个 pack item 服务。负责包装视频对象上的信息，过滤视频等。每个业务线都可能在视频上加一些自己业务用的字段。这个结构体包含几百个字段。这个服务会有几十个业务线 &amp;amp; 数百个研发提交代码。并且这个服务有上万个实例，滚动升级一次需要几个小时。升级需要排队。这个服务几乎无时无刻不在升级。而这样的服务在抖音还有很多。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;这篇文章会介绍一下 git 工作流程的演化过程。以及我们如何避免团队开发中的 git 工作流程的混乱。&lt;/p&gt;
&lt;!-- more --&gt;&lt;p&gt;&lt;strong&gt;背景&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这是一个混乱的 git 仓库的 log graph。很难追溯历史。甚至我都不知道该从哪个分支切新的分支。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/images/progressive-of-git-workflow/7PVFC6c31Iuy2mi.png&quot; alt=&quot;https://i.loli.net/2021/10/30/7PVFC6c31Iuy2mi.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/images/progressive-of-git-workflow/3UtznT1OX8YD56r.png&quot; alt=&quot;https://i.loli.net/2021/10/30/3UtznT1OX8YD56r.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;这在人多的项目比较常见。但是有一定规范的情况下还是挺好避免的。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;理想情况&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;最理想清晰的 git log graph 是下面这样，一个 feature 接一个 feature，非常清晰。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/images/progressive-of-git-workflow/wifjbIF9mWNx5tC.png&quot; alt=&quot;https://i.loli.net/2021/10/30/wifjbIF9mWNx5tC.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;理想的 git log graph&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;实际情况&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/images/progressive-of-git-workflow/HfvT9ZNRblCo8k4.png&quot; alt=&quot;https://i.loli.net/2021/10/30/HfvT9ZNRblCo8k4.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;通常情况&lt;/p&gt;
&lt;p&gt;但通常因为协同开发会产生分支之间重合的情况。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/images/progressive-of-git-workflow/rkg8wEN7vetdB26.png&quot; alt=&quot;https://i.loli.net/2021/10/30/rkg8wEN7vetdB26.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;更糟的情况&lt;/p&gt;
&lt;p&gt;更糟的情况下分支都来自非常久之前的版本。这样后面的 PR 很容易和之前的代码产生冲突。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/images/progressive-of-git-workflow/iyAHtcGlbKZJWrR.png&quot; alt=&quot;https://i.loli.net/2021/10/30/iyAHtcGlbKZJWrR.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;一种解决方式是在 feature 分支上合并一下主干。&lt;/p&gt;
&lt;p&gt;但是这种情况下之前分支的代码就会被悄无声息的修改。因为通常来说这个 merge 主干产生的 commit 是一个大而杂的 commit。很难注意到其中包含了一些因为解决冲突带来的问题。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/images/progressive-of-git-workflow/2f6UGrb75QHEdjD.png&quot; alt=&quot;https://i.loli.net/2021/10/30/2f6UGrb75QHEdjD.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;（类似这样）&lt;/p&gt;
&lt;p&gt;并且可能一段时间之后，之前代码的作者发现自己的代码被人改了。但是很难从 log 里找出是谁改的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/images/progressive-of-git-workflow/TJbMpFIeHnBCaYG.png&quot; alt=&quot;https://i.loli.net/2021/10/30/TJbMpFIeHnBCaYG.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;通常来说可以用 git blame 很容易找到一行代码最后是被谁修改的。但是因为 merge 操作仍然会保留之前的 commit message，但是主干上的代码经过多次 merge 之后就指不定显示 author 是谁了。。&lt;/p&gt;
&lt;p&gt;并且会导致 log graph 很乱。不知道这个改动是从什么时候开始的。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/images/progressive-of-git-workflow/wRVDlvCmNQbzxpn.png&quot; alt=&quot;https://i.loli.net/2021/10/30/wRVDlvCmNQbzxpn.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;同步上游的推荐方式&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/images/progressive-of-git-workflow/wgqroi2IXRTl9dx.png&quot; alt=&quot;https://i.loli.net/2021/10/30/wgqroi2IXRTl9dx.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;另一种比较好的方式是通过 rebase 上游。让这个分支看起来像是刚刚 checkout 出来的一样。这样 git graph 就不会和其他的交叉。可以很清晰的看出这次 PR 包含了哪些 commit。&lt;strong&gt;并且如果在 rebase 期间解决了冲突，那么这部分变更会明显的显示出他的作者。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/images/progressive-of-git-workflow/IzTasCJd2bchGVf.png&quot; alt=&quot;https://i.loli.net/2021/10/30/IzTasCJd2bchGVf.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;但是如果之前分支上就包括了多个 commit，那么在解决冲突时可能会发生多次冲突，可能需要解决多次冲突。这时可以先在原本分支 squash 一下。把多个 commit 合成一个 commit。这样只需要解决一次冲突了。&lt;/p&gt;
&lt;p&gt;并且很多情况下一次 commit 并不是一个完整的 feature，那么最好也在合并到主干之前先 squash 一下。&lt;/p&gt;
&lt;p&gt;GitLab 可以设置自动 fast-forward 到目标分支，这样可以自动让 log graph 不会交叉。但是 GitHub 没这个功能。所以只能靠大家手动做这件事情&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;开发分支&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/images/progressive-of-git-workflow/iNym6lHYbfscX5q.png&quot; alt=&quot;https://i.loli.net/2021/10/30/iNym6lHYbfscX5q.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;通常来说在 reviewer 比 developer 少很多的情况下会出现开头的情况。因为 PR 到很迟才会被 review。这样也很容易造成 PR 之间冲突。而且如果一个 PR 被放了很久然后又要回过头来解决冲突挺麻烦的。&lt;/p&gt;
&lt;p&gt;这种时候会有个中间的开发分支。这个分支合并的需求比较低（可能没有完整的 review 或者测试，但通常也经过自测并且通过 CI 可以编译）。而一段时间之后再从 dev 尝试合并到主干。这时 review 和测试就会比较完全。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Release 分支&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/images/progressive-of-git-workflow/rV5WMfhRyIq9ax7.png&quot; alt=&quot;https://i.loli.net/2021/10/30/rV5WMfhRyIq9ax7.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;这个和上面的 git graph 是完全一致的。但是会直接合并到主干。类似目前的情况。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/images/progressive-of-git-workflow/DughMZvXk7LWbEq.png&quot; alt=&quot;https://i.loli.net/2021/10/30/DughMZvXk7LWbEq.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;而通常来说 release 分支只有一个（在 git flow 中）。多个版本的情况其实应该非目前版本的代码就暂时不合并。而一个版本确定后也不会再变，所以一般会用 tag 来保证不变（不然任何一个 release 版本都可以 fast forward 到下一个 release。这个带版本号的 release 分支就很鸡肋了。）&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Review&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;理想情况下每个 PR 都应该及时经过 review 并尽快合并。避免迟迟不合并造成和上游冲突导致的风险和开销。&lt;strong&gt;最好情况下每个 PR 都由项目的 PO (primary owner) review，如果没有 PO 则由相关代码的原作者 review，如果是新 feature 则由这部分的 BP (backup person, or business partner) review。&lt;/strong&gt;&lt;/p&gt;
</content:encoded></item><item><title>从字节跳动离职啦</title><link>https://iwj.moe/blog/post/leave-from-ByteDance/</link><guid isPermaLink="true">https://iwj.moe/blog/post/leave-from-ByteDance/</guid><pubDate>Sat, 29 May 2021 11:28:34 GMT</pubDate><content:encoded>&lt;center&gt;
  &lt;img width=&quot;300&quot; heigth=&quot;300&quot; src=&quot;/blog/images/leave-from-ByteDance/OM1Liynx8c2au9p.png&quot; alt=&quot;溜了溜了&quot; /&gt;
&lt;/center&gt;&lt;!-- more --&gt;&lt;p&gt;昨天离职了，又可以写一篇阶段总结了。现在博客好像只剩下个人总结了。不过之后应该可以多一点时间写文了。&lt;/p&gt;
&lt;p&gt;从 19 年开始在字节跳动实习，到现在也快两年了。之前是在基础架构做内部 CI 平台，然后今年一月转岗到抖音做 POI。尽量客观的说一下自己的感受吧。&lt;/p&gt;
&lt;h3&gt;字节的优点&lt;/h3&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;li&gt;工资和工作环境还可以&lt;/li&gt;
&lt;/ol&gt;
&lt;h6&gt;内部轮子多&lt;/h6&gt;
&lt;p&gt;好处是很多事情不用重复去做，基本上所有能力都有人做了封装。同时每个人也都可以把自己想到的功能封装成库或者 CLI 给别人使用。因为公司规模大到了一定程度，内部就像一个社区一样。&lt;/p&gt;
&lt;h6&gt;基础设施&lt;/h6&gt;
&lt;p&gt;公司内几乎所有能想到的能力都做成了 web 服务。从面向研发的计算存储能力，到面向各种职能的各种能力，都有对应的平台。而且还都有很多选择。&lt;/p&gt;
&lt;p&gt;物理机，虚拟机，应用引擎，流式处理，对象存储，CDN，负载均衡等等。&lt;/p&gt;
&lt;p&gt;存储包括：关系存储、文档存储、KV 存储、列式存储、图存储、ES、HDFS、HBase、HTAP 等。&lt;/p&gt;
&lt;p&gt;并且每个存储还提供很多种方案：分库分表、主从模式、分布式。&lt;/p&gt;
&lt;p&gt;消息队列也提供很多种：Kafka、RabbitMQ、RocketMQ 等。&lt;/p&gt;
&lt;p&gt;每一个都做了很好的封装，而且提供了对应的 WEB 平台，各种语言的 SDK。&lt;/p&gt;
&lt;p&gt;上面列的这些都不光包含把开源软件做成服务，还包括了各种自研的解决方案，而且做了良好的封装，而且通过 proxy 或者 shim 等方式对开源产品的 API 进行了兼容。可以隔离研发的理解和学习成本，但是又对于性能和容量上限进行了极大的提示，同时还拥有更低的经济成本。&lt;/p&gt;
&lt;p&gt;然后用户行为分析，内容管理，运营平台，低代码建站，Serverless，工单系统，自动化工具，CI/CD，用户反馈平台等等。基本上能想到的能力都有对应的平台。而且很多都有不止一个，基本上适应所有使用场景。&lt;/p&gt;
&lt;p&gt;而且这些所有都几乎不限量使用。所有东西都可以随便用，随便玩。然后如果有实际场景和业务需要，又可以在合理范围内无限扩容。这爽快的感觉不言而喻。这些有些有专门的部门和团队在做，有专门的产品经理和设计师，也有一些是个人自发做的。但只要做的好就可以获得更多资源做的更大。很多产品基本上随便拿一个都可以商业化。&lt;/p&gt;
&lt;p&gt;这其中的既包括自上而下的也包括自下而上的。几乎每个人，不管是底层的研发，还是每个部门或者业务线的 leader。都可以自发做自己想做的平台、产品和功能。并且可以在公司内自由的推广。公司内就像市场一样，每个人都有各种机会，而且还没有经济成本。&lt;/p&gt;
&lt;h6&gt;相对比较扁平开放&lt;/h6&gt;
&lt;p&gt;除了上面提到的可以自发做很多自己想做的事情之外。在制度上每个人也都有很多的机会去做出推动和变革。比如之前组的一个老哥，一来就对于我萌做的平台的 A11Y，还有代码工程质量做了很大推动。这些没有上级会要求你做，也不会限制你做。只要可以做得好，带来收益，每个人都可以做任何想做的事情。并且还有机会获得上级和同事的支持。&lt;/p&gt;
&lt;p&gt;即使是抖音一线业务这样卷的地方，也都有很多机会去做自己想做的。比如部门里就有一个老哥从一个人推动单测的建设，到获得部门老大的支持，大范围的推广。&lt;/p&gt;
&lt;p&gt;但是这些也都需要有额外的付出，需要真切解决一些实际的问题才可以。不是任何一个人光扯嘴皮就可以的。当然，如果作为 leader 或者负责人确实可以有更大的权力。但是因为相对比较开放的沟通和评价机制，即使是作为老大的角色通常也是可以听取意见并作出改善的。&lt;/p&gt;
&lt;h6&gt;工资和工作环境还可以&lt;/h6&gt;
&lt;p&gt;工资就不说了。在能有还可以的工资之外还可以有不错的工作环境。包括各种可有可无的小恩小惠，可以白嫖零食饮料、办公用品、外设、设备、耗材。这些虽然对于不同人需求不一样。但是字节可以不会因为工资给的多点就克扣这些东西。相比国内公司福利做的还可以，相比国外公司工资还可以。&lt;/p&gt;
&lt;p&gt;（当然也只能说是还可以了。实际上每天还是会对公司里的各种东西各种吐槽。但是这种算是在哪都会有的。不存在做到让每个人都觉得完美的。）&lt;/p&gt;
&lt;h3&gt;字节的缺点&lt;/h3&gt;
&lt;p&gt;这可能是大公司的通病。因为公司的规模非常大，所以即便有着各种管理方式，还是会存在很多低效的事情和很多重复的劳动。我个人的感觉就像是一股缓慢前进的钢铁洪流，很多人就是里面的工具人，甚至是开倒车。虽然上面提到有很多机会。但是机会是哪里都有的，有能力的人在哪里都可以做的很好。而对于大部分人来说就感觉自己做的是无关紧要的破事。会感觉很空虚。&lt;/p&gt;
&lt;p&gt;然后因为内部竞争和整个行业乃至整个市场的环境。每个部门都在疯狂招人。招人的门槛无限降低，什么乱七八糟的人都往里招。所以会感觉水平低的和有问题的人特别多。虽然每天也能接触很多大牛，和很多很强很有想法的合作。但也很难避免被一些傻逼折磨。像我这样心态不好的人就很容易受影响。&lt;/p&gt;
&lt;p&gt;以上的优点和缺点我尽量说的比较客观，因为不管在哪家公司，不同部门之间都会有很大的差距。如果是部门或者因为个别人的感受特别好或者特别差我就不说了。&lt;/p&gt;
&lt;h3&gt;我为什么离职&lt;/h3&gt;
&lt;p&gt;老实说，我做很多事情都没啥特别的理由和特别明确的规划。要不然我应该过得比现在好很多。根本原因是自己不喜欢一成不变，适应了一个环境就想换个环境体验一下。这也是之前从架构转到抖音的根本原因。而且还有着天真的自己做产品的理想以及为了圆一个创业的梦想。&lt;/p&gt;
&lt;p&gt;直接原因主要是抖音这边确实是要卷一点。从我之前一个闲的蛋疼的部门到抖音来还是有点不适应。之前是做内部产品，而且作为前端岗位，通常是一个辅助的角色。工作的内容基本上很轻松就能搞定。然后还搞搞自己想做的事情。然后到了抖音做了 server。很多东西需要自己 owner。而且需要和各种角色和团队对接。加上有业务需求 push。研发相比产品人又少。所以比之前忙了太多。但是因为组里又有很多大牛（比如我的 mentor 和我 mentor 的 mentor）。所以组里的氛围又比较卷。所以还是选择先溜了。&lt;/p&gt;
&lt;h3&gt;关于转岗&lt;/h3&gt;
&lt;p&gt;字节内部活水的情况非常普遍，因为上面提到的疯狂招人，内部也是各种挖人。因为几乎每个部门都巴不得有人能进去干活。所以想转岗是非常容易的。而且内部转岗要比通过外部招聘进入要更加透明。你可以直接找任何一个组的 leader 和成员聊，而且因为在公司内部，除了保密项目之外，每个组在做什么做的咋样也都能直接看到。&lt;/p&gt;
&lt;p&gt;所以如果想换一个环境还是非常容易的，即使是像我这样的前端岗位转到后端岗位也是大有人在。但是这也意味着要放弃一些之前的成果和积累。而且转岗和职级薪资都没有关系。所以不像跳槽可以直接有经济收益。&lt;/p&gt;
&lt;h3&gt;之后做什么&lt;/h3&gt;
&lt;p&gt;离职之后很多朋友问这个，其实就像我为什么离职一样，之后做啥也不特别明确。因为我没有买房和定居的计划，在字节搬砖一年收入也还可以，所以也没啥经济压力。就是想体验一下自由职业者的生活。先去旅行一下。然后重拾一下自媒体，然后做一下自己之前一直做但没时间做的产品。然后看一下收入情况，如果能够生活可能就一直这么下去了。如果不够开销就再回去上班。&lt;/p&gt;
&lt;center&gt;
  &lt;img width=&quot;200&quot; heigth=&quot;200&quot; src=&quot;/blog/images/leave-from-ByteDance/CRfU6goiSZ9p8xk.png&quot; alt=&quot;哎，就是玩儿&quot; /&gt;
&lt;/center&gt;</content:encoded></item><item><title>2020 年终总结</title><link>https://iwj.moe/blog/post/2020-summary/</link><guid isPermaLink="true">https://iwj.moe/blog/post/2020-summary/</guid><pubDate>Sat, 02 Jan 2021 02:59:52 GMT</pubDate><content:encoded>&lt;!-- more --&gt;&lt;p&gt;今年真的经历了很多。年初的疫情其实对我的影响并不大。毕业虽然经历了些困难，但是也有惊无险的过去了。年中一直风平浪静。甚至可以每天和好兄弟们悠哉悠哉地去游泳。可到了十月份，好兄弟们一个个都离开了。兄弟们的离开更加坚定了我的离开。国庆和小刘玩完回来就开始找机会了，还借机去了北京一趟。起初先聊了几个部门。除了两个明确要求工作年限的部门之外，其他每个部门的 leader 一听我有意向都是特别希望能过去。本来觉得聊得都挺不错的。尤其是抖音那边一个全栈的部门。认识了一个在那边的一个很强的学妹。本来很想去那边了，但是到最后准备做决定的时候，还是觉得应该找一个专门的后端岗位。于是想到了 robin 那边。找他介绍了一下。确实看起来那边做的事情更吸引我一些。主要还是内部系统做多了，还是想尝试一下 toC 的。这样对之后单干也有利一点。&lt;/p&gt;
&lt;p&gt;这一波就到十一月了。但是这个月是我这年最黯淡的一个月。已经住了两年的蛋壳公寓突然暴雷。第三年我也是年付的。这波等于损失了 2w+的房租。弄得心情很烦躁。为了减少一点无谓沟通的精力浪费。和乔老哥一起又租了一套房子。在租了之后人还没住过去的时候，为了先把 Aurora 和 Lambda 接过去，一天晚上约了魁少帮忙。结果停车的时候被一个神经病滋事。结果还弄到派出所折腾了半天。&lt;/p&gt;
&lt;p&gt;整个十一月都一直在折腾这些突如其来的破事。就像我在派出所的时候那些民警说的：遇到这样的事情，只能算你倒霉。事情到了十二月初才算到头。最终也是破财消灾。自己也受了不少苦。但是人生真的大起大落。十二月十一日晚，和小刘老师确立了关系。&lt;/p&gt;
&lt;p&gt;失去了一些也才更懂得珍惜。今年领悟的最深的一个道理就是「知足常乐」。确实应该珍惜当下。就像我之前一直盼着人生能突然有一个转机。但是也许转机已经来了。只是你还没有发现。&lt;/p&gt;
&lt;p&gt;十二月十四号，终于转正满半年。虽然最后又被现在的 leader 恶心了一下。但是最后还是顺利发起了转岗流程。并且也赶在元旦前得到了面试通过的消息。至此，2020 年终于落幕了。&lt;/p&gt;
&lt;p&gt;这一年里，对我来说真的是艰难。但是还好有身边的好兄弟们。尤其感谢乔老哥能在我人生至暗的时候帮我一把。感谢这一年来每一个帮助过我的人。21 年，我会更加的珍惜。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;img src=&quot;/blog/images/2020-summary/vhqr65ikmAxz9Ca.jpg&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;最后用今晚这份红酒配拌面来结束这篇文章吧。生活有时就是这么离奇。&lt;/p&gt;
&lt;/blockquote&gt;
</content:encoded></item><item><title>JavaScript 的现状</title><link>https://iwj.moe/blog/post/JavaScript-%E7%9A%84%E7%8E%B0%E7%8A%B6/</link><guid isPermaLink="true">https://iwj.moe/blog/post/JavaScript-%E7%9A%84%E7%8E%B0%E7%8A%B6/</guid><pubDate>Wed, 02 Oct 2019 04:06:48 GMT</pubDate><content:encoded>&lt;p&gt;有些事情我必须要吐槽一下了。&lt;/p&gt;
&lt;!-- more --&gt;&lt;p&gt;JavaScript 此前一直是我比较喜欢的编程语言。类 c 的语法和动态弱类型的特点让他非常容易上手。对于编程的初学者来说非常容易编写。怎么写不容易报错。&lt;/p&gt;
&lt;p&gt;并且它是一个真正意义上的完全跨平台语言。因为它是浏览器环境唯一的编程语言。而在其他几乎任何环境都有 JS 的运行环境。还有著名的 Atwood&amp;#39;s law：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;“Any application that can be written in JavaScript, will eventually be written in JavaScript.”
— Jeff Atwood, Author, Entrepreneur, Cofounder of StackOverflow&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;不知道大家有没有想过为什么是 JS。而不是其他什么语言？&lt;del&gt;我这篇文章是黑 JS 的。之后可能会写一篇写 JS 优点的&lt;/del&gt;&lt;/p&gt;
&lt;h3&gt;基础问题：为什么要有那么多的编程语言？&lt;/h3&gt;
&lt;p&gt;不同的编程语言有什么区别？有什么是某个语言做的不到的吗？&lt;/p&gt;
&lt;p&gt;理论上来说所有编程语言都是图灵完备的。只要一个语言的运行环境实现了对应功能的接口。那么他就可以做到任何事情。&lt;/p&gt;
&lt;p&gt;但是这就够了吗？显然不啊！图灵完备的要求非常低，甚至不需要有条件分支结构。比如被曾被广泛使用的 Fortran。可为什么 Fortran 后来要加入条件分支结构？为什么一直用 Fortran？显然这是个基础功能啊。相对的。随着现代编程语言和软件工程的发展。越来越多的功能为开发者所需要。&lt;/p&gt;
&lt;p&gt;比如模式匹配（pattern matching）：&lt;/p&gt;
&lt;p&gt;很多现代编程语言都有了的功能。&lt;/p&gt;
&lt;p&gt;比如通过模式匹配实现一个阶乘：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-haskell&quot;&gt;factorial 0 = 1
factorial n = n * factorial (n-1)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而 JS 的实现方式：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const fact = N =&amp;gt; N &amp;gt; 0
  ? N * fact(N-1)
  : 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;注意到了吗，不能用模式匹配的话必须判断参数来走向不同的逻辑。&lt;/p&gt;
&lt;p&gt;再举一个实用场景的例子。我萌可能经常需要根据一个“符号”来走向不同的逻辑。比如我萌做一个 rogue 游戏。根据键盘事件来改变人物的坐标，我萌可能会写出这样的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;// 返回一个 tuple，分别表示 X 坐标和 Y 坐标的变化量
const onKeyPress = keycode =&amp;gt; {
  if (keycode === &amp;#39;up&amp;#39;) return [0, 1]
  if (keycode === &amp;#39;down&amp;#39;) return [0, -1]
  if (keycode === &amp;#39;left&amp;#39;) return [-1, 0]
  if (keycode === &amp;#39;right&amp;#39;) return [1, 0]
  // ？？这里该干嘛？？
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而使用模式匹配的话：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-haskell&quot;&gt;onKeyPress &amp;quot;up&amp;quot; = (0, 1)
onKeyPress &amp;quot;down&amp;quot; = (0, -1)
onKeyPress &amp;quot;left&amp;quot; = (-1, 0)
onKeyPress &amp;quot;right&amp;quot; = (1, 0)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;非常清晰。并且可以保证函数的返回值。而试图传入不期望的参数？这个函数的签名压根就不存在。函数的参数会包括在函数的签名中。这样不管多少逻辑分支。时间复杂度都是 O(1)。而如果像 JS 代码那样提前返回的方式。每一次都是 O(n)。如果判断就包括复杂的逻辑，那么耗时将会更久。&lt;/p&gt;
&lt;p&gt;我萌不妨实现一个上面的函数的反函数：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const genKeyCode = pos =&amp;gt; {
  if (pos[0] === 0 &amp;amp;&amp;amp; pos[1] === 1) return &amp;#39;up&amp;#39;
  if (pos[0] === 0 &amp;amp;&amp;amp; pos[1] === -1) return &amp;#39;down&amp;#39;
  if (pos[0] === -1 &amp;amp;&amp;amp; pos[1] === 0) return &amp;#39;left&amp;#39;
  if (pos[0] === 1 &amp;amp;&amp;amp; pos[1] === 0) return &amp;#39;right&amp;#39;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;使用模式匹配：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-haskell&quot;&gt;genKeyCode (0, 1) = &amp;quot;up&amp;quot;
genKeyCode (0, -1) = &amp;quot;down&amp;quot;
genKeyCode (-1, 0) = &amp;quot;left&amp;quot;
genKeyCode (1, 0) = &amp;quot;right&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;很显然，模式匹配是个很好用的功能。所以当然会有人提议给 JS 加入模式匹配的功能 &lt;a href=&quot;https://github.com/tc39/proposal-pattern-matching&quot;&gt;ECMAScript Pattern Matching&lt;/a&gt;。但是这个提案从 tc39 成立之初就有。却至今未被加入 JS 中。这是为啥？这个暂且不说。我萌先来看看前人为 JS 做出的贡献。&lt;/p&gt;
&lt;h3&gt;Lodash&lt;/h3&gt;
&lt;p&gt;Lodash 是 JS 的一个非常实用的工具库。他从 15 年开始加入了 enum&lt;a href=&quot;https://lodash.com/docs/4.17.15#cond&quot;&gt;#cond&lt;/a&gt; 方法。&lt;/p&gt;
&lt;p&gt;使用他我萌可以简化上面的 &lt;code&gt;genKeyCode&lt;/code&gt; 方法：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const genKeyCode = _.cond([
  [_.matches([0, 1]), () =&amp;gt; &amp;#39;up&amp;#39;],
  [_.matches([0, -1]), () =&amp;gt; &amp;#39;down&amp;#39;],
  [_.matches([-1, 0]), () =&amp;gt; &amp;#39;left&amp;#39;],
  [_.matches([1, 0]), () =&amp;gt; &amp;#39;right&amp;#39;],
])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;可以看到。他的作用很简单。就是通过封装。来简化一些常用的逻辑。虽然他无法改变语言本身的实现。但是可以通过封装成函数这种方式来完成这种程序编写的方式。&lt;/p&gt;
&lt;p&gt;这里必须插一句。编程的发展就是从链接到封装再到库。一个程序员的基本素质就是复用现有的实现以及编写可复用的代码。而不断写重复代码就是及其可耻的开倒车行为。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/images/JavaScript-%E7%9A%84%E7%8E%B0%E7%8A%B6/w4NiHvfnDprQyEb.png&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;之前在 review 代码的时候有个场景。编写了大量奇怪的代码来实现用 Lodash 很容易实现的功能。这感觉就和写 C++ 却不用 STL 一样。这样的为啥还要写 ES6 再编译成 ES5？你咋不直接写 ES5？很多人就是这样的自相矛盾体。当时看到这个回复我是倒吸一口凉气。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3&gt;TypeScript&lt;/h3&gt;
&lt;p&gt;TS 是个很流行的东西。&lt;del&gt;很多人对 TS 一知半解就对着 TS 一通狂吹&lt;/del&gt;&lt;/p&gt;
&lt;p&gt;他能流行起来的一个原因可能就是因为 vscode 了。编辑器的原生支持让 TS 具有最好的工具链和生态。让很多人得以有机会尝试 TS 并从 TS 中收益。但是 TS 这个语言还是有很多槽点：&lt;/p&gt;
&lt;p&gt;TS 已经具备了函数重载的能力。但为什么只允许声明重载而不允许实现重载？&lt;/p&gt;
&lt;p&gt;很多人觉得 TS 只是 JavaScript with type？那为啥不直接用 Flow ？&lt;/p&gt;
&lt;p&gt;TS 一点也没改变吗？不。TS 加入了 enum，是一个全新的东西，而并非单纯语法层面上的改动。&lt;/p&gt;
&lt;p&gt;TS 其实作为一门全新的语言。他完全可以做的更多，做的更好。他已经有能力引领 JS 的发展。可是在 TS 得势之后。他就变得小心翼翼。可能是生怕走错一步就失去现在的口碑和声誉。&lt;/p&gt;
&lt;p&gt;另一个在 TC39 成立之初就有的提案是 &lt;a href=&quot;https://github.com/tc39/proposal-optional-chaining&quot;&gt;Optional Chaining&lt;/a&gt;。早在 TS 的初期，就有人提议为 TS 加入该功能。TS 的 &lt;a href=&quot;https://github.com/microsoft/TypeScript/issues/16&quot;&gt;第 16 个 issue&lt;/a&gt; 就是对此功能的建议。可是迟迟没有被加入。而开发者的答复是什么？「害怕和 TC39 的最终定义不一致」。看起来很荒唐。TypeScript 为什么要和 ES 一致？TS 哪里和 ES 相关了？你 TS 的代码能直接被 ES 的解释器处理？你最后还不是要编译成 JS？那你现在和他一不一致又有什么关系？&lt;/p&gt;
&lt;h3&gt;CoffeeScript 👍&lt;/h3&gt;
&lt;p&gt;CoffeeScript 就是很棒一个语言。他很自然的对 JS 的易用性做出了改善。加入了很多实用的功能便于它的开发者编写优雅的代码。早在实现之初就实现了 Optional Chaining 的功能：&lt;a href=&quot;https://coffeescript.org/#existential-operator&quot;&gt;The Existential Operator&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;使用 CoffeeScript 能写出非常美观、简化的代码。它拥有一大批的受众。为 Atom 编辑器原生支持。在 JS 的发展历程中迈出了重要的一步。对开发者来说他是 ES6 之前的一个很好的跨越。&lt;/p&gt;
&lt;h3&gt;ECMA-262&lt;/h3&gt;
&lt;p&gt;JS 近些年似乎看起来比较火热。其实他在很长一段时间里都止步不前。&lt;/p&gt;
&lt;p&gt;看看 JS 的历史吧：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&quot;center&quot;&gt;Edition&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;Date published&lt;/th&gt;
&lt;th align=&quot;center&quot;&gt;Name&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;1&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;June 1997&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;2&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;June 1998&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;3&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;December 1999&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;4&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;em&gt;Abandoned&lt;/em&gt;&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;5&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;December 2009&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;5.1&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;June 2011&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;6&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;June 2015&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;ECMAScript 2015 (ES2015)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;7&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;June 2016&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;ECMAScript 2016 (ES2016)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&quot;center&quot;&gt;8&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;June 2017&lt;/td&gt;
&lt;td align=&quot;center&quot;&gt;ECMAScript 2017 (ES2017)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;&lt;/table&gt;
&lt;p&gt;1997 年发布了第一版。1999 年发布了第三版。到 2009 年才发布第五版：也就是 ES5。而到 2015 年才发布 ES6。在这中间。很长一段时间都是空白。只有广大的开发者不断贡献自己的热情，ES 却没有任何动静。直到 2015 年 ECMA 才想起来 ES。成立 tc39 来发展 ES。&lt;/p&gt;
&lt;p&gt;开发者们的热情高涨。很多一直以来的诉求都被撰写为正式的提案，并且为他实现 babel plugin，让开发者可以首先就体验到新特性的益处。但 tc39 却不知道在干什么。很多重要特性或者基础功能迟迟没有得到推进。而却有一些争议提案却即将被正式纳入规范。&lt;strong&gt;&lt;a href=&quot;https://github.com/tc39/proposal-class-fields&quot;&gt;proposal-class-fields&lt;/a&gt;&lt;/strong&gt; 就是极大的争议。&lt;/p&gt;
&lt;p&gt;这也是我写这篇文章的一个契机。9 月 5 号的时候我看到一条推。提到 9 月 8 日将在上海举办一场关于 class fileds 提案的讨论会。这才对这个提案有了细致的了解。同时也意识到其中的弊端。&lt;/p&gt;
&lt;p&gt;但让人啼笑皆非的是。TS 却很早就支持了在 class 中申明和定义属性。因为他很符合主流面向对象语言的形式。能写出很像 Java 的类的代码。而现在在 class fields 提案饱受争议的时候，甚至会有人考虑是否应该兼容一下已被广泛使用的 TS。此时的我已经是一头的问号？？？&lt;/p&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;JavaScript 真辣鸡&lt;/p&gt;
&lt;h3&gt;References&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://stackoverflow.blog/2015/07/29/why-are-there-so-many-programming-languages/&quot;&gt;“Why Are There So Many Programming Languages?”&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://coffeescript.org/&quot;&gt;CoffeeScript&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://en.wikipedia.org/wiki/Linker_(computing)&quot;&gt;Linker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/hax/js-class-fields-chinese-discussion&quot;&gt;JavaScript的『class fields』提案中文讨论&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>今天听到一期 podcast 感触良多</title><link>https://iwj.moe/blog/post/%E4%BB%8A%E5%A4%A9%E5%90%AC%E5%88%B0%E4%B8%80%E6%9C%9Fpodcast%E6%84%9F%E8%A7%A6%E8%89%AF%E5%A4%9A/</link><guid isPermaLink="true">https://iwj.moe/blog/post/%E4%BB%8A%E5%A4%A9%E5%90%AC%E5%88%B0%E4%B8%80%E6%9C%9Fpodcast%E6%84%9F%E8%A7%A6%E8%89%AF%E5%A4%9A/</guid><pubDate>Sat, 01 Jun 2019 02:01:43 GMT</pubDate><content:encoded>&lt;p&gt;今天听到了一期让我感触良多的 Podcast，我和讲述者的一些经历和观念都很相似，可是现在的自己远没有讲述者那么理想。&lt;/p&gt;
&lt;!-- more --&gt;&lt;blockquote&gt;
&lt;p&gt;&lt;a href=&quot;https://www.google.com/podcasts?feed=aHR0cHM6Ly9mZWVkcy5zaW1wbGVjYXN0LmNvbS9GREFLWTFBRg&amp;episode=MDNjZGY4YTEtN2ViZS00ODg4LTk3ZTktMTIxMjdhNzkwM2I5&quot;&gt;Google Podcasts - UX Coffee 设计咖 - #70：我有一双隐形的翅膀（周楷雯 · 独立开发者）&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;今天我们请到的是独立开发者 Kevin 周楷雯，他是独立应用《50 音起源》的作者。接受我们采访的时候，周楷雯刚刚结束度假，回到自己在青岛的海边小屋，悠闲地享受着一个美好的下午。然而就在两三年前，周楷雯的生活正一片灰暗，那时候的他刚刚经历了公司并购，项目关停，好友离职等等一系列的变故。对周楷雯来说，他高考后的这十年时间，可谓是起起伏伏。这一路来的故事，可以一直追溯到他刚刚踏入大学校门的那个夏天。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;提醒：这篇文章并没有任何干货&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我开始的想法和 kevin 很像：对人生有着自己的规划；同样高考之前不去上课；同样上了大学也不去上课，每天写代码。&lt;/p&gt;
&lt;p&gt;大一的时候是我学习效率最高的时候，也是做产品欲望最强烈的时候。那时候看了 12 Startups in 12 Months 以及一些创业公众号。我就特别想做产品。当时主动接触了很多人，谈论，请教，交流了很多想法和产品。&lt;/p&gt;
&lt;p&gt;当时做了很多东西：&lt;/p&gt;
&lt;p&gt;只花一个礼拜就做完了 &lt;a href=&quot;https://toolkit.space&quot;&gt;toolkit.space&lt;/a&gt; （一个包括了数十个在线工具的网站，模仿 atool）；&lt;/p&gt;
&lt;p&gt;只花一天就完成了 Village Game （一个模仿 Telegram 上村庄游戏的微信公众号，那是我第一次接触微信公众平台）；&lt;/p&gt;
&lt;p&gt;一个早上就用 PHP 加 JQuery 再加 LAMP 一把梭写完并上线了 jsurl （一个短网址程序，现在的 i8e.net 所用的代码）&lt;/p&gt;
&lt;p&gt;。&lt;/p&gt;
&lt;p&gt;但是完全不知道怎么去宣传，所以很多东西最终也没有其他人用。也因此在制作时都只考虑了自己方便，也没有去更深入的打磨和优化。&lt;/p&gt;
&lt;p&gt;关于宣传其实了解了很多。但是了解到的所有方式都觉得成本太高，感觉要花费太多时间精力，而且一分钱也不想花。&lt;/p&gt;
&lt;p&gt;而且当时每个产品所涉及的都是对我来说全新的技术。当时才刚刚开始用 Node（中学时一直 PHP 一把梭。&lt;/p&gt;
&lt;p&gt;但是从大二开始就怠惰了。&lt;/p&gt;
&lt;p&gt;当时暑假和学长商量着做一个产品，后来觉得太扯了就弃了。可能是因为感觉商业化太难，全心投入代价太大，不敢又没有必要破釜沉舟，所以还是想着稳妥一点。因此做东西畏首畏尾考虑一大堆，最后很多想做的都没做。&lt;/p&gt;
&lt;p&gt;当时花了太多时间在没意义的事情上面：参加了很多奇怪的比赛。&lt;/p&gt;
&lt;p&gt;当时同时搞 CTF，大数据比赛，服务外包比赛，还同时给老师打工。除此之外还在同时干很多不正经的事情。撩学妹，参加社团活动，还得打游戏，期中期末还得学看高数。反正感觉真的是屁事很多，反正是少有时间安心的做自己想做的东西了。&lt;/p&gt;
&lt;p&gt;当时可能是被迫花了太多时间干了太多自己感觉没意义的事情（确实也很没意义，比赛没有获奖，技术没有长进，钱也没挣到，成果也没有。&lt;/p&gt;
&lt;p&gt;大二下学期其实还可以，参加了 GSOC，参加了 Hackathon，找到了一个还不错的暑期实习，玩了不少游戏。也学了很多想学的东西。还做了一个不少人用的浏览器扩展（但这也是个大坑，没什么回报，还占用了我很多时间去回邮件）&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;hr&gt;
&lt;h6&gt;缺乏积淀&lt;/h6&gt;
&lt;p&gt;反省一下自己一直以来都比较缺乏积淀。一直不太喜欢写博客，很大一个原因是绝大多数东西都是从网上有的东西学到的，总觉得没必要去重复。而且特别反感很多人的博客都是一些烂大街的东西。自己也很少有什么创新，所以一直以来博客都没什么产出。&lt;/p&gt;
&lt;p&gt;经常看到一些前辈的个人 wiki，博客里很多原创文章就很羡慕。比如 &lt;a href=&quot;http://felixc.at/&quot;&gt;felixonmars 的 wiki&lt;/a&gt;，对于类似的个人可控的知识的积累非常的喜欢。这里不得不提一个特别崇拜的前辈 &lt;a href=&quot;https://blog.lilydjwg.me/&quot;&gt;百合仙子依云&lt;/a&gt;，他的博客非常高产，都是原创文章，并且都包含干货和自己思考（其实想一想很多 dalao 的博客里也会有一些个人的想法和生活的记录，可是我一个是不太希望暴露自己的生活在网上，因为太颓了；还有除了技术文章我其实很讨厌看和写大段这种记录个人生活的文字。&lt;/p&gt;
&lt;h6&gt;花了太多时间完善工作流&lt;/h6&gt;
&lt;p&gt;一直以来都有对自己产生的数据有极强的控制欲，所有网络服务的数据都希望自己可以掌控结构化的数据，便于迁移，修改，整理和保存。所以花了太多时间去寻找和配置工具。&lt;/p&gt;
&lt;p&gt;其实很多人一直用着现有的工具，随着时间也就有了一些产出和积累。（比如很多人可能就有着有道云笔记或者 OneNote 就够了）可是我总想找到一套完美的工具。笔记应用几乎把所有能找到的都试了一圈（包括 LeanNote，SimpleNote 等等很多小众的），RSS 应用也都试过一圈，自动化应用也都试过一圈。结果到最后也没能找到完美的，让自己满意的。很多应用没有接口，没法掌控所有数据，也就没有第三方客户端。现在的笔记应用感觉没有一个能让用户完全掌控数据的，所以到不如本地用 Markdown。（想写一篇文章谈谈对现在笔记产品的看法和感受）&lt;/p&gt;
&lt;p&gt;而且因为松鼠症，收藏了太多太多东西，但是没有时间去整理，或是完整阅读。因为什么都想留存下来，RSS 订阅的几十个博客和社区，日常的所有通话记录和短信，手机使用记录，标星的邮件，还有打心的推特全部都会自动化的存到某个笔记应用里。那个笔记应用里可能存了几千条笔记了，但是并没有什么卵用。而且那个笔记应用很封闭。Google Keep 里也存了一大堆东西，本来只想当个 todo list 用，但是后来很多想法，收藏的东西都放里面了，可能也存了几百条，但是也很难迁移出来了。&lt;/p&gt;
&lt;p&gt;（感觉想写一篇文章整理一下自己的工作流和工具了）&lt;/p&gt;
&lt;p&gt;在尝试这些工具上花了太多时间，最后效率也没有什么提升，而且很多东西分散在了不同的地方。&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;本来还想谈谈自己的理想，关于出国，还有产品的创意。但是实在是不太想写非技术的文字。这篇就到这里吧，算是难得的对近期的总结&lt;/em&gt;&lt;/p&gt;
</content:encoded></item><item><title>JavaScript 在浏览器环境中的多进程同步</title><link>https://iwj.moe/blog/post/JavaScript-%E5%9C%A8%E6%B5%8F%E8%A7%88%E5%99%A8%E7%8E%AF%E5%A2%83%E4%B8%AD%E7%9A%84%E5%A4%9A%E8%BF%9B%E7%A8%8B%E5%90%8C%E6%AD%A5/</link><guid isPermaLink="true">https://iwj.moe/blog/post/JavaScript-%E5%9C%A8%E6%B5%8F%E8%A7%88%E5%99%A8%E7%8E%AF%E5%A2%83%E4%B8%AD%E7%9A%84%E5%A4%9A%E8%BF%9B%E7%A8%8B%E5%90%8C%E6%AD%A5/</guid><pubDate>Thu, 02 May 2019 17:24:44 GMT</pubDate><content:encoded>&lt;p&gt;这标题看起来有点蠢，因为 JS 是单线程的，多进程其实指的是多个 JS 实例。但是多个 JS 实例是可能并行访问相同的数据的，所以还是会碰到需要多进程同步的情况的。&lt;/p&gt;
&lt;!-- more --&gt;&lt;p&gt;在单一 JS 实例中的纯同步代码确实是不用担心这个问题的。虽然由于事件模型可能会由于用户操作产生并发，但是由于 JS 是单线程的，所有同步操作都可以看做是原子的。比如下面的代码：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;var count = 0
button.addEventListener(&amp;#39;click&amp;#39;, () =&amp;gt; {
  count = count + 1
})
for (var i = 0; i &amp;lt; 1000; i += 1)
  button.dispatchEvent(new Event(&amp;#39;click&amp;#39;))
// expected count is 1000
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在其他多线程程序中这种并发操作共享内存的情况就可能会出现异常的情况。下面用 Golang 举个俗套的栗子：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-go&quot;&gt;func test() {
  var a int64 = 0
  var wg sync.WaitGroup
  wg.Add(1000)
  for i := 0; i &amp;lt; 1000; i++ {
    go (func() {
      a = a + 1
      wg.Done()
    })()
  }
  wg.Wait()
  fmt.Println(a)
  // expected output less than 1000
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;异步的情况&lt;/h3&gt;
&lt;p&gt;可是 JS 也可能会有异步的数据操作，这样的操作就不是原子的了。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;var count = 0

var readData = () =&amp;gt; new Promise(r =&amp;gt; {
  setTimeout(() =&amp;gt; r(count), 0)
})

var writeData = data =&amp;gt; new Promise(r =&amp;gt; {
  setTimeout(() =&amp;gt; r(count = data), 0)
})

button.addEventListener(&amp;#39;click&amp;#39;, async () =&amp;gt; {
  const data = await readData()
  await writeData(data + 1)
})
for (var i = 0; i &amp;lt; 1000; i += 1)
  button.dispatchEvent(new Event(&amp;#39;click&amp;#39;))
// expected count is 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;但是浏览器环境中 localStorage 操作是同步的，所以不用担心这种情况。可是在使用 indexDB 或者 webSQL 这种的时候就不安全了，其实如果直接用 transaction api 或者用 SQL 操作还可以，但是用 localForage 这种的时候就可能会出现这种问题。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const t = async () =&amp;gt; {
  const a = +await localforage.getItem(&amp;#39;a&amp;#39;)
  await localforage.setItem(&amp;#39;a&amp;#39;, a + 1)
}

for (let i = 0; i &amp;lt; 1000; i += 1) t()
// expected a = 1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;早在很多年前就有 &lt;a href=&quot;https://balpha.de/2012/03/javascript-concurrency-and-locking-the-html5-localstorage/&quot;&gt;文章&lt;/a&gt; 阐述过这个问题。文章所说的是浏览器中同域名下的多页面同时访问 localStorage 的情况。但是现在 localStorage 是多线程安全的。&lt;/p&gt;
&lt;p&gt;主要还是对于指提供了 set 和 get 两种操作的异步数据操作来说比较危险。而扩展中的 storage API 就是这样的，所以就必须进行访问控制了。&lt;/p&gt;
&lt;h6&gt;延迟操作&lt;/h6&gt;
&lt;p&gt;可以使用一个变量来标志是否正在进行读写操作，如果正在进行的话就延迟执行。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;let processing = false
const t = async () =&amp;gt; {
  if (processing) return setTimeout(t, 0)
  processing = true
  const a = +await localforage.getItem(&amp;#39;a&amp;#39;)
  await localforage.setItem(&amp;#39;a&amp;#39;, a + 1)
  processing = false
}
for (let i = 0; i &amp;lt; 1000; i += 1) t()
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;轮询阻塞&lt;/h6&gt;
&lt;p&gt;但是上面的方法就无法通过 await 进行流程控制了，不知道实际会在什么时候执行，所以最好可以阻塞到操作结束。下面的方式可以实现这样的效果。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;let processing = false
const wait = () =&amp;gt; new Promise(r =&amp;gt; {
  const _wait = () =&amp;gt; {
    setTimeout(() =&amp;gt; {
      if (processing) _wait()
      else r()
    }, 0);
  }
  _wait()
})

const t = async () =&amp;gt; {
  await wait()
  processing = true
  const a = +await localforage.getItem(&amp;#39;a&amp;#39;)
  await localforage.setItem(&amp;#39;a&amp;#39;, a + 1)
  processing = false
}
for (let i = 0; i &amp;lt; 1000; i += 1) await t()
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;使用 Promise&lt;/h6&gt;
&lt;p&gt;还有一种方式是 Promise，通过每次创建一个 Promise，在处理结束后将 Promise resolve，在创建时就等待当前的 Promise resolve。这样可以避免使用 setTimeout 造成的递归的代码。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;let processing = Promise.resolve()
const start = () =&amp;gt; {
  let end
  const start = new Promise(r =&amp;gt; { end = r })
  const wait = processing.then(() =&amp;gt; end) // 阻塞到当前执行完毕后将结束方法返回
  processing = processing.then(() =&amp;gt; start) // 设置当前状态为进行中
  return wait
}
const t = async () =&amp;gt; {
  const end = await start()
  const a = +await localforage.getItem(&amp;#39;a&amp;#39;)
  await localforage.setItem(&amp;#39;a&amp;#39;, a + 1)
  end()
}
for (let i = 0; i &amp;lt; 1000; i += 1) await t()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这些控制方式都依赖于 JS runtime 本身的调度方式。只适用于单进程的并发情况。&lt;/p&gt;
&lt;h3&gt;多进程同步&lt;/h3&gt;
&lt;p&gt;一个方法是通过进程间通讯将操作交由一个线程进行&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const init = async () =&amp;gt; {
  const c = new BroadcastChannel(&amp;#39;tabs&amp;#39;)
  const cbs = {}
  // 通过这种方式来实现可以响应的信息交互
  c.addEventListener(&amp;#39;message&amp;#39;, ({data: {id, msg}}) =&amp;gt; {
    if (cbs[id]) {
      cbs[id](msg)
      delete cbs[id]
    }
    if (msg === &amp;#39;init&amp;#39; &amp;amp;&amp;amp; isMain) {
      c.postMessage({id, msg: &amp;#39;hasMain&amp;#39;})
    }
  })

  // 实现发送消息的函数
  c.sendMessage = msg =&amp;gt; new Promise(r =&amp;gt; {
    const id = Math.random()
    c.postMessage({id, msg})
    setTimeout(() =&amp;gt; r(&amp;#39;timeout&amp;#39;), 1000)
    cbs[id] = r
  })

  const result = await c.sendMessage(&amp;#39;init&amp;#39;)
  c.isMain = result === &amp;#39;timeout&amp;#39;
  return c
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这种方式在浏览器扩展环境还是比较实用的，在浏览器环境中可以有一个一直运行的 background 进程，可以把页面中的交互操作发送给 background 进行执行。但是为了暴露相同的 API 同样需要一个初始化的过程，来判断当前环境是否是 background，如果是 background 直接执行，不是 background 则发送消息。&lt;/p&gt;
&lt;h6&gt;Web Locks API&lt;/h6&gt;
&lt;p&gt;这个是个比较新的 API，用法很简单，参考 &lt;a href=&quot;https://wicg.github.io/web-locks&quot;&gt;提案&lt;/a&gt; 即可，但是目前并未在所有平台实现，兼容性可以参考 &lt;a href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Web_Locks_API&quot;&gt;MDN&lt;/a&gt;。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const sleep = ms =&amp;gt; new Promise(r =&amp;gt; setTimeout(r, ms))
navigator.locks.request(&amp;#39;test&amp;#39;, async () =&amp;gt; {
  console.time()
  await sleep(1000)
})
navigator.locks.request(&amp;#39;test&amp;#39;, async () =&amp;gt; {
  await sleep(1000)
  console.timeEnd()
})
// expect output default: 2000ms
&lt;/code&gt;&lt;/pre&gt;
&lt;h6&gt;SharedArrayBuffer 和 Atomics&lt;/h6&gt;
&lt;p&gt;通过 SharedArrayBuffer 和 Atomics 实现锁操作可以参考 &lt;a href=&quot;https://github.com/lars-t-hansen/js-lock-and-condition&quot;&gt;js-lock-and-condition&lt;/a&gt;
通过 haredWorker 共享 SharedArrayBuffer 大概是这么个流程&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;const initLock = async () =&amp;gt; {
  const worker = new SharedWorker(&amp;#39;worker.js&amp;#39;)
  const buffer = await new Promise(resolve =&amp;gt; {
    work.onmessage = buf =&amp;gt; r(buf)
    setTimeout(() =&amp;gt; {
      const buf = new SharedArrayBuffer(100)
      work.postMessage(buf)
      resolve(buf)
    }, 100)
  })
  return new Int32Array(buffer)
}

// worker.js
let locks
onconnect = e =&amp;gt; {
  const [port] = e.ports
  if (locks) port.postMessage(locks)
  else port.onmessage = ({data}) =&amp;gt; { locks = data }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;通过 SharedArrayBuffer 和 Atomics 实现的共享内存，再在其之上实现的锁操作。这个就很麻烦了。要先通过 SharedWorker 共享 SABs，然后才能借此进行同步。并且 SABs API 之前还曾被禁用过，因此倒不如使用 localStorage API 实现：&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;
const lock = (key, fn) =&amp;gt; new Promise(resolve =&amp;gt; {
  const K = key + &amp;#39;_LOCK&amp;#39;
  const _tryLock = () =&amp;gt; {
    setTimeout(async () =&amp;gt; {
      if (localStorage[K]) _tryLock()
      else {
        localStorage[K] = 1
        await fn()
        delete localStorage[K]
        resolve()
      }
    }, 50)
  }
  _tryLock()
})

const sleep = ms =&amp;gt; new Promise(r =&amp;gt; setTimeout(r, ms))

lock(&amp;#39;t&amp;#39;, async () =&amp;gt; {
  console.time()
  await sleep(1000)
})
lock(&amp;#39;t&amp;#39;, async () =&amp;gt; {
  await sleep(1000)
  console.timeEnd()
})
// expect output default: 2000ms
&lt;/code&gt;&lt;/pre&gt;
&lt;h3&gt;参考&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://stackoverflow.com/questions/124764/are-mutexes-needed-in-javascript&quot;&gt;Are Mutexes needed in javascript?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://balpha.de/2012/03/javascript-concurrency-and-locking-the-html5-localstorage/&quot;&gt;JavaScript concurrency and locking the HTML5 localStorage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://bitbucket.org/balpha/lockablestorage/src/default/lockablestorage.js&quot;&gt;LockableStorage&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/mgtitimoli/await-mutex&quot;&gt;await-mutex&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://github.com/nodejs/node/issues/22702&quot;&gt;node issue #22702&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>关于离线优先应用的多端同步的思考和总结</title><link>https://iwj.moe/blog/post/%E5%85%B3%E4%BA%8E%E7%A6%BB%E7%BA%BF%E4%BC%98%E5%85%88%E5%BA%94%E7%94%A8%E7%9A%84%E5%A4%9A%E7%AB%AF%E5%90%8C%E6%AD%A5%E7%9A%84%E6%80%9D%E8%80%83%E5%92%8C%E6%80%BB%E7%BB%93/</link><guid isPermaLink="true">https://iwj.moe/blog/post/%E5%85%B3%E4%BA%8E%E7%A6%BB%E7%BA%BF%E4%BC%98%E5%85%88%E5%BA%94%E7%94%A8%E7%9A%84%E5%A4%9A%E7%AB%AF%E5%90%8C%E6%AD%A5%E7%9A%84%E6%80%9D%E8%80%83%E5%92%8C%E6%80%BB%E7%BB%93/</guid><pubDate>Sun, 06 Jan 2019 06:51:02 GMT</pubDate><content:encoded>&lt;p&gt;因为做了一个扩展，很多用户反馈希望能加入多端同步数据的功能。所以前段时间研究了一下对于离线优先应用的多端同步的方式。上来先看来一些现有的应用的同步实现方式以及一些现有的解决方案。然后最终选择了一个比较简易的方案。&lt;/p&gt;
&lt;!-- more --&gt;&lt;h3&gt;现有的一些应用的实现方式&lt;/h3&gt;
&lt;p&gt;印象笔记的同步方式可以参考 &lt;a href=&quot;https://dev.evernote.com/media/pdf/edam-sync.pdf&quot;&gt;官方的文档&lt;/a&gt;。以一个笔记为最小粒度，基本思路是通过一个标识 USN 来表示笔记的版本。每一次更新都使得 USN 加一。如果更新前的 USN 和远程的 USN 不同的话说明发生了冲突。而印象笔记处理冲突的方式是创建一个新的笔记，同时保留远程的和本地的版本。然后由用户来选择是否删除其中一个。如果不发生冲突的话可以上传对一个笔记的修改。&lt;/p&gt;
&lt;p&gt;Chromium 对于用户数据的同步方式一直是我很好奇的。尤其是对于书签这种又包含多个属性和类型，又可以多层级，又有序的这种复杂的数据类型。像是历史记录这种不可变的，时序的数据同步起来非常简单，只要完全增量就可以了。而书签这种就比较复杂。&lt;a href=&quot;https://www.chromium.org/developers/design-documents/sync&quot;&gt;Chromium 的文档&lt;/a&gt; 中有对于同步服务的大体框架。&lt;a href=&quot;https://www.chromium.org/developers/design-documents/sync/syncable-service-api&quot;&gt;这篇文章&lt;/a&gt; 中提到对于 Chromium 来说希望开发者可以尽量避免涉及同步服务的细节。而通过一个统一的框架来进行同步服务的开发。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/images/%E5%85%B3%E4%BA%8E%E7%A6%BB%E7%BA%BF%E4%BC%98%E5%85%88%E5%BA%94%E7%94%A8%E7%9A%84%E5%A4%9A%E7%AB%AF%E5%90%8C%E6%AD%A5%E7%9A%84%E6%80%9D%E8%80%83%E5%92%8C%E6%80%BB%E7%BB%93/5c52915631bb0.png&quot; alt=&quot;Sync service&quot;&gt;&lt;/p&gt;
&lt;p&gt;这个图大体描述了一个同步的流程：上传变更，下载变更并进行合并，之后将变更应用于本地数据。对于每种数据来说，需要实现一个 processor 用于应用更改；一个 merger 用于合并更改就可以了。具体如何合并和应用更改还是可以客户端决定的。而服务器还是会保留所有客户端上传的更改。&lt;/p&gt;
&lt;p&gt;对于书签来说，比如本地进行了对一个书签的改名操作，然后下载变更后有另一个对这个书签的改名操作。这时遇到冲突的情况，Chromium 就可能会创建一个新的书签。导致书签的重复。而 Chromium 除此之外也会储存书签的很多其他的元数据。比如创建时间，修改时间和最后访问时间。对于最后访问时间。如果发生的冲突。那么直接保留一个值最大的修改操作就可以了。然而对于书签来说还有很多种不同的操作，比如重排序，移动到其他文件夹等。都可能有不同的策略。对于书签的同步的具体实现可以看 &lt;a href=&quot;https://chromium.googlesource.com/chromium/src.git/+/71.0.3570.1/components/sync_bookmarks/&quot;&gt;./components/sync_bookmarks&lt;/a&gt; 这个目录的代码。&lt;/p&gt;
&lt;p&gt;然后除了同步的流程之外，还需要有很多逻辑来控制何时下载，何时上传等状态，&lt;a href=&quot;https://chromium.googlesource.com/chromium/src.git/+/71.0.3570.1/components/browser_sync/profile_sync_service.h&quot;&gt;这个文件&lt;/a&gt; 编写了各种操作变更同步状态的逻辑。所以对于 Chromium 来说同步服务异常的复杂。除此之外还有 UI 层和数据层直接的同步。毕竟 UI 不能每一次操作都完整读写整个储存。所以可以 UI 和数据层共用相同的 processor 这种方式，UI 和数据层直接也只需要传送修改操作就行了。&lt;/p&gt;
&lt;p&gt;并且也有很多策略来验证一致性，简化传输的数据等。&lt;a href=&quot;chrome://sync-internals/&quot;&gt;chrome://sync-internals/&lt;/a&gt; 可以查看 Chromium 所有同步所储存的数据，状态，发送的消息等信息。&lt;/p&gt;
&lt;h3&gt;一些现有的解决方案&lt;/h3&gt;
&lt;h6&gt;&lt;a href=&quot;https://github.com/share/sharedb&quot;&gt;shareDB&lt;/a&gt;&lt;/h6&gt;
&lt;p&gt;这个是为了一个 Node.js 多端同步应用开发框架 &lt;a href=&quot;https://derbyjs.com/&quot;&gt;derbyjs&lt;/a&gt; 实现的一个同步数据库。基于 WebSocket 在多终端之间同步。Demo 中的效果还是很好的。但是这个库并不是离线优先，在离线时的操作都会被丢弃。&lt;/p&gt;
&lt;p&gt;但是这个库有一定的参考价值，它的数据保存和传送使用了对于 JSON 的 &lt;a href=&quot;https://en.wikipedia.org/wiki/Operational_transformation&quot;&gt;Operational Transformation (OT)&lt;/a&gt;。并且作者自己实现了一套 OT 的标准和库：&lt;a href=&quot;https://github.com/ottypes/json1&quot;&gt;ottypes&lt;/a&gt;。主要作用是通过约定好的格式来简化传输时的数据大小。并且可以对多个操作进行撤销，交换顺序等。&lt;/p&gt;
&lt;h6&gt;&lt;a href=&quot;https://github.com/pouchdb/pouchdb&quot;&gt;PouchDB&lt;/a&gt; | &lt;a href=&quot;https://couchdb.apache.org/&quot;&gt;CouchDB&lt;/a&gt;&lt;/h6&gt;
&lt;p&gt;根据官网的介绍写着来自 Apache CouchDB。后来发现这个 PouchDB 其实就是 CouchDB 接口的一个 JS 的实现。所以就直接研究这个 CouchDB 了。&lt;/p&gt;
&lt;p&gt;这个 CouchDB 其实还是挺有意思的。官网首页就号称大数据和稳定性：&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Seamless multi-master sync, that scales from Big Data to Mobile, with an Intuitive HTTP/JSON API and designed for Reliability.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;看了一下文档发现是 erlang 实现的。主要同步原理是通过储存所有变动，保留所有冲突。所以才有了号称的稳定性。然后每次查询都可以看到所有冲突的版本。然后可以将所有版本展现给用户，由用户来决定保留哪个版本。&lt;/p&gt;
&lt;p&gt;这样确实做到了离线优先和多端同步。但是过于重量级了。使用他需要额外的数据库实例，并且只是把冲突问题交给了用户来解决，和 evernote 的方式差不多了。而且客户端也需要保留很多冗余的数据。之后可以写一篇关于 CouchDB 的文章来专门介绍一下。之后的应用如果有需要也可以采用这个。但是这个项目还是想轻量一点。所以也就没有采用 PouchDB。&lt;/p&gt;
&lt;h3&gt;总结&lt;/h3&gt;
&lt;p&gt;很重要的一点就是最小数据粒度。对于最小粒度的数据，只需要一个记录了最后更新时间的时间戳就可以确定正确的状态了。如果远程的数据更新了，在客户端没有获取到远程更新的状态时如果客户端对该数据进行了更改，那么上传时远程只要直接更新到该客户端修改后的状态即可。因为客户端的修改也是来自用户。如果只有用户可以触发修改那么该状态也是经由用户确认的正确的状态。&lt;/p&gt;
&lt;h3&gt;最终的采用的方案&lt;/h3&gt;
&lt;p&gt;现在的方案是以一个列表为最小粒度。以最后修改时间为标识。保留最后修改时间最大的修改。当获取完整数据时先获取每个列表的 ID 和最后修改时间。如果最后修改时间大于本地的修改时间则下载完整列表数据进行替换。并且每次进行更新时，确保最后修改时间小于当前时间且大于上一个版本的时间。&lt;/p&gt;
&lt;h3&gt;参考&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://developers.google.com/web/fundamentals/instant-and-offline/web-storage/offline-for-pwa&quot;&gt;Offline Storage for Progressive Web Apps&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</content:encoded></item><item><title>Vue transition-group 组件实现类似 Google Keep 的延迟动画</title><link>https://iwj.moe/blog/post/Vue-transition-group-%E7%BB%84%E4%BB%B6%E5%AE%9E%E7%8E%B0%E7%B1%BB%E4%BC%BC-Google-Keep-%E7%9A%84%E5%BB%B6%E8%BF%9F%E5%8A%A8%E7%94%BB/</link><guid isPermaLink="true">https://iwj.moe/blog/post/Vue-transition-group-%E7%BB%84%E4%BB%B6%E5%AE%9E%E7%8E%B0%E7%B1%BB%E4%BC%BC-Google-Keep-%E7%9A%84%E5%BB%B6%E8%BF%9F%E5%8A%A8%E7%94%BB/</guid><pubDate>Thu, 25 Oct 2018 21:18:44 GMT</pubDate><content:encoded>&lt;p&gt;最近在重构某项目，前端想模仿 Google Keep 来做。非常喜欢点搜索框之后卡片滑动进入的动画效果。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/images/Vue-transition-group-%E7%BB%84%E4%BB%B6%E5%AE%9E%E7%8E%B0%E7%B1%BB%E4%BC%BC-Google-Keep-%E7%9A%84%E5%BB%B6%E8%BF%9F%E5%8A%A8%E7%94%BB/5bd1cee4220b3.gif&quot; alt=&quot;GIFrecord_2018-10-25_211426-min.gif&quot;&gt;&lt;/p&gt;
&lt;p&gt;昨天研究了一晚才搞定。因为这次实在是没找到相关的文章，所以在这记录一下。&lt;/p&gt;
&lt;!-- more --&gt;&lt;p&gt;关于 transition 和 transition-group 组件的基本用法可以参考 &lt;a href=&quot;https://cn.vuejs.org/v2/guide/transitions.html&quot;&gt;官方文档&lt;/a&gt;，写了很多特殊情况的用法。但是没有这种延迟的动画效果。&lt;/p&gt;
&lt;p&gt;首先写一下动画各个阶段的样式。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-scss&quot;&gt;.slide-enter-active {
  transition: all ease-out .22s; // 进入时为 0.22s 的滑入动画
}
.slide-leave-active  {
  transition: all 0; // 离开时直接消失
}
.slide-enter-to {
  opacity: 1;
}
.slide-enter {
  transform: translateY(100px);
  opacity: 0.3;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;何时应用哪个类在文档中写的非常清晰。尤其是这个图一看就非常明了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/images/Vue-transition-group-%E7%BB%84%E4%BB%B6%E5%AE%9E%E7%8E%B0%E7%B1%BB%E4%BC%BC-Google-Keep-%E7%9A%84%E5%BB%B6%E8%BF%9F%E5%8A%A8%E7%94%BB/5bd1c699eeba9.png&quot; alt=&quot;transition.png&quot;&gt;&lt;/p&gt;
&lt;p&gt;然后是模板部分。这里用两个单独的元素来举例子。省略了不重要的部分。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;transition-group
  tag=&amp;quot;v-layout&amp;quot;
  name=&amp;quot;slide&amp;quot;
  @after-leave=&amp;quot;afterLeave&amp;quot;
&amp;gt;
  &amp;lt;v-flex key=&amp;quot;date&amp;quot; v-if=&amp;quot;card &amp;gt; 0&amp;quot;&amp;gt;
    ...
  &amp;lt;/v-flex&amp;gt;
  &amp;lt;v-flex key=&amp;quot;color&amp;quot; v-if=&amp;quot;card &amp;gt; 1&amp;quot;&amp;gt;
    ...
  &amp;lt;/v-flex&amp;gt;
&amp;lt;/transition-group&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果用&lt;code&gt;v-for&lt;/code&gt;的话就是下面这样。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-html&quot;&gt;&amp;lt;div v-for=&amp;quot;i in 10&amp;quot; key=&amp;quot;i&amp;quot; v-if=&amp;quot;card &amp;gt; i - 1&amp;quot;&amp;gt;&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后重点就在这里啦。其实就是通过延时来增加计数器。使得元素一个一个渲染啦。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;export default {
  data() {
    return {
      card: 0,
    }
  },
  activated() {
    this.slideCard()
  },
  deactivated() {
    this.card = 0
  },
  methods: {
    slideCard() {
      if (this.card === 2) return
      this.toBeHide = this.card += 1
      setTimeout(this.slideCard, 200)
    },
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后这样就可以保证组件激活的时候出现延迟进入的动画效果啦。如果组件没有&lt;code&gt;keep-alive&lt;/code&gt;的话直接在&lt;code&gt;created&lt;/code&gt;里调用&lt;code&gt;slideCard&lt;/code&gt;就行了。&lt;/p&gt;
&lt;p&gt;虽然这样就足够实现我萌想要的效果了。但是可以稍微引申一点点。我们想要重置动画的话，比如等所以卡片隐藏之后再重新来一遍。如果想知道卡片全部移除的话需要用一个计数器。因为事件里只有一个&lt;code&gt;after-leave&lt;/code&gt;，并没有全部移除的事件。所以需要在&lt;code&gt;after-leave&lt;/code&gt;事件里修改计数器来达到这个效果。&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-js&quot;&gt;export default {
  data() {
    return {
      card: 0,
      toBeHide: 0,
    }
  },
  activated() {
    this.loadListColors()
    if (this.card === 0) this.slideCard()
    else this.card = 0
  },
  methods: {
    slideCard() {
      if (this.card === 2) return
      this.toBeHide = this.card += 1
      setTimeout(this.slideCard, 200)
    },
    afterLeave() {
      this.toBeHide -= 1
      if (this.toBeHide === 0) {
        this.slideCard()
      }
    },
  },
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这就是最终的效果啦。&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;/blog/images/Vue-transition-group-%E7%BB%84%E4%BB%B6%E5%AE%9E%E7%8E%B0%E7%B1%BB%E4%BC%BC-Google-Keep-%E7%9A%84%E5%BB%B6%E8%BF%9F%E5%8A%A8%E7%94%BB/5bd1c2ddc3326.gif&quot; alt=&quot;GIFrecord_2018-10-25_211508.gif&quot;&gt;&lt;/p&gt;
&lt;p&gt;碎碎念：&lt;/p&gt;
&lt;p&gt;我一个后端开发怎么搞 vue 来了呢 (⊙o⊙)? 这个项目的同步服务正在进行一个复杂的重构，等完全弄好了整理一下发个文章吧。&lt;/p&gt;
&lt;p&gt;为什么 Google Keep 说改版就改版了。底色变的全白了好刺眼啊，还有各种难看的圆角。还是原来的好。&lt;/p&gt;
&lt;h3&gt;参考&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://cn.vuejs.org/v2/api/#transition-group&quot;&gt;https://cn.vuejs.org/v2/api/#transition-group&lt;/a&gt;
&lt;a href=&quot;https://codepen.io/dizzyluo/pen/yJLwWm&quot;&gt;https://codepen.io/dizzyluo/pen/yJLwWm&lt;/a&gt;&lt;/p&gt;
</content:encoded></item></channel></rss>