<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="zh-Hant">
    <title>六小編 Editor Leon</title>
    <subtitle>Business &amp; Information Technology </subtitle>
    <link rel="self" type="application/atom+xml" href="https://editor.leonh.space/atom.xml"/>
    <link rel="alternate" type="text/html" href="https://editor.leonh.space"/>
    <generator uri="https://www.getzola.org/">Zola</generator>
    <updated>2026-02-16T00:00:00+00:00</updated>
    <id>https://editor.leonh.space/atom.xml</id>
    <entry xml:lang="zh-Hant">
        <title>Fluent Bit 收集 Log</title>
        <published>2026-02-16T00:00:00+00:00</published>
        <updated>2026-02-16T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2026/fluent-bit/"/>
        <id>https://editor.leonh.space/2026/fluent-bit/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2026/fluent-bit/">&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;fluentbit.io&quot;&gt;Fluent Bit&lt;&#x2F;a&gt; 是主流的 log 收集器之一，典型的用法是收到 log 後再轉發給下一手，廣義的說它不只收 log，也收 metric 等其他資料，這裡直接引用 Fluent Bit 網站的概念圖：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2026&#x2F;fluent-bit&#x2F;.&#x2F;hero.svg&quot; alt=&quot;Fluent Bit&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;發 log 就發 log，為什麼要過一手 Fluent Bit？以我自己而言，每個服務的 log 會集中到 CloudWatch，對於自己寫的程式這沒什麼問題，但對於像是 Traefik、Caddy、Nginx 這類被廣泛使用的服務，它們並不原生支援 CloudWatch，偏偏 web server 的 log 對於稽核或安全方面的分析又很重要，以 Fluent Bit 來收集並轉發就成為主流的解決方案之一。&lt;&#x2F;p&gt;
&lt;p&gt;雖然 AWS 有出 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;aws&#x2F;amazon-cloudwatch-agent&#x2F;&quot;&gt;CloudWatch Agent&lt;&#x2F;a&gt;，但我覺得它的目的太過單一、有侷限性，要是哪天想跳槽離開 AWS，又要匆匆忙忙連滾帶爬把它換掉，相較之下支援多輸入／多輸出的 Fluent Bit 是更泛用的選擇。&lt;&#x2F;p&gt;
&lt;p&gt;選用 Fluent Bit 的另一個理由是 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.docker.com&#x2F;engine&#x2F;logging&#x2F;drivers&#x2F;fluentd&#x2F;&quot;&gt;Docker logging driver 原生支援 Forward 協議&lt;&#x2F;a&gt;，而 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;fluent&#x2F;fluentd&#x2F;wiki&#x2F;Forward-Protocol-Specification-v1&quot;&gt;Forward 協議&lt;&#x2F;a&gt;正是 Fluent Bit 團隊開發的，因此採用 Fluent Bit 成為理所當然的選項之一，之所以說「之一」，是因為其他收集器也大多支援 Forward，主流的選手有：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;fluent&#x2F;fluent-bit&quot;&gt;Fluent Bit&lt;&#x2F;a&gt;：本文主角&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;fluent&#x2F;fluentd&quot;&gt;Fluentd&lt;&#x2F;a&gt;：Fluent Bit 的前一代，還活著不過推薦改用 Fluent Bit&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;vectordotdev&#x2F;vector&quot;&gt;Vector&lt;&#x2F;a&gt;：大廠 DataDog 出的收集器&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;幾乎每款收集器都標榜自己輕巧不吃資源、處理 log 快吞吐又快、支援各式各樣輸入／輸出，看起來都差不多的情況下挑一套自己看得順眼的就好，也有些人跑了他們之間的評比 benchmark，有的 A 好有的 B 好，參考就好。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pei-zhi-fluent-bit&quot;&gt;配置 Fluent Bit&lt;&#x2F;h2&gt;
&lt;p&gt;在使用前必須先寫 Fluent Bit 的配置文件，這裡我們採用較新的 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.fluentbit.io&#x2F;manual&#x2F;administration&#x2F;configuring-fluent-bit&#x2F;yaml&quot;&gt;YAML&lt;&#x2F;a&gt; 配置文件，下面是一份簡單的配置文件：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;service&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  # Log_Level&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  # =========&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  # Set the verbosity level of the service, values can be:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  #&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  # - error&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  # - warning&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  # - info&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  # - debug&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  # - trace&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  #&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  # by default &amp;#39;info&amp;#39; is set, that means it includes &amp;#39;error&amp;#39; and &amp;#39;warning&amp;#39;.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  # log_level: info&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  # Parsers File&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  # ============&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  # specify an optional &amp;#39;Parsers&amp;#39; configuration file&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  parsers_file&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; parsers.conf&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  # Plugins File&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  # ============&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  # specify an optional &amp;#39;Plugins&amp;#39; configuration file to load external plugins.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  plugins_file&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; plugins.conf&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;pipeline&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  inputs&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; forward&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;      port&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 24224&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  outputs&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; stdout&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;      match&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;*&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; cloudwatch_logs&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;      match&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;*&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;      region&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ${AWS_DEFAULT_REGION}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;      log_group_name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ${LOG_GROUP_NAME}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;能夠望文生義的部分就不提，其中 &lt;code&gt;service&lt;&#x2F;code&gt; 區塊是配置 Fluent Bit 本身，旗下的 &lt;code&gt;parsers_file&lt;&#x2F;code&gt;、.plugins_file` 兩項指定的檔案不用自己生，都存在現成的 Fluent Bit 映像內。&lt;&#x2F;p&gt;
&lt;p&gt;Pipeline 指的是資料的輸入、處理、輸出的流程，這裡我們不對資料做處理，所以只有 &lt;code&gt;inputs&lt;&#x2F;code&gt; 與 &lt;code&gt;outputs&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;前面提到 Docker logging driver 支援 Forward 協議輸出 log，在 Fluent Bit 這邊，就要設定收 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.fluentbit.io&#x2F;manual&#x2F;data-pipeline&#x2F;inputs&#x2F;forward&quot;&gt;Forward log 的 input&lt;&#x2F;a&gt;，設定 input 識別名 &lt;code&gt;forward&lt;&#x2F;code&gt; 與 port 24224 就可以了，非常簡單。&lt;&#x2F;p&gt;
&lt;p&gt;Output 的話就是 stdout 和 CloudWatch，&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.fluentbit.io&#x2F;manual&#x2F;data-pipeline&#x2F;outputs&#x2F;standard-output&quot;&gt;stdout output&lt;&#x2F;a&gt; 的識別名就是 &lt;code&gt;stdout&lt;&#x2F;code&gt;，&lt;code&gt;match: &quot;*&quot;&lt;&#x2F;code&gt; 就是不比對（不過濾）。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.fluentbit.io&#x2F;manual&#x2F;data-pipeline&#x2F;outputs&#x2F;cloudwatch&quot;&gt;CloudWatch output&lt;&#x2F;a&gt; 配置項目比較多一點，但也滿好懂，其中 &lt;code&gt;region&lt;&#x2F;code&gt; 與 &lt;code&gt;log_group_name&lt;&#x2F;code&gt; 的值取自 OS 環境變數。&lt;&#x2F;p&gt;
&lt;p&gt;要打資料給 AWS CloudWatch 還需要 AWS 認證，最簡單的方法是在 OS 環境變數中就設好 &lt;code&gt;AWS_ACCESS_KEY_ID&lt;&#x2F;code&gt; 與 &lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;&#x2F;code&gt;，CloutWatch output 會自己使用，或者是讓運行 Fluent Bit 的 EC2 本身就有具備 CloudWatch permission 的 IAM role，要採用哪種取決於你有多追求把安全性無限上綱以及有多想被 AWS vendor-lock。&lt;&#x2F;p&gt;
&lt;p&gt;配置完存成檔名 fluent-bit.yaml，接著來搞容器。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;rong-qi-log-dao-fluent-bit&quot;&gt;容器 log 到 Fluent Bit&lt;&#x2F;h2&gt;
&lt;p&gt;前面提過，自己寫的服務多半可以利用 AWS SDK 直接把 log 打給 CloudWatch，所以我們關注的重點放在怎麼把 Traefik log 打給 Fluent Bit 再打給 CloudWatch。&lt;&#x2F;p&gt;
&lt;p&gt;先看 compose.yaml：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;services&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  fluent-bit&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;    image&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; cr.fluentbit.io&#x2F;fluent&#x2F;fluent-bit:4.2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;    restart&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; unless-stopped&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;    ports&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;24224:24224&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; # Forward protocal&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;    volumes&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; .&#x2F;fluent-bit-data&#x2F;fluent-bit.yaml:&#x2F;fluent-bit&#x2F;etc&#x2F;fluent-bit.yaml&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;    command&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &#x2F;fluent-bit&#x2F;bin&#x2F;fluent-bit&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; --config=&#x2F;fluent-bit&#x2F;etc&#x2F;fluent-bit.yaml&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;    environment&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  traefik&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;    image&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; traefik:v3.6&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;    restart&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; unless-stopped&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;    security_opt&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; no-new-privileges=true&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;    ports&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;80:80&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;443:443&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;443:443&#x2F;udp&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;    logging&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;      driver&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; fluentd&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;      options&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;        fluentd-address&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; 127.0.0.1:24224&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; # macOS: host.docker.internal:24224&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;        fluentd-async&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;true&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;        tag&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; traefik&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這份 compose.yaml 只保留我們關注的重點，省略其他元素。&lt;&#x2F;p&gt;
&lt;p&gt;在 Fluent Bit 方面，我們把前面的 fluent-bit.yaml 掛載進容器內，並自訂 command 讓 Fluent Bit 讀取之。&lt;&#x2F;p&gt;
&lt;p&gt;在 Traefik 方面，我們啟用 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.docker.com&#x2F;engine&#x2F;logging&#x2F;drivers&#x2F;fluentd&#x2F;&quot;&gt;Fluentd logging driver&lt;&#x2F;a&gt;，它本質上就是支援 Forward 協議的 logging driver。&lt;&#x2F;p&gt;
&lt;p&gt;前面提到，Fluent Bit 的 Forward input 的 port 為 24224，而 Traefik 的 &lt;code&gt;fluentd-address&lt;&#x2F;code&gt; 不能填 Docker container network 的位址，所以我們把 fluent-bit:24224 映射到實體素主機的 24224，Traefik 的 &lt;code&gt;fluentd-address&lt;&#x2F;code&gt; 則填入 &lt;code&gt;127.0.0.1:24224&lt;&#x2F;code&gt;，如此方可讓 Traefik log 送到 Fluent Bit，如果宿主機是 Mac，那又多了一層 Docker VM，所以得改成 &lt;code&gt;host.docker.internal:24224&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;fluent-bit&lt;&#x2F;code&gt; 服務作為非主要之週邊服務，它不應成為其他服務的依賴，不應列入其他服務的 &lt;code&gt;depends_on&lt;&#x2F;code&gt;，所以啟用 &lt;code&gt;fluentd-async&lt;&#x2F;code&gt; 來讓 Traefik 能自行啟動並送出 log，不需要關心 Fluent Bit 啟用與否。&lt;&#x2F;p&gt;
&lt;p&gt;至此一套具體而微的 Fluent Bit ＋ Traefik 系統就配置完成了，一套組合拳下來，Traefik log 輸出到容器的 stdout &#x2F; stderr，然後由 Docker logging driver 轉發給 Fluent Bit，再轉發給 CloudWatch，一筆筆 log 就這麼風塵僕僕的抵達了 CloudWatch。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>怎麼算有小數點的錢？</title>
        <published>2025-06-15T00:00:00+00:00</published>
        <updated>2025-06-15T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2025/floating-point/"/>
        <id>https://editor.leonh.space/2025/floating-point/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2025/floating-point/">&lt;p&gt;離上篇文過去了半年，連載再開！&lt;&#x2F;p&gt;
&lt;p&gt;台幣最小到元，在我小時候還見過五角的台幣，查了一下維基百科，實際上五角還是有效貨幣，不過實務上沒人用就是了，總之，雖然台灣交易一般算到元，不過其實帶有小數點的金額還發生在不同的角落，例如：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;B2B 交易，供應商報價有可能有小數點。&lt;&#x2F;li&gt;
&lt;li&gt;營業稅前金額，也有可能有小數點。&lt;&#x2F;li&gt;
&lt;li&gt;別國貨幣，也可能有角、分等，用元為單位的話也會有小數點。&lt;&#x2F;li&gt;
&lt;li&gt;油價，例如每公升 26.6 元。&lt;&#x2F;li&gt;
&lt;li&gt;通話費，例如每秒 0.05 元。&lt;&#x2F;li&gt;
&lt;li&gt;充電費，與通話費類似，例如每小時 10 元，以分計費會是每分鐘 0.166667 元。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;比較討厭的是時間計價，因為時間是 60 進位，換算成較細的單位後很容易遇到無窮小數問題。&lt;&#x2F;p&gt;
&lt;p&gt;對於電腦算錢，特別是有小數點的錢，如果不特別處理的話，一定會遇到浮點誤差，對於錢這麼重要的數字，誤差是不被允許的，那什麼是浮點誤差？&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fu-dian-wu-chai&quot;&gt;浮點誤差&lt;&#x2F;h2&gt;
&lt;p&gt;先看實際問題：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 0.1&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; +&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 0.2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;0.30000000000000004&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;0.1 加 0.2 竟然不是 0.3，很違背常理，但就這麼發生了。&lt;&#x2F;p&gt;
&lt;p&gt;用最簡單的話說，電腦底層是二進位，而一般帶有小數的十進位數字在轉換為二進位制時會出現無限循環，但電腦容量有限只能截斷，截斷後的二進位再轉回十進位時就會出現誤差。&lt;&#x2F;p&gt;
&lt;p&gt;就像 1&#x2F;3 以十進位表示是 0.3333… 無窮小數一樣，十進位制 0.1 轉為二進位制會是 0.0001100110011…，再轉回十進位制時就不再是 0.1：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 0.1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;0.1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; format&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;0.1&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;.17f&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;0.10000000000000001&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面程式碼中，一般情況下，Python 在顯示值時會幫我們「美化」，0.1 就是 0.1，但實際上，對 Python 而言，真正的值是不是精確的 0.1，而僅是近似值。再看 0.3 的例子：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 0.3&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;0.3&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; format&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;0.3&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;.17f&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;0.29999999999999999&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 0.1&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; +&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 0.2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;0.30000000000000004&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 0.1&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; +&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 0.2&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ==&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 0.3&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;False&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到，對電腦來說 0.3 不是 0.3，0.1 + 0.2 也不是 0.3，是不是很逆天？&lt;&#x2F;p&gt;
&lt;p&gt;由於以上種種，所以江湖上流傳一句話「算錢用浮點，遲早被人扁」。&lt;&#x2F;p&gt;
&lt;p&gt;對於浮點誤差比較簡明的解釋，可以看 Python 的這份文件〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.python.org&#x2F;zh-tw&#x2F;3.13&#x2F;tutorial&#x2F;floatingpoint.html&quot;&gt;浮點數運算：問題與限制&lt;&#x2F;a&gt;〉。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;0.30000000000000004.com&#x2F;&quot;&gt;Floating Point Math&lt;&#x2F;a&gt; 展示了各種語言 0.1 + 0.2 的結果。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-jue-fu-dian-wu-chai&quot;&gt;解決浮點誤差&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;di-yi-zhao-bu-yong-xiao-shu&quot;&gt;第一招——不用小數&lt;&#x2F;h3&gt;
&lt;p&gt;解決浮點誤差的第一種思路是用更小的單位算錢，因為十進制二進制轉換誤差只發生在小數點，那不用小數點總可以了吧，我用「分」算錢總可以了吧。&lt;&#x2F;p&gt;
&lt;p&gt;因為浮點誤差在多次運算中都會各自產生並累積，這個方案是在運算與儲存都以「分」為單位，只在最終顯示時才轉為以「元」表示。&lt;&#x2F;p&gt;
&lt;p&gt;但要注意的是，浮點誤差也會發生在資料庫，所以這招也得在儲存時以「分」儲存金額，如果資料庫又給其他應用或報表提供資料，它們也得以「分」計算並以「元」顯示。如果是大應用，有多個資料庫與子應用，很容易會精神錯亂，這邊忘記除以一百，那邊忘記乘以一百，或是欄位 A 是分，欄位 B 是元等狀況。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;di-er-zhao-decimal&quot;&gt;第二招——Decimal&lt;&#x2F;h3&gt;
&lt;p&gt;儘管大部分程式語言都有浮點誤差問題，但它們也都提供了類似的解決方案——decimal，不論是內建或外裝，大多程式語言都有個 decimal 工具，例如 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;MikeMcl&#x2F;decimal.js&quot;&gt;decimal.js&lt;&#x2F;a&gt; 或是 Python 內建的 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.python.org&#x2F;zh-tw&#x2F;3.11&#x2F;library&#x2F;decimal.html&quot;&gt;decimal&lt;&#x2F;a&gt; 模組。&lt;&#x2F;p&gt;
&lt;p&gt;最基礎的 decimal 用法：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt; from&lt;&#x2F;span&gt;&lt;span&gt; decimal&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Decimal&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; Decimal(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;3.14&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Decimal(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;3.14&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這個 3.14 就是 3.14，不會是 3.1401 也不會是 3.1399。&lt;&#x2F;p&gt;
&lt;p&gt;要注意的是，建立 &lt;code&gt;Decimal&lt;&#x2F;code&gt; 物件最好餵字串而不是數字，如果餵數字的話：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; Decimal(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;3.14&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Decimal(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;3.140000000000000124344978758017532527446746826171875&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Python 會以已經發生浮點誤差的數字建立 &lt;code&gt;Decimal&lt;&#x2F;code&gt; 物件，而這當然與我們的預期不符。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;Decimal&lt;&#x2F;code&gt; 物件可以直接與另一個 &lt;code&gt;Decimal&lt;&#x2F;code&gt; 物件或整數做算數運算，但不能和浮點數做運算：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; Decimal(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;3.14&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; +&lt;&#x2F;span&gt;&lt;span&gt; Decimal(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;1.618&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Decimal(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;4.758&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; Decimal(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;2&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; +&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 3&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Decimal(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;5&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; Decimal(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;3&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; +&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1.618&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# TypeError: unsupported operand type(s) for +: &amp;#39;decimal.Decimal&amp;#39; and &amp;#39;float&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Decimal 模組還可以依需求設定小數位數與進退位規則，在程式內我們會盡可能保有全部小數點，採取高精度運算，但最後秀給用戶的值可能只秀到小數點三位，甚至只秀整數，就可以利用 &lt;code&gt;quantize()&lt;&#x2F;code&gt; 方法與 &lt;code&gt;ROUND_HALF_UP&lt;&#x2F;code&gt; 做四捨五入：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt; from&lt;&#x2F;span&gt;&lt;span&gt; decimal&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Decimal,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; ROUND_HALF_UP&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; total_cost&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; Decimal(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;3.1415927&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; total_cost.quantize(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;exp&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;Decimal(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;0.001&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;),&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt; rounding&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;ROUND_HALF_UP&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Decimal(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;3.142&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;餵給 &lt;code&gt;quantize()&lt;&#x2F;code&gt; 的 &lt;code&gt;exp&lt;&#x2F;code&gt; 參數也是一個 &lt;code&gt;Deciaml&lt;&#x2F;code&gt; 物件，表示我們想要的位數，而第二個參數望文生義就是進退位規則啦，&lt;code&gt;ROUND_HALF_UP&lt;&#x2F;code&gt; 表示四捨五入，另外也有無條件進位、無條件捨去等規則。&lt;&#x2F;p&gt;
&lt;p&gt;前面說到電腦底層是以二進位制處理數字，那 decimal 是怎麼解決浮點誤差的呢？其實它也是用上第一招，全部改為整數就好，不過相對於我們自幹的土炮方式，decimal 設計的更周全，除了可以自定位數、進退位規則外，&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.python.org&#x2F;zh-tw&#x2F;3.11&#x2F;library&#x2F;decimal.html&quot;&gt;decimal&lt;&#x2F;a&gt; 還有好多其他特性，並且我們不用自己操煩單位換算的工作，省心省力。&lt;&#x2F;p&gt;
&lt;p&gt;Decimal 唯一的缺點是慢，因為它以整數形態儲存數值，並且還要額外的空間存放小數位數、正負號等資訊，相較於以浮點運算器加速的原生浮點數，decimal 顯然要慢得多了，幸好這種慢在體感上並不明顯，特別是在講究數字精確性的應用中，慢這麼一點點更顯得微不足道。&lt;&#x2F;p&gt;
&lt;p&gt;在儲存方面，各大資料庫也有提供相同的 DECIMAL 或 NUMERIC 型別，它們提供與上述 decimal 相似的特性，所以簡單的說，像錢這樣重要又可能帶小數的數字，不管是在應用層還是資料層，最好都用 decimal 來處理，從此再也沒有出門被人扁的風險。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wu-qiong-xiao-shu-wen-ti&quot;&gt;無窮小數問題&lt;&#x2F;h2&gt;
&lt;p&gt;前面提到的浮點誤差，來自十進位制與二進位制轉換時的無限循環，然而除此之外，有一些數字本身就是無窮小數，例如 1&#x2F;3 = 0.3333…。&lt;&#x2F;p&gt;
&lt;p&gt;以高功率充電樁為例，以時間計價一分鐘 28 元，即便在表面上是以分計費，但在系統實作層面，為了提供不同充電站業者足夠大的彈性，還是可能得精算到秒，以免哪個歐洲客戶想以 30 秒計費，或者要做階梯費率什麼的。&lt;&#x2F;p&gt;
&lt;p&gt;總之，以秒計費的話，一秒 28&#x2F;60 元，相當於 0.046666… 元，這種本身就是無窮小數的狀況就不能靠 decimal 了，Python 有為此開立特別處方 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.python.org&#x2F;zh-tw&#x2F;3.11&#x2F;library&#x2F;fractions.html&quot;&gt;fractions&lt;&#x2F;a&gt; 模組，與 decimal 狀況類似，JS 也有 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rawify&#x2F;Fraction.js&quot;&gt;fraction.js&lt;&#x2F;a&gt; 可供使用。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;fraction&quot;&gt;Fraction&lt;&#x2F;h3&gt;
&lt;p&gt;建立 &lt;code&gt;Fraction&lt;&#x2F;code&gt; 物件就是為給它分子、分母：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt; from&lt;&#x2F;span&gt;&lt;span&gt; fractions&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Fraction&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; Fraction(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 3&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Fraction(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 3&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;Fraction(1, 3)&lt;&#x2F;code&gt; 表示分數 1&#x2F;3。&lt;&#x2F;p&gt;
&lt;p&gt;與 decimal 類似，如果有小數，最好以字串形式餵入，避免浮點誤差：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; Fraction(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;1.1&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Fraction(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;2476979795053773&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 2251799813685248&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; Fraction(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;1.1&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Fraction(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;11&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 10&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在這個例子裡沒餵分母，分母會是 1，就是分數 1.1&#x2F;1。&lt;&#x2F;p&gt;
&lt;p&gt;要做運算的話，其他運算元最好也是 &lt;code&gt;Fraction&lt;&#x2F;code&gt; 或整數物件，如果其他運算元是浮點數的話，那結果會變成不精確的浮點數：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; Fraction(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;28&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 60&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; *&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 0.5&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;0.23333333333333334&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; Fraction(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;28&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 60&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; *&lt;&#x2F;span&gt;&lt;span&gt; Fraction(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;0.5&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Fraction(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;7&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 30&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Fraction 與 decimal 用起來頗相似，也一樣有較慢的問題，但也一樣體感不明顯，是追求數字正確時可接受的慢。&lt;&#x2F;p&gt;
&lt;p&gt;與 decimal 不同的是，資料庫沒有類似 fraction 的型別，可以考慮轉成 decimal 儲存，或是如果需要追求極度精確的話，以字串儲存：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;span&gt;(Fraction(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;7&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 30&lt;&#x2F;span&gt;&lt;span&gt;))&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;7&#x2F;30&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;算錢大概就這樣，祝大家數錢數到手抽筋 💸。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>如何在 macOS 掛載 ext4</title>
        <published>2024-12-25T00:00:00+00:00</published>
        <updated>2024-12-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2024/macos-ext4/"/>
        <id>https://editor.leonh.space/2024/macos-ext4/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2024/macos-ext4/">&lt;p&gt;上禮拜主要工作的桌機突然掛掉，經過一番交叉測試，應該是主機板掛了，還好還有筆電，接上大螢幕還可以繼續工作，程式專案的部分有遠端 Git 庫存不怕掉檔案，但其他一些文件、相片就要想辦法讀出來了，macOS 讀 Windows 的 exFAT、NTFS 沒問題，反倒是不支援 ext4，這點令人感到意外，調查一波後發現，macOS 要掛上 ext4 並不簡單，要抉擇用哪些工具鏈，還要自行編譯，自己曲曲折折走對一條路後寫下這篇筆記。&lt;&#x2F;p&gt;
&lt;p&gt;macOS 原生不支援 ext4，當插入 ext4 硬碟，系統可以認得有硬碟插入，在磁碟工具程式可以看見它以及分配的設備編號（例如 disk4s1），但無法掛載，我們需要利用 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;zh-tw&#x2F;FUSE&quot;&gt;FUSE&lt;&#x2F;a&gt; 機制來掛載它。&lt;&#x2F;p&gt;
&lt;p&gt;在 macOS 世界，FUSE 底層套件有兩個選擇，傳統的 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;macfuse&#x2F;macfuse&quot;&gt;macFUSE&lt;&#x2F;a&gt;（= OSXFUSE = Fuse4X）使用 macOS 核心擴充機制（KEXT），&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;support.apple.com&#x2F;zh-tw&#x2F;guide&#x2F;deployment&#x2F;depa5fb8376f&#x2F;web#:~:text=%E3%80%90%E9%87%8D%E8%A6%81%E4%BA%8B%E9%A0%85%E3%80%91macOS%20%E4%B8%8D%E5%86%8D%E5%BB%BA%E8%AD%B0%E4%BD%BF%E7%94%A8%20KEXT%E3%80%82KEXT%20%E6%9C%83%E8%AE%93%E4%BD%9C%E6%A5%AD%E7%B3%BB%E7%B5%B1%E9%81%AD%E5%8F%97%E5%AE%8C%E6%95%B4%E6%80%A7%E5%92%8C%E5%8F%AF%E9%9D%A0%E6%80%A7%E7%9A%84%E9%A2%A8%E9%9A%AA%E3%80%82%E4%BD%BF%E7%94%A8%E8%80%85%E6%87%89%E8%A9%B2%E6%9B%B4%E5%96%9C%E6%AD%A1%E4%B8%8D%E9%9C%80%E8%A6%81%E6%93%B4%E5%B1%95%E6%A0%B8%E5%BF%83%E4%B8%A6%E4%BD%BF%E7%94%A8%E7%B3%BB%E7%B5%B1%E5%BB%B6%E4%BC%B8%E5%8A%9F%E8%83%BD%E7%9A%84%E8%A7%A3%E6%B1%BA%E6%96%B9%E6%A1%88%E3%80%82&quot;&gt;KEXT 已經逐漸被淘汰&lt;&#x2F;a&gt;，所以我選用無 KEXT 成分的 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;macos-fuse-t&#x2F;fuse-t&quot;&gt;FUSE-T&lt;&#x2F;a&gt;，FUSE-T 是以 NFS 的方式掛載，讓 ext4 或其他檔案系統變成一個假的網路硬碟。&lt;&#x2F;p&gt;
&lt;p&gt;要把整套工具搭起來需要：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Xcode command line tools，內含 Git、make 等開發工具。&lt;&#x2F;li&gt;
&lt;li&gt;Homebrew，macOS 世界的套件管理器。&lt;&#x2F;li&gt;
&lt;li&gt;FUSE-T，FUSE 底層套件。&lt;&#x2F;li&gt;
&lt;li&gt;pkg-config，編譯 ext4fuse 所需工具。&lt;&#x2F;li&gt;
&lt;li&gt;ext4fuse，掛載 ext4 硬碟工具。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;xcode-command-line-tools&quot;&gt;Xcode command line tools&lt;&#x2F;h2&gt;
&lt;p&gt;一行安裝：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; xcode-select&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --install&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;裝完就有 Git 和 make 可用了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;homebrew&quot;&gt;Homebrew&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;brew.sh&#x2F;&quot;&gt;Homebrew&lt;&#x2F;a&gt; 是 macOS 世界的套件管理器，它也可以在 Linux 工作，只不過沒什麼人用罷了，Linux 早有 APT 等主流套件管理器了，Homebrew 安裝請參見它的文件。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fuse-t&quot;&gt;FUSE-T&lt;&#x2F;h2&gt;
&lt;p&gt;有了 Homebrew，安裝 FUSE-T 就很簡單了：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; brew install macos-fuse-t&#x2F;homebrew-cask&#x2F;fuse-t&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;裝完它會在 &#x2F;usr&#x2F;local&#x2F;lib&#x2F; 內，另外 &#x2F;usr&#x2F;local&#x2F;lib&#x2F;pkgconfig&#x2F;fuse-t.pc 為 FUSE-T 的 pkg-config 配置檔，內容如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;prefix=&#x2F;usr&#x2F;local&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;exec_prefix=${prefix}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;libdir=${prefix}&#x2F;lib&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;includedir=${prefix}&#x2F;include&#x2F;fuse&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Name: fuse-t&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Description: Userspace FUSE implementation for macOS&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Version: 1.0.44&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Libs:  -L${libdir} -Wl,-rpath,${libdir} -lfuse-t&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Cflags: -I${includedir}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;不論是 macFUSE 或 FUSE-T，他們都是 FUSE 機制的底層套件，具體要掛載 ext4 或其他檔案系統，還要另外安裝各自套件，後續我們需要 ext4fuse，並且還需要自行編譯。&lt;&#x2F;p&gt;
&lt;p&gt;編譯 ext4fuse 需要引用 FUSE-T 的 fuse.h，根據上面的定義，fuse.h 應該位在 &lt;code&gt;includedir&lt;&#x2F;code&gt; 變數位置，也就是 &#x2F;usr&#x2F;local&#x2F;include&#x2F;fuse&#x2F; 內，而 fuse.h 及其他源碼在專案 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;macos-fuse-t&#x2F;libfuse&quot;&gt;libfuse&lt;&#x2F;a&gt; 內，我們把它 clone 下來：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; git clone https:&#x2F;&#x2F;github.com&#x2F;macos-fuse-t&#x2F;libfuse&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然後把 libfuse&#x2F;include&#x2F; 所有檔案複製到 &#x2F;usr&#x2F;local&#x2F;include&#x2F;fuse&#x2F; 內：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo mkdir &#x2F;usr&#x2F;local&#x2F;include&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo mkdir &#x2F;usr&#x2F;local&#x2F;include&#x2F;fuse&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo cp&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -R&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ~&#x2F;Projects&#x2F;libfuse&#x2F;include&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;*&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &#x2F;usr&#x2F;local&#x2F;include&#x2F;fuse&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;至此前置備料完成一半，另一半是裝 pkg-config。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pkg-config&quot;&gt;pkg-config&lt;&#x2F;h2&gt;
&lt;p&gt;pkg-config 是編譯期間的相依套件管理工具，前面我們已經備好 FUSE-T 的 pkg-config 配置以及源碼，但還需要安裝 pkg-config 工具本身，為了避免落入編譯輪迴地獄，這裡我們用速成方式一行搞定：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; brew install pkgconf&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pkgconf&#x2F;pkgconf&quot;&gt;pkgconf&lt;&#x2F;a&gt; 是與 pkg-confg 相同，重新發明的輪子，具體差異為何不深究，總之安裝很間單。&lt;&#x2F;p&gt;
&lt;p&gt;至此備料完畢，總算可以編譯了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ext4fuse&quot;&gt;ext4fuse&lt;&#x2F;h2&gt;
&lt;p&gt;ext4fuse 是掛載 ext4 硬碟工具，注意它只能讀不能寫。&lt;&#x2F;p&gt;
&lt;p&gt;ext4fuse 編譯時需要引用 fuse.h，而因為我們用的是 FUSE-T，因此這 fuse.h 也必須是 FUSE-T 的 fuse.h，這句話出現了六次「fuse」，看來頗令人困惑，總之就是 ext4fuse 也要選用 FUSE-T 的修改版，首先把它 clone 下來，注意網址中間是「macos-fues-t」：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; git clone https:&#x2F;&#x2F;github.com&#x2F;macos-fuse-t&#x2F;ext4fuse&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然後跑 make：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; make&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;下面出現一陣幾哩呱啦，警告什麼的，先無視，編譯完，專案資料夾內應該會出現一支執行檔 ext4fuse，終於我們可以掛上 ext4 了：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; mkdir ~&#x2F;ext4_mount&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo .&#x2F;ext4fuse &#x2F;dev&#x2F;disk4s1 ~&#x2F;ext4_mount&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -o&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; allow_other&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;此時在 Finder 應該就可以看到掛上去的 ext4 了，賀 🎉。&lt;&#x2F;p&gt;
&lt;p&gt;用完卸載：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo umount ~&#x2F;ext4_mount&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果卸載失敗，就先關電腦，再拔 ext4 硬碟，這樣比較安全。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>重新認識異步</title>
        <published>2024-12-01T00:00:00+00:00</published>
        <updated>2024-12-01T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2024/async/"/>
        <id>https://editor.leonh.space/2024/async/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2024/async/">&lt;p&gt;異步是 Python 3.4 起引入的重大變革，透過 asyncio 模組與新的 &lt;code&gt;async&lt;&#x2F;code&gt;、&lt;code&gt;await&lt;&#x2F;code&gt; 語法達到與 JavaScript 類似的異步執行效果，這篇寫一下異步的用法與特性。&lt;&#x2F;p&gt;
&lt;p&gt;一個最簡單的異步程式例子：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;import&lt;&#x2F;span&gt;&lt;span&gt; asyncio&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;async def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; main&lt;&#x2F;span&gt;&lt;span&gt;():&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    print&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;hello&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    await&lt;&#x2F;span&gt;&lt;span&gt; asyncio.sleep(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;10&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    print&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;world&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;if&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; __name__&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ==&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;__main__&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    asyncio.run(main())&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在上面的例子內有幾個 Python 異步程式的特徵：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;由 &lt;code&gt;async&lt;&#x2F;code&gt; 關鍵字定義 &lt;code&gt;main()&lt;&#x2F;code&gt; 為異步函式。&lt;&#x2F;li&gt;
&lt;li&gt;函式內有 &lt;code&gt;await&lt;&#x2F;code&gt; 關鍵字的敘述句表示該敘述為異步執行。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;await&lt;&#x2F;code&gt; 後呼叫的函式 &lt;code&gt;asyncio.sleep()&lt;&#x2F;code&gt; 為一個具有 awaitable 的函式，對 &lt;code&gt;await&lt;&#x2F;code&gt; 關鍵字來說，後面呼叫的函式有 awaitable 屬性是必須的，否則會引發錯誤。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;asyncio.run()&lt;&#x2F;code&gt; 負責把 &lt;code&gt;main()&lt;&#x2F;code&gt; 丟進 event loop 執行。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;xie-cheng-yu-event-loop&quot;&gt;協程與 event loop&lt;&#x2F;h2&gt;
&lt;p&gt;如果我們直接跑那個 async 定義的 &lt;code&gt;main()&lt;&#x2F;code&gt;，Python 會告訴我們此函式它生成一個 coroutine（協程）物件，並且因為沒有 asyncio 模組幫忙把它丟給 event loop，此時函式不會被執行：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;main()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span&gt;coroutine&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; object&lt;&#x2F;span&gt;&lt;span&gt; main at&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; 0x&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;1053bb7c8&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;從這裡我們可以瞭解到，async 函式會生成 coroutine（協程）物件，它們是反應物與生成物的關係，口語上，我們比較常講「async 函式」，實際上，在 event loop 中被執行的是生成的 coroutine 物件，書面上或口語上可以把它們等而視之。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E4%BA%8B%E4%BB%B6%E7%8E%AF&quot;&gt;Event loop&lt;&#x2F;a&gt; 接受並調度一系列的 async 函式（嚴謹的說是協程物件）。不像上面簡化的例子，在真實的場景中，往往有多個工作需要並行前進，例如某個 API 後端，要一邊持續收 message queue 訊息，一邊要回應客戶端請求，又例如要並行下載多個檔案等等，這類涉及網路 IO 與磁碟 IO 的任務統稱為 IO-bound 任務，IO-bound 特性是 IO 存取頻繁，而 CPU 運算強度不高，如同 asyncio 名字所表示，它特別適合處理 IO-bound 類並行任務，反之如果是 CPU-bound 或 GPU-bound 這類運算密集型任務，例如算圖、音訊、視訊、影像處理，則比較適合用多進程解決，發揮 CPU 多核心的特長，或是調用 GPU 運算函式庫處理，當然也有某些應用場景可能既是 IO-bound 又是 CPU-bound 也是 GPU-bound，那它就會同時用上多進程多線程 asyncio + CUDA。&lt;&#x2F;p&gt;
&lt;p&gt;Python event loop 的一項特性是一個線程只能跑一個 event loop，event loop 不能跨線程，但是如上段結尾所提，我可以開很多個線程，每個線程都跑自己的 event loop，當然，要玩多進程又多線程還要跑 asyncio &#x2F; event loop，就很考驗我們的調度能力了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;async-await&quot;&gt;async &#x2F; await&lt;&#x2F;h2&gt;
&lt;p&gt;回到上文程式範例，event loop 會自行調度每個協程任務，無需人為介入，執行 &lt;code&gt;await asyncio.sleep(10)&lt;&#x2F;code&gt; 語句時，如果在這個 event loop 中還有其他協程任務時，睡著的這十秒並不會阻塞其他協程任務進行，在睡覺期間，event loop 會叫起別的協程任務運行，如此達到並行運算的效果。&lt;&#x2F;p&gt;
&lt;p&gt;然而因為 &lt;code&gt;await&lt;&#x2F;code&gt; 的關係，Python 會確保下一句 &lt;code&gt;print(&#x27;world&#x27;)&lt;&#x2F;code&gt; 在睡完後再執行，因為我們還是要求程式是有序執行的嘛，如果程式主要邏輯變成這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;await&lt;&#x2F;span&gt;&lt;span&gt; asyncio.sleep(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;10&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;await&lt;&#x2F;span&gt;&lt;span&gt; asyncio.sleep(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;10&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;那 Python 會睡幾秒？答案是二十秒，它們並沒有「並行睡十秒」，這似乎和前文說的並行運算有所違背，癥結就在那個 &lt;code&gt;await&lt;&#x2F;code&gt;，它保證了程式執行的有序性，問題來了，既然有序，又如何並行？&lt;&#x2F;p&gt;
&lt;h2 id=&quot;taskgroup&quot;&gt;TaskGroup&lt;&#x2F;h2&gt;
&lt;p&gt;從 Python 3.11 起，對於並行協程，引入了 task group 方案，在此之前，什麼 &lt;code&gt;asyncio.create_task()&lt;&#x2F;code&gt;、&lt;code&gt;asyncio.gather()&lt;&#x2F;code&gt; 等等的招式在此不提，task group 跑並行協程範例如下，這次我們用比較實際的（假裝）並行下載示範：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;import&lt;&#x2F;span&gt;&lt;span&gt; asyncio&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;async def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; download&lt;&#x2F;span&gt;&lt;span&gt;(url:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;span&gt;, sleep_duration:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; int&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  await&lt;&#x2F;span&gt;&lt;span&gt; asyncio.sleep(sleep_duration)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  print&lt;&#x2F;span&gt;&lt;span&gt;(url)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  return&lt;&#x2F;span&gt;&lt;span&gt; url&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;url1&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;task1&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;url2&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;task2&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;async def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; main&lt;&#x2F;span&gt;&lt;span&gt;():&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  async with&lt;&#x2F;span&gt;&lt;span&gt; asyncio.TaskGroup()&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; as&lt;&#x2F;span&gt;&lt;span&gt; tg:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    task1&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; tg.create_task(download(url1,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 10&lt;&#x2F;span&gt;&lt;span&gt;))&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    task2&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; tg.create_task(download(url2,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 3&lt;&#x2F;span&gt;&lt;span&gt;))&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    task1_result&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; = await&lt;&#x2F;span&gt;&lt;span&gt; task1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    task2_result&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; = await&lt;&#x2F;span&gt;&lt;span&gt; task2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    print&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;All done&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  return&lt;&#x2F;span&gt;&lt;span&gt; (task1_result, task2_result)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;if&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; __name__&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ==&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;__main__&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    asyncio.run(main())&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;輸出為：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;task2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;task1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;All done&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;並且此時總時長為 10 秒，不是 13 秒。&lt;&#x2F;p&gt;
&lt;p&gt;來認識一下 task group：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;async with xxx&lt;&#x2F;code&gt; 啟動了異步的 context，而 &lt;code&gt;asyncio.TaskGroup()&lt;&#x2F;code&gt; 本身就是異步 context 的生成器，它負責實作異步 context 要求的 &lt;code&gt;aenter()&lt;&#x2F;code&gt; 與 &lt;code&gt;aexit()&lt;&#x2F;code&gt;，如同一般的 context 生成器一樣，只不過前綴了 &lt;code&gt;a&lt;&#x2F;code&gt; 表示 async，異步 context 裡面理所當然的可以執行 &lt;code&gt;await xxx()&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;task1 = tg.create_task(download(url1, 10))&lt;&#x2F;code&gt; 這句會生成一個 &lt;code&gt;Task&lt;&#x2F;code&gt; 物件，這也是 event loop 的管理對象，一個 coroutine 可以顯式或隱式的被封裝成 &lt;code&gt;Task&lt;&#x2F;code&gt; 物件，然後交由 event loop 管理與執行，這裡我們需要關注的是 &lt;code&gt;Task&lt;&#x2F;code&gt; 物件一旦生成就立刻被執行，因此 &lt;code&gt;task1&lt;&#x2F;code&gt; 與 &lt;code&gt;task2&lt;&#x2F;code&gt; 兩者幾乎是同時執行，也就是並行。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;code&gt;await task1&lt;&#x2F;code&gt; 用於取得 &lt;code&gt;task1&lt;&#x2F;code&gt; 的回傳值，如上點所說 &lt;code&gt;task1&lt;&#x2F;code&gt; 本身不是函式，它是個 &lt;code&gt;Task&lt;&#x2F;code&gt; 物件，所以是 &lt;code&gt;await task1&lt;&#x2F;code&gt;，後面不用加括號。&lt;code&gt;await xxx&lt;&#x2F;code&gt; 在這裡不影響兩個任務的進行順序，如上點所說，它們在各自建立為 &lt;code&gt;Task&lt;&#x2F;code&gt; 物件時就已經執行了，所以我們看到先輸出的是只睡三秒的 task2 而不是先 &lt;code&gt;await&lt;&#x2F;code&gt; 的 task1。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;code&gt;Task&lt;&#x2F;code&gt; 是 asyncio 的一個類，顧名思義，就是任務。前面定義的異步函式、coroutine 物件都需要被&lt;strong&gt;顯式&lt;&#x2F;strong&gt;的或&lt;strong&gt;隱式&lt;&#x2F;strong&gt;的轉換成 &lt;code&gt;Task&lt;&#x2F;code&gt; 物件才能交給 event loop 去運行，&lt;code&gt;Task&lt;&#x2F;code&gt; 物件可以是一份任務，或者是 N 份任務的清單。&lt;&#x2F;p&gt;
&lt;p&gt;透過 &lt;code&gt;create_task()&lt;&#x2F;code&gt; 去運行多個任務，就可以做到並行的效果。&lt;&#x2F;p&gt;
&lt;p&gt;再舉個實際例子，某個專案要訂閱 MQTT 訊息，把訊息轉入 MongoDB job queue，又要跑 job worker，還要透過 ZeroMQ 把 job 處理結果丟出去，這麼多事摻在一起，至少有兩個無限迴圈要跑：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;監看 queue job 的 job worker&lt;&#x2F;li&gt;
&lt;li&gt;監聽 MQTT 訊息的訂閱器&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;此專案啟動範例如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;import&lt;&#x2F;span&gt;&lt;span&gt; mongodb&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;import&lt;&#x2F;span&gt;&lt;span&gt; mqtt&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;import&lt;&#x2F;span&gt;&lt;span&gt; zeromq&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;async def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; main&lt;&#x2F;span&gt;&lt;span&gt;():&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  # 先連接到 MongoDB&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  await&lt;&#x2F;span&gt;&lt;span&gt; mongodb.initialize()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  # 再綁定 ZeroMQ&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  await&lt;&#x2F;span&gt;&lt;span&gt; zeromq.bind_address()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  async with&lt;&#x2F;span&gt;&lt;span&gt; asyncio.TaskGroup()&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; as&lt;&#x2F;span&gt;&lt;span&gt; tg:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    # 跑 MongoDB job queue worker&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    tg.create_task(job_queue.run())&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    # 監聽 MQTT 訂閱訊息&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    tg.create_task(mqtt.listen())&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;if&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; __name__&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ==&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;__main__&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    asyncio.run(main())&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;主函式裡面透過 &lt;code&gt;await xxx&lt;&#x2F;code&gt; 確保了會先連線到 MongoDB 與綁定 ZeroMQ，才會去跑後面的 task group，而 task group 內跑 job worker 與接收 MQTT 訊息則是並行發生的，如此保證了該有序執行的有序執行，該並行運行的並行運行。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-yu&quot;&gt;結語&lt;&#x2F;h2&gt;
&lt;p&gt;其實用上 asyncio 很多時候是因為被套件所逼，一旦某套件說它得異步調用，那我們就得一路 &lt;code&gt;async&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;await&lt;&#x2F;code&gt; 下去，而 asyncio 對 IO-bound 工作究竟有沒有神奇的功效，目前說法莫衷一是，畢竟每個評價方式都不一樣，無論如何 asyncio 還是要用上的，等 Python GIL 封印解除，就可以到達提速五倍的境界了吧。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>前端權限檢查 CASL</title>
        <published>2024-08-15T00:00:00+00:00</published>
        <updated>2024-08-15T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2024/casl/"/>
        <id>https://editor.leonh.space/2024/casl/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2024/casl/">&lt;p&gt;本頻道之前寫過後端存取控制套件 &lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2024&#x2F;vakt&#x2F;&quot;&gt;Vakt&lt;&#x2F;a&gt; 與 &lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2024&#x2F;oso&#x2F;&quot;&gt;Oso&lt;&#x2F;a&gt;，這篇則來談談前端部份的權限檢查機制。&lt;&#x2F;p&gt;
&lt;p&gt;還是快速介紹一下常見的存取控制模式 RBAC 與 ABAC。&lt;&#x2F;p&gt;
&lt;p&gt;RBAC 全稱 role-based access control，以「角色」為基礎的權限控制，最常見的例子就是 Admin，不論我的帳號為何，只要被賦予了 Admin 角色，我就能做任何事。&lt;&#x2F;p&gt;
&lt;p&gt;相對於權限霸總 Admin，另一個角色 User 就弱小得多，通常只能讀取、查詢，不能異動資料，大多新帳號建立之初被賦予的就是 User 角色。&lt;&#x2F;p&gt;
&lt;p&gt;ABAC 全稱 attribute-based access control，它是較細粒度的存取控制模式，組織 A 旗下的用戶只能看到組織 A 的資料，看不到組織 B 的資料。&lt;&#x2F;p&gt;
&lt;p&gt;之所以能作到這些 XXAC，除了靠自己寫出很巢的 &lt;code&gt;if&lt;&#x2F;code&gt; 邏輯外，最好還是利用專門的套件來幫我們制定以及執行檢查邏輯。&lt;&#x2F;p&gt;
&lt;p&gt;在之前的文章談到後端存取控制，在前端這邊，好像可以沿用但又不完全一樣，舉個例子，前端的主選單完全與後端無關，Admin 角色登入可以看到用戶管理，別的角色看不到，或者 Admin 在畫面上多了幾顆超能力按鈕，別的角色看不到，即便後端也制定了只有 Admin 能對用戶帳號增刪改，別人做就吐 403 Forbidden，但這不表示前端可以大剌剌把那些超能力選單、按鈕都放出來給大家看，而這些超能力選單、按鈕又是 UI 層的東西，與後端的權限規則不直接相關，所以無法避免的前端得有自己的一套權限制定與檢查機制，這裡我們選用 CASL。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;casl&quot;&gt;CASL&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;casl.js.org&#x2F;v5&#x2F;en&#x2F;&quot;&gt;CASL&lt;&#x2F;a&gt;（唸作 &lt;code&gt;&#x2F;ˈkæsəl&#x2F;&lt;&#x2F;code&gt;）是 JavaScript 的存取控制套件，起手式當然是先安裝：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; npm install @casl&#x2F;ability&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;最最最簡單的用法：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;typescript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;&#x2F;&#x2F; ability.ts&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;import&lt;&#x2F;span&gt;&lt;span&gt; { defineAbility }&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; from&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;@casl&#x2F;ability&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;const&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; abilityFor&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;roles&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; string&lt;&#x2F;span&gt;&lt;span&gt;[])&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; defineAbility&lt;&#x2F;span&gt;&lt;span&gt;((&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;can&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    if&lt;&#x2F;span&gt;&lt;span&gt; (roles.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;includes&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;Admin&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;        can&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;see menu item&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;UsersMenu&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;        can&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;see menu item&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;SystemMenu&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;        can&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;delete&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;SystemLog&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;})&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;export default&lt;&#x2F;span&gt;&lt;span&gt; abilityFor&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這段程式的核心方法是 &lt;code&gt;defineAbility()&lt;&#x2F;code&gt; 與 &lt;code&gt;can()&lt;&#x2F;code&gt;，前者顧名思義就是﹍define ability﹍，而那 &lt;code&gt;can()&lt;&#x2F;code&gt; 的第一個參數是「幹什麼」，第二個參數是「對什麼」，就上例而言，只有 Admin 能：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;見到用戶管理選單&lt;&#x2F;li&gt;
&lt;li&gt;見到系統設定選單&lt;&#x2F;li&gt;
&lt;li&gt;刪除系統紀錄&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;當然這些只是規則，真正要用得呼叫它：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;typescript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;&#x2F;&#x2F; app.ts&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;import&lt;&#x2F;span&gt;&lt;span&gt; { abilityFor }&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; from&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;@&#x2F;ability&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; getUserRoles&lt;&#x2F;span&gt;&lt;span&gt;() {&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; return&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;admin&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;] }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;const&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; roles&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; getUserRoles&lt;&#x2F;span&gt;&lt;span&gt;()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;const&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; ability&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; abilityFor&lt;&#x2F;span&gt;&lt;span&gt;(roles)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;const&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; menusItems&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; MenuOptions&lt;&#x2F;span&gt;&lt;span&gt;[]&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    { label:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;home&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, name:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;首頁&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, icon: House, route:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;&#x2F;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt; },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    { label:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;transaction&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, name:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;交易清單&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, icon: List, route:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;&#x2F;transactions&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;if&lt;&#x2F;span&gt;&lt;span&gt; (ability.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;can&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;see menu item&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;UsersMenu&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)) {&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; &#x2F;&#x2F; true&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    menusItems.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;push&lt;&#x2F;span&gt;&lt;span&gt;({ label:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;user&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, name:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;用戶管理&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, icon: Users, route:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;&#x2F;users&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt; })&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我們先呼叫 &lt;code&gt;abilityFor()&lt;&#x2F;code&gt;，餵它當前用戶角色，做出 &lt;code&gt;ability&lt;&#x2F;code&gt; 物件，然後呼叫 &lt;code&gt;ability.can()&lt;&#x2F;code&gt; 確認是否可行，注意這裡的 &lt;code&gt;ability.can()&lt;&#x2F;code&gt; 不同於制定規則時的 &lt;code&gt;can()&lt;&#x2F;code&gt;，這裡的 &lt;code&gt;ability.can()&lt;&#x2F;code&gt; 用於檢查行為與對象有沒有准許。&lt;&#x2F;p&gt;
&lt;p&gt;因為範例中的角色是 Admin，當然是准許啦，就這樣 Admin 登入就可以看到用戶管理選單囉，反之，如果不是 Admin，拿到的答案會是 &lt;code&gt;false&lt;&#x2F;code&gt;，選單也就不可見囉。&lt;&#x2F;p&gt;
&lt;p&gt;不可見歸不可見，如果有個反派他手動打網址進 &#x2F;users 該怎麼辦？舉一反三一下，先制定規則，然後在前端路由程式那邊一樣呼叫 &lt;code&gt;ability.can()&lt;&#x2F;code&gt; 來確認就好，拿到 &lt;code&gt;False&lt;&#x2F;code&gt; 就把他擋掉。&lt;&#x2F;p&gt;
&lt;p&gt;對於現代化，具反應性、資料綁定的前端生態，例如 Vue.js，可以把那 &lt;code&gt;const ability = abilityFor(roles)&lt;&#x2F;code&gt; 放到 Pinia store 裡面，如此可以達到跨頁面使用，省去一直重複生成 &lt;code&gt;ability&lt;&#x2F;code&gt; 物件的工作，像我是弄個 &lt;code&gt;authStore&lt;&#x2F;code&gt;，所有與認證、授權有關的物件與方法都在裡面，大概長這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;typescript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;import&lt;&#x2F;span&gt;&lt;span&gt; abilityFor&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; from&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;@&#x2F;ability&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;import&lt;&#x2F;span&gt;&lt;span&gt; { defineStore }&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; from&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;pinia&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;import&lt;&#x2F;span&gt;&lt;span&gt; { computed, ref }&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; from&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;vue&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;export const&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; useAuthStore&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; defineStore&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  &#x2F;&#x2F; id&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;  &amp;#39;auth&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  &#x2F;&#x2F; storeSetup&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ()&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    &#x2F;&#x2F; States&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    const&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; token&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; ref&lt;&#x2F;span&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Token&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    &#x2F;&#x2F; Getters&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    const&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; payload&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; computed&lt;&#x2F;span&gt;&lt;span&gt;(()&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; return&lt;&#x2F;span&gt;&lt;span&gt; token.value.payload })&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    const&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; ability&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; computed&lt;&#x2F;span&gt;&lt;span&gt;(()&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;      return&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; abilityFor&lt;&#x2F;span&gt;&lt;span&gt;(payload.value?.roles)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    })&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    &#x2F;&#x2F; Actions&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    async function&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; signIn&lt;&#x2F;span&gt;&lt;span&gt;() { }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    async function&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; refresh&lt;&#x2F;span&gt;&lt;span&gt;() { }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    function&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; signOut&lt;&#x2F;span&gt;&lt;span&gt;() { }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    &#x2F;&#x2F; return states, getters, actions&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return&lt;&#x2F;span&gt;&lt;span&gt; { token, payload, ability, signIn, refresh, signOut }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如此一來，只要用戶登出換帳號登入，就會拿到新的 token，也就會拿到新的角色，也就會拿到只屬於自己的 &lt;code&gt;ability.can()&lt;&#x2F;code&gt; 答案了。&lt;&#x2F;p&gt;
&lt;p&gt;本文只介紹 CASL 最簡單的用法與概念，它能做的還多得多，不過簡單的版本已經能滿足一些粗粒度需求，其他以後有遇到再說吧。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>權限檢查利器 Vakt</title>
        <published>2024-08-13T00:00:00+00:00</published>
        <updated>2024-08-13T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2024/vakt/"/>
        <id>https://editor.leonh.space/2024/vakt/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2024/vakt/">&lt;p&gt;之前寫過一篇〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2024&#x2F;oso&#x2F;&quot;&gt;存取控制與 Oso&lt;&#x2F;a&gt;〉，一段時間下來也沒什麼不好，就是始終不適應 Oso 那獨家的權限規則語法 Polar，最近又偶然發現了 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;kolotaev&#x2F;vakt&quot;&gt;Vakt&lt;&#x2F;a&gt;，看起來它有可能成為 Oso 的替代品，就讓我來開箱一下吧。&lt;&#x2F;p&gt;
&lt;p&gt;Vakt 意思是挪威語的 guard。&lt;&#x2F;p&gt;
&lt;p&gt;與 Oso 一樣 Vakt 也是權限檢查套件，對於現代化應用，常見的權限模式有 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;zh-tw&#x2F;%E4%BB%A5%E8%A7%92%E8%89%B2%E7%82%BA%E5%9F%BA%E7%A4%8E%E7%9A%84%E5%AD%98%E5%8F%96%E6%8E%A7%E5%88%B6&quot;&gt;RBAC&lt;&#x2F;a&gt;，以「角色」基礎的權限管理模式，例如班長能批假單，如此以角色為中心做權限規則制定，不以一個個帳號去制定，畢竟角色應該是有限的，而帳號很有可能是無限的。&lt;&#x2F;p&gt;
&lt;p&gt;另一種典型的權限模式為 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Attribute-based_access_control&quot;&gt;ABAC&lt;&#x2F;a&gt;，它是更細化的權限管理模式，沿用上面的案例，不僅班長必須是班長，ABAC 還限制只有 A 班班長能批 A 班假單，B 班班長能批 B 班假單，化為程式碼大概長這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;if&lt;&#x2F;span&gt;&lt;span&gt; account_s.role&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ==&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;TEAM-LEADER&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    if&lt;&#x2F;span&gt;&lt;span&gt; account_s.team_id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ==&lt;&#x2F;span&gt;&lt;span&gt; application_form.account.team_id:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        return&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; True&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;類似的案例還有很多，像是作者 A 的貼文不能給 B 修改，但是網站編輯能修改所有作者的文，並且發佈權也在編輯手上。&lt;&#x2F;p&gt;
&lt;p&gt;像這樣零零碎碎的規則，想用 &lt;code&gt;if&lt;&#x2F;code&gt; 硬幹到底很有可能出錯，更好的方式是拿 Vakt 這類專門的&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;doublesllash.com&#x2F;blog&#x2F;posts&#x2F;2023&#x2F;rules-engine&quot;&gt;規則引擎&lt;&#x2F;a&gt;來替我們運算到底是 pass 還是 out。&lt;&#x2F;p&gt;
&lt;p style=&quot;background-color: hsla(0, 0%, 96%, 1.0); padding: 1rem; text-align: center;&quot;&gt;
&lt;a href=&quot;https:&#x2F;&#x2F;vocus.cc&#x2F;article&#x2F;66156de9fd89780001e83d2b&quot;&gt;繼續閱讀&lt;&#x2F;a&gt;
&lt;&#x2F;p&gt;
&lt;!-- 在 Vakt 的概念裡，能不能幹嘛依據三個面向判斷：

- action 意欲何在，例如常見的 create、read、update、delete 等。
- resource 所為何物，例如 Member、或特定的 member 1。
- subject 所來何人，例如 admin 或其他帳號，如果是 RBAC 的話就是哪個 role 囉。

在這三個面向的基礎上，我們撰寫 Vakt policy，以及向 Vakt 查詢是 allow 與否。

了解了概念，來看看程式吧：

```py
# access_control.py

from vakt import MemoryStorage, Guard, RulesChecker, PolicyAllow
from vakt.rules import Eq

from app.model import Account, Member

p01 = PolicyAllow(
    uid=&#x27;P01&#x27;,
    actions=[Eq(&#x27;list all&#x27;)],
    resources=[Eq(Account), Eq(Member)],
    subjects=[{&#x27;role&#x27;: Eq(&#x27;SYSTEM-ADMIN&#x27;)}],
)

storage = MemoryStorage()
storage.add(p01)

guard = Guard(storage, RulesChecker())
```

在這個簡單的例子中：

- `PolicyAllow`  class 表示這是一個正向表列的規則。
- 裡面我們賦予它一個識別碼 `uid=&#x27;P01&#x27;`。
- action 是 `&#x27;list all&#x27;`
- resource 可以是 `Account` 或 `Member` class。
- subject 必須是 role 為 `&#x27;SYSTEM-ADMIN&#x27;` 的帳號。

制定 policy 時，Vakt 提供了一些用於邏輯判斷的函式，像我們用到的 `Eq()`。

舉一反三一下，既然有正向表列的 `PolicyAllow` 那應該也會有負向表列的 `PolicyDeny`，就程式碼一致性而言，最好只採其一，如果一下正一下負，規則一多自己也會搞亂。

再舉一反三一下，有 `Eq()`，那在 `vakt.rules` 之下應該也還有其他的邏輯判斷函式，這裡不窮舉，後面有遇到再說。

上面這條規則，用白話講就是，只有 admin 可以列出所有帳號和會員，因此，對於一個列出所有帳號的 API 端點，每次收到請求時，它就得向 Vakt 查詢這請求是給過不給過呢？

```py
# AccountRouter.py

import vakt

from app.access_control import guard
from app.model import Account

account_list = [...]

@router.get(&#x27;&#x2F;accounts&#x27;)
def get_account_list(requestee_account: Account):
    request_account_dict = requestee_account.model_dump()

    # Check permissions
    inquiry = vakt.Inquiry(
        action=&#x27;list all&#x27;,
        resource=Account,
        subject=request_account_dict,
    )
    if not guard.is_allowed(inquiry):
        raise HTTPException(status.HTTP_403_FORBIDDEN)
    return account_list

```

在上面的例子中：

- `vakt.Inquiry()` 用於建構 Vakt 查詢物件，一樣填入那三要素：誰，對什麼，要幹嘛。
- `guard.is_allowed()` 會依查詢回覆 `True` 或 `False`，我們再依結果回應給客戶端，如果不允許，就回應 403。

如果請求者（requestee account）它的 `role` 欄位不是 `SYSTEM-ADMIN` 的話，會被 Vatk 無情地回絕，反之會欣然答應。

以上是 Vakt 最基本的用法，除了這些，以及上面提到舉一反三的部份，還有一些沒帶到的：

- policy 可以存在記憶體，也可以存到資料庫，實際上它是序列化為 JSON 存進資料庫，這也意味著 policy 可以由我們組成，存進資料庫，再讓 Vakt 讀取，這意味著 policy 可以隨需生成，例如交由 admin 帳號在前端勾選，後端生成，這個特性讓它在一些複雜應用中使用成為可能。
- 可以用程式碼的形式制定 policy，也可以用另一種以正規表達式的形式制定，個人對此敬而遠之。 --&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>存取控制與 Oso</title>
        <published>2024-08-12T00:00:00+00:00</published>
        <updated>2024-08-12T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2024/oso/"/>
        <id>https://editor.leonh.space/2024/oso/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2024/oso/">&lt;!-- [CASL](https:&#x2F;&#x2F;casl.js.org&#x2F;v5&#x2F;en&#x2F;)（唸作 `&#x2F;ˈkæsəl&#x2F;`）是 JavaScript 的存取控制套件，在進入 CASL 前，先簡單認識一下存取控制。 --&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.osohq.com&#x2F;&quot;&gt;Oso&lt;&#x2F;a&gt;（西班牙文的「熊」）是跨語言、跨框架的存取控制服務，它也有開源套件，本文主要介紹它的開源套件。&lt;&#x2F;p&gt;
&lt;p&gt;在進入 Oso 前，先簡單認識一下存取控制。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;cun-qu-kong-zhi&quot;&gt;存取控制&lt;&#x2F;h2&gt;
&lt;p&gt;「存取控制」英文叫 access control，市場上有許多不同的存取控制模型，他們的縮寫大多是 xAC，是 xxx access control 的意思。&lt;&#x2F;p&gt;
&lt;p&gt;存取控制是多人系統必備的要素之一，例如常見的 WordPress，它內建了幾個預先定義好的角色：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2024&#x2F;oso&#x2F;WP-roles.png&quot; alt=&quot;WordPress 角色&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;如圖所見，每個角色的能執行的權限不同，而每個帳號必定要賦予一個角色，形成「帳號—角色」這樣的關係，每個角色能執行的項目不同，例如編輯、管理員才能刪除文章，但除了這種系統面的權限外，每篇文章也有自身的權限，例如Ａ作者不能改Ｂ作者的文章。&lt;&#x2F;p&gt;
&lt;p&gt;像 WordPress 這樣，以角色為基礎的存取控制模型，我們稱為「&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;zh-tw&#x2F;%E4%BB%A5%E8%A7%92%E8%89%B2%E7%82%BA%E5%9F%BA%E7%A4%8E%E7%9A%84%E5%AD%98%E5%8F%96%E6%8E%A7%E5%88%B6&quot;&gt;RBAC（role-based access control）&lt;&#x2F;a&gt;」。&lt;&#x2F;p&gt;
&lt;p&gt;RBAC 的概念是以角色為中心的，也就是批假單的權限是綁在店長這個角色身上，而不是綁在王小臻身上，王小臻之所以能批假單，是因為她被賦予了店長的角色。&lt;&#x2F;p&gt;
&lt;p&gt;除了 RBAC 外，視系統的應用場景和複雜度不同還有其他的 xAC，未來有遇到再說，畫面先交還給棚內的 Oso。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;oso-jian-jie&quot;&gt;Oso 簡介&lt;&#x2F;h2&gt;
&lt;p&gt;一般要做一個 RBAC 機制，有可能自幹，但秉持著不要重新發明輪子的原則，還是交給專業的來吧！&lt;&#x2F;p&gt;
&lt;p&gt;快速帶過 Oso 的特點：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;跨平台，Python、Rust、Node.js、Ruby、Go、Java，這算跨很大了吧。&lt;&#x2F;li&gt;
&lt;li&gt;Oso 開發了一種專門用於制定安全政策的語言 Polar，加上 Oso 本身又是跨平台的，因此 &lt;strong&gt;Oso 這層存取控制可以與系統語言、框架本身作一定程度的分離&lt;&#x2F;strong&gt;，避免炒出義大利麵。&lt;&#x2F;li&gt;
&lt;li&gt;角色與權限應用的對象可以細至「資源」，可以做到像 WP 那樣Ａ不能改Ｂ的文章。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;背後有商業化服務，不用擔心哪天作者跑路或被腰斬。&lt;&#x2F;strong&gt;（請不要舉冨樫當反例謝謝）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p style=&quot;background-color: hsla(0, 0%, 96%, 1.0); padding: 1rem; text-align: center;&quot;&gt;
&lt;a href=&quot;https:&#x2F;&#x2F;vocus.cc&#x2F;article&#x2F;64f4c3dafd89780001ab5b65&quot;&gt;繼續閱讀&lt;&#x2F;a&gt;
&lt;&#x2F;p&gt;
&lt;!-- ## Oso 與 Flask

Oso 好不好強不強不知道，先用一波感受一下。

下面是一個極簡的 Flask + Oso 專案：

&lt;p style=&quot;background-color: hsla(0, 0%, 96%, 1.0); padding: 1rem; text-align: center;&quot;&gt;
&lt;a href=&quot;oso-python-quickstart-master.zip&quot;&gt;Oso 專案&lt;&#x2F;a&gt;
&lt;&#x2F;p&gt;

這個專案與 Oso 文件中的示範專案大致相同，但略作修改：

* 用 Poetry 取代 requirements.txt
* 相容於 Python 3.7 以上版本

進入 Poetry 虛擬環境後把這 Flask 跑起來：

```sh
FLASK_APP=app.server python -m flask run
```

就會看到下面的訊息：

```
 * Serving Flask app &#x27;app.server&#x27; (lazy loading)
 * Environment: production
   WARNING: This is a development server. Do not use it in a production deployment.
   Use a production WSGI server instead.
 * Debug mode: off
 * Running on http:&#x2F;&#x2F;127.0.0.1:5000 (Press CTRL+C to quit)
 ```

瀏覽器打開 http:&#x2F;&#x2F;localhost:5000&#x2F;repo&#x2F;gmail 會看到正確的內容：

![Gmail repo](gmail.png)

而打開 http:&#x2F;&#x2F;localhost:5000&#x2F;repo&#x2F;react 則會看到錯誤的內容：

![React repo](react.png)

為什麼會這樣呢？從 routing 開始找原因，打開 app&#x2F;server.py 可以看到負責處理上面路由的函式 `repo_show()`：

```py
@app.route(&quot;&#x2F;repo&#x2F;&lt;name&gt;&quot;)
def repo_show(name):
    repo = Repository.get_by_name(name)

    try:
        oso.authorize(
            actor=User.get_current_user(),
            action=&quot;read&quot;,
            resource=repo
        )
        return f&quot;&lt;h1&gt;A Repo&lt;&#x2F;h1&gt;&lt;p&gt;Welcome to repo {repo.name}&lt;&#x2F;p&gt;&quot;, 200
    except NotFoundError:
        return f&quot;&lt;h1&gt;Whoops!&lt;&#x2F;h1&gt;&lt;p&gt;Repo named {name} was not found&lt;&#x2F;p&gt;&quot;, 404
```

其中的 `try` 區塊負責做簡單的判斷，而其中呼叫的 `oso.authorize()` 又是主要關鍵，它的參數相當直白，它為我們回答「actor 能否對 resource 作 action？」，在這裡就是「user 能否對 repo 作 read？」，如果通過，該函式回傳 `None`，程式繼續往下走，如果不通過，它會發起一個例外，由 `except` 區塊接手處理。

根據現況，action 和 resource 都是已知的，那 `User.get_current_user()` 又是誰呢？根據 app&#x2F;server.py 的匯入模組來找，跑去翻 app&#x2F;models.py 就會發現，目前的 user 是 Larry 大大：

```py
@dataclass
class Repository:
    name: str
    is_public: bool = False

    @staticmethod
    def get_by_name(name):
        return repos_db.get(name)

@dataclass
class Role:
    name: str
    repository: Repository

@dataclass
class User:
    roles: List[Role]

    @staticmethod
    def get_current_user():
        return users_db[&quot;larry&quot;]

repos_db = {
    &quot;gmail&quot;: Repository(&quot;gmail&quot;),
    &quot;react&quot;: Repository(&quot;react&quot;, is_public=True),
    &quot;oso&quot;: Repository(&quot;oso&quot;),
}

users_db = {
    &quot;larry&quot;: User([Role(name=&quot;admin&quot;, repository=repos_db[&quot;gmail&quot;])]),
    &quot;anne&quot;: User([Role(name=&quot;maintainer&quot;, repository=repos_db[&quot;react&quot;])]),
    &quot;graham&quot;: User([Role(name=&quot;contributor&quot;, repository=repos_db[&quot;oso&quot;])]),
}
```

此外我們還看到了 Larry 大大所有的 repo 只有 gmail repo，而那 react repo 屬於另外一位 Anne 大大所有。

另外還可以注意到，那 react repo 有一個 `is_public=True` 屬性，這晚點會再提到。

截至目前為止，我們看的都是 Flask 或 Python 原生的邏輯，剩下的環節就是那 `oso.authorize()` 到底是依據什麼規則判斷權限的，react repo 不給看的邏輯在哪裡？我知道 Larry 沒有 react repo，但沒有歸沒有，中間具體判斷的邏輯在哪裡？

鏡頭轉向 Polar。

## Oso 的 Polar 規則定義文件

在 Oso 的世界，安全政策或安全規則是在 Polar 文件制定，app&#x2F;server.py 中有這幾行：

```py
# Initialize the Oso object. This object is usually used globally throughout
# an application.
oso = Oso()

# Tell Oso about the data you will authorize. These types can be referenced
# in the policy.
oso.register_class(User)
oso.register_class(Repository)

# Load your policy files.
oso.load_files([&quot;app&#x2F;main.polar&quot;])
```

這幾行分別負責：

* Oso 初始化
* 把兩個類 `User` 和 `Repository` 註冊給 Oso 待制定規則之用
* 從 app&#x2F;main.polar 載入安全規則

把那 app&#x2F;main.polar 開來看看：

```
actor User {}

resource Repository {
  permissions = [&quot;read&quot;, &quot;push&quot;, &quot;delete&quot;];
  roles = [&quot;contributor&quot;, &quot;maintainer&quot;, &quot;admin&quot;];

  # A user has the &quot;read&quot; permission if they have the
  # &quot;contributor&quot; role.
  &quot;read&quot; if &quot;contributor&quot;;

  # A user has the &quot;push&quot; permission if they have the
  # &quot;maintainer&quot; role.
  &quot;push&quot; if &quot;maintainer&quot;;

  # A user has the &quot;delete&quot; permission if they have the
  # &quot;admin&quot; role.
  &quot;delete&quot; if &quot;admin&quot;;

  # A user has the &quot;maintainer&quot; role if they have
  # the &quot;admin&quot; role.
  &quot;maintainer&quot; if &quot;admin&quot;;

  # A user has the &quot;contributor&quot; role if they have
  # the &quot;maintainer&quot; role.
  &quot;contributor&quot; if &quot;maintainer&quot;;
}

# This rule tells Oso how to fetch roles for a repository
has_role(actor: User, role_name: String, repository: Repository) if
  role in actor.roles and
  role_name = role.name and
  repository = role.repository;

allow(actor, action, resource) if
  has_permission(actor, action, resource);
```

這個 Polar 規則可以解構為幾個部份：

* 前面註冊的 `User` 類在此以 `actor User {}` 被聲明為 Polar 的 `actor`，在此應該將其理解為行為的發動者，不要和下面的 `roles` 搞混。
* 而 `Repository` 類則被聲明為 Polar 的 `resource`，並且被賦予了一些屬性和規則，分別有三種 `permissions` 和三種 `roles`，兩者混搭出一些存取規則。
* 一個 `has_role` 區塊。
* 一個 `allow` 區塊。

在 Oso 外面，我們是這樣呼叫它的：

```py
oso.authorize(
    actor=User.get_current_user(),
    action=&quot;read&quot;,
    resource=repo,
)
```

在 Oso 裡面，它的呼叫鏈是這樣的：

`allow` 呼叫 `has_permisssion` 呼叫 `has_role`。

實際上他們不是函式，所以用「呼叫」不太正確，但懂意思就好。

一個個來翻弄吧。

### allow

下面這塊：

```
allow(actor, action, resource) if
  has_permission(actor, action, resource);
```

意思顯而易見的，只要 `has_permission` 為真，則 `allow` 也為真，意即該 **Actor** 對該 **Resource** 的該 **Action** 通過。

此處的**Actor**、**Action**、**Resource** 如果是 Larry 大大請求 gmail repo 的話，則分別為：

* **Actor**：就是 Larry 大大，他是 gmail repo 的 admin。
* **Action**：就是讀取，`read`。
* **Resource**：就是 gmail repo。

把這三個值再代入 `has_permission`，它才能決定 `allow` 的是與非。

### has_permissioin

上面的 Polar 文件中沒有看到 `has_permission`，不過實際上是有的，只不過被簡化了。

資源內的每條規則都是簡化的形式，例如：

```
&quot;read&quot; if &quot;contributor&quot;;
```

展開成完整的形式會是：

```
has_permission(actor: Actor, &quot;read&quot;, resource: Repository) if 
    has_role(actor, &quot;contributor&quot;, resource);
```

那些簡化的規則那麼多，表示 `has_permisssion` 也就有那麼多，那上一層 `allow` 會傳給哪個？答案是根據敘述的順序分別代入求真值，每個 `has_permisssion` 都有各自的真與假，如果一輪跑完無一為真，那 `allow` 也不會為真，但只要中間有一個 `has_permission` 為真，那 `allow` 即為真。

但同樣的，`has_permission` 的真值又要 `has_role` 來決定。

### has_role

回來看 app&#x2F;main.polar 裡面還有個區塊：

```
# This rule tells Oso how to fetch roles for a repository
has_role(actor: User, role_name: String, repository: Repository) if
  role in actor.roles and
  role_name = role.name and
  repository = role.repository;
```

這個區塊定義了 `has_role` 背後的邏輯，「如果 `if` 後面的敘述為真，則 `has_role` 也會返回真」，如果 `has_role` 為真，則上層的 `has_permission`、`allow` 也將為真，於是這整條邏輯鏈丟回給 Python 的 `oso.authorize()` 也將認可為通過。

此處的 `if` 後的敘述稍微陌生一些，在此我們把上面 `user_db` 部份的內容拿下來比較好對照：

```py
users_db = {
    &quot;larry&quot;: User([Role(name=&quot;admin&quot;, repository=repos_db[&quot;gmail&quot;])]),
    &quot;anne&quot;: User([Role(name=&quot;maintainer&quot;, repository=repos_db[&quot;react&quot;])]),
    &quot;graham&quot;: User([Role(name=&quot;contributor&quot;, repository=repos_db[&quot;oso&quot;])]),
}
```

對照上面 Larry 大的屬性和下面的邏輯。

`role in actor.roles`：

* `actor.roles` 是 Python 的 `List[Role]` 型態的物件，也就是陣列，目前該陣列內只有一個 `Role` 物件。
* 而 [Polar 的 `in` 運算子是**拿來跑迭代的**](https:&#x2F;&#x2F;docs.osohq.com&#x2F;guides&#x2F;rbac.html#access-role-assignments-stored-in-the-application)，不是拿來判斷Ａ有沒有在Ｂ裡面的。
* 因為 Larry 大大體內的那個 `roles` 陣列內的 `Role` 物件只有一個，所以這 `in` 敘述就只會跑一輪。

`role_name = role.name`：

* 這句是用來比較等號左右兩邊的值，相同即返回真。
* `role.name` 已知為 `admin`。
* `role_name` 則為 `contributor` 這是從上層 `has_permission` 傳來的。

`repository = role.repository`：

* 也是是用來比較等號左右兩邊的值，相同即返回真，此例兩者皆為 `gmail`，故為真。

上面這區塊很顯然的不可能為真，因為 `role.name` 值是 `admin`，而 `role_name` 值則是 `contributor`，故必然不為真。

但若倒帶回去看前面的規則敘述，會注意到上面的規則有兩種：

* 第一種是 `xxxPermission if xxxRole`，即前面講過的 `&quot;read&quot; if &quot;contributor&quot;`，如果是**誰誰誰**就能**如此這般**。
* 第二種是 `RoleA if RoleB`，即如果是**某某Ａ**，那他也有如**某某Ｂ**的權限。

這兩種都是簡寫的規則，其中的第一種前面已經展開過，而第二種也可以展開成另一種形式，例如：

```
&quot;maintainer&quot; if &quot;admin&quot;;
&quot;contributor&quot; if &quot;maintainer&quot;;
```

兩者分別展開就是：

```
has_role(actor: Actor, &quot;maintainer&quot;, resource: Repository) if
  has_role(actor, &quot;admin&quot;, resource);

has_role(actor: Actor, &quot;contributor&quot;, resource: Repository) if
  has_role(actor, &quot;maintainer&quot;, resource);
```

其中的 `has_role(actor, &quot;admin&quot;, resource)` 代入上面的 `has_role if` 區塊推導一下，結果為真，因此 `&quot;maintainer&quot; if &quot;admin&quot;` 也為真，以此類推，`&quot;contributor&quot; if &quot;maintainer&quot;` 也為真，於是於是這整條邏輯鏈丟回給 Python 的 `oso.authorize()`，也將認可為通過。

---

反之，網址 http:&#x2F;&#x2F;localhost:5000&#x2F;repo&#x2F;react 之所以會看到錯誤的內容：

![React repo](react.png)

因為 Larry 大大身上沒有 react repo 的角色，所以在 `has_role` 區塊的 `repository = role.repository` 必然不為真，所以 `oso.authorize()` 也就吐 `False` 囉。

## 開放任何人讀公開的 Repo

先前在 app&#x2F;models.py 有看到 react repo 有一個 `is_public=True` 屬性，在此為它設計一條新規則，讓任何人皆可讀取公開的 repo。

可以在 app&#x2F;main.polar 加入以下敘述：

```
has_permission(_actor: User, &quot;read&quot;, repository: Repository) if
  repository.is_public;
```

如此當 Larry 嘗試開 react repo 時，這條新的規則就能給他一個「真」，而 `oso.authorize()` 通過囉！

最後完整的規則如下：

```
actor User {}

resource Repository {
  permissions = [&quot;read&quot;, &quot;push&quot;, &quot;delete&quot;];
  roles = [&quot;contributor&quot;, &quot;maintainer&quot;, &quot;admin&quot;];

  # A user has the &quot;read&quot; permission if they have the
  # &quot;contributor&quot; role.
  &quot;read&quot; if &quot;contributor&quot;;

  # A user has the &quot;push&quot; permission if they have the
  # &quot;maintainer&quot; role.
  &quot;push&quot; if &quot;maintainer&quot;;

  # A user has the &quot;delete&quot; permission if they have the
  # &quot;admin&quot; role.
  &quot;delete&quot; if &quot;admin&quot;;

  # A user has the &quot;maintainer&quot; role if they have
  # the &quot;admin&quot; role.
  &quot;maintainer&quot; if &quot;admin&quot;;

  # A user has the &quot;contributor&quot; role if they have
  # the &quot;maintainer&quot; role.
  &quot;contributor&quot; if &quot;maintainer&quot;;
}

# This rule tells Oso how to fetch roles for a repository
has_role(actor: User, role_name: String, repository: Repository) if
  role in actor.roles and
  role_name = role.name and
  repository = role.repository;

has_permission(_actor: User, &quot;read&quot;, repository: Repository) if
  repository.is_public;

allow(actor, action, resource) if
  has_permission(actor, action, resource);
```

## 結語

本篇介紹 Oso，它是跨平台的存取控制系統，可以與各語言、框架整合，讓專案具有 RBAC 機制，它的存取規則以自有的 Polar 格式撰寫，Polar 語法可以賦予規則一些邏輯性，而非單純的 role - action 這樣的靜態規則，可以讓存取控制的粒度細到單一資源，然而這當然是用相對複雜的語法換來的。

本文僅對 Oso 和 Polar 做基本介紹，細心的朋友可能會注意到，光一個 `Repository` 就這麼多角色、權限了，那資源一多豈不寫到手軟，實際上 Polar 還可以定義根據資源的階層賦予權限，如此即不用在每個子資源寫一堆重複的角色，但這部份目前沒有接觸，或許之後用到會再補充。

## 參考資料

- 〈[ABAC - 基於屬性的訪問控制 - 複雜場景下訪問控制解决之道](https:&#x2F;&#x2F;blog.csdn.net&#x2F;XiaoBeiTu&#x2F;article&#x2F;details&#x2F;100773968)〉
- 〈[Why Authorization is Hard](https:&#x2F;&#x2F;www.osohq.com&#x2F;post&#x2F;why-authorization-is-hard)〉
- 〈[Polar Syntax](https:&#x2F;&#x2F;docs.osohq.com&#x2F;reference&#x2F;polar&#x2F;polar-syntax.html)〉
- 〈[Build Role-Based Access Control (RBAC) in Ruby with Oso](https:&#x2F;&#x2F;docs.osohq.com&#x2F;ruby&#x2F;guides&#x2F;rbac.html)〉 --&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>從 pyee 認識觀察者模式</title>
        <published>2024-06-24T00:00:00+00:00</published>
        <updated>2024-06-24T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2024/ee/"/>
        <id>https://editor.leonh.space/2024/ee/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2024/ee/">&lt;p&gt;這個站成立這麼久，終於出現一篇談設計模式的文了，代表我的人生進階了嗎？（沒有。）&lt;&#x2F;p&gt;
&lt;p&gt;最近遇到一個需求，當某類物件狀態發生變化時，要有三四個模組被通知到，並且各自做出反應。&lt;&#x2F;p&gt;
&lt;p&gt;講得更具體一點，一個多管道社群貼文發布系統，它的核心功能是，一則訊息在系統內發出後，要發到 Twitter、Facebook、Instagram 等外部社群，當不採取觀察者模式時，程式碼大概會長這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# twitter.py&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; publish_to_twitter&lt;&#x2F;span&gt;&lt;span&gt;(post_content:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; ...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# facebook.py&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; publish_to_facebook&lt;&#x2F;span&gt;&lt;span&gt;(post_content:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; ...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# instagram.py&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; publish_to_instagram&lt;&#x2F;span&gt;&lt;span&gt;(post_content:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; ...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# main.py&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@router.post&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&#x2F;posts&#x2F;publish&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; publish&lt;&#x2F;span&gt;&lt;span&gt;(post_content:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    publish_to_twitter(post_content)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    publish_to_facebook(post_content)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    publish_to_instagram(post_content)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然而隨著社群型態的豐富，或者隨著業務需求擴展，要覆蓋的社群越來越多，例如 Mastodon、Threads、Plurk、LinkedIn、MeWe、Gab 等等，為了滿這些需求，上面的 &lt;code&gt;publish_to_xxx()&lt;&#x2F;code&gt; 也得越來越長，另外，除了發貼文外，也還有其他行為，像是偷改貼文、偷刪貼文等等，這些都是社群小編普遍需要的功能，也因此，除了好長的 &lt;code&gt;publish_to_xxx()&lt;&#x2F;code&gt; 外，還有好長的 &lt;code&gt;update_to_xxx()&lt;&#x2F;code&gt;、&lt;code&gt;delete_from_xxx()&lt;&#x2F;code&gt;，每個動作下面接的函式都又冗又長，這種時候，就可考慮採用 pyee 與觀察者模式讓應用內的模組解耦，使模組間的邊界更清晰。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pyee&quot;&gt;pyee&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;jfhbrook&#x2F;pyee&quot;&gt;pyee&lt;&#x2F;a&gt; 的「ee」表示 event emitter，名字與特性都取自 Node.js 的 event emitter 模組，顧名思義，它的主要特性就是事件發送與處理，下面我們邊用邊認識 pyee。&lt;&#x2F;p&gt;
&lt;p&gt;延續上面的案例，採用 pyee 後，上面的程式碼變這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# event.py&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; pyee&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; EventEmitter&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;ee&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; EventEmitter()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@ee.on&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;error&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; on_error&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;*&lt;&#x2F;span&gt;&lt;span&gt;args,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; **&lt;&#x2F;span&gt;&lt;span&gt;kwargs):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    logger.error({&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;event&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;error&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;args&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;: args,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;kwargs&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;: kwargs})&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# mastodon.py&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; event&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; ee&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@ee.on&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;publishing&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; publish_to_mastodon&lt;&#x2F;span&gt;&lt;span&gt;(post_content:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; *&lt;&#x2F;span&gt;&lt;span&gt;args,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; **&lt;&#x2F;span&gt;&lt;span&gt;kwargs):&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; ...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# threads.py&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; event&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; ee&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@ee.on&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;publishing&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; publish_to_threads&lt;&#x2F;span&gt;&lt;span&gt;(post_content:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; *&lt;&#x2F;span&gt;&lt;span&gt;args,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; **&lt;&#x2F;span&gt;&lt;span&gt;kwargs):&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; ...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# xxx.py&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; event&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; ee&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@ee.on&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;publishing&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; publish_to_xxx&lt;&#x2F;span&gt;&lt;span&gt;(post_content:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; *&lt;&#x2F;span&gt;&lt;span&gt;args,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; **&lt;&#x2F;span&gt;&lt;span&gt;kwargs):&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; ...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# main.py&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; event&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; ee&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@router.post&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&#x2F;posts&#x2F;publish&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; publish&lt;&#x2F;span&gt;&lt;span&gt;(post_content:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    ee.emit(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;publishing&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt; post_content&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;post_content)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;我們先在 event.py 建立一個 &lt;code&gt;EventEmitter&lt;&#x2F;code&gt; 實例 &lt;code&gt;ee&lt;&#x2F;code&gt;，它是事件發布與事件處理函式的主角。&lt;&#x2F;li&gt;
&lt;li&gt;然後我們在各個社群模組中透過裝飾器 &lt;code&gt;@ee.on(&#x27;publishing&#x27;)&lt;&#x2F;code&gt; 註冊當 publishing 事件發生時，各模組的處理函式。&lt;&#x2F;li&gt;
&lt;li&gt;在我們自己的 API 端點，收到客戶端的發布請求後，透過 &lt;code&gt;ee&lt;&#x2F;code&gt; 發射 publishing 事件，如此先前註冊的那些處理函式就會逐個執行。&lt;&#x2F;li&gt;
&lt;li&gt;另外 event.py 還有個 error 事件觀察者，這個 error 事件是事件處理器發生例外時，pyee 自己會發出的事件&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在 pyee 的設計裡，一個事件可以有多個處理器，它們是依序被呼叫的，當一個事件的處理器發生沒捕捉的例外，也沒有在 error 處理器這邊處理的話，那這個事件的後續其它處理器也就此中斷了。&lt;&#x2F;p&gt;
&lt;p&gt;上面的範例都是傳統的同步函式，而 pyee 也有異步的 &lt;code&gt;pyee.asyncio.AsyncIOEventEmitter&lt;&#x2F;code&gt; 可以使用，如果事件處理器是帶有 &lt;code&gt;async&lt;&#x2F;code&gt; 前綴的異步函式，那就得用 &lt;code&gt;AsyncIOEventEmitter&lt;&#x2F;code&gt; 才能正確工作囉，而即便是異步，事件處理函式依然是依序被呼叫的，所以前面的規則依然適用。&lt;&#x2F;p&gt;
&lt;p&gt;pyee 本身還有提供一些管理 API 為我們所用，參照下面程式碼：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; pyee.asyncio&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; AsyncIOEventEmitter&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;ee&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; EventEmitter()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# 列出所有觀察者觀察的事件&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;ee.event_names()&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; # [&amp;#39;publishing&amp;#39;, &amp;#39;updating&amp;#39;, &amp;#39;deletion&amp;#39;, ...]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# 列出一個事件的觀察者&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;ee.listeners(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;publishing&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; # [mastodon.publish_to_mastodon, ...]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在 pyee 的加持下，就算未來有更多新興社群，主程式也都不需要再改，它一律發出同樣的 publishing 事件，各個社群只要自行向 &lt;code&gt;ee&lt;&#x2F;code&gt; 註冊自己的事件處理器就好，如此主程式與那些社群串接模組就解除了依賴與耦合，這樣他們各自的邊界是不是更清晰了呢。&lt;&#x2F;p&gt;
&lt;p&gt;模組邊界清晰對多人開發的好處是顯而易見的，負責處理主要邏輯的同事和負責串接 Mastodon 的同事再也不會互相改到彼此的模組，滿足了我輩資訊人莫名對解耦的追求。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;guan-cha-zhe-mo-shi&quot;&gt;觀察者模式&lt;&#x2F;h2&gt;
&lt;p&gt;回頭談觀察者模式，在上面的例子中，那些被 &lt;code&gt;@ee.on()&lt;&#x2F;code&gt; 裝飾器註冊的函式我們稱為觀察者，他們會觀察 event emitter 發出的事件，如果是他們所關心的事件，他們就會接手處理，所以我們也可以叫他們事件處理器，從底層的觀點看，其實這都是 &lt;code&gt;EventEmitter&lt;&#x2F;code&gt; 在背後幫我們呼叫那些處理器工作，所謂觀察者是從高層次的觀點給的命名，其實函式還是函式，哪會觀察什麼東西。&lt;&#x2F;p&gt;
&lt;p&gt;前面我們以 pyee 與 publishing 事件舉例觀察者模式的應用，同樣的模式也可以套用在其他事件上，像是 updating、deletion，想得更廣一點，其他應用也有它可以發揮的地方，例如：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;WMS 系統的發料單的行為與狀態變化事件，像是 created、approved、picked、issued 等等。&lt;&#x2F;li&gt;
&lt;li&gt;MES 系統的工單行為與狀態變化事件，像是 created、approved、scheduled、in-progress、finishing 等等。&lt;&#x2F;li&gt;
&lt;li&gt;充電系統的行為與狀態變化事件，像是 available、preparing、charging、suspended、finishing、reserved、unavailable、faulted 等等。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;這些狀態變化可能來自用戶請求、程式邏輯，或來自狀態機，總之狀態變更只要牽涉到其他模組需要做出對應的動作，這種時候觀察者模式就是我們的好朋友。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;qi-ta-you-dian-xiang-you-bu-yi-yang-de-she-ji-mo-shi&quot;&gt;其它有點像又不一樣的設計模式&lt;&#x2F;h2&gt;
&lt;p&gt;觀察者模式適用的場景在應用內，隨場景不同，還可以考慮其他類似的設計模式：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;發布－訂閱模式（pub &#x2F; sub pattern），相較於觀察者模式，發布－訂閱模式會依賴一個獨立的訊息代理器（message broker），例如 MQTT 就是此模式下的典型協議，多個服務間不直接通信，而是各自對訊息代理器發布或訂閱關注的主題（topic），如此也解耦了服務間的依賴，但相對的每個服務都依賴於訊息代理器，也就是說訊息代理器本身最好是高可用的，這意味系統複雜度與成本的提升。&lt;&#x2F;li&gt;
&lt;li&gt;調度器模式（dispatcher pattern），在觀察者模式中，event emitter 只負責針對事件呼叫處理器，職責相對單純，而調度器模式中的調度器的職責較重，它主動針對事件做處理與呼叫，它所呼叫的事件處理器並不像觀察者模式中需要先註冊自己關注的事件，哪個事件該怎麼處理、該派給誰處理的職責都交給調度器主理。&lt;&#x2F;li&gt;
&lt;li&gt;Hook 模式，此模式下應用內會預先定義好幾個 hook 點，其他模組可以向系統註冊自己的 hook callback 函式，當應用跑到 hook 點時，會去跑已註冊的 hook callback 函式，然後再回到主程式繼續後面的計算，例如可以事先定義一個名為 &lt;code&gt;save_post&lt;&#x2F;code&gt; 的 hook 點，當儲存貼文時，其他的 &lt;code&gt;save_post&lt;&#x2F;code&gt; hook callback 函式會被呼叫，跑一些自己的邏輯，然後再回到主程式，hook 模式大多用於外掛、擴展機制，例如 WordPress、pytest、&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pytest-dev&#x2F;pluggy&quot;&gt;pluggy&lt;&#x2F;a&gt; 都是以 hook 為基礎給第三方發展外掛。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>初探 OpenTelemetry 與 APM</title>
        <published>2024-04-01T00:00:00+00:00</published>
        <updated>2024-04-01T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2024/otel/"/>
        <id>https://editor.leonh.space/2024/otel/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2024/otel/">&lt;p&gt;這篇介紹 OpenTelemetry 的概念與應用，首先來認識一下這字「telemetry」，華文翻為遙測或遠測，維基百科有很簡短的解釋：&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;遠測是指傳感器在測量現場收集測量對象的數據後，數據通過無線傳輸（例如無線電、超聲波或紅外線）、電話或計算機網絡、簡訊和 GSM 等各種方式傳送至遠距離的接收設備以供監測人員監測。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;簡單說就是監測，OpenTelemetry 是一套公定的監測標準，也是一套框架，用於監測應用、系統的性能指標。以粒度來看，它小到可以監測系統內每個函式的反應時間，大到可以監測一台實體或虛擬機的資源使用率，以往在跑&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;k6&#x2F;&quot;&gt;負載測試&lt;&#x2F;a&gt;時，我們是從外部視角去檢視應用在負載下的性能表現，但難以深入應用內部找到更具體的性能瓶頸，而 OpenTelemetry 搭配 APM 就能更好的知道自身應用具體是塞車在哪個端點，或者哪個函式。&lt;&#x2F;p&gt;
&lt;p&gt;OpenTelemetry 是一套公定標準，許多廠商以這個標準為基礎開發產品，這類產品統稱為 APM，全稱 applicatoin performance monitoring &#x2F; applicatoin performance management，這類產品除了主要的性能監測外，大多還有額外的附加功能，例如通知、儀表板等，市場上 APM 產品有開源的也有閉源的，因為本篇沒有業者贊助，就不特別幫他們打廣告了，開源的選擇看來看去比較有名的（或者說 SEO 做得比較好的）有二：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;signoz.io&#x2F;&quot;&gt;SigNoz&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;uptrace.dev&#x2F;&quot;&gt;Uptrace&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;個人是選用 SigNoz，理由很簡單，裝起來用看看沒什麼問題就繼續用下去，也懶得再折騰其他，雖然產品選擇眾多，但以 OpenTelemetry 為基礎的 APM 的概念都是相同的。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;hua-shi-jia-gou&quot;&gt;花式架構&lt;&#x2F;h2&gt;
&lt;p&gt;由於微服務模式興起，OpenTelemetry 為了要能適用於大單體與微服務，OpenTelemetry 本身也變成能翻玩成各種花式架構的形式，一種最簡單的形式如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;mermaid&quot;&gt;
---
displayMode: compact
---
flowchart TB
    app[&quot;App&quot;]
    apm[&quot;APM&quot;]

    app -- &quot;OTLP&quot; --&gt; apm
&lt;&#x2F;pre&gt;
&lt;p&gt;我們手上的 app 直接把監測資訊打給 APM，這裡的 OTLP 是 OpenTelemetry Protocol 的縮寫，我們無須深究 OTLP 技術細節（除非你的產品就是 APM），許多語言或框架都有現成的 OpenTelemetry 整合套件可以直接使用，這部份後面會再提到。&lt;&#x2F;p&gt;
&lt;p&gt;在更實際的情境中，除了應用層的監控，我們也會想監控主機層的資源耗用狀況，為此我們需要一個專門的 collector，於是架構變成這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;mermaid&quot;&gt;
---
displayMode: compact
---
flowchart TB
    app[&quot;App&quot;]
    collector[&quot;OTel Collector&quot;]
    apm[&quot;APM&quot;]

    subgraph machine[&quot;Machine&quot;]
        app
        collector
    end

    app -- &quot;OTLP&quot; --&gt; collector -- &quot;OTLP&quot; --&gt; apm
&lt;&#x2F;pre&gt;
&lt;p&gt;Collector 是一個獨立的程式，它會蒐集自己機身上以及別人打給它的監測資料，然後轉發出去，&lt;&#x2F;p&gt;
&lt;p&gt;Collector 是可以共用的，假設有另外一個服務跑在別人家的 serverless 平台上，我們只關心自家服務的性能，不關心底層資源的話，也可以把服務監測資訊打到另一個 collector，由一個 collector 統包，轉發給 APM：&lt;&#x2F;p&gt;
&lt;pre class=&quot;mermaid&quot;&gt;
---
displayMode: compact
---
flowchart TB
    app[&quot;App&quot;]
    collector[&quot;OTel Collector&quot;]
    apm[&quot;APM&quot;]


    subgraph machine[&quot;Machine&quot;]
        app
        collector
    end

    service[&quot;Service&quot;]

    subgraph serverless[&quot;Serverless&quot;]
        service
    end


    app -- &quot;OTLP&quot; --&gt; collector
    service -- &quot;OTLP&quot; --&gt; collector
    collector -- &quot;OTLP&quot; --&gt; apm
&lt;&#x2F;pre&gt;
&lt;p&gt;至此我們認識了 OpenTelemetry 的架構以及三個主要角色：我們自己的 app、現成的 APM 產品、現成的 collector，接著我們先把現成的 APM 和 collector 裝起來，最後再把自家 app 裝上監測器。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;signoz-apm&quot;&gt;SigNoz APM&lt;&#x2F;h2&gt;
&lt;p&gt;SigNoz 是以 OpenTelemetry 為基礎的 APM，它可以以 Docker 容器或 K8s Helm Chart 的形式架設，這裡用簡單的 Docker 容器形式。&lt;&#x2F;p&gt;
&lt;p&gt;準備一台裝好 Docker 的機器，把 SigNoz Git 抓下來：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;git&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; clone&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -b&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; main https:&#x2F;&#x2F;github.com&#x2F;SigNoz&#x2F;signoz.git&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;它裡面有安裝腳本：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;.&#x2F;signoz&#x2F;deploy&#x2F;install.sh&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;跑起來，它會抓映像，跑容器，一波進度條跑完，SigNoz 應該就跑起來了。&lt;&#x2F;p&gt;
&lt;p&gt;假設這台機器的 IP 是 192.168.16.14，那麼 SigNoz 的網頁入口就在 http:&#x2F;&#x2F;192.168.16.14:3301，打開網頁，依照畫面指示建立一個帳號，至此 SigNoz 就裝好了。&lt;&#x2F;p&gt;
&lt;p&gt;上面的安裝腳本也附帶跑了一款範例應用 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;jaegertracing&#x2F;jaeger&#x2F;tree&#x2F;main&#x2F;examples&#x2F;hotrod&quot;&gt;HotROD&lt;&#x2F;a&gt;，透過這款範例應用以及它的監測資料，我們可以自由探索體驗一下 SigNoz 的功能。&lt;&#x2F;p&gt;
&lt;p&gt;在 SigNoz 的設定頁可以設定資料保存週期，可以依自己的儲存空間量與資料產生量來決定。&lt;&#x2F;p&gt;
&lt;p&gt;等到後面我們自己的應用裝設好監測，HotROD 就可以無情地拋棄了，要關掉 HotROD 也很簡單，找到 SigNoz Docker Compose 檔案，在 deploy&#x2F;docker&#x2F;clickhouse-setup&#x2F;docker-compose.yaml，把裡面的 services.hotrod 與 services.load-hotrod 區塊註解掉，然後再跑一次安裝腳本就好：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;.&#x2F;signoz&#x2F;deploy&#x2F;install.sh&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;opentelemetry-collector&quot;&gt;OpenTelemetry Collector&lt;&#x2F;h2&gt;
&lt;p&gt;在 OpenTelemetry 體系內，Collector 負責接收、處理、匯出資料。&lt;&#x2F;p&gt;
&lt;p&gt;Collector 安裝形式也是多元的，這裡我們以系統套件方式安裝，打包好的 OpenTelemetry Collector 系統套件可以在&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;open-telemetry&#x2F;opentelemetry-collector-releases&#x2F;releases&quot;&gt;它的 GitHub 專案頁面&lt;&#x2F;a&gt;下載，裡面可以看到它的包分為兩大類：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;以 otecol 開頭，&lt;strong&gt;沒有 contrib 字樣&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;otecol-contrib 開頭，&lt;strong&gt;有 contrib 字樣&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;兩者的差異在有 &lt;strong&gt;contrib&lt;&#x2F;strong&gt; 字樣的有另外包入一些附加元件，大部份情況下我們都選用帶 contrib 字樣的套件。&lt;&#x2F;p&gt;
&lt;p&gt;假設另一台跑 app 和 OpenTelemetry Collector 的機器是 Ubuntu，就下載 otelcol-contrib_x.y.z_linux_amd64.deb，抓完把它裝起來。&lt;&#x2F;p&gt;
&lt;p&gt;裝完之後，OpenTelemetry Collector 會成為一個系統服務，配置檔在 &#x2F;etc&#x2F;otelcol-contrib&#x2F;config.yaml。&lt;&#x2F;p&gt;
&lt;p&gt;OpenTelemetry Collector 的配置也非常花式，甚至有點像天書，幸好 SigNoz 有提供現成的範本可以抄，把 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;raw.githubusercontent.com&#x2F;SigNoz&#x2F;benchmark&#x2F;main&#x2F;docker&#x2F;standalone&#x2F;config.yaml&quot;&gt;SigNoz 提供的 Collector 配置範本&lt;&#x2F;a&gt;抓下來，配置項目頗多，目前我們先關心如何把 Collector 收到的資料丟給 APM，找到以下區段，填入 APM 機台 IP：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;exporters&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  otlp&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;    endpoint&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;&amp;lt;IP of machine hosting SigNoz&amp;gt;:4317&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;    tls&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;      insecure&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; true&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這個 &lt;code&gt;exporters&lt;&#x2F;code&gt; 區段顧名思義用於配置與輸出資料有關的項目，OpenTelemetry Collector 企圖作為資料的統包仲介，因此它被設計成模組化、可擴充的，由幾塊元件構成：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;receiver：收資料&lt;&#x2F;li&gt;
&lt;li&gt;processor：處理資料&lt;&#x2F;li&gt;
&lt;li&gt;exporter：輸出資料&lt;&#x2F;li&gt;
&lt;li&gt;connector：接收與輸出資料&lt;&#x2F;li&gt;
&lt;li&gt;extension：不經手資料的其它擴充功能&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;這些元件的性質大概認識就好，有需要再深入了解，它們都是可以被程式化擴充或配置的，前面裝的帶有 contrib 字樣的套件就是幫我們包好許多子元件模組的大禮包。&lt;&#x2F;p&gt;
&lt;p&gt;回頭看配置檔的 &lt;code&gt;receivers&lt;&#x2F;code&gt; 區段：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;receivers&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  otlp&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;    protocols&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;      grpc&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;        endpoint&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; 0.0.0.0:4317&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;      http&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;        endpoint&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; 0.0.0.0:4318&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  hostmetrics&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;    collection_interval&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; 30s&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;    scrapers&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;      cpu&lt;&#x2F;span&gt;&lt;span&gt;: {}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;      disk&lt;&#x2F;span&gt;&lt;span&gt;: {}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;      load&lt;&#x2F;span&gt;&lt;span&gt;: {}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;      filesystem&lt;&#x2F;span&gt;&lt;span&gt;: {}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;      memory&lt;&#x2F;span&gt;&lt;span&gt;: {}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;      network&lt;&#x2F;span&gt;&lt;span&gt;: {}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;      paging&lt;&#x2F;span&gt;&lt;span&gt;: {}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;      process&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;        mute_process_name_error&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; true&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;        mute_process_exe_error&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; true&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;        mute_process_io_error&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; true&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;      processes&lt;&#x2F;span&gt;&lt;span&gt;: {}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  prometheus&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;    config&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;      global&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;        scrape_interval&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; 30s&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;      scrape_configs&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt; job_name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; otel-collector-binary&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;          static_configs&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt; targets&lt;&#x2F;span&gt;&lt;span&gt;: [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;localhost:8888&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到，這裡配置了三種收資料的機制：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;其一為 &lt;strong&gt;OTLP&lt;&#x2F;strong&gt;，讓此 Collector 監聽 4317 與 4318 埠，外部可以透過 gRPC 或 HTTP 打入 OTLP 資料。&lt;&#x2F;li&gt;
&lt;li&gt;其二為 &lt;strong&gt;hostmetrics&lt;&#x2F;strong&gt;，此乃 Collector 所在機台的硬體資源資料。&lt;&#x2F;li&gt;
&lt;li&gt;其三為 &lt;strong&gt;Prometheus&lt;&#x2F;strong&gt; 資料，Prometheus 是另一套應用監控標準，這裡 Collector 每隔三十秒會向 localhost:8888 請求數據指標（metric），這個 localhost:8888 是誰呢？就是 Collector 自己，它會在 8888 埠輸出自己的數據指標，自己跟自己要數據，而且微妙的是走的是 Prometheus，而非自己的 OTLP。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;注：Prometheus 源自希臘神話，涵義為先見之明者，被用於監測機制的名字應該是取其洞察的意涵。相較於 OpenTelemetry，Prometheus 更著重於 metric，而 OpenTelemetry 著重於 trace，但兩者確有重疊，後面也會再提到什麼是 metric 與 trace。&lt;&#x2F;p&gt;
&lt;p&gt;回到配置檔主題，把改好的配置檔放到 &#x2F;etc&#x2F;otelcol-contrib&#x2F;config.yaml，再重啟 otelcol-contrib 服務，Collector 應該就會把資料打給 APM 了。&lt;&#x2F;p&gt;
&lt;p&gt;在監測 app 前，我們先利用上面的 Host Metrics 來做機台資源儀表板。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;host-metrics-yi-biao-ban&quot;&gt;Host Metrics 儀表板&lt;&#x2F;h2&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2024&#x2F;otel&#x2F;.&#x2F;hostmetrics-dashboard-overview.webp&quot; alt=&quot;Host Metrics Dashboard&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;open-telemetry&#x2F;opentelemetry-collector-contrib&#x2F;tree&#x2F;main&#x2F;receiver&#x2F;hostmetricsreceiver&quot;&gt;Host Metrics&lt;&#x2F;a&gt; 為 Collector 的 receiver 模組之一，它會蒐集 Collector 所在機器的硬體資源使用狀況，前面我們配置了 CPU、記憶體等指標，SigNoz 可以根據這些數據制定一張儀表板，讓我們的 IT 戰情室看起來很厲害的感覺。&lt;&#x2F;p&gt;
&lt;p&gt;登入 SigNoz，到 Dashboard，右上角選 New Dashboard -&amp;gt; Import JSON，把 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;SigNoz&#x2F;dashboards&#x2F;blob&#x2F;main&#x2F;hostmetrics&#x2F;hostmetrics-with-variable.json&quot;&gt;SigNoz 提供的 Host Metrics dashboard JSON&lt;&#x2F;a&gt; 貼上去，收工。&lt;&#x2F;p&gt;
&lt;p&gt;這份 JSON 的結構主要由 layout 區塊與 widgets 區塊構成，layout 區塊定義的面板都有一個 UUID，widgets 定義的圖表再指定對應的 UUID，SigNoz 的佈局引擎就會根據 UUID 的對應關係排出版面與圖表，widgets 的資料來自 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;prometheus.io&#x2F;docs&#x2F;prometheus&#x2F;latest&#x2F;querying&#x2F;basics&#x2F;&quot;&gt;PromQL&lt;&#x2F;a&gt; 的查詢結果，也支援 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;clickhouse.com&#x2F;docs&#x2F;zh&#x2F;sql-reference&quot;&gt;ClickHouse SQL&lt;&#x2F;a&gt; 查詢，或者也可以從 SigNoz web UI 下查詢，然後保存成儀表板。&lt;&#x2F;p&gt;
&lt;p&gt;我們把貼上去的 JSON 存檔，應該就可以看到這個 Host Metrics 儀表板了，這樣就有個看起來很厲害的儀表板了，可以在老婆老闆同事面前帥一波，手寫查詢語句的部份以後有需要再深入。&lt;&#x2F;p&gt;
&lt;p&gt;真正要讓儀表版發揮功能，還要搭配 alert，例如當 CPU 衝到 90% 就通知特別冷卻戰術行動小組（S.C.S.O.F.），這時候看儀表板才有意義，不然誰會一直盯著螢幕看呢，除非你在&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;tpcjournal.taipower.com.tw&#x2F;article&#x2F;3008&quot;&gt;電廠&lt;&#x2F;a&gt;、&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;tlife.thsrc.com.tw&#x2F;tw&#x2F;article&#x2F;1302&quot;&gt;行車調度中心&lt;&#x2F;a&gt;、&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.tasa.org.tw&#x2F;ground_fac.php?c=20021702&amp;amp;ln=zh_TW&quot;&gt;太空中心&lt;&#x2F;a&gt;、&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.wearn.com&#x2F;bbs&#x2F;t1163761.html&quot;&gt;操盤室&lt;&#x2F;a&gt;，或 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;wall.alphacoders.com&#x2F;big.php?i=491754&quot;&gt;NCIS 多重威脅預警中心&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;app-jian-ce&quot;&gt;App 監測&lt;&#x2F;h2&gt;
&lt;p&gt;架設完 APM 與 Collector，現在可以把我們的 app 裝上監測器了。&lt;&#x2F;p&gt;
&lt;p&gt;OpenTelemetry 已經備有多種語言與框架的監測套件，視語言或框架不同，監測器裝設方式也有不同，這裡以 Python 的 FastAPI 為例。&lt;&#x2F;p&gt;
&lt;p&gt;要監測 Python app，得先裝幾個套件：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;pypi.org&#x2F;project&#x2F;opentelemetry-distro&#x2F;&quot;&gt;opentelemetry-distro&lt;&#x2F;a&gt;，這是大禮包，內有 OpenTelemetry API、SDK、監測器等工具。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;pypi.org&#x2F;project&#x2F;opentelemetry-exporter-otlp&#x2F;&quot;&gt;opentelemetry-exporter-otlp&lt;&#x2F;a&gt;，就是 Python 的 OTLP exporter 囉。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;要完整安裝大禮包，得執行它附帶的安裝工具：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;opentelemetry-bootstrap&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --action=install&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;它會幫我們下載與安裝各個套件，這很方便，但如果在 CI 環境也想加上監測的話，也要在 CI 的配置檔執行這行指令，或是包進容器映像內。&lt;&#x2F;p&gt;
&lt;p&gt;對於 app 的開發環境與生產環境，啟動監測的方法略有不同，下面分開說明。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;kai-fa-huan-jing&quot;&gt;開發環境&lt;&#x2F;h3&gt;
&lt;p&gt;開發環境下，啟動 Python app 的方式通常是跑一行指令，假設這個 app 的啟動腳本是 start-dev.py，原本的啟動命令為：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;python&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; .&#x2F;start-dev.py&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;要監測它，把啟動命令改為：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;OTEL_RESOURCE_ATTRIBUTES&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;service.&lt;&#x2F;span&gt;&lt;span&gt;name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;mocha-backend-developemnt,deployment.&lt;&#x2F;span&gt;&lt;span&gt;environment&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;development&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;OTEL_EXPORTER_OTLP_ENDPOINT&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;http:&#x2F;&#x2F;localhost:4317&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;OTEL_EXPORTER_OTLP_PROTOCOL&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;grpc&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;opentelemetry-instrument&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; python .&#x2F;start-dev.py&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;第一行宣告一些關於 app 的屬性，格式為 &lt;code&gt;key1=value1,key2=value2&lt;&#x2F;code&gt;，完整的屬性清單在&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;opentelemetry.io&#x2F;docs&#x2F;specs&#x2F;semconv&#x2F;resource&#x2F;&quot;&gt;這裡&lt;&#x2F;a&gt;，這邊定義了服務名、部署環境等，他們的 value 的值可以自行訂定，能清楚識別就好，前面說到 APM 可以接收多個 app 或服務的監測資料，在查詢資料時會用到這些屬性來對資料做篩選與層別。&lt;&#x2F;p&gt;
&lt;p&gt;第二行是 Collector 的位址，在上面的架構圖裡，我們把 Collector 與 app 裝在同一台機器，所以就是 localhost 囉。&lt;&#x2F;p&gt;
&lt;p&gt;最後一行的 &lt;code&gt;opentelemetry-instrument&lt;&#x2F;code&gt; 會根據前面設的環境變數發送監測資料給 Collector。&lt;&#x2F;p&gt;
&lt;p&gt;App 跑起來之後，玩一下，讓它產生點資料，等半分鐘，視角轉回 SigNoz，在 SigNoz 的 Service 與 Traces 應該可以見到 app 產生的監測資料，確認資料有成功收到。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;sheng-chan-huan-jing&quot;&gt;生產環境&lt;&#x2F;h3&gt;
&lt;p&gt;像上面這樣以 opentelemetry-instrument 與命令列來啟動 Python app 的方式只適用於開發環境，如果是生產環境，那啟動一款 Python web app 的方式就有各種花式可能，其中一種典型的形式是 systemd 叫起 Gunicorn，Gunicorn 叫起 Uvicorn worker 與 Python ASGI web app。&lt;&#x2F;p&gt;
&lt;p&gt;根據&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;opentelemetry-python.readthedocs.io&#x2F;en&#x2F;latest&#x2F;examples&#x2F;fork-process-model&#x2F;README.html&quot;&gt;文件&lt;&#x2F;a&gt;，我們要在 Gunicorn fork 行程之後才執行監測程式，這裡的 fork 是 Unix 系作業系統中一種建立行程（process）副本的機制，Gunicorn 作為 Python app server，啟動多 worker 的底層機制就是利用 fork 達成的，總之我們得在 Gunicorn 的配置檔中的一個 hook 端點 &lt;code&gt;post_fork()&lt;&#x2F;code&gt; 來執行監測程式：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# gunicorn.conf.py&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; gunicorn.arbiter&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Arbiter&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; gunicorn.workers.base&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Worker&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; opentelemetry&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; trace&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; opentelemetry.exporter.otlp.proto.grpc.trace_exporter&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; OTLPSpanExporter&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; opentelemetry.sdk&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; resources&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; opentelemetry.sdk.trace&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; TracerProvider&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; opentelemetry.sdk.trace.export&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; BatchSpanProcessor&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; post_fork&lt;&#x2F;span&gt;&lt;span&gt;(server: Arbiter, worker: Worker):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    server.log.info(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;f&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;Worker spawned (pid: &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;{&lt;&#x2F;span&gt;&lt;span&gt;worker.pid&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;)&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    resource&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; resources.Resource.create({&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            resources.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;DEPLOYMENT_ENVIRONMENT&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;staging&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            resources.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;SERVICE_INSTANCE_ID&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;uone_staging&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            resources.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;SERVICE_NAME&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;uone-backend-staging&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    })&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    span_processor&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; BatchSpanProcessor(OTLPSpanExporter(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;http:&#x2F;&#x2F;localhost:4317&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;))&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    tracer_provider&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; TracerProvider(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;resource&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;resource)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    tracer_provider.add_span_processor(span_processor)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    trace.set_tracer_provider(tracer_provider)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這裡主要做兩件事：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;建立 resource，大多是一些與前面相同的 app 屬性，改為以程式碼的形式表達罷了，讓 APM 得以篩選或層別我們關注的資料。&lt;&#x2F;li&gt;
&lt;li&gt;設定 tracer：
&lt;ul&gt;
&lt;li&gt;這裡牽涉到一個叫做 batch span processor 的東西，batch 是批次，processor 是處理器，那 span 是什麼？後面會再談到。&lt;&#x2F;li&gt;
&lt;li&gt;Span processor 又牽涉到一個 OTLP span exporter 物件，同樣的，我們先知道它是個 OTLP 資料匯出器就好，後面再來談 span 是什麼。這裡填入 localhost，因為在前面的架構中，OpenTelemetry Collector 與 app 是裝在同一台機器內。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;實際上，要從 app 直接打給 SigNoz 也可以，因為 SigNoz 也跑了個 Collector 來接受別人打給它的監測資料，我們把前面的架構圖 APM 的方塊再展開：&lt;&#x2F;p&gt;
&lt;pre class=&quot;mermaid&quot;&gt;
---
displayMode: compact
---
flowchart TB
    app[&quot;App&quot;]
    ap_machine_collector[&quot;OTel Collector&quot;]

    apm_collector[&quot;OTel Collector&quot;]

    subgraph ap_machine[&quot;AP Machine&quot;]
        app
        ap_machine_collector
    end

    subgraph apm[&quot;APM&quot;]
        apm_collector
    end

    app -- &quot;OTLP&quot; --&gt; ap_machine_collector -- &quot;OTLP&quot; --&gt; apm_collector
&lt;&#x2F;pre&gt;
&lt;p&gt;就可以明白 APM 也理所當然裡面有個 Collector。&lt;&#x2F;p&gt;
&lt;p&gt;另外，在 app 內部，也要呼叫監測程式，以 FastAPI 為例：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; fastapi&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; FastAPI, Request&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; opentelemetry.instrumentation.fastapi&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; FastAPIInstrumentor&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;fastapi_app&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; FastAPI()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;FastAPIInstrumentor.instrument_app(fastapi_app)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@app.get&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&#x2F;foobar&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;async def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; foobar&lt;&#x2F;span&gt;&lt;span&gt;():&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;message&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;hello world&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;重點在多加了一句 &lt;code&gt;FastAPIInstrumentor.instrument_app()&lt;&#x2F;code&gt;，其它都是 FastAPI 原本的東西。&lt;&#x2F;p&gt;
&lt;p&gt;相較於開發環境，生產環境的監測麻煩一點，不過也還好，這些都做完之後，跑個測試，上個新版，重啟服務，進入 app 玩一下，等個半分鐘，進入 SigNoz 看看有沒有 trace 資料進來吧。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jian-ce-zi-liao&quot;&gt;監測資料&lt;&#x2F;h2&gt;
&lt;p&gt;對於 OpenTelemetry 的監測資料，前面我們僅概略的知道有什麼 trace、metric，又有 span 什麼的，在 OpenTelemetry 的概念裡，它支援三類監測資料，分別是 trace、metric、log，而 span 則是 trace 類資料裡的一種概念，為了避免越說越迷糊，下面逐一展開，從最簡單的 log 開始。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;log&quot;&gt;Log&lt;&#x2F;h3&gt;
&lt;p&gt;雖然本文在此之前都未提及，但 OpenTelemetry 也支援收發 log，而 APM 也兼具 log server 的角色，以 Python 為例，OpenTelemetry 套件提供了與 Python 原生 &lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;python-log&#x2F;&quot;&gt;logging&lt;&#x2F;a&gt; 模組整合的 logging handler，只要把它配置一下，就如同其它的 logging handler 一樣，調用 Logger 物件的 &lt;code&gt;addHander()&lt;&#x2F;code&gt; 方法，把 OpenTelemetry logging handler 餵入即可，參考以下範例：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;import&lt;&#x2F;span&gt;&lt;span&gt; logging&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; opentelemetry._logs&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; set_logger_provider&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; opentelemetry.exporter.otlp.proto.grpc._log_exporter&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; OTLPLogExporter&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; opentelemetry.sdk._logs&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; LoggingHandler, LoggerProvider&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; opentelemetry.sdk._logs.export&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; BatchLogRecordProcessor&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; opentelemetry.sdk&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; resources&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;resource&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; resource&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; resources.Resource.create({&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    resources.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;DEPLOYMENT_ENVIRONMENT&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;staging&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    resources.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;SERVICE_INSTANCE_ID&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;uone_staging&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    resources.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;SERVICE_NAME&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;uone-backend-staging&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;})&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;logger_provider&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; LoggerProvider(resource)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;set_logger_provider(logger_provider)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;log_record_processor&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; BatchLogRecordProcessor(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  OTLPLogExporter(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;http:&#x2F;&#x2F;localhost:4317&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt; insecure&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;True&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;logger_provider.add_log_record_processor(log_record_processor)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;handler&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; LoggingHandler(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;logger_provider&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;logger_provider)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# Attach OTLP handler to root logger&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;logging.getLogger().addHandler(handler)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# Log directly&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;logging.info(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;Jackdaws love my big sphinx of quartz.&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這裡和前面設定 trace 有八成像，同樣的 resource、類似的 processor、類似的 set logger provider，主要差別在最後要記得呼叫 &lt;code&gt;addHandler()&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;metric&quot;&gt;Metric&lt;&#x2F;h3&gt;
&lt;p&gt;Metric 一般翻為指標或數據指標，總之就是一系列量值，例如前面 Host Metrics 見過的記憶體使用率、CPU 使用率等等，除了像這些由既有模組制定好的 metric 外，我們也可以制定自己關心的 metric，例如登入錯誤的次數等等，再搭配儀表板與警示就可以即時通知人員介入處理，更多說明可以參考 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;opentelemetry.io&#x2F;docs&#x2F;languages&#x2F;python&#x2F;instrumentation&#x2F;#metrics&quot;&gt;OpenTelemetry 文件&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;trace&quot;&gt;Trace&lt;&#x2F;h3&gt;
&lt;p&gt;Trace 指的是一次請求的鏈路軌跡，一個 trace 由帶有父子關係的 span 構成，例如一個公開的 API 端點 &#x2F;foobar，它的處理函式 &lt;code&gt;foobar()&lt;&#x2F;code&gt; 裡面呼叫了 &lt;code&gt;baz()&lt;&#x2F;code&gt;，那就會形成由一組巢狀 span 構成的 trace，其中 root span 為 &lt;code&gt;foobar()&lt;&#x2F;code&gt; 的監測資料，sub-span 為 &lt;code&gt;baz()&lt;&#x2F;code&gt; 的監測資料，監測資料內容包括處理時間、客戶端資訊、app 端資訊等，APM 方面會以端點為單位幫我們統計平均回應時間等資訊。&lt;&#x2F;p&gt;
&lt;p&gt;我們進入 SigNoz 的 Services 內頁，可以看到一些統計數據，包括 Latency、Rate、&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Apdex&quot;&gt;Apdex&lt;&#x2F;a&gt;、Key Operations、Error Percentage 等：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2024&#x2F;otel&#x2F;.&#x2F;feature-apm-min.webp&quot; alt=&quot;SigNoz Services&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;這幾項資料可以讓我們從較大的視角得知當前服務的狀態，如果要更深入看端點的反應，可以點入 Key Operation 清單中的端點，檢視單一端點的統計數據：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2024&#x2F;otel&#x2F;.&#x2F;open-traces-section-v0.6.2-73101e83a60d65f421151387953ad806.webp&quot; alt=&quot;SigNoz Tracing&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;還可以再深入到單一請求的 trace，去檢視主要的性能瓶頸發生在哪個 span：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2024&#x2F;otel&#x2F;.&#x2F;feature-distributed-tracing-min.webp&quot; alt=&quot;SigNoz Tracing&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;具體要怎麼運用這些監測，就看各人的需求啦。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-yu&quot;&gt;結語&lt;&#x2F;h2&gt;
&lt;p&gt;本文介紹了 OpenTelemetry 及以其為基礎的 APM SigNoz，透過這套標準與工具，我們得以從應用內部的視角即時的監控它的性能，本文著重於用最簡短的方式介紹 OpenTelemetry 概念與監測機制建置，OpenTelemetry 博大精深，Collector、trace、metric、log 都足以獨立為一篇文章，有興趣的讀者可以自行深入囉！個人是認為 OpenTelemetry 的文件太多太雜又太分散，對新手來說實在很難入門，希望這邊文章有幫助到不得其門而入的你。&lt;&#x2F;p&gt;
&lt;p&gt;另外，也有些 APM 產品是不走 OpenTelemetry 的，畢竟這類「標準」很多，有 OpenTelemetry、&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;statsd&#x2F;statsd&quot;&gt;StatsD&lt;&#x2F;a&gt;、Prometheus 等等，扮演 data collector 的同類工具也很多，有 OpenTelemetry Collector、Fluentd、Fluent Bit、Datadog Vector 等等，彼此都互有重疊、或有兼容、各有特色，很容易落入選擇障礙，逐套嘗試顯然不現實，在下一個大而全的新輪子被重新發明之前，選用以 OpenTelemetry 為基礎的 APM 看起來是最佳解。&lt;&#x2F;p&gt;
&lt;script type=&quot;module&quot;&gt;
import mermaid from &#x27;https:&#x2F;&#x2F;cdn.jsdelivr.net&#x2F;npm&#x2F;mermaid@10&#x2F;dist&#x2F;mermaid.esm.min.mjs&#x27;
mermaid.initialize({ startOnLoad: true, theme: &#x27;forest&#x27; })
&lt;&#x2F;script&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Python 配置管理與環境變數</title>
        <published>2023-11-08T00:00:00+00:00</published>
        <updated>2023-11-08T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2023/python-dotenv/"/>
        <id>https://editor.leonh.space/2023/python-dotenv/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2023/python-dotenv/">&lt;p&gt;在開發專案時，常常有一種需求是配置隨環境而變，例如資料庫的連線字串：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;在開發環境是 &lt;code&gt;postgresql:&#x2F;&#x2F;localhost:5432&#x2F;mocha&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;在測試環境是 &lt;code&gt;postgresql:&#x2F;&#x2F;dbadmin:dbpw@localhost&#x2F;mocha&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;在正式環境是 &lt;code&gt;postgresql:&#x2F;&#x2F;ap:pgappw@192.168.100.212:5432&#x2F;mocha&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;又或者其它第三方服務的環境或帳密不同，例如某個外部檔案儲存服務也有提供測試環境給我們用，在我們的開發環境與測試環境：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;檔案儲存服務的位址是 &lt;code&gt;192.168.101.234:5000&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;帳號是 &lt;code&gt;yc&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;密碼是 &lt;code&gt;bepoliteto&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在我們的正式環境：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;檔案儲存服務的位址是 &lt;code&gt;192.168.102.234:5000&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;帳號是 &lt;code&gt;yc&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;密碼是 &lt;code&gt;begentleto&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;像這類會隨環境而變的配置，當然不能寫死在程式碼內，一般約定成俗的慣例是把它們寫入檔名為 .env 的配置文件內，一個典型的 .env 大概像這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;ini&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;DATABASE_URL&lt;&#x2F;span&gt;&lt;span&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;postgresql:&#x2F;&#x2F;postgres:mysecretpassword@localhost:5432&#x2F;mocha&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FILE_SERVICE_ADDRESS&lt;&#x2F;span&gt;&lt;span&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;192.168.101.234:5000&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FILE_SERVICE_ACCOUNT&lt;&#x2F;span&gt;&lt;span&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;yc&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FILE_SERVICE_PASSWORD&lt;&#x2F;span&gt;&lt;span&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;bepoliteto&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這份 .env 檔案，慣例上不會進版控，而是手動拷貝到各個環境的專案資料夾內。&lt;&#x2F;p&gt;
&lt;p&gt;不論是何種語言大都有專門載入 .env 檔案的套件，它會幫我們讀取 .env 的條目鍵值，並存入相對的程式變數（或常數），在 Python 方面就是 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;theskumar&#x2F;python-dotenv&quot;&gt;python-dotenv&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;python-dotenv&quot;&gt;python-dotenv&lt;&#x2F;h2&gt;
&lt;p&gt;python-dotenv 載入 .env 檔案的條目後，會把條目們存入 OS 的環境變數中，所以我們可以透過 &lt;code&gt;os.getenv()&lt;&#x2F;code&gt; 來取得這些連線字串或帳密，最簡單的範例如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; dotenv&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; load_dotenv&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;load_dotenv(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;dotenv_path&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;.env&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  # take environment variables from .env.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# Code of your application, which uses environment variables (e.g. from `os.environ` or&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# `os.getenv`) as if they came from the actual environment.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;print&lt;&#x2F;span&gt;&lt;span&gt;(os.getenv(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;key&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;DATABASE_URL&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;))&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;把上面這份 Python 腳本跑起來就會得到前面我們定義的資料庫連線字串。&lt;&#x2F;p&gt;
&lt;p&gt;如果在執行 Python 腳本之前，我們就已經有了一個同樣名為 &lt;code&gt;DATABASE_URL&lt;&#x2F;code&gt; 的環境變數，那麼 python-dotenv 不會把它蓋掉，以原有的環境變數優先存在。&lt;&#x2F;p&gt;
&lt;p&gt;如果不想以環境變數的形式取用 .env 條目，還有另一個 &lt;code&gt;dotenv_values()&lt;&#x2F;code&gt; 方法，它會把 .env 條目存成一份 dict 物件，範例如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; dotenv&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; dotenv_values&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;config&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; dotenv_values(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;dotenv_path&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;.env&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  # config = {&amp;quot;USER&amp;quot;: &amp;quot;foo&amp;quot;, &amp;quot;EMAIL&amp;quot;: &amp;quot;foo@example.org&amp;quot;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;python-dotenv 本身是滿單純也滿簡單的套件，用法就上面兩種，希望這篇短文對也在跑多環境專案的你有幫助。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>pydantic 的愛恨糾葛</title>
        <published>2023-11-06T00:00:00+00:00</published>
        <updated>2023-11-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2023/pydantic-model/"/>
        <id>https://editor.leonh.space/2023/pydantic-model/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2023/pydantic-model/">&lt;p&gt;之前寫過一篇〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2023&#x2F;pydantic&#x2F;&quot;&gt;pydantic 小筆記&lt;&#x2F;a&gt;〉，這集繼續紀錄 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;pydantic-docs.helpmanual.io&#x2F;&quot;&gt;pydantic&lt;&#x2F;a&gt; 的其他特性，在此大部分應用場景都是 pydantic + FastAPI，誰叫他們這麼香呢。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pydantic-model-ji-cheng&quot;&gt;pydantic Model 繼承&lt;&#x2F;h2&gt;
&lt;p&gt;pydantic model 可以繼承，例如 &lt;code&gt;User&lt;&#x2F;code&gt; 被幾個子類繼承：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; pydantic&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; BaseModel&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; User&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;BaseModel&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    name:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    age:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; int&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; UserCreate&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;User&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    password:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; UserRead&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;User&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    pass&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;一旦繼承，子類也會有 &lt;code&gt;User&lt;&#x2F;code&gt; 的 &lt;code&gt;name&lt;&#x2F;code&gt; 和 &lt;code&gt;age&lt;&#x2F;code&gt; 屬性，以及子類本身特有的屬性，例如 &lt;code&gt;UserCreate&lt;&#x2F;code&gt; 就有 &lt;code&gt;name&lt;&#x2F;code&gt;、&lt;code&gt;age&lt;&#x2F;code&gt;、&lt;code&gt;password&lt;&#x2F;code&gt; 三個屬性。&lt;&#x2F;p&gt;
&lt;p&gt;這是 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;fastapi.tiangolo.com&#x2F;zh&#x2F;tutorial&#x2F;extra-models&#x2F;&quot;&gt;FastAPI 中的一種設計模式&lt;&#x2F;a&gt;，不同的子類對應不同的情境，同時也避免了打錯字、多打字的問題。&lt;&#x2F;p&gt;
&lt;p&gt;例如對於建立用戶的請求，大概會長這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@app.post&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;path&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&#x2F;user&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt; response_model&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;UserRead)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; create_user&lt;&#x2F;span&gt;&lt;span&gt;(user: UserCreate)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    # Create User logic&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return&lt;&#x2F;span&gt;&lt;span&gt; user&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;受惠於 pydantic 的檢查機制，傳送到 &lt;code&gt;create_user()&lt;&#x2F;code&gt; 的請求必須是有密碼的，而回傳的訊息則是不帶帳號密碼的，這中間不需要我們手動把 &lt;code&gt;password&lt;&#x2F;code&gt; 屬性摘除，pydantic 會自動過濾掉不存在 &lt;code&gt;UserRead&lt;&#x2F;code&gt; 類的屬性，這樣的設計也符合一般建立用戶的邏輯。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ci-model-fei-bi-model&quot;&gt;此 Model 非彼 Model&lt;&#x2F;h2&gt;
&lt;p&gt;pydantic 最大的問題是它也有所謂的 model 和 schema，當再把資料庫的概念摻進來就造成語意上的混亂。&lt;&#x2F;p&gt;
&lt;p&gt;不僅是語意上，程式碼看起來也會相當冗餘，以前面的 User 資源為例，除了有 pydantic model 的定義外，在 ORM 大概也要寫一份極其相似的「model」：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; User&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Model&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; fields.IntField(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;pk&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;True&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; fields.CharField(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;max_length&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;255&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    age&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; fields.IntField()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    password&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; fields.CharField(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;max_length&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;255&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這樣又回到了多打字、打錯字的老問題，於是各路人馬都看不下去，紛紛提出解決方案。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;sqlmodel&quot;&gt;SQLModel&lt;&#x2F;h3&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2023&#x2F;pydantic-model&#x2F;logo-margin-vector.svg&quot; alt=&quot;SQLModel&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：&lt;a href=&quot;https:&#x2F;&#x2F;sqlmodel.tiangolo.com&#x2F;&quot;&gt;SQLModel&lt;&#x2F;a&gt;&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;FastAPI 本家作者另外開發的 SQLModel，它的基類 &lt;code&gt;SQLModel&lt;&#x2F;code&gt; 同時是 pydantic 的 model，也是 SQLAlchemy 的 &lt;code&gt;declarative_base()&lt;&#x2F;code&gt; 所產生的 model，只要繼承並依照 SQLModel 的規則訂定屬性就可以同時搞定兩方面的 model。&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; User&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;SQLModel&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt; table&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;True&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    id&lt;&#x2F;span&gt;&lt;span&gt;: Optional[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;int&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; Field(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;default&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;None&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt; primary_key&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;True&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    name:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    age:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; int&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    password:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;但 SQLModel 目前還在早期開發階段，文件不齊，沒有 migration 和 seeding 的說明，不太能夠用作生產力工具。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;tortoise-orm&quot;&gt;Tortoise ORM&lt;&#x2F;h3&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2023&#x2F;pydantic-model&#x2F;tortoise.png&quot; alt=&quot;Tortoise ORM&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：&lt;a href=&quot;https:&#x2F;&#x2F;tortoise.github.io&#x2F;&quot;&gt;Tortoise ORM&lt;&#x2F;a&gt;&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;tortoise&#x2F;&quot;&gt;Tortoise ORM&lt;&#x2F;a&gt; 是異步 ORM，它並不專為 FastAPI 打造，但還是有提供一些整合工具。&lt;&#x2F;p&gt;
&lt;p&gt;它的思路是用一個 &lt;code&gt;pydantic_model_creator()&lt;&#x2F;code&gt; 函式從 Tortoise model 生出一個 pydantic model：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; Users&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;models&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Model&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; fields.IntField(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;pk&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;True&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; fields.CharField(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;max_length&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;255&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    age&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; fields.IntField()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    password&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; fields.CharField(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;max_length&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;255&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    class&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; PydanticMeta&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        exclude&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;password&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;User_Pydantic&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; pydantic_model_creator(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;cls&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;Users,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;User&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;UserIn_Pydantic&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; pydantic_model_creator(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;cls&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;Users,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;UserIn&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;因為不是為 pydantic 或 FastAPI 量身打造的，看起來就沒那麼優雅了，但它發展較為成熟，也有確定的 migration 機制。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ormar&quot;&gt;ormar&lt;&#x2F;h3&gt;
&lt;p&gt;ormar 的思路和 SQLModel 一樣，都是讓 ormar model 同時具有 pydantic 和 SQLAlchemy 的特性，相較於 SQLModel，ormar 發展較為成熟，也有 migration 的文件可看。&lt;&#x2F;p&gt;
&lt;p&gt;它的 model 定義大概長這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; User&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;ormar&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Model&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    class&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; Meta&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        metadata&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; db.metadata&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        database&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; db.database&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    id&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; int&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; ormar.Integer(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;primary_key&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;True&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    name:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; ormar.String(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;max_length&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;255&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    age:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; int&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; ormar.Integer()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    password:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; ormar.String(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;max_length&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;255&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;pydbantic&quot;&gt;pydbantic&lt;&#x2F;h3&gt;
&lt;p&gt;長的和 SQLModel 有八成像：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; User&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;DataBaseModel&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    id&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; PrimaryKey()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    name:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    age:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; int&lt;&#x2F;span&gt;&lt;span&gt; &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    password:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;一樣是以 SQLAlchemy 為基礎的。&lt;&#x2F;p&gt;
&lt;p&gt;pydbantic 的一項重要特性是「自動 migration」，這不知道該說是好還是不好，畢竟任何異動資料表的行為都應該謹慎為之，過於自動好像有風險。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;prisma&quot;&gt;Prisma&lt;&#x2F;h3&gt;
&lt;!-- &lt;figure&gt;

![Prisma](event-logo-prisma.svg)
&lt;figcaption&gt;來源：&lt;a href=&quot;https:&#x2F;&#x2F;www.prisma.io&#x2F;&quot;&gt;Prisma&lt;&#x2F;a&gt;&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt; --&gt;
&lt;p&gt;Prisma 原本是 JS 世界的 ORM，但因為核心是 Rust 撰寫，也可以被移植到 Python。&lt;&#x2F;p&gt;
&lt;p&gt;Prisma 最大的特色是它不那麼 ORM，它有自己的 schema 制定語言，不意外的就叫 Prisma，長得和 GraphQL 有八成像：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;graphql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;model User&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;  id        Int&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;     @id @default&lt;&#x2F;span&gt;&lt;span&gt;(autoincrement())&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;  name      String&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;  age       Int&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;  password  str&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;透過 Prisma 的機制，schema 會被兩方面的轉換，一方面是可以轉換成 SQL 以製作 migration 腳本，另一方面可以轉換成 Pydantic model，直接引入程式內調用，並且不只是單純的 model，其他該有的資料庫查詢接口也都傳便便了，使用過程和 gRPC 有八成像。&lt;&#x2F;p&gt;
&lt;p&gt;除了產出程式碼外，Prisma schema 作為中立的文件，也可以轉成 DBML 等其他文件形式，這又和 OpenAPI 有點像。&lt;&#x2F;p&gt;
&lt;p&gt;前面提到 schema 轉出的 migration 腳本是純 SQL 腳本，這意味著我輩的資料庫都得相同，如果生產環境是 PostgreSQL，那不好意思，整個開發團隊的個人開發環境也都要裝 PostgreSQL，否則 migration 腳本會跑不動，沒辦法用一個 SQLite 打天下，殘念，這對我的 286 來說當然是很吃力的。&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2023&#x2F;pydantic-model&#x2F;32070717-16708784-ba42-11e7-8572-a8fcc10d7f7d.gif&quot; alt=&quot;cool-retro-term&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：&lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;Swordfish90&#x2F;cool-retro-term&quot;&gt;cool-retro-term&lt;&#x2F;a&gt;&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;以上幾路方案，具體該選哪個，取決於心臟的大小，在我看來我輩開發者需要的是傻瓜 ORM，傻瓜 ORM 不需要命名資料表、不需要自己增設 &lt;code&gt;id&lt;&#x2F;code&gt;、&lt;code&gt;created_at&lt;&#x2F;code&gt;、&lt;code&gt;updated_at&lt;&#x2F;code&gt; 欄位，也不用指定那 &lt;code&gt;max_length&lt;&#x2F;code&gt;，甚至不用管文字的長短、數字的大小，Rails 的 Active Record 就具備以上所有的傻瓜特性，上述之中最接近傻瓜 ORM 的應該是 Prisma，但它又逼我要灌大軟體。&lt;&#x2F;p&gt;
&lt;p&gt;除了以上介紹的這些，其實還有令一套也是很傻瓜的 ORM 叫 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;orm.masoniteproject.com&#x2F;&quot;&gt;Masonite ORM&lt;&#x2F;a&gt;，遺憾的是沒什麼人用，殘念。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>pydantic 小筆記</title>
        <published>2023-11-05T00:00:00+00:00</published>
        <updated>2023-11-05T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2023/pydantic/"/>
        <id>https://editor.leonh.space/2023/pydantic/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2023/pydantic/">&lt;p&gt;故事要從 Python 的語言特性說起，因為 Python 是&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E9%B8%AD%E5%AD%90%E7%B1%BB%E5%9E%8B&quot;&gt;動態型別&lt;&#x2F;a&gt;的，所以一個在同一個區塊（命名空間）內的變數 &lt;code&gt;x&lt;&#x2F;code&gt;，可以一下是字串，一下是整數：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;x&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 4&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;print&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;type&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;4&lt;&#x2F;span&gt;&lt;span&gt;))&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; # at this moment, x points to an integer&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;x&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Hello, world&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;print&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;type&lt;&#x2F;span&gt;&lt;span&gt;(x))&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; # and at this moment, x points to a string&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這樣的特性降低了 Python 的學習曲線，但在程式專案變複雜後，對於「值」與「型態」的掌握會越來越難以駕馭。&lt;&#x2F;p&gt;
&lt;p&gt;大約在 Python 3.5 起引入了 type hints，華文叫類型提示或型態提示，它讓我們得以聲明變數的型態，也可以聲明函式回傳值的型態。&lt;&#x2F;p&gt;
&lt;p&gt;有了 type hints，IDE 或是靜態分析工具就可以幫助我們偵測型態上的錯誤，也因為它只是 &lt;strong&gt;hints&lt;&#x2F;strong&gt;，所以它終究無法讓 Python 變成靜態型別語言，只是一種輔助而已。&lt;&#x2F;p&gt;
&lt;p&gt;下面是最基本的 type hints 範例：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;name:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;world&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; greeting&lt;&#x2F;span&gt;&lt;span&gt;(name:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;span&gt;) -&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Hello &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; +&lt;&#x2F;span&gt;&lt;span&gt; name&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;greeting(name)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面我們用 &lt;code&gt;: str&lt;&#x2F;code&gt; 聲明 &lt;code&gt;name&lt;&#x2F;code&gt; 為字串，以及在函式我們用 &lt;code&gt;-&amp;gt; str&lt;&#x2F;code&gt; 聲明 &lt;code&gt;greeting()&lt;&#x2F;code&gt; 回傳的也是字串，當然還有更複雜的用法，可以參見〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;segmentfault.com&#x2F;a&#x2F;1190000040864758&quot;&gt;Python Type Hints 從入門到實踐&lt;&#x2F;a&gt;〉。&lt;&#x2F;p&gt;
&lt;p&gt;以上為前情提要，下面開始談本文的主角 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;pydantic-docs.helpmanual.io&#x2F;&quot;&gt;pydantic&lt;&#x2F;a&gt; 。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pydantic&quot;&gt;pydantic&lt;&#x2F;h2&gt;
&lt;p&gt;pydantic 是以 type hints 為基礎，幫我們做資料型態驗證的套件，我們可以用它定義「Model」：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; pydantic&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; BaseModel&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; EmployeeModel&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;BaseModel&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    name:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    salary:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; int&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在這個 &lt;code&gt;EmployeeModel&lt;&#x2F;code&gt; 中，我們聲明了 &lt;code&gt;name&lt;&#x2F;code&gt; 應為字串、&lt;code&gt;salary&lt;&#x2F;code&gt; 應為整數，透過繼承自 &lt;code&gt;BaseModel&lt;&#x2F;code&gt; 的特性，pydantic 會自動幫我們驗證型態的正確性。&lt;&#x2F;p&gt;
&lt;p&gt;如果試圖建立一個實例，並提供正確的型態，那不會有任何問題（也不會有任何提示說我們好棒棒）：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;employee&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; EmployeeModel(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;Bar&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    salary&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;1000&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;但如果給了一個錯誤的型態，那麼 pydantic 會引發錯誤：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;employee&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; EmployeeModel(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;Bar&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    salary&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;secret&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;應該要是整數的 &lt;code&gt;salary&lt;&#x2F;code&gt;，卻被塞了字串，引發 validation error：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Traceback (most recent call last):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  File &amp;quot;&amp;lt;stdin&amp;gt;&amp;quot;, line 1, in &amp;lt;module&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  File &amp;quot;&#x2F;home&#x2F;leon&#x2F;Projects&#x2F;pydantic&#x2F;.venv&#x2F;lib&#x2F;python3.10&#x2F;site-packages&#x2F;pydantic&#x2F;main.py&amp;quot;, line 406, in __init__&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    raise validation_error&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;pydantic.error_wrappers.ValidationError: 1 validation error for EmployeeModel&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;salary&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  value is not a valid integer (type=type_error.integer)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;fastapi-pydantic&quot;&gt;FastAPI + pydantic&lt;&#x2F;h2&gt;
&lt;p&gt;在應用上，新興的 Python 框架 FastAPI 深度整合了 pydantic 的類型驗證特性，只要在程式碼內事先聲明類型，所有的 API 端點都自動的具備型態驗證機制，我輩開發者再也不用自己刻 e-mail、URL 等等繁瑣的驗證邏輯，香！&lt;&#x2F;p&gt;
&lt;p&gt;下面是個極簡的 FastAPI 範例：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; typing&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Optional&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; fastapi&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; FastAPI&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; pydantic&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; BaseModel&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; Item&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;BaseModel&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    name:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    description: Optional[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;str&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; None&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    price:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; float&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    tax: Optional[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;float&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; None&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;app&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; FastAPI()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@app.post&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;&#x2F;items&#x2F;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;async def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; create_item&lt;&#x2F;span&gt;&lt;span&gt;(item: Item):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return&lt;&#x2F;span&gt;&lt;span&gt; item&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在 Item model 中，我們定義了四個屬性，其中的 &lt;code&gt;description&lt;&#x2F;code&gt; 與 &lt;code&gt;tax&lt;&#x2F;code&gt; 的 type hints 是 &lt;code&gt;Optional[str]&lt;&#x2F;code&gt;，而預設值為 &lt;code&gt;None&lt;&#x2F;code&gt;，&lt;code&gt;Optional[str]&lt;&#x2F;code&gt; 意味著此屬性是選填的，如果建立實例時未填，則屬性值為 &lt;code&gt;None&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;最後一個區塊定義了一個接受 POST 的 API 端點 &#x2F;items&#x2F;，此端點的處理函式 &lt;code&gt;create_item(item: Item)&lt;&#x2F;code&gt; 接受一個參數 &lt;code&gt;item&lt;&#x2F;code&gt;，並且該參數之型態應為前面定義的 Item model，在這樣的的定義與聲明下，前端送過來的 request body 會自動的被 pydantic 解析與驗證，並且那 FastAPI 自動產生的 OpenAPI 文件也會載明該端點的 JSON schema：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2023&#x2F;pydantic&#x2F;image01.png&quot; alt=&quot;FastAPI&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2023&#x2F;pydantic&#x2F;image02.png&quot; alt=&quot;FastAPI&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;huan-jing-bian-shu-guan-li-env-yu-pydantic&quot;&gt;環境變數管理 .env 與 pydantic&lt;&#x2F;h2&gt;
&lt;p&gt;在開發環境（development）與生產環境（production）採用的參數可能不同，例如連接 Twitter API 的帳密、連接 MongoDB 的帳密等等，這類參數習慣上會把它與程式碼分開，另外放在 .env 檔案內，再透過像 &lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2023&#x2F;pydantic&#x2F;content&#x2F;2023&#x2F;2023-11-07&#x2F;python-dotenv&#x2F;index.md&quot;&gt;python-dotenv&lt;&#x2F;a&gt; 這樣的套件把 .env 讀入成為可調用的變數。pydantic 也整合了 python-dotenv，可以幫我們做到上述工作，這部份的用法請參閱下面兩篇：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;rednafi.github.io&#x2F;digressions&#x2F;python&#x2F;2020&#x2F;06&#x2F;03&#x2F;python-configs.html&quot;&gt;Pedantic Configuration Management with Pydantic&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;myapollo.com.tw&#x2F;zh-tw&#x2F;python-pydantic&#x2F;&quot;&gt;用 pydantic 輕鬆進行設定管理（settings management）&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;說個題外話，關於 .env 檔案，有一條常見的鐵律是不要把 .env 提交到版控系統，個人是不太認同，只要我的 Git repository 是私有的，自架的，本機的，提交到 Git repository 又何妨？版控≠開源，當我的專案是閉源的，.env 的地位就與任何一份原碼一樣隱私，不具有特殊地位，我們要守護的也不僅是 .env，而是整個專案。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-yu&quot;&gt;結語&lt;&#x2F;h2&gt;
&lt;p&gt;綜觀以上，只要在程式碼內定義好型態，就可以享有：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;IDE 或靜態分析工具幫我們揪錯，以及程式碼的可讀性更高，大型專案更好維護&lt;&#x2F;li&gt;
&lt;li&gt;pydantic 幫我們檢查打來 API 的值的型態正確與否，省去自己寫驗證邏輯的功夫，time to market 時間再省一半&lt;&#x2F;li&gt;
&lt;li&gt;FastAPI 自動根據我們定義的型態產生 OpenAPI 文件與規格，再也不用寫 API 規格書，再也不用回覆前端同事的私訊&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;can-kao-zi-liao&quot;&gt;參考資料&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;dev.to&#x2F;icncsx&#x2F;python-is-strongly-dynamically-typed-what-does-that-mean-5810&quot;&gt;Python is strongly, dynamically typed. What does that mean?&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;myapollo.com.tw&#x2F;zh-tw&#x2F;pydantic-validate-data&#x2F;&quot;&gt;用 pydantic 輕鬆進行資料驗證&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>流程引擎 SpiffWorkflow</title>
        <published>2023-09-06T00:00:00+00:00</published>
        <updated>2023-09-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2023/spiffworkflow/"/>
        <id>https://editor.leonh.space/2023/spiffworkflow/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2023/spiffworkflow/">&lt;p&gt;SpiffWorkflow 是流程引擎，這裡的「流程」有點含糊，在談 SpiffWorkflow 前，先來說說它的流程是哪種流程。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;liu-cheng&quot;&gt;流程&lt;&#x2F;h2&gt;
&lt;p&gt;流程的英文可以是 process 或 workflow，或者有時候也常常用更簡化的形式「flow」表示，這些詞彙混用的狀況時常有之，我們也都概稱為流程，對於字面上的意思或許可以不深究，但對於應用面可就要計較一番了，對於流程，比較常見到的有：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;UI flow&lt;&#x2F;strong&gt;，這是指用戶的操作流程，通常在 UX 領域可見，典型的應用是 Penpot、Figma 這類設計應用。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Data flow&lt;&#x2F;strong&gt;，這是指資料流程，目前比較常用於表示大數據的 ETL 流程。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Business process&lt;&#x2F;strong&gt;，這是指商業流程，也是用戶最能具體感受到的流程，包括人事、總務、採購、業務等各類申辦流程，這類產品又稱為 &lt;strong&gt;BPM&lt;&#x2F;strong&gt;（business process management）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;其中後兩者比較容易搞混，如果你想搭建的是申辦、簽核系統，那就應該找 business process 方案，如果你想搭建的是資料流處理系統，那就應該找 data flow 方案。&lt;&#x2F;p&gt;
&lt;p&gt;對於 data flow，比較著名的工具有 Apache Airflow，它是以 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;airflow.apache.org&#x2F;docs&#x2F;apache-airflow&#x2F;stable&#x2F;core-concepts&#x2F;dags.html&quot;&gt;DAG&lt;&#x2F;a&gt; 為概念的資料流程執行工具，它的設計模式決定了它的使用姿勢，它當然可以有分支也當然可以有人為決策，只是相較於專門的 business process 方案，要跑平行簽核、多重指派，在 Airflow 要實現這些得花更多力氣罷了，就像你不會在 PowerPoint 寫文件，也不會在 Word 寫程式一樣，不是不能，只是不適合。&lt;&#x2F;p&gt;
&lt;p style=&quot;background-color: hsla(0, 0%, 96%, 1.0); padding: 1rem; text-align: center;&quot;&gt;
&lt;a href=&quot;https:&#x2F;&#x2F;vocus.cc&#x2F;article&#x2F;661567c8fd89780001e7adae&quot;&gt;繼續閱讀&lt;&#x2F;a&gt;
&lt;&#x2F;p&gt;
&lt;!-- 而 SpiffWorkflow 就是專門針對 business process 的流程引擎，同類型的引擎或產品有這些：

- [一等一科技 BPM](https:&#x2F;&#x2F;www.edetw.com&#x2F;uofx&#x2F;)，他們家的旗艦產品是 UOF EIP，BPM 是其中主要模組。這裡推一下一等一科技，他們家的導入、客服、技術服務都很有制度也很專業。
- [新人類資訊 FlowMaster BPM](https:&#x2F;&#x2F;www.newtype.com.tw&#x2F;flowmasterbpm.aspx)，牌價比樓上貴一點。
- [華苓科技 Agentflow BPM](https:&#x2F;&#x2F;www.flowring.com&#x2F;agentflow-%e6%99%ba%e9%9b%b2%e7%89%88&#x2F;)。
- [慧智科技 SmartBPM.NET](https:&#x2F;&#x2F;www.smartsoft.com.tw&#x2F;bpm.html)。

上面這些是國內的，還有很多鼎新什麼的，就不幫它們打廣告了。這些國內的產品都是完整的 BPM 解決方案，有流程引擎、有表單設計、有人員組織、有統計圖表等等，也有面向開發端的 API。

國外的則有：

- [Camnuda](https:&#x2F;&#x2F;camunda.com&#x2F;)，應該是國外最大的 BPM 吧。
- [ProcessMaker](https:&#x2F;&#x2F;www.processmaker.com&#x2F;)，除了 Camuda，商品化最成熟的產品。
- [jBPM](https:&#x2F;&#x2F;www.jbpm.org&#x2F;)，紅帽主導的 BPM。
- [Flowable](https:&#x2F;&#x2F;www.flowable.com&#x2F;)，德國的。
- [Alfresco Process Services](https:&#x2F;&#x2F;www.alfresco.com&#x2F;bpm-software)，做 EIP 的 Alfresco 也有 BPM 產品。
- [馳騁 BPM](http:&#x2F;&#x2F;ccflow.org&#x2F;)，中國的 BPM。
- [Bonita](https:&#x2F;&#x2F;www.bonitasoft.com&#x2F;business-process-management-bpm)。
- [Imixs Workflow](https:&#x2F;&#x2F;www.imixs.org&#x2F;)。
- [Automatiko](https:&#x2F;&#x2F;automatiko.io&#x2F;)。
- [SpiffWorkflow](https:&#x2F;&#x2F;www.spiffworkflow.org&#x2F;)。

國外產品比較偏向終端用戶的大概有 Cammda、ProcessMaker、馳騁 BPM、Flowable，其它則是比較面向開發者，包括本文的主角 SpiffWorkflow。

## BPMN

上面這些 BPM 的共同點是它們都以 BPMN 為基礎，BPMN 全稱 Business Process Model and Notation，是專門用來表示商業流程的一種圖，相較於一般的流程圖或 DAG，BPMN 有更豐富的符號來表示一段商業流程，下面是一張核彈發射流程的 BPMN：

![Nuclear Strike Workflow](NuclearStrikeWorkflow.drawio.png)


在 BPMN 的符號中：

- 圓圈表示 event，這裡有 Start event 和 End event。
- 菱形表示 gateway，簡單理解就是叉路，將軍可以決定要不要射，總統也可以決定要不要射。
- 圓角方框表示 task，就是具體要做的事情。

雖然任何一款繪圖應用都可以畫出這樣的圖，但要讓程式使用，就必須用標準組織制定的機讀格式製作 BPMN，目前 BPMN 的機讀格式標準由 [OMG](https:&#x2F;&#x2F;www.bpmn.org&#x2F;) 機構維護，本質上它是一種 XML 文件，前面那些國外的 BPM 大多可以讀入標準的 BPMN 檔案，而國內的 BPM 大多是玩自己的一套，這也沒什麼不好，只要能跑流程就好。

對於 BPMN 的認識，簡單帶過就好，因為下面 SpiffWorkflow 我們會跳過 BPMN 的部份 :p。

## SpiffWorkflow

SpiffWorkflow 是 business process 的執行引擎，具體一點說，它本質上是狀態機與[規則引擎](https:&#x2F;&#x2F;doublesllash.com&#x2F;blog&#x2F;posts&#x2F;2023&#x2F;rules-engine)的綜合體，流程制定好後，SpiffWorkflow 負責告訴我們當前該流程內所有工項的狀態，有的可能是 COMPLETED，有的可能是 READY，有的可能是 MAYBE、FUTURE 等等，隨著流程的推進，每個工項的狀態也會隨之變更，這也是 SpiffWorkflow 的主要價值所在，試想如果要自行判斷複雜流程的狀態，我們要寫多少規則才可能實現？

以上面的射核彈流程為例：

![Nuclear Strike Workflow](NuclearStrikeWorkflow.drawio.png)

SpiffWorkflow 可以讀入像這樣的 BPMN 檔案，並作為 workflow spec 之用，在 SpiffWorkflow 的設計裡：

- 制定的流程稱為 workflow spec
- 真正執行的流程稱為 workflow instance

一份 spec 之下會有許多的 instance，核彈一號的與核彈二號有各自的 workflow instance，也有各自的流程走向與結局，但它們的 spec 是相同的，都是將軍先決定，總統再決定。

### 制定 Workflow Spec

前面提到，SpiffWorkflow 可以讀入 BPMN 檔案並解析成 workflow spec，但這裡我們不這麼做，我們玩硬派的，手刻 spec，程式碼如下：

```py
from SpiffWorkflow.specs.WorkflowSpec import WorkflowSpec
from SpiffWorkflow.specs.ExclusiveChoice import ExclusiveChoice
from SpiffWorkflow.specs.Simple import Simple
from SpiffWorkflow.specs.Cancel import Cancel

from SpiffWorkflow.operators import Equal, Attrib

def my_nuclear_strike(msg):
    print(&quot;Launched:&quot;, msg)

class NuclearStrikeWorkflowSpec(WorkflowSpec):
    def __init__(self):
        super().__init__()

        # The first step of our workflow is to let the general confirm
        # the nuclear strike.
        general_choice = ExclusiveChoice(wf_spec=self, name=&#x27;general&#x27;)
        self.start.connect(taskspec=general_choice)

        # The default choice of the general is to abort.
        cancel = Cancel(wf_spec=self, name=&#x27;workflow_aborted&#x27;)
        general_choice.connect(task_spec=cancel)

        # Otherwise, we will ask the president to confirm.
        president_choice = ExclusiveChoice(wf_spec=self, name=&#x27;president&#x27;)
        cond = Equal(Attrib(name=&#x27;confirmation&#x27;), &#x27;yes&#x27;)
        general_choice.connect_if(condition=cond, task_spec=president_choice)

        # The default choice of the president is to abort.
        president_choice.connect(task_spec=cancel)

        # Otherwise, we will perform the nuclear strike.
        strike = Simple(wf_spec=self, name=&#x27;nuclear_strike&#x27;)
        president_choice.connect_if(condition=cond, task_spec=strike)

        # Now we connect our Python function to the Task named &#x27;nuclear_strike&#x27;
        strike.completed_event.connect(callback=my_nuclear_strike)

        # As soon as all tasks are either &quot;completed&quot; or  &quot;aborted&quot;, the
        # workflow implicitely ends.
```

可以看到，`SpiffWorkflow.specs` 模組下有許多用於定義 spec 的 class，這邊我們用到其中三個：

- `Simple`：表示最單純的 task。
- `ExclusiveChoice`：表示單選的 gateway。
- `Cancel`：表示 end，即取消發射。

其中 `ExclusiveChoice` 預設是走向 `Cancel`，除非將軍與總統都把 `confirmation` 設為 `yes`，這部份用到 `SpiffWorkflow.operators` 的運算子 class `Equal` 與 `Attrib`，整段流程不複雜，多看兩下應該可以參透，至於那 `confirmation` 要怎麼設、流程具體怎麼跑，讓我們看下去。

### 建立與操控 Workflow Instance

建立 workflow instance：

```py
from SpiffWorkflow.workflow import Workflow
from SpiffWorkflow.task import Task, TaskState

workflow_instance = Workflow(workflow_spec=NuclearStrikeWorkflowSpec())
```

Instance 建立之後，就可以開始把玩了，在跑流程之前，先玩轉 instance 的一些方法。

**Dump**

```py
workflow_instance.dump()
```

輸出如下：

```
358155a6-4776-46f2-aed2-cae4b18d6391&#x2F;0: Task of Root State: COMPLETED Children: 1
  a18df1d8-c5a2-4bd0-832b-39b9d38a46f3&#x2F;0: Task of Start State: READY Children: 1
    4d0eb169-15eb-42f5-a895-359590b5ce69&#x2F;0: Task of general State: FUTURE Children: 2
      32726718-f032-417c-8325-bdc2db2f878d&#x2F;0: Task of workflow_aborted State: LIKELY Children: 0
      41f2cb59-5d15-4735-b0af-fb42d75a1e0c&#x2F;0: Task of president State: MAYBE Children: 2
        821401fd-a55d-45a4-8131-fa6af712b6d9&#x2F;0: Task of workflow_aborted State: MAYBE Children: 0
        150966e3-05d2-4463-9209-d6506fa8f834&#x2F;0: Task of nuclear_strike State: MAYBE Children: 0
```

`dump()` 即把流程的狀態以父子階層的格式秀出來，這裡 SpiffWorkflow 把 event、gateway、task 都以 task 概稱之，一筆 task 有以下欄位：

- 最前面是它的 UUID，每個 spec 的 instance 的 task 的 UUID 都是獨立的。
- 中間是 task 名。
- 後面接 task 狀態，COMPLETED、READY 等等。
- 最後為旗下子 task 的數量。

---

**Get tasks**

```py
workflow_instance.get_tasks()
```

輸出如下：

```
[&lt;Task object (Root) in state COMPLETED at 0x7f9405f4f9d0&gt;,
 &lt;Task object (Start) in state READY at 0x7f9405f4d270&gt;,
 &lt;Task object (general) in state FUTURE at 0x7f9405f4cdc0&gt;,
 &lt;Task object (workflow_aborted) in state LIKELY at 0x7f9405f4e350&gt;,
 &lt;Task object (president) in state MAYBE at 0x7f9405f4d2d0&gt;,
 &lt;Task object (workflow_aborted) in state MAYBE at 0x7f9405f4fa30&gt;,
 &lt;Task object (nuclear_strike) in state MAYBE at 0x7f9405f4e3b0&gt;]
```

`get_tasks()` 也是展示當前狀態，以 Python 原生 Task list 的形式表現。

---

**Get tasks from spec name**

```py
workflow_instance.get_tasks_from_spec_name(name=&#x27;general&#x27;)
```

輸出如下：

```
[&lt;Task object (general) in state FUTURE at 0x7f9405f4cdc0&gt;]
```

以 task 名稱取得 task 物件，注意，這裡拿到的是 task list。

拿到 `Task` 物件後，也有一些方法與屬性。

### Task 方法與屬性

以將軍的發射決策 task 為例：

```py
general_task: Task = workflow_instance.get_tasks_from_spec_name(name=&#x27;general&#x27;)[0]
```

**ID**

```py
general_task.id
```

輸出如下：

```
UUID(&#x27;4d0eb169-15eb-42f5-a895-359590b5ce69&#x27;)
```

---

**狀態**

```py
general_task.get_state_name()
```

輸出如下：

```
&#x27;FUTURE&#x27;
```

對於 task 狀態，SpiffWorkflow 有以下幾種：

- **MAYBE**、**LIKELY**，這類屬於預期（PREDICTED）會發生的。
- **FUTURE**、**WAITING**、**READY**、**STARTED**，這類屬於確定（DEFINITE）會發生。
- **COMPLETED**、**ERROR**、**CANCELLED**，這類屬於結束（FINISHED）的。

隨著流程的推進，前面的決策會影響後面工項的狀態，這裡體現了 SpiffWorkflow 作為狀態機的特性。

雖然狀態這麼多，但最重要的是 READY，它表示接著要執行的 task，也是我們在使用 SpiffWorkflow 流程引擎時主要關注的狀態。

至於每種狀態的意義，除了望文生義外，也可以參見〈[SpiffWorkflow Concepts](https:&#x2F;&#x2F;spiffworkflow.readthedocs.io&#x2F;en&#x2F;latest&#x2F;concepts.html)〉。

---

### 資料

前面提到，將軍與總統都要做出決斷，把 `confirmation` 設為 `yes` 才可發射核彈，而這 `confirmation` 就會以 task data 的形式存在，做好決斷，推進流程，範例如下：

```py
# 跑前面的流程

general_task.set_data(confirmation=&#x27;yes&#x27;) # 做決斷
workflow_instance.run_task_from_id(task_id=general_task.id) # 跑流程
```

這樣決斷一波下來，總統的決斷 task 就會變成 READY，反之，若是不設定 confirmation 為 `yes`，那就變成 cancel task 是 READY 囉，總統就變成 CANCELLED。

認識完 instance 與 task，來重頭跑一次流程吧！

### 跑流程

建立 workflow instance，檢視初始狀態：

```py
workflow_instance = Workflow(workflow_spec=NuclearStrikeWorkflowSpec())
workflow_instance.dump()
```

```
d6ccb20e-69d9-4a14-9d01-4563a0addc2f&#x2F;0: Task of Root State: COMPLETED Children: 1
  ebd7018d-d190-4a3b-a4c1-02877006b77e&#x2F;0: Task of Start State: READY Children: 1
    b6917bf4-9efa-4f86-8025-2edbc5f766f8&#x2F;0: Task of general State: FUTURE Children: 2
      02a4cb8d-994b-4892-8ef5-53af2791b2e2&#x2F;0: Task of workflow_aborted State: LIKELY Children: 0
      c38a78d5-c24b-4123-a00c-772f76303134&#x2F;0: Task of president State: MAYBE Children: 2
        1949bc90-309e-404f-94fd-332693c1aa91&#x2F;0: Task of workflow_aborted State: MAYBE Children: 0
        8c518cb2-637d-4db6-85be-867039d06481&#x2F;0: Task of nuclear_strike State: MAYBE Children: 0
```

 那個 Root 和 Start task 是 SpiffWorkflow 預設的，來跑 Start task：

 ```py
start_tasks: list[Task] = workflow_instance.get_tasks_from_spec_name(name=&#x27;Start&#x27;)
for task in start_tasks:
    if task.state == TaskState.READY:
        workflow_instance.run_task_from_id(task_id=task.id)
workflow_instance.dump()
 ```

 ```
d6ccb20e-69d9-4a14-9d01-4563a0addc2f&#x2F;0: Task of Root State: COMPLETED Children: 1
  ebd7018d-d190-4a3b-a4c1-02877006b77e&#x2F;0: Task of Start State: COMPLETED Children: 1
    b6917bf4-9efa-4f86-8025-2edbc5f766f8&#x2F;0: Task of general State: READY Children: 2
      02a4cb8d-994b-4892-8ef5-53af2791b2e2&#x2F;0: Task of workflow_aborted State: LIKELY Children: 0
      c38a78d5-c24b-4123-a00c-772f76303134&#x2F;0: Task of president State: MAYBE Children: 2
        1949bc90-309e-404f-94fd-332693c1aa91&#x2F;0: Task of workflow_aborted State: MAYBE Children: 0
        8c518cb2-637d-4db6-85be-867039d06481&#x2F;0: Task of nuclear_strike State: MAYBE Children: 0
 ```

可以看到，Start task 變成 COMPLETED、general task 變成 READY。

接著跑 general task，讓他射：

```py
general_tasks: list[Task] = workflow_instance.get_tasks_from_spec_name(name=&#x27;general&#x27;)
for task in general_tasks:
    if task.state == TaskState.READY:
        task.set_data(confirmation=&#x27;yes&#x27;)
        workflow_instance.run_task_from_id(task_id=task.id)
workflow_instance.dump()
```

```
d6ccb20e-69d9-4a14-9d01-4563a0addc2f&#x2F;0: Task of Root State: COMPLETED Children: 1
  ebd7018d-d190-4a3b-a4c1-02877006b77e&#x2F;0: Task of Start State: COMPLETED Children: 1
    b6917bf4-9efa-4f86-8025-2edbc5f766f8&#x2F;0: Task of general State: COMPLETED Children: 1
      c38a78d5-c24b-4123-a00c-772f76303134&#x2F;0: Task of president State: READY Children: 2
        1949bc90-309e-404f-94fd-332693c1aa91&#x2F;0: Task of workflow_aborted State: LIKELY Children: 0
        5541d692-1c42-4872-a8d6-22fba2491761&#x2F;0: Task of nuclear_strike State: MAYBE Children: 0
```

現在 president task 變成 READY，繼續讓總統射：

```py
president_tasks: list[Task] = workflow_instance.get_tasks_from_spec_name(name=&#x27;president&#x27;)
for task in president_tasks:
    if task.state == TaskState.READY:
        task.set_data(confirmation=&#x27;yes&#x27;)
        workflow_instance.run_task_from_id(task_id=task.id)
workflow_instance.dump()
```

```
d6ccb20e-69d9-4a14-9d01-4563a0addc2f&#x2F;0: Task of Root State: COMPLETED Children: 1
  ebd7018d-d190-4a3b-a4c1-02877006b77e&#x2F;0: Task of Start State: COMPLETED Children: 1
    b6917bf4-9efa-4f86-8025-2edbc5f766f8&#x2F;0: Task of general State: COMPLETED Children: 1
      c38a78d5-c24b-4123-a00c-772f76303134&#x2F;0: Task of president State: COMPLETED Children: 1
        5541d692-1c42-4872-a8d6-22fba2491761&#x2F;0: Task of nuclear_strike State: READY Children: 0
```

現在都確認了，核彈進入戰術位置，射：

```py
strike_tasks: list[Task] = workflow_instance.get_tasks_from_spec_name(name=&#x27;nuclear_strike&#x27;)
for task in strike_tasks:
    if task.state == TaskState.READY:
        workflow_instance.run_task_from_id(task_id=task.id)
workflow_instance.dump()
```

```
Launched: &lt;SpiffWorkflow.workflow.Workflow object at 0x7fb0b09e30d0&gt;
d6ccb20e-69d9-4a14-9d01-4563a0addc2f&#x2F;0: Task of Root State: COMPLETED Children: 1
  ebd7018d-d190-4a3b-a4c1-02877006b77e&#x2F;0: Task of Start State: COMPLETED Children: 1
    b6917bf4-9efa-4f86-8025-2edbc5f766f8&#x2F;0: Task of general State: COMPLETED Children: 1
      c38a78d5-c24b-4123-a00c-772f76303134&#x2F;0: Task of president State: COMPLETED Children: 1
        5541d692-1c42-4872-a8d6-22fba2491761&#x2F;0: Task of nuclear_strike State: COMPLETED Children: 0
```

在這個例子中，將軍、總統的決斷都是直接設定就跑了，當然在更實際的應用中，SpiffWorkflow 作為流程引擎，還需要外部的程式與前端配合，才能真正讓用戶去輸入他的決斷，這部份就取決於各自的應用看要怎麼發揮了。

另外 SpiffWorkflow 欠缺一些完整的商業 BPM 產品的特性，例如權限（哪個節點歸誰簽）、代簽、會簽、組織、組織主管等，所以它真的只是流程引擎，適合應用的場景是自有專案又需要內含稍微複雜的商業流程，而不適合拿它來搭建一套請假系統、出差申請系統、請購系統等等，這些往往都牽涉到完整的組織與主管，遇到這類需求還是找商業化的 BPM 產品吧。 --&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>打通 Windows 與 WSL 任督二脈</title>
        <published>2023-07-23T00:00:00+00:00</published>
        <updated>2023-07-23T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2023/wsl/"/>
        <id>https://editor.leonh.space/2023/wsl/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2023/wsl/">&lt;p&gt;故事要從「Microsoft ❤️ Linux」說起，自 2019 年 Satya Nadella 對 Linux 高調示愛以後，就有了 WSL，這場跨越商業模式的愛能不能順利走下去，讓我們看下去。&lt;&#x2F;p&gt;
&lt;p&gt;WSL 讓我們不用裝雙開機就可以在 Windows 內跑起一套接近完整的 Linux，之所以能這樣，背後依賴的是 Hyper-V 虛擬化技術，本質上 WSL 是一個特製的 VM，也因此我們會遇到的第一個問題就是「 WSL 如何與 Windows 透過 IP 互連？」&lt;&#x2F;p&gt;
&lt;p&gt;WSL 如何與 Windows 透過 IP 互連實際上是兩個問題，見下文。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fu-wu-kai-zai-wsl-gei-windows-lian-ru&quot;&gt;服務開在 WSL，給 Windows 連入&lt;&#x2F;h2&gt;
&lt;p&gt;第一個問題，服務開在 WSL，Windows 要如何連入？&lt;&#x2F;p&gt;
&lt;p&gt;假設在 WSL 的 8086 埠跑著一支服務，試著在 Windows 連連看，最簡單的測試方式就是拿瀏覽器開看會不會通，測試下面兩個網址：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;http:&#x2F;&#x2F;localhost:8086&#x2F;&lt;&#x2F;li&gt;
&lt;li&gt;http:&#x2F;&#x2F;127.0.0.1:8086&#x2F;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在 Windows 以 http:&#x2F;&#x2F;127.0.0.1:8086&#x2F; 測試可能會被拒絕連線，因為如前面所說， WSL 本質上是一台特製的 VM，它有自己的 IP，而 127.0.0.1 是指向 Windows 自己，如果 Windows 沒有監聽 8086 埠的服務，那就會被拒絕連線。&lt;&#x2F;p&gt;
&lt;p&gt;另一方面，如果改用 http:&#x2F;&#x2F;localhost:8086&#x2F; 測試，則應該會成功，因為 Windows 很貼心的開啟了 WSL 的轉送機制，所有往 localhost 的請求都會轉給 WSL。&lt;&#x2F;p&gt;
&lt;p&gt;這邊有點違背直覺的是 localhost 竟然指向的不是常理的 127.0.0.1，而是 WSL，這點一旦疏忽，就有可能像我一樣，浪費一個下午搞不清摸不懂通與不通的難題。&lt;&#x2F;p&gt;
&lt;p&gt;至於 WSL 具體的 IP 是什麼，可以在 WSL 內的 shell 查詢：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ip addr&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;接著第二個問題，服務開在 Windows，WSL 要如何連入？&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fu-wu-kai-zai-windows-gei-wsl-lian-ru&quot;&gt;服務開在 Windows，給 WSL 連入&lt;&#x2F;h2&gt;
&lt;p&gt;基於對「WSL 本質是 VM」的理解，兩者間也必然是各有各的 IP，有專門的方法可以在 WSL 內查到 Windows 的 IP：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; cat &#x2F;etc&#x2F;resolv.conf&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;內容如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# This file was automatically generated by WSL. To stop automatic generation of this file, add the following entry to &#x2F;etc&#x2F;wsl.conf:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# [network]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# generateResolvConf = false&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;nameserver 172.22.128.1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;172.22.128.1 就是 Windows 在 WSL 網路的 IP，因此，只要開啟 http:&#x2F;&#x2F;172.22.128.1&#x2F; 應該也可以打開 Windows 的 web 服務了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;can-kao-zi-liao&quot;&gt;參考資料&lt;&#x2F;h2&gt;
&lt;p&gt;〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;learn.microsoft.com&#x2F;zh-tw&#x2F;windows&#x2F;wsl&#x2F;networking&quot;&gt;使用 WSL 存取網路應用程式&lt;&#x2F;a&gt;〉&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>PostgreSQL 備份與還原</title>
        <published>2023-06-15T00:00:00+00:00</published>
        <updated>2023-06-15T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2023/postgresql-dump-restore/"/>
        <id>https://editor.leonh.space/2023/postgresql-dump-restore/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2023/postgresql-dump-restore/">&lt;p&gt;本文為 PostgreSQL 備份之筆記，這裡的備份為冷備份。&lt;&#x2F;p&gt;
&lt;p&gt;下面以 postgres 這個帳號操作，它是 Linux 帳號，而在 PostgreSQL 裡面也有同名帳號，因為這樣，所以可以透過 PostgreSQL 的 peer 認證機制做後續的操作，簡單說就是只要 Linux 用戶確定是 postgres，那就可以直接通過 PostgreSQL 身份認證了。&lt;&#x2F;p&gt;
&lt;p&gt;PostgreSQL 備份的基本指令是 pg_dump，與之相對的，還原的基本指令是 pg_restore，下面先談備份。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;bei-fen&quot;&gt;備份&lt;&#x2F;h2&gt;
&lt;p&gt;假設有個資料庫 mocha，最簡單的備份命令如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; pg_dump&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --file=mocha.sql --dbname=mocha&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;備份下來的 mocha.sql 就是充滿 SQL 語句的純文字檔，節錄如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;--&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- PostgreSQL database dump&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;--&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Dumped from database version 15.2 (Ubuntu 15.2-1.pgdg22.04+1)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Dumped by pg_dump version 15.2 (Ubuntu 15.2-1.pgdg22.04+1)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SET&lt;&#x2F;span&gt;&lt;span&gt; statement_timeout &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 0&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SET lock_timeout =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 0&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SET&lt;&#x2F;span&gt;&lt;span&gt; idle_in_transaction_session_timeout &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 0&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SET&lt;&#x2F;span&gt;&lt;span&gt; client_encoding &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;UTF8&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SET&lt;&#x2F;span&gt;&lt;span&gt; standard_conforming_strings &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;= on&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SELECT&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; pg_catalog&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;set_config&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;search_path&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, false);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SET&lt;&#x2F;span&gt;&lt;span&gt; check_function_bodies &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; false;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SET&lt;&#x2F;span&gt;&lt;span&gt; xmloption &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; content;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SET&lt;&#x2F;span&gt;&lt;span&gt; client_min_messages &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; warning;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SET&lt;&#x2F;span&gt;&lt;span&gt; row_security &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;= off&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SET&lt;&#x2F;span&gt;&lt;span&gt; default_tablespace &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SET&lt;&#x2F;span&gt;&lt;span&gt; default_table_access_method &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt; heap;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;--&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;-- Name: _prisma_migrations; Type: TABLE; Schema: public; Owner: ap&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;--&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;CREATE TABLE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; public&lt;&#x2F;span&gt;&lt;span&gt;._prisma_migrations (&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    id &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;character varying&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;36&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; NOT NULL&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    checksum character varying&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;64&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; NOT NULL&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    finished_at &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamp with time zone&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    migration_name &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;character varying&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;255&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; NOT NULL&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    logs &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;text&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    rolled_back_at &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamp with time zone&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    started_at &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;timestamp with time zone DEFAULT now&lt;&#x2F;span&gt;&lt;span&gt;()&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; NOT NULL&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    applied_steps_count &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;integer DEFAULT&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 0&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; NOT NULL&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ALTER TABLE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; public&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;_prisma_migrations&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; OWNER TO&lt;&#x2F;span&gt;&lt;span&gt; ap;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;從裡面可以看到，它的 SQL 語句都有指定 &lt;code&gt;OWNER&lt;&#x2F;code&gt;，這意味著未來還原的時候，新的機台上也要先建好那位 owner，以本例來說，就是 ap 這個帳號，如果不想要這樣的行為，可以加 &lt;code&gt;--no-owner&lt;&#x2F;code&gt; 取消掉：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; pg_dump&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --file=mocha.sql --no-owner --dbname=mocha&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其他的參數可以隨喜使用，列舉一些如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--clean&lt;&#x2F;code&gt;，在備份檔中加入刪除原有資料庫的指令，不用手動砍掉重練。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;--create&lt;&#x2F;code&gt;，在備份檔中加入建立資料庫的指令，不用手動建 mocha 資料庫。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;範例：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; pg_dump&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --file=mocha.sql --clean --create --no-owner --dbname=mocha&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;把這行指令變成腳本：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;#!&#x2F;bin&#x2F;bash&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;pg_dump&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--file=&#x2F;mocha-data&#x2F;mocha_&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;$(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;date&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --iso-8601=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;date&amp;#39;)&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;.sql&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--clean&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--create&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--no-owner&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--dbname=mocha&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;檔名的部分用了 bash 的日期變數自動帶入。&lt;&#x2F;p&gt;
&lt;p&gt;把腳本存到 &#x2F;usr&#x2F;local&#x2F;bin&#x2F;backup-postgresql-mocha.sh，記得加上可執行權限。&lt;&#x2F;p&gt;
&lt;p&gt;另外儲存的資料夾在 &#x2F;mocha-data&#x2F;，記得也要設好寫入權限。&lt;&#x2F;p&gt;
&lt;p&gt;把這腳本設為 cron job 定期執行。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;yi-di-bei-fen&quot;&gt;異地備份&lt;&#x2F;h3&gt;
&lt;p&gt;本地備份完，可以上傳到異地、異域、異國、異星，如此即使地球爆炸了也得以保全資料到那美克星。&lt;&#x2F;p&gt;
&lt;p&gt;Linux 沒有什麼是一份腳本解決不了的，如果有，就兩份。&lt;&#x2F;p&gt;
&lt;p&gt;下面我們利用 Bash 腳本把備份的 SQL 檔案透過 HTTP 傳送給另一台 Synology NAS：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;#!&#x2F;bin&#x2F;bash&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;curl&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--data&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;api=SYNO.API.Auth&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--data&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;version=7&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--data&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;method=login&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--data&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;account=OneDogIsCute&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--data&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;passwd=TwoDogsAreSilly&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;-c&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &#x2F;tmp&#x2F;cookies.txt&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;http:&#x2F;&#x2F;200.0.0.78:5000&#x2F;webapi&#x2F;entry.cgi&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;curl&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--form&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;api=SYNO.FileStation.Upload&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--form&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;version=3&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--form&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;method=upload&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--form&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;path=&#x2F;db_backup&#x2F;mocha&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--form&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;create_parents=True&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--form&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;overwrite=overwrite&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;-L&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;-b&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &#x2F;tmp&#x2F;cookies.txt&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;-F&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;file=@&#x2F;mocha-data&#x2F;mocha-$(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;date&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --iso-8601=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;date&amp;#39;).sql&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;http:&#x2F;&#x2F;200.0.0.78:5000&#x2F;webapi&#x2F;entry.cgi&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;第一部份登入 Synology NAS 保存 cooke，第二步走 HTTP POST 把檔案丟上去。&lt;&#x2F;p&gt;
&lt;p&gt;同樣地，這份腳本也可以設為 cron job 定期執行，要注意的是它和前一個備份任務之間最好有足夠的時間差，避免備份檔還沒生成就執行上傳作業。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;huan-yuan&quot;&gt;還原&lt;&#x2F;h2&gt;
&lt;p&gt;還原的部份，可以問 ChatGPT。:p&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>PostgreSQL 複寫</title>
        <published>2023-06-13T00:00:00+00:00</published>
        <updated>2023-06-13T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2023/postgresql-replication/"/>
        <id>https://editor.leonh.space/2023/postgresql-replication/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2023/postgresql-replication/">&lt;p&gt;準備好兩台裝好 PostgreSQL 的主機，一台叫 primary，一台叫 replica，複寫的概念就是 primary 資料庫的任何異動都會即時的反應到 replica，就像影之分身一樣，當然，如果兩台主機位於異地的話就沒那麼即時了，簡單介紹完複寫的概念就來進入正題吧。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;primary-she-ding&quot;&gt;Primary 設定&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;jian-li-fu-xie-zhang-hao&quot;&gt;建立複寫帳號&lt;&#x2F;h3&gt;
&lt;p&gt;建立帳號，授予複寫權限：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;CREATE ROLE&lt;&#x2F;span&gt;&lt;span&gt; repuser &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WITH&lt;&#x2F;span&gt;&lt;span&gt; REPLICATION &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;PASSWORD&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;&amp;lt;PASSWORD&amp;gt;&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; LOGIN&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;pei-zhi-fu-xie-she-ding&quot;&gt;配置複寫設定&lt;&#x2F;h3&gt;
&lt;p&gt;配置 primary 複寫設定，以我的機台為例，配置文件在 &#x2F;etc&#x2F;postgresql&#x2F;15&#x2F;main&#x2F;postgresql.conf。&lt;&#x2F;p&gt;
&lt;p&gt;配置文件分成幾個區段：&lt;&#x2F;p&gt;
&lt;p&gt;到「CONNECTION AND AUTHENTICATION」區段，修改以下項目：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;listen_addresses&lt;&#x2F;code&gt;，從 &lt;code&gt;&#x27;localhost&#x27;&lt;&#x2F;code&gt; 改為 primary 自身的 IP，或者有多張網卡的話可以大氣一點改為 &lt;code&gt;*&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;到「WRITE-AHEAD-LOG」區段，修改以下項目：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;wal_level&lt;&#x2F;code&gt;，原本是註解掉的，註解拿掉改為 &lt;code&gt;replica&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;上面的 write-ahead log (WAL)，簡單說就是資料庫的異動紀錄，所謂的複寫就是把 primary 的 WAL 傳給 replica，就好像跑步游泳一樣，A 做什麼 B 就做什麼。&lt;&#x2F;p&gt;
&lt;p&gt;找到「REPLICATION」區段，修改以下項目：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;max_wal_senders&lt;&#x2F;code&gt;，原本是被註解掉的，把註解拿掉，改值為 &lt;code&gt;2&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;max_replication_slots&lt;&#x2F;code&gt;，原本是註解掉的，註解拿掉改為 &lt;code&gt;2&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;改完後重啟 PostgreSQL：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; sudo systemctl restart postgresql&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;jian-li-replication-slot&quot;&gt;建立 replication slot&lt;&#x2F;h3&gt;
&lt;p&gt;Primary 自己的 WAL 是會隨著時間刪除的，但在 replicatoin slot 的機制下，它會等到 replica 有收到紀錄後才排入刪除，如果 replica 斷線的話 primary 會幫它保留紀錄，等到 replica 上線的那天（好像有點浪漫）。&lt;&#x2F;p&gt;
&lt;p&gt;進入 PostgreSQL 介面，建立一個 replication slot：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SELECT * FROM&lt;&#x2F;span&gt;&lt;span&gt; pg_create_physical_replication_slot(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;replica_1_slot&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;建好離開回到 Linux shell。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;she-ding-lian-xian-yu-ren-zheng&quot;&gt;設定連線與認證&lt;&#x2F;h3&gt;
&lt;p&gt;PostgreSQL 用戶認證的配置文件在 &#x2F;etc&#x2F;postgresql&#x2F;15&#x2F;main&#x2F;pg_hba.conf，最下面原本長類似下面這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# Allow replication connections from localhost, by a user with the&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# replication privilege.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# TYPE  DATABASE     USER  ADDRESS       METHOD&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;local   replication  all                 peer&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;host    replication  all   127.0.0.1&#x2F;32  scram-sha-256&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;host    replication  all   ::1&#x2F;128       scram-sha-256&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;最下面三行：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;第一行表示所有有 replication 權限的用戶都可以從本機走 Unix socket 登入，登入認證走 Linux 系統認證。&lt;&#x2F;li&gt;
&lt;li&gt;第二行表示所有有 replication 權限的 127.0.0.1（本機）用戶都可以走 TCP socket 登入，以密碼認證。&lt;&#x2F;li&gt;
&lt;li&gt;第三行同上，只差在 IP 為本機 IPv6。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在我們的複寫場景，replica 是另外一台主機，所以幫它開一條路，加一行：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# TYPE  DATABASE     USER  ADDRESS  METHOD&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;host    replication  all   samenet  scram-sha-256&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這裡的 &lt;code&gt;samenet&lt;&#x2F;code&gt; 表示與 primary 機同一網段的客戶端，例如 primary &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.calculator.net&#x2F;ip-subnet-calculator.html?cclass=any&amp;amp;csubnet=23&amp;amp;cip=192.168.100.212&amp;amp;ctype=ipv4&amp;amp;printit=0&amp;amp;x=37&amp;amp;y=10&quot;&gt;IP 為 192.168.100.212&#x2F;23 的話，它的網段就是 192.168.100.1 - 192.168.101.254&lt;&#x2F;a&gt;，&lt;&#x2F;p&gt;
&lt;p&gt;改完後重啟 PostgreSQL：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; sudo systemctl restart postgresql&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這樣 replica 應該就可以連進來了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;replica-she-ding&quot;&gt;Replica 設定&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;ting-zhi-replica-postgresql-fu-wu&quot;&gt;停止 replica PostgreSQL 服務&lt;&#x2F;h3&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; sudo systemctl stop postgresql&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;kan-replica-zi-liao-ku&quot;&gt;砍 replica 資料庫&lt;&#x2F;h3&gt;
&lt;p&gt;砍之前先備份：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; cd &#x2F;var&#x2F;lib&#x2F;postgresql&#x2F;15&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; sudo -u postgres tar --create --file&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;main.tar&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; main&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;身為影武者是不能有自己的身份的，把 replica 原有的資料庫檔案砍掉：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; sudo -u postgres rm --recursive --force &#x2F;var&#x2F;lib&#x2F;postgresql&#x2F;15&#x2F;main&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;*&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;bei-fen-primary-zi-liao-ku&quot;&gt;備份 primary 資料庫&lt;&#x2F;h3&gt;
&lt;p&gt;在 replica 執行下面指令，把當前的 primary 資料庫備份到 replica 身上：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; sudo -u postgres bash&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;postgres_shell&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; pg_basebackup&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--host=192.168.100.212&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--username=repuser&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --password \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--pgdata=&#x2F;var&#x2F;lib&#x2F;postgresql&#x2F;15&#x2F;main&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--progress&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --verbose&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;yi-primary-zi-liao-ku-zhong-jian-replica&quot;&gt;以 primary 資料庫重建 replica&lt;&#x2F;h3&gt;
&lt;p&gt;把 primary 備份到 replica 之後，要以這份 primary 資料庫為基礎重建 replica，讓兩者有相同的基礎。&lt;&#x2F;p&gt;
&lt;p&gt;Touch 一個空檔案：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; sudo -u postgres touch &#x2F;var&#x2F;lib&#x2F;postgresql&#x2F;15&#x2F;main&#x2F;standby.signal&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;稍後 PostgreSQL 服務啟動時偵測到這個檔案就會啟動還原模式重建資料庫。&lt;&#x2F;p&gt;
&lt;p&gt;但此刻還不急著把服務叫起來，還有更多的配置等著我們…。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;pei-zhi-fu-xie-she-ding-1&quot;&gt;配置複寫設定&lt;&#x2F;h3&gt;
&lt;p&gt;這邊來配置 replica 端的複寫設定，修改檔案 &#x2F;etc&#x2F;postgresql&#x2F;15&#x2F;main&#x2F;postgresql.conf。&lt;&#x2F;p&gt;
&lt;p&gt;到「WRITE-AHEAD-LOG」區段，修改以下項目：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;wal_level&lt;&#x2F;code&gt;，原本是註解掉的，註解拿掉改為 &lt;code&gt;replica&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;找到「REPLICATION」區段，修改以下項目：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;max_wal_senders&lt;&#x2F;code&gt;，原本是被註解掉的，把註解拿掉，改值為 &lt;code&gt;2&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;max_replication_slots&lt;&#x2F;code&gt;，原本是註解掉的，註解拿掉改為 &lt;code&gt;2&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;primary_conninfo&lt;&#x2F;code&gt;，原本是註解掉的，取消註解，填入 &lt;code&gt;&#x27;host=192.168.100.212 port=5432 user=repuser password=&amp;lt;PASSWORD&amp;gt; application_name=r1&#x27;&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;primary_slot_name&lt;&#x2F;code&gt;，原本是註解掉的，取消註解，填入 &lt;code&gt;replica_1_slot&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;其中：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;前兩項的設定值與 primary 相同。&lt;&#x2F;li&gt;
&lt;li&gt;第三項的 &lt;code&gt;application_name=r1&lt;&#x2F;code&gt; 這部份的 &lt;code&gt;r1&lt;&#x2F;code&gt; 是讓 replica 與 primary 溝通的識別名稱，後面還會用到。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;qi-dong-replica-postgresql-fu-wu&quot;&gt;啟動 replica PostgreSQL 服務&lt;&#x2F;h3&gt;
&lt;p&gt;終於可以啟動了：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; sudo systemctl start postgresql&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;que-ren-fu-xie-zhuang-tai&quot;&gt;確認複寫狀態&lt;&#x2F;h3&gt;
&lt;p&gt;首先看剛啟動的 replica 的 log 確認情況：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; tail &#x2F;var&#x2F;log&#x2F;postgresql&#x2F;postgresql-15-main.log&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;應該會看到類似下面的訊息：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2023-03-14 10:00:12.276 CST [9401] LOG:  starting PostgreSQL 15.2 (Ubuntu 15.2-1.pgdg22.04+1) on x86_64-pc-linux-gnu, compiled by gcc (Ubuntu 11.3.0-1ubuntu1~22.04) 11.3.0, 64-bit&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2023-03-14 10:00:12.277 CST [9401] LOG:  listening on IPv4 address &amp;quot;0.0.0.0&amp;quot;, port 5432&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2023-03-14 10:00:12.277 CST [9401] LOG:  listening on IPv6 address &amp;quot;::&amp;quot;, port 5432&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2023-03-14 10:00:12.279 CST [9401] LOG:  listening on Unix socket &amp;quot;&#x2F;var&#x2F;run&#x2F;postgresql&#x2F;.s.PGSQL.5432&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2023-03-14 10:00:12.283 CST [9404] LOG:  database system was interrupted; last known up at 2023-03-14 09:06:15 CST&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2023-03-14 10:00:12.349 CST [9404] LOG:  entering standby mode&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2023-03-14 10:00:12.352 CST [9404] LOG:  redo starts at 0&#x2F;C000028&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2023-03-14 10:00:12.353 CST [9404] LOG:  consistent recovery state reached at 0&#x2F;C000138&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2023-03-14 10:00:12.353 CST [9401] LOG:  database system is ready to accept read-only connections&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2023-03-14 10:00:12.378 CST [9405] LOG:  started streaming WAL from primary at 0&#x2F;D000000 on timeline 1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到，此時 replica 作為分身，除了複寫外，只允許讀取，不能再手動增改資料了。&lt;&#x2F;p&gt;
&lt;p&gt;然後進 replica 的 PostgreSQL 查詢複寫狀態：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;\x # Turn &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;on&lt;&#x2F;span&gt;&lt;span&gt; the expanded display mode&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SELECT * FROM&lt;&#x2F;span&gt;&lt;span&gt; pg_stat_wal_receiver;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;那 &lt;code&gt;\x&lt;&#x2F;code&gt; 只是為了美化輸出格式，美化後的輸出應該像下面這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;-[ RECORD 1 ]---------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;pid                   | 9405&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;status                | streaming&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;receive_start_lsn     | 0&#x2F;D000000&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;receive_start_tli     | 1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;written_lsn           | 0&#x2F;D000148&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;flushed_lsn           | 0&#x2F;D000148&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;received_tli          | 1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;last_msg_send_time    | 2023-03-14 10:22:14.280002+08&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;last_msg_receipt_time | 2023-03-14 10:22:14.30126+08&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;latest_end_lsn        | 0&#x2F;D000148&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;latest_end_time       | 2023-03-14 10:00:12.385886+08&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;slot_name             | replica_1_slot&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;sender_host           | 192.168.100.212&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;sender_port           | 5432&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;conninfo              | user=repuser password=******** channel_binding=prefer dbname=replication host=192.168.100.212 port=5432 application_name=r1 fallback_application_name=15&#x2F;main sslmode=prefer sslcompression=0 sslsni=1 ssl_min_protocol_version=TLSv1.2 gssencmode=prefer krbsrvname=postgres target_session_attrs=any&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中 &lt;code&gt;status&lt;&#x2F;code&gt; 可以看到為同步串流中。&lt;&#x2F;p&gt;
&lt;p&gt;然後來看看 primary 這邊。&lt;&#x2F;p&gt;
&lt;p&gt;進 primary 的 PostgreSQL 查詢複寫狀態：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;\x # Turn &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;on&lt;&#x2F;span&gt;&lt;span&gt; the expanded display mode&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SELECT * FROM&lt;&#x2F;span&gt;&lt;span&gt; pg_stat_replication;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;結果如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;-[ RECORD 1 ]----+------------------------------&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;pid              | 27576&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;usesysid         | 16388&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;usename          | repuser&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;application_name | r1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;client_addr      | 192.168.100.213&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;client_hostname  | &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;client_port      | 50768&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;backend_start    | 2023-03-14 10:00:12.366893+08&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;backend_xmin     | &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;state            | streaming&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;sent_lsn         | 0&#x2F;D000148&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;write_lsn        | 0&#x2F;D000148&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;flush_lsn        | 0&#x2F;D000148&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;replay_lsn       | 0&#x2F;D000148&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;write_lag        | &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;flush_lag        | &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;replay_lag       | &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;sync_priority    | 0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;sync_state       | async&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;reply_time       | 2023-03-14 10:13:23.53618+08&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;state&lt;&#x2F;code&gt; 可以看到為同步串流中。&lt;&#x2F;li&gt;
&lt;li&gt;幾個 &lt;code&gt;-lag&lt;&#x2F;code&gt; 都為空，表示 replica 沒有落後。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;以上都沒有問題，複寫機制就建立成功了，所有對 primary 的操作都會同步反應到 replica，目前這樣的模式只能算是熱備份，還不能稱為高可用，因為 primary 單點故障不會有人即時代替。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>PostgreSQL 幼幼班</title>
        <published>2023-06-12T00:00:00+00:00</published>
        <updated>2023-06-12T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2023/postgresql/"/>
        <id>https://editor.leonh.space/2023/postgresql/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2023/postgresql/">&lt;p&gt;對於資料庫，除了設計精巧，內建於各大語言、平台的 SQLite 外，一直以來我都是用 MariaDB &#x2F; MySQL，原因很簡單，最多人用，從眾罷了，但最近注意到一些資料庫雲端服務廠商都是以 PostgreSQL 為基礎，並且各路大佬也都一致褒揚 PostgreSQL，且考量到 PostgreSQL 生態也足夠大，手邊有剛好要開個嚴肅的新專案，就毅然跳槽到 PostgreSQL 了（相對而言要轉到 Firebird 我就不敢），這邊是有 MariaDB &#x2F; MySQL 經驗的用戶轉學到 PostgreSQL 幼幼班的小筆記。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yi-rong-qi-yun-xing&quot;&gt;以容器運行&lt;&#x2F;h2&gt;
&lt;p&gt;由 Docker 維護的容器映像為 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;hub.docker.com&#x2F;_&#x2F;postgres&quot;&gt;postgres&lt;&#x2F;a&gt;，跑起來很簡單：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;podman&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; run&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--detach&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--env&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; POSTGRES_PASSWORD=mysecretpassword&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; pg&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--publish&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; 5432:5432&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;docker.io&#x2F;library&#x2F;postgres:16-alpine&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這裡我用的是 &lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;podman&#x2F;&quot;&gt;Podman&lt;&#x2F;a&gt;，如果是您是用 Docker（注意，您可能是盜版軟體的受害者），把 &lt;code&gt;podman&lt;&#x2F;code&gt; 換成 &lt;code&gt;docker&lt;&#x2F;code&gt;、&lt;code&gt;--publish&lt;&#x2F;code&gt; 換成 &lt;code&gt;--port&lt;&#x2F;code&gt; 就可以了。&lt;&#x2F;p&gt;
&lt;p&gt;指令的涵義大多可以從參數的名稱猜出來，例如 &lt;code&gt;--publish 5432:5432&lt;&#x2F;code&gt; 用於映射 Docker host 與容器的 port，前面的是 Docker host port、後面的是容器 port，依照 PostgreSQL 的慣例開在 5432。&lt;&#x2F;p&gt;
&lt;p&gt;另外，上面的命令也指定了 PostgreSQL 的密碼為 &lt;code&gt;myscrectpassword&lt;&#x2F;code&gt;，並且我選用了以輕薄短小的 Alpine Linux 為基礎的 PostgreSQL 16 映像檔。&lt;&#x2F;p&gt;
&lt;p&gt;典型的連線字串為 &lt;code&gt;postgresql:&#x2F;&#x2F;postgres:myscrectpassword@localhost:5432&#x2F;postgres&lt;&#x2F;code&gt;，其中 &lt;code&gt;postgres&lt;&#x2F;code&gt; 是預設的 PostgreSQL 管理員帳戶名稱，最後面的 &lt;code&gt;postgres&lt;&#x2F;code&gt; 則是預設的資料庫名稱。&lt;&#x2F;p&gt;
&lt;p&gt;作為開發環境，以容器運行 PostgreSQL 是滿好的選擇，對於生產環境，在還沒導入潮潮的 K8s 之前，只能一步一步裝囉！&lt;&#x2F;p&gt;
&lt;h2 id=&quot;an-zhuang&quot;&gt;安裝&lt;&#x2F;h2&gt;
&lt;p&gt;在 PostgreSQL 的生態裡有很多第三方機構有做安裝包，通常會賦予一些額外的工具、套件、特性，有些是收費的，還有一些是以 PostgreSQL 為基礎，在上面搭建出自己的資料庫產品的，例如官網的 EDB，其他還有 bit.io、Timescale、Neon、Supabase、Citus、Greenplum 等等，可以參見〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;wiki.postgresql.org&#x2F;wiki&#x2F;PostgreSQL_derived_databases&quot;&gt;PostgreSQL derived databases&lt;&#x2F;a&gt;〉。&lt;&#x2F;p&gt;
&lt;p&gt;PostgreSQL 經歷多年發展有許多版本還在流通，要用哪個版本首先要看還有哪些版本有&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.postgresql.org&#x2F;support&#x2F;versioning&#x2F;&quot;&gt;支援&lt;&#x2F;a&gt;，也就是還會有更新，以 PostgreSQL 11 為例，它在 2023 年 11 月結束支援，而 PostgreSQL 16 則要到 2028 年才會結束支援。&lt;&#x2F;p&gt;
&lt;p&gt;另外也可以參考&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.postgresql.org&#x2F;about&#x2F;featurematrix&#x2F;&quot;&gt;功能矩陣表&lt;&#x2F;a&gt;來決定適合的版本。&lt;&#x2F;p&gt;
&lt;p&gt;在 Windows 安裝，沒什麼懸念，到官網下載 EDB 打包的安裝包，下一步下一步就好。&lt;&#x2F;p&gt;
&lt;p&gt;如果是 Linux，Linux 預帶套件庫中的版本可能不是最新的，以 Ubuntu 22.04 為例，預帶套件庫的 PostgreSQL 版本是 14，如果要裝 PostgreSQL 16，那就要添加 PostgreSQL 維護的 APT 庫，對 PostgreSQL 小白來說，理所當然選擇最新的版本。&lt;&#x2F;p&gt;
&lt;p&gt;一氣呵成的安裝步驟如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# Create the file repository configuration:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo sh&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -c&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;echo &amp;quot;deb http:&#x2F;&#x2F;apt.postgresql.org&#x2F;pub&#x2F;repos&#x2F;apt $(lsb_release -cs)-pgdg main&amp;quot; &amp;gt; &#x2F;etc&#x2F;apt&#x2F;sources.list.d&#x2F;pgdg.list&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# Import the repository signing key:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; wget&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --quiet -O&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; - https:&#x2F;&#x2F;www.postgresql.org&#x2F;media&#x2F;keys&#x2F;ACCC4CF8.asc&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; |&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; sudo&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; apt-key add -&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# Update the package lists:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo apt-get update&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# Install the latest version of PostgreSQL.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# If you want a specific version, use &amp;#39;postgresql-12&amp;#39; or similar instead of &amp;#39;postgresql&amp;#39;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo apt-get install postgresql-16&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;第二步加金鑰系統會回報「Warning: apt-key is deprecated. Manage keyring files in trusted.gpg.d instead (see apt-key(8)).」，只是警示，可以無視，欲知詳情，參見《&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;itsfoss.com&#x2F;apt-key-deprecated&#x2F;&quot;&gt;Handling “apt-key is deprecated. Manage keyring files in trusted.gpg.d instead” in Ubuntu Linux&lt;&#x2F;a&gt;》。&lt;&#x2F;li&gt;
&lt;li&gt;第三步系統會回報「W: http:&#x2F;&#x2F;apt.postgresql.org&#x2F;pub&#x2F;repos&#x2F;apt&#x2F;dists&#x2F;jammy-pgdg&#x2F;InRelease: Key is stored in legacy trusted.gpg keyring (&#x2F;etc&#x2F;apt&#x2F;trusted.gpg), see the DEPRECATION section in apt-key(8) for details.」，原因同上，可以無視。&lt;&#x2F;li&gt;
&lt;li&gt;如果想固定在 PostgreSQL 16.X，不想承受跳主版本更新的風險，請安裝 &lt;code&gt;postgresql-16&lt;&#x2F;code&gt; 套件，如果想讓跟著主板本更新，請安裝 &lt;code&gt;postgresql&lt;&#x2F;code&gt; 套件，這裡本人選用 &lt;code&gt;postgresql-16&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;裝完之後：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;多了一個系統帳號 postgres，這也是 PostgreSQL 預設的管理帳號。&lt;&#x2F;li&gt;
&lt;li&gt;多了兩個配置資料夾 &#x2F;etc&#x2F;postgresql&#x2F; 和 &#x2F;etc&#x2F;postgresql-common。&lt;&#x2F;li&gt;
&lt;li&gt;多了一個系統服務 postgresql.service 在 &#x2F;etc&#x2F;systemd&#x2F;system&#x2F;multi-user.target.wants&#x2F;postgresql.service，它會監聽 5432 埠。&lt;&#x2F;li&gt;
&lt;li&gt;PostgreSQL 預設 locale 為 &lt;code&gt;en_US.UTF-8&lt;&#x2F;code&gt;、encoding 為 &lt;code&gt;UTF8&lt;&#x2F;code&gt;、text search 為 &lt;code&gt;english&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;資料庫的真身在 &#x2F;var&#x2F;lib&#x2F;postgresql&#x2F;16&#x2F;，知道就好不要亂動。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;pei-zhi&quot;&gt;配置&lt;&#x2F;h2&gt;
&lt;p&gt;主要的配置文件在 &#x2F;etc&#x2F;postgresql&#x2F;16&#x2F;main&#x2F;postgresql.conf，裡面參數眾多，可以參考 PostgreSQL 手冊的《&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.postgresql.tw&#x2F;server-administration&#x2F;server-configuration&quot;&gt;服務組態設定&lt;&#x2F;a&gt;》來了解，或者參考網站 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;postgresqlco.nf&#x2F;doc&#x2F;zh&#x2F;param&#x2F;&quot;&gt;POSTGRESQLCO.NF&lt;&#x2F;a&gt;，大部份保留原樣即可，下面是我個人異動的項目。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;lian-xian-yu-ren-zheng-connectoins-and-authentication&quot;&gt;連線與認證（connectoins and authentication）&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;listen_addresses&lt;&#x2F;code&gt; 從 &lt;code&gt;&#x27;localhost&#x27;&lt;&#x2F;code&gt; 改為&lt;strong&gt;機台自身的 IP&lt;&#x2F;strong&gt; 或 &lt;code&gt;&#x27;*&#x27;&lt;&#x2F;code&gt;，讓 PostgreSQL 接受外部連線。注意這是設定 PostgreSQL 要監聽的 IP 位址，不是客戶端的白名單位址。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;unix_socket_directories = &#x27;&#x2F;var&#x2F;run&#x2F;postgresql&#x27;&lt;&#x2F;code&gt; ，此項不動，只是讓我自己知道 PostgreSQL 也有接受 Unix socket 連線。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;cuo-wu-hui-bao-yu-ri-zhi-ji-lu-reporting-and-logging&quot;&gt;錯誤回報與日誌記錄（reporting and logging）&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;log_timezone = &#x27;Asia&#x2F;Taipei&#x27;&lt;&#x2F;code&gt;，此項不動，知道而已。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;yong-hu-duan-lian-xian-yu-she-can-shu-client-connection-defaults&quot;&gt;用戶端連線預設參數（client connection defaults）&lt;&#x2F;h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;datestyle = &#x27;iso, mdy&#x27;&lt;&#x2F;code&gt;，此項不動，知道而已。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;timezone = &#x27;Asia&#x2F;Taipei&#x27;&lt;&#x2F;code&gt;，此項不動，知道而已。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;改完重啟服務：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo systemctl restart postgresql&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;jin-ru-postgresql&quot;&gt;進入 PostgreSQL&lt;&#x2F;h2&gt;
&lt;p&gt;假設剛灌完什麼都沒配置，只能用 postgres 帳號進去：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -u&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; postgres psql&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;進去就會看到 PostgreSQL 的文字交互介面了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zhang-hao-yu-jiao-se&quot;&gt;帳號與角色&lt;&#x2F;h2&gt;
&lt;p&gt;PostgreSQL 的身分認證配置文件在 &#x2F;etc&#x2F;postgres&#x2F;16&#x2F;main&#x2F;pg_hba.conf，它的配置方式類似表格，最基本的像這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# type  database  user      auth-method  [auth-options]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;local   all       postgres  peer&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;逐項說明：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Type 為 &lt;code&gt;local&lt;&#x2F;code&gt;，表示走本機 Unix socket 連線。&lt;&#x2F;li&gt;
&lt;li&gt;Database 為 &lt;code&gt;all&lt;&#x2F;code&gt;，表示允許讀取所有資料庫。&lt;&#x2F;li&gt;
&lt;li&gt;User 為 &lt;code&gt;postgres&lt;&#x2F;code&gt;，表示 PostgreSQL 的帳號。&lt;&#x2F;li&gt;
&lt;li&gt;Auth-method 為 &lt;code&gt;peer&lt;&#x2F;code&gt;，表示走 OS 的帳號認證機制。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;以上綜合起來表示「PostgreSQL 帳號 postgres 有權力走 Unix socket 連線至 PostgresSQL，並且以 Linux 帳號 postgres 做認證，且能操作任何資料庫。」&lt;&#x2F;p&gt;
&lt;p&gt;來看第二個例子：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# type  database  user auth-method  [auth-options]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;local   all       all  peer&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這表示「所有的 PostgreSQL 帳號都能透過 Unix socket 連線至 PostgreSQL，並且以當下登入之 Linux 帳號做認證，且能操作任何資料庫。」&lt;&#x2F;p&gt;
&lt;p&gt;這裡所謂「所有 PostgreSQL 帳號」可以透過 SQL 查詢取得：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SELECT&lt;&#x2F;span&gt;&lt;span&gt; rolname &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span&gt; pg_roles;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;結果如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;rolname          &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;---------------------------&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; postgres&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; pg_database_owner&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; pg_read_all_data&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; pg_write_all_data&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; pg_monitor&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; pg_read_all_settings&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; pg_read_all_stats&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; pg_stat_scan_tables&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; pg_read_server_files&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; pg_write_server_files&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; pg_execute_server_program&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; pg_signal_backend&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; pg_checkpoint&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(13 rows)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到除了安裝時添加的 postgres 之外，剩下的 pg_xxx 都是系統預建的角色，不作為登入帳號之用。&lt;&#x2F;p&gt;
&lt;p&gt;回到前面的問題「所有 PostgreSQL 帳號」究竟有哪些？目前只有 postgres 這位而已，所以我今天用 Linux 帳號 leon 是進不去 PostgreSQL 的，即便身份認證那邊設了 &lt;code&gt;all&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;看起來 PostgreSQL 推薦的實踐原則也是只以 postgres 帳號當 DBA，所以在本機端，還是乖乖的用 &lt;code&gt;sudo -u postgres psql&lt;&#x2F;code&gt; 進入 PostgreSQL 吧，其他應用再單獨開立權限較小的帳號給應用使用。&lt;&#x2F;p&gt;
&lt;p&gt;在遠端連線方面，看下面的例子，因為要走 TCP&#x2F;IP，就有多了與 IP 相關的欄位：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# type  database  user      address       auth-method    [auth-options]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;host    all       all       127.0.0.1&#x2F;32  scram-sha-256&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;host    all       all       ::1&#x2F;128       scram-sha-256&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這兩個位址都是 &lt;a rel=&quot;external&quot; href=&quot;http:&#x2F;&#x2F;localhost&quot;&gt;localhost&lt;&#x2F;a&gt; 的意思，也就是本機，第一行是 IPv4，第二行是 IPv6，這兩行的整體意思為「所有的 PostgreSQL 帳號只要它是位於 localhost，都能透過 TCP&#x2F;IP 連線至 PostgreSQL，並且以 SHA-256 加密之密碼做認證，且能操作任何資料庫。」&lt;&#x2F;p&gt;
&lt;p&gt;前面提到在 Linux shell 層切換到 postgres 帳號進入 PostgreSQL：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -u&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; postgres psql&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;但一些本機的資料庫應用還是走密碼認證比較方便，要替 PostgreSQL 的 postgres 帳號添加密碼的話，可以如此這般：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ALTER USER&lt;&#x2F;span&gt;&lt;span&gt; postgres &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;PASSWORD&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;myPassword&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;設好密碼之後，也要改一下 pg_hba.conf 讓它接受走 Unix socket + 密碼認證，在 pg_hba.conf 加入設置：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# type  database  user                    auth-method  [auth-options]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;local   all       all                     scram-sha-256&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;local   all       postgres                peer&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;local   all       all                     peer&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# type  database  user      address       auth-method    [auth-options]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;host    all       all       127.0.0.1&#x2F;32  scram-sha-256&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;host    all       all       ::1&#x2F;128       scram-sha-256&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上文中第二行就是讓 PostgreSQL 接受 Unix soket 且以密碼認證的設定。&lt;&#x2F;p&gt;
&lt;p&gt;改完記得重啟 PostgreSQL 服務。&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo systemctl restart postgresql&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;依照上面的設定，在本機可以走 TCP 連線，以帳密認證，連線字串會長這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;postgresql:&#x2F;&#x2F;postgres:myPassword@localhost:5432&#x2F;mydb&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;也可以走 Unix socket 連線，以帳密認證，連線字串長這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;postgresql:&#x2F;&#x2F;postgres:myPassword@localhost&#x2F;mydb?host=&#x2F;var&#x2F;run&#x2F;postgresql&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;但要注意的是這裡的連線字串可能並非一體適用，每款應用的配置格式可能會有差異。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;bu-chong-zi-liao&quot;&gt;補充資料&lt;&#x2F;h2&gt;
&lt;p&gt;-〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;pg.vonng.com&#x2F;#&#x2F;app&#x2F;sql-locale&quot;&gt;PG中的本地化排序规则&lt;&#x2F;a&gt;〉。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>MariaDB 安裝小筆記</title>
        <published>2023-06-09T00:00:00+00:00</published>
        <updated>2023-06-09T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2023/mariadb/"/>
        <id>https://editor.leonh.space/2023/mariadb/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2023/mariadb/">&lt;p&gt;MariaDB 是 MySQL 的兄弟，有些人擔心 MySQL 被甲骨文買走之後未來會變得越來越貴，就像後來的 Java 那樣，會想要轉移到 MariaDB，而這兩者在第 10 版之後細部差異也越來越多，但就基本的 SQL 語句都還是可以無痛轉換的，更多資訊可以參閱 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;mariadb.com&#x2F;kb&#x2F;en&#x2F;documentation&#x2F;&quot;&gt;MariaDB 文件&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;這篇紀錄一下本人安裝 MariaDB 的筆記，大體上和 &lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2023&#x2F;mysql&#x2F;&quot;&gt;MySQL&lt;&#x2F;a&gt; 差不多。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ban-ben&quot;&gt;版本&lt;&#x2F;h2&gt;
&lt;p&gt;MariaDB 也有分為短期支援與長期支援版，目前的版本可以參考 〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;mariadb.org&#x2F;about&#x2F;#maintenance-policy&quot;&gt;MariaDB general release maintenance periods&lt;&#x2F;a&gt;〉。&lt;&#x2F;p&gt;
&lt;p&gt;這邊選用 10.6，到 2026 都不用擔心升級問題。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;an-zhuang&quot;&gt;安裝&lt;&#x2F;h2&gt;
&lt;p&gt;參閱 〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;mariadb.com&#x2F;kb&#x2F;en&#x2F;installing-mariadb-deb-files&#x2F;&quot;&gt;Installing MariaDB .deb Files&lt;&#x2F;a&gt;〉。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ubuntu-22-04&quot;&gt;Ubuntu 22.04&lt;&#x2F;h3&gt;
&lt;p&gt;安裝 Ubuntu 22.04 自帶 APT 庫的 &lt;code&gt;mariadb-server&lt;&#x2F;code&gt;，版次正是 10.6，此套件會一併安裝下列套件：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;galera-4&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;maraidb-client-10.6&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;mariadb-client-core-10.6&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;mariadb-common&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;mariadb-server-10.6&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;mariadb-server-core-10.6&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;mysql-common&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;socat&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;其中 Galera 是 MariaDB 的叢集系統，&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;linux.die.net&#x2F;man&#x2F;1&#x2F;socat&quot;&gt;socat&lt;&#x2F;a&gt; 則是 Galera 的依賴套件。&lt;&#x2F;p&gt;
&lt;p&gt;雖然安裝 &lt;code&gt;mariadb-server&lt;&#x2F;code&gt; 會一併安裝 Galera，但並不會自動啟用叢集，本文也不會提到叢集。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ubuntu-20-04&quot;&gt;Ubuntu 20.04&lt;&#x2F;h3&gt;
&lt;p&gt;參閱 〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;mariadb.com&#x2F;kb&#x2F;en&#x2F;mariadb-package-repository-setup-and-usage&#x2F;&quot;&gt;MariaDB Package Repository Setup and Usage&lt;&#x2F;a&gt;〉。&lt;&#x2F;p&gt;
&lt;p&gt;如果是 Ubuntu 20.04，自帶的 mariadb-server 是 10.3，可以外加 MariaDB Package Repositoty 加上 MariaDB 10.6，只要一行指令：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; curl -LsS https:&#x2F;&#x2F;r.mariadb.com&#x2F;downloads&#x2F;mariadb_repo_setup &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;|&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; sudo&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; bash&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -s -- --mariadb-server-version=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;mariadb-10.6&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;加上 APT 庫後，後續作法同上。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;an-zhuang-hou-pei-zhi&quot;&gt;安裝後配置&lt;&#x2F;h2&gt;
&lt;p&gt;安裝後服務會自動跑起來，也可以用 root 免密碼直接登入。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;mariadb.com&#x2F;kb&#x2F;en&#x2F;mysql_secure_installation&#x2F;&quot;&gt;MariaDB 建議不需要跑 &lt;code&gt;mysql_secure_installation&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;MariaDB 主配置文件在 &#x2F;etc&#x2F;mysql&#x2F;mariadb.cnf，裡面部分內容：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;ini&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# The MariaDB&#x2F;MySQL tools read configuration files in the following order:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# 0. &amp;quot;&#x2F;etc&#x2F;mysql&#x2F;my.cnf&amp;quot; symlinks to this file, reason why all the rest is read.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# 1. &amp;quot;&#x2F;etc&#x2F;mysql&#x2F;mariadb.cnf&amp;quot; (this file) to set global defaults,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# 2. &amp;quot;&#x2F;etc&#x2F;mysql&#x2F;conf.d&#x2F;*.cnf&amp;quot; to set global options.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# 3. &amp;quot;&#x2F;etc&#x2F;mysql&#x2F;mariadb.conf.d&#x2F;*.cnf&amp;quot; to set MariaDB-only options.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# 4. &amp;quot;~&#x2F;.my.cnf&amp;quot; to set user-specific options.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# If the same option is defined multiple times, the last one will apply.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;實際上主要的配置都在 &#x2F;etc&#x2F;mysql&#x2F;mariadb.conf.d&#x2F; 內，如果是 server 方面的配置就改 50-server.cnf，如果是 client 就改 50-client.cnf。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;utf-8-wen-ti&quot;&gt;UTF-8 問題&lt;&#x2F;h3&gt;
&lt;p&gt;參閱 〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;mariadb.com&#x2F;kb&#x2F;en&#x2F;setting-character-sets-and-collations&#x2F;#example-changing-the-default-character-set-to-utf-8&quot;&gt;Setting Character Sets and Collations&lt;&#x2F;a&gt;〉。&lt;&#x2F;p&gt;
&lt;p&gt;開 50-client.cnf，確認 [client] 區段有以下設定：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;ini&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;[client]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;default-character-set&lt;&#x2F;span&gt;&lt;span&gt; = utf8mb4&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;開 50-server.cnf，確認 [mysqld] 區段有以下設定：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;ini&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;[mysqld]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# MySQL&#x2F;MariaDB default is Latin1, but in Debian we rather default to the full&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# utf8 4-byte character set. See also client.cnf&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;character-set-server&lt;&#x2F;span&gt;&lt;span&gt;  = utf8mb4&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;collation-server&lt;&#x2F;span&gt;&lt;span&gt;      = utf8mb4_general_ci&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果有改動，改完之後，重啟服務：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; sudo systemctl restart mariadb&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;進去 MySQL 看一下現在的編碼：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;SHOW VARIABLES &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;LIKE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;character%&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;SHOW VARIABLES &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;LIKE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;collation%&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;結果如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;+--------------------------+----------------------------+&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;| Variable_name            | Value                      |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;+--------------------------+----------------------------+&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;| character_set_client     | utf8mb4                    |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;| character_set_connection | utf8mb4                    |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;| character_set_database   | utf8mb4                    |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;| character_set_filesystem | binary                     |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;| character_set_results    | utf8mb4                    |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;| character_set_server     | utf8mb4                    |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;| character_set_system     | utf8mb3                    |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;| character_sets_dir       | &#x2F;usr&#x2F;share&#x2F;mysql&#x2F;charsets&#x2F; |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;+--------------------------+----------------------------+&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;8 rows in set (0.001 sec)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;+----------------------+--------------------+&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;| Variable_name        | Value              |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;+----------------------+--------------------+&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;| collation_connection | utf8mb4_general_ci |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;| collation_database   | utf8mb4_general_ci |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;| collation_server     | utf8mb4_general_ci |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;+----------------------+--------------------+&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;3 rows in set (0.001 sec)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到 MariaDB 預設就是 &lt;code&gt;uft8mb4&lt;&#x2F;code&gt; 了，貼心，不會有部份文字漏字的問題。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zhang-hao-guan-li&quot;&gt;&lt;strong&gt;帳號管理&lt;&#x2F;strong&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;裝完預設帳號是 root@localhost，使用 unix socket 認證，簡單說就是用 Linux 認證，只要我能化身為 Linux root，我也就能用 root@localhost 登入 MariaDB。&lt;&#x2F;p&gt;
&lt;p&gt;但對於連接 MariaDB 的應用，就有需要另外開一組帳號了，並且改用 mysql_native_password 認證。&lt;&#x2F;p&gt;
&lt;p&gt;注意，MariaDB 與 MySQL 在此處的手法略有不同。&lt;&#x2F;p&gt;
&lt;p&gt;先用 root 登入 MySQL，開新帳號：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SET&lt;&#x2F;span&gt;&lt;span&gt; old_passwords&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;CREATE USER&lt;&#x2F;span&gt;&lt;span&gt; &amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;ap1&lt;&#x2F;span&gt;&lt;span&gt;&amp;#39;@&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;192.168.0.3&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt; IDENTIFIED &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;BY&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;ap-password&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;則帳號 ap1 就開好啦，而且它只能從 192.168.0.3 連入，否則會被拒絕。&lt;&#x2F;p&gt;
&lt;p&gt;如果想要自由連入，用百分比符號表示：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SET&lt;&#x2F;span&gt;&lt;span&gt; old_passwords&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;CREATE USER&lt;&#x2F;span&gt;&lt;span&gt; &amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;ap&lt;&#x2F;span&gt;&lt;span&gt;&amp;#39;@&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;%&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt; IDENTIFIED &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;BY&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;ap-password&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;看有沒有建成功：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SELECT&lt;&#x2F;span&gt;&lt;span&gt; Host, User &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; mysql&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;global_priv&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;剛建的新帳號已經能登入了，但還沒有權限，可以賦予它們某些資料庫的權限，以 ap 為例：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;GRANT&lt;&#x2F;span&gt;&lt;span&gt; ALL &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ON *&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;* TO&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;ap&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;@&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;%&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面的 &lt;code&gt;*.*&lt;&#x2F;code&gt; 表示所有的 database 與所有的 table，可視需要自行縮減。&lt;&#x2F;p&gt;
&lt;p&gt;最後一樣記得測一下。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>冬日的 MySQL 備份提案</title>
        <published>2023-06-08T00:00:00+00:00</published>
        <updated>2023-06-08T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2023/mysql-backup/"/>
        <id>https://editor.leonh.space/2023/mysql-backup/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2023/mysql-backup/">&lt;p&gt;前面寫了篇 &lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2023&#x2F;mysql-replication&#x2F;&quot;&gt;MySQL 複寫（熱備份）&lt;&#x2F;a&gt;，這廂就要來篇備份（冷備份）。&lt;&#x2F;p&gt;
&lt;p&gt;MySQL 備份的方法很多，有專門的備份服務，例如 SqlBak、Backup Ninja，也有一些外部或內建的備份工具如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;mysql&lt;strong&gt;d&lt;&#x2F;strong&gt;ump&lt;&#x2F;li&gt;
&lt;li&gt;mysql&lt;strong&gt;p&lt;&#x2F;strong&gt;ump&lt;&#x2F;li&gt;
&lt;li&gt;XtraBackup&lt;&#x2F;li&gt;
&lt;li&gt;MyDumper&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;這些工具都各有支持者，而本篇採用的是本家的 mysqlpump，就本人目前的備份需求來說，足矣。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;mysqlpump&quot;&gt;mysqlpump&lt;&#x2F;h2&gt;
&lt;p&gt;它是個隨附於 MySQL 的命令列工具，裝完 MySQL 應該就有了。&lt;&#x2F;p&gt;
&lt;p&gt;只要對 MySQL 有基本的認識，使用 mysqldump 應該沒什麼障礙，下面是我的基本使用參數：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &#x2F;usr&#x2F;bin&#x2F;mysqlpump&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--user=root&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--password=myrootpassword&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--compress&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--add-drop-database&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--add-drop-table&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--single-transaction&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--watch-progress&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--exclude-databases=mysql&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--result-file=&#x2F;data&#x2F;backups&#x2F;mysql_2022-10-13.sql&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;大部分的參數應該都可以望文生義，就不多解釋。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;yi-ri-qi-ming-ming-bei-fen-dang&quot;&gt;以日期命名備份檔&lt;&#x2F;h3&gt;
&lt;p&gt;上面這個命令的問題是，它的備份檔日期是死的，想要用當天日期命名的話可以改成這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &#x2F;usr&#x2F;bin&#x2F;mysqlpump&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--user=root&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--password=myrootpassword&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--compress&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--add-drop-database&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--add-drop-table&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--single-transaction&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--watch-progress&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--exclude-databases=mysql&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--result-file=&#x2F;data&#x2F;backups&#x2F;mysql_&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;$(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;date&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --iso-8601=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;date&amp;#39;)&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;.sql&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這裡用到 shell 腳本的變數（&lt;code&gt;$&lt;&#x2F;code&gt;）以及 &lt;code&gt;date&lt;&#x2F;code&gt; 來組合出當天的日期當作備份檔名。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;yin-cang-mi-ma&quot;&gt;隱藏密碼&lt;&#x2F;h3&gt;
&lt;p&gt;還有另外一個問題是，密碼是明碼，把這些寫成腳本令人感到不放心。&lt;&#x2F;p&gt;
&lt;p&gt;這個問題 MySQL 也有應對方案，它自帶一組密碼加密工具。&lt;&#x2F;p&gt;
&lt;p&gt;在開始前，先到 MySQL 建立一組專門的備份帳號，就叫它 bkpuser 吧！&lt;&#x2F;p&gt;
&lt;p&gt;用 MySQL 附的密碼加密工具生成加密的帳密檔：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; mysql_config_editor set&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --login-path=backup --user=bkpuser --password&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中參數 &lt;code&gt;login-path&lt;&#x2F;code&gt; 只是該組帳密對的識別名稱而已，沒有功能上的意義。&lt;&#x2F;p&gt;
&lt;p&gt;執行之後，需要輸入 bkpuser 的密碼。&lt;&#x2F;p&gt;
&lt;p&gt;再確認一下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;$ mysql_config_editor print --all&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;應該會輸出以下結果：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;ini&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;[backup]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;user&lt;&#x2F;span&gt;&lt;span&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;bkpuser&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;password&lt;&#x2F;span&gt;&lt;span&gt; = *****&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中的區塊名 &lt;code&gt;backup&lt;&#x2F;code&gt; 來自前面執行時的 &lt;code&gt;login-path&lt;&#x2F;code&gt; 參數值。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;mysql_config_editor&lt;&#x2F;code&gt; 這支程式會生成一個加密過的帳密檔案 ~&#x2F;.mylogin.cnf，看一下真的是亂碼：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; cat ~&#x2F;.myloing.cnf&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;因為帳密對有識別名稱，所以可以視需要添加其它帳密對，MySQL 的命令列工具幾乎都支援以 &lt;code&gt;login-path&lt;&#x2F;code&gt; 的方式做身份認證。&lt;&#x2F;p&gt;
&lt;p&gt;最後回到備份話題，設好之後就可以用 &lt;code&gt;login-path&lt;&#x2F;code&gt; 替代明碼的密碼啦：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &#x2F;usr&#x2F;bin&#x2F;mysqlpump&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--login-path=backup&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--compress&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--add-drop-database&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--add-drop-table&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--single-transaction&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--watch-progress&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--exclude-databases=mysql&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--result-file=&#x2F;data&#x2F;backups&#x2F;mysql_&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;$(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;date&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --iso-8601=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;date&amp;#39;)&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;.sql&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;日期也自動產生了，密碼也隱藏了，接著就是讓它每天自動跑啦！&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ding-qi-bei-fen&quot;&gt;定期備份&lt;&#x2F;h2&gt;
&lt;p&gt;把上面的命令變成腳本：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;#!&#x2F;usr&#x2F;bin&#x2F;bash&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;&#x2F;usr&#x2F;bin&#x2F;mysqlpump&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--login-path=backup&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--compress&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--add-drop-database&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--add-drop-table&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--single-transaction&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--watch-progress&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--exclude-databases=mysql&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--result-file=&#x2F;data&#x2F;backups&#x2F;mysql_&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;$(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;date&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --iso-8601=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;date&amp;#39;)&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;.sql&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;把腳本文存為 &#x2F;usr&#x2F;local&#x2F;bin&#x2F;backup-mysql.sh，記得加上可執行權限。&lt;&#x2F;p&gt;
&lt;p&gt;另外那放備份檔的 &#x2F;data&#x2F;backups&#x2F; 的寫入權限也要先開好。&lt;&#x2F;p&gt;
&lt;p&gt;設定 cron 任務：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; crontab&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -e&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;此處我們用特定帳號的 cron，沒有為什麼。&lt;&#x2F;p&gt;
&lt;p&gt;添加 crontab 如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;0  1 * * * &#x2F;usr&#x2F;local&#x2F;bin&#x2F;backup-mysql.sh&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;10 1 * * * find &#x2F;data&#x2F;backup&#x2F; -mtime +30 -name &amp;#39;mysql_*.sql&amp;#39; -delete;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;第一行表示每日凌晨一點跑備份腳本。&lt;&#x2F;p&gt;
&lt;p&gt;第二行是每日凌晨 1:10 刪除三十天以上的舊備份，主要是用 find 指令的比對和刪除功能。簡單搞定，沒有用到什麼複雜的 retention &#x2F; rotation 工具。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;huan-yuan&quot;&gt;還原&lt;&#x2F;h2&gt;
&lt;p&gt;很多人做到備份就停了，但建議還是實際演練一下還原作業，真的碰到時比較不會手忙腳亂。&lt;&#x2F;p&gt;
&lt;p&gt;還原當下典型的情境應該是一台空的 MySQL，回頭看我們的還原腳本有這幾個參數：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;--add-drop-database&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;--add-drop-table&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;意思是備份檔的 SQL 內有加入丟掉資料庫、丟掉資料表等指令，所以空的 MySQL 也好，已經有東西的 MySQL 也好，只要有與備份檔同名的資料庫都會被先被拋棄再重建。&lt;&#x2F;p&gt;
&lt;p&gt;還原的指令相當簡單，在 shell 環境執行：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; mysql&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -uroot -p&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; mysql_2022-10-31.sql&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如此即可還原。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>有點難又不會太難的 MySQL 複寫配置</title>
        <published>2023-06-07T00:00:00+00:00</published>
        <updated>2023-06-07T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2023/mysql-replication/"/>
        <id>https://editor.leonh.space/2023/mysql-replication/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2023/mysql-replication/">&lt;p&gt;複寫是資料庫的一種備份機制，簡單的說就是同步兩台（或Ｎ台）資料庫，當然除了備份之外，如果是雙向複寫的話也是一種高可用架構。&lt;&#x2F;p&gt;
&lt;p&gt;本篇介紹的是最單純的單向複寫，單向複寫除了可以視為熱備份之外，在應用層也可以藉此做到讀寫分離，例如報表服務，因為只有讀取，又可能有很複雜的查詢，就很適合把報表服務指向副資料庫，而不會把全部的查詢都重壓在主資料庫身上。&lt;&#x2F;p&gt;
&lt;p&gt;另外一種只有大量讀取而沒有寫入的作業就是 dump 啦，雖然有複寫就有了熱備份，但還是需要定期 dump 個冷備份出來，dump 也很適合在副資料庫上操作，同樣避免了主資料庫資源被吃光的問題。&lt;&#x2F;p&gt;
&lt;p&gt;既然是同步，那就可以進一步衍申主副資料庫的量子狀態，它們之間的確具有所謂的量子糾纏，示意圖如下：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2023&#x2F;mysql-replication&#x2F;abc831339cefbe66591e14f5a2dcb1a3.png&quot; alt=&quot;量子糾纏&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：反正我很閒&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;mysql-fu-xie-zhi-qu-ming-de-yi-shu&quot;&gt;MySQL 複寫之取名的藝術&lt;&#x2F;h2&gt;
&lt;p&gt;原本複寫的主資料庫、副資料庫的英文叫 master &#x2F; slave，但因為政治正確的關係改叫 main &#x2F; replica 或 source &#x2F; replica，但有些參數還是沿用舊名，總之：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;master = main = source&lt;&#x2F;li&gt;
&lt;li&gt;slave = replica&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;下文會混用，請自行腦補。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;qian-zhi-zuo-ye&quot;&gt;前置作業&lt;&#x2F;h2&gt;
&lt;p&gt;既然 master &#x2F; slave 是兩個機台，那它們就必須接受外部連線，預設剛裝完的 MySQL 是不接受外部連線的，請到  &#x2F;etc&#x2F;mysql&#x2F;mysql.conf.d&#x2F;mysqld.conf，找到以下區塊：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;ini&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# Instead of skip-networking the default is now to listen only on&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# localhost which is more compatible and is not less secure.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;bind-address&lt;&#x2F;span&gt;&lt;span&gt;            = 127.0.0.1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;mysqlx-bind-address&lt;&#x2F;span&gt;&lt;span&gt;     = 127.0.0.1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中 &lt;code&gt;bind-address&lt;&#x2F;code&gt; 表示只接受 127.0.0.1 的連線，也就是本機，請改為 0.0.0.0 表示接受任何位址的連線，或者設為某個 IP 只接受它的連線也可以。&lt;&#x2F;p&gt;
&lt;p&gt;後面的 &lt;code&gt;mysqlx-bind-address&lt;&#x2F;code&gt; 可以無視，它為 MySQL X plugin 連線參數，一般用不到。&lt;&#x2F;p&gt;
&lt;p&gt;其他前置作業，包括安裝、初始化配置、建帳號、設定 UTF-8 等，請見另一篇〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2023&#x2F;mysql&#x2F;&quot;&gt;安裝 MySQL 與設定真・UTF-8 編碼&lt;&#x2F;a&gt;〉，特別是 UTF-8，務必在做複寫前設好。&lt;&#x2F;p&gt;
&lt;p&gt;前置作業好了後，開始分別設定 master 與 slave。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;master&quot;&gt;Master&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;she-ding-pei-zhi&quot;&gt;設定配置&lt;&#x2F;h3&gt;
&lt;p&gt;編輯 &#x2F;etc&#x2F;mysql&#x2F;mysql.conf.d&#x2F;mysqld.conf，把一些項目改為如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;ini&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# The following can be used as easy to replay backup logs or for replication.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# note: if you are setting up a replication slave, see README.Debian about&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;#       other settings you may need to change.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;server-id&lt;&#x2F;span&gt;&lt;span&gt;                  = 1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;log_bin&lt;&#x2F;span&gt;&lt;span&gt;                    = &#x2F;var&#x2F;log&#x2F;mysql&#x2F;mysql-bin.log&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;binlog_expire_logs_seconds&lt;&#x2F;span&gt;&lt;span&gt; = 2592000&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;max_binlog_size&lt;&#x2F;span&gt;&lt;span&gt;            = 100M&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# binlog_do_db              = include_database_name&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;binlog_ignore_db&lt;&#x2F;span&gt;&lt;span&gt;           = information_schema&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;binlog_ignore_db&lt;&#x2F;span&gt;&lt;span&gt;           = mysql&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;binlog_ignore_db&lt;&#x2F;span&gt;&lt;span&gt;           = performance_schema&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;binlog_ignore_db&lt;&#x2F;span&gt;&lt;span&gt;           = sys&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;必填的設定：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;server-id&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;
每台 MySQL 的唯一碼，不論 master 或 slave 皆不可重複，此處設為 &lt;code&gt;1&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;log_bin&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;
Master 的 binlog 路徑。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;這裡的 binlog &#x2F; logbin 是 MySQL 的交易（transaction）紀錄，複寫機制就是利用傳輸 binlog 實現的，至於具體的結構格式、交握機制，對於只想順順用複寫、無心鑽研的你我來說，不是重點。&lt;&#x2F;p&gt;
&lt;p&gt;下面幾項依需求設定：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;binlog_expire_logs_seconds&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;
Binlog 檔案保留的秒數，超過即刪除，避免塞爆硬碟，預設值為 2,592,000 秒，相當於 30 天。（並非真的逐秒檢查，而是有資料異動時檢查）&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;max_binlog_size&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;
每個 binlog 檔案的大小，超過就會產生下一個 binlog 檔案，預設值 100 MB。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;binlog_do_db&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;
要複寫的資料庫，如果有多個就建立多筆 &lt;code&gt;binlog_do_db&lt;&#x2F;code&gt; 的設定。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;binlog_ignore_db&lt;&#x2F;code&gt;
不要複寫的資料庫，涵義與上面相反，在此我們忽略 MySQL 內建的四個系統資料庫。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;可以看到我這邊是以黑名單原則設定，把四個系統資料庫設為黑名單，其餘都複寫。（此處的黑白與人種膚色無關，黃種人的命也是命。）&lt;&#x2F;p&gt;
&lt;p&gt;設完之後，照慣例，重啟 MySQL：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo systemctl restart mysql&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;看一下狀態：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; systemctl status mysql&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果是綠色的就沒問題囉！&lt;&#x2F;p&gt;
&lt;h3 id=&quot;kai-li-slave-zhang-hao&quot;&gt;開立 slave 帳號&lt;&#x2F;h3&gt;
&lt;p&gt;進 MySQL。&lt;&#x2F;p&gt;
&lt;p&gt;建帳號：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;CREATE USER&lt;&#x2F;span&gt;&lt;span&gt; &amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;replica&lt;&#x2F;span&gt;&lt;span&gt;&amp;#39;@&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;%&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt; IDENTIFIED &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WITH&lt;&#x2F;span&gt;&lt;span&gt; mysql_native_password &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;BY&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;replica-password&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;開權限：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;GRANT&lt;&#x2F;span&gt;&lt;span&gt; REPLICATION SLAVE, REPLICATION CLIENT &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ON *&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;* TO&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;replica&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;@&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;%&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;FLUSH PRIVILEGES;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這裡給兩個權限：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;REPLICATION SLAVE&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;
讓 slave 可以讀到 master 的 binlog。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;REPLICATION CLIENT&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;
讓 slave 有執行 &lt;code&gt;SHOW MASTER STATUS&lt;&#x2F;code&gt;、&lt;code&gt;SHOW REPLICA STATUS&lt;&#x2F;code&gt; 和 &lt;code&gt;SHOW BINARY LOGS&lt;&#x2F;code&gt;這三個命令的權限，它們用於獲得 replication 機制當前運作狀態。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;que-ren-zhuang-tai&quot;&gt;確認狀態&lt;&#x2F;h3&gt;
&lt;p&gt;確認一下當前 master 狀態：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;SHOW &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;MASTER STATUS&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;查詢結果：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;+------------------+----------+--------------+-------------------------------------------------+-------------------+&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;| File             | Position | Binlog_Do_DB | Binlog_Ignore_DB                                | Executed_Gtid_Set |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;+------------------+----------+--------------+-------------------------------------------------+-------------------+&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;| mysql-bin.000001 | 882      |              | information_schema,mysql,performance_schema,sys |                   |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;+------------------+----------+--------------+-------------------------------------------------+-------------------+&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到，當前 binlog 檔案為 mysql-bin.000001、position 為 882。&lt;&#x2F;p&gt;
&lt;p&gt;前面我們把 binlog 路徑設在 &#x2F;var&#x2F;log&#x2F;mysql&#x2F;，實際去看確實也會有這個檔案。&lt;&#x2F;p&gt;
&lt;p&gt;Master 的部份至此告一段落。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;slave&quot;&gt;Slave&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;she-ding-pei-zhi-1&quot;&gt;設定配置&lt;&#x2F;h3&gt;
&lt;p&gt;一樣先從 &#x2F;etc&#x2F;mysql&#x2F;mysql.conf.d&#x2F;mysqld.conf 開始，把一些項目改為如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;ini&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# If MySQL is running as a replication slave, this should be&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# changed. Ref https:&#x2F;&#x2F;dev.mysql.com&#x2F;doc&#x2F;refman&#x2F;8.0&#x2F;en&#x2F;server-system-variables.html#sysvar_tmpdir&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# tmpdir             = &#x2F;tmp&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;replica_load_tmpdir&lt;&#x2F;span&gt;&lt;span&gt; = &#x2F;var&#x2F;tmp&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# The following can be used as easy to replay backup logs or for replication.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# note: if you are setting up a replication slave, see README.Debian about&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;#       other settings you may need to change.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;server-id&lt;&#x2F;span&gt;&lt;span&gt; = 10&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# log_bin  = &#x2F;var&#x2F;log&#x2F;mysql&#x2F;mysql-bin.log&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# binlog_expire_logs_seconds    = 2592000&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;max_binlog_size&lt;&#x2F;span&gt;&lt;span&gt;   = 100M&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# binlog_do_db     = include_database_name&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# binlog_ignore_db = include_database_name&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;read_only&lt;&#x2F;span&gt;&lt;span&gt; = 1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;relay_log&lt;&#x2F;span&gt;&lt;span&gt; = XXXXX-relay-bin&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;必填的項目如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;server-id&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;
每台 MySQL 的唯一碼，不論 master 或 slave 皆不可重複，此處設為 &lt;code&gt;10&lt;&#x2F;code&gt;，表示１號 master 的０號 slave。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;read_only&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;
Slave 除了複寫外，不開放其他寫入。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;relay_log&lt;&#x2F;code&gt;
Slave 的 relay log 的名稱，該值前面的 &lt;code&gt;XXXXX&lt;&#x2F;code&gt; 為此台主機名稱（hostname），例如這台 hostname 為 &lt;code&gt;awesome-db-replica&lt;&#x2F;code&gt;，那此處依照慣例會填入 &lt;code&gt;awesome-db-replica-relay-bin&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;這個 relay log 是 slave 在複寫機制下必須要有的，它是 binlog 在 slave 這邊的紀錄，用於讓 slave 知道當前複寫的狀態，具體細節在此不深究，它的真身在 &#x2F;var&#x2F;lib&#x2F;mysql&#x2F;。&lt;&#x2F;p&gt;
&lt;p&gt;選填的項目：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;replica_load_tmpdir&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;
複寫的暫存目錄，根據註解內連結的建議，擺到不會被刪的位置較佳。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;改完後一樣重啟服務：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo systemctl restart mysql&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;看一下狀態：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; systemctl status mysql&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果是綠色的就沒問題囉！&lt;&#x2F;p&gt;
&lt;h3 id=&quot;qi-dong-fu-xie&quot;&gt;啟動複寫&lt;&#x2F;h3&gt;
&lt;p&gt;登入 slave MySQL，把前面建的帳密、binlog 資訊逐一填入：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;STOP REPLICA&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;CHANGE REPLICATION SOURCE &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;TO&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  SOURCE_HOST &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;200.0.0.228&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  SOURCE_USER &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;replica&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  SOURCE_PASSWORD &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;replica-password&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;START REPLICA&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;確認狀態：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;SHOW &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;REPLICA STATUS&lt;&#x2F;span&gt;&lt;span&gt;\G;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;會有很多資訊：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;***************************[ 1. row ]***************************&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Slave_IO_State                | Waiting for source to send event&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Master_Host                   | 200.0.0.228&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Master_User                   | replica&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Master_Port                   | 3306&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Connect_Retry                 | 60&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Master_Log_File               | mysql-bin.000003&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Read_Master_Log_Pos           | 609&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Relay_Log_File                | wms-db-replica-relay-bin.000004&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Relay_Log_Pos                 | 825&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Relay_Master_Log_File         | mysql-bin.000003&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Slave_IO_Running              | Yes&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Slave_SQL_Running             | Yes&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Replicate_Do_DB               | &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Replicate_Ignore_DB           | &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Replicate_Do_Table            | &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Replicate_Ignore_Table        | &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Replicate_Wild_Do_Table       | &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Replicate_Wild_Ignore_Table   | &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Last_Errno                    | 0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Last_Error                    | &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Skip_Counter                  | 0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Exec_Master_Log_Pos           | 609&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Relay_Log_Space               | 1742&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Until_Condition               | None&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Until_Log_File                | &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Until_Log_Pos                 | 0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Master_SSL_Allowed            | No&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Master_SSL_CA_File            | &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Master_SSL_CA_Path            | &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Master_SSL_Cert               | &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Master_SSL_Cipher             | &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Master_SSL_Key                | &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Seconds_Behind_Master         | 0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Master_SSL_Verify_Server_Cert | No&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Last_IO_Errno                 | 0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Last_IO_Error                 | &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Last_SQL_Errno                | 0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Last_SQL_Error                | &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Replicate_Ignore_Server_Ids   | &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Master_Server_Id              | 1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Master_UUID                   | 3d63c6ef-492f-11ed-802a-00155d731308&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Master_Info_File              | mysql.slave_master_info&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;SQL_Delay                     | 0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;SQL_Remaining_Delay           | &amp;lt;null&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Slave_SQL_Running_State       | Replica has read all relay log; waiting for more updates&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Master_Retry_Count            | 86400&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Master_Bind                   | &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Last_IO_Error_Timestamp       | &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Last_SQL_Error_Timestamp      | &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Master_SSL_Crl                | &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Master_SSL_Crlpath            | &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Retrieved_Gtid_Set            | &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Executed_Gtid_Set             | &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Auto_Position                 | 0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Replicate_Rewrite_DB          | &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Channel_Name                  | &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Master_TLS_Version            | &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Master_public_key_path        | &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Get_master_public_key         | 0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Network_Namespace             |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;資訊量很大，主要看下面幾項：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;Slave_IO_Running&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;
沒問題應該要是 &lt;code&gt;Yes&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;Slave_SQL_Running&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;
沒問題應該要是 &lt;code&gt;Yes&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;Last_IO_Error&lt;&#x2F;code&gt;&lt;br &#x2F;&gt;
如果有問題會在此顯示問題資訊。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;jian-ce-fu-xie-ji-zhi&quot;&gt;檢測複寫機制&lt;&#x2F;h2&gt;
&lt;p&gt;前面已經看過 slave 的狀態，沒問題的話，回到 master 看狀態：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;SHOW REPLICAS;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;結果如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;+-----------+------+------+-----------+--------------------------------------+&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;| Server_id | Host | Port | Source_id | Replica_UUID                         |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;+-----------+------+------+-----------+--------------------------------------+&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;| 10        |      | 3306 | 1         | 6607513e-4941-11ed-b88c-00155d731309 |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;+-----------+------+------+-----------+--------------------------------------+&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這台 server id 為 10 的就是我們剛建的 slave 啦！&lt;&#x2F;p&gt;
&lt;p&gt;在 master 建個資料庫試一下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;CREATE DATABASE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; replication_test&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在 slave 看看有沒有複寫過來：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;SHOW DATABASES;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;有複寫成功就 OK 囉！&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>安裝 MySQL 與設定真・UTF-8 編碼</title>
        <published>2023-06-06T00:00:00+00:00</published>
        <updated>2023-06-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2023/mysql/"/>
        <id>https://editor.leonh.space/2023/mysql/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2023/mysql/">&lt;p&gt;MySQL 應該是當代最多人用的資料庫&lt;strong&gt;系統&lt;&#x2F;strong&gt;，為什麼不是 SQLite 呢？因為 SQLite 不是「系統」。&lt;&#x2F;p&gt;
&lt;p&gt;這篇是 MySQL 的安裝筆記，以及安裝後的一些設定，主要是初始設定以及 MySQL 著名的歷史問題：UTF-8 編碼問題。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;an-zhuang&quot;&gt;安裝&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;dev.mysql.com&#x2F;doc&#x2F;refman&#x2F;8.0&#x2F;en&#x2F;linux-installation-native.html&quot;&gt;MySQL 官方的安裝文件&lt;&#x2F;a&gt;就有提到多種安裝方式，最簡單的應該就是直接從 Ubuntu 內建的 APT 庫安裝了，只要一行：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; sudo apt-get install mysql-server mysql-client&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;它會安裝 MySQL、MySQL CLI 工具、設定好服務，只要一行。&lt;&#x2F;p&gt;
&lt;p&gt;安裝完可以確認一下服務的狀態：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; systemctl status mysql&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果亮綠燈就是沒問題。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;an-zhuang-hou-pei-zhi&quot;&gt;安裝後配置&lt;&#x2F;h3&gt;
&lt;p&gt;安裝後要做一些基礎配置，首先是設定 MySQL root 帳號的密碼。&lt;&#x2F;p&gt;
&lt;p&gt;用 root 帳號進 MySQL，幫 root 設個密碼：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; sudo mysql&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;進入 MySQL 後就可以改密碼囉！&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ALTER USER&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;root&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;@&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;localhost&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt; IDENTIFIED &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WITH&lt;&#x2F;span&gt;&lt;span&gt; mysql_native_password &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;by&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;root-password&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;改完要記得退出。&lt;&#x2F;p&gt;
&lt;p&gt;這個步驟是參考〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;dev.mysql.com&#x2F;doc&#x2F;refman&#x2F;8.0&#x2F;en&#x2F;default-privileges.html&quot;&gt;Securing the Initial MySQL Account&lt;&#x2F;a&gt;〉，看起來很長，其實重點就上面那行指令。&lt;&#x2F;p&gt;
&lt;p&gt;有密碼之後，就來跑其他基礎配置吧，這些工作可以用 MySQL 內附的 &lt;code&gt;mysql_secure_installation&lt;&#x2F;code&gt; 命令代勞。&lt;&#x2F;p&gt;
&lt;p&gt;把 &lt;code&gt;mysql_secure_installation&lt;&#x2F;code&gt; 執行起來：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; mysql_secure_installation&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;執行後會有一系列的問答，它會根據我們的答案為我們做出相對的配置，請根據自身的實際需求回答即可。&lt;&#x2F;p&gt;
&lt;p&gt;完整的 &lt;code&gt;Securing the Initial MySQL Account&lt;&#x2F;code&gt; 文件為〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;dev.mysql.com&#x2F;doc&#x2F;refman&#x2F;8.0&#x2F;en&#x2F;mysql-secure-installation.html&quot;&gt;mysql_secure_installation — Improve MySQL Installation Security&lt;&#x2F;a&gt;〉&lt;&#x2F;p&gt;
&lt;p&gt;這裡我的系統是 Ubuntu + MySQL，如果是 CentOS + MariaDB 的話可以另行參考早期這篇〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2019&#x2F;centos-7-mariadb&#x2F;&quot;&gt;CentOS 裝 MariaDB 10&lt;&#x2F;a&gt;〉&lt;&#x2F;p&gt;
&lt;h2 id=&quot;mysql-de-utf-8-wen-ti&quot;&gt;MySQL 的 UTF-8 問題&lt;&#x2F;h2&gt;
&lt;p&gt;不論是 MySQL 還是 MariaDB，這兩兄弟都有遺傳自 MySQL 的老問題，&lt;code&gt;utf8&lt;&#x2F;code&gt; 或 &lt;code&gt;utf8mb3&lt;&#x2F;code&gt; 是假的 UTF-8，&lt;code&gt;utf8mb4&lt;&#x2F;code&gt; 才是真的 UTF-8。&lt;&#x2F;p&gt;
&lt;p&gt;這裡所謂的「真」、「假」只是騙人進來的農場標題，總之如果沒有改成 &lt;code&gt;utf8mb4&lt;&#x2F;code&gt; 的話，可能會遇到缺字。&lt;&#x2F;p&gt;
&lt;p&gt;修改 MySQL 配置，在 &#x2F;etc&#x2F;mysql&#x2F;mysql.conf.d&#x2F;mysql.cnf，這個檔案應該是空白的，在此添加以下內容：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;ini&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;[mysql]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;default-character-set&lt;&#x2F;span&gt;&lt;span&gt; = utf8mb4&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;[client]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;default-character-set&lt;&#x2F;span&gt;&lt;span&gt; = utf8mb4&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;編輯 MySQL server 配置，在 &#x2F;etc&#x2F;mysql&#x2F;mysql.conf.d&#x2F;mysqld.cnf，預設內容很多，小心修改。&lt;&#x2F;p&gt;
&lt;p&gt;添加下面內容：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;ini&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;[mysqld]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;character-set-server&lt;&#x2F;span&gt;&lt;span&gt; = utf8mb4&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;character-set-client-handshake&lt;&#x2F;span&gt;&lt;span&gt; = FALSE&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上文的幾個檔案為 Ubuntu + MySQL 配置路徑，如果是 CentOS + MariaDB 的話請看下面：&lt;&#x2F;p&gt;
&lt;p&gt;改 &#x2F;etc&#x2F;opt&#x2F;rh&#x2F;rh-mariadb102&#x2F;my.cnf.d&#x2F;mariadb-server.cnf：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;ini&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;[client]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;default-character-set&lt;&#x2F;span&gt;&lt;span&gt; = utf8mb4&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;[mysql]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;default-character-set&lt;&#x2F;span&gt;&lt;span&gt; = utf8mb4&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;[mysqld]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;character-set-server&lt;&#x2F;span&gt;&lt;span&gt; = utf8mb4&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;character-set-client-handshake&lt;&#x2F;span&gt;&lt;span&gt; = FALSE&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;[mariadb]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;character_set_server&lt;&#x2F;span&gt;&lt;span&gt; = utf8mb4&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;character_set_client&lt;&#x2F;span&gt;&lt;span&gt; = utf8mb4&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;改完之後，重啟服務：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; sudo systemctl restart mysql&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;進去 MySQL 看一下現在的編碼：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;SHOW VARIABLES &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;LIKE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;char%&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;SHOW VARIABLES &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;LIKE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;collation%&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;會看到像這樣滿滿的 &lt;code&gt;utf8mb4&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;+--------------------------+---------+&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;| Variable_name            | Value   |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;+--------------------------+---------+&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;| character_set_client     | utf8mb4 |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;| character_set_connection | utf8mb4 |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;| character_set_database   | utf8mb4 |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;| character_set_filesystem | binary  |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;| character_set_results    | utf8mb4 |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;| character_set_server     | utf8mb4 |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;| character_set_system     | utf8    |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;| character_sets_dir       |         |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;+--------------------------+---------+&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;+----------------------+--------------------+&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;| Variable_name        | Value              |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;+----------------------+--------------------+&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;| collation_connection | utf8mb4_general_ci |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;| collation_database   | utf8mb4_general_ci |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;| collation_server     | utf8mb4_general_ci |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;+----------------------+--------------------+&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中 character_set_system 只能是 &lt;code&gt;utf8&lt;&#x2F;code&gt; 或 &lt;code&gt;utf8mb3&lt;&#x2F;code&gt;，不能改。&lt;&#x2F;p&gt;
&lt;p&gt;以上參考文章：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;simplekj52.blogspot.com&#x2F;2017&#x2F;09&#x2F;mariadb-utf8-utf8mb4.html&quot;&gt;Mariadb UTF8 utf8mb4&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;47566730&#x2F;force-mariadb-clients-to-use-utf8mb4&quot;&gt;Force MariaDB clients to use utf8mb4&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;jian-shi-database-table-column-de-bian-ma&quot;&gt;檢視 database、table、column 的編碼&lt;&#x2F;h3&gt;
&lt;p&gt;同場加映檢視 database、table、column 的編碼。&lt;&#x2F;p&gt;
&lt;p&gt;檢視 database 編碼：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SELECT&lt;&#x2F;span&gt;&lt;span&gt; default_character_set_name&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; information_schema&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;SCHEMATA&lt;&#x2F;span&gt;&lt;span&gt; &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; schema_name &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;schemaname&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;檢視 table 編碼：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SELECT&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; CCSA&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;character_set_name&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span&gt; information_schema.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;`TABLES`&lt;&#x2F;span&gt;&lt;span&gt; T, information_schema.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;`COLLATION_CHARACTER_SET_APPLICABILITY`&lt;&#x2F;span&gt;&lt;span&gt; CCSA&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; CCSA&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;collation_name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; T&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;table_collation&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    AND&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; T&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;table_schema&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;schemaname&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    AND&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; T&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;table_name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;tablename&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;檢視 column 編碼：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SELECT&lt;&#x2F;span&gt;&lt;span&gt; character_set_name&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span&gt; information_schema.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;`COLUMNS`&lt;&#x2F;span&gt;&lt;span&gt; &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WHERE&lt;&#x2F;span&gt;&lt;span&gt; table_schema &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;schemaname&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    AND&lt;&#x2F;span&gt;&lt;span&gt; table_name &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;tablename&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    AND&lt;&#x2F;span&gt;&lt;span&gt; column_name &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;columnname&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;以上來自〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;1049728&#x2F;how-do-i-see-what-character-set-a-mysql-database-table-column-is&quot;&gt;How do I see what character set a MySQL database &#x2F; table &#x2F; column is?&lt;&#x2F;a&gt;〉。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;kai-fang-wai-bu-lian-ru&quot;&gt;開放外部連入&lt;&#x2F;h2&gt;
&lt;p&gt;剛裝完的 MySQL 只接受本機連線，想要開放外部連入的話，一樣去編輯 mysqld.cnf，找到關鍵處：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# Instead of skip-networking the default is now to listen only on&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# localhost which is more compatible and is not less secure.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;bind-address            = 127.0.0.1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;mysqlx-bind-address     = 127.0.0.1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在目前的設定中，&lt;code&gt;bind-address&lt;&#x2F;code&gt; 表示僅接受來自 127.0.0.1（也就是本機）的連線，要開放外連可以改為 0.0.0.0。&lt;&#x2F;p&gt;
&lt;p&gt;下一個設定是給 MySQL X plugin 用的，可以無視。&lt;&#x2F;p&gt;
&lt;p&gt;改完設定記得重啟 MySQL 服務，重啟之後用別台機器連連看。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zhang-hao-guan-li&quot;&gt;帳號管理&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;she-ding-ren-zheng-ji-zhi&quot;&gt;設定認證機制&lt;&#x2F;h3&gt;
&lt;p&gt;由於安全的考量，MySQL 預設以 SHA 做某種認證加密，想要改回簡約的原生密碼的話，在 &#x2F;etc&#x2F;mysql&#x2F;mysql.conf.d&#x2F;mysqld.cnf 添加以下設定：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;ini&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# default_authentication_plugin = mysql_native_password # &amp;#39;default_authentication_plugin&amp;#39; is deprecated and will be removed in a future release. Please use authentication_policy instead.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;authentication_policy&lt;&#x2F;span&gt;&lt;span&gt; = mysql_native_password,,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這項設定的值其實有三個，以 &lt;code&gt;,&lt;&#x2F;code&gt; 逗號分隔，第一格是預設的密碼認證機制，第二格或第三格可以是第三方的認證外掛，這邊的例子只指定了第一格，也就是原生的密碼認證，並且沒有第二、三格，也就是沒有多重因素認證。&lt;&#x2F;p&gt;
&lt;p&gt;改完重啟服務就好囉！至於註解掉的文字看看就好，可以省略。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;kai-mysql-zhang-hao&quot;&gt;開 MySQL 帳號&lt;&#x2F;h3&gt;
&lt;p&gt;一般不會用 root 帳號做事，會依照目的另外開帳號。&lt;&#x2F;p&gt;
&lt;p&gt;先用 root 登入 MySQL，開新帳號：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;CREATE USER&lt;&#x2F;span&gt;&lt;span&gt; &amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;ap1&lt;&#x2F;span&gt;&lt;span&gt;&amp;#39;@&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;192.168.0.3&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt; IDENTIFIED &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WITH&lt;&#x2F;span&gt;&lt;span&gt; mysql_native_password &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;BY&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;ap-password&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;則帳號 ap1 就開好啦，而且它只能從 192.168.0.3 連入，否則會被拒絕。&lt;&#x2F;p&gt;
&lt;p&gt;如果想要自由連入，用百分比符號表示：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;CREATE USER&lt;&#x2F;span&gt;&lt;span&gt; &amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;ap2&lt;&#x2F;span&gt;&lt;span&gt;&amp;#39;@&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;%&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt; IDENTIFIED &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WITH&lt;&#x2F;span&gt;&lt;span&gt; mysql_native_password &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;BY&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;ap-password&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;看有沒有建成功：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SELECT&lt;&#x2F;span&gt;&lt;span&gt; Host, User &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; mysql&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;user&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;剛建的新帳號已經能登入了，但還沒有權限，可以賦予它們某些資料庫的權限，以 ap2 為例：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;GRANT&lt;&#x2F;span&gt;&lt;span&gt; ALL &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ON&lt;&#x2F;span&gt;&lt;span&gt; ap2db.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;* TO&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;ap2&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;@&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;%&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;最後一樣記得測一下。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;gai-mi-ma&quot;&gt;改密碼&lt;&#x2F;h3&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ALTER USER&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;ap1&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;@&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;192.168.0.3&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt; IDENTIFIED &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WITH&lt;&#x2F;span&gt;&lt;span&gt; mysql_native_password &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;BY&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;new-ap-password&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;wen-ti-pai-cha&quot;&gt;問題排查&lt;&#x2F;h2&gt;
&lt;p&gt;如果有各種疑難雜症，第一是先檢查配置文件，其次是看 log，在配置文件中有這麼一段：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;ini&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# Error log - should be very few entries.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;log_error&lt;&#x2F;span&gt;&lt;span&gt; = &#x2F;var&#x2F;log&#x2F;mysql&#x2F;error.log&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這就是錯誤 log 的路徑，問題都會記錄在這裡，再根據此處的描述去調查囉。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yi-xie-chang-yong-de-mysql-zhou-bian-gong-ju&quot;&gt;一些常用的 MySQL 周邊工具&lt;&#x2F;h2&gt;
&lt;p&gt;最後推薦兩個常搭配 MySQL &#x2F; MariaDB 使用的周邊工具&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2019&#x2F;centos-7-mycli&#x2F;&quot;&gt;mycli&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2019&#x2F;centos-7-adminer&#x2F;&quot;&gt;Adminer&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;其中，Adminer 是可以取代笨重的 phpMyAdmin 的聖品。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>OR-Tools 從小白到入門</title>
        <published>2023-06-01T00:00:00+00:00</published>
        <updated>2023-06-01T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2023/or-tools/"/>
        <id>https://editor.leonh.space/2023/or-tools/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2023/or-tools/">&lt;p&gt;前面介紹了 &lt;a href=&quot;#&quot;&gt;OptaPy 與 OptaPlanner 求解器（暫時下架整改）&lt;&#x2F;a&gt;，這篇談的則是另一款由 Google 出品的求解器 OR-Tools，為了文章的完整性，在進入 OR-Tools 前，還是快速介紹一下什麼是規劃問題，以及什麼是求解器。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gui-hua-wen-ti-yu-qiu-jie-qi&quot;&gt;規劃問題與求解器&lt;&#x2F;h2&gt;
&lt;p&gt;對一般用戶來說，他們可以在 LibreOffice Calc 等試算表中，建立一些公式及目標，然後使用求解器功能幫他們找出最接近目標的儲存格組合，例如「該怎麼找零才能不會讓顧客拿到一大堆硬幣？」這個問題在求解器的模型中，我們改以這種形式提問：「該怎麼組合各種幣值的鈔票與硬幣，好讓顧客拿到的貨幣數量是最少，並且金額是對的？」我們以下圖表達此問題：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2023&#x2F;or-tools&#x2F;3.png&quot; alt=&quot;LibreOffice Calc Solver&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;最後求出之解如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2023&#x2F;or-tools&#x2F;5.png&quot; alt=&quot;LibreOffice Calc Solver&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;最終顧客拿到 11 個各式貨幣，並且金額正確（詳見〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2023&#x2F;solver&#x2F;&quot;&gt;認識求解器&lt;&#x2F;a&gt;〉）。&lt;&#x2F;p&gt;
&lt;p&gt;像上面這樣的問題，屬於線性規劃問題，但還有一些更為複雜的問題，例如：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;排班、排程、排工問題，怎麼排才能公平又滿足多方需求？&lt;&#x2F;li&gt;
&lt;li&gt;路徑規劃問題，怎麼走才能讓運輸效率最大化？&lt;&#x2F;li&gt;
&lt;li&gt;裝箱問題，怎麼裝才能讓箱體利用率最大化？&lt;&#x2F;li&gt;
&lt;li&gt;裁切問題，怎麼切才能讓物料利用率最大化？&lt;&#x2F;li&gt;
&lt;li&gt;賽事問題，怎麼排才能讓賽事公平而不強碰？&lt;&#x2F;li&gt;
&lt;li&gt;投資問題，怎麼配置才能讓收益最大化？&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;這些問題的共同特徵是「運算複雜、驗證簡單」，運算之所以複雜，是因為其中有近似無限種可能，舉例來說，暴走二人組要走遍全台灣所有&lt;strong&gt;鄉鎮市區&lt;&#x2F;strong&gt;，其中路徑就有幾乎無限種可能，而驗證之所以簡單，有沒有真的走遍全台灣，用座標就可以秒驗證。&lt;&#x2F;p&gt;
&lt;p&gt;對於這類問題，我們不太可能去窮舉所有可能性再找出最佳解，因為如此所耗費的時間可能是天文數字，但可以透過求解器來幫我們找出「近似最佳解」，在理論上之「真．最佳解」無法求得的情況下，近似最佳解已足夠滿足我們的需求。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;or-tools&quot;&gt;OR-Tools&lt;&#x2F;h2&gt;
&lt;p&gt;OR-Tools 是 Google 開發的求解器，名字中的「OR」表示 operations research（作業研究），這是一門科學化探討各種作業問題的學科。OR-Tools 本身是以 C++ 開發，另提供多種語言的 API，由於 C++ 可直接編譯成各平台的原生二進位檔，因此不像 OptaPy 那樣還要等 JVM 跑起來，OR-Tools 即使在世界知名的慢速語言 Python 下跑起來都可以秒解問題，既提高了效率，也減少了客怨。&lt;&#x2F;p&gt;
&lt;p&gt;OR-Tools 內集合了多個求解器，每個求解器就是一套演算法，它們各有擅長之處，有的適合解線性問題，有的適合解約束問題，因為本人的需求為約束問題，所以下面只會談到 OR-Tools 中的約束問題求解器 CP-SAT。&lt;&#x2F;p&gt;
&lt;p&gt;在 Python 安裝 OR-Tools 就是那永遠的 &lt;code&gt;pip install&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; pip install ortools&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;p style=&quot;background-color: hsla(0, 0%, 96%, 1.0); padding: 1rem; text-align: center;&quot;&gt;
&lt;a href=&quot;https:&#x2F;&#x2F;vocus.cc&#x2F;article&#x2F;66160c9ffd89780001ecf985&quot;&gt;繼續閱讀&lt;&#x2F;a&gt;
&lt;&#x2F;p&gt;
&lt;!-- ## OR-Tools 的 CP-SAT

CP-SAT 是 OR-Tools 內建的求解器之一，「CP」表示 [constraint programming（約束編程）](https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E7%BA%A6%E6%9D%9F%E7%BC%96%E7%A8%8B)、「SAT」表示 [Boolean satisfiability problem（布林可滿足性問題）](布林可滿足性問題)，這些專業名詞有點艱澀，簡單說就是上面舉的那些規劃問題，而 CP-SAT 就是針對這類問題的求解器。

使用 OR-Tools 主要有幾個大步驟：

1. 創建變數
2. 制定約束
3. 制定目標
4. 執行求解

不論用的是哪款求解器，大多也都是這些步驟，包括 LibreOffice Calc 內的求解器以及 OptaPy、OptaPlanner，差別在於建立問題模型的思維模式不同，在 OR-Tools 這邊，我們需要把具體的問題以數學方程式的形式表達，雖說是數學方程式，但其實還是程式碼的形式，也就是說我們在建模時腦中的思維模式必須走過「理解問題 → 數學化 → 程式碼化」三個階段，這個過程不要求我們是數學專家，但需要有強健的邏輯思維，這也是邏輯孱弱的本人難以跨越之鴻溝所在。

無論如何，頭已經洗下去了，讓我們繼續吧。

## 簡單約束問題

一個簡單約束問題如下：

- 有 x、y、z 三個變數，它們的值可以是 0、1、2。
- x 不等於 y。
- x + y + z 最大可能是多少？

建立一個 cp_example.py 腳本，一開始先引入套件，並建立 `CpModel` 實例，命名為 `model`：

```py
&quot;&quot;&quot;Simple solve&quot;&quot;&quot;
from ortools.sat.python import cp_model

&quot;&quot;&quot;Minimal CP-SAT example to showcase calling the solver&quot;&quot;&quot;
# Create solver model
model = cp_model.CpModel()
```

後面我們會利用一系列 `model` 的方法來建立問題模型。

接著建立 x、y、z 三個變數，這三者皆為整數，範圍從 0 到 2。

利用 `NewIntVar()` 方法來建立整數變數，其中 `lb`、`ub` 分別表示 lower bound 與 upper bound，至於 x、y、z 具體是多少，目前還未定，要等到最後執行求解時才知道。

```py
# Create the variable.
x = model.NewIntVar(lb=0, ub=2, name=&#x27;x&#x27;)
y = model.NewIntVar(lb=0, ub=2, name=&#x27;y&#x27;)
z = model.NewIntVar(lb=0, ub=2, name=&#x27;z&#x27;)
```

接著建立約束，在此約束為「x 不等於 y」：

```py
# Create the constraints
model.Add( x != y )
```

接著制定目標，目標為三者之合應盡量大：

```py
# Objective
model.Maximize( x + y + z )
```

在以上兩段類似數學公式的參數中，可以像一般寫公式那樣在運算子和運算元間加入空格比較好閱讀，特別是在公式變得複雜的時候。

接著就可以來執行求解了：

```py
# Create a solver and solve the model
solver = cp_model.CpSolver()
status = solver.Solve(model=model)
```

那個 `status` 只是個表示解題狀態的字串，如果內容為 `OPTIMAL` 或 `FEASIBLE`，表示有找到解，除了有沒有解之外，我們也很關心那 x、y、z 以及三者之合最後是多少。

要取得模型變數的值，利用 `solver` 的 `Value()` 方法，要取得目標值，也就是三者之合，利用 `solver` 的 `ObjectiveValue()` 方法，範例如下：

```py
if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE:
    print(f&#x27;Maximum of objective function: {solver.ObjectiveValue()}\n&#x27;)
    print(f&#x27;x = {solver.Value(expression=x)}&#x27;)
    print(f&#x27;y = {solver.Value(expression=y)}&#x27;)
    print(f&#x27;z = {solver.Value(expression=z)}&#x27;)
else: print(&#x27;No solution found.&#x27;)
```

輸出如下：

```
Maximum of objective function: 5.0

x = 1
y = 2
z = 2
```

除此之外還有一些求解時的統計指標：

```py
# Statistics
print(solver.ResponseStats())
```

輸出如下：

```
CpSolverResponse summary:
status: OPTIMAL
objective: 5
best_bound: 5
integers: 1
booleans: 0
conflicts: 0
branches: 0
propagations: 0
integer_propagations: 1
restarts: 0
lp_iterations: 0
walltime: 0.00235364
usertime: 0.00235602
deterministic_time: 0
gap_integral: 0
solution_fingerprint: 0xbc76f3d3e780d9b7
```


最後完整的程式碼如下：

```py
&quot;&quot;&quot;Simple solve&quot;&quot;&quot;
from ortools.sat.python import cp_model


&quot;&quot;&quot;Minimal CP-SAT example to showcase calling the solver&quot;&quot;&quot;
# Create solver model
model = cp_model.CpModel()

# Create the variable.
x = model.NewIntVar(lb=0, ub=2, name=&#x27;x&#x27;)
y = model.NewIntVar(lb=0, ub=2, name=&#x27;y&#x27;)
z = model.NewIntVar(lb=0, ub=2, name=&#x27;z&#x27;)

# Create the constraints
model.Add( x != y )

# Objective
model.Maximize( x + y + z )

# Create a solver and solve the model
solver = cp_model.CpSolver()
status = solver.Solve(model=model)

if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE:
    print(f&#x27;Maximum of objective function: {solver.ObjectiveValue()}\n&#x27;)
    print(f&#x27;x = {solver.Value(expression=x)}&#x27;)
    print(f&#x27;y = {solver.Value(expression=y)}&#x27;)
    print(f&#x27;z = {solver.Value(expression=z)}&#x27;)
else: print(&#x27;No solution found.&#x27;)

# Statistics
print(solver.ResponseStats())
```

上面這份範例，相較於 OptaPy，實在簡單得太多，特別適合我這種傻瓜開發者，也不要因為看起來很簡單就懷疑 OR-Tools 的能力，初步用過 OptaPy 與 OR-Tools 之後個人的心得是，在懷疑它們能不能辦到前，應該先問自己有沒有正確理解問題，以及能不能從問題建立出相對的問題模型。

本文簡單介紹了 Google 的 OR-Tools 求解器，以及一些最基本的知識，更深入的部份等我參透再寫吧。:p --&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>認識求解器</title>
        <published>2023-05-28T00:00:00+00:00</published>
        <updated>2023-05-28T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2023/solver/"/>
        <id>https://editor.leonh.space/2023/solver/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2023/solver/">&lt;p&gt;這篇我要化身為 LibreOffice 小老師帶大家認識求解器，下面用實際的例子說明。&lt;&#x2F;p&gt;
&lt;p&gt;在會找錢的自助結帳機中，假設要找的錢是 12,345，那我們可以預期，客人應該不會想收到由一千多個十元和五元硬幣的零錢，而更想收到一些大面額鈔票加上少數幾個硬幣，那問題來了，究竟我們的自助結帳機要給出怎樣的貨幣組合才能滿足客人的期待呢？&lt;&#x2F;p&gt;
&lt;p&gt;對於上面的問題，我們可以在 LibreOffice Calc 如此表示：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2023&#x2F;solver&#x2F;1.png&quot; alt=&quot;LibreOffice Calc&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;其中：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;找零金額為 12,345，這是絕對不可違背的先決條件，不管找錢的組合怎麼拼怎麼湊，金額一定得要是 12,345，對於這種無法拒絕的條件，稱為硬約束。&lt;&#x2F;li&gt;
&lt;li&gt;在貨幣面額表中，有各貨幣的數量，以及面額乘以數量的金額，最下面一列分別是合計的金額，應該要等於 12,345，以及合計的貨幣數量，目前暫時先填入 0。&lt;&#x2F;li&gt;
&lt;li&gt;所謂客人的期待，具體而言就是要讓貨幣數量盡可能的少，能給大鈔的就不給硬幣，我們的目標就是任意異動各種面額的數量，讓合計金額為 12,345，且貨幣數量盡可能的少。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;了解以上三點後，先來個工人智慧求出的結果：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2023&#x2F;solver&#x2F;2.png&quot; alt=&quot;LibreOffice Calc&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;上圖中，客人拿到了貨幣組合為 12 張千元鈔票、3 張百元鈔、4 枚十元硬幣、1 枚五元硬幣，合計共 20 個貨幣。&lt;&#x2F;p&gt;
&lt;p&gt;接著動用求解器算算看，打開 LibreOffice Calc 選單列的「工具」，「求解器」，輸入一些儲存格參數：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2023&#x2F;solver&#x2F;3.png&quot; alt=&quot;LibreOffice Calc Solver&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;其中：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;目標儲存格為「C17」，即貨幣的合計數量，並希望結果為「最小」。&lt;&#x2F;li&gt;
&lt;li&gt;可變動的儲存格為「C5:C15」，即各個面額的數量。&lt;&#x2F;li&gt;
&lt;li&gt;硬約束為「D17 = D2」，即找零金額應為 12,345。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;此外，求解器還有一些選項：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2023&#x2F;solver&#x2F;4.png&quot; alt=&quot;LibreOffice Calc Solver&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;這邊我們又設定了變數為非負數、變數為整數兩項，其他選項可以自行把玩。&lt;&#x2F;p&gt;
&lt;p&gt;選項確定之後，就可以按下「求解」了，求解的結果如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2023&#x2F;solver&#x2F;5.png&quot; alt=&quot;LibreOffice Calc Solver&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;求解器的結果，客人拿到了貨幣組合為 6 張兩千元鈔票、1 張兩百元鈔票、1 張百元鈔、2 枚二十元硬幣、1 枚五元硬幣，合計共 11 個貨幣。&lt;&#x2F;p&gt;
&lt;p&gt;相較於原始的工人智慧，是不是客人更開心了呢。&lt;&#x2F;p&gt;
&lt;p&gt;上面的例子是以 LibreOffice Calc 示範，但求解器也存在其他試算表應用中，在 Gnumeric 稱為「解答器」，在 Excel 稱為「規劃求解」，網路上也很多人寫規劃求解的範例。&lt;&#x2F;p&gt;
&lt;p&gt;除了這個簡單的例子，求解器還可以應用在排班表、排課表、排排程、路徑規劃這類事務上，而這類需求往往也需要動用更專業的求解器，例如 Timefold 等等，以後有機會再來介紹。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>PM2</title>
        <published>2023-05-26T00:00:00+00:00</published>
        <updated>2023-05-26T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2023/pm2/"/>
        <id>https://editor.leonh.space/2023/pm2/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2023/pm2/">&lt;p&gt;PM2 是 Node.js 的 app server，就好比 Python 的 Uvicorn 或 Gunicorn，以及 Ruby 的 &lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2023&#x2F;passenger&#x2F;&quot;&gt;Passenger&lt;&#x2F;a&gt;，PHP 的 PHP-FPM。&lt;&#x2F;p&gt;
&lt;p&gt;這些 app server 負責調度與監管 app，這裡的 app 就是我們撰寫的專案或程式，app server 通常作為系統服務，當它跑起來之後也會把我們的 app 叫起來，並且視負載的輕重決定要開幾隻 app 程序來消化負載，也負責在 app 出錯時把 app 重啟，app server 在正式環境是影響一款應用能否穩定服務的重要角色，而 PM2 是 Node.js 生態中主流的 app server 之一。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;an-zhuang&quot;&gt;安裝&lt;&#x2F;h2&gt;
&lt;p&gt;雖然 PM2 也是 Node.js 套件，但它作為 app server，我們視其與 web server 為類似的角色，通常會全域安裝，而不是安裝在專案內，安裝指令：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; npm install pm2&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -g&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;安裝完執行看看有沒有成功：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; pm2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;初次執行應該會輸出以下訊息：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;__&#x2F;\\\\\\\\\\\\\____&#x2F;\\\\____________&#x2F;\\\\____&#x2F;\\\\\\\\\_____&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; _\&#x2F;\\\&#x2F;&#x2F;&#x2F;&#x2F;&#x2F;&#x2F;&#x2F;&#x2F;&#x2F;\\\_\&#x2F;\\\\\\________&#x2F;\\\\\\__&#x2F;\\\&#x2F;&#x2F;&#x2F;&#x2F;&#x2F;&#x2F;&#x2F;\\\___&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  _\&#x2F;\\\_______\&#x2F;\\\_\&#x2F;\\\&#x2F;&#x2F;\\\____&#x2F;\\\&#x2F;&#x2F;\\\_\&#x2F;&#x2F;&#x2F;______\&#x2F;&#x2F;\\\__&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   _\&#x2F;\\\\\\\\\\\\\&#x2F;__\&#x2F;\\\\&#x2F;&#x2F;&#x2F;\\\&#x2F;\\\&#x2F;_\&#x2F;\\\___________&#x2F;\\\&#x2F;___&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    _\&#x2F;\\\&#x2F;&#x2F;&#x2F;&#x2F;&#x2F;&#x2F;&#x2F;&#x2F;&#x2F;____\&#x2F;\\\__\&#x2F;&#x2F;&#x2F;\\\&#x2F;___\&#x2F;\\\________&#x2F;\\\&#x2F;&#x2F;_____&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     _\&#x2F;\\\_____________\&#x2F;\\\____\&#x2F;&#x2F;&#x2F;_____\&#x2F;\\\_____&#x2F;\\\&#x2F;&#x2F;________&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      _\&#x2F;\\\_____________\&#x2F;\\\_____________\&#x2F;\\\___&#x2F;\\\&#x2F;___________&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;       _\&#x2F;\\\_____________\&#x2F;\\\_____________\&#x2F;\\\__&#x2F;\\\\\\\\\\\\\\\_&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        _\&#x2F;&#x2F;&#x2F;______________\&#x2F;&#x2F;&#x2F;______________\&#x2F;&#x2F;&#x2F;__\&#x2F;&#x2F;&#x2F;&#x2F;&#x2F;&#x2F;&#x2F;&#x2F;&#x2F;&#x2F;&#x2F;&#x2F;&#x2F;&#x2F;&#x2F;__&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                          Runtime Edition&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        PM2 is a Production Process Manager for Node.js applications&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                     with a built-in Load Balancer.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                Start and Daemonize any application:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                $ pm2 start app.js&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                Load Balance 4 instances of api.js:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                $ pm2 start api.js -i 4&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                Monitor in production:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                $ pm2 monitor&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                Make pm2 auto-boot at server restart:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                $ pm2 startup&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                To go further checkout:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                http:&#x2F;&#x2F;pm2.io&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                        -------------&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;usage: pm2 [options] &amp;lt;command&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;pm2 -h, --help             all available commands and options&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;pm2 examples               display pm2 usage examples&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;pm2 &amp;lt;command&amp;gt; -h           help on a specific command&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Access pm2 files in ~&#x2F;.pm2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面的簡短介紹也差不多把 PM2 的基礎用法講完了，本文完。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;事情當然沒有那麼簡單，注意到上面訊息的最末一句「Access pm2 files in ~&#x2F;.pm2」，就先去看裡面有哪些東西吧。&lt;&#x2F;p&gt;
&lt;p&gt;~&#x2F;.pm2&#x2F; 裡面長這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;.pm2&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── logs&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── module_conf.json&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── modules&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── pids&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;└── touch&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;目前裡面大多是空的，比較要留意的只有那 logs&#x2F;，未來要排查問題時記得要來這裡找紀錄。&lt;&#x2F;p&gt;
&lt;p&gt;上面輸出訊息提示了一些 PM2 CLI 的用法，但在生產環境，我們會希望把配置寫成檔案，畢竟 CLI 參數總是會忘記，還是以配置檔的形式運作比較保險也可以納入版控管理。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pei-zhi&quot;&gt;配置&lt;&#x2F;h2&gt;
&lt;p&gt;在 ~&#x2F;.pm2&#x2F; 目錄下產生基礎的配置文件：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; cd ~&#x2F;.pm2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; pm2 init&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;產出的檔名為 ecosystem.config.js，內如如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;module&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;exports&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  apps: [&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    { script:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;index.js&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, watch:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;.&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt; },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    { script:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;.&#x2F;service-worker&#x2F;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, watch: [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;.&#x2F;service-worker&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;] },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ],&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  deploy: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    production: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      user:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;SSH_USERNAME&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      host:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;SSH_HOSTMACHINE&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      ref:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;origin&#x2F;master&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      repo:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;GIT_REPOSITORY&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      path:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;DESTINATION_PATH&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;      &amp;#39;pre-deploy-local&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;      &amp;#39;post-deploy&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;npm install &amp;amp;&amp;amp; pm2 reload ecosystem.config.js --env production&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;      &amp;#39;pre-setup&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;內容相當簡單，大多可望文生義，&lt;code&gt;apps&lt;&#x2F;code&gt; 區塊定義了兩個 Node.js app，&lt;code&gt;deploy&lt;&#x2F;code&gt; 區塊則是一些遠端部署用的參數與指令。&lt;&#x2F;p&gt;
&lt;p&gt;在 &lt;code&gt;apps&lt;&#x2F;code&gt; 區塊方面，所有的配置項目可以參考 PM2 文件〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;pm2.keymetrics.io&#x2F;docs&#x2F;usage&#x2F;application-declaration&#x2F;&quot;&gt;Configuration File&lt;&#x2F;a&gt;〉，這裡不重複說明，下面是本人常用的配置：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;apps&lt;&#x2F;span&gt;&lt;span&gt;: [&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;      &#x2F;&#x2F; General&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      name:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;liveboard&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      script:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;.&#x2F;build&#x2F;index.js&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      port:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 4000&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;      &#x2F;&#x2F; Advanced features&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      instances:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 2&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      env: { NODE_ENV:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;development&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt; },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      env_production: { NODE_ENV:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;production&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt; },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      env_staging: { NODE_ENV:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;staging&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt; },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      env_development: { NODE_ENV:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;development&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt; },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;      &#x2F;&#x2F; Log files&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      time:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;裡面的 &lt;code&gt;name&lt;&#x2F;code&gt;、&lt;code&gt;port&lt;&#x2F;code&gt; 就是字面上的意思，下面的 &lt;code&gt;instance&lt;&#x2F;code&gt; 自然就是這支 liveboard app 要開的程序數量了，透過多個程序來增加服務的容納量，這裡 PM2 會用 Node.js 的 cluster 模式來管理程序，那對外的 port 4000 由所有的 liveboard instance 共享，不會發生搶 port 的問題。&lt;&#x2F;p&gt;
&lt;p&gt;再往下是幾組 &lt;code&gt;env&lt;&#x2F;code&gt; 開頭的配置，這裡的 &lt;code&gt;env_xxx&lt;&#x2F;code&gt; 是可以自由制定的，裡面當然就是放環境變數啦，而不帶任何後綴的 &lt;code&gt;env&lt;&#x2F;code&gt; 則是預設的環境變數。&lt;&#x2F;p&gt;
&lt;p&gt;最後有個 &lt;code&gt;time: true&lt;&#x2F;code&gt;，就只是把 log 加上時間戳。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yun-xing-app&quot;&gt;運行 App&lt;&#x2F;h2&gt;
&lt;p&gt;配置好就可以跑起來了：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; pm2 start ecosystem.config.js&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;查看納管 app 狀態：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; pm2 list&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;輸出如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;⇆ PM2+ activated&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;┌────┬───────────┬───────────┬─────────┬─────────┬───────┬────────┬───┬────────┬─────┬────────┬──────┬──────────┐&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ id │ name      │ namespace │ version │ mode    │ pid   │ uptime │ ↺ │ status │ cpu │ mem    │ user │ watching │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├────┼───────────┼───────────┼─────────┼─────────┼───────┼────────┼───┼────────┼─────┼────────┼──────┼──────────┤&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ 0  │ liveboard │ default   │ 0.0.1   │ cluster │ 11784 │ 63m    │ 0 │ online │ 0%  │ 32.9mb │ user │ disabled │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ 1  │ liveboard │ default   │ 0.0.1   │ cluster │ 16076 │ 63m    │ 0 │ online │ 0%  │ 32.0mb │ user │ disabled │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;└────┴───────────┴───────────┴─────────┴─────────┴───────┴────────┴───┴────────┴─────┴────────┴──────┴──────────┘&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;chan-sheng-systemd-fu-wu-pei-zhi-dang-an&quot;&gt;產生 systemd 服務配置檔案&lt;&#x2F;h2&gt;
&lt;p&gt;另外 PM2 也可以產生 systemd 的服務配置檔，以普通用戶身分執行：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; pm2 startup&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;它會輸出：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[PM2] Init System found: systemd&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[PM2] To setup the Startup Script, copy&#x2F;paste the following command:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;sudo env PATH=$PATH:&#x2F;usr&#x2F;bin &#x2F;usr&#x2F;lib&#x2F;node_modules&#x2F;pm2&#x2F;bin&#x2F;pm2 startup systemd -u mes --hp &#x2F;home&#x2F;mes&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;依照吩咐執行：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo env PATH=&lt;&#x2F;span&gt;&lt;span&gt;$PATH&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;:&#x2F;usr&#x2F;bin &#x2F;usr&#x2F;lib&#x2F;node_modules&#x2F;pm2&#x2F;bin&#x2F;pm2 startup systemd&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -u&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; mes&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --hp&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &#x2F;home&#x2F;mes&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;輸出訊息如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;ini&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;[PM2]&lt;&#x2F;span&gt;&lt;span&gt; Init System found: systemd&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Platform systemd&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Template&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;[Unit]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;Description&lt;&#x2F;span&gt;&lt;span&gt;=PM2 process manager&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;Documentation&lt;&#x2F;span&gt;&lt;span&gt;=https:&#x2F;&#x2F;pm2.keymetrics.io&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;After&lt;&#x2F;span&gt;&lt;span&gt;=network.target&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;[Service]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;Type&lt;&#x2F;span&gt;&lt;span&gt;=forking&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;User&lt;&#x2F;span&gt;&lt;span&gt;=mes&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;LimitNOFILE&lt;&#x2F;span&gt;&lt;span&gt;=infinity&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;LimitNPROC&lt;&#x2F;span&gt;&lt;span&gt;=infinity&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;LimitCORE&lt;&#x2F;span&gt;&lt;span&gt;=infinity&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;Environment&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;PATH&lt;&#x2F;span&gt;&lt;span&gt;=&#x2F;home&#x2F;mes_staging&#x2F;.local&#x2F;bin:&#x2F;usr&#x2F;local&#x2F;sbin:&#x2F;usr&#x2F;local&#x2F;bin:&#x2F;usr&#x2F;sbin:&#x2F;usr&#x2F;bin:&#x2F;sbin:&#x2F;bin:&#x2F;usr&#x2F;games:&#x2F;usr&#x2F;local&#x2F;games:&#x2F;snap&#x2F;bin:&#x2F;usr&#x2F;bin:&#x2F;bin:&#x2F;usr&#x2F;local&#x2F;sbin:&#x2F;usr&#x2F;local&#x2F;bin:&#x2F;usr&#x2F;sbin:&#x2F;usr&#x2F;bin&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;Environment&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;PM2_HOME&lt;&#x2F;span&gt;&lt;span&gt;=&#x2F;home&#x2F;mes&#x2F;.pm2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;PIDFile&lt;&#x2F;span&gt;&lt;span&gt;=&#x2F;home&#x2F;mes&#x2F;.pm2&#x2F;pm2.pid&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;Restart&lt;&#x2F;span&gt;&lt;span&gt;=on-failure&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ExecStart&lt;&#x2F;span&gt;&lt;span&gt;=&#x2F;usr&#x2F;lib&#x2F;node_modules&#x2F;pm2&#x2F;bin&#x2F;pm2 resurrect&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ExecReload&lt;&#x2F;span&gt;&lt;span&gt;=&#x2F;usr&#x2F;lib&#x2F;node_modules&#x2F;pm2&#x2F;bin&#x2F;pm2 reload all&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ExecStop&lt;&#x2F;span&gt;&lt;span&gt;=&#x2F;usr&#x2F;lib&#x2F;node_modules&#x2F;pm2&#x2F;bin&#x2F;pm2 kill&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;[Install]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;WantedBy&lt;&#x2F;span&gt;&lt;span&gt;=multi-user.target&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Target path&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&#x2F;etc&#x2F;systemd&#x2F;system&#x2F;pm2-mes.service&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Command list&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;[ &amp;#39;systemctl enable pm2-mes&amp;#39; ]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;[PM2]&lt;&#x2F;span&gt;&lt;span&gt; Writing init configuration in &#x2F;etc&#x2F;systemd&#x2F;system&#x2F;pm2-mes.service&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;[PM2]&lt;&#x2F;span&gt;&lt;span&gt; Making script booting at startup...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;[PM2]&lt;&#x2F;span&gt;&lt;span&gt; [-] Executing: systemctl enable pm2-mes...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Created symlink &#x2F;etc&#x2F;systemd&#x2F;system&#x2F;multi-user.target.wants&#x2F;pm2-mes.service → &#x2F;etc&#x2F;systemd&#x2F;system&#x2F;pm2-mes.service.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;[PM2]&lt;&#x2F;span&gt;&lt;span&gt; [v] Command successfully executed.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;+---------------------------------------+&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;[PM2]&lt;&#x2F;span&gt;&lt;span&gt; Freeze a process list on reboot via:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;$ pm2 save&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;[PM2]&lt;&#x2F;span&gt;&lt;span&gt; Remove init script via:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;$ pm2 unstartup systemd&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如訊息所表示，它幫我們生成一份 systemd 服務配置檔案，也幫我們啟用了，如此以後重開機就會自動把 PM2 服務跑起來。&lt;&#x2F;p&gt;
&lt;p&gt;接著我們要做的是，告訴 PM2 它要把哪些 app 叫起來。延續前面的例子，先確定要跑的 app 有在運行中：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; pm2 start ecosystem.config.cjs&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; pm2 list&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;輸出如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;⇆ PM2+ activated&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;┌────┬───────────┬───────────┬─────────┬─────────┬───────┬────────┬───┬────────┬─────┬────────┬──────┬──────────┐&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ id │ name      │ namespace │ version │ mode    │ pid   │ uptime │ ↺ │ status │ cpu │ mem    │ user │ watching │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├────┼───────────┼───────────┼─────────┼─────────┼───────┼────────┼───┼────────┼─────┼────────┼──────┼──────────┤&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ 0  │ liveboard │ default   │ 0.0.1   │ cluster │ 11784 │ 63m    │ 0 │ online │ 0%  │ 32.9mb │ user │ disabled │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ 1  │ liveboard │ default   │ 0.0.1   │ cluster │ 16076 │ 63m    │ 0 │ online │ 0%  │ 32.0mb │ user │ disabled │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;└────┴───────────┴───────────┴─────────┴─────────┴───────┴────────┴───┴────────┴─────┴────────┴──────┴──────────┘&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;把目前運行中的 app 保存作為 PM2 服務啟動時要帶起來的 app：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; pm2 save&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;輸出如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[PM2] Saving current process list...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[PM2] Successfully saved in &#x2F;home&#x2F;mes&#x2F;.pm2&#x2F;dump.pm2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這個 dmp.pm2 是一份 JSON 文件，裡面紀錄一大堆前面兩個 app instance 的參數。&lt;&#x2F;p&gt;
&lt;p&gt;最後，當然就是重開機一波確認服務是否如常運行。&lt;&#x2F;p&gt;
&lt;p&gt;最後，因為 PM2 的角色是 app server，在正式環境通常還會在 app server 之前擺一個負責反向代理的 web server，最典型的就是 NGINX，由 NGINX 負責對外提供服務，再把部分請求轉交給對應的 app server，關於 NGINX 與 PM2 的轉發配置，可以參考 PM2 的〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;pm2.keymetrics.io&#x2F;docs&#x2F;tutorials&#x2F;pm2-nginx-production-setup&quot;&gt;Production Setup with Nginx&lt;&#x2F;a&gt;〉，而如果是 &lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2023&#x2F;caddy&#x2F;&quot;&gt;Caddy&lt;&#x2F;a&gt;，那可以參考以下 caddyfile 配置：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;http:&#x2F;&#x2F; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	handle_path &#x2F;* {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;		reverse_proxy :4000&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面配置會把所有的請求都轉發到 4000 埠，也就是 PM2 納管監聽的埠，至於轉發要帶上的那些 HTTP 標頭，&lt;code&gt;X-Forwarded-For&lt;&#x2F;code&gt;、&lt;code&gt;X-Real-IP&lt;&#x2F;code&gt; 什麼的，Caddy 原本就會帶上，無須額外配置，GJ。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;qi-ta-chang-yong-ming-ling&quot;&gt;其他常用命令&lt;&#x2F;h2&gt;
&lt;p&gt;還有一些常用命令：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;$ pm2 restart app_name&lt;&#x2F;code&gt;：重啟 app。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;$ pm2 stop app_name&lt;&#x2F;code&gt;：停止 app。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;$ pm2 delete app_name&lt;&#x2F;code&gt;：終止 app。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;$ pm2 logs app_name&lt;&#x2F;code&gt;：顯示 app log 訊息。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;$ pm2 describe app_name&lt;&#x2F;code&gt;：顯示 app 資訊。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;code&gt;pm2 describe app_name&lt;&#x2F;code&gt; 資訊滿多的，輸出如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Describing process with id 0 - name liveboard-svelte &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;┌───────────────────┬──────────────────────────────────────────────────────────┐&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ status            │ online                                                   │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ namespace         │ default                                                  │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ version           │ 0.0.1                                                    │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ restarts          │ 0                                                        │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ uptime            │ 4h                                                       │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ script args       │ N&#x2F;A                                                      │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ interpreter       │ node                                                     │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ interpreter args  │ N&#x2F;A                                                      │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ script id         │ 0                                                        │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ exec mode         │ cluster_mode                                             │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ node.js version   │ 18.17.1                                                  │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ node env          │ development                                              │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ watch &amp;amp; reload    │ ✘                                                        │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ unstable restarts │ 0                                                        │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ created at        │ 2023-05-09T09:05:58.233Z                                 │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;└───────────────────┴──────────────────────────────────────────────────────────┘&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; Revision control metadata &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;┌──────────────────┬───────────────────────────────────────────────────────┐&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ revision control │ git                                                   │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ last update      │ 2023-09-20T01:33:17.933Z                              │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ revision         │ e05dfe075a2f791f530ed15b3110f46ee7325772              │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ comment          │ Merge pull request &amp;#39;main&amp;#39; (#7) from main into staging │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│                  │                                                       │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│                  │ Revie                                                 │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ branch           │ staging                                               │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;└──────────────────┴───────────────────────────────────────────────────────┘&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; Actions available &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;┌────────────────────────┐&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ km:heapdump            │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ km:cpu:profiling:start │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ km:cpu:profiling:stop  │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ km:heap:sampling:start │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ km:heap:sampling:stop  │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;└────────────────────────┘&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; Trigger via: pm2 trigger liveboard-svelte &amp;lt;action_name&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; Code metrics value &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;┌────────────────────────┬───────────┐&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ Used Heap Size         │ 13.38 MiB │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ Heap Usage             │ 91.06 %   │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ Heap Size              │ 14.69 MiB │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ Event Loop Latency p95 │ 1.22 ms   │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ Event Loop Latency     │ 0.32 ms   │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ Active handles         │ 1         │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ Active requests        │ 0         │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;└────────────────────────┴───────────┘&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; Divergent env variables from local env &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;┌────────────────┬──────────────────────────────────────────┐&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│ XDG_SESSION_ID │ 180                                      │&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;└────────────────┴──────────────────────────────────────────┘&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Passenger 介紹</title>
        <published>2023-05-24T00:00:00+00:00</published>
        <updated>2023-05-24T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2023/passenger/"/>
        <id>https://editor.leonh.space/2023/passenger/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2023/passenger/">&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.phusionpassenger.com&#x2F;&quot;&gt;Passenger&lt;&#x2F;a&gt; 是在 web server 後面負責調度 web app 的 app server，關於怎麼理解 web server、app server、app 三者間的關係，我們可以看一下下面這張 Passenger 文件內的架構圖：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2023&#x2F;passenger&#x2F;python_stack.png&quot; alt=&quot;Passenger stack&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;圖片來自 &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;phusion&#x2F;passenger_library&#x2F;blob&#x2F;docs_2018&#x2F;LICENSE.md&quot;&gt;Phusion Holding B.V. 及其它貢獻者&lt;&#x2F;a&gt;&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;app-server&quot;&gt;App Server&lt;&#x2F;h2&gt;
&lt;p&gt;為何需要 app server 的角色？以一個 Rails app 來說，在開發階段只會用到 Rails 預帶的 Puma 或 Webrick 做為 web server，可是在正式環境必須有專職的 web server 在最前面負責與客戶端進行交涉，也就是大家都知道的 &lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2023&#x2F;caddy&#x2F;&quot;&gt;Caddy&lt;&#x2F;a&gt; 或 Nginx 或 Apache 的角色，web server 相當擅長於處理與客戶端之間的 HTTP(S) 協議，問題是它不知道如何與 Rails app 協同作業，在 PHP 的世界，我們有 PHP-FPM 做為 PHP 與 web server 間的溝通橋樑，在正確的組態設定下，web server 知道哪些請求可以由自己回應給客戶端，而哪些請求必須往後送給 PHP-FPM 去做處理回應後再轉回應給客戶端，類似的情境在 Ruby 的世界裡，就是由 Passanger 扮演著類似 PHP-FPM 的角色，Passenger 除了當 web server 與 Rails app 間溝通的橋樑外，還負擔調度 Rais app 的資源以及其它一些監控等的週邊措施，這樣的角色就是 app server，除了前面舉例的 PHP-FPM，Java 世界的 Tomcat 也是類似於這樣的角色。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;passenger-de-san-zhong-mo-shi&quot;&gt;Passenger 的三種模式&lt;&#x2F;h2&gt;
&lt;p&gt;了解了 app server 的角色之後，來談談 Passenger 如何與 Nginx &#x2F; Apache 協同作業。&lt;&#x2F;p&gt;
&lt;p&gt;我們知道 Nginx 或 Apache 都有模組的概念，可以掛上第三方模組來擴充功能，Passenger 就是以模組的方式掛上 web server，如下圖所示：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2023&#x2F;passenger&#x2F;int_modes-1024x321.png&quot; alt=&quot;Passenger modes&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;圖片來自 &lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;phusion&#x2F;passenger_library&#x2F;blob&#x2F;docs_2018&#x2F;LICENSE.md&quot;&gt;Phusion Holding B.V. 及其它貢獻者&lt;&#x2F;a&gt;&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;除了 Nginx &#x2F; Apache 整合模式外，另外一種 standalone 模式比較特別，它也是與 Nginx 共同工作，不一樣的點是反過來以 Passenger 為主要的配置設定對象，情境較單純，下面的篇章也都會以 standalone 模式為範例，等走過 standalone 模式後再往較複雜的整合模式走。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;passenger-yu-web-framework&quot;&gt;Passenger 與 Web Framework&lt;&#x2F;h2&gt;
&lt;p&gt;前面談完 Passenger 是以 web server 的模組來運作，那麼 Passenger 又是如何做為 Rails 的 app server呢？在 Ruby 的世界有個與 web server 溝通的標準介面 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;rack.github.io&#x2F;&quot;&gt;Rack&lt;&#x2F;a&gt;，只要 app server 與 web framework 都遵循 Rack 的標準介面去實做，那雙方就可以透過這個介面來溝通，例如 Rails 就是一個有實做 Rack 介面的 web framework，Passenger 當然也是一個有實做 Rack 介面的 app server。&lt;&#x2F;p&gt;
&lt;p&gt;雖然前面一直講 Passenger 與 Rails，不過其實 Passenger 不只可以做為 Ruby 的 app server，因為它的主要程式是以 C++ 開發的，只是初期只實做 Rack 介面，所以才會有 Passenger 只給 Ruby 用的印象，實際上現在的 Passenger 也實做了 Python 的 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.python.org&#x2F;dev&#x2F;peps&#x2F;pep-3333&#x2F;&quot;&gt;WSGI&lt;&#x2F;a&gt; 標準，意即 Passenger 也可以作為 Python web framework 的 app server，不僅如此，Passenger 6 增加了 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.phusionpassenger.com&#x2F;docs&#x2F;advanced_guides&#x2F;gls&#x2F;&quot;&gt;General Language Support&lt;&#x2F;a&gt; 的特性，利用類似 reverse proxy 的方式，號稱能支援所有的語言（狂），不過實際上到底用起來運作的如何還有待驗證。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;passenger-standalone-de-an-zhuang&quot;&gt;Passenger Standalone 的安裝&lt;&#x2F;h2&gt;
&lt;p&gt;Passenger 的安裝方式大致可以分為兩條路：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;OS 套件（DEB 或 RPM）&lt;&#x2F;li&gt;
&lt;li&gt;Ruby 套件（gem）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;要走那條路取決於後面的 app 是不是 Ruby，是 Ruby app 的話大多會用 gem 的方式安裝與配置，而這篇並不打算以 Ruby app 為範例，所以會以 OS 套件的方式來做安裝，然而前面提到過 Passenger 核心程式是以 C++ 開發，但是週邊的一些工具還是必須依賴 Ruby，所以在作業系統內還是得先裝好 Ruby。這裡我們從已經裝了 Ruby 的一台 Ubuntu 18.04 LTS 為範例，注意，在 Ubuntu 預帶的套件庫內也可能有 Passenger，不過版本過於老舊，我們不會採用，而使用 Passenger 自行維護的套件庫。依照 Passenger 的文件走，先安裝 Passenger 的 APT 套件庫的金鑰：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo apt install dirmngr gnupg&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo apt-key adv&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --keyserver&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; hkp:&#x2F;&#x2F;keyserver.ubuntu.com:80&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --recv-keys&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; 561F9B9CAC40B2F7&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo apt install apt-transport-https ca-certificates&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;再加一下 Passenger 的 APT 庫：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo sh&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -c&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;echo deb https:&#x2F;&#x2F;oss-binaries.phusionpassenger.com&#x2F;apt&#x2F;passenger bionic main &amp;gt; &#x2F;etc&#x2F;apt&#x2F;sources.list.d&#x2F;passenger.list&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;更新 APT 套件庫索引然後安裝 Passenger：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo apt update&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo apt install passenger&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;最後跑一下 Passenger 的檢查程式：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; passenger-config validate-install&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;就是這麼簡單。&lt;&#x2F;p&gt;
&lt;iframe src=&quot;https:&#x2F;&#x2F;www.youtube-nocookie.com&#x2F;embed&#x2F;O1IbUApsKYM?start=22&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;除了 Windows 以外的其它作業系統可以參照 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.phusionpassenger.com&#x2F;docs&#x2F;tutorials&#x2F;what_is_passenger&#x2F;&quot;&gt;Passenger 的文件&lt;&#x2F;a&gt;自行跟進學習，Windows 因為不被 Passenger 原生支援，所以只能心領神會了，或是試試看用 WSL 來當作學習測試環境，就這麼簡單。（？！）&lt;&#x2F;p&gt;
&lt;h2 id=&quot;passenger-standalone-de-shi-yong&quot;&gt;Passenger Standalone 的使用&lt;&#x2F;h2&gt;
&lt;p&gt;使用會分成兩個部份講，一是 Passenger 與 web app 之間的搭配組態設定；二是 Passenger 自己本身的命令列操作。&lt;&#x2F;p&gt;
&lt;p&gt;前面都在講 Ruby，這裡我們要突然翻盤拿 Passenger + &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;MasoniteFramework&#x2F;masonite&quot;&gt;Masonite&lt;&#x2F;a&gt; 來做範例，&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;the-benchmarker&#x2F;web-frameworks&quot;&gt;Masonite 是一個低效能的 Python WSGI web framework&lt;&#x2F;a&gt;，透過前面的介紹我們可以了解到 Passenger 與 Masonite 可以依照雙方的 WSGI 標準介面來做溝通。&lt;&#x2F;p&gt;
&lt;p&gt;在一個典型的 Masonite app 內，應該會看到如下的目錄結構：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── .&#x2F;app&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── .&#x2F;bootstrap&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── .&#x2F;config&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── .&#x2F;craft&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── .&#x2F;databases&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── .&#x2F;LICENSE&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── .&#x2F;__pycache__&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── .&#x2F;README.md&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── .&#x2F;requirements.txt&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── .&#x2F;resources&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── .&#x2F;routes&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── .&#x2F;storage&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── .&#x2F;tests&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;└── .&#x2F;wsgi.py&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;看最下面一行，專案根目錄內的 wsgi.py 即 Masonite 的 WSGI 實做介面，看一下內容：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;&amp;quot;&amp;quot;First Entry For The WSGI Server.&amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; masonite.app&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; App&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; bootstrap.start&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; app&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; config&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; application, providers&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;&amp;quot;&amp;quot;Instantiate Container And Perform Important Bindings&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;Some Service providers need important bindings like the WSGI application&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;and the application configuration file before they boot.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;container&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; App()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;container.bind(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;WSGI&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, app)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;container.bind(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;Application&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, application)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;container.bind(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;Container&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, container)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;container.bind(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;ProvidersConfig&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, providers)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;container.bind(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;Providers&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, [])&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;container.bind(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;WSGIProviders&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, [])&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;&amp;quot;&amp;quot;Bind all service providers&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;Let&amp;#39;s register everything into the Service Container. Once everything is&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;in the container we can run through all the boot methods. For reasons&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;some providers don&amp;#39;t need to execute with every request and should&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;only run once when the server is started. Providers will be ran&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;once if the wsgi attribute on a provider is False.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;for&lt;&#x2F;span&gt;&lt;span&gt; provider&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; in&lt;&#x2F;span&gt;&lt;span&gt; container.make(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;ProvidersConfig&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;).&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;PROVIDERS&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    located_provider&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; provider()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    located_provider.load_app(container).register()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    if&lt;&#x2F;span&gt;&lt;span&gt; located_provider.wsgi:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        container.make(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;WSGIProviders&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;).append(located_provider)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    else&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        container.make(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;Providers&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;).append(located_provider)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;for&lt;&#x2F;span&gt;&lt;span&gt; provider&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; in&lt;&#x2F;span&gt;&lt;span&gt; container.make(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;Providers&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    container.resolve(provider.boot)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;&amp;quot;&amp;quot;Get the application from the container&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;Some providers may change the WSGI Server like wrapping the WSGI server&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;in a Whitenoise container for an example. Let&amp;#39;s get a WSGI instance&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;from the container and pass it to the application variable. This&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;will allow WSGI servers to pick it up from the command line&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;application&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; container.make(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;WSGI&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;扣掉註解後其實沒有很多行，一樣直接跳到最後一行 &lt;code&gt;application = container.make(&#x27;WSGI&#x27;)&lt;&#x2F;code&gt;，定義了一個 &lt;code&gt;application&lt;&#x2F;code&gt;，依照 WSGI 的設計，WSGI server (Passenger) 會把客戶端請求送交給 &lt;code&gt;application&lt;&#x2F;code&gt;， &lt;code&gt;application&lt;&#x2F;code&gt; 負責接受請求並回應 HTTP 狀態與內容。&lt;&#x2F;p&gt;
&lt;p&gt;根據上面的描述，我們得讓 Passenger 去知道如何呼叫 &lt;code&gt;application&lt;&#x2F;code&gt;，依照 Passenger 的設計，Passenger 啟動時會去執行 passenger_wsgi.py 這個檔案，因此我們手動在 Masonite 專案根目錄內新增 passenger_wsgi.py 並寫入以下內容：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; wsgi&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; application&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;讓 Passenger 知道該去呼叫來自 wsgi.py 內的 application。&lt;&#x2F;p&gt;
&lt;p&gt;把 Passenger 跑起來：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; passenger start&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;輸出如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;=============== Phusion Passenger Standalone web server started ===============&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;PID file: &#x2F;home&#x2F;leon&#x2F;Git&#x2F;MasoniteDemo1&#x2F;passenger.3000.pid&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Log file: &#x2F;home&#x2F;leon&#x2F;Git&#x2F;MasoniteDemo1&#x2F;passenger.3000.log&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Environment: development&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Accessible via: http:&#x2F;&#x2F;0.0.0.0:3000&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;You can stop Phusion Passenger Standalone by pressing Ctrl-C.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Problems? Check https:&#x2F;&#x2F;www.phusionpassenger.com&#x2F;library&#x2F;admin&#x2F;standalone&#x2F;troubleshooting&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;===============================================================================&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;由 Passenger 給出的訊息我們可以知道埠號在 3000，開瀏覽器驗證一下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2023&#x2F;passenger&#x2F;2019-12-122023-06-3720E79A84E89EA2E5B995E693B7E59C96.png&quot; alt=&quot;Masonite&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;賀！順利看到了 &lt;del&gt;Laravel&lt;&#x2F;del&gt; Masonite 的啟始畫面。&lt;&#x2F;p&gt;
&lt;p&gt;按下 &lt;kbd&gt;CTRL-C&lt;&#x2F;kbd&gt; 即可停止 Passenger，或是開另外一個終端機再執行 &lt;code&gt;passenger stop&lt;&#x2F;code&gt; 也可停止 Passenger。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;passengerfile-json&quot;&gt;Passengerfile.json&lt;&#x2F;h2&gt;
&lt;p&gt;Passenger 有許多的設定，除了透過 &lt;code&gt;passenger&lt;&#x2F;code&gt; 命令的參數形式設定外，比較正規的做法應該是透過 Passengerfile.json 的設定檔來設定，passenger start 的時候會自己去讀 Passengerfile.json 的設定。具體有哪些設定項目多如繁星，請參閱 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.phusionpassenger.com&#x2F;docs&#x2F;references&#x2F;config_reference&#x2F;standalone&#x2F;&quot;&gt;Passenger 的文件&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-lun&quot;&gt;結論&lt;&#x2F;h2&gt;
&lt;p&gt;這篇介紹了 app server 的角色與架構，以及 Passenger 和 Masonite 之間的整合，但其實以上的介紹都還僅適用於開發環境，在正式環境中還有許多課題需要完成，包括 Python 的虛擬環境設定與 Passenger 的整合、Passenger 的安全性與效能設定、Passenger 與 web server 的整合這幾點都未加著墨，以後有機會再逐篇把我們走過的經驗分享出來。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Caddy Server 簡介</title>
        <published>2023-05-23T00:00:00+00:00</published>
        <updated>2023-05-23T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2023/caddy/"/>
        <id>https://editor.leonh.space/2023/caddy/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2023/caddy/">&lt;h2 id=&quot;jian-jie&quot;&gt;簡介&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2023&#x2F;caddy&#x2F;caddy-logo.svg&quot; alt=&quot;Caddy&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;caddyserver.com&#x2F;&quot;&gt;Caddy&lt;&#x2F;a&gt; 是一個年輕的 web server，以 Go 語言撰寫，特色：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;支援自動從 Let’s Encrypt 取得與更新 TLS 1.3 加密憑證。&lt;&#x2F;li&gt;
&lt;li&gt;以 HTTPS 作為預設的通訊協定。&lt;&#x2F;li&gt;
&lt;li&gt;支援 HTTP&#x2F;3。&lt;&#x2F;li&gt;
&lt;li&gt;支援 IPv6。&lt;&#x2F;li&gt;
&lt;li&gt;不依賴其它套件或 runtime，可獨立執行，因此很適合放在容器內跑。&lt;&#x2F;li&gt;
&lt;li&gt;以 production 為目標，可替代爺爺級的 Apache 或 Nginx。&lt;&#x2F;li&gt;
&lt;li&gt;提供組態設定 API。&lt;&#x2F;li&gt;
&lt;li&gt;支援 virtual host。&lt;&#x2F;li&gt;
&lt;li&gt;支援 reverse proxy。&lt;&#x2F;li&gt;
&lt;li&gt;支援 rewrite。&lt;&#x2F;li&gt;
&lt;li&gt;支援壓縮。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;當然其它特色還很多，但其中比較值得一提的就是上面這幾點。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;an-zhuang&quot;&gt;安裝&lt;&#x2F;h2&gt;
&lt;p&gt;前面提到 Caddy 本身不用依賴其它套件，所以官方發布的就只是個單一的執行檔，撇開之後由我們自己產生的設定檔不算的話，這就是 Caddy 的全部了！&lt;&#x2F;p&gt;
&lt;p&gt;依照 Caddy 網站的路徑下載 caddy，這裡我們直接抓 Caddy 2 的版本（以下的範例也都是以 Caddy 2 為基準），抓下來的檔名因版次或平台不同會有差異，先改名成簡潔的 &lt;code&gt;caddy&lt;&#x2F;code&gt;，再幫它加一下可執行的屬性：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; chmod a+x .&#x2F;caddy&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;除了自行下載外，一些 Linux 也有納入 Caddy 包，以 Ubuntu 系為例：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; sudo apt install caddy&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;用套件包安裝還會自動配置好系統服務，但是以開發環境來說這反而有點礙手了，往往我會把服務關掉：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; sudo systemctl disable caddy.service&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;免得它跟我搶 port。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jing-tai-wang-ye-si-fu-qi&quot;&gt;靜態網頁伺服器&lt;&#x2F;h2&gt;
&lt;p&gt;在 Caddy 所在位置新增一個 index.html，內容如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;h1&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;Hello Caddy!&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;h1&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;最單純的用法，這個簡易的指令體現了 Caddy 的簡潔：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; .&#x2F;caddy file-server --listen :2015&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;2019&#x2F;11&#x2F;30&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; 00:36:24.301 WARN admin admin endpoint disabled&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;2019&#x2F;11&#x2F;30&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; 00:59:38.744 INFO tls cleaned up storage units&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;2019&#x2F;11&#x2F;30&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; 08:36:24&lt;&#x2F;span&gt;&lt;span&gt; [INFO][cache:0xc0000d0140 Started certificate maintenance routine&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2019&#x2F;11&#x2F;30 08:36:24 Caddy &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;2&lt;&#x2F;span&gt;&lt;span&gt; serving static files on :2015&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在此我們指定埠號為 2015，Caddy 會去找當前位置下的 index.html 來顯示，因此用瀏覽器開 127.0.0.1:2015 應該會看到如下圖畫面：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2023&#x2F;caddy&#x2F;2019-11-30-10-21-14.png&quot; alt=&quot;Hello Caddy!&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;dang-an-si-fu-qi&quot;&gt;檔案伺服器&lt;&#x2F;h2&gt;
&lt;p&gt;先把剛剛的 index.html 改名或刪除，再加上 &lt;code&gt;--browse&lt;&#x2F;code&gt; 來跑，&lt;code&gt;--browse&lt;&#x2F;code&gt; 表示如果找不到 index.html，就列出當前位置下的檔案：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; .&#x2F;caddy file-server --browse --listen :2015&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;2019&#x2F;11&#x2F;30&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; 00:36:24.301 WARN admin admin endpoint disabled&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;2019&#x2F;11&#x2F;30&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; 08:36:24&lt;&#x2F;span&gt;&lt;span&gt; [INFO][cache:0xc0000d0140 Started certificate maintenance routine&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2019&#x2F;11&#x2F;30 08:36:24 Caddy &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;2&lt;&#x2F;span&gt;&lt;span&gt; serving static files on :2015&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這次打開 127.0.0.1:2015 應該可以看到類似下面的畫面：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2023&#x2F;caddy&#x2F;2019-11-30-10-19-33.png&quot; alt=&quot;Caddy&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;前面指令內的 &lt;code&gt;--browse&lt;&#x2F;code&gt; 表示開啟瀏覽目前 Caddy 所在位置的目錄檔案清單 ，也就是上面看到的抓圖內容。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;caddyfile&quot;&gt;Caddyfile&lt;&#x2F;h2&gt;
&lt;p&gt;前面都是簡單的範例，如果要做比較正規的應用，則需要一些組態設定，在 Caddy 的世界，除了用 API 之外，還支援 Caddyfile 的設定檔，API 與 Caddyfile 不同之處在於，API 必須使用 JSON 格式傳遞溝通，而 Caddyfile 的語法更像是傳統的定檔的格式，對人類來說更容易讀寫。&lt;&#x2F;p&gt;
&lt;p&gt;這裡必須再提一次這篇文章是講 Caddy 2，因為 Caddy 1 與 Caddy 2 的 Caddyfile 是不同的，雖然不同，但只是設定檔的關鍵字與格式不同，整體的 Caddyfile 語法結構還是相同的，所以還是得參考 Caddy 1 的 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;caddyserver.com&#x2F;docs&#x2F;caddyfile&quot;&gt;The Caddyfile Syntax&lt;&#x2F;a&gt; 來了解 Caddyfile 的結構。&lt;&#x2F;p&gt;
&lt;p&gt;沿用剛才的例子，做一個簡單的 index.html，以及一個 Caddyfile 內容如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;localhost:8080 {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  file_server&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;應該很容易理解，第一行表示監聽 localhost:8080，並且後方以大括號包住的區塊都是這組 localhost:8080 的設定。（在  Linux 針對埠號 1024 以下的常用埠號有權限限制，如果沒有權限的用戶要監聽埠號 80 的話會被系統阻擋，故要監聽 1024 以下的埠號須以 root 權限跑 Caddy。）&lt;&#x2F;p&gt;
&lt;p&gt;第二行表示啟動靜態檔案伺服，有 index.html 就會回應出去，並且未開啟目錄瀏覽。&lt;&#x2F;p&gt;
&lt;p&gt;跑起來看看：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; .&#x2F;caddy run&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;caddy run&lt;&#x2F;code&gt; 會自己去找同樣位置下的 Caddyfile，所以應該會看到同樣的 Hello Caddy! 網頁：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2023&#x2F;caddy&#x2F;2019-12-01-12-57-00.png&quot; alt=&quot;Caddy&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;virtual-host&quot;&gt;Virtual Host&lt;&#x2F;h2&gt;
&lt;p&gt;如果想要設定兩組網址，以 www1.example.com 與 www2.example.com 為例，可能的 Caddyfile 會長這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;www1.example.com:80 {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  root &#x2F;www&#x2F;www1.example.com&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  file_server&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;www2.example.com:80 {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  root &#x2F;www&#x2F;www2.example.com&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  file_server&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在上面的例子中，我們用 &lt;code&gt;root&lt;&#x2F;code&gt; 來指定該組網址對應到的真實路徑。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fan-xiang-dai-li&quot;&gt;反向代理&lt;&#x2F;h2&gt;
&lt;p&gt;Caddy 可以當坦承接流量，並把請求轉送到後端的服務，而且配置超簡單，非常適合跑當代百花齊放的框架、語言。&lt;&#x2F;p&gt;
&lt;p&gt;一般服務在正式環境都會配有所謂的 app server，它負責調配服務的資源，例如 Python 系的 Gunicorn、Ruby 的 &lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2023&#x2F;passenger&#x2F;&quot;&gt;Passenger&lt;&#x2F;a&gt;、PHP-FPM 等等，不論是哪種，Caddy 都可以扮演他們的反向代理，典型的 Caddyfile 配置會像這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;roysucks.com {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	encode zstd gzip&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	reverse_proxy &#x2F;* 127.0.0.1:8000&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;沒了，真的只有這樣，並且該轉送的 HTTP 標頭一個也不少。&lt;&#x2F;p&gt;
&lt;p&gt;如果是 PHP，那 Caddy 更有專門為它服務的 &lt;code&gt;php_fastcgi&lt;&#x2F;code&gt; 可用，Caddyfile 大概會像這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;php_fastcgi &#x2F;* 127.0.0.1:9000&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;是不是超級簡單，香～，當然這些都只是最簡單的用法，更進階的配置請去看 Caddy 文件囉！&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shi-ji-fan-li&quot;&gt;實際範例&lt;&#x2F;h2&gt;
&lt;p&gt;Caddyfile 可用的指令頗多，難以一一解釋，下面是本人比較常用的範例：&lt;&#x2F;p&gt;
&lt;h3 id=&quot;qian-hou-duan-du-zou-fan-xiang-dai-li&quot;&gt;前後端都走反向代理&lt;&#x2F;h3&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	debug&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	auto_https disable_redirects&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	email leon0824@gmail.com&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;http:&#x2F;&#x2F; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	# Enable Zstandard and Gzip compression&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	encode zstd gzip&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	log {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;		# output file &#x2F;var&#x2F;log&#x2F;access.log&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	handle_path &#x2F;api&#x2F;* {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;		reverse_proxy :8000&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	handle {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;		reverse_proxy :3000&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這是一個典型前後端分離、開發環境的配置，分為兩個區塊，最上面那塊沒有標注位址的是 Caddy 的全域設定，意義如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;debug&lt;&#x2F;code&gt;：在開發期間讓 log level 設為 debug，這要搭配下面的 &lt;code&gt;log&lt;&#x2F;code&gt; 共同使用。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;auto_https disable_redirects&lt;&#x2F;code&gt;：關閉自動把用戶從 HTTP 導向 HTTPS 的機制，個人不太喜歡這種隱式的重導，一般都會關掉。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;email&lt;&#x2F;code&gt;：申裝 TLS 憑證用的信箱，我們只需要提供信箱，其他 Caddy 會辦到好。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;第二個區塊就是具體的位址的設定，在這裡位址設定成 &lt;code&gt;http:&#x2F;&#x2F;&lt;&#x2F;code&gt;，這表示不需要申裝 TLS，因為想要讓開發環境單純一點。&lt;&#x2F;p&gt;
&lt;p&gt;區塊內的部份：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;encode zstd gzip&lt;&#x2F;code&gt;：讓網頁壓縮傳輸，在開發環境沒什麼意義，可開可不開。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;log&lt;&#x2F;code&gt;：開啟 log 輸出，輸出目的是 stdout，如果把註解拿掉就會寫入檔案，log level 則會是前面全域設的 debug。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;handle_path &#x2F;api&#x2F;*&lt;&#x2F;code&gt;：把所有送往 &#x2F;api&#x2F;* 的請求轉送到位於 8000 埠的後端服務。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;handle&lt;&#x2F;code&gt;：把所有請求（除了上面的 &#x2F;api&#x2F;*）轉送到 3000 埠的前端服務。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;code&gt;handle_path&lt;&#x2F;code&gt; 和 &lt;code&gt;handle&lt;&#x2F;code&gt; 的區別為，&lt;code&gt;handle_path&lt;&#x2F;code&gt; 會去除掉 &lt;code&gt;&#x2F;api&#x2F;&lt;&#x2F;code&gt; 再轉送給後端，因為我的後端並不認得那 &#x2F;api&#x2F; 路徑，而 &lt;code&gt;handle&lt;&#x2F;code&gt; 則會原樣轉送，該用哪個就取決於每個環境的狀況囉！&lt;&#x2F;p&gt;
&lt;h3 id=&quot;hou-duan-zou-fan-xiang-dai-li-qian-duan-wei-jing-tai-spa&quot;&gt;後端走反向代理 前端為靜態 SPA&lt;&#x2F;h3&gt;
&lt;p&gt;全域設定、log 設定參照前例，在此省略。&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;http:&#x2F;&#x2F; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	# Set this path to your site&amp;#39;s directory.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	root * &#x2F;usr&#x2F;share&#x2F;caddy&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	# Enable the static file server.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	file_server&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	# Enable Zstandard and Gzip compression&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	encode zstd gzip&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	handle_path &#x2F;api&#x2F;* {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;		# Proxies requests to one or more backends&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;		reverse_proxy unix&#x2F;&#x2F;run&#x2F;gunicorn.sock&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	handle {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;		try_files {path} &#x2F; # For built static site&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;跟前例大同小異，只差幾個指令：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;多設定了 &lt;code&gt;root&lt;&#x2F;code&gt; 指定靜態網頁的檔案路徑、&lt;code&gt;file_server&lt;&#x2F;code&gt; 啟動靜態資源伺服器。&lt;&#x2F;li&gt;
&lt;li&gt;最後的 &lt;code&gt;try_files&lt;&#x2F;code&gt; 把所有的網頁（除了那 &#x2F;api&#x2F;*）導向 SPA 的首頁，因為 SPA 通常都有自己的路由邏輯。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;這裡的 SPA 是 Vue 的例子，把所有對 SPA 的請求都丟給 Vue Router 處理。&lt;&#x2F;p&gt;
&lt;p&gt;最後提醒一個重點中的重點：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;檔案路徑要設對&lt;&#x2F;strong&gt;，不要像我設錯查半天。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-yu&quot;&gt;結語&lt;&#x2F;h2&gt;
&lt;p&gt;這篇文章介紹了 Caddy 2 的特性與 Caddyfile 的基本設定，Caddy 原生支援 HTTPS、HTTP&#x2F;3、virtual host、反向代理，並且配置超級簡單，效能又好，實在沒有再回去用陳年的 Apache 的理由。&lt;&#x2F;p&gt;
&lt;p&gt;Caddy 2 的文件還沒有很完整，想要用的人還是必須多多參考 Caddy2 網站或 Caddy wiki 的文件。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>用 chrony 建自己的 NTP 服務</title>
        <published>2023-05-05T00:00:00+00:00</published>
        <updated>2023-05-05T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2023/chrony/"/>
        <id>https://editor.leonh.space/2023/chrony/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2023/chrony/">&lt;p&gt;時間同步，或者說校時，是所有 OS 都內建的基礎服務之一，以 Ubuntu 為例，它會以 &lt;a rel=&quot;external&quot; href=&quot;http:&#x2F;&#x2F;ntp.ubuntu.com&quot;&gt;ntp.ubuntu.com&lt;&#x2F;a&gt; 的時間為標準來校準自己的時間，NTP（Network Time Protocol）是目前主流的時間協定。&lt;&#x2F;p&gt;
&lt;p&gt;然而在某些情況下，例如軍網、內網中，我們的機台可能無法用公網的 NTP 服務器，因此有必要自行搭建內網的 NTP 服務，如此方可確保內網內的所有機台都具有一致的時間基礎，才不會發生你那筆訂單在九點，我這邊出貨卻是在八點半的靈異現象。&lt;&#x2F;p&gt;
&lt;p&gt;chrony 是新一代的 NTP 服務，它既可以是 NTP client，也可以是 NTP server，這篇主要談的是以 chrony 作為 NTP server 的話題。&lt;&#x2F;p&gt;
&lt;p&gt;相較於老牌的 nptd、OpenNTPD、NTPsec，chrony 有以下優勢：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;新&lt;&#x2F;li&gt;
&lt;li&gt;現代化&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;是的沒錯，「新」本身就是優點，更具體的比較可以看〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;chrony.tuxfamily.org&#x2F;comparison.html&quot;&gt;Comparison of NTP implementations&lt;&#x2F;a&gt;〉，另外 chrony 本身也已經是 RedHat Linux 的預設 NTP 服務，不是射後不理型套件，可以放心使用。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;an-zhuang&quot;&gt;安裝&lt;&#x2F;h2&gt;
&lt;p&gt;在 Debian &#x2F; Ubuntu 一行安裝：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo apt install chrony&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;安裝 chrony 時會提示要取代 systemd-timesyncd，systemd-timesyncd 是 Ubuntu 預帶的 NTP client，本身不具備 NTP server 功能，而 chrony 同時具備 NTP client &#x2F; server 功能，可以放心取代。&lt;&#x2F;p&gt;
&lt;p&gt;安裝過程會自行配置並運作 chrony 服務，裝完後確認一下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; systemctl status chrony&lt;&#x2F;span&gt;&lt;span&gt; &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;應該會看到狀態顯示為「active (running)」。&lt;&#x2F;p&gt;
&lt;p&gt;另外也可以檢視當前的校時狀態：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; chronyc tracking&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;應該會看到這樣的輸出：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Reference ID    : 01220D59&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Stratum         : 3&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Ref time (UTC)  : Wed Apr 26 01:59:48 2023&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;System time     : 0.000083251 seconds slow of NTP time&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Last offset     : +0.000094192 seconds&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;RMS offset      : 0.000860929 seconds&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Frequency       : 13.559 ppm fast&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Residual freq   : +0.016 ppm&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Skew            : 1.030 ppm&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Root delay      : 0.012618504 seconds&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Root dispersion : 0.001813692 seconds&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Update interval : 64.1 seconds&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Leap status     : Normal&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;看起來就很厲害對吧。&lt;&#x2F;p&gt;
&lt;p&gt;確認有正常運作就好，後面再介紹其他命令。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pei-zhi&quot;&gt;配置&lt;&#x2F;h2&gt;
&lt;p&gt;配置檔在 &#x2F;etc&#x2F;chrony&#x2F;chrony.conf，裡面有好幾個區段，其中定義上游 NTP 的區段如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# This will use (up to):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# - 4 sources from ntp.ubuntu.com which some are ipv6 enabled&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# - 2 sources from 2.ubuntu.pool.ntp.org which is ipv6 enabled as well&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# - 1 source from [01].ubuntu.pool.ntp.org each (ipv4 only atm)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# This means by default, up to 6 dual-stack and up to 2 additional IPv4-only&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# sources will be used.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# At the same time it retains some protection against one of the entries being&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# down (compare to just using one of the lines). See (LP: #1754358) for the&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# discussion.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;#&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# About using servers from the NTP Pool Project in general see (LP: #104525).&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# Approved by Ubuntu Technical Board on 2011-02-08.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# See http:&#x2F;&#x2F;www.pool.ntp.org&#x2F;join.html for more information.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;pool ntp.ubuntu.com        iburst maxsources 4&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;pool 0.ubuntu.pool.ntp.org iburst maxsources 1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;pool 1.ubuntu.pool.ntp.org iburst maxsources 1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;pool 2.ubuntu.pool.ntp.org iburst maxsources 2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這裡預設的都是公網上的 NTP 服務器，作為 NTP client 時，如果要換成內網的另一台，可以把這些註解掉，填入內網的某一台 NTP server：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;server time.st.local&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這裡的關鍵字 &lt;code&gt;server&lt;&#x2F;code&gt; 和上面的 &lt;code&gt;pool&lt;&#x2F;code&gt; 都是用於指示上游的 NTP server 位址，差別在 &lt;code&gt;pool&lt;&#x2F;code&gt; 表示一群 NTP server，而 &lt;code&gt;server&lt;&#x2F;code&gt; 表示單台 NTP server。&lt;&#x2F;p&gt;
&lt;p&gt;在預設的配置中，並沒有開啟 chrony 的 NTP server，要開起來也很簡單，在配置檔中加入一行：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;allow&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;allow&lt;&#x2F;code&gt; 也像 &lt;code&gt;server&lt;&#x2F;code&gt; 或 &lt;code&gt;pool&lt;&#x2F;code&gt; 一樣，後面還可以接一些參數，用於限定允許連入的 IP，但多數的情境下不會制定這麼細的規則，就一個 &lt;code&gt;allow&lt;&#x2F;code&gt;，誰想連就連吧。&lt;&#x2F;p&gt;
&lt;p&gt;重啟 chrony：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;$ sudo systemctl restart chrony&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;chrony NTP server 啟動之後，拿別台機器當 NTP client 測一下沒問題就可以了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;chrony-cli-gong-ju&quot;&gt;chrony CLI 工具&lt;&#x2F;h2&gt;
&lt;p&gt;chrony 的 CLI 工具為 chrony&lt;strong&gt;c&lt;&#x2F;strong&gt;，前面我們用過 &lt;code&gt;chronyc tracking&lt;&#x2F;code&gt; 來檢視 chrony 與上游 NTP server 的校時狀況，除此之外還有一些別的命令。&lt;&#x2F;p&gt;
&lt;p&gt;檢視上游 NTP server 的狀態：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;$ chronyc sources&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;輸出如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;MS Name&#x2F;IP address         Stratum Poll Reach LastRx Last sample               &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;===============================================================================&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;^- prod-ntp-5.ntp1.ps5.cano&amp;gt;     2   8   377   108   -243us[ -299us] +&#x2F;-  106ms&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;^- pugot.canonical.com           2   9   377   306    +19ms[  +19ms] +&#x2F;-  156ms&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;^- alphyn.canonical.com          2   9   377   498  +3126us[+3080us] +&#x2F;-  177ms&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;^- prod-ntp-4.ntp1.ps5.cano&amp;gt;     2   7   377   491   -192us[ -237us] +&#x2F;-  100ms&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;^- time.cloudflare.com           3   9   377   377  -2563us[-2611us] +&#x2F;-   58ms&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;^- 125-229-162-223.hinet-ip&amp;gt;     2   7   377   110   +328us[ +272us] +&#x2F;-   58ms&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;^- 140.137.11.50                 2   9   277   512   -311us[ -356us] +&#x2F;-   46ms&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;^* twtpe2-ntp-002.aaplimg.c&amp;gt;     1   9   377    48  -8300ns[  -66us] +&#x2F;- 3441us&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中有些欄位沒有去深究它的意義，總之看起來很厲害。&lt;&#x2F;p&gt;
&lt;p&gt;檢視上游 NTP server 的一些統計數據：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;$ chronyc sourcestats&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;輸出如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Name&#x2F;IP Address            NP  NR  Span  Frequency  Freq Skew  Offset  Std Dev&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;==============================================================================&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;prod-ntp-5.ntp1.ps5.cano&amp;gt;  23  15   56m     +0.198      0.662   -704us   807us&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;pugot.canonical.com        29  12   53m     +0.937      2.395    +17ms  2888us&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;alphyn.canonical.com       29  17   56m     +0.145      1.349   +628us  1520us&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;prod-ntp-4.ntp1.ps5.cano&amp;gt;   9   6   30m     -0.050      0.877   -255us   279us&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;time.cloudflare.com        29  15   60m     +0.113      0.099  -2907us   116us&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;125-229-162-223.hinet-ip&amp;gt;  22  11   28m     -0.378      0.776    +75us   418us&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;140.137.11.50              25  12   58m     -0.028      0.144   -166us   191us&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;twtpe2-ntp-002.aaplimg.c&amp;gt;  25  13   56m     -0.005      0.082  -1070ns    91us&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;厲害！&lt;&#x2F;p&gt;
&lt;p&gt;檢視上游 NTP 的存活狀態：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;$ chronyc activity&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;輸出如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;200 OK&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;8 sources online&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;0 sources offline&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;0 sources doing burst (return to online)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;0 sources doing burst (return to offline)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;0 sources with unknown address&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;都活著，厲害。&lt;&#x2F;p&gt;
&lt;p&gt;以上介紹 chrony 的小廢文結束。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Podman 快速筆記</title>
        <published>2022-11-11T00:00:00+00:00</published>
        <updated>2022-11-11T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/podman/"/>
        <id>https://editor.leonh.space/2022/podman/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/podman/">&lt;p&gt;前情提要：之前在&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;gitea&#x2F;&quot;&gt;自架 Drone CI&lt;&#x2F;a&gt; 文中提到，那裡的 Drone server 是用 Podman 跑的，並且該文最後留下一個懸念「該怎麼讓 Drone server &#x2F; Drone runner 變成服務，並且開機會自行啟動？」而本文就是 Podman 給出的解答。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;podman&quot;&gt;Podman&lt;&#x2F;h2&gt;
&lt;p&gt;首先認識一下 Podman，它可以視為 Docker 的替代品，不過它並不只是隻模仿貓，它有以下特點：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Apache 開源授權，由紅帽開發，有富爸爸，並且至今沒有被放生，依然活躍發展中。&lt;&#x2F;li&gt;
&lt;li&gt;跑起來不需要 root 權限，也並非 daemon 服務，這種設計比 Docker 安全一點（但也因此要用些許手段才能設成服務）。&lt;&#x2F;li&gt;
&lt;li&gt;和 Docker 指令有九成像，可以秒轉換。&lt;&#x2F;li&gt;
&lt;li&gt;和 Docker 不同的是 Podman 有 pod 的概念，也就是服務集群，感覺又離最潮的 K8s 近了一點。&lt;&#x2F;li&gt;
&lt;li&gt;對於習慣用 Docker Compose 的人，也有 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;containers&#x2F;podman-compose&quot;&gt;Podman Compose&lt;&#x2F;a&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;也有類似 Docker Desktop 的 Podman Desktop，並且不用擔心它的商業授權問題。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;額外補充上列的最後一點，根據 Docker 公司的授權條款，Docker Desktop 只要是商用，並且公司規模超過某一程度，Docker Desktop 必須要付費，而 Docker Engine 仍然是免費使用，兩者大致的差異如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Docker Engine: Docker daemon + Docker CLI&lt;&#x2F;li&gt;
&lt;li&gt;Docker Desktop: Docker Engine + Docker GUI + Docker VM&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;簡單的說，Decker Desktop 有 GUI，對於非 Linux 的系統它還會幫你建 VM，實在貼心，但作為一位開發者，都會用 Docker 了，怎麼還會被 GUI 吸引，又怎麼會被 VM 困住，反過來說，對於非開發者，有 Docker GUI 一樣不會用，還是要乖乖的從 CLI 學起，如此看來那 Docker GUI 似乎沒什麼用。&lt;&#x2F;p&gt;
&lt;p&gt;總之我就免費仔～把話題拉回 Podman。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;podman-an-zhuang&quot;&gt;Podman 安裝&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;podman.io&#x2F;getting-started&#x2F;installation&quot;&gt;Podman 自己的安裝說明&lt;&#x2F;a&gt; 相當冗長，其實就一行搞定：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo apt-get install podman&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;是的 Ubuntu 原本的 APT 庫就有 Podman，當前版本為 3.4，而 Podman 自己最新的版本是 4.0，略有落後但不影響使用。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;podman-shi-yong&quot;&gt;Podman 使用&lt;&#x2F;h2&gt;
&lt;p&gt;因為是 Docker copycat，所以指令幾乎一樣，差別在抓 image 時要用完整網址，如：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;在 Docker，image 位址為  &lt;strong&gt;gitea&lt;&#x2F;strong&gt;&#x2F;&lt;u&gt;gitea:latest&lt;&#x2F;u&gt;，在 Podman，image 位址為 docker.io&#x2F;&lt;strong&gt;gitea&lt;&#x2F;strong&gt;&#x2F;&lt;u&gt;gitee:latest&lt;&#x2F;u&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;在 Docker，image 位址為 &lt;u&gt;httpd&lt;&#x2F;u&gt;，在 Podman，image 位址為 docker.io&#x2F;&lt;strong&gt;library&lt;&#x2F;strong&gt;&#x2F;&lt;u&gt;httpd&lt;&#x2F;u&gt;，沒有組織時就放「&lt;strong&gt;library&lt;&#x2F;strong&gt;」替代。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;下面是一些開容器的例子。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ubuntu&quot;&gt;Ubuntu&lt;&#x2F;h3&gt;
&lt;p&gt;以 Ubuntu 為例，如果要開個可拋式容器用一下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; podman pull docker.io&#x2F;library&#x2F;ubuntu:22.04&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; podman run&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --interactive --tty --rm&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ubuntu:22.04 bash&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;gitea&quot;&gt;Gitea&lt;&#x2F;h3&gt;
&lt;p&gt;比較完整的用法，以 Gitea 為例：&lt;&#x2F;p&gt;
&lt;p&gt;抓 Gitea image：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; podman pull docker.io&#x2F;gitea&#x2F;gitea:latest&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;建 Gitea 資料目錄：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo mkdir&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -p&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &#x2F;var&#x2F;lib&#x2F;gitea&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;跑起來：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; podman run&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--detach&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--name=gitea&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--publish&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; 14415:22&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--publish&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; 14416:3000&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--volume&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &#x2F;var&#x2F;lib&#x2F;gitea:&#x2F;data&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;gitea&#x2F;gitea:latest&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;mysql&quot;&gt;MySQL&lt;&#x2F;h3&gt;
&lt;p&gt;直接上指令：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; podman run&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--detach&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--name=mysql&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--publish&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; 3307:3306&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--env&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; MYSQL_ALLOW_EMPTY_PASSWORD=yes&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;docker.io&#x2F;library&#x2F;mysql:8.0-debian&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這裡把容器的 3306 port 對應到 Docker 主機的 3307 port，另外因為是可拋式容器，就不設密碼了。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;postgresql&quot;&gt;PostgreSQL&lt;&#x2F;h3&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; podman run&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--detach&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--env&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; POSTGRES_PASSWORD=mysecretpassword&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; pg&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;--publish&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; 5432:5432&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;docker.io&#x2F;library&#x2F;postgres:15-alpine&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;ba-rong-qi-she-cheng-fu-wu&quot;&gt;把容器設成服務&lt;&#x2F;h2&gt;
&lt;p&gt;前面提過，Podman 本身不會註冊成服務，但是有時候的確需要把一些容器以服務的形式運行，Podman 也貼心的設想到了，它可以快速幫我們產生 Linux 的 systemd 的服務配置文件。&lt;&#x2F;p&gt;
&lt;p&gt;假設有個容器叫 drone，想要產生它的 systemd 服務配置文件，可用以下指令：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; podman generate systemd&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; drone&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --files&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;它會產生一個 container-drone.service 檔案，把檔案移到 ~&#x2F;.config&#x2F;systemd&#x2F;user&#x2F;：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo mv ~&#x2F;container-drone.service ~&#x2F;.config&#x2F;systemd&#x2F;user&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;讓它自動啟動：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; systemctl&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --user&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; daemon-reload&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; systemctl&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --user&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; enable container-drone.service&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;讓它立馬啟動：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; systemctl&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --user&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; start container-drone.service&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;確認服務狀態：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; systemctl&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --user&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; status container-drone.service&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;hou-zhi-zuo-ye&quot;&gt;後置作業&lt;&#x2F;h3&gt;
&lt;p&gt;以上服務是跑在非 root 帳號，要多額外開啟 linger，此特性是讓目前帳號能開機自動登入，並跑起服務。&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo loginctl enable-linger&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;最後的最後，重開機看看是否一切運作正常。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>舊瓶也能裝新酒 Ubuntu 14.04 硬裝 Docker 實戰</title>
        <published>2022-11-07T00:00:00+00:00</published>
        <updated>2022-11-07T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/docker-on-ubuntu-14-04/"/>
        <id>https://editor.leonh.space/2022/docker-on-ubuntu-14-04/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/docker-on-ubuntu-14-04/">&lt;p&gt;因為某些因素，必須評估把陳年的 Ubuntu 14.04 撿起來裝 Docker 的方案，在 Ubuntu 14.04 已經退場多年的現在幹這事，當然不是 &lt;code&gt;apt install docker.io&lt;&#x2F;code&gt; 這麼簡單，否則這篇文章就不會出現了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;which-docker&quot;&gt;Which Docker?&lt;&#x2F;h2&gt;
&lt;p&gt;在 Ubuntu 原始的套件庫內就有 Docker，也就是前面提到的 docker.io ，不論是 Ubuntu 14.04 或現在的 Ubuntu，都一律不建議使用這個套件來安裝 Docker，因為這些 Ubuntu 自帶的 docker.io 並非由 Docker Inc. 官方維護，因此更新永遠慢一步甚至很多步。更新慢會有什麼樣的問題？答案是不安全，讓我們看看 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.cvedetails.com&#x2F;product&#x2F;28125&#x2F;Docker-Docker.html?vendor_id=13534&quot;&gt;Docker 的漏洞清單&lt;&#x2F;a&gt;，可以想像一下如果裝了幾乎不更新的 docker.io 我們的系統會暴露在多大的安全風險中。&lt;&#x2F;p&gt;
&lt;p&gt;因此，在 OS 必須是 Ubuntu 14.04 的先決條件下，我們還是要盡可能的降低系統的安全風險，所以必須使用 Docker 官方的最新套件來安裝 Docker。&lt;&#x2F;p&gt;
&lt;p&gt;就像寒戰電影中保安局局長陸先生所言：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;docker-on-ubuntu-14-04&#x2F;tg-fxuapvw2097122-1024x576.jpg&quot; alt=&quot;非常時期 用非常的方法&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;非常時期 用非常的方法（可是還是不能打民眾）&lt;&#x2F;figcaption&gt;
&lt;figcaption&gt;版權：華映娛樂股份有限公司&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;an-zhuang-docker&quot;&gt;安裝 Docker&lt;&#x2F;h2&gt;
&lt;p&gt;先裝一些基礎套件：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; sudo apt install apt-transport-https ca-certificates curl gnupg-agent software-properties-common&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;下載並把 Docker GPG 金鑰加入系統：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; curl -fsSL https:&#x2F;&#x2F;download.docker.com&#x2F;linux&#x2F;ubuntu&#x2F;gpg &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;|&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; sudo&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; apt-key add -&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;正式的把 Docker 官方套件庫加入系統：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; sudo add-apt-repository &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;deb [arch=amd64] https:&#x2F;&#x2F;download.docker.com&#x2F;linux&#x2F;ubuntu $(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;lsb_release&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -cs&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;) stable&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;安裝 Docker：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; sudo apt install docker-ce&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;按照上面的步驟把 Docker 裝完後，如果嘗試操作 image 甚至建立容器，乍看之下會覺得都是正常的，但是一旦想要進入容器內就會噴錯：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;gt; sudo docker start --interactive 62ba&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;docker: Error response from daemon: OCI runtime create failed: container_linux.go:348: starting container process caused &amp;quot;process_linux.go:301: running exec setns process for init caused \&amp;quot;exit status 23\&amp;quot;&amp;quot;: unknown&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;得到上面這堆不知所云的錯誤訊息。&lt;&#x2F;p&gt;
&lt;p&gt;必須更新 Linux 核心才能夠正常使用。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;an-zhuang-xin-ban-linux-he-xin&quot;&gt;安裝新版 Linux 核心&lt;&#x2F;h2&gt;
&lt;p&gt;在動手硬幹前先了解一下為何必須這麼做。&lt;&#x2F;p&gt;
&lt;p&gt;為了解決 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.cvedetails.com&#x2F;cve&#x2F;CVE-2019-5736&#x2F;&quot;&gt;CVE-2019-5736&lt;&#x2F;a&gt; 這個漏洞，Docker 對 Linux 核心的版本需求為 3.17 以上版本，不幸的 Ubuntu 14.04 的 Linux 核心最高只有到 3.13，幸虧透過 Ubuntu 的 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;wiki.ubuntu.com&#x2F;Kernel&#x2F;LTSEnablementStack&quot;&gt;LTS Enablement Stacks&lt;&#x2F;a&gt; 機制，能夠讓我們在 Ubuntu 14.04 上有辦法安裝 16.06 的 Linux 4.4 核心。&lt;&#x2F;p&gt;
&lt;p&gt;安裝新版 Linux 核心：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo apt-get install&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --install-recommends&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; linux-generic-lts-xenial&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;裝完之後要重開機一下，重開機後，系統應該就會是 4.4 版的核心了。&lt;&#x2F;p&gt;
&lt;p&gt;再次嘗試進入容器內，應該就會是成功的了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;hou-hua&quot;&gt;後話&lt;&#x2F;h2&gt;
&lt;p&gt;儘管技術上可行也測試 OK，不過這樣的解法最後並沒有應用到正式運作的機台上，開玩笑怎麼可能拿正式版的機台來換核心，當然要先研究不傷身體再講求療效啊！&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;docker-on-ubuntu-14-04&#x2F;2016-11-28-17.21.55-1024x626.jpg&quot; alt=&quot;第一先研究不傷身體&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;第一先研究不傷身體&lt;&#x2F;figcaption&gt;
&lt;figcaption&gt;版權：五洲製藥&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>十分鐘自架 Drone CI</title>
        <published>2022-10-22T00:00:00+00:00</published>
        <updated>2022-10-22T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/drone/"/>
        <id>https://editor.leonh.space/2022/drone/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/drone/">&lt;p&gt;前情提要：之前&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;gitea&#x2F;&quot;&gt;自架了 Gitea&lt;&#x2F;a&gt;，之後上網查一下，大多數人都是用 Drone CI 與之搭配，所以我也是選用 Drone CI 囉！&lt;&#x2F;p&gt;
&lt;p&gt;Drone CI 分為 Drone server 和 Drone runner，一個是主控機，一個是實際跑 CI 任務的機台，當然想要架多台 runner 也是可以，只要有鈔能力。&lt;&#x2F;p&gt;
&lt;p&gt;不論是 Drone server 還是 Drone runner，一律都是以容器的形式運行，請先搞定 Podman 或 Docker。&lt;&#x2F;p&gt;
&lt;p&gt;另外提醒一下，如果是在公司，又沒有鈔能力的話，不要裝 Docker Desktop，可以改裝 Docker Engine，以免成為盜版軟體的 &lt;del&gt;受益者&lt;&#x2F;del&gt; 受害者。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jia-she-drone-server&quot;&gt;架設 Drone Server&lt;&#x2F;h2&gt;
&lt;p&gt;在把容器跑起來之前，要先在 Gitea 做一下文書作業。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;qian-zhi-zuo-ye&quot;&gt;前置作業&lt;&#x2F;h3&gt;
&lt;p&gt;在 Gitea 建立 Drone 外部應用程式，在「設定 → 應用程式」，要填入兩個值，範例如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;應用程式名稱&lt;br &#x2F;&gt;
&lt;code&gt;drone&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;應用程式 URI&lt;br &#x2F;&gt;
&lt;code&gt;http:&#x2F;&#x2F;192.168.0.226:3001&#x2F;login&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;這裡的 URI 是 Drone server 的 URI，目前實際上還不存在，這是事先預想好的，謀定而後動，知止而有得。&lt;&#x2F;p&gt;
&lt;p&gt;提交後會產生一對 Client ID 和 Client Secret，請抄錄下來，範例如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Client ID&lt;br &#x2F;&gt;
&lt;code&gt;1306e04d-f653&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Client Secret&lt;br &#x2F;&gt;
&lt;code&gt;gto_de5nyz&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Gitea 的前置作業完畢，可以設置 Drone server 了。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;pao-drone-server-rong-qi&quot;&gt;跑 Drone Server 容器&lt;&#x2F;h3&gt;
&lt;p&gt;跑 Drone server 的參數頗多，先整理一下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;DRONE_GITEA_CLIENT_ID&lt;br &#x2F;&gt;
&lt;code&gt;1306e04d-f653&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;DRONE_GITEA_CLIENT_SECRET&lt;br &#x2F;&gt;
&lt;code&gt;gto_de5nyz&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;DRONE_GITEA_SERVER&lt;br &#x2F;&gt;
&lt;code&gt;http:&#x2F;&#x2F;192.168.0.226:3000&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;DRONE_RPC_SECRET&lt;br &#x2F;&gt;
&lt;code&gt;super-duper-secret&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;DRONE_SERVER_HOST&lt;br &#x2F;&gt;
&lt;code&gt;192.168.0.226:3001&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;DRONE_SERVER_PROTO&lt;br &#x2F;&gt;
&lt;code&gt;http&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;DRONE_USER_CREATE&lt;br &#x2F;&gt;
&lt;code&gt;username:drone_admin,admin:true&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;從這邊可以看到我的 Gitea 是跑在 192.168.0.226:3000，而 Drone server 預計是跑在 192.168.0.226:3001，門牌只差一號比較近跑起來會比較快（胡說八道）。&lt;&#x2F;p&gt;
&lt;p&gt;其他大部分參數都可以望文生義，就不解釋了。&lt;&#x2F;p&gt;
&lt;p&gt;這裡我是用 Podman，它的 CLI 用法和 Docker 有九成像。&lt;&#x2F;p&gt;
&lt;p&gt;下載 Drone server image：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; podman pull docker.io&#x2F;drone&#x2F;drone:2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Drone 文件採用的板號是固定主版號 &lt;code&gt;2&lt;&#x2F;code&gt;，沒有採用 &lt;code&gt;latest&lt;&#x2F;code&gt; ，應該是較為穩健的考量。&lt;&#x2F;p&gt;
&lt;p&gt;跑起來：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; podman run&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  --volume=&#x2F;var&#x2F;lib&#x2F;drone:&#x2F;data \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  --env=DRONE_GITEA_SERVER=http:&#x2F;&#x2F;192.168.0.226:3000 \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  --env=DRONE_GITEA_CLIENT_ID=1306e04d-f653 \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  --env=DRONE_GITEA_CLIENT_SECRET=gto_de5nyz \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  --env=DRONE_RPC_SECRET=super-duper-secret \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  --env=DRONE_SERVER_HOST=192.168.0.226:3001 \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  --env=DRONE_SERVER_PROTO=http \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  --env=DRONE_USER_CREATE=username:drone_admin,admin:true \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  --env=TZ=Asia&#x2F;Taipei \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  --publish=3001:80 \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  --restart=always \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  --detach=true \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  --name=drone \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  --restart=always \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;  docker.io&#x2F;drone&#x2F;drone:2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;執行後用 &lt;code&gt;podman ps&lt;&#x2F;code&gt; 確認一下，沒有死掉就好。&lt;&#x2F;p&gt;
&lt;p&gt;開瀏覽器到 http:&#x2F;&#x2F;192.168.0.226:3001&#x2F;，應該會到達 Drone server 登陸頁。&lt;&#x2F;p&gt;
&lt;p&gt;依照指示授權、開帳號完畢後，Drone 頁面就會看到 repo 了。&lt;&#x2F;p&gt;
&lt;p&gt;至此 Gitea 的一些動作（例如提交、合併）就會打給 Drone server 的 webhook，而 Drone server 就會把任務派給 runner 做，runner 則會把專案拉下來到一個新容器，Drone server 則負責監控 runner 的任務運行狀況，而具體的任務則由專案內的 .drone.yml 檔案定義，我們可以給它命令讓它跑測試、建置、部署之類的工作。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;yi-xie-xiao-zhi-ling&quot;&gt;一些小指令&lt;&#x2F;h3&gt;
&lt;p&gt;要看 log，可以執行：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; podman logs drone&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;要進入容器：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; podman exec&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -it&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; drone &#x2F;bin&#x2F;sh&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;接著來架 Drone runner。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pao-drone-runner&quot;&gt;跑 Drone Runner&lt;&#x2F;h2&gt;
&lt;p&gt;前面說過，Drone server 是主控，runner 才是跑 pipeline 的人。&lt;&#x2F;p&gt;
&lt;p&gt;Runner 本身也是個容器。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;hub.docker.com&#x2F;r&#x2F;drone&#x2F;drone-runner-docker&quot;&gt;Image&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Image 真身網址&lt;br &#x2F;&gt;
docker.io&#x2F;drone&#x2F;drone-runner-docker&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Drone Runner 在 Podman 上跑失敗，另開一台用純正 Docker 跑。&lt;&#x2F;p&gt;
&lt;p&gt;把 image 抓下來：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo docker pull docker.io&#x2F;drone&#x2F;drone-runner-docker:1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;準備參數：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;DRONE_RPC_HOST&lt;br &#x2F;&gt;
&lt;code&gt;192.168.0.226:3001&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;DRONE_RPC_PROTO&lt;br &#x2F;&gt;
&lt;code&gt;http&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;DRONE_RPC_SECRET&lt;br &#x2F;&gt;
&lt;code&gt;super-duper-secret&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;跑起來：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo docker run&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  --detach \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  --volume=&#x2F;var&#x2F;run&#x2F;docker.sock:&#x2F;var&#x2F;run&#x2F;docker.sock \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  --env=DRONE_RPC_PROTO=http \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  --env=DRONE_RPC_HOST=192.168.0.226:3001 \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  --env=DRONE_RPC_SECRET=super-duper-secret \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  --env=DRONE_RUNNER_CAPACITY=2 \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  --env=DRONE_RUNNER_NAME=my-first-runner \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  # --env=DRONE_UI_USERNAME=root \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  # --env=DRONE_UI_PASSWORD=root \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  --env&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;TZ&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;Asia&#x2F;Taipei&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  --publish=3000:3000 \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  --restart=always \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  --name=runner \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;  drone&#x2F;drone-runner-docker:1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中兩行被註解掉的是 runner web UI 的帳密，這 runner web UI 有點雞肋，主要的資訊都在 Drone server 的 web，runner 這邊的不開也罷，註解後 runner 就不會啟動 web UI 了。&lt;&#x2F;p&gt;
&lt;p&gt;透過以上的參數 Drone runner 會自行和 Drone server 註冊，不需要再做什麼人工註冊 runner 之類的文書作業了，此後當 Drone server 接到工作時就會分派給 runner。&lt;&#x2F;p&gt;
&lt;p&gt;一般來說到這邊就可以休息一下，後面還有真正的魔王，pipeline 配置等著我們。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;tong-chang-jia-ying-drone-cli&quot;&gt;同場加映 Drone CLI&lt;&#x2F;h2&gt;
&lt;p&gt;Drone CLI 是裝在本機電腦上的，負責和 Drone server 互動，能管理 Drone。&lt;&#x2F;p&gt;
&lt;p&gt;安裝：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; curl&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -L&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; https:&#x2F;&#x2F;github.com&#x2F;harness&#x2F;drone-cli&#x2F;releases&#x2F;latest&#x2F;download&#x2F;drone_linux_amd64.tar.gz&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; |&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; tar&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; zx&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo install&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -t&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &#x2F;usr&#x2F;local&#x2F;bin drone&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在 Windows，drone-cli 只能用 Scoop 裝，所以要先裝 Scoop，再裝 drone-cli，這部分請參考下列文件：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;scoop.sh&#x2F;&quot;&gt;Scoop&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.drone.io&#x2F;cli&#x2F;install&#x2F;&quot;&gt;Drone CLI 安裝文件&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;裝完後，配置連接到 Drone server 參數，在 ~&#x2F;.bashrc 加入以下內容：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;export&lt;&#x2F;span&gt;&lt;span&gt; DRONE_SERVER&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;http:&#x2F;&#x2F;192.168.0.226:3001&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;export&lt;&#x2F;span&gt;&lt;span&gt; DRONE_TOKEN&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;HnkG4wJYNY6D1VSVpKt1YGJqMVaMbuyf&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中的 token 在 Drone web 個人帳號頁可以看到。&lt;&#x2F;p&gt;
&lt;p&gt;配置後重新登入，試試：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; drone info&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;應該會看到自己的帳號資料。&lt;&#x2F;p&gt;
&lt;p&gt;詳細的用法請參閱 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.drone.io&#x2F;cli&#x2F;install&#x2F;&quot;&gt;Drone CLI 文件&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;can-kao-zi-liao&quot;&gt;參考資料&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.drone.io&#x2F;server&#x2F;provider&#x2F;gitea&#x2F;&quot;&gt;Drone server 與 Gitea 配置文件&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;can-liu-ke-ti&quot;&gt;殘留課題&lt;&#x2F;h2&gt;
&lt;p&gt;要怎麼讓 Drone server &#x2F; Drone runner 變成服務，並且自行啟動？&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>GitLab CI 從小白到入門</title>
        <published>2022-10-20T00:00:00+00:00</published>
        <updated>2022-10-20T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/gitlab-ci/"/>
        <id>https://editor.leonh.space/2022/gitlab-ci/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/gitlab-ci/">&lt;p&gt;GitLab CI 是 GitLab 的 CI 服務，本文分享 GitLab CI 的用法，以及配置文件 .gitlab-ci.yml 的寫法。&lt;&#x2F;p&gt;
&lt;p&gt;CI 是 continuous integration 的縮寫，華文叫持續整合或持續集成，通常還會與 continuous delivery、continuous deployment 摻在一起，有人把這些叫成 DevOps，儘管這些工程師黑話各有各的面向，但他們之間涵義上的重疊率真的太高，並且竊以為不斷發明新名詞、新詮釋對理解這些事物沒有幫助，所以我們反璞歸真，就稱之為 CI，因為它的發音比 DevOps 簡單，也不像那兩位 CD 同學會與生活中的 CD 撞名，所以就叫 CI 吧！&lt;&#x2F;p&gt;
&lt;h2 id=&quot;chi-xu-zheng-he-shen-mo&quot;&gt;持續整合…什麼？&lt;&#x2F;h2&gt;
&lt;p&gt;CI 是什麼，個人試著用一句話解釋－「&lt;strong&gt;提交程式碼後的自動化作業&lt;&#x2F;strong&gt;」。&lt;&#x2F;p&gt;
&lt;p&gt;自動化作業通常包括三大環節－建置、測試、佈署，具體要怎麼做這三項工作，以 GitLab 來說是由 .gitlab-ci.yml 內的敘述所定義。&lt;&#x2F;p&gt;
&lt;p&gt;以上是極簡 CI 解釋。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;wei-shen-mo-zuo-ci&quot;&gt;為什麼做 CI？&lt;&#x2F;h3&gt;
&lt;p&gt;因為資訊界目前主流的開發思想是&lt;strong&gt;持續改善&lt;&#x2F;strong&gt;，意即小步伐、快速迭代發佈新版本，目的是縮短 time to market 時間，然而帶來的副作用是每次的小變更都需要耗費大量的人力做建置、測試、佈署的工作，於是我們開始設法讓這三項工作能夠自動化作業。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Kaizen&quot;&gt;持續改善模式的建立源自於二戰時期美國為了滿足當時的軍事需求而誕生的一種快速改進製程的工作模式，並且發揚於戰後的日本&lt;&#x2F;a&gt;，在台灣，許多有日系背景的製造業也一直保有持續改善的工作模式。&lt;&#x2F;p&gt;
&lt;!-- 在本人過往從事的電子業五十年來一直以來都有著持續改善的模式，同事間也習以為常，不料轉個行變成一種被追捧的潮流，這點頗令人詫異。 --&gt;
&lt;h2 id=&quot;gitlab-ci&quot;&gt;GitLab CI&lt;&#x2F;h2&gt;
&lt;p&gt;前面提過，GitLab CI 的工作是在 .gitlab-ci.yml 檔案內所定義，而具體執行這些工作的機台稱為 runner，runner 通常都是虛擬機，runner 可以自己搭建也可以用 GitLab.com 提供的 shared runner，在 GitLab 專案的 CI&#x2F;CD 設定頁內可以看到這些 runner：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;gitlab-ci&#x2F;gitlab.com_settings_ci_cd.png&quot; alt=&quot;GitLab.com Shared Runners&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;這些 runner 的代碼或名稱並不重要，重要的是那些藍色的 tag，這些 tag 標註了一個 runner 的特性，例如後面我們會用到 Windows，屆時就會在 .gitlab-ci.yml 內去指定有 &lt;code&gt;windows&lt;&#x2F;code&gt; tag 的 runner 來執行 CI 的工作。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;stage-job-pipeline&quot;&gt;Stage &amp;amp; Job &amp;amp; Pipeline&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;gitlab-ci&#x2F;pipeline_graph_v13_6.png&quot; alt=&quot;GitLab CI Pipeline&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;前面提過的 CI 三大工作－建置、測試、佈署，在 GitLab CI 的術語稱為 stage，這些 stage 之間有著邏輯上先後次序的關係，建置成功才跑測試，測試成功才做佈署，這樣的先後關係在 GitLab CI 稱之為 pipeline，而上圖就是一個 GitLab 專案內的 pipeline，它會依照專案的 .gitlab-ci.yml 的配置自動產生，並且相當直覺的讓我們能一望即知目前的 CI 狀態。&lt;&#x2F;p&gt;
&lt;p&gt;Stage 是做為 CI 階段的單位，並未定義實際要執行的指令，具體要執行的指令則是定義在稱為 job 的單位內，以上圖來說 build stage 內有個 build-job、test stage 內則有兩個 job，以此類推。&lt;&#x2F;p&gt;
&lt;p&gt;前面提的 runner 就是 job 的執行器，要注意的是每個 job 的 runner 都是獨立的，在沒有特別配置下，彼此間是沒有共用檔案等資源的，並且在一個 job 結束後它的 runner 也隨之消滅，最後留給我們的只有 runner 跑 job 的狀態、log 等資訊，這點在寫 .gitlab-ci.yml 時要特別留意。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;gitlab-ci-yml&quot;&gt;.gitlab-ci.yml&lt;&#x2F;h3&gt;
&lt;p&gt;.gitlab-ci.yml 是 GitLab CI 用於定義前面提到過的幾個概念－stage、job 等，是整個 GitLab CI 的靈魂所在，如果曾經寫過 Dockerfile、docker-coompose.yml 或其他 provisioning 配置檔的朋友，看到 .gitlab-ci.yml 應該會冒出熟悉的&lt;del&gt;厭惡&lt;&#x2F;del&gt;感覺。&lt;&#x2F;p&gt;
&lt;p&gt;因為本人此刻剛好要建置一個 Windows 下的小玩具，所在這裡我們示範的也是以 Windows runner 為基礎的配置。&lt;&#x2F;p&gt;
&lt;p&gt;我們用的是 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;about.gitlab.com&#x2F;blog&#x2F;2020&#x2F;01&#x2F;21&#x2F;windows-shared-runner-beta&#x2F;&quot;&gt;GitLab Windows shared runner&lt;&#x2F;a&gt;，機器裏面已經幫我們&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;gitlab.com&#x2F;gitlab-org&#x2F;ci-cd&#x2F;shared-runners&#x2F;images&#x2F;gcp&#x2F;windows-containers&#x2F;-&#x2F;blob&#x2F;main&#x2F;cookbooks&#x2F;preinstalled-software&#x2F;README.md&quot;&gt;預裝好一些開發工具&lt;&#x2F;a&gt;，常用到的 C++ 編譯工具、.NET Runtime 等都已預裝。&lt;&#x2F;p&gt;
&lt;p&gt;Windows runner 與常用的 Linux runner 有著些許的不同：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Windows runner 的命令環境是 PowerShell。&lt;&#x2F;li&gt;
&lt;li&gt;在 GitLab Windows shared runner 內，預裝之一的工具有 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;chocolatey.org&#x2F;&quot;&gt;Chocolatey&lt;&#x2F;a&gt;，它是 Windows 世界的套件管理工具，我們可以用 Chocolatey 幫我們在 CLI 環境下安裝必要的程式。&lt;&#x2F;li&gt;
&lt;li&gt;Windows runner 並不是 Docker 容器，所以無法在 .gitlab-ci.yml 內指定 &lt;code&gt;image&lt;&#x2F;code&gt; 與 &lt;code&gt;services&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;參考以下範例做為我們 .gitlab-ci.yml 宇宙的起點：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;stages&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; build&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; test&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; deploy&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;default&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  tags&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; windows&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  before_script&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; Set-Variable -Name &amp;quot;time&amp;quot; -Value (date -Format &amp;quot;%H:%m&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; echo ${time}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; echo &amp;quot;started by ${GITLAB_USER_NAME}&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;build-job&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  stage&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; build&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  script&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; echo &amp;quot;running scripts in the build job&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;test-job1&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  stage&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; test&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  script&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; echo &amp;quot;running scripts in the test job 1&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;test-job2&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  stage&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; test&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  script&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; echo &amp;quot;running scripts in the test job 2&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;deploy-job&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  stage&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; deploy&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  script&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; echo &amp;quot;running scripts in the deploy job&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;先從前面已經提過的概念看起，最前面的是 &lt;code&gt;stages&lt;&#x2F;code&gt; 區塊，裡面定義了 &lt;code&gt;build&lt;&#x2F;code&gt;、&lt;code&gt;test&lt;&#x2F;code&gt;、&lt;code&gt;deploy&lt;&#x2F;code&gt; 三個 stage，他們的順序也決定了 CI 運作的順序，因此在 pipline 內看到的也會是 build &amp;gt; test &amp;gt; deploy 的順序。&lt;&#x2F;p&gt;
&lt;p&gt;第二大區塊 &lt;code&gt;default&lt;&#x2F;code&gt; 定義了後續那些 job 的預設值，首先 &lt;code&gt;tags&lt;&#x2F;code&gt; 用於指定 job 的 runner，以此範例來說，runner 必須有 &lt;code&gt;windows&lt;&#x2F;code&gt; 的 tag，才可執行 job。&lt;&#x2F;p&gt;
&lt;p&gt;接著我們關注那 &lt;code&gt;before_script&lt;&#x2F;code&gt; 區塊，顧名思義，&lt;code&gt;before_script&lt;&#x2F;code&gt; 內的命令會在每個 job 自己的命令之前先執行，一般 &lt;code&gt;before_script&lt;&#x2F;code&gt; 會用來安裝開發工具等等，例如在 GitLab Windows shared runner 裡面沒有預裝 Python，因此對一個 Python 專案而言，每個 job 都必然要先安裝 Python，像這樣的例子就會把安裝 Python 的命令寫在 &lt;code&gt;before_script&lt;&#x2F;code&gt; 區塊內，例如這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;default&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  tags&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; windows&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  before_script&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; choco install python --yes --version 3.9.6&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; $env:Path = &amp;quot;C:\Python39\;C:\Python39\Scripts\;&amp;quot; + $env:Path&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; python -m pip install --upgrade pip&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;default&lt;&#x2F;code&gt; 內的項目，在 job 內可以把它取代掉，例如在 &lt;code&gt;build-job&lt;&#x2F;code&gt; 我想指定用特定版次的 Windows，那我可以在 &lt;code&gt;build-job&lt;&#x2F;code&gt; 內另行設定它的 &lt;code&gt;tags&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;build-job&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  stage&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; build&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  tags&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; windows-1809&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  script&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; echo &amp;quot;running scripts in the build job&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;後面的四個 job，裡面的 &lt;code&gt;script&lt;&#x2F;code&gt; 區塊內則是定義該 job 要執行的命令，這就取決於各專案不同而自行發揮，這會是個充滿 try and error 的過程，祝您好運。&lt;&#x2F;p&gt;
&lt;p&gt;雖然目前這份 .gitlab-ci.yml 內容很陽春，但我們還是可以玩玩看感受一下，把 .gitlab-ci.yml 放到專案的資料夾內，再 commit 再 push 到 GitLab，開瀏覽器進 GitLab 的專案頁，在 CI&#x2F;CD 的頁面應該就可以看到這些 job 執行的狀態，也可以點進去看 job 的 log，他們正辛勤的為我們工作著呢：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Running with gitlab-runner 14.0.0 (3b6f852e)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  on windows-shared-runners-manager 6QgxEPvR&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Preparing the &amp;quot;custom&amp;quot; executor&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Using Custom executor with driver autoscaler 0.1.0 (495ee7a)...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Creating virtual machine for the job...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;以上，是 GitLab CI 的基礎使用方法。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-yu&quot;&gt;結語&lt;&#x2F;h2&gt;
&lt;p&gt;本文僅介紹了 GitLab CI 最陽春的使用與配置方法，陽春歸陽春，但也能滿足一般人的八成需求，比起看原廠那好幾個單元的文件要容易入門的多，如果對您有幫助，請幫我按讚、訂閱、開啟小鈴鐺。&lt;&#x2F;p&gt;
&lt;p&gt;最後，完整的 .gitlab-ci.yml 語法請參見〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.gitlab.com&#x2F;ee&#x2F;ci&#x2F;yaml&#x2F;&quot;&gt;Keyword reference for the .gitlab-ci.yml file&lt;&#x2F;a&gt;〉。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>十分鐘自架 Gitea</title>
        <published>2022-10-19T00:00:00+00:00</published>
        <updated>2022-10-19T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/gitea/"/>
        <id>https://editor.leonh.space/2022/gitea/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/gitea/">&lt;p&gt;想要自架 Git server，主流的選擇大概不脫 Gitea 和 GitLab，雖然本人一直以來都是 GitLab 的愛用者，但若是自架，就要多考慮兩下了，首先 GitLab 本身就是個大玩具，不論是架設還是維護都相對複雜一點，特別是維護，它離射後不理應該有段距離，而 Gitea 的優點自然就是精緻小巧卻又功能齊全啦，扣掉配置文件，就只有一個扎扎實實的執行檔，資料庫支援 SQLite，也是精緻小巧又功能齊全，這樣的組合正滿足本人射後不理的需求啦。&lt;&#x2F;p&gt;
&lt;p&gt;Gitea 的網頁也很明白的呈現自己的特點：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;gitea&#x2F;gitea.io_zh-tw_.png&quot; alt=&quot;Gitea&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Gitea 唯一缺的是沒有內建 CI，不過這不打緊，市面上也有一大堆的 CI 可與之串接，最多人用來和 Gitea 搭配的 CI 應該就是 Drone CI 了，總之雖然 Gitea 沒有 CI，但不會是個問題。&lt;&#x2F;p&gt;
&lt;p&gt;認識 Gitea 之後，讀者應該也可以拿捏拿捏 Gitea 適不適合自己了，接著往下看安裝與配置的部分。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gitea-an-zhuang-yu-pei-zhi&quot;&gt;Gitea 安裝與配置&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;an-zhuang&quot;&gt;安裝&lt;&#x2F;h3&gt;
&lt;p&gt;最簡單的安裝不是 Docker，而是 Snap，只要一行：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo snap install gitea&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;裝完之後它自己會跑起來，進入 http:&#x2F;&#x2F;127.0.0.1:3000&#x2F; 進行初始化作業，初始化頁面也很簡單，在此不多提。&lt;&#x2F;p&gt;
&lt;p&gt;初始化頁面跑完應該就可以登入了，登入後長的和以前的 GitHub 有八成像，有用過 GitHub &#x2F; GitLab 的朋友應該都可以無痛上手。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;pei-zhi&quot;&gt;配置&lt;&#x2F;h3&gt;
&lt;p&gt;Gitea 的配置文件在 &#x2F;var&#x2F;snap&#x2F;gitea&#x2F;common&#x2F;conf&#x2F;app.ini，裡面項目頗多，一般來說不需要動。&lt;&#x2F;p&gt;
&lt;p&gt;但是但是 SSH 的部分可能需要改一下。&lt;&#x2F;p&gt;
&lt;p&gt;因為 Linux 原本就有開 SSH 服務，所以埠 22 已經被占用，本著一埠做一事的原則，把 Gitea SSH 埠改到 23。&lt;&#x2F;p&gt;
&lt;p&gt;把那 app.ini 開起來，確保 [server] 區段內有以下幾行：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;ini&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;[server]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;DISABLE_SSH&lt;&#x2F;span&gt;&lt;span&gt;      = false&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;START_SSH_SERVER&lt;&#x2F;span&gt;&lt;span&gt; = true&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SSH_PORT&lt;&#x2F;span&gt;&lt;span&gt;         = 23&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這幾行應該都能望文生義。&lt;&#x2F;p&gt;
&lt;p&gt;改完後重啟服務，Gitea 的服務名稱為 snap.gitea.web.service：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo systemctl restart snap.gitea.web.service&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;看一下狀態：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo systemctl status snap.gitea.web.service&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;狀態為綠字就沒問題，再用 Git SSH 試試 pull &#x2F; push，沒問題就沒問題。&lt;&#x2F;p&gt;
&lt;p&gt;跑到這邊差不多十分鐘，收工前來聊聊為什麼要自架 Git server。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wei-he-zi-jia-git-server&quot;&gt;為何自架 Git Server？&lt;&#x2F;h2&gt;
&lt;p&gt;前面都是自架 Gitea 的 what &amp;amp; how，這邊談談 why，GitLab &#x2F; GitHub 這麼香，又有免費的 CI 額度，為什麼還要自架 Git server（和 Drone CI）？&lt;&#x2F;p&gt;
&lt;p&gt;不是因為免費的 CI 不夠用，想要無限 CI runner 大可只架 CI 再串接 gitlab.com 就好。&lt;&#x2F;p&gt;
&lt;p&gt;自架 Git server 背後令人暖心的原因是組織的資安政策，他們認為&lt;strong&gt;把 Git server、xxx server 關在內網，就會很安全&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;這種資訊鎖國政策有沒有用，看看新冠疫情就知道了，總之、反正，就是這樣。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Heroku 大逃殺之 Render</title>
        <published>2022-09-01T00:00:00+00:00</published>
        <updated>2022-09-01T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/render/"/>
        <id>https://editor.leonh.space/2022/render/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/render/">&lt;p&gt;今天是本人從 Heroku 跑路的第三天 :p，也是本小站日更的第三天，今天又又又又又要帶大家認識一下位於日本東京二十三區之…錯頻…，今天要認識 Render，它也是個 PaaS 服務，前一篇 Koyeb 總是感覺不夠傻瓜、又只支援 GitHub，而且從建置到部署這段期間的狀態標示有點不到位，就看看 Render 能不能滿足挑剔的我囉！&lt;&#x2F;p&gt;
&lt;p&gt;在傻瓜度方面，Render 看起來是很有自信的，首頁頭版下面就強打步驟 1-2-3：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;render&#x2F;Render-1-2-3.png&quot; alt=&quot;Render 1-2-3&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;實際上的感受也是如此，建立新站台時的畫面更簡單了，餵給它某個 GitLab &#x2F; GitHub 專案，它就能識別出專案的語言是 Python，套件用 pip 安裝，要填的幾乎只有站台名稱：&lt;&#x2F;p&gt;
&lt;div class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;render&#x2F;Render-New.png&quot; alt=&quot;Render - New web service&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;圖中也可以看到 Render 各方案的機台規格和金額，Starter 每月 7 美金，CPU 只有半顆…，果然世界上沒有完美的東西。&lt;&#x2F;p&gt;
&lt;p&gt;回頭看 Free 方案，CPU 更只寫著「shared」，實際上蒜粒多強大不可知，但 Render 與 Heroku 相同的是會讓服務進入休眠，並且 Render 的時間更嚴苛，15 分鐘沒人用就休眠，那說不出口的 CPU 蒜粒，就個人感受，醒來的時間比 Heroku 還要久，看來真的只能用於玩具呢！&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Heroku 大逃殺之 Koyeb</title>
        <published>2022-08-30T00:00:00+00:00</published>
        <updated>2022-08-30T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/koyeb/"/>
        <id>https://editor.leonh.space/2022/koyeb/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/koyeb/">&lt;!-- | Vendor                                             | Plan         | Price      | RAM  | CPU | Note          | 
| ---------------------------------------------------|--------------|------------|-----:|:---:|---------------|
| Heroku                                             | Free         | Free       | 512  |     | 即將終止       |
| Heroku                                             | Hobby        | $7 &#x2F; month | 512  |     |               |
| Render                                             | Free         | Free       | 512  |     |               |
| Render                                             | Starter      | $7 &#x2F; month | 512  |     |               |
| Qoddi                                              | Dev          | Free       | 256  | 1   |               |
| Qoddi                                              | Starter      | $6 &#x2F; month | 1024 | 1   |               |
| [Cyclic.sh](https:&#x2F;&#x2F;app.cyclic.sh&#x2F;#&#x2F;join&#x2F;Leon0824) | Free Forever | Free       | 1024 | ?   | 僅支援 Node.js |
| [Cyclic.sh](https:&#x2F;&#x2F;app.cyclic.sh&#x2F;#&#x2F;join&#x2F;Leon0824) | Solo         | $9 &#x2F; month | 1024 | ?   | 僅支援 Node.js | --&gt;
&lt;p&gt;人說狡兔有三窟，加上始終搞不定在 &lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;koyeb&#x2F;@%5C2022%5C2022-08-29-Fly.io%5Cindex.md&quot;&gt;Fly.io&lt;&#x2F;a&gt; 安裝字體，於是這一站我們來到 Koyeb。&lt;&#x2F;p&gt;
&lt;p&gt;Koyeb 這個不知道怎麼發音的服務，相較於 Fly.io 規模較小，免費版每月有 5 美金額度可供使用，只夠開一台它的 NANO 機，各機種規格可見下圖：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;koyeb&#x2F;koyeb-instances.png&quot; alt=&quot;Koyeb instances&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;雖然 NANO 只有 256 MB 記憶體，跑點沒人用的服務還算可以。&lt;&#x2F;p&gt;
&lt;p&gt;至於機房位置呢，目前只有巴黎，同樣的，對沒人用的服務來說，遠在歐洲也是可接受的，還多了點浪漫元素 😚。&lt;&#x2F;p&gt;
&lt;p&gt;Koyeb 建立服務的流程比較像 Netlify &#x2F; Vercel，頗為直覺，不像 Fly.io 需要繁瑣的配置文件，要填的資料在一個畫面可以全部搞定：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;koyeb&#x2F;koyeb-create-service.png&quot; alt=&quot;Koyeb Creating A Service&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;如上圖所見，它支援從 GitHub 匯入專案，也支援用 Docker 映像創建服務。&lt;&#x2F;p&gt;
&lt;p&gt;預設的埠號在 8080，也可隨喜配置。&lt;&#x2F;p&gt;
&lt;p&gt;沒了。&lt;&#x2F;p&gt;
&lt;p&gt;服務建置期間的過程有即時 log 可看，頗流暢，反倒是建置完部署到他們的節點這個過程頗慢，而且進度狀態有點模糊，總之建置完如果好像連不上，或狀態燈號異常（unhealthy），那等個十分鐘左右再次確認狀態，可能就好了。&lt;&#x2F;p&gt;
&lt;p&gt;這篇文頗短，因為這種傻瓜型服務使用上相當簡單，真的沒什麼好寫的，相較之下 Heroku &#x2F; Fly.io 像是上個世紀的產物。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Heroku 大逃殺之 Fly.io</title>
        <published>2022-08-29T00:00:00+00:00</published>
        <updated>2022-08-29T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/fly-io/"/>
        <id>https://editor.leonh.space/2022/fly-io/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/fly-io/">&lt;p&gt;Heroku 打算在 2022 年 11 月 28 終止所有免費服務，我輩免費仔只能付費升級，或是搬家，既然是免費仔，付費升級就不是個選項，要搬家的話，新家的選擇大概有這些：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Render&lt;&#x2F;li&gt;
&lt;li&gt;Fly.io&lt;&#x2F;li&gt;
&lt;li&gt;Northflank&lt;&#x2F;li&gt;
&lt;li&gt;Koyeb&lt;&#x2F;li&gt;
&lt;li&gt;Deta&lt;&#x2F;li&gt;
&lt;li&gt;Qoddi&lt;&#x2F;li&gt;
&lt;li&gt;Cyclic.sh&lt;&#x2F;li&gt;
&lt;li&gt;Railway&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;這篇記錄了搬家到 Fly.io 的過程。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fly-io&quot;&gt;Fly.io&lt;&#x2F;h2&gt;
&lt;p&gt;Heroku、Fly.io 以及前面列的一大堆服務，都可以概括為 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;doublesllash.com&#x2F;blog&#x2F;posts&#x2F;2022&#x2F;serverless&quot;&gt;serverless&lt;&#x2F;a&gt; 服務，更精確的分類是 PaaS，其中的 &lt;strong&gt;P&lt;&#x2F;strong&gt; 為 platform，所謂 platform 表示語言／框架的複合意義，PaaS 的優勢在於我們不用去管理更底層的 infrustructure，也就是硬體層、OS 層、web server 層都無須操心，由 PaaS 業者自理。&lt;&#x2F;p&gt;
&lt;p&gt;Fly.io 的特色個人認為有二：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;有東京節點，離台灣較近，速度當然也比較快囉！&lt;&#x2F;li&gt;
&lt;li&gt;初階費率低廉，雖然本人是免費仔，還是要考慮付費時的費率方案，以 Fly.io 單一共享 CPU 的費率來看，還是頗便宜的。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;fly-io&#x2F;fly.io_docs_about_pricing.png&quot; alt=&quot;Fly.io Pricing&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;qian-zhi-zuo-ye&quot;&gt;前置作業&lt;&#x2F;h2&gt;
&lt;p&gt;第一步當然是開帳號，眼睛閉閉帳號開一開之後，與 Heroku 類似，要裝 Fly.io 的 CLI 工具。&lt;&#x2F;p&gt;
&lt;p&gt;如果是 Windows，不需要 Administrator 權限，開 PowerShell 執行此命令：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;powershell&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;$ iwr https:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&#x2F;&#x2F;&lt;&#x2F;span&gt;&lt;span&gt;fly.io&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&#x2F;&lt;&#x2F;span&gt;&lt;span&gt;install.ps1 &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;-&lt;&#x2F;span&gt;&lt;span&gt;useb &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;|&lt;&#x2F;span&gt;&lt;span&gt; iex&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;裝好就登入吧：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;powershell&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;$ flyctl auth login&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;至此前置作業告一段落，後面我們進入專案目錄配置 Fly.io 設定。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jian-li-fly-io-app&quot;&gt;建立 Fly.io App&lt;&#x2F;h2&gt;
&lt;p&gt;我們這整個專案對 Fly.io 來說就是一套 app，最基本的 app 裡面大概會跑一個 web 服務，稍微複雜一點的大概會多跑一個 database 服務，在本文的範例中，我們只有一個 web 服務。&lt;&#x2F;p&gt;
&lt;p&gt;先初始化一個 Fly.io app：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;powershell&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;$ flyctl launch&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面的子命令為 &lt;code&gt;launch&lt;&#x2F;code&gt;，但它其實只是產生配置檔而已，並沒有真的 launch 什麼。&lt;&#x2F;p&gt;
&lt;p&gt;跑起來會跳出一些資訊還有問題：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Detected a Python app&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Using the following build configuration:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        Builder: paketobuildpacks&#x2F;builder:base&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;? Overwrite &amp;quot;C:\Users\leonh\Projects\katsuyo-backend\Procfile&amp;quot;? Yes&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;? App Name (leave blank to use an auto-generated name): katsuyo&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;? App Name (leave blank to use an auto-generated name): katsuyo&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;? Select organization: Katsuyo (katsuyo)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;? Select region: nrt (Tokyo, Japan)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Created app katsuyo in organization katsuyo&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Wrote config file fly.toml&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;? Would you like to set up a Postgresql database now? No&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;We have generated a simple Procfile for you. Modify it to fit your needs and run &amp;quot;fly deploy&amp;quot; to deploy your application.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;整理如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;自動偵測到這是個 Python 專案。&lt;&#x2F;li&gt;
&lt;li&gt;要改寫 Procfile 嗎？Yes（反正有版控不怕不怕）。&lt;&#x2F;li&gt;
&lt;li&gt;幫這個 Fly.io app 取名字。&lt;&#x2F;li&gt;
&lt;li&gt;選定要放在 Fly.io 的哪個組織下，每個 Fly.io 帳號都有一個自己的「Personal」組織，當然也可以另外創建更多組織，本例的組織就是另外創的 Katsuyo，這樣感覺事業做比較大，比較唬人。&lt;&#x2F;li&gt;
&lt;li&gt;要放在哪個機房，東京應該是首選。&lt;&#x2F;li&gt;
&lt;li&gt;以上設定都寫入 fly.toml 和 Procfile，還沒真正部署，可以再改。&lt;&#x2F;li&gt;
&lt;li&gt;最後一題，要設定 PostgreSQL 嗎？不用。&lt;&#x2F;li&gt;
&lt;li&gt;最後提示我們用 &lt;code&gt;fly deploy&lt;&#x2F;code&gt; 部署，不過最好先檢查一下配置再說。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;先看 Procfile：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# Modify this Procfile to fit your needs&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;web: gunicorn server:app&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;格式和 Heroku 相同，印象中這是來自某個開源專案的通用格式，但是是哪個忘記了。&lt;&#x2F;p&gt;
&lt;p&gt;它幫我們配置了用 Gunicorn 來跑專案，但在這裡的範例專案中用的是 Uvicorn，所以改一下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;# Modify this Procfile to fit your needs&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;web: uvicorn app.main:app --host 0.0.0.0 --port 8080&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;此處設定 Uvicorn 與參數：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;指定了 Python ASGI 程式進入點，這部分請根據自身專案而改。&lt;&#x2F;li&gt;
&lt;li&gt;不綁定任何 IP，因為我們不知道，也不需要知道 Fly.io 給的 IP 是什麼。&lt;&#x2F;li&gt;
&lt;li&gt;監聽 8080 埠，因為在 fly.toml 中，預設配置就是把 HTTP、HTTPS 請求送到機台內的 8080 埠。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Procfile 的部分告一段落，來看看 fly.toml 吧：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;toml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# fly.toml file generated for katsuyo on 2022-08-27T11:38:16+08:00&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;app =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;katsuyo&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;kill_signal =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;SIGINT&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;kill_timeout =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 5&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;processes = []&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;build&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  builder =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;paketobuildpacks&#x2F;builder:full&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;env&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  PORT =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;8080&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;experimental&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  allowed_public_ports = []&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  auto_rollback =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; true&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;services&lt;&#x2F;span&gt;&lt;span&gt;]]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  http_checks = []&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  internal_port =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 8080&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  processes = [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;app&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  protocol =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;tcp&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  script_checks = []&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;services&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;concurrency&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    hard_limit =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 25&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    soft_limit =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 20&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    type =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;connections&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  [[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;services&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;ports&lt;&#x2F;span&gt;&lt;span&gt;]]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    force_https =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; true&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    handlers = [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;http&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    port =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 80&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  [[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;services&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;ports&lt;&#x2F;span&gt;&lt;span&gt;]]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    handlers = [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;tls&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;http&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    port =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 443&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  [[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;services&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;tcp_checks&lt;&#x2F;span&gt;&lt;span&gt;]]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    grace_period =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;1s&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    interval =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;15s&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    restart_limit =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    timeout =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;2s&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;此處多數的配置可以望文生義，其中幾行要關注一下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;前面提過的 &lt;code&gt;internal_port = 8080&lt;&#x2F;code&gt; 指定了 Python app 應該監聽 8080 埠。&lt;&#x2F;li&gt;
&lt;li&gt;那 &lt;code&gt;[env]&lt;&#x2F;code&gt; 區塊內的設定，是讓 app 可以讀取到系統內的環境變數，可隨喜配置。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;builder = &quot;paketobuildpacks&#x2F;builder:base&quot;&lt;&#x2F;code&gt; 指定了運作時的容器映像，預設的 &lt;code&gt;base&lt;&#x2F;code&gt; 沒有 C 函式庫，本範例中的 Python 剛好有需要 SQLite，libsqlite3.so.0 是一定要的，所以得改為 &lt;code&gt;paketobuildpacks&#x2F;builder:full&lt;&#x2F;code&gt; 。這部分詳細說明參閱 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;paketo.io&#x2F;docs&#x2F;concepts&#x2F;builders&#x2F;&quot;&gt;Paketo Buildpack 文件&lt;&#x2F;a&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;fly.toml 的其他部分先擱著，以現有的設定部署看看：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;powershell&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;$ flyctl deploy&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到一堆嘰哩呱啦訊息，大部分是層層容器映像下載和部署的訊息。&lt;&#x2F;p&gt;
&lt;p&gt;如果一切沒問題，應該跑完就上線了，恭喜，灑花，轉圈圈！&lt;&#x2F;p&gt;
&lt;p&gt;但實際上不會這麼順利，否則我早就財富自由了。通常初次部署都會遇到各種疑難雜症，只能根據部署丟出的錯誤訊息來一一排除囉！&lt;&#x2F;p&gt;
&lt;p&gt;以我手上的範例專案來說，它就需要安裝字體，無奈 Fly.io 用的 builder &#x2F; buildpack 架構下似乎沒有夠簡單的方法能讓我跑 &lt;code&gt;apt install fonts-noto-cjk&lt;&#x2F;code&gt;，所以雖然部署成功，但其中字體的顯示卻是異常的，殘念。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;cli-chang-yong-ming-ling&quot;&gt;CLI 常用命令&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;看當前專案 app 資訊&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;powershell&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;$ flyctl info&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到當前專案 app 的諸元：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;App&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  Name     = katsuyo&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  Owner    = katsuyo&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  Version  = 3&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  Status   = running&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  Hostname = katsuyo.fly.dev&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Services&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;PROTOCOL PORTS&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;TCP      80 =&amp;gt; 8080 [HTTP]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         443 =&amp;gt; 8080 [TLS, HTTP]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;IP Adresses&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;TYPE ADDRESS             REGION CREATED AT&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;v4   168.220.91.169             11h34m ago&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;v6   2a09:8280:1::1:657d        11h34m ago&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;另一個類似的命令：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;powershell&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;$ flyctl status&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;App&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  Name     = katsuyo&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  Owner    = katsuyo&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  Version  = 3&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  Status   = running&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  Hostname = katsuyo.fly.dev&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  Platform = nomad&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Instances&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;ID              PROCESS VERSION REGION  DESIRED STATUS  HEALTH CHECKS           RESTARTS        CREATED&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;6bcb55a9        app     3       nrt     run     running 1 total, 1 passing      0               6h7m ago&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;個人認為這兩個命令大可合併，或作為彼此的替身。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;看 log&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;powershell&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;$ flyctl logs&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;當然就是看專案即時 log 啦！&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-yu&quot;&gt;結語&lt;&#x2F;h2&gt;
&lt;p&gt;簡單的試用一下 Fly.io，初步的感受是配置有點繁雜，那 fly.toml 看起來好像很厲害，但本人更愛好像 Netlify &#x2F; Vercel 那樣的傻瓜配置，如果 PaaS 還要搞那麼多配置那不如回到 IaaS 自己搭架構就好，想單體就單體，想微服務就微服務，爽快。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>一個譯者的體驗</title>
        <published>2022-08-14T00:00:00+00:00</published>
        <updated>2022-08-14T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/translator/"/>
        <id>https://editor.leonh.space/2022/translator/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/translator/">&lt;p&gt;2022 年，我成為了碁峰的外包翻譯，這也是本人對翻譯的初次體驗，這裡會分享關於這件事的契機、工作準備、心得等等，對未來有心從事專科翻譯的人可能有點幫助。&lt;&#x2F;p&gt;
&lt;p&gt;先介紹一下書籍吧，原文書是《&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.informit.com&#x2F;store&#x2F;principles-of-web-api-design-delivering-value-with-9780137355631&quot;&gt;Principles of Web API Design&lt;&#x2F;a&gt;》&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;translator&#x2F;ShowCover.aspx.jpeg&quot; alt=&quot;Principles of Web API Design&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：《Principles of Web API Design》&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;華文版的書名則是《&lt;a rel=&quot;external&quot; href=&quot;http:&#x2F;&#x2F;books.gotop.com.tw&#x2F;v_ACL065500&quot;&gt;Web API 設計原則&lt;&#x2F;a&gt;》。&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;translator&#x2F;ACL065500.jpg&quot; alt=&quot;Web API設計原則&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：《Web API設計原則》&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;這本書理所當然的談的是 API。關於書目的選擇，出版社的窗口會根據專長給出幾本候選，在這個階段我們僅能憑書名、介紹、目錄、頁數等有限的資訊去決定，對譯者來說，選自己熟悉的主題是比較保險的，但人多半又想在翻譯之餘也能學到新知識，此時該往外跨出多少就是值得思考的。&lt;&#x2F;p&gt;
&lt;p&gt;舉例來說，Python 在機器學習領域很夯，但對一個終日和 CRUD 搏鬥的後端開發者來說，在相關基礎知識不夠健全的情況下，要跨界去翻機器學習書籍可能就太過困難，有無法完稿的可能性，因此還是要衡量自身的能力來決定。&lt;&#x2F;p&gt;
&lt;p&gt;回到本書的例子，本人對 API 的認識還算可以，之前也寫過幾篇與本書內容相關的文章：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;oauth2-implicit-flow&#x2F;&quot;&gt;OAuth2 極簡攻略（一）Implicit Flow&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;twitter-oauth-1&#x2F;&quot;&gt;以 Authlib 實現 OAuth 1 的 Twitter 登入&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;openapi-ts&#x2F;&quot;&gt;真・用 OpenAPI 打通前後端任督二脈&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;openapi&#x2F;&quot;&gt;OpenAPI 打通前後端任督二脈&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;apiary&#x2F;&quot;&gt;Apiary API 規格文件＋假接口一次到位&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;k6&#x2F;&quot;&gt;認識負載測試與 k6&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;除了這些自己還算了解的領域外，在這次的翻譯過程中也更完整的認識了 REST，特別是它「架構性約束」、「hypermedia」等的觀念，還有其他像是「OData」、「SSE（server-sent events）」等之前沒有深入接觸過的技術也都在翻譯的過程中豐富了我對他們的理解。&lt;&#x2F;p&gt;
&lt;p&gt;書籍的部份談到這邊，下面回到翻譯的主題。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ru-he-cheng-wei-fan-yi&quot;&gt;如何成為翻譯&lt;&#x2F;h2&gt;
&lt;p&gt;以碁峰來說，流程如下：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;報名&lt;&#x2F;strong&gt;，姓名、聯絡方式、專長領域等等很基本的資料。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;聯繫&lt;&#x2F;strong&gt;，如果對方覺得可以的話，會跟你聯繫，以本人為例，去年就有報名一次，但今年又報了一次才有聯繫。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;選書&lt;&#x2F;strong&gt;，如上所述，從幾本候選的書目中挑一本有信心完成的。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;試譯&lt;&#x2F;strong&gt;，對方會指定大約十頁內文＋封面＋封底＋目錄給我們試譯，期限大約是一週，其中目錄看似字最少但其實是最難的，往往要真的去讀過該章節的內文才能準確的下出適當的標題，在翻譯目錄的同時也是讓我們自己搞清楚整本書脈絡的機會。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;審閱&lt;&#x2F;strong&gt;，文字編輯會根據試譯稿的優劣來決定是否要交給我們翻譯，即便通過，他們也會對試譯稿內容提出意見，此時就要根據文編的意見反省自己的譯文，並做出調整。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;簽約&lt;&#x2F;strong&gt;，合約上載明雙方權利義務，特別是截稿日、酬勞等都要確認，以碁峰來說，本身就是有知名度的出版社，合約也是中規中矩沒有特別偏袒某一方，與之相對的，Hahow 的合約就嚴重偏袒 Hahow 方，總之無論如何，簽署任何合約之前務必都要逐條確認內容。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;經過以上流程，就可以開始翻譯了，但要成為列名於&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;isbn.ncl.edu.tw&#x2F;NEW_ISBNNet&#x2F;&quot;&gt;全國新書資訊網&lt;&#x2F;a&gt;的譯者，那還得要交得了稿才行。&lt;&#x2F;p&gt;
&lt;p&gt;交稿後並沒有海闊天空，即使在交稿前已經自我校驗過，一定還是會有諸多的錯誤，這些錯誤可能是小到標點符號、錯字、贅字、缺字，大到錯譯、漏譯，不論大小，對讀者來說都是不可接受的缺陷，本人早年任職於絆腳石文創集團時，他們的每季文宣規定要校稿三次，即使如此，往往事後還是會發現錯誤。做為更專業的出版社，碁峰的校稿標準當然只會更高，他們有專業的文字編輯認真考核你的一字一句，包括標點符號以及構句的正確性，還有專業的外校前輩會挑出你所有誤譯、漏譯之處，作為一個新手翻譯，零零總總的缺陷大約有一千個吧，真是幸虧有專業的編輯和外校才讓本書的品質得以確保。&lt;&#x2F;p&gt;
&lt;p&gt;對一個專科譯者來說，要交稿需要三方面的能力：專業能力、英文能力、華文能力，而其中最重要的是華文能力，英文和專業反而是次之了，這部份留到後面再細談，先轉個話題談翻譯的執行與效率。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fan-yi-de-ruan-ying-ti&quot;&gt;翻譯的軟硬體&lt;&#x2F;h2&gt;
&lt;p&gt;要有效率的翻譯，在硬體方面，建議裝個雙螢幕，主螢幕擺編輯器，次螢幕擺原文 PDF，這樣可以省去大量切換畫面的瑣碎動作，雖然出版社有給我一本實體的原文書，但用到它的機會真的不多。&lt;&#x2F;p&gt;
&lt;p&gt;在軟體方面，曾經試過 Word、Notion、Visual Studio Code + Markdown，最後選的是 Google 文件，原因如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;最不卡頓&lt;&#x2F;strong&gt;，Word 本身就不是為書籍而設計的，頁數一多就卡頓，而且譯稿不需要樣式，沒有特別用 Word 的必要。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;即時存檔&lt;&#x2F;strong&gt;，全雲端化，不用擔心檔案遺失等雜七雜八的問題，還可以保有多個版本。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;可放圖表&lt;&#x2F;strong&gt;，雖然譯稿不用加樣式，但圖表還是要放的，Markdown 雖然也可以放圖表，但會打斷工作的沈浸感。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;綜合考量上述問題之後，最後選擇了 Google 文件，中間也有去試過某個專業的 CAT（computer-assisted translation）應用 Termsoup，但它吃不了 PDF，沒兩下就放棄了，回到 Google 文件的懷抱。&lt;&#x2F;p&gt;
&lt;p&gt;通常那些 CAT 應用會整合 Google 翻譯之類的工具，反而 Google 文件僅有整合自家的字典，卻沒有整合自家的翻譯服務，總之又開一個 Google 翻譯頁面當成我的小助手。&lt;&#x2F;p&gt;
&lt;p&gt;除了 Google 之外，中間也試過各家的翻譯服務，Bing、有道、IBM 等，試了一輪下來，這些 AI 機翻都不夠到位，只有 Google 算是稍微好一點的，這也是為什麼譯者這個職業還存在的原因吧 :)。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;上面談的是電腦的軟硬體，但從另一個角度來說，在翻譯的工作中，電腦軟硬體都是翻譯的硬體，唯有人才是翻譯的軟體，並且真正影響效率的還是人的層面。&lt;&#x2F;p&gt;
&lt;p&gt;以本人來說，一日最低的前進頁數是 0，最高是 10，平均是 6。&lt;&#x2F;p&gt;
&lt;p&gt;其中的 0，並非休假的意思，而是&lt;strong&gt;應該工作的日子卻毫無產出&lt;&#x2F;strong&gt;，這樣的日子比例是多少呢？大約是 1&#x2F;10，也就是說大約每十天就有一天會零產出，原因不盡然是偷懶，有很多預料外的「假期」，例如掃墓、照顧家人、生病、打疫苗、家族聚會等等，這些都是無法逃避的事務，只能盡量及早安排、及早準備，取得工作和生活之間的平衡。&lt;&#x2F;p&gt;
&lt;p&gt;如果是兼職翻譯，在工作、翻譯、生活三頭燒之下，三者的平衡就更為重要了，假設一位兼職譯者，週一至週五每日可前進 1 頁，週六日每日可前進 6 頁，那一本 400 頁的書要 23 週才可翻完，這意味著他將連續 23 週完全沒有晚上和週末的休閒時光，這是將近半年的時間，如果是你，你或家人受得了嗎？如果出現意外行程該如何？如果正職工作又有加班那又該如何？&lt;&#x2F;p&gt;
&lt;p&gt;談完了翻譯的工具和效率，最後來談談「翻譯」這件事的本質。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fan-yi-nai-da-dao-yi-zhe-du-qiao-cui&quot;&gt;翻譯乃大道 譯者獨憔悴&lt;&#x2F;h2&gt;
&lt;p&gt;上面的標題來自余光中翻譯論集《&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.chiuko.com.tw&#x2F;product&#x2F;%E7%BF%BB%E8%AD%AF%E4%B9%83%E5%A4%A7%E9%81%93%EF%BC%8C%E8%AD%AF%E8%80%85%E7%8D%A8%E6%86%94%E6%82%B4%EF%BC%9A%E4%BD%99%E5%85%89%E4%B8%AD%E7%BF%BB%E8%AD%AF%E8%AB%96%E9%9B%86&#x2F;&quot;&gt;翻譯乃大道 譯者獨憔悴&lt;&#x2F;a&gt;》，這本書談他對翻譯的看法、翻譯的本質、翻譯的技巧、翻譯的取捨。&lt;&#x2F;p&gt;
&lt;p&gt;他是這麼說的：&lt;&#x2F;p&gt;
&lt;blockquote class=&quot;wide&quot;&gt;
&lt;div class=&quot;quote&quot;&gt;
翻譯也是一種創作，至少是一種「有限的創作」。同樣，創作也可以視為一種「不拘的翻譯」或「自我的翻譯」。在這種意義下，作家在創作時，可以說是將自己的經驗「翻譯」成文字。（讀者欣賞那篇作品，過程恰恰相反，是將文字「翻譯」回去，還原成經驗。）不過這種「翻譯」，和譯者所做的翻譯，頗不相同。譯者在翻譯時，也要將一種經驗變成文字，但那種經驗已經有人轉化成文字，而文字化了的經驗已經具有清晰的面貌和確定的涵義，不容譯者擅加變更。譯者的創造性所以有限，是因為一方面他要將那種精確的經驗「傳真」過來，另一方面，在可能的範圍內，還要保留那種經驗賴以表現的原文。這種心智活動，似乎比創作更繁複些。
&lt;&#x2F;div&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;要對翻譯的本質下一個詮釋，我想不可能比余光中老師說的更好的了。&lt;&#x2F;p&gt;
&lt;p&gt;翻譯的本質是「有限的創作」，回到本文最開頭說的，做為一個專科譯者，除了專業能力、英文能力，最重要的其實是自身的華文能力，華文能力的優劣表現在對譯文「傳真度」的高低，又同時要兼顧適當的本地化，這中間牽涉的是無數的取捨。&lt;&#x2F;p&gt;
&lt;p&gt;舉一個之前在另一篇〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;jira-story&#x2F;&quot;&gt;如何理解 Jira 的 Story&lt;&#x2F;a&gt;〉裡面的例子，story 標題在英文的固定語句是：&lt;&#x2F;p&gt;
&lt;p style=&quot;font-size: large;&quot;&gt;「As a &lt;strong&gt;XXX&lt;&#x2F;strong&gt;, I want &lt;strong&gt;YYY&lt;&#x2F;strong&gt; feature so that &lt;strong&gt;ZZZ&lt;&#x2F;strong&gt;.」
&lt;&#x2F;p&gt;
&lt;p&gt;未經思索的翻譯會是這樣：&lt;&#x2F;p&gt;
&lt;p style=&quot;font-size: large;&quot;&gt;「身為一位 &lt;strong&gt;誰誰誰&lt;&#x2F;strong&gt;，我需要 &lt;strong&gt;某某某&lt;&#x2F;strong&gt; 功能才能 &lt;strong&gt;這般這般&lt;&#x2F;strong&gt;。」&lt;&#x2F;p&gt;
&lt;p&gt;但若仔細思考這段華文構句，其實是充滿贅字的，「身為一位…我需要」這部份大可刪去也完全不影響整句話的意思，而且更顯得簡潔，像這樣：&lt;&#x2F;p&gt;
&lt;p style=&quot;font-size: large;&quot;&gt;「&lt;strong&gt;某某某&lt;&#x2F;strong&gt; 要 &lt;strong&gt;如此如此&lt;&#x2F;strong&gt; 才能 &lt;strong&gt;這般這般&lt;&#x2F;strong&gt;。」&lt;&#x2F;p&gt;
&lt;p&gt;當然這只是最簡單的例子，實際上英文的子句、介係詞、代名詞等元素在翻譯成華文時都需要不斷的切分與重組，身為初階譯者的我，往往在構句時也會落入是Ａ好還是Ｂ好的猶豫中，又或者是自我懷疑是否過度超譯甚至是創譯，或許答案得等到出版之後才能知道，希望不要被評為另一位「快思慢想洪教授」。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yi-zhe-zi-yuan&quot;&gt;譯者資源&lt;&#x2F;h2&gt;
&lt;p&gt;分享幾個在這期間看到的譯者資源：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;《&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.chiuko.com.tw&#x2F;product&#x2F;%E7%BF%BB%E8%AD%AF%E4%B9%83%E5%A4%A7%E9%81%93%EF%BC%8C%E8%AD%AF%E8%80%85%E7%8D%A8%E6%86%94%E6%82%B4%EF%BC%9A%E4%BD%99%E5%85%89%E4%B8%AD%E7%BF%BB%E8%AD%AF%E8%AB%96%E9%9B%86&#x2F;&quot;&gt;翻譯乃大道 譯者獨憔悴&lt;&#x2F;a&gt;》&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;zh-tw&#x2F;Wikipedia:%E7%BF%BB%E8%AF%91%E8%85%94&quot;&gt;翻譯腔&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.ptt.cc&#x2F;bbs&#x2F;translation&#x2F;index.html&quot;&gt;批踢踢實業坊 Translation 看板&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.ptt.cc&#x2F;bbs&#x2F;translator&#x2F;index.html&quot;&gt;批踢踢實業坊 translator 看板&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>全世界最簡單的複式簿記基礎</title>
        <published>2022-07-29T00:00:00+00:00</published>
        <updated>2022-07-29T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/double-entry/"/>
        <id>https://editor.leonh.space/2022/double-entry/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/double-entry/">&lt;p&gt;本人年少輕狂時曾經寫過一篇〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;bookkeeping&#x2F;&quot;&gt;對於個人記帳軟體的一些想法&lt;&#x2F;a&gt;〉，當時我認為複式簿記用在個人記帳相當冗餘，就好比沒有人會把 5S 帶回家用一樣，但今天的我就要來打臉過去的我，來介紹一番「複式簿記」。&lt;&#x2F;p&gt;
&lt;p&gt;最近因為工作的關係，需要了解複式簿記，看了許多教材，大多著重在「借方」、「貸方」兩個不直覺的名詞，並且他們大多以會計的角度切入，這對於沒有會計基礎但又必須學習 domain knowledge 的我輩資訊人來說有點吃力，最後因緣際會下看到 Beancount 這篇〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;beancount.github.io&#x2F;docs&#x2F;the_double_entry_counting_method.html&quot;&gt;The Double-Entry Counting Method&lt;&#x2F;a&gt;〉，它以較生活化的角度切入，覺得相當淺顯易懂，讓我們能快速具備複式簿記的基礎知識，後面才能去思考系統面該如何設計，而不用膠著在會計專有名詞內。&lt;&#x2F;p&gt;
&lt;p&gt;本文是該文濃縮再濃縮、提煉再提煉後的華文版，並且加入一些個人觀點，理性勿戰，一鍵三連。&lt;&#x2F;p&gt;
&lt;p&gt;以下圖片皆來自原文，不再個別註明。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;fu-shi-bo-ji-ji-chu&quot;&gt;複式簿記基礎&lt;&#x2F;h2&gt;
&lt;p&gt;從最基本的 account（帳）講起，一個 account 用於記錄它身上的交易，下面這張圖表示一個空白的 account，箭頭代表時間的推進：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;double-entry&#x2F;2f37aa3938d599d4783ca9b74965026fba0a3b50.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;最初該 account 之 balance（餘額）為零，我們在線上表示最初時間的位置上方寫下 0：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;double-entry&#x2F;54633827be99c315dc937778221752b848411ca9.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;如果有交易發生，我們在線上畫圓並註記之，這個動作稱為 posting（過帳），下面此帳戶加了一百：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;double-entry&#x2F;004bc3354eb84bf554a8e5080a21f8d16fc29d82.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;過帳之後，在旁邊寫下新的 balance：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;double-entry&#x2F;6281f96c3465982c6bf48fccb302b40f90890311.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;隨著時間推進，不斷有新交易加入，balance 也不斷更新：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;double-entry&#x2F;1672e121ec80f8fcdb158bb497e05e6dc809dee5.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;double-entry&#x2F;862c0b57a35631a52eead2cf8cdd7b5f2a1aa106.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;交易可以是負的，balance 也因此可能是負的。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;statement&quot;&gt;Statement&lt;&#x2F;h3&gt;
&lt;p&gt;如果以表格呈現交易紀錄，就會像是我們 statement（對帳單）常看到的那種格式：&lt;&#x2F;p&gt;
&lt;div class=&quot;wide&quot;&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Date&lt;&#x2F;th&gt;&lt;th&gt;Description&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: right&quot;&gt;Amount&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: right&quot;&gt;Balance&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;2016-10-02&lt;&#x2F;td&gt;&lt;td&gt;…&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;100.00&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;1100.00&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;2016-10-05&lt;&#x2F;td&gt;&lt;td&gt;…&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;-25.00&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;1075.00&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;2016-10-06&lt;&#x2F;td&gt;&lt;td&gt;…&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;-200.00&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;875.00&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Final Balance&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;875.00&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;上方表格把正負金額放在同一個欄位，還有另一種常見的格式是把正負金額分為兩欄：&lt;&#x2F;p&gt;
&lt;div class=&quot;wide&quot;&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Date&lt;&#x2F;th&gt;&lt;th&gt;Description&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: right&quot;&gt;Debit&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: right&quot;&gt;Credit&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: right&quot;&gt;Balance&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;2016-10-02&lt;&#x2F;td&gt;&lt;td&gt;…&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;100.00&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;1100.00&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;2016-10-05&lt;&#x2F;td&gt;&lt;td&gt;…&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;25.00&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;1075.00&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;2016-10-06&lt;&#x2F;td&gt;&lt;td&gt;…&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;200.00&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;875.00&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;strong&gt;Final Balance&lt;&#x2F;strong&gt;&lt;&#x2F;td&gt;&lt;td&gt;875.00&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;在此表格中，欄位「debit」（借）表示從 account 流出的金額、「credit」（貸）表示流入 account 的金額，有時也被稱為「withdrawals」（提）、「deposits」（存）。&lt;&#x2F;p&gt;
&lt;p&gt;對於一般的銀行存款帳戶，金額有出有進，看到的 statement 比較有可能是第二種，而像信用卡 statement，往往都只用一欄顯示刷卡消費的金額以及總額。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;dan-shi-bo-ji&quot;&gt;單式簿記&lt;&#x2F;h3&gt;
&lt;p&gt;延續前面的概念，下面是某個 checking account（支票帳戶）的交易示意圖：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;double-entry&#x2F;a4ac6f0f3d2cf7df150fd501f0ab9a5942f79a80.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在某個期間，該 account 的 balance 為 1000，如果花了 79，那 balance 剩下 921：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;double-entry&#x2F;75337406afb5f23c23733fd25be8683ae151b410.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;如果想要註記這筆 79 是花在餐廳用餐上，那可以幫它打個標：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;double-entry&#x2F;d7f6ec08cb13d409752000bb42495399abc85848.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;延續此一模式，後面的交易也都可以打上各自的標籤，我們也可以用電腦去計算每個標籤累計的金額，這種記帳方式就是「單式簿記」，也是大多數人的記帳模式。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;fu-shi-bo-ji&quot;&gt;複式簿記&lt;&#x2F;h3&gt;
&lt;p&gt;假設 account 的主人除了 checking account 外，還有另一個 restaurant account，專門用來紀錄餐廳用餐消費，現在把 restaurant account 納入圖內：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;double-entry&#x2F;3088280515ab5b6da599edd5a1b2ca30100f2b0b.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;之前在單式簿記模式中的 restaurant 標籤，在此改成在 restaurant account 添加一筆交易的形式記錄之，表示從 checking account 轉移了 79 元到 restaurant account：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;double-entry&#x2F;2801d8aff3ccd91dcd584f58a5bcabbb57fb19d4.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;現在我們將兩個 account 的交易關聯起來，表示他們是一對相關的交易（transaction）：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;double-entry&#x2F;18524adffedac5e812eb65dcbb179b66b0ae9e53.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;另外也可以註記交易內容，例如「Dinner at Uncle Boons」，另外這筆交易的金額移轉應該是在同一時間發生的，並且一出一入之間，加總後正負互抵為零，這是複式簿記的重要概念：&lt;&#x2F;p&gt;
&lt;blockquote class=&quot;&quot;&gt;
&lt;div class=&quot;quote&quot;&gt;
&lt;p&gt;The sum of all the postings of a transaction must equal zero.&lt;&#x2F;p&gt;
&lt;p&gt;一筆交易中的每則過帳加總應為零。&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;在台灣，會計人也有類似的教條：&lt;&#x2F;p&gt;
&lt;blockquote class=&quot;&quot;&gt;
&lt;div class=&quot;quote&quot;&gt;
&lt;p&gt;有借必有貸，借貸必相等。&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;放大看加深此觀念的印象：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;double-entry&#x2F;a56ad72219b0d8a6c90c692655d1b24459add2d6.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;這圖表示 checking account 移出了 79 元（-79）、restaurant account 移入了 79（+79）元：&lt;&#x2F;p&gt;
&lt;pre style=&quot;text-align: center; font-size: large; background-color: hsla(0, 0%, 80%, 1.0)&quot;&gt;
( +79 ) + ( -79 ) = 0
&lt;&#x2F;pre&gt;
&lt;h3 id=&quot;geng-duo-account&quot;&gt;更多 Account&lt;&#x2F;h3&gt;
&lt;p&gt;吃餐廳也有可能刷卡，所以我們再加入另一個 credit-card account，同樣的，錢錢從 credit-card account 流到 restaurant account 了：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;double-entry&#x2F;458909db06c7f38f7896205f67d60397b292e7d9.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;這頓在 Eataly 的 35 塊午餐，讓 credit-card account 從原本的 -450 變成 -485。&lt;&#x2F;p&gt;
&lt;p&gt;除了 restaurant account，當然也可能有其他 account 用於歸納不同支出，可以是粗略的食衣住行育樂，也可以是細項的柴米油鹽醬醋茶，取決於個人選擇。&lt;&#x2F;p&gt;
&lt;p&gt;上方 credit-card account 的 balance 是負值，站在持卡人的觀點看，表示這些錢是我欠銀行、銀行借我的，出來借，總有一天要還，credit-card account 累積的負值 balance 也會在繳卡費那天從另一個 account 轉移到 credit-card account 繳清，下期 credit-card account 的 balance 也將從零開始。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;duo-zhong-posting&quot;&gt;多重 Posting&lt;&#x2F;h3&gt;
&lt;p&gt;一筆交易（transaction）也可能由兩筆以上的 posting（過帳）構成，不論有幾筆 posting，他們的正負加總應依舊為零，例如一筆 salary（薪資）一部分轉移至 checking account、一部分轉移至 taxes：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;double-entry&#x2F;19e2cc49a057dfeea1cf5254610eab4c9a124488.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;上圖中，salary account 的 2905 中的 2000 被轉移至 checking account、另外的 905 被轉移至 taxes account，算式如下：&lt;&#x2F;p&gt;
&lt;pre style=&quot;text-align: center; font-size: large; background-color: hsla(0, 0%, 80%, 1.0)&quot;&gt;
( -2905 ) + ( +2000 ) + ( -905 ) = 0
&lt;&#x2F;pre&gt;
&lt;p&gt;為何那 salary 是「負」2905，因為在複式簿記恆等式中，一筆交易的 posting 總計必須為零，而錢真正進入口袋的是名為 checking account 的戶頭，以及一部分的 taxes account，那 salary account 僅是為了滿足恆等式以及統計所需而設立的名目，其實更前面的 restaurant account 也是同樣的概念，真的有個 restaurant account 嗎？顯然沒有，他們都只是為了記帳所需而設立的名目 account。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;account-lei-xing&quot;&gt;Account 類型&lt;&#x2F;h2&gt;
&lt;p&gt;對於 account，我們可以分為幾種類型。&lt;&#x2F;p&gt;
&lt;p&gt;對於某些 account，我們關心它的 balance（餘額），以及在某個時間點的 balance，例如支票帳戶、儲蓄帳戶、信用卡帳戶、房貸帳戶，我們都很關心這些帳戶的 balance。&lt;&#x2F;p&gt;
&lt;p&gt;另外一些 account，我們關心它的 delta（差額），也就是兩個時間點間的差距，例如前述的餐廳花費，我們往往只會想知道「上個月」吃掉多少錢，不會想知道我這一生中在餐廳所有花費的累計（balance），又或者是薪資，我們往往只關心特定期間的「月薪」或「年薪」，而非這輩子累計的 balance。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;對於注重 balance 的 account，他們皆屬於 &lt;strong&gt;「asset」（資產）&lt;&#x2F;strong&gt; 或 &lt;strong&gt;「liability」（負債）&lt;&#x2F;strong&gt;，他們的報表稱為&lt;strong&gt;資產負債表（balance sheet）&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;對於注重 delta 的 account，他們皆屬於 &lt;strong&gt;「income」（收入）&lt;&#x2F;strong&gt; 或 &lt;strong&gt;「expense」（費用）&lt;&#x2F;strong&gt;，他們的報表稱為&lt;strong&gt;損益表（income statement）&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;這四類 account 慣用的正負號如下表：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Balance 為正值&lt;&#x2F;th&gt;&lt;th&gt;Balance 為負值&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Asset（資產）&lt;&#x2F;td&gt;&lt;td&gt;liability（負債）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Expense（費用）&lt;&#x2F;td&gt;&lt;td&gt;Income（收入）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;各類說明如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Asset (+)：我們所擁有的「資產」，例如存款、現金、證券、不動產，他們的 balance 有可能增減，但始終為正值。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Liability (-)：我們欠人家的，例如信用卡、貸款，例如我們貸款買房子，在獲得房產資產的同時，也多了筆房貸的負債，而每月的房貸還款相當於拿一部分存款轉移到貸款，負債也因此減少（例如從 -2,000,000 變成 -1,980,000，但始終負債還是負值）。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Expense (+)：我們用錢錢換來的酷東西，也可能是無形的東西，例如吃喝玩樂，此外稅金也列為 expense，因為站在個人觀點，我們繳稅給政府換來公共建設以及公共服務。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;Income (-)：包括薪資收入、投資收入、存款利息、證券配息、現金回饋、紅利點數等等，典型的收入會被轉移到 asset（例如存款），也有一部分轉移成 expense（例如預扣所得稅）。Income 大概是四種 account 類型中最不直覺的，或許可以這樣想，income 轉移到 asset 才是真正落袋為安，而 asset 要增加，在複式簿記恆等式規則下，Income 自然就得減少了。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;下面是一些各類 account 的範例：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;double-entry&#x2F;281c7a6ed22a8168fc1094a1faa61f2b0fdd7ca3.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;讓 account 宇宙擴張：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;double-entry&#x2F;e3008a34f9ce7a224e33d4cbb79bcebbb08d418e.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;再擴張：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;double-entry&#x2F;18b769f7a3cdf7fd67a983d3a39325b563d8d346.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在每個 account 名稱前綴那四種類型：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;double-entry&#x2F;1f84bcb0ad4787659c157280f6f4ae5ea720482d.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;不論 account 有多少，萬變不離其宗的是複式簿記恆等式：一筆交易的 posting 總和應為零。&lt;&#x2F;p&gt;
&lt;p&gt;有了這些交易資料，就可以產生一些統計值及試算表。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;trial-balance&quot;&gt;Trial Balance&lt;&#x2F;h2&gt;
&lt;p&gt;將前面的範例依照四種類型重新整理成下圖：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;double-entry&#x2F;175f3223735d9d04b2a7e3f421bc6280f3cda5eb.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;依序分別是 asset、liability、equity、income、expense，其中 equity 先暫時忽略，後面會再提到。&lt;&#x2F;p&gt;
&lt;p&gt;如果我們將每個 account 累計的 balance 算出來，並且省略中間的交易即 posting 細節，這份報表稱為「trial balance」（試算報表）。&lt;&#x2F;p&gt;
&lt;p&gt;Trial balance 呈現的是這段時間內每個 account 的 balance，因為每筆交易總和皆為零，因此所有的 account balance 總和也應該為零。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;sun-yi-biao-income-statement&quot;&gt;損益表（Income Statement）&lt;&#x2F;h2&gt;
&lt;p&gt;損益表是一種常見的財務報表，用於表示某段期間內各項收入與花費的統計金額，而兩者相減即為 &lt;strong&gt;「net income」（淨收益）&lt;&#x2F;strong&gt;，不論是公司或個人，正常應該都要賺的比花的多。&lt;&#x2F;p&gt;
&lt;p&gt;損益表的形式很簡單，就是把 income account balance 擺左邊，expense account balance 擺右邊：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;double-entry&#x2F;6d57026ecb4c28873f77167eb49ea8025bbf150b.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;但要注意他們的正負號，依照前面的表格，income account balance 應該為負值，expense account balance 應該為正值，正常情況下 net income 也應該為負值，表示還有錢可以轉移去增加資產或減少負債。&lt;&#x2F;p&gt;
&lt;p&gt;損益表所表示的期間通常是年、季、月，我們不太會關心出生以來所總累計的收入或花費。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zi-chan-fu-zhai-biao-balance-sheet&quot;&gt;資產負債表（Balance Sheet）&lt;&#x2F;h2&gt;
&lt;p&gt;資產負債表示另一種常見的財務報表，用於表示「錢在哪裡？」，除了那些吃喝玩樂花掉的，剩下的錢要嘛變成存款（資產），要嘛用於償還貸款（負債），在資產負債表中，我們也只關心資產與負債：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;double-entry&#x2F;093cd8751f07c38e909b7621675daa80db8fb634.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;此外，我們也關心資產與負債的總和，也就是&lt;strong&gt;淨值（net worth）&lt;&#x2F;strong&gt;，在財務健康的狀況下，資產應該大於負債，也就是淨值應為正值。&lt;&#x2F;p&gt;
&lt;p&gt;資產負債表的形式如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;double-entry&#x2F;d495b61bc719ab90cdfc9fd379ef62531cff0627.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;其中 equity 那一部分，表示的意義是收入與花費，以負值表示，負值越大，收入越大，而收入會跑去哪呢？當然是變成正值的資產啦（或降低負債），因此這裡 equity 的負值越大表示的是資產越大，&lt;&#x2F;p&gt;
&lt;h2 id=&quot;credit-debit&quot;&gt;Credit &amp;amp; Debit&lt;&#x2F;h2&gt;
&lt;p&gt;在此之前，我們都只有談到正負號，對於那四類 account，資產與花費總是正值、收入與負債總是負值，並且負值的 posting 表示金錢的流出、正值的 posting 表示金錢的流入。&lt;&#x2F;p&gt;
&lt;p&gt;而在守舊的會計宇宙中，他們把資產與花費列為 debit（借方），負債與收入列為 credit（貸方），並且某筆 posting 中，一個 account 有金錢流出，稱為 debiting（借），另一筆 posting 中，另一個 account 有金錢流入，稱為 crediting（貸）。&lt;&#x2F;p&gt;
&lt;p&gt;會計宇宙的借貸概念有點像化學的氧化還原，氧化表示失去電子，還原表示得到電子，在現代的氧化還原定義中，已經和氧原子沒什麼關係，與其從古典氧化還原定義推導，更多人選擇硬背，借貸也是，我輩非會計人只能硬背，但借貸的定義較氧化還原更為複雜，對於不同種類的 account，有時候錢流出去叫借，錢流進來叫貸，有時候則相反。&lt;&#x2F;p&gt;
&lt;p&gt;站在系統面的角度，引入借貸也會使資料模型與邏輯變得複雜，可能需要開兩個欄位 &lt;code&gt;debit_amount&lt;&#x2F;code&gt;、&lt;code&gt;credit_amount&lt;&#x2F;code&gt; 才能模仿真實的複式簿記帳本，而且即便有兩個欄位，你還是要根據 account 種類去區分他們身上借貸與數字正負號的關係，而本文所提倡的正負號表示法只要開一個 &lt;code&gt;amount&lt;&#x2F;code&gt; 即可，邏輯上更單純，也不用去理會到底誰是借誰又是貸。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zi-liao-biao-she-ji&quot;&gt;資料表設計&lt;&#x2F;h2&gt;
&lt;p&gt;簡化再簡化的 schema 大概會長這樣：&lt;&#x2F;p&gt;
&lt;div class=&quot;wide&quot;&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Date&lt;&#x2F;th&gt;&lt;th&gt;Account&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: right&quot;&gt;Amount&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;2016-12-04&lt;&#x2F;td&gt;&lt;td&gt;Liability:CreditCard&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;-153.45&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;2016-12-04&lt;&#x2F;td&gt;&lt;td&gt;Expense:Gifts&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;153.45&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;2016-12-06&lt;&#x2F;td&gt;&lt;td&gt;Liability:CreditCard&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;-47.23&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;2016-12-06&lt;&#x2F;td&gt;&lt;td&gt;Expense:Restaurant&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;47.23&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;2016-12-07&lt;&#x2F;td&gt;&lt;td&gt;Asset:Cash&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;-25.00&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;2016-12-07&lt;&#x2F;td&gt;&lt;td&gt;Expense:Tips&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;4.00&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;2016-12-07&lt;&#x2F;td&gt;&lt;td&gt;Expense:Alcohol&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;21.00&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;每筆交易都至少有兩筆 posting，並且 posting 加總為零。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-yu&quot;&gt;結語&lt;&#x2F;h2&gt;
&lt;p&gt;是的，在區塊鏈出現以前，就已經有古老且純粹的技術確保帳務的正確性，並且也是分散式的，我的帳本和銀行的帳本應該要對得起來，銀行的帳本和所有往來客戶的帳本也應該要對得起來，銀行和銀行間的帳本也要對得起來，如此串連，就稱為「帳本鏈」吧！&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>對於個人記帳軟體的一些想法</title>
        <published>2022-07-20T00:00:00+00:00</published>
        <updated>2022-07-20T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/bookkeeping/"/>
        <id>https://editor.leonh.space/2022/bookkeeping/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/bookkeeping/">&lt;p&gt;分享一點點對於現今市面上所有的記帳軟體的一點想法及疑惑。&lt;&#x2F;p&gt;
&lt;p&gt;像我們這種老百姓如果有在記帳的話不外乎幾種方式，這裡的方式是指電腦應用面的，而非會計上的－像流水帳，可能簡單的用試算表記一記，隨著使用經驗的豐富、資料的增長，譬如說加入帳戶管理、資產負債管理、預算控制等等，屆時可能會往幾個方向發展，一是自己擴充試算表，二是買軟體，三是採用一些記帳網站的服務，基本上第二跟第三是一樣的，只是呈現的介面不同，在這裡我要談的就是這些軟體或網站的使用模式，這樣的服務基本上都會有幾個項目，就像現實生活中一樣，要為每個銀行帳戶在軟體裡面建立相對應的帳戶，以及現金，其它可能還有股票之類的，帳戶全部建立之後，從此就要為發生在這些帳戶的每一筆交易做詳實的紀錄。&lt;&#x2F;p&gt;
&lt;p&gt;以我個人而言，記帳有兩大目的，第一個是帳戶方面的，我想知道我每個月帳戶的金額增減了多少，這個部分是以月為單位，實際上並不需要知道詳細到每一筆交易的進出狀況，因為這個部分在第二點。&lt;&#x2F;p&gt;
&lt;p&gt;第二點是預算及支出記錄，監看及控制這個月支出的種類，簡單講就是流水帳。&lt;&#x2F;p&gt;
&lt;p&gt;問題在這裡，目前的記帳軟體都是建立在複式記帳法的基礎上，錢在各個帳戶間流轉一定要有憑有據，每一筆都要記，這非常麻煩，舉例來說，現在有些銀行的存款帳戶不再是半年發一次利息了，是每個月發利息，在複式記帳的原則下，像我這樣的窮鬼每個月為了那十幾塊錢的零頭還要去記上一筆，否則帳目就不會達到平衡，這點非常討厭，再舉個例子，跟券商買股票的手續費折扣回饋，像我這種小戶一樣也都只是小小零頭，為了要讓帳目正確又非得去記那該死的一筆 :(。&lt;&#x2F;p&gt;
&lt;p&gt;其實以個人而言，記帳的目的很簡單，一是每個月帳戶金額的結算，二是知道我每個月花了多少錢，花到那裏去，可是這兩項一旦混為一談就變成一場災難，往往軟體裡面有個帳戶比較特別，它叫做現金，是的你得為每一次提領出來的錢做一次記錄，以複式記帳的法則將這筆錢從帳戶A轉移到現金項，個人淺見這樣嚴謹的記帳方式對個人用戶來說完全是多餘的，難怪很多人還是不記帳，原因有二，一是每筆支出都要記太懶，這是他們的錯，二是那些個軟體除了要記每筆支出外，還有那該死的複式記帳法跟現金項。&lt;&#x2F;p&gt;
&lt;p&gt;再說明一次，個人記帳兩大目的：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;知道我這個月和上個月比、或今年跟明年比，財產的總消長。一個帳戶，一個月，只有 1 筆資料；一年，只有 12 筆資料。&lt;&#x2F;li&gt;
&lt;li&gt;知道我每個月或每年，花了多少錢，花在哪個項目上、有沒有超出預算。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;而不用去管那些該死的帳戶平衡、現金帳戶、還有那些零頭收入。而現今的軟體（或網站）一概達不到這樣的要求，因為太簡單了 :p，只要兩張試算表就可輕易達成，一個叫帳戶、一個叫流水帳。&lt;&#x2F;p&gt;
&lt;p&gt;好像戰意很濃，消毒一下好了，本人沒學過會計，本文僅就個人需求提出的見解，部分知識習自網路，如觀念有錯請不吝指教。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>PDF 煉成陣</title>
        <published>2022-07-04T00:00:00+00:00</published>
        <updated>2022-07-04T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/pdf/"/>
        <id>https://editor.leonh.space/2022/pdf/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/pdf/">&lt;p&gt;對一般用戶來說，要把文件存成 PDF，大多是按個「匯出」、「另存新檔」、「列印」就完工的事，但在程式端搞起來可不簡單。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;sheng-cheng-pdf-de-fang-shi&quot;&gt;生成 PDF 的方式&lt;&#x2F;h2&gt;
&lt;p&gt;在 web app 的典型情境不外乎把原本是 HTML 形式的文件、報表、票卡、收據、帳單轉換成 PDF，一般用幾種方式辦到：&lt;&#x2F;p&gt;
&lt;h3 id=&quot;yong-liu-lan-qi-sheng-cheng&quot;&gt;用瀏覽器生成&lt;&#x2F;h3&gt;
&lt;p&gt;前端有許多 PDF 生成套件可供選用，但問題是你無法保證用戶瀏覽器一致，可能會有版面、字體、顏色各方面參差問題。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zai-hou-duan-sheng-cheng&quot;&gt;在後端生成&lt;&#x2F;h3&gt;
&lt;p&gt;以 Python 為例，常見的有 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;weasyprint.org&#x2F;&quot;&gt;WeasyPrint&lt;&#x2F;a&gt; 和 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;JazzCore&#x2F;python-pdfkit&quot;&gt;Python-PDFKit&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;PDFKit 是以 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;wkhtmltopdf.org&#x2F;index.html&quot;&gt;wkhtmltopdf&lt;&#x2F;a&gt; 為基礎的工具，而 wkhtmltopdf 則是以 Qt WebKit 為基礎的工具，這個 Qt WebKit 不知道是哪個年代的 WebKit，對當代的 CSS 支援極差，flexbox、grid 都不可用，不禁佩服以前共事的三寶之大寶還能用 wkhtmltopdf 生出有模有樣的標籤，給大寶一個讚！&lt;&#x2F;p&gt;
&lt;p&gt;WeasyPrint 是走自行解析 HTML 的路子，它的解析器來自其他 Python 套件，優點是輕量、快速，問題是它的解析器終究不比真正的瀏覽器，容錯、標準支援方面都不夠好，例如下面這些問題：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;支援 CSS flexbox ，但對 CSS grid 完全不支援。&lt;&#x2F;li&gt;
&lt;li&gt;支援 &lt;code&gt;justify-content&lt;&#x2F;code&gt;、&lt;code&gt;align-items&lt;&#x2F;code&gt;，但對 &lt;code&gt;place-content&lt;&#x2F;code&gt;、&lt;code&gt;place-items&lt;&#x2F;code&gt; 都不支援。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;兩者相較還是 WeasyPrint 要好得多，而且它的範例製作的挺精美，令人有想用的衝動。&lt;&#x2F;p&gt;
&lt;embed src=&quot;ticket.pdf&quot; type=&quot;application&#x2F;pdf&quot; style=&quot;height: 500px;&quot; &#x2F;&gt;
&lt;h3 id=&quot;zai-hou-duan-yong-wu-tou-liu-lan-qi-sheng-cheng&quot;&gt;在後端用無頭瀏覽器生成&lt;&#x2F;h3&gt;
&lt;p&gt;這方面個人較偏好用 &lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;playwright&#x2F;&quot;&gt;Playwright&lt;&#x2F;a&gt;，雖然這有點大材小用了，這類方案的問題是都要裝肥大的瀏覽器，瀏覽器啟動需要時間，又是吃記憶體怪獸，對我的 286 主機是一大開銷，但在其他方案沒一個能打的情況下，看來是沒有選擇的選擇。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;sheng-cheng-pdf-de-kao-liang&quot;&gt;生成 PDF 的考量&lt;&#x2F;h2&gt;
&lt;p&gt;HTML 轉 PDF 不只是另存新檔那麼簡單，還有這些考量：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;安全性，來源的 HTML 是哪邊來的，可靠嗎？有用戶輸入的內容嗎？有可能被注入攻擊嗎？&lt;&#x2F;li&gt;
&lt;li&gt;PDF 頁首頁尾有需要嗎？生成工具有支援嗎？&lt;&#x2F;li&gt;
&lt;li&gt;紙張尺寸是標準 A4 嗎？如果是自訂尺寸，生成工具有支援嗎？&lt;&#x2F;li&gt;
&lt;li&gt;生成工具有支援 CSS 換頁語句嗎？會需要跨頁表格標題列重複嗎？&lt;&#x2F;li&gt;
&lt;li&gt;要生成的內容是複雜的 data table 嗎？後端組的出來嗎？&lt;&#x2F;li&gt;
&lt;li&gt;後端無頭瀏覽器被大量調用會吃爆主機資源嗎？需要引入 queue 嗎？該即時回應嗎？&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;css-shi-cao&quot;&gt;CSS 實操&lt;&#x2F;h2&gt;
&lt;p&gt;在 CSS 方面，可以用某些屬性設定列印樣式：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;css&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;@page&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    size&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 11.3&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;cm&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 4.3&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;cm&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    margin&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 0&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;@media&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; print&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;    body&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt; div&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        outline-width&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 0&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;px !important&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;hr&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;#page-breaker&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    break-after&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; page&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    height&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 0&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    border-width&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 0&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;說明如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@page&lt;&#x2F;code&gt; 區塊負責設定頁面尺寸、邊距。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;@media print&lt;&#x2F;code&gt; 區塊用於設定其他列印時的樣式。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;break-after: page&lt;&#x2F;code&gt; 設定強制換頁屬性，上面設定了一個高度為零的換頁元素。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;weasyprint-shi-cao&quot;&gt;WeasyPrint 實操&lt;&#x2F;h2&gt;
&lt;p&gt;直接上碼：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;import&lt;&#x2F;span&gt;&lt;span&gt; weasyprint&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; html_to_pdf&lt;&#x2F;span&gt;&lt;span&gt;(html_str:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    html&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; weasyprint.HTML(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;string&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;html_str)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    pdf_bytes:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; bytes&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; html.write_pdf()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return&lt;&#x2F;span&gt;&lt;span&gt; pdf_bytes&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;html_raw&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; template.render()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;pdf_bytes&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; html_to_pdf(html_raw)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面的 &lt;code&gt;template.render()&lt;&#x2F;code&gt; 實際上是 Jinja2 的 HTML 生成函式，在這裡不深究，總之就是來個 HTML 字串。&lt;&#x2F;p&gt;
&lt;p&gt;後面用 &lt;code&gt;weasyprint.HTML()&lt;&#x2F;code&gt; 則是把 HTML 字串解析成它自己的 HTML 物件，再生成 PDF 回應出去。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;playwright-shi-cao&quot;&gt;Playwright 實操&lt;&#x2F;h2&gt;
&lt;p&gt;一樣直接上碼：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; playwright.sync_api&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; sync_playwright&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; html_to_pdf&lt;&#x2F;span&gt;&lt;span&gt;(html_str:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    pdf_bytes:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; bytes&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    with&lt;&#x2F;span&gt;&lt;span&gt; sync_playwright()&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; as&lt;&#x2F;span&gt;&lt;span&gt; p:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        browser&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; p.chromium.launch()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        page&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; browser.new_page()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        page.set_content(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;html&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;html_str)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        pdf_bytes&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; page.pdf(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;            height&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;4.3cm&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;            margin&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;                &amp;#39;top&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;0&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;                &amp;#39;right&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;0&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;                &amp;#39;bottom&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;0&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;                &amp;#39;left&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;0&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;            width&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;11.3cm&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        )&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        browser.close()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return&lt;&#x2F;span&gt;&lt;span&gt; pdf_bytes&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;html_raw&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; template.render()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;pdf_bytes&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; html_to_pdf(html_raw)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Playwright 因為要開瀏覽器，所以用 &lt;code&gt;with&lt;&#x2F;code&gt; 區塊管理瀏覽器的生命週期，事情做完就關閉。&lt;&#x2F;p&gt;
&lt;p&gt;相較於 WeasyPrint 是直接解析 HTML 轉成 PDF，Playwright 是列印的概念，所以還得在此設定列印的參數，主要也是紙張的尺寸和列印的邊距。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;windows-yi-bu-huan-jing-xia-de-playwright-wen-ti&quot;&gt;Windows、異步環境下的 Playwright 問題&lt;&#x2F;h3&gt;
&lt;p&gt;在 Playwright 網站上寫著：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;pdf&#x2F;playwright.png&quot; alt=&quot;Incompatible with SelectorEventLoop of asyncio on Windows&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;很不巧的 Jupyter 或 uvicorn 都是在 asyncio 下跑的，在他們支配的環境下，不論是 &lt;code&gt;sync_playwright&lt;&#x2F;code&gt; 或 &lt;code&gt;async_playwright&lt;&#x2F;code&gt;、不論是 &lt;code&gt;SelectorEventLoop&lt;&#x2F;code&gt; 或 &lt;code&gt;ProactorEventLoop&lt;&#x2F;code&gt;，跑起 Playwright 都會遇到 &lt;code&gt;NotImplementedError&lt;&#x2F;code&gt;，只好放大招開另一支 process 處理。&lt;&#x2F;p&gt;
&lt;p&gt;動用 multiprocessing 後的修改如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;import&lt;&#x2F;span&gt;&lt;span&gt; multiprocessing&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; as&lt;&#x2F;span&gt;&lt;span&gt; mp&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; playwright.sync_api&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; sync_playwright&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; html_to_pdf&lt;&#x2F;span&gt;&lt;span&gt;(q: mp.Queue, html_str:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    pdf_bytes:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; bytes&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    with&lt;&#x2F;span&gt;&lt;span&gt; sync_playwright()&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; as&lt;&#x2F;span&gt;&lt;span&gt; p:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        browser&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; p.chromium.launch()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        page&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; browser.new_page()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        page.set_content(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;html&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;html_str)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        pdf_bytes&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; page.pdf(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;            height&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;4.3cm&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;            margin&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;                &amp;#39;top&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;0&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;                &amp;#39;right&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;0&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;                &amp;#39;bottom&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;0&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;                &amp;#39;left&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;0&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;            width&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;11.3cm&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        )&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        browser.close()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    q.put(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;obj&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;pdf_bytes)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;html_raw&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; template.render()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;q&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; mp.Queue()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;process&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; mp.Process(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;target&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;html_to_pdf,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt; args&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;(q, html_raw,))&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;process.start()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;pdf_bytes:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;bytes&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; q.get()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;process.join()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;file_like&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; BytesIO(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;initial_bytes&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;pdf_bytes)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;q&lt;&#x2F;code&gt; 是 &lt;code&gt;Queue()&lt;&#x2F;code&gt; 物件，用於跨 process 通訊，一邊用 &lt;code&gt;q.put()&lt;&#x2F;code&gt; 丟出結果，另一邊用 &lt;code&gt;q.get()&lt;&#x2F;code&gt; 接收結果。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;process&lt;&#x2F;code&gt; 負責把 Playwright 跑起來，&lt;code&gt;start()&lt;&#x2F;code&gt; 語意很明確，就是把 process 跑起來，但最後的 &lt;code&gt;join()&lt;&#x2F;code&gt; 是什麼意思？它並沒有「join」任何東西，不要從語意的角度理解它，它的作用是確認子 process 結束，讓主 process 繼續往下走，類似於異步環境下的 &lt;code&gt;await&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;jie-yu&quot;&gt;結語&lt;&#x2F;h2&gt;
&lt;p&gt;實際操練下來，WeasyPrint 和 Playwright 比較可靠，WeasyPrint 雖然有 CSS 支援上的弱勢，但還是可以用 flexbox 組出可用的版面，又不像 Playwright 那麼吃資源，反之 Playwright 有較佳的 CSS 支援，但相對的生成速度真的比較慢一點，兩者的取捨個人也尚無定論，至於那還是有很多文章提到的 wkhtmltopdf，就忘了它吧。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>用 2Captcha 通過 CAPTCHA 人機驗證</title>
        <published>2022-07-03T00:00:00+00:00</published>
        <updated>2022-07-03T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/2captcha/"/>
        <id>https://editor.leonh.space/2022/2captcha/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/2captcha/">&lt;p&gt;往往我們在做爬蟲或 bot 的時候，最難跨過的關卡之一大概就是那 &lt;a rel=&quot;external&quot; href=&quot;http:&#x2F;&#x2F;www.captcha.net&#x2F;&quot;&gt;CAPTCHA&lt;&#x2F;a&gt; 人機驗證了，有的團隊選擇自行開發辨識技術來通過 CAPTCHA 人機驗證，然而面對越來越難的人機考驗，如 Google 的 reCAPTCHA，若真要自行開發，那得付出相當程度的時間與精神，本文要介紹的 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;2captcha.com?from=12933116&quot;&gt;2Captcha&lt;&#x2F;a&gt; 是可以幫我們通過 CAPTCHA 人機驗證的服務，透過 2Captcha API，我們可以方便快速的通過那些 CAPTCHA 考驗，省去大量自行開發的時間與金錢。&lt;&#x2F;p&gt;
&lt;p&gt;在介紹 2Captcha 前，先簡單的認識一下 CAPTCHA。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;captcha&quot;&gt;CAPTCHA&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;strong&gt;CAPTCHA&lt;&#x2F;strong&gt; 全稱是「&lt;strong&gt;C&lt;&#x2F;strong&gt;ompletely &lt;strong&gt;A&lt;&#x2F;strong&gt;utomated &lt;strong&gt;P&lt;&#x2F;strong&gt;ublic &lt;strong&gt;T&lt;&#x2F;strong&gt;uring test to tell &lt;strong&gt;C&lt;&#x2F;strong&gt;omputers and &lt;strong&gt;H&lt;&#x2F;strong&gt;umans &lt;strong&gt;A&lt;&#x2F;strong&gt;part」，翻譯成華文是超級繞口的「&lt;strong&gt;全自動區分電腦和人類的公開圖靈測試&lt;&#x2F;strong&gt;」，這串無敵長的文字簡單講就是&lt;strong&gt;人機驗證&lt;&#x2F;strong&gt;，CAPTCHA 人機驗證存在的目的通常是為了遏止網站被外部程式惡意的大量操作，然而這樣的機制也擋住了我們家可愛無害的小爬蟲 🐛 或小機器人 🤖，所以我輩開發者就需要像 2Captcha 這樣的服務幫我們通過那 CAPTCHA 考驗。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;chang-jian-de-captcha&quot;&gt;常見的 CAPTCHA&lt;&#x2F;h3&gt;
&lt;p&gt;在台灣常見到的 CAPTCHA 的場合大概有這些：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;台灣高鐵&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;台灣高鐵的 CAPTCHA 是較為傳統的文字混淆型：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;2captcha&#x2F;thsrc.png&quot; alt=&quot;台灣高鐵&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;臺灣鐵路&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;臺鐵的則是用 Google 的 reCAPTCHA：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;2captcha&#x2F;taiwan-railway.png&quot; alt=&quot;臺灣鐵路&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;網路銀行&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;除了訂票外，網銀也很常看到 CAPTCHA：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;2captcha&#x2F;banks.webp&quot; alt=&quot;網路銀行&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;特別要說明的是，上面的舉例僅是用於說明 CAPTCHA 的使用場景，請不要動搶票的歪腦筋，那有可能是違法的，下手之前請諮詢專業法律人士意見，也歡迎留言共同討論。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2captcha&quot;&gt;2Captcha&lt;&#x2F;h2&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;2captcha&#x2F;23f09926a86e999864290072e567ca3e.svg&quot; alt=&quot;2Captcha&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：&lt;a href=&quot;https:&#x2F;&#x2F;2captcha.com&#x2F;&quot;&gt;2Captcha&lt;&#x2F;a&gt;&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;先簡單認識 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;2captcha.com?from=12933116&quot;&gt;2Captcha&lt;&#x2F;a&gt;，做為一個辨識 CAPTCHA 的服務，2Captcha 背後的辨識機制其實是由人工完成的，我們傳送給 2Captcha 的圖片，都會轉送給分散在全世界的 2Captcha worker 們，經過人工辨識後再把結果回傳，而那些辛苦的 worker 們也可以透過 2Captcha 的仲介機制獲得報酬，因為是人工辨識，所以我們可以預期：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;正確率應該是高的，2Captcha 也有自己的品質把關機制，剃除掉不適任的 worker，一般來說正確率在九成以上。&lt;&#x2F;li&gt;
&lt;li&gt;應該會有些許的等待時間，才能得到辨識後的結果，在 2Captcha API 文件有提到，一般形式的 CAPTAHA 建議等待 5 秒，而較難的 reCAPTCHA 則建議等待 20 秒。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;2captcha-de-shou-fei&quot;&gt;2Captcha 的收費&lt;&#x2F;h3&gt;
&lt;p&gt;在費用方面，2Captcha 是採用動態計價，動態的基準是當前 2Captcha 與 worker 們的工作負荷，當負荷量大時會升價、負荷量小時會降價，當前的費率會顯示在 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;2captcha.com&#x2F;public_statistics&quot;&gt;Statistics&lt;&#x2F;a&gt; 頁面，例如下面是本文撰寫時的費率：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;2captcha&#x2F;2captcha-statistics.png&quot; alt=&quot;2Captcha customers statistics&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;上圖可以看到，普通的 CAPTCHA 每&lt;strong&gt;一千個&lt;&#x2F;strong&gt;收費僅 0.74 美金，而較難的 reCAPTCHA 每&lt;strong&gt;一千個&lt;&#x2F;strong&gt;則收費 2.99 美金，相較於自行開發各種花式 AI 辨識系統所要花費的人力物力，2Captcha 算是相當低廉的價格了。&lt;&#x2F;p&gt;
&lt;p&gt;在 Statistics 頁面，除了費率外，還有目前 2Captcha 服務的負載程度，以及解題的速度，上圖顯示一般的 CAPTCHA 花 15 秒，而 reCAPTCHA 要花 46 秒，儘管與 2Captcha 文件內的 5 秒、20 秒有差距，看起來還在可以接受的程度內。&lt;&#x2F;p&gt;
&lt;p&gt;在簡單的認識 2Captcha 的機制與收費之後，下面就來實際玩玩看 2Captcha 吧！&lt;&#x2F;p&gt;
&lt;h2 id=&quot;2captcha-bian-shi-yi-ban-captcha&quot;&gt;2Captcha 辨識一般 CAPTCHA&lt;&#x2F;h2&gt;
&lt;p&gt;在這個範例中，我們會示範用 Playwright 登入一個網站，並且用 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;2captcha.com?from=12933116&quot;&gt;2Captcha&lt;&#x2F;a&gt; 協助我們辨識登入時的 CAPTCHA 驗證碼。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;shi-qian-zhun-bei&quot;&gt;事前準備&lt;&#x2F;h3&gt;
&lt;p&gt;在開始前簡單的介紹一下 Playwright，Playwright 是微軟開發的瀏覽器自動化套件，類似於陳年的 Selenium，Playwright 的優點有：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;自帶瀏覽器，包括 Firefox、WebKit、Chromium，Playwright 一行指令就都幫我們配置到好。&lt;&#x2F;li&gt;
&lt;li&gt;跨語言，Playwright 有 Node.js、Python、.NET、Java 版本，並且不同的語言有著共通的邏輯與相似的 API。&lt;&#x2F;li&gt;
&lt;li&gt;自動等待元素現身，才執行相關敘述，相當大程度的省去了手動調試等待時間的心力。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;做為一個瀏覽器自動化工具，當然是最適合拿來開發爬蟲或 bot 了，特別是 client side rendering 當道的現在，很難不使用真正的瀏覽器而做出爬蟲或 bot。&lt;&#x2F;p&gt;
&lt;p&gt;在 2Captcha 方面，開立帳號後，登入可以看到 2Captcha API key：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;2captcha&#x2F;2captcha-api-key.webp&quot; alt=&quot;2Captcha API Key&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;這組 API key 用於與 2Captcha 程式交互，下面的範例程式中的 API key 是無效的，請務必換成您個人的 API key。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;deng-ru-yu-captcha-bian-shi&quot;&gt;登入與 CAPTCHA 辨識&lt;&#x2F;h3&gt;
&lt;p&gt;回到範例的主題上，下面我們會用 Playwright for Python 以及 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;2captcha&#x2F;2captcha-python&quot;&gt;2Captcha 的 Python 套件&lt;&#x2F;a&gt;，示範通過下面這個登入頁面：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;2captcha&#x2F;two&amp;#x27;s.png&quot; alt=&quot;TWO’S 創意束胸&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在這個典型的登入表單中，我們的腳本會用 Playwright 輸入帳密，並且把那 CAPTCHA 圖片透過 2Captcha API 取得驗證碼，最後點按「登入」：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;import&lt;&#x2F;span&gt;&lt;span&gt; base64&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; playwright.sync_api&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; sync_playwright&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; twocaptcha&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; TwoCaptcha&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;CAPTCHA_API_KEY&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;5c0f7e0306aa2e0398510ef9ce6dbca&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;solver&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; TwoCaptcha(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;apiKey&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;CAPTCHA_API_KEY&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; solve&lt;&#x2F;span&gt;&lt;span&gt;(image):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  try&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    result&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; solver.normal(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;      file&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;image,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;      numeric&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;4&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;      minLength&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;4&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;      maxLength&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;4&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;      caseSensitive&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    )&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  except&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; Exception&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; as&lt;&#x2F;span&gt;&lt;span&gt; e:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    print&lt;&#x2F;span&gt;&lt;span&gt;(e)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  print&lt;&#x2F;span&gt;&lt;span&gt;(result)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  return&lt;&#x2F;span&gt;&lt;span&gt; result&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; report&lt;&#x2F;span&gt;&lt;span&gt;(captcha_id:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;span&gt;, success:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; bool&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  solver.report(captcha_id, success)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; main&lt;&#x2F;span&gt;&lt;span&gt;():&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  while&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; True&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    with&lt;&#x2F;span&gt;&lt;span&gt; sync_playwright()&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; as&lt;&#x2F;span&gt;&lt;span&gt; p:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      browser&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; p.firefox.launch(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;headless&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;False&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt; slow_mo&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;500&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      context&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; browser.new_context()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      page&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; context.new_page()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      page.goto(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;http:&#x2F;&#x2F;www.twoas.idv.tw&#x2F;user.php&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      page.type(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;input[name=username]&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;RSOB&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      page.type(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;input[name=password]&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;RSOBPassword&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;)  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      screenshot_bytes&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; page.locator(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;img[alt=captcha]&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;).screenshot()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      captcha_image&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; base64.b64encode(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;s&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;screenshot_bytes).decode(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;utf-8&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      result&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; solve(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;image&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;captcha_image)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      page.type(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;input[name=captcha]&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, result[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;code&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;])&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      page.click(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;text=立即登入&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      login_message&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; page.text_content(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;div.tips&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;      try&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        assert&lt;&#x2F;span&gt;&lt;span&gt; login_message&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ==&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;登入成功&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;      except&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; AssertionError&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; as&lt;&#x2F;span&gt;&lt;span&gt; e:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        report(result[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;captchaId&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;],&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; False&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        continue&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      report(result[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;captchaId&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;],&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; True&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      page.pause()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      context.close()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      browser.close()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;      break&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;if&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; __name__&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ==&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;__main__&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  main()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面的腳本，可以分成 Playwright 操控瀏覽器的部份與 2Captcha 交互的部份。&lt;&#x2F;p&gt;
&lt;p&gt;Playwright 的部份，我們用 &lt;code&gt;page&lt;&#x2F;code&gt; 物件操控大多數的瀏覽器行為，相信即使是對 Playwright 不熟悉的朋友也可以望文生義：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;用 &lt;code&gt;goto()&lt;&#x2F;code&gt; 函式前往指定網址。&lt;&#x2F;li&gt;
&lt;li&gt;用 &lt;code&gt;type()&lt;&#x2F;code&gt; 函式找到特定元素，並輸入字元。&lt;&#x2F;li&gt;
&lt;li&gt;用 &lt;code&gt;locator()&lt;&#x2F;code&gt; 函式找到元素並操控它。&lt;&#x2F;li&gt;
&lt;li&gt;用 &lt;code&gt;screenshot()&lt;&#x2F;code&gt; 函式對元素截圖。&lt;&#x2F;li&gt;
&lt;li&gt;用 &lt;code&gt;click()&lt;&#x2F;code&gt; 函式點按特定元素。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在用 &lt;code&gt;screenshot()&lt;&#x2F;code&gt; 取得 CAPTCHA 圖片後，用 &lt;code&gt;base64&lt;&#x2F;code&gt; 模組轉換成 Base64 編碼，交給 &lt;code&gt;solve()&lt;&#x2F;code&gt; 函式處理。&lt;&#x2F;p&gt;
&lt;p&gt;在 CAPTCHA 辨識部份，此網站的 CAPTCHA 如下例：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;2captcha&#x2F;captcha.png&quot; alt=&quot;CAPTCHA&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;這是一種相對普通的 CAPTCHA，在 2Captcha 的分類上，屬於 normal 型。&lt;&#x2F;p&gt;
&lt;p&gt;在 2Captcha 的部份，先定義一個 2Captcha 的 &lt;code&gt;solver&lt;&#x2F;code&gt; 物件，並用下面的敘述得到辨識後的文字：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;result&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; solver.normal(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;  file&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;image,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;  numeric&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;4&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;  minLength&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;4&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;  maxLength&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;4&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;  caseSensitive&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;因為 CAPTCHA 屬於 normal 型，所以我們調用的是 &lt;code&gt;normal()&lt;&#x2F;code&gt; 函式，函式內的參數，除了 &lt;code&gt;file&lt;&#x2F;code&gt; 是必填，其他都是可選用的：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;file&lt;&#x2F;code&gt; 可接受一個經 Base64 編碼的 CAPTCHA 圖片物件，也可以是本地的圖片檔案，或圖片的 URL。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;numeric&lt;&#x2F;code&gt; CAPTCHA 的字元型態，&lt;code&gt;4&lt;&#x2F;code&gt; 表示會有字母與數字，其他的定義請參閱 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;2captcha.com&#x2F;2captcha-api&quot;&gt;2Captcha API 文件&lt;&#x2F;a&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;minLength&lt;&#x2F;code&gt; 最小字元長度，以此例而言，都是固定四個字元。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;maxLength&lt;&#x2F;code&gt; 最大字元長度，以此例而言，都是固定四個字元。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;caseSensitive&lt;&#x2F;code&gt; 設為 &lt;code&gt;1&lt;&#x2F;code&gt; 表示區分大小寫。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;經過上面的交互，取得 &lt;code&gt;result&lt;&#x2F;code&gt;，&lt;code&gt;result&lt;&#x2F;code&gt; 是一個結構如下的 dict 物件：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;captchaId&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;69475003267&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;code&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;4QR5&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中的 &lt;code&gt;cpatchaId&lt;&#x2F;code&gt; 是 2Captcha API 賦予的交易 ID，而 &lt;code&gt;code&lt;&#x2F;code&gt; 當然就是辨識後的答案啦！&lt;&#x2F;p&gt;
&lt;h3 id=&quot;hui-kui-bian-shi-yu-fou&quot;&gt;回饋辨識與否&lt;&#x2F;h3&gt;
&lt;p&gt;取得 &lt;code&gt;result[&#x27;code&#x27;]&lt;&#x2F;code&gt; 後，填入表單，若正確登入，我們用 &lt;code&gt;report()&lt;&#x2F;code&gt; 回饋成功紀錄給 2Captcha，並在最後的 &lt;code&gt;break&lt;&#x2F;code&gt; 語句完全結束 &lt;code&gt;while&lt;&#x2F;code&gt; 迴圈。&lt;&#x2F;p&gt;
&lt;p&gt;反之若是失敗，則用 &lt;code&gt;report()&lt;&#x2F;code&gt; 回饋失敗紀錄給 2Captcha，並在後續的 &lt;code&gt;continue&lt;&#x2F;code&gt; 語句中斷這次的 &lt;code&gt;while&lt;&#x2F;code&gt; 迴圈，進入下一個 &lt;code&gt;while&lt;&#x2F;code&gt; 迴圈，藉此做出重試的效果。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;cheng-guo&quot;&gt;成果&lt;&#x2F;h3&gt;
&lt;p&gt;最後的成果可以參考這個影片：&lt;&#x2F;p&gt;
&lt;!-- &lt;video controls src=&quot;videos&#x2F;0001-0510.mp4&quot; style=&quot;width: 100%;&quot;&gt;&lt;&#x2F;video&gt; --&gt;
&lt;iframe frameborder=&quot;0&quot; src=&quot;https:&#x2F;&#x2F;www.dailymotion.com&#x2F;embed&#x2F;video&#x2F;x87tr9m&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;對於 normal 型的使用範例，也可以參考 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;2captcha.com&#x2F;p&#x2F;simple_captcha?from=12933116&quot;&gt;2Captcha 的 normal 說明頁&lt;&#x2F;a&gt;，裡面有更多種語言的使用範例。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yong-2captcha-tong-guo-recaptcha&quot;&gt;用 2Captcha 通過 reCAPTCHA&lt;&#x2F;h2&gt;
&lt;p&gt;完成前一個較簡單的例子後，我們來玩玩看如何用 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;2captcha.com?from=12933116&quot;&gt;2Captcha&lt;&#x2F;a&gt; 通過難一點的 reCAPTCHA，也就是那著名的靈魂考驗「我不是機器人」：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;2captcha&#x2F;6280162.png&quot; alt=&quot;我不是機器人&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：網路&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;要用 2Captcha 通過 reCAPTCHA 的「我不是機器人」考驗，必須先取得目標頁面的 site key，在原始碼裡面就可以找到這把公開的 site key：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;2captcha&#x2F;sitekey.png&quot; alt=&quot;reCAPTCHA Site Key&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;有了 site key 之後，就可以依樣畫葫蘆，呼叫 2Captcha API 得到 reCAPTCHA 的通關密語。&lt;&#x2F;p&gt;
&lt;p&gt;與 2Captcha 交互的部份改成這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;result&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; solver.recaptcha(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;  sitekey&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;6LflqMEUAAAAAANhC6kkXmSLiBOLiLFsHw_anYWZ&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;  url&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;https:&#x2F;&#x2F;naweeklytimes.com&#x2F;login-2&#x2F;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;  version&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;v2&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這次用的是 &lt;code&gt;recaptcha()&lt;&#x2F;code&gt; 函式，它的參數更簡單了，應該一望即知，拿到的 result 也是同樣的 dict 結構，只是變成超長的 token：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;captchaId&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;69482081140&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;code&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;03AGdBq252s4QsHsrr9CqzkkswdnHsUPLXIcE4OI1pEX4FUP9rNY8p760yLBsd9goaMn61vw95x981lU0...&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然後我們要把那組 &lt;code&gt;code&lt;&#x2F;code&gt; 填入一個名為 &lt;code&gt;g-recaptcha-response&lt;&#x2F;code&gt; 的文字框，但這文字框是隱藏的，所以得先用 Playwright 跑一小段 JS 讓它顯示出來：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;page.eval_on_selector(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;  selector&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;textarea[name=g-recaptcha-response]&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;  expression&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;(el) =&amp;gt; el.style.display = &amp;#39;inline-block&amp;#39;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面的函式中，JS 的 &lt;code&gt;el&lt;&#x2F;code&gt; 就是來自 &lt;code&gt;selector&lt;&#x2F;code&gt; 抓到的元素。&lt;&#x2F;p&gt;
&lt;p&gt;文字框顯示出來會像這樣：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;2captcha&#x2F;g-recaptcha-response.png&quot; alt=&quot;g-recaptcha-response&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;然後就比照第一個範例，帳號、密碼、reCAPTCHA 通關密語填一填，就可以成功登入囉！&lt;&#x2F;p&gt;
&lt;p&gt;同樣地，對於 reCAPTCHA 型的使用範例，也可以參考 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;2captcha.com&#x2F;p&#x2F;recaptcha_v2?from=12933116&quot;&gt;2Captcha 的 reCAPTCHA 說明頁&lt;&#x2F;a&gt;，裡面有更多種語言的使用範例。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;cheng-guo-1&quot;&gt;成果&lt;&#x2F;h3&gt;
&lt;p&gt;完整的程式碼如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; playwright.sync_api&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; sync_playwright&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; twocaptcha&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; TwoCaptcha&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;URL&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;https:&#x2F;&#x2F;naweeklytimes.com&#x2F;login-2&#x2F;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;SITEKEY&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;6LflqMEUAAAAAANhC6kkXmSLiBOLiLFsHw_anYWZ&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;CAPTCHA_API_KEY&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;5c0f7e0306aa2e0398510ef9ce6dbca&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;solver&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; TwoCaptcha(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;apiKey&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;CAPTCHA_API_KEY&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; solve&lt;&#x2F;span&gt;&lt;span&gt;(url:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  try&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    result&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; solver.recaptcha(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;      sitekey&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;SITEKEY&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;      url&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;url,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;      version&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;v2&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    )&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  except&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; Exception&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; as&lt;&#x2F;span&gt;&lt;span&gt; e:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    print&lt;&#x2F;span&gt;&lt;span&gt;(e)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    raise&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; Exception&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  print&lt;&#x2F;span&gt;&lt;span&gt;(result)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  return&lt;&#x2F;span&gt;&lt;span&gt; result&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; report&lt;&#x2F;span&gt;&lt;span&gt;(captcha_id:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;span&gt;, success:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; bool&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  solver.report(captcha_id, success)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; main&lt;&#x2F;span&gt;&lt;span&gt;():&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  while&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; True&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    with&lt;&#x2F;span&gt;&lt;span&gt; sync_playwright()&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; as&lt;&#x2F;span&gt;&lt;span&gt; p:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      browser&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; p.firefox.launch(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;headless&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;False&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt; slow_mo&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;500&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      context&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; browser.new_context()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      page&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; context.new_page()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      page.goto(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;URL&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      page.type(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;input[name=log]&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;RSOB&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      page.type(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;input[name=pwd]&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;RSOBPassword&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;      try&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        result&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; solve(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;url&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;URL&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;      except&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; Exception&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; as&lt;&#x2F;span&gt;&lt;span&gt; e:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        continue&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      page.eval_on_selector(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;        selector&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;textarea[name=g-recaptcha-response]&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;        expression&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;(el) =&amp;gt; el.style.display = &amp;#39;inline-block&amp;#39;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      )&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      textarea&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; page.locator(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;textarea[name=g-recaptcha-response]&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      textarea.fill(result[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;code&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;])&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      page.click(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;input[type=submit]&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;      try&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        assert&lt;&#x2F;span&gt;&lt;span&gt; page.url&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ==&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;https:&#x2F;&#x2F;naweeklytimes.com&#x2F;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;      except&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; AssertionError&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; as&lt;&#x2F;span&gt;&lt;span&gt; e:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        report(result[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;captchaId&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;],&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; False&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        continue&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      report(result[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;captchaId&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;],&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; True&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      page.pause()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      context.close()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      browser.close()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;      break&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;if&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; __name__&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ==&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;__main__&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  main()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;看起來有點長，不過和黑科技 AI 相比應該是小巫見大巫。&lt;&#x2F;p&gt;
&lt;p&gt;下面是執行的影片：&lt;&#x2F;p&gt;
&lt;!-- &lt;video controls src=&quot;videos&#x2F;0001-0900.mp4&quot; style=&quot;width: 100%;&quot;&gt;&lt;&#x2F;video&gt; --&gt;
&lt;iframe frameborder=&quot;0&quot; src=&quot;https:&#x2F;&#x2F;www.dailymotion.com&#x2F;embed&#x2F;video&#x2F;x87tsx9&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
&lt;h2 id=&quot;jie-yu&quot;&gt;結語&lt;&#x2F;h2&gt;
&lt;p&gt;本文我們示範用 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;2captcha.com?from=12933116&quot;&gt;2Captcha&lt;&#x2F;a&gt; 通過普通的 CAPTCHA 及 reCAPTCHA，在這兩種最常見的 CAPTCHA 外，2Captcha 還支援其他各式各樣的 CAPTCHA，可以參考 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;2captcha.com&#x2F;2captcha-api?from=12933116&quot;&gt;2Captcha 的文件&lt;&#x2F;a&gt;與 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;2captcha.com&#x2F;demo?from=12933116&quot;&gt;demo&lt;&#x2F;a&gt; 頁。&lt;&#x2F;p&gt;
&lt;p&gt;在 API 方面，2Captcha 有提供封裝好的 Python、GO、PHP、Java、C#、C++ 套件，即使是沒有現成套件的語言，直接呼叫 2Captcha API 也不是太困難的事。&lt;&#x2F;p&gt;
&lt;p&gt;至於 2Captcha 的費用，除了真的很便宜的費率外，在設計 bot 時，只要保留住登入後的 cookie 或 local storage，即可省下每次都要重新登入的時間以及 CAPTCHA 的費用，以 Playwright 為例，它就有 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;playwright.dev&#x2F;python&#x2F;docs&#x2F;auth#reuse-authentication-state&quot;&gt;reuse authentication state&lt;&#x2F;a&gt; 的機制，可以多加利用。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;can-kao-zi-liao&quot;&gt;參考資料&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;維基百科〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;zh-tw&#x2F;%E9%AA%8C%E8%AF%81%E7%A0%81&quot;&gt;驗證碼&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>瀏覽器自動化套件 Playwright</title>
        <published>2022-07-01T00:00:00+00:00</published>
        <updated>2022-07-01T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/playwright/"/>
        <id>https://editor.leonh.space/2022/playwright/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/playwright/">&lt;p&gt;對於 web 自動化測試，一路走來我們用過許多方案，剛開始是用最多人知道的 Selenium，那是前端框架還不盛行的時代，也是要手動寫測試腳本的時代。&lt;&#x2F;p&gt;
&lt;p&gt;手動寫測試腳本這件事對工程師們來說，是繁重又缺乏創造力的工作，當時流行的 jQuery web 元件讓「抓 id &#x2F; class」變成一件相當繁瑣的事，而最大的問題是人力的耗損，算式很簡單：&lt;code&gt;會打碼 = 貴&lt;&#x2F;code&gt;，任何有成本概念的人都不會想把人力投放在測試上。（投放在&lt;strong&gt;測試&lt;&#x2F;strong&gt;上／投資在&lt;strong&gt;品質&lt;&#x2F;strong&gt;上，是兩個不同的概念，不要混為一談。）&lt;&#x2F;p&gt;
&lt;p&gt;第二階段，我們找到了 Sikuli，它是以比對螢幕圖像與位置為基礎的測試工具，也有寫與錄腳本的能力，操作也夠親切，只要有基礎的程式與邏輯概念的人都可以無痛寫（錄）出所有實境操作的劇本，但 Sikuli 的問題是只要換個解析度或換個作業系統，導致 web 元件位置變化或者 render 上的變化，那測試就過不了，只能重寫（錄）…，好在當時我們的 POS 的螢幕解析度都是固定的。&lt;&#x2F;p&gt;
&lt;p&gt;但是 POS 之外的&lt;del&gt;產品&lt;&#x2F;del&gt;&lt;ins&gt;專案&lt;&#x2F;ins&gt;，由於上面提到的問題，Sikuli 就不足以應付了，於是我們又看到了 Katalon。Katalon 是以 Selenium 為基礎的產品，它的錄製工具會自動幫我們抓網頁元素的 ID，省去了部份的抓 ID 工作，但對於複雜的元件（如 combo box、data grid）還是需要事後人工編修，但整體來說還是個可接受的解決方案。&lt;&#x2F;p&gt;
&lt;p&gt;而今天我們的新玩具叫做 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;playwright.dev&#x2F;&quot;&gt;Playwright&lt;&#x2F;a&gt;，Playwright 是微軟開發的 web 自動化測試工具，它有幾項值得一提的特性：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;跨平台，macOS、Linux、Windows 皆可用。&lt;&#x2F;li&gt;
&lt;li&gt;跨瀏覽器，可操控 WebKit、Firefox、Chromium 三大瀏覽器。&lt;&#x2F;li&gt;
&lt;li&gt;跨語言，Playwright 原本是以 Node.js 開發，後來微軟陸續移植到 Python、Java 和 .NET 上，雖然語法不同但有著相似的 API。&lt;&#x2F;li&gt;
&lt;li&gt;完整的工具鏈，Playwright 包括 Playwright 與 Playwright Test Runner 兩部份，想要拆開來用也可以。&lt;&#x2F;li&gt;
&lt;li&gt;年輕、開發活躍、有富爸爸支持，微軟自己也在用。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;也有幾項採用前得注意的點：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;不能操控手機，只能開啟瀏覽器的手機模擬模式，但我們都知道，真的和模擬的有部份的差異。&lt;&#x2F;li&gt;
&lt;li&gt;它的 WebKit 瀏覽器和 Safari 也不一樣，雖然 Safari 底層也是 WebKit，但一樣會有點差異。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;peng-playwright-qian-de-qian-zhi-zuo-ye&quot;&gt;碰 Playwright 前的前置作業&lt;&#x2F;h2&gt;
&lt;p&gt;開一個空的 Node.js 專案，資料夾架構如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├─node_modules&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├─tests&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├─package.json&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;└─package-lock.json&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;所有的測試腳本都放在 tests&#x2F; 裡面，Playwright 會去跑 test&#x2F; 與旗下資料夾內所有 *.spec.js 與 *.spec.ts 的測試腳本。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;an-zhuang&quot;&gt;安裝&lt;&#x2F;h2&gt;
&lt;p&gt;Playwright 在 NPM 分為兩個主要套件：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;playwright&lt;&#x2F;code&gt;：Playwright 套件&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;@playwright&#x2F;test&lt;&#x2F;code&gt;：Play&lt;wbr&gt;wright Test Runner 套件&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;兩者的 API 用法略有差異，依 Playwright 自己的文件說，Playwright Test Runner 比較適合 end-to-end testing 的場景，這也是我們的應用場景，所以下文我們的 Playwright 都是指 Playwright Test Runner。&lt;&#x2F;p&gt;
&lt;p&gt;安裝 Playwright Test Runner：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;npm&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; install&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --save-dev&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; @playwright&#x2F;test&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Playwright Test Runner 並不使用我們的瀏覽器，它有自帶瀏覽器，用這行指令把瀏覽器裝起來：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;npx&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; playwright install&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;一行搞定三大瀏覽器，因此我們也不需要配置任何的瀏覽器路徑等等。:)&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ji-chu-shi-yong-yu-pei-zhi&quot;&gt;基礎使用與配置&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;yong-lu-de-chan-sheng-jiao-ben&quot;&gt;用錄的產生腳本&lt;&#x2F;h3&gt;
&lt;p&gt;我們用錄的來上手：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;npx&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; playwright codegen wikipedia.org&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --output&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; .&#x2F;tests&#x2F;wikipedia.spec.js&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;跳出瀏覽器和 Playwright Inspector，裡面有錄出來的腳本：&lt;&#x2F;p&gt;
&lt;div class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;playwright&#x2F;wikipedia.png&quot; alt=&quot;Playwright&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;隨便點幾下之後，那個 wikipedia.&lt;wbr&gt;spec.js 長這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;const&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; test&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; expect&lt;&#x2F;span&gt;&lt;span&gt; }&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; require&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;@playwright&#x2F;test&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;test&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;test&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; async&lt;&#x2F;span&gt;&lt;span&gt; ({&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt; page&lt;&#x2F;span&gt;&lt;span&gt; })&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  &#x2F;&#x2F; Go to https:&#x2F;&#x2F;www.wikipedia.org&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  await&lt;&#x2F;span&gt;&lt;span&gt; page.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;goto&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;https:&#x2F;&#x2F;www.wikipedia.org&#x2F;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  &#x2F;&#x2F; Click text=中文&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  await&lt;&#x2F;span&gt;&lt;span&gt; page.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;click&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;text=中文&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  expect&lt;&#x2F;span&gt;&lt;span&gt;(page.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;url&lt;&#x2F;span&gt;&lt;span&gt;()).&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;toBe&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;Wikipedia:%E9%A6%96%E9%A1%B5&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  &#x2F;&#x2F; Check input[type=&amp;quot;checkbox&amp;quot;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  await&lt;&#x2F;span&gt;&lt;span&gt; page.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;check&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;input[type=&amp;quot;checkbox&amp;quot;]&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  &#x2F;&#x2F; Click a:has-text(&amp;quot;臺灣正體&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  await&lt;&#x2F;span&gt;&lt;span&gt; page.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;click&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;a:has-text(&amp;quot;臺灣正體&amp;quot;)&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  expect&lt;&#x2F;span&gt;&lt;span&gt;(page.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;url&lt;&#x2F;span&gt;&lt;span&gt;()).&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;toBe&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;zh-tw&#x2F;Wikipedia:%E9%A6%96%E9%A1%B5&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  &#x2F;&#x2F; Click a:has-text(&amp;quot;登入&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  await&lt;&#x2F;span&gt;&lt;span&gt; page.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;click&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;a:has-text(&amp;quot;登入&amp;quot;)&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  &#x2F;&#x2F; Click text=中文（繁體）&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  await&lt;&#x2F;span&gt;&lt;span&gt; page.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;click&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;text=中文（繁體）&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  &#x2F;&#x2F; Click [placeholder=&amp;quot;輸入您的使用者名稱&amp;quot;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  await&lt;&#x2F;span&gt;&lt;span&gt; page.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;click&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;[placeholder=&amp;quot;輸入您的使用者名稱&amp;quot;]&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  &#x2F;&#x2F; Fill [placeholder=&amp;quot;輸入您的使用者名稱&amp;quot;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  await&lt;&#x2F;span&gt;&lt;span&gt; page.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;fill&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;[placeholder=&amp;quot;輸入您的使用者名稱&amp;quot;]&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;jimmy&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  &#x2F;&#x2F; Click [placeholder=&amp;quot;輸入您的密碼&amp;quot;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  await&lt;&#x2F;span&gt;&lt;span&gt; page.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;click&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;[placeholder=&amp;quot;輸入您的密碼&amp;quot;]&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  &#x2F;&#x2F; Fill [placeholder=&amp;quot;輸入您的密碼&amp;quot;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  await&lt;&#x2F;span&gt;&lt;span&gt; page.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;fill&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;[placeholder=&amp;quot;輸入您的密碼&amp;quot;]&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;walls&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  &#x2F;&#x2F; Click button:has-text(&amp;quot;登入&amp;quot;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  await&lt;&#x2F;span&gt;&lt;span&gt; page.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;click&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;button:has-text(&amp;quot;登入&amp;quot;)&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  &#x2F;&#x2F; Go to https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;Wikipedia:%E9%A6%96%E9%A1%B5&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  await&lt;&#x2F;span&gt;&lt;span&gt; page.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;goto&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;Wikipedia:%E9%A6%96%E9%A1%B5&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  expect&lt;&#x2F;span&gt;&lt;span&gt;(page.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;url&lt;&#x2F;span&gt;&lt;span&gt;()).&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;toBe&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;Wikipedia:%E9%A6%96%E9%A1%B5&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;});&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到，錄出來的並非完美的，有好幾行多餘的敘述，而優點是非常簡單就能上手。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;pao-ce-shi&quot;&gt;跑測試&lt;&#x2F;h3&gt;
&lt;p&gt;要跑測試也很簡單：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;npx&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; playwright test&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --headed --browser=chromium&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這行指令會跑所有在 test&#x2F; 與旗下資料夾內所有 *.spec.js 與 *.spec.ts 的測試腳本。&lt;&#x2F;p&gt;
&lt;p&gt;後面我們也用參數指定用 Chromium 瀏覽器的有頭模式跑測試。&lt;&#x2F;p&gt;
&lt;p&gt;如果要跑指定腳本，就指定一下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;npx&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; playwright test tests&#x2F;wikipedia.spec.js&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --headed --browser=chromium&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;用有頭模式跑測試時也可以叫出 Playwright Inspector：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# Linux&#x2F;macOS&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;PWDEBUG&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; npm&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; run test&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# Windows with cmd.exe&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;set&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; PWDEBUG=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;npm&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; run test&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# Windows with PowerShell&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;$env:PWDEBUG&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;npm&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; run test&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;playwright&#x2F;108614092-8c478a80-73ac-11eb-9597-67dfce110e00.png&quot; alt=&quot;Playwright Inspector&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Playwright Inspector 可以手動控制腳本的進度，方便我們對測試腳本 debug。（那我們需要測試腳本的測試腳本嗎？）&lt;&#x2F;p&gt;
&lt;h3 id=&quot;pei-zhi&quot;&gt;配置&lt;&#x2F;h3&gt;
&lt;p&gt;配置檔放在專案根目錄下，檔名可以是 playwright.&lt;wbr&gt;config.js 或 playwright.&lt;wbr&gt;conifg.ts，目前我的配置很陽春如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;const&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; devices&lt;&#x2F;span&gt;&lt;span&gt; }&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; require&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;@playwright&#x2F;test&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;module&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;exports&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    reporter: [&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;list&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;],&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;json&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, { outputFile:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;test-results&#x2F;results.json&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt; }],&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;junit&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, { outputFile:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;test-results&#x2F;results.xml&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt; }],&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    ],&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    use: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        baseURL:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;http:&#x2F;&#x2F;localhost:63800&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        headless:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; false&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        slowMo:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 10&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        screen: { width:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1100&lt;&#x2F;span&gt;&lt;span&gt;, height:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 700&lt;&#x2F;span&gt;&lt;span&gt; },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        viewport: { width:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1100&lt;&#x2F;span&gt;&lt;span&gt;, height:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 700&lt;&#x2F;span&gt;&lt;span&gt; },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;        &#x2F;&#x2F; Artifacts&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        screenshot:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;on&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        trace:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;only-on-failure&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        video:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;on&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;};&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中 &lt;code&gt;reporter&lt;&#x2F;code&gt; 內設定了三組 reporter，不同的 reporter 代表不同的輸出格式，list report 讓我們方便在 console 看到測試結果的摘要，而另外兩個則分別把測試結果輸出成 JSON 和 JUnit 的結構化格式，方便整合至其它系統。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;sloMo&lt;&#x2F;code&gt; 則是讓測試的每個步驟停頓的時間，單位是 1&#x2F;1000 秒，年紀大了要跑慢一點。&lt;&#x2F;p&gt;
&lt;p&gt;而 &lt;code&gt;screenshot&lt;&#x2F;code&gt;、&lt;code&gt;trace&lt;&#x2F;code&gt;、&lt;code&gt;video&lt;&#x2F;code&gt; 用於配置測試期間的抓圖、錄影和測試軌跡紀錄的行為，可以是「不論成功失敗都不存檔」、「不論成功失敗都存檔」、「只有失敗才存檔」。&lt;&#x2F;p&gt;
&lt;p&gt;Playwright 還有其它多如牛毛的配置參數，請參閱 Playwright 文件。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;trace&quot;&gt;Trace&lt;&#x2F;h3&gt;
&lt;p&gt;跑完測試的 trace 是完整的測試軌跡紀錄（也是最肥的），用 Playwright Trace Viewer 可以完整重現測試的所有軌跡：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;npx playwright show-trace trace.zip&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;div class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;playwright&#x2F;120585896-6a1bca80-c3e7-11eb-951a-bd84002480f5.png&quot; alt=&quot;Playwright Trace Viewer&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
&lt;h3 id=&quot;expect-api&quot;&gt;Expect API&lt;&#x2F;h3&gt;
&lt;p&gt;Playwright 用 JEST 的 Expect 函式庫來驗證測試的條件，這部份也請參閱 Playwright 和 JEST 的文件。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shi-yong-ji-qiao&quot;&gt;使用技巧&lt;&#x2F;h2&gt;
&lt;p&gt;這邊分享一些自己用到的或網路上挖到的小技巧。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;deng-yuan-su-chu-xian&quot;&gt;等元素出現&lt;&#x2F;h3&gt;
&lt;p&gt;前端框架已經是 web app 的主流方案，網頁元件大量使用 fetch，特別是資料型的表格，這種表格我們叫 data grid，Playwright 要操作 data grid 內的元素必須等待 fetch 的結果顯示出來，雖然 Playwright 有 auto-waiting 的機制，但也不是萬靈丹，對那些不是由用戶觸發的 event，auto-waiting 就不太靈光，還是要自己處理等待的機制。&lt;&#x2F;p&gt;
&lt;p&gt;Playwright 有提供好幾種 wait 函式，最原始的可以用 &lt;code&gt;waitForTimeout()&lt;&#x2F;code&gt;，但粗暴地用秒數等待大法還是有可能遇到時間到了，fetch 還沒收到資料的問題，我們可以改用另一個 &lt;code&gt;waitForFunction()&lt;&#x2F;code&gt; 來自定一個等待函式，等到元素出現才往下走：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;await&lt;&#x2F;span&gt;&lt;span&gt; page.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;waitForFunction&lt;&#x2F;span&gt;&lt;span&gt;(()&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; document.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;querySelectorAll&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;.data-grid row&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;).&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;length&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; &amp;gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面的匿名函式的部分我們定義了至少 data grid 內要有一列資料，滿足條件後，&lt;code&gt;watiForFunction()&lt;&#x2F;code&gt; 才結束等待。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shi-yong-xin-de&quot;&gt;使用心得&lt;&#x2F;h2&gt;
&lt;p&gt;Playwright 有內建 auto-waiting 的機制，用於因應現代化的 SPA 頻繁存取後端 API 的特性，以往的 Selenium 都要在腳本手動寫下 wait 來確保前端收到回應後再跑下一步，而 Playwright 的 auto-waiting 的機制會自動偵測點擊之類會發送請求的事件，並自動等到有回應的時候再跑下一步…，但以上只是理想而已，實際操練一天下來，還是發現有需要手動定義 wait 的場景。&lt;&#x2F;p&gt;
&lt;p&gt;另外和 Selenium 或 Katalon 相似的是「抓 id &#x2F; class」的負擔依舊存在，並沒有變輕鬆的感覺。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-yu&quot;&gt;結語&lt;&#x2F;h2&gt;
&lt;p&gt;本文僅粗淺的介紹了 Playwright Test Runner 最基本的用法，特別是 Playwright 自己的函式庫和 JEST Expect 函式庫更是偷懶的隻字未提，這部份的用法取決於各個專案自己的測試情境，只能請讀者大大自行參閱原始文件了。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Tortoise ORM &#x2F; FastAPI 整合快速筆記</title>
        <published>2022-06-28T00:00:00+00:00</published>
        <updated>2022-06-28T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/tortoise/"/>
        <id>https://editor.leonh.space/2022/tortoise/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/tortoise/">&lt;p&gt;因為以往被 Active Record 慣壞了，導致在 Python 圈一直找不到相同順手的 ORM，又因為用的不是像 Django 那樣的大禮包框架，導致我在 ORM 的路上始終尋尋覓覓，今天又又又又又要來介紹另一款 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;tortoise.github.io&#x2F;&quot;&gt;Tortoise ORM&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;先吹一波特性：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;支援 SQLite、PostgreSQL、MariaDB、MySQL，以及透過 ODBC 支援 SQL Server、Oracle。&lt;&#x2F;li&gt;
&lt;li&gt;異步。&lt;&#x2F;li&gt;
&lt;li&gt;有 migration 機制。&lt;&#x2F;li&gt;
&lt;li&gt;支援多個套件、框架整合，包括 UnitTest、FastAPI、Quart、Sanic、Starlette、aiohttp、BlackSheep、&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2023&#x2F;pydantic-model&#x2F;&quot;&gt;Pydantic&lt;&#x2F;a&gt;，以上有得熱門有得冷門，但奇怪的是竟然沒有 Flask。即便不在上述清單內，只要搞懂 Tortoise 的初始化機制也可以自幹整合。&lt;&#x2F;li&gt;
&lt;li&gt;支援多資料庫。&lt;&#x2F;li&gt;
&lt;li&gt;支援讀寫分離。&lt;&#x2F;li&gt;
&lt;li&gt;但沒有 seeding 機制，得自幹。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;下面是 Tortoise 和 FastAPI &#x2F; pydantic 整合的快速筆記。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ding-yi-model&quot;&gt;定義 Model&lt;&#x2F;h2&gt;
&lt;p&gt;這裡的 model 指的是 ORM model，而 pydantic model 則由 ORM model 衍生而來，例如下面這個 models.py：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; tortoise&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; fields, models&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; tortoise.contrib.pydantic&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; pydantic_model_creator&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; Word&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;models&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Model&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; fields.IntField(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;pk&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;True&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    #: `word` may duplicate, do not have to be unique.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    word&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; fields.CharField(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;max_length&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;255&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt; null&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;False&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    accent_notation&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; fields.CharField(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;max_length&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;255&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt; null&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;False&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    class&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; Meta&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        table&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;words&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; # Always name with snake_case&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;WordRead&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; pydantic_model_creator(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    cls&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;Word,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;WordRead&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;WordCreate&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; pydantic_model_creator(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    cls&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;Word,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;WordCreate&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    exclude_readonly&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;True&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  # Exclude `id` on creating&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這裡我們定義了一個 Word model，旗下欄位由 &lt;code&gt;fields&lt;&#x2F;code&gt; 系列函式定義，應該是可以望文生義。&lt;&#x2F;p&gt;
&lt;p&gt;除欄位定義外，裡面還有一些值得一提的聲明：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;word&lt;&#x2F;code&gt; 上面有一行以 &lt;code&gt;#:&lt;&#x2F;code&gt; 開頭的註解，這種格式的註解會變成 pydantic 的 field description，因此也會變成 OpenAPI 的 propertie description。&lt;&#x2F;li&gt;
&lt;li&gt;子類 &lt;code&gt;Meta&lt;&#x2F;code&gt; 的 &lt;code&gt;table&lt;&#x2F;code&gt; 屬性聲明了該 model 的資料表名稱，因為個人習慣在資料表用 snake case，因此就得額外聲明此項囉。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;有了 ORM model，後續我們用 &lt;code&gt;pydantic_model_creator()&lt;&#x2F;code&gt; 建立兩個衍生的 pydantic model，習慣上他們的變數名和 model 名會取相同。&lt;&#x2F;p&gt;
&lt;p&gt;最終這份 models.py 有三個成員：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;一個 Word ORM model。&lt;&#x2F;li&gt;
&lt;li&gt;一個 WordRead pydantic model。&lt;&#x2F;li&gt;
&lt;li&gt;一個 WordCreate pydantic model，不帶 &lt;code&gt;id&lt;&#x2F;code&gt; 欄位，因為 ID 是資料庫自動產生的，無須用戶提供。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;後續我們會在 FastAPI 端點函式中使用到他們。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;tortoise-orm-zheng-he-fastapi&quot;&gt;Tortoise ORM 整合 FastAPI&lt;&#x2F;h2&gt;
&lt;p&gt;Tortoise 提供了傻瓜整合機制，自動在 FastAPI 啟動時帶起 Tortoise，結束時關閉 Tortoise 和資料庫的連線。&lt;&#x2F;p&gt;
&lt;p&gt;在 FastAPI 主程式 main.py 如此這般：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; fastapi&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; FastAPI&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; tortoise.contrib.fastapi&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; register_tortoise&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; app.models&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; WordCreate, Word, WordRead&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;app: FastAPI&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; FastAPI()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;register_tortoise(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    app&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;app,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    db_url&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;sqlite:&#x2F;&#x2F;db.sqlite&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    modules&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;models&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;: [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;app.models&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;]},&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    generate_schemas&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;True&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    add_exception_handlers&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;True&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;那句 &lt;code&gt;register_tortoise()&lt;&#x2F;code&gt; 一行就搞定，它傻瓜你聰明。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zai-fastapi-diao-yong-tortoise-model&quot;&gt;在 FastAPI 調用 Tortoise Model&lt;&#x2F;h2&gt;
&lt;p&gt;前面定義的三個 model，其中的 Word model 用於實操資料庫，其餘的兩個 model 則用於在 FastAPI 函式中聲明參數或回傳值型態，例如下面這個函式：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@app.post&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    path&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;&#x2F;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    response_model&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;WordRead,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;async def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; create_word&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    word: WordCreate,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    new_word: WordRead&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; = await&lt;&#x2F;span&gt;&lt;span&gt; Word.create(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        **&lt;&#x2F;span&gt;&lt;span&gt;word.dict(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;            exclude_unset&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;True&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        )&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    )&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return&lt;&#x2F;span&gt;&lt;span&gt; new_word&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;WordRead 被聲明成回傳的型態、WordCreate 被聲明成參數 &lt;code&gt;word&lt;&#x2F;code&gt; 的型態，而真正實操資料庫的語句則是由 &lt;code&gt;Word.create()&lt;&#x2F;code&gt; 構成，他們之間的轉換由 Tortoise ORM 處理到好。&lt;&#x2F;p&gt;
&lt;p&gt;下面是 CRUD 的另一個例子：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@app.get&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    path&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;{word}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    response_model&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;list[WordRead],&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;async def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; get_word&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    word:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    response&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; = await&lt;&#x2F;span&gt;&lt;span&gt; Word.filter(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;        word&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;word&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    )&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return&lt;&#x2F;span&gt;&lt;span&gt; response&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;沿前例類推，WordRead 同樣被聲明成回傳型態，而根據 Word model 的設計，欄位 &lt;code&gt;word&lt;&#x2F;code&gt; 不一定唯一，因此此處調用 &lt;code&gt;Word.filter()&lt;&#x2F;code&gt; 下查詢，回傳的應該是陣列，因此我們將回傳 model 聲明為 &lt;code&gt;list[WordRead]&lt;&#x2F;code&gt;，如果確定查詢只會有單筆紀錄那可以下 &lt;code&gt;Word.get()&lt;&#x2F;code&gt; 做查詢。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-yu&quot;&gt;結語&lt;&#x2F;h2&gt;
&lt;p&gt;本篇是 Tortoise ORM 在 FastAPI 的快速筆記，沒有提到某些也很重要的特性，例如各種花式欄位定義、關聯性、quary API、migration 等等，或許以後有碰到再寫吧，欽此。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>討論 OAuth 2 的 token 更新策略</title>
        <published>2022-06-15T00:00:00+00:00</published>
        <updated>2022-06-15T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/oauth-token/"/>
        <id>https://editor.leonh.space/2022/oauth-token/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/oauth-token/">&lt;p&gt;這篇是最近在想 OAuth 2 token 更新策略的一些思考，篇名曰「討論」，因為自己也不確定這些想法是不是所謂的最佳實踐，歡迎讀者留言討論。&lt;&#x2F;p&gt;
&lt;p&gt;在 OAuth 2 的各種 flow 中，最終大多會取得兩種 token，access token 和 refresh token，作為代表用戶的令牌存取服務及資源，這兩種 token 的特性如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;Access Token&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;代表用戶存取服務。&lt;&#x2F;li&gt;
&lt;li&gt;有效期短。&lt;&#x2F;li&gt;
&lt;li&gt;因為有效期短，比較不用擔心被竊，一般可以存放在 cookie、local storage、session storage、記憶體等處。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;strong&gt;Refresh Token&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;用於更新 access token 和 refresh token。&lt;&#x2F;li&gt;
&lt;li&gt;除了更新 token 外無法用於存取服務。&lt;&#x2F;li&gt;
&lt;li&gt;有效期長。&lt;&#x2F;li&gt;
&lt;li&gt;因為有效期長，需要較妥善的保存，一般建議存放在具有 HttpOnly 屬性的 cookie 內，但也還是有些人放在 local storage。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;token-geng-xin&quot;&gt;Token 更新&lt;&#x2F;h2&gt;
&lt;p&gt;因為 access token 效期短，為了塑造良好的用戶體驗，不要讓用戶頻繁登入，我們必須在客端 app 上利用 refresh token 更新 token，一般而言，更新 token 時不僅會取得新的 access token，也會取得新的 refresh token。&lt;&#x2F;p&gt;
&lt;p&gt;基於前面的條件，我們可以設想出第一版的更新策略。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;geng-xin-ce-lue-di-yi-ban&quot;&gt;更新策略第一版&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;oauth-token&#x2F;v1.png&quot; alt=&quot;OAuth 2 Token 更新&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;說明如下：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;用戶點了連結或按了某個按鈕。&lt;&#x2F;li&gt;
&lt;li&gt;進入藍色區域，檢查 access token 效期，如果一切順列，直接進入綠色方塊，拿 access token 向服務端要內容或執行某些事務。&lt;&#x2F;li&gt;
&lt;li&gt;如果客端根本就沒有 access token，那就進入黃色區域，拿 refresh token 換回一對新的 access token 和 refresh token。&lt;&#x2F;li&gt;
&lt;li&gt;如果客端的有 access token，但卻過期了，那一樣拿 refresh token 去換一對新的回來。&lt;&#x2F;li&gt;
&lt;li&gt;如果 refresh token 失敗，表示 refresh token 也是過期的，那就只好請用戶重新登入。&lt;&#x2F;li&gt;
&lt;li&gt;如果客端自己知道 refresh token 過期，那也是請用戶重新登入。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;在這看似有點亂又不太亂的策略中，客端 app 需要管理 access token 和 refresh token 的狀態，也就是知道他們的有效期，但還記得嗎，refresh token 是 HttpOnly 的 cookie，JS 讀不到。&lt;&#x2F;p&gt;
&lt;p&gt;況且不論我知不知道 refresh token 的有效與否，都得走入「Refresh token」這一步，只差在是我主動發現 refresh token 過期而導向登入頁，或是直接走進「Refresh token」方塊，失敗再導向登入頁。站在用戶的角度，他是感受不到這兩條路的差異的，一切都只發生在主客端程式的對話中，基於以上，我們可以把黃色區域整塊拿掉：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;oauth-token&#x2F;v1.5.png&quot; alt=&quot;OAuth 2 Token 更新&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;於是簡化成第二版。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;geng-xin-ce-lue-di-er-ban&quot;&gt;更新策略第二版&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;oauth-token&#x2F;v2.png&quot; alt=&quot;OAuth 2 Token 更新&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;第二版清爽多了，也減少客端 app 的邏輯，不用去管 refresh token 還有沒有，只管 access token 存不存在、有沒有過期，
不存在、過期了，就去「Refresh token」方塊跑一下，成功就成功，失敗就帶用戶重新登入。&lt;&#x2F;p&gt;
&lt;p&gt;同樣的，如果藍色方塊有問題都跑去「Refresh token」，那何不連藍色方塊都省掉：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;oauth-token&#x2F;v2.5.png&quot; alt=&quot;OAuth 2 Token 更新&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;於是我們有了第三版。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;geng-xin-ce-lue-di-san-ban&quot;&gt;更新策略第三版&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;oauth-token&#x2F;v3.png&quot; alt=&quot;OAuth 2 Token 更新&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;第三版的邏輯不由分說，無須任何判斷 token 存在、有效的邏輯，不關心狀態，只無腦更新，錯了就抓用戶去重新登入。&lt;&#x2F;p&gt;
&lt;p&gt;第三版夠傻瓜也夠聰明，但未必可行，紅色方塊右上的小圖示說明他們是要走網路的，無腦更新，表示服務端必須回應，如果客端 app 人很多，那相當於對服務端發動 DDoS 攻擊，要嘛就是服務端被打爆，要嘛就是用戶被封鎖。&lt;&#x2F;p&gt;
&lt;p&gt;這種作法主客端交互開銷巨大，對服務端來說，要考慮的是 token 的生成效率，token 的主流選擇是 JWT，JWT 是以 Base64 和 HS256 為基礎算出來的，也就是說服務端需要高效率的 Base64 與 HS256 解編碼演算方案，每種語言都有多樣的 Base64、HS256 編解碼器，也可以利用 APM 監控 refresh token 端點的負載，再視需要做優化。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;access-token-bei-lun&quot;&gt;Access Token 悖論&lt;&#x2F;h2&gt;
&lt;p&gt;同場加映 access token 悖論。&lt;&#x2F;p&gt;
&lt;p&gt;前面提過 accsss token 有效期短，較能承受外流的風險，例如一個效期僅一分鐘的 access token，就算外流了，多半也不太恐慌，因為一分鐘後失效了，反之，另一個效期短達一天的 access token 外流，那我們可能會緊張一下。&lt;&#x2F;p&gt;
&lt;p&gt;基於上述的設定，我們可以認定效期越短，安心感越大，因此想要安心感無限大，那效期就要無限短，短到趨近於零，但這樣就失去它的功能了，或許我們可以改用「次數」的觀點看效期，從原本的時間效期改為次數效期，也就是所謂的 one-time token，但 access token 還有一個無狀態的特性，該如何無須管理它的狀態又讓它僅能使用單次，答案或許是類似區塊鏈的技術。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>OAuth2 極簡攻略（一）Implicit Flow</title>
        <published>2022-06-14T00:00:00+00:00</published>
        <updated>2022-06-14T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/oauth2-implicit-flow/"/>
        <id>https://editor.leonh.space/2022/oauth2-implicit-flow/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/oauth2-implicit-flow/">&lt;h2 id=&quot;qian-yan&quot;&gt;前言&lt;&#x2F;h2&gt;
&lt;p&gt;在寫這篇文章的前兩天，原本工作正常的 &lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;twitter-oauth-1&#x2F;&quot;&gt;Twitter 登入&lt;&#x2F;a&gt;忽然變得異常的慢，甚至會逾時，在調查的過程中一直難以確認究竟是自己改了什麼，OS 網路層的問題，或是 Twitter API 異常所導致的，但那 Twitter API Status 卻始終顯示「沒問題 👍」：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;img src=&quot;twitter-api-status.png&quot; alt=&quot;Twitter API Status&quot; style=&quot;max-width: fit-content; margin-left: auto; margin-right: auto; display: block; margin-bottom: 1rem;&quot;&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;Twitter 大爺說它沒問題，那就是我有問題…為了調查原因做了一系列工作，包括新建一個 POC 專案來驗證登入流程、改 OS（以及建立開發環境）測試等，最終證明真的是 Twitter API 異常 😙，為此付出了整整兩天時間…坑爹啊。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;OAuth2 是當前公認的身份認證（&lt;strong&gt;auth&lt;&#x2F;strong&gt;entication）與授權（&lt;strong&gt;auth&lt;&#x2F;strong&gt;orization）標準，常見的應用場景是某個 app 讓用戶可以用既有的 Google &#x2F; Facebook &#x2F; Twitter &#x2F; Apple &#x2F; LINE 等帳號登入該 app，這樣做的好處有：&lt;&#x2F;p&gt;
&lt;p&gt;對用戶來說：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;用戶不用為 app 建一組以後會忘記的帳號密碼&lt;&#x2F;li&gt;
&lt;li&gt;用戶可以控制要授予 app 取得哪些 Google 帳號下的資料與權限&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;對 app 來說：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;降低用戶的註冊門檻、增加用戶的使用意願&lt;&#x2F;li&gt;
&lt;li&gt;App 可以開發與 Google API 整合的應用，豐富 app 的使用場景&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;oauth2-de-na-xie-flow&quot;&gt;OAuth2 的那些 Flow&lt;&#x2F;h2&gt;
&lt;p&gt;OAuth2 設想了多種的應用場景，除了上面提到的第三方登入，還有服務對服務的認證授權、穿戴設備（無法打帳密）的認證授權等，OAuth2 將各種授權認證流程定義出許多的 grant type，總覽如下：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;img src=&quot;oauth2-flows.webp&quot; alt=&quot;OAuth Grant Types&quot; style=&quot;max-width: fit-content; margin-left: auto; margin-right: auto; display: block; margin-bottom: 1rem;&quot;&gt;
&lt;figcaption&gt;來源：&lt;a href=&quot;https:&#x2F;&#x2F;oauth.net&#x2F;2&#x2F;grant-types&#x2F;&quot;&gt;OAuth Grant Types&lt;&#x2F;a&gt;&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;這些稱為 grant type 的流程，有許多稱呼，有時被稱為「scheme」更通俗的說法是「&lt;strong&gt;flow&lt;&#x2F;strong&gt;」，以 implicit grant type 為例，既可以叫 implicit scheme，也可以叫 &lt;strong&gt;implicit flow&lt;&#x2F;strong&gt;，下文皆以 &lt;strong&gt;flow&lt;&#x2F;strong&gt; 稱之（英文小技巧：可以用簡單的形式表達的就不用複雜的形式表達，除非你是莎士比亞）。&lt;&#x2F;p&gt;
&lt;p&gt;上圖中，被列入「Legecy」的，指的是因為流程的安全設計不夠嚴謹而不再被推薦使用的 flow。&lt;&#x2F;p&gt;
&lt;p&gt;偷偷查一下 legecy 的意思：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;oauth2-implicit-flow&#x2F;legecy.png&quot; alt=&quot;legecy&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：&lt;a href=&quot;https:&#x2F;&#x2F;dictionary.cambridge.org&#x2F;zht&#x2F;%E8%A9%9E%E5%85%B8&#x2F;%E8%8B%B1%E8%AA%9E-%E6%BC%A2%E8%AA%9E-%E7%B9%81%E9%AB%94&#x2F;legacy&quot;&gt;劍橋詞典&lt;&#x2F;a&gt;&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;「Legecy」之中的 password flow 講白了就是「你各位給我 Google 帳密，我去跟 Goole 做認證授權」，這種令人不安心的方式，被淘汰是理所當然的…。&lt;&#x2F;p&gt;
&lt;p&gt;而另一個也被列於「Legecy」的 implicit flow，雖然也是 legecy，但最常用的 authorization code flow 是以 implicit flow 為基礎強化的，所以往往提到 authorization code flow 的文章，也都會從 implicit flow 介紹起，本文也不例外。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;oauth2-implicit-flow&quot;&gt;OAuth2 Implicit Flow&lt;&#x2F;h2&gt;
&lt;p&gt;再偷偷查一下 implicit 的意思：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;img src=&quot;implicit.png&quot; alt=&quot;implicit&quot; style=&quot;max-width: fit-content; margin-left: auto; margin-right: auto; display: block; margin-bottom: 1rem;&quot;&gt;
&lt;figcaption&gt;來源：&lt;a href=&quot;https:&#x2F;&#x2F;dictionary.cambridge.org&#x2F;zht&#x2F;%E8%A9%9E%E5%85%B8&#x2F;%E8%8B%B1%E8%AA%9E-%E6%BC%A2%E8%AA%9E-%E7%B9%81%E9%AB%94&#x2F;implicit&quot;&gt;劍橋詞典&lt;&#x2F;a&gt;&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;不懂它的明白沒關係，直接從流程圖上手：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;oauth2-implicit-flow&#x2F;oauth2-implicit-flow.png&quot; alt=&quot;OAuth2 Implicit Flow&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;看起來很簡單，因為這是簡化過的流程圖，省略了資料拋接要帶的參數，較完整的說明請見下文：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;1. 用戶在 app 按下「登入」，被導入到 OAuth 服務的登入頁。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;一個典型的導向 OAuth 登入頁的網址如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;https:&#x2F;&#x2F;authorization-server.com&#x2F;authorize?&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  response_type=token&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &amp;amp;client_id=IfPrlAnhL-PhLa1qffmrRBxI&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &amp;amp;redirect_uri=https:&#x2F;&#x2F;ccc.app&#x2F;auth&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &amp;amp;scope=photo&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &amp;amp;state=hodYP_CTNRJoIK4P&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;參數說明：&lt;&#x2F;p&gt;
&lt;div class=&quot;wide&quot;&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;參數&lt;&#x2F;th&gt;&lt;th&gt;說明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;response_type&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;在 implicit flow 中，固定放 &lt;code&gt;token&lt;&#x2F;code&gt;，在其他 OAuth2 flow 中有可能是放別的值。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;client_id&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;通常是事先向 OAuth2 服務業者事先註冊取得的 app ID。如果 OAuth2 server 也是自幹，那就前後端團隊自行定義&lt;strong&gt;取得共識&lt;&#x2F;strong&gt;就好。（此點可能是導入 OAuth2 最困難的所在）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;redirect_uri&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;OAuth2 server 那端的認證授權完成後，要把用戶導回來的 callback URI，此 URI 用於接回用戶以及用戶的 token，在步驟四會再提到。往往實務上此 URI 也是要事先在 OAuth2 服務業者那邊先註冊過，不給你亂指定的。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;scope&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;app 要取得之授權範圍，內文依照 OAuth2 服務業者訂定的格式填入。如果 OAuth2 server 也是自幹，那就前後端團隊自行定義&lt;strong&gt;取得共識&lt;&#x2F;strong&gt;就好。（此點可能也是導入 OAuth2 最困難的所在）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;state&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;一組自定義亂數字串，在此送往 OAuth2 server，在 &lt;code&gt;redirect_uri&lt;&#x2F;code&gt; 接回用戶與參數時，OAuth2 server 也應該要帶上同樣的 &lt;code&gt;state&lt;&#x2F;code&gt; 字串，讓 app 得以透過 &lt;code&gt;state&lt;&#x2F;code&gt; 確認 session 的一致性，因為 HTTP 協議是&lt;strong&gt;無狀態&lt;&#x2F;strong&gt;的。也可確認回應真的是來自正確的 OAuth2 server。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;上面這些參數是 implicit flow 的基本款，某些業者（例如微軟和 Google）還會根據他們的需求，要求更多的參數。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;2. 用戶輸入帳密登入，被跳轉到授權同意頁。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;3. 行禮如儀的問「要分享ＸＸ給 ccc app 嗎？」，取得用戶同意。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;「要分享ＸＸ給 ccc app 嗎？」的ＸＸ取決於我們前面送的 &lt;code&gt;scope&lt;&#x2F;code&gt;，實際上 OAuth2 業者都會要求要事先審查，否則大多只能要求到帳號名稱和信箱這些超基本資訊。如果沒有事先通過審查，就直接在 &lt;code&gt;scope&lt;&#x2F;code&gt; 亂討授權，那被導過去的用戶會看到一個糟糕的錯誤訊息，嚴重影響用戶體驗：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;img src=&quot;twitter-error.png&quot; alt=&quot;Twitter Error&quot; style=&quot;max-width: fit-content; margin-left: auto; margin-right: auto; display: block; margin-bottom: 1rem;&quot;&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;&lt;strong&gt;4. 同意後用戶被導回 app，並在導回網址上附加 token 與其他參數。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;一個典型的導回網址與參數如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;https:&#x2F;&#x2F;ccc.app&#x2F;auth#&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  state=hodYP_CTNRJoIK4P&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &amp;amp;access_token=Ll8MyHHO3G6DRs5cHMyErDH0zrBtPG-JIehnvnJ84cpvMgIbW_YdZr9Ov-aByi4doGmEgVLi&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &amp;amp;token_type=Bearer&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &amp;amp;expires_in=86400&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &amp;amp;scope=photos&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;要稍微注意的是，參數皆是位於 &lt;code&gt;#&lt;&#x2F;code&gt; 起頭的 fragment 區段內，而非常見的以 &lt;code&gt;?&lt;&#x2F;code&gt; 起頭的 query 區段內。&lt;&#x2F;p&gt;
&lt;p&gt;參數說明：&lt;&#x2F;p&gt;
&lt;div class=&quot;wide&quot;&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;參數&lt;&#x2F;th&gt;&lt;th&gt;說明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;state&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;意義同前述。此時我們的 ccc app 應該檢查兩次往來的 &lt;code&gt;state&lt;&#x2F;code&gt; 是否一致。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;access_token&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;代表用戶的 token，此 token 為往後 app 發往後端 API 時做為令牌之用。對後端來說，如果是自幹 OAuth2 server，發行此 token 時建議以 JWT 格式發行，才可以直接對 app 送來的 token 做時間與簽章驗證，否則又要回歸到比對 session ID 的老路了。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;token_type&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;在 implicit flow 中，固定是放 &lt;code&gt;Bearer&lt;&#x2F;code&gt;，在其他 OAuth2 flow 中有可能是放別的值。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;expires_in&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;token 的有效秒數，過期後，後端就不認可此 token 了，得重新要過，重新要的機制，在 implicit flow 的設計上，是不允許在有效期內「拿 token 換 token」的（refresh token，此處的「refresh」是動詞），得搭配其他認證機制才可以拿到新 token，最擾民的作法當然是登出用戶把流程從頭來過，但應該是沒人會這麼做啦…。（其他的 OAuth2 flow 另有 &lt;code&gt;refresh_token&lt;&#x2F;code&gt;（此處的「&lt;code&gt;refresh_token&lt;&#x2F;code&gt;」是名詞）的存在，在此不表。）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;scope&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;意義同前述。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;&lt;strong&gt;5. App 透過網址上附帶的參數取得 token，對 token 做驗證，把 token 存到 local storage 備用。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;此處的驗證即前項之 &lt;code&gt;state&lt;&#x2F;code&gt; 一致性驗證。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;6. 往後 app 就可以用 token 當令箭向後端其他的 API 存取該用戶授權的資料。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;App 可以在向 API 端點發送請求時在 HTTP header 的 &lt;code&gt;Authorizatoin&lt;&#x2F;code&gt; 欄位填入 access token 作為認證之用：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Authorization: Bearer Ll8MyHHO3G6DRs5cHMyErDH0zrBtPG-JIehnvnJ84cpvMgIbW_YdZr9Ov-aByi4doGmEgVLi&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;implicit-flow-de-an-quan-wen-ti&quot;&gt;Implicit Flow 的安全問題&lt;&#x2F;h2&gt;
&lt;p&gt;Implicit flow 最大的問題是在上面的第四步，&lt;code&gt;access_token&lt;&#x2F;code&gt; 是直接放在網址內，而網址有許多人或物都讀得到：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;😇 用戶讀得到&lt;&#x2F;li&gt;
&lt;li&gt;😈 隔壁老王斜眼也讀得到&lt;&#x2F;li&gt;
&lt;li&gt;😇 瀏覽器讀得到&lt;&#x2F;li&gt;
&lt;li&gt;😈 瀏覽器的 add-on &#x2F; extension 也讀得到&lt;&#x2F;li&gt;
&lt;li&gt;😇 我們的 web app 讀得到&lt;&#x2F;li&gt;
&lt;li&gt;😈 我們的 web app 的前端套件也讀得到&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在瀏覽器，隨便一行 JavaScript 就讀得到：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;let&lt;&#x2F;span&gt;&lt;span&gt; access_token&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; = new&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; URLSearchParams&lt;&#x2F;span&gt;&lt;span&gt;(window.location.hash).&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;get&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;access_token&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我們可以採取一些措施彌補這個缺陷：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;發送有效期超短的 token，例如一個小時，搭配不擾民的刷新機制，就算偷到了也不能幹嘛&lt;&#x2F;li&gt;
&lt;li&gt;在資料拋接加入更多的防盜欄位，微軟好像就是這樣做的&lt;&#x2F;li&gt;
&lt;li&gt;在環境可控的範圍內使用，本文多次提到「自幹 OAuth2 server」是其來有自&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;以上都是治標不治本，因為 implicit flow 不夠安全，現在都建議改用 authorization code flow，關於 authorization code flow，請敬待後續分享。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ke-hu-duan-shi-zuo&quot;&gt;客戶端實做&lt;&#x2F;h2&gt;
&lt;p&gt;在前面的六大步驟中，有幾個必須是由我們的 web app 實做的，瀏覽器這邊當然有現成的套件可以使用，我們選用的是 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;mulesoft-labs&#x2F;js-client-oauth2&quot;&gt;Mulesoft Labs 的 &lt;code&gt;client-oauth2&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; 套件。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;an-zhuang&quot;&gt;安裝&lt;&#x2F;h3&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;npm&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; install client-oauth2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;jian-li-oauth2-client-shi-li&quot;&gt;建立 OAuth2 Client 實例&lt;&#x2F;h3&gt;
&lt;p&gt;針對一個特定的 OAuth2 server 建立 OAuth2 client 與填入基本端點資料：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;import&lt;&#x2F;span&gt;&lt;span&gt; ClientOAuth2&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; from&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;client-oauth2&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;const&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; implicitAuth&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; = new&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; ClientOAuth2&lt;&#x2F;span&gt;&lt;span&gt;( {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	clientId:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;IfPrlAnhL-PhLa1qffmrRBxI&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	authorizationUri:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;https:&#x2F;&#x2F;authorization-server.com&#x2F;authorize&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	redirectUri:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;https:&#x2F;&#x2F;ccc.app&#x2F;auth&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	scopes: [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;photo&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;],&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	state:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;hodYP_CTNRJoIK4P&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;} );&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這個 OAuth2 client 實例可以幫我們組出上節中第一步驟中要給用戶登入的連結：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;const&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; implicitAuthUri&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; implicitAuth.token.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;getUri&lt;&#x2F;span&gt;&lt;span&gt;();&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;把這個 &lt;code&gt;implicitAuthUri&lt;&#x2F;code&gt; 綁定到一個按鈕上讓用戶點過去即可。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;qu-de-yong-hu-token&quot;&gt;取得用戶 Token&lt;&#x2F;h3&gt;
&lt;p&gt;用戶在 authorization-server.com 登入成功後，會跳轉到我們指定的 &lt;code&gt;redirectiUri&lt;&#x2F;code&gt; 頁面，我們在此往頁面內呼叫實例的 &lt;code&gt;getToken()&lt;&#x2F;code&gt; 函式取得 token：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;const&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; uri&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; window.location.href&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; &#x2F;&#x2F; https:&#x2F;&#x2F;ccc.app&#x2F;auth&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;let&lt;&#x2F;span&gt;&lt;span&gt; token;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;window.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;onload&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; = async&lt;&#x2F;span&gt;&lt;span&gt; ()&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  token&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; = await&lt;&#x2F;span&gt;&lt;span&gt; implicitAuth.token.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;getToken&lt;&#x2F;span&gt;&lt;span&gt;( uri );&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;那 &lt;code&gt;getToken()&lt;&#x2F;code&gt; 會幫我們檢查 &lt;code&gt;redirectUri&lt;&#x2F;code&gt; 及 &lt;code&gt;state&lt;&#x2F;code&gt; 是否與前後一致。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;bao-chi-state&quot;&gt;保持 State&lt;&#x2F;h4&gt;
&lt;p&gt;用戶在登入的流程中經歷了網域的跳轉（ccc.app → authorization-server.com → ccc.app），session 已斷，如何保持 &lt;code&gt;state&lt;&#x2F;code&gt; 值是個問題，開新視窗或許是個方法，但瀏覽器會擋，或者是把 &lt;code&gt;state&lt;&#x2F;code&gt; 的值存在用戶的 local storage 內也是一招。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;bao-cun-token&quot;&gt;保存 Token&lt;&#x2F;h3&gt;
&lt;p&gt;取得 token 後，把 token 保存在 local storage 備用：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;localStorage.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;setItem&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;token&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, token.accessToken);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;shi-yong-token&quot;&gt;使用 Token&lt;&#x2F;h3&gt;
&lt;p&gt;往後想要調用 authorization-server.com 上面其他的 API，就得在 header 附上 token 作為令牌：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;const&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; token&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; localStorage.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;getItem&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;token&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;let&lt;&#x2F;span&gt;&lt;span&gt; userName;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;window.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;onLoad&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; = async&lt;&#x2F;span&gt;&lt;span&gt; ()&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  userName&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; = await&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; fetch&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;https:&#x2F;&#x2F;authorization-server.com&#x2F;users&#x2F;name&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    { headers: { Authorization:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; `Bearer ${&lt;&#x2F;span&gt;&lt;span&gt;token&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;}`&lt;&#x2F;span&gt;&lt;span&gt; } }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  );&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  localStorage.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;setItem&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;userName&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; await&lt;&#x2F;span&gt;&lt;span&gt; userName.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;json&lt;&#x2F;span&gt;&lt;span&gt;());&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;};&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;oauth2-playground&quot;&gt;OAuth2 Playground&lt;&#x2F;h2&gt;
&lt;p&gt;想要玩玩 OAuth2 的朋友可以用下面的幾個 playground 試試：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.oauth.com&#x2F;playground&#x2F;&quot;&gt;OAuth.com OAuth 2.0 Playground&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;developers.google.com&#x2F;oauthplayground&#x2F;&quot;&gt;Google OAuth 2.0 Playground&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;openidconnect.net&#x2F;&quot;&gt;OpenID Connect Playground&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>以 Authlib 實現 OAuth 1 的 Twitter 登入</title>
        <published>2022-06-13T00:00:00+00:00</published>
        <updated>2022-06-13T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/twitter-oauth-1/"/>
        <id>https://editor.leonh.space/2022/twitter-oauth-1/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/twitter-oauth-1/">&lt;p&gt;雖然 Twitter 在台灣一直不是主流，不過在國外 Twitter 是可以與 FB 相抗衡的平台之一，而目前主流的第三方認證機制 OAuth 最初也是由 Twitter 設計的，不過相較於 LINE、Google、FB 都已經採用 OAuth 2 標準做為第三方登入的機制，反而 Twitter 是少數還在用 OAuth 1 的業者，這裡我們會用 Authlib 套件來實做 Twitter 登入，並且盡量白話的講講 OAuth 1 的認證流程。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;oauth-1&quot;&gt;OAuth 1&lt;&#x2F;h2&gt;
&lt;p&gt;這裡的 OAuth 1 主要是參考 Twitter 的文件而來，畢竟現在似乎只剩下 Twitter 還在用 OAuth 1 了⋯⋯吧？！&lt;&#x2F;p&gt;
&lt;p&gt;我們從大流程講起，從用戶按下「Sign in with Twitter」開始，會進行下面的程序：&lt;&#x2F;p&gt;
&lt;div style=&quot;padding: 1em; background-color: hsla(0, 0%, 96%, 1.0); margin-bottom: 1em;&quot;&gt;
&lt;ol&gt;
&lt;li&gt;用戶在我們的 app 按下「Sign in with Twitter」&lt;&#x2F;li&gt;
&lt;li&gt;我們的 app 對 Twitter 發送一個 callback 網址及請求一組 request token&lt;&#x2F;li&gt;
&lt;li&gt;Twitter 回覆 request token 以及認可 callback 網址&lt;&#x2F;li&gt;
&lt;li&gt;將用戶重導到 Twitter 的認證頁面&lt;&#x2F;li&gt;
&lt;li&gt;用戶授權我們的 app 取得他的 access token&lt;&#x2F;li&gt;
&lt;li&gt;Twitte 回覆同樣的 request token 以及一個 request token verifier 給我們的 app&lt;&#x2F;li&gt;
&lt;li&gt;用戶從 Twitter 的認證頁被重導到 callback 網址&lt;&#x2F;li&gt;
&lt;li&gt;我們的 app 發送 request token 和 request token verifier 給 Twitter 要求用他們取得 access token&lt;&#x2F;li&gt;
&lt;li&gt;Twitter 回覆 access token 與 access token secret&lt;&#x2F;li&gt;
&lt;li&gt;我們的 app 把用戶導向會員頁面&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;也可以配圖看，順便伸展一下脖子：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;twitter-oauth-1&#x2F;sing-in-with-twitter-sequence-diagram.png&quot; alt=&quot;Sign in with Twitter Sequence Diagram&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;走這一大段路，最終要拿到的就是 access token，但得先拿到 request token 和 request token secret，而 request token secret 又得透過用戶認證取得，取得後再拿 request token &amp;amp; request token secret 向 Twitter 取得 access token。&lt;&#x2F;p&gt;
&lt;p&gt;雖然寫起來彷彿經歷千辛萬苦，但實際上整個過程不超過十秒，如同滄海一粟，稍縱即逝。&lt;&#x2F;p&gt;
&lt;!--
&lt;figure class=&quot;wide&quot;&gt;
&lt;div class=&quot;wsd&quot; wsd_style=&quot;omegapple&quot;&gt;
&lt;pre&gt;
User -&gt; App: 1. User 按「Login with Twitter」
App -&gt; Twitter: 2. App 發送 callback 網址請求 request token
Twitter -&gt; App: 3. Twitter 回覆 request token 認可 callback 網址
App -&gt; User: 4. App 將 user 導到 Twitter 認證頁
User -&gt; Twitter: 5. User 授權 app 可取得 access token
Twitter -&gt; App: 6. Twitter 回覆 request token 與 request token secret
Twitter -&gt; User: 7. Twitter 把 User 導至 app 的 callback 網址
App -&gt; Twitter: 8. App 用 request token 和 request token secret 向 Twitter 請求 Access token
Twitter -&gt; App: 9. Twitter 回覆 access token 和 access token secret
App -&gt; User: 10. App 將 user 導到 app 會員頁
&lt;&#x2F;pre&gt;
&lt;&#x2F;div&gt;
&lt;&#x2F;figure&gt;
&lt;script type=&quot;text&#x2F;javascript&quot; src=&quot;https:&#x2F;&#x2F;www.websequencediagrams.com&#x2F;service.js&quot;&gt;&lt;&#x2F;script&gt;
--&gt;
&lt;p&gt;上面的流程實際上是簡化過的版本，真正 post 來 post 的去的資料要複雜的多，並且還要用 HMAC-SHA1 編碼的簽名做防偽，幸好各大語言早就都有 OAuth 套件可以直接使用，省去自己造輪子的功夫，所以我們不求甚解的直接跳到使用套件的部份。（技術債 + 1、time to market - 3，划算）&lt;&#x2F;p&gt;
&lt;p&gt;對於 OAuth 1 細節有興趣的朋友，推薦這篇〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;joeyon1985&#x2F;article&#x2F;details&#x2F;42026039&quot;&gt;OAuth 1.0 簡介&lt;&#x2F;a&gt;〉。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yi-authlib-shi-xian-twitter-deng-ru&quot;&gt;以 Authlib 實現 Twitter 登入&lt;&#x2F;h2&gt;
&lt;p&gt;第一步是文書作業，到 Twitter Developer Portal 開立專案，啟用 3-legged OAuth，取得 Twitter API Key 以及 Twitter API Secret Key，這組 API key 在前面的 OAuth 1 流程稱為 consumer key，這種名稱不一致的情況在下文會持續發生，一個物件在 OAuth、Twitter、Authlib 內各自表述，就像台灣中國九二會談一樣毫無共識。&lt;&#x2F;p&gt;
&lt;p&gt;下面的表格整理了三者命名的對應，希望不要被這些混亂的命名搞亂。&lt;&#x2F;p&gt;
&lt;div class=&quot;wide&quot;&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;OAuth&lt;br&gt;命名&lt;&#x2F;th&gt;&lt;th&gt;Twitter&lt;br&gt;命名&lt;&#x2F;th&gt;&lt;th&gt;Authlib&lt;br&gt;命名&lt;&#x2F;th&gt;&lt;th&gt;用途&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Service Provider&lt;&#x2F;td&gt;&lt;td&gt;Twitter&lt;&#x2F;td&gt;&lt;td&gt;OAuth Server&lt;&#x2F;td&gt;&lt;td&gt;提供 OAuth 認證的平台&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Consumer&lt;&#x2F;td&gt;&lt;td&gt;App&lt;&#x2F;td&gt;&lt;td&gt;OAuth Client&lt;&#x2F;td&gt;&lt;td&gt;向 OAuth server 要求認證的 app&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Consumer Key&lt;&#x2F;td&gt;&lt;td&gt;API Key&lt;&#x2F;td&gt;&lt;td&gt;Client ID&lt;&#x2F;td&gt;&lt;td&gt;存取  Twitter API&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Consumer Secret&lt;&#x2F;td&gt;&lt;td&gt;API Secret Key&lt;&#x2F;td&gt;&lt;td&gt;Client Secret&lt;&#x2F;td&gt;&lt;td&gt;產生 HMAC-SHA1 防偽簽名&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Callback&lt;&#x2F;td&gt;&lt;td&gt;Callback URL&lt;&#x2F;td&gt;&lt;td&gt;Redirct URI&lt;&#x2F;td&gt;&lt;td&gt;用戶認證後 Twitter 把用戶導向 app 的網址&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;&#x2F;div&gt;
&lt;iframe src=&quot;https:&#x2F;&#x2F;www.youtube-nocookie.com&#x2F;embed&#x2F;_JgorcJcyus&quot; title=&quot;YouTube video player&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;authlib.org&#x2F;&quot;&gt;Authlib&lt;&#x2F;a&gt; 是 Python 的套件，同時支援 OAuth 1 與 OAuth 2，它可以讓我們的 app 有 OAuth server 或 OAuth client 的能力，OAuth server 指的是像 Twitter 這樣提供第三方登入服務的角色，又稱為 OAuth service provider，而 OAuth client 則是我們的 app，又稱為 OAuth consumer，這也是我們持有的 API key 被稱為 consumer key 的原因。&lt;&#x2F;p&gt;
&lt;p&gt;下文開始 Authlib 實現 Twitter 登入，以下的程式碼都執行在 Python shell 內，並且程式碼經過排版便於閱讀。&lt;&#x2F;p&gt;
&lt;p&gt;引入 &lt;code&gt;authlib&lt;&#x2F;code&gt; 元件及定義一些基礎參數：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; authlib.integrations.requests_client&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; OAuth1Session&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;client_id:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;      =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;my_client_id&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;        # TWITTER_API_KEY&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;client_secret:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;my_client_secret&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    # TWITTER_API_SECRET_KEY&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;client: OAuthSession&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; OAuth1Session(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    client_id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;       =&lt;&#x2F;span&gt;&lt;span&gt; client_id,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    client_secret&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;   =&lt;&#x2F;span&gt;&lt;span&gt; client_secret&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我們會用這個 &lt;code&gt;client&lt;&#x2F;code&gt; 與 Twitter API 做交流。上面的 &lt;code&gt;client_id&lt;&#x2F;code&gt; 與 &lt;code&gt;client_secret&lt;&#x2F;code&gt; 的值比較正規的做法應該用 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;theskumar&#x2F;python-dotenv&quot;&gt;python-dotenv&lt;&#x2F;a&gt; 隱藏起來，但這裡就不講究那麼多了。&lt;&#x2F;p&gt;
&lt;p&gt;用 &lt;code&gt;client&lt;&#x2F;code&gt; 去向 Twitter API 取得 &lt;code&gt;request_token&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;request_token_url:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;https:&#x2F;&#x2F;api.twitter.com&#x2F;oauth&#x2F;request_token&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;request_token:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;dict&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; client.fetch_request_token(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;url&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;request_token_url)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;print&lt;&#x2F;span&gt;&lt;span&gt;(request_token)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# request_token&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;oauth_token&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;               :&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;my_oauth_token&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;oauth_token_secret&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;        :&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;my_auth_token_secret&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;oauth_callback_confirmed&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;  :&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;true&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意到在流程圖有提到要求 request token 時要傳送一個 callback URL，但是在此並沒有指定，Twitter 會用 Twitter Developer Portal 內的 Callback URLs 的第一個值，所以不用在程式內指定，想在程式內指定也是可以的，以 staging 環境為例：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; authlib.integrations.requests_client&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; OAuth1Session&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;client_id:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;      =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;my_client_id&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;        # TWITTER_API_KEY&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;client_secret:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;my_client_secret&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    # TWITTER_API_SECRET_KEY&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;redirect_uri&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;https:&#x2F;&#x2F;staging.example.com&#x2F;auth&#x2F;twitter&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;client: OAuthSession&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; OAuth1Session(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    client_id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;       =&lt;&#x2F;span&gt;&lt;span&gt; client_id,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    client_secret&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;   =&lt;&#x2F;span&gt;&lt;span&gt; client_secret,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    redirect_uri&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    =&lt;&#x2F;span&gt;&lt;span&gt; redirect_uri&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;不過這裡的 &lt;code&gt;redirect_uri&lt;&#x2F;code&gt; 的網址也還是得要事先登錄到 Twitter Developer Portal 的 Callback URLs，否則是會被 Twitter API 阻擋的。&lt;&#x2F;p&gt;
&lt;p&gt;和 Twitter API 的交互之所以看起來這麼簡單是 Authlib 的功勞，它幫我們做掉了 OAuth 1 要求的其他欄位的值以及生成防偽簽章的苦工，只留下真正必要的參數給我們填入。&lt;&#x2F;p&gt;
&lt;p&gt;用剛拿到的 &lt;code&gt;request_token&lt;&#x2F;code&gt; 去組出一個 Twitter 用戶認證網址：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;oauth_token:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; request_token[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;oauth_token&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;oauth_token_secret:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; request_token[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;oauth_token_secret&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;authenticate_url:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;https:&#x2F;&#x2F;api.twitter.com&#x2F;oauth&#x2F;authenticate&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;client.create_authorization_url(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    url&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;             =&lt;&#x2F;span&gt;&lt;span&gt; authenticate_url,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    request_token&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;   =&lt;&#x2F;span&gt;&lt;span&gt; oauth_token&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# Authorization URL&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;https:&#x2F;&#x2F;api.twitter.com&#x2F;oauth&#x2F;authenticate?oauth_token=my_oauth_token&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;得到 authorization URL 後，把用戶導過去，用戶會看到 Twitter 的認證畫面：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;twitter-oauth-1&#x2F;authorize-logged-in.png.twimg.1920.png&quot; alt=&quot;Sign in with Twitter&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;用戶按下 Sing In 後，Twitter 會把用戶重導到我們指定的 callback URL，並附上我們的 &lt;code&gt;oauth_token&lt;&#x2F;code&gt; 與 &lt;code&gt;oauth_verifier&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;假設我們的 app 在 Twitter Developer Portal 登錄的 callback URL 是 &lt;code&gt;https:&#x2F;&#x2F;example.com&#x2F;auth&#x2F;twitter&lt;&#x2F;code&gt;，Twitter 導引用戶回來時會在 callback URL 後附加 &lt;code&gt;oauth_token&lt;&#x2F;code&gt; 與 &lt;code&gt;oauth_verifier&lt;&#x2F;code&gt; 兩個參數，因此收到的請求是這樣的：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;resp_url:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;https:&#x2F;&#x2F;example.com&#x2F;auth&#x2F;twitter?oauth_token=my_oauth_token&amp;amp;oauth_verifier=my_oauth_verifier&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;再拿 &lt;code&gt;oauth_token&lt;&#x2F;code&gt; 與 &lt;code&gt;oauth_verifier&lt;&#x2F;code&gt; 去向 Twitter API 要 access token：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;client.parse_authorization_response(resp_url)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;access_token_url:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;https:&#x2F;&#x2F;api.twitter.com&#x2F;oauth&#x2F;access_token&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;token:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; dict&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; client.fetch_access_token(access_token_url)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;print&lt;&#x2F;span&gt;&lt;span&gt;(token)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# token&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;oauth_token&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;       :&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;user_oauth_token&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;oauth_token_secret&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;user_oauth_token_secret&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;user_id&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;           :&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;user_user_id&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;screen_name&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;       :&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;user_screen_name&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意，最後拿到的 &lt;code&gt;oauth_token&lt;&#x2F;code&gt; 和 &lt;code&gt;oauth_token_secret&lt;&#x2F;code&gt; 是代表那位用戶的，而在此之前和 Twitter API 交互的 &lt;code&gt;oauth_token&lt;&#x2F;code&gt; 是屬於我們的 app 的，名字一樣但意義不同。&lt;&#x2F;p&gt;
&lt;p&gt;最後拿到的代表用戶的 &lt;code&gt;oauth_token&lt;&#x2F;code&gt;、&lt;code&gt;oauth_token_secret&lt;&#x2F;code&gt;、&lt;code&gt;user_id&lt;&#x2F;code&gt;、&lt;code&gt;screen_name&lt;&#x2F;code&gt; 應該存入我們自己的會員資料庫內，做為後續認證之用，但要注意的是不要拿 &lt;code&gt;screen_name&lt;&#x2F;code&gt; 辨識用戶，因為 Twitter 的用戶名是可以改的。&lt;&#x2F;p&gt;
&lt;p&gt;有了用戶授權給我們的 access token，除了讓我們做第三方登入外，也可以拿 access token 去做其他的應用，例如獲得用戶帳號的其它資料，用的是 &lt;code&gt;verify_credentials&lt;&#x2F;code&gt; 這支 API：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;twitter_account_verify_credentials_uri&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;https:&#x2F;&#x2F;api.twitter.com&#x2F;1.1&#x2F;account&#x2F;verify_credentials.json&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;resp&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; client.get(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    url&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;     =&lt;&#x2F;span&gt;&lt;span&gt; twitter_account_verify_credentials_uri,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    params&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;skip_status&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; True&lt;&#x2F;span&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;print&lt;&#x2F;span&gt;&lt;span&gt;(resp.json())&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# resp.json()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    ...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;protected&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; False&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;friends_count&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 5&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;favourites_count&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 9&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;utc_offset&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; None&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;time_zone&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; None&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;geo_enabled&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; False&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;verified&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; False&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;statuses_count&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 7&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;lang&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; None&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;contributors_enabled&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; False&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;is_translator&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; False&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;is_translation_enabled&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; False&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;profile_background_tile&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; False&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;profile_use_background_image&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; True&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;has_extended_profile&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; True&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;default_profile&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; True&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;default_profile_image&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; False&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;following&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; False&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;follow_request_sent&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; False&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;notifications&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; False&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;translator_type&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;none&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;suspended&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; False&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;needs_phone_verification&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; False&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    ...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;當然能拿到的資料僅限於用戶在 Twitter 認證頁同意授權給我們的部份，另外在 Twitter Developer Portal 也要對額外要求的用戶資料目的提交給 Twitter 審查，以最基本的用戶 email 為例，就要提供 terms of service 和 privacy policy 給 Twitter 才能取得。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;authlib-he-web-kuang-jia-de-zheng-he&quot;&gt;Authlib 和 Web 框架的整合&lt;&#x2F;h3&gt;
&lt;p&gt;前面的範例是以 Authlib 做為 OAuth client 實做 OAuth 1 流程的程式碼，目的是親自實證 OAuth 1 的交互流程，而在真實的 web 後端應用場景，Authlib 有更好的與 web 框架整合好的 API 可以使用，目前 Authlib 已有與 Starlette、&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2023&#x2F;pydantic&#x2F;&quot;&gt;FastAPI&lt;&#x2F;a&gt;、Flask、Django 的整合模組。&lt;&#x2F;p&gt;
&lt;p&gt;以 Starlette 為例，一個最陽春的 Starlette web app 大概長這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; starlette.applications&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Starlette&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; starlette.requests&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Request&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;app&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; Starlette()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;先幫它加上 &lt;code&gt;authlib&lt;&#x2F;code&gt; 的元件及初始化：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; starlette.applications&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Starlette&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; starlette.requests&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Request&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; authlib.integrations.starlette_client&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; OAuth&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;app&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; Starlette()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;oauth&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; OAuth()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;再加上 OAuth server 的各個基本 API 網址：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; starlette.applications&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Starlette&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; starlette.requests&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Request&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; authlib.integrations.starlette_client&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; OAuth&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;app&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; Starlette()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;oauth&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; OAuth()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;oauth.register(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;                    =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;twitter&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    api_base_url&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;            =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;https:&#x2F;&#x2F;api.twitter.com&#x2F;1.1&#x2F;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    request_token_url&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;       =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;https:&#x2F;&#x2F;api.twitter.com&#x2F;oauth&#x2F;request_token&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    request_token_params&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; None&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    access_token_url&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;https:&#x2F;&#x2F;api.twitter.com&#x2F;oauth&#x2F;access_token&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    access_token_params&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;     =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; None&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    authorize_url&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;           =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;https:&#x2F;&#x2F;api.twitter.com&#x2F;oauth&#x2F;authenticate&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    aurhorize_params&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; None&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    client_id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;               =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;my_client_id&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    client_secret&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;           =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;my_client_secret&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    client_kwargs&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;           =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; None&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;因為 OAuth 1 是標準化的協議，所以必然會有 &lt;code&gt;request_token_url&lt;&#x2F;code&gt;、&lt;code&gt;access_token_url&lt;&#x2F;code&gt;、&lt;code&gt;authorize_url&lt;&#x2F;code&gt;，也必然會有 &lt;code&gt;client_id&lt;&#x2F;code&gt;、&lt;code&gt;client_secret&lt;&#x2F;code&gt; 兩個值，儘管命名可能不一樣。&lt;&#x2F;p&gt;
&lt;p&gt;再次提醒，&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2023&#x2F;pydantic&#x2F;&quot;&gt;&lt;code&gt;client_id&lt;&#x2F;code&gt;、&lt;code&gt;client_secret&lt;&#x2F;code&gt; 的值應該要放在 .env 內&lt;&#x2F;a&gt;，這裡只是示範才省略。&lt;&#x2F;p&gt;
&lt;p&gt;加上兩個 route，一個是讓用戶按下 Sign in with Twitter 的網址，一個是讓 Twitter callback 回來的網址：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; starlette.applications&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Starlette&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; starlette.requests&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Request&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; authlib.integrations.starlette_client&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; OAuth&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;app&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; Starlette()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;oauth&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; OAuth()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;oauth.register(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;                    =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;twitter&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    api_base_url&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;            =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;https:&#x2F;&#x2F;api.twitter.com&#x2F;1.1&#x2F;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    request_token_url&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;       =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;https:&#x2F;&#x2F;api.twitter.com&#x2F;oauth&#x2F;request_token&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    request_token_params&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; None&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    access_token_url&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;https:&#x2F;&#x2F;api.twitter.com&#x2F;oauth&#x2F;access_token&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    access_token_params&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;     =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; None&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    authorize_url&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;           =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;https:&#x2F;&#x2F;api.twitter.com&#x2F;oauth&#x2F;authenticate&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    aurhorize_params&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; None&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    client_id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;               =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;my_client_id&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    client_secret&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;           =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;my_client_secret&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    client_kwargs&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;           =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; None&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@app.route&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&#x2F;login&#x2F;twitter&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;async def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; login_via_twitter&lt;&#x2F;span&gt;&lt;span&gt;(request: Request):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    twitter&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; oauth.create_client(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;twitter&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    redirect_uri&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; request.url_for(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;authorize_twitter&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return await&lt;&#x2F;span&gt;&lt;span&gt; twitter.authorize_redirect(request, redirect_uri)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@app.route&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&#x2F;auth&#x2F;twitter&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;async def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; authorize_twitter&lt;&#x2F;span&gt;&lt;span&gt;(request: Request):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    twitter&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; oauth.create_client(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;twitter&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    token&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; = await&lt;&#x2F;span&gt;&lt;span&gt; twitter.authorize_access_token(request)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    user&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; = await&lt;&#x2F;span&gt;&lt;span&gt; twitter.parse_id_token(request, token)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return&lt;&#x2F;span&gt;&lt;span&gt; user&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Authlib 對 web 框架的整合封裝度更高，前面我們還要手動照 OAuth 1 流程跑，取得 request token 什麼的，這裡完全不用，一個 &lt;code&gt;authorize_redirect()&lt;&#x2F;code&gt; 就把拿 request token 和導引用戶到 Twitter 認證頁的工作全做掉了，後面的 callback 網址收到 Twitter 導回來的用戶也是一個函式 &lt;code&gt;authorize_access_token()&lt;&#x2F;code&gt; 搞定前面手動拿 token 交換來交換去的工作，真心感謝 Authlib 大大，祝 Authlib 大大好人一生平安。&lt;&#x2F;p&gt;
&lt;p&gt;最後，Authlib 有用到 Starlette 的 session middleware 管理用戶從我們的 app 跳轉到 Twitter 又跳回來的 session，所以再把 session middleware 加進來：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; starlette.applications&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Starlette&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; starlette.requests&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Request&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; starlette.middleware.sessions&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; SessionMiddleware&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; authlib.integrations.starlette_client&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; OAuth&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;app&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; Starlette()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;app.add_middleware(SessionMiddleware,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt; secret_key&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;㊙️&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;oauth&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; OAuth()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;oauth.register(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;                    =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;twitter&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    api_base_url&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;            =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;https:&#x2F;&#x2F;api.twitter.com&#x2F;1.1&#x2F;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    request_token_url&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;       =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;https:&#x2F;&#x2F;api.twitter.com&#x2F;oauth&#x2F;request_token&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    request_token_params&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; None&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    access_token_url&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;https:&#x2F;&#x2F;api.twitter.com&#x2F;oauth&#x2F;access_token&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    access_token_params&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;     =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; None&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    authorize_url&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;           =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;https:&#x2F;&#x2F;api.twitter.com&#x2F;oauth&#x2F;authenticate&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    aurhorize_params&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; None&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    client_id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;               =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;my_client_id&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    client_secret&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;           =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;my_client_secret&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    client_kwargs&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;           =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; None&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@app.route&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&#x2F;login&#x2F;twitter&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;async def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; login_via_twitter&lt;&#x2F;span&gt;&lt;span&gt;(request: Request):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    twitter&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; oauth.create_client(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;twitter&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    redirect_uri&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; request.url_for(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;authorize_twitter&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return await&lt;&#x2F;span&gt;&lt;span&gt; twitter.authorize_redirect(request, redirect_uri)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@app.route&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&#x2F;auth&#x2F;twitter&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;async def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; authorize_twitter&lt;&#x2F;span&gt;&lt;span&gt;(request: Request):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    twitter&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; oauth.create_client(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;twitter&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    token&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; = await&lt;&#x2F;span&gt;&lt;span&gt; twitter.authorize_access_token(request)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    user&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; = await&lt;&#x2F;span&gt;&lt;span&gt; twitter.parse_id_token(request, token)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return&lt;&#x2F;span&gt;&lt;span&gt; user&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這裡是以 Starlette 做示範，在其他的 web 框架上也差不多，三招搞定 OAuth client：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;register()&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;authorize_redirect()&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;authorize_access_token()&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;如果是以 Starlette 為基礎的 FastAPI，那上面那塊程式碼更是可以幾乎完全套用。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;qian-hou-duan-fen-li-xia-de-oauth-1-liu-cheng&quot;&gt;前後端分離下的 OAuth 1 流程&lt;&#x2F;h2&gt;
&lt;p&gt;上面我們談到 web 後端的 OAuth 1 流程，如果把前後端分離的狀況也考慮進來，那流程會更複雜：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;twitter-oauth-1&#x2F;frontend-backend-oauth1-Flow.png&quot; alt=&quot;Frontend &#x2F; Backend &#x2F; Twitter OAuth 1 Flow&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;流程如下：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;用戶在前端按下「Sign in with Twitter」後，後端向 Twitter 索取 request token 並得到 Twitter 認證頁的網址，把 Twitter 認證頁網址丟給前端，前端把用戶導到 Twitter 認證頁。&lt;&#x2F;li&gt;
&lt;li&gt;用戶在 Twitter 認證頁按下 Sign In，Twitter 把用戶導到我們指定的 callback URL，並得到 request token verifier，前端把 request token verifier 傳給後端，後端拿 request token 和 request token verifier 向 Twitter 索取用戶的 access token。&lt;&#x2F;li&gt;
&lt;li&gt;得到用戶的 access token 後，後端產生自己的 bearer token 交給前端，至此用戶登入完畢，爾後前端發送到後端的請求都附上 bearer token 識別身份，這裡的 bearer token 是 OAuth 2 的 bearer token，也就是說在我們的前端和後端之間，又走了最陽春的 OAuth 2 的認證機制，這部分並非本文的重點，在此點到為止，後續有機會再分享此處的細節。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;這個流程的特點是 consumer key、consumer secret、request token secret、access token 都保留在後端，暴露到前端的則有 request token 和 request token verifier，因此：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;整套 OAuth 1 流程中，用於簽名防偽的主角是 consumer secret，它是始終只在後端的，可以保障前端即使被植入惡意程式碼也無法自行對 Twitter 溝通。&lt;&#x2F;li&gt;
&lt;li&gt;用戶的 access token 也只在後端，同樣的保障了 access token 被盜的風險。&lt;&#x2F;li&gt;
&lt;li&gt;最後用於前後端認證的是我們自己發行的 bearer token（JWT），同樣的，它也有簽名防偽的機制，後端得以驗證前端的請求是否被竄改過，藉此保證了前端請求的真實性。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;然而上述的流程並非唯一選項，有人也把和 Twitter OAuth 1 交互的部份都做在前端，這部份可以參閱〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;kernelpanic.io&#x2F;demystifying-authentication-with-fastapi-and-a-frontend&quot;&gt;Demystifying authentication with FastAPI and a frontend&lt;&#x2F;a&gt;〉。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-yu&quot;&gt;結語&lt;&#x2F;h2&gt;
&lt;p&gt;雖然 Authlib 把 OAuth 複雜的細節封裝成很簡單的三大函式，但對不了解 OAuth 1 流程的開發者來說，恐怕會陷入知其然卻不知期所以然的狀態，所以很不幸的，還是得回頭去認識 OAuth 1 的交互流程。&lt;&#x2F;p&gt;
&lt;p&gt;另外本篇的最開頭有提到，Twitter 是目前僅存仍在使用 OAuth 1 的大戶，而從 Twitter 開發人員的網站上也可以看到，Twitter 正在逐步向 OAuth 2 遷移，所以本文很有可能再過一季就會成為廢文，屆時再也沒有 OAuth 1 應用的場景，在花時間閱讀前請謹慎評估，雖然你已經看到最末了。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>真・用 OpenAPI 打通前後端任督二脈</title>
        <published>2022-06-11T00:00:00+00:00</published>
        <updated>2022-06-11T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/openapi-ts/"/>
        <id>https://editor.leonh.space/2022/openapi-ts/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/openapi-ts/">&lt;p&gt;前一篇〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;openapi&#x2F;&quot;&gt;OpenAPI 打通前後端任督二脈&lt;&#x2F;a&gt;〉介紹了 OpenAPI，以及與 OpenAPI 深度整合的 Python web 框架 FastAPI，但在前端讀取 OpenAPI 並轉為程式碼部份，身為 Swagger 家族～祖傳～正宗～本家～嫡系～的 Swagger Client 卻不怎麼香，沒帶來多少便利性，也無法讓我們少奮鬥三十年。&lt;&#x2F;p&gt;
&lt;p&gt;直到看到一套 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;ferdikoomen&#x2F;openapi-typescript-codegen&quot;&gt;OpenAPI Typescript Codegen&lt;&#x2F;a&gt;，用了覺得真的挺不錯的，特地撰文以資表揚。&lt;&#x2F;p&gt;
&lt;p&gt;顧名思義，OpenAPI Typescript Codegen 會讀入 OpenAPI 定義的 API 規格，並產出一系列 TypeScript 模組，讓我們在前端專案可以方便調用。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;2024 年起 OpenAPI Typescript Codegen 原開發者沒時間照顧它，由新團隊接手另起爐灶推出新專案 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;hey-api&#x2F;openapi-ts&quot;&gt;OpenAPI Typescript&lt;&#x2F;a&gt;，下文也以新的 OpenAPI Typescript 為基礎改寫。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;an-zhuang&quot;&gt;安裝&lt;&#x2F;h2&gt;
&lt;p&gt;它的安裝方式如同一般典型 JS 套件：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; npm install @hey-api&#x2F;openapi-ts&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --save-dev&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;chan-sheng-openapi-ke-hu-duan-mo-zu&quot;&gt;產生 OpenAPI 客戶端模組&lt;&#x2F;h2&gt;
&lt;p&gt;安裝後專案內會多一個 &lt;code&gt;openapi-ts&lt;&#x2F;code&gt; 命令，我們用它來產出 API 的 TS 模組：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; npx openapi-ts&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --input&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; http:&#x2F;&#x2F;localhost:5000&#x2F;openapi.json&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --output&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; .&#x2F;src&#x2F;openapi-client&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面的參數應該是可以望文生義，我們可以把它寫為 NPM 指令，在 package.json scripts 區塊加入下例：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;json&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;scripts&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  &amp;quot;openapi-ts&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;openapi-ts --input http:&#x2F;&#x2F;localhost:5000&#x2F;openapi.json --output .&#x2F;src&#x2F;openapi-client&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;},&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;往後只要執行 &lt;code&gt;npx openapi-ts&lt;&#x2F;code&gt; 就可以。&lt;&#x2F;p&gt;
&lt;p&gt;或者可以把參數寫入 openapi-ts.config.ts 檔案內，如下例：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;typescript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;import&lt;&#x2F;span&gt;&lt;span&gt; { defineConfig }&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; from&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;@hey-api&#x2F;openapi-ts&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;export default&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; defineConfig&lt;&#x2F;span&gt;&lt;span&gt;( {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    input:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;http:&#x2F;&#x2F;localhost:5000&#x2F;openapi.json&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    output:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;src&#x2F;openapi-client&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;} )&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如此 package.json 中的指令可以簡化為 &lt;code&gt;&quot;openapi-ts&quot;: &quot;openapi-ts&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;那個來源的 openapi.json 檔案可能如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;openapi-ts&#x2F;fastapi-overview.png&quot; alt=&quot;Swagger UI&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;div style=&quot;max-height: 90dvh; overflow: scroll&quot;&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;json&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  &amp;quot;openapi&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;3.0.2&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  &amp;quot;info&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;title&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;FastAPI&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;version&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;0.1.0&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  &amp;quot;servers&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: [&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;      &amp;quot;url&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;http:&#x2F;&#x2F;localhost:5000&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ],&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  &amp;quot;paths&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;&#x2F;items&#x2F;{item_id}&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;      &amp;quot;get&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;summary&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Read Item&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;description&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Read item by id.&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;operationId&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;read_item&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;parameters&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: [&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;required&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;schema&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;              &amp;quot;title&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Item Id&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;              &amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;integer&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;name&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;item_id&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;in&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;path&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        ],&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;responses&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;          &amp;quot;200&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;description&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Successful Response&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;content&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;              &amp;quot;application&#x2F;json&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;                &amp;quot;schema&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;                  &amp;quot;$ref&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;#&#x2F;components&#x2F;schemas&#x2F;Item&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;              }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;          &amp;quot;422&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;description&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Validation Error&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;content&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;              &amp;quot;application&#x2F;json&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;                &amp;quot;schema&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;                  &amp;quot;$ref&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;#&#x2F;components&#x2F;schemas&#x2F;HTTPValidationError&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;              }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;      &amp;quot;delete&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;summary&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Delete Item&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;description&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Delete item by id.&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;operationId&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;delete_item&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;parameters&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: [&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;required&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;schema&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;              &amp;quot;title&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Item Id&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;              &amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;integer&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;name&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;item_id&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;in&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;path&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        ],&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;responses&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;          &amp;quot;200&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;description&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Successful Response&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;content&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;              &amp;quot;application&#x2F;json&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;                &amp;quot;schema&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;              }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;          &amp;quot;422&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;description&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Validation Error&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;content&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;              &amp;quot;application&#x2F;json&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;                &amp;quot;schema&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;                  &amp;quot;$ref&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;#&#x2F;components&#x2F;schemas&#x2F;HTTPValidationError&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;              }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  &amp;quot;components&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;schemas&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;      &amp;quot;HTTPValidationError&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;title&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;HTTPValidationError&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;object&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;properties&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;          &amp;quot;detail&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;title&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Detail&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;array&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;items&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;              &amp;quot;$ref&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;#&#x2F;components&#x2F;schemas&#x2F;ValidationError&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;      &amp;quot;Item&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;title&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Item&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;required&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: [&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;          &amp;quot;id&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;          &amp;quot;name&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;          &amp;quot;quantity&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        ],&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;object&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;properties&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;          &amp;quot;id&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;title&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Id&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;integer&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;          &amp;quot;name&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;title&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Name&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;string&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;          &amp;quot;quantity&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;title&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Quantity&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;integer&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;      &amp;quot;ValidationError&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;title&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;ValidationError&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;required&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: [&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;          &amp;quot;loc&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;          &amp;quot;msg&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;          &amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        ],&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;object&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;properties&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;          &amp;quot;loc&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;title&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Location&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;array&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;items&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;              &amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;string&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;          &amp;quot;msg&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;title&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Message&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;string&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;          &amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;title&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Error Type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;string&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;&#x2F;div&gt;
&lt;p&gt;從 openapi.json 產生出的程式如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;openapi-ts&#x2F;.&#x2F;openapi-client.avif&quot; alt=&quot;OpenAPI client&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;其中：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;core&#x2F; 是底層共用的模組，本質上它是 Fetch API 的再封裝。&lt;&#x2F;li&gt;
&lt;li&gt;services.gen.ts 是串接 API 的主要函式所在。&lt;&#x2F;li&gt;
&lt;li&gt;schemas.gen.ts 是 openapi.json 內抽出來的 JSON Schema。&lt;&#x2F;li&gt;
&lt;li&gt;types.gen.ts 顧名思義就是那些從 JSON schema 轉換來的 TS type。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;這些 service、type、schema 都暴露在 index.ts 內，所以可以直接 import 整個模組調用。&lt;&#x2F;p&gt;
&lt;p&gt;對照上面 Swagger UI 的資訊，Swagger UI 中的「Read Item」會變成 &lt;code&gt;readItem()&lt;&#x2F;code&gt; 函式，而 Item schema 會分別對應到 &lt;code&gt;Item&lt;&#x2F;code&gt; type 與 &lt;code&gt;$Item&lt;&#x2F;code&gt; schema。&lt;&#x2F;p&gt;
&lt;p&gt;讓我們看以下的例子。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shi-yong-openapi-ke-hu-duan-mo-zu&quot;&gt;使用 OpenAPI 客戶端模組&lt;&#x2F;h2&gt;
&lt;p&gt;使用上很簡單，例如要向「Read Item」要資料：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;import&lt;&#x2F;span&gt;&lt;span&gt; { readItem }&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; from&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;.&#x2F;openapi-client&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;const&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; itemId&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 102573&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;async function&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; refreshItem&lt;&#x2F;span&gt;&lt;span&gt;(()&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  const&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt; response&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; = await&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; readItem&lt;&#x2F;span&gt;&lt;span&gt;(itemId)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  consle&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;debug&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;response&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; &#x2F;&#x2F; {id: 102573, name: &amp;#39;Nameless&amp;#39;, quantity: 0, ... }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;})&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;受惠於產生器和 TypeScript 完整的型別定義特性，IDE 的提示和補完機能得以滿載發揮，你幾乎不用去看 Swagger UI 就能給出該支 API 所需要的資料。&lt;&#x2F;p&gt;
&lt;p&gt;回應也是一樣，&lt;code&gt;response&lt;&#x2F;code&gt; 帶有完整型別定義，我們無須反覆查閱 Swagger UI。&lt;&#x2F;p&gt;
&lt;p&gt;不論是 GET、DELETE、POST、PUT 等等，都是如此這般調用，再也不用手刻 xxx 攔截器，再也不用自行二次封裝，靠 OpenAPI Typescript 產生的懶人包就能讓我們優雅調用 API。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;shen-fen-ren-zheng&quot;&gt;身份認證&lt;&#x2F;h3&gt;
&lt;p&gt;要附加認證資訊當然也可以，OpenAPI 本身支援好幾種 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;spec.openapis.org&#x2F;oas&#x2F;latest.html#security-scheme-object&quot;&gt;security scheme&lt;&#x2F;a&gt;，而 OpenAPI Typescript 則支援 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;zh-tw&#x2F;HTTP%E5%9F%BA%E6%9C%AC%E8%AE%A4%E8%AF%81&quot;&gt;HTTP Basic 認證&lt;&#x2F;a&gt; 和 OAuth 2 的 access token 認證。&lt;&#x2F;p&gt;
&lt;p&gt;以 access token 為例，可以這麼使用：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;import&lt;&#x2F;span&gt;&lt;span&gt; { OpenAPI, readItem }&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; from&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;.&#x2F;openapi-client&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;&#x2F;&#x2F; 全域附上 access token&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;OpenAPI.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;TOKEN&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;eyJhXXX.eyJzXXX.SflKXXX&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;const&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; itemId&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 102082&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;async function&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; refreshItem&lt;&#x2F;span&gt;&lt;span&gt;(()&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  const&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt; response&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; = await&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; readItem&lt;&#x2F;span&gt;&lt;span&gt;(itemId)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  consle&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;debug&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;response&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;})&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;只要這麼一行，下面的所有交互都會自動附上該 token，蒸的很蚌！&lt;&#x2F;p&gt;
&lt;p&gt;至於那 access token 該怎麼來就取決於 API 服務端的設計，如果是 OAuth 2 password flow，僅須一次交互，那也可以用產生的客戶端模組取得，如果是需要多次交互的，例如 &lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;oauth2-implicit-flow&#x2F;&quot;&gt;OAuth 2 implicit flow&lt;&#x2F;a&gt;、authorization code flow、PKCE flow 等，那就要自己動手取得囉，畢竟那些複雜的 flow 還有可能需要「前端的後端」參與，無法單靠一個前端套件搞定。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;she-ding-hou-duan-base-url&quot;&gt;設定後端 base URL&lt;&#x2F;h3&gt;
&lt;p&gt;上面引入的 &lt;code&gt;OpenAPI&lt;&#x2F;code&gt; 是一個 &lt;code&gt;OpenAPIConfig&lt;&#x2F;code&gt; 型態的物件，除了可以用來配置 token 外，還有個很重要的特性是配置後端位址，例如在正式環境後端位址寫於正式環境的 .env 中，再於程式中讀入：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;import&lt;&#x2F;span&gt;&lt;span&gt; { OpenAPI, readItem }&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; from&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;.&#x2F;openapi-client&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;&#x2F;&#x2F; 附上 token 與指定後端 base URL&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;OpenAPI.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;TOKEN&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;eyJhXXX.eyJzXXX.SflKXXX&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;OpenAPI.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;BASE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; = import&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;meta&lt;&#x2F;span&gt;&lt;span&gt;.env.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;VITE_BACKEND_BASE_URL&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;const&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; itemId&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 102082&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;async function&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; refreshItem&lt;&#x2F;span&gt;&lt;span&gt;(()&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  const&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt; response&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; = await&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; readItem&lt;&#x2F;span&gt;&lt;span&gt;(itemId)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  consle&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;debug&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;response&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;})&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;只要設定一次，後面所有的請求都會改向新定的 base URL，並且附上 access token。&lt;&#x2F;p&gt;
&lt;p&gt;除上例外，也可以配置發出請求時的 HTTP 標頭等參數，取決於需求運用囉！&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-yu&quot;&gt;結語&lt;&#x2F;h2&gt;
&lt;p&gt;OpenAPI 已經是實質的產業標準，許多 open data 供應方都有提供 OpenAPI 文件，配合 OpenAPI Typescript 就可以加快客端的開發腳步，而如果是自有產品，那前後端之間更是需要像 OpenAPI 這樣的橋樑建立起溝通的管道，除非你很喜歡用 Word 寫文件。&lt;&#x2F;p&gt;
&lt;p&gt;相較於在 Word 寫，直接從程式碼轉出成 OpenAPI 顯得有效率多了，後端從程式碼產生 OpenAPI 文件，前端再從 OpenAPI 文件產生程式，就像一條運作順暢的流水線，自在極意、通體舒暢。&lt;&#x2F;p&gt;
&lt;p&gt;除了 Python 的 FastAPI，其他的框架大都有內建或外掛的 OpenAPI 整合，例如 APIFlask、&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;strapi&#x2F;&quot;&gt;Strapi&lt;&#x2F;a&gt;、&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;directus.io&#x2F;&quot;&gt;Directus&lt;&#x2F;a&gt; 等等，可以多加利用。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>OpenAPI 打通前後端任督二脈</title>
        <published>2022-06-08T00:00:00+00:00</published>
        <updated>2022-06-08T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/openapi/"/>
        <id>https://editor.leonh.space/2022/openapi/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/openapi/">&lt;p&gt;過去曾經寫過一篇〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;apiary&#x2F;&quot;&gt;Apiary API 規格文件＋假接口一次到位&lt;&#x2F;a&gt;〉，簡單的說，Apiary 是幫我們產生 API 文件與假接口的工具，讓別人可以在真正的後端 API 完工前就著手開工前端的工作，而定義 Apiary 的文件是一種稱為 API Blueprint 的格式，它是以 Markdown 為基礎的擴充格式，也就是說，&lt;strong&gt;繞一圈回過頭來你各位終究還是要寫文件的&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;問題是工程師最討厭三件事：寫註解、寫測試、寫文件。&lt;&#x2F;p&gt;
&lt;p&gt;然而：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;註解可以不寫，因為好的程式碼是自我詮釋的&lt;&#x2F;li&gt;
&lt;li&gt;測試可以不寫，因為可以丟給 QA 寫 🤨&lt;&#x2F;li&gt;
&lt;li&gt;文件不可不寫，因為你的前端妹子同事會抓狂&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;於是有人發明工具，從程式碼自動產出文件，於是程式碼有了，文件也有了，接口也有了，而且比 Apiary 更棒的是，是&lt;strong&gt;真．接口&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;通常這類的工具採用的文件規範是 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.openapis.org&#x2F;&quot;&gt;OpenAPI&lt;&#x2F;a&gt; 規範，在上手工具之前，先來快速了解一下 OpenAPI。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;openapi&quot;&gt;OpenAPI&lt;&#x2F;h2&gt;
&lt;p&gt;OpenAPI 是用於描述 API 資訊的文件，包括 API 的端點、參數、輸出入格式、說明、認證等，本質上它是一個 JSON 或 YAML 文件，而文件內的 schema 則是由 OpenAPI 定義。&lt;&#x2F;p&gt;
&lt;p&gt;下面是一份 OpenAPI JSON 文件的範例：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;json&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  &amp;quot;openapi&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;3.0.0&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  &amp;quot;info&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;title&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;TestAPI&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;description&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Bare en liten test&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;version&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;1.0&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  &amp;quot;servers&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: [&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;      &amp;quot;url&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;https:&#x2F;&#x2F;api.example.com&#x2F;v1&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ],&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  &amp;quot;paths&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;&#x2F;users&#x2F;{userId}&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;      &amp;quot;get&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;summary&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Returns a user by ID&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;parameters&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: [&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;name&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;userId&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;in&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;path&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;description&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;The ID of the user to return&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;required&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;style&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;simple&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;explode&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; false&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;schema&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;              &amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;integer&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        ],&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;responses&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;          &amp;quot;200&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;description&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Successful response&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;content&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;              &amp;quot;application&#x2F;json&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;                &amp;quot;schema&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;                  &amp;quot;$ref&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;#&#x2F;components&#x2F;schemas&#x2F;inline_response_200&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;              }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;          &amp;quot;400&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;description&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;The specified user ID is invalid (e.g. not a number)&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;          &amp;quot;404&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;description&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;A user with the specified ID was not found&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  &amp;quot;components&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;schemas&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;      &amp;quot;inline_response_200&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;object&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;properties&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;          &amp;quot;id&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;integer&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;          &amp;quot;name&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;string&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;裡面的 schema 有些可望文生義，剩下的不用完全懂，JSON 本來就不是人讀而是機讀的文件，有工具會幫我們產出人讀的文件。:)&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;openapi&#x2F;Ip.Man.2.2010.DVDRip.XviD.AC3-ViSiON%5B(090659)15-24-49%5D.JPG&quot; alt=&quot;說人話！&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：洪金寶&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;有許多的工具都可以讀取 OpenAPI JSON &#x2F; YAML 檔案並產出功能豐富的 web 文檔，包括：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;swagger.io&#x2F;tools&#x2F;swagger-ui&#x2F;&quot;&gt;Swagger UI&lt;&#x2F;a&gt; 🆓 &#x2F; &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;swagger.io&#x2F;tools&#x2F;swaggerhub&#x2F;&quot;&gt;SwaggerHub&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;redoc.ly&#x2F;redoc&quot;&gt;REDOC&lt;&#x2F;a&gt; 🆓 &#x2F; &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;redoc.ly&#x2F;reference-docs&quot;&gt;Redocly&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;stoplight.io&#x2F;open-source&#x2F;elements&#x2F;&quot;&gt;Stoplight Elements&lt;&#x2F;a&gt; 🆓 &#x2F; &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;stoplight.io&#x2F;api-documentation&#x2F;&quot;&gt;Stoplight&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;readme.com&#x2F;documentation&quot;&gt;ReadMe Documentation&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;這些工具中，最常見的應該是本家的 Swagger UI（OpenAPI 在成為開放規格以前，是 Swagger 產品線的一部分），以上面的那份範例為例，經過 Swagger UI 處理後會產出如下的 web 文檔：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;openapi&#x2F;testapi.png&quot; alt=&quot;TestAPI&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：&lt;a href=&quot;https:&#x2F;&#x2F;app.swaggerhub.com&#x2F;apis-docs&#x2F;heidi&#x2F;TestAPI&#x2F;1.0#&#x2F;default&quot;&gt;TestAPI&lt;&#x2F;a&gt;&lt;&#x2F;figcaption&gt;
&lt;!-- https:&#x2F;&#x2F;app.swaggerhub.com&#x2F;apis&#x2F;heidi&#x2F;TestAPI&#x2F;1.0#&#x2F; --&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;我們可以看到，web 文檔告訴我們 API 的端點位址、要打哪些參數、回應的格式、錯誤回應有哪些、還可以試玩，這些內容在上面提到的工具都有，形式上大同小異罷了。&lt;&#x2F;p&gt;
&lt;p&gt;然而這一切的根源 OpenAPI JSON &#x2F; YAML 終究還是要用某種手段生出來，否則無法解決&lt;strong&gt;不寫文件卻又不能沒有文件&lt;&#x2F;strong&gt;的需求，於是乎有人開發出從程式碼自動產出 OpenAPI JSON &#x2F; YAML 的工具，例如 Python 的 FastAPI。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;openapi-yu-fastapi&quot;&gt;OpenAPI 與 FastAPI&lt;&#x2F;h2&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;openapi&#x2F;29516980-f308-11e9-9096-0836920fdae3.png&quot; alt=&quot;FastAPI&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：FastAPI&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;fastapi.tiangolo.com&#x2F;zh&#x2F;&quot;&gt;FastAPI&lt;&#x2F;a&gt; 是 Python 的 web 框架，它有這些特性：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;高效能而功能豐富。通常功能越多，體積越大，效能越差，而 FastAPI 在效能與功能間取了一個很好的平衡點。&lt;&#x2F;li&gt;
&lt;li&gt;整合了 &lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2023&#x2F;pydantic&#x2F;&quot;&gt;pydantic&lt;&#x2F;a&gt; 的資料驗證功能。&lt;&#x2F;li&gt;
&lt;li&gt;無綁定 ODM &#x2F; ORM，可以自由搭配任意的 ODM 操作 MongoDB 這類的文件型資料庫，或任意的 ORM 操作 SQLite 這類的關聯資料庫。&lt;&#x2F;li&gt;
&lt;li&gt;整合了 OpenAPI、Swagger UI、REDOC。&lt;&#x2F;li&gt;
&lt;li&gt;生態健康，目前 GitHub 星星數近四萬，僅次於老牌的 Django（六萬星）與 Flask（近六萬星）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;fastapi-ji-jian-shi-yong&quot;&gt;FastAPI 極簡使用&lt;&#x2F;h3&gt;
&lt;p&gt;FastAPI 是 decorator 風格的框架，我們對函式加上 FastAPI 提供的裝飾器，即可讓該函式成為一個 API 端點：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; fastapi&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; FastAPI&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;app: FastAPI&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; FastAPI()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@app.get&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;path&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&#x2F;items&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;{item_id}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; read_item&lt;&#x2F;span&gt;&lt;span&gt;(item_id:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; int&lt;&#x2F;span&gt;&lt;span&gt;) -&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; dict&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;item_id&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;: item_id}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面的陽春 API 範例中，我們做了幾件事：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@app.get()&lt;&#x2F;code&gt; 裝飾器定義了此函式 &lt;code&gt;read_item()&lt;&#x2F;code&gt; 接受 HTTP 的 GET 請求&lt;&#x2F;li&gt;
&lt;li&gt;對外暴露的 API 端點為 &lt;code&gt;&#x2F;items&#x2F;{item_id}&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;item_id&lt;&#x2F;code&gt; 則在函式簽名內定義型態應為整數&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;!-- - 我們只要專注於處理我們預先定義好的 `item_id` 即可，如果有特殊需求想要拿到完整的 request，也是可以的，在此不展開。 --&gt;
&lt;p&gt;在談到 OpenAPI 前，先把 pydantic 的資料驗證的特性整合進來，讓範例更接近實用一點。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;fastapi-pydantic&quot;&gt;FastAPI + pydantic&lt;&#x2F;h3&gt;
&lt;p&gt;沿用上面的例子，我們把 item 的真身做出來：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; fastapi&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; FastAPI&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; pydantic&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; BaseModel&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; Item&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;BaseModel&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    id&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; int&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    name:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    quantity:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; int&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;app: FastAPI&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; FastAPI()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@app.get&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;path&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&#x2F;items&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;{item_id}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; read_item&lt;&#x2F;span&gt;&lt;span&gt;(item_id:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; int&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    item: Item&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; get_single_item_by_id(item_id)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return&lt;&#x2F;span&gt;&lt;span&gt; item&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;與前一個範例比起來，這邊多了幾個特性：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;用 &lt;code&gt;Item&lt;&#x2F;code&gt; class 定義了 &lt;code&gt;item&lt;&#x2F;code&gt; 物件內的欄位與型態&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;read_item()&lt;&#x2F;code&gt; 直接回傳 &lt;code&gt;item&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;如果要表現更多 FastAPI 對 OpenAPI 整合特性，還有更多錦上添花的部份可以摻進來。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;fastapi-openapi&quot;&gt;FastAPI + OpenAPI&lt;&#x2F;h3&gt;
&lt;p&gt;我們加入更多完善 OpenAPI 文檔的元素：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; fastapi&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; FastAPI&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; pydantic&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; BaseModel&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; Item&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;BaseModel&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    id&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; int&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    name:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    quantity:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; int&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;app: FastAPI&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; FastAPI(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;servers&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;[{&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;url&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;http:&#x2F;&#x2F;localhost:5000&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;}])&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@app.get&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    path&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&#x2F;items&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;{item_id}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    operation_id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;read_item&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    response_model&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;Item,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; read_item&lt;&#x2F;span&gt;&lt;span&gt;(item_id:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; int&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;quot;&amp;quot;&amp;quot;Read item by id.&amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    item: Item&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; get_single_item_by_id(item_id)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return&lt;&#x2F;span&gt;&lt;span&gt; item&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@app.delete&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;path&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&#x2F;items&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;{item_id}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt; operation_id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;delete_item&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; delete_item&lt;&#x2F;span&gt;&lt;span&gt;(item_id:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; int&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;quot;&amp;quot;&amp;quot;Delete item by id.&amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    delete_single_item_by_id(item_id)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; None&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;相較於前一個範例，這邊又多了幾個特性：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;建構 FastAPI 實例時加入了 &lt;code&gt;servers&lt;&#x2F;code&gt; 參數，用於在 OpenAPI 文檔內指示 API 站台位址。&lt;&#x2F;li&gt;
&lt;li&gt;函式裝飾器加入了 &lt;code&gt;operation_id=&#x27;read_item&#x27;&lt;&#x2F;code&gt; 參數。&lt;&#x2F;li&gt;
&lt;li&gt;函式內加入了 docstring。&lt;&#x2F;li&gt;
&lt;li&gt;加入了另一個用於刪除的端點。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;code&gt;operationId&lt;&#x2F;code&gt; 是 OpenAPI 用於辨識 API 端點與操作的唯一值，由於在 RESTful API 的風格下，一個 API 端點 &lt;code&gt;&#x2F;items&#x2F;{item_id}&lt;&#x2F;code&gt; 可能會同時接受 GET、DELETE、PUT 等動作，每種動作分別對應到內部不同的處理函式，如同範例中所展示的那樣，但這些內部函式又不為外界所知，所以用 &lt;code&gt;operationId&lt;&#x2F;code&gt; 作為唯一的識別值有其必要性。&lt;&#x2F;p&gt;
&lt;p&gt;在一番折騰下，我們終於可以看看產出的 OpenAPI JSON 檔與 web 文檔：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;json&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  &amp;quot;openapi&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;3.0.2&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  &amp;quot;info&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;title&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;FastAPI&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;version&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;0.1.0&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  &amp;quot;servers&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: [&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;      &amp;quot;url&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;http:&#x2F;&#x2F;localhost:5000&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ],&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  &amp;quot;paths&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;&#x2F;items&#x2F;{item_id}&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;      &amp;quot;get&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;summary&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Read Item&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;description&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Read item by id.&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;operationId&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;read_item&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;parameters&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: [&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;required&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;schema&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;              &amp;quot;title&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Item Id&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;              &amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;integer&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;name&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;item_id&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;in&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;path&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        ],&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;responses&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;          &amp;quot;200&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;description&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Successful Response&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;content&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;              &amp;quot;application&#x2F;json&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;                &amp;quot;schema&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;                  &amp;quot;$ref&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;#&#x2F;components&#x2F;schemas&#x2F;Item&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;              }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;          &amp;quot;422&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;description&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Validation Error&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;content&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;              &amp;quot;application&#x2F;json&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;                &amp;quot;schema&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;                  &amp;quot;$ref&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;#&#x2F;components&#x2F;schemas&#x2F;HTTPValidationError&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;              }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;      &amp;quot;delete&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;summary&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Delete Item&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;description&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Delete item by id.&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;operationId&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;delete_item&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;parameters&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: [&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;required&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;schema&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;              &amp;quot;title&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Item Id&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;              &amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;integer&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;name&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;item_id&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;in&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;path&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        ],&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;responses&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;          &amp;quot;200&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;description&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Successful Response&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;content&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;              &amp;quot;application&#x2F;json&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;                &amp;quot;schema&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;              }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;          &amp;quot;422&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;description&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Validation Error&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;content&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;              &amp;quot;application&#x2F;json&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;                &amp;quot;schema&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;                  &amp;quot;$ref&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;#&#x2F;components&#x2F;schemas&#x2F;HTTPValidationError&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;              }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  &amp;quot;components&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;schemas&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;      &amp;quot;HTTPValidationError&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;title&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;HTTPValidationError&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;object&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;properties&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;          &amp;quot;detail&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;title&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Detail&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;array&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;items&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;              &amp;quot;$ref&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;#&#x2F;components&#x2F;schemas&#x2F;ValidationError&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;      &amp;quot;Item&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;title&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Item&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;required&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: [&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;          &amp;quot;id&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;          &amp;quot;name&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;          &amp;quot;quantity&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        ],&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;object&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;properties&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;          &amp;quot;id&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;title&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Id&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;integer&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;          &amp;quot;name&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;title&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Name&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;string&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;          &amp;quot;quantity&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;title&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Quantity&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;integer&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;      &amp;quot;ValidationError&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;title&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;ValidationError&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;required&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: [&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;          &amp;quot;loc&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;          &amp;quot;msg&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;          &amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        ],&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;object&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;properties&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;          &amp;quot;loc&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;title&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Location&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;array&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;items&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;              &amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;string&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;          &amp;quot;msg&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;title&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Message&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;string&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;          &amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;title&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Error Type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;string&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;          }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;同樣的，我們不追求用肉身理解 OpenAPI JSON，我們用 FastAPI 自動幫我們產出的 Swagger UI 的 web 文檔來輔助我們人類理解這份 API 的用法：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;openapi&#x2F;fastapi-overview.png&quot; alt=&quot;FastAPI - Swagger UI&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在上面的總覽檢視，可以看到與程式碼互相對應的部份：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Servers 區塊來自程式碼內的 &lt;code&gt;server&lt;&#x2F;code&gt; 參數&lt;&#x2F;li&gt;
&lt;li&gt;「GET &lt;code&gt;&#x2F;items&#x2F;{item_id}&lt;&#x2F;code&gt;」與「DELETE &lt;code&gt;&#x2F;items&#x2F;{item_id}&lt;&#x2F;code&gt;」分別對應到程式碼內的裝飾器與函式&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;把「GET &lt;code&gt;&#x2F;items&#x2F;{item_id}&lt;&#x2F;code&gt;」端點展開：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;openapi&#x2F;fastapi-endpoint.png&quot; alt=&quot;FastAPI - Swagger UI&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;可以看到：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;函式的 docstring 變成端點的說明文&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;item_id&lt;&#x2F;code&gt; 被列為參數，並且型態必須為整數，與程式內聲明的一致&lt;&#x2F;li&gt;
&lt;li&gt;成功回應的 JSON 格式與我們所定義的 &lt;code&gt;Item&lt;&#x2F;code&gt; model 一致&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在 FastAPI 框架中大量運用了 Python 的 type hints 特性與 pydantic 的型態驗證特性，在程式內的型態聲明不僅用於 OpenAPI 的文檔內，也真實的運作在端點函式內，pydantic 會自動幫我們檢查收到的參數型態是否相符，當一個聲明應為整數的參數，卻收到其他型別時，pydantic 會直接返回錯誤，這個特性為我們節省了大量的型態驗證工作，相較於&lt;strong&gt;不檢查&lt;&#x2F;strong&gt;或&lt;strong&gt;儲存才檢查&lt;&#x2F;strong&gt;，在更前方把不良的資料擋下是更好也更安全的策略。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;回到 OpenAPI 的主題，有與 OpenAPI 作原生整合的框架當然不只 FastAPI 一個，由 Flask 維護者之一李輝先生開發的 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;apiflask.com&#x2F;&quot;&gt;APIFlask&lt;&#x2F;a&gt; 也與 OpenAPI 做了原生整合，即便未有原生整合的框架，也大都有第三方套件可以幫助產出 OpenAPI 文件。&lt;&#x2F;p&gt;
&lt;p&gt;在此之前都是談後端與 OpenAPI 如何如何，下面談談 OpenAPI 在前端的運用。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;openapi-zai-qian-duan&quot;&gt;OpenAPI 在前端&lt;&#x2F;h2&gt;
&lt;p&gt;除了變出美美的 web 文檔外，OpenAPI JSON &#x2F; YAML 本身也是個機讀文件，裡面定義了後端 API 的 server、path、HTTP method、operation ID、parameter、schema 等前端會用到的資訊，那理所當然的也有套件可以幫助我們在前端專案用上 OpenAPI JSON &#x2F; YAML，這套件就是 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;swagger-api&#x2F;swagger-js&quot;&gt;Swagger Client&lt;&#x2F;a&gt;，Swagger Client 與 Swagger UI 一樣，也是 Swagger 家族～祖傳～正宗～本家～嫡系～的一員。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;swagger-client-yong-fa&quot;&gt;Swagger Client 用法&lt;&#x2F;h3&gt;
&lt;p&gt;一個絕簡單的使用範例：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;import&lt;&#x2F;span&gt;&lt;span&gt; SwaggerClient&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; from&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;swagger-client&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;async function&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; getOasClient&lt;&#x2F;span&gt;&lt;span&gt;() {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  return await&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; SwaggerClient&lt;&#x2F;span&gt;&lt;span&gt;({&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    url:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;http:&#x2F;&#x2F;localhost:5000&#x2F;openapi.json&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  });&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;async function&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; getItem&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;id&lt;&#x2F;span&gt;&lt;span&gt;) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  const&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; oasClient&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; = await&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; getOasClient&lt;&#x2F;span&gt;&lt;span&gt;();&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  const&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; response&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; = await&lt;&#x2F;span&gt;&lt;span&gt; oasClient.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;execute&lt;&#x2F;span&gt;&lt;span&gt;({&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    operationId:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;read_item&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    parameters: { item_id: id },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  });&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  return&lt;&#x2F;span&gt;&lt;span&gt; response.body&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;和用 &lt;code&gt;fetch()&lt;&#x2F;code&gt; 寫起來有變簡單嗎？好像差不多，因為：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;不用查 HTTP 方法與端點路徑&lt;&#x2F;li&gt;
&lt;li&gt;但要手動從 OpenAPI JSON &#x2F; YAML 查 Operation ID&lt;&#x2F;li&gt;
&lt;li&gt;要傳遞的參數、型態、回傳格式還是要對照 web 文檔看&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;沒有用了可以少奮鬥三十年的感覺…，讓我們結束這一回合。&lt;&#x2F;p&gt;
&lt;p&gt;前端的部份請參閱〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;openapi-ts&#x2F;&quot;&gt;真・用 OpenAPI 打通前後端任督二脈&lt;&#x2F;a&gt;〉&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zong-jie&quot;&gt;總結&lt;&#x2F;h2&gt;
&lt;p&gt;至今我們已經看過兩種 API 資訊交換格式 API Blueprint 與 OpenAPI，當然老外一向是造輪子在行的，還有其他的流派像是 RAML、AsyncAPI、AML &#x2F; AMF 等，目前看來 OpenAPI 已經是最常見的標準，其他流派也或多或少的往與 OpenAPI 相容的方向走，但唯一也是最大的例外是 GraphQL，相較於走 RESTful，以有固定的 schema 的 resource 為基礎的 OpenAPI，GraphQL 則是一種 &lt;strong&gt;Q&lt;&#x2F;strong&gt;uery &lt;strong&gt;L&lt;&#x2F;strong&gt;anguage，不存在固定的 schema，而是依查詢動態的回覆，兩種標準間很難做到不打折的轉換。&lt;&#x2F;p&gt;
&lt;p&gt;本文僅提到了 OpenAPI 工具的一小部份，OpenAPI JSON &#x2F; YAML 能做的還有很多，API 測試、監控、假 API、安全檢測等，在此附上兩個 OpenAPI 相關工具的連結：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;openapi.tools&#x2F;&quot;&gt;OpenAPI.Tools&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;OAI&#x2F;OpenAPI-Specification&#x2F;blob&#x2F;main&#x2F;IMPLEMENTATIONS.md&quot;&gt;OpenAPI Tools and Libraries&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;!--
在專案管理界，流傳著各式的流派，然而事實與理論總有差距：

&lt;div class=&quot;wide&quot;&gt;

| 流派 | 你以為的                    | 現實上的                    |
|------|-----------------------------|-----------------------------|
| TDD  | Test Driven Development     | Time Driven Development     |
| DDD  | Design Driven Development   | Deadline Driven Development |
| BDD  | Behavior Driven Development | Boss Driven Development     |
&lt;&#x2F;div&gt; --&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Apiary API 規格文件＋假接口一次到位</title>
        <published>2022-06-07T00:00:00+00:00</published>
        <updated>2022-06-07T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/apiary/"/>
        <id>https://editor.leonh.space/2022/apiary/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/apiary/">&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;apiary.io&#x2F;&quot;&gt;Apiary&lt;&#x2F;a&gt; 是個讓我們撰寫 API 規格的服務，但不僅如此，依照 Apiary 特有的 Markdown 語法寫下的 API 規格文件，Apiary 會根據規格文件幫我們產出文件與假資料的接口，本文介紹一下它的一些特色。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fei-yong-fang-an&quot;&gt;費用方案&lt;&#x2F;h2&gt;
&lt;p&gt;在入坑前一定要先了解他的費用，避免愛上後難以自拔。&lt;&#x2F;p&gt;
&lt;p&gt;Apiary 目前有免費與收費的方案，在它的網站上可以查到&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;apiary.io&#x2F;plans&quot;&gt;每個方案的差異&lt;&#x2F;a&gt;，不過必須特別提醒的是，Apiary 已經被甲骨文收購，並併入台灣沒什麼人用的 Oracle Cloud 產品線內，未來會不會依舊保持獨立的服務是有疑問的，另外就是甲骨文一貫的…作風，如果對甲骨文特別有疑慮的請自行斟酌使用。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;apiary&quot;&gt;Apiary&lt;&#x2F;h2&gt;
&lt;p&gt;回歸正題，Apiary 解決了我們以往在開發 API 時的痛點：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;規格文件先寫出來，但 API 還沒有完工，導致無法測試，只能先略過或是假裝 API 是好的來做測試，簡稱瞎測。&lt;&#x2F;li&gt;
&lt;li&gt;API 已經寫了，但沒空寫文件，導致串接參數像秘方一樣只能靠口耳相傳，比較邊緣的同事就難以實做串接，最後因為文件一直與實做脫節，導致產品難以維護。&lt;&#x2F;li&gt;
&lt;li&gt;有 API 也有文件，但不知道為何就是串不起來，不確定 API 寫錯還是文件寫錯，總之難免有錯。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;iframe src=&quot;https:&#x2F;&#x2F;www.youtube-nocookie.com&#x2F;embed&#x2F;clh-8tTNWOs&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
&lt;h2 id=&quot;api-blueprint&quot;&gt;API Blueprint&lt;&#x2F;h2&gt;
&lt;p&gt;前面提到過在 Apiary 撰寫文件必須使用它特規的 Markdown 語法，這種 Markdown 的變體有自己的名字 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;apiblueprint.org&#x2F;&quot;&gt;API Blueprint&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;API Blueprint 的語法以 Markdown 為基礎，只是加了一些為 API 文件需求而生的語法格式。&lt;&#x2F;p&gt;
&lt;p&gt;API Blueprint 有兩種型式，一種用於定義資料結構，例：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;markdown&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;font-weight: bold;&quot;&gt;# Data Structures&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;font-weight: bold;&quot;&gt;## Blog Post (object)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;+&lt;&#x2F;span&gt;&lt;span&gt; id: 42 (number, required)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;+&lt;&#x2F;span&gt;&lt;span&gt; text: Hello World (string)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;+&lt;&#x2F;span&gt;&lt;span&gt; author (Author) - Author of the blog post.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;font-weight: bold;&quot;&gt;## Author (object)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;+&lt;&#x2F;span&gt;&lt;span&gt; name: Boba Fett&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;+&lt;&#x2F;span&gt;&lt;span&gt; email: fett@intergalactic.com&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;應該可以望文生義。&lt;&#x2F;p&gt;
&lt;p&gt;有了資料結構後，就可以把前面定義的資料結構用於 API 定義文件內，例：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;markdown&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;font-weight: bold;&quot;&gt;# Blog Posts [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #DBEDFF;text-decoration: underline;&quot;&gt;&#x2F;posts&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;font-weight: bold;&quot;&gt;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;font-weight: bold;&quot;&gt;## Retrieve All Posts [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #DBEDFF;text-decoration: underline;&quot;&gt;GET&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;font-weight: bold;&quot;&gt;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;+&lt;&#x2F;span&gt;&lt;span&gt; Response 200 (application&#x2F;json)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;    +&lt;&#x2F;span&gt;&lt;span&gt; Attributes (array[Blog Post])&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;API Blueprint 的語法相較於另一派的 XML 或 JSON 都更簡單。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gui-ge-wen-jian-yu-jia-jie-kou&quot;&gt;規格文件與假接口&lt;&#x2F;h2&gt;
&lt;p&gt;用 API Blueprint 寫好定義文件後，Apiary 會依照文件內的定義，產生出 API 文件與假接口，具體範例可以參考這份官方範例 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;pandurangpatil.docs.apiary.io&#x2F;&quot;&gt;Sample API Documentation&lt;&#x2F;a&gt;，裡面所有的 API 接口都是可以被使用的。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;qi-ta-zhi-de-yi-ti-de-gong-neng&quot;&gt;其它值得一提的功能&lt;&#x2F;h2&gt;
&lt;p&gt;除了文件與接口外，另外值得一提的是 Apiary 支援 GitHub Sync，我們可以把 API 定義文件也放到程式專案資料夾內，享受版控的好處，同時透過 GitHub Sync 的機制，每次提交都會觸發 Apiary 也把發布的規格文件更新，好棒棒。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;can-kao-zi-liao&quot;&gt;參考資料&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;blog.alantsai.net&#x2F;posts&#x2F;2019&#x2F;03&#x2F;apiary-why-user-apiary-different-document-api-format-makes-back-and-front-communicate-easier&quot;&gt;〈[apiary][01] 設計 API 時好用的工具 - 讓前後端溝通格式不再卡卡 - 概念介紹篇〉&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>全世界最簡單的 Ansible 文章</title>
        <published>2022-05-20T00:00:00+00:00</published>
        <updated>2022-05-20T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/ansible/"/>
        <id>https://editor.leonh.space/2022/ansible/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/ansible/">&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;ansible&#x2F;ansible&quot;&gt;Ansible&lt;&#x2F;a&gt; 是 IT 界的戰略部署工具，只要制定好行動綱領，它就會依照劇本執行自動化部署，有能力讓我們跟美軍一樣在 24 小時內在全球任何一個節點上部署投射 IT 戰力。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ansible-ji-chu&quot;&gt;Ansible 基礎&lt;&#x2F;h2&gt;
&lt;p&gt;在 Ansible 的世界裡，有所謂的主控節點（control node）及受管節點（managed node），主控節點就是指揮部，通常就是我們手中那台工作用的電腦，主控電腦的起手式當然是把 Ansible 裝起來：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo add-apt-repository&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --yes --update&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ppa:ansible&#x2F;ansible&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo apt install ansible&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;而受管節點呢，不用裝任何代理器，這也是 Ansible 最大的特點，它是用 SSH 登進去工作的，而其他的類似工具則大多需要透過代理器才能工作，免代理器的設計能讓我們省一點心。&lt;&#x2F;p&gt;
&lt;p&gt;一般來說，受管節點不會只有一個，這也才顯得出 Ansible 的價值，這些受管節點可以用一個類似 hosts 的檔案整理之，看起來會像這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;ini&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;[webservers]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;192.168.122.17  &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ansible_user&lt;&#x2F;span&gt;&lt;span&gt;=web17 &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ansible_password&lt;&#x2F;span&gt;&lt;span&gt;=web17pw&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;192.168.122.18  &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ansible_user&lt;&#x2F;span&gt;&lt;span&gt;=web18 &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ansible_password&lt;&#x2F;span&gt;&lt;span&gt;=web18pw&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;[dbservers]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;db01.intranet.mydomain.net&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;db02.intranet.mydomain.net&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這份清單在 Ansible 的世界稱為 inventory，它的結構類似 INI 格式，應該是一望即知，後面的 &lt;code&gt;ansible_user&lt;&#x2F;code&gt;、&lt;code&gt;ansible_password&lt;&#x2F;code&gt; 自然就是那台節點的帳密啦！&lt;&#x2F;p&gt;
&lt;p&gt;而那兩台沒有特別標注帳密的資料庫節點，則會用主控節點當前的帳號和密鑰登入，這當然是不現實的，誰會把遠端主機的帳號設成和自己電腦一樣呢。&lt;&#x2F;p&gt;
&lt;p&gt;最基礎的安裝和設定完成，就可以來玩一波了！&lt;&#x2F;p&gt;
&lt;p&gt;假設上面這份 inventory 位置在 ~&#x2F;Projects&#x2F;ansible&#x2F;hosts，那可以如此調用：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; cd ~&#x2F;Projects&#x2F;ansible&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible webservers&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -i&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; hosts&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -a&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;&#x2F;bin&#x2F;echo hello&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;成功的訊息如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;192.168.122.17 | CHANGED | rc=0 &amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;hello&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;192.168.122.18 | CHANGED | rc=0 &amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;hello&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在這個簡單的範例中，最後面的 &lt;code&gt;webservers&lt;&#x2F;code&gt; 自然就是指 inventory 中 &lt;code&gt;[webservers]&lt;&#x2F;code&gt; 之中的節點。&lt;&#x2F;p&gt;
&lt;p&gt;再來一個範例：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible webservers&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -i&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; hosts&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -m&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ping&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;成功的回應如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;192.168.122.17 | SUCCESS =&amp;gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &amp;quot;ansible_facts&amp;quot;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        &amp;quot;discovered_interpreter_python&amp;quot;: &amp;quot;&#x2F;usr&#x2F;bin&#x2F;python3&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &amp;quot;changed&amp;quot;: false,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &amp;quot;ping&amp;quot;: &amp;quot;pong&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;192.168.122.18 | SUCCESS =&amp;gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &amp;quot;ansible_facts&amp;quot;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        &amp;quot;discovered_interpreter_python&amp;quot;: &amp;quot;&#x2F;usr&#x2F;bin&#x2F;python3&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &amp;quot;changed&amp;quot;: false,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &amp;quot;ping&amp;quot;: &amp;quot;pong&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面的 &lt;code&gt;ping&lt;&#x2F;code&gt; 並非我們常用的 &lt;code&gt;ping&lt;&#x2F;code&gt; 命令，而是 Ansible 的 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.ansible.com&#x2F;ansible&#x2F;2.9&#x2F;modules&#x2F;ping_module.html&quot;&gt;ping module&lt;&#x2F;a&gt;，它用來探測受管節點上的 Python 位置，並以 &lt;code&gt;pong&lt;&#x2F;code&gt; 回覆之。&lt;&#x2F;p&gt;
&lt;p&gt;以上都是直接在命令列使用的方法，比較適合臨時的遠端調用一些比較簡單的指令，這種使用模式在 Ansible 稱為 ad hoc 模式，如果是複雜的情境，那就需要編排明確的行動綱領，也就是後面會提到的 playbook。&lt;&#x2F;p&gt;
&lt;p&gt;在前面兩個例子分別用了 &lt;code&gt;-m ping&lt;&#x2F;code&gt; 和 &lt;code&gt;-a &quot;&#x2F;bin&#x2F;echo hello&quot;&lt;&#x2F;code&gt;，其中的 &lt;code&gt;-m&lt;&#x2F;code&gt; 表示 module，而 &lt;code&gt;-a&lt;&#x2F;code&gt; 表示給 module 的參數。&lt;&#x2F;p&gt;
&lt;p&gt;在第一個例子我們省略了 &lt;code&gt;-m&lt;&#x2F;code&gt;，此時 Ansible 會調用預設的 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.ansible.com&#x2F;ansible-core&#x2F;2.13&#x2F;collections&#x2F;ansible&#x2F;builtin&#x2F;command_module.html&quot;&gt;command module&lt;&#x2F;a&gt;，它會將引數 &lt;code&gt;&#x2F;bin&#x2F;echo hello&lt;&#x2F;code&gt; 在受管節點中執行。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;module&quot;&gt;Module&lt;&#x2F;h2&gt;
&lt;p&gt;下面是一些各路 module 的範例。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;yong-root-zhang-hao&quot;&gt;用 root 帳號&lt;&#x2F;h3&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible webservers&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -i&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; hosts&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -a&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;&#x2F;sbin&#x2F;reboot&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --become --ask-become-pass&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中的 &lt;code&gt;--become&lt;&#x2F;code&gt; 表示 &lt;code&gt;sudo&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;因為 command module 的命令不是在 shell 中執行，因此 shell 的變數、管道、導向都是無效的，如果有需要得改用 shell module：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible webservers&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -i&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; hosts&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -m&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible.builtin.shell&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -a&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;echo $TERM&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;she-shi-zhong&quot;&gt;設時鐘&lt;&#x2F;h3&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible webservers&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -i&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; hosts&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -m&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; community.general.timezone&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -a&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;name=Asia&#x2F;Taipei&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --become --ask-become-pass&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;設完時區最好重啟 cron 服務：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible webservers&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -i&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; hosts&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -m&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible.builtin.service&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -a&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;name=cron state=restarted&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --become --ask-become-pass&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;dang-an-chu-li&quot;&gt;檔案處理&lt;&#x2F;h3&gt;
&lt;p&gt;可以把檔案從主控節點拷貝到受管節點：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible webservers&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -i&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; hosts&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -m&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible.builtin.copy&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -a&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;src=&#x2F;etc&#x2F;hosts dest=&#x2F;tmp&#x2F;hosts&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;改檔案權限：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible webservers&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -i&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; hosts&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -m&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible.builtin.file&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -a&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;dest=&#x2F;srv&#x2F;foo&#x2F;b.txt mode=600 owner=mdehaan group=mdehaan&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;建目錄：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible webservers&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -i&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; hosts&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -m&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible.builtin.file&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -a&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;dest=&#x2F;path&#x2F;to&#x2F;c state=directory&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;刪目錄：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible webservers&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -i&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; hosts&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -m&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible.builtin.file&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -a&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;dest=&#x2F;path&#x2F;to&#x2F;c state=absent&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;不知道是否有政治正確的因素，竟然是用 &lt;code&gt;absent&lt;&#x2F;code&gt; 表示刪除，相當不直覺。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;tao-jian-guan-li&quot;&gt;套件管理&lt;&#x2F;h3&gt;
&lt;p&gt;安裝套件：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible webservers&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -i&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; hosts&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -m&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible.builtin.apt&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -a&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;name=mc&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --become --ask-become-pass&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;更新套件：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible webservers&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -i&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; hosts&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -m&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible.builtin.apt&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -a&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;name=mc state=latest&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --become --ask-become-pass&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;移除套件：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible webservers&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -i&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; hosts&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -m&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible.builtin.apt&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -a&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;name=mc state=absent&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --become --ask-become-pass&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;更新套件清單：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible webservers&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -i&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; hosts&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -m&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible.builtin.apt&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -a&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;update_cache=yes&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --become --ask-become-pass&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;升級全部套件：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible webservers&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -i&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; hosts&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -m&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible.builtin.apt&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -a&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;upgrade=safe&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --become --ask-become-pass&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;那個 &lt;code&gt;safe&lt;&#x2F;code&gt; 就相當於 &lt;code&gt;apt upgrade&lt;&#x2F;code&gt;，也可以是 &lt;code&gt;full&lt;&#x2F;code&gt;，相當於 &lt;code&gt;apt full-upgrade&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zhang-hao-he-qun-zu-guan-li&quot;&gt;帳號和群組管理&lt;&#x2F;h3&gt;
&lt;p&gt;建帳號：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible webservers&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -i&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; hosts&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -m&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible.builtin.user&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -a&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;name=foo password=&amp;lt;crypted password here&amp;gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --become --ask-become-pass&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;刪帳號：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible webservers&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -i&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; hosts&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -m&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible.builtin.user&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -a&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;name=foo state=absent&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --become --ask-become-pass&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;fu-wu-guan-li&quot;&gt;服務管理&lt;&#x2F;h3&gt;
&lt;p&gt;啟動服務：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible webservers&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -i&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; hosts&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -m&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible.builtin.service&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -a&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;name=ufw state=started&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --become --ask-become-pass&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;重啟服務：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible webservers&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -i&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; hosts&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -m&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible.builtin.service&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -a&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;name=ufw state=restarted&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --become --ask-become-pass&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;停止服務：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible webservers&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -i&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; hosts&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -m&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible.builtin.service&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -a&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;name=ufw state=stopped&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --become --ask-become-pass&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;huo-de-man-man-de-jie-dian-zi-xun&quot;&gt;獲得滿滿的節點資訊&lt;&#x2F;h3&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible webservers&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -i&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; hosts&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -m&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible.builtin.setup&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;playbook&quot;&gt;Playbook&lt;&#x2F;h2&gt;
&lt;p&gt;不可能什麼都靠一行指令打天下，複雜的、週期性的任務可以寫成 Playbook 讓 Ansible 替我們完成。&lt;&#x2F;p&gt;
&lt;p&gt;那和自己寫 shell 腳本有什麼不同？一般來說，在三台機器以內自己寫甚至手動搞都還游刃有餘，超過五台那還是用專門的工具吧，既可以加快效率又可以減少失誤。&lt;&#x2F;p&gt;
&lt;p&gt;當然現實上還有另一種交付模式「射後不理」也是頗常見，但這就不在本文的討論範圍內了。&lt;&#x2F;p&gt;
&lt;p&gt;回到 Playbook。Playbook 是 YAML 的結構，下面這是 mytask.yaml：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;---&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;-&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; My playbook&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  hosts&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; webservers&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  tasks&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    -&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; Leaving a mark&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;      ansible.builtin.command&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;        cmd&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;touch &#x2F;tmp&#x2F;ansible_was_here&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;跑起來：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; cd ~&#x2F;Projects&#x2F;ansible&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible-playbook&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -i&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; hosts mytask.yaml&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;結果：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;PLAY [My playbook] ****************************************************************&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;TASK [Gathering Facts] ************************************************************&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;ok: [192.168.122.17]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;ok: [192.168.122.18]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;TASK [Leaving a mark] *************************************************************&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;changed: [192.168.122.17]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;changed: [192.168.122.18]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;PLAY RECAP ************************************************************************&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;192.168.122.17: ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0   &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;192.168.122.18: ok=2 changed=1 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0   &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在這份 playbook 中，制定了一個名為「My playbook」的「play」，旗下有「task」，這唯一的 task 名為「Leaving a mark」。&lt;&#x2F;p&gt;
&lt;p&gt;根據以上的描述，playbook 的基本結構就是 playbook &amp;gt; play &amp;gt; task：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;一份 playbook 可以有多個 play（一份劇本有多場戲）&lt;&#x2F;li&gt;
&lt;li&gt;一個 play 可以有多個 task（一場戲有多個事務）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在上面的例子中，它的作用相當於：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible webservers&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -i&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; hosts&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -m&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible.builtin.command&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -a&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;touch &#x2F;tmp&#x2F;ansible_was_here&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;前面提過，command module 是預設行為，所以可以再簡化成：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible webservers&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -i&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; hosts&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -a&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;touch &#x2F;tmp&#x2F;ansible_was_here&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面的 playbook 範例中，我們餵了 &lt;code&gt;cmd&lt;&#x2F;code&gt; 參數給 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.ansible.com&#x2F;ansible-core&#x2F;2.13&#x2F;collections&#x2F;ansible&#x2F;builtin&#x2F;command_module.html&quot;&gt;command module&lt;&#x2F;a&gt;，每個 module 都有不同的參數，具體該怎麼餵就要參閱他們的文件了。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;task-de-zhi-xing-shun-xu&quot;&gt;Task 的執行順序&lt;&#x2F;h3&gt;
&lt;p&gt;如果有多個 task 和多個受管節點，則執行順序如下：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;在Ａ節點跑 task 1&lt;&#x2F;li&gt;
&lt;li&gt;在Ｂ節點跑 task 1&lt;&#x2F;li&gt;
&lt;li&gt;在Ａ節點跑 task 2&lt;&#x2F;li&gt;
&lt;li&gt;在Ｂ節點跑 task 2&lt;&#x2F;li&gt;
&lt;li&gt;以此類推&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;不會是在Ａ節點跑完 task 1234 再去Ｂ節點。&lt;&#x2F;p&gt;
&lt;p&gt;如果Ｂ節點的 task 1 失敗了，那 Ansible 會將Ｂ節點落入敗部，後續Ｂ節點的 task 也不會跑了，那能敗部復活重返榮耀嗎？不能。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;yong-root-zhang-hao-1&quot;&gt;用 root 帳號&lt;&#x2F;h3&gt;
&lt;p&gt;在命令列我們用 &lt;code&gt;--become&lt;&#x2F;code&gt; 讓 Ansible 在受管節點以 root 執行工作，在 playbook 也是類似：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;-&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; Ensure the UFW service is running&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  ansible.builtin.service&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;    name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ufw&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;    state&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; started&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  become&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; true&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這個 task 就相當於&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;-m&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible.builtin.service&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -a&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;name=ufw state=started&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --become&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;那 &lt;code&gt;sudo&lt;&#x2F;code&gt; 密碼怎麼辦呢？用老招 &lt;code&gt;--ask-become-pass&lt;&#x2F;code&gt; 即可，所以要跑這份 playbook 就會這樣下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; cd ~&#x2F;Projects&#x2F;ansible&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ansible-playbook&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -i&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; hosts&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --ask-become-pass&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; mytask.yaml&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;或者也可以在 inventory 檔案內附加 &lt;code&gt;sudo&lt;&#x2F;code&gt; 密碼：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;ini&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;[webservers]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;192.168.122.17  &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ansible_user&lt;&#x2F;span&gt;&lt;span&gt;=web17  &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ansible_password&lt;&#x2F;span&gt;&lt;span&gt;=web17pw  &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ansible_become_password&lt;&#x2F;span&gt;&lt;span&gt;=web17pw&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果不要 root，而是別的帳號，那可以再用 &lt;code&gt;become_user&lt;&#x2F;code&gt; 指定：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;yaml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;-&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; Ensure the Nginx service is running&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  ansible.builtin.service&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;    name&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; nginx&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;    state&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; started&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  become&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; true&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;  become_user&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; nginx_admin&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;以上是 playbook 的基礎，只要掌握這些基礎用法應該就可以滿足大部分的使用場景了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ansible-de-er-ba-fa-ze&quot;&gt;Ansible 的二八法則&lt;&#x2F;h2&gt;
&lt;p&gt;本文真的只是 Ansible 的基礎基礎基基礎，不過即使是 20% 的用法也能滿足 80% 的需求了。&lt;&#x2F;p&gt;
&lt;p&gt;Ansible 還有更多花式玩法：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;在 playbook 納入變數的概念&lt;&#x2F;li&gt;
&lt;li&gt;在 playbook 調用 Jinja2 模板語言和變數&lt;&#x2F;li&gt;
&lt;li&gt;產生動態的 inventory&lt;&#x2F;li&gt;
&lt;li&gt;讓帳號密碼加密使用&lt;&#x2F;li&gt;
&lt;li&gt;在 playbook 調用另一份 playbook&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;這些錦上添花的用法，端看需求自行求道囉，個人是覺得 Ansible 只是配置工具，影響工作效率但不影響應用效能，過度鑽研反而會降低工作效率，因為你花太多時間研究用不到的技術，除非你的工作真的必須是一位 Ansible 職人，所以剩下的部份等我進 Red Hat 再說吧 :p。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>時序資料庫 InfluxDB 在 WSL 安裝全紀錄</title>
        <published>2022-05-08T00:00:00+00:00</published>
        <updated>2022-05-08T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/influxdb-wsl/"/>
        <id>https://editor.leonh.space/2022/influxdb-wsl/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/influxdb-wsl/">&lt;p&gt;這集是介紹時序資料庫 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.influxdata.com&#x2F;&quot;&gt;InfluxDB&lt;&#x2F;a&gt; 在 WSL 安裝的過程，照慣例先介紹兩位主角。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shi-xu-zi-liao-ku-influxdb&quot;&gt;時序資料庫 InfluxDB&lt;&#x2F;h2&gt;
&lt;p&gt;什麼是「時序資料」？&lt;&#x2F;p&gt;
&lt;p&gt;時序資料的重點具體表現在名稱上，「時間」就是時間戳、「序列」表示資料是連續的，具備這兩種特性的資料就可稱為時序資料。&lt;&#x2F;p&gt;
&lt;p&gt;什麼樣的資料是時序資料呢？本集封面的心電圖就是很好的例子，但心電圖的資料量並不大，大概不需要特別的資料庫來優化它的儲存和查詢效能。&lt;&#x2F;p&gt;
&lt;p&gt;那一台收銀機的收銀紀錄是時序資料嗎？以一般的尺度來看它不是，雖然發票的確是照著時間一張張開出，但每張發票的金額卻並不連續，而是獨立的單點，小臻買了五百塊，後面大猩猩又結了六百塊，當然如果把尺度 zoom out 到百萬為單位，那的確可以視之為時序資料，但也要有這種尺度下合理的應用才是，視角或維度的不同決定了資料的性質，就像光既是粒子也是波，實際上幾乎所有的信號都能以波的形式看待，如果其中一個軸是時間，那皆可以時序資料視之。&lt;&#x2F;p&gt;
&lt;p&gt;時序資料之所以要有專門的資料庫來對付它，因為它還有另外兩個性質：肥大又需要長時間保存，因此放在傳統的資料庫水土不服，效率不彰，以一個取樣頻率為 0.1 秒的感測器來說，一天就會有 864,000 筆紀錄，連續跑幾個禮拜很快就會破千萬，所以時序資料庫也就應運而生了。&lt;&#x2F;p&gt;
&lt;p&gt;InfluxDB 是時序資料庫領域最著名的選擇之一，它有幾個特色：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;高效能，專門為時序資料而生。&lt;&#x2F;li&gt;
&lt;li&gt;有豐富的客戶端套件，Go、C#、Java、Kotlin、PHP、Ruby、Scala、Swift、JS、Node.js、Python、R 等等，後端、Web、iOS、Android 全方位覆蓋。&lt;&#x2F;li&gt;
&lt;li&gt;查詢語法近似 SQL，稱為 InfluxQL（限 InfluxDB 1.x）。&lt;&#x2F;li&gt;
&lt;li&gt;可自動把舊資料降採樣，釋放儲存空間。&lt;&#x2F;li&gt;
&lt;li&gt;生態成熟，有商業模式，也有周邊的工具練，當然我等免費仔今日用的是 InfluxDB OSS 版。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;要注意的是其中最香的 InfluxQL 只在 InfluxDB 1.x 原生提供，InfluxDB 2.0 後 InfluxQL 被另一種查詢語言 Flux 取代，InfluxQL 剩下半殘的功能被打入冷宮，這也是 InfluxDB 最惱人的點，很愛砍掉重練，目前 InfluxDB 有三大版併行：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;InfluxDB 1.x：有 InfluxQL。&lt;&#x2F;li&gt;
&lt;li&gt;InfluxDB 2.x：改用 Flux。&lt;&#x2F;li&gt;
&lt;li&gt;Influx IOx：徹底砍掉重練用 Rust 重寫，目前還在開發中，Flux 看似會沿用，目標是無痛轉換。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;因為 InfluxQL 實在太香了，而且 InfluxDB 1.x 依然在維護週期內，所以本文選用 InfluxDB 1.8。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;windows-yu-wsl&quot;&gt;Windows 與 WSL&lt;&#x2F;h2&gt;
&lt;p&gt;我們選用 WSL 2，Ubuntu 20.04。&lt;&#x2F;p&gt;
&lt;p&gt;要控制篇幅，這部份就不展開了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;an-zhuang-influxdb&quot;&gt;安裝 InfluxDB。&lt;&#x2F;h2&gt;
&lt;p&gt;起手式下載：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; wget https:&#x2F;&#x2F;dl.influxdata.com&#x2F;influxdb&#x2F;releases&#x2F;influxdb_1.8.10_amd64.deb&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;安裝：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo dpkg&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -i&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; .&#x2F;influxdb_1.8.10_amd64.deb&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;influxdb-fu-wu-pao-qi-lai&quot;&gt;InfluxDB 服務跑起來&lt;&#x2F;h2&gt;
&lt;p&gt;因為是 WSL，所以要手動啟動服務：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo influxd&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;跑起來會看到一些訊息，下面是比較重要的幾條：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;log&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;2022-05-05T01:18:23.709472Z&lt;&#x2F;span&gt;&lt;span&gt;     info    Using data dir  {&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;log_id&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;0aGSaLR0000&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;service&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;store&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;path&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;&#x2F;var&#x2F;lib&#x2F;influxdb&#x2F;data&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;2022-05-05T01:18:23.709807Z&lt;&#x2F;span&gt;&lt;span&gt;     info    Starting HTTP service   {&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;log_id&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;0aGSaLR0000&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;service&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;httpd&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;authentication&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;false&lt;&#x2F;span&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;2022-05-05T01:18:23.709835Z&lt;&#x2F;span&gt;&lt;span&gt;     info    opened HTTP access log  {&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;log_id&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;0aGSaLR0000&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;service&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;httpd&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;path&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;stderr&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;2022-05-05T01:18:23.709903Z&lt;&#x2F;span&gt;&lt;span&gt;     info    Listening on HTTP       {&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;log_id&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;0aGSaLR0000&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;service&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;httpd&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;addr&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;[::]:8086&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;https&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;false&lt;&#x2F;span&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;資料的真身在 &#x2F;var&#x2F;lib&#x2F;influxdb&#x2F;data&lt;&#x2F;li&gt;
&lt;li&gt;有在 8086 埠監聽 HTTP&lt;&#x2F;li&gt;
&lt;li&gt;HTTP log 會輸出到 stderr&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;試著在 Ubuntu 內連連看：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; influx&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;應該會進入 InfluxDB 的 shell：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Connected to http:&#x2F;&#x2F;localhost:8086 version 1.8.10&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;InfluxDB shell version: 1.8.10&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;試著在 Windows 連連看，最簡單的測試方式就是拿瀏覽器開看會不會通，測試下面兩個網址：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;http:&#x2F;&#x2F;localhost:8086&#x2F;&lt;&#x2F;li&gt;
&lt;li&gt;http:&#x2F;&#x2F;127.0.0.1:8086&#x2F;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;如果有通的話應該會出現 404 訊息，這 404 不重要，它本來就不是跑網頁的，證明有通就好。&lt;&#x2F;p&gt;
&lt;p&gt;在 WSL 2 測 http:&#x2F;&#x2F;127.0.0.1:8086&#x2F; 可能會出現拒絕連線錯誤，因為 WSL 2 本質上是一台 Hyper-V 虛擬機，它有自己的 IP，而那 127.0.0.1 則是指向 Windows 自己，如果 Windows 沒有監聽 8086 埠的服務，那就會出現拒絕連線。&lt;&#x2F;p&gt;
&lt;p&gt;總之，所以，要用 Ubuntu 自己的 IP 來測，我們用下面指令顯示出 IP：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;ip&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; addr&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;當然還有更多花式打法，在此不展開。&lt;&#x2F;p&gt;
&lt;p&gt;話說回來，為什麼在 Windows 開 http:&#x2F;&#x2F;localhost:8086&#x2F; 卻可以呢？因為 Windows 很貼心的開啟了 WSL 2 的轉送機制，所有往 localhost 的請求都會轉給 Ubuntu，反之亦然。&lt;&#x2F;p&gt;
&lt;p&gt;因為 localhost 和 127.0.0.1 的通與不通，讓本人花了一個下午，直到發現 WSL 2 原來真身是台虛擬機，僅以本篇紀念此事。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;influxdb-pei-zhi&quot;&gt;InfluxDB 配置&lt;&#x2F;h2&gt;
&lt;p&gt;配置檔在 &#x2F;etc&#x2F;influxdb&#x2F;influxdb.conf。&lt;&#x2F;p&gt;
&lt;p&gt;項目相當多，但初期只要關注 HTTP 區塊的幾個項目：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[http]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  # Determines whether HTTP endpoint is enabled.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  # enabled = true&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  # The bind address used by the HTTP service.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  # bind-address = &amp;quot;:8086&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  # Determines whether user authentication is enabled over HTTP&#x2F;HTTPS.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  # auth-enabled = false&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;應該可以望文生義吧，沒有要改的話默默離開即可。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;influxql&quot;&gt;InfluxQL&lt;&#x2F;h2&gt;
&lt;p&gt;InfluxQL 和 SQL 有八成像。&lt;&#x2F;p&gt;
&lt;p&gt;建帳號：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;CREATE USER&lt;&#x2F;span&gt;&lt;span&gt; &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;roy&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; WITH PASSWORD&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;sonofbitch&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; WITH&lt;&#x2F;span&gt;&lt;span&gt; ALL PRIVILEGES&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;建 DB：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;CREATE DATABASE&lt;&#x2F;span&gt;&lt;span&gt; &amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;isgdb&lt;&#x2F;span&gt;&lt;span&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;大概是這樣，其他 InfluxSQL 特有的時序資料查詢未來有碰到再說吧！&lt;&#x2F;p&gt;
&lt;h2 id=&quot;can-kao-zi-liao&quot;&gt;參考資料&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.m.wikipedia.org&#x2F;zh-tw&#x2F;%E5%B0%8F%E6%B3%A2%E5%88%86%E6%9E%90&quot;&gt;小波分析&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;abdus.dev&#x2F;posts&#x2F;fixing-wsl2-localhost-access-issue&#x2F;&quot;&gt;Fixing WSL2 localhost access issue&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.microsoft.com&#x2F;zh-tw&#x2F;windows&#x2F;wsl&#x2F;wsl-config&quot;&gt;WSL 中的進階設定組態&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Flask 快速指南</title>
        <published>2022-04-29T00:00:00+00:00</published>
        <updated>2022-04-29T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/flask/"/>
        <id>https://editor.leonh.space/2022/flask/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/flask/">&lt;p&gt;下文為 Flask 的〈Quickstart〉文件的摘要。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;an-zhuang&quot;&gt;安裝&lt;&#x2F;h2&gt;
&lt;p&gt;在 Linux，並且 Python 已經安裝好且也把 Pipenv 也裝好的情況下，建一個專案資料夾 flask-app，進入專案資料夾。&lt;&#x2F;p&gt;
&lt;p&gt;安裝 Flask 套件：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; pipenv install Flask&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;hello-world&quot;&gt;Hello, World!&lt;&#x2F;h2&gt;
&lt;p&gt;照慣例來個 Hello, World!。拿編輯器在專案資料夾內開個 hello.py 檔，內容如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; flask&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Flask&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;app&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; Flask(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;__name__&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@app.route&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&#x2F;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; hello_world&lt;&#x2F;span&gt;&lt;span&gt;():&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;Hello, World!&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意大小寫別打錯了！&lt;&#x2F;p&gt;
&lt;p&gt;切換到終端機，先加個環境變數讓 &lt;code&gt;flask run&lt;&#x2F;code&gt; 的時候知道要去跑這支腳本：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;flask-app&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; &amp;gt; export&lt;&#x2F;span&gt;&lt;span&gt; FLASK_APP&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;hello.py&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;flask-app&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; &amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; flask&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; run&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; *&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; Running on http:&#x2F;&#x2F;127.0.0.1:5000&#x2F;&lt;&#x2F;span&gt;&lt;span&gt; (Press&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; CTRL+C to quit&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;終端機提示符號前面多了那串 &lt;code&gt;(flask-app)&lt;&#x2F;code&gt; 表示該 shell 是位於 Python 虛擬環境內。&lt;&#x2F;p&gt;
&lt;p&gt;可以看到 Flask 有提示網址以及 &lt;kbd&gt;CTRL+C&lt;&#x2F;kbd&gt; 可以退出的字樣。瀏覽器打開 127.0.0.1:5000 應該就可以看到那親切的 Hello World。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;chu-cuo-mo-shi&quot;&gt;除錯模式&lt;&#x2F;h2&gt;
&lt;p&gt;在開發環境開啟除錯模式會觸發下列行為：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;啟動 debugger&lt;&#x2F;li&gt;
&lt;li&gt;啟動自動重載&lt;&#x2F;li&gt;
&lt;li&gt;啟動 Flask 的除錯模式&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;如果沒有自動重載，每次改完程式還要手動重載，很不方便。&lt;&#x2F;p&gt;
&lt;p&gt;一樣透過設定環境變數來開啟除錯模式，再跑一波 Flask：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;flask-app&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; &amp;gt; export&lt;&#x2F;span&gt;&lt;span&gt; FLASK_ENV&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;development&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;flask-app&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; &amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; flask&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; run&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;除錯模式很方便，但也透露了很多環境訊息，在正式環境記得不要打開。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;lu-you&quot;&gt;路由&lt;&#x2F;h2&gt;
&lt;p&gt;Flask 接收到某網址的請求後，會尋找該網址是否有對應的函式來做回應，這樣的動作或設計就叫路由。當代的 web app framework 都是採用路由的設計，而非真的有那個路徑的檔名存在，路由有可能是固定寫死的，也有可能寫成動態對應的，端看 app 的功能需求而定。&lt;&#x2F;p&gt;
&lt;p&gt;拿上面的 Hello World 改一下加上另一個路由：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@app.route&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&#x2F;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; index&lt;&#x2F;span&gt;&lt;span&gt;():&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;Index Page&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@app.route&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&#x2F;hello&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; hello&lt;&#x2F;span&gt;&lt;span&gt;():&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;Hello, World!&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;就只是很單純的靜態路由，應該很好理解。&lt;&#x2F;p&gt;
&lt;p&gt;開始來點動態路由，動態路由的範例：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@app.route&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&#x2F;user&#x2F;&amp;lt;username&amp;gt;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; show_user_profile&lt;&#x2F;span&gt;&lt;span&gt;(username):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    # show the user profile for that user&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return f&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;User &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;{&lt;&#x2F;span&gt;&lt;span&gt;username&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@app.route&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&#x2F;post&#x2F;&amp;lt;int:post_id&amp;gt;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; show_post&lt;&#x2F;span&gt;&lt;span&gt;(post_id):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    # show the post with the given id, the id is an integer&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return f&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;Post &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;{&lt;&#x2F;span&gt;&lt;span&gt;post_id&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@app.route&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&#x2F;path&#x2F;&amp;lt;path:subpath&amp;gt;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; show_subpath&lt;&#x2F;span&gt;&lt;span&gt;(subpath):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    # show the subpath after &#x2F;path&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return f&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;Subpath &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;{&lt;&#x2F;span&gt;&lt;span&gt;subpath&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;傳入 &lt;code&gt;app.route()&lt;&#x2F;code&gt; 內的字串如果用角括號包住（像這樣： &lt;code&gt;&amp;lt;variable_name&amp;gt;&lt;&#x2F;code&gt;）會被識別成變數，且不僅 &lt;code&gt;app.route()&lt;&#x2F;code&gt; 可以識別這樣的變數，後續的函式也一樣可以識別這樣的變數。&lt;&#x2F;p&gt;
&lt;p&gt;角括號內還可以轉型，參照上例，&lt;code&gt;&amp;lt;int: post_id&amp;gt;&lt;&#x2F;code&gt; 就是把客戶端請求的字串轉換成整數型態；&lt;code&gt;&amp;lt;path: subpath&amp;gt;&lt;&#x2F;code&gt; 就是把客戶端請求的字串轉換成路徑型態，完整的轉型清單如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;string&lt;&#x2F;li&gt;
&lt;li&gt;int&lt;&#x2F;li&gt;
&lt;li&gt;float&lt;&#x2F;li&gt;
&lt;li&gt;path&lt;&#x2F;li&gt;
&lt;li&gt;uuid&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;路由字串的最後，斜線或沒斜線在行為上是有所差異的：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@app.route&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&#x2F;projects&#x2F;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; projects&lt;&#x2F;span&gt;&lt;span&gt;():&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;The project page&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@app.route&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&#x2F;about&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; about&lt;&#x2F;span&gt;&lt;span&gt;():&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;The about page&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果客戶端請求 projects，Flask 會自動導引到 projects&#x2F;。反之如果客戶端請求 about，Flask 並不會自動導引到 about&#x2F;，如果依照上面的定義，客戶端請求 about&#x2F;，只會接收到 404 錯誤。如果你又很畫蛇添足的另外定義了 about&#x2F; 的路由，Flask 會報錯，並提示 URL 被重複定義的訊息。&lt;&#x2F;p&gt;
&lt;p&gt;這樣的機制確保了 URL 的唯一性，projects = projects&#x2F;，about 就是 about，沒有 about&#x2F;。&lt;&#x2F;p&gt;
&lt;p&gt;預設情況下，Flask 只會回應 HTTP GET 請求，如果希望 Flask 回應其它 HTTP 方法，&lt;code&gt;route()&lt;&#x2F;code&gt; 加上 methods 參數即可（注意引數名稱是複數的 methods，別拼錯了）：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; flask&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; request&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;app&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; Flask(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;__name__&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@app.route&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&#x2F;login&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt; methods&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;GET&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;POST&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;])&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; login&lt;&#x2F;span&gt;&lt;span&gt;():&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    if&lt;&#x2F;span&gt;&lt;span&gt; request.method&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ==&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;POST&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        return&lt;&#x2F;span&gt;&lt;span&gt; do_the_login()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    else&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        return&lt;&#x2F;span&gt;&lt;span&gt; show_the_login_form()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;jing-tai-dang-an&quot;&gt;靜態檔案&lt;&#x2F;h2&gt;
&lt;p&gt;依照 Flask 的慣例，在專案資料夾內建立一個子目錄 static，並把常用的靜態資源像是樣式表、圖像、JavaScript 這些檔案放進去，再配合 &lt;code&gt;url_for()&lt;&#x2F;code&gt; 就可以在 Python 腳本內自動產生這些資源檔的 URL：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;url_for(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;static&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt; filename&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;style.css&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面的例子會對應到 static&#x2F;style.css。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;mo-ban&quot;&gt;模板&lt;&#x2F;h2&gt;
&lt;p&gt;在此之前， 路由、controller、view 都摻在一起寫，從這裡開始要引進模板的概念，Flask 使用 Jinja2 做為模板引擎，有了模板引擎，就可以在 view 內寫 view 自己的邏輯，並且幫你把變數內的 HTML 標籤過濾掉，還有就是可以達成組版的效果。&lt;&#x2F;p&gt;
&lt;p&gt;使用 &lt;code&gt;render_template()&lt;&#x2F;code&gt; 方法來使用模板：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; flask&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; render_template&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@app.route&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&#x2F;hello&#x2F;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@app.route&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&#x2F;hello&#x2F;&amp;lt;name&amp;gt;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; hello&lt;&#x2F;span&gt;&lt;span&gt;(name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;None&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return&lt;&#x2F;span&gt;&lt;span&gt; render_template(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;hello.html&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;name)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;依照 Flask 的慣例，它會在 templates 資料夾內去找模板檔，而 templates 資料夾的位置會根據這支 web app 是模組或是套件而有所不同。&lt;&#x2F;p&gt;
&lt;p&gt;如果是模組：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&#x2F;application.py&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&#x2F;templates&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &#x2F;hello.html&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果是套件：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&#x2F;application&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &#x2F;__init__.py&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &#x2F;templates&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        &#x2F;hello.html&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;以上面的 hello.html 為例的模板內容：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;!&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;doctype&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; html&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;title&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;Hello from Flask&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;title&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{% if name %}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;h1&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;Hello {{ name }}!&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;h1&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{% else %}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;h1&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;Hello, World!&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;h1&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{% endif %}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;模板內還可以存取到 &lt;code&gt;request&lt;&#x2F;code&gt;、&lt;code&gt;session&lt;&#x2F;code&gt;、&lt;code&gt;g&lt;&#x2F;code&gt; 物件與 &lt;code&gt;get_flashed_messages()&lt;&#x2F;code&gt; 方法。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;cun-qu-request-zi-liao&quot;&gt;存取 request 資料&lt;&#x2F;h2&gt;
&lt;p&gt;客戶端傳來的資料都放在 &lt;code&gt;request&lt;&#x2F;code&gt; 這個全域變數內，web app 利用 &lt;code&gt;request&lt;&#x2F;code&gt; 提供的資訊與客戶端互動。&lt;&#x2F;p&gt;
&lt;p&gt;客戶端的 HTTP 方法存放在 &lt;code&gt;request&lt;&#x2F;code&gt; 的 &lt;code&gt;method&lt;&#x2F;code&gt; 屬性內，而 form 就存放在 &lt;code&gt;request&lt;&#x2F;code&gt; 的 &lt;code&gt;form&lt;&#x2F;code&gt; 屬性內。示例：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; flask&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; request&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; flask&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; render_template&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@app.route&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&#x2F;login&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt; methods&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;GET&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;POST&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;])&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; login&lt;&#x2F;span&gt;&lt;span&gt;():&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    error&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; None&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    if&lt;&#x2F;span&gt;&lt;span&gt; request.method&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ==&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;POST&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        if&lt;&#x2F;span&gt;&lt;span&gt; valid_login(request.form[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;username&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;], request.form[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;password&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;]):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;            return&lt;&#x2F;span&gt;&lt;span&gt; log_the_user_in(request.form[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;username&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;])&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        else&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            error&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;Invalid username &#x2F; password&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    # the code below is executed if the request method was GET or the credentials were invalid&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return&lt;&#x2F;span&gt;&lt;span&gt; render_template(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;login.html&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt; error&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;error)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果 &lt;code&gt;form&lt;&#x2F;code&gt; 屬性不存在，伺服器端會引發 &lt;code&gt;KeyError&lt;&#x2F;code&gt; 錯誤，客戶端則會收到 HTTP 400 錯誤。&lt;&#x2F;p&gt;
&lt;p&gt;如果是要取用 &lt;code&gt;URL&lt;&#x2F;code&gt; 參數，則使用 &lt;code&gt;args&lt;&#x2F;code&gt; 屬性內的 &lt;code&gt;get()&lt;&#x2F;code&gt; 方法：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;searchword&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; request.args.get(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;key&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果是要取用客戶端上傳的檔案，先確定在前端 HTML 表單的設定正確的屬性 &lt;code&gt;enctype=&quot;multipart&#x2F;form-data&quot;&lt;&#x2F;code&gt;，瀏覽器才會正確的把檔案上傳。調用 &lt;code&gt;request&lt;&#x2F;code&gt; 的 &lt;code&gt;files&lt;&#x2F;code&gt; 屬性就可以調用到檔案：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; flask&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; request&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@app.route&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&#x2F;upload&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt; methods&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;GET&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;POST&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;])&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; upload_file&lt;&#x2F;span&gt;&lt;span&gt;():&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    if&lt;&#x2F;span&gt;&lt;span&gt; request.method&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ==&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;POST&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        f&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; request.files[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;the_file&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        f.save(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&#x2F;var&#x2F;www&#x2F;uploads&#x2F;uploaded_file.txt&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;files&lt;&#x2F;code&gt; 本身是個 dictionary，所以必須呼叫它裡面的 key 才會接到真正的檔案，這個 &lt;code&gt;file&lt;&#x2F;code&gt; 物件的行為就像標準的 Python &lt;code&gt;file&lt;&#x2F;code&gt; 物件，但它有個 &lt;code&gt;save()&lt;&#x2F;code&gt; 方法讓我們把檔案存到自己想要的路徑。&lt;&#x2F;p&gt;
&lt;p&gt;如果想要沿用客戶端上傳的檔名，則調用 &lt;code&gt;filename&lt;&#x2F;code&gt; 屬性，但由於檔名的不可預知，可能會有安全風險，最好用 Werkzeug 的 &lt;code&gt;secure_filename()&lt;&#x2F;code&gt; 方法過濾掉：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; flask&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; request&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; werkzeug.utils&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; secure_filename&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@app.route&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&#x2F;upload&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt; methods&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;GET&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;POST&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;])&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; upload_file&lt;&#x2F;span&gt;&lt;span&gt;():&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    if&lt;&#x2F;span&gt;&lt;span&gt; request.method&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ==&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;POST&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        f&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; request.files[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;the_file&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        f.save(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&#x2F;var&#x2F;www&#x2F;uploads&#x2F;&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; +&lt;&#x2F;span&gt;&lt;span&gt; secure_filename(f.filename))&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果是要讀取 cookie 就調用 &lt;code&gt;request&lt;&#x2F;code&gt; 物件的 &lt;code&gt;cookies&lt;&#x2F;code&gt; 屬性，如果是要設置 cookie 就用 &lt;code&gt;response&lt;&#x2F;code&gt; 物件的 &lt;code&gt;set_cookie()&lt;&#x2F;code&gt; 方法。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;request&lt;&#x2F;code&gt; 的 &lt;code&gt;cookies&lt;&#x2F;code&gt; 也是一個 dictionary，裡面放了所有可以調用的 cookie。&lt;&#x2F;p&gt;
&lt;p&gt;但是如果想使用 session 的話，Flask 有提供更完善的 session 機制可以利用，不要手工用 cookie 來管理 session。&lt;&#x2F;p&gt;
&lt;p&gt;讀取 cookie 的範例：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; flask&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; request&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@app.route&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&#x2F;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; index&lt;&#x2F;span&gt;&lt;span&gt;():&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    username&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; request.cookies.get(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;username&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    # use cookies.get(key) instead of cookies[key] to not get a KeyError if the cookie is missing.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;存入 cookie 的範例：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@app.route&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&#x2F;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; index&lt;&#x2F;span&gt;&lt;span&gt;():&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    resp&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; make_response(render_template(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;...&lt;&#x2F;span&gt;&lt;span&gt;))&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    resp.set_cookie(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;username&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;the username&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return&lt;&#x2F;span&gt;&lt;span&gt; resp&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;zhong-dao-ye-mian-yu-cuo-wu-ye-mian&quot;&gt;重導頁面與錯誤頁面&lt;&#x2F;h2&gt;
&lt;p&gt;要重導使用 &lt;code&gt;redirect()&lt;&#x2F;code&gt; 方法，要中斷並報錯用 &lt;code&gt;abort()&lt;&#x2F;code&gt; 方法：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; flask&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; abort, redirect, url_for&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@app.route&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&#x2F;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; index&lt;&#x2F;span&gt;&lt;span&gt;():&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return&lt;&#x2F;span&gt;&lt;span&gt; redirect(url_for(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;login&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;))&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@app.route&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&#x2F;login&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; login&lt;&#x2F;span&gt;&lt;span&gt;():&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    abort(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;401&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    this_is_never_executed()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果不想使用 Flask 預設的陽春錯誤頁，則利用 &lt;code&gt;errorhandler()&lt;&#x2F;code&gt; 修飾子來做客製：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; flask&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; render_template&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@app.errorhandler&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;404&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; page_not_found&lt;&#x2F;span&gt;&lt;span&gt;(error):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return&lt;&#x2F;span&gt;&lt;span&gt; render_template(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;page_not_found.html&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;),&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 404&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意到 &lt;code&gt;return&lt;&#x2F;code&gt; 那行最後面的 &lt;code&gt;404&lt;&#x2F;code&gt;，雖然上面的修飾子已經是 404，但 &lt;code&gt;return&lt;&#x2F;code&gt; 後面還是要加 &lt;code&gt;404&lt;&#x2F;code&gt; Flask 才認得這是 404 錯誤頁。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;response&quot;&gt;Response&lt;&#x2F;h2&gt;
&lt;p&gt;View 的回傳值都會被自動轉換成 &lt;code&gt;response&lt;&#x2F;code&gt; 物件，如果原本是回傳字串，則字串內容會被包成 response body，再加上 200 的 HTTP 狀態碼，與 &lt;code&gt;text&#x2F;html&lt;&#x2F;code&gt; 的 &lt;code&gt;mimetype&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;具體的轉換邏輯如下：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;如果原本就是回傳 &lt;code&gt;response&lt;&#x2F;code&gt; 物件，則就原樣回傳，不經過轉換加工。&lt;&#x2F;li&gt;
&lt;li&gt;如果是字串，會被轉換成 &lt;code&gt;response&lt;&#x2F;code&gt; 物件，加上預設參數。&lt;&#x2F;li&gt;
&lt;li&gt;如果是 tuple，則依照特定格式轉換成 &lt;code&gt;response&lt;&#x2F;code&gt; 物件：&lt;code&gt;(response, status, headers)&lt;&#x2F;code&gt; 或 &lt;code&gt;(response, headers)&lt;&#x2F;code&gt; 裡面的 &lt;code&gt;status&lt;&#x2F;code&gt; 或 &lt;code&gt;headers&lt;&#x2F;code&gt; 值會變成 &lt;code&gt;response&lt;&#x2F;code&gt; 物件的屬性，其中 &lt;code&gt;header&lt;&#x2F;code&gt; 是一個 list 或 dictionary，裡面的欄位就會是 HTTP header 的資訊。&lt;&#x2F;li&gt;
&lt;li&gt;如果以上皆非，Flask 會假設該回傳值是 WSGI 並轉換成 &lt;code&gt;response&lt;&#x2F;code&gt; 物件。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;如果想要在 view 裡面先取得 &lt;code&gt;response&lt;&#x2F;code&gt; 物件，可以使用 &lt;code&gt;make_response()&lt;&#x2F;code&gt; 方法。&lt;&#x2F;p&gt;
&lt;p&gt;假設有個 view 如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@app.errorhandler&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;404&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; not_found&lt;&#x2F;span&gt;&lt;span&gt;(error):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return&lt;&#x2F;span&gt;&lt;span&gt; render_template(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;error.html&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;),&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 404&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;拿 &lt;code&gt;make_response()&lt;&#x2F;code&gt; 來幫它加工加上一組 header 資訊：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@app.errorhandler&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;404&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; not_found&lt;&#x2F;span&gt;&lt;span&gt;(error):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    resp&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; make_response(render_template(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;error.html&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;),&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 404&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    resp.headers[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;X-Something&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;A value&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return&lt;&#x2F;span&gt;&lt;span&gt; resp&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;session&quot;&gt;Session&lt;&#x2F;h2&gt;
&lt;p&gt;Session 用來紀錄、辨識用戶的活動，實現方式是加密過的 cookie。&lt;&#x2F;p&gt;
&lt;p&gt;得先設置密鑰才能使用 session：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; flask&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Flask, session, redirect, url_for, escape, request&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; werkzeug.utils&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; secure_filename&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;app&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; Flask(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;__name__&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# Set the secret key to some random bytes. Keep this really secret!&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;app.secret_key&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; = b&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;_5#y2L&amp;quot;F4Q8z&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;\n\xec&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;]&#x2F;&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@app.route&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&#x2F;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; index&lt;&#x2F;span&gt;&lt;span&gt;():&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    if&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;username&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; in&lt;&#x2F;span&gt;&lt;span&gt; session:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        return f&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;Logged in as &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;{&lt;&#x2F;span&gt;&lt;span&gt;escape(session[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;username&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;])&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;You are not logged in&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@app.route&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&#x2F;login&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt; methods&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;GET&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;POST&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;])&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; login&lt;&#x2F;span&gt;&lt;span&gt;():&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    if&lt;&#x2F;span&gt;&lt;span&gt; request.method&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ==&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;POST&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        session[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;username&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; request.form[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;username&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        return&lt;&#x2F;span&gt;&lt;span&gt; redirect(url_for(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;index&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;))&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;&amp;#39;&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;lt;form method=&amp;quot;post&amp;quot;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;lt;p&amp;gt;&amp;lt;input type=text name=username&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;lt;p&amp;gt;&amp;lt;input type=submit value=Login&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;lt;&#x2F;form&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;&amp;#39;&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@app.route&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&#x2F;logout&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; logout&lt;&#x2F;span&gt;&lt;span&gt;():&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    # remove the username from the session if it&amp;#39;s there&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    session.pop(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;username&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; None&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return&lt;&#x2F;span&gt;&lt;span&gt; redirect(url_for(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;index&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;))&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;前面模板的章節有說過，模板引擎會幫我們把表單的 HTML 過濾掉，而在這裡沒有使用模板引擎，所以手動調用了 &lt;code&gt;escape()&lt;&#x2F;code&gt; 方法來濾掉 HTML 碼。&lt;&#x2F;p&gt;
&lt;p&gt;最後附註一點，瀏覽器可能會限制單一 cookie 容量，如果發現某個值應該要有卻調用不出來的話，想想看是不是超過 cookie 的容量上限了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;kuai-shan-xun-xi&quot;&gt;快閃訊息&lt;&#x2F;h2&gt;
&lt;p&gt;快閃訊息用於發送通知給用戶，例如對用戶某個行動的反應。在前一個 &lt;code&gt;request&lt;&#x2F;code&gt; 使用 &lt;code&gt;flash()&lt;&#x2F;code&gt; 方法設定訊息，在下一個 &lt;code&gt;request&lt;&#x2F;code&gt; 就可以用 &lt;code&gt;get_flashed_messages()&lt;&#x2F;code&gt; 方法來讀出訊息。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;log-ji-lu&quot;&gt;Log 紀錄&lt;&#x2F;h2&gt;
&lt;p&gt;Flask app 物件有使用 Python 內建的 &lt;code&gt;logger&lt;&#x2F;code&gt; 模組，可以簡單調用：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;app.logger.debug(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;A value for debugging&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;app.logger.warning(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;A warning occurred (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;%d&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; apples)&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 42&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;app.logger.error(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;An error occurred&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Python Log 從小白到入門</title>
        <published>2022-04-27T00:00:00+00:00</published>
        <updated>2022-04-27T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/python-log/"/>
        <id>https://editor.leonh.space/2022/python-log/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/python-log/">&lt;p&gt;我們有時會有要在程式執行時輸出一些後台訊息的需求，用於顯示程式當下的行為，這樣的訊息我們稱為 log。&lt;&#x2F;p&gt;
&lt;p&gt;在開發階段我們會用 log 來協助除錯，而在正式環境也會留下一些必要的 log 以便日後追蹤，例如記下用戶登入的時間／地點／裝置等資訊，讓我們可以利用這些 log 來檢測出不尋常的登入行為。&lt;&#x2F;p&gt;
&lt;p&gt;與 &lt;code&gt;print()&lt;&#x2F;code&gt; 大法不同的是，log 除了把訊息顯示在螢幕外，還有有更豐富的設定選項，例如輸出到檔案，以及設定檔案的保留週期等等，這些都是 &lt;code&gt;print()&lt;&#x2F;code&gt; 大法難以實現的特性，因此對於記錄程式行為的需求，建議還是老老實實地用專門的 log 套件來實現。&lt;&#x2F;p&gt;
&lt;p&gt;在 Python 的世界，標準函式庫就有內建 log 模組 &lt;code&gt;logging&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;python-de-logging&quot;&gt;Python 的 logging&lt;&#x2F;h2&gt;
&lt;p&gt;因為是內建在標準函式庫內，所以可以直接引入：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;import&lt;&#x2F;span&gt;&lt;span&gt; logging&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;logging.debug(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;debug message&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;logging.info(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;info message&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;logging.warning(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;warning message&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;logging.error(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;error message&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;logging.critical(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;critical message&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面的範例是最簡單的用法，引入 &lt;code&gt;logging&lt;&#x2F;code&gt; 後可以直接調用，直接調用會把訊息顯示在螢幕上，上面我們調用了五個函式，他們代表的是訊息的重大程度，在 Python 文件內的定義如下：&lt;&#x2F;p&gt;
&lt;div class=&quot;wide&quot;&gt;
&lt;table&gt;
    &lt;thead&gt;
        &lt;tr&gt;&lt;th&gt;級別&lt;&#x2F;th&gt;&lt;th align=&quot;center&quot;&gt;級別數字&lt;&#x2F;th&gt;&lt;th&gt;何時使用&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;
    &lt;&#x2F;thead&gt;
    &lt;tbody&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;DEBUG&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td align=&quot;center&quot;&gt;10&lt;&#x2F;td&gt;&lt;td&gt;細節信息，僅當診斷問題時適用。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;INFO&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td align=&quot;center&quot;&gt;20&lt;&#x2F;td&gt;&lt;td&gt;確認程序按預期進行。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;WARNING&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td align=&quot;center&quot;&gt;30&lt;&#x2F;td&gt;&lt;td&gt;表明有已經或即將發生的意外（例如：磁盤空間不足）。程序仍按預計進行。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;ERROR&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td align=&quot;center&quot;&gt;40&lt;&#x2F;td&gt;&lt;td&gt;由於嚴重問題，程序的某些功能已經不能正常運行。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
        &lt;tr&gt;&lt;td&gt;&lt;code&gt;CRITICAL&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td align=&quot;center&quot;&gt;50&lt;&#x2F;td&gt;&lt;td&gt;嚴重的錯誤，表明程序已不能繼執行。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
    &lt;&#x2F;tbody&gt;
    &lt;caption&gt;來源：〈&lt;a href=&quot;https:&#x2F;&#x2F;docs.python.org&#x2F;zh-tw&#x2F;3&#x2F;howto&#x2F;logging.html&quot;&gt;如何使用 Logging 模組&lt;&#x2F;a&gt;〉&lt;&#x2F;caption&gt;
&lt;&#x2F;table&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;上面的範例執行後輸出如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;WARNING:root:warning message&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;ERROR:root:error message&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;CRITICAL:root:critical message&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意到 debug 和 info 訊息不見了！&lt;&#x2F;p&gt;
&lt;p&gt;因為 &lt;code&gt;logging&lt;&#x2F;code&gt; 預設只輸出 warning 及比 warning 更嚴重（即級別 ≥ 30）的訊息，這樣的預設配置較符合正式環境的需求，避免過多無謂的 info、debug 訊息輸出的 IO 作業把效能拖垮，或把空間灌爆。&lt;&#x2F;p&gt;
&lt;p&gt;在開發階段一般我們會把所有的訊息級別都打開方便我們除錯，因此我們把上面的範例修改一下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;import&lt;&#x2F;span&gt;&lt;span&gt; logging&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;dev_logger: logging.Logger&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; logging.getLogger(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;dev&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;dev_logger.setLevel(logging.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;DEBUG&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;handler: logging.StreamHandler&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; logging.StreamHandler()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;dev_logger.addHandler(handler)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;dev_logger.debug(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;debug message&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;dev_logger.info(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;info message&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;dev_logger.warning(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;warning message&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;dev_logger.error(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;error message&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;dev_logger.critical(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;critical message&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這個範例引入了 &lt;code&gt;logging&lt;&#x2F;code&gt; 的其它概念，下面一一說明。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;logger&quot;&gt;Logger&lt;&#x2F;h3&gt;
&lt;p&gt;在第一個範例未做任何設定的 &lt;code&gt;logging&lt;&#x2F;code&gt; 我們操作的是一個稱為 &lt;code&gt;root&lt;&#x2F;code&gt; 的 logger，而現在這個範例我們用 &lt;code&gt;getLogger()&lt;&#x2F;code&gt; 函式建立了另一個 &lt;code&gt;dev_logger&lt;&#x2F;code&gt;，後面的設定也都是基於這個新的 &lt;code&gt;dev_logger&lt;&#x2F;code&gt;，我們可以利用 logger 的機制建立無數個 logger，每個 logger 都可以有自己的組態，例如儲存到不同的檔案，或者不同的輸出的格式等等，雖然 &lt;code&gt;root&lt;&#x2F;code&gt; logger 也是可以被做設定的，但個人建議另外建立自己的 logger 再把它變成自己喜歡的形狀，而不要去更動 &lt;code&gt;root&lt;&#x2F;code&gt; logger 的行為，避免無意中改掉專案內依賴套件的 logger 行為。&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;python-log&#x2F;hqdefault.jpg&quot; alt=&quot;我能感覺出來 你裡面變了&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：網路&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;而那句 &lt;code&gt;setLevel(logging.DEBUG)&lt;&#x2F;code&gt; 自然就是設定 logger 輸出級別的函式，裡面的 &lt;code&gt;logging.DEBUG&lt;&#x2F;code&gt; 也很單純，就是整數 &lt;code&gt;10&lt;&#x2F;code&gt;，意即輸出 debug 及比 debug 更嚴重（即級別 ≥ 10）的的訊息。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;handler&quot;&gt;Handler&lt;&#x2F;h3&gt;
&lt;p&gt;在 &lt;code&gt;logging&lt;&#x2F;code&gt; 模組的 handler 負責處理 log 訊息的輸出工作，例如輸出到螢幕上或者某個檔案內，而我們這邊使用了 &lt;code&gt;logging.StreamHandler()&lt;&#x2F;code&gt; 這個 &lt;code&gt;logging&lt;&#x2F;code&gt; 模組內預帶的 handler。&lt;code&gt;StreamHandler()&lt;&#x2F;code&gt; 在不加任何參數的情況下，會把訊息輸出到 stderr，在作業系統未做額外的配置下，stderr 可以簡單的理解為顯示到螢幕上。&lt;&#x2F;p&gt;
&lt;p&gt;而 &lt;code&gt;addHandler()&lt;&#x2F;code&gt; 就望文生義了。&lt;&#x2F;p&gt;
&lt;p&gt;值得一提的是，一個 logger 可以加上數個 handler，意即 &lt;code&gt;dev_logger&lt;&#x2F;code&gt; 除了加入一個 &lt;code&gt;StreamHandler&lt;&#x2F;code&gt; 外，還可以再定義其他的 handler，例如一個 &lt;code&gt;FileHandler&lt;&#x2F;code&gt;，如此一則 log 訊息就會被顯示在螢幕上以及被存到某個 log 檔內。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;把上面的範例程式跑出來會輸出這些訊息：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;debug message&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;info message&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;warning message&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;error message&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;critical message&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;回頭和第一個範例的結果比較一下，會發現訊息的格式不太一樣，因為我們並未對 &lt;code&gt;dev_logger&lt;&#x2F;code&gt; 設定它的訊息格式，而原始的 &lt;code&gt;root&lt;&#x2F;code&gt; logger 是有預設它的訊息格式的。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;formatter&quot;&gt;Formatter&lt;&#x2F;h3&gt;
&lt;p&gt;在此我們再引入另一個 &lt;code&gt;logging&lt;&#x2F;code&gt; 設定 log 訊息格式的特性 formatter。&lt;&#x2F;p&gt;
&lt;p&gt;再把範例加工一下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;import&lt;&#x2F;span&gt;&lt;span&gt; logging&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;dev_logger: logging.Logger&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; logging.getLogger(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;dev&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;dev_logger.setLevel(logging.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;DEBUG&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;handler: logging.StreamHandler&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; logging.StreamHandler()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;formatter: logging.Formatter&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; logging.Formatter(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;%(asctime)s&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; - &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;%(name)s&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; - &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;%(levelname)s&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; - &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;%(message)s&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;handler.setFormatter(formatter)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;dev_logger.addHandler(handler)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;dev_logger.debug(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;debug message&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;dev_logger.info(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;info message&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;dev_logger.warning(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;warning message&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;dev_logger.error(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;error message&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;dev_logger.critical(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;critical message&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;先看輸出結果：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2005-03-19 15:10:26,618 - dev - DEBUG - debug message&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2005-03-19 15:10:26,620 - dev - INFO - info message&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2005-03-19 15:10:26,695 - dev - WARNING - warn message&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2005-03-19 15:10:26,697 - dev - ERROR - error message&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2005-03-19 15:10:26,773 - dev - CRITICAL - critical message&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這裡的格式是由程式內的 &lt;code&gt;formatter&lt;&#x2F;code&gt; 定義的，它是一個 &lt;code&gt;logging.Formatter&lt;&#x2F;code&gt; 物件，在實例化時餵入的字串定義了這個 formatter 的格式。除了範例內的代號，完整的格式代號如下：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;格式&lt;&#x2F;th&gt;&lt;th&gt;描述&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;%(asctime)s&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;建立 log 時間&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;%(created)f&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;建立 log 的 Unix 時間&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;%(filename)s&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;發出 log 的程式檔名&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;%(funcName)s&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;發出 log 的函式名&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;%(levelname)s&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;log 的級別&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;%(levelno)s&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;log 的級別數字&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;%(lineno)d&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;發出 log 的行號&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;%(message)s&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;log 的訊息內容&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;%(module)s&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;發出 log 的模組名稱&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;%(msecs)d&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;建立 log 時間的毫秒部份&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;%(name)s&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;發出 log 的 logger 名稱&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;%(pathname)s&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;發出 log 的程式的路徑&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;%(process)d&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;發出 log 的程序的 ID&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;%(processName)s&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;發出 log 的程序名稱&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;%(relativeCreated)d&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;log 從創建到發出的毫秒時間差&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;%(thread)d&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;發出 log 的線程 ID&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;%(threadName)s&lt;&#x2F;code&gt;&lt;&#x2F;td&gt;&lt;td&gt;發出 log 的線程名稱&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;格式代號這麼多，而在設定 log 格式時也並沒有所謂的最佳實踐格式，該怎麼設可以參考下面的原則：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;足夠的資訊量，但不要一股腦的全塞進去，追求的是穠纖合度的平衡，爆量的 log 只會把機台的 IO 和空間塞爆。&lt;&#x2F;li&gt;
&lt;li&gt;人機皆可讀，log 除了給人看之外，也可以餵給外部的 log 分析服務，做到提醒或警報的功能。&lt;&#x2F;li&gt;
&lt;li&gt;不易混淆的分隔符號，讓不論是人讀或機讀都可以快速的解析出訊息的區段，而不會在茫茫 log 海中迷失。&lt;&#x2F;li&gt;
&lt;li&gt;適度的分類與分級，利用前面提到的 logger 與級別的機制，對 log 做分類與分級，debug 用的訊息不要流到別的層級；critical 的訊息不要放到 warning 而被忽略。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;綜合以上，範例內的 &lt;code&gt;&#x27;%(asctime)s - %(name)s - %(levelname)s - %(message)s&#x27;&lt;&#x2F;code&gt; 就是個頗不錯的格式。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zhuan-an-nei-de-log-mo-zu&quot;&gt;專案內的 Log 模組&lt;&#x2F;h2&gt;
&lt;p&gt;上一個部份談的是 &lt;code&gt;logging&lt;&#x2F;code&gt; 本身的用法，若要在專案內導入 log 機制，還需要考慮這幾點：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;專案內的其它套件是否也有定義自己的 log？&lt;&#x2F;li&gt;
&lt;li&gt;要如何讓自己的 log 與其他套件的 log 和平共存而不互相干擾？&lt;&#x2F;li&gt;
&lt;li&gt;或者是如何依自己的需求魔改其他套件的 log 行為？&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;以 uvicorn 這個 ASGI server 來說，它就有自己的 log 機制，每當它跑起來就會開始吐訊息：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;INFO:     Started server process [20870]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;INFO:     Waiting for application startup.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;INFO:     Application startup complete.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;INFO:     Uvicorn running on http:&#x2F;&#x2F;127.0.0.1:8000 (Press CTRL+C to quit)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;不只 uvicorn，大多數的 HTTP server 也都會吐出類似的 log，內容有啟動資訊以及訪客的請求資訊等等。&lt;&#x2F;p&gt;
&lt;p&gt;如果你不想和它的 log 互相干擾，那比較好的方式是建立自己的 logger，並設定自己的 handler 和 formatter，就如同前一部分的範例那樣。&lt;&#x2F;p&gt;
&lt;p&gt;另外由於你的 logger 有可能會在多個其他的 Python 模組內被引入，建議也把 logger 寫成獨立的模組便於使用。&lt;&#x2F;p&gt;
&lt;p&gt;如果你想魔改其它套件的 log 納為己用，那我在此為您加油！祝您好運！&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;python-log&#x2F;6iQfPmt.jpg&quot; alt=&quot;加油，好嗎&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：《超級星光大道》&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;jie-yu&quot;&gt;結語&lt;&#x2F;h2&gt;
&lt;p&gt;這篇介紹了 &lt;code&gt;logging&lt;&#x2F;code&gt; 模組的基礎用法，有些特性是本文並未涉及的，特別是組態檔的部份，&lt;code&gt;logging&lt;&#x2F;code&gt; 可以從外部的組態檔讀入配置，這樣的特性讓我們可以更好的把專案內的 log 設定與程式邏輯分離，並把所有的 logger 組織在一份獨立的檔案內，也更符合&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E5%85%B3%E6%B3%A8%E7%82%B9%E5%88%86%E7%A6%BB&quot;&gt;關注點分離&lt;&#x2F;a&gt;的原則，或許未來可以再寫一篇來騙稿費。:p&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>建立 Android 開發環境</title>
        <published>2022-03-06T00:00:00+00:00</published>
        <updated>2022-03-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/android/"/>
        <id>https://editor.leonh.space/2022/android/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/android/">&lt;p&gt;在建立 Android 開發環境時，有幾個大玩具要處理好：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;開啟 CPU 虛擬化支援&lt;&#x2F;li&gt;
&lt;li&gt;安裝 Android Studio 與 Android SDK&lt;&#x2F;li&gt;
&lt;li&gt;配置 Android 模擬器&lt;&#x2F;li&gt;
&lt;li&gt;安裝 Java&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;cpu-xu-ni-hua-zhi-yuan&quot;&gt;CPU 虛擬化支援&lt;&#x2F;h2&gt;
&lt;p&gt;一定要開啟虛擬化支援，後面的 Android 模擬器才跑得起來。&lt;&#x2F;p&gt;
&lt;p&gt;CPU 的虛擬化技術，Intel 的叫「Intel Vertualization Technoogy」、AMD 的叫「SVM」（&lt;strong&gt;S&lt;&#x2F;strong&gt;ecure &lt;strong&gt;V&lt;&#x2F;strong&gt;irtual &lt;strong&gt;M&lt;&#x2F;strong&gt;achine)，而具體在 UEFI&#x2F;BIOS 內的開關位置，則視主機板廠商而異，因為現在 UEFI&#x2F;BIOS 做得越來越花俏也越來越難找了，找不到的可以翻翻手冊或搜尋一下。&lt;&#x2F;p&gt;
&lt;p&gt;搞定 UEFI&#x2F;BIOS 之後 Windows + AMD 的用戶還要去陳年的控制台開啟「Windows Hypervisor 平台」功能：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;android&#x2F;windows-hypervisor.png&quot; alt=&quot;Windows Hypervisor 平台&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;這樣模擬器跑起來才會順。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;android-studo-android-sdk&quot;&gt;Android Studo &amp;amp; Android SDK&lt;&#x2F;h2&gt;
&lt;p&gt;如果是 Linux 的用戶，Google 提供的 Android Studio 安裝包顯然不夠親切，建議改用 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;flathub.org&#x2F;apps&#x2F;details&#x2F;com.google.AndroidStudio&quot;&gt;Flatpak 打包的 Android Studio&lt;&#x2F;a&gt; 或 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;snapcraft.io&#x2F;android-studio&quot;&gt;Snap 打包的 Android Studio&lt;&#x2F;a&gt;，他們都是一鍵安裝，也有更新機制，可以愉快使用。&lt;&#x2F;p&gt;
&lt;p&gt;在裝完之後，最好手動配置相關的環境變數：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;export&lt;&#x2F;span&gt;&lt;span&gt; ANDROID_HOME&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;$HOME&#x2F;Android&#x2F;Sdk&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;export&lt;&#x2F;span&gt;&lt;span&gt; PATH&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;$PATH:$ANDROID_HOME&#x2F;platform-tools&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果是 Debian 系的 Linux，把上面兩行加入 ~&#x2F;.bashrc 的最末端即可，如果是其他家系的 Linux 請自行選擇適當的配置檔添加囉！&lt;&#x2F;p&gt;
&lt;p&gt;如果是 Windows，一樣要配置上面兩個環境變數，可以裝個 &lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;rapid-environment-editor&#x2F;&quot;&gt;Rapid Evnironment Editor&lt;&#x2F;a&gt;，讓我們可以更方便的修改環境變數。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;android-mo-ni-qi&quot;&gt;Android 模擬器&lt;&#x2F;h2&gt;
&lt;p&gt;在 Android Studio 裡面可以用 Device Manger 管理模擬器：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;android&#x2F;device_manager.png&quot; alt=&quot;Android Studio Device Manager&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;而建立模擬機時，目標 API 版本當然是依專案需求而定，但建議不要選太新的版本，太新意味著不夠穩定，比較容易 crash 或發生其他各種靈異現象。&lt;&#x2F;p&gt;
&lt;p&gt;以下圖為例，Tiramlsu 與 API 32 就是太新的版本，不僅不穩定，而且用戶數極少，建議等一年後再用。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;android&#x2F;system_image.png&quot; alt=&quot;Android Studio&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;要把模擬器跑起來，除了 Android Studio 外，也可以用 CLI 工具做到。&lt;&#x2F;p&gt;
&lt;p&gt;模擬器 CLI 真身是 ~&#x2F;Android&#x2F;Sdk&#x2F;emulator&#x2F;emulator 這支程式，它有一大堆選項，這裡只寫最常用的兩個：&lt;&#x2F;p&gt;
&lt;h3 id=&quot;lie-chu-mo-ni-qi&quot;&gt;列出模擬器&lt;&#x2F;h3&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;.&#x2F;emulator&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -list-avds&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;會輸出現有的模擬器名稱：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Pixel_5_API_31&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Pixel_5_API_32&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;zhi-xing-mo-ni-qi&quot;&gt;執行模擬器&lt;&#x2F;h3&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;.&#x2F;emulator&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -avd&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; Pixel_5_API_31&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;模擬器就會跑起來了，並且在終端機會有相關的 log 可看，有時候 debug 會需要。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;java&quot;&gt;Java&lt;&#x2F;h2&gt;
&lt;p&gt;雖說 Android SDK 裡面已經自帶 Java 了，但在某些外部框架，例如 NativeScript、React Native、Hippy、Weex，還是需要自建 Java 環境，也就是裝 OpenJDK。&lt;&#x2F;p&gt;
&lt;p&gt;首先要考慮的問題是 Java 的版本，Java 也有主次要版本之別，如下圖：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;android&#x2F;java-support-roadmap.png&quot; alt=&quot;Java Support Roadmap&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：&lt;a href=&quot;https:&#x2F;&#x2F;www.oracle.com&#x2F;java&#x2F;technologies&#x2F;java-se-support-roadmap.html&quot;&gt;〈Oracle Java SE Support Roadmap〉&lt;&#x2F;a&gt;&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;主流的 LTS 版本有 7、8、11、17、21：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Java 7 與 8 過於陳年，只是為了老舊系統的續命而存在，可以跳過。&lt;&#x2F;li&gt;
&lt;li&gt;Java 11、17則要視專案而定，特別是那建置工具 Gradle 與 Java 的相依性，如果不匹配的話，一樣有可能遇到各種靈異問題。&lt;&#x2F;li&gt;
&lt;li&gt;Java 21 太新，相關的工具鏈的支援性應該還不完整。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;android&#x2F;Gradle-Java-Compatibility.webp&quot; alt=&quot;Gradle-Java Compatibility&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：&lt;a href=&quot;https:&#x2F;&#x2F;docs.gradle.org&#x2F;current&#x2F;userguide&#x2F;compatibility.html&quot;&gt;《Gradle User Manual》&lt;&#x2F;a&gt;&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;參見上表，如果專案的 Gradle 是 7.x 的，就用 OpenJDK 17；如果專案的 Gradle 是 5.x、6.x 的，就用 OpenJDK 11；如果是 Gradle 4.x 的，那請把專案升級，如果沒有足夠的誘因升級，那它有可能是&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;doublesllash.com&#x2F;blog&#x2F;posts&#x2F;2021&#x2F;asset-liability&quot;&gt;負債&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;Java 版本確定之後，下一個是門當戶對的問題，因為種種的愛恨糾葛，現在的 OpenJDK 生態不再是甲骨文一家獨大，而是真的很 open 的 open，變得有點像 Linux 一樣百花齊放，各大門派都有自己的 OpenJDK：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;android&#x2F;openjdk.png&quot; alt=&quot;OpenJDK&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：&lt;a href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;OpenJDK&quot;&gt;維基百科〈OpenJDK〉&lt;&#x2F;a&gt;&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;甚至有個網站專門幫助選擇障礙的我們挑個門當戶對的 JDK：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;android&#x2F;logo.png&quot; alt=&quot;Which Version of JDK Should I Use?&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：&lt;a href=&quot;http:&#x2F;&#x2F;whichjdk.com&#x2F;&quot;&gt;Which Version of JDK Should I Use?&lt;&#x2F;a&gt;&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;如果不想花心思挑門當戶對的話，直接裝系統提供的 openJDK 套件就好。&lt;&#x2F;p&gt;
&lt;p&gt;綜合以上，要安裝系統的 OpenJDK 11 包的話，可以一行指令解決：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;sudo&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; apt install openjdk-11-jdk&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;當然隨著專案的增加，是有可能需要切換 JDK 版本的，這時就可以用 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;doublesllash.com&#x2F;blog&#x2F;posts&#x2F;2011&#x2F;linux-Alternatives&quot;&gt;Linux 的 alternative 機制&lt;&#x2F;a&gt;做切換。&lt;&#x2F;p&gt;
&lt;p&gt;如果是 Windows，非得挑一個不可的話，看起來最佳的選擇是 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;adoptium.net&#x2F;&quot;&gt;Adoptium&lt;&#x2F;a&gt; 的 Eclipse Temurin，它來自 Java 最大的門派 Eclipse Foundation，少了些被單一公司把持命根子的風險。&lt;&#x2F;p&gt;
&lt;p&gt;裝好 Java SDK 後，應該會有一個環境變數 JAVA_HOME 指向 Java SDK 安裝路徑，以 Windows 為例會像這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;JAVA_HOME=C:\Program Files\Eclipse Adoptium\jdk-11.0.17.8-hotspot&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果沒有的話就手動加一下吧。這裡一樣可以用 &lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;rapid-environment-editor&#x2F;&quot;&gt;Rapid Evnironment Editor&lt;&#x2F;a&gt; 來方便的修改環境變數。&lt;&#x2F;p&gt;
&lt;p&gt;以上大功告成。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>日文的音調標示與 HTML 的 Ruby</title>
        <published>2022-03-04T00:00:00+00:00</published>
        <updated>2022-03-04T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/ruby/"/>
        <id>https://editor.leonh.space/2022/ruby/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/ruby/">&lt;p&gt;日文的音調，有一種用線段表達的形式，就像下面這樣：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;ruby&#x2F;maxresdefault.jpg&quot; alt=&quot;日文的高低音&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：&lt;a href=&quot;&quot;&gt;〈日文的高低音〉&lt;&#x2F;a&gt;&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;至於具體該怎麼發音，可以看下面這部片，講的很清楚明白：&lt;&#x2F;p&gt;
&lt;iframe src=&quot;https:&#x2F;&#x2F;www.youtube-nocookie.com&#x2F;embed&#x2F;ir7fs8fukHM&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;那麼這樣的音調標示要怎麼不靠手工製圖做出來呢？我們可以用 &lt;code&gt;&amp;lt;ruby&amp;gt;&lt;&#x2F;code&gt; 搞定它。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ruby&quot;&gt;Ruby&lt;&#x2F;h2&gt;
&lt;p&gt;Ruby 是 HTML 設計用來標示發音的標籤，一般是用來標示注音、日文漢字的發音，當然也可以拿來標示音調啦。&lt;&#x2F;p&gt;
&lt;p&gt;用法很簡單 &lt;code&gt;&amp;lt;ruby&amp;gt;&lt;&#x2F;code&gt; 包住要標注的文字塊，裡面的 &lt;code&gt;&amp;lt;rt&amp;gt;&lt;&#x2F;code&gt; 再填入標注即可，像下面的例子：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;ruby&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;span&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;え&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;span&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;rt&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;━━┓&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;rt&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;ruby&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;span&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;き&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;span&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;p style=&quot;font-size: xx-large; font-size: xxx-large; text-align: center; font-family: serif; background-color: hsla(0, 0%, 96%, 1.0); padding: 1rem;&quot;&gt;
    &lt;ruby&gt;&lt;span&gt;え&lt;&#x2F;span&gt;&lt;rt&gt;━━┓&lt;&#x2F;rt&gt;&lt;&#x2F;ruby&gt;
    &lt;span&gt;き&lt;&#x2F;span&gt;
&lt;&#x2F;p&gt;
&lt;p&gt;可是在瀏覽器預設的排版下，線段和文字的距離太遠了，而且用各種正規的 CSS 語法都改不了，於是救星 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;stackoverflow.com&#x2F;q&#x2F;38680695&#x2F;3619192&quot;&gt;StackOverflow 上就有人提出把 &lt;code&gt;display&lt;&#x2F;code&gt; 設成 &lt;code&gt;inline-flex&lt;&#x2F;code&gt; 的作法&lt;&#x2F;a&gt;，果然有效：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;style&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;ruby&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    display&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; inline-flex&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    fliex-direction&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; column-reverse&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;ruby&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; &amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt; span&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    display&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; inline&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    line-height&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;rt&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    display&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; inline&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    line-height&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;style&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;body&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;ruby&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;span&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;え&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;span&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;rt&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;━━┓&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;rt&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;ruby&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;span&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;き&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;span&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;body&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;p style=&quot;font-size: xx-large; font-size: xxx-large; text-align: center; font-family: serif; background-color: hsla(0, 0%, 96%, 1.0); padding: 1rem;&quot;&gt;
    &lt;ruby style=&quot;display: inline-flex; flex-direction: column-reverse;&quot;&gt;
        &lt;span style=&quot;display: inline; line-height: 1;&quot;&gt;え&lt;&#x2F;span&gt;&lt;rt style=&quot;display: inline; line-height: 1;&quot;&gt;━━┓&lt;&#x2F;rt&gt;
    &lt;&#x2F;ruby&gt;
    &lt;span style=&quot;display: inline; line-height: 1;&quot;&gt;き&lt;&#x2F;span&gt;
&lt;&#x2F;p&gt;
&lt;p style=&quot;font-size: xx-large; font-size: xxx-large; text-align: center; font-family: serif; background-color: hsla(0, 0%, 96%, 1.0); padding: 1rem;&quot;&gt;
    &lt;span style=&quot;display: inline; line-height: 1;&quot;&gt;あ&lt;&#x2F;span&gt;
    &lt;ruby style=&quot;display: inline-flex; flex-direction: column-reverse;&quot;&gt;
        &lt;span style=&quot;display: inline; line-height: 1;&quot;&gt;か&lt;&#x2F;span&gt;&lt;rt style=&quot;display: inline; line-height: 1;&quot;&gt;━━┓&lt;&#x2F;rt&gt;
    &lt;&#x2F;ruby&gt;
    &lt;span style=&quot;display: inline; line-height: 1;&quot;&gt;し&lt;&#x2F;span&gt;
&lt;&#x2F;p&gt;
&lt;p style=&quot;font-size: xx-large; font-size: xxx-large; text-align: center; font-family: serif; background-color: hsla(0, 0%, 96%, 1.0); padding: 1rem;&quot;&gt;
    &lt;span style=&quot;display: inline; line-height: 1;&quot;&gt;ぎ&lt;&#x2F;span&gt;
    &lt;ruby style=&quot;display: inline-flex; flex-direction: column-reverse;&quot;&gt;
        &lt;span style=&quot;display: inline; line-height: 1;&quot;&gt;ん&lt;&#x2F;span&gt;&lt;rt style=&quot;display: inline; line-height: 1;&quot;&gt;━━&lt;&#x2F;rt&gt;
    &lt;&#x2F;ruby&gt;
    &lt;ruby style=&quot;display: inline-flex; flex-direction: column-reverse;&quot;&gt;
        &lt;span style=&quot;display: inline; line-height: 1;&quot;&gt;こ&lt;&#x2F;span&gt;&lt;rt style=&quot;display: inline; line-height: 1;&quot;&gt;━━┓&lt;&#x2F;rt&gt;
    &lt;&#x2F;ruby&gt;
    &lt;span style=&quot;display: inline; line-height: 1;&quot;&gt;う&lt;&#x2F;span&gt;
    &lt;span style=&quot;display: inline; line-height: 1;&quot;&gt;い&lt;&#x2F;span&gt;
    &lt;span style=&quot;display: inline; line-height: 1;&quot;&gt;ん&lt;&#x2F;span&gt;
&lt;&#x2F;p&gt;
&lt;p&gt;字距還是沒那麼完美，不過看來日文的學習者應該很適應這種狀況，畢竟對學習者來說，能學習到正確的發音才是重點，例如下面是某個 app 的發音標注：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;ruby&#x2F;nihongo.webp&quot; alt=&quot;日文&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;不可能把「watashi」這麼長的英文硬擠成「私」的寬度，不僅更不美觀，而且也完全失去了學習發音的主要目的。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ling-yi-zhong-si-lu&quot;&gt;另一種思路&lt;&#x2F;h2&gt;
&lt;p&gt;用 HTML Ruby 來處理只能說堪用，不同的排版引擎可能呈現出不同的結果，想要保證一致的外觀，可以改用程式處理，例如做一支能畫文字和線段的 API，像這樣：&lt;&#x2F;p&gt;
&lt;!-- ![ぎんこういん](https:&#x2F;&#x2F;katsuyo-backend.herokuapp.com&#x2F;ぎんこういん&#x2F;012000.png) --&gt;
&lt;!-- ![ぎんこういん](https:&#x2F;&#x2F;katsuyo-leonh.koyeb.app&#x2F;ぎんこういん&#x2F;012000.png) --&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;ruby&#x2F;012000.png&quot; alt=&quot;ぎんこういん&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;具體這支 API 是如何實現的，日後有機會再分享吧。&lt;&#x2F;p&gt;
&lt;p&gt;文章寫到這邊，應該都會唸日文的高低音了吧，祝大家學習愉快。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Rapid Environment Editor 設定 Windows 環境變數的工具</title>
        <published>2022-02-27T00:00:00+00:00</published>
        <updated>2022-02-27T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/rapid-environment-editor/"/>
        <id>https://editor.leonh.space/2022/rapid-environment-editor/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/rapid-environment-editor/">&lt;p&gt;本篇介紹一個幫助我們設定 Windows 環境變數的小工具 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.rapidee.com&#x2F;&quot;&gt;Rapid Environment Editor&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;huan-jing-bian-shu&quot;&gt;環境變數&lt;&#x2F;h2&gt;
&lt;p&gt;環境變數指的是儲存於 OS 層的變數，變數設定的目的可能是作業系統自己工作所需而儲存的，或是某些 app 也會去讀取環境變數來做出某些相對應的行為，更多的解釋可以參考&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F&quot;&gt;維基百科&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;path&quot;&gt;Path&lt;&#x2F;h3&gt;
&lt;p&gt;以最常見的 &lt;code&gt;Path&lt;&#x2F;code&gt; 這組環境變數為例，shell 層就會去讀取 &lt;code&gt;Path&lt;&#x2F;code&gt; 的變數內容，&lt;code&gt;Path&lt;&#x2F;code&gt; 這組變數裡面放的都是一組一組的路徑。Shell 在透過 &lt;kbd&gt;Windows&lt;&#x2F;kbd&gt; + &lt;&#x2F;kbd&gt;R&lt;&#x2F;kbd&gt; 或 cmd.exe 執行程式時，如果當時工作的路徑找不到那支要執行的程式，就會去 &lt;code&gt;Path&lt;&#x2F;code&gt; 內每一組路徑尋找，找到就怒跑起來，找不到就報錯。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;huan-jing-bian-shu-de-ge-shi&quot;&gt;環境變數的格式&lt;&#x2F;h2&gt;
&lt;p&gt;在 Windows 下環境變數長這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Path=C:\Windows\system32;C:\Windows&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;環境變數的內容，這裡的例子是路徑，注意每組路徑之間以「&lt;code&gt;;&lt;&#x2F;code&gt;」做為分隔符號。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;bian-ji-huan-jing-bian-shu&quot;&gt;編輯環境變數&lt;&#x2F;h2&gt;
&lt;p&gt;環境變數儲存於 Windows 登錄檔裡，通常會透過特定的程式來做編輯，除了 Rapid Environment Editor 之外，Windows 自己也帶有環境變數的編輯工具，不過並不好用，這裡不多做介紹。&lt;&#x2F;p&gt;
&lt;p&gt;去 Rapid Environment Editor 網站下載安裝執行後，應該會看到像這樣的視窗：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;rapid-environment-editor&#x2F;xrapidee.png.pagespeed.ic_.xnesx_nhk4.png&quot; alt=&quot;Rapid Environment Editor&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;主要分成左右兩欄，左邊為系統變數，沒事不要動它；右邊是用戶變數，如果想要改 &lt;code&gt;Path&lt;&#x2F;code&gt; 的話，找到 &lt;code&gt;Path&lt;&#x2F;code&gt;，把 &lt;code&gt;Path&lt;&#x2F;code&gt; 展開，就可以對 &lt;code&gt;Path&lt;&#x2F;code&gt; 的內容作增刪改了，最後改完要記得存檔，並且有些環境變數必須重新開機後才會重載，要留意一下。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>便利背後的隱藏成本</title>
        <published>2022-02-20T00:00:00+00:00</published>
        <updated>2022-02-20T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/commission/"/>
        <id>https://editor.leonh.space/2022/commission/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/commission/">&lt;p&gt;根據&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.fsc.gov.tw&#x2F;ch&#x2F;home.jsp?id=96&amp;amp;mcustomize=news_view.jsp&amp;amp;dataserno=202202100002&amp;amp;dtable=News&quot;&gt;金管會 2011 年 12 月份的統計資料&lt;&#x2F;a&gt;，電子支付帳戶，也就是我們常說的 XX Pay，使用人數已經達到一千五百萬人，相較於前一年 2010 年 12 月份，成長了四百萬，若以比例看，增幅高達 34%：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;commission&#x2F;statistics.webp&quot; alt=&quot;2011年12月份信用卡、電子支付統計&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：&lt;a href=&quot;https:&#x2F;&#x2F;www.fsc.gov.tw&#x2F;ch&#x2F;home.jsp?id=96&amp;mcustomize=news_view.jsp&amp;dataserno=202202100002&amp;dtable=News&quot;&gt;金融監督管理委員會&lt;&#x2F;a&gt;&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;!-- 在行之有年的信用卡方面，有效卡數三千三百萬張，而已經進入成熟期的信用卡成長率則只有 5%。 --&gt;
&lt;!-- 在流通金額上，電子支付則只有百億元的規模，完全看不見信用卡高達兩千九百億元的車尾燈，畢竟在高額消費的場景，如買車、旅遊、住宿等，信用卡還是絕對的大宗，電子支付的使用場景大部分還是發生在一般民生消費上。 --&gt;
&lt;p&gt;在銳不可當的增長趨勢下，為消費者帶來便利之外，又帶來哪些潛在的改變呢？&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fen-yi-bei-geng&quot;&gt;分一杯羹&lt;&#x2F;h2&gt;
&lt;!-- 相較於已經進入成熟期而成長停滯的信用卡，同期的有效卡數是三千四百萬張，電子支付如果能持續維持如此高的成長率，在 2024 年也將面臨用戶數飽和的臨界點三千六百萬，相當於平均每個人有兩個 XX Pay 帳號（扣除國中生以下人口），這三千六百萬的數字背後，代表的是鉅額的中間人抽成，也就是店家要分給電子支付廠商的錢。 --&gt;
&lt;p&gt;站在店家端的角度看，行動支付的崛起，意味著抽成的增加。&lt;&#x2F;p&gt;
&lt;p&gt;以信用卡來說，銀行端對店家的抽成大約是內扣 2.5%，也就是消費者花 100 塊，裡面大約有 2.5 塊會由銀行抽走，而在電子支付這邊，目前的抽成比信用卡略高，大約在 3%，同樣的也是內扣，每 100 塊就有 3 塊流向電子支付廠商，通路實際上只會收到 97 塊。&lt;&#x2F;p&gt;
&lt;!-- 儘管只有兩三塊，但一旦規模化後就是筆鉅額的收入，以一個月一百億的交易金額估算，做為交易中間人的電子支付廠商一個月就有三億，如果再把交易金額放大到與信用卡相當的每月三千億，那一個月的中間人抽成就有九十億，一年就有一千億的抽成收入。 --&gt;
&lt;p&gt;並且與信用卡不同的是，電子支付因為不用做信用評分，使用門檻更低，用戶年齡層更廣，抽成造成的衝擊也更廣。&lt;&#x2F;p&gt;
&lt;p&gt;拿小吃店與餐廳為例，在信用卡時代，只有餐廳接受刷卡，小吃店幾乎都只收現金，信用卡的抽成對小吃店毫無影響，而到了所謂的「進步」的電子支付時代，小吃店也得被迫跟上潮流，接上那一台又一台的電子支付設備，硬生生被吃掉那 3% 營業額。&lt;&#x2F;p&gt;
&lt;p&gt;如果說 3% 不算什麼，那我們轉頭看看外送平台。&lt;&#x2F;p&gt;
&lt;p&gt;外送平台對店家同樣是採取內扣抽成的模式，他們做外送服務，順便代理金流，這樣的抽成是幾趴呢？&lt;&#x2F;p&gt;
&lt;p&gt;答案是 3% 的十倍，30%。&lt;&#x2F;p&gt;
&lt;p&gt;說到底服務有價，在高抽成的背後也為店家帶來新的客源，似乎也沒什麼好抱怨的，但真正影響深遠，卻又未被大多數人察覺的，是那奇葩的「同價條款」。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;tong-jia-tiao-kuan&quot;&gt;同價條款&lt;&#x2F;h2&gt;
&lt;p&gt;不論是信用卡、電子支付、外送，他們與店家的合約中都有這麼一條「同價條款」，簡單的說，就是必須與現金同價，不能因為我要抽成，你就搞價格差異，現金價 100，刷卡價 110，外送價 150，這樣是不被允許的，於是上有政策，下有對策的戲碼就出現了。&lt;&#x2F;p&gt;
&lt;p&gt;同樣 100 塊的便當，內用與外送，會拿到完全相同份量的便當嗎？在誠信原則之下，好像應該要相同份量才合情合理，然而如果考慮到抽成以及同價條款，事情就變得複雜一些了。&lt;&#x2F;p&gt;
&lt;p&gt;攤開便當的成本結構，原本的毛利大約有 50 元，給外送抽走 30 元之後，一個便當的毛利只剩下 20 元。&lt;&#x2F;p&gt;
&lt;p&gt;於是聰明的店老闆當然只好用一些小技巧來維持毛利，例如外送份量縮水，或者更精明一點的，為了避免落人口實，另外發明「外送專用菜單」，也就是ＡＢ菜單，店內的 30 元滷肉飯在外送菜單上搖身一變成了 60 元的「&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.ptt.cc&#x2F;bbs&#x2F;joke&#x2F;M.1399114514.A.D4A.html&quot;&gt;嫩切豬肉淋飯&lt;&#x2F;a&gt;」，實際上就是多了半顆蛋＋筍絲的滷肉飯，而逼迫老闆 &lt;del&gt;出此下策&lt;&#x2F;del&gt; &lt;ins&gt;出此上策&lt;&#x2F;ins&gt;的就是那「同價條款」。&lt;&#x2F;p&gt;
&lt;p&gt;然而上面的方法不是萬靈丹，店家為了維持自身的形象，以及面臨同業的競爭，加上兩大外送平台的菜單審核制度，大部分的店家還是沒有採用ＡＢ菜單，選擇提供相同菜單與價格給消費者，接受毛利只剩 20 元的事實，將 30 元視為行銷費用，期待換來更多的外送訂單，但這樣脆弱的平衡在原物料上漲之後又馬上被輕易的打破。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wan-wu-jie-zhang&quot;&gt;萬物皆漲&lt;&#x2F;h2&gt;
&lt;p&gt;這一年來原物料上漲已經不是新聞，但在合約的同價條款的約束下，20 元的毛利又再次被壓縮到只剩 10 元，為了生存，店家除了漲價別無選擇，於是乎經過一番商業包裝後，市面上紛紛出現了精品級的小吃，原本只是惡搞的&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;pttgopolitics.com&#x2F;gossiping&#x2F;M.1635773094.A.B0C.html&quot;&gt;「嫩切豬肉淋飯」成為現實&lt;&#x2F;a&gt;，他們的共同特色是用料好了一兩分、價格漲了七八分，再來點氣氛加成，民眾還是搶著買單，反正喊窮是月底的事。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wai-song-jue-qi-quan-min-mai-dan&quot;&gt;外送崛起 全民買單&lt;&#x2F;h2&gt;
&lt;p&gt;同價條款之下，受害最深的其實是用現金的消費者，因為同價條款，他們得被迫接受更貴的價格，或更差的品質。&lt;&#x2F;p&gt;
&lt;p&gt;以速食店為例，在外送出現以前，一份 100 元的套餐，毛利 50 元，外送出現以後，毛利瞬間被壓縮到剩 20 元，為了維持毛利，只好漲價到 160 元，再調整原物料成本比例，才能回到類似的毛利：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;commission&#x2F;structure.png&quot; alt=&quot;外送影響成本結構&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在過去，我付 100 元現金，買到的是價值 50 元的餐點，而漲價之後，我付 160 元的現金，換換回的卻只是價值 64 元的餐點，多付了 60 元卻只多享受 14 元，肯德基並不會因為我是現金就多送我上校雞塊。（這裡只是舉例，實際上大品牌對外送平台有獨家的議價能力，但問題的本質依然存在。）&lt;&#x2F;p&gt;
&lt;p&gt;於是乎演變成，&lt;strong&gt;外送崛起、全民買單&lt;&#x2F;strong&gt;，就算你不點外送，還是要被迫共同承擔外送帶來的漲價，只因為合約裡的那條「同價條款」。&lt;&#x2F;p&gt;
&lt;p&gt;再轉頭看原本毛利只有 40% 甚至更低的零售業，在外送開始介入零售配送市場之後，在餐飲業發生過的戲碼將再次上演（或已經上演），而狀況只會更糟，讓我們繼續看下去。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>你真的需要多元支付嗎？</title>
        <published>2022-02-19T00:00:00+00:00</published>
        <updated>2022-02-19T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/xpay/"/>
        <id>https://editor.leonh.space/2022/xpay/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/xpay/">&lt;p&gt;多元支付當道的現代，開店不收個 XX Pay 好像古代人一樣，但是導入多元支付對店家來說究竟需要考慮哪些因素呢？在開始談多元支付的利與弊之前，先來談談市面上有哪些多元支付，現在可說是多元支付的戰國時代，除了信用卡、悠遊卡、一卡通、LINE Pay、街口以外，有更多沒聽過也沒用過的業者都在擠進這個市場，畢竟一筆交易就能抽 2% ~ 3% 的💰 ，誰不想呢。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;duo-yuan-zhi-fu-de-fen-lei&quot;&gt;多元支付的分類&lt;&#x2F;h2&gt;
&lt;p&gt;一般人認知的多元支付，不外乎那些通用型的 XX Pay，但若把它們歸納一下，可以依照運作機制的不同做個簡單的分類。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;shi-ti-xin-yong-qia&quot;&gt;實體信用卡&lt;&#x2F;h3&gt;
&lt;figure&gt;
    &lt;img alt=&quot;&quot; src=&quot;mark-oflynn-bqjswIxbhEE-unsplash.jpg&quot;&gt;
    &lt;figcaption&gt;POS Credit Card Machine&lt;&#x2F;figcaption&gt;
    &lt;figcaption&gt;圖片來自 &lt;a href=&quot;https:&#x2F;&#x2F;unsplash.com&#x2F;photos&#x2F;bqjswIxbhEE&quot;&gt;Mark OFlynn&lt;&#x2F;a&gt;&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;就是最單純的信用卡，店家需要和銀行簽約，由銀行提供卡機，並依照交易金額向&lt;strong&gt;店家&lt;&#x2F;strong&gt;收取 1.X% ~ 3.X% 不等的費用，常見的發卡組織有 JCB、Mastercard、VISA、AE、銀聯等，信用卡發行機構則大多是銀行，樂天信用卡是目前台灣唯一非銀行的發卡機構，不過等樂天銀行開幕之後應該也會把信用卡業務併入。&lt;&#x2F;p&gt;
&lt;p&gt;本文的實體信用卡的分類也包括簽帳金融卡，也就是國外的 debit card，雖然在銀行端和消費者扣款的機制不同，但對店家端的操作與帳務處理都是一樣的。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;xing-dong-xin-yong-qia&quot;&gt;行動信用卡&lt;&#x2F;h3&gt;
&lt;figure&gt;
    &lt;img alt=&quot;&quot; src=&quot;blake-wisz-Xn5FbEM9564-unsplash.jpg&quot;&gt;
    &lt;figcaption&gt;圖片來自 &lt;a href=&quot;https:&#x2F;&#x2F;unsplash.com&#x2F;photos&#x2F;Xn5FbEM9564&quot;&gt;Blake Wisz&lt;&#x2F;a&gt;&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;Apple Pay、Google Pay、台灣 Pay 這類把實體卡片虛擬化的支付，在這裡把它們歸類為行動信用卡。&lt;&#x2F;p&gt;
&lt;p&gt;以 Apple Pay 為例，消費者把信用卡登錄進去之後，Apple Pay 會產生一組虛擬信用卡號，消費時打開 Apple Pay，手機和卡機之間透過和感應式信用卡同樣的 RFID &#x2F; NFC 無線技術進行刷卡作業。除了 Apple Pay 之外，其他家也都有各自的交易機制，但使用上不脫卡機感應和 QR code 掃碼兩種。&lt;&#x2F;p&gt;
&lt;p&gt;感應式行動信用卡刷卡對店家來說，和一般的實體感應卡的操作和帳務處理作業方式是一樣的，不用另外增添設備，只要確定卡機本身是支援感應刷卡的即可，POS 端也不用做特別的處理，對消費者來說也和一般實體感應卡是一樣的，消費扣款是直接扣卡片的信用額度，並沒有「儲值錢包」的概念。&lt;&#x2F;p&gt;
&lt;p&gt;若是用 QR code 的方式，對店家來說，就不能只靠卡機，需要在 POS 端做支援才能使用。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;dian-zi-zhi-fu-dian-zi-piao-zheng&quot;&gt;電子支付、電子票證&lt;&#x2F;h3&gt;
&lt;figure&gt;
    &lt;img alt=&quot;&quot; src=&quot;markus-winkler-ENtCxRV1Boo-unsplash.jpg&quot;&gt;
    &lt;figcaption&gt;圖片來自 &lt;a href=&quot;https:&#x2F;&#x2F;unsplash.com&#x2F;photos&#x2F;ENtCxRV1Boo&quot;&gt;Markus Winkler&lt;&#x2F;a&gt;&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;目前台灣的法規，把一卡通、悠遊卡以電子票證的法令管理（&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;law.moj.gov.tw&#x2F;LawClass&#x2F;LawAll.aspx?pcode=G0380207&quot;&gt;電子票證發行管理條例&lt;&#x2F;a&gt;、&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;law.moj.gov.tw&#x2F;LawClass&#x2F;LawAll.aspx?pcode=G0380210&quot;&gt;電子票證發行機構業務管理規則&lt;&#x2F;a&gt;），而 LINE Pay (Money)、街口等則是以電子支付法令管理（&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;law.moj.gov.tw&#x2F;LawClass&#x2F;LawAll.aspx?pcode=G0380237&quot;&gt;電子支付機構管理條例&lt;&#x2F;a&gt;、&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;law.moj.gov.tw&#x2F;LawClass&#x2F;LawAll.aspx?pcode=G0380245&quot;&gt;電子支付機構業務管理規則&lt;&#x2F;a&gt;），&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.chinatimes.com&#x2F;newspapers&#x2F;20201008000243-260205?chdtv&quot;&gt;未來會整合成新的電子支付法令&lt;&#x2F;a&gt;統一管理，即便目前管轄法規上有所不同，但不論對消費者或店家而言，應用上都是一樣的。&lt;&#x2F;p&gt;
&lt;p&gt;目前這個領域上的品牌有一卡通、和一卡通結盟的 LINE Pay 和 LINE Pay Money、悠遊卡和悠遊付、iCash 和 iCash Pay、街口等，雖然有這麼多參賽者，但也只有 LINE Pay 和街口能互爭一二，其他在大眾消費市場幾乎可以無視，只存在於某些特殊場域，如學校、販賣機、停車場、大眾運輸、醫院等。&lt;&#x2F;p&gt;
&lt;p&gt;前面提的電子支付和電子票證界線模糊的趨勢也可以從支付業者的走向看出一二，原本的電子票證業者（一卡通、悠遊卡、iCash、HappyCash）也都申請了電子支付的牌照，推出了電子支付的產品，一卡通是和 LINE 結盟推出 LINE Pay Money，悠遊卡自己推出悠遊付，iCash 的 iCash Pay，HappyCash 的 HAPPY GO Pay 等。&lt;&#x2F;p&gt;
&lt;p&gt;電子支付的消費都是從消費者的儲值錢包內扣款，而錢包的儲值則大多是連結消費者的銀行帳戶，從儲值錢包扣款後會進入電子支付業者的帳戶，商家再與電子支付業者在固定週期進行對帳和結款。雖然 QR code 掃碼認證動作很簡單，但背後的資料交換的邏輯其實很複雜，身為資訊廠商的我們，當然也要&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;doublesllash.com&#x2F;blog&#x2F;posts&#x2F;2021&#x2F;pos-principle&quot;&gt;把複雜的機制包裝得很簡單優雅&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;另外一種情境是在電子支付 app 內設定好連結的信用卡，消費者一樣是透過 QR code 掃碼支付，電子支付業者再再向發卡機構請款和拆帳，這種情境雖然有牽涉到信用卡，不過那是支付業者、發卡機構、消費者三方的關係，對店家來說，與儲值錢包扣款一樣，都只要與電子支付業者做對帳與結款即可。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;qi-ta-dai-shou-ye-zhe&quot;&gt;其他代收業者&lt;&#x2F;h3&gt;
&lt;figure&gt;
    &lt;img alt=&quot;&quot; src=&quot;kai-pilger-dwBZLRPhHjc-unsplash.jpg&quot;&gt;
    &lt;figcaption&gt;圖片來自 &lt;a href=&quot;https:&#x2F;&#x2F;unsplash.com&#x2F;photos&#x2F;dwBZLRPhHjc&quot;&gt;Kai Pilger&lt;&#x2F;a&gt;&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;代收業者，像是黑貓、Uber Eats、Foodpanda，他們的雖然都不是支付，但它們的確幫店家代收了款項，也因此有對帳和結款的作業產生，因此就廣義的來看，它們也應當算是支付的一種。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zhuan-yong-xing-zhi-fu&quot;&gt;專用型支付&lt;&#x2F;h3&gt;
&lt;p&gt;最後一種是集團或品牌的專用支付，像全聯的全聯支付、全家超商的 My FamiPay 等，這類通路限定的支付因為場景有所侷限，就不多做討論。不過另外一提的是統一集團的 iCash &#x2F; iCash Pay 和遠東集團的 HappyCash &#x2F; HAPPY GO Pay 並非自家集團限定，而是通用型的支付，只是出了集團外就沒人用而已，所以還是歸類在上面的電子支付和電子票證內。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zheng-li&quot;&gt;整理&lt;&#x2F;h3&gt;
&lt;div class=&quot;wide&quot;&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;種類&lt;&#x2F;th&gt;&lt;th&gt;收款方式&lt;&#x2F;th&gt;&lt;th&gt;結款對象&lt;&#x2F;th&gt;&lt;th&gt;代表業者&lt;&#x2F;th&gt;&lt;th&gt;抽成&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;實體信用卡&lt;&#x2F;td&gt;&lt;td&gt;信用卡機刷卡或感應&lt;&#x2F;td&gt;&lt;td&gt;發卡機構&lt;&#x2F;td&gt;&lt;td&gt;各家銀行&lt;&#x2F;td&gt;&lt;td&gt;1.X% ~ 2.X%&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;行動信用卡&lt;br&gt;&lt;small&gt;感應型&lt;&#x2F;small&gt;&lt;&#x2F;td&gt;&lt;td&gt;信用卡機感應&lt;&#x2F;td&gt;&lt;td&gt;發卡機構&lt;&#x2F;td&gt;&lt;td&gt;Apple Pay、Google Pay&lt;&#x2F;td&gt;&lt;td&gt;1.X% ~ 2.X%&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;行動信用卡&lt;br&gt;&lt;small&gt;掃碼型&lt;&#x2F;small&gt;&lt;&#x2F;td&gt;&lt;td&gt;QR code&lt;&#x2F;td&gt;&lt;td&gt;發卡機構&lt;&#x2F;td&gt;&lt;td&gt;台灣 Pay&lt;&#x2F;td&gt;&lt;td&gt;1.X% ~ 2.X%&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;電子支付&lt;&#x2F;td&gt;&lt;td&gt;QR code&lt;&#x2F;td&gt;&lt;td&gt;電子支付業者&lt;&#x2F;td&gt;&lt;td&gt;LINE Pay (Money)、街口&lt;&#x2F;td&gt;&lt;td&gt;2.X% ~ 3.X%&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;電子票證&lt;&#x2F;td&gt;&lt;td&gt;票證感應卡機&lt;&#x2F;td&gt;&lt;td&gt;電子票證業者&lt;&#x2F;td&gt;&lt;td&gt;一卡通、悠遊卡&lt;&#x2F;td&gt;&lt;td&gt;1.X% ~ 2.X%&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;外送代收&lt;&#x2F;td&gt;&lt;td&gt;代收&lt;&#x2F;td&gt;&lt;td&gt;外送業者&lt;&#x2F;td&gt;&lt;td&gt;Uber Eats、Foodpanda&lt;&#x2F;td&gt;&lt;td&gt;3.X%&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;宅配代收&lt;&#x2F;td&gt;&lt;td&gt;代收&lt;&#x2F;td&gt;&lt;td&gt;宅配業者&lt;&#x2F;td&gt;&lt;td&gt;中華郵政、宅配通、宅急便&lt;&#x2F;td&gt;&lt;td&gt;宅配每件 55 ~ 65&lt;br&gt;低溫每件 155 ~ 160&lt;br&gt;貨到付款每件外加 30&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;&#x2F;div&gt;
&lt;h2 id=&quot;duo-yuan-zhi-fu-de-li-yu-bi&quot;&gt;多元支付的利與弊&lt;&#x2F;h2&gt;
&lt;p&gt;雖然現在支援 XX Pay 好像是理所當然的，但作為一個經營者，決定導入新支付前，最好還是把資訊轉化成能量化的數字。&lt;&#x2F;p&gt;
&lt;p&gt;支付業者在招攬店家時，大部分對店家的吸引手法大多是行銷面的，譬如說宣傳物曝光、會員數量、積點方案、聯合行銷企劃等等，但難就難在這些對業主來說可能都是難以量化成數字的資訊，在採用ＸＸ支付後，究竟帶來的新業績能不能大於那 2% ~ 3% 要貢獻給支付業者的費用，其實大部分業者心裡都沒個底，如果沒有仔細評估的話，很有可能就像下圖的苦主，整體營業額沒成長，反倒是ＸＸ支付的佔比越來越高，苦主要貢獻給ＸＸ支付的費用也越來越高，這種就是典型的誤交損友，不僅沒幫到你，還吃掉你的收入。&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
    &lt;img alt=&quot;&quot; src=&quot;1.png&quot;&gt;
    &lt;figcaption&gt;紅線是整體營業額，藍線是ＸＸ支付的營業額&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;看上圖說故事，紅茶苦主導入ＸＸ支付後，還配合支付業者玩了一大堆行銷活動，包括打折促銷，最後換來的是整體營業額完全沒成長，又因為打折賣自砍毛利所以收入減少，還要再付每筆 2.X% 的費用給支付業者。&lt;&#x2F;p&gt;
&lt;p&gt;看完苦主的例子，身為經營者，在面對支付業者招攬時那些行銷時的話語，真的應該靜下來思考到底那些行銷方案對你的生意到底是把餅做大還是跟你搶餅乾屑。&lt;&#x2F;p&gt;
&lt;p&gt;回到量化的主題，除了向支付業者要求過往類似店家的數據參考外，自己店內的營業數據也是很好的參考對象，舉例來說，我們可以從客群特徵來推估他們對ＸＸ支付的愛好度，在以未成年為主要客群，或者平均客單五百元以內的店家來說，顯然導入信用卡不是個好主意；或者店面位址較差的店家，那主動經營外送業務就是必要的動作，事實上參數是列不完的，每個店家又都有自己的營業特性，不存在一個萬用的公式套用在所有業者身上，但各位經營者心中應該都要有自己的一把尺，在決策時最好還是靜下心來想想導入新支付的利弊得失。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>爬 SITCA 網站</title>
        <published>2022-02-13T00:00:00+00:00</published>
        <updated>2022-02-13T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/sitca/"/>
        <id>https://editor.leonh.space/2022/sitca/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/sitca/">&lt;p&gt;SITCA 全名是超長的「證券投資信託暨顧問商業同業公會」，網站內有&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.sitca.org.tw&#x2F;ROC&#x2F;Industry&#x2F;IN2002.aspx&quot;&gt;台灣全部境內基金的各種統計資料&lt;&#x2F;a&gt;，不幸的是它沒開放 API 讓人查詢，所以只好來硬的…。&lt;&#x2F;p&gt;
&lt;p&gt;第一個要對付的叫做&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.sitca.org.tw&#x2F;ROC&#x2F;Industry&#x2F;IN2105.aspx&quot;&gt;基金基本資料表&lt;&#x2F;a&gt;，它長這樣：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;sitca&#x2F;e4b8ade88fafe6b091e59c8be8ad89e588b8e68a95e8b387e4bfa1e8a897e69aa8e9a1a7e5958fe59586e6a5ade5908ce6a5ade585ace69c83-md-lg.png&quot; alt=&quot;SITCA&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;簡單到不行的一個 form。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;version1&quot;&gt;Version1&lt;&#x2F;h2&gt;
&lt;p&gt;起初我天真的以為用 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.python.org&#x2F;zh-tw&#x2F;3&#x2F;library&#x2F;urllib.html&quot;&gt;&lt;code&gt;urllib&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; 來對付它即可，不管用，吐不出資料來。&lt;&#x2F;p&gt;
&lt;p&gt;是 cookie 搞鬼嗎？不是，關掉 cookie 正常。&lt;&#x2F;p&gt;
&lt;p&gt;是 JavaScript 嗎？不是，關掉 JavaScript 也正常。&lt;&#x2F;p&gt;
&lt;p&gt;辛苦爬文之後找到 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;python-mechanize&#x2F;mechanize&quot;&gt;mechanize&lt;&#x2F;a&gt;，它不只是抓抓網頁而已，它是更高階的模擬瀏覽器的行為，舉凡吃 cookie、騙 referrer、繞 robot.txt 都是拿手的。最後總算順利讓它吐資料出來：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;import&lt;&#x2F;span&gt;&lt;span&gt; mechanize&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;URL&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;https:&#x2F;&#x2F;www.sitca.org.tw&#x2F;ROC&#x2F;Industry&#x2F;IN2105.aspx&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;browser&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; mechanize.Browser()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;browser.open(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;URL&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;browser.select_form(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;aspnetForm&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;response&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; browser.submit().read()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;print&lt;&#x2F;span&gt;&lt;span&gt;(response)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;吐出來的資料是 HTML 碼，需要再配合 parser 把資料萃取出來。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;version2&quot;&gt;Version2&lt;&#x2F;h2&gt;
&lt;p&gt;把它修改成用函式呼叫的方式，以及增加送出 select 表單的部分：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;import&lt;&#x2F;span&gt;&lt;span&gt; mechanize&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; scrape&lt;&#x2F;span&gt;&lt;span&gt;(url:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;span&gt;, form_name:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; str&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    browser&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; mechanize.Browser()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    browser.open(url)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    browser.select_form(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;form_name)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    browser[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;ctl00$ContentPlaceHolder1$ddlQ_Comid&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;A0003&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; # 暫時只抓 A0003 第一金的資料簡化測試資料。&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return&lt;&#x2F;span&gt;&lt;span&gt; browser.submit().read()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;URL&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;https:&#x2F;&#x2F;www.sitca.org.tw&#x2F;ROC&#x2F;Industry&#x2F;IN2105.aspx&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;FORM_NAME&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;aspnetForm&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;html&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; scrape(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;URL&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; FORM_NAME&lt;&#x2F;span&gt;&lt;span&gt;).decode(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;utf-8&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;print&lt;&#x2F;span&gt;&lt;span&gt;(html)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;看第 9 行，form 裡面的每個控制項都可以用 dict 的方式存取，dict 的 key 來自於網頁中表單控制項的 &lt;code&gt;name&lt;&#x2F;code&gt; 屬性：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;sitca&#x2F;e7b6b2e9a081e6aaa2e996b1e599a8-e28094-http___www-sitca-org-tw_roc_industry_in2105-aspx-md.png&quot; alt=&quot;HTML&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;這裡的範例送出的是 &lt;code&gt;A0003&lt;&#x2F;code&gt; 第一金投信。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>認識負載測試與 k6</title>
        <published>2022-02-12T00:00:00+00:00</published>
        <updated>2022-02-12T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/k6/"/>
        <id>https://editor.leonh.space/2022/k6/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/k6/">&lt;p&gt;有玩過線上遊戲的朋友應該都聽過「&lt;strong&gt;萬人在線，全民公測&lt;&#x2F;strong&gt;」的口號，例如這個已經倒掉的《黃易群俠傳 2》：&lt;&#x2F;p&gt;
&lt;iframe src=&quot;https:&#x2F;&#x2F;www.youtube-nocookie.com&#x2F;embed&#x2F;oAUlTrbXA3I&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;問題是，怎麼確定服務做到能容納萬人在線呢？難道真的靠全民公測嗎？裝不下再「緊急加開伺服器」？（順便上一波廣告 🤔️）&lt;&#x2F;p&gt;
&lt;iframe src=&quot;https:&#x2F;&#x2F;www.youtube-nocookie.com&#x2F;embed&#x2F;Na-OzL7gSgU?start=16&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
&lt;iframe src=&quot;https:&#x2F;&#x2F;www.youtube-nocookie.com&#x2F;embed&#x2F;8zIlG3kk-rY?start=18&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;「緊急加開」也好，「威猛加開」也罷，這些都只是行銷的手法，我輩開發者如果真的靠全民公測才知道服務的容納量的話，除了心臟要很強之外，務必還要多放幾包綠色乖乖，以及被館長幹爆的準備。&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;k6&#x2F;ezgif.com-gif-maker.webp&quot; alt=&quot;館長&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：館長成吉思汗&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;如果不想把服務的穩定性寄託於命運之上，那最好還是了解一下&lt;strong&gt;負載測試&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fu-zai-ce-shi&quot;&gt;負載測試&lt;&#x2F;h2&gt;
&lt;p&gt;負載測試，或稱為壓力測試，是模擬大量用戶使用服務下，觀察服務的承載能力的一種測試模式，除了線上遊戲外，EC 領域在 XX 購物節也常常會遇到爆量的人潮，因此 EC 也是負載測試應用的領域，其他像是訂票服務、熱門的 app 這些有人數爆量可能性的服務，也都是用得上負載測試的領域。&lt;&#x2F;p&gt;
&lt;p&gt;負載測試可以讓我們知道服務的承載量，是一種較為巨觀的指標，即便是針對單一 API 端點的負載測試，依然無法單靠負載測試來解析系統內部的效能瓶頸，若想更深入地解析系統內具體的效能瓶頸，那得需要搭配其他手段才有辦法，例如 APM 、資料庫效能分析、硬體資源分析、log 分析等等。&lt;&#x2F;p&gt;
&lt;p&gt;另外在較新的 Kubernetes 或其他任何會自動擴展的架構，也可以利用負載測試來驗證自動擴展的機制有沒有正確做動，當然這也是要搭配 Kubernetes 監測工具一起觀察才能做出正確的驗證。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;fu-zai-ce-shi-de-gong-ju&quot;&gt;負載測試的工具&lt;&#x2F;h3&gt;
&lt;p&gt;用「負載測試」／「壓力測試」／「load testing」做搜尋，可以看到一票負載測試的工具或服務，比較常出現的大概是老牌的 ApacheBench 與 JMeter，但最後我們選擇的是 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;k6.io&quot;&gt;k6&lt;&#x2F;a&gt;，因為 k6 具備一些老牌工具沒有的特性，依重視度排序如下：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Integrable 可整合&lt;&#x2F;strong&gt;：可整合至 CI &#x2F; CD 流程內，以及測試結果可發送至像 Grafana 這類的外部儀表板系統內。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Scriptable 可制定腳本&lt;&#x2F;strong&gt;：可以用腳本語言撰寫測試行為，才能夠更真實的模擬用戶行為。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Able to be a service 提供服務&lt;&#x2F;strong&gt;：除了基礎的 CLI 程式外，最好還能有額外的測試服務，才能夠在未來日益加重的測試需求時，能夠用 load testing as a service 的模式做測試，而不用自行建立測試機台。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Well-documented 良好的文件&lt;&#x2F;strong&gt;：圖文並茂而不冗長的文件，而不是只有 CLI 的說明頁或那冗長的 man page。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Actively maintained 有持續維護更新&lt;&#x2F;strong&gt;：有更新才有可能支援較新的標準，例如 HTTP&#x2F;2、HTTP&#x2F;3、WebSocket、GraphQL、TLS 1.3 等。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Modern CLI design 現代化&lt;&#x2F;strong&gt;：最好有 subcommand 把子功能分門別類，並且 subcommand &#x2F; option &#x2F; parameter &#x2F; argument 的命名具有意義，不要是不知所謂的簡寫，像 Tar 這種老式工具沒有 subcommand 的概念，&lt;code&gt;tar --help&lt;&#x2F;code&gt; 一下就噴好幾頁的說明是不及格的。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;No runtime 不需要 runtime&lt;&#x2F;strong&gt;：不要有 runtime 或其它系統套件依賴，例如 JDK &#x2F; JRE &#x2F; .NET &#x2F; Microsoft C++ Redistrubutable 這些 runtime 都只增加了配置環境時的複雜度，另一方面，用 Docker 容器方案當然也是增加配置複雜度的兇手之一。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;綜合以上需求，我們選擇了 k6。&lt;&#x2F;p&gt;
&lt;p&gt;關於負載測試工具的選擇，可以參考 k6 的這篇〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;k6.io&#x2F;blog&#x2F;comparing-best-open-source-load-testing-tools&#x2F;&quot;&gt;Open source load testing tool review 2020&lt;&#x2F;a&gt;〉（中文摘錄版：〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;my.oschina.net&#x2F;wang7x&#x2F;blog&#x2F;4921058&quot;&gt;開源性能測試工具評測 2020&lt;&#x2F;a&gt;〉），作者雖然是 k6 的開發者，但內容頗為中肯，並沒有一昧自賣自誇。&lt;&#x2F;p&gt;
&lt;p&gt;k6 是以 Go 語言撰寫，編譯出的程式跨平台而且沒有 runtime，也只有單一執行檔，沒有週邊的函式庫，而 k6 的測試腳本語言是 JavaScript，我們可以在 JS 腳本內撰寫模擬用戶的流程，並且這些腳本也可以整合進大專案的版控與 CI &#x2F; CD 流程中，JavaScript 也是我們正在進行的 EC 專案的開發語言，這讓整個專案都存在 JS 宇宙內，讓環境配置更單一，符合 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;KISS%E5%8E%9F%E5%88%99&quot;&gt;KISS 原則&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;fu-zai-ce-shi-de-fen-lei&quot;&gt;負載測試的分類&lt;&#x2F;h3&gt;
&lt;p&gt;前面提到負載測試的簡單概念——模擬大量用戶湧入，並且觀察服務的承載能力，但測試實際上並不是粗暴的一次灌入萬人流量，負載測試本身還可以再做更深入的分類，參考 k6 的示意圖：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;k6&#x2F;test-types.png&quot; alt=&quot;負載測試類別&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：k6&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h4 id=&quot;smoke-testing&quot;&gt;Smoke Testing&lt;&#x2F;h4&gt;
&lt;p&gt;驗證測試腳本與服務的功能或邏輯正確，服務的功能正確與否通常由自己的單元測試腳本來驗證，而負載測試自己的腳本邏輯也是需要驗證的，所以不會貿然的寫完測試腳本馬上灌流量，而是先執行 smoke tesing 驗證測試的腳本與目的是正確的。&lt;&#x2F;p&gt;
&lt;p&gt;Smoke tesing 的流量圖如下，只有 1 個 VU，VU 即 virtual-user(s)，在 k6 裡面是發出的流量的數量，後面會再提到。&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;k6&#x2F;smoke-test.png&quot; alt=&quot;Smoke Testing&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：k6&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h4 id=&quot;load-testing&quot;&gt;Load Testing&lt;&#x2F;h4&gt;
&lt;p&gt;Load testing 是正常強度的負載驗證，所謂的正常強度是來自既有系統的經驗值，在不考慮行銷活動的高峰流量下，同一時間內正常流量的高峰值。&lt;&#x2F;p&gt;
&lt;p&gt;在 k6 文件內的範例情境：平均同時在線人數是 60 人，而最高會到 100 人。在這樣的情境下 load testing 應該要以 100 vu 為基準，流量圖如下：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;k6&#x2F;load-test.png&quot; alt=&quot;Load Testing&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：k6&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;注意到流量圖都不是瞬間就走到高原段，而是都會有前面的爬升段和尾部的下坡段，這樣的設計才更符合真實的情況，也讓我們在服務承受的負載在逐步上升的同時能觀察到硬體資源的佔用變化以及服務的回應時間變化，或是觀察服務的自動擴展機制是否生效等等。&lt;&#x2F;p&gt;
&lt;p&gt;Load testing 做為一種服務的基準負載水平驗證，適合整合進 CI &#x2F; CD 流程，一旦改版後的 load testing 測試不過，就不發布到正式環境，而發出 issue 給開發人員請他們調查改善，通過與否的指標可以是回應時間與功能正常運作與否，在 k6 文件內的舉例：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;99% 的請求應該在五秒內回應&lt;&#x2F;li&gt;
&lt;li&gt;95% 的請求應該在一秒內回應&lt;&#x2F;li&gt;
&lt;li&gt;99% 的登入測試應該要能成功作業。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;stress-testing&quot;&gt;Stress Testing&lt;&#x2F;h4&gt;
&lt;p&gt;Stress testing 用於驗證高負載下服務的狀態，例如模擬在 XX 購物節期間的同時最高在線流量，一樣是引用 k6 的範例流量圖如下：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;k6&#x2F;stress-test.png&quot; alt=&quot;Stress Testing&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：k6&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;同樣的，流量是逐步往上提昇的，我們想知道的不僅是在最高原段服務的狀態，也想知道在流量逐步上升的期間服務的狀態，包括硬體資源的消耗、服務的功能是否正常以及失效的現象、服務的回應時間變化、自動擴展機制的反應等，這些都是在行銷活動開跑前要確認過的指標與觀察點。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;spike-testing&quot;&gt;Spike Testing&lt;&#x2F;h4&gt;
&lt;p&gt;Spike testing 用於驗證更極端的流量條件下服務的表現：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;k6&#x2F;spike-test.png&quot; alt=&quot;Spike Testing&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：k6&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;在分類上，spike testing 也算是 stress testing 的一種，要注意的是，stress tesing 或 spike testing 都不要拿正式環境來玩，用 spike testing 把自己打爆一點都不浪漫的！&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;k6&#x2F;ezgif.com-gif-to-webp.webp&quot; alt=&quot;浪漫突刺&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：&lt;a href=&quot;https:&#x2F;&#x2F;youtu.be&#x2F;TcvQDzPGSgc&quot;&gt;反正我很閒&lt;&#x2F;a&gt;&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h4 id=&quot;soak-testing&quot;&gt;Soak Testing&lt;&#x2F;h4&gt;
&lt;p&gt;Soak testing 則像是 stress testing 的時間加長版，驗證服務在高流量且長時間下的表現，讓 memory leak、硬碟被 log 塞爆、資料庫空間被塞爆、外部服務被灌爆、race-condition 等平時難以察覺的錯誤都暴露出來。流量圖如下：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;k6&#x2F;soak-test.png&quot; alt=&quot;Soak Testing&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：k6&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;k6&quot;&gt;k6&lt;&#x2F;h2&gt;
&lt;p&gt;認識完負載測試，接著來玩 k6。&lt;&#x2F;p&gt;
&lt;p&gt;關於 k6 的安裝，在 macOS 是透過 Homebrew：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;brew&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; install k6&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在 Windows 可以透過安裝包、WinGet、Chocolatey 安裝，其他作業系統大多也是類似的一行安裝，不用配置 Node.js 或其他任何依賴項目。&lt;&#x2F;p&gt;
&lt;p&gt;在動手之前先了解 k6 的 JavaScript 的幾個特性：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;k6 的腳本語言 JavaScript 的引擎是由 Go 的 Goja 套件實現的，與 Node.js 無關，因此無法用 NPM 裝額外的 JS 套件，但可以用正宗的 &lt;code&gt;import&lt;&#x2F;code&gt; 引用外部模組，k6 也維護了一系列負載測試會用到的 JS 套件在 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;jslib.k6.io&#x2F;&quot;&gt;k6 JS Libraries&lt;&#x2F;a&gt;，方便我們調用。&lt;&#x2F;li&gt;
&lt;li&gt;承上，Node.js 提供的 &lt;code&gt;os&lt;&#x2F;code&gt;、&lt;code&gt;fs&lt;&#x2F;code&gt; 等本機模組是不存在於 k6 JS 的世界的，另外 k6 也不是瀏覽器，因此瀏覽器的 &lt;code&gt;window&lt;&#x2F;code&gt; 等物件也是不存在於 k6 JS 的。&lt;&#x2F;li&gt;
&lt;li&gt;另外一個特性是 k6 的 JavaScript 是&lt;strong&gt;有 IO blocking&lt;&#x2F;strong&gt; 的，因此在發出 HTTP 請求時，不需要用 &lt;code&gt;await&lt;&#x2F;code&gt; 控制流程，它會乖乖地在收到回應後才跑後面的程式。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;來看最簡單的 k6 測試腳本：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;import&lt;&#x2F;span&gt;&lt;span&gt; { sleep }&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; from&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;k6&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;import&lt;&#x2F;span&gt;&lt;span&gt; http&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; from&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;k6&#x2F;http&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;export default function&lt;&#x2F;span&gt;&lt;span&gt; () {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    http.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;get&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;https:&#x2F;&#x2F;test.k6.io&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;    sleep&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這幾行簡單的程式碼可以給我們一些聯想：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;看到 &lt;code&gt;get()&lt;&#x2F;code&gt;，那理所當然應該也會有 &lt;code&gt;post()&lt;&#x2F;code&gt; 等其他 HTTP 請求函式。&lt;&#x2F;li&gt;
&lt;li&gt;有 &lt;code&gt;post()&lt;&#x2F;code&gt;，那一定也有方法讓我們設定 HTTP header 和 payload。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;跑測試也很簡單：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;k6&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; run&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --vus 10 --duration&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; 20s script1.js&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面給了兩個參數：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;vus&lt;&#x2F;code&gt; 即前面提過的 virtual-user(s)，可以理解為請求的併發數。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;duration&lt;&#x2F;code&gt; 即測試執行的時間&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;解讀上面那句命令就是「同時開 10 根請求反覆跑，直到時間到 20 秒為止」。那總共發出幾次請求呢？不知道，要等測完才知道，因為這與受測方的回應時間長短有關，所謂的反覆跑就是收到回應後，再跑下一回合，直到 20 秒的時限到達，再注意到上面的範例程式內有 &lt;code&gt;sleep(1)&lt;&#x2F;code&gt;，所以在兩次請求中間會休息一秒，而上面這樣的行為，總共有 10 根請求在各自進行。&lt;&#x2F;p&gt;
&lt;p&gt;跑完 k6 會印出測試數據：&lt;&#x2F;p&gt;
&lt;div class=&quot;wide&quot;&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  execution: local&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     script: script1.js&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     output: -&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  scenarios: (100.00%) 1 scenario, 10 max VUs, 50s max duration (incl. graceful stop):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;           * default: 10 looping VUs for 20s (gracefulStop: 30s)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;running (20.2s), 00&#x2F;10 VUs, 160 complete and 0 interrupted iterations&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;default ✓ [======================================] 10 VUs  20s&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     data_received..................: 1.9 MB 92 kB&#x2F;s&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     data_sent......................: 29 kB  1.4 kB&#x2F;s&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     http_req_blocked...............: avg=49.91ms  min=0s       med=1µs      max=800.55ms p(90)=1µs      p(95)=796.95ms&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     http_req_connecting............: avg=12.93ms  min=0s       med=0s       max=208.81ms p(90)=0s       p(95)=205.3ms &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     http_req_duration..............: avg=209.12ms min=205.18ms med=208.78ms max=219.51ms p(90)=211.6ms  p(95)=212.93ms&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;       { expected_response:true }...: avg=209.12ms min=205.18ms med=208.78ms max=219.51ms p(90)=211.6ms  p(95)=212.93ms&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     http_req_failed................: 0.00%  ✓ 0    ✗ 160 &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     http_req_receiving.............: avg=907.59µs min=50µs     med=895µs    max=4.52ms   p(90)=1.63ms   p(95)=1.88ms  &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     http_req_sending...............: avg=72.01µs  min=22µs     med=63µs     max=382µs    p(90)=90.49µs  p(95)=129.64µs&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     http_req_tls_handshaking.......: avg=36.84ms  min=0s       med=0s       max=590.97ms p(90)=0s       p(95)=588.58ms&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     http_req_waiting...............: avg=208.14ms min=204.17ms med=207.86ms max=217.29ms p(90)=210.37ms p(95)=211.8ms &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     http_reqs......................: 160    7.927487&#x2F;s&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     iteration_duration.............: avg=1.25s    min=1.2s     med=1.2s     max=2.02s    p(90)=1.21s    p(95)=2s      &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     iterations.....................: 160    7.927487&#x2F;s&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     vus............................: 10     min=10 max=10&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     vus_max........................: 10     min=10 max=10&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;&#x2F;div&gt;
&lt;p&gt;左邊是指標（metric），右邊是統計數據，比較會看的大概是這些：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;http_req_duration&lt;&#x2F;code&gt;，一次請求從發送到收到回應的總時間&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;http_req_failed&lt;&#x2F;code&gt;，請求失敗的比例及數量&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;http_reqs&lt;&#x2F;code&gt;，請求發送的總數量及換算後每秒發出的請求數量&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;下面再來個複雜點的例子。&lt;&#x2F;p&gt;
&lt;p&gt;如果想要更細緻的控制請求的走勢，那就要在腳本內定義：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;import&lt;&#x2F;span&gt;&lt;span&gt; { check, sleep }&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; from&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;k6&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;import&lt;&#x2F;span&gt;&lt;span&gt; http&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; from&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;k6&#x2F;http&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;export let&lt;&#x2F;span&gt;&lt;span&gt; options&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    stages: [&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        { duration:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;30s&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, target:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 10&lt;&#x2F;span&gt;&lt;span&gt; },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        { duration:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;1m30s&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, target:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 30&lt;&#x2F;span&gt;&lt;span&gt; },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        { duration:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;20s&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, target:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 0&lt;&#x2F;span&gt;&lt;span&gt; },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    ],&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;export default function&lt;&#x2F;span&gt;&lt;span&gt; () {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    let&lt;&#x2F;span&gt;&lt;span&gt; res&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; http.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;get&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;https:&#x2F;&#x2F;httpbin.org&#x2F;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;    check&lt;&#x2F;span&gt;&lt;span&gt;(res, {&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;status was 200&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;: (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;res&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; res.status&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ==&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 200&lt;&#x2F;span&gt;&lt;span&gt; })&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;    sleep&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面的 &lt;code&gt;target&lt;&#x2F;code&gt; 就是指定 UVs 的參數，而 &lt;code&gt;stages&lt;&#x2F;code&gt; 可想而知是定義階段的參數。但在 &lt;code&gt;stages&lt;&#x2F;code&gt; 內的運作方式與 CLI 略有不同，此處的第一階段（&lt;code&gt;duration: &#x27;30s&#x27;, target: 10&lt;&#x2F;code&gt;）指的是「在 30 秒間逐步把 VUs 加到 10 根」而不是像 CLI 那樣是瞬間開出，依此類推，第二階段就是在一分半內逐步從 10 VUs 追加到 30 VUs；第三階段是在 20 秒內逐步從 30 VUs 降到 0 VUs。&lt;&#x2F;p&gt;
&lt;p&gt;前面在談負載測試的分類時，也有提過負載最好都是逐步增加和減少的，如此才比較好觀察到系統各個在各階段的狀態。&lt;&#x2F;p&gt;
&lt;p&gt;除了設定緩升緩降外，在主函式內我們也多了檢查 HTTP 狀態碼的敘述，確保收到的回應是成功的，而不是只是有回應。要注意的是 k6 的 &lt;code&gt;check()&lt;&#x2F;code&gt; 失敗最終並不會發出 exit code 1，僅會呈現在統計值上，若要引發錯誤得在 &lt;code&gt;thresholds&lt;&#x2F;code&gt; 內定義，這部分後面會再提到。&lt;&#x2F;p&gt;
&lt;p&gt;再聯想一下，看到 &lt;code&gt;res.status&lt;&#x2F;code&gt; 那應該就會有 &lt;code&gt;res.body&lt;&#x2F;code&gt;，是的，雖然 k6 並不是瀏覽器，但想要在測試內解析回應內容也是可以的，另外也有 &lt;code&gt;res.headers&lt;&#x2F;code&gt;，因此想要拿到 bearer token 模擬用戶登入的行為也是有可能的。&lt;&#x2F;p&gt;
&lt;p&gt;跑完的結果如下：&lt;&#x2F;p&gt;
&lt;div class=&quot;wide&quot;&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  execution: local&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     script: script4.js&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     output: -&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  scenarios: (100.00%) 1 scenario, 30 max VUs, 2m50s max duration (incl. graceful stop):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;           * default: Up to 30 looping VUs for 2m20s over 3 stages (gracefulRampDown: 30s, gracefulStop: 30s)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;running (2m21.2s), 00&#x2F;30 VUs, 1809 complete and 0 interrupted iterations&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;default ✓ [======================================] 00&#x2F;30 VUs  2m20s&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     ✓ status was 200&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     checks.........................: 100.00% ✓ 1809 ✗ 0   &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     data_received..................: 18 MB   127 kB&#x2F;s&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     data_sent......................: 247 kB  1.7 kB&#x2F;s&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     http_req_blocked...............: avg=10.54ms  min=0s       med=1µs      max=1.04s    p(90)=1µs      p(95)=1µs     &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     http_req_connecting............: avg=3.41ms   min=0s       med=0s       max=216.55ms p(90)=0s       p(95)=0s      &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     http_req_duration..............: avg=222.75ms min=205.88ms med=209.5ms  max=628.84ms p(90)=220.34ms p(95)=327.83ms&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;       { expected_response:true }...: avg=222.75ms min=205.88ms med=209.5ms  max=628.84ms p(90)=220.34ms p(95)=327.83ms&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     http_req_failed................: 0.00%   ✓ 0    ✗ 1809&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     http_req_receiving.............: avg=112.13µs min=52µs     med=107µs    max=546µs    p(90)=145µs    p(95)=159µs   &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     http_req_sending...............: avg=66.79µs  min=22µs     med=62µs     max=426µs    p(90)=84.2µs   p(95)=102.59µs&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     http_req_tls_handshaking.......: avg=6.99ms   min=0s       med=0s       max=586.46ms p(90)=0s       p(95)=0s      &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     http_req_waiting...............: avg=222.57ms min=205.71ms med=209.33ms max=628.69ms p(90)=220.15ms p(95)=327.68ms&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     http_reqs......................: 1809    12.815411&#x2F;s&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     iteration_duration.............: avg=1.23s    min=1.2s     med=1.21s    max=2.25s    p(90)=1.23s    p(95)=1.37s   &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     iterations.....................: 1809    12.815411&#x2F;s&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     vus............................: 1       min=1  max=30&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     vus_max........................: 30      min=30 max=30&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;&#x2F;div&gt;
&lt;p&gt;注意到指標區多了 &lt;code&gt;checks&lt;&#x2F;code&gt; 的數據，這就來自程式內的 &lt;code&gt;check()&lt;&#x2F;code&gt; 函式。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;如果把 k6 整合進 CI &#x2F; CD 流程內，想要在測試失敗時發出 exit code 1，那就得在 &lt;code&gt;thresholds&lt;&#x2F;code&gt; 內定義關鍵指標（metric）：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;import&lt;&#x2F;span&gt;&lt;span&gt; http&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; from&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;k6&#x2F;http&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;export let&lt;&#x2F;span&gt;&lt;span&gt; options&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    thresholds: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        http_req_failed: [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;rate&amp;lt;0.01&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;],&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;   &#x2F;&#x2F; http errors should be less than 1% &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        http_req_duration: [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;p(95)&amp;lt;200&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;],&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; &#x2F;&#x2F; 95% of requests should be below 200ms&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;export default function&lt;&#x2F;span&gt;&lt;span&gt; () {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    http.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;get&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;https:&#x2F;&#x2F;test-api.k6.io&#x2F;public&#x2F;crocodiles&#x2F;1&#x2F;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面的範例定義了兩項指標與目標，如果這兩個目標沒有達到，那跑完就會是 exit code 1，而 CI &#x2F; CD 就可以藉此發出 build failure 的訊息。&lt;&#x2F;p&gt;
&lt;p&gt;至此我們應該可以掌握 k6 的基本操作。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-yu&quot;&gt;結語&lt;&#x2F;h2&gt;
&lt;p&gt;k6 是個入門簡單，但也功能強大的負載測試工具，本篇文章大多數內容也都是取材自 k6 的文件，但也僅止於自己有碰到的部分做粗略地介紹，其他較深入的用法還是請參考 k6 的文件，內容相當豐富，如果還有下篇的話那應該是把測試結果發送到外部儀表板的相關內容了吧！（最近也滿關注 Grafana 的）&lt;&#x2F;p&gt;
&lt;p&gt;最後談一下 k6 的故事，k6 的創辦人最早是在 2000 年為當時開發的線上遊戲做負載測試，因此而開展了負載測試顧問的服務，一直到 2008 年，SaaS 架構興起之後，他們認為負載測試有可能變成一種 SaaS 的商業模式時即開始開發 k6 至今，而最新的消息是 Grafana 宣佈併購 k6。&lt;&#x2F;p&gt;
&lt;p&gt;在商業模式方面，k6 的 CLI 工具是開源的，而 k6 Cloud 則是收費的服務，透過 k6 Cloud 可以省去自行配置發測端機台的功夫，還可以任選發測機台的地理位置，以及提供精美的圖表，可以預期的是 k6 和 Grafana 將會有更深度的整合，希望 k6 未來還能保持開源＋商業的混合模式。&lt;&#x2F;p&gt;
&lt;p&gt;最後的最後私心地送上一張帥哥自拍照！&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;k6&#x2F;0F5EF268-BF0E-4EB8-BDCA-DA4FC456936C_1_105_c.jpeg&quot; alt=&quot;豆干&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>用 Spectron 對 Electron App 做測試</title>
        <published>2022-02-10T00:00:00+00:00</published>
        <updated>2022-02-10T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/spectron/"/>
        <id>https://editor.leonh.space/2022/spectron/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/spectron/">&lt;h2 id=&quot;electron&quot;&gt;Electron&lt;&#x2F;h2&gt;
&lt;p&gt;Electron 是 Node.js 生態圈的框架，用於開發跨平台&lt;strong&gt;桌面&lt;&#x2F;strong&gt;應用，目標平台可以是 Mac &#x2F; Linux &#x2F; Windows，可以視為把 web app 包裝成執行檔在自己的視窗內運行，並且透過 Electron 框架讓這支 app 有與作業系統層溝通的能力，包括存取檔案、週邊裝置等，很多目前&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;whatwebcando.today&#x2F;&quot;&gt;原生 web API 無法實現的能力&lt;&#x2F;a&gt;都可以透過 Electron 辦到。&lt;&#x2F;p&gt;
&lt;p&gt;最知名的 Electron 應用應該就是 VSCode 了，這款當代最棒的編輯器也是運用了 Electron 框架達成在桌面端跨平台的能力。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;electron-de-ce-shi&quot;&gt;Electron 的測試&lt;&#x2F;h2&gt;
&lt;p&gt;對最大宗的 Windows 平台的 Windows Forms 這類的原生桌面應用來說，市場上已經有許多 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.ibm.com&#x2F;tw-zh&#x2F;automation&#x2F;rpa&quot;&gt;RPA&lt;&#x2F;a&gt; 工具可以用於自動化測試，但因為 Electron 在介面元件上並未調用系統原生元件，而是由 HTML 元素來構成介面，而那些 RPA 工具都無法抓取到 HTML 元件，導致 RPA 完全無用武之地。&lt;&#x2F;p&gt;
&lt;p&gt;既然沒有 GUI 測試工具可以用，只能回歸寫 code 測 code 這招，Electron 有自己的測試框架 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.electronjs.org&#x2F;spectron&quot;&gt;Spectron&lt;&#x2F;a&gt;，Spectron 也可以搭配 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;mochajs.org&#x2F;&quot;&gt;Mocha&lt;&#x2F;a&gt; 等其它 JavaScript 測試框架使用，網路上爬文一下可以發現大部分也都就是 Spectron 配 Mocha 這樣的組合。&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;spectron&#x2F;mocha_and_matcha.png&quot; alt=&quot;Mocha &amp;amp; Matcha&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;Mocha &amp; Matcha&lt;&#x2F;figcaption&gt;
&lt;figcaption&gt;來源：&lt;a href=&quot;https:&#x2F;&#x2F;dictionary.cambridge.org&#x2F;zht&#x2F;&quot;&gt;劍橋詞典&lt;&#x2F;a&gt;&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;spectron-mocha&quot;&gt;Spectron + Mocha&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;electron-app&quot;&gt;Electron App&lt;&#x2F;h3&gt;
&lt;p&gt;像做菜節目一樣，前面都要先備料，我們必須先有一支 Electron app 來當作我們的測試 app，這裡我們用 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;minidiary.app&#x2F;&quot;&gt;Mini Diary&lt;&#x2F;a&gt; 這個 app 當作待測的目標，因為它夠簡單又夠複雜，所謂夠簡單－它的 app 本身功能簡單，操作也簡單；所謂夠複雜－它是 Electron 加 React 的架構，又搭配了一些 React 生態圈的套件；另一個夠複雜－它除了提供原始碼外，也幫我們打包好了各個平台上的安裝檔。&lt;&#x2F;p&gt;
&lt;p&gt;在這裡我們用 Mini Diary 的 macOS 套件包來使用，依照通用的流程安裝，它會被安裝到 &#x2F;Applications&#x2F; 目錄內，主程式就是 「&#x2F;Applications&#x2F;Mini Diary.app&#x2F;Contents&#x2F;MacOS&#x2F;Mini Diary」 這支檔案。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ban-ci-da-pei&quot;&gt;版次搭配&lt;&#x2F;h3&gt;
&lt;p&gt;前面提過 Electron app 是由 HTML 構成，為了在桌面前端 render 出這些 HTML，Electron app 裡面有包了一個 Chromium 做為前端的 renderer。而 Electron 的測試框架－Spectron，裡面也有一個 ChromeDriver 做為與 Electron 溝通的介面，就像純 web 常用的自動化測試工具 Selenium 也是透過 ChromeDriver 來操控 Chrome 一樣。&lt;&#x2F;p&gt;
&lt;p&gt;因為有 Chromium 與 ChromeDriver，就會有版次搭配的問題，兩者必須是相匹配的版次才可以成功的運作。&lt;&#x2F;p&gt;
&lt;p&gt;在 Electron 方面，&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;electron&#x2F;releases&quot;&gt;electron-releases&lt;&#x2F;a&gt; 這個專案內有列出各版本的 Electron 與 Chromium 的對應關係，以 Mini Diary 來說，它的 Electron 是 8.3.0 版，透過 Electron &#x2F; Chromium 版次表，可以查到對應的 Chromium 版次是 80。&lt;&#x2F;p&gt;
&lt;p&gt;在 Spectron 方面，它的專案文件同時也列出了 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;electron-userland&#x2F;spectron&quot;&gt;Spectron 與 Electron 的對應表&lt;&#x2F;a&gt;，透過查表可以知道 Electron 8.x 須與 Spectron 10.x 相互搭配。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zhuan-an-jian-zhi&quot;&gt;專案建置&lt;&#x2F;h3&gt;
&lt;p&gt;這裡我們為測試另建一個專案，叫做 minidiary-test，建立同名資料夾後，git init 和 npm init 一下，接著把 Mocha 和 Spectron 裝起來：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;npm&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; install mocha spectron&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;記得前面說的版次搭配性問題，有必要的話需指定 spectron 的版次。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ce-shi-cheng-shi&quot;&gt;測試程式&lt;&#x2F;h3&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;const&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; Application&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; require&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;spectron&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;).Application&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;const&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; assert&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; require&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;assert&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;describe&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;My Test App&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; function&lt;&#x2F;span&gt;&lt;span&gt; () {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    &#x2F;&#x2F; SETUP section&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    let&lt;&#x2F;span&gt;&lt;span&gt; app;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    this&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;timeout&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;20000&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;    before&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;function&lt;&#x2F;span&gt;&lt;span&gt; () {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        app&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; = new&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; Application&lt;&#x2F;span&gt;&lt;span&gt;({&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            path:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; `&#x2F;Applications&#x2F;Mini Diary.app&#x2F;Contents&#x2F;MacOS&#x2F;Mini Diary`&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        });&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        return&lt;&#x2F;span&gt;&lt;span&gt; app.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;start&lt;&#x2F;span&gt;&lt;span&gt;()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            .&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;catch&lt;&#x2F;span&gt;&lt;span&gt;(console.error)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    });&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    &#x2F;&#x2F; shutdown after all tests&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;    after&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;function&lt;&#x2F;span&gt;&lt;span&gt; () {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        if&lt;&#x2F;span&gt;&lt;span&gt; (app&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; &amp;amp;&amp;amp;&lt;&#x2F;span&gt;&lt;span&gt; app.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;isRunning&lt;&#x2F;span&gt;&lt;span&gt;()) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;            return&lt;&#x2F;span&gt;&lt;span&gt; app.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;stop&lt;&#x2F;span&gt;&lt;span&gt;();&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    });&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    &#x2F;&#x2F; TESTS section&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    &#x2F;&#x2F; test 1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;    it&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;Should have the correct title&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; async function&lt;&#x2F;span&gt;&lt;span&gt; () {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        const&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; title&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; = await&lt;&#x2F;span&gt;&lt;span&gt; app.client.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;getTitle&lt;&#x2F;span&gt;&lt;span&gt;();&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        assert.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;equal&lt;&#x2F;span&gt;&lt;span&gt;(title,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Mini Diary&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    });&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;});&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;能測試 Electron 的關鍵就在上面程式碼內的 &lt;code&gt;app&lt;&#x2F;code&gt;，把 &lt;code&gt;app&lt;&#x2F;code&gt; 定義出來後，就可以利用 Spectron 的 API 去存取 Electron app 的各項屬性，再配合 Mocha 的測試架構即可讓我們做到 Electron app 測試自動化。除了 &lt;code&gt;app&lt;&#x2F;code&gt; 以外的部分，&lt;code&gt;describe()&lt;&#x2F;code&gt;、&lt;code&gt;it()&lt;&#x2F;code&gt; 等都是 Mocha 的測試用函示，關於 Mocha 的用法請自行參閱 Mocha 的網站或其他大大們的文章。&lt;&#x2F;p&gt;
&lt;p&gt;跑看看測試：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;npx&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; mocha test&#x2F;test.js&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;結果正常的話應該像這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;My Test App&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ✓ Should have the correct title&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;1 passing (4s)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在上面的範例程式碼中，只有很簡單的確認 app 的標題是否為 Mini Diary，在實際的測試場景中顯然會更複雜，應該會有大量的調用 Sepctron API 對 app 進行自動化操作以及確認 app 視窗內的屬性值是否與預期相符，以及需要用 Chrome 開發者工具來查找元素與操作 console 等，&lt;del&gt;這部分只能先富奸了&lt;&#x2F;del&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;spectron-webdriverio&quot;&gt;Spectron &amp;amp; WebdriverIO&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;webdriver.io&#x2F;&quot;&gt;WebdriverIO&lt;&#x2F;a&gt; 是一個 JavaScript 的測試套件，它提供了一層 API 讓我們可以調用瀏覽器與網頁，雖然前面都沒提到它，但其實它也算是 Spectron 的一部分，在安裝 Spectron 的時候 WebdriverIO 也會被裝進專案內。Spectron 就是利用了 WebdriverIO 的 API 來提供給我們操控 Chromium 與 Electron app 頁面的能力，可以更具體地說，Spectron 只是對 WebdriverIO API 做了一層封裝，在 WebdriverIO API 文件內能調用的函式也都可以在 Spectron 上面做調用。&lt;&#x2F;p&gt;
&lt;p&gt;附帶提醒，延續前面提到的版次問題，新舊版的 Spectron 依賴的 WebdriverIO 也會有新舊版的問題，確認一下專案 package.json 內 &lt;code&gt;webdriverio&lt;&#x2F;code&gt; 套件的版次，並且查閱 API 文件時也要注意文件版次與專案內安裝的 &lt;code&gt;webdriverio&lt;&#x2F;code&gt; 版次要相符。&lt;&#x2F;p&gt;
&lt;p&gt;回歸正題，想要在測試腳本裡面去操控頁面元素的話，就必須利用 Spectron 的 API（也就是被封裝過的 WebdriverIO API）。&lt;&#x2F;p&gt;
&lt;p&gt;首先是選擇器，先上範例：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;let&lt;&#x2F;span&gt;&lt;span&gt; accountField&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; app.client.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;.account-input&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;accountField.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;setValu&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;user1&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;看到熟悉的 &lt;code&gt;$()&lt;&#x2F;code&gt; 錢字號函數與後面的 CSS 選擇器語法，和 jQuery 長一樣，雖然長一樣，不過 WebdriverIO 並不依賴 jQuery，只是借鑒錢字號這樣的函數命名而已，大概是希望提高對開發者的親切感吧。&lt;&#x2F;p&gt;
&lt;p&gt;在上面的範例的第二行，把元素抓到之後，就可以操作它，除了範例內做的填值之外，取值、點擊等都可以被實現。詳細的可調用的函式，請參考 WebdriverIO 文件。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;can-kao-zi-liao&quot;&gt;參考資料&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.glue42.com&#x2F;developers&#x2F;testing-your-app&#x2F;mocha-and-spectron&#x2F;index.html&quot;&gt;Glue42 Desktop: Mocha and Spectron&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;DaxiaLeeSuper&#x2F;article&#x2F;details&#x2F;78065263&quot;&gt;使用 Mocha + Spectron 测试 Electron 打包的桌面版程序（1）&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;blog.csdn.net&#x2F;DaxiaLeeSuper&#x2F;article&#x2F;details&#x2F;78107686&quot;&gt;使用 Mocha + Spectron 测试 Electron 打包的桌面版程序（2）PO 模型&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;blog.francium.tech&#x2F;spectron-for-electron-apps-automation-4c2062e37beb&quot;&gt;Spectron For Electron Apps Automation&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;livebook.manning.com&#x2F;book&#x2F;electron-in-action&#x2F;chapter-13&#x2F;&quot;&gt;Electron in Action: Chapter 13. Testing applications with Spectron&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;qi-ta-ce-shi-gong-ju&quot;&gt;其它測試工具&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;smartbear.com&#x2F;product&#x2F;testcomplete&#x2F;overview&#x2F;&quot;&gt;TestComplete&lt;&#x2F;a&gt;：Windows 桌面端收費商業應用，可以拿來測 Electron，好像很厲害，也很貴的貴。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.froglogic.com&#x2F;squish&#x2F;&quot;&gt;Squish&lt;&#x2F;a&gt;：跨平台自動化測試工具，也是很貴的貴。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>測試的一小步 專案的一大步</title>
        <published>2022-02-08T00:00:00+00:00</published>
        <updated>2022-02-08T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/one-test/"/>
        <id>https://editor.leonh.space/2022/one-test/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/one-test/">&lt;p&gt;本文僅獻給那些從來不寫測試的同學們。&lt;&#x2F;p&gt;
&lt;p&gt;我們都知道測試的重要性，教材也都鼓勵我們重視測試，追求測試覆蓋率，可是其實你知道、我知道、獨眼龍也知道，測試根本就沒有寫，東西看起來會動就好。（？！）&lt;&#x2F;p&gt;
&lt;p&gt;根據某些祖傳的統計數據，測試的程式碼平均會是其它程式碼的六倍，在資源（沒錢、沒人）與死線（沒時間）的雙重壓力下，也難怪測試始終寫不下去，因此本文只會請同學大大寫一個測試，真的一個就好。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yi-ge-ce-shi&quot;&gt;一個測試&lt;&#x2F;h2&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# test&#x2F;system&#x2F;home_page_test.rb&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; HomePageTest&lt;&#x2F;span&gt;&lt;span&gt; &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; ApplicationSystemTestCase&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  test&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;show homepage&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; do&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    visit &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;&#x2F;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    assert_text &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;Text on your homepage&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  end&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;end&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;跑一下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; bundle exec rails test:system&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這樣簡單的把首頁打開的測試，背後代表的意義是：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;專案的首頁被瀏覽器打開了。&lt;&#x2F;li&gt;
&lt;li&gt;因而專案必須真的跑起來好讓瀏覽器訪問。&lt;&#x2F;li&gt;
&lt;li&gt;你寫下了第一條測試，而且應該是成功的，好棒棒，給自己掌聲鼓勵。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;這只是測試的一小步，卻是專案的一大步。&lt;&#x2F;p&gt;
&lt;p&gt;忘掉上面六倍的恐怖數字吧！藉由這一個微小但確切的測試，看著它 pass 有沒有感到一絲絲的小確幸呢？&lt;&#x2F;p&gt;
&lt;p&gt;如果有的話（灑花轉圈圈），來寫下一個測試吧，可以是測試註冊帳號，也可以是測試登入，測試是一種累積，看到它們 pass 的那種安心感是難以言喻的，從此不用再提心吊膽、偷偷摸摸交付給客戶，測試是我們的好朋友，也是工程師的浪漫～。&lt;&#x2F;p&gt;
&lt;p&gt;本文改寫自〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;andycroll.com&#x2F;ruby&#x2F;write-one-test&#x2F;&quot;&gt;Write One Test&lt;&#x2F;a&gt;〉。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>動如脫兔的靜態網站產生器</title>
        <published>2022-02-05T00:00:00+00:00</published>
        <updated>2022-02-05T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/static-site/"/>
        <id>https://editor.leonh.space/2022/static-site/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/static-site/">&lt;!-- 一切都要從自身說起，我是個有數`位收藏癖的人，就好像有的人對剪報樂此不疲，我也有類似的癖好，不過這並非表示有多博學或什麼的，反而更像是一種自戀。

我的數位收藏癖只表現在自己寫下的文字，更具體地說就是網誌文，從最早在 2004 年啟用的 Blogger，中間也經歷過了幾次平台的轉換，但無法自行掌握文章的檔案始終令我感到不安，最後在`去年趁空把所有的文章全部手工搬家到 Publii， --&gt;
&lt;p&gt;從大學以來，一直斷斷續續有著寫網誌的習慣，回顧過去的那些累積，儘管以現在的眼光來看有點像廢文，但就像照片一樣，他們紀錄了當時的某一刻或心中的某個片段。這些長短交錯的文字，既是個人的回憶資產，但同時也是一種累贅，特別是在網誌搬家的時候。&lt;&#x2F;p&gt;
&lt;p&gt;這幾年來陸續用過 Blogger、Tumblr、WordPress、Publii、Ghost 以及更多早已遺忘或倒閉的平台，他們功能各異，互有長短，然而共同的問題是每次搬家都需要耗費大量的人工，經歷幾次的搬家與掙扎，最終我完全放棄了搬家，換一個平台，就等於又一次的歸零重啟，直到我遇見了「靜態網站產生器」。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jing-tai-wang-zhan-chan-sheng-qi&quot;&gt;靜態網站產生器&lt;&#x2F;h2&gt;
&lt;p&gt;在遇見靜態網站產生器之前，那些網誌平台託管了我們所有的內容，意味著我們並不真正的持有自己的第一手內容，而那些不論是原稿或備份，他們只能算是第二手內容，在修正錯別字或重構段落之後，都得要花額外的時間去修正原稿以及重新備份，或者乾脆放棄所謂的原稿、備份。&lt;&#x2F;p&gt;
&lt;p&gt;相較之下，靜態網站產生器的架構單純許多，網站的內容並不是資料庫的某些欄位，也不是某個區塊鏈上的雜湊或任何複雜的資料結構，有的只是一份份的純文字檔案，並且也是用最基本的檔案資料夾來整理這些純文字檔案。&lt;&#x2F;p&gt;
&lt;p&gt;這些檔案，是第一手原稿，也可以被輕易的備份。這裡的備份，沒有任何惱人的匯入匯出程序，也不會看到一堆看不懂的 XML 檔案，只需要最單純的「剪下、複製、貼上」三部曲，就能為我們的網站做備份。&lt;&#x2F;p&gt;
&lt;p&gt;靜態網站還有其他附帶的好處：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;靜態網站託管成本極低，免費的 Netlify 可以讓我們把靜態網站資料夾拖進去就變出一個站台給你，如果懂 Git 的話，還有更多其他的選擇。&lt;&#x2F;li&gt;
&lt;li&gt;靜態網站沒有後台，也就沒有人能嘗試入侵根本不存在的後台。&lt;&#x2F;li&gt;
&lt;li&gt;靜態網站沒有外掛，也就沒有外掛過時或衝突等奇奇怪怪的問題。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;總而言之，對個人取向的網站來說，採用靜態網站建立，可能是更經濟也更有效率的方式。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>給 Zola 的佈景主題 Float</title>
        <published>2022-02-04T00:00:00+00:00</published>
        <updated>2022-02-04T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/float/"/>
        <id>https://editor.leonh.space/2022/float/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/float/">&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;float-theme.netlify.app&#x2F;&quot;&gt;Float&lt;&#x2F;a&gt; 是 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.getzola.org&#x2F;&quot;&gt;Zola&lt;&#x2F;a&gt; 的佈景主題。&lt;&#x2F;p&gt;
&lt;p&gt;決定重拾網誌寫作以來，一邊用網誌發文之餘，一邊也在微調著每一個樣式細節，試圖做出一個讀起來舒服的版面與文字樣式，花心思做到讓自己覺得可以的階段後，便開始設想著是否應該把我的佈景主題分享給 Zola 的用戶使用（因為 Zola 的選擇少的可憐），於是乎經過一番奮戰後 Float 誕生了！&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fen-zhan-de-bu-fen&quot;&gt;奮戰的部份&lt;&#x2F;h2&gt;
&lt;p&gt;首先第一個要面對的是 Zola 佈景主題的限制與運作機制。&lt;code&gt;include()&lt;&#x2F;code&gt; 函式是不能在佈景主題內使用的，必須改用 &lt;code&gt;macro&lt;&#x2F;code&gt; 做類似的操作，這個差異迎來了 Float 第一次的結構調整，畢竟最初只以自用美觀為出發點，並未特別注意到佈景主題的差異。&lt;&#x2F;p&gt;
&lt;p&gt;第二是把一些寫死的部份抽離成可調整的參數，譬如說 Google Analytics ID、AdSense ID 等等，原本都是直接寫死在模板內，可是一旦要讓別的 Zola 用戶也可以用，就要改成可填入的參數，以 Zola 的做法就是讓這些參數放在 config.toml 的 &lt;code&gt;[extra]&lt;&#x2F;code&gt; 區段內。&lt;&#x2F;p&gt;
&lt;p&gt;第三是對版面的調整，以首頁來說，那些文章的卡片原本都是 200 px 寬，200 px 的寬對華文來說不會有特別奇怪，因為華文是方塊字並且可以任意斷行，但這樣的寬卻不太適合放英文，容易讓英文段落內出現大量的換行，再搭配上左右對齊的結果看起來會更詭異，因此再把版面調整到 300px 寬以取得較佳的洋文閱讀效果。&lt;&#x2F;p&gt;
&lt;p&gt;這其中也走了不少冤枉路，包括誤解了 Zola 的模板調用順序與繼承機制，最後都一一修正。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fa-bu&quot;&gt;發布&lt;&#x2F;h2&gt;
&lt;p&gt;經過一番調整總算到了感覺夠穩定也能夠被使用的階段，因此把 Float 發布到 GitLab，也向 Zola 發了 merge request 申請列入 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.getzola.org&#x2F;themes&#x2F;&quot;&gt;Zola Theme&lt;&#x2F;a&gt; 頁面內，希望小眾的 Zola 可以有點新鮮度。（我懷疑全台灣只有我在用 Zola？）&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;float&#x2F;Float.png&quot; alt=&quot;Float&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>靜態網站產生器 Zola</title>
        <published>2022-02-03T00:00:00+00:00</published>
        <updated>2022-02-03T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/zola/"/>
        <id>https://editor.leonh.space/2022/zola/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/zola/">&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.getzola.org&#x2F;&quot;&gt;Zola&lt;&#x2F;a&gt; 是一個以 Rust 語言開發的靜態網站產生器，特色是速度超快，了解靜態網站產生器的人都知道，每次寫完新的頁面都要經過重新建置的過程，這個重新建置的過程包含了幫我們把新頁面的連結加到所有應該有它出現的地方以及一些其它的編譯工作，這個過程會隨著網站頁面的增多而越來越久，而得益於 Rust 那媲美 C 的高效能，Zola 能夠更快速的處理完這個重新建置的工作。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;an-zhuang&quot;&gt;安裝&lt;&#x2F;h2&gt;
&lt;p&gt;在 elementary OS 下，用 snap 是最簡單的方式，在裝好 snapd 後，即可利用 &lt;code&gt;snap&lt;&#x2F;code&gt; 指令或是 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;snapcraft.io&#x2F;zola&quot;&gt;Snap 商店來安裝 Zola&lt;&#x2F;a&gt;：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;sudo&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; snap install zola&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --edge&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;裝完之後 Zola 會在 &#x2F;snap&#x2F;zola&#x2F; 內，有可能要重開機讓 PATH 環境變數重載才能正常使用 zola 命令。&lt;&#x2F;p&gt;
&lt;p&gt;如果是 macOS 則是透過 Homebrew 來安裝 Zola：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;brew&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; install zola&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;chu-shi-hua-yi-ge-zhuan-an&quot;&gt;初始化一個專案&lt;&#x2F;h2&gt;
&lt;p&gt;在自己電腦的網站原始檔案，我稱之為專案，建置後發布出去的稱為網站。&lt;&#x2F;p&gt;
&lt;p&gt;假設我們要初始化一個名為 ax.dev 的專案，並且這個專案預計會發布到 https:&#x2F;&#x2F;ax.space&#x2F;（純屬虛構），請服用以下指令：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;zola&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; init ax.space&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;會開始以目前最夯的對話式界面來逐步初始化這個專案：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;gt; What is the URL of your site? (https:&#x2F;&#x2F;example.com): https:&#x2F;&#x2F;ax.space&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;gt; Do you want to enable Sass compilation? [Y&#x2F;n]: Y&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;gt; Do you want to enable syntax highlighting? [y&#x2F;N]: Y&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;gt; Do you want to build a search index of the content? [y&#x2F;N]: N&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Done! Your site was created in &amp;quot;&#x2F;home&#x2F;leon&#x2F;Projects&#x2F;ax.space&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Get started by moving into the directory and using the built-in server: `zola serve`&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Visit https:&#x2F;&#x2F;www.getzola.org for the full documentation.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;裡面的問題可以依實際需求回覆，與這幾個問題相對的設定最後會儲存在專案資料夾內的 config.toml 檔案內，事後是可以更改的。&lt;&#x2F;p&gt;
&lt;p&gt;特別注意最後一題的搜尋索引功能對華文目前是無效的，因為 Zola 是用 Elasticlunr.js 來做搜尋索引，而 Elasticlunr.js 看起來自 2017 年以後已經開發停滯，看起來不能索引華文的問題也不會做修正了，殘念です。&lt;&#x2F;p&gt;
&lt;p&gt;前面 Zola 初始化的最後它讓我們執行 zola serve 來讓這個新的專案跑起來用瀏覽器開看看：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;zola&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; serve&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;跑起來會看到下面的訊息：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Building site...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;-&amp;gt; Creating 0 pages (0 orphan), 0 sections, and processing 0 images&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Done in 230ms.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Listening for changes in &#x2F;home&#x2F;leon&#x2F;Projects&#x2F;ax.dev&#x2F;{content, config.toml, static, templates, themes, sass}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Press Ctrl+C to stop&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Web server is available at http:&#x2F;&#x2F;127.0.0.1:1111&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;用瀏覽器開 http:&#x2F;&#x2F;127.0.0.1:1111&#x2F; 應該會看到如下畫面：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;zola&#x2F;2020-05-02-21-38-37-e79a84e89ea2e5b995e693b7e59c96.png&quot; alt=&quot;Welcome to Zola!&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zhuan-an-jie-gou&quot;&gt;專案結構&lt;&#x2F;h2&gt;
&lt;p&gt;新的專案應該會長這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── content&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── sass&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── static&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── templates&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── themes&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;└── config.toml&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;content&#x2F;：專案的內容存放區，大多會用加上 front-matter 的 Markdown 來撰寫。&lt;&#x2F;li&gt;
&lt;li&gt;sass&#x2F;：專案的 SASS 樣式檔存放區，這裡面的樣式檔會被 SASS 處理器編譯過。&lt;&#x2F;li&gt;
&lt;li&gt;static&#x2F;：專案會用到的靜態檔案存放區，放在這裡的檔案不會被 zola 做修改，通常用來放圖檔。&lt;&#x2F;li&gt;
&lt;li&gt;templates&#x2F;：專案的模板檔存放區，專案建置時會把模板檔＋內容檔＋SASS 組裝成 HTML 與相關的 CSS 檔案。&lt;&#x2F;li&gt;
&lt;li&gt;themes&#x2F;：網路上抓的 zola 佈景主題檔的存放區。&lt;&#x2F;li&gt;
&lt;li&gt;config.toml：專案的組態檔，包括初始化問答時做的設定都會在這裡。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;對目前的新專案來說，除了 config.toml 有內容之外，其它幾個資料夾目前都是空的。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;bu-jing-zhu-ti&quot;&gt;佈景主題&lt;&#x2F;h2&gt;
&lt;p&gt;Zola 也有佈景主題，在 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.getzola.org&#x2F;themes&#x2F;&quot;&gt;Zola themes&lt;&#x2F;a&gt;，不過選擇比起火紅的 Hugo 要少得多…。&lt;&#x2F;p&gt;
&lt;p&gt;因為 Zola 的佈景實在是不忍直視，電梯直接向下吧！&lt;&#x2F;p&gt;
&lt;h2 id=&quot;mo-ban-ji-chu-yu-wang-zhan-shou-ye&quot;&gt;模板－基礎與網站首頁&lt;&#x2F;h2&gt;
&lt;p&gt;Zola 用的模板系統是同樣以 Rust 開發的 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;tera.netlify.app&#x2F;&quot;&gt;Tera&lt;&#x2F;a&gt;，和大多數的模板系統一樣，Tera 也是以某個模板為基礎，在基礎模板內保留部份內容區域，讓其它模板填入那個保留的內容區域。&lt;&#x2F;p&gt;
&lt;p&gt;Tera 的語法長得像 Jinja 或 Twig 或 Liquid（其實我覺得模板語言都大同小異），模板內的原始碼也是大多由 HTML 碼構成，只在某些需要插值或邏輯處理的地方才用 Tera 的代碼插入，如果是插值，則以 &lt;code&gt;{{ variable }}&lt;&#x2F;code&gt; 的格式插入；如果是邏輯處理，則以 &lt;code&gt;{% if %}&lt;&#x2F;code&gt;、&lt;code&gt;{% endif %}&lt;&#x2F;code&gt; 的格式插入；如果是註解，則以 &lt;code&gt;{# comment #}&lt;&#x2F;code&gt; 的格式插入。&lt;&#x2F;p&gt;
&lt;p&gt;我們從基礎模板開始，在這裡建一個基礎模板並將它命名為 templates&#x2F;base.html：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;!&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;DOCTYPE&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; html&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;html&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;head&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;meta&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; charset&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;utf-8&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;meta&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;viewport&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; content&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;width=device-width, initial-scale=1&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;title&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;AX.dev&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;title&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;head&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;body&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;section&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;div&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      {% block content %}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      {% endblock %}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;div&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;section&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;body&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;html&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這個 base.html 裡面包含了一個網頁必備的幾個元素，但有點不一樣的地方是 &lt;code&gt;{% block content %}&lt;&#x2F;code&gt; 與 &lt;code&gt;{% endblock %}&lt;&#x2F;code&gt;，這兩個 Tera 語法表示此處設定了一組名為 &lt;code&gt;content&lt;&#x2F;code&gt; 的區塊，而這 &lt;code&gt;content&lt;&#x2F;code&gt; 區塊會由其它模板或內容來做填入。&lt;&#x2F;p&gt;
&lt;p&gt;有了 base.html 這樣的基礎模板，我們就不用在每個模板頁都寫入重複的 &lt;code&gt;&amp;lt;head&amp;gt;&lt;&#x2F;code&gt; 等內容，未來如果要修改 &lt;code&gt;&amp;lt;head&amp;gt;&lt;&#x2F;code&gt; 內的資訊也只要對 base.html 做一次修改即可，透過 Zola 的建置指令它會幫我們把所有有參考到 base.html 模板的頁面都更新一遍。&lt;&#x2F;p&gt;
&lt;p&gt;接下來做第二個模板，放在 templates&#x2F;index.html，顧名思義，這個模板就是我們訪客來到網站的首頁，內如如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{% extends &amp;quot;base.html&amp;quot; %}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{% block content %}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;h1&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  This is my blog made with Zola.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;h1&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{% endblock content %}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這邊同樣也用了 Tera 的語法。首先它宣告了要用 base.html 為基礎做擴展，再來它也定義了 &lt;code&gt;content&lt;&#x2F;code&gt; 區塊以及內容。根據以上的模板，Zola 就會把 index.html 模板加上 base.html 模板，合併建置出網頁的 index.html，此時用瀏覽器開 http:&#x2F;&#x2F;127.0.0.1:1111&#x2F; 應該就可以看到新的 index.html 網頁。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;nei-rong-section-shou-ye&quot;&gt;內容－section 首頁&lt;&#x2F;h2&gt;
&lt;p&gt;截至目前為止 content&#x2F; 還空無一物，我們來增加幾個資料夾和檔案如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;content&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;└── article&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    └── _index.md&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這裡的 article 資料夾可以用任何的字眼替換，譬如說 blog，而裡面的 _index.html 則讓 Zola 知道這個資料夾是一個 section，section 是 Zola 的一種網站組織結構，以一個普通的公司網站為例，除了一般首頁、關於我們等單頁的頁面，會定期上稿的有可能是網誌與新聞稿，這時候就可以分別為它們建立 blog 與 press_release 兩個 section，兩個 section 可以各自指定它們的模板檔與一些其它參數，各自滿足不同的讀者需求。&lt;&#x2F;p&gt;
&lt;p&gt;Zola 的 section 的另外一個特性是 section 會有 section 自己的首頁，通常是放該 section 的子頁清單，可以稱為目錄頁或是 WordPress 稱呼的彙整頁，在這裡我直接稱為 section 首頁。&lt;&#x2F;p&gt;
&lt;p&gt;回到檔案身上，對 content&#x2F;article&#x2F;_index.md 編輯如下內容：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;toml&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FDAEB7;font-style: italic;&quot;&gt;+++&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;title =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;List of articles&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;sort_by =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;date&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;template =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;article.html&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;page_template =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;article-page.html&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FDAEB7;font-style: italic;&quot;&gt;+++&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這個由 &lt;code&gt;+++&lt;&#x2F;code&gt; 包圍起來的區塊，在靜態網站產生器的世界裡稱為 front-matter，front-matter 用於儲存一些在網頁內容以外的設定，靜態網站產生器會讀入這些設定並做出相對的動作。&lt;&#x2F;p&gt;
&lt;p&gt;各家靜態網站產生器吃的 front-matter 格式各異，Zola 吃的是 TOML。在上面的例子裡，我們設定了幾項參數：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;title：該 _index.md 頁面的標題，因為 Markdown 是沒有像 HTML 的 &lt;code&gt;&amp;lt;title&amp;gt;&lt;&#x2F;code&gt; 標籤可以使用的，以及就算是改用 _index.html，我們的模板也已經把 &lt;code&gt;&amp;lt;head&amp;gt;&lt;&#x2F;code&gt; 都定義了，故即便是 _index.html 也是要另外在 front-matter 內設定 title 供 Zola 加工出正確的網頁標題。&lt;&#x2F;li&gt;
&lt;li&gt;sort_by：設定該 section 首頁內要如何對旗下子頁面的呈現做排序。&lt;&#x2F;li&gt;
&lt;li&gt;template：指定該 section 首頁要用的模板。&lt;&#x2F;li&gt;
&lt;li&gt;page_template：指定該 section 之子頁面要用的模板。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;mo-ban-section-shou-ye&quot;&gt;模板－section 首頁&lt;&#x2F;h2&gt;
&lt;p&gt;前面我們指定了 section 首頁的模板，現在就來鼓搗他，在 templates&#x2F;article.html 編輯以下內容：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{% extends &amp;quot;base.html&amp;quot; %}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{% block content %}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;h1&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; class&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;title&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  {{ section.title }}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;h1&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;ul&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  {% for page in section.pages %}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;li&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;a&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; href&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;{{ page.permalink }}&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;{{ page.title }}&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;a&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;li&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  {% endfor %}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;ul&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{% endblock content %}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在這個模板內我們再次以 base.html 模板為基礎做擴展。&lt;&#x2F;p&gt;
&lt;p&gt;模板內的 &lt;code&gt;{{ section.title }}&lt;&#x2F;code&gt; 會被前面提過的 front-matter 的 &lt;code&gt;title&lt;&#x2F;code&gt; 值插入。&lt;&#x2F;p&gt;
&lt;p&gt;後面利用了 &lt;code&gt;{% for %}&lt;&#x2F;code&gt; 迴圈幫我們把 section 內所有子頁面都一一陳列出來。&lt;&#x2F;p&gt;
&lt;p&gt;至此，用瀏覽器開 http:&#x2F;&#x2F;127.0.0.1:1111&#x2F;article&#x2F; 應該會出現這個 section 首頁，但目前尚未有子頁面。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;nei-rong-yu-mo-ban-section-zi-ye&quot;&gt;內容與模板－section 子頁&lt;&#x2F;h2&gt;
&lt;p&gt;目前 article 這個 section 只有首頁，還沒有子內容頁，現在來新增一個 content&#x2F;article&#x2F;article1.md：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;markdown&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;+++&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;title = &amp;quot;My first post&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;date = 2019-11-27&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;+++&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;This is my first article.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這篇文稿本身是 Markdown 的格式，最前面也有 front-matter 設定了標題與日期。&lt;&#x2F;p&gt;
&lt;p&gt;關於日期，是用來給 section 做排序的依據，但並不是上稿與否的參數，Zola 只是靜態網站產生器，並不是 CMS，不具備有預約上稿的能力，如果要控制某篇文稿是否要上，會利用在 front-matter 內設定發布狀態的方式來達成。&lt;&#x2F;p&gt;
&lt;p&gt;有了內容，還需要搭配模板才能組合成一個完整的頁面，依照前面的設定，我們新增 templates&#x2F;article-page.html 這個模板：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{% extends &amp;quot;base.html&amp;quot; %}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{% block content %}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;h1&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  {{ page.title }}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;h1&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;p&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;strong&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;{{ page.date }}&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;strong&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;p&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;p&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;{{ page.content | safe }}&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;p&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{% endblock content %}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這個模板裡面所使用的 Tera 標籤應該是可以很直觀的了解，唯一需要多做說明的大概就是 &lt;code&gt;{{ page.content | safe }}&lt;&#x2F;code&gt; 這塊，&lt;code&gt;page.content&lt;&#x2F;code&gt; 指的是內容檔案內真正的內容區，也就是 front-matter 以下的內容區。&lt;&#x2F;p&gt;
&lt;p&gt;再接再厲，來個 article2.md：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;markdown&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;+++&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;title = &amp;quot;My second post&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;date = 2019-11-28&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;+++&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;This is my second post.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;現在再用瀏覽器開 http:&#x2F;&#x2F;127.0.0.1:1111&#x2F;article&#x2F; 應該就會看到前面剛打的兩篇文稿，並且它們是依照日期做排序的。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jian-zhi-yu-bu-shu&quot;&gt;建置與佈署&lt;&#x2F;h2&gt;
&lt;p&gt;在模板、內容都陸續產出之後，我們用 Zola 幫我們產出所有的 HTML 頁面：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;zola&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; build&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;所有建置完的靜態 HTML 會在專案的 public&#x2F; 內：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;public&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── 404.html&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── article&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│   ├── article1&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│   │   └── index.html&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│   ├── article2&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│   │   └── index.html&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│   └── index.html&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── elasticlunr.min.js&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── index.html&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── robots.txt&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── search_index.en.js&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;└── sitemap.xml&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到，Zola 的建置命令還幫我們做了 robots.txt、sitemap.xml 等一個完整的網站需要的檔案。&lt;&#x2F;p&gt;
&lt;p&gt;透過一些像是 GitLab（或 GitHub 或 Netlify 或 Vercel 或 Cloudflare Workers）這類服務的能力，整個專案可以透過 Git 推送到遠端的 Git repository，再利用遠端服務的 CI&#x2F;CD 能力執行 &lt;code&gt;zola build&lt;&#x2F;code&gt; 與後面的佈署靜態網頁的工作。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;can-kao-zi-liao&quot;&gt;參考資料&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;dictionary.cambridge.org&#x2F;zht&#x2F;%E8%A9%9E%E5%85%B8&#x2F;%E8%8B%B1%E8%AA%9E-%E6%BC%A2%E8%AA%9E-%E7%B9%81%E9%AB%94&#x2F;matter&quot;&gt;matter 在英語-中文（繁體）詞典中的翻譯&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>談 Jira priority 的不足與 FMEA</title>
        <published>2022-01-23T00:00:00+00:00</published>
        <updated>2022-01-23T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/fmea/"/>
        <id>https://editor.leonh.space/2022/fmea/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/fmea/">&lt;p&gt;Jira 是目前市面上最多人用的軟體專案管理系統，Jira 的工作流程是以「&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;zh-tw&#x2F;%E6%95%8F%E6%8D%B7%E8%BD%AF%E4%BB%B6%E5%BC%80%E5%8F%91&quot;&gt;敏捷&lt;&#x2F;a&gt;」為主，所謂的敏捷是一種軟體開發的流派或理論，敏捷的主要精神是快速迭代，在搭建出可用的 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;zh-tw&#x2F;%E6%9C%80%E7%B0%A1%E5%8F%AF%E8%A1%8C%E7%94%A2%E5%93%81&quot;&gt;MVP&lt;&#x2F;a&gt; 後就推出市場，一方面大幅縮短 time to market 時間，一方面藉由 MVP 驗證市場反應與取得用戶回饋，再以這些資訊對 MVP 進行下一輪的改版，如此往復的迭代著。&lt;&#x2F;p&gt;
&lt;p&gt;敏捷的迭代流程其實也是持續改善的精神，與既有的 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;PDCA&quot;&gt;PDCA&lt;&#x2F;a&gt; 相當類似，不過個人認為，PDCA 的 Plan 更追求於建構完完整整的計畫，不具有 MVP 的概念。&lt;&#x2F;p&gt;
&lt;p&gt;回頭說 Jira，Jira 專案是由無數張的工卡構成，可以把工卡理解成便利貼，工卡可以代表一項工作、一個問題、一個 bug、一項測試，或者由它們組成的一個群組。&lt;&#x2F;p&gt;
&lt;p&gt;施工人員（簡稱工人）在處理工卡時，一般都是依照工卡的 priority 來決定施工的順序，因此建立工卡時，除了工卡身的粒度大小與可實現性外，如何適當的認定一張工卡的優先度也對專案施工有著決定性的影響，因為回到敏捷或持續改善的觀點來看，改善需求是無限的，因此工卡也會是無限的，但資源（人力、時間、金錢）卻是有限的，因此低優先度的工卡往往會被累積然後忽略。（除非因為某些新因素的影響而使低優先度的工卡提高優先度，但往往低優先度工卡早就被遺忘在工卡堆內而又另開一張重複的高優先度的工卡，不過這是另外一個話題了。）&lt;&#x2F;p&gt;
&lt;h2 id=&quot;you-xian-du&quot;&gt;優先度&lt;&#x2F;h2&gt;
&lt;p&gt;前面提到施工的順序會依照優先度來排序，然而很少有人認真去探究優先度本身該如何決定，不論是 Jira 的文件或是其它第三方文章都未有深入探究，彷彿優先度的認定是人類天生的本能似的，特別是在軟體產業，自從敏捷被拱上神壇之後幾乎成為不可質疑的標準（關於敏捷與商業流程如何的脫節與不相容又是另一個話題了），Jira 也跟著雞犬升天成為實施敏捷開發的不可質疑的一部分。（Jira 本身的 UX 就是很大的問題，在 UX 當道的時代竟然大家對 Jira 的容忍度這麼高令人訝異。）&lt;&#x2F;p&gt;
&lt;p&gt;再把話題拉回來，一般的團隊會把 Jira 的優先度設成三到五級，看似很直覺，P0 ~ P5，嚴重到輕微，但若深入思考，什麼應該是嚴重？什麼應該是輕微？&lt;&#x2F;p&gt;
&lt;p&gt;試想下面的案例：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;某個日記 app 儲存超過六萬個字 buffer 會爆掉，講嚴重一點好了，手機會爆炸，試問優先度如何？&lt;&#x2F;li&gt;
&lt;li&gt;日記 app 的 Twitter 登入功能壞掉，試問優先度如何？&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;在上面的兩個例子，用不同的觀點看會得到截然不同的優先度，如果用保護生命財產的觀點看，案例一顯然是超嚴重的嚴重；如果是用影響用戶體驗的觀點看，案例一顯然微不足道（誰一篇日記會寫到六萬字？），反而是案例二更加優先。&lt;&#x2F;p&gt;
&lt;p&gt;當然每個人心中都有一把尺，但如何讓優先度的認定盡量站在更一致的基準上，顯然 Jira 沒有考慮到，而且因為 Jira 的不可質疑，也沒人提出過「這樣真的可以嗎？」的疑問。&lt;&#x2F;p&gt;
&lt;p&gt;如何用更嚴謹的基準認定優先度？在此為您介紹 FMEA。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fmea&quot;&gt;FMEA&lt;&#x2F;h2&gt;
&lt;p&gt;在說 FMEA 前先介紹本人以往從事的汽車產業，汽車產業供應鏈以最末端的品牌為主體，品牌（TOYOTA、BMW、Tesla 等）在汽車供應鏈體系稱為中心廠，中心廠決定大部分的零組件規格，並向協力廠下單。&lt;&#x2F;p&gt;
&lt;p&gt;因為汽車必須受到政府特殊的安全監管，也就是汽車如果上市後發現零組件缺陷，政府可以要求中心廠召回維修，一旦發生就會造成巨大的成本支出，如果未召回導致消費者死傷，需要負擔更龐大的集體訴訟賠償，所以中心廠會極力的避免任何有缺陷的零組件進入組裝，具體的手段就是透過各大車廠與學界組織成立的 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;ISO&#x2F;TS_16949&quot;&gt;IATF 16949&lt;&#x2F;a&gt; 管理標準來規範協力廠的一切流程。IATF 16949 可以視為 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;ISO_9000&quot;&gt;ISO 9001&lt;&#x2F;a&gt; 的擴充，它規範的不僅是品質，包括營業、研發、製造、管理、財會都在 IATF 16949 的規範內被管控（其實 ISO 9001 也越管越寬了），並且追朔三層供應鏈，也就是中心廠會稽核到協力廠的供應商的供應商。&lt;&#x2F;p&gt;
&lt;p&gt;為了符合汽車對安全品質的追求，以及避免後續的召回成本，在認定產品問題與與缺陷方面，汽車產業採用了更嚴謹的系統來判定問題的「優先度」，這樣的問題評斷系統就是 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E5%A4%B1%E6%95%88%E6%A8%A1%E5%BC%8F%E4%B8%8E%E5%BD%B1%E5%93%8D%E5%88%86%E6%9E%90&quot;&gt;FMEA&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;FMEA，即 failure mode and effects analysis，中文叫「失效模式與影響分析」，聽起來很高大上，其實就是判定問題優先度的一套規範系統，FMEA 在判定問題的優先度上採用了積分制的 RPN 指標來決定，RPN 即風險優先級數（risk priority numbers），為以下三項參數的乘積（即 RPN = S * O * D）：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Severity，嚴重度，一項功能或特性失效影響的嚴重度。&lt;&#x2F;li&gt;
&lt;li&gt;Occurrence，發生度，一項功能或特性失效發生的頻度。&lt;&#x2F;li&gt;
&lt;li&gt;Detection，檢知度，一項功能或特性失效被檢測出的難度。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在汽車產業 SOD 的給分是有具體的表格可以&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.researchmfg.com&#x2F;2017&#x2F;10&#x2F;fmea-rpn&#x2F;&quot;&gt;參考&lt;&#x2F;a&gt;的，不過因為都是以汽車產業為出發點的描述，就不收錄了。&lt;&#x2F;p&gt;
&lt;p&gt;借鑒汽車產業對於問題優先度的評判系統，我們可以用更加一致的標準看待上面兩個案例：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;案例一：嚴重度 10（很嚴重）、發生度 3（不太會發生）、檢知度 2（buffer 能吃多少應該是已知的並且被定義被納入測試的），RPN = 10 * 3 * 2 = 60。&lt;&#x2F;li&gt;
&lt;li&gt;案例二：嚴重度 2（Twitter 台灣沒人用且可以用其它方式登入）、發生度 6（一按就壞）、檢知度 1（一定會被測到），RPN = 2 * 6 * 1 = 12。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;註：最新版的 FMEA 規範把 RPN 指數取消，改採分級制，可參閱〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.bsigroup.com&#x2F;localfiles&#x2F;zh-tw&#x2F;e-news&#x2F;no191&#x2F;knowledge-of-the-new-fmea-randy-chiang.pdf&quot;&gt;對於新版FMEA所應有的認知&lt;&#x2F;a&gt;〉，不過以 SOD 為基礎來決定優先度還是不變的。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-yu&quot;&gt;結語&lt;&#x2F;h2&gt;
&lt;p&gt;雖然敏捷和 Jira 都被拱上神壇，聽起來又很潮，不過對於優先度的認定標準上，顯然不及陳年的 FMEA 嚴謹與系統化，當然，目前資訊產業是顯學，不可能把這麼潮的敏捷的 Jira 去套那陳年的 FMEA 原則，FMEA 也不是一夜之間就發展出來的，中間也是經歷了幾十年的歷史教訓後才發展出全汽車產業共通的規範，同樣的歷史也大概會重演在軟體產業上，希望 Tesla 能把更嚴謹的管理體系導入我們潮潮的軟體產業。&lt;&#x2F;p&gt;
&lt;p&gt;還有一個在軟體產業令我難以接受的點—在任何在乎品質的製造業都聽過「&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;mymkc.com&#x2F;article&#x2F;content&#x2F;21341&quot;&gt;品質是習慣出來的&lt;&#x2F;a&gt;」，然而在軟體業卻還停留在「品質是檢驗出來的」這樣的落伍思維，這點以後會再專文吐嘈。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>如何理解 Jira 的 Story</title>
        <published>2022-01-21T00:00:00+00:00</published>
        <updated>2022-01-21T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/jira-story/"/>
        <id>https://editor.leonh.space/2022/jira-story/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/jira-story/">&lt;p&gt;雖然 Jira 的頁面載入的又慢，UX 又差勁，導致快要被新崛起的 Linear 幹掉，但在資訊產業 Jira 大概還是 Trello 以外最多人用的專案管理系統了吧？！&lt;&#x2F;p&gt;
&lt;p&gt;在 Jira 的專案如果是開設成 Software 的話，應該會有以下幾種 issue type：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Epic&lt;&#x2F;li&gt;
&lt;li&gt;Story&lt;&#x2F;li&gt;
&lt;li&gt;Task&lt;&#x2F;li&gt;
&lt;li&gt;Subtask&lt;&#x2F;li&gt;
&lt;li&gt;Bug&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;當然它們字面上的意思誰都知道，但是其中在 Jira 內的角色與意義為何，其實一直沒有認真的了解過，這裡試著以我在網路上吸收到以及融合本人觀點的對於這些 Jira issue type 的理解，另外需要特別提前聲明本人不完全懂也不是「敏捷」、「agile」、「scrum」流派的信仰者 XD，因此下文若有對「敏捷」理解不正確的地方敬請見諒。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-ceng-guan-xi&quot;&gt;階層關係&lt;&#x2F;h2&gt;
&lt;p&gt;在 Jira issue type 的階層關係上，越高的層次抽象性越大，也越用於描述一種較廣泛的、不精確的、整體的概念；而越低的層次更貼近單一的功能，或者需求，或者做法。&lt;&#x2F;p&gt;
&lt;p&gt;Jira 的階層，由高至低排序：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;頂天層：Project&lt;&#x2F;li&gt;
&lt;li&gt;第零層：Component&lt;&#x2F;li&gt;
&lt;li&gt;第一層：Epic&lt;&#x2F;li&gt;
&lt;li&gt;第二層：Story &#x2F; Task &#x2F; Bug&lt;&#x2F;li&gt;
&lt;li&gt;第三層：Subtask&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;這邊也把 project 與 component 加進來，雖然它們並非 issue type 的層級，不過在專案層級上的確也具有角色，所以就一併列入了。&lt;&#x2F;p&gt;
&lt;p&gt;根據 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;support.atlassian.com&#x2F;jira-software-cloud&#x2F;docs&#x2F;configure-custom-hierarchy-levels-in-advanced-roadmaps&#x2F;&quot;&gt;Jira 的文件&lt;&#x2F;a&gt;，story 用於表達較小部份的產品需求；epic 用於表達較大部份的用戶案例，一個 epic 可以被切割成數個 story；而 subtask 則是 story 的再下一層，用於當需要把 story 切割成更小的工作細項時使用。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;story&quot;&gt;Story&lt;&#x2F;h2&gt;
&lt;p&gt;在 story 這種對於需求的描述出現以前，從需求轉化為規格的這段過程，只出現於人腦（直接腦補轉換沒有付諸文字），或是做為規格的補述，可是這樣直觀的轉換中會落失需求的原始動機與需求對客戶的價值高低，這些資訊的遺漏會導致開發人員落入「&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;pedia.cloud.edu.tw&#x2F;Entry&#x2F;Detail&#x2F;?title=%E7%9F%A5%E5%85%B6%E7%84%B6%E8%80%8C%E4%B8%8D%E7%9F%A5%E5%85%B6%E6%89%80%E4%BB%A5%E7%84%B6&quot;&gt;知其然而不知其所以然&lt;&#x2F;a&gt;」的狀況，用經典的鞦韆圖可以很直觀的表達這樣的狀況。&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;jira-story&#x2F;1_v_bpdwzomuwvewkzpxdw8a.png&quot; alt=&quot;鞦韆圖&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：網路&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;在 story 這種 issue type 被設計出來以後，終於有地方可以讓需求的原始動機與價值有記載的地方，並且也把需求至規格這段原先腦補的過程以文字記錄下來，目的當然是希望團隊的所有成員在做產品的時候可以理解需求的原始動機與價值，以避免錯誤理解或過度設計的問題發生。如果拿 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E5%85%AD%E4%BD%95%E6%B3%95&quot;&gt;5W1H&lt;&#x2F;a&gt; 對一個 issue 做解構的話，story 會負責用於描述為何（why）會有這個 issue，以及是誰（who）發出這個 issue、issue 的需求是什麼（what）、在何時（when）何地（where）發生，而開發人員如何（how）在技術層面處理這個 issue，則不是建立 story 時所必須填入的內容（至少在 story 標題的地方不是）。&lt;&#x2F;p&gt;
&lt;p&gt;開發人員如果有需要可以另開 task 或 bug 類型的 issue 來記載對開發人員來說實際需要進行的工項，並連結相關的 story 與 task &#x2F; bug 工項。&lt;&#x2F;p&gt;
&lt;p&gt;另外一方面，如果要把相關的 story 整理起來，則利用 epic 讓 story 能被有組織的被歸納起來，也能為專案的諸多需求有分門別類的地方。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;story-de-li-du&quot;&gt;Story 的粒度&lt;&#x2F;h2&gt;
&lt;p&gt;Story 用於描述使用者需求，然而「使用者需求」這件事本身就難以估量，特別是在面對不同的層級的客戶，他們的需求可能會長的相當不一樣，對客戶的大老闆來說，他要一套「幫助公司營運的 ERP 」，然而對客戶的基層員工來說，他要的是「幫我在結帳頁面加個會員資訊區塊好讓我對會員進行個人化服務」，在實際建立 story 時，面對過於廣泛而粗略的需求（上帝的旨意往往就是這麼虛無飄渺），還是必須依賴人力去把這類需求拆解成數條更具體且具有可執行性的項目後再建成 story，這項工作可能落在專案經理或專案分析師上面，對老闆兼撞鐘的小公司來說，就是落在老闆兼專案經理兼開發兼測試的人上面。&lt;&#x2F;p&gt;
&lt;p&gt;另外一種情況也不適合用 story 描述，即過於細節的工項，舉例來說，在一個 POS 系統，顯然結帳頁面載入的微互動不會是客戶在意的點，這種過於細節的部份就會以 subtask 的方式描述，反之，對遊戲來說，UX 流暢感或所謂的沈浸感相當重要，同樣一件事在遊戲專案上就會提昇至 story 甚至更高的層級。&lt;&#x2F;p&gt;
&lt;p&gt;第三種不適合用 story 描述的工項，即過於工程面或規格面的部份，舉凡 API 規格、欄位命名規範、錯誤處理機制、log 檔規劃等等，皆不屬於 story 的範疇，應該用 task 來建立這些工項，有必要的話再用 Jira 的 issue link 的功能讓 story 與 task 相關連起來。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;story-de-biao-ti-ge-shi&quot;&gt;Story 的標題格式&lt;&#x2F;h2&gt;
&lt;p&gt;網路上最常看到的格式：&lt;&#x2F;p&gt;
&lt;p style=&quot;font-size: large;&quot;&gt;「As a  &lt;strong&gt;XXX&lt;&#x2F;strong&gt;, I want &lt;strong&gt;YYY&lt;&#x2F;strong&gt; feature so that &lt;strong&gt;ZZZ&lt;&#x2F;strong&gt;.」
&lt;&#x2F;p&gt;
&lt;p&gt;翻成華文是這樣：&lt;&#x2F;p&gt;
&lt;p style=&quot;font-size: large;&quot;&gt;「身為一位 &lt;strong&gt;誰誰誰&lt;&#x2F;strong&gt;，我需要 &lt;strong&gt;某某某&lt;&#x2F;strong&gt; 功能才能 &lt;strong&gt;這般這般&lt;&#x2F;strong&gt;。」&lt;&#x2F;p&gt;
&lt;p&gt;句子充滿了贅字，應該簡化成這樣：&lt;&#x2F;p&gt;
&lt;p style=&quot;font-size: large;&quot;&gt;「&lt;strong&gt;某某某&lt;&#x2F;strong&gt; 要 &lt;strong&gt;如此如此&lt;&#x2F;strong&gt; 才能 &lt;strong&gt;這般這般&lt;&#x2F;strong&gt;。」&lt;&#x2F;p&gt;
&lt;p&gt;絕對不要把贅字也寫進來，不論是洋文還是華文。&lt;&#x2F;p&gt;
&lt;p&gt;關於翻譯的取捨，推薦一本余光中的《&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.chiuko.com.tw&#x2F;product&#x2F;%E7%BF%BB%E8%AD%AF%E4%B9%83%E5%A4%A7%E9%81%93%EF%BC%8C%E8%AD%AF%E8%80%85%E7%8D%A8%E6%86%94%E6%82%B4%EF%BC%9A%E4%BD%99%E5%85%89%E4%B8%AD%E7%BF%BB%E8%AD%AF%E8%AB%96%E9%9B%86&#x2F;&quot;&gt;翻譯乃大道 譯者獨憔悴&lt;&#x2F;a&gt;》裡面一篇〈虛實之間見功夫〉，余光中不僅是詩人，也是翻譯大家，他的許多翻譯的經驗與觀念都是值得我輩譯者學習的，並且價值不因為時代而消退。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;can-kao-zi-liao&quot;&gt;參考資料&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;http:&#x2F;&#x2F;teddy-chen-tw.blogspot.com&#x2F;2013&#x2F;10&#x2F;blog-post_2.html&quot;&gt;需求的大小&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.slideshare.net&#x2F;yichingc1&#x2F;user-story-127411237&quot;&gt;User Story 的那些人與那些事&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.jianshu.com&#x2F;p&#x2F;6e8cbb9deb04&quot;&gt;Atlassian In Action – Jira之核心配置（二）&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Blender 基礎操作與輸出</title>
        <published>2022-01-15T00:00:00+00:00</published>
        <updated>2022-01-15T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/blender-basic-1/"/>
        <id>https://editor.leonh.space/2022/blender-basic-1/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/blender-basic-1/">&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.blender.org&#x2F;&quot;&gt;Blender&lt;&#x2F;a&gt; 是開源的 3D 創作軟體，它可以做 3D 建模，算圖、剪輯等，因為本人最近又點了這項技能，就寫一下 Blender 的基本操作，以及談輸出格式的話題。&lt;&#x2F;p&gt;
&lt;p&gt;以往我比較熟悉的 3D 繪圖軟體是參數式設計的 SolidWorks、Onshape 等，參數式設計主要應用在需要精確的尺寸與工程標註的機構設計或產品設計上，以及輸出各式工程圖等面向製造的功能，而 Blender 則是藝術創作導向的應用，更重視自由曲面、塑形、光影、動作這些面向，對我來說，以往在 SolidWorks 上的經驗，大概只有三維座標和三維平面的觀念是可以沿用的…。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;blender-de-mian-ban-qie-huan&quot;&gt;Blender 的面板切換&lt;&#x2F;h2&gt;
&lt;p&gt;剛啟動 Blender，會看到下面這個畫面：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;blender-basic-1&#x2F;blender-startup.webp&quot; alt=&quot;Blender 啟動畫面&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;上面有好幾個預先訂好的模式，例如選了「Video Editing」則會進入剪輯模式：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;blender-basic-1&#x2F;blender-video-editing.png&quot; alt=&quot;Blender Video Editing&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;每種模式下都幫我們搭配好了預設的面板（pane），並且這些面板都是可以做切換的，因此即使進入剪輯模組，想要臨時調用別的某個面板，也是可以的，例如我想把檔案瀏覽器切換成「圖表編輯器」，從檔案瀏覽器面板的左上角圖示處就可以開啟面板選單，並切換成圖表編輯器：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;blender-basic-1&#x2F;blender-switch-pane.webp&quot; alt=&quot;Blender 切換面板&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;另外，注意到 Blender 的快速鍵提示，在 Blender 的操作設計上，如同大多數的 3D 繪圖軟體一樣，是需要大量熟悉快速鍵才能有效率的作業的，因此務必要有著「學習 Blender，也要記憶快速鍵」的心態，才能有效率的操作 Blender。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;blender-de-juan-dong-zhou&quot;&gt;Blender 的捲動軸&lt;&#x2F;h2&gt;
&lt;p&gt;Blender 的捲動軸與我們一般認知的捲動軸完全不同，一般的捲動軸只負責捲動與翻頁，而 Blender 的捲動軸，除了捲動與翻頁，還具備縮放的功能。&lt;&#x2F;p&gt;
&lt;p&gt;下圖是一般的捲動軸，我們已經很熟悉了：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;blender-basic-1&#x2F;scroll-bar-152244.svg&quot; alt=&quot;捲動軸&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：&lt;a href=&quot;https:&#x2F;&#x2F;openclipart.org&#x2F;detail&#x2F;96835&#x2F;scroll-bar&quot;&gt;tucho_mendez&lt;&#x2F;a&gt;&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;而這是 Blender 的捲動軸：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;blender-basic-1&#x2F;blender-scrollbar-1.webp&quot; alt=&quot;Blender 捲動軸&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;注意到卷軸的兩端，各有兩個小黑點，拖拉小黑點可以做到縮放的功能，例如在上圖中，Y 軸的上下限分別是 10 到 -10，稍微拖拉位於 Y 軸下方的小黑點就可以把下限範圍改到 -2：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;blender-basic-1&#x2F;blender-scrollbar-2.webp&quot; alt=&quot;Blender 捲動軸&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;當然，在內容區用滑鼠滾輪也可以做縮放，但如果只想獨立的對 Y 軸做縮放，那就非用捲動軸不可了。&lt;&#x2F;p&gt;
&lt;p&gt;這種捲動軸在 Blender 是極為實用的設計，例如在剪輯的多軌作業下，常常需要在較細的尺度與較粗的尺度下交互作業，此時捲動軸就可以相當有效率的替我們切換。&lt;&#x2F;p&gt;
&lt;p&gt;例如有時候得用巨觀的視角安排片段的順序，此時的時間從 0 秒橫跨到 04:30 秒：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;blender-basic-1&#x2F;blender-sequence-editor-1.webp&quot; alt=&quot;Blender 序段編輯器&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;但要對那些細小的片段做調整時，就要切換到微觀的尺度作業，此時的時間縮小到從 00:36 到 01:06，並且 Y 軸比例不變：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;blender-basic-1&#x2F;blender-sequence-editor-2.webp&quot; alt=&quot;Blender 序段編輯器&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;以上是 Blender 的 UX 與 UI 的基礎，而中間的製作與剪輯的過程，我還太菜就不賣弄了，直接跳到最後的輸出的部分。 :p&lt;&#x2F;p&gt;
&lt;h2 id=&quot;blender-de-shu-chu-jie-xi-du-yu-zheng-lu&quot;&gt;Blender 的輸出解析度與幀率&lt;&#x2F;h2&gt;
&lt;p&gt;下面是 Blender 的輸出參數面板：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;blender-basic-1&#x2F;blender-output-1.webp&quot; alt=&quot;Blender 輸出&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在解析度的選擇上，目前最普遍的應該是 1920 * 1080 px，用上它就可以榮耀的貼上 Full HD 的徽章：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;blender-basic-1&#x2F;Full_hd_logo.svg&quot; alt=&quot;Full HD&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：&lt;a href=&quot;https:&#x2F;&#x2F;commons.wikimedia.org&#x2F;wiki&#x2F;File:Full_hd_logo.svg?uselang=zh-tw&quot;&gt;Wikimedia Commons&lt;&#x2F;a&gt;&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;不過時代不同了，現在不上 4K 都不好意思拿出來說嘴…。&lt;&#x2F;p&gt;
&lt;p&gt;然而，要處理 4K 的素材，那得有相當夠的算力和儲存空間，一個超高品質的 15 秒 4K 素材就吃 1.5 GB，並且不論是剪輯或 render 都會吃爆算力，現實上 1920 * 1080 還是最實際的選擇。&lt;&#x2F;p&gt;
&lt;p&gt;下面要考慮的是「幀率」，預設值是 24 FPS，每種幀率都有它出現的歷史因素，在數位時代突破了很多傳統類比的枷鎖，因此不必侷限在 24 FPS 的傳統，採用 30 FPS 是更好的選擇，因為 30 FPS 要換算成秒數較簡單 :p。&lt;&#x2F;p&gt;
&lt;p&gt;上圖中的第二部分是「框幀區間」，圖中是 1 到 250，也就是從第 1 幀輸出到第 250 幀，用 30 FPS 換算下來，會是 8.3 秒多：&lt;&#x2F;p&gt;
&lt;div style=&quot;text-align: center; font-size: x-large; margin-bottom: 1rem;&quot; class=&quot;wide&quot;&gt;
&lt;code&gt;250 frame &#x2F; (30 frame &#x2F; second) ≈ 8.3 second&lt;&#x2F;code&gt;
&lt;&#x2F;div&gt;
&lt;h3 id=&quot;su-cai-de-zheng-lu&quot;&gt;素材的幀率&lt;&#x2F;h3&gt;
&lt;p&gt;要注意的是外部素材的幀率與我們設定的幀率之間的轉換。&lt;&#x2F;p&gt;
&lt;p&gt;若有一個原始為 60 FPS，長度 10 秒的素材，則它的總幀數為 600 幀：&lt;&#x2F;p&gt;
&lt;div style=&quot;text-align: center; font-size: x-large; margin-bottom: 1rem;&quot; class=&quot;wide&quot;&gt;
&lt;code&gt;60 frame &#x2F; second * 10 second = 600 frame&lt;&#x2F;code&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;若這 600 幀的素材，放到一個 30 FPS 的 Blender 專案中會變成 20 秒：&lt;&#x2F;p&gt;
&lt;div style=&quot;text-align: center; font-size: x-large; margin-bottom: 1rem;&quot; class=&quot;wide&quot;&gt;
&lt;code&gt;600 frame &#x2F; (30 frame &#x2F; second) = 20 second&lt;&#x2F;code&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;具體的感受就是變慢動作了，原本一個一秒長的動作變成兩秒才能演完，因此若幀率不同，得在 Blender 內再對素材做速度調整。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;blender-de-shu-chu-ge-shi-yu-bian-ma&quot;&gt;Blender 的輸出格式與編碼&lt;&#x2F;h2&gt;
&lt;p&gt;下面是輸出面板的另外一部分：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;blender-basic-1&#x2F;blender-output-2.webp&quot; alt=&quot;Blender 輸出&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;輸出路徑的部分，務必確保路徑是存在的，這樣說好像很多餘，但在跨平台的作業中，Windows 的 D:\output\ 就不存在於 Linux，反之，Linux 的 ~&#x2F;output&#x2F; 也不會存在於 Windows，一旦輸出路徑無效，那 render 是不會有任何畫面的，而且也沒有任何錯誤提示，會令人感到絕望並且懷疑人生，不可不慎。&lt;&#x2F;p&gt;
&lt;p&gt;下面講影片的容器與編碼。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;rong-qi&quot;&gt;容器&lt;&#x2F;h3&gt;
&lt;p&gt;一部影片，除了視訊軌外，可能還有音軌，甚至多國音軌，還有字幕，甚至多國字幕，更完整一點的，還有章節等等，這些視訊軌、音軌、字幕等素材都會封裝在一個所謂的容器檔案內，各種容器格式支援的素材可能不同，普遍來說，多視訊軌、多音軌、多字幕等是基本的。&lt;&#x2F;p&gt;
&lt;p&gt;有的容器與影片的編碼是有搭配的，例如 WebM 容器會搭配 WebM &#x2F; VP9 編碼，但大部分的容器可以自由決定裡面素材的編碼，例如上圖中的 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;Matroska&quot;&gt;Matroska&lt;&#x2F;a&gt;，也就是常見的 .mkv 檔案，這是一種較現代化而且被普遍支援的容器格式，除了 .mkv、.webm 外，.mp4 也是一種很常見的容器格式。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;bian-ma&quot;&gt;編碼&lt;&#x2F;h3&gt;
&lt;p&gt;編碼，指的是對影片或音軌的演算法，編碼決定了影片的品質與體積，我們追求的是高品質但又省空間的編碼，另一方面也要有足夠的普及度。&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;zh-tw&#x2F;H.264&#x2F;MPEG-4_AVC&quot;&gt;H.264&lt;&#x2F;a&gt; 是最被廣泛使用的編碼之一，雖然它有著錯綜複雜的專利問題…，如果擔心潛在的&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;codec&#x2F;&quot;&gt;專利風險&lt;&#x2F;a&gt;，那 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;zh-tw&#x2F;VP9&quot;&gt;VP9&lt;&#x2F;a&gt; 或開源的 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;Theora&quot;&gt;Theora&lt;&#x2F;a&gt; 編碼也是可以考慮的選項。&lt;&#x2F;p&gt;
&lt;p&gt;編碼速度也是要考慮的因素之一，同樣的，H.264 有著廣泛的硬體支援，不論是 AMD、NVIDIA、Intel 都有支援 H.264 的優化，在 render 時可以更快的得到輸出的成品。&lt;&#x2F;p&gt;
&lt;p&gt;如果想得到更好的效能，那請務必買支援 CUDA 的 NVIDIA 卡，其他家的 GPU 運算函式庫不論是普及度或成熟度都不及 CUDA。&lt;&#x2F;p&gt;
&lt;p&gt;在聲音方面，&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;Opus_(%E9%9F%B3%E9%A2%91%E6%A0%BC%E5%BC%8F)&quot;&gt;Opus&lt;&#x2F;a&gt; 有著廣泛的支援度，並且也沒有潛在的專利問題，是不二的選擇，其次是 AAC 與 AC3，然而他們都有潛在的專利費用問題，在壓縮率上也沒有明顯的優勢，沒有特別需求的話不用考慮。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;以上是個人近日摸索 Blender 的一些心得，另外在 Blender 的剪輯方面，也有錄了一片談 Animate Property 和 Graph Editor 的操作：&lt;&#x2F;p&gt;
&lt;iframe src=&quot;https:&#x2F;&#x2F;www.youtube-nocookie.com&#x2F;embed&#x2F;_TT2aCJEIuk&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;除了 Blender 剪輯，上字幕和錄音也是另外專門的工作，有空再分享上字幕與錄音 的一些心得。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>視訊編碼格式</title>
        <published>2022-01-14T00:00:00+00:00</published>
        <updated>2022-01-14T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2022/codec/"/>
        <id>https://editor.leonh.space/2022/codec/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2022/codec/">&lt;!-- 我記得五六年前 DivX、Xvid 都還滿主流的啊～還有就是 RealNetworks 的 RMVB 和 Apple 的 QuickTime，可是時隔多年，現在好像都進化了，主要是因為 HD 視訊的普及化吧，還有就是因應在網路上傳輸的特性，上面幾種格式都邁向 H.264 的技術，原本用Motion JPEG 壓縮的影片，片長 7 分鐘，將近 500 mb，改用 x.264 進行編碼，品質幾乎一樣，檔案大小卻砍了一半不到，只有 222 mb，可是最後又發現 Picasa 相簿最大只能傳 100 mb 以內，缺點又多了一條 =&quot;=。 --&gt;
&lt;!-- 很久以前，那是個視訊編碼百家爭鳴的時代，有老牌的 RM &#x2F; RMVB，曾經我們都堅持過動畫一定要用 RMVB、蘋果的 QuickTime、DivX &#x2F; XviD 等等，那也是個對用戶頗困擾的年代，有各種的編碼，就得要有相對的解碼器，因此那也是個解碼器、播放器百家爭鳴的時代，曾經的[海海軟件全能播放器](https:&#x2F;&#x2F;cn.haihaisoft.com&#x2F;%E6%B5%B7%E6%B5%B7%E8%BD%AF%E4%BB%B6%E5%85%A8%E8%83%BD%E6%92%AD%E6%94%BE%E5%99%A8.aspx)、[Perian](https:&#x2F;&#x2F;www.perian.org&#x2F;) 都只剩下歷史的遺跡，如今時隔多年，現今市場是能打的，只剩下 H.264 家族，以及 Google 的 VP9、開源的 Theora，但仗還沒打完，下一代的爭奪已然確立，MPEG 陣營的 H.265 對決 AOMedia 陣營的 AV1，而這些陣營的存在，始終離不開專利與費用的議題。

一切要從 [MPEG](https:&#x2F;&#x2F;www.mpegstandards.org&#x2F;) 說起，MPEG 全名為 Motion Picture Experts Group，即動態影像專家小組，它是由業界代表組成的組織，負責制定影音廣播、傳輸、壓縮規格的標準，因此由 MPEG 制定的標準都被冠上「MPEG」之名，例如 DVD 影片的視訊編碼標準是 [MPEG-2: Video](https:&#x2F;&#x2F;www.mpegstandards.org&#x2F;standards&#x2F;MPEG-2&#x2F;2&#x2F;)，而高畫質的藍光影片的視訊編碼標準則是 [MPEG-4: Advanced Video Coding](https:&#x2F;&#x2F;www.mpegstandards.org&#x2F;standards&#x2F;MPEG-4&#x2F;10&#x2F;)，這個標準是與另一個標準組織 ITU 聯合開發的，因此又稱為 [ITU-T H.264](https:&#x2F;&#x2F;www.itu.int&#x2F;rec&#x2F;T-REC-H.264-202108-I&#x2F;en)。

編碼標準的演進意味著更高的畫質以及更先進的壓縮技術，而在網路時代，為了更有效率的運用有限的頻寬，同時還要滿足觀眾對畫質的更高要求，以及直播產業興起後，對壓縮速度也有更高的要求，畢竟更快的壓縮意味著更低的延遲，因此我們也需要更新一代的編碼技術來同時滿足高壓縮率、高畫質、以及高壓縮速度的三大需求，也就是要**壓的快又要壓的好** --&gt;
&lt;!-- --- --&gt;
&lt;p&gt;現行主流視訊編碼格式是 H.264，或是它的下一代 H.265，不論是 H.264 或 H.265，這些由 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.mpegstandards.org&#x2F;&quot;&gt;MPEG&lt;&#x2F;a&gt; 組織定義出來的視訊編碼標準框架，細部的實現或優化方法許多都已被大公司申請成專利，並且這些公司為了共享專利的收益，成立所謂的專利池機構，例如 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.mpegla.com&#x2F;&quot;&gt;MPEG LA&lt;&#x2F;a&gt; 就持有眾多的 MPEG 標準相關的專利，特別是目前視訊編碼最普遍被使用的 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.mpegla.com&#x2F;programs&#x2F;avc-h-264&#x2F;&quot;&gt;AVC &#x2F; H.264&lt;&#x2F;a&gt; 的相關專利，MPEG LA 手上持有的專利超過四千筆，任何想製造與 H.264 有關的軟硬體廠商都難以避免的必須向 MPEG LA 購買專利的使用授權。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zhuan-li-chi-ji-gou&quot;&gt;專利池機構&lt;&#x2F;h2&gt;
&lt;p&gt;這些大公司成立專利池機構並把相關的專利授權給它，對大公司來說，他們彼此可以透過專利池實現專利的交叉授權，消除彼此的專利壁壘，而對想使用專利的第三方公司來說，只須向專利池這個單一窗口交涉即可，不必向眾多的專利實際持有者一一交涉，而專利池收到的專利使用費則由背後的公司共享，因此我們可以說，專利池機構的成立降低了專利的使用障礙，也提高了專利的使用效率。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;h-265-de-san-da-zhuan-li-chi&quot;&gt;H.265 的三大專利池&lt;&#x2F;h3&gt;
&lt;p&gt;在 H.264 方面，MPEG LA 是業界唯一的專利池機構，然而到了 H.265 時代，或許是發現專利池機構賺得缽盆滿盈，於是更多的專利池機構成立分食專利大餅，除了 MPEG LA 外，HEVC Advance、Velos Media 相繼成為 H.265 專利池機構，並且還有部份 H.265 相關專利的持有企業並未加入任何一方專利池中，選擇獨自經營專利授權業務，三個專利池的相關公司可以參考下圖：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;codec&#x2F;HEVC.jpg&quot; alt=&quot;HEVC&#x2F;H.265 專利池&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：〈&lt;a href=&quot;https:&#x2F;&#x2F;blog.chiariglione.org&#x2F;a-crisis-the-causes-and-a-solution&#x2F;&quot;&gt;A crisis, the causes and a solution&lt;&#x2F;a&gt;〉&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;如此糾葛當然再次形成了專利壁壘，身為一個想開發 H.265 軟硬體的廠商，必然得花大量的時間去研究這些專利池所持有的專利成份，或者更有可能的一隻羊被剝 3 + N 層皮…，最終的結果當然是廠商不願冒著侵犯專利的風險而寧願停留在 H.264，這也是為什麼 H.265 壓縮率這麼高（同樣畫質下體積只有 H.264 的一半）卻難以成為主流的原因。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;h-264-de-shou-quan-tiao-kuan&quot;&gt;H.264 的授權條款&lt;&#x2F;h2&gt;
&lt;p&gt;回頭談 H.264。MPEG LA 持有的 H.264 專利不只影響 H.264 軟硬體的製造方，也影響著影片的服務商並間接影響著廣大的創作者，也就是 YouTuber 們。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;h-264-de-ruan-ying-ti-zhi-zao-fang&quot;&gt;H.264 的軟硬體製造方&lt;&#x2F;h3&gt;
&lt;p&gt;在 MPEG LA 將 H.264 的授權分為四種授權模式，前兩種針對的是含有 H.264 專利的軟硬體產品：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;codec&#x2F;avcweb-10.png&quot; alt=&quot;AVC&#x2F;H.264 License Terms Codec Manufacture and Sale&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：MPEG LA&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;ul&gt;
&lt;li&gt;銷售含有 H.264 專利的軟硬體產品予終端用戶，且該產品並非 OS 內建元件者，此類每年十萬個使用單位內免授權費，超過依層級收費。&lt;&#x2F;li&gt;
&lt;li&gt;銷售含有 H.264 專利的軟硬體產品，且內建於 OS 者，此類廠商須為客戶繳交授權費，同樣的每年十萬個使用單位內免授權費，超過依層級收費。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;這裡的第一種，例如 Adobe Media Encoder，它可以把 Adobe Premiere 的專案輸出成 H.264 影片，並且不是 OS 的一部分，因此我們買的 Adobe Premiere 的錢有一部分得貢獻給 MPEG LA 做為專利的使用費。&lt;&#x2F;p&gt;
&lt;p&gt;而第二種，則是各大 OS。當然，這些 OS 大廠，Apple、Google、Microsoft，本身也都是 MPEG LA 的成員，做為內部成員，他們或許有專門的授權方案。&lt;&#x2F;p&gt;
&lt;p&gt;簡而言之，H.264 在十萬門檻內是不用付費的。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ruan-ti-shang-de-h-264-zhuan-li-wen-ti&quot;&gt;軟體商的 H.264 專利問題&lt;&#x2F;h3&gt;
&lt;p&gt;如果你是個軟體開發者，應該知道萬能的 FFmpeg，如果在自己的 app 內使用 FFmpeg 的 H.264 相關編解碼工具是否有專利風險呢？&lt;&#x2F;p&gt;
&lt;p&gt;根據 FFmpeg 的說法，他們的程式並未參考任何專利書上的方法實做，也就是有經過 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E5%87%80%E5%AE%A4%E8%AE%BE%E8%AE%A1&quot;&gt;clean room&lt;&#x2F;a&gt;，然而這並不代表他們沒有無意間採用了與某專利相同的方法，FFmpeg 自己也提到他們不是律師，無法進行專業的專利檢查與規避：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;codec&#x2F;does-FFmpeg-use-patented-algorithms.webp&quot; alt=&quot;Does FFmpeg use patented algorithms?&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;最後 FFmpeg 給出的說明是：專利是個複雜的制度，並且各國之間也存在著法規上的差異，難以用一句話回答使用 FFmpeg 是否會踩到 H.264 專利的問題，如果你的 app 又不開源又收費，而且知名度夠大，那或許某天 MPEG LA 會認為你需要付費。這段話有一部分是我自己的解讀，FFmpeg 原文如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;codec&#x2F;www.ffmpeg.org_legal.html.png&quot; alt=&quot;FFmpeg Patent Mini-FAQ&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;h-264-ying-pian-ping-tai-ji-chuang-zuo-zhe&quot;&gt;H.264 影片平台及創作者&lt;&#x2F;h3&gt;
&lt;p&gt;MPEG LA 的另外兩種 H.264 授權模式則是針對影片的服務商以及製作方：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;codec&#x2F;avcweb-11.png&quot; alt=&quot;AVC&#x2F;H.264 License Terms Codec Participation Fees&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：MPEG LA&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;只要影片是以 H.264 編碼，並且有用到 MPEG LA 相關的專利：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;如果影片是付費觀看者
&lt;ul&gt;
&lt;li&gt;如為訂閱制，則每年訂閱人數十萬以內者，免費，超過者依層級收費。&lt;&#x2F;li&gt;
&lt;li&gt;如為單片租購制，則片長 12 分鐘內免費，片長 12 分鐘以上須收費。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;如果影片是免費觀看，平台或製作方不從影片賺錢，而從廣告或其他管道賺錢者
&lt;ul&gt;
&lt;li&gt;如為免付費電視，則收取一次放映授權費或依收視量收費。&lt;&#x2F;li&gt;
&lt;li&gt;如為網路影片，且非單片租購制或訂閱制者，皆為免費。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;訂閱制的代表就像是 Netflix，而 YouTube 的電影租購服務，則屬於單片租購制：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;codec&#x2F;youtube-movie.png&quot; alt=&quot;YouTube 電影&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;youtuber-de-h-264-zhuan-li-wen-ti&quot;&gt;YouTuber 的 H.264 專利問題&lt;&#x2F;h3&gt;
&lt;p&gt;綜觀以上，身為一位屬於最後一類的 YouTuber，你大概不用擔心誤採專利的風險，即使你成為大手 YouTuber，開啟付費會員專屬影片，那相關的授權費用應該也是 YouTube 會處理，畢竟 YouTube 也是抽了分潤，所以在 YouTube 開頻道是不用擔心 H.264 的專利問題的。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;da-po-mpeg-de-zhuan-li-gao-qiang&quot;&gt;打破 MPEG 的專利高牆&lt;&#x2F;h2&gt;
&lt;p&gt;回顧前面一大段 H.264 的各種花式專利收費，儘管其中有某些免費的方案，但那並非真正意義上的免費，更像是一種養套殺的作法，再看到那 H.265 三圈專利池再次形成了專利壁壘，最後的結果就是技術一流，但想用上它，得交三次以上的專利保護費。&lt;&#x2F;p&gt;
&lt;p&gt;由於有著以上種種專利引發的問題，一個非營利機構 Xiph.Org 試圖自行開發完全沒有專利壁壘與專利風險的視訊編碼格式，經過了幾次的迭代，推出了 Theora 編碼器，做為與 H.264 對比的技術，儘管支援廣泛，然而缺乏商業上的推廣，在使用率上始終不及 H.264，另一方面 Google 也延續收購而來的編碼技術推出同樣免專利費的 VP9 做為與 H.264 對比的技術，並大量的用在自家的 YouTube。&lt;&#x2F;p&gt;
&lt;p&gt;隨著 H.264 往下一代技術 H.265 的演進，免專利陣營也集結成 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;aomedia.org&#x2F;&quot;&gt;Alliance for Open Media&lt;&#x2F;a&gt;，結合此前 Theora 與 VP9 的技術，開發新一代的編碼技術 AV1：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;codec&#x2F;AV1485x485.png&quot; alt=&quot;AV1&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：&lt;a href=&quot;https:&#x2F;&#x2F;aomedia.org&#x2F;about&#x2F;&quot;&gt;Alliance for Open Media&lt;&#x2F;a&gt;&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;由上圖可看出 AV1 的目標很明確：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;全生態系免權利金&lt;&#x2F;li&gt;
&lt;li&gt;完全迴避既有專利&lt;&#x2F;li&gt;
&lt;li&gt;最新技術&lt;&#x2F;li&gt;
&lt;li&gt;開源協作&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;並且和原本的 Xiph.Org 相比，這次的陣容壯盛多了：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;codec&#x2F;aomedia.org_membership_members_.png&quot; alt=&quot;AOMedia Members&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;有掌握影音平台的 Amazon、Apple、Facebook、Google、Netflix、bilibili、Hulu、Vimeo。&lt;&#x2F;li&gt;
&lt;li&gt;有掌握 OS 和瀏覽器生態的 Apple、Google、Microsoft、Mozilla。&lt;&#x2F;li&gt;
&lt;li&gt;有開發晶片的 Apple、ARM、Intel、NVIDIA、AMD、Broadcom、Realtek、Xilinx。&lt;&#x2F;li&gt;
&lt;li&gt;有做軟體應用的 Apple、Adobe、VideoLAN 等等。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;同樣的，由 Google 主導的 AV1 目前最大的採用方也是自家的 YouTube。&lt;&#x2F;p&gt;
&lt;p&gt;遺憾的是，目前只有那萬能的 FFmpeg 支援 AV1 編碼輸出，而更為貼近大眾的 Adobe Media Encoder 與 Apple Compressor 都尚未支援。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-yu&quot;&gt;結語&lt;&#x2F;h2&gt;
&lt;p&gt;對影音創作者來說，專利不專利好像是與己無關的話題，現實上的感受的確也是如此，因此純就使用上的方便性與編碼性能來看，當前 VP9 與 H.264 都是很好的選擇，而如果你的軟體有支援的話，AV1 或 H.265 無疑是更好的選擇。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Google Ads 認證筆記</title>
        <published>2021-12-29T00:00:00+00:00</published>
        <updated>2021-12-29T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/google-ads-certification-notes/"/>
        <id>https://editor.leonh.space/2021/google-ads-certification-notes/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/google-ads-certification-notes/">&lt;p&gt;最近因為業務上的機緣，有需要深入了解 Google Ads 與數位行銷的相關知識，Google 有經營一個官方的教材平台「&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;skillshop.withgoogle.com&#x2F;intl&#x2F;zh-TW_ALL&#x2F;&quot;&gt;好學堂 Skillshop&lt;&#x2F;a&gt;」，好學堂除了做為教學平台外，也是 Google Ads 的認證的考試平台，取得 Google Ads 認證除了可以自我考核對 Google Ads 的熟悉程度外，如果公司想成為 Google Partner，那麼員工是否擁有 Google Ads 認證也是必須滿足的要素之一：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;google-ads-certification-notes&#x2F;google_partner.png&quot; alt=&quot;Google Partner 取得條件總覽&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：Google&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;滿足這些條件的試煉，才有可能拿到那 Google Partner 徽章：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;google-ads-certification-notes&#x2F;google_partner_badge.png&quot; alt=&quot;Google Partner 徽章&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：Google&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;下面是我們整理的重點筆記。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zhong-dian-bi-ji&quot;&gt;重點筆記&lt;&#x2F;h2&gt;
&lt;p&gt;筆記的部分，是以我自己認定的主題分類，並不依照 Google Ads 廣告活動類型（搜尋、多媒體、應用程式、購物等）下去分類，因為某些教材內的某些單元的知識是貫穿全部類型的（例如出價策略），所以這份筆記會以我自己設定的主題分類，如果看得有點亂，請善用瀏覽器的搜尋來找到相對應的主題。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;sou-xun-guang-gao&quot;&gt;搜尋廣告&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;guan-jian-zi-bi-dui-kong-zhi&quot;&gt;關鍵字比對控制&lt;&#x2F;h4&gt;
&lt;p&gt;關鍵字有四種比對模式，從最寬鬆廣義的比對模式到最精確嚴格的比對模式，讓我們得以控制行銷活動的觸及對象。&lt;&#x2F;p&gt;
&lt;p&gt;以下從最廣泛到最嚴格的比對模式排列解釋各個模式的作用：&lt;&#x2F;p&gt;
&lt;dl&gt;
&lt;dt&gt;廣泛比對&lt;&#x2F;dt&gt;
&lt;dd&gt;
&lt;p&gt;最普通也最廣泛的比對方式。&lt;&#x2F;p&gt;
&lt;p&gt;例：&lt;strong&gt;單車 藍色&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
訪客搜尋「&lt;strong&gt;單車 藍色&lt;&#x2F;strong&gt;」、「&lt;strong&gt;腳踏車 藍&lt;&#x2F;strong&gt;」、「&lt;strong&gt;自行車 藍色&lt;&#x2F;strong&gt;」等兩個關鍵字的同義字都會顯示廣告，但若是訪客只搜尋「&lt;strong&gt;單車&lt;&#x2F;strong&gt;」也有可能顯示廣告，而訪客搜尋「&lt;strong&gt;藍色&lt;&#x2F;strong&gt;」則可能&lt;strong&gt;不會&lt;&#x2F;strong&gt;顯示廣告，這其中的邏輯已經不能用傳統的交集或聯集的邏輯來解釋，Google 現在是用自然語意分析技術來決定關鍵字廣告的出現與否，上面兩個例子「&lt;strong&gt;單車&lt;&#x2F;strong&gt;」搜尋的目的性與我們的廣告關鍵字「&lt;strong&gt;單車 藍色&lt;&#x2F;strong&gt;」有更高的語意相關性，因此有較大的機率也會顯示廣告，訪客若是單獨的搜尋「&lt;strong&gt;藍色&lt;&#x2F;strong&gt;」則過於廣泛，因此不會顯示我們的廣告。&lt;&#x2F;p&gt;
&lt;&#x2F;dd&gt;
&lt;dt&gt;廣泛比對修飾符&lt;&#x2F;dt&gt;
&lt;dd&gt;
&lt;p&gt;以「&lt;code&gt;+&lt;&#x2F;code&gt;」號定義的關鍵字（或同義字）必須出現在訪客的搜尋字內。&lt;&#x2F;p&gt;
&lt;p&gt;例：&lt;strong&gt;單車 +藍色&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
訪客搜尋「&lt;strong&gt;單車&lt;&#x2F;strong&gt;」（或同義字）不會顯示廣告，訪客必須搜尋「&lt;strong&gt;單車 藍&lt;&#x2F;strong&gt;」或「&lt;strong&gt;單車 藍色&lt;&#x2F;strong&gt;」或「&lt;strong&gt;腳踏車 藍&lt;&#x2F;strong&gt;」才會出現廣告，也就是一定要有「&lt;strong&gt;藍&lt;&#x2F;strong&gt;」或它的同義字才會出現廣告。&lt;&#x2F;p&gt;
&lt;p&gt;和廣泛比對的差異在於，廣泛比對的例子裡，只搜尋「&lt;strong&gt;單車&lt;&#x2F;strong&gt;」是有可能出現廣告的。&lt;&#x2F;p&gt;
&lt;&#x2F;dd&gt;
&lt;dt&gt;詞組比對&lt;&#x2F;dt&gt;
&lt;dd&gt;
&lt;p&gt;以英文雙引號「&lt;code&gt;&quot;&quot;&lt;&#x2F;code&gt;」包住的關鍵字（或同義字）必須出現在訪客的搜尋字內，並且整句詞組都必須相同。&lt;&#x2F;p&gt;
&lt;p&gt;例：&lt;strong&gt;單車 “碟煞總成”&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
訪客搜尋「&lt;strong&gt;便宜的單車煞車總成&lt;&#x2F;strong&gt;」會出現我們的廣告，因為語意上同時符合了「&lt;strong&gt;單車&lt;&#x2F;strong&gt;」與「&lt;strong&gt;碟煞總成&lt;&#x2F;strong&gt;」；而訪客搜尋「&lt;strong&gt;單車 碟煞&lt;&#x2F;strong&gt;」則不會顯示廣告，因為我們限定了一個詞組「&lt;strong&gt;碟煞總成&lt;&#x2F;strong&gt;」，訪客少了&lt;strong&gt;總成&lt;&#x2F;strong&gt;兩字，但即使是詞組比對，Google 還是會以語意分析同義字，即訪客搜尋「&lt;strong&gt;單車 煞車總成&lt;&#x2F;strong&gt;」還是&lt;strong&gt;有可能&lt;&#x2F;strong&gt;顯示出我們的廣告，這取決於語意分析後的關聯性有多高。&lt;&#x2F;p&gt;
&lt;&#x2F;dd&gt;
&lt;dt&gt;完全比對&lt;&#x2F;dt&gt;
&lt;dd&gt;
&lt;p&gt;以英文方括號「&lt;code&gt;[]&lt;&#x2F;code&gt;」包住的關鍵字（或同義字）必須出現在訪客的搜尋字內，整句詞組都必須相同，並且訪客的搜尋字內不可有其他詞組。&lt;&#x2F;p&gt;
&lt;p&gt;例：&lt;strong&gt;單車 [碟煞總成]&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
訪客搜尋「&lt;strong&gt;便宜的單車碟煞總成&lt;&#x2F;strong&gt;」&lt;strong&gt;不會&lt;&#x2F;strong&gt;出現我們的廣告，因為多了&lt;strong&gt;便宜&lt;&#x2F;strong&gt;的語意，一定要是「&lt;strong&gt;單車的碟煞總成&lt;&#x2F;strong&gt;」或「&lt;strong&gt;單車 碟煞總成&lt;&#x2F;strong&gt;」這類語意完全一致的搜尋才會出現我們的廣告。相較於詞組比對，完全比對模式的語意比對更加嚴格，但一樣的，如果是純粹的同義字或變體字，一樣是會落入比對的，前面的例子中，訪客用自行車、腳踏車代替單車一樣會顯示出廣告，但如果是「&lt;strong&gt;碟煞總成&lt;&#x2F;strong&gt;」被換成「&lt;strong&gt;煞車總成&lt;&#x2F;strong&gt;」這種模擬兩可的狀況會不會出現廣告則還是要取決於當下 Google 語意分析後相關性多高來決定。&lt;&#x2F;p&gt;
&lt;&#x2F;dd&gt;
&lt;&#x2F;dl&gt;
&lt;p&gt;Google 自己的教材也有解釋四種比對模式的規則：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;google-ads-certification-notes&#x2F;google_ads_matching_mode.png&quot; alt=&quot;Google Ads 比對類型&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：Google&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;哪個比較好理解就看個人了。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;chu-jia-ce-lue&quot;&gt;出價策略&lt;&#x2F;h3&gt;
&lt;div class=&quot;wide&quot;&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;出價策略&lt;&#x2F;th&gt;&lt;th&gt;目標&lt;&#x2F;th&gt;&lt;th&gt;說明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;目標曝光比重&lt;br&gt;Target impression share&lt;&#x2F;td&gt;&lt;td&gt;可見度&lt;&#x2F;td&gt;&lt;td&gt;這項策略可確保你的廣告在搜尋結果網頁的特定位置上（首頁上的任何位置、首頁頂端，或絕對首頁頂端），&lt;br&gt;達到特定的曝光比重門檻。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;盡量爭取點擊&lt;br&gt;Maximize clicks&lt;&#x2F;td&gt;&lt;td&gt;點擊次數&lt;&#x2F;td&gt;&lt;td&gt;按照你選擇的目標支出金額設定出價，儘可能為你爭取最多點擊次數。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;盡量爭取轉換&lt;br&gt;Maximize coversions&lt;&#x2F;td&gt;&lt;td&gt;轉換&lt;&#x2F;td&gt;&lt;td&gt;在你的預算內儘可能爭取最多轉換量。你不用設定特定的單次點擊出價（CPC）、單次客戶開發出價（CPA），&lt;br&gt;或廣告投資報酬率（ROAS）目標。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;目標單次客戶開發出價&lt;br&gt;Target cost-per-accquisition&lt;&#x2F;td&gt;&lt;td&gt;轉換&lt;&#x2F;td&gt;&lt;td&gt;這個策略會自動設定出價，協助你爭取更多轉換，並達成平均單次客戶開發出價目標。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;成本效益管理系統&lt;br&gt;Enhanced cost-per-click&lt;&#x2F;td&gt;&lt;td&gt;轉換&lt;&#x2F;td&gt;&lt;td&gt;這個策略會根據每次點擊促成轉換的可能性，自動調整你的手動出價金額。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;目標廣告投資報酬率&lt;br&gt;Target return on ad spend&lt;&#x2F;td&gt;&lt;td&gt;收益&lt;&#x2F;td&gt;&lt;td&gt;根據你設定的目標廣告投資報酬率自動設定出價，儘可能提升轉換價值。&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;成本效益管理系統、目標單次客戶開發出價、盡量爭取轉換，以及目標廣告投資報酬率都屬於 Google Ads 智慧出價，這些轉換出價策略會運用專屬的信號組合在競價期間出價。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;services.google.com&#x2F;fh&#x2F;files&#x2F;misc&#x2F;external_display_smart_bidding_guide_2018_zh_tw.pdf&quot;&gt;多媒體廣告智慧出價指南&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;digimkt.com.tw&#x2F;digital_marketing&#x2F;google-ads-%E5%87%BA%E5%83%B9&#x2F;&quot;&gt;7 個你必須知道的 Google Ads 出價策略&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;darren-learn.com&#x2F;google%E5%87%BA%E5%83%B9%E7%AD%96%E7%95%A5&#x2F;&quot;&gt;Google Ads出價策略大解析，正確出價節省 50% 廣告費用！&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;「目標單次客戶開發出價」、「目標廣告投資報酬率」、「盡量爭取轉換」和「成本效益管理系統」都是智慧出價策略。以上順序是按自動化程度由高到低排列，如果以操控程度來看，就是由低到高。其他沒有使用智慧出價的自動出價策略包含「盡量爭取點擊」和「目標曝光比重」。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;cheng-xiao-gui-hua&quot;&gt;成效規劃&lt;&#x2F;h3&gt;
&lt;p&gt;成效規劃工具是全新的預測工具，透過機器學習預測你 Google Ads 廣告活動的潛力。有了這個工具，你就能針對目前廣告活動日後的每月預算、每季預算和每年預算，進行更深入的預測，同時還能提升投資報酬率。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;cheng-xiao-gui-hua-gong-ju-ru-he-yun-zuo&quot;&gt;成效規劃工具如何運作？&lt;&#x2F;h4&gt;
&lt;p&gt;成效規劃工具會針對你的各個廣告活動決定最佳出價，以及平均每日預算的分配方式，有助你在日後各種支出情況下提升轉換量。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;cheng-xiao-gui-hua-gong-ju-ru-he-yu-ce-guang-gao-huo-dong-cheng-xiao&quot;&gt;成效規劃工具如何預測廣告活動成效？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;預測&lt;&#x2F;li&gt;
&lt;li&gt;模擬&lt;&#x2F;li&gt;
&lt;li&gt;機器學習&lt;&#x2F;li&gt;
&lt;li&gt;驗證&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;cheng-xiao-gui-hua-gong-ju-hui-jian-yi-jin-xing-shen-mo-bian-geng&quot;&gt;成效規劃工具會建議進行什麼變更？&lt;&#x2F;h4&gt;
&lt;dl&gt;
&lt;dt&gt;搜尋「手動單次點擊出價」或「成本效益管理系統」廣告活動&lt;&#x2F;dt&gt;
&lt;dd&gt;
&lt;p&gt;建議的平均每日預算和廣告活動出價比例（1.5 表示將出價提高 50%）&lt;&#x2F;p&gt;
&lt;&#x2F;dd&gt;
&lt;dt&gt;搜尋「盡量爭取點擊」或「盡量爭取轉換」的廣告活動&lt;&#x2F;dt&gt;
&lt;dd&gt;
&lt;p&gt;建議的平均每日預算&lt;&#x2F;p&gt;
&lt;&#x2F;dd&gt;
&lt;dt&gt;搜尋「目標單次客戶開發出價」或「目標廣告投資報酬率」（ROAS）廣告活動&lt;&#x2F;dt&gt;
&lt;dd&gt;
&lt;p&gt;建議的平均每日預算、廣告活動層級的目標單次客戶開發出價，或是廣告活動層級的目標廣告投資報酬率&lt;&#x2F;p&gt;
&lt;&#x2F;dd&gt;
&lt;&#x2F;dl&gt;
&lt;h3 id=&quot;ping-gu&quot;&gt;評估&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;ye-wu-mu-biao-zhu-yao-cheng-xiao-zhi-biao-kpi&quot;&gt;業務目標（主要成效指標（KPI））&lt;&#x2F;h4&gt;
&lt;dl&gt;
&lt;dt&gt;業務標的&lt;&#x2F;dt&gt;
&lt;dd&gt;
&lt;p&gt;公司整體的頂層目標。這類目標主要著眼於提升收益、利潤和銷售量（例如商品售出數量）。大型公司通常會針對各項業務建立子目標。&lt;&#x2F;p&gt;
&lt;&#x2F;dd&gt;
&lt;dt&gt;行銷標的&lt;&#x2F;dt&gt;
&lt;dd&gt;
&lt;p&gt;與業務標的相輔相成。一些小型公司可能會受限於規模而無法訂定如此精細的目標。&lt;&#x2F;p&gt;
&lt;&#x2F;dd&gt;
&lt;dt&gt;媒體標的&lt;&#x2F;dt&gt;
&lt;dd&gt;
&lt;p&gt;是指要透過各個管道達成行銷標的所須完成的目標（例如 YouTube 廣告活動的目標）。&lt;&#x2F;p&gt;
&lt;&#x2F;dd&gt;
&lt;dt&gt;廣告活動指標&lt;&#x2F;dt&gt;
&lt;dd&gt;
&lt;p&gt;用來評估媒體標的成效的各項指標。&lt;&#x2F;p&gt;
&lt;&#x2F;dd&gt;
&lt;&#x2F;dl&gt;
&lt;h4 id=&quot;yi-ci-yi-ge-biao-de&quot;&gt;一次一個標的&lt;&#x2F;h4&gt;
&lt;p&gt;最佳做法是為客戶流程的每個階段各選一個標的。在各個階段都以達成特定預期結果為目標。同時，選擇單一標的有助於更明確地判定廣告活動成敗。如果設定兩項標的，其一成效良好，另一項成效不彰，可能會讓你無法正確評估廣告的整體成效。&lt;&#x2F;p&gt;
&lt;p&gt;下列是可用來設定行銷標的的指標。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;知名度
&lt;ul&gt;
&lt;li&gt;品牌知名度&lt;&#x2F;li&gt;
&lt;li&gt;品牌記憶&lt;&#x2F;li&gt;
&lt;li&gt;品牌首選&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;考慮度
&lt;ul&gt;
&lt;li&gt;品牌好感度&lt;&#x2F;li&gt;
&lt;li&gt;品牌偏好&lt;&#x2F;li&gt;
&lt;li&gt;品牌印象&lt;&#x2F;li&gt;
&lt;li&gt;品牌喜好程度&lt;&#x2F;li&gt;
&lt;li&gt;購買意願&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;購買
&lt;ul&gt;
&lt;li&gt;新客戶&lt;&#x2F;li&gt;
&lt;li&gt;交易&lt;&#x2F;li&gt;
&lt;li&gt;平均訂單量&lt;&#x2F;li&gt;
&lt;li&gt;投資報酬率&lt;&#x2F;li&gt;
&lt;li&gt;廣告投資報酬率&lt;&#x2F;li&gt;
&lt;li&gt;銷售成本&lt;&#x2F;li&gt;
&lt;li&gt;單次操作出價&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;忠誠度
&lt;ul&gt;
&lt;li&gt;回購&lt;&#x2F;li&gt;
&lt;li&gt;向上銷售&lt;&#x2F;li&gt;
&lt;li&gt;客戶推薦&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;ti-sheng-xing-xiao-cheng-xiao-de-qi-da-bu-zou&quot;&gt;提升行銷成效的七大步驟&lt;&#x2F;h4&gt;
&lt;ol&gt;
&lt;li&gt;分別為客戶流程的各個階段選擇一個行銷目標。&lt;&#x2F;li&gt;
&lt;li&gt;針對各個客群研擬媒體組合策略。&lt;&#x2F;li&gt;
&lt;li&gt;決定最合適的評估工具來計算投資報酬率。&lt;&#x2F;li&gt;
&lt;li&gt;訂定可以透過工具評估的目標數字。&lt;&#x2F;li&gt;
&lt;li&gt;記錄評估結果。&lt;&#x2F;li&gt;
&lt;li&gt;將各媒體管道的指標變化與先前的評估資料（或業界基準資料）做比較。&lt;&#x2F;li&gt;
&lt;li&gt;針對各項結果判定應採取的行動。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h4 id=&quot;mu-biao-yu-cheng-guo&quot;&gt;目標與成果&lt;&#x2F;h4&gt;
&lt;p&gt;從下方的示例中，你可以看到品牌意識分數的預期升幅（或增幅）為 10 分，但實際結果則增加了 12 分。這就表示廣告活動確實奏效！&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;google-ads-certification-notes&#x2F;target_and_performance.png&quot; alt=&quot;目標與成果&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：Google&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h4 id=&quot;pin-pai&quot;&gt;品牌&lt;&#x2F;h4&gt;
&lt;p&gt;&lt;strong&gt;品牌指的是什麼？&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
值得一提的是，品牌這個詞彙很容易造成混淆。某些人認為品牌就是標誌。不過在本課程中，我們指的是使用者對貴公司及產品的觀感。這就是品牌的定義。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;品牌強度&lt;&#x2F;strong&gt;（又稱品牌價值）&lt;br &#x2F;&gt;
為行銷產業用語，指的是知名品牌（如 Sony）具有的價值。背後的概念是說，知名品牌因為品牌認知度高，所以光靠品牌名稱就能帶來比一般品牌更高的收益。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;ke-hu-liu-cheng-yi-lan&quot;&gt;客戶流程一覽&lt;&#x2F;h4&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;確保使用者&lt;strong&gt;注意到&lt;&#x2F;strong&gt;你的商家、產品或服務。&lt;br &#x2F;&gt;
根據使用者的年齡、性別、興趣和行為，接觸所有符合&lt;strong&gt;理想客戶&lt;&#x2F;strong&gt;條件的使用者。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;讓使用者&lt;strong&gt;想到你&lt;&#x2F;strong&gt;的品牌，或對你的品牌產生興趣。&lt;br &#x2F;&gt;
接觸所有符合理想客戶條件的使用者，以及對廣告宣傳的產品或類別&lt;strong&gt;略有興趣&lt;&#x2F;strong&gt;的使用者。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;讓使用者願意付諸&lt;strong&gt;行動&lt;&#x2F;strong&gt;，像是購物。&lt;br &#x2F;&gt;
接觸對廣告品牌或產品展現&lt;strong&gt;高度興趣&lt;&#x2F;strong&gt;的所有使用者。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;讓使用者有充分&lt;strong&gt;理由&lt;&#x2F;strong&gt;添購你的商品，或推薦你的商家。&lt;br &#x2F;&gt;
接觸&lt;strong&gt;現有客戶&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h4 id=&quot;gu-suan-mei-ti-xiao-shou-tou-zi-bao-chou-lu&quot;&gt;估算媒體銷售投資報酬率&lt;&#x2F;h4&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;google-ads-certification-notes&#x2F;media_channel_and_sales_channel.png&quot; alt=&quot;估算媒體銷售投資報酬率&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：Google&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h4 id=&quot;xing-xiao-zu-he-mo-shi-fen-xi-mmm&quot;&gt;行銷組合模式分析（mmm）&lt;&#x2F;h4&gt;
&lt;p&gt;對於將多數行銷預算投入離線媒體管道的公司，或是主要透過離線銷售管道（實體店面）販售產品的公司來說，這個模式特別有用。&lt;&#x2F;p&gt;
&lt;p&gt;行銷組合模式分析通常適用於單一品牌（例如奧利奧餅乾），或是同類的一組品牌（例如零食）。這項分析考量了所有能提升品牌銷售量的可能因素，包括（但不限於）媒體和行銷（例如電視、廣播電台、數位平台、戶外廣告、電子郵件、優待券）、品牌考慮度、經濟情況、競爭對手的行動，甚至還有天氣。&lt;&#x2F;p&gt;
&lt;h4 id=&quot;zi-dong-hua&quot;&gt;自動化&lt;&#x2F;h4&gt;
&lt;p&gt;媒體中有許多自動化類型，可自動化的程度也不盡相同，項目包含：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;廣告素材資源&lt;&#x2F;li&gt;
&lt;li&gt;廣告空間（產品動態饋給）&lt;&#x2F;li&gt;
&lt;li&gt;出價&lt;&#x2F;li&gt;
&lt;li&gt;指定目標&lt;&#x2F;li&gt;
&lt;li&gt;刊登位置&lt;&#x2F;li&gt;
&lt;li&gt;評估和歸因分析（自動出價需自動化評估）&lt;&#x2F;li&gt;
&lt;li&gt;報表（資訊主頁、Analytics（分析）的智慧報表）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;讓自動化成功的要素：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;資料品質與資料量（使用者信號、目標對象、再行銷資料，以及整合顧客終身價值計算結果的客戶關係管理資料）&lt;&#x2F;li&gt;
&lt;li&gt;向系統傳達的指令（最好使用以資料為依據的模型，而不是制定規則或假設）&lt;&#x2F;li&gt;
&lt;li&gt;測試和校正&lt;&#x2F;li&gt;
&lt;li&gt;改變做法，從管理媒體改為訓練／指導工具&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;四種簡單的最佳化入門方式：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;自動出價&lt;&#x2F;li&gt;
&lt;li&gt;以數據為準歸因功能&lt;&#x2F;li&gt;
&lt;li&gt;廣告輪播&lt;&#x2F;li&gt;
&lt;li&gt;資訊主頁&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;ce-yan&quot;&gt;測驗&lt;&#x2F;h2&gt;
&lt;p&gt;從這裡開始是認證測驗的題庫，但下面並不包括全部的考題，僅有我認為較模稜兩可的題目，另外因為 Google 也並沒有公佈正確解答，所以下面的答案僅供參考，而答案前面的符號，代表我認為的機率：&lt;&#x2F;p&gt;
&lt;div&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th style=&quot;text-align: center&quot;&gt;圖示&lt;&#x2F;th&gt;&lt;th&gt;機率&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: center&quot;&gt;✔️&lt;&#x2F;td&gt;&lt;td&gt;正確答案的機率 80%&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: center&quot;&gt;❌&lt;&#x2F;td&gt;&lt;td&gt;錯誤答案的機率 90%&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: center&quot;&gt;❓&lt;&#x2F;td&gt;&lt;td&gt;正確／錯誤答案的機率各 50%&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;雖然不像那些數位行銷認證班那麼齊全，不過對有心自學取得認證的同學來說應該是夠用了。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;gou-wu-guang-gao&quot;&gt;購物廣告&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;ben-di-chan-pin-mu-lu-guang-gao-you-shen-mo-xiao-yong&quot;&gt;本地產品目錄廣告有什麼效用？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;✔️提升親臨門市次數&lt;&#x2F;li&gt;
&lt;li&gt;✔️宣傳商家特惠&lt;&#x2F;li&gt;
&lt;li&gt;✔️觸及行動購物者&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;zhi-hui-gou-wu-guang-gao-huo-dong-hui-zi-dong-zhi-xing-xia-lie-na-yi-ge-xiang-mu-jie-ci-jian-hua-guang-gao-huo-dong-guan-li&quot;&gt;智慧購物廣告活動會自動執行下列哪一個項目，藉此簡化廣告活動管理？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;✔️廣告製作&lt;&#x2F;li&gt;
&lt;li&gt;✔️出價&lt;&#x2F;li&gt;
&lt;li&gt;✔️指定目標&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;yi-lin-jing-ying-yi-jia-shu-dian-tui-chu-google-gou-wu-guang-gao-huo-dong-zhi-jin-yi-you-shu-yue-zhi-jiu-ta-xiang-wei-gong-si-zhao-chu-ti-sheng-ye-ji-de-ji-hui&quot;&gt;依琳經營一家書店，推出 Google 購物廣告活動至今已有數月之久。她想為公司找出提升業績的機會。&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;❓她可以利用基準化資料深入分析市場。&lt;&#x2F;li&gt;
&lt;li&gt;✔️❓她可以監控並調整新的產品群組來找出商機。&lt;&#x2F;li&gt;
&lt;li&gt;❓她可以使用曝光比重資料和出價模擬工具。&lt;&#x2F;li&gt;
&lt;li&gt;❓她可以在產品檢視畫面中使用篩選器，取得更精細的報表。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;xiao-jie-shi-fu-shi-lian-suo-dian-de-xing-xiao-zong-jian-ta-yao-zai-gu-ding-de-yu-suan-fan-wei-nei-zai-ta-de-wang-zhan-shang-jin-liang-xi-yin-qian-zai-ke-hu-qing-wen-ta-ying-gai-zai-guang-gao-huo-dong-zhong-cai-yong-na-zhong-zi-dong-chu-jia-ce-lue&quot;&gt;小潔是服飾連鎖店的行銷總監。她要在固定的預算範圍內，在她的網站上盡量吸引潛在客戶。請問她應該在廣告活動中採用哪種自動出價策略？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;✔️❓盡量爭取點擊&lt;&#x2F;li&gt;
&lt;li&gt;✔️❓盡量爭取轉換&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;jin-nian-lai-xiao-fei-zhe-de-gou-wu-xing-wei-chu-xian-shen-mo-bian-hua&quot;&gt;近年來消費者的購物行為出現什麼變化？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;✔️❓消費者購買的品項變少，但頻率上升。&lt;&#x2F;li&gt;
&lt;li&gt;❓消費者購買的品項和頻率都有所減少。&lt;&#x2F;li&gt;
&lt;li&gt;❓消費者購買的品項變多，但頻率下降。&lt;&#x2F;li&gt;
&lt;li&gt;❓消費者購買的品項和頻率都有所增加。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;google-ads-hui-ru-he-xie-zhu-shang-jia-gui-hua-shang-pin-xing-lu-yi-da-dao-guang-gao-mu-biao&quot;&gt;Google Ads 會如何協助商家規劃商品型錄以達到廣告目標？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;❌商家可以根據產品的零售價格規劃產品。這麼一來，商家只會對搜尋特定價格範圍內商品的客戶提供產品資訊。&lt;&#x2F;li&gt;
&lt;li&gt;❓商家可以每週執行商品型錄的成效報表，追蹤有多少產品已銷售到特定客層的市場。&lt;&#x2F;li&gt;
&lt;li&gt;❓商家可以直接在 Google Ads 中瀏覽產品庫存清單，並為他們想要出價刊登的項目建立產品群組。&lt;&#x2F;li&gt;
&lt;li&gt;✔️❓商家可以輸入產品的地區和單次點擊價格偏好設定，藉此自動建立多種購物廣告活動。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;a-shi-xiang-ti-gao-pin-pai-zhi-ming-du-bing-yi-pin-pai-zi-ci-wei-he-xin-lai-da-zao-guang-gao-huo-dong-ta-mei-ban-fa-mei-tian-bo-chu-zu-gou-shi-jian-lai-guan-li-chu-jia-suo-yi-ta-jue-ding-yong-zi-dong-chu-jia-gong-neng-lai-jian-qing-gong-zuo-liang&quot;&gt;阿施想提高品牌知名度，並以品牌字詞為核心來打造廣告活動。他沒辦法每天撥出足夠時間來管理出價，所以他決定用自動出價功能來減輕工作量。&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;❌目標曝光比重&lt;&#x2F;li&gt;
&lt;li&gt;❌盡量爭取轉換&lt;&#x2F;li&gt;
&lt;li&gt;✔️❓成本效益管理系統（eCPC）&lt;&#x2F;li&gt;
&lt;li&gt;❓目標廣告投資報酬率（tROAS）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;guang-gao-ke-hu-yao-ru-he-tou-guo-gou-wu-guang-gao-ti-gao-xiao-shou-liang&quot;&gt;廣告客戶要如何透過購物廣告提高銷售量？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;❌選擇符合網站外觀和風格的色彩配置&lt;&#x2F;li&gt;
&lt;li&gt;❓選擇在搜尋結果網頁的特定位置上刊登廣告&lt;&#x2F;li&gt;
&lt;li&gt;✔️善用 Google 顧客評論並提供商家宣傳資訊&lt;&#x2F;li&gt;
&lt;li&gt;❌依大學學位、職位和主管職務來指定目標購物者&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;zi-dong-chu-jia-dui-yu-google-ads-guang-gao-huo-dong-de-cheng-gong-you-he-gong-xian&quot;&gt;自動出價對於 Google Ads 廣告活動的成功有何貢獻？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;❌當客戶流程變得更加直接，就必須根據一般使用者的行為來設定出價。&lt;&#x2F;li&gt;
&lt;li&gt;❌自動出價會反覆參照資料和情境，可讓你輕鬆直接地掌握使用者意圖，並設定合適的出價。&lt;&#x2F;li&gt;
&lt;li&gt;❓自動出價的演算法整合了一定數量的信號，以此評估使用者意圖。&lt;&#x2F;li&gt;
&lt;li&gt;✔️❓自動出價會運用機器學習技術，透過演算法在各次競價中幫你設定合適的出價。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;shi-yong-zi-dong-chu-jia-wei-he-neng-zao-jiu-chu-se-de-google-ads-guang-gao-huo-dong&quot;&gt;使用自動出價為何能造就出色的 Google Ads 廣告活動？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;❌自動出價會反覆參照資料和情境，可讓你輕鬆直接地掌握使用者意圖，並設定合適的出價。&lt;&#x2F;li&gt;
&lt;li&gt;❌自動出價的演算法整合了一定數量的信號，以此評估使用者意圖。&lt;&#x2F;li&gt;
&lt;li&gt;✔️自動出價會運用機器學習技術，透過演算法在各次競價中幫你設定合適的出價。&lt;&#x2F;li&gt;
&lt;li&gt;❌由於客戶流程越來越簡單直接，因此必須根據一般使用者的行為來出價。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;duo-mei-ti-guang-gao&quot;&gt;多媒體廣告&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;guan-yu-google-display-ads-de-jia-zhi-xia-lie-xu-shu-he-zhe-zheng-que&quot;&gt;關於 Google Display Ads 的價值，下列敘述何者正確？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;❓提供業界最頂尖的自動化和出價功能，讓你在最佳時機觸及數百萬名使用者。&lt;&#x2F;li&gt;
&lt;li&gt;❌讓廣告客戶觸及尋找特定搜尋字詞的客戶。&lt;&#x2F;li&gt;
&lt;li&gt;❓最能讓廣告客戶掌控要放送廣告的地理區域。&lt;&#x2F;li&gt;
&lt;li&gt;✔️❓集結了廣告活動的要素，可觸及網路上深具價值的數百萬名目標對象。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;nai-sheng-jing-ying-yi-ge-fan-shou-dan-che-de-wang-zhan-ta-zhen-dui-zhe-lei-chan-pin-shi-yong-google-ads-duo-mei-ti-guang-gao-huo-dong-lai-ci-ji-mai-qi-bing-xuan-ze-qian-zai-mu-biao-xiao-fei-zhe-zuo-wei-zhi-ding-mu-biao-xuan-xiang-qian-zai-mu-biao-xiao-fei-zhe-neng-wei-nai-sheng-dai-lai-shen-mo-you-shi-bang-zhu-ta-da-cheng-xing-xiao-mu-biao&quot;&gt;乃聖經營一個販售單車的網站。他針對這類產品使用 Google Ads 多媒體廣告活動來刺激買氣，並選擇潛在目標消費者做為指定目標選項。潛在目標消費者能為乃聖帶來什麼優勢，幫助他達成行銷目標？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;❌根據使用者的生活方式、興趣和愛好觸及他們。&lt;&#x2F;li&gt;
&lt;li&gt;❓根據公開資料和推測資料，向使用者放送廣告。&lt;&#x2F;li&gt;
&lt;li&gt;❓找出和原有再行銷名單類似的使用者。&lt;&#x2F;li&gt;
&lt;li&gt;✔️❓接觸對他的產品最感興趣的目標對象。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;shi-yong-zhi-ding-mu-biao-xuan-xiang-ke-rang-google-ads-guang-gao-huo-dong-fa-hui-zui-da-xiao-yi-guan-yu-zhe-dian-xia-lie-xu-shu-he-zhe-zheng-que-qing-xuan-ze-liang-xiang&quot;&gt;使用指定目標選項可讓 Google Ads 廣告活動發揮最大效益。關於這點，下列敘述何者正確？（請選擇兩項）&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;✔️❓指定地區，就能在所選的特定地理位置內放送廣告。&lt;&#x2F;li&gt;
&lt;li&gt;✔️❓指定職業類別，就能只向特定領域的工作者放送廣告。&lt;&#x2F;li&gt;
&lt;li&gt;❓指定裝置，就能在電腦、平板電腦和智慧型手機等各種裝置上觸及客戶。&lt;&#x2F;li&gt;
&lt;li&gt;❓指定家戶人口，廣告客戶就能針對特定人數的家庭放送廣告。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;xiao-ban-zai-wang-zhan-he-ying-yong-cheng-shi-shang-xiao-shou-jia-ju-yong-pin-qing-wen-google-ads-ke-tou-guo-xia-lie-na-liang-zhong-fang-shi-dai-dong-ye-ji-cheng-chang-bing-xie-zhu-ta-da-cheng-mu-biao-qing-xuan-ze-liang-xiang&quot;&gt;小班在網站和應用程式上銷售家居用品。請問 Google Ads 可透過下列哪兩種方式帶動業績成長，並協助他達成目標？（請選擇兩項）&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;✔️引導使用者搜尋及探索他的產品。&lt;&#x2F;li&gt;
&lt;li&gt;❌提供清楚的預測資料，讓購物體驗更加順暢。&lt;&#x2F;li&gt;
&lt;li&gt;❌在所有搜尋引擎上刊登他的商家廣告。&lt;&#x2F;li&gt;
&lt;li&gt;❌以預設價格向他銷售待開發客戶。&lt;&#x2F;li&gt;
&lt;li&gt;✔️提高應用程式的整體安裝和互動次數。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;qing-jiang-xia-lie-ju-you-bu-tong-xu-qiu-de-xing-xiao-ren-yu-shi-he-ta-men-shi-yong-de-duo-mei-ti-guang-gao-ge-shi-pei-dui-he-shi-de-ge-shi-ke-neng-bu-zhi-yi-zhong&quot;&gt;請將下列具有不同需求的行銷人與適合他們使用的多媒體廣告格式配對 (合適的格式可能不只一種)。&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;小湯重視成效勝於一切。
&lt;ul&gt;
&lt;li&gt;回應式多媒體廣告&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;姍姍想要建立更安全的廣告，並降低感染惡意軟體的風險。
&lt;ul&gt;
&lt;li&gt;AMP HTML 廣告&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;小雪希望減少資料載入量，進而提升廣告速度。
&lt;ul&gt;
&lt;li&gt;AMP HTML 廣告&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;小米希望能全面掌控廣告素材，可自行決定如何結合使用不同的圖片、文字和標誌。
&lt;ul&gt;
&lt;li&gt;圖像廣告&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;阿三想要減少在廣告群組和廣告活動中管理廣告組合的費用。
&lt;ul&gt;
&lt;li&gt;回應式多媒體廣告&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;cheng-xiao-gui-hua-gong-ju-ru-he-wei-ni-de-shang-jia-dai-lai-zhu-yi&quot;&gt;成效規劃工具如何為你的商家帶來助益？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;✔️發揮廣告預算的最大效益，盡量提升業績&lt;&#x2F;li&gt;
&lt;li&gt;❌教導員工個人預算管理的基本概念&lt;&#x2F;li&gt;
&lt;li&gt;❓從總預算中找出能提升行銷效益的部分&lt;&#x2F;li&gt;
&lt;li&gt;❌決定你的品牌最適用哪種 Google 廣告&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;ya-dang-fa-xian-dui-yu-ta-de-xin-duo-mei-ti-guang-gao-huo-dong-lai-shuo-gu-li-cai-qu-xing-dong-shi-zui-he-shi-de-xing-xiao-biao-de-xia-lie-na-liang-ge-xiang-guan-xuan-xiang-ke-bang-zhu-ta-da-dao-zhe-ge-mu-biao-qing-xuan-ze-liang-xiang&quot;&gt;亞當發現，對於他的新多媒體廣告活動來說，「鼓勵採取行動」是最合適的行銷標的。下列哪兩個相關選項可幫助他達到這個目標？(請選擇兩項)&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;✔️標準再行銷&lt;&#x2F;li&gt;
&lt;li&gt;❌興趣相似目標對象&lt;&#x2F;li&gt;
&lt;li&gt;❌指定客層&lt;&#x2F;li&gt;
&lt;li&gt;❓自訂意願目標對象&lt;&#x2F;li&gt;
&lt;li&gt;✔️動態再行銷&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;bi-er-wei-ta-de-gong-si-fa-bu-liao-yi-xi-lie-xin-shang-pin-bing-shi-yong-google-display-ads-ying-xiang-ke-hu-de-gou-wu-jue-ce-qing-wen-google-display-ads-zhi-ding-mu-biao-gong-neng-ru-he-xie-zhu-bi-er-da-cheng-ta-de-xing-xiao-biao-de&quot;&gt;比爾為他的公司發布了一系列新商品，並使用 Google Display Ads 影響客戶的購物決策。請問 Google Display Ads 指定目標功能如何協助比爾達成他的行銷標的？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;❌協助比爾找出對他的產品感興趣的使用者，藉此針對這些目標對象改善客戶體驗。&lt;&#x2F;li&gt;
&lt;li&gt;✔️協助比爾運籌帷幄，在使用者積極瀏覽、研究或比較他所銷售的商品類型時向他們傳達訊息。&lt;&#x2F;li&gt;
&lt;li&gt;❌協助比爾依據客戶的興趣調整產品價格，藉此增加單次交易的銷售額。&lt;&#x2F;li&gt;
&lt;li&gt;❓協助比爾依據自己的自動化行銷標的，準確地向他選取的目標對象顯示廣告。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;xiao-cui-jing-ying-yi-jia-zhuan-men-zhi-zao-jian-xing-he-pan-yan-yong-pin-de-qi-ye-sui-ran-ta-de-chan-pin-shi-zhan-lu-bu-da-dan-ta-yi-zhun-bei-hao-yao-kuo-zhan-ye-wu-gui-mo-qing-wen-xiao-cui-ying-gai-shi-yong-na-yi-zhong-google-ads-guang-gao-huo-dong-lei-xing-cai-neng-zeng-jia-pin-pai-de-pu-guang-lu-bing-chu-ji-wang-lu-shang-dui-jian-xing-yu-pan-yan-gan-xing-qu-de-mu-biao-dui-xiang&quot;&gt;小崔經營一家專門製造健行和攀岩用品的企業。雖然他的產品市占率不大，但他已準備好要擴展業務規模。請問小崔應該使用哪一種 Google Ads 廣告活動類型，才能增加品牌的曝光率，並觸及網路上對健行與攀岩感興趣的目標對象？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;✔️多媒體廣告活動&lt;&#x2F;li&gt;
&lt;li&gt;❌購物廣告活動&lt;&#x2F;li&gt;
&lt;li&gt;❌影片廣告活動&lt;&#x2F;li&gt;
&lt;li&gt;❓搜尋廣告活動&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;xiao-luo-xuan-ze-yi-ying-xiang-kao-lu-du-zuo-wei-google-display-ads-guang-gao-huo-dong-de-xing-xiao-biao-de-qing-wen-xia-lie-na-liang-ge-zhi-ding-mu-biao-xuan-xiang-shi-yong-yu-xiao-luo-de-guang-gao-huo-dong-qing-xuan-ze-liang-xiang&quot;&gt;小蘿選擇以「影響考慮度」做為 Google Display Ads 廣告活動的行銷標的。請問下列哪兩個指定目標選項適用於小蘿的廣告活動？(請選擇兩項)&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;✔️自訂意願目標對象&lt;&#x2F;li&gt;
&lt;li&gt;❓類似目標對象&lt;&#x2F;li&gt;
&lt;li&gt;❓自訂興趣相似目標對象&lt;&#x2F;li&gt;
&lt;li&gt;✔️動態再行銷&lt;&#x2F;li&gt;
&lt;li&gt;❌指定客層&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;zhi-hui-duo-mei-ti-guang-gao-huo-dong-de-zhu-yao-you-dian-wei-he&quot;&gt;智慧多媒體廣告活動的主要優點為何？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;❓智慧多媒體廣告活動會自動製作影片內容。&lt;&#x2F;li&gt;
&lt;li&gt;❓以高度區隔的分析資料，進行廣告活動的每日深入剖析。&lt;&#x2F;li&gt;
&lt;li&gt;✔️❓製作智慧多媒體廣告活動時，廣告客戶所須提供的資料最少。&lt;&#x2F;li&gt;
&lt;li&gt;❓根據智慧多媒體廣告活動的成效，產生多媒體廣告聯播網的建議。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;lu-lu-zheng-zai-jian-li-xin-de-duo-mei-ti-guang-gao-huo-dong-yi-jie-ci-da-xiang-zhi-ming-du-qing-wen-ta-shi-he-shi-yong-xia-lie-na-liang-ge-zhi-ding-mu-biao-xuan-xiang-qing-xuan-ze-liang-xiang&quot;&gt;路路正在建立新的多媒體廣告活動，以藉此打響知名度。請問她適合使用下列哪兩個指定目標選項？(請選擇兩項)&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;✔️❓興趣相似目標對象&lt;&#x2F;li&gt;
&lt;li&gt;❌標準再行銷&lt;&#x2F;li&gt;
&lt;li&gt;✔️❓潛在目標消費者&lt;&#x2F;li&gt;
&lt;li&gt;❓指定客層&lt;&#x2F;li&gt;
&lt;li&gt;❓自訂意願目標對象&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;xiao-cui-xi-wang-neng-rang-guang-gao-huo-dong-de-guan-li-liu-cheng-geng-you-xiao-lu-yin-ci-kao-lu-cai-yong-zi-dong-chu-jia-zi-dong-chu-jia-ke-tou-guo-yi-xia-na-san-zhong-fang-shi-ti-gao-xiao-lu&quot;&gt;小翠希望能讓廣告活動的管理流程更有效率，因此考慮採用自動出價。自動出價可透過以下哪三種方式提高效率？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;❓整合各種不同的信號來評估使用者意願&lt;&#x2F;li&gt;
&lt;li&gt;✔️❓為每次競價設定合適的出價&lt;&#x2F;li&gt;
&lt;li&gt;✔️❓節省時間和行銷資源&lt;&#x2F;li&gt;
&lt;li&gt;✔️❓根據每日預算設定最高和最低出價金額&lt;&#x2F;li&gt;
&lt;li&gt;❌針對一天中的特定時段設定手動出價&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;ping-gu-1&quot;&gt;評估&lt;&#x2F;h3&gt;
&lt;h4 id=&quot;xia-lie-na-ge-gui-yin-mo-shi-shi-yong-ji-qi-xue-xi-lai-ping-gu-ge-bie-ke-hu-lu-jing&quot;&gt;下列哪個歸因模式使用機器學習來評估個別客戶路徑？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;❌時間衰減&lt;&#x2F;li&gt;
&lt;li&gt;✔️以數據為準&lt;&#x2F;li&gt;
&lt;li&gt;❌線性&lt;&#x2F;li&gt;
&lt;li&gt;❌最終互動&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;mu-qian-jie-ge-shou-shang-you-ge-ying-pian-guang-gao-huo-dong-zheng-zai-fang-song-ta-fa-xian-ying-pian-de-zheng-ti-guan-kan-ci-shu-sui-gao-dan-can-yu-du-que-pian-di-xia-lie-na-san-ge-wen-ti-ke-yi-bang-zhu-ta-li-qing-zhuang-kuang&quot;&gt;目前，杰哥手上有個影片廣告活動正在放送。他發現影片的整體觀看次數雖高，但參與度卻偏低。下列哪三個問題可以幫助他釐清狀況？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;✔️會不會是在執行上發生了什麼錯誤？&lt;&#x2F;li&gt;
&lt;li&gt;✔️廣告素材能否有效引起關注並突顯品牌？&lt;&#x2F;li&gt;
&lt;li&gt;❌是否已向足夠的人數放送影片？&lt;&#x2F;li&gt;
&lt;li&gt;✔️是否指定了適合的目標對象？&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;xiao-yu-yi-fang-song-sou-xun-guang-gao-huo-dong-shu-yue-zhi-jiu-xian-zai-ta-xiang-jin-xing-yi-xie-gai-shan-qing-xie-zhu-ta-zhao-chu-zai-ping-gu-cheng-xiao-hou-ke-zui-jia-hua-guang-gao-huo-dong-de-san-zhong-fang-fa&quot;&gt;小裕已放送搜尋廣告活動數月之久，現在她想進行一些改善。請協助她找出在評估成效後，可最佳化廣告活動的三種方法。&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;✔️啟用自動出價。&lt;&#x2F;li&gt;
&lt;li&gt;✔️移除成效不佳的廣告素材，嘗試新的廣告素材。&lt;&#x2F;li&gt;
&lt;li&gt;❌除非廣告活動已放送六個月以上，否則建議不要進行任何變更。&lt;&#x2F;li&gt;
&lt;li&gt;✔️評估哪個指定目標對象的成效最佳，並將出價提高一倍。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;xia-lie-guan-yu-zhuan-huan-zhui-zong-de-xu-shu-you-na-liang-xiang-shi-zheng-que-de&quot;&gt;下列關於轉換追蹤的敘述，有哪兩項是正確的？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;❌將預算自動分配至成效最好的廣告。&lt;&#x2F;li&gt;
&lt;li&gt;❌必須在 Google Ads 中加入額外的程式碼片段，才能在你的網站上追蹤使用者的互動情形。&lt;&#x2F;li&gt;
&lt;li&gt;✔️可透過成效最佳的廣告，瞭解如何創新及改良廣告素材。&lt;&#x2F;li&gt;
&lt;li&gt;✔️可以追蹤廣告的互動情形，並辨識對公司最有價值的廣告。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;xia-lie-wei-ke-yong-lai-jian-shi-zhuan-huan-zi-liao-de-zhuan-huan-zhi-biao-qing-wei-ge-xiang-zhi-biao-pei-dui-zheng-que-de-xu-shu&quot;&gt;下列為可用來檢視轉換資料的轉換指標，請為各項指標配對正確的敘述。&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;單次轉換費用（費用&#x2F;轉換次數）
&lt;ul&gt;
&lt;li&gt;顯示每次轉換平均需要多少費用。計算方式是將總費用除以「轉換」欄中的次數。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;轉換率
&lt;ul&gt;
&lt;li&gt;顯示一次廣告點擊或其他廣告互動能促成轉換的平均頻率。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;轉換價值
&lt;ul&gt;
&lt;li&gt;是各項轉換的轉換價值總和。你必須輸入轉換動作的價值，這項指標才能派上用場。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;單筆費用轉換價值（轉換價值&#x2F;費用）
&lt;ul&gt;
&lt;li&gt;用來預估投資報酬率，計算方式是將廣告的總轉換價值除以總點擊費用。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;單次點擊的轉換價值（轉換價值&#x2F;點擊）
&lt;ul&gt;
&lt;li&gt;計算方式是將總轉換價值除以符合條件的點擊次數。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;na-ge-xuan-xiang-ke-yong-lai-zi-ding-zai-zhuan-huan-lan-zhong-zhui-zong-zi-liao-de-fang-shi&quot;&gt;哪個選項可用來自訂在「轉換」欄中追蹤資料的方式？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;❌自訂表格&lt;&#x2F;li&gt;
&lt;li&gt;❌「修改欄」設定&lt;&#x2F;li&gt;
&lt;li&gt;❌「競價分析」報表&lt;&#x2F;li&gt;
&lt;li&gt;✔️「納入『轉換』欄」設定&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;xia-lie-na-liang-xiang-guan-yu-gui-yin-bao-biao-de-miao-shu-shi-zheng-que-de&quot;&gt;下列哪兩項關於「歸因」報表的描述是正確的？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;❌「歸因」報表會納入應用程式轉換和親臨門市。&lt;&#x2F;li&gt;
&lt;li&gt;✔️「歸因」報表不會納入應用程式轉換和親臨門市。&lt;&#x2F;li&gt;
&lt;li&gt;❌變更歸因模式後，可透過「歸因」報表評估及改善成效。&lt;&#x2F;li&gt;
&lt;li&gt;✔️有了「歸因」報表，就能預估歸因模式的變動可能會對轉換報表的資料造成什麼影響。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;ying-pian-guang-gao-huo-dong-hui-jiang-liu-lan-hou-zhuan-huan-gui-gong-yu-ying-pian-guang-gao-de-zui-zhong-pu-guang&quot;&gt;影片廣告活動會將瀏覽後轉換歸功於影片廣告的最終曝光。&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;✔️正確&lt;&#x2F;li&gt;
&lt;li&gt;❌錯誤&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;google-wo-de-shang-jia-de-xun-xi-gong-ju-ke-rang-ke-hu-tou-guo-na-zhong-tong-xun-fang-shi-lian-luo-shang-jia-ye-zhu&quot;&gt;「Google 我的商家」的訊息工具可讓客戶透過哪種通訊方式聯絡商家業主？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;❌公布欄&lt;&#x2F;li&gt;
&lt;li&gt;❌電子郵件&lt;&#x2F;li&gt;
&lt;li&gt;✔️簡訊&lt;&#x2F;li&gt;
&lt;li&gt;❌視訊通訊&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;qing-xuan-ze-ke-yong-lai-gu-suan-bing-ti-gao-xiao-shou-liang-de-zhuan-huan-ji-suan-xuan-xiang&quot;&gt;請選擇可用來估算並提高銷售量的轉換計算選項。&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;✔️每次&lt;&#x2F;li&gt;
&lt;li&gt;❌單次&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;mou-jia-la-zhu-gong-si-zheng-wei-xin-de-duo-mei-ti-guang-gao-zhuan-xie-wen-an-ru-guo-ta-men-xiang-tou-guo-zhe-ze-guang-gao-ying-xiang-xiao-fei-zhe-dui-zi-jia-chan-pin-de-kao-lu-du-ze-gai-ru-he-chu-li-guang-gao-su-cai&quot;&gt;某家蠟燭公司正為新的多媒體廣告撰寫文案。如果他們想透過這則廣告影響消費者對自家產品的考慮度，則該如何處理廣告素材？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;❌主打特定優惠或產品（向上銷售）&lt;&#x2F;li&gt;
&lt;li&gt;✔️❓主打品牌特質或產品的價值主張&lt;&#x2F;li&gt;
&lt;li&gt;❌❓主打品牌相關故事&lt;&#x2F;li&gt;
&lt;li&gt;❌加入明示或暗示的行動號召（立即購買、點此購買、由此購物等）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;you-jia-she-ji-ka-pei-bei-de-gong-si-xiang-zhi-dao-ta-men-na-yi-xiang-ying-pian-guang-gao-su-cai-zui-rang-guan-zhong-yin-xiang-shen-ke-xia-lie-na-ge-google-gong-ju-ke-yi-bang-zhu-ta-men-zhao-chu-da-an&quot;&gt;有家設計咖啡杯的公司想知道他們哪一項影片廣告素材最讓觀眾印象深刻，下列哪個 Google 工具可以幫助他們找出答案？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;✔️❓廣告印象&lt;&#x2F;li&gt;
&lt;li&gt;❌品牌好感度&lt;&#x2F;li&gt;
&lt;li&gt;❓品牌提升&lt;&#x2F;li&gt;
&lt;li&gt;❌轉換升幅&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;xia-lie-na-san-xiang-shi-ge-ren-huo-gong-si-jun-ke-cai-xing-de-liang-hao-ping-gu-xi-guan&quot;&gt;下列哪三項是個人或公司均可採行的良好評估習慣？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;❌參與任何會議時，都要攜帶資料&lt;&#x2F;li&gt;
&lt;li&gt;✔️勉勵大家從失敗中學習&lt;&#x2F;li&gt;
&lt;li&gt;✔️大約每個月查看一次資料&lt;&#x2F;li&gt;
&lt;li&gt;✔️持續進行測試&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;google-ti-gong-liao-duo-zhong-fang-fa-zhui-zong-guang-gao-wang-zhan-he-xing-dong-ban-wang-zhan-shang-de-lai-dian-qing-jiang-xia-lie-ge-xiang-zhuan-huan-zhui-zong-mu-biao-pei-dui-zheng-que-de-fang-fa&quot;&gt;Google 提供了多種方法追蹤廣告、網站和行動版網站上的來電。請將下列各項轉換追蹤目標配對正確的方法。&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;追蹤網站上電話號碼獲得的來電
&lt;ul&gt;
&lt;li&gt;✔️❓使用 Google 轉接號碼&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;追蹤廣告轉接來電
&lt;ul&gt;
&lt;li&gt;✔️❓在 Google 廣告上新增來電額外資訊&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;追蹤行動網站上電話號碼獲得的來電
&lt;ul&gt;
&lt;li&gt;✔️❓追蹤文字連結、圖片或按鈕的點擊次數&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;google-de-ying-pian-zhuan-huan-ke-neng-lai-zi-san-zhong-bu-tong-de-ke-hu-dong-zuo-qing-jiang-ge-xiang-zhuan-huan-dong-zuo-pei-dui-zheng-que-de-shu-chu-zi-xun&quot;&gt;Google 的影片轉換可能來自三種不同的客戶動作。請將各項轉換動作配對正確的輸出資訊。&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;按觀看次數&#x2F;觀看參與度
&lt;ul&gt;
&lt;li&gt;✔️❓按曝光次數；24 小時的預設回溯期&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;點閱後轉換
&lt;ul&gt;
&lt;li&gt;✔️❓按點擊次數；30 天的預設回溯期&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;瀏覽後轉換
&lt;ul&gt;
&lt;li&gt;✔️❓按收看時間達 30 秒，或看完廣告的次數；30 天的預設回溯期&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;jiang-ke-hu-liu-cheng-de-ge-ge-jie-duan-pei-dui-dao-xiang-guan-xing-xiao-mu-biao&quot;&gt;將客戶流程的各個階段配對到相關行銷目標。&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;忠誠度
&lt;ul&gt;
&lt;li&gt;✔️❓客戶推薦人&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;購買
&lt;ul&gt;
&lt;li&gt;✔️❓平均訂單量&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;考慮度
&lt;ul&gt;
&lt;li&gt;✔️❓品牌好感度&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;品牌意識
&lt;ul&gt;
&lt;li&gt;✔️品牌記憶&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;li-an-yong-you-yi-jia-shi-ti-shang-dian-shi-nian-lai-jing-ying-de-you-sheng-you-se-ta-de-chan-pin-bu-shi-he-zai-xian-shang-xiao-shou-huo-yun-song-ta-zui-jin-jian-li-liao-yi-ge-wang-zhan-he-ying-yong-cheng-shi-yao-lai-xuan-chuan-zi-ji-de-shang-jia-xian-zai-ta-xiang-shi-yong-google-ads-jian-li-yi-ge-guang-gao-huo-dong-bing-ping-gu-xiang-guan-cheng-xiao-li-xian-zhuan-huan-zhui-zong-ke-wei-li-an-dai-lai-shen-mo-bang-zhu&quot;&gt;莉安擁有一家實體商店，十年來經營得有聲有色。她的產品不適合在線上銷售或運送。她最近建立了一個網站和應用程式，要來宣傳自己的商家。現在她想使用 Google Ads 建立一個廣告活動，並評估相關成效。離線轉換追蹤可為莉安帶來什麼幫助？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;❌追蹤應用程式初次開啟的轉換。&lt;&#x2F;li&gt;
&lt;li&gt;✔️評估廣告點擊帶來的離線銷售。&lt;&#x2F;li&gt;
&lt;li&gt;❌將離線客戶轉換為線上購物者。&lt;&#x2F;li&gt;
&lt;li&gt;❌評估有多少購物者有意造訪她的實體商店。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;you-yi-jian-can-ting-zui-jin-gang-kai-mu-gai-can-ting-de-lao-ban-xiang-zhi-zuo-guang-gao-lai-xuan-chuan-lu-bian-qu-can-fu-wu-qing-wen-zhe-wei-lao-ban-ying-gen-ju-na-xiang-xing-xiao-biao-de-lai-zhi-zuo-guang-gao&quot;&gt;有一間餐廳最近剛開幕，該餐廳的老闆想製作廣告來宣傳路邊取餐服務。&lt;br&gt;請問這位老闆應根據哪項行銷標的來製作廣告？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;❓品牌忠誠度&lt;&#x2F;li&gt;
&lt;li&gt;✔️❓品牌好感度&lt;&#x2F;li&gt;
&lt;li&gt;❓✔️品牌意識&lt;&#x2F;li&gt;
&lt;li&gt;❌投資報酬率&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;guang-de-zai-wang-zhan-shang-xuan-chuan-ta-de-zai-di-du-jia-min-su-ta-li-yong-google-ads-lai-guan-li-guang-gao-huo-dong-ta-fan-fu-can-zhao-gou-mai-zhuan-huan-he-hou-duan-ke-hu-guan-xi-guan-li-xi-tong-shi-fa-xian-zi-liao-you-luo-chai-zao-cheng-zi-liao-chai-yi-de-yuan-yin-wei-he&quot;&gt;廣德在網站上宣傳他的在地度假民宿，他利用 Google Ads 來管理廣告活動。他反覆參照購買轉換和後端客戶關係管理系統時，發現資料有落差。&lt;br&gt;造成資料差異的原因為何？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;❓廣德為擷取不重複的訂單 ID 而修改了事件代碼。&lt;&#x2F;li&gt;
&lt;li&gt;✔️❓轉換追蹤重複計算轉換。&lt;&#x2F;li&gt;
&lt;li&gt;❌廣德啟用了「瀏覽後轉換」欄。&lt;&#x2F;li&gt;
&lt;li&gt;❓客戶關係管理不支援不重複的訂單 ID。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;xiao-cha-shi-yi-jia-xian-shang-pi-jian-ling-shou-shang-de-xing-xiao-zhu-guan-ta-ba-zhong-dian-fang-zai-zi-liao-ping-gu-mu-biao-shi-yao-ti-gao-gong-si-pi-dai-chan-pin-de-xiao-liang-ta-xuan-ze-google-de-zi-dong-hua-ji-qi-xue-xi-ji-shu-bang-zhu-gong-si-zai-shi-chang-shang-qu-de-jing-zheng-you-shi-ru-yao-chong-fen-yun-yong-zi-dong-hua-gong-neng-xiao-cha-shou-xian-bi-xu-cai-qu-shen-mo-bu-zou&quot;&gt;小察是一家線上皮件零售商的行銷主管，他把重點放在資料評估，目標是要提高公司皮帶產品的銷量。他選擇 Google 的自動化機器學習技術，幫助公司在市場上取得競爭優勢。如要充分運用自動化功能，小察首先必須採取什麼步驟？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;✔️❓首先要串連機構中的零散資料，以確保資料品質。這樣 Google 的機器學習演算法才能把正確的資料記錄下來。&lt;&#x2F;li&gt;
&lt;li&gt;❓✔️他必須瞭解 Google 自動化系統所用的資料模型。&lt;&#x2F;li&gt;
&lt;li&gt;❌他必須建立可輸入至 Google 自動化系統的資料試算表。&lt;&#x2F;li&gt;
&lt;li&gt;❌他必須在手動最佳化流程中即時指定資料值。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;jia-er-shi-yi-ming-xing-xiao-jing-li-gang-cong-chuang-yi-tuan-dui-na-li-jie-shou-yi-zu-guang-gao-ta-xiang-yao-yong-a-b-ce-shi-lai-gai-shan-guang-gao-nei-rong-jia-er-bi-jiao-de-guang-gao-lei-xing-wei-he&quot;&gt;佳兒是一名行銷經理，剛從創意團隊那裡接手一組廣告，她想要用 A&#x2F;B 測試來改善廣告內容。&lt;br&gt;佳兒比較的廣告類型為何？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;✔️❓使用者未曾接觸廣告，且預算和目標對象都相同的廣告。&lt;&#x2F;li&gt;
&lt;li&gt;❓✔️用途、放送管道和目標對象都相同的廣告。&lt;&#x2F;li&gt;
&lt;li&gt;❌使用者曾接觸廣告，且放送訊息和目標對象都相同的廣告。&lt;&#x2F;li&gt;
&lt;li&gt;❓用於品牌提升，且放送管道和目標對象都相同的廣告。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;ni-jue-ding-yao-da-liang-shang-chuan-ying-ye-di-dian-zhi-google-wo-de-shang-jia-bu-guo-you-xie-di-dian-zao-dao-ju-deng-xia-lie-na-liang-xiang-cuo-wu-hui-dao-zhi-ju-deng-qing-xuan-ze-liang-xiang&quot;&gt;你決定要大量上傳營業地點至 Google 我的商家，不過有些地點遭到拒登。下列哪兩項錯誤會導致拒登（請選擇兩項）？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;❌太晚註冊商家資訊&lt;&#x2F;li&gt;
&lt;li&gt;✔️商家名稱有誤&lt;&#x2F;li&gt;
&lt;li&gt;✔️同一實體地址有多個商家地點&lt;&#x2F;li&gt;
&lt;li&gt;❌商家資訊使用者過多&lt;&#x2F;li&gt;
&lt;li&gt;❌遺漏商家資訊相片&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;xiao-zhu-zheng-shi-zhu-liao-jie-wei-he-ta-de-wang-zhan-guo-qu-san-ge-yue-de-zhuan-huan-liang-hui-you-ru-ci-da-de-bo-dong-ta-kai-shi-diao-cha-ke-neng-zao-cheng-bo-dong-de-yuan-yin-zai-diao-cha-guo-cheng-zhong-xiao-zhu-ke-yi-cai-qu-na-liang-xiang-jian-yi-cuo-shi-qing-xuan-ze-liang-xiang&quot;&gt;小茱正試著瞭解為何她的網站過去三個月的轉換量會有如此大的波動，她開始調查可能造成波動的原因。&lt;br&gt;在調查過程中，小茱可以採取哪兩項建議措施 (請選擇兩項)？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;✔️查看轉換耗時報表，進一步瞭解網站訪客的轉換路徑。&lt;&#x2F;li&gt;
&lt;li&gt;❌進行廣告變更，查看變更後的轉換量波動是否與目前的波動情形類似。&lt;&#x2F;li&gt;
&lt;li&gt;❌請行銷團隊的其他成員再次確認報表結果，藉此排除使用者錯誤。&lt;&#x2F;li&gt;
&lt;li&gt;✔️選取特定日期，查看當天的所有帳戶變更記錄，因為廣告變更可能會對轉換造成影響。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;zai-mou-xie-qing-kuang-xia-li-ru-ping-gu-pin-pai-yi-shi-he-pin-pai-hao-gan-du-shi-wo-men-wu-fa-ping-gu-mou-ge-google-ads-guang-gao-huo-dong-zai-duo-ge-guan-dao-zhong-de-cheng-xiao-wei-shen-mo-duo-guan-dao-guang-gao-huo-dong-de-cheng-xiao-ru-ci-nan-yi-ping-gu&quot;&gt;在某些情況下（例如評估品牌意識和品牌好感度時），我們無法評估某個 Google Ads 廣告活動在多個管道中的成效。為什麼多管道廣告活動的成效如此難以評估？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;❓這些管道會整合廣告活動的資料，得出可行深入分析資料。&lt;&#x2F;li&gt;
&lt;li&gt;❓這些管道會彙整所有資料，以便你在單一頁面集中檢視。&lt;&#x2F;li&gt;
&lt;li&gt;❓這些管道只會對曾接觸廣告的使用者進行問卷調查。&lt;&#x2F;li&gt;
&lt;li&gt;✔️❓這些管道可能會透過不同工具評估品牌提升。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;bu-jiu-qian-ni-wei-zi-ji-de-xian-shang-ling-shou-shang-dian-tui-chu-liao-yi-kuan-xing-dong-ying-yong-cheng-shi-ni-tou-guo-google-ads-de-guang-gao-huo-dong-bao-biao-cha-kan-google-ads-zhang-hu-zhong-de-suo-you-zhuan-huan-zong-shu-ni-xiang-yao-liao-jie-xin-de-ying-yong-cheng-shi-dai-lai-de-ying-yong-cheng-shi-an-zhuang-zhuan-huan-yi-ji-chu-ci-kai-qi-de-zhuan-huan-cheng-xiao-yao-zhe-mo-zuo-bi-xu-xian-cai-qu-shen-mo-dong-zuo&quot;&gt;不久前，你為自己的線上零售商店推出了一款行動應用程式。你透過 Google Ads 的廣告活動報表，查看 Google Ads 帳戶中的所有轉換總數。你想要瞭解新的應用程式帶來的應用程式安裝轉換，以及初次開啟的轉換成效。&lt;br&gt;要這麼做，必須先採取什麼動作？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;✔️根據轉換動作區隔資料。&lt;&#x2F;li&gt;
&lt;li&gt;❌根據轉換調整來區隔資料。&lt;&#x2F;li&gt;
&lt;li&gt;❌根據轉換類別區隔資料。&lt;&#x2F;li&gt;
&lt;li&gt;❌根據點擊類型區隔資料。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;bi-lan-zheng-zai-zhuan-xie-ke-ding-yi-de-zhi-biao-he-zhu-yao-cheng-xiao-zhi-biao-kpi-yong-yi-ping-gu-google-ads-guang-gao-huo-dong-de-zui-zhong-cheng-xiao-bi-lan-she-ding-kpi-shi-ke-yi-can-kao-xia-lie-na-liang-xiang-xu-shu-qing-xuan-ze-liang-xiang&quot;&gt;碧蘭正在撰寫可定義的指標和主要成效指標 (KPI)，用以評估 Google Ads 廣告活動的最終成效。&lt;br&gt;碧蘭設定 KPI 時，可以參考下列哪兩項敘述 (請選擇兩項)？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;✔️KPI 必須明確、可評估，且有一定的時間範圍。&lt;&#x2F;li&gt;
&lt;li&gt;✔️❓KPI 是用來評估成效的指標。&lt;&#x2F;li&gt;
&lt;li&gt;❓要有各個媒體管道專屬的 KPI，且不能互相替換。&lt;&#x2F;li&gt;
&lt;li&gt;❓能即時顯示廣告成效的 KPI。&lt;&#x2F;li&gt;
&lt;li&gt;❓評估非點擊行為的 KPI，例如網頁瀏覽量和待開發客戶。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;wei-wei-shi-jing-yan-feng-fu-de-xing-xiao-ce-lue-zhuan-jia-ta-ren-zhi-de-ruan-ti-gong-si-xiang-gong-li-xue-xiao-chu-shou-xue-xi-ping-tai-ta-yang-cheng-liao-liang-hao-de-ge-ren-ping-gu-xi-guan-bang-zhu-gong-si-zai-mian-dui-xue-xiao-bu-duan-gai-bian-de-xu-qiu-shi-ji-shi-diao-zheng-xing-xiao-ce-lue-wei-wei-yang-cheng-de-na-liang-ge-xi-guan-neng-bang-zhu-ta-ping-gu-ji-diao-zheng-xing-xiao-ce-lue-qing-xuan-ze-liang-xiang&quot;&gt;維偉是經驗豐富的行銷策略專家，他任職的軟體公司向公立學校出售學習平台。他養成了良好的個人評估習慣，幫助公司在面對學校不斷改變的需求時，即時調整行銷策略。&lt;br&gt;維偉養成的哪兩個習慣能幫助他評估及調整行銷策略 (請選擇兩項)？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;✔️❓他持續進行測試，藉此累積資料點，這對擬定重要決策很有幫助。&lt;&#x2F;li&gt;
&lt;li&gt;❓他會在開會時詢問每個人的意見，藉此避免最高薪人士主導決策方向。&lt;&#x2F;li&gt;
&lt;li&gt;✔️❓他會獨立出可避免失敗的資料並加以分析，這樣就能最佳化實驗性行銷廣告活動的成效。&lt;&#x2F;li&gt;
&lt;li&gt;❓他每天上班時都會依據公司的主要成效指標，確認自己的績效。&lt;&#x2F;li&gt;
&lt;li&gt;❓他把所有銷售資料存放在客戶關係管理的私人資訊主頁中，避免團隊成員因看到資料而無法專心工作。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;a-ke-shi-yi-ming-xing-xiao-jing-li-ta-gang-cong-chuang-yi-tuan-dui-na-li-jie-shou-yi-zu-guang-gao-ta-xiang-tou-guo-a-b-ce-shi-bi-jiao-zhe-xie-guang-gao-de-zi-liao-a-ke-yao-bi-jiao-de-guang-gao-lei-xing-wei-he&quot;&gt;阿克是一名行銷經理，他剛從創意團隊那裡接手一組廣告。他想透過 A&#x2F;B 測試比較這些廣告的資料。&lt;br&gt;阿克要比較的廣告類型為何？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;❓用途和放送管道相同的管道，且目標對象皆為未接觸廣告的使用者。&lt;&#x2F;li&gt;
&lt;li&gt;✔️❓用途、放送管道和目標對象都相同的廣告。&lt;&#x2F;li&gt;
&lt;li&gt;❌❓用途和預算相同的廣告，且目標皆為品牌提升。&lt;&#x2F;li&gt;
&lt;li&gt;❓用途和預算相同的廣告，且目標對象皆為曾接觸廣告的使用者。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;a-luo-zheng-shi-zhu-zai-google-ads-zhang-hu-zhong-she-ding-li-xian-zhui-zong-xia-zai-yao-yong-de-zhuan-huan-fan-ben-hou-ta-jia-ru-liao-shi-yong-de-li-xian-zi-liao-bing-jin-xing-xiu-gai-ta-shi-zhu-shang-chuan-fan-ben-dan-que-wu-fa-wan-cheng-hui-ru-cheng-xu-shi-shen-mo-yuan-yin-zao-cheng-hui-ru-cheng-xu-shi-bai&quot;&gt;阿羅正試著在 Google Ads 帳戶中設定離線追蹤。下載要用的轉換範本後，他加入了適用的離線資料並進行修改。他試著上傳範本，但卻無法完成匯入程序。&lt;br&gt;是什麼原因造成匯入程序失敗？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;❓阿羅沒有在「轉換名稱」欄中加入任何資料。&lt;&#x2F;li&gt;
&lt;li&gt;❌阿羅上傳的是 Excel 檔案，而不是 Google 試算表。&lt;&#x2F;li&gt;
&lt;li&gt;✔️❓Google 點擊 ID 欄位從轉換範本中遭到移除。&lt;&#x2F;li&gt;
&lt;li&gt;❌阿羅的帳戶沒有獲得 Firebase 的開發管理員角色。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;ni-ke-yi-shi-yong-na-yi-xiang-google-gong-neng-ping-gu-zai-dian-ji-huo-liu-lan-guang-gao-hou-qin-lin-men-shi-de-ren-shu&quot;&gt;你可以使用哪一項 Google 功能，評估在點擊或瀏覽廣告後親臨門市的人數？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;❓媒體組合模式&lt;&#x2F;li&gt;
&lt;li&gt;✔️親臨門市&lt;&#x2F;li&gt;
&lt;li&gt;❓跨管道歸因模式&lt;&#x2F;li&gt;
&lt;li&gt;❓品牌提升&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;dai-fu-de-shang-jia-zhuan-mai-mei-wei-de-bei-zi-dan-gao-xu-yao-yi-ge-wang-zhan-yi-he-dang-di-qi-ta-gao-dian-dian-fen-ting-kang-li-ta-yi-jian-li-google-wo-de-shang-jia-zi-xun-qie-xiang-shi-yong-wang-zhan-zhi-zuo-gong-ju-wei-zi-jia-gao-dian-dian-da-zao-ge-ren-hua-de-wang-zhan-she-ji-ru-yao-shi-yong-google-wo-de-shang-jia-lai-da-zao-wang-zhan-ta-bi-xu-wan-cheng-na-xie-bu-zou-qing-cong-zui-shang-fang-kai-shi-yi-xu-lie-chu-bu-zou&quot;&gt;岱夫的商家專賣美味的杯子蛋糕，需要一個網站以和當地其他糕點店分庭抗禮。他已建立 Google 我的商家資訊，且想使用網站製作工具為自家糕點店打造個人化的網站設計。&lt;br&gt;如要使用 Google 我的商家來打造網站，他必須完成哪些步驟？（請從最上方開始依序列出步驟。）&lt;&#x2F;h4&gt;
&lt;ol&gt;
&lt;li&gt;✔️岱夫登入 Google 我的商家。&lt;&#x2F;li&gt;
&lt;li&gt;❓岱夫更新他所有的相關商家資訊。&lt;&#x2F;li&gt;
&lt;li&gt;❓岱夫在主選單中選取 [網站]。&lt;&#x2F;li&gt;
&lt;li&gt;❓岱夫選擇一個專屬網域，並將其指向網站。&lt;&#x2F;li&gt;
&lt;li&gt;❓岱夫加入圖片，其中包括封面相片和虛擬導覽。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h4 id=&quot;jin-se-wei-ta-de-xie-ye-gong-si-bian-lie-liao-zhu-yao-cheng-xiao-zhi-biao-kpi-bing-tou-guo-zhi-biao-pan-duan-gong-si-shi-fou-yi-da-cheng-zhe-xie-kpi-xia-lie-ge-xiang-miao-shu-you-xie-shi-jin-se-bian-lie-de-kpi-you-xie-ze-shi-zhi-biao-qing-tou-guo-xia-la-shi-xuan-dan-xuan-qu-xiang-fu-de-da-an&quot;&gt;堇瑟為她的鞋業公司編列了主要成效指標 (KPI)，並透過指標判斷公司是否已達成這些 KPI。下列各項描述有些是堇瑟編列的 KPI，有些則是指標，請透過下拉式選單選取相符的答案。&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;將 YouTube 上影片廣告的完整觀看次數提高 10%
&lt;ul&gt;
&lt;li&gt;✔️指標&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;將今年的高跟鞋銷量提高 10%
&lt;ul&gt;
&lt;li&gt;✔️KPI&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;將高跟鞋的 Google 搜尋廣告回應率提高 20%
&lt;ul&gt;
&lt;li&gt;✔️指標&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;將本季的線上訂購量提高 5%
&lt;ul&gt;
&lt;li&gt;✔️KPI&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;ni-xiang-yao-yong-google-ads-wei-shang-jia-wang-zhan-she-ding-zhuan-huan-zhui-zong-qie-yi-dao-ru-quan-yu-quan-wang-zhan-dai-ma-jie-zhu-ni-yao-jian-li-shi-jian-dai-ma-shi-jian-dai-ma-bi-xu-fang-zhi-yu-wang-ye-de-zheng-que-wei-zhi-cai-neng-zheng-chang-yun-zuo-shi-jian-dai-ma-ying-zhi-yu-wang-ye-shang-de-na-ge-wei-zhi&quot;&gt;你想要用 Google Ads 為商家網站設定轉換追蹤，且已導入全域全網站代碼。接著你要建立事件代碼，事件代碼必須放置於網頁的正確位置才能正常運作。事件代碼應置於網頁上的哪個位置？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;所有網頁的 與 代碼之間，置於全域網站代碼的正上方。&lt;&#x2F;li&gt;
&lt;li&gt;網站首頁 與 代碼的上下方。&lt;&#x2F;li&gt;
&lt;li&gt;✔️在網頁的 與 代碼之間，緊接在全域網站代碼下方。&lt;&#x2F;li&gt;
&lt;li&gt;在網頁的 與 代碼之間，置於最後一行代碼前。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;a-kai-li-yong-firebase-zhi-zuo-liao-yi-ge-ying-yong-cheng-shi-bing-jiang-shang-jia-de-shang-pin-xing-lu-lian-jie-dao-yi-google-ads-xuan-chuan-de-wang-lu-shang-dian-zhong-ta-xiang-zhui-zong-lai-zi-ying-yong-cheng-shi-de-xiao-shou-liang-qie-xu-lian-jie-firebase-he-google-ads-zhang-hu-a-kai-de-google-zhang-hu-xu-yao-na-ji-xiang-quan-xian-cai-neng-lian-jie-firebase-he-google-ads-zhang-hu&quot;&gt;阿凱利用 Firebase 製作了一個應用程式，並將商家的商品型錄連結到以 Google Ads 宣傳的網路商店中。他想追蹤來自應用程式的銷售量，且須連結 Firebase 和 Google Ads 帳戶。&lt;br&gt;阿凱的 Google 帳戶需要哪幾項權限，才能連結 Firebase 和 Google Ads 帳戶？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;Firebase 中的標準存取權&lt;&#x2F;li&gt;
&lt;li&gt;Google Ads 中專案權限的擁有者&lt;&#x2F;li&gt;
&lt;li&gt;✔️Firebase 中專案權限的擁有者&lt;&#x2F;li&gt;
&lt;li&gt;Google Ads 中的標準存取權&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;a-tang-shi-yong-google-ads-lai-xuan-chuan-ta-de-jiao-ta-che-dian-ta-zhi-zuo-liao-yi-ge-android-ying-yong-cheng-shi-qie-xu-ping-gu-mu-qian-guang-gao-huo-dong-dai-lai-de-xiao-shou-cheng-xiao-a-tang-yao-ru-he-wan-cheng-zhe-xiang-ren-wu&quot;&gt;阿湯使用 Google Ads 來宣傳他的腳踏車店。他製作了一個 Android 應用程式，且須評估目前廣告活動帶來的銷售成效。&lt;br&gt;阿湯要如何完成這項任務？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;✔️❓為應用程式內購設定轉換動作。&lt;&#x2F;li&gt;
&lt;li&gt;❓為應用程式內購設定歸因動作。&lt;&#x2F;li&gt;
&lt;li&gt;❓為網站購買設定轉換動作。&lt;&#x2F;li&gt;
&lt;li&gt;❓為網站購買設定歸因動作。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;ru-guo-mu-biao-shi-yao-ti-sheng-ke-hu-de-pin-pai-yi-shi-pin-pai-jing-li-ke-neng-hui-zhui-zong-shen-mo-huo-dong&quot;&gt;如果目標是要提升客戶的品牌意識，品牌經理可能會追蹤什麼活動？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;❌向上銷售&lt;&#x2F;li&gt;
&lt;li&gt;❌購買意願&lt;&#x2F;li&gt;
&lt;li&gt;❌交易&lt;&#x2F;li&gt;
&lt;li&gt;✔️品牌記憶&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;qiao-qi-zheng-zai-jian-li-xin-de-google-ads-guang-gao-huo-dong-ta-xiang-zhui-zong-ke-hu-zai-wang-zhan-shang-de-te-ding-dong-zuo-ta-zhi-dao-wang-zhan-yi-dao-ru-quan-yu-wang-zhan-dai-ma-yin-ci-de-yi-shi-yong-shi-jian-dai-ma-qiao-qi-ke-yi-shi-yong-xia-lie-na-liang-zhong-shi-jian-dai-ma-qing-xuan-ze-liang-xiang&quot;&gt;喬奇正在建立新的 Google Ads 廣告活動，他想追蹤客戶在網站上的特定動作。他知道網站已導入全域網站代碼，因此得以使用事件代碼。&lt;br&gt;喬奇可以使用下列哪兩種事件代碼 (請選擇兩項)？&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;✔️載入網頁&lt;&#x2F;li&gt;
&lt;li&gt;❌工作階段長度&lt;&#x2F;li&gt;
&lt;li&gt;❌跳出率&lt;&#x2F;li&gt;
&lt;li&gt;✔️點擊&lt;&#x2F;li&gt;
&lt;li&gt;❌下載&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h4 id=&quot;ni-yi-she-ding-xian-shang-ling-shou-wang-zhan-de-zhuan-huan-dong-zuo-xian-zai-ni-da-suan-she-ding-zhuan-huan-zhui-zong-dai-ma-yi-shen-ru-fen-xi-shi-yong-zhe-he-zi-jia-guang-gao-de-hu-dong-qing-xing-qing-wei-suo-xu-dai-ma-dong-zuo-pei-dui-xiang-ying-de-zhuan-huan-ping-gu-xiang-mu&quot;&gt;你已設定線上零售網站的轉換動作。現在你打算設定轉換追蹤代碼，以深入分析使用者和自家廣告的互動情形。&lt;br&gt;請為所需代碼動作配對相應的轉換評估項目。&lt;&#x2F;h4&gt;
&lt;ul&gt;
&lt;li&gt;已在網站導入 Google Analytics (分析) 後，可搭配使用 Google Ads 轉換追蹤。
&lt;ul&gt;
&lt;li&gt;連結 Google Ads 和 Analytics（分析）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Google Analytics (分析) 的匯入目標&#x2F;交易
&lt;ul&gt;
&lt;li&gt;不需要任何動作&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;適用於 Google Ads 的 Google 代碼管理工具
&lt;ul&gt;
&lt;li&gt;在 Google 代碼管理工具中加入轉換連接器&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;Google Ads 轉換追蹤
&lt;ul&gt;
&lt;li&gt;加入 gtag.js 代碼&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;xin-de&quot;&gt;心得&lt;&#x2F;h2&gt;
&lt;p&gt;認證的考題相當靈活，對習慣填鴨教育的我們來說頗有難度，大約只有 1&#x2F;3 的考題屬於教材內可以直接找得到答案的，其他 2&#x2F;3 的題目都是以教材知識為基礎的實例應用題，並且這些題目的選項很多也給人許多模稜兩可之感，例如下面這個單選題：&lt;&#x2F;p&gt;
&lt;div style=&quot;background-color: hsla(0, 0%, 96%, 1.0); padding: 1rem; text-align: center; margin-top: 1rem; margin-bottom: 1rem;&quot;&gt;
&lt;p&gt;&lt;strong&gt;近年來消費者的購物行為出現什麼變化？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;消費者購買的品項變少，但頻率上升。&lt;&#x2F;li&gt;
&lt;li&gt;消費者購買的品項和頻率都有所減少。&lt;&#x2F;li&gt;
&lt;li&gt;消費者購買的品項變多，但頻率下降。&lt;&#x2F;li&gt;
&lt;li&gt;消費者購買的品項和頻率都有所增加。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;這題既和 Googld Ads 的平台操作無關，也與數位行銷知識無關，而是一個教材內也沒有的「時事題」，看似沒有標準答案的題目，但它就這麼實實在在的考出來了。&lt;&#x2F;p&gt;
&lt;p&gt;即便是與 Google Ads 本身較為切題的題目，也相當的模稜兩可，如下面的單選題：&lt;&#x2F;p&gt;
&lt;div style=&quot;background-color: hsla(0, 0%, 96%, 1.0); padding: 1rem; text-align: center; margin-top: 1rem; margin-bottom: 1rem;&quot;&gt;
&lt;p&gt;&lt;strong&gt;在某些情況下（例如評估品牌意識和品牌好感度時），我們無法評估某個 Google Ads 廣告活動在多個管道中的成效。為什麼多管道廣告活動的成效如此難以評估？&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;這些管道會整合廣告活動的資料，得出可行深入分析資料。&lt;&#x2F;li&gt;
&lt;li&gt;這些管道會彙整所有資料，以便你在單一頁面集中檢視。&lt;&#x2F;li&gt;
&lt;li&gt;這些管道只會對曾接觸廣告的使用者進行問卷調查。&lt;&#x2F;li&gt;
&lt;li&gt;這些管道可能會透過不同工具評估品牌提升。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;想把這題題目看懂，得先認識「品牌意識」、「品牌好感度」、「多管道」這幾個教材內的專有詞彙的定義，但即便都了解了，下面的選項依然帶給我們模稜兩可之感。&lt;&#x2F;p&gt;
&lt;p&gt;面對 Googld Ads 認證考試，個人建議以下的應試策略：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;集中火力&lt;&#x2F;strong&gt;，從看教材到考認證，不要超過一個禮拜，集中火力把一個大單元的教材與認證在一週內結束掉，利用教材內容還在腦中的黃金印象期間完成一個大單元。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;善用「隨堂測驗評量」&lt;&#x2F;strong&gt;，每個認證的正式考試前都有一個「隨堂測驗評量」，可以視為簡化版的認證測驗，與正式認證不同的是，隨堂測驗的結果是會告訴我們答題的對錯的，而隨堂測驗的題目在正式認證考試內的出現率又頗高，因此一定要善加利用隨堂測驗，最好熟悉到隨堂測驗的考題全對為止。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;把不確定的題目與答案記下來&lt;&#x2F;strong&gt;，事後試著找到正確答案，受限於考試時間，沒辦法讓我們有充裕的時間慢慢找答案，可以把不確定的問題先記下來，事後再找答案，如此就算沒考過，也可以知道失分的由來，事後把答案確定後，下次再考就會得到更好的結果。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;刪去法&lt;&#x2F;strong&gt;，有些選項內有明顯錯誤的答案，可以先用刪去法，增加答對的機率。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;試誤法&lt;&#x2F;strong&gt;，結合前面的要點，讓自己的每次考試的得分能逐漸提升，最後就會過了，以本人而言，平均每張認證要考三次，善用上述的策略，能讓你每次考試信心度增加。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;cheng-guo&quot;&gt;成果&lt;&#x2F;h2&gt;
&lt;p&gt;最後現一下本人的成果：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;google-ads-certification-notes&#x2F;google_skillshop_awards.png&quot; alt=&quot;Google Ads 認證&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Time Machine 的備份提案</title>
        <published>2021-12-25T00:00:00+00:00</published>
        <updated>2021-12-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/time-machine/"/>
        <id>https://editor.leonh.space/2021/time-machine/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/time-machine/">&lt;p&gt;一般來說，資料的備份策略從規模的小到大，我們可以有幾種選擇，如果只是小規模的、輕量的，通常是燒成備份光碟，操作方式也多是手動的，然而隨著年紀的增長 Orz，資料也跟著增長，當資料量大到某種規模，手動備份就變得沒效率且不實際，這時我們就需要專門的備份軟體來自動的幫助我們進行備份的工作。&lt;&#x2F;p&gt;
&lt;p&gt;相較於手動備份，專門的備份軟體通常都會有三種備份模式給予我們選擇，分別是完全備份、增量備份及差異備份。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wan-quan-bei-fen&quot;&gt;完全備份&lt;&#x2F;h2&gt;
&lt;p&gt;完全備份就跟手動備份的方式一樣，每次的備份任務都是完整的從來源拷貝一份到目標上作為備份，很簡單易懂，可是一旦資料量龐大，這種方式就顯得笨拙，不僅會耗費大量的備份空間，執行備份的時間也耗時最久，在自動化的備份軟體裡，這種完整的備份只會用在第一次備份任務執行時，或者是時間間隔過久時也會做一次完整的備份。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zeng-liang-bei-fen&quot;&gt;增量備份&lt;&#x2F;h2&gt;
&lt;p&gt;第二種增量備份則是每一次備份是一次次的疊加上去，如同前面所言，在第一次執行備份時是採用完全備份，以這個完全備份為基礎，若來源的檔案在這之後被做了修改，在第二次進行增量備份時，它只備份上一次備份之後修改的部份，若在第二次備份後又做了修改，在第三次的增量備份執行時，也只以上一次（第二次）的備份為基礎，只備份修改的部份。增量備份的好處是快，非常快，因為它只備份修改的部份，而非完整的檔案，相對的缺點是回溯較為麻煩，假設你做了十次增量備份，想回溯到第三次，那就得從十九八七一路往回溯到第三次，不過這一切當然是備份軟體會幫你搞定，不用手動進行。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;chai-yi-bei-fen&quot;&gt;差異備份&lt;&#x2F;h2&gt;
&lt;p&gt;第三種差異備份它是以每一次的完全備份為基礎，舉例來說，第一次一樣是完全備份，第二次的差異備份是以第一次的完全備份為基礎，第三次的差異備份還是以第一次的完全備份為基礎。假設在第十一次又做了完全備份，則第十二次的差異備份即以第十一次的完全備份為基礎，以此類推。差異備份是比較調和的一種方案，回溯較增量備份較為簡便，但佔據的空間也比較大，當然這一切還是備份軟體自動處理的，我們要做的工作可能是餵給它正確的儲存媒體，這部份一般電腦沒辦法代勞。&lt;&#x2F;p&gt;
&lt;p&gt;在了解了這三種備份方式的不同之後，再補充一點，這三種方式都是可以混用的，除了第一次一定要完整的備份一次之外，之後的每一次備份想採用哪種方式都可以，只要了解它們的特性：&lt;strong&gt;完全備份即一比一的完整備份、增量備份是以上一次備份為基礎的備份、差異備份是以上一次完全備份為基礎的備份。&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;time-machine-de-bei-fen-ti-an&quot;&gt;Time Machine 的備份提案&lt;&#x2F;h2&gt;
&lt;p&gt;然而，Time Machine 採取了截然不同的備份方案，它的每一次備份看起來都是完整備份，實際上是類似增量備份與完全備份的混合體（只是類似）。&lt;&#x2F;p&gt;
&lt;p&gt;同樣地，在第一次備份時，它是用完整備份一次，而自此之後的備份類似於增量備份，它會查詢哪個檔案變動過，只拷貝變動過的檔案到備份媒體上，然而對於其它未變動過的檔案，它會自動把那些檔案建立硬連結到每一次備份的目錄內，硬連結是類似替身或捷徑的機制，一份檔案只佔據一份空間，但卻可以位於檔案系統內的多個位置，這稱為多重硬連結，這樣一來就可以達到只備份變動過的檔案，但看起來卻像是每一次都是完整備份一樣，這種方式對我們日後操作而言是相當直覺的，進入備份媒體的目錄內，每一次備份的目錄都保持絕對的完整而不零碎，當然實際上不需要這麼做，直接使用 Time Machine 的介面去操作會簡單的多。&lt;&#x2F;p&gt;
&lt;p&gt;Time Machine 可說是兼顧了佔用空間大小和回溯的簡易性，並且它也全自動的幫我們維護時間較久前的備份，24 小時範圍內的檔案可以為小時為單為進行回溯、一個月前的檔案以天為單位進行回溯、久於一個月的檔案以週為單為進行回溯。&lt;&#x2F;p&gt;
&lt;p&gt;然而 Time Machine 的這些特色必然也會有它的缺點，以檔案為基礎單位的備份造成每次備份都是拷貝一份完整的變動過的檔案到備份媒體上，這樣的缺點造就了回溯簡便的優點，一失一得之下，我認為 Time Machine 還是優於前文所述的其它三種備份方式。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Vite 與環境變數</title>
        <published>2021-12-24T00:00:00+00:00</published>
        <updated>2021-12-24T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/vite/"/>
        <id>https://editor.leonh.space/2021/vite/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/vite/">&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;cn.vitejs.dev&#x2F;&quot;&gt;Vite&lt;&#x2F;a&gt; 是 Vue.js 作者尤雨溪開發的「下一代前端開發與構建工具」，它也是新興的前端框架 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;kit.svelte.dev&#x2F;&quot;&gt;SvelteKit&lt;&#x2F;a&gt; 預設的構建工具。&lt;&#x2F;p&gt;
&lt;p&gt;在程式專案上，我們往往會把某些重要的資訊（例如後端的 URL、第三方服務的 API 端點等）放在獨立的檔案內做管理，這個檔案習慣上會命名為 .env，再利用某些機制把 .env 內的參數載入到程式中成為可調用的變數，這些變數我們稱為「環境變數」。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;huan-jing-bian-shu&quot;&gt;環境變數&lt;&#x2F;h2&gt;
&lt;p&gt;這些環境變數往往伴隨著專案的運行環境而變，在 Vite 的設計上，已經為我們預留了 development 環境（開發環境）與 production 環境（生產環境），兩者以檔名做區分，依照 Vite 的規範，開發環境的檔名是 .env.development，下面是一個最陽春的例子：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;ini&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# .env.development&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;VITE_BACKEND_HOST&lt;&#x2F;span&gt;&lt;span&gt; = http:&#x2F;&#x2F;localhost:5000&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;而在生產環境也有這個變數，但是值是不一樣的，依照 Vite 的規範，生產環境的檔名是 .env.production，內容如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;ini&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# .env.production&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;VITE_BACKEND_HOST&lt;&#x2F;span&gt;&lt;span&gt; = https:&#x2F;&#x2F;c.herokuapp.com&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;只要把這兩個檔案置於專案的根目錄下，Vite 就會自動載入，要注意的是，必須像上面的範例一樣，在變數前方有 &lt;code&gt;VITE_&lt;&#x2F;code&gt; 的前綴 Vite 才會載入，否則會無情的忽略。&lt;&#x2F;p&gt;
&lt;p&gt;當執行 &lt;code&gt;vite dev&lt;&#x2F;code&gt; 時，會載入 .evn.development；當執行 &lt;code&gt;vite build&lt;&#x2F;code&gt; 或 &lt;code&gt;vite preview&lt;&#x2F;code&gt; 時，則會載入 .env.production。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;diao-yong-huan-jing-bian-shu&quot;&gt;調用環境變數&lt;&#x2F;h2&gt;
&lt;p&gt;延續上面的例子，在程式中，我們用 &lt;code&gt;import.meta.env.VITE_BACKEND_HOST&lt;&#x2F;code&gt; 就可調用該環境變數，超級簡單。&lt;&#x2F;p&gt;
&lt;p&gt;除了我們自行定義的環境變數，Vite 還有內建四個它設定的環境變數讓我們運用：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;import.meta.env.MODE&lt;&#x2F;code&gt;：应用运行的模式。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;import.meta.env.BASE_URL&lt;&#x2F;code&gt;：部署应用时的基本 URL。它由 &lt;code&gt;base&lt;&#x2F;code&gt; 配置项决定。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;import.meta.env.PROD&lt;&#x2F;code&gt;：应用是否运行在生产环境。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;import.meta.env.DEV&lt;&#x2F;code&gt;：应用是否运行在开发环境（永远与 &lt;code&gt;import.meta.env.PROD&lt;&#x2F;code&gt; 相反）。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;以上抄錄自《Vite 官方中文文档》。&lt;&#x2F;p&gt;
&lt;p&gt;實際用起來的範例：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;console.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;log&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;import&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;meta&lt;&#x2F;span&gt;&lt;span&gt;.env.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;VITE_BACKEND_HOST&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果是在 SvelteKit 元件內則是：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;svelte&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;p&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;{&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;meta&lt;&#x2F;span&gt;&lt;span&gt;.env.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;VITE_BACKEND_HOST&lt;&#x2F;span&gt;&lt;span&gt; }&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;p&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其他前端框架應該也是類似的用法。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;bian-yi-yu-huan-jing-bian-shu&quot;&gt;編譯與環境變數&lt;&#x2F;h2&gt;
&lt;p&gt;預設情況下，Vite 會根據子命令而判斷當前是 development 還是 production。&lt;&#x2F;p&gt;
&lt;p&gt;如果是執行 &lt;code&gt;npx vite&lt;&#x2F;code&gt; 這會啟動 Vite 的開發伺服器，此時會載入 .env.development 內之變數。&lt;&#x2F;p&gt;
&lt;p&gt;如果是執行 &lt;code&gt;npx vite build&lt;&#x2F;code&gt; 則會進行 production 建置，換言之會載入的是 .env.production 內之變數。&lt;&#x2F;p&gt;
&lt;p&gt;有時候兩個環境不夠用，可能還會有個介於 development 和 production 之間的 staging 環境，此時可以自行追加一個 .env.staging 檔案，並且以 &lt;code&gt;npx vite build --mode staging&lt;&#x2F;code&gt; 的方式去建置，此時 Vite 就會去載入 .env.staging 囉！&lt;&#x2F;p&gt;
&lt;p&gt;這幾個命令也可以寫入 package.json 方便大家使用：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;json&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  &amp;quot;scripts&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;dev&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;vite&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;build:production&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;vite build&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;build:staging&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;vite build --mode staging&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;preview&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;vite preview --port 5173&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;帥吧！&lt;&#x2F;p&gt;
&lt;h2 id=&quot;an-quan-shi-xiang&quot;&gt;安全事項&lt;&#x2F;h2&gt;
&lt;p&gt;前端專案意味著運行的環境是在用戶的瀏覽器，也意味著所有程式內的參數是用戶可見的（即便有混淆過），所以不適合放任何的連線帳密等私密資料，這些私密的串接，建議放到後端去做，方為上策。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;can-kao-zi-liao&quot;&gt;參考資料&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;《&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;cn.vitejs.dev&#x2F;&quot;&gt;Vite 官方中文文档&lt;&#x2F;a&gt;》&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;dev.to&#x2F;danawoodman&#x2F;storing-environment-variables-in-sveltekit-2of3&quot;&gt;Using environment variables in SvelteKit (and Vite)&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;timdeschryver.dev&#x2F;blog&#x2F;environment-variables-with-sveltekit&quot;&gt;Environment variables with SvelteKit&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>極簡 nvm 使用指南</title>
        <published>2021-12-22T00:00:00+00:00</published>
        <updated>2021-12-22T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/nvm/"/>
        <id>https://editor.leonh.space/2021/nvm/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/nvm/">&lt;style&gt;
    .ra-button {
        padding: .3em .9em;
        border-radius: .25em;
        background: linear-gradient(#fff, #efefef);
        box-shadow: 0 1px .2em gray;
        display: inline-flex;
        align-items: center;
        cursor: pointer;
    }

    .ra-button img {
        height: 1em; 
        margin: 0 .5em 0 0;
    }
&lt;&#x2F;style&gt;
&lt;!-- &lt;div id=&quot;ra-player&quot; data-skin=&quot;https:&#x2F;&#x2F;assets.sitespeaker.link&#x2F;embed&#x2F;skins&#x2F;default&quot;&gt;
    &lt;div class=&quot;ra-button&quot; onclick=&quot;readAloud(document.getElementById(&#x27;ra-audio&#x27;), document.getElementById(&#x27;ra-player&#x27;))&quot;&gt;
        &lt;img src=&quot;https:&#x2F;&#x2F;assets.sitespeaker.link&#x2F;embed&#x2F;skins&#x2F;default&#x2F;play-icon.png&quot;&#x2F;&gt; Listen to this article
    &lt;&#x2F;div&gt;
&lt;&#x2F;div&gt;
&lt;audio id=&quot;ra-audio&quot; data-lang=&quot;cmn-TW&quot; data-voice=&quot;Google cmn-TW-Standard-A&quot; data-key=&quot;8a00c94a05eb41e3ff7cf96de4a37443&quot;&gt;&lt;&#x2F;audio&gt;
&lt;script&gt;
    function readAloud(au,pl){
        var bs=&quot;https:&#x2F;&#x2F;assets.sitespeaker.link&#x2F;embed&#x2F;&quot;;
        &#x2F;iPad|iPhone|iPod&#x2F;.test(navigator.userAgent)&amp;&amp;(au.src=bs+&quot;sound&#x2F;silence.mp3&quot;,au.play());
        var xhr=new XMLHttpRequest;xhr.open(&quot;GET&quot;,bs+&quot;js&#x2F;readaloud.min.js?ajax=1&quot;,!0),xhr.onreadystatechange=function(){
            4==xhr.readyState&amp;&amp;200==xhr.status&amp;&amp;(eval(xhr.responseText),readAloudInit(au,pl))
        },xhr.send(null)
    }
&lt;&#x2F;script&gt; --&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;nvm-sh&#x2F;nvm&quot;&gt;nvm&lt;&#x2F;a&gt; 是 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;nodejs.org&#x2F;zh-tw&#x2F;&quot;&gt;Node.js&lt;&#x2F;a&gt; 的多版本管理器，當你的開發環境有多個不同時代的 Node.js 專案、橫跨不同版次的 Node.js 時，nvm 就派得上用場。&lt;&#x2F;p&gt;
&lt;p&gt;Node.js 版本管理器不只有 nvm，由於 nvm 只支援 macOS 與 Linux，在 Windows 下，改用 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;volta.sh&#x2F;&quot;&gt;Volta&lt;&#x2F;a&gt; 或 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;jasongin&#x2F;nvs&quot;&gt;NVS&lt;&#x2F;a&gt; 是更好的選擇。（也可參閱另一篇〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;windows-python-node-js&#x2F;&quot;&gt;在 Windows 建置以 Visual Studio 為基礎的 Python &#x2F; Node.js 開發環境&lt;&#x2F;a&gt;〉&lt;&#x2F;p&gt;
&lt;p&gt;nvm 的問題是，文件與說明又臭又長，其實 80% 的人只用的到 20% 的功能，這裡只寫個人常用到的功能與指令。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;geng-xin-nvm&quot;&gt;更新 nvm&lt;&#x2F;h2&gt;
&lt;p&gt;更新 nvm 與從零安裝 nvm 是同一條命令：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; curl&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -o-&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; https:&#x2F;&#x2F;raw.githubusercontent.com&#x2F;nvm-sh&#x2F;nvm&#x2F;v0.39.3&#x2F;install.sh&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; |&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; bash&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這個安裝腳本會自行判斷是新裝還是更新。&lt;&#x2F;p&gt;
&lt;p&gt;要注意網址內有 nvm 的版號，更新 nvm 時記得帶入新版號。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;geng-xin-npm&quot;&gt;更新 npm&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.npmjs.com&#x2F;&quot;&gt;npm&lt;&#x2F;a&gt; 是 Node.js 的套件管理器，它也是需要更新的，n&lt;strong&gt;v&lt;&#x2F;strong&gt;m 有專門更新 n&lt;strong&gt;p&lt;&#x2F;strong&gt;m 的指令：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; nvm install-latest-npm&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;lie-chu-xi-tong-nei-ge-node-js-ban-ben&quot;&gt;列出系統內各 Node.js 版本&lt;&#x2F;h2&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; nvm ls&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;以本人的環境為例，有列出下列版本：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;       v16.13.1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;-&amp;gt;     v18.16.0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;default -&amp;gt; 18.16.0 (-&amp;gt; v18.16.0)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;iojs -&amp;gt; N&#x2F;A (default)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;unstable -&amp;gt; N&#x2F;A (default)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;node -&amp;gt; stable (-&amp;gt; v18.16.0) (default)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;stable -&amp;gt; 18.16 (-&amp;gt; v18.16.0) (default)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;lts&#x2F;* -&amp;gt; lts&#x2F;hydrogen (-&amp;gt; v18.16.0)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;lts&#x2F;argon -&amp;gt; v4.9.1 (-&amp;gt; N&#x2F;A)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;lts&#x2F;boron -&amp;gt; v6.17.1 (-&amp;gt; N&#x2F;A)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;lts&#x2F;carbon -&amp;gt; v8.17.0 (-&amp;gt; N&#x2F;A)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;lts&#x2F;dubnium -&amp;gt; v10.24.1 (-&amp;gt; N&#x2F;A)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;lts&#x2F;erbium -&amp;gt; v12.22.12 (-&amp;gt; N&#x2F;A)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;lts&#x2F;fermium -&amp;gt; v14.21.3&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;lts&#x2F;gallium -&amp;gt; v16.20.0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;lts&#x2F;hydrogen -&amp;gt; v18.16.0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;看似很亂，其實大部份情況下只要關注那 &lt;code&gt;default&lt;&#x2F;code&gt; 的版號即可。&lt;&#x2F;p&gt;
&lt;p&gt;那些 &lt;code&gt;default&lt;&#x2F;code&gt;、&lt;code&gt;iojs&lt;&#x2F;code&gt; 等等的是 alias，可以理解為標籤，&lt;code&gt;default&lt;&#x2F;code&gt; 標定的版號就是個人帳戶下預設的 Node.js 版號。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;lie-chu-ke-an-zhuang-de-node-js-lts-ban-ben&quot;&gt;列出可安裝的 Node.js LTS 版本&lt;&#x2F;h2&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; nvm ls-remote&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --lts&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;偶數的 LTS 版本們有著更長的維護期：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;nvm&#x2F;schedule.svg&quot; alt=&quot;Node.js Releases Schedule&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：&lt;a href=&quot;https:&#x2F;&#x2F;nodejs.org&#x2F;zh-tw&#x2F;about&#x2F;releases&#x2F;&quot;&gt;Node.js&lt;&#x2F;a&gt;&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;看看那短小的奇數版 Node.js 19…建議都使用偶數的 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;nodejs.org&#x2F;zh-tw&#x2F;about&#x2F;releases&#x2F;&quot;&gt;LTS&lt;&#x2F;a&gt; 版本，避免追著版號跑的窘境發生。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;an-zhuang-node-js&quot;&gt;安裝 Node.js&lt;&#x2F;h2&gt;
&lt;p&gt;安裝目前的 18.16.0 LTS 版，並且設為預設版次：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; nvm install&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 18.16.0 --default&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;建議都使用 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;nodejs.org&#x2F;zh-tw&#x2F;about&#x2F;releases&#x2F;&quot;&gt;LTS&lt;&#x2F;a&gt; 版本，避免追著版號跑的窘境發生。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;she-ding-yu-she-ban-ben&quot;&gt;設定預設版本&lt;&#x2F;h2&gt;
&lt;p&gt;同樣的以 18.16.0 LTS 版為例：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; nvm alias default&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 18.16.0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;wei-zhuan-an-zhi-ding-node-js-ban-ben&quot;&gt;為專案指定 Node.js 版本&lt;&#x2F;h2&gt;
&lt;p&gt;在專案的資料夾內，放一個 .nvmrc 檔案，在裡面寫下該專案的 Node.js 版號，例如某專案要用 18.16：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;18.16&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;進入該專案資料夾後，執行 &lt;code&gt;nvm use&lt;&#x2F;code&gt; 即會自動切換成 .nvmrc 指定的版本。&lt;&#x2F;p&gt;
&lt;p&gt;如果在安裝 nvm 時，有設定好 shell 整合的話，應該會自動幫我們做 &lt;code&gt;nvm use&lt;&#x2F;code&gt; 的動作。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>你的程式是資產還是負債？</title>
        <published>2021-12-15T00:00:00+00:00</published>
        <updated>2021-12-15T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/how-to-treat-your-app-as-asset/"/>
        <id>https://editor.leonh.space/2021/how-to-treat-your-app-as-asset/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/how-to-treat-your-app-as-asset/">&lt;blockquote&gt;
&lt;div class=&quot;quote&quot;&gt;
&lt;p&gt;Your product is an asset, but code is a liability.&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
&lt;div class=&quot;source&quot;&gt;
&lt;p&gt;Martin Rue&lt;wbr&gt;〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;martinrue.com&#x2F;my-engineering-axioms&#x2F;&quot;&gt;My Engineering Axioms&lt;&#x2F;a&gt;〉&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;在幾年以前，我曾經陸續讀到過幾個相似的概念——&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;5S_(%E7%AE%A1%E7%90%86)&quot;&gt;5S&lt;&#x2F;a&gt;、&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E6%96%AD%E8%88%8D%E7%A6%BB&quot;&gt;斷捨離&lt;&#x2F;a&gt;、《&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;shop.tcsb.com.tw&#x2F;zh-TW&#x2F;ste&#x2F;product?saleid=11020020005035&quot;&gt;怦然心動的人生整理魔法&lt;&#x2F;a&gt;》，它們在我心中逐漸建構起一個共同的核心觀念——「物品的價值在於被有效的使用」，這個概念成立的基礎在於人們能掌握的資源是有限的，特別是時間與空間。當一個物件被閒置在某個角落，而那個堆雜物的角落假設佔了一坪的空間，那麼相當於我花了成本相當於市價一坪的錢買了那塊地，上頭卻供奉著僅僅是被歸類於雜物的物件們，他們紮紮實實的佔據了那一坪空間不是嗎？也因此你與家人的走動空間也確實少了那一坪沒錯吧！&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;how-to-treat-your-app-as-asset&#x2F;IMG_4704.JPG&quot; alt=&quot;斷捨離&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;前面提到的概念，在個人的階段，如同斷捨離所倡導的，「斷絕不需要的東西；捨去多餘的事物；脫離對物品的執著」，具體的行為如《怦然心動的人生整理魔法》所實踐的——取出那些閒置物件，一件一件思考它對你的意義，如果它不再令你感到「怦然心動」，那麼就謝謝它曾經帶給你的意義與價值，然後就妥善地將它交給回收車，告別彼此展開新的生活。&lt;&#x2F;p&gt;
&lt;p&gt;如果我們把層次拉高到社會的層面，也會發現社會中也有類似的角落，試著回想你家附近是否曾經有過一個小孩不敢進去的公園，又或者是蓋到一半的爛尾樓、發生過火災的猛鬼大樓等，這些閒置設施（甚至是嫌惡設施）不論是私有的或公家的，顯然不像個人物件那樣能被輕易的斷捨離，必須採取斷捨離以外的手段，像是「&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;opinion.cw.com.tw&#x2F;blog&#x2F;profile&#x2F;411&#x2F;article&#x2F;6945&quot;&gt;都市更新&lt;&#x2F;a&gt;」與「&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www2.deloitte.com&#x2F;tw&#x2F;tc&#x2F;pages&#x2F;risk&#x2F;articles&#x2F;assets-smart-city.html&quot;&gt;資產活化&lt;&#x2F;a&gt;」都是常看到的做法，政府藉由推動都市更新，改建老舊建物，不僅提升城市形象，對民眾來說生活品質也有所提升，更有感的可能是區段地價房價的上漲，對政府來說則是經濟活動活絡之後帶來的人口與稅收的增加。資產活化則常見於閒置廠房土地的重建，賦予不動產新的附加價值，帶來經濟上（&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;udn.com&#x2F;news&#x2F;story&#x2F;7241&#x2F;5172530&quot;&gt;永豐餘&lt;&#x2F;a&gt;）或文化上（&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E8%A5%BF%E9%96%80%E7%B4%85%E6%A8%93&quot;&gt;西門紅樓&lt;&#x2F;a&gt;、&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E6%9D%BE%E5%B1%B1%E6%96%87%E5%89%B5%E5%9C%92%E5%8D%80&quot;&gt;松菸&lt;&#x2F;a&gt;、&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E6%9E%97%E7%99%BE%E8%B2%A8&quot;&gt;林百貨&lt;&#x2F;a&gt;）的收益。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;cheng-shi-shi-zi-chan-huan-shi-fu-zhai&quot;&gt;程式是資產還是負債？&lt;&#x2F;h2&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;how-to-treat-your-app-as-asset&#x2F;piret-ilver-98MbUldcDJY-unsplash.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;&lt;a href=&quot;https:&#x2F;&#x2F;unsplash.com&#x2F;photos&#x2F;98MbUldcDJY&quot;&gt;來源：Piret Ilver&lt;&#x2F;a&gt;&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;延續前面的概念，對於軟體資產的認定應該是較無懸念的——有開發、有維護、有用戶、有收入的軟體無疑的是資產，我們花費人力與時間付出成本，換得一個能為我們帶進收益的產品或服務，那麼反過來說，哪些程式我們應該認定成負債呢？或認定為較為無害的「閒置資產」呢？儘管很難用單一的標準認定，但這些閒置資產大多有以下特徵：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;無法帶來新客戶，而僅存的舊客戶也只是因為轉換成本過大或沒有特殊誘因所以無法下定決心替換。&lt;&#x2F;li&gt;
&lt;li&gt;在市場上不為人所知，源自於這些程式起初就是為了某個特定單一客戶的單一需求開發的，而當初在開發時沒有設想到降低耦合性的設計，導致只能解決單一問題，無法解決同產業類似的問題，也因此難以往市場推廣。&lt;&#x2F;li&gt;
&lt;li&gt;沒有人用，形成的因素有許多，包括因為當初在開發階段就直接跳過市場調研階段，或者忽視來自市場的反饋，又或者只考慮到&lt;strong&gt;功能面&lt;&#x2F;strong&gt;沒考慮到&lt;strong&gt;應用面&lt;&#x2F;strong&gt;或&lt;strong&gt;操作面&lt;&#x2F;strong&gt;或&lt;strong&gt;人性面&lt;&#x2F;strong&gt;，又或者只考慮到功能面卻又想做的太多導致 time to market 時間被拉太長，導致錯過最佳上市時機。&lt;&#x2F;li&gt;
&lt;li&gt;半成品，不論是專案或 POC，都還沒養成到能稱為&lt;strong&gt;產品&lt;&#x2F;strong&gt;的階段就被腰斬，被腰斬的原因有很多，包括資源的缺乏、時機的錯失、外在的干擾、三分鐘熱度等。&lt;&#x2F;li&gt;
&lt;li&gt;缺乏&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;people.cs.nctu.edu.tw&#x2F;~chlo&#x2F;web&#x2F;docs&#x2F;doc&#x2F;data&#x2F;se&#x2F;5.htm&quot;&gt;整體概念性&lt;&#x2F;a&gt;，整體概念性來自軟體專案管理名著《&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;zh-tw&#x2F;%E4%BA%BA%E6%9C%88%E7%A5%9E%E8%AF%9D&quot;&gt;人月神話&lt;&#x2F;a&gt;》，意指貫穿整個產品的一致性的概念，概念泛指視覺風格、操作體驗、介面元件、使用流程的綜合感受，而由於現代軟體工程的複雜，以及專案角色的模糊，導致整體概念性不易被建構，最終獲得的就是能用，但不好用的半成品。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;綜合以上，程式雖然並不佔據物理的空間，但負債程式帶來的危害卻更大，因為它佔據的是時間，空間可以被清理，但時間過去就是過去了，而閒置程式的另一項特質是它並不是放著不管也不會怎樣的物件，而是一系列需要被維護和除錯的程式碼構成的（記得那些還在使用的老用戶嗎？）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wei-lao-cheng-shi-de-huo-hua-ti-an&quot;&gt;危老程式的活化提案&lt;&#x2F;h2&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;how-to-treat-your-app-as-asset&#x2F;clark-tibbs-oqStl2L5oxI-unsplash.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;&lt;a href=&quot;https:&#x2F;&#x2F;unsplash.com&#x2F;photos&#x2F;oqStl2L5oxI&quot;&gt;來源：Clark Tibbs&lt;&#x2F;a&gt;&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;想要提升危老程式的資產價值，就必須採取有效的活化策略，包括：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;對既有客戶提出專換到新系統的誘因，這樣做的好處是多元的，讓新架構的新系統獲得既有的穩定客戶，也可以讓舊系統退役，降低維護所需要的成本，對客戶來說也可以用優惠的方案取得一套新的系統。&lt;&#x2F;li&gt;
&lt;li&gt;賦予產品更多的附加價值，包括品牌、形象、行銷等面向的資源投注，這幾樣元素很容易被誤解組成產品次要的部分，但實際上真正能打動客戶的往往就在於這些元素，而這其中包括了公司形象的建立。&lt;&#x2F;li&gt;
&lt;li&gt;把專案或 POC 的目標放在&lt;strong&gt;驗證市場&lt;&#x2F;strong&gt;，而不是&lt;strong&gt;驗證功能&lt;&#x2F;strong&gt;，你設想好的功能可能根本就不是市場要的，要嘛在實作前先調研市場需求，並且小心地不讓規格外溢成過度設計的樣子，要嘛就先做再接受來自市場反饋的意見，否則下面這張圖的狀況就會一再重演，而消耗的一樣是最寶貴的&lt;strong&gt;時間&lt;&#x2F;strong&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;how-to-treat-your-app-as-asset&#x2F;1_nGCtT_4C-tMubAwS3S0XXQ.png&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-yu&quot;&gt;結語&lt;&#x2F;h2&gt;
&lt;p&gt;回到最初的核心精神「物品的價值在於被有效的使用」，進而產生經濟價值，這樣的程式我們視為資產，反之，那些被深埋在硬碟角落的程式，無論它的功能有多麽的高大上，只要它沒有被有效使用，那它就是負債，因為你終究是花了時間去做它，付出的時間成本卻沒有獲得任何有顯著意義的收入，這樣的程式我們視為負債，然而負債也是能翻身的，前文提到幾個活化手段，不論手段為何，終究是圍繞著「提高產品的附加價值」這個核心觀念打轉，就像把企業把閒置廠房整地重塑一樣，賦予資產新的價值，對軟體這樣的無形資產來說，價值鏈的構成除了功能本身，用戶更能感受到的是產品本身的操作體驗，以及製作公司本身的企業形象，試問自己，當你看到迪士尼＋漫威影業的時候你心裡對他們製作的電影的預期是什麼，而你又希望顧客看到你的網站或 FB 專頁時能獲得什麼樣的印象或資訊？&lt;&#x2F;p&gt;
&lt;p&gt;做為一個產品開發人，常常會落入只追求到 &lt;strong&gt;know-how&lt;&#x2F;strong&gt; 的思維，卻忘了要去思考 &lt;strong&gt;know-what&lt;&#x2F;strong&gt; 以及 &lt;strong&gt;know-why&lt;&#x2F;strong&gt;，或許是被「開發」兩個字所制約，但我們更應該廣義的解讀「開發」，不僅是指「程式開發」，而應該擴大到「產品開發」，而程式只是構成整個產品的一部份，就如同建築的骨架，穩固的骨架當然是個賣點，但客戶不為因為骨架很穩就買單，他們對空間的機能性、外觀、生活機能性、交通等元素都同樣重視，同樣的軟體的用戶也不會只因為你的演算法多麽高大上而買單，他們對操作的便利性和美觀也同樣重視，另外他們也會感受公司與公司代表所呈現的形象，而這些元素都可以成為資產活化的方案之一，別忘了所謂的無形資產不僅是程式，也包括公司的品牌與形象，所謂的產品開發也包括公司的形象構建與傳播——如果你也想認真經營公司的話。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Cloudflare Tunnel 簡介</title>
        <published>2021-12-14T00:00:00+00:00</published>
        <updated>2021-12-14T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/cloudflare-tunnel/"/>
        <id>https://editor.leonh.space/2021/cloudflare-tunnel/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/cloudflare-tunnel/">&lt;p&gt;以前曾經寫過一篇〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;ngrok&#x2F;&quot;&gt;ngrok 讓本機發佈出可被訪問的網址&lt;&#x2F;a&gt;〉，這次要介紹的是類似的工具 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.cloudflare.com&#x2F;zh-tw&#x2F;products&#x2F;tunnel&#x2F;&quot;&gt;Cloudflare Tunnel&lt;&#x2F;a&gt;，他們有著共同的功能：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;我們有個本機的服務跑在 http:&#x2F;&#x2F;localhost:5000，想讓外部訪問&lt;&#x2F;li&gt;
&lt;li&gt;Cloudflare Tunnel 產生公共網址以及 Cloudflare 節點與本機間的「tunnel」&lt;&#x2F;li&gt;
&lt;li&gt;訪客訪問網址，Cloudflare 節點把請求透過 tunnel 轉送到我們本機的服務，並且把服務的回覆也透過 tunnel 反向的回傳&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;Cloudflare Tunnel（也稱為 Argo Tunnel）除了名字太長這個問題外，有著這些優勢：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Cloudflare Tunnel 與 ngrok 一樣是免費增值型服務，上述基本的 Cloudflare Tunnel 功能完全免費，進階的 Argo 則是收費的&lt;&#x2F;li&gt;
&lt;li&gt;Cloudflare Tunnel 可以搭配 Cloudflare 自家的 DNS 服務使用，也就是可以用自有的網址，而這在 ngrok 是要收費的&lt;&#x2F;li&gt;
&lt;li&gt;Cloudflare Tunnel 的服務會自動就近分配節點位置，而 ngrok 需要手動指定&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;an-zhuang-cloudflare-tunnel&quot;&gt;安裝 Cloudflare Tunnel&lt;&#x2F;h2&gt;
&lt;p&gt;使用 Cloudflare Tunnel 的前提當然是要有 Cloudflare 帳號，以及以 Cloudflare DNS 託管的網域，除這兩點外，我們得在主機上安裝 Cloudflare Tunnel 的代理程式 &lt;code&gt;cloudflared&lt;&#x2F;code&gt;，安裝檔參見 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;developers.cloudflare.com&#x2F;cloudflare-one&#x2F;connections&#x2F;connect-apps&#x2F;install-and-setup&#x2F;installation&quot;&gt;&lt;code&gt;cloudflared&lt;&#x2F;code&gt; 的下載頁&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;deng-ru&quot;&gt;登入&lt;&#x2F;h2&gt;
&lt;p&gt;初次使用時，須執行一次登入，讓 &lt;code&gt;cloudflared&lt;&#x2F;code&gt; 取得我們帳號的授權：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; cloudflared tunnel login&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;執行後會開啟授權網頁，選擇我們要授權 &lt;code&gt;cloudflared&lt;&#x2F;code&gt; 操作的網域，授權成功後，會獲得一個代表我們帳號的憑證檔在 ~&#x2F;.cloudflared&#x2F;cert.pem，後續的操作 &lt;code&gt;cloudflared&lt;&#x2F;code&gt; 會自動以此為證與 Cloudflare 服務互動。&lt;&#x2F;p&gt;
&lt;p&gt;為了方便後續說明，在此我們假設授權的網域是 cccc.ws。&lt;&#x2F;p&gt;
&lt;p&gt;如果 cccc.ws 上面已經有既有的 DNS 紀錄，並不會因為此動作而有被改掉的危機，不用擔心。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jian-li-tunnel&quot;&gt;建立 Tunnel&lt;&#x2F;h2&gt;
&lt;p&gt;幫 tunnel 取個名字，這裡叫 local：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; cloudflared tunnel create local&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這只是在 Cloudflare 建一個空的 tunnel 紀錄在案，還沒有真正可以連線的 tunnel。&lt;&#x2F;p&gt;
&lt;p&gt;要檢視自身旗下所有 tunnel，使用命令 &lt;code&gt;list&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; cloudflared tunnel list&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;會看到如下的輸出：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;You can obtain more detailed information for each tunnel with `cloudflared tunnel info &amp;lt;name&#x2F;uuid&amp;gt;`&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;ID                                   NAME   CREATED              CONNECTIONS&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;e6b1ea74-e6ad-4e36-b8b5-0f8f81ccafec local  2023-02-05T16:29:33Z&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;9ea66384-6ec7-4b6c-9a05-e57c08a32526 soda   2022-05-26T01:46:42Z&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;bang-ding-tunnel-yu-wang-zhi&quot;&gt;綁定 Tunnel 與網址&lt;&#x2F;h2&gt;
&lt;p&gt;目前 tunnel 還沒有網址，給它一個：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; cloudflared tunnel route dns local local.cccc.ws&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;當然一般而言會把兩者取相同的名字，這步顯得有點多此一舉。&lt;&#x2F;p&gt;
&lt;p&gt;如果我們手賤，原本 DNS 就有登錄一筆 existed-local.cccc.ws，卻又想用它綁定 tunnel：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; cloudflared tunnel route dns local existed-local.cccc.ws&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;那 Cloudflare 會阻止我們這麼做：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Failed to add route: code: 1003, reason: An A, AAAA, or CNAME record with that host already exists.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;lian-jie-ben-di-fu-wu&quot;&gt;連接本地服務&lt;&#x2F;h2&gt;
&lt;p&gt;有了 tunnel、有了網址，只剩把服務連上 tunnel 了。&lt;&#x2F;p&gt;
&lt;p&gt;假設我們的機台上有個服務跑在 http:&#x2F;&#x2F;localhost:1111，想要利用 Cloudflare Tunnel 讓外部人士用 https:&#x2F;&#x2F;local.cccc.ws 訪問，最簡單的一行指令如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; cloudflared tunnel run&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --url&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; http:&#x2F;&#x2F;localhost:1111 local&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;執行後下面會跑出一堆嘰哩呱啦的訊息，只要沒有紅字應該就是正常：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2023-02-05T16:50:52Z INF Connection e4fe5a22-1e6e-4a05-b24c-95fc3ec62501 registered with protocol: quic connIndex=0 ip=198.41.200.23 location=TPE&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2023-02-05T16:50:52Z INF Connection 57570b20-0c60-472b-bfac-eeebe45912ff registered with protocol: quic connIndex=1 ip=198.41.192.227 location=HKG&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2023-02-05T16:50:52Z INF Connection 02f8e0ad-dfca-499b-af48-21a8bbfa1b47 registered with protocol: quic connIndex=2 ip=198.41.200.43 location=TPE&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;2023-02-05T16:50:52Z INF Connection 39c656ce-e08d-49a7-9cfb-1d05d92228fc registered with protocol: quic connIndex=3 ip=198.41.192.27 location=NRT&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;照紀錄看起來，我們分配到的節點分別位於台北（TPE）、東京（NRT）、香港（HKG），如同最開始所說的，Cloudflare 會自動就近分配 tunnel 節點位置。&lt;&#x2F;p&gt;
&lt;p&gt;跑起來後在 Cloudflare DNS 頁面的確有多了一筆 local.cccc.ws 的紀錄。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;guan-bi-tunnel&quot;&gt;關閉 Tunnel&lt;&#x2F;h2&gt;
&lt;p&gt;對剛剛在跑著 &lt;code&gt;cloudflared&lt;&#x2F;code&gt; 的 terminal（或 console，不論怎麼叫）按下 &lt;kbd&gt;CTRL-C&lt;&#x2F;kbd&gt; 即可中斷 tunnel。&lt;&#x2F;p&gt;
&lt;p&gt;此時若去看 Cloudflare DNS 會發現那筆 local.cccc.ws 還是在，但是確實已經連不到了，此時需要手動砍掉這筆遺留的紀錄（或者射後不理也無妨）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;qi-ta-hua-shi-wan-fa&quot;&gt;其他花式玩法&lt;&#x2F;h2&gt;
&lt;p&gt;前文所提的命令列用法較適合臨時性的應用，例如開發中的專案需要給手機測試、對外展示等，從 &lt;code&gt;cloudflared&lt;&#x2F;code&gt; 的名字可以推測的出來，字尾的「d」是 daemon 的意思，因此它也可以是服務，配合 tunnel 配置文件，讓開機完就把 tunnel 跑起來，除此之外，除了最典型的轉送給本地 HTTP 服務外，Cloudflare Tunnel 也可以轉送給本地 Unix socket 或者 IP，對 Cloudflare Tunnel 的其他花式玩法有興趣的朋友可參閱〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;blog.cloudflare.com&#x2F;argo-tunnels-that-live-forever&#x2F;&quot;&gt;Argo Tunnels that live forever&lt;&#x2F;a&gt;〉一文。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;clouflare-tunnel-de-an-quan-you-shi&quot;&gt;Clouflare Tunnel 的安全優勢&lt;&#x2F;h2&gt;
&lt;p&gt;對於展示、測試開發中專案來說，搭建的 tunnel 往往是臨時性的，可能不用太在意安全性的部份，但 Cloudflare Tunnel 的野心不僅於此，他們希望可以將此技術應用在真實的服務上，帶給服務更多的安全保護，因為覺得滿好的所以也順帶寫一下。（寫這麼多 Cloudflare 會給我業配嗎？🤔）&lt;&#x2F;p&gt;
&lt;p&gt;下面的示意圖是未使用 Cloudflare Tunnel 前的一個典型的 DNS A record 訪問模型：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;cloudflare-tunnel&#x2F;BDES-1096_Argo-Tunnel-Diagram_1@3x-2.png&quot; alt=&quot;Cloudflare Tunnel&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：〈&lt;a href=&quot;https:&#x2F;&#x2F;blog.cloudflare.com&#x2F;argo-tunnels-that-live-forever&#x2F;&quot;&gt;Argo Tunnels that live forever&lt;&#x2F;a&gt;〉&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;正常的訪客透過 example.com 訪問我們的服務，並且透過 Cloudflare 節點對我們後端的 1.2.3.4 做到基本的防護。&lt;&#x2F;p&gt;
&lt;p&gt;但我們服務的 IP 1.2.3.4 仍然是暴露在公網的，所以壞人大猩猩有可能透過一些手段擷取到我們的真實 IP 1.2.3.4，進而展開攻擊。&lt;&#x2F;p&gt;
&lt;p&gt;而在 Cloudflare Tunnel 的連線模型中，一切的傳輸只透過 tunnel，我們的服務始終都是封閉的，不曾暴露於公網，壞人大猩猩最多只能攻擊到 Cloudflare 節點，碰不到我們機台的真身，這可以為我們帶來額外的安全優勢：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;cloudflare-tunnel&#x2F;BDES-1971_Argo-Tunnel-Diagrams_zh-TW.svg&quot; alt=&quot;Cloudflare Tunnel&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：Cloudflare&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;另一方面，Cloudflare 有著全球廣佈的快取節點，訪客會查詢到離他所在位置較近的 Cloudflare 節點，藉此加快訪問速度，而 Cloudflare 節點與我們 tunnel 節點間的流量，則由 Cloudflare 自行導引，就像 Cloudflare 自我宣傳的那樣：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;cloudflare-tunnel&#x2F;cloudflare-global.png&quot; alt=&quot;Cloudflare&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：Cloudflare&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>ngrok 讓本機發佈出可被訪問的網址</title>
        <published>2021-12-09T00:00:00+00:00</published>
        <updated>2021-12-09T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/ngrok/"/>
        <id>https://editor.leonh.space/2021/ngrok/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/ngrok/">&lt;p&gt;常常會遇到需要把本機開發環境的 web app 給別人或自己預覽，甚至開放給外網連入的需求，這時候就需要 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;ngrok.com&#x2F;&quot;&gt;ngrok&lt;&#x2F;a&gt;（發音：&lt;em&gt;en-grok&lt;&#x2F;em&gt;）這樣的工具幫我們搞定這一切。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ngrok-de-te-xing&quot;&gt;ngrok 的特性&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;綁定本機的埠號並接受外部連入。&lt;&#x2F;li&gt;
&lt;li&gt;可穿透 NAT 或防火牆讓外部連入。&lt;&#x2F;li&gt;
&lt;li&gt;會拿到一個 ngork 的網址方便貼到信件或訊息內。&lt;&#x2F;li&gt;
&lt;li&gt;ngrok 產生的網址支援 HTTP &#x2F; HTTPS。&lt;&#x2F;li&gt;
&lt;li&gt;有 web 界面讓我們可以監看連入的請求內容。&lt;&#x2F;li&gt;
&lt;li&gt;可加設 HTTP 帳密認證，避免被不必要的人亂連。&lt;&#x2F;li&gt;
&lt;li&gt;除了支援 HTTP 外，也支援 WebSocket 和 SSH。&lt;&#x2F;li&gt;
&lt;li&gt;可同時發布多個服務。&lt;&#x2F;li&gt;
&lt;li&gt;還支援 API，可以用 API 操控 ngork。&lt;&#x2F;li&gt;
&lt;li&gt;付費方案可以自定網址。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;ngrok-de-gong-zuo-yuan-li&quot;&gt;ngrok 的工作原理&lt;&#x2F;h2&gt;
&lt;p&gt;其實 ngrok 並未真正的穿透 NAT，它只是接受 ngork cloud 機台轉發來的請求並做出回應，而那個 ngrok 的網址其實也就是連到 ngrok cloud 的網址。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;an-zhuang-yu-chu-shi-hua&quot;&gt;安裝與初始化&lt;&#x2F;h2&gt;
&lt;p&gt;根據 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;ngrok.com&#x2F;download&quot;&gt;Install ngrok&lt;&#x2F;a&gt; 頁面的指南，使用適合自己的方式安裝 ngrok。&lt;&#x2F;p&gt;
&lt;p&gt;開始前必須先註冊帳號，在 ngrok 的網站登入後會看到一組 token：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;ngrok&#x2F;authtoken.png&quot; alt=&quot;ngrok authtoken&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;拿這串 token 來做登入：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;ngrok&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; authtoken&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;TOKE&lt;&#x2F;span&gt;&lt;span&gt;N&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;正確的話應該會看到一行寫著 authtoken 已被寫入 ngrok.yml 的訊息：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Authtoken saved to configuration file: &#x2F;home&#x2F;leon&#x2F;.ngrok2&#x2F;ngrok.yml&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;登入只要做一次就好，有這組 authtoken 才能去向 ngrok 伺服器要到一些後面操作會用到的的基本權限。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shi-yong&quot;&gt;使用&lt;&#x2F;h2&gt;
&lt;p&gt;假設電腦內已經有某個監聽 1111 埠的服務，例如用 &lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2023&#x2F;caddy&#x2F;&quot;&gt;Caddy server&lt;&#x2F;a&gt; 提供的靜態網頁，而我們想要用 ngrok 讓 1111 埠也可以對外服務：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;ngrok&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; http&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1111&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;此時會出現下面的狀態畫面：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;ngrok by @inconshreveable&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Session Status      online&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Account             Leon (Plan: Free)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Version             2.3.40&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Region              United States (us)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Web Interface       http:&#x2F;&#x2F;127.0.0.1:4040&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Forwarding          http:&#x2F;&#x2F;1b0f.ngrok.io -&amp;gt; http:&#x2F;&#x2F;localhost:1111&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Forwarding          https:&#x2F;&#x2F;1b0f.ngrok.io -&amp;gt; http:&#x2F;&#x2F;localhost:1111&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;從上面的狀態畫面可以得知一些訊息，包括有一組 Web Interface，以及對外提供服務的 HTTP &#x2F; HTTPS 網址。&lt;&#x2F;p&gt;
&lt;p&gt;是不是很簡單。&lt;&#x2F;p&gt;
&lt;p&gt;從上面可以看到，預設的節點是在美國，訪客透過 ngrok.io 網址連線會先到美國的節點，節點再把請求傳送到我們位於台灣的電腦，回覆的路徑也是相仿，如果追求更快的連線，可以改用 ngrok 位於東京的節點，指令稍微改一下即可：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;ngrok&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; http&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -region&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; jp&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1111&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;從狀態就可以看到變成東京節點了：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;ngrok by @inconshreveable&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Session Status      online&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Account             Leon (Plan: Free)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Version             2.3.40&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Region              Japan (jp)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Web Interface       http:&#x2F;&#x2F;127.0.0.1:4040&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Forwarding          http:&#x2F;&#x2F;e25b.jp.ngrok.io -&amp;gt; http:&#x2F;&#x2F;localhost:1111&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Forwarding          https:&#x2F;&#x2F;e25b.jp.ngrok.io -&amp;gt; http:&#x2F;&#x2F;localhost:1111&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;jian-kong-qing-qiu&quot;&gt;監控請求&lt;&#x2F;h2&gt;
&lt;p&gt;在 ngrok 執行時的狀態訊息內，除了有網址之外，還有一個 &lt;a rel=&quot;external&quot; href=&quot;http:&#x2F;&#x2F;127.0.0.1:4040&quot;&gt;Web Interface&lt;&#x2F;a&gt;，我們可以透過這個 Web Interface 來監控往來的請求與回覆內容，包括 HTTP 檔頭與 body （payload）都可以檢視，對開發中的除錯是滿實用的工具。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-yu&quot;&gt;結語&lt;&#x2F;h2&gt;
&lt;p&gt;其實免費版能調用的設定不多，並且免費版最大的缺點就是那又亂又長的網址了吧，只有付費版支援自訂網址，還可以用自有的網域，不過免費版就滿適合我這種小資宅宅。&lt;&#x2F;p&gt;
&lt;p&gt;有一些我認為次要的功能就沒寫上來，或許之後有用到再說吧。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Svelte 從小白到入門（十七）結束與展望</title>
        <published>2021-10-30T00:00:00+00:00</published>
        <updated>2021-10-30T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/svelte-quickstart-17/"/>
        <id>https://editor.leonh.space/2021/svelte-quickstart-17/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/svelte-quickstart-17/">&lt;p&gt;這是〈Svelte 從小白到入門〉系列的第十七集，各集連結如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-1&#x2F;&quot;&gt;Svelte 從小白到入門（一）元件、事件、Reactivity&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-2&#x2F;&quot;&gt;Svelte 從小白到入門（二）Props、邏輯、迴圈、Await&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-3&#x2F;&quot;&gt;Svelte 從小白到入門（三）事件&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-4&#x2F;&quot;&gt;Svelte 從小白到入門（四）數據綁定&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-5&#x2F;&quot;&gt;Svelte 從小白到入門（五）生命週期&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-6&#x2F;&quot;&gt;Svelte 從小白到入門（六）Store&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-7&#x2F;&quot;&gt;Svelte 從小白到入門（七）動態效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-8&#x2F;&quot;&gt;Svelte 從小白到入門（八）進出場效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-9&#x2F;&quot;&gt;Svelte 從小白到入門（九）動畫效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-10&#x2F;&quot;&gt;Svelte 從小白到入門（十）Action&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-11&#x2F;&quot;&gt;Svelte 從小白到入門（十一）CSS Class&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-12&#x2F;&quot;&gt;Svelte 從小白到入門（十二）Slot&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-13&#x2F;&quot;&gt;Svelte 從小白到入門（十三）Context&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-14&#x2F;&quot;&gt;Svelte 從小白到入門（十四）Svelte 的特殊元素&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-15&#x2F;&quot;&gt;Svelte 從小白到入門（十五）Module Context&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-16&#x2F;&quot;&gt;Svelte 從小白到入門（十六）Debugging&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-17&#x2F;&quot;&gt;Svelte 從小白到入門（十七）結束與展望&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;本集是此 Svelte 入門教學的最後一集，想要更熟悉 Svelte 的朋友可以參考下面資源：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;examples&quot;&gt;Svelte Examples&lt;&#x2F;a&gt;，有大量的範例&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;docs&quot;&gt;Svelte Docs&lt;&#x2F;a&gt;，有完整的 Svelte API 文件&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;kit.svelte.dev&#x2F;&quot;&gt;SvelteKit&lt;&#x2F;a&gt;，以 Svelte 為基礎的 web 框架，主要是補足 Svelte 所缺乏的路由特性&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 80vmax; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;f2d67921c82f41579da0526ef4ba64dd?version=3.44.0&quot;&gt;&lt;&#x2F;iframe&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Svelte 從小白到入門（十六）Debugging</title>
        <published>2021-10-29T00:00:00+00:00</published>
        <updated>2021-10-29T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/svelte-quickstart-16/"/>
        <id>https://editor.leonh.space/2021/svelte-quickstart-16/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/svelte-quickstart-16/">&lt;p&gt;這是〈Svelte 從小白到入門〉系列的第十六集，各集連結如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-1&#x2F;&quot;&gt;Svelte 從小白到入門（一）元件、事件、Reactivity&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-2&#x2F;&quot;&gt;Svelte 從小白到入門（二）Props、邏輯、迴圈、Await&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-3&#x2F;&quot;&gt;Svelte 從小白到入門（三）事件&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-4&#x2F;&quot;&gt;Svelte 從小白到入門（四）數據綁定&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-5&#x2F;&quot;&gt;Svelte 從小白到入門（五）生命週期&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-6&#x2F;&quot;&gt;Svelte 從小白到入門（六）Store&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-7&#x2F;&quot;&gt;Svelte 從小白到入門（七）動態效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-8&#x2F;&quot;&gt;Svelte 從小白到入門（八）進出場效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-9&#x2F;&quot;&gt;Svelte 從小白到入門（九）動畫效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-10&#x2F;&quot;&gt;Svelte 從小白到入門（十）Action&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-11&#x2F;&quot;&gt;Svelte 從小白到入門（十一）CSS Class&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-12&#x2F;&quot;&gt;Svelte 從小白到入門（十二）Slot&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-13&#x2F;&quot;&gt;Svelte 從小白到入門（十三）Context&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-14&#x2F;&quot;&gt;Svelte 從小白到入門（十四）Svelte 的特殊元素&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-15&#x2F;&quot;&gt;Svelte 從小白到入門（十五）Module Context&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-16&#x2F;&quot;&gt;Svelte 從小白到入門（十六）Debugging&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-17&#x2F;&quot;&gt;Svelte 從小白到入門（十七）結束與展望&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;在 Svelte 除錯，除了那一百零一招 &lt;code&gt;console.log()&lt;&#x2F;code&gt; 之外，Svelte 另外提供了 &lt;code&gt;{@debug}&lt;&#x2F;code&gt; 語句，在瀏覽器的 DevTools 內，此語句會觸發中斷，並且在 console 內印出目標變數的值。&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 700px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;56bf02e4e0f14fc8a8b79705bf7ddfce?version=3.44.0&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;上面的例子中，在 DevTools 看以看到 &lt;code&gt;user&lt;&#x2F;code&gt; 的內容：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-16&#x2F;2021-10-23.png&quot; alt=&quot;DevTools&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Svelte 從小白到入門（十五）Module Context</title>
        <published>2021-10-28T00:00:00+00:00</published>
        <updated>2021-10-28T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/svelte-quickstart-15/"/>
        <id>https://editor.leonh.space/2021/svelte-quickstart-15/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/svelte-quickstart-15/">&lt;p&gt;這是〈Svelte 從小白到入門〉系列的第十五集，各集連結如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-1&#x2F;&quot;&gt;Svelte 從小白到入門（一）元件、事件、Reactivity&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-2&#x2F;&quot;&gt;Svelte 從小白到入門（二）Props、邏輯、迴圈、Await&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-3&#x2F;&quot;&gt;Svelte 從小白到入門（三）事件&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-4&#x2F;&quot;&gt;Svelte 從小白到入門（四）數據綁定&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-5&#x2F;&quot;&gt;Svelte 從小白到入門（五）生命週期&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-6&#x2F;&quot;&gt;Svelte 從小白到入門（六）Store&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-7&#x2F;&quot;&gt;Svelte 從小白到入門（七）動態效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-8&#x2F;&quot;&gt;Svelte 從小白到入門（八）進出場效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-9&#x2F;&quot;&gt;Svelte 從小白到入門（九）動畫效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-10&#x2F;&quot;&gt;Svelte 從小白到入門（十）Action&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-11&#x2F;&quot;&gt;Svelte 從小白到入門（十一）CSS Class&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-12&#x2F;&quot;&gt;Svelte 從小白到入門（十二）Slot&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-13&#x2F;&quot;&gt;Svelte 從小白到入門（十三）Context&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-14&#x2F;&quot;&gt;Svelte 從小白到入門（十四）Svelte 的特殊元素&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-15&#x2F;&quot;&gt;Svelte 從小白到入門（十五）Module Context&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-16&#x2F;&quot;&gt;Svelte 從小白到入門（十六）Debugging&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-17&#x2F;&quot;&gt;Svelte 從小白到入門（十七）結束與展望&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;在此之前我們對某個 Svelte 元件的每一次調用，都是生成一個獨立的物件，儘管他們有相同的程式邏輯，但在物件的內部狀態並不共享，除非用 store 或 context 等的資料交換機制。如果我們想讓所有的實例都有共享的狀態，那得用上 Svelte 的 module context。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gong-xiang-qu-kuai&quot;&gt;共享區塊&lt;&#x2F;h2&gt;
&lt;p&gt;Module context 是個由 &lt;code&gt;&amp;lt;script context=&quot;module&quot;&amp;gt;&lt;&#x2F;code&gt; 所構成的區塊，Svelte 元件內的 module context 程式碼僅會在首次調用時執行，後面再次調用就不會再執行一次，定義在 module context 內的變數是可以被同一個元件的不同實體調用的，並且這樣的資料共享機制是不需要透過外部 API 實現的。&lt;&#x2F;p&gt;
&lt;p&gt;在下面的例子裡，有多個 AudioPlayer 元件存在，他們共享著一個 &lt;code&gt;current&lt;&#x2F;code&gt; 變數，透過變數的切換，我們可以做到對〈The Blue Danube Waltz〉按下播放時，其他正在播放中的樂曲自動暫停這樣人性化的特性：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 700px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;26277958b94e4e9a9e3306d176fd243b?version=3.44.0&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;在 AudioPlayer 元件中，&lt;code&gt;current&lt;&#x2F;code&gt; 為當下播放的物件，並且是所有 AudioPlayer 物件共享的，當對一首曲按播放，除了觸發預設的播放行為外，也觸發了 &lt;code&gt;stopOthers()&lt;&#x2F;code&gt;，此函式將原有的 &lt;code&gt;current&lt;&#x2F;code&gt; 暫停，並將自身播放器賦值給 &lt;code&gt;current&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;bao-lu-gong-xiang-qu-kuai&quot;&gt;暴露共享區塊&lt;&#x2F;h2&gt;
&lt;p&gt;放在 module context 區塊內的成員也可以對外暴露！&lt;&#x2F;p&gt;
&lt;p&gt;在下面的範例中，所有的播放器物件都蒐集在 &lt;code&gt;elements&lt;&#x2F;code&gt; 這個 set 內，並且暴露了一個 &lt;code&gt;stopAll()&lt;&#x2F;code&gt; 的函式：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 700px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;972bb446bd064dcbbb3154480f2a1c35?version=3.44.0&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;在父元件 app 中，我們引入並調用 AudioPlayer 暴露出來的 &lt;code&gt;stopAll()&lt;&#x2F;code&gt;，讓按鈕觸發它。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xiao-jie&quot;&gt;小結&lt;&#x2F;h2&gt;
&lt;p&gt;本集重點整理：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Module context 是元件中可跨物件（實體、實例）共享的區塊，此區塊只在該元件初次調用時執行，所以不會不斷的被新元件重新初始化&lt;&#x2F;li&gt;
&lt;li&gt;Module context 區塊由 &lt;code&gt;&amp;lt;script context=&quot;module&quot;&amp;gt;&lt;&#x2F;code&gt; 所包圍&lt;&#x2F;li&gt;
&lt;li&gt;Module context 內的成員（變數、函式等）也可以對外暴露，供父元件使用&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Svelte 從小白到入門（十四）Svelte 的特殊元素</title>
        <published>2021-10-26T00:00:00+00:00</published>
        <updated>2021-10-26T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/svelte-quickstart-14/"/>
        <id>https://editor.leonh.space/2021/svelte-quickstart-14/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/svelte-quickstart-14/">&lt;p&gt;這是〈Svelte 從小白到入門〉系列的第十四集，各集連結如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-1&#x2F;&quot;&gt;Svelte 從小白到入門（一）元件、事件、Reactivity&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-2&#x2F;&quot;&gt;Svelte 從小白到入門（二）Props、邏輯、迴圈、Await&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-3&#x2F;&quot;&gt;Svelte 從小白到入門（三）事件&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-4&#x2F;&quot;&gt;Svelte 從小白到入門（四）數據綁定&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-5&#x2F;&quot;&gt;Svelte 從小白到入門（五）生命週期&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-6&#x2F;&quot;&gt;Svelte 從小白到入門（六）Store&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-7&#x2F;&quot;&gt;Svelte 從小白到入門（七）動態效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-8&#x2F;&quot;&gt;Svelte 從小白到入門（八）進出場效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-9&#x2F;&quot;&gt;Svelte 從小白到入門（九）動畫效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-10&#x2F;&quot;&gt;Svelte 從小白到入門（十）Action&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-11&#x2F;&quot;&gt;Svelte 從小白到入門（十一）CSS Class&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-12&#x2F;&quot;&gt;Svelte 從小白到入門（十二）Slot&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-13&#x2F;&quot;&gt;Svelte 從小白到入門（十三）Context&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-14&#x2F;&quot;&gt;Svelte 從小白到入門（十四）Svelte 的特殊元素&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-15&#x2F;&quot;&gt;Svelte 從小白到入門（十五）Module Context&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-16&#x2F;&quot;&gt;Svelte 從小白到入門（十六）Debugging&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-17&#x2F;&quot;&gt;Svelte 從小白到入門（十七）結束與展望&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;Svelte 內建了一系列特有的元素，各有不同功效，本集一一認識。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;svelte-self&quot;&gt;&lt;code&gt;&amp;lt;svelte:self&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;svelte:self&amp;gt;&lt;&#x2F;code&gt; 讓元件可以在體內調用自己同一個元件。&lt;&#x2F;p&gt;
&lt;p&gt;下面的例子是一個樹狀的資料夾結構，而一個 Folder 元件內有可能要調用另一個 Folder 元件，所以在 Folder 元件內我們可以看到有用 &lt;code&gt;&amp;lt;svelte:self {...file}&amp;gt;&lt;&#x2F;code&gt; 調用另一個 Folder 元件：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 700px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;43c4e99123854106abfdd9af59694c1b?version=3.44.0&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;h2 id=&quot;svelte-component&quot;&gt;&lt;code&gt;&amp;lt;svelte:component&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;svelte:component&amp;gt;&lt;&#x2F;code&gt; 讓我們得以動態調用元件。&lt;&#x2F;p&gt;
&lt;p&gt;下面的例子裡，有紅色與綠色元件，要調用哪個顏色是根據用戶的選擇而動態改變的，除了用一系列的 &lt;code&gt;if&lt;&#x2F;code&gt; 做抉擇之外，也可以用更簡潔的 &lt;code&gt;&amp;lt;svelte:component&amp;gt;&lt;&#x2F;code&gt; 來達成：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 700px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;cb845e7751424bf594d6f376e8903721?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;在範例中，&lt;code&gt;&amp;lt;svelte:component this={selected.component}&amp;gt;&lt;&#x2F;code&gt; 的 &lt;code&gt;this&lt;&#x2F;code&gt; 用於聲明要調用的元件。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;svelte-window&quot;&gt;&lt;code&gt;&amp;lt;svelte:window&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;因為瀏覽器的 &lt;code&gt;window&lt;&#x2F;code&gt; 物件在 Svelte 建置期間是不存在的，所以想要對瀏覽器的 &lt;code&gt;window&lt;&#x2F;code&gt; 做的事，得改用 &lt;code&gt;&amp;lt;svelte:window&amp;gt;&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;下面的例子裡，我們想要對 &lt;code&gt;window&lt;&#x2F;code&gt; 監聽按鍵，所以我們用 &lt;code&gt;&amp;lt;svelte:window on:keydown={handleKeydown}&#x2F;&amp;gt;&lt;&#x2F;code&gt; 來達成：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 1100px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;fd5fcf4070a640a5aa182506a54cd5d7?version=3.44.0&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;svelte:window&amp;gt;&lt;&#x2F;code&gt; 也可以做數據綁定，可用的屬性如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;innerWidth&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;innerHeight&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;outerWidth&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;outerHeight&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;scrollX&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;scrollY&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;online&lt;&#x2F;code&gt; — an alias for &lt;code&gt;window.navigator.onLine&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;除了 &lt;code&gt;scrollX&lt;&#x2F;code&gt;、&lt;code&gt;scrollY&lt;&#x2F;code&gt; 以外，皆為唯讀屬性。&lt;&#x2F;p&gt;
&lt;p&gt;下面是綁定 &lt;code&gt;scrollY&lt;&#x2F;code&gt; 的示例：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 80vmax; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;c2993dc0716e4302b59d439aeea7e938?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;h2 id=&quot;svelte-body&quot;&gt;&lt;code&gt;&amp;lt;svelte:body&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;與 &lt;code&gt;&amp;lt;svelte:window&amp;gt;&lt;&#x2F;code&gt; 類似，想對 &lt;code&gt;document.body&lt;&#x2F;code&gt; 做的事在 Svelte 請改用 &lt;code&gt;&amp;lt;svelte:body&amp;gt;&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;下面是監聽滑鼠事件的範例：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 1100px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;9776bf8e7e11487d9ca9f2a095d9916e?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;h2 id=&quot;svelte-head&quot;&gt;&lt;code&gt;&amp;lt;svelte:head&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;與前面類似，想要塞入 &lt;code&gt;&amp;lt;head&amp;gt;&lt;&#x2F;code&gt; 的標籤請改用 &lt;code&gt;&amp;lt;svelte:head&amp;gt;&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 300px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;4cac002a6a3b4e3aa6685ac4a56d5adf?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;h2 id=&quot;svelte-options&quot;&gt;&lt;code&gt;&amp;lt;svelte:options&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;svelte:options&amp;gt;&lt;&#x2F;code&gt; 用於設定 Svelte 編譯參數。&lt;&#x2F;p&gt;
&lt;p&gt;以 &lt;code&gt;immutable&lt;&#x2F;code&gt; 為例，下面的範例中，工項每被切換一次，就會閃一下，這主要是靠 Todo 元件內的 &lt;code&gt;afterUpdate()&lt;&#x2F;code&gt; 與 &lt;code&gt;flash()&lt;&#x2F;code&gt; 實現，在預設的編譯行為下，點擊觸發的切換，會呼叫 &lt;code&gt;toggle()&lt;&#x2F;code&gt; 去更新 JS 的 &lt;code&gt;todos&lt;&#x2F;code&gt; 陣列，導致全體觸發 update 事件，也導致用戶點一個工項，卻是全體都閃了一下，所以我們在 todo 元件內用 &lt;code&gt;&amp;lt;svelte:options immutable={true}&#x2F;&amp;gt;&lt;&#x2F;code&gt; 告訴 Svelte，每個 Todo 元件都是 immutable 的，只會被新建或刪除，不會被修改，在這樣的設定下，&lt;code&gt;todos&lt;&#x2F;code&gt; 陣列的更新只會觸發那個被「翻新」的工項的發閃，既有工項因為是 immutable 的，就不會跑 &lt;code&gt;afterUpdate()&lt;&#x2F;code&gt;，也不會閃了。&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 900px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;e873d7862b0c4333a3e203ce0ddb3ffc?version=3.44.0&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;svelte:options&amp;gt;&lt;&#x2F;code&gt; 所有可用的選項如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;immutable={true}&lt;&#x2F;code&gt; — you never use mutable data, so the compiler can do simple referential equality checks to determine if values have changed&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;immutable={false}&lt;&#x2F;code&gt; — the default. Svelte will be more conservative about whether or not mutable objects have changed&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;accessors={true}&lt;&#x2F;code&gt; — adds getters and setters for the component’s props&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;accessors={false}&lt;&#x2F;code&gt; — the default&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;namespace=&quot;...&quot;&lt;&#x2F;code&gt; — the namespace where this component will be used, most commonly “svg”&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;tag=&quot;...&quot;&lt;&#x2F;code&gt; — the name to use when compiling this component as a custom element&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;詳請請參閱 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;docs#svelte_options&quot;&gt;Svelte API 文件&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;svelte-fragment&quot;&gt;&lt;code&gt;&amp;lt;svelte:fragment&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;&amp;lt;svelte:fragment&amp;gt;&lt;&#x2F;code&gt; 讓我們在置入 slot 內容時，不需用一個容器元素把內容包起來。&lt;&#x2F;p&gt;
&lt;p&gt;下面的例子裡，Box 元件以 flex 布局，每個元素間距 2 em，在插入 slot 內容時，如果是用一般容器，會形成這樣的結構：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;div&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; class&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;box&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;slot&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;header&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;No header was provided&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;slot&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;p&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;Some content between header and footer&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;p&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;div&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; slot&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;footer&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;p&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;All rights reserved.&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;p&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;p&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;Copyright (c) 2019 Svelte Industries&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;p&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;div&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;div&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;當然編譯後的原始碼沒這麼簡單，僅是示意。&lt;&#x2F;p&gt;
&lt;p&gt;因為多了一層 &lt;code&gt;&amp;lt;div&amp;gt;&lt;&#x2F;code&gt; 所以「All rights reserved.」與「Copyright (c) 2019 Svelte Industries」之間是沒有那 2 em 間距的，所以我們可以用 &lt;code&gt;&amp;lt;svelte:fragment&amp;gt;&lt;&#x2F;code&gt; 讓容器變成「透明的」：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;div&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; class&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;box&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;slot&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;header&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;No header was provided&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;slot&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;p&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;Some content between header and footer&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;p&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;p&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;All rights reserved.&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;p&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;p&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;Copyright (c) 2019 Svelte Industries&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;p&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;div&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如此一來，每行文字之間都有了 2 em 的間距。&lt;&#x2F;p&gt;
&lt;p&gt;完整的範例如下：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 500px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;ba4e5bf320f243c1ba0b7fc6cdb29cb5?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Svelte 從小白到入門（十三）Context</title>
        <published>2021-10-23T00:00:00+00:00</published>
        <updated>2021-10-23T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/svelte-quickstart-13/"/>
        <id>https://editor.leonh.space/2021/svelte-quickstart-13/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/svelte-quickstart-13/">&lt;p&gt;這是〈Svelte 從小白到入門〉系列的第十三集，各集連結如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-1&#x2F;&quot;&gt;Svelte 從小白到入門（一）元件、事件、Reactivity&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-2&#x2F;&quot;&gt;Svelte 從小白到入門（二）Props、邏輯、迴圈、Await&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-3&#x2F;&quot;&gt;Svelte 從小白到入門（三）事件&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-4&#x2F;&quot;&gt;Svelte 從小白到入門（四）數據綁定&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-5&#x2F;&quot;&gt;Svelte 從小白到入門（五）生命週期&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-6&#x2F;&quot;&gt;Svelte 從小白到入門（六）Store&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-7&#x2F;&quot;&gt;Svelte 從小白到入門（七）動態效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-8&#x2F;&quot;&gt;Svelte 從小白到入門（八）進出場效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-9&#x2F;&quot;&gt;Svelte 從小白到入門（九）動畫效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-10&#x2F;&quot;&gt;Svelte 從小白到入門（十）Action&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-11&#x2F;&quot;&gt;Svelte 從小白到入門（十一）CSS Class&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-12&#x2F;&quot;&gt;Svelte 從小白到入門（十二）Slot&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-13&#x2F;&quot;&gt;Svelte 從小白到入門（十三）Context&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-14&#x2F;&quot;&gt;Svelte 從小白到入門（十四）Svelte 的特殊元素&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-15&#x2F;&quot;&gt;Svelte 從小白到入門（十五）Module Context&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-16&#x2F;&quot;&gt;Svelte 從小白到入門（十六）Debugging&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-17&#x2F;&quot;&gt;Svelte 從小白到入門（十七）結束與展望&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;Context API 是 Svelte 中一種元件間交換資料的機制，當一個父元件以 &lt;code&gt;setContext(key, context)&lt;&#x2F;code&gt; 設定一組 context，那麼它的子元件，以及&lt;strong&gt;子元件的子元件&lt;&#x2F;strong&gt;，都可以透過 &lt;code&gt;getContext(key)&lt;&#x2F;code&gt; 來取得那組 context。&lt;&#x2F;p&gt;
&lt;p&gt;在下面的地圖範例中，我們有 Map 元件，以及子元件 MapMarker。&lt;&#x2F;p&gt;
&lt;p&gt;Map 元件的主要成員是 &lt;code&gt;map&lt;&#x2F;code&gt;，它是 &lt;code&gt;mapbox.Map()&lt;&#x2F;code&gt; 構成的地圖物件，在範例中，我們用 &lt;code&gt;setContext()&lt;&#x2F;code&gt; 把 &lt;code&gt;getMap&lt;&#x2F;code&gt; 成為一組 context，使子元件們（MapMarker）也得以取得 &lt;code&gt;map&lt;&#x2F;code&gt; 這個地圖物件：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 400px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;87cd903efa95478a932d5fc008d32276?version=3.44.0&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;在 Map 元件中，注意到 &lt;code&gt;map = new mapbox.Map()&lt;&#x2F;code&gt; 這句生成地圖物件的敘述是放在 &lt;code&gt;onMount()&lt;&#x2F;code&gt; 內，&lt;code&gt;onMount()&lt;&#x2F;code&gt; 在第五集生命週期有出現過，&lt;code&gt;onMount()&lt;&#x2F;code&gt; 指的是當 Svelte 元件在被瀏覽器生成之時，而地圖物件的生成也必須是它的容器元素已經被瀏覽器建立之後，也就是 Map 元件內的那個 &lt;code&gt;&amp;lt;div&amp;gt;&lt;&#x2F;code&gt; 必須先存在，Mapbox 才有可能以它為容器，建構出地圖物件本身，所以這些構成地圖物件的程式碼才得放在 &lt;code&gt;onMount()&lt;&#x2F;code&gt; 裡面。&lt;&#x2F;p&gt;
&lt;p&gt;並且需要注意的是，&lt;code&gt;setContext()&lt;&#x2F;code&gt; 得在元件初始化時調用，但此時 &lt;code&gt;map&lt;&#x2F;code&gt; 僅是 &lt;code&gt;undefined&lt;&#x2F;code&gt;，尚未生成地圖物件，因此我們並不直接在 context 中置入 &lt;code&gt;map&lt;&#x2F;code&gt;，而是用 &lt;code&gt;getMap()&lt;&#x2F;code&gt; 函式來處理：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;setContext&lt;&#x2F;span&gt;&lt;span&gt;(key, {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;	getMap&lt;&#x2F;span&gt;&lt;span&gt;: ()&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; map&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;});&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;因此在 MapMarker 中，我們透過 &lt;code&gt;getContext()&lt;&#x2F;code&gt; 得到的並非是 &lt;code&gt;map&lt;&#x2F;code&gt; &lt;strong&gt;真身&lt;&#x2F;strong&gt;，而是可以取得 &lt;code&gt;map&lt;&#x2F;code&gt; 的函式 &lt;code&gt;getMap()&lt;&#x2F;code&gt;，如此迂迂迴迴 &lt;del&gt;只為了求你點個讚&lt;&#x2F;del&gt; 讓我們得以把元件細節都封裝在元件內，使最外層的 App 元件看起來是如此的清晰而且語意明確：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;Map&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; lat&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;{35}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; lon&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;{-100}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; zoom&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;{2}&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FDAEB7;font-style: italic;&quot;&gt;MapMarker&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; lat&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;{37.8225}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; lon&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;{-122.0024}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; label&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;Svelte Body Shaping&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&#x2F;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FDAEB7;font-style: italic;&quot;&gt;MapMarker&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; lat&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;{29.7230}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; lon&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;{-95.4189}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; label&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;Svelte Waxing Studio&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&#x2F;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;Map&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;context-de-key&quot;&gt;Context 的 Key&lt;&#x2F;h2&gt;
&lt;p&gt;在上面的範例中，我們再做 context 時用的 &lt;code&gt;key&lt;&#x2F;code&gt; 只是一個空物件 &lt;code&gt;{}&lt;&#x2F;code&gt;，在 Svelte 的約定裡，可以用任意物件當 &lt;code&gt;key&lt;&#x2F;code&gt;，只要自己確保沒有與其他物件搞混就好，所以 Svelte 推薦我們用物件做為 &lt;code&gt;key&lt;&#x2F;code&gt;，因為在 JavaScript 的設計上，&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;medium.com&#x2F;@wanwan1313&#x2F;js%E8%A3%9C%E5%B8%96-pass-by-value-pass-by-reference-pass-by-sharing-54ef2f369853&quot;&gt;兩個一樣的空物件是參照到不同的記憶體位址&lt;&#x2F;a&gt;，即 &lt;code&gt;{} !== {}&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;當然，如果有兩個 context，&lt;code&gt;key&lt;&#x2F;code&gt; 也必須是各自獨立的。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;context-yu-store&quot;&gt;Context 與 Store&lt;&#x2F;h2&gt;
&lt;p&gt;同樣做為跨元件的資料交換機制，第六集的 store 與本集的 context 有著以下差異：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Store 是全域的，任何元件都可以使用，而 context 是一個元件及它的子代元件才可使用&lt;&#x2F;li&gt;
&lt;li&gt;承上，當一個元件有 A、B 兩個實體時（被調用兩次），他們各自的 context 是獨立的，不互相影響，他們的子元件調用到的 context 當然也是獨立的，A 的子元件吃到的 context 就是來自 A，不會進錯家門上錯床吃到 B 的 context&lt;&#x2F;li&gt;
&lt;li&gt;Store 有觀察者模式的特性，元件可以 subscribe 一個 store 的即時變化，而 context 不具有這樣的特性&lt;&#x2F;li&gt;
&lt;li&gt;承上，透過 Store 的 subscribe 機制，我們可以說 store 是 reactivity 的，而 context 是沒有 reactivity 的特性的&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;基於上述的最後一點，如果某個 context 的內容會改變，而且我們希望它的改變要反映在其他地方，那可以把 store 與 context 合併運用：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;const { these, are, stores } = getContext(...);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如此一來，這些 store 的值來自 &lt;code&gt;getContext(...)&lt;&#x2F;code&gt;，並且也有 reactivity 惹！&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xiao-jie&quot;&gt;小結&lt;&#x2F;h2&gt;
&lt;p&gt;本集重點整理：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Context 是跨元件的資料交換機制，但僅限父元件與它的子代元件間做交換&lt;&#x2F;li&gt;
&lt;li&gt;用 &lt;code&gt;setContext(key, content)&lt;&#x2F;code&gt; 設定 context、用 &lt;code&gt;getContext(key)&lt;&#x2F;code&gt; 取得 context&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;setContext()&lt;&#x2F;code&gt; 得在元件的初始化階段調用&lt;&#x2F;li&gt;
&lt;li&gt;關於 context 與 store 的差異請看上一節&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Svelte 從小白到入門（十二）Slot</title>
        <published>2021-10-22T00:00:00+00:00</published>
        <updated>2021-10-22T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/svelte-quickstart-12/"/>
        <id>https://editor.leonh.space/2021/svelte-quickstart-12/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/svelte-quickstart-12/">&lt;p&gt;這是〈Svelte 從小白到入門〉系列的第十二集，各集連結如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-1&#x2F;&quot;&gt;Svelte 從小白到入門（一）元件、事件、Reactivity&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-2&#x2F;&quot;&gt;Svelte 從小白到入門（二）Props、邏輯、迴圈、Await&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-3&#x2F;&quot;&gt;Svelte 從小白到入門（三）事件&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-4&#x2F;&quot;&gt;Svelte 從小白到入門（四）數據綁定&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-5&#x2F;&quot;&gt;Svelte 從小白到入門（五）生命週期&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-6&#x2F;&quot;&gt;Svelte 從小白到入門（六）Store&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-7&#x2F;&quot;&gt;Svelte 從小白到入門（七）動態效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-8&#x2F;&quot;&gt;Svelte 從小白到入門（八）進出場效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-9&#x2F;&quot;&gt;Svelte 從小白到入門（九）動畫效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-10&#x2F;&quot;&gt;Svelte 從小白到入門（十）Action&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-11&#x2F;&quot;&gt;Svelte 從小白到入門（十一）CSS Class&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-12&#x2F;&quot;&gt;Svelte 從小白到入門（十二）Slot&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-13&#x2F;&quot;&gt;Svelte 從小白到入門（十三）Context&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-14&#x2F;&quot;&gt;Svelte 從小白到入門（十四）Svelte 的特殊元素&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-15&#x2F;&quot;&gt;Svelte 從小白到入門（十五）Module Context&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-16&#x2F;&quot;&gt;Svelte 從小白到入門（十六）Debugging&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-17&#x2F;&quot;&gt;Svelte 從小白到入門（十七）結束與展望&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;slot&quot;&gt;Slot&lt;&#x2F;h2&gt;
&lt;p&gt;在 HTML 我們常會用 &lt;code&gt;&amp;lt;div&amp;gt;&lt;&#x2F;code&gt; 做為容器，內部放入其他的元素，而在 Svelte 元件，也有類似的用法，稱為 slot，顧名思義，一個 Svelte 元件內有了 slot，當此元件被使用時，可以被置入其他的元素。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;slot-ji-chu&quot;&gt;Slot 基礎&lt;&#x2F;h3&gt;
&lt;p&gt;一個最簡單的內含 slot 的 Svelte 元件：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 700px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;a86c63724fed4092bceeef7efcbc6a28?version=3.44.0&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;在 Box 元件內，先預放 &lt;code&gt;&amp;lt;slot&amp;gt;&amp;lt;&#x2F;slot&amp;gt;&lt;&#x2F;code&gt;，父元件調用 Box 時，就可以在 &lt;code&gt;&amp;lt;Box&amp;gt;&amp;lt;&#x2F;Box&amp;gt;&lt;&#x2F;code&gt; 中間置入其他元素。&lt;&#x2F;p&gt;
&lt;p&gt;在 App 中的第一個 Box，我們置入了 slot 的內容，所以 Box 內預留的「no content was provided」就會被取代掉。&lt;&#x2F;p&gt;
&lt;p&gt;另外注意到上例中 &lt;code&gt;&amp;lt;p&amp;gt;&lt;&#x2F;code&gt; 的樣式是在 App 定義的，而不是在 Box 內定義的。&lt;&#x2F;p&gt;
&lt;p&gt;在 App 的第二個 Box 並位置入 slot 內容，所以 Box 內預留的「no content was provided」就會顯示出來。&lt;&#x2F;p&gt;
&lt;p&gt;在 App 的第三個 Box 是巢狀置入，slot 不僅可置入一般元素，也可以置入 Svelte 元件。那能不能惡搞做出無限迴圈呢？答案是不行的，殘念です…，Svelte 在建置期間會警示並阻擋無限迴圈的元件調用。&lt;&#x2F;p&gt;
&lt;p&gt;所以要怎麼做出下面的 Woo~! That’s funky~~~ 效果呢？嗯…？&lt;&#x2F;p&gt;
&lt;iframe src=&quot;https:&#x2F;&#x2F;www.youtube-nocookie.com&#x2F;embed&#x2F;ykz_dbpYRPU&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
&lt;h3 id=&quot;ju-ming-de-slot&quot;&gt;具名的 Slot&lt;&#x2F;h3&gt;
&lt;p&gt;當一個 Svelte 元件有超過一個 slot 時，那就要為 slot 命名，以便調用時正確置入內容。&lt;&#x2F;p&gt;
&lt;p&gt;只要為 slot 加上 &lt;code&gt;name&lt;&#x2F;code&gt; 屬性就是個具名的 slot：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;slot&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;address&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;slot&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;完整的範例如下：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 400px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;05ed123d0e184550ad1fcb8a41d79861?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;在 Box 裡面有三個 slot：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;slot name=&quot;name&quot;&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;slot name=&quot;address&quot;&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;slot name=&quot;email&quot;&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;而在 App 這邊我們只填入了姓名與地址的 slot，剩下的 email 未填，以預留的「Unknown email」顯示。&lt;&#x2F;p&gt;
&lt;p&gt;一個典型的具名的 slot 用法是將名稱做為屬性填入，以上面的例子來說是這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;span&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; slot&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;address&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;43 Wallaby Way&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;span&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;slots&quot;&gt;$$slots&lt;&#x2F;h3&gt;
&lt;p&gt;在此之前，我們曾經在第二集介紹 props 時見過 &lt;code&gt;$$props&lt;&#x2F;code&gt;，&lt;code&gt;$$props&lt;&#x2F;code&gt; 是存放所有傳進元件內的 props 的變數，而 &lt;code&gt;$$slots&lt;&#x2F;code&gt; 則是所有傳進元件內的 slot 內容的變數，兩者的命名模式與語意是類似的。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;$$slots&lt;&#x2F;code&gt; 它裡面存放了所有被傳進來的 slot 內容，當我們想要在元件的任意位置調用某個 slot 內容可以用它，或者是我們也可以在元件內根據 &lt;code&gt;$$slots&lt;&#x2F;code&gt; 的值來調整該元件要展現的內容或樣式。&lt;&#x2F;p&gt;
&lt;p&gt;下面的例子裡，「Add Typescript support」有留言，因此卡片右上角有小圓點，並且卡片下方有顯示留言，而「Update document」沒有留言，因為那個 team 如同我們一樣，嘴巴上都講文件很重要，但其實沒人真正在乎，所以沒有小圓點，也沒有顯示留言區塊：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 1100px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;8f585658a7634bc08f5fe3ca4b6d5aed?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;在 Project 元件中，注意到這一區塊：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;article&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; class:has-discussion&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;{$$slots.comments}&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;div&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;		&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;h2&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;{title}&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;h2&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;div&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	{#if $$slots.comments}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;		&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;div&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; class&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;discussion&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;			&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;h3&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;Comments&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;h3&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;			&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;slot&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;comments&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;slot&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;		&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;div&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	{&#x2F;if}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;article&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在 CSS class 方面，我們用 &lt;code&gt;{$$slots.comments}&lt;&#x2F;code&gt; 做判斷，當有留言時，相當於 &lt;code&gt;true&lt;&#x2F;code&gt;，就會套用上 &lt;code&gt;.has-discussion&lt;&#x2F;code&gt; 這個 CSS class，呈現出小圓點；在留言區方面，我們用 &lt;code&gt;{#if $$slots.commments}&lt;&#x2F;code&gt; 做判斷，原理同上，當有留言時，相當於 &lt;code&gt;true&lt;&#x2F;code&gt;，就會顯示出留言區塊。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;slot-props&quot;&gt;Slot Props&lt;&#x2F;h3&gt;
&lt;p&gt;在第二集我們介紹過 Svelte 元件的 props，指的是元件對外暴露的變數，在調用元件時可以對 props 賦值，達到把值傳入元件的目的。&lt;&#x2F;p&gt;
&lt;p&gt;在 slot 層級，也有自己的 props，目的也是相仿的。&lt;&#x2F;p&gt;
&lt;p&gt;在下面的例子中，Hoverable 區塊內的「Hover over me!」是來自 App 元件給予的 slot 內容，而 Hoverable 元件本身一旦感應到游標，就會觸發變色，並且還要讓父元件 App 知道狀態發生變化，使 slot 字串變為「I am being hovered upon.」：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 800px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;f3ad5ba64235403188ceb832147a40cc?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;在 Hoverable 元件內，我們用一個 JS 變數 &lt;code&gt;hovering&lt;&#x2F;code&gt; 來判斷是否有游標在其上，再利用 &lt;code&gt;hovering={hovering}&lt;&#x2F;code&gt; 的聲明語句將 JS 變數 &lt;code&gt;hovering&lt;&#x2F;code&gt; 以 &lt;code&gt;hovering&lt;&#x2F;code&gt; 之名對外暴露。&lt;&#x2F;p&gt;
&lt;p&gt;在父元件 App 這邊，則是用 &lt;code&gt;let:hovering={active}&lt;&#x2F;code&gt; 的聲明語句將 &lt;code&gt;hovering&lt;&#x2F;code&gt; 重命名為 &lt;code&gt;active&lt;&#x2F;code&gt;，至此我們就可以在 App 同步接收到 Hoverable 元件內的狀態變化，並做出後續字串變更的判斷邏輯。&lt;&#x2F;p&gt;
&lt;p&gt;如果是具名的 slot，那它的 slot props 的用法會是這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;span&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; slot&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;name&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; let:hovering&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;{active}&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;span&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;xiao-jie&quot;&gt;小結&lt;&#x2F;h2&gt;
&lt;p&gt;本集重點整理：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Svelte 元件可以提供一個或多個空位，稱為 slot，當元件被調用時，父元件可以把內容放入 slot 中&lt;&#x2F;li&gt;
&lt;li&gt;Slot 可以有預留內容，當此 slot 沒有被置入內容時，會顯示預留的內容&lt;&#x2F;li&gt;
&lt;li&gt;元件內只有一個 slot 時，不必特地命名，但當一個元件內有提供多個 slot 時，必須為每個 slot 命名，才可正確對應&lt;&#x2F;li&gt;
&lt;li&gt;元件內的 &lt;code&gt;$$slots&lt;&#x2F;code&gt; 變數是一個存放所有被傳入的 slot 內容的物件，我們可以根據它裡面某個 slot 的存在與否在程式碼做邏輯上的決斷&lt;&#x2F;li&gt;
&lt;li&gt;Slot 也有 props，slot props 將元件內的變數在 slot 的層級對外暴露，供應給父層元件調用&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;另外本集出現了一個新語句 &lt;code&gt;let:&lt;&#x2F;code&gt;，用於在父層元件調用子元件的 slot props。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Svelte 從小白到入門（十一）CSS Class</title>
        <published>2021-10-21T00:00:00+00:00</published>
        <updated>2021-10-21T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/svelte-quickstart-11/"/>
        <id>https://editor.leonh.space/2021/svelte-quickstart-11/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/svelte-quickstart-11/">&lt;p&gt;這是〈Svelte 從小白到入門〉系列的第十一集，各集連結如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-1&#x2F;&quot;&gt;Svelte 從小白到入門（一）元件、事件、Reactivity&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-2&#x2F;&quot;&gt;Svelte 從小白到入門（二）Props、邏輯、迴圈、Await&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-3&#x2F;&quot;&gt;Svelte 從小白到入門（三）事件&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-4&#x2F;&quot;&gt;Svelte 從小白到入門（四）數據綁定&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-5&#x2F;&quot;&gt;Svelte 從小白到入門（五）生命週期&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-6&#x2F;&quot;&gt;Svelte 從小白到入門（六）Store&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-7&#x2F;&quot;&gt;Svelte 從小白到入門（七）動態效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-8&#x2F;&quot;&gt;Svelte 從小白到入門（八）進出場效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-9&#x2F;&quot;&gt;Svelte 從小白到入門（九）動畫效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-10&#x2F;&quot;&gt;Svelte 從小白到入門（十）Action&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-11&#x2F;&quot;&gt;Svelte 從小白到入門（十一）CSS Class&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-12&#x2F;&quot;&gt;Svelte 從小白到入門（十二）Slot&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-13&#x2F;&quot;&gt;Svelte 從小白到入門（十三）Context&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-14&#x2F;&quot;&gt;Svelte 從小白到入門（十四）Svelte 的特殊元素&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-15&#x2F;&quot;&gt;Svelte 從小白到入門（十五）Module Context&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-16&#x2F;&quot;&gt;Svelte 從小白到入門（十六）Debugging&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-17&#x2F;&quot;&gt;Svelte 從小白到入門（十七）結束與展望&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;css-class&quot;&gt;CSS Class&lt;&#x2F;h2&gt;
&lt;p&gt;想在 Svelte 元件中的 HTML 區域置入簡單的 JS 敘述一般會這樣做：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;button&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; class&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;{current === &amp;#39;foo&amp;#39; ? &amp;#39;selected&amp;#39; : &amp;#39;&amp;#39;}&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;foo&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;button&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;利用 Svelte 提供的 &lt;code&gt;{}&lt;&#x2F;code&gt; 大括號插入 JS 敘述，上面我們用簡單的三元表達式來決定要帶入的 CSS class。&lt;&#x2F;p&gt;
&lt;p&gt;但對於 CSS class 的添加，Svelte 有更簡便的語法，像是這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;button&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; class:selected&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;{current === &amp;#39;foo&amp;#39;}&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;foo&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;button&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;當 &lt;code&gt;current&lt;&#x2F;code&gt; 為 &lt;code&gt;foo&lt;&#x2F;code&gt; 時，按鈕就會被添加 &lt;code&gt;selected&lt;&#x2F;code&gt; 的 CSS class，方便我們少打幾個字。&lt;&#x2F;p&gt;
&lt;p&gt;完整的範例如下：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 900px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;c363f236676d4d2997d733bae582fdb6?version=3.44.0&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;如果要套用的 CSS class 又剛好和 JS 變數同名：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;div&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; class:big&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;{big}&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;	&amp;lt;!-- ... --&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;div&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面的意思是，如果 JS 變數 &lt;code&gt;big&lt;&#x2F;code&gt; 為 &lt;code&gt;true&lt;&#x2F;code&gt; 則套用 CSS class &lt;code&gt;.big&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;因為同名，Svelte 讓我們用更簡短的寫法表達：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;div&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; class:big&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;	&amp;lt;!-- ... --&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;div&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;耶又可以少打幾個字！&lt;&#x2F;p&gt;
&lt;p&gt;完整的範例如下：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 600px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;655ad149d40a414cb78240de1f121fe6?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Svelte 從小白到入門（十）Action</title>
        <published>2021-10-20T00:00:00+00:00</published>
        <updated>2021-10-20T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/svelte-quickstart-10/"/>
        <id>https://editor.leonh.space/2021/svelte-quickstart-10/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/svelte-quickstart-10/">&lt;p&gt;這是〈Svelte 從小白到入門〉系列的第十集，各集連結如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-1&#x2F;&quot;&gt;Svelte 從小白到入門（一）元件、事件、Reactivity&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-2&#x2F;&quot;&gt;Svelte 從小白到入門（二）Props、邏輯、迴圈、Await&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-3&#x2F;&quot;&gt;Svelte 從小白到入門（三）事件&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-4&#x2F;&quot;&gt;Svelte 從小白到入門（四）數據綁定&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-5&#x2F;&quot;&gt;Svelte 從小白到入門（五）生命週期&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-6&#x2F;&quot;&gt;Svelte 從小白到入門（六）Store&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-7&#x2F;&quot;&gt;Svelte 從小白到入門（七）動態效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-8&#x2F;&quot;&gt;Svelte 從小白到入門（八）進出場效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-9&#x2F;&quot;&gt;Svelte 從小白到入門（九）動畫效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-10&#x2F;&quot;&gt;Svelte 從小白到入門（十）Action&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-11&#x2F;&quot;&gt;Svelte 從小白到入門（十一）CSS Class&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-12&#x2F;&quot;&gt;Svelte 從小白到入門（十二）Slot&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-13&#x2F;&quot;&gt;Svelte 從小白到入門（十三）Context&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-14&#x2F;&quot;&gt;Svelte 從小白到入門（十四）Svelte 的特殊元素&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-15&#x2F;&quot;&gt;Svelte 從小白到入門（十五）Module Context&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-16&#x2F;&quot;&gt;Svelte 從小白到入門（十六）Debugging&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-17&#x2F;&quot;&gt;Svelte 從小白到入門（十七）結束與展望&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;action&quot;&gt;Action&lt;&#x2F;h2&gt;
&lt;p&gt;Action 是客製 Svelte 元件內 HTML 元素行為的函式，利用 Svelte 提供的機制，賦予原生的 HTML 元素具有 action 函式的特性。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zi-ding-shi-jian&quot;&gt;自訂事件&lt;&#x2F;h3&gt;
&lt;p&gt;下面我們用 action 函式賦予 HTML 元素額外的事件特性，當原生的 HTML 事件不足以表示我們的元件邏輯時，可以用 action 自行定義元素的事件，當然也包括對原生事件的二次封裝，使元素與事件在邏輯上或語意上更加一致。&lt;&#x2F;p&gt;
&lt;p&gt;下面的橘色方塊，可以被滑鼠拖移（pan），放開後又會彈回中間，為此我們定義了一系列的 pan 事件在 pannable.js 裡，引用 pannable.js 後在 HTML 區域以 &lt;code&gt;use:pannable&lt;&#x2F;code&gt; 的語句使其作用在橘色方塊的 &lt;code&gt;&amp;lt;div&amp;gt;&lt;&#x2F;code&gt; 上，爾後橘色方塊就可以以 &lt;code&gt;on:&lt;&#x2F;code&gt; 語句聲明一系列 pan 事件所要觸發的函式：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 1300px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;03b133febd6e44b991b461ea25ccded7?version=3.44.0&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;打開 pannable.js 看看：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;pnnable(node)&lt;&#x2F;code&gt; 被餵了個 &lt;code&gt;node&lt;&#x2F;code&gt;，這是 action 函式的約定作法。因為在 HTML 內我們是對橘色方塊的 &lt;code&gt;&amp;lt;div&amp;gt;&lt;&#x2F;code&gt; 使用 &lt;code&gt;use:pannable&lt;&#x2F;code&gt;，所以這裡的 &lt;code&gt;node&lt;&#x2F;code&gt; 就是那個橘色方塊&lt;&#x2F;li&gt;
&lt;li&gt;當滑鼠按下時，因為 &lt;code&gt;node.addEventListener(&#x27;mousedown&#x27;, handleMousedown)&lt;&#x2F;code&gt;，觸發了 &lt;code&gt;handleMousedown()&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;handleMousedown()&lt;&#x2F;code&gt; 裡面又監聽了 &lt;code&gt;mousemove&lt;&#x2F;code&gt; 與 &lt;code&gt;mouseup&lt;&#x2F;code&gt; 兩個原生事件，並觸發相對的函式&lt;&#x2F;li&gt;
&lt;li&gt;在那些自訂的函式內，我們用 &lt;code&gt;node.dispatchEvent(new CustomEvent())&lt;&#x2F;code&gt; 語句來建構與對外分派自訂事件&lt;&#x2F;li&gt;
&lt;li&gt;最後的 &lt;code&gt;destroy()&lt;&#x2F;code&gt; 則是當 &lt;code&gt;node&lt;&#x2F;code&gt; 被銷毀時會被觸發的函式&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;當自訂事件作用在橘色方塊後，就可以用 &lt;code&gt;on:&lt;&#x2F;code&gt; 語句聲明這些自訂的 pan 事件所要觸發的函式。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;action-can-shu&quot;&gt;Action 參數&lt;&#x2F;h3&gt;
&lt;p&gt;要為一個 action 添加參數，語法與前面兩集提過的 transition、animation 一樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;button&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; use:longpress&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;{duration}&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;press and hold&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;button&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果要餵兩個參數，則把他們包成 JS 物件的形式：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;button&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; use:longpress&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;{{duration, spiciness}}&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    press and hold&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;button&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;下面的範例中，滑桿的秒數會影響按鈕長按行為的秒數。&lt;&#x2F;p&gt;
&lt;p&gt;我們定義一個 &lt;code&gt;duration&lt;&#x2F;code&gt;，並將它與滑桿的值做綁定，再將 &lt;code&gt;duration&lt;&#x2F;code&gt; 作為參數餵給 &lt;code&gt;longpress&lt;&#x2F;code&gt; action：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 700px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;b0fb582e46c74ebebe985b022f7b6b64?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;在 longpress.js 內的結構，與前一個範例差不多，但注意到 &lt;code&gt;retren&lt;&#x2F;code&gt; 內多了一個 &lt;code&gt;update()&lt;&#x2F;code&gt;，此函式用於當 &lt;code&gt;use:longpress=&quot;{duration}&quot;&lt;&#x2F;code&gt; 的 &lt;code&gt;{duration}&lt;&#x2F;code&gt; 變化時 Svelte 會自動執行 &lt;code&gt;update()&lt;&#x2F;code&gt;，也就是說，當 &lt;code&gt;update()&lt;&#x2F;code&gt; 不存在時，&lt;code&gt;duration&lt;&#x2F;code&gt; 是不會自動更新的，&lt;strong&gt;這很不 reactivity…&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xiao-jie&quot;&gt;小結&lt;&#x2F;h2&gt;
&lt;p&gt;Action 是 Svelte 提供給我們自訂 HTML 元素行為的機制，它的聲明語法與 transition、animation 相似：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;button&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; use:longpress&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;{{duration, spiciness}}&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;而所謂的 Action 本身是一個有固定格式的函式：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;此函式接受一個 &lt;code&gt;node&lt;&#x2F;code&gt; 參數，&lt;code&gt;node&lt;&#x2F;code&gt; 將會是調用此 action 的 HTML 元素，以上例來說就會是 &lt;code&gt;&amp;lt;button&amp;gt;&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;當 action 的參數在外部被異動時，Svelte 會自動呼叫 action 內的 &lt;code&gt;update()&lt;&#x2F;code&gt;，我們可以在 &lt;code&gt;update()&lt;&#x2F;code&gt; 內為參數做更新&lt;&#x2F;li&gt;
&lt;li&gt;當 &lt;code&gt;node&lt;&#x2F;code&gt; 被刪除時，Svelte 會自動呼叫 action 內的 &lt;code&gt;destroy()&lt;&#x2F;code&gt;，我們可以將不必要的物件或邏輯刪除&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Svelte 從小白到入門（九）動畫效果</title>
        <published>2021-10-19T00:00:00+00:00</published>
        <updated>2021-10-19T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/svelte-quickstart-9/"/>
        <id>https://editor.leonh.space/2021/svelte-quickstart-9/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/svelte-quickstart-9/">&lt;p&gt;這是〈Svelte 從小白到入門〉系列的第九集，各集連結如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-1&#x2F;&quot;&gt;Svelte 從小白到入門（一）元件、事件、Reactivity&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-2&#x2F;&quot;&gt;Svelte 從小白到入門（二）Props、邏輯、迴圈、Await&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-3&#x2F;&quot;&gt;Svelte 從小白到入門（三）事件&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-4&#x2F;&quot;&gt;Svelte 從小白到入門（四）數據綁定&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-5&#x2F;&quot;&gt;Svelte 從小白到入門（五）生命週期&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-6&#x2F;&quot;&gt;Svelte 從小白到入門（六）Store&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-7&#x2F;&quot;&gt;Svelte 從小白到入門（七）動態效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-8&#x2F;&quot;&gt;Svelte 從小白到入門（八）進出場效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-9&#x2F;&quot;&gt;Svelte 從小白到入門（九）動畫效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-10&#x2F;&quot;&gt;Svelte 從小白到入門（十）Action&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-11&#x2F;&quot;&gt;Svelte 從小白到入門（十一）CSS Class&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-12&#x2F;&quot;&gt;Svelte 從小白到入門（十二）Slot&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-13&#x2F;&quot;&gt;Svelte 從小白到入門（十三）Context&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-14&#x2F;&quot;&gt;Svelte 從小白到入門（十四）Svelte 的特殊元素&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-15&#x2F;&quot;&gt;Svelte 從小白到入門（十五）Module Context&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-16&#x2F;&quot;&gt;Svelte 從小白到入門（十六）Debugging&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-17&#x2F;&quot;&gt;Svelte 從小白到入門（十七）結束與展望&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;dong-hua-xiao-guo&quot;&gt;動畫效果&lt;&#x2F;h2&gt;
&lt;p&gt;在上一集的 to-do list 範例裡，當一個工項移動到對面，下面的工項會往上補位，這個往上補位的動作，我們可以用 Svelte 的動畫效果讓它更滑順一點。&lt;&#x2F;p&gt;
&lt;p&gt;與上一集的進出場效果不同的是，動畫效果並未牽涉到 HTML 元素的出現與消失，而是既有工項的位移，我們引入 &lt;code&gt;flip&lt;&#x2F;code&gt; 幫我們處理工項從位置 A 移動到位置 B 的動畫：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 80vmax; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;7faf326787984a379a0e5cb80d364d57?version=3.44.0&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;範例中的工項現在長這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;label&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;	in:receive&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;{{key: todo.id}}&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;	out:send&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;{{key: todo.id}}&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;	animate:flip&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;{{duration: 200}}&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;三個屬性各司其職：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;in:&lt;&#x2F;code&gt; 聲明 &lt;code&gt;&amp;lt;label&amp;gt;&lt;&#x2F;code&gt; 進場時的效果，範例中是 crossfade，&lt;code&gt;receive&lt;&#x2F;code&gt; 表示預期此元素會從某個地方 send 過來，兩者間有移動與 crossfade 的效果，若出場並未來自某個 send，則會呼叫 crossfade 中自定義的 &lt;code&gt;fallback()&lt;&#x2F;code&gt;，&lt;code&gt;{{key: todo.id}}&lt;&#x2F;code&gt; 則是使 crossfade 得以判斷工項的識別碼&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;out:&lt;&#x2F;code&gt; 聲明 &lt;code&gt;&amp;lt;label&amp;gt;&lt;&#x2F;code&gt; 出場的效果，其餘同上&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;animiate:&lt;&#x2F;code&gt; 聲明當物件並未進出場，而僅是位移時做出動畫效果&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Svelte 從小白到入門（八）進出場效果</title>
        <published>2021-10-17T00:00:00+00:00</published>
        <updated>2021-10-17T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/svelte-quickstart-8/"/>
        <id>https://editor.leonh.space/2021/svelte-quickstart-8/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/svelte-quickstart-8/">&lt;p&gt;這是〈Svelte 從小白到入門〉系列的第八集，各集連結如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-1&#x2F;&quot;&gt;Svelte 從小白到入門（一）元件、事件、Reactivity&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-2&#x2F;&quot;&gt;Svelte 從小白到入門（二）Props、邏輯、迴圈、Await&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-3&#x2F;&quot;&gt;Svelte 從小白到入門（三）事件&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-4&#x2F;&quot;&gt;Svelte 從小白到入門（四）數據綁定&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-5&#x2F;&quot;&gt;Svelte 從小白到入門（五）生命週期&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-6&#x2F;&quot;&gt;Svelte 從小白到入門（六）Store&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-7&#x2F;&quot;&gt;Svelte 從小白到入門（七）動態效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-8&#x2F;&quot;&gt;Svelte 從小白到入門（八）進出場效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-9&#x2F;&quot;&gt;Svelte 從小白到入門（九）動畫效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-10&#x2F;&quot;&gt;Svelte 從小白到入門（十）Action&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-11&#x2F;&quot;&gt;Svelte 從小白到入門（十一）CSS Class&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-12&#x2F;&quot;&gt;Svelte 從小白到入門（十二）Slot&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-13&#x2F;&quot;&gt;Svelte 從小白到入門（十三）Context&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-14&#x2F;&quot;&gt;Svelte 從小白到入門（十四）Svelte 的特殊元素&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-15&#x2F;&quot;&gt;Svelte 從小白到入門（十五）Module Context&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-16&#x2F;&quot;&gt;Svelte 從小白到入門（十六）Debugging&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-17&#x2F;&quot;&gt;Svelte 從小白到入門（十七）結束與展望&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;jin-chu-chang-xiao-guo&quot;&gt;進出場效果&lt;&#x2F;h2&gt;
&lt;p&gt;進出場效果指的是 HTML 元素在出現或消失時的微互動效果，常見的有淡入、淡出、滑進、滑出等，這些效果都是原生 CSS 與 JS 的再封裝，由 Svelte 開發團隊改造成在 Svelte 元件內的語法。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;dan-ru-dan-chu&quot;&gt;淡入淡出&lt;&#x2F;h3&gt;
&lt;p&gt;最基本的用法，在 JS 引入 &lt;code&gt;fade&lt;&#x2F;code&gt;，在 HTML 標籤內以 &lt;code&gt;transition:fade&lt;&#x2F;code&gt; 的形式使用：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 600px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;39646ef156fa4174b284b6a889475f98?version=3.43.2&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;在此之前我們見過 &lt;code&gt;on:&lt;&#x2F;code&gt;、&lt;code&gt;bind:&lt;&#x2F;code&gt;，現在這類的語句又出現了新的 &lt;code&gt;transition:&lt;&#x2F;code&gt;，應該見怪不怪了…吧？&lt;&#x2F;p&gt;
&lt;h3 id=&quot;xiao-guo-can-shu&quot;&gt;效果參數&lt;&#x2F;h3&gt;
&lt;p&gt;進出場效果也可以配置參數，下面的例子是飛進飛出：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 600px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;c50be46e3b6a4d48a0e93e941569b493?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;我們用 &lt;code&gt;transition:fly=&quot;{}&quot;&lt;&#x2F;code&gt; 裡面放個物件 &lt;code&gt;{ y: 200, duration: 2000 }&lt;&#x2F;code&gt; 做為 fly 效果的配置參數。&lt;&#x2F;p&gt;
&lt;p&gt;如同上一集的 motion，即使文字飛到一半就切換狀態，Svelte 也會幫我們立即反應，不用等到第一段全部飛完才跑第二段的效果。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;jin-chang-chu-chang-bu-tong-xiao-guo&quot;&gt;進場、出場不同效果&lt;&#x2F;h3&gt;
&lt;p&gt;用 &lt;code&gt;transition:&lt;&#x2F;code&gt; 會使進場、出場同效果，如果想追求更虛華的進出場不同效果，那可以改用 &lt;code&gt;in:&lt;&#x2F;code&gt; 與 &lt;code&gt;out:&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;合併前兩個例子：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 600px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;2eec6b52e7bf47cc83016686a793d61f?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;飛入淡出，夠虛華！&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zi-ding-yi-css-xiao-guo&quot;&gt;自定義 CSS 效果&lt;&#x2F;h3&gt;
&lt;p&gt;如果追求更虛華的效果，也可以自製效果，自製效果的函式，必須接受至少一個 &lt;code&gt;node&lt;&#x2F;code&gt; 參數，表示效果要應用的 HTML 元素，以及回傳一個效果物件，這個回傳的物件可以有這些成員：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;delay&lt;&#x2F;code&gt; — milliseconds before the transition begins&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;duration&lt;&#x2F;code&gt; — length of the transition in milliseconds&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;easing&lt;&#x2F;code&gt; — a &lt;code&gt;p =&amp;gt; t&lt;&#x2F;code&gt; easing function (see the chapter on tweening)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;css&lt;&#x2F;code&gt; — a &lt;code&gt;(t, u) =&amp;gt; css&lt;&#x2F;code&gt; function, where &lt;code&gt;u === 1 - t&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;tick&lt;&#x2F;code&gt; — a &lt;code&gt;(t, u) =&amp;gt; {...}&lt;&#x2F;code&gt; function that has some effect on the node&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;以上太高深不解釋，實際看下面的例子：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 1300px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;bdc788a7d5dd4f8685e50326fa11ea0e?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;上面的 &lt;code&gt;spin()&lt;&#x2F;code&gt; 的回傳值內的 &lt;code&gt;css&lt;&#x2F;code&gt; 基本上是一系列 CSS 樣式，而這些樣式的值隨著 &lt;code&gt;elasticOut(t)&lt;&#x2F;code&gt; 而變，&lt;code&gt;t&lt;&#x2F;code&gt; 則是一個隨時間變化的變數，效果發生時為 &lt;code&gt;0&lt;&#x2F;code&gt;，效果結束時為 &lt;code&gt;1&lt;&#x2F;code&gt;，而效果發生途中 &lt;code&gt;t&lt;&#x2F;code&gt; 的值則由 &lt;code&gt;easing&lt;&#x2F;code&gt; 變數而定，總的來說這就是一個時間函數，而 &lt;code&gt;t&lt;&#x2F;code&gt; 嚴格的說應該是 &lt;code&gt;tick&lt;&#x2F;code&gt;，但簡單的理解 &lt;code&gt;t&lt;&#x2F;code&gt; 就是時間。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zi-ding-yi-js-xiao-guo&quot;&gt;自定義 JS 效果&lt;&#x2F;h3&gt;
&lt;p&gt;某些效果難以用 CSS 實現，那麼可以以 JS 實現：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 1100px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;ca318df4ab504a41affc06ec82835377?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;h3 id=&quot;xiao-guo-chu-fa-shi-jian&quot;&gt;效果觸發事件&lt;&#x2F;h3&gt;
&lt;p&gt;這些進出場效果作用的同時，也會觸發事件，我們可以用 &lt;code&gt;on:&lt;&#x2F;code&gt; 的語法來呼叫效果發生或結束時要執行的函式。&lt;&#x2F;p&gt;
&lt;p&gt;與效果相關的事件如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;introstart&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;outrostart&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;introend&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;outroend&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;應該可以望文生義。&lt;&#x2F;p&gt;
&lt;p&gt;範例如下：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 800px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;f9f93ccc9e974bf9afca670515d24389?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;h3 id=&quot;qu-yu-xiao-guo&quot;&gt;區域效果&lt;&#x2F;h3&gt;
&lt;p&gt;在下面的範例中，&lt;code&gt;slide&lt;&#x2F;code&gt; 是寫在最內層的 &lt;code&gt;&amp;lt;div&amp;gt;&lt;&#x2F;code&gt; 內，然而在 Svelte 的設計上，外層的容器一旦狀態改變，也會引發內層的 &lt;code&gt;&amp;lt;div&amp;gt;&lt;&#x2F;code&gt; 的 &lt;code&gt;slide&lt;&#x2F;code&gt; 的效果，所以當我們對「show list」做切換，那些 one、two、three 等的物件也都會產生滑動，如果不想要這樣，我們得為效果加上 &lt;code&gt;local&lt;&#x2F;code&gt; 這個修飾器，這個修飾器限定了效果只會受元素本身的容器觸發：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 1000px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;f48f1e8864774d5bac550313994c4f41?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;h3 id=&quot;crossfade&quot;&gt;Crossfade&lt;&#x2F;h3&gt;
&lt;p&gt;Crossfade 華文也叫淡入淡出，是指元素從 A 處淡出，並且在 B 處淡入。&lt;&#x2F;p&gt;
&lt;p&gt;下面的 to-do list 範例裡，當左邊「todo」內的工項被打勾，就會移動到右邊的「done」內，反之亦然：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 80vmax; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;fd65b068ca864012997e69aec7f0be74?version=3.43.2&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;在程式邏輯方面，所有的工項被存放在一個 &lt;code&gt;todos&lt;&#x2F;code&gt; 的陣列裡，當工項的 checkbox 被切換，會觸發 &lt;code&gt;mark()&lt;&#x2F;code&gt; 改變該工項是否已完成的記號 &lt;code&gt;done&lt;&#x2F;code&gt;，後面呼叫的 &lt;code&gt;remove()&lt;&#x2F;code&gt; 與 &lt;code&gt;concat()&lt;&#x2F;code&gt; 則分別把該工項從 &lt;code&gt;todo&lt;&#x2F;code&gt; 內移除，及再放回陣列內成為最後一個成員。&lt;&#x2F;p&gt;
&lt;p&gt;在效果方面我們引入了 &lt;code&gt;crossfade()&lt;&#x2F;code&gt; 函式，它有兩個效果可用，&lt;code&gt;send&lt;&#x2F;code&gt; 與 &lt;code&gt;receive&lt;&#x2F;code&gt;，對左邊的 todo 內的工項來說，當它被打勾時，整個 todo 區的子陣列發生改變，會觸發該工項的 &lt;code&gt;out:send={{key: todo.id}}&lt;&#x2F;code&gt;，而在右邊的 done 的子陣列也會發生改變，觸發該工項的 &lt;code&gt;in:receive={{key: todo.id}}&lt;&#x2F;code&gt;，此時 &lt;code&gt;out:send&lt;&#x2F;code&gt; 的這邊會移動並淡出，&lt;code&gt;in:receive&lt;&#x2F;code&gt; 的這邊也會移動並淡入，最後給予用戶的感受是無縫且絲滑的操作體驗。&lt;&#x2F;p&gt;
&lt;p&gt;上面的 &lt;code&gt;send&lt;&#x2F;code&gt; 與 &lt;code&gt;receive&lt;&#x2F;code&gt; 都接受 &lt;code&gt;{key: todo.id}&lt;&#x2F;code&gt; 這物件當作參數，這裡的 &lt;code&gt;key&lt;&#x2F;code&gt; 運用到了第二集提過的「迴圈加 key」的技巧，讓不管是 todo 區或是 done 區，工項的切來切去都可以使 crossfade 效果應用在正確的對象上。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;crossfade-de-fallback-han-shi&quot;&gt;Crossfade 的 Fallback 函式&lt;&#x2F;h3&gt;
&lt;p&gt;把上面的範例再弄複雜一點，工項除了移來移去外，用戶應該還可以加新工項及刪工項，添加與刪除並沒有「A 處移到 B 處」的情境，因此 &lt;code&gt;send&lt;&#x2F;code&gt;、&lt;code&gt;receive&lt;&#x2F;code&gt; 也無從發揮，當 &lt;code&gt;send&lt;&#x2F;code&gt; 或 &lt;code&gt;receive&lt;&#x2F;code&gt; 找不到目的地時，只要在 &lt;code&gt;crossfade()&lt;&#x2F;code&gt; 函式內做一個 &lt;code&gt;fallback()&lt;&#x2F;code&gt; 函式，就會調用那個 &lt;code&gt;fallback()&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 80vmax; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;c989c1b795cb4b82998443962653f609?version=3.43.2&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;h3 id=&quot;key-qu-kuai&quot;&gt;Key 區塊&lt;&#x2F;h3&gt;
&lt;p&gt;前面的進出場效果都是以 HTML 元素的狀態發生改變為基礎，若是想以 HTML 的內容的變化而觸發進出場效果，那得利用 key 區塊：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 600px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;11c0f0e842b34336a0947dc281de1795?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;在上面的示例中，按鈕只會改變 &lt;code&gt;number&lt;&#x2F;code&gt; 的值，不影響 &lt;code&gt;&amp;lt;p&amp;gt;&lt;&#x2F;code&gt;，因此我們在 &lt;code&gt;{number}&lt;&#x2F;code&gt; 包一層 &lt;code&gt;&amp;lt;span in:fly=&quot;{{ y: -20 }}&quot;&amp;gt;&lt;&#x2F;code&gt; 用於聲明進場效果及參數，並在更外層包覆 &lt;code&gt;{#key number}&lt;&#x2F;code&gt; 用於聲明當區塊內的 &lt;code&gt;{number}&lt;&#x2F;code&gt; 改變時，要觸發那 fly 進場效果。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xiao-jie&quot;&gt;小結&lt;&#x2F;h2&gt;
&lt;p&gt;整理一下截至目前為止 Svelte 在 HTML 標籤內出現過的幾種聲明式語句：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;on:&lt;&#x2F;code&gt;：綁定事件與函式&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;bind:&lt;&#x2F;code&gt;：綁定數據&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;transition:&lt;&#x2F;code&gt;：聲明進出場效果&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;in:&lt;&#x2F;code&gt;：聲明進場效果&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;out:&lt;&#x2F;code&gt;：聲明出場效果&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;本集重點整理：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;對於效果，我們可以用 &lt;code&gt;local&lt;&#x2F;code&gt; 修飾器限定效果的作用域只針對該效果本身的的容器反映，而不會受到更外層的容器觸發&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;crossfade()&lt;&#x2F;code&gt; 及相關的 &lt;code&gt;send&lt;&#x2F;code&gt;、&lt;code&gt;receive&lt;&#x2F;code&gt; 效果可以幫我們做到「從 A 處淡出，往 B 處淡入」的效果&lt;&#x2F;li&gt;
&lt;li&gt;本集也認識了 HTML 內的新語句 &lt;code&gt;{#key}&lt;&#x2F;code&gt; 區塊，參見最後一個範例 &lt;code&gt;{#key number}&lt;&#x2F;code&gt;，當 &lt;code&gt;{number}&lt;&#x2F;code&gt; 發生變化時，觸發區塊內的進出場效果。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Svelte 從小白到入門（七）動態效果</title>
        <published>2021-10-16T00:00:00+00:00</published>
        <updated>2021-10-16T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/svelte-quickstart-7/"/>
        <id>https://editor.leonh.space/2021/svelte-quickstart-7/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/svelte-quickstart-7/">&lt;p&gt;這是〈Svelte 從小白到入門〉系列的第七集，各集連結如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-1&#x2F;&quot;&gt;Svelte 從小白到入門（一）元件、事件、Reactivity&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-2&#x2F;&quot;&gt;Svelte 從小白到入門（二）Props、邏輯、迴圈、Await&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-3&#x2F;&quot;&gt;Svelte 從小白到入門（三）事件&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-4&#x2F;&quot;&gt;Svelte 從小白到入門（四）數據綁定&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-5&#x2F;&quot;&gt;Svelte 從小白到入門（五）生命週期&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-6&#x2F;&quot;&gt;Svelte 從小白到入門（六）Store&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-7&#x2F;&quot;&gt;Svelte 從小白到入門（七）動態效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-8&#x2F;&quot;&gt;Svelte 從小白到入門（八）進出場效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-9&#x2F;&quot;&gt;Svelte 從小白到入門（九）動畫效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-10&#x2F;&quot;&gt;Svelte 從小白到入門（十）Action&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-11&#x2F;&quot;&gt;Svelte 從小白到入門（十一）CSS Class&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-12&#x2F;&quot;&gt;Svelte 從小白到入門（十二）Slot&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-13&#x2F;&quot;&gt;Svelte 從小白到入門（十三）Context&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-14&#x2F;&quot;&gt;Svelte 從小白到入門（十四）Svelte 的特殊元素&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-15&#x2F;&quot;&gt;Svelte 從小白到入門（十五）Module Context&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-16&#x2F;&quot;&gt;Svelte 從小白到入門（十六）Debugging&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-17&#x2F;&quot;&gt;Svelte 從小白到入門（十七）結束與展望&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;tweening&quot;&gt;Tweening&lt;&#x2F;h2&gt;
&lt;p&gt;當某個值變動要反映在畫面上時，Svelte 提供了 tweening 工具讓元件在畫面上的過渡過程是平滑的。&lt;&#x2F;p&gt;
&lt;p&gt;所謂的「tweening」指的是一個物件從 A 狀態變化成 B 狀態時，中間的那些過渡過程，藉由 Svelte 提供的 &lt;code&gt;tweened&lt;&#x2F;code&gt; 函式，可以協助我們產生那些過渡，畢竟不可能由人工去定義每一瞬間的狀態。&lt;&#x2F;p&gt;
&lt;p&gt;在下面的例子裡，按鈕會改變進度條的狀態（數值），在沒有 tweening 以前，就是很直接的從 0% 切換到 100%，加上 tweening 效果後，進度條從 0% 到 100% 的過程就會是平滑過渡的：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 800px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;4a4c475a2a5543b69c847ac6f7bc2001?version=3.43.2&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;在把玩時，還可以注意到，在進度條跑到中間時，又按下 0% 的按鈕，此時跑到一半的進度條會開始倒退走，而不是傻傻的跑到底才倒退走，這些 UX 上的細節 Svelte 都已經幫我們顧慮到了，揪感心～&lt;&#x2F;p&gt;
&lt;p&gt;在程式碼的部份，我們用 &lt;code&gt;tweened()&lt;&#x2F;code&gt; 函式建構一個 tweening 物件 &lt;code&gt;progress&lt;&#x2F;code&gt;，它的設值方法和上一集的 store 如出一轍都是 &lt;code&gt;set()&lt;&#x2F;code&gt;，就不著墨太多。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;tweened()&lt;&#x2F;code&gt; 函式本身接受兩個參數，第一個參數是 &lt;code&gt;progress&lt;&#x2F;code&gt; 的初值，第二個參數是它的一些細部配置，這裡我們指定了 tweening 過渡的時長為 4,000 ms，而 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;easings.net&#x2F;zh-tw&quot;&gt;easing&lt;&#x2F;a&gt; 模式為 &lt;code&gt;cubicOut&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;tweened()&lt;&#x2F;code&gt; 的配置參數如下（太深奧了不解釋）：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;delay&lt;&#x2F;code&gt; — milliseconds before the tween starts&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;duration&lt;&#x2F;code&gt; — either the duration of the tween in milliseconds, or a &lt;code&gt;(from, to) =&amp;gt; milliseconds&lt;&#x2F;code&gt; function allowing you to (e.g.) specify longer tweens for larger changes in value&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;easing&lt;&#x2F;code&gt; — a &lt;code&gt;p =&amp;gt; t&lt;&#x2F;code&gt; function&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;interpolate&lt;&#x2F;code&gt; — a custom &lt;code&gt;(from, to) =&amp;gt; t =&amp;gt; value&lt;&#x2F;code&gt; function for interpolating between arbitrary values. By default, Svelte will interpolate between numbers, dates, and identically-shaped arrays and objects (as long as they only contain numbers and dates or other valid arrays and objects). If you want to interpolate (for example) colour strings or transformation matrices, supply a custom interpolator&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;q-dan-xiao-guo&quot;&gt;Q 彈效果&lt;&#x2F;h2&gt;
&lt;p&gt;還有一個函式是 &lt;code&gt;spring()&lt;&#x2F;code&gt;，顧名思義就是做出像彈簧般 Q 彈的效果，除了配置參數不同外，用起來和 &lt;code&gt;tweened()&lt;&#x2F;code&gt; 大同小異。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;spring()&lt;&#x2F;code&gt; 的配置參數有：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;stiffness&lt;&#x2F;code&gt;：剛性係數&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;damping&lt;&#x2F;code&gt;：阻尼係數&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;他們的作用可以透過實例感受，在下面的例子裡，我們設定 &lt;code&gt;spring()&lt;&#x2F;code&gt; 的初值為 &lt;code&gt;{ x: 50, y: 50 }&lt;&#x2F;code&gt; 的座標點，並將此座標設為變數 &lt;code&gt;coords&lt;&#x2F;code&gt; 綁定到小黑點上，小黑球的移動則是靠一系列的 &lt;code&gt;on:&lt;&#x2F;code&gt; 綁定滑鼠事件與匿名函式，一旦滑鼠從 A 點移動到 B 點，則 &lt;code&gt;spring()&lt;&#x2F;code&gt; 會幫我們計算小黑球在兩點中間的 Q 彈行為：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 1100px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;4ba9655002c64b69851a7b29a1f2652b?version=3.43.2&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;h2 id=&quot;xiao-jie&quot;&gt;小結&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;tweened()&lt;&#x2F;code&gt; 與 &lt;code&gt;spring()&lt;&#x2F;code&gt; 都有著與 store 相同的 &lt;code&gt;set()&lt;&#x2F;code&gt;、&lt;code&gt;update()&lt;&#x2F;code&gt; 方法，實際上，&lt;code&gt;tweened()&lt;&#x2F;code&gt; 與 &lt;code&gt;spring()&lt;&#x2F;code&gt; 就是上一集介紹過的 custom store，是 Svelte 感心的為用戶預先準備好的工具，真的是揪感心耶！&lt;&#x2F;p&gt;
&lt;p&gt;本集重點整理：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Tweening 指的是起末兩狀態間的過渡狀態，Svelte 提供了 &lt;code&gt;tweened()&lt;&#x2F;code&gt; 幫我們處理掉 tweening 的工作&lt;&#x2F;li&gt;
&lt;li&gt;Tweening 中間的過渡模式稱為 easing，有許多業界既有的 easing 模式可選用&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;spring()&lt;&#x2F;code&gt; 一樣是處理過渡狀態，並且是產生 Q 彈的彈簧效果，喜歡 Q 彈的人不能錯過&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;tweened()&lt;&#x2F;code&gt; 與 &lt;code&gt;spring()&lt;&#x2F;code&gt; 也都是 store，再加上被賦予的額外特性&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Svelte 從小白到入門（六）Store</title>
        <published>2021-10-13T00:00:00+00:00</published>
        <updated>2021-10-13T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/svelte-quickstart-6/"/>
        <id>https://editor.leonh.space/2021/svelte-quickstart-6/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/svelte-quickstart-6/">&lt;p&gt;這是〈Svelte 從小白到入門〉系列的第六集，各集連結如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-1&#x2F;&quot;&gt;Svelte 從小白到入門（一）元件、事件、Reactivity&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-2&#x2F;&quot;&gt;Svelte 從小白到入門（二）Props、邏輯、迴圈、Await&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-3&#x2F;&quot;&gt;Svelte 從小白到入門（三）事件&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-4&#x2F;&quot;&gt;Svelte 從小白到入門（四）數據綁定&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-5&#x2F;&quot;&gt;Svelte 從小白到入門（五）生命週期&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-6&#x2F;&quot;&gt;Svelte 從小白到入門（六）Store&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-7&#x2F;&quot;&gt;Svelte 從小白到入門（七）動態效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-8&#x2F;&quot;&gt;Svelte 從小白到入門（八）進出場效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-9&#x2F;&quot;&gt;Svelte 從小白到入門（九）動畫效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-10&#x2F;&quot;&gt;Svelte 從小白到入門（十）Action&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-11&#x2F;&quot;&gt;Svelte 從小白到入門（十一）CSS Class&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-12&#x2F;&quot;&gt;Svelte 從小白到入門（十二）Slot&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-13&#x2F;&quot;&gt;Svelte 從小白到入門（十三）Context&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-14&#x2F;&quot;&gt;Svelte 從小白到入門（十四）Svelte 的特殊元素&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-15&#x2F;&quot;&gt;Svelte 從小白到入門（十五）Module Context&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-16&#x2F;&quot;&gt;Svelte 從小白到入門（十六）Debugging&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-17&#x2F;&quot;&gt;Svelte 從小白到入門（十七）結束與展望&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;h2 id=&quot;store&quot;&gt;Store&lt;&#x2F;h2&gt;
&lt;p&gt;Store 是 Svelte 的資料交換機制，被定義成 store 的變數，可以被多個元件使用，一個 store 變數的值的變化，也可以透過 subscribe 的機制，即時反映給元件們。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ke-du-xie-de-store&quot;&gt;可讀寫的 Store&lt;&#x2F;h3&gt;
&lt;p&gt;在下面的例子裡，我們利用 Svelte 的 &lt;code&gt;writable()&lt;&#x2F;code&gt; 函式在 store.js 內定義了一個可讀寫的 store 變數 &lt;code&gt;count&lt;&#x2F;code&gt;，令其初始值為 &lt;code&gt;0&lt;&#x2F;code&gt;，並利用 JS 的標準關鍵字 &lt;code&gt;export&lt;&#x2F;code&gt; 令 &lt;code&gt;count&lt;&#x2F;code&gt; 可被外部調用，如此在其他的 Svelte 元件內即可引入 &lt;code&gt;count&lt;&#x2F;code&gt; 並讀寫它：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 600px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;ace5e93850fc462485d18646aee282a1?version=3.43.1&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;在 Incrementer 和 Decrementer 裡，我們調用了 &lt;code&gt;count.update()&lt;&#x2F;code&gt; 來更新 &lt;code&gt;count&lt;&#x2F;code&gt; 的值，因為這裡的 &lt;code&gt;count&lt;&#x2F;code&gt; 的值（&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;ithelp.ithome.com.tw&#x2F;articles&#x2F;10266925&quot;&gt;literal&lt;&#x2F;a&gt;）並不是 primitive 的 number，而是透過 &lt;code&gt;writable()&lt;&#x2F;code&gt; 建構出的物件，得用它自有的方法來更新，在 Resetter 內也是一樣，我們調用的是 &lt;code&gt;count.set()&lt;&#x2F;code&gt; 來賦值。&lt;&#x2F;p&gt;
&lt;p&gt;在 App 這邊，我們用 &lt;code&gt;count.subscribe()&lt;&#x2F;code&gt; 讓 &lt;code&gt;count&lt;&#x2F;code&gt; 的值的變化即時反映到 App 的變數 &lt;code&gt;count_value&lt;&#x2F;code&gt; 身上。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zi-dong-ding-yue&quot;&gt;自動訂閱&lt;&#x2F;h3&gt;
&lt;p&gt;Svelte store 的觀察者模式還有更簡單的用法，沿用上面的例子，把 &lt;code&gt;count.subscribe()&lt;&#x2F;code&gt; 那塊程式碼拿掉，直接在 HTML 內調用 &lt;code&gt;$count&lt;&#x2F;code&gt; 就做完了：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 600px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;979ef7ef6a44448485a93b0bd556ec34?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;&lt;code&gt;$count&lt;&#x2F;code&gt; 自動幫我們訂閱 &lt;code&gt;count&lt;&#x2F;code&gt;，也就是當 &lt;code&gt;count&lt;&#x2F;code&gt; 狀態改變時，&lt;code&gt;$count&lt;&#x2F;code&gt; 也會改變，&lt;code&gt;$count&lt;&#x2F;code&gt; 也幫我們自動關訂閱，當元件銷毀時，&lt;code&gt;$count&lt;&#x2F;code&gt; 自動關訂閱。&lt;&#x2F;p&gt;
&lt;p&gt;另外 &lt;code&gt;$count&lt;&#x2F;code&gt; 也是個 JS 變數，雖然它是由 Svelte 幫我們產生的，並且 Svelte 有對它賦予了上面的特性，但它依然是個能如同一般變數般被操作的變數，所以我們能用在 JS 區域內用 &lt;code&gt;console.log($count)&lt;&#x2F;code&gt; 來印出它的值，也可以用 &lt;code&gt;$count += 1&lt;&#x2F;code&gt; 來改它的值。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ke-du-bu-ke-xie-de-store&quot;&gt;可讀不可寫的 Store&lt;&#x2F;h3&gt;
&lt;p&gt;某些情況下我們想讓 store 僅可被讀取，但不可被寫入，我們可以改用 readable store。&lt;&#x2F;p&gt;
&lt;p&gt;下面的例子裡，&lt;code&gt;time&lt;&#x2F;code&gt; 是一個僅供讀取的 store：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 400px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;ce73f6b7fa6e4b2abf027b4f06f543c2?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;在 stores.js 裡，我們改用 &lt;code&gt;readable()&lt;&#x2F;code&gt; 來建構 store。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;readable()&lt;&#x2F;code&gt; 接受的第一個參數，是 store 的初值，第二個參數是個 callback 函式，會在 store 被初次訂閱時執行，這裡我們命名為 &lt;code&gt;start()&lt;&#x2F;code&gt;，&lt;code&gt;start()&lt;&#x2F;code&gt; 裡面的 &lt;code&gt;set&lt;&#x2F;code&gt; 又是一個 callback 函式，用於設定 store 的值，這裡我們讓它每秒跑一次把 &lt;code&gt;time&lt;&#x2F;code&gt; 更新到當前的時間點，而最後的 return 函式又是個 callback 函式，會在所有的訂閱都關閉後執行。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;yan-sheng-de-store&quot;&gt;衍生的 Store&lt;&#x2F;h3&gt;
&lt;p&gt;以某個 store 為基礎，可以做出衍生的 store。&lt;&#x2F;p&gt;
&lt;p&gt;沿用上面的範例，下面加一行頁面開啟時間的計時器：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 500px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;fa3cbf744c78465785195f3ac55aaf7b?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;&lt;code&gt;derived()&lt;&#x2F;code&gt; 用法與上面類似，第一個參數是初值，第二個參數則是一個函式，藉由 reactivity 的特性隨 &lt;code&gt;$time&lt;&#x2F;code&gt; 更新 &lt;code&gt;elapsed&lt;&#x2F;code&gt; 這個衍生 store。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zi-ding-yi-store-xing-wei&quot;&gt;自定義 Store 行為&lt;&#x2F;h3&gt;
&lt;p&gt;除了內建的 &lt;code&gt;subscribe()&lt;&#x2F;code&gt;、&lt;code&gt;set()&lt;&#x2F;code&gt;、&lt;code&gt;update()&lt;&#x2F;code&gt; 等行為，也可以在 store 內定義自己特有的方法。&lt;&#x2F;p&gt;
&lt;p&gt;我們沿用前面的例子，但這次把按鈕的行為定義在 store 內，不再做成獨立的元件：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 400px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;79437f1f631741f5af5c88951bc2113a?version=3.43.1&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;我們在 store.js 裡面做了幾件事：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;count&lt;&#x2F;code&gt; 改由自訂函式 &lt;code&gt;createCount()&lt;&#x2F;code&gt; 建構&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;createCount()&lt;&#x2F;code&gt; 內引入了 &lt;code&gt;writable()&lt;&#x2F;code&gt; 的三個方法 &lt;code&gt;subscribe&lt;&#x2F;code&gt;、&lt;code&gt;update&lt;&#x2F;code&gt; 和 &lt;code&gt;set&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;createCount()&lt;&#x2F;code&gt; 還自定義了 &lt;code&gt;increment&lt;&#x2F;code&gt;、&lt;code&gt;decrement&lt;&#x2F;code&gt;、&lt;code&gt;reset&lt;&#x2F;code&gt; 三個方法&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;createCount()&lt;&#x2F;code&gt; 最後暴露在外的只有 &lt;code&gt;subscribe&lt;&#x2F;code&gt; 以及自訂的 &lt;code&gt;increment&lt;&#x2F;code&gt;、&lt;code&gt;decrement&lt;&#x2F;code&gt;、&lt;code&gt;reset&lt;&#x2F;code&gt;，來自 &lt;code&gt;writeable()&lt;&#x2F;code&gt; 的 &lt;code&gt;update&lt;&#x2F;code&gt; 和 &lt;code&gt;set&lt;&#x2F;code&gt; 並不對外暴露&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在這樣的改造之後，外面的元件在調用 &lt;code&gt;count&lt;&#x2F;code&gt; 時，就只能呼叫允許的 &lt;code&gt;subscribe&lt;&#x2F;code&gt;、&lt;code&gt;increment&lt;&#x2F;code&gt;、&lt;code&gt;decrement&lt;&#x2F;code&gt;、&lt;code&gt;reset&lt;&#x2F;code&gt; 四個方法，而 &lt;code&gt;update&lt;&#x2F;code&gt; 和 &lt;code&gt;set&lt;&#x2F;code&gt; 將無法使用。其中 &lt;code&gt;subscribe&lt;&#x2F;code&gt; 是 store 必須存在的要素，一定要開放外界呼叫。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;store-bang-ding&quot;&gt;Store 綁定&lt;&#x2F;h3&gt;
&lt;p&gt;Store 也可以做數據綁定，在第四集我們見過一個典型的數據綁定會是像這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;input&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; bind:value&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;{name}&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;要對 Store 綁定，也是類似的做法，回顧上面的自動訂閱一節，在 Svelte 的 HTML 區域內可以用 &lt;code&gt;{$name}&lt;&#x2F;code&gt; 的方式調用 store，而 &lt;code&gt;$name&lt;&#x2F;code&gt; 也只不是個被賦予 store 特性的一個變數，因此要對 store 做綁定，也是用同樣的邏輯：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;input&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; bind:value&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;{$name}&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;完整的範例如下：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 500px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;85bed18798c14ddcad7ffd12f05edf52?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;在範例中的按鈕，它的點擊觸發的匿名函式會把 &lt;code&gt;$name&lt;&#x2F;code&gt; 後面加個驚嘆號，注意到我們並沒有用 &lt;code&gt;set()&lt;&#x2F;code&gt; 而是用更直覺的 &lt;code&gt;$name += &#x27;!&#x27;&lt;&#x2F;code&gt;，這與 &lt;code&gt;set()&lt;&#x2F;code&gt; 是等價的，Svelte 會幫我們處理好。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xiao-jie&quot;&gt;小結&lt;&#x2F;h2&gt;
&lt;p&gt;Store 與 props 同樣作為元件間資料的交換機制，Store 有著這些與 props 不同的特性：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Store 是獨立於元件的，而 props 是元件內的&lt;&#x2F;li&gt;
&lt;li&gt;Store 在使用上與元件的父子關係無關，而 props 的使用都是父元件傳給子元件的&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;本集重點整理：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Store 分為可讀寫的、可讀不可寫的、衍生的&lt;&#x2F;li&gt;
&lt;li&gt;Store 被設計成具有觀察者模式的特性，元件可以訂閱一個 store，當 store 變化時，會反映到那些訂閱者身上&lt;&#x2F;li&gt;
&lt;li&gt;Store 有簡單的訂閱、關訂閱語法，以一個名為 &lt;code&gt;name&lt;&#x2F;code&gt; 的 store 為例，元件可以用 &lt;code&gt;$name&lt;&#x2F;code&gt; 調用它，而 Svelte 會自動處理 &lt;code&gt;$name&lt;&#x2F;code&gt; 與 &lt;code&gt;name&lt;&#x2F;code&gt; 間的訂閱、關訂閱工作&lt;&#x2F;li&gt;
&lt;li&gt;Store 也可以綁定，延續上例，可以用 &lt;code&gt;bind:value={$name}&lt;&#x2F;code&gt; 的形式做綁定&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Svelte 從小白到入門（五）生命週期</title>
        <published>2021-10-12T00:00:00+00:00</published>
        <updated>2021-10-12T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/svelte-quickstart-5/"/>
        <id>https://editor.leonh.space/2021/svelte-quickstart-5/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/svelte-quickstart-5/">&lt;p&gt;這是〈Svelte 從小白到入門〉系列的第五集，各集連結如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-1&#x2F;&quot;&gt;Svelte 從小白到入門（一）元件、事件、Reactivity&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-2&#x2F;&quot;&gt;Svelte 從小白到入門（二）Props、邏輯、迴圈、Await&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-3&#x2F;&quot;&gt;Svelte 從小白到入門（三）事件&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-4&#x2F;&quot;&gt;Svelte 從小白到入門（四）數據綁定&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-5&#x2F;&quot;&gt;Svelte 從小白到入門（五）生命週期&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-6&#x2F;&quot;&gt;Svelte 從小白到入門（六）Store&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-7&#x2F;&quot;&gt;Svelte 從小白到入門（七）動態效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-8&#x2F;&quot;&gt;Svelte 從小白到入門（八）進出場效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-9&#x2F;&quot;&gt;Svelte 從小白到入門（九）動畫效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-10&#x2F;&quot;&gt;Svelte 從小白到入門（十）Action&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-11&#x2F;&quot;&gt;Svelte 從小白到入門（十一）CSS Class&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-12&#x2F;&quot;&gt;Svelte 從小白到入門（十二）Slot&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-13&#x2F;&quot;&gt;Svelte 從小白到入門（十三）Context&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-14&#x2F;&quot;&gt;Svelte 從小白到入門（十四）Svelte 的特殊元素&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-15&#x2F;&quot;&gt;Svelte 從小白到入門（十五）Module Context&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-16&#x2F;&quot;&gt;Svelte 從小白到入門（十六）Debugging&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-17&#x2F;&quot;&gt;Svelte 從小白到入門（十七）結束與展望&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;本週末是台灣國慶連假，封面圖特別改用今年國慶的視覺主題「金陽雙十」的編織圖案，祝台灣生日快樂，也感謝這個自由的國度讓我們擁有免於被囚禁在網路長城內的自由。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;sheng-ming-zhou-qi&quot;&gt;生命週期&lt;&#x2F;h2&gt;
&lt;p&gt;所謂的生命週期，指的是一個 Svelte 元件從被創建到被銷毀之間的幾個關鍵時機點，其中的 &lt;code&gt;onMount&lt;&#x2F;code&gt; 的時機點為元件被瀏覽器 render 出來後的那一刻。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;onmount&quot;&gt;onMount&lt;&#x2F;h3&gt;
&lt;p&gt;在&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-4&#x2F;#this&quot;&gt;上一集的 canvas 範例裡&lt;&#x2F;a&gt;，有出現過 &lt;code&gt;onMount()&lt;&#x2F;code&gt; 的函式，當時我們做的是把 &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;&#x2F;code&gt; 元素綁定到 JS 的 &lt;code&gt;canvas&lt;&#x2F;code&gt; 變數上，並利用 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;developer.mozilla.org&#x2F;zh-TW&#x2F;docs&#x2F;Web&#x2F;API&#x2F;Canvas_API&quot;&gt;Canvas API&lt;&#x2F;a&gt; 撰寫它的一些動態效果，因為必須調用到瀏覽器提供的 Canvas API，就必然得等到 &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;&#x2F;code&gt; 被瀏覽器 render 後才能正確調用，又或者是某些行為必許操作元件內的其他元素，也必須等待瀏覽器把元件都 render 後才可以操作，因此我們才把那些動態效果的敘述放在 &lt;code&gt;onMount()&lt;&#x2F;code&gt; 裡面。&lt;&#x2F;p&gt;
&lt;p&gt;下面的這個例子，我們在 &lt;code&gt;onMount()&lt;&#x2F;code&gt; 裡面用 &lt;code&gt;fetch()&lt;&#x2F;code&gt; 去抓外部 API 的用戶頭像，並顯示出來：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 700px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;9118e61b188a410ea304f6121df52401?version=3.43.1&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;在 &lt;code&gt;onMount()&lt;&#x2F;code&gt; 內的程式碼，不會在 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;progressbar.tw&#x2F;posts&#x2F;297&quot;&gt;SSR&lt;&#x2F;a&gt; 階段被執行，而像 &lt;code&gt;fetch()&lt;&#x2F;code&gt; 這個通常用於抓取動態內容的函式也不適合在 SSR 執行，因此放在 &lt;code&gt;onMount()&lt;&#x2F;code&gt; 內也算是適得其所。&lt;&#x2F;p&gt;
&lt;p&gt;另外注意到，在第二集曾經提過的 &lt;code&gt;{#each}&lt;&#x2F;code&gt; 迴圈也可以用 &lt;code&gt;{:else}&lt;&#x2F;code&gt;，當 &lt;code&gt;photos&lt;&#x2F;code&gt; 陣列還未生成時，可以用 &lt;code&gt;{:else}&lt;&#x2F;code&gt; 顯示佔位圖或提示文字。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ondestroy&quot;&gt;onDestroy&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;onMount&lt;&#x2F;code&gt; 會在 Svelte 元件在瀏覽器被 render 時執行，&lt;code&gt;onDestroy&lt;&#x2F;code&gt; 則是會在元件被銷毀時執行。&lt;&#x2F;p&gt;
&lt;p&gt;在下面的例子裡，父元件 App 有一個 Timer 子元件，以及一個能切換 Timer 存在與否的按鈕。&lt;&#x2F;p&gt;
&lt;p&gt;Timer 是個以 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;ithelp.ithome.com.tw&#x2F;articles&#x2F;10280198&quot;&gt;&lt;code&gt;setInterval()&lt;&#x2F;code&gt; 函式&lt;&#x2F;a&gt;建構的計時器，透過 App 給 Timer 的 callback 函式 &lt;code&gt;handleTick&lt;&#x2F;code&gt;，計時器會把 App 的累積秒數 &lt;code&gt;seconds&lt;&#x2F;code&gt; 持續隨讀秒的頻率加一加一加一，而一旦用戶按下「Close Timer」，App 內的 Timer 元件隨之銷毀，並觸發 Timer 內的 &lt;code&gt;onDestroy()&lt;&#x2F;code&gt;，其內的 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;ithelp.ithome.com.tw&#x2F;articles&#x2F;10280198&quot;&gt;&lt;code&gt;clearInterval()&lt;&#x2F;code&gt;&lt;&#x2F;a&gt; 被執行，令原本的 &lt;code&gt;timer&lt;&#x2F;code&gt; 失效。&lt;&#x2F;p&gt;
&lt;p&gt;看文字敘述好像很複雜，實際上真的很複雜…：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 700px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;ecd0eb01b02b439ca5d482c5fd312c84?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;h3 id=&quot;beforeupdate-afterupdate&quot;&gt;beforeUpdate &amp;amp; afterUpdate&lt;&#x2F;h3&gt;
&lt;p&gt;元件被更新的前後，也有相對應的生命週期函式可以調用。&lt;&#x2F;p&gt;
&lt;p&gt;顧名思義，&lt;code&gt;beforeUpdate&lt;&#x2F;code&gt; 會在元件更新前調用、&lt;code&gt;afterUpdate&lt;&#x2F;code&gt; 則會在元件更新後調用。&lt;&#x2F;p&gt;
&lt;p&gt;下面這個對話範例，我們希望隨著對話的增加，畫面自動幫用戶捲動到最下方：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 80vmax; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;07e54818544d476aace3d9d30686ab5c?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;範例中，往來的對話內容都放在 JS 的 &lt;code&gt;comments&lt;&#x2F;code&gt;，當用戶送出文字後，對話框綁定的 &lt;code&gt;handleKeydown()&lt;&#x2F;code&gt; 負責更新 &lt;code&gt;comments&lt;&#x2F;code&gt; 的內容，此時就會觸發 &lt;code&gt;beforeUpdate()&lt;&#x2F;code&gt; 與 &lt;code&gt;afterUpdate()&lt;&#x2F;code&gt;，&lt;code&gt;beforeUpdate()&lt;&#x2F;code&gt; 用於判斷是否要自動捲動到最底，如果要捲動，則令 &lt;code&gt;autoscroll&lt;&#x2F;code&gt; 為 &lt;code&gt;true&lt;&#x2F;code&gt;，而 &lt;code&gt;afterUpdate()&lt;&#x2F;code&gt; 的內容就相當單純了，呼叫 &lt;code&gt;scrollTo()&lt;&#x2F;code&gt; 捲動到最下面。&lt;&#x2F;p&gt;
&lt;p&gt;要特別注意的是，元件在初始化時，&lt;code&gt;onMount&lt;&#x2F;code&gt; 之前，也會觸發 &lt;code&gt;beforeUpdate&lt;&#x2F;code&gt;，因此我們在 &lt;code&gt;beforeUpdate()&lt;&#x2F;code&gt; 函式內有著 &lt;code&gt;autoscroll = div&lt;&#x2F;code&gt; 的敘述，這裡的「&lt;code&gt;div&lt;&#x2F;code&gt;」變數會是 &lt;code&gt;undefined&lt;&#x2F;code&gt;，轉換成 boolean 會是 &lt;code&gt;false&lt;&#x2F;code&gt;，也就是 &lt;code&gt;autoscroll = false&lt;&#x2F;code&gt;，直到瀏覽器把元件 render 後，JS 的 &lt;code&gt;div&lt;&#x2F;code&gt; 才會與 HTML 中的 &lt;code&gt;div.scrollable&lt;&#x2F;code&gt; 綁定，綁定後的 &lt;code&gt;autoscroll&lt;&#x2F;code&gt; 才有可能變為 &lt;code&gt;true&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;tick&quot;&gt;tick&lt;&#x2F;h3&gt;
&lt;p&gt;在某些情況下，出於效能的考量，Svelte 並不會立即更新元件狀態，此時我們可以呼叫 &lt;code&gt;tick()&lt;&#x2F;code&gt; 使元件更新狀態。&lt;&#x2F;p&gt;
&lt;p&gt;下面的例子裡，選取一段文字，按下 &lt;kbd&gt;TAB&lt;&#x2F;kbd&gt;，選取文字會轉為大寫，在瀏覽器的預設行為下，游標會跳到最末，而我們的程式碼想令游標保持原本選取的區段，因此引入 &lt;code&gt;tick()&lt;&#x2F;code&gt; 令元件更新，並執行保留選取區段的程式碼：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 900px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;8034821408c84d529d40339e58e0be00?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;h2 id=&quot;xiao-jie&quot;&gt;小結&lt;&#x2F;h2&gt;
&lt;p&gt;本集重點整理：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;onMount&lt;&#x2F;code&gt; 發生於 Svelte 元件被瀏覽器 render 之時&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;fetch()&lt;&#x2F;code&gt; 函式一般用於抓取屬於特定用戶的專屬內容，建議放在 &lt;code&gt;onMount()&lt;&#x2F;code&gt; 內執行&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;{#each}&lt;&#x2F;code&gt; 迴圈也可以用 &lt;code&gt;{:else}&lt;&#x2F;code&gt; 用於顯示陣列元素未生成時的內容&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;onDestroy&lt;&#x2F;code&gt; 發生於 Svelte 元件被銷毀之時&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;beforeUpdate&lt;&#x2F;code&gt; 發生於 Svelte 元件更新前&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;afterUpdate&lt;&#x2F;code&gt; 發生於 Svelte 元件更新後&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;tick()&lt;&#x2F;code&gt; 用於令元件更新&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Svelte 從小白到入門（四）數據綁定</title>
        <published>2021-10-09T00:00:00+00:00</published>
        <updated>2021-10-09T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/svelte-quickstart-4/"/>
        <id>https://editor.leonh.space/2021/svelte-quickstart-4/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/svelte-quickstart-4/">&lt;p&gt;這是〈Svelte 從小白到入門〉系列的第四集，各集連結如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-1&#x2F;&quot;&gt;Svelte 從小白到入門（一）元件、事件、Reactivity&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-2&#x2F;&quot;&gt;Svelte 從小白到入門（二）Props、邏輯、迴圈、Await&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-3&#x2F;&quot;&gt;Svelte 從小白到入門（三）事件&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-4&#x2F;&quot;&gt;Svelte 從小白到入門（四）數據綁定&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-5&#x2F;&quot;&gt;Svelte 從小白到入門（五）生命週期&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-6&#x2F;&quot;&gt;Svelte 從小白到入門（六）Store&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-7&#x2F;&quot;&gt;Svelte 從小白到入門（七）動態效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-8&#x2F;&quot;&gt;Svelte 從小白到入門（八）進出場效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-9&#x2F;&quot;&gt;Svelte 從小白到入門（九）動畫效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-10&#x2F;&quot;&gt;Svelte 從小白到入門（十）Action&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-11&#x2F;&quot;&gt;Svelte 從小白到入門（十一）CSS Class&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-12&#x2F;&quot;&gt;Svelte 從小白到入門（十二）Slot&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-13&#x2F;&quot;&gt;Svelte 從小白到入門（十三）Context&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-14&#x2F;&quot;&gt;Svelte 從小白到入門（十四）Svelte 的特殊元素&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-15&#x2F;&quot;&gt;Svelte 從小白到入門（十五）Module Context&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-16&#x2F;&quot;&gt;Svelte 從小白到入門（十六）Debugging&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-17&#x2F;&quot;&gt;Svelte 從小白到入門（十七）結束與展望&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;在前面的集數裡，我們遇到的 JS 與 HTMl 連動的關係都是單向的，也就是某個事件引發 JS 函式，而函式改變了某個變數的值，變數的值又反映在 HTML 中，但在那些 HTML 表單應用中，往往是要雙向連動的，而 Svelte 也提供了這樣的機制。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shu-ju-bang-ding&quot;&gt;數據綁定&lt;&#x2F;h2&gt;
&lt;p&gt;在下面的示例中，文字框和文字是要連動的，套用過去的經驗，會把 &lt;code&gt;&amp;lt;input&amp;gt;&lt;&#x2F;code&gt; 綁上 &lt;code&gt;on:input&lt;&#x2F;code&gt; 的事件，再製做相關的函式更新文字部份，但 Svelte 提供了另外的 &lt;code&gt;bind:&lt;&#x2F;code&gt; 語法讓這件事變得更簡單而直覺：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 300px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;8b1f0f4589934ae2bd55dac0eb52d26b?version=3.43.1&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;我們利用 &lt;code&gt;bind:value={name}&lt;&#x2F;code&gt; 這句讓 &lt;code&gt;&amp;lt;input&amp;gt;&lt;&#x2F;code&gt; 的 &lt;code&gt;value&lt;&#x2F;code&gt; 屬性和 JS 的 &lt;code&gt;name&lt;&#x2F;code&gt; 的值連動，並且是雙向的連動，不僅用戶在文字框輸入會改變同時改變兩者的值，並且如果是 JS 的 &lt;code&gt;name&lt;&#x2F;code&gt; 的值有被改動到，也會連動到 &lt;code&gt;&amp;lt;input&amp;gt;&lt;&#x2F;code&gt; 的 &lt;code&gt;value&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;qun-zu-biao-dan-yuan-su&quot;&gt;群組表單元素&lt;&#x2F;h3&gt;
&lt;p&gt;HTML 的表單元素除了輸入框外，還有單選的 radio input 和多選的 checkbox input，Svelte 提供了 &lt;code&gt;bind:group&lt;&#x2F;code&gt; 語法來對付他們，以 radio input 為例：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;input&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; type&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;radio&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; bind:group&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;{scoops}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;scoops&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; value&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;{1}&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;One scoop&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;input&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; type&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;radio&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; bind:group&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;{scoops}&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;scoops&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; value&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;{2}&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;Two scoops&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;他們都和 JS 的 &lt;code&gt;scoops&lt;&#x2F;code&gt; 連動，而 &lt;code&gt;scoops&lt;&#x2F;code&gt; 的值則是來自 &lt;code&gt;value&lt;&#x2F;code&gt;，選 One scoop，則 JS 的 &lt;code&gt;scoops&lt;&#x2F;code&gt; 將會是 &lt;code&gt;1&lt;&#x2F;code&gt;，以此類推。&lt;&#x2F;p&gt;
&lt;p&gt;Checkbox input 也是同樣的作法，差別在多選的值在 JS 內會是以陣列的結構儲存。&lt;&#x2F;p&gt;
&lt;p&gt;完整的例子如下：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 1100px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;ac734543a8b34196ba41b31fff429445?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;h3 id=&quot;textarea&quot;&gt;Textarea&lt;&#x2F;h3&gt;
&lt;p&gt;正規的 HTML &lt;code&gt;&amp;lt;textarea&amp;gt;&lt;&#x2F;code&gt; 是把內容放在開關標籤中間：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;textarea&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;Some words are *italic*, some are **bold**&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;textarea&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;但 Svelte 要做數據綁定的話，則是和 &lt;code&gt;&amp;lt;input&amp;gt;&lt;&#x2F;code&gt; 一樣用 &lt;code&gt;bind:value&lt;&#x2F;code&gt; 當作屬性放在 &lt;code&gt;&amp;lt;textarea&amp;gt;&lt;&#x2F;code&gt; 內：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 500px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;2c6a6cd08af641299a3163e1b5265088?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;h3 id=&quot;xia-la-xuan-dan&quot;&gt;下拉選單&lt;&#x2F;h3&gt;
&lt;p&gt;在 HTML 中，一個典型的下拉選單是由一個 &lt;code&gt;&amp;lt;select&amp;gt;&lt;&#x2F;code&gt; 與多個 &lt;code&gt;&amp;lt;option&amp;gt;&lt;&#x2F;code&gt; 構成的：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;select&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;option&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;Where did you go to school?&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;option&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;option&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;What is your mother&amp;#39;s name?&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;option&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;select&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在 Svelte，則是把 &lt;code&gt;bind:value&lt;&#x2F;code&gt; 加到 &lt;code&gt;&amp;lt;select&amp;gt;&lt;&#x2F;code&gt; 內，至於 &lt;code&gt;&amp;lt;option&amp;gt;&lt;&#x2F;code&gt;，可以用原本的 HTML 方式，但往往我們也會把它和 JS 的變數綁定在一起，像這樣：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 1100px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;64652bb662684c37b9db0571ad7ad0fe?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;上面的範例中，&lt;code&gt;&amp;lt;option&amp;gt;&lt;&#x2F;code&gt; 是和 JS 的 &lt;code&gt;questions&lt;&#x2F;code&gt; 綁定，比較奇怪的點是 &lt;code&gt;questions&lt;&#x2F;code&gt; 陣列內都是 object，而 &lt;code&gt;&amp;lt;option&amp;gt;&lt;&#x2F;code&gt; 的 &lt;code&gt;value&lt;&#x2F;code&gt; 竟可照吃不誤，因為「Svelte doesn’t mind」。&lt;&#x2F;p&gt;
&lt;p&gt;而真正用到雙向綁定的是 &lt;code&gt;&amp;lt;select&amp;gt;&lt;&#x2F;code&gt;，透過 &lt;code&gt;bind:value={selected}&lt;&#x2F;code&gt; 與 JS 的 &lt;code&gt;selected&lt;&#x2F;code&gt; 綁定，依照瀏覽器的邏輯，預設會幫我們帶入 &lt;code&gt;&amp;lt;option&amp;gt;&lt;&#x2F;code&gt; 的第一個項目，也就是「Where did you go to school?」，而在 JS 這邊，因為雙向綁定，&lt;code&gt;selected&lt;&#x2F;code&gt; 會是&lt;code&gt;&amp;lt;option&amp;gt;&lt;&#x2F;code&gt; 的 &lt;code&gt;value&lt;&#x2F;code&gt;，也就是 &lt;code&gt;{ id: 1, text: &#x27;Where did you go to school?&#x27; }&lt;&#x2F;code&gt; 這個物件。&lt;&#x2F;p&gt;
&lt;p&gt;其他錦上添花的部份：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;select&amp;gt;&lt;&#x2F;code&gt; 內的小函式，令選項改變時，清空答案框&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;&amp;lt;form&amp;gt;&lt;&#x2F;code&gt; 裡面加了 &lt;code&gt;preventDefault&lt;&#x2F;code&gt; 的修飾器，防止頁面重載&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;duo-xuan-xuan-dan&quot;&gt;多選選單&lt;&#x2F;h3&gt;
&lt;p&gt;在 HTML 把 &lt;code&gt;&amp;lt;select&amp;gt;&lt;&#x2F;code&gt; 加上 &lt;code&gt;multiple&lt;&#x2F;code&gt; 屬性即為多選，拿前面的 checkbox 範例改造一下：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 900px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;0b45e78e660f40f286e98d7f1c765481?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;和單選的差別只在於多選的項目反應到 JS 上會是陣列的形式。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;contenteditable&quot;&gt;Contenteditable&lt;&#x2F;h3&gt;
&lt;p&gt;賦予 &lt;code&gt;contenteditable&lt;&#x2F;code&gt; 屬性的元素，可以用 &lt;code&gt;bind:innerHTML&lt;&#x2F;code&gt; 或 &lt;code&gt;bind:textContent&lt;&#x2F;code&gt; 的方式綁定：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 600px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;089e467d8cec471987b242a49aabcad5?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;h3 id=&quot;zhen-lie-yu-hui-quan&quot;&gt;陣列與迴圈&lt;&#x2F;h3&gt;
&lt;p&gt;在下面的例子裡，JS 的 &lt;code&gt;todos&lt;&#x2F;code&gt; 被放進 HTML 內做迴圈，迴圈內也可以針與 &lt;code&gt;todos&lt;&#x2F;code&gt; 的成員（&lt;code&gt;todo&lt;&#x2F;code&gt;）做綁定：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 1300px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;4033d2fa40a7416995bb1fe08ac89d2d?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;至於那句陌生的樣式語法 &lt;code&gt;class:done={todo.done}&lt;&#x2F;code&gt; 在此先略過，如果本系列沒有斷尾的話會再提到。&lt;&#x2F;p&gt;
&lt;p&gt;這個範例也是前端框架最常見到的 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;dictionary.cambridge.org&#x2F;zht&#x2F;%E8%A9%9E%E5%85%B8&#x2F;%E8%8B%B1%E8%AA%9E&#x2F;to-do-list&quot;&gt;to-do list&lt;&#x2F;a&gt; 應用，就這樣不知不覺的做完了。:)&lt;&#x2F;p&gt;
&lt;h3 id=&quot;duo-mei-ti-yuan-su&quot;&gt;多媒體元素&lt;&#x2F;h3&gt;
&lt;p&gt;對 &lt;code&gt;&amp;lt;video&amp;gt;&lt;&#x2F;code&gt; 與 &lt;code&gt;&amp;lt;audio&amp;gt;&lt;&#x2F;code&gt; 這類多媒體，Svetle 提供更多綁定的對象：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;duration&lt;&#x2F;code&gt; (readonly) — the total duration of the video, in seconds&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;buffered&lt;&#x2F;code&gt; (readonly) — an array of &lt;code&gt;{start, end}&lt;&#x2F;code&gt; objects&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;seekable&lt;&#x2F;code&gt; (readonly) — ditto&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;played&lt;&#x2F;code&gt; (readonly) — ditto&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;seeking&lt;&#x2F;code&gt; (readonly) — boolean&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;ended&lt;&#x2F;code&gt; (readonly) — boolean&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;currentTime&lt;&#x2F;code&gt; — the current point in the video, in seconds&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;playbackRate&lt;&#x2F;code&gt; — how fast to play the video, where &lt;code&gt;1&lt;&#x2F;code&gt; is ‘normal’&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;paused&lt;&#x2F;code&gt; — this one should be self-explanatory&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;volume&lt;&#x2F;code&gt; — a value between 0 and 1&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;muted&lt;&#x2F;code&gt; — a boolean value where true is muted&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Videos additionally have readonly &lt;code&gt;videoWidth&lt;&#x2F;code&gt; and &lt;code&gt;videoHeight&lt;&#x2F;code&gt; bindings.&lt;&#x2F;p&gt;
&lt;p&gt;以上偷懶不解釋。:p&lt;&#x2F;p&gt;
&lt;h3 id=&quot;chi-du&quot;&gt;尺度&lt;&#x2F;h3&gt;
&lt;p&gt;對於 HTMl 區塊類元素可以透過綁定機制取得 &lt;code&gt;clientWidth&lt;&#x2F;code&gt;、&lt;code&gt;clientWidth&lt;&#x2F;code&gt;、&lt;code&gt;offsetWidth&lt;&#x2F;code&gt;、&lt;code&gt;offsetHeight&lt;&#x2F;code&gt;，不過就如同原始的 web api 那樣，這些值也是唯讀的，只能單向讀取。&lt;&#x2F;p&gt;
&lt;p&gt;關於 &lt;code&gt;clientWidth&lt;&#x2F;code&gt; 與 &lt;code&gt;offsetWidth&lt;&#x2F;code&gt; 的差異，可以看〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;shubo.io&#x2F;element-size-scrolling&#x2F;&quot;&gt;一次搞懂 clientHeight&#x2F;clientWidth&#x2F;offSetHeight&#x2F;offsetWidth&#x2F;scrollHeight&#x2F;scrollWidth&#x2F;scrollTop&#x2F;scrollLeft 的區別&lt;&#x2F;a&gt;〉。&lt;&#x2F;p&gt;
&lt;p&gt;參見下面的例子：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 700px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;d4a4243b49bc416dbb0a7b084fc057e2?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;裡面利用 range input 數值綁定的方式，動態變更字大小，以及利用前述的方式取得文字容器的寬高。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;this&quot;&gt;This&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;bind:this&lt;&#x2F;code&gt; 令綁定的對象參照到元素（或元件）自身，取得元素自身的參照後，就能在 JS 區域去操作該元素。&lt;&#x2F;p&gt;
&lt;p&gt;看文字可能不好理解，實例如下：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 1400px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;678f032c1794437b992e050047a0cedf?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;我們用 &lt;code&gt;&amp;lt;canvas bind:this={canvas}&amp;gt;&lt;&#x2F;code&gt; 將 HTML 元素 &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;&#x2F;code&gt; 參照到 JS 變數 &lt;code&gt;canvas&lt;&#x2F;code&gt; 上，或者我們可以把這件事理解為：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;canvas = &amp;lt;canvas&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;當然上面這行不是正確的語法，但就是這個意思。&lt;&#x2F;p&gt;
&lt;p&gt;令 &lt;code&gt;canvas&lt;&#x2F;code&gt; 為 &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;&#x2F;code&gt; 元素後，就可以在 JS 內操控它，範例內剩下九成的程式碼是一系列 Canvas 花式操作，本人也是不懂裝懂的程度，請看〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;developer.mozilla.org&#x2F;zh-TW&#x2F;docs&#x2F;Web&#x2F;API&#x2F;Canvas_API&#x2F;Tutorial&quot;&gt;Canvas 教學文件&lt;&#x2F;a&gt;〉&lt;&#x2F;p&gt;
&lt;p&gt;範例中的 &lt;code&gt;onMount()&lt;&#x2F;code&gt; 的部份，容後敘明，只要本系列沒有停更…。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;yuan-jian-bang-ding&quot;&gt;元件綁定&lt;&#x2F;h3&gt;
&lt;p&gt;綁定機制不僅可用在原生的 HTML 元素上，也可用在 Svelte 元件上。&lt;&#x2F;p&gt;
&lt;p&gt;下面的例子有 App 父元件及子元件 Keypad，就是那令人又愛又恨的數字小鍵盤。&lt;&#x2F;p&gt;
&lt;p&gt;App 負責顯示遮蔽後的密碼、Keypad 負責顯示數字鍵盤，用戶按下的數字會以字串的形式存在 Keypad 的 &lt;code&gt;value&lt;&#x2F;code&gt;，而 App 的 &lt;code&gt;pin&lt;&#x2F;code&gt; 又綁定到 Keypad 的 &lt;code&gt;value&lt;&#x2F;code&gt; props，使 &lt;code&gt;pin&lt;&#x2F;code&gt; 可以得到 &lt;code&gt;value&lt;&#x2F;code&gt; 的值，最後再加上一個簡單的遮蔽函式，因此得以即時反應用戶按下的數字在密碼遮蔽區。&lt;&#x2F;p&gt;
&lt;p&gt;另一方面，用戶在 Keypad 按下 submit 後分派給父元件 App，而 App 接收到 &lt;code&gt;submit&lt;&#x2F;code&gt; 事件後觸發 &lt;code&gt;handleSubmit()&lt;&#x2F;code&gt; 彈出提示框。&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 500px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;12f39104d273480a81aa1af0dede65a8?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;綁定機制還可以直接用在元件上。&lt;&#x2F;p&gt;
&lt;p&gt;在下面的例子裡，用 &lt;code&gt;&amp;lt;InputField bind:this={field}&amp;gt;&lt;&#x2F;code&gt; 這句把調用的這份 InputField 元件（instance）與 &lt;code&gt;field&lt;&#x2F;code&gt; 變數綁定，並且 InputField 又暴露了 &lt;code&gt;focus()&lt;&#x2F;code&gt;，因而我們可以用 &lt;code&gt;field.focus()&lt;&#x2F;code&gt; 的形式執行 InputField instance 的 &lt;code&gt;focus()&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 500px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;c43279fc500742ba867ff3f0747c4b8a?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;h2 id=&quot;xiao-jie&quot;&gt;小結&lt;&#x2F;h2&gt;
&lt;p&gt;看了各種花式 &lt;code&gt;bind:&lt;&#x2F;code&gt; 是不是越來越混亂了呢？別擔心，我也是。:p&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Svelte 從小白到入門（三）事件</title>
        <published>2021-10-07T00:00:00+00:00</published>
        <updated>2021-10-07T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/svelte-quickstart-3/"/>
        <id>https://editor.leonh.space/2021/svelte-quickstart-3/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/svelte-quickstart-3/">&lt;p&gt;這是〈Svelte 從小白到入門〉系列的第三集，各集連結如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-1&#x2F;&quot;&gt;Svelte 從小白到入門（一）元件、事件、Reactivity&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-2&#x2F;&quot;&gt;Svelte 從小白到入門（二）Props、邏輯、迴圈、Await&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-3&#x2F;&quot;&gt;Svelte 從小白到入門（三）事件&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-4&#x2F;&quot;&gt;Svelte 從小白到入門（四）數據綁定&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-5&#x2F;&quot;&gt;Svelte 從小白到入門（五）生命週期&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-6&#x2F;&quot;&gt;Svelte 從小白到入門（六）Store&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-7&#x2F;&quot;&gt;Svelte 從小白到入門（七）動態效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-8&#x2F;&quot;&gt;Svelte 從小白到入門（八）進出場效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-9&#x2F;&quot;&gt;Svelte 從小白到入門（九）動畫效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-10&#x2F;&quot;&gt;Svelte 從小白到入門（十）Action&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-11&#x2F;&quot;&gt;Svelte 從小白到入門（十一）CSS Class&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-12&#x2F;&quot;&gt;Svelte 從小白到入門（十二）Slot&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-13&#x2F;&quot;&gt;Svelte 從小白到入門（十三）Context&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-14&#x2F;&quot;&gt;Svelte 從小白到入門（十四）Svelte 的特殊元素&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-15&#x2F;&quot;&gt;Svelte 從小白到入門（十五）Module Context&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-16&#x2F;&quot;&gt;Svelte 從小白到入門（十六）Debugging&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-17&#x2F;&quot;&gt;Svelte 從小白到入門（十七）結束與展望&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;在第一集我們談到 reactivity 時也簡短的用到了 Svelte 的事件綁定 &lt;code&gt;on:click&lt;&#x2F;code&gt; 的語法，這集我們要探索更多 Svelte 的事件機制。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shi-jian&quot;&gt;事件&lt;&#x2F;h2&gt;
&lt;iframe src=&quot;https:&#x2F;&#x2F;www.youtube-nocookie.com&#x2F;embed&#x2F;smursYQRwgA&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;能ㄤㄤㄤ的不只小叮噹，Svelte 也可以ㄤㄤㄤ。&lt;&#x2F;p&gt;
&lt;p&gt;下面是 &lt;code&gt;on:mousemove&lt;&#x2F;code&gt; 的示例，在&lt;span style=&quot;color: red&quot;&gt;紅色&lt;&#x2F;span&gt;的 &lt;code&gt;&amp;lt;div&amp;gt;&lt;&#x2F;code&gt;，我們呼叫 &lt;code&gt;handleMousemove()&lt;&#x2F;code&gt; 給出滑鼠座標，相當直白（直白是種高尚的情操），而在&lt;span style=&quot;color: orange&quot;&gt;橘色&lt;&#x2F;span&gt;的 &lt;code&gt;&amp;lt;div&amp;gt;&lt;&#x2F;code&gt;，我們也可以改用在 HTML 用 inline 的方式呼叫一般函式或匿名函式：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 700px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;e8709b9fdb0d493fabf21ba5210b16b8?version=3.43.1&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;h3 id=&quot;shi-jian-xiu-shi-qi&quot;&gt;事件修飾器&lt;&#x2F;h3&gt;
&lt;p&gt;事件也有裝飾器，Svelte 原文稱為 modifier，而不是 decorator，不論名字為何，他們的角色是相同的—改變對象原有的行為。&lt;&#x2F;p&gt;
&lt;p&gt;下面這個例子用上了 &lt;code&gt;once&lt;&#x2F;code&gt; 裝飾器，所以點擊的行為只會觸發一次事件函式：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 400px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;de2c155945f74ca0b08b3d990773d9a3?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;注意裝飾器的語法 &lt;code&gt;on:click|once&lt;&#x2F;code&gt; 中間有個管道符號 &lt;code&gt;|&lt;&#x2F;code&gt; 連接事件與修飾器。&lt;&#x2F;p&gt;
&lt;p&gt;至此我們已經看過 Svelte 兩種風格的修飾器語法：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@html string&lt;&#x2F;code&gt;，&lt;code&gt;@html&lt;&#x2F;code&gt; 讓字串不做跳脫處理，而 &lt;code&gt;@html&lt;&#x2F;code&gt; 這種有 &lt;code&gt;@&lt;&#x2F;code&gt; 符號的語法看起來像是 Python 風格的裝飾器語法。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;on:click|once&lt;&#x2F;code&gt;，&lt;code&gt;once&lt;&#x2F;code&gt; 讓點擊事件只發生一次，而 &lt;code&gt;|once&lt;&#x2F;code&gt; 這種有 &lt;code&gt;|&lt;&#x2F;code&gt; 管線符號的語法看起來像是 Unix 系的管道語法&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;同時摻用兩種風格的語法，讓 Svelte 看起來是混亂的，如果把 &lt;code&gt;@html string&lt;&#x2F;code&gt; 改以 &lt;code&gt;string|html&lt;&#x2F;code&gt;，感覺起來整體風格會更為一致。&lt;&#x2F;p&gt;
&lt;p&gt;不過木已成舟，讓我們繼續跟著遊戲規則玩下去。&lt;&#x2F;p&gt;
&lt;p&gt;既有的事件修飾器如下，很偷懶的剪下貼上原文不解釋：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;preventDefault&lt;&#x2F;code&gt; - calls &lt;code&gt;event.preventDefault()&lt;&#x2F;code&gt; before running the handler. Useful for client-side form handling, for example.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;stopPropagation&lt;&#x2F;code&gt; - calls &lt;code&gt;event.stopPropagation()&lt;&#x2F;code&gt;, preventing the event reaching the next element.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;passive&lt;&#x2F;code&gt; - improves scrolling performance on touch &#x2F; wheel events (Svelte will add it automatically where it’s safe to do so).&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;nonpassive&lt;&#x2F;code&gt; - explicitly set &lt;code&gt;passive: false&lt;&#x2F;code&gt;.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;capture&lt;&#x2F;code&gt; - fires the handler during the capture phase instead of the bubbling phase (&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;developer.mozilla.org&#x2F;en-US&#x2F;docs&#x2F;Learn&#x2F;JavaScript&#x2F;Building_blocks&#x2F;Events#event_bubbling_and_capture&quot;&gt;MDN docs&lt;&#x2F;a&gt;)&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;once&lt;&#x2F;code&gt; - remove the handler after the first time it runs.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;self&lt;&#x2F;code&gt; - only trigger handler if event.target is the element itself.&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;trusted&lt;&#x2F;code&gt; - only trigger handler if &lt;code&gt;event.isTrusted&lt;&#x2F;code&gt; is &lt;code&gt;true&lt;&#x2F;code&gt;. I.e. if the event is triggered by a user action.&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;事件修飾器也可以像 Unix 管道一樣，串在一起：&lt;code&gt;on:click|once|capture={...}&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;shi-jian-fen-pai-dispatch-yu-zhuan-fa-forwarding&quot;&gt;事件分派（Dispatch）與轉發（Forwarding）&lt;&#x2F;h3&gt;
&lt;p&gt;Svelte 元件之間可以分派事件，在下面的示例裡，按鈕發生的事情如下：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;按下 Inner 元件的按鈕&lt;&#x2F;li&gt;
&lt;li&gt;點擊觸發 Inner 按鈕的 &lt;code&gt;on:click={sayHello}&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;sayHello()&lt;&#x2F;code&gt; 內的 &lt;code&gt;dispatch(&#x27;message&#x27;)&lt;&#x2F;code&gt; 又觸發父元件 App 的 &lt;code&gt;message&lt;&#x2F;code&gt; 事件&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;message&lt;&#x2F;code&gt; 事件又觸發 &lt;code&gt;handleMessage()&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;handleMessage()&lt;&#x2F;code&gt; 內執行了 &lt;code&gt;alert()&lt;&#x2F;code&gt; 跳出提示框&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 500px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;c56a74195e494f3d9aee4af60663fea7?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;這整個分派的機制得以實現，是靠 Svelte 提供的 &lt;code&gt;createEventDispatcher()&lt;&#x2F;code&gt;，我們用它創建一個 &lt;code&gt;dispatch&lt;&#x2F;code&gt; 實體，供派送事件之用。&lt;&#x2F;p&gt;
&lt;p&gt;注意到父元件 App 內的這行：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FDAEB7;font-style: italic;&quot;&gt;Inner&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; on:message&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;{handleMessage}&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&#x2F;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這邊是調用了 Inner 元件，不過 &lt;code&gt;message&lt;&#x2F;code&gt; 事件是發生在在 App 元件上的，這是 &lt;code&gt;on:&lt;&#x2F;code&gt; 事件綁定的語法，並非 props 的語法，雖然兩者長的有那麼一點像…。&lt;&#x2F;p&gt;
&lt;p&gt;基於以上的概念，我們讓它複雜一點，在 App 和 Inner 中間夾一層 Outer 元件，用 App 包 Outer、Outer 包 Inner 的方式形成巢狀引用，因為 Svelte 的事件沒有「&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;developer.mozilla.org&#x2F;zh-CN&#x2F;docs&#x2F;Learn&#x2F;JavaScript&#x2F;Building_blocks&#x2F;Events#%E4%BA%8B%E4%BB%B6%E5%86%92%E6%B3%A1%E5%8F%8A%E6%8D%95%E8%8E%B7&quot;&gt;冒泡&lt;&#x2F;a&gt;」的機制，所以前一個例子的 Inner 丟出的 &lt;code&gt;message&lt;&#x2F;code&gt; 事件被 Outer 收到就停了，並不會自然而然的再往外丟給 App，因此在不改程式碼的情況下，按鈕自然也不會有反應的。&lt;&#x2F;p&gt;
&lt;p&gt;想讓事件往外轉發，得利用 Svelte 的轉發機制（語法糖），在 Outer 元件內，調用 Inner 之處，加上 &lt;code&gt;on:message&lt;&#x2F;code&gt; 的綁定語法，Svelte 就知道「哦，要轉發出去」，看起來很簡單，但個人認為頗不自然…：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 500px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;29c9ef5b775544799e549f34a89664f0?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;事件轉發的機制也可以套用在原生的 DOM 物件上，按鈕加個 &lt;code&gt;on:click&lt;&#x2F;code&gt; 就會把點擊事件往外轉發出去了：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 400px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;9584cb8ed5fe4e60a6a8d3fcb8e03582?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;h2 id=&quot;xiao-jie&quot;&gt;小結&lt;&#x2F;h2&gt;
&lt;p&gt;整理本集重點：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;事件以 &lt;code&gt;on:&lt;&#x2F;code&gt; 語法綁定&lt;&#x2F;li&gt;
&lt;li&gt;可以在 Svelte 的 HTML 元素內寫 inline 的函式&lt;&#x2F;li&gt;
&lt;li&gt;事件可以搭配管道符號 &lt;code&gt;|&lt;&#x2F;code&gt; 接上修飾器，並且可以串接多個修飾器&lt;&#x2F;li&gt;
&lt;li&gt;Svelte 提供了用 &lt;code&gt;createEventDispatcher()&lt;&#x2F;code&gt; 建立的實例做事件分派的機制&lt;&#x2F;li&gt;
&lt;li&gt;Svelte 的事件不會冒泡，要轉送事件得用像 &lt;code&gt;on:click&lt;&#x2F;code&gt; 這樣的語句向外轉送點擊事件&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;原本一集可以寫好幾個主題，現在一集只能寫一個主題，究竟會不會斷尾呢？讓我們看下去…。&lt;&#x2F;p&gt;
&lt;p&gt;（待續）&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Svelte 從小白到入門（二）Props、邏輯、迴圈、Await</title>
        <published>2021-10-06T00:00:00+00:00</published>
        <updated>2021-10-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/svelte-quickstart-2/"/>
        <id>https://editor.leonh.space/2021/svelte-quickstart-2/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/svelte-quickstart-2/">&lt;p&gt;這是〈Svelte 從小白到入門〉系列的第二集，各集連結如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-1&#x2F;&quot;&gt;Svelte 從小白到入門（一）元件、事件、Reactivity&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-2&#x2F;&quot;&gt;Svelte 從小白到入門（二）Props、邏輯、迴圈、Await&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-3&#x2F;&quot;&gt;Svelte 從小白到入門（三）事件&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-4&#x2F;&quot;&gt;Svelte 從小白到入門（四）數據綁定&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-5&#x2F;&quot;&gt;Svelte 從小白到入門（五）生命週期&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-6&#x2F;&quot;&gt;Svelte 從小白到入門（六）Store&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-7&#x2F;&quot;&gt;Svelte 從小白到入門（七）動態效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-8&#x2F;&quot;&gt;Svelte 從小白到入門（八）進出場效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-9&#x2F;&quot;&gt;Svelte 從小白到入門（九）動畫效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-10&#x2F;&quot;&gt;Svelte 從小白到入門（十）Action&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-11&#x2F;&quot;&gt;Svelte 從小白到入門（十一）CSS Class&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-12&#x2F;&quot;&gt;Svelte 從小白到入門（十二）Slot&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-13&#x2F;&quot;&gt;Svelte 從小白到入門（十三）Context&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-14&#x2F;&quot;&gt;Svelte 從小白到入門（十四）Svelte 的特殊元素&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-15&#x2F;&quot;&gt;Svelte 從小白到入門（十五）Module Context&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-16&#x2F;&quot;&gt;Svelte 從小白到入門（十六）Debugging&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-17&#x2F;&quot;&gt;Svelte 從小白到入門（十七）結束與展望&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;在第一集我們談到了元件的概念，也提到元件是彼此隔離的，並且 Svelte 也提供了讓元件與元件間互相傳遞值的機制，這些被丟來丟去的值在 Svelte 的世界稱為 properties，簡稱 props。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;props&quot;&gt;Props&lt;&#x2F;h2&gt;
&lt;p&gt;在 Nested 元件裡，我們用 &lt;code&gt;export&lt;&#x2F;code&gt; 令 &lt;code&gt;answer&lt;&#x2F;code&gt; 對外暴露：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 400px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;38b6a5cb31224083bbb5cb637e274289?version=3.43.1&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;在 App 元件中，第一次調用 Nested 元件時我們對 &lt;code&gt;answer&lt;&#x2F;code&gt; 賦值 &lt;code&gt;42&lt;&#x2F;code&gt;，因此網頁上呈現的是「The answer is 42」。&lt;&#x2F;p&gt;
&lt;p&gt;而在第二次調用 Nested 時，我們不賦值，那 &lt;code&gt;answer&lt;&#x2F;code&gt; 的值就會是原本的 &lt;code&gt;a mystery&lt;&#x2F;code&gt;，因此網頁上呈現的是「The answer is a mystery」。&lt;&#x2F;p&gt;
&lt;p&gt;「若在 Nested 內，不對 &lt;code&gt;answer&lt;&#x2F;code&gt; 賦值，並且調用 Nested 也不賦值的話，網頁會顯示什麼呢？」此時的 &lt;code&gt;answer&lt;&#x2F;code&gt; 會是 &lt;code&gt;undefined&lt;&#x2F;code&gt;，因此網頁也會顯示成「The answer is undefined」。&lt;&#x2F;p&gt;
&lt;p&gt;看完上面的範例，好像會以為一個元件內一定要有 &lt;code&gt;export&lt;&#x2F;code&gt; 個什麼變數，該元件才能有個 props，實際上並非如此，其實 App 元件可以傳遞任意的值（props）進 Nested 元件，這些在 Nested 內並未預先定義的 props 會全部儲存在一個特殊的物件 &lt;code&gt;$$props&lt;&#x2F;code&gt; 內。&lt;&#x2F;p&gt;
&lt;p&gt;拿上一個範例改造一下：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 300px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;f0bdb085d5294093aee081f877cf6342?version=3.43.1&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;在 Nested 元件內我們沒有定義任何變數，而是直接調用傳進來的 &lt;code&gt;$$props.answer&lt;&#x2F;code&gt; 做顯示。&lt;&#x2F;p&gt;
&lt;p&gt;看起來好像很神奇，但 Svelte 文件說這會使程式難以優化，它應該是指 compile 時難以針對這種非預期的 props 做效能優化，因此建議盡可能還是在元件內明確宣告要 &lt;code&gt;export&lt;&#x2F;code&gt; 的變數。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;props-zhan-kai&quot;&gt;Props 展開&lt;&#x2F;h3&gt;
&lt;p&gt;如果打算要傳入的值都包裝在一個物件內，並且物件內的名稱和 props 的名稱故意取的一樣，那 Svelte 可以讓我們少打很多字。&lt;&#x2F;p&gt;
&lt;p&gt;下面的範例，所有要傳進 Info 的值都包在 &lt;code&gt;pkg&lt;&#x2F;code&gt; 內，並且 &lt;code&gt;pkg&lt;&#x2F;code&gt; 內的物件命名和 Info 的 props 命名一致，如此我們可以用 &lt;code&gt;{...pkg}&lt;&#x2F;code&gt; 的形式把值傳入 Info 元件，Svelte 會幫我們把 &lt;code&gt;pkg&lt;&#x2F;code&gt; 展開丟到對應的 props：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 500px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;1b0e0d5bf94f478196166a1b48caf179?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;上面我們在 App 內調用了兩次 Info 元件，第二次我們用上了 props 展開的奇計淫巧，讓我們少打了不少字。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;svelte-yuan-jian-nei-de-luo-ji&quot;&gt;Svelte 元件內的邏輯&lt;&#x2F;h2&gt;
&lt;p&gt;在 &lt;code&gt;&amp;lt;script&amp;gt;&lt;&#x2F;code&gt; 區塊內的邏輯處理，沿用既有的 JS 語法標準即可，但若是要在 Svelte 元件的 HTML 內做邏輯處理，那又得用上 Svelte 特有的語法…。&lt;&#x2F;p&gt;
&lt;p&gt;綜觀而言，這些特有語法都在破壞 JS 的一致性，我們只能把他們視為一種 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E9%A2%86%E5%9F%9F%E7%89%B9%E5%AE%9A%E8%AF%AD%E8%A8%80&quot;&gt;DSL&lt;&#x2F;a&gt;，跟著遊戲規則玩下去。&lt;&#x2F;p&gt;
&lt;p&gt;扯遠了，回到主題。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;if-else&quot;&gt;if &#x2F; else&lt;&#x2F;h3&gt;
&lt;p&gt;想要在 Svelte 元件內的 HTML 插入 &lt;code&gt;if&lt;&#x2F;code&gt; 區塊，用 &lt;code&gt;{#if}&lt;&#x2F;code&gt; 開頭，中間是 &lt;code&gt;{:else}&lt;&#x2F;code&gt;，最後以 &lt;code&gt;{&#x2F;if}&lt;&#x2F;code&gt; 結尾將上下文包起來。&lt;&#x2F;p&gt;
&lt;p&gt;從這個例子可以看到 Svelte HTML 邏輯的固定模式：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 500px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;42317cb789a04c14ae52ce6fab22e7b9?version=3.43.1&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;h3 id=&quot;hui-quan&quot;&gt;迴圈&lt;&#x2F;h3&gt;
&lt;p&gt;延續上面的概念，要建立迴圈，也是用同樣的結構，當然，Svelte 的特有語法 &lt;code&gt;{#each cats as cat, i}&lt;&#x2F;code&gt; 還是要記一下的，其中的第二個參數 &lt;code&gt;i&lt;&#x2F;code&gt; 代表從 0 開始的陣列索引號：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 500px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;c545f92b8e204872b96225a89573093d?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;h3 id=&quot;hui-quan-jia-key&quot;&gt;迴圈加 key&lt;&#x2F;h3&gt;
&lt;p&gt;迴圈也具有 reactivity 特性，一旦來源的 JS 陣列物件變動，相對應的 HTML 迴圈結構也會變動，以 &lt;code&gt;things&lt;&#x2F;code&gt; 陣列砍掉第一個成員為例，此時的行為是：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;JS 陣列物件變動，&lt;strong&gt;去掉第一個成員&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;引發 HTML 迴圈結構變動，HTML 迴圈也會減掉一個成員，但要注意的是，&lt;strong&gt;HTML 迴圈的成員加減是作用在最末，也就是最後一個成員被去掉&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;此時 JS 陣列與 HTML 迴圈數量一致，&lt;strong&gt;reactivity 機制再把雙方的數值同步&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;上面的示例展示在下方，動手玩玩看會發現文字與圖案有不同步的現象：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 700px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;10f54d10e7a24c44b8b453020f8ebf13?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;按鈕後，因為 &lt;code&gt;things = things.slice(1)&lt;&#x2F;code&gt;，文字的 apple 不見了，但在圖案那邊，由於上述第二點的關係，卻是最後一個 🥕 不見，導致兩邊不同步。&lt;&#x2F;p&gt;
&lt;p&gt;要改變這樣的行為，可以為迴圈內的元素加上 key，有了神奇的 key，上述第二點的行為就會變為：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Svelte 會去比較變更前後 key 的變化，並反映到相對的 HTML 陣列上&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在下面的示例我們為 &lt;code&gt;things&lt;&#x2F;code&gt; 內的水果們都加上了 &lt;code&gt;id&lt;&#x2F;code&gt; 作為他們的唯一識別，並在 HTML 迴圈內以 &lt;code&gt;{#each things as thing (thing.id)}&lt;&#x2F;code&gt; 的形式將 &lt;code&gt;thing.id&lt;&#x2F;code&gt; 定義為各個水果的 key，一旦 &lt;code&gt;things&lt;&#x2F;code&gt; 發生變動，Svelte 會去比較 &lt;code&gt;thing.id&lt;&#x2F;code&gt; 的變化，並反映在 HTML 陣列元素上：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 700px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;c25dd6da45e54dbdbd86f8bfac6ab2ce?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;至此，Svelte 奇怪的語法又增加了一點。&lt;&#x2F;p&gt;
&lt;p&gt;本節的例子完全是為了講解迴圈的 key 而設的，回過頭說文字圖案不同步的問題，其實也可以直接幫 Thing 元件內的 &lt;code&gt;emoji&lt;&#x2F;code&gt; 加上 reactivity 的特性就好，如 &lt;code&gt;$: emoji = emoji[name];&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;await&quot;&gt;Await&lt;&#x2F;h3&gt;
&lt;p&gt;也可以在 Svelte 元件的 HTML 內呼叫 async 函式，用法頗直觀，&lt;code&gt;{#await}&lt;&#x2F;code&gt;、&lt;code&gt;{:then}&lt;&#x2F;code&gt;、&lt;code&gt;{:catch}&lt;&#x2F;code&gt;、&lt;code&gt;{&#x2F;await}&lt;&#x2F;code&gt; 四部曲：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 900px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;25f289f817df44828c44ce8290737a79?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;另外可以注意到，在 &lt;code&gt;handleClick()&lt;&#x2F;code&gt; 裡面，我們並非是直接呼叫 &lt;code&gt;getRandomNumber()&lt;&#x2F;code&gt;，而是用了再次對 &lt;code&gt;promise&lt;&#x2F;code&gt; 賦值的方式，曲線執行 &lt;code&gt;getRandomNumber()&lt;&#x2F;code&gt;，解決了 await 函式無法被一般函式呼叫的問題。（或者說得一路 async &#x2F; await 連鎖下去的問題）&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xiao-jie&quot;&gt;小結&lt;&#x2F;h2&gt;
&lt;p&gt;整理本集重點：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;元件間被丟來丟去的值通稱為 props&lt;&#x2F;li&gt;
&lt;li&gt;元件內用 &lt;code&gt;export&lt;&#x2F;code&gt; 明確定義要對外暴露的 props&lt;&#x2F;li&gt;
&lt;li&gt;所有被丟入元件的 props 都會存入 &lt;code&gt;$$props&lt;&#x2F;code&gt; 這個物件內&lt;&#x2F;li&gt;
&lt;li&gt;Svelte 元件內 HTML 可置入邏輯語法，以 &lt;code&gt;if&lt;&#x2F;code&gt; &#x2F; &lt;code&gt;else&lt;&#x2F;code&gt; 為例，結構是 &lt;code&gt;{#if}&lt;&#x2F;code&gt;、&lt;code&gt;{:else if}&lt;&#x2F;code&gt;、&lt;code&gt;{:else}&lt;&#x2F;code&gt;、&lt;code&gt;{&#x2F;if}&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;迴圈可以加上 id 與 key&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;（待續）&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Svelte 從小白到入門（一）元件、事件、Reactivity</title>
        <published>2021-10-05T00:00:00+00:00</published>
        <updated>2021-10-05T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/svelte-quickstart-1/"/>
        <id>https://editor.leonh.space/2021/svelte-quickstart-1/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/svelte-quickstart-1/">&lt;p&gt;這是〈Svelte 從小白到入門〉系列的第一集，各集連結如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-1&#x2F;&quot;&gt;Svelte 從小白到入門（一）元件、事件、Reactivity&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-2&#x2F;&quot;&gt;Svelte 從小白到入門（二）Props、邏輯、迴圈、Await&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-3&#x2F;&quot;&gt;Svelte 從小白到入門（三）事件&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-4&#x2F;&quot;&gt;Svelte 從小白到入門（四）數據綁定&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-5&#x2F;&quot;&gt;Svelte 從小白到入門（五）生命週期&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-6&#x2F;&quot;&gt;Svelte 從小白到入門（六）Store&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-7&#x2F;&quot;&gt;Svelte 從小白到入門（七）動態效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-8&#x2F;&quot;&gt;Svelte 從小白到入門（八）進出場效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-9&#x2F;&quot;&gt;Svelte 從小白到入門（九）動畫效果&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-10&#x2F;&quot;&gt;Svelte 從小白到入門（十）Action&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-11&#x2F;&quot;&gt;Svelte 從小白到入門（十一）CSS Class&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-12&#x2F;&quot;&gt;Svelte 從小白到入門（十二）Slot&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-13&#x2F;&quot;&gt;Svelte 從小白到入門（十三）Context&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-14&#x2F;&quot;&gt;Svelte 從小白到入門（十四）Svelte 的特殊元素&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-15&#x2F;&quot;&gt;Svelte 從小白到入門（十五）Module Context&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-16&#x2F;&quot;&gt;Svelte 從小白到入門（十六）Debugging&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;svelte-quickstart-17&#x2F;&quot;&gt;Svelte 從小白到入門（十七）結束與展望&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;&quot;&gt;Svelte&lt;&#x2F;a&gt; 是年輕且發展迅猛的 JS 框架，與 React &#x2F; Vue 最大的差異是經過 compile 後的 Svelte 元件是以瀏覽器原生的 DOM 操作，省掉了 virtual DOM 這層轉換的開銷，因此在客戶端上執行的效率更高。&lt;&#x2F;p&gt;
&lt;p&gt;本篇是個人整理的 Svelte 學習筆記，內容與範例主要來自於 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;tutorial&quot;&gt;Svelte Tutorial&lt;&#x2F;a&gt;，但經過適當的簡化與改寫，希望能縮短一半的篇幅的同時傳達出相當的資訊量。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;qi-shou-shi&quot;&gt;起手式&lt;&#x2F;h2&gt;
&lt;p&gt;在 Svelte 的首頁有放開立空白 Svelte 專案的做法，但在這裡我們並不先開立專案，我們使用 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&quot;&gt;Svelte REPL&lt;&#x2F;a&gt; 做為學習的環境，因此起手式超級簡單，只要用瀏覽器開 Svelte REPL 就立馬有個現成可用的 Svelte 環境，在上面註冊帳號後也可以把自己寫的 Svelte 元件存起來。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;svelte-ji-chu&quot;&gt;Svelte 基礎&lt;&#x2F;h2&gt;
&lt;p&gt;「Svelte 元件」是構成 Svelte 專案的基礎，也是我們後續撰寫的主要對象，而 Svelte 元件本身是包含了 HTML、CSS、JS 的 .svelte 檔案。&lt;&#x2F;p&gt;
&lt;p&gt;可以把 .svelte 檔案理解為一種被強殖 Svelte 特性的 .html 檔案，裡面有 HTML 標籤與內容，也有 &lt;code&gt;&amp;lt;script&amp;gt;&lt;&#x2F;code&gt; 區塊放元件的程式邏輯，還有 &lt;code&gt;&amp;lt;style&amp;gt;&lt;&#x2F;code&gt; 區塊放元件的樣式。&lt;&#x2F;p&gt;
&lt;p&gt;元件與元件之間是隔離的，彼此不共享元件內的 JS 及 CSS，即便存在於某兩個元件內的 CSS 具有相同的命名，他們在 compile 後也會被重新命名，讓彼此的參照是獨立的，藉此達到隔離的效果，當然 Svelte 也提供了元件與元件之間傳值的機制，並且元件也可以包含另一個子元件，透過這樣重複包裝的巢狀元件，最終構築出一個完整的 web app。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;hello-world&quot;&gt;Hello world&lt;&#x2F;h3&gt;
&lt;p&gt;第一個元件是老朋友「Hello world!」，在 &lt;code&gt;&amp;lt;script&amp;gt;&lt;&#x2F;code&gt; 區塊內定義的值 &lt;code&gt;name&lt;&#x2F;code&gt;，可以直接在 HTML 內以大括號包圍的格式 &lt;code&gt;{name}&lt;&#x2F;code&gt; 呼叫，因為 &lt;code&gt;name&lt;&#x2F;code&gt; 是字串，也可以在大括號內直接調用 &lt;code&gt;name&lt;&#x2F;code&gt; 的字串函式：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 600px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;6ee6053c1bb2412fb33b7ee47cc4503f?version=3.43.1&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;h3 id=&quot;html-biao-qian-shu-xing-cha-zhi&quot;&gt;HTML 標籤屬性插值&lt;&#x2F;h3&gt;
&lt;p&gt;前面我們把 &lt;code&gt;name&lt;&#x2F;code&gt; 做為內容插入，Svelte 也可以讓我們把值做為 HTML 的屬性插入：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 600px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;1503e3a3c6bd488c9490822ca6932e4e?version=3.43.1&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;當作屬性插入時，要不要用雙引號包住都是可以的，Svelte 會自動識別與轉換（其實&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;html.spec.whatwg.org&#x2F;multipage&#x2F;syntax.html#attributes-2&quot;&gt;在 HTML 的規範內，是允許省略雙引號的&lt;&#x2F;a&gt;）。&lt;&#x2F;p&gt;
&lt;p&gt;範例內的第三種用法較特別，如果 JS 變數名故意取的和 HTML 屬性名一樣，那就可以用這種簡化的格式，讓我們可以少打幾個字。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;chao-zhuang-yuan-jian&quot;&gt;巢狀元件&lt;&#x2F;h3&gt;
&lt;p&gt;如同在 JS 引入其他套件一般，我們用 &lt;code&gt;import&lt;&#x2F;code&gt; 語句在 App.svelte 元件內引入另一個 Nested.svelte 元件，並且以 &lt;code&gt;&amp;lt;Nested&amp;gt;&lt;&#x2F;code&gt; 的形式在 HTML 內調用：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 500px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;71db6d1acdff49518dc668fc140cef38?version=3.43.1&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;要注意的是，Svlte 的元件都應該要以大寫開頭的形式命名，避免與 HTML 原生標籤混淆。&lt;&#x2F;p&gt;
&lt;p&gt;另外注意到 App.svelte 內的 &lt;code&gt;&amp;lt;p&amp;gt;&lt;&#x2F;code&gt; 被我們改成紫色，並且這並不影響子元件 Nested.svelte 內的 &lt;code&gt;&amp;lt;p&amp;gt;&lt;&#x2F;code&gt;，這印證了前面說的「元件是互相隔離的」的說法。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;nei-han-html-biao-qian-de-zi-chuan-chu-li&quot;&gt;內含 HTML 標籤的字串處理&lt;&#x2F;h3&gt;
&lt;p&gt;如果 JS 字串內含有 HTML 標籤，基於安全考量，預設的情況下是會被跳脫處理的，也就是會被處理成純文字，如果想要關閉這個特性，那得加上額外的 &lt;code&gt;@html&lt;&#x2F;code&gt; 標示：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 400px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;2842a4da0c4d44d59affc4db274ea27d?version=3.43.1&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;該怎麼理解這個 &lt;code&gt;@html&lt;&#x2F;code&gt;？個人是把它理解為一個裝飾器（decorator），然而這只是個人的解讀，在 Svelte 文件內並沒有明確定義 &lt;code&gt;@html&lt;&#x2F;code&gt; 的角色。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;reactivity&quot;&gt;Reactivity&lt;&#x2F;h2&gt;
&lt;p&gt;在我以前念的化學領域，reactivity 指的是物質的反應性，而在程式領域，reactivity 的華文是「&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E5%93%8D%E5%BA%94%E5%BC%8F%E7%BC%96%E7%A8%8B&quot;&gt;回應式程式設計&lt;&#x2F;a&gt;」，可以簡單的理解為「JS 與 HTML 的連動機制」。&lt;&#x2F;p&gt;
&lt;p&gt;我們可以透過下面的實例感受所謂的 reactivity，按鈕內的 &lt;code&gt;{count}&lt;&#x2F;code&gt; 是隨著 JS 內的 &lt;code&gt;count&lt;&#x2F;code&gt; 而變的，他們之間的連動特性是由 Svelte 提供的，因此我們再也不用手動處理元件的更新：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 500px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;e4deece8a9294e80ad1a167f90587f27?version=3.43.1&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;在上面的例子中：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;按鈕與 click 事件是透過 &lt;code&gt;on:click&lt;&#x2F;code&gt; 語句綁定的，這是 Svelte 特有的事件綁定語法，與 HTML 標籤原有的 &lt;code&gt;onclick&lt;&#x2F;code&gt; 屬性相當相似，應該很容易理解，只要記得不要打錯字。&lt;&#x2F;li&gt;
&lt;li&gt;按鈕內的 &lt;code&gt;{count === 1 ? &#x27;time&#x27; : &#x27;times&#x27;}&lt;&#x2F;code&gt; 僅是一個決定單字單複數形的三元表達式。（&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;your-english-firm.blogspot.com&#x2F;2017&#x2F;06&#x2F;zero-followed-by-plural-or-singular-noun.html&quot;&gt;零後面到底要跟單數名詞，還是複數名詞？&lt;&#x2F;a&gt;）&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;reactive-declarations&quot;&gt;Reactive Declarations&lt;&#x2F;h3&gt;
&lt;p&gt;讓上面的例子再複雜一點，放另一個變數 &lt;code&gt;doubled&lt;&#x2F;code&gt;，並且我們希望這個 &lt;code&gt;doubled&lt;&#x2F;code&gt; 是 &lt;code&gt;count&lt;&#x2F;code&gt; 的兩倍。&lt;&#x2F;p&gt;
&lt;p&gt;我們可以在 HTML 內加入一個簡單的敘述 &lt;code&gt;{count * 2}&lt;&#x2F;code&gt; 得到兩倍的數值，但若是需要重複調用，那就要無數次的 &lt;code&gt;{count * 2}&lt;&#x2F;code&gt;，這不利於未來的重構，為此我們需要另一個 Svelte 特有的語法，以 &lt;code&gt;$: doubled&lt;&#x2F;code&gt; 的形式賦予這個變數 &lt;code&gt;doubled&lt;&#x2F;code&gt; 也具有 reactivity 的特性：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 600px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;c3dd9a36387d43b6aacd67f3aaa0996f?version=3.43.1&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;被加上 &lt;code&gt;$:&lt;&#x2F;code&gt; 修飾後的 &lt;code&gt;doubled&lt;&#x2F;code&gt;，會在狀態發生變化時，自動重新執行，所謂的狀態發生變化，以上例來說，就是 &lt;code&gt;handleClick()&lt;&#x2F;code&gt; 內 &lt;code&gt;count&lt;&#x2F;code&gt; 的數值改變。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;reactive-statements&quot;&gt;Reactive Statements&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;$:&lt;&#x2F;code&gt; 不僅能為變數加上 reactivity 的特性，它也能用於其他的程式敘述，讓該句敘述也能因應狀態變化而再次執行。&lt;&#x2F;p&gt;
&lt;p&gt;下面的範例中，每次點擊都會引發 &lt;code&gt;count&lt;&#x2F;code&gt; 改變，並且我們又用 &lt;code&gt;$: console.log(&lt;code&gt;the count is ${count}&lt;&#x2F;code&gt;);&lt;&#x2F;code&gt; 令這句敘述也會隨著 &lt;code&gt;count&lt;&#x2F;code&gt; 的改變而再次執行：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 600px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;e8a56ee54f644e19af3b0ed8ea65ff32?version=3.43.1&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;&lt;code&gt;$:&lt;&#x2F;code&gt; 可以拿來修飾變數、修飾敘述，還可以拿來修飾區塊：&lt;&#x2F;p&gt;
&lt;iframe class=&quot;wide&quot; style=&quot;height: 600px; padding: 0; border: 1px solid;&quot; src=&quot;https:&#x2F;&#x2F;svelte.dev&#x2F;repl&#x2F;fca7ecab5ba34c30b004db533f3aadc3?version=3&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;h2 id=&quot;xiao-jie&quot;&gt;小結&lt;&#x2F;h2&gt;
&lt;p&gt;整理一下截至目前為止我們遇到的 Svelte 特有語法：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;@html&lt;&#x2F;code&gt;：讓內含 HTML 標籤的字串不經過跳脫處理，直接原樣送給瀏覽器 render。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;on:&lt;&#x2F;code&gt;：綁定 HTML 元素的事件行為。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;$:&lt;&#x2F;code&gt;：賦予變數或敘述令其具有 reactivity 的特性。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;元件化、資料綁定、事件綁定是每個現代化 web 框架都具有的特性，每個框架實現的方式各異，在這一集我們介紹了 Svelte 在這三方面的用法，多少會有點奇葩之感，然而這些奇葩的語法，未來只會更多，讓我們拭目以待。&lt;&#x2F;p&gt;
&lt;p&gt;（待續）&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>ExcelMerger－文創小農獨立手做 app 幫你合併 Excel 檔</title>
        <published>2021-09-30T00:00:00+00:00</published>
        <updated>2021-09-30T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/excelmerger/"/>
        <id>https://editor.leonh.space/2021/excelmerger/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/excelmerger/">&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;excelmerger&#x2F;cover.png&quot; alt=&quot;ExcelMerger&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;Leon0824&#x2F;ExcelMerger&quot;&gt;ExcelMerger&lt;&#x2F;a&gt; 是個簡單到不行的 app－它幫我們合併 Excel 檔，可以是合併一個檔案內的多個工作表，也可以是合併多個檔案。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;excelmerger&#x2F;ExcelMerger.png&quot; alt=&quot;ExcelMerger&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;安裝完，跑起來的畫面只有一個簡單的拖放區，用戶可以把 Excel 檔從檔案總管拖進去，或是點擊拖放區用開檔案的方式把 Excel 檔讀進去，ExcelMerger 會把這些檔案合併到一個新檔案內，並跳出另存新檔的畫面，剩下的步驟就如同一般的 Windows 操作那樣，把合併好的新檔案放在你喜歡的地方。&lt;&#x2F;p&gt;
&lt;p&gt;雖然說這麼簡單，但還是有些事項要提醒的：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;為了保證格式的正確性，ExcelMerge 背後是呼叫 Excel 來做剪下貼上的工作，而不是用第三方工具，所以電腦內得有裝 Excel，也因此這個小 app 是 Windows 限定的。&lt;&#x2F;li&gt;
&lt;li&gt;合併的基礎是每張表都有一致的欄位，這個小 app 並不具備人工智慧判斷欄位的性能，它其實只幫我們做重複性的複製、貼上而已。&lt;&#x2F;li&gt;
&lt;li&gt;每張表的第一列通常是標題列，ExcelMerger 只會保留一次標題列，所以不用擔心合併後的檔案會出現重複的標題列，除此之外，它真的只有做複製、貼上，不會動到原始檔案的任何東西。&lt;&#x2F;li&gt;
&lt;li&gt;本 app 只有在 Windows 10 &#x2F; Microsoft 365 Excel、Excel 2019 測過，其他 Windows &#x2F; Excel 版本能應該也能用，如果有問題請到它的專案頁回報。&lt;&#x2F;li&gt;
&lt;li&gt;不含惡意程式，由&lt;strong&gt;文創小農獨立手做，天然、有機、無毒、無殘留&lt;&#x2F;strong&gt;，如果被防毒程式警告應該是誤判，原碼公開在 GitHub，未來若有新版本也都會發布在 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;Leon0824&#x2F;ExcelMerger&quot;&gt;ExcelMerger 的 GitHub 專案頁&lt;&#x2F;a&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;至於為什麼要特地寫一支傻瓜 app 幫我們做 &lt;kbd&gt;CTRL-C&lt;&#x2F;kbd&gt;、&lt;kbd&gt;CTRL-V&lt;&#x2F;kbd&gt;，因為這樣的重複性動作對人類來說太慢也太乏味，又可能貼錯，這種機械性的重複作業正是程式最適合發揮的地方，另一個原因是我們家洪媽媽剛好有這樣的需求，幫家人提高工作效率之餘，也把 app 放出來，希望能援助到其他的地方媽媽。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;kai-fa-bi-ji&quot;&gt;開發筆記&lt;&#x2F;h2&gt;
&lt;p&gt;這支看似簡單的 app 背後卻是用相當不對稱的 Rust + Python + JavaScript 三種語言來實現：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;JavaScript 負責處理前端介面，前端框架是 Svelte，但幾乎完全沒用到 Svelte 的特性…。&lt;&#x2F;li&gt;
&lt;li&gt;Rust 的 Tauri 框架負責把 web 封裝成桌面程式，並且提供存取本機檔案的 API 給 JavaScript。&lt;&#x2F;li&gt;
&lt;li&gt;Python 負責後端呼叫 Excel 做開檔、合併、存檔的動作。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;excel-dang-an-chu-li&quot;&gt;Excel 檔案處理&lt;&#x2F;h3&gt;
&lt;p&gt;對「讀 Excel 檔案，並且合併」這個核心需求，原本是不考慮 Python 的，但搜尋了幾個 JavaScript 套件，例如 ExcelJS，用的很爽但後來才發現它不支援舊的 xls 檔案…最後試了 SheetJS，但終究因為下面的原因最後還是放棄在 JavaScript 內處理 Excel 檔：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;SheetJS 不支援儲存格樣式，即便 SheetJS Pro 好像有支援。&lt;&#x2F;li&gt;
&lt;li&gt;在 JS 處理某些舊的 xls 檔會遇到解碼問題，某些陳年的 Delphi &#x2F; InterBase 程式輸出的 xls 檔案內的文字是 Big5，但在 JavaScript 被錯誤的以 UTF-8 解成亂碼，於是又要多一層調用 iconv-lite 來轉換。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;這些問題當然都是走過路過才會遇過的，最終只好把這部分砍掉重練，改用 Python 直接呼叫 Excel，一勞永逸的解決掉解碼和樣式遺漏問題，但副作用就是從此這個 app 只能在 Windows 下使用了，並且用戶得先裝有 Excel，幸好對我們的目標用戶來說，應該九成九的人都滿足這兩點要求。&lt;&#x2F;p&gt;
&lt;p&gt;關於 Python 呼叫 Excel 的部分，可以參考另外一篇〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;write-excel-macros-with-python&#x2F;&quot;&gt;用 Python 寫 Excel 巨集&lt;&#x2F;a&gt;〉。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;qian-duan-jie-mian&quot;&gt;前端介面&lt;&#x2F;h3&gt;
&lt;p&gt;因為原本天真地以為可以用 JavaScript 就能處理好 Excel 檔案合併的工作，因此用 web 做 UI 再封裝成桌面程式便成了自然而然的選擇，即便後來改用 Python 處理 Excel 檔，然而幾經權衡之下，還是決定沿用 web 做 UI 的方案，除了因為 web UI 的頭已經洗一半，不想全部砍掉重練改用 wxPython 之外，受到公司同事的影響，個人也較偏好 web UI 的方案，布局比傳統 GUI 套件更自由，並且 web 的布局邏輯可以通用到瀏覽器端、桌面端和手機端，是一條可以共用的知識鏈，所以最終還是繼續採用以 web 為基礎的介面。&lt;&#x2F;p&gt;
&lt;p&gt;UX 方面，最初就確立了採用盡可能簡單的拖放式介面，這是受到過往使用某些 macOS app 的啟發，例如 Keka：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;excelmerger&#x2F;keka.png&quot; alt=&quot;Keka&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;一直以來我都被這樣的簡潔吸引著，這是近乎完美的簡潔，有人這樣詮釋完美：&lt;&#x2F;p&gt;
&lt;blockquote class=&quot;&quot;&gt;
&lt;div class=&quot;quote&quot;&gt;
&lt;p&gt;Perfection is achieved not when there is nothing more to add, but when there is nothing left to take away.&lt;&#x2F;p&gt;
&lt;p&gt;所謂完美境界，並非加無可加，而是減無可減。&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
&lt;div class=&quot;source&quot;&gt;
&lt;p&gt;Antoine de Saint-Exupéry - &lt;span style=&quot;font-style: italic;&quot;&gt;Ch III: The Tool, &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E4%BA%BA%E7%9A%84%E5%A4%A7%E5%9C%B0&quot;&gt;Terre des hommes&lt;&#x2F;a&gt;&lt;&#x2F;span&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;之前寫過的〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;doublesllash.com&#x2F;blog&#x2F;posts&#x2F;2021&#x2F;80-20-rule&quot;&gt;談產品設計的 80&#x2F;20 法則&lt;&#x2F;a&gt;〉、〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;doublesllash.com&#x2F;blog&#x2F;posts&#x2F;2021&#x2F;pos-principle&quot;&gt;談 POS 設計背後的原則&lt;&#x2F;a&gt;〉也都有一貫的核心思想－盡可能地去除軟體中多餘的部分，減少選擇就是提高效率（在商業上也是如此）。&lt;&#x2F;p&gt;
&lt;p&gt;關於在 web 實現拖放介面的部分，可以參考另外一篇〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;handling-files-in-frontend&#x2F;&quot;&gt;Web 處理檔案那些事－上傳篇&lt;&#x2F;a&gt;〉。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;feng-zhuang-cheng-zhuo-mian-cheng-shi&quot;&gt;封裝成桌面程式&lt;&#x2F;h3&gt;
&lt;p&gt;想把 web 封裝成桌面程式，除了 Tauri 外，還有最多人用的 Electron 與 NW.js，最後選擇小眾的 Tauri 的原因是：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;使用簡單，Tauri 身為一個 2019 年底才問世的小朋友，相較於兩位大哥哥，它具有更簡單快速上手的特性。&lt;&#x2F;li&gt;
&lt;li&gt;低耦合，Tauri 不綁定 Chromium，它是調用各 OS 自帶的 WebView 做顯示，因此 Tauri 編譯出來的安裝檔都非常小。&lt;&#x2F;li&gt;
&lt;li&gt;還是低耦合，Tauri 底層是 Rust，另外提供一層 JavaScript 的 API 讓程式得以調用本機檔案系統之類的資源，底層的 Rust 也是可抽換的，未來有可能支援 Python &#x2F; Nim 等其他語言做為底層。&lt;&#x2F;li&gt;
&lt;li&gt;潮。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;當然，上面我所看重的特性並不適用於全部的場景，在公司內可能更可能採用成熟又有富爸爸支持的 Electron，像 Tauri 這類還在發展中的工具，就留給我們這個富有實驗精神的文創小農獨立手做，天然、有機、無毒、無殘留的小 app 來用即可。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-yu&quot;&gt;結語&lt;&#x2F;h2&gt;
&lt;p&gt;希望 ExcelMerger 能幫助到有需要的朋友，喜歡的朋友請我幫按讚、訂閱、開啟小鈴鐺，一鍵三連，感恩。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>用 Python 寫 Excel 巨集</title>
        <published>2021-09-28T00:00:00+00:00</published>
        <updated>2021-09-28T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/write-excel-macros-with-python/"/>
        <id>https://editor.leonh.space/2021/write-excel-macros-with-python/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/write-excel-macros-with-python/">&lt;p&gt;說到 Excel 巨集，最直覺聯想到的應該會是 VBA，但其實微軟不只提供一種巨集語言，例如在網頁版的 Excel 並不支援 VBA，微軟給用戶的巨集語言是 Office Scripts，Office Scripts 是 TypeScript 的語法加上 Excel 物件模型的綜合體；又或者是增益集，微軟大大推的也是 JavaScript &#x2F; TypeScript 的開發方案，而本文要介紹的是以 Python 為基礎的 Excel 開發模式。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shen-mo-shi-python-xie-excel-ju-ji&quot;&gt;什麼是「Python 寫 Excel 巨集」？&lt;&#x2F;h2&gt;
&lt;p&gt;簡單的說，就是在 Python 程式內呼叫 Windows API 操控 Excel，具體的說是使用 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.xlwings.org&#x2F;&quot;&gt;xlwings&lt;&#x2F;a&gt; 提供的封裝過的的 Excel COM API 以及透過 pywin32 提供的 Windows API 橋接能力來實現在 Python 程式操控 Excel 的目的，這樣的組合用起來和 VBA 寫巨集有八成像，所以才說「用 Python 寫 Excel 巨集」。&lt;&#x2F;p&gt;
&lt;p&gt;這種模式其實和在 C# 調用 Excel COM API 是一樣的，差別在於 Python 在數據處理領域擁有 C# 無法比擬的&lt;strong&gt;成熟&lt;&#x2F;strong&gt;生態系。&lt;&#x2F;p&gt;
&lt;p&gt;和一般用 Python 套件讀寫 Excel 檔案不同的是，Python + xlwings 的做法是去操控 Excel，所以不會有讀寫後格式跑掉的問題，就算有也是 Excel 自己造成的，而用戶對 Excel 自己造成的跑版問題有著異常高的寬容度。（反之，LibreOffice 開 Office 文件有點小跑版，用戶就會勃然大怒。）&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wei-shen-mo-shi-python-vba-bu-xiang-ma-huo-js-bu-xiang-ma&quot;&gt;為什麼是 Python，VBA 不香嗎？或 JS 不香嗎？&lt;&#x2F;h2&gt;
&lt;p&gt;改用 Python 最顯著的好處當然是可以享用 Python 生態系的一切工具，特別是在數據處理這塊，這些好處列舉如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;可以用 pandas 等著名的數據套件處理 Excel 資料。&lt;&#x2F;li&gt;
&lt;li&gt;可以在 Jupyter Notebook 環境或任何你喜愛的編輯器寫 Python 腳本，再也不用碰那陳年的 VBA 編輯器。&lt;&#x2F;li&gt;
&lt;li&gt;你的巨集腳本可以被版控、被集中整理，而不會隨著 Excel 檔案散落四方，因而享有所有版控的特性，多人協作、功能分支等等。&lt;&#x2F;li&gt;
&lt;li&gt;你還可以在 Python 腳本內透過 xlwings 調用 Excel 公式。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;這幾點在其他的語言能否找到與之相當的工具，就看各個語言的套件生態豐不豐富了，至少在 npm 裡面我沒找到相對應的工具鏈。&lt;&#x2F;p&gt;
&lt;p&gt;而 VBA 本身，雖然也曾經輝煌過，十幾年前 AutoCAD 和 CorelDRAW 也一度支援 VBA 巨集，後來 AutoCAD 回歸 Lisp 的懷抱、CorelDRAW 也認清要平面設計師寫巨集是不切實際的奢望…，現在的 VBA 不論對微軟或對我們來說，都更像是個歷史遺產而不是資產，因此對沒有歷史包袱的巨集開發者來說，改用 Python 完全是個可以考慮的選項。&lt;&#x2F;p&gt;
&lt;p&gt;當然，改用 Python 必然要建立 Python 開發環境，這點額外的負擔可以參見〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;python&#x2F;&quot;&gt;建置 Python 3.9 &#x2F; 3.10 開發環境&lt;&#x2F;a&gt;〉或〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;windows-python-node-js&#x2F;&quot;&gt;在 Windows 建置以 Visual Studio 為基礎的 Python &#x2F; Node.js 開發環境&lt;&#x2F;a&gt;〉 ，讓你快速在二十分鐘內建出心愛的 Python 環境。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yong-python-cao-kong-excel&quot;&gt;用 Python 操控 Excel&lt;&#x2F;h2&gt;
&lt;p&gt;前面提過，用 Python 操控 Excel 是靠 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.xlwings.org&#x2F;&quot;&gt;xlwings&lt;&#x2F;a&gt; 與 pywin32 兩個套件實現的，這與我們利用 &lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;playwright&#x2F;&quot;&gt;Playwright&lt;&#x2F;a&gt; 或 Selenium 做瀏覽器自動化的模式相當類似，所以用 Python 寫 Excel 巨集這件是我們也可以稱為用 Python 做 Excel 自動化。&lt;&#x2F;p&gt;
&lt;p&gt;先放一部 xlwings 的影片感受一下操作體驗：&lt;&#x2F;p&gt;
&lt;iframe src=&quot;https:&#x2F;&#x2F;www.youtube-nocookie.com&#x2F;embed&#x2F;A3jrUXNokYk&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;這裡我們直接用程式說明 xlwings 的用法。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;chu-shi-hua-yu-du-dang-cun-dang&quot;&gt;初始化與讀檔、存檔&lt;&#x2F;h3&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;import&lt;&#x2F;span&gt;&lt;span&gt; xlwings&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; as&lt;&#x2F;span&gt;&lt;span&gt; xw&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;with&lt;&#x2F;span&gt;&lt;span&gt; xw.App(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;add_book&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;False&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt; visible&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;True&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; as&lt;&#x2F;span&gt;&lt;span&gt; app:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    wb: xw.Book&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; app.books.open(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;example.xlsx&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    sheet: xw.Sheet&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; wb.sheets[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    # handle the sheet&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    wb.save()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在引入 &lt;code&gt;xlwings&lt;&#x2F;code&gt; 並命名為 &lt;code&gt;xw&lt;&#x2F;code&gt; 後，我們建立了一個 &lt;code&gt;App&lt;&#x2F;code&gt; 物件，因為有存取 Excel 及開關檔案的原因，&lt;code&gt;App&lt;&#x2F;code&gt; 是被設計成支援 context manager 的，因此我們用 &lt;code&gt;with&lt;&#x2F;code&gt; 敘述讓 Python 自動幫我們處理掉區塊執行後自動關閉 Excel 的瑣事。&lt;&#x2F;p&gt;
&lt;p&gt;接著用一個 &lt;code&gt;book&lt;&#x2F;code&gt; 物件叫 Excel 開啟 example.xlsx 檔案，再把檔案內的第一個工作表指到 &lt;code&gt;sheet&lt;&#x2F;code&gt;，最後我們用 &lt;code&gt;save()&lt;&#x2F;code&gt; 叫 Excel 幫我存檔。&lt;&#x2F;p&gt;
&lt;p&gt;中間的 &lt;code&gt;# handle the sheet&lt;&#x2F;code&gt; 部分，典型的操作是找到你要的儲存格，再做後續的讀取、運算、寫入等工作。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;xuan-ze-chu-cun-ge&quot;&gt;選擇儲存格&lt;&#x2F;h3&gt;
&lt;p&gt;儲存格的選定，不論是單格或一個範圍，都是用 xlwings 的 &lt;code&gt;range()&lt;&#x2F;code&gt; 函式。&lt;code&gt;range()&lt;&#x2F;code&gt; 可以接受多種的儲存格標示法：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# Excel 標示法－單格&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;sheet.range(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;A1&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# Excel 標示法－多格&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;sheet.range(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;A1:C3&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# 以 1 為首的 tuple (row, column) 標示法－單格&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;sheet.range((&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;2&lt;&#x2F;span&gt;&lt;span&gt;))&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; # B1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# 以 1 為首的 tuple (row, column) 標示法－多格&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;sheet.range((&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;2&lt;&#x2F;span&gt;&lt;span&gt;), (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;3&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;4&lt;&#x2F;span&gt;&lt;span&gt;))&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; # B1:D3&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;建議不要用 Excel 標示法，因為欄數一超過 26 欄還要去處理 &lt;code&gt;AA&lt;&#x2F;code&gt;、&lt;code&gt;AB&lt;&#x2F;code&gt;、&lt;code&gt;AC&lt;&#x2F;code&gt; 這類兩碼欄號的邏輯問題。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;chu-cun-ge-de-qu-zhi-yu-fu-zhi&quot;&gt;儲存格的取值與賦值&lt;&#x2F;h3&gt;
&lt;p&gt;取值與賦值也相當直覺，下面改用 Python REPL 模式說明較能直觀感受到指令的回應：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; rng1&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; sheet.range((&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;2&lt;&#x2F;span&gt;&lt;span&gt;))&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; # B1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; rng1.value&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;每次 Jira 的卡頓都令人抓狂&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; rng1.value&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;用戰術上的勤奮掩蓋戰略上的怠惰&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; rng1.value&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;用戰術上的勤奮掩蓋戰略上的怠惰&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; rng2&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; sheet.range((&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;2&lt;&#x2F;span&gt;&lt;span&gt;), (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;4&lt;&#x2F;span&gt;&lt;span&gt;))&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; # B1:D1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; rng2.value&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;a&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;b&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;c&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; rng2.value&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;d&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;e&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;f&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; rng2.value&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;d&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;e&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;f&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; rng3&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; sheet.range((&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;10&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span&gt;), (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;11&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;3&lt;&#x2F;span&gt;&lt;span&gt;))&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; # A10:C11&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; rng3.value&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;a&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;b&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;c&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;], [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;10.0&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 20.0&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 30.0&lt;&#x2F;span&gt;&lt;span&gt;]]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;對於 &lt;code&gt;A10:C11&lt;&#x2F;code&gt; 這樣 3 * 2 的範圍，對應的值也會以 3 * 2 的巢狀二維陣列形式表示。&lt;&#x2F;p&gt;
&lt;p&gt;只要靠 &lt;code&gt;range()&lt;&#x2F;code&gt; 搭配程式本身的迴圈或迭代結構，就可以完成大部分的任務囉，是不是相當簡單直覺呢！&lt;&#x2F;p&gt;
&lt;h3 id=&quot;xiao-jue-qiao&quot;&gt;小訣竅&lt;&#x2F;h3&gt;
&lt;p&gt;分享一點小訣竅：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;一個 xlwings 的 &lt;code&gt;App&lt;&#x2F;code&gt; 可以開啟多份 Excel 檔案，可以互相剪剪貼貼。&lt;&#x2F;li&gt;
&lt;li&gt;也可以開多個 xlwings 的 &lt;code&gt;app&lt;&#x2F;code&gt;，此時他們會有各自的程序 ID。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;jie-yu&quot;&gt;結語&lt;&#x2F;h2&gt;
&lt;p&gt;VB 大哥已死，小弟弟 Small Basic 又注定只能是玩具不能是工具，肩負著教育責任的 Small Basic 首頁卻有著奇葩的烏龜人：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;write-excel-macros-with-python&#x2F;smallbasic-publicwebsite.azurewebsites.net_.png&quot; alt=&quot;Small Basic 烏龜人&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在這樣的勢態下，微軟僅容你 VBA 苟活，至於那美好的未來，看來是 Office Scripts 會與我們一同走過，但在 Office Scripts 尚未支援桌面版 Excel 的當下，對於沒有歷史包袱的我們，改用 Python 可能是更好的選擇，而且身分還能從「寫巨集的😞」搖身一變成🕺「大數據工程師🤓」，一整個潮到出汁💦。&lt;&#x2F;p&gt;
&lt;p&gt;本文僅介紹 xlwings 最常用的幾個功能，但 xlwings 的能力遠不止於此，它還有個特異功能，用 xlwings 的 CLI 指令讀檔，它會以 HTTP 服務的方式供應那份 Excel 檔的 API，各種花式用法請參閱 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.xlwings.org&#x2F;zh_TW&#x2F;latest&#x2F;&quot;&gt;xlwings 文件&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>被創業之路（二）商業模式</title>
        <published>2021-09-23T00:00:00+00:00</published>
        <updated>2021-09-23T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/being-started-2/"/>
        <id>https://editor.leonh.space/2021/being-started-2/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/being-started-2/">&lt;h2 id=&quot;qian-yan&quot;&gt;前言&lt;&#x2F;h2&gt;
&lt;p&gt;2019 年，我和我的團隊夥伴們「被創業」，於是我們走上創業之路，因為是「被創業」，我們有著與眾不同的心路歷程與辛酸，這些點滴會在這系列分享。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;上一集：〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;being-started-1&#x2F;&quot;&gt;被創業之路（一）被創業&lt;&#x2F;a&gt;〉&lt;&#x2F;p&gt;
&lt;p&gt;現在是 2021 年，剛好是個很適合回顧過去兩年來我們的成長、挫折與衝突的時刻。&lt;&#x2F;p&gt;
&lt;p&gt;在確立了以接案來維持收入的路線之後，我就開始我的業務生涯，此時的我們是資訊接案產業的新進者，手上沒有任何的客戶，只能透過接案平台來找新的客戶，原始的構想很單純（&lt;strong&gt;現在看來很天真&lt;&#x2F;strong&gt;），只要有機會的都接，手機 app，接、形象網站，接、系統整合，接、客製系統，接，這樣經歷了幾個月之後，我逐漸認為我們的組織或文化並不適合接案的商業模式，這樣的想法萌芽之後讓我與同事們在下半年產生了巨大的路線衝突，這點後面會再提到。&lt;&#x2F;p&gt;
&lt;p&gt;在談我為什麼認為我們不適合接案前，先談談接案這塊業務。&lt;&#x2F;p&gt;
&lt;p&gt;除了自己從接案平台陌生開發，我們當然也想著讓客戶主動發現我們，於是在最紅海的形象網站、購物網站這塊，我們另外成立了一個專屬於它的品牌網站，並且為它投放網路與紙本廣告，還請了一位全職的寫手做內容行銷，但最終這塊業務是失敗的，最終只獲得三家客戶，而且其中一家還是同事介紹的，關於它的失敗，在公司內當然很容易歸咎於戰犯就是我，因為我是業務，但過往的 ISO 教育給我的體悟是應該要&lt;strong&gt;從系統面來看失效模式&lt;&#x2F;strong&gt;，因此如果把這塊業務視為一個系統，並且把問題的層次拉高到「&lt;strong&gt;為什麼業務人員無法全力拓展業務&lt;&#x2F;strong&gt;」這個系統性的問題上，就會獲得一個完全不同方向的結論…。&lt;&#x2F;p&gt;
&lt;p&gt;除了自行開發，也有從大股東與朋友方面介紹來的客戶，這些來自各路的案子，規模小至一萬，大至兩百多萬，不論錢的多寡，這部份的業務在我看來也是失敗的，當然每個個案都有自己的故事，顧慮到隱私，不會在此透露個案細節，但同樣的，我們把思考的維度拉高到系統面，把接案的商業模式視為一個系統，可以把問題歸納成下面幾點。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;high-cost&quot;&gt;High Cost&lt;&#x2F;h2&gt;
&lt;p&gt;成本高這個問題由兩個因素構成，一是薪資，二是交期。在 high cost 的情況下就會有一種畸形的賠本交易發生，俗話說「殺頭的生意有人做，賠錢的生意沒人做」，這句話不完全對，因為又有種商業模式是「羊毛出在狗身上，豬買單」，但很遺憾的，敝公司的賠錢做是真的賠錢做…，曾經有個賠錢做的個案是想把該個案的系統在以後變成我們可以對外經營的業務之一，所以認為沒賺甚至小虧一點也可以，當做客戶幫我們負擔研發費用，但這條路看起來也走不下去，這與後面的其它問題有關…。&lt;&#x2F;p&gt;
&lt;p&gt;軟體業薪資高不是新聞，但考慮到我們公司的量體、規模、資本、知名度，我認為在其中一項沒有改善前都避免不了高成本的問題。&lt;&#x2F;p&gt;
&lt;p&gt;說到量體大、規模大、資本大、知名度大，國內有精誠、國外有 MicroFocus，當然它們現在都已不是單純的接案公司，但客製系統還是它們主要的服務項目，它們的客戶也都是 top 100 級的企業或政府機構。另一種量體較小的典型像是新芽，它們都是可以做到即便人力薪資高，公司也還是能賺錢的境界，看起來很美好，也是我最常在公司內聽見的&lt;strong&gt;夢幻粉紅色泡泡&lt;&#x2F;strong&gt;，但回歸務實，試想它們只有五個人老闆兼工友的時期呢？過的又是怎樣的生活？領的又是怎樣的薪水？辦公室也像形象圖那樣朝氣蓬勃嗎？&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;being-started-2&#x2F;banner.jpeg&quot; alt=&quot;新芽 2020&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：新芽&lt;&#x2F;figcaption&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;being-started-2&#x2F;25sprout.jpg&quot; alt=&quot;新芽 2012&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：新芽&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;沒有，&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;blog.25sprout.com&#x2F;25sprout-%E5%89%B5%E6%A5%AD%E7%9A%84-365-%E5%A4%A9-bca0aad55424&quot;&gt;新芽的 Alex 和夥伴窩在小辦公室做著僅能剛好讓自己不會餓肚子的案子&lt;&#x2F;a&gt;，但至少這很務實，但敝公司從出生的第一天就必須背負著 high cost 的問題，但與 high cost 必須同時具備的量體、規模、資本、知名度卻付之闕如。這導致了業務在報價時的困境，報高難成交，報低會虧錢，在幾番衝突之後，政策還是決定報高，結果就可想而知了…。&lt;&#x2F;p&gt;
&lt;p&gt;量體、規模、資本、知名度的前面三點，都不是自己能掌控的（除非有富爸爸幫打雞血），唯有第四點是可以被努力經營的，知名度並非狹義的知名度，我把知名度三個字解讀為「品牌」、「形象」、「傳播」三者的綜合，也因此我在去年忙完某個個案後，第四季開始在公司內不斷提出（咆嘯）重構公司品牌、形象與強化對外傳播的路線，但也不意外的沒有獲得熱烈的迴響，不支持、不反對、不幫忙、偶爾扯一下後腿，當然最後獲得的就是散沙一盤。&lt;&#x2F;p&gt;
&lt;p&gt;第二個 high cost 的因素，交期，這其實和高薪資是互為因果的問題，因為薪資高，我們有機會拿到百萬級案件當然是勢在必得，但我們的團隊規模與能力其實是無法在期限內把成品交付，這導致了專案工期的延長，當然最終就是導致成本的上升，而身為業務，在評估階段我也只能相信工程團隊的預估，不過一年下來的經驗證明，他們花兩天預估的準度和我用二十分鐘通靈式預估的準度差不了多少…。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;long-pick-up-time&quot;&gt;Long Pick Up Time&lt;&#x2F;h2&gt;
&lt;p&gt;伴隨著 high cost 而來的是 long pick up time，對接案團隊來說，每個個案都有他們的 domain knowledge，這些 domain knowledge 帶給我們的就是 long pick up time，每個個案從了解需求到報價之間都充滿著許多不確定性，並且耗費時間，零售、保險、出版、課程、製造等等，每個產業的每個客戶都有他們的需求，在滿足客戶的需求之前必須先滿足內部對成本的估算，這樣耗費時間的工作，加上前面提到的 high cost，導致時間不僅長，成本又高，並且即使洽談了，也未必會成交，原因還是因為 high cost 導致的高報價，而洽談那段短則一週，長則一個月的時間，也都紮紮實實的浪費並支出掉了。&lt;&#x2F;p&gt;
&lt;p&gt;同樣的，對 long pick up time 的問題，可以有兩種層次的看法，淺碟式的想法，只會說出「就努力做啊，創業不就是這樣」的空話。另一種系統面的思考，把 high cost 與 long pick up time 這兩個互為因果的問題放在一起思考，就會得到與我類似的結論，接案這樣的商業模式並不適合我們，要嘛改變公司的體質，要嘛改變商業模式，在我得出這兩項結論後，也試圖對內傳達，最後的結果如同上一節所說的，不支持、不反對、不幫忙、偶爾扯一下後腿。第三種想法就更有創意，「&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;evchk.wikia.org&#x2F;zh&#x2F;wiki&#x2F;Boardroom_Suggestion&quot;&gt;解決掉提出問題的人，就沒有問題了 😄&lt;&#x2F;a&gt;」。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;being-started-2&#x2F;BoardroomSuggestion.webp&quot; alt=&quot;Boardroom Suggestion&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;engineering-first&quot;&gt;Engineering First&lt;&#x2F;h2&gt;
&lt;p&gt;前面兩個問題都是系統層面的問題，如果再深入探索更內在的問題，我認為會是 engineering first，或是從人的角度看則是 engineer first。&lt;&#x2F;p&gt;
&lt;p&gt;我曾經以書籍產品舉例，在 engineering first 的角度，書寫完了（打字打完了）就等於產品做完了，但站在出版業者看，書寫完了相當於只完成 1&#x2F;3 的產品，另外 1&#x2F;3 是找人寫序、編輯、校稿、美術、排版、印刷、包裝、通路等基本作業，最後的 1&#x2F;3 是輔宣物與行銷，包括海報、網站、作者包裝、訪談、送樣書給書評、簽書會等一系列的活動，而且這些都是環環相扣的，而我們家的工程師（兼老闆）們一直以來都堅信「只要有好的內容，印在 A4 紙也能當黃金賣」的天真思想，並且要求敝業務憑一張嘴向外兜售「超棒的內容」，為什麼要憑一張嘴呢？因為這些「超棒的內容」，有部份敝業務從來沒有機會試用，有部份只停留在耳聞的階段，有部份敝人有幸參與得以一窺全貌，並且因為 high cost 的關係，這印在 A4 紙上的內容，得必須賣很貴才能打平成本。&lt;&#x2F;p&gt;
&lt;p&gt;在我在內部會議提出上面的譬喻之後，也得到一個很有趣的回應：「J·K·羅琳也是靠機運成功的啊」，這個有趣的回應不僅為對話增添了風趣也有效的迴避了我的問題 :)，或許我們該把公司的資本都拿去包牌買樂透不是嗎？&lt;&#x2F;p&gt;
&lt;p&gt;Engineering first 的另一個問題是產品的企劃面，或許是看了太多老外工程師車庫創業成功的故事，我們家的工程師（兼老闆）們也是創意四射，並且對他們自己的新企劃充滿熱情，但很遺憾的，這樣的熱情也只有三分鐘，幾次往復下來我們產生了許多的「半成品」，這些半成品就相當於只打完字的書（有的還沒打完，只寫到一半），並且自信心爆棚的認為只要有人買單，就會把產品做完，真的把自己當「已成名的 J·K·羅琳」，敝業務光是在和他們爭執「成品」和「半成品」的定義就去掉了大半個月。&lt;&#x2F;p&gt;
&lt;p&gt;而自信心爆棚的熱血工程師（兼老闆）們，在我提出對他們眼中的「產品」可以在功能面和行銷面進一步完善以便我對外推廣時，卻又被拒絕（或是得到三不一扯後腿），堅定的認為他們手中既有的「產品」（我認為只是半成品）已經是可以賣了，即便我手上沒有任何的武器，沒有 demo、沒有網頁、沒有傳單、有的連截圖都沒有、沒有定價方案、沒有預估工時、沒有成功案例，只有我的舌燦蓮花。&lt;&#x2F;p&gt;
&lt;p&gt;與 engineering first 相對應的當然就是 the ohters last，在下無業績（抱歉我真的不會賣天書）的小業務當然也只能算是 the others 的一員，雖然無業績，但自認對客戶或用戶的心態還是能掌握的，所以一再挑戰他們眼中的「產品」的問題，我的挑戰可以歸納為產品定位、競品比較、行銷三個面向，當然這三者本身就是環環相扣的部份，但無奈往往得不到有意義的回應（三不一扯後腿），久了也只能學會閉嘴。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-yu&quot;&gt;結語&lt;&#x2F;h2&gt;
&lt;p&gt;歸納下來，我們的工程師（兼老闆）們，有人兩年來都持續活在新芽的夢幻粉紅泡泡內，有人拿公司資金去炒股炒匯賺零用金，有人用虛假的計畫申請 SBIR、有人只想繼續無腦式的接案，有人不斷的提出商業點子卻只想到功能面沒想到市場面，有人把公司的存活寄望於「命運」之上，而我則苦於沒有人願意正視我提出的問題，儘管他們也都各自試圖解決現金流問題，卻也都沒有真正解決問題，前面提到的 high cost 問題，裡面的任一個環節沒有被改變的情況下，high cost 就永遠是個問題。&lt;&#x2F;p&gt;
&lt;p&gt;最近聽到&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;uxcoffee.com&#x2F;episode&#x2F;88&quot;&gt;專訪 Fourdesire 創辦人 Taco 陳威帆的 podcast&lt;&#x2F;a&gt;，Taco 分享一個他從書上看到的一個觀點：「錢就像空氣，沒有它你會窒息，但你不是為了空氣而活」，回頭審視我任職的公司，去年第四季我開始提出系統性的問題，如果那時有人願意正視並且開始改變，或許上週的會議就不會為錢所苦，可惜沒如果。&lt;&#x2F;p&gt;
&lt;iframe src=&quot;https:&#x2F;&#x2F;www.youtube-nocookie.com&#x2F;embed&#x2F;vsBf_0gDxSM&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;這篇文章內充斥著許多衝突的情節，但也不只有這樣，這些教訓帶給我許多啟示，既然經歷過錯的，那也許下次就會往更對的方向走去。&lt;&#x2F;p&gt;
&lt;p&gt;下一集：〈被創業之路（三）可惜沒如果〉&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>被創業之路（一）被創業</title>
        <published>2021-09-22T00:00:00+00:00</published>
        <updated>2021-09-22T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/being-started-1/"/>
        <id>https://editor.leonh.space/2021/being-started-1/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/being-started-1/">&lt;h2 id=&quot;qian-yan&quot;&gt;前言&lt;&#x2F;h2&gt;
&lt;p&gt;2019 年，我和我的團隊夥伴們「被創業」，於是我們走上創業之路，因為是「被創業」，我們有著與眾不同的心路歷程與辛酸，這些點滴會在這系列分享。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;2017 年我透過同學的介紹，入職絆腳石，早在我到職的初期，就已經聽說，我們這個部門，未來會是一間獨立的公司，這是大頭家的規劃。&lt;&#x2F;p&gt;
&lt;p&gt;當時沒有人認真看待，公司成立計畫完全只在嘴上談兵階段，連紙上談兵都不算，並且，依照大頭家家族的慣例，旗下有多間公司已是常態，我們也以為又是一間為了做帳需求成立的紙上公司罷了。&lt;&#x2F;p&gt;
&lt;p&gt;時間快轉到 2018 年，成立公司的話題從年初到年中不斷的被有意的提起，我們終於意識到，頭家應該是說真的了，並且令人意外的（或者換個角度也可說毫不意外的）頭家要求我們也入股，經過一番的討價還價，總算把各自的出資與股權談定，開始進行一連串的行政作業，一直到 2019 年中，公司正式成立。&lt;&#x2F;p&gt;
&lt;p&gt;在 2018 年中，藉由大頭家強力要求我們入股的態度下，我們也終於意識到兩件事—1. 公司的成立，代表著原公司人力成本的遞減，大頭家認為我們太貴，他不想養。2. 大頭家心態上認為，一個四百人的公司不需要養看似後勤的資訊團隊，外包即可。這兩點談到成本與產值的問題，在當時來看非常正確，不過大頭家忽略了一些潛在的風險，這部份以後再來驗證。&lt;&#x2F;p&gt;
&lt;p&gt;2018 年秋季，即使我們全部人都清楚明白認知到，成立公司並且自負盈虧已經是箭在弦上，但我們依舊為公司內的專案忙碌著，對於新公司的規劃，還是在紙上談兵，講了很多不著邊際的可能性，我稱為「&lt;strong&gt;不食人間煙火的粉紅色泡泡&lt;&#x2F;strong&gt;」。&lt;&#x2F;p&gt;
&lt;p&gt;可能是我的雞歪個性使然，終於在某段時間不停對內開砲，那時我用了當時流行的佛系圖來對內比喻當時的現況：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;being-started-1&#x2F;fuboy-banner.jpg&quot; alt=&quot;佛系經營&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：《King of the Hill》&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;搭配這張圖的標題寫著「&lt;strong&gt;佛系經營？&lt;&#x2F;strong&gt;」，下方丟出一堆圍繞著如何&lt;strong&gt;自力更生&lt;&#x2F;strong&gt;活下去的問題。這麼雞歪的內部簡報，最後還能活著走出會議室，感謝同事的寬容。&lt;&#x2F;p&gt;
&lt;p&gt;最後團隊內部對於公司的營收來源，聚焦於兩個方向，內部專案商品化與接案，同時也確定了內部專案商品化需要人力與時間，卻無法帶來營收，因此需要以外部接案的營收來維持收入，並同時由我—開發力最弱的人，來從事接案的接觸，也就是業務，至此，我的業務人生與衝突正式展開。&lt;&#x2F;p&gt;
&lt;p&gt;下一集：〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;being-started-2&#x2F;&quot;&gt;被創業之路（二）商業模式&lt;&#x2F;a&gt;〉&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>你的資料庫支援時間資料型別嗎？</title>
        <published>2021-09-12T00:00:00+00:00</published>
        <updated>2021-09-12T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/does-your-database-system-support-date-time-data-type/"/>
        <id>https://editor.leonh.space/2021/does-your-database-system-support-date-time-data-type/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/does-your-database-system-support-date-time-data-type/">&lt;p&gt;「資料庫支援時間資料型別嗎？」這好像不應該是個問題，但是最近在普查資料庫系統時，才意識到這的確是該被考慮的問題之一，特別是在 NoSQL 系的資料庫，受限於 JSON 原生沒有日期與時間型態的先天限制，各家 NoSQL 資料庫系統對時間與日期的處理方式也因此花招百出。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wei-shen-mo-ri-qi-shi-jian-yao-yong-te-you-de-zi-liao-xing-tai&quot;&gt;為什麼日期、時間要用特有的資料型態？&lt;&#x2F;h2&gt;
&lt;p&gt;資料庫做為儲存巨量資料的系統，提供了一些通用或特有的查詢語言讓我們與其溝通，像是 SQL 語言就是通用於 FireBird、PostreSQL、MariaDB、SQLite、MySQL 的查詢語言，儘管他們之間有著若干的語法差異，但對一般查詢工作上是沒有太大影響的，資料庫系統在做出查詢之後會回覆查詢結果。&lt;&#x2F;p&gt;
&lt;p&gt;對於一個有支援日期與時間型態的資料庫系統，我們可以直接在 SQL 寫出針對時間的查詢條件，例如一個存有數十萬筆交易紀錄的表中調出 3&#x2F;2 號下午兩點到三點的交易，這樣的時間區間會成為資料庫系統的查詢條件之一，也因此它只會回覆符合查詢區間內的那些交易紀錄，反之若是資料庫系統不支援日期、時間型態，也就是說像這樣的時間紀錄 &lt;code&gt;2021-03-02T14:13:14.567Z&lt;&#x2F;code&gt; 資料庫系統看不懂，只能當它是一組無意義的字串，那麼前面的例子就只能改成讓資料庫系統回覆全部數十萬筆的資料，再用自己寫的程式去把這巨量資料做二次加工篩選挑出我們要的那一小時交易紀錄，這樣的機制顯然是不現實的，因此資料庫系統在設計上必然得支援時間、日期型態，或者是必須提供其它的方式讓我們可以針對時間做出查詢。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;na-xie-bu-zhi-yuan-shi-jian-zi-liao-xing-tai-de-zi-liao-ku-xi-tong&quot;&gt;那些不支援時間資料型態的資料庫系統&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;sqlite&quot;&gt;SQLite&lt;&#x2F;h3&gt;
&lt;p&gt;SQL 系的資料庫系統大多數都支援日期與時間資料型態，唯一的例外是 SQLite，SQLite 並不在欄位層級支援時間、日期資料型態，時間或日期在表格內是以字串型態被儲存，SQLite 是以時間或日期相關函式的方式讓我們對查詢下出時間條件，例如下面查詢自 2014 某刻至今的秒數：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;sql&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;SELECT&lt;&#x2F;span&gt;&lt;span&gt; strftime(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;%s&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;now&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;) &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;-&lt;&#x2F;span&gt;&lt;span&gt; strftime(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;%s&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;2004-01-01 02:34:56&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;deta-base&quot;&gt;Deta Base&lt;&#x2F;h3&gt;
&lt;p&gt;Deta Base 是個雲端的 NoSQL 服務，之前曾經在〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;doublesllash.com&#x2F;blog&#x2F;posts&#x2F;2022&#x2F;serverless&quot;&gt;各家 Serverless 短評&lt;&#x2F;a&gt;〉介紹過 Deta 的另一個 serverless 服務 Deta Micros。&lt;&#x2F;p&gt;
&lt;p&gt;Deta Base 有提供 Python、JavaScript 的套件以及 REST API 的方式做查詢，但不論是何種方式都必須把程式語言的時間或日期物件轉換成字串包進 JSON 內，才能打進 Deta Base，Deta Base 本身也不認得時間，不像 SQLite 還有提供函式可以做處理，而 Deta 建議的處理方式是把時間轉換成 Unix 時間，以數字儲存，也因此可以做出加減運算，相當 geek：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;does-your-database-system-support-date-time-data-type&#x2F;1374536652530589696.png&quot; alt=&quot;Deta do not have native date &#x2F; time types.&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;一段典型的 Unix 時間會是這樣：&lt;code&gt;1616670631&lt;&#x2F;code&gt;，而因為要做運算 Deta Base 要我們用整數型態去存 Unix 時間，但 Deta Base 並未告知他們的整數是 int32 或 int64，有可能有 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;2038%E5%B9%B4%E9%97%AE%E9%A2%98&quot;&gt;2038 年問題&lt;&#x2F;a&gt;的風險。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;backendless-database&quot;&gt;Backendless Database&lt;&#x2F;h3&gt;
&lt;p&gt;Backendless 顧名思義是提供一系列 serverless 後端的服務，有點像 Firebase，近來 Backendless 也開始擴展服務項目往 codeless 應用開發領域發展，Backendless Database 則是它們家的資料庫服務。&lt;&#x2F;p&gt;
&lt;p&gt;Backendless 提供的套件有 Flutter、Swift、Android、Java、.NET、JavaScript，也有提供 REST API 接口，Backendless Database 具有 NoSQL 的特性 schema-less，也就是說不需要先行定義表格（在 NoSQL 的世界通常稱為 collection）內的欄位與型態，每次丟到同一個表的欄位也可以長不一樣，在 Backendless Database 裡，最終表格的欄位會是所有紀錄的欄位的聯集，例如第一次丟的 JSON 是長這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;json&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;title&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Saving Single Object&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;link_url&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;https:&#x2F;&#x2F;backendless.com&#x2F;docs&#x2F;rest&#x2F;data_single_object_create.html&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;score&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 100&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;timestamp2&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;05&#x2F;20&#x2F;2016 03:58:23&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;而第二次你對同一個表格丟的 JSON 卻長這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;json&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;title&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Client-side Setup&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;link_url&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;https:&#x2F;&#x2F;backendless.com&#x2F;docs&#x2F;js&#x2F;setup.html&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;rank&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 100&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;timestamp2&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;05&#x2F;20&#x2F;2016 03:58:23&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;那麼這張表聯集完的欄位是長這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;json&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;title&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Client-side Setup&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;link_url&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;https:&#x2F;&#x2F;backendless.com&#x2F;docs&#x2F;js&#x2F;setup.html&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;rank&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 100&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;score&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; null&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;timestamp2&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;05&#x2F;20&#x2F;2016 03:58:23&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;當然實際上不太可能出現上面 &lt;code&gt;score&lt;&#x2F;code&gt;、&lt;code&gt;rank&lt;&#x2F;code&gt; 傻傻分不清的離譜狀況。&lt;&#x2F;p&gt;
&lt;iframe src=&quot;https:&#x2F;&#x2F;www.youtube-nocookie.com&#x2F;embed&#x2F;7BrjWa6apLU&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;Schema-less 特性帶來的是省去 &lt;code&gt;ALTER TABLE&lt;&#x2F;code&gt; 的操作，不再需要 ORM 的 migration 機制。&lt;&#x2F;p&gt;
&lt;p&gt;但如同上面的例子 REST API 接受的內容格式是 JSON，JSON 的原始定義內是沒有日期與時間的資料型態的，必須轉為字串，因此 Backendless 那端收到的也視為字串處理。&lt;&#x2F;p&gt;
&lt;p&gt;想要定義正確的時間型態，得在 Backendless 的 schema editor 先定義好欄位的型態：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;does-your-database-system-support-date-time-data-type&#x2F;Backendless_schema_editor.png&quot; alt=&quot;Backednless schema editor&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;雖然前面說過 NoSQL 的 schema-less 特性，但其實還是可以定義欄位型態的，只是這動作變成非必要的，但在 Backendless Database 我們為了正確的指定欄位的時間型態，卻又必須「回歸正道」的設定欄位型態，這樣做等於放棄了 schema-less 的特性，並且 Backendless 也沒有提供透過 API 定義欄位型態的方式，最終這在 Backendless 變成一種比 migration 還不方便的機制，爾等必須手動去做每一次相當於 &lt;code&gt;ALTER TABLE&lt;&#x2F;code&gt; 的操作。&lt;&#x2F;p&gt;
&lt;p&gt;但即便這樣一波帥氣的手動操作完了，丟上去的 DATETIME 資料，再透過 REST API 傳回來，又變成 Unix 時間…：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;json&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;score&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 100.0&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;rank&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; null&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;created&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1616721085418&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;link_url&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;https:&#x2F;&#x2F;backendless.com&#x2F;docs&#x2F;rest&#x2F;data_single_object_create.html&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;ownerId&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; null&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;title&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Saving Single Object&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;objectId&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;2D0BBC30-5D01-4015-8CA3-2B4CC3BF36A1&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;timestamp&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1463716703000&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意到不論是我自己加的欄位 &lt;code&gt;timestamp&lt;&#x2F;code&gt; 或 Backendless 預帶的 &lt;code&gt;created&lt;&#x2F;code&gt; 都是整數型態的 Unix 時間。&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;does-your-database-system-support-date-time-data-type&#x2F;1586095649605.jpg&quot; alt=&quot;意不意外？開不開心？&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：《家有囍事》&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;追根究底還是回到 JSON 的老問題，JSON 就是沒有時間日期的資料型態，因此只能使出各種奇技淫巧來應付日期時間的需求，特別是在廣泛使用 JSON 格式的 REST API 這樣的問題就更加突顯，如果是不用 REST API，而是用 Backendless 提供的語言套件，那或許可以直接原生支援該語言的時間物件轉換成 Backendless 的時間型態，但這也意味著語言的選擇受限，或者走 REST API 就必須多一層 workaround 的功夫。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;na-xie-rao-guo-json-bu-zhi-yuan-shi-jian-xing-tai-wen-ti-de-zi-liao-ku-xi-tong&quot;&gt;那些繞過 JSON 不支援時間型態問題的資料庫系統&lt;&#x2F;h2&gt;
&lt;p&gt;JSON 不支援時間、日期資料型態顯然是個問題，那些 NoSQL 系統也注意到了，因此有了這幾種典型解法：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;不用 JSON，改用 BSON。BSON 是 binary JSON 的意思，除了變成二進位容量更小傳輸更快外，BSON 也支援日期與時間的資料型態，採用 BSON 最有代表性的就是 NoSQL 的老大 MongoDB，並且 MongoDB 的 API 套件也封裝的很優雅，程式語言原生的日期型態會被 MongoDB 套件自動地轉換成 BSON 的日期型態，這一切都是隱式發生的。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;擴充 JSON，讓 JSON 支援時間型態，RethinkDB 與 Fauna 就是這麼做的，他們的語言套件也類似 MongoDB，能幫我們自動的轉換日期、時間型態。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;用 Unix 時間，其他都不管了，如同前面提到的，這也算是一招。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;像 SQLite 那樣用函式處理日期字串，除了 SQLite 外，還有 Couchbase 也是用這招。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;那麼上面這四招，哪招用起來最舒服呢？個人是愛前兩種，他們都提供封裝優雅的語言套件，自動的幫我們做轉換，讓我們不用煩惱底層的資料型態問題。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;tong-chang-jia-ying-graphsql&quot;&gt;同場加映 GraphSQL&lt;&#x2F;h2&gt;
&lt;p&gt;前面都在講 JSON 的壞話，那 GraphQL 又是如何對待時間日期的呢？&lt;&#x2F;p&gt;
&lt;p&gt;GraphQL 原生的資料型態也沒有日期時間，它比較像是上面擴充 JSON 的做法，GraphQL 是可以自行定義新的資料型態的，或者是一樣用整數存 Unix 時間，看起來和 JSON 差不多。&lt;&#x2F;p&gt;
&lt;p&gt;那麼為何 JSON 和 GraphQL 都不把時間日期劃入原生型態呢？我也很想知道，知道的請告訴我。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;can-kao-zi-liao&quot;&gt;參考資料&lt;&#x2F;h2&gt;
&lt;p&gt;內文有帶到 shema-less 的特性，在此附上兩篇 MongoDB schema 設計的好文共賞：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;blog.toright.com&#x2F;posts&#x2F;4483&#x2F;mongodb-schema-%E8%A8%AD%E8%A8%88%E6%8C%87%E5%8D%97.html&quot;&gt;MongoDB Schema 設計指南&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;blog.toright.com&#x2F;posts&#x2F;4537&#x2F;mongodb-schema-%E8%A8%AD%E8%A8%88%E6%8C%87%E5%8D%97-part-ii-%E5%8F%8D%E6%AD%A3%E8%A6%8F%E5%8C%96%E7%9A%84%E5%A8%81%E5%8A%9B.html&quot;&gt;MongoDB Schema 設計指南 (Part Ⅱ) - 反正規化的威力&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>擴充 AWS 主機硬碟空間</title>
        <published>2021-09-11T00:00:00+00:00</published>
        <updated>2021-09-11T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/expand-aws-ebs-space/"/>
        <id>https://editor.leonh.space/2021/expand-aws-ebs-space/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/expand-aws-ebs-space/">&lt;p&gt;過往我曾經在〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;doublesllash.com&#x2F;blog&#x2F;posts&#x2F;2022&#x2F;serverless&quot;&gt;各家 Serverless 短評&lt;&#x2F;a&gt;〉談到主機雲端化帶給我們的便利，剛好最近碰到要擴充 AWS 上某台機器的硬碟，很適合讓人更具體地感受到雲端化對 IT 維運上的優勢，要擴充硬碟只需要進入 AWS 主控台調整一下設定即可，當然自建虛擬機也可以做到同樣的便利性，但自建虛擬機的 host 還是會有硬碟被瓜分光的一天，你各位終究還是要自己去機櫃抽插硬碟的。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;想要擴充硬碟空間，第一步是到 AWS 的 EBS 頁面，對目標硬碟做修改設定，在這邊我們是把原本一個 8G 的卷加成 16G：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;expand-aws-ebs-space&#x2F;ebs-modify-volume.png&quot; alt=&quot;AWS EBS 修改卷&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;卷修改完了，但卷內的分割區還是原本的 8G，所以我們登入 shell 去修改分割區。&lt;&#x2F;p&gt;
&lt;p&gt;在修改前，先確認目前的分割區配置：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; lsblk&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;NAME      MAJ:MIN RM  SIZE  RO  TYPE  MOUNTPOINT&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;xvda      202:0   0   16G   0   disk&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  -xvda1  202:1   0    8G   0   part  &#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到 disk 是 16G，但分割區 xvda1 還是只有原本配置的 8G，而剩下的 8G 目前是真空未配置的狀態。&lt;&#x2F;p&gt;
&lt;p&gt;用指令讓分割區 xvda1 吃滿未配置的空間：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo growpart &#x2F;dev&#x2F;xvda&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;再次用 &lt;code&gt;lsblk&lt;&#x2F;code&gt; 驗證結果：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;NAME      MAJ:MIN RM  SIZE  RO  TYPE  MOUNTPOINT&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;xvda      202:0   0   16G   0   disk&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  -xvda1  202:1   0   16G   0   part  &#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;確認分割區已經擴充，但…檔案系統還沒，看 xvda1 的 ext4 還是只擁有約 8G 的空間：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; df&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --human-readable --print-type&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Filesystem  Type  Size  Used  Avail Use%  Mounted on&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&#x2F;dev&#x2F;xvda1  ext4  7.7G  7.0G  734M  91%   &#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;所以還要再一步修改檔案系統：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo resize2fs &#x2F;dev&#x2F;xvda1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;再次確認：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Filesystem  Type  Size  Used  Avail Use%  Mounted on&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&#x2F;dev&#x2F;xvda1  ext4  16G   7.0G  8.5G  46%   &#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;確認變為 16G，至此大功告成。&lt;&#x2F;p&gt;
&lt;p&gt;注意到了嗎，我們擴充硬碟空間的整個程序中，是沒有重新開機的，也不需要什麼高深的熱抽換技術，就兩三個一望即知的指令，隨便拉一個三十年前用過 DOS 的小朋友都會做。&lt;&#x2F;p&gt;
&lt;p&gt;本文是 AWS 文件〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.aws.amazon.com&#x2F;zh_tw&#x2F;AWSEC2&#x2F;latest&#x2F;UserGuide&#x2F;recognize-expanded-volume-linux.html&quot;&gt;調整磁碟區大小後擴展 Linux 檔案系統&lt;&#x2F;a&gt;〉的 human readable 版。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>如何複製 EC2 到另一個 AWS 帳號</title>
        <published>2021-09-08T00:00:00+00:00</published>
        <updated>2021-09-08T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/how-to-transfer-an-ec2-snapshot-to-another-aws-account/"/>
        <id>https://editor.leonh.space/2021/how-to-transfer-an-ec2-snapshot-to-another-aws-account/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/how-to-transfer-an-ec2-snapshot-to-another-aws-account/">&lt;p&gt;來自某個任務的需求，必須把 EC2 從 A 帳號複製到 B 帳號，這篇是複製過程的筆記，比較適合的讀者是已經對 AWS 的基礎操作有經驗的人士，畢竟 AWS 的服務真的太多，而且每間公司配置的方式各有差異，不太可能涵蓋到全部的情況，因此這裡談的是最簡化的情況，具體操作還是要根據自家的 AWS 配置做變通。&lt;&#x2F;p&gt;
&lt;p&gt;另外要注意的是本文是「複製」，並不是熱移轉，並且是從 A 帳號複製到 B 帳號，而 AWS 的 Elastic IP 並無法做到跨帳號的轉移，因此 IP 必定會變動的。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ji-chu-zhi-shi&quot;&gt;基礎知識&lt;&#x2F;h2&gt;
&lt;dl&gt;
    &lt;dt&gt;AWS&lt;br&gt;Amazon Web Services&lt;&#x2F;dt&gt;
    &lt;dd&gt;
        亞馬遜雲端服務，全球最大的雲端服務商。
    &lt;&#x2F;dd&gt;
    &lt;dt&gt;EC2&lt;br&gt;Elastic Compute Cloud&lt;&#x2F;dt&gt;
    &lt;dd&gt;
        AWS 最主要的雲端主機租用服務。
    &lt;&#x2F;dd&gt;
    &lt;dt&gt;EBS&lt;br&gt;Elastic Block Storage&lt;&#x2F;dt&gt;
    &lt;dd&gt;
        搭配 EC2 使用的儲存體服務。
    &lt;&#x2F;dd&gt;
    &lt;dt&gt;EBS Volume&lt;&#x2F;dt&gt;
    &lt;dd&gt;卷，可以簡單的理解為硬碟。&lt;&#x2F;dd&gt;
    &lt;dt&gt;EBS Snapshot&lt;&#x2F;dt&gt;
    &lt;dd&gt;卷的快照，一般是做為備份用。&lt;&#x2F;dd&gt;
    &lt;dt&gt;AMI&lt;br&gt;Amazon Machine Image&lt;&#x2F;dt&gt;
    &lt;dd&gt;EC2 開立機台時的映像檔，EC2 是不用自己裝系統的，都是用現有的映像檔生成機台，映像檔可以用 AWS 提供的，也可以拿自己的快照再產生自有的映像檔。&lt;&#x2F;dd&gt;
    &lt;dt&gt;Elastic IP&lt;&#x2F;dt&gt;
    &lt;dd&gt;靜態 IP，在 EC2 開立機台會配一個公開 IP，但每次重開機都會變動，如果要靜態 IP 需要在 Elastic IP 頁面申請，並附掛到一台 EC2 身上。&lt;&#x2F;dd&gt;
    &lt;dt&gt;AWS Region&lt;&#x2F;dt&gt;
    &lt;dd&gt;AWS 全球各地的機房，不同區域提供的服務可能略有差異，不同區域間開立的資源通常也不能直接調動，不同區域間的費率也略有差異。&lt;&#x2F;dd&gt;
&lt;&#x2F;dl&gt;
&lt;h2 id=&quot;a-zhang-hao-liu-cheng&quot;&gt;A 帳號流程&lt;&#x2F;h2&gt;
&lt;p&gt;在 A 帳號，先把 EC2 的 EBS 做個快照，修改快照權限，填入 B 帳號 ID：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide class&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;how-to-transfer-an-ec2-snapshot-to-another-aws-account&#x2F;__EC2_Management_Console_2021-03-24_17-49-31.png&quot; alt=&quot;AWS EBS 快照權限&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;權限對話框的「公有／私有」請選「&lt;strong&gt;私有&lt;&#x2F;strong&gt;」，若是「公有」會公開分享到公共區，其他的 AWS 用戶也會看到喔。&lt;&#x2F;p&gt;
&lt;p&gt;帳號的 ID 可以在 AWS 控制台右上角的下拉選單內找到：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide class&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;how-to-transfer-an-ec2-snapshot-to-another-aws-account&#x2F;AWS__2021-03-24_17-46-44.png&quot; alt=&quot;AWS 帳號 ID&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;A 帳號把快照分享給 B 帳號後，A 帳號的部分就完工了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;b-zhang-hao-liu-cheng&quot;&gt;B 帳號流程&lt;&#x2F;h2&gt;
&lt;p&gt;進入 B 帳號的快照區，並切換到「私有快照」，應該就可以看到剛剛 A 帳號分享出來的快照：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide class&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;how-to-transfer-an-ec2-snapshot-to-another-aws-account&#x2F;__EC2_Management_Console_2021-03-24_17-51-23.png&quot; alt=&quot;AWS EBS 快照&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;接下來要用這份快照開新機器，但這分快照是來自 A 的分享，並無法用它做 AMI，因此先用這份快照還原一份卷（volume），再用卷作一次快照，這樣做出來的新快照就會是屬於 B 帳號的。&lt;&#x2F;p&gt;
&lt;p&gt;有了新快照就可以拿它做 AMI：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide class&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;how-to-transfer-an-ec2-snapshot-to-another-aws-account&#x2F;__EC2_Management_Console_2021-03-24_18-07-39.png&quot; alt=&quot;AWS 根據 EBS 快照創建映像&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;創建映像對話框內的選項應該都不用動，只要填入名稱就可以產出一份 AMI：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide class&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;how-to-transfer-an-ec2-snapshot-to-another-aws-account&#x2F;AMI__EC2_Management_Console_2021-03-24_18-12-17.png&quot; alt=&quot;AWS AMI&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;有了這份 AMI，就可以用它來開新的 EC2 機器了！&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zhu-yi-shi-xiang&quot;&gt;注意事項&lt;&#x2F;h2&gt;
&lt;p&gt;上面的流程是同區域的操作，如果要做跨區複製，可能要額外的步驟。&lt;&#x2F;p&gt;
&lt;script src=&quot;https:&#x2F;&#x2F;cdnjs.cloudflare.com&#x2F;ajax&#x2F;libs&#x2F;viewerjs&#x2F;1.9.0&#x2F;viewer.min.js&quot; integrity=&quot;sha512-0goo56vbVLOJt9J6TMouBm2uE+iPssyO+70sdrT+J5Xbb5LsdYs31Mvj4+LntfPuV+VlK0jcvcinWQG5Hs3pOg==&quot; crossorigin=&quot;anonymous&quot;&gt;&lt;&#x2F;script&gt;
&lt;link rel=&quot;stylesheet&quot; href=&quot;https:&#x2F;&#x2F;cdnjs.cloudflare.com&#x2F;ajax&#x2F;libs&#x2F;viewerjs&#x2F;1.9.0&#x2F;viewer.min.css&quot; integrity=&quot;sha512-1cfqrTRQ8V1TnQsSu97+x7PoylALHKOQuwpFaa6lwe6lo5EOUmGNmX3LBq&#x2F;yxUokfGaUtWkjZJGmuXqG5THgdA==&quot; crossorigin=&quot;anonymous&quot; &#x2F;&gt;
&lt;script&gt;
    const viewer = new Viewer(document.getElementsByClassName(&#x27;article-content&#x27;)[0], {
        navbar: false,
        toolbar: false,
        rotatable: false,
    });
&lt;&#x2F;script&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>備份與還原 AWS EC2 的 EBS</title>
        <published>2021-09-07T00:00:00+00:00</published>
        <updated>2021-09-07T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/backup-ebs/"/>
        <id>https://editor.leonh.space/2021/backup-ebs/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/backup-ebs/">&lt;p&gt;先前情提要簡單介紹一下 AWS。&lt;&#x2F;p&gt;
&lt;p&gt;在 AWS 的世界，與傳統虛擬主機最大的不同，即 AWS 幾乎把主機的每個部份都拆成一個服務，有運算服務、聯網服務、儲存服務、安全服務、監控服務、容器服務、VPN 服務等等等等等，我們可以自由的把各種服務混合搭配使用，並依照各種服務的用量付費標準付錢錢給 AWS。&lt;&#x2F;p&gt;
&lt;p&gt;AWS 其中的 EC2 是一種可以被量化的雲端計算服務，這也是 EC2 的全名「Elastic Compute Cloud」的含意，而與 EC2 搭配的儲存服務是 EBS (Elastic Block Store)，我們可以把 EC2 與 EBS 簡單的對比為「電腦」與「硬碟」，與傳統電腦不同的是在 AWS 的電腦與硬碟都可以隨時被更換，更換的方法可能是人工到 AWS 網站更換或是由我們自己的程式透過 AWS SDK 去更換，這樣的特性就是 elastic 這個字眼想表達的意義：彈性。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;bei-fen-ec2-de-ebs&quot;&gt;備份 EC2 的 EBS&lt;&#x2F;h2&gt;
&lt;p&gt;除了排程內的定期備份外，在對機臺做某些重大變更時我習慣會對 EBS 另外做人工備份，這些重大變更包括 OS 大升級、自己的程式大改版之類的時候。&lt;&#x2F;p&gt;
&lt;p&gt;EBS 的備份為我們熟知的快照式備份，而 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;aws.amazon.com&#x2F;tw&#x2F;ebs&#x2F;pricing&#x2F;&quot;&gt;EBS 的快照是依容量收費&lt;&#x2F;a&gt;的，並且是&lt;strong&gt;依實際容量而非額定容量計價&lt;&#x2F;strong&gt;，所以就算開 1 TB 的硬碟，但實際只用了 1 GB，那就只會拿 1 GB 來計價。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zhao-chu-yao-bei-fen-de-ebs&quot;&gt;找出要備份的 EBS&lt;&#x2F;h3&gt;
&lt;p&gt;實際進行快照備份則相當簡單，一般都是從先到 EC2 主控台確定要備份的那台機器，在機器明細頁那裡找到兩個項目：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Root device&lt;&#x2F;li&gt;
&lt;li&gt;Block devices&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;Root device 指的是掛在 &#x2F;dev&#x2F;sda1 的開機碟；Block devices 指的是全部的硬碟。&lt;&#x2F;p&gt;
&lt;p&gt;在典型的情況下應該 EC2 就只會有一顆硬碟，並且也一定是開機碟，所以 Root device 與 Block devices 應該都只會是 &#x2F;dev&#x2F;sda1。&lt;&#x2F;p&gt;
&lt;p&gt;這顆被掛在 &#x2F;dev&#x2F;sda1 的硬碟有相對應的 EBS ID，透過這組 EBS ID 可以直接連結到 EBS 的主控台頁面下的這顆（卷）EBS。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zuo-ebs-kuai-zhao&quot;&gt;做 EBS 快照&lt;&#x2F;h3&gt;
&lt;p&gt;進入 EBS 主控台的這卷 EBS 後，按下畫面上的 &lt;strong&gt;操作&lt;&#x2F;strong&gt; &lt;strong&gt;創建快照&lt;&#x2F;strong&gt; 就開始快照，並且會再幫我們跳轉到快照的主控台及這筆新快照的明細頁，依照容量的不同會花一點時間建立快照，等到快照的狀態變為綠燈「completed」後，即表示已經完成。&lt;&#x2F;p&gt;
&lt;p&gt;EBS 快照是支援熱快照的，也就是建立快照時 EC2 是不用關機的，但習慣上能關機的我還是會關機，不能關機的則盡量降低磁碟的存取量。&lt;&#x2F;p&gt;
&lt;p&gt;在等待快照建立完成的期間，可以&lt;strong&gt;在快照的 Name 欄位填一個容易往後識別的名字，不要太相信自己的記憶力&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;cong-kuai-zhao-huan-yuan&quot;&gt;從快照還原&lt;&#x2F;h2&gt;
&lt;p&gt;情境是在 EC2 的某台機器被玩壞了，必須用之前的快照還原。&lt;&#x2F;p&gt;
&lt;p&gt;首先把那台 EC2 上面掛載的壞掉的那卷 EBS 卸下，先把那台被玩壞的 EC2 停機。&lt;&#x2F;p&gt;
&lt;p&gt;接著在那台 EC2 的明細找到 Root device 找到壞的 EBS，進入那台 EBS 的明細頁，按 &lt;strong&gt;操作&lt;&#x2F;strong&gt; &lt;strong&gt;斷開卷&lt;&#x2F;strong&gt;，此時這個 EBS 卷會與 EC2 脫離，但因為還是有佔用到 AWS 的空間，所以還是會持續計費，因此&lt;strong&gt;在成功還原之後記得把確定無用的、壞的 EBS 刪除掉避免漏財&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;然後到 EBS 快照的主控台，確定要還原的快照，按 &lt;strong&gt;操作&lt;&#x2F;strong&gt; &lt;strong&gt;創建卷&lt;&#x2F;strong&gt;，會建立一個新的 EBS 卷，對那個新的 EBS 卷按 &lt;strong&gt;操作&lt;&#x2F;strong&gt; &lt;strong&gt;連接卷&lt;&#x2F;strong&gt;，並填入要掛上的 EC2 機器，以及填入掛載點，&lt;strong&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.aws.amazon.com&#x2F;zh_tw&#x2F;AWSEC2&#x2F;latest&#x2F;UserGuide&#x2F;device_naming.html&quot;&gt;如果是要做為 Root device 開機碟的話，務必填入 &#x2F;dev&#x2F;sda1 作為掛載點才可正常開機&lt;&#x2F;a&gt;&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;最後記得把確定無用的 EBS 卷或快照刪除。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>在 AWS EC2 架設 pure-ftpd 服務</title>
        <published>2021-09-05T00:00:00+00:00</published>
        <updated>2021-09-05T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/pure-ftpd-aws-ec2/"/>
        <id>https://editor.leonh.space/2021/pure-ftpd-aws-ec2/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/pure-ftpd-aws-ec2/">&lt;p&gt;有時候還是需要 FTP 這種老派的服務，總之就是要給它架起來！&lt;&#x2F;p&gt;
&lt;p&gt;搭配的系統是 Ubuntu 18.04 LTS，提供 FTP 服務的是 pure-ftpd，當初在調查 FTP daemon 的時候比較常看到別人是選用 vsftpd，可是設定好像有點複雜，最後找到 pure-ftpd 感覺比較簡單就挑它了！&lt;&#x2F;p&gt;
&lt;p&gt;本文單純就是架一台機台起來然後讓它提供 FTP 服務，不會涉及到 AWS S3。另外本文假設讀者已經懂得基本的 Linux 操作及 AWS 操作，故也不會講怎麼開 AWS &#x2F; EC2 &#x2F; IP &#x2F; SSH &#x2F; Cloud9 等主題。&lt;&#x2F;p&gt;
&lt;p&gt;在架設前需要了解一些 FTP 的基礎知識，特別是被動式 FTP（Passive FTP）的基本知識。由於 EC2 只認得自己的 Private IP，不認得自己的 Public IP，所以須強制指定它送出的 IP 設成 AWS Public IP，不然客戶端會無法正常使用。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;an-zhuang-pure-ftpd&quot;&gt;安裝 pure-ftpd&lt;&#x2F;h2&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;ubuntu$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo apt update&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;ubuntu$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo apt install pure-ftpd&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;que-ren-pure-ftpd-zheng-chang-yun-zuo&quot;&gt;確認 pure-ftpd 正常運作&lt;&#x2F;h2&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;ubuntu$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; service pure-ftpd status&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;應該會看到「Active: active (running)」的字樣，表示正常運作中。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;she-ding-pure-ftpd&quot;&gt;設定 pure-ftpd&lt;&#x2F;h2&gt;
&lt;p&gt;pure-ftpd 的設定都放在 &#x2F;etc&#x2F;pure-ftpd&#x2F;conf&#x2F; 內，每個檔名代表一個設定項目，檔案的內容為該項目的設定值，通常為 yes 或 no 或 IP 或數字。完整的設定在 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;download.pureftpd.org&#x2F;pub&#x2F;pure-ftpd&#x2F;doc&#x2F;README&quot;&gt;pure-ftpd 的文件&lt;&#x2F;a&gt;內，你可以選擇花五個小時把它看完或者直接跟我下面的設定，有其它需求再問 Google 或 StackOverflow。&lt;&#x2F;p&gt;
&lt;div class=&quot;wide&quot;&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;檔名&lt;&#x2F;th&gt;&lt;th&gt;內容&lt;&#x2F;th&gt;&lt;th&gt;說明&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;NoAnonymous&lt;&#x2F;td&gt;&lt;td&gt;yes&lt;&#x2F;td&gt;&lt;td&gt;禁止匿名登入&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;UnixAuthentication&lt;&#x2F;td&gt;&lt;td&gt;yes&lt;&#x2F;td&gt;&lt;td&gt;直接用系統帳號作為 FTP 認證帳密&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;DisplayDotFiles&lt;&#x2F;td&gt;&lt;td&gt;no&lt;&#x2F;td&gt;&lt;td&gt;不顯示隱藏檔&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;ForcePassiveIP&lt;&#x2F;td&gt;&lt;td&gt;AWS Elastic IP&lt;&#x2F;td&gt;&lt;td&gt;強迫對客戶端回覆 AWS Public IP&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;PassivePortRange&lt;&#x2F;td&gt;&lt;td&gt;30000 - 5000&lt;&#x2F;td&gt;&lt;td&gt;被動模式可使用的 port 範圍&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;ChrootEveryone&lt;&#x2F;td&gt;&lt;td&gt;yes&lt;&#x2F;td&gt;&lt;td&gt;只允許 FTP 用戶存取自己的家目錄&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;TrustedGID&lt;&#x2F;td&gt;&lt;td&gt;27&lt;&#x2F;td&gt;&lt;td&gt;開放讓 GID = 27 的成員讀取所有目錄（大權限）&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;&#x2F;div&gt;
&lt;h2 id=&quot;she-ding-aws-an-quan-zu-fang-huo-qiang&quot;&gt;設定 AWS 安全組（防火牆）&lt;&#x2F;h2&gt;
&lt;p&gt;有幾個 port 必須打開才能夠提供服務，標準的 FTP 通訊 port 21 及前面指定的 port 30000 – 50000，以上這兩組 port 要到 AWS 安全組那邊去開放入站。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jian-ftp-zhang-hu-yu-qun-zu&quot;&gt;建 FTP 帳戶與群組&lt;&#x2F;h2&gt;
&lt;p&gt;在建立之前，我們的計畫是不讓這些 FTP 用戶從 login 登入，只讓他們從 FTP 登入，所以要把 nologin 加入系統的 shell 清單內，並指定這些用戶使用 nologin。&lt;&#x2F;p&gt;
&lt;p&gt;系統的 shells 檔案在 &#x2F;etc&#x2F;shells，編輯它並在最後一行加入 &#x2F;usr&#x2F;sbin&#x2F;nologin。&lt;&#x2F;p&gt;
&lt;p&gt;開始建帳戶與群組：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;ubuntu$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo groupadd ftp&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;ubuntu$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo useradd ftp001&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -g&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ftp&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;ubuntu$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo passwd ftp001&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Enter&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; new UNIX password: ftp001password&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;幫 ftp001 建家目錄並設定權限：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;ubuntu$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo mkdir &#x2F;home&#x2F;ftp001&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;ubuntu$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo chown&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -R&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ftp001:ftp &#x2F;home&#x2F;ftp001&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;幫 ftp001 設 nologin：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;ubuntu$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo chsh ftp001&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Login&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; Shell&lt;&#x2F;span&gt;&lt;span&gt; [&#x2F;bin&#x2F;bash]: &#x2F;usr&#x2F;sbin&#x2F;nologin&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這樣 ftp001 就設定好了，本節幾個步驟可以寫成 shell script 方便日後使用。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Web 處理檔案那些事－上傳篇</title>
        <published>2021-09-02T00:00:00+00:00</published>
        <updated>2021-09-02T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/handling-files-in-frontend/"/>
        <id>https://editor.leonh.space/2021/handling-files-in-frontend/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/handling-files-in-frontend/">&lt;p&gt;一個 web app 處理檔案的方式，傳統的做法是把檔案 post 到後端，經過一番處理後再吐回來，以現代的角度看，這種原始的方式最大的問題是 UX 不佳，用戶得經歷上傳、等待處理、網頁更新、下載這幾個過程，幸虧現代化的瀏覽器和 JavaScript 的發展大爆棚，以往要在後端處理檔案的開銷也可以在前端處理，更惡質一點還能拿用戶的瀏覽器來挖礦。&lt;&#x2F;p&gt;
&lt;p&gt;以 PDF 為例，以往我們都要靠 Adobe Acrobat Reader 來開啟，但現在我們都只用瀏覽器開 PDF，不管是 Mozilla 的 PDF.js 或是 Google 的 PDFium，儘管他們的功能不比 Acrobat Reader 完整，但對於最普通的閱讀 PDF 的需求已是足夠滿足大部分人的，只剩下那些需要筆記、標注功能的少部分用戶還會想用 Acrobat Reader。&lt;&#x2F;p&gt;
&lt;p&gt;這裡我們談談最近對 web app 處理檔案上一些工程和設計面的觀點。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;dang-an-shang-chuan-yuan-jian&quot;&gt;檔案上傳元件&lt;&#x2F;h2&gt;
&lt;p&gt;在上傳元件的 UI 方面，原生的 HTML file input 元件在現代的眼光看來過於陽春：&lt;&#x2F;p&gt;
&lt;div style=&quot;background: hsla(0, 0%, 96%, 1.0); padding: 1rem; margin-bottom: 1rem;&quot;&gt;
    &lt;input type=&quot;file&quot; style=&quot;width: 100%;&quot;&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;在這樣陽春的背後，卻又親切度不足，沒有使用提示、進度提示、錯誤提示等所有能幫助降低使用者疑惑的元素，於是重視 UX 的設計師們看不下去了，腦洞大開的發表各式重新改造過的上傳元件：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;handling-files-in-frontend&#x2F;af8971d942f4ce6ec45122b24c1c610a.webp&quot; alt=&quot;File Upload- Daily UI 031&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：&lt;a href=&quot;https:&#x2F;&#x2F;dribbble.com&#x2F;shots&#x2F;16195595-File-Upload-Daily-UI-031&#x2F;attachments&#x2F;8055081?mode=media&quot;&gt;Aarushi Mishra&lt;&#x2F;a&gt;&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;這些設計的共同特徵是會有個拖放區，這個拖放區除了接受空投檔案以外，點擊也會觸發開啟檔案的對話框。&lt;&#x2F;p&gt;
&lt;p&gt;像這樣拖放型的 UI，在桌面端早已實現已久，幾乎我們想得到的任何一個 app 都有這樣的特性，特別是那些小工具型的 app，他們並不需要複雜的 UI，僅需要快速簡單直覺的完成特定的任務，這種 app 的典型是那些轉檔工具，例如 macOS 上的壓縮工具 Keka：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;handling-files-in-frontend&#x2F;keka.png&quot; alt=&quot;Keka&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;相信任何一位有拖拉過檔案的初學者都可以對 Keka 的介面輕鬆上手。&lt;&#x2F;p&gt;
&lt;p&gt;若是進一步延伸觀察到日常生活中，相較於傳統遙控器，客廳內的 Apple TV 遙控器也是橫空出世就大刀砍了那些使用率不到兩成的按鈕（是說，那麼多按鈕到底是要考驗誰呢？），像這些經過簡化的設計背後的哲學可以用兩個類似的概念解釋——「&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E6%9B%B4%E7%B3%9F%E5%B0%B1%E6%98%AF%E6%9B%B4%E5%A5%BD&quot;&gt;Worse is better&lt;&#x2F;a&gt;」＆「&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;KISS%E5%8E%9F%E5%88%99&quot;&gt;KISS&lt;&#x2F;a&gt;」。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;tuo-fang-yuan-jian&quot;&gt;拖放元件&lt;&#x2F;h3&gt;
&lt;p&gt;回頭說拖放上傳。像這種拖放型的檔案上傳元件，追求事半功倍的我輩開發人，當然是找人家封裝好的元件來用，例如 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;uppy.io&#x2F;&quot;&gt;Uppy&lt;&#x2F;a&gt; 和 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;pqina.nl&#x2F;filepond&#x2F;&quot;&gt;FilePond&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;Uppy 範例：&lt;&#x2F;p&gt;
&lt;link rel=&quot;stylesheet&quot; href=&quot;https:&#x2F;&#x2F;releases.transloadit.com&#x2F;uppy&#x2F;v2.0.1&#x2F;uppy.min.css&quot;&gt;
&lt;div class=&quot;UppyDragDrop&quot;&gt;&lt;&#x2F;div&gt;
&lt;div class=&quot;for-ProgressBar&quot;&gt;&lt;&#x2F;div&gt;
&lt;div class=&quot;uploaded-files&quot;&gt;
  &lt;p&gt;Uploaded files:&lt;&#x2F;p&gt;
  &lt;ol&gt;&lt;&#x2F;ol&gt;
&lt;&#x2F;div&gt;
&lt;!-- Load Uppy JS bundle. --&gt;
&lt;script src=&quot;https:&#x2F;&#x2F;releases.transloadit.com&#x2F;uppy&#x2F;v2.0.1&#x2F;uppy.min.js&quot;&gt;&lt;&#x2F;script&gt;
&lt;script src=&quot;https:&#x2F;&#x2F;releases.transloadit.com&#x2F;uppy&#x2F;locales&#x2F;v2.0.0&#x2F;zh_TW.min.js&quot;&gt;&lt;&#x2F;script&gt;
&lt;script&gt;
  var uppy = new Uppy.Core({
    debug: true,
    autoProceed: true,
    locale: Uppy.locales.zh_TW
  });
  uppy.use(Uppy.DragDrop, {
    target: &#x27;.UppyDragDrop&#x27;
  });
  uppy.use(Uppy.ProgressBar, {
    target: &#x27;.for-ProgressBar&#x27;,
     hideAfterFinish: false
  });
  uppy.use(Uppy.Tus, { endpoint: &#x27;https:&#x2F;&#x2F;tusd.tusdemo.net&#x2F;files&#x2F;&#x27; });
  uppy.on(&#x27;upload-success&#x27;, function (file, response) {
    var url = response.uploadURL
    var fileName = file.name

    document.querySelector(&#x27;.uploaded-files ol&#x27;).innerHTML +=
      &#x27;&lt;li&gt;&lt;a href=&quot;&#x27; + url + &#x27;&quot; target=&quot;_blank&quot;&gt;&#x27; + fileName + &#x27;&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;&#x27;
  });

  console.log(&#x27;--&gt; Uppy pre-built version with Tus, DragDrop &amp; Russian language pack has loaded&#x27;);
&lt;&#x2F;script&gt;
&lt;h3 id=&quot;zi-gan-tuo-fang-yuan-jian&quot;&gt;自幹拖放元件&lt;&#x2F;h3&gt;
&lt;p&gt;如果想要自己實現呢？那我們得先搞懂與拖放有關的事件：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;handling-files-in-frontend&#x2F;dnd-event-flow.jpg&quot; alt=&quot;Drag and Drop Event Flow&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：〈&lt;a href=&quot;https:&#x2F;&#x2F;www.xlbd.me&#x2F;posts&#x2F;2018-11-18-use-dnd-in-your-web-app.html&quot;&gt;使用 Drag and Drop 給 Web 應用提升交互體驗&lt;&#x2F;a&gt;〉&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;乍看好像很複雜，其實並不會，我們用 drop 事件處理空投檔案的邏輯部分，而 dragenter 事件、dragleave 事件、dragend 事件用於改變拖放區的外觀，讓用戶感知到元件的狀態變更，其他的各式細節請直接參考〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.xlbd.me&#x2F;posts&#x2F;2018-11-18-use-dnd-in-your-web-app.html&quot;&gt;使用 Drag and Drop 給 Web 應用提升交互體驗&lt;&#x2F;a&gt;〉，我就不多造口業了。&lt;&#x2F;p&gt;
&lt;p&gt;如果拖放區也想要有較傳統的點擊開啟檔案的行為，則是監聽 click 事件，並呼叫開啟檔案的方法。&lt;&#x2F;p&gt;
&lt;p&gt;以一個很簡單的 HTML 為例：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;div&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; id&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;dropbox-container&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;div&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; class&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;normal&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; id&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;dropbox&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;p&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; id&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;dropbox-inner&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;餵我吃檔案&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;br&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;span&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; id&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;secondary-action&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;或點我開啟檔案&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;span&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;p&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;div&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;div&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;搭配 CSS：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;css&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;#dropbox-container&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  background-color&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; #EFF6FF&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  padding&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 4&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;vmax&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  margin-top&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;rem&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  margin-bottom&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;rem&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  user-select&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; none&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;#dropbox&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  border&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 4&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;px&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; dashed #BFDBFE&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  border-radius&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 20&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;px&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  display&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; grid&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  place-content&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; center&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  aspect-ratio&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;.normal&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  background-color&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; #EFF6FF&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;.dragenter&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  background-color&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; #DBEAFE&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;#dropbox-inner&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  text-align&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; center&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  color&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; #93C5FD&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  font-size&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; x-large&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  margin&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; unset&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  line-height&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 120&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;%&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;#secondary-action&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  font-size&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; medium&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  color&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; hsla&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 0&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;%&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 60&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;%&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1.0&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;JavaScript 處理換 CSS 和檔案的邏輯：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;const&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; dropbox&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; document.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;getElementById&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;dropbox&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;function&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; setBackgroundNormal&lt;&#x2F;span&gt;&lt;span&gt;() {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  dropbox.classList.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;add&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;normal&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  dropbox.classList.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;remove&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;dragenter&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;function&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; setBackgroundDragenter&lt;&#x2F;span&gt;&lt;span&gt;() {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  dropbox.classList.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;add&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;dragenter&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  dropbox.classList.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;remove&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;normal&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;function&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; handleFiles&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;files&lt;&#x2F;span&gt;&lt;span&gt;) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  console.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;log&lt;&#x2F;span&gt;&lt;span&gt;(files);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  setBackgroundNormal&lt;&#x2F;span&gt;&lt;span&gt;()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;async function&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; click&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;e&lt;&#x2F;span&gt;&lt;span&gt;) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  const&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; arrFileHandle&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; = await&lt;&#x2F;span&gt;&lt;span&gt; window.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;showOpenFilePicker&lt;&#x2F;span&gt;&lt;span&gt;({&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    multiple:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  });&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  const&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; files&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [];&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  for&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;const&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; fileHandle&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; of&lt;&#x2F;span&gt;&lt;span&gt; arrFileHandle) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    files.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;push&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;await&lt;&#x2F;span&gt;&lt;span&gt; fileHandle.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;getFile&lt;&#x2F;span&gt;&lt;span&gt;());&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  handleFiles&lt;&#x2F;span&gt;&lt;span&gt;(files);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;function&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; dragenter&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;e&lt;&#x2F;span&gt;&lt;span&gt;) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  e.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;stopPropagation&lt;&#x2F;span&gt;&lt;span&gt;();&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  e.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;preventDefault&lt;&#x2F;span&gt;&lt;span&gt;();&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  setBackgroundDragenter&lt;&#x2F;span&gt;&lt;span&gt;();&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;function&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; dragover&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;e&lt;&#x2F;span&gt;&lt;span&gt;) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  e.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;stopPropagation&lt;&#x2F;span&gt;&lt;span&gt;();&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  e.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;preventDefault&lt;&#x2F;span&gt;&lt;span&gt;();&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  setBackgroundDragenter&lt;&#x2F;span&gt;&lt;span&gt;();&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;function&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; dragleave&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;e&lt;&#x2F;span&gt;&lt;span&gt;) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  e.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;stopPropagation&lt;&#x2F;span&gt;&lt;span&gt;();&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  e.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;preventDefault&lt;&#x2F;span&gt;&lt;span&gt;();&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  setBackgroundNormal&lt;&#x2F;span&gt;&lt;span&gt;();&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;function&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; drop&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;e&lt;&#x2F;span&gt;&lt;span&gt;) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  e.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;stopPropagation&lt;&#x2F;span&gt;&lt;span&gt;();&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  e.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;preventDefault&lt;&#x2F;span&gt;&lt;span&gt;();&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  handleFiles&lt;&#x2F;span&gt;&lt;span&gt;(e.dataTransfer.files);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;function&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; dragend&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;e&lt;&#x2F;span&gt;&lt;span&gt;) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  e.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;stopPropagation&lt;&#x2F;span&gt;&lt;span&gt;();&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  e.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;preventDefault&lt;&#x2F;span&gt;&lt;span&gt;();&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  setBackgroundNormal&lt;&#x2F;span&gt;&lt;span&gt;();&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;dropbox.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;addEventListener&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;dragenter&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, dragenter,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; false&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;dropbox.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;addEventListener&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;dragover&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, dragover,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; false&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;dropbox.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;addEventListener&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;dragleave&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, dragleave,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; false&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;dropbox.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;addEventListener&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;drop&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, drop,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; false&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;dropbox.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;addEventListener&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;dragend&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, dragend,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; false&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;dropbox.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;addEventListener&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;click&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;, click,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; false&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;div id=&quot;dropbox-container&quot;&gt;
  &lt;div class=&quot;normal&quot; id=&quot;dropbox&quot;&gt;
    &lt;p id=&quot;dropbox-inner&quot;&gt;餵我吃檔案&lt;br&gt;&lt;span id=&quot;secondary-action&quot;&gt;或點我開啟檔案&lt;&#x2F;span&gt;&lt;&#x2F;p&gt;
  &lt;&#x2F;div&gt;
&lt;&#x2F;div&gt;
&lt;style&gt;
#dropbox-container {
  background-color: #EFF6FF;
  padding: 4vmax;
  margin-top: 1rem;
  margin-bottom: 1rem;
  user-select: none;
}

#dropbox {
  border: 4px dashed #BFDBFE;
  border-radius: 20px;
  display: grid;
  place-content: center;
  aspect-ratio: 1;
}

#dropbox:hover {
  cursor: pointer;
  background-color: #DBEAFE;
}

.normal {
  background-color: #EFF6FF;
}

.dragenter {
  background-color: #DBEAFE;
}

#dropbox-inner {
  text-align: center;
  color: #93C5FD;
  font-size: x-large;
  margin: unset;
  line-height: 120%;
}

#secondary-action {
  font-size: medium;
  color: hsla(0, 0%, 60%, 1.0);
}
&lt;&#x2F;style&gt;
&lt;script&gt;
const dropbox = document.getElementById(&quot;dropbox&quot;);

function setBackgroundNormal() {
  dropbox.classList.add(&quot;normal&quot;);
  dropbox.classList.remove(&quot;dragenter&quot;);
}

function setBackgroundDragenter() {
  dropbox.classList.add(&quot;dragenter&quot;);
  dropbox.classList.remove(&quot;normal&quot;);
}

function handleFiles(files) {
  console.log(files);
  setBackgroundNormal()
}

async function click(e) {
  const arrFileHandle = await window.showOpenFilePicker({
    multiple: true,
  });

  const files = [];
  for (const fileHandle of arrFileHandle) {
    files.push(await fileHandle.getFile());
  }

  handleFiles(files);
}

function dragenter(e) {
  e.stopPropagation();
  e.preventDefault();
  setBackgroundDragenter();
}

function dragover(e) {
  e.stopPropagation();
  e.preventDefault();
  setBackgroundDragenter();
}

function dragleave(e) {
  e.stopPropagation();
  e.preventDefault();
  setBackgroundNormal();
}

function drop(e) {
  e.stopPropagation();
  e.preventDefault();
  handleFiles(e.dataTransfer.files);
}

function dragend(e) {
  e.stopPropagation();
  e.preventDefault();
  setBackgroundNormal();
}
 
dropbox.addEventListener(&quot;dragenter&quot;, dragenter, false);
dropbox.addEventListener(&quot;dragover&quot;, dragover, false);
dropbox.addEventListener(&quot;dragleave&quot;, dragleave, false);
dropbox.addEventListener(&quot;drop&quot;, drop, false);
dropbox.addEventListener(&quot;dragend&quot;, dragend, false);
dropbox.addEventListener(&quot;click&quot;, click, false);
&lt;&#x2F;script&gt;
&lt;p&gt;在 &lt;code&gt;drop()&lt;&#x2F;code&gt; 內，瀏覽器會把拖進來的檔案生成 &lt;code&gt;dataTransfer.files&lt;&#x2F;code&gt; 物件，這是一個含有空投檔案的陣列物件，而在 &lt;code&gt;click()&lt;&#x2F;code&gt;，我們呼叫的是 &lt;code&gt;window.showOpenFilePicker()&lt;&#x2F;code&gt;，它是 File System Access API 提供的函式之一，我們把 &lt;code&gt;drop()&lt;&#x2F;code&gt; 或 &lt;code&gt;click()&lt;&#x2F;code&gt; 得到的檔案統一交給 &lt;code&gt;handleFiles()&lt;&#x2F;code&gt; 印出到 console 內。&lt;&#x2F;p&gt;
&lt;p&gt;然而要注意的是， Safari 還不支援這組較新的 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;wicg.github.io&#x2F;file-system-access&#x2F;&quot;&gt;File System Access API&lt;&#x2F;a&gt;，於是得用些奇技淫巧來對付它－－在畫面上放個 file input，把這 file input 藏起來，例如用 &lt;code&gt;z-index&lt;&#x2F;code&gt; 把它放在某個元素背面，再把別的 &lt;code&gt;&amp;lt;div&amp;gt;&lt;&#x2F;code&gt; 的 click 事件傳遞給那 file input，用這種迂迴的方式達成開啟檔案的目的，相較於上面的做法，這就顯得沒那麼優雅了。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zai-tauri-yu-electron-kai-qi-dang-an&quot;&gt;在 Tauri 與 Electron 開啟檔案&lt;&#x2F;h3&gt;
&lt;p&gt;如果是把 web app 用 Tauri 或 Electron 打包成桌機 app 的情況，由於安全性考量，走網頁 API 或 file input 取得的檔案，都是不含真實路徑的，因此這兩個框架也各自提供了開啟檔案以及拖放的 API 讓我們得以取得檔案的真實路徑。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;tauri.studio&#x2F;en&#x2F;docs&#x2F;api&#x2F;js&#x2F;modules&#x2F;dialog&#x2F;&quot;&gt;以 Tauri 為例，調用的是 &lt;code&gt;open()&lt;&#x2F;code&gt; 函式&lt;&#x2F;a&gt;，&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.electronjs.org&#x2F;docs&#x2F;api&#x2F;dialog&quot;&gt;Electron 則是調用 &lt;code&gt;showOpenDialog()&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;同樣地，拖放行為也是改用框架提供的 API 來獲得檔案的真實路徑，&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;tauri.studio&#x2F;en&#x2F;docs&#x2F;api&#x2F;js&#x2F;modules&#x2F;event&quot;&gt;Tauri 讓我們用 &lt;code&gt;listen()&lt;&#x2F;code&gt; 去監控拖放事件的發生&lt;&#x2F;a&gt;，&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.electronjs.org&#x2F;docs&#x2F;tutorial&#x2F;native-file-drag-drop&quot;&gt;Electron 則是監聽一系列的拖放事件配合 &lt;code&gt;startDrag()&lt;&#x2F;code&gt;&lt;&#x2F;a&gt;，這部分的分享就點到為止，待有朝一日本人真的有親自用過再來分享。&lt;&#x2F;p&gt;
&lt;p&gt;檔案上傳了，接著是前端該怎麼處理檔案的問題。&lt;&#x2F;p&gt;
&lt;!-- ## 前端處理檔案

依照檔案類型的不同，需要不同的套件，以本文開頭提過的 PDF 為例，Firefox 有內建 PDF.js，Chrome 則是內建 PDFium 去讀取 PDF 文件，若是其他格式，那就得自行找尋了，例如：

- [id3.js](https:&#x2F;&#x2F;github.com&#x2F;43081j&#x2F;id3) 可以讀取音樂檔內的內嵌資訊
- [SheetJS](https:&#x2F;&#x2F;github.com&#x2F;SheetJS&#x2F;sheetjs) 可以編輯 CSV、LibreOffice 試算表、Excel 試算表及其他冷門格式的試算表
- [PDFMake](http:&#x2F;&#x2F;pdfmake.org&#x2F;) 可以產生出 PDF 文件。
 --&gt;
&lt;iframe src=&quot;https:&#x2F;&#x2F;www.youtube-nocookie.com&#x2F;embed&#x2F;CzyRz0qvp9o?start=44&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>升級裝有 Plesk 的 Ubuntu 16.04</title>
        <published>2021-07-15T00:00:00+00:00</published>
        <updated>2021-07-15T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/upgrade-ubuntu-16-04-with-plesk/"/>
        <id>https://editor.leonh.space/2021/upgrade-ubuntu-16-04-with-plesk/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/upgrade-ubuntu-16-04-with-plesk/">&lt;p&gt;對於伺服器系統的升級政策，有部份人主張「不作為」，理由多半是「求穩定」，他們以為不變會帶來穩定，但如果機器是對外服務的，不更新就是把自己暴露於資安風險之內，具體的證明請看 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.cvedetails.com&#x2F;&quot;&gt;CVE 網站&lt;&#x2F;a&gt;，以為不更新等於穩定的人，他們追求的與其說是政策，不如說是是一種自我信仰，如果把這層看似堅定的信仰剝開，會發現在信仰深處的藏著的是「升級了，自己的程式會壞掉」的恐懼，因此他們寧願把資安的風險暴露出來，也不願意更新自己的程式，但若站回經濟的角度看，不更新倒是一種最經濟的策略，如同我以往陳述過的觀點「應用是資產，程式碼是負債」（見〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;how-to-treat-your-app-as-asset&#x2F;&quot;&gt;你的程式是資產還是負債？&lt;&#x2F;a&gt;〉），之所以不願投入更新，根本的原因是這個應用沒有帶來足夠的經濟價值，也就是說它是閒置資產，甚至可能是負債。&lt;&#x2F;p&gt;
&lt;p&gt;儘管本人也是高度經濟導向，但還好本人既不追求這種奇怪的信仰，也不打算把資安寄託於命運，更重視客戶對我們的託付，所以貢獻這篇升級筆記，希望能點化有心卻被信仰所困的有為青年。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;我們這次的任務是把裝有 Plesk 的 Ubuntu 16.04 LTS 升級到 Ubuntu 18.04 LTS。&lt;&#x2F;p&gt;
&lt;p&gt;Ubuntu 是 Linux 的發行版之一，對 Linux 有接觸的人應該都知道；而 Plesk 則是 web 化的伺服器管理工具，包括 virtual host、防火牆、TLS 等與伺服器相關的組態設定都可以透過 Plesk 的 web 環境來操作，相較於台灣較多人用的 cPanel，個人是較偏好 Plesk。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;在我手邊的一些機台，原本還是跑 Ubuntu 16.04，然而 Ubuntu 16.04 是 2016 年的系統，目前已經超過了 LTS 的常態維護週期：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;upgrade-ubuntu-16-04-with-plesk&#x2F;ubuntu_lifecycle.png&quot; alt=&quot;Ubuntu Linux Lifecycle&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：&lt;a href=&quot;https:&#x2F;&#x2F;ubuntu.com&#x2F;about&#x2F;release-cycle&quot;&gt;Ubuntu&lt;&#x2F;a&gt;&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;而 Plesk 自 Plesk Obsidian 18.0.35 起也放下了對 Ubuntu 16.04 LTS 的支援，因此升級到 Ubuntu 18.04 LTS 是勢在必行。&lt;&#x2F;p&gt;
&lt;!-- {# &lt;script type=&quot;module&quot; src=&quot;https:&#x2F;&#x2F;cdn.jsdelivr.net&#x2F;npm&#x2F;@justinribeiro&#x2F;lite-youtube@0.6.2&#x2F;lite-youtube.js&quot;&gt;&lt;&#x2F;script&gt; #} --&gt;
&lt;!-- &lt;lite-youtube videoid=&quot;BhHdmugCPjo&quot; style=&quot;height: 40vh;&quot;&gt;&lt;&#x2F;lite-youtube&gt; --&gt;
&lt;iframe allow=&quot;autoplay *; encrypted-media *; fullscreen *&quot; frameborder=&quot;0&quot; height=&quot;450&quot; style=&quot;width:100%;max-width:660px;overflow:hidden;background:transparent;&quot; sandbox=&quot;allow-forms allow-popups allow-same-origin allow-scripts allow-storage-access-by-user-activation allow-top-navigation-by-user-activation&quot; src=&quot;https:&#x2F;&#x2F;embed.music.apple.com&#x2F;tw&#x2F;album&#x2F;%E5%8B%A2%E5%9C%A8%E5%BF%85%E8%A1%8C-single&#x2F;541852727&quot;&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;先說明大步驟如下：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;升級 Ubuntu&lt;&#x2F;li&gt;
&lt;li&gt;重新安裝 Plesk&lt;&#x2F;li&gt;
&lt;li&gt;小錯誤修正&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;看起來好像很普通，但其中有一些小細節需要注意，詳見下文。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;sheng-ji-ubuntu&quot;&gt;升級 Ubuntu&lt;&#x2F;h2&gt;
&lt;p&gt;在升級到 Ubuntu 18.04 前，先把 Ubuntu 16.04 更新到最新的狀態：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;sudo&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; apt upgrade&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;更新後，視需要重開機，然後才跑升級程序：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;sudo&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; do-release-upgrade&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;升級的過程頗長，中間可能會遇到 GRUB、Apache、OpenSSH、AppArmor、PAM 等系統元件的組態檔是否要取代或保留的提問，一般是會選擇讓原本電腦內的組態檔保留，避免升級後設定跑掉。&lt;&#x2F;p&gt;
&lt;p&gt;升級完之後重新開機，重開機之後，Plesk 納管的網站都還活著，然而問題是 Plesk 自己的入口變成卻 404，幸好 Ubuntu Linux 的 SSH 還進得去。&lt;&#x2F;p&gt;
&lt;p&gt;SSH 登入後，試著跑一些 &lt;code&gt;plesk&lt;&#x2F;code&gt; 的子命令都會收到無此工具的錯誤，例如下面這個錯誤訊息：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Unknown Plesk command-line utility: &amp;quot;admin&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可能是在 Ubuntu 升級的過程中被刪了或斷了連結，但不要就此放棄，只要再裝一次 Plesk 就可以修復。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zhong-xin-an-zhuang-plesk&quot;&gt;重新安裝 Plesk&lt;&#x2F;h2&gt;
&lt;p&gt;秉持著救機一命勝造七級浮屠的慈悲，我們再安裝一次 Plesk。&lt;&#x2F;p&gt;
&lt;p&gt;然而在裝 Plesk 前，得先還原 Plesk 的資料庫 &lt;code&gt;psa&lt;&#x2F;code&gt;，否則後面的安裝程序會找不到 &lt;code&gt;psa&lt;&#x2F;code&gt; 而出錯。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;huan-yuan-psa-zi-liao-ku&quot;&gt;還原 psa 資料庫&lt;&#x2F;h3&gt;
&lt;p&gt;資料庫的備份在 &#x2F;var&#x2F;lib&#x2F;psa&#x2F;dumps&#x2F;，進去要切換成 root 身份：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;sudo&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; su -&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;進到 root 的 shell 後進入該目錄：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;cd&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &#x2F;var&#x2F;lib&#x2F;psa&#x2F;dumps&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;找出有 &lt;code&gt;psa&lt;&#x2F;code&gt; database 的備份檔：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;zgrep&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Current Database:&amp;quot; mysql.daily&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;*&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; |&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; grep&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; psa&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;應該會有這些：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;mysql.daily.dump.0.gz:-- Current Database: `psa`&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;mysql.daily.dump.1.gz:-- Current Database: `psa`&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;mysql.daily.dump.2.gz:-- Current Database: `psa`&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;mysql.daily.dump.3.gz:-- Current Database: `psa`&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上列結果內的 mysql.daily.dump.0.gz 這份檔案是最近的備份，把它還原回去：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;zcat&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; mysql.daily.dump.0.gz&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; |&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;sed&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -n&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;&#x2F;-- Current Database: `psa`&#x2F;,&#x2F;-- Current Database:*&#x2F;p&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; |&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;MYSQL_PWD&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;`&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;cat&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &#x2F;etc&#x2F;psa&#x2F;.psa.shadow`&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; mysql&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -uadmin&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;還原完成後，再來跑 Plesk 安裝程序。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;an-zhuang-plesk&quot;&gt;安裝 Plesk&lt;&#x2F;h3&gt;
&lt;p&gt;Plesk 的安裝程式很聰明，它是採取保留原組態為主的設計，不會裝完又全機歸零。&lt;&#x2F;p&gt;
&lt;p&gt;先下載安裝程式：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;wget https:&#x2F;&#x2F;autoinstall.plesk.com&#x2F;plesk-installer&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;再讓它可執行：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;chmod +x .&#x2F;plesk-installer&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;執行它：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;sudo sh .&#x2F;plesk-installer&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;安裝步驟會問到到我們是要安裝所有的更新還是要增加或移除 Plesk 元件呢？這裡我們選「Install the latest available update」安裝所有的更新。&lt;&#x2F;p&gt;
&lt;p&gt;另外我在安裝期間會遇到某些套件下載失敗導致安裝失敗的問題，只要再跑一次安裝程式就可以重來。&lt;&#x2F;p&gt;
&lt;p&gt;如果前面的 &lt;code&gt;psa&lt;&#x2F;code&gt; 資料庫沒有先還原的話，安裝程式最後的組態階段就會有這行錯誤訊息：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;ERROR 1049 (42000): Unknown database &amp;#39;psa&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果有事先還原 &lt;code&gt;psa&lt;&#x2F;code&gt; 資料庫的話，應該可以是順順利利的完成。&lt;&#x2F;p&gt;
&lt;p&gt;裝完之後為了安心起見，先重開機一次，順便喝個水舒緩一下緊張的情緒。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;que-ren-cheng-guo-yu-xiao-cuo-wu-xiu-zheng&quot;&gt;確認成果與小錯誤修正&lt;&#x2F;h2&gt;
&lt;p&gt;重開機之後，再次用瀏覽器確認 Plesk 的納管網站及 Plesk 自己的入口是否可以正確運作，現在看起來都是正常的，賀！&lt;&#x2F;p&gt;
&lt;p&gt;但實際上有些小錯誤還要再修正，目前發現的是 Plesk 防火牆元件異常，這可以進 Plesk 的組件管理頁裝回來，或者是移除掉。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;can-kao-zi-liao&quot;&gt;參考資料&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;support.plesk.com&#x2F;hc&#x2F;en-us&#x2F;articles&#x2F;213369589-Unable-to-access-Plesk-Unknown-database-psa-&quot;&gt;Unable to access Plesk: Unknown database ‘psa’&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;support.plesk.com&#x2F;hc&#x2F;en-us&#x2F;articles&#x2F;115000198773-Plesk-is-not-accessible-Failed-opening-required-auth-php-&quot;&gt;Plesk is not accessible: Failed opening required ‘auth.php’&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>重新認識 Pixel、DPI &#x2F; PPI 以及像素密度</title>
        <published>2021-04-09T00:00:00+00:00</published>
        <updated>2021-04-09T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/rediscover-pixel-dpi-ppi-and-pixel-density/"/>
        <id>https://editor.leonh.space/2021/rediscover-pixel-dpi-ppi-and-pixel-density/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/rediscover-pixel-dpi-ppi-and-pixel-density/">&lt;p&gt;這篇從 pixel 為起點，談談什麼是 DPI、PPI、DRP 這幾個與 pixel 有關的詞彙，試圖釐清這些「解析度」與媒體（螢幕、紙張、感光元件）之間的混亂關係。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-xi-du-hua-su-xiang-su-pixel-dao-di-shen-mo-shi-shen-mo&quot;&gt;解析度？畫素？像素？Pixel？到底什麼是什麼？&lt;&#x2F;h2&gt;
&lt;p&gt;如同物質組成基本的粒子——原子，組成數位影像的基本單位我們稱為畫素或像素，英文是 pixel，一般會簡寫為 px。&lt;&#x2F;p&gt;
&lt;p&gt;下面的示意圖是一張尺寸 50 * 50 px 的圖片，裡面的格線一格代表一個 pixel，可以很明確地看到裡面的「Ａ」是由許多的小 pixel 組合而成：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;rediscover-pixel-dpi-ppi-and-pixel-density&#x2F;Ax1000.png&quot; alt=&quot;A&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;上面的 50 * 50 px 圖片是經過模擬放大數倍的效果，裡面的「Ａ」也因此充滿鋸齒感，如果不刻意放大看，它應該是這樣的：&lt;&#x2F;p&gt;
&lt;figure style=&quot;text-align: center;&quot;&gt;
    &lt;img src=&quot;Ax1.png&quot; style=&quot;width: unset;&quot; loading=&quot;lazy&quot; alt=&quot;A&quot;&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;而這張 50 * 50 px 的圖片，我們稱它為 2,500 像素的圖片，因為裡面共有 50 * 50 = 2,500 個像素。&lt;&#x2F;p&gt;
&lt;p&gt;我們一般口語的「解析度」，指的是 50 * 50 px 這樣&lt;strong&gt;寬×高&lt;&#x2F;strong&gt;的表達方式，解析度一般會用來表示圖片、螢幕的寬、高畫素數，例如上面的 50 * 50 px 的圖片，或者是 1920 * 1080 px 的螢幕，如果把時間往回推三十年，當時的螢幕解析度只有 640 * 480 px 或更低，在當時的螢幕上，像素點是清晰可見的：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;rediscover-pixel-dpi-ppi-and-pixel-density&#x2F;68747470733a2f2f692e696d6775722e636f6d2f544e756d6b446e2e706e67.png&quot; alt=&quot;cool-retro-term&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;舊式單色 CRT 螢幕模擬圖&lt;&#x2F;figcaption&gt;
&lt;figcaption&gt;來源：cool-retro-term&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;而 2,500 像素，指的是像素的總數量，一般會用來表示相片或相機感光元件的像素數量，例如一張 2,500 像素的相片，或者一台百萬像素的相機，而「百萬像素」（mega pixel）又被縮寫成 MP，所以 iPhone 的相機是 12MP，意即 iPhone 的感光元件的有效面積大約是一千兩百萬像素，在不裁切或縮放的情況下它可以輸出 4,032 * 3,024 ≈ 1,200 萬像素的照片。&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;rediscover-pixel-dpi-ppi-and-pixel-density&#x2F;NINTCHDBPICT000520794393.webp&quot; alt=&quot;iPhone keynote&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：Apple&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;「百萬像素」、「千萬像素」這類的表示方式大多用在相機上，而在非相機領域時我們比較慣用的是&lt;strong&gt;寬×高&lt;&#x2F;strong&gt;的表達方式。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xiang-su-yu-ying-mu-ppi&quot;&gt;像素與螢幕 PPI&lt;&#x2F;h2&gt;
&lt;p&gt;像素是構成影像的基本單位，在一般人的認知，解析度越高通常也意味著畫質越高，同樣是 15 英吋大小的螢幕，一個的解析度是超古老的 640 * 480 px，另一個是 1,920 * 1,440 px，兩者的 X、Y 方向的像素數各差了三倍，這之間的差異可以用不同年代的同一款遊戲的畫面來感受：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;rediscover-pixel-dpi-ppi-and-pixel-density&#x2F;JC6XJA2kwBfh63stmr55Ef-970-80.jpg.webp&quot; alt=&quot;C&amp;amp;C Remastered&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;左：1995 年《終極動員令》、右：2020 年《終極動員令》重製典藏版&lt;&#x2F;figcaption&gt;
&lt;figcaption&gt;來源：EA&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;透過上面的示意圖可以看到，在相同尺寸的螢幕上，因為解析度的差異，帶給我們視覺上畫面精細度的差異感受。&lt;&#x2F;p&gt;
&lt;p&gt;同樣 15 吋的兩個螢幕，卻可以有著差異數倍的解析度，真實世界的 1 英吋，對應在這兩個 15 吋螢幕上會得到不同的像素數，我們把這件事簡化成下面的示意圖：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;rediscover-pixel-dpi-ppi-and-pixel-density&#x2F;AA.png&quot; alt=&quot;PPI&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;上面的示意圖中，左右代表兩個相同尺寸，但解析度不同的兩個螢幕，在左邊低解析度的螢幕上，1 英吋相當於 18 px；而右邊的高解析度螢幕上，1 英吋則是相當於 36 px，由此我們可以得知 pixel 具有相對單位的性質。&lt;&#x2F;p&gt;
&lt;p&gt;像上面這樣以「1 英吋內有多少 pixel」的表示法我們用於表示&lt;strong&gt;像素密度&lt;&#x2F;strong&gt;，英文是 pixels per inch，簡稱 PPI，上面的圖分別是 18 PPI 的螢幕與 36 PPI 的螢幕，當然生活中的螢幕不可能有如此低的 PPI，除非你活在麥塊世界：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;rediscover-pixel-dpi-ppi-and-pixel-density&#x2F;maxresdefault.jpg&quot; alt=&quot;iPhone in Minecraft&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：&lt;a href=&quot;https:&#x2F;&#x2F;youtu.be&#x2F;ZFAxa0_BAzg&quot; target=&quot;_blank&quot;&gt;TSMC&lt;&#x2F;a&gt;&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;而自從蘋果開始行銷 Retina display 後，PPI 也開始成各家手機競逐的規格之一，根據蘋果的公式，只要手機螢幕大於 300 PPI，人眼在正常使用距離下就感受不到顆粒感，但對爾等 app 開發者來說，app 內的圖檔素材也必須跟上這麼高的解析度才能得到最佳的觀看效果，關於 PPI 與 app 的關係，後面會再提到，在此先點到為止，先談談常常容易和 PPI 搞混的「DPI」。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xiang-su-yu-lie-yin-dpi&quot;&gt;像素與列印 DPI&lt;&#x2F;h2&gt;
&lt;p&gt;DPI 全文為 dots per inch，和 pixels per inch 只差在最前面的「dot」，我們知道 pixel 是像素，那 dot 是什麼？&lt;&#x2F;p&gt;
&lt;p&gt;對印表機來說，DPI 的 dot 指的是「墨點」，一台規格為 600 DPI 的印表機表示它最多能在 1 英吋的尺度內印出 600 個間距相當的墨點：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;rediscover-pixel-dpi-ppi-and-pixel-density&#x2F;600dpi_Blog-760x340.png&quot; alt=&quot;TSC TX600&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：鼎翰科技（TSC）&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;印表機的 DPI 是由它的機械結構決定的，以噴墨印表機來說，是由 X 與 Y 方向的兩組步進馬達的解析度來定義這台印表機的解析度，X 方向的步進馬達負責驅動噴墨頭；Y 方向的步進馬達負責帶動紙張前進，這也是之所以我們看普通印表機的規格表上常常是寫成 600 * 600 DPI 的原因，前後兩個數字分別代表 X 方向與 Y 方向的 DPI。&lt;&#x2F;p&gt;
&lt;p&gt;對於非噴墨型的印表機，例如上圖的熱感式標籤列印機或雷射印表機，他們雖然不存在 X 軸的步進馬達，但他們的成像元件也是有密度概念的，上圖為 TSC TX600，它的成像元件密度就有達到一英吋 600 個點，密度越高代表越高的製程能力，價錢當然也是越貴。而雷射印表機則是由 Galvano 反射鏡（振鏡）的轉動解析度來決定 X 方向的 DPI，不同形式的印表機成像原理不同，但輸出密度這個概念是通用的。&lt;&#x2F;p&gt;
&lt;p&gt;然而 DPI 的混亂之處在於這個「dot」在不同的媒體有著不同的定義，對印表機來說，dot 是墨點，但圖檔內卻也有著 DPI 的設定：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;rediscover-pixel-dpi-ppi-and-pixel-density&#x2F;image_dpi.png&quot; alt=&quot;GIMP DPI &amp;amp; Size&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;上面是一張 512 * 512 px 的圖片，它的 DPI 是 72 px&#x2F;inch，這裡的 DPI 的「dot」指的是「pixel」，而不是「墨點」，但一旦送至印表機，DPI 的「dot」又會被解讀成「墨點」，因此以 X 方向來說，我們可以換算出這張影像的物理尺寸：&lt;&#x2F;p&gt;
&lt;div style=&quot;text-align: center; font-size: x-large; margin-bottom: 1rem;&quot; class=&quot;wide&quot;&gt;
&lt;code&gt;512 px &#x2F; (72 px&#x2F;inch) = 7.111 inch = 18.062 cm&lt;&#x2F;code&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;把這樣的規格拿去列印，得到的是一張寬度為 18.062 公分的印刷品，因為圖檔的 DPI 是設定成 72 dot&#x2F;inch，印表機因此會很聽話的在 1 英吋的尺度內印出 72 個墨點，而 X 方向總共有 512 個「點」，這裡的「點」既是 pixel 也是墨點，因此就會得到寬度 512 &#x2F; 72 = 7.111 英吋的圖片，相當於 18.062 公分。&lt;&#x2F;p&gt;
&lt;p&gt;如果把這張圖檔的 DPI 改為 150，那 X 方向的輸出尺寸計算如下：&lt;&#x2F;p&gt;
&lt;div style=&quot;text-align: center; font-size: x-large; margin-bottom: 1rem;&quot; class=&quot;wide&quot;&gt;
&lt;code&gt;512 px &#x2F; (150 px&#x2F;inch) = 3.413 inch = 8.670 cm&lt;&#x2F;code&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;比較一下兩種不同 DPI 設定下的輸出結果：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;rediscover-pixel-dpi-ppi-and-pixel-density&#x2F;dpi_comparison.svg&quot; alt=&quot;72 DPI &amp;amp; 150 DPI comparison&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;左邊為 72 DPI 輸出、右邊為 150 DPI 輸出&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;可以看到，512 * 512 px 的影像，在 72 DPI 下的輸出儘管尺寸較大，但也是較模糊的，而改用 150 DPI 輸出，結果銳利許多（因為更密集了），而在平面設計或排版界，往往是紙張與印表機的解析度先決，也就是說我的傳單或小冊已經注定是 A4，而輸出設備的解析度是 400 * 400 DPI，那麼我想印一頁 A4 寬度滿版的影像，我必須讓我的圖檔能夠撐到這麼大而不模糊，用上面的公式反向推算應有的 X 方向像素：&lt;&#x2F;p&gt;
&lt;div style=&quot;text-align: center; font-size: x-large; margin-bottom: 1rem;&quot; class=&quot;wide&quot;&gt;
&lt;code&gt;&lt;var style=&quot;font-weight: bolder;&quot;&gt;X&lt;&#x2F;var&gt; px &#x2F; (400 px&#x2F;inch) = 8.268 inch = 21 cm&lt;&#x2F;code&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;上面的 &lt;var&gt;X&lt;&#x2F;var&gt; 會等於 3,307 px，假設同樣是正方形的圖片，那 Y 方向也會是 3,307 px，因此這張要放到 A4 寬度滿版的相片，會是 3,307 * 3,307 ≈ 10 MP，需要千萬畫素的相機才能拍出。&lt;&#x2F;p&gt;
&lt;p&gt;隨著紙張的變大，一張正方形圖像想印成寬度滿版所需要的畫素也隨之變高，以 A3 來說，要維持 400 DPI 的解析度，大約需要兩千萬畫素的圖檔；A2 則需要約四千萬畫素圖檔；A1 則需要約九千萬畫素的圖檔，然而現在主流的專業全片幅相機，也大多是兩千萬畫素，因此實際上的做法會是把圖檔的 DPI 降低，只要能騙過人眼就好。&lt;&#x2F;p&gt;
&lt;p&gt;要騙過人眼，得利用觀看距離，對 A4 來說，一般的觀看距離是 30 公分，在這樣的距離下，大約只要 400 DPI 的列印解析度，就能讓肉眼感受不到墨點，對更大尺寸的媒體來說，觀看距離也會相對的拉遠，因此並不需要強求維持 400 DPI 的列印解析度，也能達到看不出墨點的品質，例如廣告牆：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;rediscover-pixel-dpi-ppi-and-pixel-density&#x2F;5954446966_c7b8284fa0_b.jpg&quot; alt=&quot;廣告牆&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：&lt;a href=&quot;https:&#x2F;&#x2F;www.flickr.com&#x2F;photos&#x2F;in_future&#x2F;5954446966&#x2F;&quot; targe=&quot;_blank&quot;&gt;小草&lt;&#x2F;a&gt;&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;在這樣大面積的媒材上要以 400 DPI 輸出，顯然是不現實的，不僅電腦處理不了這麼大的圖檔，也沒有實際上的益處，廣告牆的觀看距離遠至數十公尺，在這樣的距離下墨點的密度顯然無關緊要，因此實務上只要 150 DPI 就足以滿足這麼大面積、長距離觀看特性的媒體。&lt;&#x2F;p&gt;
&lt;p&gt;由上面我們可以得知，不論 DPI 怎麼設，圖檔的原始寬高像素是不會受 DPI 影響的，DPI 僅是用於決定輸出時要叫印表機把「點」印的多密集的一項參數。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;話再說回來，那張 512 * 512 px 的圖片，我硬是要印成 A4 滿版，卻又想獲得好的畫質，那應該怎麼做？&lt;&#x2F;p&gt;
&lt;p&gt;我們可以用手動放大影像的方式把影像放大：&lt;&#x2F;p&gt;
&lt;figure style=&quot;text-align: center;&quot;&gt;
    &lt;img src=&quot;kcmphdploov.png&quot; style=&quot;width: unset;&quot; loading=&quot;lazy&quot; alt=&quot;GIMP Scale Image&quot;&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;以 GIMP 來說，它就提供了四種放大演算法，但這樣基於補插點的放大只會得到變大也變模糊的影像，剛好近幾年 AI 演算法爆棚，我們可以利用 AI 演算法的工具把圖像放大，而不損失畫質：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;rediscover-pixel-dpi-ppi-and-pixel-density&#x2F;barbara_comparison.png&quot; alt=&quot;Smart Upscaler comparison&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;左邊為 512 * 512 px 原圖、右邊為放大至 3,000 * 3,000 px 的圖&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;上面左邊是原圖，右邊則是用 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;icons8.com&#x2F;upscaler&quot;&gt;Smart Upscaler&lt;&#x2F;a&gt; 放大的結果，比較頭巾的細節，很明顯的在黑科技的加持下，圖像放大了反而畫質還更好了，再用肉眼比較桌巾的格紋，也可以看到右邊的格紋更加銳利（但格紋形狀卻好像不那麼「方正」了，有一好沒兩好）。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;dpi-yu-ppi&quot;&gt;DPI 與 PPI&lt;&#x2F;h2&gt;
&lt;p&gt;綜合以上，PPI 與 DPI 都用來表示「1 英吋內點的數量」，也就是密度的概念，不同的僅在於 PPI 的「點」很明確的定義為 pixel，因此 PPI 永遠都只與螢幕相關，而 DPI 的「點」有時候是 pixel，有時候是墨點，因此有時候會用於表示螢幕的點密度，有時候又會用於表示印表機的點密度，甚至 3D 印表機也是用 DPI 表示機台的解析度，因為 3D 印表機也是由步進馬達驅動，只不過多了 Z 軸的機構。&lt;&#x2F;p&gt;
&lt;p&gt;PPI &#x2F; DPI 兩者混用的狀況時有所見，相當容易令人誤會，因而近代的作業系統也大多屏棄了 DPI 的字眼，改以更為通俗的方式讓用戶調整解析度與 UI 元件比例：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;rediscover-pixel-dpi-ppi-and-pixel-density&#x2F;macos.png&quot; alt=&quot;macOS&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;rediscover-pixel-dpi-ppi-and-pixel-density&#x2F;2021-04-15_23-56.png&quot; alt=&quot;Elementary OS&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;PPI &#x2F; DPI 兩者的概念也大多是共通的，人的眼力有所極限，能否看到「點」與距離有關，想要騙過大腦讓它認為眼前的螢幕或紙張是高品質的，那就讓螢幕／紙張離眼睛遠一點，或者讓螢幕／紙張的 PPI &#x2F; DPI 高一點，這個定律是適用於所有人的，除非他有瞳術那另當別論⋯⋯。&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;rediscover-pixel-dpi-ppi-and-pixel-density&#x2F;img_5f3bdb22521ac.webp&quot; alt=&quot;瞳術&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：火影忍者&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;另一個涵義模糊的詞彙是「解析度」，「解析度」是個更廣泛的概念，不論是 512 * 512 px、300 PPI、150 DPI，他們都可以被「解析度」稱之，解析度的英文 resolution 意義也是同樣的粗略，因此看到解析度最好還是看它後面接的是什麼才能正確地理解前後文脈絡。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xiang-su-dpr-yu-geng-duo-de-dan-wei&quot;&gt;像素、DPR 與更多的單位&lt;&#x2F;h2&gt;
&lt;p&gt;再把主題拉回像素與 PPI 的世界，前面提到過自從 Retina display 問世以來，手機的 PPI 有著飛躍式的成長，像軍備競賽一樣，隨便都破 400 PPI，下面是幾款 iPhone 的面板解析度與 PPI 表：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;裝置&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: center&quot;&gt;螢幕像素&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: center&quot;&gt;螢幕對角線尺寸&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: right&quot;&gt;PPI&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;iPhone 3&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;320 * 480 px&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;3.5 inch&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;163&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;iPhone 4&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;640 * 960 px&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;3.5 inch&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;326&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;iPhone SE2&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;750 * 1134 px&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;4.7 inch&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;326&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;iPhone 12 Pro Max&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;1284 * 2778 px&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;6.68 inch&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;458&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;上表中的 iPhon 3 是末代非 Retina display 的型號，從 iPhone 4 起，開啟了 Retina display 的時代，扣掉上面兩排已經停產的型號，看  iPhone SE2 與 iPhone 12 Pro Max 這兩款正在線上的機種，螢幕寬度的像素分別是 750 px 與 1,284 px，這樣巨大的寬度像素對於 app 或 RWD web 佈局是有問題的，試想 1,284 px 寬的螢幕，相當於一個 15 吋桌上型顯示器的像素寬度，但它的實際寬度卻只有 8 公分左右，這對 RWD 的佈局是不合理的，在 CSS media query 的判斷上 1,284 px 會落入桌機板的區間內，如此就無法為這樣高 PPI 的裝置正確的顯示出手機版的網頁，必須有一種機制來處理這樣的問題，於是在 web 方面引入了 CSS pixel 這個新尺度。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;css-pixel&quot;&gt;CSS Pixel&lt;&#x2F;h3&gt;
&lt;p&gt;CSS pixel 是 web 世界的 pixel，它與螢幕像素之間有一個倍率關係，這個倍率關係有許多名字，有稱之為 device pixel ratio，簡稱 DPR，也有稱之為 dots per pixel，簡稱 DPPX，也有人稱為 pixel density，不論是 DRP 或 DPPX 或 pixel density，他們的意義都是一樣的——螢幕像素與 CSS pixel 之間轉換的倍率值。&lt;&#x2F;p&gt;
&lt;p&gt;在 CSS pixel 與 DPR 的引入下，我們把上面的表格擴充一下：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;裝置&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: center&quot;&gt;螢幕像素&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: center&quot;&gt;螢幕對角線尺寸&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: right&quot;&gt;PPI&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: center&quot;&gt;CSS Pixel&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: center&quot;&gt;DPR&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;iPhone 3&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;320 * 480 px&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;3.5 inch&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;163&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;320 * 480 px&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;1&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;iPhone 4&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;640 * 960 px&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;3.5 inch&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;326&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;320 * 480 px&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;2&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;iPhone SE2&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;750 * 1134 px&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;4.7 inch&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;326&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;375 * 667 px&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;2&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;iPhone 12 Pro Max&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;1284 * 2778 px&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;6.68 inch&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;458&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;428 * 926 px&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;3&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;透過 DPR 與 CSS pixel 的換算，螢幕寬度從原本的 1,284、750 px 變成了少少的 428、375 px，完全可以在一個 CSS media query 區間內處理。&lt;&#x2F;p&gt;
&lt;p&gt;實際上用 CSS media query 與 JavaScript 查出來的寬度，也都會是經過 DPR 換算後的寬度，CSS pixel 與 DPR 的計算一切都是隱式發生的，就算對 CSS Pixel 與 DPR 完全沒概念的人，也不影響他開發 web，我輩 web 開發者不需考慮設備的真實解析度，一律都以 CSS pixel 的尺度去設計即可。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;dpr-dppx-pixel-density&quot;&gt;DPR &#x2F; DPPX &#x2F; Pixel Density&lt;&#x2F;h3&gt;
&lt;p&gt;在 Retina Dispaly 出現以前，CSS pixel 與螢幕 pixel 是很間單的 1:1 對應，一顆像素就是一顆像素，並沒有比例關系的概念，Retina display 問世後，一個 CSS pixel 對應到螢幕像素的關係呈一種比例關係，可以用下面的圖示理解這個比例關係：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
    &lt;img src=&quot;css_pixel.png&quot; alt=&quot;CSS pixel and device pixel&quot; loading=&quot;lazy&quot;&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;螢幕寬度固定在 8 公分左右，在 8 公分內塞入越多的螢幕像素，對人眼的感受越細緻，前面提過我們是用 PPI 來衡量單位距離內塞入的螢幕像素數，而以 160 PPI 做基準設為 1x，320 PPI 則視為 2x，以此類推。&lt;&#x2F;p&gt;
&lt;p&gt;而圖中的 CSS pixel 是被定義為 1&#x2F;160 inch，因此 CSS pixel 與真實世界的關係是穩定的，因此 CSS pixel 與螢幕像素之間也會有這樣的比例關係存在，關於 CSS pixel 的定義下面會再談到。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;回顧我們賦予這個比例關係的三個名字，各有其意義在：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Device Pixel Ratio &#x2F; DPR：描述螢幕像素與 CSS 像素間的&lt;strong&gt;比例關係&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Dots Per Pixel &#x2F; DPPX：「dot」指的是螢幕像素、「pixel」指的是 CSS pixel，描述兩者間的&lt;strong&gt;數量比&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;li&gt;Pixel Density：描述&lt;strong&gt;像素密度&lt;&#x2F;strong&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;儘管面向略有不同，但三者指涉的的確是同一個概念——CSS 像素與螢幕像素的&lt;strong&gt;比例關係&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;這邊又接回像素密度的概念，前面有提過 PPI 的意義——1 英吋內有多少 pixel，越高的 PPI 表示越精細的螢幕，而 CSS pixel 又是因應高 PPI 螢幕下的產物，CSS pixel 的原始定義是這樣的：&lt;&#x2F;p&gt;
&lt;div style=&quot;text-align: center; font-size: x-large; margin-bottom: 1rem;&quot; class=&quot;wide&quot;&gt;
    &lt;code&gt;1 CSS pixel ≈ 1&#x2F;96 inch&lt;&#x2F;code&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;但為了因應不同大小的螢幕，觀看距離也有所不同，而在原始定義之外，有其它的參考定義：&lt;&#x2F;p&gt;
&lt;div style=&quot;text-align: center; font-size: x-large; margin-bottom: 1rem;&quot; class=&quot;wide&quot;&gt;
    &lt;code&gt;Modern laptop with LCD:&lt;br&gt;1 CSS Pixel ≈ 1&#x2F;125 inch&lt;&#x2F;code&gt;
&lt;&#x2F;div&gt;
&lt;div style=&quot;text-align: center; font-size: x-large; margin-bottom: 1rem;&quot; class=&quot;wide&quot;&gt;
    &lt;code&gt;Smartphones &#x2F; Tablets:&lt;br&gt;1 CSS Pixel ≈ 1&#x2F;160 inch&lt;&#x2F;code&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;在手機上引用的是第三個參考定義，把公式翻轉一下：&lt;&#x2F;p&gt;
&lt;div style=&quot;text-align: center; font-size: x-large; margin-bottom: 1rem;&quot; class=&quot;wide&quot;&gt;
    &lt;code&gt;160 CSS Pixel ≈ 1 inch&lt;&#x2F;code&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;因此：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;在 160 PPI（每英吋有 160 顆螢幕像素）的螢幕：1 CSS pixel = 1 螢幕像素（160 &#x2F; 160 = 1），DPR = 1&lt;&#x2F;li&gt;
&lt;li&gt;在 320 PPI 的螢幕：1 CSS pixel = 2 螢幕像素（320 &#x2F; 160 = 2），DPR = 2&lt;&#x2F;li&gt;
&lt;li&gt;在 163 PPI 的螢幕：DPR = 1.02 ≈ 1&lt;&#x2F;li&gt;
&lt;li&gt;在 326 PPI 的螢幕：DPR = 2.03 ≈ 2&lt;&#x2F;li&gt;
&lt;li&gt;在 458 PPI 的螢幕：DPR = 2.86 ≈ 3&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;CSS pixel 的定義公式僅為原則，現實上還要考慮到螢幕面板的製程能力與切割最佳化因素，又或者是手機廠牌自己對人機工程的考慮，所以實際上各廠牌還是有自己的一套 CSS pixel 的決定法則，以 iOS 來說，它的 CSS pixel 實際上是以 1&#x2F;163 inch 為基礎的，這是為了與他們定義的 iOS point 一致，而 iOS point 是以 iPhone 3 的 163 PPI 為基礎的，但還是偶有例外 。&lt;&#x2F;p&gt;
&lt;p&gt;Android 陣營也有類似的狀況，Android 的 dp 單位是以 1&#x2F;160 inch 為基礎，但一樣偶有例外，關於 iOS point 與 android dp 兩個單位，後面會再提到。&lt;&#x2F;p&gt;
&lt;p&gt;不過就如同前面說的，站在開發者的角度，CSS media query 查出來的已經是 CSS pixel 的尺度，我們的佈局可以不用管真實解析度是多少、不用管 DPR 是多少、不用管設備的型號，直接在 media query 用最直觀的 pixel 做 RWD 的分區間設計就好，render 引擎會幫我們處理好這些底層換算的複雜工作，我們唯一要做的是加上這句標籤讓瀏覽器 render 出符合裝置寬度，且未縮放的頁面：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;meta&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;viewport&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; content&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;width=device-width, initial-scale=1&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;dpr-mei-na-mo-jian-dan&quot;&gt;DPR 沒那麼簡單&lt;&#x2F;h3&gt;
&lt;iframe src=&quot;https:&#x2F;&#x2F;www.youtube-nocookie.com&#x2F;embed&#x2F;rmPHuvQoh0g&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;但事情沒有那麼簡單⋯⋯對於文字、SVG、canvas、HTML 元素框線等向量元素，他們都是即時 render 下的結果，因此不論 DPR 為何，render 引擎都會給出最佳化的結果：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;rediscover-pixel-dpi-ppi-and-pixel-density&#x2F;text.svg&quot; alt=&quot;Text in different PPI comparison&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;純文字區域放大看還是相當清晰，但對於點陣圖，就會有畫質劣化的感受：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;rediscover-pixel-dpi-ppi-and-pixel-density&#x2F;image.svg&quot; alt=&quot;Image in different PPI comparison&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;因為在 1x 設備上，圖像的 1 個像素對應到 1 個 CSS 像素，而 1 CSS 像素也對應到螢幕的 1 個像素，但在 2x 的設備，圖像的 1 個像素對應到 1 個 CSS 像素，但此時 1 CSS 像素卻是對應到 4 個螢幕像素，相當於這 100 x 100 px 的影像被投放到 400 x 400 px 的平面上，這讓 render 引擎只能把那 100 * 100 px 的圖像&lt;strong&gt;粗暴的補插點放大&lt;&#x2F;strong&gt;，因此儘管 1x 設備與 2x 設備他們的手持寬度都是 8 公分左右，但螢幕像素密度差異還是讓視覺感受到的品質下降，所以對於圖像，我們必須準備不同解析度的版本供應給不同 DPR 的設備：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;rediscover-pixel-dpi-ppi-and-pixel-density&#x2F;image2.svg&quot; alt=&quot;Different images in different PPI comparison&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;綜合以上，在 web 開發上，我們可以利用 &lt;code&gt;&amp;lt;img&amp;gt;&lt;&#x2F;code&gt; 的 &lt;code&gt;srcset&lt;&#x2F;code&gt; 和 &lt;code&gt;sizes&lt;&#x2F;code&gt; 屬性來供應不同倍率的圖檔，而瀏覽器會自動根據&lt;strong&gt;設備當前的 DPR&lt;&#x2F;strong&gt; 來顯示最適合的圖檔。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;app-de-pt-yu-dp&quot;&gt;App 的 pt 與 dp&lt;&#x2F;h3&gt;
&lt;p&gt;在 app 方面，也是相同的概念，由於不同的手機也存在著各自的螢幕解析度與 PPI，因此一樣會面臨到 app 開發時需考慮窄至 320 px，寬至 1,284 px 的佈局呈現，因此 iOS 與 Android 也各自引進了類似 CSS pixel 的單位，在 iOS 用的是 point，簡稱 pt，不過這 pt 與 CSS 的 pt 又撞名，實際上兩者是不同的定義，為了分別起見我們稱為 iOS pt；而 Android 這邊用的單位是 device-independent pixel，簡稱 dp，不論是 CSS pixel、iOS pt、dp 都是類似概念下的產物。&lt;&#x2F;p&gt;
&lt;p&gt;回顧前面的 CSS pixel 定義：&lt;&#x2F;p&gt;
&lt;div style=&quot;text-align: center; font-size: x-large; margin-bottom: 1rem;&quot; class=&quot;wide&quot;&gt;
    &lt;code&gt;1 CSS pixel ≈ 1&#x2F;160 inch&lt;&#x2F;code&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;而這是 iOS pt 的定義：&lt;&#x2F;p&gt;
&lt;div style=&quot;text-align: center; font-size: x-large; margin-bottom: 1rem;&quot; class=&quot;wide&quot;&gt;
    &lt;code&gt;1 iOS pt ≈ 1&#x2F;163 inch&lt;&#x2F;code&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;而這是 dp 的定義：&lt;&#x2F;p&gt;
&lt;div style=&quot;text-align: center; font-size: x-large; margin-bottom: 1rem;&quot; class=&quot;wide&quot;&gt;
    &lt;code&gt;1 dp ≈ 1&#x2F;160 inch&lt;&#x2F;code&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;與 CSS pixel 相同的，這些公式都僅做為原則使用，僅供參考，手機上實際的 iOS pt 與螢幕解析度的比例關係是由人工定義出來的，理由與上面相同，因為面板製程能力或分割效率或廠牌對人機工程的考慮而決定，因此儘管公式不同，但結果卻是完全相同：&lt;&#x2F;p&gt;
&lt;div class=&quot;wide&quot;&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;裝置&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: center&quot;&gt;螢幕像素&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: right&quot;&gt;PPI&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: center&quot;&gt;CSS Pixel&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: center&quot;&gt;iOS pt&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: center&quot;&gt;DPR&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: center&quot;&gt;iOS Sacle Factor&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;iPhone 3&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;320 * 480 px&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;163&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;320 * 480 px&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;320 * 480 pt&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;1&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;@1x&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;iPhone 4&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;640 * 960 px&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;326&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;320 * 480 px&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;320 * 480 pt&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;2&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;@2x&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;iPhone SE2&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;750 * 1134 px&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;326&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;375 * 667 px&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;375 * 667 pt&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;2&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;@2x&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;iPhone 12 Pro Max&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;1284 * 2778 px&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;458&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;428 * 926 px&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;428 * 926 pt&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;3&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;@3x&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;一模模一樣樣。&lt;&#x2F;p&gt;
&lt;p&gt;在 Android 方面，在 dp 與 DPI（PPI）的基礎下，直接制定了以倍率為基準的區間：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;密度限定符&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: center&quot;&gt;倍率&lt;&#x2F;th&gt;&lt;th&gt;DPI (PPI)&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;ldpi&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;0.75x&lt;&#x2F;td&gt;&lt;td&gt;~ 120 DPI&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;mdpi&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;1.0x 基準&lt;&#x2F;td&gt;&lt;td&gt;~ 160 DPI&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;hdpi&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;1.5x&lt;&#x2F;td&gt;&lt;td&gt;~ 240 DPI&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;xhdpi&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;2.0x&lt;&#x2F;td&gt;&lt;td&gt;~ 320 DPI&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;xxhdpi&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;3.0x&lt;&#x2F;td&gt;&lt;td&gt;~ 480 DPI&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;xxxhdpi&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;4.0x&lt;&#x2F;td&gt;&lt;td&gt;~ 640 DPI&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;Android 的各廠牌手機都必然會落入上表的某一個組別內，因此 Android 的 app 開發者不用去考慮真實的螢幕解析度是多少，只要確保你的佈局可以適用於從 ldpi ~ xxxhdpi 而不跑版，並且提供從 0.75x 起至 4.0x 的各組倍率的圖檔，那 Android 會自動根據用戶手機的 DPI 去呈現佈局以及最適用的圖檔。&lt;&#x2F;p&gt;
&lt;p&gt;不論是 iOS 還是 Android 他們都引入了倍率的概念，儘管基準的 PPI（DPI）略有不同，在 iOS 的倍率從 @1x 到 @3x，在 Android 則從 0.75x 到 4.0x 這些倍率的用意也就是我輩開發者需要準備的圖檔倍率，而作業系統會根據用戶當前的設備去顯示最適合他的解析度的圖檔。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-yu&quot;&gt;結語&lt;&#x2F;h2&gt;
&lt;p&gt;綜合全文，對開發者而言，需要關注的點有三：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;不用去管圖檔本身的 DPI 設定，它只在列印時有意義。&lt;&#x2F;li&gt;
&lt;li&gt;平台佈局的單位是什麼？web 用 CSS pixel、iOS 用 iOS pt、Android 用 dp，用正確的單位進行版面的佈局，不要去糾結螢幕的硬體解析度。&lt;&#x2F;li&gt;
&lt;li&gt;圖檔依照平台指定的倍率提供，iOS 有 @1x ~ @4x、Android 有 ldpi ~ xxxhdpi，確保 app 內的圖檔都有提供這些倍率的版本，web 的話則是用 &lt;code&gt;&amp;lt;img&amp;gt;&lt;&#x2F;code&gt; 標籤的 &lt;code&gt;srcset&lt;&#x2F;code&gt; 和 &lt;code&gt;sizes&lt;&#x2F;code&gt; 來定義各倍率的圖檔，瀏覽器會自行抓取最適合的做顯示。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;這篇文章是本人寫過最長的一篇，在寫的途中參考了大量資料，自己也釐清了一些過往一知半解的認知，但這麼廣的議題（從像素講到 DPI &#x2F; PPI 和印表機的機構再講到 app 的單位）應該是難免有錯，有錯誤的地方請大聲怒罵指責。&lt;&#x2F;p&gt;
&lt;iframe src=&quot;https:&#x2F;&#x2F;www.youtube-nocookie.com&#x2F;embed&#x2F;2Lo_YAnGBh4&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>閱讀流水帳──&lt;br&gt;《與家人的財務界線》</title>
        <published>2021-02-26T00:00:00+00:00</published>
        <updated>2021-02-26T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/family-money-management-guide/"/>
        <id>https://editor.leonh.space/2021/family-money-management-guide/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/family-money-management-guide/">&lt;a class=&quot;rakuten&quot; href=&quot;https:&#x2F;&#x2F;affiliate.api.rakuten.com.tw&#x2F;redirect?nw=tw&amp;site=afl&amp;a=9554f6aedc05c984e5b5a88d11dede3c1ee518c05af339861c29a8769bef1ee851b013d5cf9e1cee&amp;ar=a747b10f014cf18584f9ee2ca5516854cb91d9c5d6849f5c93000e3163fa586b69315791463a07d2&amp;cs=b09d1ca40365d89a1e24c1a1b11333bd&amp;pr=e57594b8443d389c&amp;ap=pr%3De57594b8443d389c&amp;e=1&amp;url=https%3A%2F%2Fwww.rakuten.com.tw%2Fshop%2Frbook%2Fproduct%2F2014210022095%3Fscid%3Drafp-&quot;&gt;
    &lt;img loading=&quot;lazy&quot; src=&quot;https:&#x2F;&#x2F;tshop.r10s.com&#x2F;b9a&#x2F;9e4&#x2F;b18e&#x2F;220e&#x2F;0007&#x2F;5860&#x2F;e384&#x2F;11f2ea8d020242ac110002.jpg&quot;&gt;
    &lt;div class=&quot;content&quot;&gt;
        &lt;h2&gt;與家人的財務界線&lt;&#x2F;h2&gt;
        &lt;h3&gt;富媽媽教你釐清家人的金援課題,妥善管理親情的金錢漏洞。&lt;&#x2F;h3&gt;
    &lt;&#x2F;div&gt;
&lt;&#x2F;a&gt;
&lt;h2 id=&quot;xu&quot;&gt;序&lt;&#x2F;h2&gt;
&lt;p&gt;人真正的問題出在「心靈」，而非「金錢」。&lt;br &#x2F;&gt;
人遺傳了父母的身體，同時也繼承了父母「回應」、「思考」、「解決」問題的「模式」。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;part-1&quot;&gt;Part 1&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;第一章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
人面對著「錢」的壓力，同時也面對著「人」的壓力，「人」的問題，比「錢」的問題，更難處理。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;part-2&quot;&gt;Part 2&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第二章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
金錢圈有「自己、伴侶、小孩、家人和父母兄弟姐妹、慈善團體、朋友」，每一個圈，代表的是「誰可以用自己的錢？」：學會指認什麼「是自己該負責的？」、什麼「不是自己該負責的？」，學會應對「自己很自私嗎？」、「自己很不孝順嗎？」、「自己會傷害到別人嗎？」，這類的「自我懷疑」，記住！自己在「拉一條線」，不是「築一道牆」。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第三章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
整理金錢圈：太太沒有先生的經歷，不諒解先生的觀念，先生沒有太太的經歷，不理解太太的恐懼。夫妻倆坐下來，好好觀察自己和敞開自己，把過去的經驗和過去的信念，整理、梳理一下，知道彼此有怎樣的過去？有什麼印記？不做出任何判斷、承諾，好好溝通，彼此理解。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第四章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
金錢依賴（「情感依賴」轉移到「金錢依賴」）：腦袋慌亂，不知如何是好？先「退一步」，在大腦裡「觀察」自己，認出對方「勒索型語言（你不幫，就沒義氣！）」、「勒索型動作」，大腦就會出現「啊哈！原來如此！」「啊哈！對方又來了！」的聲音。夫妻之間，光是不埋怨，不指責，互相退讓，互相體諒，已經非常不容易。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第五章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
金錢義務：「凍齡老少年」是年輕的時候，不需要承擔責任，年長的時候，逃避承擔責任，從不負責任的孩子，成為不負責任的父母，再讓善良的孩子，為自己解圍。&lt;br &#x2F;&gt;
被借錢，首先意識到自己不用壓抑，也不需抱怨，接下來，開始對對方的金援，設定界線，告訴對方：「自己只能借五千元，沒有更多了！」，當對方大聲抱怨、指責、哭泣、發洩情緒的時候，自己只是以同理心，瞭解對方為何生氣？也完美地控制自己不發怒，不煩躁，只是重複：「你埋怨我沒幫上忙，我瞭。」，緊接著，不描述，也不解釋，自己為什麼不提高金額？只是重複著、堅定著，堅持自己要給的金額，毫不動搖。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第六章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
金錢性格：要測試省錢的方法，到底有沒有效益？首先要把時間換成錢，計算時薪。享受型的人，學著自己承諾，一年只能買幾個包？花費不超過多少錢。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第七章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
金錢藍圖：「貧窮困乏、辛苦奮鬥、舒適安逸、豐衣足食、豪華奢侈」，以上是每個人對自己「現在要過什麼生活？」、「未來要過什麼生活？」。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;part-3&quot;&gt;Part 3&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第八章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
當不接納自己的情緒，自己指責自己是自私的壞女人？重置，讓自己知道「該做什麼？」，知道自己「不該做什麼？」，自己變得不再困惑、不再衝突、不再恐懼。不困惑，來自一種「周詳的視野」。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第九章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
有的人，面對憤怒的人，會害怕、會軟弱，有的人，卻能保持冷靜，穩定情緒，變得很「抽離」。&lt;br &#x2F;&gt;
自己加在籬笆上的「鐵刺」，就是讓別人知道，自己守護自己的原則，如果有任何人「越界」，自己的「鐵刺」，就會刺破那個人的小腿，讓那個人尖叫，讓那個人流淚。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第十章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
把自己不喜歡的事，說清楚，雙方才能好好相處。劃清金錢界線，要注意兩件事：1 不要陷入三角關係，就是不要「間接傳話」。2 沒辦法幫忙，也不要忘記問：「我還能為你做點什麼？」。&lt;br &#x2F;&gt;
可靠的顧問團，千萬不要是閨蜜、跟自己一樣處境的人，必須是「走在自己前面，有好的溝通技巧，在這方面處理得很成熟」的人。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第十一章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
不幫忙他人，便「在意他人怎麼說？」的人，絕大多數，都是「怕被孤立」，察覺自己正處於恐懼和掙扎，應當停下來，感受自己內心的聲音。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第十二章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
人該擔負的責任，首先背負自己，其次背負別人，當自己感受到「恐懼感」，對方就是「侵犯型」，當自己感受到「愧疚感」，對方就是「操縱型」，愧疚感是世界上最難處理的情緒，來自內在、來自小時候，學過的錯誤觀念，要學習抵抗，如果不能控制愧疚感、察覺愧疚感，安頓自己內心，當要面對愧疚感，就會陷入掙扎、憤怒、糾結裡。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第十三章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
這不是「一個人」的事情，是「很多人」的事情，讓孩子跟一群不受控的孩子，一起造成了混亂，這不是「一個人」造成的，父母卻怪「一個人」，這是一種「有限的知識」，也是一種「誤解」。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;part-4&quot;&gt;Part 4&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第十四章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
「有界線」的人，不需要自責，不需要焦慮，所有的批評，應該學著拉開、舉起來、放地上、踢出去，就像一個藏玩具的幼兒園老師，看著所有人的需求，看著所有人的欲望，做出公允、恰當、符合長期利益的決定。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第十五章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
父母與子女之間，不應有著「我年輕時養你，等我老了，你應當還我！」的關係，如果，受到父母威脅，要收回情感（罵你不孝），還是冷漠對待（跟你冷戰），這是威脅，也是手段，自己要往前站一步，不要後退、不要妥協，必須分辨「什麼自己能給？什麼自己不能給？」。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第十六章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
人生，靠努力是沒有用的，人生中的所有問題，就像階梯，突破一個難點，就往上跳一階，突破不了難點，就原地一直跳、一直跳、一直跳，跳不過去，為了要跳過階梯，自己就得要學習、要承擔、要犯錯。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>閱讀流水帳──&lt;br&gt;《專注力協定》</title>
        <published>2021-02-22T00:00:00+00:00</published>
        <updated>2021-02-22T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/indistractable-how-to-control-your-attention-and-choose-your-life/"/>
        <id>https://editor.leonh.space/2021/indistractable-how-to-control-your-attention-and-choose-your-life/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/indistractable-how-to-control-your-attention-and-choose-your-life/">&lt;a class=&quot;rakuten&quot; href=&quot;https:&#x2F;&#x2F;affiliate.api.rakuten.com.tw&#x2F;redirect?nw=tw&amp;site=afl&amp;a=12f4b2a169af031d5bb753abe446398ec53d88088b8f10c11c29a8769bef1ee851b013d5cf9e1cee&amp;ar=a747b10f014cf18584f9ee2ca5516854cb91d9c5d6849f5c93000e3163fa586b69315791463a07d2&amp;cs=23a6500ba6ee8d55247d2174be4f8ecf&amp;pr=e57594b8443d389c&amp;ap=pr%3De57594b8443d389c&amp;e=1&amp;url=https%3A%2F%2Fwww.rakuten.com.tw%2Fshop%2Frbook%2Fproduct%2F2011760329377%3Fscid%3Drafp-&quot;&gt;
    &lt;img loading=&quot;lazy&quot; src=&quot;https:&#x2F;&#x2F;tshop.r10s.com&#x2F;322&#x2F;3c2&#x2F;9324&#x2F;8b0f&#x2F;90b6&#x2F;8b72&#x2F;2e99&#x2F;1106eabd320242ac110003.jpg&quot;&gt;
    &lt;div class=&quot;content&quot;&gt;
        &lt;h2&gt;專注力協定&lt;&#x2F;h2&gt;
        &lt;h3&gt;史丹佛教授教你消除逃避心理，自然而然變專注。&lt;&#x2F;h3&gt;
    &lt;&#x2F;div&gt;
&lt;&#x2F;a&gt;
&lt;h2 id=&quot;zi-xu&quot;&gt;自序&lt;&#x2F;h2&gt;
&lt;p&gt;未來的世界會有兩種人：一種是讓自己的注意力和生活都被他人所控制和綁架，另一種人則是可以驕傲地說自己心無旁騖，意思就是努力讓自己做到言出必行。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;di-yi-bu-guan-li-nei-zai-you-yin&quot;&gt;第一部 管理內在誘因&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第三章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
以為自己是在追求愉悅感，但真正驅動自己的，其實是「從痛苦中，獲得解放的欲望」，除非把分心的根本原因給解決，不然自己會持續找到方法，讓自己分心，唯有了解自己的痛苦，才能開始控制分心，並且找出更好的辦法，來處理因分心，產生偏離自我目標的行為。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第四章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
如果滿足和愉悅是永久存在的，那麼持續追尋更大利益或進步的動機，就會變得相當低。&lt;br &#x2F;&gt;
無聊：社交互動。&lt;br &#x2F;&gt;
消極性偏差：比起回憶美好時光，回憶不快樂時光，比較容易。&lt;br &#x2F;&gt;
反芻思維：一直去反覆回想不好的經驗、別人對自己做的事、某個自己沒有，但是很想要的東西。&lt;br &#x2F;&gt;
享樂適應：曾經感到開心的事，最終都不再有魅力（買新衣服）。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第五章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
心理上的禁欲，會適得其反，學習去覺察，並接受自己的渴望，學著用健康的方法來處理（處理潛意識裡，長期被忽略，需要被滿足的渴望）。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第六章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
察覺內在誘因的重新構思&lt;br &#x2F;&gt;
步驟一：尋找開始分心之前的不適感：感到焦慮、有種癮頭、覺得不安、認為自己不夠格。&lt;br &#x2F;&gt;
步驟二：把誘因寫下來：自己正試著去拿手機。&lt;br &#x2F;&gt;
步驟三：探索自己的感受：當自己即將要分心時，心裡在想什麼？也許是一段記憶、一個煩惱，接著讓這些經驗，順著水流往下漂，跟著捲入水中消失，而自己只是坐在一旁觀看。&lt;br &#x2F;&gt;
步驟四：提防交界點：自己是否曾經在等紅燈時，拿起手機？藉由一下下就好，因此讓自己後悔（因為使用手機，而浪費了自己寶貴的半小時）。&lt;br &#x2F;&gt;
十分鐘規則：發現自己因為分心，做出偏離自我目標的行為，好讓自己感到平靜，告訴自己：「可以投降，沒關係，但不是現在，再等十分鐘就好」。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第七章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
任何困難任務，都可以有玩的成分，即便不一定都是愉快的，與其試圖逃離痛苦，或是使用像是獎品之類的回饋，來提升自己的動機，真正的重點，在於對任務投以極度的專注，而找到之前沒發現的挑戰，這些新的挑戰提供了新鮮感（無聊的解藥就是好奇心），吸引了自己的注意力，並且讓自己在受到分心、誘惑之時，保持專注。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第八章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
如果自己發現，自己會聽從腦袋裡的聲音（尤其是負面想法），並讓這聲音霸凌自己，那麼知道如何去回應便尤其重要！與其接受這個聲音，不如提醒自己，阻礙是成長過程的一部分，不練習的話就無法進步，而練習有時候會是相當困難的。&lt;br &#x2F;&gt;
當認定自己的意志力會消耗殆盡，便會讓自己更難去達成目標，因為在自己其實還有辦法堅持的時候，這種想法提供了一個放棄的藉口。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;di-er-bu-ru-he-ti-zi-ji-de-mu-biao-teng-chu-shi-jian&quot;&gt;第二部 如何替自己的目標，騰出時間？&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第九章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
想想看？所有別人偷走自己時間的方法？運用三個生活領域：自己、人際關係、工作，來檢討自己的時間都花在哪裡？讓自己去思考，自己是如何計畫一天中的時間？&lt;br &#x2F;&gt;
設定執行意圖（自己想要成為什麼樣子？想要支持些什麼？想要跟周圍的世界，產生怎麼樣的連結？）：決定你要做「什麼」？、「什麼時候」要去做？定期改善行事曆，一旦排定了，就要執行。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第十章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
自己會出現的時候，真的會到場，行事曆需要去維護、關懷，然而這需要時間，就像自己不會在開會的時候，放老闆鴿子。當遇到無法依計畫行事，立刻開始用比較健康的方式來處理，像是不要再去擔心「花費時間之後，無法控制的產出和結果」，而是去專注在自己能夠掌控的，也就是「要投入多少時間？」，投入比起產出要來得確實得多，要過自己心目中理想的生活，要明確地知道，自己唯一應該專注的事情，是把時間分配給「能夠實踐自己價值觀」的事物。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第十一章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
自己重視的人，不應該獲得剩餘的時間（友誼是「餓死」的），擁有讓人感到滿足的友誼，需要三樣東西：有人聽自己說話、讓自己依賴、讓自己樂於相處。&lt;br &#x2F;&gt;
擁有一個話題，對雙方都有助益：其一、讓雙方都跳過天氣這種閒聊，並且有機會敞開來談一些更重要的事情。其二、讓人不會按照男女分成兩組人，這在夫妻聚會中經常發生。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第十二章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
讓時間規劃能夠同步，無論是跟家人、老闆，定期進行來確認彼此對時間分配的方式，有同樣的期待。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;di-san-bu-dui-fu-wai-zai-you-yin&quot;&gt;第三部 對付外在誘因&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第十三章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
科技誘因，是在替自己服務？還是自己在替它服務？應該要替自己服務，而不是顛倒過來，反客為主。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第十四章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
干擾會導致錯誤，不想被打擾時，要發出訊號，比如用螢幕標示，讓大家知道自己現在心無旁騖。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第十五章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
設立辦公時間（免去因立即要回信，而浪費時間）：透過要求對方等待，自己等於是「給對方自己找到答案的機會」。&lt;br &#x2F;&gt;
推遲發送郵件：是誰規定？每封郵件，一寫好就要寄出？&lt;br &#x2F;&gt;
刪除不需要的訊息：比如「取消訂閱」。&lt;br &#x2F;&gt;
減少每封信，所花費的時間（規定自己一則訊息，只能碰兩次）：將需要回覆的郵件，貼上標籤，用自己行事曆上，安排好的時間，來回覆這些郵件。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第十六章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
現在、立刻、馬上回覆，應該是個例外狀況，而非一個規則：&lt;br &#x2F;&gt;
規則一、把對話群組當成三溫暖，待一會就出來，也可以在對話群組裡面，安排小組會議時間，會是減少實體會議的好方法。&lt;br &#x2F;&gt;
規則二、在自己一天的時間裡，安排時間跟進這些對話群組裡的內容。&lt;br &#x2F;&gt;
規則三、被加入對話群組的人，要精挑細選，不要把每個人都拉進去，群組愈小愈好。&lt;br &#x2F;&gt;
規則四、在討論敏感議題，最好避免使用聊天群組，要記得！直接觀察對方的心情、語氣、非語言訊號，這些能力替溝通提供了，很重要的背景資訊。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第十七章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
人常用開會的方式，逃避自己付出努力，獨立解決問題：組織這場會議的人，必須要對討論的問題，提前送議程，再者，也必須盡全力，先提出一個解決方案，用簡報或摘要的方式做出來，一到兩頁即可，摘要裡面要包含，提出的問題、想法、建議。&lt;br &#x2F;&gt;
進行腦力激盪、集思廣益：必須進行超過兩個人以上的會議。&lt;br &#x2F;&gt;
公開論壇：傾聽員工訴說各自的憂慮。&lt;br &#x2F;&gt;
商業上的挑戰分享和各自獨到的見解：可以透過電子郵件，寄給該負責的利害關係人。&lt;br &#x2F;&gt;
先思考，在開會：會前，先給每個人獨自進行腦力激盪，在聚在一起討論。&lt;br &#x2F;&gt;
精心挑選與會人士：確保自己可以在進入會議後，迅速脫身。&lt;br &#x2F;&gt;
一場會議，一台筆電：避免大家在會議中使用裝置（手機），來逃避會議裡單調、無聊的感覺。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第十八章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
App&lt;br &#x2F;&gt;
步驟一：與自己價值觀不符的 app，解除安裝。&lt;br &#x2F;&gt;
步驟二：移除自己喜愛的  app，在行事曆上面，留出時間來使用社群媒體，而不是讓 app 隨心所欲，發出叮咚聲。&lt;br &#x2F;&gt;
步驟三：重新排列 app。&lt;br &#x2F;&gt;
步驟四：到「設定」裡面，選擇「通知」選項，像是「勿擾模式」、「駕駛中請勿打擾」。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第十九章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
自己最怕的事情，常常就是自己最需要去做的事情。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第二十章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
合併不同的事情：健身時，聽取文章。進行散步會議。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第二十一章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
把讓人分心的影片、縮圖、廣告給移除。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;di-si-bu-yong-xie-ding-lai-yu-fang-fen-xin&quot;&gt;第四部 用協定來預防分心&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第二十三章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
費力協定：提高多餘行為的難度（比如收起手機。），並且跟朋友或同事約定在設定好的時間內，只能看書和工作。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第二十四章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
代價協定（偷懶，就燒鈔票）：人比起追求收益，有更高的動力去迴避損失。&lt;br &#x2F;&gt;
使用時要注意：陷阱一：外在誘因關掉之後，才適用。陷阱二：用在短期任務，如果綁住的時間太長，會跟處罰聯想在一起。陷阱三：先預期自己會遇到惶恐不好的情緒，但無論如何就去做吧！陷阱四：不適合會過分苛求自己的人（完美主義者）。&lt;br &#x2F;&gt;
人要知道怎麼從失敗中重新振作？面對挫折，要用同理心對待自己，而不是自我批評，以上是讓自己回歸常軌的方法。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第二十五章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
身份協定：「我不能」連結到的是行為，「我不會」跟本身自己有關，認為自己是誰？會影響自己做出哪些事？另外，增強身分認同的方法，是透過儀式，用信徒般的紀律，專心進行任何自己想做的事情。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;di-wu-bu-rang-zhi-chang-bian-de-xin-wu-pang-wu&quot;&gt;第五部 讓職場變得心無旁騖&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第二十六章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
跟心理健康，沒有太大的連帶關係，是組員之間合作的方式、社會支持（他人的關心或協助）、工作安全，相反的，科技打造出惡性的應答文化，是員工得到憂鬱症的成因，像是第一個：每日讀不完的電子郵件、工作，不斷湧入，而感到高度的工作壓力。第二個：付出和回饋失衡。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第二十七章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
員工對自己的時間表，缺乏控制權，是離開公司的主要原因，所以，給予員工自行釐清「工作天要怎麼規劃？才能達成工作目標。」，這點很重要。另外，為員工打造出心理安全的方法，是提供一個安全的場合，讓員工可以公開談論、討論各種議題，並讓員工相信，不會因為說出想法、問題、憂慮、錯誤，而被羞辱或被處罰。&lt;br &#x2F;&gt;
主管本身，要如何創造自己的心理安全感？步驟一：把工作，視為一種學習上的問題，而不是執行上的問題。步驟二：主管承認自己也會犯錯。步驟三：崇尚好奇心，大量地提問。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;di-liu-bu-pei-yang-xin-wu-pang-wu-de-hai-zi&quot;&gt;第六部：培養心無旁騖的孩子&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第二十九章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
父母不樂見孩子的行為，會歸咎於其他事物（比如電玩），另外一個推卸責任的藉口，就是叛逆，以上簡易的答案給了父母可以逃避，不去看孩子背後黑暗且複雜的真相，不去看孩子出現這些行為，背後真正的原因？孩子會花費大量時間上網，也許是在生活中遇到問題（內心感受到孤獨和空虛）。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第三十章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
人的心靈，需要三樣東西，才能好好成長：&lt;br &#x2F;&gt;
自主權（自由權和決定權），當孩子放棄去控制自己的專注力，是因為一直都是大人在管的，也就是可以自主去探索興趣的需求並未獲得滿足（大人剝奪了孩子的玩耍時間！）。&lt;br &#x2F;&gt;
勝任感（精通、進步、成就、成長），孩子在學校，花時間在不享受的事情上，又感覺不到進步的潛力，到了晚上，孩子會寧願去做那些可以勝任的活動。&lt;br &#x2F;&gt;
歸屬感（對別人而言，自己是重要的、對自己而言，有重要的人）。&lt;br &#x2F;&gt;
大人愈常跟孩子討論，使用過多科技產品的代價，就愈是在跟孩子「一起」做決定，而不是大人「替」孩子做決定（大人跟孩子交流，讓孩子說就好，不要去批評孩子。）。&lt;br &#x2F;&gt;
大人可以從分享開始，讓孩子知道，在大人的生活中，為了管理分心問題，做出了哪些改變？讓孩子知道「當孩子面對挑戰時的不適感，大人都懂。」，幫助孩子建立「面對挫折的恢復能力」，而不是用分心的方式來逃避不適感。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第三十一章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
大人可以展現脆弱，並承認自己不知道所有問題的答案，都是一邊做，一邊找答案。&lt;br &#x2F;&gt;
讓孩子自己打造「一份符合自己價值觀的行事曆」，讓孩子向自己提問：「我的行為？對我而言？是有用的嗎？」、「我這樣子的行為？讓自己感到自豪嗎？」，好讓孩子自己監督自己和約束自己的行為。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第三十二章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
閱讀特定書籍、觀看暴力影片、開車、喝酒精飲料、使用數位裝置，每項活動，可以開始的時間不一，而且不是孩子說了就算，與其給孩子一支一應俱全的智慧型手機，最好是從一些只能打電話和傳簡訊的手機開始，並將這些發亮的螢幕放在「公共空間」，看看孩子是否會使用裝置上的設定？來關掉外在誘因。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第三十三章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
即便是年幼的孩子，都可以學會進行預先承諾，只要規則是孩子自己訂的，並且知道怎麼使用計時器，或是其他的方法來約束自己。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;di-qi-bu-da-zao-xin-wu-pang-wu-de-guan-xi&quot;&gt;第七部：打造心無旁騖的關係&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;第三十四章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
當自己的家人和朋友，都選擇看著螢幕而不是自己，自己要改變使用科技的習慣也就相當困難，再者，如果不刻意規劃時間和場所，來進行不受打擾的討論，所冒的風險就是喪失真正了解他人，並讓他人了解自己的機會，所以，當大家在一起的時候，把查看手機變成一項禁忌，並且準備幾個高明的說法，比如詢問「一切都還好嗎？」，來阻止朋友之間使用手機。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>看傳統醫學記</title>
        <published>2021-02-20T00:00:00+00:00</published>
        <updated>2021-02-20T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/experience-of-traditional-medicine/"/>
        <id>https://editor.leonh.space/2021/experience-of-traditional-medicine/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/experience-of-traditional-medicine/">&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;experience-of-traditional-medicine&#x2F;katherine-hanlon-QgcdtM9rA5s-unsplash.jpg&quot; alt=&quot;針灸&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：&lt;a href=&quot;https:&#x2F;&#x2F;unsplash.com&#x2F;photos&#x2F;QgcdtM9rA5s&quot;&gt;Katherine Hanlon&lt;&#x2F;a&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;過年期間左右手的上臂，特別是肩膀關節附近一直感到由內而外的疼痛感，左上臂的部份是老問題，舊傷一直沒有根治過，右上臂的疼痛則是來的莫名其妙，大概是姿勢不良或睡覺自己壓自己引起的吧，總之在過年期間兩手臂同時併發的痛感令人頗不好受，於是有了這次的看中醫記。&lt;&#x2F;p&gt;
&lt;p&gt;從下文開始將中醫正名為傳統醫學、西醫正名為現代醫學。&lt;&#x2F;p&gt;
&lt;p&gt;下面也會有個人對傳統醫學的觀點，把中醫的中（華）拿掉，相當於把文化情節加持拿掉，用更理性的態度談我等老百姓對傳統醫學的看法。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;在決定看傳統醫學之前，也是有查過其它的治療機構：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;最普通的健保給付的骨科或復健科附帶的紅外線或電療這類手段在以前就做過，確定是無效的，紅外線、電療只能視為是一種舒緩療法，它們無法對肌肉或關節傷部產生任何有意義的治癒效果。&lt;&#x2F;li&gt;
&lt;li&gt;傳統的國術館不敢去，我的身體不適合被粗暴的對待 &amp;gt;&#x2F;&#x2F;&#x2F;&amp;lt;。&lt;&#x2F;li&gt;
&lt;li&gt;新的整復、推拿、整脊機構，這類較傳統國術館更為可靠一點，也有許多證照，但那些發照機構是不是足夠嚴謹以及有公信力是令人懷疑的，頭銜拎瑯滿目令人眼花撩亂，又沒有足夠的心力去一一核實，因此雖然有證照加持，但這次還是先略過。&lt;&#x2F;li&gt;
&lt;li&gt;獨立開業的物理治療師，這原本是我最屬意的，但受限於台灣法規，物理治療師不能自行看診，必須透過其它醫師開立單據才可由物理治療師進行治療，也因此健保並不給付，自費的金額滿不公開透明，大約是一千至兩千，考慮到&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;being-started-2&#x2F;&quot;&gt;任職的公司正在轉型 😂&lt;&#x2F;a&gt;，因此也不適合多這筆花費。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在排除以上這些選項之後，傳統醫學是必然的選擇，選傳統醫學的另一個原因是為了解鎖體驗針灸的成就。&lt;&#x2F;p&gt;
&lt;p&gt;回到看診的情節，在經歷了兩個小時的枯等之後終於輪到我看診，診間的經歷與現代醫學的問診過程差不多，除了問答之外，醫師也要我轉動手臂讓他知道疼痛的位置及角度，較為不同的是多了把脈的部份，不知道是我皮太厚還是脈像太弱，把脈的時間比現代醫學用聽診器的時間還要長許多，看診完的結論是左手關節發炎、右手轉動肌發炎，這樣的結論令人感到安心，理由是傳統醫學還是有用更為嚴謹的說法在解釋病況，而不是賣藥式的用「氣血不順」之類模凌兩可的詞彙來唬弄病人。（如果我愛聽的話去參加&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.rulai.org&#x2F;&quot;&gt;業力引爆神教&lt;&#x2F;a&gt;的儀式就好了不用來看醫生）&lt;&#x2F;p&gt;
&lt;p&gt;結束問診的行程後，開始做藥草蒸氣噴噴行程，這部份當然也只能視為安慰療法，雖然蒸氣的溫度頗宜人但這股氣流終究對傷部終究是無療效的。&lt;&#x2F;p&gt;
&lt;p&gt;蒸氣噴完就是針灸體驗時間，初次被下針的我，感受並不強烈，只有極輕微的酸麻感，甚至入針也都沒有感覺，跟被蚊子叮的感覺真的差不多，至於療效…不可考，畢竟針灸不是立即見效的治療手法，在寫這篇文章的當下已經是針灸完兩天後，目前我傾向認為是有效的，但我也不確定這樣的有效是來自於身體的自癒還是來自針灸的效果。&lt;&#x2F;p&gt;
&lt;p&gt;針灸後在針還插在上臂的狀態下，接著的是電療，這部份和現代醫學的電療一模一樣，舒緩兼拉動深層肌肉而已。&lt;&#x2F;p&gt;
&lt;p&gt;最後談談我對針灸的看法，身為一個老百姓，其實我們也並沒有要嚴格的追求明確的療效，上文中多次談到舒緩、安慰療法等字眼，但本人並不排斥這種治療，只要是合法也合乎醫學常識的治療，即便只是安慰，對病人都是有幫助的，就像我心裡難過的時候尋求的也只是有人陪伴而已，不用一定要有什麼積極做為，不用。&lt;&#x2F;p&gt;
&lt;p&gt;但反之，明明骨子裡是安慰劑，但卻瞞稱是高級療法的欺騙行為，那就應該被無情的揭露。&lt;&#x2F;p&gt;
&lt;p&gt;關於針灸的科學驗證，還可以參考&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E4%B8%AD%E5%8C%BB%E8%8D%AF%E7%9A%84%E7%A7%91%E5%AD%A6%E6%80%A7%E9%97%AE%E9%A2%98&quot;&gt;維基百科的條目&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>閱讀流水帳──&lt;br&gt;《別讓下意識騙了你》</title>
        <published>2021-02-19T00:00:00+00:00</published>
        <updated>2021-02-19T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/jinsei-o-seiko-ni-michibiku-muishiki-o-totonoeru-gijutsu/"/>
        <id>https://editor.leonh.space/2021/jinsei-o-seiko-ni-michibiku-muishiki-o-totonoeru-gijutsu/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/jinsei-o-seiko-ni-michibiku-muishiki-o-totonoeru-gijutsu/">&lt;a class=&quot;rakuten&quot; href=&quot;https:&#x2F;&#x2F;affiliate.api.rakuten.com.tw&#x2F;redirect?nw=tw&amp;site=afl&amp;a=d83123c36c3538761bafc2d95b5d1e47474b5dfff9bc19de5f7aeac8eaec1549d61f8840ac9865e6&amp;ar=a747b10f014cf18584f9ee2ca5516854cb91d9c5d6849f5c93000e3163fa586b69315791463a07d2&amp;cs=85f4cffdd6ba85144cee6d73a7982f69&amp;pr=e57594b8443d389c&amp;ap=pr%3De57594b8443d389c&amp;e=1&amp;url=https%3A%2F%2Fwww.rakuten.com.tw%2Fshop%2Frbook%2Fproduct%2F2011771185955%3Fscid%3Drafp-&quot;&gt;
    &lt;img loading=&quot;lazy&quot; src=&quot;https:&#x2F;&#x2F;tshop.r10s.com&#x2F;cea&#x2F;277&#x2F;7291&#x2F;ad09&#x2F;b05a&#x2F;072e&#x2F;9be5&#x2F;11cde88196c4544488dc07.jpg&quot;&gt;
    &lt;div class=&quot;content&quot;&gt;
        &lt;h2&gt;別讓下意識騙了你&lt;&#x2F;h2&gt;
        &lt;h3&gt;人真的是按自己的想法而行動嗎？比起靠意志力改變人生，整理下意識更容易。&lt;&#x2F;h3&gt;
    &lt;&#x2F;div&gt;
&lt;&#x2F;a&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;前言&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
一概而論和唬人心理學：用一句話馬馬虎虎的解釋事實（心理測驗）。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第一章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
胖瘦會因為錯覺而產生變化，但是，魅力程度卻不會受到影響。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第二章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
想讓對方喜歡自己？不讓對方察覺的模仿，像是回答：「原來如此」。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第三章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
基礎動作熟到不讓腦部費力，以達到節省能量的目標（踢足球）。&lt;br &#x2F;&gt;
簡短的句子，都捨去了某些事實（星座個性分析）。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第四章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
捨棄負面想法，其實是矯枉過正，實際上是正、負面想法，沒有得到平衡，而造成的結果。&lt;br &#x2F;&gt;
先用心思考自己的個性，再勇敢面對自己的弱點，或許就可以掌握自己的特色。&lt;br &#x2F;&gt;
不是「想成為什麼？」，先弄清楚「想做什麼？」。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第五章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
觀眾有欲望或需求時，潛意識鏡頭才有效果。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第六章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
誤信和迷信來自無知。
「確認偏誤」：人不會客觀的從科學角度改變想法，因為人堅信，自己的信念是正確的。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第七章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
人生有 99%，都是活在成見裡，如何擺脫？就是稍微停下腳步，傾聽他人說話，並思考的人。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第八章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
記憶非常不可靠，人會視自己的方便，改變記憶，會選擇對自己有利的資訊，然後積極遺忘不利的訊息。&lt;br &#x2F;&gt;
人對於好像合理的事情，完全沒有招架之力，就算是不可能發生的事情，只要好像合理，大腦就會捏造記憶，使人誤以為某事是真實的（宗教騙色），所以商量反而是假記憶的根源（算命），容易對事物原有的想法，產生動搖（被誤導），就算當下自己想要理性的否定對方，並不容易，所以就漸漸助長假記憶成形。&lt;br &#x2F;&gt;
人的注意力是有限的，當大部分的注意力都被特定事物占去（煩惱），就無法再注意其他的事情。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第九章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
心理學是個尋求平均值的科學，如果硬生生的把平均值的結論套用在自己身上，無疑是在窄化自己的視野。&lt;br &#x2F;&gt;
裝什麼表情？就會產生什麼情緒。&lt;br &#x2F;&gt;
與其說人是按照自己的想法，開闢人生，倒不如說是周遭環境，讓自己自動產生所有的動作，人在無意間，被別人操弄心情，想要隨自己的想法轉換情緒，似乎十分困難？如同落葉落地，並不是落葉自己想要飄落的，而是因為環境變化所造成的，與其設法用意志力控制人生，不如巧妙整理自己的潛意識。&lt;br &#x2F;&gt;
可以前往未來，但回不到過去。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;第十章&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
心理學的「抽樣偏差」，不是捏造，只是無法再現。&lt;br &#x2F;&gt;
不要盲目相信科學論文，是否要相信這些數據或資料，都由自己決定。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>不要用 Teams 的理由</title>
        <published>2021-02-17T00:00:00+00:00</published>
        <updated>2021-02-17T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/teams/"/>
        <id>https://editor.leonh.space/2021/teams/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/teams/">&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;teams&#x2F;RE4Mzn9.jpeg&quot; alt=&quot;Microsoft Teams&quot; &#x2F;&gt;
&lt;figurecaption&gt;（圖片來源：&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.microsoft.com&#x2F;zh-tw&#x2F;microsoft-teams&#x2F;group-chat-software&quot;&gt;微軟&lt;&#x2F;a&gt;）&lt;&#x2F;figurecaption&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;Microsoft Teams 是微軟這幾年力推的群組應用，主要的對手是 Slack，說「群組應用」好像很籠統，實際上也是如此，Teams 主要是個群組聊天應用，並且在對話內還可以插入一大堆東西，比較典型的是檔案、文件、連結等等，另外還有像 Slack 一樣的「頻道」概念，頻道就相當於 LINE 的群組聊天室，而頻道之上還有「團隊」，一個團隊可以擁有多個頻道，「團隊」就相當於企業內的部門或專案部門；「頻道」則是對應到部門內的常態事務或是專案個案。&lt;&#x2F;p&gt;
&lt;p&gt;以上是 Microsoft Teams 的簡單介紹，文字上看不明白的地方可以實際去用看看，微軟目前在力推 Teams，也包括了為一般用戶提供的免費版本，而我所任職的公司則是買了 Microsoft 365 的套餐，接下來要開始講 Teams 的壞話了，微軟鐵粉可以左轉離開。:p&lt;&#x2F;p&gt;
&lt;h2 id=&quot;teams-yu-sharepoint-de-ai-hen-jiu-ge&quot;&gt;Teams 與 SharePoint 的愛恨糾葛&lt;&#x2F;h2&gt;
&lt;p&gt;在 Teams 內的團隊和頻道，一旦在 Teams 內建立了，在 SharePoint 內也會有產生相對應的「網站」，但這樣的對應關係並不完美，例如：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;在 Teams 內的聊天（貼文）內容，並不會出現在 SharePoint 內。&lt;&#x2F;li&gt;
&lt;li&gt;Teams 的 Wiki 的內容，也不會出現在 SharePoint 內（實際上是有的，但連結相當隱晦）。&lt;&#x2F;li&gt;
&lt;li&gt;Teams 的頻道一旦改名，SharePoint 內的資料夾並不會跟著改名，但兩者間的連結關係還是在的，這導致了如果你是從 SharePoint 的「網站」進入找檔案，你會很容易迷路。&lt;&#x2F;li&gt;
&lt;li&gt;承上，相反的，如果是在 SharePoint 把資料夾改名，則會破壞資料夾和 Teams 頻道的連結關係。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;總的來說，Teams 是個乍看很像 Slack 的應用，但底層的架構和 SharePoint 高度耦合的產品，它們共用了相同的底層架構，但在應用面上又各有設計，因此才會得到上面幾個枚舉的問題，兩個產品的功能有時候有相連，有時候又各自獨立，而這令用戶感到相當困惑。&lt;&#x2F;p&gt;
&lt;p&gt;在微軟力推 Teams 以前，微軟是主推 SharePoint 的，把 SharePoint 當成一切專案的入口網站，但 Teams 橫空出世後 SharePoint 的主角光環就被弱化，這樣的後遺症就是對習慣 Teams 的人來說，我不確定某些在 Teams 的操作會不會把 SharePoint 的東西弄亂掉，或者反過來對習慣 SharePoint 的人來說，我不確定在 SharePoint 的操作會不會把 Teams 弄亂掉，總之這樣高度耦合的產品給用戶的感受就是「亂」。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gong-neng-shang-de-que-shi&quot;&gt;功能上的缺失&lt;&#x2F;h2&gt;
&lt;p&gt;除了上面 Teams 與 SharePoint 間混亂的關係外，Teams 本身的部份功能也是半殘的：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Wiki 的文件不能下載也不能匯出，實際上 Wiki 是以 MHT 格式儲存的，技術上應該可以下載也可以匯出，但實際上 Teams 內並沒有提供下載與匯出的功能，真的要下載得要去 SharePoint 的「網站內容」內下載，Teams 的 Wiki 和 SharePoint 的網站內容，這兩者在應用面上是完全想像不到關聯性的。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;teams-yu-skype-kaizala-de-ding-wei-zhong-die&quot;&gt;Teams 與 Skype、Kaizala 的定位重疊&lt;&#x2F;h2&gt;
&lt;p&gt;除了上面提到的 Teams 與 SharePoint 之間混亂的關係外，Teams 的群組和聊天的特性與 Skype、Kaizala 也是高度重疊的，特別是 Teams 和 Kaizala，兩者的定位都是群組溝通應用，而且也都是 Microsoft 365 套餐的一部分，相較於 Teams，Kaizala 是微軟印度團隊的產品，在底層架構上不像 Teams 那樣和 SharePoint 共享部份結構，更像是一個獨立的新創產品，用起來也更接近 IM app 的輕巧感，個人感覺是 Kaizala 的使用體驗較接近 LINE、WhatsApp，也因此有比較低的適應曲線，但嫡系的 Teams 才是微軟的全球化戰略產品，因此在行銷資源上 Teams 還是佔有大部分，不過這兩者定位終究是太過接近，對 Microsoft 365 的套餐用戶來說，一樣會感受到「不知道要用哪個」的選擇性障礙。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-yu&quot;&gt;結語&lt;&#x2F;h2&gt;
&lt;p&gt;總結一下 Teams 的問題：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;與 SharePoint 共享架構，但又做半套，Teams 出現後 SharePoint 的市場定位變得尷尬，兩者資料間奧妙的關聯性問題令使用者感到混亂且挫折。&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;
&lt;p&gt;與 Kaizala 的定位重疊，群組究竟是要開在 Teams 還是 Kaizala？這個問題同樣的帶給使用者疑惑。（最後還是開在 LINE？！）&lt;&#x2F;p&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;最後推薦我個人的選擇，如果您已經用過 Slack，那就留在 Slack，如果您較習慣用 IM 型的應用，那 LINE 或 &lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;line-works&#x2F;&quot;&gt;LINE WORKS&lt;&#x2F;a&gt; 都是很好的選擇，如果您原本是用 SharePoint，那可以考慮改用 Kaizala，總之不要碰 Teams，您的生活會愉快許多。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Orator ORM 的 Seeding 機制</title>
        <published>2021-02-16T00:00:00+00:00</published>
        <updated>2021-02-16T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/seeding-in-orator-orm/"/>
        <id>https://editor.leonh.space/2021/seeding-in-orator-orm/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/seeding-in-orator-orm/">&lt;p style=&quot;background-color: hsla(0, 0%, 96%, 1.0); padding: 1rem; text-align: center;&quot;&gt;
  &lt;a href=&quot;..&#x2F;orator-orm-introduction&#x2F;Orator-Demo.zip&quot;&gt;本文範例原始碼下載&lt;&#x2F;a&gt;
&lt;&#x2F;p&gt;
&lt;p&gt;之前寫過一篇〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;orator-orm-introduction&#x2F;&quot;&gt;初探 Orator ORM&lt;&#x2F;a&gt;〉，有介紹了 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;orator-orm.com&#x2F;&quot;&gt;Orator ORM&lt;&#x2F;a&gt; 的基礎操作，而此篇文章就專門介紹 Orator ORM 的 seeding 機制，本篇的範例也都延續自〈初探 Orator ORM〉，還沒讀過的朋友請趕快手刀點擊閱讀。&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;seeding-in-orator-orm&#x2F;1__XamCzUkqNDFPSbI9e9Cqw.jpeg&quot; alt=&quot;阿湯哥手刀狂奔&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：&lt;a href=&quot;https:&#x2F;&#x2F;ent.ltn.com.tw&#x2F;news&#x2F;breakingnews&#x2F;1386824&quot;&gt;自由娛樂&lt;&#x2F;a&gt;&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;seeding&quot;&gt;Seeding&lt;&#x2F;h2&gt;
&lt;p&gt;Seeding 用於在資料庫內建出一堆資料，這些資料可以是用於資料庫初始化的資料，例如郵遞區號，也可以是用於測試的假資料，而 Orator ORM 使用的是 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;joke2k&#x2F;faker&quot;&gt;Faker&lt;&#x2F;a&gt; 套件當作假資料產生器。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;orator-orm-de-seeding-cao-zuo&quot;&gt;Orator ORM 的 Seeding 操作&lt;&#x2F;h2&gt;
&lt;p&gt;再次回顧一下在〈初探 Orator ORM〉文中的專案結構：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;project1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── app.db&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── oratordemo&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│   ├── database.py&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│   ├── __init__.py&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│   ├── migrations&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│   │   └── __init__.p&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│   └── models&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│        └── __init__.py&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── poetry.lock&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;└── pyproject.toml&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;分別有放 migration 腳本檔的 migrations&#x2F; 和放 model 腳本檔的 models&#x2F;，理所當然的 seeding 腳本就會放在 seeds&#x2F; 裡面，因此產生過 seeding 腳本之後的目錄結構就會長這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;project1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── app.db&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── oratordemo&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│   ├── __init__.py&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│   ├── database.py&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│   ├── config&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│   │   └── __init__.py&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│   ├── migrations&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│   │   └── __init__.py&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│   ├── models&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│   │   └── __init__.py&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│   └── seeds&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│        └── __init__.py&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── poetry.lock&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;└── pyproject.toml&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;與 migrations&#x2F; 和 models&#x2F; 一樣，這個 seeds&#x2F; 不用手動建立，Orator ORM 會替我們代勞。&lt;&#x2F;p&gt;
&lt;p&gt;另一個新的 config&#x2F; 則是放 model factory 腳本的地方，後面會再提到。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;jian-li-seeding-jiao-ben&quot;&gt;建立 Seeding 腳本&lt;&#x2F;h3&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;porject1&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; ~&#x2F;project1&#x2F;oratordemo&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; orator make:seed user_table_seeder&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;database_seeder&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; created successfully.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;user_table_seeder&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; created successfully.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;進去 seeds&#x2F; 看一下，注意到除了預期的 user_&lt;wbr&gt;table_&lt;wbr&gt;seeder.py 外，Orator ORM 還另外建了個 database_&lt;wbr&gt;seeder.py，這個 database_&lt;wbr&gt;seeder.py 只會建立一次，關於這個檔案的作用我們後面會再提到，先關心 user_&lt;wbr&gt;table_&lt;wbr&gt;seeder.py：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; orator.seeds&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Seeder&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; UserTableSeeder&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Seeder&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; run&lt;&#x2F;span&gt;&lt;span&gt;(self):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        Run the database seeds.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        pass&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;裡面只有一個空的 &lt;code&gt;run()&lt;&#x2F;code&gt; 函式，這個函式就是在跑 seeding 時會被呼叫的函式，函式內放的就是建立 User 資料的敘述，但架構上我們會把實際產生假資料的邏輯抽離，在這邊我們引入 model factory 的概念，顧名思義，model factory 就是實際產生假資料的「工廠」，而 xxx_&lt;wbr&gt;seeder.py 則負責向工廠下單。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ding-yi-model-factory&quot;&gt;定義 Model Factory&lt;&#x2F;h3&gt;
&lt;p&gt;建立 oratordemo&#x2F;&lt;wbr&gt;config&#x2F;&lt;wbr&gt;factories.py 檔案：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; orator.orm&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Factory&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; oratordemo.models.user&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; User&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;factory&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; Factory()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;@factory.define&lt;&#x2F;span&gt;&lt;span&gt;(User)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; users_factory&lt;&#x2F;span&gt;&lt;span&gt;(faker):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;#39;name&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;: faker.name(),&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;#39;age&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;: faker.random_int(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;min&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt; max&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;115&lt;&#x2F;span&gt;&lt;span&gt;),&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;#39;birthday&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;: faker.date_of_birth(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;minimum_age&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt; maximum_age&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;115&lt;&#x2F;span&gt;&lt;span&gt;).strftime(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;%Y-%m-&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;%d&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這邊會帶入一個 Orator ORM 自行產生的 Faker 物件，而 Faker 的用法也頗直覺。Faker 可以產出的假資料有許多種類，可以閱讀 Faker 的文件了解。&lt;&#x2F;p&gt;
&lt;p&gt;有了 model factory，我們回到 seeder 寫向工廠下單的部份。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;hu-jiao-model-factory&quot;&gt;呼叫 Model Factory&lt;&#x2F;h3&gt;
&lt;p&gt;把 user_&lt;wbr&gt;table_&lt;wbr&gt;seeder.py 加上呼叫 model factory 的部份：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; orator.seeds&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Seeder&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; oratordemo.config.factories&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; factory&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; oratordemo.models.user&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; User&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; UserTableSeeder&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Seeder&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    factory&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; factory&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; run&lt;&#x2F;span&gt;&lt;span&gt;(self):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        Run the database seeds.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        self&lt;&#x2F;span&gt;&lt;span&gt;.factory(User,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 50&lt;&#x2F;span&gt;&lt;span&gt;).create()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;從上面可以看出 seeder 和 model factory 的分工，model factory 負責實際的生產邏輯，而 seeder 負責向 model factory 下訂數量。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zhi-xing-seeder&quot;&gt;執行 Seeder&lt;&#x2F;h3&gt;
&lt;p&gt;若要執行特定的 seeder，執行這樣的命令：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;project1&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; ~&#x2F;project1&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; env PYTHONPATH=&amp;#39;&#x2F;home&#x2F;leon&#x2F;project1&amp;#39; orator db:seed&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --config=oratordemo&#x2F;database.py --path=oratordemo&#x2F;seeds&#x2F; --seeder=user_table_seeder&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Are&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; you sure you want to seed the database?:&lt;&#x2F;span&gt;&lt;span&gt;  (yes&#x2F;no) [no] yes&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Database&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; seeded!&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;會這麼落落長是有原因的，&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;sdispater&#x2F;orator&#x2F;issues&#x2F;41&quot;&gt;Orator ORM 在跑 seeder 內如果有 &lt;code&gt;import&lt;&#x2F;code&gt; 敘述會引發 &lt;code&gt;ModuleNotFoundError&lt;&#x2F;code&gt; 這個問題&lt;&#x2F;a&gt;，因此必須多&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;ithelp.ithome.com.tw&#x2F;articles&#x2F;10196901&quot;&gt;加一個環境變數&lt;&#x2F;a&gt;讓 Python 能找到我們要 import 的模組。&lt;&#x2F;p&gt;
&lt;p&gt;若要執行一連串的 seeder，則可以把要跑的 seeder 定義在 database_&lt;wbr&gt;seeder.py 內，這個檔案在初次建立 seeder 時會一併建立，目前還是空的，把它加上 &lt;code&gt;call()&lt;&#x2F;code&gt; 敘述來加入要跑的 seeder 們：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; orator.seeds&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Seeder&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; oratordemo.seeds.user_table_seeder&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; UserTableSeeder&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; DatabaseSeeder&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Seeder&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; run&lt;&#x2F;span&gt;&lt;span&gt;(self):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        Run the database seeds.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        self&lt;&#x2F;span&gt;&lt;span&gt;.call(UserTableSeeder)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果前面跑 seeder 的命令，把指定特定 seeder 的參數拿掉，按照約定它就會去跑這支 database_&lt;wbr&gt;seeder.py：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;project1&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; ~&#x2F;project1&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; env PYTHONPATH=&amp;#39;&#x2F;home&#x2F;leon&#x2F;project1&amp;#39; orator db:seed&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --config=oratordemo&#x2F;database.py --path=oratordemo&#x2F;seeds&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Are&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; you sure you want to seed the database?:&lt;&#x2F;span&gt;&lt;span&gt;  (yes&#x2F;no) [no] yes&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Seeded:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; UserTableSeeder&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Database&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; seeded!&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;跑完可以去看一下資料庫，應該可以看到確實有 seeding 的資料在內。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-yu&quot;&gt;結語&lt;&#x2F;h2&gt;
&lt;p&gt;Orator ORM 沿襲了 ActiveRecord 的約定大於配置的風格，因此和 Laravel 的 Eloquent ORM 也有著類似的使用體驗，雖然在 Python 的世界 SQLAlchemy 由於歷史因素還是有較高的使用率，但對新專案來說 Orator ORM 是一個可以被認真考慮的選項。&lt;&#x2F;p&gt;
&lt;p&gt;覺得這篇文章對您有幫助請幫我們一鍵三連，拍手、訂閱、分享，小小的鼓勵是支撐我們分享的原動力，感謝讀完本文的您，也歡迎留下意見與我們交流。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>初探 Orator ORM</title>
        <published>2021-02-15T00:00:00+00:00</published>
        <updated>2021-02-15T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/orator-orm-introduction/"/>
        <id>https://editor.leonh.space/2021/orator-orm-introduction/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/orator-orm-introduction/">&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;orator-orm.com&#x2F;&quot;&gt;Orator&lt;&#x2F;a&gt; 是 Python 世界內的一套 ORM，介紹 Orator 前先簡單介紹 ORM，ORM 全稱 object-relational mapping，它是協助程式語言操作資料庫的中間人，ORM 把資料庫的 table 對應到程式內的 class，而 table 內的一筆紀錄則是對應到程式內的物件，紀錄內的欄位則是對應到物件的屬性，除了資料的對應外，其他增刪改動作也都有 ORM API 函式可以呼叫，如果 table 是有關聯性的，在程式內也可以設定 class 間的關聯性。&lt;&#x2F;p&gt;
&lt;p&gt;ORM 把資料庫的操作物件化後，在程式專案內就可以不用寫 SQL，可以以操作一般物件的方式去對待資料庫，有了 ORM 的基礎觀念之後，再引入 MVC 架構的 model 角色——model 是抽象化的概念，具體對應到的也是程式的 class，因此我們可以把 table、class、model 三者視為同一個對象，只不過在不同的面向上有不同的稱呼，table 是 class 與 model 真正在資料庫內被儲存的那面，class 是 table 與 model 在程式語言內操作邏輯那面，model 是 table 與 class 在應用面被人們談及的抽象的那面。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;orator-orm-de-te-se&quot;&gt;Orator ORM 的特色&lt;&#x2F;h2&gt;
&lt;p&gt;盤點 Python 的 ORM，最常見的應該是 SQLAlchemy ORM，相較於 SQLAlchemy ORM，Orator ORM 有一些特點：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;封裝更高階，以 session 管理為例，SQLAlchemy ORM 提供更多底層操作，而 Orator ORM 的 API 簡單許多。&lt;&#x2F;li&gt;
&lt;li&gt;約定大於配置，不僅 API 簡單，Orator ORM 在定義 model 上也很簡單，Orator ORM 致敬的對象是 Rails 的 ActiveRecord，因此包括 table name 等都是依照約定自動配置，不需要人工命名，此外 model 的屬性也是不用一一定義的，只要定義 model 間的關聯性就好。&lt;&#x2F;li&gt;
&lt;li&gt;不綁定框架，與 ActiveRecord 不同的是 Orator ORM 本身是獨立套件，可以與任何其他框架合作，沒有 ActiveRecord 和 Rails 高度耦合的問題。&lt;&#x2F;li&gt;
&lt;li&gt;支援 PostreSQL、MySQL、SQLite。&lt;&#x2F;li&gt;
&lt;li&gt;支援 migration、seeding 等操作，透過 Orator ORM 提供的 CLI 工具 &lt;code&gt;orator&lt;&#x2F;code&gt; 可以產出 migration 與 seeding 的空白模板檔，因為有 migration 腳本，所以資料庫 schema 的變更也都可以納入版控管理。&lt;&#x2F;li&gt;
&lt;li&gt;用的人少，少之又少，不過開發者也是 Poetry 的開發者，並且還有在持續維護中，所以可以安心使用，不用怕被放生。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;上面這些特色，未必都是優點，取決於用的人站在什麼角度看，就以 ORM 來說，趕著 time to market 的產品可能連 ORM 和 web 框架都不用，而用把兩者封裝的更高階的 &lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;strapi&#x2F;&quot;&gt;headless CMS&lt;&#x2F;a&gt;，或是連自架都放棄，採用像 Fauna 或 Backendless 這樣的 backend-as-a-service 服務。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;orator-orm-cao-zuo&quot;&gt;Orator ORM 操作&lt;&#x2F;h2&gt;
&lt;p style=&quot;background-color: hsla(0, 0%, 96%, 1.0); padding: 1rem; text-align: center;&quot;&gt;
  &lt;a href=&quot;Orator-Demo.zip&quot;&gt;本文範例原始碼下載&lt;&#x2F;a&gt;
&lt;&#x2F;p&gt;
&lt;p&gt;下面是 Orator ORM 的簡易操作，migration 和 seeding 的部分會用腳本檔執行，ORM 的操作會進入 Python REPL 內執行。&lt;&#x2F;p&gt;
&lt;p&gt;在開始前，先從準備環境開始。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;zhun-bei-huan-jing&quot;&gt;準備環境&lt;&#x2F;h3&gt;
&lt;p&gt;我們開一個新的空白 Python 環境 project1 來操作 Orator ORM，關於 Python 的環境建立可以參考另外一篇〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;python&#x2F;&quot;&gt;建置 Python 3 開發環境&lt;&#x2F;a&gt;〉。&lt;&#x2F;p&gt;
&lt;p&gt;project1 目前還是空的，不過為了避免在後面的過程中迷航，這邊先把目錄結構呈現出來：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;project1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── app.db&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── oratordemo&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│   ├── database.py&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│   ├── __init__.py&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│   ├── migrations&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│   │   └── __init__.p&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│   └── models&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│        └── __init__.py&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├── poetry.lock&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;└── pyproject.toml&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面的目錄結構不需要手動建立，會在下面的過程中逐步建立。&lt;&#x2F;p&gt;
&lt;p&gt;下面的終端機指令都會直接標示所在的位置，省略 &lt;code&gt;cd&lt;&#x2F;code&gt; 指令的部份。&lt;&#x2F;p&gt;
&lt;p&gt;在環境內裝上 Orator ORM：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;project1&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; ~&#x2F;project1&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; poetry add orator&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;（在這邊我們是用 Poetry 管理環境和套件，如果不是用 Poetry 的朋友請自行代換成對應的命令。）&lt;&#x2F;p&gt;
&lt;p&gt;裝完後在環境內應該就會有 &lt;code&gt;orator&lt;&#x2F;code&gt; 這個 CLI 程式可以用，先跑一下認識它：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;porject1&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; ~&#x2F;project1&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; orator&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Orator&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 0.9.9&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Usage:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  command&lt;&#x2F;span&gt;&lt;span&gt; [options] [arguments]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Options:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  -h,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --help&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;                      Display this help message&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  -q,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --quiet&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;                     Do not output any message&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  -V,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --version&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;                   Display this application version&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;      --ansi&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;                      Force ANSI output&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;      --no-ansi&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;                   Disable ANSI output&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  -n,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --no-interaction&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;            Do not ask any interactive question&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  -v&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;|&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;vv&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;|&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;vvv,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --verbose[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;VERBOSE]&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;  Increase the verbosity of messages:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; for normal output,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 2&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; for more verbose output and&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 3&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; for debug&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Available&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; commands:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  help&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;              Displays help for a command&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  list&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;              Lists commands&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  migrate&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;           Run the database migrations.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; db&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  db:seed&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;           Seed the database with records.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; make&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  make:migration&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    Create a new migration file.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  make:model&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        Creates a new Model class.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  make:seed&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;         Create a new seeder file.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; migrate&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  migrate:install&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;   Create the migration repository.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  migrate:refresh&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;   Reset and re-run all migrations.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  migrate:reset&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;     Rollback all database migrations.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  migrate:rollback&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;  Rollback the last database migration.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  migrate:status&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    Show a list of migrations up&#x2F;down.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到裡面的命令滿精簡也滿有條理的，大部分都可以望名生義。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;she-ding-zi-liao-ku-lian-xian&quot;&gt;設定資料庫連線&lt;&#x2F;h3&gt;
&lt;p&gt;如同大多數的資料庫工具，必須先定義要連線的資料庫，前面提過 Orator ORM 支援 PostreSQL、MySQL、SQLite 三種常見的開源資料庫，比較可惜的是並不支援 SQL Server 和 Firebird，這邊我們會用 SQLite。&lt;&#x2F;p&gt;
&lt;p&gt;在目前還是空白的專案資料夾內，建一個 oratordemo&#x2F;database.py，在裡面定義資料庫的連線參數：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; orator&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; DatabaseManager, Model&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;DATABASES&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;  &amp;#39;sqlite&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;driver&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;sqlite&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;#39;database&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;app.db&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;db&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; DatabaseManager(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;DATABASES&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Model.set_connection_resolver(db)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; BaseModel&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Model&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  pass&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;除了連線參數外，我們還引用了 orator 的 Model class，並設定它的連線參數，其他的 model 只要繼承 BaseModel 就能與資料庫連線了！但此時執行這個腳本是沒有任何效果與意義的，我們得先定義 model。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ding-yi-model&quot;&gt;定義 model&lt;&#x2F;h3&gt;
&lt;p&gt;先認識一下創建 model 定義腳本的命令：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;project1&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; ~&#x2F;project1&#x2F;oratordemo&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; orator make:model&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --help&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Usage:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  make:model&lt;&#x2F;span&gt;&lt;span&gt; [options] [--]&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; &amp;lt;&lt;&#x2F;span&gt;&lt;span&gt;name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Arguments:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;                            The name of the model to create.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Options:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  -m,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --migration&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;                 Create a new migration file for the model.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  -p,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --path=PATH&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;                 Path to models directory&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  -h,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --help&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;                      Display this help message&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  -q,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --quiet&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;                     Do not output any message&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  -V,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --version&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;                   Display this application version&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;      --ansi&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;                      Force ANSI output&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;      --no-ansi&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;                   Disable ANSI output&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  -n,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --no-interaction&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;            Do not ask any interactive question&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  -v&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;|&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;vv&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;|&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;vvv,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --verbose[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;VERBOSE]&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;  Increase the verbosity of messages:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; for normal output,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 2&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; for more verbose output and&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 3&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; for debug&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Help:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; Creates&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; a new Model class.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我們來定義一個 User 的 model：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;project1&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; ~&#x2F;project1&#x2F;oratordemo&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; orator make:model User&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --migration&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面這行會幫我們創建 project1&#x2F;&lt;wbr&gt;oratordemo&#x2F;&lt;wbr&gt;models&#x2F;&lt;wbr&gt;user.py 檔案：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; orator&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Model&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; User&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Model&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  pass&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在定義 User model 的屬性前，讓我們先關注另一個一起產生的 migration 檔案，因為前面的那行指令有加上 &lt;code&gt;--migration&lt;&#x2F;code&gt; 的參數，所以會一併產生 users table 的 migration 檔，在 project1&#x2F;&lt;wbr&gt;oratordemo&#x2F;&lt;wbr&gt;migrations&#x2F;&lt;wbr&gt;yyyy_&lt;wbr&gt;mm_&lt;wbr&gt;dd_&lt;wbr&gt;xxxxxx_&lt;wbr&gt;create_&lt;wbr&gt;users_&lt;wbr&gt;table.py：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; orator.migrations&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Migration&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; CreateUsersTable&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Migration&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; up&lt;&#x2F;span&gt;&lt;span&gt;(self):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    Run the migrations.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    with&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; self&lt;&#x2F;span&gt;&lt;span&gt;.schema.create(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;users&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; as&lt;&#x2F;span&gt;&lt;span&gt; table:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      table.increments(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;id&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      table.timestamps()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; down&lt;&#x2F;span&gt;&lt;span&gt;(self):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    Revert the migrations.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    self&lt;&#x2F;span&gt;&lt;span&gt;.schema.drop(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;users&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其中的 &lt;code&gt;up()&lt;&#x2F;code&gt; 和 &lt;code&gt;down()&lt;&#x2F;code&gt; 分別是 migration 檔案在建立與撤銷時會被呼叫的函式，這是 ORM migration 的重要特性，因為每個 migration 檔名都帶有時間戳，因此 ORM 是可以根據時間戳對資料庫的 schema 做建立與撤銷的，ORM 也會在資料庫內創建一個特殊的表格用於記錄整個資料庫的 migration 紀錄，而且這一切都是不用人為介入的。&lt;&#x2F;p&gt;
&lt;p&gt;在 migration 腳本裡面也可以看到已經有預放兩個欄位，第一個欄位是自動遞增的 &lt;code&gt;id&lt;&#x2F;code&gt;，第二行 &lt;code&gt;table.timestamps()&lt;&#x2F;code&gt; 會產生兩個欄位，分別是記錄創建時間的 &lt;code&gt;created_at&lt;&#x2F;code&gt; 和紀錄更新時間的 &lt;code&gt;updated_at&lt;&#x2F;code&gt;，這三個欄位也是 ORM 的特性，也是約定優於配置的表現之一，表格間透過 &lt;code&gt;id&lt;&#x2F;code&gt; 來建構關聯性，時間戳則是在程式邏輯內操作 model 物件時會自動打上那兩個時間戳。&lt;&#x2F;p&gt;
&lt;p&gt;在兩個欄位之外，我們再多加幾個欄位進去：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; orator.migrations&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Migration&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; CreateUsersTable&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Migration&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; up&lt;&#x2F;span&gt;&lt;span&gt;(self):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    Run the migrations.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    with&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; self&lt;&#x2F;span&gt;&lt;span&gt;.schema.create(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;users&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; as&lt;&#x2F;span&gt;&lt;span&gt; table:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      table.increments(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;id&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      table.timestamps()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      table.string(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;name&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      table.integer(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;age&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;).unsigned().nullable()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      table.date(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;birthday&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;).nullable()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; down&lt;&#x2F;span&gt;&lt;span&gt;(self):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    Revert the migrations.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    self&lt;&#x2F;span&gt;&lt;span&gt;.schema.drop(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;users&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;望文生義，上面分別指定了欄位的型態（字串、整數、日期）與額外的屬性（無正負號、可為空值），以及欄位的名稱等。&lt;&#x2F;p&gt;
&lt;p&gt;接著來跑一次 migration：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;project1&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; ~&#x2F;project1&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; orator migrate&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --config=oratordemo&#x2F;database.py --path=oratordemo&#x2F;migrations&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Are&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; you sure you want to proceed with the migration?&lt;&#x2F;span&gt;&lt;span&gt;  (yes&#x2F;no) [no] yes&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Migration&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; table created successfully&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[OK] Migrated 2021_01_28_093757_create_users_table&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;orator&lt;&#x2F;code&gt; 預設會去讀取我們指定的 database.py 內的 &lt;code&gt;DATABASE&lt;&#x2F;code&gt;，並將其認定為與資料庫的連線方式。
此時在專案資料夾 project1 內應該會建出剛剛定義的 app.db，裡面也應該會有我們定義的表格和欄位：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;orator-orm-introduction&#x2F;1_j2uoiy9ekaJhqLKfa2n1Gg.png&quot; alt=&quot;DBeaber&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;這邊可以看到 ORM 的另一個好處，ORM 會自動幫我們處理底層資料庫 datatype 支援度差異的問題，以 SQLite 為例，它僅支援有限的 datatype，例如在 migration 有定義一個日期欄位 &lt;code&gt;table.date(&#x27;birthday&#x27;)&lt;&#x2F;code&gt;，而 SQLite 並不支援日期這樣的 datatype，ORM 就會自動幫我們轉換成 TEXT 的 datatype。&lt;&#x2F;p&gt;
&lt;p&gt;除了我們定義的 users 外，Orator ORM 還自己建了個 migration table 來記錄這個資料庫的異動紀錄：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;orator-orm-introduction&#x2F;1_NiivB3iSK4v9MVhbL4ej8A.png&quot; alt=&quot;migrations 表&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;figure&gt;
&lt;h3 id=&quot;ding-yi-guan-lian-xing&quot;&gt;定義關聯性&lt;&#x2F;h3&gt;
&lt;p&gt;我們來定義另一個 Item model，並且讓 User 與 Item 間存在著一對多的關係，也就是一個 User 可以擁有多個 Item。&lt;&#x2F;p&gt;
&lt;p&gt;建立 Item model 的模板檔：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;project1&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; ~&#x2F;project1&#x2F;oratordemo&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; orator make:model Item&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --migration&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;打開 project1&#x2F;&lt;wbr&gt;oratordemo&#x2F;&lt;wbr&gt;migrations&#x2F;&lt;wbr&gt;yyyy_&lt;wbr&gt;mm_&lt;wbr&gt;dd_&lt;wbr&gt;xxxxxx_&lt;wbr&gt;create_&lt;wbr&gt;items_&lt;wbr&gt;table.py 並加入欄位：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; orator.migrations&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Migration&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; CreateItemsTable&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Migration&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; up&lt;&#x2F;span&gt;&lt;span&gt;(self):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    Run the migrations.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    with&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; self&lt;&#x2F;span&gt;&lt;span&gt;.schema.create(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;items&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; as&lt;&#x2F;span&gt;&lt;span&gt; table:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      table.increments(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;id&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      table.timestamps()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      table.integer(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;user_id&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;).unsigned()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      table.foreign(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;user_id&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;).references(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;id&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;).on(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;users&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      table.string(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;title&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      table.string(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;description&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;).nullable()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; down&lt;&#x2F;span&gt;&lt;span&gt;(self):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    Revert the migrations.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    self&lt;&#x2F;span&gt;&lt;span&gt;.schema.drop(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;items&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;此處定義了 &lt;code&gt;user_id&lt;&#x2F;code&gt; 與 users table 的 &lt;code&gt;id&lt;&#x2F;code&gt; 欄位之間的關聯性。&lt;&#x2F;p&gt;
&lt;p&gt;跑一次 migration：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;project1&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; ~&#x2F;project1&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; orator migrate&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --config=oratordemo&#x2F;database.py --path=oratordemo&#x2F;migrations&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;看一下資料庫的關聯性：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;orator-orm-introduction&#x2F;1_et5xRfwWb2ARIjZvUBJxeQ.png&quot; alt=&quot;ER 圖&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;是不是妖受讚！&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;orator-orm-introduction&#x2F;1_szxNRUl0iRARCpmXTWbfVw.png&quot; alt=&quot;妖受讚&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：台中市議會&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;到目前爲止，我們定義了 User 與 Item 兩個 model，也建立了資料庫內的相對應的 schema，但目前的實際操作（建資料庫、表格、欄位、關聯性等）都僅及於資料庫方面，在 Model 方面至今還是只有兩個空白的模板檔（project1&#x2F;&lt;wbr&gt;oratordemo&#x2F;&lt;wbr&gt;models&#x2F;&lt;wbr&gt;user.py 與 project1&#x2F;&lt;wbr&gt;oratordemo&#x2F;&lt;wbr&gt;models&#x2F;&lt;wbr&gt;item.py）。&lt;&#x2F;p&gt;
&lt;p&gt;前面我們在 migration 檔內定義的關聯性的作用也僅止於資料庫，在 model 方面我們需要再次定義關聯性。&lt;&#x2F;p&gt;
&lt;p&gt;project1&#x2F;&lt;wbr&gt;oratordemo&#x2F;&lt;wbr&gt;models&#x2F;&lt;wbr&gt;user.py 改寫如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; orator.orm&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; has_many&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; oratordemo.database&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; BaseModel&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; User&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;BaseModel&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  @has_many&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; items&lt;&#x2F;span&gt;&lt;span&gt;(self):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    from&lt;&#x2F;span&gt;&lt;span&gt; oratordemo.models.item&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Item&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return&lt;&#x2F;span&gt;&lt;span&gt; Item&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;第一行引用了 &lt;code&gt;has_many&lt;&#x2F;code&gt;，用於定義 model 間一對多的關聯性。&lt;&#x2F;p&gt;
&lt;p&gt;第二行引用了我們最前面定義的 &lt;code&gt;BaseModel&lt;&#x2F;code&gt;， 我們定義的所有 model 都會繼承自 &lt;code&gt;BaseModel&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;後面的 &lt;code&gt;User&lt;&#x2F;code&gt; 類，裡面定義了與 &lt;code&gt;Item&lt;&#x2F;code&gt; 間一對多的關聯性，值得注意的是在 model 腳本內，我們只需要定義關聯性，不需要去定義其他的屬性，Orator ORM 會自己處理，這點是 Orator ORM 與其他 ORM 較不同的地方。&lt;&#x2F;p&gt;
&lt;p&gt;project1&#x2F;oratordemo&#x2F;models&#x2F;item.py 改寫如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; orator.orm&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; belongs_to&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; oratordemo.database&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; BaseModel&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; Item&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;BaseModel&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  @belongs_to&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; user&lt;&#x2F;span&gt;&lt;span&gt;(self):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    from&lt;&#x2F;span&gt;&lt;span&gt; oratordemo.models.user&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; User&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return&lt;&#x2F;span&gt;&lt;span&gt; User&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這裡引用了另外一個定義關聯性的修飾器 &lt;code&gt;belongs_to&lt;&#x2F;code&gt;，一樣用於指示 Item 與 User 間的關係。&lt;&#x2F;p&gt;
&lt;p&gt;上面兩個 model 的腳本檔有個較奇特之處，在 class 內的關聯性定義函式內竟然有放引用相關類別的敘述 &lt;code&gt;from oratordemo.models.item import Item&lt;&#x2F;code&gt;，不放在外面的原因是放在外面會導致循環引用的問題，所以得放在裡面。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;orm-cao-zuo&quot;&gt;ORM 操作&lt;&#x2F;h3&gt;
&lt;p&gt;這部份的操作我們進入 Python REPL：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;project1&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; project1&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; python&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Python&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 3.9.0&lt;&#x2F;span&gt;&lt;span&gt; (default) &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Type&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;help&amp;quot;, &amp;quot;copyright&amp;quot;, &amp;quot;credits&amp;quot; or &amp;quot;license&amp;quot; for more information.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;先引入我們剛剛定義好的兩個 model：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt; from&lt;&#x2F;span&gt;&lt;span&gt; oratordemo.models.user&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; User&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt; from&lt;&#x2F;span&gt;&lt;span&gt; oratordemo.models.item&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Item&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;先建個 user 帥一波：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; user1&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; User&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; user1.name&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;John&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; user1.save()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;true&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; user1.id&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;再打開 app.db 確認：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;orator-orm-introduction&#x2F;1_9_79p_HTKKlg6kHxJtwP8A.png&quot; alt=&quot;DBeaver&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;幫 &lt;code&gt;user1&lt;&#x2F;code&gt; 加個 item：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; item1&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; Item()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; item1.title&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;Banana&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; user1.items().save(item1)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span&gt;oratordemo.models.item.Item&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; object&lt;&#x2F;span&gt;&lt;span&gt; at&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; 0x&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;7fa94c828e20&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;重新載入一次 &lt;code&gt;user1&lt;&#x2F;code&gt; 就可以讀到剛加入的 item：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; user1&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; User.find(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; user1.items.first().title&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;Banana&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;再看一次 app.db 的內容：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;orator-orm-introduction&#x2F;1_9_gR9r5GQGexJyFIqP4_Tw.png&quot; alt=&quot;DBeaver&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;依照我們在 Python REPL 內的操作邏輯，只要在己的專案內引入定義好的 model class，就可以操作資料庫了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-yu&quot;&gt;結語&lt;&#x2F;h2&gt;
&lt;p&gt;這篇文章簡單介紹了比較易用的 Orator ORM，即便只是簡單介紹，卻也寫了不短的篇幅，在寫的過程中也促使自己重新複習了 Python的一些基礎特性，例如 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;pyliaorachel.github.io&#x2F;blog&#x2F;tech&#x2F;python&#x2F;2017&#x2F;09&#x2F;15&#x2F;pythons-import-trap.html&quot;&gt;import 的相對引用和絕對引用的機制&lt;&#x2F;a&gt;等。&lt;&#x2F;p&gt;
&lt;p&gt;本文中沒有說明太多 Orator ORM 的函式用法，主要著重在記錄個人實踐 POC 上的經驗，Orator ORM 的函式用法請查閱 Orator ORM 文件，Orator ORM 文件內的範例和本文略有不同，哪個適合自己就請讀者自行判斷啦！&lt;&#x2F;p&gt;
&lt;p&gt;另外本文也沒有涉及到另一個 ORM 很重要的特性 seeding，seeding 的機制可以讓我們很方便的建構資料庫的基本資料，例如縣市、郵遞區號、國家等等，另外在測試上也會用到 seeding 的機制在資料庫產生測試用假資料，以及測試後把假資料清除的工作，關於 seeding 的操作請見〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;seeding-in-orator-orm&#x2F;&quot;&gt;Orator ORM 的 Seeding 機制&lt;&#x2F;a&gt;〉一文。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Masonite Migration 之追加欄位篇</title>
        <published>2021-02-14T00:00:00+00:00</published>
        <updated>2021-02-14T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/masonite-migration/"/>
        <id>https://editor.leonh.space/2021/masonite-migration/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/masonite-migration/">&lt;p&gt;之前寫過一篇 Masonite migration 的文章，紀錄了新增表格的過程，這篇重點會放在異動既有的表格。&lt;&#x2F;p&gt;
&lt;p&gt;這裡的案例是在 transactions table 內新增 category 欄位，在開工之前插播一下我個人的命名慣例，如果框架或 ORM 沒有既有慣例的話，我個人的慣例是欄位名一律都使用英文單數，小寫，所以雖然 category 內容一定是多筆，但在這樣的命名慣例上，依然維持英文單數。&lt;&#x2F;p&gt;
&lt;p&gt;用 Masonite 的命令列工具產生 migration 檔：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; craft migration add_category_column_to_transactions_table&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --table&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; transactions&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Created&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; migration: 2019_10_11_133608_add_category_column_to_transactions_table.py&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;檔名的部份可以自行命名，沒有強制性的規則或約定，只要能清楚表達意思就可以。&lt;&#x2F;p&gt;
&lt;p&gt;把剛剛的檔案用編輯器打開，會長這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; orator.migrations&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Migration&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; AddCategoryColumnToTransactionsTable&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Migration&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; up&lt;&#x2F;span&gt;&lt;span&gt;(self):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        Run the migrations.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        with&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; self&lt;&#x2F;span&gt;&lt;span&gt;.schema.table(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;transactions&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; as&lt;&#x2F;span&gt;&lt;span&gt; table:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;            pass&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; down&lt;&#x2F;span&gt;&lt;span&gt;(self):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        Revert the migrations.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        with&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; self&lt;&#x2F;span&gt;&lt;span&gt;.schema.table(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;transactions&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; as&lt;&#x2F;span&gt;&lt;span&gt; table:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;            pass&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在 &lt;code&gt;up()&lt;&#x2F;code&gt; 區塊內加入新欄位的定義：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;table.json(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;category&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;完整的欄位型態可以參考 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;orator-orm.com&#x2F;docs&#x2F;0.9&#x2F;schema_builder.html&quot;&gt;ORM 的文件&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;最後的 migration 檔案會長這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; orator.migrations&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Migration&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; AddCategoryColumnToTransactionsTable&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Migration&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; up&lt;&#x2F;span&gt;&lt;span&gt;(self):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        Run the migrations.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        with&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; self&lt;&#x2F;span&gt;&lt;span&gt;.schema.table(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;transactions&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; as&lt;&#x2F;span&gt;&lt;span&gt; table:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            table.json(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;category&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; down&lt;&#x2F;span&gt;&lt;span&gt;(self):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        Revert the migrations.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        with&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; self&lt;&#x2F;span&gt;&lt;span&gt;.schema.table(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;transactions&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; as&lt;&#x2F;span&gt;&lt;span&gt; table:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;            pass&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;最後跑一下 &lt;code&gt;craft migrate&lt;&#x2F;code&gt; 就會把剛定義的欄位加入資料庫內了。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Masonite Controller</title>
        <published>2021-02-13T00:00:00+00:00</published>
        <updated>2021-02-13T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/masonite-controller/"/>
        <id>https://editor.leonh.space/2021/masonite-controller/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/masonite-controller/">&lt;h2 id=&quot;jian-li-controller&quot;&gt;建立 Controller&lt;&#x2F;h2&gt;
&lt;p&gt;我們已經有了一個 Transaction model &amp;amp; seed， 再來就是做 Controller。&lt;&#x2F;p&gt;
&lt;p&gt;在 Masonite 建立 Controller 相當簡單：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; craft controller Transactoin&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --resource&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;依照 Masonite 的&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E7%BA%A6%E5%AE%9A%E4%BC%98%E4%BA%8E%E9%85%8D%E7%BD%AE&quot;&gt;約定&lt;&#x2F;a&gt;，上面這行命令會建立 app&#x2F;&lt;wbr&gt;http&#x2F;&lt;wbr&gt;controllers&#x2F;&lt;wbr&gt;Transaction&lt;wbr&gt;Controller.&lt;wbr&gt;py&lt;wbr&gt;，並且由於命令後面追加的 &lt;code&gt;--resource&lt;&#x2F;code&gt; 參數，Masonite 也幫我們在 TransactionController 內加上了基本的 CRUD 方法。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;lu-you&quot;&gt;路由&lt;&#x2F;h2&gt;
&lt;p&gt;用戶在瀏覽器請求 http:&#x2F;&#x2F;127.0.01:8000&#x2F;transaction&#x2F; 時，會先由 route 來決定要分配給哪個 Contrller 的哪個方法，雖然目前 TransactionController 內容都是空的，不過我們在此先行定義 Transaction 的路由規則，後面再來寫具體的 TransactionController method。&lt;&#x2F;p&gt;
&lt;p&gt;路由檔案在 routes&#x2F;web.py，在檔案內加入一行：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Get(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&#x2F;transactions&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;TransactionController@index&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這行應該很好理解：在瀏覽器 GET &#x2F;transaction 這個位址的時候，交由 TransactionController 的 &lt;code&gt;index()&lt;&#x2F;code&gt; 方法來處理。&lt;&#x2F;p&gt;
&lt;p&gt;HTTP 動作（GET、POST 等等…）與 Controller 方法之間的對應關係也有一些既有的慣例存在，這些慣例可以參考 Laravel 的文件：&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;laravel.com&#x2F;docs&#x2F;5.8&#x2F;controllers#resource-controllers&quot;&gt;RESTful Resource Controllers&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;index-fang-fa&quot;&gt;Index 方法&lt;&#x2F;h2&gt;
&lt;p&gt;回到 TransactionController，裡面的 &lt;code&gt;index()&lt;&#x2F;code&gt; 應該是空白的長這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; index&lt;&#x2F;span&gt;&lt;span&gt;(self, request: Request, auth: Auth, view: View):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;quot;&amp;quot;&amp;quot;Show several resource listings&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    ex. Model.all()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        Get().route(&amp;quot;&#x2F;index&amp;quot;, TransactionController)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    psss&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;接下來這裡想要的設計是，如果用戶已登入，則顯示出他名下的交易紀錄；如果用戶未登入，則跳轉到登入表單畫面。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;yong-hu-ren-zheng&quot;&gt;用戶認證&lt;&#x2F;h3&gt;
&lt;p&gt;來做用戶認證，在 TransactionController 最前面先 import 模組：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; masonite.auth&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Auth&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; masonite.request&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Request&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;再回到 &lt;code&gt;index()&lt;&#x2F;code&gt; 加入判斷登入的敘述，如果未登入，跳轉到網址 &#x2F;login；如果已登入，讓變數 user 指向這位登入的用戶：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; index&lt;&#x2F;span&gt;&lt;span&gt;(self, request: Request, auth: Auth):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;quot;&amp;quot;&amp;quot;Show several resource listings&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    ex. Model.all()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        Get().route(&amp;quot;&#x2F;index&amp;quot;, TransactionController)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    if not&lt;&#x2F;span&gt;&lt;span&gt; auth.user():&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        return&lt;&#x2F;span&gt;&lt;span&gt; request.redirect(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&#x2F;login&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    user&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; auth.user()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;關於用戶認證的部份，除了在 controller 做判斷外，因應不同的需求，也可能在 route 或 view 裡面做判斷，&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.masoniteproject.com&#x2F;security&#x2F;authentication#retrieving-the-authenticated-user&quot;&gt;Masonite 原廠文件&lt;&#x2F;a&gt;有更多關於這方面的敘述。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;cha-xun-yi-deng-ru-yong-hu-de-jiao-yi-ji-lu&quot;&gt;查詢已登入用戶的交易紀錄&lt;&#x2F;h3&gt;
&lt;p&gt;前一節的最後一行已經把已登入用戶派給一個變數 user，現在利用 ORM 來查詢這位 user 大大的交易紀錄。一樣要先引用模組：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; app.Transaction&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Transaction&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這是事前已經定義好的 Transaction model，有了它才能做查詢。&lt;&#x2F;p&gt;
&lt;p&gt;在 &lt;code&gt;index()&lt;&#x2F;code&gt; 加入查詢的敘述：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; index&lt;&#x2F;span&gt;&lt;span&gt;(self, request: Request, auth: Auth):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;quot;&amp;quot;&amp;quot;Show several resource listings&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    ex. Model.all()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        Get().route(&amp;quot;&#x2F;index&amp;quot;, TransactionController)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    if not&lt;&#x2F;span&gt;&lt;span&gt; auth.user():&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        return&lt;&#x2F;span&gt;&lt;span&gt; request.redirect(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&#x2F;login&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    user&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; auth.user()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    transactions&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; Transaction.where(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;user_id&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;=&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;, user.id).get()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return&lt;&#x2F;span&gt;&lt;span&gt; transactions&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;用 transactions 變數去存查詢回來的資料，最後 return 出去，Masonite 會很聰明的自動序列化成 JSON 格式丟出去，所以瀏覽器會接收到一份 JSON 的交易紀錄。&lt;&#x2F;p&gt;
&lt;p&gt;JSON 是我們的好朋友，先把前端放一邊，確定 controller actions 都正確之後，下一步再來鼓搗前端的部份。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Masonite Model</title>
        <published>2021-02-11T00:00:00+00:00</published>
        <updated>2021-02-11T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/masonite-model/"/>
        <id>https://editor.leonh.space/2021/masonite-model/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/masonite-model/">&lt;p&gt;Masonite 與 Rails 相似，migration 把資料庫表格欄位都建立好後，還要再接著建立 model 才會在此專案內建立出真正屬於那個 table 的 class。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jian-li-model&quot;&gt;建立 model&lt;&#x2F;h2&gt;
&lt;p&gt;以一個叫 transactions 的表格為例，只建立 model：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; craft model Transaction&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;建立 model，並且也建立 seed 與 migration：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; craft model Transaction&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -m -s&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; transaction&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面這行指令相當於：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; craft model Transaction&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; craft seed Transaction&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; craft migration create_transactions_tabel&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --create=transactions&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;Model 檔案在 app&#x2F;Transaction.py。&lt;&#x2F;li&gt;
&lt;li&gt;Seed 檔案在 databases&#x2F;seeds&#x2F;transaction_table_seeder.py。&lt;&#x2F;li&gt;
&lt;li&gt;Migration 檔案在 databases&#x2F;migrations&#x2F;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;ding-yi-model-class&quot;&gt;定義 model class&lt;&#x2F;h2&gt;
&lt;p&gt;編輯後的 migratoin 檔案內容長這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; orator.migrations&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Migration&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; CreateTransactionsTable&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Migration&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; up&lt;&#x2F;span&gt;&lt;span&gt;(self):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        Run the migrations.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        with&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; self&lt;&#x2F;span&gt;&lt;span&gt;.schema.create(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;transactions&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; as&lt;&#x2F;span&gt;&lt;span&gt; table:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            table.increments(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;id&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            table.integer(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;user_id&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;).unsigned()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            table.foreign(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;user_id&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;).references(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;id&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;).on(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;users&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            table.date(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;date&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            table.integer(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;amount&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            table.string(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;receipt_number&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;).nullable()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            table.string(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;description&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;).nullable()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            table.timestamps()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; down&lt;&#x2F;span&gt;&lt;span&gt;(self):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        Revert the migrations.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        self&lt;&#x2F;span&gt;&lt;span&gt;.schema.drop(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;transactions&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Model 檔案內容長這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;&amp;quot;&amp;quot;Transaction Model.&amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; config.database&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Model&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; Transaction&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Model&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;quot;&amp;quot;&amp;quot;Transaction Model.&amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    pass&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;空的，Masonite 不會自主幫我們加任何其它的敘述進去。&lt;&#x2F;p&gt;
&lt;p&gt;在加入映設定義前，先了解 Masonite 的&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E7%BA%A6%E5%AE%9A%E4%BC%98%E4%BA%8E%E9%85%8D%E7%BD%AE&quot;&gt;約定&lt;&#x2F;a&gt;：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Model class 皆為單數名詞，頭文字大寫，如這裡的 Transaction。&lt;&#x2F;li&gt;
&lt;li&gt;Model class 映射的 table 講好就是複數名詞，都是小寫，如 transactions。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;這就是 Masonite 與開發者之間的約定，因為約定成俗，所以在映射定義內，不用特別去指定 table，一切照約定行事：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Model Class&lt;&#x2F;th&gt;&lt;th&gt;Database Table&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;Transaction&lt;&#x2F;td&gt;&lt;td&gt;transactions&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;開始定義 model 關聯性：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;&amp;quot;&amp;quot;Transaction Model.&amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; config.database&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Model&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; orator.orm&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; belongs_to&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; Transaction&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Model&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    &amp;quot;&amp;quot;&amp;quot;Transaction Model.&amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;    @belongs_to&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;user_id&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;id&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; user&lt;&#x2F;span&gt;&lt;span&gt;(self):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        from&lt;&#x2F;span&gt;&lt;span&gt; app.User&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; User&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        return&lt;&#x2F;span&gt;&lt;span&gt; User&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;from orator.orm import belongs_to&lt;&#x2F;code&gt;：引入 &lt;code&gt;belongs_to()&lt;&#x2F;code&gt; 修飾器待用。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;def user(self)&lt;&#x2F;code&gt; 這一整塊連同上面的 &lt;code&gt;@belongs_to()&lt;&#x2F;code&gt; 修飾器：定義 User class 與 Transaction class 兩者間的關聯。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;最基本的 model 結構定義完畢。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;can-kao-zi-liao&quot;&gt;參考資料&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;orator-orm.com&#x2F;docs&#x2F;0.9&#x2F;orm.html&quot;&gt;ORM&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Masonite Seeding</title>
        <published>2021-02-10T00:00:00+00:00</published>
        <updated>2021-02-10T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/masonite-seeding/"/>
        <id>https://editor.leonh.space/2021/masonite-seeding/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/masonite-seeding/">&lt;h2 id=&quot;seeding&quot;&gt;Seeding&lt;&#x2F;h2&gt;
&lt;p&gt;原意為播種，在這裡指的是批次建假資料，或是初始資料。假資料用於模擬真實的環境，供團隊成員去感受或模擬實際上線的使用，不過如果是單元測試的話，則與 seeding 的假資料無關，單元測試的假資料會在該單元測試的程式內有針對性的建立與消除，不會與 seeding 的假資料混用。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;seeding-zhi-ling&quot;&gt;Seeding 指令&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;執行全部 seeder：&lt;code&gt;craft seed:run&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;執行特定 seeder，以 Transaction model 為例：&lt;code&gt;craft seed:run Transaction&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;建立 seeder，以 Transaction model 為例：&lt;code&gt;craft seed Transaction&lt;&#x2F;code&gt;。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;fan-li&quot;&gt;範例&lt;&#x2F;h2&gt;
&lt;p&gt;如果有用 &lt;code&gt;craft auth&lt;&#x2F;code&gt; 建立過用戶認證系統的話，這個指令也幫我們建了一套 User 物件的 seeder 範例，在實做我們的 seeder 前先看看範例的做法。&lt;&#x2F;p&gt;
&lt;p&gt;從 databases&#x2F;seeds&#x2F;database_seeder.py 開始看起：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;&amp;quot;&amp;quot;Base Database Seeder Module.&amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; orator.seeds&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Seeder&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; .user_table_seeder&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; UserTableSeeder&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; DatabaseSeeder&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Seeder&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; run&lt;&#x2F;span&gt;&lt;span&gt;(self):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;quot;&amp;quot;&amp;quot;Run the database seeds.&amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        self&lt;&#x2F;span&gt;&lt;span&gt;.call(UserTableSeeder)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;前面提到過的 &lt;code&gt;craft seed:run&lt;&#x2F;code&gt; 就會執行這支看起來很空的 database_seeder.py，再透過裡面的 &lt;code&gt;call()&lt;&#x2F;code&gt; 來呼叫要被執行的 seeder。可以看到目前只有一個 UserTableSeeder，讓我們繼續看下去。&lt;&#x2F;p&gt;
&lt;p&gt;來看 database&#x2F;seeds&#x2F;user_table_seeder.py：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;&amp;quot;&amp;quot;User Table Seeder.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;You can run this seeder in order to generate users.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    - Each time it is ran it will generate 50 random users.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    - All users have the password of &amp;#39;secret&amp;#39;.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    - You can run the seeder by running: craft seed:run.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; orator.seeds&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Seeder&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; app.User&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; User&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; config.factories&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; factory&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; UserTableSeeder&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Seeder&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; run&lt;&#x2F;span&gt;&lt;span&gt;(self):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        Run the database seeds.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        factory(User,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 50&lt;&#x2F;span&gt;&lt;span&gt;).create()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;一樣很空，只是叫工廠幫我們做五十個 User 出來，至於具體工廠怎麼做出 User，從 config.factories 繼續追下去。&lt;&#x2F;p&gt;
&lt;p&gt;工廠在 config&#x2F;factories.py：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; orator.orm&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Factory&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; app.User&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; User&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; string&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; ascii_uppercase&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;factory&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; Factory()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; users_factory&lt;&#x2F;span&gt;&lt;span&gt;(faker):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;#39;name&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;: faker.name(),&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;#39;email&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;: faker.email(),&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;#39;password&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;$2b$12$WMgb5Re1NqUr.uSRfQmPQeeGWudk&#x2F;8&#x2F;aNbVMpD1dR.Et83vfL8WAu&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; # == &amp;#39;secret&amp;#39;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;factory.register(User, users_factory)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;先看最下面註冊工廠方法的敘述：User 的工廠方法使用 &lt;code&gt;users_factroy()&lt;&#x2F;code&gt;。&lt;code&gt;users_factory()&lt;&#x2F;code&gt; 接受一個 &lt;code&gt;faker&lt;&#x2F;code&gt; 參數，這個參數是 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;faker.readthedocs.io&#x2F;en&#x2F;master&#x2F;&quot;&gt;Faker&lt;&#x2F;a&gt; 的一個實例，但不用自己產生，&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;orator-orm.com&#x2F;&quot;&gt;Orator&lt;&#x2F;a&gt; 在呼叫工廠方法時會自己幫我們產生 Faker 實例並餵入。&lt;&#x2F;p&gt;
&lt;p&gt;Faker 是個產出假資料的包，在專案建立時應該就會被裝起來。Faker 的使用也很直白，看上面的範例都可以望文生義，想更深入了解可以去讀 Faker 文件。&lt;&#x2F;p&gt;
&lt;p&gt;有了這樣的工廠方法，便可應用於 seeding 與單元測試，它們都可以共用這些工廠方法，不用重複造輪子。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jian-li-seeder&quot;&gt;建立 Seeder&lt;&#x2F;h2&gt;
&lt;p&gt;在此以 Transaction model 為例，建立 seeder 與相關的工廠方法。&lt;&#x2F;p&gt;
&lt;p&gt;建立 Seeder 檔案：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; craft seed Transaction&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;建立 Transaction 的 factory 方法，編輯 config&#x2F;factories.py，加入相關敘述：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; app.Transaction&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Transaction&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; string&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; ascii_uppercase&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; transactions_factory&lt;&#x2F;span&gt;&lt;span&gt;(faker):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    return&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;#39;user_id&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;: faker.random_int(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;min&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt; max&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;User.max(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;id&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)),&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;#39;date&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;: faker.date_this_decade(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;after_today&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;True&lt;&#x2F;span&gt;&lt;span&gt;),&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;#39;amount&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;: faker.random_int(),&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;#39;receipt_number&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;: faker.bothify(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;text&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;??-########&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt; letters&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span&gt;ascii_uppercase),&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;#39;description&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;: faker.sentence(),&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;factory.register(Transaction, transactions_factory)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;基本就是抄改 &lt;code&gt;users_factory()&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;工廠方法完成後，回到 Transaction seeder 檔案，設定工廠製造數量。編輯 databases&#x2F;seeds&#x2F;transaction_table_seeder.py：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; orator.seeds&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Seeder&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; app.Transaction&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Transaction&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; config.factories&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; factory&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; TransactionTableSeeder&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Seeder&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; run&lt;&#x2F;span&gt;&lt;span&gt;(self):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        Run the database seeds.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        factory(Transaction,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1000&lt;&#x2F;span&gt;&lt;span&gt;).create()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;基本上也是抄改。&lt;&#x2F;p&gt;
&lt;p&gt;工廠有了，製作數量設定了，再來讓 Transaction seeder 也會和其它 seeder 一起被執行，在 databases&#x2F;seeds&#x2F;database_seeder.py 加入這行：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;self&lt;&#x2F;span&gt;&lt;span&gt;.call(TransactionTableSeeder)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;最後實際跑一次 seeder：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; craft seed:run&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;跑完可以進資料庫看一下是不是如預期的建了這些假資料。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Masonite 建立用戶認證系統</title>
        <published>2021-02-09T00:00:00+00:00</published>
        <updated>2021-02-09T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/masonite-auth/"/>
        <id>https://editor.leonh.space/2021/masonite-auth/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/masonite-auth/">&lt;p&gt;Masonite 內建了一套基礎的用戶認證系統，包括 route、migration、model、controller、vew 都傳便便，在剛 &lt;code&gt;craft install&lt;&#x2F;code&gt; 完的系統裡面是沒有的，需要再下 &lt;code&gt;craft auth&lt;&#x2F;code&gt; 來幫我們把上面這些檔案搭出來，因為實在太簡單了，使得本篇看來有點像廢文…。&lt;&#x2F;p&gt;
&lt;p&gt;詳情請見 Masonite 的〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.masoniteproject.com&#x2F;features&#x2F;authentication&quot;&gt;Authentication&lt;&#x2F;a&gt;〉文件。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Masonite 資料庫 Migration</title>
        <published>2021-02-07T00:00:00+00:00</published>
        <updated>2021-02-07T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/masonite-db-migration/"/>
        <id>https://editor.leonh.space/2021/masonite-db-migration/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/masonite-db-migration/">&lt;h2 id=&quot;migration&quot;&gt;Migration&lt;&#x2F;h2&gt;
&lt;p&gt;Migration 是 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;ithelp.ithome.com.tw&#x2F;articles&#x2F;10207752&quot;&gt;ORM&lt;&#x2F;a&gt; 用來紀錄資料庫表格異動的機制，隨著專案的演化，資料庫 schema 也必然需要被變更，ORM 可以紀錄每次資料庫的異動，這樣的機制就叫 migration，當代的 web 框架都有搭配各自的 ORM 也都有這樣的機制。Masonite 搭配的 ORM 是 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;orator-orm.com&#x2F;&quot;&gt;Orator&lt;&#x2F;a&gt;，有了 ORM，資料庫的 schema 異動都可以透過自己熟悉的框架語言去實現，不用碰 SQL 語法，…當然這只是理想的狀況，在決定欄位型態前還是得要了解後端資料庫的資料型態特性，或者是某些特殊的查詢一定要用 SQL 來寫，總之真實世界比理想狀況複雜的多。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jian-li-migration-dang&quot;&gt;建立 Migration 檔&lt;&#x2F;h2&gt;
&lt;p&gt;情境：既有 users table，要幫每個 user 紀錄交易資料，故需要增加另一個 transactions table，並且具有關聯性。上面的描述以 ORM 的角度應該用 class 來敘述較好，但這篇文主要是談 migration 本身故不多談 class。&lt;&#x2F;p&gt;
&lt;p&gt;Craft 再次出動：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; craft migraton create_transactions_table&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --create=transaction&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Created&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; migration: 2019_07_01_142116_create_transactions_table.py&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;create_transactions_table&lt;&#x2F;code&gt;：這次 migration 的名稱而已。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;--create=transactions&lt;&#x2F;code&gt;：表示這次是要新建 table。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;和 Rails 不同的是，&lt;code&gt;craft migration&lt;&#x2F;code&gt; 不會拿後面的參數自己去幫我們新增檔案的內容，在此後面的參數命名不會影響到產出檔案的內容，只有被明確指示的 &lt;code&gt;--create=transactions&lt;&#x2F;code&gt; 這段敘述才會被引用進檔案內容。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ding-yi-lan-wei&quot;&gt;定義欄位&lt;&#x2F;h2&gt;
&lt;p&gt;可以看到它幫我們建了一個檔案，此檔案位在 databases&#x2F;migrations&#x2F; 內，內容：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; orator.migrations&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Migration&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; CreateTransactionsTable&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Migration&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; up&lt;&#x2F;span&gt;&lt;span&gt;(self):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        Run the migrations.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        with&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; self&lt;&#x2F;span&gt;&lt;span&gt;.schema.create(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;transactions&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; as&lt;&#x2F;span&gt;&lt;span&gt; table:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            table.increments(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;id&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            table.timestamps()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; down&lt;&#x2F;span&gt;&lt;span&gt;(self):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        Revert the migrations.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        self&lt;&#x2F;span&gt;&lt;span&gt;.schema.drop(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;transactions&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;欄位資料型態的定義參閱 Orator 的〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;orator-orm.com&#x2F;docs&#x2F;0.9&#x2F;schema_builder.html&quot;&gt;Schema Builder&lt;&#x2F;a&gt;〉文件。&lt;&#x2F;p&gt;
&lt;p&gt;把要加的欄位定義進去：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;table.date(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;date&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;table.integer(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;amount&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;table.string(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;receipt_number&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;).nullable()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;table.string(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;description&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;).nullable()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;.nullable()&lt;&#x2F;code&gt; 表示此欄位接受空值。&lt;&#x2F;p&gt;
&lt;p&gt;加上與 users 的外鍵欄位：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;table.integer(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;user_id&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;).unsigned()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;table.foreign(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;user_id&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;).references(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;id&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;).on(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;users&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;外鍵要求資料型態要同原本的欄位，亦即 &lt;code&gt;transactions.user_id&lt;&#x2F;code&gt; 與 &lt;code&gt;users.id&lt;&#x2F;code&gt; 兩者資料型態要一致。因為 &lt;code&gt;users.id&lt;&#x2F;code&gt; 有 unsigned 的屬性，所以 &lt;code&gt;transactions.user_id&lt;&#x2F;code&gt; 也必須是 &lt;code&gt;unsigned&lt;&#x2F;code&gt; 的屬性。&lt;&#x2F;p&gt;
&lt;p&gt;事實上用 &lt;code&gt;craft migration&lt;&#x2F;code&gt; 做出來定義檔都會預帶一個 &lt;code&gt;id&lt;&#x2F;code&gt; 作為主鍵的敘述，像這樣：&lt;br &#x2F;&gt;
&lt;code&gt;table.increments(&#x27;id&#x27;)&lt;&#x2F;code&gt;，這邊的 &lt;code&gt;increments()&lt;&#x2F;code&gt; 都有加上 unsigned 的屬性。（好像是從 Laravel 抄來的）（其實整套都是抄 Laravel 的）&lt;&#x2F;p&gt;
&lt;p&gt;最後完整的檔案長這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; orator.migrations&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; Migration&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;class&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; CreateTransactionsTable&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Migration&lt;&#x2F;span&gt;&lt;span&gt;):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; up&lt;&#x2F;span&gt;&lt;span&gt;(self):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        Run the migrations.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        with&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; self&lt;&#x2F;span&gt;&lt;span&gt;.schema.create(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;transactions&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; as&lt;&#x2F;span&gt;&lt;span&gt; table:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            table.increments(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;id&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            table.integer(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;user_id&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;).unsigned()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            table.foreign(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;user_id&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;).references(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;id&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;).on(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;users&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            table.date(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;date&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            table.integer(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;amount&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            table.string(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;receipt_number&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;).nullable()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            table.string(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;description&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;).nullable()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            table.timestamps()&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; down&lt;&#x2F;span&gt;&lt;span&gt;(self):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        Revert the migrations.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        self&lt;&#x2F;span&gt;&lt;span&gt;.schema.drop(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;transactions&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;pao-migrate&quot;&gt;跑 Migrate&lt;&#x2F;h2&gt;
&lt;p&gt;定義都寫完，可以跑 migrate 了：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; craft migrate&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[OK] Migrated 2019_07_01_142116_create_transactions_table&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這行指令不用進 database&#x2F;migrations&#x2F; 裡面也可以跑。&lt;&#x2F;p&gt;
&lt;p&gt;最後看到 OK 就好了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;que-ren-migration-zhuang-kuang&quot;&gt;確認 Migration 狀況&lt;&#x2F;h2&gt;
&lt;p&gt;多人開發的專案會遇到別人加了個 migration 自己卻沒注意到的狀況，可以用 &lt;code&gt;craft migrate:status&lt;&#x2F;code&gt; 來確認一下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;$ craft migrate:status&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;It took 0.33ms to execute the query (&amp;quot;SELECT * FROM sqlite_master WHERE type = &amp;#39;table&amp;#39; AND name = ?&amp;quot;, [&amp;#39;migrations&amp;#39;])&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;It took 0.09ms to execute the query (&amp;#39;SELECT &amp;quot;migration&amp;quot; FROM &amp;quot;migrations&amp;quot;&amp;#39;, [])&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;+---------------------------------------------+------+&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;| Migration                                   | Ran? |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;+---------------------------------------------+------+&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;| 2018_01_09_043202_create_users_table        | Yes  |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;| 2019_07_01_142116_create_transactions_table | Yes  |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;+---------------------------------------------+------+&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Masonite 的環境管理</title>
        <published>2021-02-05T00:00:00+00:00</published>
        <updated>2021-02-05T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/masonite-environment/"/>
        <id>https://editor.leonh.space/2021/masonite-environment/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/masonite-environment/">&lt;p&gt;環境（environment）在不同的地方具有不同的涵義，如果沒搞清楚很容易混淆，以 Masonite 來說，至少有兩種環境的涵義。&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Python 的虛擬環境&lt;&#x2F;li&gt;
&lt;li&gt;Masonite 自己的環境&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;Python 虛擬環境與 Masonite 本身無關，只是我們開發者因應一台電腦多專案的需求，避免專案的包互相影響而把每個專案隔離並且獨立管理專案包的機制，這樣的機制就是 Python 虛擬環境，常用的有 Python 自帶的 venv、&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;masonite-pipenv&#x2F;&quot;&gt;Pipenv&lt;&#x2F;a&gt;、Poetry 等。&lt;&#x2F;p&gt;
&lt;p&gt;下文要開始談的是 Masonite 自己的環境，專案從無到有至少會有幾個階段：開發（development）、測試（testing）、上線（production），而且隨著專案的複雜度增加，會分的更細，例如多了 staging 或 pre-production 等等，這些階段可能都需要不同的配置，例如資料庫的連線帳密、只有假資料的資料庫等等不同的配置需求，Masonite 對這樣的需求有提供環境管理的方法，只要把環境檔配置好，Masonite 就會自行去對應目前環境下正確的配置，避免繁瑣又容易出錯工人智慧改設定檔大法。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;env&quot;&gt;.env&lt;&#x2F;h2&gt;
&lt;p&gt;初期在建立專案時的 &lt;code&gt;craft new project_name&lt;&#x2F;code&gt; 指令，會建立出 Masonite 的專案資料夾與裡面的檔案和資料夾結構，其中會預帶一個 .env-example 的檔案內容如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;APP_NAME=Masonite 2.2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;APP_ENV=local&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;APP_DEBUG=True&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;AUTH_DRIVER=cookie&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;APP_URL=http:&#x2F;&#x2F;localhost:8000&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;KEY=your-secret-key&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;MAIL_DRIVER=terminal&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;MAIL_FROM_ADDRESS=&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;MAIL_FROM_NAME=&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;MAIL_HOST=&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;MAIL_PORT=&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;MAIL_USERNAME=&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;MAIL_PASSWORD=&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;MAILGUN_SECRET=&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;MAILGUN_DOMAIN=&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;DB_CONNECTION=mysql&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;DB_HOST=127.0.0.1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;DB_PORT=3306&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;DB_DATABASE=masonite&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;DB_USERNAME=root&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;DB_PASSWORD=root&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;DB_LOG=True&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;STRIPE_CLIENT=&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;STRIPE_SECRET=&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;STORAGE_DRIVER=disk&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;S3_CLIENT=&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;S3_SECRET=&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;S3_BUCKET=&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;RACKSPACE_USERNAME=&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;RACKSPACE_SECRET=&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;RACKSPACE_CONTAINER=&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;RACKSPACE_REGION=&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;AZURE_NAME=&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;AZURE_SECRET=&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;AZURE_CONNECTION=&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;AZURE_CONTAINER=&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;QUEUE_DRIVER=async&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;QUEUE_USERNAME=&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;QUEUE_VHOST=&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;QUEUE_PASSWORD=&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;QUEUE_HOST=&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;QUEUE_PORT=&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;QUEUE_CHANNEL=&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在之後的 &lt;code&gt;craft install&lt;&#x2F;code&gt; 則會幫我們把 .env-example 加上隨機產生的 KEY 之後再存成 .env 檔案，至此我們有了一個基本的 .env 環境檔。&lt;&#x2F;p&gt;
&lt;p&gt;Masonite 會在 bootstrap&#x2F;start.py 裡面 呼叫 &lt;code&gt;LoadEnvironment()&lt;&#x2F;code&gt; 載入 .env 檔案，所謂的載入就是把這些配置寫入作業系統環境變數中。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;qu-de-huan-jing-bian-shu&quot;&gt;取得環境變數&lt;&#x2F;h2&gt;
&lt;p&gt;.env 的內容都變成作業系統環境變數之後，在 Masonite 專案內的可以使用 &lt;code&gt;env()&lt;&#x2F;code&gt; 來取得這些變數值：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;from&lt;&#x2F;span&gt;&lt;span&gt; masonite&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; import&lt;&#x2F;span&gt;&lt;span&gt; env&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;env(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;DB_PORT&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; # 3306&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;pei-zhi-huan-jing&quot;&gt;配置環境&lt;&#x2F;h2&gt;
&lt;p&gt;.env 的第二行 &lt;code&gt;APP_ENV=local&lt;&#x2F;code&gt;，會令 Masonite 嘗試載入 .env.local，因此，只要自己創建 .env.local 並且把適當的配置寫進去，就會被正確的載入，如果 .env.local 與原本的 .env 有重複的定義的話，會以 .evn.local 的為主。&lt;&#x2F;p&gt;
&lt;p&gt;依此類推，只要自行定義出 .env.development、.env.production 等環境配置檔，並且在各自的環境下也對 .env 去定義要載入的環境檔，就可以正確的做好環境管理。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;env-gitignore&quot;&gt;.env &amp;amp; .gitignore&lt;&#x2F;h2&gt;
&lt;p&gt;.env 與其它自行建立的 .env.* 檔案裡面有 KEY、資料庫連線帳密等設定，務必不要上到 Git 去，應使用手動方式佈署到各環境的專案資料夾內。也因為不用上到 Git，每個環境的 .env 也因此不會被互相影響到，如果有異動的話還是要透過另外的機制與其它成員做更新。&lt;&#x2F;p&gt;
&lt;p&gt;唯一的例外是 .env.testing，這是用來跑單元測試用的環境，必須把 .env.testing 也加到 Git 中，其他的開發成員才可以順利的跑單元測試。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;&quot;&gt;&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.masoniteproject.com&#x2F;v&#x2F;v2.2&#x2F;useful-features&#x2F;environments&quot;&gt;Masonite Documentation - Environments&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Masonite 與 Pipenv 搭配使用</title>
        <published>2021-02-04T00:00:00+00:00</published>
        <updated>2021-02-04T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/masonite-pipenv/"/>
        <id>https://editor.leonh.space/2021/masonite-pipenv/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/masonite-pipenv/">&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.masoniteproject.com&#x2F;v&#x2F;v2.2&#x2F;&quot;&gt;Masonite&lt;&#x2F;a&gt; 是新興的 Python web 框架，風格類似 Rails 與 Laravel。下文使用的是 Masonite 2.2 LTS 版。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;pipenv.pypa.io&#x2F;en&#x2F;latest&#x2F;&quot;&gt;Pipenv&lt;&#x2F;a&gt; 則是 Python 的虛擬環境與依賴包管理器，類似 Ruby 的 Bundler 或 PHP 的 Composer。&lt;&#x2F;p&gt;
&lt;p&gt;Python 生態的虛擬環境與包管理器目前還沒有一個業界標準，較多人使用的至少就有三種：venv、Pipenv、Poetry，因為 venv 是 Python 自帶的，更加底層，真正在應用面上的競爭者只有 Pipenv 與 Poetry。&lt;&#x2F;p&gt;
&lt;p&gt;這篇紀錄一下 Masonite 與 Pipenv 搭配使用的一點筆記。&lt;&#x2F;p&gt;
&lt;p&gt;在 Masonite 的安裝文件內只講到最傳統的 requirements.txt 與 Python 自帶的 venv 的用法，因為本人較慣於使用 Pipenv，所以留點紀錄在此。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;首先參照 Masonite 安裝文件把 Python、OpenSSL &#x2F; LibreSSL 相關包裝好裝滿，以及 Pipenv 也裝好裝滿。&lt;&#x2F;p&gt;
&lt;p&gt;在 Python 全域環境裝 moasonite-cli：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; pip3 install masonite-cli&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;裝完就會用 &lt;code&gt;craft&lt;&#x2F;code&gt; 可以使用。Craft 是 Masonite 的命令列工具，相當於 Rails 的 &lt;code&gt;rails&lt;&#x2F;code&gt; 命令。&lt;&#x2F;p&gt;
&lt;p&gt;用 &lt;code&gt;craft&lt;&#x2F;code&gt; 建立新專案，注意它會用專案名建立一個目錄，專案的初始檔案與目錄結構都會一併建出來。建完進入專案目錄：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; craft new project_name&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; cd project_name&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;此時換 Pipenv 出場，剛剛的目錄進來後會有 requirements.txt 傳便便在這裡，內容只有一項：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;masonite&amp;gt;=2.2,&amp;lt;2.3&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在這裡初始化一個 Pipenv 虛擬環境，Pipenv 會自動偵測到 requirements.txt 並轉成 Pipfile，並且自動設置虛擬環境與安裝依賴包：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; pipenv install&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;等 Pipenv 跑完，才進入虛擬環境跑 &lt;code&gt;craft&lt;&#x2F;code&gt; 的初始化：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; pipenv shell&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;virtual&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; environment&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; $&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; craft install&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;等 &lt;code&gt;craft&lt;&#x2F;code&gt; 跑完，驗證一下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;virtual&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; environment&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; $&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; craft serve&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;瀏覽器開出來應該可以看到 Masonite 的首頁。&lt;&#x2F;p&gt;
&lt;p&gt;簡單歸納一下其實只有以下幾點：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;masonite-cli 包，裝在 Python 全域環境。&lt;&#x2F;li&gt;
&lt;li&gt;masonite 包，裝在 Python 虛擬環境。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;craft install&lt;&#x2F;code&gt; 在虛擬環境內執行。&lt;&#x2F;li&gt;
&lt;li&gt;之後要在專案內使用 &lt;code&gt;craft&lt;&#x2F;code&gt; 命令建立任何新的 model、controller、view 等檔案也都在 Python 虛擬環境下執行。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>建置 Python 3 開發環境</title>
        <published>2021-02-03T00:00:00+00:00</published>
        <updated>2021-02-03T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/python/"/>
        <id>https://editor.leonh.space/2021/python/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/python/">&lt;h2 id=&quot;python-de-ban-ben&quot;&gt;Python 的版本&lt;&#x2F;h2&gt;
&lt;p&gt;Python 進入 3.x 的時代也好幾年了，但至今 Python 2.7.x 即便已經不再維護，它還是以某種殭屍的型態存活在各個陳年專案上，對一個沒有舊包袱的新專案來說，Python 3.x 的 x 就必須是在開立專案時要考慮的問題，在 Python 的網站上有 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.python.org&#x2F;downloads&#x2F;&quot;&gt;Python 各版本目前的生命週期表&lt;&#x2F;a&gt;：&lt;&#x2F;p&gt;
&lt;div class=&quot;wide&quot;&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Python version&lt;&#x2F;th&gt;&lt;th&gt;Maintenance status&lt;&#x2F;th&gt;&lt;th&gt;First released&lt;&#x2F;th&gt;&lt;th&gt;End of support&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;3.11&lt;&#x2F;td&gt;&lt;td&gt;bugfix&lt;&#x2F;td&gt;&lt;td&gt;2022-10-03&lt;&#x2F;td&gt;&lt;td&gt;2027-10&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;3.10&lt;&#x2F;td&gt;&lt;td&gt;bugfix&lt;&#x2F;td&gt;&lt;td&gt;2021-10-04&lt;&#x2F;td&gt;&lt;td&gt;2026-10&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;3.9&lt;&#x2F;td&gt;&lt;td&gt;security&lt;&#x2F;td&gt;&lt;td&gt;2020-10-05&lt;&#x2F;td&gt;&lt;td&gt;2025-10&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;3.8&lt;&#x2F;td&gt;&lt;td&gt;security&lt;&#x2F;td&gt;&lt;td&gt;2019-10-14&lt;&#x2F;td&gt;&lt;td&gt;2024-10&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;3.7&lt;&#x2F;td&gt;&lt;td&gt;security&lt;&#x2F;td&gt;&lt;td&gt;2018-06-27&lt;&#x2F;td&gt;&lt;td&gt;2023-06-27&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;3.6&lt;&#x2F;td&gt;&lt;td&gt;end-of-life&lt;&#x2F;td&gt;&lt;td&gt;2016-12-23&lt;&#x2F;td&gt;&lt;td&gt;2021-12-23&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;2.7&lt;&#x2F;td&gt;&lt;td&gt;end-of-life&lt;&#x2F;td&gt;&lt;td&gt;2010-07-03&lt;&#x2F;td&gt;&lt;td&gt;2020-01-01&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;可以看到從 Python 3.7 起就維持一年跳一個次版號的頻率，並維持著五年的生命週期。&lt;&#x2F;p&gt;
&lt;p&gt;在確認過 Python 本身的生命週期後，另外一個考慮的點是 Python 3.6 ~ 3.10 之間是否有淘汰某些舊的語法、標準庫、功能，如果有的話那必須進一步考慮到新專案的依賴套件是不是有用到那些被淘汰的功能，幸好 Python 3.6 ~ 3.10 主要都是增加新的功能或語法，不太有淘汰舊功能的問題，因此對於新成立的專案，採用較新的 Python 3.10 看起來是個不太有風險的決定。&lt;&#x2F;p&gt;
&lt;p&gt;補充一個消息，&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.python.org&#x2F;zh-tw&#x2F;3.9&#x2F;whatsnew&#x2F;3.9.html#you-should-check-for-deprecationwarning-in-your-code&quot;&gt;從 Python 3.9 的下一版起會淘汰設計給保留與 Python 2.7 相容的舊標準庫／函式／API&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;python-3-kai-fa-huan-jing&quot;&gt;Python 3 開發環境&lt;&#x2F;h2&gt;
&lt;p&gt;目前所有的 Linux 發行版，內建的 Python 版本各有不同，如果您的 Linux 較新，已經是內建 Python 3 的話那恭喜您本文對您是無用處的，本文完。&lt;&#x2F;p&gt;
&lt;p&gt;以常見的 Linux 發行版來說，內建的 Python 如下：&lt;&#x2F;p&gt;
&lt;div class=&quot;wide&quot;&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;Linux 發行版&lt;&#x2F;th&gt;&lt;th&gt;Python 版次&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;elementary OS 6 Odin&lt;&#x2F;td&gt;&lt;td&gt;Python 3.8.10&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Pop!_OS 20.04&lt;&#x2F;td&gt;&lt;td&gt;Python 3.8.2&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Linux Mint 20&lt;&#x2F;td&gt;&lt;td&gt;Python 3.8.2&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Ubuntu 20.04 LTS&lt;&#x2F;td&gt;&lt;td&gt;Python 3.8.2&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Debian 11&lt;&#x2F;td&gt;&lt;td&gt;Python 3.9.2&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;Rocky Linux 8.4&lt;&#x2F;td&gt;&lt;td&gt;Python 3.9.2&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;AlmaLinux OS 8.5&lt;&#x2F;td&gt;&lt;td&gt;Python 3.6.8&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;CentOS 8.4.2105&lt;&#x2F;td&gt;&lt;td&gt;Python 3.9.2&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;上面的列表涵蓋了主要的桌面端與伺服器端的 Linux 發行版，可以看到 2021 年 10 月才發布的 Python 3.10 理所當然地尚未被納入。&lt;&#x2F;p&gt;
&lt;p&gt;如果是 macOS Monterey，內建的 Python 版本分別是 2.7.18 和 3.8.9。&lt;&#x2F;p&gt;
&lt;p&gt;下面開始我們的 Python 3 開發環境建置之旅。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;duo-ban-ben-python-guan-li&quot;&gt;多版本 Python 管理&lt;&#x2F;h2&gt;
&lt;p&gt;想要把 Python 3 裝起來，當然不能簡單粗暴地把作業系統預載的 Python 取代掉，這樣做會造成後續系統維護與更新上的許多問題。在這邊我們利用 pyenv 這個多版本 Python 管理器來幫助我們安裝一個與系統預載 Python 並行存在的 Python 3。&lt;&#x2F;p&gt;
&lt;p&gt;pyenv 不僅能幫我們安裝多版本的 Python，也提供了 Python 版本切換與管理的能力，並且這些由 pyenv 管理的 Python 版本彼此間都互相獨立，包括 Python 之下的套件／模組也都是獨立的，這樣的好處就是讓一台電腦的開發環境可以既多元又單純，多元的是 Python 版本，讓新舊專案都可以有適用的 Python 版本；單純的是各版次 Python 下的套件與模組，並不會受到別的版次的 Python 所影響。&lt;&#x2F;p&gt;
&lt;p&gt;根據 pyenv 的要求，需要安裝編譯 Python 所必須的其它系統套件，因為 pyenv 安裝 Python 的機制是把指定版本的 Python 源碼抓下來在自己的電腦內編譯，因此必須安裝那些編譯工具，才可以順利的編譯出 Python。&lt;&#x2F;p&gt;
&lt;p&gt;對 Debian &#x2F; Elementary OS &#x2F; Pop!_OS &#x2F; Linux Mint &#x2F; Ubuntu 等同一脈的 Linux 發行版，需要安裝下列套件：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; sudo apt install --no-install-reocmmends &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;\&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;make&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;build-essential&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;libssl-dev&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;zlib1g-dev&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;libbz2-dev&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;libreadline-dev&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;libsqlite3-dev&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;curl&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;llvm&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;libncurses5-dev&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;libncursesw5-dev&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;tk-dev&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;liblzma-dev&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;libxml2-dev&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;libxmlsec1-dev&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;對 macOS，需要先安裝 Homebrew 以及 Command Line Tools 後安裝下列套件：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; brew install openssl readline sqlite3 xz zlib bzip2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果是其它 Linux 發行版或其它作業系統，請參考 pyenv 的文件〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pyenv&#x2F;pyenv&#x2F;wiki#suggested-build-environment&quot;&gt;Suggested build environment&lt;&#x2F;a&gt;〉一節安裝所需系統套件。（像這樣的套件依賴關係就像粽子串一樣，一個牽一個，全部拉起來就是一大堆⋯⋯。）
上面的系統套件裝完後，終於可以裝 pyenv 了，Linux 安裝 pyenv 有現成的安裝腳本可以使用：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; curl https:&#x2F;&#x2F;pyenv.run &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;|&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; bash&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果是 macOS，則是利用 Homebrew 安裝 pyenv：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; brew install pyenv&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然後把 pyenv 加入 shell 的設定檔內，讓我們進入 shell 時就先執行 &lt;code&gt;pyenv init&lt;&#x2F;code&gt; 的命令：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; echo -e &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;if command -v pyenv 1&amp;gt;&#x2F;dev&#x2F;null 2&amp;gt;&amp;amp;1; then\n  eval &amp;quot;$(pyenv init -)&amp;quot;\nfi&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; &amp;gt;&amp;gt; ~&lt;&#x2F;span&gt;&lt;span&gt;&#x2F;.zshrc&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;以及設定讓編譯 Python 時取用 Homebrew 版的 ziplib 和 bzip2 函式庫：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; echo &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;export PATH=&amp;quot;&#x2F;usr&#x2F;local&#x2F;opt&#x2F;bzip2&#x2F;bin:$PATH&amp;quot;&amp;#39;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; &amp;gt;&amp;gt; ~&lt;&#x2F;span&gt;&lt;span&gt;&#x2F;.zshrc&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt; export&lt;&#x2F;span&gt;&lt;span&gt; LDFLAGS&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;-L&#x2F;usr&#x2F;local&#x2F;opt&#x2F;zlib&#x2F;lib -L&#x2F;usr&#x2F;local&#x2F;opt&#x2F;bzip2&#x2F;lib&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt; export&lt;&#x2F;span&gt;&lt;span&gt; CPPFLAGS&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;-I&#x2F;usr&#x2F;local&#x2F;opt&#x2F;zlib&#x2F;include -I&#x2F;usr&#x2F;local&#x2F;opt&#x2F;bzip3&#x2F;include&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面是以 Zsh 為例，如果是其他 shell 要自行類推修改，完整的 pyenv 安裝流程和注意事項最好還是看 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;pyenv&#x2F;pyenv&#x2F;blob&#x2F;master&#x2F;README.md&quot;&gt;pyenv 的文件&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;安裝完之後，如果 pyenv 有被正確的加進 &lt;code&gt;$PATH&lt;&#x2F;code&gt; 內，那麼執行 &lt;code&gt;pyenv&lt;&#x2F;code&gt; 應該可以看到如下的說明畫面：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;pyenv 2.2.0-5-g54889eb&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Usage: pyenv &amp;lt;command&amp;gt; [&amp;lt;args&amp;gt;]Some useful pyenv commands are:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   activate             Activate virtual environment&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   commands             List all available pyenv commands&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   deactivate           Deactivate virtual environment&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   doctor               Verify pyenv installation and development tools to build pythons.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   exec                 Run an executable with the selected Python version&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   global               Set or show the global Python version(s)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   help                 Display help for a command&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   hooks                List hook scripts for a given pyenv command&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   init                 Configure the shell environment for pyenv&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   install              Install a Python version using python-build&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   local                Set or show the local application-specific Python version(s)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   prefix               Display prefix for a Python version&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   rehash               Rehash pyenv shims (run this after installing executables)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   root                 Display the root directory where versions and shims are kept&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   shell                Set or show the shell-specific Python version&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   shims                List existing pyenv shims&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   uninstall            Uninstall a specific Python version&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   version              Show the current Python version(s) and its origin&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   --version            Display the version of pyenv&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   version-file         Detect the file that sets the current pyenv version&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   version-name         Show the current Python version&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   version-origin       Explain how the current Python version is set&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   versions             List all Python versions available to pyenv&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   virtualenv           Create a Python virtualenv using the pyenv-virtualenv plugin&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   virtualenv-delete    Uninstall a specific Python virtualenv&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   virtualenv-init      Configure the shell environment for pyenv-virtualenv&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   virtualenv-prefix    Display real_prefix for a Python virtualenv version&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   virtualenvs          List all Python virtualenvs found in `$PYENV_ROOT&#x2F;versions&#x2F;*&amp;#39;.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   whence               List all Python versions that contain the given executable&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   which                Display the full path to an executable&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;See `pyenv help &amp;lt;command&amp;gt;&amp;#39; for information on a specific command.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;For full documentation, see: https:&#x2F;&#x2F;github.com&#x2F;pyenv&#x2F;pyenv#readme&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在做後續的動作之前，建議先跑一下 pyenv doctor 讓 pyenv 自己檢查前面提到的用於編譯的套件是不是都有裝好。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;an-zhuang-python-3&quot;&gt;安裝 Python 3&lt;&#x2F;h3&gt;
&lt;p&gt;在裝 Python 3 之前，先看一下 pyenv 可以幫我們裝哪些個版本的 Python：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; pyenv install --list&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Available&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; versions:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;2.7.18&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;3.9.7&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;3.10.0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;activepython-3.6.0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;anaconda3-2021.05&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;graalpython-21.3.0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;micropython-1.15&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;miniconda3-4.7.12&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;pypy3.8-7.3.7&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;pyston-2.3.1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;stackless-3.7.5&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到不只是標準的 CPython，其他自行封裝的（ActivePython、Anaconda、miniconda）、第三方的 Python 解釋器（PyPy、Pyston、Stackless、MicroPython）也都可以由 pyenv 安裝與管理。&lt;&#x2F;p&gt;
&lt;p&gt;上面可以看到 Python 3 最新的版號就是 3.10.0，勇敢的把它裝起來：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; pyenv install 3.10.0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;經過一連串嘰哩呱啦的下載與編譯過程，最後應該會看到一段成功的訊息。&lt;&#x2F;p&gt;
&lt;p&gt;我們可以用 &lt;code&gt;pyenv versions&lt;&#x2F;code&gt; 來確認一下當前系統內存在的 Python 有哪些：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; pyenv versions&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;*&lt;&#x2F;span&gt;&lt;span&gt; system&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;  3.10.0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如預期的，除了系統預帶的 Python 外，多了一個 Python 3.10.0。&lt;&#x2F;p&gt;
&lt;p&gt;系統預帶的 Python前面有個 * 號，表示現在是用它當作目前操作環境下的 Python，目前我們還不用做切換，直到在 Python 專案內才會在專案資料夾內定義專案的 Python 版本，而這需要下一個工具 Poetry。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;python-zhuan-an-de-xu-ni-huan-jing-yu-tao-jian-guan-li&quot;&gt;Python 專案的虛擬環境與套件管理&lt;&#x2F;h3&gt;
&lt;p&gt;到上一節為止我們利用 pyenv 在系統內裝了 Python 3.10，下面會示範在一個新專案內利用 Poetry 這個工具來建立專案的 Python 虛擬環境以及管理依賴的套件。&lt;&#x2F;p&gt;
&lt;p&gt;關於 Python 的虛擬環境／套件管理工具，除了 Poetry 外，還有其他的選擇，可以參考〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;modelpredict.com&#x2F;python-dependency-management-tools&quot;&gt;Overview of python dependency management tools&lt;&#x2F;a&gt;〉一文了解它們之間的特性與差異。前面我們用的 pyenv 其實也具有管理虛擬環境的能力，但個人比較偏好把虛擬環境和套件管理器整合在一起的工具，pyenv本身不具被依賴套件管理的能力，而同時具備虛擬環境管理與套件管理的工具，有 Poetry、Pipenv 等。&lt;&#x2F;p&gt;
&lt;p&gt;接下來開始裝 Poetry。&lt;&#x2F;p&gt;
&lt;p&gt;在安裝 Poetry前，因為 Poetry 本身也是以 Python 開發的，它也有它自己的依賴性問題，所以我們想把它裝在 Python 3.9 的環境之下，因此我們先把 Python 切換到 3.9 的環境：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; pyenv global 3.10.0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;雖然是要把 Poetry 裝在 Python 3.9 的環境內，但這並不影響後面 Poetry 建立虛擬環境時要用到的 Python 版本。&lt;&#x2F;p&gt;
&lt;p&gt;切換完可以用 &lt;code&gt;pyenv versions&lt;&#x2F;code&gt; 確認一下。&lt;&#x2F;p&gt;
&lt;p&gt;回到安裝 Poetry 的主題，Poetry 也有著傳便便的安裝腳本：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; curl -sSL https:&#x2F;&#x2F;raw.githubusercontent.com&#x2F;python-poetry&#x2F;poetry&#x2F;master&#x2F;get-poetry.py &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;|&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; python&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; -&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;一樣一陣嘰哩呱啦的安裝完成後，預設會安裝在 ~&#x2F;.poetry&#x2F; 內，首先要確保保 Poetry 的主程式 ~&#x2F;.poetry&#x2F;bin&#x2F;poetry 有在 &lt;code&gt;$PATH&lt;&#x2F;code&gt; 內，有的話應該就可以執行 &lt;code&gt;poetry&lt;&#x2F;code&gt; 了：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Poetry version 1.1.11&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;USAGE&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  poetry [-h] [-q] [-v [&amp;lt;...&amp;gt;]] [-V] [--ansi] [--no-ansi] [-n] &amp;lt;command&amp;gt; [&amp;lt;arg1&amp;gt;] ... [&amp;lt;argN&amp;gt;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;ARGUMENTS&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &amp;lt;command&amp;gt;              The command to execute&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &amp;lt;arg&amp;gt;                  The arguments of the command&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;GLOBAL OPTIONS&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  -h (--help)            Display this help message&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  -q (--quiet)           Do not output any message&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  -v (--verbose)         Increase the verbosity of messages: &amp;quot;-v&amp;quot; for normal output, &amp;quot;-vv&amp;quot; for more verbose output and &amp;quot;-vvv&amp;quot; for debug&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  -V (--version)         Display this application version&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  --ansi                 Force ANSI output&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  --no-ansi              Disable ANSI output&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  -n (--no-interaction)  Do not ask any interactive question&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;AVAILABLE COMMANDS&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  about                  Shows information about Poetry.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  add                    Adds a new dependency to pyproject.toml.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  build                  Builds a package, as a tarball and a wheel by default.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  cache                  Interact with Poetry&amp;#39;s cache&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  check                  Checks the validity of the pyproject.toml file.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  config                 Manages configuration settings.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  debug                  Debug various elements of Poetry.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  env                    Interact with Poetry&amp;#39;s project environments.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  export                 Exports the lock file to alternative formats.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  help                   Display the manual of a command&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  init                   Creates a basic pyproject.toml file in the current directory.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  install                Installs the project dependencies.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  lock                   Locks the project dependencies.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  new                    Creates a new Python project at &amp;lt;path&amp;gt;.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  publish                Publishes a package to a remote repository.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  remove                 Removes a package from the project dependencies.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  run                    Runs a command in the appropriate environment.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  search                 Searches for packages on remote repositories.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  self                   Interact with Poetry directly.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  shell                  Spawns a shell within the virtual environment.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  show                   Shows information about packages.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  update                 Update the dependencies as according to the pyproject.toml file.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  version                Shows the version of the project or bumps it when a valid bump rule is provided.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;接下來我們開始拿一個專案 project1 在裡面建立專屬於它的 Python 3.10 虛擬環境。&lt;&#x2F;p&gt;
&lt;p&gt;首先要指定這個專案要用的 Python 版本：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; pyenv &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;local&lt;&#x2F;span&gt;&lt;span&gt; 3.10.0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面第一個指令用於指定這個 project1 資料夾要用的 Python 版本，它會幫我們建立一個 .python-version 檔案，裡面只要寫上版次，pyenv 就會依照裡面指定的版次套用於這個資料夾內，後面我們也驗證了確實是用了 Python 3.10。&lt;&#x2F;p&gt;
&lt;p&gt;接著建立專案的虛擬環境：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; poetry init&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Package&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; name&lt;&#x2F;span&gt;&lt;span&gt; [project1]:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Compatible&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; Python versions&lt;&#x2F;span&gt;&lt;span&gt; [^3.10]:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Generated&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; file&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[tool.poetry]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[tool.poetry.dependencies]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;python&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; = &amp;quot;^3.10&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[tool.poetry.dev-dependencies]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[build-system]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Do&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; you confirm generation?&lt;&#x2F;span&gt;&lt;span&gt; (yes&#x2F;no) [yes]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;poetry init&lt;&#x2F;code&gt; 命令用於初始化這個 project1 專案，透過問答式的界面，最後會建立出 pyproject.toml 檔案，裡面有專案的基本資料、相容的 Python 版本、依賴的套件清單等等。&lt;&#x2F;p&gt;
&lt;p&gt;在建立完 pyproject.toml 後，實際上也定義出了一個以 Python 3.10 為基礎的虛擬環境，進去看看：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; poetry shell&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Creating&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; virtualenv ...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Spawning&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; shell within ...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;project1-...&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; &amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; python&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --version&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Python&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 3.10.0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;project1-...&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; &amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; pip&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; list&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;Package&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;    Version&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;----------&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -------&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;pip&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        21.3.1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;setuptools&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 57.4.0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;wheel&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;      0.35.1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;經過短暫的嘰哩呱啦後我們進入了 project1 虛擬環境的 shell 內，也驗證了 Python 的版本及套件清單，相當的乾淨，至此我們得到了一個以 Python 3.10.0 為基礎的乾淨的開發環境，並且它也獨立於其它的開發環境。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-yu&quot;&gt;結語&lt;&#x2F;h2&gt;
&lt;p&gt;這篇文章介紹了 Python 的多版本管理以及專案虛擬環境從無到有的建立，主要是混合運用 pyenv 及 Poetry 兩個工具，要注意的是兩者都有虛擬環境管理的能力，但我們偏好用 Poetry，它同時能管理虛擬環境及專案的套件依賴性。這篇文章僅談到虛擬環境的建立為止，並未提及專案依賴套件的加入與管理，這部份會另外開一個主題來談。&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;python&#x2F;1_iMlKOYNT4muFbIrycizCBA.jpeg&quot; alt=&quot;我以後會專門做影片給大家講解的&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：&lt;a href=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;channel&#x2F;UCMUnInmOkrWN4gof9KlhNmQ&quot;&gt;老高與小茉 Mr &amp; Mrs Gao&lt;&#x2F;a&gt;&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>在 Windows 建置以 Visual Studio 為基礎的 Python &#x2F; Node.js 開發環境</title>
        <published>2021-02-01T00:00:00+00:00</published>
        <updated>2021-02-01T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/windows-python-node-js/"/>
        <id>https://editor.leonh.space/2021/windows-python-node-js/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/windows-python-node-js/">&lt;p&gt;已經很久沒開 Windows 的我，最近又遇到非開 Windows 不可的理由，就順勢在新的 SSD 上重裝 Windows 以及 Python 與 Node.js 的開發環境。&lt;&#x2F;p&gt;
&lt;p&gt;比較特別的是這次想使用 Visual Studio 附帶的 Git 與 Python 與 C++ Build Tools 為基礎來打造開發環境，能不能順利完成，讓我們看下去。&lt;&#x2F;p&gt;
&lt;p&gt;起手式當然是先裝肥大的 Visual Studio 2019，把 「Python 開發」、「Node.js 開發」、「使用 C++ 的桌面開發」三種工作負載都裝起來，然後就去泡個茶等它。&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;windows-python-node-js&#x2F;visual-studio-installer-2020-04-22-22.09.12.png&quot; alt=&quot;Viiual Studio&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;git&quot;&gt;Git&lt;&#x2F;h2&gt;
&lt;p&gt;雖然安裝程式沒有特別告知，不過 Git 其實已經是 Visual Studio 標準安裝的一部份了，位置在 C:\Program Files (x86)\&lt;wbr&gt;Microsoft Visual Studio\&lt;wbr&gt;2019\&lt;wbr&gt;Community\&lt;wbr&gt;Common7\&lt;wbr&gt;IDE\&lt;wbr&gt;CommonExtensions\&lt;wbr&gt;Microsoft\&lt;wbr&gt;TeamFoundation\&lt;wbr&gt;Team Explorer\&lt;wbr&gt;Git\&lt;wbr&gt;git.exe 這個又臭又長的路徑，可以把這個 git.exe 加入環境變數的 Path 內，之後會比較方便。&lt;&#x2F;p&gt;
&lt;p&gt;補個題外話，微軟自家的版控工具 Team Foundation Version Control 好像已經被當棄子了吧？&lt;&#x2F;p&gt;
&lt;h2 id=&quot;python&quot;&gt;Python&lt;&#x2F;h2&gt;
&lt;p&gt;因為在裝 Visual Studio 的時候就選了「Python 開發」，因此 Python 也會被貼心地一併裝好，位置在 C:\Program Files (x86)\&lt;wbr&gt;Microsoft Visual Studio\&lt;wbr&gt;Shared\&lt;wbr&gt;Python37_64\&lt;wbr&gt;python.exe 同樣又臭又長。而 Python 的套件管理工具 pip 則在同樣的位置下的 pip&#x2F;pip.exe。一樣可以加到 Path 內方便使用。&lt;&#x2F;p&gt;
&lt;p&gt;這樣裝的 Python 是系統全域安裝，還不確定會不會帶來一些什麼潛在的缺點，要免責聲明一下。&lt;&#x2F;p&gt;
&lt;p&gt;編按：認真想建置 Python 環境的小夥伴請見〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;python&#x2F;&quot;&gt;建置 Python 3 開發環境&lt;&#x2F;a&gt;〉。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;node-js&quot;&gt;Node.js&lt;&#x2F;h2&gt;
&lt;p&gt;和前面兩位不同，雖然在 Visual Studio 安裝也有勾了「Node.js 開發」，不過 &lt;strong&gt;Visual Studio Installer 並沒有幫我們裝好 Node.js&lt;&#x2F;strong&gt;，所以請去 Node.js 網站自行下載安裝 Node.js。&lt;&#x2F;p&gt;
&lt;p&gt;在 Node.js 方面，如果考慮到未來某些 npm 套件如果是 C++ 套件的話，npm 會幫我們做編譯，但需要事先幫它準備好編譯工具，也就是 MSVC C++ 建置工具，在 Visual Studio 2019 內稱為「&lt;strong&gt;MSVC v142 – VS2019 C++ x86&#x2F;64 建置工具（v14.25）&lt;&#x2F;strong&gt;」（很有&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;youtu.be&#x2F;EUXnJraKM3k&quot;&gt;微軟風格的取名&lt;&#x2F;a&gt;啊！），它是 Visual Studio Install「使用 C++ 的桌面開發」的元件之一。&lt;&#x2F;p&gt;
&lt;p&gt;折騰完之後可以試裝一下需要編譯的 npm 套件：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;powershell&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; npm install &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;-&lt;&#x2F;span&gt;&lt;span&gt;g sqlite3&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果沒問題就是沒問題，如果有問題就不能稱之為沒問題。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-yu&quot;&gt;結語&lt;&#x2F;h2&gt;
&lt;p&gt;至此我們的電腦內有了 Git、Python、Node.js、C++ 的開發環境了，接下來就裝個 Visual Studio Code 來開開心心寫 code 吧！Visual Studio 則靜靜地躺在開始內裝Ｂ即可。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>解讀 ads.txt 的重導規則</title>
        <published>2021-01-30T00:00:00+00:00</published>
        <updated>2021-01-30T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/ads-txt/"/>
        <id>https://editor.leonh.space/2021/ads-txt/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/ads-txt/">&lt;p&gt;先認識一下 ads.txt：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;ads-txt&#x2F;ads.txt.webp&quot; alt=&quot;ads.txt&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;最單純的作法就是把 ads.txt 放到 example.com&#x2F;ads.txt，然而在我這邊，同樣拿 example.com 為例，我的 example.com 與 www.example.com 都沒有真正的主機，全都是轉址到 blog.example.com，像這樣的例子，不太可能為了放一個 ads.txt 的檔案再去開一個空間，最好是也能用轉址的模式去讀 blog.example.com&#x2F;ads.txt，AdSense 的說明也有提到這種狀況，節錄如下：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;ads-txt&#x2F;ads.txt-redirection.webp&quot; alt=&quot;ads.txt 重導規則&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;這裡解釋的並不足夠精確，於是再深入去看 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;iabtechlab.com&#x2F;ads-txt&#x2F;&quot;&gt;ads.txt 的規格文件&lt;&#x2F;a&gt;，AdSense 或是網路上其它文章可能會引用到舊版的文件，要稍微注意一下。&lt;&#x2F;p&gt;
&lt;p&gt;關於重導的部份，節錄規格文件 3.1 節：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;ads-txt&#x2F;ads.txt-1.0.3.webp&quot; alt=&quot;ads.txt 重導規則&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;一開頭就明確說明 HTTP 301、302、307 都是被接受的重導方式，廣告系統（AdSense）應該跟進重導的位址去讀取 ads.txt。&lt;&#x2F;p&gt;
&lt;p&gt;後面一部分在說明重導網域的規則，這部份可以直接參照 AdSense 的說明文即可。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>如何收受 Google AdSense 款項</title>
        <published>2021-01-28T00:00:00+00:00</published>
        <updated>2021-01-28T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/adsense/"/>
        <id>https://editor.leonh.space/2021/adsense/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/adsense/">&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;adsense.google.com&#x2F;&quot;&gt;Google AdSense&lt;&#x2F;a&gt; 是 Google 的廣告聯盟，簡單的說就是在自己的網站上置入 Google 所代理的廣告，只要有展示或點擊，Google 就會分潤給你，就如同本小站頁面內置入的那些廣告 &lt;del&gt;（例如地方媽媽系列）&lt;&#x2F;del&gt; 就是來自 AdSense 所投放的廣告。&lt;&#x2F;p&gt;
&lt;p&gt;因為最近本小站終於突破了 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;support.google.com&#x2F;adsense&#x2F;answer&#x2F;1709871?hl=zh-Hant&quot;&gt;AdSense 100 美金的付款門檻&lt;&#x2F;a&gt;，就在這邊記下收款的程序吧！&lt;&#x2F;p&gt;
&lt;p&gt;AdSense 在台灣有提供三種付款管道，分別是西聯、支票、電匯，之前有稍微查一下，支票和電匯都會在銀行端被收取較高的手續費，因此大家都主推西聯，只是又有聽說在 2021 年初 Google 打算停止用西聯這個付款管道，因此本篇可能會是末代西聯收款紀錄文…。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xi-lian-hui-kuan&quot;&gt;西聯匯款&lt;&#x2F;h2&gt;
&lt;p&gt;科普一下&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikipedia.org&#x2F;wiki&#x2F;Western_Union&quot;&gt;西聯匯款的歷史&lt;&#x2F;a&gt;，西聯匯款原本是經營全球電報業務，在 1871 年開始利用它全球電報網路提供提供匯款的業務，距現在 150 年前就想到這樣的業務可說是相當超前佈署了，近代的 PayPal 則是在 1998 年才成立，晚了西聯一百多年。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.westernunion.com&#x2F;tw&#x2F;en&#x2F;home.html&quot;&gt;西聯匯款&lt;&#x2F;a&gt;是低成本的小額國際匯款服務，目前在台灣唯一的合作銀行是&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;customer.ktb.com.tw&#x2F;new&#x2F;personal&#x2F;9c60e7b3&quot;&gt;京城銀行&lt;&#x2F;a&gt;，下圖是西聯匯款和銀行跨國匯款的比較：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;adsense&#x2F;_2021-01-22_08.12.45.png&quot; alt=&quot;西聯匯款與一般跨國匯款比較&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;圖片來源：&lt;a href=&quot;https:&#x2F;&#x2F;customer.ktb.com.tw&#x2F;new&#x2F;personal&#x2F;9c60e7b3&quot;&gt;京城銀行&lt;&#x2F;a&gt;&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;可以看到西聯匯款在匯款成本和速度上都優於銀行跨國匯款。京城銀行網頁上也有西聯匯款的步驟：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;adsense&#x2F;_2021-01-22_08.39.22.png&quot; alt=&quot;京城銀行西聯匯款步驟&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;圖片來源：&lt;a href=&quot;https:&#x2F;&#x2F;customer.ktb.com.tw&#x2F;new&#x2F;personal&#x2F;9c60e7b3&quot;&gt;京城銀行&lt;&#x2F;a&gt;&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;表格內的步驟寫的有點明白又不太明白，而且又是臨櫃的方式，爾等懶人當然要尋求更便捷的方式，於是我們打算使用京城的網路銀行。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jing-cheng-ge-ren-wang-yin&quot;&gt;京城個人網銀&lt;&#x2F;h2&gt;
&lt;p&gt;要使用網銀必須先要有戶頭，京城銀行也有可以直接在線上開戶的&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;goyee.ktb.com.tw&#x2F;CUST&#x2F;#&#x2F;&quot;&gt;數位帳戶 Goyee&lt;&#x2F;a&gt;，Goyee 的介紹頁也有明確的寫到可以使用西聯的服務：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;adsense&#x2F;_2021-01-22_21.58.07.png&quot; alt=&quot;Goyee 數位帳戶&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：&lt;a href=&quot;https:&#x2F;&#x2F;goyee.ktb.com.tw&#x2F;CUST&#x2F;#&#x2F;product&#x2F;product3&quot;&gt;京城銀行 Goyee 數位帳戶&lt;&#x2F;a&gt;&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;依照 Goyee 的網頁說明進行開戶，要注意的是 Goyee 的 FAQ 裡面有寫到最長需要 5 天做審核：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;adsense&#x2F;_2021-01-22_23.54.39.png&quot; alt=&quot;如何開立 Goyee 數位帳戶&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：&lt;a href=&quot;https:&#x2F;&#x2F;goyee.ktb.com.tw&#x2F;CUST&#x2F;#&#x2F;product&#x2F;product3&quot;&gt;京城銀行 Goyee 數位帳戶&lt;&#x2F;a&gt;&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h3 id=&quot;kai-hu-de-zhu-yi-shi-xiang&quot;&gt;開戶的注意事項&lt;&#x2F;h3&gt;
&lt;ol&gt;
&lt;li&gt;在開戶過程中，有一欄是要填英文姓名，另外在 AdSense 內設定西聯收款處也要填英文姓名，這兩個地方的英文姓名最好是一樣。&lt;&#x2F;li&gt;
&lt;li&gt;上傳身份證照片的步驟，最好是在身份證下方墊白紙，用純白的背景拍的照片上傳後的辨識比較容易成功。&lt;&#x2F;li&gt;
&lt;li&gt;數位帳戶的金融卡會在開戶成功後寄出。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;shou-qu-xi-lian-hui-kuan&quot;&gt;收取西聯匯款&lt;&#x2F;h2&gt;
&lt;p&gt;登入網銀，進入西聯匯款（匯入）頁，可以看到這個表單：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;adsense&#x2F;netbank.ktb.com.tw_KTBPIB_Webapi_www_.png&quot; alt=&quot;京城銀行個人網路銀行&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;adsense&#x2F;2021-01-26_22.25.17.png&quot; alt=&quot;京城銀行個人網路銀行&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;京城銀行個人網路銀行&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;此處要填入的收款資訊，可以在 AdSense 的付款交易紀錄內取得：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;adsense&#x2F;_2021-01-26_22.38.42.png&quot; alt=&quot;AdSense 付款交易紀錄&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;AdSense 付款交易紀錄&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;點開一筆交易紀錄就會跳出它的詳細匯款資訊：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;adsense&#x2F;_2021-01-26_22.42.33.png&quot; alt=&quot;AdSense 付款收據&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;AdSense 付款收據&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;把這些匯款資訊照填入京城網銀：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;adsense&#x2F;_2021-01-26_22.55.59.png&quot; alt=&quot;京城銀行個人網路銀行&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;京城銀行個人網路銀行&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;按確認後的第二頁，需申報匯款性質，我是選「&lt;strong&gt;專業技術事務收入&lt;&#x2F;strong&gt;」。&lt;&#x2F;p&gt;
&lt;p&gt;確認第二頁的所有資訊都正確後，再按確認進入第三頁，如果都沒問題的話，應該會出現「交易成功」的訊息，再次確認後，在網銀查詢交易明細應該就會有這筆來自 AdSense 的西聯匯款款項入帳：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;adsense&#x2F;_2021-01-27_08.03.27.png&quot; alt=&quot;京城銀行個人網路銀行&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;京城銀行個人網路銀行&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;jie-yu&quot;&gt;結語&lt;&#x2F;h2&gt;
&lt;p&gt;做為一個「小眾」、「純」個人網站經營者，AdSense 要能累積到一百美金是不容易的，但確實是可以的，會看到這篇的讀者應該也都是有過門檻的 AdSense 發佈商，恭喜發財！&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>初探 Strapi Headless CMS</title>
        <published>2021-01-02T00:00:00+00:00</published>
        <updated>2021-01-02T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2021/strapi/"/>
        <id>https://editor.leonh.space/2021/strapi/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2021/strapi/">&lt;p&gt;這篇是某個晚上試玩 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;strapi.io&#x2F;&quot;&gt;Strapi&lt;&#x2F;a&gt; 這套 headless CMS 的心得，主要是談 Strapi 和 headless CMS 帶來的變革，不太會談到具體的操作過程。&lt;&#x2F;p&gt;
&lt;p&gt;先談談 headless CMS。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;headless-cms&quot;&gt;Headless CMS&lt;&#x2F;h2&gt;
&lt;p&gt;Headless CMS 是前後端分離概念下的產物，headless CMS 可以簡單的理解為剝去前端的 CMS，headless CMS 以 API 的方式（通常是 RESTful API 或 GraphQL）供應前端內容，前端（通常是 Aurelia、Svelte、Vue、React、Angular）也透過 API 與 headless CMS 溝通，取得內容呈現，或發送內容回 headless CMS。&lt;&#x2F;p&gt;
&lt;p&gt;在上面的前後端分離的架構下，headless CMS 必須具備幾項特性：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;管理內容的能力，包括內容的欄位、資料型態、欄位關聯性、以及內容本身，以開發的角度講，就是 model 的制定與管理。另外一種內容是媒體管理，圖片、音檔、影片、PDF 等的媒體資產管理。&lt;&#x2F;li&gt;
&lt;li&gt;管理資料庫的能力，上面的內容都必須對應到資料庫，以開發的角度講，就是 ORM。&lt;&#x2F;li&gt;
&lt;li&gt;管理 API 的能力，上面的內容（model）除了向下對應到資料庫外，向外也要有對應的 API，並且 model、table、API 的連動是自動化的。&lt;&#x2F;li&gt;
&lt;li&gt;除了主要的內容外，還必須有權限、身份認證等系統必備的 API。&lt;&#x2F;li&gt;
&lt;li&gt;上面的每個特性都是有一個後台界面（Admin Panel）可以讓一般人操作，而不是只能透過程式碼的方式操作。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;從上面幾點可以看出 headless CMS 相較於典型的 MCV web 框架（如 Masonite、Laravel），多了幾項特性：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Model 是可以由用戶在 Admin Panel 自行定義的，不用由開發人員施工。&lt;&#x2F;li&gt;
&lt;li&gt;Controller 是自動化建構的，只要在 Admin Panel 定義好 model，API 就會自動產生，不用開發人員施工。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在這樣的特性下，配合大前端時代的降臨，大部分的業務邏輯都往前端實做，開發人員的精力完全可以投注在前端工程上，headless CMS 的角色就專注於當個稱職的網站後端或應用後端，是不是很棒？&lt;&#x2F;p&gt;
&lt;h2 id=&quot;strapi&quot;&gt;Strapi&lt;&#x2F;h2&gt;
&lt;p&gt;Strapi 是個開源的 headless CMS 系統，底層則是 Node.js 的 web 框架 Koa。&lt;&#x2F;p&gt;
&lt;p&gt;依照 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;strapi.io&#x2F;documentation&#x2F;developer-docs&#x2F;latest&#x2F;getting-started&#x2F;quick-start.html&quot;&gt;Strapi 的文件&lt;&#x2F;a&gt;把範例建起來之後，在 Strapi Admin Panel 內建了一個 Restaurant 的 model（Strapi 稱為 Content Type）：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;strapi&#x2F;1_ospUN-fDiVGkzoOEJ-Rnuw.png&quot; alt=&quot;Strapi&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;Strapi 會自動幫我們產生 API 與文件：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;strapi&#x2F;1_2KOMq6z79CY640RmN9hXWg.png&quot; alt=&quot;Strapi OpenAPI 文件&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;而在專案目錄內，Strapi 會自動幫我們配置出 Restaurant 的路由、model 和 API：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;my-project&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;┣ api&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;┃ ┗ restaurant&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;┃   ┣ config&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;┃   ┃ ┗ routes.json&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;┃   ┣ controllers&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;┃   ┃ ┗ restaurant.js&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;┃   ┣ documentation&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;┃   ┃ ┗ 1.0.0&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;┃   ┃   ┣ overrides&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;┃   ┃   ┗ restaurant.json&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;┃   ┣ models&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;┃   ┃ ┣ restaurant.js&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;┃   ┃ ┗ restaurant.settings.json&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;┃   ┗ services&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;┃     ┗ restaurant.js&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;┣ config&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;┃ ┣ functions&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;┃ ┃ ┣ responses&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;┃ ┃ ┃ ┗ 404.js&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;┃ ┃ ┣ bootstrap.js&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;┃ ┃ ┗ cron.js&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;┃ ┣ database.js&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;┃ ┗ server.js&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;┣ extensions&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;┃ ┣ documentation&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;┃ ┣ email&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;┃ ┣ upload&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;┃ ┗ users-permissions&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;┗ public&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ┣ uploads&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ┗ robots.txt&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到，如果有需要的話，可以再對 controller、model、service 做開發，下面分別看看這些原始碼的內容與架構。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;routing&quot;&gt;Routing&lt;&#x2F;h3&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;json&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;routes&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: [&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;method&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;GET&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;path&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;&#x2F;restaurants&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;handler&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;restaurant.find&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;config&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;                &amp;quot;policies&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: []&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;method&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;GET&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;path&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;&#x2F;restaurants&#x2F;count&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;handler&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;restaurant.count&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;config&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;                &amp;quot;policies&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: []&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;method&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;GET&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;path&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;&#x2F;restaurants&#x2F;:id&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;handler&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;restaurant.findOne&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;config&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;                &amp;quot;policies&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: []&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;method&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;POST&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;path&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;&#x2F;restaurants&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;handler&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;restaurant.create&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;config&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;                &amp;quot;policies&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: []&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;method&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;PUT&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;path&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;&#x2F;restaurants&#x2F;:id&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;handler&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;restaurant.update&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;config&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;                &amp;quot;policies&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: []&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;method&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;DELETE&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;path&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;&#x2F;restaurants&#x2F;:id&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;handler&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;restaurant.delete&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;config&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;                &amp;quot;policies&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: []&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    ]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;model&quot;&gt;Model&lt;&#x2F;h3&gt;
&lt;p&gt;欄位定義在 api&#x2F;models&#x2F;restaurant.settings.json：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;json&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;kind&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;collectionType&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;collectionName&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;restaurants&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;info&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;name&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;restaurant&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;description&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;options&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;increments&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;timestamps&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;draftAndPublish&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; true&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;    &amp;quot;attributes&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;name&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;string&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;required&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; true&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;unique&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; true&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;description&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;type&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;richtext&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        },&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;        &amp;quot;BGM&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;collection&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;file&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;via&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;related&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;allowedTypes&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: [&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;                &amp;quot;images&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;                &amp;quot;files&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;                &amp;quot;videos&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;            ],&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;plugin&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;upload&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;            &amp;quot;required&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; false&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;在 Admin Panel 定義的 model（Content Type）以及欄位都會有相對的 JSON 定義檔產生，這樣的好處是可以讓欄位定義檔本身也被 Git 管理，這也才有辦法讓其他的程式邏輯（如 controller）和 model 一同接受版控的管理。&lt;&#x2F;p&gt;
&lt;p&gt;另外一個是 model 的程式邏輯，在 api&#x2F;restaurant&#x2F;models&#x2F;restaurant.js：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;use strict&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;&#x2F;**&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; * Read the documentation (https:&#x2F;&#x2F;strapi.io&#x2F;documentation&#x2F;developer-docs&#x2F;latest&#x2F;concepts&#x2F;models.html#lifecycle-hooks)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; * to customize this model&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; *&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;module&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;exports&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {};&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;內容相當簡單，只有一段引導我們去看 model 開發文件的註解。&lt;&#x2F;p&gt;
&lt;p&gt;後面的 controller、service 也都是類似的內容。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;controller&quot;&gt;Controller&lt;&#x2F;h3&gt;
&lt;p&gt;檔案在 api&#x2F;controllers&#x2F;restaurant.js：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;use strict&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;&#x2F;**&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; * Read the documentation (https:&#x2F;&#x2F;strapi.io&#x2F;documentation&#x2F;developer-docs&#x2F;latest&#x2F;concepts&#x2F;controllers.html#core-controllers)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; * to customize this controller&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; *&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;module&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;exports&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {};&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;service&quot;&gt;Service&lt;&#x2F;h3&gt;
&lt;p&gt;檔案在 api&#x2F;services&#x2F;restaurant.js：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;use strict&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;&#x2F;**&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; * Read the documentation (https:&#x2F;&#x2F;strapi.io&#x2F;documentation&#x2F;developer-docs&#x2F;latest&#x2F;concepts&#x2F;services.html#core-services)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; * to customize this service&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; *&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;module&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;exports&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; {};&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;strapi-de-kuo-chong-ji-zhi&quot;&gt;Strapi 的擴充機制&lt;&#x2F;h3&gt;
&lt;p&gt;實際在 Strapi Admin Panel 定義好 Restaurant 以及看過專案目錄內的檔案後，可以歸納一下 Strapi 的設計及它的擴充機制，前面提過，在商業邏輯往前端移動的大前端時代的背景下，像 Strapi 這樣傻瓜型的 headless CMS 可以很快速讓我們定義出 model 的欄位以及產出相對應的 API 及文件，但因為 Strapi 依然是基於傳統的 web 框架 Koa，它還是保留了所有後端開發的架構，這樣的設計兼顧了速度與彈性。&lt;&#x2F;p&gt;
&lt;p&gt;在 Admin Panel 方面，除了 model 的定義與內容的管理外，看起來略顯陽春，但根據 Straip 的文件，&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;strapi.io&#x2F;documentation&#x2F;developer-docs&#x2F;latest&#x2F;admin-panel&#x2F;customization.html&quot;&gt;Admin Panel 也是可以被客製的&lt;&#x2F;a&gt;，另外 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;strapi.io&#x2F;documentation&#x2F;developer-docs&#x2F;latest&#x2F;plugins&#x2F;documentation.html&quot;&gt;Strapi本身也有設計 plugin 的機制&lt;&#x2F;a&gt;，包括 Strapi 自己的 GraphQL 也是以一支獨立的 plugin 的方式被使用。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zong-jie&quot;&gt;總結&lt;&#x2F;h2&gt;
&lt;p&gt;歸納一下 Strapi 的特點：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;有 Admin Panel 用於定義資料與管理資料。&lt;&#x2F;li&gt;
&lt;li&gt;定義的資料會自動產出 API 與 API 文件給前端使用。&lt;&#x2F;li&gt;
&lt;li&gt;在 Admin Panel 定義的資料型態都會以 JSON 的格式儲存，因此可以被版控系統管理。&lt;&#x2F;li&gt;
&lt;li&gt;還是可以自行做後端開發與客製。&lt;&#x2F;li&gt;
&lt;li&gt;開源，可以自架，資料庫也放在自己家。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;好處很明顯，API 的制定變得簡單又快速，time to market 時間可以省掉一半（寫後端的那一半）。&lt;&#x2F;p&gt;
&lt;p&gt;同場加映幾個也頗具特色的 headless CMS 及其它相關專案：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Slicknode：headless CMS「服務」，無開源，資料放在 Slicknode 家，特色是跑在 AWS serverless 平台上，感覺比 Strapi 能應付更大的存取需求。&lt;&#x2F;li&gt;
&lt;li&gt;Directus：和 Strapi 特色類似，也是開源專案，目前底層是 PHP 和 Zend，下一版 Directus 9 會改用 Node.js。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2023&#x2F;pydantic&#x2F;&quot;&gt;FastAPI&lt;&#x2F;a&gt;：把 headless CMS 的前台界面（如 Strapi 的 Admin Panel）再剝離的 web 框架，FastAPI 顧名思義是專門為 API 設計的框架，在程式碼內定義好 route、model、function 後 FastAPI 就會自動產出 API 文件，FastAPI 還有其它專為 API 設計的特性，可以訪問 FastAPI 網站了解。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;bu-chong&quot;&gt;補充&lt;&#x2F;h2&gt;
&lt;p&gt;Strapi 有提供 rich text 型態的欄位，它在編輯區是以 Markdown 的方式做編輯，如下圖：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;strapi&#x2F;1_F1lw5S1-IThY6QuNFThQEw.png&quot; alt=&quot;Strapi&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;不過大家都知道 Markdown 本身的格式是受限的，例如不能指定 &lt;code&gt;id&lt;&#x2F;code&gt;、&lt;code&gt;class&lt;&#x2F;code&gt;，也不能改文字顏色，雖然 Markdown 允許在內文中直接插入 HTML，不過這樣就失去了這個 Admin Panel 存在的重要特性之一：讓非開發人員可以在此管理內容，殘念です。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>寫作平台文章閱讀介面比較</title>
        <published>2020-12-24T00:00:00+00:00</published>
        <updated>2020-12-24T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2020/writing-platform-comparison/"/>
        <id>https://editor.leonh.space/2020/writing-platform-comparison/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2020/writing-platform-comparison/">&lt;p&gt;最近我們在公司內啟動了一個新的計劃 — — 鼓勵所有同仁建立自己的網誌，在上面分享自己工作上的心得筆記，或是自己個人的 side project，抑或是更廣泛的資訊產業觀察、應用等的文章，在鼓勵同仁寫作的同時，在公司的網誌上我們也打算曝光在更多的平台上，因此一輪的寫作平台的比較就開始了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xie-zuo-ping-tai-de-jue-ze&quot;&gt;寫作平台的抉擇&lt;&#x2F;h2&gt;
&lt;p&gt;在挑選曝光平台的抉擇上，因為人力畢竟是有限的，實務上不可能在每個平台都曝光，從 20 年前的 Blogger 到現在當紅的 Medium 都評估一輪後，再回到我們想增加公司文章曝光的初衷，我們先把寫作平台分成兩類：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;有編輯經營的，也就是在平台首頁有編輯（不論是人腦還是電腦）選文的。這類的平台包括 Medium、探路客 Timelog、方格子 Vocus、Matters、Wreadit、uMedia 等。&lt;&#x2F;li&gt;
&lt;li&gt;沒有編輯經營的，也就是平台首頁僅作為介紹平台功能之用，沒有編輯選文的。這類的平台大約都較早期，包括 WordPress.com、Blogger、Tumblr 等。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;至於痞客邦和隨意窩 Xuite，雖然他們也有編輯選文，不過一開始就不在考慮的名單內，因為那版面實在是不忍直視⋯⋯。&lt;&#x2F;p&gt;
&lt;p&gt;站在公司網誌的立場，我們比較偏好有編輯的平台，這類平台通常也具備某種程度的社交元素，讀者可以對你的文章表示喜歡（讚／星星／愛心）以及訂閱你的文章的功能，訂閱後有新文章刊出還會出現在讀者的首頁上，有機會讓我們的文章得到更好的傳播性。反之沒有編輯的平台就只能視為「網誌代管平台」，和自架差不多，傳播的管道只有粉絲團和 Google 的自然／付費流量。&lt;&#x2F;p&gt;
&lt;p&gt;以有／無編輯來做完初步的篩選之後，下一輪我們來比較他們的單篇文章閱讀介面，下文都會站在一個公司網誌的觀點來評論各家平台的特點。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;medium&quot;&gt;Medium&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;writing-platform-comparison&#x2F;1_9wFZrUaD2T8FC5EaojFT2A.png&quot; alt=&quot;Medium&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Medium 是這波「新・寫作平台」的領頭羊，它是業界標竿，也是下面其他平台比較上的基準，仔細比較下來可以發現，Medium 的確是在平台、作者、讀者間的平衡抓得最好的平台，不論是版面設計或廣告的呈現樣式都在三方間做到很好的體驗，具體的差異可以看下面各個平台的比較。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;tan-lu-ke-timelog&quot;&gt;探路客 Timelog&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;writing-platform-comparison&#x2F;1_3twuiA4_-uVCnn86jxAcSA.png&quot; alt=&quot;探路客&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;探路客大概是這批寫作平台中最年輕的，最近也一直在 FB 看到他們的廣告，也和 Medium 最相似，這樣的相似在我們看來是好的，因為下面的其他平台或多或少都添加了某些我們不太喜歡的元素，這些元素對於像我們這樣是要做「企業網誌」取向的網誌來說是有顧慮的。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fang-ge-zi-vocus&quot;&gt;方格子 Vocus&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;writing-platform-comparison&#x2F;1_8tAO0csekry0CMCjK7nJug.png&quot; alt=&quot;方格子&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;方格子的頁面最上方有兩行導航條，第一行導航條有著「主題徵文」與「方格人物」的站方活動，第二行導航條則是站方的文章分類。這兩行導航條的作用都是為了吸引讀者到「別人的網誌」閱讀其他文章，站在作者（不論是個人或是公司）的角度來看，這兩行導航條的存在會讓讀者隱隱約約覺得作者只是方格子這個大網站內數千萬個作者的其中之一，讓作者以及文章變成方格子的附屬的存在，就好像方格子是一本線上雜誌，導航條內的分類相當於分類專欄，而作者只是方格子的專欄作家，這也就是我們認為 Medium 和探路客較優秀的所在 — — 他們並不過度強調寫作平台的存在，你的網誌就是你的網誌，而不用附帶著幫平台導引讀者的功能，即便讀著們很清楚眼前這個網誌是使用 Medium 的服務。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;matters&quot;&gt;Matters&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;writing-platform-comparison&#x2F;1_YxiWshHUF3fuFk7Hpx7deg.png&quot; alt=&quot;Matters&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Matters 的版面較為與眾不同，呈現三欄式設計，左邊的「發現」和「追蹤」都是提供給讀者的功能，做用同樣也是導引讀者去看別人的文章，與方格子的導航條有同樣的屬性，同樣地，站在企業網誌的出發點，我們覺得這樣的元素是不適合的。&lt;&#x2F;p&gt;
&lt;p&gt;在中欄的下方有鼓勵讀者登入的區塊，這個區塊的文字令人感到疑惑，在 Matters 看文章是不需要登入的，不知道為什麼吸引讀者登入的宣傳標題會寫上「看不過癮」？另一方面，寫作平台真正要鼓勵註冊的對象應該是作者，越多的作者才有可能有越多優質的文章，才有可能吸引到越多的讀者，寫作平台作為一個同時服務作者和讀者的中間者，它要努力的方向應該是先擴大供給，再以足夠豐富的內容向讀者推廣，才有可能走向正向循環，類似的中間者角色還有遊戲機和房仲，遊戲機是靠遊戲大作來吸引玩家，房仲是靠多房源來吸引買家，他們都是先培養供給端再推廣給需求端的商業型態，而 Matters 卻在版面上大打著鼓勵讀者加入會員的廣告，令人不解。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wreadit&quot;&gt;Wreadit&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;writing-platform-comparison&#x2F;1_pqk1Gs2QHNA3Ig38oEpTWA.png&quot; alt=&quot;Wreadit&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Wreadit 是&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.breaktime.com.tw&#x2F;&quot;&gt;富盈數據&lt;&#x2F;a&gt;新成立的寫作平台，除了版面向 Medium 致敬外，經營的方向看起來和既有的 ZiMedia 差不多——歡迎作者把 Wreadit 當作第二個家。&lt;&#x2F;p&gt;
&lt;p&gt;Wreadit 版面也很乾淨，但內文中會穿插廣告，以及最上方的全站導航條元素給我們一樣的顧慮 — — 作者只是平台的專欄作家，而不是作者自己的網誌。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;umedia&quot;&gt;uMedia&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;writing-platform-comparison&#x2F;1_zTiT4gBbHzQmiJspn0rnog.png&quot; alt=&quot;uMedia&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;uMedia 是聯合報 2019 年成立的寫作平台，版面最上方有著簡單粗暴的大橫幅廣告，緊接著的是全站導航條。&lt;&#x2F;p&gt;
&lt;p&gt;現在都已經有很成熟的文章內廣告或版面內廣告了，不能理解為什麼 uMedia 要在最上面放一個這麼突兀的大廣告（數據上顯示突兀的廣告點擊率會比較高？）。&lt;&#x2F;p&gt;
&lt;p&gt;顯而易見的，uMedia 有著搶眼的廣告和全站導航條的負面元素，不過聯合報身為全台灣前十大的網站，uMedia 的傳播力也是不容小覷的，在 uMedia 的文章，&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;udn.com&#x2F;umedia&#x2F;story&#x2F;12915&#x2F;4105729&quot;&gt;是有可能會被選入聯合新聞網的&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zong-jie&quot;&gt;總結&lt;&#x2F;h2&gt;
&lt;p&gt;在以「企業網誌」為出發點，評估寫作平台時，我們比較重視的元素是在我們的網誌內希望能呈現的是以&lt;strong&gt;作者為主體的版面設計，而不是令人感覺這只是平台內的某個作者的一篇文章&lt;&#x2F;strong&gt;的感覺，如果以電商網站來比喻，我們比較喜歡看起來像是獨立商城而不是附屬商店，以墊腳石為例，他們同時有自家的電商平台以及其他商城的通路：&lt;&#x2F;p&gt;
&lt;div class=&quot;wide&quot; style=&quot;display: grid; grid-template-columns: auto auto; gap: 1vw;&quot;&gt;
    &lt;figure&gt;
        &lt;img alt=&quot;墊腳石購物網&quot; src=&quot;1_SvjttM7Lp126hWwg0-sVBw.png&quot;&gt;
        &lt;figcaption&gt;墊腳石購物網&lt;&#x2F;figcaption&gt;
    &lt;&#x2F;figure&gt;
    &lt;figure&gt;
        &lt;img alt=&quot;momo 商城內的墊腳石&quot; src=&quot;1_szsWpN_c1MCiXaVaxk0ung.png&quot;&gt;
        &lt;figcaption&gt;momo 商城內的墊腳石&lt;&#x2F;figcaption&gt;
    &lt;&#x2F;figure&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;在 momo 的店面上面也附加了許多 momo 自己的促銷廣告。Medium、探路客以外的寫作平台給人的感受就像這樣 — — 一個附屬於平台內的作者。
本文站在公司網誌作者和讀者的角度評論了幾個新的寫作平台的版面特色，然而實際上還有更多的面向可以對它們做比較，包括寫作介面的 UX、功能面的比較等，這些面向在不同的作者心中都各自佔有不同的比重，希望這篇文章能對也在選擇寫作平台的您有幫助，感謝您的閱讀。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>設定 Windows 的 SSH</title>
        <published>2020-10-06T00:00:00+00:00</published>
        <updated>2020-10-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2020/windows-ssh/"/>
        <id>https://editor.leonh.space/2020/windows-ssh/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2020/windows-ssh/">&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;Secure_Shell&quot;&gt;SSH&lt;&#x2F;a&gt; 是在 Linux 世界行之有年的遠端登入工具，近年來在 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;cloudblogs.microsoft.com&#x2F;windowsserver&#x2F;2015&#x2F;05&#x2F;06&#x2F;microsoft-loves-linux&#x2F;&quot;&gt;Microsoft ❤️ Linux&lt;&#x2F;a&gt; 的政策下，微軟也把 OpenSSH 納入 Windows 10 內，成為可選用的元件之一。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;an-zhuang-yu-she-ding-ssh&quot;&gt;安裝與設定 SSH&lt;&#x2F;h2&gt;
&lt;p&gt;下面紀錄在 Windows 10 安裝與設定 OpenSSH 的過程&lt;&#x2F;p&gt;
&lt;p&gt;到設定 → 應用程式與功能 → 選用功能 → 新增功能，找到 OpenSSH 伺服器，把它安裝：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;windows-ssh&#x2F;OpenSSH.png&quot; alt=&quot;OpenSSH 伺服器&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;裝完之後 OpenSSH 會以服務的形式在系統內，所以還要去服務管理那邊把 SSH 服務打開，並讓它自動執行。&lt;&#x2F;p&gt;
&lt;p&gt;開始 → Windows 系統管理工具 → 服務，找到 OpenSSH，把它改為自動啟動：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;windows-ssh&#x2F;OpenSSH-service.png&quot; alt=&quot;OpenSSH 服務&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;讓服務跑起來後，最後還要去確認防火牆，在安裝 OpenSSH 的同時，安裝腳本其實也幫我們的防火牆設定了一條給 SSH 用的開放埠 22 的規則，現在只要再次確認即可。&lt;&#x2F;p&gt;
&lt;p&gt;開始 → Windows 系統管理工具 → 具有進階安全性的 Windows Defender 防火牆，找到 OpenSSH SSH Server (sshd) 這條，確認是已啟用即可：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;windows-ssh&#x2F;OpenSSH-firewall.png&quot; alt=&quot;OpenSSH 防火牆&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;以上簡單幾個步驟就可以把 SSH 裝進 Windows 內，真是輕鬆寫意。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;lian-ru&quot;&gt;連入&lt;&#x2F;h2&gt;
&lt;p&gt;SSH 連入的指令，通常會是長這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;powershell&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;ssh &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;Bill Gates&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;@microsoft.com&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;有幾點要注意：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;帳號如果有空白，記得要用雙引號包起來，像上面的例子一樣。&lt;&#x2F;li&gt;
&lt;li&gt;帳號是認 C:\Users\&lt;strong&gt;bill gates&lt;&#x2F;strong&gt;\ 的「bill gates」，而不是在設定 app 裡面的帳戶名稱。&lt;&#x2F;li&gt;
&lt;li&gt;帳號必須要有密碼才能用 SSH 登入，無密碼的帳號無法登入。&lt;&#x2F;li&gt;
&lt;li&gt;OpenSSH 的其它命令列工具也可以使用。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;can-kao-zi-liao&quot;&gt;參考資料&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.microsoft.com&#x2F;zh-tw&#x2F;windows-server&#x2F;administration&#x2F;openssh&#x2F;openssh_overview&quot;&gt;Windows 的 OpenSSH&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>OneAll 第三方登入整合平台</title>
        <published>2020-09-15T00:00:00+00:00</published>
        <updated>2020-09-15T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2020/oneall/"/>
        <id>https://editor.leonh.space/2020/oneall/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2020/oneall/">&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;oneall&#x2F;89934464_2856514921095334_8434078500935172096_o.png&quot; alt=&quot;OneAll&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;http:&#x2F;&#x2F;www.oneall.com&#x2F;&quot;&gt;OneAll&lt;&#x2F;a&gt; 是個第三方登入的整合服務，提供單一的 SDK 或 API 可以整合進我們自己的產品提供用戶第三方登入的功能，OneAll 支援的平台超過四十個，下面會示範 OneAll 與 Facebook、Google、LINE 的設定流程。&lt;&#x2F;p&gt;
&lt;p&gt;雖然說 OneAll 已經提供了單一的接口，但在程式串接面以外的部份，包括在各大平台開立帳號、註冊應用、取得密鑰、送審等行政流程還是少不了的，所以即使用了 OneAll，其實還是要對各大平台的審查規範要有所了解。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;oneall-jian-li-zhan-tai&quot;&gt;OneAll 建立站台&lt;&#x2F;h2&gt;
&lt;p&gt;本文跳過申請 OneAll 帳號的部份。&lt;&#x2F;p&gt;
&lt;p&gt;在登入 OneAll 後會進入 OneAll 後台，在後台建一個站台：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;oneall&#x2F;app.oneall.com_applications_setup_start_.png&quot; alt=&quot;OneAll - Create a new Site&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;上圖的那個 domain name 只是識別用，並非用於限定調用 OneAll API，所以可以放心填。限定調用 OneAll API 的設定在下一頁：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;oneall&#x2F;app.oneall.com_applications_setup_confirm__application_setup_tokenfa11923c-ec97-48eb-bb5c-6341bd016ebf.png&quot; alt=&quot;OneAll - Quickly review the settings&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;上圖這頁才是設定 API 網址與限定呼叫網域的地方。&lt;&#x2F;p&gt;
&lt;p&gt;站台建立成功後，進入站台的控制台：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;oneall&#x2F;app.oneall.com_applications_application__applicationid721062.png&quot; alt=&quot;OneAll - Sites&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;上面的綠色訊息提示我們要在自己的 app 內裝設 OneAll 的 Plugin 或 SDK，左邊選單的「Plugins &amp;amp; Tools」裡面可以看到 OneAll 提供了幾乎所有主流平臺／框架／語言的整合工具，不過看看就好，我們繼續回到 OneAll 設定第三方登入，之後再做整合的工作。&lt;&#x2F;p&gt;
&lt;p&gt;在 Sites → Social Networks 頁面可以看到一大堆第三方登入的平台：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;oneall&#x2F;_2020-07-10_14.33.55.png&quot; alt=&quot;OneAll - Social Networks&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;上面的黃色訊息提示我們大部分的第三方登入都需要在它們的平台上申請 app ID／密碼／密鑰之類的憑證，再把憑證填入 OneAll 的平台，由 OneAll 統一幫我們串接那些第三方登入平台，而我們自己的 app 則只要串接 OneAll API 即可，雖然各大平台的憑證還是要自己花時間申請，但對串接程式撰寫時間確實是可以大幅節省的，並且各大平台 API 也都會改版，這部分也都是 OneAll 會處理好，我們只要串好 OneAll 即可。&lt;&#x2F;p&gt;
&lt;p&gt;上圖中每個圖示左方的色條顯示了這個平台的第三方登入啟用狀態，紅色表示未啟用，綠色表示啟用，可以看到上圖有一些已經是綠色的，那是因為某些少數平台不需要憑證的關係。&lt;&#x2F;p&gt;
&lt;p&gt;下文是台灣比較多人用的 Facebook、Google、LINE 的設定紀錄，至於我們自己的 app 與 OneAll 的串接部分則會另外做一集說明。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;she-ding-facebook-deng-ru&quot;&gt;設定 Facebook 登入&lt;&#x2F;h2&gt;
&lt;p&gt;要設定 Facebook 登入，需要在 FB 開立一個 app，再在 app 內申請 Facebook 登入的權限。而這可能還會牽扯到相關企業管理平台的&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.facebook.com&#x2F;business&#x2F;help&#x2F;1095661473946872?id=180505742745347&quot;&gt;商家驗證&lt;&#x2F;a&gt;或開發人員的個人驗證，下文會以都已驗證過的情況進行。&lt;&#x2F;p&gt;
&lt;p&gt;以下操作都在 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;developers.facebook.com&#x2F;apps&#x2F;&quot;&gt;FACEBOOK for Developers&lt;&#x2F;a&gt; 網站進行。&lt;&#x2F;p&gt;
&lt;p&gt;新增應用程式，程式用途為「任何其它用途」：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;oneall&#x2F;_2020-07-10_15.12.44.png&quot; alt=&quot;FACEBOOK for Developers - 建立應用程式編號&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;下一步要填一些基本資料：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;oneall&#x2F;_2020-07-10_15.17.28.png&quot; alt=&quot;FACEBOOK for Developers - 建立應用程式編號&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;就照自己的實際狀況填入，在 OneAll 的網頁也有手把手的教學，可以參考 OneAll 提供的範本填入。&lt;&#x2F;p&gt;
&lt;p&gt;進到 app 的主控板，可以看到一系列 FACEBOOK 的開發者產品，我們要申請「Facebook 登入」：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;oneall&#x2F;_2020-07-10_15.32.07.png&quot; alt=&quot;FACEBOOK for Developers - 新增產品&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;平台選「網站」：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;oneall&#x2F;_2020-07-10_15.33.57.png&quot; alt=&quot;FACEBOOK for Developers - 快速入門&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;網址依照實際網址填入：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;oneall&#x2F;_2020-07-10_15.36.07.png&quot; alt=&quot;FACEBOOK for Developers - 快速入門&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;後續的 2. 設定 Facebook JavaScript SDK、3. 檢查登入狀態、4. 新增「Facebook  登入」按鈕、5. 後續步驟都快速走過看過即可，這些步驟 OneAll 都已經處理好。&lt;&#x2F;p&gt;
&lt;p&gt;接著到「Facebook 登入」→「設定」：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;oneall&#x2F;_2020-07-10_15.45.02.png&quot; alt=&quot;應用程式主控板 - 用戶端與 OAuth 設定&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;參照 OneAll 的說明設定與填入 OAuth 重新導向 URI。&lt;&#x2F;p&gt;
&lt;p&gt;接著設定基本資料，來到「設定」→「基本資料」：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;oneall&#x2F;_2020-07-10_15.51.04.png&quot; alt=&quot;應用程式主控板 - 基本資料&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;這邊的隱私政策網址請參照 OneAll 說明的提供，類別也依照 OneAll 說明設定，其它的部分依實際狀況填入。&lt;&#x2F;p&gt;
&lt;p&gt;最單純的 Facebook 登入只會提供用戶的姓名和信箱，是不用把 app 送審的，所以只要把頁面最上面的「調整中」切換成「上線」即完成 FB 這段的設定。&lt;&#x2F;p&gt;
&lt;p&gt;上圖的應用程式編號與應用應用程式密鑰則須回填到 OneAll 的 Facebook 設定頁內，如下圖：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;oneall&#x2F;_2020-07-10_16.13.07.png&quot; alt=&quot;OneAll - Facebook - Setup&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;接著回到 OneAll 的 Facebook 設定頁，應該會看到如下圖的頁面：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;oneall&#x2F;_2020-07-10_16.21.41.png&quot; alt=&quot;OneAll - Facebook&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;可以透過 Connection Test 試一下功能的正確性，或是看一下圖示順不順眼，有必要的話回到 FACEBOOK for Developers 做調整。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;she-ding-google-deng-ru&quot;&gt;設定 Google 登入&lt;&#x2F;h2&gt;
&lt;p&gt;Google 登入的套路與 Facebook 登入相似，也是要在 Google 開立一個 app（在 Google Cloud 稱為專案），並申請登入 API 的權限，取得密鑰，回填 OneAll，不過因為 Google API 現在都是在 Google Cloud 平台做管理，基本上要先有 Google Cloud 的基礎知識，才有辦法順利設定，如果你又是用 Google Cloud 組織成員帳號做申請，則會牽扯到更複雜的權限設定，在 Google 登入設定上有問題的朋友可以在下方留言討論。&lt;&#x2F;p&gt;
&lt;p&gt;下面的操作都會在 Google Cloud 平台作業，另外請搭配 OneAll 的 Google 設定說明一起服用。&lt;&#x2F;p&gt;
&lt;p&gt;一開始先登入到 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;console.developers.google.com&#x2F;cloud-resource-manager&quot;&gt;Google API 管理資源區&lt;&#x2F;a&gt;，建立一個專案：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;oneall&#x2F;_2020-07-10_17.37.51.png&quot; alt=&quot;Google APIs - 新增專案&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;名稱和 ID 可以自行取名，換掉 Google Cloud 幫我們預帶的值，勇敢地建下去！&lt;&#x2F;p&gt;
&lt;p&gt;建完回到管理資源，應該會如下圖：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;oneall&#x2F;_2020-07-10_17.58.43.png&quot; alt=&quot;Google APIs - 管理資源&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;接著在左上角的導覽選單 → API 和服務 → 資料庫，會列出 Google 的 API 產品，從左邊進入「社交」類別，如下圖：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;oneall&#x2F;_2020-07-10_18.08.01.png&quot; alt=&quot;Google APIs - 社交&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Google 登入屬於 Google People API，猛擊進入 Google People API 頁面，把它啟用：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;oneall&#x2F;_2020-07-10_18.11.11.png&quot; alt=&quot;Google APIs - Google People API&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;啟用後會跳轉到 People API 的設定頁面：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;oneall&#x2F;_2020-07-10_18.12.54.png&quot; alt=&quot;Google APIs - People API&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;上面的訊息提示我們要有憑證，因此我們再進到導覽選單 → API 和服務 → 憑證：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;oneall&#x2F;_2020-07-10_18.14.56.png&quot; alt=&quot;Google APIs - 憑證&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;好像在打 RPG 一樣，它又提示我們要要去設定同意畫面，如下圖：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;oneall&#x2F;_2020-07-10_18.29.27.png&quot; alt=&quot;Google APIs - OAuth 同意畫面&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;此處參照 OneAll 的說明，選「外部」，按「建立」進入下個步驟，填表單文書作業：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;oneall&#x2F;_2020-07-10_19.29.48.png&quot; alt=&quot;Google APIs - OAuth 同意畫面&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;內容請參照 OneAll 提供的值填入。&lt;&#x2F;p&gt;
&lt;p&gt;應用程式首頁連結填我們自己網站的首頁，隱私權政策連結也是填入自己網站上的隱私權政策頁面的網址。&lt;&#x2F;p&gt;
&lt;p&gt;另外右邊的說明最好也讀過，確保遵守 Google 的規則，注意如果有上傳標誌，則必須經過 Google 的驗證，這點比 FB 嚴格。&lt;&#x2F;p&gt;
&lt;p&gt;填完後就「儲存」，又被跳轉到下一頁：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;oneall&#x2F;_2020-07-10_18.48.18.png&quot; alt=&quot;Google APIs - OAuth 同意畫面&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;因為我有放 logo 的關係，所以必須送交驗證，先把驗證放一邊，回到主線任務，回到左邊的憑證頁面建立憑證，選擇「OAuth 客戶端 ID」，然後繼續進行填表文書作業：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;oneall&#x2F;_2020-07-10_18.54.05.png&quot; alt=&quot;Google APIs - 建立 OAuth 用戶端 ID&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;這頁要填的內容依照 OneAll 提供的值填入，然後按「建立」，跳轉回憑證區首頁：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;oneall&#x2F;_2020-07-10_18.58.08.png&quot; alt=&quot;Google APIs - OAuth 用戶端已建立&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;這裡跳出的客戶端 ID 和密碼要填回 OneAll 的設定頁：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;oneall&#x2F;_2020-07-10_19.03.19.png&quot; alt=&quot;OneAll - Google&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;如果是免驗證的專案，到這邊就完成了。&lt;&#x2F;p&gt;
&lt;p&gt;如果是要驗證的專案，一樣要邊參考 OneAll 的說明邊進行。&lt;&#x2F;p&gt;
&lt;p&gt;回到 Google 的「API 和服務」→ 「網域驗證」：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;oneall&#x2F;_2020-07-10_19.07.52.png&quot; alt=&quot;Google APIs - 網域驗證&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;沒有第二條路，只能「Add domain」：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;oneall&#x2F;_2020-07-10_19.11.06.png&quot; alt=&quot;Google APIs - 網域驗證&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;這裡要填的網域請參考 OneAll 提供的值。&lt;&#x2F;p&gt;
&lt;p&gt;接著 Google 又要求要到 Search Console 去驗證這網域，繼續解任務。&lt;&#x2F;p&gt;
&lt;p&gt;Google 網站管理員中心把網域加入，驗證步驟選「其他方法」的「HTML 標記」如下圖：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;oneall&#x2F;_2020-07-10_19.16.52.png&quot; alt=&quot;Google 網站管理員中心 - 驗證&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;把那行 &lt;code&gt;&amp;lt;meta&amp;gt;&lt;&#x2F;code&gt; 標籤先貼回 OneAll 更新 OneAll 設定後再回到 Google 網站管理員中心按驗證，驗證成功應該會長這樣：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;oneall&#x2F;_2020-07-10_19.20.12.png&quot; alt=&quot;Google 網站管理員中心 - 驗證&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;接著回 Google API 和服務 → 網域驗證，再加一次 OneAll 提供的網域應該就會成功了：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;oneall&#x2F;_2020-07-10_19.22.30.png&quot; alt=&quot;Google APIs - 網域驗證&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;文書作業還沒結束，還要回到「OAuth 同意畫面」那邊編輯應用程式：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;oneall&#x2F;_2020-07-10_19.29.48.png&quot; alt=&quot;Google APIs - OAuth 同意畫面&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;送審前再次確認：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;送審的應用程式標誌和網站上是一樣的嗎？應該要是一樣的，但允許不同的版型或配色變體。&lt;&#x2F;li&gt;
&lt;li&gt;應用程式首頁上應該要出現隱私權政策的連結。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;完整的規範請看 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;developers.google.com&#x2F;terms&#x2F;api-services-user-data-policy&quot;&gt;Google API Services User Data Policy&lt;&#x2F;a&gt; 和 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;support.google.com&#x2F;cloud&#x2F;answer&#x2F;9110914?hl=zh-Hant&quot;&gt;OAuth API verification FAQs&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;確定都符合規範後，看到最下面的「送交驗證」，勇敢地按下去，然後又是文書作業，把答案寫一寫交卷。送交驗證後就開始等待，祈禱 Google 的效率比公務員高一點。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;she-ding-line-deng-ru&quot;&gt;設定 LINE 登入&lt;&#x2F;h2&gt;
&lt;p&gt;設定 LINE 登入的套路也跟上面的很像。&lt;&#x2F;p&gt;
&lt;p&gt;以下步驟請搭配 OneAll 說明服用。&lt;&#x2F;p&gt;
&lt;p&gt;先到 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;developers.line.biz&#x2F;console&#x2F;&quot;&gt;LINE Developers Console&lt;&#x2F;a&gt;，登入後 Create 一個 provider，取名 OneAll。&lt;&#x2F;p&gt;
&lt;p&gt;接著 Create a new channel，選「LINE Login」，然後開始文書作業：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;oneall&#x2F;_2020-07-14_11.22.18.png&quot; alt=&quot;LINE Developers - Create a channel&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;因為 LINE Developers Console 有改版，有些欄位名稱與位置和 OneAll 的文件對不上，不過差異不大，正常人應該都可以自行腦補轉換，除非是比較特別的不然就不特別提了。&lt;&#x2F;p&gt;
&lt;p&gt;按下 Create 後回到這個 Channel 的主頁，有四個頁籤如下圖：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;oneall&#x2F;_2020-07-14_11.32.04.png&quot; alt=&quot;LINE Developers - Basic Settings&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在 Basic settings 這頁下方有 OpenID Connect 的區塊，必須申請才可以取得用戶的信箱，所以請勇敢的申請，screenshot 應該上傳網站對用戶宣告個資運用的截圖，或者是偷懶上傳網站圖示也有機會通過，因為目前 LINE 對這塊是採程序核准，非人工核准。&lt;&#x2F;p&gt;
&lt;p&gt;接著切換到「LINE login」頁，依照 OneAll 給的 Callback 網址填入，如下圖：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;oneall&#x2F;_2020-07-14_11.34.24.png&quot; alt=&quot;LINE Developers - LINE Login&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;接著把上面的「Developing」模式切換成「Published」。&lt;&#x2F;p&gt;
&lt;p&gt;接著在 Basic settings 頁找到 Channel ID 和 Channel secret，把它們填回 OneAll 的 LINE 設定頁，並且在 OneAll 的 LINE 設定頁按「Change Requested Data」，把「Email Address」勾選，讓 OneAll 幫我們向 LINE 索取用戶信箱。&lt;&#x2F;p&gt;
&lt;p&gt;LINE 申請起來感覺是最簡單的，FB 居中，Google 是最煩瑣的，充斥著大量的文書作業。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;oneall-de-shang-ye-mo-shi&quot;&gt;OneAll 的商業模式&lt;&#x2F;h2&gt;
&lt;p&gt;最後談談我對 OneAll 商業模式的觀察，在 OneAll 的收費標準裡，免費版的主要限制是每年允許兩千五百個獨立用戶登入，換算下來相當於每個月兩百個獨立用戶，以這樣的標準來看，每月兩百獨立用戶以上的產品，規模可說是相當大了，大到團隊內有足夠的資源可以自行串接各大平台的第三方登入，而省去被 OneAll 收取的費用，總的來說 OneAll 對免費用戶相當慷慨，但尷尬的是用戶可以在產品初期享用 OneAll 的免費額度，直到產品與團隊都夠大了，卻又因為團隊夠大而捨 OneAll 而去，導致 OneAll 對小用戶免費，對大用戶卻也收不了錢的窘境。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Plesk 升級筆記</title>
        <published>2020-08-09T00:00:00+00:00</published>
        <updated>2020-08-09T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2020/plesk-cli/"/>
        <id>https://editor.leonh.space/2020/plesk-cli/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2020/plesk-cli/">&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;plesk-cli&#x2F;plesk_hp_21.png&quot; alt=&quot;Plesk&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.plesk.com&#x2F;&quot;&gt;Plesk&lt;&#x2F;a&gt; 是一款伺服器管理系統，透過 Plesk，我們可以比較方便的在主機上配置各種服務與應用，例如最典型的虛擬主機，Plesk 可以幫助我們配置 web server 的 vhost 設定，另外像是資料庫、DNS、郵件、防火牆等的都可以在 Plesk 內做配置，在應用方面，也支援 PHP 服務，還可以指定每一個虛擬主機所要搭配的 PHP 版本，雖然以上這些事都可以透過手工自行配置，但 Plesk 提供了友善的 web 界面以及更為周全的配置設定，包括負載監控、遠端備份、系統更新、TLS 憑證管理、第三方服務整合等，對要建構一個可靠的服務而言，Plesk 是個很省力的選擇。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;plesk-cli&quot;&gt;Plesk CLI&lt;&#x2F;h2&gt;
&lt;p&gt;一般來說我們都是用 web 界面對 Plesk 做操控，但其實 Plesk 也是有提供 CLI 的工具讓我們可以在 shell 內操作 Plesk，當然，想要用 shell 操作 Plesk 的先決條件是我們必須可以透過 SSH 登進主機內，這點前提是一定要的，如果是自己的主機自己裝的 Plesk，那應該是都有 SSH，但若是外購主機，那很有可能主機商不會提供你 SSH 的權限，那只能殘念了。&lt;&#x2F;p&gt;
&lt;p&gt;下面記錄本人的 Plesk CLI 筆記，會隨著時間而增加內容。&lt;&#x2F;p&gt;
&lt;p&gt;Plesk 的命令就是 &lt;code&gt;plesk&lt;&#x2F;code&gt;，而因為 Plesk 本身是管理許多服務的軟體，所以通常也必須搭配 &lt;code&gt;sudo&lt;&#x2F;code&gt; 使用，容先敘明。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;geng-xin-plesk&quot;&gt;更新 Plesk&lt;&#x2F;h2&gt;
&lt;p&gt;Plesk 本身也是由許多的作業系統套件所組成，但它有自己的套件管理體系，與 Plesk 自己本身組件管理有關的命令通常都是被歸納在 &lt;code&gt;installer&lt;&#x2F;code&gt; 這個子命令內，例如要更新 Plesk 自己，使用以下命令：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; plesk installer update plesk&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;要注意，上面這行一旦執行就會立馬更新，不會再次確認，使用前務必確認前置作業是否已完成，例如備份—最重要也最常被忽略的事。&lt;&#x2F;p&gt;
&lt;p&gt;正常情況下，上面的命令會幫我們把 Plesk 自己更新，但有時會遇到有另外一個 Plesk Installer 安裝程序在執行，導致我們無法重複執行的問題，這時候我們必須先把既有的安裝程序終止：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; plesk installer stop&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面這行會把原有的 Plesk 安裝程序終止，終止之後再跑上面的更新命令應該就可以順利執行了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;sheng-ji-plesk&quot;&gt;升級 Plesk&lt;&#x2F;h2&gt;
&lt;p&gt;在 Plesk 目前的版次是這樣的：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;strong&gt;18.0.28 Update 3&lt;&#x2F;strong&gt;&lt;&#x2F;p&gt;
&lt;p&gt;前面的 18.0.28 表示版次，從 18.0.28 到 18.0.29 稱為升級（upgrade），以升級的命令處理，而從 18.0.28 到 18.0.28 Update1 稱為更新（update），以上節的 &lt;code&gt;update&lt;&#x2F;code&gt; 子命令處理。更新和升級雖然都是對軟體的改版，但更新往往表示規模較小，大多是一些錯誤修正或安全性補丁，而升級的版次跳號象徵著幅度更大的改版，除了錯誤修正與安全性補丁外，可能還會有新特性的加入或舊特性的退役，然而不管是更新還是升級，都建議在施做前，對系統做一次快照備份，施做後有問題才有機會挽回。&lt;&#x2F;p&gt;
&lt;p&gt;升級的命令與更新類似，但稍有不同的是更新必須明確的指定目標版號：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; plesk installer upgrade plesk&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 18.0.29&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面這行表示「把 Plesk 升級到 18.0.29 版」。&lt;&#x2F;p&gt;
&lt;p&gt;關於 Plesk 新版的釋出資訊，可以看 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.plesk.com&#x2F;release-notes&#x2F;obsidian&#x2F;change-log&#x2F;&quot;&gt;Change Log for Plesk Obsidian&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xiang-guan-wen-zhang&quot;&gt;相關文章&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;cloudflare-lightsail-plesk&#x2F;&quot;&gt;Plesk &#x2F; Cloudflare &#x2F; Lightsail 混合架構的安全規劃&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Cloudflare &#x2F; Lightsail &#x2F; Plesk 混合架構的安全規劃</title>
        <published>2020-08-07T00:00:00+00:00</published>
        <updated>2020-08-07T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2020/cloudflare-lightsail-plesk/"/>
        <id>https://editor.leonh.space/2020/cloudflare-lightsail-plesk/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2020/cloudflare-lightsail-plesk/">&lt;p&gt;本文紀錄 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.plesk.com&#x2F;&quot;&gt;Plesk&lt;&#x2F;a&gt; &#x2F; &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.cloudflare.com&#x2F;zh-tw&#x2F;&quot;&gt;Cloudflare&lt;&#x2F;a&gt; &#x2F; &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;aws.amazon.com&#x2F;tw&#x2F;lightsail&#x2F;&quot;&gt;Lightsail&lt;&#x2F;a&gt; 混合架構的安全規劃。&lt;&#x2F;p&gt;
&lt;p&gt;開頭先介紹一下這三樣服務的角色：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Lightsail 是 Amazon 的雲產品之一，可以理解為簡化版的 AWS，Lightsail 裡面的服務有通用虛擬機、資料庫、IP、儲存空間、快照備份這些，它們在 AWS 也都有相對應的服務，但在 Lightsail 這邊提供的是設定與費率更為簡化的版本，雖然設定簡化了，但享用與 AWS 同樣強健的基礎架構，很適合做為 AWS 的入門。在下文我們以一台 IP 為 5.1.2.1 的虛擬機為例。&lt;&#x2F;li&gt;
&lt;li&gt;Cloudflare 是以 DNS 與 CDN 服務為核心的公司，以核心服務為基礎拓展了一堆安全與遠端存取相關的系列服務，以及以 CDN 全球節點為基礎提供的媒體串流服務 Stream 與 serverless 服務 Workers 等，把原本無聊的 DNS 和 CDN 生意做的多采多姿，是本人的愛廠之一。&lt;&#x2F;li&gt;
&lt;li&gt;Plesk 是伺服器管理系統，和比較多人知道的 cPanel 比起來毫不遜色，而且有和 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;lightsail.aws.amazon.com&#x2F;ls&#x2F;docs&#x2F;zh_tw&#x2F;articles&#x2F;set-up-and-configure-plesk-stack-on-lightsail&quot;&gt;Lightsail 合作的佛心版&lt;&#x2F;a&gt;，或許是本人是先用過 Plesk 後才用 cPanel 的關係，一直較偏愛 Plesk，因此也造成了難以適應 cPanel 的後遺症 XD。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;本文以這三個服務做為某個 web app 的基礎架構為例，在安全性的方面做出規劃，在此我們希望能達成下列目標：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;安全性要有，這是最基本的。&lt;&#x2F;li&gt;
&lt;li&gt;除了 80、443 以外的埠不要暴露，或者限定只給我的電腦訪問。&lt;&#x2F;li&gt;
&lt;li&gt;Plesk 控制台要有網址入口，因為 IP 太難記，並且這個網址也限定只給我的電腦訪問。&lt;&#x2F;li&gt;
&lt;li&gt;主機的公共 IP 不要暴露。&lt;&#x2F;li&gt;
&lt;li&gt;防火牆盡量不要佔用到主機本身的資源，最大化 web app 本身能運用的運算資源以及節省主機費用。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;yong-lightsail-fang-huo-qiang-zuo-bu-guan-li&quot;&gt;用 Lightsail 防火牆做埠管理&lt;&#x2F;h2&gt;
&lt;p&gt;Lightsail 的防火牆雖然滿陽春的，不過也能滿足我們對埠管理的需求，基本上只要是與網址無關的單純的 IP:port 規則，都會在 Lightsail 這邊做設定，簡單有效。&lt;&#x2F;p&gt;
&lt;p&gt;我們的 web app 除了會對外開放的 80、443 以外，還有幾個埠做為內部使用，必須管制只讓我的電腦的 IP 訪問，完整的清單如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;80，HTTP 的標準埠，對外開放。&lt;&#x2F;li&gt;
&lt;li&gt;443，HTTPS 的標準埠，對外開放。&lt;&#x2F;li&gt;
&lt;li&gt;22，SSH 的埠，需限定連入 IP。&lt;&#x2F;li&gt;
&lt;li&gt;8443，Plesk web 控制台的入口，需限定連入 IP。&lt;&#x2F;li&gt;
&lt;li&gt;8447，Plesk Installer 的入口，變更 Plesk 元件或更新時會用到，需限定連入 IP。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;埠管理這一塊很簡單，用 Lightsail 內的防火牆功能即可設定以上所有的埠號，設定畫面如下：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;cloudflare-lightsail-plesk&#x2F;Lightsail.png&quot; alt=&quot;Lightsail 防火牆&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;yong-plesk-she-ding-plesk-kong-zhi-tai-de-ru-kou-wang-zhi&quot;&gt;用 Plesk 設定 Plesk 控制台的入口網址&lt;&#x2F;h2&gt;
&lt;p&gt;前面提到 Plesk 會用到 8443 和 8447 埠，在 Lightsail 那邊我們也已經對這兩個埠設定了只有我電腦的 IP 可以訪問它們，但目前的必須透過 IP:port 的方式進入，這對永遠記不住 IP 的本人來說會很困擾，所以最好讓它們有個網址方便往後進入，Plesk 也的確提供了指定網址的功能，見下圖的「自訂 Plesk URL」：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;cloudflare-lightsail-plesk&#x2F;Plesk.png&quot; alt=&quot;Plesk&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;猛擊會出現網址的設定畫面：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;cloudflare-lightsail-plesk&#x2F;Plesk-URL.png&quot; alt=&quot;自訂 Plesk URL&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;我們選第二個，並輸入要當作 Plesk 控制台入口的網址，上例為 plesk.o.com，當然在 DNS 上也要有對應的一筆設定讓 plesk.o.com 連到這台 Plesk 主機的 IP。&lt;&#x2F;p&gt;
&lt;p&gt;接著我們要讓 plesk.o.com 只接受我的電腦的訪問。&lt;&#x2F;p&gt;
&lt;p&gt;施做前先整理一下目前的防火牆設定與 Plesk 的進入點：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;https:&#x2F;&#x2F;plesk.o.com&#x2F;，這個網址目前是接受公眾訪問的，待會要讓它只能讓我的電腦訪問。&lt;&#x2F;li&gt;
&lt;li&gt;https:&#x2F;&#x2F;5.1.2.1:8443&#x2F;，這個入口受到 Lightsail 防火牆的管制，只接受我的電腦的訪問。&lt;&#x2F;li&gt;
&lt;li&gt;https:&#x2F;&#x2F;5.1.2.1&#x2F; ，這個 IP 的入口目前並未受到管制，接受公眾訪問，待會利用 Cloudflare 的 proxy 來隱蔽此 IP，減少 IP 暴露的風險。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;開始施工，進入 Cloudflare 的防火牆，建立一條規則：封鎖對 plesk.o.com 的訪問，如下圖：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;cloudflare-lightsail-plesk&#x2F;Cloudflare-%E9%98%B2%E7%81%AB%E7%89%86.png&quot; alt=&quot;Cloudflare 防火牆&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;封鎖後再開一個小門讓我電腦的 IP 可以進去，新增一個 IP 存取規則：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;cloudflare-lightsail-plesk&#x2F;Cloudflare-Firewall.png&quot; alt=&quot;Cloudflare 防火牆&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;如此一來，plesk.o.com 就只有白名單內的 IP 可以進入。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yong-cloudflare-de-proxy-he-fang-huo-qiang-zuo-ip-yin-bi-yu-wang-zhi-fang-wen-guan-li&quot;&gt;用 Cloudflare 的 proxy 和防火牆做 IP 隱蔽與網址訪問管理&lt;&#x2F;h2&gt;
&lt;p&gt;因為 Lightsail 的防火牆只能設定簡易的 IP:port 規則，如果是牽扯到與網址有關的訪問設定，就必須依賴 Cloudflare 的服務了。&lt;&#x2F;p&gt;
&lt;p&gt;Cloudflare 的全球 CDN 節點會幫我們網站上的內容做快取，讓訪客可以就近從 CDN 節點取得內容，這樣的服務除了有加速的好處外，還可以避免讓訪客直接連線到我們的主機，讓主機與 IP 獲得某種程度的隱蔽性，另外 Cloudflare 也幫我們做了分散流量的工作，讓我們的主機不會被暴衝的流量打爆，總之在自己的服務前面掛一層 Cloudflare 絕對是好處多多，絕對不要因為免費的節點不包括台灣就貿然放棄。&lt;&#x2F;p&gt;
&lt;p&gt;回到正題，要享用 Cloudflare CDN 加速與防護的好處，只要在 Cloudflare 的 DNS 管理頁面把那朵橘色的雲打開即可，如下圖：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;cloudflare-lightsail-plesk&#x2F;Cloudflare-DNS.png&quot; alt=&quot;Cloudflare DNS&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;橘色小雲表示 Cloudflare 的 proxy 節點，也就是前面提到的 CDN 節點，它們是同概念在不同視角下的名稱，Cloudflare 把這些廣怖在全球節點稱為 edge 節點，不論是 CDN 節點、proxy 節點或是 edge 節點，指的都是同樣的事物，即佈署在全球各個地理位置的伺服器。&lt;&#x2F;p&gt;
&lt;p&gt;開啟後等待個一分鐘讓新的 DNS 設定傳播完畢生效即可。&lt;&#x2F;p&gt;
&lt;p&gt;上圖中可以看到「mail」這筆設定是沒有開橘色小雲的，因為這個郵件代管服務會因為 Cloudflare 的 proxy 而異常。除了例子裡的 mail 外，在實際經驗下最好還是對每組 DNS 紀錄的小雲做一輪測試，確保在 Cloudflare Proxy 開啟時還是能正常工作，確保原服務的可用性，這是比較妥當的作法。&lt;&#x2F;p&gt;
&lt;p&gt;上圖的「www」這一組就是我們主要對外服務的網址，它對應的真實的主機 IP 是 5.1.2.1，但因為有開啟小雲，所以訪客訪問 www.o.com 時不會連到真正的 5.1.2.1，而是會連到 Cloudflare DNS 回應的某個離訪客地理位置最近的 proxy 節點，因此訪客無法從 DNS 查詢出真正的主機 IP 是 5.1.2.1，這樣的特性可以保護我們的主機 IP 暴露，並減少壞人直接對 IP 發動攻擊的可能性。（其實透過一些手段還是有機會知道真正的 IP，所以資安的防禦不能只依賴單一服務或單一層次，必須在架構的每一層都設想安全的防禦機制。）&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zong-jie&quot;&gt;總結&lt;&#x2F;h2&gt;
&lt;p&gt;透過以上一連串的設定，我們做到幾件事：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;只公開必要的服務。&lt;&#x2F;li&gt;
&lt;li&gt;內部用的服務入口都有限定 IP。&lt;&#x2F;li&gt;
&lt;li&gt;主機 IP 不暴露。&lt;&#x2F;li&gt;
&lt;li&gt;不用主機的資源跑防火牆。&lt;&#x2F;li&gt;
&lt;li&gt;Plesk 可以用好記的網址進入，而且也受到限定 IP 的管制。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;但還有一個小漏洞：https:&#x2F;&#x2F;5.1.2.1&#x2F; 這個 IP 的入口並未受到管制。其實可以利用 Plesk 本身的防火牆來設定可以進去的白名單，這點不在本文的宗旨內就不提了。&lt;&#x2F;p&gt;
&lt;p&gt;本文比較著重在基礎架構的安全設定，但其實&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;social-engineering&#x2F;&quot;&gt;最脆弱的環節是在用戶的安全意識上&lt;&#x2F;a&gt;，其次才是系統的安全設計。&lt;&#x2F;p&gt;
&lt;p&gt;另外，設定這麼多的感想是，採用 PaaS 或 Serverless 才是更快達到 time to market 的做法，把底層的配置都轉嫁給平台，我們只要專心做產品就好，也不用考慮那麼多層面的安全設定，當然前提是平台本身要夠給力。&lt;&#x2F;p&gt;
&lt;p&gt;分享一點紀錄與心得，希望對大家有幫助。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>WordPress 備份外掛 WPvivid Backup</title>
        <published>2020-08-06T00:00:00+00:00</published>
        <updated>2020-08-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2020/wpvivid-backup/"/>
        <id>https://editor.leonh.space/2020/wpvivid-backup/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2020/wpvivid-backup/">&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;wpvivid.com&#x2F;ref&#x2F;240&#x2F;&quot;&gt;WPvivd Backup&lt;&#x2F;a&gt; 是一款佛心的 WordPress 備份外掛，功能完整，許多其它家備份外掛付費的功能在 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;tw.wordpress.org&#x2F;plugins&#x2F;wpvivid-backuprestore&#x2F;ref&#x2F;240&#x2F;&quot;&gt;WPvivid Backup 免費版&lt;&#x2F;a&gt;都無料提供。&lt;&#x2F;p&gt;
&lt;p&gt;照例列一下特色：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;排程備份。&lt;&#x2F;li&gt;
&lt;li&gt;備份完會寄給我們備份的結果。&lt;&#x2F;li&gt;
&lt;li&gt;備份的打包檔可以放在主機上、下載自己管、上傳到各大空間。&lt;&#x2F;li&gt;
&lt;li&gt;可備份檔案、資料庫。&lt;&#x2F;li&gt;
&lt;li&gt;支援 AB 站備份，A 站和 B 站互相認證後，A 站的備份檔可以傳到 B 站幫 B 站還原，省時省力省心。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;付費的 WPvivid Backup Pro 版當然有更多的功能，不過本人沒用過，可以參考 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;wpvivid.com&#x2F;backup-plugin-pro&#x2F;ref&#x2F;240&#x2F;&quot;&gt;WPvivid Backup 的比較表&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;下面是一些 WPvivid Backup 的基礎操作。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shou-dong-bei-fen-yu-huan-yuan&quot;&gt;手動備份與還原&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;wpvivid-backup&#x2F;1.png&quot; alt=&quot;WPvivid Backup&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;安裝完進到 WPvivid Backup，以上方的頁籤作為主要功能的導航。&lt;&#x2F;p&gt;
&lt;p&gt;在 Backup &amp;amp; Restore 可以做最單純的手動備份，就是中間那顆藍色的 Backup Now，而按鈕上方左邊可以選擇要備份的項目；右邊可以選擇要備份到主機或其它空間，雲端空間需要先設定連線才能使用，目前都還是灰色的。&lt;&#x2F;p&gt;
&lt;p&gt;勇敢地按下 Backup Now 會出現進度條，跑完之後往下看會看到目前這個站有的備份檔：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;wpvivid-backup&#x2F;2.png&quot; alt=&quot;WPvivid Backup&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;這邊顯示備份檔的資訊，時間，儲存方式等，還可以對備份檔做簡單的管理，以及下載到自己的電腦、從備份檔還原等功能。&lt;&#x2F;p&gt;
&lt;p&gt;切換到 Upload 頁籤，可以把自己電腦的備份檔上傳到主機，如下圖：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;wpvivid-backup&#x2F;3.png&quot; alt=&quot;WPvivid Backup&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;上傳的備份檔也會出現在 Backups 頁籤內，如果要用備份檔還原的話再按 Restore 即可。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;pai-cheng&quot;&gt;排程&lt;&#x2F;h2&gt;
&lt;p&gt;回到畫面最上方，切換到 Schedule 頁籤，在這邊可以設定排程：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;wpvivid-backup&#x2F;4.png&quot; alt=&quot;WPvivid Backup&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;這頁的設定相當直覺，啟用排程，設定週期、備份項目、儲存位置，存檔，WPvivid Backup 就會按計劃幫我們自動做備份。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;she-ding-yuan-duan-kong-jian&quot;&gt;設定遠端空間&lt;&#x2F;h2&gt;
&lt;p&gt;來到 Remote Storage 頁籤，這裡可以設定備份檔要儲存的遠端空間：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;wpvivid-backup&#x2F;5.png&quot; alt=&quot;WPvivid Backup&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;常用的空間都有支援，授權認證的方式也是走常見的認證流程。依我自己的習慣都會把備份檔放到遠端空間，避免主機掛掉的憾事發生。&lt;&#x2F;p&gt;
&lt;p&gt;在設定完遠端空間後，前面備份那邊原本灰色的圖示應該就會變成可選的彩色了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ab-zhan-hu-chuan-bei-fen&quot;&gt;AB 站互傳備份&lt;&#x2F;h2&gt;
&lt;p&gt;雖然用手動的上傳下載也可以做到 AB 站互傳備份的目的，不過就是多了人工的力氣，WPvivid Backup 內建 AB 站互傳的功能，可以幫我們節省喝一杯水的時間。&lt;&#x2F;p&gt;
&lt;p&gt;AB 站之間要互傳，大前提當然是兩個站的網路要能溝通，再透過 WPvivid Backup 的密鑰認證機制，建立認證後，才可以傳檔。&lt;&#x2F;p&gt;
&lt;p&gt;假設是 A 站要備份打包傳給 B 站，則 A 站要有 B 站的密鑰。&lt;&#x2F;p&gt;
&lt;p&gt;在 B 站產生密鑰，B 站也要裝 WPvivid Backup，到 B 站 WPvivd Backup 的 Key 頁籤：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;wpvivid-backup&#x2F;6.png&quot; alt=&quot;WPvivid Backup&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;先設定密鑰的有效時間，再按一下 Generate 就會產生一大串亂碼密鑰，回到 A 站的 Auto-migration 頁籤：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;wpvivid-backup&#x2F;7.png&quot; alt=&quot;WPvivid Backup&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;把密鑰貼上，設定一下要打包的項目，按 Clone then Transfer 就會開始幫我們做備份、打包、傳輸的動作到 B 站了。&lt;&#x2F;p&gt;
&lt;p&gt;傳完後回到 B 站的 Backup &amp;amp; Restore 頁籤，下方的 Backup 區段還不會出現剛剛 A 站傳來的打包檔，按一下 Scan uploaded backup or received backup 才會出現。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;wpvivid-backup&#x2F;2.png&quot; alt=&quot;WPvivid Backup&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;收到的備份檔，就如同其它的備份檔一樣還原即可。&lt;&#x2F;p&gt;
&lt;p&gt;AB 站互傳模式的還原之後，最好還是去每個設定與頁面看一下，是不是有網址或字串要改的，WPvivid Backup 還原後會自動使用 B 站的網址，但有些第三方外掛內的設定可能需要手動修正成 B 站的設定。頁面內容也是，有可能某些連結當初在建立是手動鍵入 A 站網址，這些細微的部分都還是要一一檢查。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-yu&quot;&gt;結語&lt;&#x2F;h2&gt;
&lt;p&gt;WPvivid Backup 是本人嘗試過幾個不同的備份外掛後覺得最好用的，推薦給大家，另外本文的連結也都很故意的埋了 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;wpvivid.com&#x2F;affiliate&#x2F;ref&#x2F;240&#x2F;&quot;&gt;WPvivid Backup 聯盟行銷&lt;&#x2F;a&gt;的連結，你各位一不小心點到還順手結了帳就會有部份回扣流入本人口袋，不可不慎。&lt;&#x2F;p&gt;
&lt;p&gt;最後補充一點關於 AB 站備份還原的注意事項，在 AB 的備份還原情境下，A 站與 B 站有許多不同的地方，這些地方在還原至 B 站後都還是要一一調整以符合 B 站的環境，要調整的地方大多是與第三方服務有關的，包括 GA 追總代碼、Pixel 代碼、GTM 代碼、以及其它類似外掛的代碼或 API 認證等等，都需要在還原後逐一調整，這部份需要特別留意一下。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>WordPress 專用的發信服務 MailPoet</title>
        <published>2020-08-04T00:00:00+00:00</published>
        <updated>2020-08-04T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2020/mailpoet/"/>
        <id>https://editor.leonh.space/2020/mailpoet/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2020/mailpoet/">&lt;p&gt;對一個 WordPress 網站來說，寄信可能不會是重要的功能，但如果是加掛了 WooCommerce 的站台，寄信就顯得相當重要，現行對消費者購物前後的系統通知信都還是以 email 做為溝通管道，如果還有發行銷信（電子報）的需求，那 email 更是應該被著重考慮的要點之一。&lt;&#x2F;p&gt;
&lt;p&gt;在介紹 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.mailpoet.com&quot;&gt;MailPoet&lt;&#x2F;a&gt; 前，先介紹一下 WordPress 原生的發信機制。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;wordpress-yuan-shi-de-fa-xin-ji-zhi&quot;&gt;WordPress 原始的發信機制&lt;&#x2F;h2&gt;
&lt;p&gt;在沒有任何外掛輔助下，WordPress 會呼叫底層 PHP 的發信機制，但這個發信機制相當原始，誕生於垃圾郵件尚未氾濫的時代，不支援後來發展出的 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.m.wikipedia.org&#x2F;zh-tw&#x2F;%E5%8F%91%E4%BB%B6%E4%BA%BA%E7%AD%96%E7%95%A5%E6%A1%86%E6%9E%B6&quot;&gt;SPF&lt;&#x2F;a&gt; 認證機制，所以很容易被別的郵件主機視為垃圾郵件，另一方面對於電子報相關的需求來說，也缺少了對開信率、點閱率追蹤的能力，這樣原始陽春的發信機制，對現在的發信需求來說完全是不可用的，而 &lt;del&gt;萬惡&lt;&#x2F;del&gt; &lt;ins&gt;好棒棒&lt;&#x2F;ins&gt;的 WordPress 的開發商 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;automattic.com&#x2F;&quot;&gt;Automattic&lt;&#x2F;a&gt; 不知道是仗著社群龐大還是什麼原因，這十幾年來竟然完全沒想過把它改善，完全擺爛放生，也真的是幸好 WordPress 的社群龐大，所以我們還可以依賴 MailPoet 這類的服務來做為發信之用。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;岔題講一下我心中理想的生態系治理模式應該是像蘋果，提供開發者從上架到收款一條龍的服務，並且嚴審上架 app，也要求開發者必須跟上新版的 SDK 或 API，而不是像 Automattic 這樣擺爛放生不管，縱容有安全風險或粗製濫造的外掛隨意上架。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xian-dai-hua-de-fa-xin-ji-zhi&quot;&gt;現代化的發信機制&lt;&#x2F;h2&gt;
&lt;p&gt;回頭講發信，市場上發送系統信或行銷信的服務有很多，例如 MailChimp、SendGrid、台灣的電子豹等，這些現代化的發信服務在系統面的共同特色都是採取 API 串接發信，而非原始的 SMTP 發信協議，透過 API 對發信機制的封裝，讓我們可以對發信這件事情做更多可程式化的操作，做出更多附加服務，這是原始 SMTP 難以做到的。&lt;&#x2F;p&gt;
&lt;p&gt;前面提到的發信服務雖然都有 API，不過再回頭考慮與 WordPress 的整合度，會發現並非每間廠商都有為 WordPress 提供原生的外掛，MailPoet 是少數幾間有為 WordPress 提供原生外掛，以及也整合了 WooCommerce 系統信的廠商之一。下面開始紀錄 MailPoet 的一些特色和操作事項。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;mailpoet&quot;&gt;MailPoet&lt;&#x2F;h2&gt;
&lt;p&gt;MailPoet 是個與 WordPress &#x2F; WooCommerce 深度整合的發信服務，包括系統信的發送以及行銷信（電子報）的發送，不論是系統信或行銷信，都可以透過 MailPoet 的 WordPress 外掛在 WordPress 內編輯信件外觀與內容，也可以在 MailPoet 外掛內追蹤信件的寄達率、開信率、點擊率等數據，MailPoet 應該是市面上與 WordPress 整合的最徹底的發信服務了，也因此，除了開帳號、認證網域、繳費等底層功能是在 MailPoet 網站操作外，幾乎所有的主要發信相關的功能都是直接在 MailPoet 外掛內操作的。&lt;&#x2F;p&gt;
&lt;p&gt;下面是一些操作紀錄，但不會包括太基礎的開立帳號或外掛安裝等操作。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;she-ding-fa-xin-xin-xiang&quot;&gt;設定發信信箱&lt;&#x2F;h3&gt;
&lt;p&gt;像 MailPoet 這種發信服務通常沒有收信功能，頂多幫你把來信轉寄到你的另一個信箱，並且利用發信服務也通常都要設定寄件信箱，讓你的客戶看到信時能正確的從「寄件人」認出你的信箱。&lt;&#x2F;p&gt;
&lt;p&gt;在 MailPoet 設定寄件信箱是需要認證的，登入後在「My authorized Emails」頁填入要做為「寄件人」的信箱：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;mailpoet&#x2F;My-authorized-Emails.png&quot; alt=&quot;My authorized Emails&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;接著至該信箱收取 MailPoet 的認證信，點擊認證連結確認你真的擁有該信箱即可，寄件人信箱認證後，在 MailPoet 的 WordPress 外掛內的「Settings」頁再把該信箱填入「 Default sender」才算設定完整。&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;mailpoet&#x2F;MailPoet-Settings.png&quot; alt=&quot;MailPoet Settings&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;figure&gt;
&lt;h3 id=&quot;she-ding-wang-yu&quot;&gt;設定網域&lt;&#x2F;h3&gt;
&lt;p&gt;前面提到，在垃圾郵件氾濫後，就有人提出 SPF 機制來對發信人作網域認證，來自 SPF 認證的網域的信件較不會被判斷成垃圾信而直接被丟進垃圾信箱，我們常用的個人信箱，像是 Gmail 等，也都有做 SPF 認證，但如果是要在自己的網域透過 MailPoet 做發信，那 MailPoet 也可以協助我們對自有的網域做 SPF 認證，具體的操作其實很簡單，把 MailPoet 提供的認證碼填入你的 DNS 紀錄即可，本人慣用的 DNS 代管是 Cloudflare，其他 DNS 代管服務就自行舉一反三吧！&lt;&#x2F;p&gt;
&lt;p&gt;先到登入 MailPoet 網站，到「My Sender Domains」填入自己的網域：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;mailpoet&#x2F;My-Sender-Domains.png&quot; alt=&quot;My Sender Domains&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;接著 MailPoet 就會提示要建立的 DNS 紀錄，把那些 DNS 紀錄鍵入 DNS 即可，下面是 Cloudflare 的例子：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;mailpoet&#x2F;Cloudflare-DNS.png&quot; alt=&quot;Cloudflare DNS&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;可以看到我們在這邊建了兩組 CNAME 與一組 TXT 的紀錄，在 Cloudflare 要注意的是不能啟用 Cloudflare proxy，否則會認證失敗。&lt;&#x2F;p&gt;
&lt;p&gt;DNS 紀錄建好後，回到 MailPoet 驗證網域，如果 DNS 紀錄有被確實存在，應該就會驗證成功，具體的效果就是從 MailPoet 寄出的信會比較不容易被判斷成垃圾信而被無情的丟入垃圾信箱。&lt;&#x2F;p&gt;
&lt;p&gt;提醒一點，設定網域這邊，也是要搭配寄件人信箱才會有用，以上面的例子來說， 我對 o.com.tw 認證了 SPF，那麼寄件信箱也必須是 xxx@o.com.tw，這整組 SPF 機制才會達到「不被當成垃圾信」的效果，如果辛苦認證了網域，寄件信箱卻還是設成 e@gmail.com 的話，那那個網域認證就是白做的，因為收信方的郵件服務根本就不會去查 o.com.tw 的 SPF 認證狀態，因為信件是來自 gmail.com，收信方的郵件服務只會去查 gmail.com 的 SPF 認證狀態。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>WordPress Child Theme</title>
        <published>2020-08-03T00:00:00+00:00</published>
        <updated>2020-08-03T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2020/wp-child-theme/"/>
        <id>https://editor.leonh.space/2020/wp-child-theme/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2020/wp-child-theme/">&lt;h2 id=&quot;theme&quot;&gt;Theme&lt;&#x2F;h2&gt;
&lt;p&gt;Theme 的檔案由許多 PHP 模板構成，而這些模板之間是有階層關係存在的，階層關係可以參考 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;wphierarchy.com&#x2F;&quot;&gt;The WordPress Template Hierarchy&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;WordPress 會根據當前訪客的訪問頁面嘗試以該頁面歸屬之模板檔內容組版出完整的網頁回應給訪客，如果專屬的模板不存在，則會退一步抓取通用模板組版回應。&lt;&#x2F;p&gt;
&lt;p&gt;舉例來說，訪客訪問一篇網誌，對照階層表，WordPress 會首先找 single-post.php 作為模板回應，但假設 single-post.php 不存在，則會找 single.php，如果還是沒有，繼續找 singular.php，再沒有就用 index.php，當然往往越通用的模板越不可能是恰當的回應內容，上述只是舉例用於解釋 WordPress 的模板階層與回退機制（fall-back）而已。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;child-theme&quot;&gt;Child theme&lt;&#x2F;h2&gt;
&lt;p&gt;一般的 theme 都會常態性的更新，因此如果要客製，不建議直接魔改 theme 的檔案，以免 theme 更新把我們的修改覆蓋掉，在 WordPress 有設計 child theme 的機制，child theme 可以繼承 parent theme 的模板與函式，並且可以自由覆蓋 parent theme 任意的模板或函式。&lt;&#x2F;p&gt;
&lt;p&gt;上面提到 WordPress 對模板檔的階層與回退機制，如果再把 child theme 的概念加入，依然是同樣的原則，會先去 child theme 找與當前請求頁面較匹配的模板檔，例如網誌會去 child theme 內的 single-post.php，若 child theme 沒有則會到 parent theme 找 single-post.php，再沒有才會去找 child theme 的 single.php，再沒有去找 parent theme 的 single.php，以此類推。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jian-li-child-theme-mu-lu-yu-dang-an&quot;&gt;建立 child theme 目錄與檔案&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;在 wp-content&#x2F;theme&#x2F; 新建一個 storefront-child 資料夾，資料夾名字可以自取，可以識別就好，這個 storefront-child 就是我們的專案資料夾，也是 Git 專案資料夾。&lt;&#x2F;li&gt;
&lt;li&gt;在專案資料夾內建立 functions.php 和 style.css。&lt;&#x2F;li&gt;
&lt;li&gt;截至目前 wp-content&#x2F; 目錄結構會長這樣：&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;wp-content&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├─ languages&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├─ mu-plugins&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├─ plugins&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├─ themes&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│  ├─ storefront&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│  └─ storefront-child&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│     ├─ style.css&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;│     └─ functions.php&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;├─ upgrade&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;└─ uploads&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;zai-yang-shi-biao-xie-ru-child-theme-ji-ben-zi-liao&quot;&gt;在樣式表寫入 child theme 基本資料&lt;&#x2F;h2&gt;
&lt;p&gt;截至目前檔案都還沒有內容，在 style.css 填入一些基本資料：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;css&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;&#x2F;*&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;Theme Name:   Storefront Child&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;Theme URI:    http:&#x2F;&#x2F;example.com&#x2F;wordpress-theme-child&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;Description:  WordPress Child Theme&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;Author:       Leon&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;Author URI:   http:&#x2F;&#x2F;example.com&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;Template:     storefront&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;Version:      1.0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;License:      GNU General Public License v2 or later&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;License URI:  http:&#x2F;&#x2F;www.gnu.org&#x2F;licenses&#x2F;gpl-2.0.html&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;Tags:         WooCommerce&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;Text Domain:  storefrontchild&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;*&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ul&gt;
&lt;li&gt;Theme Name：不可與其它既有的佈景主題同名，必填。&lt;&#x2F;li&gt;
&lt;li&gt;Theme URI：非必填。&lt;&#x2F;li&gt;
&lt;li&gt;Description：非必填。&lt;&#x2F;li&gt;
&lt;li&gt;Author：非必填。&lt;&#x2F;li&gt;
&lt;li&gt;Author URI：非必填。&lt;&#x2F;li&gt;
&lt;li&gt;Template：parent theme 的目錄名。&lt;&#x2F;li&gt;
&lt;li&gt;Version：非必填。&lt;&#x2F;li&gt;
&lt;li&gt;License：非必填。&lt;&#x2F;li&gt;
&lt;li&gt;License URI：非必填。&lt;&#x2F;li&gt;
&lt;li&gt;Tags：非必填。&lt;&#x2F;li&gt;
&lt;li&gt;Text Domain：此主題在程式碼內的代稱，某些地方會用到，非必填。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;這些基本資料對 CSS 來說是註解，但對 WP 來說是作為辨識 child theme 資訊的欄位。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zai-functions-php-zai-ru-parent-theme-de-yang-shi-biao&quot;&gt;在 functions.php 載入 parent theme 的樣式表&lt;&#x2F;h2&gt;
&lt;p&gt;目前的 child theme 不管是樣式或頁面都是空白，雖然 WordPress 對 child theme 的模板有回退機制，但對 CSS 檔案而言這樣的機制並不生效，所以要透過 functions.php 內的函數把 parent theme 的樣式表載入。&lt;&#x2F;p&gt;
&lt;p&gt;在 child theme 的資料夾內編輯 functions.php：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;php&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;lt;?&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;php&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;&#x2F;* enqueue script for parent theme stylesheeet *&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;function&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; childtheme_parent_styles&lt;&#x2F;span&gt;&lt;span&gt;() {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    &#x2F;&#x2F; enqueue style&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;    wp_enqueue_style&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;parent&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; get_template_directory_uri&lt;&#x2F;span&gt;&lt;span&gt;()&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;&#x2F;style.css&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;add_action&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;wp_enqueue_scripts&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;childtheme_parent_styles&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;截至目前為止，雖然還是沒有自己的樣式與模板，但因為 WordPress 的模板回退機制，以及我們也載入了 parent theme 的 CSS，所以在 WordPress Admin 勇敢的把佈景主題切換成 child theme 後，一切如常，頁面、樣式都沒跑掉。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ke-zhi-mo-ban&quot;&gt;客製模板&lt;&#x2F;h2&gt;
&lt;p&gt;從 parent theme 複製想改的模板檔到 child theme 再做修改，確保與為異動的部分仍然與 parent theme 保持一致以確保相容性。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ke-zhi-yang-shi&quot;&gt;客製樣式&lt;&#x2F;h2&gt;
&lt;p&gt;直接在 child theme 的樣式表做編修。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ke-zhi-han-shi&quot;&gt;客製函式&lt;&#x2F;h2&gt;
&lt;p&gt;客製函式前，必須先確定要做的功能是應該在 theme 內實現還是應該在 plugin 內實現，一般會以功能的目的來判斷，如果是與外觀內容無關的部分，會以 plugin 實現；反之若是與 theme 外觀或內容相關的部分，會以 theme 實現。&lt;&#x2F;p&gt;
&lt;p&gt;WordPress 內預埋了大量的 hook 點，這些 hook 點會在頁面組版產生的時候依序執行。我們客製的函式會透過把函式註冊到 hook 點的方式讓函式在特定的位置生效。&lt;&#x2F;p&gt;
&lt;p&gt;WordPress hook 又可分為 action hook 與 filter hook，兩者差別可參考別人寫的優文〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;audilu.com&#x2F;2011&#x2F;10&#x2F;10&#x2F;wordpress-hook&#x2F;&quot;&gt;WordPress的Hook機制與原理&lt;&#x2F;a&gt;〉。&lt;&#x2F;p&gt;
&lt;p&gt;客製函式的一般規則：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;如果要添加的函式與 parent theme 無關，可自由添加。&lt;&#x2F;li&gt;
&lt;li&gt;如果是要變更／追加於 parent theme 內的函式，則可以有三種途徑：
&lt;ul&gt;
&lt;li&gt;對 pluggable function，可直接以同樣的函式名建立於 child theme 的 functions.php 內，可完全取代 parent theme 的同名函式。&lt;&#x2F;li&gt;
&lt;li&gt;在 child theme 內定義一組函式，把 parent theme 的目標函式解除 hook。&lt;&#x2F;li&gt;
&lt;li&gt;附加到 parent theme 的既有函式後執行。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;qu-dai-pluggable-function&quot;&gt;取代 pluggable function&lt;&#x2F;h2&gt;
&lt;p&gt;Pluggable function 是一種被判斷式包住的函式，判斷式用於確認是否已經有同名函式存在，依照 WordPress 的規則，child theme 的 functions.php 會先被載入，因此 parent theme 的 pluggable function 的檢查式會發現已經有同名函式存在，而採用 child theme 的同名函式並忽略 parent theme 的同名函式。&lt;&#x2F;p&gt;
&lt;p&gt;在 storefront&#x2F;inc&#x2F;storefront-template-functions.php 有個 &lt;code&gt;storefront_credit()&lt;&#x2F;code&gt; 就是 pluggable function，它結構長這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;php&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;if&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; !&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; function_exists&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;storefront_credit&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt; ) ) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;	function&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; storefront_credit&lt;&#x2F;span&gt;&lt;span&gt;() {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;		&#x2F;&#x2F; contents for function here&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;	}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以看到在 &lt;code&gt;storefront_credit()&lt;&#x2F;code&gt; 外面還包了一層判斷式用於確認是否已經有同名的 &lt;code&gt;storefront_credit&lt;&#x2F;code&gt; 函式存在，沒有的話才會執行下面的 &lt;code&gt;storefront_credit()&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-chu-parent-theme-de-hook&quot;&gt;解除 parent theme 的 hook&lt;&#x2F;h2&gt;
&lt;p&gt;假設在 parent theme 有一函式：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;php&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;function&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; parent_function&lt;&#x2F;span&gt;&lt;span&gt;() {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; &#x2F;&#x2F;contents of function here&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;add_action&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;init&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;parent_function&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 20&lt;&#x2F;span&gt;&lt;span&gt; );&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以在 child theme 定義解除它的函式：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;php&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;function&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; remove_parent_function&lt;&#x2F;span&gt;&lt;span&gt;() {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; remove_action&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;init&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;parent_function&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 20&lt;&#x2F;span&gt;&lt;span&gt; );&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;add_action&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;wp_head&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;remove_parent_function&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt; );&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上面是以 action hook 為例，如果是 filter hook 也是以此類推，把 &lt;code&gt;remove_action()&lt;&#x2F;code&gt; 改成 &lt;code&gt;remove_filter()&lt;&#x2F;code&gt; 即可。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;fu-jia-dao-ji-you-de-han-shi-hou&quot;&gt;附加到既有的函式後&lt;&#x2F;h2&gt;
&lt;p&gt;最後一種模式，不異動 parent theme 既有函式，而是隨後執行。假設在 parent theme 有組函式用於產生 footer 的內容：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;php&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;function&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; parent_footer_content&lt;&#x2F;span&gt;&lt;span&gt;() {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; &#x2F;&#x2F; content of function here&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;add_action&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;parent_footer&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;parent_footer_content&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt; );&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;我們可以在 child theme 定義另一組函式並 hook 到同一個位置，以及確保 child theme 的 priority 大於上面的 parent theme 函式，上面的 parent theme 函式沒有指定 priority，所以會被 WordPress 認定為 10。&lt;&#x2F;p&gt;
&lt;p&gt;Child theme 函式內容如下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;php&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;function&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; child_footer_extra_content&lt;&#x2F;span&gt;&lt;span&gt;() {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; &#x2F;&#x2F; contents of function here&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;add_action&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;parent_footer&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;child_footer_extra_content&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 20&lt;&#x2F;span&gt;&lt;span&gt; );&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;can-kao-wen-jian&quot;&gt;參考文件&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;kinsta.com&#x2F;blog&#x2F;wordpress-child-theme&#x2F;&quot;&gt;How to Create a Child Theme in WordPress (Extended Guide)&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;blog.templatetoaster.com&#x2F;how-to-create-child-theme-wordpress&#x2F;&quot;&gt;How to Create Child Theme in WordPress – Tutorial for Beginners&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.hostinger.com&#x2F;tutorials&#x2F;how-to-create-wordpress-child-theme&quot;&gt;How to Create and Customize a WordPress Child Theme&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Local 本機 WordPress 開發環境建置工具</title>
        <published>2020-08-01T00:00:00+00:00</published>
        <updated>2020-08-01T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2020/local/"/>
        <id>https://editor.leonh.space/2020/local/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2020/local/">&lt;p&gt;最近又碰到那萬惡的 WordPress，重新調查了 WordPress 在本機端建置的方案，發現了一個新東西覺得還不錯用，在這邊分享。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ben-ji-huan-jing&quot;&gt;本機環境&lt;&#x2F;h2&gt;
&lt;p&gt;如同大多數的資訊專案一樣，在為 WordPress 製作新站台也往往是先在本機端建置一套開發環境，在比較穩定後再部署到生產機台中，可是萬惡的 WordPress 並沒有內建原生的開發環境，所以以往都是採用一些第三方包好的懶人包／架站機之類來使用，可是使用體驗並不好，包括必須要配置一些參數等，以及它們有些還在用老舊的 PHP 或 Apache 或 MySQL，這裡介紹的 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;localwp.com&#x2F;&quot;&gt;Local&lt;&#x2F;a&gt; 解決了一部分前面提到的的問題。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;local&quot;&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;localwp.com&#x2F;&quot;&gt;Local&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;Local 也算是本機架 WordPress 的懶人包之一，它有一些值得一提的特色：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;跨平台 Linux、macOS、Windows。&lt;&#x2F;li&gt;
&lt;li&gt;管理工具簡單好上手。&lt;&#x2F;li&gt;
&lt;li&gt;支援在本機建立多個獨立站台。&lt;&#x2F;li&gt;
&lt;li&gt;每個站台的底層套件 PHP、MySQL 可自由更換版本。&lt;&#x2F;li&gt;
&lt;li&gt;整合 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;mailhog&#x2F;MailHog&quot;&gt;MailHog&lt;&#x2F;a&gt; 用於確認信件寄發狀況。&lt;&#x2F;li&gt;
&lt;li&gt;預設支援 HTTPS。&lt;&#x2F;li&gt;
&lt;li&gt;整合 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;wp-cli.org&#x2F;&quot;&gt;WP-CLI&lt;&#x2F;a&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;整合 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;ngrok.com&#x2F;&quot;&gt;Ngrok&lt;&#x2F;a&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;整合 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.adminer.org&#x2F;&quot;&gt;Adminer&lt;&#x2F;a&gt; 作為 MySQL 管理界面。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;local-de-an-zhuang-yu-jian-li-zhan-tai&quot;&gt;Local 的安裝與建立站台&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;localwp.com&#x2F;&quot;&gt;Local&lt;&#x2F;a&gt; 的安裝與使用非常簡單，只要去 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;localwp.com&#x2F;&quot;&gt;Local 的網站&lt;&#x2F;a&gt;下載並安裝即可，並且 Local 同時提供了 Linux、macOS、Windows 的安裝包，以及一致的管理介面。&lt;&#x2F;p&gt;
&lt;p&gt;安裝後跑起來應該會看到這個畫面：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;local&#x2F;Local.png&quot; alt=&quot;Local&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;左邊的主選單，除了第二個「Local Sites」外，其它都是些付費相關的加值服務，我輩小資男女們應該用不到。&lt;&#x2F;p&gt;
&lt;p&gt;Local Sites 這頁，按 CREATE A NEW SITE 應該會看到這幾頁：&lt;&#x2F;p&gt;
&lt;h3 id=&quot;setup-site&quot;&gt;Setup Site&lt;&#x2F;h3&gt;
&lt;p&gt;第一頁填站名。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;local&#x2F;Setup-Site.png&quot; alt=&quot;Setup Site&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;先輸入站名，再把 ADVANCED OPTIONS 展開後會看到更多欄位：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Local site domain：本地的網址。&lt;&#x2F;li&gt;
&lt;li&gt;Local site path：站台專案在電腦內的資料夾。&lt;&#x2F;li&gt;
&lt;li&gt;Create site from Blueprint?：付費服務可略過。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h3 id=&quot;setup-environment&quot;&gt;Setup Environment&lt;&#x2F;h3&gt;
&lt;p&gt;第二頁決定底層組件，它預帶的 PHP 7.3.5、Nginx、MySQL 8.0.16 都是目前的主流選擇，可以直接使用。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;setup-wordpress&quot;&gt;Setup WordPress&lt;&#x2F;h3&gt;
&lt;p&gt;第三頁設定 WordPress 帳密。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;local&#x2F;Setup-WordPress.png&quot; alt=&quot;Setup WordPress&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;那個 email 不用去改它，Local 有整合 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;mailhog&#x2F;MailHog&quot;&gt;MailHog&lt;&#x2F;a&gt; 來確認信件的寄發狀況。&lt;&#x2F;li&gt;
&lt;li&gt;那個 multisite 的欄位一般不會使用。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;local-guan-li-gong-ju&quot;&gt;Local 管理工具&lt;&#x2F;h2&gt;
&lt;p&gt;依照上面的步驟建立站台後，會回到 Local Sites 的頁面：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;local&#x2F;Local-Sites.png&quot; alt=&quot;Local Sites&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;頁面內大部分的內容應該都很好懂，重點提示一下幾個可能較陌生的功能：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;最下方的 Live Link 啟動後會透過 ngrok 的服務產生一組 xxxxxxxxx.ngrok.io 的網址，這組網址可以給外部的人訪問，即使這台本機電腦可能是在內網 NAT 後方也無妨，但每次開關 Live Link 的網址都會變動，所以真的只適合拿來給同事或客戶預覽，無法作為正式網址。詳細的 ngrok 介紹可以參考另外一篇〈&lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2021&#x2F;ngrok&#x2F;&quot;&gt;ngrok 讓本機發佈出可被訪問的網址&lt;&#x2F;a&gt;〉。&lt;&#x2F;li&gt;
&lt;li&gt;Local 又自動幫我們建立 HTTPS 所需要的 TLS 憑證，不過是自簽的，需要對憑證做信任許可，在 SSL 頁面的 TRUST 按下去，Local 會把憑證加入系統的信任憑證庫內。&lt;&#x2F;li&gt;
&lt;li&gt;在 UTILITIES 內有可以進到 MailHog 的連結，MailHog 是 SMTP 發信的攔截器，所有 WordPress 寄出的信件都會被放進 MailHog 內，方便我們做格式或內容的確認。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;local-ke-yi-geng-hao-de-di-fang&quot;&gt;Local 可以更好的地方&lt;&#x2F;h2&gt;
&lt;p&gt;Local 已經是本人遇過最佳的 WordPress 懶人包，但還是有些缺憾：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Web server 只能選 Nginx 或 Apache，還無法用更新一代的 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;caddyserver.com&#x2F;&quot;&gt;Caddy&lt;&#x2F;a&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;Local 較適合單人單線開發，因為沒有同步或版控的機制，較不適合多人多線開發，不過這其實是萬惡的 WordPress 的原罪。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>裝 WP-CLI</title>
        <published>2020-07-30T00:00:00+00:00</published>
        <updated>2020-07-30T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2020/wp-cli/"/>
        <id>https://editor.leonh.space/2020/wp-cli/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2020/wp-cli/">&lt;p&gt;在裝 WordPress 之前，先裝 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;wp-cli.org&#x2F;&quot;&gt;WP-CLI&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;參照 WP-CLI 〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;make.wordpress.org&#x2F;cli&#x2F;handbook&#x2F;guides&#x2F;installing&#x2F;#recommended-installation&quot;&gt;Installing&lt;&#x2F;a&gt;〉文件。&lt;&#x2F;p&gt;
&lt;p&gt;抓下來：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; curl&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -O&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; https:&#x2F;&#x2F;raw.githubusercontent.com&#x2F;wp-cli&#x2F;builds&#x2F;gh-pages&#x2F;phar&#x2F;wp-cli.phar&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;確認一下運作正常：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; php wp-cli.phar&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --info&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;加上可執行屬性：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; chmod +x wp-cli.phar&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;移放到 &#x2F;usr&#x2F;local&#x2F;bin&#x2F;wp：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo mv wp-cli.phar &#x2F;usr&#x2F;local&#x2F;bin&#x2F;wp&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;再次確認運作正常：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; wp&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --info&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;很簡單。&lt;&#x2F;p&gt;
&lt;p&gt;我是用 CentOS，不過以上步驟應該都是通用的。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>在 WordPress.com 的文章高亮源碼</title>
        <published>2020-07-28T00:00:00+00:00</published>
        <updated>2020-07-28T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2020/wp/"/>
        <id>https://editor.leonh.space/2020/wp/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2020/wp/">&lt;p&gt;如果你想在網誌的文章內寫些源碼，但又不滿足於那些「專注於寫源碼」網誌平台，那麼其實 WordPress.com 就是最佳的選擇。&lt;&#x2F;p&gt;
&lt;p&gt;下面參照 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;wordpress.com&#x2F;support&#x2F;wordpress-editor&#x2F;blocks&#x2F;syntax-highlighter-code-block&#x2F;2&#x2F;&quot;&gt;WordPress.com 的支援文件&lt;&#x2F;a&gt;說明如何高亮源碼，主要是利用 shortcode 來達成，只要把想寫的源碼區塊的前後行以&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[code lanugage=&amp;quot;css]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;your code here&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[&#x2F;code]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;包起來即可。&lt;&#x2F;p&gt;
&lt;p&gt;裡面的 &lt;code&gt;language=&quot;css&quot;&lt;&#x2F;code&gt; 可依需求更換成需要的語言，語言列表如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;actionscript&lt;&#x2F;li&gt;
&lt;li&gt;bash&lt;&#x2F;li&gt;
&lt;li&gt;clojure&lt;&#x2F;li&gt;
&lt;li&gt;coldfusion&lt;&#x2F;li&gt;
&lt;li&gt;cpp&lt;&#x2F;li&gt;
&lt;li&gt;csharp&lt;&#x2F;li&gt;
&lt;li&gt;css&lt;&#x2F;li&gt;
&lt;li&gt;delphi&lt;&#x2F;li&gt;
&lt;li&gt;erlang&lt;&#x2F;li&gt;
&lt;li&gt;fsharp&lt;&#x2F;li&gt;
&lt;li&gt;diff&lt;&#x2F;li&gt;
&lt;li&gt;groovy&lt;&#x2F;li&gt;
&lt;li&gt;html&lt;&#x2F;li&gt;
&lt;li&gt;javascript&lt;&#x2F;li&gt;
&lt;li&gt;java&lt;&#x2F;li&gt;
&lt;li&gt;javafx&lt;&#x2F;li&gt;
&lt;li&gt;matlab&lt;&#x2F;li&gt;
&lt;li&gt;objc&lt;&#x2F;li&gt;
&lt;li&gt;perl&lt;&#x2F;li&gt;
&lt;li&gt;php&lt;&#x2F;li&gt;
&lt;li&gt;text&lt;&#x2F;li&gt;
&lt;li&gt;powershell&lt;&#x2F;li&gt;
&lt;li&gt;python&lt;&#x2F;li&gt;
&lt;li&gt;r&lt;&#x2F;li&gt;
&lt;li&gt;ruby&lt;&#x2F;li&gt;
&lt;li&gt;scala&lt;&#x2F;li&gt;
&lt;li&gt;sql&lt;&#x2F;li&gt;
&lt;li&gt;vb&lt;&#x2F;li&gt;
&lt;li&gt;xml&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;還有一些細微的選項可調，請自行閱讀原文的支援文件吧！&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>不想再當雙槍俠？用 LINE WORKS 讓你的帳號公私分明</title>
        <published>2020-06-27T00:00:00+00:00</published>
        <updated>2020-06-27T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2020/line-works/"/>
        <id>https://editor.leonh.space/2020/line-works/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2020/line-works/">&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;line-works&#x2F;promotion-image01.png&quot; alt=&quot;LINE WORKS&quot; &#x2F;&gt;
上圖來自 LINE WORKS 網站，版權歸 LINE WORKS 所有。&lt;&#x2F;p&gt;
&lt;p&gt;許多業務性質的行動電子商務人士往往需要隨身攜帶兩支手機以便隔離公用與私人的 LINE 帳號，或是比較進階一點的用戶會利用 Android 的雙開 app 讓一支手機裝兩個 LINE app，這裡提出另外一種較鮮為人知的做法，利用 LINE WORKS 做為公用帳號，也可以達到一支手機擁有兩個 LINE 的目的，還可以順帶取得漂亮的 LINE@ 帳號。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;line.worksmobile.com&#x2F;jp&#x2F;en&#x2F;&quot;&gt;LINE WORKS&lt;&#x2F;a&gt; 是 LINE 推出的工作群組的服務，除了與 LINE 互通的聊天功能外，因為是針對工作群組設計，所以多了一些組織管理的功能，不過本文主要會著重在介紹如何取得 LINE WORKS 帳號，以及 LINE WORKS 帳號與 LINE 之間的互通運用，關於組織管理的方面不會帶到太多。&lt;&#x2F;p&gt;
&lt;p&gt;先說說申請 LINE WORKS 的好處：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;公私帳號分離，這是最大的好處。&lt;&#x2F;li&gt;
&lt;li&gt;可以免費取得一個漂亮的 LINE@ 帳號，像是 master@thunderlotus 這樣的帳號。&lt;&#x2F;li&gt;
&lt;li&gt;可以與 LINE 互通，基本的聊天、照片、表情、貼圖、檔案、位置都可以互傳。&lt;&#x2F;li&gt;
&lt;li&gt;如果善用組織管理的功能，對公司來說可以很好管控員工的 LINE WORKS 帳號，不怕業務離職聯繫不到客戶。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;再說說 LINE WORKS 目前不足的地方：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;因為 LINE WORKS 帳號對一般的 LINE 來說就是個 LINE@ 好友，所以 LINE@ 目前不支援的語音通話和視訊通話在 LINE WORKS 也無法和 LINE 好友打語音通話和視訊通話（但短語音訊息是支援的），這是目前最大的問題。（但 LINE WORKS 好友對 LINE WORKS 好友是可以做語音、視訊通話的。）&lt;&#x2F;li&gt;
&lt;li&gt;若要與 LINE 聊天，必須對方主動加你為好友（透過連結或 ID 或 QR Code），因為 LINE@ 帳號無法主動加好友。（但 LINE WORKS 好友對 LINE WORKS 好友是可以直接加的。）&lt;&#x2F;li&gt;
&lt;li&gt;設定較複雜，LINE WORKS 是以組織為對象的產品，就算只想申請自己的 LINE WORKS 帳號，也要先建立組織才能建個人帳號。&lt;&#x2F;li&gt;
&lt;li&gt;LINE WORKS 雖然也會拿到 LINE@ 的帳號，但 LINE WORKS app 內無法編輯聊天室選單或做群發，想用 LINE WORKS 做免費群發的可以打消念頭了。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;LINE WORKS 這項產品目前並未在台灣市場推廣，不過還是有華文版的 web 和 app 可以使用。總體用起來除了最大的優勢—可以與 LINE 互通外，其實功能滿陽春的，當然 LINE WORKS 還有&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;line.worksmobile.com&#x2F;jp&#x2F;en&#x2F;pricing&#x2F;&quot;&gt;付費的加值服務&lt;&#x2F;a&gt;，這部份本人沒用過就不亂說了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shen-ban-line-works&quot;&gt;申辦 LINE WORKS&lt;&#x2F;h2&gt;
&lt;p&gt;當然是先到 LINE WORKS 的網頁：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;line-works&#x2F;1.png&quot; alt=&quot;LINE WORKS&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;猛擊右上的「Join for FREE」，會問我們是要用 LINE 帳號做申辦還是不要呢？&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;line-works&#x2F;2.png&quot; alt=&quot;LINE WORKS&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;我們選擇用不用 LINE 帳號做申辦，選「Create a LINE WORKS account」，會跳轉到電話驗證的部份，電話驗證的部份因為很常見了就快速跳過，驗證通過後來到重要的一步，為我們的取一個漂亮的 LINE@ 群組名：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;line-works&#x2F;3.png&quot; alt=&quot;LINE WORKS&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;這裡我取個霸氣的 @thunderlotus 做為我的群組名稱，在這個群組內的個人帳號就都會是 xxx@thunderlotus 這樣的格式。&lt;&#x2F;p&gt;
&lt;p&gt;下一步開始建立個人帳號，輸入姓名後再建立個人 ID 與密碼：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;line-works&#x2F;4.png&quot; alt=&quot;LINE WORKS&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;至此群組、帳號都建立完畢了，按下 Next 後會問我們要不要邀請同事加入這個群組，就先略過吧！&lt;&#x2F;p&gt;
&lt;h2 id=&quot;line-works-web&quot;&gt;LINE WORKS web&lt;&#x2F;h2&gt;
&lt;p&gt;前面申辦的步驟結束後會自動跳轉到 LINE WORKS 的 web app，之後也可以從 LINE WORKS 首頁右上角的的「My Service」連結進來。&lt;&#x2F;p&gt;
&lt;p&gt;下面是一些 LINE WORKS 有關的操作，本文只會提到 LINE WORKS 特別需要注意的點，如果是很常識類的聊天的操作就不會特別提了。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;xiu-gai-wei-hua-wen-jie-mian&quot;&gt;修改為華文介面&lt;&#x2F;h3&gt;
&lt;p&gt;LINE WORKS web app 與一般聊天 app 相當類似，不過初次進來會是英文介面，可以按右上角的頭像的「Language &#x2F; Time zone」來變更為華文介面：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;line-works&#x2F;5.png&quot; alt=&quot;LINE WORKS web&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;語言和時區儲存後重新整理網頁應該就可以看到華文介面了。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;jia-ru-hao-you&quot;&gt;加入好友&lt;&#x2F;h3&gt;
&lt;p&gt;因為 LINE WORKS 帳號對 LINE 來說就是個 LINE@ 帳號，因此也受到 LINE@ 的限制，LINE WORKS 無法主動加入一般的 LINE 成為好友，必須請對方在他的 LINE 裡面搜尋我們的 LINE WORKS 帳號（如本例的 master@thunderlotus）並加入，雙方才可以成為好友。&lt;&#x2F;p&gt;
&lt;p&gt;另外在手機端也有 QR Code 可以做為加入好友的管道，不過一樣必須是由 LINE 這端發起加入好友。&lt;&#x2F;p&gt;
&lt;p&gt;第二種情況，LINE WORKS 與同樣是 LINE WORKS 帳號即可自行加好友，而且還可以跨組織，也就是假設 master@thunderlotus 認識一位在蘋果社任職的朋友，master@thunderlotus 可以直接搜尋對方的 LINE WORKS ID 並加為好友。&lt;&#x2F;p&gt;
&lt;p&gt;第二種情況的操作方式，在 LINE WORKS web app 上方切換到通訊錄後，按「添加聯繫人」→ 「外部聊天對象」即可搜尋對方的 LINE WORKS ID 並加為好友：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;line-works&#x2F;6.png&quot; alt=&quot;LINE WORKS web&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;line-works-app&quot;&gt;LINE WORKS app&lt;&#x2F;h2&gt;
&lt;p&gt;LINE WORKS 除了 web app 外，也有提供&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;line.worksmobile.com&#x2F;jp&#x2F;en&#x2F;download&#x2F;&quot;&gt;各大主流平台的原生 app&lt;&#x2F;a&gt;，用起來的體驗與 LINE app 也相當類似，如果不看那些為組織設計的功能的話，用起來就像是陽春版的 LINE，另外 LINE WORKS 因為是以群組為出發點設計的產品，所以在 app 這邊是支援多帳號切換的，相當方便。&lt;&#x2F;p&gt;
&lt;p&gt;LINE WORKS 雖然可以與 LINE 互通聊天，不過一樣受到 LINE@ 的限制，語音聊天與視訊聊天是無法使用的，這是比較明顯的問題，在申辦 LINE WORKS 前最好考慮一下否則會白裝一趟。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>記事服務手機 app 短評</title>
        <published>2020-06-27T00:00:00+00:00</published>
        <updated>2020-06-27T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2020/note-taking-apps/"/>
        <id>https://editor.leonh.space/2020/note-taking-apps/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2020/note-taking-apps/">&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;note-taking-apps&#x2F;competitors.png&quot; alt=&quot;Note taking apps&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;重拾網誌以來，因為改用靜態網站產生器，所以沒有後台這種東西，因此必須發展出新的作業流程，因為本來就有用這些記事服務作筆記的習慣，因此也養成了拿這些記事服務當作網誌的草稿撰寫處。另外一個原因是它們大多有出手機 app，讓我更方便在任何時刻記下腦中的想法，在試用了幾個記事服務的 app 後，也把心得分享在這邊，方便給有同樣要用 app 記事的朋友們參考。&lt;&#x2F;p&gt;
&lt;p&gt;這陣子用過的記事服務有 Notion、Coda、Quip、Dropbox Paper。下面只會講在 Android app 端明顯的缺點，至於優點…因為都滿共通的，在這邊統一提一下這些記事服務共同的特色：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;大多原生支援 Markdown，可以以鍵盤進行大部分的操作，避免雙手在鍵盤與滑鼠間頻繁切換，提高記事的效率，並且讓記事的思緒不被打斷。&lt;&#x2F;li&gt;
&lt;li&gt;除了記事必備的文字、標題、清單、表格、圖片外，都另外提供了豐富的「區塊」可以直接插入筆記內，通常包括地圖、表情符號、月曆、試算表、雲端空間檔案、程式碼等，可以以筆記為中心把相關的檔案都整理在筆記內。&lt;&#x2F;li&gt;
&lt;li&gt;有 API 可以讓筆記與外部系統合作。&lt;&#x2F;li&gt;
&lt;li&gt;表格提供了基礎的公式或關聯性，讓表格可以當成試算表或是關聯式資料表。&lt;&#x2F;li&gt;
&lt;li&gt;具有多元的呈現方式，譬如說待辦事項區塊，可以以最單純的清單式呈現，或月曆式、看板式呈現。&lt;&#x2F;li&gt;
&lt;li&gt;具有多人協作功能，但有些是要付費才能啟用多人協作的。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;notion&quot;&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.notion.so&#x2F;&quot;&gt;Notion&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;note-taking-apps&#x2F;www.notion.so.png&quot; alt=&quot;Notion&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Notion 大概是這幾個裡面最多人用的記事服務，並且也是最大方的—自 2020 年五月起免費的個人版拿掉了以往只能存一千個區塊的限制。&lt;&#x2F;p&gt;
&lt;p&gt;缺點方面，在 Android app 端最大的問題是，只要切換成注音鍵盤，按倒退鍵刪「一個字」後，鍵盤就會自動關閉，變成要一個字一個字刪，或者是切換成英文鍵盤就可以獲得正常的刪除體驗，這點相當惱人，不管是那種方式都會被迫打斷思緒，最後…會…寫出…&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;m.facebook.com&#x2F;caigezhuanyebinlangtan&quot;&gt;財哥&lt;&#x2F;a&gt;…體。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;coda&quot;&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;coda.io&#x2F;&quot;&gt;Coda&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;note-taking-apps&#x2F;coda.io.png&quot; alt=&quot;Coda&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Coda 在 web 端體驗相當好，不輸 Notion，但是在 app 上就差很遠了，Coda 的 app 的編輯功能相當陽春，只能做基本的文字樣式的變更，不能插入超連結和圖片，強大的區塊功能也完全沒有，與 web 體驗比起來差滿多，因為 Coda 比較新，期待後面會把缺的功能逐步補上。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;quip&quot;&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;quip.com&#x2F;&quot;&gt;Quip&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;note-taking-apps&#x2F;quip.com.png&quot; alt=&quot;Quip&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Quip 算是相當老牌的記事服務了，被 Salesforce 買走後轉型成以企業端為主的團隊協作服務，也整合了很多企業端的應用，像是 Salesforce 和 Jira 的區塊等等，不過在 Android app 這端，相當可惜的簡直是不可用的狀態，它的編輯工具列九成都會被輸入法遮蔽，變成功能弱化到只能打字，希望 Quip 可以趕快修正。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;dropbox-paper&quot;&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;paper.dropbox.com&#x2F;&quot;&gt;Dropbox Paper&lt;&#x2F;a&gt;&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;note-taking-apps&#x2F;paper.dropbox.com.png&quot; alt=&quot;Dropbox Paper&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Dropbox Paper 原本是叫 Hackpad，後來被 Dropbox 收購才變成 Dropbox Paper。Dropbox Paper 不管是在 web 端或是 app 端都還比不上前面御三家，而且感覺也被 Dropbox 放生的樣子，不像 Notion 一陣子就放出更新感覺很活躍。&lt;&#x2F;p&gt;
&lt;p&gt;Dropbox Paper 在 web 與 app 端有共同的問題，會不斷的遇到斷線的錯誤提示，而且不論是走固網或行動網路皆然，明顯是它們的程式或服務端的問題，同樣也是會中斷我們寫作的思緒，期待 Dropbox 能早日走出撞牆期重返農藥給我們一個良好的 Paper 體驗。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-yu&quot;&gt;結語&lt;&#x2F;h2&gt;
&lt;p&gt;目前用來用去在 app 上和 web 綜合體驗最好的還是 Notion，只是那倒退鍵引發的注音鍵盤收起的現象真的滿惱人的 &amp;gt;“&amp;lt;。&lt;&#x2F;p&gt;
&lt;p&gt;另外還有一些遺珠，例如 HackMD 和 StackEdit，未列入的原因是它們都沒有手機 app，但其實它們在 web 上也都有很好的寫作體驗，希望有朝一日能看到它們推出 app 讓我們有更多選擇。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>打卡怎麼打？</title>
        <published>2020-06-22T00:00:00+00:00</published>
        <updated>2020-06-22T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2020/checking-in/"/>
        <id>https://editor.leonh.space/2020/checking-in/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2020/checking-in/">&lt;p&gt;試問一間自詡為新創公司的公司，打卡應該怎麼打？&lt;&#x2F;p&gt;
&lt;p&gt;這個問題對很多新設立的公司行號來說可能根本不是問題，但我想既然某社自詡為新創，那麼就來研究看看，揪～竟，卡應該怎麼打。&lt;&#x2F;p&gt;
&lt;p&gt;本文的另一個目的，也試圖想釐清，對不到十人的小公司，市面上那堆琳瑯滿目的差勤系統，到底是過度設計或恰如其分。&lt;&#x2F;p&gt;
&lt;p&gt;在擬定打卡制度以前，我們先確定各方勢力對打卡的野望是什麼，再試圖在能滿足各方期待下擬定一個小公司的打卡原則。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;he-gui&quot;&gt;合規&lt;&#x2F;h2&gt;
&lt;p&gt;最基礎的一點，必須符合台灣法規。&lt;&#x2F;p&gt;
&lt;p&gt;要點就是要有出勤紀錄，但格式不拘。細節可看以下台灣與打卡有關的條文摘錄：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;law.moj.gov.tw&#x2F;LawClass&#x2F;LawSingle.aspx?pcode=N0030002&amp;amp;flno=21&quot;&gt;勞動基準法，第四章 工作時間、休息、休假，第 30 條&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;ol start=&quot;5&quot;&gt;
&lt;li&gt;雇主應置備勞工出勤紀錄，並保存五年。&lt;&#x2F;li&gt;
&lt;li&gt;前項出勤紀錄，應逐日記載勞工出勤情形至分鐘為止。勞工向雇主申請其
出勤紀錄副本或影本時，雇主不得拒絕。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;law.moj.gov.tw&#x2F;LawClass&#x2F;LawSingle.aspx?pcode=N0030002&amp;amp;flno=21&quot;&gt;勞動基準法施行細則，第四章 工作時間、休息、休假，第 21 條&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;本法第三十條第五項所定出勤紀錄，包括以簽到簿、出勤卡、刷卡機、門
禁卡、生物特徵辨識系統、電腦出勤紀錄系統或其他可資覈實記載出勤時
間工具所為之紀錄。&lt;&#x2F;li&gt;
&lt;li&gt;前項出勤紀錄，雇主因勞動檢查之需要或勞工向其申請時，應以書面方式
提出。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;law.moj.gov.tw&#x2F;LawClass&#x2F;LawSingle.aspx?pcode=B0010064&amp;amp;flno=38&quot;&gt;勞動事件法，第三章 訴訟程序，第 38 條&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;出勤紀錄內記載之勞工出勤時間，推定勞工於該時間內經雇主同意而執行
職務。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.mol.gov.tw&#x2F;announcement&#x2F;2099&#x2F;36489&#x2F;&quot;&gt;勞動部，勞工的出勤紀錄，得由勞雇間約定最適宜雙方之記載方式。&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;blockquote&gt;
&lt;p&gt;有鑑於科技日新月異，《勞動基準法》所規定之出勤紀錄記載方式，早已不再僅以傳統之簽到簿或刷卡機為限，可輔以電腦資訊或電子通信設備協助記載，本部已應產業變遷，經濟活動越趨多元，訂定「勞工在事業場所外工作時間指導原則」，參照該指引二、(六)列舉行車紀錄器、GPS 紀錄器、電話、手機打卡、網路回報、客戶簽單、通訊軟體(如工時APP)，或其他可供稽核出勤紀錄之工具。故凡可資核實記載勞工出勤時間工具所為之紀錄，均得作為勞工之出勤紀錄；勞雇間得約定最適合雙方之工時記載方式。&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;h2 id=&quot;zi-fang-de-ye-wang&quot;&gt;資方的野望&lt;&#x2F;h2&gt;
&lt;p&gt;對不到十人的「新創」頭家來說，因為沒有配置專職的人事，希望可以簡化在人事作業上的人力成本，但又需要符合法規，以及因為自認為「新創」，希望給員工有彈性的管理制度，但又擔心會遭到對公司心懷不滿的員工的檢舉。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yuan-gong-de-qi-wang&quot;&gt;員工的期望&lt;&#x2F;h2&gt;
&lt;p&gt;頭家一直說自己是新創，可是怎麼看都是一堆舊思維，不知道新創在哪裡，特別是管理制度上還是很守舊，不知道為何逐漸走向一種越 E 化卻效率越差的管理風格。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ke-neng-de-fang-an&quot;&gt;可能的方案&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;chuan-tong-da-qia-zhong&quot;&gt;傳統打卡鐘&lt;&#x2F;h3&gt;
&lt;p&gt;最簡單古老的打卡，缺點很明顯，每月計算工時要花人專職去統計，耗時耗力，而且無法遠端打卡。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;shu-wei-da-qia-zhong&quot;&gt;數位打卡鐘&lt;&#x2F;h3&gt;
&lt;p&gt;比傳統打卡鐘好一點，每月會自動幫我們產出報表，但報表格式往往需要二次整理，且還是無法遠端打卡。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;chai-qin-app&quot;&gt;差勤 App&lt;&#x2F;h3&gt;
&lt;p&gt;跟數位打卡鐘幾乎一樣，有報表但往往需要二次整理，支援遠端打卡。&lt;&#x2F;p&gt;
&lt;p&gt;差勤 app 當然還有許多雞肋功能，不過非本文重點就略過。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;ren-shi-xi-tong&quot;&gt;人事系統&lt;&#x2F;h3&gt;
&lt;p&gt;十人份的「新創」公司因為制度簡化，不需要人事系統，如果你的十人份公司有複雜到需要人事系統的話，那千萬不要叫自己新創。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;dian-zi-biao-ge&quot;&gt;電子表格&lt;&#x2F;h3&gt;
&lt;p&gt;指的是 Google Sheets 這類的表格檔，好像很 low 但其實幾乎具備了所有的好處：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;可遠端打卡。&lt;&#x2F;li&gt;
&lt;li&gt;免二次整理，因為格式是我們自己設計的。&lt;&#x2F;li&gt;
&lt;li&gt;可補打卡。&lt;&#x2F;li&gt;
&lt;li&gt;有函式可用。&lt;&#x2F;li&gt;
&lt;li&gt;不用錢。&lt;&#x2F;li&gt;
&lt;li&gt;合法合規。&lt;&#x2F;li&gt;
&lt;li&gt;保存簡易。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;當然頭家會擔心員工未填、忘填、亂填，不管因素為何，請員工補填就好了，補填的正確性的認定法也很簡單，勞資雙方認定是正確的就是正確的，好像很薄弱，不過這其實是任何主觀事實的認定法則，反過來說，勞資有一方認定不正確，不論你是指紋打卡還是什麼高科技打卡，都是一樣要上勞資爭議仲裁的，所以用高科技把事情變複雜沒有比較厲害。&lt;&#x2F;p&gt;
&lt;p&gt;至於請假，身為一個自詡「新創」的頭家，福利應該是優於勞基法的，所以請假只應該有四種，不扣薪特休、不扣薪產假、不扣薪公假、不扣薪喪假，勞基法規定那一大堆假別在公司內應該只會是這四種之一，生病一律請特休，私事一律請特休。如果還有其它的假別，那還是別說自己很「新創」了吧。&lt;&#x2F;p&gt;
&lt;p&gt;在排除掉假新創的頭家之後，真．新創的頭家應該都可以理解到，用電子表格處理十人份的差勤是綽綽有餘，而且可以真正做到以簡馭繁。&lt;&#x2F;p&gt;
&lt;p&gt;那遇到雞歪員工特愛請假怎麼辦？這跟人事考核制度有關，跟請假規則無關，愛請假不代表績效差，所以千萬不要用請假規則條文訂死直接掛勾績效，那只會讓你的「新創」事業失去管理彈性，至於頭家怎麼使用人事考核制度來確保公司的權益與管理彈性應該是當頭家的基本能力，如果不會運用那公司還是別開了吧。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;can-kao-zi-liao&quot;&gt;參考資料&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;bryan.law&#x2F;work-from-home&#x2F;&quot;&gt;遠端工作 (WORK FROM HOME) ，勞工權益與合法性怎兼顧&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;http:&#x2F;&#x2F;books.cw.com.tw&#x2F;blog&#x2F;article&#x2F;1415&quot;&gt;遠距上班出勤紀錄怎麼辦？勞資兩方要注意哪些事？&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.businesstoday.com.tw&#x2F;article&#x2F;category&#x2F;80408&#x2F;post&#x2F;202001020010&#x2F;%E8%80%81%E9%97%86%E5%93%A1%E5%B7%A5%E9%83%BD%E8%A6%81%E7%9C%8B%EF%BC%81%E5%8B%9E%E5%8B%95%E4%BA%8B%E4%BB%B6%E6%B3%951%E6%9C%881%E6%97%A5%E4%B8%8A%E8%B7%AF%EF%BC%8C%E4%B8%8A%E4%B8%8B%E7%8F%AD%E6%89%93%E5%8D%A1%E8%A9%B2%E6%B3%A8%E6%84%8F%E5%93%AA%E4%BA%9B%E4%BA%8B%EF%BC%9F&quot;&gt;老闆員工都要看！勞動事件法1月1日上路，上下班打卡該注意哪些事？&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;twworkforce.com&#x2F;2017&#x2F;06&#x2F;01&#x2F;attendancerecord&#x2F;&quot;&gt;工時與工資的基石—讓你知道「出勤紀錄」有多重要！&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>談 CSS 命名</title>
        <published>2020-06-20T00:00:00+00:00</published>
        <updated>2020-06-20T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2020/css-naming/"/>
        <id>https://editor.leonh.space/2020/css-naming/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2020/css-naming/">&lt;p&gt;一直以來本人較偏好的 CSS 命名都是依照 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;eddiewen.gitbooks.io&#x2F;rscss&#x2F;content&#x2F;&quot;&gt;rscss&lt;&#x2F;a&gt; 的原則，可是即便如此，還是常常會遇到難以命名的問題，這篇試著把問題梳理，並提出一些可能的解法。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;rscss&quot;&gt;rscss&lt;&#x2F;h2&gt;
&lt;p&gt;先簡單介紹一下 rscss 的命名原則，在 rscss 我們把網頁拆各部位解成一塊塊的元件（component），例如 header 元件，而元件可以再容納子元件，例如 header 元件內有 brand、navigation、search 三個子元件，它們也可以有各自的子元件，一直到最後的小單位稱為元素（element），像是導覽區內的單一的連結，或是搜尋區的文字框等等，但 rscss 的元素並非一定相當於 HTML 的 tag，例如前面的導覽區的一個連結，在 rscss 的概念是元素，但在 HTML 結構上它可能是數個 tag 組合起來的：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;a&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;i&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;i&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;link text&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;a&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;總而言之 rscss 或其它的 CSS 命名原則，大多是以頁面的結構邏輯來解構與命名，而非以 HTML 碼結構來命名。&lt;&#x2F;p&gt;
&lt;p&gt;當然 rscss 很棒，但它只是原則性的指南，現實上有些狀況需要自行排除，下面談現實上會遇到某些難以命名的情況。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zhe-kuai-zen-mo-qu-ming-container-huan-shi-wrapper-huan-shi-block-huan-shi-qi-ta&quot;&gt;這塊怎麼取名？container 還是 wrapper 還是 block 還是其它？&lt;&#x2F;h2&gt;
&lt;p&gt;對於組件，rscss 的命名原則是兩個單字，中間以連字號串連，例如 &lt;code&gt;.left-sidebar&lt;&#x2F;code&gt; 或 &lt;code&gt;.search-block&lt;&#x2F;code&gt;；而對於元素，則只用一個單字命名，這樣的命名原則很直觀的區分了組件與元素結構上的層級，但若是父子皆為組件的情況，又會再次遇到命名的問題。舉例來說，對於元件，最常用的也就是那三個單字—container、wrapper、block，例如這樣的 HTML 結構：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;body&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;main&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; class&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;main-container&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;section&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; class&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;list-wrapper&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;ul&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; class&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;list-block&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;       &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;li&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;xxx&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;li&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      &amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;ul&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;section&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;main&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;body&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;以上面的例子來說，如果走到第四層，就立馬會遇到「單字不夠用」的問題，下面提出一些個人的解法。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ke-neng-de-jie-fa&quot;&gt;可能的解法&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;加入 section 之類的描述「區塊」的字，讓可巢狀的層級更多，不過這看起來不是個夠通用的解法。&lt;&#x2F;li&gt;
&lt;li&gt;對於功能型元件，強迫自己禁止使用 container、wrapper、block、section 之類的單字，而改用更具有描述性、語意性的字代替，譬如說 &lt;code&gt;.navigation-list&lt;&#x2F;code&gt;，而 container、wrapper、block、section 僅限用於排版型元件，或稱為容器型元件，就是只為了切版而存在的元件。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;bu-chong&quot;&gt;補充&lt;&#x2F;h2&gt;
&lt;ol&gt;
&lt;li&gt;關於描述「區塊」的單字們—container、wrapper、section、block，誰大誰小應該也是要在專案內被定義的，可參考別人家的文章：
&lt;ol&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;css-tricks.com&#x2F;best-way-implement-wrapper-css&#x2F;&quot;&gt;The Best Way to Implement a“Wrapper”in CSS&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;stackoverflow.com&#x2F;q&#x2F;4059163&quot;&gt;CSS Language Speak: Container vs Wrapper?&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;&#x2F;li&gt;
&lt;li&gt;別的流派的 CSS 命名原則有可能不會遇到前文描述的問題，不過過於畫蛇添足，非我所愛。把事情變複雜沒有比較厲害。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;h2 id=&quot;can-kao-zi-liao&quot;&gt;參考資料&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rstacruz&#x2F;rscss&quot;&gt;rscss&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>為什麼你的個資會外洩？談社交工程</title>
        <published>2020-06-14T00:00:00+00:00</published>
        <updated>2020-06-14T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2020/social-engineering/"/>
        <id>https://editor.leonh.space/2020/social-engineering/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2020/social-engineering/">&lt;h2 id=&quot;guang-yi-de-she-jiao-gong-cheng&quot;&gt;廣義的社交工程&lt;&#x2F;h2&gt;
&lt;p&gt;社交工程由來已久，在早年資訊不發達的年代，就已經有商業間諜藉機認識目標企業內的重點人士，透過各種檯面下的手段或欺騙來取得想要的情報，甚至不惜動用社交工程的進階版—性交工程來達到目的，當然性交工程不用非得自己出馬，可能來些&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E9%99%AA%E7%9D%A1&quot;&gt;性招待&lt;&#x2F;a&gt;就可以。不過下面要談的社交工程更侷限在近年較常發生的個資外洩與後續的詐騙，它們的根本原因其中就包括社交工程。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xian-zai-de-she-jiao-gong-cheng&quot;&gt;現在的社交工程&lt;&#x2F;h2&gt;
&lt;p&gt;這邊講的社交指的是透過一些欺騙的手段把惡意程式植入目標電腦，以 EC 營運方業主來說，最常面臨到社交手段的來源就是客服，客服大哥大姐們每天日常的工作都會收到數十封的顧客信件，一旦資安心態稍微有點鬆懈，或者毫無資安意識的情況下，很容易就陷入釣魚信的陷阱，以實際發生的例子來說，壞人會偽裝成顧客向客服詢問問題，並且在後續的信件往來中，藉故夾帶惡意程式，而這些惡意程式往往會偽裝成圖片檔，例如 photo1.jpg.exe 這樣的檔案，並且再用難以被掃描的帶密碼的 RAR 格式壓縮，以躲過郵件系統的安全偵測，一旦客服被釣到，把附檔解開，而那 photo1.jpg.exe 又因為萬惡的 Windows 預設不顯示副檔名，因此客服只會看到一個帶 JPEG 圖示的 photo1.jpg，並在毫無戒心的情況下猛擊執行，自此壞人順利的在客服電腦內植入木馬程式。&lt;&#x2F;p&gt;
&lt;p&gt;具體這支木馬程式的行為，藉由刑事局的協助我們可以一窺究竟：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;social-engineering&#x2F;%E7%A4%BE%E4%BA%A4%E9%83%B5%E4%BB%B6%E6%A8%A3%E6%9C%AC%E5%88%86%E6%9E%90.png&quot; alt=&quot;社交郵件樣本分析&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;chu-li&quot;&gt;處理&lt;&#x2F;h2&gt;
&lt;p&gt;身為經營者或經理人，不過是想做點 EC，卻被壞人有針對性的攻擊，往往是收到大量客服案件才意識到自家的資安意識有多薄弱，我知道很多人都自詡為危機管理大師，這些大師也深受老闆重用，可是老闆往往忽視事前的防範比事後的管控更重要，而遺憾的是，事前的防範需求一定有人曾經提出，也往往一定不被重視，所以即便有再多的幕僚在剛愎自用的頭家被鬼遮眼的情況之下也是無用。&lt;&#x2F;p&gt;
&lt;p&gt;扯遠了，下面還是列一下應該建立起的可實行的制度：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;big5.minghui.org&#x2F;mh&#x2F;articles&#x2F;2001&#x2F;12&#x2F;9&#x2F;21123.html&quot;&gt;發正念&lt;&#x2F;a&gt;。&lt;&#x2F;li&gt;
&lt;li&gt;建立員工的資安意識，這點最重要也最困難，那些大哥大姐不是你教了就有效果的。&lt;&#x2F;li&gt;
&lt;li&gt;建立更嚴格的郵件掃描規則，惡意程式為了避免被掃到都會包成帶密碼的 RAR，在郵件規則上，凡是無法掃描的壓縮檔一律不收。&lt;&#x2F;li&gt;
&lt;li&gt;員工不允許使用 Administrator 權限。&lt;&#x2F;li&gt;
&lt;li&gt;管制員工的 port。&lt;&#x2F;li&gt;
&lt;li&gt;Windows 副檔名一律開啟。&lt;&#x2F;li&gt;
&lt;li&gt;對 EC 後台強制開啟二段式認證。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;這些制度只有第二點是真正有價值的，其它都只是玩魔高一尺道高一丈的遊戲而已，這些被動的應變措施是列舉不完的，又會讓員工覺得「好麻煩」，真正值得投資的還是建立員工的資安意識，而這又牽扯到頭家的心態，建立資安意識絕對只會是 top-down 的策略，頭家不對內宣示對資安的重視程度與相關教育作為，就不可能期待員工會有相對的資安意識，老闆都不重視了，我幹嘛花時間自我精進老闆不重視的事情！&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>收到勒索信</title>
        <published>2020-06-13T00:00:00+00:00</published>
        <updated>2020-06-13T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2020/scam/"/>
        <id>https://editor.leonh.space/2020/scam/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2020/scam/">&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;scam&#x2F;2019-07-06-13-50-40-De-Ying-Mu-Xie-Tu-lg.png&quot; alt=&quot;勒索信&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;上面是最近收到的勒索信，之所以值得一提是&lt;strong&gt;信件內有我的密碼&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;整封信大意如下：&lt;&#x2F;p&gt;
&lt;p&gt;「我知道你的密碼是 *******，你的瀏覽器沒更新，被我偷種了木馬，因此你的帳密、個資、電腦、視訊畫面都在我的掌握之下，我可以公佈你的私密個資給你的親朋好友或是放到社交網站上，除非你轉給我八百塊比特幣，你只有三天時間考慮。」&lt;&#x2F;p&gt;
&lt;p&gt;好像很嚇人，莫非我對著螢幕偷挖鼻孔的畫面會被傳出去，oh no。&lt;&#x2F;p&gt;
&lt;p&gt;不過有一些蛛絲馬跡令人覺得起疑：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;密碼的確是對的，不過收到這封信的信箱是至少七年完全沒在用的舊信箱，壞人不可能是在我現用的電腦去找到這組帳密，因為七年沒登入過這點我很確定，或是這是一封遲來了七年的勒索信？（好像什麼浪漫情節 &amp;gt;&#x2F;&#x2F;&#x2F;&amp;lt;）&lt;&#x2F;li&gt;
&lt;li&gt;本宅宅一向走在時代的最尖端，瀏覽器一定是最新的。除非是借用到別人的電腦，瀏覽器沒更新這點才成立。&lt;&#x2F;li&gt;
&lt;li&gt;八百塊比特幣相當於兩億多新台幣，我的個資沒那麼值錢。壞人也沒有提供討價還價的管道，這點不夠專業。&lt;&#x2F;li&gt;
&lt;li&gt;去 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;haveibeenpwned.com&#x2F;&quot;&gt;Have I Been Pwned&lt;&#x2F;a&gt; 查一下可以發現這組信箱帳密在 2016 年已經有外洩過的紀錄。&lt;&#x2F;li&gt;
&lt;li&gt;拿信件內文去 Google 一下，有一模一樣的遭遇被討論過。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;基於以上幾點，本宅決定不予理會，看看三天後有沒有人收到我挖鼻孔的照片囉！&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>開源產業</title>
        <published>2020-06-10T00:00:00+00:00</published>
        <updated>2020-06-10T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2020/open-source/"/>
        <id>https://editor.leonh.space/2020/open-source/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2020/open-source/">&lt;p&gt;從《&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;crazyangelo.github.io&#x2F;Cathedral-and-Bazaar&#x2F;&quot;&gt;教堂與市集&lt;&#x2F;a&gt;》說起，這部兩千年的經典文章告訴我們開源軟體與商業軟體的差異，開源軟體的開發模式像市集，看似無章法卻在無章法中有機般的長出自己的一條路；商業軟體的開發模式則像教堂，團隊遵守嚴謹的教條依照教條的指示建構軟體專案。&lt;&#x2F;p&gt;
&lt;p&gt;然而今日再回頭看這篇二十年前的經典文章，卻令人覺得與當代的開源產業格格不入。&lt;&#x2F;p&gt;
&lt;p&gt;是的，現在的開源，已經形成一種&lt;strong&gt;產業&lt;&#x2F;strong&gt;，再拿教堂、市集來比喻可能都無法恰當的套入，而更像是兩者的混搭。凡是稍具規模的開源產品，源是開了，但是是由背後的資本企業所控制的，企業把產品開源，是有助於提昇在社群間的形像，但這一切努力最終還是以獲利為依歸。&lt;&#x2F;p&gt;
&lt;p&gt;不要誤會，我不是要批判這種現象，我本人也並非佛心來著的人，也是要賺錢吃飯的。&lt;&#x2F;p&gt;
&lt;p&gt;只是最近的一些新聞令我感受到過往那種純粹以熱請壯志為出發點的市集模式的消亡，並因此有點感慨罷了。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>語意化網頁</title>
        <published>2020-05-28T00:00:00+00:00</published>
        <updated>2020-05-28T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2020/semantic/"/>
        <id>https://editor.leonh.space/2020/semantic/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2020/semantic/">&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E8%AF%AD%E4%B9%89%E7%BD%91&quot;&gt;語意網&lt;&#x2F;a&gt;，或者也有人稱為語義網，都是英文 semantic web 的翻譯，指的是在 html 內埋入一些定義好的標籤，這些定義好的標籤裡面有可以讓機器更容易理解網頁的內容，也就是語意，這整件事就稱為語意網。&lt;&#x2F;p&gt;
&lt;p&gt;語意網的概念一直到今天都還存在，不過換了個名字，現在埋語意標籤這件事一般稱為 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E6%90%9C%E5%B0%8B%E5%BC%95%E6%93%8E%E6%9C%80%E4%BD%B3%E5%8C%96&quot;&gt;SEO&lt;&#x2F;a&gt;，目的也更明確，就是盡可能的讓搜尋引擎讀懂我的網頁，並更準確的對應到潛在訪客的關鍵字搜尋結果內，最終就是要爭取更多的訪客，不過本人是個老派的人，在本文還是用語意網稱之。&lt;&#x2F;p&gt;
&lt;p&gt;語意網要埋入的標籤，實做的規範很多，並未統一，甚至 HTML5 也加上了 &lt;code&gt;&amp;lt;main&amp;gt;&lt;&#x2F;code&gt;、&lt;code&gt;&amp;lt;section&amp;gt;&lt;&#x2F;code&gt;、&lt;code&gt;&amp;lt;article&amp;gt;&lt;&#x2F;code&gt;、&lt;code&gt;&amp;lt;aside&amp;gt;&lt;&#x2F;code&gt;、&lt;code&gt;&amp;lt;nav&amp;gt;&lt;&#x2F;code&gt; 等一系列標籤試圖讓 HTML 本身就可以語義化，不過事實證明這些在外觀和行為上和 &lt;code&gt;&amp;lt;div&amp;gt;&lt;&#x2F;code&gt; 一模一樣的標籤根本對幫助機器理解網頁這件事起不了作用，因為&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;whatwg-cn.github.io&#x2F;html&#x2F;multipage&#x2F;&quot;&gt;規範&lt;&#x2F;a&gt;上對於標籤用法訂的過於寬鬆，導致沒有人知道怎麼用才叫正確，所以可以說人人都正確，也可以說人人都不正確，可是對爬蟲來說，這樣鬆散的標籤使用方式還是無法讓爬蟲正確的理解網頁的架構與內容。&lt;&#x2F;p&gt;
&lt;p&gt;語意標籤除了給爬蟲讀優化 SEO 外，還有為障礙人士埋入的輔助標籤也是一種，這些標籤讓障礙人士的輔助設備可以更好的把網頁的理解那些是控制元素（導覽、按鈕等等）哪些是內容，這一系列統稱為 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.w3.org&#x2F;TR&#x2F;html-aria&#x2F;&quot;&gt;ARIA&lt;&#x2F;a&gt; 屬性，透過把 ARIA 屬性附加在 HTML 標籤內，就可以讓障礙人士的輔助設備理解網頁的架構。&lt;&#x2F;p&gt;
&lt;p&gt;除了上面提到的 HTML5 語意標籤與 ARIA 屬性外，目前真正實際被廣泛採納的語意標籤系統是 Schema.org 與 Open Graph。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;schema-org&quot;&gt;Schema.org&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;schema.org&#x2F;&quot;&gt;Schema.org&lt;&#x2F;a&gt; 是由 Google、Microsoft、Yahoo、Yandex 一起成立的組織，他們定義了&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;schema.org&#x2F;docs&#x2F;full.html&quot;&gt;八百多種的結構化欄位格式（schema）&lt;&#x2F;a&gt;，包括文章、新聞、電影、書籍、音樂、網誌、網站、活動等等，而 schema 的撰寫格式（syntax）可以是 RDFa、Microdata 與 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;json-ld.org&#x2F;&quot;&gt;JSON-LD&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;schema-syntax&quot;&gt;Schema &#x2F; Syntax&lt;&#x2F;h3&gt;
&lt;p&gt;中文說的「格式」在資訊領域可能對應到三個不同的英文單詞－format &#x2F; schema &#x2F; syntax。&lt;&#x2F;p&gt;
&lt;dl&gt;
  &lt;dt&gt;Format&lt;&#x2F;dt&gt;
  &lt;dd&gt;
    指的是檔案的格式，以影片為例，檔案格式會有 .mov、.mp4 等。
  &lt;&#x2F;dd&gt;
  &lt;dt&gt;Schema&lt;&#x2F;dt&gt;
  &lt;dd&gt;
    指的是資料欄位的定義，以通訊錄來說，會有姓名、電話、地址、年齡等欄位，而各個欄位又有自己的欄位定義，譬如說電話欄位只能是數字或符號，不能是文字；年齡欄位只能是整數。
    更進階的會牽涉到欄位的歸納與分組，也就是關聯式資料庫的領域。
  &lt;&#x2F;dd&gt;
  &lt;dt&gt;Syntax&lt;&#x2F;dt&gt;
  &lt;dd&gt;
    指的是語法格式，延續上面的通訊錄例子，這些欄位定義可以用不同的語法規範撰寫，可能是 JSON 或 XML 或 SQL 等。
  &lt;&#x2F;dd&gt;
&lt;&#x2F;dl&gt;
&lt;p&gt;在實務上，JSON-LD 已經是目前的主流，所以可以無視其它的選項。&lt;&#x2F;p&gt;
&lt;p&gt;雖然 Schema.org 定義了八百多種 schema，但 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;developers.google.com&#x2F;search&#x2F;docs&#x2F;guides&#x2F;search-gallery?hl=zh-tw&quot;&gt;Google 只採用幾十種&lt;&#x2F;a&gt;，Google 不認得的，除非是自有其它用途，否則看起來完全沒有實用價值。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;open-graph&quot;&gt;Open Graph&lt;&#x2F;h2&gt;
&lt;p&gt;Facebook 無視於現成的 Schema.org 而自行成立了 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;ogp.me&#x2F;&quot;&gt;Open Graph&lt;&#x2F;a&gt;。Open Graph 因為臉書的關係，也是目前主流標準之一，語法以 HTML 為基礎，比 JSON-LD 更為簡單易讀也易寫。&lt;&#x2F;p&gt;
&lt;p&gt;Open Graph 只有四個基礎欄位－title、type、image、url，範本也很簡單：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;html&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;html&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; prefix&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;og: http:&#x2F;&#x2F;ogp.me&#x2F;ns#&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;head&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;title&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;The Rock (1996)&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;title&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;meta&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; property&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;og:title&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; content&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;The Rock&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt; &#x2F;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;meta&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; property&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;og:type&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; content&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;video.movie&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt; &#x2F;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;meta&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; property&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;og:url&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; content&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;http:&#x2F;&#x2F;www.imdb.com&#x2F;title&#x2F;tt0117500&#x2F;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt; &#x2F;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    &amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;meta&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; property&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;og:image&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; content&lt;&#x2F;span&gt;&lt;span&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;http:&#x2F;&#x2F;ia.media-imdb.com&#x2F;images&#x2F;rock.jpg&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt; &#x2F;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;head&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #85E89D;&quot;&gt;html&lt;&#x2F;span&gt;&lt;span&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;Open Graph 標籤內定義的內容具體會影響網頁被貼進 Facebook 後會呈現的樣貌，而 Open Graph 雖然簡單，但相較於 Schema.org 還是有所不足，以目前的業界需求來說，在臉書放產品貼文是非常普遍的需求，但 Open Graph 卻缺少了 product 這類的 type，所以要打產品的，在臉書還是只能用 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.facebook.com&#x2F;marketplace&#x2F;&quot;&gt;Facebook Marketplace&lt;&#x2F;a&gt; 或 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.facebook.com&#x2F;business&#x2F;help&#x2F;2343035149322466?id=1077620002609475&quot;&gt;Facebook Shops&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;2020-05-27-geng-xin&quot;&gt;&lt;time&gt;2020-05-27&lt;&#x2F;time&gt; 更新&lt;&#x2F;h3&gt;
&lt;p&gt;Open Graph 有 product type，不過定義在 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;developers.facebook.com&#x2F;docs&#x2F;payments&#x2F;product&#x2F;&quot;&gt;Facebook 自己的文件&lt;&#x2F;a&gt;內。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;qi-ta&quot;&gt;其它&lt;&#x2F;h2&gt;
&lt;p&gt;除了 Schema.org 和 Open Graph，越來越多台灣人用的 Twitter 也有自己的標籤系統 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;developer.twitter.com&#x2F;en&#x2F;docs&#x2F;tweets&#x2F;optimize-with-cards&#x2F;guides&#x2F;getting-started&quot;&gt;Twitter Cards&lt;&#x2F;a&gt;，&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;developer.twitter.com&#x2F;en&#x2F;docs&#x2F;tweets&#x2F;optimize-with-cards&#x2F;guides&#x2F;getting-started#opengraph&quot;&gt;Twitter Cards 相容於 Open Graph&lt;&#x2F;a&gt;，意思是 Twitter 爬蟲找不到 Twitter Cards 標籤的情況下會去找 Open Graph 的標籤，所以不用特別為 Twitter Card 埋入標籤。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;di-san-fang-gong-ju&quot;&gt;第三方工具&lt;&#x2F;h2&gt;
&lt;p&gt;前面講了這麼多格式，其實不用靠自己手工製作埋入網頁，有許多現成的第三方服務可以幫我們把這些格式產生出來，隨便找一下就有：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;webcode.tools&#x2F;&quot;&gt;Web Code Tools&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;jie-yu&quot;&gt;結語&lt;&#x2F;h2&gt;
&lt;p&gt;Schema.org 和 Open Graph 都是目前在網頁內埋入的語意格式的主流，它們能讓 Google 和 Facebook 更精確的解讀網頁的內容，對有心經營網站的人來說是必須加入的元素。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>懶人投資法</title>
        <published>2020-05-26T00:00:00+00:00</published>
        <updated>2020-05-26T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2020/investing/"/>
        <id>https://editor.leonh.space/2020/investing/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2020/investing/">&lt;p&gt;說到投資，我一定是比不上那些帶進又帶出的大神的，可是說到「懶人」投資，好像就值得拿來獻醜一下。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;lan-ren&quot;&gt;懶人&lt;&#x2F;h2&gt;
&lt;p&gt;投資能有多懶？在懶人投資法上，一個月只看一天盤，而且可以是未開盤的假日，並且在這唯一的一天同時做完看盤、記帳、下單的所有工作。&lt;&#x2F;p&gt;
&lt;p&gt;為什麼是懶人投資？因為生活與工作都太忙碌，實在沒空天天看盤，另一方面，也不想讓自己的情緒受到漲跌的影響起起伏伏。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;you-fa-da-cai-ma&quot;&gt;有發大財嗎？&lt;&#x2F;h2&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;investing&#x2F;screenshot.jpg&quot; alt=&quot;發大財&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：&lt;a href=&quot;https:&#x2F;&#x2F;github.com&#x2F;rogeraabbccdd&#x2F;Fadacai-Wallpaper&quot;&gt;Kento&lt;&#x2F;a&gt;&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;自 2007 年左右實踐懶人投資法至今，經歷了各式大大小小的&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E8%82%A1%E7%81%BD&quot;&gt;股災&lt;&#x2F;a&gt;，報酬率當然也是起起伏伏，然而以年化報酬率（XIRR）來說，大約是每年 9% 的&lt;strong&gt;複利&lt;&#x2F;strong&gt;，比傻傻的放&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;rate.bot.com.tw&#x2F;twd&quot;&gt;定存&lt;&#x2F;a&gt;要好的多很多很多。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jian-li-zheng-que-tou-zi-guan-nian&quot;&gt;建立正確投資觀念&lt;&#x2F;h2&gt;
&lt;p&gt;雖然是懶人，還是要花時間建立正確的投資觀念的，有了正確的投資觀念，不管是實施哪種投資方法，應該都可以獲得正面而且有信心的報酬。&lt;&#x2F;p&gt;
&lt;p&gt;現在在倡導正確投資觀念的網站很多，在這邊推薦&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.masterhsiao.com.tw&#x2F;&quot;&gt;怪老子理財&lt;&#x2F;a&gt;與&lt;a rel=&quot;external&quot; href=&quot;http:&#x2F;&#x2F;greenhornfinancefootnote.blogspot.com&#x2F;&quot;&gt;綠角財經筆記&lt;&#x2F;a&gt;，雖然他們的網站外觀看起來都像上個年代的網站，但內容扎實且正確，特別是怪老子的文章有許多利用試算表來協助我們做出正確的投資決斷，相當實用。&lt;&#x2F;p&gt;
&lt;p&gt;另外不要把投資與賭博混為一談，投資是為了長期資產的累積，賭博是為了追求短期輸贏的快感，雖然股票可以同時用於投資與賭博，但請把投資的股票和賭博的股票分在兩個不同的籃子內，偶爾想追求輸贏的快感，拿賭博籃子內的股票隨便玩沒關係，只是千萬不要拖累到投資的籃子。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;lan-ren-tou-zi-fa&quot;&gt;懶人投資法&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;mai-di-mai-gao&quot;&gt;買低賣高&lt;&#x2F;h3&gt;
&lt;p&gt;買低賣高，好像在說廢話一樣，可是更奇怪的是有人就是做不到，用各種理由說服自己賣在低點，還瞎扯什麼「交易紀律」、「停損」等聽起來感覺很專業的詞彙。&lt;&#x2F;p&gt;
&lt;p&gt;買低賣高是什麼意思，絕對&lt;strong&gt;不是&lt;&#x2F;strong&gt;買在低點賣在高點，而是更單純的賣價要比買價高，也就是不要虧錢賣，當然在現實上有很多造成你必須虧錢賣的原因，有這些因才有後來說服自己虧錢賣的一堆藉口的果，有因才有果，想讓自己不虧錢賣，要從成因下手，這些因，後面會慢慢提到。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;bu-yao-yu-ce-gu-jia&quot;&gt;不要預測股價&lt;&#x2F;h3&gt;
&lt;p&gt;前面提到買低賣高有說到買低賣高&lt;strong&gt;不是&lt;&#x2F;strong&gt;買在低點賣在高點，因為懶人不預測股價、不預測趨勢、不理會空頭多頭、不相信技術分析、也不相信&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;squares-gann.com&#x2F;&quot;&gt;甘氏矩陣圖&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;什麼叫低點高點？只有試圖預測股價的人才會有低點高點的看法，在懶人的世界裡，股票就是商品（更棒的是是會生利息的商品）：&lt;&#x2F;p&gt;
&lt;p style=&quot;font-family: monospace; font-size: xx-large; background-color: hsla(0, 0%, 96%, 1.0); padding: 1rem; text-align: center&quot;&gt;
買進價 + 佣金 + 稅金就是成本&lt;br&gt;
賣出價 - 佣金 - 稅金就是收入&lt;br&gt;
買低賣高就只是收入 &gt; 成本的小學生概念
&lt;&#x2F;p&gt;
&lt;p&gt;關於技術分析，對懶人來說也是無用的，懶人只在乎&lt;strong&gt;成本&lt;&#x2F;strong&gt;與&lt;strong&gt;收入&lt;&#x2F;strong&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;試想這個情境，懶人在 &lt;strong&gt;5&#x2F;24&lt;&#x2F;strong&gt; 買入一張 &lt;strong&gt;16&lt;&#x2F;strong&gt; 塊的 00730.TW，又在 &lt;strong&gt;6&#x2F;24&lt;&#x2F;strong&gt; 再買進一張 &lt;strong&gt;18&lt;&#x2F;strong&gt; 塊的 00730.TW，試問：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;請問成本是多少？&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
懶得拿計算機，心算一下是 17.xx 塊，xx 的部份是佣金和稅金&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;請問 5&#x2F;25 ~ 6&#x2F;23 這段期間的股價、日線、週線、月線、季線、半年線、年線、XXX 線和我的成本有什麼關係？&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
答案是沒有關係，因為我們在這期間沒有參與市場。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;請問多少錢以上可以賣出？&lt;&#x2F;strong&gt;&lt;br &#x2F;&gt;
答案是 17.xx 以上即可賣出，當然實際上會希望有合理的報酬，至少要比定存高吧，不然我去放定存就好啦。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;因為不預測股價，所以才可以懶得很輕鬆。&lt;&#x2F;p&gt;
&lt;iframe src=&quot;https:&#x2F;&#x2F;www.youtube.com&#x2F;embed&#x2F;V3l4L9s37OU&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
&lt;h3 id=&quot;bu-yao-xuan-gu&quot;&gt;不要選股&lt;&#x2F;h3&gt;
&lt;p&gt;因為懶，所以不選股，ETF 是吾等懶人的救星。&lt;&#x2F;p&gt;
&lt;p&gt;當然 ETF 也是要花點心思挑一下的，費用低廉、追蹤誤差小、不追蹤衍生性標的、不玩槓桿等，了解這些 ETF 的特性背後的原理大概是買 ETF 最難以避免的功課了，可是相較前面省去的一大堆時間，篩選優質的 ETF 還是省力許多。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;gooaye.com&#x2F;&quot;&gt;股癌&lt;&#x2F;a&gt;常常說「不要跟股票談戀愛」，他的原意是不要死抱著一支股票不放，但我覺得也滿適合拿來倡導分散投資的重要性，在股票市場，我們用 ETF 這樣的商品來分散投資，再搭配政府債券等其它更牛皮的投資商品來達到資產配置的目的，有了適當資產配置，就能夠有效的減緩投資資產被大環境影響的劇烈波動，就像車子的避震器一樣，關於資產配置的原理與功效，建議去讀&lt;a rel=&quot;external&quot; href=&quot;http:&#x2F;&#x2F;greenhornfinancefootnote.blogspot.com&#x2F;search&#x2F;label&#x2F;%E8%B3%87%E7%94%A2%E9%85%8D%E7%BD%AE?max-results=3&quot;&gt;綠角的一系列文章&lt;&#x2F;a&gt;，絕對比看電視投顧老師有意義。&lt;&#x2F;p&gt;
&lt;p&gt;回到文章最開頭說的一天只看一天盤，如何做到的？只要不預測股價、不選股，就可以做到。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;na-yong-bu-dao-de-qian-tou-zi&quot;&gt;拿用不到的錢投資&lt;&#x2F;h3&gt;
&lt;p&gt;前面提到虧錢賣的因果關係，這邊一樣用商品的概念帶入，商品為什麼要虧錢賣？因為要拼現金，不論這這筆現金之後要被怎麼運用，總之就是需要這筆現金去做某些事，需要現金做 XXX 就是虧錢賣背後真正的成因，如果我們不想在未來虧錢賣，那麼就盡可能讓虧錢賣的成因消失，也就是不要讓自己陷入賣股籌錢的窘境中，維持正向的現金流，並盡可能讓收入在扣除花費和短期活用資金之餘還能夠累積資本，用這些累積的資本開始懶人投資。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jie-yu&quot;&gt;結語&lt;&#x2F;h2&gt;
&lt;p&gt;懶人投資，其實懶的很穩健，雖然每當看到技術分析／追高殺低的發大財誘惑時心中難免動搖，此時務必切記那投資經典圖：&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;investing&#x2F;53.gif&quot; alt=&quot;投資經典&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：網路&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;再次複頌懶人投資要點：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;建立正確的投資觀念，不要把投資和賭博混為一談。&lt;&#x2F;li&gt;
&lt;li&gt;買低賣高，以高於成本的價錢賣出。&lt;&#x2F;li&gt;
&lt;li&gt;不預測股價，想想投資經典圖。&lt;&#x2F;li&gt;
&lt;li&gt;不選股，用 ETF 分散投資搭配其它投資商品做資產配置。&lt;&#x2F;li&gt;
&lt;li&gt;拿閒錢投資，不要讓自己陷入被迫虧錢賣的窘境。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;投資時謹記 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;KISS%E5%8E%9F%E5%88%99&quot;&gt;KISS 原則&lt;&#x2F;a&gt;，不要把單純的事情變複雜，變複雜沒有比較厲害。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>什麼是「玩具本票」</title>
        <published>2020-05-24T00:00:00+00:00</published>
        <updated>2020-05-24T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2020/check/"/>
        <id>https://editor.leonh.space/2020/check/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2020/check/">&lt;p&gt;身為一個高大上的行動電子商務人士，用支客票喬事情也是很理所當然的，這裡試著用剛剛學到的皮毛知識解釋什麼叫「玩具本票」。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zhi-piao&quot;&gt;支票&lt;&#x2F;h2&gt;
&lt;p&gt;在談玩具本票之前，先講講支票。&lt;&#x2F;p&gt;
&lt;p&gt;一般所說的支票，指的是銀行支票，這種支票上會印有發行銀行的圖案，並且具備防偽技術，樣式不一但格式相同，實際的外觀可以參考「&lt;a rel=&quot;external&quot; href=&quot;http:&#x2F;&#x2F;citehole.blogspot.com&#x2F;2017&#x2F;09&#x2F;2017-1.html&quot;&gt;某一個洞 聚寶洞&lt;&#x2F;a&gt;」的照片。&lt;&#x2F;p&gt;
&lt;p&gt;一般公司想開立銀行支票，必須向銀行申請，銀行會根據公司的財力與信用狀況審核，取得支票後就可以開立支票作為付款之用，當然在銀行帳戶內也要放足夠的存款才能讓支票兌現，否則會造成跳票，最嚴重可能會變成消波塊。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ben-piao&quot;&gt;本票&lt;&#x2F;h2&gt;
&lt;p&gt;和由銀行發行的支票不同，本票是由公司自行畫押開立，而因為一般都是去文具店買整本套印好的空白本票，所以被戲稱為玩具本票。而本票的兌現則是由開立方兌現，與銀行無關，且本票一旦畫押開立之後也是有法律效力的，一樣惡意跳票的話最嚴重也有可能變消波塊。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;can-kao-zi-liao&quot;&gt;參考資料&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;blog.xuite.net&#x2F;chuzu0&#x2F;twblog&#x2F;312140934-%E6%9E%97%E6%8A%80%E5%B8%AB%E8%AB%87%E8%AB%87%E3%80%8E%E4%BD%95%E8%AC%82%E6%94%AF%E7%A5%A8%E3%80%81%E6%9C%AC%E7%A5%A8%E3%80%81%E7%8E%A9%E5%85%B7%E6%9C%AC%E7%A5%A8%E3%80%81%E6%94%AF%E4%BB%98%E5%91%BD%E4%BB%A4%E3%80%81%E7%8E%A9%E5%85%B7%E6%9C%AC%E7%A5%A8%E5%85%8C%E7%8F%BE%E7%AD%89%E7%AD%89%EF%BC%9F%E3%80%8F&quot;&gt;林技師談談『何謂支票、本票、玩具本票、支付命令、玩具本票兌現等等？』&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>KeePass 密碼管理器</title>
        <published>2020-04-18T00:00:00+00:00</published>
        <updated>2020-04-18T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2020/keepass/"/>
        <id>https://editor.leonh.space/2020/keepass/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2020/keepass/">&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;keepass.info&quot;&gt;KeePass&lt;&#x2F;a&gt; 是個自由的、開源的、多平台的、較適合個人用的密碼管理器。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;mi-ma-guan-li-qi&quot;&gt;密碼管理器&lt;&#x2F;h2&gt;
&lt;p&gt;「為什麼要用密碼管理器？」&lt;&#x2F;p&gt;
&lt;p&gt;「和瀏覽器幫我存密碼的功能有什麼不一樣？」&lt;&#x2F;p&gt;
&lt;p&gt;早期最簡單也最不安全，卻也最多人管理密碼的方式，就是隨意貼，現在可能是變成瀏覽器幫我們存的密碼，不論是隨意貼還是瀏覽器存密碼，對需要認真保守密碼的人來說，都不是個好的做法。&lt;&#x2F;p&gt;
&lt;p&gt;隨意貼就不說了，除了隨意貼，另外一種稍微好一點的實體紀錄法是女孩們的小本本，小本本確實有做到保密的功能，但是無法備份，掉了就是永久掉了，除此之外還有抄寫時手誤的可能性，以及使用時無法複製貼上導致的效率低落，怎麼看都不會是個嚴肅的做法。&lt;&#x2F;p&gt;
&lt;p&gt;進階一點的會開檔案存，不論何種格式、有無加密，但都會有紀錄一多就難以管理的問題，必須自己手動做格式規劃與整理，其實這也可以算是一種陽春的、獨立的密碼管理器，對帳密不多的人來說確是個簡單有效的好選擇。&lt;&#x2F;p&gt;
&lt;p&gt;瀏覽器存密碼這塊，自從主流瀏覽器都導入這項功能後，的確解決了大多數人的需求，並且還附帶了可同步的特性，電腦存了手機也可以用，相當便利。這些主流瀏覽器（Safari、Firefox、Chrome、Edge）背後的大公司們看起來也都值得信賴（Google 可能有一點…不過不會賤到拿用戶的密碼&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;%E4%B8%8D%E4%BD%9C%E6%83%A1&quot;&gt;作惡&lt;&#x2F;a&gt;）。&lt;&#x2F;p&gt;
&lt;p&gt;不過在某些場景下，瀏覽器存密碼也是無法滿足的，最典型的就是台灣的網銀：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;keepass&#x2F;banks.png&quot; alt=&quot;網路銀行&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;台灣的網銀登入時往往需要四個欄位：身份證字號、帳號、密碼、驗證碼，而帳號與密碼兩欄在屬性上又都是密碼型態，也就是網銀的帳號、密碼這兩者實質上都是密碼，這種形式的登入不論是哪個瀏覽器都無法處理的很好，很容易造成瀏覽器誤判，最常見的情況就是記錯欄位或是記不住帳號／密碼兩者其一。最後，瀏覽器的記密碼功能還是過於單純，一律是帳號＋密碼的組合，像網銀這種有兩個密碼欄位的登入頁，瀏覽器即便沒記錯，也必定會少記另外一個。&lt;&#x2F;p&gt;
&lt;p&gt;還有另一種狀況，在瀏覽器以外的場景，即瀏覽器使不上力的地方，也是非常需要一個獨立的密碼管理器的，包括一些應用軟體的帳密、遊戲的帳密、遠端登入的帳密、金鑰型的認證等等，隨著這些數位應用越來越多的佔據我們的生活，一個人所擁有的帳密只會越來越多，另外在避免共用帳密的安全顧慮之下，一個獨立的密碼管理器確實是有必要的。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;keepass-an-quan-ma&quot;&gt;KeePass 安全嗎？&lt;&#x2F;h2&gt;
&lt;p&gt;接下來要問的就是「安全嗎？」&lt;&#x2F;p&gt;
&lt;p&gt;我們可以拆三個方面檢視 KeePass 的安全性，加密算法的安全性、開源的安全性、KeePass app 的安全性。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;jia-mi-suan-fa-de-an-quan-xing&quot;&gt;加密算法的安全性&lt;&#x2F;h3&gt;
&lt;p&gt;我們在 KeePass app 內建立的每筆密碼都會彙整存在一個 kdbx 格式的檔案內，就好像 Excel 的每筆紀錄都會存在一個 xlsx 檔案內一樣。這個 kdbx 當然是加密的，根據 KeePass 的描述，它們會用 AES-256 或 ChaCha20 或 Twofish 三種加密算法對 kdbx 檔案進行加密，這三種算法也是目前業界公認最安全的算法。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;kai-yuan-de-an-quan-xing&quot;&gt;開源的安全性&lt;&#x2F;h3&gt;
&lt;p&gt;開源並不等於絕對的安全，但是自 KeePass 從 2004 年至今十六年的開源發展，已經經過相當長時間的考驗，另外 KeePass 開發團隊的每一行程式都詳實紀錄在版控系統內，可供查驗。&lt;&#x2F;p&gt;
&lt;p&gt;另外可以看看 CVE 的紀錄，只有四個漏洞，而且早已修復。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;keepass-app-de-an-quan-xing&quot;&gt;KeePass app 的安全性&lt;&#x2F;h3&gt;
&lt;p&gt;前面提過，密碼會被存到 kdbx 的檔案內，並且原生的 KeePass 並不具備雲端同步的機制（只有 FTP 的同步），即便 KeePass 有任何的漏洞，只要別人無法拿到 kdbx 檔案，就無法利用漏洞打開加密的 kdbx。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;keepass.info&#x2F;links.html#kp&quot;&gt;KeePass 也有拿到一些國家證書&lt;&#x2F;a&gt;證明自己的安全性。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;an-zhuang-yu-shi-yong-keepass&quot;&gt;安裝與使用 KeePass&lt;&#x2F;h2&gt;
&lt;p&gt;到 KeePass 網站下載安裝後，我們來快轉到使用的部份。&lt;&#x2F;p&gt;
&lt;p&gt;起手式當然是先開新檔案，幫新的 kdbx 找一個安全的目錄並取一個好記的檔名存起來，然後設定 kdbx 檔案的密碼：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;keepass&#x2F;2020-04-12-00-07-36-e79a84e89ea2e5b995e693b7e59c96.png&quot; alt=&quot;KeePass&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;除了密碼外，在下面的「Key file &#x2F; provider」還可以指定一個鑰匙檔作為解密之用，密碼和鑰匙檔可以擇一使用或兩者皆用，兩者皆用的話這個 kdbx 檔案被開啟時就必須輸入密碼和鑰匙檔才可完整解密。&lt;&#x2F;p&gt;
&lt;p&gt;設定完 kdbx 的密碼後，下一頁的一些屬性設定可以直接 OK 跳過，然後會到主視窗，點擊某筆密碼或新增一筆密碼會跳出如下的對話框：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;keepass&#x2F;2020-04-12-00-20-02-e79a84e89ea2e5b995e693b7e59c96.png&quot; alt=&quot;KeePass&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;介面實在是滿簡單的，標題、用戶名稱、密碼、密碼強度提示、網址、備註等幾個欄位，有需要的填寫後即可存檔。在對話框的 Advanced 分頁可以在這筆紀錄內夾附檔，譬如說一些金鑰檔就可以夾帶在這裡。&lt;&#x2F;p&gt;
&lt;p&gt;逐筆把密碼記錄起來後記得要把這個 kdbx 檔案存檔，個人建議是異動一筆就存檔一次。另外未避免記錄時手誤，建議在每筆紀錄確認前先到那筆的網頁去驗證一次。&lt;&#x2F;p&gt;
&lt;p&gt;以上是使用介紹，因為真的滿簡單的，就不多做說明了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;keepass-bu-shi-yong-de-qing-jing&quot;&gt;KeePass 不適用的情境&lt;&#x2F;h2&gt;
&lt;p&gt;在本文的第一句話說到 KeePass 較適合個人使用，因為相較於組織型的密碼管理系統，KeePass 少了一些功能，可能對於組織的密碼管理幫不上忙，簡單列舉一些：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;不能做分層分派，只要能打開 kdbx 的人員皆可看到全部的密碼，無法做分層分派，而往往組織用的密碼管理都是需要分層分派的。&lt;&#x2F;li&gt;
&lt;li&gt;不支援開啟人身份驗證，不論是 KeePass 現有的密碼或鑰匙檔都是屬於對密碼檔的解密驗證，欠缺了對開啟人的身份驗證，這部份往往需要結合作業系統的身份驗證機制，而 KeePass 是沒有這部份的功能的。&lt;&#x2F;li&gt;
&lt;li&gt;不支援更複雜的解密驗證，目前只有密碼與鑰匙檔兩種解密驗證，不支援晶片卡或硬體鑰匙做為解密驗證。&lt;&#x2F;li&gt;
&lt;li&gt;以本機檔案儲存 kdbx 密碼檔，對組織型密碼系統來說，可能需要非本機端的存取架構，也就是每次人員解鎖都是對中央伺服器做驗證，驗證後也都是從中央伺服器下載密碼到記憶體內，而不會在本機儲存實體檔案。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;因為以上這些特性的缺乏所以還是比較建議拿 KeePass 來做個人用途較妥。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;keepass-de-kua-ping-tai-xiong-di&quot;&gt;KeePass 的跨平台兄弟&lt;&#x2F;h2&gt;
&lt;p&gt;雖然 KeePass 本身是 Windows app，不過受益於開源的特性，在各個主流平台上都可以見到 KeePass 的兄弟 app 的身影，並且隨著 .NET 也逐漸走向開源與跨平台的路線，KeePass 本身也具備跨平台使用的能力，在 KeePass 的下載頁可以看到 KeePass 與其它各個平台的兄弟 app，之所以稱為兄弟 app 是因為那些其它平台的 app 都不是 KeePass 的移植，而是參考 KeePass 與 kdbx 的規格而獨立撰寫的 app，其中各有優勢，唯一可以不用擔心的是我們的 kdbx 都可以在我們用的電腦或手機中使用，不用擔心被一個 app 綁住或是換平台要重建資料的困擾。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Cockpit 高大上的 Linux Web 管理介面</title>
        <published>2020-04-11T00:00:00+00:00</published>
        <updated>2020-04-11T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2020/cockpit-web-server-manager/"/>
        <id>https://editor.leonh.space/2020/cockpit-web-server-manager/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2020/cockpit-web-server-manager/">&lt;h2 id=&quot;jian-jie&quot;&gt;簡介&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;cockpit-project.org&#x2F;&quot;&gt;Cockpit&lt;&#x2F;a&gt; 是一個 Red Hat 資助的開源軟體，作為一個 Linux 的 web 管理介面，讓我們可以用瀏覽器就能管理系統事務而無須動用男子漢的文字指令。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;gong-neng&quot;&gt;功能&lt;&#x2F;h2&gt;
&lt;p&gt;條列式 Cockpit 的功能與特色：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;有美美的圖表即時顯示系統負載&lt;&#x2F;li&gt;
&lt;li&gt;顯示一些基本的軟硬體資訊&lt;&#x2F;li&gt;
&lt;li&gt;關機、重開機&lt;&#x2F;li&gt;
&lt;li&gt;顯示系統日誌&lt;&#x2F;li&gt;
&lt;li&gt;管理儲存空間&lt;&#x2F;li&gt;
&lt;li&gt;管理網路介面&lt;&#x2F;li&gt;
&lt;li&gt;管理帳戶&lt;&#x2F;li&gt;
&lt;li&gt;管理服務，不用碰 &lt;code&gt;systemctl&lt;&#x2F;code&gt;&lt;&#x2F;li&gt;
&lt;li&gt;內建終端機，Cockpit web 做不到的事可以在終端機做&lt;&#x2F;li&gt;
&lt;li&gt;模組化設計，可以安裝或自行開發擴充模組&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;an-zhuang&quot;&gt;安裝&lt;&#x2F;h2&gt;
&lt;p&gt;以 Elementary OS &#x2F; Ubuntu 來說，在出廠的 APT 庫已經內建了 Cockpit，所以只要一行即可裝起來：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;sudo&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; apt install cockpit&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其它的 Linux 可以參閱 Cockpit 的安裝文件。&lt;&#x2F;p&gt;
&lt;p&gt;安裝之後 Cockpit 會以服務的形式在背景運作，服務的名稱就叫作 Cockpit Web Service。因為是服務，當然也會常態的佔據一部分的記憶體，有必要的話可以改成平時關閉，要用時再用 &lt;code&gt;systemctl&lt;&#x2F;code&gt; 把 Cockpit Web Service 叫起來。（不過我就是不想用 &lt;code&gt;systemctl&lt;&#x2F;code&gt; 來管服務才裝 Cockpit 的啊…）&lt;&#x2F;p&gt;
&lt;p&gt;另外在 Elementary OS &#x2F; Ubuntu 的 APT 庫內還有一些模組可以安裝，包括 Docker 的管理等等的，可以自行選用。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;shi-yong&quot;&gt;使用&lt;&#x2F;h2&gt;
&lt;p&gt;Cockpit 會在本機的 9090 埠提供服務，所以只要 Cockpit Web Service 有啟動的狀態下，用瀏覽器開 http:&#x2F;&#x2F;localhost:9090&#x2F; 就會進到 Cockpit 的登入頁。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;cockpit-web-server-manager&#x2F;2020-04-05-17-03-30-e79a84e89ea2e5b995e693b7e59c96.png&quot; alt=&quot;Cockpit&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;登入之後的各模組使用應該是一望即知，就不多做介紹了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;an-quan-guan-li&quot;&gt;安全管理&lt;&#x2F;h2&gt;
&lt;p&gt;因為 Cockpit 是 web，所以就需要多考慮存取的控制，要禁止本機以外的機器存取到 Cockpit 的話，請在防火牆設定那邊確定沒有開放 9090 埠。&lt;&#x2F;p&gt;
&lt;p&gt;以 Elementary OS 為例，在 &lt;strong&gt;系統設定值&lt;&#x2F;strong&gt; &lt;strong&gt;安全與隱私&lt;&#x2F;strong&gt; &lt;strong&gt;防火牆&lt;&#x2F;strong&gt; 那邊可以做設定。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;cockpit-web-server-manager&#x2F;2020-04-05-17-20-29-e79a84e89ea2e5b995e693b7e59c96.png&quot; alt=&quot;elemantary OS 防火牆&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;對 9090 埠做限制，禁止外部訪問，或只允許信任的 IP 訪問。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>自己的 VPN 自己建，用 ZeroTier 建構自己的虛擬內網</title>
        <published>2020-04-08T00:00:00+00:00</published>
        <updated>2020-04-08T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2020/zerotier-vpn/"/>
        <id>https://editor.leonh.space/2020/zerotier-vpn/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2020/zerotier-vpn/">&lt;p&gt;目前全球遭受新冠病毒的威脅，許多企業紛紛開始採用遠端工作模式減少人際接觸，然而對一些小型企業來說，遠端工作難以施行的原因可能是因為它們的 IT 環境尚未建置 VPN，對這樣的需求來說，我們可以利用 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.zerotier.com&#x2F;&quot;&gt;ZeroTier&lt;&#x2F;a&gt; 來建構自己的 VPN，ZeroTier 不需要太多的網路技術知識，對一般人而言也可以輕鬆架好架滿。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xia-zai-yu-an-zhuang&quot;&gt;下載與安裝&lt;&#x2F;h2&gt;
&lt;p&gt;ZeroTier 安裝在本地的客戶端稱為 ZeroTier One，ZeroTier One 支援多個平台，包括 macOS、Linux、Windows、iOS、Android 等等，以 Debian 系的 Linux（elementary OS、Debian、Ubuntu）來說，可以透過執行下面這行指令自動幫我們設好 APT 庫與金鑰：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;curl&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -s&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; https:&#x2F;&#x2F;install.zerotier.com&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; |&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; sudo&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; bash&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;其它作業系統的安裝方式可以參照 ZeroTier 網站。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zerotier-network&quot;&gt;ZeroTier Network&lt;&#x2F;h2&gt;
&lt;p&gt;ZeroTier Network 就是虛擬區網，用戶可以自行建立多個 ZeroTier Network 供不同的目的使用（公司網、親友網、老司機網等等），必須先建立至少一組的 ZeroTier Network 後再為每台裝置（電腦、手機）加入那個 Network。&lt;&#x2F;p&gt;
&lt;p&gt;ZeroTier Network 的建立與管理是透過 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;my.zerotier.com&#x2F;&quot;&gt;ZeroTier Central&lt;&#x2F;a&gt; 這個網站來操作，註冊的過程我們快速跳過，快轉進入到 ZeroTier Central 的 Networks 頁面：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;zerotier-vpn&#x2F;2020-04-04-15-30-55-e79a84e89ea2e5b995e693b7e59c96.png&quot; alt=&quot;ZeroTier Central&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;按左上角的「Create a Network」會幫我們建立一組 ZeroTier Network，每個 Network 由十六碼的 ID 與名稱組成，ID 是 Network 的獨立識別碼，建立後就不可被更改，ID 也是之後我們要在本機端加入 Network 的依據，名稱則只是便於在 ZeroTier Central 操作時識別而已，可隨意命名。&lt;&#x2F;p&gt;
&lt;p&gt;進入剛剛建立的那個 Network，裡面看起來很複雜，可是其實以簡單的用戶端能互聯這樣的需求來看，要設定的地方不多。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;zerotier-vpn&#x2F;2020-04-04-15-47-18-e79a84e89ea2e5b995e693b7e59c96.png&quot; alt=&quot;ZeroTier Central&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h3 id=&quot;settings&quot;&gt;Settings&lt;&#x2F;h3&gt;
&lt;p&gt;Network 的基礎設定。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Network ID：Network 獨立識別碼，用於之後加入該 Network 所使用。&lt;&#x2F;li&gt;
&lt;li&gt;Name：Network 名稱，可自行取名。&lt;&#x2F;li&gt;
&lt;li&gt;Access Control：請選 PRIVATE，確保每個加入的裝置都是你認得的。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;下面 Advanced 那邊是一些 IP 的技術參數，可以不用動。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;members&quot;&gt;Members&lt;&#x2F;h3&gt;
&lt;p&gt;加入本 Network 的裝置，目前應該是空白，到後面在本機端加入這個 Network 後就會有裝置清單與簡單的管理介面出現。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;flow-rules&quot;&gt;Flow Rules&lt;&#x2F;h3&gt;
&lt;p&gt;更細節的流量規則，一樣不用動。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;sharing&quot;&gt;Sharing&lt;&#x2F;h3&gt;
&lt;p&gt;把本 Network 的管理頁面分享給其它 ZeroTier 帳號。&lt;&#x2F;p&gt;
&lt;p&gt;最單純的情境下，四大區塊其實什麼也不用動。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ba-zhuang-zhi-jia-ru-network&quot;&gt;把裝置加入 Network&lt;&#x2F;h2&gt;
&lt;p&gt;前面我們已經建立了一組 ZeroTier Network，要把一部已經裝好 ZeroTier One 的 Linux 加入那個 Network，首先把 Network ID 記下來，然後搭配 ZeroTier One 的命令工具 &lt;code&gt;zerotier-cli&lt;&#x2F;code&gt; 使用：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;sudo&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; zerotier-cli join&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; ################&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;後面的井字號用 Network ID 填上。&lt;&#x2F;p&gt;
&lt;p&gt;因為前面在 ZeroTire Central 的 Network 的設定我們用了 private 的存取控制，所以每當有新裝置加入這個 Network 時，我們必須再回到 Network 的管理介面去授權這台新裝置加入我們的 Network。&lt;&#x2F;p&gt;
&lt;p&gt;回到 ZeroTier Central 的 Network 管理介面，在 Members 區段下應該可以發現有剛加入的新裝置，在 Auth? 位置打勾對它進行授權即可。另外也可以幫這個裝置命名方便之後的識別。&lt;&#x2F;p&gt;
&lt;p&gt;只要重複上面的步驟把每台裝置都加入 Network 後，就可以透過 ZeroTier 幫我們建構的虛擬區網互連了。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jin-jie-ying-yong&quot;&gt;進階應用&lt;&#x2F;h2&gt;
&lt;p&gt;橋接網路，可以透過在公司的那台電腦當作跳板存取公司內網的網站，或是身陷中國 GFW 內也可以透過家裡的電腦當跳板存取自由的網路。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zerotier-an-quan-ma&quot;&gt;ZeroTier 安全嗎？&lt;&#x2F;h2&gt;
&lt;p&gt;ZeroTier 的安全機制是建立在端對端的加密連線上，在 A、B 兩台電腦間是直接連線的，並不透過 ZeroTier 作為中介，並且這兩端的連線是加密的，僅有彼此能為訊息解密，而端點間的加密與認證機制也是可以信賴的。在某些情況下當兩端無法直接連線時，可能會透過 ZeroTier 的中介機台作為溝通的媒介，但由於前面提到的端對端的加密連線機制，中介機台是無法看到原始的資料的，所以總的來說，ZeroTier 是安全的，不過也還是要保有「世界上沒有絕絕對對的安全」的觀念，有疑慮、有芥蒂、覺得沒花錢就是不安心、不爽快、手會癢，那就不要用。&lt;&#x2F;p&gt;
&lt;p&gt;關於詳細的 ZeroTier 的安全機制，參見&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.zerotier.com&#x2F;zerotier&#x2F;manual#213cryptographyaname2_1_3a&quot;&gt;〈Protocol Design Whitepaper〉的 Cryptography 一節&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>如何在 elementary OS 安裝 Firefox Beta</title>
        <published>2020-03-28T00:00:00+00:00</published>
        <updated>2020-03-28T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2020/how-to-install-firefox-beta-on-elementary-os/"/>
        <id>https://editor.leonh.space/2020/how-to-install-firefox-beta-on-elementary-os/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2020/how-to-install-firefox-beta-on-elementary-os/">&lt;h2 id=&quot;firefox-beta&quot;&gt;Firefox Beta&lt;&#x2F;h2&gt;
&lt;p&gt;Firefox 除了一般的版本之外，還另外有 Beta、Developer、Nightly 版，這些額外的預覽版本是用來供應給網頁開發者等等的早期用戶，這些早期用戶可以透過預覽版的 Firefox 來測試新的網頁技術，當然既然是預覽版那麼它們的穩定性一定是不如一般版的，喜歡嚐鮮或工作上對新的網頁技術有需求的朋友可以斟酌安裝。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;elementary-os&quot;&gt;elementary OS&lt;&#x2F;h2&gt;
&lt;p&gt;elementary OS 是以 Ubuntu Desktop 為基礎，再改用 elementary OS 自己的桌面環境而來，提供了比原本 Ubuntu Desktop 更美觀也更友善的桌面體驗，並且依然保留了 Ubuntu 的系統架構與沿用 Debian 流派的套件管理系統 APT，所以網路上對 Debian &#x2F; Ubuntu 的文章大多也可以適用於 elementary OS。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ppa&quot;&gt;PPA&lt;&#x2F;h2&gt;
&lt;p&gt;前面提到過 elemantary OS 是基於 Ubuntu 的，並且也是用 APT 作為套件管理系統，因此若要安裝普通版的 Firefox，則只要一行簡單的指令：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo apt install firefox&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;因為普通版的 Firefox 有被收錄在 Ubuntu 的套件庫內，而 elementary OS 也是有沿用 Ubuntu 的套件庫，才能安裝的這麼輕鬆寫意。&lt;&#x2F;p&gt;
&lt;p&gt;然而如果想嘗試 Firefox Beta，你會發現好像有難度，在 Firefox Beta 的下載頁雖然有列出 Linux 版的下載包，但是它只提供 .tar.bz2 的壓縮包，雖然解開免安裝就可以用，但並未整合入 elementary OS 的 app 選單內，有點像以前 Windows 上的綠色軟體，但這不是我想要的，本人還是較偏好與 OS 能夠做到整合的做法，因此我們必須手動把 Firefox Beta 的 PPA 套件庫加入系統內。&lt;&#x2F;p&gt;
&lt;p&gt;PPA，即 Personal Package Archive，是讓任何大眾可以寄存自己的 DEB 套件的平台，最大的 PPA 平台 Launchpad 也是由 Ubuntu 背後的公司 Canonical 所營運的。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;launchpad.net&#x2F;~mozillateam&#x2F;+archive&#x2F;ubuntu&#x2F;firefox-next&quot;&gt;Firefox Beta PPA 的專案頁面&lt;&#x2F;a&gt;。裡面的說明有教導我們如何把這個 PPA 套件庫加入我們的 elementary OS 之內，依照頁面的指示加入 PPA 套件庫：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo add-apt-repository ppa:mozillateam&#x2F;firefox-next&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然後重整套件庫清單：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo apt update&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;最後安裝 Firefox Beta：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo apt install firefox&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意到，套件的名稱還是 &lt;code&gt;firefox&lt;&#x2F;code&gt;，也就是說這個 PPA 內的 Firefox Beta 的套件名稱還是 &lt;code&gt;firefox&lt;&#x2F;code&gt;，並且取代了原本 Ubuntu 套件庫的那個普通版的 &lt;code&gt;firefox&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;安裝完之後我們的系統內的 Firefox 應該就會被取代成 Firefox Beta。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Google Chrome 好快啊！</title>
        <published>2020-03-26T00:00:00+00:00</published>
        <updated>2020-03-26T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2020/browsers-speed/"/>
        <id>https://editor.leonh.space/2020/browsers-speed/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2020/browsers-speed/">&lt;!-- Google Chrome 有了新的 Beta 版，之前用的感覺是凡遇到 Flash 就很慢慢慢慢慢，可是現在不會勒！跟飛的一樣。還有個 Chrome Experiments 的網站可以操 Javascript 的效能，好炫呀！ --&gt;
&lt;p&gt;Google Chrome 跑 JS 的速度跟飛的一樣，如果想做更量化的數據測試，那可以上 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;browserbench.org&#x2F;&quot;&gt;BrowserBench.org&lt;&#x2F;a&gt;，裡面有三種測試：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;JetStream2&lt;&#x2F;li&gt;
&lt;li&gt;MotionMark&lt;&#x2F;li&gt;
&lt;li&gt;Speedometer&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;其中的 MotionMark 比較偏向圖形效能的測試，而 Spee&lt;strong&gt;dom&lt;&#x2F;strong&gt;eter 比較偏向 DOM 操作的測試，對現代化的 web app 來說，應該是比較適合用 Speedometer 來跑分。&lt;&#x2F;p&gt;
&lt;p&gt;同場加映 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;experiments.withgoogle.com&#x2F;&quot;&gt;Experiments with Google&lt;&#x2F;a&gt; 裡面的實驗性作品有的相當有創造力，同時又能操爆你的瀏覽器。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Java 編譯成 WebAssembly 的工具</title>
        <published>2020-03-24T00:00:00+00:00</published>
        <updated>2020-03-24T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2020/java-to-webassembly-tools/"/>
        <id>https://editor.leonh.space/2020/java-to-webassembly-tools/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2020/java-to-webassembly-tools/">&lt;p&gt;古早的年代想在網頁內埋 Java 還有 Java applet 可以用，在 Java applet 式微後，找來找去比較可以的辦法大概就是編譯成 WebAssembly 了吧！&lt;&#x2F;p&gt;
&lt;p&gt;想要把 Java 編譯成 WebAssembly，有下面三個工具可以選用：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;http:&#x2F;&#x2F;teavm.org&#x2F;&quot;&gt;TeaVM&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;leaningtech.com&#x2F;pages&#x2F;cheerpj.html&quot;&gt;CheerpJ&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;i-net-software&#x2F;JWebAssembly&quot;&gt;JWebAssembly&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;h2 id=&quot;teavm&quot;&gt;TeaVM&lt;&#x2F;h2&gt;
&lt;p&gt;TeaVM 是個 Java 到 JavaScript 的轉碼工具。TeaVM 接受的來源是 Java bytecode，意即不只是 Java，只要能編譯成 Java bytecode 的語言，包括 Kotlin 等，都可以透過 TeaVM 編譯成 WebAssembly 或轉碼成 JavaScript。&lt;&#x2F;p&gt;
&lt;p&gt;不過 Java 到 WebAssembly 這一部份還只在實驗階段，只適合拿來當玩具，不適合拿來做工具。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;cheerpj&quot;&gt;CheerpJ&lt;&#x2F;h2&gt;
&lt;p&gt;與樓上一樣是 Java bytecode 到 WebAssembe 或 JavaScript 的工具。背後有商業公司支持，因此令人感覺發展的完整度也較高，相對來說，商業用途就必須付費，只有非商業用途免費，付費除了買授權外，同時也會有顧問提供技術支援，果然天下沒有白吃的午餐啊。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jwebassembly&quot;&gt;JWebAssembly&lt;&#x2F;h2&gt;
&lt;p&gt;從名字就可以聯想到 JWebAssembly 只專注於 Java bytecode 到 WebAssembly 的轉換。一樣比較偏玩具性質。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xiao-jie&quot;&gt;小結&lt;&#x2F;h2&gt;
&lt;p&gt;一輪調查完畢，雖然三個工具各有特色，也都各有限制，技術上的限制可能是某一些 Java 的特性很難被完整的轉換，非技術上的限制就是錢錢的問題啦，目前並沒有既免費又傻瓜的轉換器這樣的東西存在，不過還是謝謝佛心的 CheerpJ 有提供非商用的免費版給大家。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Java 語言筆記</title>
        <published>2020-03-23T00:00:00+00:00</published>
        <updated>2020-03-23T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2020/java/"/>
        <id>https://editor.leonh.space/2020/java/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2020/java/">&lt;h2 id=&quot;hui-quan-kong-zhi&quot;&gt;迴圈控制&lt;&#x2F;h2&gt;
&lt;h3 id=&quot;break&quot;&gt;Break&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;break&lt;&#x2F;code&gt; 用來中斷整個迴圈：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;for&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;int&lt;&#x2F;span&gt;&lt;span&gt; i&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 0&lt;&#x2F;span&gt;&lt;span&gt;, i &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 10&lt;&#x2F;span&gt;&lt;span&gt;, i&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;++&lt;&#x2F;span&gt;&lt;span&gt;) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    if&lt;&#x2F;span&gt;&lt;span&gt; (i &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 2&lt;&#x2F;span&gt;&lt;span&gt;) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        break&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; &#x2F;&#x2F; i = 2 就中斷整個迴圈&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; else&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        System.out.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;println&lt;&#x2F;span&gt;&lt;span&gt;(i);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;輸出：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;continue&quot;&gt;Continue&lt;&#x2F;h3&gt;
&lt;p&gt;&lt;code&gt;continue&lt;&#x2F;code&gt; 用來略過某次迴圈：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;for&lt;&#x2F;span&gt;&lt;span&gt; (&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;int&lt;&#x2F;span&gt;&lt;span&gt; i&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 0&lt;&#x2F;span&gt;&lt;span&gt;, i &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;lt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 5&lt;&#x2F;span&gt;&lt;span&gt;, i&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;++&lt;&#x2F;span&gt;&lt;span&gt;) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;    if&lt;&#x2F;span&gt;&lt;span&gt; (i &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 2&lt;&#x2F;span&gt;&lt;span&gt;) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        continue&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; &#x2F;&#x2F; 跳過 i = 2 的迴圈&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; else&lt;&#x2F;span&gt;&lt;span&gt; {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        System.out.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;println&lt;&#x2F;span&gt;&lt;span&gt;(i);&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;輸出：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;0&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;3&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;4&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;label&quot;&gt;Label&lt;&#x2F;h3&gt;
&lt;p&gt;如果是巢狀迴圈的話勒？&lt;&#x2F;p&gt;
&lt;p&gt;Java 的迴圈是可以命名的，稱為 label，可以指定要 &lt;code&gt;break&lt;&#x2F;code&gt; 或 &lt;code&gt;continue&lt;&#x2F;code&gt; 的是哪一層的迴圈：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;labelA&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;: while&lt;&#x2F;span&gt;&lt;span&gt; (i &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 0&lt;&#x2F;span&gt;&lt;span&gt;) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    labelB&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;: while&lt;&#x2F;span&gt;&lt;span&gt; (j &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 0&lt;&#x2F;span&gt;&lt;span&gt;) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;        if&lt;&#x2F;span&gt;&lt;span&gt; (j &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 2&lt;&#x2F;span&gt;&lt;span&gt;) {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;            break&lt;&#x2F;span&gt;&lt;span&gt; labelA;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; &#x2F;&#x2F; 會中斷整個 labelA&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    }&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;帥吧！&lt;&#x2F;p&gt;
&lt;h2 id=&quot;chang-shu&quot;&gt;常數&lt;&#x2F;h2&gt;
&lt;p&gt;很簡單，在宣告前加個 final 關鍵字就是常數，如同多數程式語言，常數的命名習慣上全大寫。&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;java&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;final int&lt;&#x2F;span&gt;&lt;span&gt; MY_ANGEL&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 689&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;常數建立後是不能修改的，編譯時會檢查。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Aurelia 2 從零開始之建構專案</title>
        <published>2020-03-18T00:00:00+00:00</published>
        <updated>2020-03-18T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2020/aurelia/"/>
        <id>https://editor.leonh.space/2020/aurelia/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2020/aurelia/">&lt;p&gt;Aurelia 是一套前端框架，類似於 Svelte、Vue、React，與三天王最大的差異就是知名度低、台灣沒人用、沒有豐富的元件庫可以用，大多只能自幹，幸好它還是處於有人維護的狀況。&lt;&#x2F;p&gt;
&lt;p&gt;Aurelia 的特色是不使用 virtual DOM，而使用 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;developer.mozilla.org&#x2F;zh-CN&#x2F;docs&#x2F;Web&#x2F;Web_Components&#x2F;Using_shadow_DOM&quot;&gt;shadow DOM&lt;&#x2F;a&gt;，以及同樣是 HTML 標準的 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;developer.mozilla.org&#x2F;zh-CN&#x2F;docs&#x2F;Web&#x2F;Web_Components&#x2F;Using_custom_elements&quot;&gt;custom element&lt;&#x2F;a&gt; 來實現網頁元件化。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.aurelia.io&#x2F;&quot;&gt;Aurelia 2&lt;&#x2F;a&gt; 則是 Aurelia 的第二代，目前還在開發階段，只適合拿來當玩具，還不適合拿來做產品。本文所用的版本是 Aurelia v2.0.0-beta.7，主要的參考文件來自 https:&#x2F;&#x2F;docs.aurelia.io&#x2F;。&lt;&#x2F;p&gt;
&lt;p&gt;附註一下 Aurelia 1 與 Aurelia 2 的文件庫各自的位址：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Aurelia 1: https:&#x2F;&#x2F;aurelia.io&#x2F;docs&#x2F;&lt;&#x2F;li&gt;
&lt;li&gt;Aurelia 2: https:&#x2F;&#x2F;docs.aurelia.io&#x2F;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;我用的環境是 Elementary OS 7 Hera，相當於 Ubuntu 22.04 LTS。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;qian-zhi-zuo-ye&quot;&gt;前置作業&lt;&#x2F;h2&gt;
&lt;p&gt;這部份講 Git、Node.js、Visual Studio Code 等，都有的可跳過。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;an-zhuang-git&quot;&gt;安裝 Git&lt;&#x2F;h3&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo apt install git&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;an-zhuang-node-js-yu-npm&quot;&gt;安裝 Node.js 與 NPM&lt;&#x2F;h3&gt;
&lt;p&gt;一般來說只要安裝 Node.js 也會一併安裝 NPM。Node.js 安裝的管道包括下列幾種：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;OS 預帶的軟體庫內的版本&lt;&#x2F;li&gt;
&lt;li&gt;Node.js 提供的 deb 軟體庫&lt;&#x2F;li&gt;
&lt;li&gt;Node.js 提供的 snap 軟體庫&lt;&#x2F;li&gt;
&lt;li&gt;透過 nvm 安裝的 Node.js&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;OS 預帶的軟體庫過舊直接排除，其它三個都是可行的選項，對初學者我來說是用 Node.js 提供的 deb 軟體庫來做安裝，具體的操作細節可以參考這份文件：&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;nodesource&#x2F;distributions&quot;&gt;NodeSource Node.js Binary Distributions&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;在這裡裝的版本是 Node.js 18 LTS。&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# Using Ubuntu&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; curl&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -fsSL&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; https:&#x2F;&#x2F;deb.nodesource.com&#x2F;setup_18.x&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; |&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; sudo&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -E&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; bash -&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo apt-get install&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -y&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; nodejs&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;an-zhuang-visual-studio-code&quot;&gt;安裝 Visual Studio Code&lt;&#x2F;h3&gt;
&lt;p&gt;去 https:&#x2F;&#x2F;code.visualstudio.com&#x2F; 抓 deb 包回來裝。&lt;&#x2F;p&gt;
&lt;p&gt;最前面提到 Aurelia 的特色，社群小，加上 Aurelia 2 又是在早期開發階段，因此 VSC 是沒有相關的延伸模組可以使用的，請自幹吧。雖然如此，Git、JavaScript、TypeScript、HTML、CSS 相關的延伸模組還是很豐富，可以善加利用。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jian-gou-aurelia-2-zhuan-an&quot;&gt;建構 Aurelia 2 專案&lt;&#x2F;h2&gt;
&lt;p&gt;下面這行起手式指令會以問答的方式逐步帶領我們建立專案目錄與檔案：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; npx makes aurelia&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這行指令會下載 Aurelia 套件與相關的依賴套件，然後執行 Aurelia 套件內預先定義好的問答程序，這系列問答決定了要專案的雛型與 scaffold（有人翻譯成腳手架）要如何搭建，下面是本人的選擇：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;         #&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;      ######   xxx&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     ########  xxxx   ####         _                  _ _         ____&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   x   ########    ########       &#x2F; \  _   _ _ __ ___| (_) __ _  |___ \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     x  ######  #############    &#x2F; _ \| | | | &amp;#39;__&#x2F; _ \ | |&#x2F; _` |   __) |&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  xxxxx  ##  ###############    &#x2F; ___ \ |_| | | |  __&#x2F; | | (_| |  &#x2F; __&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;   xxxxx  ###############      &#x2F;_&#x2F;   \_\__,_|_|  \___|_|_|\__,_| |_____|&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    x ###############  xxx&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    ##############  #   xx&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; ##############  ######          https:&#x2F;&#x2F;aurelia.io&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  ########## x  ########         https:&#x2F;&#x2F;github.com&#x2F;aurelia&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    #####  xxxx   ########       https:&#x2F;&#x2F;twitter.com&#x2F;aureliaeffect&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;     #   x  x      ######&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;                     #&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✔ Please name this new project: › my-app&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✔ Would you like to use the default setup or customize your choices? › Custom Aurelia 2 Project&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✔ What kind of Aurelia 2 project? › Application&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✔ What Aurelia 2 release would you like to use? › Latest&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✔ Which bundler would you like to use? › Dumber&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✔ What transpiler would you like to use? › TypeScript&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✔ Do you want to use Shadow DOM or CSS Module? › Use Shadow DOM&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✔ What CSS preprocessor to use? › Sass (.scss)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✔ What unit testing framework to use? › Jest&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✔ Do you want to setup e2e test? › Yes (Playwright)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✔ What kind of sample code do you want in this project? › With direct routing&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[makes] Project my-app has been created.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;✔ Do you want to install npm dependencies now? › Yes, use npm&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;問答程序結束後，應該會多出一個 my-app 資料夾。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zhi-xing-zhuan-an&quot;&gt;執行專案&lt;&#x2F;h2&gt;
&lt;p&gt;雖然目前是空的專案，但前面的設定程序有幫我們做了一個簡單的 Hello World! 頁面，把專案跑起來看看：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; npm start&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;應該會輸出這些訊息：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;gt; my-app@0.1.0 start&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;gt; gulp&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[dumber] Starting dumber bundler v2.1.1 https:&#x2F;&#x2F;dumber.js.org&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[16:32:42] Using gulpfile ~&#x2F;Projects&#x2F;my-app&#x2F;gulpfile.js&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[16:32:42] Starting &amp;#39;default&amp;#39;...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[16:32:42] Starting &amp;#39;clean&amp;#39;...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[16:32:42] Finished &amp;#39;clean&amp;#39; after 37 ms&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[16:32:42] Starting &amp;#39;build&amp;#39;...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[dumber] 2.0.0-beta.7 @aurelia&#x2F;router&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[dumber] 2.0.0-beta.7 @aurelia&#x2F;runtime-html&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[dumber] 2.0.0-beta.7 aurelia&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[dumber] 2.5.3      tslib&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[dumber] 2.0.0-beta.7 @aurelia&#x2F;fetch-client&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[dumber] 2.0.0-beta.7 @aurelia&#x2F;kernel&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[dumber] 2.0.0-beta.7 @aurelia&#x2F;metadata&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[dumber] 2.0.0-beta.7 @aurelia&#x2F;platform&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[dumber] 2.0.0-beta.7 @aurelia&#x2F;platform-browser&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[dumber] 2.0.0-beta.7 @aurelia&#x2F;route-recognizer&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[dumber] 2.0.0-beta.7 @aurelia&#x2F;runtime&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Update index.html with entry.bundle.js&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[16:32:52] Write app-bundle.js&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[16:32:52] Write entry.bundle.js&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[16:32:53] Finished &amp;#39;build&amp;#39; after 11 s&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[16:32:53] Starting &amp;#39;startServer&amp;#39;...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;Dev server is started at: http:&#x2F;&#x2F;localhost:9000&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[16:32:53] Finished &amp;#39;startServer&amp;#39; after 11 ms&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[16:32:53] Starting &amp;#39;watch&amp;#39;...&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;由 Gulp 調度工作，Dumber 負責整合打包用到的套件，啟動伺服器並打開瀏覽器開啟 http:&#x2F;&#x2F;localhost:9000&#x2F;，應該會在瀏覽器看到陽春的 Welcome 頁。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2020&#x2F;aurelia&#x2F;welcome.png&quot; alt=&quot;Welcome to Aurelia 2&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;用 Inspector 看一下，確實是有 custom element 與 shadow DOM。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;bian-yi-zhuan-an&quot;&gt;編譯專案&lt;&#x2F;h2&gt;
&lt;p&gt;在 package.json 與 gulpfile.js 裡面同時也幫我們準備好了編譯的指令。&lt;&#x2F;p&gt;
&lt;p&gt;package.json 裡面的 build 會先執行 gulp clean 再執行 gulp build：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;json&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;scripts&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;: {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  &amp;quot;start&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;gulp&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  &amp;quot;build&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;gulp clean &amp;amp;&amp;amp; cross-env NODE_ENV=production gulp build&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  &amp;quot;clear-cache&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;gulp clear-cache&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;gulpfile.js 的 &lt;code&gt;clean()&lt;&#x2F;code&gt; 很簡單，只是把 dist 資料夾砍掉而已；&lt;code&gt;build()&lt;&#x2F;code&gt; 則負責把所有用到的 js &#x2F; html &#x2F; css 檔案都包起來丟給 Dumber 去處理依賴包：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;javascript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;function&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; build&lt;&#x2F;span&gt;&lt;span&gt;() {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  &#x2F;&#x2F; Merge all js&#x2F;css&#x2F;html file streams to feed dumber.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  &#x2F;&#x2F; dumber knows nothing about .ts&#x2F;.less&#x2F;.scss&#x2F;.md files,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  &#x2F;&#x2F; gulp-* plugins transpiled them into js&#x2F;css&#x2F;html before&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;  &#x2F;&#x2F; sending to dumber.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;  return&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; merge2&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    gulp.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;src&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;src&#x2F;**&#x2F;*.json&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;),&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;    buildJs&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;src&#x2F;**&#x2F;*.ts&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;),&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;    buildHtml&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;src&#x2F;**&#x2F;*.html&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;),&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;    buildCss&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;src&#x2F;**&#x2F;*.{scss,css}&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  )&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    &#x2F;&#x2F; Note we did extra call `dr()` here, this is designed to cater watch mode.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    &#x2F;&#x2F; dumber here consumes (swallows) all incoming Vinyl files,&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    &#x2F;&#x2F; then generates new Vinyl files for all output bundle files.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    .&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;pipe&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;dr&lt;&#x2F;span&gt;&lt;span&gt;())&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    &#x2F;&#x2F; Terser fast minify mode&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    &#x2F;&#x2F; https:&#x2F;&#x2F;github.com&#x2F;terser-js&#x2F;terser#terser-fast-minify-mode&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;    &#x2F;&#x2F; It&amp;#39;s a good balance on size and speed to turn off compress.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    .&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;pipe&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;gulpif&lt;&#x2F;span&gt;&lt;span&gt;(isProduction,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; terser&lt;&#x2F;span&gt;&lt;span&gt;({ compress:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; false&lt;&#x2F;span&gt;&lt;span&gt; })))&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    .&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;pipe&lt;&#x2F;span&gt;&lt;span&gt;(gulp.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;dest&lt;&#x2F;span&gt;&lt;span&gt;(dist, { sourcemaps: isProduction&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; ?&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; false&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; :&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;.&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt; }));&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;了解了背後的流程後就來真正的跑一次：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; npm run build&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;跑完後除了 dist 資料夾內有被打包過的 app-bundle.xxx.js 與 entry-bundle.xxx.js 之外，外面的 index.html 也有被改過，內容的 script 指向 dist 內新的 JavaScript 檔案。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;至此門外漢的 Aurelia 專案建構全紀錄結束，下一篇來寫簡單的元件。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Lorem Picsum 佔位圖產生器</title>
        <published>2019-07-22T00:00:00+00:00</published>
        <updated>2019-07-22T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2019/lorem-picsum/"/>
        <id>https://editor.leonh.space/2019/lorem-picsum/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2019/lorem-picsum/">&lt;h2 id=&quot;zhan-wei-fu&quot;&gt;佔位符&lt;&#x2F;h2&gt;
&lt;p&gt;版面已經規劃好，但是內容還沒的時候，需要一些假字來充版面，這些佔位符都是無意義的文字，但也有分段，也有標點符號，用來模擬真正的版面外觀，這些假字就叫佔位符，英文叫 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;wiki&#x2F;Lorem_ipsum&quot;&gt;lorem ipsum&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zhan-wei-tu&quot;&gt;佔位圖&lt;&#x2F;h2&gt;
&lt;p&gt;和佔位符一樣，用於模擬版面的內容，以往都是很苦工的自己畫一些備用，現在有佔位圖產生器來幫我們生出漂亮的佔位圖。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;lorem-picsum&quot;&gt;Lorem Picsum&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;picsum.photos&#x2F;&quot;&gt;Lorem Picsum&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Lorem Picsum 利用簡單的網址呼叫就可以傳回隨機圖片，而且它的圖片都還滿好看的。&lt;&#x2F;p&gt;
&lt;p&gt;產生 700 * 700 px 的正方形圖片：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;https:&#x2F;&#x2F;picsum.photos&#x2F;700&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;picsum.photos&#x2F;700&quot; alt=&quot;Lorem Picsum&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;產生 700 * 350 px 的長方形圖片：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;https:&#x2F;&#x2F;picsum.photos&#x2F;700&#x2F;350&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;picsum.photos&#x2F;700&#x2F;350&quot; alt=&quot;Lorem Picsum&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;h2 id=&quot;duo-tu-pian-chan-sheng&quot;&gt;多圖片產生&lt;&#x2F;h2&gt;
&lt;p&gt;像是照片牆、清單頁這種版型，一次需要生出大量同尺寸的佔位圖，如果用上面的做法，因為瀏覽器快取的機制，網址一樣的圖只要被請求一次就會進入快取區，後面同樣網址的圖就只會從快取區取用，如此整篇網頁都會是同一張圖，如果要避免用到快取，需要在 Lorem Picsum 的網址後面再加上 &lt;code&gt;randoom&lt;&#x2F;code&gt; 參數：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;https:&#x2F;&#x2F;picsum.photos&#x2F;700&#x2F;350?random=1&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;picsum.photos&#x2F;700&#x2F;350?random=1&quot; alt=&quot;Lorem Picsum&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;random&lt;&#x2F;code&gt; 後面的引數只要改變就會回傳不同的圖片：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;https:&#x2F;&#x2F;picsum.photos&#x2F;700&#x2F;350?random=2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;picsum.photos&#x2F;700&#x2F;350?random=2&quot; alt=&quot;Lorem Picsum&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;雖然範例上是寫 &lt;code&gt;random&lt;&#x2F;code&gt;，不過其實問號後面隨便打都是可以的，只要網址不同就不會被瀏覽器視為同一張圖片，就不會用到快取。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ying-yong&quot;&gt;應用&lt;&#x2F;h2&gt;
&lt;p&gt;除了當版型驗證之外，網誌清單頁的配圖難產的時候，或者沒有指定的時候，在前端模板用簡單的 &lt;code&gt;if&lt;&#x2F;code&gt; 判斷式在沒有指定的時候使用 Lorem Picsum 的隨機圖片，是個相當偷懶的做法。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;zong-jie&quot;&gt;總結&lt;&#x2F;h2&gt;
&lt;p&gt;總結一下 Lorem Picsum 的特點：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;可自訂長寬。&lt;&#x2F;li&gt;
&lt;li&gt;直接以網址取用，不用在自己的後端或前端裝任何套件。&lt;&#x2F;li&gt;
&lt;li&gt;支援 HTTPS。&lt;&#x2F;li&gt;
&lt;li&gt;提供模糊與灰階濾鏡。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;有一些特點在上面的範例中沒提到，可以自己去看 Lorem Picsum 網站範例研究一下。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Raspberry Rocks</title>
        <published>2019-04-04T00:00:00+00:00</published>
        <updated>2019-04-04T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2019/raspberry-rocks/"/>
        <id>https://editor.leonh.space/2019/raspberry-rocks/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2019/raspberry-rocks/">&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2019&#x2F;raspberry-rocks&#x2F;tumblr_ppbjt1ISXC1qz7pnt_1280-lg.jpg&quot; alt=&quot;Raspberry Rocks&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>囚</title>
        <published>2019-03-30T00:00:00+00:00</published>
        <updated>2019-03-30T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2019/cage/"/>
        <id>https://editor.leonh.space/2019/cage/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2019/cage/">&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2019&#x2F;cage&#x2F;tumblr_pp3rf8a4Cq1qz7pnt_1280-lg.jpg&quot; alt=&quot;囚&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Street</title>
        <published>2019-03-23T00:00:00+00:00</published>
        <updated>2019-03-23T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2019/street/"/>
        <id>https://editor.leonh.space/2019/street/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2019/street/">&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2019&#x2F;street&#x2F;1-lg.jpg&quot; alt=&quot;Street&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2019&#x2F;street&#x2F;2.jpg&quot; alt=&quot;Street&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>重返投資戰場</title>
        <published>2019-03-07T00:00:00+00:00</published>
        <updated>2019-03-07T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2019/investing/"/>
        <id>https://editor.leonh.space/2019/investing/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2019/investing/">&lt;p&gt;自從 2016 散盡家產買了房子以來，幾乎再也沒放心思在投資理財上，除了我們兄弟間的積金還是維持著大約每季結算一次的例行性工作，身為經理人的我這樣的管理方式算是不及格的吧！自己的投資可以說是完全歸零，不論是在金額上或投注的心力上。&lt;&#x2F;p&gt;
&lt;p&gt;身為一個長期投資人，當然明白越窮越要理財的道理，可是實際上親身遇到買房而投資歸零的情況下，很難再有動力回頭去重零開始配置，一直到這個 228 連假總算下決心撥出時間把荒廢已久的帳本們再打開，首先第一步就是把檔案從 Numbers 遷移到 Google Sheets 並做一些版面上的調整，再把過去三年間的一些帳務資料謄補回表格內，花幾個小時把這些基礎做好感覺是值得的，在遷移重構的過程中會令自己思考所謂被動的長期投資人的核心思想是什麼，以及重新建立起克服短期波動的得失心，而且即便僅是重構加補舊資料，尚未投入一分錢，也還是可以帶來一種踏實的感覺，很神奇，大概是心裡某個被刻意遺忘的角落又重新運作起來的感覺。&lt;&#x2F;p&gt;
&lt;p&gt;補完資料後，再調查一下現在台灣的 ETF 市場，發現一堆高成本、炒短線的牛鬼蛇神 ETF，把這些牛鬼蛇神剃除掉之後，再把標的縮小到台股，再做一些篩選比較後，第一支標的選中 00730 富邦臺灣優質高息，我要的不多，給我每年 6% 年化報酬率就好。&lt;&#x2F;p&gt;
&lt;p&gt;選好首支標的後，只後每月結算一次帳目，並衡量自己的財務能力，適度加碼或再選其它標的買進並持有，再定期閱讀綠角和怪老子的文章就好。回想這些功課，其實都沒有很難，最花時間的是表格遷移重構的過程，但也只是花時間，並無所謂困難，真正困難的是面對並克服自己的惰性，投資這件事，我發懶擺爛了三年，現在我回來了，投資是為了自己的長遠目標，上一關是買房結婚，下一關可能是子女或養老，如果不盡早投資，屆時恐怕會過得更困難。&lt;&#x2F;p&gt;
&lt;p&gt;寫文章之際也在想著，那下一項也被刻意遺忘的項目是什麼呢？健康？學歷？&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>CentOS 7 裝 Adminer</title>
        <published>2019-01-29T00:00:00+00:00</published>
        <updated>2019-01-29T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2019/centos-7-adminer/"/>
        <id>https://editor.leonh.space/2019/centos-7-adminer/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2019/centos-7-adminer/">&lt;p&gt;裝 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.adminer.org&#x2F;&quot;&gt;Adminer&lt;&#x2F;a&gt; 很簡單，它只是一個 php 檔案，抓回來放到 web server 目錄下就好：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; cd &#x2F;usr&#x2F;share&#x2F;nginx&#x2F;html&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; wget https:&#x2F;&#x2F;www.adminer.org&#x2F;latest.php&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; mv latest.php adminer.php&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可是事情果然沒那麼簡單！這樣跑下去它會報錯，找不到 PHP 連接 MariaDB 的庫。&lt;&#x2F;p&gt;
&lt;p&gt;又再次因為我是裝 CentOS Software Collections 的 PHP，所以額外的套件也都要裝 SCL 提供的，把必要的套件裝起來：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo yum install rh-php72-php-mysqlnd&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;還沒完，&#x2F;var&#x2F;opt&#x2F;rh&#x2F;rh-php72&#x2F;lib&#x2F;php&#x2F; 這個地方裡面的子目錄，群組都被設成 apache，可是因為我是用 nginx，要把群組改成 nginx 才可讓 nginx 有能力寫入這些子目錄。&lt;&#x2F;p&gt;
&lt;p&gt;以上做完當然試開一下瀏覽器看看 Adminer 有沒有正常跑起來。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>CentOS 7 裝 mycli</title>
        <published>2019-01-27T00:00:00+00:00</published>
        <updated>2019-01-27T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2019/centos-7-mycli/"/>
        <id>https://editor.leonh.space/2019/centos-7-mycli/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2019/centos-7-mycli/">&lt;h2 id=&quot;pip&quot;&gt;pip&lt;&#x2F;h2&gt;
&lt;p&gt;因為本人的某種對新版的偏執的愛好，就是想用 Python 3，可是卻又找不到 python3-pip 這個包，故有以下解。&lt;&#x2F;p&gt;
&lt;p&gt;裝 Python 3：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo yum install python&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然後參考一下 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;50408941&#x2F;recommended-way-to-install-pip3-on-centos7&#x2F;52518512#52518512&quot;&gt;Recommended way to install pip(3) on centos7&lt;&#x2F;a&gt; 這篇：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;%&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo python36&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -m&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; ensurepip&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;有興趣可以自己去調查一下 &lt;code&gt;ensurepip&lt;&#x2F;code&gt; 的由來。&lt;&#x2F;p&gt;
&lt;p&gt;這樣就有 pip 了！（撒花）&lt;&#x2F;p&gt;
&lt;p&gt;再用 pip 把它自己更新一下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; sudo&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -H&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; pip3 install&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --upgrade&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; pip&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;mycli&quot;&gt;mycli&lt;&#x2F;h2&gt;
&lt;p&gt;mycli 有多種安裝途徑，做為全域工具，比較建議的裝法是透過 pipx。&lt;&#x2F;p&gt;
&lt;p&gt;所以話鋒一轉，我們要先搞定 pipx。&lt;&#x2F;p&gt;
&lt;p&gt;一行安裝：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; python3&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -m&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; pip install&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; --user&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; pipx&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;把 &lt;code&gt;pipx&lt;&#x2F;code&gt; 加到 &lt;code&gt;PATH&lt;&#x2F;code&gt;：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; python3&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -m&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; pipx ensurepath&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;因為改了 &lt;code&gt;PATH&lt;&#x2F;code&gt; 環境變數，要重新登入。&lt;&#x2F;p&gt;
&lt;p&gt;重新登入完應該就可以用了。&lt;&#x2F;p&gt;
&lt;p&gt;用 pipx 裝 mycli：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; pipx install mycli&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;一行就裝完囉！&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; mycli&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -u&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; root&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;可以連入應該就是沒問題。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>CentOS 裝 MariaDB 10</title>
        <published>2019-01-25T00:00:00+00:00</published>
        <updated>2019-01-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2019/centos-7-mariadb/"/>
        <id>https://editor.leonh.space/2019/centos-7-mariadb/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2019/centos-7-mariadb/">&lt;p&gt;一樣因為 CentOS 自帶的 MariaDB 太舊，改用 CentOS Software Collections 提供的 MariaDB 10。&lt;&#x2F;p&gt;
&lt;p&gt;這篇參考〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.digitalocean.com&#x2F;community&#x2F;tutorials&#x2F;how-to-install-configure-lemp-stack-software-collections-centos-7&quot;&gt;How to Install and Configure a LEMP Stack using Software Collections on CentOS 7&lt;&#x2F;a&gt;〉。&lt;&#x2F;p&gt;
&lt;p&gt;先安裝：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; sudo yum install rh-mariadb102&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;跑起來：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; sudo systemctl start rh-mariadb102-mariadb&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;讓這個 SCL 管理的 MariaDB 進入 path 內：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; source scl_source enable rh-mariadb102&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;執行安裝後組態設定，應該要用 root 權限跑，但不知為何跑不了，所以改先開一個 root shell 再跑。&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; sudo bash&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; source &#x2F;opt&#x2F;rh&#x2F;rh-mariadb102&#x2F;enable&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; mysql_secure_installation&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;ol&gt;
&lt;li&gt;被問 root 密碼，在此指 DB 的 root，非 OS 的 root，目前沒有，就 Enter 過去。&lt;&#x2F;li&gt;
&lt;li&gt;設定 DB root 密碼。&lt;&#x2F;li&gt;
&lt;li&gt;關閉匿名帳號登入權限。&lt;&#x2F;li&gt;
&lt;li&gt;讓 root 只能從本機連線。&lt;&#x2F;li&gt;
&lt;li&gt;把 test 資料庫刪掉。&lt;&#x2F;li&gt;
&lt;li&gt;重載入權限表讓以上決定生效。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;設定開機啟動 MariaDB：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; sudo systemctl enable rh-mariadb102-mariadb&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;照 SCL 慣例，設定擋在 &#x2F;etc&#x2F;opt&#x2F;rh&#x2F;rh-mariadb102&#x2F; 內、程式等等都在 &#x2F;opt&#x2F;rh&#x2F;rh-mariadb102&#x2F; 內。&lt;&#x2F;p&gt;
&lt;p&gt;測試一下連線：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; mysql -u root -p&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;沒問題就這樣了。&lt;&#x2F;p&gt;
&lt;p&gt;最後一樣在 &#x2F;etc&#x2F;profile.d&#x2F; 建一支小 shell script 讓開機就自動把 SCL 的 mariadb 加到路徑內，方便執行 mysql：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;# &#x2F;bin&#x2F;bash&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;source&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; scl_source rh-mariadb102&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;後面大概再裝一下 phpMyAdmin 和 mycli 就差不多了。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>CentOS 7 裝 PHP FPM</title>
        <published>2019-01-24T00:00:00+00:00</published>
        <updated>2019-01-24T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2019/centos-7-php-fpm/"/>
        <id>https://editor.leonh.space/2019/centos-7-php-fpm/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2019/centos-7-php-fpm/">&lt;p&gt;因為我們是用 CentOS Software Collection 的 PHP 7，所以 PHP FPM 也要用 SCL 提供的。&lt;&#x2F;p&gt;
&lt;p&gt;主要是參考 〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;blog.gtwang.org&#x2F;linux&#x2F;linode-centos-7-nginx-mysql-mariadb-php-7-installation-notes&#x2F;&quot;&gt;CentOS 7 安裝 Nginx、MySQL &#x2F; MariaDB、PHP7，架設 LEMP 網頁伺服器筆記&lt;&#x2F;a&gt;〉。&lt;&#x2F;p&gt;
&lt;p&gt;先把 PHP FPM 裝起來：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; sudo yum install rh-php72-php-fpm&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;修改 &#x2F;etc&#x2F;opt&#x2F;rh&#x2F;rh-php72&#x2F;php-fpm.d&#x2F;www.conf，把 user、group 設定為 nginx：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;; Unix user&#x2F;group of processes&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;; Note: The user is mandatory. If the group is not set, the default user&amp;#39;s group&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;; will be used.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;; RPM: apache user chosen to provide access to the same directories as httpd&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;;user = apache&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;user = nginx&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;; RPM: Keep a group allowed to write in log dir.&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;;group = apache&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;group = nginx&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;把 PHP FPM 跑起來：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; sudo systemctl start rh-php72-php-fpm&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;再把 PHP FPM 設為開機啟動服務：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; sudo systemctl enable rh-php72-php-fpm&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;下一篇來研究 nginx * PHP FPM 的作法。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>CentOS 7 裝 PHP 7</title>
        <published>2019-01-23T00:00:00+00:00</published>
        <updated>2019-01-23T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2019/centos-7-php/"/>
        <id>https://editor.leonh.space/2019/centos-7-php/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2019/centos-7-php/">&lt;p&gt;參考 CentOS wiki 文章〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;wiki.centos.org&#x2F;HowTos&#x2F;php7&quot;&gt;Using PHP 7.x on CentOS 7.4&lt;&#x2F;a&gt;〉的前半部份。&lt;&#x2F;p&gt;
&lt;p&gt;預設的軟體庫只有 PHP 5，要啟用 CentOS Software Collections 這個庫才會有 PHP 7。&lt;&#x2F;p&gt;
&lt;p&gt;設定 Software Collections 庫：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; sudo yum install centos-release-scl&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然後就可以裝 PHP 7 了：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; sudo yum install rh-php72&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;一些有依賴到的套件也都會一併裝上。&lt;&#x2F;p&gt;
&lt;p&gt;裝上是裝上，是裝到那去呢？看這篇：〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.cyberciti.biz&#x2F;faq&#x2F;howto-list-find-files-in-rpm-package&#x2F;&quot;&gt;What Files Are In a RPM Package?&lt;&#x2F;a&gt;〉。&lt;&#x2F;p&gt;
&lt;p&gt;路徑在 &#x2F;opt&#x2F;rh&#x2F;rh-php72&#x2F; 內。&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.softwarecollections.org&#x2F;en&#x2F;scls&#x2F;rhscl&#x2F;rh-php71&#x2F;&quot;&gt;用 scl 把 PHP 啟用&lt;&#x2F;a&gt;：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; sudo scl enable rh-php72 bash&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這樣會執行一個新的 bash session，並且把那 PHP 7 引入 path 內，可是重開機就失效了…。&lt;&#x2F;p&gt;
&lt;p&gt;再參考一下〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.reddit.com&#x2F;r&#x2F;PHP&#x2F;comments&#x2F;8p1i0l&#x2F;php_70_or_71_on_rhel_or_centos&#x2F;&quot;&gt;PHP 7.0 or 7.1 on RHEL or CentOS&lt;&#x2F;a&gt;〉這篇的作法，在 &#x2F;etc&#x2F;profile.d&#x2F;rh-php72.sh 建一個小 shell script 讓它在登入時自動執行，把那 PHP 7 自動加入 path 內：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;#!&#x2F;bin&#x2F;bash&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;source&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; scl_source enable rh-php72&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;把這支 script 設為可執行以便日後手動使用：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; sudo chmod a+x &#x2F;etc&#x2F;profile.d&#x2F;scl.sh&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;設定 nginx * PHP 見下篇。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>CentOS 7 裝 NGINX</title>
        <published>2019-01-22T00:00:00+00:00</published>
        <updated>2019-01-22T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2019/centos-7-nginx/"/>
        <id>https://editor.leonh.space/2019/centos-7-nginx/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2019/centos-7-nginx/">&lt;p&gt;CentOS 是選用 CentOS 7.4 Minimal with Webmin (HVM)，它已經幫我們設定好一些常用的 CentOS EPEL。 &amp;lt;3&lt;&#x2F;p&gt;
&lt;p&gt;抄一下這篇〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;ivanagyro.medium.com&#x2F;%E6%96%BCcentos7%E5%AE%89%E8%A3%9D-nginx-php7-php-fpm-laravel5-6-df8631681acf&quot;&gt;於CentOS7安裝 Nginx + php7 + php-fpm + Laravel5.6&lt;&#x2F;a&gt;〉。&lt;&#x2F;p&gt;
&lt;p&gt;裝下去：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; sudo yum install nginx&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;設定開機執行 NGINX：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; sudo systemctl enable nginx&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;啟動 NGINX：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; sudo systemctl start nginx&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;&lt;code&gt;systemctl&lt;&#x2F;code&gt; 指令的解說可以讀一下 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;wiki.archlinux.org&#x2F;title&#x2F;Systemd_(%E6%AD%A3%E9%AB%94%E4%B8%AD%E6%96%87)&quot;&gt;Arch Linux 的主題文章&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;以上做完，試開一下應該就可以看到 Ｗelcome to nginx 的頁面了。注意到這個頁面檔案放在 &#x2F;usr&#x2F;share&#x2F;nginx&#x2F;html&#x2F;。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Odoo 安裝</title>
        <published>2019-01-01T00:00:00+00:00</published>
        <updated>2019-01-01T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2019/odoo/"/>
        <id>https://editor.leonh.space/2019/odoo/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2019/odoo/">&lt;p&gt;Odoo 是企業用的商業套件，主要是 ERP，不過除了庫存、帳務控制外，也包了很多很多的模組，也可以自行擴展，又是 open source 的。&lt;&#x2F;p&gt;
&lt;p&gt;利用這段長假花點時間開始研究研究。從安裝開始，安裝主要是參考官方文件 Installing Odoo，採用 DEB 的模式安裝。&lt;&#x2F;p&gt;
&lt;p&gt;Odoo 需要搭配 PostgreSQL 使用，先把 PostgreSQL 裝起來：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; sudo apt install postgresql&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;輸出 PDF 則是靠 wkhtmltopd，也裝起來：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; sudo apt install wkhtmltopdf&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;開始加 Odoo APT repository：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; wget -O - https:&#x2F;&#x2F;nightly.odoo.com&#x2F;odoo.key &lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;|&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; sudo&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; apt-key add -&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; echo &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;deb http:&#x2F;&#x2F;nightly.odoo.com&#x2F;12.0&#x2F;nightly&#x2F;deb&#x2F; .&#x2F;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; &amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; &#x2F;etc&#x2F;apt&#x2F;sources.list.d&#x2F;odoo.list&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; sudo apt update&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; sudo apt install odoo&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;以上跑完，確認一下服務有沒有跑起來：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; sudo service --status-all&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;應該會看到 &lt;code&gt;[ + ] odoo&lt;&#x2F;code&gt; 和 &lt;code&gt;[ + ] postgresql&lt;&#x2F;code&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;註記一下安裝多了或動了哪些檔案或目錄：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&#x2F;etc&#x2F;init.d&#x2F;odoo&lt;&#x2F;li&gt;
&lt;li&gt;&#x2F;etc&#x2F;logrotate.d&#x2F;odoo&lt;&#x2F;li&gt;
&lt;li&gt;&#x2F;etc&#x2F;odoo&lt;&#x2F;li&gt;
&lt;li&gt;&#x2F;lib&#x2F;systemd&#x2F;system&#x2F;odoo.service&lt;&#x2F;li&gt;
&lt;li&gt;&#x2F;usr&#x2F;bin&#x2F;odoo&lt;&#x2F;li&gt;
&lt;li&gt;&#x2F;usr&#x2F;lib&#x2F;python3&#x2F;dist-packages&#x2F;odoo&lt;&#x2F;li&gt;
&lt;li&gt;&#x2F;usr&#x2F;share&#x2F;doc&#x2F;odoo&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;瀏覽器開到 http:&#x2F;&#x2F;localhost:8069 就可以看到初次啟動的設定頁面了，自由發揮填一下，等它跑完就會自動進入主畫面，安裝完畢。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>早晨的工地</title>
        <published>2018-12-25T00:00:00+00:00</published>
        <updated>2018-12-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2018/construction/"/>
        <id>https://editor.leonh.space/2018/construction/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2018/construction/">&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2018&#x2F;construction&#x2F;tumblr_piwlmc72Zg1ukdnjuo1_1280-lg.jpg&quot; alt=&quot;早晨的工地&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>存在意味不明的抓娃娃店</title>
        <published>2018-12-15T00:00:00+00:00</published>
        <updated>2018-12-15T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2018/claw-machines/"/>
        <id>https://editor.leonh.space/2018/claw-machines/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2018/claw-machines/">&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2018&#x2F;claw-machines&#x2F;tumblr_piwlmc72Zg1ukdnjuo1_1280-lg.jpg&quot; alt=&quot;想不到吧！&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>使用序列化在兩個 Rails 站台間傳遞物件</title>
        <published>2018-11-30T00:00:00+00:00</published>
        <updated>2018-11-30T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2018/rails-serialization/"/>
        <id>https://editor.leonh.space/2018/rails-serialization/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2018/rails-serialization/">&lt;p&gt;一般 REST API 傳遞大多是用 HTTP post 發送 JSON、YAML、XML，不過這裡講的不是 client - server 的傳遞，而是 server - server 的傳遞，情境是 production 站與 staging 站間的物件傳遞，雖然說 Rails 有 seed 可以用來建資料，不過有時候開發上還是需要拿一些比較真實的資料來驗證，或者單純因為懶惰。&lt;&#x2F;p&gt;
&lt;p&gt;簡單解釋做法，就是把 production 上的物件序列化成檔案，再從 staging 站台抓下來還原城物件。在 Ruby 的世界，已經內建有 Marshal 模組來做序列化的工作，並且使用簡易。&lt;&#x2F;p&gt;
&lt;p&gt;練習隨機挑一個 book 物件，使用 Marshal 把它序列化後存到 Rails 的 public 位置：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;b&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; Book&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;find&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;Book&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;ids&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;sample&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span&gt;)[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span&gt;])&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;File&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;open&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;Rails&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;public_path&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;join&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;Book&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;id&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;#39;wb+&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; do&lt;&#x2F;span&gt;&lt;span&gt; |f|&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  f.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;write&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;Marshal&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;dump&lt;&#x2F;span&gt;&lt;span&gt;(b))&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;end&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;然後切換到 staging 站台，去 production 站抓檔案，反序列化後存入資料庫：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;f&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; open&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;#39;http:&#x2F;&#x2F;10.100.99.72&#x2F;619204&amp;#39;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;f&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; Marshal&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;load&lt;&#x2F;span&gt;&lt;span&gt;(f)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #FFAB70;&quot;&gt;b&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; Book&lt;&#x2F;span&gt;&lt;span&gt;.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;new&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;b.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;attributes&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; f.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;attributes&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;b.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;save&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;注意，直接跑 &lt;code&gt;f.save&lt;&#x2F;code&gt; 是沒用的，一定要建新的物件再指定 attributes。&lt;&#x2F;p&gt;
&lt;p&gt;可以再驗證一下：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;ruby&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;b.&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;reload&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;b&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;應該就可以看到有正確存入了。&lt;&#x2F;p&gt;
&lt;p&gt;不過 Marshal 也不是萬能的，牽扯到一些 IO 操作的物件或資料庫關聯的物件就會遇到一些問題了。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>摩耶山掬星台</title>
        <published>2018-11-28T00:00:00+00:00</published>
        <updated>2018-11-28T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2018/japan-8/"/>
        <id>https://editor.leonh.space/2018/japan-8/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2018/japan-8/">&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2018&#x2F;japan-8&#x2F;tumblr_inline_picf05qRwy1qz7pnt_1280-lg.jpg&quot; alt=&quot;摩耶山掬星台&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.feel-kobe.jp&#x2F;tw&#x2F;facilities&#x2F;detail&#x2F;?code=0000000139&quot;&gt;掬星台&lt;&#x2F;a&gt;號稱日本三大夜景，和隔壁的六甲山天覽台比起來，的確是可以更清楚的遠矅整個神戶及大阪。&lt;&#x2F;p&gt;
&lt;p&gt;旁邊的纜車站二樓也是一間小餐廳，我們點了一盤炒麵、熱茶、還有我心血來潮的一罐淡啤酒。店裡的旅客來來往往，有些家庭，有些情侶，我們也是其中之一，就靜靜的坐著看看窗外的景色，整理一下手邊的照片，慢慢的把茶喝完，並等待著日落結束。&lt;&#x2F;p&gt;
&lt;iframe src=&quot;https:&#x2F;&#x2F;www.youtube-nocookie.com&#x2F;embed&#x2F;nFzsbLxR7qg&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2018&#x2F;japan-8&#x2F;tumblr_inline_picg1zk9qX1qz7pnt_1280-md.jpg&quot; alt=&quot;摩耶山掬星台&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>六甲山牧場</title>
        <published>2018-11-27T00:00:00+00:00</published>
        <updated>2018-11-27T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2018/japan-7/"/>
        <id>https://editor.leonh.space/2018/japan-7/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2018/japan-7/">&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2018&#x2F;japan-7&#x2F;tumblr_inline_picdcuJSUn1qz7pnt_1280-md.jpg&quot; alt=&quot;六甲山牧場&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;雖然名為&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;rokkosan.jp&#x2F;&quot;&gt;六甲山牧場&lt;&#x2F;a&gt;，但實際地點在隔壁的摩耶山。從六甲山天覽台搭乘六甲摩耶交通巴士，到達時間大約下午三點，待到快五點就要準備前往下一站，掬星台。&lt;&#x2F;p&gt;
&lt;p&gt;因為是牧場，這裡大多是牛馬羊，並且到處都是山羊綿羊在逛大街，類似於神戶動物王國的體驗，但是場地更寬廣，地形也更豐富。兩者相較之下，我們自己是比較喜歡牧場的，抱歉了水豚君。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2018&#x2F;japan-7&#x2F;tumblr_inline_picdr0nVna1qz7pnt_1280-md.jpg&quot; alt=&quot;六甲山牧場&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;牧場也有販售新鮮的牛奶與霜淇淋，這裡的霜淇淋真的很好吃，值得一試，鮮奶則較普通，和一般鮮奶沒什麼差異。&lt;&#x2F;p&gt;
&lt;p&gt;除了看看動物們之外，這裡也頗適合踏青呼吸新鮮空氣，花點腳力爬上不太陡的丘陵就可以矅望整個牧場山景，也是頗具風情。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2018&#x2F;japan-7&#x2F;tumblr_inline_piceemVzFr1qz7pnt_1280-md.jpg&quot; alt=&quot;六甲山牧場&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2018&#x2F;japan-7&#x2F;tumblr_inline_picegp7fsE1qz7pnt_1280-md.jpg&quot; alt=&quot;六甲山牧場&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2018&#x2F;japan-7&#x2F;tumblr_inline_picejndOwP1qz7pnt_1280-md.jpg&quot; alt=&quot;六甲山牧場&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2018&#x2F;japan-7&#x2F;tumblr_inline_picelgLJrY1qz7pnt_1280-lg.jpg&quot; alt=&quot;六甲山牧場&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2018&#x2F;japan-7&#x2F;tumblr_inline_piceomR4j81qz7pnt_1280-md.jpg&quot; alt=&quot;六甲山牧場&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>六甲山</title>
        <published>2018-11-26T00:00:00+00:00</published>
        <updated>2018-11-26T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2018/japan-6/"/>
        <id>https://editor.leonh.space/2018/japan-6/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2018/japan-6/">&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2018&#x2F;japan-6&#x2F;tumblr_inline_pic0bdxNMK1qz7pnt_1280-lg.jpg&quot; alt=&quot;六甲山&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2018&#x2F;japan-6&#x2F;tumblr_inline_pic08eyh5U1qz7pnt_1280-md.jpg&quot; alt=&quot;六甲山&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2018&#x2F;japan-6&#x2F;tumblr_inline_pic09nDngQ1qz7pnt_1280-md.jpg&quot; alt=&quot;六甲山&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;從有馬搭纜車上&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.rokkosan.com&#x2F;top&#x2F;?lang=zh-CHT&quot;&gt;六甲山&lt;&#x2F;a&gt;後，原本的計畫：六甲山 - 紀念碑台轉車 - 摩耶山 - 六甲山牧場 - 掬星台，但紀念碑台轉車的班次查錯時間，中間意外出現兩小時的空檔，於是在紀念碑台看完風景後，臨時決定步行前往&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.rokkosan.com&#x2F;tenrandai&#x2F;?lang=zh-CHT&quot;&gt;六甲山天覽台&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2018&#x2F;japan-6&#x2F;tumblr_inline_pic0bdxNMK1qz7pnt_1280-md.jpg&quot; alt=&quot;六甲山&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2018&#x2F;japan-6&#x2F;tumblr_inline_pic0cshAVW1qz7pnt_1280-md.jpg&quot; alt=&quot;六甲山&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;天覽台這個意外的行程，它的的景色卻給了我不虛此行之感。&lt;&#x2F;p&gt;
&lt;p&gt;白色的三隻動物朋友雕塑令人感到非常療癒，日光、景色、氣候，一切都是那麼的穠纖合度的美好。&lt;&#x2F;p&gt;
&lt;p&gt;感謝老婆大人陪我走這麼一段路。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>有馬溫泉</title>
        <published>2018-11-25T00:00:00+00:00</published>
        <updated>2018-11-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2018/japan-5/"/>
        <id>https://editor.leonh.space/2018/japan-5/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2018/japan-5/">&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2018&#x2F;japan-5&#x2F;tumblr_inline_pibxshjOpF1qz7pnt_1280-lg.jpg&quot; alt=&quot;有馬溫泉&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;為了減少轉車的不確定感，從神戶到&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;visit.arima-onsen.com&#x2F;tw&#x2F;&quot;&gt;有馬&lt;&#x2F;a&gt;的交通，我們選擇搭乘 JR 高速直達巴士，確定好日期後，預先在台灣就可以從 JR 網站買好電子票並列印攜帶，日期當天就在三宮站等巴士即可，三宮的巴士總站也有非常清楚的站牌指示及多語廣播，對旅客相當便利。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2018&#x2F;japan-5&#x2F;tumblr_inline_pibxgpYWbx1qz7pnt_1280-md.jpg&quot; alt=&quot;有馬溫泉&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;有馬溫泉金之湯外面的泡腳池，遇到一位好心的台灣大叔，幫助了還在猶豫的我們，勇敢的泡下去，很燙，非常燙，不過還是謝謝大叔。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2018&#x2F;japan-5&#x2F;tumblr_inline_pibyjrHPnK1qz7pnt_1280-md.jpg&quot; alt=&quot;有馬溫泉&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;有馬溫泉有土產碳酸煎餅，吃起來就如同任何地方的煎餅一般的普通好吃，不用多介紹。要特別講一下不小心吃到的&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.arima-takenaka29.com&#x2F;&quot;&gt;竹中肉店&lt;&#x2F;a&gt;的現烤牛肉，超好吃，在對味道感到震驚之後，才注意到價格也很驚人，不愧是世界聞名的但馬牛。&lt;&#x2F;p&gt;
&lt;p&gt;有馬溫泉之後，我們步行前往纜車站。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>神戶港</title>
        <published>2018-11-24T00:00:00+00:00</published>
        <updated>2018-11-24T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2018/japan-4/"/>
        <id>https://editor.leonh.space/2018/japan-4/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2018/japan-4/">&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2018&#x2F;japan-4&#x2F;tumblr_pibxd41uy71ukdnjuo1_1280-lg.jpg&quot; alt=&quot;神戶港&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>神戶機場</title>
        <published>2018-11-22T00:00:00+00:00</published>
        <updated>2018-11-22T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2018/japan-2/"/>
        <id>https://editor.leonh.space/2018/japan-2/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2018/japan-2/">&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2018&#x2F;japan-2&#x2F;tumblr_pibvvwKoBA1ukdnjuo1_1280-lg.jpg&quot; alt=&quot;神戶機場&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>抵達日本的第一天</title>
        <published>2018-11-21T00:00:00+00:00</published>
        <updated>2018-11-21T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2018/japan-1/"/>
        <id>https://editor.leonh.space/2018/japan-1/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2018/japan-1/">&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2018&#x2F;japan-1&#x2F;tumblr_pibvqcl7al1ukdnjuo1_1280-md.jpg&quot; alt=&quot;船&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;抵達日本的第一天，從關西機場搭船往神戶機場的碼頭，前面那位日本籍的女士也展現出同樣的興奮感。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>日出一刻</title>
        <published>2018-11-20T00:00:00+00:00</published>
        <updated>2018-11-20T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2018/sun-raising/"/>
        <id>https://editor.leonh.space/2018/sun-raising/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2018/sun-raising/">&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2018&#x2F;sun-raising&#x2F;tumblr_pibw4wl27L1ukdnjuo1_1280-lg.jpg&quot; alt=&quot;日出一刻&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;在飛機上享受日出的這一刻&lt;br &#x2F;&gt;
然後繼續昏睡&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>wxPython Phoenix Alpha 發佈</title>
        <published>2017-05-25T00:00:00+00:00</published>
        <updated>2017-05-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2017/wxpython/"/>
        <id>https://editor.leonh.space/2017/wxpython/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2017/wxpython/">&lt;p&gt;〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.reddit.com&#x2F;r&#x2F;Python&#x2F;comments&#x2F;65x8bb&#x2F;wxpython_phoenix_alpha_released&#x2F;&quot;&gt;wxPython Phoenix Alpha Released&lt;&#x2F;a&gt;〉&lt;&#x2F;p&gt;
&lt;p&gt;wxPython 是給 Python 用的一套 GUI 庫，而且是跨平台的唷！以前是 Python 2限定，後來開了一個 Phoenix 專案讓 Python 3 也可以用這套 GUI 開發跨平台桌面應用，安裝也變得很簡單：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;pip&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; install wxPython&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;就這樣一行，沒惹！在網路上能找到的教材大多還是用舊的安裝方式，特別是華文世界，wxPython 的教材少得可憐。:(&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Anaconda 的執行檔的路徑</title>
        <published>2017-05-24T00:00:00+00:00</published>
        <updated>2017-05-24T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2017/anaconda/"/>
        <id>https://editor.leonh.space/2017/anaconda/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2017/anaconda/">&lt;p&gt;Anaconda 一般是安裝在自己的家目錄內，執行檔位置在 ~&#x2F;anaconda&#x2F;bin。&lt;&#x2F;p&gt;
&lt;p&gt;如果有用 &lt;code&gt;pip&lt;&#x2F;code&gt; 裝的其它套件也有執行檔的話也會在這邊。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>ActivePython 的執行檔的路徑</title>
        <published>2017-05-23T00:00:00+00:00</published>
        <updated>2017-05-23T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2017/activepython/"/>
        <id>https://editor.leonh.space/2017/activepython/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2017/activepython/">&lt;p&gt;ActivePython 的 Python3 執行檔在 &#x2F;Library&#x2F;Frameworks&#x2F;Python.framework&#x2F;Versions&#x2F;3.6&#x2F;bin&#x2F;。&lt;&#x2F;p&gt;
&lt;p&gt;同樣的路徑內也可以看到 &lt;code&gt;pip3&lt;&#x2F;code&gt; 等工具，如果有用 &lt;code&gt;pip&lt;&#x2F;code&gt; 安裝甚麼套件的話，套件的執行檔也在這邊。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>找檔軟體</title>
        <published>2017-05-22T00:00:00+00:00</published>
        <updated>2017-05-22T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2017/easyfind/"/>
        <id>https://editor.leonh.space/2017/easyfind/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2017/easyfind/">&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2017&#x2F;easyfind&#x2F;easyfind-md-lg.png&quot; alt=&quot;EasyFind&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;不是男子漢不會用 CLI 的我要找檔案，本來以為很強大的 Spotlight 其實不會幫你找系統資料夾內的檔案的！這種時候就要派出 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.devontechnologies.com&#x2F;apps&#x2F;freeware&quot;&gt;EasyFind&lt;&#x2F;a&gt; 惹！用法因為太過簡單就不多提了！&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>幫夏天的天空摻一點藍</title>
        <published>2015-12-04T00:00:00+00:00</published>
        <updated>2015-12-04T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2015/blue-in-summer-sky/"/>
        <id>https://editor.leonh.space/2015/blue-in-summer-sky/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2015/blue-in-summer-sky/">&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2015&#x2F;blue-in-summer-sky&#x2F;tumblr_nypro0EqUW1ukdnjuo1_1280.jpg&quot; alt=&quot;幫夏天的天空摻一點藍&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2015&#x2F;blue-in-summer-sky&#x2F;tumblr_nypro0EqUW1ukdnjuo2_1280.jpg&quot; alt=&quot;幫夏天的天空摻一點藍&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2015&#x2F;blue-in-summer-sky&#x2F;tumblr_nypro0EqUW1ukdnjuo3_1280.jpg&quot; alt=&quot;幫夏天的天空摻一點藍&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2015&#x2F;blue-in-summer-sky&#x2F;tumblr_nypro0EqUW1ukdnjuo4_1280.jpg&quot; alt=&quot;幫夏天的天空摻一點藍&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2015&#x2F;blue-in-summer-sky&#x2F;tumblr_nypro0EqUW1ukdnjuo5_1280.jpg&quot; alt=&quot;幫夏天的天空摻一點藍&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>自學的 SoloLearn</title>
        <published>2015-11-12T00:00:00+00:00</published>
        <updated>2015-11-12T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2015/sololearn/"/>
        <id>https://editor.leonh.space/2015/sololearn/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2015/sololearn/">&lt;p&gt;最近找到的自學編程的網站，好像沒看到有人寫過中文的介紹，那麼就讓我寫寫吧！&lt;&#x2F;p&gt;
&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.sololearn.com&#x2F;&quot;&gt;SoloLearn&lt;&#x2F;a&gt;，口號是「Learn Playing. Play Learning」，課程內容會切成許多非常簡短的章節，每個章節都可以透過影片或圖文進行，每次學習一點點，接著就是小測驗，SoloLearn 的測驗並不像 Coursera 那樣是同學互評式的，而是系統會幫你評分，題型有選擇、填充、排序，同學互評式的測驗中才會有的無標準答案型的題型在 SoloLearn 當然不會有。每次過了一個章節就可以獲得點數與成就，點數唯一的功能就是能在天梯排名中往上擠，除此之外一無用處。由此看來口號所謂的 playing 顯得有點薄弱啊！&lt;&#x2F;p&gt;
&lt;p&gt;不過儘管如此，他還是個很好的自學網站。相較於課程包羅萬象的 Coursera，SoloLearn 的課程內容、影片皆自行製作，風格、品質都很穩定。另外就像上面提到的，課程章節可以以影片或者圖文進行，兩者互相獨立，在不方便播影片的場合還是可以以圖文模式繼續上課，這點特別不錯。&lt;&#x2F;p&gt;
&lt;p&gt;SoloLearn 除了網站外，也有 iOS、Android、Windows 的 app，搭配它的超簡短章節結構，可以說利用零碎的時間邊上課邊打發時間確實是可行的。&lt;&#x2F;p&gt;
&lt;p&gt;以我現在在上的「&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.sololearn.com&#x2F;learning&#x2F;1060&quot;&gt;SQL Fundamental&lt;&#x2F;a&gt;」為例，課程架構是這樣的：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2015&#x2F;sololearn&#x2F;tumblr_inline_nxluop3jqz1qz7pnt_1280-md.png&quot; alt=&quot;SoloLearn&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Basic Concepts&lt;&#x2F;li&gt;
&lt;li&gt;Filtering, Functions, Subqueries&lt;&#x2F;li&gt;
&lt;li&gt;JOIN, Table Operations&lt;&#x2F;li&gt;
&lt;li&gt;Challenges&lt;&#x2F;li&gt;
&lt;li&gt;Certificated&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;如果進入 Basic Concepts，就會看到又分成許多小章節：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2015&#x2F;sololearn&#x2F;tumblr_inline_nxlux0XXf71qz7pnt_1280-md.png&quot; alt=&quot;SoloLearn&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;再點進去每個小章節內就會真正的進入課程內容，不過依舊被切成許多更小的節：&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2015&#x2F;sololearn&#x2F;tumblr_inline_nxlv254AIN1qz7pnt_1280-md.png&quot; alt=&quot;SoloLearn&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;如上圖所見，真正的課程內容就是這樣短短一小節而已。&lt;&#x2F;p&gt;
&lt;p&gt;左上角的「Text &#x2F; Video」可以選擇要以圖文或是影片方式上課，Text &#x2F; Video 再往右邊看就是該章節的進度條。&lt;&#x2F;p&gt;
&lt;p&gt;以上圖為例，「Introduction to Databases」內有四小節課程內容，以及四次的小測驗。&lt;&#x2F;p&gt;
&lt;p&gt;SoloLearn 的課程幾乎都是以這種方式進行的－一小段課程、一次小測驗，小章節結束也有小章節測驗、大章節結束也有大章節測驗，最後就是整個課程的大測驗。&lt;&#x2F;p&gt;
&lt;p&gt;接觸 SoloLearn 以來，對我個人比較受用的有以下幾點：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;小段落、小測驗：運用零碎的時間就足夠上完一個小單元，輕鬆無負擔。&lt;&#x2F;li&gt;
&lt;li&gt;可以以圖文、影片方式上課：方便開影片的地方就開影片，不行也無妨，切換成圖文模式即可。&lt;&#x2F;li&gt;
&lt;li&gt;課程品質穩定：內容皆為 SoloLearn 自行製作，不僅課程品質穩定，學習模式也是穩定的。&lt;&#x2F;li&gt;
&lt;li&gt;免費：是的不用錢，佛心來著，令人不禁好奇他們要靠什麼吃穿？&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>從上而下</title>
        <published>2015-11-03T00:00:00+00:00</published>
        <updated>2015-11-03T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2015/top-down/"/>
        <id>https://editor.leonh.space/2015/top-down/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2015/top-down/">&lt;p&gt;上週五早會談到現用工單的表達的問題，大家都認同的是，版面過於繚亂，當年設計的格式在經歷了數年、數人的刪改早已面目全非，以及閱讀時的字體尺寸也過小，此外也沒有地方放圖面。於是我提出徹底翻修的意見，並且以我幾個月前新設計的版面為基礎，結果被徹底打槍了⋯⋯。&lt;&#x2F;p&gt;
&lt;p&gt;所謂打槍，並非版面設計 NG，而是根本沒有進入實質討論，並且被要求提案報告後再談。此事讓我想談談公司的決策流程與文化。&lt;&#x2F;p&gt;
&lt;p&gt;先來說說所謂的提案報告吧！本人歷來所參與的會議都是產品會議，主題皆圍繞者所負責的產品相關的&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;zh.wikipedia.org&#x2F;zh-tw&#x2F;%E5%93%81%E8%B3%AA%E3%80%81%E6%88%90%E6%9C%AC%E3%80%81%E4%BA%A4%E4%BB%98&quot;&gt;品質、成本、交期&lt;&#x2F;a&gt;所展開，顯然改進文件格式並不符合產品會議的意圖。而且很遺憾的，也沒有其它任何適當的會議讓我提出改善提案，並且顯然長官的心態是不問不管不關心，或者更精確的描述他們的心態應該是「不好用但反正沒出事」就繼續沿用舊的、不好用的格式也無妨。&lt;&#x2F;p&gt;
&lt;p&gt;何以無妨？我認為所謂無妨，乃是基於對版面、佈局的不認識，這可以理解，畢竟我們都不曾接觸過專業的文件編排，但我付出更多心力在關注這一點，因為我了解眼球的第一焦點在版面上的位置、區塊明暗的強弱帶給人類的暗示、欄位的重新組織整理，這些都影響著閱讀者對文件的理解。&lt;&#x2F;p&gt;
&lt;p&gt;我也認為，身為公司的一員，這樣的提議管道必須是暢通的，特別是這類幾乎零成本的改善方案，應該有個快速的審議管道，這也是 TQM 講的全員參與的具體實現。&lt;&#x2F;p&gt;
&lt;p&gt;然而、總之、終究，這件事就這麼了了！&lt;strong&gt;TQM 講的全員參與也就此淪為口號&lt;&#x2F;strong&gt;。或許等到有一天，出事了、某位大長官看不下去了，一聲令下才會開始翻修吧！而我本人，在經過了憤怒、失落的情緒轉折後，剩下的也只有遺憾與期許——對公司的遺憾、對自己的期許。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>給 PHP 開發者的 Docker 文件（六）</title>
        <published>2015-07-17T00:00:00+00:00</published>
        <updated>2015-07-17T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2015/php-docker-6/"/>
        <id>https://editor.leonh.space/2015/php-docker-6/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2015/php-docker-6/">&lt;p&gt;（本文譯自〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.newmediacampaigns.com&#x2F;blog&#x2F;docker-for-php-developers&quot;&gt;Docker for PHP Developers&lt;&#x2F;a&gt;〉）&lt;&#x2F;p&gt;
&lt;h2 id=&quot;nginx-dockerrong-qi-container&quot;&gt;Nginx Docker容器（Container）&lt;&#x2F;h2&gt;
&lt;p&gt;我們在實例化（instantiate）Nginx Docker 容器之前，需要先傳好虛擬主機的組態檔，在你的專案目錄下建立下面檔案 src&#x2F;vhost.conf：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;server {&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  listen 80;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  index index.html;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  server_name docker.dev;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  error_log &#x2F;var&#x2F;log&#x2F;nginx&#x2F;error.log;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  access_log &#x2F;var&#x2F;log&#x2F;nginx&#x2F;access.log;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  root &#x2F;var&#x2F;www&#x2F;public;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;}&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這是個基本（rudimentary）的 Nginx 虛擬主機，它會監聽 HTTP 80 埠的連入要求；它會回應對於主機名稱 docker.dev 的所有的 HTTP 要求；它把錯誤與存取紀錄輸出到指定的路徑檔案（並且這些檔案也符號連接（symlink）到容器的標準輸出與表準錯誤描述子（descriptor））；它定義了網站根目錄在 &#x2F;var&#x2F;www&#x2F;public。我們在實例化時會把這份虛擬主機組態檔案拷貝到我們的 Docker 容器內。&lt;&#x2F;p&gt;
&lt;p&gt;在你的專案根目錄內執行下面的 Bash 指令，實例化並且跑一個基於我們訂製的 tutorial&#x2F;nginx Docker 映像的新的 Nginx Docker 容器。&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;docker&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; run&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  -d \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  -p&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; 8080:80&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  -v&lt;&#x2F;span&gt;&lt;span&gt; $(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;pwd&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&#x2F;src&#x2F;vhost.conf:&#x2F;etc&#x2F;nginx&#x2F;sites-enabled&#x2F;vhost.conf&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;  -v&lt;&#x2F;span&gt;&lt;span&gt; $(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;pwd&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&#x2F;src:&#x2F;var&#x2F;www&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; \&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;  tutorial&#x2F;nginx&lt;&#x2F;span&gt;&lt;span&gt;;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2015&#x2F;php-docker-6&#x2F;wy6xw51-md.png&quot; alt=&quot;docker run&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;我們使用旗標 &lt;code&gt;-d&lt;&#x2F;code&gt; 讓我們新的 Docker 容器在背景執行。&lt;&#x2F;p&gt;
&lt;p&gt;我們使用旗標 &lt;code&gt;-p&lt;&#x2F;code&gt; 來對映 Docker 母機（host）埠到容器埠，在本例中，我們叫 Docker 讓 Docker 母機（8080 埠）對映到 Docker 容器（80 埠）。&lt;&#x2F;p&gt;
&lt;p&gt;（待續…）&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>給 PHP 開發者的 Docker 文件（五）</title>
        <published>2015-06-28T00:00:00+00:00</published>
        <updated>2015-06-28T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2015/php-docker-5/"/>
        <id>https://editor.leonh.space/2015/php-docker-5/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2015/php-docker-5/">&lt;p&gt;（本文譯自〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.newmediacampaigns.com&#x2F;blog&#x2F;docker-for-php-developers&quot;&gt;Docker for PHP Developers&lt;&#x2F;a&gt;〉）&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ying-yong-application-she-ding&quot;&gt;應用（Application）設定&lt;&#x2F;h2&gt;
&lt;p&gt;至今我們的應用所需的 Docker 映像（image）都有了，該開始把 Docker 映像實例化（instantiate）出 Docker 容器（container）了。首先在專案跟目錄下建立如下的目錄結構。&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;src&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  public&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    index.html&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;src&#x2F; 目錄放我們的應用源碼、src&#x2F;public&#x2F; 作為我們的網頁伺服器的網站根目錄、index.html 檔案內容打「&lt;code&gt;Hello World!&lt;&#x2F;code&gt;」。&lt;&#x2F;p&gt;
&lt;p&gt;我們的應用將會透過 docker.dev 的域名（domain）存取，你應當把這個域名對映（map）到你的 Docker 母機（host）的 IP 位址，假設你是用裝在 Linux 內的原生的 Docker 的話，那麼就是你自己電腦的 IP 位址；如果你是用 Boot2Docker 的話，可以執行 Bash 指令 &lt;code&gt;boot2docker ip&lt;&#x2F;code&gt; 來找出你的 Docker 母機的 IP 位址。在此先假設你的 Docker 母機 IP 位址為 192.168.59.103，要把 docker.dev 域名對映到 192.168.59.103 這組 IP 位址的話，在你的本地電腦內的 &#x2F;etc&#x2F;hosts 檔案最後加入下面這一行：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;192.168.59.103    docker.dev&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;（待續…）&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>給 PHP 開發者的 Docker 文件（四）</title>
        <published>2015-06-25T00:00:00+00:00</published>
        <updated>2015-06-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2015/php-docker-4/"/>
        <id>https://editor.leonh.space/2015/php-docker-4/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2015/php-docker-4/">&lt;p&gt;（本文譯自〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.newmediacampaigns.com&#x2F;blog&#x2F;docker-for-php-developers&quot;&gt;Docker for PHP Developers&lt;&#x2F;a&gt;〉）&lt;&#x2F;p&gt;
&lt;h2 id=&quot;lai-jian-ge-ying-yong-application-ba&quot;&gt;來建個應用（Application）吧&lt;&#x2F;h2&gt;
&lt;p&gt;說了一堆有的沒的（jibber-jibber），來搭個有 Nginx 網頁伺服器和與其通訊的 MySQL 資料庫的應用（application）吧！這不會是複雜到超出想像的應用，但是是個絕佳的機會來學習：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;如何組建 Docker 映像&lt;&#x2F;li&gt;
&lt;li&gt;如何實例化（instantiate）Docker 容器（container）&lt;&#x2F;li&gt;
&lt;li&gt;如何用 Docker 卷宗（volume）保存（persist）資料&lt;&#x2F;li&gt;
&lt;li&gt;如何聚合（aggregate）容器輸出的紀錄檔（log）&lt;&#x2F;li&gt;
&lt;li&gt;如何利用 Docker Compose 管理互相關聯的容器&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;首先，在你的電腦內建個專案目錄並切換到該目錄內，本文所有的指令都在這目錄之內下達。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;you-xi-ji-hua&quot;&gt;遊戲計畫&lt;&#x2F;h3&gt;
&lt;p&gt;在做之前，先讓我們規劃一下該怎麼玩，首先我們需要安裝 Docker，然後我們得決定（figure out）這個應用要怎麼拆解成不同的容器。這是個簡單的應用：有一個 Nginx 網頁伺服器、一個 PHP-FPM 應用伺服器、與一個 MySQL 資料庫伺服器，因此我們的這個應用需要三個 Docker 容器，分別從三個 Docker 映像實例化而來。我們也需要決定是否要自建 Docker 映像，還是從 Docker Hub 抓既有的來用就好。&lt;&#x2F;p&gt;
&lt;p&gt;我們也得決定用哪個基礎映像（base image）來作為共用的映像，在此所有的 Docker 映像都將擴展（extend）自該基礎映像。如果我們的 Docker 容器都擴展自同一個基礎映像，那麼可以節省磁碟空間並簡化 Docker 映像的相依（dependency）數量。在本教學中我們將以 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;registry.hub.docker.com&#x2F;_&#x2F;ubuntu&quot;&gt;Ubuntu 14.04&lt;&#x2F;a&gt; 作為我們的基礎映像。&lt;&#x2F;p&gt;
&lt;p&gt;在建構或下載需要的 Docker 映像後，我們將會進行實例化並使用 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.docker.com&#x2F;compose&#x2F;&quot;&gt;Docker Compose&lt;&#x2F;a&gt; 運行我們的應用的 Docker 容器。如果一切按照計畫，我們將會運行（up）起一個可依需求（on-demand）運用的 PHP 開發環境，而這僅僅只須 Bash 指令與幾秒的時間。&lt;&#x2F;p&gt;
&lt;p&gt;最後我們會討論如何聚合容器輸出的紀錄檔（log），記住，容器是免洗的（expendable），而且我們絕不可把資料存在容器內，相反地，我們將把紀錄檔重導（redirect）到各自容器內的標準輸出（standard output）與標準錯誤（standard error）檔案描述子（descriptor），如此一來 Docker Compose 即可收集並管理容器內的紀錄資料。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;an-zhuang-docker&quot;&gt;安裝 Docker&lt;&#x2F;h3&gt;
&lt;p&gt;Docker 需要 Linux，並且它支援許多的 Linux 發行版：Ubuntu、Debian、CentOS、CRUX、Fedora、Gentoo、RedHat、與 SUSE，挑一個吧！（take your pick）。你的本地的 Linux 作業系統就是 Docker 母機（host），實例化以及運行 Docker 容器在其內。&lt;&#x2F;p&gt;
&lt;p&gt;許多人使用 Mac OS X或是 Windows，這不表示你完蛋了（out of luck），因為有個叫做 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;boot2docker&quot;&gt;Boot2Docker&lt;&#x2F;a&gt; 的東西，這工具可以建立一個微型的 Linux 虛擬機，並且這個虛擬機將會被做為 Docker 母機，而非你的本地的作業系統。別擔心，這個虛擬機&lt;strong&gt;真的超小&lt;&#x2F;strong&gt;而且開機只要約 5 秒。&lt;&#x2F;p&gt;
&lt;p&gt;裝好 Boot2Docker 後，可以雙擊 Boot2Docker 應用圖示，或者在終端機（terminal）執行這些 Bash 指令：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;boot2docker&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; init&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;boot2docker&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; up&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;eval&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;$(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;boot2docker&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; shellinit)&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這三行指令會建立虛擬機（假使尚未有建立好了的虛擬機的話），啟動虛擬機，並輸出必要的環境變數（environment variables），如此你的本地作業系統就可以與 Docker 母機相互通訊。&lt;&#x2F;p&gt;
&lt;p&gt;如果你像我一樣不喜歡打字的話，你可以建立 Bash 替身（alias）。加入下面幾行到你的 ~&#x2F;.bash_profile 檔案內：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;alias&lt;&#x2F;span&gt;&lt;span&gt; dockup&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;=&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;boot2docker init &amp;amp;&amp;amp; boot2docker up &amp;amp;&amp;amp; eval &lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;\&amp;quot;\$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;(boot2docker shellinit)&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;\&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;boot2docker&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; up&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;eval&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; &amp;quot;$(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;boot2docker&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; shellinit)&amp;quot;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這會建立一個叫做 &lt;code&gt;dockup&lt;&#x2F;code&gt;（Docker up 的縮寫）的 Bash 替身，現在只要打簡單的 dockup 在新的終端機就可以建立、啟動、初始化你的 Docker 母虛擬機。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;nginx-docker-ying-xiang&quot;&gt;Nginx Docker 映像&lt;&#x2F;h3&gt;
&lt;p&gt;首先我們來關注 Nginx 網頁伺服器，雖然可以確定在 Docker Hub 一定找得到自 Ubuntu 14.04 基礎映像擴展而來的而且合用的映像，但我們會利用這次機會來學習建構自己的 Docker 映像。建立目錄 images&#x2F;nginx&#x2F;，新增 Dockerfile 與 start.sh 到那個目錄內，此時你的專案目錄看起來應該是這樣：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;plain&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;images&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  nginx&#x2F;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    Dockerfile&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    start.sh&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;用你常用的編輯器打開 Dockerfile 並加入這些內容：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;docker&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;FROM&lt;&#x2F;span&gt;&lt;span&gt; phusion&#x2F;baseimage&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;MAINTAINER&lt;&#x2F;span&gt;&lt;span&gt; YOUR NAME &amp;lt;YOUR EMAIL&amp;gt;&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;CMD&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;&#x2F;sbin&#x2F;my_init&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;RUN&lt;&#x2F;span&gt;&lt;span&gt; apt-get update &amp;amp;&amp;amp; apt-get install -y python-software-properties&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;RUN&lt;&#x2F;span&gt;&lt;span&gt; add-apt-repository ppa:nginx&#x2F;stable&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;RUN&lt;&#x2F;span&gt;&lt;span&gt; apt-get update &amp;amp;&amp;amp; apt-get install -y nginx&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;RUN&lt;&#x2F;span&gt;&lt;span&gt; echo &lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;daemon off;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt; &amp;gt;&amp;gt; &#x2F;etc&#x2F;nginx&#x2F;nginx.conf&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;RUN&lt;&#x2F;span&gt;&lt;span&gt; ln -sf &#x2F;dev&#x2F;stdout &#x2F;var&#x2F;log&#x2F;nginx&#x2F;access.log&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;RUN&lt;&#x2F;span&gt;&lt;span&gt; ln -sf &#x2F;dev&#x2F;stderr &#x2F;var&#x2F;log&#x2F;nginx&#x2F;error.log&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;RUN&lt;&#x2F;span&gt;&lt;span&gt; mkdir -p &#x2F;etc&#x2F;service&#x2F;nginx&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;ADD&lt;&#x2F;span&gt;&lt;span&gt; start.sh &#x2F;etc&#x2F;service&#x2F;nginx&#x2F;run&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;RUN&lt;&#x2F;span&gt;&lt;span&gt; chmod +x &#x2F;etc&#x2F;service&#x2F;nginx&#x2F;run&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;EXPOSE&lt;&#x2F;span&gt;&lt;span&gt; 80&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;RUN&lt;&#x2F;span&gt;&lt;span&gt; apt-get clean &amp;amp;&amp;amp; rm -rf &#x2F;var&#x2F;lib&#x2F;apt&#x2F;lists&#x2F;* &#x2F;tmp&#x2F;* &#x2F;var&#x2F;tmp&#x2F;*&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;利用定義在 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.docker.com&#x2F;engine&#x2F;reference&#x2F;builder&#x2F;&quot;&gt;Docker 文件&lt;&#x2F;a&gt;內的指令寫入 Dockerfile 內以建構一個新的 Docker 映像。讓我們一行一行的看它們的作用分別是什麼。&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Line 1&lt;&#x2F;strong&gt; 以 &lt;code&gt;FROM&lt;&#x2F;code&gt; 開頭，用於標示父 Docker 映像的名稱，由該父映像擴展出我們的新映像。我們拿 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;phusion&#x2F;baseimage-docker&quot;&gt;phusion&#x2F;baseimage&lt;&#x2F;a&gt; 來擴展，因為它提供簡易的 Docker 容器操作工具。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Line 2&lt;&#x2F;strong&gt; 以 &lt;code&gt;MAINTAINER&lt;&#x2F;code&gt; 開頭，用於標示你自己的名稱與電郵信箱。如果你把這個 Docker 映像分享到 Docker Hub，其他開發者就能夠知道是誰做出這個映像以及有問題要問誰。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Line 3&lt;&#x2F;strong&gt; 用於初始化 phusion&#x2F;baseimage 基礎映像內建的家管（house-keeping）程序。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Line 4 - 6&lt;&#x2F;strong&gt; 用於從 Nginx 社群 PPA（personal package archive）安裝最新的穩定版的 Nginx。這個 PPA 提供最新的穩定的 Nginx，讓我們可以迅速地裝起來而不用從源碼編起。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;ine 7 - 9&lt;&#x2F;strong&gt; 用於更新 Nginx 組態檔案，讓 Nginx 不要以常駐服務（daemonized）模式運作。這幾行也把 Nginx 的存取與錯誤紀錄連結（symlink）到容器的標準輸出與標準錯描述子，如此 Docker 就可以整合（aggregate）管理我們的應用的紀錄資料。我們從不保存（persist）任何資料在容器自身內。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Line 10 - 12&lt;&#x2F;strong&gt; 用於拷貝 start.sh 檔案進容器，phusion&#x2F;baseimage 基礎映像會呼叫這個檔案來啟動 Nginx 伺服器程序。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Line 13&lt;&#x2F;strong&gt; 用於告訴 Docker 把所有實例化（instantiated）自這個映像的容器的 80 埠開放（expose）。我們需要開放 80 埠好讓連入（inbound）的HTTP要求（request）能被 Nginx 接收並做適當處理。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;strong&gt;Line 15&lt;&#x2F;strong&gt; 用於執行 phusion&#x2F;baseimage 基礎映像提供的家管程序。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;接下來，開啟 start.sh 檔案並在最後加入這些內容：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;#!&#x2F;usr&#x2F;bin&#x2F;env bash&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;service&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; nginx start&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這裡面的 Bash 指令用於啟動 Nginx 網頁伺服器程序。現在我們已經準備好建構我們的 Docker 映像了，移動到專案目錄 images&#x2F;nginx&#x2F; 內並執行這個 Bash 指令：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;docker&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; build&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; -t&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; tutorial&#x2F;nginx .&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2015&#x2F;php-docker-4&#x2F;JtUlKio-md.png&quot; alt=&quot;docker build&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;根據先前的 Dockerfile 的內容，Docker 開始建構你的 Nginx Docker 映像，並且你會在終端機看到一些輸出內容。你也會看到 Docker 會從 Docker Hub 抓下來任何有相依性（dependency）的父映像（parent image）。完成後，你可以執行 docker images 指令，會輸出現有的 Docker 映像清單，你應該會看到 tutorial&#x2F;nginx 在清單內。&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2015&#x2F;php-docker-4&#x2F;VhYhPqr-md.png&quot; alt=&quot;docker images&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;恭喜啦！你搞定了你的第一個 Docker 映像。但請記得，這還只是個 Docker 映像，還得利用它去實例化出 Docker 容器，但在做實例化前，讓我們先把 PHP-FPM 和 MySQL 的 Docker 映像都搞定。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;php-fpm-dockerying-xiang&quot;&gt;PHP-FPM Docker映像&lt;&#x2F;h3&gt;
&lt;p&gt;接下來我們來看看 PHP-FPM，我們將不會自行建構這個 Docker 映像，相反地，我已經在 Docker Hub 分享了一個 nmcteam&#x2F;php56 的映像，執行下面的 Bash 指令來把這個 PHP-FPM Docker 映像從 Docker Hub 抓回來。&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;docker&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; pull nmcteam&#x2F;php56&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h3 id=&quot;mysql-docker-ying-xiang&quot;&gt;MySQL Docker 映像&lt;&#x2F;h3&gt;
&lt;p&gt;最後咱們來關注 MySQL，我在 Docker Hub 找到了擴展自 Ubuntu 14.04 基礎映像的 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;registry.hub.docker.com&#x2F;r&#x2F;sameersbn&#x2F;mysql&#x2F;&quot;&gt;sameersbn&#x2F;mysql&lt;&#x2F;a&gt;，執行下面的 Bash 指令來把這個 MySQL Docker 映像從 Docker Hub 抓回來。&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;docker&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; pull sameersbn&#x2F;mysql&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;（待續…）&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>給 PHP 開發者的 Docker 文件（三）</title>
        <published>2015-06-24T00:00:00+00:00</published>
        <updated>2015-06-24T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2015/php-docker-3/"/>
        <id>https://editor.leonh.space/2015/php-docker-3/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2015/php-docker-3/">&lt;p&gt;（本文譯自〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.newmediacampaigns.com&#x2F;blog&#x2F;docker-for-php-developers&quot;&gt;Docker for PHP Developers&lt;&#x2F;a&gt;〉）&lt;&#x2F;p&gt;
&lt;h2 id=&quot;docker-he-vagarant-you-shen-mo-bu-tong&quot;&gt;Docker 和 Vagarant 有什麼不同？&lt;&#x2F;h2&gt;
&lt;p&gt;如果在每個專案同時運行多個容器（container）的情況下 Docker 比 Vagrant 好在哪裡？會比用 Vagrant 同時跑多部虛擬機的方式差嗎？No，原因如下述。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;docker-ying-xiang-shi-ke-kuo-zhan-de-extendable&quot;&gt;Docker 映像是可擴展的（Extendable）&lt;&#x2F;h3&gt;
&lt;p&gt;Docker 映像可擴展它的父映像（parent image），這點讓 Docker 映像更加類似於 PHP 類別（class）。例如一個 Nginx Docker 映像可擴展自 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;phusion.github.io&#x2F;baseimage-docker&#x2F;&quot;&gt;phusion&#x2F;baseimage&lt;&#x2F;a&gt; Docker 映像，並且 phusion&#x2F;baseimage 也擴展自最父層（top-most）的 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;github.com&#x2F;dockerfile&#x2F;ubuntu&quot;&gt;ubuntu Docker 映像&lt;&#x2F;a&gt;。每一個子映像只會包含與父映像有差異的部份，也就是說 ubuntu 映像只是一個小型的Ubuntu 作業系統；而 phusion&#x2F;baseimage 裡面只有一些用於強化 Docker 容器（container）與作業的工具；最後的 Nginx 映像裡面也只有 Nginx 網頁伺服器和它的組態檔案。&lt;&#x2F;p&gt;
&lt;p&gt;互不相關的 Docker 映像也有可能擴展自同一個父輩（ancestor）映像，Docker 也鼓勵這麼做，如此一來那些被重複運用的映像只要下載一次就好。舉例來說，如果一個 Nginx 和另一個 PHP-FPM 映像都擴展自同樣的 ubuntu:14.04 Docker 映像，那麼這個共同的 Ubuntu 映像只要下載一次就好了。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;docker-rong-qi-shi-qing-liang-de-lightweight&quot;&gt;Docker 容器是輕量的（Lightweight）&lt;&#x2F;h3&gt;
&lt;p&gt;Docker 容器是輕量的並且僅需求少量的系統資源就可運作，實際上只要需要的 Docker 映像下載好後，把一個 Docker 映像實例化（instantiating）成 Docker 容器跑起來只需要花幾秒鐘的時間，相較之下用 &lt;code&gt;vagrant up –provision&lt;&#x2F;code&gt; 去建構及腳本化實現（provision）一台虛擬機時常常都要花個 15 - 30 分鐘的時間。這樣的高效率基於兩個原因，首先每個 Docker 容器其實就只是個沙盒的系統程序（sandboxed system process），一個程序只做一件事；其次，所有的 Docker 容器都跑在一個共享的 Docker 母機（host machine），母機可以是你自己的 Linux 作業系統也可以只是個小型的 Linux 虛擬機。&lt;&#x2F;p&gt;
&lt;p&gt;Docker 容器同時也是暫存的（ephemeral）與免洗的（expendable），你可以摧毀（destroy）與取代（replace）某個 Docker 容器而不致影響到整個大型應用（application）。&lt;&#x2F;p&gt;
&lt;p&gt;假使容器只是暫存的，那我們要如何保存要留下的（persistent）應用資料？我也有過相同疑問。我們透過 Dockr 容器卷宗（volume）保存（persist）應用資料在 Docker 母機上。在後文中談到實例化 MySQL 容器時我們會再談談 Docker 容器卷宗。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;xiang-tong-mu-ji-bu-tong-shi-jie&quot;&gt;相同母機、不同世界&lt;&#x2F;h3&gt;
&lt;p&gt;不像 Vagrant，每個專案都需要完整的虛擬機（virtual machine）、檔案系統（filesystem）、與網路架構（network stack），Docker 容器跑在單一個共用的 Docker母機，怎麼辦到的呢？不同的 Docker 容器如果佔用到同樣的檔案系統或系統資源時不會互相衝突（collide）嗎？不會，而且這正是 Docker 的拿手絕活（pièce de résistance）。&lt;&#x2F;p&gt;
&lt;p&gt;Docker 被建立在它自有的低階的 Linux 函式庫上，稱為 libcontainer，這個工具透過對各種 Unix 與 Linux 的轉接器（adapter）來沙盒化單一的系統程序並限制它們存取系統資源。Docker 的 libcontainer 函式庫可讓不同的系統程序共存於同一個 Docker 母機上並且存取它們各自沙盒化的檔案系統與系統資源。&lt;&#x2F;p&gt;
&lt;p&gt;也就是說，Docker 並不只是 libcontainer 而已，Docker 是封裝（encapsulate）了許多包括 libcontainer 內的工具包，它具有 Docker 映像建立（creation）、移植性（portability）、版次性（versioning）、與分享（sharing）的能力。&lt;&#x2F;p&gt;
&lt;p&gt;Docker 具備多方之長（the best of both worlds）。Vagrant 只是個專注於硬體虛擬化的工具，反觀 Docker 更專注於程序隔離（isolation）。Vagrant 與 Docker 都致力達到的開發者的期望目標，它們創建互相隔離的應用運行環境，然而 Docker 以更好的效率與移植性做到這一點。&lt;&#x2F;p&gt;
&lt;p&gt;（待續…）&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>給 PHP 開發者的 Docker 文件（二）</title>
        <published>2015-06-23T00:00:00+00:00</published>
        <updated>2015-06-23T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2015/php-docker-2/"/>
        <id>https://editor.leonh.space/2015/php-docker-2/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2015/php-docker-2/">&lt;p&gt;（本文譯自〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.newmediacampaigns.com&#x2F;blog&#x2F;docker-for-php-developers&quot;&gt;Docker for PHP Developers&lt;&#x2F;a&gt;〉）&lt;&#x2F;p&gt;
&lt;h2 id=&quot;ha-luo-docker&quot;&gt;哈囉！Docker&lt;&#x2F;h2&gt;
&lt;p&gt;Docker 是一個包含許多技術的概括性詞彙，它提供的工具可以幫助你把某個應用（application）的基礎架構（infrastructure）分離成數個邏輯模塊（容器），你可以只組合某些必要的模塊去把應用的基礎架構建構成可移植（portable）的，這樣的基礎架構可以於開發（development）、上線（staging）、生產（production）環境中遷移。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;shen-mo-shi-docker-rong-qi-container&quot;&gt;什麼是 Docker 容器（Container）？&lt;&#x2F;h3&gt;
&lt;p&gt;一個容器指的是一個大型應用內的某個單一的服務。舉例來說，假設我們正在建構一個 PHP 應用，那麼我們需要一個網頁伺服器（如 nginx）；我們也需要一個應用伺服器（如 PHP-FPM）；我們還需要一個資料庫伺服器（如 MySQL）。也就是說我們的應用需要三個這樣的獨立的服務：網頁伺服器、應用伺服器、資料庫伺服器，這三樣服務都可以被分離成各自專屬的 Docker 容器，只要我們把這三個容器連結起來，我們就有了一個完整的應用。&lt;&#x2F;p&gt;
&lt;h3 id=&quot;shen-mo-shi-docker-ying-xiang-image&quot;&gt;什麼是 Docker 映像（Image）？&lt;&#x2F;h3&gt;
&lt;p&gt;把某個應用分成數個容器看來像是吃力不討好的工作，至少我以往是這麼認為的，不過實際上並不然，首先，容器實際上是映像的實例（instance），若把 Docker 的映像想像成 PHP 的類別（class），就像 PHP 類別可以讓我們實例化（instantiate）出許多獨立的物件（object）一樣，Docker 的映像也可以實例化出許多獨立的 Docker 容器。例如我們可以拿一個 PHP-FPM Docker 映像去為我們的每個應用實例化出它們的獨立的 PHP-FPM 容器。&lt;&#x2F;p&gt;
&lt;p&gt;我們也可以自製 Docker 映像，不過還是從 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;registry.hub.docker.com&#x2F;&quot;&gt;Docker Hub&lt;&#x2F;a&gt; 去找適合的 Docker 映像會比較簡單些，像是我們會抓 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;registry.hub.docker.com&#x2F;r&#x2F;sameersbn&#x2F;mysql&quot;&gt;sameersbn&#x2F;mysql&lt;&#x2F;a&gt; 這個 Docker 映像來建立 MySQL 資料庫容器。如果別人已經建好了能幫我們解決問題的 Docker 映像，為什麼還要重新發明輪子呢&lt;&#x2F;p&gt;
&lt;p&gt;（待續…）&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>給 PHP 開發者的 Docker 文件（一）</title>
        <published>2015-06-22T00:00:00+00:00</published>
        <updated>2015-06-22T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2015/php-docker-1/"/>
        <id>https://editor.leonh.space/2015/php-docker-1/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2015/php-docker-1/">&lt;p&gt;（本文譯自〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.newmediacampaigns.com&#x2F;blog&#x2F;docker-for-php-developers&quot;&gt;Docker for PHP Developers&lt;&#x2F;a&gt;〉）&lt;&#x2F;p&gt;
&lt;p&gt;數年以來我都用 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.vagrantup.com&#x2F;&quot;&gt;Vagarant&lt;&#x2F;a&gt; 管理本地的開發伺服器，根據 Vagrant 網站所述，Vagrant 是用於創建、組態輕量的（light-weight）、可重製的（reproducible）、可移植的（portable）開發環境的工具。基本上，我的每個專案所需的特有的軟體組合都靠 Vagrant 幫忙創建（create）與腳本化實現（provision）。有 Vagrant，可以幫我們達成下面三件事：&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;Vagrant 隔離每個專案的環境避免軟體衝突。&lt;&#x2F;li&gt;
&lt;li&gt;Vagrant 讓專案的每個成員都擁有相同的軟體版本。&lt;&#x2F;li&gt;
&lt;li&gt;Vagrant 建構的本地開發環境可相當於實際上線的環境。&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;不過咧！Vagrant 有個比較大的缺點，它依賴硬體層的虛擬化能力，這表示每個專案都是跑在完整的虛擬機內，而每個虛擬機內都是跑一個完完整整的作業系統，這吃掉了許多的系統資源（像是處理器、記憶體與以 gigabytes 計的磁碟空間），當數個專案同時一起跑起來時我最常見到的警告訊息就是：&lt;&#x2F;p&gt;
&lt;blockquote class=&quot;&quot;&gt;
&lt;p class=&quot;quote&quot;&gt;您的啟動磁碟快滿了。&lt;br&gt;
Your startup disk is almost full.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;邏輯上的解決方法是把數個專案跑在同一台 Vagrant 虛擬機內，著名的 Laravel Homestead 就是走這套策略，不過你的專案就再也無法彼此隔離了，也無法再為每個專案建構特有的伺服軟體架構（server software stack），然而好處是這節省了系統資源，並且簡化了基礎架構（infrastructure）的安裝與管理功夫。如果你想利用 Vagrant 管理本地專案的話，本人強力建議採用 Laravel Homestead 的單一虛擬機、多專案的架構。&lt;&#x2F;p&gt;
&lt;p&gt;不過本文要談的是另一種解決方案，先別說 Vagrant 了，你聽過 Docker 嗎？我是在一年前才首次聽說過這玩意，依我說啊，Docker 的精髓在於容器（container）。什麼是容器？？老夫深入了解並閱讀了許許多多什麼容器化（containerization）、程序隔離（process isolation）、union filesystems 的文件後。那一大堆的專有名詞和概念開始繞著我的腦袋繞圈圈，在那天結束之時，我還在抓著我的頭疑惑著 Docker 是啥？以及 Docker 能幫我幹嘛？不過自此之後我學到很多，並且我想讓你知道，身為一個開發者，Docker 是如何地改變我的生涯。&lt;&#x2F;p&gt;
&lt;p&gt;（待續…）&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>林口小田園</title>
        <published>2015-04-10T00:00:00+00:00</published>
        <updated>2015-04-10T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2015/farm/"/>
        <id>https://editor.leonh.space/2015/farm/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2015/farm/">&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2015&#x2F;farm&#x2F;tumblr_nmh7otcacY1qz7n50o1_640-md-lg.jpg&quot; alt=&quot;林口小田園&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>紀念我的肝</title>
        <published>2013-04-10T00:00:00+00:00</published>
        <updated>2013-04-10T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2013/hepar/"/>
        <id>https://editor.leonh.space/2013/hepar/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2013/hepar/">&lt;p&gt;關於最近，一定要寫一些什麼下來，算是對上面連發兩篇廢文的總結。&lt;&#x2F;p&gt;
&lt;p&gt;有時候我會拿過去的自己質疑現在的自己，為什麼剛開始可以無怨無尤的加班，是熱情？還是最近給自己的理由——那時比較閒，不若現在已被主要的工作壓得喘不過氣。又或只是「剛進來要先裝一下」？&lt;&#x2F;p&gt;
&lt;p&gt;無論如何，剛結束近月來的第 N 次凌晨加班，在留紙條給同事的當下，差點有股衝動脫筆而出——「今天請假，明天不一定。如整個禮拜都沒來請依曠職論，下週一會來辦離職。」猶豫許久，最後為了十六萬，忍了。&lt;&#x2F;p&gt;
&lt;p&gt;憤怒的癥結點終究會被挖掘出來，只怕為時已晚。我的身體有時會被某部分無法控制的情緒支配著，像劉文聰：「下一個會死什麼人，連我自己也不知道！」等到回過神來，往往為時已晚。&lt;&#x2F;p&gt;
&lt;p&gt;最後，即使我並不主觀這麼想，但發這種文，等於是對潛意識宣告著某段歷程的結語，而故事的最後往往都是說了掰掰。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>憤怒的源頭</title>
        <published>2013-04-09T00:00:00+00:00</published>
        <updated>2013-04-09T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2013/mad/"/>
        <id>https://editor.leonh.space/2013/mad/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2013/mad/">&lt;p&gt;有時候氣到自己也理不清，憤怒的原因難以察覺。&lt;&#x2F;p&gt;
&lt;p&gt;至少現在可以確定，平靜後再回想依然令人憤怒的引爆點才是引起怒火的中樞，可用此法釐清真正的源頭與那些「被掃到颱風尾」的次要或間接原因。&lt;&#x2F;p&gt;
&lt;p&gt;嫌疑犯一：為何我要應對客戶、協力廠，還要身兼製程再兼產線作業員？&lt;&#x2F;p&gt;
&lt;p&gt;目前在平靜的摩斯只想到這點，就是它令我感到憤怒無比。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>泰國素萬那普國際機場</title>
        <published>2013-03-18T00:00:00+00:00</published>
        <updated>2013-03-18T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2013/suvarnabhumi-airport/"/>
        <id>https://editor.leonh.space/2013/suvarnabhumi-airport/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2013/suvarnabhumi-airport/">&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2013&#x2F;suvarnabhumi-airport&#x2F;tumblr_mov3xj2M8E1qz7n50o1_1280-lg.jpg&quot; alt=&quot;泰國素萬那普國際機場&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>鍵盤</title>
        <published>2012-12-18T00:00:00+00:00</published>
        <updated>2012-12-18T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2012/keyboard/"/>
        <id>https://editor.leonh.space/2012/keyboard/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2012/keyboard/">&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2012&#x2F;keyboard&#x2F;8275873922_2fa468dc03_o-lg.jpg&quot; alt=&quot;鍵盤&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>TPCA</title>
        <published>2012-10-30T00:00:00+00:00</published>
        <updated>2012-10-30T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2012/tpca/"/>
        <id>https://editor.leonh.space/2012/tpca/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2012/tpca/">&lt;p&gt;今年的 TPCA Show 終於落幕了，從今年開始不再扮演設備商的角色，也因此不再需要忙碌於設備的進退場工作，最後結束當天五點前就下班走人，感覺挺不錯。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>多明</title>
        <published>2012-10-23T00:00:00+00:00</published>
        <updated>2012-10-23T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2012/duo-ming/"/>
        <id>https://editor.leonh.space/2012/duo-ming/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2012/duo-ming/">&lt;p&gt;多明走了，我們家的愛貓，過了一天哥哥還是好想你，想你粗壯的尾巴、磨蹭的神情、傻裡傻氣的睡姿。對不起我們沒有把你醫好…，如果你想投胎的話拜託請再次成為我們家的一份子好嗎，我會一直想念著你直到永遠。&lt;&#x2F;p&gt;
&lt;p&gt;謹以此短文紀念多明，我們的家人、家貓、以及快樂的泉源。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>開場白</title>
        <published>2012-10-17T00:00:00+00:00</published>
        <updated>2012-10-17T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2012/intro/"/>
        <id>https://editor.leonh.space/2012/intro/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2012/intro/">&lt;p&gt;人生在世總是要留點紀錄，可以是回憶，也許現在的自己也是未來的借鏡，總之，一個市井小民的簡言絮語就這麼開始了。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>邊什麼際效什麼應</title>
        <published>2012-07-18T00:00:00+00:00</published>
        <updated>2012-07-18T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2012/side-effect/"/>
        <id>https://editor.leonh.space/2012/side-effect/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2012/side-effect/">&lt;p&gt;函式內的變數值不可動，敢動殺全家。&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2012&#x2F;side-effect&#x2F;functional-programming-howto-e28094-python-v2-7-3-documentation-md.png&quot; alt=&quot;函式編程&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：〈&lt;a href=&quot;https:&#x2F;&#x2F;docs.python.org&#x2F;zh-tw&#x2F;3&#x2F;howto&#x2F;functional.html&quot;&gt;函式編程&lt;&#x2F;a&gt;〉&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Parameter &#x2F; Argument</title>
        <published>2012-07-17T00:00:00+00:00</published>
        <updated>2012-07-17T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2012/parameter-argument/"/>
        <id>https://editor.leonh.space/2012/parameter-argument/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2012/parameter-argument/">&lt;h2 id=&quot;parameter-argument-sha-sha-fen-bu-qing&quot;&gt;Parameter &#x2F; Argument 傻傻分不清&lt;&#x2F;h2&gt;
&lt;p&gt;定義函式的時候，函式要吃的是 argument；使用函式的時候，傳給函式吃的是 parameter：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;def&lt;&#x2F;span&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt; fib&lt;&#x2F;span&gt;&lt;span&gt;(n):&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; pass&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt; &lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;fib(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;20&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;上例中 &lt;code&gt;n&lt;&#x2F;code&gt; 是argument、&lt;code&gt;20&lt;&#x2F;code&gt; 是 parameter。&lt;&#x2F;p&gt;
&lt;p&gt;喜歡定義東定義西的 computer scientists 又有這樣的定義：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;argument = formal parameter&lt;&#x2F;li&gt;
&lt;li&gt;parameter = actual parameter&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;不過這兩者其實不需要深究差別，皆以 parameter 稱之即可。就跟大水庫理論一樣，反正總之是讓騜脫罪的理論，定義與理論內涵不是重點。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;default-argument-values&quot;&gt;Default Argument Values&lt;&#x2F;h2&gt;
&lt;p&gt;如果 default 值是 mutable 的，則每次呼叫都會累計；如果是 immutable 的則函數跑完會回歸 default：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2012&#x2F;parameter-argument&#x2F;tutorial-pdfefbc88e9a081e99da2-30e28895126efbc89-md.png&quot; alt=&quot;預設引數值&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：〈&lt;a href=&quot;https:&#x2F;&#x2F;docs.python.org&#x2F;zh-tw&#x2F;3&#x2F;tutorial&#x2F;controlflow.html#default-argument-values&quot;&gt;預設引數值&lt;&#x2F;a&gt;〉&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;keyword-arguments&quot;&gt;Keyword Arguments&lt;&#x2F;h2&gt;
&lt;p&gt;&lt;code&gt;*parameter1&lt;&#x2F;code&gt; 會包成 tuple，&lt;code&gt;**parameter2&lt;&#x2F;code&gt; 會包成 dict，當然要給 key 和 value，兩個都要用時 &lt;code&gt;*parameter1&lt;&#x2F;code&gt; 要放在 &lt;code&gt;**parameter2&lt;&#x2F;code&gt; 前面：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2012&#x2F;parameter-argument&#x2F;tutorial-pdfefbc88e9a081e99da2-31e28895126efbc89-md.png&quot; alt=&quot;關鍵字引數&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：〈&lt;a href=&quot;https:&#x2F;&#x2F;docs.python.org&#x2F;zh-tw&#x2F;3&#x2F;tutorial&#x2F;controlflow.html#keyword-arguments&quot;&gt;關鍵字引數&lt;&#x2F;a&gt;〉&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;在函式內的參數中加星號可以把參數包成 tuple 或 dict，反之亦然，在函式中對 tuple 或 dict 物件加星號可以解開成 postional parameter 或 keyword parameter：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2012&#x2F;parameter-argument&#x2F;tutorial-pdfefbc88e9a081e99da2-32e28895126efbc89-md.png&quot; alt=&quot;拆解引數列表&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：〈&lt;a href=&quot;https:&#x2F;&#x2F;docs.python.org&#x2F;zh-tw&#x2F;3&#x2F;tutorial&#x2F;controlflow.html#unpacking-argument-lists&quot;&gt;拆解引數列表&lt;&#x2F;a&gt;〉&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;h2 id=&quot;can-kao-zi-liao&quot;&gt;參考資料&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;stackoverflow.com&#x2F;questions&#x2F;36901&#x2F;what-does-double-star-asterisk-and-star-asterisk-do-for-parameters&quot;&gt;What does ** (double star &#x2F; asterisk) and * (star &#x2F; asterisk) do for parameters?&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;li&gt;〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;en.wikibooks.org&#x2F;wiki&#x2F;Python_Programming&#x2F;Functions#Variable-Length_Argument_Lists&quot;&gt;Variable-Length Argument Lists&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Python Else Clause on Loops</title>
        <published>2012-07-15T00:00:00+00:00</published>
        <updated>2012-07-15T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2012/else/"/>
        <id>https://editor.leonh.space/2012/else/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2012/else/">&lt;p&gt;真相：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2012&#x2F;else&#x2F;tutorial-pdfefbc88e9a081e99da2-27e28895126efbc89-md.png&quot; alt=&quot;Python break and continue Statements, and else Clauses on Loops&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：〈&lt;a href=&quot;https:&#x2F;&#x2F;docs.python.org&#x2F;zh-tw&#x2F;3&#x2F;tutorial&#x2F;controlflow.html#break-and-continue-statements-and-else-clauses-on-loops&quot;&gt;迴圈內的 break 和 continue 陳述式及 else 子句&lt;&#x2F;a&gt;〉&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;if&lt;&#x2F;code&gt; statement 的 &lt;code&gt;else&lt;&#x2F;code&gt;：一旦 &lt;code&gt;if&lt;&#x2F;code&gt; 的條件失效，執行 &lt;code&gt;else&lt;&#x2F;code&gt; 區塊。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;try&lt;&#x2F;code&gt; statement 的 &lt;code&gt;else&lt;&#x2F;code&gt;：沒有發生 exception，執行 &lt;code&gt;else&lt;&#x2F;code&gt; 區塊。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;for&lt;&#x2F;code&gt; loop 的 &lt;code&gt;else&lt;&#x2F;code&gt;：裡面沒有被 &lt;code&gt;break&lt;&#x2F;code&gt;，執行 &lt;code&gt;else&lt;&#x2F;code&gt; 區塊。&lt;&#x2F;li&gt;
&lt;li&gt;&lt;code&gt;while&lt;&#x2F;code&gt; loop 的 &lt;code&gt;else&lt;&#x2F;code&gt;：一旦 &lt;code&gt;while&lt;&#x2F;code&gt; 的條件失效，執行 &lt;code&gt;else&lt;&#x2F;code&gt; 區塊。&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;那 &lt;code&gt;if&lt;&#x2F;code&gt; 和 &lt;code&gt;while&lt;&#x2F;code&gt; 差在哪？&lt;&#x2F;p&gt;
&lt;p&gt;&lt;code&gt;if&lt;&#x2F;code&gt; 只判斷一次後面給的條件，跑過一次就結束 &lt;code&gt;if&lt;&#x2F;code&gt; 區塊往下執行其餘的程式碼；&lt;code&gt;while&lt;&#x2F;code&gt; 會迴圈，只要後面給的條件成立就會一直在 &lt;code&gt;while&lt;&#x2F;code&gt; 迴圈執行，一旦 &lt;code&gt;while&lt;&#x2F;code&gt; 區塊內沒有做好流程控制就可能導致無限迴圈，那就ㄎㄎ了。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Python Range</title>
        <published>2012-07-14T00:00:00+00:00</published>
        <updated>2012-07-14T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2012/range/"/>
        <id>https://editor.leonh.space/2012/range/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2012/range/">&lt;p&gt;&lt;code&gt;range( )&lt;&#x2F;code&gt; 是一個產生 &lt;code&gt;range&lt;&#x2F;code&gt; 物件的 class，注意它產生的序列不會包含賦予的結束值，文筆不好看實例最明白：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2012&#x2F;range&#x2F;tutorial-pdfefbc88e9a081e99da2-26e28895126efbc89-md.png&quot; alt=&quot;Python Range&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：〈&lt;a href=&quot;https:&#x2F;&#x2F;docs.python.org&#x2F;zh-tw&#x2F;3&#x2F;tutorial&#x2F;controlflow.html#the-range-function&quot;&gt;range() 函式&lt;&#x2F;a&gt;〉&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;所以那 &lt;code&gt;range(2, 2)&lt;&#x2F;code&gt; 會怎樣？用 &lt;code&gt;list()&lt;&#x2F;code&gt; 把 &lt;code&gt;range()&lt;&#x2F;code&gt; 包起來才會產生 &lt;code&gt;list&lt;&#x2F;code&gt; 物件：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; list&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;range&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;2&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 2&lt;&#x2F;span&gt;&lt;span&gt;))&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; bool&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;list&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;range&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;2&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 2&lt;&#x2F;span&gt;&lt;span&gt;)))&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;False&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Iterator 什麼咖</title>
        <published>2012-07-13T00:00:00+00:00</published>
        <updated>2012-07-13T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2012/iterator/"/>
        <id>https://editor.leonh.space/2012/iterator/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2012/iterator/">&lt;p&gt;在這裡一些用中文難以直觀理解的專有名詞都會直接用英文，這樣比較潮（ㄎ）。&lt;&#x2F;p&gt;
&lt;figure class=&quot;&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2012&#x2F;iterator&#x2F;Iterator-Pattern.png&quot; alt=&quot;Python Iterator&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：〈&lt;a href=&quot;https:&#x2F;&#x2F;openhome.cc&#x2F;Gossip&#x2F;DesignPattern&#x2F;IteratorPattern.htm&quot;&gt;Iterator 模式&lt;&#x2F;a&gt;〉&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2012&#x2F;iterator&#x2F;e4bba5-java-e7a88be5bc8fe7af84e4be8be4be86e68ea2e8a88e-design-patternefbc9aiterator-md.png&quot; alt=&quot;Python&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：〈&lt;a href=&quot;https:&#x2F;&#x2F;www.dotspace.idv.tw&#x2F;Jyemii&#x2F;patternscolumn&#x2F;articles&#x2F;IteratorForJava.htm&quot;&gt;以 Java 程式範例來探討 Design Pattern&lt;&#x2F;a&gt;〉&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>對「Class」的理解</title>
        <published>2012-07-12T00:00:00+00:00</published>
        <updated>2012-07-12T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2012/python-class/"/>
        <id>https://editor.leonh.space/2012/python-class/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2012/python-class/">&lt;p&gt;一直以來都搞不懂它的明白「class」的意義是什麼，照著書上做出一個 class 不是問題，問題在於不明白這樣做的意義在哪裡，就像從前國中搞不懂現在完成式和過去式到底差在哪一樣，直到前天看到周蟒咬一口才理解過來：&lt;&#x2F;p&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2012&#x2F;python-class&#x2F;objectorient-zhpy-e789a9e4bbb6e5b08ee59091e7a88be5bc8fe8a8ade8a888-write-python-language-in-chinese-google-project-hosting-md.png&quot; alt=&quot;Python&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：《&lt;a href=&quot;https:&#x2F;&#x2F;code.google.com&#x2F;archive&#x2F;p&#x2F;zhpy&#x2F;wikis&#x2F;ByteOfZhpy.wiki&quot;&gt;咬一口周蟒程式語言&lt;&#x2F;a&gt;》&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;用我自己的說法，把一種具有特有結構的資料和處理和該種資料有關的 function 包裝起來，就是 class，那些被包起來的 function 變成 method，而 class 裡面還可以放相關的變數。&lt;&#x2F;p&gt;
&lt;p&gt;把主要資料都包成 class，也就是一種模組化，如此可讓整個程式結構更加清晰。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Python 的 List</title>
        <published>2012-07-10T00:00:00+00:00</published>
        <updated>2012-07-10T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2012/python-list/"/>
        <id>https://editor.leonh.space/2012/python-list/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2012/python-list/">&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2012&#x2F;python-list&#x2F;tutorial-pdfefbc88e9a081e99da2-22e28895126efbc89-md.png&quot; alt=&quot;Python&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：Python Software Foundation&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;List的奇技淫巧：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; # Replace some items:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;...&lt;&#x2F;span&gt;&lt;span&gt; a[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;2&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 12&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; a&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 12&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 123&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1234&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; # Remove some:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;...&lt;&#x2F;span&gt;&lt;span&gt; a[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;2&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; []&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; a&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;123&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1234&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; # Insert some:&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;...&lt;&#x2F;span&gt;&lt;span&gt; a[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span&gt;:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;1&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; [’bletch’, ’xyzzy’]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; a&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;123&lt;&#x2F;span&gt;&lt;span&gt;, ’bletch’, ’xyzzy’,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1234&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; # Insert (a copy of) itself at the beginning&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; a[:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;0&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; a&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; a&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;123&lt;&#x2F;span&gt;&lt;span&gt;, ’bletch’, ’xyzzy’,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1234&lt;&#x2F;span&gt;&lt;span&gt;,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 123&lt;&#x2F;span&gt;&lt;span&gt;, ’bletch’, ’xyzzy’,&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 1234&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; # Clear the list: replace all items with an empty list&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; a[:]&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt; =&lt;&#x2F;span&gt;&lt;span&gt; []&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; a&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;[]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;can-kao-zi-liao&quot;&gt;參考資料&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.python.org&#x2F;zh-tw&#x2F;3&#x2F;tutorial&#x2F;introduction.html#lists&quot;&gt;List（串列）&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Python 的字串</title>
        <published>2012-07-09T00:00:00+00:00</published>
        <updated>2012-07-09T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2012/python-string/"/>
        <id>https://editor.leonh.space/2012/python-string/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2012/python-string/">&lt;h2 id=&quot;duo-xing-zi-chuan&quot;&gt;多行字串&lt;&#x2F;h2&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2012&#x2F;python-string&#x2F;tutorial-pdfefbc88e9a081e99da2-18e28895126efbc89-md.png&quot; alt=&quot;Python&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：Python Software Foundation&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;變數想要儲存多行字串可以用「“”“」來包住多行字串：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;print&lt;&#x2F;span&gt;&lt;span&gt;(&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;\&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;Usage: thingy [OPTIONS]&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; -h Display this usage message&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; -H hostname Hostname to connect to&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt;&amp;quot;&amp;quot;&amp;quot;&lt;&#x2F;span&gt;&lt;span&gt;)&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;zi-chuan-qie-pian&quot;&gt;字串切片&lt;&#x2F;h2&gt;
&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2012&#x2F;python-string&#x2F;tutorial-pdfefbc88e9a081e99da2-19e28895126efbc89-md.png&quot; alt=&quot;Python&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：Python Software Foundation&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;無敵記憶法，第一列表示「前兩個字」；第三列表示「除前兩個字外的其它全部」：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; word[:&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;2&lt;&#x2F;span&gt;&lt;span&gt;]&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; # The first two characters&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;’He&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span&gt; word[&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;2&lt;&#x2F;span&gt;&lt;span&gt;:]&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; # Everything except the first two characters&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;’lpA&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;&lt;h2 id=&quot;can-kao-zi-liao&quot;&gt;參考資料&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.python.org&#x2F;zh-tw&#x2F;3&#x2F;tutorial&#x2F;introduction.html#strings&quot;&gt;字串（String）&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Python 數數兒</title>
        <published>2012-07-08T00:00:00+00:00</published>
        <updated>2012-07-08T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2012/python-math/"/>
        <id>https://editor.leonh.space/2012/python-math/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2012/python-math/">&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2012&#x2F;python-math&#x2F;tutorial-pdfefbc88e9a081e99da2-16e28895126efbc89-md.png&quot; alt=&quot;Python&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：Python Software Foundation&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;「&lt;code&gt;&#x2F;&lt;&#x2F;code&gt;」是除法，就是普通人類認知的除法，有小數點。如果只要取商數不要餘數，用「&lt;code&gt;&#x2F;&#x2F;&lt;&#x2F;code&gt;」：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 8&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&#x2F;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;5&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; # Fractions aren’t lost when dividing integers&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;1.6&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt; # Integer division returns the floor: ... 7&#x2F;&#x2F;3&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;2&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&amp;gt;&amp;gt;&amp;gt;&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt; 7&lt;&#x2F;span&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;&#x2F;&#x2F;-&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;3&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #F97583;&quot;&gt;-&lt;&#x2F;span&gt;&lt;span style=&quot;color: #79B8FF;&quot;&gt;3&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;「&lt;code&gt;&#x2F;&lt;&#x2F;code&gt;」是浮點數除法、「&lt;code&gt;&#x2F;&#x2F;&lt;&#x2F;code&gt;」是整數除法，那「&#x2F;&#x2F;&#x2F;」是什麼呢？就是 &amp;gt;&#x2F;&#x2F;&#x2F;&amp;lt; 啦。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;can-kao-zi-liao&quot;&gt;參考資料&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.python.org&#x2F;zh-tw&#x2F;3&#x2F;tutorial&#x2F;introduction.html#using-python-as-a-calculator&quot;&gt;把 Python 當作計算機使用&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>直接執行 Python Script</title>
        <published>2012-07-06T00:00:00+00:00</published>
        <updated>2012-07-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2012/python-shebang/"/>
        <id>https://editor.leonh.space/2012/python-shebang/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2012/python-shebang/">&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2012&#x2F;python-shebang&#x2F;tutorial-pdfefbc88e9a081e99da2-12e28895126efbc89-md.png&quot; alt=&quot;Execuable Python Scripts&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：Python Software Foundation&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;要直接執行 myscript.py，要在該 script 內的頭一行加上：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;python&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #6A737D;&quot;&gt;#! &#x2F;usr&#x2F;bin&#x2F;env python3&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;這裡取用 env 環境變數，env 會知道 Python 放哪裡，而不用手動去找，也可避免掉換平台路徑就找不到的窘境。&lt;&#x2F;p&gt;
&lt;p&gt;然後在系統 shell 下用 chmod 命令為 myscirpt.py 加上可執行權限：&lt;&#x2F;p&gt;
&lt;pre class=&quot;giallo&quot; style=&quot;color: #E1E4E8; background-color: #24292E;&quot;&gt;&lt;code data-lang=&quot;shellscript&quot;&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span style=&quot;color: #B392F0;&quot;&gt;$&lt;&#x2F;span&gt;&lt;span style=&quot;color: #9ECBFF;&quot;&gt; chmod + x myscript.py&lt;&#x2F;span&gt;&lt;&#x2F;span&gt;&lt;&#x2F;code&gt;&lt;&#x2F;pre&gt;
&lt;p&gt;如果是在 Windows 下，副檔名存成 .pyw 可以防止醜醜的 console &lt;del&gt;氣到&lt;&#x2F;del&gt;跳出來。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;can-kao-zi-liao&quot;&gt;參考資料&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;docs.python.org&#x2F;zh-tw&#x2F;2.7&#x2F;tutorial&#x2F;interpreter.html#source-code-encoding&quot;&gt;使用 Python 直譯器&lt;&#x2F;a&gt;〉&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>我想做好人</title>
        <published>2012-03-20T00:00:00+00:00</published>
        <updated>2012-03-20T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2012/human-being/"/>
        <id>https://editor.leonh.space/2012/human-being/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2012/human-being/">&lt;p&gt;近來很忙，而且充斥著憂慮，在自己的業務已經忙得不可開交的情況下，卻又不願意讓自己的心態沈淪到不顧他人死活的心態。&lt;&#x2F;p&gt;
&lt;p&gt;我加班，但我反對加班；我接任務流彈，但我不丟任務流彈，在這樣的性格驅使下，必然會攔截到他人丟向我下屬的任務流彈，可想而知工作負擔只會日趨沈重。&lt;&#x2F;p&gt;
&lt;p&gt;我瞭解作為一個管理人應以大我為重，在業務導向下，犧牲小我或是忽略系統性的錯誤，但我無法冷漠、無法沒有正義感。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>iPhone</title>
        <published>2011-12-16T00:00:00+00:00</published>
        <updated>2011-12-16T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2011/iphone/"/>
        <id>https://editor.leonh.space/2011/iphone/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2011/iphone/">&lt;blockquote&gt;
&lt;div class=&quot;quote&quot;&gt;
&lt;p&gt;蘋果讓你換新 iPhone，是因為你手上的舊 iPhone 讓你很滿意。&lt;br &#x2F;&gt;
Google 讓你換新 Android 手機，是因為你手上舊 Android 手機讓你不滿意！&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
&lt;div class=&quot;source&quot;&gt;
&lt;p&gt;《&lt;a rel=&quot;external&quot; href=&quot;http:&#x2F;&#x2F;differentapple.blogspot.com&#x2F;2011&#x2F;12&#x2F;android-appios.html&quot;&gt;老人與蘋果&lt;&#x2F;a&gt;》&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
&lt;&#x2F;blockquote&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>完美主義作祟</title>
        <published>2011-12-15T00:00:00+00:00</published>
        <updated>2011-12-15T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2011/perfectionism/"/>
        <id>https://editor.leonh.space/2011/perfectionism/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2011/perfectionism/">&lt;p&gt;我一直很受不了，助理或秘書操作 Office 的能力比我還差，特別是對一份文件的邏輯性而言。&lt;&#x2F;p&gt;
&lt;p&gt;為什麼要用錯誤的軟體編輯錯誤的文件呢？&lt;&#x2F;p&gt;
&lt;p&gt;試算表雖然格子很多但絕不表示它適合拿來畫表單；簡報軟體雖然有繪圖功能但絕不表示它適合拿來作圖；以上兩種軟體雖然都可以打字但絕不表示它們適合拿來打文稿。&lt;&#x2F;p&gt;
&lt;p&gt;為什麼堂堂一個專職的助理連儲存格內斷行都不會呢？為什麼要寄出去的表格格式不弄整齊呢？為什麼哪個字體不用偏用 Comic Sans 呢？為什麼不知道中英文字體要分開設呢？為什麼每天在打文件卻不懂得使用樣式的功能呢？為什麼要把原圖硬塞進文稿內呢？為什麼做表單時都不考慮到將來打單人的編輯性呢？為什麼已經有格線了妳還要加一列＊號分隔呢？（活在 DOS 時代嗎？）；為什麼妳的中文永遠只有細明體跟標楷體呢？為什麼不知道等寬字與比例字呢？&lt;&#x2F;p&gt;
&lt;p&gt;為什麼我的完美主義老是在我心頭作祟呢？&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>過不去</title>
        <published>2011-11-24T00:00:00+00:00</published>
        <updated>2011-11-24T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2011/hard/"/>
        <id>https://editor.leonh.space/2011/hard/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2011/hard/">&lt;blockquote class=&quot;&quot;&gt;
&lt;div class=&quot;quote&quot;&gt;
&lt;p&gt;別和小人過不去&lt;br &#x2F;&gt;
因為他本來就過不去&lt;&#x2F;p&gt;
&lt;p&gt;別和社會過不去&lt;br &#x2F;&gt;
因為你會過不去&lt;&#x2F;p&gt;
&lt;p&gt;別和自己過不去&lt;br &#x2F;&gt;
因為一切會過去&lt;&#x2F;p&gt;
&lt;p&gt;別和往事過不去&lt;br &#x2F;&gt;
因為它已經過去&lt;&#x2F;p&gt;
&lt;p&gt;別和現實過不去&lt;br &#x2F;&gt;
因為你還要過下去&lt;&#x2F;p&gt;
&lt;p&gt;別和親人過不去&lt;br &#x2F;&gt;
因為他們會不讓你過去&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
&lt;&#x2F;blockquote&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Motto</title>
        <published>2011-11-20T00:00:00+00:00</published>
        <updated>2011-11-20T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2011/motto/"/>
        <id>https://editor.leonh.space/2011/motto/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2011/motto/">&lt;blockquote&gt;
&lt;p&gt;Every obstacle is a steppingstone to success. You should view problems in your life as opportunities to prove yourself.&lt;&#x2F;p&gt;
&lt;&#x2F;blockquote&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>衝動</title>
        <published>2011-11-16T00:00:00+00:00</published>
        <updated>2011-11-16T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2011/kill/"/>
        <id>https://editor.leonh.space/2011/kill/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2011/kill/">&lt;p&gt;每每遇到不順心又令人惱怒的事，我的心內就會浮現出衝動的性格，所幸還可以用不管是發洩、冷靜、轉移等方式暫時的排除掉，真的只是暫時的…。通常這樣的情緒是對人不對事，總是忍受不了某些人性格上的重大瑕疵，除非他們離開我的生活圈（或是我離開，如果可以的話），否則我真的很害怕哪一天會對他們衝動。&lt;&#x2F;p&gt;
&lt;p&gt;廣義的來講，不只是對其他人，對自己也是，對於如此的「重大缺點」，我也很害怕會受不了自己。&lt;&#x2F;p&gt;
&lt;p&gt;把這種事寫出來某種程度上代表它的嚴重性，通常會寫在這裡的都是在心中經過一定程度發酵的歷程或想法，不是那種國中生：「我好想扁 XXX 喔哈哈」般的語調，是很真實的、不顧一切的衝動。&lt;&#x2F;p&gt;
&lt;p&gt;或者呼應第二段，這麼冀望理想化世界的我明白不可能改變一切，那只好改變自己，不能沉淪的第二條路就是當個 escaper。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>如何分享網路</title>
        <published>2011-11-11T00:00:00+00:00</published>
        <updated>2011-11-11T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2011/internet-connection-sharing/"/>
        <id>https://editor.leonh.space/2011/internet-connection-sharing/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2011/internet-connection-sharing/">&lt;p&gt;今日在朋友分享給我的 buzz 中有一則〈&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;blog.miniasp.com&#x2F;post&#x2F;2010&#x2F;11&#x2F;06&#x2F;turn-windows-7-into-wireless-access-point&quot;&gt;如何將 NB 的無線網路變成可分享 Wi-Fi 的 AP 模式 (Ad-Hoc)&lt;&#x2F;a&gt;〉，不過，其實這世界上還有更平易近人的做法，並且不限於透過單一界面，每一台 Mac 上的基本配備—WiFi、乙太網路 、藍牙（還有 FireWire），只要其中一種界面可以上網，都可以任意分享到其它任一界面。&lt;&#x2F;p&gt;
&lt;ol&gt;
&lt;li&gt;首先進入系統偏好設定。&lt;&#x2F;li&gt;
&lt;li&gt;進入共享。&lt;&#x2F;li&gt;
&lt;li&gt;選擇 Internet 共享，來源即現在已經有網路的界面，下方則是欲分享的界面，例子就是最典型的從乙太網路分享到 WiFi。&lt;&#x2F;li&gt;
&lt;li&gt;如果要設密碼的話則再選 AirPort 選項，在這裡可以設定名稱和密碼。&lt;&#x2F;li&gt;
&lt;li&gt;最後，對 Internet 共享打個勾就好了！&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
&lt;p&gt;「就這麼簡單？」就這麼簡單，比 OGUMA 水美媒還簡單！&lt;&#x2F;p&gt;
&lt;iframe src=&quot;https:&#x2F;&#x2F;www.youtube-nocookie.com&#x2F;embed&#x2F;9dGK_IWBV_0&quot; title=&quot;YouTube video player&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>冷水澡的秘密</title>
        <published>2011-10-17T00:00:00+00:00</published>
        <updated>2011-10-17T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2011/cold-water-shower/"/>
        <id>https://editor.leonh.space/2011/cold-water-shower/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2011/cold-water-shower/">&lt;p&gt;到這裡後一直以來都洗冷水澡，天冷時亦同，這是我對我自己的一個小小地挑戰，或者說警惕，或者懲罰。效法臥薪嘗膽的精神（？），都怪自己當初識人（房東）不清，未深思熟慮就草率簽約，讓自己身受其害，所以對自己略施薄懲，每次洗澡時都會記得告誡自己，以後不可以再如此地輕率，另外這樣地修煉附加得好處是每次可增強對直銷洗腦的防禦力 + 3 :p。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>嘈嘈切切錯雜彈</title>
        <published>2011-10-02T00:00:00+00:00</published>
        <updated>2011-10-02T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2011/noise/"/>
        <id>https://editor.leonh.space/2011/noise/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2011/noise/">&lt;p&gt;這個禮拜以來轉職的念頭不斷在腦海中鼓搗著，源自於我對老闆和體制的失望，然而我當然也不會很天真地認為別的組織就不會有這樣的昏君和體制存在，「一切都是比較級」，某人如是說，而這麼沒完沒了的鼓搗，近乎到胡思亂想的地步，我想試著以這一篇小小的短文，釐清自己的想法並做自己可以接受（或忍受）的決定。&lt;&#x2F;p&gt;
&lt;p&gt;不過要先打一下臉，上次離職前也曾經寫下三件事，並訂下當三件事都完成後就可放心離職，豈知寫完沒多久就提離職了… :p。不過也就像我一貫的態度，無所謂後不後悔，只是想說「嗯…以後還是照計畫走比較好」。追根究柢，就是氣急攻心、沉不住氣。對照上次的經驗，這次的心情也是相當類似，連時間點都很像就是一年半載多。&lt;&#x2F;p&gt;
&lt;p&gt;雖然造就這種心情的原因不同，在現在的環境中我得不到任何制式的資源和支援，一切只能靠同事間的「友情支援／資源」，毫無章法，更深層的原因就不探究了，以免洗筷來比喻吧！我們就像免洗員工，不需要精雕細琢、沒有精美的包裝，只要能撐過這一頓飯，或是你能力、經驗強一點，能撐過許多頓飯，總之，髒了、主人飽了，被丟棄就是你的宿命。&lt;&#x2F;p&gt;
&lt;p&gt;再回到對照上次的經驗，能沉住氣，不恣意妄為，等待好的時機（隱晦說法，就是領完年終的時機 :p），將來的我會感覺比較良好，這就是我這次的策略，儘管在一個狗屁倒灶事一堆的荒謬世界裡，我也要試著心平氣和的過完這半年。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>工作反省</title>
        <published>2011-09-30T00:00:00+00:00</published>
        <updated>2011-09-30T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2011/introspection/"/>
        <id>https://editor.leonh.space/2011/introspection/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2011/introspection/">&lt;p&gt;延宕已久的風採購案，終於我在這兩天對老闆妥協了，但是對於老闆前後不一、和採購推諉其事的態度的不滿還是在心中累積沒有消去。不過幸好維持住的理性讓我也看到 PM 在這件事上圓融地處理方式，這點我該跟他學習。&lt;&#x2F;p&gt;
&lt;p&gt;再把時間推回前幾天，在反覆檢閱自己履歷內的自傳時，也意識到處事圓融正是我所欠缺的，往往就是主觀意識作祟（即便真的是對的），跟對方據理力爭。還記得大二時也曾經對「人和」這檔事自我反省過，其實和這次的歷程很相似，大概時間久了又變的狂妄起來了吧！或許這就是我的本性，不過慶幸的是，自我反省也是我的本性，特別是經歷越多事，越覺得對與錯的界線越來越模糊，自省是一種獨立思考的過程，讓我可以在模糊的地帶中找出一些分野。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>筆電電池或充電器</title>
        <published>2011-04-04T00:00:00+00:00</published>
        <updated>2011-04-04T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2011/battery-or-charger/"/>
        <id>https://editor.leonh.space/2011/battery-or-charger/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2011/battery-or-charger/">&lt;p&gt;本文試圖從眾多電腦廠商的電力方案中，找出最佳解。&lt;&#x2F;p&gt;
&lt;p&gt;首先，我們必須先思考，消費者要的是什麼，我們當然可以天馬行空的想著超大範圍的無線供電方案之類的夢想，不過從務實考量，在現有的科技水平下，以筆電這樣的行動應用，除了續航力，重量這檔事顯然也必須被納入考量，任何人都可以理解，越長的續航力，通常意味著越重的重量，兩者之前的平衡也就是我們關注的焦點。&lt;&#x2F;p&gt;
&lt;p&gt;在比較之前先建立假設條件：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;一顆電池續航力四小時&lt;&#x2F;li&gt;
&lt;li&gt;一顆尿袋續航力四小時&lt;&#x2F;li&gt;
&lt;li&gt;一台電腦內必然有一電池&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;為了方便起見，假設電池、尿袋、變壓器三者重量相同。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;qing-jing-yi-lian-xu-shi-yong-si-xiao-shi&quot;&gt;情境一：連續使用四小時&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;方案 Ａ：只帶電腦&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;在情境一中方案A就是最佳解，兼顧了續航力與重量，再多帶任何的設備都是多餘的。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;qing-jing-er-lian-xu-shi-yong-ba-xiao-shi&quot;&gt;情境二：連續使用八小時&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;方案 Ａ：電腦＋額外一顆電池&lt;&#x2F;li&gt;
&lt;li&gt;方案 Ｂ：電腦＋充電器&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;乍看之下兩個方案好像沒有差別，實際上方案 B 是較佳解，因為方案 Ａ 必須經過重新開機才可以再次使用，相較之下方案 Ｂ 明顯較為方便。&lt;&#x2F;p&gt;
&lt;p&gt;方案 B 的缺點是有可能找不到插座的風險，然而在本人實際在外工作的經驗來看，在台灣連續四個小時都找不到插座的機率是零，當然，若是在深山出差則另當別論，像這樣的特殊情況就要另行斟酌。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;qing-jing-san-lian-xu-shi-yong-shi-liu-xiao-shi&quot;&gt;情境三：連續使用十六小時&lt;&#x2F;h2&gt;
&lt;ul&gt;
&lt;li&gt;方案Ａ：電腦＋額外電池＋額外電池＋額外電池&lt;&#x2F;li&gt;
&lt;li&gt;方案Ｂ：電腦＋充電器&lt;&#x2F;li&gt;
&lt;li&gt;方案Ｃ：電腦＋額外電池＋額外電池＋尿袋&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;看起來不太會發生的情況，不過若是搭飛機是有可能的，在這樣的特殊情境中，考量到重量，顯而易見的方案 B 是最佳解，相信沒有人會在有限的隨身行李中再塞三顆電池吧，不過缺點是可能搭到沒插座的飛機，解決方式是訂位前先上網搜尋該班機有沒有提供插座，或者是採用方案 A 或 C。&lt;&#x2F;p&gt;
&lt;p&gt;比較過幾種情境後可以得出在大部份的情況下方案 B 通常是較佳解，那麼既然反正都只用電腦內的單顆電池，不如就取消行知有年的更換電池設計換取更多的蓄電力如何？&lt;&#x2F;p&gt;
&lt;p&gt;「不！」有人應道。&lt;&#x2F;p&gt;
&lt;p&gt;「你要知道電池是會老化的！」沒錯耶！那我更正一下，取消電池的模組化設計，電池依然可更換，只是必須由廠商進行（不論是原廠還是副廠），而節省下來的空間，就拿來增加蓄電量，這樣如何？而且自己買額外的電池也是要錢的啊！&lt;&#x2F;p&gt;
&lt;p&gt;「不！我堅持可更換電池模組的設計」從上面的情境假設我們已經可以預想到多代電池與多帶充電器比起來，充電器顯然是較佳的方案，而且隨著連續使用的時間越長，帶變壓器的優勢越為明顯（儘管不太可能發生連續使用超過十六個小時⋯）為什麼你要保留一個你不太可能會用到的設計（而捨棄直接幫你增加蓄電量的好處）呢？&lt;&#x2F;p&gt;
&lt;p&gt;「不！我不管！不～～～（理智斷線）」⋯⋯⋯。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>幫小忙</title>
        <published>2010-12-11T00:00:00+00:00</published>
        <updated>2010-12-11T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2010/moving/"/>
        <id>https://editor.leonh.space/2010/moving/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2010/moving/">&lt;p&gt;今天本來的讀書計劃被打亂，一切始於半夜的一通電話，原本規劃好的讀書行程突然變成幫蔡女士搬家，其實是件簡單的差事，只有幾包細軟物吧，大型傢具應該是另有他人代勞，然後因為我自認怕與她打交道，一路上只好一直裝酷 = =+，俗話說的好：「跟蔡女士在一起才知道什麼叫言多必失」，沈默跟放空這兩個大絕此時不用待何時，您說是唄？&lt;&#x2F;p&gt;
&lt;p&gt;至於原本安排的讀書計劃呢…改念比較不花時間乙單元，到晚上八點半剛好看完預計的部分，總計今天有看到書，又有日行一善到，爽！&lt;&#x2F;p&gt;
&lt;p&gt;最後補充一下近來的生活感想：絕對不要再跟特種部隊講太多我的事，腦殘是沒藥醫的。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>去了天龍國一趟</title>
        <published>2010-11-30T00:00:00+00:00</published>
        <updated>2010-11-30T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2010/a-dating-in-taipei/"/>
        <id>https://editor.leonh.space/2010/a-dating-in-taipei/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2010/a-dating-in-taipei/">&lt;p&gt;本來不打算寫的，因為就是很公式化的行程:p。&lt;&#x2F;p&gt;
&lt;p&gt;跟小賴小姐進行了大概每年一次的小聚會，吃了一間在板橋新站上面叫雞ｘ的偽日料店，很貴而且很鹹（據說日本都吃很鹹所以它其實很道地？），我點的 XXX 飯（忘了）倒是不錯吃，還挺豪華的，就是生魚片＋明太子＋生蛋蓋飯，另外那位小姐的 XX 茶泡飯則是貧乏的可憐，小菜的烤小豬肉串也是鹹，還有豬臊味⋯，整體評價不優。&lt;&#x2F;p&gt;
&lt;p&gt;然後跑去看一部令人很悲傷又很沈重的電影（是沈重不是沈悶！），醫院和舞廳的刻意對比很高明，高明到我一點都感受不到舞廳的歡愉，除此之外該露的都露了，即便如此，還是很厲害啊我擺脫不了那種莫名的悲傷的情緒，這片就算沒露也應該嚴格列為限制級。真是糟透了，不是電影糟透了，是看完會讓你覺得這一切糟透了！&lt;&#x2F;p&gt;
&lt;p&gt;補充一下賴小姐的本日 look（去了天龍國故意穿插點英文也是很合理的），那個髮色真是適合她到不行！在此公開表揚，另外跟她在一起不用一直喧譁這一點很棒，我也還是用我那公式化（不過並不是敷衍）的答案回她每次都會問我的「怎麼還不交女朋友」的問題。（希望我有辦法參加她的婚禮）&lt;&#x2F;p&gt;
&lt;p&gt;總體來說是很不錯的一天，高鐵很快、吃的還可以、電影很沈重⋯、跟她在一起不用為了聊天而聊天、她聰明又成熟，也很漂亮，蛋糕也很好吃 :]。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>疑？</title>
        <published>2010-11-25T00:00:00+00:00</published>
        <updated>2010-11-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2010/feded/"/>
        <id>https://editor.leonh.space/2010/feded/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2010/feded/">&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2010&#x2F;feded&#x2F;fedex_desktop-md.png&quot; alt=&quot;FedEx&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：FedEx&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;嗯⋯桌面上有 Firefox、Thunderbird、Skype、FileZilla⋯可是疑～？那不是 dock 嗎？上面也有 menu bar，桌布也很像 Mac 的，可是圖示又在左邊，電腦還是 Dell 的！案情有點不單純喔！&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>糟透了</title>
        <published>2010-11-23T00:00:00+00:00</published>
        <updated>2010-11-23T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2010/terrible/"/>
        <id>https://editor.leonh.space/2010/terrible/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2010/terrible/">&lt;p&gt;工作是如此的不順遂。&lt;&#x2F;p&gt;
&lt;p&gt;講一件開心的事：田園牛肉堡超好吃的啦，有大量的生洋蔥和蔬菜可以蓋掉太過的肉味 :]。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>PCManFM 真是好物∼</title>
        <published>2010-11-22T00:00:00+00:00</published>
        <updated>2010-11-22T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2010/pcmanfm/"/>
        <id>https://editor.leonh.space/2010/pcmanfm/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2010/pcmanfm/">&lt;p&gt;話說因為某些因素想改一整個資料夾及其下的資料夾和檔案的權限，然而 Nautilus 卻辦不到，最後尋尋覓覓終於找到了台灣之光 PCMan 大的 PCManFM，真是輕薄短小卻功能強大啊啊！&lt;&#x2F;p&gt;
&lt;p&gt;有人提出質疑：「開終端機寫 shell script 就好啦～？！」&lt;&#x2F;p&gt;
&lt;p&gt;因為我不爽啊，為什麽一個以個人桌面為主的發行版還要我開終端機才能作到我的要求？Linux 若想從伺服端攻向桌面端就要拋棄那些固有的作法。&lt;&#x2F;p&gt;
&lt;p&gt;「Linux 才不是為了打敗誰才存活的呢！」（傲嬌狀）&lt;&#x2F;p&gt;
&lt;p&gt;哦隨便啊繼續活在追求資訊自由化的理想世界吧～。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>攻城屍之第一次寄貨就上手！</title>
        <published>2010-11-21T00:00:00+00:00</published>
        <updated>2010-11-21T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2010/fedex/"/>
        <id>https://editor.leonh.space/2010/fedex/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2010/fedex/">&lt;p&gt;FedEx 快遞技能取得。&lt;&#x2F;p&gt;
&lt;p&gt;還有很多…刷漆技能、叫水電技能、送貨小弟技能、殺價技能、找廠商技能等等等等等…族繁不及備載，有空再來細細品味…。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>機瘟</title>
        <published>2010-11-20T00:00:00+00:00</published>
        <updated>2010-11-20T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2010/malfunction/"/>
        <id>https://editor.leonh.space/2010/malfunction/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2010/malfunction/">&lt;p&gt;慘…首先是我負責的機台莫名其妙掛點，加上原本的電腦會自己斷電，慘上加慘的是剛剛筆電的螢幕也掛了…大哭 ~~~&amp;gt;“&amp;lt;。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>不靠譜</title>
        <published>2010-11-19T00:00:00+00:00</published>
        <updated>2010-11-19T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2010/class/"/>
        <id>https://editor.leonh.space/2010/class/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2010/class/">&lt;p&gt;這講師真的很不靠譜，一直說工研院是國家校正單位⋯。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Cam 無限驅動機能滿載</title>
        <published>2010-11-18T00:00:00+00:00</published>
        <updated>2010-11-18T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2010/asus-j210-cam-driver-on-linux/"/>
        <id>https://editor.leonh.space/2010/asus-j210-cam-driver-on-linux/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2010/asus-j210-cam-driver-on-linux/">&lt;p&gt;天啊這支超冷門的 ASUS J210（其實型號不重要，總之是聯發科科的晶片）手機上的小小 cam 竟然能在 Linux Mint 10 下無痛驅動啊！（印象中在 Windows 下是要灌驅動的啊…。）&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>REBORN</title>
        <published>2010-11-17T00:00:00+00:00</published>
        <updated>2010-11-17T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2010/reborn/"/>
        <id>https://editor.leonh.space/2010/reborn/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2010/reborn/">&lt;p&gt;從開機選單進 Recovery Mode 它會自己跑 fsck，找到壞磁區的話會問你要不要修，全部給它修完 Linux 就重生啦 ^O^！&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Linux Mint 再起不能</title>
        <published>2010-11-16T00:00:00+00:00</published>
        <updated>2010-11-16T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2010/linux-mint/"/>
        <id>https://editor.leonh.space/2010/linux-mint/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2010/linux-mint/">&lt;p&gt;本來利用這個週末花了點時間抓了剛出爐的 Linux Mint 10 來裝一下。&lt;&#x2F;p&gt;
&lt;p&gt;Linux Mint 10 確實不錯，桌面用的 Linux 越來越進步了！雖然我真正期待的是 Linux Mint Debian Editon 64-bit，不過這些都不是重點，重點是灌好宅的正爽的時候突然電就給我跳掉關機了？！&lt;&#x2F;p&gt;
&lt;p&gt;然後試了三次，一樣，都是在開機選單完到啓動畫面這段期間斷電關機，然後想說進 Windows 看看會不會也是一樣，結果沒事⋯正常，趕緊上網爬一下文，歸納一下大概可能的原因：灰塵太多、電源供應器不穩、板子不穩、零組件漏電⋯。大概有個底之後想說再開到 Linux 看看，結果正常，只是開不了機⋯停在啓動畫面⋯看來不過斷電幾次就掛點再起不能⋯這樣的自瘉能力有點差咧⋯！&lt;&#x2F;p&gt;
&lt;p&gt;總之明天先把灰塵清一下，然後開始觀察⋯觀察。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Windows 7 的開始的所有程式在磁碟內的實際位置</title>
        <published>2010-11-15T00:00:00+00:00</published>
        <updated>2010-11-15T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2010/windows-7-start-menu-folder/"/>
        <id>https://editor.leonh.space/2010/windows-7-start-menu-folder/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2010/windows-7-start-menu-folder/">&lt;p&gt;剛剛發現所有程式（也就是以往的程式集）裡面遺留了一個應該已經被移除但卻殘留下來的空資料夾，這在以前的 Windows XP 只要很簡單的按右鍵刪除就好，想重施故技才又發現這招在 Windows 7 下是行不通的（Windows Vista 好像也是），於是又開始爬文了…這次的任務很好找，請出 Google 用關鍵字「“windows 7” 程式集 資料夾」，大略看一下，中文沒有相關的網頁，於是改用英文下「“windows 7” start menu folder」，第一頁就有許多相關的網頁，包括維基百科。&lt;&#x2F;p&gt;
&lt;p&gt;所有程式實際所在的位置：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;x:\Users\username\AppData\Roaming\Microsoft\Windows\Start Menu&lt;&#x2F;li&gt;
&lt;li&gt;x:\ProgramData\Microsoft\Windows\Start Menu&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;最後我還要很佛心的把這些加到維基百科中文版的條目裡面去！然後我發現中文條目人家早就有寫了… =“=。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>CQT 乙單元真是個騙錢的玩意</title>
        <published>2010-11-14T00:00:00+00:00</published>
        <updated>2010-11-14T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2010/cqt/"/>
        <id>https://editor.leonh.space/2010/cqt/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2010/cqt/">&lt;p&gt;ISO 9000 也是。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>就沒有懸念了嗎？</title>
        <published>2010-11-12T00:00:00+00:00</published>
        <updated>2010-11-12T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2010/long-time-no-see/"/>
        <id>https://editor.leonh.space/2010/long-time-no-see/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2010/long-time-no-see/">&lt;p&gt;「好久不見。」&lt;&#x2F;p&gt;
&lt;p&gt;然後就此毫無懸念了嗎？也不再有所期待了嗎？&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>謝謝妳們的關心 &gt;&#x2F;&#x2F;&#x2F;&lt;</title>
        <published>2010-11-11T00:00:00+00:00</published>
        <updated>2010-11-11T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2010/sweet-caring/"/>
        <id>https://editor.leonh.space/2010/sweet-caring/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2010/sweet-caring/">&lt;p&gt;話說昨天生了一場大病，讓我在一天之內陸續被插了四針，也讓我首次體驗到靜脈注射和肌肉針這兩種鬼玩意…打點滴的手背到現在都有一點點痛 ~&amp;gt;&amp;lt;。&lt;&#x2F;p&gt;
&lt;p&gt;回歸正題，我想偷偷地在這個小園地感謝昨天很關心我的那些姊姊們，有妳們的支持才得以支撐到半夜啊！（我挺）。另外再特別感謝創意總監，多虧了他我才有機會小睡一番 :p。&lt;&#x2F;p&gt;
&lt;p&gt;然後話又說回來，像打落水狗般的在我疲憊不堪的時候逼我加班到半夜的那位阿拉伯仁兄，我不會殘害你你放心，只是你的事情的優先權會被我排到很後面∼很後面，祝你好運不要出紕漏喔∼啾咪 &amp;gt;_*！&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>老實</title>
        <published>2010-11-06T00:00:00+00:00</published>
        <updated>2010-11-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2010/handsome/"/>
        <id>https://editor.leonh.space/2010/handsome/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2010/handsome/">&lt;p&gt;兩棲：「ㄟ剛剛那個就是你客戶喔？BMW？」&lt;&#x2F;p&gt;
&lt;p&gt;高作：「對啊怎樣帥嗎？」&lt;&#x2F;p&gt;
&lt;p&gt;兩棲：「我幹嘛看他帥不帥啊⋯好啦沒你帥啦」（笑）&lt;&#x2F;p&gt;
&lt;p&gt;高作：「哦是喔妳講話幹嘛那麼老實啊！！」（笑）&lt;&#x2F;p&gt;
&lt;p&gt;兩棲：「ㄟ你再這樣我受不了囉！！」（笑開懷）&lt;&#x2F;p&gt;
&lt;p&gt;高作：「我才要受不了咧！」&lt;del&gt;（科科）&lt;&#x2F;del&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>京片子</title>
        <published>2010-11-05T00:00:00+00:00</published>
        <updated>2010-11-05T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2010/38/"/>
        <id>https://editor.leonh.space/2010/38/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2010/38/">&lt;p&gt;今天不小心在同事面前露出我三八的一面，應該不會被討厭吧？:p&lt;&#x2F;p&gt;
&lt;p&gt;然後⋯越深入的想，越覺得待在這邊不錯，不過後來仔細分析後發現，主要是因為近來都沒跟那個誰誰誰共事，還有就是那位很精實的阿拉伯的誰誰誰人不在國內的關係。&lt;&#x2F;p&gt;
&lt;p&gt;不過可以預見下禮拜將會很忙碌，因為那恐怖的阿拉伯的誰誰誰要回來了！:(&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>小事兩則</title>
        <published>2010-11-04T00:00:00+00:00</published>
        <updated>2010-11-04T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2010/2009-11-02-diary/"/>
        <id>https://editor.leonh.space/2010/2009-11-02-diary/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2010/2009-11-02-diary/">&lt;p&gt;工作上那些煩心的事就不提了，分享小事兩則。&lt;&#x2F;p&gt;
&lt;p&gt;其一是我終於又找到喜歡的髮婆了！不過基於洗頭時會一直聞到小妹吐出的煙味讓我很想吐，而且收費太貴（$500 就很貴了），故不打算再度光顧。&lt;&#x2F;p&gt;
&lt;p&gt;其二是傳聞中的 Tina 麵包很好吃這件事終於在昨日獲得證實 :)，不過也挺貴的，大概是 80 元這附近價位出現的最多，有閒錢再吃。&lt;&#x2F;p&gt;
&lt;p&gt;報告一下我一般的一餐消費，其實就是中餐，40 塊，不多不少。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>被加薪了！</title>
        <published>2010-11-03T00:00:00+00:00</published>
        <updated>2010-11-03T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2010/raising/"/>
        <id>https://editor.leonh.space/2010/raising/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2010/raising/">&lt;p&gt;可是並沒有很開心，也並沒有影響我年後想換的意願。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>南崁單車道</title>
        <published>2010-05-25T00:00:00+00:00</published>
        <updated>2010-05-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2010/riding/"/>
        <id>https://editor.leonh.space/2010/riding/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2010/riding/">&lt;p&gt;趁著今天留守的機會，又再度嘗試了騎腳踏車上班的路線。&lt;&#x2F;p&gt;
&lt;p&gt;七點出門到大馬路上時，汽機車已經有一些了，如果是平常日的話那還更多，第二個點是振聲，巷弄很小又很多學生，又有腦殘人士出沒，或許改走秀山路？&lt;&#x2F;p&gt;
&lt;p&gt;從家裡到玉山街平交道這次只花了 20 分鐘，記得上次花了 30 分鐘，最後一段進入南崁工業區大約八點多一些，車輛就多的跟平日差不多多，還可以悠閒的停在早餐店吃個早餐再到公司 ^^，總計整趟路程算下來約一個小時多一點，換算成平日的話大概還要再多個 10 分鐘，而且越晚出門車況越差，所以儘管七點多出門趕得及打卡，最好還是在七點前出門較為妥當。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;yi-fu-he-bao-bao&quot;&gt;衣服和包包&lt;&#x2F;h2&gt;
&lt;p&gt;側背著筆電騎車有點痛苦，它那個不是很好的背帶弄的我有點不舒服，上下車又會被包包卡到，不過都還算輕微可以忍受。衣服有點糟 &amp;gt;&amp;lt;，一兩個小時都不會乾，味道倒是還好，不過自己聞不準 :p，如果改成背後背式的話應該還不錯，缺點是汗只怕會流更多。&lt;&#x2F;p&gt;
&lt;p&gt;最後一段的路程，過了高速公路之後沿著南崁畔的單車道，能夠徹底擺脫汽機車的單車道騎起來真的很暢快，視野遼闊，高低起伏，還有左右兩邊規劃出來的綠地。不過看那些草皮的長度大概可以想見使用率非常的低，可惜了…。&lt;&#x2F;p&gt;
&lt;p&gt;大概是延續昨晚的好心情，這一路騎來非常暢快！&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>小宋聚餐</title>
        <published>2010-05-23T00:00:00+00:00</published>
        <updated>2010-05-23T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2010/eating-together/"/>
        <id>https://editor.leonh.space/2010/eating-together/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2010/eating-together/">&lt;p&gt;我一定要在忘記前把今天記下來！&lt;&#x2F;p&gt;
&lt;p&gt;今天絕對是我今年以來最開心的一天！雖然說每一次的聚餐就代表著可能又有人要離開鋐鑫了，這也意味著在未來要聚在一起更加困難…，儘管我做了一些很瞎的事，都絲毫無法減弱這整晚的快樂。熟的也好不熟的也好，我真的很開心能再看到妳們，我愛你們 :)。另外，小宋祝妳前程似錦，以後嫁給好老公！&lt;&#x2F;p&gt;
&lt;p&gt;這次聚餐的地方是坐忘茶坊，很棒！有包廂、有網路、不貴、服務態度又好、又開到凌晨兩點這麼晚，東西好吃，真的很不錯 ^^。&lt;&#x2F;p&gt;
&lt;p&gt;我都離職這麼久了每次聚在一起還是倍感親切，對比之下，茂太，我什麼時候才能融解你這巨大的冰山呢？每兩個月聚餐：$300、員工旅遊：$10,000、真正的朋友：無價。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>捐血</title>
        <published>2010-05-18T00:00:00+00:00</published>
        <updated>2010-05-18T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2010/blood-donation/"/>
        <id>https://editor.leonh.space/2010/blood-donation/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2010/blood-donation/">&lt;p&gt;好久沒捐血了，自從收到捐血十次的獎狀之後，其實之後有去捐過一次，不過很不湊巧那陣子拉肚子 :(，到自我評量那關就只好放棄，這個週末還是不行，恐怕要等到下個週末了…這次一定要去捐到！&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>雜談</title>
        <published>2010-05-15T00:00:00+00:00</published>
        <updated>2010-05-15T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2010/2010-05-13-diary/"/>
        <id>https://editor.leonh.space/2010/2010-05-13-diary/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2010/2010-05-13-diary/">&lt;p&gt;今天的第一件大事！下班後在桌上看到一封渣打寄來的信，裡面是我早已忘記何時參加的活動抽獎獎品－新光禮券兩百大洋！噹噹～！&lt;&#x2F;p&gt;
&lt;p&gt;其它的…嗯…下個月要去做一條褲子，還有要買安全帽，報名要上的課，這三項是比較要緊的，另外就是這週末要探勘騎腳踏車上班的路線，盡量設法避開夜市那段。&lt;&#x2F;p&gt;
&lt;p&gt;今天在跟 Joe 在車上聊天時，雖然我絕對不會主動提，不過被他問了我的感情事兒，依然維持我一貫的論調，我很確信在這幾年內不會交女朋友，也許我現在並不百分之百堅持以前所謂「已經經歷過最好的一次了」，但這依然是我的核心思想，另外再加上一些其它因素，至少在 2013 年前不會改變，至於更以後的事就以後再說吧，畢竟我不需要設立一個什麼原則來限制自己，人的想法會變的，這是再正常不過的事。&lt;&#x2F;p&gt;
&lt;p&gt;新聞上看到一個老婆婆自己在路邊賣口香糖，獨立撫養她植物人的兒子，覺得很心酸，我要幫助她們，不只是她們，包括任何一切的生命跟環境跟教育。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>跟誰玩？去哪玩？</title>
        <published>2010-05-14T00:00:00+00:00</published>
        <updated>2010-05-14T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2010/trip/"/>
        <id>https://editor.leonh.space/2010/trip/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2010/trip/">&lt;p&gt;今天他們開始熱烈的討論出國玩的行程，我也只能保持一貫的置身事外，這麼說來好像我有想去的意味？&lt;&#x2F;p&gt;
&lt;p&gt;不，並非如此。&lt;&#x2F;p&gt;
&lt;p&gt;如果針對這次出國玩而要精確的描述我的情緒的話，應該這麼說，我對於老闆的決定感到 70 分的不公平，但很矛盾的對想與同事一起出去玩的意向也只有 70 分，然後對在他們出國期間想放假的期望是 100 分！以上是內在指數，外在表現要再乘以一個降幅係數。&lt;&#x2F;p&gt;
&lt;p&gt;換言之，我有我內心的期望，不過我不會求你，如果你讓我感到不滿的話我也不會怨恨你，我會直接換個環境。（有沒有這麼大隱於市啊？）&lt;&#x2F;p&gt;
&lt;p&gt;出國玩一直是我心中存在已久的懸念，不過我一直認為跟誰出去玩比去哪裡玩重要的多，目前都不具備適合的條件，留在臺灣放大假應該是最好的選擇。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>終身學習</title>
        <published>2010-05-12T00:00:00+00:00</published>
        <updated>2010-05-12T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2010/learning/"/>
        <id>https://editor.leonh.space/2010/learning/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2010/learning/">&lt;p&gt;中原推廣的新簡章又發了，每到這個時候我都會對我未來有興趣的課程做一個全盤的規劃，來源除了桃園各大專院校的推廣教育之外，還有鄉鎮市民大學、救國團的課程、以及一些私人的補習班。&lt;&#x2F;p&gt;
&lt;p&gt;在查資料的期間發現到上次問過的某個 SolidWorks 課竟然消失了 =“=！不過又很幸運的找到另一個 SolidWorks 課程，而且很明顯的品質比原來的更棒，時間也更適合我。剩下的除了固定會有的一門英文課外，這次想嘗試德文，遺珠之憾是 CQT。話說這一切都是為了工作，否則我真的很想去上一些軟性的課程，學點小才藝調劑身心，又可以認識新同學，能這樣該有多好。&lt;&#x2F;p&gt;
&lt;p&gt;最後記一下，市民大學有在徵老師，這也是個機會。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>觀音亭、Costco</title>
        <published>2010-05-11T00:00:00+00:00</published>
        <updated>2010-05-11T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2010/guanyin-costco/"/>
        <id>https://editor.leonh.space/2010/guanyin-costco/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2010/guanyin-costco/">&lt;p&gt;今天跟阿璟和三個妹（？）一同去了觀音亭參拜，當然還順道拜訪了 Heidi，很開心許久不見的她門都過得很好，永遠都充滿活力的女生們總是能把我們逗的很開心 ^^。&lt;&#x2F;p&gt;
&lt;p&gt;一如以往的，還是抱著感謝的心情參拜祂們，感謝祂讓我擁有還可以的工作，這些超讚的同事，知己的朋友，親愛的家人，這些是我的肺腑之言，儘管沒有十全十美，但誰不是這樣子呢。我很驕傲自從高中物理老師讓我覺醒至今，不論是祭拜祖先或者神明，我都能懷抱著感激的心情參拜，自我的慾望靠自己去實現，也還是要答謝祂們給我的幫助，或者說機運。&lt;&#x2F;p&gt;
&lt;p&gt;最近對交往又有了新的領悟，即便這很可能又是我再一次的後知後覺。所謂交往，就是要付出彼此的時間，而且是心甘情願的、無條件的。至於愛，那只是交往的最基本條件。&lt;&#x2F;p&gt;
&lt;p&gt;第一次去了 Costco，很棒！很多好東西，不過如果把要年費的會員卡列入考慮，並不適合我現在的生活，除非等桃園店開了才值得再評估看看。另外也親身確認到了不同於其它大賣場的經營方式，我相信一旦辦了它們的會員卡，就不會再去其它大賣場消費，確實有它的魔力所在，這就是藍海。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>德國的空中交通管制機構</title>
        <published>2010-04-19T00:00:00+00:00</published>
        <updated>2010-04-19T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2010/dfs/"/>
        <id>https://editor.leonh.space/2010/dfs/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2010/dfs/">&lt;p&gt;這幾天因為冰島的火山爆發，我們的德國客人也順理成章的留在臺灣，為了他剛剛費了一點功夫去查德國機場的航班異動情形，從新聞上只能查到一些不完整的資訊，桃園機場飛往法蘭克福的航班全部取消，最後找到了 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.dfs.de&#x2F;dfs_homepage&#x2F;en&#x2F;&quot;&gt;DFS&lt;&#x2F;a&gt;，是相當於民航局的機構，不過是以公司的方式存在的，並且由政府 100% 持有，上面寫到「German airports closed until 20:00 hrs on Sunday」。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>iPad 與 M4&#x2F;3</title>
        <published>2010-04-06T00:00:00+00:00</published>
        <updated>2010-04-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2010/ipad-m4-3/"/>
        <id>https://editor.leonh.space/2010/ipad-m4-3/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2010/ipad-m4-3/">&lt;p&gt;批評 iPad 的觀點其實跟批評 M4&#x2F;3 的觀點很像，他們說 M4&#x2F;3 畫質比 SLR 差，又說體積比傻瓜機大，不如買一台 SLR／傻瓜機，典型的舊腦袋，它明明就是比 SLR 小很多，畫質比傻瓜機好很多的逸品。&lt;&#x2F;p&gt;
&lt;p&gt;iPad 也是一樣，舊腦袋的人說它沒實體鍵盤、不能接印表機、不能看 Flash、又比 Kindle 貴、又沒有用 E-ink，它明明就是比筆電輕巧、應用又比電子書豐富的行動裝置。至於跟 ActiveX 一樣垃圾的 Flash 不支援，我個人是很爽，就像當年 Firefox 不出來世人不知道 IE 有多爛一樣，沒有 Firefox 去吃 IE 的市佔率，恐怕到今日我等還只能活在 FrontPage + IE6 這兩個爛貨做出來的垃圾網頁世界裡面，那些網頁設計師非得要有人逼才會做出一點進步，講完了！&lt;&#x2F;p&gt;
&lt;p&gt;來平衡報導一下，我對蘋果不讓 Opera 上架這事很不爽，Safari 不是垃圾，只是 Opera 太棒了，這件事蘋果應該感到慚愧。&lt;&#x2F;p&gt;
&lt;p&gt;不過我不會買 iPad，因為不合需求，我的生活中沒有需要用到這種裝置的時機，我需要的是下一代的 iPhone，掌上型的行動裝置。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>網拍水餃</title>
        <published>2010-04-04T00:00:00+00:00</published>
        <updated>2010-04-04T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2010/dumplings/"/>
        <id>https://editor.leonh.space/2010/dumplings/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2010/dumplings/">&lt;p&gt;最近想做做看網拍水餃，除了買一些別人賣的來吃看看口味如何、包裝如何、配送如何之外，經由現有賣家的評價也可以評估網拍水餃到底賣得怎麼樣。&lt;&#x2F;p&gt;
&lt;p&gt;在網路上賣水餃我認為最大的障礙就在於運費，不同於一般商品，水餃是需要低溫配送的，看一下黑貓的價目表，低溫配送就是 150 起跳價，試想有多少客人願意多付這 150 運費來買到處也買得到的水餃餃？而且這 150 完全是被黑貓賺走，想壓低總價的話只有壓縮自己的利潤…初期量太少也是沒辦法談簽約的。&lt;&#x2F;p&gt;
&lt;p&gt;我在露天上隨便挑一間小家的賣家，看她的交易紀錄：&lt;&#x2F;p&gt;
&lt;div class=&quot;wide&quot;&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th style=&quot;text-align: center&quot;&gt;日期&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: center&quot;&gt;出貨量（粒）&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: center&quot;&gt;2008-10-13&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;450&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: center&quot;&gt;2008-10-13&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;210&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: center&quot;&gt;2008-10-29&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;450&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: center&quot;&gt;2008-11-06&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;450&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: center&quot;&gt;2008-12-01&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;210&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td style=&quot;text-align: center&quot;&gt;2009-02-25&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: center&quot;&gt;450&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;&#x2F;div&gt;
&lt;p&gt;大概半個月有人買一次，看來很難做的樣子，如果是本來就在賣，網路只是多一個通路沒差，可是對像我這樣靠進貨的，大概會死很慘…。&lt;&#x2F;p&gt;
&lt;p&gt;再看另一家，這家是 PTT 合購版上面的熱門商品，懶得做表格了，直接看出貨紀錄，靠試吃試賣衝了不少量，就算沒賠錢頂多只能打平，還是很難賺。&lt;&#x2F;p&gt;
&lt;p&gt;再換個角度想，印傳單拿去工廠發，做工廠生意倒是可行，選工廠是因為人多，比台北市那一堆小小的網路公司、貿易公司只有二三十個人容易成功的多，而且家庭主婦一買就是買一整家的量。&lt;&#x2F;p&gt;
&lt;p&gt;從長計議、從長計議！&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>IKEA 的小瓢蟲</title>
        <published>2010-03-13T00:00:00+00:00</published>
        <updated>2010-03-13T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2010/ikea/"/>
        <id>https://editor.leonh.space/2010/ikea/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2010/ikea/">&lt;p&gt;去IKEA買燈途中順手拍。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2010&#x2F;ikea&#x2F;4428350357_9fc4c93672_o-lg.jpg&quot; alt=&quot;IKEA 的小瓢蟲&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Avidemux</title>
        <published>2009-12-15T00:00:00+00:00</published>
        <updated>2009-12-15T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/avidemux/"/>
        <id>https://editor.leonh.space/2009/avidemux/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/avidemux/">&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;http:&#x2F;&#x2F;avidemux.org&#x2F;&quot;&gt;Avidemux&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;簡單好用的影片編輯軟體，自由軟體、跨平台、吃資源少、視訊音訊編碼夠用。&lt;&#x2F;p&gt;
&lt;p&gt;然而如果想有更豐富的剪輯功能，那可以考慮 &lt;a href=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2022&#x2F;blender-basic-1&#x2F;&quot;&gt;Blender&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Haihaisoft Universal Player</title>
        <published>2009-12-12T00:00:00+00:00</published>
        <updated>2009-12-12T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/haihaisoft-universal-player/"/>
        <id>https://editor.leonh.space/2009/haihaisoft-universal-player/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/haihaisoft-universal-player/">&lt;p&gt;電腦送來了，不免的要調校一下，在使用的過程中，遇到的一個老問題是播影片，以往我都是裝 RM Alternative、QT Alternative、FFmpeg 和 Media Player Classic 解決，可以應付大部分的影片，我個人是很討厭解碼包的，搞的不好就會有一些不知所謂莫名其妙的問題出現 =“=，所以這次我想找看看有沒有一個播放器是都內建的，不用去動到系統，特別是對 RealMedia 和 QuickTime 這兩者，最後的選擇就是 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;cn.haihaisoft.com&#x2F;%E6%B5%B7%E6%B5%B7%E8%BD%AF%E4%BB%B6%E5%85%A8%E8%83%BD%E6%92%AD%E6%94%BE%E5%99%A8.aspx&quot;&gt;HaiHaisoft Universal Player&lt;&#x2F;a&gt;，中文叫海海軟體全能播放器，也是以 Media Player Classic 為基礎開發的衍生品，也會自己掛字幕，缺點是不支援硬解，不過也沒差啦反正我不是效能狂也不是畫質狂，平均一個月頂多看一支影片吧，沒必要為這等小事耗費太多時間（雖然我現在時間很多 ~&amp;gt;”&amp;lt;）。&lt;&#x2F;p&gt;
&lt;p&gt;多寫一點騙些稿費（？），在 Mac 下 QuickTime 是原生的 :)，其它要裝的有 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.perian.org&#x2F;&quot;&gt;Perian&lt;&#x2F;a&gt; 解大部分的格式、Flip4Mac WMV 專門對付 Windows Media、RealPlayer 專門對付RealMedia，其實也挺多的 :(，特別鞭一下 RealPlayer for Mac，整個效率就是差，偏偏 RMVB 的格式又滿普遍的 :(。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>品牌電腦 VS. 組裝電腦</title>
        <published>2009-12-11T00:00:00+00:00</published>
        <updated>2009-12-11T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/computer/"/>
        <id>https://editor.leonh.space/2009/computer/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/computer/">&lt;p&gt;從現在使用的電腦往回算，我已經將近 10 年沒買過桌機了，包括現在用的 MacBook Pro 在內，連續三台都是筆電，從最早的華碩 12 吋那台開始（這台其實是接手我哥的），第二台是陪伴我最久的聯強牌筆電，對聯強自己出的筆電，印象是神達代工的，其實還挺不錯的，從大三撐到現在都還算正常，除了它的光碟機之外 :(。&lt;&#x2F;p&gt;
&lt;p&gt;回到桌機上，雖然這麼久沒買桌機，但為家裡的服役中的舊桌機添購一些配備是有的，顯卡、記憶體之類的，除了這些零組件之外，對整體桌機的行情就沒特別去了解了！而這次的起因為我哥要買電腦，考量到預算等因素之後決定添購桌機作為他租屋處的電腦。對於採購的方向，我個人偏好品牌電腦，這是我早期買電腦（桌機）時從未出現過的念頭，好像在台灣電腦不買組裝的就顯得功力不夠似的，非得要去光華商場繞個幾圈拿幾張報價單好好比較一番不可，品牌電腦（桌機）在家庭戶、個人戶之間一直不是主流的選擇，可是在幾年前的一段時間我漸漸思考品牌電腦的可取性，主要是我發現到以往品牌電腦比人家貴上一截的售價不復存在，至於其它的缺點－選擇不夠自由、零組件資訊不明、效能搾不出來等等，早就被我拋諸腦後，以致於後來被人徵詢買電腦的意見，我都建議去買品牌電腦這樣輕鬆打發 :p，也不用當什麼好人，真棒！&lt;&#x2F;p&gt;
&lt;p&gt;其實買電腦真的可以很輕鬆，不用非得把自己丟到光華去任人宰割自亂陣腳，買品牌電腦就是了，這點不論對新手或熟手皆然，很輕鬆，而且你沒有虧到。&lt;&#x2F;p&gt;
&lt;p&gt;這次買的是 Acer Aspire M3202，價格是 14,100。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>NeoOffice 小記</title>
        <published>2009-12-03T00:00:00+00:00</published>
        <updated>2009-12-03T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/neooffice/"/>
        <id>https://editor.leonh.space/2009/neooffice/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/neooffice/">&lt;p&gt;先來個題外話，iWork的Pages裡面，字體的配對是這樣的：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Helvetica 配黑體－繁&lt;&#x2F;li&gt;
&lt;li&gt;Times 配儷宋 Pro&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;話說 Pages 不能中英文分開設字體實在是件討厭的事 :(。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;進入正題，談的也是字體，話說我以前在 Windows 下裝 OpenOffice.org，它都會內附這幾套字體：Albany、Thorndale、方正明體、方正黑體，分別對應到 Arial、Times New Roman、新細明體、微軟正黑體，作為預設樣式的字體（沒有楷體），可是很奇怪的…不管是 Linux 或 Mac 版的 OOo 或是 NeoOffice，都沒有附上這些字體，更奇怪的，偏好設定裡頭的標準字型還是設成 Albany、Thorndale、方正明體、方正黑體… =“=。&lt;&#x2F;p&gt;
&lt;p&gt;這些個奇怪的原始設定也會有奇怪的結果，若是打開以往做的文件，OOo &#x2F; NeoOffice 會自己去搜尋相似的字體秀出來，方正明體會以 Hiragino Maru Gothic ProN 替代、方正黑體會以 Hiragino Mincho ProN 替代，可是以這兩個日本字體作為代替品實在是不恰當，明體被換成黑體、黑體被換成明體了，這兩個字體本身沒有問題，是奇怪在 NeoOffice 代換的過程，所以比較好的方法還是手動指定取代清單，在偏好設定－NeoOffice－字型裡面，另外就是重新指定標準字型，一樣在偏好設定裡面。&lt;&#x2F;p&gt;
&lt;p&gt;還沒完，即便都設定好了，NeoOffice 做出來的文件拿去給 OOo 開還是有可能會跑位，因為它們取字體名稱的方式不一樣，舉例來說 Times 在 NeoOffice 裡就叫 Times Roman，在 OOo Aqua 就叫 Times，這類的問題特別容易發生在中日韓字體上，打開字體簿看一下，儷宋 Pro 起碼就有兩個名字－儷宋 Pro 跟 LiSongPro，不同的程式抓不同的名字，就算是同一個字體也對不起來，真的是見到鬼，越搞頭越大 =“=，而且這無解，換個作業系統也還是會遇到，那 MS Office 會這樣嗎？我不知道。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;找到解法了 ^^“，就跟 CSS 一樣，字體可以多重指定，用分號隔開即可，一般各個平台下常用的字體設定如下：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;Helvetica; Albany; Droid Sans; DejaVu Sans; FreeSans; Arial&lt;&#x2F;li&gt;
&lt;li&gt;Times; Times Roman; Thorndale; Droid Serif; DejaVu Serif; FreeSerif; Times New Roman&lt;&#x2F;li&gt;
&lt;li&gt;LiSong Pro; AR PL UMing TW; 方正明體; 新細明體&lt;&#x2F;li&gt;
&lt;li&gt;Heiti TC Light; 黑體-繁; Droid Sans Fallback; 方正黑體; 微軟正黑體&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;啊是不是要搞這麼累啊？T_T&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>一點小感想</title>
        <published>2009-12-01T00:00:00+00:00</published>
        <updated>2009-12-01T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/paper/"/>
        <id>https://editor.leonh.space/2009/paper/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/paper/">&lt;p&gt;只有 LaTeX 能打出優美、結構優良的文件的神話早就破滅了，那些期刊出版商只用 LaTeX 範本也是完全故步自封的作法，從一篇 paper 賣幾十塊美金就看得出來，做研究的也不是它們憑什麼賣這麼貴，它們也只會把你的成果轉寄給大師評而已，沒跟它簽約的個人戶誰買得起，這不就是知識壟斷嗎！&lt;&#x2F;p&gt;
&lt;p&gt;Elsevier &amp;amp; Wiley：「ㄎㄎ」。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>GIMP 兩三事</title>
        <published>2009-11-29T00:00:00+00:00</published>
        <updated>2009-11-29T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/gimp/"/>
        <id>https://editor.leonh.space/2009/gimp/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/gimp/">&lt;p&gt;首先 GIMP on OS X 的作者新編了一個 64 位元給雪豹用的版本，一樣是以 GIMP 2.6.7 為基礎，還有改用作業系統內建的 Python，減少了一些體積，有變快嗎？沒特別感覺，爽度大概有提高一點點吧。&lt;&#x2F;p&gt;
&lt;p&gt;話說這個好心的作者（GIMP 的本家開發者們是佛心），他編的 GIMP 裡面還多加了一些 plug-ins，其中一個要特別提一下－神奇的 GREYCstoration，從此去雜訊就靠它了，而且銳利度不會降的太誇張，不過也超吃 CPU 的，拿來處理縮圖還可以接受，原圖的話先上到 Intel i7 再來考慮… =“=。它的參數很多。&lt;&#x2F;p&gt;
&lt;p&gt;另外也因為 &lt;a rel=&quot;external&quot; href=&quot;http:&#x2F;&#x2F;gmic.sf.net&#x2F;&quot;&gt;GREYCstoration&lt;&#x2F;a&gt; 的網頁，讓我注意到它已經停止維護了，整合到另一個專案中繼續發展，稱為 G’MIC（GREYC’s Magic Image Converter），變得更好更強大，也有給 GIMP 用的 plug-in，不過我怎麼裝都沒反應 :(，只好開始辛苦的爬文 =“=。&lt;&#x2F;p&gt;
&lt;p&gt;簡單的說 GIMP on OS X 的這個 GIMP 是不能夠自行裝任何的外掛的，而 G’MIC 給 GIMP 的 plug-in 也不是給它用的，是給 MacPorts 出的 GIMP用的，整個就是搞的很複雜 @@。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;簡單解釋，想要在 Mac 下用某些軟體，最好的途徑就是官方有做 Mac 版的原生版本，像 Adobe 系列、Firefox、Google Chrome 這些比較有資源的或比較有商業考量的。&lt;&#x2F;p&gt;
&lt;p&gt;第二種就是靠第三人去開發移植，特別是人力資源比較少的開源軟體，因為主要開發能量只能專注在該軟體的原始平台上，大多是 Linux，沒有多餘的能量用於其它平台的開發，這時只能靠好心的第三方進行移植的工作，這第三方有的還不只一個，變成還有第四方、第五方…。&lt;&#x2F;p&gt;
&lt;p&gt;有兩個比較知名的社群在做這樣的苦工，MacPorts &amp;amp; Fink，他們把許多的開源軟體一個個的移植到 Mac，GIMP 也是其中之一。&lt;&#x2F;p&gt;
&lt;p&gt;然而有另外一位應該是德國人的好心人也做了類似的事，就是 GIMP on OS X，而它們移植的方式不太一樣，簡單形容 GIMP on OS X 比較像是像我這種傻瓜老百姓在用的，MacPorts 的比較像是男子漢在用的，而不巧 GIMP on OS X 因為是給傻瓜用的所以無法裝任何的外掛 XD，除非作者有加進去。不只是 G’MIC，想裝外掛只能用男子漢的 MacPorts 的 GIMP。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Mac 下的免費空間下載器</title>
        <published>2009-11-28T00:00:00+00:00</published>
        <updated>2009-11-28T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/downloader/"/>
        <id>https://editor.leonh.space/2009/downloader/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/downloader/">&lt;p&gt;One-click hosting，台灣一般稱之為免費空間，簡稱免空，中國那邊好像叫網盤吧？這類的網站多半給網友分享檔案用，特別是大檔，可是它們的另一個特色是廣告特多，多到你眼花撩亂，於是便有好心人士開發了專門從那裡抓檔的軟體，在 Mac 上找到這三款：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;wordrider.net&#x2F;freerapid&#x2F;&quot;&gt;FreeRapid&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;http:&#x2F;&#x2F;jdownloader.org&#x2F;&quot;&gt;jDownloader&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;li&gt;&lt;a rel=&quot;external&quot; href=&quot;http:&#x2F;&#x2F;www.tucaneando.com&#x2F;&quot;&gt;Tucan Manager&lt;&#x2F;a&gt;&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;這三者都是跨平台的軟體，FreeRapid 和 jDownloader 都是用 Java 寫的，Tucan Manager 是用 Python，缺點就是不若原生程式那麼的跟作業系統融合，除此之外支援的免空也各有差異。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;freerapid&quot;&gt;FreeRapid&lt;&#x2F;h2&gt;
&lt;p&gt;介面不好看，是三者中最難看的，而且它的選單列的選項與 Mac 一般情況不一致。支援的免空站應有盡有，討厭的是它有時候產生的 log 檔會放在 &#x2F;Applications 裡面，這點很討厭 :(。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;jdownloader&quot;&gt;jDownloader&lt;&#x2F;h2&gt;
&lt;p&gt;介面也不是很好看，介於三者中間，支援的免空站也是一大堆，選項很多，還有外掛可以用，自動解壓縮、自動合併之類的，功能豐富，不過就是選項太繁複。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;tucan-manager&quot;&gt;Tucan Manager&lt;&#x2F;h2&gt;
&lt;p&gt;我個人最偏愛的一套，因為它夠簡單，沒什麼好調的，跟作業系統的一致性也最好，介面也最好看，但圖示挺鳥的…，缺點是支援的免空最少，真的超少，而且選單上有標快速鍵，可是實際上卻沒作用，還有一些蟲在，維護的也是最慢的。&lt;&#x2F;p&gt;
&lt;p&gt;結論，我最後應該會留 jDownloader 吧，Tucan Manager 雖然很好，不過有一些奇怪的 bug，用起來頗不爽快。&lt;&#x2F;p&gt;
&lt;p&gt;個人比較喜歡簡單清爽的介面，像 Firefox 就不太是我的菜，更別說裝了一堆外掛了，所以 Google Chrome 才深得我心啊！（不過我現在主要是用 Safari XD）&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>拋棄掉無謂的歷史觀點吧！</title>
        <published>2009-11-20T00:00:00+00:00</published>
        <updated>2009-11-20T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/drop-the-historical-perspective/"/>
        <id>https://editor.leonh.space/2009/drop-the-historical-perspective/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/drop-the-historical-perspective/">&lt;p&gt;個人淺見，在這個世界上至少有兩樣歷史觀點應該被拋棄，因為即使一再的老調重提，也沒人鳥，也發揮不了絲毫影響力，冷飯一炒再炒一點屁用都沒有。&lt;&#x2F;p&gt;
&lt;p&gt;第一樣是台灣主權未定論，對啦或許這樣詮釋那些條文是對的，不過那些死人骨頭簽的文件不管是現在、未來都起不了任何作用，也號召不了人民的認同感，沒辦法現實是殘酷的，一直提一直提「歷史正確」什麼也得不到，換個路線再出發吧獨派人士。&lt;&#x2F;p&gt;
&lt;p&gt;第二樣類似的，是關於電腦誰抄誰的問題，這真的很無聊，二三十年前的事情還在爭爭爭！這整件事情本身是個混沌、沒有輸贏、沒有真相也無解的存在，不會有人因為誰證明了某 A 抄襲了某 B 的東西而影響他的採購決策，不會。我很懷疑這些很喜歡強調「歷史正確」的人，對待其它的人事物是怎樣？還是對這種雞毛蒜皮的小事也有糞青的存在？這件事就像路口一堆的永和豆漿、美而美、緣生圓、X 媽臭臭鍋一樣的不重要，去它的歷史正確！&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>後記：我為什麼買 Mac</title>
        <published>2009-11-18T00:00:00+00:00</published>
        <updated>2009-11-18T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/mac/"/>
        <id>https://editor.leonh.space/2009/mac/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/mac/">&lt;p&gt;這一篇文談談揪竟為什麼我要買 Mac。&lt;&#x2F;p&gt;
&lt;p&gt;話說我有一台從大三至今用了五年以上的電腦，這台聯強的筆電作業系統當然是 Windows，然而長久使用 Windows 下來，伴隨著自己年華老去，真的覺得 Windows 的中文很傷眼，開了 ClearType 裝了正黑體也只有好那一瞇瞇（微軟搞字體消鋸齒的團隊應該砍掉重練），後來在買 Mac 之前的一陣子都用 Ubuntu，早期是用文泉驛正黑，後來用 Droid Sans，話說那 Droid Sans 真的看起來很舒服 ^^。&lt;&#x2F;p&gt;
&lt;p&gt;軟體的問題的話其實根本沒問題，如前文所提的除了 Windows 外，其它我慣用的軟體都不需要依賴 Windows。&lt;&#x2F;p&gt;
&lt;p&gt;我既不用 IE，也不用 Office，我慣用的瀏覽器以前是 Firefox，後來是 Google Chrome（← 因為實在是太快了），能捨 Firefox 的原因是我幾乎沒裝什麼 add-on，個人比較喜歡簡潔快速的軟體。&lt;&#x2F;p&gt;
&lt;p&gt;Office 的話我很早就改用 OpenOffice.org 了，原因很簡單，我沒錢 ^^“，用盜版？沒有羞恥心跟守法觀念的人才用盜版！（有罵到誰嗎？？），沒錢就不要用，OpenOffice.org 的確有很多不如 MS Office 的地方沒錯，但即便如此也不能合理化用盜版的犯罪行為，嫌人家賣的貴就不要買，又沒人逼你買。&lt;&#x2F;p&gt;
&lt;p&gt;還有 IM 的話我是不用桌面端程式的，我用 Meebo，缺點：沒有原生的視訊跟傳檔支援；優點：可以擺脫裝可愛的表情圖，對話記錄可以永久保存回味。&lt;&#x2F;p&gt;
&lt;p&gt;但這 Ubuntu 也並非十全十美，像是 ATI 的顯卡驅動真的太鳥，效能差到被鬼抓走，嚴重一點可以說是不具生產力（幸好我現在不需要生產力 XD），不過在顧眼睛的前提下也就這麼忍受著。也是這樣正式興起我買 Mac 的念頭，開始認真考慮 Mac 的可行性。&lt;&#x2F;p&gt;
&lt;p&gt;歸納下來有幾點，首先最重要牠的字體消鋸齒做的很棒、看起來很舒服。其次可以滿足我用電腦的需求，我要用的軟體都有。第三點，它不貴，請把可以比價的內部零組件和軟體的價錢加起來，好像很多人都不知道軟體是要花錢買的吼？這些加加起來，還是覺得比較貴的話就當設計費吧～設計也要算錢的咩～ ^^。就是如此如此這般這般所以我買了 Mac ，ㄎㄎ。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>這是一篇護航文…</title>
        <published>2009-11-16T00:00:00+00:00</published>
        <updated>2009-11-16T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/survivorship-bias/"/>
        <id>https://editor.leonh.space/2009/survivorship-bias/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/survivorship-bias/">&lt;p&gt;聽過 survivorship bias 嗎？簡單的說就是吾人眼前所見的事實，不過是被扭曲、被放大的部份事實。&lt;&#x2F;p&gt;
&lt;p&gt;舉個綠角的例子來說，之所以為什麼舉目所見市場上滿坑滿谷的基金的廣告好像都很亮眼，那是因為表現差的基金都被合併或清算掉了，根本就不會有廣告，「死人不會講話」，剩下來都是活著的，而投資大眾若不刻意觀察，當然只能看到「基金公司想讓你看到的」，若要合理評估整體市場的盛衰，把那些陣亡的基金計入才會讓投資人得到真正的答案。&lt;&#x2F;p&gt;
&lt;p&gt;不過這裡我要談的是它的相反，有些不明究裡的人認為蘋果的東西只是金玉其外、敗絮其中，品質每況愈下，硬體維修率高，這點也常常被反蘋果人士當做攻訐的目標（干他們屁事？），但真是如此嗎？&lt;&#x2F;p&gt;
&lt;p&gt;我認為其實並非如此，道理很簡單，首先看看 Mobile01，所有的品牌電腦裡面只有蘋果自成一個群組，裡面還細分成若干個討論區，其它的呢，一律歸到電腦群組內的「品牌電腦與準系統」分區內。另外一個很有代表性的是 PTT，也是類似的情況，Mac 自成一板，其它品牌電腦連板都沒有，只以零組件分板，這兩個站台分別代表了台灣網路界 web 介面最大的論壇，跟（全球？）最大的 BBS，具有絕對的代表性，我想能這樣佔據專板的應該只有 ThinkPad 跟 VAIO 這兩個品牌了吧。&lt;&#x2F;p&gt;
&lt;p&gt;這說明什麼？想當然爾在這樣的 Mac 專板內當然只會討論 Mac，這種道理大家都懂，然而大家再想一想，今天我一定是買到品質不好的產品才會去板上大聲嚷嚷，買到正常品有必要大費周章去發廢文告訴大家嗎？然後抱怨文發在 Mac 板上，Mac 板上又只會有 Mac 文，搞的好像 Mac 故障率很高似的，跟生存者偏差一樣的道理，只是變成死亡者偏差 XD，在 Mac 板，看到 Mac 抱怨文比例偏高，就以為 Mac 真的故障率高，真是傻氣，&lt;strong&gt;活的好好的人不會出來大聲嚷嚷&lt;&#x2F;strong&gt;，而且原因二，剛剛講過了，因為 Mac 專板導致的假象，假設做個改變，不設 Mac 專板，讓 Mac 鄉民四處流竄，再去各板排排各品牌的抱怨文，就會神奇的發現 Mac 的品質又變好勒。&lt;&#x2F;p&gt;
&lt;p&gt;或者是只比筆電的話，去比較一下各廠牌筆電板跟蘋果筆電板抱怨文跟該牌在台用戶數的比例，即可對每一品牌筆電真正的品質優劣與否有一大概性的輪廓。&lt;&#x2F;p&gt;
&lt;p&gt;不要再被偏差的事實蒙蔽了。&lt;&#x2F;p&gt;
&lt;p&gt;又想起一個不是很恰當例子，記得我在買 Mac 前剛好是 Snow Leopard 出貨的那陣子，因為系統介面字體更換的問題吵的沸沸揚揚，搞的好像黑體繁多麼不堪入目，也真的嚇到我了，因為這樣著實讓我考慮再三（字體美觀是我買 Mac 的重要因素之一），後來心一橫給它買下去之後才發現…黑體繁跟儷黑 Pro 是互有優缺點，不過怎樣還是遠勝微軟正黑體，那萬惡的新細明體更不用說了。不也是這樣嗎，我舉目所見儘是黑體繁怎樣又怎樣爛，這是跟儷黑 Pro 比，但若是跟 Windows 下的字體比可是輕鬆獲勝，這不也是一種偏差嗎。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>原生圖形介面對軟體使用率之研究</title>
        <published>2009-11-14T00:00:00+00:00</published>
        <updated>2009-11-14T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/ui/"/>
        <id>https://editor.leonh.space/2009/ui/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/ui/">&lt;p&gt;標題亂掰的 XD。&lt;&#x2F;p&gt;
&lt;p&gt;在我使用 Mac 之前，在相關討論區爬文已久，觀察到一個現象，即 Mac 用戶對於某套軟體是不是採用 Mac 原生介面顯得相當在意，一直以來我對此感到相當不解，凡是有軟體它呈現的視窗元件或整體設計不同於一般 Mac 下軟體呈現的觀感，滿分 5 顆星大多只會得到 2 ~ 3 顆星的評價，而且國內外皆然，不過遊戲和男子漢用的 CLI 的軟體或程式例外，所以一票移植到 Apple X11 下的軟體使用率都奇低。&lt;&#x2F;p&gt;
&lt;p&gt;一直到我有了 Mac 之後，才了解到那類非原生介面的軟體操作起來真的很奇怪，整個就是不順手，而且與 Mac 超美的介面相較之下顯得特別的醜，X11 下的軟體效能不佳跟穩定性也是問題，真的很難讓人有慾望去用它。（不過我還是裝了 GIMP 和 Inkscape，因為真的找不到替代品。）&lt;&#x2F;p&gt;
&lt;p&gt;印象中老地方冰果室很久以前也是有寫過這主題，是舉 Skype 為例，但是我懶得翻了 &#x2F;_\。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>小評 Mac 上的兩套文書處理軟體</title>
        <published>2009-11-12T00:00:00+00:00</published>
        <updated>2009-11-12T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/document-processor/"/>
        <id>https://editor.leonh.space/2009/document-processor/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/document-processor/">&lt;p&gt;Mac 平台上的文書處理軟體，除了那些已經相當知名的 Pages、Word、OpenOffice.org Writer 外，還有一些小型的軟體可供選擇，這些小軟體功能或許不如上述知名軟體強大，但用於小型文件（或是毫不在乎文件大綱階層的人）應是綽綽有餘。&lt;&#x2F;p&gt;
&lt;p&gt;秉持著本人能用免費就不要花錢的一貫原則下，稍微小小的嘗試了其中的兩個軟體－&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;bean-osx.com&#x2F;Bean.html&quot;&gt;Bean&lt;&#x2F;a&gt; 與 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;marinersoftware.com&#x2F;discontinuedproducts&#x2F;&quot;&gt;Mariner Write&lt;&#x2F;a&gt;。&lt;&#x2F;p&gt;
&lt;p&gt;鄉民：「可是 OpenOffice.org Write 也是免錢（自由）的啊？？」&lt;&#x2F;p&gt;
&lt;p&gt;沒錯不過他的功能對於一些小文章來說又太多了，程式載入的時間又頓了點，所以我一直在尋找一個相當於 AbiWord 這樣穠纖合度的文書軟體。（Mac OS 版的 AbiWord 已經沒在維護了…）&lt;&#x2F;p&gt;
&lt;p&gt;而且OpenOffice.org Aqua不知怎麼的中文介面很醜…。&lt;&#x2F;p&gt;
&lt;p&gt;而且再而且，Mac OS 內附的文字編輯實在太鳥了，除了拿來看 readme.txt 之外真的不知道還能幹嘛？（謎之音：還能看 license.txt 和 copyright.txt，ㄎㄎ）將其介面與 iLife 隨便一個對照之下，很難相信這兩者是同一間公司出品的軟體。（WordPad on Windows 7 樂勝）&lt;&#x2F;p&gt;
&lt;p&gt;容先敘明，Mariner Write 是要錢的，要價 49.95 USD，不過目前 MacHeist 提供了有條件的、限時的免費大放送，才讓我有動力去試用它。&lt;&#x2F;p&gt;
&lt;p&gt;Bean、Mariner Write 看起來好像都很好很強大的樣子，不過 Mariner Write 存檔的格式是它自己的專有格式，毫無交換性可言，什麼功能也不用試了直接出局。&lt;&#x2F;p&gt;
&lt;p&gt;Bean 也是類似的狀況，它的主要存檔格式是 rtfd，跟 rtf 有點關係，不過一樣很抱歉，最前面提到的那三大文書處理軟體也都開不了它，也是不用再試其它的了。&lt;&#x2F;p&gt;
&lt;p&gt;即便它們也可以存成 pdf 或 rtf，不過 pdf 的特色也是它最大的優點及缺點－它不能改，這點在你把作業帶到學校臨交前才發現有錯誤時特別明顯，而且有的人不收 pdf，還通常是你的老師或老闆。&lt;&#x2F;p&gt;
&lt;p&gt;另外一個 rtf 也只能ㄎㄎ了，這格式在不同的軟體下開啟所呈現之差異，比 html &#x2F; css 在不同瀏覽器下打開的差異只能說有過之而無不及，真的很無言… =“=，這兩個功能都還不錯的軟體，都死在格式上，特別是 Mariner Write，真的挺不錯的，很可惜它不像 Pages，雖然也是專有格式，不過它爸爸是 Apple，含著金湯匙出生的，平平是專有格式，就是大家比較會買帳。&lt;&#x2F;p&gt;
&lt;p&gt;所以最後還是默默的繼續用 OOo Writer（默）。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>離職了</title>
        <published>2009-08-09T00:00:00+00:00</published>
        <updated>2009-08-09T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/quit/"/>
        <id>https://editor.leonh.space/2009/quit/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/quit/"></content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>目標</title>
        <published>2009-08-01T00:00:00+00:00</published>
        <updated>2009-08-01T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/goals/"/>
        <id>https://editor.leonh.space/2009/goals/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/goals/">&lt;ul&gt;
&lt;li&gt;ISO&lt;&#x2F;li&gt;
&lt;li&gt;CQT&lt;&#x2F;li&gt;
&lt;li&gt;Mac mini&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;等到都達成，我就…！&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>當下的願望</title>
        <published>2009-07-28T00:00:00+00:00</published>
        <updated>2009-07-28T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/wishes/"/>
        <id>https://editor.leonh.space/2009/wishes/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/wishes/">&lt;p&gt;目前關於生活上的願望，很簡單，希望每天在晚上 9 點前讓我至少跑一個小時的慢跑，還有擁有自己的進修時間，去上的課都可以準時到，回到家讓我有一段完整的時間複習上課的內容。&lt;&#x2F;p&gt;
&lt;p&gt;就這麼簡單，卻不可得…。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>挪威給全球經濟體「上一課」</title>
        <published>2009-06-14T00:00:00+00:00</published>
        <updated>2009-06-14T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/norway/"/>
        <id>https://editor.leonh.space/2009/norway/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/norway/">&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;norway&#x2F;3609683853_fa7ec948ac_o-md-lg.png&quot; alt=&quot;朝鮮日報&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：《朝鮮日報》&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;ol&gt;
&lt;li&gt;儲蓄&lt;&#x2F;li&gt;
&lt;li&gt;投資&lt;&#x2F;li&gt;
&lt;li&gt;謹慎消費&lt;&#x2F;li&gt;
&lt;li&gt;量入為出&lt;&#x2F;li&gt;
&lt;&#x2F;ol&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>兩個新玩具</title>
        <published>2009-06-13T00:00:00+00:00</published>
        <updated>2009-06-13T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/toys/"/>
        <id>https://editor.leonh.space/2009/toys/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/toys/">&lt;p&gt;因緣際會之下買了這兩個新玩具，簡單說一下感想，GPS 在市區真的很好用，Mio 新版的介面也真的很漂亮，只是它下方的 MicroSD 插槽不知道幹嘛用的，另一個問題是我都用 Linux，沒辦法更新圖資（倒是還好啦～）。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;toys&#x2F;R0019686-md.jpg&quot; alt=&quot;Mio Moov S505&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;另一個新玩具是 Pioneer 的車用音響，也是很棒的東西，不過我覺得操作有點複雜 @@。這台我是去路上找汽車音響店安裝的，裝回來這幾天在網路上比價的結果﹍價錢我很滿意，而且老闆也很親切，推一下好了，佳泰汽車用品行，地址是桃園市三民路一段 101 號。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;toys&#x2F;R0019687-md.jpg&quot; alt=&quot;Pioneer DEH-2150UBG&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>又去八里</title>
        <published>2009-06-12T00:00:00+00:00</published>
        <updated>2009-06-12T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/bali/"/>
        <id>https://editor.leonh.space/2009/bali/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/bali/">&lt;p&gt;延續上週的計畫，這個週末仍然去了八里，這次的行程和上週略有不同，除了十三行博物館外，還有沿著腳踏車道一路騎向渡船頭，還有中間的一間兔子餐廳，另外渡船頭那邊排渡輪的隊伍，真的讓我完全打消搭船的念頭 =“=。&lt;&#x2F;p&gt;
&lt;p&gt;從淡水河左岸往對面看是很不同於一般的，在出海口附近，雖然名為河，但實際上可說是海，但所見看得到對面高樓的海，又不同於一般的海，真的是滿特別的景色。&lt;&#x2F;p&gt;
&lt;p&gt;以下都是八里左岸公園。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;bali&#x2F;R0019648.jpg&quot; alt=&quot;八里&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;bali&#x2F;R0019652.jpg&quot; alt=&quot;八里&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;bali&#x2F;R0019672.jpg&quot; alt=&quot;八里&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>神秘的 AdWords</title>
        <published>2009-06-06T00:00:00+00:00</published>
        <updated>2009-06-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/adwords/"/>
        <id>https://editor.leonh.space/2009/adwords/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/adwords/">&lt;figure class=&quot;wide&quot;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;adwords&#x2F;Screenshot-Gu-Jia-Gmail-Chromium-md.png&quot; alt=&quot;Gmail&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;上圖這封信是有關於我們房子水電的報價單，今天剛收到這封信還沒特別注意，後來才察覺到﹍右邊的關鍵字廣告剛好都是室內設計、裝潢的耶～。&lt;&#x2F;p&gt;
&lt;p&gt;怎麼會這樣？ O_O，看一看這兩封信，包括標題、內文、附檔檔名，都沒有相關的關鍵字耶！只有報價單裡面有一些那方面的詞，難不成 Google 的廣告系統連附檔都會去掃描關鍵字？！&lt;&#x2F;p&gt;
&lt;p&gt;而且這類室內設計的廣告唯獨這封信有喔！別的都沒有，恐怖喔～！&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;題外話，第一次遇到有人也是用 OpenDocument 的格式，太棒了！自由軟體萬歲！！&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>顯微鏡的暗視野</title>
        <published>2009-06-05T00:00:00+00:00</published>
        <updated>2009-06-05T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/microscope/"/>
        <id>https://editor.leonh.space/2009/microscope/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/microscope/">&lt;p&gt;這是一篇拾人牙慧文﹍。&lt;&#x2F;p&gt;
&lt;p&gt;比較「有一點程度」的顯微鏡都可以切換明視野和暗視野，一直以來我也就這樣用啊用的，從來沒有認真去探究過究～～～竟它們是怎樣的機制，直到被問倒的這一天﹍。&lt;&#x2F;p&gt;
&lt;p&gt;看一下它們的英文－明視野（bright field）、暗視野（dark field）﹍，並沒有比較好理解，一般的籠統﹍。&lt;&#x2F;p&gt;
&lt;p&gt;然後再 Google 一下，都是賣望遠鏡的，直到看到阿簡老師的&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;a-chien.blogspot.com&#x2F;2008&#x2F;05&#x2F;blog-post_10.html&quot;&gt;改裝暗視野顯微鏡&lt;&#x2F;a&gt;，終於讓我挖到一個比較具體、實際、又簡單的說明。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>十三行博物館</title>
        <published>2009-06-04T00:00:00+00:00</published>
        <updated>2009-06-04T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/shihsanhang/"/>
        <id>https://editor.leonh.space/2009/shihsanhang/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/shihsanhang/">&lt;p&gt;禮拜日去了林家花園跟十三行博物館，原來林家花園就在我曾經逛過的一個夜市的另外一邊。這兩個地方我都是第一次造訪，先說說林家花園吧！沒什麼令人太驚豔的景色，感覺就是那樣的古樸，在那些假山假水間可以概略的想像著當年林家豪門內人們生活的樣子，好像很瑣碎、很悠閒、很日復一日的感覺。另外我向來對中國式的假山水沒有多大的好感，它給我的感受是有點將就、假裝、囚禁、自欺，也欺人的不是很正面的感覺。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;shihsanhang&#x2F;R0019531-md.jpg&quot; alt=&quot;十三行博物館&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;然後咻一下就到十三行了（很跳 tone），話說我以往到淡水從來沒有一次有想要搭渡輪的念頭，這真是完全的錯過了一個擁有如此美好風景的地方，這裡可以散步（而且不會人擠人）、可以看風景（而且不會人擠人）、可以吃吃喝喝（而且不會人擠人），還可以放風箏，整個就是棒！下禮拜我要再來一次！&lt;&#x2F;p&gt;
&lt;p&gt;最後晚餐在竹圍漁港解決，只有好吃而已，下次帶別人來漁港豪邁的吃生猛海鮮，大口吃肉、大口喝酒一定也挺棒的！&lt;&#x2F;p&gt;
&lt;p&gt;話說回來，這天的行程是完全沒有事先預想的，可是真的很不錯玩，關鍵是同行的人不能有東挑西撿的那種人。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>微軟正黑體</title>
        <published>2009-06-01T00:00:00+00:00</published>
        <updated>2009-06-01T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/hei/"/>
        <id>https://editor.leonh.space/2009/hei/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/hei/">&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;www.microsoft.com&#x2F;downloads&#x2F;details.aspx?FamilyID=87e726a9-5c79-4a73-9347-78a557314d0b&amp;amp;DisplayLang=zh-tw&quot;&gt;Traditional Chinese ClearType fonts for Windows XP&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;上面的連結就是微軟正黑體。&lt;&#x2F;p&gt;
&lt;p&gt;講一下字體，比較一下截至目前用過的幾款黑體字，微軟正黑體、文泉驛正黑、Droid Sans Fallback（&lt;a rel=&quot;external&quot; href=&quot;http:&#x2F;&#x2F;wenq.org&#x2F;?MicroHei&quot;&gt;文泉驛微米黑&lt;&#x2F;a&gt;），其中表現最好的就是微軟正黑體，雖然它在螢幕上顯示起來有點過細，不過列印效果卻是最棒的。&lt;&#x2F;p&gt;
&lt;p&gt;在螢幕上顯示我認為最漂亮的是 Droid Sans Fallback，不過它列印出的效果實在是差強人意，拿來當標題用用還可以接受，內文的話完全不行，只有粗而已，文泉驛正黑也是類似，印出來都過粗，而且在螢幕上的筆劃都很強硬，相較之下 Droid Sans Fallback 在螢幕上的表現就顯得圓潤一點，可惜印出來完全不行 :(。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>星巴克的天花板</title>
        <published>2009-05-31T00:00:00+00:00</published>
        <updated>2009-05-31T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/ceiling/"/>
        <id>https://editor.leonh.space/2009/ceiling/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/ceiling/">&lt;p&gt;星巴克的天花板是沒有作裝潢的，讓空調及電路管線直接外露，連同天花版一律漆上低調沉穩的咖啡色，梁柱部份則是暗紅色，光源的部份因為沒有木作，所以也沒有間接照明，都是造型簡單的筒燈，佐以軌道燈，以暖白光直接照下。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;ceiling&#x2F;2801473604_a3ccdda1b4_o-lg.jpg&quot; alt=&quot;星巴克的天花板&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;值得借鏡的例子，不過畢竟一般家庭跟星巴克結構是不一樣的，天花板沒那麼高，用筒燈滿奇怪的。面積也沒那麼大坪數，裝這麼多盞也很怪，軌道燈裝在家裡也怪怪的，雖然我還滿喜歡這種模組化的結構。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>大雁 VS. 林譽德</title>
        <published>2009-04-27T00:00:00+00:00</published>
        <updated>2009-04-27T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/jack/"/>
        <id>https://editor.leonh.space/2009/jack/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/jack/">&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;jack&#x2F;center_south_yeng-md.jpg&quot; alt=&quot;大雁&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;大雁&lt;&#x2F;figcaption&gt;
&lt;figcaption&gt;來源：《痞子英雄》&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
 &lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;jack&#x2F;149c07b0e5a04f-md.jpg&quot; alt=&quot;林譽德&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;林譽德&lt;&#x2F;figcaption&gt;
&lt;figcaption&gt;來源：《老王同學會》&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;那維勳的反差真大 @@。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>資訊焦慮</title>
        <published>2009-04-17T00:00:00+00:00</published>
        <updated>2009-04-17T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/anxiety/"/>
        <id>https://editor.leonh.space/2009/anxiety/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/anxiety/">&lt;p&gt;前一篇文章發完突然間想到資訊爆炸和資訊焦慮這兩個現象，是的我就是資訊爆炸時代下的資訊焦慮人。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>關注的事太多</title>
        <published>2009-04-16T00:00:00+00:00</published>
        <updated>2009-04-16T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/refocus/"/>
        <id>https://editor.leonh.space/2009/refocus/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/refocus/">&lt;p&gt;檢視我訂閱的 feed，除了有一些正妹的之外，還有很多：&lt;&#x2F;p&gt;
&lt;ul&gt;
&lt;li&gt;英文的（幾乎沒在看）&lt;&#x2F;li&gt;
&lt;li&gt;少量的個人網誌&lt;&#x2F;li&gt;
&lt;li&gt;3C 的、IT 產業的、電腦技術方面的、軟體的&lt;&#x2F;li&gt;
&lt;li&gt;攝影的&lt;&#x2F;li&gt;
&lt;li&gt;科學的（電子、生物、光電、科普）&lt;&#x2F;li&gt;
&lt;li&gt;居家的、建築的&lt;&#x2F;li&gt;
&lt;li&gt;財經的&lt;&#x2F;li&gt;
&lt;li&gt;生活的、休閒的、旅遊的、桃園的&lt;&#x2F;li&gt;
&lt;li&gt;政治的&lt;&#x2F;li&gt;
&lt;li&gt;文化的、歷史的&lt;&#x2F;li&gt;
&lt;li&gt;飲食的&lt;&#x2F;li&gt;
&lt;li&gt;交通運輸方面的&lt;&#x2F;li&gt;
&lt;&#x2F;ul&gt;
&lt;p&gt;這些一大堆加起來總共訂了 164 個，而且新聞性的很少，也就是說內容重複性的很少。&lt;&#x2F;p&gt;
&lt;p&gt;上面這些大部份是基於興趣（還真廣 =“=），有些是工作相關，還有一些是我認為未來必須要懂的一些知識（居家、建築、財經、生活類），另外一些是公共政策之類的（政治、文化、歷史、桃園、交通運輸類），可是相對的，這樣多的 feed 佔掉了我大部份的時間，好像本末倒置了…而且我還有個遺傳自我爸愛蒐集資料的習慣，只差別在方式不同，他剪的報大概可以繞地球一圈 =”=，而我都用網路，舉個例子來說，這個週末我在看不動產證券化（REIT），就去 Google 翻網頁出來看，除了吃飯睡覺尿尿大概都在看了，還有去 PTT 爬文看精華區，而且我還很討厭中斷的感覺…，這樣看下來當然不可能了解透徹，但夠我評估了。&lt;&#x2F;p&gt;
&lt;p&gt;就這樣…我的閒暇時間大多耗費在這些事情上，好像事業做很大的樣子 :p，可是並沒有 =“=，導致整體的時間管理上很差，也許我該試著讓自己放鬆…。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>公開資訊觀測站</title>
        <published>2009-04-14T00:00:00+00:00</published>
        <updated>2009-04-14T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/mops/"/>
        <id>https://editor.leonh.space/2009/mops/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/mops/">&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;mops.twse.com.tw&#x2F;mops&#x2F;web&#x2F;index&quot;&gt;公開資訊觀測站&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;不管是金融資產證券化還是不動產證券化的相關資料那裡都有集中彙整，想買台灣的 REIT 的人應該要買那裡列的那幾支，交易方式如同證券，一般投信賣的不動產證券化型基金沒有有投資台灣的啦。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>工作兩年</title>
        <published>2009-04-05T00:00:00+00:00</published>
        <updated>2009-04-05T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/after-2-years/"/>
        <id>https://editor.leonh.space/2009/after-2-years/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/after-2-years/">&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;after-2-years&#x2F;xinsrc_44206052509174681935515-md.jpg&quot; alt=&quot;研究發現看看窗外的景色有助於緩解壓力&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;研究發現看看窗外的景色有助於緩解壓力&lt;&#x2F;figcaption&gt;
&lt;figcaption&gt;來源：網路&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;這份工作滿兩年囉！其實上個月底就到了，這兩年來經歷了許多次的撞牆期、倦怠期，最近的一次也才剛剛度過而已，其實會有這樣的情緒出現往往是卡關的時候，我真的很討厭卡關的感覺，如果是卡關又加上死線的壓力的話真的是很想一走了之 =“=，每次經歷到類似的狀況我都會想說這次樣品做完我就離職，等到真的破關了根本就不會記得那些話，直到再次卡關又開始陷入無限輪迴之中… =”=。&lt;&#x2F;p&gt;
&lt;p&gt;從四月開始狀況就一直不錯，包括現在加班也沒什麼太大的怨言，不知道是奴性變重 XD，還是三月有出去玩幾次調劑身心的影響？&lt;&#x2F;p&gt;
&lt;p&gt;最近接觸了兩樣事物，網拍跟某傳銷公司，一直以來都扮演買家的角色，頂多就是很業餘的賣家，直到想真正開始賣東西的時候，才知道需要考慮的事情很多，成本結構、進出貨方式、訂價策略、等等…。另外那個傳銷公司，名字就不提了，簡單說就是變相的老鼠會，它是靠拉人頭賺錢，而不是靠賣產品賺錢，當然表面上是有產品的，在公平會也登記有案，可是實際上很不正派，而且制度有問題沒辦法長久，另外公司賺會員的黑心錢賺很大，這間變相老鼠會將來有機會會再寫。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>正片超難買</title>
        <published>2009-04-02T00:00:00+00:00</published>
        <updated>2009-04-02T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/film/"/>
        <id>https://editor.leonh.space/2009/film/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/film/">&lt;p&gt;晚上跑去買正片，才知道整個八德市沒有一間有在賣正片的 =“=，桃園也只有唯一的一間，就在我小時候家的那邊，實在是一片難求啊！&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>花蓮行之…回家</title>
        <published>2009-03-30T00:00:00+00:00</published>
        <updated>2009-03-30T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/hl4/"/>
        <id>https://editor.leonh.space/2009/hl4/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/hl4/">&lt;p&gt;後面就開始亂拍了 XD。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;hl4&#x2F;R0017202.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;一些心得：在車上拍照典型的狀況除了車窗的反光之外，就是車是一直前進的，景色往往稍縱即逝，若是開 M 模式的話調完快門光圈可能已經在一百公尺之外了，只得視情況用光圈優先或快門優先模式，不過我這台小 GX100 只有光圈先決，也沒什麼景深可言啦，基本上就是可以就按了… =“=。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;hl4&#x2F;R0017230.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;第一次拍這麼多的直立照才發現我直立手持相機整個都是歪的 😝，還得多練練…。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;hl4&#x2F;R0017233.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;GX100 的錄影規格還有很大很大的進步空間（人家已經是 HD 勒，CX1 還在640*480 …）&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;hl4&#x2F;R0017250.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;Picasa 相簿的縮圖品質真的差，壓縮出現的色塊隨處可見，單本相簿照片一多就很難整理，我竟然還跟它買了一年的空間！\ &#x2F;，還是 Flickr 好！&lt;&#x2F;p&gt;
&lt;p&gt;我要大光圈、高動態範圍、偏光鏡！！&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>花蓮行之知卡宣森林公園</title>
        <published>2009-03-28T00:00:00+00:00</published>
        <updated>2009-03-28T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/hl3/"/>
        <id>https://editor.leonh.space/2009/hl3/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/hl3/">&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;hl3&#x2F;R0017137.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;hl3&#x2F;R0017139.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;hl3&#x2F;R0017142.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;知卡宣森林公園，「知卡宣」三字也是從原住民語音譯過來的。這是一個由花蓮縣政府管理的公園，免門票，應該是對當地人來說是很值得一去的吧我想，另外從裡面的樹木可以看出這裡創立的時間應該不會很久，很多的樹都還用支架撐著，很漂亮的地方 :)。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;hl3&#x2F;R0017147.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;hl3&#x2F;R0017165.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;hl3&#x2F;R0017178.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;hl3&#x2F;R0017183.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>花蓮行之慕谷慕魚</title>
        <published>2009-03-27T00:00:00+00:00</published>
        <updated>2009-03-27T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/hl2/"/>
        <id>https://editor.leonh.space/2009/hl2/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/hl2/">&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;hl2&#x2F;R0017088.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;晚上住的是國廣興大飯店，因為包團的關係吧，價錢很便宜，而且一整天下來真的有累到，很快就睡著了 zzZZ。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;hl2&#x2F;R0017091.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;hl2&#x2F;R0017112.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;這天上午的行程主要是慕谷慕魚，這個奇怪的名字是從原住民語音譯過來的，從秀山鄉進入，上面有一些工程奇蹟般的電廠還有自然景觀，原住民的話我只有在商店才看得到 XD，關於慕谷慕魚的資料在網路上都查得到。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;hl2&#x2F;R0017123.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;hl2&#x2F;R0017125.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>花蓮行之太魯閣</title>
        <published>2009-03-26T00:00:00+00:00</published>
        <updated>2009-03-26T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/hl1/"/>
        <id>https://editor.leonh.space/2009/hl1/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/hl1/">&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;hl1&#x2F;R0016791.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;hl1&#x2F;R0016792.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;hl1&#x2F;R0016837.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;這是一個進香團的行程 XD，原來所謂的進香團不是真的都去進香，去玩的心態居多，只是個名義罷了，裡面都是長輩級的團員，我當然又榮登最年輕寶座啦 XD，雖然年紀都是爸媽級的，但也是很好相處的。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;hl1&#x2F;R0016853.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;hl1&#x2F;R0016874.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;hl1&#x2F;R0016902.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;第一天的行程省略掉參拜的廟宇，重點就在太魯閣，從桃園走雪山隧道過去比想像中快很多，這種跟團式的旅遊因為要顧及排程，所以很大的部份都是在車上度過的，導致很多的照片，只要是看到路被切一半的，往往就是從車窗往外照，避免不了的就是玻璃的反光囉 &amp;gt;“&amp;lt;。&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;hl1&#x2F;R0016959.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;hl1&#x2F;R0016965.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;hl1&#x2F;R0016984.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;早上出門起大霧，照片灰濛一片，一直到近午才好一點，不過基本上太陽仍然不大，從進入太魯閣之後看到的都是天空部份泛白一片，毫無細節 =“=，所以來個高動態範圍跟偏光鏡吧！XD&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;hl1&#x2F;R0016988.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;hl1&#x2F;R0017013.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;hl1&#x2F;R0017042.jpg&quot; alt=&quot;&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>心型耶！</title>
        <published>2009-03-21T00:00:00+00:00</published>
        <updated>2009-03-21T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/heart/"/>
        <id>https://editor.leonh.space/2009/heart/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/heart/">&lt;p&gt;啾咪 ^_^&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;heart&#x2F;R0016684-md-lg.JPG&quot; alt=&quot;心型&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>我在河濱公園</title>
        <published>2009-03-16T00:00:00+00:00</published>
        <updated>2009-03-16T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/riverside-park/"/>
        <id>https://editor.leonh.space/2009/riverside-park/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/riverside-park/">&lt;p&gt;今天去了河濱公園，禮拜五的下午當然是沒什麼人的，只有一些河濱部落的原住民在搭房屋，我爬高到河堤上，其實在河堤是看不到太淺的河面的。&lt;&#x2F;p&gt;
&lt;p&gt;在河堤再往外的河床上，有一條興建中和衝突中的腳踏車道，還有一些小型的菜園，那裡的人在燒著一些雜草。&lt;&#x2F;p&gt;
&lt;p&gt;躺在河堤上，眼前只有純粹的天空，「幸好地球是圓的」我當時這樣想著，才可以在這個不算高的地方看到滿滿的天空，今天的下午不是很晴朗，風對著身體的右側吹著，有點冷，不過走這一趟，心裡很孤單是不可避免的，但是有舒服一點點。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>好玩的移軸鏡</title>
        <published>2009-03-10T00:00:00+00:00</published>
        <updated>2009-03-10T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/tilt-shift/"/>
        <id>https://editor.leonh.space/2009/tilt-shift/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/tilt-shift/">&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;tiltshiftmaker.com&#x2F;&quot;&gt;TiltShiftMaker&lt;&#x2F;a&gt;&lt;&#x2F;p&gt;
&lt;p&gt;仿照&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;tinyurl.com&#x2F;y72nq6v7&quot;&gt;移軸鏡&lt;&#x2F;a&gt;感的相片，有一種彷彿置身小人國的感覺。:p&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;tilt-shift&#x2F;r0010701-tiltshift-lg.jpg&quot; alt=&quot;石門水庫&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;tilt-shift&#x2F;r0010698-tiltshift-thumbnail.jpg&quot; alt=&quot;石門水庫&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;tilt-shift&#x2F;r0010698-tiltshift2-thumbnail.jpg&quot; alt=&quot;石門水庫&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;tilt-shift&#x2F;r0010701-tiltshift-thumbnail.jpg&quot; alt=&quot;石門水庫&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;tilt-shift&#x2F;r0010703-tiltshift-thumbnail.jpg&quot; alt=&quot;石門水庫&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>深白色二人組〈Cold Silence〉</title>
        <published>2009-03-07T00:00:00+00:00</published>
        <updated>2009-03-07T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/cold-silence/"/>
        <id>https://editor.leonh.space/2009/cold-silence/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/cold-silence/">&lt;p&gt;好溫暖的聲音。 :)&lt;&#x2F;p&gt;
&lt;iframe src=&quot;https:&#x2F;&#x2F;www.youtube-nocookie.com&#x2F;embed&#x2F;N_CA4HNMzxQ&quot; title=&quot;YouTube video player&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>好閃</title>
        <published>2009-03-06T00:00:00+00:00</published>
        <updated>2009-03-06T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/refection/"/>
        <id>https://editor.leonh.space/2009/refection/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/refection/">&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;refection&#x2F;None-4-md.jpg&quot; alt=&quot;iMac&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：Engadget&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;親愛的 iMac，妳螢幕一直閃下去，我是永遠不會買的。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>三神器</title>
        <published>2009-03-05T00:00:00+00:00</published>
        <updated>2009-03-05T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/3-wishs/"/>
        <id>https://editor.leonh.space/2009/3-wishs/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/3-wishs/">&lt;p&gt;Mac mini&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;3-wishs&#x2F;None-2-md.jpg&quot; alt=&quot;Mac mini&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：Apple&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;Micro Four Third Camera&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;3-wishs&#x2F;None-3-md.jpg&quot; alt=&quot;Panasonic Lumix G&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：Panasonic&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
&lt;p&gt;Android Phone&lt;&#x2F;p&gt;
&lt;figure&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;3-wishs&#x2F;large1-md.jpg&quot; alt=&quot;Android Phone&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;figcaption&gt;來源：Google&lt;&#x2F;figcaption&gt;
&lt;&#x2F;figure&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>IMPOSSIBLE IS NOTHING</title>
        <published>2009-02-18T00:00:00+00:00</published>
        <updated>2009-02-18T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/impossible-is-nothing/"/>
        <id>https://editor.leonh.space/2009/impossible-is-nothing/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/impossible-is-nothing/">&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;impossible-is-nothing&#x2F;None-1-md.jpeg&quot; alt=&quot;IMPOSSIBLE IS NOTHING&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;楊丞琳最近幫 Adidas 拍的一系列形象廣告，要求自己比原本定下的目標多 10，在以不造成運動傷害的前提下，多跑 10 分鐘，呼吸調整好、腳步調整好、節奏調整好，GO！&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>吊扇</title>
        <published>2009-02-17T00:00:00+00:00</published>
        <updated>2009-02-17T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/fan/"/>
        <id>https://editor.leonh.space/2009/fan/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/fan/">&lt;p&gt;夏天快到了，在想著要不要裝吊扇，主要的考慮點還是家裏空間太小，放個電扇在那邊很佔位置又會把紙張吹跑，加上我個人的體質，就算是夏天要是開著電扇對我狂吹的話也覺得冷，吊扇似乎是個不錯的選擇，微微的風，又兼具照明功能，可以順道把用了八百年的老燈具換掉。&lt;&#x2F;p&gt;
&lt;p&gt;我心目中的吊扇是…白色的、扇葉不用多、懸吊距離短、設計簡單、不要有一條鍊子垂在那邊、也不要有喇叭或花朵造型的燈。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>尷尬的網誌敘述</title>
        <published>2009-02-16T00:00:00+00:00</published>
        <updated>2009-02-16T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/blog-description/"/>
        <id>https://editor.leonh.space/2009/blog-description/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/blog-description/">&lt;p&gt;好幾個小時前到剛剛我好像得失心瘋的一樣瘋狂的看網路賺錢的資訊，有些人還真的滿厲害的，一個月上千元新臺票的這樣賺…害我又動起凡心來 XD，像這樣簡潔的版面即將被我破壞殆盡？點去看一些廣告商的方案，台灣的那兩大名字很像的都有規定說廣告要放在網頁的比較高的地方，那像我這種沒 sidebar 的怎麼辦？只能放 header 乎？啊可是它們的廣告好像直的比較多耶…。&lt;&#x2F;p&gt;
&lt;p&gt;另外一個讓我感到尷尬的所在，它們在申請的表單都要填網站的敘述，這對像我這種沒什麼主題的 blog 就尷尬啦…啊…就…個人 blog 啊～偶爾摻點開箱文，技術文，沒了！身為一個尷尬網誌寫作者，會發生這樣的事也是很合情合理的吧！&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>發表總檢討</title>
        <published>2009-02-15T00:00:00+00:00</published>
        <updated>2009-02-15T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/review/"/>
        <id>https://editor.leonh.space/2009/review/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/review/">&lt;p&gt;這次首次的發表，表現奇差無比…，剛在心中檢討一下，歸納出一些因素，首先整體進度完全沒有安排，看的進度沒有掌握，導致最後接近死線的這禮拜每天睡不到三四個小時，即使這樣還是沒看完，典型的事倍功半的傻瓜。&lt;&#x2F;p&gt;
&lt;p&gt;雖然說對於要用到私人時間去讀資料讓我很不爽，不過假如拋開這點，在不得不然的情況下，從年假前到這禮拜這好幾周的時間，在有規劃進度的狀況下，這次的狀況應該不會再發生。&lt;&#x2F;p&gt;
&lt;p&gt;第二點是在讀資料的時候，為求速成只專注在結果上，忽略它的機制或機構，只知其然不知其所以然，結果就是當上台時講起來很空洞而且完全沒有說服力，還經不起考驗，一旦有人提問就…。後來我想一想，應該用洋文的 &lt;strong&gt;What、Where、Why、How&lt;&#x2F;strong&gt; 來解釋那些元件：這玩意是啥？他在什麼位置？為甚麼在這？如何跑到這來？如此一來就不會發生今日的窘狀…囧rz。&lt;&#x2F;p&gt;
&lt;p&gt;最後抱怨的時間到了，會成這樣拖拖拉拉的有很大的因素是這佔用到我大量的私人時間，搞得我看的超級不甘願，這些時間我可以去學其它方面我想上的課，為甚麼一定要耗在跟這些資料搏鬥，做義工是佛心來著的是吧！&lt;&#x2F;p&gt;
&lt;p&gt;我討厭紙上作業。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Chambers</title>
        <published>2009-02-13T00:00:00+00:00</published>
        <updated>2009-02-13T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/chambers/"/>
        <id>https://editor.leonh.space/2009/chambers/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/chambers/">&lt;p&gt;買到了 Steady &amp;amp; Co. 的 Chambers，大一時聽過的唱片，距今已八年啦！完全只為了一首 Only Holy Story！&lt;&#x2F;p&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;chambers&#x2F;post-1361090-1219406647-md.jpg&quot; alt=&quot;Chambers&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;iframe src=&quot;https:&#x2F;&#x2F;www.youtube-nocookie.com&#x2F;embed&#x2F;la4AKyMu8NU&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>綠豆椪、蔥油餅的熱量</title>
        <published>2009-02-12T00:00:00+00:00</published>
        <updated>2009-02-12T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/calorie/"/>
        <id>https://editor.leonh.space/2009/calorie/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/calorie/">&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;calorie&#x2F;1186681130-md.jpg&quot; alt=&quot;綠豆椪&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;太恐怖了！綠豆椪的熱量光一個高達四百大卡，相當於一個早餐店漢堡、一個小型便當 =“=。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;&lt;img src=&quot;https:&#x2F;&#x2F;editor.leonh.space&#x2F;2009&#x2F;calorie&#x2F;unnamed-16-md.jpeg&quot; alt=&quot;蔥油餅&quot; &#x2F;&gt;&lt;&#x2F;p&gt;
&lt;p&gt;繼恐怖的綠豆碰之後&lt;br &#x2F;&gt;
又出現了恐怖的蔥油餅&lt;br &#x2F;&gt;
一個小小的蔥油餅&lt;br &#x2F;&gt;
熱量跟支香噴噴的炸雞腿一樣&lt;br &#x2F;&gt;
五百大卡起跳&lt;br &#x2F;&gt;
嘖嘖嘖…&lt;&#x2F;p&gt;
&lt;p&gt;然後 David 說，不要期待把事情做完再去運動，事情是永遠做不完的！自己規劃一個時間，時間到了就放下手邊工作去運動。&lt;&#x2F;p&gt;
&lt;p&gt;他說的對！&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>關於孩子</title>
        <published>2009-02-09T00:00:00+00:00</published>
        <updated>2009-02-09T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/baby/"/>
        <id>https://editor.leonh.space/2009/baby/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/baby/">&lt;blockquote&gt;
&lt;div class=&quot;quote&quot;&gt;
&lt;p&gt;以前我在紐奧良時，有個還沒做爸爸，但正在考慮中的年輕男人說，「我注意到那些沒小孩的人，似乎從來沒辦法覺得自己真的很成功，而那些有小孩的人，卻從不覺得自己真的很失敗。」…&lt;&#x2F;p&gt;
&lt;p&gt;我盯著他這段話想了六個月，才想通它的因果關係：孩子教你學會包容。不是讓你變得順從，而是更有耐性。容忍小小的耽擱。你不會再用傳統的成敗標準來衡量自己。很多父母提到生兒育女讓他們變成較好的人，其中一個原因就是－它迫使你的心變得比較有彈性。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;另一個好處是他教你了解為人慷慨的喜悅。由於必須對孩子付出那麼多，於是我們漸漸習慣了這是件多麼讓人滿足的事。我們打開心胸，接納所有感性的回饋，過去我們本來是不信這套的。我們不僅從孩子身上得到這種回饋，從別處也一樣。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;當你把這許多禮物放在一起，了解它們的好處之後，你就不會再像過去一樣，把生孩子當成「我或我的小孩」的一種交易，只會想到犧牲或是自私之類的事情。你會發現孩子帶給你一種新的視野，也許可以永遠解除你的不安。&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
&lt;div class=&quot;source&quot;&gt;
&lt;p&gt;〈接受上天的恩賜〉&lt;&#x2F;p&gt;
&lt;&#x2F;div&gt;
&lt;&#x2F;blockquote&gt;
&lt;p&gt;我得好好的想一想…。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>心智圖</title>
        <published>2009-02-08T00:00:00+00:00</published>
        <updated>2009-02-08T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/mindmap/"/>
        <id>https://editor.leonh.space/2009/mindmap/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/mindmap/">&lt;p&gt;新的一年開始了，這個荒廢許久的小小網誌也活了起來。&lt;&#x2F;p&gt;
&lt;p&gt;另外很奇怪，當 blog 荒廢的時候，總是有滿多東西在腦子裡發酵盤旋想寫的耶…可是每次 blog 一弄好那些個靈感又都消失無蹤？@@ 這次的熱情不知道能維持多久喔…。&lt;&#x2F;p&gt;
&lt;h2 id=&quot;xin-zhi-tu&quot;&gt;心智圖&lt;&#x2F;h2&gt;
&lt;p&gt;近來學習到心智圖的用法跟作法，早期見到心智圖並不覺得那有什麼過人之處，實際上學過跟用過之後才體會到它的優點，我認為重點在不同於傳統以分類、階層式的思考模式，它的類腦神經元的思考方式的確帶給我全然不同的記憶方式，很實用的東西。我用的軟體是 XMind，當前 Windows 和 Linux 平台上最棒的心智圖軟體，最大的缺點是很吃記憶體 :(。另外還有一些網站也可以畫，我大都試過了，功能都太弱，不適合實際製作。&lt;&#x2F;p&gt;
&lt;p&gt;這次的說話課進度是每位同學都要上台發表不限主題的談話，時間五到十分鐘，儘管不是強制的，當然還是去台上培養經驗比較好。我挑的主題是對一般人而言有點距離的自由軟體，在我舌燦蓮花的奇技淫巧之下應該不是什麼大問題啦 :p。&lt;&#x2F;p&gt;
&lt;p&gt;這次挑題目的準則：只要靠嘴巴、不用投影片、不用講義、概念不複雜、專有名詞少。故此原本在候選名單的東眼山被刷掉了，那裡真是美到讓我難以忘懷啊！…離題了 =“=。&lt;&#x2F;p&gt;
&lt;p&gt;結果客戶來拜訪，下班陪客戶吃飯，沒去上課，ㄎ。&lt;&#x2F;p&gt;
&lt;hr &#x2F;&gt;
&lt;p&gt;牛年快樂 ^___^。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>比巧克力還巧</title>
        <published>2009-01-12T00:00:00+00:00</published>
        <updated>2009-01-12T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/coincidence/"/>
        <id>https://editor.leonh.space/2009/coincidence/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/coincidence/">&lt;p&gt;前幾天傍晚在墊腳石遇到暴力小英&lt;br &#x2F;&gt;
晚上又遇到周經理&lt;br &#x2F;&gt;
不過他好像沒認出我 =“=&lt;&#x2F;p&gt;
&lt;p&gt;遇到暴力小英&lt;br &#x2F;&gt;
她又丟給我一個新任務&lt;br &#x2F;&gt;
解完只能賺一點微薄的經驗值…&lt;&#x2F;p&gt;
&lt;p&gt;今天買了：&lt;&#x2F;p&gt;
&lt;table&gt;&lt;thead&gt;&lt;tr&gt;&lt;th&gt;#&lt;&#x2F;th&gt;&lt;th style=&quot;text-align: right&quot;&gt;$$&lt;&#x2F;th&gt;&lt;&#x2F;tr&gt;&lt;&#x2F;thead&gt;&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;行事曆&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;$139&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;藥&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;$270&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;放卡片的小袋子&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;$16&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;停車費&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;$40&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;溫泉蛋飯糰&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;$22&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;tr&gt;&lt;td&gt;聚餐錢&lt;&#x2F;td&gt;&lt;td style=&quot;text-align: right&quot;&gt;$100&lt;&#x2F;td&gt;&lt;&#x2F;tr&gt;
&lt;&#x2F;tbody&gt;&lt;&#x2F;table&gt;
&lt;p&gt;藥錢已經爆表了&lt;br &#x2F;&gt;
可是又不能不買&lt;br &#x2F;&gt;
不買怎麼回血？&lt;br &#x2F;&gt;
怎麼打怪？&lt;br &#x2F;&gt;
還好買一次可以頂很久&lt;br &#x2F;&gt;
這還好&lt;&#x2F;p&gt;
&lt;p&gt;停車費妖獸貴 =“=&lt;br &#x2F;&gt;
連摩托車都一位難求啊中壢&lt;&#x2F;p&gt;
&lt;p&gt;雜貨錢也爆…&lt;br &#x2F;&gt;
真的都不能再買啦！&lt;&#x2F;p&gt;
&lt;p&gt;其實衣服錢也爆了&lt;br &#x2F;&gt;
可是過年前要剪一次頭髮&lt;br &#x2F;&gt;
剪 100 塊的就好&lt;br &#x2F;&gt;
不用洗&lt;br &#x2F;&gt;
謝謝&lt;&#x2F;p&gt;
&lt;p&gt;忘記買威力彩&lt;br &#x2F;&gt;
可惡啊！&lt;&#x2F;p&gt;
&lt;p&gt;那個溫泉蛋&lt;br &#x2F;&gt;
還真好吃呢！（大心）&lt;&#x2F;p&gt;
&lt;p&gt;還有一二月份的發票是漂亮的藍色喔！&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>20090108</title>
        <published>2009-01-09T00:00:00+00:00</published>
        <updated>2009-01-09T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/20090108/"/>
        <id>https://editor.leonh.space/2009/20090108/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/20090108/">&lt;h2 id=&quot;00&quot;&gt;00&lt;&#x2F;h2&gt;
&lt;p&gt;待會準備要睡囉&lt;br &#x2F;&gt;
明天晚上要上課&lt;br &#x2F;&gt;
心智圖印的有夠差&lt;br &#x2F;&gt;
Linux 下的列印還有待加強&lt;&#x2F;p&gt;
&lt;p&gt;明天把數字標籤再看一遍&lt;br &#x2F;&gt;
有的太牽強的實在是記不得&lt;br &#x2F;&gt;
還有開始看第二篇專利&lt;br &#x2F;&gt;
一樣是 Bosch 的&lt;&#x2F;p&gt;
&lt;p&gt;然後今天又暴飲暴食了 =“=&lt;br &#x2F;&gt;
早上八點起床&lt;br &#x2F;&gt;
早上先運動一下好了&lt;br &#x2F;&gt;
仰臥起坐跟啞鈴&lt;br &#x2F;&gt;
然後看數字標籤跟專利&lt;br &#x2F;&gt;
下午早一點點出門先去中壢墊腳石&lt;br &#x2F;&gt;
挑挑看有沒有適合的手帳&lt;br &#x2F;&gt;
有下雨的話就再說&lt;br &#x2F;&gt;
買威力彩 = =+&lt;&#x2F;p&gt;
&lt;h2 id=&quot;05&quot;&gt;05&lt;&#x2F;h2&gt;
&lt;p&gt;怎麼會這麼早爬起來&lt;br &#x2F;&gt;
太恐怖了吧…&lt;br &#x2F;&gt;
睡不到六個鐘頭&lt;br &#x2F;&gt;
等等再回去睡回籠覺&lt;br &#x2F;&gt;
不過突然有個關於時間軸的點子&lt;br &#x2F;&gt;
有點像 Plurk 那樣&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>20090107</title>
        <published>2009-01-08T00:00:00+00:00</published>
        <updated>2009-01-08T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/20090107/"/>
        <id>https://editor.leonh.space/2009/20090107/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/20090107/">&lt;p&gt;凌晨四點多睡&lt;br &#x2F;&gt;
中午十一點半起床&lt;br &#x2F;&gt;
睡了七個半小時&lt;br &#x2F;&gt;
還好&lt;&#x2F;p&gt;
&lt;p&gt;剛剛起來就看到外面又是陰雨天&lt;br &#x2F;&gt;
想買兩樣東西&lt;br &#x2F;&gt;
水跟手帳&lt;br &#x2F;&gt;
看來等到明天上課再順道買好了&lt;&#x2F;p&gt;
&lt;p&gt;這禮拜無薪假的第三天&lt;br &#x2F;&gt;
目標是把第一篇專利看完&lt;br &#x2F;&gt;
還有把心智圖跟數字標籤稍微複習一下&lt;br &#x2F;&gt;
不要暴飲暴食&lt;br &#x2F;&gt;
記得運動&lt;&#x2F;p&gt;
&lt;p&gt;剛剛那顆大飯糰就當早餐兼午餐了吧&lt;br &#x2F;&gt;
再吃也吃不下了&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>20090106</title>
        <published>2009-01-07T00:00:00+00:00</published>
        <updated>2009-01-07T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2009/20090106/"/>
        <id>https://editor.leonh.space/2009/20090106/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2009/20090106/">&lt;p&gt;今天因為有 Macworld 轉播&lt;br &#x2F;&gt;
所以會比較晚睡&lt;br &#x2F;&gt;
明天起床時間十點&lt;&#x2F;p&gt;
&lt;p&gt;保持每日跑步三十分鐘&lt;br &#x2F;&gt;
仰臥起坐三十分鐘&lt;br &#x2F;&gt;
啞鈴一百下&lt;br &#x2F;&gt;
記帳時間至多三十分鐘&lt;br &#x2F;&gt;
每天都要洗澡 ^^“&lt;br &#x2F;&gt;
刷牙&lt;br &#x2F;&gt;
擦藥&lt;br &#x2F;&gt;
找不到護唇膏的話只好再買一個&lt;&#x2F;p&gt;
&lt;p&gt;持之以恆吧&lt;br &#x2F;&gt;
跟記帳一樣&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>The Last Stop</title>
        <published>2008-06-01T00:00:00+00:00</published>
        <updated>2008-06-01T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2008/stop/"/>
        <id>https://editor.leonh.space/2008/stop/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2008/stop/">&lt;p&gt;好的，用力記住，一輩子都不能忘。&lt;br &#x2F;&gt;
壞的，也要記起來，學著讓自己更好。&lt;&#x2F;p&gt;
&lt;p&gt;As my soul heals the shame&lt;br &#x2F;&gt;
I will grow through this pain&lt;br &#x2F;&gt;
Lord I’m doing all I can&lt;br &#x2F;&gt;
To be a better man&lt;&#x2F;p&gt;
&lt;iframe src=&quot;https:&#x2F;&#x2F;www.youtube-nocookie.com&#x2F;embed&#x2F;2FnsJ_Dm4As&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Notepad++ 的 column mode</title>
        <published>2008-05-25T00:00:00+00:00</published>
        <updated>2008-05-25T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2008/notepad/"/>
        <id>https://editor.leonh.space/2008/notepad/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2008/notepad/">&lt;p&gt;Notepad++ 是一個比記事本強上千百倍而且又愛台灣的編輯器，對 Unicode 有良好的支援，作者是在法國的台灣人，所以這個軟體也有親切到不行的中文介面，絕非其它部份翻譯的狗屁不通的軟體可比擬，最大的重點是不用錢！&lt;&#x2F;p&gt;
&lt;p&gt;Column mode 好像沒有看到比較統一的中文翻譯，一般會說成垂直選取或者是欄編輯模式。簡單來說，正常的文件編輯狀況下，選取的功能都是橫式的由左到右，這跟英文的書寫習慣有關，而這類的文字編輯器並沒有橫書／直書的格式可以選擇，一律都是橫式，而所謂的 column mode 指的就是可以直的選取、編輯等等，文字、段落的走向是橫的，不過選取是垂直的，就是這個意思。&lt;&#x2F;p&gt;
&lt;p&gt;不巧這個平時很少用到的功能今天派上用場，Notepad++ 切換成 column mode 很簡單，先按住 &lt;kbd&gt;Alt&lt;&#x2F;kbd&gt; 再選取就是了。&lt;&#x2F;p&gt;
&lt;p&gt;Notepad++ 的作者侯先生不僅免費給我們這麼好用的軟體，連原始碼都開放出來，真的是佛心來著的。&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>石門水庫補完</title>
        <published>2008-04-30T00:00:00+00:00</published>
        <updated>2008-04-30T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2008/shihmen/"/>
        <id>https://editor.leonh.space/2008/shihmen/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2008/shihmen/">&lt;p&gt;石門水庫收票口進去後不久左手邊有溪州公園，公園往上有一步道，某次去走走到中途的涼亭就太累了…放棄 =“=，可是心裡老是惦記著這事，總想有一天非把它走過一趟完整的不可，就在禮拜六終於可以成行了。&lt;&#x2F;p&gt;
&lt;p&gt;這次一樣先走到中途的涼亭休息一會，繼續往上之後到一個三岔路口，往下走是來路溪州公園，往上走是溪州山，另一側往下走是雲霄山莊，也就是壩頂附近，如果不算溪州山的話，看來這個交會處就是溪州公園往雲霄飯店的頂點，這裡離剛剛休息的涼亭沒多遠，上次應該繼續走完的 =“=。&lt;&#x2F;p&gt;
&lt;p&gt;一路往雲霄飯店走，雲霄飯店不知是在哪年開始結束營業，一直整修到現在都未完工，變成半荒廢的工地，對於第一次走的人來說，在山裡面，前後無人，又不知道終點的情形下是有一些恐怖，不過實際上那邊的工地不要太白目亂闖的話，安全是無虞的，而且實際上到雲霄飯店幾乎就是終點了，再往下走一小小段路就是壩頂旁邊，是一個漂亮的碼頭，最後就是繞大馬路走回公園對面的停車場啦～。&lt;&#x2F;p&gt;
&lt;p&gt;在往壩頂的一路上往下面看都可以看到有很大的水池，後來下去之後開車到收票口轉個彎過去近一點看看，下面遊客比較少，而且景色由上往下看和由下往上看是別有一番風味的，我比較喜歡下面的感覺 ^^。這次其實還有往溪州山那條還沒走完，就等下次吧，我要把石門水庫全攻略！&lt;&#x2F;p&gt;
&lt;p&gt;走台四線往水庫方向過了崁津大橋後不久左手邊有一座五路財神廟，之前看摩托車週記介紹過，可是都沒說在哪邊，還真巧給我遇到。@@&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>重新開始跑步</title>
        <published>2008-04-24T00:00:00+00:00</published>
        <updated>2008-04-24T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2008/jogging/"/>
        <id>https://editor.leonh.space/2008/jogging/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2008/jogging/">&lt;p&gt;休息了幾天&lt;br &#x2F;&gt;
今天又開始跑步了&lt;br &#x2F;&gt;
狀況越來越好&lt;br &#x2F;&gt;
決定停下來的原因是不想跑&lt;br &#x2F;&gt;
並不是跑不動&lt;br &#x2F;&gt;
畢竟…循序漸進比較好&lt;br &#x2F;&gt;
也抓到一些訣竅…呼吸跟節奏真的很重要&lt;&#x2F;p&gt;
&lt;p&gt;跑步…也是一種陶冶性情的方式&lt;&#x2F;p&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>請假睡過頭</title>
        <published>2008-04-21T00:00:00+00:00</published>
        <updated>2008-04-21T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2008/off/"/>
        <id>https://editor.leonh.space/2008/off/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2008/off/">&lt;p&gt;原本像趁著今天工作上難得的喘息，請個半天假去把一些瑣碎的事情辦一辦的，沒想到一個不注意…醒來就中午了 XD，只好趕緊弄一弄。&lt;&#x2F;p&gt;
&lt;p&gt;第一站是台灣銀行，領了金融卡，結果出乎意料的不是 Visa 金融卡，是很普通的金融卡，我記得當初是選 Visa 金融卡的啊 @@，然後現在在台銀用網路 ATM 轉帳成功一筆，就可以現領一台讀卡機，等於花八塊的手續費換一台讀卡機，相當地划算，像現在報稅季節到了有讀卡機和自然人憑證就粉方便。&lt;&#x2F;p&gt;
&lt;p&gt;第二站是開證券戶，不過剛好經過大潤發看到對面有捐血車在彰銀前面，就很大方的把車停進彰銀停車格跑去捐血 XD，這次的贈品除了食物外，是一個不知道幹嘛有點莫名其妙的小袋子 @@，還是台南的比較闊氣，送飯店餐券，總之那個袋子我沒拿，捐血的神聖任務也完成了，捐完血可是讓我體重瞬間下降 0.5 公斤，整個人都瘦了一圈呢！XD&lt;&#x2F;p&gt;
&lt;p&gt;回到原本的主線任務，跑去證券行開戶，結果我到的時間人家已經收盤很久了，大廳的燈都關囉，可是那邊的行員還真是出乎我意料的親切（大心 ^^），完全不會因為我是個新小戶就怠慢，比我想像中的好太多，這樣三件事做下來也花了一個下午，到家裏附近買個碗糕、一罐啤酒，吃從醒來到此刻的第一頓像樣的食物，話說都沒吃就跑去捐血捐完還真的有點暈暈的。&lt;&#x2F;p&gt;
&lt;p&gt;今天意外的聽到了兩首歌，My Little Airport 跟陳建年的，我好像從沒好好認真的聽陳建年的歌，從現在起我會注意他的歌曲。&lt;&#x2F;p&gt;
&lt;iframe src=&quot;https:&#x2F;&#x2F;www.youtube-nocookie.com&#x2F;embed&#x2F;84KyqXsmXPk?controls=0&quot; frameborder=&quot;0&quot; allow=&quot;accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share&quot; allowfullscreen&gt;&lt;&#x2F;iframe&gt;
</content>
        
    </entry>
    <entry xml:lang="zh-Hant">
        <title>Feed 的翻譯</title>
        <published>2005-10-27T00:00:00+00:00</published>
        <updated>2005-10-27T00:00:00+00:00</updated>
        
        <author>
          <name>
            
              Unknown
            
          </name>
        </author>
        
        <link rel="alternate" type="text/html" href="https://editor.leonh.space/2005/feed/"/>
        <id>https://editor.leonh.space/2005/feed/</id>
        
        <content type="html" xml:base="https://editor.leonh.space/2005/feed/">&lt;p&gt;&lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;n.yam.com&#x2F;&quot;&gt;蕃薯藤新聞&lt;&#x2F;a&gt;的 feed 服務翻譯成「資訊快遞」，發覺把 feed 翻譯成「快遞」還滿傳神的，feed 這個新應用要落實平民化的過程，中文翻譯是一定要的。&lt;&#x2F;p&gt;
&lt;p&gt;像我爸，完全低階的使用者（可是他會用 Gmail ^^），有一次他問我該怎麼針對文章的標題或時間去做搜尋，我答不出來…，雖然網頁檔案有時間的屬性，內文中也大多有標示時間，可是對電腦來說那只是文字，特別的例外是 &lt;a rel=&quot;external&quot; href=&quot;https:&#x2F;&#x2F;news.google.com.tw&#x2F;&quot;&gt;Google 新聞&lt;&#x2F;a&gt;，它可以把蒐集的新聞中的時間解析出來，但我猜想這個解析的初步作業裡含有人工的成分，但這並不影響 Google 新聞的呈現，因此並沒有違背 Google 說的「Google 新聞沒有任何編輯人員可以選取報導，或決定哪則報導會放在最上方。」，但是回到原本的主題來，此法乃特解，這時候 feed 的好處就出現了，它的格式裡面就有定義好了標題、時間等的標籤，相當的格式化，這才是通解啊。&lt;&#x2F;p&gt;
</content>
        
    </entry>
</feed>
