

<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>node.js &#8211; Max的程式語言筆記</title>
	<atom:link href="https://stackoverflow.max-everyday.com/tag/node-js/feed/" rel="self" type="application/rss+xml" />
	<link>https://stackoverflow.max-everyday.com</link>
	<description>我要當一個豬頭，快樂過每一天</description>
	<lastBuildDate>Tue, 17 Mar 2026 07:12:55 +0000</lastBuildDate>
	<language>zh-TW</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.4</generator>

<image>
	<url>https://stackoverflow.max-everyday.com/wp-content/uploads/2017/02/max-stackoverflow-256.png</url>
	<title>node.js &#8211; Max的程式語言筆記</title>
	<link>https://stackoverflow.max-everyday.com</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Tailwind CSS</title>
		<link>https://stackoverflow.max-everyday.com/2026/03/tailwind-css/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/03/tailwind-css/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Tue, 17 Mar 2026 05:06:14 +0000</pubDate>
				<category><![CDATA[node.js筆記]]></category>
		<category><![CDATA[node.js]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=7978</guid>

					<description><![CDATA[Tailwind 是一個與眾不同的 CSS 框架...]]></description>
										<content:encoded><![CDATA[
<p>Tailwind 是一個與眾不同的 CSS 框架。傳統的 CSS 框架如 Bootstrap 會提供按鈕、導覽列等現成的元件，但 Tailwind 提供的是一堆原子化的工具類別。簡單來說，它把所有 CSS 屬性拆解成一個個小標籤，讓你直接在 HTML 裡面組合出想要的樣子。你不需要離開 HTML 檔案去寫另外的 CSS 檔案，這大幅減少了切換視窗的時間。</p>



<h2 class="wp-block-heading">工具優先的開發流程</h2>



<p>在傳統開發中，如果你想做一個圓角按鈕，你需要幫 HTML 加上一個類別名稱，然後在 CSS 檔案裡定義背景顏色、圓角半徑與內距。在 Tailwind 中，你只需要在 HTML 的 class 屬性裡寫上 bg-blue-500 rounded px-4 py-2 等字眼。這種寫法雖然一開始會讓 HTML 看起來有點擁擠，但它保證了樣式的一致性，且因為不需要想類別名稱（例如該叫 primary-btn 還是 main-button），開發速度會變得很驚人。</p>



<h2 class="wp-block-heading">為什麼開發者喜歡它</h2>



<p>使用 Tailwind 最棒的一點是它內建了響應式設計。如果你想讓一張圖片在手機上隱藏但在電腦上顯示，只需要加上 hidden md:block 即可。此外，它內建了一套非常專業的調色盤與間距規範，即便你不是設計專業，透過這些預設值組合出來的介面通常也會非常有質感。由於它在打包時會自動刪除沒用到的程式碼，最終生成的 CSS 檔案體積通常比傳統寫法還要小。</p>



<h2 class="wp-block-heading">與框架的整合</h2>



<p>Tailwind 與 React 或 Vue 的搭配非常完美。因為這些框架本身就是以組件為核心，你可以把一長串的 Tailwind 標籤封裝在一個組件裡，這樣你只需要寫一次樣式，就能在專案中重複使用。目前 Node.js 環境下的開發工具如 Vite 都能完美支援 Tailwind，讓你在修改代碼的瞬間就能在瀏覽器看到樣式的變化。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>如何把計數器按鈕用 Tailwind 美化？</p>



<p>我們把剛才那個素面朝天的計數器按鈕，加上 Tailwind CSS 的工具類別。你會發現不需要寫任何一行傳統的 CSS，就能讓介面變得很專業。</p>



<h2 class="wp-block-heading">React 結合 Tailwind 的寫法</h2>



<p>在 React 中，我們使用 className 屬性來加入 Tailwind 的類別。</p>



<p>JavaScript</p>



<pre class="wp-block-code"><code>import React, { useState } from 'react';

function Counter() {
  const &#91;count, setCount] = useState(0);

  return (
    &lt;div className="flex flex-col items-center justify-center min-h-screen bg-gray-100"&gt;
      &lt;p className="text-2xl font-bold text-gray-800 mb-4"&gt;目前數字：{count}&lt;/p&gt;
      &lt;button 
        onClick={() =&gt; setCount(count + 1)}
        className="px-6 py-2 bg-blue-500 text-white rounded-lg shadow-md hover:bg-blue-600 transition duration-300"
      &gt;
        加 1
      &lt;/button&gt;
    &lt;/div&gt;
  );
}
</code></pre>



<h2 class="wp-block-heading">Vue 結合 Tailwind 的寫法</h2>



<p>Vue 的寫法同樣直觀，直接在 template 的 class 屬性中加入類別即可。</p>



<p>HTML</p>



<pre class="wp-block-code"><code>&lt;script setup&gt;
import { ref } from 'vue';
const count = ref(0);
&lt;/script&gt;

&lt;template&gt;
  &lt;div class="flex flex-col items-center justify-center min-h-screen bg-slate-50"&gt;
    &lt;p class="text-3xl font-extrabold text-indigo-600 mb-6"&gt;目前數字：{{ count }}&lt;/p&gt;
    &lt;button 
      @click="count++"
      class="px-8 py-3 bg-indigo-500 text-white font-semibold rounded-full hover:scale-105 active:scale-95 transition-transform shadow-lg"
    &gt;
      加 1
    &lt;/button&gt;
  &lt;/div&gt;
&lt;/template&gt;
</code></pre>



<h2 class="wp-block-heading">樣式類別解構說明</h2>



<p>觀察上面的程式碼，你會看到一些常用的 Tailwind 語法：</p>



<ul class="wp-block-list">
<li>佈局控制：flex 與 items-center 讓內容居中，min-h-screen 讓背景填滿整個螢幕高度。</li>



<li>間距與大小：px-6 代表左右內距，mb-4 代表下方外距。</li>



<li>外觀與顏色：bg-blue-500 是藍色背景，rounded-lg 是大圓角，shadow-md 是陰影效果。</li>



<li>互動效果：hover:bg-blue-600 讓滑鼠移上去時變色，transition 則讓變化過程有平滑的動畫感。</li>
</ul>



<p>這種開發方式讓你不需要在 HTML 檔案與 CSS 檔案之間反覆橫跳。只要記住常用的類別名稱，開發速度會比傳統寫法快上好幾倍。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>在 VS Code 裡面安裝 Tailwind 的套件，讓它自動提示這些類別名稱</p>



<p>在 VS Code 中開發 Tailwind CSS，最核心的工具就是官方提供的擴充功能。它能讓你在撰寫時像擁有讀心術一樣，自動補完那些細碎的類別名稱。</p>



<h2 class="wp-block-heading">安裝 Tailwind CSS IntelliSense</h2>



<p>首先，請在 VS Code 的左側選單點擊擴充功能圖示，搜尋並安裝 <strong>Tailwind CSS IntelliSense</strong>。安裝完成後，只要你在 HTML 或組件的 class 屬性中開始輸入，它就會跳出建議清單。例如輸入 flex，它會顯示這代表 display: flex;，並且還會預覽顏色與間距的大小，這對記憶類別名稱非常有幫助。</p>



<h2 class="wp-block-heading">搭配 Prettier 自動排序類別</h2>



<p>當你的類別寫得越來越長，排列順序可能會變得很亂。這時候可以安裝 <strong>Prettier &#8211; Code formatter</strong> 並搭配 Tailwind 官方的插件 <strong>prettier-plugin-tailwindcss</strong>。它會在你存檔時，自動幫你把類別按照一定的邏輯重新排序。例如，它會把佈局相關的類別放在前面，顏色與裝飾性的放在後面，讓程式碼看起來非常整齊。</p>



<h2 class="wp-block-heading">檢查與調試工具</h2>



<p>如果你不確定某個類別到底對應到什麼 CSS 屬性，只需將滑鼠懸停在該類別名稱上，VS Code 就會彈出一個小視窗顯示完整的 CSS 內容。此外，如果你寫錯了語法（例如拼錯顏色名稱），編輯器會出現紅色波浪線警告你，這能避免很多低級錯誤。</p>



<h2 class="wp-block-heading">設定檔的重要性</h2>



<p>要讓這些功能正常運作，你的專案根目錄必須要有一個 <strong>tailwind.config.js</strong> 檔案。Node.js 會讀取這個設定檔來決定你的專案有哪些自定義的顏色、字體或間距。如果你發現自動提示沒有出現，通常是因為 Node.js 還沒安裝好相關套件，或是設定檔放錯了位置。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>基礎的 tailwind.config.js 設定範例，試著自定義專案的專屬顏色</p>



<p>這是一份基礎的 <code>tailwind.config.js</code> 設定檔範例。你可以透過這個檔案，定義專案專屬的品牌顏色或字體，讓 Tailwind 不再只是預設的藍色或灰色。</p>



<h2 class="wp-block-heading">基礎設定檔結構</h2>



<p>在專案根目錄建立 <code>tailwind.config.js</code>，內容如下：</p>



<p>JavaScript</p>



<pre class="wp-block-code"><code>/** @type {import('tailwindcss').Config} */
export default {
  content: &#91;
    "./index.html",
    "./src/**/*.{js,ts,jsx,tsx,vue}",
  ],
  theme: {
    extend: {
      colors: {
        'brand-primary': '#1a5cff',
        'brand-dark': '#0f172a',
        'maruko-pink': '#ff85a2',
      },
      spacing: {
        '128': '32rem',
      }
    },
  },
  plugins: &#91;],
}
</code></pre>



<h2 class="wp-block-heading">設定項目的意義</h2>



<p><code>content</code> 區塊非常重要，它告訴 Tailwind 應該去哪些檔案裡面找你寫過的類別名稱。如果沒設定好，Node.js 在打包程式碼時，會以為你沒用到這些樣式，導致網頁看起來白白的。<code>theme</code> 底下的 <code>extend</code> 則是用來擴充自定義屬性的地方。例如上面設定了 <code>maruko-pink</code>，你就可以在 HTML 裡直接寫 <code>text-maruko-pink</code> 或 <code>bg-maruko-pink</code>。</p>



<h2 class="wp-block-heading">如何套用自定義顏色</h2>



<p>當你在設定檔加入自定義顏色後，VS Code 的 IntelliSense 套件會自動讀取並在建議清單中顯示。這在開發系列產品或維護個人品牌時非常有用，你不需要記住複雜的十六進位顏色碼，只需要記住 <code>brand-primary</code> 這種有意義的名字即可。</p>



<h2 class="wp-block-heading">Node.js 的後台作業</h2>



<p>每當你修改這個設定檔並存檔，Node.js 執行的開發伺服器（如 Vite）會偵測到變動，並即時重新產生對應的 CSS 內容。這就是為什麼現代前端開發體驗這麼流暢的原因：你改動設定，瀏覽器幾乎在同一秒就反應出結果。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>在現有的 React 或 Vue 專案中，透過 npm 指令快速初始化這份設定檔</p>



<p>要在專案中啟動 Tailwind CSS，你需要用到 Node.js 的套件管理工具 npm。這幾個指令會自動幫你下載必要的檔案，並幫你把設定檔準備好。</p>



<h2 class="wp-block-heading">初始化步驟</h2>



<p>請在 VS Code 的終端機（Terminal）輸入以下指令：</p>



<p>Bash</p>



<pre class="wp-block-code"><code># 1. 安裝 Tailwind 及其相依套件
npm install -D tailwindcss postcss autoprefixer

# 2. 產生 tailwind.config.js 與 postcss.config.js 檔案
npx tailwindcss init -p
</code></pre>



<p>第一行指令會把 Tailwind 安裝在你的開發環境中。第二行指令中的 <code>-p</code> 非常重要，它會同時幫你產生 PostCSS 的設定檔。PostCSS 就像是一個翻譯官，負責把 Tailwind 的特殊語法轉譯成所有瀏覽器都能看懂的標準 CSS。</p>



<h2 class="wp-block-heading">引入 Tailwind 到專案</h2>



<p>檔案產生後，你需要在專案的 CSS 進入點（通常是 <code>src/index.css</code> 或 <code>src/main.css</code>）加入這三行指令：</p>



<p>CSS</p>



<pre class="wp-block-code"><code>@tailwind base;
@tailwind components;
@tailwind utilities;
</code></pre>



<p>這三行程式碼就像是開啟開關。Node.js 在執行時看到這三行，就會把 Tailwind 全部的工具類別庫載入進來，讓你可以在 React 或 Vue 的組件裡開始使用 <code>flex</code>、<code>bg-blue-500</code> 等類別。</p>



<h2 class="wp-block-heading">執行與驗證</h2>



<p>最後，只要啟動你的開發伺服器（通常是執行 <code>npm run dev</code>），Node.js 就會開始監控你的程式碼。如果你在設定檔中加了自定義顏色，或是修改了 HTML 裡的類別，你會發現瀏覽器會即時更新。這種開發節奏非常快，配合 VS Code 的自動補完功能，你會感覺自己像是在玩積木一樣拼湊出網頁。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>把這整套工具（Vite + React/Vue + Tailwind）一次打包成可以放上網路伺服器的靜態檔案</p>



<p>當你的網頁開發完成後，最終目標是讓全世界的人都能看到。這時候你需要透過 Node.js 把寫好的 React 或 Vue 原始碼「編譯」成瀏覽器真正讀得懂的 HTML、CSS 與 JavaScript 檔案。</p>



<h2 class="wp-block-heading">打包專案的指令</h2>



<p>在 VS Code 終端機輸入以下指令：</p>



<p>Bash</p>



<pre class="wp-block-code"><code>npm run build
</code></pre>



<p>執行這個指令後，Node.js 會啟動 Vite 或 Webpack 等打包工具。它會掃描你所有的 React/Vue 組件，找出你用到的 Tailwind 類別，然後把沒用到的樣式全部刪除（這叫作 Tree Shaking），最後在專案根目錄產生一個名為 <strong>dist</strong>（或 build）的資料夾。</p>



<h2 class="wp-block-heading">dist 資料夾裡有什麼</h2>



<p>打開 dist 資料夾，你會看到一個 <code>index.html</code> 以及存放 JavaScript 與 CSS 的資料夾。</p>



<ul class="wp-block-list">
<li><strong>精簡化 (Minification)：</strong> 程式碼會被壓縮成密密麻麻的一片，刪除所有空格與換行，縮短變數名稱，以減小檔案體積。</li>



<li><strong>快取處理：</strong> 檔名通常會帶有一串亂碼（例如 <code>main-a1b2c3.js</code>），這是為了確保當你更新網頁時，使用者的瀏覽器能抓到最新的版本，而不是舊的暫存檔。</li>
</ul>



<h2 class="wp-block-heading">如何部署到網路伺服器</h2>



<p>你只需要把這個 <strong>dist</strong> 資料夾裡的內容，上傳到任何支援靜態網頁的空間即可。目前最流行的自動化方案包括：</p>



<ol start="1" class="wp-block-list">
<li><strong>Vercel / Netlify：</strong> 只要把程式碼推送到 GitHub，它們會自動幫你執行 <code>npm run build</code> 並把網頁跑起來。</li>



<li><strong>GitHub Pages：</strong> 適合個人作品集，完全免費。</li>



<li><strong>自己的伺服器：</strong> 透過 Node.js 或 Apache/Nginx 伺服器來託管這些檔案。</li>
</ol>



<h2 class="wp-block-heading">本地預覽打包結果</h2>



<p>在正式上傳前，建議先在本地檢查打包後的網頁是否正常。你可以執行：</p>



<p>Bash</p>



<pre class="wp-block-code"><code>npm run preview
</code></pre>



<p>這會啟動一個臨時伺服器，讓你讀取 dist 資料夾的內容。這比開發模式更接近使用者看到的真實樣子，特別是可以用來檢查 Tailwind 的樣式有沒有因為設定錯誤而被意外刪除。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>把你的專案與 GitHub 連動，達成「只要存檔推送，網頁就自動更新」的自動化流程</p>



<p>要達成只要將程式碼推送到 GitHub，網頁就自動更新，我們需要建立一套自動化部署（CI/CD）流程。這聽起來很專業，但現在的工具已經把過程簡化到點幾下按鍵就能完成。</p>



<h2 class="wp-block-heading">為什麼要連動 GitHub</h2>



<p>傳統做法是每次改完程式碼都要手動執行打包，再用 FTP 上傳到伺服器，既麻煩又容易出錯。連動 GitHub 後，GitHub 就像是一個中繼站。當你執行 <code>git push</code> 把程式碼傳上去，像是 Vercel 或 Netlify 這樣的平台就會偵測到變動，主動接手幫你執行 Node.js 打包指令，並把結果發布到網路上。</p>



<h2 class="wp-block-heading">實作步驟：以 Vercel 為例</h2>



<ol start="1" class="wp-block-list">
<li><strong>建立 GitHub 儲存庫：</strong> 在 GitHub 上新增一個專案，並把你的 React 或 Vue 程式碼推送到上面。</li>



<li><strong>連結 Vercel 帳號：</strong> 到 Vercel 官網用 GitHub 帳號登入。</li>



<li><strong>匯入專案：</strong> 點擊 Import Project，選取你剛才上傳的那個儲存庫。</li>



<li><strong>設定框架：</strong> Vercel 通常會自動偵測你是用 Vite、React 還是 Vue，它會預設好打包指令（例如 <code>npm run build</code>）。你只需要點擊 Deploy。</li>
</ol>



<h2 class="wp-block-heading">享受自動化的成果</h2>



<p>部署完成後，你會得到一個網址。從這一刻起，你只要在 VS Code 修改程式碼，執行以下標準動作：</p>



<p>Bash</p>



<pre class="wp-block-code"><code>git add .
git commit -m "更新了按鈕顏色"
git push
</code></pre>



<p>一旦 push 成功，Vercel 就會開始跑進度條。大約一分鐘內，你的線上網址就會自動更新成最新的樣子。如果你不小心寫壞了程式碼導致打包失敗，Vercel 也會傳郵件通知你，並保留上一個正常的版本，不會讓網頁直接掛掉。</p>



<h2 class="wp-block-heading">協作與預覽功能</h2>



<p>如果你是跟朋友一起開發，當有人提交新的程式碼分支（Pull Request）時，Vercel 還會幫那個分支產生一個專屬的「預覽網址」。這讓你可以在正式合併到主程式之前，先點開網址看看效果是否符合預期。這在專業開發團隊中是標準配備，而現在你一個人就能輕鬆達成。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>使用 Vercel 或 Netlify 這類現代化的雲端平台，<strong>不需要</strong>手動寫 <code>.yaml</code> 檔。</p>



<h2 class="wp-block-heading">為什麼不需要 .yaml 檔</h2>



<p>在過去或是在一些較為傳統的環境（例如 GitHub Actions 或 Jenkins），你需要寫一份 YAML 格式的設定檔來告訴伺服器：第一步要安裝 Node.js，第二步要跑 <code>npm install</code>，第三步要 <code>npm run build</code>。</p>



<p>但現在的 Vercel 或 Netlify 內建了<strong>框架偵測技術</strong>。當你把 GitHub 專案連動過去時，它一看到專案裡有 <code>package.json</code> 檔案，就會自動判斷出這是一個前端專案，並主動幫你跑完所有流程。這就是為什麼我前面提到的流程可以這麼簡單。</p>



<h2 class="wp-block-heading">什麼時候才需要用到 .yaml</h2>



<p>雖然目前的操作可以跳過，但在以下兩種情況，你還是會遇到 YAML 檔案：</p>



<ul class="wp-block-list">
<li><strong>GitHub Actions：</strong> 如果你想在 GitHub 上自動跑測試，或是你想把打包後的檔案傳送到你「自己買的 Linux 伺服器」而不是 Vercel，你就必須在專案目錄下的 <code>.github/workflows/</code> 資料夾裡寫一份 YAML 檔。</li>



<li><strong>Docker 部署：</strong> 如果你的專案非常複雜，需要用到 Docker 容器化技術，通常也會用到 <code>docker-compose.yaml</code> 來管理環境。</li>
</ul>



<h2 class="wp-block-heading">懶人的最佳實踐</h2>



<p>對於個人品牌、作品集或中小型專案，我強烈建議先用 <strong>Vercel</strong> 這種免設定的方案。它把所有複雜的 YAML 設定都隱藏在後台介面中了。你只需要專注在 VS Code 裡寫好你的 React 或 Vue 程式碼，剩下的自動化工作，平台會幫你處理得妥妥當當。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/03/tailwind-css/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>React, Vue 框架比較</title>
		<link>https://stackoverflow.max-everyday.com/2026/03/react-vue-compare/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/03/react-vue-compare/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Tue, 17 Mar 2026 04:54:41 +0000</pubDate>
				<category><![CDATA[node.js筆記]]></category>
		<category><![CDATA[node.js]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=7975</guid>

					<description><![CDATA[下面分別用 React 與 Vue 寫一個簡單的...]]></description>
										<content:encoded><![CDATA[
<p>下面分別用 React 與 Vue 寫一個簡單的數字加減功能，讓你直接感受寫法上的差異。</p>



<h2 class="wp-block-heading">React 的寫法</h2>



<p>React 使用稱為 JSX 的語法，這讓你可以在 JavaScript 程式碼中直接寫 HTML 標籤。它強調資料流的單向控制，修改資料必須透過特定的函式。</p>



<p>JavaScript</p>



<pre class="wp-block-code"><code>import React, { useState } from 'react';

function Counter() {
  const &#91;count, setCount] = useState(0);

  return (
    &lt;div&gt;
      &lt;p&gt;目前數字：{count}&lt;/p&gt;
      &lt;button onClick={() =&gt; setCount(count + 1)}&gt;加 1&lt;/button&gt;
    &lt;/div&gt;
  );
}
</code></pre>



<p>在 React 中，你必須手動呼叫 setCount 來告訴程式資料改變了，頁面才會更新。這種方式雖然寫起來稍微繁瑣，但對於追蹤程式邏輯非常有幫助，讓你知道資料是在什麼時候被改動的。</p>



<h2 class="wp-block-heading">Vue 的寫法</h2>



<p>Vue 的寫法將 HTML、JavaScript 與樣式分開處理，對初學者來說非常直觀。它採用響應式系統，你只需要直接改動變數的值，頁面就會自動更新。</p>



<p>HTML</p>



<pre class="wp-block-code"><code>&lt;script setup&gt;
import { ref } from 'vue';
const count = ref(0);
&lt;/script&gt;

&lt;template&gt;
  &lt;div&gt;
    &lt;p&gt;目前數字：{{ count }}&lt;/p&gt;
    &lt;button @click="count++"&gt;加 1&lt;/button&gt;
  &lt;/div&gt;
&lt;/template&gt;
</code></pre>



<p>Vue 的 template 區塊看起來就像一般的網頁 HTML，這讓設計師或習慣傳統網頁開發的人能快速上手。你可以看到在按鈕點擊時，只需要簡單寫 count++ 就能完成更新，開發效率非常高。</p>



<h2 class="wp-block-heading">Node.js 的角色</h2>



<p>在這個範例中，Node.js 雖然沒有出現在程式碼畫面裡，但它是這兩個框架背後的推手。不論是 React 還是 Vue，你都需要在電腦安裝 Node.js 才能執行開發伺服器，並把這些程式碼轉換成瀏覽器看得懂的內容。</p>



<p>你想試著在你的電腦上安裝 Node.js 並跑跑看這些程式碼嗎？</p>



<h2 class="wp-block-heading">定位與本質的區別</h2>



<p>首先必須釐清 Node.js 與另外兩者的根本不同。Node.js 是一個執行環境，它讓 JavaScript 這種程式語言可以脫離瀏覽器，在電腦或伺服器上直接執行。如果把開發網頁比喻成蓋房子，Node.js 就像是水電系統與地基等看不見的後端架構。React 與 Vue 則屬於前端框架或函式庫，專門用來處理使用者看得到的介面，像是房子的裝潢、家具與互動開關。通常開發者會利用 Node.js 提供的工具來安裝與管理 React 或 Vue 的專案。</p>



<h2 class="wp-block-heading">React 與 Vue 的開發特色</h2>



<p>React 是由 Meta 維護的函式庫，核心思想是把網頁拆解成一個個細小的組件。它強調透過 JavaScript 來處理一切邏輯，開發自由度非常高，但學習者需要具備較強的 JavaScript 基礎，且在開發大型專案時，需要自行挑選與組合各種第三方工具。Vue 則被稱為漸進式框架，開發體驗比較貼近傳統的 HTML 寫法。Vue 的設計非常貼心，官方直接提供路由與狀態管理等工具，讓開發者不用在眾多選項中糾結。對於剛接觸前端框架的人來說，Vue 的語法通常比較直觀且容易上手。</p>



<h2 class="wp-block-heading">選擇建議與市場趨勢</h2>



<p>在選擇這三者時，Node.js 是現代網頁開發者的必備基礎，因為它是運行開發工具的引擎。至於前端的選擇，如果目標是進入大型跨國企業或追求最豐富的社群資源，React 是目前的市場主流，職缺數量也相對較多。如果你希望快速開發原型專案，或是團隊成員偏好簡單明瞭的結構，Vue 會是更有效率的選擇。在實際工作中，這三者並非競爭關係，多數情況下會是使用 Node.js 環境來同時開發 React 或 Vue 的前端介面。</p>



<p></p>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/03/react-vue-compare/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>[docker][node.js] Cannot find package &#8216;multer&#8217; imported from /app/server.js</title>
		<link>https://stackoverflow.max-everyday.com/2026/03/docker-node-js-cannot-find-package-multer-imported-from-app-server-js/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/03/docker-node-js-cannot-find-package-multer-imported-from-app-server-js/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Fri, 13 Mar 2026 00:43:18 +0000</pubDate>
				<category><![CDATA[node.js筆記]]></category>
		<category><![CDATA[node.js]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=7941</guid>

					<description><![CDATA[直接使用 npm run dev, 網頁正常執行...]]></description>
										<content:encoded><![CDATA[
<p>直接使用 npm run dev, 網頁正常執行, 但打包為映像檔之後, 執行卻會出錯.</p>



<p>打包映像檔，名稱自訂為 my-web-app</p>



<pre class="wp-block-code"><code>docker build -t my-web-app .</code></pre>



<p>啟動容器，將本機的 8080 埠對應到容器的 8080 埠</p>



<pre class="wp-block-code"><code>docker rm -f my-running-site
docker run -d -p 8080:8080 --name my-running-site my-web-app</code></pre>



<p>在 docker 看到錯誤訊息:</p>



<pre class="wp-block-preformatted">2026-03-13 08:30:43.727 | node:internal/modules/esm/resolve:873<br>2026-03-13 08:30:43.727 |   throw new ERR_MODULE_NOT_FOUND(packageName, fileURLToPath(base), null);<br>2026-03-13 08:30:43.727 |         ^<br>2026-03-13 08:30:43.727 | <br>2026-03-13 08:30:43.727 | Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'multer' imported from /app/server.js<br>2026-03-13 08:30:43.727 |     at packageResolve (node:internal/modules/esm/resolve:873:9)<br>2026-03-13 08:30:43.727 |     at moduleResolve (node:internal/modules/esm/resolve:946:18)<br>2026-03-13 08:30:43.727 |     at defaultResolve (node:internal/modules/esm/resolve:1188:11)<br>2026-03-13 08:30:43.727 |     at ModuleLoader.defaultResolve (node:internal/modules/esm/loader:708:12)<br>2026-03-13 08:30:43.727 |     at #cachedDefaultResolve (node:internal/modules/esm/loader:657:25)<br>2026-03-13 08:30:43.727 |     at ModuleLoader.resolve (node:internal/modules/esm/loader:640:38)<br>2026-03-13 08:30:43.727 |     at ModuleLoader.getModuleJobForImport (node:internal/modules/esm/loader:264:38)<br>2026-03-13 08:30:43.727 |     at ModuleJob._link (node:internal/modules/esm/module_job:168:49) {<br>2026-03-13 08:30:43.727 |   code: 'ERR_MODULE_NOT_FOUND'<br>2026-03-13 08:30:43.727 | }<br>2026-03-13 08:30:43.727 | <br>2026-03-13 08:30:43.727 | Node.js v20.20.1</pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>這個錯誤代表 Node.js 在執行 server.js 時，找不到名為 multer 的套件。雖然你可能在本地電腦開發時有安裝，但 Docker 容器是一個獨立的環境，必須確保套件也被安裝在容器內部。</p>



<h3 class="wp-block-heading">檢查 package.json 設定</h3>



<p>請確認你的 package.json 檔案中，dependencies 區塊是否包含 multer。如果沒有，請在本地資料夾執行 npm install multer，這會自動更新設定檔。</p>



<h3 class="wp-block-heading">修改 Dockerfile</h3>



<p>請檢查 Dockerfile，確保有執行安裝指令。通常正確的順序是先複製 package.json，執行安裝，再複製其餘程式碼。以下是一個標準的寫法範例：</p>



<p>Dockerfile</p>



<pre class="wp-block-code"><code>FROM node:20
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
CMD &#91;"node", "server.js"]
</code></pre>



<h3 class="wp-block-heading">重新構建鏡像</h3>



<p>如果你剛新增了套件，必須重新編譯 Docker 鏡像，否則容器會一直使用舊的狀態。請執行以下指令：</p>



<p>docker build -t 你的鏡像名稱 .</p>



<h3 class="wp-block-heading">檢查掛載卷的問題</h3>



<p>如果你使用 docker-compose 並且掛載了整個開發目錄，有時候本地的 node_modules 會覆蓋容器內的環境。建議在 docker-compose.yml 中使用匿名卷來保護容器內的套件：</p>



<p>YAML</p>



<pre class="wp-block-code"><code>services:
  web:
    build: .
    volumes:
      - .:/app
      - /app/node_modules
</code></pre>



<p>這樣做可以確保容器使用自己在構建階段安裝的 multer，而不是抓取本地可能不相容或遺漏的資料夾。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>問題出在多階段構建（Multi-stage build）的邏輯。你在第二階段只安裝了生產環境套件，但你的 server.js 卻引用了 multer。</p>



<h3 class="wp-block-heading">核心原因</h3>



<p>在第二階段的指令 RUN npm install &#8211;production 中，npm 只會安裝 package.json 裡面 dependencies 區塊的套件。如果你的 multer 被歸類在 devDependencies，或者你在第一階段安裝後沒有將 package.json 同步更新，第二階段就不會安裝它。</p>



<h3 class="wp-block-heading">解決方法</h3>



<p>請檢查 package.json，確保 multer 位於 dependencies 而非 devDependencies：</p>



<p>JSON</p>



<pre class="wp-block-code"><code>{
  "dependencies": {
    "multer": "^1.4.5-lts.1",
    "express": "^4.18.2"
  }
}
</code></pre>



<p>如果 multer 已經在 dependencies 中，但仍然報錯，通常是因為 server.js 依賴的是第一階段編譯後的產物，或是 node_modules 沒被正確帶入。</p>



<h3 class="wp-block-heading">建議的 Dockerfile 修正</h3>



<p>你可以嘗試將安裝好的 node_modules 從第一階段直接複製過來，這樣可以確保兩邊環境完全一致，也不用再執行一次 npm install：</p>



<p>Dockerfile</p>



<pre class="wp-block-code"><code># 第一階段：編譯
FROM node:20-alpine AS build-stage
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build

# 第二階段：執行
FROM node:20-alpine
WORKDIR /app
# 直接從 build-stage 複製安裝好的 production 套件
COPY --from=build-stage /app/node_modules ./node_modules
COPY --from=build-stage /app/dist ./dist
COPY --from=build-stage /app/package.json ./package.json
COPY server.js ./

EXPOSE 8080
CMD &#91;"node", "server.js"]
</code></pre>



<h3 class="wp-block-heading">檢查 server.js 的路徑</h3>



<p>如果你的 server.js 裡面有一行 import multer from &#8216;multer&#8217;，但它其實是在執行 dist 資料夾裡的內容，請確認路徑是否正確。通常在執行 node server.js 時，它會在當前目錄的 node_modules 找套件。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>error:</p>



<pre class="wp-block-preformatted">Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'cors' imported from /app/server.js<br>    at packageResolve (node:internal/modules/esm/resolve:873:9)<br>    at moduleResolve (node:internal/modules/esm/resolve:946:18)<br>    at defaultResolve (node:internal/modules/esm/resolve:1188:11)<br>    at ModuleLoader.defaultResolve (node:internal/modules/esm/loader:708:12)<br>    at #cachedDefaultResolve (node:internal/modules/esm/loader:657:25)<br>    at ModuleLoader.resolve (node:internal/modules/esm/loader:640:38)<br>    at ModuleLoader.getModuleJobForImport (node:internal/modules/esm/loader:264:38)<br>    at ModuleJob._link (node:internal/modules/esm/module_job:168:49) {<br>  code: 'ERR_MODULE_NOT_FOUND'<br>}</pre>



<p>出現這個錯誤的原因與剛才的 multer 完全相同，這代表你的 server.js 雖然引用了 cors 套件，但 Docker 容器的環境中並沒有安裝它。</p>



<h3 class="wp-block-heading">檢查 package.json 的依賴設定</h3>



<p>請打開你的 package.json 檔案，確認 dependencies 區塊中是否有 cors。如果沒有，請在本地終端機執行：</p>



<p>npm install cors</p>



<p>這會將 cors 寫入 package.json。因為你在 Dockerfile 的第二階段使用了 RUN npm install &#8211;production，如果 package.json 裡沒有列出這個套件，Docker 就不會安裝它，導致程式執行時找不到模組。</p>



<p>package.json:</p>



<pre class="wp-block-code"><code>"dependencies": {
    ...
    "cors": "^2.8.6",
    ...
}</code></pre>



<h3 class="wp-block-heading">為什麼會連續出現找不到套件的錯誤</h3>



<p>這通常是因為你在開發 server.js 時，直接使用 npm install &lt;package&gt; 安裝了多個套件（如 multer, cors），但可能沒有成功同步到 Docker 構建流程中。</p>



<h3 class="wp-block-heading">建議的解決步驟</h3>



<ol start="1" class="wp-block-list">
<li>確保所有 server.js 用到的套件（如 express, multer, cors）都出現在 package.json 的 dependencies 中。</li>



<li>重新執行 Docker 編譯指令，確保緩存被更新：docker build &#8211;no-cache -t 你的鏡像名稱 .</li>



<li>檢查 server.js 的開頭，確保引用的名稱與安裝的套件名稱一致：import cors from &#8216;cors&#8217;;</li>
</ol>



<p>如果你的 package.json 已經有這些套件卻還是報錯，建議檢查 Dockerfile 是否有正確執行到第二階段的 npm install。</p>



<p></p>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/03/docker-node-js-cannot-find-package-multer-imported-from-app-server-js/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>React Table Row 在mobile device 無法換行</title>
		<link>https://stackoverflow.max-everyday.com/2026/03/react-table-row-mobile-device-wrap/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/03/react-table-row-mobile-device-wrap/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Thu, 12 Mar 2026 05:34:09 +0000</pubDate>
				<category><![CDATA[node.js筆記]]></category>
		<category><![CDATA[node.js]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=7927</guid>

					<description><![CDATA[AI 詠唱出來的第一版 UI, 如果是只有使用 ...]]></description>
										<content:encoded><![CDATA[
<p>AI 詠唱出來的第一版 UI, 如果是只有使用 desktop device only, 是沒問題, table row 的顯示方式在 mobile device 上顯示不友善, 因為建言編號沒換行, 造成版面被擠壓, 換成 div 的顯示方式, 讓 mobile deivce 針對同一筆row, 可以顯示為多行.</p>



<h3 class="wp-block-heading">Key Improvements:</h3>



<ol class="wp-block-list">
<li><strong>Responsive Layout</strong>:
<ul class="wp-block-list">
<li><strong>Desktop View</strong>: Maintains a clean, professional table layout with sortable columns.</li>



<li><strong>Mobile View</strong>: Automatically switches to a <strong>card-based layout</strong>. This prevents the &#8220;squeezed&#8221; look of the table on small screens and ensures that all information (ID, subject, status, and deadline) is clearly readable.</li>
</ul>
</li>



<li><strong>Unified Styling</strong>:
<ul class="wp-block-list">
<li>The background colors for table rows and cards are now consistent across both User and Management modes to avoid visual confusion.</li>



<li>Status badges and typography have been polished for better clarity.</li>
</ul>
</li>



<li><strong>Mobile Fixes</strong>:
<ul class="wp-block-list">
<li><strong>Language Switcher</strong>: Fixed the issue where the language switcher was not clickable on mobile devices. It now uses a robust React state management with a backdrop to handle closing the menu.</li>



<li><strong>Search &amp; Filters</strong>: Adjusted the search input and status filter buttons to be more space-efficient on smaller screens.</li>
</ul>
</li>



<li><strong>Management Dashboard</strong>:
<ul class="wp-block-list">
<li>Refactored the management view to include the same responsive card system.</li>



<li>Enhanced the status filter UI with clear counts and interactive feedback.</li>
</ul>
</li>
</ol>



<p>The application has been verified to build and lint correctly, ensuring a stable experience across all devices.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>異動的程式碼:</p>



<pre class="wp-block-code"><code>         &lt;div className="bg-white border border-slate-200 rounded-xl overflow-hidden shadow-sm">
<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">-          &lt;div className="overflow-x-auto"></mark>
<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-green-cyan-color">+          {/* Desktop Table View */}
+          &lt;div className="hidden md:block overflow-x-auto">
</mark>             &lt;table className="w-full text-left border-collapse">
               &lt;thead>
                 &lt;tr className="bg-slate-50 text-slate-500 text-sm uppercase tracking-wider font-semibold">
@@ -1170,6 +1171,72 @@ export default function App() {
               &lt;/tbody>
             &lt;/table>
           &lt;/div>
<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-green-cyan-color">+
+          {/* Mobile Card View */}
+          &lt;div className="md:hidden divide-y divide-slate-100">
+            {currentItems.length > 0 ? (
+              currentItems.map((s) => {
+                const deadlineInfo = getDeadlineInfo(s);
+                let translatedDeadlineText = deadlineInfo.text;
+                if (deadlineInfo.isDueToday) {
+                  translatedDeadlineText = t.dueToday;
+                } else if (deadlineInfo.isOverdue) {
+                  translatedDeadlineText = `${t.overdue} ${Math.abs(getWorkingDaysDifference(TODAY, new Date(deadlineInfo.deadlineDate)))} ${t.days}`;
+                } else {
+                  translatedDeadlineText = `${t.remaining} ${getWorkingDaysDifference(TODAY, new Date(deadlineInfo.deadlineDate))} ${t.days}`;
+                }
+
+                return (
+                  &lt;div
+                    key={s.id}
+                    onClick={() => handleSuggestionClick(s)}
+                    className="p-4 hover:bg-slate-50 active:bg-slate-100 transition-colors cursor-pointer space-y-3"
+                  >
+                    &lt;div className="flex justify-between items-start">
+                      &lt;div className="flex flex-col space-y-1">
+                        &lt;span className="text-&#91;10px] font-mono text-slate-400 uppercase tracking-wider">{s.id}&lt;/span>
+                        &lt;h3 className="font-bold text-slate-900 leading-tight">{s.subject}&lt;/h3>
+                        {s.isExtended &amp;&amp; (
+                          &lt;span className="text-&#91;10px] text-blue-500 font-bold uppercase tracking-tighter">{t.extendedOnce}&lt;/span>
+                        )}
+                      &lt;/div>
+                      &lt;StatusBadge status={s.status} language={language} isUserMode={true} />
+                    &lt;/div>
+
+                    &lt;div className="flex justify-between items-center text-xs">
+                      &lt;div className="flex items-center text-slate-500">
+                        &lt;Building2 size={14} className="mr-1" />
+                        &lt;span>{s.department}&lt;/span>
+                      &lt;/div>
+
+                      &lt;div className="text-right">
+                        {s.status === 'unprocessed' ? (
+                          &lt;span className="text-slate-400 italic">{t.suggestionReceived}&lt;/span>
+                        ) : (
+                          &lt;div className="flex flex-col items-end">
+                            &lt;div className={`flex items-center space-x-1 font-medium
+                              ${deadlineInfo.isDueToday ? 'text-red-600' : ''}
+                              ${deadlineInfo.isOverdue ? 'text-red-500 italic' : ''}
+                              ${!deadlineInfo.isOverdue &amp;&amp; !deadlineInfo.isDueToday ? 'text-slate-600' : ''}
+                            `}>
+                              {deadlineInfo.isDueToday || deadlineInfo.isOverdue ? &lt;AlertTriangle size={12} /> : &lt;Clock size={12} className="text-orange-400" />}
+                              &lt;span>{translatedDeadlineText}&lt;/span>
+                            &lt;/div>
+                            &lt;span className="text-&#91;10px] text-slate-400">{deadlineInfo.deadlineDate}&lt;/span>
+                          &lt;/div>
+                        )}
+                      &lt;/div>
+                    &lt;/div>
+                  &lt;/div>
+                );
+              })
+            ) : (
+              &lt;div className="p-12 text-center text-slate-400">
+                {t.noResults}
+              &lt;/div>
+            )}
+          &lt;/div>
+        &lt;/div>
</mark>           &lt;div className="px-6 py-4 bg-slate-50 border-t border-slate-100 flex flex-col sm:flex-row justify-between items-center gap-4 text-sm text-slate-500">
             &lt;div className="flex items-center space-x-4">
               &lt;p>{t.all} {sortedSuggestions.length} {t.totalResults}&lt;/p>
@@ -1192,10 +1259,9 @@ export default function App() {
               &lt;/button>
             &lt;/div>
           &lt;/div>
-        &lt;/div>
-      &lt;/motion.div>
-    );
-  };
<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-green-cyan-color">+        &lt;/motion.div>
+      );
+    };</mark>

   const renderManagement = () => {
     const t = translations&#91;language];
@@ -1328,13 +1394,15 @@ export default function App() {
                     setSearchQuery(e.target.value);
                     setCurrentPage(1);
                   }}
<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">-                  className="pl-9 pr-4 py-1.5 rounded-lg border border-slate-200 text-sm focus:ring-2 focus:ring-slate-800 outline-none"
</mark><mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-green-cyan-color">+                  className="pl-9 pr-4 py-1.5 rounded-lg border border-slate-200 text-sm focus:ring-2 focus:ring-slate-800 outline-none w-40 sm:w-64"
</mark>                 />
<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">-          &lt;div className="overflow-x-auto"></mark>
<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">+
+          {/* Desktop Table View */}
+          &lt;div className="hidden md:block overflow-x-auto"></mark>
             &lt;table className="w-full text-left border-collapse">
               &lt;thead>
                 &lt;tr className="bg-slate-50 text-slate-500 text-sm uppercase tracking-wider font-semibold">
@@ -1374,7 +1442,6 @@ export default function App() {
                 {currentItems.length > 0 ? (
                   currentItems.map((s) => {
                     const deadlineInfo = getDeadlineInfo(s);
<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">-                    // Translate deadline text
</mark>                     let translatedDeadlineText = deadlineInfo.text;
                     if (deadlineInfo.isDueToday) {
                       translatedDeadlineText = t.dueToday;
@@ -1388,30 +1455,44 @@ export default function App() {
                       &lt;tr
                         key={s.id}
                         onClick={() => handleSuggestionClick(s)}
<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">-                        className="hover:bg-slate-50 transition-colors cursor-pointer border-b border-slate-50 last:border-0"
</mark><mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-green-cyan-color">+                        className="hover:bg-slate-50 transition-colors cursor-pointer group border-b border-slate-50 last:border-0"
</mark>                       >
<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">-                        &lt;td className="px-6 py-4 text-sm font-mono text-slate-500">{s.id}&lt;/td>
-                        &lt;td className="px-6 py-4 font-medium text-slate-900">{s.subject}&lt;/td>
-                        &lt;td className="px-6 py-4 text-slate-600">{s.department}&lt;/td>
-                        &lt;td className="px-6 py-4"></mark>
<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-green-cyan-color">+                        &lt;td className="px-6 py-5 text-sm font-mono text-slate-500">
+                          &lt;span>{s.id}&lt;/span>
+                        &lt;/td>
+                        &lt;td className="px-6 py-5 font-medium text-slate-900">
+                          &lt;div className="flex flex-col">
+                            &lt;span>{s.subject}&lt;/span>
+                            {s.isExtended &amp;&amp; (
+                              &lt;span className="text-&#91;10px] text-blue-500 font-bold uppercase tracking-tighter">{t.extendedOnce}&lt;/span>
+                            )}
+                          &lt;/div>
+                        &lt;/td>
+                        &lt;td className="px-6 py-5 text-slate-600">{s.department}&lt;/td>
+                        &lt;td className="px-6 py-5"></mark>
                           &lt;StatusBadge status={s.status} language={language} isUserMode={false} />
                         &lt;/td>
<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">-                        &lt;td className="px-6 py-4 text-right"></mark>
<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-green-cyan-color">+                        &lt;td className="px-6 py-5 text-right">
</mark>                           &lt;div className="flex flex-col items-end space-y-0.5">
                             {s.status === 'unprocessed' ? (
<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">-                              &lt;div className="text-slate-400 text-xs italic"></mark>
<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-green-cyan-color">+                              &lt;div className="text-slate-400 text-sm italic"></mark>
                                 {t.suggestionReceived}
                               &lt;/div>
                             ) : (
                               &lt;>
<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">-                                &lt;div className={`flex items-center justify-end space-x-1 text-xs
</mark><mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-green-cyan-color">+                                &lt;div className={`flex items-center justify-end space-x-1</mark>
                                   ${deadlineInfo.isDueToday ? 'text-red-600 font-medium' : ''}
                                   ${deadlineInfo.isOverdue ? 'text-red-500 font-medium italic' : ''}
                                   ${!deadlineInfo.isOverdue &amp;&amp; !deadlineInfo.isDueToday ? 'text-slate-600' : ''}
                                 `}>
<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-green-cyan-color">+                                  {!deadlineInfo.isOverdue &amp;&amp; !deadlineInfo.isDueToday &amp;&amp; &lt;Clock size={16} className="text-orange-400" />}
+                                  {deadlineInfo.isDueToday &amp;&amp; &lt;AlertTriangle size={16} />}
+                                  {deadlineInfo.isOverdue &amp;&amp; &lt;AlertTriangle size={16} />}</mark>
                                   &lt;span>{translatedDeadlineText}&lt;/span>
                                 &lt;/div>
<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">-                                &lt;div className="text-&#91;10px] text-slate-400">{deadlineInfo.deadlineDate}&lt;/div></mark>
<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-green-cyan-color">+                                &lt;div className="text-&#91;10px] text-slate-400">
+                                  {t.deadlineLabel}: {deadlineInfo.deadlineDate}
+                                &lt;/div></mark>
                               &lt;/>
                             )}
                           &lt;/div>
@@ -1429,27 +1510,92 @@ export default function App() {
               &lt;/tbody>
             &lt;/table>
           &lt;/div>
<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color">-          &lt;div className="px-6 py-4 bg-slate-50 border-t border-slate-100 flex flex-col sm:flex-row justify-between items-center gap-4 text-sm text-slate-500">
-            &lt;div className="flex items-center space-x-4">
-              &lt;p>{t.all} {sortedSuggestions.length} {t.totalResults}&lt;/p>
-            &lt;/div>
-            &lt;div className="flex items-center space-x-4">
-              &lt;button
-                onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
-                disabled={currentPage === 1}
-                className="p-1 rounded hover:bg-slate-200 transition-colors disabled:opacity-30"
-              >
-                &lt;ChevronLeft size={20} />
-              &lt;/button>
-              &lt;span className="font-medium text-slate-900">{t.page} {currentPage} {t.of} {totalPages || 1} {t.pageSuffix}&lt;/span>
-              &lt;button
-                onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
-                disabled={currentPage === totalPages || totalPages === 0}
-                className="p-1 rounded hover:bg-slate-200 transition-colors disabled:opacity-30"
-              >
-                &lt;ChevronRight size={20} />
-              &lt;/button>
-            &lt;/div></mark>
<mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-green-cyan-color">+
+          {/* Mobile Card View */}
+          &lt;div className="md:hidden divide-y divide-slate-100">
+            {currentItems.length > 0 ? (
+              currentItems.map((s) => {
+                const deadlineInfo = getDeadlineInfo(s);
+                let translatedDeadlineText = deadlineInfo.text;
+                if (deadlineInfo.isDueToday) {
+                  translatedDeadlineText = t.dueToday;
+                } else if (deadlineInfo.isOverdue) {
+                  translatedDeadlineText = `${t.overdue} ${Math.abs(getWorkingDaysDifference(TODAY, new Date(deadlineInfo.deadlineDate)))} ${t.days}`;
+                } else {
+                  translatedDeadlineText = `${t.remaining} ${getWorkingDaysDifference(TODAY, new Date(deadlineInfo.deadlineDate))} ${t.days}`;
+                }
+
+                return (
+                  &lt;div
+                    key={s.id}
+                    onClick={() => handleSuggestionClick(s)}
+                    className="p-4 hover:bg-slate-50 active:bg-slate-100 transition-colors cursor-pointer space-y-3"
+                  >
+                    &lt;div className="flex justify-between items-start">
+                      &lt;div className="flex flex-col space-y-1">
+                        &lt;span className="text-&#91;10px] font-mono text-slate-400 uppercase tracking-wider">{s.id}&lt;/span>
+                        &lt;h3 className="font-bold text-slate-900 leading-tight">{s.subject}&lt;/h3>
+                        {s.isExtended &amp;&amp; (
+                          &lt;span className="text-&#91;10px] text-blue-500 font-bold uppercase tracking-tighter">{t.extendedOnce}&lt;/span>
+                        )}
+                      &lt;/div>
+                      &lt;StatusBadge status={s.status} language={language} isUserMode={false} />
+                    &lt;/div>
+
+                    &lt;div className="flex justify-between items-center text-xs">
+                      &lt;div className="flex items-center text-slate-500">
+                        &lt;Building2 size={14} className="mr-1" />
+                        &lt;span>{s.department}&lt;/span>
+                      &lt;/div>
+                      &lt;div className="text-right">
+                        {s.status === 'unprocessed' ? (
+                          &lt;span className="text-slate-400 italic">{t.suggestionReceived}&lt;/span>
+                        ) : (
+                          &lt;div className="flex flex-col items-end">
+                            &lt;div className={`flex items-center space-x-1 font-medium
+                              ${deadlineInfo.isDueToday ? 'text-red-600' : ''}
+                              ${deadlineInfo.isOverdue ? 'text-red-500 italic' : ''}
+                              ${!deadlineInfo.isOverdue &amp;&amp; !deadlineInfo.isDueToday ? 'text-slate-600' : ''}
+                            `}>
+                              {deadlineInfo.isDueToday || deadlineInfo.isOverdue ? &lt;AlertTriangle size={12} /> : &lt;Clock size={12} className="text-orange-400" />}
+                              &lt;span>{translatedDeadlineText}&lt;/span>
+                            &lt;/div>
+                            &lt;span className="text-&#91;10px] text-slate-400">{deadlineInfo.deadlineDate}&lt;/span>
+                          &lt;/div>
+                        )}
+                      &lt;/div>
+                    &lt;/div>
+                  &lt;/div>
+                );
+              })
+            ) : (
+              &lt;div className="p-12 text-center text-slate-400">
+                {t.noResults}
+              &lt;/div>
+            )}
+          &lt;/div>
+        &lt;/div>
+              &lt;/div>
+            )}
+          &lt;/div>
+        &lt;/div>
+        &lt;div className="px-6 py-4 bg-slate-50 border-t border-slate-100 flex flex-col sm:flex-row justify-between items-center gap-4 text-sm text-slate-500">
+          &lt;div className="flex items-center space-x-4">
+            &lt;p>{t.all} {sortedSuggestions.length} {t.totalResults}&lt;/p>
+          &lt;/div>
+          &lt;div className="flex items-center space-x-4">
+            &lt;button
+              onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
+              disabled={currentPage === 1}
+              className="p-1 rounded hover:bg-slate-200 transition-colors disabled:opacity-30"
+            >
+              &lt;ChevronLeft size={20} />
+            &lt;/button>
+            &lt;span className="font-medium text-slate-900">{t.page} {currentPage} {t.of} {totalPages || 1} {t.pageSuffix}&lt;/span>
+            &lt;button
+              onClick={() => setCurrentPage(prev => Math.min(totalPages, prev + 1))}
+              disabled={currentPage === totalPages || totalPages === 0}
+              className="p-1 rounded hover:bg-slate-200 transition-colors disabled:opacity-30"
+            >
+              &lt;ChevronRight size={20} />
+            &lt;/button></mark>
           &lt;/div>
         &lt;/div></code></pre>



<p></p>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/03/react-table-row-mobile-device-wrap/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Node.js 多語系檔案</title>
		<link>https://stackoverflow.max-everyday.com/2026/03/node-js-i18next/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/03/node-js-i18next/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Mon, 09 Mar 2026 07:22:57 +0000</pubDate>
				<category><![CDATA[node.js筆記]]></category>
		<category><![CDATA[node.js]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=7864</guid>

					<description><![CDATA[在 Node.js 專管語系檔案時，通常會使用物...]]></description>
										<content:encoded><![CDATA[
<p>在 Node.js 專管語系檔案時，通常會使用物件格式來儲存字串。要在字串中嵌入變數，最常見的做法是使用大括號作為佔位符，並配合取代函數或是 i18next 這類套件來處理。</p>



<h3 class="wp-block-heading">基礎實作方式</h3>



<p>如果不使用外部套件，可以定義一個簡單的取代函式。在 translations.ts 中，字串會長這樣：</p>



<p>TypeScript</p>



<pre class="wp-block-code"><code>export const translations = {
  welcome: "你好，{name}！歡迎回來。",
  items: "你有 {count} 個待辦事項。"
};
</code></pre>



<p>接著撰寫一個處理邏輯，將大括號內的標記替換為實際數值：</p>



<p>TypeScript</p>



<pre class="wp-block-code"><code>function t(key: keyof typeof translations, variables: Record&lt;string, string | number&gt;) {
  let message = translations&#91;key];
  
  Object.keys(variables).forEach(placeholder =&gt; {
    const value = variables&#91;placeholder];
    message = message.replace(`{${placeholder}}`, String(value));
  });
  
  return message;
}

const greeting = t("welcome", { name: "小明" });
console.log(greeting); // 輸出：你好，小明！歡迎回來。
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">使用 i18next 套件</h3>



<p>如果你使用的是業界標準的 i18next，語法會稍微不同，它預設支援雙大括號或是特定格式。在語系檔中：</p>



<p>TypeScript</p>



<pre class="wp-block-code"><code>{
  "welcome": "你好，{{name}}！"
}
</code></pre>



<p>在程式碼中調用的方式：</p>



<p>TypeScript</p>



<pre class="wp-block-code"><code>import i18next from 'i18next';

i18next.t('welcome', { name: '小明' });
</code></pre>



<p>這種方式的好處是套件會自動幫你處理複雜的格式，例如數字千分位或日期格式化。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">注意事項</h3>



<p>確保變數名稱在字串與程式碼中完全一致。如果翻譯檔是從外部 JSON 匯入的，建議建立一個介面來約束變數名稱，避免因為打錯字導致變數沒有成功被取代，畫面直接顯示出 {name} 本身。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>支援多層級物件的翻譯解析函式</p>



<p>這是一個支援多層級物件（例如：common.welcome）且能自動取代變數的進階實作。這種結構在大型 Node.js 專案中非常實用。</p>



<h3 class="wp-block-heading">translations.ts 語系檔結構</h3>



<p>建議將語系內容定義為巢狀物件，並使用 TypeScript 的物件結構來確保型別安全：</p>



<p>TypeScript</p>



<pre class="wp-block-code"><code>export const translations = {
  common: {
    welcome: "你好，{name}！歡迎來到我們的平台。",
    error: "錯誤碼：{code}"
  },
  shop: {
    cart: "你的購物車中有 {count} 件商品。"
  }
};
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">解析函式實作</h3>



<p>這個函式會根據點分隔符號（例如 &#8220;common.welcome&#8221;）找到正確的字串，再執行變數替換：</p>



<p>TypeScript</p>



<pre class="wp-block-code"><code>type TranslationData = typeof translations;

function getDeepValue(obj: any, path: string) {
  return path.split('.').reduce((prev, curr) =&gt; prev &amp;&amp; prev&#91;curr], obj);
}

function translate(key: string, params: Record&lt;string, string | number&gt; = {}) {
  let template = getDeepValue(translations, key);

  if (!template || typeof template !== 'string') {
    return key; // 如果找不到對應字串，回傳路徑名稱作為提示
  }

  // 遍歷所有傳入的變數並進行全域替換
  Object.keys(params).forEach(param =&gt; {
    const value = params&#91;param];
    const regex = new RegExp(`{${param}}`, 'g');
    template = template.replace(regex, String(value));
  });

  return template;
}
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">如何使用</h3>



<p>你可以在專案中的任何地方呼叫這個函式。這段邏輯模擬了 i18n 工具的基本行為：</p>



<p>TypeScript</p>



<pre class="wp-block-code"><code>const userName = "王小明";
const loginMessage = translate("common.welcome", { name: userName });
console.log(loginMessage); // 輸出：你好，王小明！歡迎來到我們的平台。

const cartMessage = translate("shop.cart", { count: 5 });
console.log(cartMessage); // 輸出：你的購物車中有 5 件商品。
</code></pre>



<h3 class="wp-block-heading">進階建議</h3>



<p>如果你的翻譯需求變得更複雜，例如需要處理複數形式（1 item vs 2 items）或性別差異，建議轉向使用 i18next 或 formatjs 這些成熟的函式庫。但對於大多數中小型專案，上述的字串取代邏輯已經足夠應付 90% 的情境。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>要在 React 元件中處理這類動態變數，最直覺且不需要改動太多現有程式碼的方法是將 <code>t.hi</code> 視為範本字串，並在渲染時使用 <code>replace</code> 方法。</p>



<h3 class="wp-block-heading">直接在 JSX 中處理</h3>



<p>你可以直接在 <code>&lt;h2&gt;</code> 標籤內對字串進行取代。這種做法最快，適合專案規模較小的情況：</p>



<p>TypeScript</p>



<pre class="wp-block-code"><code>&lt;h2 className="text-2xl font-semibold text-slate-900 mb-1"&gt;
  {t.hi.replace('{username}', username)}
&lt;/h2&gt;
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">建立一個簡單的翻譯工具函式</h3>



<p>如果你的專案中有很多類似的變數需求，建議寫一個輔助函式處理取代邏輯，避免在 UI 程式碼中到處寫 <code>replace</code>：</p>



<p>TypeScript</p>



<pre class="wp-block-code"><code>// 輔助函式定義
const interpolate = (str: string, params: Record&lt;string, string&gt;) =&gt; {
  let result = str;
  for (const &#91;key, value] of Object.entries(params)) {
    result = result.replace(`{${key}}`, value);
  }
  return result;
};

// 在元件中使用
&lt;h2 className="text-2xl font-semibold text-slate-900 mb-1"&gt;
  {interpolate(t.hi, { username })}
&lt;/h2&gt;
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">將翻譯內容改為函式（推薦做法）</h3>



<p>為了確保型別安全並減少字串處理的效能損耗，另一種常見的做法是讓語系檔中的特定項目支援函式：</p>



<p>TypeScript</p>



<pre class="wp-block-code"><code>// 修改語系檔定義
export const translations = {
  zh: {
    // ... 其他內容
    hi: (username: string) =&gt; `Hi ${username}！`,
  }
};

// 在元件中使用
&lt;h2 className="text-2xl font-semibold text-slate-900 mb-1"&gt;
  {typeof t.hi === 'function' ? t.hi(username) : t.hi}
&lt;/h2&gt;
</code></pre>



<h3 class="wp-block-heading">總結建議</h3>



<ol start="1" class="wp-block-list">
<li>如果只有一兩個地方要用，直接用 <strong>replace</strong> 即可。</li>



<li>如果你的校務建言系統規模會持續擴大，建議建立一個 <strong>interpolate</strong> 工具函式，這樣以後遇到 <code>{date}</code> 或 <code>{count}</code> 等變數也能統一處理。</li>



<li>檢查你的 <code>t.welcome</code> 是否也包含變數，處理邏輯是一樣的。</li>
</ol>



<p></p>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/03/node-js-i18next/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>開發模式執行 npm run dev 時，Vite 預設只會啟動「前端開發伺服器」</title>
		<link>https://stackoverflow.max-everyday.com/2026/03/npm-run-dev-vite-server/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/03/npm-run-dev-vite-server/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Mon, 09 Mar 2026 02:49:49 +0000</pubDate>
				<category><![CDATA[node.js筆記]]></category>
		<category><![CDATA[node.js]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=7855</guid>

					<description><![CDATA[當你在開發模式執行 npm run dev 時，...]]></description>
										<content:encoded><![CDATA[
<p>當你在開發模式執行 <code>npm run dev</code> 時，Vite 預設只會啟動「前端開發伺服器」，它並不認識你在 <code>server.js</code> 寫的 API 路由。</p>



<p>要讓前端順利呼叫到 <code>/api/config-test</code>，你有兩個主要的處理方式：</p>



<h3 class="wp-block-heading">1. 同時啟動兩個服務 (最保險)</h3>



<p>在開發階段，你需要同時開啟兩個終端機視窗：</p>



<ul class="wp-block-list">
<li><strong>視窗 A</strong>：執行 <code>node server.js</code>（啟動後端 API，預設在 8080 埠）。</li>



<li><strong>視窗 B</strong>：執行 <code>npm run dev</code>（啟動前端介面，預設在 5173 埠）。</li>
</ul>



<p>此時，如果你直接在瀏覽器輸入 <code>http://localhost:8080/api/config-test</code>，你應該就能看到 JSON 資料。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">2. 設定 Vite 代理 (Proxy)</h3>



<p>為了讓前端程式碼（5173）呼叫 <code>/api/...</code> 時能自動轉發給後端（8080），你需要修改 <code>vite.config.ts</code> (或 <code>.js</code>)：</p>



<p>TypeScript</p>



<pre class="wp-block-code"><code>// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

export default defineConfig({
  plugins: &#91;react()],
  server: {
    proxy: {
      // 當前端呼叫 /api 時，轉發到 8080
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
      },
    },
  },
})
</code></pre>



<p>設定好 Proxy 後，你在前端程式碼中就可以這樣呼叫：</p>



<p>JavaScript</p>



<pre class="wp-block-code"><code>fetch('/api/config-test')
  .then(res =&gt; res.json())
  .then(data =&gt; console.log(data));
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">3. 在前端畫面中顯示資料 (驗證方式)</h3>



<p>如果你想直接在 AI Studio 產生的畫面上看到結果，可以在你的 <code>.tsx</code> 元件中加入這段測試代碼：</p>



<p>TypeScript</p>



<pre class="wp-block-code"><code>import { useEffect, useState } from 'react';

function TestComponent() {
  const &#91;dbInfo, setDbInfo] = useState('載入中...');

  useEffect(() =&gt; {
    fetch('/api/config-test')
      .then(res =&gt; res.json())
      .then(data =&gt; setDbInfo(data.dbName))
      .catch(err =&gt; setDbInfo('呼叫失敗'));
  }, &#91;]);

  return &lt;div&gt;資料庫名稱：{dbInfo}&lt;/div&gt;;
}
</code></pre>



<h3 class="wp-block-heading">總結驗證流程</h3>



<ol start="1" class="wp-block-list">
<li>確保 <strong>Docker 容器已關閉</strong>（避免佔用 8080 埠）。</li>



<li>啟動後端：<code>node server.js</code>。</li>



<li>啟動前端：<code>npm run dev</code>。</li>



<li>打開瀏覽器 <code>http://localhost:5173/api/config-test</code>。如果看到 JSON，代表 Proxy 設定成功。</li>
</ol>



<p></p>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/03/npm-run-dev-vite-server/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>連線 Azure SQL 的 Node.js 範例代碼</title>
		<link>https://stackoverflow.max-everyday.com/2026/03/azure-sql-node-js-example/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/03/azure-sql-node-js-example/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Mon, 09 Mar 2026 02:43:28 +0000</pubDate>
				<category><![CDATA[Azure 筆記]]></category>
		<category><![CDATA[node.js筆記]]></category>
		<category><![CDATA[azure]]></category>
		<category><![CDATA[node.js]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=7849</guid>

					<description><![CDATA[既然你的開發環境已經串接成功，接下來就是把 se...]]></description>
										<content:encoded><![CDATA[
<p>既然你的開發環境已經串接成功，接下來就是把 <code>server.js</code> 升級，讓它具備連線到 <strong>Azure SQL Database</strong> 的能力。</p>



<p>在 Azure App Service 上，我們強烈建議使用 <strong>受控識別 (Managed Identity)</strong>。這能讓你的程式碼在沒有帳號密碼的情況下，安全地存取資料庫。</p>



<h3 class="wp-block-heading">1. 安裝必要套件</h3>



<p>在你的專案目錄下執行以下指令，安裝 SQL 驅動程式與 Azure 身分驗證工具：</p>



<p>PowerShell</p>



<pre class="wp-block-code"><code>npm install mssql @azure/identity
</code></pre>



<h3 class="wp-block-heading">2. 更新 <code>server.js</code> 加入 SQL 連線邏輯</h3>



<p>請將 <code>server.js</code> 修改為以下內容。這個版本整合了原本的靜態檔案託管與新的資料庫查詢功能：</p>



<p>JavaScript</p>



<pre class="wp-block-code"><code>import express from 'express';
import path from 'path';
import { fileURLToPath } from 'url';
import sql from 'mssql';
import { DefaultAzureCredential } from '@azure/identity';

const app = express();
const port = process.env.PORT || 8080;

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

app.use(express.static(path.join(__dirname, 'dist')));

// Azure SQL 連線設定
const config = {
    server: process.env.DB_SERVER, // 例如: your-server.database.windows.net
    database: process.env.DB_NAME,
    options: {
        encrypt: true,
        trustServerCertificate: false
    },
    authentication: {
        type: 'azure-active-directory-default', // 使用受控識別
        options: {
            credential: new DefaultAzureCredential()
        }
    }
};

// 建立一個 API 路由來抓取資料庫資料
app.get('/api/sql-data', async (req, res) =&gt; {
    try {
        console.log('正在嘗試連線到 SQL Server...');
        let pool = await sql.connect(config);
        let result = await pool.request().query('SELECT TOP 5 * FROM YourTableName');
        res.json(result.recordset);
    } catch (err) {
        console.error('SQL 錯誤:', err);
        res.status(500).json({ error: '無法讀取資料庫', details: err.message });
    }
});

app.get('/api/config-test', (req, res) =&gt; {
    res.json({
        dbServer: process.env.DB_SERVER || "未設定",
        dbName: process.env.DB_NAME || "未設定"
    });
});

app.get('*', (req, res) =&gt; {
    res.sendFile(path.join(__dirname, 'dist', 'index.html'));
});

app.listen(port, () =&gt; {
    console.log(`Server running at http://localhost:${port}`);
});
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">3. 如何在 Local 測試連線？</h3>



<p>由於 <code>DefaultAzureCredential</code> 非常聰明，它會依照環境切換驗證方式：</p>



<ol start="1" class="wp-block-list">
<li><strong>在 Local</strong>：它會嘗試抓取你電腦上 <code>az login</code> 的身分。請確保你在 PowerShell 執行過 <code>az login</code> 並有權限存取該資料庫。</li>



<li><strong>在 Azure</strong>：它會自動改用 App Service 的「受控識別」。</li>
</ol>



<p>測試時，請先在 PowerShell 設定環境變數：</p>



<p>PowerShell</p>



<pre class="wp-block-code"><code>$env:DB_SERVER="你的伺服器.database.windows.net"
$env:DB_NAME="你的資料庫名稱"
node server.js
</code></pre>



<h3 class="wp-block-heading">4. 重要：資料庫端的權限設定</h3>



<p>即便程式碼寫好了，你還是必須在 Azure SQL Database 裡手動幫你的 App Service (或你的個人帳號) 建立權限：</p>



<p>SQL</p>



<pre class="wp-block-code"><code>-- 在 SQL 查詢編輯器執行
CREATE USER &#91;你的-App-Service-名稱] FROM EXTERNAL PROVIDER;
ALTER ROLE db_datareader ADD MEMBER &#91;你的-App-Service-名稱];
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">5. 最後打包 Docker 的提醒</h3>



<p>因為我們新增了 <code>mssql</code> 與 <code>@azure/identity</code>，請確保你的 <code>Dockerfile</code> 執行階段有包含 <code>RUN npm install --production</code>，或者直接複製整個 <code>node_modules</code>。</p>



<p>你的基礎建設已經全部準備就緒：<strong>前端 Vite + 後端 Node.js + Docker 打包 + SQL 連線邏輯</strong>。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/03/azure-sql-node-js-example/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>[node.js] ReferenceError: require is not defined in ES module scope, you can use import insteadThis file is being treated as an ES module because it has a &#8216;.js&#8217; file extension</title>
		<link>https://stackoverflow.max-everyday.com/2026/03/node-js-referenceerror-require-is-not-defined-in-es-module-scope-you-can-use-import-insteadthis-file-is-being-treated-as-an-es-module-because-it-has-a-js-file-extension/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/03/node-js-referenceerror-require-is-not-defined-in-es-module-scope-you-can-use-import-insteadthis-file-is-being-treated-as-an-es-module-because-it-has-a-js-file-extension/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Mon, 09 Mar 2026 02:42:24 +0000</pubDate>
				<category><![CDATA[node.js筆記]]></category>
		<category><![CDATA[node.js]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=7847</guid>

					<description><![CDATA[這個錯誤是因為你的 package.json 中...]]></description>
										<content:encoded><![CDATA[
<p>這個錯誤是因為你的 <code>package.json</code> 中設定了 <code>"type": "module"</code>，這告訴 Node.js 整個專案都必須使用現代的 <strong>ES Modules (import/export)</strong> 語法，而不支援舊版的 <code>require</code>。</p>



<p>你有兩個快速修復的方法：</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">方法一：將 <code>server.js</code> 改為 ES Modules 語法（推薦）</h3>



<p>這是目前 Node.js 開發的主流做法。請將 <code>server.js</code> 的內容修改如下：</p>



<p>JavaScript</p>



<pre class="wp-block-code"><code>import express from 'express';
import path from 'path';
import { fileURLToPath } from 'url';

const app = express();
const port = process.env.PORT || 8080;

// 在 ES Modules 中需要手動定義 __dirname
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);

app.use(express.static(path.join(__dirname, 'dist')));

app.get('/api/config-test', (req, res) =&gt; {
    res.json({
        message: "環境變數讀取測試",
        dbName: process.env.DB_NAME || "未設定",
        status: "Server is running"
    });
});

app.get('*', (req, res) =&gt; {
    res.sendFile(path.join(__dirname, 'dist', 'index.html'));
});

app.listen(port, () =&gt; {
    console.log(`Node Server 正在執行中：http://localhost:${port}`);
});
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">方法二：更改副檔名為 <code>.cjs</code></h3>



<p>如果你不想改動程式碼語法，最簡單的方式是將 <code>server.js</code> 重新命名為 <strong><code>server.cjs</code></strong>。</p>



<ul class="wp-block-list">
<li>Node.js 會自動將 <code>.cjs</code> 視為 CommonJS 格式，這樣你原本的 <code>require</code> 就能正常運作。</li>



<li>注意：如果你改了檔名，Dockerfile 裡的 <code>CMD ["node", "server.js"]</code> 也要同步改成 <code>CMD ["node", "server.cjs"]</code>。</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">為什麼會發生這個錯誤？</h3>



<p>當 <code>package.json</code> 標記為 <code>"type": "module"</code> 時，Node.js 預期所有的 <code>.js</code> 檔案都遵循 ESM 規範。在 ESM 中，<code>require</code>、<code>__dirname</code>、<code>__filename</code> 這些變數預設是不存在的，必須透過 <code>import</code> 或特定的 API（如 <code>fileURLToPath</code>）來取得。</p>



<h3 class="wp-block-heading">下一步驗證</h3>



<p>修改完畢後，再次執行：</p>



<p>PowerShell</p>



<pre class="wp-block-code"><code>node server.js
</code></pre>



<p>現在伺服器應該能成功啟動並監聽 8080 埠。</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>這是在 Vite 專案中設定 Proxy 的完整步驟。這樣你在開發模式下（<code>npm run dev</code>），前端對 <code>/api</code> 的請求就會被自動轉發到你的 Node.js Server（8080 埠），解決跨網域（CORS）的問題。</p>



<h3 class="wp-block-heading">1. 修改 <code>vite.config.ts</code></h3>



<p>請將你的 <code>vite.config.ts</code> 內容更新如下。這會告訴 Vite：只要看到 <code>/api</code> 開頭的請求，就去問 <code>localhost:8080</code>。</p>



<p>TypeScript</p>



<pre class="wp-block-code"><code>import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'

// https://vite.dev/config/
export default defineConfig({
  plugins: &#91;react()],
  server: {
    proxy: {
      // 匹配所有以 /api 開頭的請求
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        secure: false,
      },
    },
  },
})
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">2. 在前端程式碼中呼叫 API</h3>



<p>現在你可以在你的 React 元件（例如 <code>App.tsx</code> 或 AI Studio 產生的元件）中，直接使用相對路徑呼叫 API。</p>



<p>TypeScript</p>



<pre class="wp-block-code"><code>import { useEffect, useState } from 'react';

function App() {
  const &#91;dbName, setDbName] = useState&lt;string&gt;('連線中...');

  useEffect(() =&gt; {
    // 不需要寫完整的 http://localhost:8080，直接寫 /api 即可
    fetch('/api/config-test')
      .then((res) =&gt; res.json())
      .then((data) =&gt; {
        setDbName(data.dbName);
      })
      .catch((err) =&gt; {
        console.error('API 呼叫錯誤:', err);
        setDbName('呼叫失敗');
      });
  }, &#91;]);

  return (
    &lt;div className="p-4"&gt;
      &lt;h1 className="text-xl font-bold"&gt;系統資訊&lt;/h1&gt;
      &lt;p&gt;目前資料庫環境變數：&lt;span className="text-blue-600"&gt;{dbName}&lt;/span&gt;&lt;/p&gt;
    &lt;/div&gt;
  );
}

export default App;
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">3. 如何啟動開發環境進行驗證</h3>



<p>請開啟<strong>兩個</strong>終端機視窗，分別執行以下指令：</p>



<ul class="wp-block-list">
<li><strong>終端機 A (啟動 API Server)</strong>：PowerShell<code># 確保你在 my-ai-app 目錄下 # 如果要測試環境變數，可以先設定 $env:DB_NAME="My_Local_Test_DB" node server.js</code></li>



<li><strong>終端機 B (啟動 Vite Dev Server)</strong>：PowerShell<code>npm run dev</code></li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">4. 驗證結果</h3>



<ol start="1" class="wp-block-list">
<li>打開瀏覽器前往 Vite 提供的網址（通常是 <code>http://localhost:5173</code>）。</li>



<li>如果你看到頁面上顯示「目前資料庫環境變數：My_Local_Test_DB」，代表 <strong>Proxy 轉發成功</strong>。</li>



<li>如果顯示「未設定」，代表 Server 有抓到請求但沒抓到環境變數。</li>



<li>如果顯示「呼叫失敗」，請檢查 Console 是否有紅字報錯。</li>
</ol>



<h3 class="wp-block-heading">為什麼這對部署到 Azure 有幫助？</h3>



<p>當你最終打包成 Docker 並部署到 Azure 時，<code>server.js</code> 會直接託管靜態檔案（在同一個 Port），那時就不需要 Proxy 了。這種開發模式的設定（Proxy）是為了讓你<strong>在寫程式的階段</strong>能享受到 Vite 的快速熱更新（HMR），同時又能存取後端資料庫。</p>



<p></p>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/03/node-js-referenceerror-require-is-not-defined-in-es-module-scope-you-can-use-import-insteadthis-file-is-being-treated-as-an-es-module-because-it-has-a-js-file-extension/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>node.js 讀取電腦上的環境變數</title>
		<link>https://stackoverflow.max-everyday.com/2026/03/node-js-express-env/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/03/node-js-express-env/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Mon, 09 Mar 2026 02:38:33 +0000</pubDate>
				<category><![CDATA[node.js筆記]]></category>
		<category><![CDATA[node.js]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=7844</guid>

					<description><![CDATA[太棒了，我們現在就把這套流程跑通。這是一個非常實...]]></description>
										<content:encoded><![CDATA[
<p>太棒了，我們現在就把這套流程跑通。這是一個非常實務的作法：先建立一個能讀取環境變數的 Node.js 伺服器，這能確保未來你在 Azure App Service 設定資料庫連線字串（Connection String）時，程式碼不需要重新打包就能直接讀取。</p>



<p>請按照以下步驟完成設定：</p>



<h3 class="wp-block-heading">1. 修改 <code>server.js</code> 加入環境變數測試</h3>



<p>我們在 <code>server.js</code> 中加入一個簡單的 API 路徑，用來確認 Node.js 是否能讀取到系統環境變數。</p>



<p>JavaScript</p>



<pre class="wp-block-code"><code>const express = require('express');
const path = require('path');
const app = express();
// Azure App Service 預設會使用 PORT 環境變數，本機測試建議用 8080
const port = process.env.PORT || 8080;

app.use(express.static(path.join(__dirname, 'dist')));

// 未來這就是你連線 Azure SQL 的地方
app.get('/api/config-test', (req, res) =&gt; {
    res.json({
        message: "環境變數讀取測試",
        // 模擬未來的資料庫名稱，如果沒設定就顯示 '未設定'
        dbName: process.env.DB_NAME || "未設定",
        status: "Server is running"
    });
});

app.get('*', (req, res) =&gt; {
    res.sendFile(path.join(__dirname, 'dist', 'index.html'));
});

app.listen(port, () =&gt; {
    console.log(`Node Server 正在執行中：http://localhost:${port}`);
});
</code></pre>



<h3 class="wp-block-heading">2. 在 Local Docker 驗證環境變數</h3>



<p>打包映像檔（如果你已經改過 Dockerfile，請重新執行 build）：</p>



<p>PowerShell</p>



<pre class="wp-block-code"><code>docker build -t policy-app-node .
</code></pre>



<p>在啟動容器時，我們可以用 <code>-e</code> 參數傳入環境變數，模擬 Azure 的設定：</p>



<p>PowerShell</p>



<pre class="wp-block-code"><code>docker run -d -p 8080:8080 --name my-running-app -e DB_NAME="MyAzureSQL" policy-app-node
</code></pre>



<h3 class="wp-block-heading">3. 如何驗證是否成功？</h3>



<ol start="1" class="wp-block-list">
<li>打開瀏覽器訪問 <code>http://localhost:8080/api/config-test</code>。</li>



<li>你應該會看到 JSON 內容：<code>{"message":"環境變數讀取測試","dbName":"MyAzureSQL","status":"Server is running"}</code>。</li>



<li>如果 <code>dbName</code> 顯示 <code>MyAzureSQL</code>，代表你的 Docker 容器已經具備「從外部讀取設定」的能力。</li>
</ol>



<h3 class="wp-block-heading">4. 未來對接到 Azure SQL 的邏輯</h3>



<p>當你準備好要寫資料庫邏輯時，只需要在 <code>server.js</code> 加入 <code>mssql</code> 套件，並利用 <code>process.env.DB_SERVER</code> 等變數即可。</p>



<p>JavaScript</p>



<pre class="wp-block-code"><code>// 未來的資料庫連線設定範例
const sqlConfig = {
    server: process.env.DB_SERVER, 
    database: process.env.DB_NAME,
    authentication: {
        type: 'azure-active-directory-default' // 這就是我們一開始提到的受控識別
    },
    options: { encrypt: true }
};
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p><strong>下一步建議</strong>：</p>



<p>既然 Local 驗證已經通過（可以看到網頁也能讀到環境變數），你是否想要嘗試將這個 Image 推送到 <strong>Azure Container Registry (ACR)</strong>？這是將容器部署到 Azure App Service 的最後一哩路。</p>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/03/node-js-express-env/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>failed to connect to the docker API</title>
		<link>https://stackoverflow.max-everyday.com/2026/03/failed-to-connect-to-the-docker-api/</link>
					<comments>https://stackoverflow.max-everyday.com/2026/03/failed-to-connect-to-the-docker-api/#respond</comments>
		
		<dc:creator><![CDATA[max-stackoverflow]]></dc:creator>
		<pubDate>Mon, 09 Mar 2026 02:25:05 +0000</pubDate>
				<category><![CDATA[node.js筆記]]></category>
		<category><![CDATA[docker]]></category>
		<category><![CDATA[node.js]]></category>
		<guid isPermaLink="false">https://stackoverflow.max-everyday.com/?p=7839</guid>

					<description><![CDATA[已經寫好 無法執行 Dockerfile, 執行...]]></description>
										<content:encoded><![CDATA[
<p>已經寫好 無法執行 Dockerfile, 執行 docker build 後顯示錯誤訊息:</p>



<pre class="wp-block-preformatted">PS C:\max-new-app\my-ai-app> docker build -t max-new-app .<br>ERROR: failed to connect to the docker API at npipe:////./pipe/dockerDesktopLinuxEngine; check if the path is correct and if the daemon is running: open //./pipe/dockerDesktopLinuxEngine: The system cannot find the file specified.</pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>解法</p>



<p>這個錯誤訊息表示你的 Docker 指令找不到 Docker 引擎，通常是因為 <strong>Docker Desktop 尚未啟動</strong>，或者 <strong>背景服務沒有正常運行</strong>。</p>



<p>請依照以下步驟檢查並排除問題：</p>



<h3 class="wp-block-heading">1. 啟動 Docker Desktop</h3>



<p>最常見的原因是你還沒打開 Docker Desktop。請在 Windows 搜尋列輸入 <strong>Docker Desktop</strong> 並執行它。</p>



<ul class="wp-block-list">
<li>觀察右下角系統匣（System Tray）的小鯨魚圖示。</li>



<li>必須等到小鯨魚圖示停止閃爍，且顯示 <strong>Docker Desktop is running</strong> 綠色狀態，才能執行 <code>docker build</code>。</li>
</ul>



<h3 class="wp-block-heading">2. 檢查 WSL 2 整合設定</h3>



<p>如果你在 PowerShell 執行指令，Docker 需要與 WSL 2 正確連動。請開啟 Docker Desktop 視窗進行檢查：</p>



<ol start="1" class="wp-block-list">
<li>點擊右上角的 <strong>Settings (齒輪圖示)</strong>。</li>



<li>進入 <strong>General</strong>，確認 <strong>Use the WSL 2 based engine</strong> 有被勾選。</li>



<li>進入 <strong>Resources > WSL integration</strong>，確認 <strong>Enable integration with my default WSL distro</strong> 有開啟。</li>
</ol>



<h3 class="wp-block-heading">3. 重啟 Docker 服務</h3>



<p>如果 Docker Desktop 看起來有開但還是報錯，請嘗試重啟：</p>



<ol start="1" class="wp-block-list">
<li>右鍵點擊系統匣的鯨魚圖示。</li>



<li>選擇 <strong>Restart</strong>。</li>



<li>如果還是不行，可以嘗試在管理員權限的 PowerShell 執行以下指令來強制重啟服務：</li>
</ol>



<p>PowerShell</p>



<pre class="wp-block-code"><code>net stop com.docker.service
net start com.docker.service
</code></pre>



<h3 class="wp-block-heading">4. 權限檢查</h3>



<p>有時候權限不足也會導致無法存取 npipe（具名管道）。請試著關閉目前的 PowerShell，改用 <strong>以管理員身分執行</strong> 再次進入目錄執行：</p>



<p>PowerShell</p>



<pre class="wp-block-code"><code>cd C:\max-new-app\my-ai-app
docker build -t max-new-app .
</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 class="wp-block-heading">成功執行後的下一步</h3>



<p>當 <code>docker build</code> 成功後，你會看到一系列的 <code>Step 1/X</code> 跑完。接著請執行：</p>



<p>PowerShell</p>



<pre class="wp-block-code"><code># 啟動並對應埠號
docker run -d -p 8080:80 --name policy-web max-new-app
</code></pre>



<p>然後在瀏覽器輸入 <code>http://localhost:8080</code> 即可驗證。</p>



<p></p>
]]></content:encoded>
					
					<wfw:commentRss>https://stackoverflow.max-everyday.com/2026/03/failed-to-connect-to-the-docker-api/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
	</channel>
</rss>
