<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:media="http://search.yahoo.com/mrss/">
  <channel>
    <title>Node.js｜林鼎淵 Dean Lin</title>
    <link>https://deanlin.net/tags/node.js/</link>
    <image><url>https://deanlin.net/images/author_hu_aa256fc934e9a932.jpg</url><title>Node.js｜林鼎淵 Dean Lin</title><link>https://deanlin.net/tags/node.js/</link><width>144</width><height>144</height></image>
    <description>林鼎淵（Dean Lin）的部落格：生成式 AI 企業內訓講師、外商資安公司軟體專家、7 本書作者。400 多篇文章用白話分享生成式 AI 實戰、Claude Code 與 Vibe Coding 教學、團隊合作與職場成長。</description>
    <generator>Hugo</generator>
    <language>zh-tw</language>
    <copyright>© 2020–2026 林鼎淵 Dean Lin</copyright>
    <lastBuildDate>Sat, 18 Oct 2025 15:47:32 +0800</lastBuildDate>
    <atom:link href="https://deanlin.net/tags/node.js/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Cursor 教學：Vibe Coding 實戰！從釐清需求到部署上線，一次搞懂完整開發流程</title>
      <link>https://deanlin.net/blog/202510/cursor-vibe-coding-end-to-end/</link>
      <pubDate>Sat, 18 Oct 2025 15:47:32 +0800</pubDate>
      <guid>https://deanlin.net/blog/202510/cursor-vibe-coding-end-to-end/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>AI 實戰</category>
      <description>如果你想嘗試 Vibe Coding，這是一隻讓「開始」變容易，「想法」有成果的影片。</description>
      <media:content url="https://deanlin.net/blog/202510/cursor-vibe-coding-end-to-end/images/cursor-vibe-coding-end-to-end-og_hu_3581f7e197b32ff0.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<p>如果你想嘗試 Vibe Coding，這是一隻讓「開始」變容易，「想法」有成果的影片。</p>
<p>一開始會分享與 <strong>AI 釐清需求，討論出產品規格的技巧；</strong>並說明請 AI 撰寫程式前，我會如何<strong>設計專案的規則</strong>，讓 AI Agent 撰寫程式時，有參考的依據。</p>
<p>在準備好專案的規格與規則後，我才會讓 <strong>AI Agent 開始動手寫程式、完成專案</strong>。</p>
<p>但程式完成，不代表結果符合預期；你可能會遇到<strong>錯誤訊息、外觀破版，或是想要新增更多功能。</strong>這邊也會跟大家示範，可以如何引導 AI，讓專案更加完整。</p>
<p>在最後會分析整個專案架構，帶大家了解 <strong>AI Agent 到底幫我們做了哪些事；</strong>並透過 <strong>GitHub Pages 發布專案</strong>，讓你完成的網頁可以直接分享給朋友。</p>
<blockquote>
<p>建議搭配影片觀看實際的操作步驟。</p>
</blockquote>
<div class="yt" data-yt="wOnjdSDt-8A" data-title="【Cursor 教學】Vibe Coding 實戰！從釐清需求到部署上線，一次搞懂完整開發流程 #spec #git #nodejs #deploy #ai #sdd">
  <a class="yt-link" href="https://www.youtube.com/watch?v=wOnjdSDt-8A" target="_blank" rel="noopener" aria-label="播放影片：【Cursor 教學】Vibe Coding 實戰！從釐清需求到部署上線，一次搞懂完整開發流程 #spec #git #nodejs #deploy #ai #sdd">
    <img src="https://i.ytimg.com/vi/wOnjdSDt-8A/hqdefault.jpg" alt="" width="480" height="360" loading="lazy" decoding="async">
    <span class="yt-play" aria-hidden="true"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false"><path d="M8 5.2v13.6L19 12z"/></svg></span>
    <span class="yt-title">【Cursor 教學】Vibe Coding 實戰！從釐清需求到部署上線，一次搞懂完整開發流程 #spec #git #nodejs #deploy #ai #sdd</span>
  </a>
</div>

<figure class="figure"><img src="https://deanlin.net/blog/202510/cursor-vibe-coding-end-to-end/images/cursor-vibe-coding-end-to-end-01.webp" width="1400" height="788" alt="Cursor 教學：Vibe Coding 實戰！從釐清需求到部署上線，一次搞懂完整開發流程（圖 2）" loading="lazy" decoding="async">
  <figcaption><a href="https://youtu.be/wOnjdSDt-8A">https://youtu.be/wOnjdSDt-8A</a></figcaption>
</figure>

<blockquote>
<p>第一次接觸 Cursor 與 Vibe Coding 的朋友，可以先參考下面的文章：</p>
<p>➤ <a href="https://deanlin.net/blog/202509/cursor-getting-started/">Cursor 教學：入門到實戰，用 AI Agent 自動化你的工作流！</a></p>
<p>➤ <a href="https://deanlin.net/blog/202509/cursor-productivity-extensions/">Cursor 教學：安裝 3 款讓 Vibe Coding 效率翻倍的工具！用「個人形象網站」實戰演練</a></p>
</blockquote>
<hr>
<h2 class="h-nomark" id="-釐清需求生成專案規格">① 釐清需求，生成專案規格<a class="h-anchor" href="#-%e9%87%90%e6%b8%85%e9%9c%80%e6%b1%82%e7%94%9f%e6%88%90%e5%b0%88%e6%a1%88%e8%a6%8f%e6%a0%bc" aria-label="這個段落的連結"></a></h2>
<p>我覺得 Vibe Coding 的目的，是要在短時間實踐你腦中的靈感，或解決現實生活遇到的痛點。</p>
<h3 id="-最小可行性方案">最小可行性方案<a class="h-anchor" href="#-%e6%9c%80%e5%b0%8f%e5%8f%af%e8%a1%8c%e6%80%a7%e6%96%b9%e6%a1%88" aria-label="這個段落的連結"></a></h3>
<p>儘管每個人想完成的專案大小不同，但建議大家先釐清目標的「最小可行性方案」</p>
<p>把想要一口氣搞定大型專案的想法，從腦中拿掉；否則最終產生的結果，要馬不會動，要馬問題一堆。</p>
<p>後續反而要花更多的時間，一一排除這些問題。</p>
<h3 id="-製作純前端網頁">製作純前端網頁<a class="h-anchor" href="#-%e8%a3%bd%e4%bd%9c%e7%b4%94%e5%89%8d%e7%ab%af%e7%b6%b2%e9%a0%81" aria-label="這個段落的連結"></a></h3>
<p>同時建議沒有程式背景的朋友，可以把「純前端網頁」，也就是這篇文章的範例，當成第一個 Vibe Coding 專案。</p>
<p>因為不涉及後端、資料庫，所以比較沒有資訊安全、隱私外洩的問題。</p>
<p>而且完成後可以直接把網頁分享給朋友，非常有成就感。</p>
<h3 id="-真的會用到的工具">真的會用到的工具<a class="h-anchor" href="#-%e7%9c%9f%e7%9a%84%e6%9c%83%e7%94%a8%e5%88%b0%e7%9a%84%e5%b7%a5%e5%85%b7" aria-label="這個段落的連結"></a></h3>
<p>另外專案的目標，最好設定為自己「真的會用到」的工具。</p>
<p>像大家現在看我上面的 YT 影片，可能覺得我侃侃而談，講得流暢不卡詞。</p>
<p>這不是因為我有多厲害，而是因為面前有一個提詞器；準備好逐字稿後，我只要錄影時照著念就好了。</p>
<p>一開始我是使用網路上，其他人分享的線上提詞器；但免費的廣告一堆，付費的又塞了一堆用不到的功能。</p>
<p>既然如此，我乾脆直接用 Vibe Coding 寫一款完全符合自己使用需求的提詞器，同時也讓大家免費使用。</p>
<h3 id="-新增空資料夾來開始專案">新增空資料夾，來開始專案<a class="h-anchor" href="#-%e6%96%b0%e5%a2%9e%e7%a9%ba%e8%b3%87%e6%96%99%e5%a4%be%e4%be%86%e9%96%8b%e5%a7%8b%e5%b0%88%e6%a1%88" aria-label="這個段落的連結"></a></h3>
<p>我們要先透過討論釐清這個網站需要有的功能；因此這邊不使用 AI Agent，而是先切換到 Ask 的模式來討論。</p>
<p><img src="https://deanlin.net/blog/202510/cursor-vibe-coding-end-to-end/images/cursor-vibe-coding-end-to-end-02.webp" width="947" height="548" alt="新增空資料夾，來開始專案（圖 2）" loading="lazy" decoding="async"></p>
<pre tabindex="0"><code>我想要設計一個「純前端」的「提詞器」網頁
想要以「最小可行性方案 MVP」來進行設計
請從實際使用者的角度，將功能依據「必要、次要」的方式來分類
並告訴我使用者體驗最佳的操作流程
</code></pre><p>此時你會得到 AI 初步的規劃建議，如果你對結果不滿意，可以繼續討論。</p>
<p>假使覺得這個規劃可以接受，就可以把它轉換成方便網頁工程師理解的需求規格。</p>
<pre tabindex="0"><code>請扮演專業的 UI/UX 設計師，根據上面的討論
用下面的格式，回傳「具體、可執行」的網頁設計規格。
目標是幫助前端工程師能依據描述直接開始開發，內容需具備視覺與互動細節。

輸出格式：
# [頁面名稱]
- 功能簡述：（一句話概述頁面目的）
- 色系搭配：（主色、輔色、背景色、按鈕色）

## [由上而下列出使用元素]
請依畫面垂直順序列出元件，格式如下：
- 元件名稱（如：頂部導覽列 / 按鈕 / Banner 區塊）
  - 顏色：
  - 寬度：
  - 高度：
  - 字型大小與樣式（如有文字）：
  - 功能說明（該元件的目的）：
  - 若為[按鈕] → 說明觸發任務（例：「送出表單」「切換主題」）
  - 若為[功能按鍵] → 說明使用方式（例：「滑過顯示選單」「長按複製」）

⚠️ 請只輸出上述格式內容，不要附加分析、理由或額外說明。
</code></pre><p>透過這個提示詞，你就會得到一份非常詳盡的規格：</p>
<p><img src="https://deanlin.net/blog/202510/cursor-vibe-coding-end-to-end/images/cursor-vibe-coding-end-to-end-03.webp" width="947" height="548" alt="新增空資料夾，來開始專案（圖 3）" loading="lazy" decoding="async"></p>
<p>接下來切換成 Agent 模式，請他將前面對話的內容彙整成一個 Markdown 格式的 Prompt，儲存到專案「idea」的資料夾中。</p>
<pre tabindex="0"><code>將上面對話的結論彙整成 Markdown 格式的 Spec
請儲存到專案根目錄的「idea」資料夾中（若無自動建立），以好辨識的英文命名檔案
還不需要實作
</code></pre><p><img src="https://deanlin.net/blog/202510/cursor-vibe-coding-end-to-end/images/cursor-vibe-coding-end-to-end-04.webp" width="947" height="548" alt="新增空資料夾，來開始專案（圖 4）" loading="lazy" decoding="async"></p>
<p>之所以這麼做，就是產生一個<strong>可以讓我們手動編輯細節的規格文件</strong>，而且後續開發時可以回顧專案的設計。</p>
<p>儘管相關訊息可以從過去的對話找到，但相信我，你幾乎不會翻過去的對話內容。</p>
<blockquote class="pullquote"><p>儘管我們可以使用 AI Agent 模式，讓 AI 直接寫程式；但我更建議大家先討論出具體、明確的專案框架後，再請 AI 做事。</p>
</blockquote>
<p>近幾年流行一個名詞，叫做 SDD（Specification-Driven Development），也就是規格驅動開發</p>
<p>比如今天的範例，就是用規格驅動 AI 開發，當目標足夠明確、解決的問題足夠具體時，你對生成的結果才有較高的掌握度，Vibe Coding 的成功率才會上升。</p>
<p>如果你在想這些步驟會不會花太多時間。</p>
<blockquote class="pullquote"><p>我可以用十多年的開發經驗跟你保證，如果初期沒寫好文件，日後要花好幾倍的時間去反覆修改。</p>
</blockquote>
<hr>
<h2 class="h-nomark" id="-設計規則讓-ai-知道做事邏輯">② 設計規則，讓 AI 知道做事邏輯<a class="h-anchor" href="#-%e8%a8%ad%e8%a8%88%e8%a6%8f%e5%89%87%e8%ae%93-ai-%e7%9f%a5%e9%81%93%e5%81%9a%e4%ba%8b%e9%82%8f%e8%bc%af" aria-label="這個段落的連結"></a></h2>
<p>今天的範例為「前端網頁」，可以只用簡單的 HTML、CSS、JavaScript 來完成。</p>
<p>但為了讓大家對「專案」有更具體的概念，所以我以三大前端框架 Vue、React、Angular 做示範。</p>
<p>在每個框架下，其實都還有各自的元件庫可以使用</p>
<ul>
<li><strong>Vue</strong>：Element Plus、Vuetify、Naive UI</li>
<li><strong>React</strong>：Ant Design、Material UI、Chakra UI</li>
<li><strong>Angular</strong>：Angular Material、NG-ZORRO、PrimeNG</li>
</ul>
<p>如果你對這些框架不熟悉，建議你可以先挑一個（ex: <strong>React + Material UI</strong>），未來 Vibe Coding 就直接指定他。</p>
<p>之所以這麼做，是因為<strong>每個框架與元件庫會遇到的坑都不一樣</strong>，假使每次都用不同的框架來製作專案。</p>
<p><strong>那可能每個坑都得走一遍，這是一件非常沒有效率的事情。</strong></p>
<h3 id="-建立專案的-rules">建立專案的 Rules<a class="h-anchor" href="#-%e5%bb%ba%e7%ab%8b%e5%b0%88%e6%a1%88%e7%9a%84-rules" aria-label="這個段落的連結"></a></h3>
<p>接下我們要去建立專案的 Rules，讓他製作網站時知道要依循什麼規則。</p>
<p>點擊右上角的設定，選擇「Rules &amp; Memories」，在 Project Rules 點擊新增。</p>
<p><img src="https://deanlin.net/blog/202510/cursor-vibe-coding-end-to-end/images/cursor-vibe-coding-end-to-end-05.webp" width="947" height="548" alt="建立專案的 Rules（圖 5）" loading="lazy" decoding="async"></p>
<p>檔名我就直接打「spec」，然後選擇 Always Apply 讓他每次對話都會觸發。</p>
<p><img src="https://deanlin.net/blog/202510/cursor-vibe-coding-end-to-end/images/cursor-vibe-coding-end-to-end-06.webp" width="947" height="548" alt="建立專案的 Rules（圖 6）" loading="lazy" decoding="async"></p>
<p>然後貼上設計的規則，在規則的部分，使用 Markdown 的語法撰寫就好，大家有興趣可以自行客製化，我只是提供一個基礎的範本。</p>
<pre tabindex="0"><code>## 使用的框架與元件庫
- 框架: React + Vite
- Node 版本: 加 .nvmrc
- 元件庫: Material UI

## 注意事項
- 請先搜尋網路上類似工具使用的套件
- 需考量 RWD 響應頁面
- 建立 .gitignore 來排除指定檔案上傳（ex: node_modules）

## README
- 專案介紹、功能列表、需求（Node 版本）、安裝與啟動、部署步驟、授權

## GitHub Pages 部署
- 使用 GitHub Actions
- 僅 master branch 觸發
</code></pre><hr>
<h2 class="h-nomark" id="-讓-ai-agent-參考規格規則開發專案">③ 讓 AI Agent 參考規格、規則開發專案<a class="h-anchor" href="#-%e8%ae%93-ai-agent-%e5%8f%83%e8%80%83%e8%a6%8f%e6%a0%bc%e8%a6%8f%e5%89%87%e9%96%8b%e7%99%bc%e5%b0%88%e6%a1%88" aria-label="這個段落的連結"></a></h2>
<p>在專案的需求規格以及規則都設計好後，接下來就可以用 AI Agent 設計初版網頁了。</p>
<p>建議開一個新對話，選擇 Agent 模式，你直接跟他說：</p>
<pre tabindex="0"><code>請參考「idea」的 SPEC，在專案根目錄新增「teleprompter-app」來實作專案
</code></pre><p>稍等一段時間，AI Agent 就會根據 SPEC 把這個網站製作出來。</p>
<p><img src="https://deanlin.net/blog/202510/cursor-vibe-coding-end-to-end/images/cursor-vibe-coding-end-to-end-07.webp" width="947" height="548" alt="③ 讓 AI Agent 參考規格、規則開發專案（圖 7）" loading="lazy" decoding="async"></p>
<p>如果他有自動幫你運行專案，你可以先請他關閉：</p>
<pre tabindex="0"><code>幫我關閉專案
</code></pre><p><img src="https://deanlin.net/blog/202510/cursor-vibe-coding-end-to-end/images/cursor-vibe-coding-end-to-end-08.webp" width="947" height="548" alt="③ 讓 AI Agent 參考規格、規則開發專案（圖 8）" loading="lazy" decoding="async"></p>
<p>這是因為在專案完成後，建議大家用新的 Cursor 視窗來來開啟這個專案，因為前面專案的規格與規則是為了產生這個專案而寫的，專案產生完畢後，繼續使用原有的規格、規則，可能會導致 AI 混亂。</p>
<p>另外在安裝專案套件與啟動專案這塊，你可以直接跟 AI 說：</p>
<pre tabindex="0"><code>幫我安裝套件並啟動專案
</code></pre><p>不過我是更建議大家打開 package.json 這個檔案，裡面有專案的各種資訊，像 scripts 這塊就是你可以透過 npm 執行的各種指令。</p>
<p><img src="https://deanlin.net/blog/202510/cursor-vibe-coding-end-to-end/images/cursor-vibe-coding-end-to-end-09.webp" width="947" height="548" alt="③ 讓 AI Agent 參考規格、規則開發專案（圖 9）" loading="lazy" decoding="async"></p>
<p>像是基礎的開發啟動是 <code>npm run dev</code>、想要建立靜態檔用 <code>npm run build</code> 。</p>
<p>如果想安裝套件，就用 <code>npm install</code>，他會安裝 <code>dependencies</code> 與 <code>devDependencies</code> 所列出的套件。</p>
<p>這邊我們打開終端機，在下面貼上指令「npm run dev」來啟動專案（具體指令請參考 scripts）</p>
<p><img src="https://deanlin.net/blog/202510/cursor-vibe-coding-end-to-end/images/cursor-vibe-coding-end-to-end-10.webp" width="947" height="548" alt="③ 讓 AI Agent 參考規格、規則開發專案（圖 10）" loading="lazy" decoding="async"></p>
<p>但提醒大家，不管我們前期的規格對功能、外型描述得多仔細；他完成的結果還是有可能會不如預期（比如影片範例就是一片空白）。</p>
<figure class="figure"><img src="https://deanlin.net/blog/202510/cursor-vibe-coding-end-to-end/images/cursor-vibe-coding-end-to-end-11.webp" width="947" height="548" alt="可以按下 F12 查看主控台是否有錯誤訊息" loading="lazy" decoding="async">
  <figcaption>可以按下 F12 查看主控台是否有錯誤訊息</figcaption>
</figure>

<blockquote class="pullquote"><p><strong>別忘記，生成式 AI 的結果本來就是充滿隨機性的。</strong></p>
</blockquote>
<p>這些是我為何建議先完成最小可行性產品，因為範圍小，所以釐清錯誤、調整內容相對方便。</p>
<blockquote class="pullquote"><p><strong>一但範圍大了，你連怎麼調整都會沒有頭緒。</strong></p>
</blockquote>
<p>所以千萬別想著靠一個指令就生成一套系統，因為光生成一個簡單的網頁都會遇到很多問題了。</p>
<hr>
<h2 class="h-nomark" id="-解決問題優化專案新增功能">④ 解決問題、優化專案、新增功能<a class="h-anchor" href="#-%e8%a7%a3%e6%b1%ba%e5%95%8f%e9%a1%8c%e5%84%aa%e5%8c%96%e5%b0%88%e6%a1%88%e6%96%b0%e5%a2%9e%e5%8a%9f%e8%83%bd" aria-label="這個段落的連結"></a></h2>
<p>以前端網頁來說，問題通常分成兩大塊：</p>
<ul>
<li><strong>外型</strong>：你可能覺得頁面的版型、顏色、按鈕大小不符合你的預期</li>
<li><strong>功能</strong>：有些操作起來不符合你的預期</li>
</ul>
<h3 id="-解決問題">解決問題<a class="h-anchor" href="#-%e8%a7%a3%e6%b1%ba%e5%95%8f%e9%a1%8c" aria-label="這個段落的連結"></a></h3>
<p>比如我想要讓提詞器播放順利，可以這麼說：</p>
<pre tabindex="0"><code>提詞器進入全螢幕後遇到以下問題：
1. 第二次點擊播放時，進度條會直接到底
2. 觸控板滑動時，頁面滑動與滑鼠感受不同
3. 滑到中間位置時按播放，感覺是更從前面的位置開始
</code></pre><h3 id="-優化專案">優化專案<a class="h-anchor" href="#-%e5%84%aa%e5%8c%96%e5%b0%88%e6%a1%88" aria-label="這個段落的連結"></a></h3>
<p>另外這個網頁有一些快捷鍵，我希望具體的使用方式可以放在首頁輸入框下方，這邊我就貼上一個比較模糊的指令，跟他說：</p>
<pre tabindex="0"><code>在首頁「輸入框」下方，新增「快捷鍵」提示的區塊，說明快捷鍵功能，請使用明顯的色系搭配
</code></pre><p>這個 Prompt 我就沒有指定具體色系、寬度、高度，由 AI 自動判斷。</p>
<p>等 AI 完成實作後，如果你不滿意，可以透過後續引導來微調細節。</p>
<p><img src="https://deanlin.net/blog/202510/cursor-vibe-coding-end-to-end/images/cursor-vibe-coding-end-to-end-12.webp" width="947" height="548" alt="優化專案（圖 12）" loading="lazy" decoding="async"></p>
<h3 id="-新增功能">新增功能<a class="h-anchor" href="#-%e6%96%b0%e5%a2%9e%e5%8a%9f%e8%83%bd" aria-label="這個段落的連結"></a></h3>
<p>這邊我再新增一個功能來做示範，比如我希望貼上文字後，能有中文、英文、符號的字數統計，以及總共行數：</p>
<pre tabindex="0"><code>在首頁「輸入框」下方，增加「即時字數統計」的功能，在 1 列分 3 塊顯示
- 中文字數、中文標點符號數
- 英文單詞數、英文標點符號數
- 總共行數
希望文字有對應顏色搭配方便辨識
</code></pre><p>等 AI 搞定後，我們回到網頁，然後貼上文字，初步確認計算是否符合需求。</p>
<p><img src="https://deanlin.net/blog/202510/cursor-vibe-coding-end-to-end/images/cursor-vibe-coding-end-to-end-13.webp" width="947" height="548" alt="新增功能（圖 13）" loading="lazy" decoding="async"></p>
<p>其實這個提詞器還有很多細節可以優化，這邊給大家看我最後調整了哪些細節：</p>
<ul>
<li>在首頁的地方，我增加了自己社群的資訊</li>
<li>把「字數統計、快捷鍵說明」這兩個區塊都改成一行顯示，減少空間上的浪費</li>
<li>進入全螢幕畫面後，我把原本置中的內容改成靠左以符合我的習慣</li>
<li>因為錄製影片時前面會有腳架，所以我設計了間隔，讓文字不要被腳架擋住</li>
<li>設計了分段的功能，讓我可以調整講話節奏</li>
<li>右上角多了一個「倒數計時器」，讓我知道現在的稿子會講多久</li>
</ul>
<div class="gallery-row cols-2">

<figure class="figure"><img src="https://deanlin.net/blog/202510/cursor-vibe-coding-end-to-end/images/cursor-vibe-coding-end-to-end-14.webp" width="947" height="548" alt="新增功能（圖 14）" fetchpriority="high" decoding="async">
</figure>

<figure class="figure"><img src="https://deanlin.net/blog/202510/cursor-vibe-coding-end-to-end/images/cursor-vibe-coding-end-to-end-15.webp" width="947" height="548" alt="Cursor 教學：Vibe Coding 實戰！從釐清需求到部署上線，一次搞懂完整開發流程（圖 2）" loading="lazy" decoding="async">
  <figcaption><a href="https://deanlin.net/teleprompter-app">https://deanlin.net/teleprompter-app</a></figcaption>
</figure>


</div>

<blockquote class="pullquote"><p>我覺得 Vibe Coding 的魅力不是一鍵搞定，而是你可以根據實際需求快速迭代產品，但前提是你知道自己要的是什麼。</p>
</blockquote>
<hr>
<h2 class="h-nomark" id="-了解-ai-生成的專案架構">⑤ 了解 AI 生成的專案架構<a class="h-anchor" href="#-%e4%ba%86%e8%a7%a3-ai-%e7%94%9f%e6%88%90%e7%9a%84%e5%b0%88%e6%a1%88%e6%9e%b6%e6%a7%8b" aria-label="這個段落的連結"></a></h2>
<p>接下來讓我們來瀏覽一下專案資料夾，了解 AI 幫我們做了哪些事情：</p>
<h3 id="-packagejson">package.json<a class="h-anchor" href="#-packagejson" aria-label="這個段落的連結"></a></h3>
<ul>
<li>scripts 這裡，有專案啟動、建立靜態資源的指令</li>
<li>dependencies、devDependencies 則是專案使用套件的版本資訊</li>
</ul>
<p>未來隨著專案規模擴大，你可能會遇到框架與套件、套件與套件之間相容性的問題，到時可能就會有鎖定版本、升降版本的需求，原則上就是在這邊調整的。</p>
<h3 id="-node_modules">node_modules<a class="h-anchor" href="#-node_modules" aria-label="這個段落的連結"></a></h3>
<p>「dependencies、devDependencies」列出的套件，都會被安裝到 node_moodules 的資料夾當中。</p>
<p>以 Node.js 的專案來說，通常體積最大的就是 node_moodules 這個資料夾。</p>
<p>因為套件版本已經有 package.json 控管，所以我們並不會把 node_moodules 上傳到 Git。</p>
<p>至於如何把他從 Git 版控中移除，就要談到 .gitignore 這個檔案了</p>
<h3 id="-gitignore">.gitignore<a class="h-anchor" href="#-gitignore" aria-label="這個段落的連結"></a></h3>
<p>這個檔案是用來告訴 Git 哪些檔案或資料夾不該被版本控制追蹤：</p>
<ul>
<li>MacOS 與 Windows 的系統暫存檔（.DS_Store/Thumbs.db）</li>
<li>儲存專案套件的 node_modules 資料夾</li>
<li>自動生成的結果，如 build 這類資料夾</li>
<li>像 .env 這種會儲存個人設定與密鑰的設定檔</li>
</ul>
<h3 id="-readme">README<a class="h-anchor" href="#-readme" aria-label="這個段落的連結"></a></h3>
<p>這份文件會包含專案簡介、功能列表、安裝環境需求等資訊，可以幫助你快速掌握這個專案的重點資訊。</p>
<hr>
<h2 class="h-nomark" id="-用-github-pages-免費發布網頁">⑥ 用 GitHub Pages 免費發布網頁<a class="h-anchor" href="#-%e7%94%a8-github-pages-%e5%85%8d%e8%b2%bb%e7%99%bc%e5%b8%83%e7%b6%b2%e9%a0%81" aria-label="這個段落的連結"></a></h2>
<p>確認專案都符合需求後，我們可以透過 GitHub Pages 發布專案，具體的執行流程可以參考專案中 「.github/workflow」 底下的 「deploy.yml」 設定檔。</p>
<p><img src="https://deanlin.net/blog/202510/cursor-vibe-coding-end-to-end/images/cursor-vibe-coding-end-to-end-16.webp" width="947" height="548" alt="⑥ 用 GitHub Pages 免費發布網頁（圖 16）" loading="lazy" decoding="async"></p>
<p>在 GitHub 建立 Repository 後，會根據這邊設計的流程來發布專案。</p>
<h3 id="-建立-repository">建立 Repository<a class="h-anchor" href="#-%e5%bb%ba%e7%ab%8b-repository" aria-label="這個段落的連結"></a></h3>
<p>Repository 名稱輸入「teleprompter-app」，因為我們這個專案預計要發佈到 GitHub Pages，所以選擇 Public。</p>
<p><img src="https://deanlin.net/blog/202510/cursor-vibe-coding-end-to-end/images/cursor-vibe-coding-end-to-end-17.webp" width="947" height="548" alt="建立 Repository（圖 17）" loading="lazy" decoding="async"></p>
<h3 id="-把電腦專案與-github-repository-連接">把電腦專案與 GitHub Repository 連接<a class="h-anchor" href="#-%e6%8a%8a%e9%9b%bb%e8%85%a6%e5%b0%88%e6%a1%88%e8%88%87-github-repository-%e9%80%a3%e6%8e%a5" aria-label="這個段落的連結"></a></h3>
<p>在終端機輸入 <code>git init</code> 來初始化專案 git。</p>
<p><img src="https://deanlin.net/blog/202510/cursor-vibe-coding-end-to-end/images/cursor-vibe-coding-end-to-end-18.webp" width="947" height="548" alt="把電腦專案與 GitHub Repository 連接（圖 18）" loading="lazy" decoding="async"></p>
<p>然後回到網頁，複製第一行 <code>git add…</code> 貼到專案的終端機，這樣我們就可以把電腦的專案與 GitHub 上面的專案連結。</p>
<p><img src="https://deanlin.net/blog/202510/cursor-vibe-coding-end-to-end/images/cursor-vibe-coding-end-to-end-19.webp" width="947" height="548" alt="把電腦專案與 GitHub Repository 連接（圖 19）" loading="lazy" decoding="async"></p>
<h3 id="-確認有更新到-github-repository">確認有更新到 GitHub Repository<a class="h-anchor" href="#-%e7%a2%ba%e8%aa%8d%e6%9c%89%e6%9b%b4%e6%96%b0%e5%88%b0-github-repository" aria-label="這個段落的連結"></a></h3>
<p>點擊提交，就會在自己的電腦記錄這個變更，但如果想更新到雲端，你需要點擊 Push 才會成功喔！</p>
<p><img src="https://deanlin.net/blog/202510/cursor-vibe-coding-end-to-end/images/cursor-vibe-coding-end-to-end-20.webp" width="947" height="548" alt="確認有更新到 GitHub Repository（圖 20）" loading="lazy" decoding="async"></p>
<p>Push 後我們回到 GitHub 網頁，刷新一下頁面，如果能看到專案的內容，就代表你成功上傳專案了！</p>
<p><img src="https://deanlin.net/blog/202510/cursor-vibe-coding-end-to-end/images/cursor-vibe-coding-end-to-end-21.webp" width="947" height="548" alt="確認有更新到 GitHub Repository（圖 21）" loading="lazy" decoding="async"></p>
<h3 id="-到-actions-分頁查看部署的狀況">到 Actions 分頁查看部署的狀況<a class="h-anchor" href="#-%e5%88%b0-actions-%e5%88%86%e9%a0%81%e6%9f%a5%e7%9c%8b%e9%83%a8%e7%bd%b2%e7%9a%84%e7%8b%80%e6%b3%81" aria-label="這個段落的連結"></a></h3>
<p>成功的話會有綠色勾勾，失敗則是紅色叉叉，會需要點進去釐清具體的錯誤原因。</p>
<p><img src="https://deanlin.net/blog/202510/cursor-vibe-coding-end-to-end/images/cursor-vibe-coding-end-to-end-22.webp" width="947" height="548" alt="到 Actions 分頁查看部署的狀況（圖 22）" loading="lazy" decoding="async"></p>
<p>錯誤通常都是因為：</p>
<ul>
<li>靜態資源沒找到（static resource）</li>
<li>語法格式錯誤（line error）</li>
<li>套件相依問題（npm dependency）</li>
</ul>
<p><img src="https://deanlin.net/blog/202510/cursor-vibe-coding-end-to-end/images/cursor-vibe-coding-end-to-end-23.webp" width="947" height="548" alt="到 Actions 分頁查看部署的狀況（圖 23）" loading="lazy" decoding="async"></p>
<p>把它複製起來貼到 AI 對話框，通常都能快速釐清問題並解決。</p>
<p>如果一切順利，點擊進去就會看到這個專案的網址嚕：</p>
<p><img src="https://deanlin.net/blog/202510/cursor-vibe-coding-end-to-end/images/cursor-vibe-coding-end-to-end-24.webp" width="947" height="548" alt="到 Actions 分頁查看部署的狀況（圖 24）" loading="lazy" decoding="async"></p>
<p>之後這個專案都是使用這個網址，把它點開來，順利的話，就可以看到成品嚕～</p>
<figure class="figure"><img src="https://deanlin.net/blog/202510/cursor-vibe-coding-end-to-end/images/cursor-vibe-coding-end-to-end-25.webp" width="1400" height="1125" alt="Cursor 教學：Vibe Coding 實戰！從釐清需求到部署上線，一次搞懂完整開發流程（圖 5）" loading="lazy" decoding="async">
  <figcaption><a href="https://deanlin.net/teleprompter-app/">https://deanlin.net/teleprompter-app/</a></figcaption>
</figure>

<p>如果影片看到了這裡，可以幫我點個讚鼓勵一下，我會持續分享更多實用教學。</p>
<hr>
<h2 id="結語vibe-coding-的底層邏輯">結語：Vibe Coding 的底層邏輯<a class="h-anchor" href="#%e7%b5%90%e8%aa%9evibe-coding-%e7%9a%84%e5%ba%95%e5%b1%a4%e9%82%8f%e8%bc%af" aria-label="這個段落的連結"></a></h2>
<p>過去學程式、寫程式，是一個痛苦，且漫長的週期。</p>
<p>沒學到一定的程度，你連小程式都開發不了。</p>
<p>但現在有了 AI，即使你不會寫程式，也能透過對話的方式，一步步完成專案的建立。</p>
<p>像今天這隻影片就分享了用 Vibe Coding 從 0 到 1 的完整過程，雖然專案的規模較小，不過日後面對更大型的專案時，底層邏輯與流程也是一樣的。</p>
<p>建議大家先從小專案開始練習、熟悉開發的流程；這樣日後想設計大專案時，會更有完整的概念。</p>
<p>另外隨著專案規模擴大，功能增加，你還需要搭配一些自動化測試的工具來輔助。</p>
<p>單靠人工的方式手動測試，不但沒效率，還很容易有遺漏的測試項目。</p>
<p>而且新功能的增加，有可能會導致舊功能壞掉，<strong>如果這隻 YT 影片的按讚破 200，我會再出一隻影片。</strong></p>
<p>跟大家分享用 AI 撰寫測試程式的概念與技巧～</p>
<blockquote class="pullquote"><p>之所以製作一系列 Vibe Coding 影片，是為了避免大家踩到我遇過的「坑」。</p>
</blockquote>
<p>如果你對 AI 工具、自我成長、職涯發展感興趣，<a href="https://dean-lin.medium.com/subscribe">歡迎追蹤我</a>！我會我會持續分享新的觀點 💡</p>
<figure class="figure"><img src="https://deanlin.net/blog/202510/cursor-vibe-coding-end-to-end/images/cursor-vibe-coding-end-to-end-26.webp" width="1400" height="914" alt="林鼎淵著作《工程師下班有約：企業內訓講師帶你認清職涯真相！》" loading="lazy" decoding="async">
  <figcaption><a href="https://www.tenlong.com.tw/products/9786264142083">https://www.tenlong.com.tw/products/9786264142083</a></figcaption>
</figure>

<a class="linkcard" href="https://deanlin.net/categories/ai/">
  <span class="lc-body">
    <span class="lc-title">AI 實戰</span>
    <span class="lc-desc">實測生成式 AI 工具、Vibe Coding 與自動化流程的第一手心得，把 AI 變成每天都用得上的即戰力。</span>
    <span class="lc-site"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M5 12h14M13 6l6 6-6 6"/></svg>林鼎淵 Dean Lin</span>
  </span>
</a>

]]></content:encoded>
    </item>
    <item>
      <title>Cursor 教學：安裝 3 款讓 Vibe Coding 效率翻倍的工具！用「個人形象網站」實戰演練</title>
      <link>https://deanlin.net/blog/202509/cursor-productivity-extensions/</link>
      <pubDate>Sun, 28 Sep 2025 19:36:06 +0800</pubDate>
      <guid>https://deanlin.net/blog/202509/cursor-productivity-extensions/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>AI 實戰</category>
      <description>在這篇文章中，我會示範安裝 3 款工具，讓 Cursor AI Agent 效率直接翻倍，並以「個人形象網站」為例，完整實戰給你看！</description>
      <media:content url="https://deanlin.net/blog/202509/cursor-productivity-extensions/images/cursor-productivity-extensions-og_hu_a51633a2c6413bdb.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<p>最近幫企業上 Vibe Coding 的課程時，除了請他們安裝 Cursor 外。</p>
<p>在課程中還會透過實際範例，分享哪些工具安裝後，可以提升 Vibe Coding 的效率。</p>
<p>因為其實有很多的需求，根本不需要寫程式，直接使用別人完成的套件就好。</p>
<p>你也不用煩惱該使用哪個套件，以及套件該如何使用；因為安裝好這些工具後，AI Agent 會根據你提出的需求，自行判斷這些工具使用的時機點。</p>
<blockquote>
<p>建議搭配影片觀看實際的操作步驟。</p>
</blockquote>
<div class="yt" data-yt="5DEV_K0JauQ" data-title="【Cursor 教學】安裝 3 款讓 Vibe Coding 效率翻倍的工具！用「個人形象網站」實戰演練 #python #nodejs #nvm #git #ai #vibecoding">
  <a class="yt-link" href="https://www.youtube.com/watch?v=5DEV_K0JauQ" target="_blank" rel="noopener" aria-label="播放影片：【Cursor 教學】安裝 3 款讓 Vibe Coding 效率翻倍的工具！用「個人形象網站」實戰演練 #python #nodejs #nvm #git #ai #vibecoding">
    <img src="https://i.ytimg.com/vi/5DEV_K0JauQ/hqdefault.jpg" alt="" width="480" height="360" loading="lazy" decoding="async">
    <span class="yt-play" aria-hidden="true"><svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true" focusable="false"><path d="M8 5.2v13.6L19 12z"/></svg></span>
    <span class="yt-title">【Cursor 教學】安裝 3 款讓 Vibe Coding 效率翻倍的工具！用「個人形象網站」實戰演練 #python #nodejs #nvm #git #ai #vibecoding</span>
  </a>
</div>

<p><img src="https://deanlin.net/blog/202509/cursor-productivity-extensions/images/cursor-productivity-extensions-01.webp" width="1233" height="690" alt="Cursor 教學：安裝 3 款讓 Vibe Coding 效率翻倍的工具！用「個人形象網站」實戰演練（圖 1）" fetchpriority="high" decoding="async"></p>
<pre tabindex="0"><code>文章大綱

① 突破 AI Agent 限制，用 Python 自動萃取文件、網路爬蟲
② 安裝 Node.js，讓 AI 生成漂亮、專業的形象網頁
③ 加入 Git 版控，Vibe Coding 放心爆改
</code></pre><p>在這篇文章中，我會帶你一步步操作，讓你看完後也能打造出屬於自己的個人形象網站！</p>
<blockquote>
<p><a href="https://youtu.be/kVniOF36GEk">如果你是第一次接觸 Cursor，可以先看這隻影片。</a></p>
</blockquote>
<hr>
<h3 id="文章開始前先提醒一下大家">文章開始前，先提醒一下大家：<a class="h-anchor" href="#%e6%96%87%e7%ab%a0%e9%96%8b%e5%a7%8b%e5%89%8d%e5%85%88%e6%8f%90%e9%86%92%e4%b8%80%e4%b8%8b%e5%a4%a7%e5%ae%b6" aria-label="這個段落的連結"></a></h3>
<h3 id="ai-agent-的功能非常強大給他權限後不僅能自動安裝所需套件執行指令還能新增修改甚至刪除檔案因此使用時務必小心">AI Agent 的功能非常強大，給他權限後，不僅能自動安裝所需套件、執行指令，還能新增、修改甚至刪除檔案，因此使用時務必小心。<a class="h-anchor" href="#ai-agent-%e7%9a%84%e5%8a%9f%e8%83%bd%e9%9d%9e%e5%b8%b8%e5%bc%b7%e5%a4%a7%e7%b5%a6%e4%bb%96%e6%ac%8a%e9%99%90%e5%be%8c%e4%b8%8d%e5%83%85%e8%83%bd%e8%87%aa%e5%8b%95%e5%ae%89%e8%a3%9d%e6%89%80%e9%9c%80%e5%a5%97%e4%bb%b6%e5%9f%b7%e8%a1%8c%e6%8c%87%e4%bb%a4%e9%82%84%e8%83%bd%e6%96%b0%e5%a2%9e%e4%bf%ae%e6%94%b9%e7%94%9a%e8%87%b3%e5%88%aa%e9%99%a4%e6%aa%94%e6%a1%88%e5%9b%a0%e6%ad%a4%e4%bd%bf%e7%94%a8%e6%99%82%e5%8b%99%e5%bf%85%e5%b0%8f%e5%bf%83" aria-label="這個段落的連結"></a></h3>
<h2 class="h-nomark" id="-突破-ai-agent-限制用-python-自動萃取文件網路爬蟲">① 突破 AI Agent 限制，用 Python 自動萃取文件、網路爬蟲<a class="h-anchor" href="#-%e7%aa%81%e7%a0%b4-ai-agent-%e9%99%90%e5%88%b6%e7%94%a8-python-%e8%87%aa%e5%8b%95%e8%90%83%e5%8f%96%e6%96%87%e4%bb%b6%e7%b6%b2%e8%b7%af%e7%88%ac%e8%9f%b2" aria-label="這個段落的連結"></a></h2>
<p>第一個建議大家安裝的工具是 Python。</p>
<p>我們安裝 Python 不是為了寫程式，而是為了讓 AI Agent 在面對問題時，有更多的解決方案。</p>
<h3 id="-安裝-python">安裝 Python<a class="h-anchor" href="#-%e5%ae%89%e8%a3%9d-python" aria-label="這個段落的連結"></a></h3>
<p>安裝 Python 這塊，大家可以直接到<a href="http://python.org/downloads/">官網</a>，根據自己的作業系統下載安裝檔即可。</p>
<p><img src="https://deanlin.net/blog/202509/cursor-productivity-extensions/images/cursor-productivity-extensions-02.webp" width="1400" height="753" alt="安裝 Python（圖 2）" loading="lazy" decoding="async"></p>
<p>安裝步驟我就不再贅述，安裝完成後，我們可以在 Cursor 打開一個新的終端機，然後輸入</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>python --version
</span></span></code></pre></div><p><img src="https://deanlin.net/blog/202509/cursor-productivity-extensions/images/cursor-productivity-extensions-03.webp" width="741" height="181" alt="安裝 Python（圖 3）" loading="lazy" decoding="async"></p>
<p>如果有出現版號，就代表有順利安裝，接下來讓我們進入到實戰階段。</p>
<blockquote>
<p>若輸入 <code>python &ndash;version</code> 沒有反應，請改用 <code>python3 &ndash;version</code></p>
</blockquote>
<h3 id="-提取文件資訊">提取文件資訊<a class="h-anchor" href="#-%e6%8f%90%e5%8f%96%e6%96%87%e4%bb%b6%e8%b3%87%e8%a8%8a" aria-label="這個段落的連結"></a></h3>
<p>像有些人會把 Cursor 當成 RAG 的工具使用，透過他快速檢索、分析、統整資訊。</p>
<p>但他有一個痛點，就是 Cursor 只能檢索「文字檔」，比如 Markdown/txt 這類型的文件，或是 JavaScript/Python 這類的程式。</p>
<p>假如你的檔案類型是 Word/PPT/PDF，Cursor 預設是沒辦法幫你檢索的。</p>
<p><strong>但如果有安裝 Python，這一切就變成了有可能，像這邊我在資料夾放了自己的講師履歷。</strong></p>
<p>如果今天想把裡面的文件、圖片提取出來，執行下面的指令，AI Agent 就會安裝對應的套件來解析文件，並根據需求將文件儲存成對應的 Markdown 檔案。</p>
<pre tabindex="0"><code>
資料夾有 pdf/ppt/doc 等多種格式的文件，我想請你把所有文件內的「文字」取出來，並確定可以用繁體中文顯示。
並建立一個「doc」的資料夾，用 Markdown 的格式儲存，並整理文字結構，設計合適的大標、中標、小標，方便閱讀。
如果 ppt/doc 裡面有圖片，請建立一個「img」的資料夾，圖片檔的命名，前綴請使用文件檔名。
</code></pre><p>在執行這段指令後，AI Agent 就會設計執行步驟，一開始會先安裝需要的 Python 套件，設計程式來提取文件裡面的文字、圖片。</p>
<p><img src="https://deanlin.net/blog/202509/cursor-productivity-extensions/images/cursor-productivity-extensions-04.webp" width="541" height="586" alt="提取文件資訊（圖 4）" loading="lazy" decoding="async"></p>
<p>稍等一下，我們就可以在「doc」與「img」資料夾中，看到文字與圖片有順利的提取出來；節省過去需要手動複製貼上的步驟。</p>
<p><img src="https://deanlin.net/blog/202509/cursor-productivity-extensions/images/cursor-productivity-extensions-05.webp" width="1400" height="764" alt="提取文件資訊（圖 5）" loading="lazy" decoding="async"></p>
<p>把文字提取出來後，就可以把 Cursor 當成 RAG 工具來協助統整資料了；首先把「doc」資料夾拖到 AI 的對話框，這樣 AI 才知道他要處理的內容範圍</p>
<p>接著貼上下面的 Prompt:</p>
<pre tabindex="0"><code>我是個講師，正在準備個人形象網站的素材，內容要呈現如下資訊：
- 擅長領域: 搭配可以佐證的資訊
- 個人成就: 找出可數據化的成就，展現實力的強大
- 課程資訊: 課程主題、說明、價格、受眾、預期成果
請先建立「integrate」資料夾，並將上面提到的面向用 Markdown 格式建立，並像個善於推銷自己的 KOL 般，用自信的語氣撰寫文案
</code></pre><p>這段 Prompt 我先表明自己的身份是「講師」，要求他幫我整理出「擅長領域、個人成就、課程資訊」的資訊，以此做為「個人形象網站」的素材。</p>
<p>稍等一段時間，在「integrate」資料夾，就能看到 AI 按照需求，幫我們完成了資料的彙整。</p>
<p><img src="https://deanlin.net/blog/202509/cursor-productivity-extensions/images/cursor-productivity-extensions-06.webp" width="1400" height="764" alt="提取文件資訊（圖 6）" loading="lazy" decoding="async"></p>
<h3 id="-用網頁爬蟲蒐集資訊">用網頁爬蟲蒐集資訊<a class="h-anchor" href="#-%e7%94%a8%e7%b6%b2%e9%a0%81%e7%88%ac%e8%9f%b2%e8%92%90%e9%9b%86%e8%b3%87%e8%a8%8a" aria-label="這個段落的連結"></a></h3>
<p>另外有些人會使用 AI 來蒐集網路資料，但許多網站是禁止爬蟲的。</p>
<p>這邊我打開 <a href="https://notebooklm.google.com/">NotebookLM</a> 示範給大家看，如果來源貼上用 <a href="https://chatgpt.com/s/t_68d21677946081918576092e1c2d37c1">ChatGPT 分享的對話</a>，NotebookLM 的解析是錯誤的，甚至像是 <a href="https://deanlin.net/blog/202507/afteroffice1-15-years-one-book/">Medium 這類部落格文章</a>也無法辨識</p>
<p><img src="https://deanlin.net/blog/202509/cursor-productivity-extensions/images/cursor-productivity-extensions-07.webp" width="1400" height="764" alt="用網頁爬蟲蒐集資訊（圖 7）" loading="lazy" decoding="async"></p>
<p>但如果你請 AI Agent 幫你用 Selenium 這個網路爬蟲工具，模擬真實使用者就能夠完成這項爬蟲任務了。</p>
<p>大家可以嘗試下面的指令：</p>
<pre tabindex="0"><code>先建立「crawler」資料夾，然使用 Selenium 模擬 Chrome 瀏覽器來設計爬蟲程式
取得下面 ChatGPT 分享連結的對話內容，這個網頁有反爬蟲設計，所以網頁開啟後，先等 5 秒鐘渲染元素再去抓取與 ChatGPT 的對話
https://chatgpt.com/s/t_68d21677946081918576092e1c2d37c1
抓到對話後建立「output」，將資料寫入 Markdown 檔案，檔名使用網頁標題，並優化 Markdown 格式為好閱讀的結構
</code></pre><p>這段提示詞有 2 個重點：</p>
<ul>
<li>要求 AI 使用 Selenium 模擬 Chrome 瀏覽器來設計爬蟲程式</li>
<li>提醒他網頁有反爬蟲設計，所以網頁開啟後，要先等 5 秒鐘渲染元素再去抓取與 ChatGPT 的對話</li>
</ul>
<p>稍等一段時間，在 output 的資料夾中，就能看到 Cursor 突破原有的爬蟲限制，取得與 ChatGPT 的對話內容。</p>
<p><img src="https://deanlin.net/blog/202509/cursor-productivity-extensions/images/cursor-productivity-extensions-08.webp" width="1400" height="764" alt="用網頁爬蟲蒐集資訊（圖 8）" loading="lazy" decoding="async"></p>
<p>剛好透過這個範例提醒大家，雖然我們用 Vibe Coding 完成了這個爬蟲程式。</p>
<p><strong>但他的成功是建立在指令非常明確之上，如果沒有網路爬蟲的基礎概念，AI 的失敗率是非常高的。</strong></p>
<blockquote class="pullquote"><p>專業知識，是你在 AI 時代的護城河。</p>
</blockquote>
<hr>
<h2 class="h-nomark" id="-安裝-nodejs讓-ai-生成漂亮專業的形象網頁">② 安裝 Node.js，讓 AI 生成漂亮、專業的形象網頁<a class="h-anchor" href="#-%e5%ae%89%e8%a3%9d-nodejs%e8%ae%93-ai-%e7%94%9f%e6%88%90%e6%bc%82%e4%ba%ae%e5%b0%88%e6%a5%ad%e7%9a%84%e5%bd%a2%e8%b1%a1%e7%b6%b2%e9%a0%81" aria-label="這個段落的連結"></a></h2>
<p>大多數人 Vibe Coding 的第一個作品，通常是網頁。</p>
<p>但如果你只是請 Cursor 幫你設計一個公司或是個人的形象網頁，那 AI 通常是使用簡單的 HTML 與 CSS 做基礎設計。</p>
<p><strong>但如果安裝了 Node.js，你就可以直接使用知名的前端三大框架 Vue/React/Angular 來做設計。</strong></p>
<p>這些框架生成出來的結果會更符合現代審美，未來想擴充功能時，也有豐富的套件可以直接使用。</p>
<h3 id="-安裝-nodejs">安裝 Node.js<a class="h-anchor" href="#-%e5%ae%89%e8%a3%9d-nodejs" aria-label="這個段落的連結"></a></h3>
<p>在安裝 Node.js 這塊，我建議大家透過 NVM 來安裝</p>
<ul>
<li><a href="https://github.com/nvm-sh/nvm?tab=readme-ov-file#installing-and-updating">Mac 執行指令就可以安裝了</a></li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash
</span></span></code></pre></div><ul>
<li><a href="https://github.com/coreybutler/nvm-windows/releases">但如果你是 Windows，就需要下載 exe 檔來安裝</a></li>
</ul>
<p><img src="https://deanlin.net/blog/202509/cursor-productivity-extensions/images/cursor-productivity-extensions-09.webp" width="1075" height="451" alt="安裝 Node.js（圖 9）" loading="lazy" decoding="async"></p>
<p>因為 Node.js 有多個版本，而 NVM 這款工具可以幫你快速在不同版本間切換、更新。</p>
<p>nvm 安裝完成後，開啟終端機輸入 <code>nvm -v</code>，確認有成功安裝：</p>
<p><img src="https://deanlin.net/blog/202509/cursor-productivity-extensions/images/cursor-productivity-extensions-10.webp" width="767" height="221" alt="安裝 Node.js（圖 10）" loading="lazy" decoding="async"></p>
<p>接下來輸入 <code>nvm install &ndash;lts</code> 來安裝最新版本的 Node.js</p>
<p>安裝完畢後，輸入 <code>node -v</code> 便可檢視剛剛安裝與現在使用的對應版本</p>
<p><img src="https://deanlin.net/blog/202509/cursor-productivity-extensions/images/cursor-productivity-extensions-11.webp" width="767" height="229" alt="安裝 Node.js（圖 11）" loading="lazy" decoding="async"></p>
<p>完成上述步驟後，接下來就可以開始製作網頁了。</p>
<h3 id="-生成個人形象網頁">生成個人形象網頁<a class="h-anchor" href="#-%e7%94%9f%e6%88%90%e5%80%8b%e4%ba%ba%e5%bd%a2%e8%b1%a1%e7%b6%b2%e9%a0%81" aria-label="這個段落的連結"></a></h3>
<p>在前面，已經透過 AI 從 Word/PDF/PPT 裡面取出了個人資訊與相關照片，接下來我就直接示範如何用 Cursor 生成個人形象網頁的初版。</p>
<blockquote>
<p>提醒大家，照片取出來後，建議給予分類、命名，這樣 AI 生成的結果會更好。</p>
</blockquote>
<p><img src="https://deanlin.net/blog/202509/cursor-productivity-extensions/images/cursor-productivity-extensions-12.webp" width="1024" height="644" alt="生成個人形象網頁（圖 12）" loading="lazy" decoding="async"></p>
<p>下面我們把生成網頁，要參考的文字、圖片資料夾拖到與 AI 的對話，然後貼上下面指令：</p>
<pre tabindex="0"><code>建立一個「personal-website」資料夾，我想要用 React 框架，設計一個個人形象網頁。
網頁一定要呈現的頁面如下：
- 頂部: 有個人形象照與基礎資訊
- 擅長領域: 切換不同領域時更換對應顏色
- 個人成就: 加上互動效果與圖片
- 課程資訊: 以卡片顯示，含說明與價格
- 聯絡洽詢: 頁面最下方
設計重點:
- RWD 響應式設計
- 有一致的色系規範
- 搭配對應圖片（請複製到專案並用相對路徑）
- 互動效果與視覺化呈現（例如 hover、切換）
整體水準:
- 高度專業感
- 資訊重點清晰
- 圖片長寬比例需考量版面配置
</code></pre><p>這段指令除了一開始有指定使用的<strong>前端框架為 React </strong>外，下面有分成 3 塊指引：</p>
<ol>
<li>首先我跟他說明頁面要呈現的資訊</li>
<li>接著提醒設計要注意的重點</li>
<li>最後則是放上一些額外的需求</li>
</ol>
<p>然後稍等一段時間，個人形象網頁的初版就完成了：</p>
<div class="gallery-row cols-2">

<figure class="figure"><img src="https://deanlin.net/blog/202509/cursor-productivity-extensions/images/cursor-productivity-extensions-13.webp" width="1400" height="713" alt="生成個人形象網頁（圖 13）" fetchpriority="high" decoding="async">
</figure>

<figure class="figure"><img src="https://deanlin.net/blog/202509/cursor-productivity-extensions/images/cursor-productivity-extensions-14.webp" width="1400" height="713" alt="生成個人形象網頁（圖 14）" loading="lazy" decoding="async">
</figure>


</div>

<blockquote class="pullquote"><p>但還是要提醒大家，AI 只是加速製作網頁的過程，你要自己去確認細節是否符合自己的需求。</p>
</blockquote>
<p>像是基本的文字描述、圖片擺放是否恰當，以及 RWD 的設計是否完善等等。</p>
<p><strong>如果不知道在哪新增圖片、或是文字大小不知道怎麼調整，可以直接向 AI 詢問。</strong></p>
<p>如果生成網頁的過程遇到 Bug，出現一堆紅字，那也非常正常。</p>
<p><img src="https://deanlin.net/blog/202509/cursor-productivity-extensions/images/cursor-productivity-extensions-15.webp" width="1400" height="757" alt="生成個人形象網頁（圖 15）" loading="lazy" decoding="async"></p>
<p>你可以把錯誤訊息複製下來，貼到 AI 的對話窗，請他協助解決。</p>
<p><strong>這方法能解決大多數的基礎問題，但如果遇到的錯誤太多，或是邏輯上的問題，就需要根據實際情境來處理，這邊會再找時間寫一篇文章分享。</strong></p>
<hr>
<h2 class="h-nomark" id="-加入-git-版控vibe-coding-放心爆改">③ 加入 Git 版控，Vibe Coding 放心爆改<a class="h-anchor" href="#-%e5%8a%a0%e5%85%a5-git-%e7%89%88%e6%8e%a7vibe-coding-%e6%94%be%e5%bf%83%e7%88%86%e6%94%b9" aria-label="這個段落的連結"></a></h2>
<p>Vibe Coding 時，你的每個指令都可能大幅調整專案的程式。</p>
<p>但 AI 修改的結果未必是你要的，甚至有時會把正常的功能給改壞了。</p>
<p>為了把改壞的內容救回來，我會建議大家安裝 Git，這個程式碼版本控制工具，並註冊 GitHub 帳號來管理專案。</p>
<h3 id="-安裝-git">安裝 Git<a class="h-anchor" href="#-%e5%ae%89%e8%a3%9d-git" aria-label="這個段落的連結"></a></h3>
<p>首先，我們可以到 <a href="https://git-scm.com/downloads">Git 官網下載</a>安裝檔</p>
<p><img src="https://deanlin.net/blog/202509/cursor-productivity-extensions/images/cursor-productivity-extensions-16.webp" width="1400" height="757" alt="安裝 Git（圖 16）" loading="lazy" decoding="async"></p>
<p>安裝好 Git 後，開啟 Cursor 終端機輸入指令 `git -v` 確認有正確安裝</p>
<p>接著用下面的指令來完成初始化設定，請自行調整成自己的英文名字與 email</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git config --global user.name <span style="color:#a5d6ff">&#34;baobao&#34;</span>
</span></span><span style="display:flex;"><span>git config --global user.email <span style="color:#a5d6ff">&#34;baobao@gmail.com&#34;</span>
</span></span></code></pre></div><p>然後產生 SSH Key，這是為了確保這台電腦有更新專案的權限，這邊也請自行調整成自己的 mail</p>
<pre tabindex="0"><code>ssh-keygen -t ed25519 -C &#34;baobao@gmail.com&#34;
</code></pre><p>SSH Key 生成出來後，可以透過下面的指令顯示公鑰，這會在下一個步驟用到</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>cat ~/.ssh/id_ed25519.pub
</span></span></code></pre></div><h3 id="-註冊-github">註冊 GitHub<a class="h-anchor" href="#-%e8%a8%bb%e5%86%8a-github" aria-label="這個段落的連結"></a></h3>
<p>這邊我們來到 <a href="https://github.com/">GitHub 的官網</a>，你可以直接使用 Gmail 來註冊。</p>
<p><img src="https://deanlin.net/blog/202509/cursor-productivity-extensions/images/cursor-productivity-extensions-17.webp" width="1400" height="757" alt="註冊 GitHub（圖 17）" loading="lazy" decoding="async"></p>
<p>註冊完成後，我們到帳號右上角的 Settings</p>
<p><img src="https://deanlin.net/blog/202509/cursor-productivity-extensions/images/cursor-productivity-extensions-18.webp" width="1400" height="757" alt="註冊 GitHub（圖 18）" loading="lazy" decoding="async"></p>
<p>選擇側邊欄的「SSH and GPG Keys」，點擊「New SSH Key」來新增剛剛生成的公鑰</p>
<p><img src="https://deanlin.net/blog/202509/cursor-productivity-extensions/images/cursor-productivity-extensions-19.webp" width="1400" height="757" alt="註冊 GitHub（圖 19）" loading="lazy" decoding="async"></p>
<p><img src="https://deanlin.net/blog/202509/cursor-productivity-extensions/images/cursor-productivity-extensions-20.webp" width="1400" height="757" alt="註冊 GitHub（圖 20）" loading="lazy" decoding="async"></p>
<p>接著建立一個新的 repository，用來儲存剛剛 AI 生成的網頁專案。</p>
<p><img src="https://deanlin.net/blog/202509/cursor-productivity-extensions/images/cursor-productivity-extensions-21.webp" width="1400" height="757" alt="註冊 GitHub（圖 21）" loading="lazy" decoding="async"></p>
<p>Repository name 可以自由輸入，這邊我打「personal-website」，Description 可保留空</p>
<p><img src="https://deanlin.net/blog/202509/cursor-productivity-extensions/images/cursor-productivity-extensions-22.webp" width="1400" height="757" alt="註冊 GitHub（圖 22）" loading="lazy" decoding="async"></p>
<p>如果你不想把這個 Repository 公開，可以在「Configuration」把 visibility 改成「Private」</p>
<p><img src="https://deanlin.net/blog/202509/cursor-productivity-extensions/images/cursor-productivity-extensions-23.webp" width="1400" height="757" alt="註冊 GitHub（圖 23）" loading="lazy" decoding="async"></p>
<h3 id="-把專案更新到-github">把專案更新到 GitHub<a class="h-anchor" href="#-%e6%8a%8a%e5%b0%88%e6%a1%88%e6%9b%b4%e6%96%b0%e5%88%b0-github" aria-label="這個段落的連結"></a></h3>
<p>接著回到剛剛 Cursor 幫我們生成的專案，在終端機輸入 <code>git init</code> 來初始化專案 git。</p>
<p><img src="https://deanlin.net/blog/202509/cursor-productivity-extensions/images/cursor-productivity-extensions-24.webp" width="1120" height="210" alt="把專案更新到 GitHub（圖 24）" loading="lazy" decoding="async"></p>
<p>然後回到網頁，複製第一行 <code>git add…</code> 貼到專案的終端機，這樣我們就可以把電腦的專案與 GitHub 上面的專案連結。</p>
<p><img src="https://deanlin.net/blog/202509/cursor-productivity-extensions/images/cursor-productivity-extensions-25.webp" width="1400" height="239" alt="把專案更新到 GitHub（圖 25）" loading="lazy" decoding="async"></p>
<p>然後點擊左上方的 Source Control，你可以手動在框框內輸入這次的更新內容，又或是點擊旁邊的星星按鈕，讓 AI 自動產生：</p>
<p><img src="https://deanlin.net/blog/202509/cursor-productivity-extensions/images/cursor-productivity-extensions-26.webp" width="1400" height="617" alt="把專案更新到 GitHub（圖 26）" loading="lazy" decoding="async"></p>
<p>確認文字描述符合自己的預期後，點擊 commit，就會在自己的電腦記錄這個變更；但如果想更新到雲端，你需要點擊 Push 才會成功喔！</p>
<p>Push 後我們回到 GitHub 網頁，刷新一下頁面，如果能看到專案的內容，就代表你成功了！</p>
<p><img src="https://deanlin.net/blog/202509/cursor-productivity-extensions/images/cursor-productivity-extensions-27.webp" width="1400" height="727" alt="把專案更新到 GitHub（圖 27）" loading="lazy" decoding="async"></p>
<p>接著回到專案，嘗試與 AI 提出一個修改的需求</p>
<pre tabindex="0"><code>我希望可以有 Dark Mode 與 Light Mode 的設計
</code></pre><p>等 AI 執行完畢後，你可以在 Source Control 這個地方看到 Vibe Coding 更新的內容。</p>
<p><img src="https://deanlin.net/blog/202509/cursor-productivity-extensions/images/cursor-productivity-extensions-28.webp" width="1400" height="820" alt="把專案更新到 GitHub（圖 28）" loading="lazy" decoding="async"></p>
<p>如果發現他改壞了，後續對話也無法解決，你可以全部放棄變更！</p>
<p><img src="https://deanlin.net/blog/202509/cursor-productivity-extensions/images/cursor-productivity-extensions-29.webp" width="1400" height="820" alt="把專案更新到 GitHub（圖 29）" loading="lazy" decoding="async"></p>
<p>這邊我再給他一個要調整的內容</p>
<pre tabindex="0"><code>1. 形象照的圖片顯示上半部就好
2. 最上方個人簡介區塊下面要有留白
3. 我的成就裡面的圖片，可以根據不同比例完整顯示，要上下左右置中，多個地方用透明顯示
4. 職涯發展顧問、職涯發展的顏色太淡，不好辨識
</code></pre><p>如果對這次的調整滿意，就提交並發布一版保存；提交我們的 commit，點擊同步變更後他就會提交到 GitHub 雲端上面，以上是最基礎的 Git 操作。</p>
<p>他是一個很棒的版本控制工具，功能非常多，這邊放上常用指令，有興趣深入研究的朋友可以保存一下。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git init：在目前資料夾初始化 git
</span></span><span style="display:flex;"><span>git clone &lt;repo-url&gt;：將遠端的專案下載到電腦
</span></span><span style="display:flex;"><span>git checkout &lt;branch&gt;：切換到指定分支
</span></span><span style="display:flex;"><span>git checkout -b &lt;branch&gt;：建立並切換到新分支
</span></span><span style="display:flex;"><span>git merge &lt;branch&gt;：將指定分支合併到當前分支
</span></span><span style="display:flex;"><span>git stash：暫存目前變更
</span></span><span style="display:flex;"><span>git stash pop：取回並套用暫存變更
</span></span><span style="display:flex;"><span>git push origin &lt;branch&gt;：將本地分支推送到遠端
</span></span><span style="display:flex;"><span>git pull origin &lt;branch&gt;：拉取遠端分支並合併
</span></span><span style="display:flex;"><span>git fetch：抓取遠端最新狀態
</span></span></code></pre></div><p>如果不知道指令怎麼下，別忘記有 AI 這個好夥伴可以詢問～</p>
<hr>
<h2 id="vibe-coding-的工具很重要但電腦品質也不能太差">Vibe Coding 的工具很重要，但電腦品質也不能太差<a class="h-anchor" href="#vibe-coding-%e7%9a%84%e5%b7%a5%e5%85%b7%e5%be%88%e9%87%8d%e8%a6%81%e4%bd%86%e9%9b%bb%e8%85%a6%e5%93%81%e8%b3%aa%e4%b9%9f%e4%b8%8d%e8%83%bd%e5%a4%aa%e5%b7%ae" aria-label="這個段落的連結"></a></h2>
<p>在舉辦幾場 Vibe Coding 的工作坊後；我發現一件特別的事，明明指令相同，有些學員可以很快取得期待的結果，而有些學員則是很容易卡住。</p>
<p>一開始我以為是運氣問題，但深入討論後，<strong>發現瓶頸其實是出在電腦的硬體規格上面。</strong></p>
<p>因為 Vibe Coding 會需要索引專案、執行指令、編譯程式，假如電腦的記憶體、CPU，以及硬碟的配置較差。</p>
<p>那你執行的每一步都要會花更多的時間，有時不是 AI 做不出來，只是速度慢到你以為他卡住了。</p>
<p>其實電腦也不需要買多好的，我前陣子買了一台一萬八的 Windows 電腦，CPU 是 Intel i7、記憶體 16 GB、硬碟用 SSD，跑起來就很夠用了。</p>
<hr>
<h2 id="結語掌握讓-vibe-coding-效率翻倍的工具">結語：掌握讓 Vibe Coding 效率翻倍的工具<a class="h-anchor" href="#%e7%b5%90%e8%aa%9e%e6%8e%8c%e6%8f%a1%e8%ae%93-vibe-coding-%e6%95%88%e7%8e%87%e7%bf%bb%e5%80%8d%e7%9a%84%e5%b7%a5%e5%85%b7" aria-label="這個段落的連結"></a></h2>
<p>最後來回顧一下今天分享的 3 個工具：</p>
<ol>
<li>我們可以透過 Python 幫我們讀取不同格式的文件內容，甚至用網路爬蟲取得詳細資訊。</li>
<li>安裝 Node.js 後，我們可以使用最流行前端框架，打造更專業、漂亮的網站。</li>
<li>有了 Git 協助版本控管後，我們就可以用 Vibe Coding 大膽嘗試創意的想法</li>
</ol>
<p><strong>安裝這些工具不是為了讓你變成程式設計師，而是讓 AI 有更多武器來幫你解決問題。</strong></p>
<p>看到這裡，大家應該可以感受到 Vibe Coding 的可能性與便利。</p>
<blockquote class="pullquote"><p>但還是要提醒大家，Vibe Coding 並不是完全零門檻的事情。</p>
</blockquote>
<p>在實務上，當問題變得更複雜，或需要跨領域整合時，專業知識仍然是必要的。</p>
<p>像今天分享的這些工具，對沒有資訊背景的人來說，可能都是知識盲區。</p>
<blockquote class="pullquote"><p>AI 能做到哪些事情，其實是取決於你對這個世界的認知。</p>
</blockquote>
<p>希望今天的分享，能讓你使用 Cursor 做 Vibe Coding 時，獲得更好的體驗。</p>
<p>如果你對 AI 工具、自我成長、職涯發展感興趣，<a href="https://dean-lin.medium.com/subscribe">歡迎追蹤我</a>！我會我會持續分享新的觀點 💡</p>
<figure class="figure"><img src="https://deanlin.net/blog/202509/cursor-productivity-extensions/images/cursor-productivity-extensions-30.webp" width="1400" height="914" alt="林鼎淵著作《工程師下班有約：企業內訓講師帶你認清職涯真相！》" loading="lazy" decoding="async">
  <figcaption><a href="https://www.tenlong.com.tw/products/9786264142083">https://www.tenlong.com.tw/products/9786264142083</a></figcaption>
</figure>

<a class="linkcard" href="https://deanlin.net/categories/ai/">
  <span class="lc-body">
    <span class="lc-title">AI 實戰</span>
    <span class="lc-desc">實測生成式 AI 工具、Vibe Coding 與自動化流程的第一手心得，把 AI 變成每天都用得上的即戰力。</span>
    <span class="lc-site"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M5 12h14M13 6l6 6-6 6"/></svg>林鼎淵 Dean Lin</span>
  </span>
</a>

]]></content:encoded>
    </item>
    <item>
      <title>靠問 ChatGPT 寫程式，能完成 OpenAI &amp; Linebot 的 Side Project 嗎？（上）</title>
      <link>https://deanlin.net/blog/202301/chatgpt-line-bot-side-project-part-1/</link>
      <pubDate>Wed, 25 Jan 2023 14:35:08 +0800</pubDate>
      <guid>https://deanlin.net/blog/202301/chatgpt-line-bot-side-project-part-1/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>AI 實戰</category>
      <description>許多網紅都說 ChatGPT 寫程式的能力超強，不但可以解 LeeCode 還能幫你優化程式，甚至寫點小專案都不是問題，但事實真的是如此嗎？就讓筆者透過一個 Side Project 來測試看看。</description>
      <media:content url="https://deanlin.net/blog/202301/chatgpt-line-bot-side-project-part-1/images/chatgpt-line-bot-side-project-part-1-og_hu_517affb4376a2a91.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<p>許多網紅都說 ChatGPT 寫程式的能力超強，不但可以解 LeeCode 還能幫你優化程式，甚至寫點小專案都不是問題，但事實真的是如此嗎？就讓筆者透過一個 Side Project 來測試看看。</p>
<p><img src="https://deanlin.net/blog/202301/chatgpt-line-bot-side-project-part-1/images/chatgpt-line-bot-side-project-part-1-01.webp" width="1260" height="689" alt="靠問 ChatGPT 寫程式，能完成 OpenAI &amp; Linebot 的 Side Project 嗎？（上）（圖 1）" fetchpriority="high" decoding="async"></p>
<pre tabindex="0"><code>大綱

▋ 詢問 ChatGPT 實作的步驟
▋ 前置作業 1：申請 OpenAI API key
▋ 前置作業 2：取得 Line 的 Channel access token 和 Channel secret
▋ 建立 OpenAI-Linebot 的 Node.js 專案
▋ 小結：他還還努力學習中
</code></pre><h2 id="-詢問-chatgpt-實作的步驟">詢問 ChatGPT 實作的步驟<a class="h-anchor" href="#-%e8%a9%a2%e5%95%8f-chatgpt-%e5%af%a6%e4%bd%9c%e7%9a%84%e6%ad%a5%e9%a9%9f" aria-label="這個段落的連結"></a></h2>
<p>目前看起來，ChatGPT 回應給我的實作步驟可行性很高，而且每個重點都提示到了。</p>
<p><img src="https://deanlin.net/blog/202301/chatgpt-line-bot-side-project-part-1/images/chatgpt-line-bot-side-project-part-1-02.webp" width="1074" height="738" alt="詢問 ChatGPT 實作的步驟（圖 2）" loading="lazy" decoding="async"></p>
<hr>
<h2 id="前置作業-1申請-openai-api-key">前置作業 1：申請 OpenAI API key<a class="h-anchor" href="#%e5%89%8d%e7%bd%ae%e4%bd%9c%e6%a5%ad-1%e7%94%b3%e8%ab%8b-openai-api-key" aria-label="這個段落的連結"></a></h2>
<p><strong>SETP 1</strong>：進入 OpenAI 官網：<a href="https://beta.openai.com/">https://beta.openai.com/</a></p>
<p><strong>SETP 2</strong>：註冊 &amp; 登入 OpenAI 帳號，進入首頁。</p>
<p><img src="https://deanlin.net/blog/202301/chatgpt-line-bot-side-project-part-1/images/chatgpt-line-bot-side-project-part-1-03.webp" width="1400" height="729" alt="前置作業 1：申請 OpenAI API key（圖 3）" loading="lazy" decoding="async"></p>
<p><strong>SETP 3</strong>：點擊右上角圖像，選擇「View API keys」。</p>
<p><img src="https://deanlin.net/blog/202301/chatgpt-line-bot-side-project-part-1/images/chatgpt-line-bot-side-project-part-1-04.webp" width="722" height="476" alt="前置作業 1：申請 OpenAI API key（圖 4）" loading="lazy" decoding="async"></p>
<p><strong>SETP 4</strong>：點擊「Create new secret key」。</p>
<p><img src="https://deanlin.net/blog/202301/chatgpt-line-bot-side-project-part-1/images/chatgpt-line-bot-side-project-part-1-05.webp" width="1400" height="631" alt="前置作業 1：申請 OpenAI API key（圖 5）" loading="lazy" decoding="async"></p>
<p><strong>SETP 5</strong>：產生出來的 API Key 請保存好，之後你都不會再看到他了。</p>
<p><img src="https://deanlin.net/blog/202301/chatgpt-line-bot-side-project-part-1/images/chatgpt-line-bot-side-project-part-1-06.webp" width="1400" height="584" alt="前置作業 1：申請 OpenAI API key（圖 6）" loading="lazy" decoding="async"></p>
<hr>
<h2 id="前置作業-2取得-line-的-channel-access-token-和-channel-secret">前置作業 2：取得 Line 的 Channel access token 和 Channel secret<a class="h-anchor" href="#%e5%89%8d%e7%bd%ae%e4%bd%9c%e6%a5%ad-2%e5%8f%96%e5%be%97-line-%e7%9a%84-channel-access-token-%e5%92%8c-channel-secret" aria-label="這個段落的連結"></a></h2>
<p>因為步驟比較複雜，所以我有詢問一下 ChatGPT ：</p>
<p><img src="https://deanlin.net/blog/202301/chatgpt-line-bot-side-project-part-1/images/chatgpt-line-bot-side-project-part-1-07.webp" width="1085" height="656" alt="前置作業 2：取得 Line 的 Channel access token 和 Channel secret（圖 7）" loading="lazy" decoding="async"></p>
<p><strong>SETP 1</strong>：登入開發者平台，網址：<a href="https://developers.line.biz/zh-hant/">https://developers.line.biz/zh-hant/</a> 。</p>
<p><strong>SETP 2</strong>：建立一個 provider，名字自取。</p>
<p><img src="https://deanlin.net/blog/202301/chatgpt-line-bot-side-project-part-1/images/chatgpt-line-bot-side-project-part-1-08.webp" width="1400" height="730" alt="前置作業 2：取得 Line 的 Channel access token 和 Channel secret（圖 8）" loading="lazy" decoding="async"></p>
<p><img src="https://deanlin.net/blog/202301/chatgpt-line-bot-side-project-part-1/images/chatgpt-line-bot-side-project-part-1-09.webp" width="878" height="489" alt="前置作業 2：取得 Line 的 Channel access token 和 Channel secret（圖 9）" loading="lazy" decoding="async"></p>
<p><strong>SETP 3</strong>：選擇「Create a Message API channel」，我們要做一個聊天機器人。</p>
<p><img src="https://deanlin.net/blog/202301/chatgpt-line-bot-side-project-part-1/images/chatgpt-line-bot-side-project-part-1-10.webp" width="1400" height="668" alt="前置作業 2：取得 Line 的 Channel access token 和 Channel secret（圖 10）" loading="lazy" decoding="async"></p>
<p>接下來填寫一些必填資訊：</p>
<ul>
<li><strong>Company or owner&rsquo;s country or region</strong>：Taiwan</li>
<li><strong>Channel name</strong>：ChatGPT bot（自由填寫）</li>
<li><strong>Channel description</strong>：ChatGPT bot（自由填寫）</li>
<li><strong>Category</strong>：公眾人物、專業人士（自由填寫）</li>
<li><strong>Subcategory</strong>：IT、電腦（自由填寫）</li>
</ul>
<p><strong>SETP 4</strong>：最下面兩個同意事項要打勾才能按下「Create」按鈕。</p>
<p><img src="https://deanlin.net/blog/202301/chatgpt-line-bot-side-project-part-1/images/chatgpt-line-bot-side-project-part-1-11.webp" width="869" height="289" alt="前置作業 2：取得 Line 的 Channel access token 和 Channel secret（圖 11）" loading="lazy" decoding="async"></p>
<p>最後會讓你確認一下填寫的細節，還有一些規則提醒。</p>
<p><img src="https://deanlin.net/blog/202301/chatgpt-line-bot-side-project-part-1/images/chatgpt-line-bot-side-project-part-1-12.webp" width="901" height="548" alt="前置作業 2：取得 Line 的 Channel access token 和 Channel secret（圖 12）" loading="lazy" decoding="async"></p>
<p><strong>SETP 5</strong>：在 Basic settings 取得 Channel secret（這邊 ChatGPT 說錯成 General settings，不過大體意思能理解）。</p>
<p><img src="https://deanlin.net/blog/202301/chatgpt-line-bot-side-project-part-1/images/chatgpt-line-bot-side-project-part-1-13.webp" width="1332" height="543" alt="前置作業 2：取得 Line 的 Channel access token 和 Channel secret（圖 13）" loading="lazy" decoding="async"></p>
<p><strong>SETP 6</strong>：在 Messaging API 取得 Channel access token。</p>
<p><img src="https://deanlin.net/blog/202301/chatgpt-line-bot-side-project-part-1/images/chatgpt-line-bot-side-project-part-1-14.webp" width="1301" height="641" alt="前置作業 2：取得 Line 的 Channel access token 和 Channel secret（圖 14）" loading="lazy" decoding="async"></p>
<blockquote>
<p><strong>注意</strong>：<br>Channel access token 是用來驗證和保護機器人與 LINE Platform 之間的通訊，因此請勿將它公開給任何人。<br>Channel secret 則是用來驗證 Webhook 事件的簽章。</p>
</blockquote>
<hr>
<h2 id="建立-openai-linebot-的-nodejs-專案">建立 OpenAI-Linebot 的 Node.js 專案<a class="h-anchor" href="#%e5%bb%ba%e7%ab%8b-openai-linebot-%e7%9a%84-nodejs-%e5%b0%88%e6%a1%88" aria-label="這個段落的連結"></a></h2>
<p>下面的步驟，我是透過詢問 ChatGPT 所得到的：「接下來要如何建立一個 Node.js 專案，並安裝&quot;openai&quot; 和 &ldquo;@line/bot-sdk&rdquo; 等套件呢？有相關指令與程式範例嗎？」</p>
<p><strong>SETP 1</strong>：建立一個新的專案目錄，並在終端機中進入該目錄。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>mkdir myproject
</span></span><span style="display:flex;"><span>cd myproject
</span></span></code></pre></div><p><strong>SETP 2</strong>：建立一個 package.json 檔案，並設定專案的相關資訊。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>npm init
</span></span></code></pre></div><p><strong>SETP 3</strong>：安裝 &ldquo;openai&rdquo;、&quot;@line/bot-sdk&quot;、&ldquo;express&quot;套件（這邊原本是建議裝 line-bot-sdk 這個套件，但因為無法使用，所以請 ChatGPT 重選了一次）。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>npm install openai @line/bot-sdk express
</span></span></code></pre></div><blockquote>
<p>注意！在使用上面的指令前，需要先安裝 Node.js 和 npm。</p>
</blockquote>
<p><strong>SETP 4</strong>：建立一個新的檔案放程式碼。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>touch index.js
</span></span></code></pre></div><p><strong>SETP 5</strong>：在 index.js 中引用所需的套件，並設定 OpenAI 的 API key、Linebot 的 channelAccessToken、channelSecret。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#ff7b72">const</span> openai <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;openai&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> line <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;@line/bot-sdk&#39;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>openai.apiKey <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#34;YOUR_OPENAI_API_KEY&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> config <span style="color:#ff7b72;font-weight:bold">=</span> {
</span></span><span style="display:flex;"><span>  channelAccessToken<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;YOUR_CHANNEL_ACCESS_TOKEN&#39;</span>,
</span></span><span style="display:flex;"><span>  channelSecret<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;YOUR_CHANNEL_SECRET&#39;</span>
</span></span><span style="display:flex;"><span>};
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> client <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">new</span> line.Client(config);
</span></span></code></pre></div><p><strong>SETP 6</strong>：在 index.js 中建立一個 HTTP server 並設定 line 的 webhook。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#ff7b72">const</span> express <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;express&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> app <span style="color:#ff7b72;font-weight:bold">=</span> express();
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> port <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">3000</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>app.post(<span style="color:#a5d6ff">&#39;/webhook&#39;</span>, line.middleware(config), (req, res) =&gt; {
</span></span><span style="display:flex;"><span>  Promise
</span></span><span style="display:flex;"><span>    .all(req.body.events.map(handleEvent))
</span></span><span style="display:flex;"><span>    .then((result) =&gt; res.json(result))
</span></span><span style="display:flex;"><span>    .<span style="color:#ff7b72">catch</span>((err) =&gt; {
</span></span><span style="display:flex;"><span>      console.error(err);
</span></span><span style="display:flex;"><span>      res.status(<span style="color:#a5d6ff">500</span>).end();
</span></span><span style="display:flex;"><span>    });
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>app.listen(port, () =&gt; {
</span></span><span style="display:flex;"><span>  console.log(<span style="color:#a5d6ff">`app is running on port </span><span style="color:#a5d6ff">${</span>port<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>);
</span></span><span style="display:flex;"><span>})
</span></span></code></pre></div><p><strong>SETP 7</strong>：在 index.js 中建立一個事件處理函式，當使用者發送訊息時，使用 OpenAI API 將訊息傳到指定的語言模型處理，並將回應的結果傳回給使用者。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#ff7b72">function</span> handleEvent (event) {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">if</span> (event.type <span style="color:#ff7b72;font-weight:bold">!==</span> <span style="color:#a5d6ff">&#39;message&#39;</span> <span style="color:#ff7b72;font-weight:bold">||</span> event.message.type <span style="color:#ff7b72;font-weight:bold">!==</span> <span style="color:#a5d6ff">&#39;text&#39;</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">return</span> Promise.resolve(<span style="color:#79c0ff">null</span>);
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">return</span> openai.Completion.create({
</span></span><span style="display:flex;"><span>    engine<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;text-davinci-002&#34;</span>,
</span></span><span style="display:flex;"><span>    prompt<span style="color:#ff7b72;font-weight:bold">:</span> event.message.text,
</span></span><span style="display:flex;"><span>  }).then(response =&gt; {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">return</span> client.replyMessage(event.replyToken, {
</span></span><span style="display:flex;"><span>      type<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;text&#39;</span>,
</span></span><span style="display:flex;"><span>      text<span style="color:#ff7b72;font-weight:bold">:</span> response.choices[<span style="color:#a5d6ff">0</span>].text
</span></span><span style="display:flex;"><span>    });
</span></span><span style="display:flex;"><span>  });
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p><strong>SETP 8</strong>：最後啟動伺服器 <code>node index.js</code></p>
<p><img src="https://deanlin.net/blog/202301/chatgpt-line-bot-side-project-part-1/images/chatgpt-line-bot-side-project-part-1-15.webp" width="1270" height="202" alt="建立 OpenAI-Linebot 的 Node.js 專案（圖 15）" loading="lazy" decoding="async"></p>
<p>喔喔喔！居然沒有報錯！！！<br>之後可以請 ChatGPT 幫我完成 Side Project 了嗎？</p>
<blockquote>
<p><strong>小提醒</strong>：程式能啟動跟能順利執行是兩件事。</p>
</blockquote>
<hr>
<h2 id="小結他還在努力學習中">小結：他還在努力學習中<a class="h-anchor" href="#%e5%b0%8f%e7%b5%90%e4%bb%96%e9%82%84%e5%9c%a8%e5%8a%aa%e5%8a%9b%e5%ad%b8%e7%bf%92%e4%b8%ad" aria-label="這個段落的連結"></a></h2>
<p>其實過程並沒有想像中順利，上面放的步驟已經是多次優化後得到的答案，下面筆者放幾個請 ChatGPT 優化的片段。</p>
<p><strong>錯誤 1</strong>： 我按造步驟執行後，會噴錯誤：「TypeError: line.Client is not a constructor」</p>
<p><img src="https://deanlin.net/blog/202301/chatgpt-line-bot-side-project-part-1/images/chatgpt-line-bot-side-project-part-1-16.webp" width="1088" height="749" alt="小結：他還在努力學習中（圖 16）" loading="lazy" decoding="async"></p>
<p><strong>錯誤 2</strong>：我調整後，會噴錯誤：「TypeError: Cannot read properties of undefined (reading &lsquo;validateSignature&rsquo;)」</p>
<p><img src="https://deanlin.net/blog/202301/chatgpt-line-bot-side-project-part-1/images/chatgpt-line-bot-side-project-part-1-17.webp" width="1091" height="682" alt="小結：他還在努力學習中（圖 17）" loading="lazy" decoding="async"></p>
<p>其實還有其他錯誤，只是這兩個最嚴重，所以筆者單獨拿出來說一下。</p>
<p>對初學者而言而言，他們可能會覺得是程式哪裡寫錯了，只要修改一下就好；但實際上，真正的問題是<strong> ChatGPT 一開始推薦安裝的套件「line-bot-sdk」就是有問題的</strong>。</p>
<p>這邊放個截圖給大家看一下，<strong>這款套件已經有 7 年多沒有更新，而且幾乎沒有下載量！</strong>如果是缺乏經驗的開發者，應該會在這裡卡很久。</p>
<p><img src="https://deanlin.net/blog/202301/chatgpt-line-bot-side-project-part-1/images/chatgpt-line-bot-side-project-part-1-18.webp" width="1400" height="707" alt="小結：他還在努力學習中（圖 18）" loading="lazy" decoding="async"></p>
<p>後來我請他重新推薦，ChatGPT 才給了比較合理的套件，最後我請他改用「@line/bot-sdk」重寫一遍才得到上面的結果。</p>
<p><img src="https://deanlin.net/blog/202301/chatgpt-line-bot-side-project-part-1/images/chatgpt-line-bot-side-project-part-1-19.webp" width="1075" height="519" alt="小結：他還在努力學習中（圖 19）" loading="lazy" decoding="async"></p>
<p>不知道這篇文章會不會勸退一些使用者？</p>
<p>其實 ChatGPT 的問題並不只這些，<a href="https://deanlin.net/blog/202301/chatgpt-line-bot-side-project-part-2/">下一篇文章</a>我會帶大家把後面「啟動 server 並在 Line 開發者平台設定 webhook 的網址、測試機器人是否能正確回應使用者的訊息」等步驟走完，希望筆者踩過的坑能讓讀者日後使用時少走點彎路。</p>
<blockquote>
<p>本次 ChatGPT 回應的完整程式請參考筆者的 <a href="https://github.com/dean9703111/linebot-openai-chatgpt/blob/master/bug-index.js">Github</a></p>
</blockquote>
<p>未來筆者會出一系列使用 ChatGPT、openAI 的文章，如果你對這個議題感興趣，可以持續關注我的文章！</p>
<a class="linkcard" href="https://deanlin.net/categories/ai/">
  <span class="lc-body">
    <span class="lc-title">AI 實戰</span>
    <span class="lc-desc">實測生成式 AI 工具、Vibe Coding 與自動化流程的第一手心得，把 AI 變成每天都用得上的即戰力。</span>
    <span class="lc-site"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M5 12h14M13 6l6 6-6 6"/></svg>林鼎淵 Dean Lin</span>
  </span>
</a>

<p><strong>想將 AI 導入自己的職場與生活嗎？歡迎<a href="https://www.books.com.tw/products/0011004216">購買筆者出版的新書</a></strong>，幫助您<strong>在 AI 的浪潮中站穩腳步，並在未來的職場競爭中佔據優勢！</strong></p>
<figure class="figure"><img src="https://deanlin.net/blog/202301/chatgpt-line-bot-side-project-part-1/images/chatgpt-line-bot-side-project-part-1-20.webp" width="1400" height="319" alt="林鼎淵著作《ChatGPT 與 AI 繪圖效率大師（新增 Copilot、Gamma、Runway、Suno）》" loading="lazy" decoding="async">
  <figcaption><a href="https://www.books.com.tw/products/0011004216">https://www.books.com.tw/products/0011004216</a></figcaption>
</figure>

]]></content:encoded>
    </item>
    <item>
      <title>透過 Jaeger with OpenTelemetry 追蹤 Kafka 資料傳遞路徑與運行狀況</title>
      <link>https://deanlin.net/blog/202211/trace-kafka-with-jaeger/</link>
      <pubDate>Wed, 23 Nov 2022 23:11:29 +0800</pubDate>
      <guid>https://deanlin.net/blog/202211/trace-kafka-with-jaeger/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>我們可以把 Kafka 當成傳遞消息的「轉運站」，但有時就算 Consumer 順利拿到 Producer 的消息，我們也無法保證接下來的運作都符合預期。</description>
      <media:content url="https://deanlin.net/blog/202211/trace-kafka-with-jaeger/images/trace-kafka-with-jaeger-og_hu_e4284c2e62441bb9.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<p>我們可以把 Kafka 當成傳遞消息的「轉運站」，但有時就算 Consumer 順利拿到 Producer 的消息，我們也無法保證接下來的運作都符合預期。</p>
<p>而且會用到 Kafka 的系統，大多以微服務（Microservices）為主；若沒有搭配合適的追蹤系統，我們很難排查 Consumer 收到消息後發生的錯誤。</p>
<p><img src="https://deanlin.net/blog/202211/trace-kafka-with-jaeger/images/trace-kafka-with-jaeger-01.webp" width="1400" height="726" alt="透過 Jaeger with OpenTelemetry 追蹤 Kafka 資料傳遞路徑與運行狀況（圖 1）" fetchpriority="high" decoding="async"></p>
<blockquote>
<p>這篇文章以實作和邏輯講解為主，如果想了解 Jaeger 這個分布式追蹤系統，請參考<a href="https://deanlin.net/blog/202211/distributed-tracing-jaeger-opentelemetry/">連結</a>；想了解 Kafka 這個分布式消息串流平台，請參考<a href="https://deanlin.net/blog/202211/what-is-kafka/">連結1</a>、<a href="https://deanlin.net/blog/202211/nodejs-kafka-hands-on/">連結2</a></p>
</blockquote>
<p>建議讀者直接到<a href="https://github.com/dean9703111/kafka-jaeger-demo">筆者的 Github</a> Clone 一份下來跑跑看，會更好理解裏面的邏輯（順手按個 ⭐ 更好 🤩）。</p>
<pre tabindex="0"><code>大綱

一、自定義追蹤的 Parent &amp; Child 關聯（Manually propagating span context）
二、使用 Kafka 作為傳遞資料的轉運站
三、在 Local 建立 Node.js 專案來達成目標
四、啟動專案，透過 Jaeger UI 確認 Trace 路徑是否符合預期
</code></pre><h2 id="ㄧ自定義追蹤的-parent--child-關聯manually-propagating-span-context">ㄧ、自定義追蹤的 Parent &amp; Child 關聯（Manually propagating span context）<a class="h-anchor" href="#%e3%84%a7%e8%87%aa%e5%ae%9a%e7%be%a9%e8%bf%bd%e8%b9%a4%e7%9a%84-parent--child-%e9%97%9c%e8%81%afmanually-propagating-span-context" aria-label="這個段落的連結"></a></h2>
<p>其實 OpenTelemetry 有提供許多 Instrumentation 的方案，如：</p>
<ul>
<li><a href="https://www.npmjs.com/package/@opentelemetry/instrumentation-http">@opentelemetry/instrumentation-http</a></li>
<li><a href="https://www.npmjs.com/package/@opentelemetry/instrumentation-grpc">@opentelemetry/instrumentation-grpc</a></li>
<li><a href="https://www.npmjs.com/package/opentelemetry-instrumentation-kafkajs">opentelemetry-instrumentation-kafkajs</a></li>
</ul>
<p>透過上面的套件可以自動產生追蹤紀錄，但有時我們只需要追蹤特定路徑，而使用上面的套件會產生許多不需要的資訊，還容易產生效能瓶頸。</p>
<blockquote>
<p>比如使用「instrumentation-http」這款套件時，它會把所有 http 相關的操作都記錄下來，但我們實際上只關心其中的幾個路徑。</p>
</blockquote>
<p>因此在某些情況下，自定義追蹤的 Parent &amp; Child 關聯是有必要的；下面筆者擷取關鍵的程式碼來做說明：</p>
<h3 id="-透過-w3ctracecontextpropagator-把-span-資訊-inject-到-carrier">透過 W3CTraceContextPropagator 把 span 資訊 inject 到 carrier<a class="h-anchor" href="#-%e9%80%8f%e9%81%8e-w3ctracecontextpropagator-%e6%8a%8a-span-%e8%b3%87%e8%a8%8a-inject-%e5%88%b0-carrier" aria-label="這個段落的連結"></a></h3>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#ff7b72">let</span> span <span style="color:#ff7b72;font-weight:bold">=</span> tracer.startSpan(<span style="color:#a5d6ff">&#39;example&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> carrier <span style="color:#ff7b72;font-weight:bold">=</span> {};
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> propagator <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">new</span> W3CTraceContextPropagator();
</span></span><span style="display:flex;"><span>propagator.inject(
</span></span><span style="display:flex;"><span>    api.trace.setSpanContext(api.context.active(), span.spanContext()),
</span></span><span style="display:flex;"><span>    carrier,
</span></span><span style="display:flex;"><span>    api.defaultTextMapSetter,
</span></span><span style="display:flex;"><span>);
</span></span><span style="display:flex;"><span>span.end()
</span></span></code></pre></div><h3 id="-透過-w3ctracecontextpropagator-extract-carrier-資訊使其成為能使用的-context">透過 W3CTraceContextPropagator extract carrier 資訊，使其成為能使用的 Context<a class="h-anchor" href="#-%e9%80%8f%e9%81%8e-w3ctracecontextpropagator-extract-carrier-%e8%b3%87%e8%a8%8a%e4%bd%bf%e5%85%b6%e6%88%90%e7%82%ba%e8%83%bd%e4%bd%bf%e7%94%a8%e7%9a%84-context" aria-label="這個段落的連結"></a></h3>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#ff7b72">const</span> propagator <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">new</span> W3CTraceContextPropagator();
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// 解析 carrier
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> parentCtx <span style="color:#ff7b72;font-weight:bold">=</span> propagator.extract(api.context.active(), carrier, api.defaultTextMapGetter);
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// 設定 parent 關聯
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> span <span style="color:#ff7b72;font-weight:bold">=</span> tracer.startSpan(<span style="color:#a5d6ff">`report_consumer`</span>, <span style="color:#79c0ff">undefined</span>, parentCtx);
</span></span><span style="display:flex;"><span>span.end()
</span></span></code></pre></div><hr>
<h2 id="二使用-kafka-作為傳遞資料的轉運站">二、使用 Kafka 作為傳遞資料的轉運站<a class="h-anchor" href="#%e4%ba%8c%e4%bd%bf%e7%94%a8-kafka-%e4%bd%9c%e7%82%ba%e5%82%b3%e9%81%9e%e8%b3%87%e6%96%99%e7%9a%84%e8%bd%89%e9%81%8b%e7%ab%99" aria-label="這個段落的連結"></a></h2>
<p>假使今天有個功能是「產出報表」，但它的執行時間長達「3 分鐘」；我想正常的使用者是不會等一隻 API 回傳等這麼久，針對這個議題，我們不妨改個思路：</p>
<p><strong>SETP 1</strong>：在報表的 Table 新增一個「status」欄位，用來表達報表建立的狀況（init、processing、done、fail）。</p>
<p><strong>SETP 2</strong>：前端呼叫產出報表的 API 時</p>
<ul>
<li>在報表的 Table 新增初始資訊，並將 Status 設定為「init」。</li>
<li>將相關資訊透過 Producer 塞入 Kafka。</li>
</ul>
<p><strong>SETP 3</strong>：前端收到 API 回傳的報表 id 後</p>
<ul>
<li>透過 Consumer 監聽報表執行的狀況；可以用報表的 id 作為 Topic 命名邏輯，ex：reports/{id}。</li>
</ul>
<p><strong>SETP 4</strong>：監聽產出報表的 Consumer 從 Kafka 收到命令後</p>
<ul>
<li>透過 id 從資料庫撈出對應的報表資訊，將 Status 設定為「processing」。</li>
<li>呼叫相關服務產出報表，如果其中一個服務發生錯誤，就將 Status 設定為「fail」。</li>
<li>如果報表順利產出，就將 Status 設定為「done」。</li>
</ul>
<blockquote>
<p>對 Status 的任何變動，都會透過 Producer 將資訊傳遞到 <strong>reports/{id} </strong>這個 Topic，讓前端監聽的的 Consumer 可以作出對應的變動。</p>
</blockquote>
<p>上面是比較完整的業務邏輯，建議讀者可以自行實做看看，下面筆者就單純示範透過 Kafka 傳遞 carrier 資訊的部分：</p>
<h3 id="-在-producer-的-value-放上-carrier-的資訊記得要轉為字串">在 Producer 的 value 放上 carrier 的資訊（記得要轉為字串）<a class="h-anchor" href="#-%e5%9c%a8-producer-%e7%9a%84-value-%e6%94%be%e4%b8%8a-carrier-%e7%9a%84%e8%b3%87%e8%a8%8a%e8%a8%98%e5%be%97%e8%a6%81%e8%bd%89%e7%82%ba%e5%ad%97%e4%b8%b2" aria-label="這個段落的連結"></a></h3>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#ff7b72">await</span> producer.connect()
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">await</span> producer
</span></span><span style="display:flex;"><span>  .send({
</span></span><span style="display:flex;"><span>    topic,
</span></span><span style="display:flex;"><span>    messages<span style="color:#ff7b72;font-weight:bold">:</span> Array({
</span></span><span style="display:flex;"><span>      key<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">`report`</span>,
</span></span><span style="display:flex;"><span>      value<span style="color:#ff7b72;font-weight:bold">:</span> JSON.stringify(carrier)
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>  });
</span></span></code></pre></div><h3 id="-從-consumer-收到的-message-中取出-carrier-的資訊">從 Consumer 收到的 message 中取出 carrier 的資訊<a class="h-anchor" href="#-%e5%be%9e-consumer-%e6%94%b6%e5%88%b0%e7%9a%84-message-%e4%b8%ad%e5%8f%96%e5%87%ba-carrier-%e7%9a%84%e8%b3%87%e8%a8%8a" aria-label="這個段落的連結"></a></h3>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#ff7b72">await</span> consumer.connect()
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">await</span> consumer.subscribe({ topic, fromBeginning<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#79c0ff">true</span> })
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">await</span> consumer.run({
</span></span><span style="display:flex;"><span>  eachMessage<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#ff7b72">async</span> ({ topic, partition, message }) =&gt; {
</span></span><span style="display:flex;"><span>    console.log(<span style="color:#a5d6ff">&#34;topic:&#34;</span> <span style="color:#ff7b72;font-weight:bold">+</span> topic <span style="color:#ff7b72;font-weight:bold">+</span> <span style="color:#a5d6ff">&#34;  message:&#34;</span> <span style="color:#ff7b72;font-weight:bold">+</span> message.value)
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> carrier <span style="color:#ff7b72;font-weight:bold">=</span> JSON.parse(message.value)
</span></span><span style="display:flex;"><span>  },
</span></span><span style="display:flex;"><span>})
</span></span></code></pre></div><hr>
<h2 id="三在-local-建立-nodejs-專案來達成目標">三、在 Local 建立 Node.js 專案來達成目標<a class="h-anchor" href="#%e4%b8%89%e5%9c%a8-local-%e5%bb%ba%e7%ab%8b-nodejs-%e5%b0%88%e6%a1%88%e4%be%86%e9%81%94%e6%88%90%e7%9b%ae%e6%a8%99" aria-label="這個段落的連結"></a></h2>
<p>專案的目標就如同標題，我們要透過 Jaeger with OpenTelemetry 追蹤 Kafka 資料傳遞路徑與運行狀況。</p>
<p><strong>STEP 1</strong>：建立專案、安裝必要套件。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>mkdir -p kafka-jaeger-demo
</span></span><span style="display:flex;"><span>cd kafka-jaeger-demo
</span></span><span style="display:flex;"><span>mkdir -p deploy/kafkaCluster/kraft
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>npm init -f
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>npm install --save @opentelemetry/api
</span></span><span style="display:flex;"><span>npm install --save @opentelemetry/resources
</span></span><span style="display:flex;"><span>npm install --save @opentelemetry/semantic-conventions
</span></span><span style="display:flex;"><span>npm install --save @opentelemetry/sdk-trace-base
</span></span><span style="display:flex;"><span>npm install --save @opentelemetry/exporter-jaeger
</span></span><span style="display:flex;"><span>npm install --save axios
</span></span><span style="display:flex;"><span>npm install --save express
</span></span><span style="display:flex;"><span>npm install --save kafkajs
</span></span></code></pre></div><p><strong>SETP 2</strong>：新增「docker-compose.yml」貼上如下程式，並將 docker 啟動。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#7ee787">version</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#39;2.0&#39;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#7ee787">services</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">jaeger</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">image</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">jaegertracing/all-in-one:1.38</span><span style="color:#6e7681"> </span><span style="color:#8b949e;font-style:italic"># 當時用 1.39 或 latest 有出現一些 bug，故選用此版本</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">environment</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">COLLECTOR_ZIPKIN_HOST_PORT=:9411</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">ports</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">&#34;5775:5775/udp&#34;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">&#34;6831:6831/udp&#34;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">&#34;6832:6832/udp&#34;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">&#34;5778:5778&#34;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">&#34;16686:16686&#34;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">&#34;14250:14250&#34;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">&#34;14268:14268&#34;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">&#34;14269:14269&#34;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">&#34;9411:9411&#34;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">kafka</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">image</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#39;bitnami/kafka:latest&#39;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">ports</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">&#39;9092:9092&#39;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">environment</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">KAFKA_ENABLE_KRAFT=yes</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">KAFKA_CFG_PROCESS_ROLES=broker,controller</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://127.0.0.1:9092</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">KAFKA_BROKER_ID=1</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=1@127.0.0.1:9093</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">ALLOW_PLAINTEXT_LISTENER=yes</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">volumes</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#8b949e;font-style:italic"># 這是設定資料要存在哪路的路徑</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">./deploy/kafkaCluster/kraft:/bitnami/kafka:rw</span><span style="color:#6e7681">
</span></span></span></code></pre></div><p>接著輸入指令拉下並啟動 Kafka &amp; Jaeger 環境。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker-compose up -d
</span></span></code></pre></div><p><strong>STEP 3</strong>：新增「tracing.js」貼上如下程式，基本上就是「Opentelemetry」的基礎運用。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#a5d6ff">&#39;use strict&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> opentelemetry <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;@opentelemetry/api&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> { Resource } <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;@opentelemetry/resources&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> { SemanticResourceAttributes } <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;@opentelemetry/semantic-conventions&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> { BasicTracerProvider, BatchSpanProcessor } <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;@opentelemetry/sdk-trace-base&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> { JaegerExporter } <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;@opentelemetry/exporter-jaeger&#39;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>module.exports <span style="color:#ff7b72;font-weight:bold">=</span> (serviceName) =&gt; {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> provider <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">new</span> BasicTracerProvider({
</span></span><span style="display:flex;"><span>        resource<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#ff7b72">new</span> Resource({
</span></span><span style="display:flex;"><span>            [SemanticResourceAttributes.SERVICE_NAME]<span style="color:#ff7b72;font-weight:bold">:</span> serviceName,
</span></span><span style="display:flex;"><span>        })
</span></span><span style="display:flex;"><span>    });
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">let</span> exporter;
</span></span><span style="display:flex;"><span>    exporter <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">new</span> JaegerExporter({ endpoint<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;http://127.0.0.1:14268/api/traces&#34;</span> });
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    provider.addSpanProcessor(<span style="color:#ff7b72">new</span> BatchSpanProcessor(exporter));
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// Initialize the OpenTelemetry APIs to use the NodeTracerProvider bindings
</span></span></span><span style="display:flex;"><span>    provider.register();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">return</span> opentelemetry.trace.getTracer(<span style="color:#a5d6ff">&#39;my-service-tracer&#39;</span>);
</span></span><span style="display:flex;"><span>};
</span></span></code></pre></div><p><strong>STEP 4</strong>：新增「service.js」貼上如下程式；我們在這裡建立供使用者呼叫的 API（/api/report），當 API 被呼叫後會開始追蹤，並透過 Producer 將資料塞入 Kafka。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#ff7b72">const</span> api <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;@opentelemetry/api&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> tracer <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;./tracing&#39;</span>)(<span style="color:#a5d6ff">&#39;service&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> express <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#34;express&#34;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> PORT <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#34;8080&#34;</span>;
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> app <span style="color:#ff7b72;font-weight:bold">=</span> express();
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> { Kafka, logLevel } <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;kafkajs&#39;</span>)
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> kafka <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">new</span> Kafka({
</span></span><span style="display:flex;"><span>  logLevel<span style="color:#ff7b72;font-weight:bold">:</span> logLevel.ERROR,
</span></span><span style="display:flex;"><span>  brokers<span style="color:#ff7b72;font-weight:bold">:</span> [<span style="color:#a5d6ff">`localhost:9092`</span>],
</span></span><span style="display:flex;"><span>  clientId<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;example-producer&#39;</span>,
</span></span><span style="display:flex;"><span>})
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> producer <span style="color:#ff7b72;font-weight:bold">=</span> kafka.producer()
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> topic <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#39;report&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> { W3CTraceContextPropagator } <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;@opentelemetry/core&#39;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">async</span> <span style="color:#ff7b72">function</span> report(parentSpan) {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">try</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> ctx <span style="color:#ff7b72;font-weight:bold">=</span> api.trace.setSpan(
</span></span><span style="display:flex;"><span>      api.context.active(),
</span></span><span style="display:flex;"><span>      parentSpan
</span></span><span style="display:flex;"><span>    );
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">let</span> span <span style="color:#ff7b72;font-weight:bold">=</span> tracer.startSpan(<span style="color:#a5d6ff">&#39;report_producer&#39;</span>, <span style="color:#79c0ff">undefined</span>, ctx);
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> carrier <span style="color:#ff7b72;font-weight:bold">=</span> {};
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> propagator <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">new</span> W3CTraceContextPropagator();
</span></span><span style="display:flex;"><span>    propagator.inject(
</span></span><span style="display:flex;"><span>      api.trace.setSpanContext(api.context.active(), span.spanContext()),
</span></span><span style="display:flex;"><span>      carrier,
</span></span><span style="display:flex;"><span>      api.defaultTextMapSetter,
</span></span><span style="display:flex;"><span>    );
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">await</span> producer.connect()
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">await</span> producer
</span></span><span style="display:flex;"><span>      .send({
</span></span><span style="display:flex;"><span>        topic,
</span></span><span style="display:flex;"><span>        messages<span style="color:#ff7b72;font-weight:bold">:</span> Array({
</span></span><span style="display:flex;"><span>          key<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">`report`</span>,
</span></span><span style="display:flex;"><span>          value<span style="color:#ff7b72;font-weight:bold">:</span> JSON.stringify(carrier)
</span></span><span style="display:flex;"><span>        })
</span></span><span style="display:flex;"><span>      });
</span></span><span style="display:flex;"><span>    span.end()
</span></span><span style="display:flex;"><span>  } <span style="color:#ff7b72">catch</span> (e) {
</span></span><span style="display:flex;"><span>    console.log(e)
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">throw</span> e
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>app.get(<span style="color:#a5d6ff">&#34;/api/report&#34;</span>, <span style="color:#ff7b72">async</span> (req, res) =&gt; {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">try</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">let</span> span <span style="color:#ff7b72;font-weight:bold">=</span> tracer.startSpan(<span style="color:#a5d6ff">&#39;report_request&#39;</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">let</span> result <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> report(span)
</span></span><span style="display:flex;"><span>    span.end()
</span></span><span style="display:flex;"><span>    res.send(result);
</span></span><span style="display:flex;"><span>  } <span style="color:#ff7b72">catch</span> (e) {
</span></span><span style="display:flex;"><span>    res.send(e);
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>app.listen(parseInt(PORT, <span style="color:#a5d6ff">10</span>), () =&gt; {
</span></span><span style="display:flex;"><span>  console.log(<span style="color:#a5d6ff">`Service listening for requests on http://localhost:</span><span style="color:#a5d6ff">${</span>PORT<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>);
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><p><strong>STEP 5</strong>：新增「consumer.js」貼上如下程式；專門監聽「report」這個 Topic，在收到 message 後會開始後續的處理（有刻意設計一個執行錯誤的結果）。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#ff7b72">const</span> api <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;@opentelemetry/api&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> tracer <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;./tracing&#39;</span>)(<span style="color:#a5d6ff">&#39;consumer&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> { W3CTraceContextPropagator } <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;@opentelemetry/core&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> { Kafka, logLevel } <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;kafkajs&#39;</span>)
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> kafka <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">new</span> Kafka({
</span></span><span style="display:flex;"><span>  logLevel<span style="color:#ff7b72;font-weight:bold">:</span> logLevel.INFO,
</span></span><span style="display:flex;"><span>  brokers<span style="color:#ff7b72;font-weight:bold">:</span> [<span style="color:#a5d6ff">`localhost:9092`</span>],
</span></span><span style="display:flex;"><span>  clientId<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;example-consumer&#39;</span>,
</span></span><span style="display:flex;"><span>})
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> topic <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#39;report&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> consumer <span style="color:#ff7b72;font-weight:bold">=</span> kafka.consumer({ groupId<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;test-group&#39;</span> })
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> run <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">async</span> () =&gt; {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">await</span> consumer.connect()
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">await</span> consumer.subscribe({ topic, fromBeginning<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#79c0ff">true</span> })
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">await</span> consumer.run({
</span></span><span style="display:flex;"><span>    eachMessage<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#ff7b72">async</span> ({ topic, partition, message }) =&gt; {
</span></span><span style="display:flex;"><span>      console.log(<span style="color:#a5d6ff">&#34;topic:&#34;</span> <span style="color:#ff7b72;font-weight:bold">+</span> topic <span style="color:#ff7b72;font-weight:bold">+</span> <span style="color:#a5d6ff">&#34;  message:&#34;</span> <span style="color:#ff7b72;font-weight:bold">+</span> message.value)
</span></span><span style="display:flex;"><span>      <span style="color:#ff7b72">const</span> carrier <span style="color:#ff7b72;font-weight:bold">=</span> JSON.parse(message.value)
</span></span><span style="display:flex;"><span>      <span style="color:#ff7b72">const</span> propagator <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">new</span> W3CTraceContextPropagator();
</span></span><span style="display:flex;"><span>      <span style="color:#ff7b72">const</span> parentCtx <span style="color:#ff7b72;font-weight:bold">=</span> propagator.extract(api.context.active(), carrier, api.defaultTextMapGetter);
</span></span><span style="display:flex;"><span>      <span style="color:#ff7b72">if</span> (topic <span style="color:#ff7b72;font-weight:bold">===</span> <span style="color:#a5d6ff">&#34;report&#34;</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">const</span> span <span style="color:#ff7b72;font-weight:bold">=</span> tracer.startSpan(<span style="color:#a5d6ff">`report_consumer`</span>, <span style="color:#79c0ff">undefined</span>, parentCtx);
</span></span><span style="display:flex;"><span>        report(span)
</span></span><span style="display:flex;"><span>        span.end()
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>  })
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">function</span> report(parentSpan) {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">const</span> parentCtx <span style="color:#ff7b72;font-weight:bold">=</span> api.trace.setSpan(
</span></span><span style="display:flex;"><span>    api.context.active(),
</span></span><span style="display:flex;"><span>    parentSpan
</span></span><span style="display:flex;"><span>  );
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">let</span> span <span style="color:#ff7b72;font-weight:bold">=</span> tracer.startSpan(<span style="color:#a5d6ff">&#39;handle_report&#39;</span>, <span style="color:#79c0ff">undefined</span>, parentCtx);
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">for</span> (<span style="color:#ff7b72">let</span> i <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">0</span>; i <span style="color:#ff7b72;font-weight:bold">&lt;</span> <span style="color:#a5d6ff">10</span>; i<span style="color:#ff7b72;font-weight:bold">++</span>) {
</span></span><span style="display:flex;"><span>    doWork(span, i);
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>  span.end();
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">function</span> doWork(parentSpan, i) {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">const</span> parentCtx <span style="color:#ff7b72;font-weight:bold">=</span> api.trace.setSpan(
</span></span><span style="display:flex;"><span>    api.context.active(),
</span></span><span style="display:flex;"><span>    parentSpan
</span></span><span style="display:flex;"><span>  );
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">let</span> span <span style="color:#ff7b72;font-weight:bold">=</span> tracer.startSpan(<span style="color:#a5d6ff">`doWork:</span><span style="color:#a5d6ff">${</span>i<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>, <span style="color:#79c0ff">undefined</span>, parentCtx);
</span></span><span style="display:flex;"><span>  <span style="color:#8b949e;font-style:italic">// 讓每個執行的時間不一樣
</span></span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">for</span> (<span style="color:#ff7b72">let</span> i <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">0</span>; i <span style="color:#ff7b72;font-weight:bold">&lt;=</span> Math.floor(Math.random() <span style="color:#ff7b72;font-weight:bold">*</span> <span style="color:#a5d6ff">40000000</span>); i<span style="color:#ff7b72;font-weight:bold">++</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// empty
</span></span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">if</span> (i <span style="color:#ff7b72;font-weight:bold">===</span> <span style="color:#a5d6ff">5</span>) { <span style="color:#8b949e;font-style:italic">// 產生錯誤訊息
</span></span></span><span style="display:flex;"><span>    span.recordException(<span style="color:#ff7b72">new</span> Error(<span style="color:#a5d6ff">`doWork:</span><span style="color:#a5d6ff">${</span>i<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff"> error`</span>));
</span></span><span style="display:flex;"><span>    span.setStatus({ code<span style="color:#ff7b72;font-weight:bold">:</span> api.SpanStatusCode.ERROR, message<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;Something wrong!&#34;</span> });
</span></span><span style="display:flex;"><span>    span.addEvent(<span style="color:#a5d6ff">&#39;log&#39;</span>, {
</span></span><span style="display:flex;"><span>      <span style="color:#a5d6ff">&#39;log.severity&#39;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;error&#39;</span>,
</span></span><span style="display:flex;"><span>      <span style="color:#a5d6ff">&#39;log.message&#39;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;doWork:${i} error&#39;</span>
</span></span><span style="display:flex;"><span>    });
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>  span.end();
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>run().<span style="color:#ff7b72">catch</span>(e =&gt; console.error(<span style="color:#a5d6ff">`[example/consumer] </span><span style="color:#a5d6ff">${</span>e.message<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>, e))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> errorTypes <span style="color:#ff7b72;font-weight:bold">=</span> [<span style="color:#a5d6ff">&#39;unhandledRejection&#39;</span>, <span style="color:#a5d6ff">&#39;uncaughtException&#39;</span>]
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> signalTraps <span style="color:#ff7b72;font-weight:bold">=</span> [<span style="color:#a5d6ff">&#39;SIGTERM&#39;</span>, <span style="color:#a5d6ff">&#39;SIGINT&#39;</span>, <span style="color:#a5d6ff">&#39;SIGUSR2&#39;</span>]
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// 收到無法處理錯誤時的處理
</span></span></span><span style="display:flex;"><span>errorTypes.forEach(type =&gt; {
</span></span><span style="display:flex;"><span>  process.on(type, <span style="color:#ff7b72">async</span> e =&gt; {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">try</span> {
</span></span><span style="display:flex;"><span>      console.log(<span style="color:#a5d6ff">`process.on </span><span style="color:#a5d6ff">${</span>type<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>)
</span></span><span style="display:flex;"><span>      console.error(e)
</span></span><span style="display:flex;"><span>      <span style="color:#ff7b72">await</span> consumer.disconnect()
</span></span><span style="display:flex;"><span>      process.exit(<span style="color:#a5d6ff">0</span>)
</span></span><span style="display:flex;"><span>    } <span style="color:#ff7b72">catch</span> (_) {
</span></span><span style="display:flex;"><span>      process.exit(<span style="color:#a5d6ff">1</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  })
</span></span><span style="display:flex;"><span>})
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// 收到中斷訊號時的處理
</span></span></span><span style="display:flex;"><span>signalTraps.forEach(type =&gt; {
</span></span><span style="display:flex;"><span>  process.once(type, <span style="color:#ff7b72">async</span> () =&gt; {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">try</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#ff7b72">await</span> consumer.disconnect()
</span></span><span style="display:flex;"><span>    } <span style="color:#ff7b72">finally</span> {
</span></span><span style="display:flex;"><span>      process.kill(process.pid, type)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  })
</span></span><span style="display:flex;"><span>})
</span></span></code></pre></div><hr>
<h2 id="四啟動專案透過-jaeger-ui-確認-trace-路徑是否符合預期">四、啟動專案，透過 Jaeger UI 確認 Trace 路徑是否符合預期<a class="h-anchor" href="#%e5%9b%9b%e5%95%9f%e5%8b%95%e5%b0%88%e6%a1%88%e9%80%8f%e9%81%8e-jaeger-ui-%e7%a2%ba%e8%aa%8d-trace-%e8%b7%af%e5%be%91%e6%98%af%e5%90%a6%e7%ac%a6%e5%90%88%e9%a0%90%e6%9c%9f" aria-label="這個段落的連結"></a></h2>
<blockquote>
<p>建議使用有分頁功能的終端機（如 iTerm2），這樣比較好觀察與操作。</p>
</blockquote>
<p><strong>STEP 1</strong>：在專案根目錄下輸入 <code>node service.js</code> 啟動 Service。</p>
<p><strong>STEP 2</strong>：在終端機貼上指令 <code>curl http://localhost:8080/api/report</code> 來呼叫 API。</p>
<p><strong>STEP 3</strong>：在專案根目錄下輸入 <code>node consumer.js</code> 啟動監聽模式；這邊提醒一下，如果監聽不存在的 Topic 會報錯。</p>
<p><img src="https://deanlin.net/blog/202211/trace-kafka-with-jaeger/images/trace-kafka-with-jaeger-02.webp" width="1400" height="1058" alt="四、啟動專案，透過 Jaeger UI 確認 Trace 路徑是否符合預期（圖 2）" loading="lazy" decoding="async"></p>
<p><strong>STEP 4</strong>：到 Jaeger UI 上（<a href="http://localhost:16686/search">http://localhost:16686/search</a>）確認 Trace 路徑是否符合預期。</p>
<figure class="figure"><img src="https://deanlin.net/blog/202211/trace-kafka-with-jaeger/images/trace-kafka-with-jaeger-03.webp" width="1400" height="728" alt="上面是呼叫 API 時留下的紀錄" loading="lazy" decoding="async">
  <figcaption>上面是呼叫 API 時留下的紀錄</figcaption>
</figure>

<p>下圖「report_producer」跟「report_consumer」之間的空白，就是資料在 Kafka 轉運站傳遞的時間。</p>
<figure class="figure"><img src="https://deanlin.net/blog/202211/trace-kafka-with-jaeger/images/trace-kafka-with-jaeger-04.webp" width="1400" height="596" alt="我們可以看到 trace 中每個 span 執行的狀況（這邊能看到錯誤是出現在 dowork5 的 span 上）" loading="lazy" decoding="async">
  <figcaption>我們可以看到 trace 中每個 span 執行的狀況（這邊能看到錯誤是出現在 dowork5 的 span 上）</figcaption>
</figure>

<blockquote class="pullquote"><p>範例到這裡告一段落，希望文中 Jaeger 與 Kafka 的組合應用，有幫讀者打開不一樣的思路。</p>
</blockquote>
]]></content:encoded>
    </item>
    <item>
      <title>手把手帶你建立 Node.js 專案 ＆ Kafka 環境，透過實作了解 Kafka 的運行邏輯</title>
      <link>https://deanlin.net/blog/202211/nodejs-kafka-hands-on/</link>
      <pubDate>Sun, 20 Nov 2022 22:46:53 +0800</pubDate>
      <guid>https://deanlin.net/blog/202211/nodejs-kafka-hands-on/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>本篇文章將會帶大家一步步建立 Node.js 專案 ＆ Kafka 環境，透過具體實踐來了解 Kafka 的運行邏輯。</description>
      <media:content url="https://deanlin.net/blog/202211/nodejs-kafka-hands-on/images/nodejs-kafka-hands-on-og_hu_c30b47177bb01ebe.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<p>在<a href="https://deanlin.net/blog/202211/what-is-kafka/">上篇文章</a>中，筆者帶大家了解 Kafka 這門技術的使用情境、優點，以及它具體是如何解決問題的。</p>
<p>而本篇文章將會帶大家一步步建立 Node.js 專案 ＆ Kafka 環境，透過具體實踐來了解 Kafka 的運行邏輯。</p>
<p><img src="https://deanlin.net/blog/202211/nodejs-kafka-hands-on/images/nodejs-kafka-hands-on-01.webp" width="1400" height="739" alt="手把手帶你建立 Node.js 專案 ＆ Kafka 環境，透過實作了解 Kafka 的運行邏輯（圖 1）" fetchpriority="high" decoding="async"></p>
<pre tabindex="0"><code>大綱

ㄧ、手把手帶你建立 Node.js 專案 ＆ Kafka 環境
二、啟動專案，了解 Kafka 的運行邏輯
  ➤ 1 個 Producer VS 1 個 Consumer
  ➤ Kafka 持久化驗證
  ➤ 1 個 Partition 的數據只允許 Consumer Group 中的某個 Consumer 消費
  ➤ 2 個 Partition VS 2 個 Consumer
</code></pre><h2 id="ㄧ手把手帶你建立-nodejs-專案--kafka-環境">ㄧ、手把手帶你建立 Node.js 專案 ＆ Kafka 環境<a class="h-anchor" href="#%e3%84%a7%e6%89%8b%e6%8a%8a%e6%89%8b%e5%b8%b6%e4%bd%a0%e5%bb%ba%e7%ab%8b-nodejs-%e5%b0%88%e6%a1%88--kafka-%e7%92%b0%e5%a2%83" aria-label="這個段落的連結"></a></h2>
<p><a href="https://deanlin.net/blog/202211/what-is-kafka/">上一篇文章</a>講了這麼多理論，下面就建立一個 Node.js 專案，來實際了解 Kafka 的運作邏輯吧！</p>
<blockquote>
<p>如果懶得按照步驟執行，可以直接到<a href="https://github.com/dean9703111/kafka-demo">筆者的 Github</a> Clone 一份下來。<br>筆者 local 的 Node.js 版本為 v18.12.0。</p>
</blockquote>
<p><strong>SETP 1</strong>：建立專案、安裝 kafkajs 套件。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>npm init -f
</span></span><span style="display:flex;"><span>npm install --save kafkajs
</span></span></code></pre></div><p><strong>SETP 2</strong>：建立儲存 Kafka 數據的資料夾（這是下一步「docker-compose.yml」裏面「volumes」的路徑）</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>mkdir -p deploy/kafkaCluster/kraft
</span></span></code></pre></div><p><strong>SETP 3</strong>：新增「docker-compose.yml」貼上如下程式。</p>
<blockquote>
<p>這裡筆者選用不依賴 zookeeper 的 Kafka 版本（KRaft），過去主流會使用 zookeeper 來保存消息，但 Kafka 官方表示在不久後就不再支援 zookeeper，故筆者做此選擇，至於 KRaft 本身有哪些優勢，感興趣的朋友可以參考<a href="https://www.cnblogs.com/smartloli/p/16430041.html">連結</a>。</p>
</blockquote>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#7ee787">version</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#34;3&#34;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#7ee787">services</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">kafka</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">image</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#39;bitnami/kafka:latest&#39;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">ports</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">&#39;9092:9092&#39;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">environment</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">KAFKA_ENABLE_KRAFT=yes</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">KAFKA_CFG_PROCESS_ROLES=broker,controller</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">KAFKA_CFG_CONTROLLER_LISTENER_NAMES=CONTROLLER</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">KAFKA_CFG_LISTENERS=PLAINTEXT://:9092,CONTROLLER://:9093</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">KAFKA_CFG_LISTENER_SECURITY_PROTOCOL_MAP=CONTROLLER:PLAINTEXT,PLAINTEXT:PLAINTEXT</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">KAFKA_CFG_ADVERTISED_LISTENERS=PLAINTEXT://127.0.0.1:9092</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">KAFKA_BROKER_ID=1</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">KAFKA_CFG_NODE_ID=1</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">KAFKA_CFG_CONTROLLER_QUORUM_VOTERS=1@127.0.0.1:9093</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">ALLOW_PLAINTEXT_LISTENER=yes</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">volumes</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#8b949e;font-style:italic"># 這是設定資料要存在哪路的路徑</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">./deploy/kafkaCluster/kraft:/bitnami/kafka:rw</span><span style="color:#6e7681">
</span></span></span></code></pre></div><p><strong>SETP 4</strong>：輸入以下指令把 Kafka 的 Docker 拉下來並執行，你可以把這個 Docker 想像成 Kafka Cluster 中的 Broker。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker-compose up -d
</span></span></code></pre></div><p><strong>SETP 5</strong>：新增「producer.js」貼上如下程式，我們會透過它每隔 2 秒產生消息到指定的 topic。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#ff7b72">const</span> { Kafka, CompressionTypes, logLevel } <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;kafkajs&#39;</span>)
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> kafka <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">new</span> Kafka({
</span></span><span style="display:flex;"><span>  logLevel<span style="color:#ff7b72;font-weight:bold">:</span> logLevel.ERROR,
</span></span><span style="display:flex;"><span>  brokers<span style="color:#ff7b72;font-weight:bold">:</span> [<span style="color:#a5d6ff">`localhost:9092`</span>],
</span></span><span style="display:flex;"><span>  clientId<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;example-producer&#39;</span>,
</span></span><span style="display:flex;"><span>})
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> topic <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#39;topic-test&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> numPartitions <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">2</span>;
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> crateTopic <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">async</span> (topic) =&gt; {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">const</span> admin <span style="color:#ff7b72;font-weight:bold">=</span> kafka.admin();
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">await</span> admin.connect();
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">const</span> topics <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> admin.listTopics();
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">if</span> (<span style="color:#ff7b72;font-weight:bold">!</span>topics.includes(topic)) {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">await</span> admin.createTopics({
</span></span><span style="display:flex;"><span>      topics<span style="color:#ff7b72;font-weight:bold">:</span> [{
</span></span><span style="display:flex;"><span>        topic<span style="color:#ff7b72;font-weight:bold">:</span> topic,
</span></span><span style="display:flex;"><span>        numPartitions<span style="color:#ff7b72;font-weight:bold">:</span> numPartitions
</span></span><span style="display:flex;"><span>      }],
</span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> fetchTopicOffsets <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> admin.fetchTopicOffsets(<span style="color:#a5d6ff">&#39;topic-test&#39;</span>)
</span></span><span style="display:flex;"><span>    console.log(<span style="color:#a5d6ff">`Crate topic:</span><span style="color:#a5d6ff">${</span>topic<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff"> successful!`</span>)
</span></span><span style="display:flex;"><span>    console.log(fetchTopicOffsets)
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">await</span> admin.disconnect();
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> producer <span style="color:#ff7b72;font-weight:bold">=</span> kafka.producer()
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> getRandomNumber <span style="color:#ff7b72;font-weight:bold">=</span> () =&gt; Math.round(Math.random(<span style="color:#a5d6ff">10</span>) <span style="color:#ff7b72;font-weight:bold">*</span> <span style="color:#a5d6ff">1000</span>)
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> createMessage <span style="color:#ff7b72;font-weight:bold">=</span> (num, partition <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">0</span>) =&gt; ({
</span></span><span style="display:flex;"><span>  key<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">`key-</span><span style="color:#a5d6ff">${</span>num<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>,
</span></span><span style="display:flex;"><span>  value<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">`value-</span><span style="color:#a5d6ff">${</span>num<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">-</span><span style="color:#a5d6ff">${</span><span style="color:#ff7b72">new</span> Date().toISOString()<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>,
</span></span><span style="display:flex;"><span>  partition<span style="color:#ff7b72;font-weight:bold">:</span> partition
</span></span><span style="display:flex;"><span>})
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> sendMessage <span style="color:#ff7b72;font-weight:bold">=</span> () =&gt; {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">return</span> producer
</span></span><span style="display:flex;"><span>    .send({
</span></span><span style="display:flex;"><span>      topic,
</span></span><span style="display:flex;"><span>      compression<span style="color:#ff7b72;font-weight:bold">:</span> CompressionTypes.GZIP,
</span></span><span style="display:flex;"><span>      messages<span style="color:#ff7b72;font-weight:bold">:</span> Array(createMessage(getRandomNumber()))
</span></span><span style="display:flex;"><span>      <span style="color:#8b949e;font-style:italic">// 用來測試 kafka 多個 Partition 與 Consumer 的關係
</span></span></span><span style="display:flex;"><span>      <span style="color:#8b949e;font-style:italic">// messages: Array(createMessage(getRandomNumber(), 0), createMessage(getRandomNumber(), 1))
</span></span></span><span style="display:flex;"><span>    })
</span></span><span style="display:flex;"><span>    .then(console.log)
</span></span><span style="display:flex;"><span>    .<span style="color:#ff7b72">catch</span>(e =&gt; console.error(<span style="color:#a5d6ff">`[example/producer] </span><span style="color:#a5d6ff">${</span>e.message<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>, e))
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> run <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">async</span> () =&gt; {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">await</span> crateTopic(topic);
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">await</span> producer.connect()
</span></span><span style="display:flex;"><span>  setInterval(sendMessage, <span style="color:#a5d6ff">2000</span>)
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>run().<span style="color:#ff7b72">catch</span>(e =&gt; console.error(<span style="color:#a5d6ff">`[example/producer] </span><span style="color:#a5d6ff">${</span>e.message<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>, e))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> errorTypes <span style="color:#ff7b72;font-weight:bold">=</span> [<span style="color:#a5d6ff">&#39;unhandledRejection&#39;</span>, <span style="color:#a5d6ff">&#39;uncaughtException&#39;</span>]
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> signalTraps <span style="color:#ff7b72;font-weight:bold">=</span> [<span style="color:#a5d6ff">&#39;SIGTERM&#39;</span>, <span style="color:#a5d6ff">&#39;SIGINT&#39;</span>, <span style="color:#a5d6ff">&#39;SIGUSR2&#39;</span>]
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// 收到無法處理錯誤時的處理
</span></span></span><span style="display:flex;"><span>errorTypes.forEach(type =&gt; {
</span></span><span style="display:flex;"><span>  process.on(type, <span style="color:#ff7b72">async</span> e =&gt; {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">try</span> {
</span></span><span style="display:flex;"><span>      console.log(<span style="color:#a5d6ff">`process.on </span><span style="color:#a5d6ff">${</span>type<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>)
</span></span><span style="display:flex;"><span>      console.error(e)
</span></span><span style="display:flex;"><span>      <span style="color:#ff7b72">await</span> consumer.disconnect()
</span></span><span style="display:flex;"><span>      process.exit(<span style="color:#a5d6ff">0</span>)
</span></span><span style="display:flex;"><span>    } <span style="color:#ff7b72">catch</span> (_) {
</span></span><span style="display:flex;"><span>      process.exit(<span style="color:#a5d6ff">1</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  })
</span></span><span style="display:flex;"><span>})
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// 收到中斷訊號時的處理
</span></span></span><span style="display:flex;"><span>signalTraps.forEach(type =&gt; {
</span></span><span style="display:flex;"><span>  process.once(type, <span style="color:#ff7b72">async</span> () =&gt; {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">try</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#ff7b72">await</span> consumer.disconnect()
</span></span><span style="display:flex;"><span>    } <span style="color:#ff7b72">finally</span> {
</span></span><span style="display:flex;"><span>      process.kill(process.pid, type)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  })
</span></span><span style="display:flex;"><span>})
</span></span></code></pre></div><p><strong>SETP 6</strong>：新增「consumer.js」貼上如下程式，我們透過它模擬消費者消費特定 topic 的消息。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#ff7b72">const</span> { Kafka, logLevel } <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;Kafkajs&#39;</span>)
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> Kafka <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">new</span> Kafka({
</span></span><span style="display:flex;"><span>  logLevel<span style="color:#ff7b72;font-weight:bold">:</span> logLevel.INFO,
</span></span><span style="display:flex;"><span>  brokers<span style="color:#ff7b72;font-weight:bold">:</span> [<span style="color:#a5d6ff">`localhost:9092`</span>],
</span></span><span style="display:flex;"><span>  clientId<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;example-consumer&#39;</span>,
</span></span><span style="display:flex;"><span>})
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> topic <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#39;topic-test&#39;</span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> consumer <span style="color:#ff7b72;font-weight:bold">=</span> Kafka.consumer({ groupId<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;test-group&#39;</span> })
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// 用來測試 Kafka 持久化
</span></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// const consumer = Kafka.consumer({ groupId: &#39;test-group2&#39; })
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> run <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">async</span> () =&gt; {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">await</span> consumer.connect()
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">await</span> consumer.subscribe({ topic, fromBeginning<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#79c0ff">true</span> })
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">await</span> consumer.run({
</span></span><span style="display:flex;"><span>    eachMessage<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#ff7b72">async</span> ({ topic, partition, message }) =&gt; {
</span></span><span style="display:flex;"><span>      <span style="color:#ff7b72">const</span> prefix <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">`</span><span style="color:#a5d6ff">${</span>topic<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">[</span><span style="color:#a5d6ff">${</span>partition<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff"> | </span><span style="color:#a5d6ff">${</span>message.offset<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">] / </span><span style="color:#a5d6ff">${</span>message.timestamp<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>
</span></span><span style="display:flex;"><span>      console.log(<span style="color:#a5d6ff">`- </span><span style="color:#a5d6ff">${</span>prefix<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff"> </span><span style="color:#a5d6ff">${</span>message.key<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">#</span><span style="color:#a5d6ff">${</span>message.value<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>)
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>  })
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>run().<span style="color:#ff7b72">catch</span>(e =&gt; console.error(<span style="color:#a5d6ff">`[example/consumer] </span><span style="color:#a5d6ff">${</span>e.message<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>, e))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> errorTypes <span style="color:#ff7b72;font-weight:bold">=</span> [<span style="color:#a5d6ff">&#39;unhandledRejection&#39;</span>, <span style="color:#a5d6ff">&#39;uncaughtException&#39;</span>]
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> signalTraps <span style="color:#ff7b72;font-weight:bold">=</span> [<span style="color:#a5d6ff">&#39;SIGTERM&#39;</span>, <span style="color:#a5d6ff">&#39;SIGINT&#39;</span>, <span style="color:#a5d6ff">&#39;SIGUSR2&#39;</span>]
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// 收到無法處理錯誤時的處理
</span></span></span><span style="display:flex;"><span>errorTypes.forEach(type =&gt; {
</span></span><span style="display:flex;"><span>  process.on(type, <span style="color:#ff7b72">async</span> e =&gt; {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">try</span> {
</span></span><span style="display:flex;"><span>      console.log(<span style="color:#a5d6ff">`process.on </span><span style="color:#a5d6ff">${</span>type<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>)
</span></span><span style="display:flex;"><span>      console.error(e)
</span></span><span style="display:flex;"><span>      <span style="color:#ff7b72">await</span> consumer.disconnect()
</span></span><span style="display:flex;"><span>      process.exit(<span style="color:#a5d6ff">0</span>)
</span></span><span style="display:flex;"><span>    } <span style="color:#ff7b72">catch</span> (_) {
</span></span><span style="display:flex;"><span>      process.exit(<span style="color:#a5d6ff">1</span>)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  })
</span></span><span style="display:flex;"><span>})
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// 收到中斷訊號時的處理
</span></span></span><span style="display:flex;"><span>signalTraps.forEach(type =&gt; {
</span></span><span style="display:flex;"><span>  process.once(type, <span style="color:#ff7b72">async</span> () =&gt; {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">try</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#ff7b72">await</span> consumer.disconnect()
</span></span><span style="display:flex;"><span>    } <span style="color:#ff7b72">finally</span> {
</span></span><span style="display:flex;"><span>      process.kill(process.pid, type)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>  })
</span></span><span style="display:flex;"><span>})
</span></span></code></pre></div><hr>
<h2 id="二啟動專案了解-kafka-的運行邏輯">二、啟動專案，了解 Kafka 的運行邏輯<a class="h-anchor" href="#%e4%ba%8c%e5%95%9f%e5%8b%95%e5%b0%88%e6%a1%88%e4%ba%86%e8%a7%a3-kafka-%e7%9a%84%e9%81%8b%e8%a1%8c%e9%82%8f%e8%bc%af" aria-label="這個段落的連結"></a></h2>
<blockquote>
<p>建議使用有分頁功能的終端機（如 iTerm2），這樣比較好觀察與操作。</p>
</blockquote>
<h3 class="h-nomark" id="-1-個-producer-vs-1-個-consumer">1 個 Producer VS 1 個 Consumer<a class="h-anchor" href="#-1-%e5%80%8b-producer-vs-1-%e5%80%8b-consumer" aria-label="這個段落的連結"></a></h3>
<p>在專案根目錄下輸入 <code>node producer.js</code> 模擬「生產者傳送消息」，並在另一個分頁輸入 <code>node consumer.js</code> 模擬「消費者消費消息」。</p>
<p>透過下面的 Gif 大家可以看到消息傳送＆消費的過程。</p>
<figure class="figure figure-video">
  <video class="gifv" muted loop playsinline controls preload="none" poster="https://deanlin.net/blog/202211/nodejs-kafka-hands-on/images/nodejs-kafka-hands-on-02.webp" width="960" height="625" aria-label="1 個 Producer VS 1 個 Consumer（圖 2）" data-gifv>
    <source src="https://deanlin.net/blog/202211/nodejs-kafka-hands-on/images/nodejs-kafka-hands-on-02.mp4" type="video/mp4">
  </video>
</figure>

<blockquote>
<p>確認消息可以順利生產與消費後，記得透過「Crontrol + C」將程式關閉。</p>
</blockquote>
<h3 id="-kafka-持久化驗證">Kafka 持久化驗證<a class="h-anchor" href="#-kafka-%e6%8c%81%e4%b9%85%e5%8c%96%e9%a9%97%e8%ad%89" aria-label="這個段落的連結"></a></h3>
<p>這邊我們再做一個<strong>持久化</strong>的實驗，我們把「consumer.js」裏面 groupId 的值改為「test-group2」，來確認是否可以重新消費過去的消息。</p>
<p>通過下圖我們可以得知 Kafka 的資料是持久化的，不會因為有消費者消費而消失；因此擴張的時候，只需要增加消費者來處理資料即可。</p>
<p><img src="https://deanlin.net/blog/202211/nodejs-kafka-hands-on/images/nodejs-kafka-hands-on-03.webp" width="1400" height="659" alt="Kafka 持久化驗證（圖 3）" loading="lazy" decoding="async"></p>
<h3 class="h-nomark" id="-1-個-partition-的數據只允許-consumer-group-中的某個-consumer-消費">1 個 Partition 的數據只允許 Consumer Group 中的某個 Consumer 消費<a class="h-anchor" href="#-1-%e5%80%8b-partition-%e7%9a%84%e6%95%b8%e6%93%9a%e5%8f%aa%e5%85%81%e8%a8%b1-consumer-group-%e4%b8%ad%e7%9a%84%e6%9f%90%e5%80%8b-consumer-%e6%b6%88%e8%b2%bb" aria-label="這個段落的連結"></a></h3>
<p>接著我們來驗證，在 Producer 的 Partition 只有 1 個的情境下，是不是相同的 group_id 下只會有一個 consumer 進行消費；這次要開啟 3 個分頁，1 個模擬生產者，2 個模擬消費者。</p>
<p>透過下面的 Gif，大家可以看到在同一個 Consumer Group 中，的確只會有一個 Consumer 進行消費。</p>
<figure class="figure figure-video">
  <video class="gifv" muted loop playsinline controls preload="none" poster="https://deanlin.net/blog/202211/nodejs-kafka-hands-on/images/nodejs-kafka-hands-on-04.webp" width="960" height="551" aria-label="1 個 Partition 的數據只允許 Consumer Group 中的某個 Consumer 消費（圖 4）" data-gifv>
    <source src="https://deanlin.net/blog/202211/nodejs-kafka-hands-on/images/nodejs-kafka-hands-on-04.mp4" type="video/mp4">
  </video>
</figure>

<h3 class="h-nomark" id="-2-個-partition-vs-2-個-consumer">2 個 Partition VS 2 個 Consumer<a class="h-anchor" href="#-2-%e5%80%8b-partition-vs-2-%e5%80%8b-consumer" aria-label="這個段落的連結"></a></h3>
<p>先前有說到 Partition 數量最好與 Consumer 的數量相等，這樣效率才會最高（一個人一個碗）；這裡一樣是開啟 3 個分頁，1 個模擬生產者，2 個模擬消費者；同時對「producer.js」裡面的程式做微調如下：</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// 將原本的註解
</span></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// messages: Array(createMessage(getRandomNumber())),
</span></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// 解開下面的註解，測試 Kafka 多個 Partition 與 Consumer 的關係
</span></span></span><span style="display:flex;"><span>messages<span style="color:#ff7b72;font-weight:bold">:</span> Array(createMessage(getRandomNumber(),<span style="color:#a5d6ff">0</span>),createMessage(getRandomNumber(),<span style="color:#a5d6ff">1</span>)),
</span></span></code></pre></div><p>透過下面的 Gif，可以觀察到在 2 個 Partition VS 2 個 Consumer 時，消息會均勻的分配。</p>
<figure class="figure figure-video">
  <video class="gifv" muted loop playsinline controls preload="none" poster="https://deanlin.net/blog/202211/nodejs-kafka-hands-on/images/nodejs-kafka-hands-on-05.webp" width="960" height="551" aria-label="2 個 Partition VS 2 個 Consumer（圖 5）" data-gifv>
    <source src="https://deanlin.net/blog/202211/nodejs-kafka-hands-on/images/nodejs-kafka-hands-on-05.mp4" type="video/mp4">
  </video>
</figure>

<hr>
<p>在文章的最後，筆者想分享一段話：「只透過文字是無法理解一門技術的，需要透過實作才有辦法拼出技術的全貌。」</p>
<p>今天的文章就分享到這裡，筆者之後再找時間分享更深入的組合應用，也歡迎讀者分享自己的使用心得，希望藉助彼此的經驗，讓我們一起在工程師的道路走得更遠。</p>
]]></content:encoded>
    </item>
    <item>
      <title>善用分布式追蹤系統，幫你找出程式的效能瓶頸與問題影響範圍（Jaeger with OpenTelemetry Node.js）</title>
      <link>https://deanlin.net/blog/202211/distributed-tracing-jaeger-opentelemetry/</link>
      <pubDate>Tue, 15 Nov 2022 09:01:13 +0800</pubDate>
      <guid>https://deanlin.net/blog/202211/distributed-tracing-jaeger-opentelemetry/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>大型專案免不了要多個團隊合作，而每個團隊熟悉的程式語言可能不同，所部署的服務也可能分佈在多台 Server 上面。在這樣複雜的架構下，如果系統的某個服務出問題，又或是遇到效能瓶頸；即便有 log 紀錄，但在上下依賴不清楚的狀態下，工程師需要花不少時間來釐清問題原因。</description>
      <media:content url="https://deanlin.net/blog/202211/distributed-tracing-jaeger-opentelemetry/images/distributed-tracing-jaeger-opentelemetry-og_hu_10728b3352759c18.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<p>大型專案免不了要多個團隊合作，而每個團隊熟悉的程式語言可能不同，所部署的服務也可能分佈在多台 Server 上面。</p>
<p>在這樣複雜的架構下，如果系統的某個服務出問題，又或是遇到效能瓶頸；即便有 log 紀錄，但在上下依賴不清楚的狀態下，工程師需要花不少時間來釐清問題原因。</p>
<p><img src="https://deanlin.net/blog/202211/distributed-tracing-jaeger-opentelemetry/images/distributed-tracing-jaeger-opentelemetry-01.webp" width="1400" height="730" alt="善用分布式追蹤系統，幫你找出程式的效能瓶頸與問題影響範圍（Jaeger with OpenTelemetry Node.js）（圖 1）" fetchpriority="high" decoding="async"></p>
<pre tabindex="0"><code>大綱
一、為什麼需要分布式追蹤系統？
二、如何使用分布式追蹤系統？
三、手把手帶你建立 Node.js 專案 ＆ Jaeger 環境
四、啟動專案，並在 Jaeger UI 上查看自己發出的 Traces
</code></pre><h2 id="一為什麼需要分布式追蹤系統">一、為什麼需要分布式追蹤系統？<a class="h-anchor" href="#%e4%b8%80%e7%82%ba%e4%bb%80%e9%ba%bc%e9%9c%80%e8%a6%81%e5%88%86%e5%b8%83%e5%bc%8f%e8%bf%bd%e8%b9%a4%e7%b3%bb%e7%b5%b1" aria-label="這個段落的連結"></a></h2>
<p>下面舉 2 個簡單的例子：</p>
<p>如果系統每個月會「定時產出月報」給客戶，但這個功能突然壞了，在排查問題時可能會經歷如下痛苦：</p>
<ol>
<li>是「排程」的部分沒執行，還是「產出月報」的功能有問題？</li>
<li>產出月報的功能會呼叫「多個服務」，每個服務分別在不同的 Server 上。</li>
<li>服務間有不同的執行順序與依賴關係，需要按照每個節點排查。</li>
</ol>
<p>假使系統顯示 Dashboard（儀表板）的時間過長被客戶抱怨，工程師在優化時需要以下資訊才能判斷問題：</p>
<ol>
<li>Dashboard 渲染時會呼叫很多 API，而耗時長的 API 背後通常有複雜的商業邏輯（ex：串連多個服務後才得到的結果）。</li>
<li>即便知道哪個 API 是效能瓶頸，但因為串連的服務太多，需要逐一下 log 才能判斷出是哪個環節出了問題。</li>
</ol>
<p>相信有不少人遇過這些困擾，所以今天筆者想要跟大家分享，如何用「Jaeger + OpenTelemetry + Node.js」搭建分布式追蹤系統，來幫我們：</p>
<ol>
<li>快速找出問題來源。</li>
<li>判斷問題的影響範圍。</li>
<li>透過圖像梳理服務的依賴關係，以及依賴的合理性。</li>
<li>分析效能問題。</li>
</ol>
<blockquote>
<p><strong>小提醒</strong>：</p>
<p>每間公司的組織架構不同，第一時間接收錯誤訊息的未必是開發工程師，也可能是負責分析數據的團隊；如果系統的 log 訊息描述不清楚時，他們很難表達到底是哪裡出了問題，但如果使用分布式追蹤系統，他們就能很明確的指出問題，與工程師進行有效溝通。</p>
</blockquote>
<hr>
<h2 id="二如何使用分布式追蹤系統">二、如何使用分布式追蹤系統？<a class="h-anchor" href="#%e4%ba%8c%e5%a6%82%e4%bd%95%e4%bd%bf%e7%94%a8%e5%88%86%e5%b8%83%e5%bc%8f%e8%bf%bd%e8%b9%a4%e7%b3%bb%e7%b5%b1" aria-label="這個段落的連結"></a></h2>
<blockquote>
<p>下一節會帶讀者在 local 環境建立一個微型專案，這邊先簡述使用情境。</p>
</blockquote>
<p>我們的 Service A 提供下面 4 支 API：</p>
<ul>
<li><strong>GET /api/reportA</strong>：A 報告故意拋出錯誤。</li>
<li><strong>GET /api/reportB</strong>：B 報告內部會呼叫多個服務。</li>
<li><strong>GET /api/reportC</strong>：C 報告跑很久，為效能瓶頸。</li>
<li><strong>GET /api/allReports</strong>：產出 A、B、C、Other 報告。 我們的 Service B 僅提供下面 1 支 API：</li>
<li><strong>GET /api/reportOther</strong>：Other 報告會呼叫另一台 Server 的 API。</li>
</ul>
<p>如果執行「GET /api/allReports」這隻 API，它的運行架構是：</p>
<pre tabindex="0"><code>genAllReports(Service A)
    ├── reportA
    ├── reportB
    │       ├── reportB-1
    │       └── reportB-2
    ├── reportC
    └── reportOther(Service B)
</code></pre><p>因為在開頭有先說「GET /api/reportA」會拋出錯誤、「GET /api/reportC」是效能瓶頸；但如果在一無所知的狀態下直接呼叫「GET /api/allReports」這隻 API，那發生錯誤時，只能按照傳統的作法來慢慢排查 log。</p>
<p>初步了解專案架構後，下面我們先上幾張 Jaeger 的分析圖：</p>
<p><img src="https://deanlin.net/blog/202211/distributed-tracing-jaeger-opentelemetry/images/distributed-tracing-jaeger-opentelemetry-02.webp" width="1400" height="833" alt="二、如何使用分布式追蹤系統？（圖 2）" loading="lazy" decoding="async"></p>
<p>右上角的圖表中，紅色圓點是錯誤的標記，越大的圓點代表內含的 Span（邏輯運行單元）越多。</p>
<p>左邊則是搜尋條件：</p>
<ul>
<li><strong>Service</strong>：要追蹤的服務。</li>
<li><strong>Operations</strong>：曾經觸發過的操作。</li>
<li><strong>Tags</strong>：我們可以為 Span 設計 Tag 方便過濾。</li>
<li><strong>Lookback</strong>：以時間過濾。</li>
<li><strong>Min Duration/Min Duration</strong>：用最小/最大執行時間作為過濾條件。</li>
<li><strong>Limit Results</strong>：筆數顯示。</li>
</ul>
<figure class="figure"><img src="https://deanlin.net/blog/202211/distributed-tracing-jaeger-opentelemetry/images/distributed-tracing-jaeger-opentelemetry-03.webp" width="1090" height="793" alt="如果執行過程中有錯誤，會有警示的 label；若使用到不同 Service 也會用不同顏色的 label 區分。" loading="lazy" decoding="async">
  <figcaption>如果執行過程中有錯誤，會有警示的 label；若使用到不同 Service 也會用不同顏色的 label 區分。</figcaption>
</figure>

<p><img src="https://deanlin.net/blog/202211/distributed-tracing-jaeger-opentelemetry/images/distributed-tracing-jaeger-opentelemetry-04.webp" width="1400" height="807" alt="二、如何使用分布式追蹤系統？（圖 4）" loading="lazy" decoding="async"></p>
<p>我們可以點擊觀看執行的時間軸（Trace Timeline），上圖是「GET /api/allReports」這隻 API 的 Trace，我們可以很清楚每個 Span 之間的執行順序、依賴關係、執行時間、執行狀況。</p>
<ul>
<li><strong>Duration</strong>：總運行時間耗費 2s。</li>
<li><strong>Services</strong>：關聯的服務為 2（service-a、service-b）。</li>
<li><strong>Depth</strong>：最深的 Span 有 6 層（hadle_report_other 那個）。</li>
<li><strong>Total Spans</strong>：總共有 11 個 Span。</li>
</ul>
<figure class="figure"><img src="https://deanlin.net/blog/202211/distributed-tracing-jaeger-opentelemetry/images/distributed-tracing-jaeger-opentelemetry-05.webp" width="1400" height="792" alt="也可以切換到圖形（Trace Graph）介面來觀察。" loading="lazy" decoding="async">
  <figcaption>也可以切換到圖形（Trace Graph）介面來觀察。</figcaption>
</figure>

<p>相信圖片比起文字更有說服力，在圖表與搜尋條件的輔助下，我們能更快找到問題點。</p>
<hr>
<h2 id="三手把手帶你建立-nodejs-專案jaeger-環境">三、手把手帶你建立 Node.js 專案＆Jaeger 環境<a class="h-anchor" href="#%e4%b8%89%e6%89%8b%e6%8a%8a%e6%89%8b%e5%b8%b6%e4%bd%a0%e5%bb%ba%e7%ab%8b-nodejs-%e5%b0%88%e6%a1%88jaeger-%e7%92%b0%e5%a2%83" aria-label="這個段落的連結"></a></h2>
<p>這邊筆者就不細說程式邏輯，主要的目的是讓讀者在 local 端建立分布式追蹤系統，如果懶得按照步驟執行，可以直接到<a href="https://github.com/dean9703111/jaeger-demo">筆者的 Github</a> Clone 一份下來。</p>
<blockquote>
<p>筆者 local 的 Node.js 版本為 v18.12.0</p>
</blockquote>
<p>SETP 1：建立專案、安裝必要套件。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>npm init -f
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>npm install --save @opentelemetry/api
</span></span><span style="display:flex;"><span>npm install --save @opentelemetry/resources
</span></span><span style="display:flex;"><span>npm install --save @opentelemetry/semantic-conventions
</span></span><span style="display:flex;"><span>npm install --save @opentelemetry/sdk-trace-node
</span></span><span style="display:flex;"><span>npm install --save @opentelemetry/sdk-trace-base
</span></span><span style="display:flex;"><span>npm install --save @opentelemetry/exporter-jaeger
</span></span><span style="display:flex;"><span>npm install --save @opentelemetry/instrumentation-http
</span></span><span style="display:flex;"><span>npm install --save axios
</span></span><span style="display:flex;"><span>npm install --save express
</span></span></code></pre></div><p>SETP 2：新增「docker-compose.yml」貼上如下程式。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#7ee787">version</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#39;2.0&#39;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#7ee787">services</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">jaeger</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">container_name</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">practice-jaeger</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">image</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">jaegertracing/all-in-one:1.38</span><span style="color:#6e7681"> </span><span style="color:#8b949e;font-style:italic"># 當時用 1.39 或 latest 有出現一些 bug，故選用此版本</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">environment</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">COLLECTOR_ZIPKIN_HOST_PORT=:9411</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">ports</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">&#34;5775:5775/udp&#34;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">&#34;6831:6831/udp&#34;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">&#34;6832:6832/udp&#34;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">&#34;5778:5778&#34;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">&#34;16686:16686&#34;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">&#34;14250:14250&#34;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">&#34;14268:14268&#34;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">&#34;14269:14269&#34;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span>- <span style="color:#a5d6ff">&#34;9411:9411&#34;</span><span style="color:#6e7681">
</span></span></span></code></pre></div><p>SETP 3：輸入以下指令把 jaeger 的 Docker 拉下來並執行。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker-compose up -d
</span></span></code></pre></div><p>SETP 4：新增「tracing.js」貼上如下程式，如果你想要對「Opentelemetry」有更深入的了解，可以參考<a href="https://opentelemetry.io/docs/instrumentation/js/getting-started/nodejs/">官網</a>；筆者範例主要是增加「exporter-jaeger、instrumentation-http」這兩個設定。</p>
<blockquote>
<p>有些人分不清 Opentelemetry 跟 Jaeger 的角色定位；簡單來說，Opentelemetry 幫忙產生/收集觀察用的數據，而 Jaeger 則是一種分布式追蹤工具。</p>
</blockquote>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#a5d6ff">&#39;use strict&#39;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> opentelemetry <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;@opentelemetry/api&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> { registerInstrumentations } <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;@opentelemetry/instrumentation&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> { NodeTracerProvider } <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;@opentelemetry/sdk-trace-node&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> { Resource } <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;@opentelemetry/resources&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> { SemanticResourceAttributes } <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;@opentelemetry/semantic-conventions&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> { BatchSpanProcessor } <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;@opentelemetry/sdk-trace-base&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> { JaegerExporter } <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;@opentelemetry/exporter-jaeger&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> { HttpInstrumentation } <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;@opentelemetry/instrumentation-http&#39;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>module.exports <span style="color:#ff7b72;font-weight:bold">=</span> (serviceName) =&gt; {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> provider <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">new</span> NodeTracerProvider({
</span></span><span style="display:flex;"><span>        resource<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#ff7b72">new</span> Resource({
</span></span><span style="display:flex;"><span>            [SemanticResourceAttributes.SERVICE_NAME]<span style="color:#ff7b72;font-weight:bold">:</span> serviceName,
</span></span><span style="display:flex;"><span>        }),
</span></span><span style="display:flex;"><span>    });
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">let</span> exporter;
</span></span><span style="display:flex;"><span>    exporter <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">new</span> JaegerExporter({ endpoint<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;http://127.0.0.1:14268/api/traces&#34;</span> });
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    provider.addSpanProcessor(<span style="color:#ff7b72">new</span> BatchSpanProcessor(exporter));
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// Initialize the OpenTelemetry APIs to use the NodeTracerProvider bindings
</span></span></span><span style="display:flex;"><span>    provider.register();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    registerInstrumentations({
</span></span><span style="display:flex;"><span>        <span style="color:#8b949e;font-style:italic">// // when boostraping with lerna for testing purposes
</span></span></span><span style="display:flex;"><span>        instrumentations<span style="color:#ff7b72;font-weight:bold">:</span> [
</span></span><span style="display:flex;"><span>            <span style="color:#ff7b72">new</span> HttpInstrumentation(),
</span></span><span style="display:flex;"><span>        ],
</span></span><span style="display:flex;"><span>    });
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">return</span> opentelemetry.trace.getTracer(<span style="color:#a5d6ff">&#39;my-service-tracer&#39;</span>);
</span></span><span style="display:flex;"><span>};
</span></span></code></pre></div><p>SETP 5：新增「service-a.js」貼上如下程式，我們在這裡建立 Service A 的 4 支 API。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#ff7b72">const</span> api <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;@opentelemetry/api&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> tracer <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;./tracing&#39;</span>)(<span style="color:#a5d6ff">&#39;service-a&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> express <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#34;express&#34;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> PORT <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#34;8080&#34;</span>;
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> app <span style="color:#ff7b72;font-weight:bold">=</span> express();
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> axios <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;axios&#39;</span>)
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> waitFor <span style="color:#ff7b72;font-weight:bold">=</span> delay =&gt; <span style="color:#ff7b72">new</span> Promise(resolve =&gt; setTimeout(resolve, delay));
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">async</span> <span style="color:#ff7b72">function</span> reportA() {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">let</span> span <span style="color:#ff7b72;font-weight:bold">=</span> tracer.startSpan(<span style="color:#a5d6ff">&#39;report_a&#39;</span>)
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">await</span> waitFor(<span style="color:#a5d6ff">500</span>);
</span></span><span style="display:flex;"><span>  span.setAttribute(<span style="color:#a5d6ff">&#39;report&#39;</span>, <span style="color:#a5d6ff">&#39;A&#39;</span>)
</span></span><span style="display:flex;"><span>  span.recordException(<span style="color:#ff7b72">new</span> Error(<span style="color:#a5d6ff">&#39;Report A not found&#39;</span>))
</span></span><span style="display:flex;"><span>  span.setStatus({ code<span style="color:#ff7b72;font-weight:bold">:</span> api.SpanStatusCode.ERROR, message<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;Something wrong!&#34;</span> })
</span></span><span style="display:flex;"><span>  span.addEvent(<span style="color:#a5d6ff">&#39;log&#39;</span>, {
</span></span><span style="display:flex;"><span>    <span style="color:#a5d6ff">&#39;log.severity&#39;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;error&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a5d6ff">&#39;log.message&#39;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;Report A not found&#39;</span>,
</span></span><span style="display:flex;"><span>    <span style="color:#a5d6ff">&#39;endReport.id&#39;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;123&#39;</span>,
</span></span><span style="display:flex;"><span>  })
</span></span><span style="display:flex;"><span>  span.end()
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">throw</span> <span style="color:#a5d6ff">&#34;Report A not found&#34;</span>;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">async</span> <span style="color:#ff7b72">function</span> reportB() {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">return</span> <span style="color:#ff7b72">await</span> tracer.startActiveSpan(<span style="color:#a5d6ff">&#39;report_b&#39;</span>, <span style="color:#ff7b72">async</span> (span) =&gt; {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">await</span> waitFor(<span style="color:#a5d6ff">100</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">let</span> child1 <span style="color:#ff7b72;font-weight:bold">=</span> tracer.startSpan(<span style="color:#a5d6ff">&#39;report_b_1&#39;</span>)
</span></span><span style="display:flex;"><span>    child1.end()
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">let</span> child2 <span style="color:#ff7b72;font-weight:bold">=</span> tracer.startSpan(<span style="color:#a5d6ff">&#39;report_b_2&#39;</span>)
</span></span><span style="display:flex;"><span>    child2.end()
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    span.setAttribute(<span style="color:#a5d6ff">&#39;report&#39;</span>, <span style="color:#a5d6ff">&#39;B&#39;</span>)
</span></span><span style="display:flex;"><span>    span.end()
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">return</span> { <span style="color:#a5d6ff">&#34;b&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;context&#34;</span> }
</span></span><span style="display:flex;"><span>  })
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">async</span> <span style="color:#ff7b72">function</span> reportC() {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">let</span> span <span style="color:#ff7b72;font-weight:bold">=</span> tracer.startSpan(<span style="color:#a5d6ff">&#39;report_c&#39;</span>)
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">await</span> waitFor(<span style="color:#a5d6ff">2000</span>);
</span></span><span style="display:flex;"><span>  span.setAttribute(<span style="color:#a5d6ff">&#39;report&#39;</span>, <span style="color:#a5d6ff">&#39;C&#39;</span>)
</span></span><span style="display:flex;"><span>  span.end()
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">return</span> { <span style="color:#a5d6ff">&#34;c&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;context&#34;</span> }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">async</span> <span style="color:#ff7b72">function</span> reportOther() {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">try</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">let</span> span <span style="color:#ff7b72;font-weight:bold">=</span> tracer.startSpan(<span style="color:#a5d6ff">&#39;report_other_request&#39;</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">let</span> result;
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">await</span> api.context.<span style="color:#ff7b72">with</span>(api.trace.setSpan(api.context.active(), span),<span style="color:#ff7b72">async</span> () =&gt; {
</span></span><span style="display:flex;"><span>      <span style="color:#ff7b72">const</span> { data } <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> axios.get(<span style="color:#a5d6ff">`http://localhost:8081/api/reportOther`</span>);
</span></span><span style="display:flex;"><span>      result <span style="color:#ff7b72;font-weight:bold">=</span> data;
</span></span><span style="display:flex;"><span>    });
</span></span><span style="display:flex;"><span>    span.end()
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">return</span> result
</span></span><span style="display:flex;"><span>  } <span style="color:#ff7b72">catch</span> (e) {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">throw</span> e
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>app.get(<span style="color:#a5d6ff">&#34;/api/allReports&#34;</span>, <span style="color:#ff7b72">async</span> (req, res) =&gt; {
</span></span><span style="display:flex;"><span>  tracer.startActiveSpan(<span style="color:#a5d6ff">&#39;gen_all_reports&#39;</span>, <span style="color:#ff7b72">async</span> (span) =&gt; {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> promises <span style="color:#ff7b72;font-weight:bold">=</span> [
</span></span><span style="display:flex;"><span>      reportA(),
</span></span><span style="display:flex;"><span>      reportB(),
</span></span><span style="display:flex;"><span>      reportC(),
</span></span><span style="display:flex;"><span>      reportOther()
</span></span><span style="display:flex;"><span>    ];
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">let</span> result <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> Promise.allSettled(promises);
</span></span><span style="display:flex;"><span>    span.end()
</span></span><span style="display:flex;"><span>    res.send(result);
</span></span><span style="display:flex;"><span>  })
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>app.get(<span style="color:#a5d6ff">&#34;/api/reportA&#34;</span>, <span style="color:#ff7b72">async</span> (req, res) =&gt; {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">try</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">let</span> result <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> reportA()
</span></span><span style="display:flex;"><span>    res.send(result);
</span></span><span style="display:flex;"><span>  } <span style="color:#ff7b72">catch</span> (e) {
</span></span><span style="display:flex;"><span>    res.send(e);
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>app.get(<span style="color:#a5d6ff">&#34;/api/reportB&#34;</span>, <span style="color:#ff7b72">async</span> (req, res) =&gt; {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">try</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">let</span> result <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> reportB()
</span></span><span style="display:flex;"><span>    res.send(result);
</span></span><span style="display:flex;"><span>  } <span style="color:#ff7b72">catch</span> (e) {
</span></span><span style="display:flex;"><span>    res.send(e);
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>app.get(<span style="color:#a5d6ff">&#34;/api/reportC&#34;</span>, <span style="color:#ff7b72">async</span> (req, res) =&gt; {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">try</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">let</span> result <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> reportC()
</span></span><span style="display:flex;"><span>    res.send(result);
</span></span><span style="display:flex;"><span>  } <span style="color:#ff7b72">catch</span> (e) {
</span></span><span style="display:flex;"><span>    res.send(e);
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>app.listen(parseInt(PORT, <span style="color:#a5d6ff">10</span>), () =&gt; {
</span></span><span style="display:flex;"><span>  console.log(<span style="color:#a5d6ff">`Service A listening for requests on http://localhost:</span><span style="color:#a5d6ff">${</span>PORT<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>);
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><p>SETP 6：新增「service-b.js」貼上如下程式，我們在這裡建立的 API 是要給 Service A 呼叫的。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#ff7b72">const</span> tracer <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;./tracing&#39;</span>)(<span style="color:#a5d6ff">&#39;service-b&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> express <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#34;express&#34;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> PORT <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#34;8081&#34;</span>;
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> app <span style="color:#ff7b72;font-weight:bold">=</span> express();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>app.get(<span style="color:#a5d6ff">&#34;/api/reportOther&#34;</span>, (req, res) =&gt; {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">try</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">let</span> span <span style="color:#ff7b72;font-weight:bold">=</span> tracer.startSpan(<span style="color:#a5d6ff">&#39;handle_report_other&#39;</span>);
</span></span><span style="display:flex;"><span>    span.end()
</span></span><span style="display:flex;"><span>    res.send({ <span style="color:#a5d6ff">&#34;other&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;context&#34;</span> });
</span></span><span style="display:flex;"><span>  } <span style="color:#ff7b72">catch</span> (e) {
</span></span><span style="display:flex;"><span>    res.send(e);
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>app.listen(parseInt(PORT, <span style="color:#a5d6ff">10</span>), () =&gt; {
</span></span><span style="display:flex;"><span>  console.log(<span style="color:#a5d6ff">`Service B listening for requests on http://localhost:</span><span style="color:#a5d6ff">${</span>PORT<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>);
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><hr>
<h2 id="四啟動專案並在-jaeger-ui-上查看自己發出的-traces">四、啟動專案，並在 Jaeger UI 上查看自己發出的 Traces<a class="h-anchor" href="#%e5%9b%9b%e5%95%9f%e5%8b%95%e5%b0%88%e6%a1%88%e4%b8%a6%e5%9c%a8-jaeger-ui-%e4%b8%8a%e6%9f%a5%e7%9c%8b%e8%87%aa%e5%b7%b1%e7%99%bc%e5%87%ba%e7%9a%84-traces" aria-label="這個段落的連結"></a></h2>
<blockquote>
<p>建議使用有分頁功能的終端機（如 iTerm2），這樣比較好觀察與操作。</p>
</blockquote>
<p>在專案根目錄下輸入 <code>node service-a.js</code> 啟動 Service A 服務，並在另一個分頁輸入 <code>node service-b.js</code> 啟動 Service B 服務。</p>
<p>然後在終端機第三個分頁複製貼上以下指令，呼叫 API。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl http://localhost:8080/api/reportA
</span></span><span style="display:flex;"><span>curl http://localhost:8080/api/reportB
</span></span><span style="display:flex;"><span>curl http://localhost:8080/api/reportC
</span></span><span style="display:flex;"><span>curl http://localhost:8081/api/reportOther
</span></span><span style="display:flex;"><span>curl http://localhost:8080/api/allReports
</span></span></code></pre></div><p><img src="https://deanlin.net/blog/202211/distributed-tracing-jaeger-opentelemetry/images/distributed-tracing-jaeger-opentelemetry-06.webp" width="1400" height="772" alt="四、啟動專案，並在 Jaeger UI 上查看自己發出的 Traces（圖 6）" loading="lazy" decoding="async"></p>
<p>如果都按照步驟執行，在瀏覽器的網址輸入「 <a href="http://localhost:16686/search">http://localhost:16686/search</a> 」就可以開始追蹤你所設定的 Traces 嚕！</p>
<p><img src="https://deanlin.net/blog/202211/distributed-tracing-jaeger-opentelemetry/images/distributed-tracing-jaeger-opentelemetry-07.webp" width="1400" height="824" alt="四、啟動專案，並在 Jaeger UI 上查看自己發出的 Traces（圖 7）" loading="lazy" decoding="async"></p>
<h3 id="一些小建議">一些小建議<a class="h-anchor" href="#%e4%b8%80%e4%ba%9b%e5%b0%8f%e5%bb%ba%e8%ad%b0" aria-label="這個段落的連結"></a></h3>
<ul>
<li>雖然分布式追蹤系統能幫我們許多忙，但如果所有的服務都靠它追蹤，那產生的 Record 數量會非常龐大，所以建議「追蹤重要的服務」為主。</li>
<li>這邊為了方便 Demo，筆者使用 http 來做 instrumentation 的舉例，但實務上 RPC(Remote Procedure Call) 的使用率可能更高。</li>
</ul>
<p>筆者也是剛學習分布式追蹤系統的新人，如果描述有誤，或是有其他建議，再麻煩大神們不吝賜教，感謝～</p>
]]></content:encoded>
    </item>
    <item>
      <title>使用 nvm &#43; .nvmrc 讓 Node.js 版本在不同的專案間自動切換</title>
      <link>https://deanlin.net/blog/202210/nvm-nvmrc-auto-switch/</link>
      <pubDate>Wed, 26 Oct 2022 23:01:53 +0800</pubDate>
      <guid>https://deanlin.net/blog/202210/nvm-nvmrc-auto-switch/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>Node.js 版本更新速度極快，但未必所有套件都能相容於新的版本，這造成每個專案的 Node.js 版本不盡相同；所以今天要介紹如何透過 .nvmrc 搭配 zsh or bash script 讓你在專案間自動切換 Node.js 版本。</description>
      <media:content url="https://deanlin.net/blog/202210/nvm-nvmrc-auto-switch/images/nvm-nvmrc-auto-switch-og_hu_5470ca57d11f8c34.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<p>Node.js 版本更新速度極快，但未必所有套件都能相容於新的版本，這造成每個專案的 Node.js 版本不盡相同；即使 nvm 可以幫我們快速切換 Node.js 版本還是不太方便，所以今天要介紹如何透過 .nvmrc 搭配 zsh or bash script 讓你在專案間自動切換 Node.js 版本。</p>
<p><img src="https://deanlin.net/blog/202210/nvm-nvmrc-auto-switch/images/nvm-nvmrc-auto-switch-01.webp" width="1400" height="731" alt="使用 nvm &#43; .nvmrc 讓 Node.js 版本在不同的專案間自動切換（圖 1）" fetchpriority="high" decoding="async"></p>
<blockquote>
<p>筆者原本選用「avn」這個套件，但它真的已經年久失修了無法使用<a href="https://github.com/wbyoung/avn/issues/109#issuecomment-945155272">連結</a></p>
</blockquote>
<h2 id="step-1建立兩個-nodejs-專案">STEP 1：建立兩個 Node.js 專案<a class="h-anchor" href="#step-1%e5%bb%ba%e7%ab%8b%e5%85%a9%e5%80%8b-nodejs-%e5%b0%88%e6%a1%88" aria-label="這個段落的連結"></a></h2>
<p>在終端機複製貼上下方指令即可。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>mkdir project-a
</span></span><span style="display:flex;"><span>cd project-a
</span></span><span style="display:flex;"><span>npm init -y
</span></span><span style="display:flex;"><span>cd ..
</span></span><span style="display:flex;"><span>mkdir project-b
</span></span><span style="display:flex;"><span>cd project-b
</span></span><span style="display:flex;"><span>npm init -y
</span></span><span style="display:flex;"><span>cd ..
</span></span></code></pre></div><h2 id="step-2安裝特定的-nodejs-版本以供測試">STEP 2：安裝特定的 Node.js 版本以供測試<a class="h-anchor" href="#step-2%e5%ae%89%e8%a3%9d%e7%89%b9%e5%ae%9a%e7%9a%84-nodejs-%e7%89%88%e6%9c%ac%e4%bb%a5%e4%be%9b%e6%b8%ac%e8%a9%a6" aria-label="這個段落的連結"></a></h2>
<p>尚未 nvm 安裝的讀者，可以先透過如下指令安裝，更新的版本請參考<a href="https://github.com/nvm-sh/nvm#installing-and-updating">連結</a>。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.2/install.sh | bash
</span></span></code></pre></div><p>安裝完後建議重開終端機讓它生效，接著透過它安裝 Node.js 特定版本：</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>nvm install <span style="color:#a5d6ff">12</span>
</span></span><span style="display:flex;"><span>nvm install <span style="color:#a5d6ff">16</span>
</span></span></code></pre></div><h2 id="step-3在兩個-nodejs-專案中加入nvmrc並指定-nodejs-版本">STEP 3：在兩個 Node.js 專案中加入「.nvmrc」並指定 Node.js 版本<a class="h-anchor" href="#step-3%e5%9c%a8%e5%85%a9%e5%80%8b-nodejs-%e5%b0%88%e6%a1%88%e4%b8%ad%e5%8a%a0%e5%85%a5nvmrc%e4%b8%a6%e6%8c%87%e5%ae%9a-nodejs-%e7%89%88%e6%9c%ac" aria-label="這個段落的連結"></a></h2>
<p>接下來我們就分別在兩個專案中新增「.nvmrc」的檔案，並個別指定 12、16 版本方便測試（讀者在終端機複製貼上下方指令即可）。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>cd project-a
</span></span><span style="display:flex;"><span>touch .nvmrc
</span></span><span style="display:flex;"><span>echo <span style="color:#a5d6ff">&#34;12&#34;</span> &gt;&gt; .nvmrc
</span></span><span style="display:flex;"><span>cd ..
</span></span><span style="display:flex;"><span>cd project-b
</span></span><span style="display:flex;"><span>touch .nvmrc
</span></span><span style="display:flex;"><span>echo <span style="color:#a5d6ff">&#34;16&#34;</span> &gt;&gt; .nvmrc
</span></span><span style="display:flex;"><span>cd ..
</span></span></code></pre></div><blockquote>
<p>筆者為了方便 Demo 而指定大版本，「.nvmrc」檔案是能吃「x.y.z」版本號的。</p>
</blockquote>
<h2 id="step-4調整-zsh-script">STEP 4：調整 zsh script<a class="h-anchor" href="#step-4%e8%aa%bf%e6%95%b4-zsh-script" aria-label="這個段落的連結"></a></h2>
<p>在終端機輸入 <code>vi ~/.zshrc</code>（每個人的路徑可能不同），到最後一行貼上下方 script：</p>
<pre tabindex="0"><code># place this after nvm initialization!
autoload -U add-zsh-hook
load-nvmrc() {
  local node_version=&#34;$(nvm version)&#34;
  local nvmrc_path=&#34;$(nvm_find_nvmrc)&#34;
  if [ -n &#34;$nvmrc_path&#34; ]; then
    local nvmrc_node_version=$(nvm version &#34;$(cat &#34;${nvmrc_path}&#34;)&#34;)
    if [ &#34;$nvmrc_node_version&#34; = &#34;N/A&#34; ]; then
      nvm install
    elif [ &#34;$nvmrc_node_version&#34; != &#34;$node_version&#34; ]; then
      nvm use
    fi
  elif [ &#34;$node_version&#34; != &#34;$(nvm version default)&#34; ]; then
    echo &#34;Reverting to nvm default version&#34;
    nvm use default
  fi
}
add-zsh-hook chpwd load-nvmrc
load-nvmrc
</code></pre><p>儲存後再輸入 <code>source ~/.zshrc</code> 即可讓 script 生效。</p>
<p>這邊簡單說明 script 運行邏輯：</p>
<ul>
<li>資料夾下<strong>有</strong>「.nvmrc」的檔案：若指定的 Node.js 版本已安裝就透過「nvm use」切換，假使該版本尚未安裝就會執行「nvm install」。</li>
<li>資料夾下<strong>沒有</strong>「.nvmrc」的檔案：切換回 Node.js 默認的版本。</li>
</ul>
<p>唯一要注意的是，<strong>記得把這段 script 放在 nvm 初始化後的位置</strong>（或是直接放在最後面也可以）。</p>
<blockquote>
<p>上面的 script 參考自 nvm 官網，如果讀者使用的是 bash script 請參考<a href="https://github.com/nvm-sh/nvm#bash">連結</a>。</p>
</blockquote>
<h2 id="step-5實測是否有效">STEP 5：實測是否有效<a class="h-anchor" href="#step-5%e5%af%a6%e6%b8%ac%e6%98%af%e5%90%a6%e6%9c%89%e6%95%88" aria-label="這個段落的連結"></a></h2>
<p>筆者在開頭故意切換到 Node.js 18 的版本，然後再依序切換到「project-a」以及「project-b」的資料夾，從下圖大家可以看到 Node.js 的版本都順利切換嚕！</p>
<p><img src="https://deanlin.net/blog/202210/nvm-nvmrc-auto-switch/images/nvm-nvmrc-auto-switch-02.webp" width="701" height="218" alt="STEP 5：實測是否有效（圖 2）" loading="lazy" decoding="async"></p>
<blockquote class="pullquote"><p>希望這篇文章，有幫大家解決不同專案的 Node.js 版本切換之苦～</p>
</blockquote>
]]></content:encoded>
    </item>
    <item>
      <title>Node.js &#43; worker_threads 多執行緒實戰範例</title>
      <link>https://deanlin.net/blog/202210/nodejs-worker-threads/</link>
      <pubDate>Thu, 20 Oct 2022 12:06:17 +0800</pubDate>
      <guid>https://deanlin.net/blog/202210/nodejs-worker-threads/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>一般狀況下 Node.js 為單執行緒，如果同步執行時遇到「阻塞」的程式，後續要執行的程式就會全部被卡住；為了解決這個問題， Node.js 在 v12 推出了 worker_threads，這個 Module 讓 Node.js 可以做多執行緒的平行處理！</description>
      <media:content url="https://deanlin.net/blog/202210/nodejs-worker-threads/images/nodejs-worker-threads-og_hu_615bee066aaa9969.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<p>一般狀況下 Node.js 為單執行緒，如果同步執行時遇到「阻塞」的程式，後續要執行的程式就會全部被卡住；為了解決這個問題， Node.js 在 v12 推出了 <code>worker_threads</code>，這個 Module 讓 Node.js 可以做多執行緒的平行處理！</p>
<p><img src="https://deanlin.net/blog/202210/nodejs-worker-threads/images/nodejs-worker-threads-01.webp" width="1400" height="732" alt="Node.js &#43; worker_threads 多執行緒實戰範例（圖 1）" fetchpriority="high" decoding="async"></p>
<blockquote>
<p>今天這篇文章會採用 Demo 的形式，讓大家理解程式運作的順序，而「阻塞」的程式則是選用 <code>斐波那契數列（fibonacci sequence）</code> 來模擬。</p>
</blockquote>
<h2 id="單執行緒會遇到什麼問題">單執行緒會遇到什麼問題<a class="h-anchor" href="#%e5%96%ae%e5%9f%b7%e8%a1%8c%e7%b7%92%e6%9c%83%e9%81%87%e5%88%b0%e4%bb%80%e9%ba%bc%e5%95%8f%e9%a1%8c" aria-label="這個段落的連結"></a></h2>
<p>單執行緒如果遇上需要高 CPU 運算的程式，那整體效能就會大幅下降；下面我們用一個簡單的程式來做說明：</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#ff7b72">function</span> getFibonacciNumber (num) {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">if</span> (num <span style="color:#ff7b72;font-weight:bold">===</span> <span style="color:#a5d6ff">0</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">return</span> <span style="color:#a5d6ff">0</span>;
</span></span><span style="display:flex;"><span>  } <span style="color:#ff7b72">else</span> <span style="color:#ff7b72">if</span> (num <span style="color:#ff7b72;font-weight:bold">===</span> <span style="color:#a5d6ff">1</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">return</span> <span style="color:#a5d6ff">1</span>;
</span></span><span style="display:flex;"><span>  } <span style="color:#ff7b72">else</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">return</span> getFibonacciNumber(num <span style="color:#ff7b72;font-weight:bold">-</span> <span style="color:#a5d6ff">1</span>) <span style="color:#ff7b72;font-weight:bold">+</span> getFibonacciNumber(num <span style="color:#ff7b72;font-weight:bold">-</span> <span style="color:#a5d6ff">2</span>);
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>console.log(<span style="color:#a5d6ff">`42th Fibonacci Result:`</span> <span style="color:#ff7b72;font-weight:bold">+</span> getFibonacciNumber(<span style="color:#a5d6ff">42</span>));
</span></span><span style="display:flex;"><span>console.log(<span style="color:#a5d6ff">`15th Fibonacci Result:`</span> <span style="color:#ff7b72;font-weight:bold">+</span> getFibonacciNumber(<span style="color:#a5d6ff">15</span>));
</span></span><span style="display:flex;"><span>console.log(<span style="color:#a5d6ff">&#34;被卡住的輕量級任務&#34;</span>);
</span></span></code></pre></div><p class="code-source">原始碼：<a href="https://gist.github.com/dean9703111/ef1bc467983a03bb631a90dcdd5a8d54" rel="noopener">single-thread.js</a>（GitHub Gist）</p>
<p>上述程式的執行結果為：</p>
<pre tabindex="0"><code>42th Fibonacci Result:267914296
15th Fibonacci Result:610
被卡住的輕量級任務
</code></pre><p>你可以將程式中的 3 個 console.log 理解為：</p>
<ol>
<li><strong>42th Fibonacci</strong>：會讓程式阻塞的重量級運算。</li>
<li><strong>15th Fibonacci</strong>：運算需要的時間不長，但被上面的程式卡住了。</li>
<li><strong>被卡住的輕量級任務</strong>：只是簡單的輸出，卻被上面的程式卡的動彈不得。</li>
</ol>
<hr>
<h2 id="多執行緒如何解決">多執行緒如何解決<a class="h-anchor" href="#%e5%a4%9a%e5%9f%b7%e8%a1%8c%e7%b7%92%e5%a6%82%e4%bd%95%e8%a7%a3%e6%b1%ba" aria-label="這個段落的連結"></a></h2>
<p>既然知道 <code>斐波那契數列（fibonacci sequence）</code> 是阻塞的元兇，那我們可以把這個運算抽離出來，給新的執行緒來計算。</p>
<p>在實作方面，先新增一個 <code>worker.js</code> 的檔案，讓他處理由「主執行緒」指派的「新執行緒」；這裡的執行緒會透過 <code>parentPort.postMessage</code> 函數與「主執行緒」傳遞訊息。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#ff7b72">const</span> { parentPort, workerData } <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#34;worker_threads&#34;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>parentPort.postMessage(getFibonacciNumber(workerData.num))
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">function</span> getFibonacciNumber (num) {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">if</span> (num <span style="color:#ff7b72;font-weight:bold">===</span> <span style="color:#a5d6ff">0</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">return</span> <span style="color:#a5d6ff">0</span>;
</span></span><span style="display:flex;"><span>  } <span style="color:#ff7b72">else</span> <span style="color:#ff7b72">if</span> (num <span style="color:#ff7b72;font-weight:bold">===</span> <span style="color:#a5d6ff">1</span>) {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">return</span> <span style="color:#a5d6ff">1</span>;
</span></span><span style="display:flex;"><span>  } <span style="color:#ff7b72">else</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">return</span> getFibonacciNumber(num <span style="color:#ff7b72;font-weight:bold">-</span> <span style="color:#a5d6ff">1</span>) <span style="color:#ff7b72;font-weight:bold">+</span> getFibonacciNumber(num <span style="color:#ff7b72;font-weight:bold">-</span> <span style="color:#a5d6ff">2</span>);
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p class="code-source">原始碼：<a href="https://gist.github.com/dean9703111/15577f1cfec9a82f3c25344f7acaa3df" rel="noopener">worker.js</a>（GitHub Gist）</p>
<p>接著我們新增一個 <code>index.js</code> 的檔案，來設計主執行緒的程式，筆者在這新增一個「runWorker」的函式，將會造成「阻塞」的程式交給它來執行。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#ff7b72">const</span> { Worker } <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#34;worker_threads&#34;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> runWorker <span style="color:#ff7b72;font-weight:bold">=</span> (number) =&gt; {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">const</span> worker <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">new</span> Worker(<span style="color:#a5d6ff">&#34;./worker.js&#34;</span>, { workerData<span style="color:#ff7b72;font-weight:bold">:</span> { num<span style="color:#ff7b72;font-weight:bold">:</span> number } });
</span></span><span style="display:flex;"><span>  worker.on(<span style="color:#a5d6ff">&#34;message&#34;</span>, result =&gt; {
</span></span><span style="display:flex;"><span>    console.log(<span style="color:#a5d6ff">`</span><span style="color:#a5d6ff">${</span>number<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">th Fibonacci Result: </span><span style="color:#a5d6ff">${</span>result<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>);
</span></span><span style="display:flex;"><span>  });
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  worker.on(<span style="color:#a5d6ff">&#34;error&#34;</span>, error =&gt; {
</span></span><span style="display:flex;"><span>    console.log(error);
</span></span><span style="display:flex;"><span>  });
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  worker.on(<span style="color:#a5d6ff">&#34;exit&#34;</span>, exitCode =&gt; {
</span></span><span style="display:flex;"><span>    console.log(<span style="color:#a5d6ff">`It exited with code </span><span style="color:#a5d6ff">${</span>exitCode<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>);
</span></span><span style="display:flex;"><span>  })
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>runWorker(<span style="color:#a5d6ff">42</span>)
</span></span><span style="display:flex;"><span>runWorker(<span style="color:#a5d6ff">15</span>)
</span></span><span style="display:flex;"><span>console.log(<span style="color:#a5d6ff">&#34;主執行緒的任務&#34;</span>);
</span></span></code></pre></div><p class="code-source">原始碼：<a href="https://gist.github.com/dean9703111/00e22524c68601f108dbe1630f7b1534" rel="noopener">worker-threads.js</a>（GitHub Gist）</p>
<p>經過這樣的調整後：</p>
<ol>
<li>繁重的運算由「新執行緒」處理，且不會互相阻塞；解決了 Fibonacci 在「15th」的運算明明很快，但於單執行緒卻被「42th」拖到後面才執行的問題。</li>
<li>從下圖的運算結果，你可以觀察到主執行緒不會被阻塞。</li>
</ol>
<p><img src="https://deanlin.net/blog/202210/nodejs-worker-threads/images/nodejs-worker-threads-02.webp" width="910" height="278" alt="多執行緒如何解決（圖 2）" loading="lazy" decoding="async"></p>
<hr>
<h2 id="結語">結語<a class="h-anchor" href="#%e7%b5%90%e8%aa%9e" aria-label="這個段落的連結"></a></h2>
<p>過去 Node.js 常常被說不適合執行高 CPU 運算的任務（因為會阻塞）。</p>
<p>但在導入 <code>worker_threads</code> 使其可以運用多執行緒後，我們只要將「高 CPU 運算」的任務交給其他執行緒處理，主執行緒就能繼續處理其他任務不被阻塞，以此提高 Server 的吞吐量。</p>
]]></content:encoded>
    </item>
    <item>
      <title>在 Node.js 使用 Redis 來 Cache API 的回傳資料，以減少查詢資料的回傳時間</title>
      <link>https://deanlin.net/blog/202210/nodejs-redis-api-cache/</link>
      <pubDate>Wed, 19 Oct 2022 12:29:58 +0800</pubDate>
      <guid>https://deanlin.net/blog/202210/nodejs-redis-api-cache/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>現在使用者對網站的要求越來越高，即使你的內容做得很棒，但如果頁面載入時間超過 3 秒，使用者可能在被你的內容吸引前就離開了網站。如果你做的是電子商務系統，這個「痛點」會更為明顯，一但讓客戶等太久，公司就會流失大筆訂單…</description>
      <media:content url="https://deanlin.net/blog/202210/nodejs-redis-api-cache/images/nodejs-redis-api-cache-og_hu_e1fdaa74534a2a48.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<p>現在使用者對網站的要求越來越高，即使你的內容做得很棒，但如果頁面載入時間超過 3 秒，使用者可能在被你的內容吸引前就離開了網站。</p>
<p>如果你做的是電子商務系統，這個「痛點」會更為明顯，一但讓客戶等太久，公司就會流失大筆訂單。</p>
<p>為了解決這個問題，我們可以將資料設計成「快取」，這樣不但能大幅減少查詢資料的回傳時間（毫秒級別），還能有效降低後端資料庫的存取壓力。</p>
<p><img src="https://deanlin.net/blog/202210/nodejs-redis-api-cache/images/nodejs-redis-api-cache-01.webp" width="1400" height="732" alt="在 Node.js 使用 Redis 來 Cache API 的回傳資料，以減少查詢資料的回傳時間（圖 1）" fetchpriority="high" decoding="async"></p>
<blockquote>
<p>本篇文章將以：「Node.js X Koa X Redis」作為範例解說；如果你完全沒接觸過 Redis，可以先參考<a href="https://deanlin.net/blog/202108/install-redis-on-macos/">這篇文章</a>安裝環境。</p>
</blockquote>
<h2 id="應用場景說明">應用場景說明<a class="h-anchor" href="#%e6%87%89%e7%94%a8%e5%a0%b4%e6%99%af%e8%aa%aa%e6%98%8e" aria-label="這個段落的連結"></a></h2>
<p>如果今天要設計一款「等公車」的服務，那你勢必要串接「第三方 API」來獲取即時訊息。</p>
<p>但有時第三方 API 的回傳時間並不理想，甚至有存取次數的限制；如果我們想降低第三方 API 存取次數，並減少使用者等待的回傳時間，我們可以針對資料做「快取（Cache）」設計。</p>
<p>等公車這個服務有幾個特點：</p>
<ol>
<li>「公車到站時間」對所有使用者來說都是一樣的。</li>
<li>公車即將抵達的時間以「分鐘」來顯示，所以我們存取的第三方 API 的頻率不用太高。</li>
<li>公車的線路非常多，但不會每條線路都有剛好使用者在查。</li>
</ol>
<p>我們可以根據特點來設計資料更新的方案：</p>
<ol>
<li>我們並不需要輪詢所有的路線，因為很多資料我們就算取得，也沒有使用者會需要。</li>
<li>因為即將抵達的時間以「分鐘」顯示，所以我們只要把更新的頻率設計在 1 分鐘以下即可（ex：5 秒鐘）。</li>
<li>更新的策略可以採取「若 Redis 有資料就直接採用，若無則呼叫第三方 API 獲取資料」。</li>
</ol>
<blockquote>
<p>筆者之前有分享過用 Redis 解決「商品秒殺」的應用，有興趣的讀者可以參考<a href="https://deanlin.net/blog/202108/nodejs-redis-flash-sale-concurrency/">這篇文章</a>。</p>
</blockquote>
<hr>
<h2 id="專案環境設置">專案環境設置<a class="h-anchor" href="#%e5%b0%88%e6%a1%88%e7%92%b0%e5%a2%83%e8%a8%ad%e7%bd%ae" aria-label="這個段落的連結"></a></h2>
<p>這個小專案我們會使用到如下套件：</p>
<ul>
<li>koa：可以理解為 Express.js 的精簡版後端框架。</li>
<li>@koa/router：路由器設定用的，看起來會比較直觀。</li>
<li>ioredis：與 Redis 連線、溝通的套件。</li>
<li>axios：HTTP 請求工具，我們透過它來 Call API。</li>
</ul>
<p>使用如下指令便可「建立專案資料夾＆初始化專案＆安裝套件」：</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>mkdir redis-cache-example
</span></span><span style="display:flex;"><span>cd redis-cache-example
</span></span><span style="display:flex;"><span>npm init -y
</span></span><span style="display:flex;"><span>npm install --save koa @koa/router ioredis axios
</span></span></code></pre></div><hr>
<h2 id="找個方便使用的第三方-api">找個方便使用的第三方 API<a class="h-anchor" href="#%e6%89%be%e5%80%8b%e6%96%b9%e4%be%bf%e4%bd%bf%e7%94%a8%e7%9a%84%e7%ac%ac%e4%b8%89%e6%96%b9-api" aria-label="這個段落的連結"></a></h2>
<p>本專案的主要目的，是讓大家理解如何將 API 回傳的資料快取。</p>
<p>所以這邊筆者選擇<a href="https://jsonplaceholder.typicode.com/">JSONPlaceholder</a>的服務，他有提供虛擬的 RESTful API（重點是不用申請 Key 方便很多）。</p>
<figure class="figure"><img src="https://deanlin.net/blog/202210/nodejs-redis-api-cache/images/nodejs-redis-api-cache-02.webp" width="1400" height="1027" alt="在 Node.js 使用 Redis 來 Cache API 的回傳資料，以減少查詢資料的回傳時間（圖 1）" loading="lazy" decoding="async">
  <figcaption><a href="https://jsonplaceholder.typicode.com/todos">https://jsonplaceholder.typicode.com/todos</a></figcaption>
</figure>

<p>上圖是 API 的回傳格式，大家可以看到，每個物件都有「id」的欄位，我們的程式會使用到這個參數來做 Demo（最多只有到 200，超過會回傳錯誤訊息）。</p>
<hr>
<h2 id="專案程式">專案程式<a class="h-anchor" href="#%e5%b0%88%e6%a1%88%e7%a8%8b%e5%bc%8f" aria-label="這個段落的連結"></a></h2>
<p>本篇文章的目的是讓讀者理解 Redis 對效能有多大的提升，因此程式架構超級簡單，大概分成三個部分：</p>
<ol>
<li>將相關套件引入（koa、@koa/router、ioredis、axios）</li>
<li>Redis 資料庫連線</li>
<li>Get Method 的 RESTful API</li>
</ol>
<p>而在 Get Method 存取 Redis 資料庫的邏輯為：</p>
<ol>
<li>先去 Redis 確認是否有快取，如果有就直接回傳。</li>
<li>如果 Redis 沒資料，就要 Call API，並將第一次獲得的資料儲存到 Redis 裡面（資料過期時間設定為 5 秒鐘）。</li>
</ol>
<p>為了方便讀者實作，筆者下面直接提供完整程式，你只要在專案資料夾下新增 <code>index.js</code>，然後複製貼上程式即可：</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#ff7b72">const</span> Koa <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;koa&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> Router <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;@koa/router&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> Redis <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;ioredis&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> axios <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;axios&#39;</span>)
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> app <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">new</span> Koa();
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> router <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">new</span> Router();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> redis <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">new</span> Redis({
</span></span><span style="display:flex;"><span>  host<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;127.0.0.1&#39;</span>,
</span></span><span style="display:flex;"><span>  port<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">6379</span>,
</span></span><span style="display:flex;"><span>  password<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;&#39;</span>
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>redis.on(<span style="color:#a5d6ff">&#34;error&#34;</span>, <span style="color:#ff7b72">function</span> (error) {
</span></span><span style="display:flex;"><span>  console.error(error);
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>router.get(<span style="color:#a5d6ff">&#39;/todos/:id&#39;</span>, <span style="color:#ff7b72">async</span> ctx =&gt; {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">try</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> cacheTodos <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> redis.get(<span style="color:#a5d6ff">`todos:</span><span style="color:#a5d6ff">${</span>ctx.params.id<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">if</span> (cacheTodos) {
</span></span><span style="display:flex;"><span>      console.log(<span style="color:#a5d6ff">&#34;從 Redis 獲得資料&#34;</span>);
</span></span><span style="display:flex;"><span>      ctx.body <span style="color:#ff7b72;font-weight:bold">=</span> JSON.parse(cacheTodos);
</span></span><span style="display:flex;"><span>      <span style="color:#ff7b72">return</span>;
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// 呼叫 API
</span></span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> response <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> axios.get(<span style="color:#a5d6ff">`https://jsonplaceholder.typicode.com/todos/</span><span style="color:#a5d6ff">${</span>ctx.params.id<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>);
</span></span><span style="display:flex;"><span>    console.log(<span style="color:#a5d6ff">&#34;將第一次獲得的資料儲存到 Redis&#34;</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// 把 API 回傳結果放入 Redis，並設定 5 秒的過期時間（Expire Time）
</span></span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">await</span> redis.set(<span style="color:#a5d6ff">`todos:</span><span style="color:#a5d6ff">${</span>ctx.params.id<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>, JSON.stringify(response.data), <span style="color:#a5d6ff">&#34;EX&#34;</span>, <span style="color:#a5d6ff">5</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    ctx.body <span style="color:#ff7b72;font-weight:bold">=</span> response.data;
</span></span><span style="display:flex;"><span>  } <span style="color:#ff7b72">catch</span> (err) {
</span></span><span style="display:flex;"><span>    ctx.body <span style="color:#ff7b72;font-weight:bold">=</span> err;
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>app.use(router.routes());
</span></span><span style="display:flex;"><span>app.listen(<span style="color:#a5d6ff">3000</span>);
</span></span></code></pre></div><p class="code-source">原始碼：<a href="https://gist.github.com/dean9703111/c3cc905908e578703631d482641106cc" rel="noopener">redis-cache.js</a>（GitHub Gist）</p>
<hr>
<h2 id="測試程式確認結果是否符合預期">測試程式，確認結果是否符合預期<a class="h-anchor" href="#%e6%b8%ac%e8%a9%a6%e7%a8%8b%e5%bc%8f%e7%a2%ba%e8%aa%8d%e7%b5%90%e6%9e%9c%e6%98%af%e5%90%a6%e7%ac%a6%e5%90%88%e9%a0%90%e6%9c%9f" aria-label="這個段落的連結"></a></h2>
<p>STEP 1：先在終端機輸入 <code>redis-server</code>，啟動 Redis Server。</p>
<p><img src="https://deanlin.net/blog/202210/nodejs-redis-api-cache/images/nodejs-redis-api-cache-03.webp" width="1400" height="1098" alt="測試程式，確認結果是否符合預期（圖 3）" loading="lazy" decoding="async"></p>
<p>STEP 2：輸入 <code>node index.js</code> 運行專案。</p>
<p>STEP 3：打開 POSTMAN，實際體驗有了 Redis Cache API 的資料後，減少了多少回傳時間。</p>
<figure class="figure"><img src="https://deanlin.net/blog/202210/nodejs-redis-api-cache/images/nodejs-redis-api-cache-04.webp" width="1246" height="631" alt="第一次存取 API，需要等待「626ms」才會回傳。" loading="lazy" decoding="async">
  <figcaption>第一次存取 API，需要等待「626ms」才會回傳。</figcaption>
</figure>

<figure class="figure"><img src="https://deanlin.net/blog/202210/nodejs-redis-api-cache/images/nodejs-redis-api-cache-05.webp" width="1246" height="631" alt="第二次因為資料已經儲存到 Redis，我們等待的時間縮減為「4ms」，是毫秒級的回傳！" loading="lazy" decoding="async">
  <figcaption>第二次因為資料已經儲存到 Redis，我們等待的時間縮減為「4ms」，是毫秒級的回傳！</figcaption>
</figure>

<p>如果想感受差異，可以看看下面筆者錄製的 Gif，裡面也有呈現過 5 秒就要重新抓取資料的邏輯。</p>
<figure class="figure figure-video">
  <video class="gifv" muted loop playsinline controls preload="none" poster="https://deanlin.net/blog/202210/nodejs-redis-api-cache/images/nodejs-redis-api-cache-06.webp" width="960" height="317" aria-label="測試程式，確認結果是否符合預期（圖 6）" data-gifv>
    <source src="https://deanlin.net/blog/202210/nodejs-redis-api-cache/images/nodejs-redis-api-cache-06.mp4" type="video/mp4">
  </video>
</figure>

<hr>
<h2 id="結語">結語<a class="h-anchor" href="#%e7%b5%90%e8%aa%9e" aria-label="這個段落的連結"></a></h2>
<p>本篇文章的重點在於「Cache 的思路」，Redis 只是協助我們達成目標的工具，即使改用 Memcached 也能達到相同目的。</p>
<p>希望這個簡單的範例讓大家對 Redis 的應用有更近一步的認識，也歡迎大家留言分享自己的應用喔！</p>
]]></content:encoded>
    </item>
    <item>
      <title>Table Join 時，選擇文字欄位（VARCHAR）與數值欄位（INTEGER）在效能上的差異</title>
      <link>https://deanlin.net/blog/202205/join-performance-varchar-vs-integer/</link>
      <pubDate>Fri, 13 May 2022 15:22:40 +0800</pubDate>
      <guid>https://deanlin.net/blog/202205/join-performance-varchar-vs-integer/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>因為剛好朋友在專案的搜尋上遇到效能瓶頸，在看過程式碼後，發現他在 Table 做 Join 時都是挑選文字欄位，因此研判這應該就是導致效率低下的主要原因；為了實驗文字欄位（VARCHAR）與數值欄位（INTEGER）在 Join…</description>
      <media:content url="https://deanlin.net/blog/202205/join-performance-varchar-vs-integer/images/join-performance-varchar-vs-integer-og_hu_70ef2de9769a14b2.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<p>因為剛好朋友在專案的搜尋上遇到效能瓶頸，在看過程式碼後，發現他在 Table 做 Join 時都是挑選文字欄位，因此研判這應該就是導致效率低下的主要原因；為了實驗文字欄位（VARCHAR）與數值欄位（INTEGER）在 Join 時的效能差異，特別撰寫了這篇文章，但實驗有點結果打破了我過往的認知…</p>
<p><img src="https://deanlin.net/blog/202205/join-performance-varchar-vs-integer/images/join-performance-varchar-vs-integer-01.webp" width="1233" height="652" alt="Table Join 時，選擇文字欄位（VARCHAR）與數值欄位（INTEGER）在效能上的差異（圖 1）" fetchpriority="high" decoding="async"></p>
<pre tabindex="0"><code>大綱
一、模擬情境說明
二、在 DB 建立測試測試對照組 Table
三、設計＆建立模擬資料
四、設計測試程式
五、模擬每個情境，取得測試結果
六、令人意外的總結
</code></pre><blockquote>
<p>大家也可以<a href="https://github.com/dean9703111/table-join-varchar-int-performance">直接 Clone 筆者 GitHub 上的專案</a>來了解喔。（如果順便按個 Star更好😀）</p>
</blockquote>
<h2 id="一模擬情境說明">一、模擬情境說明<a class="h-anchor" href="#%e4%b8%80%e6%a8%a1%e6%93%ac%e6%83%85%e5%a2%83%e8%aa%aa%e6%98%8e" aria-label="這個段落的連結"></a></h2>
<p>假設一個商城系統：</p>
<ul>
<li>有 1W 個顧客</li>
<li>每位顧客有 10 筆訂單（10W 筆訂單）</li>
<li>每筆訂單有 10 筆購買的商品（100W 筆購買的商品）</li>
</ul>
<p>如果今天想要搜尋某個價格區間的「商品」，有哪些「使用者」購買；使用文字欄位（ex：VARCHAR）與數值欄位（ex：INT），在效能上實際會有多少差異。</p>
<hr>
<h2 id="二在-db-建立測試測試對照組-table">二、在 DB 建立測試測試對照組 Table<a class="h-anchor" href="#%e4%ba%8c%e5%9c%a8-db-%e5%bb%ba%e7%ab%8b%e6%b8%ac%e8%a9%a6%e6%b8%ac%e8%a9%a6%e5%b0%8d%e7%85%a7%e7%b5%84-table" aria-label="這個段落的連結"></a></h2>
<p>如果想知道詳細的程式，或是打算在 Local 端模擬一樣的環境，大家可以參考我在<a href="https://github.com/dean9703111/table-join-varchar-int-performance">GitHub 上面的專案</a></p>
<p>這裡我們分成 3 種情境來做測試：</p>
<p><strong>情境 A</strong>： 將 Join 的欄位設計 Foreign Key，搜尋用到的欄位也設計 Index<br><strong>情境 B</strong>： 將 Foreign Key 的設計拿掉，改為使用 Index<br><strong>情境 C</strong>： 把 Index 全部拿掉，來看看到底有多慘</p>
<p>DB 我選擇的是 MySQL，而 Table 的設計如下：</p>
<h3 id="-users顧客">users（顧客）<a class="h-anchor" href="#-users%e9%a1%a7%e5%ae%a2" aria-label="這個段落的連結"></a></h3>
<a class="linkcard" href="https://sheetsu.com/tables/b96042fd7f" target="_blank" rel="noopener">
  <span class="lc-body">
    <span class="lc-title">Table</span>
    <span class="lc-site"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M7 17 17 7M8.5 7H17v8.5"/></svg>sheetsu.com</span>
  </span>
</a>

<h3 id="-orders訂單">orders（訂單）<a class="h-anchor" href="#-orders%e8%a8%82%e5%96%ae" aria-label="這個段落的連結"></a></h3>
<a class="linkcard" href="https://sheetsu.com/tables/74b89104e6" target="_blank" rel="noopener">
  <span class="lc-body">
    <span class="lc-title">Table</span>
    <span class="lc-site"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M7 17 17 7M8.5 7H17v8.5"/></svg>sheetsu.com</span>
  </span>
</a>

<p>設計「user_id、user_name」是為了對比文字欄位（ex：VARCHAR）與數值欄位（ex：INTEGER）的差異，<strong>皆有建立 Foreign Key</strong>。</p>
<h3 id="-items購買的商品">items（購買的商品）<a class="h-anchor" href="#-items%e8%b3%bc%e8%b2%b7%e7%9a%84%e5%95%86%e5%93%81" aria-label="這個段落的連結"></a></h3>
<a class="linkcard" href="https://sheetsu.com/tables/5417e3d700" target="_blank" rel="noopener">
  <span class="lc-body">
    <span class="lc-title">Table</span>
    <span class="lc-site"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M7 17 17 7M8.5 7H17v8.5"/></svg>sheetsu.com</span>
  </span>
</a>

<p>設計「order_id、order_sn」是為了對比文字欄位（ex：VARCHAR）與數值欄位（ex：INTEGER）的差異，<strong>皆有建立 Foreign Key</strong>。</p>
<p>可用如下指令建立資料：</p>
<ul>
<li>Clone 專案：<code>git clone <a href="mailto:git@github.com">git@github.com</a>:dean9703111/table-join-varchar-int-performance.git</code></li>
<li>安裝套件：<code>npm install</code></li>
<li>安裝 sequelize cli：<code>npm install -g sequelize-cli</code></li>
<li>將 config 資料夾底下的 config.exmaple.json 複製為 config.json，並填入自己的 DB 資訊</li>
<li>建立 DB：<code>sequelize db:create</code></li>
<li>執行 migration 建立 Tables：<code>sequelize db:migrate</code></li>
</ul>
<blockquote>
<p>資料都是使用 Node.js 搭配 sequelize 這款套件來建立的，如果想了解詳細使用方式，可以參考我先前的的<a href="https://deanlin.net/blog/202205/sequelize-migration-seeder/">文章</a>。</p>
</blockquote>
<figure class="figure"><img src="https://deanlin.net/blog/202205/join-performance-varchar-vs-integer/images/join-performance-varchar-vs-integer-02.webp" width="1204" height="240" alt="建立測試 DB" loading="lazy" decoding="async">
  <figcaption>建立測試 DB</figcaption>
</figure>

<figure class="figure"><img src="https://deanlin.net/blog/202205/join-performance-varchar-vs-integer/images/join-performance-varchar-vs-integer-03.webp" width="1204" height="1354" alt="建立測試各種情境的 Table" loading="lazy" decoding="async">
  <figcaption>建立測試各種情境的 Table</figcaption>
</figure>

<blockquote>
<p>在沒設計 Index 的 Table（users/orders） 對照組中，會新增 user_id/order_id 的欄位，此欄位內容與 id 欄位相同，單純用來測試效能用的。</p>
</blockquote>
<hr>
<h2 id="三設計建立模擬資料">三、設計＆建立模擬資料<a class="h-anchor" href="#%e4%b8%89%e8%a8%ad%e8%a8%88%e5%bb%ba%e7%ab%8b%e6%a8%a1%e6%93%ac%e8%b3%87%e6%96%99" aria-label="這個段落的連結"></a></h2>
<p>我們需要設計 3 個 Seeder，將期望的資料塞入：</p>
<p><strong>STEP 1</strong>：建立 1W 個顧客<br><strong>STEP 2</strong>：為每位顧客建立 10 筆訂單（user_id、user_name 需與 user Table 關聯）<br><strong>STEP 3</strong>：為每筆訂單建立 10 筆購買的商品（order_id、order_sn 需與 order Table 關聯）</p>
<p>執行全部 Seeder：</p>
<pre tabindex="0"><code>sequelize db:seed:all
</code></pre><p><img src="https://deanlin.net/blog/202205/join-performance-varchar-vs-integer/images/join-performance-varchar-vs-integer-04.webp" width="1204" height="558" alt="三、設計＆建立模擬資料（圖 4）" loading="lazy" decoding="async"></p>
<blockquote>
<p>因為沒建立 Index 的 Table 搜尋效能比想像的更加悲劇，所以在這個情境下將 User 的數量減為 1/10，也就是說購買商品(item)的數量只有 10W 筆。</p>
</blockquote>
<hr>
<h2 id="四設計測試程式">四、設計測試程式<a class="h-anchor" href="#%e5%9b%9b%e8%a8%ad%e8%a8%88%e6%b8%ac%e8%a9%a6%e7%a8%8b%e5%bc%8f" aria-label="這個段落的連結"></a></h2>
<p>我們需要設計一個可以根據上面 3 種情境測試的程式，同時用 3 種常見的查詢來比較各自的效能：</p>
<ol>
<li>單筆查詢：<code>findOne</code></li>
<li>多筆查詢：<code>findMutiple</code></li>
<li>設定條件的批量查詢 ：<code>searchRange</code> (大約會找出 1/5 的資料，用來測試匯出大量報表功能)</li>
</ol>
<p>程式放在下面，有興趣再看就好，這不是本文的重點，我知道還有很多細節可以優化 😇</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#ff7b72">const</span> { sequelize } <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#34;./models&#34;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> { QueryTypes } <span style="color:#ff7b72;font-weight:bold">=</span> sequelize;
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">function</span> getJoinCol (colType, setting) {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">let</span> orderJoinCol <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#34;&#34;</span>;
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">let</span> itemJoinCol <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#34;&#34;</span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">if</span> (setting <span style="color:#ff7b72;font-weight:bold">===</span> <span style="color:#a5d6ff">&#34;&#34;</span> <span style="color:#ff7b72;font-weight:bold">&amp;&amp;</span> colType <span style="color:#ff7b72;font-weight:bold">===</span> <span style="color:#a5d6ff">&#34;Int&#34;</span>) {
</span></span><span style="display:flex;"><span>        orderJoinCol <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">`orders.user_id = users.user_id`</span>
</span></span><span style="display:flex;"><span>        itemJoinCol <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">`items.order_id = orders.order_id`</span>
</span></span><span style="display:flex;"><span>    } <span style="color:#ff7b72">else</span> <span style="color:#ff7b72">if</span> (colType <span style="color:#ff7b72;font-weight:bold">===</span> <span style="color:#a5d6ff">&#34;Int&#34;</span>) {
</span></span><span style="display:flex;"><span>        orderJoinCol <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">`</span><span style="color:#a5d6ff">${</span>setting<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">orders.user_id = </span><span style="color:#a5d6ff">${</span>setting<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">users.id`</span>
</span></span><span style="display:flex;"><span>        itemJoinCol <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">`</span><span style="color:#a5d6ff">${</span>setting<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">items.order_id = </span><span style="color:#a5d6ff">${</span>setting<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">orders.id`</span>
</span></span><span style="display:flex;"><span>    } <span style="color:#ff7b72">else</span> <span style="color:#ff7b72">if</span> (colType <span style="color:#ff7b72;font-weight:bold">===</span> <span style="color:#a5d6ff">&#34;VarChar&#34;</span>) {
</span></span><span style="display:flex;"><span>        orderJoinCol <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">`</span><span style="color:#a5d6ff">${</span>setting<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">orders.user_name = </span><span style="color:#a5d6ff">${</span>setting<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">users.name`</span>
</span></span><span style="display:flex;"><span>        itemJoinCol <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">`</span><span style="color:#a5d6ff">${</span>setting<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">items.order_sn = </span><span style="color:#a5d6ff">${</span>setting<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">orders.sn`</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">return</span> { <span style="color:#a5d6ff">&#34;orderJoinCol&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> orderJoinCol, <span style="color:#a5d6ff">&#34;itemJoinCol&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> itemJoinCol }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">async</span> <span style="color:#ff7b72">function</span> findOne (colType, setting) {
</span></span><span style="display:flex;"><span>    console.time(<span style="color:#a5d6ff">`</span><span style="color:#a5d6ff">${</span>colType<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">-</span><span style="color:#a5d6ff">${</span>setting<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">FindOne`</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">let</span> {orderJoinCol,itemJoinCol} <span style="color:#ff7b72;font-weight:bold">=</span> getJoinCol (colType, setting)
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> results <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> sequelize.query(<span style="color:#a5d6ff">`SELECT 
</span></span></span><span style="display:flex;"><span><span style="color:#a5d6ff">           </span><span style="color:#a5d6ff">${</span>setting<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">users.name, </span><span style="color:#a5d6ff">${</span>setting<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">items.price FROM </span><span style="color:#a5d6ff">${</span>setting<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">users
</span></span></span><span style="display:flex;"><span><span style="color:#a5d6ff">           LEFT JOIN </span><span style="color:#a5d6ff">${</span>setting<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">orders ON </span><span style="color:#a5d6ff">${</span>orderJoinCol<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">
</span></span></span><span style="display:flex;"><span><span style="color:#a5d6ff">           LEFT JOIN </span><span style="color:#a5d6ff">${</span>setting<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">items ON </span><span style="color:#a5d6ff">${</span>itemJoinCol<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">
</span></span></span><span style="display:flex;"><span><span style="color:#a5d6ff">           WHERE </span><span style="color:#a5d6ff">${</span>setting<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">items.name = &#39;Dumpitem5000-10-10&#39;`</span>, { type<span style="color:#ff7b72;font-weight:bold">:</span> QueryTypes.SELECT });
</span></span><span style="display:flex;"><span>    console.timeEnd(<span style="color:#a5d6ff">`</span><span style="color:#a5d6ff">${</span>colType<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">-</span><span style="color:#a5d6ff">${</span>setting<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">FindOne`</span>);
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">async</span> <span style="color:#ff7b72">function</span> findMutiple (colType, setting) {
</span></span><span style="display:flex;"><span>    console.time(<span style="color:#a5d6ff">`</span><span style="color:#a5d6ff">${</span>colType<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">-</span><span style="color:#a5d6ff">${</span>setting<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">FindMutiple`</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">let</span> {orderJoinCol,itemJoinCol} <span style="color:#ff7b72;font-weight:bold">=</span> getJoinCol (colType, setting)
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> results <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> sequelize.query(<span style="color:#a5d6ff">`SELECT 
</span></span></span><span style="display:flex;"><span><span style="color:#a5d6ff">           </span><span style="color:#a5d6ff">${</span>setting<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">users.name, </span><span style="color:#a5d6ff">${</span>setting<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">items.price FROM </span><span style="color:#a5d6ff">${</span>setting<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">users
</span></span></span><span style="display:flex;"><span><span style="color:#a5d6ff">           LEFT JOIN </span><span style="color:#a5d6ff">${</span>setting<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">orders ON </span><span style="color:#a5d6ff">${</span>orderJoinCol<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">
</span></span></span><span style="display:flex;"><span><span style="color:#a5d6ff">           LEFT JOIN </span><span style="color:#a5d6ff">${</span>setting<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">items ON </span><span style="color:#a5d6ff">${</span>itemJoinCol<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">
</span></span></span><span style="display:flex;"><span><span style="color:#a5d6ff">           WHERE </span><span style="color:#a5d6ff">${</span>setting<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">items.name in 
</span></span></span><span style="display:flex;"><span><span style="color:#a5d6ff">            (&#39;Dumpitem3000-10-10&#39;,&#39;Dumpitem5000-10-10&#39;,&#39;Dumpitem7500-10-10&#39;)`</span>
</span></span><span style="display:flex;"><span>        , { type<span style="color:#ff7b72;font-weight:bold">:</span> QueryTypes.SELECT });
</span></span><span style="display:flex;"><span>    console.timeEnd(<span style="color:#a5d6ff">`</span><span style="color:#a5d6ff">${</span>colType<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">-</span><span style="color:#a5d6ff">${</span>setting<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">FindMutiple`</span>);
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">async</span> <span style="color:#ff7b72">function</span> searchRange (colType, setting) {
</span></span><span style="display:flex;"><span>    console.time(<span style="color:#a5d6ff">`</span><span style="color:#a5d6ff">${</span>colType<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">-</span><span style="color:#a5d6ff">${</span>setting<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">SearchRange`</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">let</span> {orderJoinCol,itemJoinCol} <span style="color:#ff7b72;font-weight:bold">=</span> getJoinCol (colType, setting)
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> results <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> sequelize.query(<span style="color:#a5d6ff">`SELECT 
</span></span></span><span style="display:flex;"><span><span style="color:#a5d6ff">           </span><span style="color:#a5d6ff">${</span>setting<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">users.name, </span><span style="color:#a5d6ff">${</span>setting<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">items.price FROM </span><span style="color:#a5d6ff">${</span>setting<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">users
</span></span></span><span style="display:flex;"><span><span style="color:#a5d6ff">           LEFT JOIN </span><span style="color:#a5d6ff">${</span>setting<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">orders ON </span><span style="color:#a5d6ff">${</span>orderJoinCol<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">
</span></span></span><span style="display:flex;"><span><span style="color:#a5d6ff">           LEFT JOIN </span><span style="color:#a5d6ff">${</span>setting<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">items ON </span><span style="color:#a5d6ff">${</span>itemJoinCol<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">
</span></span></span><span style="display:flex;"><span><span style="color:#a5d6ff">           WHERE </span><span style="color:#a5d6ff">${</span>setting<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">items.price &lt; 80`</span>, { type<span style="color:#ff7b72;font-weight:bold">:</span> QueryTypes.SELECT });
</span></span><span style="display:flex;"><span>    console.timeEnd(<span style="color:#a5d6ff">`</span><span style="color:#a5d6ff">${</span>colType<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">-</span><span style="color:#a5d6ff">${</span>setting<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">SearchRange`</span>);
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">function</span> test (type) {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">switch</span> (type) {
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">case</span> <span style="color:#a5d6ff">&#39;foreign&#39;</span><span style="color:#ff7b72;font-weight:bold">:</span>
</span></span><span style="display:flex;"><span>            findOne(<span style="color:#a5d6ff">&#34;Int&#34;</span>, <span style="color:#a5d6ff">&#34;foreign_&#34;</span>);
</span></span><span style="display:flex;"><span>            findMutiple(<span style="color:#a5d6ff">&#34;Int&#34;</span>, <span style="color:#a5d6ff">&#34;foreign_&#34;</span>);
</span></span><span style="display:flex;"><span>            searchRange(<span style="color:#a5d6ff">&#34;Int&#34;</span>, <span style="color:#a5d6ff">&#34;foreign_&#34;</span>);
</span></span><span style="display:flex;"><span>            findOne(<span style="color:#a5d6ff">&#34;VarChar&#34;</span>, <span style="color:#a5d6ff">&#34;foreign_&#34;</span>);
</span></span><span style="display:flex;"><span>            findMutiple(<span style="color:#a5d6ff">&#34;VarChar&#34;</span>, <span style="color:#a5d6ff">&#34;foreign_&#34;</span>);
</span></span><span style="display:flex;"><span>            searchRange(<span style="color:#a5d6ff">&#34;VarChar&#34;</span>, <span style="color:#a5d6ff">&#34;foreign_&#34;</span>);
</span></span><span style="display:flex;"><span>            <span style="color:#ff7b72">break</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">case</span> <span style="color:#a5d6ff">&#39;index&#39;</span><span style="color:#ff7b72;font-weight:bold">:</span>
</span></span><span style="display:flex;"><span>            findOne(<span style="color:#a5d6ff">&#34;Int&#34;</span>, <span style="color:#a5d6ff">&#34;index_&#34;</span>);
</span></span><span style="display:flex;"><span>            findMutiple(<span style="color:#a5d6ff">&#34;Int&#34;</span>, <span style="color:#a5d6ff">&#34;index_&#34;</span>);
</span></span><span style="display:flex;"><span>            searchRange(<span style="color:#a5d6ff">&#34;Int&#34;</span>, <span style="color:#a5d6ff">&#34;index_&#34;</span>);
</span></span><span style="display:flex;"><span>            findOne(<span style="color:#a5d6ff">&#34;VarChar&#34;</span>, <span style="color:#a5d6ff">&#34;index_&#34;</span>);
</span></span><span style="display:flex;"><span>            findMutiple(<span style="color:#a5d6ff">&#34;VarChar&#34;</span>, <span style="color:#a5d6ff">&#34;index_&#34;</span>);
</span></span><span style="display:flex;"><span>            searchRange(<span style="color:#a5d6ff">&#34;VarChar&#34;</span>, <span style="color:#a5d6ff">&#34;index_&#34;</span>);
</span></span><span style="display:flex;"><span>            <span style="color:#ff7b72">break</span>;
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">default</span><span style="color:#ff7b72;font-weight:bold">:</span>
</span></span><span style="display:flex;"><span>            findOne(<span style="color:#a5d6ff">&#34;Int&#34;</span>, <span style="color:#a5d6ff">&#34;&#34;</span>);
</span></span><span style="display:flex;"><span>            findMutiple(<span style="color:#a5d6ff">&#34;Int&#34;</span>, <span style="color:#a5d6ff">&#34;&#34;</span>);
</span></span><span style="display:flex;"><span>            searchRange(<span style="color:#a5d6ff">&#34;Int&#34;</span>, <span style="color:#a5d6ff">&#34;&#34;</span>);
</span></span><span style="display:flex;"><span>            findOne(<span style="color:#a5d6ff">&#34;VarChar&#34;</span>, <span style="color:#a5d6ff">&#34;&#34;</span>);
</span></span><span style="display:flex;"><span>            findMutiple(<span style="color:#a5d6ff">&#34;VarChar&#34;</span>, <span style="color:#a5d6ff">&#34;&#34;</span>);
</span></span><span style="display:flex;"><span>            searchRange(<span style="color:#a5d6ff">&#34;VarChar&#34;</span>, <span style="color:#a5d6ff">&#34;&#34;</span>);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// test(&#34;foreign&#34;)//有建立 foreign Key 的
</span></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// test(&#34;index&#34;)//有建立 index 的
</span></span></span><span style="display:flex;"><span>test()<span style="color:#8b949e;font-style:italic">//什麼都沒有設定的
</span></span></span></code></pre></div><p class="code-source">原始碼：<a href="https://gist.github.com/dean9703111/30fa6b0419a8ffd10886ab7aba9921f5" rel="noopener">table-join-index.js</a>（GitHub Gist）</p>
<hr>
<h2 id="五模擬每個情境取得測試結果">五、模擬每個情境，取得測試結果<a class="h-anchor" href="#%e4%ba%94%e6%a8%a1%e6%93%ac%e6%af%8f%e5%80%8b%e6%83%85%e5%a2%83%e5%8f%96%e5%be%97%e6%b8%ac%e8%a9%a6%e7%b5%90%e6%9e%9c" aria-label="這個段落的連結"></a></h2>
<h3 id="-情境-a-將-join-的欄位設計-foreign-key搜尋用到的欄位也設計-index">情境 A： 將 Join 的欄位設計 Foreign Key，搜尋用到的欄位也設計 Index<a class="h-anchor" href="#-%e6%83%85%e5%a2%83-a-%e5%b0%87-join-%e7%9a%84%e6%ac%84%e4%bd%8d%e8%a8%ad%e8%a8%88-foreign-key%e6%90%9c%e5%b0%8b%e7%94%a8%e5%88%b0%e7%9a%84%e6%ac%84%e4%bd%8d%e4%b9%9f%e8%a8%ad%e8%a8%88-index" aria-label="這個段落的連結"></a></h3>
<ul>
<li>ER Diagram</li>
</ul>
<figure class="figure"><img src="https://deanlin.net/blog/202205/join-performance-varchar-vs-integer/images/join-performance-varchar-vs-integer-05.webp" width="954" height="555" alt="以 foreign 為開頭的 Table" loading="lazy" decoding="async">
  <figcaption>以 foreign 為開頭的 Table</figcaption>
</figure>

<blockquote>
<p>小知識 : 如果 Foreign Key關聯的欄位為 Primary Key，它會自動建立 Index。</p>
</blockquote>
<ul>
<li>執行程式</li>
</ul>
<p><img src="https://deanlin.net/blog/202205/join-performance-varchar-vs-integer/images/join-performance-varchar-vs-integer-06.webp" width="860" height="910" alt="情境 A： 將 Join 的欄位設計 Foreign Key，搜尋用到的欄位也設計 Index（圖 6）" loading="lazy" decoding="async"></p>
<p><strong>測試數據：</strong></p>
<ul>
<li>INTEGER 單筆查詢：787.003ms</li>
<li>INTEGER 多筆查詢：773.931ms</li>
<li>INTEGER 設定條件的批量查詢：3.555s</li>
<li>VARCHAR 單筆查詢：767.987ms</li>
<li>VARCHAR 多筆查詢：773.861ms</li>
<li>VARCHAR 設定條件的批量查詢：6.049s</li>
</ul>
<h3 id="-情境-b-將-foreign-key-的設計拿掉改為使用-index">情境 B： 將 Foreign Key 的設計拿掉，改為使用 Index<a class="h-anchor" href="#-%e6%83%85%e5%a2%83-b-%e5%b0%87-foreign-key-%e7%9a%84%e8%a8%ad%e8%a8%88%e6%8b%bf%e6%8e%89%e6%94%b9%e7%82%ba%e4%bd%bf%e7%94%a8-index" aria-label="這個段落的連結"></a></h3>
<ul>
<li>ER Diagram</li>
</ul>
<figure class="figure"><img src="https://deanlin.net/blog/202205/join-performance-varchar-vs-integer/images/join-performance-varchar-vs-integer-07.webp" width="860" height="457" alt="以 Index 為開頭的 Table" loading="lazy" decoding="async">
  <figcaption>以 Index 為開頭的 Table</figcaption>
</figure>

<ul>
<li>執行程式</li>
</ul>
<p><img src="https://deanlin.net/blog/202205/join-performance-varchar-vs-integer/images/join-performance-varchar-vs-integer-08.webp" width="860" height="913" alt="情境 B： 將 Foreign Key 的設計拿掉，改為使用 Index（圖 8）" loading="lazy" decoding="async"></p>
<p><strong>測試數據：</strong></p>
<ul>
<li>INTEGER 單筆查詢：464.168ms</li>
<li>INTEGER 多筆查詢：540.567ms</li>
<li>INTEGER 設定條件的批量查詢：2.624s</li>
<li>VARCHAR 單筆查詢：447.98ms</li>
<li>VARCHAR 多筆查詢：549.225ms</li>
<li>VARCHAR 設定條件的批量查詢：4.824s</li>
</ul>
<h3 id="-情境-c-把-index-全部拿掉來看看到底有多慘">情境 C： 把 Index 全部拿掉，來看看到底有多慘<a class="h-anchor" href="#-%e6%83%85%e5%a2%83-c-%e6%8a%8a-index-%e5%85%a8%e9%83%a8%e6%8b%bf%e6%8e%89%e4%be%86%e7%9c%8b%e7%9c%8b%e5%88%b0%e5%ba%95%e6%9c%89%e5%a4%9a%e6%85%98" aria-label="這個段落的連結"></a></h3>
<ul>
<li>ER Diagram</li>
</ul>
<figure class="figure"><img src="https://deanlin.net/blog/202205/join-performance-varchar-vs-integer/images/join-performance-varchar-vs-integer-09.webp" width="852" height="398" alt="什麼都沒設計，僅有自動生成的 Primary Key" loading="lazy" decoding="async">
  <figcaption>什麼都沒設計，僅有自動生成的 Primary Key</figcaption>
</figure>

<ul>
<li>執行程式</li>
</ul>
<p><img src="https://deanlin.net/blog/202205/join-performance-varchar-vs-integer/images/join-performance-varchar-vs-integer-10.webp" width="852" height="909" alt="情境 C： 把 Index 全部拿掉，來看看到底有多慘（圖 10）" loading="lazy" decoding="async"></p>
<p><strong>測試數據</strong>：</p>
<ul>
<li>INTEGER 單筆查詢：1.217s</li>
<li>INTEGER 多筆查詢：1.205s</li>
<li>INTEGER 設定條件的批量查詢：3:08.252 (m:ss.mmm)</li>
<li>VARCHAR 單筆查詢：1.755s</li>
<li>VARCHAR 多筆查詢：1.964s</li>
<li>VARCHAR 設定條件的批量查詢：44.285s</li>
</ul>
<hr>
<h2 id="六令人意外的總結">六、令人意外的總結<a class="h-anchor" href="#%e5%85%ad%e4%bb%a4%e4%ba%ba%e6%84%8f%e5%a4%96%e7%9a%84%e7%b8%bd%e7%b5%90" aria-label="這個段落的連結"></a></h2>
<p>這邊先列出比較表，方便大家理解：</p>
<a class="linkcard" href="https://sheetsu.com/tables/64262891c4" target="_blank" rel="noopener">
  <span class="lc-body">
    <span class="lc-title">Table</span>
    <span class="lc-site"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M7 17 17 7M8.5 7H17v8.5"/></svg>sheetsu.com</span>
  </span>
</a>

<p>在這張表裡面有我預料的結果，也有我沒想到的結果，我歸類如下：</p>
<ul>
<li>批量查詢時，如果有設定 Foreign Key、Index，INTEGER 的搜尋效率優於 VARCHAR。</li>
<li>在有設定 Foreign Key、Index，那單筆、多筆查詢的效率是差不多的，跟使用 INTEGER 還是 VARCHAR 關聯性不大。</li>
<li>Foreign Key 的搜尋效率略低於 Index（可能是我在資料表上設計的不足）。</li>
<li>如果沒建立 Index，那搜尋效率慘不忍睹（即使資料只有 1/10，效能還是超悲劇）。</li>
<li>如果沒建立 Index，在批量查詢時，INTEGER 查詢效率低於 VARCHAR（這部分我真的搞不懂為什麼）。</li>
</ul>
<p>老實說，這個實驗結果反而把我弄得有點迷糊，也許還要從更多面向做測試，追尋知識的道路真的任重道遠🤕</p>
<blockquote>
<p>本篇是從「搜尋花費時間」的角度來實驗，歡迎高手從硬體消耗資源的角度來實驗。</p>
</blockquote>
]]></content:encoded>
    </item>
    <item>
      <title>初探 sequelize，在 Node.js 快速建立 Migration、Seeder（ MySQL）</title>
      <link>https://deanlin.net/blog/202205/sequelize-migration-seeder/</link>
      <pubDate>Fri, 13 May 2022 15:14:28 +0800</pubDate>
      <guid>https://deanlin.net/blog/202205/sequelize-migration-seeder/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>最近在尋找 Node.js 裡面方便操作 Migration、Seeder 的套件，逛著逛著發現 sequelize 在 GitHub 上擁有超高人氣；抱著踩雷的心態來嘗試，於是便寫了這篇 sequelize 的筆記，希望可以幫助到第一次使用 sequelize 的朋友們。</description>
      <media:content url="https://deanlin.net/blog/202205/sequelize-migration-seeder/images/sequelize-migration-seeder-og_hu_b8d9851da479dae8.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<p>最近在尋找 Node.js 裡面方便操作 Migration、Seeder 的套件，逛著逛著發現 sequelize 在 GitHub 上擁有超高人氣；抱著踩雷的心態來嘗試，於是便寫了這篇 sequelize 的筆記，希望可以幫助到第一次使用 sequelize 的朋友們。</p>
<ul>
<li><strong>我能學到什麼</strong> 🤔：sequelize 基礎使用方式。</li>
<li><strong>我能學會嗎</strong> 🤔：筆者從零開始建立專案，並附上詳細的指令與說明，相信大家都能學得開心。</li>
<li><strong>文章目標</strong> 🤩：建立一個有使用者（user）、訂單（order）、購買貨物（item）的資料庫，設計 Table 彼此的關連性，透過 Query 確認資料。</li>
</ul>
<p><img src="https://deanlin.net/blog/202205/sequelize-migration-seeder/images/sequelize-migration-seeder-01.webp" width="1238" height="658" alt="初探 sequelize，在 Node.js 快速建立 Migration、Seeder（ MySQL）（圖 1）" fetchpriority="high" decoding="async"></p>
<pre tabindex="0"><code>大綱
一、初始化專案
二、透過 Config 建立 DB
三、用 Migration 建立 Table
四、用 Seeder 建立假資料(Dummy Data)
五、簡易 Query
</code></pre><blockquote>
<p>大家也可以<a href="https://github.com/dean9703111/sequelize-mysql-migration-seeder">直接 Clone 筆者 GitHub 上的專案</a>來了解喔。（如果順便按個 Star更好😀）</p>
</blockquote>
<h2 id="ㄧ初始化專案">ㄧ、初始化專案<a class="h-anchor" href="#%e3%84%a7%e5%88%9d%e5%a7%8b%e5%8c%96%e5%b0%88%e6%a1%88" aria-label="這個段落的連結"></a></h2>
<p><strong>STEP 1</strong>：建立專案資料夾</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>mkdir sequelize-demo <span style="color:#ff7b72;font-weight:bold">&amp;&amp;</span> cd  sequelize-demo
</span></span><span style="display:flex;"><span>npm init
</span></span></code></pre></div><p><strong>STEP 2</strong>：安裝 sequelize</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>npm install -g sequelize-cli
</span></span><span style="display:flex;"><span>npm install --save sequelize
</span></span></code></pre></div><p><strong>STEP 3</strong>：安裝你想使用的 DB（本篇文章以 MySQL 為範例）</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>npm install --save mysql2 <span style="color:#8b949e;font-style:italic">#本篇文章</span>
</span></span><span style="display:flex;"><span>npm install --save pg pg-hstore <span style="color:#8b949e;font-style:italic"># Postgres</span>
</span></span><span style="display:flex;"><span>npm install --save mariadb
</span></span><span style="display:flex;"><span>npm install --save sqlite3
</span></span><span style="display:flex;"><span>npm install --save tedious <span style="color:#8b949e;font-style:italic"># Microsoft SQL Server</span>
</span></span></code></pre></div><hr>
<h2 id="二透過-config-建立-db">二、透過 Config 建立 DB<a class="h-anchor" href="#%e4%ba%8c%e9%80%8f%e9%81%8e-config-%e5%bb%ba%e7%ab%8b-db" aria-label="這個段落的連結"></a></h2>
<p><strong>STEP 1</strong>：用指令初始化相關設定檔</p>
<pre tabindex="0"><code>sequelize init
</code></pre><p><strong>STEP 2</strong>：調整 sequelize 的 <code>config/config.json</code> 檔</p>
<p>預設使用的環境都是<code>development</code>，記得調整成自己的 DB 登入參數</p>
<pre tabindex="0"><code>&#34;development&#34;: {
    &#34;username&#34;: &#34;root&#34;,
    &#34;password&#34;: &#34;baobaoverycute&#34;,
    &#34;database&#34;: &#34;sequelize_demo&#34;,
    &#34;host&#34;: &#34;127.0.0.1&#34;,
    &#34;dialect&#34;: &#34;mysql&#34;
  },
</code></pre><p><strong>STEP 3</strong>：依照剛剛設定的 <code>config/config.json</code> 來建立 DB</p>
<pre tabindex="0"><code>sequelize db:create
</code></pre><p><img src="https://deanlin.net/blog/202205/sequelize-migration-seeder/images/sequelize-migration-seeder-02.webp" width="1400" height="299" alt="二、透過 Config 建立 DB（圖 2）" loading="lazy" decoding="async"></p>
<blockquote>
<p>如果想要移除該 DB，可透過指令： <code>sequelize db:drop</code></p>
</blockquote>
<hr>
<h2 id="三用-migration-建立-table">三、用 Migration 建立 Table<a class="h-anchor" href="#%e4%b8%89%e7%94%a8-migration-%e5%bb%ba%e7%ab%8b-table" aria-label="這個段落的連結"></a></h2>
<p><strong>STEP 1</strong>：新增所需 Tables &amp; 基礎欄位</p>
<p>根據文章目標，我們需要建立 3 個 Tables：使用者（user）、訂單（order）、購買貨物（item）</p>
<pre tabindex="0"><code>sequelize model:generate --name user --attributes name:string,mail:string
sequelize model:generate --name order --attributes user_id:integer,total_price:integer
sequelize model:generate --name item --attributes order_id:integer,price:integer
</code></pre><blockquote>
<p>如果沒有填寫「attributes」的參數會無法建立 Table。</p>
</blockquote>
<p><img src="https://deanlin.net/blog/202205/sequelize-migration-seeder/images/sequelize-migration-seeder-03.webp" width="1400" height="318" alt="三、用 Migration 建立 Table（圖 3）" loading="lazy" decoding="async"></p>
<p><strong>STEP 2</strong>：前往 migrations 的資料夾，設計 Table 間的關聯性</p>
<p>調整 order Table，建立 user_id 與 user 的關聯性（一個 user 會對應到多筆 order）</p>
<pre tabindex="0"><code>user_id: {
    type: Sequelize.INTEGER,
    references: {
        model: &#39;users&#39;,
        key: &#39;id&#39;
    },
},
</code></pre><p>調整 item Table，建立 order_id 與 item 的關聯性（一個 order 會對應到多筆 item）</p>
<pre tabindex="0"><code>oder_id: {
    type: Sequelize.INTEGER,
    references: {
        model: &#39;orders&#39;,
        key: &#39;id&#39;
    },
},
</code></pre><p><strong>STEP 3</strong>：修改完成之後，就可以下指令建立剛剛設定的 Table 啦～</p>
<pre tabindex="0"><code>sequelize db:migrate
</code></pre><p><img src="https://deanlin.net/blog/202205/sequelize-migration-seeder/images/sequelize-migration-seeder-04.webp" width="1400" height="546" alt="三、用 Migration 建立 Table（圖 4）" loading="lazy" decoding="async"></p>
<p>下圖是用 MySQLWorkbench 產生的 ER Diagram</p>
<p><img src="https://deanlin.net/blog/202205/sequelize-migration-seeder/images/sequelize-migration-seeder-05.webp" width="1400" height="1022" alt="三、用 Migration 建立 Table（圖 5）" loading="lazy" decoding="async"></p>
<p>如果想要回溯 migration 可用如下指令：</p>
<ul>
<li>退一個版本：<code>sequelize db:migrate:undo</code></li>
<li>退到初始狀態：<code>sequelize db:migrate:undo:all</code></li>
<li>退到指定版本：<code>sequelize db:migrate:undo:all &ndash;to XXXXXXXXXXXXXX-create-user.js</code></li>
</ul>
<hr>
<h2 id="四用-seeder-建立假資料dummy-data">四、用 Seeder 建立假資料(Dummy Data)<a class="h-anchor" href="#%e5%9b%9b%e7%94%a8-seeder-%e5%bb%ba%e7%ab%8b%e5%81%87%e8%b3%87%e6%96%99dummy-data" aria-label="這個段落的連結"></a></h2>
<p><strong>目標</strong>：建立 100 個使用者（user），每個使用者有 10 筆訂單（order），每筆訂單下面有 10 個貨物（item）。</p>
<p><strong>STEP 1</strong>：先建立 Seeder 檔案</p>
<pre tabindex="0"><code>sequelize seed:generate --name demo-user
sequelize seed:generate --name demo-order
sequelize seed:generate --name demo-item
</code></pre><p><img src="https://deanlin.net/blog/202205/sequelize-migration-seeder/images/sequelize-migration-seeder-06.webp" width="1400" height="313" alt="四、用 Seeder 建立假資料(Dummy Data)（圖 6）" loading="lazy" decoding="async"></p>
<p><strong>STEP 2</strong>：建立各自 Seeder 資料產生的邏輯，下面以 order 舉例：</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#a5d6ff">&#34;use strict&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>module.exports <span style="color:#ff7b72;font-weight:bold">=</span> {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">async</span> up(queryInterface, Sequelize) {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">function</span> getRandomInt(max) {
</span></span><span style="display:flex;"><span>      <span style="color:#ff7b72">return</span> Math.floor(Math.random() <span style="color:#ff7b72;font-weight:bold">*</span> max);
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// 100 個使用者，每個使用者有 10 筆訂單（order）
</span></span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">var</span> orderArray <span style="color:#ff7b72;font-weight:bold">=</span> [];
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">for</span> (<span style="color:#ff7b72">let</span> i <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">1</span>; i <span style="color:#ff7b72;font-weight:bold">&lt;=</span> <span style="color:#a5d6ff">100</span>; i<span style="color:#ff7b72;font-weight:bold">++</span>) {
</span></span><span style="display:flex;"><span>      <span style="color:#ff7b72">for</span> (<span style="color:#ff7b72">let</span> j <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">1</span>; j <span style="color:#ff7b72;font-weight:bold">&lt;=</span> <span style="color:#a5d6ff">10</span>; j<span style="color:#ff7b72;font-weight:bold">++</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">const</span> order <span style="color:#ff7b72;font-weight:bold">=</span> {
</span></span><span style="display:flex;"><span>          user_id<span style="color:#ff7b72;font-weight:bold">:</span> i,
</span></span><span style="display:flex;"><span>          total_price<span style="color:#ff7b72;font-weight:bold">:</span> Math.floor(Math.random() <span style="color:#ff7b72;font-weight:bold">*</span> <span style="color:#a5d6ff">1000</span>),
</span></span><span style="display:flex;"><span>          createdAt<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#ff7b72">new</span> Date(),
</span></span><span style="display:flex;"><span>          updatedAt<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#ff7b72">new</span> Date(),
</span></span><span style="display:flex;"><span>        };
</span></span><span style="display:flex;"><span>        orderArray.push(order);
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">return</span> queryInterface.bulkInsert(<span style="color:#a5d6ff">&#34;Orders&#34;</span>, orderArray);
</span></span><span style="display:flex;"><span>  },
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">async</span> down(queryInterface, Sequelize) {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">return</span> queryInterface.bulkDelete(<span style="color:#a5d6ff">&#34;Orders&#34;</span>, <span style="color:#79c0ff">null</span>, {});
</span></span><span style="display:flex;"><span>  },
</span></span><span style="display:flex;"><span>};
</span></span></code></pre></div><p class="code-source">原始碼：<a href="https://gist.github.com/dean9703111/1c59dbb3e507a3de6c6bfab4e3ead327" rel="noopener">sequelize-order.js</a>（GitHub Gist）</p>
<p><strong>STEP 3</strong>：執行 Seeder</p>
<p>執行單個 Seeder</p>
<pre tabindex="0"><code>sequelize db:seed --seed my_seeder_file.js
</code></pre><p>執行全部 Seeder</p>
<pre tabindex="0"><code>sequelize db:seed:all
</code></pre><p><img src="https://deanlin.net/blog/202205/sequelize-migration-seeder/images/sequelize-migration-seeder-07.webp" width="1400" height="560" alt="四、用 Seeder 建立假資料(Dummy Data)（圖 7）" loading="lazy" decoding="async"></p>
<p><strong>STEP 4</strong>：前往 DB ，確認資料是否有正確寫入：</p>
<p>建立 100 個使用者（users）。</p>
<p><img src="https://deanlin.net/blog/202205/sequelize-migration-seeder/images/sequelize-migration-seeder-08.webp" width="1308" height="658" alt="四、用 Seeder 建立假資料(Dummy Data)（圖 8）" loading="lazy" decoding="async"></p>
<p>建立 100 個使用者（users），每個使用者有 10 筆訂單（orders），共 1000 筆資料。</p>
<p><img src="https://deanlin.net/blog/202205/sequelize-migration-seeder/images/sequelize-migration-seeder-09.webp" width="1308" height="658" alt="四、用 Seeder 建立假資料(Dummy Data)（圖 9）" loading="lazy" decoding="async"></p>
<p>建立 100 個使用者（users），每個使用者有 10 筆訂單（orders），每筆訂單下面有 10 個貨物（items），共 10000 筆資料。</p>
<p><img src="https://deanlin.net/blog/202205/sequelize-migration-seeder/images/sequelize-migration-seeder-10.webp" width="1308" height="700" alt="四、用 Seeder 建立假資料(Dummy Data)（圖 10）" loading="lazy" decoding="async"></p>
<p>取消執行的 Seeder 可用如下指令：</p>
<ul>
<li>取消最近的 Seeder：<code>sequelize db:seed:undo</code></li>
<li>取消指定的 Seeder：<code>sequelize db:seed:undo &ndash;seed my_seeder_file.js</code></li>
<li>取消全部 Seeder：<code>sequelize db:seed:undo:all</code></li>
</ul>
<hr>
<h2 id="五簡易-query">五、簡易 Query<a class="h-anchor" href="#%e4%ba%94%e7%b0%a1%e6%98%93-query" aria-label="這個段落的連結"></a></h2>
<p><strong>STEP 1</strong>：在專案資料夾下建立一個 <code>row-queries-exmaple.js</code> 的檔案。</p>
<p><strong>STEP 2</strong>：將如下程式複製貼上，這是一個簡單 Raw Queries；會找出 price 低於 50 的 5 筆購買貨物，以及對應的使用者。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#ff7b72">const</span> { sequelize } <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#34;./models&#34;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> { QueryTypes } <span style="color:#ff7b72;font-weight:bold">=</span> sequelize;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">async</span> <span style="color:#ff7b72">function</span> rawQueries() {
</span></span><span style="display:flex;"><span>  console.time(<span style="color:#a5d6ff">&#34;rawQueries&#34;</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">const</span> results <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> sequelize.query(<span style="color:#a5d6ff">&#34;SELECT users.name, items.price FROM users\
</span></span></span><span style="display:flex;"><span><span style="color:#a5d6ff">         LEFT JOIN orders ON orders.user_id = users.id\
</span></span></span><span style="display:flex;"><span><span style="color:#a5d6ff">         LEFT JOIN items ON items.order_id = orders.id\
</span></span></span><span style="display:flex;"><span><span style="color:#a5d6ff">         WHERE items.price &lt; 50 LIMIT 5&#34;</span>, { type<span style="color:#ff7b72;font-weight:bold">:</span> QueryTypes.SELECT });
</span></span><span style="display:flex;"><span>  console.log(JSON.stringify(results, <span style="color:#79c0ff">null</span>, <span style="color:#a5d6ff">2</span>));
</span></span><span style="display:flex;"><span>  console.timeEnd(<span style="color:#a5d6ff">&#34;rawQueries&#34;</span>);
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>rawQueries();
</span></span></code></pre></div><p class="code-source">原始碼：<a href="https://gist.github.com/dean9703111/78f3d7405983a6f5888550b67bf8dbee" rel="noopener">sequelize-raw-queries.js</a>（GitHub Gist）</p>
<p><strong>STEP 3</strong>：在終端機輸入：<code>node row-queries-exmaple.js</code></p>
<p>下圖即可看到看到搜尋結果、程式執行時間。</p>
<p><img src="https://deanlin.net/blog/202205/sequelize-migration-seeder/images/sequelize-migration-seeder-11.webp" width="1400" height="962" alt="五、簡易 Query（圖 11）" loading="lazy" decoding="async"></p>
<blockquote class="pullquote"><p>如果你想要知道 Table Join 時，選擇文字欄位（VARCHAR）與數值欄位（INTEGER）在效能（performance）上的差異，<a href="https://deanlin.net/blog/202205/join-performance-varchar-vs-integer/">可以參考這篇文章</a>。</p>
</blockquote>
]]></content:encoded>
    </item>
    <item>
      <title>帶你將 Node.js Web App 給 Docker 化，並用 Kubernetes 部署的超簡單範例</title>
      <link>https://deanlin.net/blog/202201/nodejs-docker-kubernetes-deploy/</link>
      <pubDate>Fri, 21 Jan 2022 10:28:04 +0800</pubDate>
      <guid>https://deanlin.net/blog/202201/nodejs-docker-kubernetes-deploy/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>公司很早就導入了 Kubernetes（K8s），主要是為了自動化部署以及管理、監測多台機器上的 Container，在遇到高併發情境時可以自動擴展。過去我都是以輔助者的角色跟 DevOps 說我希望有哪些功能，所以便藉著寫這篇文章的機會來初探這個已經成為業界潮流的技術。</description>
      <media:content url="https://deanlin.net/blog/202201/nodejs-docker-kubernetes-deploy/images/nodejs-docker-kubernetes-deploy-og_hu_1aafeb649fb6e222.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<p>公司很早就導入了 Kubernetes（K8s），主要是為了自動化部署以及管理、監測多台機器上的 Container，在遇到高併發情境時可以自動擴展。過去我都是以輔助者的角色跟 DevOps 說我希望有哪些功能，所以便藉著寫這篇文章的機會來初探這個已經成為業界潮流的技術。</p>
<p><img src="https://deanlin.net/blog/202201/nodejs-docker-kubernetes-deploy/images/nodejs-docker-kubernetes-deploy-01.webp" width="1400" height="740" alt="帶你將 Node.js Web App 給 Docker 化，並用 Kubernetes 部署的超簡單範例（圖 1）" fetchpriority="high" decoding="async"></p>
<pre tabindex="0"><code>文章大綱
一、安裝 Docker、kubectl、minikube 等工具
  ➤ 安裝 Docker
  ➤ 安裝 kubectl
  ➤ 安裝 minikube
二、建立 Node.js Web App
  ➤ 建立「package.json」
  ➤ 建立「server.js」
  ➤ 運行 Node.js Web App
三、將 Node.js Web App 給 Docker 化
  ➤ 建立「Dockerfile」
  ➤ 建立「.dockerignore」
  ➤ 製作 Docker Image
  ➤ 印出建立的 Images
  ➤ 將建立好的 Docker Image 跑起來
  ➤ 確認 Docker Image 運行狀態，並進入網址確認畫面如預期
四、撰寫 K8s 使用的 yaml 檔＆部署 K8s
  ➤ K8s 的 yaml 檔基礎說明
  ➤ K8s 部署
  ➤ 查看部署後 pods 狀態
  ➤ 取得 deployment 的外部 ip，確認是否會顯示網頁
  ➤ 刪除執行中的 K8s
  ➤ 關閉 minikube
</code></pre><blockquote>
<p>本篇文章是在 Mac 的作業系統下操作的</p>
</blockquote>
<h2 id="一安裝-dockerkubectlminikube-等工具">一、安裝 Docker、kubectl、minikube 等工具<a class="h-anchor" href="#%e4%b8%80%e5%ae%89%e8%a3%9d-dockerkubectlminikube-%e7%ad%89%e5%b7%a5%e5%85%b7" aria-label="這個段落的連結"></a></h2>
<h3 id="-安裝-docker">安裝 Docker<a class="h-anchor" href="#-%e5%ae%89%e8%a3%9d-docker" aria-label="這個段落的連結"></a></h3>
<p>前往<a href="https://docs.docker.com/desktop/mac/install/">官網下載 Docker 安裝檔</a>，安裝過程就是狂按下一步，這裡就不再多做介紹，下載後要把 App 打開依照步驟執行才算是完成安裝喔！</p>
<figure class="figure"><img src="https://deanlin.net/blog/202201/nodejs-docker-kubernetes-deploy/images/nodejs-docker-kubernetes-deploy-02.webp" width="1400" height="505" alt="這邊建議順便註冊一組 Docker 帳號，方便你日後的操作。" loading="lazy" decoding="async">
  <figcaption>這邊建議順便註冊一組 Docker 帳號，方便你日後的操作。</figcaption>
</figure>

<h3 id="-安裝-kubectl">安裝 kubectl<a class="h-anchor" href="#-%e5%ae%89%e8%a3%9d-kubectl" aria-label="這個段落的連結"></a></h3>
<p>安裝他才能操作 K8s 喔！</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># 安裝 kubectl</span>
</span></span><span style="display:flex;"><span>brew install kubectl
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># 確認是否安裝成功</span>
</span></span><span style="display:flex;"><span>kubectl version --client
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># 查看目前 cluster 運行狀態</span>
</span></span><span style="display:flex;"><span>kubectl get all
</span></span></code></pre></div><h3 id="-安裝-minikube">安裝 minikube<a class="h-anchor" href="#-%e5%ae%89%e8%a3%9d-minikube" aria-label="這個段落的連結"></a></h3>
<p>這個工具可以讓你在本機(Local)架設 K8s 叢集(Cluster)喔！</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># 安裝 minikube</span>
</span></span><span style="display:flex;"><span>brew install minikube
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># 啟動 minikube，啟動 k8s cluster</span>
</span></span><span style="display:flex;"><span>minikube start
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># 查看 minikube 狀態</span>
</span></span><span style="display:flex;"><span>minikube status
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># 取得目前 minikube 的 IP address</span>
</span></span><span style="display:flex;"><span>minikube ip
</span></span></code></pre></div><hr>
<h2 id="二建立-nodejs-web-app">二、建立 Node.js Web App<a class="h-anchor" href="#%e4%ba%8c%e5%bb%ba%e7%ab%8b-nodejs-web-app" aria-label="這個段落的連結"></a></h2>
<blockquote>
<p>你也可以直接去<a href="https://github.com/dean9703111/nodejs-docker-k8s-auto-scaling-example">筆者的 github</a>，直接將專案 Clone 到本機來做測試。</p>
</blockquote>
<h3 id="-建立packagejson">建立「package.json」<a class="h-anchor" href="#-%e5%bb%ba%e7%ab%8bpackagejson" aria-label="這個段落的連結"></a></h3>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#7ee787">&#34;name&#34;</span>: <span style="color:#a5d6ff">&#34;docker_web_app&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#7ee787">&#34;version&#34;</span>: <span style="color:#a5d6ff">&#34;1.0.0&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#7ee787">&#34;description&#34;</span>: <span style="color:#a5d6ff">&#34;Node.js on Docker&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#7ee787">&#34;author&#34;</span>: <span style="color:#a5d6ff">&#34;Dean Lin&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#7ee787">&#34;main&#34;</span>: <span style="color:#a5d6ff">&#34;server.js&#34;</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#7ee787">&#34;scripts&#34;</span>: {
</span></span><span style="display:flex;"><span>    <span style="color:#7ee787">&#34;start&#34;</span>: <span style="color:#a5d6ff">&#34;node server.js&#34;</span>
</span></span><span style="display:flex;"><span>  },
</span></span><span style="display:flex;"><span>  <span style="color:#7ee787">&#34;dependencies&#34;</span>: {
</span></span><span style="display:flex;"><span>    <span style="color:#7ee787">&#34;express&#34;</span>: <span style="color:#a5d6ff">&#34;^4.17.2&#34;</span>
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><p class="code-source">原始碼：<a href="https://gist.github.com/dean9703111/dba0f27584d5c0d7f778aa7cacb1d4b3" rel="noopener">node_web_app.json</a>（GitHub Gist）</p>
<h3 id="-建立serverjs">建立「server.js」<a class="h-anchor" href="#-%e5%bb%ba%e7%ab%8bserverjs" aria-label="這個段落的連結"></a></h3>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#a5d6ff">&#34;use strict&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> express <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#34;express&#34;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// Constants
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> PORT <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">8080</span>;
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> HOST <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#34;0.0.0.0&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// App
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> app <span style="color:#ff7b72;font-weight:bold">=</span> express();
</span></span><span style="display:flex;"><span>app.get(<span style="color:#a5d6ff">&#34;/&#34;</span>, (req, res) =&gt; {
</span></span><span style="display:flex;"><span>  res.send(<span style="color:#a5d6ff">&#34;Hello World&#34;</span>);
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>app.listen(PORT, HOST);
</span></span><span style="display:flex;"><span>console.log(<span style="color:#a5d6ff">`Running on http://</span><span style="color:#a5d6ff">${</span>HOST<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">:</span><span style="color:#a5d6ff">${</span>PORT<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>);
</span></span></code></pre></div><p class="code-source">原始碼：<a href="https://gist.github.com/dean9703111/0e82ccc37a3ae3cb68e78e047464c1ec" rel="noopener">node_web_app.js</a>（GitHub Gist）</p>
<h3 id="-運行-nodejs-web-app">運行 Node.js Web App<a class="h-anchor" href="#-%e9%81%8b%e8%a1%8c-nodejs-web-app" aria-label="這個段落的連結"></a></h3>
<p>在終端機先輸入<code>yarn</code>將套件安裝，再輸入<code>yarn start</code>確認專案可以運行。</p>
<figure class="figure"><img src="https://deanlin.net/blog/202201/nodejs-docker-kubernetes-deploy/images/nodejs-docker-kubernetes-deploy-03.webp" width="1292" height="390" alt="如果你是 Clone 筆者專案，記得要 cd 進入 docker_web_app 裡面再安裝＆執行喔！" loading="lazy" decoding="async">
  <figcaption>如果你是 Clone 筆者專案，記得要 cd 進入 docker_web_app 裡面再安裝＆執行喔！</figcaption>
</figure>

<figure class="figure"><img src="https://deanlin.net/blog/202201/nodejs-docker-kubernetes-deploy/images/nodejs-docker-kubernetes-deploy-04.webp" width="1334" height="796" alt="看到 Hello World 就恭喜完成第一步嚕" loading="lazy" decoding="async">
  <figcaption>看到 Hello World 就恭喜完成第一步嚕</figcaption>
</figure>

<hr>
<h2 id="三將-nodejs-web-app-給-docker-化">三、將 Node.js Web App 給 Docker 化<a class="h-anchor" href="#%e4%b8%89%e5%b0%87-nodejs-web-app-%e7%b5%a6-docker-%e5%8c%96" aria-label="這個段落的連結"></a></h2>
<h3 id="-建立dockerfile">建立「Dockerfile」<a class="h-anchor" href="#-%e5%bb%ba%e7%ab%8bdockerfile" aria-label="這個段落的連結"></a></h3>
<p>填寫製作 Docker Image 時需要參考的環境＆指令。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-dockerfile" data-lang="dockerfile"><span style="display:flex;"><span><span style="color:#ff7b72">FROM</span><span style="color:#6e7681"> </span><span style="color:#a5d6ff">node:14</span><span style="color:#f85149">
</span></span></span><span style="display:flex;"><span><span style="color:#f85149">
</span></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># Create app directory</span><span style="color:#f85149">
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">WORKDIR</span><span style="color:#6e7681"> </span><span style="color:#a5d6ff">/usr/src/app</span><span style="color:#f85149">
</span></span></span><span style="display:flex;"><span><span style="color:#f85149">
</span></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># Install app dependencies</span><span style="color:#f85149">
</span></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># A wildcard is used to ensure both package.json AND package-lock.json are copied</span><span style="color:#f85149">
</span></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># where available (npm@5+)</span><span style="color:#f85149">
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">COPY</span> package*.json ./<span style="color:#f85149">
</span></span></span><span style="display:flex;"><span><span style="color:#f85149">
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">RUN</span> npm install<span style="color:#f85149">
</span></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># If you are building your code for production</span><span style="color:#f85149">
</span></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># RUN npm ci --only=production</span><span style="color:#f85149">
</span></span></span><span style="display:flex;"><span><span style="color:#f85149">
</span></span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># Bundle app source</span><span style="color:#f85149">
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">COPY</span> . .<span style="color:#f85149">
</span></span></span><span style="display:flex;"><span><span style="color:#f85149">
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">EXPOSE</span><span style="color:#6e7681"> </span><span style="color:#a5d6ff">8080</span><span style="color:#f85149">
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">CMD</span> [ <span style="color:#a5d6ff">&#34;node&#34;</span>, <span style="color:#a5d6ff">&#34;server.js&#34;</span> ]<span style="color:#f85149">
</span></span></span></code></pre></div><p class="code-source">原始碼：<a href="https://gist.github.com/dean9703111/b4ed238746412ea272addb3e7667c1ee" rel="noopener">Dockerfile</a>（GitHub Gist）</p>
<h3 id="-建立dockerignore">建立「.dockerignore」<a class="h-anchor" href="#-%e5%bb%ba%e7%ab%8bdockerignore" aria-label="這個段落的連結"></a></h3>
<p>填寫製作 Docker Image 時不想要複製的檔案。</p>
<pre tabindex="0"><code>node_modules
npm-debug.log
</code></pre><p class="code-source">原始碼：<a href="https://gist.github.com/dean9703111/346e2e3876af9dbd6e8ee568b1b72bc5" rel="noopener">.dockerignore</a>（GitHub Gist）</p>
<h3 id="-製作-docker-image">製作 Docker Image<a class="h-anchor" href="#-%e8%a3%bd%e4%bd%9c-docker-image" aria-label="這個段落的連結"></a></h3>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># 登入 Docker</span>
</span></span><span style="display:flex;"><span>docker login
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># ！如果 Docker app沒有開啟就執行下面步驟會報錯！</span>
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># 建立 Docker Image</span>
</span></span><span style="display:flex;"><span>docker build . -t &lt;your username&gt;/node-web-app
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># 筆者範例指令</span>
</span></span><span style="display:flex;"><span>docker build . -t babydragon9703111/node-web-app
</span></span></code></pre></div><p><img src="https://deanlin.net/blog/202201/nodejs-docker-kubernetes-deploy/images/nodejs-docker-kubernetes-deploy-05.webp" width="1400" height="336" alt="製作 Docker Image（圖 5）" loading="lazy" decoding="async"></p>
<h3 id="-印出建立的-images">印出建立的 Images<a class="h-anchor" href="#-%e5%8d%b0%e5%87%ba%e5%bb%ba%e7%ab%8b%e7%9a%84-images" aria-label="這個段落的連結"></a></h3>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>docker images
</span></span></code></pre></div><p><img src="https://deanlin.net/blog/202201/nodejs-docker-kubernetes-deploy/images/nodejs-docker-kubernetes-deploy-06.webp" width="1400" height="199" alt="印出建立的 Images（圖 6）" loading="lazy" decoding="async"></p>
<h3 id="-將建立好的-docker-image-跑起來">將建立好的 Docker Image 跑起來<a class="h-anchor" href="#-%e5%b0%87%e5%bb%ba%e7%ab%8b%e5%a5%bd%e7%9a%84-docker-image-%e8%b7%91%e8%b5%b7%e4%be%86" aria-label="這個段落的連結"></a></h3>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># 裡面填寫的 port 是本地端要跑的（對外 port）</span>
</span></span><span style="display:flex;"><span>docker run -p <span style="color:#ff7b72;font-weight:bold">[</span>port<span style="color:#ff7b72;font-weight:bold">]</span>:8080 -d &lt;your username&gt;/node-web-app
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># 筆者範例指令</span>
</span></span><span style="display:flex;"><span>docker run -p 8080:8080 -d babydragon9703111/node-web-app
</span></span></code></pre></div><h3 id="-確認-docker-image-運行狀態並進入網址確認畫面如預期">確認 Docker Image 運行狀態，並進入網址確認畫面如預期<a class="h-anchor" href="#-%e7%a2%ba%e8%aa%8d-docker-image-%e9%81%8b%e8%a1%8c%e7%8b%80%e6%85%8b%e4%b8%a6%e9%80%b2%e5%85%a5%e7%b6%b2%e5%9d%80%e7%a2%ba%e8%aa%8d%e7%95%ab%e9%9d%a2%e5%a6%82%e9%a0%90%e6%9c%9f" aria-label="這個段落的連結"></a></h3>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># Get container ID</span>
</span></span><span style="display:flex;"><span>docker ps
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># Print app output</span>
</span></span><span style="display:flex;"><span>docker logs &lt;container id&gt;
</span></span></code></pre></div><p><img src="https://deanlin.net/blog/202201/nodejs-docker-kubernetes-deploy/images/nodejs-docker-kubernetes-deploy-07.webp" width="1400" height="264" alt="確認 Docker Image 運行狀態，並進入網址確認畫面如預期（圖 7）" loading="lazy" decoding="async"></p>
<figure class="figure"><img src="https://deanlin.net/blog/202201/nodejs-docker-kubernetes-deploy/images/nodejs-docker-kubernetes-deploy-08.webp" width="1334" height="796" alt="看到 Hello World 就距離成功非常接近嚕！" loading="lazy" decoding="async">
  <figcaption>看到 Hello World 就距離成功非常接近嚕！</figcaption>
</figure>

<hr>
<h2 id="四撰寫-k8s-使用的-yaml-檔部署-k8s">四、撰寫 K8s 使用的 yaml 檔＆部署 K8s<a class="h-anchor" href="#%e5%9b%9b%e6%92%b0%e5%af%ab-k8s-%e4%bd%bf%e7%94%a8%e7%9a%84-yaml-%e6%aa%94%e9%83%a8%e7%bd%b2-k8s" aria-label="這個段落的連結"></a></h2>
<h3 id="-k8s-的-yaml-檔基礎說明">K8s 的 yaml 檔基礎說明<a class="h-anchor" href="#-k8s-%e7%9a%84-yaml-%e6%aa%94%e5%9f%ba%e7%a4%8e%e8%aa%aa%e6%98%8e" aria-label="這個段落的連結"></a></h3>
<ul>
<li><strong>apiVersion</strong>：API 版本，需要依照 kind 的物件來決定 version。</li>
<li><strong>kind</strong>：建立的物件。</li>
<li><strong>metadata</strong>：描述物件及標籤。</li>
<li><strong>spec</strong>：物件要的功能。</li>
</ul>
<p>建立<code>nodetest-k8s.yaml</code>來做測試範例，裡面的 image 等相關資訊記得算成自己的</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-yaml" data-lang="yaml"><span style="display:flex;"><span><span style="color:#7ee787">apiVersion</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">v1</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#7ee787">kind</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">Service</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#7ee787">metadata</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">name</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">node-app-service</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#7ee787">spec</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">selector</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">app</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">node-web-app</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">ports</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span>- <span style="color:#7ee787">protocol</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">&#34;TCP&#34;</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span><span style="color:#7ee787">port</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">8080</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span><span style="color:#7ee787">targetPort</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">8080</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">type</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">LoadBalancer</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">---</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#7ee787">apiVersion</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">apps/v1</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#7ee787">kind</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">Deployment</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#7ee787">metadata</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">name</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">node-web-app</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#7ee787">spec</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">selector</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">matchLabels</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span><span style="color:#7ee787">app</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">node-web-app</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">replicas</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">3</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">  </span><span style="color:#7ee787">template</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">metadata</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span><span style="color:#7ee787">labels</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span><span style="color:#7ee787">app</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">node-web-app</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">    </span><span style="color:#7ee787">spec</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">      </span><span style="color:#7ee787">containers</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">        </span>- <span style="color:#7ee787">name</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">node-web-app</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">          </span><span style="color:#7ee787">image</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">babydragon9703111/node-web-app:latest</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">          </span><span style="color:#7ee787">imagePullPolicy</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">Always</span><span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">          </span><span style="color:#7ee787">ports</span>:<span style="color:#6e7681">
</span></span></span><span style="display:flex;"><span><span style="color:#6e7681">            </span>- <span style="color:#7ee787">containerPort</span>:<span style="color:#6e7681"> </span><span style="color:#a5d6ff">8080</span><span style="color:#6e7681">
</span></span></span></code></pre></div><p class="code-source">原始碼：<a href="https://gist.github.com/dean9703111/fbdb562caf70b78ee7801d377c546e12" rel="noopener">nodetest-k8s.yaml</a>（GitHub Gist）</p>
<h3 id="-k8s-部署">K8s 部署<a class="h-anchor" href="#-k8s-%e9%83%a8%e7%bd%b2" aria-label="這個段落的連結"></a></h3>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># 啟動 k8s cluster</span>
</span></span><span style="display:flex;"><span>minikube start
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># 執行部署</span>
</span></span><span style="display:flex;"><span>kubectl apply -f nodetest-k8s.yaml
</span></span></code></pre></div><p><img src="https://deanlin.net/blog/202201/nodejs-docker-kubernetes-deploy/images/nodejs-docker-kubernetes-deploy-09.webp" width="1400" height="545" alt="K8s 部署（圖 9）" loading="lazy" decoding="async"></p>
<h3 id="-查看部署後-pods-狀態">查看部署後 pods 狀態<a class="h-anchor" href="#-%e6%9f%a5%e7%9c%8b%e9%83%a8%e7%bd%b2%e5%be%8c-pods-%e7%8b%80%e6%85%8b" aria-label="這個段落的連結"></a></h3>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># 此指令會查 default namespace 的資源</span>
</span></span><span style="display:flex;"><span>kubectl get pods
</span></span></code></pre></div><p>因為我們在 spec 的 replicas 的值設為 3，所以會產生 3 個 pods。</p>
<p><img src="https://deanlin.net/blog/202201/nodejs-docker-kubernetes-deploy/images/nodejs-docker-kubernetes-deploy-10.webp" width="1400" height="231" alt="查看部署後 pods 狀態（圖 10）" loading="lazy" decoding="async"></p>
<h3 id="-取得-deployment-的外部-ip確認是否會顯示網頁">取得 deployment 的外部 ip，確認是否會顯示網頁<a class="h-anchor" href="#-%e5%8f%96%e5%be%97-deployment-%e7%9a%84%e5%a4%96%e9%83%a8-ip%e7%a2%ba%e8%aa%8d%e6%98%af%e5%90%a6%e6%9c%83%e9%a1%af%e7%a4%ba%e7%b6%b2%e9%a0%81" aria-label="這個段落的連結"></a></h3>
<pre tabindex="0"><code>minikube service node-app-service --url
</code></pre><figure class="figure"><img src="https://deanlin.net/blog/202201/nodejs-docker-kubernetes-deploy/images/nodejs-docker-kubernetes-deploy-11.webp" width="1400" height="649" alt="如果網頁有正確顯示就大功告成啦～" loading="lazy" decoding="async">
  <figcaption>如果網頁有正確顯示就大功告成啦～</figcaption>
</figure>

<h3 id="-刪除執行中的-k8s">刪除執行中的 K8s<a class="h-anchor" href="#-%e5%88%aa%e9%99%a4%e5%9f%b7%e8%a1%8c%e4%b8%ad%e7%9a%84-k8s" aria-label="這個段落的連結"></a></h3>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic"># 指定刪除剛剛建立的 K8s</span>
</span></span><span style="display:flex;"><span>kubectl delete -f nodetest-k8s.yaml
</span></span></code></pre></div><p>剛下達指令刪除時，在 STATUS 的部分會顯示 Terminating，過一段時間就會消失嚕～</p>
<p><img src="https://deanlin.net/blog/202201/nodejs-docker-kubernetes-deploy/images/nodejs-docker-kubernetes-deploy-12.webp" width="1400" height="301" alt="刪除執行中的 K8s（圖 12）" loading="lazy" decoding="async"></p>
<h3 id="-關閉-minikube">關閉 minikube<a class="h-anchor" href="#-%e9%97%9c%e9%96%89-minikube" aria-label="這個段落的連結"></a></h3>
<pre tabindex="0"><code># 如果沒關閉的話，你會發現電腦燙燙的，CPU 居高不下
minikube delete
</code></pre><p><img src="https://deanlin.net/blog/202201/nodejs-docker-kubernetes-deploy/images/nodejs-docker-kubernetes-deploy-13.webp" width="769" height="116" alt="關閉 minikube（圖 13）" loading="lazy" decoding="async"></p>
<hr>
<blockquote class="pullquote"><p>以上就是 Node.js X Docker X Kubernetes 超簡單範例啦！希望讀者可以透過這篇文章對這些技術有基礎認識～</p>
</blockquote>
<blockquote class="pullquote"><p>如果想更近一步了解 K8s Auto Scaling 的技術，歡迎閱讀筆者寫的這篇文章：「<a href="https://deanlin.net/blog/202201/kubernetes-autoscaling-local/">模擬 K8s Auto Scaling（可在 Local 端測試）</a>」</p>
</blockquote>
]]></content:encoded>
    </item>
    <item>
      <title>[面試][後端]請簡述 Node.js 的 Event Loop</title>
      <link>https://deanlin.net/blog/202109/fullstack-career-notes-day-14/</link>
      <pubDate>Wed, 29 Sep 2021 08:46:22 +0800</pubDate>
      <guid>https://deanlin.net/blog/202109/fullstack-career-notes-day-14/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>職涯成長</category>
      <description>這是一個在了解後，無論面試還是工作，CP 值都很高的題目，建議讀者要真的融會貫通。</description>
      <media:content url="https://deanlin.net/blog/202109/fullstack-career-notes-day-14/images/cover_hu_579b7f5274d9e2b1.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<blockquote>
<p>熟悉的起手式：「我方便問你一個 Node.js 核心的問題嗎？」</p>
</blockquote>
<p>這是一個在了解後，無論面試還是工作，CP 值都很高的題目，建議讀者要真的融會貫通。</p>
<h2 id="大綱">大綱<a class="h-anchor" href="#%e5%a4%a7%e7%b6%b1" aria-label="這個段落的連結"></a></h2>
<ol>
<li><strong>請簡述 Node.js 的 Event Loop</strong>
<ul>
<li>1.1 面試官為什麼會問？</li>
<li>1.2 面試官想從答案確認什麼？</li>
<li>1.3 筆者提供的簡答</li>
</ul>
</li>
<li><strong>回答問題所需具備的知識</strong>
<ul>
<li>2.1 Event Loop 執行的邏輯</li>
<li>2.2 用 Node.js 範例程式確認自己是否理解 Event Loop</li>
<li>2.3 解析 Node.js 範例程式執行順序</li>
</ul>
</li>
<li><strong>衍伸問題</strong>
<ul>
<li>3.1 可以簡述 Node.js 跟 JavaScript 的差異嗎？</li>
<li>3.2 readFile 和 createReadStream 函式的差異？</li>
<li>3.3 REPL 是什麼？</li>
</ul>
</li>
</ol>
<hr>
<h2 class="h-nomark" id="1-請簡述-nodejs-的-event-loop">1. 請簡述 Node.js 的 Event Loop<a class="h-anchor" href="#1-%e8%ab%8b%e7%b0%a1%e8%bf%b0-nodejs-%e7%9a%84-event-loop" aria-label="這個段落的連結"></a></h2>
<h3 class="h-nomark" id="11-面試官為什麼會問">1.1 面試官為什麼會問？<a class="h-anchor" href="#11-%e9%9d%a2%e8%a9%a6%e5%ae%98%e7%82%ba%e4%bb%80%e9%ba%bc%e6%9c%83%e5%95%8f" aria-label="這個段落的連結"></a></h3>
<p>只要你的<code>履歷上有 Node.js</code>，10 間公司至少有 3 間會問這個<mark>常見面試題</mark>；而且問題的起手式出奇的一致：「我方便問你一個 Node.js 核心的問題嗎？」</p>
<hr>
<h3 class="h-nomark" id="12-面試官想從答案確認什麼">1.2 面試官想從答案確認什麼？<a class="h-anchor" href="#12-%e9%9d%a2%e8%a9%a6%e5%ae%98%e6%83%b3%e5%be%9e%e7%ad%94%e6%a1%88%e7%a2%ba%e8%aa%8d%e4%bb%80%e9%ba%bc" aria-label="這個段落的連結"></a></h3>
<ul>
<li>你了解 Event Loop 對 Node.js 的重要性</li>
<li>你能大概說明 Event Loop 每個階段的任務</li>
<li>如果出白板題要你解釋程式執行的順序，你可以正確回答</li>
</ul>
<h3 class="h-nomark" id="13-筆者提供的簡答">1.3 筆者提供的簡答<a class="h-anchor" href="#13-%e7%ad%86%e8%80%85%e6%8f%90%e4%be%9b%e7%9a%84%e7%b0%a1%e7%ad%94" aria-label="這個段落的連結"></a></h3>
<p><code>Node.js 之所以高效，是因為採取單執行緒與 Event Loop 的概念</code>；將所有需要等待結果、請求外部資源的函式，全部丟到 Event Loop 中等待；而 Event Loop 的邏輯是 Node.js 底層用 C 語言撰寫的 libuv 庫來運行的。</p>
<hr>
<h2 class="h-nomark" id="2-回答問題所需具備的知識">2. 回答問題所需具備的知識<a class="h-anchor" href="#2-%e5%9b%9e%e7%ad%94%e5%95%8f%e9%a1%8c%e6%89%80%e9%9c%80%e5%85%b7%e5%82%99%e7%9a%84%e7%9f%a5%e8%ad%98" aria-label="這個段落的連結"></a></h2>
<h3 class="h-nomark" id="21-event-loop-執行的邏輯">2.1 Event Loop 執行的邏輯<a class="h-anchor" href="#21-event-loop-%e5%9f%b7%e8%a1%8c%e7%9a%84%e9%82%8f%e8%bc%af" aria-label="這個段落的連結"></a></h3>
<p><img src="https://deanlin.net/blog/202109/fullstack-career-notes-day-14/images/fullstack-career-notes-day-14-01.webp" width="1024" height="579" alt="2.1 Event Loop 執行的邏輯（圖 1）" fetchpriority="high" decoding="async"></p>
<ul>
<li><strong>了解 MacroTask 的 6 個階段</strong>
<ul>
<li><strong>Timer</strong><br>等計時器（setTimeout、setInterval）的時間一到，會把他們的 callback 放在這裡等待執行。</li>
<li><strong>Pending callbacks</strong><br>作業系統層級使用（ex：TCP errors、sockets 連線被拒絕）。</li>
<li><strong>Idle, Prepare</strong><br>內部使用。</li>
<li><strong>Polling：</strong>
<ul>
<li>如果 Queue 不為空，依次取出 callback 函數執行，直到 Queue 為空或是抵達系統最大限制。</li>
<li>如果 Queue 為空但有設置「setImmediate」就進入 Check 階段。</li>
<li>如果 Queue 為空但沒有設置「setImmediate」就會在 Polling 階段等待，直到 Queue 有東西或是 Timer 時間抵達。</li>
</ul>
</li>
<li><strong>Check</strong><br>處理 setImmediate 的 callback。</li>
<li><strong>Close callbacks</strong><br>處理關閉連線、檔案的 callback。</li>
</ul>
</li>
</ul>
<p><mark>Macrotask Queue 在 Evevnt Loop 優先層級較低，接下來介紹優先層級更高的兩個 Queue。</mark></p>
<ul>
<li>
<p><strong>NextTick Queue 優先層級最高</strong><br>「process.nextTick()」的 callback 可以隨意插隊；只要這個 Queue 有東西就會優先執行。</p>
</li>
<li>
<p><strong>MicroTask Queue 優先層級次高</strong><br>「Promise」的狀態從 pending 轉變為 resolve 或 reject 時，執行的 callback 會進入這個 Queue。</p>
<blockquote>
<p><strong>注意</strong>：<mark>Promise 在創建時是同步的</mark>，不會進入 Event Loop。</p>
</blockquote>
</li>
</ul>
<hr>
<h3 class="h-nomark" id="22-用-nodejs-範例程式確認自己是否理解-event-loop">2.2 用 Node.js 範例程式確認自己是否理解 Event Loop<a class="h-anchor" href="#22-%e7%94%a8-nodejs-%e7%af%84%e4%be%8b%e7%a8%8b%e5%bc%8f%e7%a2%ba%e8%aa%8d%e8%87%aa%e5%b7%b1%e6%98%af%e5%90%a6%e7%90%86%e8%a7%a3-event-loop" aria-label="這個段落的連結"></a></h3>
<p>大家可以先用紙筆模擬看看下面程式的執行順序：</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span>console.log(<span style="color:#a5d6ff">&#34;start&#34;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>process.nextTick(<span style="color:#ff7b72">function</span> () {
</span></span><span style="display:flex;"><span>  console.log(<span style="color:#a5d6ff">&#34;nextTick1&#34;</span>);
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>setTimeout(<span style="color:#ff7b72">function</span> () {
</span></span><span style="display:flex;"><span>  console.log(<span style="color:#a5d6ff">&#34;setTimeout&#34;</span>);
</span></span><span style="display:flex;"><span>}, <span style="color:#a5d6ff">0</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">new</span> Promise(<span style="color:#ff7b72">function</span> (resolve, reject) {
</span></span><span style="display:flex;"><span>  console.log(<span style="color:#a5d6ff">&#34;promise&#34;</span>);
</span></span><span style="display:flex;"><span>  resolve(<span style="color:#a5d6ff">&#34;resolve&#34;</span>);
</span></span><span style="display:flex;"><span>}).then(<span style="color:#ff7b72">function</span> (result) {
</span></span><span style="display:flex;"><span>  console.log(result);
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>(<span style="color:#ff7b72">async</span> <span style="color:#ff7b72">function</span> () {
</span></span><span style="display:flex;"><span>  console.log(<span style="color:#a5d6ff">&#34;async&#34;</span>);
</span></span><span style="display:flex;"><span>})();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>setImmediate(<span style="color:#ff7b72">function</span> () {
</span></span><span style="display:flex;"><span>  console.log(<span style="color:#a5d6ff">&#34;setImmediate&#34;</span>);
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>process.nextTick(<span style="color:#ff7b72">function</span> () {
</span></span><span style="display:flex;"><span>  console.log(<span style="color:#a5d6ff">&#34;nextTick2&#34;</span>);
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>console.log(<span style="color:#a5d6ff">&#34;end&#34;</span>);
</span></span></code></pre></div><p>如果答案跟下面結果一樣，那代表你很了解 Node.js 的 Event Loop 邏輯：</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span>start
</span></span><span style="display:flex;"><span>promise
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">async</span>
</span></span><span style="display:flex;"><span>end
</span></span><span style="display:flex;"><span>nextTick1
</span></span><span style="display:flex;"><span>nextTick2
</span></span><span style="display:flex;"><span>resolve
</span></span><span style="display:flex;"><span>setTimeout
</span></span><span style="display:flex;"><span>setImmediate
</span></span></code></pre></div><hr>
<h3 class="h-nomark" id="23-解析-nodejs-範例程式執行順序">2.3 解析 Node.js 範例程式執行順序<a class="h-anchor" href="#23-%e8%a7%a3%e6%9e%90-nodejs-%e7%af%84%e4%be%8b%e7%a8%8b%e5%bc%8f%e5%9f%b7%e8%a1%8c%e9%a0%86%e5%ba%8f" aria-label="這個段落的連結"></a></h3>
<ul>
<li><strong>SETP 1：</strong> 順序執行，先把同步函式解決。
<ol>
<li>
<p>因為 <code>Promise</code> 在創建時是同步的，所以會先執行。</p>
</li>
<li>
<p>在 function 前面加上 async 宣告為異步函式，執行時會被轉換為 Promise。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span>console.log(<span style="color:#a5d6ff">&#34;start&#34;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">new</span> Promise(<span style="color:#ff7b72">function</span> (resolve, reject) {
</span></span><span style="display:flex;"><span>  console.log(<span style="color:#a5d6ff">&#34;promise&#34;</span>);
</span></span><span style="display:flex;"><span>  resolve(<span style="color:#a5d6ff">&#34;resolve&#34;</span>);
</span></span><span style="display:flex;"><span>}).then(<span style="color:#ff7b72">function</span> (result) {
</span></span><span style="display:flex;"><span>  console.log(result);
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>(<span style="color:#ff7b72">async</span> <span style="color:#ff7b72">function</span> () {
</span></span><span style="display:flex;"><span>  console.log(<span style="color:#a5d6ff">&#34;async&#34;</span>);
</span></span><span style="display:flex;"><span>})();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>console.log(<span style="color:#a5d6ff">&#34;end&#34;</span>);
</span></span></code></pre></div></li>
</ol>
</li>
<li><strong>STEP 2：</strong> 進入 Event Loop。
<ol>
<li>
<p>如果執行過程中遇到 <code>NextTick Queue</code> 就優先執行。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span>process.nextTick(<span style="color:#ff7b72">function</span> () {
</span></span><span style="display:flex;"><span>  console.log(<span style="color:#a5d6ff">&#34;nextTick1&#34;</span>);
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>process.nextTick(<span style="color:#ff7b72">function</span> () {
</span></span><span style="display:flex;"><span>  console.log(<span style="color:#a5d6ff">&#34;nextTick2&#34;</span>);
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div></li>
<li>
<p>在處理完最高優先層級後，如果執行過程中遇到 <code>MicroTask Queue</code> 就執行。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#ff7b72">new</span> Promise(<span style="color:#ff7b72">function</span> (resolve, reject) {
</span></span><span style="display:flex;"><span>  console.log(<span style="color:#a5d6ff">&#34;promise&#34;</span>);
</span></span><span style="display:flex;"><span>  resolve(<span style="color:#a5d6ff">&#34;resolve&#34;</span>);
</span></span><span style="display:flex;"><span>}).then(<span style="color:#ff7b72">function</span> (result) {
</span></span><span style="display:flex;"><span>  console.log(result);
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div></li>
<li>
<p>在前兩個高優先層級的 Queue 清空後，返回 Event Loop 繼續執行，<code>setTimeout</code> 設定的時間已到先執行。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span>setTimeout(<span style="color:#ff7b72">function</span> () {
</span></span><span style="display:flex;"><span>  console.log(<span style="color:#a5d6ff">&#34;setTimeout&#34;</span>);
</span></span><span style="display:flex;"><span>}, <span style="color:#a5d6ff">0</span>);
</span></span></code></pre></div></li>
<li>
<p>接著再在「Check」 階段執行 <code>setImmediate</code></p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span>setImmediate(<span style="color:#ff7b72">function</span> () {
</span></span><span style="display:flex;"><span>  console.log(<span style="color:#a5d6ff">&#34;setImmediate&#34;</span>);
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div></li>
</ol>
</li>
</ul>
<p>以上步驟執行完成後就是範例程式的解答啦～希望對大家理解 Event Loop 有幫助。</p>
<hr>
<h2 class="h-nomark" id="3-衍伸問題">3. 衍伸問題<a class="h-anchor" href="#3-%e8%a1%8d%e4%bc%b8%e5%95%8f%e9%a1%8c" aria-label="這個段落的連結"></a></h2>
<h3 class="h-nomark" id="31-可以簡述-nodejs-跟-javascript-的差異嗎">3.1 可以簡述 Node.js 跟 JavaScript 的差異嗎？<a class="h-anchor" href="#31-%e5%8f%af%e4%bb%a5%e7%b0%a1%e8%bf%b0-nodejs-%e8%b7%9f-javascript-%e7%9a%84%e5%b7%ae%e7%95%b0%e5%97%8e" aria-label="這個段落的連結"></a></h3>
<p><mark>考點：確認求職者是否理解自己天天使用的工具</mark></p>
<p>JavaScript 是程式語言，可以在合適的瀏覽器中運行；而 Node.js 是一個能執行JavaScript 的環境，它以 Chrome V8 引擎為核心，再加上 C/C++套件，讓 Sever 端也可以執行 JavaScript。</p>
<hr>
<h3 class="h-nomark" id="32-readfile-和-createreadstream-函式的差異">3.2 readFile 和 createReadStream 函式的差異？<a class="h-anchor" href="#32-readfile-%e5%92%8c-createreadstream-%e5%87%bd%e5%bc%8f%e7%9a%84%e5%b7%ae%e7%95%b0" aria-label="這個段落的連結"></a></h3>
<p><mark>考點：判斷求職者過去專案中使否使用過這類函式，並理解差異</mark></p>
<p><code>readFile</code> 函式會將讀取到的完整內容<mark>存在記憶體</mark>後傳給使用者，而 <code>createReadStream</code> 函式則是<mark>逐塊讀取</mark>檔案，不是全部存在記憶體中；因此要讀取較大的檔案時，建議使用 createReadStream ，利用其逐塊讀取的特性能減少使用者等待時間。</p>
<hr>
<h3 class="h-nomark" id="33-repl-是什麼">3.3 REPL 是什麼？<a class="h-anchor" href="#33-repl-%e6%98%af%e4%bb%80%e9%ba%bc" aria-label="這個段落的連結"></a></h3>
<p><mark>考點：對專有名詞是否熟悉，能否解釋</mark></p>
<p>REPL（Read Eval Print Loop）是一個用來<mark>執行程式語言的虛擬環境</mark>。</p>
<p>就像是我們在瀏覽器的 Console 可以執行 JavaScript 程式碼；在安裝完 Node.js 後，新增一個 JavaScript 檔，在終端機輸入<code>node xxx.js</code>便可以執行它。</p>
<hr>
<p>感謝大家的閱讀，如果喜歡我的文章可以<code>訂閱</code>接收通知；如果有幫助到你，按<code>Like</code>可以讓我更有寫文的動力，我們明天見～</p>
<h3 id="參考資源">參考資源：<a class="h-anchor" href="#%e5%8f%83%e8%80%83%e8%b3%87%e6%ba%90" aria-label="這個段落的連結"></a></h3>
<ol>
<li><a href="https://medium.com/dean-lin/c7129063d0f4">Node.js 的 Event Loop (事件輪詢)到底在做什麼？(筆者部落格)</a></li>
<li>15 個常見的 Node.js 面試問題及答案</li>
<li><a href="https://ithelp.ithome.com.tw/articles/10230126">你有沒有想過，到底 Server 是如何「同時處理多個 requests」的？ - Node.js 篇</a></li>
</ol>
<hr>
<hr>
<blockquote>
<p><img src="https://deanlin.net/blog/202109/fullstack-career-notes-day-14/images/fullstack-career-notes-day-14-02.webp" width="1024" height="233" alt="參考資源：（圖 2）" loading="lazy" decoding="async"></p>
<p>在許多人的幫助下，本系列文章已成功出版，除了添加新的篇章，更完善了每個案例的應對進退；如果對現在的職涯感到迷茫，也許這本書能帶給你不一樣的觀點～</p>
<p><strong>天瓏書局：</strong> <a href="https://www.tenlong.com.tw/products/9786263334571">https://www.tenlong.com.tw/products/9786263334571</a></p>
</blockquote>
]]></content:encoded>
    </item>
    <item>
      <title>[面試][後端]你會的後端框架不只一個，可以說明一下它們之間的差異嗎？</title>
      <link>https://deanlin.net/blog/202109/fullstack-career-notes-day-13/</link>
      <pubDate>Tue, 28 Sep 2021 08:52:51 +0800</pubDate>
      <guid>https://deanlin.net/blog/202109/fullstack-career-notes-day-13/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>職涯成長</category>
      <description>工程師真的很難，會得不多會被問為什麼沒學，會得很多還要被質疑專業程度；這種比較類型的問題，除非你有事先做功課，不然就算你都會用也未必能在現場對答如流。</description>
      <media:content url="https://deanlin.net/blog/202109/fullstack-career-notes-day-13/images/cover_hu_fd19aa6d1c00a16c.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<blockquote>
<p>你是十八般武藝樣樣精通，還是樣樣通樣樣鬆？</p>
</blockquote>
<p>工程師真的很難，會得不多會被問為什麼沒學，會得很多還要被質疑專業程度；這種比較類型的問題，除非你有事先做功課，不然就算你都會用也未必能在現場對答如流。</p>
<h2 id="大綱">大綱<a class="h-anchor" href="#%e5%a4%a7%e7%b6%b1" aria-label="這個段落的連結"></a></h2>
<ol>
<li><strong>你會的後端框架不只一個，可以說明一下它們之間的差異嗎？</strong>
<ul>
<li>1.1 面試官為什麼會問？</li>
<li>1.2 面試官想從答案確認什麼？</li>
<li>1.3 筆者提供的簡答</li>
</ul>
</li>
<li><strong>回答問題所需具備的知識</strong>
<ul>
<li>2.1 PHP VS Node.js</li>
<li>2.2 用 supervisor 讓 Node.js 熱更新</li>
<li>2.3 不一定要拼個誰輸誰贏</li>
</ul>
</li>
<li><strong>衍伸問題</strong>
<ul>
<li>3.1 既然你先學了 Node.js 的框架，為何後來改用 PHP 的框架？</li>
<li>3.2 使用框架有什麼好處跟壞處？</li>
</ul>
</li>
</ol>
<hr>
<h2 class="h-nomark" id="1-你會的後端框架不只一個可以說明一下它們之間的差異嗎">1. 你會的後端框架不只一個，可以說明一下它們之間的差異嗎？<a class="h-anchor" href="#1-%e4%bd%a0%e6%9c%83%e7%9a%84%e5%be%8c%e7%ab%af%e6%a1%86%e6%9e%b6%e4%b8%8d%e5%8f%aa%e4%b8%80%e5%80%8b%e5%8f%af%e4%bb%a5%e8%aa%aa%e6%98%8e%e4%b8%80%e4%b8%8b%e5%ae%83%e5%80%91%e4%b9%8b%e9%96%93%e7%9a%84%e5%b7%ae%e7%95%b0%e5%97%8e" aria-label="這個段落的連結"></a></h2>
<h3 class="h-nomark" id="11-面試官為什麼會問">1.1 面試官為什麼會問？<a class="h-anchor" href="#11-%e9%9d%a2%e8%a9%a6%e5%ae%98%e7%82%ba%e4%bb%80%e9%ba%bc%e6%9c%83%e5%95%8f" aria-label="這個段落的連結"></a></h3>
<p>因為<mark>履歷資訊</mark>中顯示求職者會如下的後端（Backend）框架：</p>
<ul>
<li>PHP：Laravel</li>
<li>Node.js：Express.js、AdonisJS</li>
</ul>
<p>當你會的東西多，面試官就會想了解你是每個都學一點，還是真的了解它。</p>
<hr>
<h3 class="h-nomark" id="12-面試官想從答案確認什麼">1.2 面試官想從答案確認什麼？<a class="h-anchor" href="#12-%e9%9d%a2%e8%a9%a6%e5%ae%98%e6%83%b3%e5%be%9e%e7%ad%94%e6%a1%88%e7%a2%ba%e8%aa%8d%e4%bb%80%e9%ba%bc" aria-label="這個段落的連結"></a></h3>
<ul>
<li>能否簡單敘述 PHP 與 Node.js 的差異</li>
<li>舉例過去用這些框架做過哪些專案或是功能</li>
<li>為什麼會學習新的框架</li>
</ul>
<hr>
<h3 class="h-nomark" id="13-筆者提供的簡答">1.3 筆者提供的簡答<a class="h-anchor" href="#13-%e7%ad%86%e8%80%85%e6%8f%90%e4%be%9b%e7%9a%84%e7%b0%a1%e7%ad%94" aria-label="這個段落的連結"></a></h3>
<p>之前使用 <code>PHP 的 Laravel 框架</code>開發過內部管理系統，也曾用 <code>Node.js 的 Express.js 框架</code>完成過餐廳點餐系統。</p>
<p>兩者的<mark>差異主要因為程式語言</mark>而產生的，PHP 是同步語言、多執行緒，Node.js 是一個能執行 JavaScript 的環境，支援非同步，為單執行緒。</p>
<p>如果專案要求<mark>穩定一致的效能</mark>，我會偏向使用 PHP 框架；而如果要<mark>承接高流量</mark>，我會選擇 Node.js 框架。</p>
<hr>
<h2 class="h-nomark" id="2-回答問題所需具備的知識">2. 回答問題所需具備的知識<a class="h-anchor" href="#2-%e5%9b%9e%e7%ad%94%e5%95%8f%e9%a1%8c%e6%89%80%e9%9c%80%e5%85%b7%e5%82%99%e7%9a%84%e7%9f%a5%e8%ad%98" aria-label="這個段落的連結"></a></h2>
<h3 class="h-nomark" id="21-php-vs-nodejs">2.1 PHP VS Node.js<a class="h-anchor" href="#21-php-vs-nodejs" aria-label="這個段落的連結"></a></h3>
<p>如果面對一個問題只有<mark>一種解決方案</mark>時，面試官會<mark>深入詢問</mark>這個技術；但如果面對問題有<mark>兩個以上的解決方案</mark>，他就會問你在<mark>不同情境</mark>下該使用什麼技術，每個技術都有各自的優勢，這邊我們就從不同的角度來做分析吧～</p>
<ul>
<li><strong>同步的 PHP vs 異步的 Node.js</strong>
<ul>
<li>同步的 PHP 會為每個 Request 建立一個 thread，每個 thread 都會消耗記憶體，也就是說如果想要服務更多的客戶就要花心思在<mark>硬體設備</mark>上。</li>
<li>異步的 Node.js 是非阻塞 (non-blocking) I/O 設計，搭配上 Event Loop 可以用一句很形象的話說明：「與其人多但好多人閒著；還不如一個人拼命往死裡做。」</li>
</ul>
</li>
<li><strong>多執行緒的 PHP VS 單執行緒的 Node.js</strong>
<ul>
<li>
<p><strong>多執行緒 (multi-threaded)的 PHP</strong><br>使用阻塞 (blocking) I/O 的設計</p>
<blockquote>
<p>PHP 可以為每個 Request 都開一個執行緒；如果你想要實現非阻塞也能透過 <a href="https://reactphp.org/">reactPHP</a> 來實現。</p>
</blockquote>
</li>
<li>
<p><strong>單執行緒 (single-threaded)的 Node.js</strong><br>使用非阻塞 (non-blocking) I/O 設計 + 提供非同步 (asynchronous) 處理</p>
<blockquote>
<p>Node.js 原則上是單執行緒，但可以透過 <a href="https://pm2.keymetrics.io/">PM2</a> 的 Cluster Mode 達到多執行緒。</p>
</blockquote>
</li>
</ul>
</li>
<li><strong>應用場景</strong><br>從 Client 端與 Server 端的溝通頻率來做分析：
<ul>
<li><strong>溝通頻率不高</strong><br>PHP 是個不錯的選擇（ex：個人部落格，像 WordPress 就是用 PHP 寫的）。</li>
<li><strong>溝通頻率頻繁</strong><br>Node.js 更能激發性能（ex：single-page-application、聊天室）；它適合大吞吐量的系統，較不適合需要大量 CPU 運算的系統。</li>
</ul>
</li>
<li><strong>可使用的資源</strong>
<ul>
<li>
<p><strong>PHP 有 Composer</strong><br>Composer 的命名方式：「所有者/package 名稱」</p>
</li>
<li>
<p><strong>Node.js 有 NPM</strong><br>NPM 的命名方式：「package 名稱」</p>
<blockquote>
<p>因為 NPM 的命名方式太自由，導致上面有很多的資源都是無效資源，<mark>關鍵字第一個找到的未必是最合適的</mark>。</p>
</blockquote>
</li>
</ul>
</li>
<li><strong>人才市場</strong>
<ul>
<li><strong>人才數量</strong><br>PHP 比 Node.js 早了 10 幾年發行，因此市場上使用 PHP 的人比使用 Node.js 的人更多；如果<mark>職位流動性高，用 PHP 開發</mark>會比較合適。</li>
<li><strong>薪資</strong><br>物以稀為貴，同樣都是新人，<code>會 Node.js 的薪水會略高於會 PHP</code> 的求職者。</li>
</ul>
</li>
<li><strong>專案容量</strong><br>用 Laravel 跟 Express.js 來比較：
<ul>
<li><strong>專案初期</strong><br>一開始 Laravel 因為包山包海所以專案體積較大。</li>
<li><strong>專案後期</strong><br>儘管 Express 是 Node.js 輕量級的框架，但因為<mark>實作過程往往要擴充一堆 package</mark>，而且一個 package 往往會有很多的 dependency；這些原因容易導致最終實作出來的是一個笨重的專案。</li>
</ul>
</li>
<li><strong>程式語言轉換</strong><br>除非你的運氣逆天，每次都剛好找到符合你技能樹的公司，而且這間公司專案用的技術還很固定；不然肯定要面對程式語言的轉換，這邊分享幾個轉換時要注意的重點。
<ul>
<li><strong>找類型相似的框架</strong><br>從 PHP 跳到 Node.js 一定會面臨諸多不適應；但如果你選擇的是跟 <mark>Laravel 很類似的 AdonisJS 框架</mark>，這份不適感會下降許多，反之亦然。</li>
<li><strong>語言語法</strong><br>如果你已經熟悉了一種程式語言，我覺得轉換到另外一個語言的學習成本不會太高；但許多<mark>內建函式都要重新學習、生命週期也要重新熟悉、錯誤處理的經驗也要重新累積</mark>。</li>
<li><strong>專案規模</strong><br>如果是只需要 CRUD 邏輯的小專案，那我覺得藉此多瞭解一門程式語言是一個很棒的機會；但如果面臨的是業務邏輯複雜的大專案，我強烈建議一定要選自己熟悉的程式語言，或者團隊中的扛霸子熟悉這門語言；不然出事根本沒人可以救得了。</li>
</ul>
</li>
<li><strong>效能</strong>
<ul>
<li><strong>響應速度</strong><br>Node.js 靠著 V8 引擎加上非同步的特型，擁有較快的響應速度。</li>
</ul>
</li>
<li><strong>面對錯誤的處理</strong>
<ul>
<li>
<p>PHP 遇到某個 Request 錯誤時，它只對這個 Request 產生影響。</p>
</li>
<li>
<p>Node.js 因為所有 Request 都在單執行緒的 Web-server 中，所以某個 Request 導致的未知錯誤就可能影響到整個 Server。</p>
<blockquote>
<p>不過這塊可以透過 PM2 的 Cluster Mode 與自動重啟來補救。</p>
</blockquote>
</li>
</ul>
</li>
<li><strong>熱更新</strong>
<ul>
<li>PHP 修改後，只要刷新頁面就能看到。</li>
<li>Node.js 預設沒有熱更新，需要關閉專案後重啟。</li>
</ul>
</li>
</ul>
<hr>
<h3 class="h-nomark" id="22-用-supervisor-讓-nodejs-熱更新">2.2 用 supervisor 讓 Node.js 熱更新<a class="h-anchor" href="#22-%e7%94%a8-supervisor-%e8%ae%93-nodejs-%e7%86%b1%e6%9b%b4%e6%96%b0" aria-label="這個段落的連結"></a></h3>
<p>在開發階段推薦使用 supervisor 這款套件。</p>
<ul>
<li>
<p><strong>STEP 1</strong>：在全域安裝套件<code>npm install supervisor -g</code></p>
</li>
<li>
<p><strong>STEP 2</strong>：建立一個<code>myapp.js</code>的檔案，並複製貼上下面程式</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#ff7b72">var</span> http <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#34;http&#34;</span>);
</span></span><span style="display:flex;"><span>http
</span></span><span style="display:flex;"><span>  .createServer(<span style="color:#ff7b72">function</span> (req, res) {
</span></span><span style="display:flex;"><span>    res.writeHead(<span style="color:#a5d6ff">200</span>, { <span style="color:#a5d6ff">&#34;Content-Type&#34;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#34;text/plain&#34;</span> });
</span></span><span style="display:flex;"><span>    res.end(<span style="color:#a5d6ff">&#34;Hello World!&#34;</span>);
</span></span><span style="display:flex;"><span>  })
</span></span><span style="display:flex;"><span>  .listen(<span style="color:#a5d6ff">3000</span>, <span style="color:#a5d6ff">&#34;127.0.0.1&#34;</span>);
</span></span></code></pre></div></li>
<li>
<p><strong>STEP 3</strong>：用<code>node myapp.js</code>啟動程式後，修改<code>res.end(&quot;Hello World!&quot;);</code>的內容；再刷新網頁後<mark>內容不變</mark>。</p>
</li>
<li>
<p><strong>STEP 4</strong>：改用<code>supervisor myapp.js</code>啟動程式，修改<code>res.end(&quot;Hello World!&quot;);</code>的內容；再刷新網頁後<mark>內容更新</mark>。</p>
</li>
</ul>
<p>supervisor 也可以做專案資料夾的監控，但是要記得排除一些資料夾（ex：儲存 log 檔的）；否則你的專案就無時無刻在重起了。</p>
<blockquote>
<p>如果是正式的網站且流量大，建議使用 PM2 來做處理，它也有監控資料夾的功能。</p>
</blockquote>
<hr>
<h3 class="h-nomark" id="23-不一定要拼個誰輸誰贏">2.3 不一定要拼個誰輸誰贏<a class="h-anchor" href="#23-%e4%b8%8d%e4%b8%80%e5%ae%9a%e8%a6%81%e6%8b%bc%e5%80%8b%e8%aa%b0%e8%bc%b8%e8%aa%b0%e8%b4%8f" aria-label="這個段落的連結"></a></h3>
<blockquote>
<p>存在即合理</p>
</blockquote>
<ul>
<li><strong>小專案不用花太多時間在評估框架及程式語言選擇上</strong><br>有些公司對專案要用什麼框架以及程式語言保持著非常謹慎的態度，謹慎的態度是對的，但也要考量到專案的規模；如果今天的專案就是 CRUD 的簡單應用，<mark>預估使用者數量不多或是主要來自於內部成員，用什麼框架及程式語言其實都差不多</mark>。</li>
<li><strong>看組織成員擅長的語言</strong><br>如果公司高層為了追求潮流而要求開發人員用一個不熟悉的框架去做專案，他最好祈求專案上線後平平安安；不然<mark>出事了公司沒有能夠處理的人才</mark>，沒有人會在意一個不能用的網站效能有多好。</li>
<li><strong>每個程式語言都有自己的擁護者</strong><br>在不同的條件下，每個程式語言都有自己的優勢，而且每年都會誕生新的框架與程式語言；筆者覺得與其不停盲目追逐新的技術，不如先深入研究一個成熟的技術，<mark>當你有辦法靠它解決各種現實難題時，再去學新的框架跟程式語言也不晚</mark>。</li>
</ul>
<hr>
<h2 class="h-nomark" id="3-衍伸問題">3. 衍伸問題<a class="h-anchor" href="#3-%e8%a1%8d%e4%bc%b8%e5%95%8f%e9%a1%8c" aria-label="這個段落的連結"></a></h2>
<h3 class="h-nomark" id="31-既然你先學了-nodejs-的框架為何後來改用-php-的框架">3.1 既然你先學了 Node.js 的框架，為何後來改用 PHP 的框架？<a class="h-anchor" href="#31-%e6%97%a2%e7%84%b6%e4%bd%a0%e5%85%88%e5%ad%b8%e4%ba%86-nodejs-%e7%9a%84%e6%a1%86%e6%9e%b6%e7%82%ba%e4%bd%95%e5%be%8c%e4%be%86%e6%94%b9%e7%94%a8-php-%e7%9a%84%e6%a1%86%e6%9e%b6" aria-label="這個段落的連結"></a></h3>
<p><mark>考點：這題其實是想了解求職者的人格特質</mark></p>
<p>這兩個都是為了解決問題的技術，我一開始用 Node.js 的 Express 框架<mark>獨立完成</mark>了幾份專案；但後來有一個<code>需要 Team work</code> 的大型專案，因為當時的組長比較熟悉 PHP 的 Laravel 框架，所以我便藉由這個機會<mark>學習新的技術</mark>；也因為這次的技術轉換，讓我<mark>了解不同框架在部署以及效能各方面的差異</mark>。</p>
<blockquote>
<p><strong>我想透過上面的回答給面試官幾個訊息：</strong></p>
<ul>
<li>我能獨立完成專案，也可以團隊合作。</li>
<li>我願意學習新技術。</li>
<li>學習的同時會分析不同框架的優劣。</li>
</ul>
</blockquote>
<hr>
<h3 class="h-nomark" id="32-使用框架有什麼好處跟壞處">3.2 使用框架有什麼好處跟壞處？<a class="h-anchor" href="#32-%e4%bd%bf%e7%94%a8%e6%a1%86%e6%9e%b6%e6%9c%89%e4%bb%80%e9%ba%bc%e5%a5%bd%e8%99%95%e8%b7%9f%e5%a3%9e%e8%99%95" aria-label="這個段落的連結"></a></h3>
<p><mark>考點：確認求職者對框架的認知</mark></p>
<ul>
<li><strong>好處</strong><br>框架能夠<mark>讓初學者快速入門並開發出有一定水平</mark>的專案，因為框架提供了許多常用的函式，因此可以<mark>大幅縮短專案時程</mark>；在團隊協作上框架優勢更為明顯，能夠讓成員<mark>有一致的開發方式</mark>。</li>
<li><strong>壞處</strong><br>因為框架已經幫開發人員做了太多事情，這會導致<mark>開發人員並不理解許多功能的原理</mark>，這也造成了<mark>一但框架無法滿足功能，初學者的生產力會斷崖式下降</mark>；如果長期使用，也容易<mark>讓自己的開發思維被限制在框架內</mark>。</li>
</ul>
<hr>
<h3 id="筆者碎碎念">筆者碎碎念<a class="h-anchor" href="#%e7%ad%86%e8%80%85%e7%a2%8e%e7%a2%8e%e5%bf%b5" aria-label="這個段落的連結"></a></h3>
<p>希望讀者不要只把我提出來的問題當成面試會出的考題；當面試官會這樣詢問時，通常是因為實務上也會遇到類似的問題。</p>
<p>感謝大家的閱讀，如果喜歡我的文章可以<code>訂閱</code>接收通知；如果有幫助到你，按<code>Like</code>可以讓我更有寫文的動力，我們明天見～</p>
<h3 id="參考資源">參考資源：<a class="h-anchor" href="#%e5%8f%83%e8%80%83%e8%b3%87%e6%ba%90" aria-label="這個段落的連結"></a></h3>
<ol>
<li><a href="https://thomashunter.name/posts/2012-06-21-php-vs-nodejs">PHP + Apache Stack vs Node.js</a></li>
<li><a href="https://web.archive.org/web/20220124190306/https://www.itread01.com/content/1549876684.html" title="原網頁已失效，連到網際網路檔案館的存檔">服務端 I/O 效能大比拼：Node、PHP、Java、Go</a></li>
<li><a href="https://jishuin.proginn.com/p/763bfbd2cab5">簡析 Node.js 特點與應用場景</a></li>
<li><a href="https://www.jianshu.com/p/6d84e5efe99d">詳細版 | 用 Supervisor 守護你的 Node.js 進程</a></li>
<li><a href="https://iter01.com/403097.html">淺談 Node.js 和 PHP 程式管理</a></li>
</ol>
<hr>
<hr>
<blockquote>
<p><img src="https://deanlin.net/blog/202109/fullstack-career-notes-day-13/images/fullstack-career-notes-day-13-01.webp" width="1024" height="233" alt="參考資源：（圖 1）" fetchpriority="high" decoding="async"></p>
<p>在許多人的幫助下，本系列文章已成功出版，除了添加新的篇章，更完善了每個案例的應對進退；如果對現在的職涯感到迷茫，也許這本書能帶給你不一樣的觀點～</p>
<p><strong>天瓏書局：</strong> <a href="https://www.tenlong.com.tw/products/9786263334571">https://www.tenlong.com.tw/products/9786263334571</a></p>
</blockquote>
]]></content:encoded>
    </item>
    <item>
      <title>用 Node.js 享受 Cloud MongoDB 吧</title>
      <link>https://deanlin.net/blog/202108/nodejs-mongodb-atlas/</link>
      <pubDate>Thu, 26 Aug 2021 21:53:02 +0800</pubDate>
      <guid>https://deanlin.net/blog/202108/nodejs-mongodb-atlas/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>你知道有免費的 Cloud MongoDB 資源可以使用嗎？雖然跟著官方教學走就能順利使用，但有些小細節我還是補充一下避免新手擔心，希望大家可以無障礙的使用這些資源～</description>
      <media:content url="https://deanlin.net/blog/202108/nodejs-mongodb-atlas/images/nodejs-mongodb-atlas-og_hu_6a09ad8cf4161e98.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<p>你知道有免費的 Cloud MongoDB 資源可以使用嗎？雖然跟著官方教學走就能順利使用，但有些小細節我還是補充一下避免新手擔心，希望大家可以無障礙的使用這些資源～</p>
<p><img src="https://deanlin.net/blog/202108/nodejs-mongodb-atlas/images/nodejs-mongodb-atlas-01.webp" width="1400" height="785" alt="用 Node.js 享受 Cloud MongoDB 吧（圖 1）" fetchpriority="high" decoding="async"></p>
<pre tabindex="0"><code>大綱
一、建立雲端 MongoDB
  SETP 1：註冊帳號
  SETP 2：建立組織(Organization)
  SETP 3：建立專案(Project)
  SETP 4：建立叢集（Build your first cluster）
  SETP 5：建立 DB 使用者（Create your first database user）
  SETP 6：加入可以存取的 IP（Add IP Address to your Access List）
  SETP 7：載入範例資料（Load Sample Data）
  SETP 8：連線到這個叢集（Connect to your cluster）
二、用 Node.js 取得雲端 MongoDB 的資料
</code></pre><h2 id="一建立雲端-mongodb">一、建立雲端 MongoDB<a class="h-anchor" href="#%e4%b8%80%e5%bb%ba%e7%ab%8b%e9%9b%b2%e7%ab%af-mongodb" aria-label="這個段落的連結"></a></h2>
<p><strong>SETP 1</strong>：註冊帳號</p>
<p>建議直接用 Google 帳號登入就好了，這個是真的有「免費」資源讓大家使用的。</p>
<p><img src="https://deanlin.net/blog/202108/nodejs-mongodb-atlas/images/nodejs-mongodb-atlas-02.webp" width="1304" height="616" alt="一、建立雲端 MongoDB（圖 2）" loading="lazy" decoding="async"></p>
<p>基本上你只打勾才能下一步啦</p>
<p><img src="https://deanlin.net/blog/202108/nodejs-mongodb-atlas/images/nodejs-mongodb-atlas-03.webp" width="1304" height="315" alt="一、建立雲端 MongoDB（圖 3）" loading="lazy" decoding="async"></p>
<p><strong>SETP 2</strong>：建立組織(Organization)</p>
<blockquote>
<p>左下角的「Get Started」是官方教學，但如果在這個階段「Build your first cluster」會失敗 QQ，所以建議按照文章操作順序比較穩定</p>
</blockquote>
<p>點擊「Create an Organization」</p>
<p><img src="https://deanlin.net/blog/202108/nodejs-mongodb-atlas/images/nodejs-mongodb-atlas-04.webp" width="1315" height="879" alt="一、建立雲端 MongoDB（圖 4）" loading="lazy" decoding="async"></p>
<p>輸入組織名稱後點擊「Next」</p>
<p><img src="https://deanlin.net/blog/202108/nodejs-mongodb-atlas/images/nodejs-mongodb-atlas-05.webp" width="1315" height="724" alt="一、建立雲端 MongoDB（圖 5）" loading="lazy" decoding="async"></p>
<p>點擊「Create Organization」建立組織，你之後還是可以邀請其他人進來這個組織，不急於現在。</p>
<p><img src="https://deanlin.net/blog/202108/nodejs-mongodb-atlas/images/nodejs-mongodb-atlas-06.webp" width="1141" height="724" alt="一、建立雲端 MongoDB（圖 6）" loading="lazy" decoding="async"></p>
<p><strong>SETP 3</strong>：建立專案(Project)</p>
<p>點擊「New Project」</p>
<p><img src="https://deanlin.net/blog/202108/nodejs-mongodb-atlas/images/nodejs-mongodb-atlas-07.webp" width="1322" height="518" alt="一、建立雲端 MongoDB（圖 7）" loading="lazy" decoding="async"></p>
<p>輸入專案名稱</p>
<p><img src="https://deanlin.net/blog/202108/nodejs-mongodb-atlas/images/nodejs-mongodb-atlas-08.webp" width="1322" height="518" alt="一、建立雲端 MongoDB（圖 8）" loading="lazy" decoding="async"></p>
<p>點擊「Create Project」建立專案</p>
<p><img src="https://deanlin.net/blog/202108/nodejs-mongodb-atlas/images/nodejs-mongodb-atlas-09.webp" width="1169" height="689" alt="一、建立雲端 MongoDB（圖 9）" loading="lazy" decoding="async"></p>
<hr>
<p><strong>SETP 4</strong>：建立叢集（Build your first cluster）</p>
<p>選最右邊「FREE」的</p>
<p><img src="https://deanlin.net/blog/202108/nodejs-mongodb-atlas/images/nodejs-mongodb-atlas-10.webp" width="1046" height="941" alt="一、建立雲端 MongoDB（圖 10）" loading="lazy" decoding="async"></p>
<p>這邊我是選擇「Google Cloud」在「Taiwan」的機器，選好後按「Create Cluster」</p>
<p><img src="https://deanlin.net/blog/202108/nodejs-mongodb-atlas/images/nodejs-mongodb-atlas-11.webp" width="1046" height="959" alt="一、建立雲端 MongoDB（圖 11）" loading="lazy" decoding="async"></p>
<p><strong>SETP 5</strong>：建立 DB 使用者（Create your first database user）</p>
<p>點擊「Add New Database User」建立使用者</p>
<p><img src="https://deanlin.net/blog/202108/nodejs-mongodb-atlas/images/nodejs-mongodb-atlas-12.webp" width="1400" height="809" alt="一、建立雲端 MongoDB（圖 12）" loading="lazy" decoding="async"></p>
<p>他有提供三種驗證方式，如果是初學者我建議選擇「Password」就好，而密碼我建議用「Autogenerate Secure Password」產生的比較安全；其他權限都先使用默認就好，按下「Add User」便建立成功～</p>
<p><img src="https://deanlin.net/blog/202108/nodejs-mongodb-atlas/images/nodejs-mongodb-atlas-13.webp" width="1400" height="809" alt="一、建立雲端 MongoDB（圖 13）" loading="lazy" decoding="async"></p>
<p><strong>SETP 6</strong>：加入可以存取的 IP（Add IP Address to your Access List）</p>
<p>點擊「Add IP Address」加入 IP</p>
<p><img src="https://deanlin.net/blog/202108/nodejs-mongodb-atlas/images/nodejs-mongodb-atlas-14.webp" width="1400" height="809" alt="一、建立雲端 MongoDB（圖 14）" loading="lazy" decoding="async"></p>
<p>如果你電腦的 IP 是固定的就選「ADD CURRENT IP ADDRESS」，如果是使用手機分享或是其他浮動 IP，你可以選「ALLOW ACCESS FROM ANYWHERE」，但如果選了這個就真的要小心保護你的帳號密碼不要外流</p>
<p><img src="https://deanlin.net/blog/202108/nodejs-mongodb-atlas/images/nodejs-mongodb-atlas-15.webp" width="905" height="547" alt="一、建立雲端 MongoDB（圖 15）" loading="lazy" decoding="async"></p>
<p><strong>SETP 7</strong>：載入範例資料（Load Sample Data）</p>
<p>為了方便讓大家測試與學習，官方提供範例資料讓大家使用</p>
<p><img src="https://deanlin.net/blog/202108/nodejs-mongodb-atlas/images/nodejs-mongodb-atlas-16.webp" width="1400" height="554" alt="一、建立雲端 MongoDB（圖 16）" loading="lazy" decoding="async"></p>
<p>點擊「Load Sample Dataset」即可，大概等個 5~8 分鐘左右載入</p>
<p><img src="https://deanlin.net/blog/202108/nodejs-mongodb-atlas/images/nodejs-mongodb-atlas-17.webp" width="773" height="409" alt="一、建立雲端 MongoDB（圖 17）" loading="lazy" decoding="async"></p>
<p><strong>SETP 8</strong>：連線到這個叢集（Connect to your cluster）</p>
<p>點擊「Connect」來看連線的方式</p>
<p><img src="https://deanlin.net/blog/202108/nodejs-mongodb-atlas/images/nodejs-mongodb-atlas-18.webp" width="1214" height="405" alt="一、建立雲端 MongoDB（圖 18）" loading="lazy" decoding="async"></p>
<p>這裡我推薦使用「MongoDB Compass」，有 GUI 介面就是友善～</p>
<p><img src="https://deanlin.net/blog/202108/nodejs-mongodb-atlas/images/nodejs-mongodb-atlas-19.webp" width="892" height="693" alt="一、建立雲端 MongoDB（圖 19）" loading="lazy" decoding="async"></p>
<p>下載及安裝的方式請依照自己的作業系統處理，這裏我就不贅述了；但下面連線字串真的要複製起來等等連線要用</p>
<p><img src="https://deanlin.net/blog/202108/nodejs-mongodb-atlas/images/nodejs-mongodb-atlas-20.webp" width="892" height="812" alt="一、建立雲端 MongoDB（圖 20）" loading="lazy" decoding="async"></p>
<p>打開安裝好的「MongoDB Compass」後，在框框內貼上剛剛複製的連線字串，記得要把 <code>&lt;password&gt;</code> 換成在 「STEP 5 建立 DB 使用者」的密碼</p>
<p><img src="https://deanlin.net/blog/202108/nodejs-mongodb-atlas/images/nodejs-mongodb-atlas-21.webp" width="1274" height="313" alt="一、建立雲端 MongoDB（圖 21）" loading="lazy" decoding="async"></p>
<p>連進去後就能看到範例資料嚕～</p>
<p><img src="https://deanlin.net/blog/202108/nodejs-mongodb-atlas/images/nodejs-mongodb-atlas-22.webp" width="1276" height="782" alt="一、建立雲端 MongoDB（圖 22）" loading="lazy" decoding="async"></p>
<hr>
<h2 id="二用-nodejs-取得雲端-mongodb-的資料">二、用 Node.js 取得雲端 MongoDB 的資料<a class="h-anchor" href="#%e4%ba%8c%e7%94%a8-nodejs-%e5%8f%96%e5%be%97%e9%9b%b2%e7%ab%af-mongodb-%e7%9a%84%e8%b3%87%e6%96%99" aria-label="這個段落的連結"></a></h2>
<p><strong>STEP 1</strong>：建立一個資料夾，輸入 <code>npm init</code></p>
<p><strong>SETP 2</strong>：安裝 MongoDB 套件 <code>npm install mongodb</code></p>
<p><strong>STEP 3</strong>：建立 <code>index.js</code> 並貼上官方範例程式</p>
<p><strong>SETP 4</strong>：將 <code>uri</code> 的內容要改成自己的（你怎麼連線到 MongoDB Compass 這裡就是怎麼設定）</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#ff7b72">const</span> { MongoClient } <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#34;mongodb&#34;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#8b949e;font-style:italic">// Replace the uri string with your MongoDB deployment&#39;s connection string.
</span></span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> uri <span style="color:#ff7b72;font-weight:bold">=</span>
</span></span><span style="display:flex;"><span>  <span style="color:#a5d6ff">&#34;mongodb+srv://&lt;user&gt;:&lt;password&gt;@&lt;cluster-url&gt;?retryWrites=true&amp;writeConcern=majority&#34;</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> client <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">new</span> MongoClient(uri);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">async</span> <span style="color:#ff7b72">function</span> run() {
</span></span><span style="display:flex;"><span>  <span style="color:#ff7b72">try</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">await</span> client.connect();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> database <span style="color:#ff7b72;font-weight:bold">=</span> client.db(<span style="color:#a5d6ff">&#39;sample_mflix&#39;</span>);
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> movies <span style="color:#ff7b72;font-weight:bold">=</span> database.collection(<span style="color:#a5d6ff">&#39;movies&#39;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// Query for a movie that has the title &#39;Back to the Future&#39;
</span></span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> query <span style="color:#ff7b72;font-weight:bold">=</span> { title<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;Back to the Future&#39;</span> };
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> movie <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> movies.findOne(query);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    console.log(movie);
</span></span><span style="display:flex;"><span>  } <span style="color:#ff7b72">finally</span> {
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// Ensures that the client will close when you finish/error
</span></span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">await</span> client.close();
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>run().<span style="color:#ff7b72">catch</span>(console.dir);
</span></span></code></pre></div><p class="code-source">原始碼：<a href="https://gist.github.com/dean9703111/f24587554ce195dc3249ff68f7bdd3ff" rel="noopener">Mongo_example.js</a>（GitHub Gist）</p>
<p><strong>STEP 5</strong>：在終端機輸入 <code>node index.js</code></p>
<p>如果順利印出下面的資訊就代表你成功踏入 MongoDB 學習的第一步嚕～</p>
<p><img src="https://deanlin.net/blog/202108/nodejs-mongodb-atlas/images/nodejs-mongodb-atlas-23.webp" width="1400" height="539" alt="二、用 Node.js 取得雲端 MongoDB 的資料（圖 23）" loading="lazy" decoding="async"></p>
<p><strong>STEP 6</strong>：如果你有驗證的心，可以回到 MongoDB Compass</p>
<p>選好 DB 以及 Collection 後，在 filter 輸入 <code>{ title: &lsquo;Back to Future&rsquo; }</code> ，確認回傳結果是否與 Node.js 查到的相同</p>
<p><img src="https://deanlin.net/blog/202108/nodejs-mongodb-atlas/images/nodejs-mongodb-atlas-24.webp" width="1275" height="776" alt="二、用 Node.js 取得雲端 MongoDB 的資料（圖 24）" loading="lazy" decoding="async"></p>
]]></content:encoded>
    </item>
    <item>
      <title>用 Node.js &#43; Redis 解決高併發秒殺問題</title>
      <link>https://deanlin.net/blog/202108/nodejs-redis-flash-sale-concurrency/</link>
      <pubDate>Wed, 18 Aug 2021 20:06:42 +0800</pubDate>
      <guid>https://deanlin.net/blog/202108/nodejs-redis-flash-sale-concurrency/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>商城在高併發環境下除了要考量到使用者體驗外，還要處理「超賣」的問題；Redis 是一種記憶體資料庫，它有高讀寫效率的特性、能承受高 qps，而這篇文章則是運用它獨特的「鎖」解決超賣問題。</description>
      <media:content url="https://deanlin.net/blog/202108/nodejs-redis-flash-sale-concurrency/images/nodejs-redis-flash-sale-concurrency-og_hu_6a3c65a275786513.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<p>商城在高併發環境下除了要考量到使用者體驗外，還要處理「超賣」的問題；Redis 是一種記憶體資料庫，它有高讀寫效率的特性、能承受高 qps，而這篇文章則是運用它獨特的「鎖」解決超賣問題。</p>
<p><img src="https://deanlin.net/blog/202108/nodejs-redis-flash-sale-concurrency/images/nodejs-redis-flash-sale-concurrency-01.webp" width="1087" height="608" alt="用 Node.js &#43; Redis 解決高併發秒殺問題（圖 1）" fetchpriority="high" decoding="async"></p>
<pre tabindex="0"><code>大綱
一、文章目標
二、使用技術
  ➤ Redis
  ➤ Lua
三、程式架構
  ➤ 主程式：redisSecKill.js
  ➤ Lua腳本：secKill.lua
四、執行結果
五、補充
</code></pre><h2 id="一文章目標">一、文章目標<a class="h-anchor" href="#%e4%b8%80%e6%96%87%e7%ab%a0%e7%9b%ae%e6%a8%99" aria-label="這個段落的連結"></a></h2>
<ol>
<li>產生秒殺商品基礎資訊(商品名稱、庫存)</li>
<li>模擬秒殺情境，確認是否會超賣</li>
<li>確認有保存購買人資訊</li>
</ol>
<hr>
<h2 id="二使用技術">二、使用技術<a class="h-anchor" href="#%e4%ba%8c%e4%bd%bf%e7%94%a8%e6%8a%80%e8%a1%93" aria-label="這個段落的連結"></a></h2>
<h3 id="-redis">Redis<a class="h-anchor" href="#-redis" aria-label="這個段落的連結"></a></h3>
<ol>
<li>如果完全沒有相關基礎，建議先參考<a href="https://deanlin.net/blog/202108/install-redis-on-macos/">這篇文章</a>來安裝 Redis 以及他的 GUI 工具</li>
<li>本篇文章使用 Redis 的 Hash、List type 來儲存資訊</li>
</ol>
<h3 id="-lua">Lua<a class="h-anchor" href="#-lua" aria-label="這個段落的連結"></a></h3>
<ol>
<li>為了避免超賣，這裡採用 Lua 腳本</li>
<li>在 Redis Server 執行 EVAL 指令時，在結果回傳前只會執行當下 Lua 腳本的邏輯，其他 Client 端的命令須等待直到 EVAL 執行完為止。</li>
</ol>
<blockquote>
<p>Lua 腳本的邏輯應盡量簡單以保證執行效率，否則會影響 Client 端的體驗</p>
</blockquote>
<hr>
<h2 id="三程式架構">三、程式架構<a class="h-anchor" href="#%e4%b8%89%e7%a8%8b%e5%bc%8f%e6%9e%b6%e6%a7%8b" aria-label="這個段落的連結"></a></h2>
<h3 id="-主程式redisseckilljs">主程式：redisSecKill.js<a class="h-anchor" href="#-%e4%b8%bb%e7%a8%8b%e5%bc%8fredisseckilljs" aria-label="這個段落的連結"></a></h3>
<ol>
<li>先啟用 Redis Client 端（如未安裝相關套件，請在專案資料夾下輸入 <code>yarn add ioredis</code>）</li>
<li>用 <strong>prepare</strong> 函式，以 Hash type 建立參加秒殺的產品庫存</li>
<li>用 <strong>secKill</strong> 函式模擬使用者購買行為，緩存並執行 Lua 腳本</li>
</ol>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#ff7b72">const</span> fs <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;fs&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> Redis <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;ioredis&#39;</span>);
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> redis <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">new</span> Redis({
</span></span><span style="display:flex;"><span>    host<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;127.0.0.1&#39;</span>,
</span></span><span style="display:flex;"><span>    port<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">6379</span>,
</span></span><span style="display:flex;"><span>    password<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;&#39;</span>
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>redis.on(<span style="color:#a5d6ff">&#34;error&#34;</span>, <span style="color:#ff7b72">function</span> (error) {
</span></span><span style="display:flex;"><span>    console.error(error);
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">async</span> <span style="color:#ff7b72">function</span> prepare (item_name) {
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// 參加秒殺活動的商品庫存
</span></span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">await</span> redis.hmset(item_name, <span style="color:#a5d6ff">&#34;Total&#34;</span>, <span style="color:#a5d6ff">100</span>, <span style="color:#a5d6ff">&#34;Booked&#34;</span>, <span style="color:#a5d6ff">0</span>);
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">const</span> secKillScript <span style="color:#ff7b72;font-weight:bold">=</span> fs.readFileSync(<span style="color:#a5d6ff">&#39;./secKill.lua&#39;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">async</span> <span style="color:#ff7b72">function</span> secKill (item_name, user_name) {
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// 1. 緩存腳本取得 sha1 值
</span></span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> sha1 <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">await</span> redis.script(<span style="color:#a5d6ff">&#34;load&#34;</span>, secKillScript);
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// console.log(sha1);
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// 2. 透過 evalsha 執行腳本
</span></span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// redis Evalsha 命令基本語法如下
</span></span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// EVALSHA sha1 numkeys key [key ...] arg [arg ...] 
</span></span></span><span style="display:flex;"><span>    redis.evalsha(sha1, <span style="color:#a5d6ff">1</span>, item_name, <span style="color:#a5d6ff">1</span>, <span style="color:#a5d6ff">&#34;order_list&#34;</span>, user_name);
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">async</span> <span style="color:#ff7b72">function</span> main () {
</span></span><span style="display:flex;"><span>    console.time(<span style="color:#a5d6ff">&#39;secKill&#39;</span>)
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> item_name <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#34;item_name&#34;</span>
</span></span><span style="display:flex;"><span>    prepare(item_name)
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">for</span> (<span style="color:#ff7b72">var</span> i <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">1</span>; i <span style="color:#ff7b72;font-weight:bold">&lt;</span> <span style="color:#a5d6ff">10000</span>; i<span style="color:#ff7b72;font-weight:bold">++</span>) {
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">const</span> user_name <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#a5d6ff">&#34;baobao&#34;</span> <span style="color:#ff7b72;font-weight:bold">+</span> i
</span></span><span style="display:flex;"><span>        secKill(item_name, user_name)
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>    console.timeEnd(<span style="color:#a5d6ff">&#39;secKill&#39;</span>)
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>main()
</span></span></code></pre></div><p class="code-source">原始碼：<a href="https://gist.github.com/dean9703111/64cfadcce24bc32d5fabbafd07da98a6" rel="noopener">redisSecKill.js</a>（GitHub Gist）</p>
<h3 id="-lua-腳本seckilllua">Lua 腳本：secKill.lua<a class="h-anchor" href="#-lua-%e8%85%b3%e6%9c%acseckilllua" aria-label="這個段落的連結"></a></h3>
<p>在 Lua 腳本執行「取得商品庫存」➜「如果庫存足夠就-1」➜「儲存購買者資訊(List type)」</p>
<pre tabindex="0"><code>local n = tonumber(ARGV[1])
local order_name = ARGV[2]
if not n  or n == 0 then
    return 0       
end                
local vals = redis.call(&#34;HMGET&#34;, KEYS[1], &#34;Total&#34;, &#34;Booked&#34;);
local total = tonumber(vals[1])
local blocked = tonumber(vals[2])
if not total or not blocked then
    return 0       
end                
if blocked + n &lt;= total then
    redis.call(&#34;HINCRBY&#34;, KEYS[1], &#34;Booked&#34;, n)
    redis.call(&#34;LPUSH&#34;, order_name,ARGV[3])                                   
    return n;   
end                
return 0
</code></pre><p class="code-source">原始碼：<a href="https://gist.github.com/dean9703111/d5981082cc8c93f1c51fb785a3895f99" rel="noopener">secKill.lua</a>（GitHub Gist）</p>
<hr>
<h2 id="四執行結果">四、執行結果<a class="h-anchor" href="#%e5%9b%9b%e5%9f%b7%e8%a1%8c%e7%b5%90%e6%9e%9c" aria-label="這個段落的連結"></a></h2>
<p><strong>SETP 1</strong>：在專案資料夾下輸入 <code>node redisSecKill.js</code> 模擬秒殺</p>
<p><img src="https://deanlin.net/blog/202108/nodejs-redis-flash-sale-concurrency/images/nodejs-redis-flash-sale-concurrency-02.webp" width="1019" height="73" alt="四、執行結果（圖 2）" loading="lazy" decoding="async"></p>
<p><strong>SETP 2</strong>：待執行完後，用 Redis GUI 程式確認商品沒有超賣</p>
<p><img src="https://deanlin.net/blog/202108/nodejs-redis-flash-sale-concurrency/images/nodejs-redis-flash-sale-concurrency-03.webp" width="1400" height="563" alt="四、執行結果（圖 3）" loading="lazy" decoding="async"></p>
<p><strong>SETP 3</strong>：確認訂單都有對應的買家</p>
<p><img src="https://deanlin.net/blog/202108/nodejs-redis-flash-sale-concurrency/images/nodejs-redis-flash-sale-concurrency-04.webp" width="1400" height="1004" alt="四、執行結果（圖 4）" loading="lazy" decoding="async"></p>
<h2 id="五補充">五、補充<a class="h-anchor" href="#%e4%ba%94%e8%a3%9c%e5%85%85" aria-label="這個段落的連結"></a></h2>
<p>上面程式只是 MVP，現實狀況還有很多要討論的：</p>
<ol>
<li>哪些商品被列為秒殺，如何紀錄</li>
<li>設定秒殺開始時間、結束時間</li>
<li>存在 Redis 的資料怎麼存入關連式資料庫中</li>
</ol>
<pre tabindex="0"><code>如果你對 Redis 感興趣，筆者這邊也整理了很多相關資源：
1. 一文讓你徹底瞭解 Redis(進階)，史上最全,不看後悔！！！【建議收藏】
2. 【总结】瞬时高并发（秒杀/活动）Redis 方案
3. redis 之 sorted sets 类型及操作
4. node.js 中对 redis 的安装和基本操作
5. Node.js 中实践 Redis Lua 脚本
6. 使用 Redis 搭建电商秒杀系统
</code></pre><blockquote class="pullquote"><p>筆者也是剛學習 Redis 的新人，如果描述有誤，或是有更好的解決方式，再麻煩大神們不吝賜教，感謝～</p>
</blockquote>
]]></content:encoded>
    </item>
    <item>
      <title>Node.js 的 Event Loop (事件輪詢)到底在做什麼？</title>
      <link>https://deanlin.net/blog/202108/nodejs-event-loop/</link>
      <pubDate>Tue, 10 Aug 2021 16:13:57 +0800</pubDate>
      <guid>https://deanlin.net/blog/202108/nodejs-event-loop/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>因為網路上的教學文我大部分都有看沒有懂，只好自己實作一遍來了解他的運作機制；希望我這篇文章足夠口語化，讓讀者能輕鬆理解，如果有解釋錯誤或是不夠清晰的部分再煩請告知，謝謝。</description>
      <media:content url="https://deanlin.net/blog/202108/nodejs-event-loop/images/nodejs-event-loop-og_hu_37b29e95635eaae5.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<p>因為網路上的教學文我大部分都有看沒有懂，只好自己實作一遍來了解他的運作機制；希望我這篇文章足夠口語化，讓讀者能輕鬆理解，如果有解釋錯誤或是不夠清晰的部分再煩請告知，謝謝。</p>
<p><img src="https://deanlin.net/blog/202108/nodejs-event-loop/images/nodejs-event-loop-01.webp" width="1400" height="788" alt="Node.js 的 Event Loop (事件輪詢)到底在做什麼？（圖 1）" fetchpriority="high" decoding="async"></p>
<pre tabindex="0"><code>大綱
一、請用一段話簡述 Node.js 的 Event Loop
二、Node.js 在 Event Loop 邏輯下的執行順序
三、Event Loop 執行的邏輯
  ➤ 了解 MacroTask 的 6 個階段
  ➤ NextTick Queue 優先層級最高
  ➤ MicroTask Queue 優先層級次高
四、解析一開始 Node.js 範例程式執行順序
  ➤ 順序執行，先把同步函式解決
  ➤ 進入 Event Loop
</code></pre><h2 id="一請用一段話簡述-nodejs-的-event-loop">一、請用一段話簡述 Node.js 的 Event Loop<a class="h-anchor" href="#%e4%b8%80%e8%ab%8b%e7%94%a8%e4%b8%80%e6%ae%b5%e8%a9%b1%e7%b0%a1%e8%bf%b0-nodejs-%e7%9a%84-event-loop" aria-label="這個段落的連結"></a></h2>
<p>「Node.js 之所以高效，是因為採取單執行緒與 Event Loop 的概念；將所有需要等待結果、請求外部資源的函式，全部丟到 Event Loop 中等待；而 Event Loop 的邏輯是 Node.js 底層用 C 語言撰寫的 libuv 庫來運行的。」</p>
<p>有興趣的朋友可以來 GitHub 上看 libuv 的程式：</p>
<ol>
<li>前往 libuv 的 <a href="https://github.com/libuv/libuv">GitHub</a></li>
<li>看運作的核心程式碼「<a href="https://github.com/libuv/libuv/blob/v1.x/src/unix/core.c">core.c</a>」</li>
<li>搜尋「uv_run」，直接看最原本的邏輯</li>
</ol>
<p><img src="https://deanlin.net/blog/202108/nodejs-event-loop/images/nodejs-event-loop-02.webp" width="868" height="832" alt="一、請用一段話簡述 Node.js 的 Event Loop（圖 2）" loading="lazy" decoding="async"></p>
<hr>
<h2 id="二nodejs-在-event-loop-邏輯下的執行順序">二、Node.js 在 Event Loop 邏輯下的執行順序<a class="h-anchor" href="#%e4%ba%8cnodejs-%e5%9c%a8-event-loop-%e9%82%8f%e8%bc%af%e4%b8%8b%e7%9a%84%e5%9f%b7%e8%a1%8c%e9%a0%86%e5%ba%8f" aria-label="這個段落的連結"></a></h2>
<p>大家可以先用大腦模擬看看下面程式的執行順序：</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span>console.log(<span style="color:#a5d6ff">&#39;start&#39;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>process.nextTick(<span style="color:#ff7b72">function</span> () {
</span></span><span style="display:flex;"><span>    console.log(<span style="color:#a5d6ff">&#39;nextTick1&#39;</span>);
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>setTimeout(<span style="color:#ff7b72">function</span> () {
</span></span><span style="display:flex;"><span>    console.log(<span style="color:#a5d6ff">&#39;setTimeout&#39;</span>);
</span></span><span style="display:flex;"><span>}, <span style="color:#a5d6ff">0</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">new</span> Promise(<span style="color:#ff7b72">function</span> (resolve, reject) {
</span></span><span style="display:flex;"><span>    console.log(<span style="color:#a5d6ff">&#39;promise&#39;</span>);
</span></span><span style="display:flex;"><span>    resolve(<span style="color:#a5d6ff">&#39;resolve&#39;</span>);
</span></span><span style="display:flex;"><span>}).then(<span style="color:#ff7b72">function</span> (result) {
</span></span><span style="display:flex;"><span>    console.log(result);
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>(<span style="color:#ff7b72">async</span> <span style="color:#ff7b72">function</span> () {
</span></span><span style="display:flex;"><span>    console.log(<span style="color:#a5d6ff">&#39;async&#39;</span>);
</span></span><span style="display:flex;"><span>})();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>setImmediate(<span style="color:#ff7b72">function</span> () {
</span></span><span style="display:flex;"><span>    console.log(<span style="color:#a5d6ff">&#39;setImmediate&#39;</span>);
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>process.nextTick(<span style="color:#ff7b72">function</span> () {
</span></span><span style="display:flex;"><span>    console.log(<span style="color:#a5d6ff">&#39;nextTick2&#39;</span>);
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>console.log(<span style="color:#a5d6ff">&#39;end&#39;</span>);
</span></span></code></pre></div><p class="code-source">原始碼：<a href="https://gist.github.com/dean9703111/a8e641d53e5e81d7831b596a2e47af72" rel="noopener">event_loop.js</a>（GitHub Gist）</p>
<p>如果答案跟下面結果一樣，那代表你很了解 Node.js 的 Event Loop 邏輯：</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span>start
</span></span><span style="display:flex;"><span>promise
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">async</span>
</span></span><span style="display:flex;"><span>end
</span></span><span style="display:flex;"><span>nextTick1
</span></span><span style="display:flex;"><span>nextTick2
</span></span><span style="display:flex;"><span>resolve
</span></span><span style="display:flex;"><span>setTimeout
</span></span><span style="display:flex;"><span>setImmediate
</span></span></code></pre></div><hr>
<h2 id="三event-loop-執行的邏輯">三、Event Loop 執行的邏輯<a class="h-anchor" href="#%e4%b8%89event-loop-%e5%9f%b7%e8%a1%8c%e7%9a%84%e9%82%8f%e8%bc%af" aria-label="這個段落的連結"></a></h2>
<figure class="figure"><img src="https://deanlin.net/blog/202108/nodejs-event-loop/images/nodejs-event-loop-03.webp" width="1400" height="792" alt="Node.js 的 Event Loop" loading="lazy" decoding="async">
  <figcaption>Node.js 的 Event Loop</figcaption>
</figure>

<h3 id="-了解-macrotask-的-6-個階段">了解 MacroTask 的 6 個階段<a class="h-anchor" href="#-%e4%ba%86%e8%a7%a3-macrotask-%e7%9a%84-6-%e5%80%8b%e9%9a%8e%e6%ae%b5" aria-label="這個段落的連結"></a></h3>
<p><strong>Timer</strong>：等計時器（setTimeout、setInterval）的時間一到，會把他們的 callback 放在這裡等待執行。</p>
<p><strong>Pending callbacks</strong>：作業系統層級使用（TCP errors、sockets 連線被拒絕）。</p>
<p><strong>Idle, Prepare</strong>：內部使用。</p>
<p><strong>Polling</strong>：</p>
<ol>
<li>如果 Queue 不為空，依次取出 callback 函數執行，直到 Queue 為空或是抵達系統最大限制。</li>
<li>如果 Queue 為空但<em>有</em>設置「setImmediate」就進入 check 階段。</li>
<li>如果 Queue 為空但<em>沒有</em>設置「setImmediate」就會在 Polling 階段等待直到 Queue 有東西或是 Timer 時間抵達。</li>
</ol>
<p><strong>Check</strong>：處理 setImmediate 的 callback。</p>
<p><strong>Close callbacks</strong>：處理關閉連線、檔案的 callback。</p>
<blockquote>
<p>Macrotask Queue 在 Evevnt Loop 優先層級較低，接下來介紹優先層級更高的兩個 Queue。</p>
</blockquote>
<h3 id="-nexttick-queue-優先層級最高">NextTick Queue 優先層級最高<a class="h-anchor" href="#-nexttick-queue-%e5%84%aa%e5%85%88%e5%b1%a4%e7%b4%9a%e6%9c%80%e9%ab%98" aria-label="這個段落的連結"></a></h3>
<p>「process.nextTick()」的 callback 可以隨意插隊；只要這個 Queue 有東西就會優先執行。</p>
<h3 id="-microtask-queue-優先層級次高">MicroTask Queue 優先層級次高<a class="h-anchor" href="#-microtask-queue-%e5%84%aa%e5%85%88%e5%b1%a4%e7%b4%9a%e6%ac%a1%e9%ab%98" aria-label="這個段落的連結"></a></h3>
<p>「Promise 」的狀態從 pending 轉變為 resolve 或 reject 時，此時執行的 callback 會進入這個 Queue。</p>
<blockquote>
<p>注意：Promise 在創建時是同步的，不會進入 Event Loop。</p>
</blockquote>
<hr>
<h2 id="四解析一開始-nodejs-範例程式執行順序">四、解析一開始 Node.js 範例程式執行順序<a class="h-anchor" href="#%e5%9b%9b%e8%a7%a3%e6%9e%90%e4%b8%80%e9%96%8b%e5%a7%8b-nodejs-%e7%af%84%e4%be%8b%e7%a8%8b%e5%bc%8f%e5%9f%b7%e8%a1%8c%e9%a0%86%e5%ba%8f" aria-label="這個段落的連結"></a></h2>
<h3 id="-順序執行先把同步函式解決">順序執行，先把同步函式解決<a class="h-anchor" href="#-%e9%a0%86%e5%ba%8f%e5%9f%b7%e8%a1%8c%e5%85%88%e6%8a%8a%e5%90%8c%e6%ad%a5%e5%87%bd%e5%bc%8f%e8%a7%a3%e6%b1%ba" aria-label="這個段落的連結"></a></h3>
<ol>
<li>因為 Promise 在創建時是同步的，所以會先執行</li>
<li>在 function 前面加上 async 宣告為異步函式，執行時會被轉換為 Promise</li>
</ol>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span>console.log(<span style="color:#a5d6ff">&#39;start&#39;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">new</span> Promise(<span style="color:#ff7b72">function</span> (resolve, reject) {
</span></span><span style="display:flex;"><span>    console.log(<span style="color:#a5d6ff">&#39;promise&#39;</span>);
</span></span><span style="display:flex;"><span>    resolve(<span style="color:#a5d6ff">&#39;resolve&#39;</span>);
</span></span><span style="display:flex;"><span>}).then(<span style="color:#ff7b72">function</span> (result) {
</span></span><span style="display:flex;"><span>    console.log(result);
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>(<span style="color:#ff7b72">async</span> <span style="color:#ff7b72">function</span> () {
</span></span><span style="display:flex;"><span>    console.log(<span style="color:#a5d6ff">&#39;async&#39;</span>);
</span></span><span style="display:flex;"><span>})();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>console.log(<span style="color:#a5d6ff">&#39;end&#39;</span>);
</span></span></code></pre></div><p class="code-source">原始碼：<a href="https://gist.github.com/dean9703111/44406b76d559e2183b9514e290da2f08" rel="noopener">sync_function.js</a>（GitHub Gist）</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span>start
</span></span><span style="display:flex;"><span>promise
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">async</span>
</span></span><span style="display:flex;"><span>end
</span></span></code></pre></div><h3 id="-進入-event-loop">進入 Event Loop<a class="h-anchor" href="#-%e9%80%b2%e5%85%a5-event-loop" aria-label="這個段落的連結"></a></h3>
<ol>
<li>如果執行過程中遇到 NextTick Queue 就優先執行</li>
</ol>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span>process.nextTick(<span style="color:#ff7b72">function</span> () {
</span></span><span style="display:flex;"><span>    console.log(<span style="color:#a5d6ff">&#39;nextTick1&#39;</span>);
</span></span><span style="display:flex;"><span>});
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>process.nextTick(<span style="color:#ff7b72">function</span> () {
</span></span><span style="display:flex;"><span>    console.log(<span style="color:#a5d6ff">&#39;nextTick2&#39;</span>);
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><p class="code-source">原始碼：<a href="https://gist.github.com/dean9703111/5eaffaea4c9d482ffb36a111fbf1fd2f" rel="noopener">nextTick_queue.js</a>（GitHub Gist）</p>
<pre tabindex="0"><code>nextTick1
nextTick2
</code></pre><p>2. 在處理完最高優先層級後，如果執行過程中遇到 MicroTask Queue 就執行</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#ff7b72">new</span> Promise(<span style="color:#ff7b72">function</span> (resolve, reject) {
</span></span><span style="display:flex;"><span>    console.log(<span style="color:#a5d6ff">&#39;promise&#39;</span>);
</span></span><span style="display:flex;"><span>    resolve(<span style="color:#a5d6ff">&#39;resolve&#39;</span>);
</span></span><span style="display:flex;"><span>}).then(<span style="color:#ff7b72">function</span> (result) {
</span></span><span style="display:flex;"><span>    console.log(result);
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><p class="code-source">原始碼：<a href="https://gist.github.com/dean9703111/d5a22e3600e7d0334cb213051bc8ca40" rel="noopener">MicroTask_queue.js</a>（GitHub Gist）</p>
<pre tabindex="0"><code>resolve
</code></pre><p>3. 在前兩個高優先層級的 Queue 清空後，返回 Event Loop 繼續執行，setTimeout 設定的時間已到先執行。</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span>setTimeout(<span style="color:#ff7b72">function</span> () {
</span></span><span style="display:flex;"><span>    console.log(<span style="color:#a5d6ff">&#39;setTimeout&#39;</span>);
</span></span><span style="display:flex;"><span>}, <span style="color:#a5d6ff">0</span>);
</span></span></code></pre></div><p class="code-source">原始碼：<a href="https://gist.github.com/dean9703111/cdcdddce3fd03f545e6f159956cf9c7f" rel="noopener">setTimeout_function.js</a>（GitHub Gist）</p>
<pre tabindex="0"><code>setTimeout
</code></pre><p>4. 接著再在「Check」 階段執行 setImmediate</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span>setImmediate(<span style="color:#ff7b72">function</span> () {
</span></span><span style="display:flex;"><span>    console.log(<span style="color:#a5d6ff">&#39;setImmediate&#39;</span>);
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div><p class="code-source">原始碼：<a href="https://gist.github.com/dean9703111/5cba974b22f28334f0b21e268081e3dc" rel="noopener">setImmediate_function.js</a>（GitHub Gist）</p>
<pre tabindex="0"><code>setImmediate
</code></pre><p>以上步驟執行完成後就是範例程式的解答啦～希望對大家理解 Event Loop 有幫助。</p>
<p><strong>參考資源：</strong></p>
<ol>
<li><a href="https://notes.andywu.tw/2020/%E5%AE%8C%E6%95%B4%E5%9C%96%E8%A7%A3node-js%E7%9A%84event-loop%E4%BA%8B%E4%BB%B6%E8%BF%B4%E5%9C%88/">完整圖解Node.js的Event Loop(事件迴圈)</a></li>
<li><a href="https://www.youtube.com/watch?v=yHnlOAKYxQU&amp;ab_channel=%E6%9D%8E%E7%9A%93%E8%AF%9A">15 尚硅谷 前端面试题 nodejs事件轮询机制</a></li>
</ol>
]]></content:encoded>
    </item>
    <item>
      <title>把 Node.js 專案打包成執行檔，讓朋友無須安裝環境就能暢快體驗 — pkg的介紹以及實際範例分享</title>
      <link>https://deanlin.net/blog/202103/package-nodejs-app-with-pkg/</link>
      <pubDate>Fri, 05 Mar 2021 17:17:32 +0800</pubDate>
      <guid>https://deanlin.net/blog/202103/package-nodejs-app-with-pkg/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>如果你是一個很常寫 side project 的人，你一定遇到過想跟朋友炫耀自己完成的 side project；但卻因為雙方電腦環境不同而無法炫耀的問題🤣，今天跟各位介紹一款打包 node 專案成為執行檔的工具 — 『pkg』，同時佐以我過去分享過的 side…</description>
      <media:content url="https://deanlin.net/blog/202103/package-nodejs-app-with-pkg/images/package-nodejs-app-with-pkg-og_hu_71a9916a45d3280.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<p>如果你是一個很常寫 side project 的人，你一定遇到過想跟朋友炫耀自己完成的 side project；但卻因為雙方電腦環境不同而無法炫耀的問題🤣，今天跟各位介紹一款打包 node 專案成為執行檔的工具 — 『pkg』，同時佐以我過去分享過的 side project — 『透過爬蟲讓 Airtable 管理你的 Medium 文章』作為此工具的驗證。</p>
<p><img src="https://deanlin.net/blog/202103/package-nodejs-app-with-pkg/images/package-nodejs-app-with-pkg-01.webp" width="1400" height="784" alt="把 Node.js 專案打包成執行檔，讓朋友無須安裝環境就能暢快體驗 — pkg的介紹以及實際範例分享（圖 1）" fetchpriority="high" decoding="async"></p>
<pre tabindex="0"><code>一、打包工具 pkg
  ➤ 安裝
  ➤ pkg 打包參數說明
  ➤ package.json 參數說明
二、實際範例展示
  ➤ package.json 參數設定
  ➤ 產生執行檔
  ➤ 執行上可能遇到的問題
  ➤ 搭配環境變數執行的方式
</code></pre><h2 id="一打包工具-pkg">一、打包工具 pkg<a class="h-anchor" href="#%e4%b8%80%e6%89%93%e5%8c%85%e5%b7%a5%e5%85%b7-pkg" aria-label="這個段落的連結"></a></h2>
<h3 id="-安裝">安裝<a class="h-anchor" href="#-%e5%ae%89%e8%a3%9d" aria-label="這個段落的連結"></a></h3>
<p>全局安裝，讓每個 Node.js 專案都能使用</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>npm install -g pkg
</span></span></code></pre></div><h3 id="-pkg-打包參數說明">pkg 打包參數說明<a class="h-anchor" href="#-pkg-%e6%89%93%e5%8c%85%e5%8f%83%e6%95%b8%e8%aa%aa%e6%98%8e" aria-label="這個段落的連結"></a></h3>
<p>安裝完後輸入 <code>pkg -h</code> 會顯示他能用的打包參數，下面說明常用的：</p>
<ul>
<li>-t：目標打包給哪個系統用的(Linux, macOS and Windows)，如未設定會產生 3 個檔案</li>
<li>-c：打包會參考 package.json 或是其他 json 檔案</li>
<li>-o：檔案輸出名稱</li>
</ul>
<p><img src="https://deanlin.net/blog/202103/package-nodejs-app-with-pkg/images/package-nodejs-app-with-pkg-02.webp" width="701" height="558" alt="pkg 打包參數說明（圖 2）" loading="lazy" decoding="async"></p>
<h3 id="-packagejson-參數說明">package.json 參數說明<a class="h-anchor" href="#-packagejson-%e5%8f%83%e6%95%b8%e8%aa%aa%e6%98%8e" aria-label="這個段落的連結"></a></h3>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#7ee787">&#34;pkg&#34;</span>: {
</span></span><span style="display:flex;"><span>        <span style="color:#7ee787">&#34;scripts&#34;</span>: [
</span></span><span style="display:flex;"><span>            <span style="color:#a5d6ff">&#34;build/**/*.js&#34;</span><span style="color:#8b949e;font-style:italic">//需要打包的其他js文件
</span></span></span><span style="display:flex;"><span>        ],
</span></span><span style="display:flex;"><span>        <span style="color:#7ee787">&#34;assets&#34;</span>: [
</span></span><span style="display:flex;"><span>            <span style="color:#a5d6ff">&#34;dist/**/*&#34;</span><span style="color:#8b949e;font-style:italic">//靜態文件目錄
</span></span></span><span style="display:flex;"><span>        ]
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h2 id="二實際範例展示">二、實際範例展示<a class="h-anchor" href="#%e4%ba%8c%e5%af%a6%e9%9a%9b%e7%af%84%e4%be%8b%e5%b1%95%e7%a4%ba" aria-label="這個段落的連結"></a></h2>
<blockquote>
<p>以下範例的原始碼：<a href="https://github.com/dean9703111/medium_to_airtable">https://github.com/dean9703111/medium_to_airtable</a></p>
</blockquote>
<p>有時你設計了一個很有趣的功能，但如果他的前置作業太麻煩，非相關領域的人沒法無腦操作時就會阻礙他的推廣；相反如果他毫無使用門檻就會很多人願意嘗試看看。</p>
<p>這裏我就使用之前分享的 side project — 『透過爬蟲讓 Airtable 管理你的 Medium 文章』一步步說明如何操作。</p>
<h3 id="-packagejson-參數設定">package.json 參數設定<a class="h-anchor" href="#-packagejson-%e5%8f%83%e6%95%b8%e8%a8%ad%e5%ae%9a" aria-label="這個段落的連結"></a></h3>
<ul>
<li><strong>撰寫 scripts</strong>：記得不要去死背套件指令，需要記憶的事情交給電腦就好。這裏我新增了一個『build』的指令，他會以 main.js 作為入口檔，依據 package.json 的設定產生一個給 macOS 使用的執行檔。</li>
<li><strong>設定 pkg</strong>：把『node_modeles』的資料夾全部視為靜態檔即可</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span><span style="color:#f85149">...</span>
</span></span><span style="display:flex;"><span>  <span style="color:#7ee787">&#34;scripts&#34;</span>: {
</span></span><span style="display:flex;"><span>    <span style="color:#7ee787">&#34;build&#34;</span>: <span style="color:#a5d6ff">&#34;pkg main.js -t node12-macos-x64 -c package.json&#34;</span>
</span></span><span style="display:flex;"><span>  },
</span></span><span style="display:flex;"><span>  <span style="color:#7ee787">&#34;pkg&#34;</span>: {
</span></span><span style="display:flex;"><span>    <span style="color:#7ee787">&#34;assets&#34;</span>: [
</span></span><span style="display:flex;"><span>      <span style="color:#a5d6ff">&#34;node_modules/**/*&#34;</span>
</span></span><span style="display:flex;"><span>    ]
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span><span style="color:#f85149">...</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><h3 id="-產生執行檔">產生執行檔<a class="h-anchor" href="#-%e7%94%a2%e7%94%9f%e5%9f%b7%e8%a1%8c%e6%aa%94" aria-label="這個段落的連結"></a></h3>
<p>在上一步我們寫好 scripts 後可以用如下指令產生執行檔：</p>
<ol>
<li>使用 npm：<code>npm run build</code></li>
<li>使用 yarn：<code>yarn build</code></li>
</ol>
<p><img src="https://deanlin.net/blog/202103/package-nodejs-app-with-pkg/images/package-nodejs-app-with-pkg-03.webp" width="1350" height="188" alt="產生執行檔（圖 3）" loading="lazy" decoding="async"></p>
<p>如果沒有特別指定檔案產生的位置就會出現在資料夾的根目錄喔～</p>
<p><img src="https://deanlin.net/blog/202103/package-nodejs-app-with-pkg/images/package-nodejs-app-with-pkg-04.webp" width="488" height="746" alt="產生執行檔（圖 4）" loading="lazy" decoding="async"></p>
<h3 id="-執行上可能遇到的問題">執行上可能遇到的問題<a class="h-anchor" href="#-%e5%9f%b7%e8%a1%8c%e4%b8%8a%e5%8f%af%e8%83%bd%e9%81%87%e5%88%b0%e7%9a%84%e5%95%8f%e9%a1%8c" aria-label="這個段落的連結"></a></h3>
<blockquote>
<p>你可以直接下載macOS的執行檔玩玩看：<a href="https://github.com/dean9703111/medium_to_airtable/raw/master/app/medium-to-airtable">https://github.com/dean9703111/medium_to_airtable/raw/master/app/medium-to-airtable</a></p>
</blockquote>
<p>只要用 Terminal 前往檔案資料夾的位置，輸入 <code>./medium-to-airtable</code> 即可執行，但應該會遇到如下錯誤：</p>
<p><img src="https://deanlin.net/blog/202103/package-nodejs-app-with-pkg/images/package-nodejs-app-with-pkg-05.webp" width="800" height="308" alt="執行上可能遇到的問題（圖 5）" loading="lazy" decoding="async"></p>
<p>回去看程式碼會發現錯誤的原因是因為無法讀取 .env 的環境變數</p>
<p><img src="https://deanlin.net/blog/202103/package-nodejs-app-with-pkg/images/package-nodejs-app-with-pkg-06.webp" width="845" height="400" alt="執行上可能遇到的問題（圖 6）" loading="lazy" decoding="async"></p>
<h3 id="-搭配環境變數執行的方式">搭配環境變數執行的方式<a class="h-anchor" href="#-%e6%90%ad%e9%85%8d%e7%92%b0%e5%a2%83%e8%ae%8a%e6%95%b8%e5%9f%b7%e8%a1%8c%e7%9a%84%e6%96%b9%e5%bc%8f" aria-label="這個段落的連結"></a></h3>
<p>所以我們只需要在執行檔案前輸入原本設定在 .env 檔的資料就能夠順利執行：</p>
<pre tabindex="0"><code>ENV1=xxx ENV2=yyy ENV3=zzz ./execute_file
</code></pre><p>以這個 side-project 來說會長這個樣子（參數要怎麼填請<a href="https://deanlin.net/blog/202101/medium-to-airtable-crawler-part-2/">參考這篇文章</a>）：</p>
<pre tabindex="0"><code>MEDIUM_PAGE=&#34;https://medium.com/你的網頁&#34; SHOW_BROWSER=false AIRTABLE_API_KEYKEY=&#39;你申請的api&#39; AIRTABLE_BASE=&#39;airtable的id&#39; ./medium-to-airtable
</code></pre><p>然後就可以順利執行嚕～</p>
<p><img src="https://deanlin.net/blog/202103/package-nodejs-app-with-pkg/images/package-nodejs-app-with-pkg-07.webp" width="1285" height="395" alt="搭配環境變數執行的方式（圖 7）" loading="lazy" decoding="async"></p>
<pre tabindex="0"><code>如果你對這個專案有興趣可參考相關系列文章：
➤ 透過爬蟲讓 Airtable 管理你的 Medium 文章(上) — 遇到需求時的分析以及執行步驟
➤ 透過爬蟲讓 Airtable 管理你的 Medium 文章(下) — 如何使用這個工具
➤ 透過爬蟲讓 Airtable 管理你的 Medium 文章(補充篇) — 文章字數統計
</code></pre><hr>
<blockquote class="pullquote"><p>認真說這個打包工具比我想像的還要神，而且使用起來也沒什麼難度，如果手上剛好有熱騰騰的 side-project 趕快用這個套件來炫耀一波吧！</p>
</blockquote>
]]></content:encoded>
    </item>
    <item>
      <title>工程師的開發環境大補帖：Node.js、NVM、git、yarn、VSCode</title>
      <link>https://deanlin.net/blog/202101/developer-environment-setup/</link>
      <pubDate>Fri, 15 Jan 2021 11:21:18 +0800</pubDate>
      <guid>https://deanlin.net/blog/202101/developer-environment-setup/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>工欲善其事，必先利其器</description>
      <media:content url="https://deanlin.net/blog/202101/developer-environment-setup/images/developer-environment-setup-og_hu_47edff0bf094e58d.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<p><img src="https://deanlin.net/blog/202101/developer-environment-setup/images/developer-environment-setup-01.webp" width="1400" height="694" alt="工程師的開發環境大補帖：Node.js、NVM、git、yarn、VSCode（圖 1）" fetchpriority="high" decoding="async"></p>
<blockquote>
<p><em>工欲善其事，必先利其器</em></p>
</blockquote>
<h2 id="為什麼寫這篇文章">為什麼寫這篇文章？<a class="h-anchor" href="#%e7%82%ba%e4%bb%80%e9%ba%bc%e5%af%ab%e9%80%99%e7%af%87%e6%96%87%e7%ab%a0" aria-label="這個段落的連結"></a></h2>
<p>因為我自己在<code>剛接觸程式時最痛恨的就是環境設定</code><br>不知道是我跟電腦不合還是怎樣，每次我學新東西的時候總是在環境設定這個環節卡的不要不要的<br>為了避免讀者在之後實戰的文章中一邊看一邊心理罵:「<strong>為什麼我們程式一模一樣但我的就是會錯?</strong>」這樣的憾事發生，所以我決定詳細的說明這個專案所需安裝的一切環境，並附上我本機安裝的工具版本，讓大家在日後的文章中可以順利操作與學習</p>
<blockquote>
<p><em>你們實際操作時通常都有新的版本了，如果最新版本有問題可以切換到筆者的版本來操作</em></p>
</blockquote>
<h2 id="需要安裝哪些工具">需要安裝哪些工具<a class="h-anchor" href="#%e9%9c%80%e8%a6%81%e5%ae%89%e8%a3%9d%e5%93%aa%e4%ba%9b%e5%b7%a5%e5%85%b7" aria-label="這個段落的連結"></a></h2>
<blockquote>
<p><code>說明包含 windows/mac 兩個系統的設定</code></p>
</blockquote>
<ol>
<li><a href="https://deanlin.net/blog/202101/developer-environment-setup/#e634">Node.js</a></li>
<li><a href="https://deanlin.net/blog/202101/developer-environment-setup/#8082">NVM</a></li>
<li><a href="https://deanlin.net/blog/202101/developer-environment-setup/#6dcd">git</a></li>
<li><a href="https://deanlin.net/blog/202101/developer-environment-setup/#bf69">yarn</a></li>
<li><a href="https://deanlin.net/blog/202101/developer-environment-setup/#d582">VSCode</a></li>
</ol>
<h2 id="開發環境介紹--設定">開發環境介紹 &amp; 設定<a class="h-anchor" href="#%e9%96%8b%e7%99%bc%e7%92%b0%e5%a2%83%e4%bb%8b%e7%b4%b9--%e8%a8%ad%e5%ae%9a" aria-label="這個段落的連結"></a></h2>
<h2 class="h-nomark" id="1-nodejs-官網">1. Node.js <a href="https://nodejs.org/en/about/">官網</a><a class="h-anchor" href="#1-nodejs-%e5%ae%98%e7%b6%b2" aria-label="這個段落的連結"></a></h2>
<p><img src="https://deanlin.net/blog/202101/developer-environment-setup/images/developer-environment-setup-02.webp" width="119" height="73" alt="1. Node.js 官網（圖 2）" loading="lazy" decoding="async"></p>
<p>能夠在 server 端運行 JavaScript 的開放原始碼、跨平台 JavaScript 的執行環境。前端工程師的福音</p>
<blockquote>
<p><em>筆者安裝的版本：v16.19.1 (使用nvm安裝，後面會介紹)</em></p>
</blockquote>
<hr>
<h2 class="h-nomark" id="2-nvm-官網">2. nvm <a href="https://github.com/nvm-sh/nvm">官網</a><a class="h-anchor" href="#2-nvm-%e5%ae%98%e7%b6%b2" aria-label="這個段落的連結"></a></h2>
<p>由於 Node.js 更新速度是非常快的，而許多套件有 Node.js 版本的相依性，透過NVM你可以輕鬆切換 Node.js 版本來符合不同專案的使用環境</p>
<h3 id="-windows-安裝步驟">windows 安裝步驟<a class="h-anchor" href="#-windows-%e5%ae%89%e8%a3%9d%e6%ad%a5%e9%a9%9f" aria-label="這個段落的連結"></a></h3>
<ol>
<li>先進入<a href="https://github.com/coreybutler/nvm-windows/releases">windows下載頁面</a></li>
<li>選擇最新版本的下載 <strong>nvm-setup.zip</strong>(筆者版本為1.1.7，你看到這篇文章時可能有提供更新的版本)</li>
</ol>
<p><img src="https://deanlin.net/blog/202101/developer-environment-setup/images/developer-environment-setup-03.webp" width="1400" height="841" alt="windows 安裝步驟（圖 3）" loading="lazy" decoding="async"></p>
<p>3. 下載後一路瘋狂下一步即可</p>
<p>4. 安裝後開啟命令提示字元(CMD)</p>
<p>5. 在 CMD 輸入指令確認 nvm 是否安裝成功</p>
<p><img src="https://deanlin.net/blog/202101/developer-environment-setup/images/developer-environment-setup-04.webp" width="1200" height="591" alt="windows 安裝步驟（圖 4）" loading="lazy" decoding="async"></p>
<h3 id="-mac-安裝步驟">mac 安裝步驟<a class="h-anchor" href="#-mac-%e5%ae%89%e8%a3%9d%e6%ad%a5%e9%a9%9f" aria-label="這個段落的連結"></a></h3>
<ol>
<li>打開終端機(Terminal)，以下步驟皆在終端機操作</li>
<li>mac 建議先下載 Homebrew</li>
</ol>
<pre tabindex="0"><code>/bin/bash -c &#34;$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)&#34;
</code></pre><p>3. 下載後安裝 nvm</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>brew install nvm
</span></span></code></pre></div><p>4. 在終端機輸入指令確認 nvm 是否安裝成功</p>
<p><img src="https://deanlin.net/blog/202101/developer-environment-setup/images/developer-environment-setup-05.webp" width="1400" height="553" alt="mac 安裝步驟（圖 5）" loading="lazy" decoding="async"></p>
<h3 id="-用-nvm-安裝-nodejs">用 NVM 安裝 node.js<a class="h-anchor" href="#-%e7%94%a8-nvm-%e5%ae%89%e8%a3%9d-nodejs" aria-label="這個段落的連結"></a></h3>
<ul>
<li><code>指令：nvm install [version]</code></li>
<li>請切記要安裝一個 Node.js 版本(請選用<a href="https://nodejs.org/en/">LTS版本</a>)，不然你寫的程式會沒有可以運行的環境</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>nvm install 16.19.1
</span></span></code></pre></div><blockquote>
<p>筆者nvm安裝版本：0.39.3</p>
</blockquote>
<hr>
<h2 class="h-nomark" id="3-git-官網">3. git <a href="https://git-scm.com/">官網</a><a class="h-anchor" href="#3-git-%e5%ae%98%e7%b6%b2" aria-label="這個段落的連結"></a></h2>
<p><img src="https://deanlin.net/blog/202101/developer-environment-setup/images/developer-environment-setup-06.webp" width="200" height="84" alt="3. git 官網（圖 6）" loading="lazy" decoding="async"></p>
<p>這是對於程式版本控制的工具，透過他你可以輕鬆回到過去，並且在多人協作時也能有效管控<br><a href="https://git-scm.com/download/win">windows下載頁面</a>、<a href="https://git-scm.com/download/mac">mac下載頁面</a></p>
<blockquote>
<p><em>我有朋友使用 windows 怎麼樣都無法正常安裝 git，如果你也遇到了這個不幸，你還是有<a href="https://desktop.github.com/">Git桌面板</a>可以選擇的</em></p>
</blockquote>
<h3 id="常用指令">常用指令<a class="h-anchor" href="#%e5%b8%b8%e7%94%a8%e6%8c%87%e4%bb%a4" aria-label="這個段落的連結"></a></h3>
<ul>
<li>抓取遠端數據庫(僅確認有什麼更新，不合併)</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git fetch
</span></span></code></pre></div><ul>
<li>抓取遠端數據庫更新</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git pull
</span></span></code></pre></div><ul>
<li>更新遠端數據庫</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>git push
</span></span></code></pre></div><blockquote>
<p><em>筆者windows git安裝版本：2.27.0.windows.1</em></p>
</blockquote>
<hr>
<h2 class="h-nomark" id="4-yarn">4. yarn<a class="h-anchor" href="#4-yarn" aria-label="這個段落的連結"></a></h2>
<p><img src="https://deanlin.net/blog/202101/developer-environment-setup/images/developer-environment-setup-07.webp" width="160" height="84" alt="4. yarn（圖 7）" loading="lazy" decoding="async"></p>
<p>這是專門安裝套件的軟體，站在巨人的肩膀上你才能看得更遠，妥善運用別人提供的套件，程式的路能走得更遠<br><a href="https://classic.yarnpkg.com/lang/en/">下載頁面</a></p>
<h3 id="常用指令-1">常用指令<a class="h-anchor" href="#%e5%b8%b8%e7%94%a8%e6%8c%87%e4%bb%a4-1" aria-label="這個段落的連結"></a></h3>
<ul>
<li>依據 package.json 安裝所有套件</li>
</ul>
<pre tabindex="0"><code>yarn
</code></pre><ul>
<li>安裝到 dependencies</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>yarn add <span style="color:#ff7b72;font-weight:bold">[</span>package<span style="color:#ff7b72;font-weight:bold">]</span>
</span></span></code></pre></div><ul>
<li>安裝到 devDependencies</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>yarn add <span style="color:#ff7b72;font-weight:bold">[</span>package<span style="color:#ff7b72;font-weight:bold">]</span> –save-dev
</span></span></code></pre></div><ul>
<li>全域安裝套件(使該套件可在電腦任何位置被呼叫)</li>
</ul>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>yarn global add <span style="color:#ff7b72;font-weight:bold">[</span>package<span style="color:#ff7b72;font-weight:bold">]</span>
</span></span></code></pre></div><blockquote>
<p><em>筆者yarn安裝版本：1.19.2</em></p>
</blockquote>
<hr>
<h2 class="h-nomark" id="5-vscode-官網">5. VScode <a href="https://code.visualstudio.com/">官網</a><a class="h-anchor" href="#5-vscode-%e5%ae%98%e7%b6%b2" aria-label="這個段落的連結"></a></h2>
<p><img src="https://deanlin.net/blog/202101/developer-environment-setup/images/developer-environment-setup-08.webp" width="80" height="80" alt="5. VScode 官網（圖 8）" loading="lazy" decoding="async"></p>
<p>現在已經過了用記事本在打程式的年代了，透過 IDE(整合開發環境)可以大幅增加你撰寫程式的速度<br><a href="https://code.visualstudio.com/">VScode下載頁面</a></p>
]]></content:encoded>
    </item>
    <item>
      <title>【Day28】為爬蟲加上通知 - 用 axios 發出 LINE 通知</title>
      <link>https://deanlin.net/blog/202010/fb-ig-crawler-day-28/</link>
      <pubDate>Tue, 13 Oct 2020 08:53:06 +0800</pubDate>
      <guid>https://deanlin.net/blog/202010/fb-ig-crawler-day-28/</guid>
      <dc:creator>林鼎淵</dc:creator>
      <category>技術教學</category>
      <description>雖然眾多的套件都能實現這個目標，但還是把套件做個簡單的區分：</description>
      <media:content url="https://deanlin.net/blog/202010/fb-ig-crawler-day-28/images/cover_hu_8b8e2b164043efaf.jpg" medium="image" type="image/jpeg" width="1200" height="675" />
      <content:encoded><![CDATA[<blockquote>
<p>成功要選人少的路走，套件要選人多的來用</p>
</blockquote>
<h2 id="如何選擇-nodejs-中發出-request-的套件">如何選擇 Node.js 中發出 Request 的套件？<a class="h-anchor" href="#%e5%a6%82%e4%bd%95%e9%81%b8%e6%93%87-nodejs-%e4%b8%ad%e7%99%bc%e5%87%ba-request-%e7%9a%84%e5%a5%97%e4%bb%b6" aria-label="這個段落的連結"></a></h2>
<p>雖然眾多的套件都能實現這個目標，但還是把套件做個簡單的區分：</p>
<ol>
<li><strong>古老且沒再更新的套件</strong>：<a href="https://www.npmjs.com/package/https">https</a>、<a href="https://www.npmjs.com/package/xmlhttprequest">xmlhttprequest</a>、<a href="https://github.com/request/request">request</a>(這個套件在今年二月宣告廢棄)</li>
<li><strong>使用人數眾多且持續更新的套件</strong>：<a href="https://www.npmjs.com/package/superagent">superagent</a>、<a href="https://www.npmjs.com/package/node-fetch">node-fetch</a>、<a href="https://www.npmjs.com/package/got">got</a>、<a href="https://www.npmjs.com/package/axios">axios</a></li>
</ol>
<p>原則上選擇<mark>使用人數眾多且持續更新的套件最安全</mark>，因為古老的套件通常文件不齊全且無人維護，如果某一天宣告廢棄你的專案就要大量改寫了</p>
<blockquote>
<p>本專案採用 <code>axios</code> 來實現這個需求，如果你有興趣你也能嘗試不同的套件來完成今天的功能</p>
</blockquote>
<hr>
<h2 id="今日目標">今日目標<a class="h-anchor" href="#%e4%bb%8a%e6%97%a5%e7%9b%ae%e6%a8%99" aria-label="這個段落的連結"></a></h2>
<h3 class="h-nomark" id="1-axios-基礎介紹">1. axios 基礎介紹<a class="h-anchor" href="#1-axios-%e5%9f%ba%e7%a4%8e%e4%bb%8b%e7%b4%b9" aria-label="這個段落的連結"></a></h3>
<p>1.1 安裝 / GET / POST 基礎用法</p>
<p>1.2 請求配置說明</p>
<h3 class="h-nomark" id="2-用-axios-發出-line-通知">2. 用 axios 發出 LINE 通知<a class="h-anchor" href="#2-%e7%94%a8-axios-%e7%99%bc%e5%87%ba-line-%e9%80%9a%e7%9f%a5" aria-label="這個段落的連結"></a></h3>
<hr>
<h2 class="h-nomark" id="1-axios-基礎介紹-1">1. axios 基礎介紹<a class="h-anchor" href="#1-axios-%e5%9f%ba%e7%a4%8e%e4%bb%8b%e7%b4%b9-1" aria-label="這個段落的連結"></a></h2>
<h3 class="h-nomark" id="11-安裝--get--post-基礎用法">1.1 安裝 / GET / POST 基礎用法<a class="h-anchor" href="#11-%e5%ae%89%e8%a3%9d--get--post-%e5%9f%ba%e7%a4%8e%e7%94%a8%e6%b3%95" aria-label="這個段落的連結"></a></h3>
<ul>
<li>
<p><strong>安裝</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>npm install axios
</span></span><span style="display:flex;"><span>yarn add axios
</span></span></code></pre></div></li>
<li>
<p><strong>GET 基礎用法</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#ff7b72">const</span> axios <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;axios&#39;</span>); <span style="color:#8b949e;font-style:italic">// 引入套件
</span></span></span><span style="display:flex;"><span>axios.get(<span style="color:#a5d6ff">&#39;url/users&#39;</span>, { <span style="color:#8b949e;font-style:italic">// 填入 api 網址
</span></span></span><span style="display:flex;"><span>    params<span style="color:#ff7b72;font-weight:bold">:</span> { <span style="color:#8b949e;font-style:italic">// 傳入 params 物件
</span></span></span><span style="display:flex;"><span>        ID<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">12345</span>
</span></span><span style="display:flex;"><span>    }
</span></span><span style="display:flex;"><span>})
</span></span><span style="display:flex;"><span>.then(<span style="color:#ff7b72">function</span> (response) { 
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// 處理成功後要做的事
</span></span></span><span style="display:flex;"><span>    console.log(response);
</span></span><span style="display:flex;"><span>})
</span></span><span style="display:flex;"><span>.<span style="color:#ff7b72">catch</span>(<span style="color:#ff7b72">function</span> (error) {
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// 發生意外地處理
</span></span></span><span style="display:flex;"><span>    console.log(error);
</span></span><span style="display:flex;"><span>})
</span></span></code></pre></div></li>
<li>
<p><strong>POST 基礎用法</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span>axios.post(<span style="color:#a5d6ff">&#39;url/users&#39;</span>, { <span style="color:#8b949e;font-style:italic">// 要傳送的資料由後方物件帶入
</span></span></span><span style="display:flex;"><span>    firstName<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;Fred&#39;</span>,
</span></span><span style="display:flex;"><span>    lastName<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;Flintstone&#39;</span>
</span></span><span style="display:flex;"><span>})
</span></span><span style="display:flex;"><span>.then(<span style="color:#ff7b72">function</span> (response) {
</span></span><span style="display:flex;"><span>    console.log(response);
</span></span><span style="display:flex;"><span>})
</span></span><span style="display:flex;"><span>.<span style="color:#ff7b72">catch</span>(<span style="color:#ff7b72">function</span> (error) {
</span></span><span style="display:flex;"><span>    console.log(error);
</span></span><span style="display:flex;"><span>});
</span></span></code></pre></div></li>
</ul>
<hr>
<h3 class="h-nomark" id="12-請求配置說明">1.2 請求配置說明<a class="h-anchor" href="#12-%e8%ab%8b%e6%b1%82%e9%85%8d%e7%bd%ae%e8%aa%aa%e6%98%8e" aria-label="這個段落的連結"></a></h3>
<ul>
<li>
<p><strong>axios 的組成結構</strong>：</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span>axios.(config 物件)
</span></span><span style="display:flex;"><span>.then(<span style="color:#ff7b72">function</span> (response) {})
</span></span><span style="display:flex;"><span>.<span style="color:#ff7b72">catch</span>(<span style="color:#ff7b72">function</span> (error) {});
</span></span></code></pre></div></li>
<li>
<p><code>config 物件</code>：發出 Request 的配置選項，其中只有 url 為必填</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// url 為必填
</span></span></span><span style="display:flex;"><span>    url<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;/users&#39;</span>,
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// method 是發出 Request 時使用的方法
</span></span></span><span style="display:flex;"><span>    method<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;get&#39;</span>, <span style="color:#8b949e;font-style:italic">// 默認為 get
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// baseURL 如果填寫就會被添加到 url 前面，除非 url 為絕對路徑
</span></span></span><span style="display:flex;"><span>    baseURL<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;https://some-domain.com/api/&#39;</span>,
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// 你可以在這個地方定義自己的 headers
</span></span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// 通常會拿來設定 Authorization、Content-Type
</span></span></span><span style="display:flex;"><span>    headers<span style="color:#ff7b72;font-weight:bold">:</span> {<span style="color:#a5d6ff">&#39;Content-Type&#39;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;application/json&#39;</span>},
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// params 通常跟著 GET method 一起使用
</span></span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// 這裡填寫的參數會帶入 url 後面，ex : .../user?ID=123
</span></span></span><span style="display:flex;"><span>    params<span style="color:#ff7b72;font-weight:bold">:</span> {
</span></span><span style="display:flex;"><span>        ID<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">123</span>
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// data 只適用於 POST、PUT、PATCH 這幾個 method
</span></span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// 主要作為 Request 傳送的資料
</span></span></span><span style="display:flex;"><span>    data<span style="color:#ff7b72;font-weight:bold">:</span> {
</span></span><span style="display:flex;"><span>        firstName<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;Fred&#39;</span>
</span></span><span style="display:flex;"><span>    },
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// Request 時間超過 1000毫秒(1秒)後會被中止
</span></span></span><span style="display:flex;"><span>    timeout<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">1000</span>,
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// 選項: &#39;arraybuffer&#39;, &#39;document&#39;, &#39;json&#39;, &#39;text&#39;, &#39;stream&#39;
</span></span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// 瀏覽器才有 &#39;blob&#39;
</span></span></span><span style="display:flex;"><span>    responseType<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;json&#39;</span>, <span style="color:#8b949e;font-style:italic">// 默認為 json
</span></span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>     <span style="color:#8b949e;font-style:italic">// 限制 http 回應時的內容大小
</span></span></span><span style="display:flex;"><span>    maxContentLength<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">2000</span>,
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></div><blockquote>
<p>這裡只列出常用的配置選項，如果你有興趣深入研究請參考<a href="https://www.npmjs.com/package/axios#request-config">官方文件</a></p>
</blockquote>
</li>
<li>
<p><strong>下面列出使用 then 時常用的回應參數</strong></p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span>axios.get(<span style="color:#a5d6ff">&#39;/users/123&#39;</span>)
</span></span><span style="display:flex;"><span>.then(<span style="color:#ff7b72">function</span>(response) {
</span></span><span style="display:flex;"><span>    console.log(response.data); <span style="color:#8b949e;font-style:italic">// api 回傳的資料會在這裡
</span></span></span><span style="display:flex;"><span>    console.log(response.status); <span style="color:#8b949e;font-style:italic">// 這個是 HTTP狀態碼
</span></span></span><span style="display:flex;"><span>    console.log(response.config); <span style="color:#8b949e;font-style:italic">// 這個 Request 的 config
</span></span></span><span style="display:flex;"><span>});
</span></span></code></pre></div></li>
</ul>
<hr>
<h2 class="h-nomark" id="2-用-axios-發出-line-通知-1">2. 用 axios 發出 LINE 通知<a class="h-anchor" href="#2-%e7%94%a8-axios-%e7%99%bc%e5%87%ba-line-%e9%80%9a%e7%9f%a5-1" aria-label="這個段落的連結"></a></h2>
<p>在對 axios 有基礎了解後我們就把昨日在 POSTMAN 設定 Request 的參數搬移到專案程式吧。<code>在專案 tools 資料夾內新增 lineNotify.js</code>，今天我們要用這隻程式發出 LINE 通知</p>
<ul>
<li>因為傳送 data 的資料格式為 form-data ，所以請先下指令 <code>yarn add form-data</code> 安裝套件喔</li>
</ul>
<h3 id="toolslinenotifyjs">tools/lineNotify.js<a class="h-anchor" href="#toolslinenotifyjs" aria-label="這個段落的連結"></a></h3>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-javascript" data-lang="javascript"><span style="display:flex;"><span><span style="color:#ff7b72">const</span> axios <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;axios&#39;</span>)
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">var</span> FormData <span style="color:#ff7b72;font-weight:bold">=</span> require(<span style="color:#a5d6ff">&#39;form-data&#39;</span>);
</span></span><span style="display:flex;"><span>require(<span style="color:#a5d6ff">&#39;dotenv&#39;</span>).config();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#ff7b72">function</span> lineNotify () {
</span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> token <span style="color:#ff7b72;font-weight:bold">=</span> process.env.LINE_TOKEN;
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// 使用 form-data 傳遞資料
</span></span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> form_data <span style="color:#ff7b72;font-weight:bold">=</span> <span style="color:#ff7b72">new</span> FormData();
</span></span><span style="display:flex;"><span>    form_data.append(<span style="color:#a5d6ff">&#34;message&#34;</span>, <span style="color:#a5d6ff">&#39;測試 lineNotify&#39;</span>);
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>    <span style="color:#8b949e;font-style:italic">// 設定 LINE Notify 的 權杖 &amp; form-data
</span></span></span><span style="display:flex;"><span>    <span style="color:#ff7b72">const</span> headers <span style="color:#ff7b72;font-weight:bold">=</span> Object.assign({
</span></span><span style="display:flex;"><span>        <span style="color:#a5d6ff">&#39;Authorization&#39;</span><span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">`Bearer </span><span style="color:#a5d6ff">${</span>token<span style="color:#a5d6ff">}</span><span style="color:#a5d6ff">`</span>
</span></span><span style="display:flex;"><span>    }, form_data.getHeaders());
</span></span><span style="display:flex;"><span>    
</span></span><span style="display:flex;"><span>    axios({
</span></span><span style="display:flex;"><span>        method<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;post&#39;</span>,
</span></span><span style="display:flex;"><span>        url<span style="color:#ff7b72;font-weight:bold">:</span> <span style="color:#a5d6ff">&#39;https://notify-api.line.me/api/notify&#39;</span>,
</span></span><span style="display:flex;"><span>        data<span style="color:#ff7b72;font-weight:bold">:</span> form_data,
</span></span><span style="display:flex;"><span>        headers<span style="color:#ff7b72;font-weight:bold">:</span> headers
</span></span><span style="display:flex;"><span>    }).then(<span style="color:#ff7b72">function</span> (response) {
</span></span><span style="display:flex;"><span>        <span style="color:#8b949e;font-style:italic">// HTTP狀態碼 200 代表成功
</span></span></span><span style="display:flex;"><span>        console.log(<span style="color:#a5d6ff">&#34;HTTP狀態碼:&#34;</span> <span style="color:#ff7b72;font-weight:bold">+</span> response.status);
</span></span><span style="display:flex;"><span>        <span style="color:#8b949e;font-style:italic">// 觀察回傳的資料是否與 POSTMAN 測試一致
</span></span></span><span style="display:flex;"><span>        console.log(response.data);
</span></span><span style="display:flex;"><span>    }).<span style="color:#ff7b72">catch</span>(<span style="color:#ff7b72">function</span> (error) {
</span></span><span style="display:flex;"><span>        console.error(<span style="color:#a5d6ff">&#34;LINE通知發送失敗&#34;</span>);
</span></span><span style="display:flex;"><span>        <span style="color:#ff7b72">if</span> (error.response) { <span style="color:#8b949e;font-style:italic">// 顯示錯誤原因            
</span></span></span><span style="display:flex;"><span>            console.error(<span style="color:#a5d6ff">&#34;HTTP狀態碼:&#34;</span> <span style="color:#ff7b72;font-weight:bold">+</span> error.response.status);
</span></span><span style="display:flex;"><span>            console.error(error.response.data);
</span></span><span style="display:flex;"><span>        } <span style="color:#ff7b72">else</span> {
</span></span><span style="display:flex;"><span>            console.error(error);
</span></span><span style="display:flex;"><span>        }
</span></span><span style="display:flex;"><span>    });
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>lineNotify()
</span></span></code></pre></div><hr>
<h3 id="執行程式">執行程式<a class="h-anchor" href="#%e5%9f%b7%e8%a1%8c%e7%a8%8b%e5%bc%8f" aria-label="這個段落的連結"></a></h3>
<ol>
<li>
<p>在專案資料夾的終端機(Terminal)執行指令</p>
<div class="highlight"><pre tabindex="0" style="color:#e6edf3;background-color:#0d1117;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-text-size-adjust:none;"><code class="language-bash" data-lang="bash"><span style="display:flex;"><span>node tools/lineNotify.js
</span></span></code></pre></div></li>
<li>
<p>確認印出的 HTTP狀態碼 &amp; 回傳的資料與 POSTMAN 測試結果一致</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-28/images/fb-ig-crawler-day-28-01.webp" width="1024" height="70" alt="執行程式（圖 1）" fetchpriority="high" decoding="async"></p>
</li>
<li>
<p>如果你的 LINE 收到發出的通知就代表你成功嚕！</p>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-28/images/fb-ig-crawler-day-28-02.webp" width="716" height="444" alt="執行程式（圖 2）" loading="lazy" decoding="async"></p>
</li>
</ol>
<hr>
<h3 id="ℹ-專案原始碼">ℹ️ 專案原始碼<a class="h-anchor" href="#%e2%84%b9-%e5%b0%88%e6%a1%88%e5%8e%9f%e5%a7%8b%e7%a2%bc" aria-label="這個段落的連結"></a></h3>
<ul>
<li>今天的完整程式碼可以在<a href="https://github.com/dean9703111/ithelp_30days/tree/master/day28">這裡</a>找到喔</li>
<li>我昨天的把昨天的程式碼打包成<a href="https://github.com/dean9703111/ithelp_30days/raw/master/sampleCode/day27_sample_code.zip">壓縮檔</a>，你可以在乾淨的環境為專案加入發出LINE通知的功能
<ul>
<li>請記得在終端機下指令 <strong>yarn</strong> 才會把之前的套件安裝</li>
<li>windows需下載與你chrome版本相同的<a href="http://chromedriver.storage.googleapis.com/index.html">chrome driver</a>放在專案根目錄</li>
<li>要在tools/google_sheets資料夾放上自己的憑證，申請流程請參考<a href="https://deanlin.net/blog/202010/fb-ig-crawler-day-17/">Day17</a></li>
<li>調整fanspages資料夾內目標爬蟲的粉專網址</li>
<li>調整.env檔
<ul>
<li>填上FB登入資訊</li>
<li>填上FB版本(classic/new)</li>
<li>填上IG登入資訊</li>
<li>填上SPREADSHEET_ID</li>
<li>填上爬蟲執行時間(CRONJOB_TIME)</li>
<li>填上LINE Notify申請的權杖(LINE_TOKEN)，申請流程請參考<a href="https://deanlin.net/blog/202010/fb-ig-crawler-day-27/">Day27</a></li>
</ul>
</li>
<li>在終端機下指令 <strong>npm install forever -g</strong> ，讓你在終端機的任何位置都能管控排程</li>
<li>在sh資料夾中設定執行排程的shell script</li>
</ul>
</li>
</ul>
<hr>
<h3 id="參考資源">參考資源<a class="h-anchor" href="#%e5%8f%83%e8%80%83%e8%b3%87%e6%ba%90" aria-label="這個段落的連結"></a></h3>
<ol>
<li><a href="https://medium.com/dot-js/%E7%94%B1%E5%89%8D%E7%AB%AFrequest-%E7%9A%84%E5%B9%BE%E7%A8%AE%E6%96%B9%E6%B3%95-fbf8a0b4023a">由前端request 的幾種方法</a></li>
<li><a href="https://www.twilio.com/blog/2017/08/http-requests-in-node-js.html">5 Ways to Make HTTP Requests in Node.js</a></li>
<li><a href="https://ithelp.ithome.com.tw/articles/10212120">axios 基本使用 &amp; Config</a></li>
</ol>
<hr>
<blockquote>
<p><img src="https://deanlin.net/blog/202010/fb-ig-crawler-day-28/images/fb-ig-crawler-day-28-03.webp" width="1024" height="233" alt="參考資源（圖 3）" loading="lazy" decoding="async"></p>
<p><strong>在許多人的幫助下，本系列文章已出版成書，並添加了新的篇章與細節補充：</strong></p>
<ul>
<li>加入更多實務經驗，用完整的開發流程讓讀者了解專案每個階段要注意的事項</li>
<li>將爬蟲的步驟與技巧做更詳細的說明，讓讀者可以輕鬆入門</li>
<li>調整專案架構
<ul>
<li>優化爬蟲程式，以更廣的視角來擷取網頁資訊</li>
<li>增加資料驗證、錯誤通知等功能，讓爬蟲執行遇到問題時可以第一時間通知使用者</li>
<li>排程部分改用 node-schedule &amp; pm2 的組合，讓讀者可以輕鬆管理專案程序並獲得更精確的 log 資訊</li>
</ul>
</li>
</ul>
<p><strong>有興趣的朋友可以到天瓏書局選購，感謝大家的支持。</strong></p>
<p><strong>購書連結</strong>：<a href="https://www.tenlong.com.tw/products/9789864348008">https://www.tenlong.com.tw/products/9789864348008</a></p>
</blockquote>
]]></content:encoded>
    </item>
  </channel>
</rss>
