<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="http://cyj.me/feed/atom.xml" rel="self" type="application/atom+xml" /><link href="http://cyj.me/" rel="alternate" type="text/html" /><updated>2025-05-07T10:34:28+00:00</updated><id>http://cyj.me/feed/atom.xml</id><title type="html">Chen Yangjian</title><subtitle>Everything about Chen Yangjian.</subtitle><author><name>Chen Yangjian</name><uri>http://cyj.me</uri></author><entry><title type="html">前端模块在广告业务的实践</title><link href="http://cyj.me/programming/2018/05/24/about-module-iii/" rel="alternate" type="text/html" title="前端模块在广告业务的实践" /><published>2018-05-24T00:00:00+00:00</published><updated>2018-05-24T00:00:00+00:00</updated><id>http://cyj.me/programming/2018/05/24/about-module-iii</id><content type="html" xml:base="http://cyj.me/programming/2018/05/24/about-module-iii/"><![CDATA[<p>About Module 是一系列文章，本系列想要讨论的：</p>

<ul>
  <li><a href="/programming/2018/05/22/about-module-i/">前端模块的历史沿革 A Brief History of Module</a></li>
  <li><a href="/programming/2018/05/23/about-module-ii/">前端模块的现状 The Status Quo of Module</a></li>
  <li>前端模块在广告业务的实践 The Module Practiced in Cara</li>
</ul>

<p>本文为系列文章中的最后一篇，讲述前端模块在广告业务的实践。</p>

<h2 id="use-cases">Use Cases</h2>

<p>在广告业务中，一方面是快速严谨的平台技术，一方面是稳定压倒一切的广告代码。在笔者维护的一个项目中，就存在这两种情况：</p>

<ul>
  <li>既使用 Webpack + React + Redux 等流行技术方案开发相关功能；</li>
  <li>也使用 jQuery（甚至自研的类 jQuery 库 yen）开发广告代码。</li>
</ul>

<p>对后者，我们有一个给予 Node.js 中间件和自主开发的前端模块加载器的方案。但这个方案并不支持以下使用场景：</p>

<ul>
  <li>基于 loose-envify 和 dead code elimination 的条件依赖（前文有提及）</li>
  <li>Webpack 自定义 loader（比如 worker-loader）</li>
  <li>放飞自我的模块查找逻辑（比如 main 只写 dist ，实际文件在 dist/index.js ，诸如此类）</li>
</ul>

<p>这套方案能够让前端代码和寻常 CommonJS 模块基本一致：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// relative specifier</span>
<span class="kd">var</span> <span class="nx">foo</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">./foo</span><span class="dl">'</span><span class="p">)</span>

<span class="c1">// non-relative specifier</span>
<span class="kd">var</span> <span class="nx">bar</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">lib/bar</span><span class="dl">'</span><span class="p">)</span>

<span class="c1">// non-relative specifier that resolves to npm package</span>
<span class="kd">var</span> <span class="nx">jQuery</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">jquery</span><span class="dl">'</span><span class="p">)</span>
</code></pre></div></div>

<p>到 2017 年末，为了整合上述两种使用场景，我们再度重构了这套方案，现在可以：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// relative specifier</span>
<span class="kd">var</span> <span class="nx">foo</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">./foo</span><span class="dl">'</span><span class="p">)</span>

<span class="c1">// non-relative specifier</span>
<span class="kd">var</span> <span class="nx">bar</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">lib/bar</span><span class="dl">'</span><span class="p">)</span>
<span class="kd">var</span> <span class="nx">jQuery</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">jquery</span><span class="dl">'</span><span class="p">)</span>

<span class="c1">// directory as specifier</span>
<span class="kd">var</span> <span class="nx">dist</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">./dist</span><span class="dl">'</span><span class="p">)</span>

<span class="c1">// conditional require</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">NODE_ENV</span> <span class="o">==</span> <span class="dl">'</span><span class="s1">production</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">./logger/prod</span><span class="dl">'</span><span class="p">)</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
  <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">./logger/dev</span><span class="dl">'</span><span class="p">)</span>
<span class="p">}</span>

<span class="c1">// worker-loader</span>
<span class="kd">var</span> <span class="nx">Worker</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">worker-loader!./worker</span><span class="dl">'</span><span class="p">)</span>
</code></pre></div></div>

<p>这套方案叫做 <a href="https://github.com/erzu/porter">Porter</a>。</p>

<h2 id="entrypoint">Entrypoint</h2>

<p>由于依赖自定义的模块加载器，在运行页面入口代码之前，我们得先预备好模块加载器。Porter 支持两种使用方式：</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">&lt;!-- 1. 自动合并 --&gt;</span>
<span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"./app.js?main"</span><span class="nt">&gt;&lt;/script&gt;</span>
<span class="c">&lt;!-- 2. 显式声明 --&gt;</span>
<span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"/loader.js"</span> <span class="na">data-main=</span><span class="s">"./app.js"</span><span class="nt">&gt;&lt;/script&gt;</span>
</code></pre></div></div>

<p>两者都会先执行模块加载器的代码，并在转换 <code class="language-plaintext highlighter-rouge">app.js</code> 代码时，尝试去解决 app.js 所声明的依赖。从浏览器和 Porter 两端看上述代码的加载流程，大致如下：</p>

<p><img src="http://ossgw.alicdn.com/creatives-assets/oss/uploads/2018/08/03/ac381710-9713-11e8-b170-a914946a31c4.svg" alt="" /></p>

<p>可以看到，Porter 在处理 <code class="language-plaintext highlighter-rouge">app.js?main</code> 请求时，会在返回实际代码之前解析 app.js 实际依赖，从而返回正确的 app.js transport：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">define</span><span class="p">(</span><span class="dl">'</span><span class="s1">app.js</span><span class="dl">'</span><span class="p">,</span> <span class="p">[</span><span class="dl">'</span><span class="s1">jquery</span><span class="dl">'</span><span class="p">],</span> <span class="kd">function</span><span class="p">(</span><span class="nx">require</span><span class="p">,</span> <span class="nx">exports</span><span class="p">,</span> <span class="nx">module</span><span class="p">)</span> <span class="p">{</span>
  <span class="c1">// actual code of app.js</span>
<span class="p">})</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">&lt;script src="/loader.js" data-main="./app.js"&gt;&lt;/script&gt;</code> 的情况与此类似，区别只是 <code class="language-plaintext highlighter-rouge">loader.js</code> 单独加载，不再赘述。</p>

<h2 id="webpack-diff">Webpack Diff</h2>

<p>前文可能给人一种感觉，Porter 是在 Webpack 的阴影中亦步亦趋。我觉得，是也不是。</p>

<p>无论是广受欢迎的 Webpack，还是野蛮生长的 Porter，都是为了解决同样的问题，使前端代码能够模块化开发、自如消费社区模块、并且遵循标准。从这方面讲，Porter 是 Webpack 的学生。</p>

<p>从功能实现角度来讲，（至少对 Node.js 应用来说）Porter 的使用体验要比 Webpack 好很多。Porter 基本无需配置，指定前端代码所在目录，指定缓存目录并在服务中可见，就可以自如使用了。相比 Webpack，Porter 在如下角度比较轻量：</p>

<ul>
  <li>Entry（或者叫 Entrypoint）是在页面中直接写的，无需单独配置</li>
  <li>不需要单独监听文件改动</li>
  <li>也不需要每次都生成一整个 bundle</li>
</ul>

<p>Porter 在开发模式的 bundle 逻辑是按 package 的，会自动按 package 合并文件，且在浏览器加载流程中可以多层缓存（服务端缓存、浏览器端 Service Worker 缓存等等）。</p>

<p>Porter 在生产模式时，是不需要启动中间件的。为生产模式编译、打包脚本时，可以选择给每个 Entry 打包所有依赖，也可以选择按 package 打包，Entry 仅包含自身 package 中的依赖。和 Webpack vendor/common 或者更新一些的 DLL 插件一样，Porter 还可以选择按 Entry 打包两个版本，根据 Entry 不同而不同的 vendor，以及 Entry 之间共享的 common。</p>

<h2 id="convert-to-es-module">Convert to ES Module</h2>

<blockquote>
  <p>本节为开放讨论，实际工作尚未完成</p>
</blockquote>

<p>想要一步到位是不太可能的，比较欣慰的是，海棠中所使用的模块化方案在 2017 年已经支持 Babel（但 TypeScript 还得再等等），所以上述用例已经可以转换为：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// relative specifier</span>
<span class="k">import</span> <span class="dl">'</span><span class="s1">./foo</span><span class="dl">'</span>

<span class="c1">// non-relative specifier</span>
<span class="k">import</span> <span class="dl">'</span><span class="s1">lib/bar</span><span class="dl">'</span>
<span class="k">import</span> <span class="nx">$</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">jQuery</span><span class="dl">'</span>

<span class="c1">// alias specifier (./dist =&gt; ./dist/index.js)</span>
<span class="k">import</span> <span class="dl">'</span><span class="s1">./dist</span><span class="dl">'</span>

<span class="c1">// worker-loader</span>
<span class="k">import</span> <span class="nx">Worker</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">worker-loader!./worker</span><span class="dl">'</span>

<span class="c1">// dynamic import is NOT ready yet</span>
</code></pre></div></div>

<p>听起来似乎马上用就行，但实际上上述代码是依赖 <code class="language-plaintext highlighter-rouge">babel.transform()</code> 转换为 CommonJS 模块，再以普通脚本模式运行的。也就说前文中提及的 <code class="language-plaintext highlighter-rouge">module</code> 模式的好处、 <code class="language-plaintext highlighter-rouge">import</code> 声明式依赖的好处，在运行时其实捞不到。不管怎么说，ESLint 工具在检验源码时确实是更加有的放矢了。</p>

<p>如果要让浏览器执行真正的 ES Module，我们可能需要修改 Entrypoint，大致改为：</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;script </span><span class="na">type=</span><span class="s">"module"</span> <span class="na">src=</span><span class="s">"./app.mjs"</span><span class="nt">&gt;&lt;/script&gt;</span>
<span class="nt">&lt;script </span><span class="na">nomodule</span> <span class="na">src=</span><span class="s">"./app.js?main"</span><span class="nt">&gt;&lt;/script&gt;</span>
</code></pre></div></div>

<p>此处借用 Node.js 所采用的专用扩展名 .mjs ，只为表示使用 type=”module” 引入 ES Module，使用属性 nomodule 设置降级方案。实际浏览器中脚本的执行模式是以引入方式决定的。</p>

<p>不过，浏览器中默认的 ES Module 依赖解决逻辑并没有考虑 npm 管理的部分，要让上述方案正式可行，我们还需要扩展 Loader 逻辑，加入版本判断。这里头还隐藏了一个比较大的问题，在动态依赖方面也有，我一并描述。</p>

<h2 id="import-with-context">import with context</h2>

<blockquote>
  <p>本节为开放讨论，实际工作仍未完成</p>
</blockquote>

<p>ES Module 给出的动态依赖解决方案是 <code class="language-plaintext highlighter-rouge">import()</code> 。假设我们现在有 foo/bar.js ：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// foo/bar.js</span>
<span class="nx">require</span><span class="p">.</span><span class="k">async</span><span class="p">(</span><span class="dl">'</span><span class="s1">./qux.js</span><span class="dl">'</span><span class="p">,</span> <span class="kd">function</span><span class="p">(</span><span class="nx">qux</span><span class="p">)</span> <span class="p">{</span>
  <span class="c1">// use qux.js</span>
<span class="p">})</span>

<span class="c1">// fetches foo/qux.js</span>
</code></pre></div></div>

<p>上述动态依赖引入逻辑执行时，会以当前模块路径为上下文来解决依赖 <code class="language-plaintext highlighter-rouge">resolve(specifier, context)</code> 。这就是我所说的隐藏的逻辑。在 ES Module 中，这一层逻辑同样没有显式表达出来。而且由于 <code class="language-plaintext highlighter-rouge">import()</code> 的特殊调用形式，对开发者自定义的模块化方案来说，基本上是无从自主实现的。</p>

<p>声明式的 <code class="language-plaintext highlighter-rouge">import</code> 同样会隐藏上下文，例如：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">$</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">jquery</span><span class="dl">'</span>
</code></pre></div></div>

<p>因为可能存在 <code class="language-plaintext highlighter-rouge">a =&gt; jquery@1.x</code> 而 <code class="language-plaintext highlighter-rouge">b =&gt; jquery@3.x</code> ，我们需要知道是谁依赖 <code class="language-plaintext highlighter-rouge">jquery</code> 。假如在这一层不处理，等请求 <code class="language-plaintext highlighter-rouge">jquery.js</code> 到了服务端，就无从下手了。</p>

<h2 id="bonus-css-import">Bonus: CSS @import</h2>

<p>Porter 对 CSS 的 <code class="language-plaintext highlighter-rouge">@import</code> 同样做了处理，只是方式比较简单，是基于 postcss 插件 postcss-import 实现的：</p>

<p><img src="http://ossgw.alicdn.com/creatives-assets/oss/uploads/2018/08/03/4ac47810-9714-11e8-8cc4-e5bdfc7cf1ea.svg" alt="" /></p>

<h2 id="afterword">Afterword</h2>

<p>到这里，All About Module 系列文章就结束了。撰写这个系列的初衷之一，是回答两个 Porter 相关问题：</p>

<ul>
  <li>为什么会有 Porter</li>
  <li>Porter 的下一步是什么</li>
</ul>

<p>希望看完这个系列，你能在了解 ES Module 的同时，对 Porter 也感兴趣。</p>]]></content><author><name>Chen Yangjian</name><uri>http://cyj.me</uri></author><category term="programming" /><summary type="html"><![CDATA[About Module 是一系列文章，本系列想要讨论的：]]></summary></entry><entry><title type="html">前端模块的现状</title><link href="http://cyj.me/programming/2018/05/23/about-module-ii/" rel="alternate" type="text/html" title="前端模块的现状" /><published>2018-05-23T00:00:00+00:00</published><updated>2018-05-23T00:00:00+00:00</updated><id>http://cyj.me/programming/2018/05/23/about-module-ii</id><content type="html" xml:base="http://cyj.me/programming/2018/05/23/about-module-ii/"><![CDATA[<p>All About Module 是一系列文章，本系列想要讨论的：</p>

<ul>
  <li><a href="/programming/2018/05/22/about-module-i/">前端模块的历史沿革 A Brief History of Module</a></li>
  <li>前端模块的现状 The Status Quo of Module</li>
  <li><a href="/programming/2018/05/24/about-module-iii/">前端模块在广告业务的实践 The Module Practiced in Cara</a></li>
</ul>

<p>本文为系列文章第二篇，讨论前端模块的现状。在历史沿革一文中，我们讨论了 CommonJS 以及一系列衍生规范，甚至连最最新的 <code class="language-plaintext highlighter-rouge">import()</code> 都有提及，唯独没有讲  ES Module。本文就从它说起。</p>

<h2 id="es-module">ES Module</h2>

<p>ES Module 是 2015 年颁布的 ES2015（原名 ES6）标准所覆盖的特性之一，设计目标是整合 CommonJS、AMD 等已有模块方案，提供一个标准的、更高效的做法。ES Module 与现有方案的区别主要在以下方面：</p>

<ul>
  <li>声明式而非命令式，或者说 <code class="language-plaintext highlighter-rouge">import</code> 是声明语句 Declaration 而非表达式 Statement</li>
  <li><code class="language-plaintext highlighter-rouge">import</code> 和 <code class="language-plaintext highlighter-rouge">export</code> 的值也和 CommonJS 这种以 <code class="language-plaintext highlighter-rouge">exports</code> <code class="language-plaintext highlighter-rouge">Object</code> 为载体的方式不同</li>
  <li>默认运行环境为 <code class="language-plaintext highlighter-rouge">module</code> ，相当于 <code class="language-plaintext highlighter-rouge">script</code> 模式的普通脚本 <code class="language-plaintext highlighter-rouge">'use strict'</code> 开启严格模式</li>
</ul>

<p>第一点的区别，主要在 ES Module 中无法使用 import 声明带变量的依赖、或者动态引入依赖：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// WRONG</span>
<span class="k">import</span> <span class="o">*</span> <span class="k">as</span> <span class="nx">env</span> <span class="k">from</span> <span class="s2">`./env/</span><span class="p">${</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">NODE_ENV</span><span class="p">}</span><span class="s2">.js`</span>

<span class="c1">// WRONG</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">BROWSER</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">import</span> <span class="dl">"</span><span class="s2">./browser.js</span><span class="dl">"</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
  <span class="k">import</span> <span class="dl">"</span><span class="s2">./node.js</span><span class="dl">"</span>
<span class="p">}</span>
</code></pre></div></div>

