<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Enix Jin's Blog]]></title><description><![CDATA[Thoughts, stories and ideas.]]></description><link>http://blog.enixjin.net/</link><generator>Ghost 0.7</generator><lastBuildDate>Sat, 02 Aug 2025 04:09:21 GMT</lastBuildDate><atom:link href="http://blog.enixjin.net/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[神秘油猴脚本组织bilibili把我们的浏览器变成CDN]]></title><description><![CDATA[<p>这些视频网站都不要脸了</p>

<p>不阻止的话，网页看bilibili直播，会占满上传的带宽（对，有多少占多少），造成网络卡顿，或者触发电信的反pcdn机制，间接断网</p>

<pre><code>// ==UserScript==
// @name B站直播P2P屏蔽脚本
// @namespace https://enixjin.net
// @version 1.0
// @description 屏蔽直播的P2P上传功能，减少设备资源占用
// @author enixjin
// @match *://live.bilibili.com/*
// @grant none
// @run-at document-start
// ==/UserScript==

(function() {
'use strict';
    delete window.RTCPeerConnection;
    delete window.mozRTCPeerConnection;
    delete window.webkitRTCPeerConnection;
    console.log('P2P blocked!')</code></pre>]]></description><link>http://blog.enixjin.net/shen-mi-you-hou-jiao-ben-zu-zhi-bilibiliba-wo-men-de-liu-lan-qi-bian-cheng-cdn/</link><guid isPermaLink="false">ffb11d9b-8a8c-4ad5-82a9-38da19c1c64e</guid><dc:creator><![CDATA[Enix Jin]]></dc:creator><pubDate>Mon, 10 Mar 2025 12:40:54 GMT</pubDate><content:encoded><![CDATA[<p>这些视频网站都不要脸了</p>

<p>不阻止的话，网页看bilibili直播，会占满上传的带宽（对，有多少占多少），造成网络卡顿，或者触发电信的反pcdn机制，间接断网</p>

<pre><code>// ==UserScript==
// @name B站直播P2P屏蔽脚本
// @namespace https://enixjin.net
// @version 1.0
// @description 屏蔽直播的P2P上传功能，减少设备资源占用
// @author enixjin
// @match *://live.bilibili.com/*
// @grant none
// @run-at document-start
// ==/UserScript==

(function() {
'use strict';
    delete window.RTCPeerConnection;
    delete window.mozRTCPeerConnection;
    delete window.webkitRTCPeerConnection;
    console.log('P2P blocked!');
})();
</code></pre>]]></content:encoded></item><item><title><![CDATA[修复9月30号Let's Encrypt证书更换导致的AWS Lambda调用出现SSL错误]]></title><description><![CDATA[letsencrypt
AWS
Lambda
SSL]]></description><link>http://blog.enixjin.net/fix-letsencrypt-aws-lambda/</link><guid isPermaLink="false">68bfa9c6-de29-4dae-8778-1df57d425c70</guid><dc:creator><![CDATA[Enix Jin]]></dc:creator><pubDate>Fri, 01 Oct 2021 12:55:05 GMT</pubDate><media:content url="http://blog.enixjin.net/content/images/2021/10/lets-encrypt-ssl-secured-website-wide.png" medium="image"/><content:encoded><![CDATA[<img src="http://blog.enixjin.net/content/images/2021/10/lets-encrypt-ssl-secured-website-wide.png" alt="修复9月30号Let's Encrypt证书更换导致的AWS Lambda调用出现SSL错误"><p>9月30号，著名的免费HTTPS证书供应商更新了他们的根证书。</p>

<p>从老的方式：</p>

<p><img src="http://blog.enixjin.net/content/images/2021/10/a7b089f88b45cd3c1d4483fb6fea37bc.png" alt="修复9月30号Let's Encrypt证书更换导致的AWS Lambda调用出现SSL错误"></p>

<p>更新到了：</p>

<p><img src="http://blog.enixjin.net/content/images/2021/10/4f05e095091433f504907a1a2c8fbbbf.png" alt="修复9月30号Let's Encrypt证书更换导致的AWS Lambda调用出现SSL错误"></p>

<p>根据官方的文档：<a href="https://letsencrypt.org/docs/certificate-compatibility/">https://letsencrypt.org/docs/certificate-compatibility/</a></p>

<p>Android>7 iOS>10 ubuntu>16的系统应该不会收到影响</p>

<p>然而，悲剧的是，AWS的Lambda的runtime，似乎使用的是很老的证书系统，从.Net Core写的Lambda访问Let's Encrypt搭建的网站的话，会出现SSL的链接错误。</p>

<p>由于无法更改Lambda的runtime，研究下来似乎只有重新签名才能解决问题。</p>

<p><img src="http://blog.enixjin.net/content/images/2021/10/b7178c48ddb1418eca4b1d97d4265d0c.png" alt="修复9月30号Let's Encrypt证书更换导致的AWS Lambda调用出现SSL错误"></p>

<p>简单步骤如下：（ubuntu+nginx+certbot）</p>

<p>1.首先更新certbot到>1.6</p>

<blockquote>
  <p>sudo snap install --classic certbot</p>
  
  <p>certbot --version</p>
</blockquote>

<p>2.1和2.2选一个做</p>

<p>2.1 （推荐）更新位于<code>/etc/letsencrypt/renewal/</code>下的网站配置文件</p>

<p>添加<code>preferred_chain = ISRG Root X1</code>来指定chain</p>

<p>然后运行命令强制重新签名</p>

<blockquote>
  <p>sudo certbot renew --force-renewal</p>
</blockquote>

<p>2.2 （不推荐）不更新配置文件，强行更新</p>

<blockquote>
  <p>sudo certbot renew --force-renewal --preferred-chain 'ISRG Root X1'</p>
</blockquote>

<p>更新完毕后，从AWS的Lambda就可以访问你的Let's Encrypt网站，HTTPS，而不出现SSL错误了。</p>]]></content:encoded></item><item><title><![CDATA[WebAssembly介绍及简单入门]]></title><description><![CDATA[WebAssembly introduction web NodeJS ]]></description><link>http://blog.enixjin.net/webassembly-introduction/</link><guid isPermaLink="false">d9b9c3bf-3759-4acc-a5f0-6d9fc09b0e87</guid><category><![CDATA[技术]]></category><category><![CDATA[NodeJS]]></category><category><![CDATA[Javascript]]></category><dc:creator><![CDATA[Enix Jin]]></dc:creator><pubDate>Wed, 17 Apr 2019 02:42:58 GMT</pubDate><media:content url="http://blog.enixjin.net/content/images/2019/04/1_4ZMcCrF95AUvVzJ4S6Lo-g.png" medium="image"/><content:encoded><![CDATA[<blockquote>
  <img src="http://blog.enixjin.net/content/images/2019/04/1_4ZMcCrF95AUvVzJ4S6Lo-g.png" alt="WebAssembly介绍及简单入门"><p>Any application that can be compiled to WebAssembly, will be compiled to WebAssembly eventually.</p>
  
  <p>-- Ending's law</p>
</blockquote>

<p>JavaScript诞生起到现在已经成为最流行的编程语言(之一)，背后正是由Web及相关技术发展所推动的。JS应用正在变得越来越复杂（各种前后端技术、扩展到了手机APP、桌面），但这也暴露出了JS的问题：</p>

<ul>
<li>语法灵活导致开发大型项目困难</li>
<li>性能在一些场景下不如C/C++等其他语言</li>
</ul>

<p>一些公司研发了各种框架/语言/工具来尝试补救，比较有名的有微软的TypeScript(强类型，提升代码健壮性)、Google的Dart(新的虚拟机直接运行Dart提升性能)，Firefox的asm.js(JS 的子集，引擎针对性能优化)。</p>

<p>那么，WebAssembly，于2015年诞生，2018年发布了1.0版本，被预言为未来的标准，现在已经被4大浏览器(FF,Chrome,Safri,Edge)支持的技术，到底是什么呢？</p>

<hr>

<h3 id="webassembly">WebAssembly介绍</h3>

<p>WebAssembly是一种新的编码方式，可以在现代的网络浏览器中运行 － 它是一种低级的类汇编语言，具有紧凑的二进制格式，可以接近原生的性能运行，并为诸如C / C ++等语言提供一个编译目标，以便它们可以在Web上运行。它也被设计为可以与JavaScript共存，允许两者一起工作。（<a href="https://developer.mozilla.org/zh-CN/docs/WebAssembly">MDN官方定义</a>）</p>

<p>简单来说，WebAssembly不是一门编程语言，而是一份字节码标准。WebAssembly字节码是一种抹平了不同CPU架构的机器码，WebAssembly字节码不能直接在任何一种CPU架构上运行，但由于非常接近机器码，可以非常快的被翻译为对应架构的机器码，因此WebAssembly运行速度和机器码接近。（和Java bytecode一个思路）</p>

<p><img src="http://blog.enixjin.net/content/images/2019/04/1_zaeiNOe-kSznuKi1ndPbpw.png" alt="WebAssembly介绍及简单入门"></p>

<p>目前能编译成 WebAssembly 字节码的高级语言有：</p>

<ul>
<li><a href="https://github.com/AssemblyScript/assemblyscript">AssemblyScript</a> : 语法和TypeScript一致</li>
<li>c\c++ : 大量现成的库，后面的例子里面我们会用C</li>
<li>Rust</li>
<li>Kotlin : <a href="https://kotlinlang.org/docs/reference/native-overview.html">文档</a>;</li>
</ul>

<hr>

<h3 id="webassemblycjs">用WebAssembly结合C和JS</h3>

<p>其实使用WebAssembly最简单的方式是用TS写AssemblyScript（非常简单，只要注意类型的限制就行）。但是出于利用现有C库的考虑，我在下面例子里会使用WebAssembly把C和JS结合在一起。</p>

<h4 id="">安装</h4>

<p>参见<a href="https://webassembly.org/getting-started/developers-guide/">https://webassembly.org/getting-started/developers-guide/</a> ，我的工作环境是win10的WSL Ubuntu</p>

<pre><code>&gt; git clone https://github.com/emscripten-core/emsdk.git
&gt; cd emsdk
&gt; ./emsdk install latest
&gt; ./emsdk activate latest
&gt; source ./emsdk_env.sh --build=Release //每次打开新命令行都要跑，或者把内容复制到你的bashrc里
</code></pre>

<h3 id="">编译</h3>

<p>我们先用C语言写一个简单的fibonacci数量计算：</p>

<pre><code>#include &lt;emscripten.h&gt;

EMSCRIPTEN_KEEPALIVE
int fib(int n) {
  int i, t, a = 0, b = 1;
  for (i = 0; i &lt; n; i++) {
    t = a + b;
    a = b;
    b = t;
  }
  return b;
}
</code></pre>

<p>这里的<code>emscripten.h</code>是emsdk提供的头文件,<code>EMSCRIPTEN_KEEPALIVE</code>标记的函数,在编译的时候不会被treeshake.</p>

<p>我们创建一个项目，把这段C代码保存到在<code>/c/fib.c</code></p>

<p>我们尝试把这段代码编译：</p>

<pre><code>&gt; emcc ./c/fib.c -o ./out/fib.js
&gt; ll out
</code></pre>

<p>我们看到编译出了两个文件，fib.js（胶水代码）、fib.wasm（字节码）。当然，这两个文件有点大，94k和22k，那是因为我们没有启用编译优化，我们可以加个参数编译：</p>

<pre><code>&gt; emcc -O3 ./c/fib.c -o ./out/fib.js
</code></pre>

<p>里面的<code>-O3</code>代表优化级别（数字越大，优化级别越高），再ll看下，fib.js是12k，fib.wasm是85个字节</p>

<h3 id="nodejswasm">NodeJS调用wasm</h3>

<p>为了方便演示不同调用方式，我们把刚才的编译命令加个选项，暴露出ccall和cwrap</p>

<pre><code>&gt; emcc -O3 ./c/fib.c -o ./out/fib.js -s EXTRA_EXPORTED_RUNTIME_METHODS='["ccall", "cwrap"]'
</code></pre>

<p>从nodejs调用wasm是非常简单的，创建一个js文件直接引用生成的js，有三种调用方式:</p>

<pre><code>const em_module = require('./out/fib');

em_module.onRuntimeInitialized = () =&gt; { //确认runtime初始化完毕
    console.log("--      call with _   --");
    console.log(em_module._fib(12)); // 如果不care类型安全,直接加_调用
    console.log("--      ccall         --");
    console.log(em_module.ccall("fib", 'number', ['number'],[12])); // 用ccall定义入参和出参
    console.log("--      cwrap         --");
    let fib = em_module.cwrap("fib", 'number', ['number']); // 用cwrap得到函数
    console.log(fib(12));
};
</code></pre>

<h3 id="webwasm">Web调用wasm</h3>

<p>两种方式:1.使用生成的胶水代码:</p>

<pre><code>&lt;script src="/out/fib.js"&gt;&lt;/script&gt;
&lt;script&gt;
  Module.onRuntimeInitialized = _ =&gt; {
    //三种调用方式：加_直接调用、ccall、cwrap
    const fib = Module.cwrap('fib', 'number', ['number']);
    console.log(fib(12));
  };
&lt;/script&gt;
</code></pre>

<p>2.或者更好，在支持WebAssembly的现代浏览器上，直接load字节码wasm文件：</p>

<pre><code>&lt;script&gt;
 (async () =&gt; {
    const response = await fetch('/out/fib.wasm');
    const buffer = await response.arrayBuffer();
    const module = await WebAssembly.compile(buffer);
    const instance = await WebAssembly.instantiate(module);
    const result = instance.exports.fib(12);
    console.log(result);
 })();
&lt;/script&gt;
</code></pre>

<p>如果你的web server正确配置了MIME type：<code>Content-Type: application/wasm</code>的话，代码可以进一步简化为：</p>

<pre><code>&lt;script&gt;
 (async () =&gt; {
    const fetchPromise = fetch('/out/fib.wasm');
    const { instance } = await WebAssembly.instantiateStreaming(fetchPromise);
    const result = instance.exports.fib(12);
    console.log(result);
 })();
&lt;/script&gt;
</code></pre>

<h3 id="c">编译现有的C语言库</h3>

