<?xml version="1.0" encoding="UTF-8" ?>
  <rss
    version="2.0"
    xmlns:content="http://purl.org/rss/1.0/modules/content/"
    xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>编码与禅</title>
        <link>https://elliot00.com</link>
        <description>Elliot's blog feed</description>
        <atom:link href="https://elliot00.com/rss.xml" rel="self" type="application/rss+xml" />
        <language>zh</language>
        <lastBuildDate>Wed, 04 Mar 2026 04:46:10 GMT</lastBuildDate>
        <follow_challenge>
            <feedId>67437090448621568</feedId>
            <userId>67386573774055424</userId>
        </follow_challenge>
        <item>
          <title>接雨水</title>
          <link>https://elliot00.com/posts/trapping-rain-water</link>
          <description>这篇文章介绍了三种计算接雨水问题的方法：单调栈解法通过寻找“凹”形区域并以水平切片方式计算水量，前后缀分解通过预先计算每个位置左右两侧的最大柱子高度来确定垂直切片的水量，而相向双指针法则在优化空间复杂度的同时，通过双指针相向遍历实时更新最大高度并累加水量。</description>
          <pubDate>Sun, 01 Mar 2026 08:06:41 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <h2 id="user-content-单调栈解法" class="">单调栈解法<a class="" tabindex="-1" href="#单调栈解法">#</a></h2><p>延续<a href="/posts/monotonic-stack">单调栈</a>的思路，运用空间想象力，要想接住雨水，必须要找到一个「凹」形区域。如果从左向右遍历，先是遍历到一些从高到低的柱子，直到遇到一个比当前最低的柱子高的柱子，并且栈里还有柱子（要注意至少有三根柱子，数字0也看作高度0的柱子），就意味着「凹」形区域形成了。
</p><p>首先用一个单调栈维护递减的柱子高度。如图所示，当找到这样一个可以接水的区域时，可以以水平切片的方式，逐步求出当前柱子到栈顶柱子间水平切片可以装的水量。
</p><p><img alt="单调栈示意图" src="https://r2.elliot00.com/algorithm/trap-mono-stack.png" width="1224" height="862"></p><p>每一个水平切片都是一个矩形，面积是宽高相乘，宽度是当前柱子索引减去左侧柱子索引再加1；高度根据木桶效应，应当取当前柱子和左侧柱子高度较小的那个，同时不能忘记，要减去已经算过的上一层高度。
</p><p>汇总一下信息：
</p><ol><li>单调栈维护​<strong>高度递减</strong>​的柱子（为了方便计算，可以保存索引）
</li><li>遇到比栈顶高的柱子，出栈，根据前面提到的水平切片计算方法，更新答案
</li><li>直到​<strong>栈空</strong>​或​<strong>栈里只有一根柱子</strong>​或当前柱子​<strong>比栈顶矮</strong>​，继续遍历下一个柱子
</li></ol><p>代码：
</p><pre tabindex="0"><code><span><span>(</span><span>define</span><span> (</span><span>trap</span><span> height</span><span>)</span></span>
<span><span>  (</span><span>let</span><span> loop ((height height)</span></span>
<span><span>             (i </span><span>0</span><span>)</span></span>
<span><span>             ;; 保存索引和高度</span></span>
<span><span>             (stack </span><span>'()</span><span>)</span></span>
<span><span>             (ans </span><span>0</span><span>))</span></span>
<span><span>    (</span><span>if</span><span> (</span><span>null?</span><span> height)</span></span>
<span><span>        ans</span></span>
<span><span>        (</span><span>let</span><span> ((h (</span><span>car</span><span> height)))</span></span>
<span><span>          (</span><span>cond</span><span> ((</span><span>null?</span><span> stack) </span><span>; 栈空，入栈</span></span>
<span><span>                 (loop (</span><span>cdr</span><span> height) (1+ i) (</span><span>cons</span><span> (</span><span>cons</span><span> i h) stack) ans))</span></span>
<span><span>                ((</span><span>&#x3C;</span><span> h (</span><span>cdr</span><span> (</span><span>car</span><span> stack))) </span><span>; 更矮的柱子，入栈</span></span>
<span><span>                 (loop (</span><span>cdr</span><span> height) (1+ i) (</span><span>cons</span><span> (</span><span>cons</span><span> i h) stack) ans))</span></span>
<span><span>                (</span><span>else</span></span>
<span><span>                 (</span><span>let</span><span> ((pre-h (</span><span>cdr</span><span> (</span><span>car</span><span> stack))) </span><span>; 上一层的高</span></span>
<span><span>                       (pop-stack (</span><span>cdr</span><span> stack))) </span><span>; 栈顶出栈后剩下的栈</span></span>
<span><span>                   (</span><span>if</span><span> (</span><span>null?</span><span> pop-stack)</span></span>
<span><span>                       ;; 柱子不够形成凹形区域，继续向后找</span></span>
<span><span>                       (loop (</span><span>cdr</span><span> height) (1+ i) (</span><span>cons</span><span> (</span><span>cons</span><span> i h) pop-stack) ans)</span></span>
<span><span>                       (</span><span>let*</span><span> ((left (</span><span>car</span><span> pop-stack))</span></span>
<span><span>                              (left-i (</span><span>car</span><span> left))</span></span>
<span><span>                              (left-h (</span><span>cdr</span><span> left))</span></span>
<span><span>                              (width (</span><span>-</span><span> i left-i </span><span>1</span><span>))</span></span>
<span><span>                              (height-diff (</span><span>-</span><span> (</span><span>min</span><span> left-h h) pre-h)))</span></span>
<span><span>                         (loop height</span></span>
<span><span>                               i</span></span>
<span><span>                               pop-stack</span></span>
<span><span>                               (</span><span>+</span><span> ans (</span><span>*</span><span> width height-diff))))))))))))</span></span></code></pre><h2 id="user-content-前后缀分解" class="">前后缀分解<a class="" tabindex="-1" href="#前后缀分解">#</a></h2><p><img alt="前后缀分解示意图" src="https://r2.elliot00.com/algorithm/trap-pre-suf.png" width="1362" height="928"></p><p>也可以竖着去切片，每次算竖着的一格可以装多少水，那么每个矩形区域的宽度就固定了，但是怎么知道高度呢？
</p><p>以图中第三个矩形为例，作为一个大的可以接水的区域的一部分，这整个区域的水面高度肯定是取决于整个区域两端柱子中较矮的那个。从第3个矩形的位置向左右两边看，水面高度应该取决于向左能看到的最高柱子以向右能看到的最高柱子中较矮的那个，在这个例子中是最后一个柱子。
</p><p>如果能知道每一个位置上，向左看最高的柱子以向右看最高的柱子，不就可以据此算出当前位置能接的水了吗？
</p><p>写两个函数，根据输入的列表，一次性算出每个位置前向和后向的最高柱子：
</p><pre tabindex="0"><code><span><span>(</span><span>define</span><span> (</span><span>prefix-max</span><span> lst</span><span>)</span></span>
<span><span>  (</span><span>let</span><span> loop ((lst lst)</span></span>
<span><span>             (max-val -1)</span></span>
<span><span>             (result </span><span>'()</span><span>))</span></span>
<span><span>    (</span><span>if</span><span> (</span><span>null?</span><span> lst)</span></span>
<span><span>        (</span><span>reverse</span><span> result)</span></span>
<span><span>        (</span><span>let</span><span> ((new-max (</span><span>max</span><span> max-val (</span><span>car</span><span> lst))))</span></span>
<span><span>          (loop (</span><span>cdr</span><span> lst) new-max (</span><span>cons</span><span> new-max result))))))</span></span>
<span></span>
<span><span>(</span><span>define</span><span> (</span><span>suffix-max</span><span> lst</span><span>)</span></span>
<span><span>  (</span><span>let</span><span> loop ((lst (</span><span>reverse</span><span> lst))</span></span>
<span><span>             (max-val -1)</span></span>
<span><span>             (result </span><span>'()</span><span>))</span></span>
<span><span>    (</span><span>if</span><span> (</span><span>null?</span><span> lst)</span></span>
<span><span>        result</span></span>
<span><span>        (</span><span>let</span><span> ((new-max (</span><span>max</span><span> max-val (</span><span>car</span><span> lst))))</span></span>
<span><span>          (loop (</span><span>cdr</span><span> lst) new-max (</span><span>cons</span><span> new-max result))))))</span></span></code></pre><p>剩下要做的就是同时遍历三个列表，求出每个位置的水量再加总就可以了：
</p><pre tabindex="0"><code><span><span>(</span><span>define</span><span> (</span><span>trap</span><span> height</span><span>)</span></span>
<span><span>  (</span><span>let</span><span> loop ((pre-max (prefix-max height))</span></span>
<span><span>             (suf-max (suffix-max height))</span></span>
<span><span>             (height height)</span></span>
<span><span>             (ans </span><span>0</span><span>))</span></span>
<span><span>    (</span><span>if</span><span> (</span><span>null?</span><span> height)</span></span>
<span><span>        ans</span></span>
<span><span>        (</span><span>let</span><span> ((p (</span><span>car</span><span> pre-max))</span></span>
<span><span>              (s (</span><span>car</span><span> suf-max))</span></span>
<span><span>              (h (</span><span>car</span><span> height)))</span></span>
<span><span>          (loop (</span><span>cdr</span><span> pre-max)</span></span>
<span><span>                (</span><span>cdr</span><span> suf-max)</span></span>
<span><span>                (</span><span>cdr</span><span> height)</span></span>
<span><span>                (</span><span>+</span><span> ans (</span><span>-</span><span> (</span><span>min</span><span> p s) h)))))))</span></span></code></pre><h2 id="user-content-相向双指针" class="">相向双指针<a class="" tabindex="-1" href="#相向双指针">#</a></h2><p>注意到上一个方法用了两个列表存前缀最大和后缀最大，空间复杂度是​<code class="">O(n)</code>​，能否优化？
</p><p>可以维护两个指针，从柱子的头尾相向遍历，总是移动较矮的那一端，移动的同时记录当前最大并更新答案。
</p><pre tabindex="0"><code><span><span>(</span><span>define</span><span> (</span><span>trap</span><span> height</span><span>)</span></span>
<span><span>  (</span><span>let</span><span> ((n (</span><span>vector-length</span><span> height)))</span></span>
<span><span>    (</span><span>let</span><span> loop ((left </span><span>0</span><span>)</span></span>
<span><span>               (right (</span><span>-</span><span> n </span><span>1</span><span>))</span></span>
<span><span>               (pre-max </span><span>0</span><span>)</span></span>
<span><span>               (suf-max </span><span>0</span><span>)</span></span>
<span><span>               (ans </span><span>0</span><span>))</span></span>
<span><span>      (</span><span>if</span><span> (</span><span>&#x3C;</span><span> left right)</span></span>
<span><span>          (</span><span>let</span><span> ((new-pre-max (</span><span>max</span><span> pre-max (</span><span>vector-ref</span><span> height left)))</span></span>
<span><span>                (new-suf-max (</span><span>max</span><span> suf-max (</span><span>vector-ref</span><span> height right))))</span></span>
<span><span>            ;; 哪边矮动哪边，动一格，算一格</span></span>
<span><span>            (</span><span>if</span><span> (</span><span>&#x3C;</span><span> new-pre-max new-suf-max)</span></span>
<span><span>                (loop (</span><span>+</span><span> left </span><span>1</span><span>)</span></span>
<span><span>                      right</span></span>
<span><span>                      new-pre-max</span></span>
<span><span>                      new-suf-max</span></span>
<span><span>                      ;; 如果当前柱子就是最高，就是加上0，等于没有更新答案</span></span>
<span><span>                      ;; 否则一减就是水的高度，宽度是1，不用乘</span></span>
<span><span>                      (</span><span>+</span><span> ans (</span><span>-</span><span> new-pre-max (</span><span>vector-ref</span><span> height left))))</span></span>
<span><span>                (loop left</span></span>
<span><span>                      (</span><span>-</span><span> right </span><span>1</span><span>)</span></span>
<span><span>                      new-pre-max</span></span>
<span><span>                      new-suf-max</span></span>
<span><span>                      (</span><span>+</span><span> ans (</span><span>-</span><span> new-suf-max (</span><span>vector-ref</span><span> height right))))))</span></span>
<span><span>          ans))))</span></span></code></pre>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>单调栈</title>
          <link>https://elliot00.com/posts/monotonic-stack</link>
          <description>单调栈通过维护温度值的单调性，将寻找“下一个更高温度”的时间复杂度从 O(n²) 优化至 O(n)，其核心在于避免了重复比较并确保每个元素只入栈出栈一次。</description>
          <pubDate>Sun, 28 Dec 2025 07:52:43 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <p>参考题目：<a href="https://leetcode.cn/problems/daily-temperatures/description/">739. 每日温度</a></p><h2 id="user-content-暴力法" class="">暴力法<a class="" tabindex="-1" href="#暴力法">#</a></h2><p>使用暴力方法做这题很简单，首先拆解出一个子问题：给你一个索引i和一个数组a，找出数组中i之后第一个大于a[i]的数的索引j，没有就返回0。
</p><pre tabindex="0"><code><span><span>(</span><span>define</span><span> (</span><span>find-first-larger</span><span> i a</span><span>)</span></span>
<span><span>  (</span><span>let</span><span> loop ((j (</span><span>+</span><span> i </span><span>1</span><span>)))</span></span>
<span><span>    (</span><span>cond</span></span>
<span><span>     ;; 到最后还没有找到，返回0</span></span>
<span><span>     ((</span><span>=</span><span> j (</span><span>vector-length</span><span> a)) </span><span>0</span><span>)</span></span>
<span><span>     ;; a[j] > a[i]，返回j</span></span>
<span><span>     ((</span><span>></span><span> (</span><span>vector-ref</span><span> a j) (</span><span>vector-ref</span><span> a i)) j)</span></span>
<span><span>     ;; 否则继续往后找</span></span>
<span><span>     (</span><span>else</span><span> (loop (</span><span>+</span><span> j </span><span>1</span><span>))))))</span></span></code></pre><p>剩下的事情就是遍历原数组，对每一个索引i调用​<code class="">find-first-larger</code>​，组合得到新的数组就是答案了。
</p><p>暴力做法的时间复杂度是​<code class="">O(n^2)</code>​，有没有方法在线性时间解决问题呢？
</p><h2 id="user-content-单调栈" class="">单调栈<a class="" tabindex="-1" href="#单调栈">#</a></h2><p>可以先尝试用动态规划的思路，假设当前在原数组任意位置，有哪些可能的状态？
</p><ol><li>当前数前面的数都已经找到了答案
</li><li>当前数前面还有数没有找到答案
</li></ol><p>首先分析情况一，如果说刚刚遍历到当前数，还没有做任何操作，而前方没有任何未找到答案的数，那只能说明当前数是第一个数。
</p><p>所以情况二才是需要重点分析的，如果前方有数没有找到答案，这些数会有什么性质？
</p><p>如果在遍历过程中将没找到答案的数按顺序记录下来，这些数有可能是​[2, 4, 3]吗？​<strong>不可能</strong>​，为什么呢？因为题目指明了要找每一个数的下一个更大的数，如果2后面有4的话，2不可能被归类在没有答案的部分。也就是说如果按从左到右的顺序记录当前还没找到答案的数，这些数一定是​<strong>非递增</strong>​的。
</p><p>那么如果当前数x小于等于这个记录中最右边的数y，也就意味着当前数x不可能是y的答案，更加不可能是记录中其它数的答案。至此在当前位置除了将当前数也加入未找到答案的记录中就无事可做了。
</p><p>反之如果当前数x大于y，那么可以说y的答案就是当前数x，可以将y从未找到答案的记录中删去，但是还不知道当前数是否有可能仍然是剩下的记录中的数的答案，所以还要重复这个比较过程，直接剩下记录最右边也就是记录中最小的数都大于等于当前数x。
</p><p>总结描述一下过程，设当前数为x，未找到答案的数的记录称为pending：
</p><ol><li>若pending为空，将x存入pending，遍历下一个数
</li><li>pending不为空，将最新存入pending的数取出，设为y，与x比较：
<ul><li>若x > y，则，从pending中删去y，y的答案记为x，重复步骤1、2
</li><li>否则，将x存入pending，遍历下一个数
</li></ul></li></ol><p>可见这个pending需要用一个​<strong>后入先出</strong>​的数据结构，那么最合适的就是栈了。又由于之前已经证明，pending内的元素具有单调性，所以这种结构被称为单调栈。
</p><p>注意到这里有一个内层循环，但是时间复杂度并不是​<code class="">O(n^2)</code>​，即使考虑极端情况​<code class="">[100, 99, 98, 97, 96]</code>​，也顶多每个元素入栈出栈一次，最终时间复杂度还是​<code class="">O(n)</code>​的。
</p><p>另外题目要求的是几天后会出现一个更高温度，pending可以用来存索引，更新答案是用当前索引减去栈顶存储的索引就可以了。
</p><pre tabindex="0"><code><span><span>(</span><span>define</span><span> (</span><span>daily-temperatures</span><span> temperatures</span><span>)</span></span>
<span><span>  (</span><span>let*</span><span> ((n (</span><span>vector-length</span><span> temperatures))</span></span>
<span><span>         (ans (make-vector n </span><span>0</span><span>)))</span></span>
<span><span>    (</span><span>let</span><span> loop ((i </span><span>0</span><span>)</span></span>
<span><span>               ;; 用链表做栈</span></span>
<span><span>               (pending </span><span>'()</span><span>))</span></span>
<span><span>      (</span><span>if</span><span> (</span><span>&#x3C;</span><span> i n)</span></span>
<span><span>          (</span><span>if</span><span> (</span><span>null?</span><span> pending)</span></span>
<span><span>              ;; pending为空，进入下一步</span></span>
<span><span>              (loop (</span><span>+</span><span> i </span><span>1</span><span>)</span></span>
<span><span>                    ;; 为了更新答案方便，pending实际存的是索引</span></span>
<span><span>                    (</span><span>cons</span><span> i pending))</span></span>
<span><span>              (</span><span>let*</span><span> ((x (</span><span>vector-ref</span><span> temperatures i))</span></span>
<span><span>                     (j (</span><span>car</span><span> pending))</span></span>
<span><span>                     (y (</span><span>vector-ref</span><span> temperatures j)))</span></span>
<span><span>                (</span><span>if</span><span> (</span><span>></span><span> x y)</span></span>
<span><span>                    (</span><span>begin</span></span>
<span><span>                      (</span><span>vector-set!</span><span> ans j (</span><span>-</span><span> i j))</span></span>
<span><span>                      (loop i (</span><span>cdr</span><span> pending)))</span></span>
<span><span>                    (loop (</span><span>+</span><span> i </span><span>1</span><span>)</span></span>
<span><span>                          (</span><span>cons</span><span> i pending)))))</span></span>
<span><span>          ans))))</span></span></code></pre><p>还有一种倒着遍历的思路，代码写出来似乎没有从左到右遍历易懂，但是描述却非常形象：
</p><p>将题目给的数组想象成连续的山峰的高度，如​<code class="">[2, 4, 5, 1, 2, 6]</code>​，倒着遍历，将遍历过程想象成爬山，假设当前爬到高度为4的山峰，那么​<code class="">[5, 1, 2, 6]</code>​是已经爬过的，现在你人站在山上向后看，是不是只能看到高度为5和6的两座山峰呢？因为比5矮的两座被挡住了嘛。
</p><p>说回题目，也就是说处在4的位置上，只有6和5才有资格继续做可能的答案。
</p><p>推广一下，如果用pending记录有可能作为前面的数字的答案的数，pending一定是​<strong>非递减</strong>​的。
</p><p>具体地说，倒着遍历数组，设当前数x，用pending记录可能作为前面数字答案的数：
</p><ol><li>pending为空，将当前数推入pending
</li><li>pending不为空，比较x和于pending栈顶的数y
<ul><li>x大于等于y，y就不可能是前面某个数的答案，将y弹出，重复步骤1、2
</li><li>否则，y就是x的答案，更新答案，x还有可能是前面某个数的答案，x推入pending
</li></ul></li></ol><pre tabindex="0"><code><span><span>(</span><span>define</span><span> (</span><span>daily-temperatures</span><span> temperatures</span><span>)</span></span>
<span><span>  (</span><span>let*</span><span> ((n (</span><span>vector-length</span><span> temperatures))</span></span>
<span><span>         (ans (make-vector n </span><span>0</span><span>)))</span></span>
<span><span>    (</span><span>let</span><span> loop ((i (</span><span>-</span><span> n </span><span>1</span><span>))</span></span>
<span><span>               (pending </span><span>'()</span><span>))</span></span>
<span><span>      (</span><span>if</span><span> (</span><span>></span><span> i -1)</span></span>
<span><span>          (</span><span>if</span><span> (</span><span>null?</span><span> pending)</span></span>
<span><span>              (loop (</span><span>-</span><span> i </span><span>1</span><span>)</span></span>
<span><span>                    (</span><span>cons</span><span> i pending))</span></span>
<span><span>              (</span><span>let*</span><span> ((x (</span><span>vector-ref</span><span> temperatures i))</span></span>
<span><span>                     (j (</span><span>car</span><span> pending))</span></span>
<span><span>                     (y (</span><span>vector-ref</span><span> temperatures j)))</span></span>
<span><span>                (</span><span>if</span><span> (</span><span>>=</span><span> x y)</span></span>
<span><span>                    (loop i</span></span>
<span><span>                          (</span><span>cdr</span><span> pending))</span></span>
<span><span>                    (</span><span>begin</span></span>
<span><span>                      (</span><span>vector-set!</span><span> ans i (</span><span>-</span><span> j i))</span></span>
<span><span>                      (loop (</span><span>-</span><span> i </span><span>1</span><span>)</span></span>
<span><span>                            (</span><span>cons</span><span> i pending))))))</span></span>
<span><span>          ans))))</span></span></code></pre><h2 id="user-content-总结" class="">总结<a class="" tabindex="-1" href="#总结">#</a></h2><p>和暴力做法相比，单调栈做法是充分利用了单调性来节省多余的计算。不论单调栈pending存储的是待找到答案的部分（从左到右遍历），还是可能作为答案的部分（从右到左遍历）。用暴力法，如果给你待找到答案的部分，你可能会逐个与当前数作比较，但是只要能想到利用单调性，如未找到答案部分一定是非递增的，那么只用将栈顶元素也就是最小的元素与当前数作比较了。
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>相向双指针（二）</title>
          <link>https://elliot00.com/posts/opposite-direction-two-points-2</link>
          <description>本文系统讲解了相向双指针算法的基本原理和实际应用。相向双指针通过在数组或字符串的两端设置指针，让它们向中间移动来解决问题，这种模式特别适合处理对称性、反转和双向遍历的场景。</description>
          <pubDate>Sat, 06 Dec 2025 11:35:10 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <h2 id="user-content-167-两数之和-ii---输入有序数组" class="">167. 两数之和 II - 输入有序数组<a class="" tabindex="-1" href="#167-两数之和-ii---输入有序数组">#</a></h2><p>因为数组是有序的，如果从数组两头向中间遍历，两端数之和大于目标，说明要找一个更小的数，需要左移右指针；如果两端数之和小于目标，说明要找一个更大的数，需要右移左指针：
</p><pre tabindex="0"><code><span><span>(</span><span>define</span><span> (</span><span>two-sum</span><span> numbers target</span><span>)</span></span>
<span><span>  (</span><span>let</span><span> loop ((left </span><span>0</span><span>)</span></span>
<span><span>             (right (</span><span>-</span><span> (</span><span>vector-length</span><span> numbers) </span><span>1</span><span>)))</span></span>
<span><span>    (</span><span>if</span><span> (</span><span>&#x3C;</span><span> left right)</span></span>
<span><span>        (</span><span>let</span><span> ((sum (</span><span>+</span><span> (</span><span>vector-ref</span><span> numbers left)</span></span>
<span><span>                      (</span><span>vector-ref</span><span> numbers right))))</span></span>
<span><span>          (</span><span>cond</span></span>
<span><span>           ((</span><span>></span><span> sum target) (loop left (</span><span>-</span><span> right </span><span>1</span><span>)))</span></span>
<span><span>           ((</span><span>&#x3C;</span><span> sum target) (loop (</span><span>+</span><span> left </span><span>1</span><span>) right))</span></span>
<span><span>           (</span><span>else</span><span> (</span><span>list</span><span> left right))))</span></span>
<span><span>        (</span><span>list</span><span> -1 -1))))</span></span></code></pre><p>不过题目有个小陷阱，特别提到数组下标从1开始，只要最后返回的地方两个指针都加一就可以了。
</p><h2 id="user-content-633-平方数之和" class="">633. 平方数之和<a class="" tabindex="-1" href="#633-平方数之和">#</a></h2><p>既然是找平方的和等于c的两个数，说明要找的两个数一定比c小，准确地说是要不超过根号c（向下取整），设为k。题目说明了是整数里找，整数有无限个，虽然确定了上限k，下限怎么定呢？注意-a的平方和a的平方是相同的，所以可以把这一题转换为在元素为0到k的整数数组中，找两个数平方和等于c，只是计数时不能忽略c刚好为-a的平方加a的平方的情况，循环条件应为​<code class="">left &#x3C;= right</code>​。
</p><pre tabindex="0"><code><span><span>(</span><span>define</span><span> (</span><span>judge-square-sum</span><span> c</span><span>)</span></span>
<span><span>  (</span><span>let</span><span> loop ((left </span><span>0</span><span>)</span></span>
<span><span>             (right (</span><span>inexact->exact</span><span> (</span><span>floor</span><span> (</span><span>sqrt</span><span> c)))))</span></span>
<span><span>    (</span><span>if</span><span> (</span><span>&#x3C;=</span><span> left right)</span></span>
<span><span>        (</span><span>let</span><span> ((sum (</span><span>+</span><span> (</span><span>*</span><span> left left)</span></span>
<span><span>                      (</span><span>*</span><span> right right))))</span></span>
<span><span>          (</span><span>cond</span></span>
<span><span>           ((</span><span>></span><span> sum c) (loop left (</span><span>-</span><span> right </span><span>1</span><span>)))</span></span>
<span><span>           ((</span><span>&#x3C;</span><span> sum c) (loop (</span><span>+</span><span> left </span><span>1</span><span>) right))</span></span>
<span><span>           (</span><span>else</span><span> #t</span><span>)))</span></span>
<span><span>        #f</span><span>)))</span></span></code></pre><h2 id="user-content-2824-统计和小于目标的下标对数目" class="">2824. 统计和小于目标的下标对数目<a class="" tabindex="-1" href="#2824-统计和小于目标的下标对数目">#</a></h2><p>题目中的​<code class="">i &#x3C; j</code>​条件容易引起误解，实际只是在说指针不能在同一个位置。可以排序再应用求两数和的方法，当左右指针下值相加小于target，说明当前左指针下数加​<code class="">[left + 1, right]</code>​指针下任一数（右指针下一定是区间内最大的），都小于target，也就是有​<code class="">right - left</code>​个下标对符合条件。
</p><pre tabindex="0"><code><span><span>(</span><span>define</span><span> (</span><span>count-pairs</span><span> nums target</span><span>)</span></span>
<span><span>  (</span><span>let</span><span> ((sorted (sort nums &#x3C;)))</span></span>
<span><span>    (</span><span>let</span><span> loop ((left </span><span>0</span><span>)</span></span>
<span><span>               (right (</span><span>-</span><span> (</span><span>vector-length</span><span> sorted) </span><span>1</span><span>))</span></span>
<span><span>               (ans </span><span>0</span><span>))</span></span>
<span><span>      (</span><span>if</span><span> (</span><span>&#x3C;</span><span> left right)</span></span>
<span><span>          (</span><span>let</span><span> ((sum (</span><span>+</span><span> (</span><span>vector-ref</span><span> sorted left)</span></span>
<span><span>                        (</span><span>vector-ref</span><span> sorted right))))</span></span>
<span><span>            (</span><span>if</span><span> (</span><span>&#x3C;</span><span> sum target)</span></span>
<span><span>                (loop (</span><span>+</span><span> left </span><span>1</span><span>)</span></span>
<span><span>                      right</span></span>
<span><span>                      (</span><span>+</span><span> ans (</span><span>-</span><span> right left)))</span></span>
<span><span>                (loop left</span></span>
<span><span>                      (</span><span>-</span><span> right </span><span>1</span><span>)</span></span>
<span><span>                      ans)))</span></span>
<span><span>          ans))))</span></span></code></pre><h2 id="user-content-15-三数之和" class="">15. 三数之和<a class="" tabindex="-1" href="#15-三数之和">#</a></h2><p>需要找x,y,z和等於0，可以排序数组，遍历数组，以当前数为x，题目就变成了找y + z = -x的双指针问题了。
</p><p>先試試在有序的數組中，找y和z：
</p><pre tabindex="0"><code><span><span>(</span><span>define</span><span> (</span><span>find-y-z</span><span> nums x</span><span>)</span></span>
<span><span>  (</span><span>let</span><span> loop ((left </span><span>0</span><span>)</span></span>
<span><span>             (right (</span><span>-</span><span> (</span><span>vector-length</span><span> nums) </span><span>1</span><span>))</span></span>
<span><span>             (ans </span><span>'()</span><span>))</span></span>
<span><span>    (</span><span>if</span><span> (</span><span>&#x3C;</span><span> left right)</span></span>
<span><span>        (</span><span>let*</span><span> ((y (</span><span>vector-ref</span><span> nums left))</span></span>
<span><span>               (z (</span><span>vector-ref</span><span> nums right))</span></span>
<span><span>               (sum (</span><span>+</span><span> y z))</span></span>
<span><span>               (neg-x (</span><span>-</span><span> x)))</span></span>
<span><span>          (</span><span>cond</span></span>
<span><span>           ((</span><span>></span><span> sum neg-x) (loop left (</span><span>-</span><span> right </span><span>1</span><span>) ans))</span></span>
<span><span>           ((</span><span>&#x3C;</span><span> sum neg-x) (loop (</span><span>+</span><span> left </span><span>1</span><span>) right ans))</span></span>
<span><span>           (</span><span>else</span><span> (loop (</span><span>+</span><span> left </span><span>1</span><span>)</span></span>
<span><span>                       (</span><span>-</span><span> right </span><span>1</span><span>)</span></span>
<span><span>                       (</span><span>cons</span><span> (</span><span>list</span><span> y z) ans)))))</span></span>
<span><span>        ans)))</span></span></code></pre><p>將返回值改造下，返回xyz：
</p><pre tabindex="0"><code><span><span>(</span><span>define</span><span> (</span><span>find-x-y-z</span><span> nums x</span><span>)</span></span>
<span><span>  (</span><span>let</span><span> loop ((left </span><span>0</span><span>)</span></span>
<span><span>             (right (</span><span>-</span><span> (</span><span>vector-length</span><span> nums) </span><span>1</span><span>))</span></span>
<span><span>             (ans </span><span>'()</span><span>))</span></span>
<span><span>    (</span><span>if</span><span> (</span><span>&#x3C;</span><span> left right)</span></span>
<span><span>        (</span><span>let*</span><span> ((y (</span><span>vector-ref</span><span> nums left))</span></span>
<span><span>               (z (</span><span>vector-ref</span><span> nums right))</span></span>
<span><span>               (sum (</span><span>+</span><span> y z))</span></span>
<span><span>               (neg-x (</span><span>-</span><span> x)))</span></span>
<span><span>          (</span><span>cond</span></span>
<span><span>           ((</span><span>></span><span> sum neg-x) (loop left (</span><span>-</span><span> right </span><span>1</span><span>) ans))</span></span>
<span><span>           ((</span><span>&#x3C;</span><span> sum neg-x) (loop (</span><span>+</span><span> left </span><span>1</span><span>) right ans))</span></span>
<span><span>           (</span><span>else</span><span> (loop (</span><span>+</span><span> left </span><span>1</span><span>)</span></span>
<span><span>                       (</span><span>-</span><span> right </span><span>1</span><span>)</span></span>
<span><span>                       (</span><span>cons</span><span> (</span><span>list</span><span> x y z) ans)))))</span></span>
<span><span>        ans)))</span></span></code></pre><p>但是還要處理去重，如果遍歷nums，應該要跳過重複的x，雙指針的左端點也不應該每次都從0開始了
</p><pre tabindex="0"><code><span><span>(</span><span>define</span><span> (</span><span>find-x-y-z</span><span> nums x start</span><span>)</span></span>
<span><span>  (</span><span>let</span><span> loop ((left start)</span></span>
<span><span>             (right (</span><span>-</span><span> (</span><span>vector-length</span><span> nums) </span><span>1</span><span>))</span></span>
<span><span>             (ans </span><span>'()</span><span>))</span></span>
<span><span>    (</span><span>if</span><span> (</span><span>&#x3C;</span><span> left right)</span></span>
<span><span>        (</span><span>let*</span><span> ((y (</span><span>vector-ref</span><span> nums left))</span></span>
<span><span>               (z (</span><span>vector-ref</span><span> nums right))</span></span>
<span><span>               (sum (</span><span>+</span><span> y z))</span></span>
<span><span>               (neg-x (</span><span>-</span><span> x)))</span></span>
<span><span>          (</span><span>cond</span></span>
<span><span>           ((</span><span>></span><span> sum neg-x) (loop left (</span><span>-</span><span> right </span><span>1</span><span>) ans))</span></span>
<span><span>           ((</span><span>&#x3C;</span><span> sum neg-x) (loop (</span><span>+</span><span> left </span><span>1</span><span>) right ans))</span></span>
<span><span>           (</span><span>else</span><span> (loop (</span><span>+</span><span> left </span><span>1</span><span>)</span></span>
<span><span>                       (</span><span>-</span><span> right </span><span>1</span><span>)</span></span>
<span><span>                       (</span><span>cons</span><span> (</span><span>list</span><span> x y z) ans)))))</span></span>
<span><span>        ans)))</span></span>
<span></span>
<span><span>(</span><span>define</span><span> (</span><span>three-sum</span><span> nums</span><span>)</span></span>
<span><span>  (</span><span>let</span><span> ((len (</span><span>vector-length</span><span> nums))</span></span>
<span><span>        (sorted (sort nums &#x3C;)))</span></span>
<span><span>    (</span><span>let</span><span> loop ((i </span><span>0</span><span>)</span></span>
<span><span>               (result </span><span>'()</span><span>))</span></span>
<span><span>      (</span><span>if</span><span> (</span><span>&#x3C;</span><span> i len)</span></span>
<span><span>          (</span><span>if</span><span> (</span><span>and</span><span> (</span><span>></span><span> i </span><span>0</span><span>)</span></span>
<span><span>                   (</span><span>eq?</span><span> (</span><span>vector-ref</span><span> sorted i)</span></span>
<span><span>                        (</span><span>vector-ref</span><span> sorted (</span><span>-</span><span> i </span><span>1</span><span>))))</span></span>
<span><span>              (loop (</span><span>+</span><span> i </span><span>1</span><span>) result)</span></span>
<span><span>              (</span><span>let</span><span> ((new-ans (find-x-y-z sorted</span></span>
<span><span>                                         (</span><span>vector-ref</span><span> sorted i)</span></span>
<span><span>                                         (</span><span>+</span><span> i </span><span>1</span><span>))))</span></span>
<span><span>                (loop (</span><span>+</span><span> i </span><span>1</span><span>)</span></span>
<span><span>                      (</span><span>append</span><span> result new-ans))))</span></span>
<span><span>          result))))</span></span></code></pre><p>提交后没有通过，因为只跳过了重复的x，下面是最终代码：
</p><pre tabindex="0"><code><span><span>(</span><span>define</span><span> (</span><span>find-x-y-z</span><span> nums x start end</span><span>)</span></span>
<span><span>  (</span><span>let</span><span> loop ((left start)</span></span>
<span><span>             (right end)</span></span>
<span><span>             (ans </span><span>'()</span><span>))</span></span>
<span><span>    (</span><span>if</span><span> (</span><span>&#x3C;</span><span> left right)</span></span>
<span><span>        (</span><span>let*</span><span> ((y (</span><span>vector-ref</span><span> nums left))</span></span>
<span><span>               (z (</span><span>vector-ref</span><span> nums right))</span></span>
<span><span>               (sum (</span><span>+</span><span> y z))</span></span>
<span><span>               (neg-x (</span><span>-</span><span> x)))</span></span>
<span><span>          (</span><span>cond</span></span>
<span><span>           ((</span><span>and</span><span> (</span><span>></span><span> left start)</span></span>
<span><span>                 (</span><span>eq?</span><span> (</span><span>vector-ref</span><span> nums (</span><span>-</span><span> left </span><span>1</span><span>)) y))</span></span>
<span><span>            (loop (</span><span>+</span><span> left </span><span>1</span><span>) right ans))</span></span>
<span><span>           ((</span><span>and</span><span> (</span><span>&#x3C;</span><span> right end)</span></span>
<span><span>                 (</span><span>eq?</span><span> (</span><span>vector-ref</span><span> nums (</span><span>+</span><span> right </span><span>1</span><span>)) z))</span></span>
<span><span>            (loop left (</span><span>-</span><span> right </span><span>1</span><span>) ans))</span></span>
<span><span>           ((</span><span>></span><span> sum neg-x) (loop left (</span><span>-</span><span> right </span><span>1</span><span>) ans))</span></span>
<span><span>           ((</span><span>&#x3C;</span><span> sum neg-x) (loop (</span><span>+</span><span> left </span><span>1</span><span>) right ans))</span></span>
<span><span>           (</span><span>else</span><span> (loop (</span><span>+</span><span> left </span><span>1</span><span>)</span></span>
<span><span>                       (</span><span>-</span><span> right </span><span>1</span><span>)</span></span>
<span><span>                       (</span><span>cons</span><span> (</span><span>list</span><span> x y z) ans)))))</span></span>
<span><span>        ans)))</span></span>
<span></span>
<span><span>(</span><span>define</span><span> (</span><span>three-sum</span><span> nums</span><span>)</span></span>
<span><span>  (</span><span>let</span><span> ((len (</span><span>vector-length</span><span> nums))</span></span>
<span><span>        (sorted (sort nums &#x3C;)))</span></span>
<span><span>    (</span><span>let</span><span> loop ((i </span><span>0</span><span>)</span></span>
<span><span>               (result </span><span>'()</span><span>))</span></span>
<span><span>      (</span><span>if</span><span> (</span><span>&#x3C;</span><span> i len)</span></span>
<span><span>          (</span><span>if</span><span> (</span><span>and</span><span> (</span><span>></span><span> i </span><span>0</span><span>)</span></span>
<span><span>                   (</span><span>eq?</span><span> (</span><span>vector-ref</span><span> sorted i)</span></span>
<span><span>                        (</span><span>vector-ref</span><span> sorted (</span><span>-</span><span> i </span><span>1</span><span>))))</span></span>
<span><span>              (loop (</span><span>+</span><span> i </span><span>1</span><span>) result)</span></span>
<span><span>              (</span><span>let</span><span> ((new-ans (find-x-y-z sorted</span></span>
<span><span>                                         (</span><span>vector-ref</span><span> sorted i)</span></span>
<span><span>                                         (</span><span>+</span><span> i </span><span>1</span><span>)</span></span>
<span><span>                                         (</span><span>-</span><span> len </span><span>1</span><span>))))</span></span>
<span><span>                (loop (</span><span>+</span><span> i </span><span>1</span><span>)</span></span>
<span><span>                      (</span><span>append</span><span> result new-ans))))</span></span>
<span><span>          result))))</span></span></code></pre><p>還能做一些優化，按升序排序後，如果xyz和大於0了，後面的數再相加只會更大，可以直接跳出循環過程，但算法時間複雜度​<code class="">O(N^2)</code>​不能再優化了。
</p><h2 id="user-content-16-最接近的三数之和" class="">16. 最接近的三数之和<a class="" tabindex="-1" href="#16-最接近的三数之和">#</a></h2><p>在求解过程中要维护一个表示最小差距的变量：
</p><pre tabindex="0"><code><span><span>(</span><span>define</span><span> (</span><span>three-sum-closest</span><span> nums target</span><span>)</span></span>
<span><span>  (</span><span>let</span><span> ((len (</span><span>vector-length</span><span> nums))</span></span>
<span><span>        (sorted (sort nums &#x3C;))</span></span>
<span><span>        (answer +inf.0)</span></span>
<span><span>        (min-gap +inf.0))</span></span>
<span><span>    (</span><span>let</span><span> outer-loop ((i </span><span>0</span><span>))</span></span>
<span><span>      (when (</span><span>&#x3C;</span><span> i len)</span></span>
<span><span>        ;; 跳过重复的i</span></span>
<span><span>        (</span><span>if</span><span> (</span><span>and</span><span> (</span><span>></span><span> i </span><span>0</span><span>)</span></span>
<span><span>                 (</span><span>eq?</span><span> (</span><span>vector-ref</span><span> sorted i)</span></span>
<span><span>                      (</span><span>vector-ref</span><span> sorted (</span><span>-</span><span> i </span><span>1</span><span>))))</span></span>
<span><span>            (outer-loop (</span><span>+</span><span> i </span><span>1</span><span>))</span></span>
<span><span>            (</span><span>let</span><span> ((x (</span><span>vector-ref</span><span> sorted i)))</span></span>
<span><span>              (</span><span>let</span><span> inner-loop ((j (</span><span>+</span><span> i </span><span>1</span><span>))</span></span>
<span><span>                               (k (</span><span>-</span><span> len </span><span>1</span><span>)))</span></span>
<span><span>                (when (</span><span>&#x3C;</span><span> j k)</span></span>
<span><span>                  (</span><span>let*</span><span> ((y (</span><span>vector-ref</span><span> sorted j))</span></span>
<span><span>                         (z (</span><span>vector-ref</span><span> sorted k))</span></span>
<span><span>                         (sum (</span><span>+</span><span> x y z))</span></span>
<span><span>                         (gap (</span><span>abs</span><span> (</span><span>-</span><span> sum target))))</span></span>
<span><span>                    ;; 差距更小，更新答案</span></span>
<span><span>                    (when (</span><span>&#x3C;</span><span> gap min-gap)</span></span>
<span><span>                      (</span><span>set!</span><span> min-gap gap)</span></span>
<span><span>                      (</span><span>set!</span><span> answer sum))</span></span>
<span><span>                    (</span><span>cond</span></span>
<span><span>                     ((</span><span>&#x3C;</span><span> sum target)</span></span>
<span><span>                      (</span><span>let</span><span> loop ((new-j (</span><span>+</span><span> j </span><span>1</span><span>)))</span></span>
<span><span>                        ;; 跳过重复</span></span>
<span><span>                        (</span><span>if</span><span> (</span><span>and</span><span> (</span><span>&#x3C;</span><span> new-j k)</span></span>
<span><span>                                 (</span><span>eq?</span><span> (</span><span>vector-ref</span><span> sorted new-j)</span></span>
<span><span>                                      (</span><span>vector-ref</span><span> sorted j)))</span></span>
<span><span>                            (loop (</span><span>+</span><span> new-j </span><span>1</span><span>))</span></span>
<span><span>                            (inner-loop new-j k))))</span></span>
<span><span>                     ((</span><span>></span><span> sum target)</span></span>
<span><span>                      (</span><span>let</span><span> loop ((new-k (</span><span>-</span><span> k </span><span>1</span><span>)))</span></span>
<span><span>                        ;; 跳过重复</span></span>
<span><span>                        (</span><span>if</span><span> (</span><span>and</span><span> (</span><span>></span><span> new-k j)</span></span>
<span><span>                                 (</span><span>eq?</span><span> (</span><span>vector-ref</span><span> sorted new-k)</span></span>
<span><span>                                      (</span><span>vector-ref</span><span> sorted k)))</span></span>
<span><span>                            (loop (</span><span>-</span><span> new-k </span><span>1</span><span>))</span></span>
<span><span>                            (inner-loop j new-k))))</span></span>
<span><span>                     (</span><span>else</span></span>
<span><span>                      (</span><span>set!</span><span> answer sum)</span></span>
<span><span>                      (</span><span>set!</span><span> min-gap </span><span>0</span><span>))))))</span></span>
<span><span>              (outer-loop (</span><span>+</span><span> i </span><span>1</span><span>))))))</span></span>
<span><span>    answer))</span></span></code></pre><h2 id="user-content-923-三数之和的多种可能" class="">923. 三数之和的多种可能<a class="" tabindex="-1" href="#923-三数之和的多种可能">#</a></h2><p>题目要求最后答案要做模运算，先定义要模的常量：
</p><pre tabindex="0"><code><span><span>(</span><span>define</span><span> MOD</span><span> 1000000007)</span></span></code></pre><p>和三数之和比，不用去重，但是要想出怎么算所有的可能。
</p><p>假设按求三数之和的方法，固定第一个数，双指针刚刚找到了满足条件的两个数，这时有两种情况：
</p><ol><li>两个数相等，由于已经排序了，说明双指针之间所有数都一样，可能的组合就是​<code class="">C(k, 2)</code>​，k是这个数的计数。并且这时双指针循环可以结束，因为中间没有不同的数了。
</li><li>两个数不相等，设区间内和左指针下数相同的有m个，和右指针下数相同的有n个，那就是从m中取1个，再从n中取1个，有m * n种组合。另外在区间中间，可能还有符合条件的其它数，所以双指针循环还要继续。
</li></ol><p>第一种情况可以直接跳出双指针循环，第二种情况需要两边分别计数，先为第二种情况写一个函数：
</p><pre tabindex="0"><code><span><span>(</span><span>define</span><span> (</span><span>count-rest-two</span><span> arr left right</span><span>)</span></span>
<span><span>  (</span><span>let</span><span> ((a (</span><span>vector-ref</span><span> arr left))</span></span>
<span><span>        (b (</span><span>vector-ref</span><span> arr right)))</span></span>
<span><span>    (</span><span>let</span><span> loop ((start (</span><span>+</span><span> left </span><span>1</span><span>))</span></span>
<span><span>               (end (</span><span>-</span><span> right </span><span>1</span><span>))</span></span>
<span><span>               (l-count </span><span>1</span><span>)</span></span>
<span><span>               (r-count </span><span>1</span><span>))</span></span>
<span><span>      (</span><span>if</span><span> (</span><span>&#x3C;=</span><span> start end)</span></span>
<span><span>          (</span><span>cond</span></span>
<span><span>           ((</span><span>eq?</span><span> a (</span><span>vector-ref</span><span> arr start))</span></span>
<span><span>            (loop (</span><span>+</span><span> start </span><span>1</span><span>)</span></span>
<span><span>                  end</span></span>
<span><span>                  (</span><span>+</span><span> l-count </span><span>1</span><span>)</span></span>
<span><span>                  r-count))</span></span>
<span><span>           ((</span><span>eq?</span><span> b (</span><span>vector-ref</span><span> arr end))</span></span>
<span><span>            (loop start</span></span>
<span><span>                  (</span><span>-</span><span> end </span><span>1</span><span>)</span></span>
<span><span>                  l-count</span></span>
<span><span>                  (</span><span>+</span><span> r-count </span><span>1</span><span>)))</span></span>
<span><span>           (</span><span>else</span><span> (</span><span>*</span><span> l-count r-count)))</span></span>
<span><span>          (</span><span>*</span><span> l-count r-count)))))</span></span></code></pre><p>注意这里在一个循环过程中求两边计数，最后左右指针相遇时，指针下的数可能刚好和前一个相同，如果用小于判断出循环，结果就不对了。也可以把这个函数写成两边分别循环求计数。
</p><p>考虑到计算完在双指针中间可能还有解，可以把计数完后双指针的位置也返回出来用于下一次双指针过程，稍作修改：
</p><pre tabindex="0"><code><span><span>(use-modules (srfi srfi-11))</span></span>
<span></span>
<span><span>(</span><span>define</span><span> (</span><span>count-rest-two</span><span> arr left right</span><span>)</span></span>
<span><span>  (</span><span>let</span><span> ((a (</span><span>vector-ref</span><span> arr left))</span></span>
<span><span>        (b (</span><span>vector-ref</span><span> arr right)))</span></span>
<span><span>    (</span><span>let</span><span> loop ((start (</span><span>+</span><span> left </span><span>1</span><span>))</span></span>
<span><span>               (end (</span><span>-</span><span> right </span><span>1</span><span>))</span></span>
<span><span>               (l-count </span><span>1</span><span>)</span></span>
<span><span>               (r-count </span><span>1</span><span>))</span></span>
<span><span>      (</span><span>if</span><span> (</span><span>&#x3C;=</span><span> start end)</span></span>
<span><span>          (</span><span>cond</span></span>
<span><span>           ((</span><span>eq?</span><span> a (</span><span>vector-ref</span><span> arr start))</span></span>
<span><span>            (loop (</span><span>+</span><span> start </span><span>1</span><span>)</span></span>
<span><span>                  end</span></span>
<span><span>                  (</span><span>+</span><span> l-count </span><span>1</span><span>)</span></span>
<span><span>                  r-count))</span></span>
<span><span>           ((</span><span>eq?</span><span> b (</span><span>vector-ref</span><span> arr end))</span></span>
<span><span>            (loop start</span></span>
<span><span>                  (</span><span>-</span><span> end </span><span>1</span><span>)</span></span>
<span><span>                  l-count</span></span>
<span><span>                  (</span><span>+</span><span> r-count </span><span>1</span><span>)))</span></span>
<span><span>           (</span><span>else</span><span> (values start end (</span><span>*</span><span> l-count r-count))))</span></span>
<span><span>          (values start end (</span><span>*</span><span> l-count r-count))))))</span></span>
<span></span>
<span><span>(</span><span>define</span><span> (</span><span>three-sum-multi</span><span> arr target</span><span>)</span></span>
<span><span>  (</span><span>let</span><span> ((len (</span><span>vector-length</span><span> arr))</span></span>
<span><span>        (sorted (sort arr &#x3C;)))</span></span>
<span><span>    (</span><span>let</span><span> outer-loop ((i </span><span>0</span><span>)</span></span>
<span><span>                     (ans </span><span>0</span><span>))</span></span>
<span><span>      (</span><span>if</span><span> (</span><span>&#x3C;</span><span> i (</span><span>-</span><span> len </span><span>2</span><span>))</span></span>
<span><span>          (</span><span>let</span><span> loop ((left (</span><span>+</span><span> i </span><span>1</span><span>))</span></span>
<span><span>                     (right (</span><span>-</span><span> len </span><span>1</span><span>))</span></span>
<span><span>                     (ans ans))</span></span>
<span><span>            (</span><span>if</span><span> (</span><span>&#x3C;</span><span> left right)</span></span>
<span><span>                (</span><span>let</span><span> ((sum (</span><span>+</span><span> (</span><span>vector-ref</span><span> sorted i)</span></span>
<span><span>                              (</span><span>vector-ref</span><span> sorted left)</span></span>
<span><span>                              (</span><span>vector-ref</span><span> sorted right))))</span></span>
<span><span>                  (</span><span>cond</span></span>
<span><span>                   ((</span><span>></span><span> sum target) (loop left (</span><span>-</span><span> right </span><span>1</span><span>) ans))</span></span>
<span><span>                   ((</span><span>&#x3C;</span><span> sum target) (loop (</span><span>+</span><span> left </span><span>1</span><span>) right ans))</span></span>
<span><span>                   (</span><span>else</span></span>
<span><span>                    (</span><span>if</span><span> (</span><span>eq?</span><span> (</span><span>vector-ref</span><span> sorted left)</span></span>
<span><span>                             (</span><span>vector-ref</span><span> sorted right))</span></span>
<span><span>                        (</span><span>let</span><span> ((count (</span><span>+</span><span> (</span><span>-</span><span> right left) </span><span>1</span><span>)))</span></span>
<span><span>                          (outer-loop (</span><span>+</span><span> i </span><span>1</span><span>)</span></span>
<span><span>                                      (</span><span>+</span><span> ans</span></span>
<span><span>                                         (</span><span>quotient</span><span> (</span><span>*</span><span> count (</span><span>-</span><span> count </span><span>1</span><span>)) </span><span>2</span><span>))))</span></span>
<span><span>                        (let-values (((new-left new-right new-count) (count-rest-two sorted left right)))</span></span>
<span><span>                          (loop new-left</span></span>
<span><span>                                new-right</span></span>
<span><span>                                (</span><span>+</span><span> ans new-count)))))))</span></span>
<span><span>                (outer-loop (</span><span>+</span><span> i </span><span>1</span><span>) ans)))</span></span>
<span><span>          (modulo ans MOD)))))</span></span></code></pre>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>相向双指针（一）</title>
          <link>https://elliot00.com/posts/opposite-direction-two-points-1</link>
          <description>本文系统讲解了相向双指针算法的基本原理和实际应用。相向双指针通过在数组或字符串的两端设置指针，让它们向中间移动来解决问题，这种模式特别适合处理对称性、反转和双向遍历的场景。</description>
          <pubDate>Sun, 23 Nov 2025 10:51:13 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <p><img alt="Opposite-direction Two Points" src="https://r2.elliot00.com/algorithm/opposite-direction-two-points.png" width="1022" height="483"></p><h2 id="user-content-344-反转字符串" class="">344. 反转字符串<a class="" tabindex="-1" href="#344-反转字符串">#</a></h2><p>先来一道送分题，只要在数组的头尾各放一个指针，交换指针下的元素，之后两个指针同时向中心移动就可以反转数组了。
</p><p>如果数组元素个数是单数，那么中间的元素不用处理；如果是偶数个元素，两个指针分别在中间两个元素上做最后一次交换就可以停止，所以循环条件是​<code class="">left &#x3C; right</code>​：
</p><pre tabindex="0"><code><span><span>def</span><span> reverse_string</span><span>(</span><span>s</span><span>)</span><span>:</span></span>
<span><span>    left </span><span>=</span><span> 0</span></span>
<span><span>    right </span><span>=</span><span> len</span><span>(</span><span>s</span><span>)</span><span> -</span><span> 1</span></span>
<span><span>    while</span><span> left </span><span>&#x3C;</span><span> right</span><span>:</span></span>
<span><span>        temp </span><span>=</span><span> s</span><span>[</span><span>left</span><span>]</span></span>
<span><span>        s</span><span>[</span><span>left</span><span>]</span><span> =</span><span> s</span><span>[</span><span>right</span><span>]</span></span>
<span><span>        s</span><span>[</span><span>right</span><span>]</span><span> =</span><span> temp</span></span>
<span><span>        left </span><span>+=</span><span> 1</span></span>
<span><span>        right </span><span>-=</span><span> 1</span></span></code></pre><p>交换的部分使用的是通用写法，要利用Python的特性可以直接用​<code class="">x, y = y, x</code>​的交换写法。
</p><h2 id="user-content-345-反转字符串中的元音字母" class="">345. 反转字符串中的元音字母<a class="" tabindex="-1" href="#345-反转字符串中的元音字母">#</a></h2><p>送分题结束，再来一首热身题。本题实际只需要在上一题的基础上，额外做一个是否是元音的判断，只有两个指针下都是元音才交换字符。
</p><pre tabindex="0"><code><span><span>def</span><span> reverse_vowels</span><span>(</span><span>s</span><span>)</span><span>:</span></span>
<span><span>    l</span><span>,</span><span> r </span><span>=</span><span> 0</span><span>,</span><span> len</span><span>(</span><span>s</span><span>)</span><span> -</span><span> 1</span></span>
<span><span>    # python不能直接改字符串，所以先转成列表</span></span>
<span><span>    chars </span><span>=</span><span> list</span><span>(</span><span>s</span><span>)</span></span>
<span><span>    vowels </span><span>=</span><span> set</span><span>(</span><span>'</span><span>aeiouAEIOU</span><span>'</span><span>)</span></span>
<span></span>
<span><span>    while</span><span> l </span><span>&#x3C;</span><span> r</span><span>:</span></span>
<span><span>        if</span><span> s</span><span>[</span><span>l</span><span>]</span><span> not</span><span> in</span><span> vowels</span><span>:</span></span>
<span><span>            l </span><span>+=</span><span> 1</span></span>
<span><span>        elif</span><span> s</span><span>[</span><span>r</span><span>]</span><span> not</span><span> in</span><span> vowels</span><span>:</span></span>
<span><span>            r </span><span>-=</span><span> 1</span></span>
<span><span>        else</span><span>:</span></span>
<span><span>            # 都是元音，交换  </span></span>
<span><span>            chars</span><span>[</span><span>l</span><span>],</span><span> chars</span><span>[</span><span>r</span><span>]</span><span> =</span><span> chars</span><span>[</span><span>r</span><span>],</span><span> chars</span><span>[</span><span>l</span><span>]</span></span>
<span><span>            l </span><span>+=</span><span> 1</span></span>
<span><span>            r </span><span>-=</span><span> 1</span></span>
<span></span>
<span><span>    return</span><span> ''</span><span>.</span><span>join</span><span>(</span><span>chars</span><span>)</span></span></code></pre><h2 id="user-content-1750-删除字符串两端相同字符后的最短长度" class="">1750. 删除字符串两端相同字符后的最短长度<a class="" tabindex="-1" href="#1750-删除字符串两端相同字符后的最短长度">#</a></h2><p>根据题目要求，循环条件需要加上左右指针下字符相同。内部可以用子循环，只要字符相同就一直移动指针，最后右指针减左指针加一就是剩余长度了。
</p><pre tabindex="0"><code><span><span>def</span><span> minimum_length</span><span>(</span><span>s</span><span>)</span><span>:</span></span>
<span><span>    left</span><span>,</span><span> right </span><span>=</span><span> 0</span><span>,</span><span> len</span><span>(</span><span>s</span><span>)</span><span> -</span><span> 1</span></span>
<span><span>    while</span><span> left </span><span>&#x3C;</span><span> right </span><span>and</span><span> s</span><span>[</span><span>left</span><span>]</span><span> ==</span><span> s</span><span>[</span><span>right</span><span>]:</span></span>
<span><span>        c </span><span>=</span><span> s</span><span>[</span><span>left</span><span>]</span></span>
<span><span>        while</span><span> left </span><span>&#x3C;=</span><span> right </span><span>and</span><span> s</span><span>[</span><span>left</span><span>]</span><span> ==</span><span> c</span><span>:</span></span>
<span><span>            left </span><span>+=</span><span> 1</span></span>
<span><span>        while</span><span> left </span><span>&#x3C;=</span><span> right </span><span>and</span><span> s</span><span>[</span><span>right</span><span>]</span><span> ==</span><span> c</span><span>:</span></span>
<span><span>            right </span><span>-=</span><span> 1</span></span>
<span><span>    return</span><span> right </span><span>-</span><span> left </span><span>+</span><span> 1</span></span></code></pre><h2 id="user-content-2105-给植物浇水-ii" class="">2105. 给植物浇水 II<a class="" tabindex="-1" href="#2105-给植物浇水-ii">#</a></h2><p>这一题直接按着题目描述模拟就可以了。
</p><pre tabindex="0"><code><span><span>def</span><span> minimum_refill</span><span>(</span><span>plants</span><span>,</span><span> capacity_a</span><span>,</span><span> capacity_b</span><span>)</span><span>:</span></span>
<span><span>    ans </span><span>=</span><span> 0</span></span>
<span></span>
<span><span>    # a b 当前水量</span></span>
<span><span>    a </span><span>=</span><span> capacity_a</span></span>
<span><span>    b </span><span>=</span><span> capacity_b</span></span>
<span></span>
<span><span>    # a b 当前位置</span></span>
<span><span>    left </span><span>=</span><span> 0</span></span>
<span><span>    right </span><span>=</span><span> len</span><span>(</span><span>plants</span><span>)</span><span> -</span><span> 1</span></span>
<span></span>
<span><span>    while</span><span> left </span><span>&#x3C;</span><span> right</span><span>:</span></span>
<span><span>        # a的水不够，补充水，答案加1</span></span>
<span><span>        if</span><span> a </span><span>&#x3C;</span><span> plants</span><span>[</span><span>left</span><span>]:</span></span>
<span><span>            a </span><span>=</span><span> capacity_a</span></span>
<span><span>            ans </span><span>+=</span><span> 1</span></span>
<span><span>        # a浇水，右移</span></span>
<span><span>        a </span><span>-=</span><span> plants</span><span>[</span><span>left</span><span>]</span></span>
<span><span>        left </span><span>+=</span><span> 1</span></span>
<span></span>
<span><span>        # b的水不够，补水，答案加1</span></span>
<span><span>        if</span><span> b </span><span>&#x3C;</span><span> plants</span><span>[</span><span>right</span><span>]:</span></span>
<span><span>            b </span><span>=</span><span> capacity_b</span></span>
<span><span>            ans </span><span>+=</span><span> 1</span></span>
<span><span>        # b浇水，左移</span></span>
<span><span>        b </span><span>-=</span><span> plants</span><span>[</span><span>right</span><span>]</span></span>
<span><span>        right </span><span>-=</span><span> 1</span></span>
<span></span>
<span><span>    # a b 相遇，如果水不够答案再加1</span></span>
<span><span>    if</span><span> left </span><span>==</span><span> right </span><span>and</span><span> max</span><span>(</span><span>a</span><span>,</span><span> b</span><span>)</span><span> &#x3C;</span><span> plants</span><span>[</span><span>left</span><span>]:</span></span>
<span><span>        ans </span><span>+=</span><span> 1</span></span>
<span><span>    return</span><span> ans</span></span></code></pre><h2 id="user-content-977-有序数组的平方" class="">977. 有序数组的平方<a class="" tabindex="-1" href="#977-有序数组的平方">#</a></h2><p>最简单的方式就是直接map平方再排序（有负数，必需再排序一次），但是题目既然提了数组是有序的，最好要利用上这个性质。
</p><div><div><div></div><div>Tip</div></div><div><p>题目里的“非递增”和“非递减”是数学术语，不能按字面理解，直接将非递增理解为数组的后一个元素必然小于等于前一个，非递减理解为后一个一定大于等于前一个就好。
</p></div></div><p>因为数组本身是非递减的，数组的负数部分的平方就是非递增的，而非负数部分的平方就是非递减的了。如果能找出数组中负数和非负数的分界，从这里开始用两个指针分别指向负数和非负数部分，每次把平方较小的推入结果数组中，之后移动指针就可以解决问题。
</p><p>先写一个函数用二分查找来找分界点（这里也是利用数组有序的特性）：
</p><pre tabindex="0"><code><span><span>import</span><span> bisect</span></span>
<span></span>
<span><span>def</span><span> find_boundary</span><span>(</span><span>nums</span><span>)</span><span>:</span></span>
<span><span>    first_non_negative </span><span>=</span><span> bisect</span><span>.</span><span>bisect_left</span><span>(</span><span>nums</span><span>,</span><span> 0</span><span>)</span></span>
<span><span>    return</span><span> first_non_negative </span><span>-</span><span> 1</span><span>,</span><span> first_non_negative</span></span></code></pre><p>用i、j两个指针分别指向最后的负数和第一个非负数（没有负数也没关系，i初始化在-1，相当于单向遍历了）：
</p><pre tabindex="0"><code><span><span>def</span><span> sorted_squares</span><span>(</span><span>nums</span><span>)</span><span>:</span></span>
<span><span>    i</span><span>,</span><span> j </span><span>=</span><span> find_boundary</span><span>(</span><span>nums</span><span>)</span></span>
<span><span>    ans </span><span>=</span><span> []</span></span>
<span><span>    while</span><span> i </span><span>>=</span><span> 0</span><span> or</span><span> j </span><span>&#x3C;</span><span> len</span><span>(</span><span>nums</span><span>):</span></span>
<span><span>        # 先处理两个边界情况</span></span>
<span><span>        if</span><span> i </span><span>&#x3C;</span><span> 0</span><span>:</span></span>
<span><span>            ans</span><span>.</span><span>append</span><span>(</span><span>nums</span><span>[</span><span>j</span><span>]</span><span> **</span><span> 2</span><span>)</span></span>
<span><span>            j </span><span>+=</span><span> 1</span></span>
<span><span>        elif</span><span> j </span><span>==</span><span> len</span><span>(</span><span>nums</span><span>):</span></span>
<span><span>            ans</span><span>.</span><span>append</span><span>(</span><span>nums</span><span>[</span><span>i</span><span>]</span><span> **</span><span> 2</span><span>)</span></span>
<span><span>            i </span><span>-=</span><span> 1</span></span>
<span><span>        else</span><span>:</span></span>
<span><span>            x </span><span>=</span><span> nums</span><span>[</span><span>i</span><span>]</span><span> **</span><span> 2</span></span>
<span><span>            y </span><span>=</span><span> nums</span><span>[</span><span>j</span><span>]</span><span> **</span><span> 2</span></span>
<span><span>            # 小的先进，i向左移，j向右移</span></span>
<span><span>            if</span><span> x </span><span>&#x3C;</span><span> y</span><span>:</span></span>
<span><span>                ans</span><span>.</span><span>append</span><span>(</span><span>x</span><span>)</span></span>
<span><span>                i </span><span>-=</span><span> 1</span></span>
<span><span>            else</span><span>:</span></span>
<span><span>                ans</span><span>.</span><span>append</span><span>(</span><span>y</span><span>)</span></span>
<span><span>                j </span><span>+=</span><span> 1</span></span>
<span><span>    return</span><span> ans</span></span></code></pre><h3 id="user-content-思路二">思路二<a class="" tabindex="-1" href="#思路二">#</a></h3><p>如果把双指针分别放在数组开头和结尾，根据原数组的非递减特性，求平方后最大的数，不是在开头（有负数的情况）就是在结尾。那么先初始化一个和原数组等长的答案数组，从答案数组的末尾向开头遍历，每次比较原数组的开头和结尾平方的大小，就可以做归并排序，从右往左、从大到小更新答案。
</p><pre tabindex="0"><code><span><span>def</span><span> sorted_squares</span><span>(</span><span>nums</span><span>)</span><span>:</span></span>
<span><span>    n </span><span>=</span><span> len</span><span>(</span><span>nums</span><span>)</span></span>
<span><span>    ans </span><span>=</span><span> [</span><span>0</span><span>]</span><span> *</span><span> n</span></span>
<span><span>    left</span><span>,</span><span> right </span><span>=</span><span> 0</span><span>,</span><span> n </span><span>-</span><span> 1</span></span>
<span></span>
<span><span>    # 倒着更新</span></span>
<span><span>    for</span><span> i </span><span>in</span><span> range</span><span>(</span><span>n </span><span>-</span><span> 1</span><span>,</span><span> -</span><span>1</span><span>,</span><span> -</span><span>1</span><span>):</span></span>
<span><span>        l_square </span><span>=</span><span> nums</span><span>[</span><span>left</span><span>]</span><span> **</span><span> 2</span></span>
<span><span>        r_square </span><span>=</span><span> nums</span><span>[</span><span>right</span><span>]</span><span> **</span><span> 2</span></span>
<span><span>        # 每次将较大的更新进答案</span></span>
<span><span>        if</span><span> l_square </span><span>></span><span> r_square</span><span>:</span></span>
<span><span>            ans</span><span>[</span><span>i</span><span>]</span><span> =</span><span> l_square</span></span>
<span><span>            # 左边大就右移左指针，否则就左移右指针</span></span>
<span><span>            left </span><span>+=</span><span> 1</span></span>
<span><span>        else</span><span>:</span></span>
<span><span>            ans</span><span>[</span><span>i</span><span>]</span><span> =</span><span> r_square</span></span>
<span><span>            right </span><span>-=</span><span> 1</span></span>
<span><span>    return</span><span> ans</span></span></code></pre><p>在循环过程中，还可以利用原数组有序的特性，省略重复的平方运算，比如比较​<code class="">abs(nums[left]) > nums[right]</code>​，或者​<code class="">-nums[left] > nums[right]</code>​。
</p><h2 id="user-content-948-令牌放置" class="">948. 令牌放置<a class="" tabindex="-1" href="#948-令牌放置">#</a></h2><p>直觉上，应该用最小的能量换分，直到能量不够，用分数换最大的能量。
</p><p>尝试先从小到大排序令牌，再用相向双指针，能量够就从左边最小的令牌换分，能量不够就从右边最大的令牌换能量。
</p><pre tabindex="0"><code><span><span>def</span><span> bag_of_tokens_score</span><span>(</span><span>tokens</span><span>,</span><span> power</span><span>)</span><span>:</span></span>
<span><span>    tokens</span><span>.</span><span>sort</span><span>()</span></span>
<span><span>    score </span><span>=</span><span> 0</span></span>
<span><span>    left</span><span>,</span><span> right </span><span>=</span><span> 0</span><span>,</span><span> len</span><span>(</span><span>tokens</span><span>)</span><span> -</span><span> 1</span></span>
<span><span>    while</span><span> left </span><span>&#x3C;=</span><span> right</span><span>:</span></span>
<span><span>        # 能量足够，换取分数</span></span>
<span><span>        if</span><span> power </span><span>>=</span><span> tokens</span><span>[</span><span>left</span><span>]:</span></span>
<span><span>            power </span><span>-=</span><span> tokens</span><span>[</span><span>left</span><span>]</span></span>
<span><span>            left </span><span>+=</span><span> 1</span></span>
<span><span>            score </span><span>+=</span><span> 1</span></span>
<span><span>        # 能量不够，兑换能量</span></span>
<span><span>        elif</span><span> score </span><span>></span><span> 0</span><span>:</span></span>
<span><span>            power </span><span>+=</span><span> tokens</span><span>[</span><span>right</span><span>]</span></span>
<span><span>            right </span><span>-=</span><span> 1</span></span>
<span><span>            score </span><span>-=</span><span> 1</span></span>
<span><span>        # 能量和分数都不足，直接退出</span></span>
<span><span>        else</span><span>:</span></span>
<span><span>            break</span></span>
<span><span>    return</span><span> score</span></span></code></pre><p>思路没感觉有问题，但是在测试用例​<code class="">[200, 100]</code>​上出错了，答案应该是1，但输出结果是0。仔细想想是因为在我的代码里只要分数足够而能量不足，就会消耗分数去换能量，但是没有考虑到如果兑换完能量后剩下的能量还是不够换分，那就白白亏掉了一分。
</p><p>再次确认下用积分换能量的条件：
</p><ol><li>当前能量小于左侧最小令牌，无法消耗能量得分
</li><li>当前分数大于0
</li><li>当前能量加上最左侧令牌能量大于等于右侧令牌（这样至少不会亏）
</li><li>左右指针不能在同一个位置（一个令牌只能用一次）
</li></ol><p>用代码表示：
</p><pre tabindex="0"><code><span><span>def</span><span> bag_of_tokens_score</span><span>(</span><span>tokens</span><span>,</span><span> power</span><span>)</span><span>:</span></span>
<span><span>    tokens</span><span>.</span><span>sort</span><span>()</span></span>
<span><span>    score </span><span>=</span><span> 0</span></span>
<span><span>    left</span><span>,</span><span> right </span><span>=</span><span> 0</span><span>,</span><span> len</span><span>(</span><span>tokens</span><span>)</span><span> -</span><span> 1</span></span>
<span><span>    while</span><span> left </span><span>&#x3C;=</span><span> right</span><span>:</span></span>
<span><span>        # 能量足够，换取分数</span></span>
<span><span>        if</span><span> power </span><span>>=</span><span> tokens</span><span>[</span><span>left</span><span>]:</span></span>
<span><span>            power </span><span>-=</span><span> tokens</span><span>[</span><span>left</span><span>]</span></span>
<span><span>            left </span><span>+=</span><span> 1</span></span>
<span><span>            score </span><span>+=</span><span> 1</span></span>
<span><span>        # 能量不够，兑换能量，但要兑换后还能再获得分数</span></span>
<span><span>        elif</span><span> score </span><span>></span><span> 0</span><span> and</span><span> right </span><span>></span><span> left </span><span>and</span><span> (power </span><span>+</span><span> tokens</span><span>[</span><span>right</span><span>]</span><span>) </span><span>>=</span><span> tokens</span><span>[</span><span>left</span><span>]:</span></span>
<span><span>            power </span><span>+=</span><span> tokens</span><span>[</span><span>right</span><span>]</span></span>
<span><span>            right </span><span>-=</span><span> 1</span></span>
<span><span>            score </span><span>-=</span><span> 1</span></span>
<span><span>        # 能量和分数都不足，直接退出</span></span>
<span><span>        else</span><span>:</span></span>
<span><span>            break</span></span>
<span><span>    return</span><span> score</span></span></code></pre>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>完全掌握不定長滑動窗口</title>
          <link>https://elliot00.com/posts/sliding-window</link>
          <description>本文通過多個LeetCode題目實例，系統講解了不定長滑動窗口算法的應用。文章詳細展示了如何在不同場景下使用滑動窗口的兩步走套路：右指針擴展窗口並更新狀態，當條件不滿足時移動左指針收縮窗口。涵蓋了最大無重複子數組、最長無重複字符子串、刪除元素後全1子數組、最小長度子數組和乘積小於K子數組等多種情況，並提供了Python和Scheme兩種語言的代碼實現，展示了狀態記錄和指針一步到位等優化技巧。</description>
          <pubDate>Sun, 09 Nov 2025 06:06:48 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <h2 id="user-content-1695-删除子数组的最大得分" class="">1695. 删除子数组的最大得分<a class="" tabindex="-1" href="#1695-删除子数组的最大得分">#</a></h2><p>和<a href="/posts/fixed-size-sliding-window">上一篇</a>中介紹的定長滑動窗口不同，本題沒有說要找一個特定長度的子數組，如果要暴力遍歷所有可能的子數組，算法時間複雜度將爲​<code class="">O(n^2)</code>​。能否將之前提到的三步走套用到這個問題上呢？
</p><p>不妨先跟着例子​<code class="">[4,2,4,5,6]</code>​看下，我用括號表示窗口，嘗試只移動右指針增大窗口，用ans表示最大的符合條件的子數組長度：
</p><p>(4) 2 4 5 6 這個窗口表示的子數組是符合元素不重複的條件的，ans更新爲1
</p><p>(4 2) 4 5 6 子數組[4, 2]仍然符合條件，ans更新爲2
</p><p>(4 2 4) 5 6 現在窗口內包含重複數字了
</p><p>在這裏停下，思考一下，如果在這左指針依然不動，顯然，再移動右指針得到的子數組還是不符合條件的。換句話說，當左指針不動，而右指針移動到一個位置使得題目要求的條件不滿足時，所有以當前左指針開始，右端在當前右指針之後的子數組也同時全部被排除了！既然排除了繼續移動右指針的必要，現在來試試移動左指針吧。
</p><p>4 (2 4) 5 6 現在窗口符合條件了，再移動右指針試試
</p><p>4 (2 4 5) 6 仍然符合條件，ans更新爲3
</p><p>4 (2 4 5 6) 符合條件，ans更新爲4，右指針已經不能再動了，而再移動左指針得到的子數組只會更短，因此4就是最終答案
</p><p>我將這類問題總結爲兩步：
</p><ol><li>移動右指針，更新某個狀態，如果滿足條件更新答案，如果不滿足條件，執行步驟2
</li><li>移動左指針，更新某個狀態，如果滿足條件，執行步驟1，如果不滿足，重複步驟2
</li></ol><p>這種邏輯適合用命令式寫法表達，這裏用Python來演示：
</p><pre tabindex="0"><code><span><span>class</span><span> Solution</span><span>:</span></span>
<span><span>    def</span><span> maximumUniqueSubarray</span><span>(</span><span>self</span><span>,</span><span> nums</span><span>:</span><span> List</span><span>[</span><span>int</span><span>]</span><span>)</span><span> -></span><span> int</span><span>:</span></span>
<span><span>        # 用set來記錄是否重複</span></span>
<span><span>        record </span><span>=</span><span> set</span><span>()</span></span>
<span></span>
<span><span>        left </span><span>=</span><span> 0</span></span>
<span></span>
<span><span>        # 窗口內的和</span></span>
<span><span>        s </span><span>=</span><span> 0</span></span>
<span><span>        ans </span><span>=</span><span> 0</span></span>
<span></span>
<span><span>        # 實際上for i in nums就可以，這裏只是爲了清楚表示左右指針，用了right索引</span></span>
<span><span>        for</span><span> right </span><span>in</span><span> range</span><span>(</span><span>len</span><span>(</span><span>nums</span><span>)):</span></span>
<span><span>            current </span><span>=</span><span> nums</span><span>[</span><span>right</span><span>]</span></span>
<span><span>            # 當前右指針下值出現在record，說明有重複了</span></span>
<span><span>            while</span><span> current </span><span>in</span><span> record</span><span>:</span></span>
<span><span>                # 移動左指針，更新狀態</span></span>
<span><span>                record</span><span>.</span><span>remove</span><span>(</span><span>nums</span><span>[</span><span>left</span><span>])</span></span>
<span><span>                s </span><span>-=</span><span> nums</span><span>[</span><span>left</span><span>]</span></span>
<span><span>                left </span><span>+=</span><span> 1</span></span>
<span><span>            # 更新狀態，更新答案</span></span>
<span><span>            record</span><span>.</span><span>add</span><span>(</span><span>current</span><span>)</span></span>
<span><span>            s </span><span>+=</span><span> current</span></span>
<span><span>            ans </span><span>=</span><span> max</span><span>(</span><span>ans</span><span>,</span><span> s</span><span>)</span></span>
<span><span>        return</span><span> ans</span></span></code></pre><h2 id="user-content-3-无重复字符的最长子串" class="">3. 无重复字符的最长子串<a class="" tabindex="-1" href="#3-无重复字符的最长子串">#</a></h2><p>試着代入上一題所用的兩步，首先還是得有個狀態變量記錄是否有重複，這裏仍然用​<code class="">set</code>​，還有答案要求的是窗口長，可以用​<code class="">right - left + 1</code>​來算。
</p><pre tabindex="0"><code><span><span>class</span><span> Solution</span><span>:</span></span>
<span><span>    def</span><span> lengthOfLongestSubstring</span><span>(</span><span>self</span><span>,</span><span> s</span><span>:</span><span> str</span><span>)</span><span> -></span><span> int</span><span>:</span></span>
<span><span>        ans </span><span>=</span><span> left </span><span>=</span><span> 0</span></span>
<span><span>        record </span><span>=</span><span> set</span><span>()</span></span>
<span></span>
<span><span>        for</span><span> right</span><span>,</span><span> c </span><span>in</span><span> enumerate</span><span>(</span><span>s</span><span>):</span></span>
<span><span>            # 出現重複</span></span>
<span><span>            while</span><span> c </span><span>in</span><span> record</span><span>:</span></span>
<span><span>                # 更新狀態，移左指針</span></span>
<span><span>                record</span><span>.</span><span>remove</span><span>(</span><span>s</span><span>[</span><span>left</span><span>])</span></span>
<span><span>                left </span><span>+=</span><span> 1</span></span>
<span></span>
<span><span>            # 沒有重複，移右指針，更新record</span></span>
<span><span>            record</span><span>.</span><span>add</span><span>(</span><span>c</span><span>)</span><span>  # 加入 c</span></span>
<span></span>
<span><span>            # 更新答案</span></span>
<span><span>            ans </span><span>=</span><span> max</span><span>(</span><span>ans</span><span>,</span><span> right </span><span>-</span><span> left </span><span>+</span><span> 1</span><span>)</span></span>
<span></span>
<span><span>        return</span><span> ans</span></span></code></pre><p>但是只是針對這一題，可以想一想，當右側進窗口，出現重複時，說明必然是這個新進入窗口的字符和前面窗口內相同的字符重複了，那麼是否可以用一個表記錄窗口內每一個字符上一次出現的位置，出現重複時，左指針不就可以直接移動到重複字符上次出現位置的右邊了嗎？
</p><pre tabindex="0"><code><span><span>(</span><span>define</span><span> (</span><span>length-of-longest-substring</span><span> s</span><span>)</span></span>
<span><span>  ;; 題目規定了字符完全在ASCII範圍內，所以可以用128位的數組來記錄</span></span>
<span><span>  (</span><span>let</span><span> ((last-seen (make-vector </span><span>128</span><span> -1))</span></span>
<span><span>        (len (string-length s)))</span></span>
<span><span>    (</span><span>let</span><span> loop ((left </span><span>0</span><span>)</span></span>
<span><span>               (right </span><span>0</span><span>)</span></span>
<span><span>               (ans </span><span>0</span><span>))</span></span>
<span><span>      (</span><span>if</span><span> (</span><span>&#x3C;</span><span> right len)</span></span>
<span><span>          (</span><span>let*</span><span> ((current-char (string-ref s right))</span></span>
<span><span>                 ;; 字符轉成ASCII碼</span></span>
<span><span>                 (char-ascii (</span><span>char->integer</span><span> current-char))</span></span>
<span><span>                 (char-last-index (</span><span>vector-ref</span><span> last-seen char-ascii))</span></span>
<span><span>                 ;; 如果有重複，左指針可以一步到位移動到重複字符後面</span></span>
<span><span>                 (new-left (</span><span>max</span><span> left (</span><span>+</span><span> char-last-index </span><span>1</span><span>))))</span></span>
<span><span>            ;; 右入窗口，更新last-seen，更新答案</span></span>
<span><span>            (</span><span>vector-set!</span><span> last-seen char-ascii right)</span></span>
<span><span>            (loop new-left</span></span>
<span><span>                  (</span><span>+</span><span> right </span><span>1</span><span>)</span></span>
<span><span>                  (</span><span>max</span><span> ans (</span><span>+</span><span> (</span><span>-</span><span> right new-left) </span><span>1</span><span>))))</span></span>
<span><span>          ans))))</span></span></code></pre><h2 id="user-content-1493-删掉一个元素以后全为-1-的最长子数组" class="">1493. 删掉一个元素以后全为 1 的最长子数组<a class="" tabindex="-1" href="#1493-删掉一个元素以后全为-1-的最长子数组">#</a></h2><p>這一題就是要找一個最長的，最多有一個0的子數組，返回這個子數組長度減一。
狀態量可以是窗口內零的計數，不超過1時，移動右指針，否則移動左指針。
</p><pre tabindex="0"><code><span><span>class</span><span> Solution</span><span>:</span></span>
<span><span>    def</span><span> longestSubarray</span><span>(</span><span>self</span><span>,</span><span> nums</span><span>:</span><span> List</span><span>[</span><span>int</span><span>]</span><span>)</span><span> -></span><span> int</span><span>:</span></span>
<span><span>        ans </span><span>=</span><span> count </span><span>=</span><span> left </span><span>=</span><span> 0</span></span>
<span><span>        for</span><span> right</span><span>,</span><span> x </span><span>in</span><span> enumerate</span><span>(</span><span>nums</span><span>):</span></span>
<span><span>            # 移右，更新計數</span></span>
<span><span>            if</span><span> x </span><span>==</span><span> 0</span><span>:</span></span>
<span><span>                count </span><span>+=</span><span> 1</span></span>
<span><span>            while</span><span> count </span><span>></span><span> 1</span><span>:</span></span>
<span><span>                # 不滿足條件，移左，更新計數</span></span>
<span><span>                count </span><span>-=</span><span> (</span><span>1</span><span> if</span><span> nums</span><span>[</span><span>left</span><span>]</span><span> ==</span><span> 0</span><span> else</span><span> 0</span><span>)</span></span>
<span><span>                left </span><span>+=</span><span> 1</span></span>
<span><span>            ans </span><span>=</span><span> max</span><span>(</span><span>ans</span><span>,</span><span> right </span><span>-</span><span> left</span><span>)</span></span>
<span><span>        return</span><span> ans</span></span></code></pre><p>由於這一題要求是要麼有一個零，要麼沒有零，如果右指針下出現多餘的零，那麼左指針就要移動到窗口內上一個零的右邊。所以這一題也是可以一步更新左指針的：
</p><pre tabindex="0"><code><span><span>(</span><span>define</span><span> (</span><span>longest-subarray</span><span> nums</span><span>)</span></span>
<span><span>  (</span><span>let</span><span> ((len (</span><span>vector-length</span><span> nums)))</span></span>
<span><span>    (</span><span>let</span><span> loop ((last-zero -1) </span><span>; 記錄上一次出現0的位置</span></span>
<span><span>               (left </span><span>0</span><span>)</span></span>
<span><span>               (right </span><span>0</span><span>)</span></span>
<span><span>               (ans </span><span>0</span><span>))</span></span>
<span><span>      (</span><span>if</span><span> (</span><span>&#x3C;</span><span> right len)</span></span>
<span><span>          (</span><span>let*</span><span> ((current-zero? (</span><span>eq?</span><span> (</span><span>vector-ref</span><span> nums right) </span><span>0</span><span>))</span></span>
<span><span>                 ;; 不符合條件時直接將左指針移到上一次0的右邊去</span></span>
<span><span>                 (new-left (</span><span>if</span><span> current-zero?</span></span>
<span><span>                               (</span><span>max</span><span> (</span><span>+</span><span> last-zero </span><span>1</span><span>) left)</span></span>
<span><span>                               left)))</span></span>
<span><span>            (loop (</span><span>if</span><span> current-zero? right last-zero)</span></span>
<span><span>                  new-left</span></span>
<span><span>                  (</span><span>+</span><span> right </span><span>1</span><span>)</span></span>
<span><span>                  (</span><span>max</span><span> ans (</span><span>-</span><span> right new-left))))</span></span>
<span><span>          ans))))</span></span></code></pre><h2 id="user-content-209-长度最小的子数组" class="">209. 长度最小的子数组<a class="" tabindex="-1" href="#209-长度最小的子数组">#</a></h2><p>問題從求符合條件的最長的子數組，變成了求符合條件的最短子數組，但是解題思路其實還是一樣的。只是具體步驟變爲：
</p><ol><li>移動窗口右指針，更新窗口內和，不滿足條件，重複步驟1，符合條件，執行步驟2
</li><li>移動左指針，窗口和減去左側值，更新答案（窗口長度），直到不符合條件時再執行步驟1
</li></ol><pre tabindex="0"><code><span><span>class</span><span> Solution</span><span>:</span></span>
<span><span>    def</span><span> minSubArrayLen</span><span>(</span><span>self</span><span>,</span><span> target</span><span>:</span><span> int</span><span>,</span><span> nums</span><span>:</span><span> List</span><span>[</span><span>int</span><span>]</span><span>)</span><span> -></span><span> int</span><span>:</span></span>
<span><span>        n </span><span>=</span><span> len</span><span>(</span><span>nums</span><span>)</span></span>
<span><span>        impossible </span><span>=</span><span> n </span><span>+</span><span> 1</span></span>
<span></span>
<span><span>        ans </span><span>=</span><span> impossible</span></span>
<span><span>        s </span><span>=</span><span> left </span><span>=</span><span> 0</span></span>
<span></span>
<span><span>        for</span><span> right</span><span>,</span><span> x </span><span>in</span><span> enumerate</span><span>(</span><span>nums</span><span>):</span></span>
<span><span>            s </span><span>+=</span><span> x</span></span>
<span><span>            while</span><span> s </span><span>>=</span><span> target</span><span>:</span></span>
<span><span>                ans </span><span>=</span><span> min</span><span>(</span><span>ans</span><span>,</span><span> right </span><span>-</span><span> left </span><span>+</span><span> 1</span><span>)</span></span>
<span><span>                s </span><span>-=</span><span> nums</span><span>[</span><span>left</span><span>]</span></span>
<span><span>                left </span><span>+=</span><span> 1</span></span>
<span></span>
<span><span>        return</span><span> 0</span><span> if</span><span> ans </span><span>==</span><span> impossible </span><span>else</span><span> ans</span></span></code></pre><p>題外話，每次求最小值時，都會將答案初始化爲無窮大或一個不可能達到的最大值；求最大值時則反過來初始化爲無窮小，這種特殊值在數學上叫作​<strong>單位元</strong>​。簡單說這種值和另一個值a做運算，總是得到a。
</p><h2 id="user-content-713-乘积小于-k-的子数组" class="">713. 乘积小于 K 的子数组<a class="" tabindex="-1" href="#713-乘积小于-k-的子数组">#</a></h2><p>這次要求的是符合條件的子數組數量，思路是一貫的，先移動右指針，窗口积超過了K，移動左指針。當窗口符合條件時，右指針不變，顯然以當窗口左端點右邊直到右端點的任一點做左端點得到的子數組都是符合條件的子數組，也就是說答案就是當前窗口的長度​<code class="">right - left + 1</code>​。
</p><pre tabindex="0"><code><span><span>class</span><span> Solution</span><span>:</span></span>
<span><span>    def</span><span> numSubarrayProductLessThanK</span><span>(</span><span>self</span><span>,</span><span> nums</span><span>:</span><span> List</span><span>[</span><span>int</span><span>],</span><span> k</span><span>:</span><span> int</span><span>)</span><span> -></span><span> int</span><span>:</span></span>
<span><span>        # 排除特殊情況</span></span>
<span><span>        if</span><span> k </span><span>&#x3C;=</span><span> 1</span><span>:</span></span>
<span><span>            return</span><span> 0</span></span>
<span></span>
<span><span>        product </span><span>=</span><span> 1</span></span>
<span><span>        ans </span><span>=</span><span> left </span><span>=</span><span> 0</span></span>
<span><span>        for</span><span> right</span><span>,</span><span> x </span><span>in</span><span> enumerate</span><span>(</span><span>nums</span><span>):</span></span>
<span><span>            product </span><span>*=</span><span> x</span></span>
<span><span>            while</span><span> product </span><span>>=</span><span> k</span><span>:</span></span>
<span><span>                product </span><span>//=</span><span> nums</span><span>[</span><span>left</span><span>]</span></span>
<span><span>                left </span><span>+=</span><span> 1</span></span>
<span><span>            ans </span><span>+=</span><span> right </span><span>-</span><span> left </span><span>+</span><span> 1</span></span>
<span><span>        return</span><span> ans</span></span></code></pre>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>完全掌握定長滑動窗口</title>
          <link>https://elliot00.com/posts/fixed-size-sliding-window</link>
          <description>本文通过多个LeetCode题目实例，系统讲解了滑动窗口算法的应用。文章详细展示了如何在不同场景下使用滑动窗口的三步走套路：右指针进窗口、更新答案、左指针出窗口。涵盖了定长窗口、变长思想、循环数组处理等多种情况，并提供了完整的Scheme代码实现。</description>
          <pubDate>Sat, 01 Nov 2025 09:33:30 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <h2 id="user-content-1456-定长子串中元音的最大数目" class="">1456. 定长子串中元音的最大数目<a class="" tabindex="-1" href="#1456-定长子串中元音的最大数目">#</a></h2><p>根據題目要求，需要從字符串s中找出長度爲k的子串中可能包含的最大元音字母數，最容易想到的方法就是依次遍歷所有可能的子字符串，統計每個子字符串的元音數，比較得出最大值。這樣做的時間複雜度是​<code class="">O(kn)</code>​，有沒有可能做到​<code class="">O(n)</code>​？
</p><h3 id="user-content-思路">思路<a class="" tabindex="-1" href="#思路">#</a></h3><p><img alt="滑動窗口" src="https://r2.elliot00.com/algorithm/sliding-window.png" width="512" height="344"></p><p>如圖所示，如果題目要求子串長度是3，如果先遍歷第一個子串“abc”，可以得出這個子串中有一個元音“a”，記錄這個結果；第二個子串是“bci”，再遍歷這個子串，發現最後一個字母“i”是元音，問題出現了，在上一次遍歷中已經知道了前兩個字母“bc”中沒有元音，怎麼節省重複的遍歷？
</p><p>假設現在剛剛完成第一個子串的遍歷，這時我們得到的答案是1，如果把整個字符串想象成一個滑軌，在這個滑軌上有一個長度（或寬度？）爲3的窗戶，它最右邊是“c”，最左邊是“a”，窗戶內的字符已經統計過，現在將窗戶向右推動一格，最右邊就新加入了字符“i”，而最左邊則離開了字符“a”。
</p><p>可以發現在第一個子串之後，可以像移動窗戶一樣，窗戶的左右兩端同時向右移動，每次移動，如果右邊是元音，將計數加一，如果左邊是元音，因爲它離開了窗口，所以將計數減一。等到窗口最右邊到了原字符串的末尾就可以停止了。相當於只遍歷一次，時間複雜度爲​<code class="">O(n)</code>​。
</p><p>具體到代碼裏，可以用兩個指針表示這個窗口的左右端，滑動窗口的步驟爲：
</p><ol><li>右邊進窗口：判斷當前右指針指向的字符是否是元音，更新局部計數，右移指針。如果窗口大小不足，重複第1步
</li><li>更新答案：在本題更新方法是​<code class="">ans = max(ans, local_count)</code></li><li>左邊出窗口：更新局部計數（是元音，減一，否則不變），左指針右移
</li></ol><h3 id="user-content-代码">代码<a class="" tabindex="-1" href="#代码">#</a></h3><p>具體用代碼來表示這個過程：
</p><pre tabindex="0"><code><span><span>(</span><span>define</span><span> (</span><span>max-vowels</span><span> s k</span><span>)</span></span>
<span><span>  (</span><span>define</span><span> (</span><span>vowel?</span><span> c</span><span>)</span></span>
<span><span>    (</span><span>member</span><span> c </span><span>'</span><span>(</span><span>#\a</span><span> #\e</span><span> #\i</span><span> #\o</span><span> #\u</span><span>)))</span></span>
<span></span>
<span><span>  (</span><span>let</span><span> loop ((ans </span><span>0</span><span>) </span><span>; 答案</span></span>
<span><span>             (vowel </span><span>0</span><span>) </span><span>; 窗口內的元音計數</span></span>
<span><span>             (index </span><span>0</span><span>)) </span><span>; 窗口右端</span></span>
<span><span>    (</span><span>if</span><span> (</span><span>&#x3C;</span><span> index (string-length s))</span></span>
<span><span>      (</span><span>let*</span><span> ((window-not-full? (</span><span>&#x3C;</span><span> index (</span><span>-</span><span> k </span><span>1</span><span>)))</span></span>
<span><span>             (current-vowel? (vowel? (string-ref s index)))</span></span>
<span><span>             (new-vowel (</span><span>if</span><span> current-vowel? (</span><span>+</span><span> vowel </span><span>1</span><span>) vowel)))</span></span>
<span><span>        (</span><span>if</span><span> window-not-full? </span><span>; 檢查窗口是否展開</span></span>
<span><span>          (loop ans new-vowel (</span><span>+</span><span> index </span><span>1</span><span>)) </span><span>; 窗口未展開，重複第一步</span></span>
<span><span>          (loop (</span><span>max</span><span> ans new-vowel) </span><span>; 更新答案</span></span>
<span><span>                ;; 左出，如果左端是元音，需要將窗口內元音計數減一</span></span>
<span><span>                (</span><span>if</span><span> (vowel? (string-ref s (</span><span>+</span><span> (</span><span>-</span><span>  index k) </span><span>1</span><span>)))</span></span>
<span><span>                    (</span><span>-</span><span> new-vowel </span><span>1</span><span>)</span></span>
<span><span>                    new-vowel)</span></span>
<span><span>                (</span><span>+</span><span> index </span><span>1</span><span>))))</span></span>
<span><span>      ans)))</span></span></code></pre><p>實際代碼裏可以不用維持兩個指針變量，因爲窗口大小是固定的，右指針減窗口大小再加一就是左指針了。
</p><h2 id="user-content-643-子数组最大平均数-i" class="">643. 子数组最大平均数 I<a class="" tabindex="-1" href="#643-子数组最大平均数-i">#</a></h2><h3 id="user-content-思路-1">思路<a class="" tabindex="-1" href="#思路-1">#</a></h3><p>完成上一題後，可以試試這一題是否能套進之前滑動窗口的套路中去。另外還有個小技巧，由於k是固定的，要求的是平均值最大，那麼可以不必在每個窗口下求平均值，只要找出最大的窗口和，最後再除以k算平均值就可以。
</p><ol><li>右進：更新局部和，加上右端值，右移指針。如果窗口大小不足，重複第1步
</li><li>更新答案：比較窗口內的和與全局最大和，保留更大的一個
</li><li>左出：窗口和減去左端值，左指針右移
</li></ol><h3 id="user-content-代碼">代碼<a class="" tabindex="-1" href="#代碼">#</a></h3><pre tabindex="0"><code><span><span>(</span><span>define</span><span> (</span><span>find-max-average</span><span> nums k</span><span>)</span></span>
<span><span>  (</span><span>let</span><span> loop ((max-sum -inf.0) </span><span>; 全局最大和</span></span>
<span><span>             (s </span><span>0</span><span>) </span><span>; 窗口內的和</span></span>
<span><span>             (i </span><span>0</span><span>)) </span><span>; 右指針</span></span>
<span><span>    (</span><span>if</span><span> (</span><span>&#x3C;</span><span> i (</span><span>vector-length</span><span> nums))</span></span>
<span><span>        (</span><span>let</span><span> ((new-s (</span><span>+</span><span> s (</span><span>vector-ref</span><span> nums i)))</span></span>
<span><span>              (window-not-full? (</span><span>&#x3C;</span><span> i (</span><span>-</span><span> k </span><span>1</span><span>))))</span></span>
<span><span>          (</span><span>if</span><span> window-not-full?</span></span>
<span><span>              (loop max-sum new-s (</span><span>+</span><span> i </span><span>1</span><span>)) </span><span>; 窗口未滿</span></span>
<span><span>              (loop</span></span>
<span><span>               (</span><span>max</span><span> max-sum new-s) </span><span>; 更新最大和</span></span>
<span><span>               (</span><span>-</span><span> new-s (</span><span>vector-ref</span><span> nums (</span><span>+</span><span> (</span><span>-</span><span> i k) </span><span>1</span><span>))) </span><span>; 左端點離開窗口</span></span>
<span><span>               (</span><span>+</span><span> i </span><span>1</span><span>))))</span></span>
<span><span>      (</span><span>/</span><span> max-sum k))))</span></span></code></pre><h2 id="user-content-2379-得到-k-个黑块的最少涂色次数" class="">2379. 得到 K 个黑块的最少涂色次数<a class="" tabindex="-1" href="#2379-得到-k-个黑块的最少涂色次数">#</a></h2><h3 id="user-content-思路-2">思路<a class="" tabindex="-1" href="#思路-2">#</a></h3><p>首先分析題目可知，本質上是要數窗口內字符‘W’的數量，找出最少的窗口。繼續往三步走套：
</p><ol><li>右進：如果右端是W，更新局部計數，右移指針。如果窗口大小不足，重複第1步
</li><li>更新答案：比較窗口內的計數和全局計數，保留更小的一個
</li><li>左出：如果左端是W，局部計數減一，否則不變，左端右移。
</li></ol><h3 id="user-content-代碼-1">代碼<a class="" tabindex="-1" href="#代碼-1">#</a></h3><pre tabindex="0"><code><span><span>(</span><span>define</span><span> (</span><span>minimum-recolors</span><span> blocks k</span><span>)</span></span>
<span><span>  (</span><span>let</span><span> loop ((min-w-count +inf.0) </span><span>; 初始值設成正無窮，方便比較</span></span>
<span><span>             (w-count     </span><span>0</span><span>)</span></span>
<span><span>             (i           </span><span>0</span><span>))</span></span>
<span><span>    (</span><span>if</span><span> (</span><span>&#x3C;</span><span> i (string-length blocks))</span></span>
<span><span>        ;; 右進</span></span>
<span><span>        (</span><span>let</span><span> ((local-w-count (</span><span>if</span><span> (</span><span>char=?</span><span> (string-ref blocks i) </span><span>#\W</span><span>)</span></span>
<span><span>                                 (</span><span>+</span><span> w-count </span><span>1</span><span>)</span></span>
<span><span>                                 w-count)))</span></span>
<span><span>          (</span><span>if</span><span> (</span><span>&#x3C;</span><span> i (</span><span>-</span><span> k </span><span>1</span><span>)) </span><span>; 判斷窗口大小是否足夠</span></span>
<span><span>              (loop min-w-count local-w-count (</span><span>+</span><span> i </span><span>1</span><span>))</span></span>
<span><span>              (loop (</span><span>min</span><span> min-w-count local-w-count) </span><span>; 更新答案</span></span>
<span><span>                    ;; 左出</span></span>
<span><span>                    (</span><span>if</span><span> (</span><span>char=?</span><span> (string-ref blocks (</span><span>+</span><span> (</span><span>-</span><span> i k) </span><span>1</span><span>)) </span><span>#\W</span><span>)</span></span>
<span><span>                        (</span><span>-</span><span> local-w-count </span><span>1</span><span>)</span></span>
<span><span>                        local-w-count)</span></span>
<span><span>                    (</span><span>+</span><span> i </span><span>1</span><span>))))</span></span>
<span><span>        min-w-count)))</span></span></code></pre><h2 id="user-content-1423-可获得的最大点数" class="">1423. 可获得的最大点数<a class="" tabindex="-1" href="#1423-可获得的最大点数">#</a></h2><h3 id="user-content-思路-3">思路<a class="" tabindex="-1" href="#思路-3">#</a></h3><p>題目要求只能從開頭或末尾取走牌，似乎和固定窗口滑動沒有關係了，但是如果反過來想，那就是要找一個大小爲n-k的定長窗口，窗口內數的和最小，這就相當於是從前後拿走了和最大的牌了。
</p><p>三步走：
</p><ol><li>右進：更新局部計數，右移指針。如果窗口大小不足，重複第1步
</li><li>更新答案：比較窗口內的計數和全局計數，保留更小的一個
</li><li>左出：局部計數減去左端值，左指針右移。
</li></ol><p>給讀者一個小挑戰，嘗試獨立寫出代碼
</p><h3 id="user-content-思路二">思路二<a class="" tabindex="-1" href="#思路二">#</a></h3><p>在反向思考後，可以再換個思路
</p><p>考慮所有可能的取法：
</p><ul><li>取前k張牌
</li><li>前k-1個加後1個
</li><li>前k-2個加後2個
</li><li>……
</li><li>前2個加後k-2個
</li><li>前1個加後k-1個
</li><li>後k個
</li></ul><p>生活中你可能見過那種圓柱浴室，裏面的窗帘可以繞着圈子拉，把題目中的數組想象成首尾相連的軌道，一個k長的窗帘在這個軌道上滑動。但是，第一次展開窗口後，窗口是向“左邊”移動的。
</p><p>用代碼去描述，可以先求出前k個數的和，從i=1開始枚舉到i=k，每次將當前和增加 <code class="">cardPoints[n - i] - cardPoints[k - i]</code></p><h3 id="user-content-代碼-2">代碼<a class="" tabindex="-1" href="#代碼-2">#</a></h3><pre tabindex="0"><code><span><span>(</span><span>define</span><span> (</span><span>max-score</span><span> cardPoints k</span><span>)</span></span>
<span><span>  (</span><span>let</span><span> ((n (</span><span>vector-length</span><span> cardPoints))</span></span>
<span><span>        (init-sum (</span><span>let</span><span> loop ((i </span><span>0</span><span>)</span></span>
<span><span>                             (s </span><span>0</span><span>))</span></span>
<span><span>                    (</span><span>if</span><span> (</span><span>eq?</span><span> i k)</span></span>
<span><span>                        s</span></span>
<span><span>                        (loop (</span><span>+</span><span> i </span><span>1</span><span>) (</span><span>+</span><span> s (</span><span>vector-ref</span><span> cardPoints i)))))))</span></span>
<span><span>    (</span><span>let</span><span> loop ((i </span><span>1</span><span>)</span></span>
<span><span>               (cur-sum init-sum)</span></span>
<span><span>               (max-sum init-sum))</span></span>
<span><span>      (</span><span>if</span><span> (</span><span>></span><span> i k)</span></span>
<span><span>          max-sum</span></span>
<span><span>          (</span><span>let</span><span> ((new-sum (</span><span>+</span><span> (</span><span>-</span><span> cur-sum (</span><span>vector-ref</span><span> cardPoints (</span><span>-</span><span> k i)))</span></span>
<span><span>                            (</span><span>vector-ref</span><span> cardPoints (</span><span>-</span><span> n i)))))</span></span>
<span><span>            (loop (</span><span>+</span><span> i </span><span>1</span><span>)</span></span>
<span><span>                  new-sum</span></span>
<span><span>                  (</span><span>max</span><span> max-sum new-sum)))))))</span></span></code></pre><h2 id="user-content-2090-半径为-k-的子数组平均值" class="">2090. 半径为 k 的子数组平均值<a class="" tabindex="-1" href="#2090-半径为-k-的子数组平均值">#</a></h2><h3 id="user-content-思路-4">思路<a class="" tabindex="-1" href="#思路-4">#</a></h3><p>仍然使用三步走套路，但是要注意窗口大小不是題目中的k，左右指針和k的關係要注意
</p><ol><li>右進：更新局部和，右移指針。如果窗口大小不足，重複第1步
</li><li>更新答案：更新到答案數組的​<code class="">i - k</code>​位
</li><li>左出：局部計數減去左端值，左指針右移。
</li></ol><h3 id="user-content-代碼-3">代碼<a class="" tabindex="-1" href="#代碼-3">#</a></h3><pre tabindex="0"><code><span><span>(</span><span>define</span><span> (</span><span>get-averages</span><span> nums k</span><span>)</span></span>
<span><span>  (</span><span>let*</span><span> ((len (</span><span>vector-length</span><span> nums))</span></span>
<span><span>         (win-size (</span><span>+</span><span> (</span><span>*</span><span> k </span><span>2</span><span>) </span><span>1</span><span>))</span></span>
<span><span>         (ans (make-vector len -1)))</span></span>
<span><span>    (</span><span>let</span><span> loop ((index </span><span>0</span><span>)</span></span>
<span><span>               (pre-sum </span><span>0</span><span>))</span></span>
<span><span>      (</span><span>if</span><span> (</span><span>&#x3C;</span><span> index len)</span></span>
<span><span>          (</span><span>let</span><span> ((sum (</span><span>+</span><span> pre-sum (</span><span>vector-ref</span><span> nums index)))) </span><span>; 右側進窗口，記錄和</span></span>
<span><span>            (</span><span>if</span><span> (</span><span>&#x3C;</span><span> index (</span><span>*</span><span> k </span><span>2</span><span>))</span></span>
<span><span>                (loop (</span><span>+</span><span> index </span><span>1</span><span>) sum) </span><span>; 構建窗口</span></span>
<span><span>                (</span><span>begin</span></span>
<span><span>                  (</span><span>vector-set!</span><span> ans (</span><span>-</span><span> index k) (</span><span>/</span><span> sum win-size)) </span><span>; 更新答案</span></span>
<span><span>                  (loop (</span><span>+</span><span> index </span><span>1</span><span>)</span></span>
<span><span>                        ;; 出窗口</span></span>
<span><span>                        (</span><span>-</span><span> sum (</span><span>vector-ref</span><span> nums (</span><span>-</span><span> index (</span><span>*</span><span> k </span><span>2</span><span>))))))))</span></span>
<span><span>          ans))))</span></span></code></pre><h2 id="user-content-1052-爱生气的书店老板" class="">1052. 爱生气的书店老板<a class="" tabindex="-1" href="#1052-爱生气的书店老板">#</a></h2><h3 id="user-content-思路-5">思路<a class="" tabindex="-1" href="#思路-5">#</a></h3><p>由題目知，grumpy爲0時，顧客一定滿意，老板使用控制技能，在minutes內不生氣，只會使結果加上grumpy爲1對應的顧客數，那麼如果算出grumpy爲0的顧客數，再滑動窗口，找出grumpy爲1的顧客數最多的窗口，加起來就得到結果。
</p><h3 id="user-content-代碼-4">代碼<a class="" tabindex="-1" href="#代碼-4">#</a></h3><p>實作可以不用先遍歷一次求在不控制情緒情況下的顧客和，因爲只需要在最後用到這個值，在滑動窗口的同時累加就可以
</p><pre tabindex="0"><code><span><span>(</span><span>define</span><span> (</span><span>max-satisfied</span><span> customers grumpy minutes</span><span>)</span></span>
<span><span>  (</span><span>let</span><span> ((len (</span><span>vector-length</span><span> customers)))</span></span>
<span><span>    (</span><span>let</span><span> loop ((origin-sum </span><span>0</span><span>) </span><span>; 記錄不控制情緒情況下，滿意顧客數</span></span>
<span><span>               (extra-sum </span><span>0</span><span>) </span><span>; 記錄控制情緒所能獲得的最大顧客數</span></span>
<span><span>               (s </span><span>0</span><span>) </span><span>; 當前窗口通過控制情緒可以使其滿意的顧客數</span></span>
<span><span>               (i </span><span>0</span><span>))</span></span>
<span><span>      (</span><span>if</span><span> (</span><span>&#x3C;</span><span> i len)</span></span>
<span><span>          ;; 進窗口</span></span>
<span><span>          (</span><span>let*</span><span> ((angry? (</span><span>eq?</span><span> (</span><span>vector-ref</span><span> grumpy i) </span><span>1</span><span>))</span></span>
<span><span>                 (current-customers (</span><span>vector-ref</span><span> customers i))</span></span>
<span><span>                 (new-origin-sum (</span><span>+</span><span> origin-sum (</span><span>if</span><span> angry? </span><span>0</span><span> current-customers))) </span><span>; 如果沒生氣，更新origin-sum</span></span>
<span><span>                 (new-s (</span><span>+</span><span> s (</span><span>if</span><span> angry? current-customers </span><span>0</span><span>)))) </span><span>; 如果生氣，更新s</span></span>
<span><span>            (</span><span>if</span><span> (</span><span>&#x3C;</span><span> i (</span><span>-</span><span> minutes </span><span>1</span><span>))</span></span>
<span><span>                ;; 展開窗口</span></span>
<span><span>                (loop new-origin-sum</span></span>
<span><span>                      origin-sum</span></span>
<span><span>                      new-s</span></span>
<span><span>                      (</span><span>+</span><span> i </span><span>1</span><span>))</span></span>
<span><span>                (loop new-origin-sum</span></span>
<span><span>                      ;; 更新答案</span></span>
<span><span>                      (</span><span>max</span><span> extra-sum new-s)</span></span>
<span><span>                      ;; 出窗口</span></span>
<span><span>                      (</span><span>if</span><span> (</span><span>eq?</span><span> (</span><span>vector-ref</span><span> grumpy (</span><span>+</span><span> (</span><span>-</span><span> i minutes) </span><span>1</span><span>)) </span><span>1</span><span>)</span></span>
<span><span>                          (</span><span>-</span><span> new-s (</span><span>vector-ref</span><span> customers (</span><span>+</span><span> (</span><span>-</span><span> i minutes) </span><span>1</span><span>)))</span></span>
<span><span>                          new-s)</span></span>
<span><span>                      (</span><span>+</span><span> i </span><span>1</span><span>))))</span></span>
<span><span>          (</span><span>+</span><span> origin-sum extra-sum)))))</span></span></code></pre><h2 id="user-content-1652-拆炸弹" class="">1652. 拆炸弹<a class="" tabindex="-1" href="#1652-拆炸弹">#</a></h2><h3 id="user-content-思路-6">思路<a class="" tabindex="-1" href="#思路-6">#</a></h3><p>這一題乍一看好像不知道怎麼去構建窗口，沒關係，先列出例子中所有的索引，試着找出關係
</p><p>n = 4, k = 3:
</p><table><thead><tr><th>i</th><th>a</th><th>b</th><th>c</th></tr></thead><tbody><tr><td>0</td><td>1</td><td>2</td><td>3</td></tr><tr><td>1</td><td>2</td><td>3</td><td>0</td></tr><tr><td>2</td><td>3</td><td>0</td><td>1</td></tr><tr><td>3</td><td>0</td><td>1</td><td>2</td></tr></tbody></table><p>看到循環數組，首先想到應該有模運算，可以發現， </p><p>如果把a到c看作一個滑動窗口，按從左往右滑動排序後表格如下：
</p><table><thead><tr><th>window</th><th>i（答案第i位）</th></tr></thead><tbody><tr><td>(0, 1, 2)</td><td>3</td></tr><tr><td>(1, 2, 3)</td><td>0</td></tr><tr><td>(2, 3, 0)</td><td>1</td></tr><tr><td>(3, 0, 1)</td><td>2</td></tr></tbody></table><p>那在滑動過程中，能從右指針推出應該更新答案的第幾位嗎？可以的，按前面c也就是右指針和i的關係，做逆運算： </p><p>再看看題目裏例子三k = -2的情況，是否有不同
</p><p>n = 4, k = -2
</p><p>k是負數，但是窗口長度（或寬度）肯定只能是正數2，還是一樣從左往右滑動
</p><table><thead><tr><th>window</th><th>i（答案第i位）</th></tr></thead><tbody><tr><td>(0, 1)</td><td>2</td></tr><tr><td>(1, 2)</td><td>3</td></tr><tr><td>(2, 3)</td><td>0</td></tr><tr><td>(3, 0)</td><td>1</td></tr></tbody></table><p>因爲題目規定k爲負數時，算當前索引前k個數的和，所以右指針加1就是索引，但注意不能溢出，還是需要模運算： </p><p>這樣一來問題就簡單了，除了k等於0時直接返回全0數組，其他情況只要維護一個長爲​<code class="">abs(k)</code>​的窗口，向右滑動，根據右端點索引和答案索引的關係，去更新相應的答案就可以了。
</p><h3 id="user-content-代碼-5">代碼<a class="" tabindex="-1" href="#代碼-5">#</a></h3><pre tabindex="0"><code><span><span>(</span><span>define</span><span> (</span><span>decrypt</span><span> code k</span><span>)</span></span>
<span><span>  (</span><span>let*</span><span> ((n (</span><span>vector-length</span><span> code))</span></span>
<span><span>         (ans (make-vector n </span><span>0</span><span>))</span></span>
<span><span>         (abs-k (</span><span>abs</span><span> k)))</span></span>
<span><span>    (</span><span>if</span><span> (</span><span>eq?</span><span> k </span><span>0</span><span>)</span></span>
<span><span>        ans </span><span>; k等於0直接返回全0數組</span></span>
<span><span>        (</span><span>let</span><span> loop ((sum </span><span>0</span><span>)</span></span>
<span><span>                   (right </span><span>0</span><span>)</span></span>
<span><span>                   (left </span><span>0</span><span>))</span></span>
<span><span>          (</span><span>if</span><span> (</span><span>&#x3C;</span><span> left n)</span></span>
<span><span>              ;; 進窗口</span></span>
<span><span>              (</span><span>let</span><span> ((new-sum(</span><span>+</span><span> sum (</span><span>vector-ref</span><span> code (modulo right n)))))</span></span>
<span><span>                (</span><span>if</span><span> (</span><span>&#x3C;</span><span> right (</span><span>-</span><span> abs-k </span><span>1</span><span>))</span></span>
<span><span>                    (loop new-sum</span></span>
<span><span>                          (</span><span>+</span><span> right </span><span>1</span><span>)</span></span>
<span><span>                          left) </span><span>; 先展開窗口</span></span>
<span><span>                    (</span><span>begin</span></span>
<span><span>                      ;; 更新答案</span></span>
<span><span>                      (</span><span>vector-set!</span><span> ans</span></span>
<span><span>                                   (</span><span>if</span><span> (</span><span>></span><span> k </span><span>0</span><span>) </span><span>; 根據k和右指針決定更新答案數組哪一位</span></span>
<span><span>                                       (modulo (</span><span>-</span><span> right k) n)</span></span>
<span><span>                                       (modulo (</span><span>+</span><span> right </span><span>1</span><span>) n))</span></span>
<span><span>                                   new-sum)</span></span>
<span><span>                      ;; 出窗口</span></span>
<span><span>                      (loop (</span><span>-</span><span> new-sum (</span><span>vector-ref</span><span> code left))</span></span>
<span><span>                            (</span><span>+</span><span> right </span><span>1</span><span>)</span></span>
<span><span>                            (</span><span>+</span><span> left </span><span>1</span><span>)))))</span></span>
<span><span>              ans)))))</span></span></code></pre>
mjx-container[jax="SVG"] {
  direction: ltr;
}

mjx-container[jax="SVG"] > svg {
  overflow: visible;
  min-height: 1px;
  min-width: 1px;
}

mjx-container[jax="SVG"] > svg a {
  fill: blue;
  stroke: blue;
}

mjx-container[jax="SVG"][display="true"] {
  display: block;
  text-align: center;
  margin: 1em 0;
}

mjx-container[jax="SVG"][display="true"][width="full"] {
  display: flex;
}

mjx-container[jax="SVG"][justify="left"] {
  text-align: left;
}

mjx-container[jax="SVG"][justify="right"] {
  text-align: right;
}

g[data-mml-node="merror"] > g {
  fill: red;
  stroke: red;
}

g[data-mml-node="merror"] > rect[data-background] {
  fill: yellow;
  stroke: none;
}

g[data-mml-node="mtable"] > line[data-line], svg[data-table] > g > line[data-line] {
  stroke-width: 70px;
  fill: none;
}

g[data-mml-node="mtable"] > rect[data-frame], svg[data-table] > g > rect[data-frame] {
  stroke-width: 70px;
  fill: none;
}

g[data-mml-node="mtable"] > .mjx-dashed, svg[data-table] > g > .mjx-dashed {
  stroke-dasharray: 140;
}

g[data-mml-node="mtable"] > .mjx-dotted, svg[data-table] > g > .mjx-dotted {
  stroke-linecap: round;
  stroke-dasharray: 0,140;
}

g[data-mml-node="mtable"] > g > svg {
  overflow: visible;
}

[jax="SVG"] mjx-tool {
  display: inline-block;
  position: relative;
  width: 0;
  height: 0;
}

[jax="SVG"] mjx-tool > mjx-tip {
  position: absolute;
  top: 0;
  left: 0;
}

mjx-tool > mjx-tip {
  display: inline-block;
  padding: .2em;
  border: 1px solid #888;
  font-size: 70%;
  background-color: #F8F8F8;
  color: black;
  box-shadow: 2px 2px 5px #AAAAAA;
}

g[data-mml-node="maction"][data-toggle] {
  cursor: pointer;
}

mjx-status {
  display: block;
  position: fixed;
  left: 1em;
  bottom: 1em;
  min-width: 25%;
  padding: .2em .4em;
  border: 1px solid #888;
  font-size: 90%;
  background-color: #F8F8F8;
  color: black;
}

foreignObject[data-mjx-xml] {
  font-family: initial;
  line-height: normal;
  overflow: visible;
}

mjx-container[jax="SVG"] path[data-c], mjx-container[jax="SVG"] use[data-c] {
  stroke-width: 3;
}

          ]]>
          </content:encoded>
        </item>
<item>
          <title>探索Emacs字體設定</title>
          <link>https://elliot00.com/posts/emacs-font-detail</link>
          <description>这篇文章分享了作者在 Emacs 中配置 Maple Mono NF CN 等宽字体时发现的误区与解决方案，详解了 set-face-attribute 的限制与 Fontset 的正确用法，旨在帮助读者实现更灵活、无卡顿的中英文字体配置。</description>
          <pubDate>Sun, 21 Sep 2025 10:06:23 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <p>作爲一個顏值黨，折騰字體是少不了的，上周我寫了篇博客總結了一些字體配置的方案，但就在幾天前我嘗試使用Maple Mono NF CN字體時，卻發現我對Emacs的字體配置存在一些誤解。爲了能更好地配置字體，我重新閱讀了相關的文檔，並嘗試了更多的配置組合，本文將記錄我的一些發現。
</p><p>我使用的環境：
</p><ul><li>Emacs 30.2
</li><li>macOS 15.4
</li></ul><h2 id="user-content-set-face-attribute的限制" class="">set-face-attribute的限制<a class="" tabindex="-1" href="#set-face-attribute的限制">#</a></h2><p>前面提到，我打算在Emacs中使用Maple Mono NF CN這款中英文2:1等寬的字體，於是我簡單使用了以下配置：
</p><pre tabindex="0"><code><span><span>(</span><span>set-face-attribute</span><span> 'default</span><span> nil</span><span> :font </span><span>"</span><span>Maple Mono NF CN</span><span>"</span><span> :height </span><span>180</span><span>)</span></span></code></pre><p>當我打開Emacs，看上去似乎設置成功了，可以當我輸入中文時，卻感覺一陣明顯的卡頓，接着出現的中文字符並不是我所設置的字體的樣子，可是這個字體確實是包含中文字符的，在其他GUI應用中可以驗證這點，問題出在哪呢？
</p><div><div><div></div><div>Tip</div></div><div><p>使用​<code class="">M-x describe-char</code>​可以查看單個字符的詳細信息。
</p></div></div><p>查詢​set-face-attribute​的文檔沒能找到答案，最終在這篇<a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/Face-Attributes.html">文檔</a>中發現了端倪：
</p><blockquote><p>:font
</p><p>    The font used to display the face. Its value should be a font object or a fontset. If it is a font object, it specifies the font to be used by the face for displaying ASCII characters. See Low-Level Font Representation, for information about font objects, font specs, and font entities. See Fontsets, for information about fontsets.
</p></blockquote><p>也就是說，如果這個參數不是設置爲一個​<strong>fontset</strong>​，那麼Emacs只會用指定的字體來顯示​<strong>ASCII</strong>​字符。在<a href="/posts/emacs-font-setup">上篇文章</a>中我提到了爲中英文分別設置字體的方法：
</p><pre tabindex="0"><code><span><span>(</span><span>set-face-attribute</span><span> 'default</span><span> nil</span><span> :family </span><span>"</span><span>Maple Mono NF</span><span>"</span><span> :height </span><span>180</span><span>)</span></span>
<span><span>(</span><span>set-fontset-font</span><span> t </span><span>'han</span><span> (</span><span>font-spec</span><span> :family </span><span>"</span><span>LXGW WenKai TC</span><span>"</span><span>))</span></span></code></pre><p>但是當時我以爲這樣設置是將除了​<code class="">'han</code>​字符集以外的所有字符都用第一個字體顯示，剩下的漢字部分用第二個字體，但是根據前面提到的，這樣使用​<code class="">set-face-attribute</code>​只會設定ASCII字符而已，驗證這點也很簡單，打開Emacs，輸入一個中文的句號，就會發現惱人的卡頓問題再次出現，並且這個句號沒有使用我所設定的任何一種字體。這也説得過去，因爲中文的標點不在ASCII中，也不在​<code class="">'han</code>​字符集中。
</p><p>如果把最初的配置修改成：
</p><pre tabindex="0"><code><span><span>(</span><span>set-face-attribute</span><span> 'default</span><span> nil</span><span> :font </span><span>"</span><span>Maple Mono NF CN</span><span>"</span><span> :height </span><span>180</span><span>)</span></span>
<span><span>(</span><span>set-fontset-font</span><span> t </span><span>'unicode</span><span> (</span><span>font-spec</span><span> :family </span><span>"</span><span>Maple Mono NF CN</span><span>"</span><span>))</span></span></code></pre><p>就能保證中文、英文、標點都使用這款指定的字體顯示了，但是明明這個字體是包含了中文字符的，爲什麼要用這麼麻煩的方式設置？
</p><h2 id="user-content-fontsets" class="">Fontsets<a class="" tabindex="-1" href="#fontsets">#</a></h2><blockquote><p>A fontset is a list of fonts, each assigned to a range of character codes. An individual font cannot display the whole range of characters that Emacs supports, but a fontset can. Fontsets have names, just as fonts do, and you can use a fontset name in place of a font name when you specify the font for a frame or a face.
</p></blockquote><p>從文檔來看，​<strong>Fontset</strong>​正是爲了解決單個字體不能滿足所有字符顯示的問題而生的，並且每個Fontset都有自已的名字，可以用在所有使用字體名的地方。用​<code class="">emacs -q</code>​打開Emacs，通過​<code class="">M-x list-fontsets</code>​可以查看Emacs默認創建的Fontset：
</p><pre tabindex="0"><code><span><span>Fontset: -*-*-*-*-*-*-*-*-*-*-*-*-fontset-default</span></span>
<span><span>Fontset: -ns-*-*-*-*-*-10-*-*-*-*-*-fontset-standard</span></span>
<span><span>Fontset: -*-Menlo-regular-normal-normal-*-*-*-*-*-m-0-fontset-startup</span></span></code></pre><p>可以在​<strong>scratch</strong>​下輸入一些文字，用​<code class="">M-x describe-char</code>​查看字符信息，還可以按下​<code class="">M-x describe-fontset</code>​獲取當前窗口使用的Fontset詳情。可以發現，只輸入英文字母時，Emacs優先使用了「fontset-startup」中的Menlo字體，但是這個字體裏沒有包含中文字體，所以當輸入中文字符時，就會fallback到另一個字體，在我的系統上，默認是「PingFang」字體。
</p><p>回顧​<code class="">set-fontset-font</code>​這個函數，它是用來改變Fontset的主要方法，它的第一個參數就是Fontset名，如果是​<code class="">t</code>​的話，就等同於​<code class="">fontset-default</code>​。
</p><pre tabindex="0"><code><span><span>(</span><span>set-fontset-font</span><span> t </span><span>'unicode</span><span> (</span><span>font-spec</span><span> :family </span><span>"</span><span>Maple Mono NF CN</span><span>"</span><span>))</span></span></code></pre><p>如果只使用以上一行配置啓動Emacs，會發生什麼呢？答案是：輸入中文字符時的卡頓消失了，因爲直接指定了字體，不用fallback；但是英文字符卻仍然是以​<strong>Menlo</strong>​字體顯示的，因爲​<code class="">fontset-default</code>​是最後的後備，​<code class="">fontset-startup</code>​的優先級更高。
</p><p>現在可以來說明爲什麼想要用一個包含多種文字的字體卻要兩行代碼配置了：
</p><pre tabindex="0"><code><span><span>;; 繞過 </span><span>`</span><span>fontset-startup</span><span>'</span><span> 使用指定字體顯示ASCII字符</span></span>
<span><span>(</span><span>set-face-attribute</span><span> 'default</span><span> nil</span><span> :font </span><span>"</span><span>Maple Mono NF CN</span><span>"</span><span> :height </span><span>180</span><span>)</span></span>
<span><span>;; 保證其它字符回退至指定字體</span></span>
<span><span>(</span><span>set-fontset-font</span><span> t </span><span>'unicode</span><span> (</span><span>font-spec</span><span> :family </span><span>"</span><span>Maple Mono NF CN</span><span>"</span><span>))</span></span></code></pre><p>話說回來，既然Emacs優先使用了​<code class="">fontset-startup</code>​，那可不可以直接設置這個Fontset？經過測試，是可以的：
</p><pre tabindex="0"><code><span><span>(</span><span>set-fontset-font</span><span> "</span><span>fontset-startup</span><span>"</span><span> 'unicode</span><span> (</span><span>font-spec</span><span> :family </span><span>"</span><span>Maple Mono NF CN</span><span>"</span><span> :size </span><span>18.0</span><span>))</span></span></code></pre><h2 id="user-content-自定義fontset" class="">自定義Fontset<a class="" tabindex="-1" href="#自定義fontset">#</a></h2><p>當然想要更靈活地使用不同字體，還是需要將​<code class="">set-face-attribute</code>​和​<code class="">set-fontset-font</code>​結合起來使用的。前文已經提到，​<code class="">set-face-attribute</code>​的​<code class="">:font</code>​參數不一定是字體名，可以用Fontset名來替代，下面以爲​<strong>org-mode</strong>​的一級標題單獨設置Fontset爲例說明：
</p><pre tabindex="0"><code><span><span>;; 創建Fontset</span></span>
<span><span>(</span><span>create-fontset-from-fontset-spec</span></span>
<span><span>  (</span><span>font-xlfd-name</span></span>
<span><span>    (</span><span>font-spec</span><span> :family </span><span>"</span><span>Iosevka Nerd Font</span><span>"</span></span>
<span><span>               :registry </span><span>"</span><span>fontset-my</span><span>"</span><span>))) </span><span>; 這裏名稱必須是 </span><span>`</span><span>fontset-xxx</span><span>'</span><span> 格式</span></span>
<span><span>(</span><span>set-fontset-font</span><span> "</span><span>fontset-my</span><span>"</span><span> 'han</span><span> (</span><span>font-spec</span><span> :family </span><span>"</span><span>LXGW WenKai Mono TC</span><span>"</span><span>))</span></span>
<span><span>(</span><span>set-face-attribute</span><span> 'org-level-1</span><span> nil</span><span> :fontset </span><span>"</span><span>fontset-my</span><span>"</span><span> :height </span><span>180</span><span>)</span></span></code></pre><p>但是要注意，其實按文檔所寫的，將​<code class="">:font</code>​參數設置爲Fontset名是沒用的，真正能起作用的是文檔中沒有寫的​<code class="">:fontset</code>​參數。至少在​<strong>Emacs 30.2</strong>​版本是這樣。
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>Emacs字體配置</title>
          <link>https://elliot00.com/posts/emacs-font-setup</link>
          <description>本文介紹了在 Emacs 中配置不同字體的多種方案，以滿足程序員和中文用戶的特定需求。</description>
          <pubDate>Sun, 24 Aug 2025 14:01:48 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <h2 id="user-content-方案一全局等寬字體" class="">方案一：全局等寬字體<a class="" tabindex="-1" href="#方案一全局等寬字體">#</a></h2><p>程序員大部分是使用等寬字體的，因爲平時一些容易混淆的如 <em>lI0O</em> 等字符在等寬字體中可以輕易的被分辨出來，而且代碼裏也經常會使用縮進，等寬字體會使多行代碼看起來更美觀。要配置一個全局的等寬字體是最簡單的：
</p><pre tabindex="0"><code><span><span>(</span><span>set-face-attribute</span><span> 'default</span><span> nil</span><span> :family </span><span>"</span><span>Maple Mono NF</span><span>"</span><span> :height </span><span>180</span><span>)</span></span></code></pre><p>這個函數的第一個參數被稱爲​<strong>FACE</strong>​，可以理解爲在Emacs中可以被設置字體、顏色等屬性的單元。按​<code class="">M-x describe-face</code>​可以查看有不同的FACE的屬性。
</p><h2 id="user-content-方案二中文特殊字體" class="">方案二：中文特殊字體<a class="" tabindex="-1" href="#方案二中文特殊字體">#</a></h2><p>通常一個字體很難滿足所有的需求，Emacs提供了方法對特定字符集設置字體，比如針對漢字設置：
</p><pre tabindex="0"><code><span><span>(</span><span>set-fontset-font</span><span> t </span><span>'han</span><span> (</span><span>font-spec</span><span> :family </span><span>"</span><span>LXGW WenKai TC</span><span>"</span><span>))</span></span></code></pre><p>第二個參數也可以是Unicode範圍：
</p><pre tabindex="0"><code><span><span>(</span><span>set-fontset-font</span><span> t '(</span><span>#x2ff0</span><span> .</span><span> #x9ffc</span><span>) (</span><span>font-spec</span><span> :family </span><span>"</span><span>LXGW WenKai TC</span><span>"</span><span>))</span></span></code></pre><h2 id="user-content-方案三文檔使用非等寬字體" class="">方案三：文檔使用非等寬字體<a class="" tabindex="-1" href="#方案三文檔使用非等寬字體">#</a></h2><p>其實使用方案一可以設置任意全局字體，但是如果想在代碼編輯中使用等寬字體，而在如​<code class="">org-mode</code>​等文檔中使用非等寬的更適合閱讀的字體，可以這樣做：
</p><pre tabindex="0"><code><span><span>(</span><span>set-face-attribute</span><span> 'default</span><span> nil</span><span> :family </span><span>"</span><span>默認字體</span><span>"</span><span> :height </span><span>180</span><span>)</span></span>
<span><span>(</span><span>set-face-attribute</span><span> 'variable-pitch</span><span> nil</span><span> :family </span><span>"</span><span>非等寬字體，如Helvetica</span><span>"</span><span>)</span></span>
<span><span>(</span><span>set-face-attribute</span><span> 'fixed-pitch</span><span> nil</span><span> :family </span><span>"</span><span>等寬字體，如Maple Mono NF</span><span>"</span><span>)</span></span></code></pre><p>設置多套方案，之後可以通過​<code class="">hook</code>​功能在特定mode下啓用​<code class="">variable-pitch-mode</code>​：
</p><pre tabindex="0"><code><span><span>;; 在 </span><span>`</span><span>org-mode</span><span>'</span><span> 內使用非等寬字體</span></span>
<span><span>(</span><span>add-hook</span><span> 'org-mode-hook</span><span> (</span><span>lambda</span><span> () (</span><span>variable-pitch-mode</span><span> t)))</span></span></code></pre><h2 id="user-content-方案四單獨face配置" class="">方案四：單獨face配置<a class="" tabindex="-1" href="#方案四單獨face配置">#</a></h2><p>在方案一說過，​<code class="">set-face-attribute</code>​這個函數可以針對不同的face設置樣式（不僅僅是字體），例如在​<code class="">org-mode</code>​中，​<code class="">table</code>​模塊具有自動對齊功能，但是這個對齊是通過文本的空格來對齊的，如果設置的字體不是每個字符都等寬或者有合理的比例（如中英文2:1）的話，表格顯示就會很難看了。那能否只給表格設置一個中英文2:1的字體呢？可以這樣實現：
</p><pre tabindex="0"><code><span><span>(</span><span>set-face-attribute</span><span> 'org-table</span><span> nil</span><span> :family </span><span>"</span><span>LXGW WenKai Mono TC</span><span>"</span><span>)</span></span></code></pre><p>一些主題如​<strong>modus</strong>​提供混合字體支持，只需按方案三配置​<code class="">fixed-pitch</code>​，主題會自動將其應用到​<code class="">org-mode</code>​的表格和代碼塊等需要等寬字體的地方，其它部分可以保持使用非等寬字體。
</p><h2 id="user-content-方案五face-remap-add-relative" class="">方案五：face-remap-add-relative<a class="" tabindex="-1" href="#方案五face-remap-add-relative">#</a></h2><p>現在一些字體會支持很多特性，如連字符、自帶​<strong>nerdfont</strong>​支持、中英文等寬等，也許在大多數場景下都夠用了，只在一些特殊情況下——如在中文筆記裏使用一個對中文顯示更美觀的等寬字體，就可以使用​<code class="">face-remap-add-relative</code>​：
</p><pre tabindex="0"><code><span><span>(</span><span>set-face-attribute</span><span> 'default</span><span> nil</span><span> :height </span><span>180</span><span> :family </span><span>"</span><span>Maple Mono NF</span><span>"</span><span>)</span></span>
<span><span>(</span><span>add-hook</span><span> 'org-mode-hook</span><span> (</span><span>lambda</span><span> () (</span><span>face-remap-add-relative</span><span> 'default</span><span> :family </span><span>"</span><span>LXGW WenKai Mono TC</span><span>"</span><span>)))</span></span></code></pre><h2 id="user-content-總結" class="">總結<a class="" tabindex="-1" href="#總結">#</a></h2><p>以下幾個方案只是根據我認爲有可能的需求（主要是中文用戶的需求）簡單列了一下，實際使用中可以根據需要組合。
</p><p>最後還有一種大道至簡的方法，適用於只需要整體上文字和諧的用戶：​找一個支持​<strong>nerdfont</strong>​、支持中英文2:1的字體，只用一種字體。<a href="https://font.subf.dev/zh-cn/">Maple Mono</a>有一個變體就能做到。
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>解释器与CPS变换</title>
          <link>https://elliot00.com/posts/interpreter-with-cps</link>
          <description>本文从乘积计算的简单问题出发，深入探讨了Racket中call/cc的实现原理和应用，揭示了续体编程的强大能力与控制流本质。</description>
          <pubDate>Sat, 24 May 2025 13:58:03 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <p>如何写一个函数计算列表中所有数字的乘积并打印计算过程中的每一个数？在声明式语言中通常可以借助高阶函数​<code class="">foldl</code>​来实现，例如Racket：
</p><pre tabindex="0"><code><span><span>#lang</span><span> racket</span></span>
<span></span>
<span><span>(</span><span>define</span><span> (</span><span>product</span><span> lst</span><span>)</span></span>
<span><span>  (</span><span>foldl</span><span> (</span><span>lambda</span><span> (</span><span>item</span><span> acc</span><span>)</span></span>
<span><span>           (</span><span>printf</span><span> "Got </span><span>~a</span><span>\n</span><span>"</span><span> item)</span></span>
<span><span>           (</span><span>*</span><span> item acc))</span></span>
<span><span>         1</span></span>
<span><span>         lst))</span></span>
<span></span>
<span><span>(product </span><span>'</span><span>(</span><span>1</span><span> 2</span><span> 3</span><span> 4</span><span> 5</span><span>))</span></span></code></pre><p>这是一个典型的声明式写法，代码只是向一个高阶函数传递了每一步要做什么、初始值是什么以及要处理的列表，而如何索引、循环的细节都被隐藏了起来。
</p><p>但是如果列表中包含0（任何数乘0都等于0，结果显然为0），我们希望遇到0时直接停止计算，这种写法就无法满足。首先在Racket中没有语句的概念，也就没法直接用​<code class="">return</code>​语句直接中止整个函数；另外由于计算过程在一个​<em>lambda</em>​中，一般的命令式语言也无法直接在其中中断外部函数，一些语言如​Kotlin<sup><a href="#fn.1" class="" id="user-content-fnr.1">1</a></sup>​为这个场景提供了特殊方法。
</p><p>那么在Racket中要怎么做？回忆一下求阶乘函数：
</p><pre tabindex="0"><code><span><span>(</span><span>define</span><span> (</span><span>fact</span><span> n</span><span>)</span></span>
<span><span>  (</span><span>if</span><span> (</span><span>&#x3C;=</span><span> n </span><span>1</span><span>)</span></span>
<span><span>    1</span></span>
<span><span>    (</span><span>*</span><span> n (fact (</span><span>-</span><span> n </span><span>1</span><span>)))))</span></span></code></pre><p>由于计算列表乘积也相对简单，所以如果不借助高阶函数，也可以直接用递归解决：
</p><pre tabindex="0"><code><span><span>(</span><span>define</span><span> (</span><span>product-rec</span><span> lst</span><span>)</span></span>
<span><span>  (</span><span>let</span><span> iter ([lst lst]</span></span>
<span><span>             [res </span><span>1</span><span>])</span></span>
<span><span>    (</span><span>match</span><span> lst</span></span>
<span><span>      [</span><span>'</span><span>() res]</span></span>
<span><span>      [(</span><span>cons</span><span> 0</span><span> _</span><span>)</span></span>
<span><span>       (</span><span>println</span><span> "Got zero!"</span><span>)</span></span>
<span><span>       0</span><span>]</span></span>
<span><span>      [(</span><span>cons</span><span> x xs)</span></span>
<span><span>       (</span><span>printf</span><span> "Got </span><span>~a</span><span>\n</span><span>"</span><span> x)</span></span>
<span><span>       (iter xs (</span><span>*</span><span> x res))])))</span></span></code></pre><p>那为什么递归程序可以做到提前停止？
</p><p>回顾一下<a href="/posts/write-a-mini-lisp-interpreter">简易解释器的实现</a>，如果​<em>evaluate</em>​变为​<strong>Continuation-passing</strong>​风格，如​<code class="">evaluate 0</code>​这个操作的后续（Continuation）是什么？可以认为是什么也不做或打印到控制台，我们可以把这个称为​<strong>最终后续</strong>​。再来看看函数调用表达式，​<code class="">(evaluate '(foo 1 2) env)</code>​，整个求值步骤是先对​<code class="">foo</code>​求值，后续是对参数求值，最后取出环境中的函数体，对函数体求值，之后就是最终后续（打印结果）了。
</p><p>可以发现，从解释器的核心​<code class="">evaluate</code>​函数的Continuation角度来看，前面的递归程序可以提前终止的原因就是，它暗示了解释器应当在遇到0时，直接使用最终后续。
</p><h2 id="user-content-first-class-continuation" class="">first-class continuation<a class="" tabindex="-1" href="#first-class-continuation">#</a></h2><p>在1960年代，有人提出「functions as first-class citizens」概念，表示在编程语言中函数和一般数据类型享有同等地位，例如被当做参数传递或被另一个函数当做返回值，后来这种特性常被称作「first-class function」。
</p><p>而在Racket中，不仅有「first-class function」，还有「first-class continuation」。这意味着在Racket代码中可以获取continuation，并将其当做数据来处理。前面提到解释器在解释表达式时，有一个「最终后续」，如打印求值结果到，如果可以在代码中直接「调用」这个continuation，不就能实现不论当前在多深的嵌套回调中，都可以直接让整个解释过程终止吗？
</p><p>Racket提供了这样一个函数，叫作​<code class="">call/cc</code>​，全名是​<em>call-with-current-continuation</em>​，这个函数接受一个一元函数f做参数，f函数的参数就是「current continuation」。比如这个计算​<code class="">(+ 3 (* 2 6) 5)</code>​中，​<code class="">(* 2 6)</code>​的continuation是什么？可以看作是​<code class="">(lambda (x) (+ 3 x 5))</code>​：
</p><pre tabindex="0"><code><span><span>(</span><span>define</span><span> k </span><span>#f</span><span>)</span></span>
<span><span>(</span><span>+</span><span> 3</span><span> (</span><span>call/cc</span><span> (</span><span>lambda</span><span> (</span><span>cont</span><span>)</span></span>
<span><span>                (</span><span>set!</span><span> k cont)</span></span>
<span><span>                (</span><span>*</span><span> 2</span><span> 6</span><span>)))</span></span>
<span><span>   5</span><span>) </span><span>;; => 20</span></span></code></pre><p>以上代码的结果和直接计算​<code class="">(+ 3 (* 2 6) 5)</code>​没有区别，但是其中使用了​<code class="">call/cc</code>​并将k设置成了cont。在这个计算后k变成了什么呢？
</p><pre tabindex="0"><code><span><span>(k </span><span>2</span><span>) </span><span>;; => 10</span></span>
<span><span>(k </span><span>1</span><span>) </span><span>;; => 9</span></span></code></pre><p>没错，k正是​<code class="">(* 2 6)</code>​的continuation，看上去它和​<code class="">(lambda (x) (+ 3 x 5))</code>​是等价的。
</p><p>回到开头所说的​<code class="">product</code>​函数，现在用​<code class="">call/cc</code>​来改造它：
</p><pre tabindex="0"><code><span><span>(</span><span>define</span><span> (</span><span>product</span><span> lst</span><span>)</span></span>
<span><span>  (</span><span>call/cc</span></span>
<span><span>    (</span><span>lambda</span><span> (</span><span>return</span><span>)</span></span>
<span><span>      (</span><span>foldl</span></span>
<span><span>        (</span><span>lambda</span><span> (</span><span>item</span><span> acc</span><span>)</span></span>
<span><span>          (</span><span>cond</span></span>
<span><span>            [(</span><span>zero?</span><span> item)</span></span>
<span><span>             (</span><span>println</span><span> "Got zero!"</span><span>)</span></span>
<span><span>             (return </span><span>0</span><span>)]</span></span>
<span><span>            [</span><span>else</span></span>
<span><span>(</span><span>printf</span><span> "Got </span><span>~a</span><span>\n</span><span>"</span><span> item)</span></span>
<span><span>              (</span><span>*</span><span> item acc)]))</span></span>
<span><span>        1</span></span>
<span><span>        lst))))</span></span>
<span></span>
<span><span>(product </span><span>'</span><span>(</span><span>1</span><span> 2</span><span> 0</span><span> 3</span><span> 4</span><span> 5</span><span>))</span></span></code></pre><p>注意这个​<code class="">return</code>​，它并不是一般语言中的关键字哦，它只是一个普通的形式参数，在这里它代表整个函数体的continuation。如在​<code class="">(display (product 8))</code>​中，这个​<code class="">return</code>​就相当于是​<code class="">(lambda (x) (display x))</code>​了。通过遇到0时直接调用continuation，就做到了在foldl函数的lambda参数内部提前终止product的执行的效果。
</p><h2 id="user-content-实现callcc" class="">实现call/cc<a class="" tabindex="-1" href="#实现callcc">#</a></h2><p>这个神奇的​<code class="">call/cc</code>​函数是如何实现的呢？首先应该将我们的解释器的​<code class="">evaluate</code>​过程改造成CPS。
</p><p>第一步，先提取出一个​<code class="">evaluate-cps</code>​函数，它比原​<code class="">evaluate</code>​函数多出一个​<code class="">cont</code>​参数：
</p><pre tabindex="0"><code><span><span>(</span><span>define</span><span> (</span><span>evaluate-cps</span><span> expr</span><span> env</span><span> cont</span><span>)</span></span>
<span><span>  (</span><span>match</span><span> expr</span></span>
<span><span>    ;; TODO</span></span>
<span><span>    ))</span></span>
<span></span>
<span><span>(</span><span>define</span><span> (</span><span>evaluate</span><span> expr</span><span> env</span><span>)</span></span>
<span><span>  (evaluate-cps expr env </span><span>displayln</span><span>)) </span><span>;; 用displayln做最终延续</span></span></code></pre><p>现在来逐步完善模式匹配的各个分支，首先是原子表达式部分，只需要简单地包裹上延续函数：
</p><pre tabindex="0"><code><span><span>[(? </span><span>number?</span><span>) (cont expr)]</span></span>
<span><span>[(? </span><span>boolean?</span><span>) (cont expr)]</span></span>
<span><span>[(? </span><span>symbol?</span><span>) (cont (lookup-env env expr))]</span></span></code></pre><p>接下来是if表达式，首先应该对​<code class="">cond-expr</code>​部分求值，这个过程的延续是根据求值的结果，决定递归地对​<code class="">then-expr</code>​还是​<code class="">else-expr</code>​求值，并且使用整个if表达式的延续做最终的延续。用代码描述如下：
</p><pre tabindex="0"><code><span><span>[</span><span>`</span><span>(</span><span>if</span><span> ,</span><span>cond-expr </span><span>,</span><span>then-expr </span><span>,</span><span>else-expr)</span></span>
<span><span>    (evaluate-cps cond-expr env</span></span>
<span><span>                (</span><span>λ</span><span> (</span><span>cond-value</span><span>)</span></span>
<span><span>                    (</span><span>if</span><span> cond-value</span></span>
<span><span>                        (evaluate-cps then-expr env cont)</span></span>
<span><span>                        (evaluate-cps else-expr env cont))))]</span></span></code></pre><p>定义函数和变量也并不复杂：
</p><pre tabindex="0"><code><span><span>[</span><span>`</span><span>(</span><span>define</span><span> ,</span><span>name </span><span>,</span><span>val-expr)</span></span>
<span><span>    (evaluate-cps val-expr env</span></span>
<span><span>     (</span><span>λ</span><span> (</span><span>value</span><span>)</span></span>
<span><span>      (extend-current-frame env name value)</span></span>
<span><span>      (cont value)))]</span></span>
<span><span>[</span><span>`</span><span>(fn (</span><span>,</span><span>name </span><span>,</span><span>params </span><span>...</span><span>) </span><span>,</span><span>body </span><span>...</span><span>)</span></span>
<span><span>    (</span><span>let</span><span> ([func (function params body env)])</span></span>
<span><span>    (extend-current-frame env name func)</span></span>
<span><span>    (cont func))]</span></span></code></pre><p>函数调用部分：
</p><pre tabindex="0"><code><span><span>[</span><span>`</span><span>(</span><span>,</span><span>func-expr </span><span>,</span><span>arg-exprs </span><span>...</span><span>)</span></span>
<span><span>  (evaluate-cps func-expr env</span></span>
<span><span>                (</span><span>λ</span><span> (</span><span>proc</span><span>)</span></span>
<span><span>                  (</span><span>let</span><span> loop ([arg-exprs arg-exprs]</span></span>
<span><span>                             [arg-vals </span><span>'</span><span>()])</span></span>
<span><span>                    (</span><span>if</span><span> (</span><span>null?</span><span> arg-exprs)</span></span>
<span><span>                      (</span><span>cond</span></span>
<span><span>                        [(primitives? func-expr)</span></span>
<span><span>                         (cont (</span><span>apply</span><span> proc (</span><span>reverse</span><span> arg-vals)))]</span></span>
<span><span>                        [(function? proc)</span></span>
<span><span>                         (</span><span>let</span><span> ([new-frame (make-frame)])</span></span>
<span><span>                           (</span><span>for</span><span> ([param (function-params proc)]</span></span>
<span><span>                                 [arg (</span><span>reverse</span><span> arg-vals)])</span></span>
<span><span>                             (extend-frame new-frame param arg))</span></span>
<span><span>                           (</span><span>let</span><span> ([new-env (extend-env (function-env proc) new-frame)])</span></span>
<span><span>                             (evaluate-cps (function-body proc) new-env cont)))]</span></span>
<span><span>                        [</span><span>else</span><span> (</span><span>error</span><span> 'evaluate</span><span> "not a procedure: ~a"</span><span> proc)])</span></span>
<span><span>                      (evaluate-cps (</span><span>car</span><span> arg-exprs) env</span></span>
<span><span>                                    (</span><span>λ</span><span> (</span><span>arg-val</span><span>)</span></span>
<span><span>                                    (loop (</span><span>cdr</span><span> arg-exprs) (</span><span>cons</span><span> arg-val arg-vals))))))))]</span></span></code></pre><p>以上代码首先对函数名求值，并传递一个循环过程作为continuation，依次对参数求值，将对下一个函数的求值过程作为continuation传递，直到参数用完，最后的continuation是对函数体求值。这里还体现出CPS代码的一个特点，即由于通过参数将后续操作显式传递，开发者可以自由控制求值顺序，比如先求值参数列表，甚至倒着从最后一个参数开始也可以。
</p><p>最后实现​<code class="">call/cc</code>​函数，先定义一个结构存放延续：
</p><pre tabindex="0"><code><span><span>(</span><span>struct</span><span> continuation</span></span>
<span><span>  (cont) </span><span>#:transparent</span><span>)</span></span></code></pre><p>遇到​<code class="">call/cc</code>​调用时将当前延续封装保存：
</p><pre tabindex="0"><code><span><span>[</span><span>`</span><span>(</span><span>call/cc</span><span> ,</span><span>proc-expr)</span></span>
<span><span>  (evaluate-cps proc-expr env</span></span>
<span><span>                (</span><span>λ</span><span> (</span><span>proc</span><span>)</span></span>
<span><span>                  (</span><span>if</span><span> (function? proc) </span><span>;; 判断一下参数类型</span></span>
<span><span>                    (</span><span>letrec</span><span> ([k (continuation cont)] </span><span>;; 存储当前cont</span></span>
<span><span>                             [new-frame (extend-frame (make-frame) (</span><span>car</span><span> (function-params proc)) k)]</span></span>
<span><span>                             [new-env (extend-env (function-env proc) new-frame)])</span></span>
<span><span>                      (evaluate-cps (function-body proc) </span><span>;; 对参数的函数体求值</span></span>
<span><span>                                    new-env</span></span>
<span><span>                                    cont))</span></span>
<span><span>                      (</span><span>error</span><span> 'call/cc</span><span> "expected a function"</span><span>))))]</span></span></code></pre><p>还需要注意​<code class="">continuation</code>​是一个结构体，为了使它能被当作普通函数调用，还需要做最后一点修改：
</p><pre tabindex="0"><code><span><span>[</span><span>`</span><span>(</span><span>,</span><span>func-expr </span><span>,</span><span>arg-exprs </span><span>...</span><span>)</span></span>
<span><span>  (evaluate-cps func-expr env</span></span>
<span><span>                (</span><span>λ</span><span> (</span><span>proc</span><span>)</span></span>
<span><span>                  (</span><span>let</span><span> loop ([arg-exprs arg-exprs]</span></span>
<span><span>                             [arg-vals </span><span>'</span><span>()])</span></span>
<span><span>                    (</span><span>if</span><span> (</span><span>null?</span><span> arg-exprs)</span></span>
<span><span>                      (</span><span>cond</span></span>
<span><span>                        ;; 省略</span></span>
<span><span>                        [(</span><span>continuation?</span><span> proc)</span></span>
<span><span>                         (</span><span>if</span><span> (</span><span>=</span><span> (</span><span>length</span><span> arg-vals) </span><span>1</span><span>)</span></span>
<span><span>                           ((continuation-cont proc) (</span><span>car</span><span> arg-vals))</span></span>
<span><span>                           (</span><span>error</span><span> 'call/cc</span><span> "continuation expects 1 argument"</span><span>))]</span></span>
<span><span>                        [</span><span>else</span><span> (</span><span>error</span><span> 'evaluate</span><span> "not a procedure: ~a"</span><span> proc)])</span></span>
<span><span>                      (evaluate-cps (</span><span>car</span><span> arg-exprs) env</span></span>
<span><span>                                    (</span><span>λ</span><span> (</span><span>arg-val</span><span>)</span></span>
<span><span>                                    (loop (</span><span>cdr</span><span> arg-exprs) (</span><span>cons</span><span> arg-val arg-vals))))))))]</span></span></code></pre><p>为了不帖大段代码，以上只展示了核心部分，完整的代码我放在了<a href="https://gist.github.com/Eliot00/f1fc5f0a340a53a62c6cdf0b1569776f">GitHub</a>上，并实现了对​<code class="">lambda</code>​、​<code class="">set!</code>​等的支持。
</p><h2 id="user-content-问题" class="">问题<a class="" tabindex="-1" href="#问题">#</a></h2><p>​<code class="">call/cc</code>​的强大不止于此，你甚至可以用它实现​<code class="">try-catch</code>​、​<code class="">generator</code>​和​<code class="">async/await</code>​等等<sup><a href="#fn.2" class="" id="user-content-fnr.2">2</a></sup>。那么它有没有什么缺点呢？
</p><p>回到最开始演示​<code class="">call/cc</code>​的代码，乘法计算的continuation被保存到变量k中，那么对于表达式​<code class="">(k (k (k 2)))</code>​你期待得到什么值呢？似乎应该是26,然而实际上却是10。因为调用​<code class="">(k 2)</code>​会指使解释器直接应用continuation而丢弃嵌套的外层​<code class="">(k (k ...))</code>​，这是​<code class="">call/cc</code>​的第一个问题，虽然它看上去和普通函数没区别，但实际它没法像普通函数那样组合。
</p><p>另一个问题是使用它的代码阅读起来有点困难，有点反直觉，比如下面这段代码你能一眼看出结果应该是什么吗？
</p><pre tabindex="0"><code><span><span>(</span><span>let</span><span> ([x (</span><span>call/cc</span><span> (</span><span>lambda</span><span> (</span><span>k</span><span>) k))])</span></span>
<span><span>  (x (</span><span>lambda</span><span> (</span><span>ignore</span><span>) </span><span>"hi"</span><span>)))</span></span></code></pre><p>另一个极端的例子是著名的阴阳迷题：
</p><pre tabindex="0"><code><span><span>(</span><span>let*</span><span> ((yin</span></span>
<span><span>         ((</span><span>lambda</span><span> (</span><span>cc</span><span>) (</span><span>display</span><span> #\@</span><span>) cc) (</span><span>call-with-current-continuation</span><span> (</span><span>lambda</span><span> (</span><span>c</span><span>) c))))</span></span>
<span><span>       (yang</span></span>
<span><span>         ((</span><span>lambda</span><span> (</span><span>cc</span><span>) (</span><span>display</span><span> #\*</span><span>) cc) (</span><span>call-with-current-continuation</span><span> (</span><span>lambda</span><span> (</span><span>c</span><span>) c)))))</span></span>
<span><span>    (yin yang))</span></span></code></pre><p>同时还要注意到，每次遇到​<code class="">call/cc</code>​，我们的解释器都把当前过程的​<strong>整个后续</strong>​给保存下来了<sup><a href="#fn.3" class="" id="user-content-fnr.3">3</a></sup>，虽然前面提到能借助它实现生成器、协程等，但实际上效率堪忧。
</p><h2 id="user-content-footnotes" class="">Footnotes:<a class="" tabindex="-1" href="#footnotes">#</a></h2><div><sup><a class="" id="user-content-fn.1" href="#fnr.1">1</a></sup><div><p>Kotlin的Return to labels: <a href="https://kotlinlang.org/docs/returns.html#return-to-labels">https://kotlinlang.org/docs/returns.html#return-to-labels</a></p></div></div><div><sup><a class="" id="user-content-fn.2" href="#fnr.2">2</a></sup><div><p>可以在R. Kent Dybvig的书The Scheme Programming Language中找到
</p></div></div><div><sup><a class="" id="user-content-fn.3" href="#fnr.3">3</a></sup><div><p>针对这一点，后来有人提出了Delimited Continuation
</p></div></div>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>实现简易Lisp解释器</title>
          <link>https://elliot00.com/posts/write-a-mini-lisp-interpreter</link>
          <description>用Racket实现简易Lisp解释器：从eval到递归阶乘计算。</description>
          <pubDate>Tue, 29 Apr 2025 07:16:23 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <p>之前写了篇<a href="/posts/a-gentle-introduction-to-cps">有关CPS的文</a>，原本是打算继续写关于CPS变换和​<em>Delimited Continuation</em>​的内容的，只是在写作过程中不继涌现出新的问题，索性将要写的内容打散，做一个新的系列。
</p><p>为了方便引入后续要介绍的概念，本文先使用Racket写一个简易的mini Lisp解释器，目标是做到支持递归的阶乘计算为止。
</p><h2 id="user-content-从eval开始" class="">从eval开始<a class="" tabindex="-1" href="#从eval开始">#</a></h2><p>首先来定义核心的eval函数：
</p><pre tabindex="0"><code><span><span>#lang</span><span> racket</span></span>
<span></span>
<span><span>(</span><span>define</span><span> (</span><span>evaluate</span><span> expr</span><span>)</span></span>
<span><span>  (</span><span>match</span><span> expr</span></span>
<span><span>    [(? </span><span>number?</span><span>) expr]</span></span>
<span><span>    [(? </span><span>boolean?</span><span>) expr]))</span></span>
<span></span>
<span><span>(evaluate </span><span>'42</span><span>)</span></span>
<span><span>(evaluate </span><span>'</span><span>#f</span><span>)</span></span></code></pre><p>这当然还是非常小儿科的代码，只能原封不动地吐出数值类型和布尔类型的字面量。下面加上对变量的支持：
</p><pre tabindex="0"><code><span><span>(</span><span>define</span><span> (</span><span>make-environment</span><span>)</span></span>
<span><span>  (</span><span>make-hash</span><span>))</span></span>
<span></span>
<span><span>(</span><span>define</span><span> (</span><span>lookup-env</span><span> env</span><span> symbol</span><span>)</span></span>
<span><span>  (</span><span>hash-ref</span></span>
<span><span>    env</span></span>
<span><span>    symbol</span></span>
<span><span>    (</span><span>λ</span><span> () (</span><span>error</span><span> 'evaluate</span><span> "failed to find symbol: ~a"</span><span> symbol))))</span></span></code></pre><p>这是一个简单的实现，只是封装了哈希表来模拟一个全局的「环境」，用来存储「符号」与其对应的值，接下来修改​<code class="">evaluate</code>​函数：
</p><pre tabindex="0"><code><span><span>(</span><span>define</span><span> (</span><span>evaluate</span><span> expr</span><span> env</span><span>)</span></span>
<span><span>  (</span><span>match</span><span> expr</span></span>
<span><span>    [(? </span><span>number?</span><span>) expr]</span></span>
<span><span>    [(? </span><span>boolean?</span><span>) expr]</span></span>
<span><span>    [(? </span><span>symbol?</span><span>) (lookup-env env expr)]))</span></span></code></pre><p>现在可以定义一个全局环境并测试：
</p><pre tabindex="0"><code><span><span>(</span><span>define</span><span> env (make-environment))</span></span>
<span></span>
<span><span>(evaluate </span><span>'year</span><span> env)</span></span></code></pre><p>但是这段代码会报错，因为现在的​<code class="">env</code>​是空的，现在需要提供一个自定义变量的机制，让我们为这个mini Lisp添加第一个内置函数吧。
</p><pre tabindex="0"><code><span><span>(</span><span>define</span><span> (</span><span>evaluate</span><span> expr</span><span> env</span><span>)</span></span>
<span><span>  (</span><span>match</span><span> expr</span></span>
<span><span>    [(? </span><span>number?</span><span>) expr]</span></span>
<span><span>    [(? </span><span>boolean?</span><span>) expr]</span></span>
<span><span>    [(? </span><span>symbol?</span><span>) (lookup-env env expr)]</span></span>
<span><span>    ;; 支持使用(define name value)来定义变量</span></span>
<span><span>    [</span><span>`</span><span>(</span><span>define</span><span> ,</span><span>name </span><span>,</span><span>val-expr)</span></span>
<span><span>      (</span><span>let</span><span> ([value (evaluate val-expr env)])</span></span>
<span><span>        (</span><span>hash-set!</span><span> env name value)</span></span>
<span><span>        value)]))</span></span></code></pre><p>实现非常简单，当遇到​<code class="">(define name val-expr)</code>​形式的代码时，首先递归调用​<code class="">evaluate</code>​对​<code class="">val-expr</code>​求值，再加结果写入​<code class="">env</code>​中。
</p><p>测试：
</p><pre tabindex="0"><code><span><span>(evaluate </span><span>'</span><span>(</span><span>define</span><span> year </span><span>2025</span><span>) env)</span></span>
<span><span>(evaluate </span><span>'year</span><span> env) </span><span>;; 2025</span></span></code></pre><p>现在不会报错了。
</p><h2 id="user-content-if" class="">if<a class="" tabindex="-1" href="#if">#</a></h2><p>因为我们要定义的mini Lisp是严格求值的，所以需要在解释器中内置if机制：
</p><pre tabindex="0"><code><span><span>[</span><span>`</span><span>(</span><span>if</span><span> ,</span><span>cond-expr </span><span>,</span><span>then-expr </span><span>,</span><span>else-expr)</span></span>
<span><span>  (</span><span>if</span><span> (evaluate cond-expr env)</span></span>
<span><span>    (evaluate then-expr env)</span></span>
<span><span>    (evaluate else-expr env))]</span></span></code></pre><p>同样是递归地先对条件表达式求值，再根据结果决定对then还是else表达式求值。
</p><p>测试：
</p><pre tabindex="0"><code><span><span>(evaluate </span><span>'</span><span>(</span><span>define</span><span> foo </span><span>#t</span><span>) env)</span></span>
<span><span>(evaluate </span><span>'</span><span>(</span><span>if</span><span> foo </span><span>1</span><span> 0</span><span>) env) </span><span>;; 输出1</span></span></code></pre><h2 id="user-content-函数" class="">函数<a class="" tabindex="-1" href="#函数">#</a></h2><p>下一步来支持函数定义和调用：
</p><p>如果遇到​<code class="">(fn (foo param) body)</code>​，可以生成一个lambda，并用函数名foo作键存入env中。
</p><pre tabindex="0"><code><span><span>[</span><span>`</span><span>(fn (</span><span>,</span><span>name </span><span>,</span><span>params </span><span>...</span><span>) </span><span>,</span><span>body)</span></span>
<span><span>  (</span><span>let</span><span> ([proc (</span><span>λ</span><span> args</span></span>
<span><span>                (</span><span>for</span><span> ([param params]</span></span>
<span><span>                      [arg args])</span></span>
<span><span>                  (extend-environment env param arg))</span></span>
<span><span>                (evaluate body env))])</span></span>
<span><span>    (</span><span>hash-set!</span><span> env name proc)</span></span>
<span><span>    proc)]</span></span></code></pre><p>最后当遇到函数调用时，需要将分别函数名以及所有参数求值，利用​<code class="">apply</code>​应用过程。
</p><pre tabindex="0"><code><span><span>[</span><span>`</span><span>(</span><span>,</span><span>func </span><span>,</span><span>args </span><span>...</span><span>)</span></span>
<span><span>  (</span><span>let</span><span> ([proc (evaluate func env)]</span></span>
<span><span>        [arg-vals (</span><span>map</span><span> (</span><span>λ</span><span> (</span><span>arg</span><span>) (evaluate arg env)) args)])</span></span>
<span><span>    (</span><span>if</span><span> (</span><span>procedure?</span><span> proc)</span></span>
<span><span>      (</span><span>apply</span><span> proc arg-vals)</span></span>
<span><span>      (</span><span>error</span><span> 'evaluate</span><span> "not a procedure: ~a"</span><span> proc)))]</span></span></code></pre><p>需要注意的是，这一段代码必需放在整个模式匹配的末尾（因为前面定义函数、if等都是这个函数调用形式的特殊情况）。
</p><p>现在可以测试定义并调用函数了：
</p><pre tabindex="0"><code><span><span>(evaluate </span><span>'</span><span>(fn (foo a b c) b) env)</span></span>
<span><span>(evaluate </span><span>'</span><span>(foo </span><span>4</span><span> 8</span><span> 3</span><span>) env) </span><span>;; 8</span></span>
<span><span>(evaluate </span><span>'a</span><span> env) </span><span>;; 4</span></span></code></pre><p>定义和调用看上去没什么问题，但是测试代码中的最后一行​<code class="">(evaluate 'a env)</code>​居然得到结果为4，这是因为当前所有符号都是记录在一个全局的env中的，这并不是正常语言期望得到的结果。
</p><h2 id="user-content-作用域" class="">作用域<a class="" tabindex="-1" href="#作用域">#</a></h2><p>为了解决以上问题，需要对环境处理以及函数定义做些改造。
</p><pre tabindex="0"><code><span><span>(</span><span>define</span><span> (</span><span>make-base-environment</span><span>)</span></span>
<span><span>  (</span><span>list</span><span> primitives))</span></span>
<span></span>
<span><span>(</span><span>define</span><span> (</span><span>extend-env</span><span> env</span><span> frame</span><span>)</span></span>
<span><span>  (</span><span>cons</span><span> frame env))</span></span>
<span></span>
<span><span>(</span><span>define</span><span> make-frame</span></span>
<span><span>  make-hash</span><span>)</span></span>
<span></span>
<span><span>(</span><span>define</span><span> primitives</span></span>
<span><span>  (make-frame </span><span>`</span><span>((</span><span>+</span><span> .</span><span> ,+</span><span>)</span></span>
<span><span>                (</span><span>-</span><span> .</span><span> ,-</span><span>)</span></span>
<span><span>                (</span><span>*</span><span> .</span><span> ,*</span><span>)</span></span>
<span><span>                (</span><span>/</span><span> .</span><span> ,/</span><span>)</span></span>
<span><span>                (</span><span>></span><span> .</span><span> ,></span><span>)</span></span>
<span><span>                (</span><span>&#x3C;</span><span> .</span><span> ,&#x3C;</span><span>)</span></span>
<span><span>                (</span><span>=</span><span> .</span><span> ,=</span><span>))))</span></span>
<span></span>
<span><span>(</span><span>define</span><span> (</span><span>extend-current-frame</span><span> env</span><span> symbol</span><span> value</span><span>)</span></span>
<span><span>  (extend-frame (</span><span>car</span><span> env) symbol value))</span></span>
<span></span>
<span><span>(</span><span>define</span><span> (</span><span>extend-frame</span><span> frame</span><span> symbol</span><span> value</span><span>)</span></span>
<span><span>  (</span><span>hash-set!</span><span> frame symbol value))</span></span>
<span></span>
<span><span>(</span><span>define</span><span> (</span><span>lookup-env</span><span> env</span><span> symbol</span><span>)</span></span>
<span><span>  (</span><span>let</span><span> loop ([frames env])</span></span>
<span><span>    (</span><span>cond</span></span>
<span><span>      [(</span><span>null?</span><span> frames)</span></span>
<span><span>       (</span><span>error</span><span> 'evaluate</span><span> "failed to find symbol: ~a"</span><span> symbol)]</span></span>
<span><span>      [(</span><span>hash-has-key?</span><span> (</span><span>car</span><span> frames) symbol)</span></span>
<span><span>       (</span><span>hash-ref</span><span> (</span><span>car</span><span> frames) symbol)]</span></span>
<span><span>      [</span><span>else</span><span> (loop (</span><span>cdr</span><span> frames))])))</span></span></code></pre><p>现在将​<code class="">environment</code>​改造成​<code class="">frame</code>​组成的栈，那么函数的实参就应该在一个单独的frame中。同时在这个实现中，加入了一个​<code class="">primitives</code>​，直接借用了Racket的四则运算和比较运算函数，方便后续定义阶乘函数。
</p><pre tabindex="0"><code><span><span>(</span><span>struct</span><span> function</span></span>
<span><span>  (params body env) </span><span>#:transparent</span><span>)</span></span>
<span></span>
<span><span>(</span><span>define</span><span> (</span><span>evaluate</span><span> expr</span><span> env</span><span>)</span></span>
<span><span>  (</span><span>match</span><span> expr</span></span>
<span><span>    ;; ... 省略</span></span>
<span><span>    [</span><span>`</span><span>(fn (</span><span>,</span><span>name </span><span>,</span><span>params </span><span>...</span><span>) </span><span>,</span><span>body)</span></span>
<span><span>      (</span><span>let</span><span> ([func (function params body env)])</span></span>
<span><span>        (extend-current-frame env name func)</span></span>
<span><span>        func)]</span></span>
<span><span>    [</span><span>`</span><span>(</span><span>,</span><span>func </span><span>,</span><span>args </span><span>...</span><span>)</span></span>
<span><span>      (</span><span>let</span><span> ([proc (evaluate func env)]</span></span>
<span><span>            [arg-vals (</span><span>map</span><span> (</span><span>λ</span><span> (</span><span>arg</span><span>) (evaluate arg env)) args)])</span></span>
<span><span>        (</span><span>cond</span></span>
<span><span>          [(</span><span>procedure?</span><span> proc)</span></span>
<span><span>           (</span><span>apply</span><span> proc arg-vals)]</span></span>
<span><span>          [(function? proc)</span></span>
<span><span>           (</span><span>let</span><span> ([new-frame (make-frame)])</span></span>
<span><span>             (</span><span>for</span><span> ([param (function-params proc)]</span></span>
<span><span>                   [arg arg-vals])</span></span>
<span><span>               (extend-frame new-frame param arg))</span></span>
<span><span>             (</span><span>let</span><span> ([new-env (extend-env (function-env proc) new-frame)])</span></span>
<span><span>               (evaluate (function-body proc) new-env)))]</span></span>
<span><span>          [</span><span>else</span><span> (</span><span>error</span><span> 'evaluate</span><span> "not a procedure: ~a"</span><span> proc)]))]))</span></span></code></pre><p>自定义函数被实现为一个结构体，并且保存了函数定义时的env。同时函数应用部分也就需要区分开内置函数和自定义函数，对函数体求值前，创建新的frame，并依次将所有实际参数绑定到形参上。
</p><p>测试：
</p><pre tabindex="0"><code><span><span>(evaluate </span><span>'</span><span>(fn (fact n)</span></span>
<span><span>               (</span><span>if</span><span> (</span><span>=</span><span> n </span><span>1</span><span>)</span></span>
<span><span>                 n</span></span>
<span><span>                 (</span><span>*</span><span> n (fact (</span><span>-</span><span> n </span><span>1</span><span>))))) env)</span></span>
<span><span>(evaluate </span><span>'</span><span>(fact </span><span>8</span><span>) env) </span><span>;; 40320</span></span></code></pre><p>完成！
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>TypeScript小技巧（二）</title>
          <link>https://elliot00.com/posts/typescript-using-and-satisfies</link>
          <description>本文介绍了TypeScript中引入的using关键字和satisfies操作符的使用：using简化了资源管理，自动调用[Symbol.dispose]方法释放资源，而satisfies则用于确保对象符合特定类型，同时限制对象属性的扩展。</description>
          <pubDate>Sun, 09 Mar 2025 08:29:50 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <p>点击查看<a href="/posts/typescript-react-tips">上一篇</a></p><h2 id="user-content-使用using关键字" class="">使用using关键字<a class="" tabindex="-1" href="#使用using关键字">#</a></h2><p>如数据库、文件等IO相关的API，一般需要在合适的时机将资源释放，经常需要写这样的代码：
</p><pre tabindex="0"><code><span><span>try</span><span> {</span></span>
<span><span>    db</span><span>.</span><span>xxx</span><span>();</span></span>
<span><span>} </span><span>catch</span><span> (e) {</span></span>
<span><span>    // handle error</span></span>
<span><span>} </span><span>finally</span><span> {</span></span>
<span><span>    db</span><span>.</span><span>close</span><span>();</span></span>
<span><span>}</span></span></code></pre><p>在这个<a href="https://github.com/tc39/proposal-explicit-resource-management">TC39提案</a>里，引入了一个新的关键字​<code class="">using</code>​，用于简化这个流程，当然现在还没有什么浏览器支持这个特性，而TypeScript 5.2对这个关键字提供了支持（tsc会把它转换成旧语法）。首先需要有一个暴露​<code class="">[Symbol.dispose]</code>​方法的对象，然后在一个代码块中使用using代替const初始化这个对象。
</p><pre tabindex="0"><code><span><span>function</span><span> createDb</span><span>()</span><span> {</span></span>
<span><span>  console</span><span>.</span><span>log</span><span>(</span><span>"</span><span>Open!</span><span>"</span><span>)</span></span>
<span></span>
<span><span>  return</span><span> {</span></span>
<span><span>    fetch</span><span>:</span><span> (</span><span>params</span><span>:</span><span> string</span><span>)</span><span> =></span><span> {</span></span>
<span><span>      console</span><span>.</span><span>log</span><span>(params)</span></span>
<span><span>    }</span><span>,</span></span>
<span><span>    [</span><span>Symbol</span><span>.</span><span>dispose</span><span>]</span><span>:</span><span> ()</span><span> =></span><span> {</span></span>
<span><span>      console</span><span>.</span><span>log</span><span>(</span><span>"</span><span>Close!</span><span>"</span><span>)</span></span>
<span><span>    }</span></span>
<span><span>  }</span></span>
<span><span>}</span></span>
<span></span>
<span><span>{</span></span>
<span><span>  using</span><span> db</span><span> =</span><span> createDb</span><span>()</span></span>
<span><span>  db</span><span>.</span><span>fetch</span><span>(</span><span>"</span><span>select * from table</span><span>"</span><span>)</span></span>
<span><span>}</span></span></code></pre><p>最后当离开作用域时，该对象的​<code class="">[Symbol.dispose]</code>​方法就会被自动调用了。
</p><p>它还有个对应的async版本，需要这么写：
</p><pre tabindex="0"><code><span><span>function</span><span> createAsyncDb</span><span>()</span><span> {</span></span>
<span><span>  console</span><span>.</span><span>log</span><span>(</span><span>"</span><span>Open!</span><span>"</span><span>)</span></span>
<span></span>
<span><span>  return</span><span> {</span></span>
<span><span>    fetch</span><span>:</span><span> async</span><span> (</span><span>params</span><span>:</span><span> string</span><span>)</span><span> =></span><span> {</span></span>
<span><span>      console</span><span>.</span><span>log</span><span>(params)</span></span>
<span><span>    }</span><span>,</span></span>
<span><span>    [</span><span>Symbol</span><span>.</span><span>asyncDispose</span><span>]</span><span>:</span><span> async</span><span> ()</span><span> =></span><span> {</span></span>
<span><span>      console</span><span>.</span><span>log</span><span>(</span><span>"</span><span>Close!</span><span>"</span><span>)</span></span>
<span><span>    }</span></span>
<span><span>  }</span></span>
<span><span>}</span></span>
<span></span>
<span><span>{</span></span>
<span><span>  await using</span><span> db</span><span> =</span><span> createAsyncDb</span><span>()</span></span>
<span><span>  await</span><span> db</span><span>.</span><span>fetch</span><span>(</span><span>"</span><span>select * from table</span><span>"</span><span>)</span></span>
<span><span>}</span></span></code></pre><p>不过我是不认同为了省去一个手动close的操作，引入一个新关键字的，相比之下，Kotlin的<a href="https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.io/use.html">use</a>方式我认为更好。
</p><h2 id="user-content-satisfies" class="">satisfies<a class="" tabindex="-1" href="#satisfies">#</a></h2><p>有时会遇到一种情况，如果一个属性的类型是和类型，它的字面量默认不会被推断为字面的类型：
</p><pre tabindex="0"><code><span><span>type</span><span> Foo</span><span> =</span><span> Record</span><span>&#x3C;</span><span>string</span><span>,</span><span> string</span><span> |</span><span> number</span><span> |</span><span> undefined</span><span>></span></span>
<span></span>
<span><span>const</span><span> foo</span><span>:</span><span> Foo</span><span> =</span><span> {</span></span>
<span><span>  a</span><span>:</span><span> "</span><span>nice</span><span>"</span></span>
<span><span>}</span></span>
<span></span>
<span><span>// 这里就会收到类型错误，因为tsc不认为a是字符串类型</span></span>
<span><span>// foo.a不被看作string类型，而是string | number | undefined</span></span>
<span><span>foo</span><span>.</span><span>a</span><span>.</span><span>startsWith</span><span>(</span><span>"</span><span>n</span><span>"</span><span>)</span></span></code></pre><p>如果改写成：
</p><pre tabindex="0"><code><span><span>const</span><span> foo</span><span> =</span><span> {</span></span>
<span><span>  a</span><span>:</span><span> "</span><span>nice</span><span>"</span></span>
<span><span>}</span><span> satisfies</span><span> Foo</span></span></code></pre><p>就不会再有错误提示了。使用​<code class="">satisfies</code>​的另一个区别是，它会限制对象初始化后不能拓展属性：
</p><pre tabindex="0"><code><span><span>const</span><span> foo</span><span> =</span><span> {</span></span>
<span><span>  hello</span><span>:</span><span> "</span><span>nice</span><span>"</span></span>
<span><span>}</span><span> satisfies</span><span> Foo</span></span>
<span></span>
<span></span>
<span><span>// 错误</span></span>
<span><span>foo</span><span>.</span><span>past</span><span> =</span><span> 1</span></span>
<span></span>
<span><span>const</span><span> bar</span><span>:</span><span> Foo</span><span> =</span><span> {</span></span>
<span><span>  hello</span><span>:</span><span> "</span><span>nice</span><span>"</span></span>
<span><span>}</span></span>
<span></span>
<span><span>// 没问题</span></span>
<span><span>bar</span><span>.</span><span>past</span><span> =</span><span> 1</span></span></code></pre><p>同时它还可以和​<code class="">as const</code>​一起用：
</p><pre tabindex="0"><code><span><span>const</span><span> foo</span><span> =</span><span> {</span></span>
<span><span>  hello</span><span>:</span><span> "</span><span>nice</span><span>"</span><span>,</span></span>
<span><span>  bar</span><span>:</span><span> 3</span><span>,</span></span>
<span><span>}</span><span> as</span><span> const</span><span> satisfies</span><span> Foo</span></span>
<span></span>
<span><span>// Cannot assign to 'hello' because it is a read-only property.</span></span>
<span><span>foo</span><span>.</span><span>hello</span><span> =</span><span> 1</span></span></code></pre>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>Android开发拾遗：回调转协程</title>
          <link>https://elliot00.com/posts/kotlin-callback-to-coroutine</link>
          <description>本文介绍了如何使用Kotlin的suspendCancellableCoroutine将基于回调的Java SDK接口转换为协程风格的同步代码写法，以解决多层依赖关系下的回调地狱问题。</description>
          <pubDate>Wed, 26 Feb 2025 08:59:44 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <p>Kotlin提供了非常易用的協程API，但是在開發過程中遇到第三方SDK通過Java暴露出來的接口全是通過回調處理數據的情況。接口使用流程大致是：
</p><pre tabindex="0"><code><span><span>val</span><span> listener </span><span>=</span><span> object</span><span> : </span><span>ISomeInfoListener</span><span> {</span></span>
<span><span>    override</span><span> fun</span><span> onInfoUpdate</span><span>(info: </span><span>Info</span><span>) {</span></span>
<span><span>    }</span></span>
<span><span>}</span></span>
<span></span>
<span><span>SDK.</span><span>getInstance</span><span>().</span><span>registerListener</span><span>(listener)</span></span>
<span></span>
<span><span>SDK.</span><span>getInstance</span><span>().</span><span>unregisterListener</span><span>(listener)</span></span></code></pre><p>當多個操作/數據之間存在先後依賴關係時，就容易陷入回調地獄，寫起來非常不舒服。
</p><p>比如想在導航結束後，收集終點信息，存入數據庫，使用回調的形式寫：
</p><pre tabindex="0"><code><span><span>val</span><span> listener </span><span>=</span><span> object</span><span> : </span><span>IGuidanceListener</span><span> {</span></span>
<span><span>    override</span><span> fun</span><span> onCompleted</span><span>() {</span></span>
<span><span>        val</span><span> destinationInfoListener </span><span>=</span><span> object</span><span> : </span><span>IDestinationInfoListener</span><span> {</span></span>
<span><span>            override</span><span> fun</span><span> onArrived</span><span>(info: </span><span>Info</span><span>) {</span></span>
<span><span>                scope.</span><span>launch</span><span> {</span></span>
<span><span>                    saveToDb</span><span>(info)</span></span>
<span><span>                }</span></span>
<span><span>            }</span></span>
<span><span>        }</span></span>
<span><span>        SDK.</span><span>getInstance</span><span>().</span><span>registerListener</span><span>(destinationInfoListener)</span></span>
<span><span>    }</span></span>
<span><span>}</span></span>
<span></span>
<span><span>SDK.</span><span>getInstance</span><span>().</span><span>registerListener</span><span>(listener)</span></span></code></pre><p>以上還是高度簡化後的代碼，實際場景下流程更複雜，代碼可讀性很低，修改起來也很麻煩。那麼有沒有辦法把一個回調操作封裝成 <code class="">suspend</code> ，以同步方式來組織呢？
</p><p>可以使用 <code class="">suspendCancellableCoroutine</code> 來做。例如上面的操作，可以轉化爲：
</p><pre tabindex="0"><code><span><span>suspend</span><span> fun</span><span> getSomeInfo</span><span>(): </span><span>Info</span><span> =</span><span> suspendCancellableCoroutine</span><span> { continuation </span><span>-></span></span>
<span><span>    val</span><span> listener </span><span>=</span><span> object</span><span> : </span><span>ISomeInfoListener</span><span> {</span></span>
<span><span>        override</span><span> fun</span><span> onInfoUpdate</span><span>(info: </span><span>Info</span><span>) {</span></span>
<span><span>            continuation.</span><span>resume</span><span>(info)</span></span>
<span><span>        }</span></span>
<span><span>    }</span></span>
<span></span>
<span><span>    SDK.</span><span>getInstance</span><span>().</span><span>registerListener</span><span>(listener)</span></span>
<span></span>
<span><span>    continuation.</span><span>invokeOnCancellation</span><span> { SDK.</span><span>getInstance</span><span>().</span><span>unregisterListener</span><span>(listener) }</span></span>
<span><span>}</span></span></code></pre><p>在我使用的 <em>Kotlin 2.1.0</em> 版本中， <code class="">continuation.resume</code> 的函數簽名發生了一點變化：
</p><pre tabindex="0"><code><span><span>continuation.</span><span>resume</span><span>(resourceToResumeWith) { cause, resourceToClose, context </span><span>-></span></span>
<span><span>    // resourceToResumeWith 和 resourceToClose 實際上是同一個值</span></span>
<span><span>    resourceToClose.</span><span>close</span><span>()</span></span>
<span><span>}</span></span></code></pre><p>需要傳入一個 <code class="">onCancellation</code> 的回調，如果resume了一個需要關閉的Resource，可以用這個回調來處理。
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>CPS变换浅析</title>
          <link>https://elliot00.com/posts/a-gentle-introduction-to-cps</link>
          <description>本文介绍了CPS（Continuation-passing style）的基本概念，通过具体的代码示例解释了什么是continuation以及如何进行CPS变换。文章使用阶乘和斐波那契数列的实现来展示CPS变换的过程，并探讨了CPS变换在控制程序执行流程方面的作用。</description>
          <pubDate>Sun, 23 Feb 2025 06:25:50 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <h2 id="user-content-什么是cps" class="">什么是CPS<a class="" tabindex="-1" href="#什么是cps">#</a></h2><p>CPS全称「Continuation-passing style」，可见这是一种编码的风格，一种传递「Continuation」的风格。那么这个Continuation又是什么呢？用代码来说明，考虑一个求平均值的运算：
</p><pre tabindex="0"><code><span><span>(</span><span>/</span><span> (</span><span>+</span><span> 3</span><span> 5</span><span>) </span><span>2</span><span>)</span></span></code></pre><p>将3和5加起来后，再除以2，这个将加法的结果除以2的过程，就是过程​<code class="">(+ 3 5)</code>​的continuation了，如果直白的翻译就是「后续部分」，用代码表示就是：
</p><pre tabindex="0"><code><span><span>(</span><span>λ</span><span> (</span><span>x</span><span>) (</span><span>/</span><span> x </span><span>2</span><span>))</span></span></code></pre><p>CPS指的就是将这个后续操作，作为一个显式的参数传递：
</p><pre tabindex="0"><code><span><span>(</span><span>define</span><span> (</span><span>add-cps</span><span> x</span><span> y</span><span> cont</span><span>)</span></span>
<span><span>  (cont (</span><span>+</span><span> a b)))</span></span>
<span></span>
<span><span>(add-cps x y (</span><span>λ</span><span> (</span><span>x</span><span>) (</span><span>/</span><span> x </span><span>2</span><span>)))</span></span></code></pre><p>虽然可能CPS这个名词可能不为广大程序员所熟知，但应该很多程序员都写过这种形式的代码。Continuation表示后续的操作，如在JS中，表示延时1秒后再执行一个操作：
</p><pre tabindex="0"><code><span><span>setTimeout</span><span>(</span><span>()</span><span> =></span><span> {</span></span>
<span><span>    console</span><span>.</span><span>log</span><span>(</span><span>"</span><span>Hello World!</span><span>"</span><span>)</span></span>
<span><span>}</span><span>,</span><span> 1000</span><span>)</span></span></code></pre><p>将一个匿名函数传递给​<code class="">setTimeout</code>​，1秒后这个从参数传递的函数会被调用，只是在JS里这个表达后续操作的函数通常被称为​<code class="">回调函数</code>​，表示并不立即调用而是回头再调用的意思。
</p><h2 id="user-content-cps变换" class="">CPS变换<a class="" tabindex="-1" href="#cps变换">#</a></h2><p>所谓CPS变换，就是将代码转换为CPS这种风格的意思。如这段计算阶乘的代码：
</p><pre tabindex="0"><code><span><span>#lang</span><span> racket</span></span>
<span></span>
<span><span>(</span><span>define</span><span> (</span><span>factorial</span><span> n</span><span>)</span></span>
<span><span>  (</span><span>if</span><span> (</span><span>=</span><span> n </span><span>0</span><span>)</span></span>
<span><span>      1</span></span>
<span><span>      (</span><span>*</span><span> n (factorial (</span><span>-</span><span> n </span><span>1</span><span>)))))</span></span>
<span></span>
<span><span>(</span><span>displayln</span><span> (factorial </span><span>5</span><span>))</span></span></code></pre><p>将最后这个打印的操作显式传递，改写函数：
</p><pre tabindex="0"><code><span><span>#lang</span><span> racket</span></span>
<span></span>
<span><span>(</span><span>define</span><span> (</span><span>factorial-cps</span><span> n</span><span> cont</span><span>)</span></span>
<span><span>  (</span><span>if</span><span> (</span><span>=</span><span> n </span><span>0</span><span>)</span></span>
<span><span>      (cont </span><span>1</span><span>)</span></span>
<span><span>      (factorial-cps (</span><span>-</span><span> n </span><span>1</span><span>) (</span><span>λ</span><span> (</span><span>result</span><span>)</span></span>
<span><span>                               (cont (</span><span>*</span><span> n result))))))</span></span>
<span></span>
<span><span>(factorial-cps </span><span>5</span><span> (</span><span>λ</span><span> (</span><span>result</span><span>) (</span><span>displayln</span><span> result)))</span></span>
<span><span>;; 也可以eta化简一下 (factorial-cps 5 displayln)</span></span></code></pre><p>一个有趣的点是，做了CPS变换后，函数变成了「​<strong>尾递归</strong>​」的形式。
</p><p>现在把难度提高一点，如果把阶乘函数内用到的内置函数也做变换呢？
</p><pre tabindex="0"><code><span><span>#lang</span><span> racket</span></span>
<span></span>
<span><span>(</span><span>define</span><span> (</span><span>=*</span><span> a</span><span> b</span><span> cont</span><span>)</span></span>
<span><span>  (cont (</span><span>=</span><span> a b)))</span></span>
<span></span>
<span><span>(</span><span>define</span><span> (</span><span>-*</span><span> a</span><span> b</span><span> cont</span><span>)</span></span>
<span><span>  (cont (</span><span>-</span><span> a b)))</span></span>
<span></span>
<span><span>(</span><span>define</span><span> (</span><span>**</span><span> a</span><span> b</span><span> cont</span><span>)</span></span>
<span><span>  (cont (</span><span>*</span><span> a b)))</span></span>
<span></span>
<span><span>(</span><span>define</span><span> (</span><span>factorial-cps</span><span> n</span><span> cont</span><span>)</span></span>
<span><span>  (=* n </span><span>0</span><span> (</span><span>λ</span><span> (</span><span>b</span><span>)</span></span>
<span><span>            (</span><span>if</span><span> b</span></span>
<span><span>              (cont </span><span>1</span><span>)</span></span>
<span><span>              (-* n </span><span>1</span><span> (</span><span>λ</span><span> (</span><span>n*</span><span>)</span></span>
<span><span>                        (factorial-cps n* (</span><span>λ</span><span> (</span><span>f</span><span>)</span></span>
<span><span>                                            (** n f cont)))))))))</span></span></code></pre><h2 id="user-content-作用管窥" class="">作用管窥<a class="" tabindex="-1" href="#作用管窥">#</a></h2><p>那么CPS变换究竟有什么作用呢？从前面计算阶乘的代码来看，它似乎只让代码变得更加难读懂了。这一点既是CPS代码的缺点，也是其优点，这实际上是因为程序的控制流程被显式暴露出来了。
</p><p>再看一个计算斐波那契数的例子：
</p><pre tabindex="0"><code><span><span>#lang</span><span> racket</span></span>
<span></span>
<span><span>(</span><span>define</span><span> (</span><span>fib</span><span> n</span><span>)</span></span>
<span><span>  (</span><span>cond</span></span>
<span><span>    [(</span><span>=</span><span> n </span><span>0</span><span>) </span><span>0</span><span>]</span></span>
<span><span>    [(</span><span>=</span><span> n </span><span>1</span><span>) </span><span>1</span><span>]</span></span>
<span><span>    [</span><span>else</span><span> (</span><span>+</span><span> (fib (</span><span>-</span><span> n </span><span>1</span><span>))</span></span>
<span><span>             (fib (</span><span>-</span><span> n </span><span>2</span><span>)))]))</span></span>
<span></span>
<span><span>(fib </span><span>42</span><span>) </span><span>;; 267914296</span></span>
<span></span>
<span><span>;; 转换后</span></span>
<span><span>(</span><span>define</span><span> (</span><span>=*</span><span> a</span><span> b</span><span> k</span><span>)</span></span>
<span><span>  (k (</span><span>=</span><span> a b)))</span></span>
<span></span>
<span><span>(</span><span>define</span><span> (</span><span>+*</span><span> a</span><span> b</span><span> k</span><span>)</span></span>
<span><span>  (k (</span><span>+</span><span> a b)))</span></span>
<span></span>
<span><span>(</span><span>define</span><span> (</span><span>-*</span><span> a</span><span> b</span><span> k</span><span>)</span></span>
<span><span>  (k (</span><span>-</span><span> a b)))</span></span>
<span></span>
<span><span>(</span><span>define</span><span> (</span><span>fib*</span><span> n</span><span> k</span><span>)</span></span>
<span><span>  (=* n </span><span>0</span></span>
<span><span>    (</span><span>λ</span><span> (</span><span>k1</span><span>)</span></span>
<span><span>      (</span><span>if</span><span> k1</span></span>
<span><span>        (k </span><span>0</span><span>)</span></span>
<span><span>        (=* n </span><span>1</span></span>
<span><span>          (</span><span>λ</span><span> (</span><span>k2</span><span>)</span></span>
<span><span>            (</span><span>if</span><span> k2</span></span>
<span><span>              (k </span><span>1</span><span>)</span></span>
<span><span>              (-* n </span><span>1</span></span>
<span><span>                (</span><span>λ</span><span> (</span><span>k3</span><span>)</span></span>
<span><span>                  (fib* k3</span></span>
<span><span>                    (</span><span>λ</span><span> (</span><span>k4</span><span>)</span></span>
<span><span>                      (-* n </span><span>2</span></span>
<span><span>                        (</span><span>λ</span><span> (</span><span>k5</span><span>)</span></span>
<span><span>                          (fib* k5</span></span>
<span><span>                            (</span><span>λ</span><span> (</span><span>k6</span><span>)</span></span>
<span><span>                              (+* k4 k6</span></span>
<span><span>                                (</span><span>λ</span><span> (</span><span>k7</span><span>)</span></span>
<span><span>                                  (k k7))))))))))))))))))</span></span></code></pre><p>可以看到，通过continuation的传递，这里显式地确定了，先计算​<code class="">n - 1</code>​，再计算​<code class="">n - 2</code>​，计算顺序是由我们自主控制的；如果把它改成：
</p><pre tabindex="0"><code><span><span>(</span><span>define</span><span> (</span><span>fib*</span><span> n</span><span> k</span><span>)</span></span>
<span><span>  (=* n </span><span>0</span></span>
<span><span>    (</span><span>λ</span><span> (</span><span>k1</span><span>)</span></span>
<span><span>      (</span><span>if</span><span> k1</span></span>
<span><span>        (k </span><span>0</span><span>)</span></span>
<span><span>        (=* n </span><span>1</span></span>
<span><span>          (</span><span>λ</span><span> (</span><span>k2</span><span>)</span></span>
<span><span>            (</span><span>if</span><span> k2</span></span>
<span><span>              (k </span><span>1</span><span>)</span></span>
<span><span>              (-* n </span><span>2</span></span>
<span><span>                (</span><span>λ</span><span> (</span><span>k3</span><span>)</span></span>
<span><span>                  (fib* k3</span></span>
<span><span>                    (</span><span>λ</span><span> (</span><span>k4</span><span>)</span></span>
<span><span>                      (-* n </span><span>1</span></span>
<span><span>                        (</span><span>λ</span><span> (</span><span>k5</span><span>)</span></span>
<span><span>                          (fib* k5</span></span>
<span><span>                            (</span><span>λ</span><span> (</span><span>k6</span><span>)</span></span>
<span><span>                              (+* k4 k6</span></span>
<span><span>                                (</span><span>λ</span><span> (</span><span>k7</span><span>)</span></span>
<span><span>                                  (k k7))))))))))))))))))</span></span></code></pre><p>则改变了运算顺序，结果不变。
</p><p>利用这种能力，可以在函数式语言中实现一个类似C语言的for循环：
</p><pre tabindex="0"><code><span><span>#lang</span><span> racket</span></span>
<span></span>
<span><span>(</span><span>define</span><span> (</span><span>for-loop-cps</span><span> init</span><span> cond?</span><span> update</span><span> body</span><span> k</span><span>)</span></span>
<span><span>  (</span><span>init</span></span>
<span><span>    (</span><span>λ</span><span> (</span><span>initial-state</span><span>)</span></span>
<span><span>      (</span><span>define</span><span> (</span><span>loop</span><span> state</span><span>)</span></span>
<span><span>        (cond?</span></span>
<span><span>          state</span></span>
<span><span>          (</span><span>λ</span><span> (</span><span>continue?</span><span>)</span></span>
<span><span>            (</span><span>if</span><span> continue?</span></span>
<span><span>                (body</span></span>
<span><span>                  state</span></span>
<span><span>                  (</span><span>λ</span><span> ()</span></span>
<span><span>                    (update</span></span>
<span><span>                      state</span></span>
<span><span>                      (</span><span>λ</span><span> (</span><span>new-state</span><span>)</span></span>
<span><span>                        (loop new-state)))))</span></span>
<span><span>                (k state)))))</span></span>
<span><span>      (loop initial-state))))</span></span>
<span></span>
<span><span>(</span><span>define</span><span> (</span><span>init-for</span><span> k</span><span>)</span></span>
<span><span>  (k </span><span>0</span><span>))</span></span>
<span></span>
<span><span>(</span><span>define</span><span> (</span><span>cond-for</span><span> i</span><span> k</span><span>)</span></span>
<span><span>  (k (</span><span>&#x3C;</span><span> i </span><span>10</span><span>)))</span></span>
<span></span>
<span><span>(</span><span>define</span><span> (</span><span>update-for</span><span> i</span><span> k</span><span>)</span></span>
<span><span>  (k (</span><span>+</span><span> i </span><span>1</span><span>)))</span></span>
<span></span>
<span><span>(</span><span>define</span><span> (</span><span>body-for</span><span> i</span><span> k</span><span>)</span></span>
<span><span>  (</span><span>display</span><span> "i = "</span><span>)</span></span>
<span><span>  (</span><span>display</span><span> i)</span></span>
<span><span>  (</span><span>newline</span><span>)</span></span>
<span><span>  (k))</span></span>
<span></span>
<span><span>(for-loop-cps</span></span>
<span><span>  init-for</span></span>
<span><span>  cond-for</span></span>
<span><span>  update-for</span></span>
<span><span>  body-for</span></span>
<span><span>  (</span><span>λ</span><span> (</span><span>final-state</span><span>)</span></span>
<span><span>    (</span><span>display</span><span> "Loop ended. Final i = "</span><span>)</span></span>
<span><span>    (</span><span>display</span><span> final-state)))</span></span></code></pre><p>当然CPS变换的作用不止于此，还可以用它来实现生成器、协程，并且CPS变换的过程也可以通过程序来自动实现，这些内容篇幅较长，以后再单独来展开。
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>反叛、客体与自我的迷思——《地下室手记》读后感</title>
          <link>https://elliot00.com/posts/notes-from-the-underground-reflections</link>
          <description>本文通过细致解构《地下室手记》中的多个主题，探讨了地下室人对自由意志、自我认知与他者凝视的复杂体验。文章首先质疑理性化社会中“一切皆可公式化”的现象，指出当理性凌驾于一切时，个体的主观能动性如何被剥夺；随后，通过对自我客体化、他者凝视及精神自虐等现象的剖析，展示了地下室人在不断被外界标签化的同时，内心反抗与自我救赎的矛盾冲突；而丽莎的出现，更激发了他对成为拯救者的弥赛亚梦的幻想，折射出一种在绝望中求生的悲剧情怀。</description>
          <pubDate>Sun, 09 Feb 2025 10:49:46 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <h2 id="user-content-序" class="">序<a class="" tabindex="-1" href="#序">#</a></h2><p>我的阅读向来比较随缘，当我感到有一段时间没有看过小说时，便想找本来看看，凑巧翻到了陀思妥耶夫斯基的《地下室手记》，就读了下去。虽说是小说，形式却和我读过的一般主要由叙事构成的小说不同，全书分为两个部分，第一部分完全是主角「地下室人」的内心独白。在读完这本书后，我感到这种特殊的形式恰好巧妙地体现出一种反叛，这正是作者想要通过文字的内容所表达的，同时又通过文字的形式表达出来。这或许是个僭越的错觉——在某个恍惚的瞬间，那些撕心裂肺的文字竟像是从我胸腔里涌出的。当然不是妄比陀氏的如椽巨笔，而是那些在黑暗甬道中跌撞的喘息，意外叩响了相隔百年的回音壁。
</p><p>那么，不如我也来写点什么吧……但我不打算写篇论文，只是要记录一些碎片，这能算是读后感吗？也许吧。
</p><h2 id="user-content-关于自由与理性" class="">关于自由与理性<a class="" tabindex="-1" href="#关于自由与理性">#</a></h2><blockquote><p>唉，先生们，当事情已经发展到表格和算术的地步，当只有二二得四红极一时的时候，还有什么自己的意志可言呢？即便没有我的意志，二二也是得四。这也能算自己的意志吗！
</p></blockquote><p>如果说可以设计出一种最好的社会制度，人的一切活动都按照所谓「最好的」、「理性的」方式来进行，那这时人还需要有意志吗？如果整个社会都变成一个大公式，个人的主观能动性与自由意志又如何存在？毕竟二二永远得四，这是客观规律，是不以人的意志为转移的。
</p><p>当然，陀思妥耶夫斯基在这里并非全盘否定理性与科学的价值。「二二得四」的频繁出现，正是在警惕那种将人性简化为可计算、可预测公式的理性主义倾向。理性本身无可厚非，它是认识世界的有力工具；但当理性被绝对化、工具化时，往往会忽略人性中那些复杂、不可预见、甚至略带荒诞的方面。正是在这种背景下，理性化的社会安排便可能剥夺了人的自主性与创造性，甚至让人沦为一个被冷冰冰的法则所左右的「机器」。
</p><p>作者在书中多次提到一个「水晶宫」的意象，译者注释这来自车尔尼雪夫斯基的小说，象征着乌托邦，而陀思妥耶夫斯基是反乌托邦的，如果不能在这个水晶宫里吐舌头，那不如住到地下室去。我倒不以为关于二二得四、关于水晶宫，必须放在反乌托邦的语境中。可以追问：当社会运转完全依赖理性、客观规律、科学以及预设的制度时，人的自主性和不可预测的活力何去何从？
</p><p>恰如马克思在《关于费尔巴哈的提纲》指出的：
</p><blockquote><p>从前的一切唯物主义（包括费尔巴哈的唯物主义）的主要缺点是：对对象、现实、感性，只是从客体的或者直观的形式去理解，而不是把它们当作感性的人的活动，当作实践去理解，不是从主体方面去理解。因此，和唯物主义相反，能动的方面却被唯心主义抽象地发展了
</p></blockquote><p>在一些人对唯心主义、唯物主义浅薄的理解里，可能唯心主义就是有神论，唯物主义就是无神论，唯物就是客观的，实证的。这样一来，有趣的是，人的主观能动性，却在对客观、对理性的绝对肯定中被无情的否定了。
</p><p>水晶宫是精心堆砌的幸福的殿堂，但同时也是自由的坟场。
</p><h2 id="user-content-自我与他者" class="">自我与他者<a class="" tabindex="-1" href="#自我与他者">#</a></h2><p>人似乎生来就能区分自身与外物，小孩子说话想要某个东西，起初是「这个」、「那个」，逐渐地他可以明白「这个」还有专门的名字：「糖果」、「桔子」。人们将具有共性的东西，赋予一个抽象的符号名称，并且能在群体间共享（如果一门语言永远只能有一个人懂，那就也不能称为语言了）。在所有的代词中，有一个特殊的词，那就是「我」，所有人都可以说「我」，但却并不是同一个「我」。
</p><h3 id="user-content-自我的客体化">自我的客体化<a class="" tabindex="-1" href="#自我的客体化">#</a></h3><p>把区别于「我」的事物，称为「对象」，一般来说意识，就是指关于对象的意识。我们常说主体、客体，主体就是自我了，客体就是被意识的对象了。在这种意识中，意识自身和意识的对象，有着清晰的分界线，眼前的桌子当然不会是「我」，「我」也不是眼前的桌子。但是，意识，也可以把自身作为对象的！譬如说反思，站在他人的角度上，审视自己，这就将自我对象化了。我是意识的主体，但同时被意识的我也是客体。
</p><p>其实在说主体客体时，已经暗示了，「我」的存在，无法是孤立的存在。「我」总是与世界同在。
</p><h3 id="user-content-他者的凝视">他者的凝视<a class="" tabindex="-1" href="#他者的凝视">#</a></h3><p>前面说到反思，这是一种主动将自身对象化的行为，但是还存有一种更加普遍的，被动的主体性的丧失，即当他者的目光如探照灯般刺破黑暗时，我们突然发现自己早已被钉死在某个符号坐标之上。别人会怎么看我呢？「书呆子」、「小丑」，在他人的目光中，地下室人感到一种被客体化的恐慌。
</p><p>萨特曾经讲过一个比喻（我记不真切了，也许不是萨特说的，内容也可能不对）：一个男人正站在公寓的门前，通过锁眼偷窥门内的女人；这时他听到楼下传来脚步声，于是立马蹲下身子假装在系鞋带。通过这种伪装的行为，男人试图在被客体化时掌握主动权，他拥有一种自由，即在他人的目光中，将自己塑造成一个偷窥者或一个路过的人的自由。
</p><p>我们的主人公就试图在他人的凝视中，扭转自己的形象，为此他付出了一笔显然超出他收入所能承受水平的费用去参加同学的送别宴会，并且花时间精心打扮自己，只为了获得「敬意」。
</p><blockquote><p>“我请求您的友谊，兹维尔科夫，我羞辱了您，但是……”
“羞辱？您——您！羞辱了我——我？！您要知道，阁下，您在任何时候、任何情况下都羞辱不了我！”
</p></blockquote><p>对于地下室人来说残忍的是，他渴望得到尊重，但他者的目光只是轻描淡写地扫过，如同扫过空气中的灰尘，连片刻的停留都没有。傲慢的军官忽视他，昔日的同学只当他是个可怜的小丑，故意更改宴会的时间却不通知他；连他自以为的「羞辱」，都完全被无视了。这种「不被看见」比直接的羞辱更具毁灭性，因为这意味着他在社会符号体系中被彻底抹除。地下室人要如何消解这种痛苦呢？
</p><h3 id="user-content-精神自虐">精神自虐<a class="" tabindex="-1" href="#精神自虐">#</a></h3><p>认为我是小丑吗？那我便扮演小丑给你看！在被忽视后，地下室人卖力地表演着，莫名地大笑、冷哼，来回踱步，故意发出声响。这种扮演可以说是一种反抗，即使是小丑，也是我自由的选择，他在试图夺回被凝视的主动权。既然你们要将我客体化，那我就自己完成这个客体化过程！是的，我就是小丑、可怜虫，我是地下室的老鼠，阴暗卑鄙无耻下流。
</p><p>另一方面，地下室人越是自我贬抑，其实越显出他对崇高的隐秘渴望。这种自我否定恰恰构成了更复杂的肯定：
</p><blockquote><p>我故意夸大自己的卑劣，是为了让你们没有资格审判我。
</p></blockquote><p>我卑劣，但我有承认自己卑劣的勇气，所以我反比你们更加高尚！有人曾质疑卢梭，认为他的《忏悔录》存在捏造的成分，是为了彰显自己勇于反思忏悔的伟大。
</p><p>这种精神上的自虐，反而使地下室人得到一种快感。将自身贬低为老鼠，却使他更感到自己作为一个活生生的人而存在着。
</p><h3 id="user-content-丽莎他者唤起的弥赛亚梦">丽莎：他者唤起的弥赛亚梦<a class="" tabindex="-1" href="#丽莎他者唤起的弥赛亚梦">#</a></h3><p>在聚餐结束后的偶遇（尽管我还有别的词汇来形容，但为了地下室人的体面，还是用偶遇吧）中，地下室人邂逅了丽莎——一个在他眼中既是卑微现实时的对立面，又是他心中那遥不可及的拯救符号。与以往在军官和富有同学面前所承受的冷眼和忽视不同，丽莎的出现为他提供了一次自我重构的契机。那一刻，他不再只是一个被社会抛弃的小丑，而仿佛化身为能够给予救赎的弥赛亚，一个试图以英雄般的姿态对抗内心孤独和现实压迫的存在。
</p><p>在丽莎面前，地下室人展开了截然不同的表演。他不再仅仅满足于自我贬低和被迫的自我否定，而是开始沉溺于那种将自卑转化为拯救力量的幻想之中。他幻想自己能成为她生命中的一束微光，拯救她于堕落与无望之中，同时也借此获得对自我价值的最后确认。
</p><p>然而，这样的拯救并非来自真实的爱与理解，而更像是地下室人内心深处那种激烈矛盾情感的外化。他在丽莎身上看到的不仅是一个可以拯救的对象，更是一面镜子，映射出他那充满焦虑、矛盾与破碎自尊的内心世界。丽莎既代表着他追求超越现状的渴望，又暴露了他无力逃脱自我否定命运的悲剧本质。她的温柔、脆弱，和那种从生活深渊中挣扎出来的倔强，令地下室人既为之倾倒，又深陷自我厌弃的恶性循环之中。
</p><p>可以说，地下室人试图以「弥赛亚」的姿态对抗命运的嘲弄和现实的残酷，却又在这一过程中暴露了他固有的虚无和悲哀。丽莎成为了他最后的救赎幻影，是他用来逃避现实、重新塑造自我形象的唯一依托。然而，这种由幻想而生的英雄梦，终究逃不开那不可调和的宿命：在自我崇高的理想与对卑微本质的无力割舍之间，地下室人的每一次挣扎，都似乎预示着自我毁灭的步伐。
</p><h2 id="user-content-终" class="">终<a class="" tabindex="-1" href="#终">#</a></h2><p>未竟……
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>2024</title>
          <link>https://elliot00.com/posts/review-2024</link>
          <description>2024年度总结</description>
          <pubDate>Sun, 29 Dec 2024 07:14:22 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <p>又到了一年一度该总结的时候了，虽说我前几年并没有做过，但考虑到今天是2024年最后一个周日，正好赶上我生日，那就稍微写一点吧。
</p><h2 id="user-content-代码" class="">代码<a class="" tabindex="-1" href="#代码">#</a></h2><h3 id="user-content-新技术">新技术<a class="" tabindex="-1" href="#新技术">#</a></h3><ul><li>点亮了Android/Kotlin技能树
</li><li>更进一步了解了Wayland协议，做了IME的初版实现
</li><li>前端方面学习了SolidJS
</li><li>尝试了新的LISP方言——Racket，并用它刷了一些LeetCode题
</li></ul><h3 id="user-content-自由软件">自由软件<a class="" tabindex="-1" href="#自由软件">#</a></h3><ul><li>写了<a href="https://github.com/Eliot00/mp-org">mp-org</a>并收到了用户的赞助
</li><li>第一个npm库<a href="https://codeberg.org/Elliot00/docube">docube</a>并用在了自己的博客上
</li><li>复制了一个<a href="https://txtmoji.elliot00.com/">txtmoji</a>，娱乐项目
</li><li>开始做我的<a href="https://app.benkyou.fun/">日语学习网站</a>，使用SolidJS
</li></ul><h2 id="user-content-博客" class="">博客<a class="" tabindex="-1" href="#博客">#</a></h2><p>今年是分享比较多的一年，除掉本文，今年博客一共更新了16篇文章，对比之下22年加上23年只更新了5篇，​<del>不禁要给自己点个赞</del>​。今年开了两个系列的坑，希望明年能继续保持更新频率。
</p><h2 id="user-content-生活" class="">生活<a class="" tabindex="-1" href="#生活">#</a></h2><p>平静的2024，没什么变化。
</p><h2 id="user-content-读书" class="">读书<a class="" tabindex="-1" href="#读书">#</a></h2><p>今年读的书大部分是技术类的，除技术类外今年读过的最喜欢的一本书是弗兰克·梯利的《西方哲学史》。
</p><h2 id="user-content-音乐" class="">音乐<a class="" tabindex="-1" href="#音乐">#</a></h2><p>去年是摇滚的一年，今年则是R&#x26;B的一年。发现R&#x26;B也挺对我的口味的，并且听了很多华语R&#x26;B，首次听了陶喆和方大同的歌，还有因为《歌手》节目认识了黄宣，非常不错的年轻歌手。
</p><p>本想把披头士那首跨越时空的新歌《Now and Then》也放进来，才发现这歌其实是去年年底发的，就这样吧。
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>初中生也能懂的机器学习（一）</title>
          <link>https://elliot00.com/posts/ml-regression-note</link>
          <description>本文旨在为对机器学习感兴趣的初学者提供一份详细的线性回归入门教程。文章从基础概念入手，逐步深入，并结合实际案例进行讲解。通过阅读本文，读者可以了解线性回归的原理、实现步骤，并掌握基本的Python编程技巧。</description>
          <pubDate>Sun, 17 Nov 2024 08:54:22 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <h2 id="user-content-前言" class="">前言<a class="" tabindex="-1" href="#前言">#</a></h2><p>虽然曾经有看过一些关于机器学习的书及视频，但总感觉没有特别确切地理解了机器学习。本着费曼学习法的原则，我想尝试假设为一位仅有初中数学知识的学生讲解什么是机器学习，看我是否能较为清晰地解释机器学习的基本概念。
</p><p>本文假定的读者是一位刚刚上初三的学生，已经掌握：
</p><ol><li>整数、小数四则运算
</li><li>平面直角坐标系
</li><li>一元函数
</li></ol><p>当然文中会列出一些代码，但并不会影响到对机器学习的理解，可以跳过。
</p><h2 id="user-content-机器也可以学习吗" class="">机器也可以学习吗？<a class="" tabindex="-1" href="#机器也可以学习吗">#</a></h2><p>人类，显然可以做学习的主语，但，人类的造物是否也可以学习呢？首先来考察下，人类通常如何学习？
</p><p>当我们看到天空乌云密布，就可以知道不久将要下雨；面包上出现了霉点，就不应当再食用，否则可能中毒；农民会根据节气安排劳作。至少可以说，人类拥有利用经验的能力，通过经验，预测或判断天气、食物是否安全、何时播种等。
</p><p>机器是否也能像人一样，利用经验去解决问题呢？这正是「机器学习」这门学科的研究方向。
</p><p>软件工程师应该很熟悉用代码来表示一个计算过程，对于一个已知的计算过程，例如分析物体受的外力，有牛顿第二定律：
</p><p>如果用Python代码来表达的话，可以写成这样：
</p><pre tabindex="0"><code><span><span>def</span><span> F</span><span>(</span><span>m</span><span>,</span><span> a</span><span>)</span><span>:</span></span>
<span><span>    return</span><span> m </span><span>*</span><span> a</span></span></code></pre><p>只要有物体的重量和加速度，即可求得对物体施加的外力，这是一个「演绎」的过程。
</p><p>如果在另一条时间线上，人类没有发现这条定律，但是通过实验，记录了很多组F、m、a的数据，如何逆向「归纳」出m、a和F的关系呢？这就是机器学习要解决的问题了。
</p><p>如果将已知计算方法，给定参数求结果的过程看作一台流水线机器，给它原材料，它会输出产品；那么机器学习要做的事就是在没有机器图纸的情况下，「逆向工程」破解这台机器。
</p><h3 id="user-content-一些术语">一些术语<a class="" tabindex="-1" href="#一些术语">#</a></h3><p>如前所述，对机器学习来说数据是非常重要的，这里用kaggle上的一个<a href="https://www.kaggle.com/datasets/nehalbirla/vehicle-dataset-from-cardekho/data">二手车辆数据</a>来举例：
</p><table><thead><tr><th>品牌</th><th>买车年份</th><th>行驶里程</th><th>价格</th></tr></thead><tbody><tr><td>Honda</td><td>2014</td><td>14100</td><td>450000</td></tr><tr><td>Maruti</td><td>2007</td><td>70000</td><td>60000</td></tr></tbody></table><p>这样一系列的数据称为「​<strong>数据集</strong>​」（data set），每一行称为一个「​<strong>样本</strong>​」（sample），每一列反映了车辆数据的一个属性，称为「​<strong>特征</strong>​」（feature）。
</p><p>数据之间存在一种潜在的关系（哲学地说，没有关系也是一种关系），如行驶里程和价格的关系，通过数据集归纳总结数据的关系的过程可以称为「​<strong>学习</strong>​」（learning）或「​<strong>训练</strong>​」（train）。
</p><p>在数据中如果有一个标记值，如价格，我们训练的目的是通过数据得到车辆行驶里程和价格的关系，把真实世界中里程和价格的关系称为「​<strong>真实</strong>​」（ground truth），训练的结果就叫做「​<strong>模型</strong>​」（model）或「​<strong>假设</strong>​」（hypothesis）。
</p><p>如果标记值是一个连续的量（可以用连续的数字表示），如行驶里程、轴长等，求特征值和标记值之间的关系，这种训练任务叫做「​<strong>回归</strong>​」（regression）；如果标记值是离散的量（有限的类别值），如新车或旧车，就叫「​<strong>分类</strong>​」（classification）。
</p><p>回归和分类的共同点是数据集中已有确定的标记信息，这种有标记信息的学习任务被称为「​<strong>监督学习</strong>​」（supervised learning），相对的，另一类样本中没有标记信息的学习任务被称为「​<strong>无监督学习</strong>​」（unsupervised learning），其代表为「​<strong>聚类</strong>​」（clustering）。
</p><p>通过分析数据，可以根据特征之间的相似性，将数据分组，每个组称为「​<strong>簇</strong>​」（cluster），如将有相似价格、燃油类型、行驶里程等特征的车分到同一簇，便于进一步分析。与分类不同，在数据样本里没有直接已知的分类标准，分簇的标准是数据间的潜在联系。
</p><p>最后，对于训练得到的模型，需要有种手段去验证它，就需要将已有的数据集分出一部分来，用于检验模型的好坏。这部分数据样本称为「​<strong>测试样本</strong>​」（testing sample），那么数据集中用来做训练的样本，自然就叫做「​<strong>训练</strong>​」（training sample）了。
</p><h2 id="user-content-线性回归" class="">线性回归<a class="" tabindex="-1" href="#线性回归">#</a></h2><h3 id="user-content-函数">函数<a class="" tabindex="-1" href="#函数">#</a></h3><p>在了解什么是线性回归前，让我们一起稍稍回顾下函数的概念。
</p><p>将一群对象的整体称为集合，集合中的对象就称为元素。如中国人是一个集合，每个中国人就是这个集合的一个元素；自然数也是一个集合，1、2、3……就是这个集合的元素。一个元素都没有的集合被叫做空集。
</p><p>设有两个非空的集合X、Y，如果X中的每一个元素x，都在Y集合中有​<strong>唯一</strong>​的元素y与之对应的话，就把X到Y的这种关系称为X到Y的一个函数，记作f（这里的字母没有特别意义，可以随便用abcd替代）。例如正整数集合，对每个元素加一，结果还是属于正整数，这样的关系可以描述成函数：
</p><p>也可以写成：
</p><p>把集合X称为函数f的​<strong>作用域</strong>​，也就是x的取值范围，在这里是正整数，数学符号是​​，函数f所有可能的输出的集合，称为f的​<strong>值域</strong>​。
</p><p>再回看对回归的定义，如果把数据集中的特征值集合当作输入X，标记值当作输出Y，就可以认为真实世界里有一个X到Y的函数记为t（truth），回归学习的任务就是从输入和输出上尽可能地还原出t，从这个角度说，回归这个命名是非常传神的。
</p><p>函数有很多种，线性函数、高次函数、多元函数等等，相对应地，本文要讲的线性回归任务，即把输入和输出之间看作是一个线性函数的关系。
</p><p>那什么是线性函数呢？简单地说就是在函数图像上表现为一条直线的函数。可以表示为​​，其中m和b是未知的常数。如果你是在我的博客上看到这篇文章，那么应该可以在下面看到一个交互式的函数图，可以试看拖动调整m和b的值，查看这两个未知变量对函数图像的影响。
</p>
<p>如果没有看到这个嵌入的交互网页，可以用浏览器打开<a href="https://www.desmos.com/calculator/hl3fkigwsj">desmos</a>查看。
</p><p><img alt="线性函数" src="https://r2.elliot00.com/ml/linear-function-nR08kb.webp" width="2220" height="1442"></p><p>如果只看图像的右半部分，也就是横轴x大于0的部分，可以发现当w变化时，直线和横轴之间的夹角也会发生变化，w就被称为函数的斜率，它影响了函数图像的倾斜程度。当w不变时，改变b，会发现直线在上下移动，b就称为函数的截距（b的绝对值就是直线和y轴交点到0点的距离）。
</p><h3 id="user-content-实践任务">实践任务<a class="" tabindex="-1" href="#实践任务">#</a></h3><p>现在来找一个问题做个实践。仍然以车辆数据为例，一般来说可以认为车辆越旧（买入年代越早），卖价就会越低，这样就可以将特征（年份）和标记值（价格）联系起来，记年份为x，价格为y，x和y的真实关系为函数t。
</p><p>现在假设这个关系是个线性关系，记作​<code class="">f(x) = wx + b</code>​，其中w和b是未知的，我们的训练任务应该是求w和b的值，使得综合来看f(x)和t最接近。
</p><h3 id="user-content-代价函数">代价函数<a class="" tabindex="-1" href="#代价函数">#</a></h3><p>那么怎么判断选定了w和b后的f(x)和t是否接近呢？
</p><p>数学家勒让德给出了一种方法：​<strong>最小二乘法</strong>​。 首先给我们的数据编个号，设有m行数据，定义序号i属于1到m，用​x<sub>i</sub>​表示第i个年份，用​y<sub>i</sub>​表示第i个价格，这样就有了m组​(x<sub>i</sub>, y<sub>i</sub>)​。对每一组样本数据，将​x<sub>i</sub>​代入假设函数，也能得到一个输出值，把这个输出值记作估计值​​，y上的小帽子代表估计。
</p><p>每一组训练样本中，用​<strong>估计值</strong>​减去​<strong>y</strong>​，得到的值称为​<strong>误差</strong>​，再把误差求平方，再把每一组数据上的误差加起来求和，这样看起来是不是能让这个误差平方和最小的假设函数就是最好的假设函数呢？
</p><div><div><div></div><div>Tip</div></div><div><p>和微积分一样，最小二乘法的发现者也具有争议。勒让德于1806年率先提出了最小二乘法，后来高斯声称他其实更早就已经发现，并在1829年给出了最小二乘法法优于其他方法的证明。
</p></div></div><p>这样我们训练的目的，就是找出一对w和b，使得假设函数f(x) = wx + b在每一组样本上得到的估计值，与观测值y的误差的平方的和最小。用数学符号表示为：
</p><p>最终得到的最好的假设函数就是我们要训练的结果，也可以叫做模型，而w和b就是模型的参数。
</p><div><div><div></div><div>Note</div></div><div><p>注意前面我将y称为观测值，而不是真实值。回想一下小学几何知识：「两点确定一条直线」，理论上根本不需要复杂的计算，有两个样本数据不就可以得到线性函数了吗？但要考虑到人类的观测都是有误差的，所以对于观测的样本数据，不能直接说它是真实数据，训练以使假设函数和数据样本「适配」的过程，也被称为「​<strong>拟合</strong>​」，而不是求真。
</p></div></div><p>但在实践中，我们要在这个方法上做点变形，使用​<strong>均方误差</strong>​。也就是对于m组数据，求误差的平方的和，再除以m求平均值，再除个2。对于不同的w和b的取值，都可以在训练数据上计算出一个均方误差来，那么是不是可以把它表示成一个关于w和b的二元二次函数呢？这个函数在机器学习中就叫「​<strong>代价函数</strong>​」（cost function）。用数学符号表示为：
</p><div><div><div></div><div>Note</div></div><div><p>如果读者已经忘了什么是二元二次函数，这里做个不严谨的解释：J(w, b)，括号里有两个量，会影响到函数的取值，就称为二元函数，f(x)就是一元函数，二次指的是函数表达式里有个平方（二次方）。
</p></div></div><p>用一个表格来举个例子：
</p><table><thead><tr><th>x</th><th>y</th><th>设w=2,b=2</th><th>误差</th></tr></thead><tbody><tr><td>2</td><td>3</td><td>6</td><td>3</td></tr><tr><td>3</td><td>5</td><td>8</td><td>3</td></tr><tr><td>4</td><td>9</td><td>10</td><td>1</td></tr></tbody></table><p>这里当w取2，b也取2时，代价函数的值，也就是均方误差是多少呢？就是3的平方加3的平方加1的平方，再除训练样本数量3，再除2，结果是六分之十九。
</p><div><div><div></div><div>Tip</div></div><div><p>求平均值直接除m不就可以了吗？为什么要额外除个2呢？数学直觉比较好的朋友可以想想。提示：试试对代价函数求偏导。
</p></div></div><p>总结一下，现在我们有了两个函数：
</p><ol><li>假设函数f(x)
</li><li>代价函数J(w, b)
</li></ol><p>我们的任务就是找出一对参数w和b，使得J(w, b)最小，这样代入假设函数就得到最后的模型了。那具体用什么方法去做呢？在回答这个问题之前，让我们先观察一下数据和代价函数的图像。
</p><h3 id="user-content-数据处理">数据处理<a class="" tabindex="-1" href="#数据处理">#</a></h3><p>下面用Python来处理数据并画图：
</p><pre tabindex="0"><code><span><span>import</span><span> pandas </span><span>as</span><span> pd</span></span>
<span><span>import</span><span> matplotlib</span><span>.</span><span>pyplot </span><span>as</span><span> plt</span></span>
<span></span>
<span><span>cars </span><span>=</span><span> pd</span><span>.</span><span>read_csv</span><span>(</span><span>'</span><span>./data/car_details_v4.csv</span><span>'</span><span>)</span></span>
<span><span>cars </span><span>=</span><span> cars</span><span>[</span><span>[</span><span>'</span><span>Price</span><span>'</span><span>,</span><span> '</span><span>Year</span><span>'</span><span>]</span><span>].</span><span>dropna</span><span>(</span><span>subset</span><span>=</span><span>[</span><span>'</span><span>Price</span><span>'</span><span>, </span><span>'</span><span>Year</span><span>'</span><span>]</span><span>)</span></span>
<span></span>
<span><span>plt</span><span>.</span><span>scatter</span><span>(</span><span>cars</span><span>[</span><span>'</span><span>Year</span><span>'</span><span>]</span><span>,</span><span> cars</span><span>[</span><span>'</span><span>Price</span><span>'</span><span>]</span><span>,</span><span> alpha</span><span>=</span><span>0.7</span><span>,</span><span> edgecolors</span><span>=</span><span>'</span><span>w</span><span>'</span><span>,</span><span> linewidth</span><span>=</span><span>0.5</span><span>)</span></span>
<span><span>plt</span><span>.</span><span>xlabel</span><span>(</span><span>'</span><span>Year</span><span>'</span><span>)</span></span>
<span><span>plt</span><span>.</span><span>ylabel</span><span>(</span><span>'</span><span>Price</span><span>'</span><span>)</span></span>
<span><span>plt</span><span>.</span><span>show</span><span>()</span></span></code></pre><p><img alt="年份价格关系图" src="https://r2.elliot00.com/ml/year-price-1Uu2sA.webp" width="1134" height="878"></p><pre tabindex="0"><code><span><span>import</span><span> numpy </span><span>as</span><span> np</span></span>
<span></span>
<span><span>X </span><span>=</span><span> cars</span><span>[</span><span>[</span><span>'</span><span>Year</span><span>'</span><span>]</span><span>].</span><span>values</span></span>
<span><span>y </span><span>=</span><span> cars</span><span>[</span><span>[</span><span>'</span><span>Price</span><span>'</span><span>]</span><span>].</span><span>values</span></span>
<span></span>
<span><span>def</span><span> cost</span><span>(</span><span>w</span><span>,</span><span> b</span><span>,</span><span> X</span><span>,</span><span> y</span><span>)</span><span>:</span></span>
<span><span>    m </span><span>=</span><span> len</span><span>(</span><span>y</span><span>)</span></span>
<span><span>    f </span><span>=</span><span> w </span><span>*</span><span> X </span><span>+</span><span> b</span></span>
<span><span>    errors </span><span>=</span><span> f </span><span>-</span><span> y</span></span>
<span><span>    return</span><span> (</span><span>1</span><span> /</span><span> (</span><span>2</span><span> *</span><span> m)) </span><span>*</span><span> np</span><span>.</span><span>sum</span><span>(</span><span>errors </span><span>**</span><span> 2</span><span>)</span></span>
<span></span>
<span><span># 不能在图上表示无限的w和b，所以只展示部分参数范围</span></span>
<span><span>w_range </span><span>=</span><span> np</span><span>.</span><span>linspace</span><span>(</span><span>-</span><span>10000</span><span>,</span><span> 10000</span><span>,</span><span> 100</span><span>)</span></span>
<span><span>b_range </span><span>=</span><span> np</span><span>.</span><span>linspace</span><span>(</span><span>-</span><span>10000</span><span>,</span><span> 10000</span><span>,</span><span> 100</span><span>)</span></span>
<span></span>
<span><span># 计算代价函数值</span></span>
<span><span>cost_values </span><span>=</span><span> np</span><span>.</span><span>zeros</span><span>(</span><span>(</span><span>len</span><span>(</span><span>w_range</span><span>)</span><span>, </span><span>len</span><span>(</span><span>b_range</span><span>)</span><span>)</span><span>)</span></span>
<span></span>
<span><span>for</span><span> i</span><span>,</span><span> w </span><span>in</span><span> enumerate</span><span>(</span><span>w_range</span><span>):</span></span>
<span><span>    for</span><span> j</span><span>,</span><span> b </span><span>in</span><span> enumerate</span><span>(</span><span>b_range</span><span>):</span></span>
<span><span>        cost_values</span><span>[</span><span>i</span><span>,</span><span> j</span><span>]</span><span> =</span><span> cost</span><span>(</span><span>w</span><span>,</span><span> b</span><span>,</span><span> X</span><span>,</span><span> y</span><span>)</span></span>
<span></span>
<span><span># 画出代价函数图像</span></span>
<span><span>w_grid</span><span>,</span><span> b_grid </span><span>=</span><span> np</span><span>.</span><span>meshgrid</span><span>(</span><span>w_range</span><span>,</span><span> b_range</span><span>)</span></span>
<span><span>fig </span><span>=</span><span> plt</span><span>.</span><span>figure</span><span>(</span><span>figsize</span><span>=</span><span>(</span><span>12</span><span>, </span><span>8</span><span>)</span><span>)</span></span>
<span><span>ax </span><span>=</span><span> fig</span><span>.</span><span>add_subplot</span><span>(</span><span>111</span><span>,</span><span> projection</span><span>=</span><span>'</span><span>3d</span><span>'</span><span>)</span></span>
<span><span>ax</span><span>.</span><span>plot_surface</span><span>(</span><span>w_grid</span><span>,</span><span> b_grid</span><span>,</span><span> cost_values.T</span><span>,</span><span> cmap</span><span>=</span><span>'</span><span>viridis</span><span>'</span><span>)</span></span>
<span></span>
<span><span>ax</span><span>.</span><span>set_xlabel</span><span>(</span><span>'</span><span>w (Slope)</span><span>'</span><span>)</span></span>
<span><span>ax</span><span>.</span><span>set_ylabel</span><span>(</span><span>'</span><span>b (Intercept)</span><span>'</span><span>)</span></span>
<span><span>ax</span><span>.</span><span>set_zlabel</span><span>(</span><span>'</span><span>Cost</span><span>'</span><span>)</span></span>
<span></span>
<span><span>plt</span><span>.</span><span>title</span><span>(</span><span>'</span><span>Cost Function Surface</span><span>'</span><span>)</span></span>
<span><span>plt</span><span>.</span><span>show</span><span>()</span></span></code></pre><p><img alt="代价函数图" src="https://r2.elliot00.com/ml/cost-function-8kR10q.webp" width="1304" height="1298"></p><p>从图形上看，代价函数处在三维空间里，像是一个山谷的形状，这个「山谷」的谷底就是我们要找的那一点。但是机器学习不能靠用肉眼去看图像，应该用计算公式（算法）来找这一点。
</p><h3 id="user-content-梯度与极值">梯度与极值<a class="" tabindex="-1" href="#梯度与极值">#</a></h3><p>三维空间的图形分析起来有一点麻烦，所以让我们先对代价函数做一次「降维打击」吧。
</p><p>先假设b参数固定不变，当作一个常数，那么代价函数就从二元二次函数变成了一元二次函数。想象有一个碗，从中间切开它，忽略它的厚度，切面是不是就相当于一个U型的线呢？固定b，就相当于从图像上与b轴平行的位置「切了一刀」。
</p><p>我们来看看这样一个二次函数有什么性质：
</p>
<p>二次函数构成的曲线图像也是有斜率的，只是与直线有固定的斜率不同，曲线的斜率是动态的。拖动Q点的位置，查看曲线斜率的变化。如果你没有看到交互式的图像，这里还准备了一张静态图：
</p><p><img alt="一元二次函数" src="https://r2.elliot00.com/ml/slope-mR1q8z.webp" width="1696" height="1040"></p><p>静态图中标出了5个点的斜率（就是图中的slope，绘图库默认没有中文字体就用英文表示了）。
</p><p>总之，从图上可以看出，图像显示区域内，最低的那一点，它的斜率是0，切线是水平线，在这一点越往右边，斜率越大；越往左边，斜率越小。并且，左边的斜率都是负数，右边的都是正数。
</p><p>如何去求任一点的斜率？在数学上有个方法，对函数「​<strong>求导</strong>​」，得到一个导函数，代入原函数上一点的x值，得到「​<strong>导数</strong>​」，这个导数就是这一点的斜率。先不用管导数是什么，只要记住有这么个方法就行了。
</p><div><div><div></div><div>Tip</div></div><div><p>这里举例的函数，实际上有个简单的方法去求它的极小值。搜索关键词：导数、驻点、极值
</p></div></div><h3 id="user-content-梯度下降法">梯度下降法<a class="" tabindex="-1" href="#梯度下降法">#</a></h3><p>前置的理论介绍得差不多了，接下来进入实操阶段，这里介绍真正用来做线性回归训练的算法：梯度下降法。
</p><p>首先要给参数w和b设置一个初始值，通常会先都设为0。接下来：
</p><ol><li>更新w为​​
</li><li>更新b为​​
</li></ol><p>还是先以将b固定，只考虑w的情况来讨论，w的更新公式里有个希腊字母α（alpha），它表示的是「​<strong>学习率</strong>​」（learning rate），一般是0到1之间的正数。学习率后面那个复杂的符号叫做代价函数J上对w的偏导，可以视为忽略b后的二次函数的导数，也就是一元二次函数曲线上w点的斜率。
</p><p>两个偏导用数学符号表示为：
</p><p>如果你有学过微积分，可以试着自己推导一下，本文最后也会放上求偏导的过程。
</p><div><div><div></div><div>Tip</div></div><div><p>参数w和b是在学习中逐渐更新求得的，而像学习率这样事先给定的控制学习过程的量，又被叫做「​<strong>超参数</strong>​」（hyperparameter）。
</p></div></div><p>回顾刚刚提到的只考虑w的一元二次代价函数的性质，如果我们选的初始w值在极小值点的右边，那么斜率是一个正数，乘上正的学习率，结果还是正数，w更新为w减一个正数，是不是就越来越小了呢？也就是w向左移动（向极小值点的方向移动）了。
</p><p>反过来，如果w在极小值点的左边，那么斜率是一个负数，乘上正的学习率，结果是负数，w更新为w减一个负数，岂不是就相当于加一个正数？那么w不就增大了吗？此时w向右（也就是极小值所在的方向）移动了。
</p><p>那么再来看看学习率这个数字有什么用。如果学习率非常小，是不是可以认为每一步对w的更新都很小呢？也就是学习的速度变慢了。如果学习率取值非常大呢？就用前面图像所示的​<code class="">f(x) = 2x^2 + 4x + 3</code>​为例，这里直接给出它的导函数：​<code class="">f'(x) = 4x + 4</code>​，如果学习率设为1000，w初始为0，第一次更新后，w变为0 - 4 * 1000等于-4000，再次更新后会变为15992000，诶，怎么左脚踩右脚上天了呢？可见，学习率​<strong>既不能太小，也不能太大</strong>​。
</p><p>另一方面，当w离极小值点越远，绝对值就越大；离极值点越近，绝对值就越小。也就是说，w从右到左靠近极值点时，每一步更新减去的值会越来越小；从左到右靠近极值点时，每一步的更新增加的值也是越来越小的。可见这个斜率（导数）的性质相当好，居然具有自我调节的作用。
</p><p>最后，如果w已经在极小值点上了，这时这点的斜率为0，任何数减去或加上0，结果还是这个数本身。所以，在梯度下降法中，只要最后参数不再变化了，就说明模型训练完成了。
</p><h3 id="user-content-代码实现">代码实现<a class="" tabindex="-1" href="#代码实现">#</a></h3><p>现在尝试将梯度下降法用代码实现出来。
</p><p>首先得注意，在计算机中，所有数据都是用二进制表示的。什么是二进制？我们说n进制，就代表将数字的每一位，用0到n-1来表示，如生活中常用的十进制，代表每一位数学只能用0到9来表示，大于9就要向前进一位。二进制就显然每一位只能是0或1了。
</p><p>由于存储设备空间是有限的，同时也为了处理方便，计算机通常用固定的位数——如32位——来存储数学，这就意味着数学上像π这样的无限不循环小数无法被精确的表示。另外，十进制的有些数，如0.1，转换成二进制表示会变成一无限循环小数，存储时也要损失精度。
</p><p>这样一来，前面说的用斜率是否为0去判断模型训练是否完成就行不通了。怎么办呢？可以定义一个非常小的小数，这里管它叫epsilon，如果某次参数更新后，代价函数值变化小于这个epsilon了，就可以认为训练成功了。为了双重保险，再加上一个最大更新次数，更新参数的次数超过这个值，算法也直接停止。
</p><pre tabindex="0"><code><span><span>epislon </span><span>=</span><span> 1e-6</span><span> # 0.000001的简写</span></span>
<span><span>max_iterations </span><span>=</span><span> 10000</span></span></code></pre><div><div><div></div><div>Note</div></div><div><p>这种判断方法其实也还有问题，但这里例子中的年份和价格本身相关性也不是很好，所以更细节的内容留到下一篇讲多元线性回归再讲吧。
</p></div></div><p>下一步设置学习率alpha和初始参数w和b，并读入车辆数据：
</p><pre tabindex="0"><code><span><span>import</span><span> numpy </span><span>as</span><span> np</span></span>
<span><span>import</span><span> pandas </span><span>as</span><span> pd</span></span>
<span></span>
<span><span>alpha </span><span>=</span><span> 0.01</span></span>
<span></span>
<span><span>class</span><span> LinearModel</span><span>:</span></span>
<span><span>    def</span><span> __init__</span><span>(</span><span>self</span><span>,</span><span> data_path</span><span>)</span><span>:</span></span>
<span><span>        cars </span><span>=</span><span> pd</span><span>.</span><span>read_csv</span><span>(</span><span>data_path</span><span>)</span></span>
<span><span>        cars </span><span>=</span><span> cars</span><span>[</span><span>[</span><span>'</span><span>Price</span><span>'</span><span>,</span><span> '</span><span>Year</span><span>'</span><span>]</span><span>].</span><span>dropna</span><span>(</span><span>subset</span><span>=</span><span>[</span><span>'</span><span>Price</span><span>'</span><span>, </span><span>'</span><span>Year</span><span>'</span><span>]</span><span>)</span></span>
<span><span>        self</span><span>.</span><span>X </span><span>=</span><span> cars</span><span>[</span><span>[</span><span>'</span><span>Year</span><span>'</span><span>]</span><span>].</span><span>values</span></span>
<span><span>        self</span><span>.</span><span>y </span><span>=</span><span> cars</span><span>[</span><span>[</span><span>'</span><span>Price</span><span>'</span><span>]</span><span>].</span><span>values</span></span>
<span><span>        self</span><span>.</span><span>w </span><span>=</span><span> 0.0</span></span>
<span><span>        self</span><span>.</span><span>b </span><span>=</span><span> 0.0</span></span></code></pre><p>这里使用了Python中的类，虽说日常写代码我更喜欢用函数定义，但既然在机器学习中常常说模型，这里就用类来定义它，做个名称上的对应吧。
</p><p>具体的代码里引用了numpy和pandas这两个库，用于简化代码，例如这里通过​<code class="">cars[['Year']].values</code>​就取出了csv文件中所有的年份这一列，后续还可以直接用​<code class="">w * X</code>​的形式计算对所有特征值做乘积。
</p><p>接着就要定义出类方法形式的代价函数和应用梯度下降法的训练过程了：
</p><pre tabindex="0"><code><span><span>def</span><span> train</span><span>(</span><span>self</span><span>)</span><span>:</span></span>
<span><span>    iteration </span><span>=</span><span> 0</span></span>
<span><span>    prev_cost </span><span>=</span><span> float</span><span>(</span><span>'</span><span>inf</span><span>'</span><span>)</span></span>
<span><span>    while</span><span> iteration </span><span>&#x3C;</span><span> max_iterations</span><span>:</span></span>
<span><span>        cost </span><span>=</span><span> self</span><span>.</span><span>cost</span><span>()</span></span>
<span><span>        if</span><span> abs</span><span>(</span><span>prev_cost </span><span>-</span><span> cost</span><span>)</span><span> &#x3C;</span><span> epsilon</span><span>:</span></span>
<span><span>            break</span></span>
<span><span>        prev_cost </span><span>=</span><span> cost</span></span>
<span><span>        self</span><span>.</span><span>gradient_descent</span><span>()</span></span>
<span><span>        iteration </span><span>+=</span><span> 1</span></span>
<span><span>    print</span><span>(</span><span>f</span><span>"训练经过了</span><span>{</span><span>iteration</span><span>}</span><span>次迭代"</span><span>)</span></span>
<span><span>    print</span><span>(</span><span>f</span><span>"最终 w=</span><span>{</span><span>self</span><span>.w</span><span>}</span><span> b=</span><span>{</span><span>self</span><span>.b</span><span>}</span><span>"</span><span>)</span></span>
<span></span>
<span><span>def</span><span> cost</span><span>(</span><span>self</span><span>)</span><span>:</span></span>
<span><span>    m </span><span>=</span><span> len</span><span>(</span><span>self</span><span>.y</span><span>)</span></span>
<span><span>    f </span><span>=</span><span> self</span><span>.</span><span>w </span><span>*</span><span> self</span><span>.</span><span>X </span><span>+</span><span> self</span><span>.</span><span>b</span></span>
<span><span>    errors </span><span>=</span><span> f </span><span>-</span><span> self</span><span>.</span><span>y</span></span>
<span><span>    return</span><span> (</span><span>1</span><span> /</span><span> (</span><span>2</span><span> *</span><span> m)) </span><span>*</span><span> np</span><span>.</span><span>sum</span><span>(</span><span>errors </span><span>**</span><span> 2</span><span>)</span></span>
<span></span>
<span><span>def</span><span> gradient_descent</span><span>(</span><span>self</span><span>)</span><span>:</span></span>
<span><span>    dw</span><span>,</span><span> db </span><span>=</span><span> self</span><span>.</span><span>compute_gradients</span><span>()</span></span>
<span><span>    self</span><span>.</span><span>w </span><span>-=</span><span> alpha </span><span>*</span><span> dw</span></span>
<span><span>    self</span><span>.</span><span>b </span><span>-=</span><span> alpha </span><span>*</span><span> db</span></span>
<span></span>
<span><span>def</span><span> compute_gradients</span><span>(</span><span>self</span><span>)</span><span>:</span></span>
<span><span>    m </span><span>=</span><span> len</span><span>(</span><span>self</span><span>.y</span><span>)</span></span>
<span><span>    f </span><span>=</span><span> self</span><span>.</span><span>w </span><span>*</span><span> self</span><span>.</span><span>X </span><span>+</span><span> self</span><span>.</span><span>b</span></span>
<span><span>    errors </span><span>=</span><span> f </span><span>-</span><span> self</span><span>.</span><span>y</span></span>
<span><span>    dw </span><span>=</span><span> (</span><span>1</span><span> /</span><span> m) </span><span>*</span><span> np</span><span>.</span><span>sum</span><span>(</span><span>errors </span><span>*</span><span> self</span><span>.X</span><span>)</span></span>
<span><span>    db </span><span>=</span><span> (</span><span>1</span><span> /</span><span> m) </span><span>*</span><span> np</span><span>.</span><span>sum</span><span>(</span><span>errors</span><span>)</span></span>
<span><span>    return</span><span> dw</span><span>,</span><span> db</span></span></code></pre><p>整个模型的定义就已经完成了，最后一步就是读取数据并执行train方法了：
</p><pre tabindex="0"><code><span><span>if</span><span> __name__</span><span> ==</span><span> '</span><span>__main__</span><span>'</span><span>:</span></span>
<span><span>    model </span><span>=</span><span> LinearModel</span><span>(</span><span>'</span><span>./data/car_details_v4.csv</span><span>'</span><span>)</span></span>
<span><span>    model</span><span>.</span><span>train</span><span>()</span></span></code></pre><p>但执行起来就会发现不对了，最后的输出显示w和b都变成了​<code class="">nan</code>​，Python解释器也抛出了错误：​<code class="">RuntimeWarning: overflow encountered in reduce</code>​。还是因为之前提到的问题，计算机用固定位数能表示的数是有限的，计算过程中发生了溢出（超出了能表示的范围），怎么解决这个问题呢？
</p><p>可以先分析下数据本身，通过pandas库的​<code class="">describe</code>​方法，简单分析一下数据集：
</p><pre tabindex="0"><code><span><span>              Price         Year</span></span>
<span><span>count  2.059000e+03  2059.000000</span></span>
<span><span>mean   1.702992e+06  2016.425449</span></span>
<span><span>std    2.419881e+06     3.363564</span></span>
<span><span>min    4.900000e+04  1988.000000</span></span>
<span><span>25%    4.849990e+05  2014.000000</span></span>
<span><span>50%    8.250000e+05  2017.000000</span></span>
<span><span>75%    1.925000e+06  2019.000000</span></span>
<span><span>max    3.500000e+07  2022.000000</span></span></code></pre><p>可以看到年份的范围太小了，而相对的价格的范围又太大了，最便宜的车不到五万块，最贵的却有3500万！
</p><div><div><div></div><div>Tip</div></div><div><p>事实上我具体看了下价格最大的那行数据，是一台法拉利 488 GTB，只能说法拉利，不愧是你。
</p></div></div><p>那么能不能通过调节alpha和最大迭代次数，让学习速度慢一点？可以这么做，但是在实践中发现这样太慢了。能不能在特征数据上做些处理？
</p><h3 id="user-content-特征缩放">特征缩放<a class="" tabindex="-1" href="#特征缩放">#</a></h3><p>在一些有裁判打分的体育比赛中，为了公平起见，通常会去掉一个最高分和一个最低分，避免异常数据干扰结果。在线性回归中，为了避免特征数据过散或过紧凑等问题，需要对数据做一个处理，这个过程称为「​<strong>特征缩放</strong>​」（feature scaling）。
</p><p>特征缩放的途径有多种，这里选用一种叫做标准化的方法：
</p><p>第一步先求特征的平均值（mean），表示为：
</p><p>再求标准差（standard deviation），即用所有特征值减均值，求平方，再求和，再求平均，再开平方。表示为：
</p><p>最后，用原特征值减去均值，再除以标准差，就得到了标准化的特征。表示为：
</p><p>numpy这个库提供了方法用于快速计算均值和标准差，下面修改代码：
</p><pre tabindex="0"><code><span><span>class</span><span> LinearModel</span><span>:</span></span>
<span><span>    def</span><span> __init__</span><span>(</span><span>self</span><span>,</span><span> data_path</span><span>)</span><span>:</span></span>
<span><span>        cars </span><span>=</span><span> pd</span><span>.</span><span>read_csv</span><span>(</span><span>data_path</span><span>)</span></span>
<span><span>        cars </span><span>=</span><span> cars</span><span>[</span><span>[</span><span>'</span><span>Price</span><span>'</span><span>,</span><span> '</span><span>Year</span><span>'</span><span>]</span><span>].</span><span>dropna</span><span>(</span><span>subset</span><span>=</span><span>[</span><span>'</span><span>Price</span><span>'</span><span>, </span><span>'</span><span>Year</span><span>'</span><span>]</span><span>)</span></span>
<span></span>
<span><span>        self</span><span>.</span><span>X_mean </span><span>=</span><span> cars</span><span>[</span><span>'</span><span>Year</span><span>'</span><span>].</span><span>mean</span><span>()</span></span>
<span><span>        self</span><span>.</span><span>X_std </span><span>=</span><span> cars</span><span>[</span><span>'</span><span>Year</span><span>'</span><span>].</span><span>std</span><span>()</span></span>
<span><span>        self</span><span>.</span><span>y_mean </span><span>=</span><span> cars</span><span>[</span><span>'</span><span>Price</span><span>'</span><span>].</span><span>mean</span><span>()</span></span>
<span><span>        self</span><span>.</span><span>y_std </span><span>=</span><span> cars</span><span>[</span><span>'</span><span>Price</span><span>'</span><span>].</span><span>std</span><span>()</span></span>
<span></span>
<span><span>        self</span><span>.</span><span>X </span><span>=</span><span> ((cars</span><span>[</span><span>[</span><span>'</span><span>Year</span><span>'</span><span>]</span><span>].</span><span>values </span><span>-</span><span> self</span><span>.</span><span>X_mean) </span><span>/</span><span> self</span><span>.</span><span>X_std)</span></span>
<span><span>        self</span><span>.</span><span>y </span><span>=</span><span> ((cars</span><span>[</span><span>[</span><span>'</span><span>Price</span><span>'</span><span>]</span><span>].</span><span>values </span><span>-</span><span> self</span><span>.</span><span>y_mean) </span><span>/</span><span> self</span><span>.</span><span>y_std)</span></span>
<span></span>
<span><span>        self</span><span>.</span><span>w </span><span>=</span><span> 0.0</span></span>
<span><span>        self</span><span>.</span><span>b </span><span>=</span><span> 0.0</span></span></code></pre><p>初始化数据时，将X和y都标准化，标准化前后数据对比：
</p><pre tabindex="0"><code><span><span>原始数据概况:</span></span>
<span><span>              Price         Year</span></span>
<span><span>count  2.059000e+03  2059.000000</span></span>
<span><span>mean   1.702992e+06  2016.425449</span></span>
<span><span>std    2.419881e+06     3.363564</span></span>
<span><span>min    4.900000e+04  1988.000000</span></span>
<span><span>25%    4.849990e+05  2014.000000</span></span>
<span><span>50%    8.250000e+05  2017.000000</span></span>
<span><span>75%    1.925000e+06  2019.000000</span></span>
<span><span>max    3.500000e+07  2022.000000</span></span>
<span><span></span></span>
<span><span>标准化后的数据概况:</span></span>
<span><span>               Year         Price</span></span>
<span><span>count  2.059000e+03  2.059000e+03</span></span>
<span><span>mean   1.693880e-14 -4.917549e-17</span></span>
<span><span>std    1.000000e+00  1.000000e+00</span></span>
<span><span>min   -8.450992e+00 -6.835014e-01</span></span>
<span><span>25%   -7.210951e-01 -5.033276e-01</span></span>
<span><span>50%    1.708161e-01 -3.628244e-01</span></span>
<span><span>75%    7.654235e-01  9.174349e-02</span></span>
<span><span>max    1.657335e+00  1.375977e+01</span></span></code></pre><p>注意这样最后训练出的参数是在标准化后的数据上得到的，也可以把参数还原，标准化时做了减法和除法，所以还原时用乘法和加法：
</p><pre tabindex="0"><code><span><span>def</span><span> train</span><span>(</span><span>self</span><span>)</span><span>:</span></span>
<span><span>    iteration </span><span>=</span><span> 0</span></span>
<span><span>    prev_cost </span><span>=</span><span> float</span><span>(</span><span>'</span><span>inf</span><span>'</span><span>)</span></span>
<span><span>    while</span><span> iteration </span><span>&#x3C;</span><span> max_iterations</span><span>:</span></span>
<span><span>        cost </span><span>=</span><span> self</span><span>.</span><span>cost</span><span>()</span></span>
<span><span>        if</span><span> abs</span><span>(</span><span>prev_cost </span><span>-</span><span> cost</span><span>)</span><span> &#x3C;</span><span> epsilon</span><span>:</span></span>
<span><span>            break</span></span>
<span><span>        prev_cost </span><span>=</span><span> cost</span></span>
<span><span>        self</span><span>.</span><span>gradient_descent</span><span>()</span></span>
<span><span>        iteration </span><span>+=</span><span> 1</span></span>
<span></span>
<span><span>    print</span><span>(</span><span>f</span><span>"</span><span>\n</span><span>训练经过了</span><span>{</span><span>iteration</span><span>}</span><span>次迭代"</span><span>)</span></span>
<span><span>    print</span><span>(</span><span>f</span><span>"标准化空间中的参数: w=</span><span>{</span><span>self</span><span>.w</span><span>}</span><span> b=</span><span>{</span><span>self</span><span>.b</span><span>}</span><span>"</span><span>)</span></span>
<span></span>
<span><span>    # 将参数转换回原始空间</span></span>
<span><span>    self</span><span>.</span><span>w_original </span><span>=</span><span> self</span><span>.</span><span>w </span><span>*</span><span> (</span><span>self</span><span>.</span><span>y_std </span><span>/</span><span> self</span><span>.</span><span>X_std)</span></span>
<span><span>    self</span><span>.</span><span>b_original </span><span>=</span><span> self</span><span>.</span><span>y_mean </span><span>-</span><span> self</span><span>.</span><span>w_original </span><span>*</span><span> self</span><span>.</span><span>X_mean </span><span>+</span><span> self</span><span>.</span><span>b </span><span>*</span><span> self</span><span>.</span><span>y_std</span></span>
<span></span>
<span><span>    print</span><span>(</span><span>f</span><span>"</span><span>\n</span><span>原始空间中的参数:"</span><span>)</span></span>
<span><span>    print</span><span>(</span><span>f</span><span>"w=</span><span>{</span><span>self</span><span>.w_original</span><span>:.2f</span><span>}</span><span>"</span><span>)</span></span>
<span><span>    print</span><span>(</span><span>f</span><span>"b=</span><span>{</span><span>self</span><span>.b_original</span><span>:.2f</span><span>}</span><span>"</span><span>)</span></span></code></pre><p>再次运行代码，终于，经过343次迭代后，梯度下降算法收敛，结果如下：
</p><pre tabindex="0"><code><span><span>训练经过了343次迭代</span></span>
<span><span>标准化空间中的参数: w=0.3014701935037997 b=-4.573355347224044e-15</span></span>
<span><span></span></span>
<span><span>原始空间中的参数:</span></span>
<span><span>w=216889.58</span></span>
<span><span>b=-435638671.20</span></span></code></pre><h3 id="user-content-预测">预测<a class="" tabindex="-1" href="#预测">#</a></h3><p>现在线性回归的模型已经训练结束了，但是这个模型目前似乎仅仅向我们展示了两个参数的值，没有起到什么作用。给模型类添加一个predict方法：
</p><pre tabindex="0"><code><span><span>def</span><span> predict</span><span>(</span><span>self</span><span>,</span><span> year</span><span>)</span><span>:</span></span>
<span><span>    # 将输入年份标准化</span></span>
<span><span>    year_normalized </span><span>=</span><span> (year </span><span>-</span><span> self</span><span>.</span><span>X_mean) </span><span>/</span><span> self</span><span>.</span><span>X_std</span></span>
<span><span>    # 在标准化空间中预测</span></span>
<span><span>    price_normalized </span><span>=</span><span> self</span><span>.</span><span>w </span><span>*</span><span> year_normalized </span><span>+</span><span> self</span><span>.</span><span>b</span></span>
<span><span>    # 将预测结果转换回原始空间</span></span>
<span><span>    price </span><span>=</span><span> price_normalized </span><span>*</span><span> self</span><span>.</span><span>y_std </span><span>+</span><span> self</span><span>.</span><span>y_mean</span></span>
<span><span>    return</span><span> price</span></span></code></pre><p>只是要注意，特征经过标准化后，训练得到的模型参数也是基于标准化后的数据的，因此在预测时要么将输入的年份标准化，要么将参数还原，否则得到的结果是不对的。
</p><p>看下2014年的二手车能卖多少钱：
</p><pre tabindex="0"><code><span><span>model</span><span>.</span><span>predict</span><span>(</span><span>2014</span><span>)</span><span> # 输出1176937.0349997955</span></span></code></pre><h2 id="user-content-问题" class="">问题<a class="" tabindex="-1" href="#问题">#</a></h2><p>以上就展示了对二手车数据中销售年份与销售价格之间关系的线性回归训练过程，但是实际上还有很多问题没有解决，这些问题需要再用更多篇幅详细解释，但在这里先简单列一下。
</p><h3 id="user-content-测试集与模型评估">测试集与模型评估<a class="" tabindex="-1" href="#测试集与模型评估">#</a></h3><p>作为机器学习的结果，这个模型显然不应该只是去「fit」训练数据，如果我们得出了售出年份和价格的关系，那么给出一个在训练样本中没有出现过的年份，应该也能「​<strong>预测</strong>​」出车的价格。
</p><p>为了能评估模型，最好能将数据集分成两个部分，一部分用于训练，另一部分用于测试。由于现实任务中数据量大小不一，各种模型复杂度不同，所以没有一个通用的最好的划分方式。一般有按比例如3分测试7分训练；还有交叉验证如将数据分10份，做10次训练和测试，每次用不同的一份数据做测试，其余的做训练，最后取测试结果的平均值。
</p><p>我们将模型在未见过的新数据上的表现，称为模型的「​<strong>泛化</strong>​」（generalization），怎么评估模型在测试集上的泛化能力好不好？
</p><p>其中一种方法是使用前面提过的均方误差，均方误差应该越小越好。
</p><h3 id="user-content-多元特征">多元特征<a class="" tabindex="-1" href="#多元特征">#</a></h3><p>实际上，以我们的经验来说，二手车价格肯定不会只和年份有关。就像前面数据显示的那样，两年前买的五菱和两年前买的法拉利，二手价格显然是截然不同的。怎么综合如品牌、燃油类型、行驶里程等特征，训练一个更「实用」模型呢？
</p><h3 id="user-content-更多的特征缩放方式">更多的特征缩放方式<a class="" tabindex="-1" href="#更多的特征缩放方式">#</a></h3><p>除了标准化以外，还有多种其它方式没有介绍，它们各有什么优缺点呢？还有关于将数据集划分为训练集和测试集的问题，应该先缩放再划分呢？还是先划分再缩放？
</p><h2 id="user-content-一点点数学" class="">一点点数学<a class="" tabindex="-1" href="#一点点数学">#</a></h2><p>最后的最后，再来一点点数学吧。如果你觉得了解了线性回归后感到很兴奋以至于无法入睡，以下内容将对你的睡眠问题起到很大的帮助。
</p><h3 id="user-content-梯度到底是个啥">梯度到底是个啥<a class="" tabindex="-1" href="#梯度到底是个啥">#</a></h3><p>在梯度下降法中，我没有解释这个方法的名称，参数的更新公式中有学习率，有代价函数的偏导数，那么梯度在哪里？
</p><p>以二元函类为例，对于函数f(x, y)，它对x的偏导数，其实就是它在x方向上的变化率。现在设在xoy平面上，有一以点(x<sub>0</sub>, y<sub>0</sub>)
为起点的射线l，函数沿着这个射线方向上的变化率，就是函数在这个方向上的方向导数，记作：
</p><p>什么是梯度呢？设函数f(x, y)在区域D内有一阶连续偏导数，那么就称向量​​为函数在点(x<sub>0</sub>, y<sub>0</sub>)的梯度，记作​​。多元函数以此类推。
</p><p>再回到方向导数，如果函数z = f(x, y)在(x<sub>0</sub>, y<sub>0</sub>)处可微，则意味着期沿着任意非零向量的方向导数都存在，有：
</p><p>其中(l<sub>x</sub>, l<sub>y</sub>)是方向向量​​的单位向量，即​​。
</p><p>这个式子可以用向量的内积形式写成：
</p><p>根据柯西-施瓦茨不等式，我们有：
</p><p>当且仅当向量​​与梯度向量方向相同时，等号成立。这说明：
</p><ol><li>函数在梯度方向上的方向导数最大，其值等于梯度的模
</li><li>在与梯度方向相反的方向上，方向导数取得最小值，等于梯度的模的相反数
</li><li>在与梯度正交的方向上，方向导数为零
</li></ol><p>这就是为什么在梯度下降法中，我们用减去偏导数的形式更新参数，实质上是沿着梯度的反方向更新参数，就是代价函数值下降最快的方向。
</p><h3 id="user-content-代价函数中的12">代价函数中的1/2<a class="" tabindex="-1" href="#代价函数中的12">#</a></h3><p>前面有提过，代价函数里特意除了一个2，这里我们来看下其求偏导的过程，你就能知道为什么要特意除以2了。
</p><p>首先求J(w,b)关于w的偏导数：
</p><p>然后求J(w,b)关于b的偏导数：
</p><p>其实代价函数里的1/2就是为了在求偏导时方便消去。
</p><h3 id="user-content-convex-function">Convex function<a class="" tabindex="-1" href="#convex-function">#</a></h3><p>其实前面在讲一元二次函数在极小值点两边的导数性质时，忽略了一件事，就是如​<code class="">y = -x^2</code>​这样的函数，它的图像不是一个U型的，而是N型的，那怎么证明我们的偏导是个U型，或者说代价函数是个「山谷」型的呢？需要证明代价函数是一个<a href="https://en.wikipedia.org/wiki/Convex_function">Convex function</a>，就是要证明其<a href="https://zh.wikipedia.org/zh-cn/%E9%BB%91%E5%A1%9E%E7%9F%A9%E9%99%A3">Hessian矩阵</a>为半正定的。
</p><div><div><div></div><div>Note</div></div><div><p>Convex function直接翻译是凸函数，但按国内的理解，U型的函数应该是凹的，国内的一些教材对凹凸函数的定义也确实和国外相反。
</p></div></div><p>首先计算代价函数的二阶偏导数：
</p><p>其Hessian矩阵为：
</p><p>要证明H是半正定的，需要证明其所有顺序主子式都非负。
</p><p>对于2×2矩阵，只需要：
</p><ul><li> 显然成立，因为是平方和
</li><li>:
</li></ul>根据柯西不等式<p>因此，Hessian矩阵是半正定的，所以J(w,b)是凸函数。也就证明它有局部极小值点，而且也是全局极小值。
</p>
mjx-container[jax="SVG"] {
  direction: ltr;
}

mjx-container[jax="SVG"] > svg {
  overflow: visible;
  min-height: 1px;
  min-width: 1px;
}

mjx-container[jax="SVG"] > svg a {
  fill: blue;
  stroke: blue;
}

mjx-container[jax="SVG"][display="true"] {
  display: block;
  text-align: center;
  margin: 1em 0;
}

mjx-container[jax="SVG"][display="true"][width="full"] {
  display: flex;
}

mjx-container[jax="SVG"][justify="left"] {
  text-align: left;
}

mjx-container[jax="SVG"][justify="right"] {
  text-align: right;
}

g[data-mml-node="merror"] > g {
  fill: red;
  stroke: red;
}

g[data-mml-node="merror"] > rect[data-background] {
  fill: yellow;
  stroke: none;
}

g[data-mml-node="mtable"] > line[data-line], svg[data-table] > g > line[data-line] {
  stroke-width: 70px;
  fill: none;
}

g[data-mml-node="mtable"] > rect[data-frame], svg[data-table] > g > rect[data-frame] {
  stroke-width: 70px;
  fill: none;
}

g[data-mml-node="mtable"] > .mjx-dashed, svg[data-table] > g > .mjx-dashed {
  stroke-dasharray: 140;
}

g[data-mml-node="mtable"] > .mjx-dotted, svg[data-table] > g > .mjx-dotted {
  stroke-linecap: round;
  stroke-dasharray: 0,140;
}

g[data-mml-node="mtable"] > g > svg {
  overflow: visible;
}

[jax="SVG"] mjx-tool {
  display: inline-block;
  position: relative;
  width: 0;
  height: 0;
}

[jax="SVG"] mjx-tool > mjx-tip {
  position: absolute;
  top: 0;
  left: 0;
}

mjx-tool > mjx-tip {
  display: inline-block;
  padding: .2em;
  border: 1px solid #888;
  font-size: 70%;
  background-color: #F8F8F8;
  color: black;
  box-shadow: 2px 2px 5px #AAAAAA;
}

g[data-mml-node="maction"][data-toggle] {
  cursor: pointer;
}

mjx-status {
  display: block;
  position: fixed;
  left: 1em;
  bottom: 1em;
  min-width: 25%;
  padding: .2em .4em;
  border: 1px solid #888;
  font-size: 90%;
  background-color: #F8F8F8;
  color: black;
}

foreignObject[data-mjx-xml] {
  font-family: initial;
  line-height: normal;
  overflow: visible;
}

mjx-container[jax="SVG"] path[data-c], mjx-container[jax="SVG"] use[data-c] {
  stroke-width: 3;
}

          ]]>
          </content:encoded>
        </item>
<item>
          <title>尝试用Vim充当kitty的scrollback pager</title>
          <link>https://elliot00.com/posts/vim-as-kitty-pager</link>
          <description>这篇文章介绍了如何将kitty终端模拟器与Vim结合使用，以实现更强大的搜索、跳转和复制功能，并提供了解决相关问题的方法。</description>
          <pubDate>Wed, 09 Oct 2024 03:44:34 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <p>之前有<a href="https:​//elliot00.com/​posts/terminal-emulator-kitty">介绍</a>过，kitty是一个高度可配置的终端模拟器，但是一些初次使用的小伙伴表示kitty不像其它一些终端模拟器可以方便的搜索输出的内容。其实kitty有一个​<strong>Scrollback pager</strong>​的功能，可以结合第三方工具，将当前屏幕内容通过管道传递给第三方工具——官方默认使用的是​<code class="">less</code>​。
</p><p>但是我想用Vim来提供更强的搜索、跳转、复制等功能，并且和平时用Vim编辑文件有同样的操作体验，所以就研究了下kitty和Vim结合的方法，分享出来。
</p><h2 id="user-content-基础用法" class="">基础用法<a class="" tabindex="-1" href="#基础用法">#</a></h2><pre tabindex="0"><code><span><span>scrollback_pager vim -</span></span></code></pre><p>添加这行配置后，按下​<code class="">ctrl+shift+g</code>​，kitty会通过管道将显示的内容传给​<code class="">scrollback_pager</code>​后的命令，​<code class="">vim -</code>​命令可以从​<code class="">stdin</code>​中读取内容，这样就可以实现用Vim来查找、复制kitty上的内容了。
</p><h3 id="user-content-无法直接退出">无法直接退出<a class="" tabindex="-1" href="#无法直接退出">#</a></h3><p>使用上述命令进入Vim后，会发现无法直接​<code class="">:q!</code>​退出，提示buffer还没有写入，这种情况下可以用只读模式进Vim，如​<code class="">vim -R -</code>​或者直接用​<code class="">view -</code>​。
</p><h3 id="user-content-进入vim时执行命令">进入Vim时执行命令<a class="" tabindex="-1" href="#进入vim时执行命令">#</a></h3><p>可以使用​<code class="">vim -c "ex command" -</code>​在读取kitty页面后执行ex命令，如​<code class="">vim -c "normal G" -</code>​自动跳到行尾，​<code class="">vim -c "map &#x3C;silent> q :qa!&#x3C;CR>" -</code>​映射​<code class="">q</code>​键直接退出等等。
</p><h3 id="user-content-fish">fish<a class="" tabindex="-1" href="#fish">#</a></h3><p>如果你使用​<strong>fish shell</strong>​，恭喜你将遇到一个存在10年的老<a href="https://github.com/fish-shell/fish-shell/issues/1396">bug</a>，解决办法嘛，可以使用​<code class="">vim -u NONE</code>​不加载配置，但治标不治本。
</p><h3 id="user-content-ansi-escape-sequences">ANSI escape sequences<a class="" tabindex="-1" href="#ansi-escape-sequences">#</a></h3><p>终端模拟器可以通过<a href="https://zh.wikipedia.org/wiki/ANSI%E8%BD%AC%E4%B9%89%E5%BA%8F%E5%88%97">ANSI escape sequences</a>来控制显示文本的颜色、粗细等等，但对于Vim来说，这些只是普通字符而已，所以如果直接使用Vim做kitty的scrollback pager，常常会看到一些乱码，如下图：
</p><p><img alt="Screenshot of Vim" src="https://r2.elliot00.com/kitty/ansi-escape.png" width="2564" height="676"></p><p>已知的解决办法有两种：
</p><ul><li>先使用清理工具再用管道传给Vim，如<a href="https://github.com/lunixbochs/vtclean">vtclean</a></li><li>使用Vim插件，如<a href="https://github.com/vim-scripts/AnsiEsc.vim">AnsiEsc</a></li></ul><p>还有一种将buffer写入文件，再在Vim内置终端内查看的<a href="https://github.com/kovidgoyal/kitty/issues/2327#issuecomment-1059786996">邪道方法</a>，但似乎只适用于NeoVim。
</p><h2 id="user-content-其它方式" class="">其它方式<a class="" tabindex="-1" href="#其它方式">#</a></h2><h3 id="user-content-kitty插件">kitty插件<a class="" tabindex="-1" href="#kitty插件">#</a></h3><p>如果只是需要更类似其他终端模拟器的搜索功能，也可以使用插件实现，已经有造好的轮子：<a href="https://github.com/trygveaa/kitty-kitten-search">kitty-kitten-search</a>。
</p><h3 id="user-content-返朴归真">返朴归真<a class="" tabindex="-1" href="#返朴归真">#</a></h3><p>考虑到大部分情况下需要搜索复制功能是为了快速打开报错的文件之类的，其实直接用kitty内置的<a href="https://sw.kovidgoyal.net/kitty/kittens/hints/">hints</a>功能也是一个不错的选择。
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>在NextJS14中集成twikoo评论系统</title>
          <link>https://elliot00.com/posts/next-with-twikoo</link>
          <description>本文介绍了在NextJS中集成Twikoo的方法。此外，提到NextJS在使用notFound API时可能存在的bug及解决方案。</description>
          <pubDate>Sun, 22 Sep 2024 07:05:50 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <p>某天邮件收到了<a href="https://www.elephantsql.com/blog/end-of-life-announcement.html">ElephantSQL即将停止服务的通知</a>，博客的评论系统可能要没有数据库用了，加上在用的评论系统本身的开发也陷入了停滞状态，于是就打算物色个新的支持自己部署的评论系统。最终选择了使用<a href="https://twikoo.js.org">twikoo</a>，+因为可以暂时不用花钱+。
</p><p>Twikoo官网提供了CDN引入的方式，在NextJS中，可以通过<a href="https://nextjs.org/docs/app/building-your-application/optimizing/scripts">Script</a>来加载twikoo的JS：
</p><pre tabindex="0"><code><span><span>'</span><span>use client</span><span>'</span></span>
<span></span>
<span><span>import</span><span> Script </span><span>from</span><span> '</span><span>next/script</span><span>'</span></span>
<span></span>
<span><span>export</span><span> default</span><span> function</span><span> Comment</span><span>()</span><span> {</span></span>
<span><span>  return</span><span> (</span></span>
<span><span>    &#x3C;</span><span>div</span><span>></span></span>
<span><span>      &#x3C;</span><span>div</span><span> id</span><span>=</span><span>"</span><span>tcomment</span><span>"</span><span>>&#x3C;/</span><span>div</span><span>></span></span>
<span><span>      &#x3C;</span><span>Script</span></span>
<span><span>        src</span><span>=</span><span>"</span><span>https://cdn.jsdelivr.net/npm/twikoo@1.6.39/dist/twikoo.min.js</span><span>"</span></span>
<span><span>        onReady</span><span>=</span><span>{</span><span>()</span><span> =></span><span> {</span></span>
<span><span>          window</span><span>.</span><span>twikoo</span><span>.</span><span>init</span><span>(</span><span>{</span></span>
<span><span>            envId</span><span>:</span><span> ''</span><span>,</span><span> // 根据后端部署方式不同</span></span>
<span><span>            el</span><span>:</span><span> '</span><span>#tcomment</span><span>'</span><span>,</span></span>
<span><span>          }</span><span>)</span></span>
<span><span>        }</span><span>}</span></span>
<span><span>      /></span></span>
<span><span>    &#x3C;/</span><span>div</span><span>></span></span>
<span><span>  )</span></span>
<span><span>}</span></span></code></pre><p>目前NextJS似乎在<a href="https://nextjs.org/docs/app/api-reference/functions/not-found">notFound</a>这个​<code class="">API</code>​上有点<a href="https://github.com/vercel/next.js/issues/58055">bug</a>，如果你的页面组件使用了​<code class="">notFound</code>​，并且出现了类似​<code class="">NotFoundError: Failed to execute 'removeChild' on 'Node'</code>​的错误，检查一下Comment组件，是否用了​<code class="">&#x3C;div></code>​将​<code class="">&#x3C;Script></code>​包裹在内了。
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>在NextJS中为rehype代码块添加复制按钮</title>
          <link>https://elliot00.com/posts/next-rehype-copy-button</link>
          <description>本文介绍了作者在博客中使用rehype-pretty-code和shiki来美化代码块时，如何通过React Server Components和自定义MDX组件，解决在NextJS中使用带复制按钮的代码块时遇到的问题，并在不同的格式（MDX和Org-mode）下实现了功能的具体方法。</description>
          <pubDate>Fri, 13 Sep 2024 05:57:48 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <p>我的博客中使用了​<code class="">rehype-pretty-code</code>​加​<code class="">shiki</code>​来美化代码块，rehype-pretty-code提供了一个shiki的​<strong>transformer</strong>​来自动给代码块加上复制按钮，它会生成这样的代码：
</p><pre tabindex="0"><code><span><span>&#x3C;</span><span>button</span></span>
<span><span>  data</span><span>=</span><span>"</span><span>code内的代码</span><span>"</span></span>
<span><span>  onclick​</span><span>=</span><span>\</span><span>"navigator.clipboard.writeText(this.attributes.data.value);this.classList.add(&#x26;#x27;rehype-pretty-copied&#x26;#x27;);window.setTimeout(()</span><span> =​</span><span>></span><span> this.classList.remove(</span><span>&#x26;#x27;</span><span>rehype-pretty-copied</span><span>&#x26;#x27;</span><span>), 3000);\"</span></span>
<span><span>></span></span>
<span><span>  &#x3C;</span><span>span</span><span> class</span><span>=</span><span>\</span><span>"ready\"</span><span>>&#x3C;/</span><span>span</span><span>></span></span>
<span><span>  &#x3C;</span><span>span</span><span> class</span><span>=</span><span>\</span><span>"success\"</span><span>>&#x3C;/</span><span>span</span><span>></span></span>
<span><span>&#x3C;/</span><span>button</span><span>></span></span></code></pre><p>但是在NextJS中目前想要不做额外处理地使用它，只能使用<a href="https:​//react.dev/​reference​/rsc/​server-components">React Server Components</a>，将生成的HTML文本传入​<code class="">dangerouslySetInnerHTML</code>​：
</p><pre tabindex="0"><code><span><span>function</span><span> MyComponent</span><span>()</span><span> {</span></span>
<span><span>  return</span><span> &#x3C;</span><span>div</span><span> dangerouslySetInnerHTML</span><span>=</span><span>{</span><span>{ __html</span><span>:</span><span> html</span><span> }</span><span>}</span><span> /></span></span>
<span><span>}</span></span></code></pre><p>但在某些场景下没法直接用服务端组件，下面给出对应的解决办法。
</p><h2 id="user-content-mdx" class="">MDX<a class="" tabindex="-1" href="#mdx">#</a></h2><p>如果要结合MDX使用，MDX会把生成的​<code class="">button</code>​当成React组件处理，而React组件的​<code class="">onClick</code>​属性需要的是函数对象而不是字符串，为了防止<a href="https:​//en.wikipedia.org/​wiki/Cross-site_scripting">XSS</a>这类安全问题又不能将字符串直接eval成函数，这里就会报错。
</p><p>解决办法是通过MDX自定义components的方式，先自定义一个复制按钮组件：
</p><pre tabindex="0"><code><span><span>'</span><span>use client</span><span>'</span></span>
<span></span>
<span><span>import</span><span> { </span><span>type</span><span> PropsWithoutRef</span><span>,</span><span> useState } </span><span>from</span><span> '</span><span>react</span><span>'</span></span>
<span></span>
<span><span>export</span><span> default</span><span> function</span><span> CopyCodeButton</span><span>(</span><span>{</span></span>
<span><span>  code</span><span>,</span></span>
<span><span>}</span><span>:</span><span> PropsWithoutRef</span><span>&#x3C;{ code</span><span>:</span><span> string</span><span> }></span><span>)</span><span> {</span></span>
<span><span>  const</span><span> [</span><span>isCopied</span><span>,</span><span> setIsCopied</span><span>]</span><span> =</span><span> useState</span><span>(</span><span>false</span><span>)</span></span>
<span></span>
<span><span>  const</span><span> copy</span><span> =</span><span> async</span><span> ()</span><span> =></span><span> {</span></span>
<span><span>    await</span><span> navigator</span><span>.</span><span>clipboard</span><span>.</span><span>writeText</span><span>(</span><span>code</span><span>)</span></span>
<span><span>    setIsCopied</span><span>(</span><span>true</span><span>)</span></span>
<span></span>
<span><span>    setTimeout</span><span>(</span><span>()</span><span> =></span><span> {</span></span>
<span><span>      setIsCopied</span><span>(</span><span>false</span><span>)</span></span>
<span><span>    }</span><span>,</span><span> 2500</span><span>)</span></span>
<span><span>  }</span></span>
<span></span>
<span><span>  return</span><span> (</span></span>
<span><span>    &#x3C;</span><span>button</span></span>
<span><span>      className</span><span>=</span><span>"</span><span>rehype-pretty-copy</span><span>"</span></span>
<span><span>      title</span><span>=</span><span>"</span><span>Copy code</span><span>"</span></span>
<span><span>      aria-label</span><span>=</span><span>"</span><span>Copy code</span><span>"</span></span>
<span><span>      onClick</span><span>=</span><span>{</span><span>copy</span><span>}</span></span>
<span><span>    ></span></span>
<span><span>      {</span><span>isCopied </span><span>?</span><span> CheckIcon </span><span>:</span><span> CopyIcon</span><span>}</span></span>
<span><span>    &#x3C;/</span><span>button</span><span>></span></span>
<span><span>  )</span></span>
<span><span>}</span></span></code></pre><p>这个组件不是服务端组件，所以在开头第一行要加​<code class="">"use client"</code>​，​<code class="">className</code>​可以复用一下，子组件切换复用有点麻烦，干脆直接自定义的图标了。
</p><p>下一步就是通过MDX的API替换生成的button：
</p><pre tabindex="0"><code><span><span>&#x3C;</span><span>MDXContent</span></span>
<span><span>  components</span><span>=</span><span>{</span><span>{</span></span>
<span><span>    button</span><span>(</span><span>props</span><span>)</span><span> {</span></span>
<span><span>      const</span><span> {</span><span> children</span><span>,</span><span> className</span><span>,</span><span> ...</span><span>rest</span><span> }</span><span> =</span><span> props</span></span>
<span></span>
<span><span>      // 判断一下是否是插件生成的</span></span>
<span><span>      if</span><span> (className</span><span> ===</span><span> '</span><span>rehype-pretty-copy</span><span>'</span><span>)</span><span> {</span></span>
<span><span>        return</span><span> &#x3C;</span><span>CopyCodeButton</span><span> code</span><span>=</span><span>{</span><span>rest</span><span>.</span><span>data</span><span>}</span><span> /></span></span>
<span><span>      } </span><span>else</span><span> {</span></span>
<span><span>        return</span><span> &#x3C;</span><span>button</span><span> {</span><span>...</span><span>props</span><span>}</span><span> /></span></span>
<span><span>      }</span></span>
<span><span>    }</span><span>,</span></span>
<span><span>  }</span><span>}</span></span>
<span><span>/></span></span></code></pre><p>这样就可以实现复制代码按钮了。
</p><h2 id="user-content-org" class="">Org<a class="" tabindex="-1" href="#org">#</a></h2><p>直接使用我的<a href="https:​//www.npmjs.com/​package​/@docube/​org">@docube/org</a>通常来说是没有问题的，但是由于我的文章页面的结构大致是这样的：
</p><pre tabindex="0"><code><span><span>function</span><span> Post</span><span>()</span><span> {</span></span>
<span><span>  return</span><span> (</span></span>
<span><span>    &#x3C;</span><span>article</span><span>></span></span>
<span><span>      &#x3C;</span><span>header</span><span>>&#x3C;/</span><span>header</span><span>></span></span>
<span><span>      {</span><span>content</span><span>}</span></span>
<span><span>      &#x3C;</span><span>address</span><span>>&#x3C;/</span><span>address</span><span>></span></span>
<span><span>    &#x3C;/</span><span>article</span><span>></span></span>
<span><span>  )</span></span>
<span><span>}</span></span></code></pre><p>React的​<code class="">dangerouslySetInnerHTML</code>​不能直接作用到​<code class="">Fragment</code>​上，也就是必须要给content加个父元素，我个人有点受不了……
</p><p>为了能不加额外的父元素，我使用了​<code class="">html-react-parser</code>​这个库，它又带来了新的问题，也就是为了安全，它会直接忽略​<code class="">onclick</code>​属性，导致只能渲染按钮却没有复制的功能。
</p><p>解决办法如下：
</p><pre tabindex="0"><code><span><span>import</span><span> reactParse </span><span>from</span><span> '</span><span>html-react-parser</span><span>'</span></span>
<span></span>
<span><span>function</span><span> Content</span><span>()</span><span> {</span></span>
<span><span>  return</span><span> (</span></span>
<span><span>    &#x3C;></span></span>
<span><span>      {</span><span>reactParse</span><span>(</span><span>post</span><span>.</span><span>body</span><span>,</span><span> {</span></span>
<span><span>        replace</span><span>:</span><span> (</span><span>dom</span><span>)</span><span> =></span><span> {</span></span>
<span><span>          if</span><span> (</span></span>
<span><span>            '</span><span>attribs</span><span>'</span><span> in</span><span> dom </span><span>&#x26;&#x26;</span></span>
<span><span>            dom</span><span>.</span><span>name</span><span> ===</span><span> '</span><span>button</span><span>'</span><span> &#x26;&#x26;</span></span>
<span><span>            dom</span><span>.</span><span>attribs</span><span>[</span><span>'</span><span>class</span><span>'</span><span>] </span><span>===</span><span> '</span><span>rehype-pretty-copy</span><span>'</span></span>
<span><span>          ) {</span></span>
<span><span>            delete</span><span> dom</span><span>.</span><span>attribs</span><span>.</span><span>onclick</span></span>
<span><span>            return</span><span> &#x3C;</span><span>CopyCodeButton</span><span> code</span><span>=</span><span>{</span><span>dom</span><span>.</span><span>attribs</span><span>.</span><span>data</span><span>}</span><span> /></span></span>
<span><span>          }</span></span>
<span><span>        }</span><span>,</span></span>
<span><span>      })</span><span>}</span></span>
<span><span>    &#x3C;></span></span>
<span><span>  )</span></span>
<span><span>}</span></span></code></pre>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>开发docube问题记录</title>
          <link>https://elliot00.com/posts/docube-dev-note</link>
          <description>本文介绍了作者将博客内容格式从MDX迁移到Org-mode的过程中，开发了一个名为Docube的JavaScript/TypeScript库，并在文中详细描述了库的设计理念、实现细节以及在发布npm包时遇到的问题和解决方法。</description>
          <pubDate>Sun, 01 Sep 2024 07:10:07 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <h2 id="user-content-因缘" class="">因缘<a class="" tabindex="-1" href="#因缘">#</a></h2><p>不久前打算将我的博客内容格式从​<code class="">mdx</code>​转到​<code class="">orgmode</code>​，此前我一直在使用<a href="https://contentlayer.dev/">contentlayer</a>管理我的mdx文档，但是因为一些原因这个项目停止维护了，并且虽然它具有一定的定制化能力，但和Markdown的绑定太深，无法满足我迁移到orgmode的需求，于是我花了点时间做了我个人的第一个JavaScript/TypeScript库：<a href="https://codeberg.org/Elliot00/docube">docube</a>。
</p><h2 id="user-content-设计" class="">设计<a class="" tabindex="-1" href="#设计">#</a></h2><p>通常我更喜欢使用可定制性高的软件，但是像Vim、Emacs这类软件常常被抱怨新手上手难度太高，似乎高度可定制和开箱即用是非常冲突的理念，所以我希望做一个硬核用户可以自定义行为，普通用户又可以快速上手使用的应用。在具体实践上，我借助了<a href="https://effect.website/">effect</a>这个库，抽象出了一个通用的转换流程：
</p><pre tabindex="0"><code><span><span>graph TD</span></span>
<span><span>    A[Loader] -->|Array of FileLike| B{Split Process}</span></span>
<span><span>    B -->|Optional| C[ModuleResolver]</span></span>
<span><span>    B -->|Main| D[FileConverter]</span></span>
<span><span>    D -->|FileLike| E[Writer]</span></span>
<span><span>    E --> F[End]</span></span>
<span></span>
<span><span>    subgraph "Per FileLike"</span></span>
<span><span>    D</span></span>
<span><span>    E</span></span>
<span><span>    end</span></span></code></pre><p>我的原始的需求就是将本地的org文件读取解析成HTML文本格式，并和其它元数据一起组成JSON文件+TypeScript定义文件的形式，之后在React里直接引用。核心的流程就是通过​<code class="">Loader</code>​获取抽象的​<code class="">FileLkie[]</code>​，再调用​<code class="">FileConverter</code>​转换内容，最后通过​<code class="">Writer</code>​写入，因为我想尽量保持核心的通用性，所以​<code class="">ModuleResolver</code>​（主要是用来生成JS模块和类型定义）在这里是可选的，用户可以通过注入对应的依赖来改变默认的行为。
</p><p>常规的使用方式并不需要了解这些概念，下面是这段是我的博客从contentlayer迁移后的代码：
</p><pre tabindex="0"><code><span><span>import</span><span> { transform } </span><span>from</span><span> '</span><span>@docube/mdx</span><span>'</span></span>
<span><span>import</span><span> rehypeProbeImageSize </span><span>from</span><span> '</span><span>./lib/rehypeImage</span><span>'</span></span>
<span><span>import</span><span> remarkGfm </span><span>from</span><span> '</span><span>remark-gfm</span><span>'</span></span>
<span></span>
<span><span>transform</span><span>({</span></span>
<span><span>  name</span><span>:</span><span> '</span><span>Post</span><span>'</span><span>,</span></span>
<span><span>  directory</span><span>:</span><span> '</span><span>./posts</span><span>'</span><span>,</span></span>
<span><span>  include</span><span>:</span><span> '</span><span>**/*.mdx</span><span>'</span><span>,</span></span>
<span><span>  fields</span><span>:</span><span> (</span><span>s</span><span>)</span><span> =></span><span> ({</span></span>
<span><span>    title</span><span>:</span><span> s</span><span>.</span><span>String</span><span>,</span></span>
<span><span>    tags</span><span>:</span><span> s</span><span>.</span><span>Array</span><span>(</span><span>s</span><span>.</span><span>String</span><span>)</span><span>,</span></span>
<span><span>    series</span><span>:</span><span> s</span><span>.</span><span>String</span><span>,</span></span>
<span><span>    createdAt</span><span>:</span><span> s</span><span>.</span><span>String</span><span>,</span></span>
<span><span>    publishedAt</span><span>:</span><span> s</span><span>.</span><span>String</span><span>,</span></span>
<span><span>    summary</span><span>:</span><span> s</span><span>.</span><span>String</span><span>,</span></span>
<span><span>  })</span><span>,</span></span>
<span><span>  remarkPlugins</span><span>:</span><span> [remarkGfm]</span><span>,</span></span>
<span><span>  rehypePlugins</span><span>:</span><span> [rehypeProbeImageSize]</span><span>,</span></span>
<span><span>})</span></span></code></pre><p>执行这段代码就可以得到一个生成的​<code class="">.docube/generated/posts</code>​模块，顶层导出了​<code class="">allPosts</code>​变量，在NextJS里，可以<a href="https://github.com/Eliot00/elliot00.com/blob/master/app/posts/%5Bslug%5D/page.tsx">这样使用</a>：
</p><pre tabindex="0"><code><span><span>import</span><span> { allPosts } </span><span>from</span><span> '</span><span>@docube/generated</span><span>'</span></span>
<span><span>import</span><span> { getMDXComponent } </span><span>from</span><span> '</span><span>mdx-bundler/client</span><span>'</span></span>
<span></span>
<span><span>// ...</span></span>
<span><span>  const</span><span> MDXContent</span><span> =</span><span> getMDXComponent</span><span>(</span><span>post</span><span>.</span><span>body</span><span>)</span></span>
<span><span>// ...</span></span>
<span></span>
<span><span>// ...</span></span>
<span><span>export</span><span> async</span><span> function</span><span> generateMetadata</span><span>(</span><span>{ </span><span>params</span><span> }</span><span>:</span><span> Props</span><span>)</span><span>:</span><span> Promise</span><span>&#x3C;</span><span>Metadata</span><span>> {</span></span>
<span><span>  const</span><span> {</span><span> slug</span><span> }</span><span> =</span><span> params</span></span>
<span><span>  // post即是自动生成的Post类型</span></span>
<span><span>  const</span><span> post</span><span> =</span><span> allPosts</span><span>.</span><span>find</span><span>(</span><span>(</span><span>post</span><span>)</span><span> =></span><span> post</span><span>.</span><span>_meta</span><span>.</span><span>slug</span><span> ===</span><span> slug</span><span>)</span></span>
<span></span>
<span><span>  if</span><span> (</span><span>!</span><span>post) </span><span>notFound</span><span>()</span></span>
<span></span>
<span><span>  return</span><span> {</span></span>
<span><span>    title</span><span>:</span><span> `</span><span>${</span><span>post</span><span>.</span><span>title</span><span>}</span><span> - Elliot</span><span>`</span><span>,</span></span>
<span><span>    keywords</span><span>:</span><span> post</span><span>.</span><span>tags</span><span> as</span><span> string</span><span>[]</span><span>,</span></span>
<span><span>    description</span><span>:</span><span> post</span><span>.</span><span>summary</span><span>,</span></span>
<span><span>  }</span></span>
<span><span>}</span></span></code></pre><p>而如果需要个性化使用，如提供一种新的文本格式的支持，只需要引用​<code class="">@docube/common</code>​的​<code class="">makeTransformer</code>​，修改传入的FileConverter依赖就可实现，具体见<a href="https://codeberg.org/Elliot00/docube/src/branch/main/packages/markdown/src/index.ts">@docube/markdown</a>的实现。
</p><h2 id="user-content-问题" class="">问题<a class="" tabindex="-1" href="#问题">#</a></h2><p>虽说我已经写过不少TypeScript代码，但在npm上发布库还是第一次，过程中还是遇到了不少问题的，在此记录一下，避免后来人踩坑。
</p><h3 id="user-content-monorepo">Monorepo<a class="" tabindex="-1" href="#monorepo">#</a></h3><p>考虑到我至少需要默认支持mdx和org两种格式，所以一开始我就想要创建多个库，因此采用了monorepo的形式。Monorepo说白了就是在一个代码仓库里包含有关联的多个项目，可以共享同样的外围工具如lint、format等，项目之间需要重构更新依赖相对来说要比多仓库轻松些。
</p><p>对于JS项目，在根目录的package.json添加如​<code class="">"workspaces": ["packages/*"]</code>​，就可以在packages目录里包含多个子包。但是在开发时，如果B包依赖A包，​<code class="">tsserver</code>​实际上检查的是A包build后的dist，而不是A包的TS代码，也就是说如果A包更新了，需要先build一下，才能使LSP正确地工作。如果不想手动执行命令，可以用一些工具的​<code class="">Watch Mode</code>​功能，检测到包变化自动rebuild，当然前提是开发机器内存够用:)。
</p><h4 id="user-content-同步依赖">同步依赖<a class="" tabindex="-1" href="#同步依赖">#</a></h4><p>多个子项目依赖同一个依赖的情况是非常常见的，一般来说最好能全局共享这种相同的依赖，将其保持在一个相同版本。这方面NPM那边没有定义这个功能，不像​<code class="">Cargo</code>​可以让子项目继承Workspace的依赖。要实现这个目的的话，要么用<a href="https://www.npmjs.com/package/syncpack">syncpack</a>这类专门处理这个问题的工具，要么用​<code class="">pnpm</code>​这类的包管理工具的<a href="https://pnpm.io/cli/update#--recursive--r">Workspace支持</a>。
</p><h3 id="user-content-发版">发版<a class="" tabindex="-1" href="#发版">#</a></h3><p>将包发布到npm上只需要build后执行​<code class="">npm publish</code>​就可以了，但是如果更新的包被另外几个包依赖了，那么后者也需要更新。这个问题有个辅助工具<a href="https://github.com/changesets/changesets">changesets</a>，它能自动帮助更新相关有改动的包的版本，并维护​<strong>Changelog</strong>​。
</p><h4 id="user-content-scope">scope<a class="" tabindex="-1" href="#scope">#</a></h4><p>NPM有一个比较好的设计是你可以给包名加一个范围前缀，比如有个通用的名字叫time，不同的组织可以用​<code class="">@google/time</code>​、​<code class="">@microsoft/time</code>​，一方面是避免想用的名字被抢，一方面是对于大企业来说可以标识一下这是自己的官方包。这里对新手的一个坑点是，当你创建了一个scope，然后想发布一个包，如​<code class="">@docube/mdx</code>​，默认情况下这个包会被当做是你组织下的私有包，而私有包是要收费的，需要用​<code class="">npm publish --access=public</code>​明确表明这是个公开的包，或者在package.json里写明：
</p><pre tabindex="0"><code><span><span>{</span></span>
<span><span>  "publishConfig"</span><span>:</span><span> {</span></span>
<span><span>    "access"</span><span>:</span><span> "</span><span>public</span><span>"</span></span>
<span><span>  }</span><span>,</span></span>
<span><span>  ...</span></span>
<span><span>}</span></span></code></pre><h3 id="user-content-lint">lint<a class="" tabindex="-1" href="#lint">#</a></h3><p><code class="">turbo</code>​默认生成的Monorepo模板内部使用了eslint <strong>v8</strong>​，而当前最新的eslint版本是​<strong>v9</strong>​，这两个版本之间有不兼容的改动，所以如果在这个模板上新建项目，并且不指定安装的eslint版本的话，将无法使用​<code class="">turbo lint</code>​命令，解决办法一个是安装eslint时指定使用v8版本，另一个详见<a href="https://codeberg.org/Elliot00/docube/src/branch/main/packages/eslint-config">我的配置</a>。
</p><h3 id="user-content-可选依赖">可选依赖<a class="" tabindex="-1" href="#可选依赖">#</a></h3><p>我本人对软件使用有一点小洁癖，不会用到的依赖就尽量不想要装到我的电脑上。如在Markdown支持上，很多人会在Markdown文件的开头放上一段​<code class="">yaml</code>​格式的文本来提供一些如撰写时间、作者等元信息：
</p><pre tabindex="0"><code><span><span>---</span></span>
<span><span>date</span><span>:</span><span> 2024-02-02T04:14:54-08:00</span></span>
<span><span>draft</span><span>:</span><span> false</span></span>
<span><span>params</span><span>:</span></span>
<span><span>  author</span><span>:</span><span> John Smith</span></span>
<span><span>title</span><span>:</span><span> Example</span></span>
<span><span>weight</span><span>:</span><span> 10</span></span>
<span><span>---</span></span>
<span></span>
<span><span>...</span></span></code></pre><p>这个被称为​<em>front matter</em>​，但是处理这段文本的库每个人可能有不同的偏好选择（NPM上下载量较大的两个都有三年以上没有更新了）；并且有些情况下，这个front matter不一定是yaml格式，如静态站生成器hugo就提供了yaml、toml和json三种选择。
</p><p>如果我在我的库里直接依赖一个实现，那么既便我为用户提供了自定义解析这段文本的配置，用户也必须下载一个他用不到的第三方库，甚至就算是不需要front matter的用户也不得不安装。为此我使用了可选依赖，可选依赖定义在package.json的​<strong>optionalDependencies</strong>​，我在开发中使用的是bun，使用​<code class="">bun add gray-matter --optional</code>​就可以将这个​<code class="">gray-matter</code>​包安装为可选模式。
</p><p>在我的库代码里，可以用​<code class="">try-catch</code>​加​<code class="">import</code>​来判断用户有没有安装我默认的依赖，大致逻辑如下：
</p><pre tabindex="0"><code><span><span>if</span><span> (</span><span>options</span><span>.</span><span>frontMatterExtractor</span><span>) {</span></span>
<span><span>    frontMatterData </span><span>=</span><span> options</span><span>.</span><span>frontMatterData</span><span>(content)</span></span>
<span><span>} </span><span>else</span><span> {</span></span>
<span><span>    try</span><span> {</span></span>
<span><span>        const</span><span> matter</span><span> =</span><span> import</span><span>(</span><span>"</span><span>gray-matter</span><span>"</span><span>)</span></span>
<span><span>        // ...</span></span>
<span><span>    } </span><span>catch</span><span> (e) {</span></span>
<span><span>        // ...</span></span>
<span><span>    }</span></span>
<span><span>}</span></span></code></pre><p>不想要front matter的用户，或者想用自己的逻辑处理的用户，可以用​<code class="">npm install --omit=optinal</code>​来避免安装我默认的可选包（具体命令根据使用的包管理器不同）。
</p><h2 id="user-content-终" class="">终<a class="" tabindex="-1" href="#终">#</a></h2><p>这篇博客就是我用org格式写的(<strong>’ｰ’</strong>)
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>Android开发拾遗：DataStore与JSON结合</title>
          <link>https://elliot00.com/posts/android-json-data-store</link>
          <description>本文探讨了如何在Android应用中使用JSON格式的DataStore存储配置数据，并提供了详细的代码示例和步骤来实现这一点。</description>
          <pubDate>Mon, 15 Jul 2024 05:05:42 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <p>在本系列的<a href="/posts/android-proto-data-store">上一篇</a>中我介绍了Android的​<code class="">Proto DataStore</code>​的用法，但是我对​<code class="">protobuf</code>​的schema定义并不熟悉，所以就想着有没有使用JSON格式结合DataStore存储数据，事实证明这是可能的。
</p><p>首先注意到​<code class="">DataStore</code>​与​<code class="">protobuf</code>​相关类是解耦的，​<code class="">dataStore</code>​函数需要的参数是一个文件名------持久化数据存放位置，以及一个​<code class="">serializer</code>​对象。现在尝试改动serializer来实现用JSON存储配置数据。
</p><p>先安装依赖：
</p><pre tabindex="0"><code><span><span>plugins</span><span> {</span></span>
<span><span>    // ...</span></span>
<span><span>    alias</span><span>(libs.plugins.jetbrains.kotlin.serialization)</span></span>
<span><span>}</span></span>
<span></span>
<span><span>dependencies</span><span> {</span></span>
<span><span>    // datastore还是必要的</span></span>
<span><span>    implementation</span><span>(libs.androidx.datastore)</span></span>
<span></span>
<span><span>    // 用于序列化/反序列化</span></span>
<span><span>    implementation</span><span>(libs.kotlinx.serialization.json)</span></span>
<span><span>}</span></span></code></pre><pre tabindex="0"><code><span><span># libs.versions.toml</span></span>
<span><span>[versions]</span></span>
<span><span>datastore</span><span> =</span><span> "</span><span>1.1.1</span><span>"</span></span>
<span><span>kotlin</span><span> =</span><span> "</span><span>1.9.0</span><span>"</span></span>
<span><span>kotlinxSerializationJson</span><span> =</span><span> "</span><span>1.1.0</span><span>"</span></span>
<span></span>
<span><span>[libraries]</span></span>
<span><span>kotlinx-serialization-json</span><span> =</span><span> { </span><span>module</span><span> =</span><span> "</span><span>org.jetbrains.kotlinx:kotlinx-serialization-json</span><span>"</span><span>,</span><span> version</span><span>.</span><span>ref</span><span> =</span><span> "</span><span>kotlinxSerializationJson</span><span>"</span><span> }</span></span>
<span></span>
<span><span>[plugins]</span></span>
<span><span>jetbrains-kotlin-serialization</span><span> =</span><span> { </span><span>id</span><span> =</span><span> "</span><span>org.jetbrains.kotlin.plugin.serialization</span><span>"</span><span>,</span><span> version</span><span>.</span><span>ref</span><span> =</span><span> "</span><span>kotlin</span><span>"</span><span> }</span></span></code></pre><p>下一步是创建一个用于序列化/反序列化的数据类（之前Proto DataStore中这个类是自动生成的）：
</p><pre tabindex="0"><code><span><span>import</span><span> kotlinx.serialization.Serializable</span></span>
<span></span>
<span><span>@Serializable</span></span>
<span><span>data</span><span> class</span><span> Settings</span><span>(</span></span>
<span><span>    val</span><span> theme: </span><span>Theme</span><span> =</span><span> Theme.SYSTEM,</span></span>
<span><span>    val</span><span> isDebugMode: </span><span>Boolean</span><span> =</span><span> false</span><span>,</span></span>
<span><span>)</span></span>
<span></span>
<span><span>enum</span><span> class</span><span> Theme</span><span> { SYSTEM, LIGHT, DARK }</span></span></code></pre><p>接下来设置好​<code class="">Serializer</code>​对象：
</p><pre tabindex="0"><code><span><span>object</span><span> SettingsSerializer</span><span> : </span><span>Serializer</span><span>&#x3C;</span><span>Settings</span><span>> {</span></span>
<span><span>    override</span><span> val</span><span> defaultValue: </span><span>Settings</span></span>
<span><span>        get</span><span>() </span><span>=</span><span> Settings</span><span>()</span></span>
<span></span>
<span><span>    override</span><span> suspend</span><span> fun</span><span> readFrom</span><span>(input: </span><span>InputStream</span><span>): </span><span>Settings</span><span> {</span></span>
<span><span>        return</span><span> try</span><span> {</span></span>
<span><span>            Json.</span><span>decodeFromString</span><span>(</span></span>
<span><span>                deserializer </span><span>=</span><span> Settings.</span><span>serializer</span><span>(),</span></span>
<span><span>                string </span><span>=</span><span> input.</span><span>readBytes</span><span>().</span><span>decodeToString</span><span>()</span></span>
<span><span>            )</span></span>
<span><span>        } </span><span>catch</span><span> (e: </span><span>SerializationException</span><span>) {</span></span>
<span><span>            e.</span><span>printStackTrace</span><span>()</span></span>
<span><span>            defaultValue</span></span>
<span><span>        }</span></span>
<span><span>    }</span></span>
<span></span>
<span><span>    override</span><span> suspend</span><span> fun</span><span> writeTo</span><span>(t: </span><span>Settings</span><span>, output: </span><span>OutputStream</span><span>) {</span></span>
<span><span>        withContext</span><span>(Dispatchers.IO) {</span></span>
<span><span>            output.</span><span>write</span><span>(</span></span>
<span><span>                Json.</span><span>encodeToString</span><span>(</span></span>
<span><span>                    serializer </span><span>=</span><span> Settings.</span><span>serializer</span><span>(),</span></span>
<span><span>                    value</span><span> =</span><span> t</span></span>
<span><span>                ).</span><span>encodeToByteArray</span><span>()</span></span>
<span><span>            )</span></span>
<span><span>        }</span></span>
<span><span>    }</span></span>
<span><span>}</span></span></code></pre><p>将其设置到​<code class="">Context</code>​上：
</p><pre tabindex="0"><code><span><span>val</span><span> Context.dataStore </span><span>by</span><span> dataStore</span><span>(</span><span>"settings.json"</span><span>, SettingsSerializer)</span></span></code></pre><p>简单写个UI试验下：
</p><pre tabindex="0"><code><span><span>@Composable</span></span>
<span><span>fun</span><span> MainScreen</span><span>() {</span></span>
<span><span>    Scaffold</span><span>(modifier </span><span>=</span><span> Modifier.</span><span>fillMaxSize</span><span>()) { innerPadding </span><span>-></span></span>
<span><span>        val</span><span> context </span><span>=</span><span> LocalContext.current</span></span>
<span><span>        val</span><span> settings </span><span>by</span><span> context.dataStore.</span><span>data</span><span>.</span><span>collectAsState</span><span>(initial </span><span>=</span><span> Settings</span><span>())</span></span>
<span><span>        val</span><span> scope </span><span>=</span><span> rememberCoroutineScope</span><span>()</span></span>
<span></span>
<span><span>        Surface</span><span>(modifier </span><span>=</span><span> Modifier.</span><span>padding</span><span>(innerPadding)) {</span></span>
<span><span>            Column</span><span> {</span></span>
<span><span>                Text</span><span>(</span><span>if</span><span> (settings.isDebugMode) </span><span>"Debug"</span><span> else</span><span> "Release"</span><span>)</span></span>
<span><span>                Button</span><span>(onClick </span><span>=</span><span> {</span></span>
<span><span>                    scope.</span><span>launch</span><span> {</span></span>
<span><span>                        context.dataStore.</span><span>updateData</span><span> {</span></span>
<span><span>                            it.</span><span>copy</span><span>(isDebugMode </span><span>=</span><span> !</span><span>it.isDebugMode)</span></span>
<span><span>                        }</span></span>
<span><span>                    }</span></span>
<span><span>                }) {</span></span>
<span><span>                    Text</span><span>(text </span><span>=</span><span> "Toggle"</span><span>)</span></span>
<span><span>                }</span></span>
<span><span>            }</span></span>
<span><span>        }</span></span>
<span><span>    }</span></span>
<span><span>}</span></span></code></pre><p><img alt="json-data-store" src="https://r2.elliot00.com/kotlin/json-data-store.webp" width="600" height="1333"></p><p>完成！
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>Android开发拾遗：如何使用Proto DataStore</title>
          <link>https://elliot00.com/posts/android-proto-data-store</link>
          <description>本文介绍了如何在Android中使用Proto DataStore来持久化复杂的用户数据，包括schema定义、Gradle配置和代码实现。</description>
          <pubDate>Tue, 09 Jul 2024 03:51:43 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <p>持久化一些用户数据是一个常见的需求，例如保存用户设置，Android提供了一个方便的机制，叫做<a href="https://developer.android.com/topic/libraries/architecture/datastore">DataStore</a>。其中有两套API，一个是​<code class="">Preferences DataStore</code>​，可以存取简单的​<code class="">key-value</code>​数据；另一个是​<code class="">Proto DataStore</code>​，顾名思义，这需要开发者定义一个<a href="https://protobuf.dev/">protocol buffers</a>的schema，可以存取自定义的数据类型，并提供类型安全保证。
</p><p>最初我仅用到了简单的键值对API，因为业务上需要的配置项逐渐变多，并且在一些地方我需要使用枚举，所以萌生了从​<code class="">Preferences DataStore</code>​转到​<code class="">Proto DataStore</code>​的念头。但可惜的是，Android的文档关于​<code class="">Proto DataStore</code>​没有详细的描述，在参考了一些开源代码后，我找到了能适配​<code class="">Kotlin</code>​以及​<code class="">Gradle Kotlin DSL</code>​的使用方法，在此记录一下。
</p><h2 id="user-content-schema" class="">Schema<a class="" tabindex="-1" href="#schema">#</a></h2><p>首先要在​<code class="">app/src/main/proto</code>​目录下创建一个protobuf文件，如​<code class="">settings.proto</code>​：
</p><pre tabindex="0"><code><span><span>syntax</span><span> =</span><span> "proto3"</span><span>;</span></span>
<span></span>
<span><span>// 这里替换成自己的包名</span></span>
<span><span>option</span><span> java_package</span><span> =</span><span> "com.example.application"</span><span>;</span></span>
<span><span>option</span><span> java_multiple_files</span><span> =</span><span> true</span><span>;</span></span>
<span></span>
<span><span>message</span><span> Settings</span><span> {</span></span>
<span><span>  int32</span><span> example_counter</span><span> =</span><span> 1</span><span>;</span></span>
<span><span>}</span></span></code></pre><h2 id="user-content-gradle配置" class="">Gradle配置<a class="" tabindex="-1" href="#gradle配置">#</a></h2><p>有了schema之后，还需要有对应的用来序列化/反序列化的数据结构，这个数据结构可以通过库来生成。修改Gradle配置：
</p><pre tabindex="0"><code><span><span>import</span><span> com.google.protobuf.gradle.id</span></span>
<span></span>
<span><span>plugins</span><span> {</span></span>
<span><span>    // ...</span></span>
<span><span>    id</span><span>(</span><span>"com.google.protobuf"</span><span>) version </span><span>"0.9.1"</span></span>
<span><span>}</span></span>
<span></span>
<span><span>dependencies</span><span> {</span></span>
<span><span>    // 添加这两个依赖</span></span>
<span><span>    implementation</span><span>(</span><span>"androidx.datastore:datastore:1.1.1"</span><span>)</span></span>
<span><span>    implementation</span><span>(</span><span>"com.google.protobuf:protobuf-javalite:3.17.3"</span><span>)</span></span>
<span><span>}</span></span>
<span></span>
<span><span>// 这里Android Studio可能有lint报错，直接忽略，sync gradle</span></span>
<span><span>protobuf</span><span> {</span></span>
<span><span>    protoc</span><span> {</span></span>
<span><span>        artifact </span><span>=</span><span> "com.google.protobuf:protoc:3.21.9"</span></span>
<span><span>    }</span></span>
<span></span>
<span><span>    generateProtoTasks</span><span> {</span></span>
<span><span>        all</span><span>().</span><span>forEach</span><span> { task </span><span>-></span></span>
<span><span>            task.</span><span>builtins</span><span> {</span></span>
<span><span>                id</span><span>(</span><span>"java"</span><span>) {</span></span>
<span><span>                    option</span><span>(</span><span>"lite"</span><span>)</span></span>
<span><span>                }</span></span>
<span><span>            }</span></span>
<span><span>        }</span></span>
<span><span>    }</span></span>
<span><span>}</span></span></code></pre><h2 id="user-content-在代码中使用" class="">在代码中使用<a class="" tabindex="-1" href="#在代码中使用">#</a></h2><p>先创建一个​<code class="">SettingsSerializer.kt</code>​文件，定义序列化器：
</p><pre tabindex="0"><code><span><span>// 这个Settings类是自动生成的</span></span>
<span><span>object</span><span> SettingsSerializer</span><span> : </span><span>Serializer</span><span>&#x3C;</span><span>Settings</span><span>> {</span></span>
<span><span>  override</span><span> val</span><span> defaultValue: </span><span>Settings</span><span> =</span><span> Settings.</span><span>getDefaultInstance</span><span>()</span></span>
<span></span>
<span><span>  override</span><span> suspend</span><span> fun</span><span> readFrom</span><span>(input: </span><span>InputStream</span><span>): </span><span>Settings</span><span> {</span></span>
<span><span>    try</span><span> {</span></span>
<span><span>      return</span><span> Settings.</span><span>parseFrom</span><span>(input)</span></span>
<span><span>    } </span><span>catch</span><span> (exception: </span><span>InvalidProtocolBufferException</span><span>) {</span></span>
<span><span>      throw</span><span> CorruptionException</span><span>(</span><span>"Cannot read proto."</span><span>, exception)</span></span>
<span><span>    }</span></span>
<span><span>  }</span></span>
<span></span>
<span><span>  override</span><span> suspend</span><span> fun</span><span> writeTo</span><span>(</span></span>
<span><span>    t: </span><span>Settings</span><span>,</span></span>
<span><span>    output: </span><span>OutputStream</span><span>) </span><span>=</span><span> t.</span><span>writeTo</span><span>(output)</span></span>
<span><span>}</span></span>
<span></span>
<span><span>val</span><span> Context.settingsDataStore: </span><span>DataStore</span><span>&#x3C;</span><span>Settings</span><span>> </span><span>by</span><span> dataStore</span><span>(</span></span>
<span><span>  fileName </span><span>=</span><span> "settings.pb"</span><span>,</span></span>
<span><span>  serializer </span><span>=</span><span> SettingsSerializer</span></span>
<span><span>)</span></span></code></pre><p>读取时就可以使用冷流：
</p><pre tabindex="0"><code><span><span>val</span><span> settings </span><span>=</span><span> context.settingsDataStore.</span><span>data</span><span>.</span><span>first</span><span>()</span></span></code></pre><p>如果要设置值，可以使用​<code class="">updateData</code>​方法：
</p><pre tabindex="0"><code><span><span>suspend</span><span> fun</span><span> incrementCounter</span><span>() {</span></span>
<span><span>  context.settingsDataStore.</span><span>updateData</span><span> { currentSettings </span><span>-></span></span>
<span><span>    currentSettings.</span><span>toBuilder</span><span>()</span></span>
<span><span>      .</span><span>setExampleCounter</span><span>(currentSettings.exampleCounter </span><span>+</span><span> 1</span><span>)</span></span>
<span><span>      .</span><span>build</span><span>()</span></span>
<span><span>    }</span></span>
<span><span>}</span></span></code></pre>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>Android开发拾遗：对onTerminate的误解</title>
          <link>https://elliot00.com/posts/android-on-terminate</link>
          <description>在Android应用中，通过继承Timber的Tree类实现日志记录功能，在Application的onCreate方法中启动日志记录，尽管onTerminate在真机上不会被调用，可以使用Activity的onSaveInstanceState方法保存和恢复临时数据。</description>
          <pubDate>Wed, 26 Jun 2024 03:12:01 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <p>昨天收到一个需求需要在Android应用一个完整的生命周期——启动到退出——内，将Log信息收集到文件中。通过继承Timber的Tree类可以实现将日志保存到文件的功能，​<code class="">Application</code>​类有​<code class="">onCreate</code>​方法，可以在这里开始调用​<code class="">Timber.plant</code>​，以及创建以时间命名的日志文件等。那么要怎么检测应用退出？
</p><blockquote><p>单纯的清理操作如​​<code class="">Timber.uproot</code>​​其实没必要做，但有时会有在应用退出前保存一些状态的需求。
</p></blockquote><p>在​<code class="">Application</code>​类内输入​<code class="">override fun on</code>​，自动补全会跳出​<code class="">onTerminate</code>​，看名字，这就是需要用到的生命周期方法了。但是当将写好的代码编译打包到实机上运行时，却发现这个方法没有被调用。
</p><p>查了下文档，发现这个方法仅适用于模拟器环境，在真机上根本就不会被调。
</p><p>那如果真的有在应用退出前保存临时数据，并在再次启动时恢复的需求要怎么做？
</p><p>一个方法是使用​<code class="">Activity</code>​上的​<code class="">onSaveInstanceState</code>​。注意到​<code class="">onCreate</code>​方法实际上有个​<code class="">Bundle?</code>​类型的参数​<code class="">savedInstanceState</code>​。可以在​<code class="">onSaveInstanceState</code>​中保存临时状态，在​<code class="">onCreate</code>​时恢复。另外还有​<code class="">onRestoreInstanceState</code>​可用于恢复，它仅当​<code class="">Activity</code>​被重建时，在​<code class="">onStart</code>​和​<code class="">onPostCreate</code>​之间被调用。
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>Android开发拾遗：如何减少重组</title>
          <link>https://elliot00.com/posts/android-compose-stability</link>
          <description>在Android代码中，常见到一些数据类标有@Stable或@Immutable注解，这些注解与Jetpack Compose的性能优化相关，本文探讨了它们的作用和可能的解决方案。</description>
          <pubDate>Mon, 24 Jun 2024 08:15:25 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <h2 id="user-content-前言" class="">前言<a class="" tabindex="-1" href="#前言">#</a></h2><p>在Github上浏览Android代码时，常看到有一些数据类上有​<code class="">@Stable</code>​或​<code class="">@Immutable</code>​注解，遂查询了些相关资料，发现与​<code class="">Jetpack Compose</code>​的性能有关。虽然我一贯坚持在没有充分依据的情况下不应当去做所谓的「性能优化」，但记录一下可能的解决方案还是值得的。
</p><p><a href="https://developer.android.com/develop/ui/compose">Jetpack Compose</a>是目前Android推荐的声明式的UI框架。在过去的XML视图中如果要写一个点击按钮改变文字的界面，大概是这样：
</p><pre tabindex="0"><code><span><span>class</span><span> MainActivity</span><span> : </span><span>AppCompatActivity</span><span>() {</span></span>
<span><span>    private</span><span> var</span><span> count </span><span>=</span><span> 0</span></span>
<span></span>
<span><span>    override</span><span> fun</span><span> onCreate</span><span>(savedInstanceState: </span><span>Bundle</span><span>?) {</span></span>
<span><span>        super</span><span>.</span><span>onCreate</span><span>(savedInstanceState)</span></span>
<span><span>        setContentView</span><span>(R.layout.activity_main)</span></span>
<span></span>
<span><span>        val</span><span> textViewCount: </span><span>TextView</span><span> =</span><span> findViewById</span><span>(R.id.textViewCount)</span></span>
<span><span>        val</span><span> buttonIncrement: </span><span>Button</span><span> =</span><span> findViewById</span><span>(R.id.buttonIncrement)</span></span>
<span></span>
<span><span>        buttonIncrement.</span><span>setOnClickListener</span><span> {</span></span>
<span><span>            count</span><span>++</span></span>
<span><span>            textViewCount.text </span><span>=</span><span> count.</span><span>toString</span><span>()</span></span>
<span><span>        }</span></span>
<span><span>    }</span></span>
<span><span>}</span></span></code></pre><p>需要命令式地从XML布局中找出对应的元素，设置事件监听器，在必要时修改视图的属性。另外还需要一个XML布局文件：
</p><pre tabindex="0"><code><span><span>&#x3C;</span><span>LinearLayout</span><span> xmlns</span><span>:</span><span>android</span><span>=</span><span>"</span><span>http://schemas.android.com/apk/res/android</span><span>"</span></span>
<span><span>    xmlns</span><span>:</span><span>app</span><span>=</span><span>"</span><span>http://schemas.android.com/apk/res-auto</span><span>"</span></span>
<span><span>    xmlns</span><span>:</span><span>tools</span><span>=</span><span>"</span><span>http://schemas.android.com/tools</span><span>"</span></span>
<span><span>    android</span><span>:</span><span>layout_width</span><span>=</span><span>"</span><span>match_parent</span><span>"</span></span>
<span><span>    android</span><span>:</span><span>layout_height</span><span>=</span><span>"</span><span>match_parent</span><span>"</span></span>
<span><span>    android</span><span>:</span><span>orientation</span><span>=</span><span>"</span><span>vertical</span><span>"</span></span>
<span><span>    android</span><span>:</span><span>padding</span><span>=</span><span>"</span><span>16dp</span><span>"</span></span>
<span><span>    tools</span><span>:</span><span>context</span><span>=</span><span>"</span><span>.MainActivity</span><span>"</span><span>></span></span>
<span></span>
<span><span>    &#x3C;</span><span>TextView</span></span>
<span><span>        android</span><span>:</span><span>id</span><span>=</span><span>"</span><span>@+id/textViewCount</span><span>"</span></span>
<span><span>        android</span><span>:</span><span>layout_width</span><span>=</span><span>"</span><span>wrap_content</span><span>"</span></span>
<span><span>        android</span><span>:</span><span>layout_height</span><span>=</span><span>"</span><span>wrap_content</span><span>"</span></span>
<span><span>        android</span><span>:</span><span>text</span><span>=</span><span>"</span><span>0</span><span>"</span></span>
<span><span>        android</span><span>:</span><span>textSize</span><span>=</span><span>"</span><span>24sp</span><span>"</span><span> /></span></span>
<span></span>
<span><span>    &#x3C;</span><span>Button</span></span>
<span><span>        android</span><span>:</span><span>id</span><span>=</span><span>"</span><span>@+id/buttonIncrement</span><span>"</span></span>
<span><span>        android</span><span>:</span><span>layout_width</span><span>=</span><span>"</span><span>wrap_content</span><span>"</span></span>
<span><span>        android</span><span>:</span><span>layout_height</span><span>=</span><span>"</span><span>wrap_content</span><span>"</span></span>
<span><span>        android</span><span>:</span><span>layout_marginTop</span><span>=</span><span>"</span><span>16dp</span><span>"</span></span>
<span><span>        android</span><span>:</span><span>text</span><span>=</span><span>"</span><span>Increment</span><span>"</span><span> /></span></span>
<span></span>
<span><span>&#x3C;/</span><span>LinearLayout</span><span>></span></span></code></pre><p>相比之下Jetpack Compose代码要更简洁，可读性更高：
</p><pre tabindex="0"><code><span><span>@Composable</span></span>
<span><span>fun</span><span> Counter</span><span>() {</span></span>
<span><span>    var</span><span> count </span><span>by</span><span> remember</span><span> { </span><span>mutableStateOf</span><span>(</span><span>0</span><span>) }</span></span>
<span></span>
<span><span>    Column</span><span>() {</span></span>
<span><span>        Text</span><span>(text </span><span>=</span><span> "</span><span>$count</span><span>"</span><span>)</span></span>
<span><span>        Button</span><span>(onClick </span><span>=</span><span> { count</span><span>++</span><span> }) {</span></span>
<span><span>            Text</span><span>(text </span><span>=</span><span> "Increment"</span><span>)</span></span>
<span><span>        }</span></span>
<span><span>    }</span></span>
<span><span>}</span></span></code></pre><p>Jetpack Compose的UI是由​<em>可组合函数（composable functionns）</em>​组成的，这类函数必须用​<code class="">@Composable</code>​注解，它不返回值，也不用修改什么全局变量。
</p><h2 id="user-content-重组" class="">重组<a class="" tabindex="-1" href="#重组">#</a></h2><p>Composable函数可以视为纯函数，相同的输入总是得到相同的输出（UI），所以如果要改变UI，并不需要获取某个组件对象再全修改它的属性，只要改变输入的数据即可。例如上面代码中​<code class="">Counter</code>​内的​<code class="">Text</code>​，这是库提供的可组合函数，当输入的text参数变化，渲染的文字就会发生变化，这样一个过程被称为​<strong>recomposition</strong>​，或许可以翻译为「重组」。得益于纯函数的特性，Jetpack Compose天生拥有较好的性能表现，它可以对可组合函数乱序调用、并行调用，也可以尽可能地跳过不必要的recomposition。
</p><p>Android Studio提供了一个叫做<a href="https://developer.android.com/studio/debug/layout-inspector">Layout Inspector</a>的工具可以用来查看哪些组件发生了重组以及重组次数。现在就用这个工具看看Jetpack Compose够不够智能，可以跳过不必要的重组呢？
</p><pre tabindex="0"><code><span><span>@Composable</span></span>
<span><span>fun</span><span> Demo</span><span>() {</span></span>
<span><span>    var</span><span> num </span><span>by</span><span> remember</span><span> { </span><span>mutableIntStateOf</span><span>(</span><span>0</span><span>) }</span></span>
<span></span>
<span><span>    Column</span><span> {</span></span>
<span><span>        Text</span><span>(text </span><span>=</span><span> "Hello, World!"</span><span>)</span></span>
<span><span>        RandomButton</span><span>(num </span><span>=</span><span> num, onClick </span><span>=</span><span> { num </span><span>=</span><span> Random.</span><span>nextInt</span><span>(</span><span>0</span><span>, </span><span>100</span><span>)})</span></span>
<span><span>    }</span></span>
<span><span>}</span></span>
<span></span>
<span><span>@Composable</span></span>
<span><span>fun</span><span> RandomButton</span><span>(num: </span><span>Int</span><span>, onClick: () </span><span>-></span><span> Unit) {</span></span>
<span><span>    Button</span><span>(onClick </span><span>=</span><span> onClick) {</span></span>
<span><span>        Text</span><span>(text </span><span>=</span><span> num.</span><span>toString</span><span>())</span></span>
<span><span>    }</span></span>
<span><span>}</span></span></code></pre><p>每次点击按钮，按钮上的文字就会随机变化，理论上只有​<code class="">RandomButton</code>​用到了​<code class="">num</code>​，在​<code class="">Column</code>​中的另一个​<code class="">Text</code>​是否可以避免被重组？
</p><p><img alt="skipped" src="https://r2.elliot00.com/kotlin/layout_inspector.png" width="1616" height="632"></p><p>可以看到点击按钮后只有​<code class="">RandomButton</code>​发生了重组，Text则被跳过了。
</p><h2 id="user-content-稳定性" class="">稳定性<a class="" tabindex="-1" href="#稳定性">#</a></h2><p>接下来看一个稍复杂点的例子：
</p><pre tabindex="0"><code><span><span>data</span><span> class</span><span> Artist</span><span>(</span><span>var</span><span> firstName: </span><span>String</span><span>, </span><span>var</span><span> lastName: </span><span>String</span><span>)</span></span>
<span></span>
<span><span>@Composable</span></span>
<span><span>fun</span><span> Demo</span><span>() {</span></span>
<span><span>    var</span><span> num </span><span>by</span><span> remember</span><span> { </span><span>mutableIntStateOf</span><span>(</span><span>0</span><span>) }</span></span>
<span></span>
<span><span>    Column</span><span> {</span></span>
<span><span>        Greeting</span><span>(artist </span><span>=</span><span> Artist</span><span>(firstName </span><span>=</span><span> "John"</span><span>, lastName </span><span>=</span><span> "Lennon"</span><span>))</span></span>
<span><span>        RandomButton</span><span>(num </span><span>=</span><span> num, onClick </span><span>=</span><span> { num </span><span>=</span><span> Random.</span><span>nextInt</span><span>(</span><span>0</span><span>, </span><span>100</span><span>)})</span></span>
<span><span>    }</span></span>
<span><span>}</span></span>
<span></span>
<span><span>@Composable</span></span>
<span><span>fun</span><span> Greeting</span><span>(artist: </span><span>Artist</span><span>) {</span></span>
<span><span>    Text</span><span>(text </span><span>=</span><span> "Hello, </span><span>${</span><span>artist.firstName</span><span>}</span><span> ${</span><span>artist.lastName</span><span>}</span><span>"</span><span>)</span></span>
<span><span>}</span></span></code></pre><p><img alt="not skipped" src="https://r2.elliot00.com/kotlin/layout_inspector2.png" width="1582" height="1876"></p><p>即使​<code class="">Greeting</code>​的参数从来没有被修改过，它也无法被跳过重组。为什么这里Jetpack Compose不再「智能」了呢？假设我是Compose库开发者，一方面我需要保证较好的性能，但另一方面，更重要的是渲染不能出错，不能让应该更新的视图没有被更新；所以我需要有某种方法去检验一个可组合函数是否可以在重组中被跳过，并在无法确定是否应该跳过时，​<strong>不要跳过</strong>​。
</p><p>Jetpack Compose通过一个叫「稳定性」的指标来判断一个可组合函数是否可以被跳过，如果一个Composable的所有参数都是稳定的，那么这个Composable就是可跳过的。那么什么值被视为稳定的？首先是可变但每次变化会通知Compose的，例如​<code class="">MutableState</code>​。
</p><pre tabindex="0"><code><span><span>@Composable</span></span>
<span><span>fun</span><span> Demo</span><span>() {</span></span>
<span><span>    var</span><span> num </span><span>by</span><span> remember</span><span> { </span><span>mutableIntStateOf</span><span>(</span><span>0</span><span>) }</span></span>
<span><span>    var</span><span> name </span><span>by</span><span> remember</span><span> { </span><span>mutableStateOf</span><span>(</span><span>"Paul"</span><span>) }</span></span>
<span></span>
<span><span>    Column</span><span> {</span></span>
<span><span>        // 虽然name是可变的，但是MutableState的变化可被Compose监测，没有改变就可以跳过重组</span></span>
<span><span>        Greeting</span><span>(name)</span></span>
<span><span>        RandomButton</span><span>(num </span><span>=</span><span> num, onClick </span><span>=</span><span> { num </span><span>=</span><span> Random.</span><span>nextInt</span><span>(</span><span>0</span><span>, </span><span>100</span><span>)})</span></span>
<span><span>    }</span></span>
<span><span>}</span></span>
<span></span>
<span><span>@Composable</span></span>
<span><span>fun</span><span> Greeting</span><span>(name: </span><span>String</span><span>) {</span></span>
<span><span>    Text</span><span>(text </span><span>=</span><span> "Hello, </span><span>$name</span><span>"</span><span>)</span></span>
<span><span>}</span></span></code></pre><p>另一种方式是直接使用不可变值（对象的值和其属性都不可变），如Kotlin的基本类型​<code class="">Int</code>​、​<code class="">String</code>​还有所有字段都是不可变的​<code class="">data class</code>​。
</p><pre tabindex="0"><code><span><span>data</span><span> class</span><span> Artist</span><span>(</span><span>val</span><span> firstName: </span><span>String</span><span>, </span><span>val</span><span> lastName: </span><span>String</span><span>)</span></span>
<span></span>
<span><span>@Composable</span></span>
<span><span>fun</span><span> Demo</span><span>() {</span></span>
<span><span>    var</span><span> num </span><span>by</span><span> remember</span><span> { </span><span>mutableIntStateOf</span><span>(</span><span>0</span><span>) }</span></span>
<span></span>
<span><span>    Column</span><span> {</span></span>
<span><span>        // skippable</span></span>
<span><span>        Greeting</span><span>(artist </span><span>=</span><span> Artist</span><span>(firstName </span><span>=</span><span> "John"</span><span>, lastName </span><span>=</span><span> "Lennon"</span><span>))</span></span>
<span><span>        RandomButton</span><span>(num </span><span>=</span><span> num, onClick </span><span>=</span><span> { num </span><span>=</span><span> Random.</span><span>nextInt</span><span>(</span><span>0</span><span>, </span><span>100</span><span>)})</span></span>
<span><span>    }</span></span>
<span><span>}</span></span></code></pre><h2 id="user-content-stable和immutable注解" class="">Stable和Immutable注解<a class="" tabindex="-1" href="#stable和immutable注解">#</a></h2><p>如果数据类中有一个List会怎么样？
</p><pre tabindex="0"><code><span><span>data</span><span> class</span><span> Band</span><span>(</span><span>val</span><span> name: </span><span>String</span><span>, </span><span>val</span><span> albums: </span><span>List</span><span>&#x3C;</span><span>String</span><span>>)</span></span>
<span></span>
<span><span>@Composable</span></span>
<span><span>fun</span><span> Demo</span><span>() {</span></span>
<span><span>    var</span><span> num </span><span>by</span><span> remember</span><span> { </span><span>mutableIntStateOf</span><span>(</span><span>0</span><span>) }</span></span>
<span></span>
<span><span>    Column</span><span> {</span></span>
<span><span>        BandProfile</span><span>(band </span><span>=</span><span> Band</span><span>(name </span><span>=</span><span> "The Beatles"</span><span>, albums </span><span>=</span><span> listOf</span><span>(</span><span>"Rubber Soul"</span><span>, </span><span>"Revolver"</span><span>, </span><span>"Abbey Road"</span><span>)))</span></span>
<span><span>        RandomButton</span><span>(num </span><span>=</span><span> num, onClick </span><span>=</span><span> { num </span><span>=</span><span> Random.</span><span>nextInt</span><span>(</span><span>0</span><span>, </span><span>100</span><span>)})</span></span>
<span><span>    }</span></span>
<span><span>}</span></span></code></pre><p>这次虽然数据类Band内的字段都是不可变的，但​<code class="">BandProfile</code>​仍然无法跳过重组，为什么？因为List是个接口，它不能真正保证不可变，包括​<code class="">Map</code>​、​<code class="">Set</code>​，都被Jetpack Compose识别为不稳定的。
</p><p>在数据类上加上注解​<code class="">@Stable</code>​就可以让Jetpack Compose将其视为稳定的，比这更强一级的注解是​<code class="">@Immutable</code>​，这告诉Compose被注解的类是不可变的。
</p><p>但要注意这两个注解只是一个「口头承诺」，实际是否稳定不可变是由开发者自己保证的。
</p><h3 id="user-content-不可变集合">不可变集合<a class="" tabindex="-1" href="#不可变集合">#</a></h3><p>针对集合数据，Jetpack Compose也支持Kotlin的<a href="https://github.com/Kotlin/kotlinx.collections.immutable">不可变集合</a>，如果一个集合确实是不可变的，并且因为它是不稳定的导致Jetpack Compose产生性能问题，可以尝试用不可变集合替代。
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>Android开发拾遗：MVVM与MVI</title>
          <link>https://elliot00.com/posts/android-mvi</link>
          <description>本文介绍了复杂项目中代码拆分的重要性，以 ASP.NET 的 MVC 模式为例，探讨如何解耦 UI 和业务逻辑。重点讨论了 Android 开发中的 MVVM 和 MVI 模式，通过对比说明它们在状态管理和 UI 交互上的不同，阐明了各自的优缺点及适用场景。</description>
          <pubDate>Tue, 04 Jun 2024 07:23:12 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <p>如果想要写一个可运行的应用，将所有的代码都放在同一个文件里并不会影响其编译运行，但在实践中，当一个软件的功能越来越复杂，代码量不断增多，为了项目的可维护性，一般需要遵循一些模式将代码拆分开。像微软的<a href="https://dotnet.microsoft.com/en-us/apps/aspnet/mvc">ASP.NET</a>这类Web应用框架，就为开发者预设了一套模板，使用被称为「MVC」的架构模式，要求用户将代码分散到不同的目录，各自继承一些特定的类，致力于将UI表现层与业务逻辑解耦。
</p><blockquote><p>架构模式（architectural pattern）是软件架构中在给定环境下，針對常遇到的问题的、通用且可重用的解决方案。
</p></blockquote><p>近期在浏览Android开发相关内容时常看到「MVI」架构模式，但与​<em>ASP.NET</em>​不同，Android并没有一个很强的约束要求开发者必须以某种模式写代码，网上找到的一些对于MVI的介绍也没有把它讲得很清楚。所以这次就结合实践中的一些问题，讲讲我对MVI的理解。
</p><p>为了说明MVI，需要回顾一下过去的Android开发范式。在Android使用​<code class="">XML</code>​构建视图的时代，受推崇的架构模式是​<strong>MVVM</strong>​，这种模式在很多流行的GUI框架——如<a href="https://vuejs.org/">Vue.js</a>——中被广泛使用。
</p><h2 id="user-content-mvvm" class="">MVVM<a class="" tabindex="-1" href="#mvvm">#</a></h2><p>MVVM是model-view-viewmodel的缩写，ViewModel层作为View层和Model层之间的桥梁，避免视图和模型之间的直接交互。
</p><p><img alt="MVVM" src="https://upload.wikimedia.org/wikipedia/commons/thumb/8/87/MVVMPattern.png/660px-MVVMPattern.png"></p><p>以一个购物应用为例，ViewModel层可以继承官方库中的ViewModel类：
</p><pre tabindex="0"><code><span><span>class</span><span> CartViewModel</span><span> : </span><span>ViewModel</span><span>() {</span></span>
<span><span>    private</span><span> val</span><span> _itemQuantity </span><span>=</span><span> MutableLiveData</span><span>(</span><span>0</span><span>)</span></span>
<span><span>    val</span><span> itemQuantity: </span><span>LiveData</span><span>&#x3C;</span><span>Int</span><span>> </span><span>=</span><span> _itemQuantity</span></span>
<span></span>
<span><span>    private</span><span> val</span><span> _itemPrice </span><span>=</span><span> MutableLiveData</span><span>(</span><span>10.0</span><span>)</span></span>
<span><span>    val</span><span> itemPrice: </span><span>LiveData</span><span>&#x3C;</span><span>Double</span><span>> </span><span>=</span><span> _itemPrice</span></span>
<span></span>
<span><span>    val</span><span> totalPrice: </span><span>LiveData</span><span>&#x3C;</span><span>Double</span><span>> </span><span>=</span><span> MediatorLiveData</span><span>&#x3C;</span><span>Double</span><span>>().</span><span>apply</span><span> {</span></span>
<span><span>        addSource</span><span>(_itemQuantity) { quantity </span><span>-></span></span>
<span><span>            value</span><span> =</span><span> quantity </span><span>*</span><span> (_itemPrice.</span><span>value</span><span> ?: </span><span>0.0</span><span>)</span></span>
<span><span>        }</span></span>
<span><span>    }</span></span>
<span></span>
<span></span>
<span><span>    fun</span><span> addToCart</span><span>() {</span></span>
<span><span>        _itemQuantity.</span><span>value</span><span> =</span><span> (_itemQuantity.</span><span>value</span><span> ?: </span><span>0</span><span>) </span><span>+</span><span> 1</span></span>
<span><span>    }</span></span>
<span></span>
<span><span>    fun</span><span> checkout</span><span>() {</span></span>
<span><span>        // ...</span></span>
<span><span>    }</span></span>
<span><span>}</span></span></code></pre><p>在XML布局中绑定ViewModel的数据：
</p><pre tabindex="0"><code><span><span>&#x3C;</span><span>layout</span><span> xmlns</span><span>:</span><span>android</span><span>=</span><span>"</span><span>http://schemas.android.com/apk/res/android</span><span>"</span><span>></span></span>
<span><span>    &#x3C;</span><span>data</span><span>></span></span>
<span><span>        &#x3C;</span><span>variable</span></span>
<span><span>            name</span><span>=</span><span>"</span><span>viewModel</span><span>"</span></span>
<span><span>            type</span><span>=</span><span>"</span><span>com.example.MainViewModel</span><span>"</span><span> /></span></span>
<span><span>    &#x3C;/</span><span>data</span><span>></span></span>
<span></span>
<span><span>    &#x3C;</span><span>LinearLayout</span></span>
<span><span>        android</span><span>:</span><span>id</span><span>=</span><span>"</span><span>@+id/main</span><span>"</span></span>
<span><span>        android</span><span>:</span><span>layout_width</span><span>=</span><span>"</span><span>match_parent</span><span>"</span></span>
<span><span>        android</span><span>:</span><span>layout_height</span><span>=</span><span>"</span><span>match_parent</span><span>"</span></span>
<span><span>        android</span><span>:</span><span>orientation</span><span>=</span><span>"</span><span>vertical</span><span>"</span></span>
<span><span>        android</span><span>:</span><span>padding</span><span>=</span><span>"</span><span>16dp</span><span>"</span><span>></span></span>
<span></span>
<span><span>        &#x3C;</span><span>EditText</span></span>
<span><span>            android</span><span>:</span><span>inputType</span><span>=</span><span>"</span><span>number</span><span>"</span></span>
<span><span>            android</span><span>:</span><span>text</span><span>=</span><span>"</span><span>@{String.valueOf(viewModel.itemQuantity)}</span><span>"</span><span> /></span></span>
<span></span>
<span><span>        &#x3C;</span><span>TextView</span></span>
<span><span>            android</span><span>:</span><span>text</span><span>=</span><span>"</span><span>@{String.valueOf(viewModel.totalPrice)}</span><span>"</span><span> /></span></span>
<span></span>
<span><span>        &#x3C;</span><span>Button</span></span>
<span><span>            android</span><span>:</span><span>onClick</span><span>=</span><span>"</span><span>@{() -> viewModel.addToCart()}</span><span>"</span><span> /></span></span>
<span><span>    &#x3C;/</span><span>LinearLayout</span><span>></span></span>
<span><span>&#x3C;/</span><span>layout</span><span>></span></span></code></pre><p>ViewModel与XML View之间建立了数据的双向绑定，ViewModel中的LiveData变更会直接体现在UI上，不用手写数据变更监听的代码，实际应用中，ViewModel层可能会从Model层获取商品数据，不同商品有不同价格，ViewModel处理价格的计算逻辑，View只负责最终结果的呈现。
</p><p>这样就实现了关注点分离，利于代码的维护，例如可以在不改变ViewModel的情况下使用Android新的UI库——Jetpack Compose，只需要在Compose中使用：
</p><pre tabindex="0"><code><span><span>/**</span></span>
<span><span>* 需要安装几个依赖</span></span>
<span><span>* androidx.lifecycle:lifecycle-viewmodel-compose-android</span></span>
<span><span>* androidx.compose.runtime:runtime-livedata</span></span>
<span><span>*/</span></span>
<span></span>
<span><span>val</span><span> totalPrice </span><span>by</span><span> viewModel.totalPrice.</span><span>observeAsState</span><span>()</span></span>
<span></span>
<span><span>..</span><span>.</span></span>
<span></span>
<span><span>Button</span><span>(onClick </span><span>=</span><span> { viewModel.</span><span>addToCart</span><span>() })</span></span></code></pre><p>就可以了。
</p><p>那这么做有没有什么缺陷呢？MVI和这种模式的区别在哪？
</p><h2 id="user-content-mvi" class="">MVI<a class="" tabindex="-1" href="#mvi">#</a></h2><p>虽然一般来说在MVVM中，数据绑定是双向的，但为了数据安全，通常只向View暴露一个只读的LiveData，避免意外的数据修改：
</p><pre tabindex="0"><code><span><span>private</span><span> val</span><span> _stateA </span><span>=</span><span> MutableLiveData</span><span>(</span><span>0</span><span>)</span></span>
<span><span>val</span><span> stateA </span><span>=</span><span> _stateA</span></span>
<span><span>private</span><span> val</span><span> _stateB </span><span>=</span><span> MutableLiveData</span><span>(</span><span>0</span><span>)</span></span>
<span><span>val</span><span> stateB </span><span>=</span><span> _stateB</span></span>
<span></span>
<span><span>// 使用StateFlow也是一样</span></span>
<span><span>private</span><span> val</span><span> _stateA </span><span>=</span><span> MutableStateFlow</span><span>(</span><span>0</span><span>)</span></span>
<span><span>val</span><span> stateA </span><span>=</span><span> _stateA.</span><span>asStateFlow</span><span>()</span></span>
<span><span>private</span><span> val</span><span> _stateB </span><span>=</span><span> MutableStateFlow</span><span>(</span><span>0</span><span>)</span></span>
<span><span>val</span><span> stateB </span><span>=</span><span> _stateB.</span><span>asStateFlow</span><span>()</span></span>
<span></span>
<span><span>// 再暴露一些封装的方法用于更新state</span></span>
<span><span>fun</span><span> changeXX</span><span>() {</span></span>
<span><span>}</span></span></code></pre><p>这些state可能会散落在UI的各处，给每个state都重复一遍私有和公开的声明也让代码有点繁瑣。如果要做单元测试，测试代码也会不简洁。MVI模式的核心就在于：单向数据流、单一不可变的状态对象及事件驱动的状态管理。
</p><p>MVI是model-view-intent的编写：
</p><p><img alt="MVI" src="https://miro.medium.com/v2/resize:fit:1400/format:webp/1*g096nFb3zpzDDiZIJWAEUA.png"></p><p>在实际的代码中，通常会封装一个State数据类：
</p><pre tabindex="0"><code><span><span>data</span><span> class</span><span> ShopingState</span><span>(</span></span>
<span><span>    val</span><span> isLoading: </span><span>Boolean</span><span> =</span><span> false</span><span>,</span></span>
<span><span>    val</span><span> goods: </span><span>Goods</span><span> =</span><span> emptyList</span><span>(),</span></span>
<span><span>    val</span><span> unpaid: </span><span>Double</span><span> =</span><span> 0.0</span><span>,</span></span>
<span><span>    val</span><span> error: </span><span>String</span><span>? </span><span>=</span><span> null</span><span>,</span></span>
<span><span>)</span></span></code></pre><p>仍然可以使用ViewModel类，将业务逻辑放在ViewModel中，但这次只有一个state对象：
</p><pre tabindex="0"><code><span><span>// 可能需要依赖注入封装了Data Layer的repository</span></span>
<span><span>class</span><span> MyViewModel</span><span>(</span><span>private</span><span> val</span><span> repository: </span><span>GoodsRepository</span><span>) : </span><span>ViewModel</span><span>() {</span></span>
<span><span>    private</span><span> val</span><span> _state </span><span>=</span><span> MutableStateFlow</span><span>(</span><span>ShopingState</span><span>())</span></span>
<span><span>    val</span><span> state </span><span>=</span><span> _state.</span><span>asStateFlow</span><span>()</span></span>
<span><span>}</span></span></code></pre><p>但这次ViewModel不直接暴露可以更新state的方法，而是使用一个自定义的Intent：
</p><pre tabindex="0"><code><span><span>// 使用sealed便于安全地模式区配</span></span>
<span><span>sealed</span><span> object</span><span> ShopingIntent</span><span> {</span></span>
<span><span>    data</span><span> object</span><span> LoadGoods</span><span> : </span><span>ShopingIntent</span></span>
<span><span>    data</span><span> object</span><span> Checkout</span><span> : </span><span>ShopingIntent</span></span>
<span><span>    data</span><span> class</span><span> AddToCart</span><span>(</span><span>val</span><span> id: </span><span>String</span><span>) : </span><span>ShopingIntent</span></span>
<span><span>}</span></span>
<span></span>
<span><span>class</span><span> MyViewModel</span><span>(</span><span>private</span><span> val</span><span> repository: </span><span>GoodsRepository</span><span>) : </span><span>ViewModel</span><span>() {</span></span>
<span><span>    fun</span><span> onIntent</span><span>(intent: </span><span>ShopingIntent</span><span>) {</span></span>
<span><span>        when</span><span> (intent) {</span></span>
<span><span>            is</span><span> ShopingIntent.LoadGoods </span><span>-></span><span> {</span></span>
<span><span>                //</span></span>
<span><span>            }</span></span>
<span><span>            is</span><span> ShopingIntent.Checkout </span><span>-></span><span> {</span></span>
<span><span>            }</span></span>
<span><span>        }</span></span>
<span><span>    }</span></span>
<span><span>}</span></span></code></pre><p>将整个视图和ViewModel分开来：
</p><pre tabindex="0"><code><span><span>val</span><span> state </span><span>by</span><span> viewModel.state.</span><span>collectAsState</span><span>()</span></span>
<span></span>
<span><span>ShopingScreen</span><span>(</span></span>
<span><span>    state </span><span>=</span><span> state,</span></span>
<span><span>    onIntent </span><span>=</span><span> viewModel::</span><span>onIntent</span></span>
<span><span>)</span></span>
<span></span>
<span><span>@Composable</span></span>
<span><span>fun</span><span> ShopingScreen</span><span>(</span><span>..</span><span>) {</span></span>
<span><span>    // UI层只是向外抛出一个Intent，不再关心ViewModel如何处理数据</span></span>
<span><span>    Button</span><span>(onClick </span><span>=</span><span> { </span><span>onIntent</span><span>(ShopingIntent.LoadGoods) })</span></span>
<span><span>}</span></span></code></pre><p>在实际应用中，Intent不一定必须由ViewModel处理，例如我用的某个第三方SDK要求在Activity上调用某个方法：
</p><pre tabindex="0"><code><span><span>MainScree</span><span>(</span></span>
<span><span>    state </span><span>=</span><span> state,</span></span>
<span><span>    onIntent </span><span>=</span><span> { intent </span><span>-></span></span>
<span><span>        when</span><span> (intent) {</span></span>
<span><span>            is</span><span> MyIntent.Foo </span><span>-></span><span> {</span></span>
<span><span>            }</span></span>
<span><span>            is</span><span> MyIntent.Bar </span><span>-></span><span> {</span></span>
<span><span>            }</span></span>
<span><span>            else</span><span> -></span><span> viewModel.</span><span>onIntent</span><span>(intent)</span></span>
<span><span>        }</span></span>
<span><span>    }</span></span>
<span><span>)</span></span></code></pre><p>所有的状态数据都集中在了一处，数据只能单向流入UI层，用户交互产生Intent，对Intent的处理决定是否需要更新状态。整个UI可以脱离ViewModel的业务逻辑代码，单独做测试、预览。
</p><p>相比之下，MVI不像MVC或MVVM那样定义清晰，Android官方也没有强制开发者必须遵守这个模式。可以将其看作在MVVM架构上的助力View和ViewModel解耦的范式，解耦合通常是好的，但要注意没有银弹，单一状态对象也可能带来一些不简洁的代码，如每次状态变更都需要copy：
</p><pre tabindex="0"><code><span><span>_state.</span><span>update</span><span> { it.</span><span>copy</span><span>(isLoading </span><span>=</span><span> true</span><span>) }</span></span></code></pre><p>还有由于Kotlin不是像Haskell一样天生不可变的纯函数式语言，頻繁的copy也可能会影响性能（得看JIT的优化）？
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>Android开发拾遗：异步与协程</title>
          <link>https://elliot00.com/posts/kotlin-async</link>
          <description>这篇文章主要介绍了Kotlin协程的基本用法，包括语法、结构化并发、异步流、生命周期管理等方面。</description>
          <pubDate>Mon, 06 May 2024 07:14:27 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <p>最近公司在做一个Android应用，用的<a href="https://kotlinlang.org">Kotlin</a>。虽然之前为了我的输入法计划写过一点Android，不过基本上是仗着对其他语言的熟悉摸索着写，没有系统了解过这门语言以及Android开发的相关概念。趁着假期闲下来了，打算看看官方文档，总结一下不甚了解的Android开发相关知识，因此有了这篇文。预计会分成几篇做一个系列，本文主要是协程相关内容。
</p><h2 id="user-content-异步" class="">异步<a class="" tabindex="-1" href="#异步">#</a></h2><p>为了便于展开后续内容，还是要简单聊聊老生常谈的话题：什么是异步？为什么要异步？
</p><p>想象现在有一家小咖啡店，只有一个店员，一个咖啡机。有三个人先后来到店里买咖啡，店员依次为每个人点单（花费1分钟），开启机器制作（等待5分钟），打包咖啡（1分钟），按顺序完成所有工作一共花费了21分钟。
</p><p><img alt="sync" src="https://r2.elliot00.com/kotlin/coffee1.png" width="1584" height="656"></p><p>但是，咖啡机运转时（IO操作），店员（CPU）实际上是闲着的，如果店员不是按顺序完成一个人的全部任务，比如当第一位客人的咖啡开始制作后就转头给第二位客人点单，是不是能节省时间呢？
</p><p><img alt="async" src="https://r2.elliot00.com/kotlin/coffee2.png" width="1842" height="834"></p><p>这种情况下，将花费17分钟（忽略转身花费的时间），节省了四分钟。
</p><p>异步解释起来很简单，就是​<strong>不按顺序步骤执行任务</strong>​。之所以要异步，根本上是为了​<strong>不浪费计算资源</strong>​，当遇到如文件读取之类的IO任务时，不要让CPU闲着等待而是转头去执行其它任务。换句话说，不是为了异步而异步，对于计算密集型任务，不按顺序执行，算上调度任务带来的额外开销反而会使整个任务执行时间更长。
</p><p>很多时候，异步使人感到困惑是因为这一个术语被用来表达了多个有关联但不相同的概念。如有时异步这个词暗含了「并发」的意义；有时人们又用异步来指代编程语言提供的便于人们实现异步程序的机制。
</p><h2 id="user-content-协程" class="">协程<a class="" tabindex="-1" href="#协程">#</a></h2><p>操作系统提供了虚拟CPU、虚拟内存、线程等一系列抽象给应用程序实现并发，而Kotlin则提供了一个相比线程更轻量化的机制来帮助开发者写出异步非阻塞程序，即「协程」。
</p><p>看看如何使用协程：
</p><pre tabindex="0"><code><span><span>import</span><span> kotlinx.coroutines.</span><span>*</span></span>
<span></span>
<span><span>fun</span><span> main</span><span>() </span><span>=</span><span> runBlocking</span><span> { </span><span>// this: CoroutineScope</span></span>
<span><span>    launch</span><span> { </span><span>// launch a new coroutine and continue</span></span>
<span><span>        delay</span><span>(</span><span>1000L</span><span>) </span><span>// non-blocking delay for 1 second (default time unit is ms)</span></span>
<span><span>        println</span><span>(</span><span>"World!"</span><span>) </span><span>// print after delay</span></span>
<span><span>    }</span></span>
<span><span>    println</span><span>(</span><span>"Hello"</span><span>) </span><span>// main coroutine continues while a previous one is delayed</span></span>
<span><span>}</span></span></code></pre><p>要搞明白Kotlin的协程使用，先要了解一点相关语法。首先是​<code class="">fun main() = ...</code>​，如果你写过<a href="https://www.haskell.org/">Haskell</a>对这种形式应该不会陌生了，在Kotliln中一个函数如果只包含一个表达式就可以简写成一个类似赋值语句的形式：
</p><pre tabindex="0"><code><span><span>fun</span><span> main</span><span>() </span><span>=</span><span> println</span><span>(</span><span>"hello"</span><span>)</span></span>
<span></span>
<span><span>// 等于</span></span>
<span><span>fun</span><span> main</span><span>() {</span></span>
<span><span>    println</span><span>(</span><span>"hello"</span><span>)</span></span>
<span><span>}</span></span></code></pre><p>那么​<code class="">runBlock {...}</code>​和​<code class="">launch {...}</code>​又是什么特殊的语句块吗？在Kotlin中，如果一个函数的最后一个参数是 <em>Lambda表达式</em>​，调用时就可以写成一种语句块的形式：
</p><pre tabindex="0"><code><span><span>fun</span><span> foo</span><span>(bar: () </span><span>-></span><span> Unit) {</span></span>
<span><span>    bar</span><span>()</span></span>
<span><span>}</span></span>
<span></span>
<span><span>fun</span><span> main</span><span>() {</span></span>
<span><span>    foo</span><span> {</span></span>
<span><span>        println</span><span>(</span><span>"Hello"</span><span>)</span></span>
<span><span>    }</span></span>
<span></span>
<span><span>    // 等价于 =></span></span>
<span><span>    foo</span><span>(bar </span><span>=</span><span> { </span><span>println</span><span>(</span><span>"Hello"</span><span>) })</span></span>
<span><span>}</span></span></code></pre><p>看上去使用​<code class="">launch</code>​就可以启用一个新的协程，但是如果这样写代码：
</p><pre tabindex="0"><code><span><span>import</span><span> kotlinx.coroutines.</span><span>*</span></span>
<span></span>
<span><span>fun</span><span> main</span><span>() {</span></span>
<span><span>    foo</span><span>()</span></span>
<span><span>}</span></span>
<span></span>
<span><span>fun</span><span> foo</span><span>() {</span></span>
<span><span>    launch</span><span> {</span></span>
<span><span>        println</span><span>(</span><span>"Hello"</span><span>)</span></span>
<span><span>    }</span></span>
<span><span>}</span></span></code></pre><p>将会得到一个错误：‘Unresolved reference: launch’，这是为何？来看一下​<code class="">runBlocking</code>​的函数签名：
</p><pre tabindex="0"><code><span><span>expect </span><span>fun</span><span> &#x3C;</span><span>T</span><span>> </span><span>runBlocking</span><span>(</span></span>
<span><span>    context: </span><span>CoroutineContext</span><span> =</span><span> EmptyCoroutineContext,</span></span>
<span><span>    block: </span><span>suspend</span><span> CoroutineScope</span><span>.() </span><span>-></span><span> T</span></span>
<span><span>): </span><span>T</span></span></code></pre><p>忽略掉其他部分，只看​<code class="">CoroutineScope.() -> T</code>​，这在Kotlin中称为​<strong>扩展方法</strong>​，举个例子：
</p><pre tabindex="0"><code><span><span>fun</span><span> Int</span><span>.</span><span>addNine</span><span>(): </span><span>Int</span><span> {</span></span>
<span><span>    return</span><span> this</span><span> +</span><span> 9</span></span>
<span><span>}</span></span>
<span></span>
<span><span>fun</span><span> main</span><span>() {</span></span>
<span><span>    val</span><span> result </span><span>=</span><span> 4</span><span>.</span><span>addNine</span><span>()</span></span>
<span><span>    println</span><span>(</span><span>"Result is </span><span>$result</span><span>"</span><span>)</span></span>
<span><span>}</span></span></code></pre><p>Kotlin内置的Int类型是没有addNine方法的，但是我们可以用​<code class="">fun Int.addNine()</code>​这种形式去拓展它，并且就好像是在写这个类本身的方法一样，甚至可以引用​<code class="">this</code>​。这种语法可以帮我们为无法直接改动源代码的外部对象拓展接口。
</p><p>所以实际上不能直接用​<code class="">launch</code>​的原因是，这是属于​<code class="">CoroutineScope</code>​类内的一个方法（其实launch也是用CoroutineScope.launch形式定义的扩展方法）。​<code class="">runBlocking</code>​声明了​<code class="">block</code>​参数应该是这个CoroutineScope类的扩展方法，调用时是在一个CoroutineScope对象上调用的，所以作为runBlocking的block参数的Lambda内可以使用launch以及其它来自CoroutineScope的属性、方法。
</p><pre tabindex="0"><code><span><span>fun</span><span> main</span><span>() </span><span>=</span><span> runBlocking</span><span> {</span></span>
<span><span>    // 和直接用launch一样，this指向一个CoroutineScope对象</span></span>
<span><span>    this</span><span>.</span><span>launch</span><span> {</span></span>
<span><span>        delay</span><span>(</span><span>1000L</span><span>)</span></span>
<span><span>    }</span></span>
<span><span>}</span></span></code></pre><p>Kotlin中每一个协程都要在​<code class="">CoroutineScope</code>​内启动，​<code class="">runBlocking</code>​会构建一个​<code class="">CoroutineScope</code>​对象，从名字能看出来，它会阻塞当前线程，等待内部的协程完成，所以通常放在异步调用的「根部」使用，如前面的例子就是在​<code class="">main()</code>​函数上使用的。
</p><h2 id="user-content-挂起函数" class="">挂起函数<a class="" tabindex="-1" href="#挂起函数">#</a></h2><p>Kotlin中协程相关内容大部分交给库实现，语法层面只有一个特别的，那就是「挂起函数」，使用​<code class="">suspend</code>​关键字定义。
</p><pre tabindex="0"><code><span><span>fun</span><span> main</span><span>() </span><span>=</span><span> runBlocking</span><span> {</span></span>
<span><span>    launch</span><span> { </span><span>doWorld</span><span>() }</span></span>
<span><span>    println</span><span>(</span><span>"Hello"</span><span>)</span></span>
<span><span>}</span></span>
<span></span>
<span><span>suspend</span><span> fun</span><span> doWorld</span><span>() {</span></span>
<span><span>    delay</span><span>(</span><span>1000L</span><span>)</span></span>
<span><span>    println</span><span>(</span><span>"World!"</span><span>)</span></span>
<span><span>}</span></span></code></pre><p>和其它语言中的异步函数相同点在于，挂起函数也具有传染性，即其内部可以调用其它普通函数，但调用挂起函数的函数也得是挂起函数。​<code class="">runBlocking</code>​就是普通函数到挂起函数之间的桥梁。
</p><p>挂起函数内是同步语义的：
</p><pre tabindex="0"><code><span><span>suspend</span><span> fun</span><span> foo</span><span>() {</span></span>
<span><span>    bar</span><span>()</span></span>
<span><span>}</span></span>
<span></span>
<span><span>suspend</span><span> fun</span><span> bar</span><span>() {</span></span>
<span><span>}</span></span>
<span></span>
<span><span>suspend</span><span> fun</span><span> baz</span><span>() {</span></span>
<span><span>}</span></span></code></pre><p>相当于JavaScript中：
</p><pre tabindex="0"><code><span><span>async</span><span> function</span><span> foo</span><span>()</span><span> {</span></span>
<span><span>    await</span><span> bar</span><span>()</span></span>
<span><span>    await</span><span> baz</span><span>()</span></span>
<span><span>}</span></span>
<span></span>
<span><span>async</span><span> function</span><span> bar</span><span>()</span><span> {</span></span>
<span><span>}</span></span>
<span></span>
<span><span>async</span><span> function</span><span> baz</span><span>()</span><span> {</span></span>
<span><span>}</span></span></code></pre><p>这种设计大概是为了让开发者在协程上下文内不去关注要使用的函数是否是挂起的，用与调用普通同步函数一致的方式去调用挂起函数，用下列代码看下顺序调用挂起函数的用时：
</p><pre tabindex="0"><code><span><span>import</span><span> kotlinx.coroutines.</span><span>*</span></span>
<span><span>import</span><span> kotlin.system.measureTimeMillis</span></span>
<span></span>
<span><span>fun</span><span> main</span><span>() </span><span>=</span><span> runBlocking</span><span> {</span></span>
<span><span>    val</span><span> time </span><span>=</span><span> measureTimeMillis</span><span> {</span></span>
<span><span>        hello</span><span>()</span></span>
<span><span>        hello</span><span>()</span></span>
<span><span>        hello</span><span>()</span></span>
<span><span>    }</span></span>
<span><span>    println</span><span>(</span><span>"Completed in </span><span>$time</span><span> ms"</span><span>)</span></span>
<span><span>}</span></span>
<span></span>
<span><span>suspend</span><span> fun</span><span> hello</span><span>() {</span></span>
<span><span>    // delay也是个挂起函数</span></span>
<span><span>    delay</span><span>(</span><span>1000</span><span>)</span></span>
<span><span>    println</span><span>(</span><span>"hello"</span><span>)</span></span>
<span><span>}</span></span>
<span></span>
<span><span>/* Result:</span></span>
<span><span>hello</span></span>
<span><span>hello</span></span>
<span><span>hello</span></span>
<span><span>Completed in 3019 ms</span></span>
<span><span>*/</span></span></code></pre><h2 id="user-content-launch" class="">launch<a class="" tabindex="-1" href="#launch">#</a></h2><p>通过​<code class="">launch</code>​和挂起函数结合，可以精细地控制代码中的并发与同步操作的。
</p><pre tabindex="0"><code><span><span>fun</span><span> main</span><span>() </span><span>=</span><span> runBlocking</span><span> {</span></span>
<span><span>    val</span><span> time </span><span>=</span><span> measureTimeMillis</span><span> {</span></span>
<span><span>        launch</span><span> {</span></span>
<span><span>            hello</span><span>(</span><span>1</span><span>)</span></span>
<span><span>        }</span></span>
<span><span>        launch</span><span> {</span></span>
<span><span>            hello</span><span>(</span><span>2</span><span>)</span></span>
<span><span>        }</span></span>
<span><span>        hello</span><span>(</span><span>3</span><span>)</span></span>
<span><span>    }</span></span>
<span><span>    // 最后一个hello()阻塞了println</span></span>
<span><span>    println</span><span>(</span><span>"Completed in </span><span>$time</span><span> ms"</span><span>)</span></span>
<span><span>}</span></span>
<span></span>
<span></span>
<span><span>suspend</span><span> fun</span><span> hello</span><span>(count: </span><span>Int</span><span>) {</span></span>
<span><span>    delay</span><span>(</span><span>1000</span><span>)</span></span>
<span><span>    println</span><span>(</span><span>"hello #</span><span>$count</span><span>"</span><span>)</span></span>
<span><span>}</span></span>
<span></span>
<span><span>/* Result:</span></span>
<span><span>hello #3</span></span>
<span><span>Completed in 1023 ms</span></span>
<span><span>hello #1</span></span>
<span><span>hello #2</span></span>
<span><span>*/</span></span></code></pre><p>launch会返回一个​<code class="">Job</code>​对象，有着类似线程的API：
</p><pre tabindex="0"><code><span><span>fun</span><span> log</span><span>(msg: </span><span>String</span><span>) </span><span>=</span><span> println</span><span>(</span><span>"[</span><span>${</span><span>Thread.</span><span>currentThread</span><span>().name</span><span>}</span><span>] </span><span>$msg</span><span>"</span><span>)</span></span>
<span></span>
<span><span>fun</span><span> main</span><span>() </span><span>=</span><span> runBlocking</span><span> {</span></span>
<span><span>    val</span><span> time </span><span>=</span><span> measureTimeMillis</span><span> {</span></span>
<span><span>        val</span><span> job </span><span>=</span><span> launch</span><span> {</span></span>
<span><span>            launch</span><span> {</span></span>
<span><span>                log</span><span>(</span><span>"job1 start"</span><span>)</span></span>
<span><span>                delay</span><span>(</span><span>1000L</span><span>)</span></span>
<span><span>                log</span><span>(</span><span>"job1 end"</span><span>)</span></span>
<span><span>            }</span></span>
<span><span>            launch</span><span> {</span></span>
<span><span>                log</span><span>(</span><span>"job2 start"</span><span>)</span></span>
<span><span>                delay</span><span>(</span><span>2000L</span><span>)</span></span>
<span><span>                log</span><span>(</span><span>"job2 end"</span><span>)</span></span>
<span><span>            }</span></span>
<span><span>            launch</span><span> {</span></span>
<span><span>                log</span><span>(</span><span>"job3 start"</span><span>)</span></span>
<span><span>                delay</span><span>(</span><span>5000L</span><span>)</span></span>
<span><span>                log</span><span>(</span><span>"job3 end"</span><span>)</span></span>
<span><span>            }</span></span>
<span><span>        }</span></span>
<span><span>        delay</span><span>(</span><span>3000L</span><span>)</span></span>
<span><span>        job.</span><span>cancel</span><span>()</span></span>
<span><span>        job.</span><span>join</span><span>()</span></span>
<span><span>        log</span><span>(</span><span>"job end"</span><span>)</span></span>
<span><span>    }</span></span>
<span><span>    log</span><span>(</span><span>"Completed in </span><span>$time</span><span> ms"</span><span>)</span></span>
<span><span>}</span></span>
<span></span>
<span><span>/* Result:</span></span>
<span><span>[main @coroutine#3] job1 start</span></span>
<span><span>[main @coroutine#4] job2 start</span></span>
<span><span>[main @coroutine#5] job3 start</span></span>
<span><span>[main @coroutine#3] job1 end</span></span>
<span><span>[main @coroutine#4] job2 end</span></span>
<span><span>[main @coroutine#1] job end</span></span>
<span><span>[main @coroutine#1] Completed in 3077 ms</span></span>
<span><span>*/</span></span></code></pre><h2 id="user-content-async-await" class="">async-await<a class="" tabindex="-1" href="#async-await">#</a></h2><p>在Kotlin中​<code class="">async</code>​和​<code class="">await</code>​不是关键字，和​<code class="">launch</code>​一样，​<code class="">async</code>​可以开启一个新协程，但不同的是它会返回一个​<code class="">Deferred&#x3C;T></code>​对象，类似JavaSciprt中的​<code class="">Promise&#x3C;T></code>​，可以通过对其调用​<code class="">await()</code>​方法得到结果。
</p><pre tabindex="0"><code><span><span>fun</span><span> main</span><span>() </span><span>=</span><span> runBlocking</span><span> {</span></span>
<span><span>    val</span><span> time </span><span>=</span><span> measureTimeMillis</span><span> {</span></span>
<span><span>        // 并行执行</span></span>
<span><span>        val</span><span> res1 </span><span>=</span><span> async</span><span> { </span><span>foo</span><span>() }</span></span>
<span><span>        val</span><span> res2 </span><span>=</span><span> async</span><span> { </span><span>bar</span><span>() }</span></span>
<span></span>
<span><span>        // 等待两个协程完成</span></span>
<span><span>        println</span><span>(</span><span>"Result is </span><span>${</span><span>res1.</span><span>await</span><span>() </span><span>+</span><span> res2.</span><span>await</span><span>()</span><span>}</span><span>"</span><span>)</span></span>
<span><span>    }</span></span>
<span><span>    println</span><span>(</span><span>"Completed in </span><span>$time</span><span> ms"</span><span>)</span></span>
<span><span>}</span></span>
<span></span>
<span><span>suspend</span><span> fun</span><span> foo</span><span>(): </span><span>Int</span><span> {</span></span>
<span><span>    delay</span><span>(</span><span>1000L</span><span>)</span></span>
<span><span>    return</span><span> 1</span></span>
<span><span>}</span></span>
<span></span>
<span><span>suspend</span><span> fun</span><span> bar</span><span>(): </span><span>Int</span><span> {</span></span>
<span><span>    delay</span><span>(</span><span>1500L</span><span>)</span></span>
<span><span>    return</span><span> 2</span></span>
<span><span>}</span></span></code></pre><h2 id="user-content-协程上下文" class="">协程上下文<a class="" tabindex="-1" href="#协程上下文">#</a></h2><p>先看一眼<a href="https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/launch.html">launch</a>的函数签名：
</p><pre tabindex="0"><code><span><span>fun</span><span> CoroutineScope</span><span>.</span><span>launch</span><span>(</span></span>
<span><span>    context: </span><span>CoroutineContext</span><span> =</span><span> EmptyCoroutineContext,</span></span>
<span><span>    start: </span><span>CoroutineStart</span><span> =</span><span> CoroutineStart.DEFAULT,</span></span>
<span><span>    block: </span><span>suspend</span><span> CoroutineScope</span><span>.() </span><span>-></span><span> Unit</span></span>
<span><span>): </span><span>Job</span></span></code></pre><blockquote><p>The coroutine context is inherited from a CoroutineScope. Additional context elements can be specified with context argument. If the context does not have any dispatcher nor any other ContinuationInterceptor, then Dispatchers.Default is used. The parent job is inherited from a CoroutineScope as well, but it can also be overridden with a corresponding context element.
</p></blockquote><p>Kotlin的函数支持默认参数，从文档可知，前面使用launch的过程中没有给它指定第一个参数​<code class="">context</code>​，实际上它会默认使用​<code class="">Dispatchers.Default</code>​。这里的​<code class="">CoroutineContext</code>​是什么？​<code class="">Dispatchers</code>​又是什么呢？
</p><p><code class="">CoroutineContext</code>​是一个接口，它是一个​<code class="">Element</code>​接口的​<code class="">indexed set</code>​（Element实际上又继承自Coroutine），可以用它来控制协程的行为。<a href="https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-coroutine-dispatcher/">CoroutineDispatcher</a>就是一个实现了Element接口的类，它可以用来指定协程在某个特定的线程上或线程池上运行。
</p><p>不同于Python或JavaScript，Kotlin可以充分利用现代多核CPU来做并行计算，使用​<code class="">Dispatchers.Default</code>​可以让协程跑在CPU密集任务的线程池上，还有​<code class="">Dispatchers.IO</code>​适合在Android应用中执行文件读取、网络请求等IO任务而不会阻塞UI线程，​<code class="">Dispatchers.Main</code>​在主线程中运行，还可以用​<code class="">newSingleThreadContext()</code>​来启用一个单独的新线程。
</p><p>来段代码看下：
</p><pre tabindex="0"><code><span><span>import</span><span> kotlinx.coroutines.</span><span>*</span></span>
<span></span>
<span><span>fun</span><span> showThread</span><span>(tag: </span><span>String</span><span>) </span><span>=</span><span> println</span><span>(</span><span>"</span><span>$tag</span><span> Running in </span><span>${</span><span>Thread.</span><span>currentThread</span><span>().name</span><span>}</span><span>"</span><span>)</span></span>
<span></span>
<span><span>fun</span><span> main</span><span>() </span><span>=</span><span> runBlocking</span><span>&#x3C;</span><span>Unit</span><span>> {</span></span>
<span><span>    launch</span><span> {</span></span>
<span><span>        showThread</span><span>(tag </span><span>=</span><span> "#1"</span><span>)</span></span>
<span><span>        launch</span><span>(</span><span>newSingleThreadContext</span><span>(</span><span>"MyOwnThread"</span><span>)) {</span></span>
<span><span>            delay</span><span>(</span><span>1000L</span><span>)</span></span>
<span><span>            showThread</span><span>(tag </span><span>=</span><span> "#2"</span><span>)</span></span>
<span><span>        }</span></span>
<span><span>    }</span></span>
<span></span>
<span><span>    launch</span><span>(Dispatchers.Default) {</span></span>
<span><span>        showThread</span><span>(tag </span><span>=</span><span> "#3"</span><span>)</span></span>
<span><span>        repeat</span><span>(</span><span>5</span><span>) {</span></span>
<span><span>            launch</span><span> {</span></span>
<span><span>                val</span><span> foo </span><span>=</span><span> 10</span><span> *</span><span> 10</span></span>
<span><span>                showThread</span><span>(tag </span><span>=</span><span> "#4"</span><span>)</span></span>
<span><span>            }</span></span>
<span><span>        }</span></span>
<span><span>    }</span></span>
<span><span>}</span></span>
<span></span>
<span><span>/* Result:</span></span>
<span><span>#3 Running in DefaultDispatcher-worker-2 @coroutine#3</span></span>
<span><span>#1 Running in main @coroutine#2</span></span>
<span><span>#4 Running in DefaultDispatcher-worker-1 @coroutine#4</span></span>
<span><span>#4 Running in DefaultDispatcher-worker-2 @coroutine#8</span></span>
<span><span>#4 Running in DefaultDispatcher-worker-2 @coroutine#5</span></span>
<span><span>#4 Running in DefaultDispatcher-worker-1 @coroutine#7</span></span>
<span><span>#4 Running in DefaultDispatcher-worker-2 @coroutine#6</span></span>
<span><span>#2 Running in MyOwnThread @coroutine#9</span></span>
<span><span>*/</span></span></code></pre><p>前面提到CoroutineContext是一个​<code class="">indexed set</code>​结构，也就是说可以用类似哈希表的API来获取当前的上下文信息：
</p><pre tabindex="0"><code><span><span>fun</span><span> main</span><span>() </span><span>=</span><span> runBlocking</span><span>&#x3C;</span><span>Unit</span><span>> {</span></span>
<span><span>    launch</span><span> {</span></span>
<span><span>        delay</span><span>(</span><span>1000L</span><span>)</span></span>
<span><span>        // 回顾下扩展方法的语法，你应该不会对这个coroutineContext从哪来感到迷惑</span></span>
<span><span>        println</span><span>(</span><span>"Context: </span><span>${</span><span>coroutineContext[Job]</span><span>}</span><span>"</span><span>)</span></span>
<span><span>    }</span></span>
<span><span>}</span></span></code></pre><p>CoroutineContext重载了加法运算符，可以用比较直观的代码来组合多个上下文Element：
</p><pre tabindex="0"><code><span><span>import</span><span> kotlinx.coroutines.</span><span>*</span></span>
<span></span>
<span><span>fun</span><span> showThread</span><span>(tag: </span><span>String</span><span>) </span><span>=</span><span> println</span><span>(</span><span>"</span><span>$tag</span><span> Running in </span><span>${</span><span>Thread.</span><span>currentThread</span><span>().name</span><span>}</span><span>"</span><span>)</span></span>
<span></span>
<span><span>fun</span><span> main</span><span>() </span><span>=</span><span> runBlocking</span><span>&#x3C;</span><span>Unit</span><span>> {</span></span>
<span><span>    // 用+运算符来组合两个上下文元素</span></span>
<span><span>    launch</span><span>(Dispatchers.Default </span><span>+</span><span> CoroutineName</span><span>(</span><span>"MyCoroutine"</span><span>)) {</span></span>
<span><span>        showThread</span><span>(tag </span><span>=</span><span> "#1"</span><span>)</span></span>
<span><span>        repeat</span><span>(</span><span>5</span><span>) {</span></span>
<span><span>            launch</span><span> {</span></span>
<span><span>                val</span><span> foo </span><span>=</span><span> 10</span><span> *</span><span> 10</span></span>
<span><span>                showThread</span><span>(tag </span><span>=</span><span> "#2"</span><span>)</span></span>
<span><span>            }</span></span>
<span><span>        }</span></span>
<span><span>    }</span></span>
<span><span>}</span></span>
<span></span>
<span><span>/* Result:</span></span>
<span><span>#1 Running in DefaultDispatcher-worker-1 @MyCoroutine#2</span></span>
<span><span>#2 Running in DefaultDispatcher-worker-1 @MyCoroutine#7</span></span>
<span><span>#2 Running in DefaultDispatcher-worker-2 @MyCoroutine#3</span></span>
<span><span>#2 Running in DefaultDispatcher-worker-2 @MyCoroutine#5</span></span>
<span><span>#2 Running in DefaultDispatcher-worker-2 @MyCoroutine#6</span></span>
<span><span>#2 Running in DefaultDispatcher-worker-1 @MyCoroutine#4</span></span>
<span><span>*/</span></span></code></pre><p>仔细观察输出，可以发现，内部的launch并没有指定上下文，但看上去是复用了上层的上下文，这是怎么做到的？
</p><h2 id="user-content-结构化并发" class="">结构化并发<a class="" tabindex="-1" href="#结构化并发">#</a></h2><p>Kotlin采用了结构化并发的概念，这个概念可能源自结构化编程，<a href="https://en.wikipedia.org/wiki/Edsger_W._Dijkstra">Edsger Dijkstra</a>曾经提出过「Goto有害论」，并提出要用结构化编程来改善程序。简而言之，结构化编程希望限制​<strong>控制流</strong>​只有​<strong>单一入口</strong>​和​<strong>单一出口</strong>​。
</p><blockquote><p>很多并发/线程相关的术语都是Edsger Dijkstra创造的，他在1972年获得了图灵奖。
</p></blockquote><p><img alt="goto" src="https://r2.elliot00.com/kotlin/goto-statement.png" width="652" height="614"></p><p>不像顺序执行的语句，使用goto跳转执行的程序可以在任意时间跳转到任务意指令位置去执行，大量采用这种控制流的代码最终可读性会非常糟糕。
</p><p><img alt="control structures" src="https://r2.elliot00.com/kotlin/control-flow2.png" width="2847" height="846"></p><p>结构化的控制流通过块来控制层级，一块程序在执行中途经过条件、循环、函数调用等子层级的程序块，最终还是会从上层出口退出。结构化并发也是类似的思路，通过CoroutineScope来组织具有父子层级的协程，还是通过代码来说明：
</p><pre tabindex="0"><code><span><span>import</span><span> kotlinx.coroutines.</span><span>*</span></span>
<span></span>
<span><span>fun</span><span> log</span><span>(msg: </span><span>String</span><span>) </span><span>=</span><span> println</span><span>(</span><span>"</span><span>${</span><span>Thread.</span><span>currentThread</span><span>().name</span><span>}</span><span> $msg</span><span>"</span><span>)</span></span>
<span></span>
<span><span>fun</span><span> main</span><span>() </span><span>=</span><span> runBlocking</span><span> {</span></span>
<span><span>    val</span><span> job </span><span>=</span><span> launch</span><span>(</span><span>CoroutineName</span><span>(</span><span>"MyCoroutine"</span><span>)) {</span></span>
<span><span>        launch</span><span> {</span></span>
<span><span>            delay</span><span>(</span><span>2000</span><span>)</span></span>
<span><span>            log</span><span>(</span><span>"Child1 done"</span><span>)</span></span>
<span><span>        }</span></span>
<span><span>        launch</span><span> {</span></span>
<span><span>            delay</span><span>(</span><span>2000</span><span>)</span></span>
<span><span>            log</span><span>(</span><span>"Child2 done"</span><span>)</span></span>
<span><span>        }</span></span>
<span><span>    }</span></span>
<span></span>
<span><span>    // 挂起等待父级job结束</span></span>
<span><span>    job.</span><span>join</span><span>()</span></span>
<span><span>    log</span><span>(</span><span>"Parent done"</span><span>)</span></span>
<span><span>}</span></span>
<span></span>
<span><span>/* Result:</span></span>
<span><span>main @MyCoroutine#3 Child1 done</span></span>
<span><span>main @MyCoroutine#4 Child2 done</span></span>
<span><span>main @coroutine#1 Parent done</span></span>
<span><span>*/</span></span></code></pre><p>首先可以看到，外层的上下文被传递下去了，如果去看<a href="https://github.com/Kotlin/kotlinx.coroutines/blob/master/kotlinx-coroutines-core/common/src/Builders.common.kt#L43">launch的源码</a>就会看到，launch内部将当前Scope的上下文和参数中的上下文（这里没有指定，用的是参数默认值EmptyCoroutineContext）做了合并操作。其次是，父层的协程在等待子层的协程结束后才结束，控制流最终回到了外层。
</p><p>再看这段代码：
</p><pre tabindex="0"><code><span><span>fun</span><span> main</span><span>() </span><span>=</span><span> runBlocking</span><span> {</span></span>
<span><span>    val</span><span> job </span><span>=</span><span> launch</span><span>(</span><span>CoroutineName</span><span>(</span><span>"MyCoroutine"</span><span>)) {</span></span>
<span><span>        launch</span><span> {</span></span>
<span><span>            log</span><span>(</span><span>"Child1 start"</span><span>)</span></span>
<span><span>            delay</span><span>(</span><span>1000</span><span>)</span></span>
<span><span>            log</span><span>(</span><span>"Child1 done"</span><span>)</span></span>
<span><span>        }</span></span>
<span><span>        launch</span><span> {</span></span>
<span><span>            log</span><span>(</span><span>"Child2 start"</span><span>)</span></span>
<span><span>            delay</span><span>(</span><span>3000</span><span>)</span></span>
<span><span>            log</span><span>(</span><span>"Child2 done"</span><span>)</span></span>
<span><span>        }</span></span>
<span><span>    }</span></span>
<span><span>    delay</span><span>(</span><span>1500</span><span>)</span></span>
<span><span>    job.</span><span>cancelAndJoin</span><span>()</span></span>
<span><span>    log</span><span>(</span><span>"Parent done"</span><span>)</span></span>
<span><span>}</span></span>
<span></span>
<span><span>/* Result:</span></span>
<span><span>main @MyCoroutine#3 Child1 start</span></span>
<span><span>main @MyCoroutine#4 Child2 start</span></span>
<span><span>main @MyCoroutine#3 Child1 done</span></span>
<span><span>main @coroutine#1 Parent done</span></span>
<span><span>*/</span></span></code></pre><p>第二个子协程在延时3秒后打印​<em>Child2 donw</em>​，但是父级在一秒半时取消了工作，可以看到，还没完成工作的Child2也被取消了。
</p><p>如果子协程里有错误呢？
</p><pre tabindex="0"><code><span><span>fun</span><span> main</span><span>() </span><span>=</span><span> runBlocking</span><span>&#x3C;</span><span>Unit</span><span>> {</span></span>
<span><span>    try</span><span> {</span></span>
<span><span>        calc</span><span>()</span></span>
<span><span>    } </span><span>catch</span><span> (e: </span><span>ArithmeticException</span><span>) {</span></span>
<span><span>        log</span><span>(</span><span>"捕获到错误"</span><span>)</span></span>
<span><span>    }</span></span>
<span><span>}</span></span>
<span></span>
<span><span>suspend</span><span> fun</span><span> calc</span><span>(): </span><span>Int</span><span> =</span><span> coroutineScope</span><span> {</span></span>
<span><span>    val</span><span> foo </span><span>=</span><span> async</span><span>&#x3C;</span><span>Int</span><span>> {</span></span>
<span><span>        log</span><span>(</span><span>"准备返回1"</span><span>)</span></span>
<span><span>        delay</span><span>(</span><span>3000</span><span>)</span></span>
<span><span>        log</span><span>(</span><span>"代码不会执行到这"</span><span>)</span></span>
<span><span>        1</span></span>
<span><span>    }</span></span>
<span><span>    val</span><span> bar </span><span>=</span><span> async</span><span>&#x3C;</span><span>Int</span><span>> {</span></span>
<span><span>        log</span><span>(</span><span>"准备抛错"</span><span>)</span></span>
<span><span>        throw</span><span> ArithmeticException</span><span>()</span></span>
<span><span>    }</span></span>
<span></span>
<span><span>    foo.</span><span>await</span><span>() </span><span>+</span><span> bar.</span><span>await</span><span>()</span></span>
<span><span>}</span></span>
<span></span>
<span><span>/* Result:</span></span>
<span><span>main @coroutine#2 准备返回1</span></span>
<span><span>main @coroutine#3 准备抛错</span></span>
<span><span>main @coroutine#1 捕获到错误</span></span>
<span><span>*/</span></span></code></pre><p>可以看到，子协程的错误会向上传递，并且会导致同一层级其它没有完成的协程任务被取消。
</p><p>但是Kotlin并没有在语法层面强制性要求结构化并发，所以其实可以绕过这一行为（但一般不推荐）：
</p><pre tabindex="0"><code><span><span>fun</span><span> main</span><span>() </span><span>=</span><span> runBlocking</span><span> {</span></span>
<span><span>    val</span><span> job </span><span>=</span><span> launch</span><span>(</span><span>CoroutineName</span><span>(</span><span>"MyCoroutine"</span><span>)) {</span></span>
<span><span>        GlobalScope.</span><span>launch</span><span> {</span></span>
<span><span>            log</span><span>(</span><span>"Global start"</span><span>)</span></span>
<span><span>            delay</span><span>(</span><span>2000</span><span>)</span></span>
<span><span>            log</span><span>(</span><span>"Global done"</span><span>)</span></span>
<span><span>        }</span></span>
<span><span>        launch</span><span> {</span></span>
<span><span>            log</span><span>(</span><span>"Child2 start"</span><span>)</span></span>
<span><span>            delay</span><span>(</span><span>3000</span><span>)</span></span>
<span><span>            log</span><span>(</span><span>"Child2 done"</span><span>)</span></span>
<span><span>        }</span></span>
<span><span>    }</span></span>
<span><span>    delay</span><span>(</span><span>1500</span><span>)</span></span>
<span><span>    job.</span><span>cancelAndJoin</span><span>()</span></span>
<span><span>    log</span><span>(</span><span>"Parent done"</span><span>)</span></span>
<span></span>
<span><span>    delay</span><span>(</span><span>2000</span><span>)</span></span>
<span><span>}</span></span>
<span></span>
<span><span>/* Result:</span></span>
<span><span>DefaultDispatcher-worker-1 @coroutine#3 Global start</span></span>
<span><span>main @MyCoroutine#4 Child2 start</span></span>
<span><span>main @coroutine#1 Parent done</span></span>
<span><span>DefaultDispatcher-worker-1 @coroutine#3 Global done</span></span>
<span><span>*/</span></span></code></pre><h2 id="user-content-取消协程" class="">取消协程<a class="" tabindex="-1" href="#取消协程">#</a></h2><p>前面举的例子里已经有好几个和取消相关了，要取消协程似乎很简单了，果真如此吗？尝试这段代码：
</p><pre tabindex="0"><code><span><span>fun</span><span> main</span><span>() </span><span>=</span><span> runBlocking</span><span> {</span></span>
<span><span>    val</span><span> startTime </span><span>=</span><span> System.</span><span>currentTimeMillis</span><span>()</span></span>
<span><span>    val</span><span> job </span><span>=</span><span> launch</span><span>(Dispatchers.Default) {</span></span>
<span><span>        var</span><span> nextPrintTime </span><span>=</span><span> startTime</span></span>
<span><span>        var</span><span> i </span><span>=</span><span> 0</span></span>
<span><span>        while</span><span> (i </span><span>&#x3C;</span><span> 5</span><span>) { </span><span>// computation loop, just wastes CPU</span></span>
<span><span>            // print a message twice a second</span></span>
<span><span>            if</span><span> (System.</span><span>currentTimeMillis</span><span>() </span><span>>=</span><span> nextPrintTime) {</span></span>
<span><span>                println</span><span>(</span><span>"job: I'm sleeping </span><span>${</span><span>i</span><span>++</span><span>}</span><span> ..."</span><span>)</span></span>
<span><span>                nextPrintTime </span><span>+=</span><span> 500L</span></span>
<span><span>            }</span></span>
<span><span>        }</span></span>
<span><span>    }</span></span>
<span><span>    delay</span><span>(</span><span>1300L</span><span>) </span><span>// delay a bit</span></span>
<span><span>    println</span><span>(</span><span>"main: I'm tired of waiting!"</span><span>)</span></span>
<span><span>    job.</span><span>cancelAndJoin</span><span>() </span><span>// cancels the job and waits for its completion</span></span>
<span><span>    println</span><span>(</span><span>"main: Now I can quit."</span><span>)</span></span>
<span><span>}</span></span></code></pre><p>即便已经调用了​<code class="">cancelAndJoin</code>​，协程仍然继续运行直到满足了退出​<code class="">while</code>​语句的条件，这是为什么？我们来把这段代码稍稍修改下：
</p><pre tabindex="0"><code><span><span>fun</span><span> main</span><span>() </span><span>=</span><span> runBlocking</span><span> {</span></span>
<span><span>    val</span><span> startTime </span><span>=</span><span> System.</span><span>currentTimeMillis</span><span>()</span></span>
<span><span>    val</span><span> job </span><span>=</span><span> launch</span><span>(Dispatchers.Default) {</span></span>
<span><span>        var</span><span> nextPrintTime </span><span>=</span><span> startTime</span></span>
<span><span>        var</span><span> i </span><span>=</span><span> 0</span></span>
<span><span>        while</span><span> (i </span><span>&#x3C;</span><span> 5</span><span>) {</span></span>
<span><span>            if</span><span> (System.</span><span>currentTimeMillis</span><span>() </span><span>>=</span><span> nextPrintTime) {</span></span>
<span><span>                println</span><span>(</span><span>"job: I'm sleeping </span><span>${</span><span>i</span><span>++</span><span>}</span><span> ..."</span><span>)</span></span>
<span></span>
<span><span>                // 为了清晰表示isActive的来源用了this，实际可以省略</span></span>
<span><span>                println</span><span>(</span><span>"当前CoroutineScope </span><span>${</span><span>this</span><span>.isActive</span><span>}</span><span>"</span><span>)</span></span>
<span><span>                nextPrintTime </span><span>+=</span><span> 500L</span></span>
<span><span>            }</span></span>
<span><span>        }</span></span>
<span><span>    }</span></span>
<span><span>    delay</span><span>(</span><span>1300L</span><span>)</span></span>
<span><span>    println</span><span>(</span><span>"main: I'm tired of waiting!"</span><span>)</span></span>
<span><span>    job.</span><span>cancelAndJoin</span><span>()</span></span>
<span><span>    println</span><span>(</span><span>"main: Now I can quit."</span><span>)</span></span>
<span><span>}</span></span>
<span></span>
<span><span>/* Result:</span></span>
<span><span>job: I'm sleeping 0 ...</span></span>
<span><span>当前CoroutineScope true</span></span>
<span><span>job: I'm sleeping 1 ...</span></span>
<span><span>当前CoroutineScope true</span></span>
<span><span>job: I'm sleeping 2 ...</span></span>
<span><span>当前CoroutineScope true</span></span>
<span><span>main: I'm tired of waiting!</span></span>
<span><span>job: I'm sleeping 3 ...</span></span>
<span><span>当前CoroutineScope false</span></span>
<span><span>job: I'm sleeping 4 ...</span></span>
<span><span>当前CoroutineScope false</span></span>
<span><span>main: Now I can quit.</span></span>
<span><span>*/</span></span></code></pre><p>虽然调用取消方法没能实际取消协程工作，但是可以发现在调用cancelAndJoin后Scope上有一个​<code class="">isActive</code>​值被置为了​<strong>false</strong>​。在Kotlin中，协程的取消实际上「协作式」的！也就是说取消协程需要协程内部的配合，比如，在这个例子里，加一个如果isActive变false就​<code class="">break</code>​的判断，就可以实现取消功能了。
</p><p>但是，为什么之前调用了​<code class="">delay</code>​的协程就可以直接取消？这里没有什么黑魔法，只是delay是来自官方​<code class="">kotlinx.coroutines</code>​的挂起函数，所有官方库提供的挂起函数都针对取消做了处理。当外部调用cancel时，delay会抛出一个<a href="https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/-cancellation-exception/">CancellationException</a>异常，导致协程结束。
</p><p>如果想在自己的代码里省去判断isActive的逻辑，可以调用<a href="https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines/ensure-active.html">ensureActive</a>函数，相当于：
</p><pre tabindex="0"><code><span><span>if</span><span> (</span><span>!</span><span>isActive) {</span></span>
<span><span>    throw</span><span> CancellationException</span><span>()</span></span>
<span><span>}</span></span></code></pre><h2 id="user-content-生命周期与协程" class="">生命周期与协程<a class="" tabindex="-1" href="#生命周期与协程">#</a></h2><p>Android中一些重要的类，如​<code class="">Activity</code>​，拥有由系统管理的生命周期，在不同的状态下系统会调用相关的生命周期方法，如初始化、暂停、销毁等，在Android中使用协程，需要注意生命周期问题，在适当的时机及时取消协程以避免内存泄漏。
</p><p>建议在Activity、Fragment中使用<a href="https://developer.android.com/topic/libraries/architecture/coroutines#lifecyclescope">LifecycleScope</a>，在ViewModel中使用<a href="https://developer.android.com/topic/libraries/architecture/coroutines#viewmodelscope">ViewModelScope</a>，避免使用GlobalScope。
</p><h2 id="user-content-异步流" class="">异步流<a class="" tabindex="-1" href="#异步流">#</a></h2><p>流最早源自函数式语言，Kotlin中的<a href="https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/-flow/">flow</a>在API上和其它语言的基本大同小异，所以这里只讲一下它和协程以及Android开发相关的地方。
</p><h3 id="user-content-flowon">flowOn<a class="" tabindex="-1" href="#flowon">#</a></h3><p>通过​<code class="">flowOn</code>​可以控制流所在线程：
</p><pre tabindex="0"><code><span><span>import</span><span> kotlinx.coroutines.</span><span>*</span></span>
<span><span>import</span><span> kotlinx.coroutines.flow.</span><span>*</span></span>
<span></span>
<span><span>fun</span><span> log</span><span>(msg: </span><span>String</span><span>) </span><span>=</span><span> println</span><span>(</span><span>"</span><span>${</span><span>Thread.</span><span>currentThread</span><span>().name</span><span>}</span><span> $msg</span><span>"</span><span>)</span></span>
<span></span>
<span><span>fun</span><span> main</span><span>() </span><span>=</span><span> runBlocking</span><span> {</span></span>
<span><span>    myFlow</span><span>()</span></span>
<span><span>        .</span><span>map</span><span> { it </span><span>*</span><span> 3</span><span> }</span></span>
<span><span>        .</span><span>collect</span><span> {</span></span>
<span><span>            log</span><span>(</span><span>"Collect </span><span>$it</span><span>"</span><span>)</span></span>
<span><span>        }</span></span>
<span></span>
<span><span>    myFlow</span><span>()</span></span>
<span><span>        .</span><span>map</span><span> { it </span><span>*</span><span> 3</span><span> }</span></span>
<span><span>        .</span><span>flowOn</span><span>(Dispatchers.IO)</span></span>
<span><span>        .</span><span>collect</span><span> { </span><span>// 注意collect不受flowOn影响</span></span>
<span><span>            log</span><span>(</span><span>"Collect </span><span>$it</span><span>"</span><span>)</span></span>
<span><span>        }</span></span>
<span><span>}</span></span>
<span></span>
<span><span>fun</span><span> myFlow</span><span>() </span><span>=</span><span> flow</span><span> {</span></span>
<span><span>    repeat</span><span>(</span><span>3</span><span>) {</span></span>
<span><span>        // 模拟一个IO操作</span></span>
<span><span>        delay</span><span>(</span><span>1000</span><span>)</span></span>
<span><span>        log</span><span>(</span><span>"Emit </span><span>$it</span><span>"</span><span>)</span></span>
<span><span>        emit</span><span>(it)</span></span>
<span><span>    }</span></span>
<span><span>}</span></span>
<span></span>
<span><span>/* Result:</span></span>
<span><span>main @coroutine#1 Emit 0</span></span>
<span><span>main @coroutine#1 Collect 0</span></span>
<span><span>main @coroutine#1 Emit 1</span></span>
<span><span>main @coroutine#1 Collect 3</span></span>
<span><span>main @coroutine#1 Emit 2</span></span>
<span><span>main @coroutine#1 Collect 6</span></span>
<span><span>DefaultDispatcher-worker-1 @coroutine#2 Emit 0</span></span>
<span><span>main @coroutine#1 Collect 0</span></span>
<span><span>DefaultDispatcher-worker-1 @coroutine#2 Emit 1</span></span>
<span><span>main @coroutine#1 Collect 3</span></span>
<span><span>DefaultDispatcher-worker-1 @coroutine#2 Emit 2</span></span>
<span><span>main @coroutine#1 Collect 6</span></span>
<span><span>*/</span></span></code></pre><h3 id="user-content-取消">取消<a class="" tabindex="-1" href="#取消">#</a></h3><p><code class="">collect</code>​是一个挂起函数，所有想取消一个流的收集工作和取消普通协程一样：
</p><pre tabindex="0"><code><span></span>
<span><span>fun</span><span> main</span><span>() </span><span>=</span><span> runBlocking</span><span> {</span></span>
<span><span>    val</span><span> job </span><span>=</span><span> launch</span><span> {</span></span>
<span><span>        try</span><span> {</span></span>
<span><span>            myFlow</span><span>().</span><span>collect</span><span> {</span></span>
<span><span>                log</span><span>(</span><span>"Collect </span><span>$it</span><span>"</span><span>)</span></span>
<span><span>            }</span></span>
<span><span>        // 如果不知道为什么可以catch，回看前面关于协程取消部分</span></span>
<span><span>        } </span><span>catch</span><span> (e: </span><span>CancellationException</span><span>) {</span></span>
<span><span>            log</span><span>(</span><span>"取消"</span><span>)</span></span>
<span><span>        } </span><span>finally</span><span> {</span></span>
<span><span>            // 还可以利用finally做些清理工作</span></span>
<span><span>            log</span><span>(</span><span>"清理资源"</span><span>)</span></span>
<span><span>        }</span></span>
<span><span>    }</span></span>
<span><span>    delay</span><span>(</span><span>2200</span><span>)</span></span>
<span><span>    job.</span><span>cancelAndJoin</span><span>()</span></span>
<span><span>    log</span><span>(</span><span>"Job done"</span><span>)</span></span>
<span><span>}</span></span>
<span></span>
<span><span>/* Result:</span></span>
<span><span>main @coroutine#2 Emit 0</span></span>
<span><span>main @coroutine#2 Collect 0</span></span>
<span><span>main @coroutine#2 Emit 1</span></span>
<span><span>main @coroutine#2 Collect 1</span></span>
<span><span>main @coroutine#2 取消</span></span>
<span><span>main @coroutine#2 清理资源</span></span>
<span><span>main @coroutine#1 Job done</span></span>
<span><span>*/</span></span></code></pre><h3 id="user-content-stateflow和sharedflow">StateFlow和SharedFlow<a class="" tabindex="-1" href="#stateflow和sharedflow">#</a></h3><p>在Android关于状态流的文档中提到，Flow是「冷流」，而StateFlow和SharedFlow是「热流」，区别体现在哪里？
</p><p>冷流：
</p><pre tabindex="0"><code><span><span>fun</span><span> main</span><span>() </span><span>=</span><span> runBlocking</span><span> {</span></span>
<span><span>    val</span><span> foo </span><span>=</span><span> myFlow</span><span>()</span></span>
<span><span>        .</span><span>map</span><span> {</span></span>
<span><span>            println</span><span>(</span><span>"Map </span><span>$it</span><span>"</span><span>)</span></span>
<span><span>            it </span><span>*</span><span> 3</span></span>
<span><span>        }</span></span>
<span><span>        .</span><span>filter</span><span> {</span></span>
<span><span>            println</span><span>(</span><span>"Filter </span><span>$it</span><span>"</span><span>)</span></span>
<span><span>            it </span><span>></span><span> 5</span></span>
<span><span>        }</span></span>
<span><span>    println</span><span>(</span><span>"没有收集，流的中间过程都没有执行"</span><span>)</span></span>
<span><span>    println</span><span>(</span><span>"收集"</span><span>)</span></span>
<span><span>    foo.</span><span>collect</span><span> { </span><span>println</span><span>(it) }</span></span>
<span></span>
<span><span>    delay</span><span>(</span><span>2000</span><span>)</span></span>
<span></span>
<span><span>    // 整个流会再跑一遍</span></span>
<span><span>    println</span><span>(</span><span>"再次收集"</span><span>)</span></span>
<span><span>    foo.</span><span>collect</span><span> { </span><span>println</span><span>(it) }</span></span>
<span><span>}</span></span>
<span></span>
<span><span>fun</span><span> myFlow</span><span>() </span><span>=</span><span> flow</span><span> {</span></span>
<span><span>    repeat</span><span>(</span><span>3</span><span>) {</span></span>
<span><span>        println</span><span>(</span><span>"Emit </span><span>$it</span><span>"</span><span>)</span></span>
<span><span>        emit</span><span>(it)</span></span>
<span><span>    }</span></span>
<span><span>}</span></span>
<span></span>
<span><span>/* Result:</span></span>
<span><span>没有收集，流的中间过程都没有执行</span></span>
<span><span>收集</span></span>
<span><span>Emit 0</span></span>
<span><span>Map 0</span></span>
<span><span>Filter 0</span></span>
<span><span>Emit 1</span></span>
<span><span>Map 1</span></span>
<span><span>Filter 3</span></span>
<span><span>Emit 2</span></span>
<span><span>Map 2</span></span>
<span><span>Filter 6</span></span>
<span><span>6</span></span>
<span><span>再次收集</span></span>
<span><span>Emit 0</span></span>
<span><span>Map 0</span></span>
<span><span>Filter 0</span></span>
<span><span>Emit 1</span></span>
<span><span>Map 1</span></span>
<span><span>Filter 3</span></span>
<span><span>Emit 2</span></span>
<span><span>Map 2</span></span>
<span><span>Filter 6</span></span>
<span><span>6</span></span>
<span><span>*/</span></span></code></pre><p>热流：
</p><pre tabindex="0"><code><span><span>fun</span><span> main</span><span>() </span><span>=</span><span> runBlocking</span><span> {</span></span>
<span><span>    val</span><span> sharedFlow </span><span>=</span><span> MutableSharedFlow</span><span>&#x3C;</span><span>Int</span><span>>()</span></span>
<span><span>    val</span><span> job </span><span>=</span><span> launch</span><span> {</span></span>
<span><span>        launch</span><span> {</span></span>
<span><span>            var</span><span> i </span><span>=</span><span> 0</span></span>
<span><span>            while</span><span>(</span><span>true</span><span>) {</span></span>
<span><span>                println</span><span>(</span><span>"Emit </span><span>$i</span><span>"</span><span>)</span></span>
<span><span>                sharedFlow.</span><span>emit</span><span>(i)</span></span>
<span><span>                i</span><span>++</span></span>
<span><span>                delay</span><span>(</span><span>1000</span><span>)</span></span>
<span><span>            }</span></span>
<span><span>        }</span></span>
<span></span>
<span><span>        launch</span><span> {</span></span>
<span><span>            sharedFlow.</span><span>collect</span><span> { </span><span>println</span><span>(</span><span>"Collector#1 </span><span>$it</span><span>"</span><span>) }</span></span>
<span><span>        }</span></span>
<span></span>
<span><span>        delay</span><span>(</span><span>3000</span><span>)</span></span>
<span><span>        launch</span><span> {</span></span>
<span><span>            sharedFlow.</span><span>collect</span><span> { </span><span>println</span><span>(</span><span>"Collector#2 </span><span>$it</span><span>"</span><span>)}</span></span>
<span><span>        }</span></span>
<span><span>    }</span></span>
<span><span>    delay</span><span>(</span><span>5000</span><span>)</span></span>
<span><span>    job.</span><span>cancelAndJoin</span><span>()</span></span>
<span><span>    println</span><span>(</span><span>"Done"</span><span>)</span></span>
<span><span>}</span></span>
<span></span>
<span><span>/* Result:</span></span>
<span><span>Emit 0</span></span>
<span><span>Emit 1</span></span>
<span><span>Collector#1 1</span></span>
<span><span>Emit 2</span></span>
<span><span>Collector#1 2</span></span>
<span><span>Emit 3</span></span>
<span><span>Collector#1 3</span></span>
<span><span>Collector#2 3</span></span>
<span><span>Emit 4</span></span>
<span><span>Collector#1 4</span></span>
<span><span>Collector#2 4</span></span>
<span><span>Done</span></span>
<span><span>*/</span></span></code></pre><p>对比输出可以发现，冷流每次收集都能得到相同的数据，只有在收集时流才会开始执行，每次收集都重头重新执行了一遍；而热流即使没有收集者/观察者也会直接推送数据，收集时不能保证一定得到全部数据，第二个收集者延迟了3秒后，就没能得到前几次emit的数据。
</p><p><code class="">StateFlow</code>​是一个继承自​<code class="">SharedFlow</code>​的热流，不同的是，收集器总是得到它的最新值，发射数据时会和上一个数据做比较，只有数据不同时才会发射，在创建StateFlow时也必须提供一个初始值。
</p><p>这两个热流在Android开发中具体要怎么用？
</p><h3 id="user-content-android与热流">Android与热流<a class="" tabindex="-1" href="#android与热流">#</a></h3><p>以一个连接蓝牙设备的流程做例子，以下是一个极度简化版的代码：
</p><pre tabindex="0"><code><span><span>class</span><span> MyViewModel</span><span> : </span><span>ViewModel</span><span>() {</span></span>
<span><span>    // StateFlow和SharedFlow有各自的可变版本MutableStateFlow和MutableSharedFlow</span></span>
<span><span>    private</span><span> val</span><span> _isConnected </span><span>=</span><span> MutableStateFlow</span><span>(</span><span>false</span><span>)</span></span>
<span><span>    // 通过asStateFlow将其转为不可变版本并暴露出去</span></span>
<span><span>    val</span><span> isConnected </span><span>=</span><span> _isConnected.</span><span>asStateFlow</span><span>()</span></span>
<span></span>
<span><span>    fun</span><span> connect</span><span>() {</span></span>
<span><span>        // 摸拟真实场景连接</span></span>
<span><span>        viewModelScope.</span><span>launch</span><span> {</span></span>
<span><span>            delay</span><span>(</span><span>1000</span><span>)</span></span>
<span><span>            _isConnected.</span><span>emit</span><span>(</span><span>true</span><span>)</span></span>
<span><span>        }</span></span>
<span><span>    }</span></span>
<span><span>}</span></span>
<span></span>
<span><span>..</span><span>.</span></span>
<span></span>
<span><span>// 通过collectAsState将最新值收集为State，State.value变化将引起Compose重组</span></span>
<span><span>val</span><span> isConnected </span><span>by</span><span> viewModel.isConnected.</span><span>collectAsState</span><span>()</span></span>
<span></span>
<span><span>Text</span><span>(text </span><span>=</span><span> if</span><span> (isConnected) </span><span>"已连接"</span><span> else</span><span> "未连接"</span><span>)</span></span>
<span><span>Button</span><span>(onClick </span><span>=</span><span> { viewModel.</span><span>connect</span><span>() }) {</span></span>
<span><span>    Text</span><span>(text </span><span>=</span><span> "连接"</span><span>)</span></span>
<span><span>}</span></span>
<span></span>
<span><span>..</span><span>.</span></span></code></pre><p>这里通过StateFlow表现了一个「是否连接」的状态，并通过状态变化更新了UI。如果需要表现一个开始连接和成功连接的事件，弹出提示要怎么做？
</p><pre tabindex="0"><code><span><span>// 仅供说明，实际代码不应该这么写</span></span>
<span><span>enum</span><span> class</span><span> BluetoothState</span><span> {</span></span>
<span><span>    UNCONNECTED, CONNECTING, CONNECTED</span></span>
<span><span>}</span></span>
<span></span>
<span><span>class</span><span> MyViewModel</span><span> : </span><span>ViewModel</span><span>() {</span></span>
<span><span>    private</span><span> val</span><span> _state </span><span>=</span><span> MutableStateFlow</span><span>(BluetoothState.UNCONNECTED)</span></span>
<span><span>    val</span><span> state </span><span>=</span><span> _state.</span><span>asStateFlow</span><span>()</span></span>
<span></span>
<span><span>    fun</span><span> connect</span><span>() {</span></span>
<span><span>        viewModelScope.</span><span>launch</span><span> {</span></span>
<span><span>            _state.</span><span>emit</span><span>(BluetoothState.CONNECTING)</span></span>
<span><span>            delay</span><span>(</span><span>2000</span><span>)</span></span>
<span><span>            _state.</span><span>emit</span><span>(BluetoothState.CONNECTED)</span></span>
<span><span>        }</span></span>
<span><span>    }</span></span>
<span><span>}</span></span>
<span></span>
<span><span>val</span><span> scope </span><span>=</span><span> rememberCoroutineScope</span><span>()</span></span>
<span><span>val</span><span> snackbarHostState </span><span>=</span><span> remember</span><span> { </span><span>SnackbarHostState</span><span>() }</span></span>
<span><span>val</span><span> state </span><span>by</span><span> viewModel.state.</span><span>collectAsState</span><span>()</span></span>
<span></span>
<span><span>LaunchedEffect</span><span>(state) {</span></span>
<span><span>    when</span><span> (state) {</span></span>
<span><span>        BluetoothState.CONNECTING </span><span>-></span><span> scope.</span><span>launch</span><span> { snackbarHostState.</span><span>showSnackbar</span><span>(</span><span>"连接中"</span><span>) }</span></span>
<span><span>        BluetoothState.CONNECTED </span><span>-></span><span> scope.</span><span>launch</span><span> { snackbarHostState.</span><span>showSnackbar</span><span>(</span><span>"已连接"</span><span>) }</span></span>
<span><span>        else</span><span> -></span><span> {}</span></span>
<span><span>    }</span></span>
<span><span>}</span></span>
<span></span>
<span><span>Scaffold</span><span>(snackbarHost </span><span>=</span><span> { </span><span>SnackbarHost</span><span>(hostState </span><span>=</span><span> snackbarHostState)}) {</span></span>
<span><span>    Column</span><span>(modifier </span><span>=</span><span> Modifier.</span><span>padding</span><span>(it)) {</span></span>
<span><span>        Button</span><span>(onClick </span><span>=</span><span> { viewModel.</span><span>connect</span><span>() }) {</span></span>
<span><span>            Text</span><span>(text </span><span>=</span><span> "连接"</span><span>)</span></span>
<span><span>        }</span></span>
<span><span>    }</span></span>
<span><span>}</span></span></code></pre><p>代码看上去没有大问题，但是如果启动应用，点击连接，等到两次snackbar提示结束后，旋转屏幕，将会看到snackbar再次弹出显示「已连接」。屏幕旋转会引起Compose重组，热流数据的消费者也重建了，StateFlow向消费者提供了最新的数据，如果需要表示一个UI的状态，这是期望行为；但对于数据只需要消费一次的场景，或者说表现事件的场景，这被叫做「粘性事件」，是需要避免的，StateFlow就不适用了。
</p><p>StateFlow的构造函数需要一个初始值，看看SharedFlow的构造函数是什么样的：
</p><pre tabindex="0"><code><span><span>public</span><span> fun</span><span> &#x3C;</span><span>T</span><span>> </span><span>MutableSharedFlow</span><span>(</span></span>
<span><span>    // 重播数量，StateFlow是1，新的消费者收集数据时，StateFlow会重播一次最近emit的值</span></span>
<span><span>    replay: </span><span>Int</span><span> =</span><span> 0</span><span>,</span></span>
<span><span>    // 额外缓冲容量，缓存还没被消费的数据</span></span>
<span><span>    extraBufferCapacity: </span><span>Int</span><span> =</span><span> 0</span><span>,</span></span>
<span><span>    // 缓冲区溢出时的处理策略，默认挂起等待消费者订阅</span></span>
<span><span>    onBufferOverflow: </span><span>BufferOverflow</span><span> =</span><span> BufferOverflow.SUSPEND</span></span>
<span><span>): </span><span>MutableSharedFlow</span><span>&#x3C;</span><span>T</span><span>> {</span></span></code></pre><p>可以看出，SharedFlow具有比StateFlow更高的可配置性，可以用它来避免「粘性事件」：
</p><pre tabindex="0"><code><span><span>class</span><span> MyViewModel</span><span> : </span><span>ViewModel</span><span>() {</span></span>
<span><span>    private</span><span> val</span><span> _event </span><span>=</span><span> MutableSharedFlow</span><span>&#x3C;</span><span>BluetoothState</span><span>>()</span></span>
<span><span>    val</span><span> event </span><span>=</span><span> _event.</span><span>asSharedFlow</span><span>()</span></span>
<span></span>
<span><span>    fun</span><span> connect</span><span>() {</span></span>
<span><span>        viewModelScope.</span><span>launch</span><span> {</span></span>
<span><span>            _event.</span><span>emit</span><span>(BluetoothState.CONNECTING)</span></span>
<span><span>            delay</span><span>(</span><span>2000</span><span>)</span></span>
<span><span>            _event.</span><span>emit</span><span>(BluetoothState.CONNECTED)</span></span>
<span><span>        }</span></span>
<span><span>    }</span></span>
<span><span>}</span></span>
<span></span>
<span><span>LaunchedEffect</span><span>(Unit) {</span></span>
<span><span>    viewModel.event.</span><span>collect</span><span> {</span></span>
<span><span>        when</span><span> (it) {</span></span>
<span><span>            BluetoothState.CONNECTING </span><span>-></span><span> scope.</span><span>launch</span><span> { snackbarHostState.</span><span>showSnackbar</span><span>(</span><span>"连接中"</span><span>) }</span></span>
<span><span>            BluetoothState.CONNECTED </span><span>-></span><span> scope.</span><span>launch</span><span> { snackbarHostState.</span><span>showSnackbar</span><span>(</span><span>"已连接"</span><span>) }</span></span>
<span><span>            else</span><span> -></span><span> {}</span></span>
<span><span>        }</span></span>
<span><span>    }</span></span>
<span><span>}</span></span></code></pre><p>由于​<code class="">replay</code>​默认值是0，旋转屏幕重建订阅的消费者后，最近一次的事件值不会向这个新的订阅者重播了。
</p><h4 id="user-content-冷流转热流">冷流转热流<a class="" tabindex="-1" href="#冷流转热流">#</a></h4><p>在Flow上调用<a href="https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/share-in.html">shareIn</a>可以将其转化为SharedFlow，<a href="https://kotlinlang.org/api/kotlinx.coroutines/kotlinx-coroutines-core/kotlinx.coroutines.flow/state-in.html">stateIn</a>可以将其转化为StateFlow：
</p><pre tabindex="0"><code><span><span>fun</span><span> &#x3C;</span><span>T</span><span>> </span><span>Flow</span><span>&#x3C;</span><span>T</span><span>></span><span>.</span><span>shareIn</span><span>(</span></span>
<span><span>    // Flow生产者所在的协程Scope，</span></span>
<span><span>    scope: </span><span>CoroutineScope</span><span>,</span></span>
<span><span>    // 开始生产数据的策略</span></span>
<span><span>    started: </span><span>SharingStarted</span><span>,</span></span>
<span><span>    replay: </span><span>Int</span><span> =</span><span> 0</span></span>
<span><span>): </span><span>SharedFlow</span><span>&#x3C;</span><span>T</span><span>></span></span>
<span></span>
<span><span>fun</span><span> &#x3C;</span><span>T</span><span>> </span><span>Flow</span><span>&#x3C;</span><span>T</span><span>></span><span>.</span><span>stateIn</span><span>(scope: </span><span>CoroutineScope</span><span>, started: </span><span>SharingStarted</span><span>, initialValue: </span><span>T</span><span>): </span><span>StateFlow</span><span>&#x3C;</span><span>T</span><span>></span></span></code></pre>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>给tauri做条件编译实现真全平台</title>
          <link>https://elliot00.com/posts/tauri-conditional-compile</link>
          <description>本文讲述了作者使用 unplugin-preprocessor-directives 插件解决了 Tauri 应用在特定硬件上无法启动以及跨平台打包问题。</description>
          <pubDate>Mon, 15 Apr 2024 03:03:00 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <p>为了实现用Org文档来写公众号，我用Rust的tauri框架写了一个<a href="https://github.com/Eliot00/mp-org">Webview应用</a>。本来这个简单的功能可以直接用网页来实现，但是当时我手头上没有服务器，正好也想试试tauri，于是就做成了使用webview的桌面应用。但是最近有用户发现在Apple M3 Max芯片上，这个本应该跨平台的webview应用无法启动。这种特定硬件上才有的问题实在是难以调试，转念一想，原本这个程序大部分代码就在前端，是不是可以打包出一份纯网页应用，这样就可以让无法使用桌面版的用户用网页版了。
</p><p>tauri有一个​<code class="">command</code>​机制用于前端调用Rust，我的应用里有两处使用了这个机制去读取用户本地的文件，如果单独将现有的前端代码打包成网页应用发布必然会在运行时报错。那么有没有可能在面向浏览器build的时候，将这部分代码替换掉？首先想到的是类似Rust的​<strong>条件编译</strong>​机制：
</p><pre tabindex="0"><code><span><span>// The function is only included in the build when compiling for macOS</span></span>
<span><span>#[cfg(target_os </span><span>=</span><span> "</span><span>macos</span><span>"</span><span>)]</span></span>
<span><span>fn</span><span> foo</span><span>() {</span></span>
<span><span>  // 调用mac平台特定API</span></span>
<span><span>}</span></span>
<span></span>
<span><span>#[cfg(target_os </span><span>=</span><span> "</span><span>windows</span><span>"</span><span>)]</span></span>
<span><span>fn</span><span> foo</span><span>() {</span></span>
<span><span>  // 调用windows特定API</span></span>
<span><span>}</span></span></code></pre><p>看上去有两个同名函数似乎编译器应该报错，实际上Rust编译器会在编译时根据cfg标记来决定编译哪一个函数，包括第三方包也可以通过标记决定是否引入，在面向Windows平台编译的产物里就不会包含和macOS有关的功能。
</p><p>当然，JavaScript并不是编译型语言，但是现代的前端框架都会用到像​<code class="">webpack</code>​、​<code class="">vite</code>​的这样的构建工具，如tauri就默认使用了vite，那么有没有办法通过某个环境变量来控制vite，在tauri打包时包含调用Rust的代码，而在面向浏览器打包时去除它们？正当我在考虑是否要花点时间造轮子给vite写个插件的时候，意外发现已经有人想到过并且实现了<a href="https://github.com/KeJunMao/unplugin-preprocessor-directives">这个插件</a>。
</p><p>首先在原有的tauri项目里安装并配置：
</p><pre tabindex="0"><code><span><span>// vite.config.js</span></span>
<span><span>// pnpm add unplugin-preprocessor-directives -D</span></span>
<span></span>
<span><span>import</span><span> PreprocessorDirectives </span><span>from</span><span> '</span><span>unplugin-preprocessor-directives/vite</span><span>'</span></span>
<span></span>
<span><span>export</span><span> default</span><span> defineConfig</span><span>({</span></span>
<span><span>  plugins</span><span>:</span><span> [</span></span>
<span><span>    PreprocessorDirectives</span><span>({ </span><span>/* options */</span><span> })</span><span>,</span></span>
<span><span>  ]</span><span>,</span></span>
<span><span>})</span></span></code></pre><p>接着修改前端代码：
</p><pre tabindex="0"><code><span><span>// 插件根据环境变量判断是否需要打包这段代码</span></span>
<span><span>// #if TARGET_PLATFORM == 'desktop'</span></span>
<span><span>import</span><span> { invoke } </span><span>from</span><span> "</span><span>@tauri-apps/api/tauri</span><span>"</span><span>;</span></span>
<span><span>// #endif</span></span>
<span></span>
<span><span>export</span><span> function</span><span> setThemeById</span><span>(</span><span>id</span><span>:</span><span> string</span><span>,</span><span> callback</span><span>:</span><span> (</span><span>theme</span><span>:</span><span> string</span><span>)</span><span> =></span><span> void</span><span>)</span><span> {</span></span>
<span><span>  // #if TARGET_PLATFORM == 'desktop'</span></span>
<span><span>  invoke</span><span>&#x3C;</span><span>string</span><span>>(</span><span>"</span><span>get_theme_content</span><span>"</span><span>,</span><span> { themeId</span><span>:</span><span> id })</span><span>.</span><span>then</span><span>(callback);</span></span>
<span><span>  // #endif</span></span>
<span><span>}</span></span></code></pre><p>这个插件还支持​<code class="">else</code>​和​<code class="">elif</code>​标记，具体使用很来还是很灵活的，这里就不过多演示了。
</p><p>修改下​<code class="">package.json</code>​，为打包命令加上环境变量，建议安装​<code class="">cross-env</code>​，避免在Windows下出错：
</p><pre tabindex="0"><code><span><span>{</span></span>
<span><span>  "scripts"</span><span>:</span><span> {</span></span>
<span><span>    "dev"</span><span>:</span><span> "</span><span>cross-env TARGET_PLATFORM=desktop vite</span><span>"</span><span>,</span></span>
<span><span>    "build"</span><span>:</span><span> "</span><span>cross-env TARGET_PLATFORM=desktop tsc &#x26;&#x26; vite build</span><span>"</span><span>,</span></span>
<span><span>    ...</span></span>
<span><span>  }</span></span>
<span><span>}</span></span></code></pre><p>由于tauri应用开发预览和打包实际用的命令是​<code class="">pnpm tauri dev</code>​和​<code class="">pnpm tauri build</code>​，所以可以把​<code class="">pnpm dev</code>​和​<code class="">pnpm build</code>​留给打包Web应用，修改​<code class="">src-tauri</code>​内的​<code class="">tauri.config.json</code>​：
</p><pre tabindex="0"><code><span><span>{</span></span>
<span><span>  "build"</span><span>:</span><span> {</span></span>
<span><span>    "beforeDevCommand"</span><span>:</span><span> "</span><span>pnpm dev:desktop</span><span>"</span><span>,</span></span>
<span><span>    "beforeBuildCommand"</span><span>:</span><span> "</span><span>pnpm build:desktop</span><span>"</span><span>,</span></span>
<span><span>    "devPath"</span><span>:</span><span> "</span><span>http://localhost:1420</span><span>"</span><span>,</span></span>
<span><span>    "distDir"</span><span>:</span><span> "</span><span>../dist</span><span>"</span></span>
<span><span>  }</span><span>,</span></span>
<span><span>  ...</span></span>
<span><span>}</span></span></code></pre><p>再更改​<code class="">package.json</code>​：
</p><pre tabindex="0"><code><span><span>{</span></span>
<span><span>  "scripts"</span><span>:</span><span> {</span></span>
<span><span>    "dev:desktop"</span><span>:</span><span> "</span><span>cross-env TARGET_PLATFORM=desktop vite</span><span>"</span><span>,</span></span>
<span><span>    "dev"</span><span>:</span><span> "</span><span>cross-env TARGET_PLATFORM=browser vite</span><span>"</span><span>,</span></span>
<span><span>    "build:desktop"</span><span>:</span><span> "</span><span>cross-env TARGET_PLATFORM=desktop tsc &#x26;&#x26; vite build</span><span>"</span><span>,</span></span>
<span><span>    "build"</span><span>:</span><span> "</span><span>cross-env TARGET_PLATFORM=browser tsc &#x26;&#x26; vite build</span><span>"</span><span>,</span></span>
<span><span>    ...</span></span>
<span><span>  }</span></span>
<span><span>}</span></span></code></pre><p>启动桌面端，功能没变，Web端也运行正常，搞定！
</p><h2 id="user-content-后记" class="">后记<a class="" tabindex="-1" href="#后记">#</a></h2><p>才发现Vercel的免费账户支持200个免费项目，干脆把网页端部署到Vercel了，地址：<a href="https://mp-org.vercel.app/">https://mp-org.vercel.app/</a></p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>软件开发之道——聊聊UNIX哲学</title>
          <link>https://elliot00.com/posts/talk-about-unix-philosophy</link>
          <description>本文从多个角度探讨了UNIX哲学在现代软件开发中的适用性。首先，文章分析了三条UNIX哲学的核心原则，并结合实例阐述了它们的具体应用。其次，文章探讨了在现代软件开发中，如何理解和应用这些原则。最后，文章分析了时代因素对UNIX哲学适用性的影响，并提出了一些建议。</description>
          <pubDate>Fri, 22 Mar 2024 14:05:55 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <p>汉语里有个成语「吃一堑，长一智」，在日积月累的实践中，工程师们总会从过往的经验中得到一些知识，来避免可能的错误，提高工作的效率。软件工程领域也不例外，还没开始工作的学生们也常会听到诸如模块化、重构、解耦合等等概念。在上个世纪六、七十年代，UNIX黑客们就曾总结出一份UNIX软件开发的指南，并命名为「UNIX哲学」。但是几十年过去了，计算机的发展日新月异，这些原则仍然适用吗？
</p><h2 id="user-content-概览" class="">概览<a class="" tabindex="-1" href="#概览">#</a></h2><p>按英文wiki的说法，1994年Peter H. Salus曾总结了三条UNIX程序开发原则：
</p><ol><li>编写只做一件事并做好的程序
</li><li>编写协同工作的程序
</li><li>编写能处理文本流的程序，因为文本流是通用的接口
</li></ol><p>这三条原则单独拿出来看可能会让人摸不着头脑，尤其是第三条。怎么去理解这三条原则？
</p><h2 id="user-content-模块化与组合" class="">模块化与组合<a class="" tabindex="-1" href="#模块化与组合">#</a></h2><p>我想凡是学习过现代的高级语言的程序员都知道，如果有某个数字在一段代码中多次反复出现，那么可以把它提取成一个 <strong>变量</strong> ；如果有一个过程反复出现，那么可以将其提炼成一个 <strong>函数</strong> 。这样做有什么好处？最直观的一点是，如果需要修改一个值或过程，只用修改一处而不是修改所有用到的地方。大部分高级语言都支持将代码组织成多个小的模块，如果一个模块出了问题，可以在一个小范围内修改，就像有一节水管漏水，只需要更换漏水的那一节而不是整个供水系统。
</p><p><img alt="pipeline" src="https://r2.elliot00.com/legacy/winter-681175_1280.jpg" width="1280" height="853"></p><p>如果把这种模块化的原则从代码层面推广到软件层面呢？看一个简单示例：
</p><pre tabindex="0"><code><span><span>$</span><span> ls -l </span><span>|</span><span> grep</span><span> “^-“</span><span> |</span><span> wc</span><span> -l</span></span></code></pre><p>这段shell命令可以用来统计当前文件夹下的文件数量。其中 <code class="">ls -l</code> 用于列出当前目录下的文件和子目录， <code class="">grep "^-"</code> 从文本中查找 <code class="">-</code> 开头的行（ls的输出格式中这代表文件）， <code class="">wc -l</code> 则可以统计文本的行数， <code class="">|</code> 用于将上一个程序的输入传递给下一个程序做输入。这是个简单的例子，但却完美诠释了UNIX哲学。这些程序都很小巧并且专注于做好一件事，单独使用它们都无法满足我的需求------统计文件数量，但是它们都被设计为可以与其它程序协作------通过文本流沟通，我不需要再开发一个新的程序，只是把已有的这三个程序组合起来，就做到了我想做的事。
</p><h3 id="user-content-形式的美与现实的困境">形式的美与现实的困境<a class="" tabindex="-1" href="#形式的美与现实的困境">#</a></h3><p>如果这个世界上的每个程序都是小巧玲珑的，能够相互配合，像组织良好的代码一样，没有冗余，听上去似乎很优雅。如果说代码复用节约了程序员的时间，软件层面的复用不是更加节省了程序员群体的工作量吗？
</p><p>最近我做了个将Org文档转换到微信公众号文章格式的<a href="https://github.com/Eliot00/mp-org">小工具</a>，它的功能很简单，只不过在解析Org文档后将其转换成HTML并且内联CSS样式，在草草地完成了核心功能后我把它上传到了Github，没过几天我收到了一位用户的反馈，其中一个问题是：本地Org文档的图片都是相对本地的文件路径，能否支持自动上传OSS。
</p><p>老实说一开始我没想过会有人使用我的工具并给我反馈，收到反馈后我开始抽时间解决这些问题，这并不困难，但是这个过程引起了我的一些反思。
</p><h4 id="user-content-如何定义一件事">如何定义「一件事」？<a class="" tabindex="-1" href="#如何定义一件事">#</a></h4><p>怎样算是「一件事」？这并没有一个标准，开发者理解的一件事和用户的理解很可能是不同的。
</p><p>除了这个文档转换的小工具，近期我投入最多精力的应用是一个输入法程序，仅以中文输入法为例，它似乎应该专注于将用户的输入转换到中文字符这一件事上。我发现市面上的一些输入法有一个功能，即按下引号键（可以引申到其它的左右区配的标点符号），可以一次性输入中文的左右引号，并将光标保持在引号中间。在引号上还有个特别的功能是，由于中文引号区分左右而在英文键盘上只有一个引号键，当用户奇数次按这个键时，输入左引号；偶数次按这个键，输入右引号。这个功能似乎很方便，并且都属于转换用户输入到中文字符这一个功能。
</p><p>但是不巧，帮助用户更好地编辑文本也是文本编辑器的活，很多编辑器也支持按左符号键自动输入右符号，有时候输入法的功能会和编辑器起冲突！
</p><blockquote><p>如果你去搜索输入法的英文，会发现它叫「Input Method Editor」。
</p></blockquote><p>回到如何解决用户提出的问题的话题上来，我写这个工具最初的目的只是希望它处理下Org文档，可以让我直接复制到公众号平台发布，上传图片到OSS的工具有很多，由于我插入图片比较少，所以我在需要插入图片的时候直接在shell跑一个命令，将图片上传再把OSS链接贴回Org文档内。
</p><p>既然已经有很多用于上传本地图片到OSS了，那么这显然不应该是我的小工具该做的了，于是我提供了一个配置项，用户可以指定一个外部程序，当我的工具在遍历Org的AST时，将本地图片链接通过 <strong>stdio</strong> 传递给外部程序，并用其返回的文本替换已有的本地图片路径。完成这个功能后我感到很完美，我遵循了先贤的教诲，我的程序没有做多余的事，并且可以文本流的方式与别的程序配合使用。但在用户看来，这可能很不合理，我只想下载一个应用，完成我要做的事情，怎么我下载的软件还要借助别的程序一起完成这个功能呢？
</p><h4 id="user-content-用户都是专家">用户都是专家？<a class="" tabindex="-1" href="#用户都是专家">#</a></h4><p>不久前我在v2ex看了篇帖子，大致内容是有个在一个Github仓库里提了个issue，愤愤地表示「为什么不能给我下个下载链接让我下载后双击打开直接用」。当然我并不认为开发者有义务做什么，只是这使用联想到一些人常把开源软件和难用联系到一起，在我看来一个可能的原因是，开发者常常认为用户也是和自己一样的专家。
</p><p>仍然以我的小工具为例，如果有一个非程序员用户来使用它，他得先了解 <code class="">stdio</code> 、 <code class="">shell</code> 等各种概念，但可能他只是期望打打字，动动鼠标点击几下就完成工作。这种工具对他来说实在太糟糕了。计算机的广泛应用是在图形界面操作系统兴起之后，在今天大部分计算机用户的眼里，软件就是双击鼠标可以打开的一个界面，例如一个文件查看器，双击打开就能看一个目录下有多少文件，文件是什么类型，再双击又可以浏览文件内容，至于shell、管道是什么这些东西并没有人关心。
</p><p>术业有专攻，既便是计算机专业的用户，也顶多只是自己所研究领域的专家，就在我写这一段文字的前一天，我看到一位使用neovim的程序员发帖说明了自己为什么从neovim的native LSP（需要安装多个插件配合）换回到CoC（一个相对大而全的插件）。
</p><h3 id="user-content-时代因素">时代因素<a class="" tabindex="-1" href="#时代因素">#</a></h3><p>UNIX哲学是前人总结的经验之谈，计算机行业是个新兴行业同时也是飞速发展的行业，过去的经验有没有一些是由当时的客观条件促成的呢？我个人的看法是，至少有两点影响了UNIX哲学：
</p><ol><li>过去的硬件相对现在性能差，价格高
</li><li>过去使用计算机的人大都是专家
</li></ol><p>早期的计算机是一种昂贵的设备，只有一些专业人士可以接触，最早的硬盘只有MB级别的容量，对比之下今天的一些手机应用安装包就 <strong>超过1G</strong> ！重复造轮子在当时看来不仅不优雅，还是不可原谅的浪费行为。同时当时计算机还没有「飞入寻常百姓家」，在技术人员眼里，通过管道组合命令实现需求只是家常便饭，而且相比使用一个融合了太多功能彼此之间高度耦合的庞然大物要自由灵活得多。
</p><h3 id="user-content-好的案例">好的案例<a class="" tabindex="-1" href="#好的案例">#</a></h3><p>如果要我说一个我认为的好的软件的案例，我会说是 <strong>Vim</strong> 和 <strong>Emacs</strong> 。可能有人会反驳，尤其是Emacs，它不是号称什么都能做吗？看网页、炒股、煮咖啡，甚至有人戏称它是一个伪装成编辑器的操作系统。是的，这看上去破坏了前面说的第一条原则，一个软件只应做好一件事，而Emacs却能做无数件事！Vim和Emacs都支持通过脚本来拓展，事实上它们的本体都很好地专注在文本编辑这件事上（尤其是Vim），同时它们都内嵌了一个脚本语言的解释器，很多强大的功能是通过插件来实现的，这也可以看做是核心程序和插件程序的组合。
</p><p>在Vim和Emacs里，应用本体变成了一个库，提供接口给插件调用，核心部分和一些功能的具体实现分离开来，这其实也体现了UNIX哲学的另一个原则------分离原则。Vim诞生于1991年，Emacs则是1975年，而当微软发布在2016提出了LSP（Language Server Protocol）之后，这两个古老的软件都通过社区插件支持了这个流行的协议，而它们的本体却无需做什么大改动。
</p><p><img alt="toy blocks" src="https://r2.elliot00.com/legacy/child-1864718_1280.jpg" width="1280" height="853"></p><p>再说回「不是所有用户都是专家」的问题，的确，即使在程序员群体中，Vim和Emacs也常被抱怨门槛过高，要花很多时间去配置，要学习一门脚本语言，为什么不直接用开箱即用的IDE？对这个问题，我只能说这不是开发者的错，也不是使用者的问题。好像有点「非战之罪」的味道，用户群体的需求不同，知识背景也不同，很难找出一个好的平衡，依据过往的经验，只能说，尽量保持应用的小和精，为用户提供最大的灵活性，同时如果有精力可以提供一个开箱即用的「整合包」，类似<a href="https://spacevim.org">SpaceVim</a>和<a href="https://www.spacemacs.org/">Spacemacs</a>做的那样。
</p><h2 id="user-content-文本" class="">文本<a class="" tabindex="-1" href="#文本">#</a></h2><p>很多商业软件提供配置功能，在一个图形界面上点击、选择就可以决定启用或关闭哪些功能，看上去给了用户很大的自由度，然而用户根本无法掌控自己的配置。如果用户说：「我现在有两台电脑，我要在它们之间同步配置」，这些封闭的应用会说：「来注册账户吧，使用本公司出品的云服务，轻松同步应用配置」。商业应用总是倾向于捆绑用户，向用户推售「全家桶」。
</p><p>相比之下，UNIX哲学推荐使用文本格式控制应用行为，用户可以阅读，可以使用其他程序自由地编辑。可以使用git、rsync、nix之类的工具来同步配置，用户可以自由选择，而不是必需使用某家企业的服务。
</p><p>文本是最简单、通用的格式，如前文所述，通过管道机制连接不同程序的输入与输出，大大增强了UNIX程序的可组合性和灵活性。
</p><h2 id="user-content-kiss-principle" class="">KISS principle<a class="" tabindex="-1" href="#kiss-principle">#</a></h2><p>亲吻原则？不不不，KISS是「Keep it simple, stupid!」的缩写（计算机人总爱搞一些奇怪的缩写）。保持简单和笨拙。
</p><blockquote><p>雕琢前先要有原型，跑之前先学会走
</p></blockquote><p>Paul Graham在其文集《黑客与画家》讲过一个故事，在创业时期，他的团队使用Lisp开发了一个让用户以「所见即所得」的方式搭建网上商店的应用，他们每一两天就发布一个新版本，总是更快地将新功能推送给用户，最终打败了当时所有的竞争对手。存在于脑海中的优秀的设计、创新的想法和宏伟的构想，如果连第一个能运行的原型都没有做出来，那就只是空中楼阁，没有任何意义。
</p><p>先解决问题本身比完善的设计更为重要。我个人的经验是，如果你陷入对一个复杂、精致的系统的幻想之中，先起身去喝杯茶清醒清醒，回来后强迫自己在短时间内写一个「残次品」出来，相对来说，「能跑就行」是一个软件的优秀品质。
</p><blockquote><p>过早优化是万恶之源
</p></blockquote><p>可能程序员多多少少都有点完美主义，总是希望能做到尽善尽美，在最开始编写一个程序的时候就想着各种「优化」。有些程序员可能读过Martin Fowler的经典之作《重构》，但可能只读了／记住了一部分。例如，我见过一些程序员在写新功能时总是先全部写到一个大函数里，等到这个函数行数多了之后，就将其几乎全部的内容提取成另一个函数，我难以理解这种行为，这既没有增加代码的可读性，新提取的函数也没有可重用性。只能理解为，在他看来「提取函数」是一种好的行为，要在系统出问题前先做些「好事」。
</p><blockquote><p>题外话：《重构》是本好书，强烈推荐没有读过的程序员去读一读。这里提到它是因为我发现一些程序员经常把重构的目的和重构的手段弄混了，最极端的是认为重构只有提取函数和提取变量两种方式，并认为做这两件事就是好的。
</p></blockquote><p>除了代码本身的过早「优化」，还有一种对程序性能的过早「优化」。一些程序员总对性能有极大的焦虑，在程序初期就对一些细节做优化，缓存、内联，用上各种手段，甚至于为此用上一些「黑魔法」，牺牲了代码的可读性。可事实上，整个系统都没有做过Benchmark，也就是说，根本不知道性能瓶颈在哪里就开始了所谓的「优化」，最后往往是无用功。
</p><h2 id="user-content-最后" class="">最后<a class="" tabindex="-1" href="#最后">#</a></h2><p>在技术日新月异的当下，UNIX哲学仍然具有一定的指导意义，只是要辩证地看待它，结合实际灵活应用才是关键。说到底，软件工程是一个实践学科，经验、原则是在实践中积累和验证的，「Talk is cheap」，就在实践中继续探索吧。
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>通过Org做公众号排版</title>
          <link>https://elliot00.com/posts/org-to-mp</link>
          <description>本文介绍了一个将Org格式文本转换为微信公众号富文本格式的工具。该工具可以帮助用户在编辑Org格式文本后，一键复制到公众号后台发布。</description>
          <pubDate>Fri, 08 Mar 2024 08:51:26 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <p>我的博客基本上都会同步到微信公众号，但是我从来不用公众号后台提供的编辑器，一个原因是它不好用，另一个原因是我已经写好了markdown文本格式，不想再在富文本编辑器里做一次排版工作了。过去我一直在使用 <a href="https://github.com/doocs/md">https://github.com/doocs/md</a> ，但因为一些问题，我想从Markdown格式迁移到Org格式，找了一下没发现有可以将Org转换到公众号格式的工具，于是打算自己写一个工具用于转换。
</p><h2 id="user-content-界面预览" class="">界面预览<a class="" tabindex="-1" href="#界面预览">#</a></h2><p><img alt="preview" src="https://r2.elliot00.com/legacy/%E6%88%AA%E5%B1%8F2024-03-08%2014.14.39.png" width="2520" height="2422"></p><p><a href="https://github.com/Eliot00/mp-org">代码仓库</a></p><p>目前的UI和功能都还比较简单，只是将Org文本贴进去，预览一下样式，再复制粘贴到公众号后台。
</p><h2 id="user-content-原理" class="">原理<a class="" tabindex="-1" href="#原理">#</a></h2><p>微信公众号编辑器支持直接粘贴富文本内容。所谓富文本，顾名思义就是除了文字以外，还包含字体、大小、颜色、对齐方式等样式信息。如果可以从Org文本生成一个富文本内容，那么就可以做到编辑Org格式文本，再一键复制到公众号发布了。
</p><h3 id="user-content-dsl转换">DSL转换<a class="" tabindex="-1" href="#dsl转换">#</a></h3><p>首先需要做的，是两种不同DSL之间的转换。什么是DSL？
</p><p>DSL（Domain-specific language，领域专用语言），是一种专门为了某个特定领域或应用场景而定制的计算机语言。相比通用编程语言，DSL更加小巧、简单、高度专注于特定问题域。它旨在提高开发效率，让程序员或最终用户能够用更自然的方式表达意图。标记语言是DSL的一种典型形式，主要用于对文本内容进行注释和赋予含义。标记语言使用一组预先定义好的结构化标记，将文档划分为不同的逻辑区块，并对每个区块的类型和语义做出约定，最经典的一种标记语言就是HTML（HyperText Markup Language 超文本标记语言）。
</p><p>Org也是一种标记语言，举个例子，Org格式规定将一段文本用星号包起来，例如 <code class="">*bold*</code> ，就表示这段文本应当加粗，支持Org格式的应用，如Emacs就可以通过解析Org文档，获取这个信息，从而在界面上将这段文本加粗显示。
</p><p>那为什么要从Org格式转换到HTML格式呢？因为微信支持HTML格式的富文本，并且HTML可以和另一个DSL——CSS结合，描述更丰富的样式信息。
</p><p>具体的做法是通过一种叫做Parser的程序，将Org转换成一种树形结构，称为AST（Abstract syntax tree，抽象语法树）。例如一段Org文本：
</p><pre><code class="language-org">* Example
some text
- item1
- item2
</code></pre><p>可以解析成这样一种结构：
</p><pre tabindex="0"><code><span><span>{</span></span>
<span><span>    "type"</span><span>:</span><span> "</span><span>org-data</span><span>"</span><span>,</span></span>
<span><span>    "children"</span><span>:</span><span> [</span></span>
<span><span>    {</span></span>
<span><span>        "type"</span><span>:</span><span> "</span><span>section</span><span>"</span><span>,</span></span>
<span><span>        "children"</span><span>:</span><span> [</span></span>
<span><span>        {</span></span>
<span><span>            "type"</span><span>:</span><span> "</span><span>headline</span><span>"</span><span>,</span></span>
<span><span>            "level"</span><span>:</span><span> 1</span><span>,</span></span>
<span><span>            "children"</span><span>:</span><span> [</span></span>
<span><span>            {</span></span>
<span><span>                "type"</span><span>:</span><span> "</span><span>text</span><span>"</span><span>,</span></span>
<span><span>                "value"</span><span>:</span><span> "</span><span>Example</span><span>"</span></span>
<span><span>            }</span></span>
<span><span>            ]</span></span>
<span><span>        }</span><span>,</span></span>
<span><span>        {</span></span>
<span><span>            "type"</span><span>:</span><span> "</span><span>paragraph</span><span>"</span><span>,</span></span>
<span><span>            "children"</span><span>:</span><span> [</span></span>
<span><span>            {</span></span>
<span><span>                "type"</span><span>:</span><span> "</span><span>text</span><span>"</span><span>,</span></span>
<span><span>                "value"</span><span>:</span><span> "</span><span>some text</span><span>\n</span><span>"</span></span>
<span><span>            }</span></span>
<span><span>            ]</span></span>
<span><span>        }</span><span>,</span></span>
<span><span>        {</span></span>
<span><span>            "type"</span><span>:</span><span> "</span><span>plain-list</span><span>"</span><span>,</span></span>
<span><span>            "children"</span><span>:</span><span> [</span></span>
<span><span>            {</span></span>
<span><span>                "type"</span><span>:</span><span> "</span><span>list-item</span><span>"</span><span>,</span></span>
<span><span>                "children"</span><span>:</span><span> [</span></span>
<span><span>                {</span></span>
<span><span>                    "type"</span><span>:</span><span> "</span><span>paragraph</span><span>"</span><span>,</span></span>
<span><span>                    "children"</span><span>:</span><span> [</span></span>
<span><span>                    {</span></span>
<span><span>                        "type"</span><span>:</span><span> "</span><span>text</span><span>"</span><span>,</span></span>
<span><span>                        "value"</span><span>:</span><span> "</span><span>item1</span><span>\n</span><span>"</span></span>
<span><span>                    }</span></span>
<span><span>                    ]</span></span>
<span><span>                }</span></span>
<span><span>                ]</span></span>
<span><span>            }</span><span>,</span></span>
<span><span>            {</span></span>
<span><span>                "type"</span><span>:</span><span> "</span><span>list-item</span><span>"</span><span>,</span></span>
<span><span>                "children"</span><span>:</span><span> [</span></span>
<span><span>                {</span></span>
<span><span>                    "type"</span><span>:</span><span> "</span><span>paragraph</span><span>"</span><span>,</span></span>
<span><span>                    "children"</span><span>:</span><span> [</span></span>
<span><span>                    {</span></span>
<span><span>                        "type"</span><span>:</span><span> "</span><span>text</span><span>"</span><span>,</span></span>
<span><span>                        "value"</span><span>:</span><span> "</span><span>item2</span><span>"</span></span>
<span><span>                    }</span></span>
<span><span>                    ]</span></span>
<span><span>                }</span></span>
<span><span>                ]</span></span>
<span><span>            }</span></span>
<span><span>            ]</span></span>
<span><span>        }</span></span>
<span><span>        ]</span></span>
<span><span>    }</span></span>
<span><span>    ]</span></span>
<span><span>}</span></span></code></pre><p>通过遍历这棵树的节点，可以递归地将其转换成HTML。
</p><pre tabindex="0"><code><span><span>&#x3C;</span><span>h1</span><span>></span><span>Example</span><span>&#x3C;/</span><span>h1</span><span>></span></span>
<span><span>&#x3C;</span><span>p</span><span>></span><span>some text</span><span>&#x3C;/</span><span>p</span><span>></span></span>
<span><span>&#x3C;</span><span>ul</span><span>></span></span>
<span><span>    &#x3C;</span><span>li</span><span>></span></span>
<span><span>    &#x3C;</span><span>p</span><span>></span><span>item1</span><span>&#x3C;/</span><span>p</span><span>></span></span>
<span><span>    &#x3C;/</span><span>li</span><span>></span></span>
<span><span>    &#x3C;</span><span>li</span><span>></span></span>
<span><span>    &#x3C;</span><span>p</span><span>></span><span>item2</span><span>&#x3C;/</span><span>p</span><span>></span></span>
<span><span>    &#x3C;/</span><span>li</span><span>></span></span>
<span><span>&#x3C;/</span><span>ul</span><span>></span></span></code></pre><h3 id="user-content-css内联">CSS内联<a class="" tabindex="-1" href="#css内联">#</a></h3><p>只是用HTML表示一个节点是标题还是无序列表，所携带的样式信息还是太少了。要想描述文本的字体、大小、颜色、间距等细节，还需要借助CSS（Cascading Style Sheets，层叠样式表）进行渲染控制。但是注意，我们不能单独写一个CSS文件上传到公众号后台，要想直接将使用CSS定义了样式的HTML复制到公众号后台，需要使用HTML的内联CSS语法：
</p><pre tabindex="0"><code><span><span>&#x3C;</span><span>p</span><span> style</span><span>=</span><span>"</span><span>color: red;</span><span>"</span><span>></span><span>hello</span><span>&#x3C;/</span><span>p</span><span>></span></span></code></pre><p>在从Org转换到HTML的过程中，可以直接为要生成的元素写死内联样式，但是为了扩展性（也许用户需要自定义CSS样式），在实际的代码中是在转换过程完成后，再将一个单独的CSS文本解析注入到已有的HTML文本中去的。
</p><h3 id="user-content-拷贝富文本">拷贝富文本<a class="" tabindex="-1" href="#拷贝富文本">#</a></h3><p>生成了内联样式的HTML后，怎么把它复制到公众号编辑器里呢？如果你直接打开一个网页，右键查看源代码，把这个源代码复制到公众号，会发现只是复制了HTML的源码文本，而不是最终呈现的富文本样式。怎么把富文本复制到系统的剪切板？其实只需要很短的代码就可以实现：
</p><pre tabindex="0"><code><span><span>const</span><span> type</span><span> =</span><span> "</span><span>text/html</span><span>"</span><span>;</span></span>
<span><span>const</span><span> blob</span><span> =</span><span> new</span><span> Blob</span><span>([</span><span>htmlStr</span><span>]</span><span>,</span><span> {</span><span> type</span><span> }</span><span>);</span></span>
<span><span>const</span><span> data</span><span> =</span><span> [</span><span>new</span><span> ClipboardItem</span><span>({</span><span> [</span><span>type</span><span>]</span><span>:</span><span> blob</span><span> })];</span></span>
<span><span>navigator</span><span>.</span><span>clipboard</span><span>.</span><span>write</span><span>(</span><span>data</span><span>);</span></span></code></pre><h2 id="user-content-roadmap" class="">Roadmap<a class="" tabindex="-1" href="#roadmap">#</a></h2><p>目前这个工具的功能还是非常简单的，后续打算把之前用过的markdown转换的工具的功能都移植过来：
</p><ul><li>链接转换：订阅号不支持外链，只允许内部其他图文链接，要分开处理，外链转成脚注，内部链接不变
</li><li>自定义样式
</li><li>图片上传OSS，可以和picgo之类的工具结合一下
</li></ul>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>使用Rust实现Wayland输入法协议</title>
          <link>https://elliot00.com/posts/rust-wayland-input-method</link>
          <description>本文介绍了如何使用Rust语言实现Wayland输入法协议，包括输入法基础、Wayland协议、Rust实现等内容。该示例可帮助开发者理解Wayland输入法机制并推动相关生态发展。</description>
          <pubDate>Sat, 02 Mar 2024 15:04:10 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <p>对于GNU/Linux系统而言，如何使用输入法一直是一个困扰新手用户(应该主要是东亚用户)的问题。键盘这一输入设备最初是为使用拉丁字母的人们设计的，例如英文，每个单词都是由26个字母组成的，就算把大小写分开也只需要52个实体按键就可以打字（好吧我没算标点符号）；但是中文有上万个汉字，要全部映射到键盘按键是不可能的。幸运的是，计算机系统是硬件与软件的综合，如果直接用硬件支持很困难，我们还可以通过软件来实现想要的功能，这种软件就是输入法。
</p><h2 id="user-content-输入法具体做什么" class="">输入法具体做什么？<a class="" tabindex="-1" href="#输入法具体做什么">#</a></h2><blockquote><p>输入法（或输入法编辑器，常缩写为 IME）是一种操作系统组件或程序，它使用户能够通过使用输入设备上原生的字符序列（或鼠标操作）来生成其输入设备上没有的文字。对于拥有比键盘上的按键更多的字位的语言来说，使用一种输入法通常是必要的。
</p></blockquote><p>以中文输入为例，如果我手上有一个美式键盘（带有26个字母键），现在我想在文本编辑器应用中输入一个「道」字，需要做哪些工作？
</p><h3 id="user-content-字符映射">字符映射<a class="" tabindex="-1" href="#字符映射">#</a></h3><p>首先得想个办法做个映射，回忆一下，曾经我们学习过一种汉字的拉丁字母表示法------拼音。诚然，拼音本来是用来标示字的读音的，但正好每个汉字都有一个或多个对应的拼音序列<sup><a href="#fn.1" class="" id="user-content-fnr.1">1</a></sup>，例如「道」字，可以用拼音序列「dao」来表示。
</p><h3 id="user-content-硬件到软件">硬件到软件<a class="" tabindex="-1" href="#硬件到软件">#</a></h3><p>现在我开始在键盘上按下「dao」这三个字母键了，先不管输入法会如何处理这个序列，首先考虑一个问题，输入法如何知道我按下了这几个键？首先是键盘上的电路起作用，它扫描到对应位置的按键被我按下，接着它将这个对应按键的扫描码通过通信信道（如USB）传递到主板上，经过一些硬件处理，最终要通知到计算机的核心部件：CPU；接下来该软件登场了，一个特别的软件------操作系统，它是硬件与用户程序之间的「中间人」，操作系统中有一个专门的模块，将按键消息封装成应用软件可以使用的数据结构，但到这里还没有结束，操作系统还要决定将这个消息发送给谁，最终应用程序得到按键消息，做出自己的处理。
</p><p>最后一步所谓的应用软件通常也是多层的，例如，桌面上常有多个不同的窗口应用，应该有一个上层应用来管理，每个窗口谁显示在上面，显示在什么位置。它应该要先拿到按键事件，比如Windows的桌面系统，当用户按了​<code class="">Alt+Tab</code>​，它要处理不同窗口的切换；用户在活动的记事本应用上按​<code class="">a</code>​键，它应该把这个事件传递给这个活动的窗口应用，记事本就在它的文本框中显示中​<code class="">a</code>​。
</p><h3 id="user-content-反馈">反馈<a class="" tabindex="-1" href="#反馈">#</a></h3><p>注意到中文里有很多同音字，也就是说一个拼音序列「dao」对应的可不止是一个汉字「道」。使用过拼音输入法的人们应该都知道，输入法有一个候选项的概念，要把所有可能的字或词列出来，显示在一个小窗口里，通常还会标上序号，用户按下对应的数字键可以确认输入对应的候选。这个候选窗口通常显示在当前编辑位置的正下方，输入法程序怎么知道当前编辑光标在哪里？
</p><p>由于经常要按下好几个键才能输入一个字，在按键的过程中如果输入框空空如也总是不太好的，最好能在输入框显示当前的拼音序列，但要和普通输入区分开来（如加一个下划线），当用户确认候选后，再替换掉这一部分文字。有时候如果用户发现整个拼音有错，希望按esc键取消这次输入，那么还应该清空这段文本。这种反馈当前输入的文本在输入法里通常叫「preedit text」。输入法怎么让客户程序知道这一段文本的特别之处？怎么通知编辑器什么时候替换掉这段文本，什么时候取消了输入？
</p><h2 id="user-content-困境" class="">困境<a class="" tabindex="-1" href="#困境">#</a></h2><p>由此可以看出，输入法程序既需要与编辑文字的图形应用通信，也需要和一个管理图形应用的桌面系统应用通信，当我在文本编辑器窗口上按下「dao」这三个键，它不能让文本编辑器直接拿到这个按键序列，它要和桌面系统沟通，先截获按键事件，做一些处理，最后，它告诉文本编辑器，不要显示「dao」，而是显示「道」这个汉字。这意味着，要想输入汉字，只靠输入法软件是不行的，桌面系统要支持给到输入法按键事件，形形色色的应用也要学会听输入法的话。
</p><p>Windows和macOS这两个流行的商业系统支持输入法要容易些，它们有官方指定的桌面环境，甚至有官方指定的第三方应用开发语言，当然，也有官方指定的输入法框架。而GNU/Linux因为开源去中心化的特点，五花八门的发行版，不一样的桌面环境，各种不同技术方案的第三方应用，造成了输入法支持碎片化严重的问题。
</p><h2 id="user-content-wayland" class="">Wayland<a class="" tabindex="-1" href="#wayland">#</a></h2><p>Wayland是类UNIX系统上的新一代图形显示协议，它是传统的X11的继任者，目前主流的两个桌面环境KDE和GNOME都支持了Wayland。Wayland也定义了用于输入法相关的协议，随着Wayland生态的发展，Wayland输入法协议有望成为GNU/Linux输入法的统一标准。
</p><p>Wayland设计为C/S架构，各种GUI应用程序如浏览器是客户端，服务端与各种客户端通信，派发来自IO设备的各种事件；也负责把各个应用输出的图像组合起来，显示在屏幕上，这个过程称为「Compositing」，这个服务端程序也被称为「Compositor」（混成器）。
</p><p>下面简单介绍几个后面会提到的Wayland核心概念：
</p><ul><li>object: 一个抽象概念，每个Wayland资源都是一个object，拥有唯一的ID
</li><li>display: 一个display代表一个Wayland客户端
</li><li>registry: 客户端通过这个object可以得到Compositor提供的全局object列表，然后按需绑定
</li><li>surface: 客户端绘图表面
</li><li>request: 客户端可以向Compositor发送请求，如请求重绘屏幕区域
</li><li>event: Compositor向客户端广播事件，如键盘按键事件
</li></ul><h3 id="user-content-输入法协议">输入法协议<a class="" tabindex="-1" href="#输入法协议">#</a></h3><p>在Wayland设计中，输入法不直接与客户端程序通信，而是由Compositor充当中间人，客户端应用与Compositor之间的协议叫<a href="https://wayland.app/protocols/text-input-unstable-v3">text-input</a>，输入法与Compositor之间的协议叫<a href="https://wayland.app/protocols/input-method-unstable-v1">input-method</a>，这两个协议都还处于unstable状态，意味着未来可能会出现不兼容的修改。
</p><p>输入法与Compositor之间的协议有四个部分：
</p><table><thead><tr><th>名称</th><th>功能简介</th></tr></thead><tbody><tr><td>zwp<sub>input</sub><sub>method</sub><sub>context</sub><sub>v1</sub></td><td>输入法上下文，可控制光标位置、文字上屏等</td></tr><tr><td>zwp<sub>input</sub><sub>method</sub><sub>v1</sub></td><td>激活或取消激活输入法</td></tr><tr><td>zwp<sub>input</sub><sub>panel</sub><sub>v1</sub></td><td>获取zwp<sub>input</sub><sub>panel</sub><sub>surface对象</sub></td></tr><tr><td>zwp<sub>input</sub><sub>panel</sub><sub>surface</sub><sub>v1</sub></td><td>输入法面板界面控制</td></tr></tbody></table><h2 id="user-content-rust实现" class="">Rust实现<a class="" tabindex="-1" href="#rust实现">#</a></h2><p>接下来是代码时间！先来实现一个Hello World级别的输入法，这也是一个邪恶的输入法，它将打乱用户的所有输入！
</p><p>首先要引入<a href="https://github.com/Smithay/wayland-rs">两个依赖</a>：
</p><pre tabindex="0"><code><span><span>[dependencies]</span></span>
<span><span>wayland-client</span><span> =</span><span> { </span><span>version</span><span> =</span><span> "</span><span>0.31.1</span><span>"</span><span> }</span></span>
<span><span>wayland-protocols</span><span> =</span><span> { </span><span>version</span><span> =</span><span> "</span><span>0.31.0</span><span>"</span><span>,</span><span> features</span><span> =</span><span> [</span><span>"</span><span>unstable</span><span>"</span><span>,</span><span> "</span><span>client</span><span>"</span><span>] }</span></span></code></pre><p>注意：​<strong>输入法在Wayland语境下，也是一个客户端程序</strong>​，所以在依赖里用到了​<code class="">wayland-client</code>​这个crate。
</p><pre tabindex="0"><code><span><span>use</span><span> wayland_client</span><span>::</span><span>{</span></span>
<span><span>    event_created_child,</span></span>
<span><span>    protocol</span><span>::</span><span>{</span></span>
<span><span>        wl_keyboard</span><span>::</span><span>{</span><span>self</span><span>, </span><span>KeyState</span><span>},</span></span>
<span><span>        wl_registry,</span></span>
<span><span>    },</span></span>
<span><span>    Connection</span><span>, </span><span>Dispatch</span><span>, </span><span>QueueHandle</span><span>, </span><span>WEnum</span><span>,</span></span>
<span><span>};</span></span>
<span><span>use</span><span> wayland_protocols</span><span>::</span><span>wp</span><span>::</span><span>input_method</span><span>::</span><span>zv1</span><span>::</span><span>client</span><span>::</span><span>{</span></span>
<span><span>    zwp_input_method_context_v1,</span></span>
<span><span>    zwp_input_method_v1</span><span>::</span><span>{</span><span>self</span><span>, </span><span>EVT_ACTIVATE_OPCODE</span><span>},</span></span>
<span><span>};</span></span></code></pre><p>接着来定义一个​<code class="">struct</code>​保存应用状态和需要用到的Wayland对象：
</p><pre tabindex="0"><code><span><span>#[derive(</span><span>Default</span><span>)]</span></span>
<span><span>struct</span><span> AppState</span><span> {</span></span>
<span><span>    running</span><span>:</span><span> bool</span><span>,</span></span>
<span><span>    input_method</span><span>:</span><span> Option</span><span>&#x3C;</span><span>zwp_input_method_v1</span><span>::</span><span>ZwpInputMethodV1</span><span>>,</span></span>
<span><span>    context</span><span>:</span><span> Option</span><span>&#x3C;</span><span>zwp_input_method_context_v1</span><span>::</span><span>ZwpInputMethodContextV1</span><span>>,</span></span>
<span><span>}</span></span></code></pre><p>下一步定义主函数部分：
</p><pre tabindex="0"><code><span><span>fn</span><span> main</span><span>() {</span></span>
<span><span>    // 创建Wayland连接</span></span>
<span><span>    let</span><span> conn</span><span> =</span><span> Connection</span><span>::</span><span>connect_to_env</span><span>()</span><span>.</span><span>unwrap</span><span>();</span></span>
<span></span>
<span><span>    // 创建event queue，以使输入法接收来自Compositor的事件</span></span>
<span><span>    let</span><span> mut</span><span> event_queue</span><span> =</span><span> conn</span><span>.</span><span>new_event_queue</span><span>();</span></span>
<span><span>    let</span><span> qhandle</span><span> =</span><span> event_queue</span><span>.</span><span>handle</span><span>();</span></span>
<span></span>
<span><span>    // 客户端必不可少的object</span></span>
<span><span>    let</span><span> display</span><span> =</span><span> conn</span><span>.</span><span>display</span><span>();</span></span>
<span></span>
<span><span>    // 请求创建wl_registry对象，用于绑定全局object</span></span>
<span><span>    display</span><span>.</span><span>get_registry</span><span>(</span><span>&#x26;</span><span>qhandle</span><span>, ());</span></span>
<span></span>
<span><span>    let</span><span> mut</span><span> state</span><span> =</span><span> AppState</span><span> {</span></span>
<span><span>        running</span><span>:</span><span> true</span><span>,</span></span>
<span><span>        ..</span><span>Default</span><span>::</span><span>default</span><span>()</span></span>
<span><span>    };</span></span>
<span></span>
<span><span>    // 开启循环，不断接收事件</span></span>
<span><span>    while</span><span> state</span><span>.</span><span>running {</span></span>
<span><span>        event_queue</span><span>.</span><span>blocking_dispatch</span><span>(</span><span>&#x26;</span><span>mut</span><span> state</span><span>)</span><span>.</span><span>unwrap</span><span>();</span></span>
<span><span>    }</span></span>
<span><span>}</span></span></code></pre><p>在​<code class="">main</code>​函数里似乎没有处理从Compositor来的事件，那么具体的事件处理代码在哪里呢？既然是Rust实现，怎么能少了Rust的一大重要特性，​<code class="">trait</code>​呢？
</p><pre tabindex="0"><code><span><span>impl</span><span> Dispatch</span><span>&#x3C;</span><span>wl_registry</span><span>::</span><span>WlRegistry</span><span>, ()> </span><span>for</span><span> AppState</span><span> {</span></span>
<span><span>    // 这个事件会告知客户端Compositor支持的接口</span></span>
<span><span>    fn</span><span> event</span><span>(</span></span>
<span><span>        state</span><span>:</span><span> &#x26;</span><span>mut</span><span> Self</span><span>,</span></span>
<span><span>        registry</span><span>:</span><span> &#x26;</span><span>wl_registry</span><span>::</span><span>WlRegistry</span><span>,</span></span>
<span><span>        event</span><span>:</span><span> &#x3C;wl_registry</span><span>::</span><span>WlRegistry</span><span> as</span><span> wayland_client</span><span>::</span><span>Proxy</span><span>></span><span>::</span><span>Event</span><span>,</span></span>
<span><span>        _data</span><span>:</span><span> &#x26;</span><span>(),</span></span>
<span><span>        _conn</span><span>:</span><span> &#x26;</span><span>Connection</span><span>,</span></span>
<span><span>        qh</span><span>:</span><span> &#x26;</span><span>QueueHandle</span><span>&#x3C;</span><span>Self</span><span>>,</span></span>
<span><span>    ) {</span></span>
<span><span>        if</span><span> let</span><span> wl_registry</span><span>::</span><span>Event</span><span>::</span><span>Global</span><span> {</span></span>
<span><span>            name</span><span>, </span><span>interface</span><span>, </span><span>..</span></span>
<span><span>        } </span><span>=</span><span> event</span></span>
<span><span>        {</span></span>
<span><span>            println!</span><span>(</span><span>"</span><span>{} {}</span><span>"</span><span>, </span><span>name</span><span>, </span><span>interface</span><span>);</span></span>
<span><span>            // 在这里可以绑定zwp_input_method_v1</span></span>
<span><span>            match</span><span> &#x26;</span><span>interface</span><span>[</span><span>..</span><span>] {</span></span>
<span><span>                "</span><span>zwp_input_method_v1</span><span>"</span><span> =></span><span> {</span></span>
<span><span>                    let</span><span> input_method</span><span> =</span><span> registry</span></span>
<span><span>                        .</span><span>bind</span><span>::</span><span>&#x3C;zwp_input_method_v1</span><span>::</span><span>ZwpInputMethodV1</span><span>, </span><span>_</span><span>, </span><span>_</span><span>>(</span><span>name</span><span>, </span><span>1</span><span>, </span><span>qh</span><span>, ());</span></span>
<span><span>                    state</span><span>.</span><span>input_method </span><span>=</span><span> Some</span><span>(</span><span>input_method</span><span>);</span></span>
<span><span>                }</span></span>
<span><span>                _</span><span> =></span><span> {}</span></span>
<span><span>            }</span></span>
<span><span>        }</span></span>
<span><span>    }</span></span>
<span><span>}</span></span></code></pre><p>现在我们绑定了全局接口zwp<sub>input</sub><sub>method</sub><sub>v1</sub>，接下来就需要处理输入法激活和取消事件，并且也得通过它拿到context对象。
</p><pre tabindex="0"><code><span><span>impl</span><span> Dispatch</span><span>&#x3C;</span><span>zwp_input_method_v1</span><span>::</span><span>ZwpInputMethodV1</span><span>, ()> </span><span>for</span><span> AppState</span><span> {</span></span>
<span><span>    fn</span><span> event</span><span>(</span></span>
<span><span>        state</span><span>:</span><span> &#x26;</span><span>mut</span><span> Self</span><span>,</span></span>
<span><span>        _proxy</span><span>:</span><span> &#x26;</span><span>zwp_input_method_v1</span><span>::</span><span>ZwpInputMethodV1</span><span>,</span></span>
<span><span>        event</span><span>:</span><span> zwp_input_method_v1</span><span>::</span><span>Event</span><span>,</span></span>
<span><span>        _data</span><span>:</span><span> &#x26;</span><span>(),</span></span>
<span><span>        _conn</span><span>:</span><span> &#x26;</span><span>Connection</span><span>,</span></span>
<span><span>        qhandle</span><span>:</span><span> &#x26;</span><span>QueueHandle</span><span>&#x3C;</span><span>Self</span><span>>,</span></span>
<span><span>    ) {</span></span>
<span><span>        println!</span><span>(</span><span>"</span><span>current event is {:#?}</span><span>"</span><span>, </span><span>event</span><span>);</span></span>
<span><span>        match</span><span> event</span><span> {</span></span>
<span><span>            zwp_input_method_v1</span><span>::</span><span>Event</span><span>::</span><span>Activate</span><span> { </span><span>id</span><span> } </span><span>=></span><span> {</span></span>
<span><span>                println!</span><span>(</span><span>"</span><span>method activate</span><span>"</span><span>);</span></span>
<span></span>
<span><span>                // 截获键盘，之后就可以由输入法处理键盘事件</span></span>
<span><span>                id</span><span>.</span><span>grab_keyboard</span><span>(</span><span>qhandle</span><span>, ());</span></span>
<span></span>
<span><span>                // 保存context后续使用</span></span>
<span><span>                state</span><span>.</span><span>context </span><span>=</span><span> Some</span><span>(</span><span>id</span><span>);</span></span>
<span><span>            }</span></span>
<span><span>            zwp_input_method_v1</span><span>::</span><span>Event</span><span>::</span><span>Deactivate</span><span> { </span><span>context</span><span> } </span><span>=></span><span> {</span></span>
<span><span>                // 销毁context</span></span>
<span><span>                state</span><span>.</span><span>context </span><span>=</span><span> None</span><span>;</span></span>
<span><span>                context</span><span>.</span><span>destroy</span><span>();</span></span>
<span><span>                println!</span><span>(</span><span>"</span><span>method inactive</span><span>"</span><span>);</span></span>
<span><span>            }</span></span>
<span><span>            _</span><span> =></span><span> {}</span></span>
<span><span>        }</span></span>
<span><span>    }</span></span>
<span></span>
<span><span>    event_created_child!</span><span>(</span><span>AppState</span><span>, zwp_input_method_v1</span><span>::</span><span>ZwpInputMethodV1</span><span>, [</span></span>
<span><span>        EVT_ACTIVATE_OPCODE</span><span> =></span><span> (zwp_input_method_context_v1</span><span>::</span><span>ZwpInputMethodContextV1</span><span>, ()),</span></span>
<span><span>    ]);</span></span>
<span><span>}</span></span>
<span></span>
<span><span>impl</span><span> Dispatch</span><span>&#x3C;</span><span>zwp_input_method_context_v1</span><span>::</span><span>ZwpInputMethodContextV1</span><span>, ()> </span><span>for</span><span> AppState</span><span> {</span></span>
<span><span>    fn</span><span> event</span><span>(</span></span>
<span><span>        _state</span><span>:</span><span> &#x26;</span><span>mut</span><span> Self</span><span>,</span></span>
<span><span>        _context</span><span>:</span><span> &#x26;</span><span>zwp_input_method_context_v1</span><span>::</span><span>ZwpInputMethodContextV1</span><span>,</span></span>
<span><span>        event</span><span>:</span><span> zwp_input_method_context_v1</span><span>::</span><span>Event</span><span>,</span></span>
<span><span>        _data</span><span>:</span><span> &#x26;</span><span>(),</span></span>
<span><span>        _conn</span><span>:</span><span> &#x26;</span><span>Connection</span><span>,</span></span>
<span><span>        _qhandle</span><span>:</span><span> &#x26;</span><span>QueueHandle</span><span>&#x3C;</span><span>Self</span><span>>,</span></span>
<span><span>    ) {</span></span>
<span><span>        // 这里暂时空着</span></span>
<span><span>        println!</span><span>(</span><span>"</span><span>current content event is {:#?}</span><span>"</span><span>, </span><span>event</span><span>);</span></span>
<span><span>    }</span></span>
<span><span>}</span></span></code></pre><p>拿到了context对象，截获了键盘事件，最后一步就是前面所说的邪恶的事了：
</p><pre tabindex="0"><code><span><span>impl</span><span> Dispatch</span><span>&#x3C;</span><span>wl_keyboard</span><span>::</span><span>WlKeyboard</span><span>, ()> </span><span>for</span><span> AppState</span><span> {</span></span>
<span><span>    fn</span><span> event</span><span>(</span></span>
<span><span>        state</span><span>:</span><span> &#x26;</span><span>mut</span><span> Self</span><span>,</span></span>
<span><span>        _proxy</span><span>:</span><span> &#x26;</span><span>wl_keyboard</span><span>::</span><span>WlKeyboard</span><span>,</span></span>
<span><span>        event</span><span>:</span><span> wl_keyboard</span><span>::</span><span>Event</span><span>,</span></span>
<span><span>        _data</span><span>:</span><span> &#x26;</span><span>(),</span></span>
<span><span>        _conn</span><span>:</span><span> &#x26;</span><span>Connection</span><span>,</span></span>
<span><span>        _qhandle</span><span>:</span><span> &#x26;</span><span>QueueHandle</span><span>&#x3C;</span><span>Self</span><span>>,</span></span>
<span><span>    ) {</span></span>
<span><span>        match</span><span> event</span><span> {</span></span>
<span><span>            wl_keyboard</span><span>::</span><span>Event</span><span>::</span><span>Key</span><span> {</span></span>
<span><span>                key</span><span>,</span></span>
<span><span>                state</span><span>:</span><span> WEnum</span><span>::</span><span>Value</span><span>(KeyState</span><span>::</span><span>Pressed</span><span>),</span></span>
<span><span>                ..</span></span>
<span><span>            } </span><span>=></span><span> {</span></span>
<span><span>                let</span><span> new_key</span><span> =</span><span> key</span><span> +</span><span> 1</span><span>;</span></span>
<span></span>
<span><span>                let</span><span> key_string</span><span> =</span><span> match</span><span> new_key</span><span> {</span></span>
<span><span>                    16</span><span> =></span><span> "</span><span>q</span><span>"</span><span>,</span></span>
<span><span>                    17</span><span> =></span><span> "</span><span>w</span><span>"</span><span>,</span></span>
<span><span>                    18</span><span> =></span><span> "</span><span>e</span><span>"</span><span>,</span></span>
<span><span>                    19</span><span> =></span><span> "</span><span>r</span><span>"</span><span>,</span></span>
<span><span>                    20</span><span> =></span><span> "</span><span>t</span><span>"</span><span>,</span></span>
<span><span>                    21</span><span> =></span><span> "</span><span>y</span><span>"</span><span>,</span></span>
<span><span>                    22</span><span> =></span><span> "</span><span>u</span><span>"</span><span>,</span></span>
<span><span>                    23</span><span> =></span><span> "</span><span>i</span><span>"</span><span>,</span></span>
<span><span>                    24</span><span> =></span><span> "</span><span>o</span><span>"</span><span>,</span></span>
<span><span>                    25</span><span> =></span><span> "</span><span>p</span><span>"</span><span>,</span></span>
<span><span>                    26</span><span> =></span><span> "</span><span>[</span><span>"</span><span>,</span></span>
<span><span>                    27</span><span> =></span><span> "</span><span>]</span><span>"</span><span>,</span></span>
<span><span>                    28</span><span> =></span><span> "</span><span>\n</span><span>"</span><span>,</span></span>
<span><span>                    30</span><span> =></span><span> "</span><span>a</span><span>"</span><span>,</span></span>
<span><span>                    31</span><span> =></span><span> "</span><span>s</span><span>"</span><span>,</span></span>
<span><span>                    32</span><span> =></span><span> "</span><span>d</span><span>"</span><span>,</span></span>
<span><span>                    33</span><span> =></span><span> "</span><span>f</span><span>"</span><span>,</span></span>
<span><span>                    34</span><span> =></span><span> "</span><span>g</span><span>"</span><span>,</span></span>
<span><span>                    35</span><span> =></span><span> "</span><span>h</span><span>"</span><span>,</span></span>
<span><span>                    36</span><span> =></span><span> "</span><span>j</span><span>"</span><span>,</span></span>
<span><span>                    37</span><span> =></span><span> "</span><span>k</span><span>"</span><span>,</span></span>
<span><span>                    38</span><span> =></span><span> "</span><span>l</span><span>"</span><span>,</span></span>
<span><span>                    39</span><span> =></span><span> "</span><span>;</span><span>"</span><span>,</span></span>
<span><span>                    40</span><span> =></span><span> "</span><span>'</span><span>"</span><span>,</span></span>
<span><span>                    41</span><span> =></span><span> "</span><span>`</span><span>"</span><span>,</span></span>
<span><span>                    42</span><span> =></span><span> "</span><span>\\</span><span>"</span><span>,</span></span>
<span><span>                    44</span><span> =></span><span> "</span><span>z</span><span>"</span><span>,</span></span>
<span><span>                    45</span><span> =></span><span> "</span><span>x</span><span>"</span><span>,</span></span>
<span><span>                    46</span><span> =></span><span> "</span><span>c</span><span>"</span><span>,</span></span>
<span><span>                    47</span><span> =></span><span> "</span><span>v</span><span>"</span><span>,</span></span>
<span><span>                    48</span><span> =></span><span> "</span><span>b</span><span>"</span><span>,</span></span>
<span><span>                    49</span><span> =></span><span> "</span><span>n</span><span>"</span><span>,</span></span>
<span><span>                    50</span><span> =></span><span> "</span><span>m</span><span>"</span><span>,</span></span>
<span><span>                    51</span><span> =></span><span> "</span><span>,</span><span>"</span><span>,</span></span>
<span><span>                    52</span><span> =></span><span> "</span><span>.</span><span>"</span><span>,</span></span>
<span><span>                    53</span><span> =></span><span> "</span><span>/</span><span>"</span><span>,</span></span>
<span><span>                    _</span><span> =></span><span> ""</span><span>,</span></span>
<span><span>                };</span></span>
<span></span>
<span><span>                if</span><span> let</span><span> Some</span><span>(</span><span>context</span><span>) </span><span>=</span><span> &#x26;</span><span>state</span><span>.</span><span>context {</span></span>
<span><span>                    context</span><span>.</span><span>commit_string</span><span>(</span><span>1</span><span>, </span><span>key_string</span><span>.</span><span>to_string</span><span>());</span></span>
<span><span>                }</span></span>
<span><span>            }</span></span>
<span><span>            _</span><span> =></span><span> {}</span></span>
<span><span>        }</span></span>
<span><span>    }</span></span>
<span><span>}</span></span></code></pre><h3 id="user-content-调试">调试<a class="" tabindex="-1" href="#调试">#</a></h3><p>代码部分结束了，要怎么运行这个「调皮」的输入法呢？直接使用​<code class="">cargo run</code>​？有兴趣的读者可以试试看看会有什么错误。前面提到过，输入法需要三方同心协力才能发挥作用，只有输入法实现了协议，那还是孤掌难鸣，现在急需的是一个同样实现了协议的Compositor！
</p><p><a href="https://gitlab.freedesktop.org/wayland/weston">weston</a>就是一个好选择，它是Wayland官方给出的参考实现，非常轻量化，可以直接当做KDE的一个窗口程序打开；最重要的是，它实现了input-method-v1。
</p><p>首先安装weston，然后是配置weston让它使用我们刚刚写的微型输入法，编辑*~/.config/weston.ini*文件，写入：
</p><pre tabindex="0"><code><span><span>[input-method]</span></span>
<span><span>path</span><span>=</span><span>编译后的bin文件路径</span></span></code></pre><p>接着在你当前的桌面环境下启动weston，在weston窗口内打开终端模拟器，输入命令​<code class="">weston-editor</code>​开启一个简单的编辑器应用，试着用新鲜出炉的输入法打一个"hello world"吧。
</p><h2 id="user-content-footnotes" class="">Footnotes:<a class="" tabindex="-1" href="#footnotes">#</a></h2><div><sup><a class="" id="user-content-fn.1" href="#fnr.1">1</a></sup><div><p><a href="https://www.zhihu.com/question/35811498">https://www.zhihu.com/question/35811498</a> 严谨地说，其实存在少量未知读音的汉字
</p></div></div>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>全键盘工作流新利器——kitty</title>
          <link>https://elliot00.com/posts/terminal-emulator-kitty</link>
          <description>kitty是一个高度可定制的终端模拟器。它支持多字体显示不同语言,窗口水平/垂直分屏,标签页切换,SSH连接复用等功能。kitty最大的特点是其插件系统“kittens”,可以扩展更多功能,如显示图像、快速打开文件等。相比其他现代终端模拟器,kitty配置灵活,扩展性强。总体来说,这是一款强大且可定制的终端工具。</description>
          <pubDate>Mon, 26 Feb 2024 07:46:56 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <blockquote><p>以下单独提到的所有“终端”均为“终端模拟器”的简称。
</p></blockquote><p>大约两年前就有人向我安利过<a href="https://sw.kovidgoyal.net/kitty/overview/">kitty</a>，但当时我正在使用<a href="https://wezfurlong.org/wezterm/">wezterm</a>，并没有更换的想法，正好最近我使用的nix打包的wezterm出了点小问题，于是决定来试试这款据称非常强大的终端模拟器。本文就来简单介绍下这个工具，并适当和wezterm做一些对比。
</p><h2 id="user-content-基本功能" class="">基本功能<a class="" tabindex="-1" href="#基本功能">#</a></h2><h3 id="user-content-配置">配置<a class="" tabindex="-1" href="#配置">#</a></h3><p>kitty有很高的可以配置性，用户可以按需要定制快捷键和UI，并且它是通过文本文件来配置的，意味着配置可以通过网络分享，在异地重现。wezterm也是通过文本文件配置，不同的是，kitty使用自定义的文本​<code class="">conf</code>​格式，而wezterm使用图灵完备的​<code class="">lua</code>​语言配置。
</p><h3 id="user-content-多字体">多字体<a class="" tabindex="-1" href="#多字体">#</a></h3><p>在终端上我最多的使用场景是使用Vim编辑文本，常常会遇到多语言文本混合的情况，我希望能为不同语言的字符使用不同的字体（通常一个单独的字体也往往不能包罗万象，如果终端只能设置一个字体将会导致乱码）。wezterm对此提供了很好的支持，用户可以提供多个字体做为fallback，如果字体中不包含要渲染的字符，就从fallback列表里往下查找：
</p><pre tabindex="0"><code><span><span>return</span><span> {</span></span>
<span><span>  font</span><span> =</span><span> wezterm</span><span>.</span><span>font_with_fallback</span><span> {</span></span>
<span><span>    '</span><span>Cascadia Code</span><span>'</span><span>,     </span><span>-- 拉丁字母、标点</span></span>
<span><span>    '</span><span>LXGW WenKai</span><span>'</span><span>,       </span><span>-- 中文</span></span>
<span><span>    '</span><span>CaskaydiaCove NFM</span><span>'</span><span>, </span><span>-- icons</span></span>
<span><span>  },</span></span>
<span><span>}</span></span></code></pre><p>kitty也支持类似的功能，但配置起来要麻烦点。首先可以设置一个字体家族做主要字体，然后通过​<code class="">symbol_map</code>​配置将部分unicode映射到其它字体上，例如​<code class="">symbol_map U+E0A0-U+E0A3,U+E0C0-U+E0C7 PowerlineSymbols</code>​。这里列出我使用的配置:
</p><pre tabindex="0"><code><span><span>font_family      Cascadia Code</span></span>
<span><span>font_size 18.0</span></span>
<span><span></span></span>
<span><span># symbol_map</span></span>
<span><span>symbol_map U+23FB-U+23FE,U+2665,U+26A1,U+2B58,U+E000-U+E00A,U+E0A0-U+E0A3,U+E0B0-U+E0C8,U+E0CA,U+E0CC-U+E0D2,U+E0D4,U+E200-U+E2A9,U+E300-U+E3E3,U+E5FA-U+E6AD,U+E700-U+E7BC,U+E7C4-U+E7C5,U+EA60-U+EA88,U+EA8A-U+EA8C,U+EA8F-U+EAC7,U+EAC9,U+EACC-U+EAFA,U+EAFC-U+EB09,U+EB0B-U+EB4E,U+EB50-U+EBEB,U+F000-U+F00E,U+F010-U+F01E,U+F021-U+F03E,U+F040-U+F04E,U+F050-U+F05E,U+F060-U+F06E,U+F070-U+F07E,U+F080-U+F08E,U+F090-U+F09E,U+F0A0-U+F0AE,U+F0B0-U+F0B2,U+F0C0-U+F0CE,U+F0D0-U+F0DE,U+F0E0-U+F0EE,U+F0F0-U+F0FE,U+F100-U+F10E,U+F110-U+F115,U+F118-U+F11E,U+F120-U+F12E,U+F130-U+F13E,U+F140-U+F14E,U+F150-U+F15E,U+F160-U+F16E,U+F170-U+F17E,U+F180-U+F18E,U+F190-U+F19E,U+F1A0-U+F1AE,U+F1B0-U+F1BE,U+F1C0-U+F1CE,U+F1D0-U+F1DE,U+F1E0-U+F1EE,U+F1F0-U+F1FE,U+F200-U+F20E,U+F210-U+F21E,U+F221-U+F22D,U+F230-U+F23E,U+F240-U+F24E,U+F250-U+F25E,U+F260-U+F26E,U+F270-U+F27E,U+F280-U+F28E,U+F290-U+F29E,U+F2A0-U+F2AE,U+F2B0-U+F2BE,U+F2C0-U+F2CE,U+F2D0-U+F2DE,U+F2E0,U+F300-U+F32F,U+F400-U+F533,U+F0001-U+F012E,U+F0131-U+F0205,U+F0207-U+F02D0,U+F02D2-U+F02D4,U+F02D6-U+F02F4,U+F02F6-U+F0386,U+F0388-U+F043C,U+F043E-U+F05CC,U+F05CE-U+F0AF5,U+F0AF7-U+F0AF8,U+F0AFA-U+F0AFB,U+F0AFD-U+F0B02,U+F0B04,U+F0B06-U+F0C15,U+F0C18-U+F1AF0 CaskaydiaCove Nerd Font Mono</span></span>
<span><span>symbol_map U+4E00-U+9FFF,U+3400-U+4DBF,U+20000-U+2A6DF,U+2A700–U+2B73F,U+2B740–U+2B81F,U+2B820–U+2CEAF,U+F900-U+FAFF,U+2F800-U+2FA1F LXGW WenKai</span></span></code></pre><h3 id="user-content-窗口拆分tabs">窗口拆分、Tabs<a class="" tabindex="-1" href="#窗口拆分tabs">#</a></h3><p>我喜欢终端应该自带窗口拆分、多tabs切换功能，幸好kitty和wezterm都支持这两个功能。值得一提的是，kitty把一个Tab内水平/垂直的切分，称为​<code class="">Window</code>​（wezterm中是​<code class="">Pane</code>​），为了区分，将桌面系统的窗口称为​<code class="">OS window</code>​。
</p><p><img alt="windows" src="https://r2.elliot00.com/kitty/windows.png" width="2544" height="2708"></p><p>通过与shell、ssh等工具的集成，kitty还可以做到多开tab/窗口的同时复用session。例如当前已经通过kitty的ssh扩展连接到远端机器，再通过快捷键新建tab，可以无需再次ssh验证，直接开启一个远程会话。
</p><h2 id="user-content-高级扩展" class="">高级扩展<a class="" tabindex="-1" href="#高级扩展">#</a></h2><p>kitty使用C语言来编写高性能要求的部分，同时又支持使用Python语言编写插件来提供拓展。kitty中把这种插件叫做​<code class="">kittens</code>​，对应的，有一个叫做​<code class="">kitten</code>​的命令行工具来调用这些扩展，而在快捷键配置里，又可以映射快捷键到kitten的调用上，非常灵活且强大。这里介绍几个官方自带的kittens。
</p><h3 id="user-content-icat">icat<a class="" tabindex="-1" href="#icat">#</a></h3><p>这个工具可以在终端中显示图片，使用命令​<code class="">kitten icat &#x3C;image file name></code>​可以调用。这个功能可以很轻易地与其他应用集成，例如终端文件管理器。
</p><h3 id="user-content-ssh">ssh<a class="" tabindex="-1" href="#ssh">#</a></h3><p>前面提到在多窗口、多tabs中可以复用ssh connections，就需要这个kitten的支持。wezterm也支持这个功能，只是wezterm没有插件系统，这个功能是内置的。
</p><h3 id="user-content-hints">hints<a class="" tabindex="-1" href="#hints">#</a></h3><p>在Vim中用过easymotion类插件的朋友应该会觉得这个功能很亲切，这个kitten支持快速检索出当前窗口中的URL文本，在文本上标出序号，通过按下对应的数字键快速打开对应的URL。
</p><p><img alt="hints" src="https://r2.elliot00.com/kitty/hints.png" width="1296" height="1000"></p><p>不止于此，这个插件也可以和Vim集成，通过快捷键快速打开文件并编辑。
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>Emacs配置tree-sitter</title>
          <link>https://elliot00.com/posts/emacs-tree-sitter</link>
          <description>这篇文章介绍了如何使用 tree-sitter 为 Emacs 提供对多种编程语言的语法高亮和结构化编辑支持。tree-sitter 是一个解析器生成工具和增量解析库，它可以生成特定编程语言的解析器，并实时分析代码文件，构建一个详细的语法树。该文章提供了详细的配置步骤，包括添加语法库的源码仓库地址、修改语言模式映射等，并介绍了如何安装 grammar 动态库。</description>
          <pubDate>Sat, 23 Dec 2023 08:59:43 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <p>使用编辑器写代码时，和编辑普通文本不同，一个重要的需求是要能识别编程语言的不同组成部分，这样就能根据代码的不同部分提供高亮，又或者如移除函数，折叠代码块等结构化编辑功能。
</p><h2 id="user-content-tree-sitter" class="">tree-sitter<a class="" tabindex="-1" href="#tree-sitter">#</a></h2><p>为了满足以上需求，需要找一个能识别解析编程语言的工具，<a href="https://tree-sitter.github.io/tree-sitter/">tree-sitter</a>就是这样一个工具（集）。tree-sitter是一个解析器生成工具和增量解析库。它能够生成一个特定于编程语言的解析器，并实时分析代码文件，
构建一个详细的语法树。这个语法树反映了代码的结构，使得编辑器可以进行复杂的语法高亮和结构化编辑操作。
</p><p><a href="https://github.com/emacs-tree-sitter">emacs-tree-sitter</a>旨在为Emacs提供tree-sitter支持，而从Emacs 29开始这个古老而强大的编辑器提供了内置的tree-sitter支持。但是Emacs内置的包叫做​<code class="">treesit</code>​，API和外部的emacs-tree-sitter不一样，我找了一些配置
片段，终于成功地应用了它，因此在我的个人博客里记录一下。
</p><h2 id="user-content-配置" class="">配置<a class="" tabindex="-1" href="#配置">#</a></h2><p>首先要确认Emacs版本，可以通过​<code class="">(treesit-available-p)</code>​验证treesit是否可用。接下来添加这样一段配置：
</p><pre tabindex="0"><code><span><span>(</span><span>use-package</span><span> treesit</span></span>
<span><span>  :config (</span><span>setq</span><span> treesit-font-lock-level </span><span>4</span><span>)</span></span>
<span><span>  :init</span></span>
<span><span>  (</span><span>setq</span><span> treesit-language-source-alist</span></span>
<span><span>    '((elisp      </span><span>.</span><span> (</span><span>"</span><span>https://github.com/Wilfred/tree-sitter-elisp</span><span>"</span><span>))</span></span>
<span><span>      (rust       </span><span>.</span><span> (</span><span>"</span><span>https://github.com/tree-sitter/tree-sitter-rust</span><span>"</span><span>))</span></span>
<span><span>      (toml       </span><span>.</span><span> (</span><span>"</span><span>https://github.com/tree-sitter/tree-sitter-toml</span><span>"</span><span>))))</span></span>
<span><span>  (</span><span>add-to-list</span><span> 'major-mode-remap-alist</span><span> '(</span><span>python-mode</span><span> .</span><span> python-ts-mode))</span></span>
<span><span>  (</span><span>add-to-list</span><span> 'auto-mode-alist</span><span> '(</span><span>"</span><span>\\.rs\\'</span><span>"</span><span> .</span><span> rust-ts-mode))</span></span>
<span><span>  (</span><span>add-to-list</span><span> 'auto-mode-alist</span><span> '(</span><span>"</span><span>\\.ts\\'</span><span>"</span><span> .</span><span> typescript-ts-mode)))</span></span></code></pre><p>接下来详细讲解一下这段配置：
</p><p>首先​<code class="">use-package</code>​不是必要的，只是用它方便初始化和集中配置管理。接着是​<code class="">treesit-font-lock-level</code>​，这是配置高亮层级的，从低到高值可以是1到4。下一步是添加语法库的源码仓库地址，​<code class="">tree-sitter</code>​并不是一个工具包含世界上的所有语言解析器，使用者可以自己按需指定单个语言的解析器，在需要的时候加载对应的编译好的动态库。最后，​<code class="">treesit</code>​给Emacs里的​<code class="">&#x3C;language>-mode</code>​加了个对应的​<code class="">&#x3C;language>-ts-mode</code>​，一些已经内置的语言的mode，可以用​<code class="">major-mode-remap-alist</code>​修改，像​<code class="">Rust</code>​的​<code class="">rust-mode</code>​没有内置，那就直接根据文件后缀映射一下使用treesit提供的​<code class="">rust-ts-mode</code>​。
</p><p>在开始使用前，还有一件事，前面的配置只是添加了grammer的地址，但是还没下载安装，需要使用​<code class="">M-x treesit-install-language-grammar</code>​来安装。grammar动态库默认应该会放在​<code class="">~/.emacs.d/tree-sitter</code>​里。现在就可以打开需要编辑的代码文件愉快地编辑了。
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>自制输入法：拼音输入法与HMM</title>
          <link>https://elliot00.com/posts/input-method-hmm</link>
          <description>文章主要介绍了如何使用隐马尔可夫模型（HMM）来实现一个简单的中文拼音输入法。1. 首先需要收集汉字字库和中文语料库，并对语料库进行预处理，如将句子分割成单词、给每个字标注拼音等。2. 然后，使用语料库训练HMM模型，训练过程包括计算初始概率、转移概率和输出概率。3. 最后，在输入拼音序列时，使用HMM模型进行解码，输出最可能对应的汉字序列。文章还讨论了在实践中使用HMM模型时遇到的问题，如语料处理问题、真实世界的输入、与时俱进、计算问题等，并提出了解决这些问题的方案。</description>
          <pubDate>Thu, 13 Apr 2023 08:09:39 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <h2 id="user-content-拼音输入法" class="">拼音输入法<a class="" tabindex="-1" href="#拼音输入法">#</a></h2><p>输入法的基本功能是将键盘输入序列映射到另一种文字序列，例如键盘按下nihao这五个键，程序输出汉字“你好”。中文用户最常用的输入法应该是拼音输入法，现在来试试用Python写一个最简单的拼音输入法。
</p><pre tabindex="0"><code><span><span># 首先找一本汉字字典，然后把这个字典变成Python中的字典（</span></span>
<span></span>
<span><span>code_table </span><span>=</span><span> {</span></span>
<span><span>  '</span><span>a</span><span>'</span><span>:</span><span> [</span><span>'</span><span>啊</span><span>'</span><span>]</span></span>
<span><span>  '</span><span>ai</span><span>'</span><span>:</span><span> [</span><span>'</span><span>爱</span><span>'</span><span>,</span><span> '</span><span>艾</span><span>'</span><span>,</span><span> '</span><span>哎</span><span>'</span><span>]</span><span>,</span></span>
<span><span>  # ...</span></span>
<span><span>}</span></span></code></pre><p>用户每输入一个拼音，就在这个字典里查找对应的字，然后让用户从这个汉字列表里面选一个。这个输入法一次只能打一个字，如果要打词语，打句子呢？再去找汉语词典、成语词典、歇后语大全，然后把这些信息通通存起来吗？这是个办法，但是我们无法穷尽所有可能的中文组合，总会有不在数据库中的句子或词组，如何创造一个当前数据库中没有的句子或词组呢？可以先将用户输入的拼音拆分，找出所有在现有数据库中存在的单字或词语，然后做一个排列组合。问题是，汉字存在大量的同音字，因此拼音输入法的​<strong>重码率非常高</strong>​，例如ni可能对应了几十个汉字，拼音序列越长，可能的组合就越多，能不能把最有可能的组合排在前面？
</p><h2 id="user-content-隐马尔可夫模型hidden-markov-model-hmm" class="">隐马尔可夫模型（Hidden Markov Model, HMM）<a class="" tabindex="-1" href="#隐马尔可夫模型hidden-markov-model-hmm">#</a></h2><blockquote><p>这部分的介绍是ChatGPT帮忙写的，建议读者阅读一些相关文章，如<a href="https://www.cnblogs.com/skyme/p/4651331.html">一文搞懂HMM</a></p></blockquote><p>当我们在输入汉字时，汉字之间的关系是非常重要的。我们可以认为汉字之间存在着“亲缘关系”，有些汉字之间“关系很近”，例如在一篇文章中，“我”字的后面很可能是“的”字，“的”字的后面很可能是“是”字，这种关系称为二元关系。
</p><p>HMM是一种能够建模序列数据的统计模型，能够考虑当前时刻的可见状态（拼音）和上一个时刻的隐含状态（汉字）之间的关系。在拼音输入法中，我们可以利用HMM来学习汉字之间的“亲缘关系”，并且根据输入的拼音序列来预测最有可能的汉字序列。
</p><p>具体地说，在HMM中，我们使用一个二元模型来建模汉字之间的关系。在二元模型中，每个汉字只和它的前一个汉字有关，也就是说，每个汉字的出现概率只与它的前一个汉字出现的概率有关。这种模型非常适合用于建模汉字之间的“亲缘关系”。
</p><p>通过训练HMM模型，我们可以学习汉字之间的转移概率和汉字对应拼音的概率。这些概率可以帮助我们预测给定拼音序列的最有可能汉字序列。因此，在拼音输入法中，我们可以使用HMM来帮助我们生成最有可能的汉字序列，从而实现自然的中文输入。
</p><h2 id="user-content-训练hmm" class="">训练HMM<a class="" tabindex="-1" href="#训练hmm">#</a></h2><p>假设现在有一些原始中文语料（例如所有中文维基百科的词条文本），如何训练一个隐马尔可夫模型呢？首先我们要解决什么问题呢，就是对于一个给定的拼音序列（可见状态链），我们知道有多少种隐含状态（可能的汉字），知道每一个汉字出现在另一个汉字后面的概率（转换状态），知道汉字对应的拼音的概率（输出概率），求最有可能的汉字序列（隐含状态链）。
</p><p>根据我们的需求，开始训练HMM，首先可以把文章分割成一个个的句子，给每一个字都标注上拼音。遍历每一个句子，计算以下信息：
</p><ul><li>初始概率I(W1)：每个字W1出现在句子开头的概率，等于​<strong>出现的次数/所有句子数</strong>​
</li><li>转换概率T(W1 -> W2)：每个字W1的下一个字是字W2的概率，等于​<strong>W1后是W2的次数/W1后是任意字的次数</strong></li><li>输出概率E(W -> Y1)：每个字W的可能的拼音Y1的概率，等于​<strong>字拼音为Y1的次数/字出现次数</strong>​，不是多音字的话，这个概率就是1
</li></ul><h2 id="user-content-简化模型" class="">简化模型<a class="" tabindex="-1" href="#简化模型">#</a></h2><p>由于汉字和拼音的数量很多，为了简要说明，在这里我假设中文的世界里只有三个拼音，九个汉字：
</p><pre tabindex="0"><code><span><span>from</span><span> collections </span><span>import</span><span> defaultdict</span></span>
<span></span>
<span><span>w1 </span><span>=</span><span> '</span><span>你</span><span>'</span><span>,</span><span> '</span><span>泥</span><span>'</span><span>,</span><span> '</span><span>尼</span><span>'</span></span>
<span><span>w2 </span><span>=</span><span> '</span><span>号</span><span>'</span><span>,</span><span> '</span><span>好</span><span>'</span><span>,</span><span> '</span><span>毫</span><span>'</span></span>
<span><span>w3 </span><span>=</span><span> '</span><span>压</span><span>'</span><span>,</span><span> '</span><span>鸭</span><span>'</span><span>,</span><span> '</span><span>呀</span><span>'</span></span>
<span></span>
<span><span>init_prob </span><span>=</span><span> {</span></span>
<span><span>    '</span><span>你</span><span>'</span><span>:</span><span> 0.6</span><span>,</span><span> '</span><span>泥</span><span>'</span><span>:</span><span> 0.3</span><span>,</span><span> '</span><span>尼</span><span>'</span><span>:</span><span> 0.1</span></span>
<span><span>}</span></span>
<span></span>
<span><span>trans_prob </span><span>=</span><span> {</span></span>
<span><span>    '</span><span>你</span><span>'</span><span>:</span><span> {</span></span>
<span><span>        '</span><span>号</span><span>'</span><span>:</span><span> 0.1</span><span>,</span></span>
<span><span>        '</span><span>好</span><span>'</span><span>:</span><span> 0.7</span><span>,</span></span>
<span><span>        '</span><span>毫</span><span>'</span><span>:</span><span> 0.2</span><span>,</span></span>
<span><span>    },</span></span>
<span><span>    '</span><span>泥</span><span>'</span><span>:</span><span> {</span></span>
<span><span>        '</span><span>号</span><span>'</span><span>:</span><span> 0.3</span><span>,</span></span>
<span><span>        '</span><span>好</span><span>'</span><span>:</span><span> 0.3</span><span>,</span></span>
<span><span>        '</span><span>毫</span><span>'</span><span>:</span><span> 0.4</span><span>,</span></span>
<span><span>    },</span></span>
<span><span>    '</span><span>尼</span><span>'</span><span>:</span><span> {</span></span>
<span><span>        '</span><span>号</span><span>'</span><span>:</span><span> 0.4</span><span>,</span></span>
<span><span>        '</span><span>好</span><span>'</span><span>:</span><span> 0.3</span><span>,</span></span>
<span><span>        '</span><span>毫</span><span>'</span><span>:</span><span> 0.3</span><span>,</span></span>
<span><span>    },</span></span>
<span><span>    '</span><span>号</span><span>'</span><span>:</span><span> {</span></span>
<span><span>        '</span><span>压</span><span>'</span><span>:</span><span> 0.2</span><span>,</span></span>
<span><span>        '</span><span>鸭</span><span>'</span><span>:</span><span> 0.2</span><span>,</span></span>
<span><span>        '</span><span>呀</span><span>'</span><span>:</span><span> 0.6</span><span>,</span></span>
<span><span>    },</span></span>
<span><span>    '</span><span>好</span><span>'</span><span>:</span><span> {</span></span>
<span><span>        '</span><span>压</span><span>'</span><span>:</span><span> 0.1</span><span>,</span></span>
<span><span>        '</span><span>鸭</span><span>'</span><span>:</span><span> 0.2</span><span>,</span></span>
<span><span>        '</span><span>呀</span><span>'</span><span>:</span><span> 0.7</span><span>,</span></span>
<span><span>    },</span></span>
<span><span>    '</span><span>毫</span><span>'</span><span>:</span><span> {</span></span>
<span><span>        '</span><span>压</span><span>'</span><span>:</span><span> 0.3</span><span>,</span></span>
<span><span>        '</span><span>鸭</span><span>'</span><span>:</span><span> 0.3</span><span>,</span></span>
<span><span>        '</span><span>呀</span><span>'</span><span>:</span><span> 0.4</span><span>,</span></span>
<span><span>    },</span></span>
<span><span>}</span></span>
<span></span>
<span><span># 没有多音字，就默认输出概率emiss_prob[字][拼音]全为1</span></span>
<span><span>emiss_prob </span><span>=</span><span> defaultdict</span><span>(</span><span>lambda</span><span>: </span><span>defaultdict</span><span>(</span><span>lambda</span><span>: </span><span>1</span><span>))</span></span></code></pre><h2 id="user-content-暴力求解" class="">暴力求解<a class="" tabindex="-1" href="#暴力求解">#</a></h2><p>对于一个拼音序列​<code class="">['ni', 'hao', 'ya']</code>​，我们可以先根据每一个拼音对应的所有的可能的字，得出所有可能的汉字序列（隐含状态链），然后我们对每一个可能的隐含状态链，求这个隐含状态链得到相应的可见状态链（确定的拼音序列）的概率，给出概率最大的那个隐含状态链就可以了。设这个概率为P，在这个例子里P等于​<code class="">I(W1) * E(W1 -> 'ni') * T(W1 -> W2) * E(W2 -> 'hao') * T(W2 -> W3) * E(W3 -> 'ya')</code>​。例如对于“你好呀”三个字，概率P为“你”字出现在句子开头的概率，乘以“你”的拼音是“ni”的概率，乘以“你”的下一个字是“好”的概率，乘以“好”的拼音是“hao”的概率，乘以“好”的下一个字是“呀”的概率，乘以“呀”的拼音是“ya”的概率。
</p><pre tabindex="0"><code><span><span>import</span><span> itertools</span></span>
<span></span>
<span></span>
<span><span>def</span><span> count_prob</span><span>(</span><span>group</span><span>)</span><span>:</span></span>
<span><span>    (c1</span><span>,</span><span> c2</span><span>,</span><span> c3) </span><span>=</span><span> group</span></span>
<span><span>    prob </span><span>=</span><span> init_prob</span><span>[</span><span>c1</span><span>]</span><span> *</span><span> emiss_prob</span><span>[</span><span>c1</span><span>]</span><span>[</span><span>'</span><span>ni</span><span>'</span><span>]</span><span> *</span><span> trans_prob</span><span>[</span><span>c1</span><span>]</span><span>[</span><span>c2</span><span>]</span><span> *</span><span> emiss_prob</span><span>[</span><span>c2</span><span>]</span><span>[</span><span>'</span><span>hao</span><span>'</span><span>]</span><span> *</span><span> trans_prob</span><span>[</span><span>c2</span><span>]</span><span>[</span><span>c3</span><span>]</span><span> *</span><span> emiss_prob</span><span>[</span><span>c3</span><span>]</span><span>[</span><span>'</span><span>ya</span><span>'</span><span>]</span></span>
<span><span>    return</span><span> (prob</span><span>,</span><span> ''</span><span>.</span><span>join</span><span>(</span><span>group</span><span>)</span><span>)</span></span>
<span></span>
<span><span>result </span><span>=</span><span> sorted</span><span>(</span><span>[</span><span>count_prob</span><span>(</span><span>i</span><span>)</span><span> for</span><span> i </span><span>in</span><span> itertools.</span><span>product</span><span>(</span><span>w1</span><span>,</span><span> w2</span><span>,</span><span> w3</span><span>)</span><span>],</span><span> key</span><span>=lambda</span><span> x</span><span>: x</span><span>[</span><span>0</span><span>]</span><span>,</span><span> reverse</span><span>=</span><span>True</span><span>)</span></span>
<span><span>for</span><span> i </span><span>in</span><span> result</span><span>:</span></span>
<span><span>    print</span><span>(</span><span>f</span><span>"text: </span><span>{</span><span>i</span><span>[</span><span>0</span><span>]</span><span>}</span><span> prob: </span><span>{</span><span>i</span><span>[</span><span>1</span><span>]</span><span>}</span><span>"</span><span>)</span></span>
<span></span>
<span><span>"""</span></span>
<span><span>result:</span></span>
<span><span>text: 0.294 prob: 你好呀</span></span>
<span><span>text: 0.084 prob: 你好鸭</span></span>
<span><span>text: 0.063 prob: 泥好呀</span></span>
<span><span>text: 0.054 prob: 泥号呀</span></span>
<span><span>text: 0.048 prob: 你毫呀</span></span>
<span><span>text: 0.048 prob: 泥毫呀</span></span>
<span><span>text: 0.042 prob: 你好压</span></span>
<span><span>text: 0.036 prob: 你号呀</span></span>
<span><span>text: 0.036 prob: 你毫压</span></span>
<span><span>text: 0.036 prob: 你毫鸭</span></span>
<span><span>text: 0.036 prob: 泥毫压</span></span>
<span><span>text: 0.036 prob: 泥毫鸭</span></span>
<span><span>text: 0.024000000000000004 prob: 尼号呀</span></span>
<span><span>text: 0.020999999999999998 prob: 尼好呀</span></span>
<span><span>text: 0.018 prob: 泥号压</span></span>
<span><span>text: 0.018 prob: 泥号鸭</span></span>
<span><span>text: 0.018 prob: 泥好鸭</span></span>
<span><span>text: 0.012 prob: 你号压</span></span>
<span><span>text: 0.012 prob: 你号鸭</span></span>
<span><span>text: 0.012 prob: 尼毫呀</span></span>
<span><span>text: 0.009 prob: 泥好压</span></span>
<span><span>text: 0.009 prob: 尼毫压</span></span>
<span><span>text: 0.009 prob: 尼毫鸭</span></span>
<span><span>text: 0.008000000000000002 prob: 尼号压</span></span>
<span><span>text: 0.008000000000000002 prob: 尼号鸭</span></span>
<span><span>text: 0.006 prob: 尼好鸭</span></span>
<span><span>text: 0.003 prob: 尼好压</span></span>
<span><span>"""</span></span></code></pre><h2 id="user-content-维特比算法" class="">维特比算法<a class="" tabindex="-1" href="#维特比算法">#</a></h2><p>暴力求解的方法对于拼音序列比较短的情况还可以接受，但是对于长度较长的拼音序列，可能性的组合数会非常大，计算复杂度会指数级增长，因此需要一种高效的算法来解决这个问题。现在再一次回顾一下暴力求解的过程，只是这次分三步来算:
</p><pre tabindex="0"><code><span><span>prob_map </span><span>=</span><span> {}</span></span>
<span></span>
<span><span># 首先让拼音序列只有“ni”，对应的只有w1这三个可能的汉字</span></span>
<span><span>count </span><span>=</span><span> 0</span></span>
<span><span>for</span><span> c </span><span>in</span><span> w1</span><span>:</span></span>
<span><span>    prob_map</span><span>[</span><span>c</span><span>]</span><span> =</span><span> init_prob</span><span>[</span><span>c</span><span>]</span><span> *</span><span> emiss_prob</span><span>[</span><span>c</span><span>]</span><span>[</span><span>'</span><span>ni</span><span>'</span><span>]</span></span>
<span><span>    count </span><span>+=</span><span> 1</span></span>
<span></span>
<span><span>print</span><span>(</span><span>prob_map</span><span>)</span></span>
<span><span>print</span><span>(</span><span>f</span><span>"计算次数：</span><span>{</span><span>count</span><span>}</span><span>"</span><span>)</span></span>
<span></span>
<span><span># 现在拼音序列加上“hao”，对应的可能的汉字数量为w1和w2的笛卡尔积，有9种可能</span></span>
<span><span>count </span><span>=</span><span> 0</span></span>
<span><span>for</span><span> (c1</span><span>,</span><span> c2) </span><span>in</span><span> itertools</span><span>.</span><span>product</span><span>(</span><span>w1</span><span>,</span><span> w2</span><span>):</span></span>
<span><span>    prob_map</span><span>[</span><span>f</span><span>"</span><span>{</span><span>c1</span><span>}{</span><span>c2</span><span>}</span><span>"</span><span>]</span><span> =</span><span> init_prob</span><span>[</span><span>c1</span><span>]</span><span> *</span><span> emiss_prob</span><span>[</span><span>c1</span><span>]</span><span>[</span><span>'</span><span>ni</span><span>'</span><span>]</span><span> *</span><span> trans_prob</span><span>[</span><span>c1</span><span>]</span><span>[</span><span>c2</span><span>]</span><span> *</span><span> emiss_prob</span><span>[</span><span>c2</span><span>]</span><span>[</span><span>'</span><span>hao</span><span>'</span><span>]</span></span>
<span><span>    count </span><span>+=</span><span> 1</span></span>
<span></span>
<span><span>print</span><span>(</span><span>prob_map</span><span>)</span></span>
<span><span>print</span><span>(</span><span>f</span><span>"计算次数：</span><span>{</span><span>count</span><span>}</span><span>"</span><span>)</span></span>
<span></span>
<span><span># 第三步，拼音序列加上“ya”，对应的可能的汉字序列有27种可能</span></span>
<span><span>count </span><span>=</span><span> 0</span></span>
<span><span>for</span><span> (c1</span><span>,</span><span> c2</span><span>,</span><span> c3) </span><span>in</span><span> itertools</span><span>.</span><span>product</span><span>(</span><span>w1</span><span>,</span><span> w2</span><span>,</span><span> w3</span><span>):</span></span>
<span><span>    prob_map</span><span>[</span><span>f</span><span>"</span><span>{</span><span>c1</span><span>}{</span><span>c2</span><span>}{</span><span>c3</span><span>}</span><span>"</span><span>]</span><span> =</span><span> init_prob</span><span>[</span><span>c1</span><span>]</span><span> *</span><span> emiss_prob</span><span>[</span><span>c1</span><span>]</span><span>[</span><span>'</span><span>ni</span><span>'</span><span>]</span><span> *</span><span> trans_prob</span><span>[</span><span>c1</span><span>]</span><span>[</span><span>c2</span><span>]</span><span> *</span><span> emiss_prob</span><span>[</span><span>c2</span><span>]</span><span>[</span><span>'</span><span>hao</span><span>'</span><span>]</span><span> *</span><span> trans_prob</span><span>[</span><span>c2</span><span>]</span><span>[</span><span>c3</span><span>]</span><span> *</span><span> emiss_prob</span><span>[</span><span>c3</span><span>]</span><span>[</span><span>'</span><span>ya</span><span>'</span><span>]</span></span>
<span><span>    count </span><span>+=</span><span> 1</span></span>
<span></span>
<span><span>print</span><span>(</span><span>prob_map</span><span>)</span></span>
<span><span>print</span><span>(</span><span>f</span><span>"计算次数：</span><span>{</span><span>count</span><span>}</span><span>"</span><span>)</span></span>
<span></span>
<span><span>"""</span></span>
<span><span>result:</span></span>
<span><span>{'你': 0.6, '泥': 0.3, '尼': 0.1}</span></span>
<span><span>计算次数：3</span></span>
<span><span>{'你': 0.6, '泥': 0.3, '尼': 0.1, '你号': 0.06, '你好': 0.42, '你毫': 0.12, '泥号': 0.09, '泥好': 0.09, '泥毫': 0.12, '尼号': 0.04000000000000001, '尼好': 0.03, '尼毫': 0.03}</span></span>
<span><span>计算次数：9</span></span>
<span><span>{'你': 0.6, '泥': 0.3, '尼': 0.1, '你号': 0.06, '你好': 0.42, '你毫': 0.12, '泥号': 0.09, '泥好': 0.09, '泥毫': 0.12, '尼号': 0.04000000000000001, '尼好': 0.03, '尼毫': 0.03, '你号压': 0.012, '你号鸭': 0.012, '你号呀': 0.036, '你好压': 0.042, '你好鸭': 0.084, '你好呀': 0.294, '你毫压': 0.036, '你毫鸭': 0.036, '你毫呀': 0.048, '泥号压': 0.018, '泥号鸭': 0.018, '泥号呀': 0.054, '泥好压': 0.009, '泥好鸭': 0.018, '泥好呀': 0.063, '泥毫压': 0.036, '泥毫鸭': 0.036, '泥毫呀': 0.048, '尼号压': 0.008000000000000002, '尼号鸭': 0.008000000000000002, '尼号呀': 0.024000000000000004, '尼好压': 0.003, '尼好鸭': 0.006, '尼好呀': 0.020999999999999998, '尼毫压': 0.009, '尼毫鸭': 0.009, '尼毫呀': 0.012}</span></span>
<span><span>计算次数：27</span></span>
<span><span>"""</span></span></code></pre><p>可以发现，最终的概率是一个乘法运算的结果，根据我小学数学的知识，如果两个正数相乘，那这两个数越大，结果就应该越大。如果说我们在第二步计算的时候，不再考虑所有的排列组合，而是直接把第一个字固定为上一步求出来的概率最大的“你”，是不是就能保证结果是最大的呢？毕竟只要是正数相乘（这里的概率显然都是正数），那么乘​<strong>上一步的最大值</strong>​，一定比乘上一步不是最大的值得到的概率值要​<strong>大</strong>​！
</p><p>现在来介绍一下维特比算法，维特比算法是一种动态规划的算法，可以在计算量相对较小的情况下，得到概率最大的隐含状态链。维特比算法的思路是，对于每个时刻，计算到达该时刻的所有隐含状态链中概率最大的那一个，直到计算到最后一个时刻，得到最终的概率最大的隐含状态链。
</p><p>具体地，我们定义一个矩阵V，其中V[t][i]表示在时刻t，隐含状态为i的最大概率（也就是到达状态i的所有路径中概率最大的那一条路径的概率），然后逐个时刻进行计算，直到计算到最后一个时刻，得到概率最大的隐含状态链。
</p><p>计算方法如下：
</p><ol><li>在t=1时刻，对于每个隐含状态i，令V[1][i] = I(W1) * E(W1 -> yi)；
</li><li>在t>1的时刻，对于每个隐含状态i，计算V[t][i]：
</li></ol><p><code class="">V[t][i] = max(V[t - 1][j] * T(j -> i) * E(i -> yi))</code></p><p>其中max操作表示求最大值，j表示上一个时刻的隐含状态，T(j -> i)表示从j状态到i状态的转移概率，E(i -> yi)表示i状态对应的汉字输出为yi拼音的概率。最后，在所有时刻中，找到概率最大的隐含状态链。
</p><pre tabindex="0"><code><span><span>obs </span><span>=</span><span> [</span><span>'</span><span>ni</span><span>'</span><span>,</span><span> '</span><span>hao</span><span>'</span><span>,</span><span> '</span><span>ya</span><span>'</span><span>]</span></span>
<span></span>
<span><span>V </span><span>=</span><span> {</span><span> k</span><span>:</span><span> {}</span><span> for</span><span> k </span><span>in</span><span> range</span><span>(</span><span>len</span><span>(</span><span>obs</span><span>))}</span></span>
<span><span># 加一个look back方便回溯</span></span>
<span><span>look_back</span><span>:</span><span> list</span><span>[</span><span>tuple</span><span>[</span><span>float</span><span>,</span><span> str</span><span>]]</span><span> =</span><span> [</span><span> (</span><span>0</span><span>,</span><span> ''</span><span>) </span><span>for</span><span> _ </span><span>in</span><span> obs </span><span>]</span></span>
<span></span>
<span><span>count </span><span>=</span><span> 0</span></span>
<span></span>
<span><span># 时刻0</span></span>
<span><span>t </span><span>=</span><span> 0</span></span>
<span><span>for</span><span> c </span><span>in</span><span> w1</span><span>:</span></span>
<span><span>    prob </span><span>=</span><span> init_prob</span><span>[</span><span>c</span><span>]</span><span> *</span><span> emiss_prob</span><span>[</span><span>c</span><span>]</span><span>[</span><span>'</span><span>ni</span><span>'</span><span>]</span></span>
<span><span>    V</span><span>[</span><span>t</span><span>]</span><span>[</span><span>c</span><span>]</span><span> =</span><span> prob</span></span>
<span><span>    if</span><span> prob </span><span>></span><span> look_back</span><span>[</span><span>t</span><span>]</span><span>[</span><span>0</span><span>]</span><span>:</span></span>
<span><span>        look_back</span><span>[</span><span>t</span><span>]</span><span> =</span><span> (prob</span><span>,</span><span> c)</span></span>
<span><span>    count </span><span>+=</span><span> 1</span></span>
<span></span>
<span><span>t </span><span>+=</span><span> 1</span></span>
<span><span># 时刻1</span></span>
<span><span>for</span><span> (pinyin</span><span>,</span><span> words) </span><span>in</span><span> zip</span><span>(</span><span>obs</span><span>[</span><span>1</span><span>:</span><span>]</span><span>,</span><span> (w2, w3)</span><span>):</span></span>
<span><span>    for</span><span> c </span><span>in</span><span> words</span><span>:</span></span>
<span><span>        (prev_max_prob</span><span>,</span><span> prev_state) </span><span>=</span><span> look_back</span><span>[</span><span>t </span><span>-</span><span> 1</span><span>]</span></span>
<span><span>        cur_prob </span><span>=</span><span> prev_max_prob </span><span>*</span><span> trans_prob</span><span>[</span><span>prev_state</span><span>]</span><span>[</span><span>c</span><span>]</span><span> *</span><span> emiss_prob</span><span>[</span><span>c</span><span>]</span><span>[</span><span>pinyin</span><span>]</span></span>
<span><span>        V</span><span>[</span><span>t</span><span>]</span><span>[</span><span>c</span><span>]</span><span> =</span><span> cur_prob</span></span>
<span><span>        if</span><span> cur_prob </span><span>></span><span> look_back</span><span>[</span><span>t</span><span>]</span><span>[</span><span>0</span><span>]</span><span>:</span></span>
<span><span>            look_back</span><span>[</span><span>t</span><span>]</span><span> =</span><span> (cur_prob</span><span>,</span><span> c)</span></span>
<span><span>        count </span><span>+=</span><span> 1</span></span>
<span></span>
<span><span>    # 时刻+1</span></span>
<span><span>    t </span><span>+=</span><span> 1</span></span>
<span></span>
<span><span>print</span><span>(</span><span>f</span><span>"V：</span><span>{</span><span>V</span><span>}</span><span>"</span><span>)</span></span>
<span><span>print</span><span>(</span><span>f</span><span>"最可能的隐状态链：</span><span>{</span><span>[</span><span>c </span><span>for</span><span> (_, c) </span><span>in</span><span> look_back</span><span>]</span><span>}</span><span>"</span><span>)</span></span>
<span><span>print</span><span>(</span><span>f</span><span>"最大概率：</span><span>{</span><span>look_back</span><span>[</span><span>-</span><span>1</span><span>]</span><span>[</span><span>0</span><span>]</span><span>}</span><span>"</span><span>)</span></span>
<span><span>print</span><span>(</span><span>f</span><span>"结果字符串：</span><span>{</span><span>''</span><span>.</span><span>join</span><span>(</span><span>c </span><span>for</span><span> (_, c) </span><span>in</span><span> look_back</span><span>)</span><span>}</span><span>"</span><span>)</span></span>
<span><span>print</span><span>(</span><span>f</span><span>"计算次数：</span><span>{</span><span>count</span><span>}</span><span>"</span><span>)</span></span>
<span></span>
<span><span>"""</span></span>
<span><span>result:</span></span>
<span></span>
<span><span>V：{0: {'你': 0.6, '泥': 0.3, '尼': 0.1}, 1: {'号': 0.06, '好': 0.42, '毫': 0.12}, 2: {'压': 0.042, '鸭': 0.084, '呀': 0.294}}</span></span>
<span><span>最可能的隐状态链：['你', '好', '呀']</span></span>
<span><span>最大概率：0.294</span></span>
<span><span>结果字符串：你好呀</span></span>
<span><span>计算次数：9</span></span>
<span><span>"""</span></span></code></pre><p>可以发现循环的次数比暴力解法要少，维特比算法的时间复杂度为O(T * N<sup>2</sup>)，暴力解法的时间复杂度是O(N<sup>T</sup>)，其中T是时刻数（拼音序列长度），N是隐含状态数（拼音对应汉字数），拼音对应的汉字状态数不会太多，因此当拼音序列越长的时候，暴力解法的时间复杂度就随指数级别增长，相比之下维特比算法就更高效了。
</p><h2 id="user-content-实践中的一些问题" class="">实践中的一些问题<a class="" tabindex="-1" href="#实践中的一些问题">#</a></h2><p>以上已经实现了一个维特比算法的核心部分，但是如果真的要做一个可以用的输入法程序，还是存在不少实践上的问题的，未来有空可以再单独写一些文章来谈一谈。
</p><h3 id="user-content-语料处理问题">语料处理问题<a class="" tabindex="-1" href="#语料处理问题">#</a></h3><ol><li><p>注音不准确
</p><p>  直接从互联网上获取的语料，如维基百科文本、百度贴吧文本，这些中文文本本身是没有拼音标注的，人工一个个去标未免太麻烦，这里可以利用一些汉字转拼音的程序去做，但是有一些多音字比较麻烦，程序生成的未必是正确读音。
</p></li><li><p>语料文本太杂
</p><p>  原始语料里可能存在英文、阿拉伯数字混杂的情况，如果直接不处理它们，难免会造成失真，例如“今天8号”，去掉阿拉伯数字，就变成“号”紧跟在“天”字后面了，影响了转换概率的计算。如何去处理它们又是个复杂的问题，例如混在中文中的时间“1070-1138年”、“12:59分”，需要想个办法妥善处理。
</p></li></ol><h3 id="user-content-真实世界的输入">真实世界的输入<a class="" tabindex="-1" href="#真实世界的输入">#</a></h3><p>在算法的核心部分，我们一直把输入的拼音当作一个序列，序列中的每一个元素刚好是一个单字的拼音，然而实际输入法中，用户输入一一串字符串，如“jintiantianqibucuo”，还需要有个办法能把拼音拆分开，拆分的时候也有问题，如“tian”是一个整体还是“ti”和“an”两个拼音呢？
</p><h3 id="user-content-与时俱进">与时俱进<a class="" tabindex="-1" href="#与时俱进">#</a></h3><p>互联网的发展也促进了语言文字的发展，每隔一段时间都会涌现一批新的“热门词条”，是不是要不停地更新语料？语料库越来越大，训练出来的模型参数越来越大，是不是磁盘空间占用，程序的内存占用也会水涨船高？模型的精度是否随着语料库的增长而线性增长？
</p><h3 id="user-content-计算问题">计算问题<a class="" tabindex="-1" href="#计算问题">#</a></h3><p>最后一个问题是计算的平滑处理。在维特比算法中，浮点数的计算是一个非常重要的部分。由于计算机的存储精度有限，因此在计算转移概率时需要进行平滑处理，以避免数值上溢或下溢的问题。
</p><p>除了计算机的存储精度有限之外，还有一些其他原因也会导致计算平滑处理的必要性。
</p><p>一方面，语言是非常复杂和多变的，不可能完全覆盖所有情况。当我们遇到一些新词或者生僻字时，由于缺乏足够的数据来支持计算，就会出现概率值为0的情况。这时候如果不进行平滑处理，就会导致整个模型的预测结果出现很大的偏差。
</p><p>另一方面，语料库的大小和质量也会影响到计算平滑处理的必要性。如果我们的语料库比较小，很容易就会出现数据稀疏的情况，导致一些概率值为0。而如果我们的语料库质量较差，比如存在大量的噪声和错误数据，那么这些错误数据也会影响到模型的精度，因此需要进行平滑处理来弥补这些问题。
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>使用Nix Flake构建可重现系统</title>
          <link>https://elliot00.com/posts/reproducible-system-with-nix-flake</link>
          <description>这篇文章讨论了 NixOS 的可复现性问题。NixOS 虽然标榜自己是可复现的，但实际上却受到  nix channel  的影响，使得相同的配置在不同的环境下可能会产生不同的结果。为了解决这个问题，Nix 引入了 flakes 机制，允许用户显式声明依赖的版本，从而保证可复现性。文章还介绍了如何将 NixOS 配置转换为 flake 版本，以及如何在 flake 中定义多个系统配置。最后，文章还讨论了如何使用 flake 来创建可复现的开发环境。</description>
          <pubDate>Sat, 08 Apr 2023 12:24:49 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <p>在之前的<a href="https://elliot00.com/posts/nix-note">介绍Nix的文章</a>里，我提到了如何使用nix代码管理NixOS系统配置。通过函数式语言来描述窗口管理、系统软件包、字体等等，可以说每个NixOS的用户都有一个个人专属的定制化Linux发行版，相同的配置可以复现出同样的结果，但是现在我要说，NixOS并不是真正或者并不是完全可复现的。
</p><p><strong>Reproducible</strong>​可是写在Nix官网上的三大特性中的第一个，难道说它虚假宣传了吗？
</p><h2 id="user-content-nix-channels" class="">Nix channels<a class="" tabindex="-1" href="#nix-channels">#</a></h2><p>要搞清楚这个问题，我们需要先看一下​<code class="">nix channels</code>​这个东西。首先任何人都可以用Nix语言写软件包、NixOS模块，而官方有一个超大的git仓库叫做<a href="https://github.com/NixOS/nixpkgs">nixpkgs</a>，里面集合了超过八万个软件包以及所有的NixOS模块，那么怎么指示Nix使用哪个软件包仓库，以及使用这个仓库的哪个版本呢？就是通过“channel”了，channel其实就是指向某个git仓库的分支，例如​<code class="">nixpkgs-unstable</code>​这个channel就是指向了官方nixpkgs仓库的nixpkgs-unstable分支。如果这个分支上新增了很多commit，例如更新了某些包，修复了一些bug，我们就可以使用​<code class="">nix-channel --update</code>​来更新channel，获取该分支最新的版本，再重新​<code class="">nixos-rebuild</code>​来升级我们的系统。
</p><p>现在假设我有一台NixOS的机器使用的channel是​<code class="">nixos-unstable</code>​，我购买了一台新机器，我希望重用原有的Nix配置，但是我在这台新设备上设置channel时使用了​<code class="">nixos-21.11</code>​，虽然我使用了相同的配置，但是channel指向的分支不同，得到的包版本也大概率不一样了；退一步说，即使两台机器都使用nixos-unstable这个channel，但是添加时间不同，对应的同一个分支上的commit也就不同，比如旧机器上的channel还停留在该分支一个月前的版本，那么显然同一份配置得到的结果也不同了。
</p><p>这个问题想必用过Python的包管理工具pip的朋友会很熟悉，如果一个开源的Python程序没有提供一份​<code class="">requirements.txt</code>​来声明依赖版本的话，即使根据代码使用pip安装所有需要的依赖，这个程序也未必能跑起来，因为你现在用pip安装的某个Python包可能比作者当时安装的高了一个大版本，带来了一些不兼容的改动。由此可见，影响可复现性的关键点在哪里呢？在于依赖的版本没有被显式地声明。Nix纯函数式的环境被channel这个副作用给污染了。
</p><h2 id="user-content-nix-flakes" class="">Nix flakes<a class="" tabindex="-1" href="#nix-flakes">#</a></h2><p>我们已经知道问题出在哪里了，那么要怎么解决问题呢？一些现代的编程语言的包管理工具已经给出了答案，很简单，就是​<strong>显式声明依赖版本</strong>​。为此，Nix引入了一个称为flakes的机制。这个机制让我们可以通过一个​<code class="">flake.nix</code>​文件来声明依赖：
</p><pre tabindex="0"><code><span><span>{</span></span>
<span><span>  inputs</span><span> =</span><span> {</span></span>
<span><span>    nixpkgs</span><span>.</span><span>url</span><span> =</span><span> "</span><span>github:NixOS/nixpkgs/nixos-22.11</span><span>"</span><span>;</span></span>
<span><span>  };</span></span>
<span></span>
<span><span>  outputs</span><span> =</span><span> { </span><span>self</span><span>,</span><span> nixpkgs</span><span>,</span><span> ... </span><span>}@</span><span>inputs</span><span>: {</span></span>
<span><span>    nixosConfigurations</span><span>.</span><span>"</span><span>host-name</span><span>"</span><span> =</span><span> nixpkgs</span><span>.</span><span>lib</span><span>.</span><span>nixosSystem</span><span> {</span></span>
<span><span>      system</span><span> =</span><span> "</span><span>x86_64-linux</span><span>"</span><span>;</span></span>
<span><span>      modules</span><span> =</span><span> [</span></span>
<span><span>        ./configuration.nix</span></span>
<span><span>      ]</span><span>;</span></span>
<span><span>    };</span></span>
<span><span>  };</span></span>
<span><span>}</span></span></code></pre><p>这个代码声明了一个set，其中最重要的有两个属性，一个是​<strong>inputs</strong>​，这个属性就是用来声明依赖的，可以是某个本地仓库的路径，也可以是某个仓库的链接，可以指定git的tag、分支或commit hash，详细的格式可以参考<a href="https://nixos.org/manual/nix/stable/command-ref/new-cli/nix3-flake.html#flake-references">官方手册</a>。
</p><p>另一个重要的属性是​<strong>outputs</strong>​，outputs是一个函数，输入就是inputs声明的flakes依赖，输出是一个set，可以包含任意属性，但是有一些特殊属性会被一些nix子命令用到，例如要build的包，NixOS模块等。
</p><p><code class="">flake.nix</code>​目前必须被包含在git仓库里，每一个包含flake.nix的仓库又可以作为inputs被其他flake引用。除了​<code class="">flake.nix</code>​文件，Nix还会自动生成一个​<code class="">flake.lock</code>​文件，里面包含一些元信息，它会将inputs锁定在一个具体的版本。
</p><p>flakes目前是一个实验特性，要想在NixOS中使用，首先需要修改​<code class="">configuration.nix</code>​，然后再跑一遍​<code class="">sudo nixos-rebuild switch</code>​应用修改：
</p><pre tabindex="0"><code><span><span>{ </span><span>config</span><span>,</span><span> pkgs</span><span>,</span><span> lib</span><span>,</span><span> ... </span><span>}:</span></span>
<span></span>
<span><span>{</span></span>
<span><span>  # ...</span></span>
<span></span>
<span><span>  nix</span><span> =</span><span> {</span></span>
<span><span>    extraOptions</span><span> =</span><span> ''</span></span>
<span><span>      experimental-features = nix-command flakes</span></span>
<span><span>    ''</span><span>;</span></span>
<span><span>  };</span></span>
<span><span>}</span></span></code></pre><p>现在可以将NixOS的配置也改写成flake版本了，首先要将上面的flake.nix文件放到配置目录​<code class="">/etc/nixos</code>​目录下，将配置目录初始化为git仓库，​<code class="">git add flake.nix</code>​，接着运行​<code class="">nix flake update</code>​，这会生成或更新（当前没有就生成）一个​<code class="">flake.lock</code>​文件，最后rebuild的命令需要改变一下，使用​<code class="">sudo nixos-rebuild switch --flake '.#'</code>​就可以了。
</p><p>一个flake里面可以定义多个系统配置：
</p><pre tabindex="0"><code><span><span>outputs</span><span> =</span><span> { </span><span>self</span><span>,</span><span> nixpkgs</span><span>,</span><span> ... </span><span>}@</span><span>inputs</span><span>: {</span></span>
<span><span>  # 主机名是john</span></span>
<span><span>  nixosConfigurations</span><span>.</span><span>"</span><span>john</span><span>"</span><span> =</span><span> nixpkgs</span><span>.</span><span>lib</span><span>.</span><span>nixosSystem</span><span> {</span></span>
<span><span>    system</span><span> =</span><span> "</span><span>x86_64-linux</span><span>"</span><span>;</span></span>
<span><span>    modules</span><span> =</span><span> [</span></span>
<span><span>      ./john-configuration.nix</span></span>
<span><span>    ]</span><span>;</span></span>
<span><span>  };</span></span>
<span></span>
<span><span>  # 主机名是paul</span></span>
<span><span>  nixosConfigurations</span><span>.</span><span>"</span><span>paul</span><span>"</span><span> =</span><span> nixpkgs</span><span>.</span><span>lib</span><span>.</span><span>nixosSystem</span><span> {</span></span>
<span><span>    system</span><span> =</span><span> "</span><span>x86_64-linux</span><span>"</span><span>;</span></span>
<span><span>    modules</span><span> =</span><span> [</span></span>
<span><span>      ./paul-configuration.nix</span></span>
<span><span>    ]</span><span>;</span></span>
<span><span>  };</span></span>
<span><span>}</span><span>;</span></span></code></pre><p>命令行参数指明需要build哪一个：
</p><pre tabindex="0"><code><span><span>$</span><span> sudo nixos-rebuild switch --flake </span><span>'</span><span>.#</span><span>'</span><span> # 默认build当前主机名</span></span>
<span><span>$</span><span> sudo nixos-rebuild switch --flake </span><span>'</span><span>.#paul</span><span>'</span><span> # 指定build</span></span></code></pre><p>如果要在另一台机器上复现当前配置，只需要clone当前的配置仓库，保证在同一个commit，那么根据同样的flake.lock，就可以保证使用的是同一个版本的软件源，得到同样的NixOS配置了。
</p><h2 id="user-content-可复现的开发环境" class="">可复现的开发环境<a class="" tabindex="-1" href="#可复现的开发环境">#</a></h2><p><code class="">nix-shell</code>​是Nix生态中一个非常强大的命令，可以用来开启一个由Nix声明的隔离的shell环境。和前面提到的一样，nix-shell也受channel影响，使得nix-shell可能在不同环境下生成不同结果，现在也可以通过flake来改进这一点。
</p><p>下面是一个来自<a href="https://github.com/oxalica/rust-overlay">rust-overlay</a>的flake.nix配置：
</p><pre tabindex="0"><code><span><span>{</span></span>
<span><span>  description</span><span> =</span><span> "</span><span>A devShell example</span><span>"</span><span>;</span></span>
<span></span>
<span><span>  inputs</span><span> =</span><span> {</span></span>
<span><span>    nixpkgs</span><span>.</span><span>url</span><span>      =</span><span> "</span><span>github:NixOS/nixpkgs/nixos-unstable</span><span>"</span><span>;</span></span>
<span><span>    rust-overlay</span><span>.</span><span>url</span><span> =</span><span> "</span><span>github:oxalica/rust-overlay</span><span>"</span><span>;</span></span>
<span><span>    flake-utils</span><span>.</span><span>url</span><span>  =</span><span> "</span><span>github:numtide/flake-utils</span><span>"</span><span>;</span></span>
<span><span>  };</span></span>
<span></span>
<span><span>  outputs</span><span> =</span><span> { </span><span>self</span><span>,</span><span> nixpkgs</span><span>,</span><span> rust-overlay</span><span>,</span><span> flake-utils</span><span>,</span><span> ... </span><span>}:</span></span>
<span><span>    flake-utils</span><span>.</span><span>lib</span><span>.</span><span>eachDefaultSystem</span><span> (</span><span>system</span><span>:</span></span>
<span><span>      let</span></span>
<span><span>        overlays</span><span> =</span><span> [</span><span> (</span><span>import</span><span> rust-overlay</span><span>) </span><span>]</span><span>;</span></span>
<span><span>        pkgs</span><span> =</span><span> import</span><span> nixpkgs</span><span> {</span></span>
<span><span>          inherit</span><span> system</span><span> overlays</span><span>;</span></span>
<span><span>        };</span></span>
<span><span>      in</span></span>
<span><span>      with</span><span> pkgs</span><span>;</span></span>
<span><span>      {</span></span>
<span><span>        devShells</span><span>.</span><span>default</span><span> =</span><span> mkShell</span><span> {</span></span>
<span><span>          buildInputs</span><span> =</span><span> [</span></span>
<span><span>            openssl</span></span>
<span><span>            pkg-config</span></span>
<span><span>            exa</span></span>
<span><span>            fd</span></span>
<span><span>            rust-bin</span><span>.</span><span>beta</span><span>.</span><span>latest</span><span>.</span><span>default</span></span>
<span><span>          ]</span><span>;</span></span>
<span></span>
<span><span>          shellHook</span><span> =</span><span> ''</span></span>
<span><span>            alias ls=exa</span></span>
<span><span>            alias find=fd</span></span>
<span><span>          ''</span><span>;</span></span>
<span><span>        };</span></span>
<span><span>      }</span></span>
<span><span>    );</span></span>
<span><span>}</span></span></code></pre><p>将这个文件放进git仓库，生成lock文件，然后运行​<code class="">nix develop</code>​，就可以得到一个安装了固定Rust版本的开发环境。
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>优雅地使用Git</title>
          <link>https://elliot00.com/posts/use-git-gracefully</link>
          <description>这篇文章内容与Git的使用相关，包括了提交信息的规范，如何保持清晰的提交历史和修复不规范的提交。文章还介绍了用于协助管理Git的工具和技术，如Git hooks、Git子命令、Git别名、EditorConfig等。最后，文章还提供了日志查询、跟踪空文件夹、处理大文件和克隆仓库等方面的技巧。</description>
          <pubDate>Sun, 11 Sep 2022 04:42:19 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <h2 id="user-content-保持清晰的提交记录" class="">保持清晰的提交记录<a class="" tabindex="-1" href="#保持清晰的提交记录">#</a></h2><h3 id="user-content-统一规范的提交信息">统一规范的提交信息<a class="" tabindex="-1" href="#统一规范的提交信息">#</a></h3><p>Git强制commit必须有一个summary信息，但是并没有要求开发者怎么写，看看以下几种提交历史：
</p><ul><li>随意版
</li></ul><pre tabindex="0"><code><span><span>changed</span></span>
<span><span>bug</span></span>
<span><span>commit</span></span></code></pre><ul><li>较明确版（django-oscar）
</li></ul><pre tabindex="0"><code><span><span>Use nodejs v14 for test builds.</span></span>
<span><span>Read sandbox cache settings from CACHE_URL</span></span></code></pre><ul><li>规范版（Vim）
</li></ul><pre tabindex="0"><code><span><span>patch 9.0.0316: screen flickers when 'cmdheight' is zero</span></span>
<span><span></span></span>
<span><span>Problem:    Screen flickers when 'cmdheight' is zero.</span></span>
<span><span>Solution:   Redraw over existing text instead of clearing.</span></span>
<span><span></span></span>
<span><span></span></span>
<span><span>patch 9.0.0315: shell command is displayed in message window</span></span>
<span><span></span></span>
<span><span>Problem:    Shell command is displayed in message window.</span></span>
<span><span>Solution:   Do not echo the shell command in the message window.</span></span></code></pre><ul><li>规范版（React）
</li></ul><pre tabindex="0"><code><span><span>docs(examples): react-router example</span></span>
<span><span>chore(publish): do not release without changed packages</span></span></code></pre><p>我想大部分开发者应当认同，commit message至少应该描述下本次提交做了些什么，那么相比之下，其实第一种写了等于没写，至少得做到第二种的形式，才能算有用的提交记录。
</p><p>在众多提交信息规范中，由前端框架​<code class="">Angular</code>​团队的提出的<a href="https://github.com/angular/angular/blob/main/CONTRIBUTING.md#-commit-message-format">规范</a>应该是最受欢迎的，该规范将提交summary分成三个部分：​<code class="">header</code>​、​<code class="">body</code>​、​<code class="">footer</code>​，其中​<code class="">header</code>​为必填。
</p><pre tabindex="0"><code><span><span>&#x3C;type>(&#x3C;scope>): &#x3C;subject></span></span>
<span><span>&#x3C;BLANK LINE></span></span>
<span><span>&#x3C;body></span></span>
<span><span>&#x3C;BLANK LINE></span></span>
<span><span>&#x3C;footer></span></span></code></pre><p><code class="">header</code>​包含三个部分：
</p><ul><li><code class="">type</code>​：提交类型，​<code class="">test</code>​、​<code class="">feat</code>​、​<code class="">fix</code>​等
</li><li><code class="">scope</code>​：作用域
</li><li><code class="">subject</code>​：主题，对修改的简述，小写字母开头，现在时态，结尾无句号
</li></ul><p><code class="">body</code>​是对​<code class="">subject</code>​的补充，包括本次修改的动机，与之前行为的对比。
</p><p><code class="">footer</code>​主要是关于​<em>Breaking Changes</em>​的描述或者是关闭某个相关issue
</p><p>这个格式看上去有些复杂，不过可以通过工具辅助完成，例如我曾写的辅助脚本<a href="https://github.com/Eliot00/commit-formatter">commit-formatter</a>。
</p><h3 id="user-content-清理无用的提交信息">清理无用的提交信息<a class="" tabindex="-1" href="#清理无用的提交信息">#</a></h3><h4 id="user-content-amend">amend<a class="" tabindex="-1" href="#amend">#</a></h4><p>有时候完成了​<code class="">git commit</code>​操作，却突然发现有个拼写错误，这时候可以修改后再次提交，但是这样一个小改动没必要多创建一条提交记录（当然这可以通过lint、git-hook避免，但那是另一个问题），这时候可以先将改动的文件加入暂存区，再使用​<code class="">git commit --amend</code>​改写提交，将这次的小改动加入到上次的提交中。这个操作会打开默认编辑器让你编辑提交信息，如果不需要改动提交记录，还可以使用​<code class="">git commit --amend --no-edit</code>​。
</p><h4 id="user-content-squash">squash<a class="" tabindex="-1" href="#squash">#</a></h4><p>有时候我们需要压缩多个提交信息到一个，例如在开发某个功能时，对一个小范围改动产生了多次不必要提交，或者在参与开源项目时，我们需要基于自己的分支提交PR，而Reviewer对我们提出了一些改动意见。这时候可以使用​<code class="">squash</code>​。
</p><p>例如有如下提交：
</p><pre tabindex="0"><code><span><span>* 65e76f2 - (HEAD -> test) type</span></span>
<span><span>* 3334086 - typo</span></span>
<span><span>* d834363 - feat: previewImage support zoom</span></span></code></pre><p>这时使用命令​<code class="">git rebase -i HEAD~3</code>​，会在终端打开默认编辑器：
</p><pre tabindex="0"><code><span><span>pick d834363 feat: previewImage support zoom</span></span>
<span><span>pick 3334086 typo</span></span>
<span><span>pick 65e76f2 type</span></span>
<span><span></span></span>
<span><span># Commands:</span></span>
<span><span>#  p, pick = use commit</span></span>
<span><span>#  r, reword = use commit, but edit the commit message</span></span>
<span><span>#  e, edit = use commit, but stop for amending</span></span>
<span><span>#  s, squash = use commit, but meld into previous commit</span></span>
<span><span>#  f, fixup = like "squash", but discard this commit's log message</span></span>
<span><span>#  x, exec = run command (the rest of the line) using shell</span></span></code></pre><p>每个提交信息前有个单词​<code class="">pick</code>​，在下面的注释中，解释了​<code class="">pick</code>​以前其他单词的意义，可以看到​<code class="">s</code>​或​<code class="">squash</code>​的意义为保留信息但合并进上一个提交，现在编辑后面两个​<code class="">pick</code>​，改成：
</p><pre tabindex="0"><code><span><span>pick d834363 feat: previewImage support zoom</span></span>
<span><span>s 3334086 typo</span></span>
<span><span>s 65e76f2 type</span></span></code></pre><p>保存并确定后，再次使用​<code class="">git log</code>​查看提交历史，可以发现三次commit信息被合并了。
</p><h3 id="user-content-使用rebase同步">使用rebase同步<a class="" tabindex="-1" href="#使用rebase同步">#</a></h3><p>有时候，一些项目的提交历史混乱的原因可能是开发者使用了不恰当的操作，例如只知道对远端分支使用​<code class="">pull</code>​和​<code class="">push</code>​。
</p><p>应该有很多人在使用​<code class="">git pull</code>​时见过这个警告：
</p><pre tabindex="0"><code><span><span>warning: Pulling without specifying how to reconcile divergent branches is</span></span>
<span><span>discouraged. You can squelch this message by running one of the following</span></span>
<span><span>commands sometime before your next pull:</span></span>
<span><span></span></span>
<span><span>  git config pull.rebase false  # merge (the default strategy)</span></span>
<span><span>  git config pull.rebase true   # rebase</span></span>
<span><span>  git config pull.ff only       # fast-forward only</span></span></code></pre><p>现在假设A和B在同一个dev分支上开发，A修改了代码并创建提交commit1，通过​<code class="">git push</code>​推送到了服务器，这时B在本地也创建了commit2，他使用​<code class="">git push</code>​就会收到报错，因为B没有同步远端dev分支最新的更改。
</p><p><img alt="branch" src="https://wac-cdn.atlassian.com/dam/jcr:63e58c34-b273-4e48-a6b1-6e3ba4d4a0ea/01%20bubble%20diagram-01.svg"></p><blockquote><p>图片来自<a href="https://www.atlassian.com/git/tutorials/syncing/git-pull">Gitbucket</a></p></blockquote><p>此时如果他pull远端分支，就会产生一个额外的合并commit。为什么呢？实际上，这里的pull操作就等价于​<code class="">git fetch &#x3C;remote> &#x26;&#x26; git merge &#x3C;remote>/branch</code>​，将远端的分支修改下载到本地，然后合并到本地分支。
</p><p><img alt="pull" src="https://wac-cdn.atlassian.com/dam/jcr:0269bb2d-eb7f-43d8-80a2-8afa88d11eea/02%20bubble%20diagram-02.svg"></p><p>怎么避免这个merge提交呢？可以使用​<code class="">git pull --rebase</code>​，
</p><p><img alt="pull --rebase" src="https://wac-cdn.atlassian.com/dam/jcr:d5633068-d448-4140-953e-2ab31553ce10/03%20bubble%20diagram-03-updated@2x%20kopiera.png"></p><p>rebase看上去像是先将本地的提交先拿出来，再插到另一个分支的最顶端去，这样就得到了一条线性的提交历史。注意图中原本本地的E F G变成了E' F' G'，后面会提到。
</p><p>回看前面的警告，通过​<code class="">git config pull.rebase true</code>​可以设置默认的pull操作为​<code class="">git pull --rebase</code>​。
</p><p>同样的，对于同一个机器上的不同分支，其实也可以用​<code class="">git rebase other-branch</code>​操作来代替merge。
</p><h4 id="user-content-rebase的黄金法则">rebase的黄金法则<a class="" tabindex="-1" href="#rebase的黄金法则">#</a></h4><p>rebase操作有一个黄金法则：​<strong>不要在共享分支使用rebase！</strong></p><p>或许就因为这个法则，让一些程序员不敢使用rebase。那么，rebase在什么情况下危险呢？
</p><p>正如前面提到的，本地的提交，经过rebase之后，实际上是生成了内容一样的新提交，E‘ F' G'的hash与原来的E F G是不一样的。假设现在分支情况如下：
</p><div>A -> B -> C # remote/dev

A -> B -> C # 甲/dev
A -> B -> D -> E # 甲/feature

A -> B -> C -> F # 乙/dev
</div><p>如果甲在本地的dev分支rebase了feature：
</p><div>A -> B -> C # remote/dev

A -> B -> D -> C' # 甲/dev
A -> B -> D # 甲/feature

A -> B -> C -> F # 乙/dev
</div><p>接着甲要push本地的dev到远端，麻烦来了，甲本地的dev和远端在B之后就对不上了，如果甲不管不顾，使用​<code class="">git push --force</code>​，这下乙要push他本地的改动将会遇到报错，乙使用​<code class="">git pull</code>​，Git会尝试合并分支：
</p><div>A -> B -> D -> C'
    |         /
    |        /
    -> C -> F  ---> M
</div><p>如果所有人都像甲一样操作，那这个共享的dev分支最后会变得非常混乱。
</p><p>但是如果是像前面提到的，甲本地的dev是​<code class="">A -> B -> C -> D</code>​，远端原本是​<code class="">A -> B -> C</code>​，经过乙push后变成​<code class="">A -> B -> C -> E</code>​，甲使用​<code class="">git pull --rebase</code>​是没有问题的，这时本地变成了​<code class="">A -> B -C -> E -> D'</code>​，为什么这个操作是安全的呢？这里远端的dev分支是共享的，但是本地的dev可以视作私有的分支，​<code class="">git pull --rebase</code>​相当于rebase了远端的dev分支，最后push的结果其实是向远端push了一个新的提交，这时乙再使用​<code class="">git pull</code>​后的结果就是​=A -> B -> C -> E -> D' =​。
</p><p>再比如，在Github上fork一个仓库，checkout一个​<code class="">dev</code>​分支做了一些更改后创建了一个PR，虽然这个​<code class="">dev</code>​分支在一个公开的代码托管平台上，所有人都可以看到，但是它只是为了最终合并进目标仓库的主线而建立的，仍然可以视为私有分支，在这个PR被合并前，可以通过rebase同步目标主分支的改动，用squash压缩提交信息，这些都是安全操作。
</p><p>综上，安全使用amend、squash、rebase等操作的前提就是，​<strong>不要改动已经共享了的提交</strong>​，如果将共享的远端分支上的​<code class="">A -> B -C</code>​变成​<code class="">A -> B -> D -> F</code>​，那就会造成混乱了。
</p><h2 id="user-content-辅助工具" class="">辅助工具<a class="" tabindex="-1" href="#辅助工具">#</a></h2><h3 id="user-content-git-hooks">Git hooks<a class="" tabindex="-1" href="#git-hooks">#</a></h3><p>Git提供了hook机制，可以在特定事件前后触发特定操作。例如，在代码提交前检查测试覆盖率，检查代码格式化等等。Python的开源工具<a href="https://pre-commit.com/">pre-commit</a>就提供了很多好用的Hooks。
</p><h3 id="user-content-git子命令">Git子命令<a class="" tabindex="-1" href="#git子命令">#</a></h3><p>如果你为Git写了一个扩展脚本，那么你可以用​<code class="">git-foo</code>​来命名你的可执行文件，Git允许你使用​<code class="">git boo</code>​的子命令形式调用自定义脚本。
</p><h3 id="user-content-git别名">Git别名<a class="" tabindex="-1" href="#git别名">#</a></h3><p>可以为一些常用且比较长的命令配置一个短的别名，例如：
</p><pre tabindex="0"><code><span><span># 快速commit</span></span>
<span><span>git</span><span> config</span><span> --global</span><span> alias.cm</span><span> '</span><span>commit -m</span><span>'</span></span>
<span></span>
<span><span># 简洁美观的日志</span></span>
<span><span>git</span><span> config</span><span> --global</span><span> alias.lg</span><span> "</span><span>log --graph --pretty=format:'%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)&#x3C;%an>%Creset'</span><span>"</span></span>
<span></span>
<span><span># 搜索commit</span></span>
<span><span>git</span><span> config</span><span> --global</span><span> alias.se</span><span> '</span><span>!git rev-list --all | xargs git grep -F</span><span>'</span></span></code></pre><h3 id="user-content-editorconfig">EditorConfig<a class="" tabindex="-1" href="#editorconfig">#</a></h3><p>不同的编辑器/IDE都会有自己的项目配置文件，如JetBrains系列的​<code class="">.idea</code>​，VSCode的​<code class="">.vscode</code>​，我个人认为这种文件不应该提交到公共仓库里，因为不应该强制所有开发者使用相同的工具（Android开发这类与IDE高度绑定的项目也许是例外）。
</p><p>那这时候怎么保证不同开发者使用不同的编辑器，同时保持统一的代码风格呢？一个办法是使用前面提到的git hooks，在提交前做格式化；另一个办法就是使用<a href="https://editorconfig.org/">EditorConfig</a>，在项目里放置一个​<code class="">.editorconfig</code>​文件，配置缩进、换行符等，基本上主流编辑器都会尊重这个配置。
</p><pre tabindex="0"><code><span><span>root</span><span> =</span><span> true</span></span>
<span></span>
<span><span>[*]</span></span>
<span><span>charset</span><span> =</span><span> u</span><span>tf-8</span></span>
<span><span>indent_style</span><span> =</span><span> s</span><span>pace</span></span>
<span><span>indent_size</span><span> =</span><span> 2</span></span>
<span><span>end_of_line</span><span> =</span><span> l</span><span>f</span></span>
<span><span>insert_final_newline</span><span> =</span><span> true</span></span>
<span><span>trim_trailing_whitespace</span><span> =</span><span> true</span></span>
<span></span>
<span><span>[*.md]</span></span>
<span><span>insert_final_newline</span><span> =</span><span> false</span></span>
<span><span>trim_trailing_whitespace</span><span> =</span><span> false</span></span></code></pre><h2 id="user-content-杂项" class="">杂项<a class="" tabindex="-1" href="#杂项">#</a></h2><h3 id="user-content-日志查询">日志查询<a class="" tabindex="-1" href="#日志查询">#</a></h3><p>Git命令行提供了一些选项去快速查找提交：
</p><ul><li>根据commit信息查找：​<code class="">git log --all --grep='&#x3C;pattern>'</code>​
</li><li>根据提交人查找：​<code class="">git log --committer=&#x3C;pattern></code>​
</li><li>根据日期：​<code class="">git log --since=&#x3C;date></code>​、​<code class="">git log --before=&#x3C;date></code>​
</li></ul><p>更多查询条件，可以查看<a href="https://www.git-scm.com/docs/git-log">官方文档</a>。
</p><h3 id="user-content-追踪空文件夹">追踪空文件夹<a class="" tabindex="-1" href="#追踪空文件夹">#</a></h3><p>Git本身是不能追踪空的目录的，但是有时候确实会有需要将一个空目录放到仓库的需求，这时可以在这个目录下放一个空的​<code class="">.gitkeep</code>​文件，这个文件名只是一个命名惯例，并没有特殊意义，接下来要去修改​<code class="">.gitignore</code>​文件：
</p><pre><code class="language-gitignore"># 应该忽略的目录
/foo

# 排除.gitkeep文件
!.gitkeep
</code></pre><p>这样就可以让Git忽略该目录下除了​<code class="">.gitkeep</code>​外所有文件，但是保留这个目录。
</p><h3 id="user-content-大文件">大文件<a class="" tabindex="-1" href="#大文件">#</a></h3><h4 id="user-content-lfs">LFS<a class="" tabindex="-1" href="#lfs">#</a></h4><p>Git是为文本文件设计的，但是有时需要在仓库中放一些大的二进制文件，如图片、音频等设计资源，这会让仓库体积变得庞大，如果二进制文件变更，变更历史也会变得很大，要解决这个问题，就可以用LFS（Large File Storage）扩展，简单说就是它允许将大文件保存在另外的仓库，在本地保留一个指针。详情见<a href="https://github.com/git-lfs/git-lfs">LFS</a></p><h4 id="user-content-gc">gc<a class="" tabindex="-1" href="#gc">#</a></h4><p><code class="">git gc</code>​命令可以帮助清理Git数据库中不需要的文件，减少磁盘占用，在<a href="https://github.com/NixOS/nixpkgs">nixpkgs</a>这样有着巨量提交的大型仓库上工作时这个命令很有用。
</p><h4 id="user-content-只需要最近的一次提交">只需要最近的一次提交<a class="" tabindex="-1" href="#只需要最近的一次提交">#</a></h4><p>有时我们暂时只需要一个仓库最新的代码，不需要所有的Git提交历史，那么可以使用​<code class="">git clone --depth 1 repo-url</code>​来克隆仓库，这可以节省下载时间和本地磁盘占用。
</p><h3 id="user-content-删除未追踪文件">删除未追踪文件<a class="" tabindex="-1" href="#删除未追踪文件">#</a></h3><blockquote><p>2024/11/01添加
</p></blockquote><p>某次我在git仓库下执行了一个批量重命名文件的操作，发现不小心敲错了文件名，这时使用​<code class="">git restore</code>​可以快速恢复原文件，但是产生的错误文件并没有被清理掉，这时可以用​<code class="">git clean</code>​清理未被追踪的文件。
</p><p>默认情况下直接​<code class="">git clean</code>​会被拒绝，需要用​<code class="">git clean -i</code>​交互式处理或​<code class="">git clean -f</code>​强制删除，也可以先用​<code class="">git clean -n</code>​查看哪些文件会被删除。
</p><h3 id="user-content-二分查找定位问题">二分查找定位问题<a class="" tabindex="-1" href="#二分查找定位问题">#</a></h3><blockquote><p>2024/11/01添加
</p></blockquote><p>如果需要在git仓库里确认一个bug具体是什么时候引入的，可以使用​<code class="">git bisect</code>​命令，比如某个应用的新版本距离上个版本有100个commit，新版出现了一个bug，但不知道是从哪次commit开始有的，可以这样操作：
</p><pre tabindex="0"><code><span><span>git</span><span> bisect</span><span> start</span><span> &#x3C;</span><span>当前commi</span><span>t</span><span>></span><span> &#x3C;</span><span>上一版确定没问题的commi</span><span>t</span><span>></span></span>
<span></span>
<span><span># 这样仓库会跳到两次提交中间的一次提交上</span></span>
<span></span>
<span><span># 编译运行应用，如果没问题</span></span>
<span><span>git</span><span> bisect</span><span> good</span></span>
<span></span>
<span><span># 如果有问题</span></span>
<span><span>git</span><span> bisect</span><span> bad</span></span>
<span></span>
<span><span># 看名字就知道，这个命令是一个二分查找的过程</span></span>
<span><span># 重复上述标记good或bad的步骤，如果找到了</span></span>
<span><span># git会提示 ‘xxx is the first bad commit’</span></span>
<span><span># 此时可以退出</span></span>
<span><span>git</span><span> bisect</span><span> reset</span></span></code></pre>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>关于TypeScript结合React开发的一些技巧</title>
          <link>https://elliot00.com/posts/typescript-react-tips</link>
          <description>这篇文章介绍了作者在前端项目中使用 TypeScript 与 React 的一些经验。文章首先推荐使用 TypeScript 的自动推断功能，这样可以减少一些类型标注的工作。然后介绍了一些 TypeScript 的工具类型，如 Omit 和 Pick，这些工具类型可以帮助我们重用类型定义，避免重复工作。接着，文章讨论了在 TypeScript 中引入不明确性的问题，并推荐使用 declare 全局声明和声明合并来解决这个问题。此外，文章还介绍了 useRef 的类型，以及如何使用类型收窄和条件渲染来实现更简洁的代码。最后，文章给出了如何解决项目中大量的条件渲染问题的一些建议，例如使用组件工厂或组合。</description>
          <pubDate>Sat, 19 Mar 2022 11:15:36 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <p>TypeScript是JavaScript的超集，为JS带来了静态类型支持，这可以帮助我们写出更清晰可靠的接口，带来更好的IDE提示。在前端项目中使用TypeScript与React的组合已经有一段时间了，是时候写一篇博客总结分享一下。下面就列举一些个人觉得在做项目中有帮助的点。
</p><h2 id="user-content-利用自动推断" class="">利用自动推断<a class="" tabindex="-1" href="#利用自动推断">#</a></h2><p><code class="">TypeScript</code>​具有一定的推断类型的能力，一些情况下可以让程序员偷个懒，少写点类型。
</p><pre tabindex="0"><code><span><span>// 无需标注变量类型</span></span>
<span><span>const</span><span> foo</span><span> =</span><span> '</span><span>123</span><span>'</span><span> // string</span></span>
<span><span>const</span><span> bar</span><span> =</span><span> 123</span><span> // number</span></span>
<span><span>const</span><span> baz</span><span> =</span><span> str</span><span>.</span><span>match</span><span>(</span><span>/</span><span>[</span><span>A-Z</span><span>]</span><span>/</span><span>)</span><span> // RefExpMatchArray | null</span></span></code></pre><p>将箭头函数传递给组件props时，如果props具有类型，箭头函数就不需要标注类型:
</p><pre tabindex="0"><code><span><span>type</span><span> FieldProps</span><span> =</span><span> {</span></span>
<span><span>  onChange</span><span>:</span><span> (</span><span>value</span><span>:</span><span> number</span><span>)</span><span> =></span><span> void</span></span>
<span><span>}</span></span>
<span></span>
<span><span>&#x3C;</span><span>Field</span><span> onChange</span><span>=</span><span>{value </span><span>=></span><span> setValue</span><span>(value)} /></span></span></code></pre><p>另外，千万不要忘记TS/JS中​<strong>函数可是一等公民</strong>​，例如需要写一个给数字隔三位加上分隔符的函数，发现有一个​<code class="">Intl</code>​模块可以帮上忙：
</p><pre tabindex="0"><code><span><span>// 并不需要这要做</span></span>
<span><span>function</span><span> numberWithDelimiter</span><span>(</span><span>raw</span><span>:</span><span> number</span><span>)</span><span>:</span><span> string</span><span> {</span></span>
<span><span>  return</span><span> new</span><span> Intl</span><span>.</span><span>NumberFormat</span><span>(</span><span>'</span><span>en-US</span><span>'</span><span>)</span><span>.</span><span>format</span><span>(raw)</span></span>
<span><span>}</span></span>
<span></span>
<span><span>// 只要一个简单的赋值就行，numberWithDelimiter类型与format方法完全一致</span></span>
<span><span>const</span><span> numberWithDelimiter</span><span> =</span><span> new</span><span> Intl</span><span>.</span><span>NumberFormat</span><span>(</span><span>'</span><span>en-US</span><span>'</span><span>)</span><span>.</span><span>format</span></span></code></pre><h2 id="user-content-工具类型" class="">工具类型<a class="" tabindex="-1" href="#工具类型">#</a></h2><p>如果在项目中引用了一些第三方组件，在所有使用的地方有一些共同的属性，例如​<code class="">&#x3C;Input type="number" .../></code>​，所有的​<code class="">type</code>​都要固定为number，我们一般要抽取一个组件出来：
</p><pre tabindex="0"><code><span><span>type</span><span> MyInputProps</span><span> =</span><span> {</span></span>
<span><span>  value</span><span>:</span><span> number</span></span>
<span><span>  onChange</span><span>:</span><span> (</span><span>value</span><span>:</span><span> number</span><span>)</span><span> =></span><span> void</span></span>
<span><span>}</span></span>
<span></span>
<span><span>const</span><span> MyInput</span><span>:</span><span> React</span><span>.</span><span>FC</span><span>&#x3C;</span><span>MyInputProps</span><span>></span><span> =</span><span> props</span><span> =></span><span> {</span></span>
<span><span>  return</span><span> &#x3C;</span><span>Input</span><span> type</span><span>=</span><span>"</span><span>number {...props} /></span></span>
<span><span>}</span></span></code></pre><p>这在本质上就是写一个​<em>偏函数</em>​，但是如果引用的第三方库有完备的类型声明，这样写就把第三方库原有的类型重写了一遍，可以这样声明组件props类型来节省代码：
</p><pre tabindex="0"><code><span><span>import</span><span> type</span><span> { InputProps } </span><span>from</span><span> '</span><span>lib</span><span>'</span></span>
<span></span>
<span><span>type</span><span> MyInputProps</span><span> =</span><span> Omit</span><span>&#x3C;</span><span>InputProps</span><span>,</span><span> "</span><span>type</span><span>"</span><span>></span></span></code></pre><p><code class="">Omit</code>​是TS的一个​<strong>工具类型</strong>​，作用就像其名称显示地那样，从一个类型中，剔除一些属性。在上述例子里，使用Omit避免了重复声明第三方已有的类型。TS还有很多有用的<a href="https://www.typescriptlang.org/docs/handbook/utility-types.html">工具类型</a>，如​<code class="">Pick</code>​从已有类型中提取部分属性组成新类型，​<code class="">NonNullable</code>​剔除​<code class="">null</code>​与​<code class="">undefined</code>​，​<code class="">Parameters</code>​和​<code class="">ReturnType</code>​获取函数参数与返回值类型等，可以根据需要灵活运用。
</p><h2 id="user-content-适当引入一些不明确性" class="">适当引入一些不明确性<a class="" tabindex="-1" href="#适当引入一些不明确性">#</a></h2><blockquote><p>Explicit is better than implicit.
</p></blockquote><p>以上这句话出自​<em>The Zen of Python</em>​，那具体什么是​<code class="">explicit</code>​什么是​<code class="">implicit</code>​呢？以​<code class="">Ant Design Pro</code>​为例，其提供了一种注入全局变量的功能：
</p><pre tabindex="0"><code><span><span>export</span><span> default</span><span> defineConfig</span><span>({</span></span>
<span><span>  define</span><span>:</span><span> {</span></span>
<span><span>    API_URL</span><span>:</span><span> '</span><span>https://api-test.xxx.com</span><span>'</span><span>,</span><span> // API地址</span></span>
<span><span>    API_SECRET_KEY</span><span>:</span><span> '</span><span>XXXXXXXXXXXXXXXX</span><span>'</span><span>,</span><span> // API调用密钥</span></span>
<span><span>  }</span><span>,</span></span>
<span><span>});</span></span></code></pre><p>这样定义的变量无需​<code class="">import</code>​就可以直接使用，如果团队所有开发者都熟悉这个特性倒也没事，但是如果团队加入了新成员，看到某处代码使用了这种全局变量并且IDE还无法跳转到定义处，想必是要怀疑人生了。相比之下我更喜欢在需要处明确声明要引入的内容。
</p><p>但是，在​<code class="">TypeScript</code>​也存在一些可以容忍的不明确的性质。
</p><h3 id="user-content-declare全局声明">declare全局声明<a class="" tabindex="-1" href="#declare全局声明">#</a></h3><p>在​<code class="">TypeScript</code>​发展的早期，很多流行的库都是仅使用​<code class="">JavaScript</code>​而没有类型声明的，当时就出现了"d.ts"为后缀的类型定义文件，可以为JS代码增加类型。TS自身就带了一些声明文件，如浏览器全局对象​<code class="">window</code>​的声明：
</p><pre tabindex="0"><code><span><span>// typescript/lib/lib.dom.d.ts</span></span>
<span><span>declare</span><span> var</span><span> window</span><span>:</span><span> Window</span><span> &#x26;</span><span> typeof</span><span> globalThis</span><span>;</span></span></code></pre><p>这个也可以用于为自己的代码声明类型，我习惯这样使用：
</p><pre tabindex="0"><code><span><span>// API.d.ts</span></span>
<span><span>declare</span><span> namespace</span><span> API</span><span> {</span></span>
<span><span>  export</span><span> type</span><span> Production</span><span> =</span><span> {</span></span>
<span><span>    ...</span></span>
<span><span>  }</span></span>
<span><span>}</span></span>
<span></span>
<span><span>// 需要使用的地方无需import</span></span>
<span><span>function</span><span> getProduction</span><span>()</span><span>:</span><span> API</span><span>.</span><span>Production</span></span></code></pre><p>虽然没有明确的import，但是IDE的跳转定义可以正常工作，并且由于只是类型定义，不至于产生意想不到的运行时错误，省去一个import语句我觉得是利大于弊的。可以将所有的接口响应值都封装在一个模块里，如果后端使用了​<code class="">OpenAPI</code>​这类工具，还可以借助一些像<a href="https://www.npmjs.com/package/openapi-typescript">openapi-typescript</a>这样的插件直接为接口导出一份类型文件。
</p><h3 id="user-content-声明合并">声明合并<a class="" tabindex="-1" href="#声明合并">#</a></h3><p>通常我更乐意用​<code class="">type</code>​来声明类型，但有时​<code class="">interface</code>​一个比较“脏”的特性却可以派上用场。
</p><pre tabindex="0"><code><span><span>interface</span><span> User</span><span> {</span></span>
<span><span>  name</span><span>:</span><span> string</span></span>
<span><span>}</span></span>
<span></span>
<span><span>interface</span><span> User</span><span> {</span></span>
<span><span>  age</span><span>:</span><span> number</span></span>
<span><span>}</span></span>
<span></span>
<span><span>// 同名是合法的，同名interface会被合并，等价于</span></span>
<span><span>interface</span><span> User</span><span> {</span></span>
<span><span>  name</span><span>:</span><span> string</span></span>
<span><span>  age</span><span>:</span><span> number</span></span>
<span><span>}</span></span></code></pre><p><code class="">interface</code>​的这个特性，与<a href="https://www.typescriptlang.org/docs/handbook/declaration-merging.html#module-augmentation">module augmentation</a>功能一起，可以帮助库开发者为用户提供更好的可扩展性。例如<a href="https://mui.com">MUI</a>要自定义主题配置：
</p><pre tabindex="0"><code><span><span>import</span><span> *</span><span> as</span><span> React </span><span>from</span><span> '</span><span>react</span><span>'</span><span>;</span></span>
<span><span>import</span><span> { createTheme</span><span>,</span><span> ThemeProvider } </span><span>from</span><span> '</span><span>@mui/material/styles</span><span>'</span><span>;</span></span>
<span><span>import</span><span> Button </span><span>from</span><span> '</span><span>@mui/material/Button</span><span>'</span><span>;</span></span>
<span></span>
<span><span>const</span><span> theme</span><span> =</span><span> createTheme</span><span>(</span><span>{</span></span>
<span><span>  palette</span><span>:</span><span> {</span></span>
<span><span>    neutral</span><span>:</span><span> {</span></span>
<span><span>      main</span><span>:</span><span> '</span><span>#64748B</span><span>'</span><span>,</span></span>
<span><span>      contrastText</span><span>:</span><span> '</span><span>#fff</span><span>'</span><span>,</span></span>
<span><span>    }</span><span>,</span></span>
<span><span>  }</span><span>,</span></span>
<span><span>}</span><span>);</span></span>
<span></span>
<span><span>declare</span><span> module</span><span> '</span><span>@mui/material/styles</span><span>'</span><span> {</span></span>
<span><span>  interface</span><span> Palette</span><span> {</span></span>
<span><span>    neutral</span><span>:</span><span> Palette</span><span>[</span><span>'</span><span>primary</span><span>'</span><span>];</span></span>
<span><span>  }</span></span>
<span></span>
<span><span>  // allow configuration using `createTheme`</span></span>
<span><span>  interface</span><span> PaletteOptions</span><span> {</span></span>
<span><span>    neutral</span><span>?:</span><span> PaletteOptions</span><span>[</span><span>'</span><span>primary</span><span>'</span><span>];</span></span>
<span><span>  }</span></span>
<span><span>}</span></span>
<span></span>
<span><span>// Update the Button's color prop options</span></span>
<span><span>declare</span><span> module</span><span> '</span><span>@mui/material/Button</span><span>'</span><span> {</span></span>
<span><span>  interface</span><span> ButtonPropsColorOverrides</span><span> {</span></span>
<span><span>    neutral</span><span>:</span><span> true</span><span>;</span></span>
<span><span>  }</span></span>
<span><span>}</span></span>
<span></span>
<span><span>export</span><span> default</span><span> function</span><span> CustomColor</span><span>()</span><span> {</span></span>
<span><span>  return</span><span> (</span></span>
<span><span>    &#x3C;</span><span>ThemeProvider</span><span> theme</span><span>=</span><span>{</span><span>theme</span><span>}</span><span>></span></span>
<span><span>      &#x3C;</span><span>Button</span><span> color</span><span>=</span><span>"</span><span>neutral</span><span>"</span><span> variant</span><span>=</span><span>"</span><span>contained</span><span>"</span><span>></span></span>
<span><span>        neutral</span></span>
<span><span>      &#x3C;/</span><span>Button</span><span>></span></span>
<span><span>    &#x3C;/</span><span>ThemeProvider</span><span>></span></span>
<span><span>  );</span></span>
<span><span>}</span></span></code></pre><p>这样就扩充了MUI原有的类型定义，​<code class="">neutral</code>​成了合法的​<code class="">color</code>​值。
</p><h2 id="user-content-useref的类型" class="">useRef的类型<a class="" tabindex="-1" href="#useref的类型">#</a></h2><p><code class="">useRef</code>​在React的文档中描述为：
</p><blockquote><p>useRef 返回一个可变的 ref 对象，其 .current属性被初始化为传入的参数（initialValue）。返回的 ref 对象在组件的整个生命周期内持续存在。
</p></blockquote><p><code class="">useRef</code>​或​<code class="">createRef</code>​常被误解为用于获取子组件的DOM节点，但其实它的参数可以是任意对象，由于即使组件重新渲染，​<code class="">useRef</code>​返回的ref对象保存的也仍然是对同一个对象的引用，所以可以用来处理一些复杂的闭包场景。
</p><p>当​<code class="">TypeScript</code>​与​<code class="">useRef</code>​结合到一起，如果尝试给其返回对象的current赋值，有时会出现一个难以理解的类型错误：​<code class="">Cannot assign to current because it is a read-only.</code>​，这就很奇怪了，为什么current是不可变的？
</p><p>值得一提的是，React本身在开发环境是有类型检查的，但是用的不是​<code class="">TypeScript</code>​，而是Facebook自家的​<code class="">FlowJS</code>​。查看​<code class="">useRef</code>​的<a href="https://github.com/facebook/react/blob/42f15b324f50d0fd98322c21646ac3013e30344a/packages/react/src/ReactHooks.js#L96">源码</a>，它的类型就是一个普通的泛型函数：​<code class="">T => { current: T }</code>​，但是我们开发React应用时是用到​<code class="">TypeScript</code>​做静态类型检查的，实际上依赖了​<code class="">@types/react</code>​这个库，查看源码，可以发现在这里​<code class="">useRef</code>​确实用到了重载：
</p><pre tabindex="0"><code><span><span>function</span><span> useRef</span><span>&#x3C;</span><span>T</span><span>></span><span>(</span><span>initialValue</span><span>:</span><span> T</span><span>)</span><span>:</span><span> MutableRefObject</span><span>&#x3C;</span><span>T</span><span>>;</span></span>
<span><span>function</span><span> useRef</span><span>&#x3C;</span><span>T</span><span>></span><span>(</span><span>initialValue</span><span>:</span><span> T</span><span>|</span><span>null</span><span>)</span><span>:</span><span> RefObject</span><span>&#x3C;</span><span>T</span><span>>;</span></span>
<span><span>function</span><span> useRef</span><span>&#x3C;</span><span>T</span><span> =</span><span> undefined</span><span>></span><span>()</span><span>:</span><span> MutableRefObject</span><span>&#x3C;</span><span>T</span><span> |</span><span> undefined</span><span>>;</span></span></code></pre><p>从类型名称大概可以看出来了，如果返回值是​<code class="">MutableRefObject&#x3C;T></code>​应该是不会报错的，事实也确实如此。​<code class="">RefObject</code>​内的​<code class="">current</code>​属性是​<code class="">readonly</code>​的。要避免这个错误，可以这样使用​<code class="">useRef</code>​：
</p><pre tabindex="0"><code><span><span>const</span><span> ref</span><span> =</span><span> useRef</span><span>&#x3C;</span><span>number</span><span> |</span><span> null</span><span>></span><span>(</span><span>null</span><span>);</span></span></code></pre><p>如果使用​<code class="">useRef&#x3C;sometype>(null)</code>​，那么​<code class="">sometype</code>​就匹配上了泛型​<code class="">T</code>​，参数与​<code class="">T | null</code>​匹配，整个调用就匹配上了​<code class="">useRef&#x3C;T>(initialValue: T | null): RefObject&#x3C;T></code>​，而如果使用​<code class="">useRef&#x3C;sometype | null>(null)</code>​，那么就是联合类型​<code class="">sometype | null</code>​匹配上泛型参数​<code class="">T</code>​，参数​<code class="">initialValue</code>​也就是​<code class="">T</code>​类型，函数调用就匹配上了​<code class="">useRef&#x3C;T>(initialValue: T): MutableRefObject&#x3C;T></code>​。在​<code class="">Rust</code>​社区里，有时候我们把这叫做*「类型配平」*，这种操作确实有点像化学方程式配平:)
</p><p><strong>题外话</strong>​: 这个<a href="https://github.com/DefinitelyTyped/DefinitelyTyped/issues/31065">issue</a>里包含了有关​<code class="">@types/react</code>​为什么要这样标注​<code class="">useRef</code>​类型的讨论。
</p><h2 id="user-content-类型收窄与条件渲染" class="">类型收窄与条件渲染<a class="" tabindex="-1" href="#类型收窄与条件渲染">#</a></h2><p><code class="">类型收窄</code>​这个名词也许不是很常见，但是很可能你已经在不知不觉中使用过，最常见的应该是这样一个非空判断：
</p><pre tabindex="0"><code><span><span>function</span><span> Foo</span><span>()</span><span> {</span></span>
<span><span>  const</span><span> data</span><span>:</span><span> string</span><span>[]</span><span> |</span><span> undefined</span><span> =</span><span> useRequest</span><span>()</span></span>
<span></span>
<span><span>  if</span><span> (</span><span>!</span><span>data) {</span></span>
<span><span>    // data: undefined</span></span>
<span><span>    return</span><span> '</span><span>Not found</span><span>'</span></span>
<span><span>  }</span></span>
<span><span>  // data: string[]</span></span>
<span></span>
<span><span>  return</span><span> (</span></span>
<span><span>    &#x3C;</span><span>ul</span><span>></span></span>
<span><span>      {</span><span>data</span><span>.</span><span>map</span><span>(</span><span>item</span><span> =></span><span> &#x3C;</span><span>li</span><span> key</span><span>=</span><span>{</span><span>item</span><span>}</span><span>></span><span>{</span><span>item</span><span>}</span><span>&#x3C;/</span><span>li</span><span>></span><span>)</span><span>}</span></span>
<span><span>    &#x3C;/</span><span>ul</span><span>></span></span>
<span><span>  )</span></span>
<span><span>}</span></span></code></pre><p><code class="">data</code>​原本的类型是​<code class="">string[] | undefined</code>​，这是一个联合类型，但进入​<code class="">if</code>​分支，data的类型就只是​<code class="">undefined</code>​，类型变“窄”了，由于在这个分支内最终使用了​<code class="">return</code>​，离开这个分支后，​<code class="">data</code>​的类型变成了​<code class="">string[]</code>​，可以放心的使用​<code class="">map</code>​方法。类型收窄通常是很直观的，比如在上面的例子里，如果程序会执行到​<code class="">if (!data)</code>​分支内，那么​<code class="">data</code>​必然不可能是​<code class="">string[]</code>​类型；同样地，进入​<code class="">if</code>​分支后就直接​<code class="">return</code>​，那么如果​<code class="">data</code>​是​<code class="">undefined</code>​，必然不会执行后续代码，反之可知在​<code class="">if</code>​之后的​<code class="">data</code>​就一定是​<code class="">string[]</code>​类型了。像​<code class="">instanceof</code>​、​<code class="">in</code>​、​<code class="">switch</code>​等操作都可以将类型由较宽泛的收缩到较窄的。另外，如果​<code class="">if</code>​分支内没有​<code class="">return</code>​，那么​<code class="">data</code>​的类型只在​<code class="">if</code>​语句块内收窄，后面就不能安全地使用​<code class="">map</code>​了，在这里只有​<code class="">return</code>​将函数返回，才算杜绝了后续​<code class="">data</code>​类型为​<code class="">null</code>​的可能，同理，如果用​<code class="">throw</code>​抛出异常也能让分支后的​<code class="">data</code>​类型收窄。
</p><p>只是举个这样的例子，可能会让人感觉类型收窄似乎是个很多余的概念，即使是使用​<code class="">JavaScript</code>​不也一样是这么判断是否为空嘛，其实相比之下，​<code class="">TypeScript</code>​的类型收窄还是带来了一些好处的，首先它保证了静态的类型检查，不能使用当前类型上没有的方法，同时也提供了较好的IDE自动补全支持，在例子中最后的​<code class="">data</code>​类型被推断为​<code class="">string[]</code>​，IDE可以自动补全相关的​<code class="">map</code>​方法，​<code class="">map</code>​的回调函数参数​<code class="">item</code>​也会被相应地推断为​<code class="">string</code>​类型，可以放心对其使用​<code class="">startWith</code>​等原型方法。
</p><p>接下来看看一个更接近真实项目代码的例子，现在有一个租房系统的后台应用，页面固定布局如图：
</p><p><img alt="layout" src="https://r2.elliot00.com/legacy/layout.drawio.png" width="741" height="591"></p><p>但是在业务流程中，有三种登录角色，分别是Admin、Landlord、Tenant，理所当然这三种角色登录后台后看到的UI细节并不相同。
</p><pre tabindex="0"><code><span><span>// 假设这是后端的数据模型</span></span>
<span><span>type</span><span> Admin</span><span> =</span><span> {</span></span>
<span><span>  id</span><span>:</span><span> number</span></span>
<span><span>  email</span><span>:</span><span> string</span></span>
<span><span>  password</span><span>:</span><span> string</span></span>
<span><span>}</span></span>
<span></span>
<span><span>type</span><span> Landlord</span><span> =</span><span> {</span></span>
<span><span>  id</span><span>:</span><span> number</span></span>
<span><span>  name</span><span>?:</span><span> string</span></span>
<span><span>  email</span><span>:</span><span> string</span></span>
<span><span>  password</span><span>:</span><span> string</span></span>
<span><span>  avatar</span><span>?:</span><span> string</span></span>
<span><span>}</span></span>
<span></span>
<span><span>type</span><span> Tenant</span><span> =</span><span> {</span></span>
<span><span>  id</span><span>:</span><span> number</span></span>
<span><span>  name</span><span>?:</span><span> string</span></span>
<span><span>  email</span><span>:</span><span> string</span></span>
<span><span>  password</span><span>:</span><span> string</span></span>
<span><span>  avatar</span><span>?:</span><span> string</span></span>
<span><span>  phoneNumber</span><span>:</span><span> string</span></span>
<span><span>}</span></span>
<span></span>
<span><span>// 获取当前登录用户</span></span>
<span><span>type</span><span> User</span><span> =</span><span> Admin</span><span> |</span><span> Landlord</span><span> |</span><span> Tenant</span></span>
<span><span>function</span><span> currentUser</span><span>()</span><span>:</span><span> User</span><span> {</span></span>
<span><span>}</span></span></code></pre><p>在前端如何方便的根据不同角色渲染不同内容呢？可以为每个角色类型加上一个​<em>标签</em>​：
</p><pre tabindex="0"><code><span><span>type</span><span> Admin</span><span> =</span><span> {</span></span>
<span><span>  ...</span></span>
<span><span>  role</span><span>:</span><span> '</span><span>Admin</span><span>'</span></span>
<span><span>}</span></span>
<span></span>
<span><span>type</span><span> Landlord</span><span> =</span><span> {</span></span>
<span><span>  ...</span></span>
<span><span>  role</span><span>:</span><span> '</span><span>Landlord</span><span>'</span></span>
<span><span>}</span></span>
<span></span>
<span><span>type</span><span> Tenant</span><span> =</span><span> {</span></span>
<span><span>  ...</span></span>
<span><span>  role</span><span>:</span><span> '</span><span>Tenant</span><span>'</span></span>
<span><span>}</span></span>
<span></span>
<span><span>const</span><span> Header</span><span>:</span><span> React</span><span>.</span><span>FC</span><span> =</span><span> ()</span><span> =></span><span> {</span></span>
<span><span>  const</span><span> user</span><span> =</span><span> useUser</span><span>()</span><span> // type: User</span></span>
<span></span>
<span><span>  switch</span><span> (</span><span>user</span><span>.</span><span>role</span><span>)</span><span> {</span></span>
<span><span>    case</span><span> '</span><span>Tenant</span><span>'</span><span>:</span></span>
<span><span>      // 合法，此处user类型收窄为Tenant</span></span>
<span><span>      return</span><span> &#x3C;</span><span>div</span><span>></span><span>{</span><span>user</span><span>.</span><span>phoneNumber</span><span>}</span><span>&#x3C;/</span><span>div</span><span>></span></span>
<span><span>    case</span><span> '</span><span>Landlord</span><span>'</span><span>:</span></span>
<span><span>      return</span><span> ...</span></span>
<span><span>    case</span><span> '</span><span>Admin</span><span>'</span><span>:</span></span>
<span><span>      return</span><span> ...</span></span>
<span><span>    default</span><span>:</span></span>
<span><span>      const</span><span> exhausted</span><span>:</span><span> never</span><span> =</span><span> user</span></span>
<span><span>      return</span><span> null</span></span>
<span><span>  }</span></span>
<span><span>}</span></span></code></pre><p>这里的​<code class="">role</code>​并不是字符串类型，而是一个​<em>字面量类型</em>​，在​<code class="">switch</code>​语句里，这样一个附加字段就可以帮助我们将​<code class="">User</code>​类型收窄到更加具体的类型，从而根据角色改变组件的渲染。
</p><p>在最后我用了一个​<code class="">const exhausted: never = user</code>​，这是一个小技巧，我之前<a href="https://elliot00.com/posts/typescript-mutex-param">有篇博客</a>提到用​<code class="">never</code>​来实现互斥参数，​<code class="">never</code>​在TS里是​<code class="">bottom type</code>​，为了节省篇幅简单点说就是除了​<code class="">never</code>​自身外任何值都不能赋值给​<code class="">never</code>​，由于前面所有的​<code class="">case</code>​已经包含了​<code class="">user</code>​所有可能的情况并且都还有​<code class="">return</code>​，所以​<code class="">default</code>​内的代码不会被执行，​<code class="">user</code>​的类型在这里被收窄为​<code class="">never</code>​类型，这时候这个赋值是合法的。但是如果有人添加了一种新的角色类型​<code class="">CustomerService</code>​到联合类型​<code class="">User</code>​上，​<code class="">user</code>​就不能被收窄到​<code class="">never</code>​类型，这里就会产生一个类型错误，提示需要在​<code class="">switch</code>​语句中添加=case=​以包含所有情况。这样就可以达到​<em>穷尽性检查</em>​的目的。
</p><h2 id="user-content-如何解决太多的条件渲染" class="">如何解决太多的条件渲染<a class="" tabindex="-1" href="#如何解决太多的条件渲染">#</a></h2><p>要是项目里有很多
</p><pre tabindex="0"><code><span><span>user</span><span>?.</span><span>role</span><span> ===</span><span> '</span><span>Admin</span><span>'</span><span> ?</span><span> (</span><span>user</span><span>.</span><span>status</span><span> ===</span><span> '</span><span>active</span><span>'</span><span> ?</span><span> &#x3C;</span><span>Component1</span><span> /> </span><span>:</span><span> &#x3C;</span><span>Comp2</span><span> />) </span><span>:</span><span> null</span></span></code></pre><p>这样的代码，在多人维护的情况下很可能出现越来越多的重复判断、嵌套判断，这样的代码会让人阅读起来很头疼，维护起来更加头疼。如果一个项目里存在大量的条件渲染，如何保持代码的整洁呢？
</p><h3 id="user-content-组件工厂">组件工厂？<a class="" tabindex="-1" href="#组件工厂">#</a></h3><pre tabindex="0"><code><span><span>// 在接口上定义组件类型</span></span>
<span><span>interface</span><span> User</span><span> {</span></span>
<span><span>  Toolbar</span><span>:</span><span> React</span><span>.</span><span>FC</span></span>
<span><span>}</span></span>
<span></span>
<span><span>function</span><span> Page</span><span>()</span><span> {</span></span>
<span><span>  const</span><span> user</span><span> =</span><span> userUser</span><span>()</span></span>
<span></span>
<span><span>  return</span><span> (</span></span>
<span><span>    &#x3C;</span><span>Layout</span><span> toolbar</span><span>=</span><span>{</span><span>&#x3C;</span><span>user.Toolbar</span><span> /></span><span>}</span><span>></span></span>
<span><span>      &#x3C;</span><span>Main</span><span> /></span></span>
<span><span>    &#x3C;/</span><span>Layout</span><span>></span></span>
<span><span>  )</span></span>
<span><span>}</span></span></code></pre><h3 id="user-content-组合">组合？<a class="" tabindex="-1" href="#组合">#</a></h3><p>也许有时候我们并不需要在所有地方都判断状态，例如这个用户角色的问题，可以将这个状态与路由绑定，将页面组件拆分成很多个小部件：
</p><pre tabindex="0"><code><span><span>type</span><span> ContainerProps</span><span> =</span><span> {</span></span>
<span><span>  navbar</span><span>:</span><span> React</span><span>.</span><span>ReactNode</span></span>
<span><span>  extra</span><span>?:</span><span> React</span><span>.</span><span>ReactNode</span></span>
<span><span>}</span></span>
<span></span>
<span><span>const</span><span> Container</span><span>:</span><span> React</span><span>.</span><span>FC</span><span>&#x3C;</span><span>ContainerProps</span><span>></span><span> =</span><span> (</span><span>{</span><span> navbar</span><span>,</span><span> extra</span><span>,</span><span> children</span><span> }</span><span>)</span><span> =></span><span> {</span></span>
<span><span>  return</span><span> (</span></span>
<span><span>    &#x3C;></span></span>
<span><span>      &#x3C;</span><span>Header</span><span>></span></span>
<span><span>        {</span><span>navbar</span><span>}</span></span>
<span><span>        {</span><span>extra</span><span>}</span></span>
<span><span>      &#x3C;/</span><span>Header</span><span>></span></span>
<span><span>      &#x3C;</span><span>Body</span><span>></span><span>{</span><span>children</span><span>}</span><span>&#x3C;/</span><span>Body</span><span>></span></span>
<span><span>      &#x3C;</span><span>Footer</span><span> /></span></span>
<span><span>    &#x3C;/></span></span>
<span></span>
<span><span>// Admin page</span></span>
<span><span>&#x3C;</span><span>Container</span><span> navbar</span><span>=</span><span>{</span><span>&#x3C;</span><span>AdminNavbar</span><span> /></span><span>}</span><span> extra</span><span>=</span><span>{</span><span>&#x3C;</span><span>OnlyAdmin</span><span> /></span><span>}</span><span>></span></span>
<span><span>  &#x3C;</span><span>Main</span><span> /></span></span>
<span><span>&#x3C;/</span><span>Container</span><span>></span></span>
<span></span>
<span><span>// Landlord page</span></span>
<span><span>&#x3C;</span><span>Container</span><span> navbar</span><span>=</span><span>{</span><span>&#x3C;</span><span>LandlordNavbar</span><span> /></span><span>}</span><span>></span></span>
<span><span>  &#x3C;</span><span>Landlord</span><span> /></span></span>
<span><span>&#x3C;/</span><span>Container</span><span>></span></span></code></pre><p>在路由组件中我们根据角色将渲染不同的页面组件，这些组件中相同的地方可以提取到一个公共的容器，将有差异的地方通过​<code class="">props</code>​传递，某些页面独有的元素可以定义成可选属性，​<code class="">undefined</code>​和​<code class="">null</code>​都是合法的JSX元素，但是不会被渲染。我个人更喜欢这样声明式的写法。
</p><h2 id="user-content-非空断言操作符" class="">非空断言操作符<a class="" tabindex="-1" href="#非空断言操作符">#</a></h2><blockquote><p>更新于2024/10/30
</p></blockquote><p>如果有时因为一些第三方库的限制，出现某个值类型可能为空，但在业务上可以保证其不会为空的情况，可以使用非空断言操作符来简化代码：
</p><pre tabindex="0"><code><span><span>validate</span><span>(data) </span><span>// data不会为空，但类型仍为string | null</span></span>
<span></span>
<span><span>// 不用非空断言操作符的情况</span></span>
<span><span>useData</span><span>(data </span><span>as</span><span> string</span><span>)</span></span>
<span></span>
<span><span>// 使用非空断言操作符 `!`</span></span>
<span><span>useData</span><span>(data</span><span>!</span><span>)</span></span>
<span></span>
<span><span>// 也可以与点操作符结合</span></span>
<span><span>data</span><span>!</span><span>.</span><span>title</span></span></code></pre>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>Nix入坑笔记</title>
          <link>https://elliot00.com/posts/nix-note</link>
          <description>这篇文章介绍了一套解决软件包管理和环境配置问题的工具： Nix 。Nix是一系列工具的合集，通过一种纯函数式的方式来管理软件包。Nix提供了一个函数式语言来描述软件包，每一个软件包就是Nix语言中的一个表达式。Nix工具集中，`nix-env`命令用于安装、升级或删除包，它和其他Linux发行版的包管理工具或Mac上的homebrew作用类似。NixOS是一个基于Nix的Linux发行版，整个NixOS就是一个声明式的系统，只要备份好configuration，就可以随时恢复原样，拷贝配置文件就可以在新设备生成一个一样的系统。</description>
          <pubDate>Sun, 26 Dec 2021 04:23:01 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <p>对于经常使用计算机工作的人（尤其是程序员）来说，工作设备上往往会积聚大量的文档、软件以及配置文件；如果我们需要在多台不同设备间切换，或者单纯是更换了新电脑，要是可以在不同的设备上同步配置，将会节省我们很多时间；另外假如有时由于某个操作导致系统出现了异常，要如何轻松回退到之前的状态呢？现在市面上有各类云盘工具可以用于备份和同步文件，有版本管理工具可以帮助管理文档版本。那么对于软件呢？软件的配置文件可以备份和同步，但是试想一下如果在一台机器上曾经安装了应用A，而在另一台机器上重新安装A时，A的版本发生了变化，直接使用最新版会导致旧的配置不可用；进而可以试想每个软件都有不同的依赖，如果应用A依赖B的1.0版本，而在新设备上安装的B是最新的2.0版本，这也可能导致程序A无法工作。正如标题所示，在此我要介绍一套能解决以上问题的工具：​<code class="">Nix</code>​。
</p><h2 id="user-content-函数与可复现" class="">函数与可复现<a class="" tabindex="-1" href="#函数与可复现">#</a></h2><p>更换使用的计算机，我们希望可以在新的机器上​<em>复现</em>​旧机器的内容，也就是获得和旧机器一样的软件版本、配置信息等；当出现问题需要他人帮助时，我们希望可以控制变量，为帮助者​<em>复现</em>​一个与我们当前环境最接近的环境。我们曾经接触过什么东西是可以复现的呢？
</p><p>回顾一下数学中函数的定义：“函数（英語：Function）在数学中为两不为空集的集合间的一种对应关系：输入值集合中的每项元素皆能对应唯一一项输出值集合中的元素。（维基百科）”，例如​<code class="">f(x) = x^2</code>​就是一个函数，对于任意一个输入x，都只能有​<strong>唯一</strong>​一个输出，如果一个东西输入为x，输出同时既可以是y，也可以是z，那么这就不是函数。可以发现，一个函数，只要输入不变，输出也一定不会变，也就是说数学上的函数是可以复现结果的，不论外界条件（如时间）如何变化，只有输入是改变结果的唯一渠道，输入不变就可以一直得到不变的结果。
</p><p><img alt="函数" src="https://r2.elliot00.com/legacy/function.png" width="1114" height="656"></p><p>通过上图可以看到，如果x轴是输入而y轴是输出，那么画一条x轴的垂直线，如果它能与曲线拥有超过一个的交点，那么这个图像就不是函数的图像。
</p><p>以上是数学中的函数，在计算机领域，也存在一个“函数”，但是这两个概念并不相等。考察下面这个​<code class="">Python</code>​语言中的函数：
</p><pre tabindex="0"><code><span><span>def</span><span> isAdult</span><span>(</span><span>age</span><span>:</span><span> int</span><span>)</span><span> -></span><span> bool</span><span>:</span></span>
<span><span>    return</span><span> age </span><span>></span><span> 18</span></span></code></pre><p>这种函数被成为​<strong>纯函数</strong>​，对于一个确切的​<code class="">age</code>​值，这个函数只会返回一个确切的结果，这种情况下，这个函数相当于数学定义中的函数。再看另一种函数：
</p><pre tabindex="0"><code><span><span>adultAge </span><span>=</span><span> 18</span></span>
<span></span>
<span><span>def</span><span> isAdult</span><span>(</span><span>age</span><span>:</span><span> int</span><span>)</span><span> -></span><span> bool</span><span>:</span></span>
<span><span>    return</span><span> age </span><span>></span><span> adultAge</span></span></code></pre><p>这个函数就是不纯的，因为​<code class="">adultAge</code>​可以被更改，而函数依赖这样一个可以被改变的自由变量，因此相同的输入可能获得不同的结果，例如当参数age为19时，函数返回​<code class="">true</code>​，之后​<code class="">adultAge</code>​被修改为20，同样的输入函数会返回​<code class="">false</code>​。这样不纯的函数就不能视为数学上的函数。可以说，在编程语言中，纯函数是可以复现的，而非纯函数不可以。
</p><h2 id="user-content-nix-纯函数式的软件包管理工具" class="">Nix: 纯函数式的软件包管理工具<a class="" tabindex="-1" href="#nix-纯函数式的软件包管理工具">#</a></h2><p>Nix是一系列工具的合集，通过一种纯函数式的方式。Nix提供了一个​<strong>函数式语言</strong>​来描述软件包，每一个软件包就是Nix语言中的一个​<em>表达式</em>​，例如下面这个​<code class="">hello</code>​包：
</p><pre tabindex="0"><code><span><span>{ </span><span>lib</span></span>
<span><span>,</span><span> stdenv</span></span>
<span><span>,</span><span> fetchurl</span></span>
<span><span>,</span><span> testVersion</span></span>
<span><span>,</span><span> hello</span></span>
<span><span>}:</span></span>
<span></span>
<span><span>stdenv</span><span>.</span><span>mkDerivation</span><span> rec</span><span> {</span></span>
<span><span>  pname</span><span> =</span><span> "</span><span>hello</span><span>"</span><span>;</span></span>
<span><span>  version</span><span> =</span><span> "</span><span>2.10</span><span>"</span><span>;</span></span>
<span></span>
<span><span>  src</span><span> =</span><span> fetchurl</span><span> {</span></span>
<span><span>    url</span><span> =</span><span> "</span><span>mirror://gnu/hello/</span><span>${</span><span>pname</span><span>}</span><span>-</span><span>${</span><span>version</span><span>}</span><span>.tar.gz</span><span>"</span><span>;</span></span>
<span><span>    sha256</span><span> =</span><span> "</span><span>0ssi1wpaf7plaswqqjwigppsg5fyh99vdlb9kzl7c9lng89ndq1i</span><span>"</span><span>;</span></span>
<span><span>  };</span></span>
<span></span>
<span><span>  doCheck</span><span> =</span><span> true</span><span>;</span></span>
<span></span>
<span><span>  passthru</span><span>.</span><span>tests</span><span>.</span><span>version</span><span> =</span></span>
<span><span>    testVersion</span><span> { </span><span>package</span><span> =</span><span> hello</span><span>; };</span></span>
<span></span>
<span><span>  meta</span><span> =</span><span> with</span><span> lib</span><span>; {</span></span>
<span><span>    description</span><span> =</span><span> "</span><span>A program that produces a familiar, friendly greeting</span><span>"</span><span>;</span></span>
<span><span>    longDescription</span><span> =</span><span> ''</span></span>
<span><span>      GNU Hello is a program that prints "Hello, world!" when you run it.</span></span>
<span><span>      It is fully customizable.</span></span>
<span><span>    ''</span><span>;</span></span>
<span><span>    homepage</span><span> =</span><span> "</span><span>https://www.gnu.org/software/hello/manual/</span><span>"</span><span>;</span></span>
<span><span>    changelog</span><span> =</span><span> "</span><span>https://git.savannah.gnu.org/cgit/hello.git/plain/NEWS?h=v</span><span>${</span><span>version</span><span>}</span><span>"</span><span>;</span></span>
<span><span>    license</span><span> =</span><span> licenses</span><span>.</span><span>gpl3Plus</span><span>;</span></span>
<span><span>    maintainers</span><span> =</span><span> [</span><span> maintainers</span><span>.</span><span>eelco</span><span> ]</span><span>;</span></span>
<span><span>    platforms</span><span> =</span><span> platforms</span><span>.</span><span>all</span><span>;</span></span>
<span><span>  };</span></span>
<span><span>}</span></span></code></pre><p>这是一个Nix语言中的函数，也是Nix概念下的“软件包”，​<code class="">Haskell</code>​程序员可能会对此感到很熟悉，Nix中的函数定义很简洁，格式是​<code class="">pattern: body</code>​，pattern是一个模式，如果没有接触过函数式语言的话，可以参考​<code class="">JavaScript</code>​中的解构对象，body是函数体，要想定义包，这个函数需要返回一个​<code class="">derivation</code>​，也就是对包的构建过程的描述。Nix中调用函数不需要括号，也不需要​<code class="">return</code>​，函数体表达式结果就是返回值，采用​<code class="">func param</code>​格式，因此这个​<code class="">hello</code>​包的函数体就是调用了​<code class="">stdenv.mkDerivation</code>​函数返回其结果，其中包含构建该软件包所需的属性。只要输入相同，我们就能得到完全相同的软件版本。一个软件包所需要的全部依赖必须被定义在表达式内，而不能去环境变量、其他目录获取。Nix语言编译后的结果就是表达式所描述的程序包。
</p><p>既然Nix下的包就是Nix语言的一个表达式，那我们从一个编程语言的表达式的角度来看看Nix包的性质：
</p><ol><li>表达式可以求值，可以认为求值结果就是一个软件包，值可以比较，值不相同，就是包不同
</li><li>软件包的版本、依赖版本、构建过程等必须由表达式描述，更改这些属性，会得到不同的值，也就是不同的包
</li><li>结合1、2，即使如果原本有包A，依赖​<code class="">Python3.7</code>​，现在我们创建一个依赖​<code class="">Python3.6</code>​的版本，虽然都是可以认为这也是包A，但实质上他们是两个包，因为值不相同
</li><li>因此系统上可以同时出现很多个版本的包A，他们实质上并不相同，其他的包可以依赖不同的包A，从旧的包A派生一个新的包A2.0，不会改变那些依赖旧的包A的其他包，除非修改了其他包的表达式定义
</li></ol><h2 id="user-content-profile与channel" class="">Profile与Channel<a class="" tabindex="-1" href="#profile与channel">#</a></h2><p>Nix工具集中，​<code class="">nix-env</code>​命令用于安装、升级或删除包，它和其他Linux发行版的包管理工具或Mac上的homebrew作用类似，不同之处在于nix-env对系统环境的更改是​<em>原子化</em>​的，​<em>可回滚</em>​的。每次通过​<code class="">nix-env</code>​修改用户环境，都会生成一个新的profile，类似于一次Git记录，可以像Git一样，回滚到某一次变更记录上。​<code class="">nix-env --list-generations</code>​命令可以列出所有的版本，可以在其中自由切换，为了节省硬盘空间，也可以使用垃圾回收机制清除不必要的记录。
</p><p><code class="">nix-channel</code>​是一个用来管理Channel的工具，Channel就是一个简单的指向某个Nix表达式集合，或者说：软件包仓库。例如<a href="https://nixos.org/channels/nixpkgs-unstable">[[https://nixos.org/channels/nixpkgs-unstable</a>]]（目前其中包含八万多个包）。
</p><h2 id="user-content-隔离的开发环境" class="">隔离的开发环境<a class="" tabindex="-1" href="#隔离的开发环境">#</a></h2><p>我经常在开发环境中使用​<code class="">Docker</code>​，因为我对开发环境有一些“洁癖”，比如在开发的某个项目需要用到redis，而我在其他地方不是经常使用，那么我会使用Docker镜像来代替全局安装。另外，现代的编程语言包管理工具通常都具有隔离环境的作用，比如​<code class="">nodejs</code>​的​<code class="">npm</code>​，在一个项目下添加依赖​<code class="">react16</code>​，它会被安装一个隔离的环境中，不会影响到另一个项目下使用​<code class="">react17</code>​；​<code class="">Python</code>​的官方包管理工具​<code class="">pip</code>​会把Python包安装到全局，所以做Python开发一般都会使用​<code class="">virtualenv</code>​创建与全局隔离的虚拟环境（顺带推荐一个支持<a href="https://www.python.org/dev/peps/pep-0582/">PEP582</a>的Python包管理工具<a href="https://pdm.fming.dev/">PDM</a>）。​<code class="">nix-shell</code>​就是一个类似​<code class="">virtualenv</code>​的工具。
</p><p>假如日常系统全局环境使用的Python版本是​<code class="">3.8</code>​，但是想在某个单独的环境里使用​<code class="">Python3.10</code>​，尝试尝试它的模式匹配功能，那么就可以使用命令​<code class="">nix-shell -p python310</code>​，nix-shell会准备需要的依赖，并且自动进入一个配置好的单独shell环境中。
</p><p><img alt="nix-shell" src="https://r2.elliot00.com/legacy/Screenshot_20211226_103523_nix_shell.png" width="1913" height="620"></p><p>nix-shell内的包不会影响到外界。
</p><h2 id="user-content-nixos" class="">NixOS<a class="" tabindex="-1" href="#nixos">#</a></h2><p>NixOS是一个基于Nix的Linux发行版。与​<code class="">pacman</code>​等包管理工具不同，Nix本身是跨平台的，可以脱离NixOS使用。事实上是先有Nix才有的NixOS，借用姜文《邪不压正》台词：“就是为了这口醋，才包的这顿饺子”。虽然NixOS也是一个Linux发行版，但是它和常规的​<code class="">GNU/Linux发行版</code>​有一些可能会劝退新手的区别:
</p><ol><li>首先它不支持<a href="https://en.wikipedia.org/wiki/Filesystem_Hierarchy_Standard">FHS</a>，所以一些假定系统上存在这些目录的程序可能不能方便地正常工作
</li><li>没有像Ubuntu那样方便的安装工具，不过按照官方手册来装也并不是很费事
</li></ol><p>所以在尝试使用NixOS之前一定要考虑清楚，最好是虚拟机里先试试再决定是否作为日常使用的系统。
</p><p>首先是安装，可以参考<a href="https://nixos.org/manual/nixos/stable/">官方手册</a>，首先是分区，然后mount，最后通过​<code class="">nixos-generate-config</code>​命令生成一个​<code class="">/etc/nixos/configuration.nix</code>​，这个文件定义了整个系统的配置、软件包、硬件、环境变量等，这是一个Nix语言的文件，支持模块化：
</p><pre tabindex="0"><code><span><span>{ </span><span>config</span><span>,</span><span> pkgs</span><span>,</span><span> ... </span><span>}:</span></span>
<span></span>
<span><span>{</span></span>
<span><span>  imports</span><span> =</span></span>
<span><span>    [</span><span> # Include the results of the hardware scan.</span></span>
<span><span>      ./hardware-configuration.nix</span></span>
<span><span>      ./home.nix</span></span>
<span><span>    ]</span><span>;</span></span>
<span></span>
<span><span>  boot</span><span>.</span><span>loader</span><span>.</span><span>systemd-boot</span><span>.</span><span>enable</span><span> =</span><span> true</span><span>;</span></span>
<span><span>  boot</span><span>.</span><span>loader</span><span>.</span><span>efi</span><span>.</span><span>canTouchEfiVariables</span><span> =</span><span> true</span><span>;</span></span>
<span></span>
<span><span>  environment</span><span>.</span><span>systemPackages</span><span> =</span><span> with</span><span> pkgs</span><span>; </span><span>[</span></span>
<span><span>    vimHugeX</span></span>
<span><span>    python3Full</span></span>
<span><span>    nodejs</span></span>
<span><span>    wget</span></span>
<span><span>    firefox</span></span>
<span><span>  ]</span><span>;</span></span>
<span><span>}</span></span></code></pre><p>以上是部分示例，还是比较简洁易懂的，配置好就可以使用​<code class="">nixos-install</code>​命令安装系统了，之后要修改配置，新增系统包，都可以来修改这个文件，执行​<code class="">nixos-rebuild switch</code>​命令重新编译，就像​<code class="">nix-env</code>​一样，上一个系统状态也会保留，在boot选项里可以选择回到改动之前的版本，可以通过配置gc来定时清理太旧的profile：
</p><pre tabindex="0"><code><span><span>nix</span><span>.</span><span>autoOptimiseStore</span><span> =</span><span> true</span><span>;</span></span>
<span><span>nix</span><span>.</span><span>gc</span><span> =</span><span> {</span></span>
<span><span>  automatic</span><span> =</span><span> true</span><span>;</span></span>
<span><span>  dates</span><span> =</span><span> "</span><span>weekly</span><span>"</span><span>;</span></span>
<span><span>  options</span><span> =</span><span> "</span><span>--delete-older-than 10d</span><span>"</span><span>;</span></span>
<span><span>}</span><span>;</span></span></code></pre><p>可以说整个NixOS就是一个声明式的系统，只要备份好​<em>configuration</em>​，就可以随时恢复原样，拷贝配置文件就可以在新设备生成一个一样的系统。由于​<code class="">/etc/nixos/*.nix</code>​就是对系统的定义，那么如果系统出了问题，只要拷贝一份配置文件给别人，别人就可以清楚地知道你的系统状态，方便复现问题，定位问题（经常看一些开源项目的issue就会发现，有一些问题就因为使用者环境的复杂，开发者无法复现问题，从而长期得不到解决）。
</p><p>说完优点再说说缺点，除了之前提到的与常规发行版的区别，再说几个我认为的缺点：
</p><ol><li>用nix-env安装包，不会自动记录在配置里，这可能引起困惑
</li><li>文档相对较少
</li><li>自己创建社区暂时没有的包，需要学习Nix语言（算一个使用门槛上的缺点吧）
</li></ol><p>对于Nix的介绍就先到这里了，关于一些使用的细节我会发布到<a href="https://wiki.elliot00.com/">个人wiki</a>上以供参考，等到再深入使用一段时间后再来谈谈Nix相关内容吧～
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>Django+React全栈开发：文章详情</title>
          <link>https://elliot00.com/posts/react-django-article-detail</link>
          <description>这篇文章讨论了构建一个博客应用时遇到的身份验证问题和组件交互的解决方案。它首先介绍了修改一个组件以从后端获取文章正文导致的身份验证问题，并提供了解决方法。接下来，文章介绍了 React Hooks 的概念，以及如何使用 Hooks 实现类似于类组件生命周期的功能。它还讨论了使用 Hooks 时的一些注意事项，例如 Hooks 只能出现在函数式组件或自定义 Hook 中，并且必须在最顶层。最后，文章给出了一个练习，让读者尝试在页面上呈现文章标题、创建日期等其他信息。</description>
          <pubDate>Sun, 31 Oct 2021 13:30:48 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <h2 id="user-content-验证问题" class="">验证问题<a class="" tabindex="-1" href="#验证问题">#</a></h2><p>修改​<code class="">ArticleDetail</code>​组件，从后端拿到文章正文。现在如果直接启动应用，会发现获取文章列表时后端返回了403响应，因为在<a href="https://elliot00.com/posts/react-django-jwt">上一节</a>中设置了身份验证，每次请求必须携带正确的JWT，但是事实上对于文章列表和详情的​<code class="">GET</code>​请求完全可以忽略验证，毕竟这是一个博客应用，现在先处理这个问题。
</p><p>这里有一个简陋的解决办法，改写上一章编写的​<code class="">jwt_auth/authentication.py</code>​：
</p><pre tabindex="0"><code><span><span>class</span><span> JWTAuthentication</span><span>(</span><span>BaseAuthentication</span><span>):</span></span>
<span></span>
<span><span>    def</span><span> authenticate</span><span>(</span><span>self</span><span>,</span><span> request</span><span>)</span><span>:</span></span>
<span><span>        header </span><span>=</span><span> request</span><span>.</span><span>META</span><span>.</span><span>get</span><span>(</span><span>'</span><span>HTTP_AUTHORIZATION</span><span>'</span><span>)</span></span>
<span><span>        if</span><span> header </span><span>is</span><span> None</span><span>:</span></span>
<span><span>            return</span><span> None</span></span>
<span><span>        ......</span></span></code></pre><p>如果请求头不包含token则直接返回None，接下来由定义在视图类中的​<code class="">permissions</code>​类处理请求，我们之前设置的是​<code class="">IsAdminOrReadonly</code>​，未登录用户将拥有只读权限。这个办法有些粗糙，读者可以尝试改进。
</p><h2 id="user-content-hooks" class="">Hooks<a class="" tabindex="-1" href="#hooks">#</a></h2><p>前端获取文章详情和文章列表组件中请求文章列表的方式差不多。但是文章列表组件是一个类组件，API请求是在类组件的​<code class="">componentDidMount</code>​这个生命周期里完成的。​<code class="">ArticleDetail</code>​是一个函数组件，前面提到过，函数组件是无状态的，没有生命周期， 那么是否要把函数组件改成类组件呢？
</p><p>React16为函数式组件提供了一个名为Hook的新特性，官方介绍是这样的：
</p><blockquote><p><em>Hooks</em> are a new addition in React 16.8. They let you use state and other React features without writing a class.
</p></blockquote><p>Hook以函数的形式让函数组件获得类组件的state和生命周期功能。这次要用到两个Hook，分别是​<code class="">useState</code>​和​<code class="">useEffect</code>​，下面先看看​<code class="">useState</code>​：
</p><pre tabindex="0"><code><span><span>import</span><span> { useParams } </span><span>from</span><span> '</span><span>react-router-dom</span><span>'</span><span>;</span></span>
<span><span>import</span><span> { useState } </span><span>from</span><span> '</span><span>react</span><span>'</span><span>;</span></span>
<span></span>
<span><span>const</span><span> ArticleDetail</span><span> =</span><span> ()</span><span> =></span><span> {</span></span>
<span><span>  const</span><span> {</span><span> articleId</span><span> }</span><span> =</span><span> useParams</span><span>()</span><span>;</span></span>
<span><span>  const</span><span> [</span><span>body</span><span>,</span><span> setBody</span><span>]</span><span> =</span><span> useState</span><span>(</span><span>''</span><span>)</span><span>;</span></span>
<span></span>
<span><span>  ......</span></span>
<span><span>}</span></span></code></pre><p>增加了两行代码，​<code class="">useState</code>​的参数用来设置state的默认值，返回值是一个列表，第一个元素是创建的state，第二个元素是一个函数，可以用来修改state。目前这个state还没有被用上，现在来写一个简单的交互功能，添加一个按钮，点击一下将页面正文内容变成hello world：
</p><pre tabindex="0"><code><span><span>const</span><span> ArticleDetail</span><span> =</span><span> ()</span><span> =></span><span> {</span></span>
<span><span>  const</span><span> {</span><span> articleId</span><span> }</span><span> =</span><span> useParams</span><span>()</span><span>;</span></span>
<span><span>  const</span><span> [</span><span>body</span><span>,</span><span> setBody</span><span>]</span><span> =</span><span> useState</span><span>(</span><span>''</span><span>)</span><span>;</span></span>
<span></span>
<span><span>  return</span><span> (</span></span>
<span><span>    &#x3C;</span><span>main</span><span>></span></span>
<span><span>      &#x3C;</span><span>div</span><span>></span><span>{</span><span>body</span><span>}</span><span>&#x3C;/</span><span>div</span><span>></span></span>
<span><span>      &#x3C;</span><span>button</span><span> onClick</span><span>=</span><span>{</span><span>()</span><span> =></span><span> setBody</span><span>(</span><span>'</span><span>Hello world</span><span>'</span><span>)</span><span>}</span><span>></span><span>hello</span><span>&#x3C;/</span><span>button</span><span>></span></span>
<span><span>    &#x3C;/</span><span>main</span><span>></span></span>
<span><span>  )</span></span>
<span><span>}</span></span></code></pre><p>这里用到了React的合成事件​<code class="">onClick</code>​，当按钮被点击时，就会触发回调函数，回调函数使用了Hook返回的​<code class="">setBody</code>​函数，将body的值更改为「Hello world」。可以回顾一下类组件，上面的代码其实等价于下面这个类组件：
</p><pre tabindex="0"><code><span><span>class</span><span> ArticleDetail</span><span> extends</span><span> react</span><span>.</span><span>Component</span><span> {</span></span>
<span><span>  constructor</span><span>(</span><span>props</span><span>)</span><span> {</span></span>
<span><span>    super</span><span>(</span><span>props</span><span>);</span></span>
<span><span>    this</span><span>.</span><span>state</span><span> =</span><span> {</span></span>
<span><span>      body</span><span>:</span><span> ''</span><span>,</span></span>
<span><span>    }</span></span>
<span><span>  }</span></span>
<span></span>
<span><span>  render</span><span>()</span><span> {</span></span>
<span><span>    return</span><span> (</span></span>
<span><span>      &#x3C;</span><span>main</span><span>></span></span>
<span><span>        &#x3C;</span><span>div</span><span>></span></span>
<span><span>          {</span><span>this</span><span>.</span><span>state</span><span>.</span><span>body</span><span>}</span></span>
<span><span>        &#x3C;/</span><span>div</span><span>></span></span>
<span><span>        &#x3C;</span><span>button</span><span> onClick</span><span>=</span><span>{</span><span>()</span><span> =></span><span> this</span><span>.</span><span>setState</span><span>(</span><span>{ body</span><span>:</span><span> '</span><span>Hello world</span><span>'</span><span>}</span><span>)</span><span>}</span><span>></span><span>hello</span><span>&#x3C;/</span><span>button</span><span>></span></span>
<span><span>      &#x3C;/</span><span>main</span><span>></span></span>
<span><span>    )</span></span>
<span></span>
<span><span>  }</span></span>
<span><span>}</span></span></code></pre><p>相比之下，是不是感觉函数式的写法更简洁呢？
</p><p>那么，怎么做到和​<code class="">ArticleList</code>​组件​<code class="">componentDidMount</code>​生命周期相同的功能呢？这就需要用到另一个Hook：​<code class="">useEffect</code>​。
</p><pre tabindex="0"><code><span><span>import</span><span> { useState</span><span>,</span><span> useEffect } </span><span>from</span><span> '</span><span>react</span><span>'</span><span>;</span></span>
<span></span>
<span><span>const</span><span> ArticleDetail</span><span> =</span><span> ()</span><span> =></span><span> {</span></span>
<span><span>  const</span><span> {</span><span> articleId</span><span> }</span><span> =</span><span> useParams</span><span>()</span><span>;</span></span>
<span><span>  const</span><span> [</span><span>body</span><span>,</span><span> setBody</span><span>]</span><span> =</span><span> useState</span><span>(</span><span>''</span><span>)</span><span>;</span></span>
<span></span>
<span><span>  useEffect</span><span>(</span><span>()</span><span> =></span><span> {</span></span>
<span><span>    fetch</span><span>(</span><span>`</span><span>/api/articles/</span><span>${</span><span>articleId</span><span>}</span><span>`</span><span>)</span></span>
<span><span>      .</span><span>then</span><span>(</span><span>response</span><span> =></span><span> response</span><span>.</span><span>json</span><span>())</span></span>
<span><span>      .</span><span>then</span><span>(</span><span>result</span><span> =></span><span> setBody</span><span>(</span><span>result</span><span>.</span><span>body</span><span>))</span></span>
<span><span>      .</span><span>catch</span><span>(</span><span>console</span><span>.</span><span>error</span><span>)</span><span>;</span></span>
<span><span>  }</span><span>,</span><span> [</span><span>articleId</span><span>])</span><span>;</span></span>
<span></span>
<span><span>  return</span><span> (</span></span>
<span><span>    &#x3C;</span><span>main</span><span>></span></span>
<span><span>      &#x3C;</span><span>div</span><span> className</span><span>=</span><span>"</span><span>m-2 text-center</span><span>"</span><span>></span><span>{</span><span>body</span><span>}</span><span>&#x3C;/</span><span>div</span><span>></span></span>
<span><span>    &#x3C;/</span><span>main</span><span>></span></span>
<span><span>  )</span></span>
<span><span>}</span></span></code></pre><p>稍微给正文加点样式方便区分，注意useEffect函数的两个参数，第一个参数是一个回调函数，通过GET请求获取后端的文章详情，这里用到了从路由参数获得的​<code class="">articleId</code>​来拼接字符串得到所需文章的后端地址，序列化响应后通过setBody设置body的值；第二个参数是一个列表，useEffect会监视列表中的值，当值发生变化时将会重新执行回调函数更新组件，如果这个列表设置为空将会只执行一次就结束。
</p><h2 id="user-content-注意事项" class="">注意事项<a class="" tabindex="-1" href="#注意事项">#</a></h2><p>使用Hook有一些注意事项：
</p><ul><li>Hooks只能出现在函数式组件或自定义Hook中
</li><li>Hooks必须在最顶层（不能在条件语句、循环体或者嵌套函数内部）
</li></ul><p>如果有时候你想在某个条件下才执行​<code class="">useEffect</code>​的内容，那么不要把整个Hook放到条件语句内，而是在​<code class="">useEffect</code>​的回调函数内部做条件判断。
</p><p>开发者可以自定义Hook来复用代码，例如多个组件都需要一个获取用户信息的逻辑，那么就可以封装一个自定义Hook：
</p><pre tabindex="0"><code><span><span>function</span><span> useUser</span><span>(</span><span>friendID</span><span>)</span><span> {</span></span>
<span><span>  const</span><span> [</span><span>user</span><span>,</span><span> setUser</span><span>]</span><span> =</span><span> useState</span><span>(</span><span>null</span><span>);</span></span>
<span><span>  useEffect</span><span>(</span><span>......</span><span>)</span></span>
<span></span>
<span><span>  return</span><span> user</span><span>;</span></span>
<span><span>}</span></span></code></pre><p>一般约定这类自定义Hook函数以​<code class="">use</code>​开发，回顾之前的代码，可以发现，其实我们早就已经用到了一个Hook函数：​<code class="">useParams</code>​。
</p><h2 id="user-content-练习" class="">练习<a class="" tabindex="-1" href="#练习">#</a></h2><p>读者可以尝试在页面上呈现文章标题创建日期等其他信息。
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>百宝箱：数字人生</title>
          <link>https://elliot00.com/posts/digital-life</link>
          <description>本文探讨了一个数字化人生的框架，囊括了笔记记录、知识管理、时间管理等方面。作者使用 TiddlyWiki 搭建了个人公开 wiki，满足随时可用、多终端同步、方便地 Review 等需求。而 Logseq 作为私人笔记工具，实现了双向链接和知识图谱。在知识管理上，作者结合 OKR 和 PARA 两个方法，将长期目标与关键结果转化为具体项目，并通过 Logseq 的查询功能以及自定义 CSS 实现 GTD 四象限面板。时间管理方面，作者利用 Logseq 的 Schedule 和 Deadline 特性来安排每日待办事项。文章结尾强调了构建“第二大脑”需要“第一大脑”的帮助，回归思考与检索，将碎片知识转化为有用的信息。</description>
          <pubDate>Sun, 15 Aug 2021 07:46:19 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <p>我是一个非常喜欢折腾的人，经常去尝试一些新鲜的工具、技术。在信息时代，互联网使得信息的获取变得极为便利，随时可以获取我想要的大量资料。但是，人的时间是有限的，大脑容量也是有限的。面对繁杂的信息，如何将其转化为自己的知识，甚而转化为实际产出，如一篇博客，或是一个开源软件？
</p><p>在一些科幻作品中，存在将人的大脑数据转移到计算机中，抛弃肉体的技术。虽然当下没有这种技术，但仍可以借助信息媒介，管理与保存各类信息，萨特认为“人就是他行为的总和”，来试试用电信号来存储人生的一部分吧～
</p><p>数字化人生，当然离不开软件的辅助，我设想的工具大致要满足以下几点：
</p><ul><li>最好是开源的
</li><li>随时可用，多终端同步
</li><li>知识获取 -> 知识提炼 -> 知识输出
</li><li>时间管理
</li><li>方便地Review
</li><li>文本使用通用的格式如Markdown（方便迁移）
</li></ul><p>通过一段时间的实践，我找到了一些合适的工具来处理我的需求。
</p><h2 id="user-content-笔记记录" class="">笔记记录<a class="" tabindex="-1" href="#笔记记录">#</a></h2><p>相比博客，笔记要碎片化一些，并且其中有一部分属于隐私无法公开。对于公开的部分，我选择了<a href="https://tiddlywiki.com/">TiddlyWiki</a>来制作一个公开的个人wiki站点。
</p><p>TiddlyWiki诞生于2004年，它最与众不同的一点是，它仅仅是一个HTML文件。这意味着你可以随意定制，轻松备份，快速部署。
</p><p>这个笔记应用非常轻量，功能也比较简单，不过社区插件还是很丰富的，官方有句话叫“Hackability as a Human Right"，用户自己的内容应该可以由用户自由处理，而不是被牢牢锁在某个应用的笼子里，这也是我尽量避免使用商业软件的原因。
</p><p>个人笔记记载的内容可以涉及很多领域，这其中可能会有些与个人生活之类相关的往往是不适宜公开的，此外，随手的记录一般还是需要经过一些润色处理，如果一开始就要求记录语句通顺逻辑清晰，那么是不利于养成随手记录的习惯的，所以我还使用了另一款笔记应用来记录私人的笔记内容，那就是<a href="https://logseq.com/">Logseq</a>。
</p><p>相比TiddlyWiki，Logseq原生具有双向链接的特性，例如我最近有一个滑动窗口相关的笔记，我可以在这篇笔记内写上​<code class="">[[滑动窗口算法]]</code>​，当前笔记页面就会链接到”滑动窗口算法“这个页面上，并且在”滑动窗口算法”这个页面会自动获得到当前笔记的链接。
</p><p><img alt="example" src="https://r2.elliot00.com/legacy/2021-08-14%2015-29-45%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE.png" width="799" height="536"></p><p>所有的关联都是双向的，这样的好处是，我不用手动去建立树形关联，只要所有与​<em>滑动窗口算法</em>​相关的页面都有指向它的链接，就相当于构建了一棵滑动窗口算法相关的知识树。Logseq会帮你构建一个知识图谱，显示出各个笔记页面之间的关联。
</p><p><img alt="关系图谱" src="https://r2.elliot00.com/legacy/2021-08-14%2016-12-07%E5%B1%8F%E5%B9%95%E6%88%AA%E5%9B%BE.png" width="422" height="380"></p><p>与TiddlyWiki纯文本的特性类似，Logseq所有的笔记都是​<code class="">Markdown</code>​文件，笔记不会上传到云端，所有的操作都是针对本地的文件（包括它的Web版），不过由于所有笔记都是文件的特性，不论是迁移、备份都非常方便，官方也提供使用Github同步的方式，但是为了避免污染Github提交记录，我选择使用坚果云在不同设备之间同步笔记。
</p><p><img alt="pipeline" src="https://r2.elliot00.com/legacy/pipeline.png" width="684" height="117"></p><p>在Logseq上积攒的内容，我会从中挑选一些分享到个人wiki上，如果在一个领域积攒的内容达到一定程度，那么就可以考虑写一篇博客来总结了。一个完整的从知识的获取到内化再到输出的路线形成了。
</p><p>然而，在获取信息的过程中，一般会碰到不同领域的资源，从各种渠道发现的一篇不错的关于devops的文章、高效命令行工具、新的美观的字体、美食的制作视频等等，“稍后再看”越积越多，最后变成了“再也不看”。如何管理这些繁杂的内容？
</p><h2 id="user-content-知识管理" class="">知识管理<a class="" tabindex="-1" href="#知识管理">#</a></h2><h3 id="user-content-okr与para">OKR与PARA<a class="" tabindex="-1" href="#okr与para">#</a></h3><p>OKR是Objective &#x26; Key Results的缩写，意思是目标与关键结果。
</p><p><img alt="OKR" src="https://r2.elliot00.com/legacy/okr.png" width="1400" height="850"></p><p>OKR的关键点，是将目标拆解为数个关键性的事项，并且在执行过程中不断地review。
</p><p>PARA则是Projects、Areas、Resources、Archives四个单词的缩写，它们分别代表：
</p><ul><li>项目：当前聚焦的工作，有明确的目标与时间范围，例如准备月底做一次演讲，一个月后体重减少5斤
</li><li>领域：需要持续付出时间的活动领域，如健康、收入、专业技能，项目都属于某个领域
</li><li>资源：知识储备，一闪而过的灵感、想法等，要与当前聚焦的领域-项目相关
</li><li>归档：当前非聚焦领域的想法、资源
</li></ul><h3 id="user-content-实践">实践<a class="" tabindex="-1" href="#实践">#</a></h3><p>以上是一些理论，在具体的实践中，我参考了<a href="https://index.pmthinking.com/para--notion-">P.A.R.A. 的 Notion 实践</a>和<a href="https://www.bmpi.dev/self/okr-gtd-note-logseq/">OKR + GTD + Note => Logseq</a>这两篇文章，将这两个方法结合起来，OKR的Objects与PARA的Areas是类似的，而KR则类似Projects。参考关于OKR的文章，我暂时以年为尺度，划定年度目标，建立一个​<code class="">2021OKR</code>​页面，在长期关注的领域，例如社区影响力，添加上一个Key Result或者说Project：平均每月产出一篇博客（12篇博客，截至日期为2021年末）。使用一个有规律的名字，如​<code class="">2021-OKR-O4-KR-1</code>​为其打上标记（这样方便程序化处理）：
</p><p><img alt="OKR example" src="https://r2.elliot00.com/legacy/OKR-example.png" width="742" height="447"></p><p>每个KR后面都有一个标签，标签指向另一个页面，这个页面用来汇总review。先看一下如何拆解项目，Logseq中通过​<code class="">/</code>​键可以使用指令，输入​<code class="">/TODO</code>​回车就可以创建一个TODO，可以更改状态为DOING或DONE，每个TODO块会记录从DOING到DONE的时间。但是并不需要每次都在review页面新建TODO，Logseq每天会自动新建一个以当前日期命名的页面，只要每日的TODO关联上相应的review页，利用双向链接的特性，在review页可以浏览到所有关联的TODO。
</p><p>此外，Logseq还基于<a href="https://en.wikipedia.org/wiki/Datalog">Datalog</a>查询语言实现了强大的查询功能，例如统计当前KR相关的打卡次数：
</p><pre tabindex="0"><code><span><span>#+BEGIN_QUERY</span></span>
<span><span>{</span><span>:title</span><span> "</span><span>打卡（次数）</span><span>"</span></span>
<span><span> :query</span><span> [</span><span>:find</span><span> (</span><span>count</span><span> ?b)</span></span>
<span><span>         :in</span><span> $ ?current-page</span></span>
<span><span>         :where</span></span>
<span><span>         [?p </span><span>:page/name</span><span> ?current-page]</span></span>
<span><span>         [?b </span><span>:block/marker</span><span> ?marker]</span></span>
<span><span>         [?b </span><span>:block/ref-pages</span><span> ?p]</span></span>
<span><span>         [(</span><span>=</span><span> "</span><span>DONE</span><span>"</span><span> ?marker)]]</span></span>
<span><span> :inputs</span><span> [</span><span>:current-page</span><span>]}</span></span>
<span><span>#+END_QUERY</span></span></code></pre><p>这类查询代码在Logseq里可以右键设置为template，使用​<code class="">/Template</code>​命令就可以快速粘贴模板，不用重复输入。
</p><p>现在总结一下我的工作流：
</p><p><img alt="practice" src="https://r2.elliot00.com/legacy/practice.png" width="799" height="423"></p><p>确定一些当前需要关注的领域的项目，当我在Github、v2ex、twitter等平台发现感兴趣的技术、觉得有价值文章、想法，可以随手记录下来，只有与当前项目相关的内容属于Resources，其余内容放入Archives归档，不时review项目，根据完成情况调整项目数量，如果项目太多，时间不够充裕，那么就降低项目优先级，甚至取消项目，暂时放入归档；反过来如果时间精力充足，那么归档中的内容也可以调整为激活的项目。回顾，整理，是这个工作流程的核心。
</p><h2 id="user-content-时间管理" class="">时间管理<a class="" tabindex="-1" href="#时间管理">#</a></h2><p>前面提到的<a href="https://www.bmpi.dev/self/okr-gtd-note-logseq/">文章</a>里讲到了如何利用查询语句和自定义​<code class="">CSS</code>​实现GTD四象限面板，其实除了普通的待办外，Logseq还支持Schedule与Deadline：
</p><p><img alt="示例" src="https://r2.elliot00.com/legacy/2021-08-15_14-53.png" width="458" height="373"></p><p>如上图示例，可以用这两种形式来表示需要每日重复执行的内容，而一些临时的或者与当前关注的项目无关的内容可以使用普通TODO，每天打开Logseq后，我会先计划今天需要完成的事项，优先完成与项目关联的事项，日程与deadline可以提醒我事项的紧急程度。
</p><h2 id="user-content-总结" class="">总结<a class="" tabindex="-1" href="#总结">#</a></h2><p><img alt="notes-graph" src="https://r2.elliot00.com/legacy/2021-08-15_15-02.png" width="457" height="390"></p><p>上图是我使用Logseq两周后形成的知识图谱，好像宇宙中的星系一样，已经有几个“恒星”崭露头角。
</p><p>现在有很多关于知识管理、人生管理的概念，最近频繁听到一个词：​<strong>第二大脑</strong>​。确实，人的大脑没有像异步运行时一样的任务调度器，人脑是不适合自上而下的思维的，当我们想要探索一个领域的知识，沿着一个线索可以发现其无数的分支，最后很可能会陷入信息的海洋，越来越迷失最初的方向。所以，我们需要工具来辅助。
</p><p>通过一段时间的实践，我想，要想建立“第二大脑”，最重要的还是“第一大脑”的帮助。将随手网罗的碎片知识丢在储藏室里就此不管是没有用的，不停的回顾，整理，检索，建立关联，对真理的探究，离不开自己的思考。
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>Django+React全栈开发：JWT</title>
          <link>https://elliot00.com/posts/react-django-jwt</link>
          <description>这篇教程为如何在 Django 后端实现 JWT 认证提供了一个示例。它讨论了 HTTP 无状态的含义以及 JWT 是什么。然后，它解释了如何创建新的 Django 应用程序并设置用于身份验证的视图。接下来，它描述了如何使用 python-jose 库来生成和验证 JWT 令牌。它还解释了如何将 JWTAuthentication 类用作自定义认证类，以便在需要认证的视图中使用它。最后，它展示了发送 HTTP 请求的示例，以使用有效的 JWT 令牌验证用户。总的来说，这篇教程提供了有关如何在 Django 后端中实现 JWT 认证的全面概述，包括所有必要的步骤和代码示例。</description>
          <pubDate>Mon, 05 Jul 2021 15:03:21 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <p>在很多有关网络协议的教程里，大概都能看到「​<code class="">HTTP</code>​协议是无状态的」这句话，​<strong>无状态</strong>​主要是指HTTP不会记忆当前连接的状态，不同请求之间相互独立。
</p><p>可以举个不太准确的例子，无状态就像一些不那么智能的语音助手，你对它说：“我想买双鞋”，它可能会为你打开购物网站，而接下来你说：“最便宜的多少钱？”，它可能就无法识别你在说什么，因为它没有联系上一段对话。与无状态协议相对的是有状态协议，例如一些通信协议会要求“握手”，完成握手后才能继续其他连接。
</p><p>因为这个特性，服务端无法直接了解到当前请求的用户是谁，因此需要一些辅助手段来做身份验证。**JWT（JSON Web Token）**就是其中一种。从名字大概可以看出，它将​<code class="">JSON</code>​编码成一串固定格式的字符串，作为身份验证的令牌。更多详情可以查看<a href="https://jwt.io">jwt.io</a>。
</p><h2 id="user-content-创建应用" class="">创建应用<a class="" tabindex="-1" href="#创建应用">#</a></h2><p>现在来尝试写一个用于JWT认证的应用，首先用命令​<code class="">python manage.py startapp jwt_auth</code>​新建一个应用。
</p><p>首先在​<code class="">jwt_auth</code>​这个应用下新建一个登录视图：
</p><pre tabindex="0"><code><span><span>from</span><span> rest_framework</span><span>.</span><span>decorators </span><span>import</span><span> api_view</span></span>
<span><span>from</span><span> django</span><span>.</span><span>contrib</span><span>.</span><span>auth </span><span>import</span><span> authenticate</span></span>
<span></span>
<span></span>
<span><span>@</span><span>api_view</span><span>(</span><span>[</span><span>'</span><span>POST</span><span>'</span><span>]</span><span>)</span></span>
<span><span>def</span><span> login</span><span>(</span><span>request</span><span>)</span><span>:</span></span>
<span><span>    username </span><span>=</span><span> request</span><span>.</span><span>data</span><span>[</span><span>"</span><span>username</span><span>"</span><span>]</span></span>
<span><span>    password </span><span>=</span><span> request</span><span>.</span><span>data</span><span>[</span><span>"</span><span>password</span><span>"</span><span>]</span></span>
<span><span>    user </span><span>=</span><span> authenticate</span><span>(</span><span>request</span><span>,</span><span> username</span><span>=</span><span>username</span><span>,</span><span> password</span><span>=</span><span>password</span><span>)</span></span></code></pre><p>登录视图要求从请求体中取出用户名和密码，通过Django内置的​<code class="">authenticate</code>​函数验证用户名密码是否正确，如果用户名密码正确，这个函数会返回对应的​<code class="">User</code>​对象，否则返回​<code class="">None</code>​。下一步我们要做的就是生成一个​<code class="">Token</code>​，当作这个用户的“签名”，后续需要验证的请求里，只要看到这个“签名”，就表示是这个用户本人的操作。
</p><h2 id="user-content-登录视图" class="">登录视图<a class="" tabindex="-1" href="#登录视图">#</a></h2><p>可以通过​<code class="">python-jose</code>​这个库来生成和验证​<code class="">JWT</code>​。首先安装它：
</p><pre tabindex="0"><code><span><span>pip</span><span> install</span><span> python-jose[cryptography]</span></span></code></pre><p>使用方法如下：
</p><pre tabindex="0"><code><span><span>>>></span><span> from</span><span> jose </span><span>import</span><span> jwt</span></span>
<span><span>>>></span><span> token </span><span>=</span><span> jwt</span><span>.</span><span>encode</span><span>(</span><span>{</span><span>'</span><span>key</span><span>'</span><span>: </span><span>'</span><span>value</span><span>'</span><span>}</span><span>,</span><span> '</span><span>secret</span><span>'</span><span>,</span><span> algorithm</span><span>=</span><span>'</span><span>HS256</span><span>'</span><span>)</span></span>
<span><span>u</span><span>'</span><span>eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJrZXkiOiJ2YWx1ZSJ9.FG-8UppwHaFp1LgRYQQeS6EDQF7_6-bMFegNucHjmWg</span><span>'</span></span>
<span></span>
<span><span>>>></span><span> jwt</span><span>.</span><span>decode</span><span>(</span><span>token</span><span>,</span><span> '</span><span>secret</span><span>'</span><span>,</span><span> algorithms</span><span>=</span><span>[</span><span>'</span><span>HS256</span><span>'</span><span>]</span><span>)</span></span>
<span><span>{</span><span>u</span><span>'</span><span>key</span><span>'</span><span>:</span><span> u</span><span>'</span><span>value</span><span>'</span><span>}</span></span></code></pre><p>给​<code class="">jwt.encode</code>​的三个参数非别是要编码的值，密钥和签名算法。密钥可以用一个随机生成的字符串，例如使用​<code class="">openssl rand -hex 32</code>​命令生成一个32位随机数。
</p><p>密钥和签名算法这两个固定的配置项在实际代码中推荐不要像上面的示例一样直接写字面量，可以在项目的​<code class="">settings.py</code>​中定义：
</p><pre tabindex="0"><code><span><span>JWT_SECRET_KEY</span><span> =</span><span> "</span><span>0dcb42e12219beab48e811926bedaf827fe99acdad44ba381117d7e29648acf4</span><span>"</span></span>
<span><span>ALGORITHM</span><span> =</span><span> "</span><span>HS256</span><span>"</span></span>
<span><span>ACCESS_TOKEN_EXPIRE_MINUTES</span><span> =</span><span> 30</span></span></code></pre><p>还有一个token的过期时间，可以按需定义，这里先设成30分钟，后面会用到。项目的​<code class="">settings.py</code>​中的内容可以通过​<code class="">from django.conf import settings</code>​获取到。这样如果我们的单个应用需要打包发布出去，使用这个应用的用户不至于无法配置这些选项。
</p><p>实际代码：
</p><pre tabindex="0"><code><span><span>data </span><span>=</span><span> {</span><span>"</span><span>sub</span><span>"</span><span>:</span><span> user</span><span>.</span><span>username</span><span>,</span><span> "</span><span>exp</span><span>"</span><span>:</span><span> datetime</span><span>.</span><span>utcnow</span><span>()</span><span> +</span><span> timedelta</span><span>(</span><span>minutes</span><span>=</span><span>settings.ACCESS_TOKEN_EXPIRE_MINUTES</span><span>)}</span></span>
<span><span>token </span><span>=</span><span> jwt</span><span>.</span><span>encode</span><span>(</span><span>data</span><span>,</span><span> settings.JWT_SECRET_KEY</span><span>,</span><span> algorithm</span><span>=</span><span>settings.ALGORITHM</span><span>)</span></span>
<span><span>return</span><span> Response</span><span>(</span><span>data</span><span>=</span><span>{</span><span>"</span><span>access_token</span><span>"</span><span>: token}</span><span>,</span><span> status</span><span>=</span><span>status.HTTP_200_OK</span><span>)</span></span></code></pre><p>将当前用户名和过期时间作为encode的第一个参数，注意​<code class="">HS356</code>​算法用来生成签名摘要，不是安全的加密算法，因此不要把敏感信息放到​<code class="">data</code>​中去。
</p><p>完整视图代码如下：
</p><pre tabindex="0"><code><span><span>from</span><span> datetime </span><span>import</span><span> datetime</span><span>,</span><span> timedelta</span></span>
<span></span>
<span><span>from</span><span> django</span><span>.</span><span>conf </span><span>import</span><span> settings</span></span>
<span><span>from</span><span> jose </span><span>import</span><span> jwt</span></span>
<span><span>from</span><span> rest_framework </span><span>import</span><span> status</span></span>
<span><span>from</span><span> rest_framework</span><span>.</span><span>decorators </span><span>import</span><span> api_view</span></span>
<span><span>from</span><span> rest_framework</span><span>.</span><span>response </span><span>import</span><span> Response</span></span>
<span><span>from</span><span> django</span><span>.</span><span>contrib</span><span>.</span><span>auth </span><span>import</span><span> authenticate</span></span>
<span></span>
<span></span>
<span><span>@</span><span>api_view</span><span>(</span><span>[</span><span>'</span><span>POST</span><span>'</span><span>]</span><span>)</span></span>
<span><span>def</span><span> login</span><span>(</span><span>request</span><span>)</span><span>:</span></span>
<span><span>    username </span><span>=</span><span> request</span><span>.</span><span>data</span><span>[</span><span>"</span><span>username</span><span>"</span><span>]</span></span>
<span><span>    password </span><span>=</span><span> request</span><span>.</span><span>data</span><span>[</span><span>"</span><span>password</span><span>"</span><span>]</span></span>
<span><span>    user </span><span>=</span><span> authenticate</span><span>(</span><span>request</span><span>,</span><span> username</span><span>=</span><span>username</span><span>,</span><span> password</span><span>=</span><span>password</span><span>)</span></span>
<span><span>    if</span><span> user </span><span>is</span><span> not</span><span> None</span><span>:</span></span>
<span><span>        data </span><span>=</span><span> {</span><span>"</span><span>sub</span><span>"</span><span>:</span><span> user</span><span>.</span><span>username</span><span>,</span><span> "</span><span>exp</span><span>"</span><span>:</span><span> datetime</span><span>.</span><span>utcnow</span><span>()</span><span> +</span><span> timedelta</span><span>(</span><span>minutes</span><span>=</span><span>settings.ACCESS_TOKEN_EXPIRE_MINUTES</span><span>)}</span></span>
<span><span>        token </span><span>=</span><span> jwt</span><span>.</span><span>encode</span><span>(</span><span>data</span><span>,</span><span> settings.JWT_SECRET_KEY</span><span>,</span><span> algorithm</span><span>=</span><span>settings.ALGORITHM</span><span>)</span></span>
<span><span>        return</span><span> Response</span><span>(</span><span>data</span><span>=</span><span>{</span><span>"</span><span>access_token</span><span>"</span><span>: token}</span><span>,</span><span> status</span><span>=</span><span>status.HTTP_200_OK</span><span>)</span></span>
<span><span>    return</span><span> Response</span><span>(</span><span>status</span><span>=</span><span>status.HTTP_401_UNAUTHORIZED</span><span>)</span></span></code></pre><p>下一步给这个视图注册Url：
</p><pre tabindex="0"><code><span><span># 新建jwt_auth/urls.py</span></span>
<span><span>from</span><span> django</span><span>.</span><span>urls </span><span>import</span><span> path</span></span>
<span><span>from</span><span> .</span><span>views </span><span>import</span><span> login</span></span>
<span></span>
<span></span>
<span><span>urlpatterns </span><span>=</span><span> [</span></span>
<span><span>    path</span><span>(</span><span>'</span><span>login/</span><span>'</span><span>,</span><span> login</span><span>),</span></span>
<span><span>]</span></span>
<span></span>
<span><span># 项目主目录的urls.py也要修改</span></span>
<span><span>urlpatterns </span><span>=</span><span> [</span></span>
<span><span>    path</span><span>(</span><span>'</span><span>admin/</span><span>'</span><span>,</span><span> admin.site.urls</span><span>),</span></span>
<span><span>    path</span><span>(</span><span>'</span><span>api/</span><span>'</span><span>,</span><span> include</span><span>(</span><span>'</span><span>article.urls</span><span>'</span><span>)),</span></span>
<span><span>    path</span><span>(</span><span>'</span><span>auth/</span><span>'</span><span>,</span><span> include</span><span>(</span><span>'</span><span>jwt_auth.urls</span><span>'</span><span>))</span></span>
<span><span>]</span></span></code></pre><p>可以启动项目验证一下：
</p><pre tabindex="0"><code><span><span>$</span><span> http</span><span> POST</span><span> http://127.0.0.1:8000/auth/login/</span><span> username=</span><span>"</span><span>elliot</span><span>"</span><span> password=</span><span>"</span><span>12345678</span><span>"</span></span>
<span><span>{</span></span>
<span><span>    "access_token"</span><span>:</span><span> "</span><span>eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJlbGxpb3QiLCJleHAiOjE2MjU0OTU3MzZ9.JY9urGY27liuKdRvvdEEdpktHDpaxb7GF_qz63NyzcQ</span><span>"</span></span>
<span><span>}</span></span></code></pre><p>发送正确的请求可以得到一个包含JWT的响应，得到的JWT要怎么使用呢？
</p><h2 id="user-content-authentication" class="">Authentication<a class="" tabindex="-1" href="#authentication">#</a></h2><pre tabindex="0"><code><span><span># 新建jwt_auth/authentication.py</span></span>
<span></span>
<span><span>from</span><span> rest_framework</span><span>.</span><span>authentication </span><span>import</span><span> BaseAuthentication</span></span>
<span></span>
<span></span>
<span><span>class</span><span> JWTAuthentication</span><span>(</span><span>BaseAuthentication</span><span>):</span></span>
<span></span>
<span><span>    def</span><span> authenticate</span><span>(</span><span>self</span><span>,</span><span> request</span><span>)</span><span>:</span></span>
<span><span>        pass</span></span></code></pre><p>自定义一个​<code class="">JWTAuthentication</code>​类，继承DRF框架内的​<code class="">BaseAuthentication</code>​，我们只需要实现​<code class="">authenticate</code>​方法就可以完成自定义的认证类。当验证成功时这个方法方法返回一个元组，元组第一个元素是对应的用户，验证失败的时候，我们可以直接抛出一个​<code class="">AuthenticationFailed</code>​错误。
</p><p>实际代码：
</p><pre tabindex="0"><code><span><span>from</span><span> django</span><span>.</span><span>contrib</span><span>.</span><span>auth</span><span>.</span><span>models </span><span>import</span><span> User</span></span>
<span><span>from</span><span> rest_framework</span><span>.</span><span>authentication </span><span>import</span><span> BaseAuthentication</span></span>
<span><span>from</span><span> rest_framework</span><span>.</span><span>exceptions </span><span>import</span><span> AuthenticationFailed</span></span>
<span><span>from</span><span> django</span><span>.</span><span>conf </span><span>import</span><span> settings</span></span>
<span><span>from</span><span> jose </span><span>import</span><span> jwt</span></span>
<span></span>
<span></span>
<span><span>class</span><span> JWTAuthentication</span><span>(</span><span>BaseAuthentication</span><span>):</span></span>
<span></span>
<span><span>    def</span><span> authenticate</span><span>(</span><span>self</span><span>,</span><span> request</span><span>)</span><span>:</span></span>
<span><span>        # 从请求头取出token</span></span>
<span><span>        header </span><span>=</span><span> request</span><span>.</span><span>META</span><span>.</span><span>get</span><span>(</span><span>'</span><span>HTTP_AUTHORIZATION</span><span>'</span><span>)</span></span>
<span><span>        try</span><span>:</span></span>
<span><span>            token </span><span>=</span><span> header</span><span>.</span><span>split</span><span>()</span><span>[</span><span>1</span><span>]</span></span>
<span><span>            user </span><span>=</span><span> self</span><span>.</span><span>get_user</span><span>(</span><span>token</span><span>)</span></span>
<span><span>            return</span><span> user</span><span>,</span><span> None</span></span>
<span><span>        except</span><span> Exception</span><span>:</span></span>
<span><span>            raise</span><span> AuthenticationFailed</span><span>(</span><span>'</span><span>Authentication Failed</span><span>'</span><span>)</span></span>
<span></span>
<span><span>    def</span><span> get_user</span><span>(</span><span>self</span><span>,</span><span> token</span><span>)</span><span>:</span></span>
<span><span>        # 验证token并解码数据取出用户名</span></span>
<span><span>        payload </span><span>=</span><span> jwt</span><span>.</span><span>decode</span><span>(</span><span>token</span><span>,</span><span> settings.JWT_SECRET_KEY</span><span>,</span><span> algorithms</span><span>=</span><span>[</span><span>settings.ALGORITHM</span><span>]</span><span>)</span></span>
<span><span>        username </span><span>=</span><span> payload</span><span>.</span><span>get</span><span>(</span><span>"</span><span>sub</span><span>"</span><span>)</span></span>
<span><span>        # 根据用户名取出对应用户</span></span>
<span><span>        user </span><span>=</span><span> User</span><span>.</span><span>objects</span><span>.</span><span>get</span><span>(</span><span>username</span><span>=</span><span>username</span><span>)</span></span>
<span><span>        return</span><span> user</span></span></code></pre><p>现在可以在需要认证的视图里，引用自定义的JWT认证类。
</p><pre tabindex="0"><code><span><span>from</span><span> jwt_auth</span><span>.</span><span>authentication </span><span>import</span><span> JWTAuthentication</span></span>
<span></span>
<span></span>
<span><span>class</span><span> ArticleViewSet</span><span>(</span><span>viewsets</span><span>.</span><span>ModelViewSet</span><span>):</span></span>
<span><span>    ...</span></span>
<span><span>    authentication_classes </span><span>=</span><span> [</span><span>JWTAuthentication</span><span>]</span></span></code></pre><p>验证一下：
</p><pre tabindex="0"><code><span><span>$</span><span> http</span><span> GET</span><span> http://127.0.0.1:8000/api/articles/</span><span> '</span><span>Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJlbGxpb3QiLCJleHAiOjE2MjU0OTgwOTd9.oKveILGQ_C8cp33IZNjsz7pdvsMxVazd9CccCNXxSt0</span><span>'</span></span>
<span></span>
<span><span>[</span></span>
<span><span>    {</span></span>
<span><span>        "</span><span>author</span><span>"</span><span>: 1,</span></span>
<span><span>        "</span><span>body</span><span>"</span><span>: </span><span>"</span><span>author is readonly</span><span>"</span><span>,</span></span>
<span><span>        "</span><span>created</span><span>"</span><span>: </span><span>"</span><span>2021-04-18T07:39:31.175273Z</span><span>"</span><span>,</span></span>
<span><span>        "</span><span>id</span><span>"</span><span>: 8,</span></span>
<span><span>        "</span><span>title</span><span>"</span><span>: </span><span>"</span><span>author is readonly</span><span>"</span><span>,</span></span>
<span><span>        "</span><span>updated</span><span>"</span><span>: </span><span>"</span><span>2021-04-18T07:39:31.175525Z</span><span>"</span></span>
<span><span>    },</span></span>
<span><span>    ......</span></span>
<span><span>]</span></span></code></pre><p>注意请求头格式为​<em>Authorization: Bearer Token</em>​（仔细看代码会发现代码获取该请求头的键是​<code class="">HTTP_AUTHORIZATION</code>​，原因参见<a href="https://docs.djangoproject.com/en/3.2/ref/request-response/#django.http.HttpRequest.META">文档</a>），如果没有提供Token或者Token不合法，就无法获取文章信息了。
</p><h2 id="user-content-第三方库" class="">第三方库<a class="" tabindex="-1" href="#第三方库">#</a></h2><p>这篇文章中的代码量很少也很粗糙，仅仅是为了展示一下自定义JWT认证的流程，在实际使用中建议使用<a href="https://github.com/jazzband/djangorestframework-simplejwt">djangorestframwork-simplejwt</a>。
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>Dive into Rust: Object Oriented</title>
          <link>https://elliot00.com/posts/rust-object-oriented</link>
          <description>文章探讨了什么是面向对象编程，以及如何在Rust中实现面向对象编程。文章认为，面向对象编程不等于封装、继承、多态，继承和多态甚至不能算并列的概念。Rust没有继承，但可以通过trait来抽象共享行为，实现多态。文章还讨论了鸭子类型，以及如何以Rust的方式实现鸭子类型。文章最后总结了Rust中泛型与trait的详细用法，读者可以参考官方文档或其他资料。</description>
          <pubDate>Sun, 30 May 2021 12:47:27 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <p>如何快速自定义一个集合类型？熟悉一些面向对象语言的程序员可能会这么写：
</p><pre tabindex="0"><code><span><span>class</span><span> MyCollection</span><span>(</span><span>Collection</span><span>):</span></span>
<span><span>    ...</span></span></code></pre><p>继承某个内置的类型（如果存在的话），在该内置类型的基础上进行扩展。但是对于熟悉Python的程序员来说，这样做并不妥当。
</p><h2 id="user-content-鸭子类型" class="">鸭子类型<a class="" tabindex="-1" href="#鸭子类型">#</a></h2><blockquote><p>当看到一只鸟走起来像鸭子、游泳起来像鸭子、叫起来也像鸭子，那么这只鸟就可以被称为鸭子。
</p></blockquote><p>一个简单的例子：
</p><pre tabindex="0"><code><span><span>class</span><span> Collection</span><span>:</span></span>
<span><span>    def</span><span> __init__</span><span>(</span><span>self</span><span>,</span><span> nums</span><span>)</span><span>:</span></span>
<span><span>        self</span><span>.</span><span>_nums </span><span>=</span><span> list</span><span>(</span><span>nums</span><span>)</span></span>
<span></span>
<span><span>    def</span><span> __len__</span><span>(</span><span>self</span><span>)</span><span>:</span></span>
<span><span>        return</span><span> len</span><span>(</span><span>self</span><span>._nums</span><span>)</span></span>
<span></span>
<span><span>    def</span><span> __getitem__</span><span>(</span><span>self</span><span>,</span><span> position</span><span>)</span><span>:</span></span>
<span><span>        return</span><span> self</span><span>.</span><span>_nums</span><span>[</span><span>position</span><span>]</span></span>
<span></span>
<span><span>c </span><span>=</span><span> Collection</span><span>(</span><span>[</span><span>'</span><span>A</span><span>'</span><span>, </span><span>'</span><span>B</span><span>'</span><span>, </span><span>'</span><span>C</span><span>'</span><span>, </span><span>'</span><span>D</span><span>'</span><span>, </span><span>'</span><span>E</span><span>'</span><span>, </span><span>'</span><span>F</span><span>'</span><span>]</span><span>)</span></span>
<span></span>
<span><span># 可以对c调用内置的len方法获取长度</span></span>
<span><span>print</span><span>(</span><span>len</span><span>(</span><span>c</span><span>))</span><span>可以</span></span>
<span></span>
<span><span># 可以使用索引</span></span>
<span><span>print</span><span>(</span><span>c</span><span>[</span><span>2</span><span>])</span></span>
<span></span>
<span><span># 可以使用for循环遍历元素</span></span>
<span><span>for</span><span> i </span><span>in</span><span> c</span><span>:</span></span>
<span><span>    print</span><span>(</span><span>i</span><span>)</span></span>
<span><span>    </span></span>
<span><span># 还可以使用切片操作 </span></span>
<span><span>print</span><span>(</span><span>c</span><span>[</span><span>1</span><span>:</span><span>3</span><span>])</span></span></code></pre><p>看，这个自定义的​<code class="">Collection</code>​类并没有继承Python内置的​<code class="">Iterable</code>​之类的类型，但是表现得好像是某个内置的可迭代类型一样。
</p><p>这种前后都有双下划线的方法一般被称作魔术方法，它不应该被用户自己调用，它被解释器视作一种“协议”，无论你自定义的类是否和标准库的某个类型有继承关系，只要实现了对应的协议，就能使你自定义的类型拥有如索引、切片等功能。
</p><p>可以说对于我们自定义的​<code class="">Collection</code>​类型，Python并不关心它是否​<strong>是</strong>​集合类型的子类，而是它是否​<strong>具有</strong>​集合的“能力”。
</p><p>下面这个例子表明，​<code class="">Python</code>​甚至不要求在定义类时实现接口：
</p><pre tabindex="0"><code><span><span>from</span><span> collections</span><span>.</span><span>abc </span><span>import</span><span> Iterable</span></span>
<span></span>
<span><span>class</span><span> Statement</span><span>:</span></span>
<span><span>    def</span><span> __init__</span><span>(</span><span>self</span><span>,</span><span> string</span><span>)</span><span>:</span></span>
<span><span>        self</span><span>.</span><span>words </span><span>=</span><span> string</span><span>.</span><span>split</span><span>()</span></span>
<span></span>
<span><span>def</span><span> my_iter</span><span>(</span><span>self</span><span>)</span><span>:</span></span>
<span><span>    for</span><span> word </span><span>in</span><span> self</span><span>.</span><span>words</span><span>:</span></span>
<span><span>        yield</span><span> word</span></span>
<span></span>
<span><span>Statement</span><span>.</span><span>__iter__</span><span> =</span><span> my_iter</span></span>
<span></span>
<span><span>s </span><span>=</span><span> Statement</span><span>(</span><span>"</span><span>Python is a programming language that lets you work quickly and integrate systems more effectively</span><span>"</span><span>)</span></span>
<span></span>
<span><span>print</span><span>(</span><span>list</span><span>(</span><span>iter</span><span>(</span><span>s</span><span>)))</span></span>
<span></span>
<span><span>print</span><span>(</span><span>isinstance</span><span>(</span><span>s</span><span>,</span><span> Iterable</span><span>))</span><span> # True</span></span></code></pre><p>直接在运行时动态注册了​<code class="">Statement</code>​类的​<code class="">__iter__</code>​方法（这被社区称为猴子补丁，我本人并不喜欢在真实项目中使用），就可以对这个自定义类型调用​<code class="">iter</code>​函数，甚至​<code class="">isinstance(s, Iterable)</code>​返回的结果都是​<code class="">True</code>​。
</p><h2 id="user-content-mixin" class="">Mixin<a class="" tabindex="-1" href="#mixin">#</a></h2><p>之前在一次分享会上举过一个例子，如果在系统中有这样的继承链：
</p><pre tabindex="0"><code><span><span># 伪代码</span></span>
<span></span>
<span><span>class</span><span> 飞机</span><span>:</span></span>
<span><span>    def</span><span> 起飞</span><span>:</span></span>
<span><span>        pass</span></span>
<span></span>
<span><span>    def</span><span> 剩余油量</span><span>:</span></span>
<span><span>        pass</span></span>
<span></span>
<span><span>class</span><span> 直升机</span><span>(</span><span>飞机</span><span>):</span></span>
<span><span>    pass</span></span>
<span></span>
<span><span>class</span><span> 战斗机</span><span>(</span><span>飞机</span><span>):</span></span>
<span><span>    pass</span></span></code></pre><p>如果这个系统中需要引入一个新的飞行物对象，但是它表示的是海鸥，要怎么做呢？直接继承现有的飞机基类吗？如果这样做我们将获得一个拥有油量信息的钢铁海鸥，一个独特的新物种。或者可以再提取一个共同的抽象基类，这个抽象基类只有飞机和海鸥共同的部分。
</p><p>但是如果我们以鸭子类型的方式去思考，添加一个新类型，为什么一定确定它是某个类型的子类呢？不管是直升机还是海鸥，它们需要的共同点只是可以飞而已。
</p><p><code class="">Django</code>​是Python中流行的Web框架之一，在​<code class="">Django</code>​中可以这样定义视图：
</p><pre tabindex="0"><code><span><span>class</span><span> HybridDetailView</span><span>(</span><span>JSONResponseMixin</span><span>,</span><span> SingleObjectTemplateResponseMixin</span><span>,</span><span> BaseDetailView</span><span>):</span></span>
<span><span>    def</span><span> render_to_response</span><span>(</span><span>self</span><span>,</span><span> context</span><span>)</span><span>:</span></span>
<span><span>        # Look for a 'format=json' GET argument</span></span>
<span><span>        if</span><span> self</span><span>.</span><span>request</span><span>.</span><span>GET</span><span>.</span><span>get</span><span>(</span><span>'</span><span>format</span><span>'</span><span>)</span><span> ==</span><span> '</span><span>json</span><span>'</span><span>:</span></span>
<span><span>            return</span><span> self</span><span>.</span><span>render_to_json_response</span><span>(</span><span>context</span><span>)</span></span>
<span><span>        else</span><span>:</span></span>
<span><span>            return</span><span> super</span><span>().</span><span>render_to_response</span><span>(</span><span>context</span><span>)</span></span></code></pre><p><code class="">Django</code>​并没有定义一个全面的父类，而是定义了多个​<code class="">Mixin</code>​类，中文通常翻译成混入类，每个混入类负责一部分功能，例如​<code class="">JSONResponseMixin</code>​负责​<code class="">JSON</code>​响应，这有点类似​<code class="">CSharp</code>​、​<code class="">Java</code>​的​<code class="">interface</code>​（接口），不同的是​<code class="">CSharp</code>​不支持多重继承，但是允许实现多个接口，而Python中的​<code class="">Mixin</code>​只是个大家默认遵守的约定，可以这样写的原因在于Python支持多重继承，一个子类可以继承自多个父类。​<code class="">Mixin</code>​不应该影响到子类本身的功能，它应该抽象一个通用的功能用于扩展子类，其本身通常不能实例化。
</p><p>这样的代码在形式上是继承多个父类，但是从实际表现上看，更像是把不同混入类的功能​<strong>组合</strong>​起来。比如上面的代码里组合了​<code class="">JSON</code>​响应与模板响应的功能，根据请求返回不同类型的响应。混合鸭子的叫声、形态、飞行方式，就能得到一只定制的“鸭子”，这取决于你需要哪些功能。
</p><h2 id="user-content-rust中的面向对象" class="">Rust中的面向对象<a class="" tabindex="-1" href="#rust中的面向对象">#</a></h2><p>现在轮到主角Rust出场了。Rust是一门支持多范式的编程语言，其中包括面向对象范式。但是首先，到底什么是面向对象？借用一下官方教程<a href="https://doc.rust-lang.org/book/ch17-01-what-is-oo.html">The Book</a>的描述：如果按照<a href="https://en.wikipedia.org/wiki/Design_Patterns">GOF</a>对面向对象的定义，面向对象的程序由对象构成，对象将数据与操作数据的过程打包在一起，那​<code class="">Rust</code>​无疑是支持面向对象的，​<code class="">Rust</code>​由​<code class="">enum</code>​和​<code class="">struct</code>​组织数据，通过​<code class="">impl</code>​为它们绑定方法。
</p><p>但是，部分程序员可能要反对这个说法，部分人认为只有具备​<strong>封装、继承、多态</strong>​这样的形式，才算的上面向对象，而Rust甚至都没有​<code class="">class</code>​，就像有人认为JS和Python也不能完全算面向对象语言一样。
</p><h2 id="user-content-封装继承多态" class="">封装、继承、多态<a class="" tabindex="-1" href="#封装继承多态">#</a></h2><p>这三个词确实很深入人心，有可能每个软件工程师都听过，这里就讨论下在Rust中的这三个特性。
</p><p>首先说说封装，封装在我看来主要作用是隔离不同的抽象层级，底层开发负责实现细节，而在这上一层的开发者则只关心暴露出来的接口。例如​<code class="">Python</code>​中的​<code class="">list</code>​，我们知道它拥有接口让我们获取其内部的元素数量，而不必去了解内部实现细节，这是标准库开发人员负责的。如果我们在这个对象的基础上封装一个最小栈，可以通过​<code class="">min</code>​方法获取列表中的最小值，我们负责封装这个接口，至于我们是维护一个单独的栈保存最小值，还是在调用接口时遍历整个列表，是内部细节，这个类型的使用者无需知道。
</p><p>当然，对于部分语言来说，还提供了机制强制对外部调用者隐藏属性，Rust中就有<a href="https://doc.rust-lang.org/reference/visibility-and-privacy.html">pub</a>关键字来限制​<strong>可访问性</strong>​。
</p><pre tabindex="0"><code><span><span>mod</span><span> my_test {</span></span>
<span><span>    pub</span><span> struct</span><span> Test</span><span> {</span></span>
<span><span>        foo</span><span>:</span><span> i32</span><span>,</span></span>
<span><span>        pub</span><span> bar</span><span>:</span><span> i32</span><span>,</span></span>
<span><span>    }</span></span>
<span><span>}</span></span>
<span></span>
<span><span>fn</span><span> main</span><span>() {</span></span>
<span><span>    use</span><span> my_test</span><span>::</span><span>Test</span><span>;</span></span>
<span></span>
<span><span>    let</span><span> test</span><span> =</span><span> Test</span><span> { </span><span>foo</span><span>:</span><span> 1</span><span>, </span><span>bar</span><span>:</span><span> 2</span><span>}; </span><span>// 错误</span></span>
<span><span>}</span></span></code></pre><p>由于​<code class="">foo</code>​字段没有用​<code class="">pub</code>​关键字标识，所以它是一个私有字段，无法直接访问。
</p><p>接着是继承，​<strong>Rust中没有继承</strong>​。不能实现一个子结构体继承父结构体。继承主要有两个作用，一个是复用代码，子类自动获得父类的属性与方法，但是代码复用并不一定非用继承不可；另一个则用于​<strong>多态</strong>​，一个子类型可以被用在需要父类型的地方。
</p><p>这样看起来，多态和继承这两个概念相提并论就有点怪异了。继承成了实现多态的一种途径，多态的概念更宽泛一点。
</p><p>既然Rust没有继承，那么以上继承的两个功能（主要是后者）在Rust中要如何实现呢？多态要怎么实现呢？
</p><p>Rust可以通过​<code class="">trait</code>​来抽象共享行为，就以之前举的飞机的例子，各种飞机，还有海鸥，都可以飞行，但是具体飞行方式则有些不同：
</p><pre tabindex="0"><code><span><span>trait</span><span> Fly</span><span> {</span></span>
<span><span>    fn</span><span> fly</span><span>(</span><span>&#x26;</span><span>self</span><span>);</span></span>
<span><span>}</span></span>
<span></span>
<span><span>struct</span><span> Helicopter</span><span>;</span></span>
<span></span>
<span><span>// 为直升机实现飞行特性</span></span>
<span><span>impl</span><span> Fly</span><span> for</span><span> Helicopter</span><span> {</span></span>
<span><span>    fn</span><span> fly</span><span>(</span><span>&#x26;</span><span>self</span><span>) {</span></span>
<span><span>        println!</span><span>(</span><span>"</span><span>转动螺旋桨起飞</span><span>"</span><span>);</span></span>
<span><span>    }</span></span>
<span><span>}</span></span>
<span></span>
<span><span>struct</span><span> Seagull</span><span>;</span></span>
<span></span>
<span><span>impl</span><span> Fly</span><span> for</span><span> Seagull</span><span> {</span></span>
<span><span>    fn</span><span> fly</span><span>(</span><span>&#x26;</span><span>self</span><span>) {</span></span>
<span><span>        println!</span><span>(</span><span>"</span><span>扇动翅膀起飞</span><span>"</span><span>);</span></span>
<span><span>    }</span></span>
<span><span>}</span></span></code></pre><p>通过​<code class="">impl trait for struct/enum</code>​的语法，可以将一个功能抽象出来，针对不同的类型去实现，对比Python的Mixin，trait也可以​<strong>组合</strong>​，可以对一个类型实现多个trait。和Mixin以及C#的接口一样，trait也可以有默认实现。
</p><pre tabindex="0"><code><span><span>trait</span><span> Fly</span><span> {</span></span>
<span><span>    fn</span><span> fly</span><span>(</span><span>&#x26;</span><span>self</span><span>);</span></span>
<span><span>}</span></span>
<span></span>
<span><span>trait</span><span> Dashboard</span><span> {</span></span>
<span><span>    // 可以提供默认实现，当然在这里只是演示用，无意义</span></span>
<span><span>    fn</span><span> speed</span><span>(</span><span>&#x26;</span><span>self</span><span>) </span><span>-></span><span> i32</span><span> {</span></span>
<span><span>        100</span></span>
<span><span>    }</span></span>
<span><span>}</span></span>
<span></span>
<span><span>struct</span><span> Helicopter</span><span>;</span></span>
<span></span>
<span><span>impl</span><span> Fly</span><span> for</span><span> Helicopter</span><span> {</span></span>
<span><span>    fn</span><span> fly</span><span>(</span><span>&#x26;</span><span>self</span><span>) {</span></span>
<span><span>        println!</span><span>(</span><span>"</span><span>转动螺旋桨起飞</span><span>"</span><span>);</span></span>
<span><span>    }</span></span>
<span><span>}</span></span>
<span></span>
<span><span>// 可以使用默认实现，也可以覆盖默认行为</span></span>
<span><span>impl</span><span> Dashboard</span><span> for</span><span> Helicopter</span><span> {}</span></span></code></pre><p><code class="">trait</code>​的核心思想是组合，​<code class="">trait</code>​是对行为的抽象，不同的对象可以具有相似的行为，对象是数据与行为的组合。前面​<code class="">Django</code>​的例子中，虽然在语法上是多重继承，但本质上不也是组合吗？相比继承，组合更适合表示一个对象​<strong>具有某功能或特性</strong>​，而不是​<strong>是某个种类</strong>​。
</p><p>再看鸭子类型，当一个地方需要一只会叫的鸭子，只要我们提供的对象具有鸭子的叫声就行，这不正是多态吗？那么在Rust的类型系统中，如何表现多态呢？
</p><p>下面这段代码可以通过编译：
</p><pre tabindex="0"><code><span><span>enum</span><span> Status</span><span> {</span></span>
<span><span>    Successful</span><span>,</span></span>
<span><span>    Failed</span><span>,</span></span>
<span><span>}</span></span>
<span></span>
<span><span>fn</span><span> print_status</span><span>(</span><span>status</span><span>:</span><span> Status</span><span>) {</span></span>
<span><span>    match</span><span> status</span><span> {</span></span>
<span><span>        Status</span><span>::</span><span>Successful</span><span> =></span><span> println!</span><span>(</span><span>"</span><span>successful!</span><span>"</span><span>),</span></span>
<span><span>        Status</span><span>::</span><span>Failed</span><span> =></span><span> println!</span><span>(</span><span>"</span><span>failed</span><span>"</span><span>)</span></span>
<span><span>    }</span></span>
<span><span>}</span></span>
<span></span>
<span><span>fn</span><span> main</span><span>() {</span></span>
<span><span>    let</span><span> status</span><span> =</span><span> Status</span><span>::</span><span>Successful</span><span>;</span></span>
<span><span>    print_status</span><span>(</span><span>status</span><span>);</span></span>
<span><span>}</span></span></code></pre><p>当然，Rust的枚举在类型系统上是一个​<em>和类型</em>​，这里的​<code class="">Status::Successful</code>​和​<code class="">Status::Failed</code>​是同一个类型（​<code class="">Status</code>​），通常被称为​<code class="">variants</code>​（变体），再看另一个代码示例：
</p><pre tabindex="0"><code><span><span>// 省略了前面的结构体与trait定义部分</span></span>
<span></span>
<span><span>fn</span><span> main</span><span>() {</span></span>
<span><span>    let</span><span> h</span><span> =</span><span> Helicopter</span><span>;</span></span>
<span><span>    let</span><span> s</span><span> =</span><span> Seagull</span><span>;</span></span>
<span><span>    generic_func</span><span>(</span><span>h</span><span>);</span></span>
<span><span>    generic_func</span><span>(</span><span>s</span><span>);</span></span>
<span><span>}</span></span>
<span></span>
<span><span>fn</span><span> generic_func</span><span>&#x3C;</span><span>T</span><span>:</span><span> Fly</span><span>>(</span><span>flyable</span><span>:</span><span> T</span><span>) {</span></span>
<span><span>    flyable</span><span>.</span><span>fly</span><span>();</span></span>
<span><span>}</span></span></code></pre><p>代码可以通过编译，我在这里利用了​<strong>泛型</strong>​，自定义的函数需要一个​<code class="">T</code>​类型的参数，这个​<code class="">T</code>​类型被限定为：实现了Fly这个trait的类型，这被称为​<em>trait bounds</em>​。
</p><p>对代码稍作修改：
</p><pre tabindex="0"><code><span><span>use</span><span> std</span><span>::</span><span>fmt</span><span>::</span><span>Debug</span><span>;</span></span>
<span></span>
<span><span>trait</span><span> Fly</span><span> {</span></span>
<span><span>    fn</span><span> fly</span><span>(</span><span>&#x26;</span><span>self</span><span>);</span></span>
<span><span>}</span></span>
<span></span>
<span><span>#[derive(</span><span>Debug</span><span>)]</span></span>
<span><span>struct</span><span> Helicopter</span><span>;</span></span>
<span></span>
<span><span>impl</span><span> Fly</span><span> for</span><span> Helicopter</span><span> {</span></span>
<span><span>    fn</span><span> fly</span><span>(</span><span>&#x26;</span><span>self</span><span>) {</span></span>
<span><span>        println!</span><span>(</span><span>"</span><span>转动螺旋桨起飞</span><span>"</span><span>);</span></span>
<span><span>    }</span></span>
<span><span>}</span></span>
<span></span>
<span><span>#[derive(</span><span>Debug</span><span>)]</span></span>
<span><span>struct</span><span> Seagull</span><span>;</span></span>
<span></span>
<span><span>impl</span><span> Fly</span><span> for</span><span> Seagull</span><span> {</span></span>
<span><span>    fn</span><span> fly</span><span>(</span><span>&#x26;</span><span>self</span><span>) {</span></span>
<span><span>        println!</span><span>(</span><span>"</span><span>扇动翅膀起飞</span><span>"</span><span>);</span></span>
<span><span>    }</span></span>
<span><span>}</span></span>
<span></span>
<span><span>fn</span><span> main</span><span>() {</span></span>
<span><span>    let</span><span> h</span><span> =</span><span> Helicopter</span><span>;</span></span>
<span><span>    let</span><span> s</span><span> =</span><span> Seagull</span><span>;</span></span>
<span><span>    generic_func</span><span>(</span><span>h</span><span>);</span></span>
<span><span>    generic_func</span><span>(</span><span>s</span><span>);</span></span>
<span><span>}</span></span>
<span></span>
<span><span>fn</span><span> generic_func</span><span>&#x3C;</span><span>T</span><span>:</span><span> Fly</span><span> +</span><span> Debug</span><span>>(</span><span>flyable</span><span>:</span><span> T</span><span>) {</span></span>
<span><span>    println!</span><span>(</span><span>"</span><span>正在飞行的是：{:?}</span><span>"</span><span>, </span><span>flyable</span><span>);</span></span>
<span><span>    flyable</span><span>.</span><span>fly</span><span>();</span></span>
<span><span>}</span></span></code></pre><p>这里通过​<code class="">derive</code>​宏为两个结构体实现了​<code class="">Debug trait</code>​，实现了这个trait就可以打印出结构体自身的名称，同时要在泛型方法的类型限定上加上这一trait，​<code class="">T: trait1 + trait2</code>​这样的语法可以限定一个类型必须实现多个trait。打印结果为：
</p><pre tabindex="0"><code><span><span>正在飞行的是：Helicopter</span></span>
<span><span>转动螺旋桨起飞</span></span>
<span><span>正在飞行的是：Seagull</span></span>
<span><span>扇动翅膀起飞</span></span></code></pre><p>在编写代码时只需编写一个泛型函数，而Rust在编译后实际上会为每个不同类型创建单独的函数，这种方式称为​<strong>静态分发</strong>​，它的缺点是会使编译后的体积增大。另一种方法称为​<strong>动态分发</strong>​，它将类型判断放到运行时，空间占用小了，但是带来了更多的运行时开销：
</p><pre tabindex="0"><code><span><span>fn</span><span> main</span><span>() {</span></span>
<span><span>    let</span><span> h</span><span> =</span><span> Helicopter</span><span>;</span></span>
<span><span>    let</span><span> s</span><span> =</span><span> Seagull</span><span>;</span></span>
<span><span>    generic_func</span><span>(</span><span>&#x26;</span><span>h</span><span>);</span></span>
<span><span>    generic_func</span><span>(</span><span>&#x26;</span><span>s</span><span>);</span></span>
<span><span>}</span></span>
<span></span>
<span><span>fn</span><span> generic_func</span><span>(</span><span>flyable</span><span>:</span><span> &#x26;</span><span>dyn</span><span> Fly</span><span>) {</span></span>
<span><span>    flyable</span><span>.</span><span>fly</span><span>();</span></span>
<span><span>}</span></span></code></pre><p>代码改动不大，通过​<code class="">&#x26;</code>​借用或者​<code class="">Box</code>​智能指针包装类型，并且要加上​<code class="">dyn</code>​关键字，即可实现动态分发。
</p><blockquote><p>题外话：泛型多态不仅仅只针对trait bounds，可以查看<a href="https://doc.rust-lang.org/reference/items/generics.html">reference</a>等资料。
</p></blockquote><p>这就是属于Rust的一种静态类型的“鸭子类型”，​<code class="">generic_func</code>​需要的是能飞的对象，不在乎它是飞机还是海鸥，不在乎它们是否有共同的父类。
</p><h2 id="user-content-总结" class="">总结<a class="" tabindex="-1" href="#总结">#</a></h2><p>这篇文章的主要目的，是要说明如何以Rust的方式实现面向对象编程的，Rust并不是完全的独辟蹊径，列举Python的例子就是为了说明这一点。另外，面向对象不等于封装、继承、多态，继承和多态甚至不能算并列的概念。
</p><p>至于Rust中泛型与​<code class="">trait</code>​的详细用法，限于篇幅，再者相关资料如官方文档叙述很详细了，就不详细说明了，可以参考以下资料：
</p><ul><li><a href="https://doc.rust-lang.org/book/ch19-03-advanced-traits.html">Advanced Traits</a></li><li><a href="https://www.youtube.com/watch?v=grU-4u0Okto">一个Youtube视频</a></li></ul>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>Dive into Rust: Ownership, Borrowing, Lifetime</title>
          <link>https://elliot00.com/posts/dive-into-rust-ownership-borrowing-lifetime</link>
          <description>文章介绍了内存安全问题，以及Rust通过所有权、借用和生存期三个机制来保证内存安全的做法。文章还从Python程序员的视角，对Rust的内存安全机制进行了分析和理解。文章认为，Rust的内存安全机制是一种独特的机制，对于熟悉Python这类语言的程序员初次接触会感到比较陌生。</description>
          <pubDate>Sun, 30 May 2021 12:46:00 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <p>最近在Rust中文论坛看到一位Java程序员的<a href="https://rustcc.cn/article?id=0a6c3e41-0821-404c-91c8-3f9d4a038dbe">提问</a>，主要是有关Rust所有权规则的理解问题，想了想，对于常用Java、Python这类有垃圾回收机制语言的程序员，Rust独特的内存安全机制确实较难理解。这篇博客准备总结一下我对Rust内存安全问题的理解，提供一些从Python分析Rust的视角，抛砖引玉。
</p><h2 id="user-content-常见内存安全问题" class="">常见内存安全问题<a class="" tabindex="-1" href="#常见内存安全问题">#</a></h2><p>先列举一下一些常见的内存安全问题：
</p><ul><li>缓冲区溢出
</li><li>解引用空指针
</li><li>访问未初始化内存
</li><li>错误释放
</li><li>释放后使用
</li><li>重复释放
</li><li>……
</li></ul><p>对于这些内存安全问题，一般常见的编程语言有以下解决方案：
</p><ol><li>程序员自己负责：C/C++系，通常是系统级编程语言的做法，程序员自己注意这些问题，优点是没有运行时负担，这也是为什么一般都是需要高性能的系统级语言采用这种做法，缺点很明显，高度依赖程序员的细心程度，一不小心就出问题，并且很有可能项目上线很久才会发现
</li><li>垃圾回收：典型代表Java，由语言提供运行时管理内存，例如在一个值不会再被访问时释放该空间，优点是不需要程序员手动管理，心智负担小，缺点是牺牲了性能（其实还是会有内存泄漏等问题
</li><li>Rust：Rust表示鱼和熊掌要兼得，提倡零成本抽象，提供一套特定的*规则*，只有遵守规则的程序才能通过编译，在编译期解决内存安全问题，性能没有牺牲，程序员脑子也没有牺牲（大概
</li></ol><p>下面就聊一聊Rust用来保障内存安全的机制：​<strong>所有权（Ownership）、借用（Borrowing）、生存期（Lifetime）</strong></p><h2 id="user-content-所有权" class="">所有权<a class="" tabindex="-1" href="#所有权">#</a></h2><p>首先看一段Python代码：
</p><pre tabindex="0"><code><span><span>import</span><span> sys</span></span>
<span></span>
<span><span>class</span><span> Foo</span><span>:</span></span>
<span><span>    def</span><span> __init__</span><span>(</span><span>self</span><span>,</span><span> value</span><span>:</span><span> int</span><span>)</span><span> -></span><span> None</span><span>:</span></span>
<span><span>        self</span><span>.</span><span>value </span><span>=</span><span> value</span></span>
<span></span>
<span></span>
<span><span>a </span><span>=</span><span> Foo</span><span>(</span><span>3</span><span>)</span></span>
<span><span>print</span><span>(</span><span>sys.</span><span>getrefcount</span><span>(</span><span>a</span><span>))</span></span>
<span><span>b </span><span>=</span><span> a</span></span>
<span><span>print</span><span>(</span><span>sys.</span><span>getrefcount</span><span>(</span><span>a</span><span>))</span></span></code></pre><p>输出结果是2和3，Python的垃圾回收机制主要是基于​<strong>引用计数</strong>​，简单理解就是把变量看成对实际内存中值的一个引用，那么当这个引用数量为0时，这个值也就不需要了，可以从内存中清理掉。上面我们创建了​<code class="">Foo(3)</code>​这个值，赋值给a变量，这里结果是2，官方文档解释是因为在调用​<code class="">sys.getrefcount</code>​时参数作为临时变量引用，使引用计数被加了一次，后面又赋值a给了b，引用计数又加了1。
</p><p>上面是Python中的规则，当然解释器实际的垃圾回收是很复杂的，还有一些其它辅助手段，但总之这个过程是没有我们人为干预的。
</p><p>下面再看一段Rust代码：
</p><pre tabindex="0"><code><span><span>#[derive(</span><span>Debug</span><span>)]</span></span>
<span><span>struct</span><span> Student</span><span> {</span></span>
<span><span>    name</span><span>:</span><span> String</span></span>
<span><span>}</span></span>
<span></span>
<span><span>fn</span><span> print_student</span><span>(</span><span>student</span><span>:</span><span> Student</span><span>) {</span></span>
<span><span>    println!</span><span>(</span><span>"</span><span>{:?}</span><span>"</span><span>, </span><span>student</span><span>);</span></span>
<span><span>}</span></span>
<span></span>
<span><span>fn</span><span> main</span><span>() {</span></span>
<span><span>    let</span><span> ming</span><span> =</span><span> Student</span><span> { </span><span>name</span><span>:</span><span> String</span><span>::</span><span>from</span><span>(</span><span>"</span><span>ming</span><span>"</span><span>) };</span></span>
<span><span>    print_student</span><span>(</span><span>ming</span><span>);</span></span>
<span><span>    let</span><span> hong</span><span> =</span><span> &#x26;</span><span>ming</span><span>;  </span><span>// 试着改成let hong = ming;再看报错</span></span>
<span><span>    println!</span><span>(</span><span>"</span><span>raw: {:?}, borrow: {:?}</span><span>"</span><span>, </span><span>ming</span><span>, </span><span>hong</span><span>);</span></span>
<span><span>}</span></span></code></pre><p>我特意写了个错误明显的程序，可以分别剖析，首先直接运行这段程序，看下编译器报错：
</p><pre tabindex="0"><code><span><span>error[E0382]: borrow of moved value: `ming`</span></span>
<span><span>  --> src/main.rs:13:16</span></span>
<span><span>   |</span></span>
<span><span>11 |     let ming = Student { name: String::from("ming") };</span></span>
<span><span>   |         ---- move occurs because `ming` has type `Student`, which does not implement the `Copy` trait</span></span>
<span><span>12 |     print_student(ming);</span></span>
<span><span>   |                   ---- value moved here</span></span>
<span><span>13 |     let hong = &#x26;ming;  // 试着改成let hong = ming;再看报错</span></span>
<span><span>   |                ^^^^^ value borrowed here after move</span></span></code></pre><p>Rust的编译器错误提示非常清晰，这里告诉我们，​<code class="">ming</code>​拥有类型​<code class="">Student</code>​的一个实例，在调用​<code class="">print_student(ming);</code>​时发生了“move”，并且还给了个提示说是因为我没有为其实现​<code class="">Copy trait</code>​，接着又告诉我们，在第13行尝试在发生“move”后借用这个值。对于不熟悉Rust的同学会觉得不知所有，这里先提一下Rust的​<strong>所有权</strong>​规则（来自《Rust程序设计语言》）：
</p><ol><li>Rust 中的每一个值都有一个被称为其​<strong>所有者</strong>​（​<em>owner</em>​）的变量。
</li><li>值在任一时刻有且只有一个所有者。
</li><li>当所有者（变量）离开作用域，这个值将被丢弃。
</li></ol><p>根据规则一，我们知道在定义变量ming的时候，ming就是实际的Student值的所有者，享有所有权，根据规则二，我们知道，在任何时候，Student实际值，都只能有一个主人，再回顾一下编译器提示：move occurs because <code class="">ming</code> has type <code class="">Student</code>​, which does not implement the <code class="">Copy</code> trait。
</p><p>对于Python程序：
</p><pre tabindex="0"><code><span><span>a </span><span>=</span><span> [</span><span>1</span><span>,</span><span> 2</span><span>,</span><span> 3</span><span>,</span><span> 4</span><span>]</span></span>
<span><span>b </span><span>=</span><span> a</span></span>
<span><span>print</span><span>(</span><span>id</span><span>(</span><span>a</span><span>)</span><span>,</span><span> id</span><span>(</span><span>b</span><span>))</span></span></code></pre><p>Python中可以认为任何变量实质上都类似于C中的指针，只是指向实际值的一个标记，所以这里打印a和b指向的内存中的位置是相同的，a就是b。
</p><p>而在Rust中：
</p><pre tabindex="0"><code><span><span>fn</span><span> main</span><span>() {</span></span>
<span><span>    let</span><span> ming</span><span> =</span><span> Student</span><span> { </span><span>name</span><span>:</span><span> String</span><span>::</span><span>from</span><span>(</span><span>"</span><span>ming</span><span>"</span><span>) };</span></span>
<span><span>    let</span><span> ming2</span><span> =</span><span> ming</span><span>; </span><span>// 此时ming没用了</span></span>
<span><span>    // println!("{}", ming); 这行会报错</span></span>
<span><span>    println!</span><span>(</span><span>"</span><span>{:?}</span><span>"</span><span>, </span><span>ming2</span><span>);</span></span>
<span><span>}</span></span></code></pre><p><code class="">let ming2 = ming;</code>​，根据规则二，一个值同一时间只能有一个所有者，所以这里ming2成了所有者，所有权“move”了，ming不再存在，编译器提到了Copy，只有实现了Copy的数据类型，才会在这种重新赋值的场景下，不“move”所有权，而是复制一份新的值。
</p><pre tabindex="0"><code><span><span>fn</span><span> main</span><span>() {</span></span>
<span><span>    let</span><span> a</span><span> =</span><span> 1</span><span>;</span></span>
<span><span>    let</span><span> b</span><span> =</span><span> a</span><span>;</span></span>
<span><span>    println!</span><span>(</span><span>"</span><span>a: {}, b: {}</span><span>"</span><span>, </span><span>a</span><span>, </span><span>b</span><span>);</span></span>
<span><span>}</span></span></code></pre><p>上面这段代码就不报错，因为Rust中一些基础类型是实现了Copy的，所以​<code class="">let b = a;</code>​的时候，b拥有所有权的是一个被复制出来的1，不违反规则二。
</p><p>调用​<code class="">print_student</code>​函数，就是把ming的所有权“move”给了这个函数的参数这个变量，再看看规则三，首先什么是作用域呢？作用域就是一个值在程序中有效的范围：
</p><pre tabindex="0"><code><span><span>{                      </span><span>// s 在这里无效, 它尚未声明</span></span>
<span><span>    let</span><span> s</span><span> =</span><span> "</span><span>hello</span><span>"</span><span>;   </span><span>// 从此处起，s 是有效的</span></span>
<span></span>
<span><span>    // 使用 s</span></span>
<span><span>}                      </span><span>// 此作用域已结束，s 不再有效</span></span></code></pre><p>可以简单理解成Rust中任何值遇到右花括号​<code class="">}</code>​就被销毁死掉，Rust强制执行这一规则，那么在​<code class="">print_student</code>​函数结束的时候，实际的student值在内存中不复存在了，可是我们却要在这之后再次使用ming，于是编译器报错，但是注意，编译器给出的实际错误并不直接说student已经不复存在，而是说我borrowed after move，什么是borrow下一步说。
</p><p>要解决这个问题，可以在​<code class="">print_student</code>​结束的时候用返回值，将所有权再还回来：
</p><pre tabindex="0"><code><span><span>fn</span><span> print_student</span><span>(</span><span>student</span><span>:</span><span> Student</span><span>) </span><span>-></span><span> Student</span><span> {</span></span>
<span><span>    println!</span><span>(</span><span>"</span><span>{:?}</span><span>"</span><span>, </span><span>student</span><span>);</span></span>
<span><span>    student</span></span>
<span><span>}</span></span>
<span></span>
<span><span>fn</span><span> main</span><span>() {</span></span>
<span><span>    let</span><span> ming</span><span> =</span><span> Student</span><span> { </span><span>name</span><span>:</span><span> String</span><span>::</span><span>from</span><span>(</span><span>"</span><span>ming</span><span>"</span><span>) };</span></span>
<span><span>    let</span><span> ming</span><span> =</span><span> print_student</span><span>(</span><span>ming</span><span>);</span></span>
<span><span>    let</span><span> hong</span><span> =</span><span> &#x26;</span><span>ming</span><span>; </span><span>// 这个时候ming还存在</span></span>
<span><span>    println!</span><span>(</span><span>"</span><span>raw: {:?}, borrow: {:?}</span><span>"</span><span>, </span><span>ming</span><span>, </span><span>hong</span><span>);</span></span>
<span><span>}</span></span></code></pre><p>这样就不会报错了，但是难道Rust中每个函数都必须将参数当做返回值返回吗？
</p><p>还有一个办法：
</p><pre tabindex="0"><code><span><span>#[derive(</span><span>Debug</span><span>, </span><span>Clone</span><span>)]</span></span>
<span><span>struct</span><span> Student</span><span> {</span></span>
<span><span>    name</span><span>:</span><span> String</span></span>
<span><span>}</span></span>
<span></span>
<span><span>fn</span><span> print_student</span><span>(</span><span>student</span><span>:</span><span> Student</span><span>) </span><span>-></span><span> Student</span><span> {</span></span>
<span><span>    println!</span><span>(</span><span>"</span><span>{:?}</span><span>"</span><span>, </span><span>student</span><span>);</span></span>
<span><span>    student</span></span>
<span><span>}</span></span>
<span></span>
<span><span>fn</span><span> main</span><span>() {</span></span>
<span><span>    let</span><span> ming</span><span> =</span><span> Student</span><span> { </span><span>name</span><span>:</span><span> String</span><span>::</span><span>from</span><span>(</span><span>"</span><span>ming</span><span>"</span><span>) };</span></span>
<span><span>    print_student</span><span>(</span><span>ming</span><span>.</span><span>clone</span><span>()); </span><span>// 这里传入的是复制体</span></span>
<span><span>    let</span><span> hong</span><span> =</span><span> &#x26;</span><span>ming</span><span>;</span></span>
<span><span>    println!</span><span>(</span><span>"</span><span>raw: {:?}, borrow: {:?}</span><span>"</span><span>, </span><span>ming</span><span>, </span><span>hong</span><span>);</span></span>
<span><span>}</span></span></code></pre><p>可以为Student实现Clone这个trait（关于Copy，有一套特殊规则，详见<a href="https://doc.rust-lang.org/std/marker/trait.Copy.html">文档</a>），在传入参数时显式调用clone方法，把复制体传进去。
</p><p>但这样还要在每次调用函数时复制一份原有的值，还有其它方法吗？
</p><h2 id="user-content-借用" class="">借用<a class="" tabindex="-1" href="#借用">#</a></h2><p>一般大学计算机相关专业会以C语言作为学生的入门语言，我们在学习C语言的时候，一般都会被告知使用​<code class="">malloc</code>​分配的内存，一定要记得​<code class="">free</code>​掉，程序运行的时候，不同的数据会使用不同的内存，如何在数据不再需要时，释放内存一直是一个麻烦的问题，忘了释放会浪费内存；释放过一次但程序员忘记了，再次释放，这是个未定义行为；数据后面还需要用却提前释放内存了，那更是可怕。前面我们提到Rust的所有权规则第三条，离开作用域就是释放内存，这听上去很简单，但从之前的代码中，这似乎会在一些场景下为我们带来麻烦。
</p><p>前面我们已经提到了Rust中多个变量与数据之间的两种关系，分别是移动（move）与克隆（clone）。
</p><pre tabindex="0"><code><span><span>let</span><span> a</span><span> =</span><span> String</span><span>::</span><span>from</span><span>(</span><span>"</span><span>hello</span><span>"</span><span>);</span></span>
<span><span>let</span><span> b</span><span> =</span><span> a</span><span>; </span><span>// move</span></span>
<span></span>
<span><span>let</span><span> a</span><span> =</span><span> String</span><span>::</span><span>from</span><span>(</span><span>"</span><span>hello</span><span>"</span><span>);</span></span>
<span><span>let</span><span> b</span><span> =</span><span> a</span><span>.</span><span>clone</span><span>(); </span><span>// clone</span></span></code></pre><p>我们已经发现在使用函数时，所有权规则给我们带来了一些麻烦，于是Rust给我们提供了另一个机制。
</p><pre tabindex="0"><code><span><span>let</span><span> hong</span><span> =</span><span> &#x26;</span><span>ming</span><span>; </span><span>// 这个时候ming还存在</span></span></code></pre><p>还记得这一行代码以及那个错误提示“value borrowed here after move”吗？​<code class="">&#x26;变量</code>​表示对变量的引用（reference），​<code class="">&#x26;ming</code>​创建了一个指向ming的引用，将它赋值给hong，hong并不会获得实际值的所有权，因此不违反所有权规则二，引用有些像C中的指针，但并不相同。
</p><p>在讲引用前，要讲一下Rust变量的不可变性，在Python中，实际上是有不可变对象与可变对象之分的，但是没有不可以修改的变量这个概念（要想实现一个运行时不能被修改的类变量需要一些骚操作）：
</p><pre tabindex="0"><code><span><span>//</span><span> 可以随意更改a的值和类型</span></span>
<span><span>a </span><span>=</span><span> 1</span></span>
<span><span>a </span><span>=</span><span> 2</span></span>
<span><span>a </span><span>=</span><span> "</span><span>hello world</span><span>"</span></span></code></pre><p>因为变量只是一个“指针”，即使它指向的是不可变对象，也可以随意改变它所指向的数据。
</p><pre tabindex="0"><code><span><span>let</span><span> a</span><span> =</span><span> 1</span><span>;</span></span>
<span><span>a</span><span> =</span><span> 3</span><span>; </span><span>// 错误</span></span>
<span></span>
<span><span>let</span><span> a</span><span> =</span><span> 3</span><span>; </span><span>// 但是这种覆盖式的初始化是允许的</span></span></code></pre><p>默认情况下，Rust定义一个变量之后就不允许对其进行修改，不仅是类型不可变，值也不能改。
</p><pre tabindex="0"><code><span><span>let</span><span> mut</span><span> a</span><span> =</span><span> 1</span><span>;</span></span>
<span><span>a</span><span> =</span><span> 2</span><span>;</span></span></code></pre><p>必须使用mut（mutable）关键字，显式标识这个值可以被修改。那么这和引用有什么关系呢？
</p><p>以后端常说的增删改查来说，如果一个数据，需要被读取10次，这没太大问题，因为读这个操作没有副作用，不会改用数据本身，我们常说HTTP的GET方法具有幂等性也是这个原因，可以如果是修改呢？删除呢？
</p><pre tabindex="0"><code><span><span>let</span><span> a</span><span> =</span><span> 1</span><span>;</span></span>
<span><span>let</span><span> borrow_a</span><span> =</span><span> &#x26;</span><span>a</span><span>; </span><span>// 不可变借用</span></span>
<span><span>let</span><span> borrow_a1</span><span> =</span><span> &#x26;</span><span>a</span><span>; </span><span>// 没问题</span></span>
<span></span>
<span><span>let</span><span> mut</span><span> b</span><span> =</span><span> 1</span><span>;</span></span>
<span><span>let</span><span> mut_borrow_b</span><span> =</span><span> &#x26;</span><span>mut</span><span> b</span><span>; </span><span>// 可变借用</span></span>
<span><span>let</span><span> mut_borrow_b1</span><span> =</span><span> &#x26;</span><span>mut</span><span> b</span><span>; </span><span>// 错误</span></span>
<span></span>
<span><span>let</span><span> c</span><span> =</span><span> 1</span><span>;</span></span>
<span><span>let</span><span> mut_borrow_c</span><span> =</span><span> &#x26;</span><span>mut</span><span> c</span><span>; </span><span>// 不行</span></span></code></pre><p>对于不可变借用，可以有很多个，但​<strong>同一时间可变变量的可变引用只能有一个</strong>​，不可变数据没这个限制，因为它就不能有可变引用。
</p><p>这一条规则避免了​<strong>数据竞争</strong>​：
</p><ul><li>两个或更多指针同时访问同一数据。
</li><li>至少有一个指针被用来写入数据。
</li><li>没有同步数据访问的机制。
</li></ul><p>Rust直接避免了以上情况，有以上行为的程序无法编译通过。
</p><p>Rust引用的另一条规则是，​<strong>引用必须总是有效的</strong>​，这个怎么理解呢？再看我们之前出错的程序，​<code class="">let hong = &#x26;ming;</code>​，hong是一个引用，在这之前ming已经在​<code class="">print_student</code>​函数中被销毁了，这在内存安全问题中一般被称为悬垂指针或悬垂引用，C语言中，一个指针指向的内存可能被你手动释放或者分配给其它数据了，但是这个指针没有被更改，程序可以编译通过，这会造成难以排查的bug。Rust在编译前避免这一点。
</p><p>现在我们可以理解为什么编译器报错“borrowed after move”了。
</p><p>那什么是借用呢？就是在函数使用引用做参数的时候称之为借用，我一般统统叫借用，有借有还，a借给b，所有权最终还在a那里，之前的函数可以这么改：
</p><pre tabindex="0"><code><span><span>fn</span><span> print_student</span><span>(</span><span>student</span><span>:</span><span> &#x26;</span><span>Student</span><span>) {</span></span>
<span><span>    println!</span><span>(</span><span>"</span><span>{:?}</span><span>"</span><span>, </span><span>student</span><span>);</span></span>
<span><span>}</span></span>
<span></span>
<span><span>fn</span><span> main</span><span>() {</span></span>
<span><span>    let</span><span> ming</span><span> =</span><span> Student</span><span> { </span><span>name</span><span>:</span><span> String</span><span>::</span><span>from</span><span>(</span><span>"</span><span>ming</span><span>"</span><span>) };</span></span>
<span><span>    print_student</span><span>(</span><span>&#x26;</span><span>ming</span><span>);</span></span>
<span><span>    let</span><span> hong</span><span> =</span><span> &#x26;</span><span>ming</span><span>;</span></span>
<span><span>    println!</span><span>(</span><span>"</span><span>raw: {:?}, borrow: {:?}</span><span>"</span><span>, </span><span>ming</span><span>, </span><span>hong</span><span>);</span></span>
<span><span>}</span></span></code></pre><p>参数使用​<code class="">&#x26;Student</code>​而不直接使用Student。
</p><h2 id="user-content-生存期" class="">生存期<a class="" tabindex="-1" href="#生存期">#</a></h2><p>很多地方都喜欢把Rust中的​<strong>Lifetime</strong>​翻译成生命周期，但是我觉得这个Lifetime和life cycle并不是一个意思，而life cycle一般才被翻译成生命周期，lifetime还是叫生存期比较好。
</p><p>什么是生存期呢？简单讲生存期基本上就是作用域，不过生存期一般是用来描述一个​<strong>引用</strong>​的作用范围，说白了就是一个引用可以合法的活多久。
</p><p>那么结合上文引用的第二条规则，就能用生存期来描述了：引用的生存期，绝对不能大于其引用的值的作用域，直白说就是&#x26;a不能比a活得长。
</p><pre tabindex="0"><code><span><span>{</span></span>
<span><span>    let</span><span> r</span><span>;                </span><span>// ---------+-- 'a</span></span>
<span><span>                          //          |</span></span>
<span><span>    {                     </span><span>//          |</span></span>
<span><span>        let</span><span> x</span><span> =</span><span> 5</span><span>;        </span><span>// -+-- 'b  |</span></span>
<span><span>        r</span><span> =</span><span> &#x26;</span><span>x</span><span>;           </span><span>//  |       |</span></span>
<span><span>    }                     </span><span>// -+       |</span></span>
<span><span>                          //          |</span></span>
<span><span>    println!</span><span>(</span><span>"</span><span>r: {}</span><span>"</span><span>, </span><span>r</span><span>); </span><span>//          |</span></span>
<span><span>}  </span></span></code></pre><p>以上程序来自《Rust程序设计语言》，根据​<strong>所有权规则第三条</strong>​，r从声明那一刻可以一直“活”到最后一个右花括号，而它引用的变量x，只能活到倒数第二个右花括号，这样的程序自然是无法通过编译的。
</p><h3 id="user-content-生存期注解">生存期注解<a class="" tabindex="-1" href="#生存期注解">#</a></h3><p>考虑这样一个函数：
</p><pre tabindex="0"><code><span><span>fn</span><span> main</span><span>() {</span></span>
<span><span>    let</span><span> a</span><span> =</span><span> 10</span><span>;</span></span>
<span><span>    let</span><span> b</span><span> =</span><span> 18</span><span>;</span></span>
<span><span>    println!</span><span>(</span><span>"</span><span>max number is {}</span><span>"</span><span>, </span><span>max</span><span>(</span><span>&#x26;</span><span>a</span><span>, </span><span>&#x26;</span><span>b</span><span>));</span></span>
<span><span>}</span></span>
<span></span>
<span><span>fn</span><span> max</span><span>(</span><span>left</span><span>:</span><span> &#x26;</span><span>i32</span><span>, </span><span>right</span><span>:</span><span> &#x26;</span><span>i32</span><span>) </span><span>-></span><span> &#x26;</span><span>i32</span><span> {</span></span>
<span><span>    if</span><span> left</span><span> ></span><span> right</span><span> {</span></span>
<span><span>        left</span></span>
<span><span>    } </span><span>else</span><span> {</span></span>
<span><span>        right</span></span>
<span><span>    }</span></span>
<span><span>}</span></span></code></pre><p>编译器会告诉我们：
</p><pre tabindex="0"><code><span><span>7 | fn max(left: &#x26;i32, right: &#x26;i32) -> &#x26;i32 {</span></span>
<span><span>  |              ----         ----     ^ expected named lifetime parameter</span></span>
<span><span>  |</span></span>
<span><span>  = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `left` or `right`</span></span>
<span><span>help: consider introducing a named lifetime parameter</span></span>
<span><span>  |</span></span>
<span><span>7 | fn max&#x3C;'a>(left: &#x26;'a i32, right: &#x26;'a i32) -> &#x26;'a i32 {</span></span>
<span><span>  |       ^^^^       ^^^^^^^         ^^^^^^^     ^^^</span></span></code></pre><p>编译器直接告诉我们要怎样修改程序：
</p><pre tabindex="0"><code><span><span>fn</span><span> max</span><span>&#x3C;'</span><span>a</span><span>>(</span><span>left</span><span>:</span><span> &#x26;</span><span>'</span><span>a</span><span> i32</span><span>, </span><span>right</span><span>:</span><span> &#x26;</span><span>'</span><span>a</span><span> i32</span><span>) </span><span>-></span><span> &#x26;</span><span>'</span><span>a</span><span> i32</span><span> {</span></span>
<span><span>    if</span><span> left</span><span> ></span><span> right</span><span> {</span></span>
<span><span>        left</span></span>
<span><span>    } </span><span>else</span><span> {</span></span>
<span><span>        right</span></span>
<span><span>    }</span></span>
<span><span>}</span></span></code></pre><p><code class="">&#x26;'a i32</code>​这样的形式被称为生存期注解，这种情况下，我们告诉编译器，​<strong>函数返回值至少会和两个参数里寿命最短的那个活一样长</strong>​。这样编译器就能发现以下这种错误：
</p><pre tabindex="0"><code><span><span>fn</span><span> main</span><span>() {</span></span>
<span><span>    let</span><span> a</span><span> =</span><span> 10</span><span>;</span></span>
<span><span>    let</span><span> max_number</span><span>;</span></span>
<span><span>    {</span></span>
<span><span>        let</span><span> b</span><span> =</span><span> 18</span><span>;</span></span>
<span><span>        max_number</span><span> =</span><span> max</span><span>(</span><span>&#x26;</span><span>a</span><span>, </span><span>&#x26;</span><span>b</span><span>);</span></span>
<span><span>    }</span></span>
<span><span>    println!</span><span>(</span><span>"</span><span>max number is {}</span><span>"</span><span>, </span><span>max_number</span><span>);</span></span>
<span><span>}</span></span></code></pre><p>编译器会报错：
</p><pre tabindex="0"><code><span><span>6 |         max_number = max(&#x26;a, &#x26;b);</span></span>
<span><span>  |                              ^^ borrowed value does not live long enough</span></span>
<span><span>7 |     }</span></span>
<span><span>  |     - `b` dropped here while still borrowed</span></span>
<span><span>8 |     println!("max number is {}", max_number);</span></span>
<span><span>  |                                  ---------- borrow later used here</span></span></code></pre><p>编译器告诉我们这个借用活得不够长，如果这段程序被允许编译通过，那么就会造成​<strong>悬垂引用</strong>​问题了，如果a确实大于b那就还好，反过来，则会出现b被销毁，而b的借用还活着的情况。
</p><p>在未来的Rust程序中，手动标记的生存期将会越来越少，编译器会自动帮我们推断出大部分情况下借用的生存期。
</p><h2 id="user-content-总结" class="">总结<a class="" tabindex="-1" href="#总结">#</a></h2><p>下面是文章开头提到的那位Java程序员提问的代码：
</p><pre tabindex="0"><code><span><span>struct</span><span> InnerA</span><span> {</span></span>
<span><span>}</span></span>
<span><span>struct</span><span> InnerB</span><span>&#x3C;'</span><span>a</span><span>> {</span></span>
<span><span>    inner_a</span><span>:</span><span> &#x26;</span><span>'</span><span>a</span><span> InnerA</span><span>,</span></span>
<span><span>}</span></span>
<span><span>struct</span><span> Outer</span><span> {</span></span>
<span><span>    inner_a</span><span>:</span><span> InnerA</span><span>,</span></span>
<span><span>    inner_b</span><span>:</span><span> InnerB</span><span>,</span></span>
<span><span>}</span></span>
<span><span>fn</span><span> main</span><span>() {</span></span>
<span></span>
<span><span>    let</span><span> inner_a</span><span> =</span><span> InnerA</span><span> {};</span></span>
<span><span>    let</span><span> inner_b</span><span> =</span><span> InnerB</span><span> {</span></span>
<span><span>        inner_a</span><span>:</span><span> &#x26;</span><span>inner_a</span><span>,</span></span>
<span><span>    };</span></span>
<span></span>
<span><span>    let</span><span> outer</span><span> =</span><span> Outer</span><span> {</span></span>
<span><span>        inner_a</span><span>,</span></span>
<span><span>        inner_b</span><span>,</span></span>
<span><span>    };</span></span>
<span><span>    println!</span><span>(</span><span>"</span><span>OK</span><span>"</span><span>);</span></span>
<span><span>}</span></span></code></pre><p>编译器提示需要指定生存期，接着他改成了这样：
</p><pre tabindex="0"><code><span><span>struct</span><span> InnerA</span><span> {</span></span>
<span><span>}</span></span>
<span><span>struct</span><span> InnerB</span><span>&#x3C;'</span><span>a</span><span>> {</span></span>
<span><span>    inner_a</span><span>:</span><span> &#x26;</span><span>'</span><span>a</span><span> InnerA</span><span>,</span></span>
<span><span>}</span></span>
<span><span>struct</span><span> Outer</span><span>&#x3C;'</span><span>b</span><span>> {</span></span>
<span><span>    inner_a</span><span>:</span><span> InnerA</span><span>,</span></span>
<span><span>    inner_b</span><span>:</span><span> InnerB</span><span>&#x3C;'</span><span>b</span><span>>,</span></span>
<span><span>}</span></span>
<span><span>fn</span><span> main</span><span>() {</span></span>
<span></span>
<span><span>    let</span><span> inner_a</span><span> =</span><span> InnerA</span><span> {};</span></span>
<span><span>    let</span><span> inner_b</span><span> =</span><span> InnerB</span><span> {</span></span>
<span><span>        inner_a</span><span>:</span><span> &#x26;</span><span>inner_a</span><span>,</span></span>
<span><span>    };</span></span>
<span></span>
<span><span>    let</span><span> outer</span><span> =</span><span> Outer</span><span> {</span></span>
<span><span>        inner_a</span><span>,</span></span>
<span><span>        inner_b</span><span>,</span></span>
<span><span>    };</span></span>
<span><span>    println!</span><span>(</span><span>"</span><span>OK</span><span>"</span><span>);</span></span>
<span><span>}</span></span></code></pre><p>想必看到这里，读者已经能看出程序的错误之处以及如何修复了，当然具体的修改方式还得视需求而定。
</p><p>Rust的内存安全机制还是比较独特的，尤其是熟悉Python这类语言的程序员初次接触会感到比较陌生。
</p><p>在此总结一下：
</p><ol><li>数据同一时间有且只能有一个所有者，Python程序员尤其注意
</li><li>RAII，数据离开作用域立即被释放内存，Python程序员可以理解成语言层面的一个上下文管理器，离开with语句就调用​<code class="">__exit__</code>​方法
</li><li>数据默认不可变，同一时间只能有一个可变引用，可以认为没有实际上的可变数据，只有共享数据与独占数据之分，一个数据能被多个人访问的时候不能变，只有有唯一可变引用或者没有引用的时候，这时候是​<strong>“独占”</strong>​的，所以可以安全地修改。
</li></ol>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>TypeScript实现互斥参数</title>
          <link>https://elliot00.com/posts/typescript-mutex-param</link>
          <description>作者想通过 typeScript 的类型定义来限制一个函数的两个参数只能取其一。但是按照 TypeScript 的类型系统，直接将参数定义为两个 interface 的联合类型是没有用的，因为联合类型允许同时存在两个类型的值。而 never 类型可以表示一个只会抛出异常或者内部死循环的函数的返回值，并且任何其他类型的值都不能赋值给这个类型的变量，作者利用这个性质来限制了参数的取值范围，从而保证用户只会使用两个互斥属性中的一个。</description>
          <pubDate>Wed, 28 Apr 2021 13:59:47 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <p>之前写过一个markdown相关的组件，将后端解析markdown后生成的字符串通过​<code class="">dangerouslySetInnerHTML</code>​插入DOM，并且设置了不少样式，通过模块化的CSS引入。
</p><pre tabindex="0"><code><span><span>import</span><span> style </span><span>from</span><span> "</span><span>./MarkdownBody.module.css</span><span>"</span></span>
<span></span>
<span><span>const</span><span> MarkdownBody</span><span>:</span><span> react</span><span>.</span><span>FC</span><span>&#x3C;</span><span>Props</span><span>></span><span> =</span><span> (</span><span>{</span><span> content</span><span> }</span><span>)</span><span> =></span><span> {</span></span>
<span></span>
<span><span>    return</span><span> (</span></span>
<span><span>        &#x3C;</span><span>div</span><span> className</span><span>=</span><span>{</span><span>style</span><span>.</span><span>markdown</span><span>}</span><span> dangerouslySetInnerHTML</span><span>=</span><span>{</span><span>{ __html</span><span>:</span><span> content</span><span> }</span><span>}</span><span>>&#x3C;/</span><span>div</span><span>></span></span>
<span><span>    )</span></span>
<span><span>}</span></span></code></pre><p>之前它一直正常工作，但是最近我有另一个页面，需要和markdown页面保持相同的样式，当然，最直观的办法就是直接把样式文件提出来，不过在这里我想到一个有趣的问题：一个组件的props，或者说一个函数的参数，也没有可能类型安全地定义为互斥的？
</p><pre tabindex="0"><code><span><span>&#x3C;</span><span>MarkdownBody</span><span> content</span><span>=</span><span>{</span><span>...</span><span>}</span><span> /></span></span>
<span></span>
<span><span>&#x3C;</span><span>MarkdownBody</span><span>></span></span>
<span><span>    &#x3C;</span><span>div</span><span>></span><span>...</span><span>&#x3C;/</span><span>div</span><span>></span></span>
<span><span>&#x3C;/</span><span>MarkdownBody</span><span>></span></span></code></pre><p>同一个组件，没有children，只传递一个名为content的props，与不传递content，而是子组件的情况，采用不同的渲染方式。
</p><p>或者举个函数的例子：
</p><pre tabindex="0"><code><span><span>foo</span><span>(p1); </span><span>// 允许</span></span>
<span><span>foo</span><span>(p2); </span><span>// 允许</span></span>
<span><span>foo</span><span>(p1</span><span>,</span><span> p2); </span><span>// 错误</span></span></code></pre><p>当然，即使不用TypeScript，JS本身是允许调用函数时与定义时不匹配的：
</p><pre tabindex="0"><code><span><span>function</span><span> foo</span><span>(</span><span>a</span><span>,</span><span> b</span><span>)</span><span> {</span></span>
<span><span>    console</span><span>.</span><span>log</span><span>(</span><span>a</span><span>,</span><span> b</span><span>);</span></span>
<span><span>}</span></span>
<span></span>
<span><span>foo</span><span>(); </span><span>// undefined undefined</span></span>
<span><span>foo</span><span>(</span><span>1</span><span>); </span><span>// 1 undefined</span></span></code></pre><p>可以通过判断参数是否是​<code class="">undefined</code>​得知是否传入了某个参数，在​<code class="">TypeScript</code>​里可以把两个参数都定义成可选。但是如果这段代码出现在第三方库中，那只能期望用户看了文档并且遵守约定，有没有类型安全的方式？
</p><h2 id="user-content-联合类型" class="">联合类型<a class="" tabindex="-1" href="#联合类型">#</a></h2><p><code class="">TypeScript</code>​中可以定义联合类型​<em>Union Types</em>​，如：
</p><pre tabindex="0"><code><span><span>let</span><span> foo</span><span>:</span><span> string</span><span> |</span><span> number</span><span>;</span></span></code></pre><p>但是如果直接将参数定义为两个interface的联合类型是没用的：
</p><pre tabindex="0"><code><span><span>interface</span><span> P1</span><span> {</span></span>
<span><span>    a</span><span>:</span><span> string</span></span>
<span><span>}</span></span>
<span></span>
<span><span>interface</span><span> P2</span><span> {</span></span>
<span><span>    b</span><span>:</span><span> number</span></span>
<span><span>}</span></span>
<span></span>
<span><span>function</span><span> foo</span><span>(</span><span>p</span><span>:</span><span> P1</span><span> |</span><span> P2</span><span>)</span><span> {</span></span>
<span><span>    console</span><span>.</span><span>log</span><span>(p)</span></span>
<span><span>}</span></span>
<span></span>
<span><span>// 都可以通过编译</span></span>
<span><span>foo</span><span>({a</span><span>:</span><span> "</span><span>hello</span><span>"</span><span>})</span></span>
<span><span>foo</span><span>({b</span><span>:</span><span> 123</span><span>})</span></span>
<span><span>foo</span><span>({a</span><span>:</span><span> "</span><span>hello</span><span>"</span><span>,</span><span> b</span><span>:</span><span> 12</span><span>})</span></span></code></pre><h2 id="user-content-never" class="">never<a class="" tabindex="-1" href="#never">#</a></h2><p>一些语言的类型系统里，会有一个底部类型，它是所有类型的子类型，在​<code class="">TypeScript</code>​里，就有这样一个类型，​<code class="">never</code>​，它可以用来表示一个只会抛出异常或者内部死循环的函数的返回值（或者说没有返回值）。它有一个特性，即任何其他类型的值都不能赋值给这个类型的变量。
</p><pre tabindex="0"><code><span><span>let</span><span> foo</span><span>:</span><span> never</span><span> =</span><span> 123</span><span>; </span><span>// Error: number 类型不能赋值给 never 类型</span></span>
<span></span>
<span><span>// ok, 作为函数返回类型的 never</span></span>
<span><span>let</span><span> bar</span><span>:</span><span> never</span><span> =</span><span> (</span><span>()</span><span> =></span><span> {</span></span>
<span><span>  throw</span><span> new</span><span> Error</span><span>(</span><span>'</span><span>Throw my hands in the air like I just dont care</span><span>'</span><span>)</span><span>;</span></span>
<span><span>}</span><span>)();</span></span></code></pre><p>利用这个性质，将上面的代码修改一下：
</p><pre tabindex="0"><code><span><span>type</span><span> Param</span><span> =</span><span> {</span></span>
<span><span>    a</span><span>:</span><span> string</span></span>
<span><span>    b</span><span>?:</span><span> never</span></span>
<span><span>} </span><span>|</span><span> {</span></span>
<span><span>    a</span><span>?:</span><span> never</span></span>
<span><span>    b</span><span>:</span><span> number</span></span>
<span><span>}</span></span>
<span></span>
<span><span>function</span><span> foo</span><span>(</span><span>p</span><span>:</span><span> Param</span><span>)</span><span> {</span></span>
<span><span>    console</span><span>.</span><span>log</span><span>(p)</span></span>
<span><span>}</span></span>
<span></span>
<span><span>foo</span><span>({a</span><span>:</span><span> "</span><span>hello</span><span>"</span><span>})</span></span>
<span><span>foo</span><span>({b</span><span>:</span><span> 123</span><span>})</span></span>
<span><span>foo</span><span>({a</span><span>:</span><span> "</span><span>hello</span><span>"</span><span>,</span><span> b</span><span>:</span><span> 12</span><span>}) </span><span>// 报错</span></span></code></pre><p><img alt="error" src="https://i.loli.net/2021/04/28/etzSwlqQ13b7ka9.png"></p><p>这样就可以保证用户只会使用两个互斥属性中的一个，在组件内简单做个条件渲染就可以了。
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>探究Python类型注解</title>
          <link>https://elliot00.com/posts/python-type-hint</link>
          <description>这篇文章介绍了作者在 Python 中造轮子的经历，以及过程中遇到的问题和解决办法。作者尝试使用 Prisma 作为灵感，在 Python 中创建了一个 ORM 库。接着讨论了 Python 中的类型注解，以及使用 dataclass 和 TypedDict 来定义类型。最后，作者提到了一种尝试使用模式匹配来限定可变参数类型的方法，但由于该方法目前在 Python 中还没有实现，因此无法使用。</description>
          <pubDate>Fri, 23 Apr 2021 14:15:47 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <p>最近业余时间在尝试造一些轮子，为了记录造轮子过程中的一些问题，准备开一个新系列，既然是造轮子，那这个系列就叫车轮滚滚吧～
</p><h2 id="user-content-orm" class="">ORM<a class="" tabindex="-1" href="#orm">#</a></h2><p>个人开发体验中，Python中的几个主流ORM库都有些让我不太舒服的点，最近的项目中用到了<a href="https://www.prisma.io/">Prisma</a>，催生了我在Python下撸一个ORM的想法。Prisma通过​<strong>DSL</strong>​定义数据模型，生成类型安全的客户端代码，可以借鉴一些想法在Python下试试。
</p><h2 id="user-content-类型注解" class="">类型注解<a class="" tabindex="-1" href="#类型注解">#</a></h2><p>Python是一门动态强类型语言，动态类型语言的特点就是灵活，比如在Java中有一种多态形式叫函数重载，多个函数可以有相同的名字，但是参数数量和类型不同，但是在Python中，函数参数的类型是动态的，数量也可以是动态的：
</p><pre tabindex="0"><code><span><span>def</span><span> add</span><span>(</span><span>lhs</span><span>,</span><span> rhs</span><span>)</span><span>:</span></span>
<span><span>    return</span><span> lhs </span><span>+</span><span> rhs</span></span>
<span></span>
<span><span>def</span><span> init</span><span>(</span><span>*</span><span>args</span><span>,</span><span> **</span><span>kwargs</span><span>)</span><span>:</span></span>
<span><span>    pass</span></span></code></pre><p>这种灵活的特性可以让我们的代码十分简洁，但这也是有代价的，比如a函数明明需要一个字符串参数，而我们却将一个返回值可能为空的b函数的返回值不加判断地作为a的参数，虽然在运行时会抛出错误，但是这个错误明明在运行前就可以避免的：
</p><p><img alt="普通函数" src="https://i.loli.net/2021/04/21/54sYujrNETc63b2.png"></p><p><img alt="添加类型注解" src="https://i.loli.net/2021/04/22/we6dqXtln2AMu95.png"></p><p><img alt="类型提示" src="https://i.loli.net/2021/04/21/SYzjA84FyMQVbeR.png"></p><p>虽然没有类型注解的版本给​<code class="">greet</code>​函数传递空值一样会报错，但是只有在运行后才会看到，而添加类型注解之后，编辑器就可以提前检测到类型问题。
</p><p>并且，当我们给greet函数的参数name添加类型注解后，编辑器的自动补全体验也会更好，编辑器会帮助提示该类型所拥有的属性和方法。
</p><h2 id="user-content-dataclass" class="">dataclass<a class="" tabindex="-1" href="#dataclass">#</a></h2><p>前面提了类型注解的一些优点，但其实官方明确表示过，类型注解不会做运行时推断：
</p><p>例如，将​<code class="">greet</code>​函数作为一个类的静态方法：
</p><pre tabindex="0"><code><span><span>class</span><span> A</span><span>:</span></span>
<span><span>    greet </span><span>=</span><span> staticmethod</span><span>(</span><span>greet</span><span>)</span></span>
<span></span>
<span><span># 不会给出提示</span></span>
<span><span>A</span><span>.</span><span>greet</span><span>(</span><span>12</span><span>)</span></span></code></pre><p>没有手动写注解的地方，不会有提示，比如下面的嵌套调用：
</p><pre tabindex="0"><code><span><span># person加注解和不加，结果不同</span></span>
<span><span>def</span><span> hof</span><span>(</span><span>person</span><span>)</span><span>:</span></span>
<span><span>    greet</span><span>(</span><span>person.name</span><span>)</span></span></code></pre><p>不过值得一提的是，类型注解的实现上并没有做到官方宣称的对运行时没有影响：
</p><pre tabindex="0"><code><span><span>class</span><span> Foo</span><span>:</span></span>
<span><span>    @</span><span>classmethod</span></span>
<span><span>    def</span><span> create</span><span>(</span><span>cls</span><span>,</span><span> *</span><span>args</span><span>)</span><span> -></span><span> Foo:</span></span>
<span><span>        ...</span></span></code></pre><p>这段代码会带来运行时错误，<a href="https://stackoverflow.com/questions/55320236/does-python-evaluate-type-hinting-of-a-forward-reference">解决办法见此</a>。
</p><p><code class="">Python3.7</code>​带来了一个新功能，叫做​<code class="">Data Class</code>​，如下代码：
</p><pre tabindex="0"><code><span><span>from</span><span> dataclasses </span><span>import</span><span> dataclass</span></span>
<span></span>
<span></span>
<span><span>@</span><span>dataclass</span></span>
<span><span>class</span><span> Point</span><span>:</span></span>
<span><span>    x</span><span>:</span><span> float</span></span>
<span><span>    y</span><span>:</span><span> float</span></span></code></pre><p>这个装饰器会自动为用户定义的类生成​<code class="">__init__</code>​，​<code class="">__repr__</code>​等方法，自动生成的​<code class="">__init__</code>​方法居然带有类型注解，这种类定义像不像在定义一个ORM的model？看看Django的模型定义：
</p><pre tabindex="0"><code><span><span>from</span><span> django</span><span>.</span><span>db </span><span>import</span><span> models</span></span>
<span></span>
<span><span>class</span><span> Person</span><span>(</span><span>models</span><span>.</span><span>Model</span><span>):</span></span>
<span><span>    first_name </span><span>=</span><span> models</span><span>.</span><span>CharField</span><span>(</span><span>max_length</span><span>=</span><span>30</span><span>)</span></span>
<span><span>    last_name </span><span>=</span><span> models</span><span>.</span><span>CharField</span><span>(</span><span>max_length</span><span>=</span><span>30</span><span>)</span></span></code></pre><p>如果能直接为这种数据类生成带类型注解的​<code class="">create</code>​、​<code class="">update</code>​等API,岂不是很方便？可是我不论是用装饰器，还是元类，动态添加的方法的类型注解都无法被PyCharm识别，所以dataclass这东西有什么黑魔法？知道我看到了这个问题<a href="https://intellij-support.jetbrains.com/hc/en-us/community/posts/360002765319-How-to-support-dynamic-type-hint-in-self-code-">How to support dynamic type hint in self code?</a>：
</p><blockquote><p>PyCharm understands @dataclass decorator, but doesn't understand your custom decorator @mydataclass. You could use @dataclass for MyTest and change it to be similar to Test.
</p><p>If it is required to have custom decorators this way, then the only option is to write some plugin for PyCharm.
</p></blockquote><p>好吧，自己写个第三方插件当然可以分析代码，实现更强的类型推断，但这也太不体面了......看来还得考虑代码生成
</p><h2 id="user-content-typeddict" class="">TypedDict<a class="" tabindex="-1" href="#typeddict">#</a></h2><p>Python中的函数有着不定长度的可变参数定义：
</p><pre tabindex="0"><code><span><span>def</span><span> foo</span><span>(</span><span>*</span><span>args</span><span>,</span><span> **</span><span>kwargs</span><span>)</span><span>:</span></span>
<span><span>    ...</span></span></code></pre><p>分别代表不限长度的位置参数和不限长度的关键字参数：
</p><pre tabindex="0"><code><span><span>foo</span><span>(</span><span>a</span><span>,</span><span> b</span><span>,</span><span> c</span><span>,</span><span> d</span><span>,</span><span> e</span><span>=</span><span>1</span><span>,</span><span> f</span><span>=</span><span>2</span><span>,</span><span> g</span><span>=</span><span>3</span><span>)</span></span></code></pre><p>两种参数会被当成元组和字典处理，而在类型注解中有一个<a href="https://docs.python.org/3/library/typing.html#typing.TypedDict">TypedDict</a>，可以定义固定类型的字典：
</p><pre tabindex="0"><code><span><span>class</span><span> Point2D</span><span>(</span><span>TypedDict</span><span>):</span></span>
<span><span>    x</span><span>:</span><span> int</span></span>
<span><span>    y</span><span>:</span><span> int</span></span>
<span><span>    label</span><span>:</span><span> str</span></span>
<span></span>
<span><span>a</span><span>:</span><span> Point2D </span><span>=</span><span> {</span><span>'</span><span>x</span><span>'</span><span>:</span><span> 1</span><span>,</span><span> '</span><span>y</span><span>'</span><span>:</span><span> 2</span><span>,</span><span> '</span><span>label</span><span>'</span><span>:</span><span> '</span><span>good</span><span>'</span><span>}</span><span>  # OK</span></span>
<span><span>b</span><span>:</span><span> Point2D </span><span>=</span><span> {</span><span>'</span><span>z</span><span>'</span><span>:</span><span> 3</span><span>,</span><span> '</span><span>label</span><span>'</span><span>:</span><span> '</span><span>bad</span><span>'</span><span>}</span><span>           # Fails type check</span></span>
<span></span>
<span><span>assert</span><span> Point2D</span><span>(</span><span>x</span><span>=</span><span>1</span><span>,</span><span> y</span><span>=</span><span>2</span><span>,</span><span> label</span><span>=</span><span>'</span><span>first</span><span>'</span><span>)</span><span> ==</span><span> dict</span><span>(</span><span>x</span><span>=</span><span>1</span><span>,</span><span> y</span><span>=</span><span>2</span><span>,</span><span> label</span><span>=</span><span>'</span><span>first</span><span>'</span><span>)</span></span></code></pre><p>与之类似的还有<a href="https://docs.python.org/3/library/typing.html#typing.NamedTuple">NamedTuple</a>，那可不可以在父类中用它们来限定可变参数类型呢？按照模式匹配的思路，应该可以这样写：
</p><pre tabindex="0"><code><span><span>def</span><span> foo</span><span>(</span><span>*</span><span>args</span><span>:</span><span> *</span><span>Args</span><span>,</span><span> **</span><span>kwargs</span><span>:</span><span> **</span><span>KArgs</span><span>)</span><span>: </span><span>...</span></span></code></pre><p>事实证明不行，<a href="https://github.com/python/mypy/issues/4441">这个Issue</a>是2018年开的，我写这篇文章的时候已经2021了......
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>Django+React全栈开发：关联用户</title>
          <link>https://elliot00.com/posts/react-django-user</link>
          <description>这篇文章介绍了如何在Django REST Framework中设置用户权限。首先，它解释了如何将用户与文章关联，并创建了一个自定义的权限类 IsAdminOrReadOnly ，该类允许管理员创建、删除和修改文章，而其他用户只能读取文章。然后，它展示了如何将该权限类添加到视图类中，并解释了如何使用 perform_create 方法和 read_only_fields 属性来确保只有管理员才能创建文章，并且作者字段是只读的。最后，它演示了如何使用HTTPie工具测试API的正确性。</description>
          <pubDate>Sun, 18 Apr 2021 07:39:42 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <p><a href="https://elliot00.com/posts/react-django-custom-auth">上一篇文章</a>其实已经讲了一点登录验证相关的内容，不过主要还是为了回答一位群友关于定制DJango用户模型的提问而临时写的，认证（authentication）与授权（authorization）实质上是两个步骤，但是一般都放在一起讲，认证是识别身份，你用管理员账户登录，密码正确，身份被确认为管理员，这是认证，因为是文章作者，所以有编辑文章的权限，这是授权。
</p><h2 id="user-content-文章关联用户" class="">文章关联用户<a class="" tabindex="-1" href="#文章关联用户">#</a></h2><p>用户与文章应该是​<strong>一对多</strong>​的关系，首先要修改我们的Article模型：
</p><pre tabindex="0"><code><span><span>class</span><span> Article</span><span>(</span><span>models</span><span>.</span><span>Model</span><span>):</span></span>
<span><span>    author </span><span>=</span><span> models</span><span>.</span><span>ForeignKey</span><span>(</span></span>
<span><span>        User</span><span>,</span></span>
<span><span>        on_delete</span><span>=</span><span>models.CASCADE</span><span>,</span></span>
<span><span>        related_name</span><span>=</span><span>'</span><span>articles</span><span>'</span></span>
<span><span>    )</span></span>
<span><span># ......</span></span></code></pre><p>接着去执行迁移操作：
</p><pre tabindex="0"><code><span><span>$</span><span> python manage.py makemigrations</span></span>
<span><span>$</span><span> python manage.py migrate</span></span></code></pre><p>注意到之前的文章都是没有作者的，所以执行迁移的时候会要求要么退出迁移，在模型上添加默认值，或者现在给出默认值。建议多熟悉迁移操作并且适当掌握一些​<code class="">SQL</code>​用法，这种涉及到数据变更的，有时候还得手动解决冲突。这里给我们之前创建的管理员账户ID做默认值就行，当然开发环境简单粗暴点也可以直接删库。
</p><p>先修改一下序列化器，加入​<code class="">author</code>​字段：
</p><pre tabindex="0"><code><span><span>class</span><span> ArticleSerializer</span><span>(</span><span>serializers</span><span>.</span><span>ModelSerializer</span><span>):</span></span>
<span></span>
<span><span>    class</span><span> Meta</span><span>:</span></span>
<span><span>        model </span><span>=</span><span> Article</span></span>
<span><span>        fields </span><span>=</span><span> [</span><span>'</span><span>id</span><span>'</span><span>,</span><span> '</span><span>title</span><span>'</span><span>,</span><span> '</span><span>body</span><span>'</span><span>,</span><span> '</span><span>author</span><span>'</span><span>,</span><span> '</span><span>created</span><span>'</span><span>,</span><span> '</span><span>updated</span><span>'</span><span>]</span></span></code></pre><p>然后用命令行工具httpie测试一下API：
</p><pre tabindex="0"><code><span><span>$</span><span> http POST 127.0.0.1:8000/api/articles/ </span><span>title</span><span>=</span><span>"</span><span>user</span><span>"</span><span> body</span><span>=</span><span>"</span><span>relationship</span><span>"</span><span> author</span><span>=</span><span>1</span></span></code></pre><p>可以正常创建新文章，但是，我们只要在POST请求里带上用户ID，就可以为任意用户创建文章，这是不可取的，现在来解决这个问题。
</p><h2 id="user-content-权限类" class="">权限类<a class="" tabindex="-1" href="#权限类">#</a></h2><p>上一节已经讲过了自定义权限类，现在来写一个只允许管理员创建删除修改，其他用户只读的权限类。
</p><p>新建​<code class="">article/permissions.py</code>​：
</p><pre tabindex="0"><code><span><span>from</span><span> rest_framework </span><span>import</span><span> permissions</span></span>
<span></span>
<span></span>
<span><span>class</span><span> IsAdminOrReadOnly</span><span>(</span><span>permissions</span><span>.</span><span>BasePermission</span><span>):</span></span>
<span><span>    def</span><span> has_permission</span><span>(</span><span>self</span><span>,</span><span> request</span><span>,</span><span> view</span><span>)</span><span>:</span></span>
<span><span>        if</span><span> request</span><span>.</span><span>method </span><span>in</span><span> permissions</span><span>.</span><span>SAFE_METHODS</span><span>:</span></span>
<span><span>            return</span><span> True</span></span>
<span><span>        return</span><span> request</span><span>.</span><span>user</span><span>.</span><span>is_superuser</span></span></code></pre><p>继承父类的​<code class="">has_permission</code>​方法，这个方法的返回值是布尔值，​<code class="">True</code>​即表示授权通过，对任意用户的请求在​<code class="">SAFE_METHODS</code>​内的，直接通过，否则就看用户是否是管理员。
</p><pre tabindex="0"><code><span><span># SAFE_METHODS源码</span></span>
<span><span>SAFE_METHODS</span><span> =</span><span> (</span><span>'</span><span>GET</span><span>'</span><span>,</span><span> '</span><span>HEAD</span><span>'</span><span>,</span><span> '</span><span>OPTIONS</span><span>'</span><span>)</span></span></code></pre><p>现在要修改一下视图类：
</p><pre tabindex="0"><code><span><span># 引入自定义的权限类</span></span>
<span><span>from</span><span> article</span><span>.</span><span>permissions </span><span>import</span><span> IsAdminOrReadOnly</span></span>
<span></span>
<span></span>
<span><span>class</span><span> ArticleViewSet</span><span>(</span><span>viewsets</span><span>.</span><span>ModelViewSet</span><span>):</span></span>
<span><span>    # 加上这一行</span></span>
<span><span>    permission_classes </span><span>=</span><span> [</span><span>IsAdminOrReadOnly</span><span>]</span></span></code></pre><p>现在再使用httpie测试，会得到错误响应：
</p><pre tabindex="0"><code><span><span>{</span></span>
<span><span>    "detail"</span><span>:</span><span> "</span><span>Authentication credentials were not provided.</span><span>"</span></span>
<span><span>}</span></span></code></pre><p>现在带上验证信息再请求一次：
</p><pre tabindex="0"><code><span><span>$</span><span> http -a elliot:test1234 POST 127.0.0.1:8000/api/articles/ </span><span>title</span><span>=</span><span>"</span><span>user</span><span>"</span><span> body</span><span>=</span><span>"</span><span>relationship</span><span>"</span><span> author</span><span>=</span><span>1</span></span>
<span></span>
<span><span>......</span></span>
<span><span>{</span></span>
<span><span>    "author": 1,</span></span>
<span><span>    "body": "relationship",</span></span>
<span><span>    "created": "2021-04-18T07:24:40.144287Z",</span></span>
<span><span>    "id": 5,</span></span>
<span><span>    "title": "user",</span></span>
<span><span>    "updated": "2021-04-18T07:24:40.144560Z"</span></span>
<span><span>}</span></span></code></pre><p>这次成功了，但是还有个问题，就是​<code class="">author</code>​这个字段仍然需要传递，并且只要是管理员，这个author值可以填任何已存在的用户ID。现在再来修复这个问题。
</p><h2 id="user-content-请求对象" class="">请求对象<a class="" tabindex="-1" href="#请求对象">#</a></h2><pre tabindex="0"><code><span><span>class</span><span> ArticleViewSet</span><span>(</span><span>viewsets</span><span>.</span><span>ModelViewSet</span><span>):</span></span>
<span></span>
<span><span>    # 添加这个方法</span></span>
<span><span>    def</span><span> perform_create</span><span>(</span><span>self</span><span>,</span><span> serializer</span><span>)</span><span>:</span></span>
<span><span>        serializer</span><span>.</span><span>save</span><span>(</span><span>author</span><span>=</span><span>self</span><span>.request.user</span><span>)</span></span></code></pre><p>这里覆写了父类的​<code class="">perform_create</code>​方法，在序列化器保存时，从请求对象中获取​<code class="">user</code>​值并赋值给author参数，但是用户仍然可以传递author字段，可以在序列化器中把它设置为只读：
</p><pre tabindex="0"><code><span><span>class</span><span> ArticleSerializer</span><span>(</span><span>serializers</span><span>.</span><span>ModelSerializer</span><span>):</span></span>
<span></span>
<span><span>    class</span><span> Meta</span><span>:</span></span>
<span><span>        # 添加属性</span></span>
<span><span>        read_only_fields </span><span>=</span><span> [</span><span>'</span><span>author</span><span>'</span><span>,</span><span> ]</span></span></code></pre><p>再次测试：
</p><pre tabindex="0"><code><span><span>$</span><span> http -a elliot:test1234 POST 127.0.0.1:8000/api/articles/ </span><span>title</span><span>=</span><span>"</span><span>author is readonly</span><span>"</span><span> body</span><span>=</span><span>"</span><span>author is readonly</span><span>"</span><span> author</span><span>=</span><span>2</span></span>
<span><span>.....</span></span>
<span><span>{</span></span>
<span><span>    "author": 1,</span></span>
<span><span>    "body": "author is readonly",</span></span>
<span><span>    "created": "2021-04-18T07:39:31.175273Z",</span></span>
<span><span>    "id": 8,</span></span>
<span><span>    "title": "author is readonly",</span></span>
<span><span>    "updated": "2021-04-18T07:39:31.175525Z"</span></span>
<span><span>}</span></span></code></pre><p>这样即使传递了非法的author字段也会被忽略掉。
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>Django+React全栈开发：自定义验证与授权</title>
          <link>https://elliot00.com/posts/react-django-custom-auth</link>
          <description>这篇文章主要讲了如何自定义 Django 的用户模型、序列化器、验证、权限和限流。1. 自定义用户模型：可以使用继承 AbstractUser 或 AbstractBaseUser 来扩展原生 User 模型，并添加额外的字段和方法。2. 序列化器：定义了如何将模型数据转换成可用于 API 请求和响应的格式。3. 验证：可以使用 TokenAuthentication 或 BasicAuthentication 进行验证，并设置 Token 过期时间。4. 权限：可以使用 AdministratorLevel 这样的自定义权限类来限制不同用户对 API 的访问。5. 限流：可以使用 UserRateThrottle 来限制用户请求的频率。</description>
          <pubDate>Sat, 17 Apr 2021 12:04:51 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <p>因为之前有人问过Django的自定义用户模型，就写了这篇文章，放在我的《Django+React全栈开发》系列里凑个数，不过和后续内容关联性不大，不感兴趣可以直接跳过。
</p><h2 id="user-content-自定义user" class="">自定义User<a class="" tabindex="-1" href="#自定义user">#</a></h2><p>Django原生的​<strong>User模型</strong>​已经足够满足一般小网站的需求，但是有时候不可避免要对用户模型做一些定制，官方文档给出了​<strong>四种方法</strong>​：
</p><ul><li>proxy model
</li><li>OneToOneField
</li><li>继承AbstractUser
</li><li>继承AbstractBaseUser
</li></ul><p>前两个方法​<strong>适用于只要扩展用户信息或增加一些处理方法而和身份验证无关</strong>​，而后两者则适用于对于身份验证有定制需求。
</p><h3 id="user-content-继承abstractuser">继承AbstractUser<a class="" tabindex="-1" href="#继承abstractuser">#</a></h3><p>Django官方文档对于如何定制用户模型有着详尽的解释，这里仅仅讲讲我在某次实践中是如何使用的。
</p><p>首先我们可以新建一个​<code class="">Django app</code>​，我们可以把验证授权相关的功能都放在这里，假定命名为​<code class="">core</code>​。假如我们需要多种分级的等级标识，而不仅仅是原生​<code class="">User</code>​模型的​<code class="">is_staff</code>​字段指示用户是否是管理员，例如需要三重等级，可以像下面这样编写代码：
</p><pre tabindex="0"><code><span><span># core/models.py</span></span>
<span><span>class</span><span> User</span><span>(</span><span>AbstractUser</span><span>):</span></span>
<span><span>    LEVEL_SET</span><span> =</span><span> (</span></span>
<span><span>        (</span><span>0</span><span>,</span><span> '</span><span>Super User</span><span>'</span><span>)</span><span>,</span></span>
<span><span>        (</span><span>1</span><span>,</span><span> '</span><span>Normal User</span><span>'</span><span>)</span><span>,</span></span>
<span><span>        (</span><span>2</span><span>,</span><span> '</span><span>Internship</span><span>'</span><span>)</span><span>,</span></span>
<span><span>    )</span></span>
<span><span>    level </span><span>=</span><span> models</span><span>.</span><span>IntegerField</span><span>(</span><span>choices</span><span>=</span><span>LEVEL_SET</span><span>,</span><span> default</span><span>=</span><span>2</span><span>)</span></span>
<span></span>
<span><span>    class</span><span> Meta</span><span>:</span></span>
<span><span>        ordering </span><span>=</span><span> (</span><span>'</span><span>date_joined</span><span>'</span><span>,</span><span>)</span></span></code></pre><p>之后需要在项目的​<code class="">settings.py</code>​文件中加入：
</p><pre tabindex="0"><code><span><span># 字符串内容是“app名.模型名”</span></span>
<span><span>AUTH_USER_MODEL</span><span> =</span><span> '</span><span>core.User</span><span>'</span></span></code></pre><p>可以在​<code class="">core/admin.py</code>​中注册我们的定制模型：
</p><pre tabindex="0"><code><span><span>from</span><span> django</span><span>.</span><span>contrib </span><span>import</span><span> admin</span></span>
<span><span>from</span><span> django</span><span>.</span><span>contrib</span><span>.</span><span>auth</span><span>.</span><span>admin </span><span>import</span><span> UserAdmin</span></span>
<span><span>from</span><span> .</span><span>models </span><span>import</span><span> User</span></span>
<span></span>
<span><span>admin</span><span>.</span><span>site</span><span>.</span><span>register</span><span>(</span><span>User</span><span>,</span><span> UserAdmin</span><span>)</span></span></code></pre><p>有一点需要注意，Django的模型需要迁移操作，对于定制的​<code class="">User</code>​，最好在项目刚刚开始的时候，在你还没有执行第一次​<code class="">python manage.py migrate</code>​的时候完成上述操作，当然如果还在开发阶段，即使之前执行过迁移操作，也可以通过删除项目中所有​<code class="">migrations</code>​文件夹以及​<code class="">sqlite</code>​文件来初始化。
</p><p>此后，如果你的模型中有需要自定义用户模型做外键的需求，​<strong>例如文章与文章作者</strong>​，可以参考如下设置：
</p><pre tabindex="0"><code><span><span>from</span><span> django</span><span>.</span><span>conf </span><span>import</span><span> settings</span></span>
<span><span>from</span><span> django</span><span>.</span><span>db </span><span>import</span><span> models</span></span>
<span></span>
<span><span>class</span><span> Article</span><span>(</span><span>models</span><span>.</span><span>Model</span><span>):</span></span>
<span><span>    author </span><span>=</span><span> models</span><span>.</span><span>ForeignKey</span><span>(</span></span>
<span><span>        settings.AUTH_USER_MODEL</span><span>,</span></span>
<span><span>        on_delete</span><span>=</span><span>models.CASCADE</span><span>,</span></span>
<span><span>    )</span></span></code></pre><p>任何需要使用到我们自定义用户模型的地方都可以这样操作。
</p><h3 id="user-content-序列化">序列化<a class="" tabindex="-1" href="#序列化">#</a></h3><p>可以参考如下代码：
</p><pre tabindex="0"><code><span><span># core/serializers.py</span></span>
<span><span>from</span><span> rest_framework </span><span>import</span><span> serializers</span></span>
<span><span>from</span><span> core</span><span>.</span><span>models </span><span>import</span><span> User</span></span>
<span></span>
<span></span>
<span><span>class</span><span> UserSerializer</span><span>(</span><span>serializers</span><span>.</span><span>HyperlinkedModelSerializer</span><span>):</span></span>
<span><span>    password </span><span>=</span><span> serializers</span><span>.</span><span>CharField</span><span>(</span><span>style</span><span>=</span><span>{</span><span>'</span><span>input_type</span><span>'</span><span>: </span><span>'</span><span>password</span><span>'</span><span>}</span><span>,</span><span> label</span><span>=</span><span>'</span><span>密码</span><span>'</span><span>,</span><span> write_only</span><span>=</span><span>True</span><span>)</span></span>
<span></span>
<span><span>    class</span><span> Meta</span><span>:</span></span>
<span><span>        model </span><span>=</span><span> User</span></span>
<span><span>        fields </span><span>=</span><span> [</span><span>'</span><span>url</span><span>'</span><span>,</span><span> '</span><span>id</span><span>'</span><span>,</span><span> '</span><span>username</span><span>'</span><span>,</span><span> '</span><span>password</span><span>'</span><span>,</span><span> '</span><span>email</span><span>'</span><span>,</span><span> '</span><span>level</span><span>'</span><span>,</span><span> '</span><span>is_active</span><span>'</span><span>,</span><span> '</span><span>date_joined</span><span>'</span><span>]</span></span></code></pre><p>这里我们设置​<code class="">password</code>​字段时加入了​<code class="">write_only=True</code>​这个参数，这样我们的view视图将​<strong>只会在处理=POST=、=PUT=、=PATCH=请求时（如果你允许这些请求的话）写入密码而不会在返回用户列表或详情信息时显示密码</strong>​。
</p><p>接下来可以写个简单的视图试试：
</p><pre tabindex="0"><code><span><span># 别忘了引入我们自定义的模型与序列化器</span></span>
<span><span>class</span><span> UserViewSet</span><span>(</span><span>viewsets</span><span>.</span><span>ModelViewSet</span><span>):</span></span>
<span><span>    queryset </span><span>=</span><span> User</span><span>.</span><span>objects</span><span>.</span><span>get_queryset</span><span>()</span></span>
<span><span>    serializer_class </span><span>=</span><span> UserSerializer</span></span></code></pre><p>现在尝试使用​<code class="">POST</code>​请求创建一个新用户吧（不过要记得注册路由），最简单的方法是直接用浏览器打开访问​<strong>127.0.0.1:8000/api/users/</strong>​。接着使用新建的账户密码验证登录，你会发现验证失败。
</p><p>为了安全起见，我们设置的密码会经过​<strong>加密处理</strong>​再放入数据库，同样，验证用户密码时，也会​<strong>对密码加密再比对密文</strong>​，这样即使是拥有查看数据库权限的人也​<strong>无法查看用户密码的明文</strong>​。但是这里我们的​<strong>视图没有对密码进行加密</strong>​就被存入了数据库，而用户验证时却是用的Django自身的API，比对的是密文，也就是验证时你提交的密码被加密，而数据库中的密码却没有加密，这样就出现了无法匹配的现象。
</p><p>可以通过覆写​<code class="">ViewSet</code>​的​<code class="">create</code>​方法来修复这个bug：
</p><pre tabindex="0"><code><span><span>from</span><span> django</span><span>.</span><span>contrib</span><span>.</span><span>auth</span><span>.</span><span>hashers </span><span>import</span><span> make_password</span></span>
<span></span>
<span></span>
<span><span>class</span><span> UserViewSet</span><span>(</span><span>viewsets</span><span>.</span><span>ModelViewSet</span><span>):</span></span>
<span><span>    # ......</span></span>
<span><span>    def</span><span> create</span><span>(</span><span>self</span><span>,</span><span> request</span><span>,</span><span> *</span><span>args</span><span>,</span><span> **</span><span>kwargs</span><span>)</span><span>:</span></span>
<span><span>        serializer </span><span>=</span><span> self</span><span>.</span><span>get_serializer</span><span>(</span><span>data</span><span>=</span><span>request.data</span><span>)</span></span>
<span><span>        serializer</span><span>.</span><span>is_valid</span><span>(</span><span>raise_exception</span><span>=</span><span>True</span><span>)</span></span>
<span><span>        serializer</span><span>.</span><span>validated_data</span><span>[</span><span>'</span><span>password</span><span>'</span><span>]</span><span> =</span><span> make_password</span><span>(</span><span>serializer.validated_data</span><span>[</span><span>'</span><span>password</span><span>'</span><span>])</span></span>
<span><span>        self</span><span>.</span><span>perform_create</span><span>(</span><span>serializer</span><span>)</span></span>
<span><span>        headers </span><span>=</span><span> self</span><span>.</span><span>get_success_headers</span><span>(</span><span>serializer.data</span><span>)</span></span>
<span><span>        return</span><span> Response</span><span>(</span><span>serializer.data</span><span>,</span><span> status</span><span>=</span><span>status.HTTP_201_CREATED</span><span>,</span><span> headers</span><span>=</span><span>headers</span><span>)</span></span></code></pre><p>这里调用Django提供的​<code class="">make_password</code>​函数来生成正确的加密的密码。
</p><p>既然是编写​<code class="">REST</code>​风格的API，那么建议对于用户的增加、修改、删除都使用这个视图。对于用户改密码的需求，可以在序列化器中添加一个​<code class="">old_password</code>​字段，并设置为当前密码，同时要改写视图类的​<code class="">partial_update</code>​方法。以下是一个我用来实现超管直接修改所有用户密码的需求（不要问我为什么会有这种需求～）的方式：
</p><pre tabindex="0"><code><span><span>class</span><span> UserViewSet</span><span>(</span><span>viewsets</span><span>.</span><span>ModelViewSet</span><span>):</span></span>
<span><span>    # ......</span></span>
<span><span>    def</span><span> partial_update</span><span>(</span><span>self</span><span>,</span><span> request</span><span>,</span><span> *</span><span>args</span><span>,</span><span> **</span><span>kwargs</span><span>)</span><span>:</span></span>
<span><span>        if</span><span> '</span><span>password</span><span>'</span><span> in</span><span> request</span><span>.</span><span>data</span><span>:</span></span>
<span><span>            request</span><span>.</span><span>data</span><span>[</span><span>'</span><span>password</span><span>'</span><span>]</span><span> =</span><span> make_password</span><span>(</span><span>request.data</span><span>[</span><span>'</span><span>password</span><span>'</span><span>])</span></span>
<span><span>        kwargs</span><span>[</span><span>'</span><span>partial</span><span>'</span><span>]</span><span> =</span><span> True</span></span>
<span><span>        return</span><span> self</span><span>.</span><span>update</span><span>(</span><span>request</span><span>,</span><span> *</span><span>args</span><span>,</span><span> **</span><span>kwargs</span><span>)</span></span></code></pre><p>通过设置​<code class="">partial</code>​参数为​<code class="">True</code>​并将内容传递给​<code class="">update</code>​来实现仅针对密码部分更新。
</p><h2 id="user-content-自定义token验证" class="">自定义Token验证<a class="" tabindex="-1" href="#自定义token验证">#</a></h2><p>常规情况下我们通过用户的用户名与密码来识别用户身份，最基础的方法是每次请求都需要用户名及密码，但是这非常麻烦且容易暴露敏感信息，一般不采用。比较常见的方式是基于​<code class="">OAuth</code>​、​<code class="">Session</code>​以及​<code class="">Token</code>​的验证方式。​<code class="">REST framework</code>​为我们提供了可用的TokenAPI，这里介绍一下在此基础上做一些扩展。
</p><p><strong>注意：这里的Token和下一章要讲的JWT并不等同。</strong></p><p>一般来说登录验证会使用到一些成熟的第三方库，这里拿原生的Token验证来练习一下。
</p><h3 id="user-content-token类">Token类<a class="" tabindex="-1" href="#token类">#</a></h3><p>这里使用的基于Token的验证就是客户端发送用户密码，服务端创建一个与用户相对应的随机字符串，之后客户端每次请求时在请求头中加上这段字符串，服务端解码Token再与数据库信息进行比对，即可通过验证。
</p><p>为了使用​<code class="">REST framework</code>​提供的Token我们需要在​<code class="">settings.py</code>​中注册：
</p><pre tabindex="0"><code><span><span>INSTALLED_APPS</span><span> =</span><span> [</span></span>
<span><span>    ...</span></span>
<span><span>    '</span><span>rest_framework.authtoken</span><span>'</span></span>
<span><span>]</span></span></code></pre><p>如果你已经创建过用户，可以使用命令​<code class="">python manage.py shell</code>​，按如下操作：
</p><pre tabindex="0"><code><span><span>>>></span><span> from</span><span> core</span><span>.</span><span>models </span><span>import</span><span> User</span></span>
<span><span>>>></span><span> from</span><span> rest_framework</span><span>.</span><span>authtoken</span><span>.</span><span>models </span><span>import</span><span> Token</span></span>
<span></span>
<span><span>>>></span><span> for</span><span> user </span><span>in</span><span> User</span><span>.</span><span>objects</span><span>.</span><span>all</span><span>():</span></span>
<span><span>>>></span><span>     Token</span><span>.</span><span>objects</span><span>.</span><span>get_or_create</span><span>(</span><span>user</span><span>=</span><span>user</span><span>)</span></span></code></pre><p>同时修改​<code class="">core/models.py</code>​，通过Django的信号机制，在每次新建用户时为其创建Token：
</p><pre tabindex="0"><code><span><span>......</span></span>
<span></span>
<span><span>@</span><span>receiver</span><span>(</span><span>post_save</span><span>,</span><span> sender</span><span>=</span><span>settings.AUTH_USER_MODEL</span><span>)</span></span>
<span><span>def</span><span> create_auth_token</span><span>(</span><span>sender</span><span>,</span><span> instance</span><span>=</span><span>None</span><span>,</span><span> created</span><span>=</span><span>False</span><span>,</span><span> **</span><span>kwargs</span><span>)</span><span>:</span></span>
<span><span>    # 接收用户创建信号，每次新建用户后自动创建token</span></span>
<span><span>    if</span><span> created</span><span>:</span></span>
<span><span>        Token</span><span>.</span><span>objects</span><span>.</span><span>create</span><span>(</span><span>user</span><span>=</span><span>instance</span><span>)</span></span></code></pre><p>接下来修改你需要添加权限的视图：
</p><pre tabindex="0"><code><span><span>from</span><span> rest_framework</span><span>.</span><span>authentication </span><span>import</span><span> TokenAuthentication</span></span>
<span><span>from</span><span> rest_framework</span><span>.</span><span>permissions </span><span>import</span><span> IsAuthenticated</span></span>
<span></span>
<span><span># ......</span></span>
<span></span>
<span><span>class</span><span> ArticleViewSet</span><span>(</span><span>viewsets</span><span>.</span><span>ModelViewSet</span><span>):</span></span>
<span><span>    authentication_classes </span><span>=</span><span> [</span><span>TokenAuthentication</span><span>]</span></span>
<span><span>    permission_classes </span><span>=</span><span> [</span><span>permissions</span><span>.</span><span>IsAuthenticated</span><span>]</span></span>
<span><span>    queryset </span><span>=</span><span> Article</span><span>.</span><span>objects</span><span>.</span><span>all</span><span>()</span></span>
<span><span>    serializer_class </span><span>=</span><span> ArticleSerializer</span></span></code></pre><p>通过​<code class="">authentication_classes</code>​指定要使用的验证类，有关​<code class="">permission_classes</code>​的内容下节在说。现在我们设置一下项目的​<code class="">urls.py</code>​：
</p><pre tabindex="0"><code><span><span>from</span><span> rest_framework</span><span>.</span><span>authtoken </span><span>import</span><span> views</span></span>
<span></span>
<span></span>
<span><span>urlpatterns </span><span>=</span><span> [</span></span>
<span><span>    ......</span></span>
<span><span>    path</span><span>(</span><span>'</span><span>api-token-auth/</span><span>'</span><span>,</span><span> views.obtain_auth_token</span><span>),</span></span>
<span><span>]</span></span></code></pre><p>现在向该接口发送POST请求提交用户密码，将会得到Token，仅在将该Token放在请求头​<code class="">headers</code>​中，才可得到​<code class="">articles</code>​的正确响应，使用命令行工具httpie调试的示例如下：
</p><pre tabindex="0"><code><span><span>$</span><span> http POST http://127.0.0.1:8000/api-token-auth/ </span><span>username</span><span>=</span><span>"</span><span>user</span><span>"</span><span> password</span><span>=</span><span>"</span><span>password</span><span>"</span><span>                           </span></span>
<span><span>HTTP/1.1 200 OK</span></span>
<span><span>......</span></span>
<span></span>
<span><span>{</span></span>
<span><span>    "token": "bed522b6f41b962b5c829598e990b9f058518c9d"</span></span>
<span><span>}</span></span>
<span></span>
<span><span>$</span><span> http http://127.0.0.1:8000/articles/ </span><span>'</span><span>Authorization: Token bed522b6f41b962b5c829598e990b9f058518c9d</span><span>'</span></span></code></pre><p>你可以尝试一下不带​<code class="">Authorization</code>​这一串会得到什么响应。
</p><h3 id="user-content-token过期">Token过期<a class="" tabindex="-1" href="#token过期">#</a></h3><p>但是​<code class="">REST framework</code>​自带的Token有着不小的缺陷，最典型的一点是这个Token​<strong>没有过期机制</strong>​，这意味着如果有谁截获了你的Token，就可以无限制的使用，安全风险实在太大。下面我们来试试扩展一下原生的Token验证，新建​<code class="">core/authentication.py</code>​：
</p><pre tabindex="0"><code><span><span>import</span><span> datetime</span></span>
<span><span>from</span><span> django</span><span>.</span><span>conf </span><span>import</span><span> settings</span></span>
<span><span>from</span><span> django</span><span>.</span><span>core</span><span>.</span><span>cache </span><span>import</span><span> cache</span></span>
<span><span>from</span><span> rest_framework</span><span>.</span><span>authentication </span><span>import</span><span> TokenAuthentication</span></span>
<span><span>from</span><span> rest_framework </span><span>import</span><span> exceptions</span></span>
<span><span>from</span><span> django</span><span>.</span><span>utils</span><span>.</span><span>translation </span><span>import</span><span> ugettext_lazy </span><span>as</span><span> _</span></span>
<span></span>
<span><span># 记得要在settings.py中设置REST_FRAMEWORK_TOKEN_EXPIRE_MINUTES变量</span></span>
<span><span># 这是为了方便以后调节过期时间，例如给该变量赋值为60，则为一小时过期</span></span>
<span><span>EXPIRE_MINUTES</span><span> =</span><span> getattr</span><span>(</span><span>settings</span><span>,</span><span> '</span><span>REST_FRAMEWORK_TOKEN_EXPIRE_MINUTES</span><span>'</span><span>,</span><span> 1</span><span>)</span></span>
<span></span>
<span></span>
<span><span>class</span><span> ExpiringTokenAuthentication</span><span>(</span><span>TokenAuthentication</span><span>):</span></span>
<span><span>    """</span></span>
<span><span>    Setup token expired time</span></span>
<span><span>    """</span></span>
<span><span>    def</span><span> authenticate_credentials</span><span>(</span><span>self</span><span>,</span><span> key</span><span>)</span><span>:</span></span>
<span><span>        model </span><span>=</span><span> self</span><span>.</span><span>get_model</span><span>()</span></span>
<span><span>        # 利用Django的cache减少数据库操作</span></span>
<span><span>        cache_user </span><span>=</span><span> cache</span><span>.</span><span>get</span><span>(</span><span>key</span><span>)</span></span>
<span><span>        if</span><span> cache_user</span><span>:</span></span>
<span><span>            return</span><span> cache_user</span><span>,</span><span> key</span></span>
<span></span>
<span><span>        try</span><span>:</span></span>
<span><span>            token </span><span>=</span><span> model</span><span>.</span><span>objects</span><span>.</span><span>select_related</span><span>(</span><span>'</span><span>user</span><span>'</span><span>).</span><span>get</span><span>(</span><span>key</span><span>=</span><span>key</span><span>)</span></span>
<span><span>        except</span><span> model</span><span>.</span><span>DoesNotExist</span><span>:</span></span>
<span><span>            raise</span><span> exceptions</span><span>.</span><span>AuthenticationFailed</span><span>(</span><span>_</span><span>(</span><span>"</span><span>无效令牌</span><span>"</span><span>))</span></span>
<span></span>
<span><span>        if</span><span> not</span><span> token</span><span>.</span><span>user</span><span>.</span><span>is_active</span><span>:</span></span>
<span><span>            raise</span><span> exceptions</span><span>.</span><span>AuthenticationFailed</span><span>(</span><span>_</span><span>(</span><span>"</span><span>用户被禁用</span><span>"</span><span>))</span></span>
<span></span>
<span><span>        time_now </span><span>=</span><span> datetime</span><span>.</span><span>datetime</span><span>.</span><span>now</span><span>()</span></span>
<span></span>
<span><span>        if</span><span> token</span><span>.</span><span>created </span><span>&#x3C;</span><span> time_now </span><span>-</span><span> datetime</span><span>.</span><span>timedelta</span><span>(</span><span>minutes</span><span>=</span><span>EXPIRE_MINUTES</span><span>):</span></span>
<span><span>            token</span><span>.</span><span>delete</span><span>()</span></span>
<span><span>            raise</span><span> exceptions</span><span>.</span><span>AuthenticationFailed</span><span>(</span><span>_</span><span>(</span><span>"</span><span>认证信息已过期</span><span>"</span><span>))</span></span>
<span></span>
<span><span>        if</span><span> token</span><span>:</span></span>
<span><span>            # EXPIRE_MINUTES * 60 because the param is seconds</span></span>
<span><span>            cache</span><span>.</span><span>set</span><span>(</span><span>key</span><span>,</span><span> token.user</span><span>,</span><span> EXPIRE_MINUTES </span><span>*</span><span> 60</span><span>)</span></span>
<span></span>
<span><span>        return</span><span> token</span><span>.</span><span>user</span><span>,</span><span> token</span></span></code></pre><p>同时我们可以修改​<code class="">core/views.py</code>​，定制验证视图，如果当前Token没有过期则返回cache中的Token，否则创建新Token：
</p><pre tabindex="0"><code><span><span>from</span><span> rest_framework</span><span>.</span><span>authtoken</span><span>.</span><span>views </span><span>import</span><span> ObtainAuthToken</span></span>
<span></span>
<span><span># ......</span></span>
<span></span>
<span><span>class</span><span> ObtainExpiringAuthToken</span><span>(</span><span>ObtainAuthToken</span><span>):</span></span>
<span><span>    # 别忘了from rest_framework.authentication import BasicAuthentication</span></span>
<span><span>    # 这是通过post用户名密码获取token的视图，可不能采取token验证哦</span></span>
<span><span>    authentication_classes </span><span>=</span><span> [</span><span>BasicAuthentication</span><span>]</span></span>
<span></span>
<span><span>    def</span><span> post</span><span>(</span><span>self</span><span>,</span><span> request</span><span>,</span><span> *</span><span>args</span><span>,</span><span> **</span><span>kwargs</span><span>)</span><span>:</span></span>
<span><span>        serializer </span><span>=</span><span> self</span><span>.</span><span>serializer_class</span><span>(</span><span>data</span><span>=</span><span>request.data</span><span>)</span></span>
<span><span>        if</span><span> serializer</span><span>.</span><span>is_valid</span><span>():</span></span>
<span><span>            user </span><span>=</span><span> serializer</span><span>.</span><span>validated_data</span><span>[</span><span>'</span><span>user</span><span>'</span><span>]</span></span>
<span><span>            token</span><span>,</span><span> created </span><span>=</span><span> Token</span><span>.</span><span>objects</span><span>.</span><span>get_or_create</span><span>(</span><span>user</span><span>=</span><span>user</span><span>)</span></span>
<span><span>            time_now </span><span>=</span><span> datetime</span><span>.</span><span>datetime</span><span>.</span><span>now</span><span>()</span></span>
<span></span>
<span><span>            if</span><span> created </span><span>or</span><span> (token</span><span>.</span><span>created </span><span>&#x3C;</span><span> time_now </span><span>-</span><span> datetime</span><span>.</span><span>timedelta</span><span>(</span><span>minutes</span><span>=</span><span>EXPIRE_MINUTES</span><span>)</span><span>)</span><span>:</span></span>
<span><span>                token</span><span>.</span><span>delete</span><span>()</span></span>
<span><span>                token </span><span>=</span><span> Token</span><span>.</span><span>objects</span><span>.</span><span>create</span><span>(</span><span>user</span><span>=</span><span>user</span><span>)</span></span>
<span><span>                token</span><span>.</span><span>created </span><span>=</span><span> time_now</span></span>
<span><span>                token</span><span>.</span><span>save</span><span>()</span></span>
<span><span>            # 这里可以定制返回信息</span></span>
<span><span>            context </span><span>=</span><span> {</span></span>
<span><span>                '</span><span>id</span><span>'</span><span>:</span><span> user</span><span>.</span><span>id</span><span>,</span></span>
<span><span>                '</span><span>username</span><span>'</span><span>:</span><span> user</span><span>.</span><span>username</span><span>,</span></span>
<span><span>                '</span><span>token</span><span>'</span><span>:</span><span> token</span><span>.</span><span>key</span></span>
<span><span>            }</span></span>
<span></span>
<span><span>            return</span><span> Response</span><span>(</span><span>context</span><span>)</span></span>
<span><span>        else</span><span>:</span></span>
<span><span>            return</span><span> Response</span><span>(</span><span>serializer.errors</span><span>,</span><span> status</span><span>=</span><span>status.HTTP_400_BAD_REQUEST</span><span>)</span></span></code></pre><p>这样我们要修改​<code class="">urls.py</code>​以启用我们新的验证视图：
</p><pre tabindex="0"><code><span><span>from</span><span> core</span><span>.</span><span>views </span><span>import</span><span> ObtainExpiringAuthToken</span></span>
<span></span>
<span></span>
<span><span>urlpatterns </span><span>=</span><span> [</span></span>
<span><span>    ......</span></span>
<span><span>    path</span><span>(</span><span>'</span><span>api-token-auth/</span><span>'</span><span>,</span><span> ObtainExpiringAuthToken.</span><span>as_view</span><span>()),</span></span>
<span><span>]</span></span></code></pre><p>现在你可以修改​<code class="">settings.py</code>​中的​<em>REST<sub>FRAMEWORK</sub><sub>TOKEN</sub><sub>EXPIRE</sub><sub>MINUTES</sub></em>​变量为​<code class="">1</code>​来看看Token过期的效果。
</p><h2 id="user-content-定制permission" class="">定制permission<a class="" tabindex="-1" href="#定制permission">#</a></h2><p>既然有了验证，也就是对用户的身份进行识别是管理员、普通用户，还是未登录用户，那么肯定要​<strong>针对不同类型的用户给予不同权限，否则整个验证过程就失去了意义</strong>​。事实上我们之前在​<code class="">articles</code>​API中已经使用了​<code class="">REST framework</code>​提供的​<code class="">IsAuthenticated</code>​权限，指定只有经过登录验证的用户可以访问。现在让我们设置一个基于用户级别的权限吧，新建​<code class="">core/permissions.py</code>​：
</p><pre tabindex="0"><code><span><span>from</span><span> rest_framework </span><span>import</span><span> permissions</span></span>
<span></span>
<span></span>
<span><span>class</span><span> AdministratorLevel</span><span>(</span><span>permissions</span><span>.</span><span>BasePermission</span><span>):</span></span>
<span><span>    # 客户端向服务端发送请求后，此方法被调用，根据返回的布尔值决定用户是否拥有权限</span></span>
<span><span>    def</span><span> has_permission</span><span>(</span><span>self</span><span>,</span><span> request</span><span>,</span><span> view</span><span>)</span><span>:</span></span>
<span><span>        if</span><span> request</span><span>.</span><span>user</span><span>.</span><span>is_authenticated</span><span>:</span></span>
<span><span>            if</span><span> request</span><span>.</span><span>method </span><span>in</span><span> permissions</span><span>.</span><span>SAFE_METHODS</span><span>:</span></span>
<span><span>                return</span><span> True</span></span>
<span><span>            # 普通管理员可修改数据</span></span>
<span><span>            elif</span><span> request</span><span>.</span><span>method</span><span>.</span><span>upper</span><span>()</span><span> in</span><span> (</span><span>'</span><span>POST</span><span>'</span><span>,</span><span> '</span><span>PUT</span><span>'</span><span>,</span><span> '</span><span>PATCH</span><span>'</span><span>) </span><span>and</span><span> request</span><span>.</span><span>user</span><span>.</span><span>level </span><span>==</span><span> 1</span><span>:</span></span>
<span><span>                return</span><span> True</span></span>
<span><span>            # 超级管理员拥有所有权限</span></span>
<span><span>            elif</span><span> request</span><span>.</span><span>user</span><span>.</span><span>level </span><span>==</span><span> 0</span><span>:</span></span>
<span><span>                return</span><span> True</span></span>
<span><span>            else</span><span>:</span></span>
<span><span>                return</span><span> False</span></span>
<span><span>        return</span><span> False</span></span></code></pre><p>现在可以修改​<code class="">articles API</code>​的视图，用我们自定义的权限类替换掉之前的​<code class="">IsAuthenticated</code>​，并且新建多个不同等级的用户，试试它们的权限吧。这里的if-else分支可以优化一下，不妨试试。
</p><h2 id="user-content-throttling" class="">Throttling<a class="" tabindex="-1" href="#throttling">#</a></h2><p>顾名思义，throttling起到节流作用，它和permissions有些类似，但可以用来限制客户端的请求频率。
</p><p>例如，我们想要用户的一个Token在一小时内过期，但只要用户保持活跃，那么在较长的一段时间内不必重复登录。可以添加一个通过旧Token获取新Token的接口，由前端判断如果用户在活跃状态下，那么可以在用户不知道的情况下获取新的Token。
</p><pre tabindex="0"><code><span><span># core/views.py</span></span>
<span><span>from</span><span> rest_framework</span><span>.</span><span>views </span><span>import</span><span> APIView</span></span>
<span></span>
<span><span># ......</span></span>
<span></span>
<span><span>class</span><span> TokenForToken</span><span>(</span><span>APIView</span><span>):</span></span>
<span><span>    authentication_classes </span><span>=</span><span> [</span><span>ExpiringTokenAuthentication</span><span>]</span></span>
<span><span>    permission_classes </span><span>=</span><span> [</span><span>permissions</span><span>.</span><span>IsAuthenticated</span><span>]</span></span>
<span></span>
<span><span>    def</span><span> get</span><span>(</span><span>self</span><span>,</span><span> request</span><span>,</span><span> format</span><span>=</span><span>None</span><span>)</span><span>:</span></span>
<span><span>        user </span><span>=</span><span> request</span><span>.</span><span>user</span></span>
<span><span>        # 这里有个小bug，留给读者去思考了</span></span>
<span><span>        token</span><span>,</span><span> created </span><span>=</span><span> Token</span><span>.</span><span>objects</span><span>.</span><span>get_or_create</span><span>(</span><span>user</span><span>=</span><span>user</span><span>)</span></span>
<span><span>        time_now </span><span>=</span><span> datetime</span><span>.</span><span>datetime</span><span>.</span><span>now</span><span>()</span></span>
<span><span>        token</span><span>.</span><span>delete</span><span>()</span></span>
<span><span>        token </span><span>=</span><span> Token</span><span>.</span><span>objects</span><span>.</span><span>create</span><span>(</span><span>user</span><span>=</span><span>user</span><span>)</span></span>
<span><span>        token</span><span>.</span><span>created </span><span>=</span><span> time_now</span></span>
<span><span>        token</span><span>.</span><span>save</span><span>()</span></span>
<span><span>        return</span><span> Response</span><span>(</span><span>{</span><span>'</span><span>token</span><span>'</span><span>: token.key}</span></span></code></pre><p>在​<code class="">urls.py</code>​中注册此视图，我们就可以用旧的Token来替换新的Token，但是如果你想要限制用户使用此方法的次数，则可以设置​<code class="">Throttling</code>​。如下修改​<code class="">settings.py</code>​：
</p><pre tabindex="0"><code><span><span>REST_FRAMEWORK</span><span> =</span><span> {</span></span>
<span><span>    '</span><span>DEFAULT_THROTTLE_CLASSES</span><span>'</span><span>:</span><span> [</span></span>
<span><span>        '</span><span>rest_framework.throttling.UserRateThrottle</span><span>'</span></span>
<span><span>    ]</span><span>,</span></span>
<span><span>    '</span><span>DEFAULT_THROTTLE_RATES</span><span>'</span><span>:</span><span> {</span></span>
<span><span>        '</span><span>user</span><span>'</span><span>:</span><span> '</span><span>10/day</span><span>'</span></span>
<span><span>    }</span></span>
<span><span>}</span></span></code></pre><p>接着在​<code class="">core/views.py</code>​中修改：
</p><pre tabindex="0"><code><span><span>from</span><span> rest_framework</span><span>.</span><span>throttling </span><span>import</span><span> UserRateThrottle</span></span>
<span></span>
<span><span># ......</span></span>
<span><span>class</span><span> TokenForToken</span><span>(</span><span>APIView</span><span>):</span></span>
<span><span>    authentication_classes </span><span>=</span><span> [</span><span>ExpiringTokenAuthentication</span><span>]</span></span>
<span><span>    permission_classes </span><span>=</span><span> [</span><span>permissions</span><span>.</span><span>IsAuthenticated</span><span>]</span></span>
<span><span>    throttle_classes </span><span>=</span><span> [</span><span>UserRateThrottle</span><span>]</span></span>
<span></span>
<span><span>    # ......</span></span></code></pre><p>这样可以限制​<strong>每个用户每天最多请求10次</strong>​。更多​<strong>throttling</strong>​的用法请查看​<code class="">REST framework</code>​官方文档。
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>Django+React全栈开发：路由</title>
          <link>https://elliot00.com/posts/react-django-route</link>
          <description>这篇文章主要介绍了如何使用 react-router-dom 来搭建一个简单的单页应用（SPA）。首先介绍了 react-router-dom 的基本概念和用法，然后通过一个例子演示了如何使用 react-router-dom 来构建一个包含首页、详情页和关于页的SPA。最后还提供了一个练习题，让读者尝试重写文章详情组件以显示真正的文章详情。</description>
          <pubDate>Sat, 17 Apr 2021 06:42:43 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <h2 id="user-content-react-router" class="">react-router<a class="" tabindex="-1" href="#react-router">#</a></h2><p>现在的网站一般来讲很少只有单个“页面”，对于我们的博客来说，除了文章列表的界面，起码还得得有个文章详情页才行。
</p><p><strong>单页应用（SPA）</strong>​：可能你在官方介绍​<code class="">create-react-app</code>​这个脚手架时已经看到了这个名词，但千万不要误以为单页面的意思是没有“​<strong>可以点击的链接</strong>​”的。在这里所说的单页应用实际上就是：既然我们将一个网页应用看作一堆组件的组合，那么动态的页面其实​<strong>只需要动态更新显示部分组件</strong>​就行，而不是像传统做法那样，​<strong>服务端提供完整的新页面</strong>​，所有资源都重新加载。
</p><p>好了，看到这里你应该明白，​<code class="">create-react-app</code>​是一个适于构建单页应用的脚手架，但不意味着想要做一个文章详情页就要再次​<code class="">yarn create react-app</code>​新建一个项目了吧。
</p><p>好了，说了这么多，开始写代码吧。这里我们需要学习一个新东西：​<code class="">react-router-dom</code>​。首先进入我们的​<code class="">frontend</code>​目录，终端运行​<code class="">yarn add react-router-dom</code>​来安装依赖。
</p><h2 id="user-content-函数组件" class="">函数组件<a class="" tabindex="-1" href="#函数组件">#</a></h2><p>之前我们已经讲过了​<strong>类组件</strong>​，在​<code class="">React</code>​中我们也可以创建​<strong>函数形式</strong>​的组件，函数组件又称为​<strong>无状态组件</strong>​，它可以接收一个​<code class="">props</code>​作为参数，但是不可以使用​<code class="">state</code>​，​<strong>它没有状态，也没有生命周期函数</strong>​（在本教程介绍​<code class="">React Hooks</code>​之前这句话是正确的）。
</p><p>为了介绍函数组件，这里先拆分一下组件，从​<code class="">ArticleList</code>​拆出一个​<code class="">ArticleItem</code>​。
</p><pre tabindex="0"><code><span><span>const</span><span> ArticleItem</span><span> =</span><span> props</span><span> =></span><span> {</span></span>
<span><span>  const</span><span> {</span><span>title</span><span>,</span><span> created</span><span>,</span><span> updated</span><span>}</span><span> =</span><span> props</span><span>.</span><span>item</span><span>;</span></span>
<span></span>
<span><span>  return</span><span> (</span></span>
<span><span>    &#x3C;</span><span>div</span><span> className</span><span>=</span><span>"</span><span>py-3</span><span>"</span><span>></span></span>
<span><span>      &#x3C;</span><span>div</span><span> className</span><span>=</span><span>"</span><span>text-2xl font-semibold</span><span>"</span><span>></span><span>{</span><span>title</span><span>}</span><span>&#x3C;/</span><span>div</span><span>></span></span>
<span><span>      &#x3C;</span><span>div</span><span> className</span><span>=</span><span>"</span><span>space-x-2</span><span>"</span><span>></span></span>
<span><span>        &#x3C;</span><span>span</span><span>></span><span>创建时间：</span><span>&#x3C;</span><span>time</span><span> title</span><span>=</span><span>{</span><span>created</span><span>}</span><span>></span><span>{</span><span>dayjs</span><span>()</span><span>.</span><span>to</span><span>(</span><span>dayjs</span><span>(</span><span>created</span><span>))</span><span>}</span><span>&#x3C;/</span><span>time</span><span>>&#x3C;/</span><span>span</span><span>></span></span>
<span><span>        &#x3C;</span><span>span</span><span>></span><span>更新时间：</span><span>&#x3C;</span><span>time</span><span> title</span><span>=</span><span>{</span><span>updated</span><span>}</span><span>></span><span>{</span><span>dayjs</span><span>()</span><span>.</span><span>to</span><span>(</span><span>dayjs</span><span>(</span><span>updated</span><span>))</span><span>}</span><span>&#x3C;/</span><span>time</span><span>>&#x3C;/</span><span>span</span><span>></span></span>
<span><span>      &#x3C;/</span><span>div</span><span>></span></span>
<span><span>    &#x3C;/</span><span>div</span><span>></span></span>
<span><span>  )</span></span>
<span><span>}</span></span></code></pre><p>函数组件顾名思义就是一个函数，只要它返回一个JSX元素，就可以被当作组件使用，这里使用了​<strong>箭头函数</strong>​，要了解这些基础知识的细节，推荐去看​<code class="">MDN</code>​、阮一峰的<a href="https://es6.ruanyifeng.com/">ES6入门教程</a>或者<a href="https://zh.javascript.info/">现代JavaScript教程</a>。
</p><p><code class="">ArticleItem</code>​组件的内容是从​<code class="">ArticleList</code>​复制过来的，那么现在去修改​<code class="">ArticleList</code>​的内容：
</p><pre tabindex="0"><code><span><span>......</span></span>
<span><span> &#x3C;</span><span>div</span><span> className</span><span>=</span><span>"</span><span>font-sans</span><span>"</span><span>></span></span>
<span><span>     {</span><span>articleList</span><span>.</span><span>map</span><span>(</span><span>item</span><span> =></span></span>
<span><span>       &#x3C;</span><span>ArticleItem</span><span> key</span><span>=</span><span>{</span><span>item</span><span>.</span><span>id</span><span>}</span><span> item</span><span>=</span><span>{</span><span>item</span><span>}</span><span>/></span></span>
<span><span>     )</span><span>}</span></span>
<span><span> &#x3C;/</span><span>div</span><span>></span></span>
<span><span> ......</span></span></code></pre><p>我们将​<code class="">aritcleList</code>​中的元素当作​<code class="">ArticleItem</code>​的​<code class="">props</code>​传递下去。可以看到这里的​<code class="">ArticleItem</code>​组件就是一个函数，它的返回值就是要渲染的内容。我个人的习惯是有组件需要复用了或者组件太大了再去提取组件，这里纯粹为了演示下函数组件写法。
</p><h2 id="user-content-路由" class="">路由<a class="" tabindex="-1" href="#路由">#</a></h2><p>在开始写代码之前，让我们先来构思一下路由划分：
</p><ol><li>首页，展示文章列表
</li><li>详情页，显示文章详情
</li><li>about页，展示博主信息
</li></ol><p>也就是说我们需要做三个页面，通常网站都会有个导航栏，一般来说进入这三个页面中的任意一个，导航栏都不会消失，也就是导航栏是可以​<strong>复用</strong>​的，而页面的主体部分，则可以动态替换。这样我们就知道，需要以下几个组件：
</p><ol><li>App组件，主体框架
</li><li>导航栏组件
</li><li>文章列表组件
</li><li>文章详情组件
</li><li>About组件
</li></ol><p>首先改写​<code class="">App.js</code>​：
</p><pre tabindex="0"><code><span><span>import</span><span> {</span></span>
<span><span>  BrowserRouter</span><span> as</span><span> Router</span><span>,</span></span>
<span><span>  Switch</span><span>,</span></span>
<span><span>  Route</span><span>,</span></span>
<span><span>} </span><span>from</span><span> "</span><span>react-router-dom</span><span>"</span><span>;</span></span>
<span><span>import</span><span> ArticleList </span><span>from</span><span> "</span><span>./ArticleList</span><span>"</span><span>;</span></span>
<span><span>import</span><span> About </span><span>from</span><span> "</span><span>./About</span><span>"</span><span>;</span></span>
<span><span>import</span><span> Nav </span><span>from</span><span> "</span><span>./Nav</span><span>"</span><span>;</span></span>
<span></span>
<span><span>function</span><span> App</span><span>()</span><span> {</span></span>
<span><span>  return</span><span> (</span></span>
<span><span>    &#x3C;</span><span>Router</span><span>></span></span>
<span><span>      &#x3C;</span><span>div</span><span>></span></span>
<span><span>        &#x3C;</span><span>Nav</span><span> /></span></span>
<span></span>
<span><span>        &#x3C;</span><span>Switch</span><span>></span></span>
<span><span>          &#x3C;</span><span>Route</span><span> path</span><span>=</span><span>"</span><span>/about</span><span>"</span><span>></span></span>
<span><span>            &#x3C;</span><span>About</span><span> /></span></span>
<span><span>          &#x3C;/</span><span>Route</span><span>></span></span>
<span><span>          &#x3C;</span><span>Route</span><span> path</span><span>=</span><span>"</span><span>/</span><span>"</span><span>></span></span>
<span><span>            &#x3C;</span><span>ArticleList</span><span> /></span></span>
<span><span>          &#x3C;/</span><span>Route</span><span>></span></span>
<span><span>        &#x3C;/</span><span>Switch</span><span>></span></span>
<span><span>      &#x3C;/</span><span>div</span><span>></span></span>
<span><span>    &#x3C;/</span><span>Router</span><span>></span></span>
<span><span>  )</span></span>
<span><span>}</span></span>
<span></span>
<span><span>export</span><span> default</span><span> App</span><span>;</span></span></code></pre><p>同时还要新建​<code class="">Nav.js</code>​和​<code class="">About.js</code>​：
</p><pre tabindex="0"><code><span><span>import</span><span> {Link} </span><span>from</span><span> "</span><span>react-router-dom</span><span>"</span><span>;</span></span>
<span></span>
<span><span>const</span><span> Nav</span><span> =</span><span> ()</span><span> =></span><span> {</span></span>
<span><span>  return</span><span> (</span></span>
<span><span>    &#x3C;</span><span>nav</span><span>></span></span>
<span><span>      &#x3C;</span><span>ul</span><span>></span></span>
<span><span>        &#x3C;</span><span>li</span><span>></span></span>
<span><span>          &#x3C;</span><span>Link</span><span> to</span><span>=</span><span>"</span><span>/</span><span>"</span><span>></span><span>Home</span><span>&#x3C;/</span><span>Link</span><span>></span></span>
<span><span>        &#x3C;/</span><span>li</span><span>></span></span>
<span><span>        &#x3C;</span><span>li</span><span>></span></span>
<span><span>          &#x3C;</span><span>Link</span><span> to</span><span>=</span><span>"</span><span>/about</span><span>"</span><span>></span><span>About</span><span>&#x3C;/</span><span>Link</span><span>></span></span>
<span><span>        &#x3C;/</span><span>li</span><span>></span></span>
<span><span>      &#x3C;/</span><span>ul</span><span>></span></span>
<span><span>    &#x3C;/</span><span>nav</span><span>></span></span>
<span><span>  )</span></span>
<span><span>}</span></span>
<span></span>
<span><span>export</span><span> default</span><span> Nav</span></span></code></pre><pre tabindex="0"><code><span><span>// 这个组件主要就是个人简介，读者自由发挥就好</span></span>
<span><span>const</span><span> About</span><span> =</span><span> ()</span><span> =></span><span> {</span></span>
<span><span>  return</span><span> (</span></span>
<span><span>    &#x3C;</span><span>div</span><span>></span><span>hello world</span><span>&#x3C;/</span><span>div</span><span>></span></span>
<span><span>  )</span></span>
<span><span>}</span></span>
<span></span>
<span><span>export</span><span> default</span><span> About</span></span></code></pre><p>别忘了在之前的​<code class="">index.js</code>​中我们渲染的是​<code class="">ArticleList</code>​，现在去更改它：
</p><pre tabindex="0"><code><span><span>......</span></span>
<span><span>ReactDOM</span><span>.</span><span>render</span><span>(</span></span>
<span><span>  &#x3C;</span><span>React.StrictMode</span><span>></span></span>
<span><span>    &#x3C;</span><span>App</span><span> /></span></span>
<span><span>  &#x3C;/</span><span>React.StrictMode</span><span>></span><span>,</span></span>
<span><span>  document</span><span>.</span><span>getElementById</span><span>(</span><span>'</span><span>root</span><span>'</span><span>)</span></span>
<span><span>);</span></span></code></pre><p>主要看​<code class="">App</code>​和​<code class="">Nav</code>​两个组件，首先引入了​<code class="">react-router-dom</code>​中​<code class="">BrowserRouter</code>​包裹其它元素，​<code class="">Link</code>​组件放在Nav中做导航链接，​<code class="">Switch</code>​和​<code class="">Route</code>​搭配使用，​<code class="">Switch</code>​会搜索子元素=Route=​，当找到其路径与当前​<code class="">url</code>​相匹配的​<code class="">Route</code>​时，则渲染此​<code class="">Route</code>​内容，并忽略其它的​<code class="">Route</code>​。例如当前​<code class="">url</code>​为根路径​<code class="">/</code>​，那么就会渲染这里最后一个​<code class="">Route</code>​中的​<code class="">ArticleList</code>​，这样我们点击不同的Link，Switch组件渲染的内容就会切换，达到换页面的目的。如果按F12打开查看元素，你会发现点击不同导航链接，App组件内的元素会切换，而​<code class="">NetWork</code>​中则显示并没有发送任何网络请求。
</p><h2 id="user-content-详情页" class="">详情页<a class="" tabindex="-1" href="#详情页">#</a></h2><p>现在还剩最后一个页面需要完成，就是文章详情页。现在去修改​<code class="">ArticleList.js</code>​，让其根据文章ID创建不同的​<code class="">Link</code>​：
</p><pre tabindex="0"><code><span><span>......</span></span>
<span><span>import</span><span> { Link } </span><span>from</span><span> "</span><span>react-router-dom</span><span>"</span><span>;</span></span>
<span></span>
<span><span>......</span></span>
<span></span>
<span><span>const</span><span> ArticleItem</span><span> =</span><span> props</span><span> =></span><span> {</span></span>
<span><span>  const</span><span> {</span><span>title</span><span>,</span><span> created</span><span>,</span><span> updated</span><span>,</span><span> id</span><span>}</span><span> =</span><span> props</span><span>.</span><span>item</span><span>;</span></span>
<span></span>
<span><span>  return</span><span> (</span></span>
<span><span>    &#x3C;</span><span>div</span><span> className</span><span>=</span><span>"</span><span>py-3</span><span>"</span><span>></span></span>
<span><span>      &#x3C;</span><span>Link</span><span> to</span><span>=</span><span>{</span><span>`</span><span>/articles/</span><span>${</span><span>id</span><span>}</span><span>`</span><span>}</span><span>></span></span>
<span><span>        &#x3C;</span><span>div</span><span> className</span><span>=</span><span>"</span><span>text-2xl font-semibold</span><span>"</span><span>></span><span>{</span><span>title</span><span>}</span><span>&#x3C;/</span><span>div</span><span>></span></span>
<span><span>      &#x3C;/</span><span>Link</span><span>></span></span>
<span><span>      ......</span></span>
<span><span>    &#x3C;/</span><span>div</span><span>></span></span>
<span><span>  )</span></span>
<span><span>}</span></span>
<span></span>
<span><span>class</span><span> ArticleList</span><span> extends</span><span> Component</span><span> {</span></span>
<span><span>......</span></span>
<span><span>}</span></span></code></pre><p>我们使用​<code class="">ES6</code>​语法的模板字符串，注意​<code class="">&#x3C;Link to={...}></code>​里的​<strong>不是单引号</strong>​，而是​<strong>键盘左上角esc键下面那个反引号</strong>​。这和​<code class="">Python</code>​中的​<code class="">f</code>​字符串有些类似，都允许在字符串中嵌入变量，但是​<code class="">ES6</code>​的写起来有点麻烦。​<code class="">JSX</code>​的实现也离不开模板字符串哦。
</p><p>OK，现在让我们在​<code class="">src</code>​目录下新建一个​<code class="">ArticleDetail.js</code>​：
</p><pre tabindex="0"><code><span><span>// ArticleDetail.js</span></span>
<span><span>import</span><span> React </span><span>from</span><span> "</span><span>react</span><span>"</span><span>;</span></span>
<span><span>import</span><span> { useParams } </span><span>from</span><span> '</span><span>react-router-dom</span><span>'</span><span>;</span></span>
<span></span>
<span><span>const</span><span> ArticleDetail</span><span> =</span><span> ()</span><span> =></span><span> {</span></span>
<span><span>// 取出url中的参数</span></span>
<span><span>  const</span><span> {</span><span> articleId</span><span> }</span><span> =</span><span> useParams</span><span>()</span><span>;</span></span>
<span></span>
<span><span>  return</span><span> (</span></span>
<span><span>    &#x3C;</span><span>div</span><span>></span></span>
<span><span>      article </span><span>{</span><span>articleId</span><span>}</span></span>
<span><span>    &#x3C;/</span><span>div</span><span>></span></span>
<span><span>  )</span></span>
<span><span>}</span></span>
<span></span>
<span><span>export</span><span> default</span><span> ArticleDetail</span></span></code></pre><p>对应的，在​<code class="">App.js</code>​中添加一个匹配项：
</p><pre tabindex="0"><code><span><span>// 注意要把根路径放在最后面</span></span>
<span><span>&#x3C;</span><span>Switch</span><span>></span></span>
<span><span>    &#x3C;</span><span>Route</span><span> path</span><span>=</span><span>"</span><span>/about</span><span>"</span><span>></span></span>
<span><span>        &#x3C;</span><span>About</span><span> /></span></span>
<span><span>    &#x3C;/</span><span>Route</span><span>></span></span>
<span><span>    &#x3C;</span><span>Route</span><span> path</span><span>=</span><span>"</span><span>/articles/:articleId</span><span>"</span><span>></span></span>
<span><span>        &#x3C;</span><span>ArticleDetail</span><span> /></span></span>
<span><span>    &#x3C;/</span><span>Route</span><span>></span></span>
<span><span>    &#x3C;</span><span>Route</span><span> path</span><span>=</span><span>"</span><span>/</span><span>"</span><span>></span></span>
<span><span>        &#x3C;</span><span>ArticleList</span><span> /></span></span>
<span><span>    &#x3C;/</span><span>Route</span><span>></span></span>
<span><span>&#x3C;/</span><span>Switch</span><span>></span></span></code></pre><p>现在在网页上点击文章标题或者导航栏的链接试试看吧。
</p><h2 id="user-content-练习" class="">练习<a class="" tabindex="-1" href="#练习">#</a></h2><p>现在我们的文章详情组件只是简单地显示了​<code class="">article + id</code>​，可以尝试重写组件以显示真正的文章详情。之前说过函数组件又叫无状态组件，没有​<code class="">state</code>​，也没有生命周期，这里暂时先不讲​<code class="">Hooks</code>​（其实我们已经不知不觉中使用过了），所以你可能要将​<code class="">ArticleDetail</code>​改写为类组件，并通过​<code class="">props</code>​传递文章=id=​并在​<code class="">componentDidMount</code>​中请求API。
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>Django+React全栈开发：界面优化</title>
          <link>https://elliot00.com/posts/react-django-ui-optimize</link>
          <description>这篇文章主要讲解了前端开发中的一些操作，包括时间处理、条件渲染、样式添加等内容。具体包括：使用dayjs库处理时间，实现显示文章创建时间和更新时间；使用条件渲染，在加载数据完成前显示“加载中”字样；使用CSS文件和内联样式为组件添加样式；使用原子化CSS和TailwindCSS框架来美化页面。这些操作对于前端开发人员来说是非常重要的，可以帮助他们快速构建出美观且实用的用户界面。</description>
          <pubDate>Sun, 11 Apr 2021 09:46:05 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <h2 id="user-content-时间处理" class="">时间处理<a class="" tabindex="-1" href="#时间处理">#</a></h2><p>不少博客都会在文章列表界面仅显示文章发布距今的时间（如下图），之前我们是粗暴地将后台传回的​<code class="">ISO 8601</code>​格式的时间字符串显示出来，现在我们来处理一下。
</p><p>首先让我们看看后台的数据经​<code class="">rest_framework</code>​序列化后是什么样子：​<code class="">2021-04-10T08:56:01.576834Z</code>​，这样显示太不体面了，给它修改一下。
</p><p>这里我们可以使用​<code class="">dayjs</code>​来处理时间，现在来到前端部分，打开​<code class="">DjangoWithReact/frontend</code>​目录，使用​<code class="">yarn</code>​安装依赖，运行命令​<code class="">yarn add dayjs</code>​。为了直接看到效果，别忘了把前后端两个程序都启动起来。现在来修改​<code class="">frontend/src/ArticleList.js</code>​：
</p><pre tabindex="0"><code><span><span>import</span><span> React</span><span>,</span><span> {Component} </span><span>from</span><span> "</span><span>react</span><span>"</span><span>;</span></span>
<span><span>import</span><span> dayjs </span><span>from</span><span> "</span><span>dayjs</span><span>"</span><span>;</span></span>
<span><span>import</span><span> "</span><span>dayjs/locale/zh-cn</span><span>"</span></span>
<span></span>
<span><span>// 本地化</span></span>
<span><span>dayjs</span><span>.</span><span>locale</span><span>(</span><span>'</span><span>zh-cn</span><span>'</span><span>)</span></span>
<span></span>
<span><span>class</span><span> ArticleList</span><span> extends</span><span> Component</span><span> {</span></span>
<span><span>  // 省略...</span></span>
<span></span>
<span><span>  render</span><span>()</span><span> {</span></span>
<span><span>      return</span><span> (</span></span>
<span><span>      &#x3C;</span><span>div</span><span> className</span><span>=</span><span>"</span><span>article-list</span><span>"</span><span>></span></span>
<span><span>        {</span><span>this</span><span>.</span><span>state</span><span>.</span><span>articleList</span><span>.</span><span>map</span><span>(</span><span>item</span><span> =></span></span>
<span><span>          &#x3C;</span><span>div</span><span> key</span><span>=</span><span>{</span><span>item</span><span>.</span><span>id</span><span>}</span><span>></span></span>
<span><span>            &#x3C;</span><span>h4</span><span>></span><span>{</span><span>item</span><span>.</span><span>title</span><span>}</span><span>&#x3C;/</span><span>h4</span><span>></span></span>
<span><span>            &#x3C;</span><span>p</span><span>></span></span>
<span><span>              &#x3C;</span><span>strong</span><span>></span><span>{</span><span>item</span><span>.</span><span>body</span><span>}</span><span>&#x3C;/</span><span>strong</span><span>></span></span>
<span><span>              &#x3C;</span><span>br</span><span>/></span></span>
<span><span>              &#x3C;</span><span>em</span><span>></span><span>创建时间：</span><span>{</span><span>dayjs</span><span>(</span><span>item</span><span>.</span><span>created</span><span>)</span><span>.</span><span>format</span><span>(</span><span>"</span><span>YYYY-MM-DD</span><span>"</span><span>)</span><span>}</span><span>&#x3C;/</span><span>em</span><span>></span></span>
<span><span>              &#x3C;</span><span>em</span><span>></span><span>更新时间：</span><span>{</span><span>dayjs</span><span>(</span><span>item</span><span>.</span><span>updated</span><span>)</span><span>.</span><span>format</span><span>(</span><span>"</span><span>YYYY-MM-DD</span><span>"</span><span>)</span><span>}</span><span>&#x3C;/</span><span>em</span><span>></span></span>
<span><span>            &#x3C;/</span><span>p</span><span>></span></span>
<span><span>          &#x3C;/</span><span>div</span><span>></span></span>
<span><span>        )</span><span>}</span></span>
<span><span>      &#x3C;/</span><span>div</span><span>></span></span>
<span><span>    );</span></span>
<span><span>  }</span></span>
<span></span>
<span><span>}</span></span></code></pre><p>现在打开浏览器浏览​<code class="">localhost:3000</code>​，你就能看到和上面图中一样的效果了，Django默认使用的是UTC时间，我们这里自动根据时区做了本地化，可以把格式化字符串改成​<code class="">YYYY-MM-DD HH:mm</code>​与后端数据对比。
</p><p>为了简洁起见，可以把时间改成相对时间，如昨天、上个月等，这需要引入​<code class="">dayjs</code>​的​<code class="">relativeTime</code>​插件：
</p><pre tabindex="0"><code><span><span>import</span><span> relativeTime </span><span>from</span><span> "</span><span>dayjs/plugin/relativeTime</span><span>"</span></span>
<span></span>
<span><span>dayjs</span><span>.</span><span>locale</span><span>(</span><span>'</span><span>zh-cn</span><span>'</span><span>)</span></span>
<span><span>dayjs</span><span>.</span><span>extend</span><span>(</span><span>relativeTime</span><span>)</span></span>
<span></span>
<span><span>class</span><span> ArticleList</span><span> extends</span><span> Component</span><span> {</span></span>
<span><span>  // ...</span></span>
<span></span>
<span><span>  render</span><span>()</span><span> {</span></span>
<span><span>      return</span><span> (</span></span>
<span><span>        ......</span></span>
<span><span>              {</span><span>/* 加个title，鼠标悬浮显示原始时间字符 */</span><span>}</span></span>
<span><span>              &#x3C;</span><span>em</span><span>></span><span>创建时间：</span><span>&#x3C;</span><span>time</span><span> title</span><span>=</span><span>{</span><span>item</span><span>.</span><span>created</span><span>}</span><span>></span><span>{</span><span>dayjs</span><span>()</span><span>.</span><span>to</span><span>(</span><span>dayjs</span><span>(</span><span>item</span><span>.</span><span>created</span><span>))</span><span>}</span><span>&#x3C;/</span><span>time</span><span>>&#x3C;/</span><span>em</span><span>></span></span>
<span><span>              &#x3C;</span><span>em</span><span>></span><span>更新时间：</span><span>&#x3C;</span><span>time</span><span> title</span><span>=</span><span>{</span><span>item</span><span>.</span><span>updated</span><span>}</span><span>></span><span>{</span><span>dayjs</span><span>()</span><span>.</span><span>to</span><span>(</span><span>dayjs</span><span>(</span><span>item</span><span>.</span><span>updated</span><span>))</span><span>}</span><span>&#x3C;/</span><span>time</span><span>>&#x3C;/</span><span>em</span><span>></span></span>
<span></span>
<span><span>    );</span></span>
<span><span>  }</span></span>
<span></span>
<span><span>}</span></span></code></pre><p>效果如下：
</p><p><img alt="效果图" src="https://i.loli.net/2021/04/11/z4wMmbcIZsxDPQR.png"></p><p>其实这里的需求很简单，不太需要用到​<code class="">dayjs</code>​这种时间处理库，也可以拿原生的<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl">Intl</a>API来实现，这里就不讲了。
</p><h2 id="user-content-条件渲染" class="">条件渲染<a class="" tabindex="-1" href="#条件渲染">#</a></h2><p>之前我们自己定义的名为​<code class="">articleList</code>​的数组对象现在​<strong>还在代码中</strong>​，但是事实上我们并不需要它，我们的文章数据是从后台​<code class="">API</code>​中取出的。现在删去原本定义​<code class="">articleList</code>​部分的代码，并在我们的​<strong>类组件ArticleList</strong>​的​<code class="">构造函数</code>​中修改以下部分：
</p><pre tabindex="0"><code><span><span>constructor</span><span>(</span><span>props</span><span>) {</span></span>
<span><span>    super</span><span>(</span><span>props</span><span>);</span></span>
<span><span>    this</span><span>.</span><span>state</span><span> =</span><span> {</span></span>
<span><span>    // 将state中的内容改成空数组</span></span>
<span><span>      articleList</span><span>:</span><span> []</span><span>,</span></span>
<span><span>    };</span></span>
<span><span>  }</span></span>
<span><span>  ......</span><span>.</span></span></code></pre><p>现在来关注一个组件中的​<code class="">render</code>​函数：
</p><pre tabindex="0"><code><span><span>render</span><span>() {</span></span>
<span><span>    const</span><span> {</span><span>articleList</span><span>}</span><span> =</span><span> this</span><span>.</span><span>state</span><span>;</span></span>
<span><span>    ......</span></span>
<span><span>  }</span></span></code></pre><p>增加这一行代码，从​<code class="">this.state</code>​对象中直接取出​<code class="">articleList</code>​，这样之前​<code class="">JSX</code>​中的​<code class="">this.state.articleList</code>​就都可以直接简写为​<code class="">articleList</code>​了。目前我们在​<strong>本地环境</strong>​中，获取API非常​<strong>快速</strong>​，并且数据量也很少，那么如果​<strong>在网络不好的情况下，后台的数据还没有取到</strong>​，我们的页面当然不能​<strong>一片空白</strong>​吧？
</p><p>修改​<code class="">render</code>​函数的返回值部分如下：
</p><pre tabindex="0"><code><span><span>return</span><span> (</span></span>
<span><span>    Array</span><span>.</span><span>isArray</span><span>(</span><span>articleList</span><span>) </span><span>&#x26;&#x26;</span><span> articleList</span><span>.</span><span>length</span><span> !==</span><span> 0</span></span>
<span><span>    ?</span></span>
<span><span>    &#x3C;</span><span>div</span><span> className</span><span>=</span><span>"</span><span>article-list</span><span>"</span><span>></span></span>
<span><span>        ......</span></span>
<span><span>    &#x3C;/</span><span>div</span><span>></span></span>
<span><span>    :</span></span>
<span><span>    &#x3C;</span><span>div</span><span>></span><span>Loading</span><span>&#x3C;/</span><span>div</span><span>></span></span>
<span><span>);</span></span></code></pre><p>这里我们在​<code class="">JSX</code>​中使用了​<strong>三元运算符</strong>​，代码很简单，只是当​<code class="">articleList</code>​数组不为空时显示文章列表，否则渲染​<code class="">&#x3C;p>加载中...&#x3C;/p></code>​。不过在​<code class="">JS</code>​中判断一个数组是否为空不能用Python的思维，例如​<code class="">arr =</code> []=​或​<code class="">arr ==</code> []=​都只会返回​<code class="">false</code>​。（PS：忍不住又想吐槽JS了。。。
</p><p>现在打开浏览器刷新，不过由于这里获取到数据很快，所以会“加载中”的字样会一闪而过，可以将整个​<code class="">componentDidMount</code>​函数注释掉看看效果。
</p><p>这就是​<code class="">React</code>​中的条件渲染了，可以根据情况来决定组件的渲染，目前我们的页面还非常简单，等以后我们添加了导航栏、侧边栏等元素时，你当然不希望因为文章数据还没取到而使得页面空空如也吧。
</p><p>后续讲到​<code class="">Hooks</code>​的时候，会用​<code class="">react-query</code>​这个强大的库来替代原生fetch，现在先用简陋实现吧。
</p><blockquote><p>可以尝试封装​​<strong>ErrorPage</strong>​​组件，在请求错误时显示
</p></blockquote><h3 id="user-content-提示">提示<a class="" tabindex="-1" href="#提示">#</a></h3><p>对代码做以下修改，将​<code class="">fetch</code>​捕获到的错误，设置到​<code class="">this.state</code>​中。完成练习后，暂停​<code class="">Django</code>​的运行，验证你做的是否正确吧。
</p><pre tabindex="0"><code><span><span>constructor</span><span>(</span><span>props</span><span>) {</span></span>
<span><span>    super</span><span>(</span><span>props</span><span>);</span></span>
<span><span>    this</span><span>.</span><span>state</span><span> =</span><span> {</span></span>
<span><span>      articleList</span><span>:</span><span> []</span><span>,</span></span>
<span><span>      error</span><span>:</span><span> null</span><span>,</span></span>
<span><span>    };</span></span>
<span><span>  }</span></span>
<span></span>
<span><span>  componentDidMount</span><span>() {</span></span>
<span><span>    fetch</span><span>(</span><span>'</span><span>/articles/</span><span>'</span><span>)</span></span>
<span><span>      ......</span></span>
<span><span>      .</span><span>catch</span><span>(</span><span>e</span><span> =></span><span> this</span><span>.</span><span>setState</span><span>({error</span><span>:</span><span> e</span><span>}));</span></span>
<span><span>  }</span></span></code></pre><h2 id="user-content-添加样式" class="">添加样式<a class="" tabindex="-1" href="#添加样式">#</a></h2><p>可能你已经发现了，​<code class="">frontend/src</code>​目录下，除了后缀名为​<code class="">js</code>​的文件，还有后缀为​<code class="">css</code>​的同名文件，打开​<code class="">App.js</code>​看一看（如果你还没有删除它），还能发现​<code class="">import './App.css';</code>​这一行代码。
</p><p>现在让我们也来写一个​<code class="">ArticleList.css</code>​：
</p><pre tabindex="0"><code><span><span>.</span><span>article-list</span><span> {</span></span>
<span><span>    text-align</span><span>:</span><span> center</span><span>;</span></span>
<span><span>}</span></span></code></pre><p>只是个文本居中显示，注意到我们已经在组件​<code class="">render</code>​函数的​<code class="">JSX</code>​中定义了​<code class="">div</code>​元素的​<code class="">className</code>​。现在只要在​<code class="">App.js</code>​中引入样式文件就行，在代码顶部添加​<code class="">import './ArticleList.css';</code>​。现在可以在页面中看到文字居中显示了。
</p><p>也可以在​<code class="">JSX</code>​中直接设定样式，例如：
</p><pre tabindex="0"><code><span><span>&#x3C;</span><span>h4</span><span> style</span><span>=</span><span>{</span><span>{ color</span><span>:</span><span> "</span><span>red</span><span>"</span><span> }</span><span>}</span><span>></span><span>{</span><span>item</span><span>.</span><span>title</span><span>}</span><span>&#x3C;/</span><span>h4</span><span>></span></span></code></pre><p>现在可以看到标题变成了红色。
</p><p>还可以使用​<code class="">CSS Module</code>​，我们使用了类名选择器，如​<code class="">className</code>"btn"=​，浏览器会对拥有这个类名的元素应用对应的样式规则，但是在不同组件里的按钮可能是不一样的样式，那么我们就要注意命名，名字相同很可能会导致意想不到的效果。CSS模块可以解决这个问题，这种样式文件的命名规则为​<code class="">组件名.module.css</code>​，例如​<code class="">ArticleList.module.css</code>​：
</p><pre tabindex="0"><code><span><span>.</span><span>title</span><span> {</span></span>
<span><span>    color</span><span>:</span><span> blueviolet</span><span>;</span></span>
<span><span>    font-size</span><span>:</span><span> 20</span><span>px</span><span>;</span></span>
<span><span>}</span></span></code></pre><p>接着修改组件：
</p><pre tabindex="0"><code><span><span>// 注意引入方式</span></span>
<span><span>import</span><span> style </span><span>from</span><span> "</span><span>./ArticleList.module.css</span><span>"</span><span>;</span></span>
<span></span>
<span></span>
<span><span>class</span><span> ArticleList</span><span> extends</span><span> Component</span><span> {</span></span>
<span><span>......</span></span>
<span></span>
<span><span>    render</span><span>()</span><span> {</span></span>
<span><span>        const</span><span> {</span><span> articleList</span><span> }</span><span> =</span><span> this</span><span>.</span><span>state</span><span>;</span></span>
<span><span>        return</span><span> (</span></span>
<span><span>            Array</span><span>.</span><span>isArray</span><span>(</span><span>articleList</span><span>) </span><span>&#x26;&#x26;</span><span> articleList</span><span>.</span><span>length</span><span> !==</span><span> 0</span></span>
<span><span>                ?</span></span>
<span><span>                &#x3C;</span><span>div</span><span>></span></span>
<span><span>                    {</span><span>articleList</span><span>.</span><span>map</span><span>(</span><span>item</span><span> =></span></span>
<span><span>                        &#x3C;</span><span>div</span><span> key</span><span>=</span><span>{</span><span>item</span><span>.</span><span>id</span><span>}</span><span>></span></span>
<span><span>                            {</span><span>/* 使用对应命名空间的类名 */</span><span>}</span></span>
<span><span>                            &#x3C;</span><span>div</span><span> className</span><span>=</span><span>{</span><span>style</span><span>.</span><span>title</span><span>}</span><span>></span><span>{</span><span>item</span><span>.</span><span>title</span><span>}</span><span>&#x3C;/</span><span>div</span><span>></span></span>
<span><span>                        &#x3C;/</span><span>div</span><span>></span></span>
<span><span>                    )</span><span>}</span></span>
<span><span>                &#x3C;/</span><span>div</span><span>></span></span>
<span><span>                :</span></span>
<span><span>                &#x3C;</span><span>div</span><span>></span><span>Loading</span><span>&#x3C;/</span><span>div</span><span>></span></span>
<span><span>        );</span></span>
<span><span>    }</span></span>
<span></span>
<span><span>}</span></span></code></pre><p>浏览器查看元素，可以发现这个元素的类名被自动格式化成这样​<code class="">&#x3C;div class="ArticleList_title__m5rub">React&#x3C;/div></code>​，如果在这个组件引入了其它样式文件，并且有同名的选择器，那么也不怕样式覆盖了。
</p><h2 id="user-content-原子化css" class="">原子化CSS<a class="" tabindex="-1" href="#原子化css">#</a></h2><p>简单来说原子CSS就是每个类名对于唯一的CSS规则，通过在HTML中组合类名，而不是修改CSS文件来改变样式。下面使用<a href="https://tailwindcss.com/">TailwindCSS</a>来感受下，首先按照<a href="https://tailwindcss.com/docs/guides/create-react-app">官网教程</a>配置好，然后可以删掉之前的文章列表组件的样式文件不用，修改组件：
</p><pre tabindex="0"><code><span><span>// 直接使用预先定义好的类名组合</span></span>
<span><span>&#x3C;</span><span>div</span><span> className</span><span>=</span><span>"</span><span>font-sans</span><span>"</span><span>></span></span>
<span><span>  {</span><span>articleList</span><span>.</span><span>map</span><span>(</span><span>item</span><span> =></span></span>
<span><span>    &#x3C;</span><span>div</span><span> key</span><span>=</span><span>{</span><span>item</span><span>.</span><span>id</span><span>}</span><span> className</span><span>=</span><span>"</span><span>py-3</span><span>"</span><span>></span></span>
<span><span>      &#x3C;</span><span>div</span><span> className</span><span>=</span><span>"</span><span>text-2xl font-semibold</span><span>"</span><span>></span><span>{</span><span>item</span><span>.</span><span>title</span><span>}</span><span>&#x3C;/</span><span>div</span><span>></span></span>
<span><span>      &#x3C;</span><span>div</span><span> className</span><span>=</span><span>"</span><span>space-x-2</span><span>"</span><span>></span></span>
<span><span>        &#x3C;</span><span>span</span><span>></span><span>创建时间：</span><span>&#x3C;</span><span>time</span><span> title</span><span>=</span><span>{</span><span>item</span><span>.</span><span>created</span><span>}</span><span>></span><span>{</span><span>dayjs</span><span>()</span><span>.</span><span>to</span><span>(</span><span>dayjs</span><span>(</span><span>item</span><span>.</span><span>created</span><span>))</span><span>}</span><span>&#x3C;/</span><span>time</span><span>>&#x3C;/</span><span>span</span><span>></span></span>
<span><span>        &#x3C;</span><span>span</span><span>></span><span>更新时间：</span><span>&#x3C;</span><span>time</span><span> title</span><span>=</span><span>{</span><span>item</span><span>.</span><span>updated</span><span>}</span><span>></span><span>{</span><span>dayjs</span><span>()</span><span>.</span><span>to</span><span>(</span><span>dayjs</span><span>(</span><span>item</span><span>.</span><span>updated</span><span>))</span><span>}</span><span>&#x3C;/</span><span>time</span><span>>&#x3C;/</span><span>span</span><span>></span></span>
<span><span>      &#x3C;/</span><span>div</span><span>></span></span>
<span><span>    &#x3C;/</span><span>div</span><span>></span></span>
<span><span>  )</span><span>}</span></span>
<span><span>&#x3C;/</span><span>div</span><span>></span></span></code></pre><p>此外，如果某个组合样式重复出现，那么也可以使用​<code class="">@apply</code>​方法：
</p><pre tabindex="0"><code><span><span>.</span><span>btn</span><span> {</span></span>
<span><span>    @</span><span>apply</span><span> py-</span><span>2 </span><span>px-</span><span>4 </span><span>font-semibold</span><span> rounded-lg</span><span> shadow-md</span><span>;</span></span>
<span><span>}</span></span>
<span><span>.</span><span>btn-green</span><span> {</span></span>
<span><span>    @</span><span>apply</span><span> text-white</span><span> bg-green-</span><span>500 </span><span>hover</span><span>:</span><span>bg-green-700</span><span>;</span></span>
<span><span>}</span></span></code></pre><p>可以尝试修改类名，通过tailwindcss去美化页面。
</p><p>其实React中是有像<a href="https://ant.design/">AntDesign</a>这样的美观易用的组件库存在的，但是样式不是这个系列的重点，同时库提供的组件已经预先实现了很多我们要做的需求，学习嘛，还是以手动实现为主。
</p><p><img alt="antd" src="https://i.loli.net/2021/04/11/YhHfun3PCRt6qMI.png"></p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>Django+React全栈开发：视图</title>
          <link>https://elliot00.com/posts/react-django-view-layer</link>
          <description>这篇文章介绍了如何使用Django REST framework来构建一个API。文章首先介绍了基本视图的写法，然后介绍了API view和类视图的写法，最后介绍了viewsets的写法。文章还介绍了如何使用官方提供的路由系统routers与viewsets搭配使用。</description>
          <pubDate>Sat, 10 Apr 2021 10:16:09 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <h2 id="user-content-基础视图" class="">基础视图<a class="" tabindex="-1" href="#基础视图">#</a></h2><p>现在来写一个简单的函数视图：
</p><pre tabindex="0"><code><span><span># article/views.py</span></span>
<span><span>from</span><span> article</span><span>.</span><span>serializers </span><span>import</span><span> ArticleSerializer</span></span>
<span><span>from</span><span> article</span><span>.</span><span>models </span><span>import</span><span> Article</span></span>
<span><span>from</span><span> django</span><span>.</span><span>http </span><span>import</span><span> HttpResponse</span><span>,</span><span> JsonResponse</span></span>
<span><span>from</span><span> django</span><span>.</span><span>views</span><span>.</span><span>decorators</span><span>.</span><span>csrf </span><span>import</span><span> csrf_exempt</span></span>
<span><span>from</span><span> rest_framework</span><span>.</span><span>parsers </span><span>import</span><span> JSONParser</span></span>
<span></span>
<span></span>
<span><span>@</span><span>csrf_exempt</span></span>
<span><span>def</span><span> article_list</span><span>(</span><span>request</span><span>)</span><span>:</span></span>
<span><span>    if</span><span> request</span><span>.</span><span>method </span><span>==</span><span> '</span><span>GET</span><span>'</span><span>:</span></span>
<span><span>        articles </span><span>=</span><span> Article</span><span>.</span><span>objects</span><span>.</span><span>all</span><span>()</span></span>
<span><span>        serializer </span><span>=</span><span> ArticleSerializer</span><span>(</span><span>articles</span><span>,</span><span> many</span><span>=</span><span>True</span><span>)</span></span>
<span><span>        return</span><span> JsonResponse</span><span>(</span><span>serializer.data</span><span>,</span><span> safe</span><span>=</span><span>False</span><span>)</span></span>
<span></span>
<span><span>    elif</span><span> request</span><span>.</span><span>method </span><span>==</span><span> '</span><span>POST</span><span>'</span><span>:</span></span>
<span><span>        data </span><span>=</span><span> JSONParser</span><span>().</span><span>parse</span><span>(</span><span>request</span><span>)</span></span>
<span><span>        serializer </span><span>=</span><span> ArticleSerializer</span><span>(</span><span>data</span><span>=</span><span>data</span><span>)</span></span>
<span><span>        if</span><span> serializer</span><span>.</span><span>is_valid</span><span>():</span></span>
<span><span>            serializer</span><span>.</span><span>save</span><span>()</span></span>
<span><span>            return</span><span> JsonResponse</span><span>(</span><span>serializer.data</span><span>,</span><span> status</span><span>=</span><span>201</span><span>)</span></span>
<span></span>
<span><span>        return</span><span> JsonResponse</span><span>(</span><span>serializer.errors</span><span>,</span><span> status</span><span>=</span><span>400</span><span>)</span></span></code></pre><p>以上视图函数将列出所有已存在的​<code class="">Article</code>​对象，并且接受​<code class="">POST</code>​请求来新增文章。这里使用了装饰器​<code class="">csrf_exempt</code>​，因为​<code class="">Django</code>​为了防止​<code class="">CSRF</code>​攻击要求​<code class="">POST</code>​请求需要带上​<code class="">CSRF token</code>​，但是我们这里要用工具来简单测试一下，所以加上装饰器用于忽略。
</p><p>除了​<code class="">list</code>​视图，我们再添加一个​<code class="">detail</code>​视图，一遍通过​<code class="">id</code>​来访问具体的文章：
</p><pre tabindex="0"><code><span><span>@</span><span>csrf_exempt</span></span>
<span><span>def</span><span> article_detail</span><span>(</span><span>request</span><span>,</span><span> pk</span><span>)</span><span>:</span></span>
<span><span>    try</span><span>:</span></span>
<span><span>        article </span><span>=</span><span> Article</span><span>.</span><span>objects</span><span>.</span><span>get</span><span>(</span><span>pk</span><span>=</span><span>pk</span><span>)</span></span>
<span><span>    except</span><span> Article</span><span>.</span><span>DoesNotExist</span><span>:</span></span>
<span><span>        return</span><span> HttpResponse</span><span>(</span><span>status</span><span>=</span><span>404</span><span>)</span></span>
<span></span>
<span><span>    if</span><span> request</span><span>.</span><span>method </span><span>==</span><span> '</span><span>GET</span><span>'</span><span>:</span></span>
<span><span>        serializer </span><span>=</span><span> ArticleSerializer</span><span>(</span><span>article</span><span>)</span></span>
<span><span>        return</span><span> JsonResponse</span><span>(</span><span>serializer.data</span><span>)</span></span>
<span></span>
<span><span>    elif</span><span> request</span><span>.</span><span>method </span><span>==</span><span> '</span><span>PUT</span><span>'</span><span>:</span></span>
<span><span>        data </span><span>=</span><span> JSONParser</span><span>().</span><span>parse</span><span>(</span><span>request</span><span>)</span></span>
<span><span>        serializer </span><span>=</span><span> ArticleSerializer</span><span>(</span><span>article</span><span>,</span><span> data</span><span>=</span><span>data</span><span>)</span></span>
<span><span>        if</span><span> serializer</span><span>.</span><span>is_valid</span><span>():</span></span>
<span><span>            serializer</span><span>.</span><span>save</span><span>()</span></span>
<span><span>            return</span><span> JsonResponse</span><span>(</span><span>serializer.data</span><span>)</span></span>
<span><span>        return</span><span> JsonResponse</span><span>(</span><span>serializer.errors</span><span>,</span><span> status</span><span>=</span><span>400</span><span>)</span></span>
<span></span>
<span><span>    elif</span><span> request</span><span>.</span><span>method </span><span>==</span><span> '</span><span>DELETE</span><span>'</span><span>:</span></span>
<span><span>        article</span><span>.</span><span>delete</span><span>()</span></span>
<span><span>        return</span><span> HttpResponse</span><span>(</span><span>status</span><span>=</span><span>204</span><span>)</span></span></code></pre><p>别忘了修改​<code class="">article/urls.py</code>​：
</p><pre tabindex="0"><code><span><span>urlpatterns </span><span>=</span><span> [</span></span>
<span><span>    path</span><span>(</span><span>'</span><span>articles/</span><span>'</span><span>,</span><span> views.article_list</span><span>),</span></span>
<span><span>    path</span><span>(</span><span>'</span><span>articles/&#x3C;int:pk>/</span><span>'</span><span>,</span><span> views.article_detail</span><span>),</span></span>
<span><span>]</span></span></code></pre><p>现在可以使用一些工具来测试一下我们的API了，如果你熟练使用​<code class="">Postman</code>​，那么下面的内容可以忽略了。
</p><p><code class="">Linux</code>​各个发行版基本都默认安装了​<code class="">curl</code>​，不过这里我推荐使用​<code class="">httpie</code>​这款工具，使用​<code class="">pip install httpie</code>​安装工具。
</p><pre tabindex="0"><code><span><span>$</span><span> http http://127.0.0.1:8000/articles/</span></span>
<span><span>HTTP/1.1 200 OK</span></span>
<span><span>......</span></span>
<span></span>
<span><span>[</span></span>
<span><span>    {</span></span>
<span><span>        "body": "React is good",</span></span>
<span><span>        "created": "2020-03-21T21:22:55.553124",</span></span>
<span><span>        "id": 3,</span></span>
<span><span>        "title": "React",</span></span>
<span><span>        "updated": "2020-03-21T21:22:55.553182"</span></span>
<span><span>    },</span></span>
<span><span>    ......</span></span>
<span><span>]</span></span></code></pre><p>获取第一篇文章：
</p><pre tabindex="0"><code><span><span>$</span><span> http http://127.0.0.1:8000/articles/1/</span></span>
<span><span>HTTP/1.1 200 OK</span></span>
<span><span>......</span></span>
<span></span>
<span><span>{</span></span>
<span><span>    "body": "React is good",</span></span>
<span><span>    "created": "2020-03-21T21:10:53.922033",</span></span>
<span><span>    "id": 1,</span></span>
<span><span>    "title": "React",</span></span>
<span><span>    "updated": "2020-03-21T21:10:53.922128"</span></span>
<span><span>}</span></span></code></pre><p><code class="">http</code>​命令后跟​<code class="">POST</code>​、​<code class="">PUT</code>​等，可执行相应操作，如
</p><pre tabindex="0"><code><span><span>$</span><span> http POST http://127.0.0.1:8000/articles/ </span><span>title</span><span>=</span><span>'</span><span>hello</span><span>'</span><span> body</span><span>=</span><span>'</span><span>一起来写 代码吧</span><span>'</span></span></code></pre><p>详情请查看官方文档。或者你更喜欢图形界面工具，可以去试试功能强大的​<code class="">Postman</code>​。
</p><h2 id="user-content-api-view" class="">API view<a class="" tabindex="-1" href="#api-view">#</a></h2><p>虽然现在我们的API已经能工作了，但是使用原生的视图有点麻烦，​<code class="">REST framework</code>​为我们提供了一些方便的封装：
</p><pre tabindex="0"><code><span><span># 改写article/views.py</span></span>
<span><span>from</span><span> rest_framework </span><span>import</span><span> status</span></span>
<span><span>from</span><span> rest_framework</span><span>.</span><span>decorators </span><span>import</span><span> api_view</span></span>
<span><span>from</span><span> rest_framework</span><span>.</span><span>response </span><span>import</span><span> Response</span></span>
<span><span>from</span><span> article</span><span>.</span><span>serializers </span><span>import</span><span> ArticleSerializer</span></span>
<span><span>from</span><span> article</span><span>.</span><span>models </span><span>import</span><span> Article</span></span>
<span></span>
<span></span>
<span><span>@</span><span>api_view</span><span>(</span><span>[</span><span>'</span><span>GET</span><span>'</span><span>, </span><span>'</span><span>POST</span><span>'</span><span>]</span><span>)</span></span>
<span><span>def</span><span> article_list</span><span>(</span><span>request</span><span>)</span><span>:</span></span>
<span><span>    if</span><span> request</span><span>.</span><span>method </span><span>==</span><span> '</span><span>GET</span><span>'</span><span>:</span></span>
<span><span>        articles </span><span>=</span><span> Article</span><span>.</span><span>objects</span><span>.</span><span>all</span><span>()</span></span>
<span><span>        serializer </span><span>=</span><span> ArticleSerializer</span><span>(</span><span>articles</span><span>,</span><span> many</span><span>=</span><span>True</span><span>)</span></span>
<span><span>        return</span><span> Response</span><span>(</span><span>serializer.data</span><span>)</span></span>
<span></span>
<span><span>    elif</span><span> request</span><span>.</span><span>method </span><span>==</span><span> '</span><span>POST</span><span>'</span><span>:</span></span>
<span><span>        serializer </span><span>=</span><span> ArticleSerializer</span><span>(</span><span>data</span><span>=</span><span>request.data</span><span>)</span></span>
<span><span>        if</span><span> serializer</span><span>.</span><span>is_valid</span><span>():</span></span>
<span><span>            serializer</span><span>.</span><span>save</span><span>()</span></span>
<span><span>            return</span><span> Response</span><span>(</span><span>serializer.data</span><span>,</span><span> status</span><span>=</span><span>status.HTTP_201_CREATED</span><span>)</span></span>
<span></span>
<span><span>        return</span><span> Response</span><span>(</span><span>serializer.errors</span><span>,</span><span> status</span><span>=</span><span>status.HTTP_400_BAD_REQUEST</span><span>)</span></span>
<span></span>
<span></span>
<span><span>@</span><span>api_view</span><span>(</span><span>[</span><span>'</span><span>GET</span><span>'</span><span>, </span><span>'</span><span>PUT</span><span>'</span><span>, </span><span>'</span><span>DELETE</span><span>'</span><span>]</span><span>)</span></span>
<span><span>def</span><span> article_detail</span><span>(</span><span>request</span><span>,</span><span> pk</span><span>)</span><span>:</span></span>
<span><span>    try</span><span>:</span></span>
<span><span>        article </span><span>=</span><span> Article</span><span>.</span><span>objects</span><span>.</span><span>get</span><span>(</span><span>pk</span><span>=</span><span>pk</span><span>)</span></span>
<span><span>    except</span><span> Article</span><span>.</span><span>DoesNotExist</span><span>:</span></span>
<span><span>        return</span><span> Response</span><span>(</span><span>status</span><span>=</span><span>status.HTTP_404_NOT_FOUND</span><span>)</span></span>
<span></span>
<span><span>    if</span><span> request</span><span>.</span><span>method </span><span>==</span><span> '</span><span>GET</span><span>'</span><span>:</span></span>
<span><span>        serializer </span><span>=</span><span> ArticleSerializer</span><span>(</span><span>article</span><span>)</span></span>
<span><span>        return</span><span> Response</span><span>(</span><span>serializer.data</span><span>)</span></span>
<span></span>
<span><span>    elif</span><span> request</span><span>.</span><span>method </span><span>==</span><span> '</span><span>PUT</span><span>'</span><span>:</span></span>
<span><span>        serializer </span><span>=</span><span> ArticleSerializer</span><span>(</span><span>article</span><span>,</span><span> data</span><span>=</span><span>request.data</span><span>)</span></span>
<span><span>        if</span><span> serializer</span><span>.</span><span>is_valid</span><span>():</span></span>
<span><span>            serializer</span><span>.</span><span>save</span><span>()</span></span>
<span><span>            return</span><span> Response</span><span>(</span><span>serializer.data</span><span>)</span></span>
<span><span>        return</span><span> Response</span><span>(</span><span>serializer.errors</span><span>,</span><span> status</span><span>=</span><span>status.HTTP_400_BAD_REQUEST</span><span>)</span></span>
<span></span>
<span><span>    elif</span><span> request</span><span>.</span><span>method </span><span>==</span><span> '</span><span>DELETE</span><span>'</span><span>:</span></span>
<span><span>        article</span><span>.</span><span>delete</span><span>()</span></span>
<span><span>        return</span><span> Response</span><span>(</span><span>status</span><span>=</span><span>status.HTTP_204_NO_CONTENT</span><span>)</span></span></code></pre><p>现在代码量相比之前减少了一些，​<code class="">REST framework</code>​为我们封装了​<code class="">Request</code>​与​<code class="">Response</code>​类，详情请浏览<a href="https://www.django-rest-framework.org">官网</a>。
</p><h2 id="user-content-类视图" class="">类视图<a class="" tabindex="-1" href="#类视图">#</a></h2><p>聪明的你想必已经想到了，​<code class="">REST framework</code>​也如​<code class="">Django</code>​一样为我们提供了类视图：
</p><pre tabindex="0"><code><span><span># 导入相关类</span></span>
<span><span>from</span><span> rest_framework </span><span>import</span><span> status</span></span>
<span><span>from</span><span> rest_framework</span><span>.</span><span>views </span><span>import</span><span> APIView</span></span>
<span><span>from</span><span> rest_framework</span><span>.</span><span>response </span><span>import</span><span> Response</span></span>
<span><span>from</span><span> django</span><span>.</span><span>http </span><span>import</span><span> Http404</span></span>
<span><span>from</span><span> article</span><span>.</span><span>serializers </span><span>import</span><span> ArticleSerializer</span></span>
<span><span>from</span><span> article</span><span>.</span><span>models </span><span>import</span><span> Article</span></span></code></pre><p>类视图：
</p><pre tabindex="0"><code><span><span>class</span><span> ArticleList</span><span>(</span><span>APIView</span><span>):</span></span>
<span><span>    def</span><span> get</span><span>(</span><span>self</span><span>,</span><span> request</span><span>,</span><span> format</span><span>=</span><span>None</span><span>)</span><span>:</span></span>
<span><span>        articles </span><span>=</span><span> Article</span><span>.</span><span>objects</span><span>.</span><span>all</span><span>()</span></span>
<span><span>        serializer </span><span>=</span><span> ArticleSerializer</span><span>(</span><span>articles</span><span>,</span><span> many</span><span>=</span><span>True</span><span>)</span></span>
<span><span>        return</span><span> Response</span><span>(</span><span>serializer.data</span><span>)</span></span>
<span></span>
<span><span>    def</span><span> post</span><span>(</span><span>self</span><span>,</span><span> request</span><span>,</span><span> format</span><span>=</span><span>None</span><span>)</span><span>:</span></span>
<span><span>        serializer </span><span>=</span><span> ArticleSerializer</span><span>(</span><span>data</span><span>=</span><span>request.data</span><span>)</span></span>
<span><span>        if</span><span> serializer</span><span>.</span><span>is_valid</span><span>():</span></span>
<span><span>            serializer</span><span>.</span><span>save</span><span>()</span></span>
<span><span>            return</span><span> Response</span><span>(</span><span>serializer.data</span><span>,</span><span> status</span><span>=</span><span>status.HTTP_201_CREATED</span><span>)</span></span>
<span><span>        return</span><span> Response</span><span>(</span><span>serializer.errors</span><span>,</span><span> status</span><span>=</span><span>status.HTTP_400_BAD_REQUEST</span><span>)</span></span></code></pre><p>详情视图：
</p><pre tabindex="0"><code><span><span>class</span><span> ArticleDetail</span><span>(</span><span>APIView</span><span>):</span></span>
<span><span>    def</span><span> get_object</span><span>(</span><span>self</span><span>,</span><span> pk</span><span>)</span><span>:</span></span>
<span><span>        try</span><span>:</span></span>
<span><span>            return</span><span> Article</span><span>.</span><span>objects</span><span>.</span><span>get</span><span>(</span><span>pk</span><span>=</span><span>pk</span><span>)</span></span>
<span><span>        except</span><span> Article</span><span>.</span><span>DoesNotExist</span><span>:</span></span>
<span><span>            raise</span><span> Http404</span></span>
<span></span>
<span><span>    def</span><span> get</span><span>(</span><span>self</span><span>,</span><span> request</span><span>,</span><span> pk</span><span>,</span><span> format</span><span>=</span><span>None</span><span>)</span><span>:</span></span>
<span><span>        snippet </span><span>=</span><span> self</span><span>.</span><span>get_object</span><span>(</span><span>pk</span><span>)</span></span>
<span><span>        serializer </span><span>=</span><span> ArticleSerializer</span><span>(</span><span>snippet</span><span>)</span></span>
<span><span>        return</span><span> Response</span><span>(</span><span>serializer.data</span><span>)</span></span>
<span></span>
<span><span>    def</span><span> put</span><span>(</span><span>self</span><span>,</span><span> request</span><span>,</span><span> pk</span><span>,</span><span> format</span><span>=</span><span>None</span><span>)</span><span>:</span></span>
<span><span>        snippet </span><span>=</span><span> self</span><span>.</span><span>get_object</span><span>(</span><span>pk</span><span>)</span></span>
<span><span>        serializer </span><span>=</span><span> ArticleSerializer</span><span>(</span><span>snippet</span><span>,</span><span> data</span><span>=</span><span>request.data</span><span>)</span></span>
<span><span>        if</span><span> serializer</span><span>.</span><span>is_valid</span><span>():</span></span>
<span><span>            serializer</span><span>.</span><span>save</span><span>()</span></span>
<span><span>            return</span><span> Response</span><span>(</span><span>serializer.data</span><span>)</span></span>
<span><span>        return</span><span> Response</span><span>(</span><span>serializer.errors</span><span>,</span><span> status</span><span>=</span><span>status.HTTP_400_BAD_REQUEST</span><span>)</span></span>
<span></span>
<span><span>    def</span><span> delete</span><span>(</span><span>self</span><span>,</span><span> request</span><span>,</span><span> pk</span><span>,</span><span> format</span><span>=</span><span>None</span><span>)</span><span>:</span></span>
<span><span>        snippet </span><span>=</span><span> self</span><span>.</span><span>get_object</span><span>(</span><span>pk</span><span>)</span></span>
<span><span>        snippet</span><span>.</span><span>delete</span><span>()</span></span>
<span><span>        return</span><span> Response</span><span>(</span><span>status</span><span>=</span><span>status.HTTP_204_NO_CONTENT</span><span>)</span></span></code></pre><p>通过实现诸如​<code class="">get</code>​，​<code class="">put</code>​等方法，实现对不同​<code class="">HTTP</code>​请求的处理。对于类视图，我们需要修改一下​<code class="">article/urls.py</code>​：
</p><pre tabindex="0"><code><span><span>urlpatterns </span><span>=</span><span> [</span></span>
<span><span>    path</span><span>(</span><span>'</span><span>articles/</span><span>'</span><span>,</span><span> views.ArticleList.</span><span>as_view</span><span>()),</span></span>
<span><span>    path</span><span>(</span><span>'</span><span>articles/&#x3C;int:pk>/</span><span>'</span><span>,</span><span> views.ArticleDetail.</span><span>as_view</span><span>()),</span></span>
<span><span>]</span></span></code></pre><p>现在使用工具测试，API又能正常工作啦。
</p><h2 id="user-content-viewsets" class="">viewsets<a class="" tabindex="-1" href="#viewsets">#</a></h2><p><code class="">REST framework</code>​为我们提供了一个更强大的视图集合，这里包括了一些常用的视图，直接上代码：
</p><pre tabindex="0"><code><span><span>from</span><span> rest_framework </span><span>import</span><span> viewsets</span></span>
<span><span>from</span><span> article</span><span>.</span><span>serializers </span><span>import</span><span> ArticleSerializer</span></span>
<span><span>from</span><span> article</span><span>.</span><span>models </span><span>import</span><span> Article</span></span>
<span></span>
<span></span>
<span><span>class</span><span> ArticleViewSet</span><span>(</span><span>viewsets</span><span>.</span><span>ModelViewSet</span><span>):</span></span>
<span><span>    queryset </span><span>=</span><span> Article</span><span>.</span><span>objects</span><span>.</span><span>all</span><span>()</span></span>
<span><span>    serializer_class </span><span>=</span><span> ArticleSerializer</span></span></code></pre><p>我们写的​<code class="">ArticleViewSet</code>​类只有两行代码，但是却涵盖了之前所有的功能。​<code class="">viewsets</code>​可以与官方提供的路由系统​<code class="">routers</code>​搭配使用，修改​<code class="">article/urls.py</code>​：
</p><pre tabindex="0"><code><span><span>from</span><span> django</span><span>.</span><span>urls </span><span>import</span><span> path</span><span>,</span><span> include</span></span>
<span><span>from</span><span> rest_framework</span><span>.</span><span>routers </span><span>import</span><span> DefaultRouter</span></span>
<span><span>from</span><span> article </span><span>import</span><span> views</span></span>
<span></span>
<span></span>
<span><span>router </span><span>=</span><span> DefaultRouter</span><span>()</span></span>
<span><span>router</span><span>.</span><span>register</span><span>(</span><span>r</span><span>'</span><span>articles</span><span>'</span><span>,</span><span> views.ArticleViewSet</span><span>)</span></span>
<span></span>
<span><span>urlpatterns </span><span>=</span><span> [</span></span>
<span><span>    path</span><span>(</span><span>''</span><span>,</span><span> include</span><span>(</span><span>router.urls</span><span>)),</span></span>
<span><span>]</span></span></code></pre><p><strong>注意</strong>​：确保你的​<strong>项目urls.py</strong>​里包含了​<code class="">article</code>​的urls，习惯上把所有的路由放到​<code class="">api/</code>​下。
</p><pre tabindex="0"><code><span><span># 项目级urls</span></span>
<span><span>urlpatterns </span><span>=</span><span> [</span></span>
<span><span>    path</span><span>(</span><span>'</span><span>api/</span><span>'</span><span>,</span><span> include</span><span>(</span><span>'</span><span>article.urls</span><span>'</span><span>)),</span></span>
<span><span>]</span></span></code></pre><p>我们准备做前后端分离开发，那么其实这里并不需要再去写模板了，之前的templates文件夹可以删除，但是为了方便调试，​<code class="">REST framework</code>​提供了默认的一套模板，所以如果你打开浏览器访问默认的地址​<code class="">127.0.0.1:8000/api</code>​，是可以看到一个API Root的页面的，并且用浏览器去访问api端点会看到拥有样式的页面而不是简单的JSON，框架会根据请求来判断返回形式，非常人性化。
</p><p><strong>这里只是简略介绍一下几种编写视图的方法，要深入了解还是要去看官方文档，视图写法还是比较灵活的，可以根据需要选择不同的写法。</strong></p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>Django+React全栈开发：前置知识</title>
          <link>https://elliot00.com/posts/react-django-basis</link>
          <description>这篇文章介绍了在互联网时代中常见的程序类型，如C/S和B/S架构，以及Web服务器和Web框架的概念。还介绍了MVC模式，Django的MTV模式，以及前端与后端的概念。文章强调了学习时多造轮子的重要性，但在实际生产生活中则尽量应用成熟的已有的应用。</description>
          <pubDate>Sat, 10 Apr 2021 09:12:03 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <h2 id="user-content-前言" class="">前言<a class="" tabindex="-1" href="#前言">#</a></h2><p>这篇文章来简要讲一下在后续开发工作中可能碰到的一些概念，我会尽量将这些概念讲得易于理解，并列出一些我认为比较好的学习资源，以尽量避免读者在以后碰到这些概念时茫然无措。
</p><p>当然我也并不是什么权威人士，本文的内容都是建立在目前我的认知基础上的，难以避免会有谬误之处，如果需要深入了解，还是要多多查资料，并在实践中消化。
</p><h2 id="user-content-正文" class="">正文<a class="" tabindex="-1" href="#正文">#</a></h2><h3 id="user-content-互联网时代的程序">互联网时代的程序<a class="" tabindex="-1" href="#互联网时代的程序">#</a></h3><p>在互联网时代，如果不考虑游戏，好像人们日常生活中完全不需要网络的软件很少。相信大家至少都用​<code class="">Python</code>​写过一些简单的小脚本，这些程序仅仅运行在我们自己的电脑上，不需要和其他人交流，甚至只是做些简单的计算便运行结束。当然人人都有社交的需求，计算机也要谈恋爱（划掉，目前我们接触的软件基本都是网络程序了。当然，虽然都是​<strong>通信</strong>​，但是​<strong>互联网并不是像人们面对面聊天一样的直接</strong>​，要深入了解建议多搜索​<strong>计算机网络</strong>​相关内容。
</p><h4 id="user-content-cs">C/S<a class="" tabindex="-1" href="#cs">#</a></h4><p><strong>Client-Server</strong>​：客户端-服务端架构。这个相信大家都很熟悉，例如常用的​<code class="">QQ</code>​、​<code class="">微信</code>​，使用这类程序我们要在电脑或手机上安装它们的​<strong>客户端</strong>​，这个客户端程序会和它们各自的​<strong>服务端程序</strong>​通信，如果我们要用QQ发个消息给朋友，那么通常是这个消息经过我们的客户端发送到腾讯的服务端，再由服务端发送给朋友的客户端。当然这个过程并不是像嘴上说的发送接收那么简单啦。要注意这个客户端可不一定是只有​<code class="">GUI</code>​，也就是有​<strong>图形用户界面</strong>​的程序才是客户端，很多客户端程序可能你并不能直接看到。
</p><h4 id="user-content-bs">B/S<a class="" tabindex="-1" href="#bs">#</a></h4><p><strong>Browser-Server</strong>​：浏览器端-服务端架构。这个大家想必更熟悉，平时浏览的各种网站，都是这种模式。有人认为网站不能算是计算机程序，对此我不能表示认同。
</p><h4 id="user-content-对比">对比<a class="" tabindex="-1" href="#对比">#</a></h4><p><strong>接下来说说我对这两种模式的理解</strong>​。不知道为什么有些人认为​<code class="">C/S</code>​与​<code class="">B/S</code>​是并列关系的两种不同东西。​<strong>我认为事实上=B/S=属于=C/S=</strong>​。试想，我们用浏览器去浏览一个网站，事实上这个浏览器不就是​<strong>客户端程序</strong>​吗？只不过无数的网站服务端都共享浏览器这么一个客户端而已。
</p><p>我认为所谓​<code class="">B/S</code>​不过是一种特殊的​<code class="">C/S</code>​罢了，那么这么做有什么好处呢？想一想，大家是否有经常要更新手机上的软件的需求，例如​<code class="">QQ</code>​，往往腾讯做出来了新功能，那么就要用户更新一下客户端，而通过浏览器访问的网站，​<strong>只要浏览器支持它的前端功能</strong>​，那么往往用户什么都不用做，就能体验到新功能了。
</p><p>随着互联网的发展，网速越来越快，延迟越来越低，有人甚至提出将应用都放在云端，​<strong>例如让手机变成一个大型浏览器，手机上只负责展示内容，这样对手机配置的要求也转移到对网速的需求上了</strong>​。当然现在的网络速度还不够快，我曾经玩过云游戏，可以直接在配置一般的电脑上玩大型游戏，不过现在体验还并不是很好。​<strong>微信的小程序功能，也体现了这种思想，很多简单的应用，并不需要在我们的手机上再安装一个单独的app</strong>​。
</p><h3 id="user-content-web-server">Web Server<a class="" tabindex="-1" href="#web-server">#</a></h3><h4 id="user-content-通信协议">通信协议<a class="" tabindex="-1" href="#通信协议">#</a></h4><p><strong>通信协议是一个很重要的东西，它规定了计算机之间应该怎样通信，如果没有协议，那么就是鸡同鸭讲，互联网也无从谈起了</strong>​。
</p><p>例如人与人之间交流，​<strong>我们知道我们的语言的语法规则，知道某个词在句子中的意思，这是使用同样语言的人都知道的规定</strong>​，我们可以​<strong>将大脑中的信息，编码成声音信号</strong>​发出去，而听话的人，则也懂得​<strong>将这个声音信号解码成大脑能理解的信息</strong>​。而一只鹦鹉，虽然它也能​<strong>接收到我们发出的声音信号</strong>​，但是它​<strong>不懂我们的语法规则</strong>​，永远只能学舌，却不能和我们交流。
</p><p>在​<code class="">B/S</code>​架构的程序中，基本上是基于​<code class="">HTTP</code>​这个协议来通信。建议​<code class="">TCP/IP</code>​和​<code class="">HTTP</code>​协议要重点去了解。
</p><h4 id="user-content-网络服务器">网络服务器<a class="" tabindex="-1" href="#网络服务器">#</a></h4><p>在这里​<strong>服务器不单单指一类计算机硬件</strong>​，事实上网络服务器是软硬件一体的，​<strong>只有一台计算机没有软件你什么也做不了，只有软件没有运行环境也是这样</strong>​。
</p><h4 id="user-content-请求与响应">请求与响应<a class="" tabindex="-1" href="#请求与响应">#</a></h4><p>一个​<strong>HTTP服务器程序</strong>​主要需要​<strong>接收来自客户端的请求</strong>​，​<strong>以及根据这个请求做出相应的响应</strong>​。
</p><p><img alt="Basic representation of a client/server connection through HTTP" src="https://developer.mozilla.org/en-US/docs/Learn/Common_questions/What_is_a_web_server/web-server.svg"></p><p>例如你去访问​<code class="">a.com</code>​这个网站，服务器将根据你的​<code class="">GET</code>​请求，返回一个响应，这其中包含了这个网站首页的​<code class="">HTML</code>​文件，浏览器就会把这个页面呈现给你了。还有非常常见的一个响应是当你*请求一个并不存在的资源时*​，你会得到一个​<strong>404响应</strong>​。
</p><h4 id="user-content-静态服务器与动态服务器">静态服务器与动态服务器<a class="" tabindex="-1" href="#静态服务器与动态服务器">#</a></h4><p><strong>静态服务器并不是说响应的内容渲染出来是静态页面</strong>​，而是将服务器上的资源​<strong>原汁原味</strong>​的传递给你。
</p><p><strong>动态服务器则不同，动态服务器通常会包含数据库，在经由静态服务器将资源呈现给你之前，还会动态地根据数据库内容对资源做更新</strong>​。
</p><h4 id="user-content-web框架">Web框架<a class="" tabindex="-1" href="#web框架">#</a></h4><p>这个很好理解，框架​<strong>就是一个帮你省事的工具</strong>​，从头开发一套系统非常麻烦，前人种树，后人乘凉，使用Web框架帮助你快速开发，节省时间。​<code class="">Django</code>​就是一个非常不错的Web框架。
</p><h3 id="user-content-架构模式">架构模式<a class="" tabindex="-1" href="#架构模式">#</a></h3><h4 id="user-content-mvc模式">MVC模式<a class="" tabindex="-1" href="#mvc模式">#</a></h4><p>MVC是​<code class="">Model</code>​、​<code class="">View</code>​、​<code class="">Controller</code>​的缩写。分别代表​<strong>模型</strong>​、​<strong>视图</strong>​、​<strong>控制</strong>​。
</p><ul><li>模型层：这一层表示核心的数据以及对数据的处理方法，处在最底层。
</li><li>视图层：这一层是需要展示给用户看的内容。
</li><li>控制层：这一层​​<strong>处在模型与视图的中间</strong>​​，连接模型与视图。
</li></ul><p>例如我们在博客上写一篇文章，看到的网页是视图层的内容，我们​<strong>写好文章，点击发布按钮</strong>​，这时控制层​<strong>根据点击发布按钮这个操作，决定把用户输入的数据，拿去要求模型层存储数据</strong>​，我们点击一篇文章的链接，控制层则​<strong>根据这个请求从模型层取出这个文章的数据，转给视图层处理，让我们看到</strong>​。
</p><p>此外还有​<code class="">MVP</code>​、​<code class="">MVVM</code>​模式，可以查查资料去了解，不过这种东西我觉得不实际运用很难真正理解。
</p><h4 id="user-content-django的mtv模式">Django的MTV模式<a class="" tabindex="-1" href="#django的mtv模式">#</a></h4><p>在上一篇文章中，我们已经初步涉及了​<code class="">Django</code>​的MTV模式，事实上这是对​<code class="">Django</code>​对​<code class="">MVC</code>​模式的一种实现，看过很多文章任务Django中的​<code class="">View</code>​就是​<code class="">MVC</code>​的控制层，而​<code class="">Template</code>​模板层则是视图层，我觉得这是​<strong>不对</strong>​的。其实Django的​<strong>视图层</strong>​与​<strong>模板层</strong>​都是决定如何展示给用户数据的部分，可以说都是​<code class="">MVC</code>​中的​<strong>视图层</strong>​，​<strong>Django官方</strong>​表示事实上整个​<code class="">Django</code>​自身就是控制层。
</p><p>回到我之前举的写文章的例子，我认为，关于​<code class="">Django</code>​的​<code class="">MTV</code>​的到底分别代表​<code class="">MVC</code>​中的什么的分歧点在于，​<strong>视图层到底决不决定如何展现内容</strong>​。起码​<code class="">Django</code>​认为应该这样。而控制层实质上由​<code class="">Django</code>​的​<code class="">URL分发器</code>​来实现的，它通过不同的URL请求，去决定调用不同的​<code class="">View</code>​，​<code class="">View</code>​再去操控不同的​<code class="">Template</code>​。
</p><h3 id="user-content-前端与后端">前端与后端<a class="" tabindex="-1" href="#前端与后端">#</a></h3><h4 id="user-content-html-css-javascript">HTML CSS JavaScript<a class="" tabindex="-1" href="#html-css-javascript">#</a></h4><p>这三样东西基本上是一个网站必不可少的。
</p><p>前面我提到​<code class="">B/S</code>​只是一种特殊的​<code class="">C/S</code>​，那么浏览器这个客户端根据什么来让各种各样的网页看上去不一样，功能也不一样呢？
</p><ul><li><strong>HTML(HyperText Markup Language)</strong>​：超文本标记语言。注意​<strong>标记</strong>​这两个字，例如一个​<code class="">h1</code>​的标签，浏览器就知道，这里是一个一号大小的标题。
</li><li><strong>CSS(Cascading Style Sheets)</strong>​：层叠样式表。仅仅一个​<code class="">h1</code>​在浏览器中看上去可能不好看，通过CSS则可以决定这个​<code class="">h1</code>​的样式，例如变成红色文字。
</li><li><strong>JavaScript</strong>​：这是一个脚本语言，它使得网页具有交互能力，例如将鼠标移动到​<code class="">h1</code>​标题上会弹出一个下拉选择框。这也是​<strong>之前为什么说只使用静态服务器的网站不代表网页是静态不能动的</strong>​。
</li></ul><p>建议去<a href="https://developer.mozilla.org/zh-CN/">MDN</a>系统学习，不过由于某些历史原因，JavaScript学习起来有点难受（老实说刚刚接触这门语言的时候我觉得这就是坨屎，逃），要加油哦。
</p><h4 id="user-content-前后端分离">前后端分离<a class="" tabindex="-1" href="#前后端分离">#</a></h4><p>之前在Django中使用了模板语言，传统开发模式下，页面处理逻辑，页面的渲染实际上部分是由后端负责的，或者说前后端的分界线比较模糊的，前后端分离的开发模式，分离了关注点，两端可以分别开发调试，大大提升了开发效率，为了做到这一点，我们就不能再使用之前模板的方式。
</p><h4 id="user-content-restful-api">RESTful API<a class="" tabindex="-1" href="#restful-api">#</a></h4><p><strong>REST(Representational State Transfer)</strong>​：​<strong>表现层状态转换</strong>​。这个名字其实很直观，例如我们博客应用中的文章，​<strong>可能实际的资源是一串字符串</strong>​，但是当​<strong>呈现到表现层时</strong>​，我们可以将它变成​<code class="">txt</code>​文件，​<code class="">HTML</code>​或者​<code class="">JSON</code>​。也就是说，​<strong>服务端的资源</strong>​，在客户端拿到前，​<strong>发生了状态的转换</strong>​。因为​<strong>HTTP协议是无状态协议</strong>​，客户端通过​<strong>不同的HTTP请求</strong>​，让服务端将原始资源转换成不同状态响应回来。
</p><p><strong>RESTful API</strong>​有个重要的概念是​<strong>URI(Uniform Resource Identifier)统一资源标识符</strong>​，这应该是​<strong>名词而不是动词</strong>​。例如，博客中的文章，访问它的​<code class="">URI</code>​可以是​<code class="">article</code>​，如​<code class="">www.api.blog.com/article/</code>​，而不应该是​<code class="">get-article</code>​，​<code class="">create-article</code>​，​<code class="">update-article</code>​，​<code class="">delete-article</code>​，要​<strong>实现获取、创建、修改、删除操作</strong>​，只需要客户端发送​<strong>对应的HTTP请求</strong>​就行（分别为​<code class="">GET</code>​,​<code class="">POST</code>​,​<code class="">PUT</code>​,​<code class="">DELETE</code>​）。统一资源标识符只应表示资源本身。
</p><p><code class="">Django</code>​利用​<code class="">Django REST framework</code>​这个库可以实现这一点，后续将重点介绍这个库。前后端分离开发也可以基于此实现，前端与后端约定好接口，通过JSON做数据交换。
</p><p><a href="https://www.zhihu.com/question/28557115">推荐文章</a></p><h2 id="user-content-开始造轮子吧" class="">开始造轮子吧<a class="" tabindex="-1" href="#开始造轮子吧">#</a></h2><p><strong>其实个人做一个博客根本不需要前后端分离开发模式，甚至根本都不需要写代码，完全有直接可用的应用</strong>​。
</p><p>这里还是要表达一下我的主要想法：​<strong>学习时多造轮子，工程中多用轮子</strong>​。也就是学习的时候能怎么折腾怎么折腾，实际生产生活的时候则尽量应用成熟的已有的应用。
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>Django+React全栈开发：序列化器</title>
          <link>https://elliot00.com/posts/react-django-serializer</link>
          <description>文章介绍了 REST framework 的使用方法，首先是安装 REST framework 并将其添加到 Django 项目的设置中。然后介绍了如何扩展模型，包括创建 Article 模型以及在其中定义字段。之后，文章介绍了如何创建序列化器来对模型进行序列化和反序列化，包括手动创建序列化器和使用 ModelSerializer。最后，文章介绍了如何在交互模式下使用序列化器。</description>
          <pubDate>Sat, 10 Apr 2021 08:58:38 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <h2 id="user-content-rest-framework" class="">REST framework<a class="" tabindex="-1" href="#rest-framework">#</a></h2><p>我们继续之前的内容，让我们先进入后端项目文件夹​<code class="">backend</code>​，激活虚拟环境，并安装​<code class="">REST framework</code>​：
</p><pre tabindex="0"><code><span><span>$</span><span> source venv/bin/activate</span></span>
<span><span>// 激活后命令提示符前面会出现(venv)</span></span>
<span><span>(venv) $ pip install djangorestframework</span></span></code></pre><blockquote><p>Django REST framework is a powerful and flexible toolkit for building Web APIs.
</p></blockquote><p><strong>官方介绍：Django REST framework是一个用于构建WEB API的强大而灵活的工具。</strong></p><p>还记得在上一篇文章中提到的​<code class="">RESTful API</code>​吗？这次我们就来试试使用​<code class="">REST framework</code>​这个库来改造我们之前写的程序。
</p><p>安装完成后，记得到项目文件夹​<code class="">backend/backend/settings.py</code>​文件中注册：
</p><pre tabindex="0"><code><span><span>INSTALLED_APPS</span><span> =</span><span> [</span></span>
<span><span>    '</span><span>django.contrib.admin</span><span>'</span><span>,</span></span>
<span><span>    '</span><span>django.contrib.auth</span><span>'</span><span>,</span></span>
<span><span>    '</span><span>django.contrib.contenttypes</span><span>'</span><span>,</span></span>
<span><span>    '</span><span>django.contrib.sessions</span><span>'</span><span>,</span></span>
<span><span>    '</span><span>django.contrib.messages</span><span>'</span><span>,</span></span>
<span><span>    '</span><span>django.contrib.staticfiles</span><span>'</span><span>,</span></span>
<span><span>    '</span><span>rest_framework</span><span>'</span><span>,</span><span> # 加上这个</span></span>
<span><span>    '</span><span>article.apps.ArticleConfig</span><span>'</span><span>,</span></span>
<span><span>]</span></span></code></pre><h2 id="user-content-扩展模型" class="">扩展模型<a class="" tabindex="-1" href="#扩展模型">#</a></h2><p>打开我们的​<code class="">backend/article/models.py</code>​文件，原本的内容有点少，我们首先把这个模型扩展一下：
</p><pre tabindex="0"><code><span><span>from</span><span> django</span><span>.</span><span>db </span><span>import</span><span> models</span></span>
<span></span>
<span></span>
<span><span>class</span><span> Article</span><span>(</span><span>models</span><span>.</span><span>Model</span><span>):</span></span>
<span><span>    title </span><span>=</span><span> models</span><span>.</span><span>CharField</span><span>(</span><span>max_length</span><span>=</span><span>50</span><span>)</span></span>
<span><span>    body </span><span>=</span><span> models</span><span>.</span><span>TextField</span><span>()</span></span>
<span><span>    created </span><span>=</span><span> models</span><span>.</span><span>DateTimeField</span><span>(</span><span>auto_now_add</span><span>=</span><span>True</span><span>)</span></span>
<span><span>    updated </span><span>=</span><span> models</span><span>.</span><span>DateTimeField</span><span>(</span><span>auto_now</span><span>=</span><span>True</span><span>)</span></span>
<span></span>
<span><span>    class</span><span> Meta</span><span>:</span></span>
<span><span>        ordering </span><span>=</span><span> (</span><span>'</span><span>-created</span><span>'</span><span>,</span><span>)</span></span></code></pre><p>注意我们定义的继承了​<code class="">models.Model</code>​的类​<code class="">Article</code>​，与数据库中的​<strong>表</strong>​有对应关系，可以看到这个类的不同属性分别是不同的对象示例，它们对应了数据库中的​<strong>表</strong>​的不同​<strong>列</strong>​，并且代表了不同的数据类型。
</p><p><code class="">created</code>​与​<code class="">updated</code>​通过将两个不同的参数设置为​<code class="">True</code>​实现了​<strong>自动保存创建时间与最后修改时间</strong>​的功能。
</p><p>这里在元类里定义了以创建时间降序排序，注意​<code class="">ordering</code>​应该是一个元组，所以别忘了逗号​<code class="">,</code></p><p>好啦，还记得每次更改模型后要做什么？
</p><pre tabindex="0"><code><span><span>(venv)  $ python manage.py makemigrations article </span></span>
<span><span>Migrations for 'article':</span></span>
<span><span>  article/migrations/0001_initial.py</span></span>
<span><span>    - Create model Article</span></span>
<span><span>(venv)  $ python manage.py migrate</span></span>
<span><span>Operations to perform:</span></span>
<span><span>  Apply all migrations: admin, article, auth, authtoken, contenttypes, sessions</span></span>
<span><span>Running migrations:</span></span>
<span><span>  Applying contenttypes.0001_initial... OK</span></span>
<span><span>  ……</span></span>
<span><span>  Applying sessions.0001_initial... OK</span></span></code></pre><p>现在迁移完成了，但是现在不急着去写视图和模板，我们要开始制作自己的RESTful API。
</p><h2 id="user-content-序列化" class="">序列化<a class="" tabindex="-1" href="#序列化">#</a></h2><p>还记得之前提过，​<code class="">REST</code>​的意思是​<code class="">表现层状态转换</code>​，我们​<strong>需要有一个工具来对模型进行序列化与反序列化</strong>​，通俗的讲法序列化就是将语言中的​<strong>对象转化为可以存储或传输</strong>​的形式，反序列化就是反过来的过程。
</p><p>在前后端分离模式的开发中，由于前后端语言往往是不同的，例如后端​<code class="">Java</code>​，前端​<code class="">JavaScript</code>​，或者有移动端的​<code class="">Kotlin</code>​，往往需要一种较为通用的格式，​<code class="">JSON</code>​就是一个常见的选择。
</p><p>好了，开始行动吧，在​<code class="">article</code>​文件夹中新建一个​<code class="">serializers.py</code>​文件：
</p><pre tabindex="0"><code><span><span># article/serializers.py</span></span>
<span><span>from</span><span> rest_framework </span><span>import</span><span> serializers</span></span>
<span><span>from</span><span> article</span><span>.</span><span>models </span><span>import</span><span> Article</span></span>
<span></span>
<span></span>
<span><span>class</span><span> ArticleSerializer</span><span>(</span><span>serializers</span><span>.</span><span>Serializer</span><span>):</span></span>
<span><span>    id</span><span> =</span><span> serializers</span><span>.</span><span>IntegerField</span><span>(</span><span>read_only</span><span>=</span><span>True</span><span>)</span></span>
<span><span>    title </span><span>=</span><span> serializers</span><span>.</span><span>CharField</span><span>(</span><span>required</span><span>=</span><span>True</span><span>,</span><span> max_length</span><span>=</span><span>50</span><span>)</span></span>
<span><span>    body </span><span>=</span><span> serializers</span><span>.</span><span>CharField</span><span>(</span><span>required</span><span>=</span><span>True</span><span>)</span></span>
<span><span>    created </span><span>=</span><span> serializers</span><span>.</span><span>DateTimeField</span><span>(</span><span>read_only</span><span>=</span><span>True</span><span>)</span></span>
<span><span>    updated </span><span>=</span><span> serializers</span><span>.</span><span>DateTimeField</span><span>(</span><span>read_only</span><span>=</span><span>True</span><span>)</span></span>
<span></span>
<span><span>    def</span><span> create</span><span>(</span><span>self</span><span>,</span><span> validated_data</span><span>)</span><span>:</span></span>
<span><span>        return</span><span> Article</span><span>.</span><span>objects</span><span>.</span><span>create</span><span>(</span><span>**</span><span>validated_data</span><span>)</span></span>
<span></span>
<span><span>    def</span><span> update</span><span>(</span><span>self</span><span>,</span><span> instance</span><span>,</span><span> validated_data</span><span>)</span><span>:</span></span>
<span><span>        instance</span><span>.</span><span>title </span><span>=</span><span> validated_data</span><span>.</span><span>get</span><span>(</span><span>'</span><span>title</span><span>'</span><span>,</span><span> instance.title</span><span>)</span></span>
<span><span>        instance</span><span>.</span><span>body </span><span>=</span><span> validated_data</span><span>.</span><span>get</span><span>(</span><span>'</span><span>body</span><span>'</span><span>,</span><span> instance.title</span><span>)</span></span>
<span><span>        instance</span><span>.</span><span>save</span><span>()</span></span>
<span><span>        return</span><span> instance</span></span></code></pre><p>由于​<code class="">TextField</code>​是​<code class="">Django</code>​定义的针对大文本内容的扩展字段，所以在​<code class="">rest_framework</code>​中还是只能用​<code class="">CharField</code>​来序列化。覆写​<code class="">create</code>​与​<code class="">update</code>​方法来定义调用​<code class="">serializer.save()</code>​时的行为。参数​<code class="">required=True</code>​表示必填，​<code class="">read_only=True</code>​表示只读。
</p><h2 id="user-content-shell" class="">shell<a class="" tabindex="-1" href="#shell">#</a></h2><p><code class="">Django</code>​为我们提供了一个交互式的调试环境，输入命令​<code class="">python manage.py shell</code>​命令，进入交互环境。
</p><p>先来看看序列化一个​<code class="">Article</code>​实例：
</p><pre tabindex="0"><code><span><span>>>></span><span> from</span><span> article</span><span>.</span><span>models </span><span>import</span><span> Article</span></span>
<span><span>>>></span><span> from</span><span> article</span><span>.</span><span>serializers </span><span>import</span><span> ArticleSerializer</span></span>
<span><span>>>></span><span> from</span><span> rest_framework</span><span>.</span><span>renderers </span><span>import</span><span> JSONRenderer</span></span>
<span><span>>>></span><span> from</span><span> rest_framework</span><span>.</span><span>parsers </span><span>import</span><span> JSONParser</span></span>
<span><span>>>></span><span> article </span><span>=</span><span> Article</span><span>(</span><span>title</span><span>=</span><span>"</span><span>React</span><span>"</span><span>,</span><span> body</span><span>=</span><span>"</span><span>React is good</span><span>"</span><span>)</span></span>
<span><span>>>></span><span> article</span><span>.</span><span>save</span><span>()</span></span>
<span><span>>>></span><span> serializer </span><span>=</span><span> ArticleSerializer</span><span>(</span><span>article</span><span>)</span></span>
<span><span>>>></span><span> serializer</span><span>.</span><span>data</span></span>
<span><span>{</span><span>'</span><span>id</span><span>'</span><span>:</span><span> 2</span><span>,</span><span> '</span><span>title</span><span>'</span><span>:</span><span> '</span><span>React</span><span>'</span><span>,</span><span> '</span><span>body</span><span>'</span><span>:</span><span> '</span><span>React is good</span><span>'</span><span>,</span><span> '</span><span>created</span><span>'</span><span>:</span><span> '</span><span>2020-03-21T21:19:31.732703</span><span>'</span><span>,</span><span> '</span><span>updated</span><span>'</span><span>:</span><span> '</span><span>2020-03-21T21:19:31.732728</span><span>'</span><span>}</span></span></code></pre><p>之前通过序列化器将实例序列化为​<code class="">Python</code>​内置的字典类型，现在看看将其转为​<code class="">JSON</code>​：
</p><pre tabindex="0"><code><span><span>>>></span><span> content </span><span>=</span><span> JSONRenderer</span><span>().</span><span>render</span><span>(</span><span>serializer.data</span><span>)</span></span>
<span><span>>>></span><span> content</span></span>
<span><span>b</span><span>'</span><span>{"id":2,"title":"React","body":"React is good","created":"2020-03-21T21:19:31.732703","updated":"2020-03-21T21:19:31.732728"}</span><span>'</span></span></code></pre><p>反序列化与上面类似，但步骤相反：
</p><pre tabindex="0"><code><span><span>>>></span><span> import</span><span> io</span></span>
<span><span>>>></span><span> stream </span><span>=</span><span> io</span><span>.</span><span>BytesIO</span><span>(</span><span>content</span><span>)</span></span>
<span><span>>>></span><span> data </span><span>=</span><span> JSONParser</span><span>().</span><span>parse</span><span>(</span><span>stream</span><span>)</span></span>
<span><span>>>></span><span> serializer </span><span>=</span><span> ArticleSerializer</span><span>(</span><span>data</span><span>=</span><span>data</span><span>)</span></span>
<span><span>>>></span><span> serializer</span><span>.</span><span>is_valid</span><span>()</span></span>
<span><span>True</span></span>
<span><span>>>></span><span> serializer</span><span>.</span><span>validated_data</span></span>
<span><span>OrderedDict</span><span>(</span><span>[</span><span>(</span><span>'</span><span>title</span><span>'</span><span>, </span><span>'</span><span>React</span><span>'</span><span>), (</span><span>'</span><span>body</span><span>'</span><span>, </span><span>'</span><span>React is good</span><span>'</span><span>)</span><span>]</span><span>)</span></span>
<span><span>>>></span><span> serializer</span><span>.</span><span>save</span><span>()</span></span>
<span><span>&#x3C;</span><span>Article</span><span>:</span><span> Article </span><span>object</span><span> (</span><span>3</span><span>)</span><span>></span></span></code></pre><h2 id="user-content-modelserializer" class="">ModelSerializer<a class="" tabindex="-1" href="#modelserializer">#</a></h2><p><code class="">REST framework</code>​为我们提供了一个更为简洁的编写序列化器的方式：
</p><pre tabindex="0"><code><span><span># 修改原本的ArticleSerializer类</span></span>
<span><span>class</span><span> ArticleSerializer</span><span>(</span><span>serializers</span><span>.</span><span>ModelSerializer</span><span>):</span></span>
<span></span>
<span><span>    class</span><span> Meta</span><span>:</span></span>
<span><span>        model </span><span>=</span><span> Article</span></span>
<span><span>        fields </span><span>=</span><span> [</span><span>'</span><span>id</span><span>'</span><span>,</span><span> '</span><span>title</span><span>'</span><span>,</span><span> '</span><span>body</span><span>'</span><span>,</span><span> '</span><span>created</span><span>'</span><span>,</span><span> '</span><span>updated</span><span>'</span><span>]</span></span></code></pre><p>可以在交互模式下看看：
</p><pre tabindex="0"><code><span><span>>>></span><span> from</span><span> article</span><span>.</span><span>serializers </span><span>import</span><span> ArticleSerializer</span></span>
<span><span>>>></span><span> serializer </span><span>=</span><span> ArticleSerializer</span><span>()</span></span>
<span><span>>>></span><span> print</span><span>(</span><span>repr</span><span>(</span><span>serializer</span><span>))</span></span>
<span><span>ArticleSerializer</span><span>():</span></span>
<span><span>    id</span><span> =</span><span> IntegerField</span><span>(</span><span>label</span><span>=</span><span>'</span><span>ID</span><span>'</span><span>,</span><span> read_only</span><span>=</span><span>True</span><span>)</span></span>
<span><span>    title </span><span>=</span><span> CharField</span><span>(</span><span>max_length</span><span>=</span><span>50</span><span>)</span></span>
<span><span>    body </span><span>=</span><span> CharField</span><span>(</span><span>style</span><span>=</span><span>{</span><span>'</span><span>base_template</span><span>'</span><span>: </span><span>'</span><span>textarea.html</span><span>'</span><span>}</span><span>)</span></span>
<span><span>    created </span><span>=</span><span> DateTimeField</span><span>(</span><span>read_only</span><span>=</span><span>True</span><span>)</span></span>
<span><span>    updated </span><span>=</span><span> DateTimeField</span><span>(</span><span>read_only</span><span>=</span><span>True</span><span>)</span></span>
<span><span>>>></span><span> </span></span></code></pre><p><code class="">ModelSerializer</code>​帮我们自动生成了所需的字段，并且拥有​<code class="">create</code>​与​<code class="">update</code>​方法的默认实现。这是官方为我们提供的实现一个序列化器的快捷方式。注意到这里还对模型中的​<code class="">TextField</code>​类型的​<code class="">body</code>​做了特殊处理，定义了其渲染成​<code class="">HTML</code>​时的格式。
</p><p>如果你对原生​<code class="">Django</code>​的表单熟悉，你会发现这个​<code class="">Serializer</code>​与原生的​<code class="">Form</code>​非常相似。
</p><h2 id="user-content-总结" class="">总结<a class="" tabindex="-1" href="#总结">#</a></h2><p>现在我们熟悉了​<strong>序列化</strong>​与​<strong>反序列化</strong>​，在下一篇文章中，我们将为我们的API编写一个​<strong>新的视图（View）</strong>​。
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>Django+React全栈开发：MTV初试</title>
          <link>https://elliot00.com/posts/react-django-mtv</link>
          <description>这篇文章讲解了如何在 Django 中创建一个简单的博客应用程序。它首先介绍了环境配置，包括操作系统、Python 版本、Django 版本、Node 版本和编辑器。然后，它指导读者下载 Django 并创建项目和第一个 Django app。接着，它介绍了如何编写模型、视图和模板，并演示了如何使用 Django 的后台管理功能来管理文章。最后，它提供了练习建议，鼓励读者去官网跟着官方给的小教程敲一遍代码。</description>
          <pubDate>Sat, 10 Apr 2021 08:47:40 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <p>第一步先来配置基础的开发环境，先把后端配置起来。
</p><h2 id="user-content-环境配置" class="">环境配置<a class="" tabindex="-1" href="#环境配置">#</a></h2><p>首先说一下我的环境：
</p><ul><li><strong>Manjaro Linux 21.01</strong></li><li><strong>Python 3.9.2</strong></li><li><strong>Django 3.2</strong></li><li><strong>Django REST framework 3.12.4</strong></li><li><strong>Node 15.12.0</strong></li><li><strong>yarn 1.22.10</strong></li></ul><p>编辑器或者IDE：新手推荐PyCharm，或者可以参考<a href="https://github.com/Eliot00/ElliotVim">我的Vim配置</a>。
</p><p>操作系统影响不大，其它的大版本不差太多就行了。
</p><h2 id="user-content-django下载" class="">Django下载<a class="" tabindex="-1" href="#django下载">#</a></h2><p>首先新建一个项目文件夹​<code class="">mkdir DjangoWithReact</code>​，先来做后端部分，所以在这个文件夹下新建​<code class="">backend</code>​文件夹，为了和本地环境隔离开，我们新建一个虚拟环境：
</p><pre tabindex="0"><code><span><span>#</span><span> 在DjangoWithReact/backend目录下</span></span>
<span><span>#</span><span> 新建虚拟环境，如果是有两个Python版本的Linux发行版，记得用python3</span></span>
<span><span>python -m venv venv</span></span>
<span><span>#</span><span> 激活虚拟环境</span></span>
<span><span>source venv/bin/activate</span></span>
<span><span>#</span><span> 如果要退出用下面这个</span></span>
<span><span>deactivate</span></span></code></pre><p>如果你使用​<code class="">Windows</code>​系统，命令会有些不同，百度就可以搜到，这里就不贴了。
</p><p>现在我们安装​<code class="">Django</code>​并且创建项目（注意在虚拟环境下操作）：
</p><pre tabindex="0"><code><span><span>pip install django</span></span>
<span><span>django-admin startproject backend .</span></span></code></pre><p>命令中的​<code class="">.</code>​代表当前文件夹（backend），这样做的原因是如果直接运行​<code class="">django-admin startproject backend</code>​，Django会直接在当前文件夹下再建一个名为backend的文件夹做项目的根目录，这个点让Django直接在当前已存在的文件夹创建项目。现在我们会发现项目文件夹下已经多了个​<code class="">manage.py</code>​文件，同时还有一个内层的backend文件夹，现在来创建第一个Django app。
</p><pre tabindex="0"><code><span><span>#</span><span> DjangoWithReact/backend 目录下</span></span>
<span><span>python manage.py startapp article</span></span></code></pre><p>现在你应该能看到这样的目录结构：
</p><div>.
├── article
│   ├── admin.py
│   ├── apps.py
│   ├── __init__.py
│   ├── migrations
│   │   └── __init__.py
│   ├── models.py
│   ├── tests.py
│   └── views.py
├── backend
│   ├── asgi.py
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── db.sqlite3
└── manage.py
</div><blockquote><p>在​​<code class="">backend/backend/settings.py</code>​​文件中编辑，找到列表​​<code class="">INSTALLED_APPS</code>​​这个列表，添加='article.apps.ArticleConfig'=​​，这个操作将我们新建的app注册到Django项目中。
</p></blockquote><p>使用命令​<code class="">python manage.py runserver</code>​，打开浏览器在地址栏输入​<code class="">http://127.0.0.1:8000</code>​看到Django标志的小火箭图标，表示环境已经成功搭建。
</p><h2 id="user-content-模型层" class="">模型层<a class="" tabindex="-1" href="#模型层">#</a></h2><p>现在来开始编写我们的第一个模型，进入​<code class="">article</code>​目录，打开并修改​<code class="">models.py</code>​文件：
</p><pre tabindex="0"><code><span><span>from</span><span> django</span><span>.</span><span>db </span><span>import</span><span> models</span></span>
<span><span>from</span><span> django</span><span>.</span><span>utils </span><span>import</span><span> timezone</span></span>
<span></span>
<span></span>
<span><span>class</span><span> Article</span><span>(</span><span>models</span><span>.</span><span>Model</span><span>):</span></span>
<span><span>    title </span><span>=</span><span> models</span><span>.</span><span>CharField</span><span>(</span><span>max_length</span><span>=</span><span>50</span><span>)</span></span>
<span><span>    body </span><span>=</span><span> models</span><span>.</span><span>TextField</span><span>()</span></span>
<span></span>
<span><span>    def</span><span> __str__</span><span>(</span><span>self</span><span>)</span><span> -></span><span> str</span><span>:</span></span>
<span><span>        return</span><span> self</span><span>.</span><span>title</span></span></code></pre><p>我们定义了一个名为​<code class="">Article</code>​的类，它拥有两个属性。​<code class="">title</code>​表示标题，使用​<code class="">CharField</code>​字段，设定最大长度为50，​<code class="">body</code>​则是正文，使用​<code class="">TextField</code>​字段。
</p><p>接着我们进行数据迁移，使用命令​<code class="">python manage.py makemigrations article</code>​，注意运行命令的位置，应该在外层的​<code class="">backend</code>​文件夹里。
</p><div>(venv) $ python manage.py makemigrations article 
Migrations for 'article':
  article/migrations/0001_initial.py
    - Create model Article
</div><p>可以看到在​<code class="">article</code>​文件夹中自动生成了​<code class="">migrations</code>​文件夹，并且多了一个​<code class="">Python</code>​文件。接着使用​<code class="">python manage.py migrate</code>​命令将数据模型迁移到数据库中。
</p><p>我们现在数据库中新增一些数据进去，使用命令​<code class="">python manage.py makesuperuser</code>​创建一个管理员账户，Django已经帮我们内置了一个后台管理功能：
</p><div>(venv)  $ python manage.py createsuperuser
Username (leave blank to use 'elliot'): test
Email address: 
Password: 
Password (again): 
This password is too common.
Bypass password validation and create user anyway? [y/N]: y
Superuser created successfully.
</div><blockquote><p>Linux/Unix 系统下不会显示输入的密码，别怕，不是你的键盘坏了。
</p></blockquote><p>接着修改​<code class="">article/admin.py</code>​：
</p><pre tabindex="0"><code><span><span>from</span><span> django</span><span>.</span><span>contrib </span><span>import</span><span> admin</span></span>
<span><span>from</span><span> article</span><span>.</span><span>models </span><span>import</span><span> Article</span></span>
<span></span>
<span></span>
<span><span>admin</span><span>.</span><span>site</span><span>.</span><span>register</span><span>(</span><span>Article</span><span>)</span></span></code></pre><p>接着​<code class="">python manage.py runserver</code>​运行Django，进入​<code class="">http://127.0.0.1:8000/admin</code>​：
</p><p><img alt="管理后台" src="https://i.loli.net/2021/04/10/QGwSzEZXDYTtcVh.png"></p><p><code class="">Users</code>​和​<code class="">Groups</code>​是Django提供的，我们先点进​<code class="">Article</code>​看看，试着选择​<code class="">Add article</code>​来新建几个文章。
</p><h2 id="user-content-视图层" class="">视图层<a class="" tabindex="-1" href="#视图层">#</a></h2><p>接下来将眼光放到​<code class="">article/views.py</code>​中：
</p><pre tabindex="0"><code><span><span>from</span><span> django</span><span>.</span><span>shortcuts </span><span>import</span><span> render</span></span>
<span><span>from</span><span> article</span><span>.</span><span>models </span><span>import</span><span> Article</span></span>
<span></span>
<span></span>
<span><span>def</span><span> article_list</span><span>(</span><span>request</span><span>)</span><span>:</span></span>
<span><span>    articles </span><span>=</span><span> Article</span><span>.</span><span>objects</span><span>.</span><span>all</span><span>()</span></span>
<span><span>    context </span><span>=</span><span> {</span><span>'</span><span>articles</span><span>'</span><span>:</span><span> articles</span><span>}</span></span>
<span></span>
<span><span>    return</span><span> render</span><span>(</span><span>request</span><span>,</span><span> '</span><span>article/article_list.html</span><span>'</span><span>,</span><span> context</span><span>)</span></span></code></pre><p>还记得我们浏览网页时，浏览器上地址栏里会有个url吗？​<code class="">Django</code>​根据用户请求的URL来决定使用哪一个视图，所以再去编辑一下​<code class="">backend/urls.py</code>​：
</p><pre tabindex="0"><code><span><span># 这里的backend是内层的backend文件夹</span></span>
<span><span>from</span><span> django</span><span>.</span><span>contrib </span><span>import</span><span> admin</span></span>
<span><span>from</span><span> django</span><span>.</span><span>urls </span><span>import</span><span> path</span></span>
<span><span>from</span><span> django</span><span>.</span><span>urls</span><span>.</span><span>conf </span><span>import</span><span> include</span></span>
<span></span>
<span><span>urlpatterns </span><span>=</span><span> [</span></span>
<span><span>    path</span><span>(</span><span>'</span><span>admin/</span><span>'</span><span>,</span><span> admin.site.urls</span><span>),</span></span>
<span><span>    path</span><span>(</span><span>''</span><span>,</span><span> include</span><span>(</span><span>'</span><span>article.urls</span><span>'</span><span>)),</span></span>
<span><span>]</span></span></code></pre><p>这里选择包含了​<code class="">article</code>​的urls，所以要新建一个​<code class="">article/urls.py</code>​文件：
</p><pre tabindex="0"><code><span><span>from</span><span> django</span><span>.</span><span>urls </span><span>import</span><span> path</span></span>
<span><span>from</span><span> article </span><span>import</span><span> views</span></span>
<span></span>
<span></span>
<span><span>urlpatterns </span><span>=</span><span> [</span></span>
<span><span>    path</span><span>(</span><span>'</span><span>article_list</span><span>'</span><span>,</span><span> views.article_list</span><span>),</span></span>
<span><span>]</span></span></code></pre><h2 id="user-content-模板层" class="">模板层<a class="" tabindex="-1" href="#模板层">#</a></h2><p>现在我们有了数据模型，有了决定视图渲染的视图函数，为了把数据在浏览器上呈现给其他人看，我们还需要一个​<code class="">html</code>​文件。在​<code class="">article</code>​文件夹下新建文件夹​<code class="">templates</code>​，再在​<code class="">templates</code>​中新建​<code class="">article</code>​文件夹，在里面新建​<code class="">article_list.html</code>​（还记得视图函数的最后一行吗）。
</p><pre tabindex="0"><code><span><span>{% for article in articles %}</span></span>
<span><span>  &#x3C;</span><span>h5</span><span>></span><span>{{ article.title }}</span><span>&#x3C;/</span><span>h5</span><span>></span></span>
<span><span>  &#x3C;</span><span>p</span><span>></span><span>{{ article.body }}</span><span>&#x3C;/</span><span>p</span><span>></span></span>
<span><span>{% endfor %}</span></span></code></pre><p>现在运行Django，打开​<code class="">http://127.0.0.1:8000/article_list/</code>​，你将看到一个列出所有文章标题与内容的网页。
</p><p><img alt="管理后台" src="https://i.loli.net/2021/04/10/Kp5EINa3k4wiMWD.png"></p><p>好啦，现在你已经拥有一个简洁的博客网页了，并且可以后台管理，本系列教程到此结束，完结撒花。
</p><p><strong>开个玩笑，哈哈</strong></p><p>这是只是简单体验一下Django的​<code class="">MTV</code>​架构模式，在后续章节中将深入讲解，并利用​<code class="">Django REST framework</code>​将我们的应用改造为​<code class="">RESTful API</code>​。
</p><h2 id="user-content-练习" class="">练习<a class="" tabindex="-1" href="#练习">#</a></h2><p>想要加深一下对Django的了解，请去<a href="https://www.djangoproject.com/">官网</a>跟着官方给的小教程敲一遍代码。
</p><p>一般来说官方文档应该是我们需要常读的最好的资料之一，但是没有必要把整个文档从头到尾看一遍并熟记，我们并不需要应付考试，​<strong>先粗略看看官方指引，接着在实践中遇到问题，再去查看官方文档，我认为这更为有效。</strong></p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>Django+React全栈开发：前言</title>
          <link>https://elliot00.com/posts/react-django-preface</link>
          <description>这篇文章主要介绍了如何使用Django和React进行开发。它首先列出了开发环境，包括后端的后端Django、DRF，前端的React、NextJS、TailwindCSS，以及操作系统Manjaro Linux。然后，它介绍了阅读本系列文章需要具备的前置基础，例如Python和JavaScript语法基础，以及一台可用的电脑。最后，它介绍了该系列文章的许可协议和问题交流方式。</description>
          <pubDate>Sat, 10 Apr 2021 05:37:31 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <p>应该是2020年开始写这个系列的，不过当时写的比较混乱，最近<a href="https://www.dusaiphoto.com/">杜塞大佬的Django+Vue系列</a>更新完了，并且​<code class="">Django3.2</code>​也发布了，那我就厚着脸皮附上一个Django+React版本吧。
</p><h2 id="user-content-开发环境" class="">开发环境<a class="" tabindex="-1" href="#开发环境">#</a></h2><p>目前可能会用到的有：
</p><ul><li>后端：Django、DRF
</li><li>前端：React、NextJS、TailwindCSS
</li><li>操作系统：我在一台Manjaro Linux机器上开发
</li></ul><h2 id="user-content-前置基础" class="">前置基础<a class="" tabindex="-1" href="#前置基础">#</a></h2><p>输出也是一种巩固知识的方式，这一系列主要是讲Django与React的，所以要想阅读顺畅还需要一些基础：
</p><ul><li>Python与JavaScript语法基础
</li><li>有一台可用的电脑可以随时写代码
</li></ul><h2 id="user-content-许可协议" class="">许可协议<a class="" tabindex="-1" href="#许可协议">#</a></h2><p><a href="/">我的博客</a>所有文章都是采用<a href="https://creativecommons.org/licenses/by-nc-sa/4.0/">署名-非商业性使用-相同方式共享 4.0 国际</a>协议发布的，只要遵守协议内容就可以了。
</p><h2 id="user-content-问题交流" class="">问题交流<a class="" tabindex="-1" href="#问题交流">#</a></h2><p>所有代码后续会放在<a href="https://github.com/Eliot00/DjangoWithReact">Github</a>上。之前一次更新把博客的注册评论功能去掉了，后续可能会用第三方服务，遇到问题可直接在公众号留言，或者添加杜塞的Django QQ群：​<strong>107143175</strong>​。
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>近期关于哲学的一些感想</title>
          <link>https://elliot00.com/posts/thinking-philosophy</link>
          <description>这篇文章探讨了哲学的本质及其与现实生活的关系。作者认为，哲学是对人生的系统性反思，并不是远离现实生活的空谈。哲学家对名、实关系、矛盾双方相互转化的思考，都反映了他们对现实世界的深刻洞察。虽然哲学本身并不创造任何东西，但它能给我们一种看待世界的新视角，帮助我们换一种角度思考，从而在现实生活中获得积极有用的启示。</description>
          <pubDate>Sat, 20 Mar 2021 08:58:06 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <p>一直以来我都非常感兴趣，哲学这门学科在研究什么呢？因为平时常常会听到哲学被贴上​<em>远离世俗</em>​、​<em>空谈</em>​、​<em>无用</em>​等标签，我想会不会像计算机科学常被误解为修电脑的一样，是一种不了解带来的偏见？带着一些疑问，我阅读了一些相关书籍，顺带记录下一些思考，当然我仍然是个门外汉，水平有限，不值一哂。
</p><h2 id="user-content-哲学脱离现实吗" class="">哲学脱离现实吗？<a class="" tabindex="-1" href="#哲学脱离现实吗">#</a></h2><p>冯友兰在《中国哲学简史》中说：”哲学是对人生的系统的反思“，当然这只是众多解释的一种，反思人生，并不是个容易的事情，系统性的反思，那就更加难了。不过如果说是对​<strong>人生</strong>​的反思，那又怎么会远离生活，脱离现实呢？
</p><p>公孙龙子是战国时期名家的代表人物，他有个很出名的​<strong>白马非马</strong>​之辩，不管什么颜色的马，都是一种马，这是我们的常识，公孙龙却说：”马者。所以命形也；白者，所以命色也。命色者非命形也。故曰：白马非马。“，换句话说，公孙龙是在强调”马“、”白“与”白马“的内涵不同，用古希腊的哲学语言来说，公孙龙在强调”共相“间的区别。
</p><p>同时公孙龙也是名家**”离坚白“**学派的代表，离坚白之辩大致是说，假如有块坚硬的白色石头，用眼看，只是白石，用手摸，就只是坚石，坚与白两种共相彼此分离。
</p><p>举了名家的例子，名家恰好旨在​<strong>欲推是辩，以正名实而化天下</strong>​，可以发现，表面上名家在诡辩，让人感觉很荒诞，但这也反映出名家对名、实关系的反思，这与直觉、常识有些不符，我觉得正因为由反思带来的与直觉的不相符，使人感觉哲学，离现实很远。另外，这也引出另一个问题，不仅仅是名家，也许由于时代背景的原因，春秋战国时期诸子百家，都在研究一种形而上与治世的结合。
</p><h2 id="user-content-反者道之动" class="">反者道之动<a class="" tabindex="-1" href="#反者道之动">#</a></h2><p>柏拉图认为在理想国中，应当​<strong>哲学家为王</strong>​，与其差不多同时代的中国的诸子百家虽然有各种不同的观念，但也几乎有一个共同的观点，即​<strong>内圣外王</strong>​。但是有些区别的地方在于，柏拉图认为哲学家统治理想国，是”被迫“，违背本心的，而中国的内圣外王，内圣是内在修养，人皆可以为尧舜，而人成了圣，他不仅适合为王，并且他积极为王，积极引导他人，出世与入世在这里相统一了起来。
</p><p>在道家著作《道德经》中，常常能看到诸如*<strong>*”大音希声，大象无形“​*、</strong>​”有无相生，难易相成，长短相形“​<strong>这样将一组对立的概念放在一起的句子，我认为它们可以概括为</strong>​”反者道之动“​<strong>，矛盾的双方，在一定条件下会向着相反方向转化。这么看来，古代哲人在追求超道德，追求与”天“、”道“的合一，这是</strong>​出世*<strong>*，而圣人治天下，又成了​*入世</strong>​，恰恰符合”反者道之动“。
</p><h2 id="user-content-哲学有用吗" class="">哲学”有用“吗？<a class="" tabindex="-1" href="#哲学有用吗">#</a></h2><p>知乎上有句流传很广的话，叫”先问是不是，再问问什么“，而”哲学有用吗？“这个问题，那首先也得想清楚什么是有用，什么是无用。我们在逛商场的时候，看到一件商品，也会想到这件东西买回去有没有用，这里的用，是指在日常生活上的价值，那以这个有用论，哲学有没有用呢？
</p><p>《庄子》的人间世篇记载了一个故事，庄子与弟子遇到一棵大树，弟子问这样的大树怎么无人砍伐，庄子说这是不材之木，无所可用，所以能留存下来。好木材能打造家具，对人来说，是有用，对树来说，却是无用，因为会遭砍伐，反过来，对人无用的树，却不会被砍，这就是庄子说的​<strong>无用之用</strong>​。这里有用与无用的转换，只是换个角度看问题。
</p><p>前面提到哲学是对人生系统性的反思，反思，当然不会是重新思一遍，而是要​<strong>换一种思路，从另一个角度</strong>​再次思考。关于庄子，还有一个故事，就是庄子的妻子去世，庄子却​<em>鼓盆而歌</em>​，说：
</p><blockquote><p>“不然。是其始死也，我独何能无概然！察其始而本无生，非徒无生也而本无形，非徒无形也而本无气。杂乎芒芴之间，变而有气，气变而有形，形变而有生，今又变而之死，是相与为春秋冬夏四时行也。人且偃然寝于巨室，而我噭噭然随而哭之，自以为不通乎命，故止也”
</p></blockquote><p>庄子认为，有无，生死，成毁，如同圆上相对的点，而道则在圆心，圣人应与道合一，从道的角度看，不论生，也不论死，忘年忘义，振于无竟。常有人说道家的政治理论是愚民，使民无知，其实不然，有个词叫​<strong>坐忘</strong>​，道家的最终追求是​<strong>不知</strong>​，人生而​<strong>无知</strong>​，学而​<strong>有知</strong>​，忘而​<strong>不知</strong>​。就像金庸小说里，张无忌学太极拳，令狐冲学独孤九剑，招式繁复，变化多端，学完却要尽数忘却，要无招胜有招。
</p><p>所以我想，哲学并不创造什么东西，就生产创造而言无用，但在现实生活中，它让我们换一种角度思考，就像庄子，亲人去世，固然痛苦，但在“道”的眼里，就如同四时变换一般，是形态的转移，无所谓生，也无所谓死。这是哲学对现实生活积极有用的方面。
</p><blockquote><p>哲学不报告任何事实，所以不能用具体的、物理的方法解决任何问题。例如，它既不能使人长生不死，也不能使人致富不穷。可是它能够给人一种观点，从这种观点可以看出生死相同，得失相等。从实用的观点看，哲学是无用的。哲学能给我们一种观点，而观点可能很有用。用《庄子》的话说，这是“无用之用”（《人间世》）。
</p></blockquote>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>通过WebAssembly实现插件机制</title>
          <link>https://elliot00.com/posts/rust-plugin-with-webassembly</link>
          <description>这篇文章分为三个部分，分别介绍了插件、WebAssembly以及总结。在第一部分，作者介绍了插件的原理和实现方法，并讨论了在Rust中动态加载插件的可能性。在第二部分，作者介绍了WebAssembly的概念、原理和实现方法，并演示了如何在Rust中调用WebAssembly编写的函数。在第三部分，作者总结了WebAssembly的前景。</description>
          <pubDate>Mon, 22 Feb 2021 14:55:33 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <h2 id="user-content-插件" class="">插件<a class="" tabindex="-1" href="#插件">#</a></h2><p>前两天在鼓捣<a href="https://github.com/inherd/coco">coco</a>的插件系统，我们常说要面向接口开发而不是面向实现，插件这个东西，就像后端框架里的中间件，我们按照框架定义的接口实现中间件，这也可以算一种插件，我们有很多机制实现“编译前插件”，但是像coco这样要编译发布的二进制程序，有什么办法让用户定义插件来补充功能呢？不能在运行期间插入用户的代码再重新编译整个程序吧？有办法在运行时加载用户的库文件吗？有的，这项技术被称为<a href="https://en.wikipedia.org/wiki/Dynamic_loading">Dynamic Loading</a>。
</p><p><img alt="Plugin" src="https://i.loli.net/2021/02/22/SdDIRAY45lGKXWV.png"></p><p>在coco中借助<a href="https://crates.io/crates/dlopen">dlopen</a>这个crate实现了动态加载，但是在Rust中动态加载似乎必须要​<strong>unsafe</strong>​，就在为coco实现插件机制的某个瞬间，我突然有了个新的想法。
</p><h2 id="user-content-webassembly" class="">WebAssembly<a class="" tabindex="-1" href="#webassembly">#</a></h2><blockquote><p>WebAssembly是一种新的编码方式，可以在现代的网络浏览器中运行 － 它是一种低级的类汇编语言，具有紧凑的二进制格式，可以接近原生的性能运行，并为诸如C / C ++等语言提供一个编译目标，以便它们可以在Web上运行。它也被设计为可以与JavaScript共存，允许两者一起工作。------MDN
</p></blockquote><p>WebAssembly最开始设计是在浏览器中运行的，不过就像JS的node一样，现在WebAssembly也有了独立于浏览器之外的运行时，比如<a href="https://github.com/bytecodealliance/wasmtime">wasmtime</a>和<a href="https://github.com/wasmerio/wasmer">wasmer</a>，并且它们都提供了嵌入各个主流语言的辅助库，也就是说，我可以在这个运行时支持的语言内自由地调用wasm二进制文件内的函数咯？试试看！
</p><h2 id="user-content-wasmtime" class="">wasmtime<a class="" tabindex="-1" href="#wasmtime">#</a></h2><p>我尝试用wasmtime做了个demo，首先新建一个crate：
</p><pre tabindex="0"><code><span><span>$</span><span> cargo new adder --lib</span></span></code></pre><p>打开​<code class="">src/lib.rs</code>​文件，写一个简单的求和函数：
</p><pre tabindex="0"><code><span><span>#[no_mangle]</span></span>
<span><span>pub</span><span> extern</span><span> "</span><span>C</span><span>"</span><span> fn</span><span> adder</span><span>(</span><span>a</span><span>:</span><span> i32</span><span>, </span><span>b</span><span>:</span><span> i32</span><span>) </span><span>-></span><span> i32</span><span> {</span></span>
<span><span>    a</span><span> +</span><span> b</span></span>
<span><span>}</span></span></code></pre><p><code class="">no_mangle</code>​告诉Rust编译器不要修改函数名称，以便后续调用。另外还要修改​<code class="">Cargo.toml</code>​文件：
</p><pre tabindex="0"><code><span><span>[lib]</span></span>
<span><span>crate-type</span><span> =</span><span> [</span><span>'</span><span>cdylib</span><span>'</span><span>]</span></span></code></pre><p>接着就可以这个命令编译：
</p><pre tabindex="0"><code><span><span>#</span><span> rustup target add wasm32-wasi</span></span>
<span><span>#</span><span> 如果没有设置target要用上面的命令设置下</span></span>
<span><span>$</span><span> cargo build --target wasm32-wasi</span></span></code></pre><p>这下就可以在项目下的target目录里找到对应的​<code class="">adder.wasm</code>​文件了。
</p><p>接着再创建一个crate：
</p><pre tabindex="0"><code><span><span>$</span><span> cargo new wasm_test</span></span></code></pre><p>在​<code class="">main.rs</code>​写入如下代码：
</p><pre tabindex="0"><code><span><span>use</span><span> std</span><span>::</span><span>error</span><span>::</span><span>Error</span><span>;</span></span>
<span><span>use</span><span> wasmtime</span><span>::*</span><span>;</span></span>
<span></span>
<span><span>fn</span><span> main</span><span>() </span><span>-></span><span> Result</span><span>&#x3C;(), </span><span>Box</span><span>&#x3C;</span><span>dyn</span><span> Error</span><span>>> {</span></span>
<span><span>    let</span><span> engine</span><span> =</span><span> Engine</span><span>::</span><span>default</span><span>();</span></span>
<span><span>    let</span><span> store</span><span> =</span><span> Store</span><span>::</span><span>new</span><span>(</span><span>&#x26;</span><span>engine</span><span>);</span></span>
<span><span>    let</span><span> module</span><span> =</span><span> Module</span><span>::</span><span>from_file</span><span>(</span><span>&#x26;</span><span>engine</span><span>, </span><span>"</span><span>adder.wasm</span><span>"</span><span>)</span><span>?</span><span>;</span></span>
<span><span>    let</span><span> instance</span><span> =</span><span> Instance</span><span>::</span><span>new</span><span>(</span><span>&#x26;</span><span>store</span><span>, </span><span>&#x26;</span><span>module</span><span>, </span><span>&#x26;</span><span>[])</span><span>?</span><span>;</span></span>
<span><span>    let</span><span> adder</span><span> =</span><span> instance</span></span>
<span><span>        .</span><span>get_func</span><span>(</span><span>"</span><span>adder</span><span>"</span><span>)</span></span>
<span><span>        .</span><span>expect</span><span>(</span><span>"</span><span>adder was not an exported function</span><span>"</span><span>);</span></span>
<span><span>    let</span><span> adder</span><span> =</span><span> adder</span><span>.</span><span>get2</span><span>::</span><span>&#x3C;</span><span>i32</span><span>, </span><span>i32</span><span>, </span><span>i32</span><span>>()</span><span>?</span><span>;</span></span>
<span><span>    let</span><span> result</span><span> =</span><span> adder</span><span>(</span><span>2</span><span>, </span><span>4</span><span>)</span><span>?</span><span>;</span></span>
<span><span>    println!</span><span>(</span><span>"</span><span>result is {}</span><span>"</span><span>, </span><span>result</span><span>);</span></span>
<span><span>    Ok</span><span>(())</span></span>
<span><span>}</span></span></code></pre><p>不要忘了引入wasmtime依赖，不过现在运行会直接报错：
</p><div>Error: wrong number of imports provided, 0 != 4
</div><p>调试后发现错误出在创建​<code class="">instance</code>​的地方，浏览文档发现如果wasm中有import依赖项，那么这里第三个参数就不能是空数组，而是包括所有依赖的数组，不过这么简单的函数哪来的依赖呢？通过wasm2wat工具将wasm文件转成文本格式，再用grep搜索一下，果然，编译后自动添加了一些依赖：
</p><pre tabindex="0"><code><span><span>❯</span><span> wasm2wat adder.wasm </span><span>|</span><span> grep</span><span> import</span></span>
<span><span>  (import "wasi_snapshot_preview1" "fd_write" (func $_ZN4wasi13lib_generated22wasi_snapshot_preview18fd_write17ha0aef7cef0a152b0E (type 6)))</span></span>
<span><span>  (import "wasi_snapshot_preview1" "environ_sizes_get" (func $__wasi_environ_sizes_get (type 2)))</span></span>
<span><span>  (import "wasi_snapshot_preview1" "proc_exit" (func $__wasi_proc_exit (type 0)))</span></span>
<span><span>  (import "wasi_snapshot_preview1" "environ_get" (func $__wasi_environ_get (type 2)))</span></span></code></pre><p>因为我们编译的目标是​<code class="">wasm32-wasi</code>​，wasi全称是​<strong>WebAssembly System Interface</strong>​，是一个标准化的WebAssembly系统接口，而wasmtime在说明如何在Rust中使用的部分给的代码却是适用于​<code class="">wasm32-unknown-unknown</code>​的（不得不说文档质量不太好），这里如果把编译target改成wasm32-unknown-unknown就可以直接运行。
</p><p>不过不改编译目标就要稍微修改下程序，根据搜到的<a href="https://github.com/bytecodealliance/wasmtime/issues/1730">issue</a>，我改了下代码：
</p><pre tabindex="0"><code><span><span>use</span><span> std</span><span>::</span><span>error</span><span>::</span><span>Error</span><span>;</span></span>
<span><span>use</span><span> wasi_cap_std_sync</span><span>::</span><span>WasiCtxBuilder</span><span>;</span></span>
<span><span>use</span><span> wasmtime</span><span>::*</span><span>;</span></span>
<span><span>use</span><span> wasmtime_wasi</span><span>::</span><span>Wasi</span><span>;</span></span>
<span></span>
<span><span>fn</span><span> main</span><span>() </span><span>-></span><span> Result</span><span>&#x3C;(), </span><span>Box</span><span>&#x3C;</span><span>dyn</span><span> Error</span><span>>> {</span></span>
<span><span>    let</span><span> engine</span><span> =</span><span> Engine</span><span>::</span><span>default</span><span>();</span></span>
<span><span>    let</span><span> store</span><span> =</span><span> Store</span><span>::</span><span>new</span><span>(</span><span>&#x26;</span><span>engine</span><span>);</span></span>
<span></span>
<span><span>    let</span><span> mut</span><span> linker</span><span> =</span><span> Linker</span><span>::</span><span>new</span><span>(</span><span>&#x26;</span><span>store</span><span>);</span></span>
<span><span>    let</span><span> wasi</span><span> =</span><span> Wasi</span><span>::</span><span>new</span><span>(</span></span>
<span><span>        &#x26;</span><span>store</span><span>,</span></span>
<span><span>        WasiCtxBuilder</span><span>::</span><span>new</span><span>()</span></span>
<span><span>            .</span><span>inherit_stdio</span><span>()</span></span>
<span><span>            .</span><span>inherit_args</span><span>()</span><span>?</span></span>
<span><span>            .</span><span>build</span><span>()</span><span>?</span><span>,</span></span>
<span><span>    );</span></span>
<span><span>    wasi</span><span>.</span><span>add_to_linker</span><span>(</span><span>&#x26;</span><span>mut</span><span> linker</span><span>)</span><span>?</span><span>;</span></span>
<span></span>
<span><span>    let</span><span> module</span><span> =</span><span> Module</span><span>::</span><span>from_file</span><span>(</span><span>&#x26;</span><span>engine</span><span>, </span><span>"</span><span>adder.wasm</span><span>"</span><span>)</span><span>?</span><span>;</span></span>
<span><span>    let</span><span> instance</span><span> =</span><span> linker</span><span>.</span><span>instantiate</span><span>(</span><span>&#x26;</span><span>module</span><span>)</span><span>?</span><span>;</span></span>
<span><span>    let</span><span> adder</span><span> =</span><span> instance</span></span>
<span><span>        .</span><span>get_func</span><span>(</span><span>"</span><span>adder</span><span>"</span><span>)</span></span>
<span><span>        .</span><span>expect</span><span>(</span><span>"</span><span>adder was not an exported function</span><span>"</span><span>);</span></span>
<span><span>    let</span><span> adder</span><span> =</span><span> adder</span><span>.</span><span>get2</span><span>::</span><span>&#x3C;</span><span>i32</span><span>, </span><span>i32</span><span>, </span><span>i32</span><span>>()</span><span>?</span><span>;</span></span>
<span><span>    let</span><span> answer</span><span> =</span><span> adder</span><span>(</span><span>1</span><span>, </span><span>7</span><span>)</span><span>?</span><span>;</span></span>
<span><span>    println!</span><span>(</span><span>"</span><span>the answer is {}</span><span>"</span><span>, </span><span>answer</span><span>);</span></span>
<span><span>    Ok</span><span>(())</span></span>
<span><span>}</span></span></code></pre><p>搞定！wasmtime目前支持五种语言，wasmer支持更多，这样用户可以用C/C++或者Go来写插件啦，我们可以在Rust程序中调用，并且不用写unsafe了。
</p><h2 id="user-content-总结" class="">总结<a class="" tabindex="-1" href="#总结">#</a></h2><p>通过WebAssembly我们可以在Rust中调用其它语言写的库，反过来其实也可以，WebAssembly成为了一种中间语言或者说虚拟机，例如在Python中，由于动态类型特性，可以这样使用：
</p><pre tabindex="0"><code><span><span>import</span><span> wasmtime</span><span>.</span><span>loader</span></span>
<span><span>import</span><span> adder  </span><span># 直接引入adder.wasm</span></span>
<span></span>
<span></span>
<span><span>print</span><span>(</span><span>adder.</span><span>adder</span><span>(</span><span>1</span><span>,</span><span> 7</span><span>))</span></span></code></pre><p>我很看好WebAssembly的前景，独立运行时的出现，使得WebAssembly成为一个通用的​<strong>公共语言运行时</strong>​，实现“​<strong>Run any code on any client</strong>​”。
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>谈谈目的与手段</title>
          <link>https://elliot00.com/posts/purpose-and-means</link>
          <description>这篇文章探讨了人们在选择工具时容易陷入“工具至上”的误区，忘记了工具是用来辅助人完成工作的。作者以程序员圈子中对最佳语言、最佳编辑器的争论为例，指出人们往往把追求工具的目的（提高生产力、简洁性等）当成了目的本身。作者还提到，自己在下围棋时也曾陷入类似的误区，原本下棋只是为了享受计算中纯粹的乐趣，最后却常常因为输棋而陷入苦恼，想要提高棋力，又强迫自己下下去，最终却丢失了想要的乐趣。作者认为，我们需要多多思考，辨别手段与目的，避免在追求工具的路上迷失自己。</description>
          <pubDate>Fri, 12 Feb 2021 14:39:49 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <p>上次写了篇文章讲Vim，没过多久就在社交网络上看到一些有关编辑器的争吵，看到有人说折腾了Vim很久，感到很受折磨，总是用不好，不禁有些感慨，有些时候，我们一直走，不愿停下脚步，却忘了出发时的方向。
</p><p>最近读了古斯塔夫·勒庞的《乌合之众》，这本书的副标题是“大众心理研究”，其中有一段论述挺有意思：
</p><blockquote><p>词语威力的大小与它们所唤起的形象密切相关，跟它本身的意思却毫无关系。有时，意思越不明确的词越能引起行动，比如“民主”、“社会主义”、“平等”、“自由”等等这些词，它们的意思非常宽泛，几大本书都不足以把它们讲清。然而，它们朗朗上口，确实拥有神奇的力量，好像能解决一切问题。词语综合了各种无意识的渴望和实现它们的希望。
</p></blockquote><p>勒庞认为群体是轻信的，是不讲究真相而热衷于表象的。在程序员圈子里，常常有对所谓最佳语言、最佳编辑器的争论，所谓“最佳”、“高效”、“简洁”都成了一个个魔咒，追求目的的手段，似乎被当成了目的。很多人没有意识到，执着于某个工具的时候，其实已经忘了工具是用来辅助人完成工作的，锤子很擅长敲钉子，螺丝刀可以拧螺丝，但不论锤子再“好”，要拧螺丝的时候，还是要用螺丝刀。
</p><p>我很喜欢围棋，但是现在有很长时间没有下棋了，原本下棋只是因为享受计算中纯粹的乐趣，最后却常常因为输棋而陷入苦恼，想要提高棋力，又强迫自己下下去，最终却丢失了我想要的乐趣。
</p><p>如何辨别手段与目的？或者说我们当下在做的，是我们一开始想要的吗？我没有确切的答案，只能多多思考。
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>百宝箱：Vim进阶之路</title>
          <link>https://elliot00.com/posts/toolchain-vim</link>
          <description>这篇文章描述了作者学习 Vim 编辑器的经历，总结了三个阶段的使用经验。第一阶段，作者习惯了 Vim 的编辑方式，但仍然有鼠标思维，导致效率不高。第二阶段，作者开始记忆更多的快捷键和技巧，提高了对 Vim 的熟练程度，减少了鼠标思维。第三阶段，作者意识到高效使用 Vim 需要思维方式的转变，需要思考如何更少的按键。文章还举了一些例子来说明如何通过思考来提高编辑效率。最后，作者总结说，虽然 Vim 是一个强大的编辑器，但并不推荐编程新手使用，因为新手需要一个功能齐全的 IDE 来提高效率。</description>
          <pubDate>Mon, 18 Jan 2021 15:39:43 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <h2 id="user-content-前言" class="">前言<a class="" tabindex="-1" href="#前言">#</a></h2><p><strong>工欲善其事，必先利其器。</strong>​强大的工具能提升工作效率，节约时间让我们做更多的事情。通过搜索引擎可以查询到很多某某方面的某工具，不过照搬照用未必就是最好的，在工具使用过程中有时也需要些思考，打磨出最适合自己的工具，于是我决定写一系列博客来记录一下个人工具链的打造过程。
</p><p>Vim是我使用时间最长的编辑工具了，我想在Vim的使用方式上一共经过了三个阶段，才慢慢获得了个人目前来说最合适的编辑体验，这篇博客也是要记录一下这个过程（并不是入门指南或插件推荐之类的文章）。
</p><h2 id="user-content-stage0" class="">Stage0<a class="" tabindex="-1" href="#stage0">#</a></h2><p>一段时间内，我使用一台陈旧的Ubuntu机器编程，没法使用Visual Studio这样的IDE，Vim是为数不多的选择之一，这个时候Vim对我来说就是一个不占内存、编辑方式独特的编辑器。
</p><p>一个连续的编辑体验，最好应该不使用鼠标，大多数人应该都不愿意在编辑过程中不得不腾出一只手来操控鼠标。
</p><p>Vim独特的编辑模式可以让我们轻松摆脱鼠标，但是如果没有摆脱鼠标思维，那仍然难以获得最佳的编辑体验。举例来说，在normal模式下，假如光标此时在行首，对于一个忘记加分号的段落：
</p><pre tabindex="0"><code><span><span>let</span><span> foo</span><span> =</span><span> 30</span></span></code></pre><p>我会不断按l键，将光标移动到行尾，再按下a键修改。当然这略有些夸张，但大致如此。尽管我已经习惯了手不离开键盘移动光标，但这仍然是鼠标思维。仔细想想，这和在普通编辑器中直接移动鼠标，点击要修改部分的末尾，按删除键，再输入并没有太大区别。尽管Vim有多种模式，但我大部分时间都在插入模式上。
</p><h2 id="user-content-stage1" class="">Stage1<a class="" tabindex="-1" href="#stage1">#</a></h2><p>我开始慢慢意识到，虽然我摆脱了鼠标的使用，但仍没有摆脱鼠标式的思维方式。我开始记忆更多的快捷键，更多的技巧。除了预装的vimtutor，我还收集了一些有关Vim技巧的文章，通过反复练习加强记忆。
</p><p>例如，进入插入模式不只是用​<code class="">i</code>​，​<code class="">o</code>​向下新建一行并插入，​<code class="">O</code>​则向上新建一行，​<code class="">a</code>​与​<code class="">A</code>​，​<code class="">p</code>​与​<code class="">P</code>​等同理，虽然这些在初识Vim时就已经知道了，但是缺乏练习还是会让人很快遗忘。
</p><p>在这一阶段，我对Vim的正常模式、命令模式、可视模式等模式的使用率也提高了，例如​<code class="">ce</code>​快速改变单词，​<code class="">ci"</code>​修改引号内内容，​<code class="">~</code>​改变大小写，​<code class="">:s/foo/bar/g</code>​快速替换等等，鼠标操作的思考模式渐渐减少。
</p><h2 id="user-content-stage2" class="">Stage2<a class="" tabindex="-1" href="#stage2">#</a></h2><p>一个编辑器还不至于上升到哲学的高度，但是要高效使用Vim，确实需要一些思维方式的转变，记住快捷键很重要，但在使用时也要有点思考，并渐渐将思考转变成肌肉记忆。
</p><p>有些Vim老手会玩一种叫<a href="https://www.vimgolf.com">VimGolf</a>的游戏，简单来说就是比拼对于一个编辑操作使用更少的按键，例如上个例子中在行末加分号，比较几种不同方式：
</p><p>1 多次​<code class="">l</code>​到行尾，按​<code class="">a</code>​键，输入分号，按键数为行字符数+1
2 使用​<code class="">$</code>​到行尾，再按a键，输入分号，按键3次
3 按下​<code class="">A</code>​键，输入分号，按键2次
</p><p>在以上方法中，显然第三种按键更少，更节省时间。渐渐多思考如何更少的按键，我能感到编辑速度变得更快了。
</p><p>再举个例子，在下面这段代码中的操作符两旁加空格：
</p><pre tabindex="0"><code><span><span>1</span><span>+</span><span>3</span><span>+</span><span>89</span><span>+</span><span>18</span></span></code></pre><p>1 移动再插入
2 <code class="">f+s + =​，之后用​=;.</code>​重复查找修改操作
3 <code class="">:%s/+/ + /g</code>​使用命令模式来替换
</p><p>第一种显然低效，第二种相比第一种的方便之处在于，它将鼠标式思维的在加号前后两处添加空格，改成了可以重复的，查找加号，替换加号的一次查找替换操作，鼠标思维的前面添加->移动光标->后面添加变成了一次替换编辑操作。可重复在Vim中非常重要，​<code class="">;</code>​可以重复上一次​<code class="">f</code>​的查找操作，​<code class="">.</code>​可以重复上一次编辑操作。
</p><p>第三种方式则是一步到位，可以将​<code class="">:%s</code>​换成​<code class="">:1,4s</code>​将替换范围限制在1到4行，可以按下​<code class="">V:</code>​，则自动将当前行作为替换范围，还可以将最后的​<code class="">g</code>​替换成​<code class="">c</code>​，在每次替换时手动确认等等。
</p><p>在这个例子中，当有多个行，大量需要修改时，第三种方式更便捷，但有时，第二种方式，或者说，构造可重复操作可能更好。例如在多行行首添加=#=​符号，可以按​<code class="">I#</code>​，再不断用​<code class="">j.</code>​重复操作。甚至可以用宏，录制一些可以重复的复杂操作。
</p><h2 id="user-content-总结" class="">总结<a class="" tabindex="-1" href="#总结">#</a></h2><p>虽然这篇博客一直在说Vim，但是即使是使用其它编辑工具，要想提高效率，多练习、多思考是必不可少的。另外，虽然我很喜欢使用Vim，但是还是要说，​<strong>绝对不推荐编程新手使用Vim</strong>​，单单从功能齐全来说，我认为没有任何一个编辑器可以和IDE相比较，虽然可以配置很多插件来优化体验，但是插件配置需要花时间，并且大量插件也弱化了编辑器“轻量级”的优势，所以并不推荐新手使用Vim。技巧、工具，是手段，提高效率，才是目的。
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>再见，2020</title>
          <link>https://elliot00.com/posts/review-2020</link>
          <description>文章记录了作者2020年的工作、学习和生活经历。在技术领域，作者学习了React、C#、TypeScript和Rust等编程语言，还参与了开源项目的贡献。在生活方面，作者去了郑州旅游，与对象见面。文章最后列出了2021年的目标，包括与爱的人一起度过2021、用ASP.NET core建一个问答论坛、深入了解Azure Function与Blazor、用Rust实现一个基于中间件的异步HTTP服务端框架、练听力，学好英语、看完两本厚书：CSAPP、SICP等。</description>
          <pubDate>Wed, 30 Dec 2020 13:14:10 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <p>今年确实是个很不平凡的一年，一不留神居然就到年尾了，就写篇文章记录一下这一年我都做了什么事吧。
</p><h2 id="user-content-技术" class="">技术<a class="" tabindex="-1" href="#技术">#</a></h2><h3 id="user-content-前端">前端<a class="" tabindex="-1" href="#前端">#</a></h3><p>疫情在家的时间，看起了早有耳闻的React。不得不说现在网络上各种教程真的很多，不过不是很喜欢看视频，看的都是些文字资料，官方文档把基础部分看了一遍，就开始尝试做一些Demo。从React开始了解了一些前端的新概念，顺带着就把博客用React改造了一番，用了NextJS，现在深深觉得学习新东西最快的方式是找一个实际项目去做，很多需求如何实现，一些坑如何避免，没有实际使用真的无法了解，只是看看教程最终只能是多掌握个Hello World。
</p><h3 id="user-content-语言">语言<a class="" tabindex="-1" href="#语言">#</a></h3><p>重拾了C#，说起来我和计算机的缘分始于C#，很多年前的一个夏天，在福州路买了本C#的书，当时我还以为这个语言叫C井，甚至也不明白到底什么是编程，大概过了一年我才拥有人生第一台电脑，为了安装Visual Studio（当时应该是2012版吧）还费了不少功夫。不过很长一段时间都没有深入学习，今年想在Django之外了解一些别的Web框架，又再次学起了C#，和当年相比，.NET已经发展到跨平台的.NET core，年底还出了多端统一的.NET 5，学习之路还很漫长。
</p><p>学了React，自然要系统地过一遍JavaScript基础，这下正好有时间，用了几天时间看了一遍<a href="https://zh.javascript.info/">现代JavaScript教程</a>的基础部分，JS有些历史包袱有时候挺让人难受，个人还是比较喜欢静态强类型语言的风格，写Python也习惯加上类型注解，正好了解到TypeScript的存在，Next对TS支持也非常好，所以先在自己的博客上尝鲜体验了一下。
</p><p>要说目前为止我见过最让我感到惊艳的语言，非Rust莫属了，目前还没有在工作中用到，不过确实给我在编写程序中带来了一些新的思考。
</p><h3 id="user-content-开源">开源<a class="" tabindex="-1" href="#开源">#</a></h3><p>虽然生活和工作中一直都在从开源产品中受益，但今年确实是第一次向开源世界做了点自己微小的贡献，首先是用Rust写了一个<a href="https://github.com/Eliot00/commit-formatter">git commit的格式化工具</a>，另外就是给<a href="https://github.com/ant-design-blazor/ant-design-blazor">ant-design-blazor</a>和<a href="https://charj-lang.org/">charj</a>这两个项目提了pr，看着Github的star数上升还有pr被合并，还是蛮有成就感的。
</p><h2 id="user-content-生活" class="">生活<a class="" tabindex="-1" href="#生活">#</a></h2><p>七月份去了一趟郑州，做为一个土生土长的南方人，在这之前从来没去过秦岭淮河线以北，所以这是我第一次去北方。尝到了一些地方美食，第一次喝了郑州的胡辣汤。因为疫情与对象一共没见几次面，短暂的陪伴弥足珍贵。
</p><h2 id="user-content-2021" class="">2021<a class="" tabindex="-1" href="#2021">#</a></h2><p>不想写计划（毕竟计划不一定都能完成），就列一下明年想做的事吧～
</p><ul><li>和爱的人一起度过2021
</li><li>用ASP.NET core建一个问答论坛
</li><li>深入了解Azure Function与Blazor
</li><li>Rust实现一个基于中间件的异步HTTP服务端框架
</li><li>练听力，学好英语
</li><li>看完两本厚书：CSAPP、SICP
</li></ul>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>论博客的进化与前端发展史</title>
          <link>https://elliot00.com/posts/blog-evolution</link>
          <description>这篇文章描述了作者使用NextJS重新构建个人博客的技术栈变更历程。作者一开始使用Django和MySQL搭建了博客，但随着对前端技术的学习，他逐渐将博客的后端部分提取出来，使用RESTful风格的API和Docker来部署。为了解决SPA单页应用的SEO问题，作者使用了NextJS框架，它采用约定式路由和服务端渲染的方式来实现更好的搜索引擎优化。NextJS还提供了Link组件和Shallow Routing等功能，可以在不重新加载整个页面的时候更新部分内容，从而提高页面的切换速度。作者还使用了GraphQL和Serverless技术来进一步优化博客的性能和部署方式。最后，作者表达了对未来使用Blazor框架来构建SPA后台的计划。</description>
          <pubDate>Sat, 19 Dec 2020 05:54:07 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <h2 id="user-content-概述" class="">概述<a class="" tabindex="-1" href="#概述">#</a></h2><p>这次的标题十分标题党了，这篇文章实际上想要聊聊我的个人博客的技术栈变更与我感受中的前端技术的发展。
</p><p>事实上，博客这个词，对我来说似乎是上个世纪的东西，在我小时候微博已经流行起来，我是在微博之后才知道博客这个东西的。大约在2017年左右吧，突然想找个输出文字的地方，做一些记录，当时注册了一个微信公众号，直到19年才开始搭建了这个个人博客。其实搭建博客可以说是醉翁之意不在酒，成熟的博客生成应用挺多的，没必要自己折腾，但是当时趁着促销买了个阿里云服务器，正好又想学习一些前端技术，这个博客就应运而生了。
</p><p>有时为了实现一些功能，了解到一些新的技术，有时又正好相反，将一些新东西应用到博客上，致使博客成了个完全的“冗余工程”，我的博客的技术栈改变，也就恰好变成了我个人的技术栈成长历程，，正好在这里记录一下，顺带要记录一下​<code class="">NextJS</code>​的使用体验。
</p><h2 id="user-content-模板渲染session与jquery" class="">模板渲染、Session与jQuery<a class="" tabindex="-1" href="#模板渲染session与jquery">#</a></h2><p>一开始，做为一名主要使用​<code class="">Python</code>​的后端程序员，我尝试使用​<code class="">Django</code>​来开发博客。​<code class="">Django</code>​是一个遵循​<code class="">MVC</code>​模式的Web框架，​<code class="">Python</code>​相关的应用在性能上一般都不强，所以​<code class="">Django</code>​主打的卖点也是​<strong>快速交付</strong>​，内置的用户系统、管理后台、模型迁移等，最初版本的博客没花多久就做好了。
</p><p>数据库采用​<code class="">MySQL</code>​，在​<code class="">Django</code>​中使用内置的模板引擎处理前端页面：
</p><pre><code class="language-django">{% if latest_question_list %}
    &#x3C;ul>
    {% for question in latest_question_list %}
        &#x3C;li>&#x3C;a href="/polls/{{ question.id }}/">{{ question.question_text }}&#x3C;/a>&#x3C;/li>
    {% endfor %}
    &#x3C;/ul>
{% else %}
    &#x3C;p>No polls are available.&#x3C;/p>
{% endif %}
</code></pre><p>在约定的文件夹内放置静态文件，使用​<code class="">nginx</code>​代理。​<code class="">UI</code>​样式主要是通过​<code class="">BootStrap</code>​，毕竟我对​<code class="">CSS</code>​实在是不熟悉，对于一些页面动效也是通过框架或者一些搜索到的​<code class="">jQuery</code>​代码来解决，这里参考了不少<a href="https://www.dusaiphoto.com/">杜塞的博客</a>。
</p><p><code class="">Django</code>​内置了用户与组模型，管理后台也是内置的，登录验证也通过默认的Session中间件，利用第三方库实现了​<code class="">OAuth</code>​登录，在这些方面基本上没花时间，一开始博客开放了登录注册与评论功能，可以通过​<code class="">Github</code>​登录，但后来觉得没必要又去掉了。这里UI与业务代码是完全紧密联系在一起的。
</p><h2 id="user-content-restfuldocker与tls" class="">RESTful、Docker与TLS<a class="" tabindex="-1" href="#restfuldocker与tls">#</a></h2><p>因为工作的原因，我开始向全栈的方向发展，开始学习​<code class="">JavaScript</code>​，在这之前就常常听到前端三大框架的名字了，因为偶然间加了一个​<code class="">React</code>​群，于是开始学习​<code class="">React</code>​。在这期间我先把博客的后端部分提取了出来，借助​<code class="">Django REST framework</code>​这个库快速地完成了这部分工作，尽量使接口符合​<code class="">RESTful</code>​规范，毕竟应用简单，没什么需要妥协而违反规范的地方，评论功能已经移除，但是登录接口仍然保留着，所以做了个​<code class="">JWT</code>​登录认证的接口，后来还是删除了。剩下的​<code class="">Django</code>​部分不再关心如何呈现用户界面，仅仅根据请求将需要的数据通过​<code class="">JSON</code>​返回给前端，也并不关心前端是什么。
</p><p>我的服务器操作系统是​<code class="">Ubuntu 18.04</code>​，在最初的部署过程中，需要考虑​<code class="">Python</code>​版本，以及​<code class="">nginx</code>​，​<code class="">MySQL</code>​的安装问题，未来如果服务器要迁移（毕竟阿里云学生机活动只有两年），也比较麻烦，于是用上了​<code class="">Docker</code>​，写好镜像文件后，运行容器即可。
</p><p>在安全问题与钱包的权衡中，我选择了<a href="https://letsencrypt.org/">Let's Encrypt</a>的免费证书实现​<code class="">HTTPS</code>​，安装很方便，每三个月自动续签一次。
</p><h2 id="user-content-spa与ssr" class="">SPA与SSR<a class="" tabindex="-1" href="#spa与ssr">#</a></h2><p>顺利地在项目中使用了​<code class="">React</code>​做了管理后台之后，开始考虑在博客中用上。但是平时开发的都是​<code class="">SPA</code>​单页应用，怎么解决​<strong>SEO</strong>​问题呢？
</p><p>之前我使用​<code class="">Django</code>​的模板引擎来做服务端渲染，也就是在返回响应之前，已经将文章内容等数据插入​<code class="">HTML</code>​文件中，最终用户在浏览器得到的都是静态文件，如果用户请求了另一个页面，那么他得到的是完全不同的静态文件。
</p><p>而用​<code class="">React</code>​写的​<code class="">SPA</code>​则是在客户端渲染，使用虚拟​<code class="">DOM</code>​，整个应用往往只有单个​<code class="">HTML</code>​文件，切换页面也不再重新请求新的页面，而是更换需要改变的​<strong>组件</strong>​。
</p><p>那么这两者的优缺点也就很明显了，传统的服务端渲染（SSR），首页打开是较快的，因为不会一次加载过多内容，对搜索引擎也是友好的，毕竟爬虫可以直接获取到静态的、包含数据​<code class="">HTML</code>​文件，但是切换页面则需要更换整个页面重新渲染，并且前后端，表现层与业务层紧耦合；单页应用在客户端渲染，页面切换快，异步获取数据，前后端分离，但是由于首次访问就要获取整个应用资源，因此首屏加载慢，并且经由​<code class="">JS</code>​在客户端操作渲染，爬虫难以获得需要的数据，对​<code class="">SEO</code>​不利。
</p><h2 id="user-content-nextjspagesroute与定制app" class="">NextJS：pages、route与定制app<a class="" tabindex="-1" href="#nextjspagesroute与定制app">#</a></h2><p>通过​<code class="">React</code>​与​<code class="">SEO</code>​这两个关键字，我发现了<a href="https://nextjs.org/">NextJS</a>这个​<code class="">React</code>​脚手架，在其官网我又看到了关键词​<code class="">SSR</code>​服务端渲染，难道前端经过多年发展又绕回去了吗？当然不是，历史的发展总是螺旋上升的，这里的​<code class="">SSR</code>​与传统的方式已经不同了，准确点说，这种技术应该叫​<code class="">SSG</code>​静态站点生成或​<code class="">Jamstack</code>​。
</p><p><code class="">NextJS</code>​采用约定式路由，在​<code class="">pages</code>​目录下的文件名，如​<code class="">about.js</code>​，则对应​<code class="">/about</code>​这个​<code class="">URL</code>​，其中默认导出的组件就是页面组件。特殊的是​<code class="">index.js</code>​对应的是​<code class="">/</code>​。除此之外还有动态路由，要求文件名用方括号括起来，如​<code class="">[tag].js</code>​。可以匹配到​<code class="">/a</code>​、​<code class="">/someThing</code>​、​<code class="">/?tag=crumb</code>​等。​<code class="">pages</code>​目录下默认是页面级组件，共享组件则放到​<code class="">src/component</code>​中。
</p><p>框架提供了​<code class="">useRoute</code>​这个​<code class="">Hook</code>​让我们便捷地使用路由​<code class="">api</code>​，这里我主要希望在页面切换的时候监听切换事件，改变​<code class="">loading</code>​状态，改善用户弱网环境下体验（实际上很难看到这个切换过程，原因后面再说）。监听路由切换是挺方便的，但是如果每个页面都需要注册一次监听，组件卸载时取消监听，重复代码未免太多了。
</p><p><code class="">NextJS</code>​提供了修改容器组件的功能，在​<code class="">pages</code>​文件夹下新建​<code class="">_app.js</code>​：
</p><pre><code class="language-react">// _app.tsx节选
function MyApp({ Component, pageProps }: AppProps) {
  const [loading, setLoading] = useState(false)
  const router = useRouter()

  const startLoading = () => {
    console.log('route change start')
    setLoading(true)
  }

  const stopLoading = () => {
    console.log('route change complete')
    setLoading(false)
  }

  useEffect(() => {
    router.events.on('routeChangeStart', startLoading)
    router.events.on('routeChangeComplete', stopLoading)
    router.events.on('routeChangeError', stopLoading)

    return () => {
      router.events.off('routeChangeStart', startLoading)
      router.events.off('routeChangeComplete', stopLoading)
      router.events.on('routeChangeError', stopLoading)
    }

  }, [])

  return (
    &#x3C;Fragment>
      &#x3C;Header />
        &#x3C;Component loading={loading} {...pageProps} />
      &#x3C;Footer />
      &#x3C;BackTop />
      &#x3C;style jsx global>{`
        body {
          background-color: #f6f6f6
        };
      `}&#x3C;/style>
    &#x3C;/Fragment>
  )
}
</code></pre><p>上面代码中使用了内置的​<code class="">css-in-component</code>​，一种内联式的样式写法。当时我使用的​<code class="">React</code>​版本已经有了​<code class="">Hooks</code>​，整个博客代码里我全部使用的函数式组件，事实上这里用类组件也是可以的。
</p><h2 id="user-content-data-fetch" class="">Data Fetch<a class="" tabindex="-1" href="#data-fetch">#</a></h2><p>前面提到​<code class="">NextJS</code>​可以让搜索引擎获取到预渲染的，拥有数据的静态页面，那么​<code class="">Next</code>​中具体怎么获取数据呢？
</p><ul><li><strong>SSR</strong>​：服务端渲染，通过在页面级组件中导出​<code class="">getServerSideProps</code>​函数，在这个函数内访问​<code class="">API</code>​，最后返回一个​<code class="">{props: {...}}</code>​对象，返回值的​<code class="">props</code>​将被注入到页面组件的​<code class="">props</code>​中，这个方法的运行时机是每次客户端请求时（这种形式下​<code class="">NextJS</code>​会默认用户使用​<code class="">Node</code>​做服务器，但仅限于UI层，仍然是前后端分离的），适合页面数据变化多的情况。
<ul><li>可以渐进加载，数据完全获取之前用户首先得到较少的、轻量的页面
</li><li>前端需要一个=Node=服务做一个中间层，前后端分离
</li><li>每次请求都需要向后端获取一次数据
</li></ul></li><li><strong>SSG</strong>​：静态站点生成，在页面中导出​<code class="">getStaticProps</code>​方法，这个方法只会在​<code class="">build</code>​时运行一次，不会出现在客户端，所以甚至可以在这里访问数据库和文件系统，如果所有页面都是​<code class="">SSG</code>​，构建之后的应用可以直接以​<code class="">Serverless</code>​方式部署，只需一个​<code class="">CDN</code>​就可以部署项目。同一个页面下​<code class="">SSR</code>​与​<code class="">SSG</code>​是互斥的，但不同页面可以根据需要来做。对于​<code class="">[id].js</code>​这样的动态路由，则可以配合​<code class="">getStaticPaths</code>​这个方法，返回所有可能的路径，​<code class="">Next</code>​会自动生成所有页面。
<ul><li>部署便捷
</li><li>仅在构建时在服务端调用数据获取函数
</li></ul></li><li><strong>ISG</strong>​：增长式静态再生成，前面说了对于页面数据变化频繁的应用，可以使用​<code class="">getServerSideProps</code>​，但是较新版本的​<code class="">NextJS</code>​变得更加强大，对于​<code class="">getStaticProps</code>​，可以在返回的对象中加上​<code class="">revalidate</code>​属性，值以秒为单位，​<code class="">Next</code>​会在有新请求进入后的固定时间后验证后端数据，如果确实有新的更改，将先返回由新数据组成的页面，在这之后再自动重新​<code class="">build</code>​。而​<code class="">getStaticPaths</code>​则可以通过在返回值中设置​<code class="">fallback</code>​属性为​<code class="">true</code>​，这样例如​<code class="">posts/[uuid].js</code>​上次​<code class="">build</code>​后有24篇文章，那么访问​<code class="">/posts/25</code>​将可以不用直接返回404，而是重新验证请求是否有新文章（详情查看<a href="https://nextjs.org/docs/basic-features/data-fetching#getserversideprops-server-side-rendering">文档</a>）。这两种方式可以配合使用。
<ul><li>增长式构建
</li><li>快速响应，结合了传统服务端渲染与SPA的优点
</li></ul></li><li><strong>客户端请求</strong>​：在以上任何一种形式下，我们都仍然可以在客户端执行请求，并且​<code class="">Next</code>​提供了一个非常优秀的基于​<code class="">Hooks</code>​的请求库<a href="https://github.com/vercel/swr">swr</a>。
</li></ul><h2 id="user-content-cicdserverless与graphql" class="">CI/CD、Serverless与GraphQL<a class="" tabindex="-1" href="#cicdserverless与graphql">#</a></h2><p>一开始我打算利用<a href="https://github.com/features/actions">Github Actions</a>来做自动部署，不过恰好碰上​<code class="">Next</code>​更新，​<code class="">Next</code>​所属的​<code class="">Vercel</code>​云服务更加好用了，于是我就选择了官方推荐的方式，只要提供一个​<code class="">git</code>​仓库的链接，​<code class="">Vercel</code>​会在每次​<code class="">push</code>​后自动以​<code class="">Serverless</code>​的形式部署，并且提供域名与​<code class="">HTTPS</code>​证书（其实就是Let's Encypt的证书）。
</p><p>这时候我想把后端也改造成​<code class="">Serverless</code>​应用，顺带发现了<a href="https://hasura.io/cloud/">hasura</a>，可以提供免费的​<code class="">GraphQL</code>​服务，只要有一个​<code class="">postgresql</code>​数据库就可以生成一个基于​<code class="">GraphQL</code>​的后端，简单的​<code class="">CRUD</code>​是完全没问题的，所以也没有继续折腾了。但对于前端来说，我仅仅是要在构建时获取数据，​<code class="">Apollo</code>​对我来说太重了，没必要，于是我找到了​<code class="">graphql-request</code>​这个库，基本上只是对​<code class="">fetch</code>​的简单封装，足够使用了。
</p><pre tabindex="0"><code><span><span>// 示例</span></span>
<span><span>const</span><span> response</span><span> =</span><span> await</span><span> request</span><span>(</span><span>GraphQLEndpoint</span><span>,</span><span> query</span><span>,</span><span> variables</span><span>)</span></span></code></pre><h2 id="user-content-linkshallow-routing与筛选分页" class="">Link、Shallow Routing与筛选分页<a class="" tabindex="-1" href="#linkshallow-routing与筛选分页">#</a></h2><p>前面提到服务端渲染在切换页面的速度上有缺陷，因为请求新页面需要返回完整的新页面的静态文件，哪怕页面大部分布局都没变。​<code class="">Next</code>​提供的​<code class="">Link</code>​组件，在默认情况下，会在闲置时自动请求​<code class="">JSON</code>​数据，这样等到用户点击链接时，就可以做到快速更换内容，渲染新页面，也是因为这个在非弱网环境下我看不到页面​<code class="">loading</code>​效果。
</p><p><img alt="Network" src="https://i.loli.net/2020/11/30/syNhlm2c6HWjXDo.png"></p><p>在我的博客中，为文章模型设置了不少外键，像文章栏目、标签这些，还有分页，想要为这些设置页面，​<code class="">Next</code>​提供了一种不用重新抓取数据更新页面的方式​<code class="">Shallow Routing</code>​：
</p><pre><code class="language-react">// 代码节选
&#x3C;Button
    onClick={() => route.push(`/posts?column=${item.column.name}`, undefined, { shallow: true })}>{item.column.name}&#x3C;/Button>

const route = useRoute()

useEffect(() => {
    if (column) {
      setArticles(articles => sourceArticles.filter(article => article.column.name === route.query.column))
    }
    console.log(articles)
  }, [route.query.column])
</code></pre><p>在​<code class="">useEffect</code>​这个​<code class="">Hook</code>​中根据​<code class="">route.query.column</code>​的变化决定是否更新文章数据源，就可以做到筛选，并且不需要重新获取数据，页面只有部分更新。
</p><p>但是我非常贪心，既想使用静态模式，又想每次筛选只拿到筛选所需的数据，而不是一次取得所有数据，在客户端筛选，这可以借助动态路由来做（这里我使用的TypeScript，Next全面支持TS）：
</p><div>pages
├── about.tsx
├── _app.tsx
├── posts
│   └── [id].tsx
├── series.tsx
└── [column]
│   └── [page].tsx
├── [tag]
│   └── [page].tsx
└── [page].tsx
</div><p>但是这种形式，实质上对于栏目、标签、页数的筛选，其实都是首页列表页，这造成仅仅只是​<code class="">getStaticPaths</code>​函数不同，剩下全是重复代码，这是令人无法接受的，并且这只能接受*<strong>*/[column]/[page​*这样的路由，而不能是</strong>​/column=Python&#x26;page=2**这样的​<code class="">query</code>​形式。
</p><p>那么有没有办法一次接受所有的动态路由呢？实际上是有的，​<code class="">[...slug]</code>​这种命名的页面组件就可以，但是参数只能是一个​<strong>数组</strong>​，例如​<code class="">['a', 'b']</code>​对应​<code class="">/a/b</code>​，这样我没法分辨​<code class="">column</code>​与​<code class="">tag</code>​，并且与前面说的一样，没法以​<code class="">query</code>​参数的形式访问。
</p><p>关于这方面，有一个<a href="https://github.com/vercel/next.js/discussions/17269">issue</a>，有可能会在某一个版本实现​<code class="">getStaticParams</code>​这样的​<code class="">API</code>​，对于博客这种数据量小的应用使用​<code class="">Shallow Routing</code>​完全没问题，但是对于如知乎这样的大型平台，筛选、搜索、分页功能都是必不可少的。
</p><h2 id="user-content-下一步" class="">下一步<a class="" tabindex="-1" href="#下一步">#</a></h2><p>现在在我的博客中最初的​<code class="">Django</code>​部分已经完全废弃了，自然Django提供的管理后台也就不能用了，在<a href="https://www.elliot00.com/posts/detail/21">上一篇文章</a>中介绍了​<code class="">Blazor</code>​，接下来预计会花费几个周末来搭建一个​<code class="">SPA</code>​的后台，毕竟后台应用不需要​<code class="">SEO</code>​，​<code class="">SPA</code>​会更合适，用​<code class="">CSharp</code>​来写前端，过去​<code class="">JS</code>​向桌面端、移动端渗透，反过来，静态语言也开始染指​<code class="">Web</code>​前端了。
</p><p>总的来说，这个博客的不断重新构建的过程，也是我学习一些前端技能的试验过程，在这个过程中倒有一种经历了前端技术发展变迁的感觉，从“切图仔”，慢慢地工程化、体系化，随着​<code class="">Node</code>​以及一些框架的发展，前端开发体验也在不断提高。博客本身成了一个实验室，各种东西轮番体验了一遍，这个过程暂时还不会停止，毕竟业余时间写写代码还是挺有意思的。博客<a href="https://github.com/Eliot00/elliot00.com">已开源</a>。
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>刷题笔记0x07：不同路径</title>
          <link>https://elliot00.com/posts/leetcode-different-ways</link>
          <description>这篇文章讨论了一个机器人从网格左上角移动到右下角的不同路径数问题。它首先介绍了问题的描述，然后提出了一个递归公式 `f(x, y) = f(x-1, y) + f(x, y-1)` 来计算从起点到坐标点 `(x, y)` 的路径数。接着，文章解释了如何使用二维数组来避免数组索引出现负数，并提供了代码实现。最后，文章还提到了可以用排列组合的方法来解决这个问题。</description>
          <pubDate>Sat, 19 Dec 2020 05:52:51 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <h2 id="user-content-不同路径" class="">不同路径<a class="" tabindex="-1" href="#不同路径">#</a></h2><h3 id="user-content-描述">描述<a class="" tabindex="-1" href="#描述">#</a></h3><p><a href="https://leetcode-cn.com/problems/unique-paths/">原题目链接</a></p><blockquote><p>一个机器人位于一个 m x n 网格的左上角 （起始点在下图中标记为“Start” ）。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角（在下图中标记为“Finish”）。
问总共有多少条不同的路径？
</p></blockquote><p><img alt="robot_maze" src="https://i.loli.net/2020/02/17/9pg1MCu3TxhbWNU.png"></p><h3 id="user-content-思路">思路<a class="" tabindex="-1" href="#思路">#</a></h3><p>首先想到的是，既然这个机器人只能​<strong>向下</strong>​或​<strong>向右</strong>​走，那么，设机器人当前所在位置坐标为​<code class="">(x, y)</code>​，可以得出结论，机器人只能从​<code class="">(x-1, y)</code>​或​<code class="">(x, y-1)</code>​来。
</p><p>现在，让我们设函数​<code class="">f(x, y)</code>​为机器人到达坐标点​<code class="">(x, y)</code>​的路径数。那么，由之前的结论可得出公式，即​<code class="">f(x, y) = f(x-1, y) + f(x, y-1)</code>​。
</p><p>另外，对于所有​<strong>合法的终点</strong>​（也就是除了起点以外），当​<code class="">y = 0</code>​或​<code class="">x = 0</code>​时，路径数都为​<code class="">1</code>​。
</p><h3 id="user-content-代码">代码<a class="" tabindex="-1" href="#代码">#</a></h3><p>在具体实现时要考虑一下，如果用​<strong>二维数组</strong>​来记录从​<strong>起点</strong>​到​<strong>终点</strong>​的路径数，C语言数组的索引是​<strong>不能为负数</strong>​的。例如有一个​<code class="">3 x 2</code>​的格子，可以如下表操作：
</p><table><thead><tr><th>0</th><th>0</th><th>0</th><th>0</th></tr></thead><tbody><tr><td>0</td><td>0</td><td>1</td><td>0</td></tr><tr><td>0</td><td>1</td><td>0</td><td>0</td></tr></tbody></table><p>将​<code class="">3 x 2</code>​的格子改造成​<code class="">4 x 3</code>​，并且外圈​<strong>置0</strong>​，同时把坐标为​<code class="">(1, 2)</code>​的格子与​<code class="">(2, 1)</code>​的格子置为1，因为​<strong>从起点到这两点路径数都为</strong>​​<code class="">1</code>​，这样后面所有坐标都有了计算的基点。对于这一点如果不能理解，可以在纸上画图，并把后面坐标的值按照公式​<code class="">f(x, y) = f(x-1, y) + f(x, y-1)</code>​填一下。
</p><p>这样，我们相当于把原本的格子向右下移动了，以此避免索引出现负数。
</p><p>首要工作就是要声明数组并赋值：
</p><pre tabindex="0"><code><span><span>int</span><span> dp</span><span>[m</span><span>+</span><span>1</span><span>][n</span><span>+</span><span>1</span><span>];</span></span>
<span><span>// 全部置0</span></span>
<span><span>memset</span><span>(dp</span><span>,</span><span> 0</span><span>,</span><span> sizeof</span><span>(dp));</span></span>
<span><span>// 先把这两个坐标设为1</span></span>
<span><span>dp</span><span>[</span><span>1</span><span>][</span><span>2</span><span>] </span><span>=</span><span> 1</span><span>;</span></span>
<span><span>dp</span><span>[</span><span>2</span><span>][</span><span>1</span><span>] </span><span>=</span><span> 1</span><span>;</span></span></code></pre><p>接着就可以迭代计算了，既然我们把格子向右下​<code class="">移动</code>​了，那么要从索引​<code class="">1</code>​开始。
</p><pre tabindex="0"><code><span><span> for</span><span> (</span><span>int</span><span> x </span><span>=</span><span> 1</span><span>; x </span><span>&#x3C;=</span><span> m; x</span><span>++</span><span>)</span></span>
<span><span>{</span></span>
<span><span>    for</span><span> (</span><span>int</span><span> y </span><span>=</span><span> 1</span><span>; y </span><span>&#x3C;=</span><span> n; y</span><span>++</span><span>)</span></span>
<span><span>    {</span></span>
<span><span>        // 前面我们已经设好这两个坐标的值为1了，不能把它们又改掉了</span></span>
<span><span>        if</span><span> ((x </span><span>==</span><span> 1</span><span> &#x26;&#x26;</span><span> y </span><span>==</span><span> 2</span><span>) </span><span>||</span><span> (x </span><span>==</span><span> 2</span><span> &#x26;&#x26;</span><span> y </span><span>==</span><span> 1</span><span>))</span></span>
<span><span>        {</span></span>
<span><span>            continue</span><span>;</span></span>
<span><span>        }</span></span>
<span><span>        dp</span><span>[x][y] </span><span>=</span><span> dp</span><span>[x</span><span>-</span><span>1</span><span>][y] </span><span>+</span><span> dp</span><span>[x][y</span><span>-</span><span>1</span><span>];</span></span>
<span><span>    }</span></span>
<span><span>}</span></span></code></pre><p>完整代码如下，之所以要先有个​<code class="">if</code>​判断，是因为这个题目输入​<code class="">1，1</code>​时要返回​<code class="">1</code>​：
</p><pre tabindex="0"><code><span><span>int</span><span> uniquePaths</span><span>(</span><span>int</span><span> m</span><span>,</span><span> int</span><span> n</span><span>)</span></span>
<span><span>{</span></span>
<span><span>    if</span><span> (m </span><span>==</span><span> 1</span><span> ||</span><span> n </span><span>==</span><span> 1</span><span>)</span></span>
<span><span>    {</span></span>
<span><span>        return</span><span> 1</span><span>;</span></span>
<span><span>    }</span></span>
<span><span>    int</span><span> dp</span><span>[m</span><span>+</span><span>1</span><span>][n</span><span>+</span><span>1</span><span>];</span></span>
<span><span>    memset</span><span>(dp</span><span>,</span><span> 0</span><span>,</span><span> sizeof</span><span>(dp))</span><span>;</span></span>
<span><span>    dp</span><span>[</span><span>1</span><span>][</span><span>2</span><span>] </span><span>=</span><span> 1</span><span>;</span></span>
<span><span>    dp</span><span>[</span><span>2</span><span>][</span><span>1</span><span>] </span><span>=</span><span> 1</span><span>;</span></span>
<span><span>    for</span><span> (</span><span>int</span><span> x </span><span>=</span><span> 1</span><span>; x </span><span>&#x3C;=</span><span> m; x</span><span>++</span><span>)</span></span>
<span><span>    {</span></span>
<span><span>        for</span><span> (</span><span>int</span><span> y </span><span>=</span><span> 1</span><span>; y </span><span>&#x3C;=</span><span> n; y</span><span>++</span><span>)</span></span>
<span><span>        {</span></span>
<span><span>            if</span><span> ((x </span><span>==</span><span> 1</span><span> &#x26;&#x26;</span><span> y </span><span>==</span><span> 2</span><span>) </span><span>||</span><span> (x </span><span>==</span><span> 2</span><span> &#x26;&#x26;</span><span> y </span><span>==</span><span> 1</span><span>))</span></span>
<span><span>            {</span></span>
<span><span>                continue</span><span>;</span></span>
<span><span>            }</span></span>
<span><span>            dp</span><span>[x][y] </span><span>=</span><span> dp</span><span>[x</span><span>-</span><span>1</span><span>][y] </span><span>+</span><span> dp</span><span>[x][y</span><span>-</span><span>1</span><span>];</span></span>
<span><span>        }</span></span>
<span><span>    }</span></span>
<span><span>    return</span><span> dp</span><span>[m][n];</span></span>
<span></span>
<span><span>}</span></span></code></pre><p><img alt="Result" src="https://i.loli.net/2020/02/17/fAqpn4QsaLZKdO8.png"></p><h3 id="user-content-排列组合">排列组合<a class="" tabindex="-1" href="#排列组合">#</a></h3><p>这一题其实还可以用高中数学方法解决，因为这其实就是个排列组合问题。这个留给读者自己去想啦。
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>给个人博客添加后台管理友链与全站公告功能</title>
          <link>https://elliot00.com/posts/add-friendlink-notification</link>
          <description>这篇文章介绍了如何在个人博客网站上添加友情链接和全站公告通知功能。首先，作者创建了一个名为“extras”的 Django 应用，并添加了一个 FriendLink 模型来存储友情链接数据。然后，作者创建了一个 Django 模板标签，以便在需要显示友情链接的地方使用。接着，作者创建了另一个模型 SiteMessage 来存储全站公告数据，并同样创建了一个模板标签来显示全站公告。最后，作者还对网页结构进行了一些调整，使之更加协调。</description>
          <pubDate>Sat, 19 Dec 2020 05:52:33 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <p>给个人博客网站添加了友情链接与全站公告通知功能，写篇文章记录一下。
</p><p>首先考虑了使用Django的模板标签，也是为了练练手，相关知识可以去查看Django官方文档。下面是具体实现。
</p><h2 id="user-content-友情链接功能" class="">友情链接功能<a class="" tabindex="-1" href="#友情链接功能">#</a></h2><h3 id="user-content-模型">模型<a class="" tabindex="-1" href="#模型">#</a></h3><p>其实友情链接直接在首页页面html里面写就可以（到现在我还没有友链呢~），但是我想通过后台来管理这个数据，索性专门建了个名为=extras=的=app=。
</p><pre tabindex="0"><code><span><span>python</span><span> manage.py</span><span> startapp</span><span> extras</span></span></code></pre><p>编辑​<code class="">models.py</code>​文件：
</p><pre tabindex="0"><code><span><span>from</span><span> django</span><span>.</span><span>db </span><span>import</span><span> models</span></span>
<span><span>from</span><span> model_utils</span><span>.</span><span>models </span><span>import</span><span> TimeStampedModel</span></span>
<span><span>from</span><span> django</span><span>.</span><span>utils</span><span>.</span><span>translation </span><span>import</span><span> gettext_lazy </span><span>as</span><span> _</span></span>
<span></span>
<span><span># Create your models here.</span></span>
<span><span>class</span><span> FriendLink</span><span>(</span><span>TimeStampedModel</span><span>):</span></span>
<span><span>    site_name </span><span>=</span><span> models</span><span>.</span><span>CharField</span><span>(</span><span>_</span><span>(</span><span>'</span><span>site_name</span><span>'</span><span>)</span><span>,</span><span> max_length</span><span>=</span><span>100</span><span>)</span></span>
<span><span>    site_domain </span><span>=</span><span> models</span><span>.</span><span>URLField</span><span>(</span><span>_</span><span>(</span><span>'</span><span>site_domain</span><span>'</span><span>))</span></span>
<span></span>
<span><span>    def</span><span> __str__</span><span>(</span><span>self</span><span>)</span><span>:</span></span>
<span><span>        return</span><span> self</span><span>.</span><span>site_name</span></span></code></pre><p><code class="">TimeStampedModel</code>​是一个提供​<strong>自更新</strong>​的创建与修改字段的抽象基类。这里的​<code class="">gettext_lazy</code>​是用来做国际化的，对我的小破站其实没什么用，之所以有这个是因为这段是我从​<strong>追梦人物</strong>​的开源代码上复制来的，懒得改了。
</p><p>写完代码可别忘了模型​<strong>迁移</strong>​。
</p><h3 id="user-content-模板标签">模板标签<a class="" tabindex="-1" href="#模板标签">#</a></h3><p>这里使用的是​<strong>包含标签</strong>​，首先要在app目录下创建​<code class="">templatetags</code>​目录，并在其中创建​<code class="">__init__.py</code>​文件使之成为一个包。接着创建​<code class="">sidebar_tags.py</code>​，在其中编写我们的Python代码。
</p><pre tabindex="0"><code><span><span>from</span><span> django </span><span>import</span><span> template</span></span>
<span><span>from</span><span> ..</span><span>models </span><span>import</span><span> FriendLink</span></span>
<span></span>
<span></span>
<span><span>register </span><span>=</span><span> template</span><span>.</span><span>Library</span><span>()</span></span>
<span></span>
<span><span>@</span><span>register</span><span>.</span><span>inclusion_tag</span><span>(</span><span>'</span><span>inclusions/_friend_link.html</span><span>'</span><span>,</span><span> takes_context</span><span>=</span><span>True</span><span>)</span></span>
<span><span>def</span><span> show_friend_links</span><span>(</span><span>context</span><span>,</span><span> num</span><span>=</span><span>5</span><span>)</span><span>:</span></span>
<span><span>    friend_link_list </span><span>=</span><span> FriendLink</span><span>.</span><span>objects</span><span>.</span><span>all</span><span>()</span><span>[</span><span>:</span><span>num</span><span>]</span></span>
<span><span>    return</span><span> {</span></span>
<span><span>            '</span><span>friend_link_list</span><span>'</span><span>:</span><span> friend_link_list</span></span>
<span><span>    }</span></span></code></pre><p>默认显示5条数据，Django模板标签可以​<strong>接收参数</strong>​，像​<code class="">{% show_friend_links 5 %}</code>​这样使用。
</p><p>注意这里使用的是包含标签，​<code class="">@register.inclusion_tag('inclusions/_friend_link.html', takes_context=True)</code>​在装饰器里写上了需要包含的模板路径。包含标签通过渲染这个包含的模板去显示数据。
</p><p>所以要在放置模板的​<code class="">templates</code>​文件夹中新建相应的文件：
</p><pre tabindex="0"><code><span><span>&#x3C;</span><span>div</span><span> class</span><span>=</span><span>"</span><span>card mb-4 hvr-glow</span><span>"</span><span> style</span><span>=</span><span>"</span><span>display: flex</span><span>"</span><span>></span></span>
<span><span>    &#x3C;</span><span>div</span><span> class</span><span>=</span><span>"</span><span>card-body</span><span>"</span><span>></span></span>
<span><span>        &#x3C;</span><span>p</span><span> class</span><span>=</span><span>"</span><span>card-title text-muted</span><span>"</span><span>></span></span>
<span><span>            &#x3C;</span><span>i</span><span> class</span><span>=</span><span>"</span><span>fas fa-link</span><span>"</span><span> style</span><span>=</span><span>"</span><span>color: lightpink;</span><span>"</span><span>>&#x3C;/</span><span>i</span><span>></span><span> 友情链接</span></span>
<span><span>        &#x3C;/</span><span>p</span><span>></span></span>
<span><span>        {% if friend_link_list %}</span></span>
<span><span>            {% for link in friend_link_list %}</span></span>
<span><span>            &#x3C;</span><span>p</span><span> class</span><span>=</span><span>"</span><span>card-text hvr-forward col-12</span><span>"</span><span>></span></span>
<span><span>                &#x3C;</span><span>a</span><span> href</span><span>=</span><span>"</span><span>{{ link.site_domain }}</span><span>"</span><span> target</span><span>=</span><span>"</span><span>_blank</span><span>"</span><span> class</span><span>=</span><span>"</span><span>text-muted no-underline</span><span>"</span><span>></span></span>
<span><span>                    {{ link.site_name }}</span></span>
<span><span>                &#x3C;/</span><span>a</span><span>></span></span>
<span><span>            &#x3C;/</span><span>p</span><span>></span></span>
<span><span>            {% endfor %}</span></span>
<span><span>        {% endif %}</span></span>
<span><span>    &#x3C;/</span><span>div</span><span>></span></span>
<span><span>&#x3C;/</span><span>div</span><span>></span></span></code></pre><p>用了​<code class="">bootstrap</code>​的​<code class="">card</code>​样式，还是比较好看的。
</p><p>接下来在需要使用的地方用​<code class="">{% load sidebar_tags %}</code>​载入标签，在需要添加的地方使用​<code class="">{% show_friend_links %}</code>​就可以了。
</p><h2 id="user-content-全站公告功能" class="">全站公告功能<a class="" tabindex="-1" href="#全站公告功能">#</a></h2><h3 id="user-content-模型-1">模型<a class="" tabindex="-1" href="#模型-1">#</a></h3><pre tabindex="0"><code><span><span>class</span><span> SiteMessage</span><span>(</span><span>TimeStampedModel</span><span>):</span></span>
<span><span>    content </span><span>=</span><span> models</span><span>.</span><span>TextField</span><span>(</span><span>verbose_name</span><span>=</span><span>"</span><span>正文</span><span>"</span><span>)</span></span>
<span></span>
<span><span>    class</span><span> Meta</span><span>:</span></span>
<span><span>        verbose_name_plural </span><span>=</span><span> '</span><span>通知</span><span>'</span></span>
<span></span>
<span><span>    def</span><span> __str__</span><span>(</span><span>self</span><span>)</span><span>:</span></span>
<span><span>        return</span><span> self</span><span>.</span><span>content</span><span>[:</span><span>20</span><span>]</span></span></code></pre><p>同样使用​<code class="">TimeStampedModel</code>​省得写创建时间，程序员就是要偷懒，啦啦啦~
</p><p>同样地在写完后要​<strong>迁移</strong>​。
</p><h3 id="user-content-标签">标签<a class="" tabindex="-1" href="#标签">#</a></h3><pre tabindex="0"><code><span><span># 要加上导入这个SiteMessage类</span></span>
<span><span>from</span><span> ..</span><span>models </span><span>import</span><span> FriendLink</span><span>,</span><span> SiteMessage</span></span>
<span></span>
<span></span>
<span><span>@</span><span>register</span><span>.</span><span>inclusion_tag</span><span>(</span><span>'</span><span>inclusions/_site_message.html</span><span>'</span><span>,</span><span> takes_context</span><span>=</span><span>True</span><span>)</span></span>
<span><span>def</span><span> show_site_message</span><span>(</span><span>context</span><span>)</span><span>:</span></span>
<span><span>    try</span><span>:</span></span>
<span><span>        message </span><span>=</span><span> SiteMessage</span><span>.</span><span>objects</span><span>.</span><span>last</span><span>()</span></span>
<span><span>        data </span><span>=</span><span> {</span></span>
<span><span>                '</span><span>content</span><span>'</span><span>:</span><span> message</span><span>.</span><span>content</span><span>.</span><span>replace</span><span>(</span><span>"</span><span>\r\n</span><span>"</span><span>,</span><span> "</span><span>&#x3C;br/></span><span>"</span><span>),</span></span>
<span><span>                '</span><span>created</span><span>'</span><span>:</span><span> message</span><span>.</span><span>created</span><span>.</span><span>strftime</span><span>(</span><span>"</span><span>%Y/%m/</span><span>%d</span><span>"</span><span>),</span></span>
<span><span>        }</span></span>
<span><span>    except</span><span>:</span></span>
<span><span>        data </span><span>=</span><span> {</span></span>
<span><span>                "</span><span>content</span><span>"</span><span>:</span><span> "</span><span>o(╥﹏╥)o服务器连接失败~</span><span>"</span></span>
<span><span>        }</span></span>
<span><span>    return</span><span> data</span></span></code></pre><p>通过​<code class="">SiteMessage.objects.last()</code>​每次只取最新的数据。使用方法同上。
</p><h2 id="user-content-效果图" class="">效果图<a class="" tabindex="-1" href="#效果图">#</a></h2><p><img alt="效果图" src="https://i.loli.net/2019/12/29/Aflm5Dh6VZykFxs.png"></p><h2 id="user-content-关于网页结构" class="">关于网页结构<a class="" tabindex="-1" href="#关于网页结构">#</a></h2><p>为了让页面显示协调一点，我花了不少时间，前端真的是博大精深（太难了）。
</p><ol><li>最开始我有两个​<code class="">base.html</code>​文件，一个有侧边栏，一个没有，按需要继承。
</li><li>接着我将​<code class="">base.html</code>​修改，固定了侧边栏，放在​<code class="">{% block side %}{% endblock side %}</code>​中，并且将两个标签都放进去，在文章详情页面，侧边栏需要目录，目录下面用​<code class="">{% block.super %}</code>​来显示父级的内容。
</li><li>之前想着让目录侧边栏随着鼠标滚动固定在页面上，而另两个侧边内容自动消失，使用​<code class="">bootstrap</code>​的滚动监听，不过后来发现侧边栏内容还是要多显示比较好，与全都放到粘性侧边栏里了。
</li><li>最后还是只有一个​<code class="">base.html</code>​文件（写这篇文的时候发现我忘了删除没用的那个。。。），并且把侧边栏​<code class="">block</code>​给去掉了。
</li></ol><p>代码都放在了Github上，关注公众号在底部菜单查看Github地址。
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>刷题笔记0x06：双指针问题</title>
          <link>https://elliot00.com/posts/leetcode-double-pointer</link>
          <description>文章讨论了leetcode上双指针问题的解决方法和思路。对于三数之和问题，它首先介绍了暴力穷举和用哈希表换时间的方法，然后介绍了利用排序和双指针降低时间复杂度的方法以及一些需要注意的细节。接着，文章介绍了删除链表倒数第N个节点问题的解决方法，它首先介绍了两个指针跑的过程，然后介绍了注意链表长度为n，要求删除倒数第n的情况。最后，文章还提到了两个相似的题目：最接近的三数之和与四数之和。</description>
          <pubDate>Sat, 19 Dec 2020 05:52:03 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <p>最近在leetcode做了几题双指针题目，来做个总结。
</p><h2 id="user-content-三数之和" class="">三数之和<a class="" tabindex="-1" href="#三数之和">#</a></h2><blockquote><p>题目要求在有n个整数的列表中找出三个数相加刚好为0的所有解。详情见leetcode<a href="https://leetcode-cn.com/problems/3sum/">三数之和</a>。
</p></blockquote><h3 id="user-content-思路">思路<a class="" tabindex="-1" href="#思路">#</a></h3><p>首先这题暴力穷举是可以解决的，可以把所有组合都试一遍，但显然这样时间复杂度窜到O(n<sup>3</sup>)了。
</p><p>试着优化，可以试试用空间换时间，建个哈希表，遍历列表，将直接凑三个数简化为确定一个数，寻找能和它相加为0的两个数。这下时间复杂度降到了O(n<sup>2</sup>)，空间复杂度O(n)。
</p><p>别急，还可以想想办法节省空间。这里利用一下排序。先将整个列表按升序排序，对于这个排好序的列表而言：
</p><ul><li>某个元素​<code class="">nums[i]</code>​如果大于0，那么就可以直接返回了，因为是按升序排序的，后面的数只会更大，不可能相加等于0。
</li><li>当前值​<code class="">nums[i]</code>​,使左指针​<code class="">L</code>​为​<code class="">i+1</code>​，右指针​<code class="">R</code>​为​<code class="">len(nums) - 1</code>​。三个数相加，等于0就保存结果，大于0，那么右边值太大，R减一，反过来，小于0，左边值太小，L加一。
</li></ul><p>思路有了，剩下的就是可能还有一些重复值要去掉，避免重复计算，以及列表长度过小的情况。
</p><h3 id="user-content-具体代码">具体代码<a class="" tabindex="-1" href="#具体代码">#</a></h3><pre tabindex="0"><code><span><span>class</span><span> Solution</span><span>:</span></span>
<span><span>    def</span><span> threeSum</span><span>(</span><span>self</span><span>,</span><span> nums</span><span>:</span><span> List</span><span>[</span><span>int</span><span>]</span><span>)</span><span> -></span><span> List</span><span>[</span><span>List</span><span>[</span><span>int</span><span>]]</span><span>:</span></span>
<span><span>        n</span><span>=</span><span>len</span><span>(</span><span>nums</span><span>)</span></span>
<span><span>        res</span><span>=</span><span>[]</span></span>
<span><span>        if</span><span> not</span><span> nums </span><span>or</span><span> n </span><span>&#x3C;</span><span> 3</span><span>:</span></span>
<span><span>            return</span><span> []</span></span>
<span><span>        nums</span><span>.</span><span>sort</span><span>()</span></span>
<span><span>        res</span><span>=</span><span>[]</span></span>
<span><span>        for</span><span> i </span><span>in</span><span> range</span><span>(</span><span>n</span><span>):</span></span>
<span><span>            if</span><span> nums</span><span>[</span><span>i</span><span>]</span><span> ></span><span> 0</span><span>:</span></span>
<span><span>                return</span><span> res</span></span>
<span><span>            if</span><span> i </span><span>></span><span> 0</span><span> and</span><span> nums</span><span>[</span><span>i</span><span>]</span><span> ==</span><span> nums</span><span>[</span><span>i</span><span>-</span><span>1</span><span>]:</span></span>
<span><span>                continue</span></span>
<span><span>            L</span><span>=</span><span>i</span><span>+</span><span>1</span></span>
<span><span>            R</span><span>=</span><span>n</span><span>-</span><span>1</span></span>
<span><span>            while</span><span> L </span><span>&#x3C;</span><span> R</span><span>:</span></span>
<span><span>                if</span><span> nums</span><span>[</span><span>i</span><span>]</span><span> +</span><span> nums</span><span>[</span><span>L</span><span>]</span><span> +</span><span> nums</span><span>[</span><span>R</span><span>]</span><span> ==</span><span> 0</span><span>:</span></span>
<span><span>                    res</span><span>.</span><span>append</span><span>(</span><span>[</span><span>nums</span><span>[</span><span>i</span><span>]</span><span>, nums</span><span>[</span><span>L</span><span>]</span><span>, nums</span><span>[</span><span>R</span><span>]</span><span>]</span><span>)</span></span>
<span><span>                    while</span><span> L </span><span>&#x3C;</span><span> R </span><span>and</span><span> nums</span><span>[</span><span>L</span><span>]</span><span> ==</span><span> nums</span><span>[</span><span>L</span><span>+</span><span>1</span><span>]:</span></span>
<span><span>                        L</span><span>=</span><span>L</span><span>+</span><span>1</span></span>
<span><span>                    while</span><span> L </span><span>&#x3C;</span><span> R </span><span>and</span><span> nums</span><span>[</span><span>R</span><span>]</span><span> ==</span><span> nums</span><span>[</span><span>R</span><span>-</span><span>1</span><span>]:</span></span>
<span><span>                        R</span><span>=</span><span>R</span><span>-</span><span>1</span></span>
<span><span>                    L</span><span>=</span><span>L</span><span>+</span><span>1</span></span>
<span><span>                    R</span><span>=</span><span>R</span><span>-</span><span>1</span></span>
<span><span>                elif</span><span> nums</span><span>[</span><span>i</span><span>]</span><span> +</span><span> nums</span><span>[</span><span>L</span><span>]</span><span> +</span><span> nums</span><span>[</span><span>R</span><span>]</span><span> ></span><span> 0</span><span>:</span></span>
<span><span>                    R</span><span>=</span><span>R</span><span>-</span><span>1</span></span>
<span><span>                else</span><span>:</span></span>
<span><span>                    L</span><span>=</span><span>L</span><span>+</span><span>1</span></span>
<span><span>        return</span><span> res</span></span></code></pre><h3 id="user-content-相似题目">相似题目<a class="" tabindex="-1" href="#相似题目">#</a></h3><p>leetcode上还有个最接近的三数之和与四数之和，思路都差不多，可以去尝试一下。
</p><h2 id="user-content-删除链表倒数第n个节点" class="">删除链表倒数第N个节点<a class="" tabindex="-1" href="#删除链表倒数第n个节点">#</a></h2><blockquote><p>题目要求给定链表与要n，删去倒数第n个元素并且返回链表头。详情见<a href="https://leetcode-cn.com/problems/remove-nth-node-from-end-of-list/">删除链表的倒数第N个节点</a></p></blockquote><h3 id="user-content-思路-1">思路<a class="" tabindex="-1" href="#思路-1">#</a></h3><p>这个问题首先想到双指针解决，建立​<code class="">slow</code>​和​<code class="">fast</code>​两个指针。
</p><ul><li>两个指针都指向链表头部，让​<em>fast</em>​先跑​<em>n</em>​步，然后两个指针再一起跑。
</li><li><em>fast</em>​到末尾时，​<em>slow</em>​刚好就是倒数第​<code class="">n+1</code>​个元素。让​<code class="">slow.next</code>​指向倒数第​<code class="">n-1</code>​，也就是​<code class="">slow.next = slow.next.next</code>​，即完成删除。
</li><li>注意在让fast先走n步过程中，如果​<code class="">fast.next</code>​存在，则fast前进直到前进n步为止，否则​<code class="">fast.next</code>​不存在就返回​<code class="">head.next</code>​。这么做的原因是存在链表长度为n，要求删除倒数第n的情况。
</li></ul><h3 id="user-content-具体代码-1">具体代码<a class="" tabindex="-1" href="#具体代码-1">#</a></h3><pre tabindex="0"><code><span><span>class</span><span> Solution</span><span>:</span></span>
<span><span>    def</span><span> removeNthFromEnd</span><span>(</span><span>self</span><span>,</span><span> head</span><span>:</span><span> ListNode</span><span>,</span><span> n</span><span>:</span><span> int</span><span>)</span><span> -></span><span> ListNode:</span></span>
<span><span>        if</span><span> not</span><span> head </span><span>or</span><span> not</span><span> head</span><span>.</span><span>next</span><span>:</span></span>
<span><span>            return</span><span> None</span></span>
<span><span>        fast</span><span>,</span><span> slow </span><span>=</span><span> head</span><span>,</span><span> head</span></span>
<span><span>        for</span><span> i </span><span>in</span><span> range</span><span>(</span><span>n</span><span>):</span></span>
<span><span>            if</span><span> fast</span><span>.</span><span>next</span><span>:</span></span>
<span><span>                fast </span><span>=</span><span> fast</span><span>.</span><span>next</span></span>
<span><span>            else</span><span>:</span></span>
<span><span>                return</span><span> head</span><span>.</span><span>next</span></span>
<span><span>        while</span><span> fast</span><span>.</span><span>next</span><span>:</span></span>
<span><span>            fast </span><span>=</span><span> fast</span><span>.</span><span>next</span></span>
<span><span>            slow </span><span>=</span><span> slow</span><span>.</span><span>next</span></span>
<span><span>        slow</span><span>.</span><span>next </span><span>=</span><span> slow</span><span>.</span><span>next</span><span>.</span><span>next</span></span>
<span><span>        return</span><span> head</span></span></code></pre>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>刷题笔记0x08:石子游戏</title>
          <link>https://elliot00.com/posts/leetcode-stone-game</link>
          <description>这篇文章给出了一个动态规划算法来求解游戏“Nim”的必胜策略，这个问题的条件是：有两堆石子，两人轮流任意取走一堆中任意数量的石子，最后不能取走石子的人输掉游戏。文章旨在回答谁能够赢下游戏，即谁能够让对手不能取走石子。该算法使用了一个三维数组 `dp[i][j][2]` 来存储游戏状态，其中 `i` 表示先手能取石子的位置的最小值，`j` 表示先手能取石子的位置的最大值，`2` 表示先手和后手两种情况。算法通过递推的方式来计算 `dp` 数组，最终得出先手能否赢下游戏的结论。</description>
          <pubDate>Sat, 19 Dec 2020 05:51:10 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <h2 id="user-content-题目描述" class="">题目描述<a class="" tabindex="-1" href="#题目描述">#</a></h2><p><img src="https://pic2.zhimg.com/v2-ba3e5d4aa7443e692cf3190702ec5f85_b.png"></p><h2 id="user-content-思路" class="">思路<a class="" tabindex="-1" href="#思路">#</a></h2><p>首先，我们要求的答案是两人中谁会赢，或者说最终谁手中的石子数多。
</p><p>其次，我们知道两人都是高手，将发挥出最高水平，并且石子总数为奇数，一定会有人赢。
</p><p>那么让我们假设f(i, j)为对于一个序列索引i到j区间内，先手玩家最大所得与后手玩家最大所得的差值。
</p><p>先假设个数组piles为[2, 7, 3, 9]的例子画个图看看。
</p><p><img src="https://pic4.zhimg.com/v2-8a74aaa0941d03d6e21a76b7384baba3_b.png"></p><p>先不急着把所有情况填满，显而易见的是，对于​<code class="">i = j</code>​的情况，差值显然就等于piles[i]或者说piles[j]了（因为这种情况区间里就​<strong>一堆</strong>​石头啊，后手根本没得拿）。
</p><p>另外还可以发现，类似i = 1, j = 3和i = 3, j = 1的情况，它们表示的是同样的区间，所以我们可以抛弃表格左下角，并假定​<code class="">0 &#x3C;= i &#x3C;= size &#x26;&#x26; i &#x3C;= j &#x3C;= size</code>​，其中size表示题目给定的数组长度。
</p><p><img src="https://pic2.zhimg.com/v2-4b95e8c6c3c977574b2030635c34faed_b.png"></p><p>但是这样好像不太好总结规律，让我们稍微调整一下表格，每个表格里放​<strong>两个数</strong>​，分别表示先手可得的石子数和后手可得的石子数。
</p><p><img src="https://pic4.zhimg.com/v2-ff62183f79758e3faa249c0451f0009b_b.png"></p><p>对于先手而言，只有两种选择，拿左边的或者右边的，我们设置一个三维数组dp[i][j][2]，dp[i][j][0]表示先手在区间内可得石子数，dp[i][j][1]就是后手的了。
</p><pre tabindex="0"><code><span><span>dp</span><span>[i][j][</span><span>0</span><span>] </span><span>=</span><span> max</span><span>(</span><span>piles</span><span>[i] </span><span>+</span><span> dp</span><span>[i</span><span>-</span><span>1</span><span>][j][</span><span>1</span><span>]</span><span>,</span><span> piles</span><span>[j] </span><span>+</span><span> dp</span><span>[i][j</span><span>-</span><span>1</span><span>][</span><span>1</span><span>]);</span></span>
<span><span>/*</span></span>
<span><span>当我是先手时，比如说我拿走最右边一堆piles[j]，</span></span>
<span><span>那么当前情况,相当于是对方是先手（dp[i][j-1][0]）</span></span>
<span><span>而我变成dp[i][j-1]情况下的后手</span></span>
<span><span>拿左边同理</span></span>
<span><span>*/</span></span>
<span></span>
<span><span>// 先手拿了左边时，后手是[i-1][j]情况下的先手</span></span>
<span><span>dp</span><span>[i][j][</span><span>1</span><span>] </span><span>=</span><span> dp</span><span>[i</span><span>-</span><span>1</span><span>][j][</span><span>0</span><span>];</span></span>
<span><span>// 同理先手拿走右边时</span></span>
<span><span>dp</span><span>[i][j][</span><span>1</span><span>] </span><span>=</span><span> dp</span><span>[i][j</span><span>-</span><span>1</span><span>][</span><span>0</span><span>];</span></span>
<span></span>
<span><span>// 再回到我们最开始发现的规律</span></span>
<span><span>// if i == j: 先手拿走唯一的那堆石头</span></span>
<span><span>dp</span><span>[i][j][</span><span>0</span><span>] </span><span>=</span><span> piles</span><span>[i];</span></span>
<span><span>dp</span><span>[i][j][</span><span>1</span><span>] </span><span>=</span><span> 0</span><span>;</span></span></code></pre><h2 id="user-content-代码实现" class="">代码实现<a class="" tabindex="-1" href="#代码实现">#</a></h2><p>具体写代码时，我们要得到的答案就是表格的右上角dp[i][j][0] > dp[i][j][1]这个表达式是否为true，也就是先手是不是比后手拿的石头多。为此，我们可以试着斜着遍历数组直到右上角。
</p><pre tabindex="0"><code><span><span>bool</span><span> stoneGame</span><span>(</span><span>int</span><span>*</span><span> piles</span><span>,</span><span> int</span><span> pilesSize</span><span>)</span></span>
<span><span>{</span></span>
<span><span>    int</span><span> dp</span><span>[pilesSize][pilesSize][</span><span>2</span><span>];</span></span>
<span><span>    memset</span><span>(dp</span><span>,</span><span> 0</span><span>,</span><span> sizeof</span><span>(dp))</span><span>;</span></span>
<span></span>
<span><span>    // 基准情况</span></span>
<span><span>    for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> 0</span><span>; i </span><span>&#x3C;</span><span> pilesSize; i</span><span>++</span><span>)</span></span>
<span><span>    {</span></span>
<span><span>        dp</span><span>[i][i][</span><span>0</span><span>] </span><span>=</span><span> piles</span><span>[i];</span></span>
<span><span>        dp</span><span>[i][i][</span><span>1</span><span>] </span><span>=</span><span> 0</span><span>;</span></span>
<span><span>    }</span></span>
<span></span>
<span><span>    // 斜着遍历数组</span></span>
<span><span>    for</span><span> (</span><span>int</span><span> l </span><span>=</span><span> 2</span><span>; l </span><span>&#x3C;=</span><span> pilesSize; l</span><span>++</span><span>)</span></span>
<span><span>    {</span></span>
<span><span>        for</span><span> (</span><span>int</span><span> i </span><span>=</span><span> 0</span><span>; i </span><span>&#x3C;=</span><span> pilesSize </span><span>-</span><span> l; i</span><span>++</span><span>)</span></span>
<span><span>        {</span></span>
<span><span>            int</span><span> j </span><span>=</span><span> l </span><span>+</span><span> i </span><span>-</span><span> 1</span><span>;</span></span>
<span><span>            int</span><span> left </span><span>=</span><span> piles</span><span>[i] </span><span>+</span><span> dp</span><span>[i</span><span>+</span><span>1</span><span>][j][</span><span>1</span><span>];</span></span>
<span><span>            int</span><span> right </span><span>=</span><span> piles</span><span>[j] </span><span>+</span><span> dp</span><span>[i][j</span><span>-</span><span>1</span><span>][</span><span>1</span><span>];</span></span>
<span><span>            // 省去写一个max函数</span></span>
<span><span>            if</span><span> (left </span><span>></span><span> right)</span></span>
<span><span>            {</span></span>
<span><span>                dp</span><span>[i][j][</span><span>0</span><span>] </span><span>=</span><span> left;</span></span>
<span><span>                dp</span><span>[i][j][</span><span>1</span><span>] </span><span>=</span><span> dp</span><span>[i</span><span>+</span><span>1</span><span>][j][</span><span>0</span><span>];</span></span>
<span><span>            }</span></span>
<span><span>            else</span></span>
<span><span>            {</span></span>
<span><span>                dp</span><span>[i][j][</span><span>0</span><span>] </span><span>=</span><span> right;</span></span>
<span><span>                dp</span><span>[i][j][</span><span>1</span><span>] </span><span>=</span><span> dp</span><span>[i][j</span><span>-</span><span>1</span><span>][</span><span>0</span><span>];</span></span>
<span><span>            }</span></span>
<span><span>        }</span></span>
<span><span>    }</span></span>
<span><span>    return</span><span> dp</span><span>[</span><span>0</span><span>][pilesSize</span><span>-</span><span>1</span><span>][</span><span>0</span><span>] </span><span>></span><span> dp</span><span>[</span><span>0</span><span>][pilesSize</span><span>-</span><span>1</span><span>][</span><span>1</span><span>];</span></span>
<span><span>}</span></span></code></pre><h2 id="user-content-后记" class="">后记<a class="" tabindex="-1" href="#后记">#</a></h2><p>这道题的条件我隐约觉得有些不对劲，翻了一下评论，果然，这个游戏居然​<strong>先手必胜</strong>​，也就是说这题有个时间复杂度为O(1)的解法：
</p><div>return true;
</div><p><img src="https://pic1.zhimg.com/v2-6161495841dbf245d807a23efc452ec8_b.jpg"></p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>Django+React全栈开发：代码组织</title>
          <link>https://elliot00.com/posts/react-django-code-organization</link>
          <description>这篇文章讨论了在 JavaScript 中使用 ES6 模块组织代码的方法，包括导入和导出模块、模块的命名和别名、以及如何将代码组织成不同的模块。文章还介绍了在 React 中如何将组件拆分成不同的模块，以及如何使用默认导出的文件。</description>
          <pubDate>Sat, 19 Dec 2020 05:42:22 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <blockquote><p>好久没有更新博客了，现在终于有时间写了，以后尽量周更
</p></blockquote><h2 id="user-content-es6模块" class="">ES6模块<a class="" tabindex="-1" href="#es6模块">#</a></h2><p>习惯使用​<code class="">Django</code>​框架后，对于模块化编程的好处想必大家都深有体会，尤其是当你曾经将大量的逻辑写在同一个文件甚至同一个函数中，到了某个时间点需要去修改这个程序中某个功能的时候。
</p><p>在​<code class="">JavaScript</code>​中，我们也可以利用模块分割代码，优化我们的应用结构。以之前的代码为例：
</p><pre tabindex="0"><code><span><span>import</span><span> {</span></span>
<span><span>  Switch</span><span>,</span></span>
<span><span>  Link</span><span>,</span></span>
<span><span>  Route</span></span>
<span><span>} </span><span>from</span><span> "</span><span>react-router-dom</span><span>"</span></span>
<span><span>import</span><span> About </span><span>from</span><span> "</span><span>./About</span><span>"</span></span>
<span><span>import</span><span> ArticleDetail </span><span>from</span><span> "</span><span>./ArticleDetail</span><span>"</span></span></code></pre><p><code class="">ES6</code>​有​<code class="">import</code>​与​<code class="">export</code>​语句对应导入与导出，值得注意的是，以上代码中可以看到​<code class="">import</code>​语句有两种不同的写法，让我们先来看看如何导出：
</p><pre tabindex="0"><code><span><span>// a.js</span></span>
<span><span>const</span><span> mode</span><span> =</span><span> '</span><span>default</span><span>'</span></span>
<span><span>const</span><span> age</span><span> =</span><span> 54</span></span>
<span></span>
<span><span>export</span><span> {mode</span><span>,</span><span> age}</span></span>
<span></span>
<span><span>// 也可以在定义变量时直接导出</span></span>
<span><span>export</span><span> foo = () => {</span></span>
<span><span>    console.log(</span><span>"hello"</span><span>)</span></span>
<span><span>}</span></span>
<span></span>
<span><span>// 以上导出方式对应的导入</span></span>
<span><span>import</span><span> { mode</span><span>,</span><span> age } </span><span>from</span><span> '</span><span>./b.js</span><span>'</span></span>
<span></span>
<span><span>// 可以使用通配符*与as将a中导出的全部内容导入为一个对象</span></span>
<span><span>import</span><span> *</span><span> as</span><span> Foo </span><span>from</span><span> '</span><span>./b.js</span><span>'</span></span>
<span></span>
<span><span>// 使用as避免命名冲突或避免过长的名称</span></span>
<span><span>import</span><span> { </span><span>mode</span><span> as</span><span> aMode } </span><span>from</span><span> '</span><span>./b.js</span><span>'</span></span></code></pre><p>以上导出方式对应了我们之前代码中​<strong>需要花括号</strong>​的导入方式，还有一种​<code class="">default</code>​语句：
</p><pre tabindex="0"><code><span><span>const</span><span> User</span><span> =</span><span> {</span></span>
<span><span>    username</span><span>:</span><span> '</span><span>elliot</span><span>'</span><span>,</span></span>
<span><span>    email</span><span>:</span><span> '</span><span>hack00mind@gmail.com</span><span>'</span></span>
<span><span>}</span></span>
<span></span>
<span><span>// 注意一个文件中只能有一个export default</span></span>
<span><span>export</span><span> default</span><span> User</span></span>
<span></span>
<span><span>// 但是可以与export一起用</span></span>
<span><span>export</span><span> const</span><span> year</span><span> =</span><span> 2020</span></span>
<span></span>
<span><span>// 导入default的名称可以省略花括号</span></span>
<span><span>import</span><span> User</span><span>,</span><span> { year } </span><span>from</span><span> '</span><span>./foo.js</span><span>'</span></span></code></pre><h2 id="user-content-代码组织" class="">代码组织<a class="" tabindex="-1" href="#代码组织">#</a></h2><p>在​<code class="">React</code>​中，我们将页面拆分成多个不同的​<code class="">组件</code>​，我们已经大致将不同的功能、不同页面的组件放到了不同文件中，这些代码都在​<code class="">src</code>​目录下，但在工作中，随着业务的增长，我们要考虑将组件拆分到更多不同的模块中去。
</p><div>src
├── About.js
├── App.css
├── App.js
├── App.test.js
├── ArticleDetail.css
├── ArticleDetail.js
├── ArticleList.css
├── ArticleList.js
├── index.css
├── index.js
├── logo.svg
├── serviceWorker.js
└── setupTests.js
</div><p>现在我们的文件已经略显凌乱了，虽然我们已经将一些不同的组件拆分到了不同的文件中，但是不同名称的不同文件混在了一起，看上去还是不舒服。
</p><p>我们可以将同一个组件相关的代码文件，样式文件，测试文件放到同一个文件夹中：
</p><div>├── About
│   └── index.js
├── App
│   ├── index.css
│   ├── test.js
│   └── index.js
├── ArticleDetail
│   ├── index.css
│   └── index.js
├── ArticleList
│   ├── index.css
│   └── index.js
├── index.css
├── index.js
├── logo.svg
├── serviceWorker.js
└── setupTests.js
</div><p>现在我们的文件在结构上要更加清晰些，或者还可以如下所示：
</p><div>├── components
│   ├── About
│   │   └── index.js
│   ├── App
│   │   ├── index.css
│   │   ├── test.js
│   │   └── index.js
│   ├── ArticleDetail
│   │   ├── index.css
│   │   └── index.js
│   └── ArticleList
│       ├── index.css
│       └── index.js
├── constants
│   └── index.js
├── index.css
├── index.js
├── logo.svg
├── serviceWorker.js
└── setupTests.js
</div><p>注意到原本的组件文件名都被改成了​<code class="">index.js</code>​，这是​<code class="">node</code>​项目中入口文件的默认文件名，当然你可以改成自己想要的其它名称。当使用这个默认名称时，在导入时可以省略​<code class="">index.js</code>​：
</p><pre tabindex="0"><code><span><span>import</span><span> React </span><span>from</span><span> '</span><span>react</span><span>'</span></span>
<span><span>import</span><span> {</span></span>
<span><span>  Switch</span><span>,</span></span>
<span><span>  Link</span><span>,</span></span>
<span><span>  Route</span></span>
<span><span>} </span><span>from</span><span> "</span><span>react-router-dom</span><span>"</span></span>
<span></span>
<span><span>// 注意文件位置变动，引入时相对路径要修改</span></span>
<span><span>import</span><span> '</span><span>./App.css</span><span>'</span></span>
<span><span>import</span><span> ArticleList </span><span>from</span><span> "</span><span>../ArticleList</span><span>"</span></span>
<span><span>import</span><span> About </span><span>from</span><span> "</span><span>../About</span><span>"</span></span>
<span><span>import</span><span> ArticleDetail </span><span>from</span><span> "</span><span>../ArticleDetail</span><span>"</span></span></code></pre><p>考虑下列场景：
</p><pre tabindex="0"><code><span><span>/*</span></span>
<span><span>文件结构如下</span></span>
<span><span>./Buttons</span></span>
<span><span>├── CancelButton.js</span></span>
<span><span>├── index.js</span></span>
<span><span>└── SubmitButton.js</span></span>
<span><span>*/</span></span>
<span></span>
<span><span>// Buttons/index.js</span></span>
<span><span>import</span><span> CancelButton </span><span>from</span><span> '</span><span>./CancelButton</span><span>'</span></span>
<span><span>import</span><span> SubmitButton </span><span>from</span><span> '</span><span>./SubmitButton</span><span>'</span></span>
<span></span>
<span><span>export</span><span> {</span></span>
<span><span>    CancelButton</span><span>,</span></span>
<span><span>    SubmitButton</span><span>,</span></span>
<span><span>}</span></span>
<span></span>
<span><span>// 在其他文件中可以如此引入</span></span>
<span><span>import</span><span> { CancelButton</span><span>,</span><span> SubmitButton } </span><span>from</span><span> '</span><span>../Buttons</span><span>'</span></span></code></pre>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>Ant Design Pro页面内切换组件问题</title>
          <link>https://elliot00.com/posts/react-dynamic-import</link>
          <description>这篇文章主要讨论了在 Ant Design Pro 中使用 Tab 组件切换不同详情内容的实现方法。作者首先介绍了遇到的问题，然后提出了几种可能的解决方案，最终采用结合 React 的 Suspense 和 React.lazy 实现动态引入子组件的方法。文章还讨论了在工程实践中权衡项目进度与代码优化的重要性。</description>
          <pubDate>Sat, 19 Dec 2020 05:41:43 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <h2 id="user-content-目的" class="">目的<a class="" tabindex="-1" href="#目的">#</a></h2><p>最近在项目中要使用​<code class="">Ant Design Pro</code>​，页面布局大致如图：
</p><p><img alt="layout" src="https://i.loli.net/2020/09/12/N8V6mHXoBpKTkfD.png"></p><p>在详情页中需要显示非常复杂的数据，想要的效果是点击主要内容区域的​<code class="">tab</code>​可以切换到对应的详情内容。
</p><h2 id="user-content-问题" class="">问题<a class="" tabindex="-1" href="#问题">#</a></h2><p>我们在每个页面中都使用了框架提供的​<code class="">PageHeaderWrapper</code>​这个组件，如上图的​<em>详情</em>​，​<em>规则</em>​这两个​<code class="">tab</code>​，但是可能是因为我们使用的版本比较老，一直没搜到这个组件相关​<code class="">API</code>​的信息，只了解到可以通过向​<code class="">PageHeaderWrapper</code>​提供​<code class="">tabList</code>​这个​<code class="">props</code>​来显示​<code class="">Tab</code>​，​<code class="">tabActiveKey</code>​设置激活的​<code class="">Tab</code>​，同时有​<code class="">onTabChange</code>​这个回调。
</p><p>现在点击切换​<code class="">Tab</code>​已经可以实现，但是​<strong>如何让内页面主要内容随着Tab切换而改变呢</strong>​？我对前端还不够熟悉，对​<code class="">Ant Design Pro</code>​的路由了解也不深，大概想到这么几个办法：
</p><ol><li>在框架规定的​<code class="">config.js</code>​中配置路由，添加多个路由，Tab下包含Link，点击切换到对应页面
</li><li>维护一个​<code class="">state</code>​，不同的详情组件都放在=PageHeaderWrapper=中，切换Tab改变组件的​<code class="">display</code>​属性
</li><li>直接用​<code class="">state</code>​维护子组件，切换Tab改变​<code class="">state</code>​，实现子组件的切换
</li></ol><p>由于要切换显示的详情内容其实都来自同一个​<code class="">API</code>​一次提供的数据，如​<code class="">cloth/1</code>​，这里面可能包含了很复杂的数据，只是为了在视觉显示上区分开来，如果采用方法1,似乎就要有三个不同的页面，每个页面都要单独请求一次后端数据，方法2和3思路差不多，都是通过​<code class="">state</code>​来动态改变显示效果，但是总觉得实现起来不够优雅，会给后期维护带来麻烦。主要还是对前端了解不够深。
</p><h2 id="user-content-最终解决方案" class="">最终解决方案<a class="" tabindex="-1" href="#最终解决方案">#</a></h2><p>参考了<a href="https://tuohuang.info/ant-design-tab-navigation.html#.X1w9vnUzbeQ">一篇文章</a>，感觉我可以结合思路2、3与​<code class="">React</code>​的<a href="https://reactjs.org/docs/code-splitting.html#route-based-code-splitting">Suspense</a>来做。
</p><pre tabindex="0"><code><span><span>import</span><span> React</span><span>,</span><span> { Suspense } </span><span>from</span><span> '</span><span>react</span><span>'</span><span>;</span></span>
<span></span>
<span><span>const</span><span> OtherComponent</span><span> =</span><span> React</span><span>.</span><span>lazy</span><span>(</span><span>()</span><span> =></span><span> import</span><span>(</span><span>'</span><span>./OtherComponent</span><span>'</span><span>));</span></span>
<span></span>
<span><span>function</span><span> MyComponent</span><span>()</span><span> {</span></span>
<span><span>  return</span><span> (</span></span>
<span><span>    &#x3C;</span><span>div</span><span>></span></span>
<span><span>      &#x3C;</span><span>Suspense</span><span> fallback</span><span>=</span><span>{</span><span>&#x3C;</span><span>div</span><span>></span><span>Loading...</span><span>&#x3C;/</span><span>div</span><span>></span><span>}</span><span>></span></span>
<span><span>        &#x3C;</span><span>OtherComponent</span><span> /></span></span>
<span><span>      &#x3C;/</span><span>Suspense</span><span>></span></span>
<span><span>    &#x3C;/</span><span>div</span><span>></span></span>
<span><span>  );</span></span>
<span><span>}</span></span></code></pre><p>如上，可以借助​<code class="">React.lazy</code>​实现动态引入，并将这种*“懒加载”*的组件包裹在​<code class="">Suspense</code>​中渲染，最终我的实现方法大致如下：
</p><pre tabindex="0"><code><span><span>// 省略多余代码，将tab切换后的key值塞入state中</span></span>
<span><span>const</span><span> handleTabChange</span><span> =</span><span> key</span><span> =></span><span> {</span></span>
<span><span>    this</span><span>.</span><span>setState</span><span>(</span><span>{</span><span>activeKey</span><span>:</span><span> key</span><span>}</span><span>)</span></span>
<span><span>}</span></span>
<span></span>
<span><span>render</span><span>() {</span></span>
<span><span>    // 省略</span></span>
<span><span>    const</span><span> {</span><span> activeKey</span><span> }</span><span> =</span><span> this</span><span>.</span><span>state</span></span>
<span><span>    const</span><span> Child</span><span> =</span><span> React</span><span>.</span><span>lazy</span><span>(</span><span>()</span><span> =></span><span> import</span><span>(</span><span>`</span><span>./</span><span>${</span><span>activeKey</span><span>}</span><span>`</span><span>))</span></span>
<span><span>    return</span><span> (</span></span>
<span><span>        &#x3C;</span><span>PageHeaderWrapper</span><span> ......></span></span>
<span><span>            &#x3C;Suspense</span><span> fallback</span><span>=</span><span>{</span><span>&#x3C;</span><span>div</span><span>></span><span>Loading...</span><span>&#x3C;/</span><span>div</span><span>></span><span>}</span><span>></span></span>
<span><span>                &#x3C;</span><span>Child</span><span> data</span><span>=</span><span>{</span><span>data</span><span>}</span><span> /></span></span>
<span><span>            &#x3C;/</span><span>Suspense</span><span>></span></span>
<span><span>        &#x3C;/</span><span>PageHeaderWrapper</span><span>></span></span>
<span><span>    )</span></span>
<span><span>}</span></span></code></pre><p>以上是大致的实现，主要就是根据切换tab后的​<code class="">key</code>​值，动态引入对应的子组件并渲染。主要就是为了将不同的模块分离开，提高复用性吧。当然我的做法可能有问题，毕竟对​<code class="">React</code>​和​<code class="">Ant Design Pro</code>​了解还不够深入，权当抛砖引玉了。
</p><h2 id="user-content-思考" class="">思考<a class="" tabindex="-1" href="#思考">#</a></h2><p>在实践的过程中，经常有可能只是要写个简单的功能，我们会随意写个函数，最终随着功能的复杂化，可能我们的代码量越来越大，有时候就会见到“超级文件”、“超级函数”这样的代码，给维护和复用带来很多困难。其实对于工程实践中的问题，我们常常听到很多名词，诸如面向对象、函数式编程、模块化编程、TDD，以及语言层面，例如​<code class="">JavaScript</code>​的超集​<code class="">TypeScript</code>​，​<code class="">Python</code>​中的类型注解，​<code class="">Rust</code>​的​<strong>所有权</strong>​等等，很多东西在项目前期往往看不到好处，反而让程序员觉得浪费时间，但是也许在后期这些东西能避免很多问题。但是也常有人说，不要​<strong>过早优化</strong>​，如何权衡或者说掌握好项目进度与代码优化的之间的平衡，这是个值得思考的问题。
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>Serverless学习笔记0x00</title>
          <link>https://elliot00.com/posts/aws-lambda</link>
          <description>本文介绍了 AWS Lambda、触发器、SAM、共享依赖、Fastapi 等内容。AWS Lambda 是一种无服务器计算服务，无需预置或管理服务器即可运行代码。触发器负责根据不同方式调用函数，如 API Gateway、CloudWatch Events 等。SAM（AWS Serverless Application Model）是一种脚手架工具，可以帮助快速构建所需的 Serverless 应用。共享依赖功能允许多个函数共享相同的第三方库或通用代码，无需在每个函数中重复上传依赖。Fastapi 是一个 Python Web 框架，可以用来构建 RESTful API。文章描述了如何将一个本地 Fastapi 项目快速迁移到 Serverless 架构中。最后，文章列出了一些需要进一步了解的问题，如基于 Lambda 的身份验证、Websocket API、持续集成等。</description>
          <pubDate>Sat, 19 Dec 2020 05:41:09 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <h2 id="user-content-aws-lambda" class="">AWS Lambda<a class="" tabindex="-1" href="#aws-lambda">#</a></h2><p>最近在工作中接触到​<code class="">Serverless</code>​架构，学习了一些AWS相关的服务，为了避免遗忘，在这里先记录一下目前的收获。
</p><p><strong>AWS</strong>​的官方文档第一句话就为​<code class="">AWS Lambda</code>​做了一个简单介绍：
</p><blockquote><p>AWS Lambda is a compute service that lets you run code without provisioning or managing servers.
</p></blockquote><p>无服务并不是不需要服务器，而是可以让开发者更加专注于业务代码，而非程序运行的实际环境。无服务器计算有多种不同形式，​<code class="">AWS Lambda</code>​则属于其中的​<strong>FaaS</strong>​（函数即是服务）。一个Lambda Function就是一个服务单元，可以将传统的后端拆分到很小的粒度，将针对某个资源的CRUD操作拆分成Function，想要执行对应的操作就去触发对应的函数。Lambda提供对​<code class="">Python</code>​、​<code class="">JavaScript</code>​、​<code class="">C#</code>​等语言的支持，既然底层要调用我们所写的的代码，自然要以一种callback的形式来编写：
</p><pre tabindex="0"><code><span><span>def</span><span> lambda_handler</span><span>(</span><span>event</span><span>,</span><span> context</span><span>)</span><span>:</span></span>
<span><span>    # </span><span>TODO</span><span> implement</span></span>
<span><span>    return</span><span> {</span></span>
<span><span>        '</span><span>statusCode</span><span>'</span><span>:</span><span> 200</span><span>,</span></span>
<span><span>        '</span><span>body</span><span>'</span><span>:</span><span> json</span><span>.</span><span>dumps</span><span>(</span><span>"</span><span>Hello AWS Lambda</span><span>"</span><span>)</span></span>
<span><span>    }</span></span></code></pre><p>关于Serverless的优缺点网络上讨论很多，关键还是根据实际情况决定是否使用，就不赘述了。
</p><h2 id="user-content-触发器" class="">触发器<a class="" tabindex="-1" href="#触发器">#</a></h2><p><img alt="测试" src="https://i.loli.net/2020/10/11/hcViz6ZH8PdoQ1E.png"></p><p>当然，既然创建了服务，必然会有需要使用的时候，不可能仅仅用来测试，比如定时的天气查询服务，图片上传生成外链服务，持续集成，持续发布，还有常见的后端API，AWS为Lambda提供了一系列触发器，根据不同方式来invoke：
</p><p><img alt="触发器" src="https://i.loli.net/2020/10/11/yCZjcit2wm3PY9p.png"></p><p>例如使用API Gateway，可以快速构建一个API，AWS会提供一个endpoint，只需几个简单的步骤，无须关心服务器系统版本，配置nginx，配置Docker。
</p><h2 id="user-content-sam" class="">SAM<a class="" tabindex="-1" href="#sam">#</a></h2><p>AWS提供了一整套​<code class="">Serverless</code>​的生态，​<code class="">Lambda</code>​只是其中一部分，譬如要创建一个购物系统API，其中免不了要配置​<code class="">API Gateway</code>​，​<code class="">Layer</code>​，程序员往往喜欢有集中的，CLI形式的配置方式，而不是在网页上点击，尤其是编辑代码，每个人都有自己习惯的编辑器，而不是在网页上直接编辑。
</p><p><strong>AWS Serverless Application Model</strong>​是官方提供的脚手架工具，可以帮助我们快速构建需要的Serverless应用。使用​<code class="">sam init</code>​可以快速初始化一个应用，sam也提供了一些模板应用，可以直接拿来套用。sam默认使用名为​<code class="">template.yaml</code>​的文件作为配置文件，使用​<code class="">sam build</code>​命令构建打包项目，​<code class="">sam deploy</code>​部署，另外还提供了本地运行开发测试的功能，不过这需要先安装Docker。
</p><p>当然也有一些封装层次更高的框架如<a href="https://aws.github.io/chalice/index">AWS Chalice</a>，看了一下官网的介绍，感觉能简化不少操作，不过在目前的项目中由于一些原因无法使用。
</p><h2 id="user-content-共享依赖" class="">共享依赖<a class="" tabindex="-1" href="#共享依赖">#</a></h2><p>如果一个Function依赖第三方库，在​<code class="">AWS Lambda</code>​中，需要直接将依赖库文件和自己的代码一并压缩成zip文件上传，这样当多个Function依赖相同的第三方库或者一些通用代码，这种在每个Function中上传依赖的方式将变得很麻烦，对此AWS提供了一个​<code class="">Layer</code>​功能，可以将将公共依赖放到一个​<code class="">Layer</code>​中，多个Function之间可以共享一个​<code class="">Layer</code>​，一个Function也可以构建在多个​<code class="">Layer</code>​之上，以此减少重复工作。
</p><p>不过​<code class="">Layer</code>​仍然是将需要的依赖代码直接上传，在这里我遇到了一个恶心的问题。起初，我在​<code class="">template.yaml</code>​中配置Layer，并写了一个shell脚本将依赖文件下载到​<code class="">ContenUri</code>​配置项指定的文件夹。
</p><p>template.yaml文件（省略部分）：
</p><pre tabindex="0"><code><span><span>SharedLayer</span><span>:</span></span>
<span><span>  Type</span><span>:</span><span> AWS::Serverless::LayerVersion</span></span>
<span><span>  Properties</span><span>:</span></span>
<span><span>    LayerName</span><span>:</span><span> elliot-demo-shared</span></span>
<span><span>    Description</span><span>:</span><span> common dependencies</span></span>
<span><span>    ContentUri</span><span>:</span><span> shared/</span></span>
<span><span>    RetentionPolicy</span><span>:</span><span> Delete</span></span></code></pre><p>shell脚本：
</p><pre tabindex="0"><code><span><span>#!/bin/bash</span></span>
<span><span>set</span><span> -eo</span><span> pipefail</span></span>
<span><span>echo</span><span> "</span><span>Install dependencies</span><span>"</span></span>
<span><span>rm</span><span> -rf</span><span> ./shared</span></span>
<span><span>pip3</span><span> install</span><span> --target</span><span> ./shared/python</span><span> -r</span><span> requirements.txt</span></span>
<span><span>echo</span><span> "</span><span>Start build</span><span>"</span></span>
<span><span>time</span><span> sam build</span></span></code></pre><p>当依赖项有更新时，执行shell脚本，安装依赖并执行​<code class="">build</code>​，将这些依赖库打包上传，但是不幸的是，​<code class="">Python</code>​中有很多​<code class="">C/C++</code>​依赖，这些第三方库会在本地根据系统环境进行编译，而​<code class="">AWS Lambda</code>​是运行在基于​<code class="">Linux</code>​的容器中的，我们本地开发环境则是​<code class="">Mac os</code>​，因此本地下载的依赖部署之后就会报错。Function无法正常运行。
</p><p>当然我家里有一台Linux机器，但是不能保证每次开发都在Linux环境进行，最初我的解决办法比较麻烦，在Mac上启动一个​<code class="">Docker</code>​容器，并挂载数据卷，容器内pip安装依赖到挂载的文件夹中，以此获得在Linux环境下编译的二进制包。
</p><p>最后发现​<code class="">sam</code>​其实提供了​<code class="">--use-container</code>​参数，可以在容器内build，修改配置文件如下：
</p><pre tabindex="0"><code><span><span>SharedLayer</span><span>:</span></span>
<span><span>  Type</span><span>:</span><span> AWS::Serverless::LayerVersion</span></span>
<span><span>  Properties</span><span>:</span></span>
<span><span>    LayerName</span><span>:</span><span> elliot-demo-shared</span></span>
<span><span>    Description</span><span>:</span><span> common dependencies</span></span>
<span><span>    ContentUri</span><span>:</span><span> shared</span></span>
<span><span>    CompatibleRuntimes</span><span>:</span></span>
<span><span>      - </span><span>python3.8</span></span>
<span><span>    RetentionPolicy</span><span>:</span><span> Delete</span></span>
<span><span>  Metadata</span><span>:</span></span>
<span><span>    BuildMethod</span><span>:</span><span> makefile</span></span></code></pre><p>定义​<code class="">Metadata</code>​，要求通过​<code class="">makefile</code>​构建​<code class="">Layer</code>​，接下来可以删除原先的​<code class="">shared</code>​文件夹下的内容，将​<code class="">requirements.txt</code>​移动进来，并且新建​<code class="">makefile</code>​文件：
</p><pre tabindex="0"><code><span><span>build-SharedLayer</span><span>:</span></span>
<span><span>    mkdir -p "</span><span>$(</span><span>ARTIFACTS_DIR</span><span>)</span><span>/python/"</span></span>
<span><span>    python3</span><span> -m</span><span> pip</span><span> install</span><span> -r</span><span> requirements.txt</span><span> -t</span><span> "</span><span>$(</span><span>ARTIFACTS_DIR</span><span>)</span><span>/python/"</span><span> -i</span><span> https</span><span>:</span><span>//pypi.tuna.tsinghua.edu.cn/simple</span></span></code></pre><p><strong>注意makefile内命令行要以Tab开头，编辑器中我们一般都将Tab转换成空格了，这里要改回来</strong></p><p>这样就可以在特定的容器内进行build操作了，为了避免sam每次build都要拉取Docker镜像，可以使用​<code class="">--skip-pull-image</code>​参数跳过。不过这样还是有些不方便，每次都要在容器内build，即使依赖项没有发生改变也会​<strong>重新构建Layer</strong>​，希望未来​<code class="">AWS</code>​能改善这方面体验。
</p><h2 id="user-content-fastapi" class="">Fastapi<a class="" tabindex="-1" href="#fastapi">#</a></h2><p>参考官方模板，我写了一些Demo，在这之后，我开始对​<strong>如何将一个本地项目快速迁移到Serverless</strong>​感兴趣。
</p><p>如果我有一个以Python常用Web框架如​<code class="">Django</code>​，​<code class="">fastapi</code>​编写的RESTful API项目，具有类似按资源划分的项目结构，如何将其快速迁移到Serverless的架构中呢？
</p><p>在搜集资料的时候，我发现了一篇文章<a href="https://dev.to/paurakhsharma/microservice-in-python-using-fastapi-24cc">Microservice in Python using FastAPI</a>，文中使用fastapi构建了一个​<code class="">Microservice</code>​架构的应用。
</p><p><img alt="Microservice" src="https://i.loli.net/2020/10/11/sVXCNh6up2KcjWr.jpg"></p><p>上图来自<a href="https://www.zhihu.com/question/65502802/answer/802678798">什么是微服务架构？ - 老刘的回答 - 知乎</a></p><p>如果按照常规思路，我们一般会将REST API项目按照Resources划分，如User、Post、Comment，目录内可能包含资源的model、migration、route、view、controller等，最后在外层目录，或许有个类似​<code class="">main.js</code>​、​<code class="">StartUp.cs</code>​的文件统一注册所有资源，那么按照microservices的思路，可以设置如下结构：
</p><div>.
├── cast_service
│   ├── app
│   │   ├── api
│   │   │   ├── casts.py
│   │   │   ├── db_manager.py
│   │   │   ├── db.py
│   │   │   ├── __init__.py
│   │   │   └── models.py
│   │   ├── __init__.py
│   │   └── main.py
│   ├── __init__.py
│   └── requirements.txt
├── __init__.py
└── movie_service
    ├── app
    │   ├── api
    │   │   ├── db_manager.py
    │   │   ├── db.py
    │   │   ├── __init__.py
    │   │   ├── models.py
    │   │   ├── movies.py
    │   │   └── service.py
    │   ├── __init__.py
    │   └── main.py
    ├── __init__.py
    └── requirements.txt
</div><p>每个sesrvice目录下的requirements.txt文件是SAM的硬性要求，每个包含Lambda函数的文件夹下都要有一个，SAM工具会自动执行pip install下载并上传，当然这里我们使用Layer，单独应用内没有特殊的依赖，所以直接留空就行了。按照那篇文章中Microservices应用部署的方式，我完全可以将​<code class="">nginx</code>​替换为​<code class="">API Gateway</code>​，将​<code class="">Docker</code>​容器换成​<code class="">AWS Lambda</code>​，我将每个单独的资源视为一个Lambda服务，可以这样编写sam配置（有部分省略）：
</p><pre tabindex="0"><code><span><span>Resources</span><span>:</span></span>
<span><span>  RouteApi</span><span>:</span></span>
<span><span>    Type</span><span>:</span><span> AWS::Serverless::Api</span></span>
<span><span>    Properties</span><span>:</span></span>
<span><span>      StageName</span><span>:</span><span> Demo</span></span>
<span><span>      EndpointConfiguration</span><span>:</span><span> REGIONAL</span></span>
<span></span>
<span><span>  MoviesFunction</span><span>:</span></span>
<span><span>    Type</span><span>:</span><span> AWS::Serverless::Function</span></span>
<span><span>    Properties</span><span>:</span></span>
<span><span>      Events</span><span>:</span></span>
<span><span>        Base</span><span>:</span></span>
<span><span>          Properties</span><span>:</span></span>
<span><span>            RestApiId</span><span>:</span></span>
<span><span>              Ref</span><span>:</span><span> RouteApi</span></span>
<span><span>            Path</span><span>:</span><span> /api/v1/movies</span></span>
<span><span>            Method</span><span>:</span><span> ANY</span></span>
<span><span>          Type</span><span>:</span><span> Api</span></span>
<span><span>        Others</span><span>:</span></span>
<span><span>          Properties</span><span>:</span></span>
<span><span>            RestApiId</span><span>:</span></span>
<span><span>              Ref</span><span>:</span><span> RouteApi</span></span>
<span><span>            Path</span><span>:</span><span> /api/v1/movies/{proxy+}</span></span>
<span><span>            Method</span><span>:</span><span> ANY</span></span>
<span><span>          Type</span><span>:</span><span> Api</span></span>
<span><span>      FunctionName</span><span>:</span><span> elliot-fastapi-movies</span></span>
<span><span>      CodeUri</span><span>:</span><span> microservices/movie_service/</span></span>
<span><span>      Handler</span><span>:</span><span> app.main.handler</span></span>
<span></span>
<span><span>  CastsFunction</span><span>:</span></span>
<span><span>    Type</span><span>:</span><span> AWS::Serverless::Function</span></span>
<span><span>    Properties</span><span>:</span></span>
<span><span>      Events</span><span>:</span></span>
<span><span>        Base</span><span>:</span></span>
<span><span>          Properties</span><span>:</span></span>
<span><span>            RestApiId</span><span>:</span></span>
<span><span>              Ref</span><span>:</span><span> RouteApi</span></span>
<span><span>            Path</span><span>:</span><span> /api/v1/casts</span></span>
<span><span>            Method</span><span>:</span><span> ANY</span></span>
<span><span>          Type</span><span>:</span><span> Api</span></span>
<span><span>        Others</span><span>:</span></span>
<span><span>          Properties</span><span>:</span></span>
<span><span>            RestApiId</span><span>:</span></span>
<span><span>              Ref</span><span>:</span><span> RouteApi</span></span>
<span><span>            Path</span><span>:</span><span> /api/v1/casts/{proxy+}</span></span>
<span><span>            Method</span><span>:</span><span> ANY</span></span>
<span><span>          Type</span><span>:</span><span> Api</span></span>
<span><span>      FunctionName</span><span>:</span><span> elliot-fastapi-casts</span></span>
<span><span>      CodeUri</span><span>:</span><span> microservices/cast_service/</span></span>
<span><span>      Handler</span><span>:</span><span> app.main.handler</span></span></code></pre><p>通过​<code class="">{proxy+}</code>​可以将API端点剩余部分交给我们的fastapi应用处理。接下来就是fastapi如何处理适配Lambda的问题了，我在Github上发现了<a href="https://github.com/jordaneremieff/mangum">Mangum</a>这个库，可以利用它将任何Python的​<code class="">ASGI</code>​应用（如<a href="https://www.djangoproject.com/">Django3.0</a>以上版本、<a href="https://www.starlette.io/">Starlette</a>、<a href="https://fastapi.tiangolo.com/">fastapi</a>等）转换成Lambda handler：
</p><pre tabindex="0"><code><span><span>from</span><span> fastapi </span><span>import</span><span> FastAPI</span></span>
<span><span>from</span><span> mangum </span><span>import</span><span> Mangum</span></span>
<span></span>
<span><span>from</span><span> app</span><span>.</span><span>api</span><span>.</span><span>movies </span><span>import</span><span> movies</span></span>
<span><span>from</span><span> app</span><span>.</span><span>api</span><span>.</span><span>db </span><span>import</span><span> metadata</span><span>,</span><span> database</span><span>,</span><span> engine</span></span>
<span></span>
<span></span>
<span><span>metadata</span><span>.</span><span>create_all</span><span>(</span><span>engine</span><span>)</span></span>
<span></span>
<span><span>prefix </span><span>=</span><span> "</span><span>/api/v1/movies</span><span>"</span></span>
<span></span>
<span><span>app </span><span>=</span><span> FastAPI</span><span>(</span><span>openapi_prefix</span><span>=</span><span>"</span><span>/Demo</span><span>"</span><span>,</span><span> openapi_url</span><span>=</span><span>f</span><span>"</span><span>{</span><span>prefix</span><span>}</span><span>/openapi.json"</span><span>,</span><span> docs_url</span><span>=</span><span>f</span><span>"</span><span>{</span><span>prefix</span><span>}</span><span>/docs"</span><span>)</span></span>
<span></span>
<span></span>
<span><span>@</span><span>app</span><span>.</span><span>on_event</span><span>(</span><span>"</span><span>startup</span><span>"</span><span>)</span></span>
<span><span>async</span><span> def</span><span> startup</span><span>()</span><span>:</span></span>
<span><span>    await</span><span> database</span><span>.</span><span>connect</span><span>()</span></span>
<span></span>
<span></span>
<span><span>@</span><span>app</span><span>.</span><span>on_event</span><span>(</span><span>"</span><span>shutdown</span><span>"</span><span>)</span></span>
<span><span>async</span><span> def</span><span> shutdown</span><span>()</span><span>:</span></span>
<span><span>    await</span><span> database</span><span>.</span><span>disconnect</span><span>()</span></span>
<span></span>
<span><span>app</span><span>.</span><span>include_router</span><span>(</span><span>movies</span><span>,</span><span> prefix</span><span>=</span><span>prefix</span><span>,</span><span> tags</span><span>=</span><span>[</span><span>'</span><span>movies</span><span>'</span><span>]</span><span>)</span></span>
<span></span>
<span><span>handler </span><span>=</span><span> Mangum</span><span>(</span><span>app</span><span>)</span></span></code></pre><p>最终的目录结构如下：
</p><p><img alt="目录结构" src="https://i.loli.net/2020/10/11/fyoQmlwLgUxvFIM.png"></p><p>这种结构的代码仍然还可以使用之前所述文章中的​<code class="">Microservices</code>​的方式部署应用，或者可以屏蔽每个资源下的​<code class="">main.py</code>​，改用一个​<code class="">FastAPI</code>​类的实例挂载所有路由，以普通的fastapi应用的形式部署。所有代码已经上传到<a href="https://github.com/Eliot00/elliotFunction">Github</a>。
</p><p>官网对Serverless、FaaS的阐述，这个应用似乎已经背离了AWS推荐的方式，官网的文章认为最好是将程序拆分到同一个资源的增删改查四种操作作为四个不同的服务，或许这种形式可以称为RaaS（REST的前提，U​<strong>R</strong>​I，Resources），开个玩笑。
</p><p>每个Lambda函数预留了1000的并发量，单个函数处理CURD确实会带来一定性能上的损失，不过如果一个项目前期对是否使用Serverless有些犹豫不决，在未来某个时间节点可能会切换，或者是要短时间迁移一个旧项目，这里似乎能作为一种参考方式。
</p><p>当然前提是你按照类似的形式组织了代码，而不是将所有代码放到一个文件里，这不是玩笑，确实有人是这么做的～
</p><p>写到这里我突然想到，或许我可以实现一个​<code class="">Generator/Adapter</code>​，或者说一个脚手架，用于生成一个fastapi的范例结构，并最终帮助我自动拆解项目，将路由提取出来部署到AWS Serverless生态中。这将是个巨大的挑战，我想将它放到接下来一年的个人娱乐项目TODO list中。
</p><p>在编程世界里，我认为存在着两类语言或框架的设计，一类充分相信程序员，认为程序员可以掌控一切，如​<code class="">C/C++</code>​语言，另一类则可能认为程序员都是满脑子浆糊的蠢货，必须加以严格的限制，如​<code class="">Rust</code>​。尽管有时候更愿意相信自己可以掌控全局，但是不得不承认，在多人协作中，都会倾向于施加一定的规范、限制，否则每个人都按照自己的习惯，随意，最终的结果往往不太好。所以在Web框架中我更喜欢​<code class="">ASP.NET core</code>​或​<code class="">Django</code>​这类框架，而非​<code class="">fastapi</code>​、​<code class="">flask</code>​这类灵活小巧的框架。
</p><h2 id="user-content-一些问题" class="">一些问题<a class="" tabindex="-1" href="#一些问题">#</a></h2><ul><li>根据官方示例做了基于Lambda的Auth，接下来要具体了解一下IAM的内容了
</li><li>Websocket API
</li><li>其它触发器的使用
</li><li>绕过API Gateway，Lambda之间互相调用
</li><li>持续集成
</li></ul><p>……
</p><p>目前准备去了解的问题，写下一篇笔记前可以先研究这些了。
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>简单探索Rust Web开发</title>
          <link>https://elliot00.com/posts/rust-web-framework</link>
          <description>这篇文章评测了  Rust  的 web 开发相关框架，并与微软的  Blazor  在  WebAssembly  部分做了简单的对比。作者对  Actix  和  Rocket  这两个  Rust  中最知名的 web 框架进行了介绍，并讲述了如何使用它们来构建简单 web 应用。然后，作者介绍了  Rust  中使用  serde  进行数据序列化的过程，并展示了如何使用  Actix  处理 JSON 请求和返回 JSON 响应。接着，作者简要介绍了  Rust  的  trait  机制，并展示了如何使用  trait  来实现自定义类型。最后，作者对  Yew  和  Blazor  这两个  Rust  和  dotNET  的前端框架进行了对比，并对  Rust  和  dotNET  在 Web 开发领域的未来发展进行了展望。</description>
          <pubDate>Sat, 19 Dec 2020 05:40:36 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <h2 id="user-content-摘要" class="">摘要<a class="" tabindex="-1" href="#摘要">#</a></h2><p>对​<code class="">Rust</code>​的web开发相关框架做个简单评测，同时在​<code class="">WebAssembly</code>​部分与微软的​<code class="">Blazor</code>​做个简单的对比。只是一次浅薄的评测，仅仅为了看看目前用​<code class="">Rust</code>​做web开发体验如何，性能方面Rust稳站顶端，因此不做评价。
</p><h2 id="user-content-后端开发" class="">后端开发<a class="" tabindex="-1" href="#后端开发">#</a></h2><p>目前在​<code class="">Rust</code>​中最知名的两个web框架要数<a href="https://rocket.rs/">Rocket</a>和<a href="https://actix.rs">Actix</a>了，Rocket更注重易用性，内置大量开箱即用的功能，Actix则更注重性能，不过目前两个框架互相吸取长处，Rocket性能有所提升，Actix相关生态也更加丰富，并且背后有微软的支持，已经用在了Azure的生产环境中。
</p><p>这里我选择试试Actix，主要是Rockte必须保持使用​<code class="">Rust nightly</code>​（对Rust版本发布感兴趣可以看<a href="https://kaisery.github.io/trpl-zh-cn/appendix-07-nightly-rust.html">这篇文章</a>），并且要一直保证使用最新版，好在Rust提供了强大的工具链，可以使用​<code class="">rustup</code>​这个工具覆盖某个文件夹的设置：
</p><pre tabindex="0"><code><span><span>$</span><span> rustup override set nightly</span></span></code></pre><p>这样我们仅在设置的目录下使用nightly版本的Rust，其它目录下使用的仍然是稳定版。不过个人不太喜欢一直使用超前版本，所以最后选择了Actix。
</p><h3 id="user-content-从hello-world开始">从hello world开始<a class="" tabindex="-1" href="#从hello-world开始">#</a></h3><p>首先创建项目：
</p><pre tabindex="0"><code><span><span>$</span><span> cargo new hello_world </span><span>&#x26;&#x26;</span><span> cd</span><span> hello_world</span></span></code></pre><p>在我们的​<code class="">Cargo.toml</code>​文件中写入依赖：
</p><pre tabindex="0"><code><span><span>[dependencies]</span></span>
<span><span>actix-web</span><span> =</span><span> "</span><span>3</span><span>"</span></span></code></pre><p>写入以下内容到​<code class="">src/main.rs</code>​，并使用​<code class="">cargo run</code>​运行项目：
</p><pre tabindex="0"><code><span><span>use</span><span> actix_web</span><span>::</span><span>{get, </span><span>App</span><span>, </span><span>HttpResponse</span><span>, </span><span>HttpServer</span><span>, </span><span>Responder</span><span>};</span></span>
<span></span>
<span><span>#[get(</span><span>"</span><span>/</span><span>"</span><span>)]</span></span>
<span><span>async</span><span> fn</span><span> hello</span><span>() </span><span>-></span><span> impl</span><span> Responder</span><span> {</span></span>
<span><span>    HttpResponse</span><span>::</span><span>Ok</span><span>()</span><span>.</span><span>body</span><span>(</span><span>"</span><span>Hello world!</span><span>"</span><span>)</span></span>
<span><span>}</span></span>
<span></span>
<span><span>#[actix_web</span><span>::</span><span>main]</span></span>
<span><span>async</span><span> fn</span><span> main</span><span>() </span><span>-></span><span> std</span><span>::</span><span>io</span><span>::</span><span>Result</span><span>&#x3C;()> {</span></span>
<span><span>    HttpServer</span><span>::</span><span>new</span><span>(</span><span>||</span><span> {</span></span>
<span><span>        App</span><span>::</span><span>new</span><span>()</span></span>
<span><span>            .</span><span>service</span><span>(</span><span>hello</span><span>)</span></span>
<span><span>    })</span></span>
<span><span>    .</span><span>bind</span><span>(</span><span>"</span><span>127.0.0.1:8080</span><span>"</span><span>)</span><span>?</span></span>
<span><span>    .</span><span>run</span><span>()</span></span>
<span><span>    .</span><span>await</span></span>
<span><span>}</span></span></code></pre><h3 id="user-content-handler与路由定义">Handler与路由定义<a class="" tabindex="-1" href="#handler与路由定义">#</a></h3><p>在Actix中，任何返回值实现了​<code class="">Responder</code>​这个​<code class="">trait</code>​的函数，都可以作为一个​<code class="">handler</code>​，相当于​<code class="">MVC</code>​中的​<code class="">controller</code>​。
</p><p>利用Rust的​<strong>宏机制</strong>​，在Actix中我们可以便捷地定义路由：
</p><pre tabindex="0"><code><span><span>// 标记HTTP方法以及endpoint</span></span>
<span><span>#[get(</span><span>"</span><span>/</span><span>"</span><span>)]</span></span>
<span><span>async</span><span> fn</span><span> hello</span><span>() </span><span>-></span><span> impl</span><span> Responder</span><span> {</span></span>
<span><span>    HttpResponse</span><span>::</span><span>Ok</span><span>()</span><span>.</span><span>body</span><span>(</span><span>"</span><span>Hello world!</span><span>"</span><span>)</span></span>
<span><span>}</span></span>
<span></span>
<span><span>// Actix为Rust中一些基础类型实现了Responder，所以也可以这样</span></span>
<span><span>#[get(</span><span>"</span><span>/</span><span>"</span><span>)]</span></span>
<span><span>async</span><span> fn</span><span> hello</span><span>() </span><span>-></span><span> &#x26;</span><span>'</span><span>stattic</span><span> str</span><span> {</span></span>
<span><span>    "</span><span>Hello world</span><span>"</span></span>
<span><span>}</span></span>
<span></span>
<span><span>// 或者也可以像这样在main函数中定义路由</span></span>
<span><span>use</span><span> actix_web</span><span>::</span><span>{web, </span><span>App</span><span>, </span><span>HttpResponse</span><span>, </span><span>HttpServer</span><span>, </span><span>Responder</span><span>};</span></span>
<span></span>
<span><span>async</span><span> fn</span><span> hello</span><span>() </span><span>-></span><span> impl</span><span> Responder</span><span> {</span></span>
<span><span>    HttpResponse</span><span>::</span><span>Ok</span><span>()</span><span>.</span><span>body</span><span>(</span><span>"</span><span>Hello world!</span><span>"</span><span>)</span></span>
<span><span>}</span></span>
<span></span>
<span><span>#[actix_web</span><span>::</span><span>main]</span></span>
<span><span>async</span><span> fn</span><span> main</span><span>() </span><span>-></span><span> std</span><span>::</span><span>io</span><span>::</span><span>Result</span><span>&#x3C;()> {</span></span>
<span><span>    HttpServer</span><span>::</span><span>new</span><span>(</span><span>||</span><span> {</span></span>
<span><span>        App</span><span>::</span><span>new</span><span>()</span></span>
<span><span>            .</span><span>route</span><span>(</span><span>"</span><span>/</span><span>"</span><span>, web</span><span>::</span><span>get</span><span>()</span><span>.</span><span>to</span><span>(</span><span>hello</span><span>))</span></span>
<span><span>    })</span></span>
<span><span>    .</span><span>bind</span><span>(</span><span>"</span><span>127.0.0.1:8080</span><span>"</span><span>)</span><span>?</span></span>
<span><span>    .</span><span>run</span><span>()</span></span>
<span><span>    .</span><span>await</span></span>
<span><span>}</span></span></code></pre><h3 id="user-content-request">Request<a class="" tabindex="-1" href="#request">#</a></h3><p>这里仅仅简单讲讲​<code class="">JSON Request</code>​，​<code class="">Rust</code>​拥有非常强大的序列化crate（在Rust里我们一般不说库或者包），<a href="https://serde.rs/">serde</a>，看看如何在Actix中使用它：
</p><pre tabindex="0"><code><span><span>use</span><span> actix_web</span><span>::</span><span>{web, </span><span>App</span><span>, </span><span>HttpServer</span><span>, </span><span>Result</span><span>};</span></span>
<span><span>use</span><span> serde</span><span>::</span><span>Deserialize</span><span>;</span></span>
<span></span>
<span><span>#[derive(</span><span>Deserialize</span><span>)]</span></span>
<span><span>struct</span><span> Info</span><span> {</span></span>
<span><span>    username</span><span>:</span><span> String</span><span>,</span></span>
<span><span>}</span></span>
<span></span>
<span><span>/// extract `Info` using serde</span></span>
<span><span>async</span><span> fn</span><span> index</span><span>(</span><span>info</span><span>:</span><span> web</span><span>::</span><span>Json</span><span>&#x3C;</span><span>Info</span><span>>) </span><span>-></span><span> Result</span><span>&#x3C;</span><span>String</span><span>> {</span></span>
<span><span>    Ok</span><span>(</span><span>format!</span><span>(</span><span>"</span><span>Welcome {}!</span><span>"</span><span>, </span><span>info</span><span>.</span><span>username))</span></span>
<span><span>}</span></span>
<span></span>
<span><span>#[actix_web</span><span>::</span><span>main]</span></span>
<span><span>async</span><span> fn</span><span> main</span><span>() </span><span>-></span><span> std</span><span>::</span><span>io</span><span>::</span><span>Result</span><span>&#x3C;()> {</span></span>
<span><span>    HttpServer</span><span>::</span><span>new</span><span>(</span><span>||</span><span> App</span><span>::</span><span>new</span><span>()</span><span>.</span><span>route</span><span>(</span><span>"</span><span>/</span><span>"</span><span>, web</span><span>::</span><span>post</span><span>()</span><span>.</span><span>to</span><span>(</span><span>index</span><span>)))</span></span>
<span><span>        .</span><span>bind</span><span>(</span><span>"</span><span>127.0.0.1:8080</span><span>"</span><span>)</span><span>?</span></span>
<span><span>        .</span><span>run</span><span>()</span></span>
<span><span>        .</span><span>await</span></span>
<span><span>}</span></span></code></pre><p>这是来自官网的示例，现在可以使用​<code class="">httpie</code>​或者​<code class="">Postman</code>​等工具发送POST请求测试，如：
</p><pre tabindex="0"><code><span><span>$</span><span> http POST 127.0.0.1:8080 </span><span>name</span><span>=</span><span>bob</span></span>
<span><span>HTTP/1.1 200 OK</span></span>
<span><span>content-length: 12</span></span>
<span><span>content-type: text/plain; charset=utf-8</span></span>
<span><span>date: Sat, 21 Nov 2020 02:46:07 GMT</span></span>
<span></span>
<span><span>Welcome bob!</span></span></code></pre><p>使用​<code class="">serde</code>​可以轻松为我们自定义的数据结构实现序列化与反序列化：
</p><pre tabindex="0"><code><span><span>use</span><span> serde</span><span>::</span><span>{</span><span>Serialize</span><span>, </span><span>Deserialize</span><span>};</span></span>
<span></span>
<span><span>#[derive(</span><span>Serialize</span><span>, </span><span>Deserialize</span><span>, </span><span>Debug</span><span>)]</span></span>
<span><span>struct</span><span> Point</span><span> {</span></span>
<span><span>    x</span><span>:</span><span> i32</span><span>,</span></span>
<span><span>    y</span><span>:</span><span> i32</span><span>,</span></span>
<span><span>}</span></span></code></pre><h3 id="user-content-response">Response<a class="" tabindex="-1" href="#response">#</a></h3><p>Actix默认会使用内置的中间件自动压缩数据，支持​<code class="">gzip</code>​等多种编码。与Request一样，可以借助​<code class="">serde</code>​轻松序列化数据，返回​<code class="">JSON</code>​响应。代码这里就不讲了，官网有详细示例。
</p><p>这里稍微介绍一下Rust的​<code class="">trait</code>​机制，Rust更加拥抱函数式编程范式，但也支持面向对象的一些特性，但可能令一些从​<code class="">JAVA</code>​、​<code class="">C#</code>​之类语言入门的程序员来说，Rust可能会使他们感到不适，Rust没有​<code class="">class</code>​这个概念，也没有继承机制。与众不同的是，Rust实现了一套​<code class="">trait</code>​机制。
</p><p>面向对象的继承，主要为了两个目的，一是复用代码，子类可以沿用父类的方法，而Rust可以通过默认​<code class="">trait</code>​方法来达到这一点，注意​<code class="">trait</code>​的主要思想是基于组合的，但可以表现得像继承；继承的另一点作用则是为了多态，子类型可以在父类型被使用的地方使用，而在Rust的泛型系统中，可以通过​<code class="">trait bounds</code>​来实现多态，甚至有些像动态语言中的鸭子类型，如前面讲过的，Actix的handler函数，只要返回值实现了​<code class="">Responder</code>​这个​<code class="">trait</code>​就可以，因此可以用​<code class="">impl Responder</code>​来表示这个泛型，而无需从顶层定义一个超类。
</p><p>这里仅以一个我写的<a href="https://github.com/Eliot00/commit-formatter">commit格式化工具</a>来做个简单示例，展示一下Rust的​<code class="">trait</code>​:
</p><pre tabindex="0"><code><span><span>use</span><span> std</span><span>::</span><span>fmt</span><span>::</span><span>{</span><span>self</span><span>, </span><span>Display</span><span>, </span><span>Formatter</span><span>};</span></span>
<span></span>
<span><span>// 这里是我自定义的类型</span></span>
<span><span>pub</span><span> struct</span><span> CommitType</span><span> {</span></span>
<span><span>    text</span><span>:</span><span> &#x26;</span><span>'</span><span>static</span><span> str</span><span>,</span></span>
<span><span>    description</span><span>:</span><span> &#x26;</span><span>'</span><span>static</span><span> str</span><span>,</span></span>
<span><span>}</span></span>
<span></span>
<span><span>// 这里为自定义类型实现内置的Display这个trait</span></span>
<span><span>impl</span><span> Display</span><span> for</span><span> CommitType</span><span> {</span></span>
<span><span>    fn</span><span> fmt</span><span>(</span><span>&#x26;</span><span>self</span><span>, </span><span>f</span><span>:</span><span> &#x26;</span><span>mut</span><span> Formatter</span><span>) </span><span>-></span><span> fmt</span><span>::</span><span>Result</span><span> {</span></span>
<span><span>        write!</span><span>(</span><span>f</span><span>, </span><span>"</span><span>{:9}: {}</span><span>"</span><span>, </span><span>self</span><span>.</span><span>text, </span><span>self</span><span>.</span><span>description)</span></span>
<span><span>    }</span></span>
<span><span>}</span></span></code></pre><p>这其实有些类似​<code class="">C#</code>​中的接口或​<code class="">Python</code>​的​<code class="">mixin</code>​，本质上都是一种组合的思想，现在我自定义的类型​<code class="">CommitType</code>​的实例就可以用​<code class="">println!</code>​这个宏打印输出到控制台了。不需要继承某个​<code class="">String</code>​类，只是表达自定义类型**”有某某属性“，而不是”是某某种类“**，组合在这里比继承更合适。
</p><h3 id="user-content-数据库">数据库<a class="" tabindex="-1" href="#数据库">#</a></h3><p>Actix目前可以使用​<code class="">sqlx</code>​来操作一些常用数据库，也可以使用如​<code class="">async_pg</code>​这样专门针对​<code class="">postgresql</code>​的crate，当然也可以使用如​<code class="">Diesel</code>​这样的​<code class="">ORM</code>​框架，支持​<code class="">migrate</code>​操作，但暂时不支持异步。
</p><p>其它还有诸如测试、中间件、​<code class="">Websockets</code>​等特性就不一一展开了。
</p><h2 id="user-content-前端开发" class="">前端开发<a class="" tabindex="-1" href="#前端开发">#</a></h2><h3 id="user-content-yew">Yew<a class="" tabindex="-1" href="#yew">#</a></h3><p>Rust也可以基于​<code class="">WebAssembly</code>​来做前端开发。
</p><blockquote><p>WebAssembly是一种新的编码方式，可以在现代的网络浏览器中运行 － 它是一种低级的类汇编语言，具有紧凑的二进制格式，可以接近原生的性能运行，并为诸如C / C ++等语言提供一个编译目标，以便它们可以在Web上运行。它也被设计为可以与JavaScript共存，允许两者一起工作。
</p><p>——MDN web docs
</p></blockquote><p>Rust为​<code class="">WebAssembly</code>​提供了一套工具链，可以参考官网的<a href="https://rustwasm.github.io/docs/book/">《WebAssembly手册》</a>。这里主要看一下Rust的​<code class="">Yew</code>​框架，它的目的是为了像​<code class="">React</code>​那样以组件的形式写​<code class="">WebAssembly</code>​应用。
</p><p>目前来说​<code class="">Yew</code>​还是个玩具项目，官网文档不全，一些设计也还不稳定，是不能放到生产环境中的。这里使用了一个来自<a href="https://zhuanlan.zhihu.com/p/101118828">知乎</a>的基于​<code class="">Parcel</code>​的模板创建了项目，看一下组件：
</p><pre tabindex="0"><code><span><span>use</span><span> yew</span><span>::</span><span>prelude</span><span>::*</span><span>;</span></span>
<span></span>
<span><span>pub</span><span> struct</span><span> App</span><span> {}</span></span>
<span></span>
<span><span>pub</span><span> enum</span><span> Msg</span><span> {}</span></span>
<span></span>
<span><span>impl</span><span> Component</span><span> for</span><span> App</span><span> {</span></span>
<span><span>    type</span><span> Message</span><span> =</span><span> Msg</span><span>;</span></span>
<span><span>    type</span><span> Properties</span><span> =</span><span> ();</span></span>
<span></span>
<span><span>    fn</span><span> create</span><span>(</span><span>_</span><span>:</span><span> Self</span><span>::</span><span>Properties</span><span>, </span><span>_</span><span>:</span><span> ComponentLink</span><span>&#x3C;</span><span>Self</span><span>>) </span><span>-></span><span> Self</span><span> {</span></span>
<span><span>        App</span><span> {}</span></span>
<span><span>    }</span></span>
<span></span>
<span><span>    fn</span><span> update</span><span>(</span><span>&#x26;</span><span>mut</span><span> self</span><span>, </span><span>_msg</span><span>:</span><span> Self</span><span>::</span><span>Message</span><span>) </span><span>-></span><span> ShouldRender</span><span> {</span></span>
<span><span>        true</span></span>
<span><span>    }</span></span>
<span></span>
<span><span>    fn</span><span> change</span><span>(</span><span>&#x26;</span><span>mut</span><span> self</span><span>, </span><span>_props</span><span>:</span><span> Self</span><span>::</span><span>Properties</span><span>) </span><span>-></span><span> ShouldRender</span><span> {</span></span>
<span><span>        true</span></span>
<span><span>    }</span></span>
<span></span>
<span><span>    fn</span><span> view</span><span>(</span><span>&#x26;</span><span>self</span><span>) </span><span>-></span><span> Html</span><span> {</span></span>
<span><span>        html!</span><span> {</span></span>
<span><span>            &#x3C;</span><span>p</span><span>>{ </span><span>"</span><span>Hello world!</span><span>"</span><span> }&#x3C;</span><span>/</span><span>p</span><span>></span></span>
<span><span>        }</span></span>
<span><span>    }</span></span>
<span><span>}</span></span></code></pre><p>看上去非常像React的类组件，为一个​<code class="">struct</code>​实现​<code class="">Component</code>​这个​<code class="">trait</code>​即可使我们的​<code class="">struct</code>​成为一个组件。但不知道为什么​<code class="">Yew</code>​没有为这个​<code class="">trait</code>​里面除了​<code class="">view</code>​这个对应React的​<code class="">render</code>​生命周期函数以外的其它函数定义默认实现，导致哪怕并没有额外逻辑也得自己实现一遍另外的生命周期。
</p><p>在配置上，看一下在项目主目录的​<code class="">package.json</code>​：
</p><pre tabindex="0"><code><span><span>{</span></span>
<span><span>"scripts"</span><span>:</span><span> {</span></span>
<span><span>    "start"</span><span>:</span><span> "</span><span>parcel index.html</span><span>"</span><span>,</span></span>
<span><span>    "build"</span><span>:</span><span> "</span><span>parcel build index.html</span><span>"</span></span>
<span><span>  }</span><span>,</span></span>
<span><span>   "devDependencies"</span><span>:</span><span> {</span></span>
<span><span>    "parcel-bundler"</span><span>:</span><span> "</span><span>^1.12.4</span><span>"</span><span>,</span></span>
<span><span>    "parcel-plugin-wasm.rs"</span><span>:</span><span> "</span><span>^1.2.16</span><span>"</span></span>
<span><span>  }</span></span>
<span><span>}</span></span></code></pre><p>使用​<code class="">Parcel</code>​比官网示例的​<code class="">web-pack</code>​要方便一些，不需要做过多配置，可以在js中直接引用应用。
</p><p>而Rust方面的依赖如下：
</p><pre tabindex="0"><code><span><span>[dependencies]</span></span>
<span><span>wasm-bindgen</span><span> =</span><span> "</span><span>0.2</span><span>"</span></span>
<span><span>yew</span><span> =</span><span> "</span><span>0.16</span><span>"</span></span></code></pre><p><code class="">wasm-bindgen</code>​是由Rust官方维护的用于​<code class="">wasm</code>​到​<code class="">JavaScript</code>​直接绑定的胶水工具。
</p><p>目前给我的感受是，​<code class="">Yew</code>​要想成为Rust做​<code class="">WebAssembly</code>​的第一选择，那么必须要发力提升开发者体验，完善文档，并且要完善自己的集成工具，不能让开发者同时在​<code class="">node</code>​和​<code class="">Rust</code>​之间来回切换，两边都要构建，这些工作应该自动完成。
</p><h3 id="user-content-对比blazor">对比Blazor<a class="" tabindex="-1" href="#对比blazor">#</a></h3><p><code class="">Blazor</code>​是微软推出的前端UI框架，今年发布了稳定的​<code class="">Blazor WebAssembly</code>​，这里来体验一下​<code class="">Blazor</code>​的开发流程。
</p><p>写这篇文章的时候微软已经推出了​<code class="">.NET 5</code>​，不过我使用的​<code class="">Manjaro Linux</code>​的包管理中心还没有更新，虽然可以自己编译安装，但是我对安装包有”包管理洁癖“，为了体验最新版，我使用了Docker。当然如果使用微软平台，可以使用宇宙第一IDE​<code class="">Visual Studio</code>​，开发体验更上一层楼，可惜Mac版本似乎功能不全，而Linux更是不支持（也许我该买一台Windows机器了）。
</p><p>首先是拉取镜像并启动容器，然后使用VS Code上的<a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers">Remote - Containers</a>扩展，就能愉快的在容器中开发了，这个扩展也是微软出的，微软大法好！
</p><p>使用命令：
</p><pre tabindex="0"><code><span><span>$</span><span> dotnet new blazorwasm -o WebApplication</span></span></code></pre><p>创建应用，​<code class="">dotNET</code>​自动创建了模板应用，可以直接使用​<code class="">dotnet watch run</code>​命令运行。
</p><p><img alt="运行效果图" src="https://i.loli.net/2020/11/21/yqwL5HPUemhs1Ap.png"></p><p>看一下​<code class="">index.razor</code>​：
</p><pre tabindex="0"><code><span><span>@page "/"</span></span>
<span></span>
<span><span>&#x3C;</span><span>h1</span><span>></span><span>Hello, world!</span><span>&#x3C;/</span><span>h1</span><span>></span></span>
<span></span>
<span><span>Welcome to your new app.</span></span>
<span></span>
<span><span>&#x3C;</span><span>SurveyPrompt</span><span> Title</span><span>=</span><span>"</span><span>How is Blazor working for you?</span><span>"</span><span> /></span></span></code></pre><p><code class="">Blazor</code>​中的组件文件以​<code class="">razor</code>​作为后缀名，即使在VS Code中，仍然可以实现组件间跳转与代码提示，虽然比不上Visual Studio，但毕竟也是微软自家产品，体验还行，光标移动到​<code class="">SurveyPrompt</code>​这个组件，按下​<code class="">F12</code>​即可跳转到这个组件的定义：
</p><pre tabindex="0"><code><span><span>&#x3C;</span><span>div</span><span> class</span><span>=</span><span>"</span><span>alert alert-secondary mt-4</span><span>"</span><span> role</span><span>=</span><span>"</span><span>alert</span><span>"</span><span>></span></span>
<span><span>    &#x3C;</span><span>span</span><span> class</span><span>=</span><span>"</span><span>oi oi-pencil mr-2</span><span>"</span><span> aria-hidden</span><span>=</span><span>"</span><span>true</span><span>"</span><span>>&#x3C;/</span><span>span</span><span>></span></span>
<span><span>    &#x3C;</span><span>strong</span><span>></span><span>@Title</span><span>&#x3C;/</span><span>strong</span><span>></span></span>
<span></span>
<span><span>    &#x3C;</span><span>span</span><span> class</span><span>=</span><span>"</span><span>text-nowrap</span><span>"</span><span>></span></span>
<span><span>        Please take our</span></span>
<span><span>        &#x3C;</span><span>a</span><span> target</span><span>=</span><span>"</span><span>_blank</span><span>"</span><span> class</span><span>=</span><span>"</span><span>font-weight-bold</span><span>"</span><span> href</span><span>=</span><span>"</span><span>https://go.microsoft.com/fwlink/?linkid=2137916</span><span>"</span><span>></span><span>brief survey</span><span>&#x3C;/</span><span>a</span><span>></span></span>
<span><span>    &#x3C;/</span><span>span</span><span>></span></span>
<span><span>    and tell us what you think.</span></span>
<span><span>&#x3C;/</span><span>div</span><span>></span></span>
<span></span>
<span><span>@code {</span></span>
<span><span>    // Demonstrates how a parent component can supply parameters</span></span>
<span><span>    [Parameter]</span></span>
<span><span>    public string Title { get; set; }</span></span>
<span><span>}</span></span></code></pre><p>使用​<code class="">@</code>​标识符，就可以嵌入​<code class="">C#</code>​代码，这种形式可能会让熟悉React的程序员感到不习惯，整个组件不像React将DOM部分以​<code class="">JSX</code>​的形式嵌入到​<code class="">JavaScript</code>​中，反过来是将​<code class="">C#</code>​嵌入到​<code class="">HTML</code>​中，不过对于​<code class="">C#</code>​程序员可谓是十分舒适的，不需要手动进行任何配置，可以随意使用C#的逻辑构建页面。
</p><p>引入样式也非常方便，在组件所在的​<code class="">Shared</code>​文件夹下，放置与组件同名的​<code class="">CSS</code>​样式文件就行了。​<code class="">Blazor</code>​提供了一套默认的样式，同时也可以使用​<code class="">Ant Design</code>​的​<code class="">Blazor</code>​迁移版。
</p><p>看一个循环组件渲染的例子：
</p><pre tabindex="0"><code><span><span>@page "/"</span></span>
<span></span>
<span><span>&#x3C;</span><span>h1</span><span>></span><span>@heading</span><span>&#x3C;/</span><span>h1</span><span>></span></span>
<span><span>&#x3C;</span><span>h4</span><span>></span><span>Remaining - @todos.Count(todo => !todo.Done)</span><span>&#x3C;/</span><span>h4</span><span>></span></span>
<span></span>
<span><span>&#x3C;</span><span>ul</span><span>></span></span>
<span><span>    @foreach (var todo in todos)</span></span>
<span><span>    {</span></span>
<span><span>        &#x3C;</span><span>li</span><span>></span></span>
<span><span>            &#x3C;</span><span>input</span><span> type</span><span>=</span><span>"</span><span>checkbox</span><span>"</span><span> @bind</span><span>=</span><span>"</span><span>todo.Done</span><span>"</span><span> /></span></span>
<span><span>            &#x3C;</span><span>label</span><span>></span><span>@todo.Item</span><span>&#x3C;/</span><span>label</span><span>></span></span>
<span><span>        &#x3C;/</span><span>li</span><span>></span></span>
<span><span>    }</span></span>
<span><span>&#x3C;/</span><span>ul</span><span>></span></span>
<span></span>
<span><span>@code{</span></span>
<span><span>    string heading = "To Do List";</span></span>
<span></span>
<span><span>    class Todo</span></span>
<span><span>    {</span></span>
<span><span>        public bool Done { get; set; }</span></span>
<span><span>        public string Item { get; set; }</span></span>
<span><span>    }</span></span>
<span><span>    List</span><span>&#x3C;</span><span>Todo</span><span>></span><span> todos = new List</span><span>&#x3C;</span><span>Todo</span><span>></span><span>()</span></span>
<span><span>    {</span></span>
<span><span>        new Todo(){ Done = false, Item = "Corn" },</span></span>
<span><span>        new Todo(){ Done = false, Item = "Apples" },</span></span>
<span><span>        new Todo(){ Done = false, Item = "Bacon" }</span></span>
<span><span>    };</span></span>
<span><span>}</span></span></code></pre><p><code class="">MVVM</code>​架构源自于微软的桌面UI框架，​<code class="">Blazor</code>​看上去也是基于​<code class="">MVVM</code>​，所以感觉和​<code class="">Vue</code>​有点相像，不知道​<code class="">Vue</code>​程序员看了会不会有亲切感。
</p><p><img alt="效果图" src="https://i.loli.net/2020/11/21/cP9QBKyzTXGpkSg.png"></p><p>模板程序里还有一个​<code class="">Counter</code>​组件和​<code class="">Fetch data</code>​的示例，感兴趣可以自己创建一个应用尝试。整个开发过程中仅有​<code class="">C#</code>​、​<code class="">HTML</code>​、​<code class="">CSS</code>​，完全看不到​<code class="">JS</code>​的身影，但是JS前端生态丰富，开发前端应用要用到JS的库怎么办？​<code class="">Blazor</code>​也提供了​= IJSRuntime=​这个依赖注入接口，可以在组件中调用JS。
</p><p>目前看来​<code class="">Blazor</code>​的开发可以说是开箱即用的，甚至让人感觉不到在使用​<code class="">WebAssembly</code>​，只是在用​<code class="">C#</code>​替代​<code class="">JavaScript</code>​。目前来说，个人感觉​<code class="">Blazor</code>​除了帮助开发者快速构建​<code class="">WebAssembly</code>​应用以外，还为一些使用​<code class="">.NET</code>​为主要技术栈的中小企业提供了让后端快速开发一些简单前端应用的能力，熟悉​<code class="">C#</code>​的程序员也可以快速构建全栈应用，只是在国内群众基础实在太浅。
</p><p>不得不说近几年微软对于开发者还是非常有诚意的，尤其在Web开发领域，体验过​<code class="">ASP.NET core</code>​之后再看其它框架就有些黯然失色了。可惜的是，微软在中国错失了最好时机，领先的大厂没有一个使用​<code class="">.NET</code>​，大厂不招，工作机会少，新手也就不愿意学，愿意使用​<code class="">.NET</code>​的公司就招不到人，最终陷入恶性循环。
</p><h2 id="user-content-总结" class="">总结<a class="" tabindex="-1" href="#总结">#</a></h2><p>目前来看，Rust的后端开发体验还是不错的，至于前端，本身目前前端还是​<code class="">JavaScript</code>​的天下，​<code class="">WebAssembly</code>​的应用范围还是作为JS的一个补充，再者Rust下的​<code class="">WebAssembly</code>​开发体验远低于​<code class="">Blazor</code>​（仅仅讨论开发者体验），还有很长的路要走。
</p><p>Rust语言可以说站在巨人的肩膀上，提出了非常多优秀的设计，着实让人眼前一亮，目前来看，一个高效的​<code class="">IDE</code>​支持，对Rust来说还是很重要的。Rust自带的工具链​<code class="">rustup</code>​、​<code class="">cargo</code>​、​<code class="">fmt</code>​、​<code class="">clippy</code>​、​<code class="">wasm-pack</code>​等已经很强大，但还是需要一个集成环境，帮助程序员更加流畅的开发。
</p><p>不负责任地预测一波​<code class="">dotNET</code>​平台未来必将在国内Web开发领域拥有一席之地（国外还是比较流行的）！
</p>
          ]]>
          </content:encoded>
        </item>
<item>
          <title>刷题笔记0x09：单词拆分</title>
          <link>https://elliot00.com/posts/leetcode-word-split</link>
          <description>这篇文章介绍了如何使用动态规划解决一个字符串分割问题，即判断一个给定的字符串能否被空格拆分为一个或多个在字典中出现的单词。文章从分析题意、设计状态转移方程到代码实现，最后还探讨了代码优化方案。文章思路清晰，代码简洁，是一篇优秀的算法题解。</description>
          <pubDate>Sat, 19 Dec 2020 05:36:32 GMT</pubDate>
          <content:encoded>
          <![CDATA[
            <h2 id="user-content-题目分析" class="">题目分析<a class="" tabindex="-1" href="#题目分析">#</a></h2><blockquote><p>给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict，判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。<a href="https://leetcode-cn.com/problems/word-break/">https://leetcode-cn.com/problems/word-break/</a></p></blockquote><p>这个题目一开始被我误解成是判断列表中的字符串是否都是​<code class="">s</code>​的子串，最后看到官方示例才纠正回来～
</p><p><img alt="示例截图" src="https://imgkr.cn-bj.ufileos.com/adef2345-10cb-4c28-b48e-6e2afc14f62e.png"></p><p>可以发觉，这个题目我们是要找出​<strong>断点</strong>​打断字符串​<code class="">s</code>​，让打断后的各个单词都在单词列表中。
</p><p>下面分析一下，如果我们有一个长度为​<code class="">i</code>​的字符串，那么假设​<code class="">dp[j]</code>​表示字符串到​<code class="">j</code>​位置的子字符串是否符合题目条件，可以得出，如果​<code class="">dp[i]</code>​也就是长度为​<code class="">i</code>​的原字符串​<code class="">s</code>​符合条件，那么一定有一个​<code class="">0 &#x3C; j &#x3C; i</code>​的情况下，​<code class="">dp[j]</code>​符合条件，并且，剩下的​<code class="">j</code>​到​<code class="">i</code>​的部分是在单词列表里面的。
</p><p>通过分析我们发现这是一个可以用​<strong>动态规划</strong>​解决的问题，状态转移方程为：
</p><div>// 伪代码
dp[i] = dp[j] and (s[j:i] in wordDict)
</div><h2 id="user-content-代码实现" class="">代码实现<a class="" tabindex="-1" href="#代码实现">#</a></h2><p>在具体代码中，我们设​<code class="">dp</code>​数组​<strong>长度为s长度加一</strong>​，​<code class="">dp[0]</code>​设为​<strong>真</strong>​做初始条件，从​<code class="">i=1</code>​开始迭代。
</p><pre tabindex="0"><code><span><span>impl</span><span> Solution</span><span> {</span></span>
<span><span>    pub</span><span> fn</span><span> word_break</span><span>(</span><span>s</span><span>:</span><span> String</span><span>, </span><span>word_dict</span><span>:</span><span> Vec</span><span>&#x3C;</span><span>String</span><span>>) </span><span>-></span><span> bool</span><span> {</span></span>
<span><span>        let</span><span> length</span><span> =</span><span> s</span><span>.</span><span>chars</span><span>()</span><span>.</span><span>count</span><span>();</span></span>
<span><span>        let</span><span> mut</span><span> dp</span><span>:</span><span> Vec</span><span>&#x3C;</span><span>bool</span><span>> </span><span>=</span><span> vec!</span><span>[</span><span>false</span><span>; </span><span>length</span><span> +</span><span> 1</span><span>];</span></span>
<span><span>        dp</span><span>[</span><span>0</span><span>] </span><span>=</span><span> true</span><span>;</span></span>
<span><span>        for</span><span> i</span><span> in</span><span> 1</span><span>..=</span><span>length</span><span> {</span></span>
<span><span>            for</span><span> j</span><span> in</span><span> 0</span><span>..</span><span>i</span><span> {</span></span>
<span><span>                let</span><span> word</span><span> =</span><span> s</span><span>.</span><span>as_str</span><span>()[</span><span>j</span><span>..</span><span>i</span><span>]</span><span>.</span><span>to_string</span><span>();</span></span>
<span><span>                if</span><span> dp</span><span>[</span><span>j</span><span>] </span><span>&#x26;&#x26;</span><span> word_dict</span><span>.</span><span>contains</span><span>(</span><span>&#x26;</span><span>word</span><span>) {</span></span>
<span><span>                    dp</span><span>[</span><span>i</span><span>] </span><span>=</span><span> true</span><span>;</span></span>
<span><span>                    break</span><span>;</span></span>
<span><span>                }</span></span>
<span><span>            }</span></span>
<span><span>        }</span></span>
<span><span>        dp</span><span>[</span><span>length</span><span>]</span></span>
<span><span>    }</span></span>
<span><span>}</span></span></code></pre><p><img alt="示意图" src="https://imgkr.cn-bj.ufileos.com/4a85e3c0-8d04-498c-9344-dc6e3732c6f7.png"></p><p>最后成功超越100%，不过这和Leetcode上用​<code class="">Rust</code>​刷题的少也有关系。
</p><p><img alt="Result" src="https://imgkr.cn-bj.ufileos.com/5054f6b5-2a70-4b6e-ab79-d6abcf94265b.png"></p><h2 id="user-content-优化" class="">优化<a class="" tabindex="-1" href="#优化">#</a></h2><p>我们的代码还有优化的空间，不过由于现在耗时四舍五入0ms了，为了展示差别，用​<code class="">Python</code>​演示下（这样感觉Python很没有排面啊：
</p><p>先上个普通Python版：
</p><pre tabindex="0"><code><span><span>class</span><span> Solution</span><span>:</span></span>
<span><span>    def</span><span> wordBreak</span><span>(</span><span>self</span><span>,</span><span> s</span><span>:</span><span> str</span><span>,</span><span> wordDict</span><span>:</span><span> List</span><span>[</span><span>str</span><span>]</span><span>)</span><span> -></span><span> bool</span><span>:</span></span>
<span><span>        length </span><span>=</span><span> len</span><span>(</span><span>s</span><span>)</span></span>
<span><span>        dp </span><span>=</span><span> [</span><span>False</span><span>]</span><span> *</span><span> (length </span><span>+</span><span> 1</span><span>)</span></span>
<span><span>        dp</span><span>[</span><span>0</span><span>]</span><span> =</span><span> True</span></span>
<span><span>        for</span><span> i </span><span>in</span><span> range</span><span>(</span><span>1</span><span>,</span><span> length </span><span>+</span><span> 1</span><span>):</span></span>
<span><span>            for</span><span> j </span><span>in</span><span> range</span><span>(</span><span>i</span><span>):</span></span>
<span><span>                if</span><span> dp</span><span>[</span><span>j</span><span>]</span><span> and</span><span> s</span><span>[</span><span>j</span><span>:</span><span>i</span><span>]</span><span> in</span><span> wordDict</span><span>:</span></span>
<span><span>                    dp</span><span>[</span><span>i</span><span>]</span><span> =</span><span> True</span></span>
<span><span>                    break</span></span>
<span><span>        return</span><span> dp</span><span>[</span><span>-</span><span>1</span><span>]</span></span></code></pre><p><img alt="Result" src="https://imgkr.cn-bj.ufileos.com/7df16b0f-1d1b-4a1d-a425-7620825c8d76.png"></p><p>由于线性表查找时间复杂度为​<code class="">O(N)</code>​，而哈希表查找时间复杂度为​<code class="">O(1)</code>​，所有我们利用​<strong>字典生成式</strong>​将原本的单词列表转成哈希结构的字典：
</p><pre tabindex="0"><code><span><span>class</span><span> Solution</span><span>:</span></span>
<span><span>    def</span><span> wordBreak</span><span>(</span><span>self</span><span>,</span><span> s</span><span>:</span><span> str</span><span>,</span><span> wordDict</span><span>:</span><span> List</span><span>[</span><span>str</span><span>]</span><span>)</span><span> -></span><span> bool</span><span>:</span></span>
<span><span>        length </span><span>=</span><span> len</span><span>(</span><span>s</span><span>)</span></span>
<span><span>        dp </span><span>=</span><span> [</span><span>False</span><span>]</span><span> *</span><span> (length </span><span>+</span><span> 1</span><span>)</span></span>
<span><span>        dp</span><span>[</span><span>0</span><span>]</span><span> =</span><span> True</span></span>
<span><span>        wordDict </span><span>=</span><span> {</span><span>key</span><span>:</span><span> value </span><span>for</span><span> value</span><span>,</span><span> key </span><span>in</span><span> enumerate</span><span>(</span><span>wordDict</span><span>)}</span></span>
<span><span>        for</span><span> i </span><span>in</span><span> range</span><span>(</span><span>1</span><span>,</span><span> length </span><span>+</span><span> 1</span><span>):</span></span>
<span><span>            for</span><span> j </span><span>in</span><span> range</span><span>(</span><span>i</span><span>):</span></span>
<span><span>                if</span><span> dp</span><span>[</span><span>j</span><span>]</span><span> and</span><span> s</span><span>[</span><span>j</span><span>:</span><span>i</span><span>]</span><span> in</span><span> wordDict</span><span>:</span></span>
<span><span>                    dp</span><span>[</span><span>i</span><span>]</span><span> =</span><span> True</span></span>
<span><span>                    break</span></span>
<span><span>        return</span><span> dp</span><span>[</span><span>-</span><span>1</span><span>]</span></span></code></pre><p><img alt="Result" src="https://imgkr.cn-bj.ufileos.com/48559d7c-948f-448f-ac5c-f998eed526d6.png"></p><p>快了一丢丢～
</p><p>评论区也有先求出​<code class="">wordDict</code>​中最长单词长度​<code class="">maxWordLength</code>​，仅遍历当前​<code class="">i</code>​位置向前​<code class="">maxWordLength</code>​的元素，以减少循环次数，不过我试了几次运行时间没有显著提升。
</p>
          ]]>
          </content:encoded>
        </item>
    </channel>
  </rss>