<p>对务实主义的 Node.js 开发者来说，这些区别都让 npm 所营造出来的海量社区代码陷入一种尴尬的境地，无论是升级还是兼容都需要大量的工作。对此，David Herman 撰文解释，<a href="http://calculist.org/blog/2012/06/29/static-module-resolution/">ES Module 所带来的好处远大于不便</a>：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">静态 import 能确保被编译成变量引用</code>，这些引用在当前执行环境运行时能被解析器（通过 JIT 编译 polymorphic inline cache）优化，执行更有效率</li>
  <li><code class="language-plaintext highlighter-rouge">静态 export 能让变量检测更准确</code>，在 JSHint、ESLint 等代码检测工具中，变量是否定义是个非常受欢迎的功能，而静态 export 能让这一检测更具准确性</li>
  <li><code class="language-plaintext highlighter-rouge">更完备的循环依赖处理</code>，在 Node.js 等已有的 CommonJS 实现中，循环依赖是通过传递未完成的 exports 对象解决的，对于直接引用 exports.foo 或者父模块覆盖 module.exports 的情况，传统方式无从解决，而因为 ES Module 传递的是引用，便不会有这些问题</li>
</ul>

<p>其他还有对未来可能新增的标准（宏、类型系统等）更兼容等，在《Exploring ES6》<a href="http://2ality.com/2014/09/es6-modules-final.html">一书也有介绍</a>，不再赘述。</p>

<h2 id="es-module-in-browser">ES Module in Browser</h2>

<p>在 ES Module 标准出来之前，尽管社区实现的 Loader 一箩筐，但浏览器自身一直没有选定模块方案，支持 ES Module 对浏览器来说还是比较少顾虑的。
由于 ES Module 的执行环境和普通脚本不同，浏览器选择增加 <code class="language-plaintext highlighter-rouge">&lt;script type="module"&gt;</code> ，只有 <code class="language-plaintext highlighter-rouge">&lt;script type="module"&gt;</code> 中的脚本（和 <code class="language-plaintext highlighter-rouge">import</code> 进来的脚本）才是 <code class="language-plaintext highlighter-rouge">module</code> 模式。也只有 <code class="language-plaintext highlighter-rouge">module</code> 模式执行的脚本，才可以声明 <code class="language-plaintext highlighter-rouge">import</code> 。也就是说，下面这种代码是不行的：</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;script&gt;</span>
<span class="k">import</span> <span class="nx">foo</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">./foo.js</span><span class="dl">"</span>
<span class="nt">&lt;/script&gt;</span>

<span class="nt">&lt;script </span><span class="na">type=</span><span class="s">"javascript"</span><span class="nt">&gt;</span>
<span class="k">import</span> <span class="nx">bar</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">./bar.js</span><span class="dl">"</span>
<span class="nt">&lt;/script&gt;</span>
</code></pre></div></div>

<p>目前，几大常青浏览器都<a href="https://caniuse.com/#search=import">已支持 ES Module</a>。最后一个支持的是 Firefox，2018 年 5 月 8 日发布的 Firefox 60 正式支持 ES Module。</p>

<p>此外，考虑到向后兼容，浏览器还增加 <code class="language-plaintext highlighter-rouge">&lt;script nomodule&gt;</code> 标签。开发者可以使用 <code class="language-plaintext highlighter-rouge">&lt;script nomodule&gt;</code> 标签兼容不支持 ES Module 的浏览器：</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;script </span><span class="na">type=</span><span class="s">"module"</span> <span class="na">src=</span><span class="s">"./app.js"</span><span class="nt">&gt;&lt;/script&gt;</span>
<span class="nt">&lt;script </span><span class="na">nomodule</span> <span class="na">src=</span><span class="s">"./app.bundle.js"</span><span class="nt">&gt;&lt;/script&gt;</span>
</code></pre></div></div>

<h2 id="es-module-in-nodejs">ES Module in Node.js</h2>

<p>但在 Node.js 这边，ES Module 遭遇的声音要大很多。前 Node.js 领导者 Isaacs Schlutuer 甚至认为 ES Module 太过阳春白雪且不考虑实际情况，毫无价值（<a href="http://blog.izs.me/post/25906678790/on-es-6-modules">adds nothing</a>）。本系列的第一篇文章也讲到，WHATWG Loader 标准在 Node.js 社区同样碰壁。但值得庆幸的是，WHATWG Loader 成员并没有就此放弃，而是选择以一种更直接的方式，在兼顾存量代码的前提下，去完善 <a href="https://nodejs.org/api/esm.html">Node.js 对 ES Module 的支持</a>。</p>

<p>首先纠结的是如何支持 module 执行模式，是自动检测，还是 <code class="language-plaintext highlighter-rouge">'use module'</code> ，还是在 <code class="language-plaintext highlighter-rouge">package.json</code> 里增加 <code class="language-plaintext highlighter-rouge">module</code> 属性作为专门的入口，还是干脆增加一个新的扩展名？</p>

<p>最终 Node.js 选择增加新的扩展名 .mjs ：</p>

<ul>
  <li>在 <code class="language-plaintext highlighter-rouge">.mjs</code> 中可以自如使用 <code class="language-plaintext highlighter-rouge">import</code> 和 <code class="language-plaintext highlighter-rouge">export</code></li>
  <li>在 <code class="language-plaintext highlighter-rouge">.mjs</code> 中不可以使用 <code class="language-plaintext highlighter-rouge">require</code></li>
  <li>在 <code class="language-plaintext highlighter-rouge">.js</code> 中只能使用 <code class="language-plaintext highlighter-rouge">require</code></li>
  <li>在 <code class="language-plaintext highlighter-rouge">.js</code> 中不可以使用 <code class="language-plaintext highlighter-rouge">import</code> 和 <code class="language-plaintext highlighter-rouge">export</code></li>
</ul>

<p>也就是两套模块系统完全独立。此外，依赖查找方式也有变化，原本 <code class="language-plaintext highlighter-rouge">require.extensions</code> 是：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span> <span class="dl">'</span><span class="s1">.js</span><span class="dl">'</span><span class="p">:</span> <span class="p">[</span><span class="nb">Function</span><span class="p">],</span>
  <span class="dl">'</span><span class="s1">.json</span><span class="dl">'</span><span class="p">:</span> <span class="p">[</span><span class="nb">Function</span><span class="p">],</span>
  <span class="dl">'</span><span class="s1">.node</span><span class="dl">'</span><span class="p">:</span> <span class="p">[</span><span class="nb">Function</span><span class="p">]</span> <span class="p">}</span>
</code></pre></div></div>

<p>如今（需要开启 <code class="language-plaintext highlighter-rouge">--experimental-modules</code> 选项）则是：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span> <span class="dl">'</span><span class="s1">.js</span><span class="dl">'</span><span class="p">:</span> <span class="p">[</span><span class="nb">Function</span><span class="p">],</span>
  <span class="dl">'</span><span class="s1">.json</span><span class="dl">'</span><span class="p">:</span> <span class="p">[</span><span class="nb">Function</span><span class="p">],</span>
  <span class="dl">'</span><span class="s1">.node</span><span class="dl">'</span><span class="p">:</span> <span class="p">[</span><span class="nb">Function</span><span class="p">],</span>
  <span class="dl">'</span><span class="s1">.mjs</span><span class="dl">'</span><span class="p">:</span> <span class="p">[</span><span class="nb">Function</span><span class="p">]</span> <span class="p">}</span>
</code></pre></div></div>

<p>但两套独立的模块系统也导致第二个纠结的方面，模块系统彼此之间如何互通？对浏览器来说这不是问题，但对 Node.js 来说，npm 中海量的 CommonJS 模块是它不得不考虑的。</p>

<p>最终确定的方案倒也简单，在 <code class="language-plaintext highlighter-rouge">.mjs</code> 里，开发者可以 <code class="language-plaintext highlighter-rouge">import</code> CommonJS（虽然只能 <code class="language-plaintext highlighter-rouge">import</code> default）：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="dl">'</span><span class="s1">fs</span><span class="dl">'</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">fs</span><span class="dl">'</span>
<span class="k">import</span> <span class="p">{</span> <span class="nx">readFile</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">fs</span><span class="dl">'</span>
<span class="k">import</span> <span class="nx">foo</span> <span class="k">from</span> <span class="dl">'</span><span class="s1">./foo</span><span class="dl">'</span>
<span class="c1">// etc.</span>
</code></pre></div></div>

<p>在 <code class="language-plaintext highlighter-rouge">.js</code> 里，开发者自然不能 <code class="language-plaintext highlighter-rouge">import</code> ES Module，但他们可以 <code class="language-plaintext highlighter-rouge">import()</code> ：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span><span class="p">(</span><span class="dl">'</span><span class="s1">./foo</span><span class="dl">'</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="nx">foo</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="c1">// use foo</span>
<span class="p">})</span>

<span class="k">async</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">bar</span> <span class="o">=</span> <span class="k">await</span> <span class="k">import</span><span class="p">(</span><span class="dl">'</span><span class="s1">./bar</span><span class="dl">'</span><span class="p">)</span>
  <span class="c1">// use bar</span>
<span class="p">}()</span>
</code></pre></div></div>

<p>注意，和浏览器以引入方式判断运行模式不同，Node.js 中脚本的运行模式是和扩展名绑定的。也就是说，依赖的查找方式会有所不同：</p>

<ul>
  <li>在 <code class="language-plaintext highlighter-rouge">.js</code> 中 <code class="language-plaintext highlighter-rouge">require('./foo')</code> 找的是 <code class="language-plaintext highlighter-rouge">./foo.js</code> 或者 <code class="language-plaintext highlighter-rouge">./foo/index.js</code></li>
  <li>在 <code class="language-plaintext highlighter-rouge">.mjs</code> 中 <code class="language-plaintext highlighter-rouge">import './bar'</code> 找的是 <code class="language-plaintext highlighter-rouge">./bar.mjs</code> 或者 <code class="language-plaintext highlighter-rouge">./bar/index.mjs</code></li>
</ul>

<p>善用这些特性，我们现在就可以将已有的 npm 模块升级成 ES Module，并且仍然支持 CommonJS 方式。</p>

<h2 id="dual-mode-packages">Dual-Mode Packages</h2>

<p>双模式 npm 包（Dual-Mode Package）概念（以及本文大量 Node.js 相关内容）来自《<a href="https://medium.com/@giltayar/native-es-modules-in-nodejs-status-and-future-directions-part-i-ee5ea3001f71">Native ES Modules in Node.js: Stauts and Future Directions, Part I</a>》一文。</p>

<p>首先 package.json 中的 main 需要去掉扩展名：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span>
  <span class="dl">"</span><span class="s2">name</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">some-package</span><span class="dl">"</span><span class="p">,</span>
  <span class="dl">"</span><span class="s2">main</span><span class="dl">"</span><span class="p">:</span> <span class="dl">"</span><span class="s2">./index</span><span class="dl">"</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Node.js 会自行根据父模块的运行模式决定应当加载哪个文件。也就是说，我们需要 <code class="language-plaintext highlighter-rouge">index.mjs</code> 和 <code class="language-plaintext highlighter-rouge">index.js</code> 两个文件，前者提供 ES Module，后者提供 CommonJS 模块。这当然不是说我们得人肉写两个版本，而是可以选择使用 ES Module 书写全部代码，继而在发布前使用 Babel 或者 Traceur 或者其他 transpiler 编译。</p>

<p>这样，通过双模式的 npm 包，我们既可以升级模块代码，也可以兼顾新旧两种使用方式。</p>

<h2 id="dynamic-import">Dynamic Import</h2>

<p>静态 <code class="language-plaintext highlighter-rouge">import</code> 固然好，但动态引入依赖这一实际需求不能不考虑。例如在 React 等代码中我们经常看到条件依赖：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="p">(</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">NODE_ENV</span> <span class="o">!==</span> <span class="dl">'</span><span class="s1">production</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">./cjs/react.development.js</span><span class="dl">'</span><span class="p">)</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
  <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">./cjs/react.production.js</span><span class="dl">'</span><span class="p">)</span>
<span class="p">}</span>

<span class="k">if</span> <span class="p">(</span><span class="nx">process</span><span class="p">.</span><span class="nx">env</span><span class="p">.</span><span class="nx">BROWSER</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">./browser.js</span><span class="dl">'</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>目前采用的处理方式是先使用 loose-envify 替换代码中的环境变量名，继而让 webpack 移除条件判断中的无效分支。假如环境变量为 <code class="language-plaintext highlighter-rouge">{ NODE_ENV: 'production', BROWSER: false }</code> ，上述代码将变为：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">./cjs/react.production.js</span><span class="dl">'</span><span class="p">)</span>
</code></pre></div></div>

<p>为此，Domenic Denicola 起草 <code class="language-plaintext highlighter-rouge">import()</code> <a href="https://github.com/tc39/proposal-dynamic-import">标准提案</a>，作用与 <code class="language-plaintext highlighter-rouge">System.import()</code> 相若，来满足上述使用情况。<code class="language-plaintext highlighter-rouge">import()</code> 的特殊之处在于，import 本身是关键词，<code class="language-plaintext highlighter-rouge">import()</code> 并非普通函数，因此它不会影响已有代码。但对想要兜底实现相关逻辑的人来说，想要自主实现 <code class="language-plaintext highlighter-rouge">import()</code> 就变得很困难了。</p>

<p>除了用来处理动态依赖，<code class="language-plaintext highlighter-rouge">import()</code> 也被用来从 <code class="language-plaintext highlighter-rouge">script</code> 环境引入 <code class="language-plaintext highlighter-rouge">module</code> 。在 HTML 中：</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;script&gt;</span>
<span class="k">import</span><span class="p">(</span><span class="dl">'</span><span class="s1">./foo.js</span><span class="dl">'</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="nx">foo</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="c1">// use foo</span>
<span class="p">})</span>
<span class="nt">&lt;/script&gt;</span>
</code></pre></div></div>

<p>在 Node.js 中（<code class="language-plaintext highlighter-rouge">.js</code> 文件）：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span><span class="p">(</span><span class="dl">'</span><span class="s1">./foo.mjs</span><span class="dl">'</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="nx">foo</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="c1">// use foo</span>
<span class="p">})</span>
</code></pre></div></div>

<h2 id="importmeta">import.meta</h2>

<p>另一个 <code class="language-plaintext highlighter-rouge">import</code> 的特殊之处在于 <code class="language-plaintext highlighter-rouge">import.meta</code> ，无论是 Node.js 还是浏览器，在 <code class="language-plaintext highlighter-rouge">module</code> 模式下，开发者都可以通过 <code class="language-plaintext highlighter-rouge">import.meta</code> 获取当前模块的元数据。目前仅暴露 <code class="language-plaintext highlighter-rouge">import.meta.url</code> ，浏览器中拿到的是当前模块的 url，Node.js 中则是 <code class="language-plaintext highlighter-rouge">file:///</code> 开头的文件路径。</p>

<h2 id="afterword">Afterword</h2>

<p>到目前为止，使用 ES Module 编写浏览器、Node.js 通用的 JavaScript 代码已经完全可行，而且还不依赖任何编译或者打包工具。开发者只需要确保 ES Module 扩展名为 <code class="language-plaintext highlighter-rouge">.mjs</code> ，就可以在浏览器里：</p>

<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;script </span><span class="na">type=</span><span class="s">"module"</span><span class="nt">&gt;</span>
<span class="k">import</span> <span class="dl">"</span><span class="s2">./foo.mjs</span><span class="dl">"</span>
<span class="nt">&lt;/script&gt;</span>
</code></pre></div></div>

<p>在 Node.js 里（仍需开启 <code class="language-plaintext highlighter-rouge">--experimental-modules</code> 选项）：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="dl">"</span><span class="s2">./foo</span><span class="dl">"</span>
<span class="c1">// or</span>
<span class="k">import</span> <span class="dl">"</span><span class="s2">./foo.mjs</span><span class="dl">"</span>
</code></pre></div></div>

<p>以上便是 ES Module 在浏览器和 Node.js 中的现状，看起来前途光明。那么，我们还需要编译或者打包工具么？我将在本系列的最后一篇《<a href="/programming/2018/05/24/about-module-iii/">前端模块在广告业务的实践 The Module Practiced in Cara</a>》回答这个问题。</p>]]></content><author><name>Chen Yangjian</name><uri>http://cyj.me</uri></author><category term="programming" /><summary type="html"><![CDATA[All About Module 是一系列文章，本系列想要讨论的：]]></summary></entry><entry><title type="html">前端模块的历史沿革</title><link href="http://cyj.me/programming/2018/05/22/about-module-i/" rel="alternate" type="text/html" title="前端模块的历史沿革" /><published>2018-05-22T00:00:00+00:00</published><updated>2018-05-22T00:00:00+00:00</updated><id>http://cyj.me/programming/2018/05/22/about-module-i</id><content type="html" xml:base="http://cyj.me/programming/2018/05/22/about-module-i/"><![CDATA[<p>All About Module 是一系列文章，本系列想要讨论的：</p>

<ul>
  <li>前端模块的历史沿革 A Brief History of Module</li>
  <li><a href="/programming/2018/05/23/about-module-ii/">前端模块的现状 The Status Quo of Module</a></li>
  <li><a href="/programming/2018/05/24/about-module-iii/">前端模块在广告业务的实践 The Module Practiced in Cara</a></li>
</ul>

<p>本文讨论的<strong>前端模块的历史沿革</strong>可以用一张图概括：</p>