<p>接下来，我们尝试调用<a href="https://github.com/webmproject/libwebp">webp</a>（library to encode and decode images in WebP format）这个C的库。先拉下项目:</p>

<pre><code>&gt; git clone https://github.com/webmproject/libwebp
</code></pre>

<p>然后写个简单的版本号调用文件web.c(参考webp的<a href="https://developers.google.com/speed/webp/docs/api">API定义</a>):</p>

<pre><code>#include "emscripten.h"
#include "src/webp/encode.h"

EMSCRIPTEN_KEEPALIVE
int version() {
  return WebPGetEncoderVersion();
}
</code></pre>

<p>通过<code>-I</code>参数把整个项目所有的代码都指给编译器:</p>

<pre><code>&gt; emcc -O3 -I libwebp webp.c libwebp/src/{dec,dsp,demux,enc,mux,utils}/*.c -s EXTRA_EXPORTED_RUNTIME_METHODS='["ccall", "cwrap"]' -o ./out/webp.js
</code></pre>

<p>浏览器里调用生成的wasm:</p>

<pre><code>&lt;script src="/out/webp.js"&gt;&lt;/script&gt;
&lt;script&gt;
  Module.onRuntimeInitialized = _ =&gt; {
    const api = {
      version: Module.cwrap('version', 'number', []),
    };
    console.log(api.version());
  };
&lt;/script&gt;
</code></pre>]]></content:encoded></item><item><title><![CDATA[从零开始的Microservices服务搭建(三)]]></title><description><![CDATA[如何在微服务里处理事务]]></description><link>http://blog.enixjin.net/build-your-own-microservice-part-3/</link><guid isPermaLink="false">7689205e-4836-4036-9311-c5b46acf8599</guid><category><![CDATA[技术]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[NodeJS]]></category><dc:creator><![CDATA[Enix Jin]]></dc:creator><pubDate>Tue, 02 Apr 2019 06:55:31 GMT</pubDate><media:content url="http://blog.enixjin.net/content/images/2019/04/1_ooJd1DaaG7lIjoC-t3QJNA.png" medium="image"/><content:encoded><![CDATA[<blockquote>
  <img src="http://blog.enixjin.net/content/images/2019/04/1_ooJd1DaaG7lIjoC-t3QJNA.png" alt="从零开始的Microservices服务搭建(三)"><p>Telling a programmer there's already a library to do Something is like telling a songwriter there's already a song about love.</p>
  
  <p>-- Enix Jin</p>
</blockquote>

<p>前两篇：</p>

<ul>
<li><a href="https://blog.enixjin.net/build-your-own-microservice-part-1/">从零开始的Microservices服务搭建(一)</a></li>
<li><a href="https://blog.enixjin.net/build-your-own-microservice-part-2/">从零开始的Microservices服务搭建(二)</a></li>
</ul>

<p>微服务，通过把系统解耦成一个个“微小”的服务解决了大型系统的复杂性问题。然而，在享受分布式系统的好处的同时，我们也会面临分布式系统的复杂性带来的问题。比如，如何跨多个微服务管理分布式事务？</p>

<p>第三步的代码在<a href="https://gitlab.com/enixjin/microservices/tree/step3">这里</a></p>

<hr>

<h3 id="">什么是分布式事务？</h3>

<p>当微服务架构将大型系统分解为自封装的小服务时，它同时也破坏了事务。</p>

<p>这意味着大型系统中的本地事务现在会被分布到按顺序调用的多个微服务中。</p>

<p>以下是使用本地事务的客户订单示例：</p>

<p><img src="http://blog.enixjin.net/content/images/2019/04/Untitled-UML-4.png" alt="从零开始的Microservices服务搭建(三)"></p>

<p>在上面的客户订单示例中，如果用户将Put Order操作发送到系统，系统将创建一个本地数据库事务。 如果任何步骤失败，则事务回滚。ACID（A:原子性，C:一致性，I:隔离性，D:耐久性），由数据库系统来保证。</p>

<p>然而，当我们使用微服务架构时，我们创建了CustomerMicroservice和OrderMicroservice，它们都有独立的数据库。客户订单就会变成这样：</p>

<p><img src="http://blog.enixjin.net/content/images/2019/04/Untitled-UML-5.png" alt="从零开始的Microservices服务搭建(三)"></p>

<p>当用户发送Put Order请求时，将分别调用两个微服务并存到他们自己的数据库中。由于事务现在跨多个数据库，因此它现在被视为<strong>分布式事务</strong>。</p>

<hr>

<h3 id="">有什么问题呢？</h3>

<p>在单一系统中，ACID由数据库来保证，在微服务架构中，我们需要考虑以下问题：</p>

<h4 id="">我们如何保持事务的原子性？</h4>

<p>在数据库系统中，原子性意味着一个事务中所有步骤要么<strong>都完成</strong>要么<strong>都没有完成</strong>。 默认情况下，基于微服务的系统没有全局事务协调器。 在上面的示例中，如果CreateOrder方法失败，我们如何回滚CustomerMicroservice应用的更改？</p>

<h4 id="">我们是否针对并发请求隔离用户操作？</h4>

<p>如果一个Object正在由一个事务写入的，同时（在事务结束之前），如果有人访问改对象，系统应该返回旧数据还是将更新数据？ 在上面的示例中，一旦UpdateCustomerFund成功但仍在等待CreateOrder的响应，当前客户资金信息请求是否会返回变更后的金额？</p>

<hr>

<h3 id="">解决方案</h3>

<p>上述问题对于基于微服务的系统很重要。否则，你无法判断事务是否已成功完成。</p>

<p>大致思考下，有以下两种模式可以解决问题：（我选择后者，原因后面会讲）</p>

<ul>
<li>两阶段提交</li>
<li>SAGA (来自于1987年Hector GM和Kenneth Salem论文)</li>
</ul>

<h4 id="">两段提交</h4>

<p><strong>两段提交</strong>被广泛用于数据库系统。 在某些情况下，你可以使用<strong>两段提交</strong>进行微服务。但是，事实上，两段提交在微服务架构中被认为是不切实际的。</p>

<p>那什么是两阶段提交呢？</p>

<p>正如其名称提示，<strong>两段提交</strong>有两个阶段：<strong>准备阶段</strong>和<strong>提交阶段</strong>。 </p>

<p>在准备阶段，将要求所有微服务准备一些可以原子方式完成的数据更改。一旦准备好所有微服务，提交阶段将要求所有微服务进行实际更改。</p>

<p>所以，两段提交通常需要有一个全局协调器(Coordinator)来维护事务的生命周期，协调器需要在准备和提交阶段调用微服务。</p>

<p>还是客户下单的例子，如果用两段提交：</p>

<p><img src="http://blog.enixjin.net/content/images/2019/04/Untitled-UML-6.png" alt="从零开始的Microservices服务搭建(三)"></p>

<p>在上面的示例中，当用户发送放置订单请求时，协调器将首先创建包含所有上下文信息的全局事务。然后它会告诉CustomerMicroservice准备用创建的交易更新客户资金。接着，CustomerMicroservice将检查客户是否有足够的资金来进行交易。一旦CustomerMicroservice可以执行更改，它将锁定对象的进一步更改并告诉协调器它已准备好。同时，在OrderMicroservice中创建订单时会发生同样的事情。一旦协调器确认所有微服务都准备好应用他们的更改，它就会要求他们通过请求提交事务来应用他们的更改。此时，所有对象都将被解锁。</p>

<p>如果在任何时候单个微服务无法准备，协调器将中止事务并开始回滚过程。比如以下的回滚图：</p>

<p><img src="http://blog.enixjin.net/content/images/2019/04/Untitled-UML-7.png" alt="从零开始的Microservices服务搭建(三)"></p>

<h4 id="">使用两段提交的好处</h4>

<p>两段提交是一种非常强大的一致性协议。首先，准备和提交阶段保证事务是原子的。事务将以所有微服务成功返回或所有微服务没有任何改变而结束。其次，两段提交允许读写分离。 这意味着在协调器提交更改之前，字段上的更改不可见。</p>

<h4 id="">两段提交的缺点</h4>

<p>虽然两段提交能解决事务问题，但对于微服务是不推荐的。原因？因为它是同步阻塞的。</p>

<p>协议需要在事务完成之前锁定更改对象。在上面的例子里，如果客户下订单，则将为客户锁定“资金”字段。 这会防止客户申请新订单。这是在两段提交里有道理的，因为如果“准备好的”对象在声称它已“准备好”之后发生了更改，那么提交阶段可能无法正常工作。</p>

<p>在数据库系统中，事务通常在50毫秒级别。但是，微服务在远程调用方面存在长时间延迟的可能性，尤其是需要与外部服务（比如支付服务）集成时。锁可能成为系统性能瓶颈。此外，如果一个事务请求锁定另一个事务需要的资源时，非常可能有两个事务相互锁定（死锁）。</p>

<hr>

<h3 id="saga">SAGA</h3>

<p>Saga模式是另一种广泛使用的分布式事务模式。与同步的两段提交不同，Saga模式是异步和响应式的。在Saga模式中，分布式事务由所有相关微服务上的异步本地事务完成。微服务通过事件总线(Event Bus)相互通信。</p>

<p>以下是客户订单示例的Saga模式图：</p>

<p><img src="http://blog.enixjin.net/content/images/2019/04/Untitled-UML-8.png" alt="从零开始的Microservices服务搭建(三)"></p>

<p>在上面的示例中，OrderMicroservice收到下订单的请求。它首先启动本地事务以创建订单，然后发出OrderCreated事件。CustomerMicroservice会在收听到此事件时尝试更新客户资金。如果从账户成功扣款，则会发出CustomerFundUpdated事件，表示交易结束。</p>

<p>如果某个微服务无法完成其本地事务，其他微服务将运行补偿事务(compensation transactions)以回滚更改。 以下是补偿交易的Saga模式图：</p>

<p><img src="http://blog.enixjin.net/content/images/2019/04/Untitled-UML-9.png" alt="从零开始的Microservices服务搭建(三)"></p>

<h4 id="saga">Saga模式的优点</h4>

<p>Saga模式的一大优势是它很好的支持了耗时长的交易。因为每个微服务仅关注其自己的本地原子事务，所以即使某个微服务运行很长时间，也不会阻塞其他微服务。同时，Saga也允许事务等待期间用户的新订单。此外，由于所有本地事务都是并行发生的，因此任何对象都没有锁定。</p>

<h4 id="saga">Saga模式的缺点</h4>

<p>Saga模式很难调试，特别是当事务涉及多个微服务时。此外，如果系统变得复杂，事件消息队列可能变得难以维护。Saga模式的另一个缺点是它没有读取隔离。例如，客户可以看到正在创建的订单成功了，但在下一秒，订单将因补偿交易而被删除。</p>

<hr>

<h3 id="httpsgitlabcomenixjinmicroservicestreestep3">代码(<a href="https://gitlab.com/enixjin/microservices/tree/step3">这里</a>)的一些说明</h3>

<ul>
<li>在第二步代码的基础上，增加了一个微服务：Order，账户系统就在原先的Auth上扩展（因为懒）</li>
<li>引入了<a href="https://www.rabbitmq.com/install-debian.html">RabbitMQ</a>作为事件消息队列</li>
</ul>

<p>比如我们通过gateway，登录之后下一个订单：</p>

<p><img src="http://blog.enixjin.net/content/images/2019/04/----_20190402144628.png" alt="从零开始的Microservices服务搭建(三)"></p>

<p>order微服务收到消息之后会执行订单创建并往消息队列OrderCreated里发布一个orderid：</p>

<pre><code>@Post({url: "/"})
async sendMessage(req) {
    let user = await encryption.getAuthentication(req);
    // 1.create order
    // 2.send to queue
    if (user) {
        await mqTools.sendToQueue("OrderCreated", {orderid: req.body.orderid});
        return {success: true, body: req.body, nodePort: global.config.servicePort};
    }
}
</code></pre>

<p>账户系统在听到这个消息后去操作账户：</p>

<pre><code>//app.ts
mqTools.subscribe("OrderCreated").subscribe((msg) =&gt; {
  return customerService.handleOrderMessage(msg);
});

//customerService.ts
async handleOrderMessage(msg: string) {
    let message = JSON.parse(msg);
    logger.debug(`handle order id:${message.orderid}`);
    let success = false;
    if (success) {
        // success
        await this.updateCustomerFund();
        // current no one interested in this message
        await mqTools.sendToQueue("CustomerFundUpdated", {orderid: message.orderid});
    } else {
        // fail
        await mqTools.sendToQueue("CustomerFundFailed", {orderid: message.orderid});
    }
}
</code></pre>

<p>如果处理失败，order微服务会监听到CustomerFundFailed里的消息：</p>

<pre><code>//orderService.ts
async handleCustomerFundMessage(msg: string) {
    let message = JSON.parse(msg);
    logger.debug(`handle order fail id:${message.orderid}`);
    await this.compensateOrder(message.orderid);
}
</code></pre>

<p>可以观察到两个微服务的日志：</p>

<p><img src="http://blog.enixjin.net/content/images/2019/04/----_20190402145238.png" alt="从零开始的Microservices服务搭建(三)"></p>

<p><img src="http://blog.enixjin.net/content/images/2019/04/----_20190402145256.png" alt="从零开始的Microservices服务搭建(三)"></p>]]></content:encoded></item><item><title><![CDATA[从零开始的Microservices服务搭建(二)]]></title><description><![CDATA[<blockquote>
  <p>So much complexity in software comes from trying to make one thing do two things.</p>
  
  <p>And the rest of the complexity comes from making two things do one thing.</p>
  
  <p>-- Enix Jin</p>
</blockquote>

<p>上一篇：<a href="https://blog.enixjin.net/build-your-own-microservice-part-1/">从零开始的Microservices服务搭建(一)</a></p>

<p>基于微服务架构为软件开发带来了许多好处，包括小型开发团队、更短的开发周期、语言选择的灵活性、服务可扩展性等。</p>

<p>然而，不幸的是，微服务还引入了分布式系统的许多复杂问题。其中第一个挑战就是如何在微服务架构中实现灵活、安全、有效的身份验证（Authentication）和授权（Authorization）方案。</p>]]></description><link>http://blog.enixjin.net/build-your-own-microservice-part-2/</link><guid isPermaLink="false">7817b73b-7881-47eb-b5a7-efc905c88f5f</guid><category><![CDATA[技术]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[NodeJS]]></category><dc:creator><![CDATA[Enix Jin]]></dc:creator><pubDate>Fri, 01 Mar 2019 05:52:38 GMT</pubDate><media:content url="http://blog.enixjin.net/content/images/2019/02/MS_central_logging_1.png" medium="image"/><content:encoded><![CDATA[<blockquote>
  <img src="http://blog.enixjin.net/content/images/2019/02/MS_central_logging_1.png" alt="从零开始的Microservices服务搭建(二)"><p>So much complexity in software comes from trying to make one thing do two things.</p>
  
  <p>And the rest of the complexity comes from making two things do one thing.</p>
  
  <p>-- Enix Jin</p>
</blockquote>

<p>上一篇：<a href="https://blog.enixjin.net/build-your-own-microservice-part-1/">从零开始的Microservices服务搭建(一)</a></p>

<p>基于微服务架构为软件开发带来了许多好处，包括小型开发团队、更短的开发周期、语言选择的灵活性、服务可扩展性等。</p>

<p>然而，不幸的是，微服务还引入了分布式系统的许多复杂问题。其中第一个挑战就是如何在微服务架构中实现灵活、安全、有效的身份验证（Authentication）和授权（Authorization）方案。</p>

<hr>

<p>名词解释：</p>

<ul>
<li>验证（Authentication）: <strong>你是谁</strong>（Who you are），一般采用<code>用户名+密码</code>方式</li>
<li>授权（Authorization）: <strong>你能做什么</strong>（What you can do），比如你能否对某个资源做删除操作</li>
</ul>

<hr>

<h4 id="authenticationauthorization">传统应用中的Authentication和Authorization</h4>

<p>我们先看下在传统应用中，这个问题一般是怎么解决的：</p>

<ul>
<li>当用户登录时，应用程序的安全模块验证用户的身份</li>
<li>在验证用户是合法的之后，为用户创建Session，并且将唯一的Session ID与Session相关联。Session中会存储登录用户信息，例如用户名、角色</li>
<li>服务器将Session ID返回给客户端</li>
<li>客户端将Session ID记录为cookie，并在后续请求中将其发送到服务端</li>
<li>服务可以使用Session ID来验证用户的身份、权限，而无需每次都输入用户名和密码进行身份验证</li>
</ul>

<p>大致流程如下图：</p>

<p><img src="http://blog.enixjin.net/content/images/2019/02/1_Vgz8vTZM9M0yL5MSMVnMWA.png" alt="从零开始的Microservices服务搭建(二)"></p>

<hr>

<h4 id="microservicesauthenticationauthorization">Microservices中的Authentication和Authorization</h4>

<p>在微服务架构下，应用程序被分成多个微服务，每个微服务器只实现一个模块的业务逻辑。 在拆分之后，需要对每个微服务的访问请求进行身份验证和授权。如果您参考传统应用的实现，你将遇到以下问题：</p>

<ul>
<li>验证和授权需要在每个微服务中重复实现。（虽然我们可以使用代码库来重用部分代码，但这反过来会导致所有微服务依赖于特定的代码库及其版本，从而影响微服务语言/框架选择的灵活性）</li>
<li>微服务应遵循单一责任原则。 微服务只处理单个业务逻辑。 身份验证和授权的全局逻辑不应放在微服务实现中。</li>
<li>HTTP是无状态协议。对于服务器，每次用户的HTTP请求是独立的。无状态意味着服务器可以根据需要将客户端请求发送到集群中的任何节点。HTTP的无状态设计对负载平衡有明显的好处。由于没有状态，用户请求可以分发到任何服务器。对于不需要身份验证的服务，例如浏览新闻页面，没有问题。但是，许多服务（例如在线购物和企业管理系统）是需要验证用户身份的。因此，需要以基于HTTP协议的方式保存用户的登录状态，以防止用户需要对每个请求执行验证。传统方式是使用服务器端的Session来保存用户状态。由于服务器是有状态的，无法水平扩展。</li>
</ul>

<p>解决方案？</p>

<p>基于保留服务器端Session（不推荐）的解决大致有如下几个：</p>

<ul>
<li>1.负载均衡确保同一用户每次都访问同一个集群节点 （负载均衡会做的比较复杂，而且万一处于某种原因需要切换节点，会造成session丢失）</li>
<li>2.Session复制，把session完整同步到每个节点上（服务器越多，对内存和带宽的消耗越大）</li>
<li>3.独立的Session服务器，所有服务的Session存在一个共享的存储上（比如Redis）</li>
</ul>

<p>但是，既然我们最初的设计就是基于REST的微服务，我们决定抛弃Session，采用Token的方式：</p>

<p>Token和Session之间的主要区别在于存储的位置不同。Session集中存储在服务器中，而Token由用户自己持有，并且通常以localStorage的形式存储在浏览器中。Token编码用户的身份信息，并且每次将请求发送到服务器时，服务器因此可以确定访问者的身份并确定其是否可以访问所请求的资源。</p>

<p>在上一篇里，我们把Gateway做为外部请求的唯一入口。这意味着所有请求都通过Gateway，有效地隐藏了微服务。我们能不能在Gateway上来做Authentication和Authorization呢？当然是可以的。我们甚至能更进一步，把Token机制的一个缺点一起解决掉。</p>

<p>我们知道为了保证服务器的无状态，我们一般把token放在HTTP的头里面，每次发送时都由服务器解析这个token，从而获取里面的payload、有效期等信息（比如<a href="https://jwt.io/">JWT</a>）。Token机制的一个问题是Token一旦发出去了，在有效期内是无法撤消的。但是现在有了Gateway这个统一入口，我们可以把JWT Token在Gateway转换成不透明的外部令牌。每次请求时，Gateway负责把这个外部令牌转换成内部微服务使用的JWT Token。这样在保持微服务无状态的情况下，我们顺便解决了Token无法撤消的问题，如下图：</p>

<p><img src="http://blog.enixjin.net/content/images/2019/02/1_2V7W9hSBns86HubojFPHiw.png" alt="从零开始的Microservices服务搭建(二)"></p>

<p><a href="https://gitlab.com/enixjin/microservices/tree/step2">Step2的source code在这里</a></p>

<p>Gateway暴露login和logout的方法，注意为了简化代码，我们没有把Token->JWT的内容放到Redis里面，而是直接在内存里的一个Map。我们也假设了所有的微服务使用了同一个JWT key（虽然稍微做扩展就能区分）。</p>

<pre><code>@Post("/login")
async login(req) {
    let authService = global.microservices.get("/api/auth");
    if (authService &amp;&amp; authService.nodes.filter(node =&gt; node.active === 1).length &gt; 0) {
        ...
        let uuid = uuidv4();
        global.token.set(uuid, resp.body.jwt);
        return {token: uuid};
    } else {
        throw new serviceException("no active auth service!", 400, "auth service fail!");
    }
}

@Delete("/logout")
async logout(req) {
    if (req.get("token")) {
        global.token.delete(req.get("token"));
    }
    return {success: true};
}
</code></pre>

<p>Gateway的proxy部分也稍作修改，如果能根据Token获取到JWT，就把它写在header里面发送给微服务。（参加<a href="https://gitlab.com/enixjin/microservices/blob/step2/gateway/src/controller/proxyController.ts">代码</a>）</p>

<p>最后，为了测试权限系统，我们对message微服务稍做修改：</p>

<pre><code>@Get("")
async list() {
    return {success: true, nodePort: global.config.servicePort};
}

@Post({
    url: "",
    callbacks: [async (req, res, next) =&gt; {
        logger.debug(req.get("jwt"));
        let user = await encryption.getAuthentication(req);
        if (user.username === "enixjin") {
            next();
        } else {
            res.status(403).jsonp({error: `your are:${user.username}, only enixjin could access this api`});
        }
    }]
})
async sendMessage(req) {
    return {success: true, body: req.body, nodePort: global.config.servicePort};
}
</code></pre>

<p>可以看到GET方法对所有登录用户开放，而POST方法只对JWT的payload里解出的username为<code>enixjin</code>的用户开放。</p>

<p>简单的测试下：</p>

<p>登录：</p>

<p><img src="http://blog.enixjin.net/content/images/2019/03/----_20190301130430.png" alt="从零开始的Microservices服务搭建(二)"></p>

<p>然后我们用这个token去访问message微服务</p>

<p><img src="http://blog.enixjin.net/content/images/2019/03/----_20190301130430-1.png" alt="从零开始的Microservices服务搭建(二)"></p>

<p>如果尝试用不同用户的token去发布一个message时候，服务器正确的返回了403：</p>

<p><img src="http://blog.enixjin.net/content/images/2019/03/----_20190301130430-2.png" alt="从零开始的Microservices服务搭建(二)"></p>

<h3 id="next">总结&amp;Next</h3>

<p>Step2的代码实现了下面的目标：</p>

<ul>
<li>由Gateway实现了登录</li>
<li>token->jwt的转换、存储使得系统更安全，并且注销成为可能</li>
<li>最大程度的使microservice独立运行，只需要考虑自己的业务逻辑</li>
</ul>

<p>后面的计划：微服务之间的互相调用问题、熔断、恢复、隔离等</p>]]></content:encoded></item><item><title><![CDATA[从零开始的Microservices服务搭建(一)]]></title><description><![CDATA[NodeJS Typescript Microservice]]></description><link>http://blog.enixjin.net/build-your-own-microservice-part-1/</link><guid isPermaLink="false">28334704-70ec-4e5b-8b16-d160a30e0bd9</guid><category><![CDATA[技术]]></category><category><![CDATA[NodeJS]]></category><category><![CDATA[TypeScript]]></category><category><![CDATA[Javascript]]></category><dc:creator><![CDATA[Enix Jin]]></dc:creator><pubDate>Thu, 21 Feb 2019 06:39:06 GMT</pubDate><media:content url="http://blog.enixjin.net/content/images/2019/02/REST-Standardization.png" medium="image"/><content:encoded><![CDATA[<blockquote>
  <img src="http://blog.enixjin.net/content/images/2019/02/REST-Standardization.png" alt="从零开始的Microservices服务搭建(一)"><p>Architecture（架构）一词源于建筑学，指建筑物在大尺度上是如何靠内部支撑物相互结合而稳固构造的方式</p>
  
  <p>Architect（架构师）是为满足软件设计目标而在较大尺度上进行整体构思的角色</p>
  
  <p>-- 架构、架构师的定义</p>
</blockquote>

<p>建议读本文前先找几本关于Microservice的书阅读一下，我的<a href="https://join.slack.com/t/jinyexin/shared_invite/enQtNTAwOTI5OTM4NDMzLTQ4OWMwZDRkNzY3YjQ1NTEyNWI2YTM5MjRiZjQwYWU4NjZiYzQwOTU1ZGYyMjM2ZGRlMGM2YmQzNjcxMzg5MDk">Slack bookshare</a>里面有几本。</p>

<hr>

<p>相比设计思想一脉相承的SOA（Service Oriented Architecture），Microservices 是一种把服务分解到更细粒度，松耦合的架构风格。</p>

<p>使用微服务，你的代码将被分解为独立的服务，而这些服务可以用不同的语言开发、部署在不同的地方、作为单独的进程运行。这样的架构风格提供了更高的模块化，使程序更易于开发、测试、部署，最重要的是，更改和维护。（和敏捷开发契合度很高）</p>

<p>市面上有很多微服务的框架，Java的有Spring Cloud和Dubbo（不推荐，缺少很多组件且曾经多年没人维护，使用老旧的RPC），NodeJS也有Seneca和Moleculer可选。不过对于有理想和追求的程序员来说，有什么比自己亲手搭建一个能使自己更了解这种架构呢？（只知道调用API，调整别人的配置有什么意思!）</p>

<hr>

<p>搭建前的一些技术decision:</p>

<ul>
<li>微服务的通信全部基于REST(REST服务的搭建不是重点，为了省代码，使用我的小框架<a href="https://www.npmjs.com/package/@jinyexin/core">@jinyexin/core</a>)</li>
<li>自己实现中央式（即微服务之间不相互注册）服务注册、发现，不依赖eureka或者zookeeper</li>
<li>对外REST服务全部走Gateway，做到对Web端/APP端，拆分微服务和不拆分零区别</li>
</ul>

<p>第一步的目标：</p>

<ul>
<li>Gateway：实现服务的注册、发现，REST请求的代理</li>
<li>message：一个简单的信息服务</li>
<li>auth：用户的注册、登录服务(整个系统的用户验证会在第二步考虑)</li>
</ul>

<p>搭建第一步的代码在<a href="https://gitlab.com/enixjin/microservices/tree/step1">这里</a></p>

<hr>

<h3 id="gateway">Gateway</h3>

<p>我们把所有的服务注册信息放在一个map里面，key或者说name就是服务的介入点URL，value里面包含了所有已经发现的微服务节点。</p>

<p>类型定义如下：</p>

<pre><code>export type Microservice = {
    name: string; //example: /api/message
    nodes: Array&lt;{ host: string, port: number, lastHeartbeat?: Date }&gt;;
}
</code></pre>

<p>在gateway上提供的注册、心跳服务如下(<a href="https://gitlab.com/enixjin/microservices/blob/step1/gateway/src/controller/gatewayController.ts">GatewayController</a>)：</p>

<pre><code>@Get("")
async list() {
    return Array.from(global.microservices.values());
}

@Post("")
async register(req) {
    ...//解析注册请求并加入到map里面
}

@Put("")
async heartbeat(req) {
    ...//心跳，更新map中每个服务节点的最后心跳时间
}
</code></pre>

<p>同时，我们实现了一个非常简单的代理（<a href="https://gitlab.com/enixjin/microservices/blob/step1/gateway/src/controller/proxyController.ts">ProxyController</a>），把所有其他请求都代理到相应的microservice节点上，如果有多个可用节点就随机抽一个。（代码略）</p>

<p>最后，我们在Gateway的启动代码<a href="https://gitlab.com/enixjin/microservices/blob/step1/gateway/src/app.ts">app.ts</a>里加了一小段逻辑来踢出60秒内没有心跳的节点：</p>

<pre><code>setInterval(
    () =&gt; {
        global.microservices.forEach((microservice: Microservice) =&gt; {
            let now = new Date();
            microservice.nodes = microservice.nodes.filter(node =&gt; (now.getTime() - node.lastHeartbeat.getTime()) &lt;= 60 * 1000);
        })
    },
    10 * 1000
);
</code></pre>

<p>整个Gateway的有效代码仅仅200左右</p>

<hr>

<h3 id="messageauth">微服务 Message &amp; Auth</h3>

<p>由于我们的目标是验证这个微服务框架，Message和auth只是两个个不同的服务而已。每个服务的关键在于启动时<a href="https://gitlab.com/enixjin/microservices/blob/step1/message/src/app.ts">app.ts</a>的注册和发送心跳到Gateway：</p>

<pre><code>httpTools
    .sendHttp("localhost", 3000, "/api/gateway", "POST", JSON.stringify({
        "name": endpoint,
        "nodes": [
            {"host": "localhost", "port": global.config.servicePort}
        ]
    }))
    .then(() =&gt; {
        logger.info(`success connected to gateway`);
        application.init();
        setInterval(() =&gt; {
            httpTools.sendHttp("localhost", 3000, "/api/gateway", "PUT", JSON.stringify({
                "name": endpoint,
                "nodes": [
                    {"host": "localhost", port: global.config.servicePort}
                ]
            })).then(logger.silly);
        }, 30 * 1000);
    })
    .catch(() =&gt; {
        logger.error("fail to register, exit.");
        process.exit(0);
    });
</code></pre>

<p>每个微服的有效代码量在50行左右</p>

<hr>

<h3 id="">启动及节点的扩展</h3>

<p>我们使用<a href="https://pm2.io/">pm2</a>作为我们的进程管理工具。</p>

<p>在pm2的配置文件中（<a href="https://gitlab.com/enixjin/microservices/blob/step1/ecosystem.config.js">source</a>），我们启动一个Gateway在3000，message服务分别起两个实例在4000和4001，auth服务在5000</p>

<p>（或者你想玩下容器的话，可以用docker起上述的4个节点。Dockerfile已经包含在项目里，启动命令可参考<code>docker run -p 3000:3000 gateway</code>）</p>

<p>这样，在每个子项目都已经<code>npm i &amp;&amp; npm run build</code>之后，我们可以直接在根目录上运行<code>pm2 start</code>来个启动整个微服务群：</p>

<p><img src="http://blog.enixjin.net/content/images/2019/02/20190221140521.png" alt="从零开始的Microservices服务搭建(一)"></p>

<p>或者输入<code>pm2 monit</code>来进行监控：</p>

<p><img src="http://blog.enixjin.net/content/images/2019/02/20190221140731.png" alt="从零开始的Microservices服务搭建(一)"></p>

<p>好了，接下来我们所有的测试工作都是通过在3000端口，这个<strong>唯一</strong>对外服务的API Gateway来进行的。</p>

<p>我们可以发送一个请求来查看节点的注册状况（{Get} <a href="http://localhost:3000/api/gateway">http://localhost:3000/api/gateway</a> ）：</p>

<p>可以看到三个节点都顺利注册完成：</p>

<p><img src="http://blog.enixjin.net/content/images/2019/02/20190221141051.png" alt="从零开始的Microservices服务搭建(一)"></p>

<p>让我们再试试有两个节点的message微服务（{Get} <a href="http://localhost:3000/api/message">http://localhost:3000/api/message</a> ）：</p>

<p><img src="http://blog.enixjin.net/content/images/2019/02/----_20190221142044.png" alt="从零开始的Microservices服务搭建(一)"></p>

<p>多刷几次，发现body中的端口号在变，说明访问是随机分发给活着的节点的</p>

<p>最后，我们试下auth的login接口({Post} <a href="http://localhost:3000/api/auth/login">http://localhost:3000/api/auth/login</a> ):</p>

<p><img src="http://blog.enixjin.net/content/images/2019/02/----_20190221143042.png" alt="从零开始的Microservices服务搭建(一)"></p>

<p>可以看到post的请求body和返回body都被正确处理了</p>

<hr>

<h3 id="next">总结&amp;Next</h3>

<p>微服务并没有什么神秘的，我们用区区几百行代码，就从零开始搭起了一个微服务的架子。通过搭建这个简单的例子，是不是对服务发现、注册，Gateway的运行原理有了更多的了解呢？</p>

<p>预告：这只能说是个雏形，下一步，我们会关心实际点的应用例子：Authentication（你是谁）和Authorization（你能干什么）如何在微服务中的解决方案。</p>]]></content:encoded></item><item><title><![CDATA[谈谈函数式编程]]></title><description><![CDATA[函数式 Functional Programming]]></description><link>http://blog.enixjin.net/think_about_functional_programming/</link><guid isPermaLink="false">ad630835-2651-4416-90e0-0fa11663ac54</guid><dc:creator><![CDATA[Enix Jin]]></dc:creator><pubDate>Wed, 07 Nov 2018 00:11:23 GMT</pubDate><media:content url="http://blog.enixjin.net/content/images/2018/10/fQUJjqf.png" medium="image"/><content:encoded><![CDATA[<img src="http://blog.enixjin.net/content/images/2018/10/fQUJjqf.png" alt="谈谈函数式编程"><p>从50年代的 Lisp 开始，函数式编程已经存在很长时间了。（在过去的几年里的Clojure、Scala、Erlang 和Elixir)。那么，<strong>函数式编程</strong>也就是FP(Functional Programming)到底是什么？和<strong>命令式编程</strong>(Imperative programming)有什么区别？有什么有点和缺点？如果真的像FP鼓吹者说的那样完美，为啥实际应用中不多见？</p>

<hr>

<h3 id="">什么是函数式编程？</h3>

<p>要回答这些问题，我们要先来看看函数式编程的定义。像OO(面向对象)的编程思想一样，FP(函数式)编程也只是一个programming paradigm而以。这个想法大致来说，包含两点：</p>

<ul>
<li>一切都是纯函数</li>
<li>没有共享状态、可变状态、没有副作用</li>
<li>用纯函数构建一切</li>
</ul>

<p>第一条很容易理解，函数是一等公民，你可以把函数赋予变量，也可以把函数作为参数传给其他函数或作为返回值。</p>

<p>对于第二条来说，我们写程序中经常用到的常量(<code>let a = 0;</code>)就不存在了。有些介绍FP的文章笼统的描述为FP里所有的变量都是final的，这其实是误解。FP里面没有变量的概念，全部是纯函数，取代前面那段代码的应该是：<code>let a = () =&gt;0;</code>。没错，a虽然可以看作一个final的变量，但其实看作一个返回0的<strong>纯函数</strong>而以。</p>

<p>对于第三条，一个常见的疑问就是：没有变量还能正常写逻辑吗？答案是：完全可以。<a href="https://zh.wikipedia.org/wiki/%CE%9B%E6%BC%94%E7%AE%97">λ演算</a>和<a href="https://zh.wikipedia.org/wiki/%E5%9B%BE%E7%81%B5%E6%9C%BA">图灵机</a>已经被证明了是具有同样能力，任何命令式语言能做的函数式语言也都能做到。(比如Lisp、ML、Haskell)。</p>

<p>比如反转字符串的方法，完全可以不用临时变量而用递归：</p>

<pre><code>function reverse(str:string) {
  if(str.length === 0) {
    return str;
  }
  else {
    return reverse(str.substr(1, str.length)) + str.substring(0, 1);
  }
}
</code></pre>

<hr>

<h3 id="">函数式编程的优点</h3>

<p>看到上面那段递归，可能有人就会问了：为什么我要吃饱了撑的把代码写这么奇葩？递归不是吃内存又慢么？还别说，如果能完全函数式，好处可是大大的有！</p>

<h4 id="">测试</h4>

<p>如果一个方法是纯函数的，意为着这个方法的返回值依赖且只依赖于输入值。我不需要去mock一大堆全局的依赖来测试这个方法了。我只需要提供输入值，然后检查下返回值是不是预期的就行，不要太简单。</p>

<h4 id="">调试</h4>

<p>在实际的项目中，你有没有被有些无法确定触发条件的bug而烦恼呢？函数式编程可没有这种烦恼，如果一个函数在某个输入值下出错，他就一定会每次都出错！更妙的是，由于一定没有读取、改变全局变量啊什么的，只要输入一样，无论是本机、测试机、还是生产机，都必然能重现出这个错误！想想以前去服务器上打log、翻日志，企图重现某个bug的日子，现在能在本地重现，是不是有点小激动！</p>

<h4 id="">并发及部署</h4>

<p>假设我们有如下一段程序：</p>

<pre><code>let a = funcA();
let b = funcB();
let c = funcC(a,b);
//或者更functional的写法
let c = funcC(funcA(),funcB());
//或者惰性求值
let c = funcC(funcA,funcB);
</code></pre>

<p>如果funcA和funcB都是比较耗时的，我能不能把这两个function改成并行跑呢？在指令式环境下，这需要仔细分析、评估funcA和funcB是否使用/修改了外部资源、有没有互相影响后才行。而如果是纯FP的话，甚至编译器就可以自动完成这两个function的拆分和并发工作，因为两个不同的function，必然是不互相影响的。</p>

<p>基于相同的道理，在FP的场景下，我如果修改了funcA,比如说修正了一个bug。我不需要重启整个应用，只要有办法热替换这个funcA就行了（目标：100%在线时间达成！）。</p>

<hr>

<h3 id="sideeffect">如何处理纯函数的side effect</h3>

<p>上面一部分介绍了函数式编程的好处。那么，在一个<strong>真实</strong>的项目里，我们需要把我们的信息打印到某个地方，我们需要读取用户的输入，我们需要通过网络发送请求到其他服务。绝大部分情况下一个<strong>真实</strong>的项目的目的就是对现有资源产生修改：文件修改、数据库修改等。我们怎么可能通过纯函数（没有side effect）来构建一个必然会产生side effect的系统呢？</p>

<p>一个简短的回答是：能，但是他们利用函数式的规则“作弊”了，就像很多数学家一样。</p>

<p>当然，这里的作弊，是指他们利用规则上的漏洞，撑开了足够大的口子（事实上一个大象都能通过的口子），来实现系统。他们主要做了以下两点：</p>

<ul>
<li>依赖注入DI</li>
<li>懒加载</li>
</ul>

<h4 id="">依赖注入</h4>

<p>让我们从一个简单的打日志函数来说起吧：</p>

<pre><code>//version IP
function log(message: string): void {
  const date = new Date().toISOString();
  console.log(`${date}: ${message}`);
}
</code></pre>

<p>很显然，这个函数是不纯的。首先它依赖于系统的时间，其次他依赖于console来进行IO。我们如何把这个改成纯函数呢？答案是<strong>依赖注入</strong>:把所有的依赖变成参数注入进来。</p>

<pre><code>//version FP
function log(message: string, date: Date, cnsl: Console): void {
  const dt = date.toISOString();
  cnsl.log(`${dt}: ${message}`);
}
</code></pre>

<p>乍一看这有点蠢，因为某种意义上说，我们只是把side effect抛给上一层调用方了。然而，仔细想想，这样的改动会带来前一段说过的函数式编成的各种好处。</p>

<p>举个例子：IP版本的<code>log</code>函数是很难测试的，因为你每次调用都会hit到不同的时间值。FP版本的就没有这个问题：</p>

<pre><code>const date = {toISOString: () =&gt; ``};
const cnsl = {
  log: (msg) =&gt; msg
};
log("hello world", date, cnsl);
</code></pre>

<p>在上面的例子中，我们把log转换成了纯函数，不依赖全局变量比如Date、console，只要给定相同的参数祖，一定会得到相同的结果。上面我们轻易的mock了Data和consle。那我们是不能能靠这种转换，创建一个正常的应用呢？理论上是可能的。我们可以用DI，把所有的动态依赖一层一层网上推，直到应用的启动命令为止。</p>

<pre><code>app(new Date(),cnsl,aaa,bbb,ccc,...,blabla);//with 100 other parameters
</code></pre>

<p>有点难以接受是吧，但是，如果说启动命令参数过多是一个可以理解的缺点，另外一个缺点就是过深的参数传递。</p>

<p>以React为例子：React中上下层组件的数据传递是通过constructor注入props或事件回调来实现的。现在我们假设有6层的组件嵌套，第1层组件与6层组件需要处理同一份数据会怎么样？第一层不断的向子组件传递数据，然后子组件向外抛事件，这条组件链上的所有组件都必须要缓存监听这个数据。那么，如果中间有一个组件不小心处理了这个数据并且错了会怎么样？这个时候你只能一层层的往外排查所有的组件，因为整个过程是黑盒，你根本不知道到底是哪个组件搞得事，排查这么多层是不是想想都有点小激动呢？（这也是为什么会有Redux、Mobx的原因之一）</p>

<p>顺便一提React，最近出了个<a href="https://reactjs.org/docs/hooks-intro.html">Hooks</a>来解决类似的，组件嵌套过深时如何传播数据的问题。额，通过建立一个监听所有组件生命周期的hook注入机制（我们函数式编程是不存在全局变量的，这辈子都不会有，嘿嘿）</p>

<h4 id="">懒加载</h4>

<p>让我们先来看这段绕口令：<strong>一个side effect并不是一个side effect直到这个side effect实际被调用</strong>。怎么理解呢，先看如下代码：</p>

<pre><code>function lunch(): void {
  //Detonate a nuclear bomb
}
</code></pre>

<p>一经调用，如假包换的发射一枚核弹。当然，如果我们不想引发doomsday的话，我们可以利用下first-class functions，把这个函数包装下：</p>

<pre><code>function readyToLunch(): Function {
  return lunch;
}
let bomb1 = readyToLunch();
//no nuclear bomb lunched!
let bomb2 = readyToLunch();
bomb1 === bomb2; //true
</code></pre>

<p>你看，我们还顺便验证了这个readyToLunch是个纯函数，即每次调用的返回值是同一个。事实上，我们可以用这种方式构建一个系统执行的链，把系统中的所有逻辑都通过懒加载来实现。</p>

<p>我写了个简单的Chain Promise的例子，项目源代码在<a href="https://gitlab.com/enixjin/chainpromise">这里</a>。达到的效果是这样的。</p>

<pre><code>let chain = ChainPromise.from(promise)
  .map(x =&gt; {
    //remove me to make this function pure
    console.log(x, "+1");
    return x + 1;
  })
  .map(x =&gt; {
    //remove me to make this function pure
    console.log(x, "+2");
    return Promise.resolve(x + 2);
  })
  .flatMap(x =&gt; {
    //remove me to make this function pure
    console.log(x, "*", x);
    return x * x;
  })
  .map(x =&gt; {
    //remove me to make this function pure
    console.log(x, "-5");
    return x - 5;
  });
chain.subscribe(console.log, console.error);
</code></pre>

<p>这一串操作，map或者flatmap之后，所产生的<code>chain</code>是个确定的值。因为每个函数都是懒加载，如果第一个promise没有被resolve或者没有人subscribe，是不会被执行的。没执行=没有side effect，problem resolved！</p>

<p>以下为这个chain promise的代码（也就20行不到而已）：</p>

<pre><code>export class ChainPromise {
  static of = (v: any) =&gt; new ChainPromise(() =&gt; Promise.resolve(v));
  static from = (v: Promise&lt;any&gt;) =&gt; new ChainPromise(() =&gt; v);

  constructor(public f: (...args) =&gt; Promise&lt;any&gt;) {
  }

  map(g): ChainPromise {
    return new ChainPromise(x =&gt; Promise.resolve(this.f(x)).then(v =&gt; g(v)));
  }

  flatMap(g): ChainPromise {
    return new ChainPromise(x =&gt; Promise.resolve(this.f(x))).map(g);
  }

  subscribe(onSuccess?: Function, onFail?: Function): void {
    Promise.resolve(this.f()).then(x =&gt; onSuccess(x)).catch(e =&gt; onFail(e));
  }
}
</code></pre>

<p>这样的懒加载在现实世界里有应用的例子么？假设我们有几千万几亿行数据要处理，FP的纯函数就能帮上大忙了。我们可以用纯函数实现我们自己版本的map和reduce，支持并发计算。当然，在CPU上跑这个是很没效率的，因为最大并发数量被CPU核心所限制（4核、8核、16核）。我们需要把这些纯函数跑在GPU上。（1.我们希望并发越大越好 2.每个计算任务其实不复杂 3.每个计算都是纯函数）</p>

<p>为了在GPU上跑这些计算任务，你就需要切实的描述出这些计算任务，然而又不执行他们，是不是就是上面的chain promise了。理想情况下，我们还需要一个框架来帮我们读取所有数据，分配到GPU上，并收集汇总数据。是的，我说的就是机器学习框架<a href="https://www.tensorflow.org/">TensorFlow</a>。</p>

<p>比如实现两个数字相加的逻辑，在TensorFlow里面是这么写的：</p>

<pre><code>node1 = tf.constant(3.0, tf.float32)
node2 = tf.constant(4.0, tf.float32)
node3 = tf.add(node1, node2)
</code></pre>

<hr>

<h3 id="">总结</h3>

<p>函数式编程是个好东西。虽然我们几乎不可能在一个真实的系统中完全以纯函数来实现，但这并不妨碍我们以函数式编程来实现我们系统中的某些组件，并享受它带来的各种好处。</p>]]></content:encoded></item><item><title><![CDATA[API设计，从RPC、SOAP、REST到GraphQL（四）]]></title><description><![CDATA[<p>前两篇：</p>

<p>（一）<a href="https://blog.enixjin.net/api_design_part1/">前言&amp;RPC</a></p>

<p>（二）<a href="https://blog.enixjin.net/api_design_part2/">SOAP</a></p>

<p>（三）<a href="https://blog.enixjin.net/api_design_part2/">REST</a></p>

<h3 id="graphqlnext">（四）GraphQL（Next？）</h3>

<blockquote>
  <p>Talk is cheap. Show me the code.</p>
</blockquote>

<p>先放个GraphQL的代码，在<a href="https://gitlab.com/enixjin/graphql">这里</a>。（竟然是一个月前的代码了，写文章还是不能拖拉啊）</p>

<p>如同上一篇文章所说，REST是现在API设计的绝对主流思想，但也不是没有缺点的。比如太多的HTTP请求，抓取了过多的信息等。当然，现在有好多各种各样的框架、协议尝试对REST做一些改进。比如<a href="http://stateless.co/hal_specification.html">HAL</a>、<a href="https://swagger.io">Swagger</a>、<a href="http://www.odata.org">OData JSON API</a>。（以上几个都可以花时间浏览下，还是很有启发的）</p>

<p>还有很多小的工程或者项目的内部工具都是为了更加爽快的使用REST而诞生的。我不想一一列举这些工具，这里只是从设计角度，假设我们自己要设计一个基于REST的改进工具，我们会对这个工具有那些要求：</p>

<h4 id="1http">1.最小化HTTP请求数量</h4>

<p>每个HTTP请求都是有代价的，握手、延迟、</p>]]></description><link>http://blog.enixjin.net/api_design_part4/</link><guid isPermaLink="false">cc7940f8-cb2b-474a-b86a-aeff6c783785</guid><category><![CDATA[技术]]></category><dc:creator><![CDATA[Enix Jin]]></dc:creator><pubDate>Mon, 09 Jul 2018 05:22:31 GMT</pubDate><media:content url="http://blog.enixjin.net/content/images/2018/06/og_image.png" medium="image"/><content:encoded><![CDATA[<img src="http://blog.enixjin.net/content/images/2018/06/og_image.png" alt="API设计，从RPC、SOAP、REST到GraphQL（四）"><p>前两篇：</p>

<p>（一）<a href="https://blog.enixjin.net/api_design_part1/">前言&amp;RPC</a></p>

<p>（二）<a href="https://blog.enixjin.net/api_design_part2/">SOAP</a></p>

<p>（三）<a href="https://blog.enixjin.net/api_design_part2/">REST</a></p>

<h3 id="graphqlnext">（四）GraphQL（Next？）</h3>

<blockquote>
  <p>Talk is cheap. Show me the code.</p>
</blockquote>

<p>先放个GraphQL的代码，在<a href="https://gitlab.com/enixjin/graphql">这里</a>。（竟然是一个月前的代码了，写文章还是不能拖拉啊）</p>

<p>如同上一篇文章所说，REST是现在API设计的绝对主流思想，但也不是没有缺点的。比如太多的HTTP请求，抓取了过多的信息等。当然，现在有好多各种各样的框架、协议尝试对REST做一些改进。比如<a href="http://stateless.co/hal_specification.html">HAL</a>、<a href="https://swagger.io">Swagger</a>、<a href="http://www.odata.org">OData JSON API</a>。（以上几个都可以花时间浏览下，还是很有启发的）</p>

<p>还有很多小的工程或者项目的内部工具都是为了更加爽快的使用REST而诞生的。我不想一一列举这些工具，这里只是从设计角度，假设我们自己要设计一个基于REST的改进工具，我们会对这个工具有那些要求：</p>

<h4 id="1http">1.最小化HTTP请求数量</h4>

<p>每个HTTP请求都是有代价的，握手、延迟、解析甚至如果在移动平台上过多HTTP请求对手机电池的消耗都是必须考虑的。理想情况下，我们的新API设计，需要允许客户端需要有能力在最小的HTTP请求数中，得到需要的数据。</p>

<h4 id="2payload">2.最小化payload</h4>

<p>和上个目标一样，受限于带宽、CPU、内存等等因素，理想情况下，我们的新API设计，需要能够发送且只发送客户端需要的最小数据。当然，为了实现这个目标，客户端需要有能力描述要求的数据内容。</p>

<h4 id="3">3.可读性要强</h4>

<p>从SOAP中获取的教训，API的交互要简单。对程序员来说，如果能用<code>curl</code>、<code>wget</code>或者类似的简单工具直接交互才是最好的。</p>

<h4 id="4">4.类型支持</h4>

<p>另外一个我们从RPC和SOAP中学到的教训是，服务器和客户端之间的协议（contracts）非常有用。因为如果用JSON的话，所有字段本质上都是string类型传输的，如果有强类型系统（type systems）来做检测就更好了。顺便一提，这也是为什么我一直支持typescript开发nodejs系统的原因，在正式、多人的项目里，强类型系统对开发的帮助是在是太重要了。</p>

<p>比如说，我们可以定义博客的数据结构：</p>

<pre><code>type posts {
  title:String
  description:String
  create_date:Date
  tags:[String]
  author:User
}
</code></pre>

<h4 id="5">5.查询语言的支持</h4>

<p>多年来，设计大型系统时一个非常重要的原则就是“separation of concerns”。然而，不幸的是，大部分系统，最后都变成了拥有一个暴复杂逻辑的中央数据层的怪物。假设子系统A、B、C需要用户数据，但是他们对数据的形状什么的都有各自的特殊要求。理想的设计是提供一个统一的，支持对数据的形状进行裁剪、过滤的接口。这么看来，一种简单的查询语言是最符合我们要求的。</p>

<p>理论上SQL是符合我们的要求的，唯一的问题是SQL太复杂，而且和基本上已经成为事实数据格式标准的JSON不是太友好。我们可以考虑类似mongodb的做法，以选择特定field为例：</p>

<pre><code>select title,description from posts; // SQL

db.posts.find(
  { },
  { title: 1, description: 1, _id: 0 }
)  //mongodb
</code></pre>

<p>所以，我们的新查询语言可以是这样的：</p>

<pre><code>{
  posts {
    title
    description
    author {
      name
    }
  }
}
</code></pre>

<p>恩，事实上，上面的简单<strong>类型系统</strong>加上这个简单的<strong>查询语言</strong>，就是GraphQL了。记住，GraphQL只是一个specification，只是一个规格定义，并不牵涉到任何具体的语言、实现、客户端。在我的<a href="https://gitlab.com/enixjin/graphql">这个小例子</a>里面,语言上我用了typescript，框架上用了express，数据库只是内存里的一个数组而已。</p>

<p><img src="http://blog.enixjin.net/content/images/2018/07/image.png" alt="API设计，从RPC、SOAP、REST到GraphQL（四）"></p>

<p>引入GraphQL并不意味着需要改写现有的系统接口，GraphQL可以看做一个前端和后端之间的灵活有效的业务逻辑层。后面的数据源可以是一个SQL数据库、一个Microservice、甚至是另外一个API。</p>

<hr>

<p>对代码的一些说明：</p>

<p><a href="https://gitlab.com/enixjin/graphql/blob/master/src/server.ts">server.ts</a>:</p>

<pre><code>// check auth here
let checkAuth = (req, res, next) =&gt; {
  // console.log(req.headers.jwt);
  next();
};

app.use('/graphql', checkAuth, graphqlHTTP({
  schema: schema,
  // rootValue: root,
  graphiql: true //Set to false if you don't want graphiql enabled
}));
</code></pre>

<p>实现了一个简单的权限校验函数<code>checkAuth</code>,引入了schema定义，在<code>/graphql</code>开放了网页版测试客户端。</p>

<p><a href="https://gitlab.com/enixjin/graphql/blob/master/src/type/UserType.ts">UserType.ts</a>:</p>

<pre><code>export const UserType = new GraphQLObjectType({
  name: "user",
  fields: {
    id: {
        type: new GraphQLNonNull(GraphQLInt),
        description: "id of user"
    },
    username: {
        type: new GraphQLNonNull(GraphQLString),
        description: "username of user"
    },
    password: {
        type: new GraphQLNonNull(GraphQLString),
        args: {
            "hide": {
                type: GraphQLBoolean,
                defaultValue: true
            }
        },
        description: "password of user, default is hide",
        resolve: (_, { hide }) =&gt; {
            if (hide) {
                return "******";
            } else {
                return _.password;
            }
        }
    }
  }
});
</code></pre>

<p>用户的强类型定义，等同于官方的<a href="https://graphql.org/learn/schema/#type-language">这种</a>定义方式。一个比较有趣的地方是我在这个定义里面默认隐藏了密码字段的值，只有当你覆盖默认设置<code>password(hide:false)</code>时才会显示密码。</p>

<p><a href="https://gitlab.com/enixjin/graphql/blob/master/src/query/UserQuery.ts">UserQuery.ts</a>:</p>

<pre><code>export let userQuery = () =&gt; ({
    users: {
        type: new GraphQLList(UserType),
        resolve: () =&gt; {
            return db;
        }
    },
    user: {
        type: UserType,
        args: {
            "id": {
                type: new GraphQLNonNull(GraphQLInt)
            }
        },
        resolve: (_, {
            id
        }) =&gt; {
            return db.find(u =&gt; u.id === id);
        }
    }
});
</code></pre>

<p>定义了我们这个graphql会暴露2个查询接口，现实所有用户的<code>users</code>和按照id查询的单个用户查询<code>user</code>。</p>

<p><img src="http://blog.enixjin.net/content/images/2018/07/image-6.png" alt="API设计，从RPC、SOAP、REST到GraphQL（四）"></p>

<hr>

<p>GraphQL虽然是一种强大而灵活的工具，优点已经在上面阐述过了。然而，公平的说，GraphQL也有自己的缺点：</p>

<h4 id="1">1.数据集成和错误处理</h4>

<p>因为很多时候，GraphQL在系统里起到了数据网关、Gateway的作用。不同的数据源如何整合、异步的数据处理、各个不同系统之间混乱的架构关系等都是不小的挑战。另外、如果后台的部分数据服务不可用，该如何报错？REST的用HTTP status code来表示状态的方法无法沿用到整合了多个后台数据源的GraphQL上。</p>

<h4 id="2cache">2.Cache</h4>

<p>高度灵活的动态查询意味着难以缓存，你可能需要使用类似Apollo提供的<a href="https://www.apollographql.com/docs/react/advanced/caching.html">Cache</a>一样，设计一套复杂的逻辑。</p>

<h4 id="3cpu">3.CPU消耗</h4>

<p>解析、验证、类型检测一个查询语句是一个非常消耗CPU的动作。特别是在NodeJS这种默认单CPU的环境下面</p>

<hr>

<p><strong>总结</strong>：</p>

<p>API设计的选型对整个系统的健壮性、可扩展性是影响非常大的。挑选一个合适当前项目的设计是架构师的主要工作之一。不要把自己限制在某个特定的设计上，合适才是最重要的。如果我要给一个状态机做接口，明显RPC风格的更合适；如果大部分是数据的CRUD，那一定要用REST。如果都有，混合两种风格的API也未尝不可。</p>]]></content:encoded></item><item><title><![CDATA[API设计，从RPC、SOAP、REST到GraphQL（三）]]></title><description><![CDATA[<p>前两篇：</p>

<p>（一）<a href="https://blog.enixjin.net/api_design_part1/">前言&amp;RPC</a></p>

<p>（二）<a href="https://blog.enixjin.net/api_design_part2/">SOAP</a></p>

<h3 id="restnow">（三）REST（now）</h3>

<p>接下来，在RPC和SOAP之后，我们迎来了REST。</p>

<p>REST（<strong>Re</strong>presentational <strong>S</strong>tate <strong>T</strong>ransfer），实际上一般指的是RESTful的架构风格，是由Roy Fielding在2000年的<a href="https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm">这篇</a>著名论文里提出的。</p>

<p>那REST到底对API设计做了什么改进呢？先来回顾一下前面两篇文章里介绍的RPC和SOAP，RPC类似于系统和系统之间讲黑话，SOAP改进了RPC，使之有了统一“普通话”。但是“普通话”是不是最优解呢？我们来看一个调用远程系统创建用户时可能遇到的接口名：</p>

<pre><code>createUser() //很平常的命名

insertUser() //也可以接受

chuangjianyonghu() //别说你没看到过

chuangjianUser() //土洋结合

cjyh() //中文拼音首字母缩写

api123321() //恩，保密工作做的不错？
</code></pre>

<p>根本就有无穷种可能性好伐？那么，</p>]]></description><link>http://blog.enixjin.net/api_design_part3/</link><guid isPermaLink="false">d65e0709-bf6a-4ceb-966d-437efda881d3</guid><category><![CDATA[技术]]></category><dc:creator><![CDATA[Enix Jin]]></dc:creator><pubDate>Fri, 22 Jun 2018 01:23:18 GMT</pubDate><media:content url="http://blog.enixjin.net/content/images/2018/06/rest.jpg" medium="image"/><content:encoded><![CDATA[<img src="http://blog.enixjin.net/content/images/2018/06/rest.jpg" alt="API设计，从RPC、SOAP、REST到GraphQL（三）"><p>前两篇：</p>

<p>（一）<a href="https://blog.enixjin.net/api_design_part1/">前言&amp;RPC</a></p>

<p>（二）<a href="https://blog.enixjin.net/api_design_part2/">SOAP</a></p>

<h3 id="restnow">（三）REST（now）</h3>

<p>接下来，在RPC和SOAP之后，我们迎来了REST。</p>

<p>REST（<strong>Re</strong>presentational <strong>S</strong>tate <strong>T</strong>ransfer），实际上一般指的是RESTful的架构风格，是由Roy Fielding在2000年的<a href="https://www.ics.uci.edu/~fielding/pubs/dissertation/rest_arch_style.htm">这篇</a>著名论文里提出的。</p>

<p>那REST到底对API设计做了什么改进呢？先来回顾一下前面两篇文章里介绍的RPC和SOAP，RPC类似于系统和系统之间讲黑话，SOAP改进了RPC，使之有了统一“普通话”。但是“普通话”是不是最优解呢？我们来看一个调用远程系统创建用户时可能遇到的接口名：</p>

<pre><code>createUser() //很平常的命名

insertUser() //也可以接受

chuangjianyonghu() //别说你没看到过

chuangjianUser() //土洋结合

cjyh() //中文拼音首字母缩写

api123321() //恩，保密工作做的不错？
</code></pre>

<p>根本就有无穷种可能性好伐？那么，我们能不能有一个接口的规范？比如，对于任何系统，暴露出来的创建用户一定是<code>createUser</code>？REST以一种革命性的抽象，巧妙的解决了这个问题。</p>

<p>SOAP虽然工作在HTTP之上，但实际上并没有好好的利用HTTP，所有的内容都在request和response的body里面。那如何好好利用HTTP协议本身呢？</p>

<p>答案就是：面向资源（<strong>resource</strong>）</p>

<p>加上利用HTTP协议本身。包括协议中的8种方法：OPTIONS,GET,HEAD,POST,PUT,DELETE, TRACE,CONNECT，REST, 以及HTTP response status codes：2xx,4xx,5xx，HTTP Header的定义，HTTP 是无状态的等特性。</p>

<p>其中最最关键的抽象是把系统的交互看成一个<strong>对有名字的资源的访问</strong>。</p>

<p>比如上面用户相关的REST接口：</p>

<pre><code>{GET}     /users            //获取用户列表，返回User[]
{POST}    /users            //创建用户,返回id
{GET}     /users/1          //获取id为1的用户信息,返回User
{PUT}     /users/1          //更新id为1的用户信息,返回User
{DELETE}  /users/1          //删除id为1的用户信息,无返回或者返回是否命中
{GET}     /users?name=enix  //搜索名字为enix的用户列表,返回User[]
......
</code></pre>

<p>你看，他即不是黑话，也不是自由度很高的普通话，而是在现有的、已经被广泛使用的一种通信规范（HTTP）的基础上来设计接口。用一个URI（Uniform Resource Identifiers），上面例子里面的/users来代表user这个资源，所有的接口都利用HTTP的特性在这个URI上做文章。如果你遵守REST的规范来设计接口的话，所有人设计出来的API基本都是一样的。</p>

<p>网上介绍REST的文章太多了，这里只提一点，不要设计出RPC风格的伪REST接口。下面是两个例子。</p>

<hr>

<p>给用户发送消息的接口,很多系统会设计成这样：</p>

<pre><code>{POST}   /sendSMSMessageToUser
body:{"userid":1,"message":"hello there"}
</code></pre>

<p>在思想上，这还是个<strong>面向方法</strong>的RPC接口，我们需要转换思想，<strong>面向资源</strong>。</p>

<p>正确的REST style的方法是这样的：</p>

<pre><code>{POST}   /users/1/message
body:{"message":"hello there"}
</code></pre>

<p>虽然这两种方式看起来很像，但是我们的关注点从sendXXXX这个方法，转移到了user的message这个resource上面了。（另外，后面的例子里面把sms也去掉了，因为这是server端的问题，client端只需要表达需要给用户发送消息就行了，至于是email还是sms，只是server端的实现细节罢了）</p>

<hr>

<p>另外一个经典的伪REST接口类型：</p>

<pre><code>{POST} /users/login
body:{"username":"enix","password":123456}
</code></pre>

<p>恩，这是一个很普通的用户登录接口，但是很多系统里面，这样的登录接口，不管成功失败，返回的HTTP status code统一都是200！！！</p>

<pre><code>HTTP status 200 body:{success:true,data:{...}}     //登录成功
HTTP status 200 body:{success:false,data:{...}}    //登录失败
</code></pre>

<p>上面响应技术上讲叫做envelope（还记得SOAP的xml里面的SOAP-ENV:Envelope吗？），就是把所有结果包在一个信封里面，信封外面写着远程调用的结果，一般是成功或者失败，实际响应内容放到子一级的data里。所以这还是一个RPC style的接口！</p>

<p>而正确的接口应该这么返回：</p>

<pre><code>HTTP status 200 body:{token:"..."}    //登录成功
HTTP status 403 body:{message:"..."}  //登录失败
</code></pre>

<p><strong>Remember, HTTP status code is your envelope in REST!!!</strong></p>

<hr>

<h4 id="rest">REST的优点</h4>

<p><strong>Uniform Interface</strong>：这个前面已经提到过了，系统之间交互，有一个可预期的接口实在是太重要了。</p>

<p><strong>Stateless</strong>：曾几何时，系统扩容绕不开的一个话题就是session同步。HTTP请求本身就是无状态的，怎么保持用户的登录状态呢？大部分系统使用了session机制，也就是浏览器里面记录一个sessionid，对应服务器里面的一块内存，内存里面记录了用户的信息。这个机制在单服务器的时候工作完美，但是当服务器需要扩展性能时，比方说前面加了个负载均衡，后面变成了两台服务器时，就会带来一个问题：如何在保持session同步？要么两个服务器内存同步要么存到第三方（memcache或者redis），比如下面这个典型的配置：
<img src="http://blog.enixjin.net/content/images/2018/06/image-5.png" alt="API设计，从RPC、SOAP、REST到GraphQL（三）">
而REST接口是无状态的，依赖header里面token，每次都实时解析（<a href="https://jwt.io/">JWT</a>了解一下），虽然略微牺牲了一点性能（其实绝大部分情况只是把header里面的“token” decode&amp;verify一下，不会牵涉到数据库读取，相比去memcache里面同步session，未必性能更差），但是在扩展性能的时候实在太方便了！性能不够？实时顶几台服务器上去。性能过剩？随便下线几台服务器也不在话下。这是热插拔，少了启动时要去cache同步所有session的这个预热过程。</p>

<p><strong>Cache</strong>：过去，你可能需要在应用级别（上图的每个instance）里自己实现cache逻辑。如果使用REST的话，cache的相关处理就能提到更高级的地方（load balancing）来做了。为什么呢？因为所有的资源访问都暴露在HTTP层了。比如前面那个例子里的<code>/users</code>，<code>{GET} /users/:id</code>这个URL是可以被cache的，只有当有人去<code>POST</code>或者<code>PUT</code>、<code>DELETE</code>过这个URL时，才需要刷新缓存。更妙的是，应用层根本不需要再去关心缓存了，不需要到处写<code>@Cacheable("users")</code>了，完全可以交给前面的一层去做！</p>

<hr>

<h4 id="rest">REST的缺点</h4>

<p><strong>健谈</strong>：和SOAP的罗嗦（报文体积大）不同，REST接口倾向于健谈（发送请求多）。这其实和现在的前后端划分有关系。以前，大家都利用jsp asp php等类似的技术，在服务端组织数据、生成页面，然后返回给浏览器。现在，因为不止有web端，我们可能还有手机app端、微信端、桌面端，给每个程序开发一套接口是很不经济的。统一接口使用REST的代价就是请求变多了。不再是后端算好所有东西给前端了，前端也有了自己的框架（angular react）和自己的逻辑，后端只是提供数据，也就是资源给前端了。以前一个HTTP请求就能搞定的事情，现在可能要发好多个。特别是在<a href="https://http2.github.io/faq/">HTTP/2</a>还没有完全普及的现在，浏览器同时发出的请求数量是有限制的（HTTP/1.1可以看<a href="https://docs.pushtechnology.com/cloud/latest/manual/html/designguide/solution/support/connection_limitations.html">这里</a>的表格）。</p>

<p><strong>TMI</strong>：Too Many Information或者说Overfetching。比如说我有一个博客的首页，我需要load一个post的列表，我会发一个简单的list请求</p>

<pre><code>{GET}     /posts
</code></pre>

<p>返回的结果可能是</p>

<pre><code>HTTP status 200
body:[
  {
      "title" : "hello",
      "author" : {...},
      "description" : "world",
      ...   //以及其他20个字段
  },
  ...
]
</code></pre>

<p>作为一个blog的首页，我可能只需要<code>title</code>、<code>author</code>、<code>description</code>这三个字段就行了，另外20个字段我完全不关心啊。当然，你可以用<a href="http://jsonapi.org/format/#fetching-sparse-fieldsets">sparse fieldsets</a>或者类似的技术来避免这个问题。然而这个问题是REST面向资源的设计的nature所带来的，很难彻底避免（GraphQL对REST的改进之一就是为了解决这个痛点）。</p>

<hr>

<p>扩展阅读：</p>

<p>REST论文的主要作者在2017年的<a href="https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/46310.pdf">Reflections on the REST Architectural Style and “Principled Design of the Modern Web Architecture”</a></p>

<p>一个哥们写了一篇黑REST的文章<a href="https://medium.freecodecamp.org/rest-is-the-new-soap-97ff6c09896d">REST is the new SOAP</a></p>

<p>然后被另外一个哥们的一篇<a href="https://philsturgeon.uk/api/2017/12/18/rest-confusion-explained/">A Response to REST is the new SOAP</a>驳得体无完肤，还是很有趣的。</p>

<hr>

<p>下一篇预告：<a href="https://blog.enixjin.net/api_design_part4/"><strong>GraphQL</strong></a></p>]]></content:encoded></item><item><title><![CDATA[API设计，从RPC、SOAP、REST到GraphQL（二）]]></title><description><![CDATA[<blockquote>
  <p>SOAP is what most people would consider a moderate success.</p>
  
  <p>-- Don Box &lt;<a href="https://www.xml.com/pub/a/ws/2001/04/04/soap.html">A Brief History of SOAP</a>></p>
</blockquote>

<p>上一篇：<a href="https://blog.enixjin.net/api_design_part1/">前言&amp;RPC</a></p>

<h3 id="soap90s">（二）SOAP（90s）</h3>

<p>RPC之后的下一个主流API设计风潮就是SOAP了。</p>

<p>SOAP（Simple Object Access Protocol）由Dave Winer, Don Box, Bob Atkinson, 和 Mohsen Al-Ghosein 在1998年为微软所设计。</p>

<p>这几个设计人员敏锐的观察到了RPC的最大缺陷：没有统一标准。使用RPC就好比发明团伙内部的黑话一样，更精简、更加保密、更加可定制，坏处就是要求双方（sender,</p>]]></description><link>http://blog.enixjin.net/api_design_part2/</link><guid isPermaLink="false">f780bb70-2737-47ab-bfd7-258d730c35f3</guid><category><![CDATA[技术]]></category><dc:creator><![CDATA[Enix Jin]]></dc:creator><pubDate>Mon, 11 Jun 2018 14:37:23 GMT</pubDate><media:content url="http://blog.enixjin.net/content/images/2018/06/bar-soap-liquid-soap-16x9.jpg" medium="image"/><content:encoded><![CDATA[<blockquote>
  <img src="http://blog.enixjin.net/content/images/2018/06/bar-soap-liquid-soap-16x9.jpg" alt="API设计，从RPC、SOAP、REST到GraphQL（二）"><p>SOAP is what most people would consider a moderate success.</p>
  
  <p>-- Don Box &lt;<a href="https://www.xml.com/pub/a/ws/2001/04/04/soap.html">A Brief History of SOAP</a>></p>
</blockquote>

<p>上一篇：<a href="https://blog.enixjin.net/api_design_part1/">前言&amp;RPC</a></p>

<h3 id="soap90s">（二）SOAP（90s）</h3>

<p>RPC之后的下一个主流API设计风潮就是SOAP了。</p>

<p>SOAP（Simple Object Access Protocol）由Dave Winer, Don Box, Bob Atkinson, 和 Mohsen Al-Ghosein 在1998年为微软所设计。</p>

<p>这几个设计人员敏锐的观察到了RPC的最大缺陷：没有统一标准。使用RPC就好比发明团伙内部的黑话一样，更精简、更加保密、更加可定制，坏处就是要求双方（sender,receiver）也要懂黑话，而且一旦大家都说一种黑话了，换黑话就困难了。（是不是能联想到某些RPC框架了？）</p>

<p>于是，这个很有雄心的协议使用了XML来进行应用间通信，相当于说：“大家都别说各自的黑话了，都说用我这个普通话交流”。实际上，由于已经有XML-RPC（微软做事比较拖拉，前面的Dave Winer等不急在1998年提交的），只要给XML添加一个行为类型系统就能定义出这种“普通话”了。</p>

<p><img src="http://blog.enixjin.net/content/images/2018/06/image.png" alt="API设计，从RPC、SOAP、REST到GraphQL（二）">
<img src="http://blog.enixjin.net/content/images/2018/06/image-3.png" alt="API设计，从RPC、SOAP、REST到GraphQL（二）"></p>

<p>时至今日，在这种“普通话”上已经发展出了一套复杂的技术链，你可能已经想到了，就是Web Service。由WSDL（Web Services Description Language）提供接口功能说明、由XML schema定义XML的结构类型、由UDDI（Universal Description Discovery and Integration）来做服务发现。</p>

<p>Web Service火了一段时间。然而，可悲的是，人们发现它创造的问题比解决的多。。。所以，现在，你会发现，现在越来越少的新接口是使用SOAP开发的了。</p>

<hr>

<h4 id="soap">还是先说下SOAP的优点吧：</h4>

<p>SOAP最大的好处：<strong>标准化了接口</strong>。相比于直接野蛮调用远程方法，SOAP给出了一个平台无关的、可预期的、类型安全的接口描述。WSDL用XML格式描述了哪个服务器提供什么服务，怎样找到它，以及该服务使用怎样的接口规范。从自己和对方沟通确定这个RPC应该用什么黑话来沟通相比，使用SOAP有一个可预期的标准流程：</p>

<ol>
<li>获得该服务的WSDL描述  </li>
<li>根据WSDL构造一条格式化的SOAP请求发送给服务器  </li>
<li>接收一条同样SOAP格式的应答  </li>
<li>根据先前的WSDL解码数据</li>
</ol>

<p>这一统一，使得大部分语言和他们的IDE都实现直接从WSDL里面生成文档和访问代码。</p>

<p>另外一个好处是鼓励了<strong>从面向方法到面向接口</strong>的转换。虽然这可能并不是SOAP设计目的，但接口的定义的存在，或多或少的把原先RPC粗暴的<strong>暴露方法</strong>变成了<strong>暴露接口</strong>。面向接口编程的好处是不言而喻的（文章太多，感兴趣可自行搜索）。同时，面向接口使得不同语言，不同技术的互相访问变的更便捷了。你只要暴露出你服务的wsdl就行了，我可以根据定义用我自己的语言来开发，终于不用再调用别人的sdk了。</p>

<hr>

<h4 id="soap">SOAP的缺点也同样耀眼：</h4>

<p>SOAP是一种<strong>罗嗦，且难以置信的罗嗦</strong>的接口。你几乎无法在没有<strong>一大堆工具</strong>的帮助下使用SOAP。你需要工具来写测试、你需要工具来监听请求和响应、你需要工具来解析XML、你需要工具从XML中按照定义再解析出需要的数据，等等、等等。如果有一个REST的接口，我打开浏览器，一分钟之内就可以测好，如果是SOAP的话。。。今天仍然有好多老系统在用SOAP，但是当你开发一个新系统时，你会发现如果使用Web Service太过于笨重了。</p>

<p>SOAP的罗嗦也带来了另外一个问题：<strong>理论性能不佳</strong>。比如说访问一个远程的方法getOpenData，理论上我只需要发送下列信息到endpoint：(伪代码)</p>

<pre><code>getOpenData?name=enix  
</code></pre>

<p>而如果你用SOAP的话...</p>

<pre><code>&lt;?xml version = "1.0"?&gt;  
&lt;SOAP-ENV:Envelope  
   xmlns:SOAP-ENV = "http://www.w3.org/2001/12/soap-envelope"
   SOAP-ENV:encodingStyle = "http://www.w3.org/2001/12/soap-encoding"&gt;
   &lt;SOAP-ENV:Body xmlns:m = "http://www.xyz.org/quotations"&gt;
      &lt;m:getOpenData&gt;
         &lt;m:name&gt;enix&lt;/m:name&gt;
      &lt;/m:getOpenData&gt;
   &lt;/SOAP-ENV:Body&gt;
&lt;/SOAP-ENV:Envelope&gt;  
</code></pre>

<p>而且，请记住，返回值也一定是这么一坨XML。在以前网速慢、流量贵的时候，这样发来发去简直就是犯罪！！！另外，解析、构建XML都不是一件简单的事情（SAX还是DOM？），解析和构建也非常消耗性能！！！</p>

<p>最后也是最重要的，SOAP本质上只是<strong>RPC的一种扩展及优化</strong>罢了。设计思路上仍然是以方法为中心的，比如上面例子中的getOpenData，本质上还是服务方的一个非常底层的方法暴露给调用方而已。系统还是存在强耦合性。</p>

<p>PLUS：这里有一段Java vs .Net的视频<a href="https://www.youtube.com/watch?v=kLO1djacsfg">Java 4-EVER</a>（墙外），里面爸爸声嘶力竭试图挽回学JAVA的孩子时喊出的“They(MS) actually enable us to send XML-message through SOAP! Through SOAP”很有喜感。</p>

<p>下一篇预告：<a href="https://blog.enixjin.net/api_design_part3/"><strong>REST</strong></a></p>]]></content:encoded></item><item><title><![CDATA[API设计，从RPC、SOAP、REST到GraphQL（一）]]></title><description><![CDATA[API Design, from RPC, SOAP, REST to GraphQL]]></description><link>http://blog.enixjin.net/api_design_part1/</link><guid isPermaLink="false">7323d8e7-64ac-4ab9-8309-34857ac382a7</guid><category><![CDATA[技术]]></category><dc:creator><![CDATA[Enix Jin]]></dc:creator><pubDate>Sun, 27 May 2018 13:57:00 GMT</pubDate><media:content url="http://blog.enixjin.net/content/images/2018/05/migration-microservices-to-graphql-4-638.jpg" medium="image"/><content:encoded><![CDATA[<blockquote>
  <img src="http://blog.enixjin.net/content/images/2018/05/migration-microservices-to-graphql-4-638.jpg" alt="API设计，从RPC、SOAP、REST到GraphQL（一）"><p>"There is no such thing as a new idea. It is impossible. We simply take a lot of old ideas and put them into a sort of mental kaleidoscope."</p>
  
  <p>-- 马克·吐温 &lt;<a href="https://www.amazon.cn/dp/B01HESPEGU/ref=sr_1_1?ie=UTF8&amp;qid=1527246348&amp;sr=8-1&amp;keywords=%E9%A9%AC%E5%85%8B%E5%90%90%E6%B8%A9%E8%87%AA%E4%BC%A0">马克·吐温自传</a>></p>
</blockquote>

<p>我自己的几个工具（<a href="https://www.npmjs.com/package/@jinyexin/core">@jinyexin/core</a>, <a href="https://www.npmjs.com/package/%40jinyexin%2Fcorecli">@jinyexin/corecli</a>, <a href="https://www.npmjs.com/package/@jinyexin/wechat">@jinyexin/wechat</a>）是基于REST设计的，在定义应该由框架自动生成哪些REST接口的时候，总是感觉到有很多<strong>痛点</strong>（下文谈到REST的时候会讲）。于是GraphQL进入了我的视线。</p>

<p>为了比较各种API设计的优劣，我想先回顾下历史上各个时代的API设计理念，从而理解为什么我们需要“又”一种新的API设计方式。</p>

<p>历史上，按照时间的先后，大致出现过4种主流的设计：</p>

<ul>
<li>RPC</li>
<li>SOAP</li>
<li>REST</li>
<li>GraphQL</li>
</ul>

<hr>

<h3 id="">前言：新技术的成本核算</h3>

<p>新技术是总让人着迷的，但在给一个组织、一个团队或者一个框架引入一种新的技术或工具时，有很多东西需要衡量。比如：学习的成本、把旧的功能在将来的某个时间点转换成新技术的成本、某段时间内维护新旧两套技术的成本等。有了这么多成本的考量，新技术必须<strong>显著</strong>的“更快、更高、更强”才行。（奥利匹克格言）</p>

<p>GraphQL在大部分应用场景中，在我看来，是利大于弊的。</p>

<hr>

<h3 id="rpc60s90s">（一）RPC（远古时期，60s-90s）</h3>

<p><strong>RPC</strong>(Remote Procedure Call), 中文“远程方法调用”。几乎可以算是一种最最古老的API设计模式了。</p>

<p>60年代，计算机刚刚诞生的时候，每台计算机都有几间房间那么大。（参见下图）</p>

<p><img src="http://blog.enixjin.net/content/images/2018/05/image.png" alt="API设计，从RPC、SOAP、REST到GraphQL（一）"></p>

<p>少的可怜的带宽和计算能力、计算机数量使得当时的工程师优先考虑远程方法调用，而不是互相暴露的数据接口这种更通用的方法。（API驱动的开发当时还只是理论上的事情）</p>

<p>一直到90年代为止，以<strong>CORBA</strong>（<a href="https://en.wikipedia.org/wiki/Common_Object_Request_Broker_Architecture">Common Object Request Broker Architecture</a>），Java的RMI（<a href="https://en.wikipedia.org/wiki/Java_remote_method_invocation">Remote Method Invocation</a>）为代表，绝大部分计算机都以RPC这种交互模式工作。</p>

<p>RPC有自己的优点。对于开发人员来说，RPC的设计理念是使<strong>远程方法调用体验、写法都和本地方法调用一样</strong>。RPC屏蔽了网络细节，虽然缓慢得多且不太可靠，但RPC使得跨系统的异步调用保持了代码连续性。</p>

<p>请注意，直到今天为止，在执行某些不可靠或者异步的操作时(数据库操作、外部HTTP服务调用)，我们仍然为了保持代码的连续性而在奋斗（如：<a href="https://blog.enixjin.net/you-still-need-promise-even-have-async-and-await-in-javascript/">有了async/await,你仍然需要promise</a>）</p>

<p>RPC的另外一个优点是<strong>对数据结构没有任何限制</strong>，RPC的关注点是方法，只要方法需要，任何格式或者结构的数据都能传给远程方法，任何格式或者结构的数据都能被远程方法返回，非常灵活。RPC的灵活也带来了性能方面的优势，相比于固定于XML格式的SOAP，限制在HTTP上的REST、GraphQL。RPC可以任何你认为合理的方式来实现，在追求极致性能的场合下，还是有很大的用武之地的。</p>

<p>然而，RPC的优点也造成了RPC的缺点。</p>

<p>首先，<strong>保持代码的连续性需要上下文</strong>。RPC从设计上，就混淆了本地代码和远程代码，把本地系统和远程系统紧紧的耦合在一起。还记得高内聚低耦合（High cohesion &amp; Low coupling）的设计标准么？很长一段时间，访问远程系统的主流方式是引入对方的SDK。</p>

<p>另外，相对于更关注于数据的接口设计方式，这种高耦合的还让开发人员很难区分本地和远程代码的边界。我需要知道这么多远程系统的代码级实现细节吗？我需要捕获发生在远程系统错误吗？怎么处理这些错误？如果远程系统没有响应怎么办？相应调用失败后如何重试？</p>

<p>RPC的另外一个重要缺点是<strong>API的扩散</strong>。理论上来说，一个RPC服务应该暴露一组少量且完备的API，足以且只足以让客户端完成指定任务。然而，大量的调用节点（方法），几乎没有数据结构，使得这项任何非常有挑战性。在实际的项目中，特别是长期大的项目或者开发人员变动时，很难使这些暴露给客户端的方法地稳定且保持在一个合理的数量上。</p>

<p>另外，RPC也<strong>缺少统一的标准</strong>。RPC的核心模块：通讯、序列化，通讯可以TCP可以HTTP，序列化和反序列化更有无数的坑。微服务名著<a href="http://shop.oreilly.com/product/0636920033158.do">Building Microservices</a>里面就在一直吐槽RPC的坑和缺点。</p>

<p>下集预告：<a href="https://blog.enixjin.net/api_design_part2/"><strong>SOAP</strong></a></p>]]></content:encoded></item><item><title><![CDATA[一个简单的用TS实现的DI]]></title><description><![CDATA[<p>把原先项目里自己顺手实现的简单DI抽了出来</p>

<p><a href="https://github.com/enixjin/simpleDependencyInjection">https://github.com/enixjin/simpleDependencyInjection</a></p>]]></description><link>http://blog.enixjin.net/simple_dependency_inject/</link><guid isPermaLink="false">60cdd029-0ab4-4a18-b4eb-edeba36c9134</guid><category><![CDATA[NodeJS]]></category><category><![CDATA[技术]]></category><category><![CDATA[TypeScript]]></category><dc:creator><![CDATA[Enix Jin]]></dc:creator><pubDate>Wed, 04 Apr 2018 05:38:41 GMT</pubDate><media:content url="http://blog.enixjin.net/content/images/2018/04/Dagger-feature.png" medium="image"/><content:encoded><![CDATA[<img src="http://blog.enixjin.net/content/images/2018/04/Dagger-feature.png" alt="一个简单的用TS实现的DI"><p>把原先项目里自己顺手实现的简单DI抽了出来</p>

<p><a href="https://github.com/enixjin/simpleDependencyInjection">https://github.com/enixjin/simpleDependencyInjection</a></p>]]></content:encoded></item><item><title><![CDATA[用Typescript实现一个超简单的Blockchain POC]]></title><description><![CDATA[POC of Blockchain in Typescript]]></description><link>http://blog.enixjin.net/poc_blockchain_typescript/</link><guid isPermaLink="false">f89fd081-2e93-4ce7-9b98-1e5319e6b22a</guid><category><![CDATA[NodeJS]]></category><category><![CDATA[技术]]></category><category><![CDATA[TypeScript]]></category><dc:creator><![CDATA[Enix Jin]]></dc:creator><pubDate>Fri, 16 Mar 2018 03:37:05 GMT</pubDate><media:content url="http://blog.enixjin.net/content/images/2018/03/blockchain.jpg" medium="image"/><content:encoded><![CDATA[<img src="http://blog.enixjin.net/content/images/2018/03/blockchain.jpg" alt="用Typescript实现一个超简单的Blockchain POC"><p><a href="https://github.com/enixjin/blockchain">https://github.com/enixjin/blockchain</a> <br>
说明有空再补。。。</p>]]></content:encoded></item><item><title><![CDATA[即使有了async/await，你仍然需要Promise来写好代码]]></title><description><![CDATA[即使有了async/await，你仍然需要Promise来写好JavaScript的同步代码]]></description><link>http://blog.enixjin.net/you-still-need-promise-even-have-async-and-await-in-javascript/</link><guid isPermaLink="false">e31ac675-b5f1-4ff1-a702-f8d598742303</guid><category><![CDATA[Javascript]]></category><category><![CDATA[NodeJS]]></category><category><![CDATA[技术]]></category><dc:creator><![CDATA[Enix Jin]]></dc:creator><pubDate>Fri, 19 Jan 2018 06:14:26 GMT</pubDate><media:content url="http://blog.enixjin.net/content/images/2018/01/7e3f86bca86ca98ad3de9d55670de2131ec94fda.png" medium="image"/><content:encoded><![CDATA[<img src="http://blog.enixjin.net/content/images/2018/01/7e3f86bca86ca98ad3de9d55670de2131ec94fda.png" alt="即使有了async/await，你仍然需要Promise来写好代码"><p>如果你使用的是Typescript>1.7，或者NodeJS>7.6， 那么你就已经可以使用新的async/await语法来部分代替原来的promise写法了。</p>

<p>先来看一个最简单的例子</p>

<p>Promise写法：</p>

<pre><code>doSomethingWithPromise(): Promise&lt;string&gt; {  
        return Promise.resolve("success");
    }

doSomethingWithPromise().then(result=&gt;console.log(`result is ${result}`));// result is success  
</code></pre>

<p>aysnc/await写法：</p>

<pre><code>async doSomething(){  
        return Promise.resolve("success");
    }
let result = await doSomething();  
console.log(`result is ${result}`);// result is success  
</code></pre>

<p>网上基本所有的关于async/await的例子都差不多这个意思。</p>

<p>怎么样，把代码从异步回调地狱里面解救出来了吧。类似同步代码的感觉是不是让你蠢蠢欲动，把以前所有的代码都翻新成async/await？(事实上只能翻新Server端的，浏览器现在还不全部支持)</p>

<p>然而你会很快面对一个事实：<strong>即使你有了async/await，以同步风格编写代码有时是不可能的，至少不是简单的。</strong></p>

<p>给我一个例子：</p>

<p>假设我们要做一个批萨，大概有以下步骤：</p>

<ul>
<li>做饼（dough）</li>
<li>做酱（sauce）</li>
<li>尝一下酱，根据口味决定放什么样的奶酪（cheese）</li>
</ul>

<p>让我们先直接用async/await随便写一个，代码如下：</p>

<pre><code>async function makePizza(sauceType = 'red') {

    let dough  = await makeDough();
    let sauce  = await makeSauce(sauceType);
    let cheese = await grateCheese(sauce.determineCheese());

    dough.add(sauce);
    dough.add(cheese);

    return dough;
}
</code></pre>

<p>这个代码足够简单，逻辑简单明了，先做饼，再做酱，然后决定奶酪。只有一个问题：我们把所有的事情线性一个一个做了，我们应该允许JavaScript引擎并行的跑这些任务！</p>

<p>所以，现在我们的代码是这样跑的：</p>

<pre><code>|-------- dough --------&gt; |-------- sauce --------&gt; |-- cheese --&gt;
</code></pre>

<p>然后我们期望我们的代码能这样跑：</p>

<pre><code>|-------- dough --------&gt;
|-------- sauce --------&gt; |-- cheese --&gt;
</code></pre>

<p>所以，我们可以改进下我们的代码：</p>

<pre><code>async function makePizza(sauceType = 'red') {

    let [ dough, sauce ] = await Promise.all([makeDough(), makeSauce(sauceType)]);
    let cheese = await grateCheese(sauce.determineCheese());

    dough.add(sauce);
    dough.add(cheese);

    return dough;
}
</code></pre>

<p>恩，比上个版本好一点了，虽然我们用到了<code>Promise.all</code>来并行出发2个任务。但是仔细想想，如果做饼和做酱的时间不一样长呢？比如做酱的时间要远远短于做饼的时间？结果就会变成这样：</p>

<pre><code>|-------- dough --------&gt;
|--- sauce ---&gt;           |-- cheese --&gt;
</code></pre>

<p>实际上决定奶酪这件事情，在酱做好之后就能直接做了，不需要等待饼做完。</p>

<p>让我们尝试完全用Promise来解决这个问题：</p>

<pre><code>function makePizza(sauceType = 'red') {

  let doughPromise  = makeDough();
  let saucePromise  = makeSauce(sauceType);
  let cheesePromise = saucePromise.then(sauce =&gt; {
    return grateCheese(sauce.determineCheese());
  });

  return Promise.all([ doughPromise, saucePromise, cheesePromise ])
    .then(([ dough, sauce, cheese ]) =&gt; {
      dough.add(sauce);
      dough.add(cheese);
      return dough;
  });
}
</code></pre>

<p>执行的结果应该就是我们期望的：</p>

<pre><code>|--------- dough ---------&gt;
|---- sauce ----&gt; |-- cheese --&gt;
</code></pre>

<p>我们可以尝试把代码改成async/await风格的：</p>

<pre><code>async function makePizza(sauceType = 'red') {

    let doughPromise = makeDough();
    let saucePromise = makeSauce(sauceType);

    let sauce  = await saucePromise;
    let cheese = await grateCheese(sauce.determineCheese());
    let dough  = await doughPromise;

    dough.add(sauce);
    dough.add(cheese);

    return dough;
}
</code></pre>

<p>然而，你一定注意到了我们必须先建好饼和酱的Promise并执行他们，再调用await来同步的获取执行结果。恩，这种写法其实比纯promise的写法没有高明到哪里去。。。</p>

<p><strong>总结：即使你准备使用async/await，你仍然需要彻底了解Promise的机制和原理，必要的时候，不要犹豫使用Promise</strong></p>]]></content:encoded></item><item><title><![CDATA[由最近VPN大量被封和外国电视剧电影下架想到的]]></title><description><![CDATA[由最近VPN大量被封和外国电视剧电影下架想到的]]></description><link>http://blog.enixjin.net/vpn_and_foreign_tv_series_is_block_in_china/</link><guid isPermaLink="false">42a87e69-9e42-48e7-b891-f5640c6a0a9f</guid><category><![CDATA[随笔]]></category><dc:creator><![CDATA[Enix Jin]]></dc:creator><pubDate>Thu, 13 Jul 2017 01:35:11 GMT</pubDate><media:content url="http://blog.enixjin.net/content/images/2017/07/ad.jpg" medium="image"/><content:encoded><![CDATA[<img src="http://blog.enixjin.net/content/images/2017/07/ad.jpg" alt="由最近VPN大量被封和外国电视剧电影下架想到的"><p>想想也许我的孩子长大之后不知道Google、Twitter、YouTube、Facebook，从没看过美剧日剧，还真是有点后怕。
<img src="http://blog.enixjin.net/content/images/2017/07/1.jpg" alt="由最近VPN大量被封和外国电视剧电影下架想到的">
<img src="http://blog.enixjin.net/content/images/2017/07/2.jpg" alt="由最近VPN大量被封和外国电视剧电影下架想到的">
<img src="http://blog.enixjin.net/content/images/2017/07/3.jpg" alt="由最近VPN大量被封和外国电视剧电影下架想到的">
<img src="http://blog.enixjin.net/content/images/2017/07/4.jpg" alt="由最近VPN大量被封和外国电视剧电影下架想到的">
<img src="http://blog.enixjin.net/content/images/2017/07/5.jpg" alt="由最近VPN大量被封和外国电视剧电影下架想到的"></p>

<p>附：王小波1996的一篇文章</p>

<blockquote>
  <p>二十多年前，我在云南插队。当地气候炎热，出产各种热带水果，就是没有椰子。整个云南都不长椰子，根据野史记载，这其中有个缘故。据说，在三国以前，云南到处都是椰子，树下住着幸福的少数民族。众所周知，椰子有很多用处，椰茸可以当饭吃，椰子油也可食用。椰子树叶里的纤维可以织粗糙的衣裙，椰子树干是木材。这种树木可以满足人的大部分需要，当地人也就不事农耕，过着悠闲的生活。</p>
  
  <p>忽一日，诸葛亮南征来到此地，他要教化当地人，让他们遵从我们的生活方式：干我们的活，穿我们的衣服，服从我们的制度。这件事起初不大成功，当地人没看出我们的生活方式有什么优越之处。首先，秋收春种，活得很累，起码比摘椰子要累；其次，汉族人的衣着在当地也不适用。就以诸葛先生为例，那身道袍料子虽好，穿在身上除了捂汗和捂痱子，捂不出别的来；至于那顶道冠，既不遮阳，也不挡雨，只能招马蜂进去做窝。当地天热，摘两片椰树叶把羞处遮遮就可以了。至于汉朝的政治制度，对当地的少数民族来说，未免太过烦琐。诸葛先生磨破了嘴皮子，言必称孔孟，但也没人听。他不觉得自己的道理不对，却把帐算在了椰子树身上：下了一道命令，一夜之间就把云南的椰树砍了个精光；免得这些蛮夷之人听不进圣贤的道理。没了这些树，他说话就有人听了——对此，我的解释是，诸葛亮他老人家南征，可不是一个人去的，还带了好多的兵，砍树用的刀斧也可以用来砍人，砍树这件事说明他手下的人手够用，刀斧也够用。当地人明白了这个意思，就怕了诸葛先生。我这种看法你尽可以不同意——我知道你会说，诸葛亮乃古之贤人，不会这样赤裸裸地用武力威胁别人；所以，我也不想坚持这种观点。</p>
  
  <p>对于此事，野史上是这么解释的：蛮夷之人，有些稀奇之物，就此轻狂，胆敢渺视天朝大邦；没了这些珍稀之物，他们就老实了。这就是说，云南人当时犯有轻狂的毛病，这是一种道德缺陷。诸葛先生砍树，是为了纠正这种毛病，是为他们好。我总觉得这种说法有点太过惊世骇俗。人家有几样好东西，活得好一点，心情也好一点，这就是轻狂；非得把这些好东西毁了，让人家心情沉痛，这就是不轻狂——我以为这是野史作者的意见，诸葛先生不是这样的人。</p>
  
  <p>野史是不能当真的，但云南现在确实没有椰子，而过去是有的。所以这些椰树可能是诸葛亮砍的。假如这不是耍野蛮，就该有种道义上的解释。我觉得诸葛亮砍椰子时，可能是这么想的：人人理应生来平等，但现在不平等了：四川不长椰树，那里的人要靠农耕为生；云南长满了椰树，这里的人就活得很舒服。让四川也长满椰树，这是一种达到公平的方法，但是限于自然条件，很难做到。所以，必需把云南的椰树砍掉，这样才公平。假如有不平等，有两种方式可以拉平：一种是向上拉平，这是最好的，但实行起来有困难；比如，有些人生来四肢健全，有些人则生有残疾，一种平等之道是把所有的残疾人都治成正常人，这可不容易做到。另一种是向下拉平，要把所有的正常人都变成残疾人就很容易：只消用铁棍一敲，一声惨叫，这就变过来了。诸葛先生采取的是向下拉平之道，结果就害得我吃不上椰子。在云南时，我觉得嘴淡时就啃几个木瓜。木瓜淡而无味，假如没熟透，啃后满嘴都是麻的。但我没有抱怨木瓜树，这种树内地也是不长的。假如它的果子太好吃，诸葛先生也会把它砍光啦。</p>
  
  <p>我这篇文章题目在说椰子，实质在谈平等问题，挂羊头卖狗肉，正是我的用意。人人理应生来平等，这一点人人都同意。但实际上是不平等的，而且最大的不平等不是有人有椰子树，有人没有椰子树。如罗素先生所说，最大的不平等是知识的差异——有人聪明有人笨，这就是问题之所在。这里所说的知识、聪明是广义的，不单包括科学知识，还包括文化素质，艺术的品味，等等。这种椰子树长在人脑里，不光能给人带来物质福利，还有精神上的幸福；这后一方面的差异我把它称为幸福能力的差异。有些作品，有些人能欣赏，有些人就看不懂，这就是说，有些人的幸福能力较为优越。这种优越最招人嫉妒。消除这种优越的方法之一就是给聪明人头上一闷棍，把他打笨些。但打轻了不管用，打重了会把脑子打出来，这又不是我们的本意。另一种方法则是：一旦聪明人和傻人起了争执，我们总说傻人有理。久而久之，聪明人也会变傻。这种法子现在正用着呢。</p>
</blockquote>]]></content:encoded></item></channel></rss>