https://geeknote.net/yuchiXiong
鱼翅
逼逼机|赛博电子厂贪玩普工|一辈子都毕不了业的学生
https://geeknote-storage.oss-cn-hongkong.aliyuncs.com/w7swa3ztzs6sz3j169pwf7xlvvhv?x-oss-process=image%2Fresize%2Cm_fill%2Cw_160%2Ch_160
2023-11-10T15:42:14Z
yuchiXiong
https://geeknote.net/yuchiXiong
https://geeknote.net/yuchiXiong/posts/2393
2023-06-20T09:25:49Z
2023-06-20T09:25:49Z
吐槽一下:还得是拓展坞懂视频接口
<h2>
<a id="%E5%9B%BD%E9%99%85%E6%83%AF%E4%BE%8B%EF%BC%8C________%E3%80%82" href="#%E5%9B%BD%E9%99%85%E6%83%AF%E4%BE%8B%EF%BC%8C________%E3%80%82" class="anchor"></a>国际惯例,________。</h2>
<p>保命声明:</p>
<ul>
<li>
<p>第一、纯吐槽,我是硬件小白里的小白里的小白,说错了恳请哥哥姐姐们多指正</p>
</li>
<li>
<p>第二、不是水军,不是软文,<del>哪有软文骂产品的</del></p>
</li>
<li>
<p><del>第三、还是 tmd 的公平</del></p>
</li>
</ul>
<h2>
<a id="%E4%B8%80%E5%8F%B0%E6%98%BE%E7%A4%BA%E5%99%A8%E6%95%B4%E5%87%BA%E4%B8%80%E6%A1%A3%E5%AD%90%E9%97%AE%E9%A2%98" href="#%E4%B8%80%E5%8F%B0%E6%98%BE%E7%A4%BA%E5%99%A8%E6%95%B4%E5%87%BA%E4%B8%80%E6%A1%A3%E5%AD%90%E9%97%AE%E9%A2%98" class="anchor"></a>一台显示器整出一档子问题</h2>
<p>因为早就受够了那台 1080p 的显示器,趁着 618 入了一台新的 27 寸显示器,2k 8bit,两个 HDMI2.0 接口和一个 DP1.2 接口,根据产品介绍,HDMI2.0 接口下可以达到 2k144hz,dp1.2 接口下可以达到 2k180hz 。</p>
<p>小新 14pro 2021 给的是 2C1A(两个 Type-C 一个 Type-A)的接口,电源要固定占用一个 C 口,实际可用的接口只有 1C1A,因而之前那台 1080p75hz 的显示器一直用的某联 5 合 1 拓展坞(U1S1 这种情况下还是用的挺好的,正因为如此我用了两年都没发现问题)。</p>
<p>众所周知 DP 接口是未来,理论上我应该优先使用 DP 接口,但拓展坞毕竟是几年前买的,只有一个 HDMI 接口,本着“144hz 又不是不能用”的心态,我直接把旧显示器的 HDMI 线拽过来插上了。<strong>这一插,插出一堆问题。</strong></p>
<p>如上所述,新的显示器在 HDMI2.0 接口下理论支持 2k144hz 的画面质量,但通过拓展坞接上以后,2k 分辨率下却只能达到 60hz。起初我以为是旧显示器的 HDMI 线的问题,换了两根线无果后我通过 DP1.4 转 Type-C 直连到电脑后完美实现了 2k180hz ,这时我终于意识到问题大概出在了拓展坞上。</p>
<h2>
<a id="%E8%B7%9F%E5%AE%A2%E6%9C%8D%E6%8E%B0%E6%89%AF%E4%BA%86%E4%B8%80%E9%80%9A%E5%90%8E%EF%BC%8C%E6%88%91%E5%8F%91%E7%8E%B0%E4%BA%86%E6%9C%89%E8%B6%A3%E7%9A%84%E4%BA%8B%E6%83%85" href="#%E8%B7%9F%E5%AE%A2%E6%9C%8D%E6%8E%B0%E6%89%AF%E4%BA%86%E4%B8%80%E9%80%9A%E5%90%8E%EF%BC%8C%E6%88%91%E5%8F%91%E7%8E%B0%E4%BA%86%E6%9C%89%E8%B6%A3%E7%9A%84%E4%BA%8B%E6%83%85" class="anchor"></a>跟客服掰扯了一通后,我发现了有趣的事情</h2>
<p>在反复测试中我发现,使用拓展坞接入的显示器仅支持 2k60hz 或 1080p120hz,当强行设置为 1080p144hz 时,显示器会显示无信号。</p>
<p>一种可能的猜想是带宽不够。HDMI2.0 的带宽是 18Gbps,考虑到拓展坞是两年前(2021/10/05)与电脑一起购置的,并不能确定这款产品使用的到底是 HDMI2.0 还是 HDMI1.4,于是我火速打开了某宝咨询客服。由于对客服并没有抱太大的期望,保险起见我同时找了两家店的客服,其中一家客服很明确的告诉我,我购置的这款拓展坞提供的 HDMI 接口是 1.4 版本的,而另一家客服在一通掰扯后被迫去找了原厂询问,最后给到了“这款产品曾经有过两个版本,早期的版本无论是 2k 还是 4k 都只能达到 30hz,后期的版本才支持 <strong>2k120hz</strong> 和 4k60hz”的回复。</p>
<p>好了,问题大致有了结论了,这个锅八成得由拓展坞来背了。</p>
<p>不过我并不是一个对刷新率有着极致追求的人,如果能够同时连接 2k120hz 与 1080p75hz 两台显示器,或许也能一定程度上满足我开(yu)发(le)的需求,于是我在又找了某联一款 9 合 1 的产品,这款产品提供了 DP + VGA + HDMI 三种视频接口,出于我对某联一定程度上的不信任,在购买前我还是问了一下客服,果不其然客服告诉我“这款拓展坞并不能做到多屏异显,只能做到多屏镜像。”</p>
<p>说实话我到现在都没太懂多屏镜像是出于个啥需求,不死心的我又问客服是否有支持多屏异显的产品,随后客服给了我一个链接,这是一款声称支持 DP + HDMI 的 9 合 1 拓展坞,其中 DP 和 HDMI 最高支持 4k60hz 的画面质量,并且可以做到多屏异显。</p>
<p>价格嘛,比之前那个 9 合 1 也就贵几十块钱,如果合适的话倒也没啥所谓(<del>人傻钱也不多说的就是我</del>),但事到如今我对某联的信任度正在直线下降,直觉告诉我这件事没那么简单,于是我提出了一个直接导致这篇水文诞生的问题:<strong>这个拓展坞在同时连接 DP 和 HDMI 的情况下,是两个接口都能达到 4k60hz 的画面质量,还是只有其中一个接口能达到?如果不是两个都能达到,当一个接口输出 4k60hz 时另一个接口能输出多少?</strong></p>
<p>再又经过了几轮掰扯以后,我得到了答案:<strong>两个接口共用 4k60hz 的带宽</strong>。</p>
<p>短暂的沉默后我意识到我好像已经完全明白某联(或者说大部分厂商?)的拓展坞是怎么回事了。</p>
<h2>
<a id="%E6%8E%A5%E5%8F%A3%E7%89%88%E6%9C%AC%E4%B8%8E%E7%94%BB%E9%9D%A2%E8%B4%A8%E9%87%8F%E4%B8%8E%E5%B8%A6%E5%AE%BD" href="#%E6%8E%A5%E5%8F%A3%E7%89%88%E6%9C%AC%E4%B8%8E%E7%94%BB%E9%9D%A2%E8%B4%A8%E9%87%8F%E4%B8%8E%E5%B8%A6%E5%AE%BD" class="anchor"></a>接口版本与画面质量与带宽</h2>
<p>一个很明显的现象是,无论是某联的商品介绍还是客服的回答,都很少直接提到拓展坞的视频接口版本,而是直接给出了画面质量,比如 4k60hz,2k120hz 等等。</p>
<p>按照客服给到的 HDMI1.4 版本的说法来看,HDMI1.4 支持的带宽是 10.2Gbps,我们可以简单的计算一下 1080p 和 2k 画质下各个刷新率所需要的带宽:</p>
<pre class="highlight"><code class="language-JavaScript">// 1080p60hz
1920 * 1080 * 60 * 8 * 3 / 1024 / 1024 / 1024 // 2.9Gbps
// 1080p75hz
1920 * 1080 * 75 * 8 * 3 / 1024 / 1024 / 1024 // 3.5Gbps
// 1080p90hz
1920 * 1080 * 90 * 8 * 3 / 1024 / 1024 / 1024 // 4.2Gbps
// 1080p120hz
1920 * 1080 * 120 * 8 * 3 / 1024 / 1024 / 1024 // 5.6Gbps
// 1080p144hz
1920 * 1080 * 144 * 8 * 3 / 1024 / 1024 / 1024 // 6.7Gbps
// 2k60hz
2560 * 1440 * 60 * 8 * 3 / 1024 / 1024 / 1024 // 4.9Gbps
// 2k75hz
2560 * 1440 * 75 * 8 * 3 / 1024 / 1024 / 1024 // 6.2Gbps
// 2k90hz
2560 * 1440 * 90 * 8 * 3 / 1024 / 1024 / 1024 // 7.4Gbps
// 2k120hz
2560 * 1440 * 120 * 8 * 3 / 1024 / 1024 / 1024 // 9.9Gbps
</code></pre>
<p>很容易看出理论上 HDMI1.4 至少支持 1080p144hz 和 2k75hz 是绰绰有余的,那么为什么实际上只能支持 1080p120hz 和 2k60hz 呢?这就要让我们来计算一下某联官方给到的 4k30hz 和 4k60hz 的带宽了:</p>
<pre class="highlight"><code class="language-JavaScript">// 4k30hz
4096 * 2160 * 30 * 8 * 3 / 1024 / 1024 / 1024 // 6.0Gbps
// 4k60hz
4096 * 2160 * 60 * 8 * 3 / 1024 /1024 / 1024 // 11.8Gbps
</code></pre>
<p>以我手中的这个拓展坞为例,它支持的 4k30hz 仅需要约 6.0Gbps 的带宽,在 2k 画质下,6.0Gbps 的带宽最多只能支持到 60hz 的刷新率,而在 1080p 画质下,最多只能支持到 120hz 的刷新率。</p>
<p>是不是有点眼熟?好像和我用起来<strong>完全一致</strong>。另外从 1080p144hz 就不够了这件事情上也很容易发现,某联给到的所谓 HDMI1.4 的实际带宽只有 6.0Gbps,完全达不到 HDMI1.4 的标准,印崔斯汀。</p>
<p>进一步的我们可以参照上面的计算结果看一下某联的“HDMI2.0”接口的实际表现,11.8Gbps 的带宽在 2k 画质下最多只能支持到 120hz 的刷新率,巧不巧?前面客服告诉我最新的版本 HDMI2.0 最多可以输出的也是 2k120hz 。</p>
<p>这也就意味着,某联标拓展坞提供的 HDMI 和 DP 接口带宽本质上与标准接口的版本带宽是没有太大关系的,这或许也是它们通常只提最高画质而不提接口版本的原因,更通俗的讲,它们只是刚好贴着 4k60hz 的带宽而已。</p>
<p>请注意哦,上述的所有计算使用的都是 8bit,如果你使用 10bit 的显示器,那么拓展坞能够提供的画面输出质量会更有限。</p>
<h2>
<a id="%E6%88%91%E4%B8%8D%E7%9F%A5%E9%81%93%E7%9A%84%E9%97%B2%E8%AF%9D%E6%97%B6%E9%97%B4" href="#%E6%88%91%E4%B8%8D%E7%9F%A5%E9%81%93%E7%9A%84%E9%97%B2%E8%AF%9D%E6%97%B6%E9%97%B4" class="anchor"></a>我不知道的闲话时间</h2>
<p>当然我并不完全是要说某联的拓展坞有多么的差,毕竟它的价格确实很便宜。另外我对视频接口的了解也不多,这种做法目前看起来更像是某种行业共识?再说雷电拓展坞的价格摆在那里感觉就足够说明一切了。</p>
<p>我猜想大部分游戏玩家或者设计工作者应该都是不用拓展坞的,否则网络上<strong>怎会如此安静</strong>。<del><strong>都怪小新的接口实在太少了</strong></del></p>
<p>最后多说一句,按照拓展坞这个实际带宽来看,我现在只想立刻马上拔掉它用 DP to C 直连电脑,这个带宽接在雷电 4 上真的是太浪费了。</p>
<p>最后的最后,我做梦都没想到买个显示器能学(zheng)到(chu)这么多知(po)识(shi),<del>以后再也不买显示器了(bushi</del></p>
国际惯例,________。
保命声明:
第一、纯吐槽,我是硬件小白里的小白里的小白,说错了恳请哥哥姐姐们多指正
第二、不是水军,不是软文,哪有软文骂产品的
第三、还是 tmd 的...
鱼翅
https://geeknote.net/yuchiXiong
https://geeknote.net/yuchiXiong/posts/2382
2023-06-15T03:12:45Z
2023-11-10T15:42:14Z
瞎话 JavaScript 函数式:add(3)(4)一点都不酷
<blockquote>
<p>这可能是一篇很无聊的文章,它无聊就无聊在或许这个世界真的需要这样的文章。</p>
</blockquote>
<h2>
<a id="%E5%9B%BD%E9%99%85%E6%83%AF%E4%BE%8B%EF%BC%8C%E5%86%99%E5%9C%A8%E5%89%8D%E9%9D%A2" href="#%E5%9B%BD%E9%99%85%E6%83%AF%E4%BE%8B%EF%BC%8C%E5%86%99%E5%9C%A8%E5%89%8D%E9%9D%A2" class="anchor"></a>国际惯例,写在前面</h2>
<p>作为一名野鸡大学毕业的野路子程序员,第一次听说函数式编程这个概念的时候我大概已经工作一年了。<del>彼时的我正在做 Ruby&JavaScript 全栈工程师的美梦。</del></p>
<p>一名有抱负(暂时)的工程师通常不会放过任何一个知识盲点,于是我立刻打开某书某乎某 SDN 找来了几篇文章学习。</p>
<p>当柯里化、纯函数、高阶函数等一个个概念出现在我的眼前时,我看的<del>如痴如醉</del>云里雾里,不久之后我就把这些概念完全抛在了脑后。</p>
<p>某天下午我在一篇文章里我看到了如下这样一段代码,我忽而意识到我不能继续这样自己糊弄自己了。</p>
<pre class="highlight"><code class="language-JavaScript">function isEven(n) {
if (n === 0) {
return true;
}
return isOdd(n - 1);
}
function isOdd(n) {
if (n === 0) {
return false;
}
return isEven(n - 1);
}
</code></pre>
<p>我想大部分有过一点 JavaScript 开发经验的开发者都能看得出这段代码的问题,一方面这段代码如此抽象的实现了一个判断奇偶数的功能颇有高射炮打蚊子的感觉,另一方面相互递归无疑是低效且危险的。</p>
<p>光阴任然,我在龟速学习的道路上一转眼又混过去两年,当有一天我又看到一篇文章里看到了如下代码块的时候(请注意,下面的代码块是我从原文抄过来的,包括注释),我知道有些事情不能再拖了。</p>
<pre class="highlight"><code class="language-JavaScript">// 初级程序员
let arr = [1, 2, 3, 4]
let newArr = []
for (var i = 0; i < arr.length; i++) {
newArr.push(arr[i] + 1)
}
console.log(newArr) //[2, 3, 4, 5]
// 函数式编程
let arr = [1, 2, 3, 4]
let newArr = (arr, fn) => {
let res = []
for (var i = 0; i < arr.length; i++) {
res.push(fn(arr[i]))
}
return res
}
let add = item => item + 1 //每项加1
let multi = item => item * 5 //每项乘5
let sum = newArr(arr, add)
let product = newArr(arr, multi)
console.log(sum, product) // [2, 3, 4, 5] [5, 10, 15, 20]
</code></pre>
<h2>
<a id="%E5%87%BD%E6%95%B0%E5%BC%8F%E7%BC%96%E7%A8%8B%E4%BB%8E%E5%93%AA%E5%84%BF%E6%9D%A5%EF%BC%9F" href="#%E5%87%BD%E6%95%B0%E5%BC%8F%E7%BC%96%E7%A8%8B%E4%BB%8E%E5%93%AA%E5%84%BF%E6%9D%A5%EF%BC%9F" class="anchor"></a>函数式编程从哪儿来?</h2>
<p>提到函数式编程,不得不提的是函数式编程的鼻祖——<strong>Lambda 演算法</strong>。</p>
<p>考虑到 Lambda 演算法可能并不是一个人尽皆知的概念,我们将要提到同一时期的另一个概念——<strong>图灵机</strong>,相信这样就有部分人会稍微明白些了。</p>
<p>简单来说,图灵机是一种理论上的计算模型,用大白话说就是它假设了一台机器,而这台机器最大的特点是任何<strong>可以用科学计算解决的问题</strong>都可以用它来解决(也就是说显然也存在不可以用科学计算解决的问题,此时图灵机亦无能为力)。</p>
<p>图灵机是存在于假象之中的机器,因为世界上并不存在无限长的纸带。但与之同时,图灵机又明确了这样一台机器应该具备的最基本的特性,上世纪的计算机科学家们就是基于这些特性才发明了计算机。</p>
<p>本质上,Lambda 演算法与图灵机有着同样的目的,我们的祖师爷艾伦·图灵(Alan Turing)已经证明了 Lambda 演算法与图灵机在计算能力上的等价性(在 Lambda 演算法中模拟了图灵机的行为),而这一结论有一个更为人尽皆知的名字——<strong>图灵等价</strong>。</p>
<p>正是因为等价性的存在,即便当今的计算机大多都是基于图灵机实现的,依然不妨碍我们实现大量的继承了 Lambda 演算法思想的编程语言,这些语言正是我们今天所说的<strong>函数式编程语言</strong>。</p>
<h2>
<a id="Lambda+%E6%BC%94%E7%AE%97%E6%B3%95%E5%AE%9A%E4%B9%89%E4%BA%86%E4%BB%80%E4%B9%88%EF%BC%9F" href="#Lambda+%E6%BC%94%E7%AE%97%E6%B3%95%E5%AE%9A%E4%B9%89%E4%BA%86%E4%BB%80%E4%B9%88%EF%BC%9F" class="anchor"></a>Lambda 演算法定义了什么?</h2>
<p>不同于图灵机,Lambda 演算法并不是一台机器,而是一种数学模型,是一堆数学公式。</p>
<p>一个<strong>通用的</strong>计算模型要通过有限的手段来抽象无限的问题,Lambda 演算法通过<strong>函数</strong>实现了这种抽象能力。事实上 Lambda 演算法里除了函数就没有别的东西了。</p>
<p>把 Lambda 演算法推一遍不是本文的目的(真要那样放一个 Wiki 的链接就完事了,<del>再说我也推不出来。</del>)</p>
<p>阿隆佐·丘奇(Alonzo Church)通过一系列的参数传递与消除,实现了对数字、运算以及程序语言三种基本结构(顺序、分支、循环)的抽象,其中关于数字的抽象如今被称为<strong>丘奇数</strong>,这些内容看起来并不容易理解,下面是从 Google 上抄过来的一部分,能看明白定义出来的是个什么东西,以及怎么被应用到后续的运算中就够了,感兴趣的同学可以自行搜索关键字并深入了解。</p>
<pre class="highlight"><code class="language-JavaScript">// 数字的抽象
const zero = s => z => z;
const one = s => z => s(z);
const two = s => z => s(s(z));
const three = s => z => s(s(s(z)));
const four = s => z => s(s(s(s(z))));
// 加法的抽象
const add = x => y => s => z => x(s)(y(s)(z));
// 加法的应用
const five = add(two)(three);
</code></pre>
<p>很容易发现上述代码具备如下几个特点:</p>
<ol>
<li>所有函数都只有一个参数</li>
<li>函数可以作为函数的参数和返回值</li>
<li>纯粹的<strong>数学推演</strong>,不会修改一个已有的值</li>
</ol>
<p>如果你已经看过一些函数式编程的文章了,那么你应该已经发现了,上述分别对应了<strong>柯里化</strong>、<strong>函数一等公民</strong>以及<strong>不可变性</strong>这些概念。实际上如果更进一步的讲,<strong>如今谈论函数式编程提到的这些概念均是来源于此,这是它们的因果。</strong></p>
<p>当然,要实现完整的运算与三种结构并不是那么简单,这里特地把<strong>循环</strong>拿出来过一下。</p>
<p>要通过函数实现一个循环结构并不困难,相信大家都能在第一时间想到<strong>递归</strong>,如今在面试的时候也经常会遇到诸如「不使用递归实现 deepClone」这样的问题,考察的就是这个。</p>
<p>一个常见的的递归模板如下:</p>
<pre class="highlight"><code class="language-JavaScript">function loop(n) {
if (n === 0) {
return;
}
// do something
loop(n - 1);
}
</code></pre>
<p>然鹅,在数学推演的世界里,在一个函数定义完成前将其应用到自身的运算中的行为并不严谨(当然,我并不确定这是不是主因,欢迎补充),因而 Lambda 演算法使用了一种特别的技巧来实现递归,这种技巧有一个很酷<del>一听就让人想融资</del>的名字——<strong>Y Combinator(Y 组合子)</strong>。</p>
<p>关于 Y 组合子这里不过多展开了,大致的思路是把函数的定义和调用分离,感兴趣可以看文末附加的参考文章。</p>
<p>很多人会困惑上述是一坨什么东西,这样复杂的结构看起来并没有比如今面向过程,面向对象的代码更加易读和易维护。</p>
<p>然而如果是跟着全文走的同学应该还记得 Lambda 演算法的目的是什么。回过头来我们发现,现在 Lambda 演算法确实做到了仅通过寥寥无几的几个特征(单参数,函数一等公民,函数定义等)实现了一个通用计算模型所必备的所有特性,这也就意味着,Lambda 演算法是完全可以用来实现一台通用计算机的。</p>
<p>这个结论在今天有另一个词来描述——<strong>图灵完备</strong>。</p>
<h2>
<a id="%E5%87%BD%E6%95%B0%E5%BC%8F+%21%3D%3D+%E5%87%BD%E6%95%B0%E5%BC%8F" href="#%E5%87%BD%E6%95%B0%E5%BC%8F+%21%3D%3D+%E5%87%BD%E6%95%B0%E5%BC%8F" class="anchor"></a>函数式 !== 函数式</h2>
<p>事情并没有就此结束,Lambda 演算法复杂的数学推演让人望而却步,实际上在使用一门基于 Lambda 演算法设计的编程语言开发程序时并不一定要从定义数字/运算/结构开始,正如我们在图灵机上实现的编程语言亦不需要从纸带打孔开始定义程序一样。</p>
<p>在 Lambda 演算法的基础上人们发明了一系列的编程语言如最为著名的 Lisp 语言家族。这些语言共同继承了 Lambda 演算法的思想,其特点是通过函数来实现对问题的抽象,由此形成了一种特有的编程范式。</p>
<p>多啰嗦几句,编程范式是编程语言用于抽象并解决现实问题的<strong>通用</strong>方法,以大家熟识<del>可能也不见得熟识</del>的面向对象编程为例,在面向对象编程中,我们通常把事物抽象成类与对象来对现实问题的建模,在这个过程中我们需要考虑类,对象,属性甚至方法之间的组合与复用逻辑,因此有了如继承、多态、封装,接口,抽象类等一系列的概念,这些概念均是编程范式的一部分。</p>
<p>同理可得,函数式编程是通过函数来抽象现实问题的<strong>通用</strong>程序设计方法,而高阶函数,compose 等正是函数式编程中用于组合与复用函数这一单元的概念。</p>
<p>时至今日,函数式编程语言百花齐放,各种各样的特性被加入到了函数式编程语言中,这些特性有的是为了性能,有的是为了开发体验,还有的是为了可读性。这些特性并不是 Lambda 演算法所必须的,甚至有些特性是从其它编程范式中借鉴过来的,一方面这使得广义上的函数式编程与狭义上的函数式编程有了区别,另一方面也使得函数式编程语言的特性更加丰富,更加适合实际开发,事实上当前有很多流行的通用编程语言都结合了多种编程范式,JavaScript 便是其中之一。</p>
<h2>
<a id="%E6%89%80%E4%BB%A5%E5%AD%A6%E4%B9%A0%E5%87%BD%E6%95%B0%E5%BC%8F%E5%88%B0%E5%BA%95%E6%98%AF%E5%AD%A6%E4%BB%80%E4%B9%88" href="#%E6%89%80%E4%BB%A5%E5%AD%A6%E4%B9%A0%E5%87%BD%E6%95%B0%E5%BC%8F%E5%88%B0%E5%BA%95%E6%98%AF%E5%AD%A6%E4%BB%80%E4%B9%88" class="anchor"></a>所以学习函数式到底是学什么</h2>
<p>在学习一门范式时对范式拥有一个正确的认知非常重要。从图灵等价性上我们很容易得出函数式编程与面向对象编程是等价的这样的结论,这也就意味着并不存在函数式编程可以解决而面向对象编程无法解决的问题,亦不存在谁比谁高级的说法。</p>
<p>在尝试了解函数式编程到底是什么的过程中,我曾接连尝试阅读了《计算机程序的构造和解释》(SICP)和《计算的本质》两本书但最后都以失败告终。</p>
<p>之后因机缘巧合大致翻阅了《程序设计方法》(HTDP)一书的前十几章,这是一本使用 Racket 语言描述的书,Racket 是 Lisp 大家族下的一门编程语言,亦是纯函数式编程语言。有趣的地方在于这本书从头到尾都没有提到函数式编程这个词,在那之后我又看了 UCB CS61A 关于函数抽象的部分,同样也没有提到柯里化、纯函数等乱七八糟的概念。</p>
<p>如今看来我认为这些才是真正能够教会你函数式编程的东西,函数式编程并不是复杂概念的集合,它只是一种协助你编写代码的方式罢了,在理解了它的本质以后,剩下的就只是平平凡凡的写代码而已了。<strong>如果不能在一开始就有这种认知,很有可能在学习的过程中走火入魔。</strong></p>
<p>印象里好像《Ruby 元编程》中有一句非常经典的话:<strong>根本没有什么元编程,只有编程</strong>,我想函数式编程大概亦是如此。</p>
<p>对中文互联网的环境多少是有点怨念的,当我们使用搜索引擎搜索「JavaScript 函数式编程」会找到大量的文章,这些文章与其说在讲如何使用 JavaScript 进行函数式编程,不如说是在讲如何使用 JavaScript 实现 Lambda 演算法。</p>
<p>而当搜索柯里化的应用时就更有趣了,大量的文章在讲如何实现一个 add(3)(4) 的函数,最后的结论无非是惰性求值和参数复用等概念,却对这样做的性能开销只字不提。就更少有人提及最初的 Lambda 演算法本身就只支持一个参数这件事情了。</p>
<p>Lambda 只使用一个参数就实现了对问题的抽象,因而完全不需要支持多个参数,在编程语言为了表达能力而支持多个参数的今天反而柯里化回去强行使用一个参数真的是有意义的吗?</p>
<p>相比之下另外一些文章堪称扭曲事实,以纯函数无副作用的线程安全性出发,最终得到了一个函数式编程比面向对象编程更高级的结论。另一侧还有人完全忽视函数式编程的抽象程度,将其描述为一种“有水平的程序员都在业务开发里大量用到”的范式,如果 Bilibili 的搜索排名还没有变化,那么我很推荐大家去试试搜索「函数式编程」,看看第一个视频的标题。<del>后来我点开视频看了大概 10 秒以后终于理解了所谓不可描述之事,<strong>流量密码都让你们懂完了,当个人吧</strong>。</del></p>
<h2>
<a id="%E6%9C%80%E5%90%8E%EF%BC%8C%E5%A4%9A%E8%8C%83%E5%BC%8F%E7%9A%84+JavaScript" href="#%E6%9C%80%E5%90%8E%EF%BC%8C%E5%A4%9A%E8%8C%83%E5%BC%8F%E7%9A%84+JavaScript" class="anchor"></a>最后,多范式的 JavaScript</h2>
<p>让我们把目光再次回到 JavaScript 上来,JavaScript 是一门多范式的编程语言,它既可以面向对象,也可以面向过程,同时也可以函数式。</p>
<p>大家所熟知的工具库如 Lodash 在使用方法上有着相当的函数式的影子:</p>
<pre class="highlight"><code class="language-JavaScript">const arr = _.map([1, 2, 3], item => item + 1);
const fib = _.memoize(n => n < 2 ? n : fib(n - 1) + fib(n - 2));
</code></pre>
<p>多范式语言的价值在于它可以让我们在解决问题时更加灵活,在《JavaScript 设计模式与开发实践》一书中,作者曾经多次使用面向对象与函数式两种范式来实现同一种模式,而事实上相比于面向对象,函数式的实现更加简洁,更加优雅,也更加符合大家对 JavaScript 的印象。</p>
<h2>
<a id="%E7%A2%8E%E7%A2%8E%E5%BF%B5%E5%92%8C%E5%8F%A0+Buff+%E7%8E%AF%E8%8A%82" href="#%E7%A2%8E%E7%A2%8E%E5%BF%B5%E5%92%8C%E5%8F%A0+Buff+%E7%8E%AF%E8%8A%82" class="anchor"></a>碎碎念和叠 Buff 环节</h2>
<p><del>本文不针对任何人,也不针对任何文章,只是想说说我自己的看法。</del></p>
<p>正如上一节所说,JavaScript 是一门多范式的编程语言,早年间受到 Java 的影响,大家热衷于照搬 Java 的面向对象实践,在近几年随着 React 对函数式编程的推崇,函数式编程又渐渐成为了大家的热点。</p>
<p>坦白说我想写这篇文章很久了,曾有一段时间非常担心自己对函数式的理解可能并不全面甚至正确而迟迟不敢动笔,但转念一想人家 add(3)(4) 都有人写还有人点赞我有啥不敢写的,笑死。</p>
<p>本文的初版是一篇更有戾气的文章,但冷静下来还是觉得纠正更重要。于我个人,于我身边的很多朋友的成长经历来看,大家都有过被那些文章忽悠瘸了的过往,<strong>再说这互联网似乎从来都不缺戾气。</strong></p>
<p>最后的最后,感谢能看到这里的你,如果文中有什么错误或者不妥的地方,欢迎指正。</p>
<h2>
<a id="%E5%8F%82%E8%80%83" href="#%E5%8F%82%E8%80%83" class="anchor"></a>参考</h2>
<ul>
<li>
<a href="https://www.bilibili.com/video/BV1pU4y1v7Hj/?spm_id_from=333.337.search-card.all.click">Lambda 演算科普系列</a>:winter 老师在 Bilibili 关于 Lambda 演算法的系列科普视频</li>
<li>
<a href="https://book.douban.com/subject/1140942/">程序设计方法(中文版)</a>:我看的第一版,第二版已经出版了,但是我没看过</li>
<li>
<a href="https://inst.eecs.berkeley.edu/~cs61a/fa22/">UCB CS61A</a>:链接是 2022 年秋季的课程,这门课程是免费的,每年会同步更新,想看最新的直接搜索 CS61A 即可</li>
<li>
<a href="https://book.douban.com/subject/26382780/">JavaScript 设计模式与开发实践</a>:多范式的 JavaScript 在实现常用设计模式时并不一定要照抄 Java 的实现,这本书的常见设计模式中很多地方都是使用函数式的方式实现的,值得一读</li>
<li>
<a href="https://coderemixer.com/2018/12/07/y-combinator-in-ten-minutes/">十分钟速通 Y Combinator</a>:使用 JavaScript 实现 Y 组合子的过程</li>
</ul>
这可能是一篇很无聊的文章,它无聊就无聊在或许这个世界真的需要这样的文章。
国际惯例,写在前面
作为一名野鸡大学毕业的野路子程序员,第一次听说函数式编程这个概念的时候我大概已经工作一年了。彼...
鱼翅
https://geeknote.net/yuchiXiong
https://geeknote.net/yuchiXiong/posts/1903
2022-12-31T02:30:11Z
2023-03-31T03:54:07Z
前端条件竞态乱谈——可能被我误解的函数防抖
<h1>
<a id="%E5%89%8D%E8%A8%80" href="#%E5%89%8D%E8%A8%80" class="anchor"></a>前言</h1>
<p>故事发生的时间距离今天其实已经快一个月了,由于一些特(tuo)殊(yan)的原因,今天终于动笔开始写(shui)下了这篇博文, <del>再不写都™明年了</del> 。</p>
<p><code>ChatGPT</code> 想必是过去的一个月里相当火热的话题了,抱着一些好奇心,我也注册并尝试了搜索了一些内容。让我觉得相当惊艳的是它可以通过不断的反问来验证自己对于一些概念理解的正确性,十分 nice。</p>
<p>今天要聊的这部分内容也是在使用 <code>ChatGPT</code> 过程中发现的,文章内容较主观,欢迎讨论。</p>
<h1>
<a id="1.+%E8%B5%B7%E5%9B%A0" href="#1.+%E8%B5%B7%E5%9B%A0" class="anchor"></a>1. 起因</h1>
<p>事情的起因是某一天我在群里提起 <code>ChatGPT</code> ,顺手截了几张图,这个时候有一位群友 <a href="https://github.com/Billdex">Billdex</a> 提到搜一下 <code>singleflight</code> 。</p>
<blockquote>
<p>我:GoLang 中的 Singleflight 是什么?</p>
</blockquote>
<blockquote>
<p>ChatGPT: Singleflight 是一个 Go 语言库, 它提供了一种方法来管理并发的函数调用,并确保只有一个函数在同一时间被调用。它的目的是为了防止并发的函数调用之间的竞争条件,从而避免不必要的资源浪费和性能问题。</p>
</blockquote>
<blockquote>
<p>我:它的实现原理是什么</p>
</blockquote>
<blockquote>
<p>ChatGPT: Singleflight 库实现的原理是通过一个独占锁来控制对函数的并发调用。当调用singleflight.Do() 函数时,它会对函数进行拦截,并将它们放到一个队列中,并通过独占锁来确保只有一个函数在同一时间被调用。如果有其他的并发调用请求,它们会被挂起直到当前的函数调用完成。这样就可以避免多个函数在同一时间被调用,从而避免竞争条件的产生。</p>
</blockquote>
<p>我对 <code>GoLang</code> 可谓一窍不通,本来这件事到这里就该结束了,随后会淹没在更多的消息记录的海洋里。</p>
<p>但好巧不巧群里有个写 <code>CPP</code> 的小伙伴 <a href="https://github.com/hajimeru">hajimeru</a> 碰巧路过,于是群里出现了这样一段对话:</p>
<blockquote>
<p>hajimeru: 这是事件队列的思路?</p>
</blockquote>
<blockquote>
<p>Billdex: 不太一样,倒是有点类似于前端的接口防抖,用在后端做并发控制</p>
</blockquote>
<p>如前面所说,我既不会 <code>GoLang</code> 对后端了解的也不多,但你要说前端的防抖函数那我可不困了,这段对话一下就勾起了我的好奇心。</p>
<h1>
<a id="2.+%E9%98%B2%E6%8A%96%E5%87%BD%E6%95%B0" href="#2.+%E9%98%B2%E6%8A%96%E5%87%BD%E6%95%B0" class="anchor"></a>2. 防抖函数</h1>
<p>函数防抖是前端常用的优化手段之一,如果一个函数在 n 秒内被反复调用,函数防抖会重新计算函数的执行时间以确保在指定的时间内只执行一次。</p>
<p>一个比较常见的例子是搜索框实现,从产品的角度来说,产品每多一步操作都可能导致用户的流失,因而许多产品都将搜索设计为随着用户输入自动响应搜索结果的交互形式。</p>
<p>做前端的同学都知道,实现这样的功能是一定少不了防抖函数的,通过监听搜索框的输入事件进行请求会产生多次无用的请求,而用户往往只需要最后一次请求作为结果,这样很容易带来服务的性能浪费。</p>
<p>于是在这里,我过往对于防抖函数的一个错觉就产生了: <strong>防抖函数的作用是不断的推后函数执行的时间直到超时</strong> 。</p>
<p>那么自然而然的,我也认为它与 <code>Singleflight</code> 用来规避条件竞争不是一码事。</p>
<h1>
<a id="3.+%E9%97%AE%E9%97%AE+AI+%E5%90%A7" href="#3.+%E9%97%AE%E9%97%AE+AI+%E5%90%A7" class="anchor"></a>3. 问问 AI 吧</h1>
<blockquote>
<p>我:可以认为 Singleflight 避免函数调用条件竞争的策略与 UI 编程中的函数防抖类似吗?</p>
</blockquote>
<blockquote>
<p>ChatGPT: 是的,Singleflight 避免函数调用条件竞争的策略与 UI 编程中的函数防抖类似。函数防抖也是一种控制函数调用的方法,它的目的是为了避免频繁的函数调用,从而降低系统的性能。它的实现原理是通过设置一个定时器, 在函数调用之后延迟一段时间来执行函数。如果在定时器超时前再次调用该函数,定时器会被重置,直到超时时才真正执行函数。这样就可以避免频繁的函数调用,提高系统的性能。Singleflight 库也是通过类似的方法来避免函数调用条件竞争,它通过独占锁来控制函数调用,确保只有一个函数在同一时间被调用,从而避免竞争条件的产生。</p>
</blockquote>
<p>在看到这个答案以后的最初几秒里,我是诧异的。随后我的脑子里闪现出另外一件事情,我忽然意识到我可能错过了什么东西,进一步的,我意识到过去对函数防抖的理解可能是片面的。</p>
<h1>
<a id="4.+%E6%9D%A1%E4%BB%B6%E7%AB%9E%E6%80%81" href="#4.+%E6%9D%A1%E4%BB%B6%E7%AB%9E%E6%80%81" class="anchor"></a>4. 条件竞态</h1>
<p>提到条件竞态,长期使用 <code>JavaScript</code> 的我总是觉得与我无瓜,毕竟 <code>JavaScript</code> 是单线程的语言,而我通常见到谈论条件竞态的场景几乎都是多线程场景。</p>
<p>随便翻阅了几篇某金,某乎,某SDN的文章,大家都在用一个共同的例子来演示条件竞态的现象,两个线程交替执行的过程中,由于 <strong>执行顺序是不可预估的</strong> ,且两者 <strong>都会修改各自持有的内存里的同一个变量</strong> ,于是最后的代码执行结果与执行顺序一样变成了不可预估的。</p>
<p>稍等一下。</p>
<p>我们都知道,<code>JavaScript</code> 的异步任务执行时机是不可预估的,假设有如下代码,回调函数一定会在 1000ms 后执行:</p>
<pre class="highlight"><code class="language-JavaScript">setTimeout(() => console.log('run!'), 1000);
</code></pre>
<p>但如果位于它上方的代码是这样的呢:</p>
<pre class="highlight"><code class="language-JavaScript">while (true) {}
setTimeout(() => console.log('run!'), 1000);
</code></pre>
<p>恐怕这个 <code>setTimeout</code> 这辈子都不会执行了。</p>
<p>放在网络请求场合下,这件事情就变得更加没谱了,按顺序发送的 n 个请求响应的时间是不可预估的。</p>
<p>这也许是一个大家都知道,但又比较容易被忽视的细节:<code>JavaScript</code> 不仅有能力,还非常容易达成这种 <strong>执行顺序不可预估</strong> 的情况。</p>
<p>另一方面,<code>JavaScript</code> 数据区分引用类型和值类型。被常用来做为业务数据抽象的对象,数组等均属于引用类型,以下代码很好的说明了问题:</p>
<pre class="highlight"><code class="language-JavaScript">const obj = {
name: 'yuchi',
};
const copy1 = obj;
copy1.name = 'xiaoming';
console.log(copy1); // { name: 'xiaoming' }
console.log(obj); // { name: 'xiaoming' }
</code></pre>
<p>接下来让我们把这两者组合在一起,当多个 <strong>不可预估执行顺序</strong> 的异步任务同时在回调函数里修改同一个 <strong>引用类型</strong> 时,会发生什么?</p>
<pre class="highlight"><code class="language-JavaScript">fetchA().then(res => {
setData(res);
});
fetchB().then(res => {
setData(res);
});
fetchC().then(res => {
setData(res);
});
fetchD().then(res => {
setData(res);
});
</code></pre>
<p>由于多次请求的执行顺序是不可预估的,最终 <code>data</code> 的值可能是四个请求中的任意一个,也可能是请求发送前的初始值(四个请求都失败了)。</p>
<p>当自动搜索的需求不添加防抖的策略时,其执行过程本质上与上述代码是几乎一致的。</p>
<p>具体来说的表现为:</p>
<ol>
<li>伴随用户每一次输入,<code>keyword: abc</code> 最终产生了 3 次请求,我们分别标记为 <code>search('a') | search('ab') | search('abc')</code> ,它们对应的响应回调函数我们标记为 <code>callback1 | callback2 | callback3</code>
</li>
<li>由于接口请求返回的顺序不可预估,则最后一次执行的回调函数可能是 <code>callback1 | callback2 | callback3</code> 中的任意一个。</li>
<li>变更 <code>UI</code> ,最终呈现出的检索结果可能是 <code>search('a') | search('ab') | search('abc')</code> 中的任意一个。</li>
</ol>
<p>发现了吗,这和前面提到的多个线程交替执行修改共享内存的变量导致结果不可预估的例子如出一辙。</p>
<p>也就是说,单线程异步的 <code>JavaScript</code> 代码 <strong>不仅可能出现条件竞态,还很容易</strong> 。</p>
<h1>
<a id="5.+%E5%86%8D%E7%9C%8B%E9%98%B2%E6%8A%96%E5%87%BD%E6%95%B0" href="#5.+%E5%86%8D%E7%9C%8B%E9%98%B2%E6%8A%96%E5%87%BD%E6%95%B0" class="anchor"></a>5. 再看防抖函数</h1>
<p>回头过来再看看防抖函数,我忽然间意识到,不断推迟函数执行时机的防抖策略实际上也一同帮我解决了这种异步函数高频触发场景下的条件竞态问题。</p>
<p>当然,这并不意味着在前端的条件竞态场景下,我们只能或应该通过函数防抖来解决,具体场景有具体的解决方案。</p>
<p>它带给我的启发是:一直以来我可能都忽略了 <strong>防抖函数通过放弃抖动请求规避条件竞态</strong> 的这一使用场景。</p>
<p>也就是说,<code>ChatGPT</code> 的回答和 <a href="https://github.com/Billdex">Billdex</a> 的理解是对的,这很有趣🙌。</p>
前言
故事发生的时间距离今天其实已经快一个月了,由于一些特(tuo)殊(yan)的原因,今天终于动笔开始写(shui)下了这篇博文, 再不写都™明年了 。
ChatGPT 想必是过去的一个月里...
鱼翅
https://geeknote.net/yuchiXiong
https://geeknote.net/yuchiXiong/posts/1555
2022-09-25T01:33:11Z
2022-12-11T04:56:25Z
数据结构其二 并查集
<h1>
<a id="%E5%89%8D%E8%A8%80" href="#%E5%89%8D%E8%A8%80" class="anchor"></a>前言</h1>
<p>看了下时间距离上一篇 <a href="/2022/02/13/xian-xing-biao/">线性表</a> 的文章已经过去了半年了……</p>
<p>虽然这段时间倒也没有停止学习,但也由于各种原因总之是一直拖着没产出。所幸最近因为一些机缘巧合杠上了并查集,趁热打铁的赶紧把这篇文章写了。</p>
<p>考虑到自己学习这些东西的过程其实也不是线性的(总是忍不住东搞搞西搞搞),所以这篇就以更加轻松的方式来简单分享一下最近对并查集这种数据结构的学习好了。</p>
<h1>
<a id="0.1+%E5%85%B3%E4%BA%8E%E5%B9%B6%E6%9F%A5%E9%9B%86" href="#0.1+%E5%85%B3%E4%BA%8E%E5%B9%B6%E6%9F%A5%E9%9B%86" class="anchor"></a>0.1 关于并查集</h1>
<p>其实早在我写 <a href="/2022/02/13/xian-xing-biao/">数据结构其一 线性表</a> 一文之前就已经自己写了一个简单的并查集类实现 <a href="https://github.com/yuchiXiong/data-structure-and-algorithm/commit/ae60aa61c4e4581be3b5508e8887f1f7331b53e7">yuchiXiong/data-structure-and-algorithm/commit/ae60aa61c4e4581be3b5508e8887f1f7331b53e7</a> 了,当时刚开始看 <a href="https://book.douban.com/subject/19952400/">《算法(第 4 版)》</a>,书的第一章结尾用并查集做了简单的案例。</p>
<p>打开这段代码的时候,就发现文件头写了三行注释,很好的说明了我当时写完这个代码的心境:</p>
<pre class="highlight"><code>/**
* ! 对于这个数据结构的疑问
* 1. 给定的数据一定是有序的吗?find 函数越界问题
* 2. 使用场景
*/
</code></pre>
<p>另外从代码中也发现了不少 Bug,反思了一下觉得自己一直不肯写这篇的一大原因可能是<strong>没有理解</strong>。</p>
<h1>
<a id="0.2+Why+again%3F" href="#0.2+Why+again%3F" class="anchor"></a>0.2 Why again?</h1>
<p>故事要从本月 18 号的 LeetCode 每日一题 <a href="https://leetcode.cn/problems/making-a-large-island/">LC27. 最大人工岛</a> 说起了。</p>
<p>作为一个算法废人,我一直是 “困难唯唯诺诺,简单重拳出击” 的摆烂心态,此外我一直坚信一件事情:</p>
<p><strong>当你看完一个算法题 5 分钟内一点思路都没有,那大概率是因为你还不具备解决该类问题的知(suan)识(fa)储(tao)备(lu),而非你脑子有问题/狗头</strong></p>
<p>但好巧不巧,在 LeetCode 数据结构入门的专题训练中,有一道 <a href="https://leetcode.cn/problems/max-area-of-island/">LC695. 岛屿的最大面积</a>。</p>
<p>这道题的思路很简单,由于每一个岛屿单元所处的岛屿面积一定等于它上下左右四个方向邻接单元上的岛屿面积之和,很容易写出 DFS 的代码模板。</p>
<p>当然,要记得定义递归的终止条件,即如果当前单元为 0 时,意味着已经遍历到海洋了,不需要继续了。另外为了防止重复遍历,代码还对已经遍历过的岛屿进行了标记。</p>
<p>代码见 <a href="https://github.com/yuchiXiong/data-structure-and-algorithm/commit/63659b20643b1e6b7b8bfb59efd283d21257868a">yuchiXiong/data-structure-and-algorithm/commit/63659b20643b1e6b7b8bfb59efd283d21257868a</a></p>
<p>显而易见的事情是 <a href="https://leetcode.cn/problems/making-a-large-island/">LC27. 最大人工岛</a> 的一种暴力解法就是尝试将地图上的每一片海洋都改成岛屿然后求得最大岛屿面积,最终得到的最大值就是题解。</p>
<p>虽然不能保证效率,但能暴力解决的问题都不是问题/再次狗头,开整。</p>
<p>在经过了一段时间的死嗑之后,我写下了这段代码:</p>
<pre class="highlight"><code class="language-JavaScript">/**
* @param {number[][]} grid
* @return {number}
*/
const largestIsland = function (grid) {
let count = 2;
let max = maxAreaOfIsland(grid, count++);
for (let i = 0; i < grid.length; i++) {
for (let j = 0; j < grid[i].length; j++) {
// 跳过岛屿,我们只对海洋做操作
if (grid[i][j] !== 0) {
continue;
}
// 尝试检测当前海洋的四面是否存在岛屿,如果四面都没有岛屿,则当前人工岛面积为 1
if (tryDFS(grid, i, j) === 0) {
max = max < 1 ? 1 : max;
continue;
}
// 修改当前单元 海洋->岛屿 并求得最大岛屿面积
const origin = grid[i][j];
grid[i][j] = 1;
const cur = maxAreaOfIsland(grid, count++);
max = max < cur ? cur : max;
grid[i][j] = origin;
}
}
return max;
};
/**
* @param {number[][]} grid
* @param {number} count
* @return {number}
*/
var maxAreaOfIsland = function (grid, count) {
let max = 0;
for (let i = 0; i < grid.length; i++) {
for (let j = 0; j < grid[i].length; j++) {
if (grid[i][j] === 1) {
max = Math.max(max, dfs(grid, i, j, count));
}
}
}
return max;
};
/**
* @param {number[][]} grid
* @param {number} i
* @param {number} j
* @param {number} count
* @return {number}
*/
const dfs = (grid, i, j, count) => {
if (i < 0 || i >= grid.length || j < 0 || j >= grid[0].length) return 0;
if (grid[i][j] === 0 || grid[i][j] === count) return 0;
grid[i][j] = count;
return 1
+ dfs(grid, i - 1, j, count)
+ dfs(grid, i + 1, j, count)
+ dfs(grid, i, j - 1, count)
+ dfs(grid, i, j + 1, count);
}
/**
* 返回当前单元四个方向范围内的岛屿面积总和
* @param {number[][]} grid
* @param {number} i
* @param {number} j
* @return {number}
*/
const tryDFS = (grid, i, j) => {
return (i >= 1 ? (grid[i - 1][j]) : 0)
+ (i < grid.length - 1 ? grid[i + 1][j] : 0)
+ (j >= 1 ? grid[i][j - 1] : 0)
+ (j < grid[i].length - 1 ? grid[i][j + 1] : 0);
}
</code></pre>
<p>其中<code>maxAreaOfIsland</code>函数是直接复制的 <a href="https://leetcode.cn/problems/max-area-of-island/">LC695. 岛屿的最大面积</a> 的答案。</p>
<p>需要解释的几点:</p>
<ol>
<li>前面提到在 DFS 过程中会标记已经遍历过的岛屿,也就是说每一次<code>maxAreaOfIsland</code>后<code>grid</code>数组中的岛屿标识会由 1 变成 2,为了使程序能够继续执行第二轮<code>maxAreaOfIsland</code>,需要将<code>grid</code>数组复原,但这样的又会增加相当大的开销,于是最终我将函数改成了上面的版本,每次传入一个<code>count</code>变量来标识轮次,这样 DFS 也能重复执行了;</li>
<li>为了尽可能提高效率,程序首先跳过了岛屿单元,然后对四周都没有岛屿的单元做了提前处理,详见注释;</li>
</ol>
<p>在完成了一些简单的优化后,一些之前不能通过的 case 也能跑出一个可以接受的执行时间了,但最终还是卡在了 case70 上。</p>
<p>在对这个 case 进行了简单的分析之后我发现:</p>
<ol>
<li>这是一个 417*417 规模的数组;</li>
<li>岛屿和海洋的数量几乎相同;</li>
<li>岛屿与海洋的分布稀疏;</li>
</ol>
<p>不难发现上述优化策略很难产生效果。</p>
<p>本来这题就该到此为止了,毕竟死磕没有意义,又不能说服自己 “答案是对的,只是超时了而已”。</p>
<p>然而接下来一个不经意的操作,让我发现了这一题的相关标签中赫然写着一个并查集。</p>
<p>如果是我没有接触过的东西那倒也罢了,一个我手写过的东西为什么能把我困住?</p>
<h1>
<a id="1.+%E5%B9%B6%E6%9F%A5%E9%9B%86" href="#1.+%E5%B9%B6%E6%9F%A5%E9%9B%86" class="anchor"></a>1. 并查集</h1>
<p>既然这是一篇分享并查集的博客,那么还是有必要介绍一下并查集的。</p>
<p>从字面上来看,并查集是一种集合数据结构,它主要定义了联合(union)和查询(find)两种操作,因此很多时候也被叫做 UnionFind。</p>
<blockquote>
<p><a href="https://zh.m.wikipedia.org/zh-sg/%E5%B9%B6%E6%9F%A5%E9%9B%86">wiki:并查集</a>
查询:查询某个元素属于哪个集合,通常是返回集合内的一个 “代表元素”。这个操作是为了判断两个元素是否在同一个集合之中。
合并:将两个集合合并为一个。
添加:添加一个新集合,其中有一个新元素。添加操作不如查询和合并操作重要,常常被忽略。</p>
</blockquote>
<p>并查集定义的查询操作能够快速的反应集合中两个元素的连通性,而合并/联合操作则可以将集合中的两个元素连通,它被广泛的应用在动态连通性问题中。</p>
<p>对并查集应用的一个简单的例子就是族谱,设想我们如何判断两个人 A 和 B 是否来自于同一个家族?</p>
<p>一个简单的方法就是向上溯源,如果 A 和 B 的长辈中有同一人,则我们可以认为这两个人来自同一个家族。</p>
<p>将问题的规模扩展到三人,如果我们当前已经知晓 A 和 B 来自同一个家族,则我们只需要证明第三人 C 与 A/B 任意一人来自同一个家族,就可以认为 C 与另外一人来自同一个家族。</p>
<p>由此我们可以得到并查集中元素的三个重要性质:</p>
<ol>
<li>自反性,A 和 A 是连通的;</li>
<li>对称性,如果 A 和 B 是连通的,那么 B 和 A 也是连通的;</li>
<li>传递性,如果 A 和 B 是连通的且 B 和 C 是连通的,那么 A 和 C 也是连通的。</li>
</ol>
<h1>
<a id="2.+%E5%B9%B6%E6%9F%A5%E9%9B%86%E7%9A%84%E5%AE%9E%E7%8E%B0" href="#2.+%E5%B9%B6%E6%9F%A5%E9%9B%86%E7%9A%84%E5%AE%9E%E7%8E%B0" class="anchor"></a>2. 并查集的实现</h1>
<p>只提概念不谈实现是没有意义的。</p>
<p>并查集的主要操作有两个,查询和合并,实现这两个方法是实现并查集最重要的部分,除此之外在不同版本的并查集实现中往往存在着各种 API 定义上的差异,但数据结构本身是用来解决程序问题的,如果拘泥于 API 形式亦是有问题的,这个问题后面还会再提到。</p>
<p>在前面的例子里我们提到一个问题:如何判断两个人 A 和 B 是否来自于同一个家族?</p>
<p>给出的答案很简单:如果 A 和 B 的长辈中有同一人,则我们可以认为这两个人来自同一个家族。</p>
<p>在并查集的实现中我们亦可以这样做。</p>
<p>在开始之前,我们要简单分析一下上面的描述,在这个系统里存在一种父子关系,且多个元素可能有一个共同的根元素,很显然,没有比树更合适的数据结构了。</p>
<p>当然,并查集具有检测两个元素是否连通的能力,也就意味着在并查集中可能存在多个没有交集的树,因此我们更进一步的表达为森林这样一种数据结构。</p>
<h2>
<a id="2.1+%E6%9E%84%E5%BB%BA%E4%B8%80%E4%B8%AA%E5%B9%B6%E6%9F%A5%E9%9B%86" href="#2.1+%E6%9E%84%E5%BB%BA%E4%B8%80%E4%B8%AA%E5%B9%B6%E6%9F%A5%E9%9B%86" class="anchor"></a>2.1 构建一个并查集</h2>
<p>实现一个基本的并查集需要一个合适的变量来存储森林这种结构,前面在 <a href="/2022/02/13/xian-xing-biao/">线性表</a> 一文中提到过,无论逻辑呈现什么样的结构,在计算机存储时都只有线性存储与链式存储两种。这里一样拥有这种自由,<a href="https://book.douban.com/subject/19952400/">《算法(第 4 版)》</a> 中使用了顺序结构。</p>
<p>使用顺序表实现森林时首先将并查集中的所有元素的值作为 key 创建一个顺序表(姑且不去讨论元素值类型与顺序表下标数值类型转换的问题),在初始化时它的值是它自身,即当前元素仅与自己连通。</p>
<p>而后在每次进行合并操作时,我们会修改元素的指向,类似下面这样</p>
<pre class="highlight"><code class="language-JavaScript">trees[targetA] = targetB;
</code></pre>
<p>此时意味着元素 A 的父元素是元素 B,同时以意味着元素 A 与元素 B 连通。</p>
<h2>
<a id="2.1+%E6%9F%A5%E8%AF%A2" href="#2.1+%E6%9F%A5%E8%AF%A2" class="anchor"></a>2.1 查询</h2>
<p>就像家族关系的例子一样,判断两个元素是否连通,只需要判断它们是否具有一个公共的根元素即可。</p>
<p>基于上面 2.1 构建的存储结构,找到根元素只需要不断的向上查找,一直找到到某一个自身与父元素相等的节点,这个元素就是整个树的根元素。</p>
<pre class="highlight"><code class="language-JavaScript">function getRootNode(target) {
while (trees[target] !== target) {
target = trees[target];
}
return target;
}
</code></pre>
<p>接下来的判断就简单了,当<code>getRootNode(elementA)</code>与<code>getRootNode(elementB)</code>相同时,很显然两个元素是连通的。</p>
<h2>
<a id="2.2+%E5%90%88%E5%B9%B6" href="#2.2+%E5%90%88%E5%B9%B6" class="anchor"></a>2.2 合并</h2>
<p>由上面的逻辑要实现合并也非常简单,不过在进行两个元素的合并之前首先要判断是否连通,如果元素 A 和 B 已经连通了,就没有必要浪费时间了。</p>
<p>要进行两个元素的连通,只要把其中一方的父元素指向另一方就可以了,不过通常来说我们并不能得知当前正在进行连通的两个元素是独立的还是处于一个树中,贸然修改其指向有可能会使它脱离原来的树,因此我们应该对两个树的根元素进行操作。</p>
<p>简单的参考代码如下:</p>
<pre class="highlight"><code class="language-JavaScript">function unionNode(a, b) {
const parentA = getRootNode(a);
const parentB = getRootNode(b);
if (parentA === parentB)
return;
parent[parentB] = parentA;
}
</code></pre>
<h2>
<a id="2.3+%E6%9B%B4%E8%BF%9B%E4%B8%80%E6%AD%A5" href="#2.3+%E6%9B%B4%E8%BF%9B%E4%B8%80%E6%AD%A5" class="anchor"></a>2.3 更进一步</h2>
<p>基本的实现其实到这里就已经够了,但设想这样一种情况:</p>
<pre class="highlight"><code class="language-JavaScript">const uf = new UnionFind();
uf.union(1, 2);
uf.union(3, 2);
uf.union(4, 3);
uf.union(5, 4);
</code></pre>
<p>尝试在纸上画出这组 case 的连通过程,很容易发现由于每一次构建的过程中的元素 A 都是一个指向自身的只有一个节点的树,导致合并以后构建的树实际退化成了单链表。对于树中任意一个节点要找到它的根元素,单链表的效率一定是低于一颗合理排布的树的。</p>
<p>优化策略本质上是要维护树的平衡性,我们只需要在连接的过程中把更小的那颗树连接到更大的那棵树的根节点上,就可以尽可能保证树的平衡性,也可以避免上述极端情况的发生。不过要实现这一步,我们需要增加一个类变量来记录每一颗树的大小,以及在每一次连通的时候对树的大小进行累加。</p>
<p>调整以后的代码见 <a href="https://github.com/yuchiXiong/data-structure-and-algorithm/blob/e68b19772b57eaac4e49dba89f783266f0f9eef3/%E5%B8%B8%E8%A7%81%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E5%AE%9E%E7%8E%B0/UnionFind/UnionFind.cpp#L27">yuchiXiong/data-structure-and-algorithm/blob/e68b19772b57eaac4e49dba89f783266f0f9eef3/常见数据结构实现/UnionFind/UnionFind.cpp#L27</a></p>
<p>相比二叉树,一个树可以拥有的子节点数是没有限制的。如果一颗树除根元素以外的所有元素均指向同一个根元素,即这棵树只有两层,那么除根元素以外的任意一个节点找到它的根元素都只需要向上查找一次。</p>
<p>基于这个假设,我们可以在查找的过程中进行路径压缩,使树变得更加扁平化。</p>
<p>理想情况下,所有元素都应该直接连接在根元素下作为根元素的子节点,下面代码巧妙的实现了它:</p>
<pre class="highlight"><code class="language-JavaScript">function getRootNode(target) {
if (this.trees[target] !== target) {
this.trees[target] = this.getRootNode(this.trees[target]);
}
return this.trees[target];
}
</code></pre>
<p>为什么要在查找的过程中而不是在连通的过程中修改树的结构?</p>
<blockquote>
<p>理想情况下,我们希望每个节点都直接链接到它的根节点上,但我们又不想像 quick-find 算法那样通过修改大量链接做到这一点。我们接近这种理想状态的方式很简单,就是在检查节点的同时将它们直接链接到根节点。这种方法乍一看很激进,但它的实现非常容易,而且这些树并没有阻止我们进行这种修改的特殊结构:如果这么做能够改进算法的效率,我们就应该实现它。
————<a href="https://book.douban.com/subject/19952400/">《算法(第 4 版)》</a></p>
</blockquote>
<p>至此我们实现了并查集数据结构最重要的部分,可以看到的是它的代码并不复杂,但又很好的给出了一个解决连通性问题的方案。
代码参考 <a href="https://github.com/yuchiXiong/data-structure-and-algorithm/blob/e68b19772b57eaac4e49dba89f783266f0f9eef3/%E5%B8%B8%E8%A7%81%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E5%AE%9E%E7%8E%B0/UnionFind/UnionFind.cpp">UnionFind.cpp</a></p>
<h1>
<a id="3.+%E5%9B%9E%E5%88%B0%E5%B2%9B%E5%B1%BF%E4%B8%8A" href="#3.+%E5%9B%9E%E5%88%B0%E5%B2%9B%E5%B1%BF%E4%B8%8A" class="anchor"></a>3. 回到岛屿上</h1>
<p>我自然没有忘记小岛上还有问题等着我解决。</p>
<p>并查集中的每一个连通元素整体都被称为一个连通分量,当一个并查集中的所有元素都连通时,这个并查集只存在一个连通分量。</p>
<p>回到 <a href="https://leetcode.cn/problems/max-area-of-island/">LC695. 岛屿的最大面积</a> 的问题中,如果把岛屿的每一个单元(即<code>grid</code>二维数组的每一个元素)看作并查集中的一个元素,则基于这组元素构建的并查集中,一定包含连通分量数量 N 个岛屿,其中最大面积的岛屿实际上就是最大连通分量的大小。</p>
<p>去除了 DFS 的代码基于并查集进行了重新实现:
<a href="https://github.com/yuchiXiong/data-structure-and-algorithm/commit/445c5d669c468667ddfed32b921759cd586a815c">yuchiXiong/data-structure-and-algorithm/commit/445c5d669c468667ddfed32b921759cd586a815c</a></p>
<p>不得不吐槽的一件事情:代码真的长了好多……</p>
<p>不过依然有一些地方值得留意:</p>
<ol>
<li>在这一版代码中,不再直接使用顺序表来存储父节点信息,而是通过 Hash 表替代,这样做的好处在于可以接受更多类型的 key;</li>
<li>把每一个元素在二维数组扁平化以后的序号作为并查集的元素,<code>grid[i][j]</code>在并查集中的 key 为<code>(i - 1) * grid.length + j</code>;</li>
</ol>
<p>在解决这个问题时,并查集的优点并没有那么明显,一定程度上来说代码还显得十分啰嗦,而在解决 <a href="https://leetcode.cn/problems/making-a-large-island/">LC27. 最大人工岛</a> 问题时,并查集带来的收益就要明显的多了。</p>
<p>使用地图信息构建一次并查集以后,我们可以清晰的得知如下信息:</p>
<ol>
<li>当前地图中的岛屿数量;</li>
<li>当前地图中的每一个单元所处岛屿的面积;</li>
<li>当前地图中任意两个岛屿单元是否处于同一个岛屿中;</li>
</ol>
<p>利用这些信息,我们只需要遍历地图,在每一个海洋单元上计算它的上下左右四个单元所处的岛屿面积之和,然后找到最大值即可得到题解。</p>
<p>当然,如果上下左右四个邻接单元有位于同一个岛屿的单元,我们只计算一次。</p>
<p><a href="https://github.com/yuchiXiong/data-structure-and-algorithm/commit/d8bbe157f7a07d881e7b06cee94e5a3a25a70d0a">yuchiXiong/data-structure-and-algorithm/commit/d8bbe157f7a07d881e7b06cee94e5a3a25a70d0a</a></p>
<p>至此,两道 LeetCode 问题也圆满的解决了。</p>
<h1>
<a id="4.+%E6%9C%80%E5%90%8E%E8%AF%B4%E4%B8%A4%E5%8F%A5" href="#4.+%E6%9C%80%E5%90%8E%E8%AF%B4%E4%B8%A4%E5%8F%A5" class="anchor"></a>4. 最后说两句</h1>
<p>重新审视了去年实现的代码,除了注释中的疑问外还有不少的 bug,于是趁着这个机会也一并修复了 <a href="https://github.com/yuchiXiong/data-structure-and-algorithm/commit/e68b19772b57eaac4e49dba89f783266f0f9eef3">Github: yuchiXiong/data-structure-and-algorithm/commit/e68b19772b57eaac4e49dba89f783266f0f9eef3</a>。</p>
<p>并查集是用来处理元素连通性问题的数据结构,它仅仅关注元素与元素之间的连通问题,不关心森林用顺序存储还是链式存储实现。在合适的时间选择合适的方法才是最重要的。</p>
<p>在查找一些并查集代码实现的案例时,会发现不同人写下的代码是不同的,有的初始化时传递了连通分量来进行,有的仅在使用时创建并进行自连通,但本质上这些都是根据实际问题不同而自由发挥的。</p>
<p>另外一件值得一提的事情是,在尝试找一些编程语言自带的 DisjointSet 时,发现 Ruby 的 Set 类中提供了一个 disjoint?方法,当两个集合有相同的元素时,该方法返回 false,反之则返回 true。Set 是一种不会出现重复元素的数据结构,并查集同样是没有交集的森林,如果把森林中的每个树看作是一个 Set,仅仅一个函数就可以以完整的实现并查集的功能了。一方面我十分感叹竟然可以通过如此简单的方式实现并查集,同时另一方面则是更多的引人深思,数据结构的 API 设计本就没有标准可言。</p>
<h1>
<a id="4.+%E5%8F%82%E8%80%83" href="#4.+%E5%8F%82%E8%80%83" class="anchor"></a>4. 参考</h1>
<ul>
<li><a href="https://book.douban.com/subject/19952400/">《算法(第 4 版)》</a></li>
<li><a href="https://ruby-doc.org/stdlib-2.4.0/libdoc/set/rdoc/Set.html#method-i-disjoint-3F">Ruby Class: Set</a></li>
</ul>
前言
看了下时间距离上一篇 线性表 的文章已经过去了半年了……
虽然这段时间倒也没有停止学习,但也由于各种原因总之是一直拖着没产出。所幸最近因为一些机缘巧合杠上了并查集,趁热打铁的赶紧把这篇文...
鱼翅
https://geeknote.net/yuchiXiong
https://geeknote.net/yuchiXiong/posts/391
2022-03-02T01:01:10Z
2022-10-28T14:42:00Z
数据结构其一 线性表
<h1>
<a id="%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%EF%BC%9A+%E7%BA%BF%E6%80%A7%E8%A1%A8" href="#%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%EF%BC%9A+%E7%BA%BF%E6%80%A7%E8%A1%A8" class="anchor"></a>数据结构: 线性表</h1>
<h1>
<a id="%E5%89%8D%E8%A8%80%EF%BC%88%E5%95%B0%E5%97%A6%E4%B8%A4%E5%8F%A5%EF%BC%89" href="#%E5%89%8D%E8%A8%80%EF%BC%88%E5%95%B0%E5%97%A6%E4%B8%A4%E5%8F%A5%EF%BC%89" class="anchor"></a>前言(啰嗦两句)</h1>
<p>作为一名普通野鸡大学的毕业的科班生,虽然在学校学过《数据结构》这样一门课程,但彼时的我所有的编码经验也就只是写过一些诸如杨辉三角,水仙花数之类的例题,因而在结课后很快就还给老师了……</p>
<p>最近颇乏于<del>写应用层代码</del><strong>画页面</strong>,终于又双叒叕难得想学数据结构了,遂打算开一个系列,希望能够有始有终的写完。</p>
<p><a href="https://github.com/yuchiXiong/data-structure">yuchiXiong/data-structure: 我又双叒叕准备学数据结构啦</a></p>
<h1>
<a id="1.+%E7%BA%BF%E6%80%A7%E8%A1%A8%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F" href="#1.+%E7%BA%BF%E6%80%A7%E8%A1%A8%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F" class="anchor"></a>1. 线性表是什么?</h1>
<blockquote>
<p><a href="https://zh.wikipedia.org/wiki/%E7%BA%BF%E6%80%A7%E8%A1%A8">wiki</a>: 线性表是一个具有顺序的,由同类型数据组成的集合,它是常用的数据结构之一。
线性表具有几个基本特征:</p>
</blockquote>
<ol>
<li>有限性,线性表是一个有限序列。</li>
<li>有序性,线性表具有基本的顺序属性。</li>
<li>同一性,线性表元素具有相同的类型。</li>
</ol>
<p>关于上述3,在参考广义表的相关定义后,我认为应该更完整的描述为:线性表由同类型数据组成,且该类型被限定为原子类型(如整数、字符串、布尔值等),而非结构类型(如数组、结构体、类等)。</p>
<p>关于 <code>线性</code> ,我们知道数据结构中还定义了其他诸多非线性数据结构如树、图等。它们在计算机中存储时并非真的表现为树状或图状,而是从逻辑上抽象为树状或图状。线性结构与之类似,当我们将线性表的元素按照逻辑顺序排列时,它呈现为一条线段,因而我们称之为线性结构。对应的,当我们将广义表的元素按照其层次排列时,它并不是一条线段,而更像是一个平面,这正是我们说广义表是一种非线性数据结构的原因,同时也是线性表同一性限制存在的原因。</p>
<h1>
<a id="2.+%E9%A1%BA%E5%BA%8F%E5%AD%98%E5%82%A8%E4%B8%8E%E9%93%BE%E5%BC%8F%E5%AD%98%E5%82%A8" href="#2.+%E9%A1%BA%E5%BA%8F%E5%AD%98%E5%82%A8%E4%B8%8E%E9%93%BE%E5%BC%8F%E5%AD%98%E5%82%A8" class="anchor"></a>2. 顺序存储与链式存储</h1>
<p>线性表的线性是逻辑层面的,但实际无论是线性表,广义表,还是树,图等,在计算机中存储时,都无外乎两种形式:顺序存储和链式存储。</p>
<h2>
<a id="2.1+%E9%A1%BA%E5%BA%8F%E5%AD%98%E5%82%A8%E4%B8%8E%E9%A1%BA%E5%BA%8F%E8%A1%A8" href="#2.1+%E9%A1%BA%E5%BA%8F%E5%AD%98%E5%82%A8%E4%B8%8E%E9%A1%BA%E5%BA%8F%E8%A1%A8" class="anchor"></a>2.1 顺序存储与顺序表</h2>
<p>在计算机中存储数据的第一种方式是顺序存储,基于顺序存储方式实现的线性表称为顺序表,举一个比较形象的例子:</p>
<p><em>你和小伙伴一起去看电影,你当然希望可以和小伙伴们坐在一起,因而你们选择购买了某一座位开始往后的多个</em><em><strong>连续</strong></em><em>的座位。</em></p>
<p>不难发现,在这一案例中有几个重要的细节:</p>
<ol>
<li>我们需要一次性购买多个连续的座位。</li>
<li>当我们知道第一个座位的编号时,很容通过第一个座位的编号,计算出后面的座位的编号。</li>
<li>当我们希望在保持原有的座位顺序的前提下,插入一个新的座位时,我们需要将原有的每一个座位向后移动一个位置。</li>
<li>当我们希望在保持原有的座位顺序的前提下,删除一个已有座位时,我们需要将原有的每一个座位向前移动一个位置。</li>
</ol>
<p>我们将这个例子映射到顺序表中,就可以得到顺序表的几个重要特征:</p>
<ol>
<li>在使用顺序表时,我们需要一次性申请存储元素所需要的空间。</li>
<li>通过元素下标我们可以快速的访问元素。</li>
<li>在插入/删除顺序表中的元素时,我们需要将原有的每一个元素向后/向前移动一个位置。</li>
</ol>
<p>这些特征决定了顺序表最重要的优点:访问速度快。通过下标我们能在 <code>O(1)</code> 时间复杂度内访问到某个元素。</p>
<p>同时也暴露了顺序表的缺点:</p>
<ol>
<li>由于在使用前需要一次性申请指定容量的空间,当遇到数据增长速度较快的场景时,顺序表容量可能会达到最大值而无法添加新的数据,但如果一次性申请的空间过大,则又容易造成浪费。</li>
<li>如果需要对顺序表进行扩容,则需要额外的空间来进行数据的复制。</li>
<li>如果需要插入元素到顺序表中,则需要将原有的每一个元素向后移动一个位置,时间复杂度为 <code>O(n)</code>。</li>
</ol>
<p>最后,以 <code>C</code> 语言为例,我们可以将数组看作一个顺序表,但值得注意的是,在部分动态语言如 <code>Python/JavaScript/Ruby</code> 等中,数组元素的类型是动态的,它可以是数组、对象等任何类型的数据,此时的数组则更应该看作广义表而非线性表。</p>
<h2>
<a id="2.2+%E9%93%BE%E5%BC%8F%E5%AD%98%E5%82%A8%E4%B8%8E%E9%93%BE%E8%A1%A8" href="#2.2+%E9%93%BE%E5%BC%8F%E5%AD%98%E5%82%A8%E4%B8%8E%E9%93%BE%E8%A1%A8" class="anchor"></a>2.2 链式存储与链表</h2>
<p>在计算机中存储数据的另一种方式是链式存储,基于链式存储方式实现的线性表称为链表。</p>
<p>链表的每一个节点分散存储在不同的地址,而后使用一个 <code>next</code> 指针来指向下一个节点,最终实现了维护其逻辑上的线性的目的。这样做的好处是当我们需要对链表进行修改时,无需再对其他元素进行移动操作:</p>
<ol>
<li>当需要在链表中插入新节点时,我们只需要修改目标位置上一个位置节点的 <code>next</code> 指针指向新节点,然后将新节点的 <code>next</code> 指针指向原有位置上的节点即可。</li>
<li>当需要在链表中删除一个节点时,我们只需要将目标位置上一个位置节点的 <code>next</code> 指针指向目标位置下一个节点即可。</li>
<li>当需要在表头插入新节点时,我们只需要将新节点的 <code>next</code> 指针指向原有的表头即可。</li>
<li>当需要在表尾插入新节点时,我们只需要将表尾节点的 <code>next</code> 指针指向新节点即可。</li>
</ol>
<p>但由于链表的线性关系是通过 <code>next</code> 指针来实现的,因此当希望读取链表的某一个元素时,无法通过下标的方式以 <code>O(1)</code> 的时间复杂度完成而是需要进行遍历,时间复杂度为 <code>O(n)</code>。</p>
<p>基本单链表节点实现 <a href="https://github.com/yuchiXiong/data-structure/blob/main/LinearList/LinkedNode.cpp">LinkedNode.cpp</a></p>
<h1>
<a id="3.+%E5%B8%B8%E8%A7%81%E7%BA%BF%E6%80%A7%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84" href="#3.+%E5%B8%B8%E8%A7%81%E7%BA%BF%E6%80%A7%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84" class="anchor"></a>3. 常见线性数据结构</h1>
<p>在实际的使用过程中,我们经常基于线性表来实现一些更符合业务场景的数据结构。</p>
<h2>
<a id="3.1+%E5%8F%98%E9%95%BF%E6%95%B0%E7%BB%84" href="#3.1+%E5%8F%98%E9%95%BF%E6%95%B0%E7%BB%84" class="anchor"></a>3.1 变长数组</h2>
<p>由于顺序表始终需要维护一个固定的长度,在使用过程中扩容极不方便,因而我们可以基于顺序表来实现一个变长数组,使其能够在使用的过程中进行动态扩容。</p>
<p><code>Java Collection Framework</code>(简称 <code>JCF</code>) 中实现的 <code>ArrayList/Vector</code> 以及 <code>CPP STL Container adaptors</code> 中提供的 <code>Vector</code> 都可以看作一个变长数组。其实现的原理大致为,当向数组中添加或删除元素时,首先检查数组的容量:</p>
<ol>
<li>如果当前数组的容量不足以添加新的元素,将数组扩容(通常为原来的两倍),然后将原来的数据复制到新的数组中。</li>
<li>如果当前数组的使用容量低于阈值(通常是当前容量的 <code>1/4</code> ),将数组的容量缩小为原来的一半,然后将原来的数据复制到新的数组中。</li>
</ol>
<p>变长数组实现 <a href="https://github.com/yuchiXiong/data-structure/blob/main/LinearList/Vector.cpp">Vector.cpp</a></p>
<h2>
<a id="3.2+%E9%93%BE%E5%BC%8F%E7%BA%BF%E6%80%A7%E5%AE%B9%E5%99%A8" href="#3.2+%E9%93%BE%E5%BC%8F%E7%BA%BF%E6%80%A7%E5%AE%B9%E5%99%A8" class="anchor"></a>3.2 链式线性容器</h2>
<p>基础的链表节点实现在使用时并不太方便,我们可以基于此封装一个与变长数组相似的链式线性容器。</p>
<p>基于单链表的链式线性容器实现 <a href="https://github.com/yuchiXiong/data-structure/blob/main/LinearList/SLinkedList.cpp">SLinkedList.cpp</a></p>
<p>基于双向链表的链式线性容器实现 <a href="https://github.com/yuchiXiong/data-structure/blob/main/LinearList/LinkedList.cpp">LinkedList.cpp</a></p>
<h2>
<a id="3.3+%E9%A1%BA%E5%BA%8F%E6%A0%88" href="#3.3+%E9%A1%BA%E5%BA%8F%E6%A0%88" class="anchor"></a>3.3 顺序栈</h2>
<p>栈是一种受限制的线性表,其特点是 <code>First In Last Out</code> (简称 <code>FILO</code> ),即先进后出。</p>
<p>现实生活中的羽毛球桶就可以看作一个栈,当一个球桶里装满球时,只能先取出后放进的羽毛球,再取出倒数第二次序放入的羽毛球。</p>
<p>栈有两个基本操作:</p>
<ul>
<li>将元素押入栈顶,即入栈</li>
<li>从栈中弹出栈顶元素,即出栈</li>
</ul>
<p>栈是一种非常实用的数据结构,例如前端开发中比较常提及的页面跳转栈,在页面跳转过程中,我们可以将页面跳转的每一步都保存在栈中,当页面进行回退操作时,我们可以从栈中取出每一步的跳转页面,直到栈为空。</p>
<p>基于线性表我们可以很快的实现一个栈数据结构,不过由于线性表具体分为顺序表和链表两种,因而实现栈也有两种方式。其中通过顺序表实现的栈被称为顺序栈。</p>
<p>实现顺序栈通常来说不太困难,但考虑到顺序表固定长度的特性,为了使顺序栈拥有更好的可用性,我们可以通过变长数组来实现。而对于栈的弹出和押入操作,只需要简单的对变长数组队尾的元素进行简单的操作即可。同时,由于栈的操作中不涉及到在已有序列中插入和删除元素的操作,因而恰好避开了顺序表在插入和删除元素时需要移动元素的问题。</p>
<p>顺序栈实现 <a href="https://github.com/yuchiXiong/data-structure/blob/main/LinearList/ArrayStack.cpp">ArrayStack.cpp</a></p>
<h2>
<a id="3.4+%E9%93%BE%E5%BC%8F%E6%A0%88" href="#3.4+%E9%93%BE%E5%BC%8F%E6%A0%88" class="anchor"></a>3.4 链式栈</h2>
<p>除了使用顺序表实现的栈,我们还可以使用链表来实现栈。链式栈和顺序栈本质上来说没有什么太大的区别,只是底层的存储方式不太一样。</p>
<p>基于链表实现的栈,主要的思路是维护一个链表的头节点,当押入新元素到栈内时,只需要基于新元素创建一个新的节点,并将头节点插入到新节点的后面即可。而弹出时,只需要将内部维护的头节点更新为当前头节点的下一个节点即可。以下给出的参考代码实现是基于双向链表的,但由于栈数据结构始终只对尾节点进行操作,因而使用双向链表并不能简化操作,甚至还会导致实现的抽象难度增加。</p>
<p>链式栈实现 <a href="https://github.com/yuchiXiong/data-structure/blob/main/LinearList/LinkedStack.cpp">LinkedStack.cpp</a></p>
<h2>
<a id="3.5+%E9%A1%BA%E5%BA%8F%E9%98%9F%E5%88%97%EF%BC%8C%E5%81%87%E6%BA%A2%E5%87%BA%E5%92%8C%E5%BE%AA%E7%8E%AF%E9%98%9F%E5%88%97" href="#3.5+%E9%A1%BA%E5%BA%8F%E9%98%9F%E5%88%97%EF%BC%8C%E5%81%87%E6%BA%A2%E5%87%BA%E5%92%8C%E5%BE%AA%E7%8E%AF%E9%98%9F%E5%88%97" class="anchor"></a>3.5 顺序队列,假溢出和循环队列</h2>
<p>队列同样是一种受限制的线性表,其特点是 <code>First In First Out</code> (简称 <code>FIFO</code> ),即先进先出。</p>
<p>在现实生活中,我们其实经常使用到队列的特性,比如超市的排队等等。对于一个队列我们有两种基本的操作:</p>
<ul>
<li>元素入队,即把元素插入队列的尾部</li>
<li>元素出队,即把队列的头元素删除</li>
</ul>
<p>与顺序栈和链式栈相同,队列也有两种实现方式,一种是顺序队列,另一种是链式队列。</p>
<p>同样考虑到顺序表定长的特性,在实际实现中我们可以考虑基于变长数组来实现队列以支持自适应动态扩容和缩容。</p>
<p>当元素入队时,我们只需要把元素插入到变长数组的尾部即可,但当元素出队时问题就显的稍微复杂一些了。当元素出队时,我们需要删除队列的头元素,考虑到如果将后续元素都向前位移必定会造成大量的性能损耗,我们可以维护一个标识来表示实际的队列头。然而这样又回引出新的问题,既当元素不断的出队与入队,队头的标识会越来越靠后,这样势必会导致顺序队列中存在大量的连续的空间浪费。</p>
<p>基于变长数组的顺序队列实现 <a href="https://github.com/yuchiXiong/data-structure/blob/main/LinearList/ArrayQueue.cpp">ArrayQueue.cpp</a></p>
<p>我们可以使用循环队列来解决这些问题,既当队列元素入队时,如果当前队尾元素的下标超过了队列的最大容量,那么我们就将队尾元素的下标重置为0,这样可以保证队列中的元素是连续的同时,不浪费之前的空间。而当元素出队时,我们还是一样删除队尾元素即可。当然,由于循环队列从存储上来说并不是完全连续的,在对变长数组进行动态扩容时我们需要两段的元素列表分别拷贝到对应的位置中。</p>
<p>循环顺序队列实现 <a href="https://github.com/yuchiXiong/data-structure/blob/main/LinearList/CircularQueue.cpp">CircularQueue.cpp</a></p>
<h2>
<a id="3.6+%E9%93%BE%E5%BC%8F%E9%98%9F%E5%88%97" href="#3.6+%E9%93%BE%E5%BC%8F%E9%98%9F%E5%88%97" class="anchor"></a>3.6 链式队列</h2>
<p>很显然,基于链式存储结构实现的队列被称为链式队列。</p>
<p>由于队列的两种常用操作分别作用于队头和队尾,当我们使用单链表实现时,操作队尾元素很容易,但是操作队头元素则需要遍历整个队列,这样会带来较大的性能开销。此时我们可以增加一个指向上一个节点的 <code>prev</code> 指针来将单链表修改为双向链表,然后以同样的方式保存对头元素的指针和队尾元素的指针,之后就可以方便快捷的进行出队和入队操作了。</p>
<p>双向链表实现 <a href="https://github.com/yuchiXiong/data-structure/blob/main/LinearList/DoublyLinkedNode.cpp">DoublyLinkedNode.cpp</a></p>
<p>基于双向链表的链式队列实现 <a href="https://github.com/yuchiXiong/data-structure/blob/main/LinearList/LinkedQueue.cpp">LinkedQueue.cpp</a></p>
<h1>
<a id="4.+%E5%8F%82%E8%80%83" href="#4.+%E5%8F%82%E8%80%83" class="anchor"></a>4. 参考</h1>
<ul>
<li><a href="https://en.cppreference.com/w/cpp/container">CPP Reference Containers library</a></li>
<li><a href="https://www.matools.com/api/java8">Java8 API 中文文档</a></li>
</ul>
数据结构: 线性表
前言(啰嗦两句)
作为一名普通野鸡大学的毕业的科班生,虽然在学校学过《数据结构》这样一门课程,但彼时的我所有的编码经验也就只是写过一些诸如杨辉三角,水仙花数之类的例题,因...
鱼翅
https://geeknote.net/yuchiXiong
https://geeknote.net/yuchiXiong/posts/297
2021-10-03T09:05:36Z
2022-10-28T14:40:15Z
JavaScript元编程——基于Proxy实现active_record动态查找
<h2>
<a id="1.+%E5%85%83%E7%BC%96%E7%A8%8B" href="#1.+%E5%85%83%E7%BC%96%E7%A8%8B" class="anchor"></a>1. 元编程</h2>
<p>在网络上无意间看到《JavaScript权威指南》第七版的目录,除了<code>NodeJS</code>外,很意外的看到有一个章节叫元编程。</p>
<p>第一次听说元编程这一概念还是来自于<code>Ruby</code>,《Ruby元编程》这本书,很遗憾的是这本书我只看了一点点……对于元编程,我所掌握的也就只有<code>Open Class</code>和<code>method_missing</code>而已了,不过本文也就只是使用了这么点简单的内容。</p>
<h3>
<a id="1.1+Open+Class" href="#1.1+Open+Class" class="anchor"></a>1.1 Open Class</h3>
<p>在很多面向对象的语言里是无法修改一个类的,但在<code>Ruby</code>中如下代码是合法的:</p>
<pre class="highlight"><code class="language-ruby"><span class="k">class</span> <span class="nc">Book</span>
<span class="nb">attr_accessor</span> <span class="ss">:name</span>
<span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="nb">name</span><span class="p">)</span>
<span class="vi">@name</span> <span class="o">=</span> <span class="nb">name</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">to_s</span>
<span class="s2">"书名:</span><span class="si">#{</span><span class="vi">@name</span><span class="si">}</span><span class="s2">"</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">book</span> <span class="o">=</span> <span class="no">Book</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s2">"《Ruby 元编程》"</span><span class="p">)</span>
<span class="nb">puts</span> <span class="n">book</span><span class="p">.</span><span class="nf">to_s</span>
<span class="c1"># Open Class</span>
<span class="k">class</span> <span class="nc">Book</span>
<span class="k">def</span> <span class="nf">pure_name</span>
<span class="vi">@name</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="s2">"《"</span> <span class="o">&&</span> <span class="vi">@name</span><span class="p">[</span><span class="o">-</span><span class="mi">1</span><span class="p">]</span> <span class="o">==</span> <span class="s2">"》"</span> <span class="p">?</span> <span class="vi">@name</span><span class="p">[</span><span class="mi">1</span><span class="o">..-</span><span class="mi">2</span><span class="p">]</span> <span class="p">:</span> <span class="vi">@name</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="nb">puts</span> <span class="n">book</span><span class="p">.</span><span class="nf">pure_name</span>
</code></pre>
<p>虽然重复定义了<code>Book</code>类,但后定义的<code>pure_name</code>方法被“加入”到了原有的类定义中。通过这种方式我们可以在任意位置对我们的代码进行扩展,这一技巧被称为<code>Monkey Patch</code>,以下是一个更实用一点的例子,我们打开了<code>Array</code>类。</p>
<pre class="highlight"><code class="language-ruby"><span class="c1"># 通过 Open Class 为数组添加一个用于求平均值的方法</span>
<span class="n">arr</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">6</span><span class="p">,</span> <span class="mi">7</span><span class="p">,</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">10</span><span class="p">]</span>
<span class="k">class</span> <span class="nc">Array</span>
<span class="k">def</span> <span class="nf">average</span>
<span class="n">sum</span> <span class="o">/</span> <span class="n">size</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="nb">puts</span> <span class="n">arr</span><span class="p">.</span><span class="nf">average</span> <span class="c1"># 输出 5</span>
</code></pre>
<p>除了扩展方法外,我们还可以通过这种手段使程序更具有表现力:</p>
<pre class="highlight"><code class="language-ruby"><span class="n">arr</span> <span class="o">=</span> <span class="p">[</span><span class="o">...</span><span class="p">]</span>
<span class="n">arr</span><span class="p">.</span><span class="nf">first</span> <span class="c1"># 等同于 arr[0]</span>
<span class="n">arr</span><span class="p">.</span><span class="nf">second</span> <span class="c1"># 等同于 arr[1]</span>
<span class="n">arr</span><span class="p">.</span><span class="nf">last</span> <span class="c1"># 等同于 arr[-1]</span>
</code></pre>
<p>不过这种手段也容易带来问题,例如打开类以后覆盖了一个已有的方法,那么极容易导致其它位置的方法调用出现问题。</p>
<h3>
<a id="1.2.+method_missing" href="#1.2.+method_missing" class="anchor"></a>1.2. method_missing</h3>
<p><code>Ruby</code>对象在调用方法时,如果不能找到目标方法,则会尝试执行<code>method_missing</code>方法,我们可以将<code>method_missing</code>方法看作一层代理:</p>
<pre class="highlight"><code class="language-ruby"><span class="k">class</span> <span class="nc">Array</span>
<span class="k">def</span> <span class="nf">method_missing</span><span class="p">(</span><span class="nb">method</span><span class="p">)</span>
<span class="k">case</span> <span class="nb">method</span>
<span class="k">when</span> <span class="ss">:average</span>
<span class="n">sum</span> <span class="o">/</span> <span class="n">size</span>
<span class="k">when</span> <span class="ss">:to_binary</span>
<span class="n">map</span><span class="p">{</span> <span class="o">|</span><span class="n">num</span><span class="o">|</span> <span class="n">num</span><span class="p">.</span><span class="nf">to_s</span><span class="p">(</span><span class="mi">2</span><span class="p">)</span> <span class="p">}</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="k">end</span>
<span class="n">arr</span> <span class="o">=</span> <span class="p">(</span><span class="mi">1</span><span class="o">..</span><span class="mi">10</span><span class="p">).</span><span class="nf">to_a</span>
<span class="c1"># 如下两个方法都没有直接在 Array 类中定义,而是在查询方法失败以后通过 method_missing 方法进行了处理</span>
<span class="nb">puts</span> <span class="n">arr</span><span class="p">.</span><span class="nf">average</span> <span class="c1"># 返回 5</span>
<span class="nb">puts</span> <span class="n">arr</span><span class="p">.</span><span class="nf">to_binary</span> <span class="c1"># 返回数组元素转为二进制之后组成的数组</span>
</code></pre>
<h2>
<a id="2.+%E5%9F%BA%E4%BA%8Eprototype%E5%92%8Cproxy%E5%B0%9D%E8%AF%95JavaScript%E5%85%83%E7%BC%96%E7%A8%8B" href="#2.+%E5%9F%BA%E4%BA%8Eprototype%E5%92%8Cproxy%E5%B0%9D%E8%AF%95JavaScript%E5%85%83%E7%BC%96%E7%A8%8B" class="anchor"></a>2. 基于prototype和proxy尝试JavaScript元编程</h2>
<p>我们知道<code>JavaScript</code>的类实际上是借由<code>prototype</code>实现的语法糖,利用<code>prototype</code>一样可以实现类似于上述的<code>Open Class</code>。</p>
<pre class="highlight"><code class="language-javascript"><span class="kd">const</span> <span class="nx">indexAlias</span> <span class="o">=</span> <span class="p">{</span>
<span class="na">first</span><span class="p">:</span> <span class="mi">0</span><span class="p">,</span>
<span class="na">second</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
<span class="na">third</span><span class="p">:</span> <span class="mi">2</span><span class="p">,</span>
<span class="na">fourth</span><span class="p">:</span> <span class="mi">3</span><span class="p">,</span>
<span class="na">fifth</span><span class="p">:</span> <span class="mi">4</span><span class="p">,</span>
<span class="na">sixth</span><span class="p">:</span> <span class="mi">5</span><span class="p">,</span>
<span class="na">seventh</span><span class="p">:</span> <span class="mi">6</span><span class="p">,</span>
<span class="na">eighth</span><span class="p">:</span> <span class="mi">7</span><span class="p">,</span>
<span class="na">ninth</span><span class="p">:</span> <span class="mi">8</span><span class="p">,</span>
<span class="na">tenth</span><span class="p">:</span> <span class="mi">9</span><span class="p">,</span>
<span class="na">twentieth</span><span class="p">:</span> <span class="mi">19</span><span class="p">,</span>
<span class="na">thirtieth</span><span class="p">:</span> <span class="mi">29</span><span class="p">,</span>
<span class="na">last</span><span class="p">:</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span>
<span class="p">}</span>
<span class="nb">Object</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span><span class="nx">indexAlias</span><span class="p">).</span><span class="nx">map</span><span class="p">(</span><span class="nx">alias</span> <span class="o">=></span> <span class="p">{</span>
<span class="nb">Array</span><span class="p">.</span><span class="nx">prototype</span><span class="p">[</span><span class="nx">alias</span><span class="p">]</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">index</span> <span class="o">=</span> <span class="nx">indexAlias</span><span class="p">[</span><span class="nx">alias</span><span class="p">]</span> <span class="o">===</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">length</span> <span class="o">-</span> <span class="mi">1</span> <span class="p">:</span> <span class="nx">indexAlias</span><span class="p">[</span><span class="nx">alias</span><span class="p">];</span>
<span class="k">return</span> <span class="k">this</span><span class="p">[</span><span class="nx">index</span><span class="p">];</span>
<span class="p">}</span>
<span class="p">});</span>
<span class="kd">const</span> <span class="nx">testArr</span> <span class="o">=</span> <span class="nb">Object</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span><span class="nb">Array</span><span class="p">.</span><span class="k">from</span><span class="p">(</span><span class="k">new</span> <span class="nb">Array</span><span class="p">(</span><span class="mi">100</span><span class="p">)));</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">testArr</span><span class="p">.</span><span class="nx">first</span><span class="p">());</span> <span class="c1">// 等同于 testArr[0]</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">testArr</span><span class="p">.</span><span class="nx">second</span><span class="p">());</span> <span class="c1">// 等同于 testArr[1]</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">testArr</span><span class="p">.</span><span class="nx">third</span><span class="p">());</span> <span class="c1">// 等同于 testArr[2]</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">testArr</span><span class="p">.</span><span class="nx">last</span><span class="p">());</span> <span class="c1">// 等同于 testArr[3]</span>
</code></pre>
<p>上述例子动态的为数组类扩展了多个<strong>类似的</strong>方法。</p>
<p>不过这里有一个小细节,其实我并不一定需要所有的方法,有时候可能到头来只调用了<code>first</code>和<code>last</code>方法,但这些方法却实实在在的都挂到了<code>prototype</code>上。</p>
<p>基于代理来实现的方法动态定义其实可以解决这个问题。</p>
<pre class="highlight"><code class="language-javascript"><span class="kd">const</span> <span class="nx">arr</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="mi">5</span><span class="p">,</span> <span class="mi">6</span><span class="p">,</span> <span class="mi">7</span><span class="p">,</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="mi">10</span><span class="p">];</span>
<span class="kd">const</span> <span class="nx">customArr</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Proxy</span><span class="p">(</span><span class="nx">arr</span><span class="p">,</span> <span class="p">{</span>
<span class="na">get</span><span class="p">:</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">target</span><span class="p">,</span> <span class="nx">prop</span><span class="p">,</span> <span class="nx">receiver</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nb">Reflect</span><span class="p">.</span><span class="nx">has</span><span class="p">(</span><span class="nx">target</span><span class="p">,</span> <span class="nx">prop</span><span class="p">))</span> <span class="k">return</span> <span class="nb">Reflect</span><span class="p">.</span><span class="kd">get</span><span class="p">(...</span><span class="nx">arguments</span><span class="p">);</span>
<span class="k">switch</span> <span class="p">(</span><span class="nx">prop</span><span class="p">)</span> <span class="p">{</span>
<span class="k">case</span> <span class="dl">'</span><span class="s1">average</span><span class="dl">'</span><span class="p">:</span>
<span class="k">return</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">reduce</span><span class="p">((</span><span class="nx">sum</span><span class="p">,</span> <span class="nx">item</span><span class="p">)</span> <span class="o">=></span> <span class="nx">sum</span> <span class="o">+</span> <span class="nx">item</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="o">/</span> <span class="k">this</span><span class="p">.</span><span class="nx">length</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">});</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">customArr</span><span class="p">.</span><span class="nx">average</span><span class="p">());</span>
</code></pre>
<p>需要注意几点细节:</p>
<ol>
<li>此处通过代理扩展的是<strong>实例方法而非类方法</strong>。</li>
<li>考虑到数组和对象都可以用字面量的方式完成初始化,打开<code>Array/Object</code>类的时候,或许<code>prototype</code>会更管用一些,因为<strong>prototype修改的是原有的类而代理是创建新的类</strong>。</li>
</ol>
<p>当然,完全可以将<code>average</code>方法直接放到<code>prototype</code>上,但如果我们要定义的是多个存在联系的方法,使用这种代理会灵活的多,关于这一点,接下来要尝试实现的<code>active_record</code>动态查找可能是一个不错的案例。</p>
<h2>
<a id="3.+%E5%9F%BA%E4%BA%8EProxy%E5%AE%9E%E7%8E%B0active_record%E5%8A%A8%E6%80%81%E6%9F%A5%E6%89%BE" href="#3.+%E5%9F%BA%E4%BA%8EProxy%E5%AE%9E%E7%8E%B0active_record%E5%8A%A8%E6%80%81%E6%9F%A5%E6%89%BE" class="anchor"></a>3. 基于Proxy实现active_record动态查找</h2>
<p><code>active_record</code>是<code>Ruby On Rails</code>中的<code>ORM</code>库,它有一个非常有用的魔法:假设存在一张数据表<code>users</code>,它有三个字段:</p>
<ul>
<li><code>username</code></li>
<li><code>nickname</code></li>
<li><code>email</code></li>
</ul>
<p>根据以往我们对<code>ORM</code>的理解,此时需要创建一个实体类,且这个实体类一眼两个需要声明上述的三个属性。不过,在<code>ActiveRecord</code>里,创建实体类你只需要继承<code>ActiveRecord</code>即可,它会自动的添加类属性,同时还有包括如下三个方法在内的大量数据读写方法:</p>
<ul>
<li><code>find_by_username</code></li>
<li><code>find_by_nickname</code></li>
<li><code>find_by_email</code></li>
</ul>
<p>其原理是根据数据表的字段名列表动态定义了各字段的查询方法。</p>
<p><code>JavaScript</code>基于代理也可以实现类似的效果,下面的示例代码没有真正的链接数据库,而是使用了一个对象结构来进行模拟,同时为了让示例看起来像那么回事儿,还实现了<code>active_record</code>持久化数据的两个方法<code>save/create</code>。</p>
<p>先来看看最终的效果:</p>
<pre class="highlight"><code class="language-javascript"><span class="kd">const</span> <span class="nx">ActiveRecord</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">./ActiveRecord</span><span class="dl">'</span><span class="p">);</span>
<span class="c1">// 1. 初始化一个数据源(模拟数据库)</span>
<span class="kd">const</span> <span class="nx">DB</span> <span class="o">=</span> <span class="p">{};</span>
<span class="nx">ActiveRecord</span><span class="p">.</span><span class="nx">init</span><span class="p">({</span>
<span class="na">db</span><span class="p">:</span> <span class="nx">DB</span><span class="p">,</span>
<span class="p">});</span>
<span class="kd">class</span> <span class="nx">User</span> <span class="kd">extends</span> <span class="nx">ActiveRecord</span> <span class="p">{</span> <span class="c1">// 2. 定义一个实体类</span>
<span class="p">}</span>
<span class="c1">// 3.1 创建一条数据的方式1: 实例化一个对象然后调用 save 方法</span>
<span class="kd">const</span> <span class="nx">yuchi</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">User</span><span class="p">({</span>
<span class="na">userName</span><span class="p">:</span> <span class="dl">'</span><span class="s1">yuchi</span><span class="dl">'</span><span class="p">,</span>
<span class="na">password</span><span class="p">:</span> <span class="dl">'</span><span class="s1">123456</span><span class="dl">'</span><span class="p">,</span>
<span class="na">nickName</span><span class="p">:</span> <span class="dl">'</span><span class="s1">鱼翅</span><span class="dl">'</span>
<span class="p">});</span>
<span class="nx">yuchi</span><span class="p">.</span><span class="nx">save</span><span class="p">();</span>
<span class="c1">// 3.2 创建一条数据的方式2: 直接使用 create 类方法</span>
<span class="nx">User</span><span class="p">.</span><span class="nx">create</span><span class="p">({</span>
<span class="na">userName</span><span class="p">:</span> <span class="dl">'</span><span class="s1">xiaoming</span><span class="dl">'</span><span class="p">,</span>
<span class="na">password</span><span class="p">:</span> <span class="dl">'</span><span class="s1">11111</span><span class="dl">'</span><span class="p">,</span>
<span class="na">nickName</span><span class="p">:</span> <span class="dl">'</span><span class="s1">小明</span><span class="dl">'</span>
<span class="p">});</span>
<span class="c1">// 4. 查看虚拟的数据库数据 </span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">DB</span><span class="p">);</span>
<span class="c1">// 5. 通过属性生成的动态查询方法进行查询</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">User</span><span class="p">.</span><span class="nx">findByUserName</span><span class="p">(</span><span class="dl">'</span><span class="s1">yuchi</span><span class="dl">'</span><span class="p">));</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">User</span><span class="p">.</span><span class="nx">findByNickName</span><span class="p">(</span><span class="dl">'</span><span class="s1">小明</span><span class="dl">'</span><span class="p">));</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">User</span><span class="p">.</span><span class="nx">findByPassword</span><span class="p">(</span><span class="dl">'</span><span class="s1">11111</span><span class="dl">'</span><span class="p">));</span>
<span class="c1">// 再创建一个</span>
<span class="kd">class</span> <span class="nx">Book</span> <span class="kd">extends</span> <span class="nx">ActiveRecord</span> <span class="p">{</span> <span class="p">}</span>
<span class="nx">Book</span><span class="p">.</span><span class="nx">create</span><span class="p">({</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">《我们的土地》</span><span class="dl">'</span><span class="p">,</span> <span class="na">author</span><span class="p">:</span> <span class="dl">'</span><span class="s1">[墨西哥] 卡洛斯·富恩特斯</span><span class="dl">'</span><span class="p">,</span> <span class="na">pageTotal</span><span class="p">:</span> <span class="dl">'</span><span class="s1">1036</span><span class="dl">'</span><span class="p">,</span> <span class="na">price</span><span class="p">:</span> <span class="dl">'</span><span class="s1">168</span><span class="dl">'</span><span class="p">,</span> <span class="na">ISBN</span><span class="p">:</span> <span class="dl">'</span><span class="s1">9787521211542</span><span class="dl">'</span> <span class="p">});</span>
<span class="nx">Book</span><span class="p">.</span><span class="nx">create</span><span class="p">({</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">《戛纳往事》</span><span class="dl">'</span><span class="p">,</span> <span class="na">author</span><span class="p">:</span> <span class="dl">'</span><span class="s1">[法]吉尔·雅各布</span><span class="dl">'</span><span class="p">,</span> <span class="na">pageTotal</span><span class="p">:</span> <span class="dl">'</span><span class="s1">712</span><span class="dl">'</span><span class="p">,</span> <span class="na">price</span><span class="p">:</span> <span class="dl">'</span><span class="s1">148</span><span class="dl">'</span><span class="p">,</span> <span class="na">ISBN</span><span class="p">:</span> <span class="dl">'</span><span class="s1">9787308211208</span><span class="dl">'</span> <span class="p">});</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">查询结果:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">Book</span><span class="p">.</span><span class="nx">findByName</span><span class="p">(</span><span class="dl">'</span><span class="s1">《戛纳往事》</span><span class="dl">'</span><span class="p">));</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">查询结果:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">Book</span><span class="p">.</span><span class="nx">findByAuthor</span><span class="p">(</span><span class="dl">'</span><span class="s1">[法]吉尔·雅各布</span><span class="dl">'</span><span class="p">));</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">查询结果:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">Book</span><span class="p">.</span><span class="nx">findByPageTotal</span><span class="p">(</span><span class="dl">'</span><span class="s1">712</span><span class="dl">'</span><span class="p">));</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">查询结果:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">Book</span><span class="p">.</span><span class="nx">findByPrice</span><span class="p">(</span><span class="dl">'</span><span class="s1">168</span><span class="dl">'</span><span class="p">));</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">查询结果:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">Book</span><span class="p">.</span><span class="nx">findByISBN</span><span class="p">(</span><span class="dl">'</span><span class="s1">9787308211208</span><span class="dl">'</span><span class="p">));</span>
</code></pre>
<p>以下是<code>ActiveRecord</code>类的实现,它有如下细节:</p>
<ol>
<li>
<code>ActiveRecord</code>是一个经过代理的类。</li>
<li>创建一个<code>ActiveRecord</code>类的子类,然后初始化,实际调用的是父类的构造函数,同时也会触发代理(注意<code>Proxy</code>里的代码,为了保证返回的对象依然是子类对象,手动修改了构造函数指向)。</li>
<li>
<code>ActiveRecord</code>类经过代理后,增加了动态查询类方法。</li>
<li>
<code>ActiveRecord</code>类的子类实例化后得到的也是一个经过代理的对象,代理中实现了一些实例方法。</li>
</ol>
<pre class="highlight"><code class="language-javascript"><span class="c1">// 定义基础的 ActiveRecord 抽象类,并支持动态的初始化实例属性</span>
<span class="kd">class</span> <span class="nx">BaseActiveRecord</span> <span class="p">{</span>
<span class="kd">constructor</span><span class="p">(</span><span class="nx">record</span><span class="p">)</span> <span class="p">{</span>
<span class="nb">Object</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span><span class="nx">record</span><span class="p">).</span><span class="nx">map</span><span class="p">(</span><span class="nx">item</span> <span class="o">=></span> <span class="k">this</span><span class="p">[</span><span class="nx">item</span><span class="p">]</span> <span class="o">=</span> <span class="nx">record</span><span class="p">[</span><span class="nx">item</span><span class="p">])</span>
<span class="p">}</span>
<span class="c1">// 一个用于验证代理后的类依然可以被继承的基础方法,也顺便用于数据序列化以便于存到 DB 中</span>
<span class="nx">toJSON</span><span class="p">()</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">res</span> <span class="o">=</span> <span class="p">{};</span>
<span class="nb">Object</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span><span class="k">this</span><span class="p">).</span><span class="nx">map</span><span class="p">(</span><span class="nx">item</span> <span class="o">=></span> <span class="nx">res</span><span class="p">[</span><span class="nx">item</span><span class="p">]</span> <span class="o">=</span> <span class="k">this</span><span class="p">[</span><span class="nx">item</span><span class="p">]);</span>
<span class="k">return</span> <span class="nx">res</span><span class="p">;</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="c1">// 代理基础 ActiveRecord 类</span>
<span class="kd">const</span> <span class="nx">ActiveRecord</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Proxy</span><span class="p">(</span><span class="nx">BaseActiveRecord</span><span class="p">,</span> <span class="p">{</span>
<span class="c1">// 代理构造方法,主要意图在希望实例化以后返回的 AR 对象一样是被代理过的</span>
<span class="na">construct</span><span class="p">:</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">target</span><span class="p">,</span> <span class="nx">args</span><span class="p">,</span> <span class="nx">newTarget</span><span class="p">)</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">nativeObj</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">target</span><span class="p">(</span><span class="nx">args</span><span class="p">[</span><span class="mi">0</span><span class="p">]);</span>
<span class="nx">nativeObj</span><span class="p">.</span><span class="nx">__proto__</span> <span class="o">=</span> <span class="nx">newTarget</span><span class="p">.</span><span class="nx">prototype</span><span class="p">;</span>
<span class="k">return</span> <span class="k">new</span> <span class="nb">Proxy</span><span class="p">(</span><span class="nx">nativeObj</span><span class="p">,</span> <span class="p">{</span>
<span class="na">get</span><span class="p">:</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">obj</span><span class="p">,</span> <span class="nx">prop</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nb">Reflect</span><span class="p">.</span><span class="nx">has</span><span class="p">(</span><span class="nx">obj</span><span class="p">,</span> <span class="nx">prop</span><span class="p">))</span> <span class="k">return</span> <span class="nb">Reflect</span><span class="p">.</span><span class="kd">get</span><span class="p">(...</span><span class="nx">arguments</span><span class="p">);</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">prop</span> <span class="o">!==</span> <span class="dl">'</span><span class="s1">save</span><span class="dl">'</span><span class="p">)</span> <span class="k">throw</span> <span class="k">new</span> <span class="nb">Error</span><span class="p">(</span><span class="s2">`</span><span class="p">${</span><span class="nx">prop</span><span class="p">}</span><span class="s2"> is not a function!`</span><span class="p">)</span>
<span class="c1">// 定义了一个 save 方法,自动根据实体类的名字将数据存到对应的表里</span>
<span class="k">return</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">tableName</span> <span class="o">=</span> <span class="nx">obj</span><span class="p">.</span><span class="nx">__proto__</span><span class="p">.</span><span class="kd">constructor</span><span class="p">.</span><span class="nx">name</span><span class="p">.</span><span class="nx">toLowerCase</span><span class="p">()</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">s</span><span class="dl">'</span><span class="p">;</span>
<span class="nx">ActiveRecord</span><span class="p">.</span><span class="nx">db</span><span class="p">[</span><span class="nx">tableName</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="nx">ActiveRecord</span><span class="p">.</span><span class="nx">db</span><span class="p">[</span><span class="nx">tableName</span><span class="p">]</span> <span class="o">||</span> <span class="p">[]);</span>
<span class="nx">ActiveRecord</span><span class="p">.</span><span class="nx">db</span><span class="p">[</span><span class="nx">tableName</span><span class="p">].</span><span class="nx">push</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">toJSON</span><span class="p">());</span>
<span class="p">}</span>
<span class="p">},</span>
<span class="p">});</span>
<span class="p">},</span>
<span class="c1">// 代理类属性和方法</span>
<span class="na">get</span><span class="p">:</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">obj</span><span class="p">,</span> <span class="nx">prop</span><span class="p">,</span> <span class="nx">receiver</span><span class="p">)</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nb">Reflect</span><span class="p">.</span><span class="nx">has</span><span class="p">(</span><span class="nx">obj</span><span class="p">,</span> <span class="nx">prop</span><span class="p">))</span> <span class="k">return</span> <span class="nb">Reflect</span><span class="p">.</span><span class="kd">get</span><span class="p">(...</span><span class="nx">arguments</span><span class="p">);</span>
<span class="kd">const</span> <span class="nx">tableName</span> <span class="o">=</span> <span class="nx">receiver</span><span class="p">.</span><span class="nx">prototype</span><span class="p">.</span><span class="kd">constructor</span><span class="p">.</span><span class="nx">name</span><span class="p">.</span><span class="nx">toLowerCase</span><span class="p">()</span> <span class="o">+</span> <span class="dl">'</span><span class="s1">s</span><span class="dl">'</span><span class="p">;</span>
<span class="k">switch</span> <span class="p">(</span><span class="nx">prop</span><span class="p">)</span> <span class="p">{</span>
<span class="c1">// 定义一个 create 方法,基本与 save 方法相同</span>
<span class="k">case</span> <span class="dl">'</span><span class="s1">create</span><span class="dl">'</span><span class="p">:</span>
<span class="k">return</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
<span class="nx">ActiveRecord</span><span class="p">.</span><span class="nx">db</span><span class="p">[</span><span class="nx">tableName</span><span class="p">]</span> <span class="o">=</span> <span class="p">(</span><span class="nx">ActiveRecord</span><span class="p">.</span><span class="nx">db</span><span class="p">[</span><span class="nx">tableName</span><span class="p">]</span> <span class="o">||</span> <span class="p">[]);</span>
<span class="nx">ActiveRecord</span><span class="p">.</span><span class="nx">db</span><span class="p">[</span><span class="nx">tableName</span><span class="p">].</span><span class="nx">push</span><span class="p">(</span><span class="nx">arguments</span><span class="p">[</span><span class="mi">0</span><span class="p">]);</span>
<span class="p">}</span>
<span class="nl">default</span><span class="p">:</span>
<span class="c1">// 根据属性动态定义 findByAttr 方法</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">prop</span><span class="p">.</span><span class="nx">startsWith</span><span class="p">(</span><span class="dl">'</span><span class="s1">findBy</span><span class="dl">'</span><span class="p">))</span> <span class="p">{</span>
<span class="kd">const</span> <span class="nx">attr</span> <span class="o">=</span> <span class="nx">prop</span><span class="p">.</span><span class="nx">slice</span><span class="p">(</span><span class="nx">prop</span><span class="p">.</span><span class="nx">indexOf</span><span class="p">(</span><span class="dl">'</span><span class="s1">findBy</span><span class="dl">'</span><span class="p">)</span> <span class="o">+</span> <span class="mi">6</span><span class="p">,</span> <span class="nx">prop</span><span class="p">.</span><span class="nx">length</span><span class="p">).</span><span class="nx">toLowerCase</span><span class="p">();</span>
<span class="k">return</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span>
<span class="k">return</span> <span class="nx">ActiveRecord</span><span class="p">.</span><span class="nx">db</span><span class="p">[</span><span class="nx">tableName</span><span class="p">].</span><span class="nx">filter</span><span class="p">(</span><span class="nx">item</span> <span class="o">=></span> <span class="nx">item</span><span class="p">[</span><span class="nb">Object</span><span class="p">.</span><span class="nx">keys</span><span class="p">(</span><span class="nx">item</span><span class="p">).</span><span class="nx">filter</span><span class="p">(</span><span class="nx">item</span> <span class="o">=></span> <span class="nx">item</span><span class="p">.</span><span class="nx">toLowerCase</span><span class="p">()</span> <span class="o">===</span> <span class="nx">attr</span><span class="p">)[</span><span class="mi">0</span><span class="p">]]</span> <span class="o">===</span> <span class="nx">arguments</span><span class="p">[</span><span class="mi">0</span><span class="p">]);</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">});</span>
<span class="c1">// 用来初始化 DB 数据源的配置</span>
<span class="nx">ActiveRecord</span><span class="p">.</span><span class="nx">init</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">option</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">ActiveRecord</span><span class="p">.</span><span class="nx">db</span> <span class="o">=</span> <span class="nx">option</span><span class="p">.</span><span class="nx">db</span><span class="p">;</span>
<span class="p">}</span>
<span class="nx">module</span><span class="p">.</span><span class="nx">exports</span> <span class="o">=</span> <span class="nx">ActiveRecord</span><span class="p">;</span>
</code></pre>
<p>代码:
<a href="https://github.com/yuchiXiong/activeRecordByProxy">yuchiXiong/activeRecordByProxy</a></p>
1. 元编程
在网络上无意间看到《JavaScript权威指南》第七版的目录,除了NodeJS外,很意外的看到有一个章节叫元编程。
第一次听说元编程这一概念还是来自于Ruby,《Ruby元编程...
鱼翅
https://geeknote.net/yuchiXiong
https://geeknote.net/yuchiXiong/posts/65
2021-07-08T13:09:38Z
2022-10-28T14:36:29Z
从 Rails 说起的前端发展历程
<h2>
<a id="%E5%89%8D%E8%A8%80" href="#%E5%89%8D%E8%A8%80" class="anchor"></a>前言</h2>
<p>其实想写这篇文很久了……</p>
<p>从毕业实习接触 <code>Rails</code> 到现在两年多了。一开始是基于 <code>Rails</code> 框架来给 <code>Android</code> 客户端提供 <code>API</code> 接口,后来也尝试在 <code>Rails</code> 上倒腾了一些前端的东西,说从 <code>Rails</code> 中受益匪浅是一点都不夸张的。</p>
<p><code>Ruby On Rails</code> 作为经典的 <code>MVC</code> 框架,在前端领域有着自己独到的见解,在学习的过程中也不断的让我反思自己曾经信奉的那些东西是否过于片面。</p>
<p>本文将简单的分享 <code>Rails</code> 框架的前端技术发展历程,亦是对自己摸索的总结,在这之前有一点很重要那就是:<strong>本文没有战争。</strong></p>
<hr>
<h2>
<a id="%E5%BD%93%E6%88%91%E4%BB%AC%E4%BB%8A%E5%A4%A9%E8%B0%88%E8%AE%BA%E8%B5%B7%E5%89%8D%E7%AB%AF%EF%BC%8C%E6%88%91%E4%BB%AC%E6%9B%B4%E5%A4%9A%E7%9A%84%E6%98%AF%E5%9C%A8%E8%B0%88%E8%AE%BA%E4%BB%80%E4%B9%88%EF%BC%9F" href="#%E5%BD%93%E6%88%91%E4%BB%AC%E4%BB%8A%E5%A4%A9%E8%B0%88%E8%AE%BA%E8%B5%B7%E5%89%8D%E7%AB%AF%EF%BC%8C%E6%88%91%E4%BB%AC%E6%9B%B4%E5%A4%9A%E7%9A%84%E6%98%AF%E5%9C%A8%E8%B0%88%E8%AE%BA%E4%BB%80%E4%B9%88%EF%BC%9F" class="anchor"></a>当我们今天谈论起前端,我们更多的是在谈论什么?</h2>
<p>试想一个问题:如果今天有两位前端工程师碰面,他们大概会聊什么?</p>
<p>我的脑子里很快浮现出一些关键字:<code>vue</code>, <code>react</code>, <code>angular</code>, <code>webpack</code>, <code>vite</code>, <code>redux</code>, 跨平台,小程序,组件化,工程化,微前端……</p>
<p>相信对于大部分前端工程师来说,我们工作的每一天都在使用或者探讨这些东西。</p>
<p>不难发现,这些东西的背后有着一个很基本的共同逻辑:如何更好的基于 <code>JavaScript</code> 来完成页面的构建。</p>
<p>于是我们推导出一个结论——当今天我们聊起前端,我们更多的是在谈论以 <code>JS Render</code> 为基础的技术体系。</p>
<p>于是问题来了……我们真的只有 <code>JS Render</code> 吗?</p>
<p>让我们简单的过一下前端技术发展经历的几个比较典型的阶段:</p>
<ol>
<li>
<code>MVC</code> 时代,通过服务端语言开发的模板引擎,动态,快速的生成 <code>HTML</code> 页面。关键字:<code>JSP</code>, <code>ASP</code> 等。
<ul>
<li>优点:开发效率高,复杂程度低。</li>
<li>缺点:结构混乱,交互体验差,分工不明确。</li>
</ul>
</li>
<li>
<code>AJAX</code> 时代,通过 <code>AJAX</code> 显著改善了交互的问题,初显分离之态。关键字:<code>AJXA</code>, <code>JQuery</code> 等。
<ul>
<li>优点:交互效果显著改善。</li>
<li>缺点:依赖 <code>JavaScript</code> 带来了大量的兼容问题和性能问题,以及代码结构依然混乱,且大量依赖于 <code>AJAX</code> 实现的页面出现了 <code>SEO</code> 问题。</li>
</ul>
</li>
<li>
<code>SPA</code> 时代,通过 <code>JavaScript</code> 实现的渲染引擎,直接由 <code>JS</code> 完成页面的渲染,前后端完全分离。关键字:<code>vue</code>, <code>react</code>, <code>ajax</code>, 前后端分离等。
<ul>
<li>优点:富客户端,交互效果起飞,以工程化的方式解决了代码混乱的问题,人员分工明确。</li>
<li>缺点:依然没有解决 <code>JavaScript</code> 带来的兼容问题与 <code>SEO</code> 问题,同时渲染引擎加入了渲染生命周期,以及各种数据抽象层提高了编码的心智负担。</li>
</ul>
</li>
</ol>
<p>以上的列举不算特别完整和严谨,但大体上能够说明前端技术发展历程的主要思路中,有两个比较重要的点:</p>
<ol>
<li>如何更高效的组织代码,解决代码结构混乱的问题。</li>
<li>如何解决用户体验差的问题。</li>
</ol>
<p>而此时,我们就不得不回过头来简单的探讨一下过去的 <code>MVC</code> 架构下的前端到底是怎样的了。</p>
<hr>
<h2>
<a id="MVC%E4%B8%8B%E7%9A%84%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91%E6%98%AF%E4%BB%80%E4%B9%88%E6%A0%B7%E5%AD%90%EF%BC%9F" href="#MVC%E4%B8%8B%E7%9A%84%E5%89%8D%E7%AB%AF%E5%BC%80%E5%8F%91%E6%98%AF%E4%BB%80%E4%B9%88%E6%A0%B7%E5%AD%90%EF%BC%9F" class="anchor"></a>MVC下的前端开发是什么样子?</h2>
<p>相信说到 <code>MVC</code> 架构下的前端开发,不少经验丰富的前端工程师会有一些久远的回忆,一些刻板印象涌上心头:</p>
<ol>
<li>开发时 <code>HTML</code> 与服务端语言混编。</li>
<li>稍微复杂的需求需要编写大量 <code>DOM</code> 操作来实现。</li>
<li>交互体验差,每一次换页都需要重新加载服务端资源。</li>
<li>……</li>
</ol>
<p>以 <code>Rails</code> 为例,<code>MVC</code> 的 <code>View</code> 层通常使用模板引擎技术来实现快速生成 <code>HTML</code>, 其工作原理很简单,既通过服务端语言来组合 <code>HTML</code> 片段,例如下面的代码是一种名为 <code>erb</code> 的模板引擎,它通过 <code>Ruby</code> 来生成一段列表标签。</p>
<pre class="highlight"><code class="language-erb"><span class="nt"><ul></span>
<span class="cp"><%-</span> <span class="vi">@books</span><span class="p">.</span><span class="nf">map</span> <span class="k">do</span> <span class="o">|</span><span class="n">book</span><span class="o">|</span> <span class="cp">%></span>
<span class="nt"><li></span><span class="cp"><%=</span> <span class="n">book</span><span class="p">[</span><span class="ss">:name</span><span class="p">]</span> <span class="cp">%></span><span class="nt"></li></span>
<span class="cp"><%-</span> <span class="k">end</span> <span class="cp">%></span>
<span class="nt"></ul></span>
</code></pre>
<p>最终得到的 <code>HTML</code> 被发送给浏览器端,渲染出结果:
<img src="/attachments/X84MFSY9fVH5sEQvjQs6H3VD/QQ%E5%9B%BE%E7%89%8720210708210255.png" alt="图片"></p>
<p>我们同样可以为这样的结构编写 <code>CSS</code>, <code>JavaScript</code>,但就像传统的静态页一样,我们只能在每一个页面单独的引入该页面要使用的 <code>CSS</code> 与 <code>JavaScript</code> 文件,且在缺乏构建工具的情况下,我们将无缘使用 <code>Less</code>, <code>Sass</code> 等预处理技术。</p>
<p>最终当我们完成代码的编写后,它可能看起来就像这样:</p>
<p><img src="/attachments/kw8J1gQ16EAfwZTxENZFAMS5/QQ%E5%9B%BE%E7%89%8720210708210446.png" alt="图为 GitLab 源码中的视图,使用的模板引擎是 HAML"></p>
<p>而其交互体验可能就像这样:
<img src="/attachments/gEwwUZQFLiV2EcEF42PRiHnE/1.gif" alt="图例为某些使用 JSP 开发的系统"></p>
<p>正如前面所说,这样的项目有几个很要命的问题:</p>
<ol>
<li>代码结构的问题:模板引擎的语法并不完全与 <code>HTML</code> 一致,最好的例子就是上面的 <code>HAML</code>。同时由于混杂了大量的服务端逻辑,它要求开发人员需要对后端逻辑有一定的了解。</li>
<li>交互体验的问题:传统的 <code>MVC</code> 在访问链接,提交表单时,往往伴随浏览器的刷新行为,而每一次刷新都会导致浏览器重新拉取资源与页面渲染,交互体验极差。</li>
<li>分工不明确,事实上从模板引擎的角度来看,能够更好完成编码的反而是更熟悉服务端开发的人员,并非以编写页面为擅长的前端开发人员。</li>
<li>缺少必要的构建工具,没有办法使用更高效的开发技术如 <code>Sass</code>,<code>ECMAScript</code> 的部分新特性。</li>
<li>资源管理不合理,需要多次重复拉取相同的资源文件。</li>
</ol>
<p>站在今天的视角我们很容易理解这些问题,也能很容易想到对策。</p>
<p>而站在产品和用户体验的角度,我们首先要解决的是交互部分的问题,让页面无刷新更新最简单的方法就是 <code>AJAX</code> 。</p>
<hr>
<h2>
<a id="AJAX%3A+%E9%80%9A%E8%BF%87%E5%B1%80%E9%83%A8%E5%88%B7%E6%96%B0%E6%9D%A5%E6%8F%90%E5%8D%87%E7%94%A8%E6%88%B7%E4%BD%93%E9%AA%8C" href="#AJAX%3A+%E9%80%9A%E8%BF%87%E5%B1%80%E9%83%A8%E5%88%B7%E6%96%B0%E6%9D%A5%E6%8F%90%E5%8D%87%E7%94%A8%E6%88%B7%E4%BD%93%E9%AA%8C" class="anchor"></a>AJAX: 通过局部刷新来提升用户体验</h2>
<p><code>AJAX</code> 可以帮助我们在不刷新浏览器的情况下完成局部更新,实现更高效可用的交互。<code>AJAX</code> 作为一项成熟的技术发展已经接近 20 年了,这里不再进行赘述了。</p>
<p>简单看一下通过 <code>AJAX</code> 改造前后的页面跳转效果</p>
<p><img src="/attachments/eMHv5GeoroTAw2bXqJm9hZAy/%E4%B8%8D%E9%80%82%E7%94%A8ajax%E6%97%B6%E7%9A%84%E9%A1%B5%E9%9D%A2%E8%B7%B3%E8%BD%AC.gif" alt="不使用ajax时的页面跳转"></p>
<p><img src="/attachments/TzWQrMcx8siQi6tYpbPDsxsj/%E4%BD%BF%E7%94%A8ajax%E6%97%B6%E7%9A%84%E9%A1%B5%E9%9D%A2%E8%B7%B3%E8%BD%AC.gif" alt="使用ajax时的页面跳转"></p>
<p>但 <code>AJAX</code> 的引入并不是没有代价的,完全依靠 <code>AJAX</code> 来动态更新的数据没有办法获得 <code>SEO</code> 的支持。同时使用 <code>AJAX</code> 意味着需要编写些许的 <code>JavaScript</code> 代码来完成 <code>DOM</code> 节点的更新操作。另一个更复杂的问题在于,当我们期望通过 <code>AJAX</code> 来完成整个应用的无刷新换页、提交时,意味着我们有大量的 <code>HTML</code> 会在 <code>JavaScript</code> 中动态的插入,即由此来完成页面的局部刷新。此时,代码的可维护性大大的降低,最终化为一头失控的猛兽。</p>
<p>难道我们一定要在完全由 <code>JS Redner</code> 的环境里才能实现浏览器的无缝刷新吗?其实不然。</p>
<hr>
<h2>
<a id="Turbolinks%EF%BC%9APJAX%E7%9A%84%E9%AD%94%E6%B3%95" href="#Turbolinks%EF%BC%9APJAX%E7%9A%84%E9%AD%94%E6%B3%95" class="anchor"></a>Turbolinks:PJAX的魔法</h2>
<p><code>Rails</code> 在 4.0 版本默认引入了一个扩展—— <code>Turbolinks</code>。</p>
<p><code>Turbolinks</code> 是一个轻量,但略带侵略性的 <code>JavaScript</code> 库。关于它的一切,其实要从更早的一项技术说起——<code>PJAX</code>.</p>
<p><code>PJAX</code> 是一项简单易懂却很有效的技术,在它的 <code>GitHub</code> 页上有这样的介绍:<code>PJAX = AJAX + pushState</code>.</p>
<p>其工作原理很简单:</p>
<ol>
<li>当在页面中点击一个 <code>a</code> 标签,或提交 <code>form</code> 表单时,并非以浏览器的默认行为进行跳转或提交,而是以 <code>xhr</code> 的方式请求目标地址。</li>
<li>服务端在完成请求的处理后,依然基于模板引擎的技术返回渲染完成后的静态 <code>HTML</code> 片段。</li>
<li>将服务端返回的 <code>HTML</code> 片段替换到当前页面的指定位置。</li>
<li>使用 <code>history.pushState</code> 更新浏览器当前的 <code>URL</code> 以正常维护浏览器的地址栏。</li>
</ol>
<p><code>PJAX</code> 将上述的步骤封装为一个方法,开发者可以快速的实现指定节点的局部渲染:</p>
<pre class="highlight"><code class="language-javascript"><span class="nx">$</span><span class="p">(</span><span class="nb">document</span><span class="p">).</span><span class="nx">pjax</span><span class="p">(</span><span class="dl">'</span><span class="s1">a</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">#pjax-container</span><span class="dl">'</span><span class="p">);</span>
</code></pre>
<p>整理一下思路,看似无奇的操作背后隐藏着哪些细节:</p>
<ol>
<li>可以有选择的替换页面的部分标签或全部标签来实现局部刷新。</li>
<li>由于指向的超链接本质上依然返回 <code>HTML</code>, 因而不影响 <code>SEO</code> 效果。</li>
<li>由于返回的部分会被更新到视图中,因而服务端在 <code>PJAX</code> 请求的页面应当仅返回 <code>HTML</code> 片段而非完整的 <code>HTML</code> 页。</li>
</ol>
<p><code>Turbolinks</code> 是 <code>Rails</code> 基于 <code>PJAX</code> 的再实现,它吸取了 <code>PJAX</code> 的思路,同时还做出了改进和扩展。</p>
<p>相比 <code>PJAX</code> 中隐式的条件需要调整服务端渲染的片段,<code>Turbolinks</code> 更加大胆的选择了让服务端照常返回完整的 <code>HTML</code> 页,而后在本地对页面的 <code>head</code> 标签进行合并,同时将 <code>body</code> 标签完整的替换到当前页面以实现页面的无缝刷新。</p>
<p>这样做的第一大好处是,当应用运行在不支持 <code>history.pushState</code> 的环境时,由于服务端照常返回了完整的页面,可以实现优雅降级,浏览器依然能够正常的加载页面。</p>
<p>第二大好处在于,在这种策略之下 <code>Turbolink</code> 不需要开发者手动的指定节点进行局部渲染,一经安装,整个应用都自动实现了无缝刷新。</p>
<p>第三大好处,<code>Turbolinks</code> 合并的 <code>head</code> 标签中,不会拉取重复的资源,减少了资源文件的重复加载。</p>
<p>除此之外 <code>Turbolinks</code> 还扩充了部分功能,其中比较典型的包括:</p>
<ol>
<li>在页面加载完成后,通过 <code>Node.cloneNode()</code> 来缓存页面,使得在网络断开期间也能访问已经缓存的页面。</li>
<li>在 <code>Turbolinks</code> 换页期间维护了一个内部的进度条用来向用户展示 <code>loading</code> 状态,与其它的特性相同,它不需要用户编写任何额外的代码。</li>
</ol>
<p><img src="/attachments/C2GGLtvMMGttqC7gx7Dv2SwX/turbolinks%E6%8D%A2%E9%A1%B5.gif" alt="turbolinks换页"></p>
<p>观察上图,注意几个细节:</p>
<ol>
<li>点击链接时,浏览器并没有刷新。</li>
<li>页面更新前后浏览器顶部自己维护了加载的进度条。</li>
<li>页面切换时,<code>NetWork</code> 里实际发送的是 <code>xhr</code> 类型的请求。</li>
<li>请求返回的是完整的 <code>HTML</code> 页。</li>
</ol>
<p>总结一下,<code>Turbolinks</code> 是一个吸收了 <code>PJAX</code> 思路,以最低成本实现更高交互的轻量级 <code>JavaScript</code> 库。</p>
<p>说完了 <code>Turbolinks</code> 的优点,我们还得说一说 <code>Turbolinks</code> 的缺点,毕竟没有哪一项技术是完美的。</p>
<p><code>Turbolinks</code> 的存在的第一个问题,由 <code>PJAX</code> 继承而来,且由于 <code>Turbolinks</code> 机制上的改进,这个问题被更加放大。当节点被动态添加至页面时,有一个我们不得不注意到一个问题:已经注册的 <code>JavaScript</code> 事件如何处理?</p>
<p>在使用 <code>PJAX</code> 的时候,我们仅添加片段到页面中,并不太会经常出现需要给这些节点添加移出事件的情况。而当整个页面都被替换掉的时候,问题就变得突出了,其问题具体来说体现在:当页面被替换后,由于浏览器没有刷新,当前页面注册的事件、设置的定时器等均依然有效,相对来说下一个页面渲染时的环境并不纯净。此时如果某一个行为触发了上一个页面的事件,则会产生不可预期的影响。另一层考虑则是这些反复留存下来的事件本质上也会造成内存泄露。</p>
<p>解决这一问题的方法从整体思路上说是统一的,既使操作幂等。</p>
<p><code>Turbolinks</code> 提供了换页生命周期的钩子函数,使得可以在换页后做一些事情:</p>
<pre class="highlight"><code class="language-javascript"><span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">"</span><span class="s2">turbolinks:load</span><span class="dl">"</span><span class="p">,</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
<span class="c1">// ...</span>
<span class="p">})</span>
</code></pre>
<p>请注意,正是由于页面的生命周期发生了变化,实际由 <code>Turbolinks</code> 维护的页面只会执行一次 <code>DOMContentLoaded</code> 事件,因而如果期望某一行为始终在页面加载后执行,则应该使用 <code>turbolinks:load</code> 来替换 <code>onload</code>, <code>DOMContentLoaded</code> 事件。</p>
<p>解决上述问题具体的方式包括:</p>
<ol>
<li>在 <code>Turbolinks</code> 对应的生命周期里挂载和移除事件,计时器等。</li>
<li>使用事件委托,本质依然是幂等,只当节点存在时触发事件。</li>
<li>见下文。</li>
</ol>
<p>另外,<code>Turbolinks</code> 还存在的另一个问题则来源于 <code>addEventListener</code> 方法,在多次换页过程中触发的 <code>document.addEventListener</code> 会反复添加事件监听,这样会导致换页后依然能够执行上一个页面 <code>turbolinks:load</code> 生命周期里的内容。此时,生命周期内的操作幂等性就变得更加重要了。</p>
<hr>
<h2>
<a id="Turbolinks+Prefetch%EF%BC%9A%E9%A2%84%E5%8A%A0%E8%BD%BD%E7%9A%84%E6%96%B0%E6%80%9D%E8%B7%AF" href="#Turbolinks+Prefetch%EF%BC%9A%E9%A2%84%E5%8A%A0%E8%BD%BD%E7%9A%84%E6%96%B0%E6%80%9D%E8%B7%AF" class="anchor"></a>Turbolinks Prefetch:预加载的新思路</h2>
<p>当提到 <code>Turbolinks</code> 时,有一个相关衍生的生态组件就不得不拿出来聊一聊了——<code>Turbolink Prefetch</code>。</p>
<p>正如 <code>Prefetch</code> 这个名字一样,<code>Turbolinks Prefetch</code> 会进行预加载操作以提升页面的访问速度,其原理实际是一种叫做 <code>InstantClick</code> 技术。主要原理为:当用户鼠标置于链接上时,提前拉取目标地址的页面并缓存,当用户点击鼠标时直接从缓存中读取该页面。</p>
<p><img src="/attachments/UfiJbB6BqCX1ryjXkK93qLiK/turbolinks-prefetch.gif" alt="turbolinks-prefetch"></p>
<p>观察上图:</p>
<ol>
<li>当鼠标经过部分链接时,观察 <code>NetWork</code> 自动发送了请求。</li>
<li>往上滚动时,虽然鼠标再次经过重复的链接,单并没有重复发送请求。</li>
<li>点击已请求过的链接,<code>NetWork</code> 并没有发送其它请求,仅在页面加载后请求了页面上的资源文件。</li>
<li>与前面 <code>Turbolinks</code> 的演示相同,对页面的请求依然使用使用的是 <code>xhr</code> 方式。</li>
</ol>
<p>虽然从技术角度来说,借助 <code>Turbolinks</code> 已有的缓存机制实现这一功能似乎并没有什么太大的难度,但我认为这样的思路却非常值得学习:即利用用户的碎片时间提前拉取资源以减少用户的等待时间。从目前来看,这一思路在混合 <code>APP</code> 实现 <code>WebView</code> 秒开时起到了非常不错的效果。</p>
<hr>
<h2>
<a id="Assets+Pipeline%EF%BC%9ARails+%E8%87%AA%E5%B7%B1%E7%9A%84%E6%9E%84%E5%BB%BA%E5%B7%A5%E5%85%B7" href="#Assets+Pipeline%EF%BC%9ARails+%E8%87%AA%E5%B7%B1%E7%9A%84%E6%9E%84%E5%BB%BA%E5%B7%A5%E5%85%B7" class="anchor"></a>Assets Pipeline:Rails 自己的构建工具</h2>
<p>在我们谈论 <code>MVC</code> 下实现的前端有哪些问题时,提到了这样几个问题:</p>
<ol>
<li>缺少必要的构建工具,没有办法使用更高效的开发技术如 Sass,ECMAScript 的部分新特性。</li>
<li>资源管理不合理,需要多次重复拉取相同的资源文件。</li>
<li>……</li>
</ol>
<p>前面所介绍的 <code>Turbolinks</code>,<code>Turbolinks Prefetch</code> 等均是来自于交互方面的优化。而上面这些问题则指向了构建工具这一话题。</p>
<p>在今天,当我们开发完成一个前端项目,通常进入生产环境前需要对源代码进行一系列的操作,它们包括:</p>
<ol>
<li>编译代码,把 <code>Sass</code>, <code>Less</code>, 以及高版本的 <code>ECMAScript</code> 等非浏览器可以直接执行的内容编译成低版本代码或原生代码;</li>
<li>压缩代码,去除代码中不必要的换行,注释等,减少文件体积;</li>
<li>混淆代码,将 <code>JS</code> 代码编译为不可读的丑化代码,进一步缩减文件体积;</li>
<li>合并文件,减少 <code>HTTP</code> 请求次数;</li>
<li>通过摘要算法计算 <code>Hash</code> 作为静态资源文件的文件名,使资源可以根据文件内容的变化来合理利用浏览器缓存;</li>
</ol>
<p>而实现这些操作则需要依赖构建工具,比较有名的就是大家都熟悉的 <code>webpack</code>。</p>
<p>实际上,<code>Rails</code> 在很早就提供了这样一套框架用于静态资源的构建和管理——<code>Assets Pipeline</code>。</p>
<p>放在今天,我们以一名前端工程师的视角来看 <code>Assets Pipeline</code> 其实是非常容易理解的。它主要利用 <code>Sprockets</code> 这一构建工具来实现了上面描述静态资源压缩,,使得在 <code>MVC</code> 的结构下一样可以合理,优雅的管理前端资源。</p>
<p>同时 <code>Assets Pipeline</code> 还自带了 <code>Sass</code>, <code>CoffeeScript</code> 这些从 <code>Ruby</code> 社区孵化出的前端技术的支持。其中 <code>Sass</code> 提高了编写 <code>CSS</code> 的效率。而 <code>CoffeeScript</code> 则为 <code>ES5</code> 添加了诸多有用的语法扩展。如此,在 <code>MVC</code> 结构下也能获得良好的开发体验。</p>
<p>注意一个细节:<code>Assets Pipeline</code> 在进行生产环境的预编译行为时,本质上与现在我们运行 <code>npm run build</code> 使用 <code>webpack</code> 进行打包有着非常大的相似之处。不同之处在于,<code>Rails MVC</code> 下的前端开发并不会依赖 <code>JS Render</code> ,由此导致现代前端基于 <code>webpack</code> 构建的静态资源和 <code>Rails</code> 基于 <code>Assets Pipeline</code> 构建的静态资源在体积上有着量级的区别。</p>
<p>正是得益于这种技术栈不依赖 <code>JS</code> 构建页面,<code>Assets Pipeline</code> 可以放心大胆的将静态资源合并为一个单文件而不用担心体积过大造成资源加载慢的问题。</p>
<p>因而当我们查看一个 <code>Rails</code> 传统技术开发的网站,会发现它仅在浏览器第一次加载页面时,完整的拉取脚本与样式文件,而后在 <code>Turbolinks</code> 启动后,每一次换页都不会也无需拉取静态资源文件,而是仅获取需要显示的 <code>HTML</code> 内容。</p>
<p>有一个非常有意思的点:我们的站点在用户交互上与 <code>SPA</code> 一样在首屏拉取资源,也像 <code>SPA</code> 一样在无缝换页。</p>
<hr>
<h2>
<a id="UJS%EF%BC%9A%E4%BD%BF%E7%94%A8%E9%9D%9E%E4%BE%B5%E5%85%A5%E5%BC%8F%E7%9A%84+JavaScript+%E7%BC%A9%E5%87%8F%E9%87%8D%E5%A4%8D%E4%BB%A3%E7%A0%81" href="#UJS%EF%BC%9A%E4%BD%BF%E7%94%A8%E9%9D%9E%E4%BE%B5%E5%85%A5%E5%BC%8F%E7%9A%84+JavaScript+%E7%BC%A9%E5%87%8F%E9%87%8D%E5%A4%8D%E4%BB%A3%E7%A0%81" class="anchor"></a>UJS:使用非侵入式的 JavaScript 缩减重复代码</h2>
<p>除了引入完整的构建体系以外,其实 <code>Rails</code> 还做有一个较为有趣的前端构建技术名为 <code>UJS</code>,它是 <code>Unobtrusive JavaScript</code> 的缩写,翻译为中文为 “非侵入式 <code>JavaScript</code>”。</p>
<p><code>UJS</code> 本是指将 <code>JS</code> 与页面结构分离的代码风格,而 <code>Rails UJS</code> 只是基于这样一种风格编码的前端工具库,整个扩展仅有 700+ 行代码。</p>
<p><code>Rails UJS</code> 内部实现了前面我们在 <code>PJAX</code> 和 <code>Turbolinks</code> 里提到的让 <code>a</code> 标签和 <code>form</code> 表单自动使用 <code>xhr</code> 提交的方法,但它并没有针对这一行为做后续的操作,而是提供了对应的事件回调。即是说,开发人员可以自由的设置某一个 <code>a</code> 标签或 <code>form</code> 表单为 <code>xhr</code> 提交,而需要做的仅仅是在该标签上增加一个 <code>data-remote=true</code> 属性,而后即可在回调中拿到对应的请求值。</p>
<p>对比一下使用原生与使用 <code>Rails UJS</code> 实现表单 <code>xhr</code> 提交的代码</p>
<pre class="highlight"><code class="language-javascript"><span class="c1">// 原生</span>
<span class="kd">let</span> <span class="nx">form</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">"</span><span class="s2">form</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">form</span><span class="p">.</span><span class="nx">onsubmit</span> <span class="o">=</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">e</span><span class="p">.</span><span class="nx">preventDefault</span><span class="p">();</span>
<span class="c1">//阻止submit默认提交行为</span>
<span class="kd">let</span> <span class="nx">fd</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">FormData</span><span class="p">(</span><span class="nx">form</span><span class="p">);</span>
<span class="nx">fd</span><span class="p">.</span><span class="nx">append</span><span class="p">(</span><span class="dl">'</span><span class="s1">userId</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">1008611</span><span class="dl">'</span><span class="p">);</span>
<span class="kd">let</span> <span class="nx">xhr</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">XMLHttpRequest</span><span class="p">();</span>
<span class="nx">xhr</span><span class="p">.</span><span class="nx">open</span><span class="p">(</span><span class="dl">"</span><span class="s2">POST</span><span class="dl">"</span><span class="p">,</span><span class="dl">"</span><span class="s2">http://www.xxx/api/xxx</span><span class="dl">"</span><span class="p">);</span>
<span class="nx">xhr</span><span class="p">.</span><span class="nx">onreadystatechange</span> <span class="o">=</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="nx">xhr</span><span class="p">.</span><span class="nx">readyState</span> <span class="o">===</span> <span class="mi">4</span> <span class="o">&&</span> <span class="nx">xhr</span><span class="p">.</span><span class="nx">status</span> <span class="o">===</span> <span class="mi">200</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">xhr</span><span class="p">.</span><span class="nx">responseText</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nx">xhr</span><span class="p">.</span><span class="nx">send</span><span class="p">(</span><span class="nx">fd</span><span class="p">);</span>
<span class="p">}</span>
</code></pre>
<pre class="highlight"><code class="language-javascript"><span class="c1">// 使用 rails_ujs </span>
<span class="c1">// 需要在对应的标签上添加 data-remote=true 就像</span>
<span class="c1">// <form data-remote="true" id="remoteForm" action="/welcome/test_render">...</form></span>
<span class="nb">document</span><span class="p">.</span><span class="nx">querySelector</span><span class="p">(</span><span class="dl">"</span><span class="s2">#remoteLink</span><span class="dl">"</span><span class="p">).</span><span class="nx">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">ajax:success</span><span class="dl">'</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">xhr</span><span class="p">,</span> <span class="nx">status</span><span class="p">,</span> <span class="nx">err</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">xhr</span><span class="p">,</span> <span class="nx">status</span><span class="p">,</span> <span class="nx">err</span><span class="p">);</span>
<span class="p">});</span>
</code></pre>
<p>可以看到代码会简洁很多。</p>
<p>除此之外,<code>Rails UJS</code> 也支持了更多的 <code>xhr</code> 参数如 <code>method</code> 等,使得我们基于这种方式来构建 <code>xhr</code> 变的更快更高效,减少了大量的重复代码。</p>
<p>另一个有趣的用法是,在通过 <code>xhr</code> 请求时,由服务端渲染一段 <code>JavaScript</code>,可以很高效的实现一些交互逻辑。但由于这种方式不便于组织 <code>JS</code> 代码文件,此处不过多展开,详情可以读扩展阅读《<a href="https://ruby-china.org/topics/29046">Rails 用 RJS 简单有效的实现页面局部刷新</a>》。</p>
<hr>
<h2>
<a id="Stimulus%EF%BC%9ATurbolinks+%E5%B9%82%E7%AD%89%E9%97%AE%E9%A2%98%E7%9A%84%E6%9C%80%E7%BB%88%E8%A7%A3%EF%BC%9F" href="#Stimulus%EF%BC%9ATurbolinks+%E5%B9%82%E7%AD%89%E9%97%AE%E9%A2%98%E7%9A%84%E6%9C%80%E7%BB%88%E8%A7%A3%EF%BC%9F" class="anchor"></a>Stimulus:Turbolinks 幂等问题的最终解?</h2>
<p><code>Turbolinks</code> 为我们提供了基本的页面交互 <code>SPA</code> 化的解决方案,使我们能以非常轻量的方式来实现一个体验极佳的站点。</p>
<p><code>Assets Pipeline</code> 为我们带来的构建手段。</p>
<p><code>Rails UJS</code> 简化了我们构建 <code>AJAX</code> 交互的代码量。</p>
<p>但站在今天的角度来看,似乎编码体验上并没有太大的改进。我们依然在重复的码着 <code>JQuery</code>,我们也依然在重复的操作 <code>DOM</code>,甚至对于如何复用一个页面组件,我们依然没有一个合适的方式。与此同时,引入的 <code>Turbolinks</code> 带来了新的负担,编写 <code>JS</code> 代码的时候我们需要更加注意幂等性……</p>
<p>当我们开始思考这样的问题的时候,<code>Stimulus</code> 来了。</p>
<p><code>Stimulus</code> 同样是一个轻量的前端库,它通过 <code>H5</code> 的 <code>Mutation Observer</code> 这一特性彻底解决了 <code>Turbolinks</code> 幂等的问题。</p>
<p><code>Mutation Observer</code> 可以完成 <code>DOM</code> 节点的监听,在 <code>DOM</code> 节点发生变化时,指定对应的逻辑。我们可以通俗的理解这一 <code>API</code> 为原生的 <code>DOM</code> 节点添加了自己的生命周期。</p>
<p>直接看下面的 <code>Stimulus</code> 代码:</p>
<pre class="highlight"><code class="language-javascript"><span class="k">import</span> <span class="p">{</span> <span class="nx">Controller</span> <span class="p">}</span> <span class="k">from</span> <span class="dl">"</span><span class="s2">stimulus</span><span class="dl">"</span>
<span class="k">export</span> <span class="k">default</span> <span class="kd">class</span> <span class="kd">extends</span> <span class="nx">Controller</span> <span class="p">{</span>
<span class="kd">static</span> <span class="nx">values</span> <span class="o">=</span> <span class="p">{</span> <span class="na">url</span><span class="p">:</span> <span class="nb">String</span><span class="p">,</span> <span class="na">refreshInterval</span><span class="p">:</span> <span class="nb">Number</span> <span class="p">}</span>
<span class="nx">connect</span><span class="p">()</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">load</span><span class="p">()</span>
<span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">hasRefreshIntervalValue</span><span class="p">)</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">startRefreshing</span><span class="p">()</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="nx">disconnect</span><span class="p">()</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">stopRefreshing</span><span class="p">()</span>
<span class="p">}</span>
<span class="nx">load</span><span class="p">()</span> <span class="p">{</span>
<span class="nx">fetch</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">urlValue</span><span class="p">)</span>
<span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">response</span> <span class="o">=></span> <span class="nx">response</span><span class="p">.</span><span class="nx">text</span><span class="p">())</span>
<span class="p">.</span><span class="nx">then</span><span class="p">(</span><span class="nx">html</span> <span class="o">=></span> <span class="k">this</span><span class="p">.</span><span class="nx">element</span><span class="p">.</span><span class="nx">innerHTML</span> <span class="o">=</span> <span class="nx">html</span><span class="p">)</span>
<span class="p">}</span>
<span class="nx">startRefreshing</span><span class="p">()</span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">refreshTimer</span> <span class="o">=</span> <span class="nx">setInterval</span><span class="p">(()</span> <span class="o">=></span> <span class="p">{</span>
<span class="k">this</span><span class="p">.</span><span class="nx">load</span><span class="p">()</span>
<span class="p">},</span> <span class="k">this</span><span class="p">.</span><span class="nx">refreshIntervalValue</span><span class="p">)</span>
<span class="p">}</span>
<span class="nx">stopRefreshing</span><span class="p">()</span> <span class="p">{</span>
<span class="k">if</span> <span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">refreshTimer</span><span class="p">)</span> <span class="p">{</span>
<span class="nx">clearInterval</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">refreshTimer</span><span class="p">)</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="p">}</span>
</code></pre>
<p>我有理由相信写过 <code>vue</code>, <code>react</code> 组件的现代前端工程师可以没有什么压力的阅读这段代码……</p>
<p><code>Stimulus</code> 通过 <code>data-controller</code> 将逻辑关联到 <code>DOM</code> 节点,同时提供了对应节点的生命周期钩子函数,当节点被渲染后,会触发 <code>connect</code> 方法,而节点被销毁时会触发 <code>disconnect</code> 。同时 <code>Stimulus</code> 还支持定义状态与方法,最终这些内容被合理的挂载到节点的各位置,就像这样:</p>
<pre class="highlight"><code class="language-html"><span class="nt"><div</span> <span class="na">data-controller=</span><span class="s">"content-loader"</span>
<span class="na">data-content-loader-url-value=</span><span class="s">"/messages.html"</span>
<span class="na">data-content-loader-refresh-interval-value=</span><span class="s">"5000"</span><span class="nt">></div></span>
</code></pre>
<p>这一段代码复制于 <code>Stimulus</code> 的文档,它实现了一个可以自动异步加载页面的组件。</p>
<p>由于节点拥有了自己的生命周期,只有当节点真正被渲染在页面上时 <code>Stimulus</code> 才会真正的执行对应的挂载逻辑,因而我们不再需要担心伴随 <code>Turbolinks</code> 的换页带来的事件绑定心智问题。</p>
<p>同时,<code>Stimulus</code> 也完全解决了复用的问题,当需要在另一位置复用这一组件时,我们只需要为该节点添加上对应的 <code>data-controller</code> 属性即可。</p>
<p>最后,<code>Stimulus</code> 通常在单一文件里定义组件,也就是说,基于 <code>Stimulus</code> 开发的应用不会再出现同一个 <code>JS</code> 文件里有成百上千行不知所云毫无关联的 <code>JavaScript</code> 代码的情况。</p>
<hr>
<h2>
<a id="%E7%BB%93%E8%AF%AD%EF%BC%88%E7%A2%8E%E7%A2%8E%E5%BF%B5%EF%BC%89" href="#%E7%BB%93%E8%AF%AD%EF%BC%88%E7%A2%8E%E7%A2%8E%E5%BF%B5%EF%BC%89" class="anchor"></a>结语(碎碎念)</h2>
<p>其实本来打算再倒腾下 <code>Hotwire</code> 的,但是实在时间有限……</p>
<p>前端是一个大的领域,即使放到一个框架里,内容也是非常多的。</p>
<p>关于 <code>Rails</code> 的前端内容大概就先写到这里了,后面如果有空还会再补充。</p>
<p>最后放上 <a href="https://ruby-china.org/">Ruby China</a> 链接,可以从交互等方面来体验一下,几乎完全与 <code>SPA</code> 类似的交互体验。</p>
<hr>
<h2>
<a id="%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99" href="#%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99" class="anchor"></a>参考资料</h2>
<ul>
<li><a href="https://github.com/rails/rails">Ruby On Rails</a></li>
<li><a href="https://github.com/defunkt/jquery-pjax">JQuery PJAX</a></li>
<li><a href="https://github.com/turbolinks/turbolinks">Turbolinks</a></li>
<li><a href="https://github.com/huacnlee/turbolinks-prefetch">Turbolinks Prefetch</a></li>
<li><a href="https://github.com/rails/jquery-ujs">JQuery UJS</a></li>
<li><a href="https://github.com/rails/rails-ujs/tree/master">Rails UJS</a></li>
<li><a href="https://github.com/hotwired/stimulus">Stimulus</a></li>
</ul>
前言
其实想写这篇文很久了……
从毕业实习接触 Rails 到现在两年多了。一开始是基于 Rails 框架来给 Android 客户端提供 API 接口,后来也尝试在 Rails 上倒腾了一些...
鱼翅
https://geeknote.net/yuchiXiong
https://geeknote.net/yuchiXiong/posts/55
2021-07-06T14:53:30Z
2022-10-28T14:36:15Z
在 Rails 中接入微信支付 V3 瞎谈
<h1>
<a id="0.+%E5%86%99%E5%9C%A8%E5%89%8D%E9%9D%A2" href="#0.+%E5%86%99%E5%9C%A8%E5%89%8D%E9%9D%A2" class="anchor"></a>0. 写在前面</h1>
<p>其实公司项目早在去年就做完第一版的微信支付了,但是由于种种原因并没有上线,一直拖到了今年。</p>
<p>让人 *疼 的一件事情是,虽然基础功能都已经实现了,代码也有了,但是考虑微信支付更新了V3版本,以及反正产品还没有正式上微信支付,公司最终决定直接接入V3版本的微信支付,之前的代码基本上没法儿再用了。</p>
<p>另外 <code>Ruby</code> 这个没人疼没人爱的孩子又着实可怜的不行,V2版本的微信支付还可以参考使用社区姜老师(<code>@jasl</code>)的 <a href="https://github.com/jasl/wx_pay">微信支付GEM</a>,V3版本干脆就没有可以参考的 <code>Ruby</code> 代码了。</p>
<p>好就好在V3开始的微信支付相对之前的V2要简单许多,以及相关的文档也比较完整,本文主要提供一些代码参考,以及分享一些接入途中遇到的坑。</p>
<h1>
<a id="1.+%E9%9C%80%E8%A6%81%E5%87%86%E5%A4%87%E4%BB%80%E4%B9%88%EF%BC%9F" href="#1.+%E9%9C%80%E8%A6%81%E5%87%86%E5%A4%87%E4%BB%80%E4%B9%88%EF%BC%9F" class="anchor"></a>1. 需要准备什么?</h1>
<p>接入云服务通常来说都需要准备一系列的各种 <code>公钥 私钥 证书 APPID APPSECRET</code> 等乱七八糟的东西,此处进行简单的列举,详细的申请步骤可以参考 <a href="https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_1.shtml">《微信支付接入前准备》</a> 一文,相对来说已经描述的比较清晰了。</p>
<p>直接放代码:</p>
<pre class="highlight"><code class="language-ruby"><span class="c1"># 在申请微信支付时由平台分配的开发者ID</span>
<span class="no">APPID</span> <span class="o">=</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">credentials</span><span class="p">[</span><span class="ss">:wx_pay</span><span class="p">][</span><span class="ss">:appid</span><span class="p">]</span>
<span class="c1"># 在申请微信支付时的收款账号</span>
<span class="no">MCHID</span> <span class="o">=</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">credentials</span><span class="p">[</span><span class="ss">:wx_pay</span><span class="p">][</span><span class="ss">:mch_id</span><span class="p">]</span>
<span class="c1"># 微信支付 API V3 平台公钥,将用在回调验签中</span>
<span class="no">API_V3_SECRET</span> <span class="o">=</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">credentials</span><span class="p">[</span><span class="ss">:wx_pay</span><span class="p">][</span><span class="ss">:api_v3_secret</span><span class="p">]</span>
<span class="c1"># 微信支付相关接口域名</span>
<span class="no">DOMAIN</span> <span class="o">=</span> <span class="s1">'https://api.mch.weixin.qq.com'</span><span class="p">.</span><span class="nf">freeze</span>
</code></pre>
<p>以上几个参数是可以直接在后台拿到的(大概……),在后台进行支付申请的时候应该还有一组证书文件需要下载,下载后解压大概长这样:</p>
<p><img src="https://assets-blog-xiongyuchi.oss-cn-beijing.aliyuncs.com/uploads/production/blog_photos/48/1609125555-image.png" alt="图片"></p>
<p>此时我们需要获取证书的序列号,该序列号会被用在签名里。</p>
<p>如何查看证书序列号,可以查看 <a href="https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay7_0.shtml#part-5">《微信支付 - 如何查看证书序列号》</a> 一文,在终端中利用 <code>OpenSSL</code> 可以很轻松的拿到。</p>
<p>如果你不方便使用终端,可以尝试使用如下的 <code>Ruby</code> 代码获取(其实也是一样的):</p>
<pre class="highlight"><code class="language-ruby"><span class="nb">require</span> <span class="s1">'openssl'</span>
<span class="nb">puts</span> <span class="no">OpenSSL</span><span class="o">::</span><span class="no">X509</span><span class="o">::</span><span class="no">Certificate</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="nb">open</span><span class="p">(</span><span class="n">your_cert_file_path</span><span class="p">).</span><span class="nf">read</span><span class="p">).</span><span class="nf">serial</span><span class="p">.</span><span class="nf">to_s</span><span class="p">(</span><span class="mi">16</span><span class="p">)</span>
</code></pre>
<blockquote>
<p>注意此处 <code>OpenSSL::X509::Certificate#serial.to_s</code> 得到的是一个十进制的值,而微信支付方统一使用的是十六进制的,因此我们传递了参数将其转为16进制。</p>
</blockquote>
<p>由于证书会放在服务器上,该序列号是不会变更的,所以可以直接写入 <code>credentials</code> 里</p>
<pre class="highlight"><code class="language-ruby"><span class="no">SERIAL_NO</span> <span class="o">=</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">credentials</span><span class="p">[</span><span class="ss">:wx_pay</span><span class="p">][</span><span class="ss">:serial_no</span><span class="p">]</span>
</code></pre>
<p>至此,我们准备好了所有的参数。</p>
<h1>
<a id="2.+%E7%AD%BE%E5%90%8D" href="#2.+%E7%AD%BE%E5%90%8D" class="anchor"></a>2. 签名</h1>
<p>云服务为了保证通讯的双方合法,通常会使用签名的方式来进行验证,我们请求任何一个微信支付方的接口时都需要进行签名并将签名置于请求头中。</p>
<p>关于签名与验签的详细说明请翻阅微信支付文档:</p>
<p><a href="https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_0.shtml">《微信支付 - 签名生成》</a></p>
<p><a href="https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_1.shtml">《微信支付 - 签名验证》</a></p>
<p>其中签名用于向微信支付发起请求,验签用于验证支付回调是否来自于微信支付。</p>
<p>以下是参考代码,我将签名与验证签名的两个方法封装在了同一个类中:</p>
<pre class="highlight"><code class="language-ruby"><span class="nb">require</span> <span class="s1">'openssl'</span>
<span class="nb">require</span> <span class="s1">'base64'</span>
<span class="k">class</span> <span class="nc">Signature</span>
<span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">private_key</span> <span class="o">=</span> <span class="nb">open</span><span class="p">(</span><span class="n">your_apiclient_key</span><span class="p">.</span><span class="nf">pem_path</span><span class="p">).</span><span class="nf">read</span><span class="p">)</span>
<span class="vi">@private_key</span> <span class="o">=</span> <span class="n">private_key</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">sign</span><span class="p">(</span><span class="o">*</span><span class="n">params</span><span class="p">)</span>
<span class="n">str</span> <span class="o">=</span> <span class="n">params</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="s1">''</span><span class="p">).</span><span class="nf">join</span><span class="p">(</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span>
<span class="n">pkey</span> <span class="o">=</span> <span class="no">OpenSSL</span><span class="o">::</span><span class="no">PKey</span><span class="o">::</span><span class="no">RSA</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="vi">@private_key</span><span class="p">)</span>
<span class="n">digest</span> <span class="o">=</span> <span class="no">OpenSSL</span><span class="o">::</span><span class="no">Digest</span><span class="o">::</span><span class="no">SHA256</span><span class="p">.</span><span class="nf">new</span>
<span class="n">signature</span> <span class="o">=</span> <span class="n">pkey</span><span class="p">.</span><span class="nf">sign</span><span class="p">(</span><span class="n">digest</span><span class="p">,</span> <span class="n">str</span><span class="p">)</span>
<span class="no">Base64</span><span class="p">.</span><span class="nf">strict_encode64</span><span class="p">(</span><span class="n">signature</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">def</span> <span class="nf">verify</span><span class="p">(</span><span class="n">params</span> <span class="o">=</span> <span class="p">[],</span> <span class="n">sign</span><span class="p">)</span>
<span class="n">str</span> <span class="o">=</span> <span class="n">params</span><span class="p">.</span><span class="nf">push</span><span class="p">(</span><span class="s1">''</span><span class="p">).</span><span class="nf">join</span><span class="p">(</span><span class="s2">"</span><span class="se">\n</span><span class="s2">"</span><span class="p">)</span>
<span class="n">pkey</span> <span class="o">=</span> <span class="no">OpenSSL</span><span class="o">::</span><span class="no">PKey</span><span class="o">::</span><span class="no">RSA</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="vi">@private_key</span><span class="p">)</span>
<span class="n">digest</span> <span class="o">=</span> <span class="no">OpenSSL</span><span class="o">::</span><span class="no">Digest</span><span class="o">::</span><span class="no">SHA256</span><span class="p">.</span><span class="nf">new</span>
<span class="n">pkey</span><span class="p">.</span><span class="nf">verify</span><span class="p">(</span><span class="n">digest</span><span class="p">,</span> <span class="no">Base64</span><span class="p">.</span><span class="nf">decode64</span><span class="p">(</span><span class="n">sign</span><span class="p">),</span> <span class="n">str</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<p>此处的构造函数中传入了一个动态的证书是因为:</p>
<p><strong>签名与验签需要使用不同的证书!</strong></p>
<p><strong>签名与验签需要使用不同的证书!</strong></p>
<p><strong>签名与验签需要使用不同的证书!</strong></p>
<p>重要的事情我说了三遍了_(:з」∠)_,以下是 <a href="https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_1.shtml">微信支付 - 签名验证</a> 的截图:</p>
<p><img src="https://assets-blog-xiongyuchi.oss-cn-beijing.aliyuncs.com/uploads/production/blog_photos/48/1609126710-image.png" alt="图片"></p>
<h1>
<a id="3.+%E5%B0%81%E8%A3%85%E8%AF%B7%E6%B1%82" href="#3.+%E5%B0%81%E8%A3%85%E8%AF%B7%E6%B1%82" class="anchor"></a>3. 封装请求</h1>
<p>完成签名后我们可以开始尝试接入微信支付的相关API了,按照以往的惯例我们会把这些请求做一个简单的封装,例如统一下单接口(代码写的有点丑,别打我_(:з」∠)_):</p>
<pre class="highlight"><code class="language-ruby"><span class="k">class</span> <span class="nc">Service</span>
<span class="no">DOMAIN</span> <span class="o">=</span> <span class="s1">'https://api.mch.weixin.qq.com'</span><span class="p">.</span><span class="nf">freeze</span>
<span class="no">APPID</span> <span class="o">=</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">credentials</span><span class="p">[</span><span class="ss">:wx_pay</span><span class="p">][</span><span class="ss">:appid</span><span class="p">]</span>
<span class="no">MCHID</span> <span class="o">=</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">credentials</span><span class="p">[</span><span class="ss">:wx_pay</span><span class="p">][</span><span class="ss">:mch_id</span><span class="p">]</span>
<span class="no">SERIAL_NO</span> <span class="o">=</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">application</span><span class="p">.</span><span class="nf">credentials</span><span class="p">[</span><span class="ss">:wx_pay</span><span class="p">][</span><span class="ss">:serial_no</span><span class="p">]</span>
<span class="c1"># * app支付接口</span>
<span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">transactions_app</span><span class="p">(</span><span class="n">order_info</span><span class="p">)</span>
<span class="c1"># * 构造请求参数</span>
<span class="nb">method</span> <span class="o">=</span> <span class="s1">'POST'</span><span class="p">.</span><span class="nf">freeze</span>
<span class="n">url</span> <span class="o">=</span> <span class="s1">'/v3/pay/transactions/app'</span><span class="p">.</span><span class="nf">freeze</span>
<span class="n">timestamp</span> <span class="o">=</span> <span class="n">order_info</span><span class="p">[</span><span class="ss">:timestamp</span><span class="p">]</span>
<span class="n">nonce_str</span> <span class="o">=</span> <span class="n">order_info</span><span class="p">[</span><span class="ss">:nonce_str</span><span class="p">]</span>
<span class="n">body</span> <span class="o">=</span> <span class="p">{</span>
<span class="s2">"appid"</span><span class="p">:</span> <span class="no">APPID</span><span class="p">,</span>
<span class="s2">"mchid"</span><span class="p">:</span> <span class="no">MCHID</span><span class="p">,</span>
<span class="s2">"description"</span><span class="p">:</span> <span class="n">order_info</span><span class="p">[</span><span class="ss">:description</span><span class="p">],</span>
<span class="s2">"out_trade_no"</span><span class="p">:</span> <span class="n">order_info</span><span class="p">[</span><span class="ss">:out_trade_no</span><span class="p">],</span>
<span class="s2">"notify_url"</span><span class="p">:</span> <span class="n">your_notify_url</span><span class="p">,</span>
<span class="s2">"amount"</span><span class="p">:</span> <span class="p">{</span>
<span class="s2">"total"</span><span class="p">:</span> <span class="n">order_info</span><span class="p">[</span><span class="ss">:amount</span><span class="p">]</span>
<span class="p">}</span>
<span class="p">}</span>
<span class="c1"># 构造签名</span>
<span class="n">signature</span> <span class="o">=</span> <span class="no">WeChat</span><span class="o">::</span><span class="no">Util</span><span class="o">::</span><span class="no">CodeV3</span><span class="p">.</span><span class="nf">new</span><span class="p">.</span><span class="nf">sign</span><span class="p">(</span><span class="nb">method</span><span class="p">,</span> <span class="n">url</span><span class="p">,</span> <span class="n">timestamp</span><span class="p">,</span> <span class="n">nonce_str</span><span class="p">,</span> <span class="n">body</span><span class="p">.</span><span class="nf">to_json</span><span class="p">)</span>
<span class="c1"># 构造请求头</span>
<span class="n">authorization_str</span> <span class="o">=</span> <span class="s1">'WECHATPAY2-SHA256-RSA2048 '</span> <span class="o">+</span> <span class="s2">"mchid=</span><span class="se">\"</span><span class="s2">"</span> <span class="o">+</span> <span class="no">MCHIDMCHID</span> <span class="o">+</span> <span class="s2">"</span><span class="se">\"</span><span class="s2">,"</span> <span class="o">+</span> <span class="s2">"nonce_str=</span><span class="se">\"</span><span class="s2">"</span> <span class="o">+</span> <span class="n">nonce_str</span> <span class="o">+</span> <span class="s2">"</span><span class="se">\"</span><span class="s2">,"</span> <span class="o">+</span> <span class="s2">"timestamp=</span><span class="se">\"</span><span class="s2">"</span> <span class="o">+</span> <span class="n">timestamp</span><span class="p">.</span><span class="nf">to_s</span> <span class="o">+</span> <span class="s2">"</span><span class="se">\"</span><span class="s2">,"</span> <span class="o">+</span> <span class="s2">"serial_no=</span><span class="se">\"</span><span class="s2">"</span> <span class="o">+</span> <span class="no">SERIAL_NO</span> <span class="o">+</span> <span class="s2">"</span><span class="se">\"</span><span class="s2">,"</span> <span class="o">+</span> <span class="s2">"signature=</span><span class="se">\"</span><span class="s2">"</span> <span class="o">+</span> <span class="n">signature</span> <span class="o">+</span> <span class="s2">"</span><span class="se">\"</span><span class="s2">"</span>
<span class="c1"># 发送请求</span>
<span class="n">response</span> <span class="o">=</span> <span class="no">RestClient</span><span class="p">.</span><span class="nf">post</span><span class="p">(</span><span class="no">DOMAIN</span> <span class="o">+</span> <span class="n">url</span><span class="p">,</span> <span class="n">body</span><span class="p">.</span><span class="nf">to_json</span><span class="p">,</span> <span class="n">headers</span> <span class="o">=</span> <span class="p">{</span>
<span class="s2">"Content-Type"</span><span class="p">:</span> <span class="s1">'application/json'</span><span class="p">,</span>
<span class="s2">"Accept"</span><span class="p">:</span> <span class="s1">'application/json'</span><span class="p">,</span>
<span class="s2">"Authorization"</span><span class="p">:</span> <span class="n">authorization_str</span>
<span class="p">})</span>
<span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="n">response</span><span class="p">.</span><span class="nf">body</span><span class="p">)</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<h1>
<a id="4.+%E8%8E%B7%E5%8F%96%E5%B9%B3%E5%8F%B0%E8%AF%81%E4%B9%A6" href="#4.+%E8%8E%B7%E5%8F%96%E5%B9%B3%E5%8F%B0%E8%AF%81%E4%B9%A6" class="anchor"></a>4. 获取平台证书</h1>
<p>在一个完整的支付环节中,正确的向微信支付发送请求是第一步,第二步我们需要对响应进行验证以确保请求确实来自于微信支付方。</p>
<p>无论是验证响应,还是后续的微信支付回调通知,都需要使用相同的方法对签名进行验证。虽然相关的算法在前面已经实现了,但由于用于验签的证书与签名所使用的的证书是不一样的,因此需要提前准备证书。</p>
<p>微信支付平台证书是动态的,每隔一段时间会进行替换以确保安全性。因此需要通过接口拉取证书。</p>
<p><a href="http://192.168.31.72:3000/docs/index.html#/Order/post_v5_orders_huawei_pay_purchases_verify">《微信支付 - 获取平台证书列表》</a></p>
<p>发送请求与前面的其他接口一样,只要按照文档进行签名等既可,没什么需要讲的。此处主要走一下拉取到证书后的流程。</p>
<p>获取平台证书列表返回的响应中,平台证书是经过加密的,要获取证书首先需要进行解密操作。</p>
<p><a href="https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_2.shtml">《微信支付 - 证书和回调版本解密》</a></p>
<p>微信支付对证书加密使用的是 <code>AEAD_AES_256_GCM</code> 加密算法,虽然在 <code>Ruby</code> 文档中找到了对应的标准库方法,但其实现细节似乎与微信支付给出的参数有些区别导致没有解密成功。</p>
<blockquote>
<p>解释:
在<a href="https://ruby-doc.org/stdlib-3.0.0/libdoc/openssl/rdoc/OpenSSL/Cipher.html">标准库文档</a>中,找到了 <code>AES-128-GCM</code> 相关内容:
<img src="https://assets-blog-xiongyuchi.oss-cn-beijing.aliyuncs.com/uploads/production/blog_photos/48/1609135747-image.png" alt="图片">
但让人疑惑的是微信支付返回的参数中的并没有解密需要的 <code>auth_tag</code> 。
如果有了解 <code>AEAD</code> 算法的朋友麻烦在评论区赐教解释一下_(:з」∠)_</p>
</blockquote>
<p>最终找到了一个 <a href="https://github.com/onelogin/aead">AEAD - GEM</a>,引入到工程后发现不能正常工作,查阅相关 <a href="https://github.com/onelogin/aead/issues/8">issue</a> 找到了末尾的建议:</p>
<p><img src="https://assets-blog-xiongyuchi.oss-cn-beijing.aliyuncs.com/uploads/production/blog_photos/48/1609136215-image.png" alt="图片"></p>
<p><img src="https://assets-blog-xiongyuchi.oss-cn-beijing.aliyuncs.com/uploads/production/blog_photos/48/1609136227-image.png" alt="图片"></p>
<p>也就是说如果要使用该 <code>GEM</code> ,我们需要添加的是由 <code>shopify</code> 修改过的版本,查看 <code>repo</code> 发现后者是由 <code>shopify</code> <code>fork</code> 后修改过的版本,如果不放心的话可以把代码 <code>clone</code> 部署到私有云上……<code>Gemfile</code> 里指定自己的私有云 <code>repo</code> 地址既可。</p>
<p>而后我们对证书进行解密既可:</p>
<pre class="highlight"><code class="language-ruby"><span class="nb">require</span> <span class="s1">'aead'</span>
<span class="k">def</span> <span class="nc">self</span><span class="o">.</span><span class="nf">aead_decode</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="n">nonce</span><span class="p">,</span> <span class="n">aad</span><span class="p">,</span> <span class="n">cipher_text</span><span class="p">)</span>
<span class="n">mode</span> <span class="o">=</span> <span class="no">AEAD</span><span class="o">::</span><span class="no">Cipher</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="s1">'AES-256-GCM'</span><span class="p">)</span>
<span class="n">cipher</span> <span class="o">=</span> <span class="n">mode</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">key</span><span class="p">)</span>
<span class="n">cipher</span><span class="p">.</span><span class="nf">decrypt</span><span class="p">(</span><span class="n">nonce</span><span class="p">,</span> <span class="n">aad</span><span class="p">,</span> <span class="n">cipher_text</span><span class="p">)</span>
<span class="k">end</span>
</code></pre>
<p>证书虽然每隔一段时间会更新,但并不需要每次都需要拉取,根据 <a href="https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay5_0.shtml">《微信支付 - 平台证书更新指引》</a> 的建议,此处我们在每次进行验签前,先将本地已经存储的证书序列号与 微信响应/回调请求 中声明的证书序列号做对比,当本次响应/请求使用的证书与本地不符时,才拉取最新的证书并持久化到本地,最终实现惰性更新。以下是参考代码:</p>
<pre class="highlight"><code class="language-ruby"><span class="k">def</span> <span class="nf">fetch</span><span class="p">(</span><span class="n">serial</span><span class="p">)</span>
<span class="c1"># 1. 通过serial序列号查找本地是否存在响应的证书,如果有,将证书实例返回</span>
<span class="o">...</span>
<span class="k">return</span> <span class="no">OpenSSL</span><span class="o">::</span><span class="no">X509</span><span class="o">::</span><span class="no">Certificate</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="nb">open</span><span class="p">(</span><span class="n">cert_file_name</span><span class="p">).</span><span class="nf">read</span><span class="p">)</span> <span class="k">if</span> <span class="no">File</span><span class="p">.</span><span class="nf">exists?</span> <span class="n">cert_file_name</span>
<span class="c1"># 2. 本地不存在时,请求接口拉取当前最新的证书列表</span>
<span class="n">cert_list</span> <span class="o">=</span> <span class="no">WeChat</span><span class="o">::</span><span class="no">Service</span><span class="p">.</span><span class="nf">certificates</span>
<span class="n">cert</span> <span class="o">=</span> <span class="n">cert_list</span><span class="p">[</span><span class="ss">:body</span><span class="p">][</span><span class="s1">'data'</span><span class="p">].</span><span class="nf">select</span><span class="p">{</span><span class="o">|</span><span class="n">cert</span><span class="o">|</span> <span class="n">cert</span><span class="p">[</span><span class="s1">'serial_no'</span><span class="p">]</span> <span class="o">==</span> <span class="n">serial</span> <span class="p">}[</span><span class="mi">0</span><span class="p">]</span>
<span class="k">return</span> <span class="kp">false</span> <span class="k">if</span> <span class="n">cert</span><span class="p">.</span><span class="nf">nil?</span>
<span class="c1"># 3. 对证书进行解密</span>
<span class="n">plaintext</span> <span class="o">=</span> <span class="n">aead_decode</span><span class="p">(</span><span class="no">API_V3_SECRET</span><span class="p">,</span>
<span class="n">cert</span><span class="p">[</span><span class="s1">'encrypt_certificate'</span><span class="p">][</span><span class="s1">'nonce'</span><span class="p">],</span>
<span class="n">cert</span><span class="p">[</span><span class="s1">'encrypt_certificate'</span><span class="p">][</span><span class="s1">'associated_data'</span><span class="p">],</span>
<span class="no">Base64</span><span class="p">.</span><span class="nf">decode64</span><span class="p">(</span><span class="n">cert</span><span class="p">[</span><span class="s1">'encrypt_certificate'</span><span class="p">][</span><span class="s1">'ciphertext'</span><span class="p">]))</span>
<span class="c1"># 4. 将证书持久化到本地并返回证书实例</span>
<span class="n">file</span> <span class="o">=</span> <span class="no">File</span><span class="p">.</span><span class="nf">open</span><span class="p">(</span><span class="n">cert_file_name</span><span class="p">,</span> <span class="s1">'a+'</span><span class="p">)</span>
<span class="n">file</span><span class="o">&</span><span class="p">.</span><span class="nf">syswrite</span><span class="p">(</span><span class="n">plaintext</span><span class="p">)</span>
<span class="no">OpenSSL</span><span class="o">::</span><span class="no">X509</span><span class="o">::</span><span class="no">Certificate</span><span class="p">.</span><span class="nf">new</span> <span class="nb">open</span><span class="p">(</span><span class="n">cert_file_name</span><span class="p">).</span><span class="nf">read</span>
<span class="k">end</span>
</code></pre>
<h1>
<a id="5.+%E5%AF%B9%E5%9B%9E%E8%B0%83%E8%AF%B7%E6%B1%82%E3%80%81%E5%93%8D%E5%BA%94%E8%BF%9B%E8%A1%8C%E9%AA%8C%E7%AD%BE" href="#5.+%E5%AF%B9%E5%9B%9E%E8%B0%83%E8%AF%B7%E6%B1%82%E3%80%81%E5%93%8D%E5%BA%94%E8%BF%9B%E8%A1%8C%E9%AA%8C%E7%AD%BE" class="anchor"></a>5. 对回调请求、响应进行验签</h1>
<p>验证签名比较简单,根据 <a href="https://pay.weixin.qq.com/wiki/doc/apiv3/wechatpay/wechatpay4_1.shtml">《微信支付 - 签名验证》</a> 的指引,我们需要对 响应/回调请求 进行如下验证:</p>
<ul>
<li>检验微信支付方使用的证书</li>
<li>使用微信支付证书中的公钥验证签名</li>
<li>解密响应报文,获取响应数据</li>
<li>验证订单数据</li>
</ul>
<p>此处有一个不得不提的坑是在验证签名时应使用 <strong>请求报文的原文</strong> ,在 <code>Rails</code> 中可以通过如下方法获取:</p>
<pre class="highlight"><code class="language-ruby"><span class="n">request</span><span class="p">.</span><span class="nf">raw_post</span>
</code></pre>
<p>以下使用异步回调通知进行示例(示例中删除了部分业务代码):</p>
<pre class="highlight"><code class="language-ruby"><span class="c1"># frozen_string_literal: true</span>
<span class="nb">require</span> <span class="s1">'pay/wechat/wechat'</span>
<span class="nb">require</span> <span class="s1">'pay/wechat/util'</span>
<span class="k">class</span> <span class="nc">WechatController</span> <span class="o"><</span> <span class="no">ApplicationController</span>
<span class="c1"># * 微信支付后的回调接口</span>
<span class="k">def</span> <span class="nf">pay_notify</span>
<span class="n">wechat_pay_serial</span> <span class="o">=</span> <span class="n">request</span><span class="p">.</span><span class="nf">headers</span><span class="p">[</span><span class="s1">'Wechatpay-Serial'</span><span class="p">]</span>
<span class="n">wechat_timestamp</span> <span class="o">=</span> <span class="n">request</span><span class="p">.</span><span class="nf">headers</span><span class="p">[</span><span class="s1">'Wechatpay-Timestamp'</span><span class="p">]</span>
<span class="n">wechat_pay_signature</span> <span class="o">=</span> <span class="n">request</span><span class="p">.</span><span class="nf">headers</span><span class="p">[</span><span class="s1">'Wechatpay-Signature'</span><span class="p">]</span>
<span class="n">wechat_pay_nonce</span> <span class="o">=</span> <span class="n">request</span><span class="p">.</span><span class="nf">headers</span><span class="p">[</span><span class="s1">'Wechatpay-Nonce'</span><span class="p">]</span>
<span class="c1"># 获取请求体原文</span>
<span class="n">body</span> <span class="o">=</span> <span class="n">request</span><span class="p">.</span><span class="nf">raw_post</span>
<span class="c1"># 1. 验证微信平台证书与当前持有的证书是否相同</span>
<span class="c1"># 当微信平台证书序列号与本地证书序列号不相同时,应答错误信息,等待下一次回调</span>
<span class="k">unless</span> <span class="no">WeChat</span><span class="o">::</span><span class="no">Util</span><span class="o">::</span><span class="no">ResultVerify</span><span class="p">.</span><span class="nf">new</span><span class="p">.</span><span class="nf">serial_verify?</span><span class="p">(</span><span class="n">wechat_pay_serial</span><span class="p">)</span>
<span class="k">raise</span> <span class="o">......</span>
<span class="k">end</span>
<span class="c1"># 2. 验证回调签名</span>
<span class="c1"># 获取微信平台证书公钥并进行签名验证</span>
<span class="n">wechat_pay_public_key</span> <span class="o">=</span> <span class="no">WeChat</span><span class="o">::</span><span class="no">Util</span><span class="o">::</span><span class="no">Certificate</span><span class="p">.</span><span class="nf">get</span><span class="p">(</span><span class="n">wechat_pay_serial</span><span class="p">).</span><span class="nf">public_key</span><span class="p">.</span><span class="nf">to_pem</span>
<span class="k">unless</span> <span class="no">WeChat</span><span class="o">::</span><span class="no">Util</span><span class="o">::</span><span class="no">CodeV3</span><span class="p">.</span><span class="nf">new</span><span class="p">(</span><span class="n">wechat_pay_public_key</span><span class="p">).</span><span class="nf">verify</span><span class="p">([</span><span class="n">wechat_timestamp</span><span class="p">,</span> <span class="n">wechat_pay_nonce</span><span class="p">,</span> <span class="n">body</span><span class="p">],</span> <span class="n">wechat_pay_signature</span><span class="p">)</span>
<span class="k">return</span> <span class="n">render</span> <span class="ss">json: </span><span class="o">......</span><span class="p">,</span> <span class="ss">status: </span><span class="mi">400</span>
<span class="k">end</span>
<span class="c1"># 3. 解密请求主体中的订单信息并验证</span>
<span class="n">nonce</span> <span class="o">=</span> <span class="n">params</span><span class="p">[</span><span class="s2">"resource"</span><span class="p">][</span><span class="s2">"nonce"</span><span class="p">]</span>
<span class="n">aad</span> <span class="o">=</span> <span class="n">params</span><span class="p">[</span><span class="s2">"resource"</span><span class="p">][</span><span class="s2">"associated_data"</span><span class="p">]</span>
<span class="n">cipher_text</span> <span class="o">=</span> <span class="n">params</span><span class="p">[</span><span class="s2">"resource"</span><span class="p">][</span><span class="s2">"ciphertext"</span><span class="p">]</span>
<span class="n">decode_str</span> <span class="o">=</span> <span class="no">WeChat</span><span class="o">::</span><span class="no">Util</span><span class="o">::</span><span class="no">Certificate</span><span class="p">.</span><span class="nf">send</span><span class="p">(</span><span class="ss">:aead_decode</span><span class="p">,</span> <span class="no">WeChat</span><span class="o">::</span><span class="no">API_V3_SECRET</span><span class="p">,</span> <span class="n">nonce</span><span class="p">,</span> <span class="n">aad</span><span class="p">,</span> <span class="no">Base64</span><span class="p">.</span><span class="nf">decode64</span><span class="p">(</span><span class="n">cipher_text</span><span class="p">))</span>
<span class="n">res</span> <span class="o">=</span> <span class="no">JSON</span><span class="p">.</span><span class="nf">parse</span><span class="p">(</span><span class="n">decode_str</span><span class="p">)</span>
<span class="n">order</span> <span class="o">=</span> <span class="no">Order</span><span class="p">.</span><span class="nf">find_by_out_trade_no</span><span class="p">(</span><span class="n">res</span><span class="p">[</span><span class="s2">"out_trade_no"</span><span class="p">])</span>
<span class="k">unless</span> <span class="n">order</span><span class="p">.</span><span class="nf">total_fee</span> <span class="o">==</span> <span class="n">res</span><span class="p">[</span><span class="s2">"amount"</span><span class="p">][</span><span class="s2">"total"</span><span class="p">]</span>
<span class="k">return</span> <span class="n">render</span> <span class="ss">json: </span><span class="o">...</span><span class="p">,</span> <span class="ss">status: </span><span class="mi">400</span>
<span class="k">end</span>
<span class="k">unless</span> <span class="no">WeChat</span><span class="o">::</span><span class="no">ServiceV3</span><span class="o">::</span><span class="no">AppID</span> <span class="o">==</span> <span class="n">res</span><span class="p">[</span><span class="s1">'appid'</span><span class="p">]</span>
<span class="k">return</span> <span class="n">render</span> <span class="ss">json: </span><span class="o">...</span><span class="p">,</span> <span class="ss">status: </span><span class="mi">400</span>
<span class="k">end</span>
<span class="k">unless</span> <span class="no">WeChat</span><span class="o">::</span><span class="no">ServiceV3</span><span class="o">::</span><span class="no">MchID</span> <span class="o">==</span> <span class="n">res</span><span class="p">[</span><span class="s1">'mchid'</span><span class="p">]</span>
<span class="k">return</span> <span class="n">render</span> <span class="ss">json: </span><span class="o">...</span><span class="p">,</span> <span class="ss">status: </span><span class="mi">400</span>
<span class="k">end</span>
<span class="c1"># 发货处理</span>
<span class="o">...</span>
<span class="n">render</span> <span class="ss">json: </span><span class="p">{</span><span class="s1">'code'</span><span class="p">:</span> <span class="s2">"SUCCESS"</span><span class="p">,</span> <span class="s1">'message'</span><span class="p">:</span> <span class="s1">''</span><span class="p">}</span>
<span class="k">rescue</span> <span class="no">ActiveRecord</span><span class="o">::</span><span class="no">StaleObjectError</span>
<span class="k">return</span> <span class="n">render</span> <span class="ss">json: </span><span class="o">...</span><span class="p">,</span> <span class="ss">status: </span><span class="mi">400</span>
<span class="k">rescue</span> <span class="no">WeChat</span><span class="o">::</span><span class="no">Util</span><span class="o">::</span><span class="no">CertificateNotFound</span>
<span class="k">return</span> <span class="n">render</span> <span class="ss">json: </span><span class="o">...</span><span class="p">,</span> <span class="ss">status: </span><span class="mi">400</span>
<span class="k">rescue</span> <span class="no">StandardError</span> <span class="o">=></span> <span class="n">e</span>
<span class="n">logger</span><span class="p">.</span><span class="nf">error</span> <span class="s2">"</span><span class="si">#{</span><span class="n">e</span><span class="p">.</span><span class="nf">full_message</span><span class="si">}</span><span class="s2">"</span>
<span class="n">render</span> <span class="ss">json: </span><span class="o">...</span><span class="p">,</span> <span class="ss">status: </span><span class="mi">500</span>
<span class="k">end</span>
<span class="k">end</span>
</code></pre>
<p>几乎就是纯面向过程编程……代码中有些诸如 <code>xx.send</code> 的东西大家就当没看见好了_(:з」∠)_(蒙混过关)……</p>
<h1>
<a id="6.+%E7%BB%93%E8%AF%AD" href="#6.+%E7%BB%93%E8%AF%AD" class="anchor"></a>6. 结语</h1>
<p>至此我们就完成了基础的微信支付中所涉及到的大部分问题,个人感觉微信支付升级到V3之后确实要简单了不少。<del>即便如此每次接入一个新的云服务的时候都感觉是被按在地上补补各种加密……</del></p>
<p>2021/7/6:目前已经有dalao写了新的轮子了……
<a href="https://ruby-china.org/topics/41254">微信支付 RubyGem 发布 - 适用于微信支付 V3 接口</a></p>
0. 写在前面
其实公司项目早在去年就做完第一版的微信支付了,但是由于种种原因并没有上线,一直拖到了今年。
让人 *疼 的一件事情是,虽然基础功能都已经实现了,代码也有了,但是考虑微信支付更新...
鱼翅
https://geeknote.net/yuchiXiong