<p><img src="https://img.alicdn.com/tfscom/TB1wIZoby6guuRjy1XdXXaAwpXa.png_1200x1200.jpg" alt="" /></p>

<h2 id="serverjs">ServerJS</h2>

<p>不过作为一篇掉书袋的文章，我得从老老年间开始说起。和许多前端技术点类似，前端模块的概念离不开后端贡献。早在 1997 年，Netscape 想要 JavaScript 能在 Java 平台上运行，以配合当时的 JavaScript/Java 联姻，因此开发了 <a href="https://developer.mozilla.org/en-US/docs/Mozilla/Projects/Rhino">Rhino</a>。Rhino 意为犀牛，得名的原因正是那本著名的《JavaScript 权威指南》（<a href="https://www.google.com/search?ei=a57-Ws3iFI2UsgXTm4zQAg&amp;q=when+is+javascript+the+definitive+guide+first+published&amp;oq=when+is+javascript+the+definitive+guide+first+published&amp;gs_l=psy-ab.3..33i21k1.15666.20254.0.20317.24.16.0.0.0.0.509.1795.2-2j2j0j1.5.0....0...1c.1.64.psy-ab..20.3.990...33i22i29i30k1j33i160k1.0.qTjUY6_Oipc">1996 年首次出版</a>）：</p>

<p><img src="https://img.alicdn.com/tfscom/TB1XYfImrsrBKNjSZFpXXcXhFXa.png_1200x1200.jpg" alt="" /></p>

<p>Rhino 进入到 21 世纪后，随着 JavaScript 语言渐趋成熟（应该归功于 ECMAScript 5），所谓「同构」（使用 JavaScript 搞定前后端）项目的呼声渐起。一个名为 <a href="https://wiki.mozilla.org/ServerJS">ServerJS</a> 的标准化项目逐渐展开，旨在规范化 JavaScript 在服务端使用时的模块化，以及 Filesystem API、I/O Streams、Socket IO 等等服务端开发领域所涉及内容的标准化。</p>

<h2 id="commonjs">CommonJS</h2>

<p>JavaScript 社区在 2009 年有两件比较大的事情：</p>

<ul>
  <li>ServerJS 发展成大家耳熟能详的 <a href="http://wiki.commonjs.org/wiki/CommonJS">CommonJS</a>，最终发展出 <a href="http://wiki.commonjs.org/wiki/Modules/1.1.1">Modules/1.1.1</a> 和 <a href="http://wiki.commonjs.org/wiki/Modules/Async/A">Modules/Async/A</a></li>
  <li>Ryan Dahl 发布动如脱兔的 Node.js</li>
</ul>

<p>直接使用 CommonJS 规范实现模块体系的 Node.js 广受欢迎，相信绝大部分 Web 开发者至今都管 Node.js 的模块体系叫 CommonJS 规范：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="dl">'</span><span class="s1">use strict</span><span class="dl">'</span>
<span class="kd">const</span> <span class="nx">math</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">./math</span><span class="dl">'</span><span class="p">)</span>
<span class="nx">exports</span><span class="p">.</span><span class="nx">sum</span> <span class="o">=</span> <span class="kd">function</span><span class="p">(...</span><span class="nx">nums</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">return</span> <span class="nx">nums</span><span class="p">.</span><span class="nx">reduce</span><span class="p">((</span><span class="nx">result</span><span class="p">,</span> <span class="nx">num</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">result</span> <span class="o">+</span> <span class="nx">num</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>事实上，两者的关系并非我们认为的标准制定者和标准执行者的角色。作为当时的 Node.js 领头人，Isaacs Schlueter 曾在 2013 年对 CommonJS 的人提出的「为何不遵从标准」的疑问发表过<a href="https://github.com/nodejs/node-v0.x-archive/issues/5132#issuecomment-15432598">一则比较激烈的评论</a>：</p>

<blockquote>
  <p>A few good things came out of CommonJS. The module system we have now is basically indistinguishable from the original “securable modules” proposal that Kris Kowal originally came up with. (Of course, we went pretty far off the reservation in v0.4, with the whole node_modules folder thing, and loading packages via their “main” field. Maybe we should remove those features, I’m sure that Node users would appreciate us being more spec-compliant!)</p>
</blockquote>

<p>评论中认为，CommonJS 标准已经成为小众服务端 JS（Server Side JS）方案的文档集中地，而 Node.js 已经赢得服务端 JS 的竞争，如同 Node.js 创始人 Ryan Dahl 所说：</p>

<blockquote>
  <p>“Forget CommonJS. It’s dead. We are server side JavaScript.”</p>
</blockquote>

<p>Node.js 就是服务端 JavaScript。更为重要的是，Isaac 更看重真实用户的声音而不是所谓标准制定者的意见，而当时 CommonJS 工作组所提出的新标准更多的是添乱（比如所谓的 Package 标准）。简而言之，到 2013 年的时候，其实 <a href="https://nodejs.org/docs/latest/api/modules.html">Node.js Modules</a> 就已经自成一家了。</p>

<blockquote>
  <p>上上段引用中，有个大家可能熟悉的名字，Kris Kowal。这个人不仅起草了 CommonJS 模块规范，还提出 Promise 规范，并开发最初的 Promise 实现 <a href="https://github.com/kriskowal/q">Q.js</a>。只是他这两项开创性的工作在后来都被更好或者更标准化的方案取代了，前者是 ES Module，后者则先是有青出于蓝的 bluebird，继而也被标准化的 Promise 内建实现取代。</p>

  <p>哦对，他还写过一个 <a href="https://github.com/kriskowal/uncommonjs/blob/master/modules/specification.md">UncommonJS 规范</a>。</p>
</blockquote>

<h2 id="the-clash-of-loaders">The Clash of Loaders</h2>

<p>回到风起云涌的 2009 年，真正在前端领域摸爬滚打的开发者们正对着一堆 <code class="language-plaintext highlighter-rouge">&lt;script&gt;</code> 标签发愁。在浏览器中如何管理依赖，在当时是一个很时髦的话题。YUI 2 和 Google Closure Library 都提出过基于 namespace 的方案，但治标不治本，仍然需要人肉确保脚本的加载、打包顺序。</p>

<p>CommonJS 提出后，前端同学广受启发，加上浏览器加载的异步特性，想出许多 API，最终被标准化为 Modules/Async/A 的是 <code class="language-plaintext highlighter-rouge">require.ensure</code> ：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">require</span><span class="p">.</span><span class="nx">ensure</span><span class="p">([</span><span class="dl">'</span><span class="s1">increment</span><span class="dl">'</span><span class="p">],</span> <span class="kd">function</span><span class="p">(</span><span class="nx">require</span><span class="p">)</span> <span class="p">{</span>
    <span class="kd">var</span> <span class="nx">inc</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">increment</span><span class="dl">'</span><span class="p">).</span><span class="nx">inc</span><span class="p">;</span>
    <span class="kd">var</span> <span class="nx">a</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>
    <span class="nx">inc</span><span class="p">(</span><span class="nx">a</span><span class="p">);</span> <span class="c1">// 2</span>
<span class="p">});</span>
</code></pre></div></div>

<p><a href="https://webpack.js.org/guides/code-splitting/">Webpack code splitting</a> 后，传统方式即使用 <code class="language-plaintext highlighter-rouge">require.ensure</code> 方法加载相关的分片脚本（新方式则是直接使用 <code class="language-plaintext highlighter-rouge">import</code> ）。但如何请求脚本只是一方面，更为棘手的问题是如何处理 CommonJS 模块让它可以在浏览器中自如加载：</p>

<ul>
  <li>有提出 transport（代码转换）方案的，将代码放在一个匿名函数中，异步加载，顺序执行；</li>
  <li>有提出 XHR 请求模块代码，再 eval 或者 new Function 执行的；</li>
  <li>有提出应当直接改良 CommonJS，推出纯异步的模块加载方案的；</li>
</ul>

<p>方案一很快被大家所接受，但在 transport （前身叫 <a href="http://wiki.commonjs.org/wiki/Modules/Wrappings">Wrapping</a>）上仍然有显著分歧：</p>

<ul>
  <li>有认为这层 transport 应当强制，要求开发者写明模块 id、依赖</li>
  <li>有认为这层 transport 应当强制，但 id、依赖可以自动解析</li>
  <li>有认为这层 transport 没必要要求开发者来写，可以通过工具自动转换，id、依赖可以在转换时解析</li>
</ul>

<p>前两者，代表了 2009-2010 年间绝大部分模块加载器的实现方案，比如要求写明依赖的 kslite：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">KSLITE</span><span class="p">.</span><span class="nx">declare</span><span class="p">(</span><span class="dl">'</span><span class="s1">app</span><span class="dl">'</span><span class="p">,</span> <span class="p">[</span><span class="dl">'</span><span class="s1">math</span><span class="dl">'</span><span class="p">],</span> <span class="kd">function</span><span class="p">(</span><span class="nx">require</span><span class="p">,</span> <span class="nx">exports</span><span class="p">,</span> <span class="nx">module</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">math</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">math</span><span class="dl">'</span><span class="p">)</span>
<span class="p">})</span>
</code></pre></div></div>

<p>比如由加载器自动解析 id 和依赖的 SeaJS：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">define</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">require</span><span class="p">,</span> <span class="nx">exports</span><span class="p">,</span> <span class="nx">modules</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">math</span><span class="dl">'</span><span class="p">)</span>
<span class="p">})</span>
<span class="c1">// SeaJS 会从当前 JavaScript 解析 id 和依赖</span>
</code></pre></div></div>

<p>解析 id 的方式是获取当前脚本的 <code class="language-plaintext highlighter-rouge">src</code> 再去匹配 SeaJS 配置或者解析好的 <code class="language-plaintext highlighter-rouge">base</code> ，解析依赖的方式在当时则有较多的讨论。简单来说，都离不开 <code class="language-plaintext highlighter-rouge">Function.prototype.toString()</code> 以及正则表达式。</p>

<p>在当时，支持方案一且采用第三种观点的人并不多。SeaJS 开发者玉伯<a href="https://github.com/dexteryy/OzJS/issues/10#issuecomment-14034196">对构建工具的态度</a>是有所保留的，认为能解决一定问题但增加开发人员的使用成本。</p>

<p>而方案三，直接改良 CommonJS 让它更适合浏览器环境，则催生了最广为人知的模块加载器 <a href="http://requirejs.org/">RequireJS</a>。它认为与其兼容麻烦的同步 <code class="language-plaintext highlighter-rouge">require</code> 调用，不如采用纯异步 API：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">requirejs</span><span class="p">([</span><span class="dl">'</span><span class="s1">foo</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">bar</span><span class="dl">'</span><span class="p">],</span> <span class="kd">function</span><span class="p">(</span><span class="nx">foo</span><span class="p">,</span> <span class="nx">bar</span><span class="p">)</span> <span class="p">{</span>
  <span class="c1">// use foo, bar</span>
<span class="p">})</span>
</code></pre></div></div>

<p>这套 API 也有其规范，叫做 <a href="https://github.com/amdjs/amdjs-api/wiki/AMD">AMD（Asynchronouse Module Definition）</a>。但相比 CommonJS，AMD 有两个问题。比较小的问题是依赖的声明与使用不在一个地方，依赖数组 <code class="language-plaintext highlighter-rouge">['foo', 'bar']</code> 可能很长，和匿名函数的参数对应时会有麻烦。比较严重的问题，是 foo、bar 这两个 js 的执行顺序不确定。在 CommonJS 里，先 require 的先执行。但在 AMD 里，foo、bar 谁先执行是个黑箱。</p>

<blockquote>
  <p>其实 RequireJS 也<a href="http://requirejs.org/docs/commonjs.html">支持 CommonJS 风格的模块定义</a>，即上面举例的 kslite、SeaJS 中所采用的风格。</p>
</blockquote>

<p>到这一部分，在 SeaJS 的<a href="https://github.com/seajs/seajs/issues/588">一则 issue</a> 中也有详细讲述。比较有趣的是，因为 2013 年发生了 Node.js 小组与 CommonJS 规范小组分道扬镳的事情，有 StackOverflow 答主认为 RequireJS 才是新时尚，但其实他们只是一棵树上开的两朵花。</p>

<p>到 Node.js 成为事实 Server Side JS 标准的 2013 年，CommonJS 在前后端领域的实践也日趋成熟，连原本追随 YUI 3 的 KISSY 都演化出一个名为 <a href="https://github.com/kissyteam/modulex">modulex</a> 的模块加载方案，兼容 CommonJS 与 AMD。开始用 JavaScript 跨后后端的前端同学提出一个著名的疑问：</p>

<blockquote>
  <p>如何真正做到前后端共享代码？</p>
</blockquote>

<p>代码复用简直是软件开发领域的圣杯，虽然有时会被务实主义者弃如敝履，但仍然不妨碍它成为程序员的野望之一。等于对当时计划使用 Node.js 开启新项目、并且项目中真的就需要前后端共享代码的我来说，遵从 CommonJS 并采用构建工具 transport 模块成 Loader 能够加载的版本，是最自然不过的选择。</p>

<p>我的方案是编写一个 Express 中间件，动态处理前端代码，解析 id 和依赖，剩下的交给 SeaJS 处理。SeaJS 彼时在做的 spm（当时做 *pm 也是一种流行）也开始支持模块转换，只是方式略有差别。然而这种在已有 Loader 基础上二次加工所产出的方案，都敌不过一个 2012 年开始的项目的降维打击。</p>

<h2 id="browserify--webpack">Browserify &amp; Webpack</h2>

<p><a href="https://webpack.js.org/">Webpack</a> 选择了一个很好的切入点，立足于前端领域当时的几大痛点：</p>

<ul>
  <li>由于浏览器对 HTTP 请求数的限制，以及多请求时的额外耗时，前端资源需要尽可能合并，尤其在初兴的无线领域，网络情况更为复杂；</li>
  <li>模块很重要但是模块化标准太多，有用 CommonJS 写然后用 npm 管理的，也有用 RequireJS 写然后直接放 Github 的，社区代码使用成本较高；</li>
  <li>没人真的想要 *pm，只用 npm 就够了。</li>
</ul>

<p>Webpack 最出色的特性一是它的模块解析粒度以及因此带来的强大打包能力（图片、SVG 都给你转 data uri 打包进去），二是它的可扩展性。虽然中间经历起伏，但它的社区一直活跃，相关转换工具（Babel、PostCSS、CSS Modules）可以变成插件快速接入，还能自定义 Loader（比如在我看来神烦的 worker-loader）。这些特性加在一起，无往而不利。</p>

<p>而且它还支持那波 Loader 所无从想象的，2015 年发布的 ES6 标准（后来索性叫 ES2015）带来的 <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/import">ES Module</a>：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">import</span> <span class="nx">defaultExport</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">module-name</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="o">*</span> <span class="k">as</span> <span class="nx">name</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">module-name</span><span class="dl">"</span><span class="p">;</span>
<span class="k">import</span> <span class="p">{</span> <span class="k">export</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">module-name</span><span class="dl">"</span><span class="p">;</span>
</code></pre></div></div>

<p>这便是构建工具带来的好处了，发挥空间远比传统浏览器 Loader 来得大，可以轻松加入像 Babel、Traceur 等 transpiler 支持。
夸完 Webpack，也得说说 Node.js 社区开出的另一朵花，比 Webpack 还要早一年发布的 <a href="http://browserify.org/">Browserify</a>。</p>

<p><img src="http://browserify.org/images/browserify.png" alt="" /></p>

<p>Node.js 社区早期活跃成员 substack 开发 Browserify 的初衷非常简单：</p>

<blockquote>
  <p>Browsers don’t have the require method defined, but Node.js does. With Browserify you can write code that uses require in the same way that you would use it in Node.</p>
</blockquote>

<p>相比 Webpack，Browserify 的方案要更朴实一些，就是允许浏览器代码使用 require ，并且是 require 自有模块、Node.js 自带模块、npm 社区模块全都支持。另一个和务实主义的 Webpack 比较不一样的地方，是 Browserify 对异步 require 的态度。<a href="https://github.com/browserify/browserify/issues/58">尽管社区呼声一直很高</a>，作者坚持认为 Browserify 的 require 应当和 Node.js 保持一致。</p>

<p>Browserify 支持非 CommonJS 模块的方式是 transform，通过在 package.json 的 browserify.transform  数组配置转换插件，Browserify 可以吧非 CommonJS 输入转变为 CommonJS，继而进入后续流程。</p>

<h2 id="whatwg-loader">WHATWG Loader</h2>

<p>在 ECMAScript 2015 发布的同年，独辟蹊径的 <a href="https://github.com/systemjs/systemjs">SystemJS</a> 也开始开发。因为 Web 平台技术日新月异，浏览器所提供的 API 与性能表现和 2009 年早已今非昔比，SystemJS 选择在浏览器中直接加入 transpiler 支持。SystemJS 中可供选择的 transpiler 有著名的 Babel 和 TypeScript，也有如今似乎有些销声匿迹的 Traceur。</p>

<p>SystemJS 的背后其实也是一套标准，<a href="https://whatwg.github.io/loader/">WHATWG 的 Loader</a>。这套标准想要规范的基本上就是 SystemJS 所表现的，一个功能强大的、在浏览器和 Node.js 中都能运行的、自成一体、支持 Transpiler 配置的模块加载器。在当时的讨论中，这一标准的最终目的是期望进入 ECMAScript 规范。我们可以从它规范中对当时 tc39 新提出的 Reflect 全局变量、Symbol 的使用看出端倪。</p>

<p>然而比较尴尬的是，无论是 tc39 还是 Node.js CTC，都没有接纳这一标准。tc39 的成员 Domenic Denicola 认为 Loader 标准是和 ES Module 的兼容度并不好，且有过度设计之嫌。而 Node.js 中则是务实主义再度占据上风，认为 Loader 标准对 Node.js 已经完备的模块体系<a href="https://github.com/WebAssembly/design/issues/256">弊大于利</a>，<a href="https://github.com/whatwg/loader/issues/54">很不实际</a>。最为直接的争论点，就是 <code class="language-plaintext highlighter-rouge">Loader.import</code> 应当同步还是异步：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// WHATWG Loader</span>
<span class="nx">Loader</span><span class="p">.</span><span class="k">import</span><span class="p">(</span><span class="dl">'</span><span class="s1">./foo.js</span><span class="dl">'</span><span class="p">).</span><span class="nx">then</span><span class="p">(</span><span class="nx">foo</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="c1">// use foo</span>
<span class="p">})</span>

