<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:cc="http://cyber.law.harvard.edu/rss/creativeCommonsRssModule.html">
    <channel>
        <title><![CDATA[Stories by Ray Lee | 李宗叡 on Medium]]></title>
        <description><![CDATA[Stories by Ray Lee | 李宗叡 on Medium]]></description>
        <link>https://medium.com/@raycodingjourney?source=rss-b11a48c41717------2</link>
        <image>
            <url>https://cdn-images-1.medium.com/fit/c/150/150/1*xtAjPjwtqoIiIOe3UAKohw.jpeg</url>
            <title>Stories by Ray Lee | 李宗叡 on Medium</title>
            <link>https://medium.com/@raycodingjourney?source=rss-b11a48c41717------2</link>
        </image>
        <generator>Medium</generator>
        <lastBuildDate>Sat, 14 Nov 2026 11:23:55 GMT</lastBuildDate>
        <atom:link href="https://medium.com/@raycodingjourney/feed" rel="self" type="application/rss+xml"/>
        <webMaster><![CDATA[yourfriends@medium.com]]></webMaster>
        <atom:link href="http://medium.superfeedr.com/" rel="hub"/>
        <item>
            <title><![CDATA[CSS Position]]></title>
            <link>https://medium.com/learn-or-die/css-position-a77eed52c1ce?source=rss-b11a48c41717------2</link>
            <guid isPermaLink="false">https://medium.com/p/a77eed52c1ce</guid>
            <category><![CDATA[css]]></category>
            <category><![CDATA[frontend]]></category>
            <dc:creator><![CDATA[Ray Lee | 李宗叡]]></dc:creator>
            <pubDate>Sat, 20 Dec 2025 02:01:27 GMT</pubDate>
            <atom:updated>2025-12-20T02:01:27.651Z</atom:updated>
            <content:encoded><![CDATA[<h4># 前言</h4><p>深入理解 CSS position：五種定位方式的行為與比較</p><p>在 CSS 中，position 屬性決定了元素在頁面中的定位方式。雖然語法只有幾種選項，但它們的行為差異很大，掌握這些特性能幫助我們更靈活地控制版面。以下逐一介紹 static、relative、absolute、fixed、sticky 五種模式。</p><h4># Demo</h4><p>已經大概懂理論的可跳過以下內容，直接看 <a href="https://blog.learn-or-die.com/demo/css-position">Demo</a></p><h4># static（預設）</h4><ul><li>所有元素的預設值。</li><li>完全遵循文件流（normal flow）排版。</li><li>top / left / right / bottom 無效。</li><li>z-index 無效。</li><li>使用情境</li><li>適合大部分一般排版。</li></ul><h4># relative（相對定位）</h4><ul><li>佔位與 static 一樣，仍在文件流中。</li><li>可以透過 top / left / right / bottom 做「相對原位」的偏移。</li><li>偏移後，原本的空間依舊保留 → 容易與其他元素重疊。</li><li>可理解為，佔位依舊，偏移只是渲染的結果，所以其他元素的佔位都跟 static 一樣，不受到影響</li><li>可作為子元素 absolute 的定位基準。</li><li>使用情境</li><li>小幅度調整元素位置。</li><li>建立「定位上下文」給子元素。</li></ul><h4># absolute（絕對定位）</h4><ul><li>脫離文件流，不再佔空間。</li><li>依附於最近的「非 static 祖先」（relative / absolute / fixed / sticky）。</li><li>如果找不到，就以整個 &lt;html&gt; 為基準。</li><li>使用情境</li><li>Tooltip、彈出視窗、容器內的絕對位置元素。</li><li>需要在容器中「自由擺放」的元件。</li></ul><h4># fixed（固定定位）</h4><ul><li>脫離文件流，不再佔空間。</li><li>參考點為整個 viewport（瀏覽器視窗）。</li><li>捲動頁面時，位置保持不動。</li><li>使用情境</li><li>固定導覽列、浮動按鈕、全螢幕遮罩。</li></ul><p>⸻</p><ol><li>sticky（黏性定位）</li></ol><ul><li>混合 relative 與 fixed 的特性。</li><li>一開始跟著文件流排版（像 relative）。</li><li>當捲動超過指定門檻（如 top: 0），會黏在容器邊界。</li><li>黏附的範圍只在「最近的可捲動容器」內，不會超出容器。</li><li>使用情境</li><li>表格表頭固定。</li><li>容器內的區域標籤列。</li></ul><h4># 比較表</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*wxCuITw0JJYUTiAX4j8IfQ.png" /></figure><h4># 一句話速記</h4><ul><li>static：乖乖照文件流。</li><li>relative：在隊伍裡，微調位置。</li><li>absolute：脫隊，自由定位，但要找基準。</li><li>fixed：脫隊，永遠固定在螢幕。</li><li>sticky：在 scrollable 容器裡，先跟著走，遇到邊界就黏住。</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=a77eed52c1ce" width="1" height="1" alt=""><hr><p><a href="https://medium.com/learn-or-die/css-position-a77eed52c1ce">CSS Position</a> was originally published in <a href="https://medium.com/learn-or-die">Learn or Die</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[使用 RAF 實作 Debounce]]></title>
            <link>https://medium.com/learn-or-die/%E4%BD%BF%E7%94%A8-raf-%E5%AF%A6%E4%BD%9C-debounce-63e57704fea0?source=rss-b11a48c41717------2</link>
            <guid isPermaLink="false">https://medium.com/p/63e57704fea0</guid>
            <category><![CDATA[browsers]]></category>
            <category><![CDATA[react]]></category>
            <category><![CDATA[frontend]]></category>
            <dc:creator><![CDATA[Ray Lee | 李宗叡]]></dc:creator>
            <pubDate>Mon, 15 Dec 2025 00:37:16 GMT</pubDate>
            <atom:updated>2025-12-15T00:37:16.262Z</atom:updated>
            <content:encoded><![CDATA[<h4># 前言</h4><p>使用 RAF 實作 debounce 功能</p><h4># 程式碼</h4><pre>React.useEffect(() =&gt; {<br>    // 在非 mobile 裝置上，不顯示<br>    if (!isMobile) {<br>        setShowCooperativeOverlay(false);<br>        if (overlayHideRafRef.current) {<br>            // 從 RAF queue 當中移除當前 queue 當中的 task<br>            cancelAnimationFrame(overlayHideRafRef.current);<br>            overlayHideRafRef.current = null;<br>        }<br>        return;<br>    }<br><br>    const showAndDebounceHide = () =&gt; {<br>        // 這邊是主要顯示 overlay 的開關，當 event 偵測到，就把開關打開<br>        setShowCooperativeOverlay((prev) =&gt; true);<br>        // 儲存打開的時間點<br>        lastActivityTsRef.current = performance.now();<br>        // 這個是要放入 RAF queue 的 callback，它的作用只有一個，那就是在指定的區間過後，關閉 overlay<br>        const tick = (now) =&gt; {<br>            // 由於 ref 跟 now 都是動態變動的，所以每一個 frame 都會執行該 callback，判斷是否超過指定時間區間<br>            if (now - lastActivityTsRef.current &gt;= HIDE_DELAY_MS) {<br>                // 關閉 overlay<br>                setShowCooperativeOverlay(false);<br>                // 清理 hide ref，這樣下次又觸發時，又會塞一個進去<br>                overlayHideRafRef.current = null;<br>                // 這邊就 return 掉了，相當於 break 遞迴，所以下一個 frame 就不會再執行這個 callback<br>                return;<br>            }<br>            // 這邊是一個遞迴，所以每一個 frame 的 RAF 都會執行這個 callback<br>            overlayHideRafRef.current = requestAnimationFrame(tick);<br>        };<br><br>        // 如果 hide ref 是 null，那就開始遞迴 callback<br>        if (!overlayHideRafRef.current) {<br>            overlayHideRafRef.current = requestAnimationFrame(tick);<br>        }<br>    };<br><br>    const onWheel = () =&gt; showAndDebounceHide();<br>    const onTouchMove = () =&gt; showAndDebounceHide();<br><br>    // 監聽兩個事件，並且使用 passive 來做效能優化<br>    window.addEventListener(&#39;wheel&#39;, onWheel, { passive: true });<br>    window.addEventListener(&#39;touchmove&#39;, onTouchMove, { passive: true });<br><br>    return () =&gt; {<br>        window.removeEventListener(&#39;wheel&#39;, onWheel);<br>        window.removeEventListener(&#39;touchmove&#39;, onTouchMove);<br>        // 如果 hide ref 不為空，代表目前還在遞迴 callback 中，清理 RAF<br>        if (overlayHideRafRef.current) {<br>            cancelAnimationFrame(overlayHideRafRef.current);<br>            overlayHideRafRef.current = null;<br>        }<br>    };<br>}, [isMobile]);</pre><h4># 示意圖</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*kqMZc_ugZhi760Q6J00aXg.png" /></figure><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=63e57704fea0" width="1" height="1" alt=""><hr><p><a href="https://medium.com/learn-or-die/%E4%BD%BF%E7%94%A8-raf-%E5%AF%A6%E4%BD%9C-debounce-63e57704fea0">使用 RAF 實作 Debounce</a> was originally published in <a href="https://medium.com/learn-or-die">Learn or Die</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[MySQL Tablespace]]></title>
            <link>https://medium.com/learn-or-die/mysql-tablespace-aef58b43925c?source=rss-b11a48c41717------2</link>
            <guid isPermaLink="false">https://medium.com/p/aef58b43925c</guid>
            <category><![CDATA[mysql]]></category>
            <dc:creator><![CDATA[Ray Lee | 李宗叡]]></dc:creator>
            <pubDate>Wed, 10 Dec 2025 05:26:05 GMT</pubDate>
            <atom:updated>2025-12-10T05:26:05.786Z</atom:updated>
            <content:encoded><![CDATA[<h4># 前言</h4><p>有經驗的 MySQL 運維常常會建議 <strong>將 innodb_undo_tablespaces 设置成 2, 或更大的值</strong>，為什麼？</p><p>在 <strong>InnoDB</strong> 中，<strong>innodb_undo_tablespaces</strong> 是一個經常被提到的參數，但很多人對它的內涵並不完全清楚。本文會從 Undo Log → Rollback Segment → Undo Tablespace → I/O 機制 的角度來逐層解析，並回答為什麼 MySQL 建議將 innodb_undo_tablespaces 設為 2 或以上。</p><h4># Undo Log 的作用</h4><p>在事務型資料庫中，Undo Log（撤銷日誌） 至關重要，主要功能有三：</p><ul><li>事務回滾 (Rollback)：當交易失敗或顯式回滾時，Undo Log 能將資料恢復到先前狀態。</li><li>MVCC (多版本並發控制)：查詢需要舊版本資料時，透過 Undo Log 建立一致性快照。</li><li>一致性保障：確保部分失敗的交易不會破壞資料正確性。</li></ul><h4># Undo Tablespace 是什麼？</h4><ul><li>Undo Tablespace = 磁碟檔案 (undo_001.ibt, undo_002.ibt …)。</li><li>這些檔案存放 Undo Pages (16KB)，裡面承載了 rollback segment 與實際的 undo log。</li><li>從 MySQL 5.7 開始，undo tablespace 可以獨立於系統表空間 (ibdata1) 之外管理。</li></ul><h4># Rollback Segment 的角色</h4><p>為什麼 Undo Tablespace 不直接存放「Undo Slot」？ 因為 InnoDB 引入了 Rollback Segment 這一層來做中介，主要原因有：</p><ul><li>分組管理 Undo Slot：避免所有 slot 都集中在一個大結構，降低 contention。</li><li>更細粒度的鎖：分散鎖競爭，提升高併發能力。</li><li>Undo Page 回收：purge thread 能以 rollback segment 為單位清理舊 undo log。</li><li>相容於 Oracle 的設計：這是業界常見的資料庫架構概念。</li></ul><p>👉 可以想像：</p><ul><li>Tablespace = 一棟大樓</li><li>Rollback Segment = 樓層</li><li>Undo Slot = 房間</li><li>Transaction = 住戶</li></ul><h4># Undo Slot 與 Transaction</h4><ul><li>每個 Transaction 開始時，會從 rollback segment 取得一個 Undo Slot。</li><li>Transaction 在執行 INSERT / UPDATE / DELETE 時，產生的 undo log 會寫進這個 slot 對應的 undo page。</li><li>一個 rollback segment 可以同時管理多個 transaction（因為 slot 很多）。</li></ul><h4># 每個 Tablespace 有多少 Rollback Segment？</h4><ul><li>固定值：每個 undo tablespace = 128 個 rollback segments。</li><li>總 rollback segment 數量 = innodb_undo_tablespaces × 128。</li><li>MySQL 5.6 時，rollback segments 全部放在 ibdata1，數量上限就是 128。</li><li>從 MySQL 5.7 開始，可以分散到多個 undo tablespace。</li></ul><h4># 為什麼建議至少 2 個 Tablespace？</h4><ul><li>分散 I/O 壓力</li><li>即使一個 tablespace 有 128 rollback segments，所有 undo log 最終還是寫到同一個檔案。</li><li>多個 tablespace = 多個檔案 → I/O 分散，不再成為單一熱點。</li><li>線上清理 (Truncate) 機制</li><li>Undo tablespace 可以被清理釋放空間，但前提是要至少有 2 個，系統才能在「清理一個」的同時「繼續使用另一個」。</li><li>降低鎖爭用</li><li>多個檔案、多組 rollback segments，能讓 transaction 的 undo log 分配更均勻，減少 slot 爭用。</li></ul><h4># Undo Tablespace 與 Memory / Disk 的關係</h4><ul><li>Undo tablespace 是 磁碟檔案，但實際讀寫會經過 InnoDB Buffer Pool。</li><li>流程如下：</li><li>Transaction 產生 undo log → 先寫進 buffer pool 的 undo page。</li><li>Buffer pool 定期 flush → 把修改後的 undo page 寫回 tablespace 檔案。</li><li>所以：</li><li>大部分操作在記憶體完成（接近記憶體速度）。</li><li>只有 flush / purge / truncate 時會實際觸發磁碟 I/O。</li></ul><h4># Tablespace 數量 vs 高併發效能</h4><ul><li>更多 tablespace → 更多 rollback segments → 更多檔案 I/O 通道。</li><li>從 1 → 2，效益最大；從 2 → 4，仍然有幫助；再往上效益遞減。</li><li>官方建議：</li><li>MySQL 5.7：innodb_undo_tablespaces=2（預設值也是 2）。</li><li>MySQL 8.0.14+：系統自動管理，一般保留 2~4 個就足夠。</li></ul><h4># 示意圖</h4><h4># 整體結構</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*bsDgI8yMJM_Aj84J0DqfhQ.png" /></figure><h4># 單個/多個 Tablespace 比較</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*hKh8LRPcnlxCyAvvlWZbjQ.png" /></figure><h4># 總結</h4><ul><li>Undo Tablespace = 磁碟檔案，裡面有 128 rollback segments，每個 segment 管理多個 undo slot。</li><li>Transaction → Undo Slot → Rollback Segment → Tablespace → 檔案，這是完整的分層。</li><li>多個 tablespace 的好處：</li><li>分散 I/O 壓力</li><li>支援線上清理</li><li>減少鎖競爭</li><li>建議設定：2~4 個 tablespace，足以應付高併發，大於這個數通常沒有額外效益。</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=aef58b43925c" width="1" height="1" alt=""><hr><p><a href="https://medium.com/learn-or-die/mysql-tablespace-aef58b43925c">MySQL Tablespace</a> was originally published in <a href="https://medium.com/learn-or-die">Learn or Die</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Index — Basic Principles]]></title>
            <link>https://medium.com/learn-or-die/index-basic-principles-453f88bf9588?source=rss-b11a48c41717------2</link>
            <guid isPermaLink="false">https://medium.com/p/453f88bf9588</guid>
            <category><![CDATA[index]]></category>
            <category><![CDATA[mysql]]></category>
            <dc:creator><![CDATA[Ray Lee | 李宗叡]]></dc:creator>
            <pubDate>Mon, 08 Dec 2025 04:12:30 GMT</pubDate>
            <atom:updated>2025-12-08T04:12:30.473Z</atom:updated>
            <content:encoded><![CDATA[<h4># 前言</h4><p>index 筆記</p><h4># index 四大原則</h4><p>index 的運作有四大原則</p><ul><li>Fast Lookup</li><li>Scan in One Direction</li><li>From Left To Right</li><li>Scan on Range Conditions</li></ul><h4># Fast Lookup</h4><p>index 最基本的動作，就是可以快速的從 index 當中找到目標，假如條件為 <strong>release_year = 2019</strong>，如果 release_year 有 index，那就是使用二分法快速尋找，如下圖</p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/0*F6R97HLVN64U2m0T" /></figure><p>如果 index 運作正常，從百萬行甚至上億行中找資料，跟從幾千行中找資料，速度是不會差很多的</p><h4># Scan In One Direction</h4><p>當 index 中的目標起始點被找到時，會由那個點開始往一個方向掃，可以是 Asc or Desc，但不可兩者共存</p><p>比方說，條件為 <strong>WHERE age &gt;= 35 ORDER BY age ASC LIMIT 3</strong>，那執行流程如下：</p><ul><li>先找到 <strong>age &gt;= 35</strong> 的第一行</li><li>依照 ASC 的方向開始往下掃描</li><li>掃 3 行後停下</li></ul><p>如下圖</p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/0*Qjgl0TEeHaG2ENPD" /></figure><h4># From Left To Right</h4><p>single column index 很好理解，但通常使用 multi-column index 能更有機會達成 performance improvement</p><p>而 multi-column index 的規則就是 <strong>From Left To Right Without Skipping a Column</strong></p><p>假如 index 為 <strong>(firstname, lastname, country)</strong>，那他只適用於以下的 query</p><ul><li>SELECT * FROM contacts WHERE firstname = &#39;James&#39;</li><li>SELECT * FROM contacts WHERE firstname = &#39;James&#39; AND lastname = &#39;Walker&#39;</li><li>SELECT * FROM contacts WHERE firstname = &#39;James&#39; AND lastname = &#39;Walker&#39; AND country = &#39;US&#39;</li></ul><h4># 篩子模式</h4><p>整個查詢的步驟，就像個篩子一樣，將範圍逐步地縮小。步驟可拆分如下：</p><ul><li>首先，以 Fast Lookup 的方式找到最左 index <strong>firstname = ‘James’</strong></li><li>篩子範圍縮小，選擇中段的 <strong>lastname = ‘Walker’</strong> condition</li><li>最終再次將篩子範圍縮小 <strong>country = ‘US’</strong> 的範圍</li><li>具體的步驟，如下圖</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/0*s5fxv0v3akXEb6pQ" /></figure><p>儘管 index 的 column 組成變了，但這個步驟是不會變的，比如我們將 index 改成 <strong>(firstname, lastname, country)</strong>，雖然順序變了，但篩子還是篩子</p><figure><img alt="" src="https://cdn-images-1.medium.com/proxy/0*I2nIhdqvUkGDD7_S" /></figure><p>然而，<strong>如果 query 使用到 index 當中所有的 column 的話</strong>，那 multi-column index 的順序可能不重要，但大部分的情況，都是某個 query 使用到 multi-column index 當中的部分 column，這時順序就很重要</p><h4># 順序很重要</h4><p>假如 condition 是 WHERE country = &#39;US&#39;，那不管是 <strong>index (firstname, lastname, country)</strong>，或是 <strong>index (lastname, firstname, country)</strong> 都不適用，multi-column index 的原則是 <strong>From Left To Right</strong>，所以 country column 必須位於最左側</p><p>比方說，index (country, lastname, firstname) 就完美適用於以下的 query</p><ul><li>SELECT * FROM contacts WHERE country = ‘US’</li><li>SELECT * FROM contacts WHERE country = ‘US’ AND lastname = ‘Walker’</li><li>SELECT * FROM contacts WHERE country = ‘US’ AND lastname = ‘Walker’ AND firstname = ‘James’</li></ul><h4># 情境列舉</h4><p>當使用 multi-column index 時，大概歸納一下情境，會有以下幾種：</p><ul><li>query 皆為 equality check：<br>- query condition 使用到的 column 都有被包含在 index 當中，且 <strong>index columns</strong> 為 <strong>condition columns</strong> 的 superset，此時是 best match。比如 index 為 <strong>(country, firstname, lastname)<br>- </strong>query 為 <strong>country = ‘a’ AND firstname = ‘b’<br>- country = ‘a’<br>- country = ‘a’ AND firstname = ‘b’ AND lastname = ‘c’</strong>，</li><li>query condition 使用到的 column 都有被包含在 index 當中，但 <strong>index columns</strong> 不算 <strong>condition columns</strong> 的 perfect superset，則前面符合的部分會採用 fast lookup，即二分法 log(N)，而剩餘的部分會 scan every loaded index entries。此情境不算 best match，但速度也會比沒有使用 index 來得快，因為前頭有用到 fast lookup，以及後面的 scan 此需 scan loaded index entries，如果沒有 index，則是 scan full table rows。假設 index 為 <strong>(country, firstname, lastname, age)</strong>，則以下 query 符合此情境<br>- <strong>country = ‘a’ AND firstname = ‘b’ AND age = ‘c’<br>- country = ‘a’ AND lastname = ‘b’ AND age = ‘c’<br>- country = ‘a’ AND age = ‘c’</strong></li><li>query condition 使用到的 column 都有被包含在 index 當中，但 index 當中的第一個 column 不在 query condition columns 當中。index 是有順序性的，比如在 tree 中的結構像是 column1 -&gt; column2 -&gt; column3，所以如果知道 first column 要找的目標，那在 first column node 就可以使用二分法直接快速定位。而如果沒有使用到 first index column 的話，效能依然會比沒有使用 index 來得好，但比不上 best match。假設 index 為 **(firstname, lastname, country)**以下的 query 都可以使用到部分 index，但不是 best match<br>- <strong>lastname = ‘b’ AND country = ‘c’<br>- country = ‘c’ AND lastname = ‘b’<br>- </strong>query 含有 range condition，假設 index 為 <strong>(country, firstname, lastname, age)</strong></li><li>從 range condition 的那一個 index 開始往後的 index 都無法被使用，因為 range condition 無法預測數量，所以只能一行一行掃</li><li><strong>country = ‘a’ AND firstname = ‘b’ AND lastname &gt;= ‘b’ AND age = ‘c’</strong>，此 query 當中，index <strong>country</strong> &amp; <strong>firstname</strong> 可以被使用且快速定位縮小 <strong>lastname</strong> 須掃描的行數，而 <strong>age</strong> index 無法被使用</li></ul><h4># 參考出處</h4><ul><li>Index Beyond the Basics</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=453f88bf9588" width="1" height="1" alt=""><hr><p><a href="https://medium.com/learn-or-die/index-basic-principles-453f88bf9588">Index — Basic Principles</a> was originally published in <a href="https://medium.com/learn-or-die">Learn or Die</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Browser — Event Flow]]></title>
            <link>https://medium.com/learn-or-die/browser-event-flow-9862265b44f9?source=rss-b11a48c41717------2</link>
            <guid isPermaLink="false">https://medium.com/p/9862265b44f9</guid>
            <category><![CDATA[frontend]]></category>
            <category><![CDATA[browsers]]></category>
            <dc:creator><![CDATA[Ray Lee | 李宗叡]]></dc:creator>
            <pubDate>Fri, 31 Oct 2025 00:57:22 GMT</pubDate>
            <atom:updated>2025-10-31T00:57:22.613Z</atom:updated>
            <content:encoded><![CDATA[<h4># 瀏覽器事件流</h4><p>瀏覽器事件流完整解析：Capturing、Target、Bubbling</p><p>在 JavaScript 中，DOM 事件並不是只在目標元素觸發就結束，而是會經過一個完整的 事件流 (Event Flow)。理解事件流有助於我們正確地處理事件、避免衝突，甚至做出高效的事件委派。</p><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*4uyBctJbeSfClcONAU1B3Q.png" /></figure><h4># 事件流的三個階段</h4><ul><li>Capturing Phase（捕獲階段）<br>事件從 window 出發<br>沿著 DOM tree 從外層往內層傳遞<br>直到目標元素的父層<br>只有註冊了 { capture: true } 的 listener 會在這裡觸發</li><li>Target Phase（目標階段）<br>事件抵達目標元素<br>觸發該元素上所有監聽器</li><li>Bubbling Phase（冒泡階段）<br>事件從目標元素往外層冒泡<br>依序經過父層、祖先節點 → body → document → window<br>預設的 addEventListener（不加 capture）就是在這個階段觸發</li></ul><h4># 哪些事件不會冒泡？</h4><ul><li>不是所有事件都有 bubbling phase，例如：<br>❌ focus / blur → 不冒泡，但有 focusin / focusout 會冒泡<br>❌ mouseenter / mouseleave → 不冒泡，但對應的 mouseover / mouseout 會冒泡<br>❌ scroll、resize、load、error → 不冒泡</li><li>這些事件依然有捕獲和目標階段，只是冒泡結束在目標元素。</li></ul><h4># Input 與 Change 事件</h4><ul><li>input：輸入框內容一變就立刻觸發，並且會冒泡。</li><li>change：只有在元素失去焦點（blur）且值有變化時才觸發。</li><li>在子 input 打字時，父層 也能透過事件冒泡捕捉到 input 事件。</li></ul><h4># Void Elements 限制</h4><ul><li>某些元素屬於 void elements，不能包含子元素，因此不可能成為 parent。</li><li>常見的有：&lt;input&gt;、&lt;img&gt;、&lt;br&gt;、&lt;meta&gt;、&lt;link&gt;…</li><li>子 input 不會觸發父 input 的事件 listener，因為結構上就不可能出現「input 包 input」。</li><li>事件只會冒泡到合法的父容器（如 &lt;div&gt;、form、body）。</li></ul><h4># Modal 的冒泡</h4><ul><li>如果是一般容器元素（如 modal &lt;div&gt;），子 modal 的事件會冒泡到 parent modal。</li><li>如果希望父 modal 接手事件 → 直接監聽父元素。</li><li>如果不希望父 modal 介入 → 在子元素呼叫 event.stopPropagation()。</li></ul><h4># 兄弟元素不會互相觸發</h4><ul><li>事件冒泡方向是「子 → 父 → 祖先」，不會橫向傳遞到兄弟元素。</li><li>點擊一個 ，不會觸發另一個兄弟 的事件。</li></ul><h4># 為什麼要 stopPropagation？</h4><ul><li>有些情境下，我們需要確保事件只由自己處理，不讓其他 listener 攔走，例如：<br>- 全域 ESC handler<br>- 表單驗證失敗時阻止後續送出<br>- 避免不同 library 的衝突</li><li>常見的控制方法：<br>- preventDefault() → 阻止瀏覽器預設行為<br>- stopPropagation() → 阻止事件繼續冒泡<br>- stopImmediatePropagation() → 阻止冒泡，並且阻止同一元素的後續 listener</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/748/1*6KPpaMY1Hj17-yJOeXcmxw.png" /></figure><h4># stopImmediatePropagation 的特殊情境</h4><ul><li>當同一個元素有多個 listener 時：<br>- stopPropagation() 只會阻止冒泡，後續 listener 仍然執行。<br>- stopImmediatePropagation() 會立刻中斷，讓同一元素的後續 listener 不再執行。</li><li>範例</li></ul><pre>btn.addEventListener(&quot;click&quot;, () =&gt; console.log(&quot;A&quot;));<br>btn.addEventListener(&quot;click&quot;, (e) =&gt; {<br>  console.log(&quot;B stopImmediatePropagation&quot;);<br>  e.stopImmediatePropagation();<br>});<br>btn.addEventListener(&quot;click&quot;, () =&gt; console.log(&quot;C&quot;)); // ❌ 不會執行</pre><h4># Listener 的執行順序</h4><ol><li>先註冊 → 先執行<br>- 同一元素、同一事件類型、同一階段下，listener 按 addEventListener 的註冊順序執行。</li><li>捕獲 vs 冒泡<br>- 捕獲階段的 listener（capture: true）先於冒泡 listener。</li><li>中斷控制<br>- stopPropagation()：事件仍會執行同一元素的後續 listener，但不再冒泡。<br>- stopImmediatePropagation()：事件立即停止，不執行後續 listener。</li></ol><h4># 總結</h4><ul><li>事件流分為 捕獲 → 目標 → 冒泡 三個階段。</li><li>大多數事件會冒泡，但有些事件是例外（focus、mouseenter、scroll 等）。</li><li>input 與 change 的差異在觸發時機，且 input 會冒泡。</li><li>void elements 不能有子元素，因此不會成為 parent。</li><li>modal 等容器元素會接收子元素的冒泡事件。</li><li>sibling 元素之間不會互相觸發事件。</li><li>stopPropagation 與 stopImmediatePropagation 可用來控制事件流，避免衝突。</li><li>listener 執行順序取決於註冊順序與階段。</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=9862265b44f9" width="1" height="1" alt=""><hr><p><a href="https://medium.com/learn-or-die/browser-event-flow-9862265b44f9">Browser — Event Flow</a> was originally published in <a href="https://medium.com/learn-or-die">Learn or Die</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Browser — window]]></title>
            <link>https://medium.com/learn-or-die/browser-window-192180e37cfa?source=rss-b11a48c41717------2</link>
            <guid isPermaLink="false">https://medium.com/p/192180e37cfa</guid>
            <category><![CDATA[frontend]]></category>
            <category><![CDATA[browsers]]></category>
            <dc:creator><![CDATA[Ray Lee | 李宗叡]]></dc:creator>
            <pubDate>Mon, 27 Oct 2025 01:04:49 GMT</pubDate>
            <atom:updated>2025-10-27T01:04:49.397Z</atom:updated>
            <content:encoded><![CDATA[<h4># 前言</h4><p>寫前端時常常會調用到 <strong>window</strong>，所以這是什麼呢？</p><h4># 解釋</h4><p>在瀏覽器（Browser）的語境中，window 是一個全域物件（global object）。它代表瀏覽器中開啟的一個視窗或標籤頁。</p><p>簡單來說：</p><ul><li>在 JavaScript 裡，window 就是瀏覽器給你操作「當前視窗／頁面」的介面。</li><li>幾乎所有瀏覽器提供的 API（例如 alert()、setTimeout()、document 等）都是 window 物件的屬性。</li></ul><h4># window 的角色</h4><ul><li>全域命名空間<br>- 在瀏覽器裡宣告的全域變數或函式（var foo = 123;）其實會掛載到 window.foo 上。<br>- window.alert(“hi”) 與 alert(“hi”) 等效。</li><li>控制視窗<br>- 包含當前視窗的大小與位置：window.innerWidth, window.innerHeight。<br>- 可以開新視窗或操作現有的（受瀏覽器安全性限制）：window.open(…)。</li><li>事件與生命週期<br>- 載入事件：window.onload = function () {…}。<br>- 卷動事件：window.onscroll = …。</li><li>存取其他物件<br>- window.document：代表 DOM 文件（HTML）。<br>- window.localStorage, window.sessionStorage：儲存資料。<br>- window.location：存取網址資訊。</li></ul><h4># 架構圖</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*0DWyphPqYzQNNWwy2pXMng.png" /></figure><h4># 總結</h4><p>在瀏覽器中，window 就是 JavaScript 的全域物件，提供操作「瀏覽器視窗 / 標籤頁」以及「頁面 DOM」的入口。</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=192180e37cfa" width="1" height="1" alt=""><hr><p><a href="https://medium.com/learn-or-die/browser-window-192180e37cfa">Browser — window</a> was originally published in <a href="https://medium.com/learn-or-die">Learn or Die</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[瀏覽器中的 Document]]></title>
            <link>https://medium.com/learn-or-die/%E7%80%8F%E8%A6%BD%E5%99%A8%E4%B8%AD%E7%9A%84-document-2b60de938df8?source=rss-b11a48c41717------2</link>
            <guid isPermaLink="false">https://medium.com/p/2b60de938df8</guid>
            <category><![CDATA[react]]></category>
            <category><![CDATA[browsers]]></category>
            <category><![CDATA[frontend]]></category>
            <dc:creator><![CDATA[Ray Lee | 李宗叡]]></dc:creator>
            <pubDate>Sun, 26 Oct 2025 03:37:36 GMT</pubDate>
            <atom:updated>2025-10-26T03:37:36.246Z</atom:updated>
            <content:encoded><![CDATA[<h4># 前言</h4><p>在前端中常常會調用到 <strong>Document</strong>，所以它到底是什麼？</p><h4># document 的定位</h4><ul><li>來源：由瀏覽器在載入 HTML 時自動建立。</li><li>性質：一個 物件，是 瀏覽器提供給 JavaScript 的 API。</li><li>用途：代表目前載入的整份 HTML 文件（DOM 樹），讓 JS 可以讀寫它。</li></ul><h4># 為什麼需要 document</h4><ul><li>JavaScript 自己不認識 HTML，它只是一門程式語言。</li><li>瀏覽器透過 document 把 HTML DOM 揭露出來，讓 JS 能這樣操作：</li><li>找元素<br>const root = document.getElementById(&quot;root&quot;);</li><li>改變內容<br>root.textContent = &quot;Hello World&quot;;</li><li>新增節點</li></ul><pre>const btn = document.createElement(&quot;button&quot;);<br>btn.textContent = &quot;Click me&quot;;<br>root.appendChild(btn);</pre><h4># 和 DOM 的關係</h4><ul><li>DOM (Document Object Model) 是 HTML 文件的結構化表示（像樹狀結構）。</li><li>document 是這個 DOM 的入口點。</li><li>所以我們說：<br>DOM = 資料結構（整棵樹）。<br>document = 瀏覽器提供的 API，用來讀寫 DOM。</li></ul><h4># 示意圖</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/714/1*4gwSfBqFXqpr9B7b-jw-uw.png" /></figure><h4># 小結</h4><p>是的，document 就是瀏覽器提供的 API，讓 JS 可以操作 DOM（新增、刪除、修改、監聽事件）。</p><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=2b60de938df8" width="1" height="1" alt=""><hr><p><a href="https://medium.com/learn-or-die/%E7%80%8F%E8%A6%BD%E5%99%A8%E4%B8%AD%E7%9A%84-document-2b60de938df8">瀏覽器中的 Document</a> was originally published in <a href="https://medium.com/learn-or-die">Learn or Die</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[藉由實戰理解 Request Animation Frame]]></title>
            <link>https://medium.com/learn-or-die/%E8%97%89%E7%94%B1%E5%AF%A6%E6%88%B0%E7%90%86%E8%A7%A3-request-animation-frame-06429b3f12b4?source=rss-b11a48c41717------2</link>
            <guid isPermaLink="false">https://medium.com/p/06429b3f12b4</guid>
            <category><![CDATA[react]]></category>
            <category><![CDATA[browsers]]></category>
            <dc:creator><![CDATA[Ray Lee | 李宗叡]]></dc:creator>
            <pubDate>Fri, 24 Oct 2025 01:42:00 GMT</pubDate>
            <atom:updated>2025-10-24T01:42:00.301Z</atom:updated>
            <content:encoded><![CDATA[<h4># 前言</h4><ul><li>請 AI 幫我寫了一個 <strong>判斷是否為 mobile browser 的 hook</strong>，現在回頭理解這段程式碼</li><li>本篇的主題在於理解下面的 React 程式碼</li><li>學習重點為</li><li>RAF (Request Animation Frame)</li><li>CAF (Request Animation Frame)</li><li>MQL (Media Query List)</li></ul><h4># RAF</h4><ul><li><strong>RAF</strong> 方法通知瀏覽器我們想要產生動畫，並且要求瀏覽器在下次重繪畫面前呼叫特定函數更新動畫。這個方法接受一個引數作為下次重繪前調用的回呼函數，簡單理解就是，<strong>我告訴瀏覽器，下次重繪畫面前，執行我指定的 callback</strong></li><li><strong>何時是下次重繪前？</strong> 這取決於 <strong>FPS (Frame Per Second)</strong>，如果 <strong>FPS</strong> 為 60，那就是每秒執行 60 次，所以大概就是 <strong>1/60 秒約 160 ms</strong></li><li><strong>所以是每 160 ms 都會跑一次嗎？</strong> 不是的，每次呼叫 <strong>RAF</strong>，它只會執行一次，執行的時機點為 <strong>下次重繪前</strong></li><li>可以理解為有一個 <strong>RAF Queue</strong>，在 160 ms 內呼叫多次 <strong>RAF</strong> 時，就相當於往這個 queue 裡面塞了多個 callback，而 160 ms 一到，瀏覽器就會執行這個 queue 內的所有 callback，然後清除 queue</li><li>當執行 <strong>RAF</strong> 時，瀏覽器會往 <strong>RAF</strong> queue 當中塞進指定的 callback，然後給該 callback 一個 ID 並且回傳該 ID</li></ul><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*xt-B0cpynUQA5KU4w7pTHg.png" /></figure><h4># CAF</h4><ul><li><strong>CAF</strong> 其實就是從 <strong>RAF Queue</strong> 當中把指定的 callback 移除，所以要帶 ID 進去</li></ul><h4># 程式碼</h4><pre>export const useResponsive = () =&gt; {<br>    // detectIsMobile() 的文章位置，上面有提到<br>    const [isMobile, setIsMobile] = useState(() =&gt; detectIsMobile());<br>    const frame = useRef(0);<br><br>    useEffect(() =&gt; {<br>        // RAF &amp; CAF 的邏輯，上面有解釋。所以這段的邏輯，就是建立一個 callback，這個 callback 每次執行時，會先檢查<br>        // frame 是否已經被佔用了，這個 frame 會放 RAF 的回傳，也就是 RAF Queue 當中的 callback ID<br>        // 如果已經被佔用了，我們就使用 CAF，把它從 RAF Queue 當中移除，避免重繪前一次執行多個 callback<br>        // 所以可以將 frame 理解為一個 slot，它每次只允許一個 RAF Queue callback ID 存在，進而確保 <br>        // RAF Queue 在每一個 frame 的週期內，不會出現多個 callback<br>        const update = () =&gt; {<br>            // rAF coalescing to avoid update storms<br>            if (frame.current) cancelAnimationFrame(frame.current);<br>            frame.current = requestAnimationFrame(() =&gt; setIsMobile(detectIsMobile()));<br>        };<br><br>        // 這邊從 Window 當中取得這兩種 Media Query List 的物件<br>        // 如果瀏覽器不支援，那會有 error，就把值設為 null<br>        // filter 會濾掉 value 為 null 的物件<br>        const mqls = [<br>            &#39;(any-pointer: coarse)&#39;,<br>            &#39;(any-hover: none)&#39;,<br>        ].map(q =&gt; {<br>            try {<br>                return window.matchMedia(q);<br>            } catch {<br>                return null;<br>            }<br>        }).filter(Boolean);<br><br>        // 只要觸發 resize event，就執行 update callback，來重新偵測是否 mobile 裝置<br>        window.addEventListener(&#39;resize&#39;, update);<br>        // 只要這兩個 Media Query List 物件有變化，那也執行 update callback 來重新偵測是否 mobile 裝置<br>        mqls.forEach(mql =&gt; mql.addEventListener?.(&#39;change&#39;, update));<br><br>        // cleanup<br>        return () =&gt; {<br>            window.removeEventListener(&#39;resize&#39;, update);<br>            mqls.forEach(mql =&gt; mql.removeEventListener?.(&#39;change&#39;, update));<br>            if (frame.current) cancelAnimationFrame(frame.current);<br>        };<br>    }, []);<br><br>    return isMobile;<br>};</pre><h4># 總結</h4><p>上面的程式碼主要可以分成兩個部分的邏輯</p><ul><li><strong>update</strong> 邏輯主要在於優化，他的效果相似於 <strong>Debounce</strong> 概念，都是避免 <strong>當 event 大量觸發時</strong>，大量地觸發 <strong>detectIsMobile</strong> callback</li><li><strong>RAF</strong> &amp; <strong>CAF</strong> 的搭配使用，確保 <strong>detectIsMobile()</strong> 在每 160 ms 內只會被執行 1 次</li><li>也可以用 setTimeout 來實作，在極短時間內其實不會有區別，但 <strong>RAF</strong> &amp; <strong>CAF</strong> 是瀏覽器原生的功能，可以確保 <strong>在每一個 frame</strong> 都會被執行</li><li>另一個部分，就是將 <strong>update</strong> 作為事件觸發的 listener</li><li><strong>CAF</strong> 的使用至關重要，否則會造成每一個 frame 週期內的 <strong>RAF Queue</strong> 都塞入了大量的 callback</li><li>下面提供有無使用 <strong>CAF</strong> 的差異圖</li></ul><h4># 有 CAF 優化</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*ffRnDxCy5IwKfq6-bqcSAg.png" /></figure><h4># 無 CAF 優化</h4><figure><img alt="" src="https://cdn-images-1.medium.com/max/1024/1*rcLE5awbR6SVSf7ryK0tFg.png" /></figure><h4># Reference</h4><ul><li>AI</li><li><a href="https://developer.mozilla.org/zh-TW/docs/Web/API/Window/requestAnimationFrame">Documentation</a></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=06429b3f12b4" width="1" height="1" alt=""><hr><p><a href="https://medium.com/learn-or-die/%E8%97%89%E7%94%B1%E5%AF%A6%E6%88%B0%E7%90%86%E8%A7%A3-request-animation-frame-06429b3f12b4">藉由實戰理解 Request Animation Frame</a> was originally published in <a href="https://medium.com/learn-or-die">Learn or Die</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[理解 Prevent Default]]></title>
            <link>https://medium.com/learn-or-die/%E7%90%86%E8%A7%A3-prevent-default-d0d1df84d6e6?source=rss-b11a48c41717------2</link>
            <guid isPermaLink="false">https://medium.com/p/d0d1df84d6e6</guid>
            <category><![CDATA[javascript]]></category>
            <category><![CDATA[browsers]]></category>
            <category><![CDATA[frontend]]></category>
            <dc:creator><![CDATA[Ray Lee | 李宗叡]]></dc:creator>
            <pubDate>Wed, 22 Oct 2025 07:09:21 GMT</pubDate>
            <atom:updated>2025-10-22T07:09:21.313Z</atom:updated>
            <content:encoded><![CDATA[<h4># 前言</h4><p>身為一個後端工程師，如果把自己侷限於後端，那格局就小了</p><p>學習 <strong>Prevent Default</strong> 基礎知識</p><h4># 概念解析</h4><h4># 瀏覽器的預設行為</h4><ul><li>表單送出，點擊預設會送出表單</li></ul><pre>&lt;form&gt;<br>  &lt;button type=&quot;submit&quot;&gt;送出&lt;/button&gt;<br>&lt;/form&gt;</pre><ul><li>連結跳轉，點擊會預設跳轉</li></ul><pre>&lt;a href=&quot;https://example.com&quot;&gt;Example&lt;/a&gt;</pre><ul><li>按 ESC → 瀏覽器可能會退出全螢幕</li><li>按 Space → 預設會滾動頁面</li><li>按 Ctrl+S → 預設會觸發瀏覽器的「儲存網頁」</li></ul><h4># 阻擋瀏覽器行為</h4><p>event.preventDefault() 的用途，就是阻止瀏覽器對某些 HTML 元素或操作的「預設行為」。</p><ul><li>表單送出，點下按鈕時，瀏覽器會預設送出表單並重新載入頁面</li></ul><pre>&lt;form&gt;<br>  &lt;button type=&quot;submit&quot;&gt;送出&lt;/button&gt;<br>&lt;/form&gt;</pre><ul><li>用 <strong>e.preventDefault()</strong> 可以攔截掉，改成用 JavaScript 處理。</li></ul><pre>form.addEventListener(&#39;submit&#39;, (e) =&gt; {<br>  e.preventDefault();<br>  console.log(&#39;攔截送出，自己處理 AJAX&#39;);<br>});</pre><ul><li>連結跳轉，點擊會預設跳轉。</li></ul><pre>&lt;a href=&quot;https://example.com&quot;&gt;Example&lt;/a&gt;</pre><ul><li>用 <strong>preventDefault()</strong> 可以阻止跳轉：</li></ul><pre>link.addEventListener(&#39;click&#39;, (e) =&gt; {<br>  e.preventDefault();<br>  console.log(&#39;不跳轉，改成彈窗&#39;);<br>});</pre><ul><li>按 ESC → 瀏覽器可能會退出全螢幕</li><li>按 Space → 預設會滾動頁面</li><li>按 Ctrl+S → 預設會觸發瀏覽器的「儲存網頁」</li><li><strong>preventDefault()</strong> 就能攔截，交給你自己的程式處理。</li></ul><h4># 參考</h4><ul><li>文件</li><li>AI</li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=d0d1df84d6e6" width="1" height="1" alt=""><hr><p><a href="https://medium.com/learn-or-die/%E7%90%86%E8%A7%A3-prevent-default-d0d1df84d6e6">理解 Prevent Default</a> was originally published in <a href="https://medium.com/learn-or-die">Learn or Die</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
        <item>
            <title><![CDATA[Rate Limit 測試 with Vegeta]]></title>
            <link>https://medium.com/learn-or-die/rate-limit-%E6%B8%AC%E8%A9%A6-with-vegeta-c98ef978d1a9?source=rss-b11a48c41717------2</link>
            <guid isPermaLink="false">https://medium.com/p/c98ef978d1a9</guid>
            <category><![CDATA[devops]]></category>
            <category><![CDATA[vegeta]]></category>
            <dc:creator><![CDATA[Ray Lee | 李宗叡]]></dc:creator>
            <pubDate>Tue, 14 Oct 2025 00:33:17 GMT</pubDate>
            <atom:updated>2025-10-14T00:33:17.764Z</atom:updated>
            <content:encoded><![CDATA[<h4># 問題點</h4><p>產品使用了複數的第三方的 API key 以達到 API key rotation，目的在於提高 rate limit，事先跟第三方確認過這是合乎他們規範的，而且他們只認 key 且不會鎖 IP。 但從 log 上可以看到第三方的 API 偶有 error，想進一步判斷這個 error 是否是因為第三方的 rate limit 所造成</p><h4># 解決方式</h4><ul><li>使用 <strong>Vegeta</strong> 來併發 request，並根據結果來判斷</li><li>使用了 3 把 key，每把的 rate limit 是 30 / second</li><li>建立 3 個 txt file，分別為<br>key1.txt<br>key2.txt<br>key3.txt</li><li>內容都很單純，如下<br>GET <a href="https://apiurl/?apiKey=key1&amp;otherQuery=query">https://apiUrl?apiKey=key1&amp;otherQuery=query</a></li><li>開三個 terminal tabs，分別執行 3 個 <strong>Vegeta</strong> command with different txt file<br>vegeta attack -rate=29 -duration=120s -targets=key1.txt | vegeta report</li><li>然後就可以根據書出來判斷 <strong>有沒有被 429 or 其他 error</strong></li></ul><pre>Requests      [total, rate, throughput]         3480, 29.01, 28.60<br>Duration      [total, attack, wait]             2m2s, 2m0s, 1.697s<br>Latencies     [min, mean, 50, 90, 95, 99, max]  219.419ms, 1.031s, 356.828ms, 955.369ms, 2.013s, 20.493s, 28.371s<br>Bytes In      [total, mean]                     31010849, 8911.16<br>Bytes Out     [total, mean]                     0, 0.00<br>Success       [ratio]                           100.00%<br>Status Codes  [code:count]                      200:3480  <br>Error Set:</pre><ul><li>資料顯示，2 m 2s 發出 &amp; 收到 3480 個 request with 200 status，平均每秒 29 個 request。3 個 tabs 的結果都沒有 error，代表 <strong>起碼在 2 分鐘的時間間隔，第三方服務不會根據總 request 的數量來封鎖 IP</strong></li></ul><img src="https://medium.com/_/stat?event=post.clientViewed&referrerSource=full_rss&postId=c98ef978d1a9" width="1" height="1" alt=""><hr><p><a href="https://medium.com/learn-or-die/rate-limit-%E6%B8%AC%E8%A9%A6-with-vegeta-c98ef978d1a9">Rate Limit 測試 with Vegeta</a> was originally published in <a href="https://medium.com/learn-or-die">Learn or Die</a> on Medium, where people are continuing the conversation by highlighting and responding to this story.</p>]]></content:encoded>
        </item>
    </channel>
</rss>