<span class="c1">// or with async/await</span>
<span class="kd">const</span> <span class="nx">foo</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">Loader</span><span class="p">.</span><span class="k">import</span><span class="p">(</span><span class="dl">'</span><span class="s1">./foo.js</span><span class="dl">'</span><span class="p">)</span>
</code></pre></div></div>

<p>前者大家很熟悉，和 <code class="language-plaintext highlighter-rouge">require.async</code> 没啥差别，只是 callback 变成了 Promise。后者就有些异想天开，直接把模块的执行上下文换成一个 <code class="language-plaintext highlighter-rouge">async function</code> ，这显然是不切实际的。当时的 Node.js 负责人 Trevor Norris 说，在服务端同步加载一个模块的耗时只有几毫秒，异步毫无必要。更不可接受的是，如果引入 Promise 变成异步，引起 Promise 雪球效应（Promise Snowball Effect），整个运行体系都会受影响。</p>

<p>WHATWG Loader 标准和 CommonJS 标准一样，最终变成一个历史的见证，a collection of interesting ideas。但正如我们总结内部失败项目常用的思辨技巧，极为 Loader 标准的参与者，例如 SystemJS 作者 <a href="https://github.com/guybedford">Guy Bedford</a>、热心网友 <a href="https://github.com/bmeck">Bradley Meck</a>，都从这个项目中积累了丰富经验，在此过程中的思辨，最终又影响了 ES Module 和 Node.js Module（前身即 CommonJS）的融合。</p>

<p>这波融合从 ECMAScript 2015 标准发布开始，到 <code class="language-plaintext highlighter-rouge">&lt;script type="module"&gt;</code> 进入主流浏览器，以及 Node.js 通过 <code class="language-plaintext highlighter-rouge">.mjs</code> 支持 ES Module 结束。也将是下一篇文章《The Status Quo of Module》将要讨论的。</p>

<h2 id="import">import()</h2>

<p>ES Module 和已经成型的 CommonJS、RequireJS 有着明显的区别：</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">import</code> 是声明 Declaration，不像 CommonJS 的 require() 是表达式 Statement</li>
  <li><code class="language-plaintext highlighter-rouge">import</code> 是预先解析、预先加载的，不像 RequireJS（或者 SystemJS）是执行到点了再发一个请求</li>
</ul>

<p>但对实际业务来说，后两者特性其实是有必要的。比如可能要用到类似反射（Reflection）特性：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="p">(</span><span class="nx">type</span> <span class="o">==</span> <span class="dl">'</span><span class="s1">foo</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">./foo</span><span class="dl">'</span><span class="p">)</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
  <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">./bar</span><span class="dl">'</span><span class="p">)</span>
<span class="p">}</span>

<span class="c1">// 或者夸张一点</span>
<span class="kd">const</span> <span class="p">{</span> <span class="nx">env</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">querystring</span><span class="dl">'</span><span class="p">).</span><span class="nx">parse</span><span class="p">(</span><span class="nx">location</span><span class="p">.</span><span class="nx">search</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
<span class="nx">require</span><span class="p">(</span><span class="s2">`./env/</span><span class="p">${</span><span class="nx">env</span><span class="p">}</span><span class="s2">`</span><span class="p">)</span>
</code></pre></div></div>

<p>比如单页应用可能要用到异步模块请求：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">async</span> <span class="kd">function</span> <span class="nx">loadPage</span><span class="p">(</span><span class="nx">name</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">const</span> <span class="nx">page</span> <span class="o">=</span> <span class="k">await</span> <span class="nx">SystemJS</span><span class="p">.</span><span class="k">import</span><span class="p">(</span><span class="s2">`./pages/</span><span class="p">${</span><span class="nx">name</span><span class="p">}</span><span class="s2">`</span><span class="p">)</span>
  <span class="nx">page</span><span class="p">.</span><span class="nx">load</span><span class="p">()</span>
<span class="p">}</span>
</code></pre></div></div>

<p>为了解决这两个问题，Domenic Denicola 提出 <code class="language-plaintext highlighter-rouge">import()</code> API。<code class="language-plaintext highlighter-rouge">import()</code> 是在现有条件下的 hack，启发自 WHATWG Loader 标准讨论，当时对一个嵌套 <code class="language-plaintext highlighter-rouge">import</code> 的支持方式有争议，即上文提及的 <code class="language-plaintext highlighter-rouge">await import</code> 。</p>

<p>在 JavaScript 运行环境中，<code class="language-plaintext highlighter-rouge">import</code> 是个关键词，我们不能拿它来命名函数或者变量，所以反过来说，用它来做异步模块加载 API 的函数名，其实很合适。确切地说，它是一个特殊的函数，<code class="language-plaintext highlighter-rouge">typeof import</code> 是拿不到信息的。</p>

<p>这个 WHATWG Loader 生出的又一个 interesting idea，如今已进入 <a href="https://html.spec.whatwg.org/multipage/webappapis.html#hostimportmoduledynamically(referencingscriptormodule,-specifier,-promisecapability)">HTML 标准</a>，在 Chrome 中已经支持。更令人鼓舞的是，这也将作为 Node.js 支持 ES Module 的武器之一，是的最终 Node.js 中不仅可以自如使用 ES Module，还可以<a href="https://medium.com/@giltayar/native-es-modules-in-nodejs-status-and-future-directions-part-i-ee5ea3001f71">在一定程度上与现有的 CommonJS 模块互通</a>。</p>

<blockquote>
  <p>This leads us to two additional rules, which I call The Rules of Interoperability:</p>
  <ul>
    <li>CJS can import ESM, but only using await import()</li>
    <li>ESM can import CJS using the import statement, but only a default import</li>
  </ul>
</blockquote>

<h2 id="afterword">Afterword</h2>

<p>以上帝视角来审视浏览器模块在这些年所经历的沿革，给人一种指点江山挥斥方遒的事后诸葛亮之感。到 2018 年，常青浏览器均已支持 <code class="language-plaintext highlighter-rouge">&lt;script type="module"&gt;</code> ，Node.js 的下一个 LTS 版本预计也会正式支持 <code class="language-plaintext highlighter-rouge">.mjs</code> ，希望 ES Module 和 <code class="language-plaintext highlighter-rouge">import()</code> 一起，给这场纷争画上一个句号。</p>

<p>本文刻意漏掉了 ES Module 的介绍与讨论，反倒是额外介绍了 <code class="language-plaintext highlighter-rouge">import()</code> ，目的是给系列文章的下一篇《<a href="/programming/2018/05/23/about-module-ii/">前端模块的现状 The Status Quo of Module</a>》做铺垫，一定要看哦。</p>]]></content><author><name>Chen Yangjian</name><uri>http://cyj.me</uri></author><category term="programming" /><summary type="html"><![CDATA[All About Module 是一系列文章，本系列想要讨论的：]]></summary></entry><entry><title type="html">Programming Leoric II</title><link href="http://cyj.me/programming/2018/01/11/programming-leoric-ii/" rel="alternate" type="text/html" title="Programming Leoric II" /><published>2018-01-11T00:00:00+00:00</published><updated>2018-01-11T00:00:00+00:00</updated><id>http://cyj.me/programming/2018/01/11/programming-leoric-ii</id><content type="html" xml:base="http://cyj.me/programming/2018/01/11/programming-leoric-ii/"><![CDATA[<p>上周末花了两天时间给 Leoric 增加 SQLite 支持。SQLite 是个极轻量的数据库，在轻应用架构中<a href="https://www.sqlite.org/mostdeployed.html">非常流行</a>，常见于浏览器、移动设备的操作系统、以及一些较为大型的客户端软件（比如 Skeype、iTunes、微信）。它本身非常简单，数据库实体可以是一个文件，也可以待在内存：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">new</span> <span class="nx">Database</span><span class="p">(</span><span class="dl">'</span><span class="s1">foo.sqlite3</span><span class="dl">'</span><span class="p">)</span>
<span class="k">new</span> <span class="nx">Database</span><span class="p">(</span><span class="dl">'</span><span class="s1">:memory:</span><span class="dl">'</span><span class="p">)</span>
</code></pre></div></div>

<p>所以在考虑给 Leoric 支持 MySQL 之外哪些数据库的时候，我想着接起来会非常容易，第一个考虑的是它。但事实上，SQLite 的 Node.js 支持很弱。在 npmjs.com 里能搜到的客户端，比较成熟的大概是如下几个：</p>

<ul>
  <li>better-sqlite3</li>
  <li>dblite</li>
  <li>sqlite</li>
  <li>sqlite3</li>
</ul>

<p>下载量最多的是 sqlite3，sqlite、better-sqlite3 和它都差不多，都是 C binding 的简单封装，然后三个在数据库初始化的时候 API 还全都不一样：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// sqlite3</span>
<span class="k">new</span> <span class="nx">Database</span><span class="p">(</span><span class="nx">db</span><span class="p">,</span> <span class="nx">mode</span><span class="p">,</span> <span class="nx">callback</span><span class="p">)</span>  <span class="c1">// callback based</span>
<span class="c1">// better-sqlite3</span>
<span class="k">new</span> <span class="nx">Database</span><span class="p">(</span><span class="nx">db</span><span class="p">,</span> <span class="p">{</span> <span class="nx">memory</span><span class="p">,</span> <span class="nx">readonly</span><span class="p">,</span> <span class="nx">fileMustExist</span> <span class="p">})</span> <span class="c1">// synchronous api</span>
<span class="c1">// sqlite</span>
<span class="nx">sqlite</span><span class="p">.</span><span class="nx">open</span><span class="p">(</span><span class="nx">db</span><span class="p">,</span> <span class="p">{</span> <span class="nx">Promsie</span> <span class="p">})</span>  <span class="c1">// callback/Promise based</span>
</code></pre></div></div>

<p>执行 SQL 的时候，则都是 SQLite 提供的四件套：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">db</span><span class="p">.</span><span class="nx">all</span><span class="p">(</span><span class="nx">sql</span><span class="p">,</span> <span class="p">[...</span><span class="nx">param</span><span class="p">],</span> <span class="nx">callback</span><span class="p">)</span>   <span class="c1">// all rows</span>
<span class="nx">db</span><span class="p">.</span><span class="nx">each</span><span class="p">(</span><span class="nx">sql</span><span class="p">,</span> <span class="p">[...</span><span class="nx">param</span><span class="p">],</span> <span class="nx">callback</span><span class="p">)</span>  <span class="c1">// iterate rows one by one</span>
<span class="nx">db</span><span class="p">.</span><span class="kd">get</span><span class="p">(</span><span class="nx">sql</span><span class="p">,</span> <span class="p">[...</span><span class="nx">param</span><span class="p">],</span> <span class="nx">callback</span><span class="p">)</span>   <span class="c1">// one row</span>
<span class="nx">db</span><span class="p">.</span><span class="nx">run</span><span class="p">(</span><span class="nx">sql</span><span class="p">,</span> <span class="p">[...</span><span class="nx">param</span><span class="p">],</span> <span class="nx">callback</span><span class="p">)</span>   <span class="c1">// queries other than SELECT</span>
</code></pre></div></div>

<p>前三个的作用差不多，区别只是 <code class="language-plaintext highlighter-rouge">each</code> 可以用来优化大数据量的遍历，<code class="language-plaintext highlighter-rouge">get</code> 则方便只获取一条数据（用 <code class="language-plaintext highlighter-rouge">all</code> 配合 <code class="language-plaintext highlighter-rouge">LIMIT</code> 其实效果差不多）。<code class="language-plaintext highlighter-rouge">all</code> 返回的数据结构类似：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span> <span class="p">{</span> <span class="nx">id</span><span class="p">,</span> <span class="nx">foo</span><span class="p">,</span> <span class="nx">bar</span><span class="p">,</span> <span class="p">...</span> <span class="p">},</span>
  <span class="p">{</span> <span class="nx">id</span><span class="p">,</span> <span class="nx">foo</span><span class="p">,</span> <span class="nx">bar</span> <span class="p">}</span> <span class="p">]</span>
</code></pre></div></div>

<p>这种结构在查询单表的时候非常方便，但是在遇到 JOIN 的时候，比如 Leoric 里常用的示例 <code class="language-plaintext highlighter-rouge">Post.include('comments')</code>，上面这种结构会导致 <code class="language-plaintext highlighter-rouge">posts</code> 表中的字段被 <code class="language-plaintext highlighter-rouge">comments</code> 表中的同名字段覆盖：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span> <span class="p">{</span> <span class="na">id</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span> <span class="nx">content</span><span class="p">,</span> <span class="nx">post_id</span><span class="p">,</span> <span class="p">...</span> <span class="p">}</span> <span class="p">]</span>
</code></pre></div></div>

<p>这里的 <code class="language-plaintext highlighter-rouge">id</code> 和 <code class="language-plaintext highlighter-rouge">content</code>（假设都有 content 字段）都会是 <code class="language-plaintext highlighter-rouge">comments</code> 表的，<code class="language-plaintext highlighter-rouge">posts</code> 的就取不到了。这个问题 sqlite3 里<a href="https://github.com/mapbox/node-sqlite3/issues/443">三年前就有人提</a>了，然而并没有得到重视。</p>

<p>解决的办法很简单，可以想 postgres 模块那样支持 <a href="https://node-postgres.com/features/queries#row-mode"><code class="language-plaintext highlighter-rouge">rowMode: 'array'</code></a>：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">client</span><span class="p">.</span><span class="nx">query</span><span class="p">({</span> <span class="na">text</span><span class="p">:</span> <span class="nx">sql</span><span class="p">,</span> <span class="na">rowMode</span><span class="p">:</span> <span class="dl">'</span><span class="s1">array</span><span class="dl">'</span> <span class="p">},</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="p">{</span> <span class="nx">rows</span><span class="p">,</span> <span class="nx">fields</span> <span class="p">})</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="c1">// rows =&gt; [ [ 1, 'foo', ... ],</span>
  <span class="c1">//           [ 2, 'bar', ... ] ]</span>
  <span class="c1">// fields =&gt; [ { name: 'id', table: 'foo' },</span>
  <span class="c1">//             { name: 'text', table: 'foo' } ]</span>
<span class="p">})</span>
</code></pre></div></div>

<p>也可以像 mysql 模块那样更进一步，支持 <code class="language-plaintext highlighter-rouge">nestTables</code>：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">client</span><span class="p">.</span><span class="nx">query</span><span class="p">({</span> <span class="nx">sql</span><span class="p">,</span> <span class="na">nestTables</span><span class="p">:</span> <span class="kc">true</span> <span class="p">},</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">rows</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="c1">// rows =&gt; [ { foo: { id: 1, text: 'foo' } },</span>
  <span class="c1">//           { foo: { id: 2, text: 'bar' } } ]</span>
<span class="p">})</span>
</code></pre></div></div>

<p>还有一个委曲求全的解决办法，就是用 identifier alias 把 <code class="language-plaintext highlighter-rouge">SELECT foo.*</code> 改成 <code class="language-plaintext highlighter-rouge">SELECT foo.id AS "foo:id"</code> 之类的，但开倒车不是维护 Leoric 的正确思路，我并没有考虑。</p>

<p>我最终选择的方式是<a href="https://github.com/mapbox/node-sqlite3/pull/932">给 sqlite3 提 pr</a>，增加 <code class="language-plaintext highlighter-rouge">.all({ sql, rowMode }, [...param], callback)</code> 这种调用形式，同时把底层接口改为默认带上 <code class="language-plaintext highlighter-rouge">fields</code>：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">db</span><span class="p">.</span><span class="nx">all</span><span class="p">({</span> <span class="nx">sql</span><span class="p">,</span> <span class="na">rowMode</span><span class="p">:</span> <span class="dl">'</span><span class="s1">array</span><span class="dl">'</span> <span class="p">},</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">rows</span><span class="p">,</span> <span class="nx">fields</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
  <span class="c1">// rows =&gt; [ [ 1, 'foo', ... ],</span>
  <span class="c1">//           [ 2, 'bar', ... ] ]</span>
  <span class="c1">// fields =&gt; [ { name: 'id', table: 'foo' },</span>
  <span class="c1">//             { name: 'text', table: 'foo' } ]</span>
<span class="p">})</span>

<span class="nx">db</span><span class="p">.</span><span class="nx">all</span><span class="p">({</span> <span class="nx">sql</span><span class="p">,</span> <span class="na">rowMode</span><span class="p">:</span> <span class="dl">'</span><span class="s1">nest</span><span class="dl">'</span> <span class="p">},</span> <span class="p">(</span><span class="nx">err</span><span class="p">,</span> <span class="nx">rows</span><span class="p">)</span> <span class="p">{</span>
  <span class="c1">// rows =&gt; [ { foo: { id: 1, text: 'foo' } },</span>
  <span class="c1">//           { foo: { id: 2, text: 'bar' } } ]</span>
<span class="p">})</span>
</code></pre></div></div>

<p>但从 sqlite3 的历史 pr 处理情况来看，我非常担心这个 pr 要到此为止。sqlite3 模块的维护者并不十分热衷响应社区的反馈。我觉得，如果实在等不到，又实在想给 Leoric 支持 SQLite，我还是直接 fork 一个比较好。</p>

<p>哦对了，我好像还一直没说 dblite，这个客户端倒是提供了一个比较通用的 <a href="https://github.com/WebReflection/dblite#bootstrap"><code class="language-plaintext highlighter-rouge">db.query()</code></a> 方法，不需要我再判断应该用 <code class="language-plaintext highlighter-rouge">db.run()</code> 还是 <code class="language-plaintext highlighter-rouge">db.all()</code>。但它其实只是一个 <code class="language-plaintext highlighter-rouge">sqlite3</code> 命令的浅包装，与其他客户端直接接口调用比起来，它还得 spawn 一个进程来完成查询，不推荐使用。</p>

<p>另一个有关 SQLite 比较有趣的差别是 column metadata 的获取方式。在 MySQL、PostgreSQL、<a href="https://docs.microsoft.com/en-us/sql/relational-databases/system-information-schema-views/columns-transact-sql">MSSQL</a> 这些数据库里，我们可以通过 <code class="language-plaintext highlighter-rouge">information_schema.columns</code> 表查询字段元数据，会返回包括字段名、字段类型、是否主键、能否为空等信息，是<a href="https://en.wikipedia.org/wiki/Information_schema">一项 ANSI 标准</a>。遗憾的是，SQLite 没有遵循。在 SQLite 要获取表结构信息，需要<a href="https://stackoverflow.com/questions/947215/how-to-get-a-list-of-column-names-on-sqlite3-iphone">使用 Pragma</a>：</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">PRAGMA</span> <span class="n">table_info</span><span class="p">(</span><span class="k">table_name</span><span class="p">);</span>
</code></pre></div></div>

<p>最后一个比较有趣的，SQLite <a href="https://sqlite.org/autoinc.html">不支持自定义的自增字段</a>，也就是说字段描述里不能使用 <code class="language-plaintext highlighter-rouge">AUTO_INCREMENT</code>。但在定义主键的时候，有个比较隐蔽的逻辑：</p>

<blockquote>
  <p>In SQLite, a column with type INTEGER PRIMARY KEY is an alias for the ROWID (except in WITHOUT ROWID tables) which is always a 64-bit signed integer.</p>
</blockquote>

<p>如果主键是 <code class="language-plaintext highlighter-rouge">INTEGER</code>，则主键将变成内建的 <code class="language-plaintext highlighter-rouge">ROWID</code> 的别名，而 <code class="language-plaintext highlighter-rouge">ROWID</code> 是表中唯一的自增字段。所以用 MySQL 时可以 <code class="language-plaintext highlighter-rouge">bigint(20) AUTO_INCREMENT PRIMARY KEY</code>，用 SQLite 时得改成 <code class="language-plaintext highlighter-rouge">INTEGER PRIAMRY KEY</code>。</p>

<p>而且定义主键时又可以用 <code class="language-plaintext highlighter-rouge">AUTOINCREMENT</code>（没有下划线）：</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">foo</span> <span class="p">(</span><span class="n">id</span> <span class="nb">INTEGER</span> <span class="k">PRIMARY</span> <span class="k">KEY</span> <span class="n">AUTOINCREMENT</span><span class="p">);</span>
</code></pre></div></div>

<p>这时主键的自增行为与不加 <code class="language-plaintext highlighter-rouge">AUTOINCREMENT</code> 时有些微差别，会确保新插入的 <code class="language-plaintext highlighter-rouge">id</code> 值是当前表中最大的。如果没有 <code class="language-plaintext highlighter-rouge">AUTOINCREMENT</code>，会在 <code class="language-plaintext highlighter-rouge">id</code> 值超过最大值时，尝试寻找表中还没有使用过的 <code class="language-plaintext highlighter-rouge">id</code> 值，也就是可能新插入的 <code class="language-plaintext highlighter-rouge">id</code> 值并非表中最大值。</p>

<p>真是一些无聊的区别……</p>

<p>总之，给 Leoric 增加 SQLite 支持所耗费的工作远超我的预计，希望我的 pr 能最终被合并吧。</p>]]></content><author><name>Chen Yangjian</name><uri>http://cyj.me</uri></author><category term="programming" /><summary type="html"><![CDATA[上周末花了两天时间给 Leoric 增加 SQLite 支持。SQLite 是个极轻量的数据库，在轻应用架构中非常流行，常见于浏览器、移动设备的操作系统、以及一些较为大型的客户端软件（比如 Skeype、iTunes、微信）。它本身非常简单，数据库实体可以是一个文件，也可以待在内存：]]></summary></entry><entry><title type="html">Programming Leoric I</title><link href="http://cyj.me/programming/2018/01/09/programming-leoric-i/" rel="alternate" type="text/html" title="Programming Leoric I" /><published>2018-01-09T00:00:00+00:00</published><updated>2018-01-09T00:00:00+00:00</updated><id>http://cyj.me/programming/2018/01/09/programming-leoric-i</id><content type="html" xml:base="http://cyj.me/programming/2018/01/09/programming-leoric-i/"><![CDATA[<p>2016 年初由于工作所需，我开发了一个比较粗浅的模块，用来映射 MySQL 表到 JavaScript 类，取名 xx-orm。两年后，Node.js 社区的 ORM 方案仍然是五花八门，在 npmjs.com 搜 orm 能让人挑花眼。我把 xx-orm 从应用代码中剥离出来，取名 <a href="https://github.com/dotnil/Leoric">Leoric</a>，为这场混战添一把柴火。</p>

<p>Leoric 最朴素的需求是为了做字段名 <code class="language-plaintext highlighter-rouge">column_name</code> 到属性名 <code class="language-plaintext highlighter-rouge">attributeName</code> 的映射。因为 DBA 通常使用 <code class="language-plaintext highlighter-rouge">snake_case</code> 来表达数据库名、表名、字段名，但是 JavaScript 默认的代码风格又是 <code class="language-plaintext highlighter-rouge">camelCase</code> 的：</p>

<table>
  <thead>
    <tr>
      <th>column name</th>
      <th>attribute name</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>foo</td>
      <td>foo</td>
    </tr>
    <tr>
      <td>foo_bar</td>
      <td>fooBar</td>
    </tr>
  </tbody>
</table>

<p>另一件事情是 Leoric 目前的一个 feature 或者 bug，无需 Model 属性定义以及 Migration。在开发 xx-orm 时，我们的表结构设计都是通过数据库服务所提供的设计工具进行。待设计完成后，只需继承 <code class="language-plaintext highlighter-rouge">Bone</code> 然后 <code class="language-plaintext highlighter-rouge">connect</code> 数据模型和数据库：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="p">{</span> <span class="nx">Bone</span><span class="p">,</span> <span class="nx">connect</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">Leoric</span><span class="dl">'</span><span class="p">)</span>
<span class="kd">class</span> <span class="nx">User</span> <span class="kd">extends</span> <span class="nx">Bone</span> <span class="p">{}</span>
<span class="nx">connect</span><span class="p">({</span> <span class="na">client</span><span class="p">:</span> <span class="dl">'</span><span class="s1">mysql</span><span class="dl">'</span><span class="p">,</span> <span class="na">models</span><span class="p">:</span> <span class="p">[</span><span class="nx">User</span><span class="p">]</span> <span class="p">})</span>
</code></pre></div></div>

<p>这样 <code class="language-plaintext highlighter-rouge">User</code> 就可以用了，所有 <code class="language-plaintext highlighter-rouge">users</code> 表中的字段信息都会被自动导入：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">User</span><span class="p">.</span><span class="nx">columns</span>
<span class="c1">// =&gt; ['id', 'name', 'age', 'created_at', ... ]</span>
<span class="nx">User</span><span class="p">.</span><span class="nx">attribtes</span>
<span class="c1">// =&gt; ['id', 'name', 'age', 'createdAt', ... ]</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">User</code> 的使用者只需要关心映射后的属性名 <code class="language-plaintext highlighter-rouge">attribute name</code>。可以阅读《<a href="http://cyj.me/Leoric/zh/basics#%E5%91%BD%E5%90%8D%E7%BA%A6%E5%AE%9A">Leoric 基础</a>》一文了解更多相关内容。</p>

<p>在这个朴素需求之上，Leoric 的绝大多数特性都是借鉴 Active Record 的，比如查询、关联关系的 API 设计。但对于熟悉 Node.js 但不了解 Ruby on Rails 的程序员来说，前面这句不会给人直观印象，所以下文将 Leoric 与 Node.js 流行的 ORM 库做个比较。</p>

<p>目前社区中最成熟的方案大致是 Sequelize、Bookshelf（大多数会直接用它底层的 Knex） 、还有 sails 框架所包含的 Waterline。Waterline 是一个志在兼收并蓄的模块，不仅能够映射关系型数据库，还可以把底层存储换成文件系统、Redis 等等。功能太过强大，惹不起惹不起，这里就不深入讨论了。</p>

<p>Leoric 的主要比较对象是 Sequelize、Knex。从查询说起，假设我们需要查询 <code class="language-plaintext highlighter-rouge">WHERE (foo IS NULL OR foo = 1) AND deleted_at IS NULL</code>，在 Sequelize 里：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">Table</span><span class="p">.</span><span class="nx">findAll</span><span class="p">({</span>
  <span class="na">where</span><span class="p">:</span> <span class="p">{</span>
    <span class="p">[</span><span class="nx">Op</span><span class="p">.</span><span class="nx">and</span><span class="p">]:</span> <span class="p">[</span>
      <span class="p">{</span> <span class="p">[</span><span class="nx">Op</span><span class="p">.</span><span class="nx">or</span><span class="p">]:</span> <span class="p">[</span>
        <span class="p">{</span> <span class="na">foo</span><span class="p">:</span> <span class="kc">null</span> <span class="p">},</span>
        <span class="p">{</span> <span class="na">foo</span><span class="p">:</span> <span class="mi">1</span> <span class="p">}</span> <span class="p">]</span> <span class="p">},</span>
      <span class="p">{</span> <span class="na">deletedAt</span><span class="p">:</span> <span class="kc">null</span> <span class="p">}</span>
    <span class="p">]</span>
  <span class="p">}</span>
<span class="p">})</span>
</code></pre></div></div>

<p>在 Knex 里：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">Table</span><span class="p">.</span><span class="nx">where</span><span class="p">(</span><span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
  <span class="k">this</span><span class="p">.</span><span class="nx">where</span><span class="p">({</span> <span class="na">foo</span><span class="p">:</span> <span class="kc">null</span> <span class="p">}).</span><span class="nx">orWhere</span><span class="p">({</span> <span class="na">foo</span><span class="p">:</span> <span class="nx">values</span> <span class="p">})</span>
<span class="p">}).</span><span class="nx">andWhere</span><span class="p">({</span> <span class="na">deletedAt</span><span class="p">:</span> <span class="kc">null</span> <span class="p">})</span>
</code></pre></div></div>

<p>用 Leoric，则是：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">Table</span><span class="p">.</span><span class="nx">find</span><span class="p">(</span><span class="dl">'</span><span class="s1">(foo = null or foo = ?) and deletedAt is null</span><span class="dl">'</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
</code></pre></div></div>

<p>其实 Sequelize 这种查询方式 Leoric 也支持，但作为一个曾经的 Ruby on Rails 小粉丝，我还是认为 SQL-like 的表达方式是最合适的。注意这里传入的字符串并不会直接被放到 <code class="language-plaintext highlighter-rouge">WHERE</code>，而是会被解析、过滤，最后再拼到 SQL 中去。</p>

<p>如果认为 placeholder 形式不够直观，也可以用 <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals">tagged template literal</a>：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">Table</span><span class="p">.</span><span class="nx">find</span><span class="s2">`(foo = null or foo = </span><span class="p">${</span><span class="nx">foo</span><span class="p">}</span><span class="s2">) and deletedAt is null`</span>
</code></pre></div></div>

<p>可惜这个我所认为的优势并<a href="https://cnodejs.org/topic/5a48f2a7f320ae9f0dd581f8">不被 cnodejs.org 所认同</a>。如果你仍然对 Leoric 的查询 API 感兴趣，不妨阅读《<a href="http://cyj.me/Leoric/zh/querying">Leoric 查询接口</a>》一文。</p>

<p>另一个比较方便的是关联关系的处理。使用 Leoric，我们可以在 Model 中声明<a href="http://cyj.me/Leoric/zh/associations">多种映射关系</a>：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="p">{</span> <span class="nx">Bone</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">requier</span><span class="p">(</span><span class="dl">'</span><span class="s1">Leoric</span><span class="dl">'</span><span class="p">)</span>
<span class="kd">class</span> <span class="nx">Comment</span> <span class="kd">extends</span> <span class="nx">Bone</span> <span class="p">{}</span>
<span class="kd">class</span> <span class="nx">User</span> <span class="kd">extends</span> <span class="nx">Bone</span> <span class="p">{}</span>
<span class="kd">class</span> <span class="nx">Post</span> <span class="kd">extends</span> <span class="nx">Bone</span> <span class="p">{</span>
  <span class="kd">static</span> <span class="nx">describe</span><span class="p">()</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">hasMany</span><span class="p">(</span><span class="dl">'</span><span class="s1">comments</span><span class="dl">'</span><span class="p">)</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">belongsTo</span><span class="p">(</span><span class="dl">'</span><span class="s1">author</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="na">className</span><span class="p">:</span> <span class="dl">'</span><span class="s1">User</span><span class="dl">'</span><span class="p">,</span> <span class="na">foreignKey</span><span class="p">:</span> <span class="dl">'</span><span class="s1">authorId</span><span class="dl">'</span> <span class="p">})</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">hasMany</span><span class="p">(</span><span class="dl">'</span><span class="s1">tagMaps</span><span class="dl">'</span><span class="p">)</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">hasMany</span><span class="p">(</span><span class="dl">'</span><span class="s1">tags</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="na">through</span><span class="p">:</span> <span class="dl">'</span><span class="s1">tagMaps</span><span class="dl">'</span> <span class="p">})</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>查询的时候就可以一次取出：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">Post</span><span class="p">.</span><span class="nx">include</span><span class="p">(</span><span class="dl">'</span><span class="s1">comments</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">author</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">tags</span><span class="dl">'</span><span class="p">).</span><span class="nx">where</span><span class="p">(</span><span class="dl">'</span><span class="s1">posts.id = ?</span><span class="dl">'</span><span class="p">,</span> <span class="p">[</span><span class="mi">8</span><span class="p">,</span> <span class="mi">24</span><span class="p">])</span>
</code></pre></div></div>

<p>上面这种关联关系，使用 Sequelize 表示，可能是这样的：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">Post</span> <span class="o">=</span> <span class="nx">sequelize</span><span class="p">.</span><span class="nx">define</span><span class="p">(</span><span class="dl">'</span><span class="s1">post</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="p">...</span> <span class="p">})</span>
<span class="kd">const</span> <span class="nx">Comment</span> <span class="o">=</span> <span class="nx">sequelize</span><span class="p">.</span><span class="nx">define</span><span class="p">(</span><span class="dl">'</span><span class="s1">comment</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="p">...</span> <span class="p">})</span>
<span class="kd">const</span> <span class="nx">User</span> <span class="o">=</span> <span class="nx">sequelize</span><span class="p">.</span><span class="nx">define</span><span class="p">(</span><span class="dl">'</span><span class="s1">user</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="p">...</span> <span class="p">})</span>
<span class="nx">Post</span><span class="p">.</span><span class="nx">belongsTo</span><span class="p">(</span><span class="nx">User</span><span class="p">,</span> <span class="p">{</span> <span class="na">as</span><span class="p">:</span> <span class="dl">'</span><span class="s1">author</span><span class="dl">'</span><span class="p">,</span> <span class="na">foreignKey</span><span class="p">:</span> <span class="dl">'</span><span class="s1">authorId</span><span class="dl">'</span> <span class="p">})</span>
<span class="nx">Post</span><span class="p">.</span><span class="nx">hasMany</span><span class="p">(</span><span class="nx">Comment</span><span class="p">)</span>
<span class="nx">Post</span><span class="p">.</span><span class="nx">hasMany</span><span class="p">(</span><span class="nx">TagMap</span><span class="p">)</span>
<span class="nx">Post</span><span class="p">.</span><span class="nx">hasMany</span><span class="p">(</span><span class="nx">Tag</span><span class="p">,</span> <span class="p">{</span> <span class="na">through</span><span class="p">:</span> <span class="dl">'</span><span class="s1">TagMap</span><span class="dl">'</span> <span class="p">})</span>
</code></pre></div></div>

<p>使用 Sequelize 查询的时候：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">Post</span><span class="p">.</span><span class="nx">findAll</span><span class="p">({</span>
  <span class="na">include</span><span class="p">:</span> <span class="p">[</span>
    <span class="p">{</span> <span class="na">model</span><span class="p">:</span> <span class="nx">Comment</span> <span class="p">},</span>
    <span class="p">{</span> <span class="na">model</span><span class="p">:</span> <span class="nx">User</span> <span class="p">},</span>
    <span class="p">{</span> <span class="na">model</span><span class="p">:</span> <span class="nx">Tag</span><span class="p">,</span>
      <span class="na">through</span><span class="p">:</span> <span class="p">{</span> <span class="cm">/* ? */</span> <span class="p">}</span> <span class="p">}</span>
  <span class="p">],</span>
  <span class="na">where</span><span class="p">:</span> <span class="p">{</span>
    <span class="na">id</span><span class="p">:</span> <span class="p">{</span> <span class="na">$in</span><span class="p">:</span> <span class="p">[</span><span class="mi">8</span><span class="p">,</span> <span class="mi">24</span><span class="p">]</span> <span class="p">}</span>
  <span class="p">}</span>
<span class="p">})</span>
</code></pre></div></div>

<p>一些基于 Knex 的 ORM 也支持关联关系，但使用的语法比 Sequelize 还要繁琐，这里就不深入讨论了。在关联关系定义上，Leoric 和 Sequelize 相差不多，Leoric 的 API 更现代化一些。而在查询的时候，Leoric 则要简洁许多，毕竟 <strong>Leoric 懂得你的查询表达式</strong>。</p>]]></content><author><name>Chen Yangjian</name><uri>http://cyj.me</uri></author><category term="programming" /><summary type="html"><![CDATA[2016 年初由于工作所需，我开发了一个比较粗浅的模块，用来映射 MySQL 表到 JavaScript 类，取名 xx-orm。两年后，Node.js 社区的 ORM 方案仍然是五花八门，在 npmjs.com 搜 orm 能让人挑花眼。我把 xx-orm 从应用代码中剥离出来，取名 Leoric，为这场混战添一把柴火。]]></summary></entry><entry><title type="html">Yen</title><link href="http://cyj.me/programming/2015/05/25/yen/" rel="alternate" type="text/html" title="Yen" /><published>2015-05-25T00:00:00+00:00</published><updated>2015-05-25T00:00:00+00:00</updated><id>http://cyj.me/programming/2015/05/25/yen</id><content type="html" xml:base="http://cyj.me/programming/2015/05/25/yen/"><![CDATA[<p>二〇一三年我翻译了一本我见过 O’Reilly 出版的最水的书，名叫《DOM Enlightenment》。
我将它译作《<a href="http://book.douban.com/subject/25882606/">DOM 启蒙</a>》，中间拖延症几经反复，终于在二〇一四年初交稿并付梓。</p>

<p>除最后一章讲述的如何编写 jQuery 风格的 DOM 库，以及倒数第二章讲述的 DOM 事件外，本书内容
都是比较粗浅的 DOM API 介绍，文字说明和代码演示都大同小异，在翻译的过程中我一度认为作者是
写了个程序批量生成这些章节的。为了不使翻译过程太过机械化，个别地方我还顺手翻译了示例代码里的
注释，努力做一个尽责的翻译。</p>

<p>不过本文无关这一翻译过程，本文想说的是一个轮子，一个受《DOM 启蒙》最后一章启发，API 完全抄袭
jQuery 的 DOM 操作库。</p>

<h2 id="缘起">缘起</h2>

<p>受工作内容所限，在我的日常工作中，通常是没办法直接引用成熟的前端库的，不然整个页面的首次加载
尺寸肯定会超出技术规格的要求。在之前，我们都是直接裸写 DOM 操作、事件绑定等代码。如果我们是
在开发无线 Web 应用，那这么做其实是对的，iOS 和 Android 对基本的 DOM API 标准支持都很好，
<code class="language-plaintext highlighter-rouge">document.querySelector</code>、<code class="language-plaintext highlighter-rouge">.firstElementChild</code>、<code class="language-plaintext highlighter-rouge">.addEventListener</code> 等全都可以用。</p>

<p>但在我们还需要支持 IE 的历史版本，尽情使用原生 DOM API 对我们来说还是太奢侈了。比如我们经常
得写这样的代码来获取当前元素的相邻元素节点：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/*
 * 还得事先加上如下修补代码：
 *
 *     window.Node = window.Node || { ELEMENT_NODE: 1 }
 */</span>
<span class="kd">function</span> <span class="nx">nextElementSibling</span><span class="p">(</span><span class="nx">el</span><span class="p">)</span> <span class="p">{</span>
  <span class="kd">var</span> <span class="nx">sibling</span>

  <span class="k">while</span> <span class="p">(</span><span class="nx">sibling</span> <span class="o">=</span> <span class="nx">el</span><span class="p">.</span><span class="nx">nextSibling</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="nx">sibling</span><span class="p">.</span><span class="nx">nodeType</span> <span class="o">==</span> <span class="nx">Node</span><span class="p">.</span><span class="nx">ELEMENT_NODE</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">return</span> <span class="nx">sibling</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>这种时候就很怀念 jQuery 或者其他 DOM 操作库了：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">$</span><span class="p">(</span><span class="nx">el</span><span class="p">).</span><span class="nx">next</span><span class="p">()</span>
</code></pre></div></div>

<h2 id="炮制">炮制</h2>

<p>假设你有个函数，名字叫做 <code class="language-plaintext highlighter-rouge">jSelect</code> 什么的，它能接收多种参数：</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">new jSelect('.foo &gt; p')</code>：CSS 选择器</li>
  <li><code class="language-plaintext highlighter-rouge">new jSelect(document.body)</code>：DOM 元素</li>
  <li><code class="language-plaintext highlighter-rouge">new jSelect(document.scripts)</code>：DOM 元素集合</li>
  <li><code class="language-plaintext highlighter-rouge">new jSelect({})</code>：原始数据类型（Object、Array 等）</li>
</ol>

<p>并能按传入参数类型的不同，分别作如下处理：</p>

<ol>
  <li>根据当前上下文查找 CSS 选择器，将返回的 DOM 元素集合按第三点处理</li>
  <li>将 DOM 元素作为函数实例的第 <code class="language-plaintext highlighter-rouge">0</code> 个属性</li>
  <li>将 DOM 元素集合展开，放到第 <code class="language-plaintext highlighter-rouge">0</code>、<code class="language-plaintext highlighter-rouge">1</code>、<code class="language-plaintext highlighter-rouge">2</code>…… 等属性</li>
  <li>如果是 Array，则像传入 DOM 元素集合一般展开；如果是 Object，则作为第 <code class="language-plaintext highlighter-rouge">0</code> 个属性</li>
</ol>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">jSelect</span><span class="p">(</span><span class="nx">selector</span><span class="p">,</span> <span class="nx">context</span><span class="p">)</span> <span class="p">{</span>
  <span class="nx">context</span> <span class="o">=</span> <span class="nx">context</span> <span class="o">||</span> <span class="nb">document</span>

  <span class="c1">// case 1</span>
  <span class="k">if</span> <span class="p">(</span><span class="k">typeof</span> <span class="nx">selector</span> <span class="o">==</span> <span class="dl">'</span><span class="s1">string</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">return</span> <span class="nx">jSelect</span><span class="p">(</span><span class="nx">context</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="nx">selector</span><span class="p">))</span>
  <span class="p">}</span>
  <span class="c1">// case 2</span>
  <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="nx">selector</span><span class="p">.</span><span class="nx">nodeType</span> <span class="o">==</span> <span class="nx">Node</span><span class="p">.</span><span class="nx">ELEMENT_NODE</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="nx">selector</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">length</span> <span class="o">=</span> <span class="mi">1</span>
  <span class="p">}</span>
  <span class="c1">// case 3 and case 4 - Array</span>
  <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="dl">'</span><span class="s1">length</span><span class="dl">'</span> <span class="k">in</span> <span class="nx">selector</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">for</span> <span class="p">(</span><span class="kd">var</span> <span class="nx">i</span> <span class="o">=</span> <span class="mi">0</span><span class="p">,</span> <span class="nx">len</span> <span class="o">=</span> <span class="nx">selector</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span> <span class="nx">i</span> <span class="o">&lt;</span> <span class="nx">len</span><span class="p">;</span> <span class="nx">i</span><span class="o">++</span><span class="p">)</span> <span class="p">{</span>
      <span class="k">this</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span> <span class="o">=</span> <span class="nx">selector</span><span class="p">[</span><span class="nx">i</span><span class="p">]</span>
    <span class="p">}</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">length</span> <span class="o">=</span> <span class="nx">selector</span><span class="p">.</span><span class="nx">length</span>
  <span class="p">}</span>
  <span class="c1">// case 4 - Object</span>
  <span class="k">else</span> <span class="p">{</span>
    <span class="k">this</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">=</span> <span class="nx">selector</span>
    <span class="k">this</span><span class="p">.</span><span class="nx">length</span> <span class="o">=</span> <span class="mi">1</span>
  <span class="p">}</span>

  <span class="k">this</span><span class="p">.</span><span class="nx">context</span> <span class="o">=</span> <span class="nx">context</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="方法">方法</h3>

<p>同时，假设 <code class="language-plaintext highlighter-rouge">jSelect.prototype</code> 上有如下方法：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Traversing</span>
<span class="nx">jSelect</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">next</span><span class="p">()</span>
<span class="nx">jSelect</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">prev</span><span class="p">()</span>
<span class="nx">jSelect</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">children</span><span class="p">()</span>
<span class="nx">jSelect</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">first</span><span class="p">()</span>
<span class="nx">jSelect</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">last</span><span class="p">()</span>

<span class="c1">// Query</span>
<span class="nx">jSelect</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">find</span><span class="p">()</span>

<span class="c1">// Manipulation</span>
<span class="nx">jSelect</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">remove</span><span class="p">()</span>
<span class="nx">jSelect</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">html</span><span class="p">()</span>

<span class="c1">// ... and so on.</span>
</code></pre></div></div>

<p>那现在你就可以愉快地查询、遍历、操作 DOM 了：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">new</span> <span class="nx">jSelect</span><span class="p">(</span><span class="dl">'</span><span class="s1">.foo &gt; p:first-child</span><span class="dl">'</span><span class="p">).</span><span class="nx">next</span><span class="p">().</span><span class="nx">remove</span><span class="p">()</span>
</code></pre></div></div>

<h3 id="instanceof">instanceof</h3>

<p>而且这个函数还能省略掉 <code class="language-plaintext highlighter-rouge">new</code>，不管加不加 <code class="language-plaintext highlighter-rouge">new</code> 都会实例化：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">jSelect</span><span class="p">(</span><span class="nx">selector</span><span class="p">,</span> <span class="nx">context</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="p">(</span><span class="k">this</span> <span class="k">instanceof</span> <span class="nx">jSelect</span><span class="p">))</span> <span class="p">{</span>
    <span class="k">return</span> <span class="k">new</span> <span class="nx">jSelect</span><span class="p">(</span><span class="nx">selector</span><span class="p">,</span> <span class="nx">context</span><span class="p">)</span>
  <span class="p">}</span>

  <span class="c1">// implementation code mentioned above</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="fn">fn</h3>

<p>不仅如此，你还能直接通过 <code class="language-plaintext highlighter-rouge">jSelect.fn</code> 注册方法，因为：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">jSelect</span><span class="p">.</span><span class="nx">fn</span> <span class="o">=</span> <span class="nx">jSelect</span><span class="p">.</span><span class="nx">prototype</span>
</code></pre></div></div>

<p>于是你可以：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">jSelect</span><span class="p">.</span><span class="nx">fn</span><span class="p">.</span><span class="nx">highlight</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
  <span class="k">return</span> <span class="k">this</span><span class="p">.</span><span class="nx">each</span><span class="p">(</span><span class="kd">function</span><span class="p">(</span><span class="nx">el</span><span class="p">)</span> <span class="p">{</span>
    <span class="nx">$</span><span class="p">(</span><span class="nx">el</span><span class="p">).</span><span class="nx">css</span><span class="p">({</span> <span class="na">backgroundColor</span><span class="p">:</span> <span class="dl">'</span><span class="s1">yellow</span><span class="dl">'</span> <span class="p">})</span>
  <span class="p">})</span>
<span class="p">}</span>
</code></pre></div></div>

<h3 id="array-like-object">Array-like Object</h3>

<p>像 jSelect 构造函数返回的实例这种对象（<code class="language-plaintext highlighter-rouge">{ '0': ..., '1': ..., '2': ..., length: 3 }</code>）
我们管它叫 Array-like Object，在《DOM 启蒙》里我将它译作“类数组对象”。</p>

<p>有一个优化这种对象在 Chrome 终端里的输出格式的<a href="http://stackoverflow.com/questions/6599071/array-like-objects-in-javascript">小窍门</a>，就是再给这个对象实现一个
<code class="language-plaintext highlighter-rouge">.splice</code> 方法。当终端发现当前对象既有 <code class="language-plaintext highlighter-rouge">.length</code> 又有 <code class="language-plaintext highlighter-rouge">.splice</code>，就会认为它是个数组，
输出格式就会变成数组：</p>

<p><img src="http://img.alicdn.com/tfscom/TB1JSb0HVXXXXXqXXXXSutbFXXX.jpg" alt="" /></p>

<p>所以我们还需要实现 <code class="language-plaintext highlighter-rouge">jSelect.prototype.splice</code>，偷懒直接用数组的吧：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">jSelect</span><span class="p">.</span><span class="nx">fn</span><span class="p">.</span><span class="nx">splice</span> <span class="o">=</span> <span class="nb">Array</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="nx">splice</span>
</code></pre></div></div>

<p>到这里，你就已经实现了一个高仿 jQuery 的 DOM 操作库。其实，jQuery 的作者在发布 jQuery
之前，原本就想把它叫做 jSelect 的，可惜后者域名被人注册了，只好改名 jQuery。</p>

<h2 id="界线">界线</h2>

<p>延续这份 jSelect，假如我们又实现了 <code class="language-plaintext highlighter-rouge">jSelect.fn.animate</code>、<code class="language-plaintext highlighter-rouge">jSelect.fn.on</code>、
<code class="language-plaintext highlighter-rouge">jSelect.fn.off</code>、<code class="language-plaintext highlighter-rouge">jSelect.fn.ajax</code>，岂不是彻底山寨了 jQuery？</p>

<p>浏览器兼容性又该怎么办呢？有些<a href="https://github.com/erzu/yen">修复起来颇麻烦、代价颇昂贵（代码量飙升）的坑</a>，要不要处理？</p>

<p>这个时候你就需要厘清自己的思路，你是需要一个完完整整带有自己个人风格的 jQuery，还是仅仅为了
解决实际业务中某类特定问题，又很喜欢 jQuery 的 API 风格，所以需要一个 jQuery API 子集？</p>

<p>作为一个有职业操守的前端工程师，造轮子的时候一定要时刻提醒自己这些问题……</p>

<h3 id="jquery-的不足">jQuery 的不足</h3>

<p>在迷你模块盛行的今天（君不见 NPM 里到处都是代码量一百行不到的模块），大而全的前端库愈发不受
欢迎了。有的人不喜欢 <code class="language-plaintext highlighter-rouge">jQuery.fn.ajax</code>，所以有了 <a href="http://visionmedia.github.io/superagent/">superagent</a>；有的人指出 <a href="https://github.com/kriskowal/q/wiki/Coming-from-jQuery">jQuery
里的 Promise 实现不符合规范</a>，无法和 Q、bluebird 等 Promise 实现愉快地互通。</p>

<p>不过，我等受 jQuery 启发良多，现在吃饱了骂厨子，指摘 jQuery 的不是，其实是挺没必要的。
毕竟这是一款历史悠久的前端库了，有些包袱是不得不背的。</p>

<p>推荐阅读同事墨智老师的书《<a href="http://book.douban.com/annotation/33856482/">jQuery 技术内幕</a>》。</p>

<h3 id="yen">yen</h3>

<p>从前文中的 jSelect 拓展开，加入事件绑定与触发，就成了我们组最近开源的模块 yen。jQuery
的简写是个美元符 <code class="language-plaintext highlighter-rouge">$</code>，人民币的符号 ￥，<a href="http://www.zhihu.com/question/20529506">和日元的符号相同</a>，所以我们就把这个小小山寨品
叫做 <code class="language-plaintext highlighter-rouge">yen</code>。</p>

<p>然而用的时候仍然是：</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">$</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">yen</span><span class="dl">'</span><span class="p">)</span>
</code></pre></div></div>

<p>哎呀真是充满了各种怪诞。</p>]]></content><author><name>Chen Yangjian</name><uri>http://cyj.me</uri></author><category term="programming" /><summary type="html"><![CDATA[二〇一三年我翻译了一本我见过 O’Reilly 出版的最水的书，名叫《DOM Enlightenment》。 我将它译作《DOM 启蒙》，中间拖延症几经反复，终于在二〇一四年初交稿并付梓。]]></summary></entry><entry><title type="html">变幻中的 2014 I</title><link href="http://cyj.me/life/2014/12/31/transitions-of-2014-i/" rel="alternate" type="text/html" title="变幻中的 2014 I" /><published>2014-12-31T00:00:00+00:00</published><updated>2014-12-31T00:00:00+00:00</updated><id>http://cyj.me/life/2014/12/31/transitions-of-2014-i</id><content type="html" xml:base="http://cyj.me/life/2014/12/31/transitions-of-2014-i/"><![CDATA[<p>2014年已成过去，在此整理过去这一年拍的一些照片，聊作记录。</p>

<p><img src="/assets/img/2014-recap-i/2014-01-10.jpg" alt="" /></p>

<p>2014年从团建开始，杭州、北京一起去安吉泡温泉。每年团建都会有高大上和矮矬穷线路配合，每年大家
也都会自动选择矮矬穷的那一条，今年冬季便是1月10日去安吉一处山上。</p>

<p>泡温泉其实来来去去都差不多，这次比较大的收获是木头教我游泳，我终于能在泳池里浮起来了。</p>

<p><img src="/assets/img/2014-recap-i/2014-01-11.jpg" alt="" /></p>

<p>次日，去安吉附近一处地方，叫做百草园。前一晚我们住在安吉城区，还去打台球吃，都是私照，不便发。
当天阴雨，百草园一片灰蒙蒙，我们要在这团湿冷里头烧烤，感觉独特。</p>

<p>当时百草园应该仍在建设维护中，飞禽走兽的部分有好些都还在施工。我在烧烤间隙出来转悠，沿途看到
野猪、浣熊，然后就在这个山头，看到这只巨大的棕熊。它趴在一座假山上，睥睨天下，旁边的围墙和
假山差不多高，离得特别近，感觉它轻松一跃就能翻墙出来。</p>

<p>看它没有要出来的意思，我就拍了这张照片。</p>

<p><img src="/assets/img/2014-recap-i/2014-01-23.jpg" alt="" /></p>

<p>邻近年末，1月23日这天是个好天气，黄昏时候去礼信年年食堂楼上的平台看落日，看到两位工友在探讨
革命友谊，画面太美我果断拍下。</p>

<p><img src="/assets/img/2014-recap-i/2014-01-24.jpg" alt="" /></p>

<p>没想到次日仍然好天气，幸好早有准备，带了三脚架到昨天发现的五号楼天台上拍落日，以及一段
延时摄影。拍得有点久，中途还抽空给娘亲打了个电话。一直到天色变暗，月明星稀，才手工回家。</p>

<p><img src="/assets/img/2014-recap-i/2014-02-07.jpg" alt="" /></p>

<p>在家过年，正月里，阳历2月7日这天，看到门口有馄饨车经过。卖馄饨的是位老娘客。冬天湿冷，吃一碗
热气腾腾的馄饨比较暖身。单说营养，或者美味，我认为不如后来在温岭吃的芡糕。老家卖的这些馄饨，
包的肉馅还没有盖在上头的肉末多，放调料的时候味精一大勺，餐盒也很可疑。</p>

<p>可当深夜肚饿，听着外头梆梆梆敲过，估计我仍要跪舔。</p>

<p><img src="/assets/img/2014-recap-i/2014-02-08.jpg" alt="" /></p>

<p>次日闲着无事，开车去以前上学时经常路过的地方看看。这地方叫天长，天长地久的天长，盛产牛肉。
冬天枯水期，溪水就剩一点细流，这不知名的树叶子掉光，树枝仍在这阴沉沉的天空挥斥方遒。</p>

<p><img src="/assets/img/2014-recap-i/2014-02-09.jpg" alt="" /></p>

<p>2月9日，踏上往杭州的路，到金华境内的时候，下起鹅毛大雪。再往前一些，因为限流大堵车，只好下车
拍几张照片。堵过那一段，往前的路积雪严重，硬路肩上将近一拳厚的雪。所幸车子还算稳，没有打滑。</p>

<p><img src="/assets/img/2014-recap-i/2014-02-10.jpg" alt="" /></p>

<p>当晚雪仍在下，次日起来，学院路就成了这幅样子。</p>

<p><img src="/assets/img/2014-recap-i/2014-02-15.jpg" alt="" /></p>

<p>到杭州的第一个周末，2月15日，和老婆去爬玉皇山。因为很早就去过灵隐、三天竺，还经常去法喜寺吃
斋饭，知道杭州在东晋时就有寺庙，佛教颇为兴盛。然而这次去玉皇山才知道，道教在这也历史悠久。
去得晚，爬到山顶太阳就快下山了。</p>

<p>哦对，去玉皇山其实还有个原因。要去南山路边一处地方，办公园卡。2015年来了，建议大家也去办。</p>

<p><img src="/assets/img/2014-recap-i/2014-02-23.jpg" alt="" /></p>

<p>2月23日，应该又是周末，和老婆去西溪湿地看梅花。应该是我执意要去的，因为之前来的时候是夏季，
没看到梅花。这次算是得偿所愿，看到一片非常大的梅林，梅花开得很旺。</p>

<p>我记得杭州花圃和植物园也有类似的梅林，这次或许可以去看看。</p>

<p><img src="/assets/img/2014-recap-i/2014-03-07.jpg" alt="" /></p>

<p>3月7日部门前端技术分享，气氛热烈。站在2014年的尾巴上回头看看，有几位同事在这一年里离开我们，
也有一些新同事加入进来。看着这些照片，感慨良多。</p>

<p><img src="/assets/img/2014-recap-i/2014-03-07-2.jpg" alt="" /></p>

<p>当天下午，是整个部门的年会，主题叫即刻，也是 geek 的谐音。我参加表演了一个节目，跟故非、
宫晴，还有崇志一起演个相声剧。后来看视频，发现自己在说的时候一直晃，毫无台风，果然不是吃
这口饭的料。</p>

<p>还给大家介绍延时摄影，前期准备，拍摄技巧，后期处理，以及自己拍的作品展示，刚好把1月份拍的东西
拿出来秀了一下。</p>

<p>上午的前端技术分享，凭借小凌、丝姐、和邓老板的努力，我的小组忝列第一，拿走了￥800的腐败券。</p>

<p><img src="/assets/img/2014-recap-i/2014-03-15.jpg" alt="" /></p>

<p>3月15日，和老婆一起去九溪，到山脚之后走了岔路，往龙井去了。似乎中途还吵了一架，完全想不起来
是为什么了。也可能不是这次，唉，年代久远，已不可考。</p>

<p><img src="/assets/img/2014-recap-i/2014-04-05.jpg" alt="" /></p>

<p>4月5日，清明小长假，老婆的奶奶来杭州找她玩，我陪她们一起去西溪湿地。春天已经彻底来了，到处
都是新绿，走在绿堤上心旷神怡。沿路还有花儿各种，游人交织，甚至还有现场表演的黄梅戏。</p>

<p><img src="/assets/img/2014-recap-i/2014-04-19.jpg" alt="" /></p>

<p>4月19日，在瓜瓜家聚餐喝酒。应该是春节过后，瓜瓜就一直念叨这个了，说我家有好多酒，你们来帮我
把它喝掉吧。于是在这个周末，雪卒带了一些牛肉熟食，紫云飞带了一些花生辅食，我又买了一点啤酒当
伴手礼，大家齐聚瓜瓜家，在他们家客厅喝了一晚上。</p>

<p>好吧，其实我没喝。我当时鼻炎、咽炎一同发作，情况十分严重，贪生怕死得很。如今回想，正如雪卒在
散场的时候所说，这一天我没喝，可惜了。</p>

<p>还在那天晚上散场的时候，瓜瓜抱着我说接下来会有一些改变，我还不解其意。结果没出几个月，他就
离开我们，创业去了。后知后觉的我呀。</p>

<p><img src="/assets/img/2014-recap-i/2014-05-29.jpg" alt="" /></p>

<p>项目忙得昏天黑地，做 Node.js 做得不亦乐乎，一转眼到了5月29日，邻近端午节。我按照先前的计划，
回家过节。</p>

<p>每次我贪生怕死的时候，我都会回家看看。大四寒假的时候如此，这次端午节也是如此。2014年是我在
杭州的第十年，反而愈发觉得自己是不是跟这地方有些水土不服了。</p>

<p>这天下班，夜色很诱人，提醒着在这上班的匆忙的人们，天光正好，及时行乐。</p>

<p><img src="/assets/img/2014-recap-i/2014-05-30.jpg" alt="" /></p>

<p>次日起了个大早，搭车去杭州东站，坐高铁回家。自然是因为正月里那次在高速上面堵怕了。</p>

<p><img src="/assets/img/2014-recap-i/2014-06-24.jpg" alt="" /></p>

<p>回来之后仍然忙碌，6月24日晚上，和老婆一块去白堤转悠。这天空气很通透，非常适合拍夜景。夜幕下的
北山路、宝石山、和保俶塔，提醒着你生活很美好，别忙忙碌碌着就错过了。</p>

<p><img src="/assets/img/2014-recap-i/2014-06-26.jpg" alt="" /></p>

<p>夏季团队建设，和去年去海南遇到台风，去南麂遇到海面大风被迫提前结束游玩一样，这次去千岛湖也
遇上十分不美的天气，一路下着雨。</p>

<p>在千岛湖住的酒店倒是非常不错，有泳池。因为1月份在安吉的时候知道怎么浮起来了，我学游泳的热情
高涨。团队在千岛湖待了两天，我就在泳池泡了两天。终于能够游出去了。</p>

<p>期间还在酒店看了一部电影《铁线虫入侵》，结果字幕是生编乱造的，后来看了优酷的版本才恍然大悟。</p>

<p>原谅这张照片质量不好，是 iPhone 4 拍的。我拿相机也拍了一些，然而在这部电脑里都找不到了。</p>

<p>以上，2014年上半部。</p>]]></content><author><name>Chen Yangjian</name><uri>http://cyj.me</uri></author><category term="life" /><summary type="html"><![CDATA[2014年已成过去，在此整理过去这一年拍的一些照片，聊作记录。]]></summary></entry><entry><title type="html">变幻中的 2014 II</title><link href="http://cyj.me/life/2014/12/31/transitions-of-2014-ii/" rel="alternate" type="text/html" title="变幻中的 2014 II" /><published>2014-12-31T00:00:00+00:00</published><updated>2014-12-31T00:00:00+00:00</updated><id>http://cyj.me/life/2014/12/31/transitions-of-2014-ii</id><content type="html" xml:base="http://cyj.me/life/2014/12/31/transitions-of-2014-ii/"><![CDATA[<p>继续下半年的。下半年换了手机，相机拍得少了，手机拍得却多了。</p>

<p><img src="/assets/img/2014-recap-ii/2014-07-05.jpg" alt="" /></p>

<p>7月5日和老婆去吃老码头火锅，因为之前去吃的时候拿了券，打算这次消费掉。结果自然是老码头又给了
我几张，面值还挺大。现在想想，起这个头是因为我和老鸭去九九家看他小孩，当晚去老码头聚餐，于是
拿了券。后来又喊上小伙伴去吃了一次。</p>

<p>老码头的红锅我吃了不会犯咽炎，味道又好，所以我一直对它印象不错。</p>

<p><img src="/assets/img/2014-recap-ii/2014-07-19.jpg" alt="" /></p>

<p>似乎是跟组里三位90后小朋友周末一块吃饭，在西溪天堂的美泰泰国餐厅。7月19日，这天天气也很好，
夜色十分美妙。无论工作还是生活，他们比我要放得开，更敢于追求自己想要的结果。又正是青春年少，
快马轻裘的时候，我很羡慕。</p>

<p>万秀台当时装修已毕，尚未开门迎客。</p>

<p><img src="/assets/img/2014-recap-ii/2014-07-27.jpg" alt="" /></p>

<p>现在的咖啡馆都流行养猫，经常去的寒烟也不能例外。7月27日这天游完泳，去寒烟咖啡坐了会，顺便
解决晚饭。刚坐下，就看到这小婊咂窜上我的座位，往我背后钻。它的爪子都还尖利，在后头闹腾的时候，
有时会抓到我，很有些疼。就把它捞到前面来，放在腿边。</p>

<p>结果它毫不客气地睡下了。</p>

<p><img src="/assets/img/2014-recap-ii/2014-07-31.jpg" alt="" /></p>

<p>7月31日晚上，空气通透，很适合拍夜景。我拿了相机上天台，俯瞰学院路。这一年文二路地铁施工，
晚上回家比较早的时候，容易赶上晚高峰，堵上好一会。</p>

<p><img src="/assets/img/2014-recap-ii/2014-09-10.jpg" alt="" /></p>

<p>9月10日，教师节，也是组里最小的乌戈小朋友的生日。HFC 小团体里级别最高、岁数最大的王老师，
选在这天请大家吃饭，庆祝爱子考上北京电影学院。</p>

<p>头一次吃西溪天堂这家外婆家的包厢，有专门的服务员，懒得点菜还可以给你搭配好。买单离场，还一路
送到门口，各种尊贵。</p>

<p><img src="/assets/img/2014-recap-ii/2014-09-13.jpg" alt="" /></p>

<p>9月13日，忘记为什么去紫金港了，顺道去东区走廊转悠，用手机拍了这张照片。</p>

<p><img src="/assets/img/2014-recap-ii/2014-09-19.jpg" alt="" /></p>

<p>9月19日，阿里巴巴 IPO 当晚，部门老大，财务自由，急流勇退回去当富家翁去了。公司给大家发了一件
T恤，后边印着非常低调有内涵的一句，梦想还是要有的，万一实现了呢？后来又演绎出“女神还是要追的，
万一她瞎了呢”之类的版本。</p>

<p>这一晚，改变了许多旺旺群和朋友圈。好多人原本每天聊的是相机、自行车、穷游，如今开始聊车与房。
公司里冒出好多没牌照的车，让原本就已不敷使用的车位愈发紧张。</p>

<p><img src="/assets/img/2014-recap-ii/2014-10-01.jpg" alt="" /></p>

<p>国庆日，妹妹和她的小男朋友订婚。好吧管准妹夫叫小男朋友说不过去。夫妻俩都比我高，一个174
一个180，很登对。</p>

<p>掌勺的是我堂姑父。堂姑姑格局不够，撑不起一家酒楼，叫他很憋屈。</p>

<p><img src="/assets/img/2014-recap-ii/2014-10-04.jpg" alt="" /></p>

<p>后院长满杂草，生活着一窝小野猫。</p>

<p><img src="/assets/img/2014-recap-ii/2014-10-05.jpg" alt="" /></p>

<p>十月五日去老婆家，丈母娘给烧的青蟹，迄今为止吃过最肥美的。</p>

<p><img src="/assets/img/2014-recap-ii/2014-10-06.jpg" alt="" /></p>

<p>十月六日在老婆的奶奶家，奶奶年轻的时候在食堂烧过菜，这桌菜肴不费吹灰之力。因为这次回家也没有
开车，我们买了十月七日从温岭回杭州的动车票。后来刚好看到同事桑行也回家了，有便车可以回杭州。
果断退了火车票，蹭他的车，改为当晚提前回杭，以免次日路阻。</p>

<p><img src="/assets/img/2014-recap-ii/2014-10-07.jpg" alt="" /></p>

<p>国庆节最后一天，门口突然飞来一直鹦鹉，长得很漂亮。</p>

<p><img src="/assets/img/2014-recap-ii/2014-10-12.jpg" alt="" /></p>

<p>10月12日，在杭州备考的表弟这天找我，说想去浙大看看。欣然同意，带他去玉泉转了转，顺便爬老和山。
爬到老和山顶，还不尽兴，索性一路爬到北高峰。</p>

<p>这天天气晴好，然而地表空气不太干净，灰黄色的。</p>

<p><img src="/assets/img/2014-recap-ii/2014-10-18.jpg" alt="" /></p>

<p>同事小凌新房交付，荣升万科业主，我和紫云飞陪同道贺，顺便参观。</p>

<p><img src="/assets/img/2014-recap-ii/2014-10-26.jpg" alt="" /></p>

<p>约了组里的同事们一起去植物园，最终成行的只有三个人，感慨自己号召力不够。秋天里植物园还算漂亮，
然而有好大一片园区都是新造，地面上还都是新土。我照例要去寻那玉泉鱼跃，去看嘴巴张开能吞掉你
拳头的大青鱼。</p>

<p>在那磨蹭完，出来之后发现植物园还有菊花展。于是三个大男人在一堆堆的菊花里转悠许久。</p>

<p><img src="/assets/img/2014-recap-ii/2014-10-29.jpg" alt="" /></p>

<p>同事胡伯入职两周年，请大家吃炉鱼。我身为组长，忘记买蛋糕，十分失职。</p>

<p><img src="/assets/img/2014-recap-ii/2014-11-02.jpg" alt="" /></p>

<p>11月2日，又在寒烟咖啡馆。7月份看到的那只小婊咂不见了，问服务员，答曰说来话长。</p>

<p><img src="/assets/img/2014-recap-ii/2014-11-03.jpg" alt="" /></p>

<p>从11月开始，不再有园区的停车位，决定尝试班车上下班。11月3日应该是头一天，天气非常好，路过
西湖国际，看到马路对面等车回家的路人。</p>

<p><img src="/assets/img/2014-recap-ii/2014-11-04.jpg" alt="" /></p>

<p>11月4日，还是这条路，西湖国际门口，有钱任性的蓝翔技校重型机械学院叉车系某教授因为不想付￥5
元停车费，怒叉保安岗亭。</p>

<p><img src="/assets/img/2014-recap-ii/2014-11-09.jpg" alt="" /></p>

<p>11月9日，这天应该是周日，因为有项颇紧急的临时任务，我被抓来公司，一并参谋，终于切实感受
一把双十一。中午在沽水人家吃饭，看到邻桌行为举止古怪，联想到前一阵山东某处的邪教教众在麦当劳
的暴力事件，我十分紧张。好吧，玩笑话放一边，这应该是一群为双十一拼搏已久的同事，在这节骨眼上，
仍在加班，正在彼此鼓励。</p>

<p><img src="/assets/img/2014-recap-ii/2014-11-10.jpg" alt="" /></p>

<p>11月10日深夜，0点将至，篮球场变为演播室。</p>

<p><img src="/assets/img/2014-recap-ii/2014-11-11.jpg" alt="" /></p>

<p>11月11日0点，灯火通明，今夜无人入眠。</p>

<p><img src="/assets/img/2014-recap-ii/2014-11-12.jpg" alt="" /></p>

<p>11月12日，木头犒劳大家，请吃烤肉。完了还邀众人银乐迪，门口的章鱼椅子非常拽。</p>

<p><img src="/assets/img/2014-recap-ii/2014-11-15.jpg" alt="" /></p>

<p>11月15日，和老婆一道去城北转悠。去了扇博物馆，手工艺博物馆，等等。</p>

<p><img src="/assets/img/2014-recap-ii/2014-11-24.jpg" alt="" /></p>

<p>11月24日，去二食堂吃饭，看到自己获奖的照片，心情复杂，一边激动一边告诉自己别得瑟。能得奖全赖
设计师后期，所以奖品三脚架我想都没想要怎么分，直接归她。</p>

<p>这是2013年拍的照片了，当时还在华星现代产业园上班。这些照片处理成的视频，原本放在一些产品登录
页面背后作为动态背景，后来也都下掉了。</p>

<p><img src="/assets/img/2014-recap-ii/2014-12-12.jpg" alt="" /></p>

<p>双十二，团队聚餐。时间选得非常尴尬，吃完竟然还要回公司。于是我11点多从公司出发，奔赴城西银泰
吃饭，2点左右回公司，坐不了多久，挨到5点多，又差不多得回家了。</p>

<p>城西银泰这家铁板烧非常好吃，叫大渔。</p>

<p><img src="/assets/img/2014-recap-ii/2014-12-13.jpg" alt="" /></p>

<p>次日中午，老婆做的一个番茄饭。</p>

<p><img src="/assets/img/2014-recap-ii/2014-12-14.jpg" alt="" /></p>

<p>12月14日，和老婆去爬西山。山势奇特，有段路是在山脊上行走。</p>

<p><img src="/assets/img/2014-recap-ii/2014-12-20.jpg" alt="" /></p>

<p>12月20日，丈母娘来杭州，在城西银泰的翠华餐厅吃饭。这家店价格颇高冷，一盘广东菜心要￥38，
肉疼，真拿我当上市公司高管了不成。邻近圣诞节，翠华门口的圣诞树非常好看。</p>

<p><img src="/assets/img/2014-recap-ii/2014-12-21.jpg" alt="" /></p>

<p>次日，感觉车子积碳愈发严重，里程也到了该保养的时候了，就开车去4S店。果不其然被狠宰一刀，
接待的客户经理在单子里加上了火花塞和燃油宝。我不熟悉业务，无从分辨燃油管路清洗是否必须做
这两项，只能安慰自己这两项加起来￥200多，姑且由之。</p>

<p>弄妥之后，效果还是挺明显的。</p>

<p><img src="/assets/img/2014-recap-ii/2014-12-27.jpg" alt="" /></p>

<p>再组织一次4月份那样的酒会成了我的夙愿，刚好凌征乔迁新居，因此招呼大家去他家吃火锅，由我来买
火锅所需的材料。结果时间比较短，基本上是吃完就撤。果然是要酒过三巡才有谈天说地的气氛。</p>

<p>这一天是12月27日，离2014年结束还有4天。终于见到胡伯伯贤伉俪，算是取得一项成就。相关成就还剩
紫云飞的和邓老板的，2015年要达成。</p>

<p><img src="/assets/img/2014-recap-ii/2014-12-28.jpg" alt="" /></p>

<p>次日，和老婆去西溪湿地龙舌嘴那边，在一间非常高冷的名叫花间堂的酒店里，找到一家书店，名叫
“猫的天空之城”。这是店里头那只美短。</p>

<p>生活中似乎离不开猫，总能够在哪看到它们。</p>]]></content><author><name>Chen Yangjian</name><uri>http://cyj.me</uri></author><category term="life" /><summary type="html"><![CDATA[继续下半年的。下半年换了手机，相机拍得少了，手机拍得却多了。]]></summary></entry><entry><title type="html">困惑</title><link href="http://cyj.me/life/2014/10/15/at-doubt/" rel="alternate" type="text/html" title="困惑" /><published>2014-10-15T00:00:00+00:00</published><updated>2014-10-15T00:00:00+00:00</updated><id>http://cyj.me/life/2014/10/15/at-doubt</id><content type="html" xml:base="http://cyj.me/life/2014/10/15/at-doubt/"><![CDATA[<p>今年能写的文章一只手数得过来，在大拇指那一篇中我罗嗦了一些近况，现在轮到食指，我还是接着说
最近的事情吧。</p>

<h2 id="喜事">喜事</h2>

<p>前些天国庆长假，发生了一件大事，我妹妹订婚了。我妹妹小我
三岁，小的时候还吵架，长大一些，我去了镇上读初中，又去了县城读高中，又去了省城读大学，便聚少
离多了。我妹妹随我爸，长得瘦高，后来去练田径，是城关一中的体育特招生，那时候练得很辛苦。
转眼我离开瑞安已经十年，我妹妹也女大当嫁了。准妹夫也是练体育的，专长篮球，为人处事有外表所
看不出的机灵圆滑。</p>

<p>祝福他俩。</p>

<h2 id="劳作与不劳而获">劳作与不劳而获</h2>

<p>订婚那天，中午在瑞安，男方在葡萄园大酒店设宴，晚上在鹿木乡村，女方在自家院子里摆酒，觥筹交错，
宾主尽欢。与往常所有摆在家的酒席一样，父母是最忙碌的人。是夜推杯换盏完毕，各回各家之后，
他们俩，加上乡邻们做帮手，还要清理战场。</p>

<p>次日继续清理，归还借来的炉子、桌子、凳子、盘子，洒扫楼面，等等等等。当晚总算所有事情忙完，
可以睡个安稳觉，不曾想，竟被梁上君子光顾，从厨房窗户格栅里钻进来，偷走一只手机、包包里几千
大洋、以及一盒巧克力糖。</p>

<p>“为什么这世上还有贼这种行当，既然手脚俱全，身手不差，为什么不去做点正当营生？” 我和我娘一样
百思不得其解。我觉得我跟我娘比，多读了几年书带来的“好处”只是带来更多的困惑。</p>

<p>这不是我第一次遇到小偷，但这绝对是最让我失望的一次。大学刚毕业的时候，下雨天有小偷尾随我从我
外套兜里摸走了钱包，摸走那一霎那，我有所察觉，还回头看看，当时却以为是钱包在路上丢了。后来，
在学校外边背着包走，又被一个小偷摸包，察觉之后，还跟人顶牛（幸好他身高跟我差不多，不然会被人
当成有着最萌身高差的男同当街亲昵吧），走出来一段之后气不过，还跟人比中指，然后继续对峙，
最终还是不了了之。</p>

<p>然而这次竟然是在家里，是在曾经路不拾遗夜不闭户的鹿木。</p>

<h2 id="窃取与欺骗">窃取与欺骗</h2>

<p>我自然解答不了为什么有人愿意去作贼这样的问题，更何况类似的问题我能问出好多。前些年房子装修，
我体会过每走进一家店，别人看着你的眼神里就是两个￥。前些年在留下，一辆三轮有盖子的摩托车
发神经，掉头的时候撞到我车后保险杠，肇事逃逸。昨天去餐馆吃饭，点的两条小鲳鱼厨房给做错了，
红烧变成了清蒸，发现错误之后直接回炉热了热，变成红烧又给端了上来，鱼肉已经老得不行了。</p>

<p>赚我钱，不赔我钱，我还都能承受，但我实在接受不了欺骗。</p>

<h2 id="困惑与不相信">困惑与不相信</h2>

<p>前一阵我看张公子的书，其中有篇文章叫做《天下第一奇毒》，说真正的天下第一奇毒，并不是什么毒药，
而是对这世间种种的不信任，对所有人的猜忌。我觉得我活得越久，中毒越深。</p>

<p>这也让我想起十年前我要离开瑞安，前往杭州的时候，我外公对我的叮咛。当时，父母因为我考上好大学
而心满意足，唠叨了好些年的“乖啊”、“好好读书啊”终于剧终，亲戚们无从嘱咐，只说好，外公说的却是，
“出门在外要小心呀”、“旅途中骗子、小偷很多啊”、“现在的人坏兮坏”之类的话。</p>

<p>但我外公不是不信人的人，相反，他为人耿直，见不得别人吃亏，年青时长得高大，还是党员，在村子
算是颇有些威望的。所以我更愿意把他的这些叮咛看作是农村人出门在外时的自我保护。在家时，
乡里乡亲彼此熟识，有小偷小摸的只要露了形迹，在地方就很难生活下去，闲言碎语早就纷至沓来了。
然而出门在外，成了外乡人，就觉得失了依靠，开始中毒了。</p>

<p>也正因为这些困惑，我很容易注意到我所接触的文化作品中关于农民的部分，以至于，我还特地去找了一本
关于河南一个村子的书《中国在梁庄》来看。</p>

<p>我看到《七武士》里对农民的评价，他们懦弱、无力，但他们也狡猾、藏私，他们对付不了山贼，但他们
总能成为最终的胜利者，没落的只是武士。我也有《中国在梁庄》的作者梁鸿那样的困惑，我可能已经成了
异乡人，终将失去生我养我的村庄。</p>

<p>再过七年，我在杭州待的时间就要超过我在瑞安所度过的了，到那个时候，我又算是什么呢？</p>]]></content><author><name>Chen Yangjian</name><uri>http://cyj.me</uri></author><category term="life" /><summary type="html"><![CDATA[今年能写的文章一只手数得过来，在大拇指那一篇中我罗嗦了一些近况，现在轮到食指，我还是接着说 最近的事情吧。]]></summary></entry><entry><title type="html">2014 行程过半</title><link href="http://cyj.me/life/2014/06/21/midway/" rel="alternate" type="text/html" title="2014 行程过半" /><published>2014-06-21T00:00:00+00:00</published><updated>2014-06-21T00:00:00+00:00</updated><id>http://cyj.me/life/2014/06/21/midway</id><content type="html" xml:base="http://cyj.me/life/2014/06/21/midway/"><![CDATA[<p>以前写博客怪自己偷懒，会说哎呀最近好忙，技术文都写不出来，只好写篇《最近二三事》凑数。不成想，
今年竟然忙到连凑数的文章都写不出来了。</p>

<p>先汇报一下近况：</p>

<ol>
  <li>项目昏天黑地一通忙，好像可以喘口气了。</li>
  <li>这半年没拍什么照片，只做了一个园区的延时摄影，也不够高端，小孩过家家。</li>
  <li>想买的镜头还是没买，仍然在舍不得花钱买大件的阶段。</li>
  <li>用了三年多的本本又坏了，这次是内存。</li>
  <li>借了一台 rMBP，在这个屏幕上看照片真的好细腻，想把 cyj.me 的图片都放到 CDN，顺便支持
<code class="language-plaintext highlighter-rouge">@2x</code>。</li>
  <li>吉他又断断续续拿起来弹，以前会的都忘了。</li>
</ol>

<p>在这个信息片段化、生活节奏持续加快的年代，我写文章的思路也成了上面这幅样子，一点点片段，
一点点无奈。</p>

<h2 id="google">Google</h2>

<p>从五月底开始，Google 就处在基本无从访问的状态，当时以为是为了欢度5月35日，没想到现在都快到
七月份了，反而愈演愈烈，几乎 Google 的所有产品都不可用了，其中就包括本站在用的
<a href="http://www.google.com/webfonts">Google Fonts</a> 和 <a href="www.google.com/analytics">Google Analytics</a>。</p>

<p>自己用用的产品，可以靠 VPN 或者找代理、IP 访问，但我不可能要求诸位在看我的文章的时候还得
翻了个墙。所以我屈服了，我去掉了本站中 Google 相关内容。</p>

<h2 id="与莎莫的500天">与莎莫的500天</h2>

<p>有部电影叫做《<a href="www.google.com/analytics">与莎莫的500天</a>》，是个与一位名叫 Summer
的姑娘的故事。我以前最喜欢莎莫小姐，夏日炎炎，在家睡觉、出门玩耍两相宜。然而到了杭州，莎莫
给我的感觉就一直不太好，闷热、无处可去，在空调房里感受时间一点点流逝。</p>

<p>更多的，是因为每年夏季似乎都是最躁动的时候。我们曾经可以访问 YouTube、我们也曾经连 Github
都上不去。我们看着窗外热浪，一切如火如荼；然而室内空调很足，吹得人毫无欲望。</p>

<p>不如拥被高卧。</p>

<h2 id="回头望望">回头望望</h2>

<p>带队一年余，又有小伙伴要离开，不是第一次，却令人分外伤感。我们按照业绩、成长来考核自己，
也按最简单直接的方式彼此丈量。哦，你和我做的事情差不多，为什么你可以这样，我却只能如此这般。
我回答不了，我也不想拆穿。多说多错，我说的，我以为的，其他人十之八九不是这样看。</p>

<p>我去，我发现写短句都开始押韵了，绝对适合写歌词啊。</p>

<p>去年此时，为了勉励自己，也为了影响他人，还勉强写了篇半鸡汤半鸡血的文章，高谈阔论、指指点点，
《<a href="/life/the-way-lifes-meant-to-be">认为做前端应当怎样怎样</a>》。原以为今年可以消停一些，
没想到口气更大，直接向乔布斯看齐：</p>

<blockquote>
  <p>We are here to put a dent in the universe. Otherwise why bother being here?</p>
</blockquote>

<p>我竟然好意思说要在这寰宇留下点印记……</p>

<p>鸡汤、鸡血放一边，其实我想表达的非常简单，每天多努力一点，就行了。别忘了星爷说过：</p>

<blockquote>
  <p>做人没有梦想，和咸鱼有什么分别？</p>
</blockquote>

<h2 id="行乐须及春">行乐须及春</h2>

<p>看太多成功学的东西，打太多鸡血，喝太多鸡汤，都容易像老罗那样，“我不为输赢，我就是认真”。
做成了当然好，可以开学校、可以出书、甚至可以卖手机，但这过程太辛苦。</p>

<p>所以偶尔也消停些，来点李白的逍遥自在，遇到麻烦了，脑补一下“人生在世不称意，明朝散发弄扁舟”，
意淫一下“仰天大笑出门去，我辈岂是蓬蒿人”。</p>

<p>及时行乐挺好。</p>

<h2 id="错过">错过</h2>

<p>有次我问小伙伴们，长这么大，有没有后悔一些事情，曰无。我倒是有，说不得。</p>]]></content><author><name>Chen Yangjian</name><uri>http://cyj.me</uri></author><category term="life" /><summary type="html"><![CDATA[以前写博客怪自己偷懒，会说哎呀最近好忙，技术文都写不出来，只好写篇《最近二三事》凑数。不成想， 今年竟然忙到连凑数的文章都写不出来了。]]></summary></entry></feed>