比較 Svelte 渲染元件的三個方法:mount vs render vs hydrate
Svelte Version: 5.25.7
Outline
render()
:在 Server 端上,將從 Component 產生 HTML。mount()
:在 Client 端上,從空的 DOM 節點建立 Component 的 DOM 附加到 HTML。hydrate()
:混合render()
與mount()
。功能上類似mount()
,但不是從空的 DOM 節點開始,而是從 Server 端render()
過的資料開始建立。
使用時機
如果需要在 server 端渲染 e.g. SEO 的需求,可以使用 render()
,Server 端會將完整的 HTML 輸出至 Client 端。而 mount()
完全相反,僅在 Client 端時建立 DOM 節點。而 hydrate()
同時兼顧了 Server 端 SEO 的需求與 Client 端與使用者互動的需求,
render:Server 端渲染
當您在 Server 端上使用 render()
功能時,Component 將被編譯為 HTML 字串,可以將其傳送到 Client 端:
1 | // Server-side code (e.g., in a Node server) |
App 為 Svelte 的 Componenet,props 則是傳入 App 的屬性參數。
mount:Client 端渲染
如果你有一個空 DOM 節點或是即沒有預先渲染的 HTML e.g. <div id="app"></div>
並且希望 Svelte 在 Client 端上從頭建立 DOM,可以使用 mount()
:
1 | import { mount } from 'svelte'; |
hydrate:升級伺服器渲染的 HTML
假設以下程式碼已在 Server 端產生的 HTML 並傳送到 Client 端:
1 | <div id="app">something</div> |
我們可以在 Client 端上使用 hydrate()
來讓 Server 端渲染的物件具有互動性 (interactive)。
1 | import { hydrate } from 'svelte'; |
可以想像成 Server 端已經渲染好 HTML,用 hydrate()
附加額外邏輯和事件處理來「提升」此靜態 HTML 的功能:
Reference
React vs Svelte 開發體驗:子元件暫時更新父層資料
當程式規模慢慢變大時,為了管理與維護會將大元件拆分成許多小元件,在父子元件之間傳遞資料是開發時很常見的。但在傳遞資料上,React 跟 Svelte 沒什麼區別,同樣都是將資料傳入元件的 attribute 中,子元件透過 props 來獲得父層傳入的資料。所以我們這章舉一個比較複雜一點的例子:讓子元件可以暫時更新資料,但如果父元件的資料更新時,子元件也要更新為父元件元件的值。我們將透過範例比較兩者在開發體驗上的差異。
開發體驗
開發者在使用工具、框架、語言、API、系統或流程時,整體的感受、效率、流暢度與滿意度。
使用情境
當你在開發表單、步驟流程或是可編輯區塊時,子元件常會需要「暫時修改」父元件提供的資料。例如使用者輸入值,但尚未儲存前,這些變更應該是本地的。這時候就需要讓子元件在不直接修改父層狀態的前提下,保有一定程度的控制權。
React:需要手動管理狀態同步
React 採用單向資料流設計,而且我們無法直接修改 props
。在子元件想要暫時更新資料時,必須透過 useState
加上 useEffect
來同步 props
的變化 (demo)。
ParentComponent.js
1 | import React, { useState } from 'react'; |
ChildComponent.js
1 | import React, { useState, useEffect } from 'react'; |
在這個範例中:
- 子元件透過
useState
創建一個本地的localCount
,用來「暫時更新」畫面上的計數值。 - 當父元件的
count
改變時,useEffect
會同步更新localCount
。 - 子元件的操作不會影響父元件的實際資料。
Svelte:直覺操作、語法簡潔
Svelte 允許我們在子元件中直接修改 props,而不需要額外建立本地狀態。這種語法大幅簡化了開發流程,對開發者非常友善 (demo)。
Parent.svelte
1 | <script> |
Child.svelte
1 | <script> |
- 無需
useEffect
或本地狀態。 - 可直接操作傳入的
count
,操作簡單明瞭。 - 預設變更僅限於子元件本地,不會同步到父層(除非你明確綁定
bind:
或使用事件)。
以上 React 與 Svelte 兩個例子功能相同,但 Svelte 的語法設計,讓開發者可以更快速專注在邏輯本身,而非額外的狀態管理機制。
延伸閱讀
React vs Svelte 開發體驗:雙向資料綁定
資料綁定 (Data Binding) 是連結應用程式資料 (Application) 與使用者介面 (UI) 的方式或機制。React 是使用單向資料綁定 (one-way data flow),需手動呼叫 setState
來更新狀態;而 Svelte 使用 bind:
指令來達成雙向資料綁定。我們將透過範例來了解兩者在開發體驗上的差異。
開發體驗
開發者在使用工具、框架、語言、API、系統或流程時,整體的感受、效率、流暢度與滿意度。
使用情境
當你在開發表單、表單輸入與使用者互動或即時資料顯示,需要反向更新狀態變數,並即時顯示給使用者 (e.g. 動態價格計算、表單即時驗證、即時預覽)。
React:單向資料綁定
React 採用單向資料流 (one-way data flow),透過手動呼叫 setstate
來更新狀態。
1 | import { useState } from 'react'; |
useState
宣告狀態變數text
,初始值為空字串。value={text}
綁定輸入框顯示值。onChange
在輸入變動時更新text
。
Svelte:直覺式雙向綁定
Svelte 提供了 bind:
指令,簡化雙向資料綁定,讓變數與元件保持同步。
1 | <script> |
- 使用
let
宣告響應式 (reactivity) 變數text
。 bind:value={text}
自動雙向綁定輸入值與變數。- 使用者輸入會更新
text
,變數更新也會同步回輸入框。
哲學
React 與 Svelte 在資料綁定上的差異,反映了各自的框架哲學:
- React 注重控制與可預測性,適合處理複雜應用邏輯與資料流。
- Svelte 注重簡潔與開發效率,讓使用者開發時更貼近原生思維。
延伸閱讀
React vs Svelte 開發體驗:狀態管理
在管理資料狀態上,React 採用的是 useState
Hook,而 Svelte 則是直接使用變數,使用起來更直觀。我們將透過範例來了解兩者在開發體驗上的差異。
開發體驗
開發者在使用工具、框架、語言、API、系統或流程時,整體的感受、效率、流暢度與滿意度。
React 狀態管理:使用 useState
Hook
React 讓元件可以擁有內部狀態的方式是透過 useState
Hook 來完成。
1 | import { useState } from 'react'; |
useState(0)
:初始化count
為 0。setCount
:更新count
的函式,每次按鈕點擊讓值加 1。- 透過 React 的 Virtual DOM,變化的值會觸發元件重新渲染。
非同步更新注意事項
React 的狀態更新是非同步的,因此如果在同一函式中連續呼叫多次 setCount(count + 1)
,可能無法即時反映最新的狀態。建議改用以下方式來避免此問題:
1 | setCount(prevCount => prevCount + 1); |
這樣可以保證邏輯永遠是基於最新狀態更新。
Svelte 狀態管理:響應式變數
Svelte 採用一種更直覺的狀態處理方式。它不需要額外的 Hook,直接使用變數即可自動觸發畫面更新。
1 | <script> |
- 使用
let count = 0
宣告變數。 - 直接對
count
進行操作,例如count++
,Svelte 就能自動感知變化並更新 DOM。 - 這是透過 Svelte 的 編譯時響應式系統 (reactivity system) 實現的,完全不需要 Virtual DOM。
Note: React.js 使用 Virtual DOM 來追蹤是否元件需要被更新;而 Svelte 沒有使用 Virtual DOM 而是直些更新在 DOM 上。
React vs Svelte 狀態更新差異
功能面向 | React | Svelte |
---|---|---|
語法複雜度 | 使用 Hook,語法稍多 | 直覺操作變數,語法簡潔 |
DOM 更新機制 | Virtual DOM 比對再更新 | 編譯階段生成 DOM 操作 |
狀態更新方式 | 必須使用 setState / setCount | 直接修改變數 |
更新非同步處理 | 是,需注意資料一致性 | 否,變數改變即更新 |
可讀性與維護性 | 中等 | 高 |
如果追求極簡語法與優化效能,Svelte 提供了非常流暢的開發體驗。而 React 則擁有強大的生態系與社群支持。
延伸閱讀
使用 Node.js 監控資料夾變更,並以 Event Emitter 封裝 File Watcher
Node.js 監控資料夾
在 Node.js 中,我們可以透過 fs.watch
來監控資料夾是否有異動,不需要寫太多程式碼。以下是一個基本範例:
1 | const fs = require('fs'); |
這段程式會持續監控 ./watched
資料夾的內容變化,當有新增、修改或刪除檔案時,就會觸發 callback 函式。
根據副檔名處理不同邏輯
如果我們想要針對不同的副檔名做不同處理,可以進一步使用 path.extname()
判斷檔案類型:
1 | const fs = require('fs'); |
不過隨著專案變大,副檔名的判斷邏輯(if-else 或 switch)會越來越複雜、難以維護。這時就可以考慮封裝成 Event Driven 的架構,提升可讀性與擴充性。
將邏輯封裝成 Pub/Sub 模型的 Watcher
Node.js 提供的 EventEmitter 非常適合實作發布/訂閱(Publish/Subscribe)模型。EventEmitter 詳細介紹可以參考這篇文章:
使用 Node.js 的 EventEmitter 建立事件監聽機制:概念 & 實作當 fs.watch
偵測到檔案更動,我們使用 .emit(extension)
來「發布」這個事件,而訂閱該副檔名的使用者透過 .on(extension, callback)
就能被「通知」。這讓我們可以針對特定副檔名建立模組化的監聽邏輯,使架構更清晰、易於擴充。
我們利用 class 繼承 EventEmitter,在 constructor 參數傳入資料夾,並呼叫 start()
時開始監控:
1 | // watcher.js |
這段程式碼會根據檔案副檔名觸發對應的事件,達成根據檔案類型處理相對應的 callback。我們建立一個 watcher 並註冊感興趣的副檔名,並呼叫 start()
開始監控的資料夾:
1 | // index.js |
這樣的寫法使得每個副檔名的處理邏輯可以被模組化,也可以讓多個檔案處理者獨立訂閱自己關心的事件。
Conclusion
透過封裝 fs.watch
並結合 EventEmitter 的事件機制,我們實作了一個簡單且可擴充的資料夾監控系統。這個模式讓你可以專注在各類檔案的邏輯處理,而不需要在程式碼中寫太複雜的 if-else
部分。這樣的架構能讓你在不干擾其他邏輯的情況下,自由地新增更多檔案類型處理器,提升模組化與維護性。
Reference
使用 Node.js 的 EventEmitter 建立事件監聽機制:概念 & 實作
概念
在 Node.js 中,建立 EventListener 可以使用 EventEmitter,概念與 publish/subscribe 非常相似,如果之前曾經使用過 Redis 的 Pub/Sub 機制,會更容易理解。
假設有一個人先訂閱(subscribe)了一個名為 ‘xxx’ 的頻道(channel),之後當有人發布(publish)訊息到 ‘xxx’ 頻道時,訂閱者就會看到發布者發出的訊息。
Event Listener
回到 Node.js 的 Event Listener 機制,我們可以透過 EventEmitter 建立一個 publish/subscribe 物件。執行物件的 .on 方法就類似於訂閱(subscribe),而 .emit 方法則類似於發布(publish)。以下是範例程式碼:
1 | const EventEmitter = require('events'); |
在這個範例中,我們使用 EventEmitter 建立了一個 channel 並且監聽 ‘join’ 事件。當有使用者加入時,會透過 .emit(‘join’) 通知 ‘join’ 事件。我們可以建立一個 HTTP server 並稍微修改一下來驗證我們的想法:
1 | // server.js |
.emit
不僅用來觸發事件,還可以同時傳入多個參,當你呼叫 .emit('join', arg1, arg2, ...)
時,所有註冊了這個事件的監聽器 .on('join', (arg1, arg2) => {...})
都會依序被呼叫,且能接收到這些參數。
net.createServer
是使用 Node.js 的 net module 來建立一個 TCP server。當有 client 進來時會執行 callback function,這個 callback 第一個參數代表接收到一個 net.Socket
的物件。
在 socket 的 callback 中,可以透過 client.remoteAddress
與 client.remotePort
來獲取連線 client 的 IP 與 port,組合成唯一個 id
。然後使用 .emit('join', id, client)
觸發一個 join
事件,同時把這個 id
與 client
傳入給 join 事件的 callback,最後儲存在 channel.clients
中。
我們來測試一下,先開一個 server,然後使用 telnet localhost 30000
來連到 server,在 server 上可以看到 xxx join
的訊息。
1 | 先開一個 server |
Error Event
在前面的例子,這個 join
事件名稱是可以自定義的,可以是任何字串,所以如果觸發一個不存在的事件 .emit('notExist')
,其實是不會發生什麼事情的,但是有一個例外:error
事件。如果沒有人註冊 error
事件,但有人在 error
事件中發出訊息,程式會直接停止運行並印出錯誤訊息。
1 | const events = require('events'); |
如果在執行 error
callback 時又發生錯誤,則會丟出 uncaughtException
錯誤,可以透過 process.on('uncaughtException')
來捕捉這個錯誤。
1 | process.on('uncaughtException', err => { |
上述的例子可以增加這兩個事件:
1 | process.on('uncaughtException', err => { |
廣播給所有使用者
現在 server 建立好了,我們想要讓多個使用者加入,如果其中有一個 client 發話,其他 client 都可以收到訊息。
1 | const net = require('net'); |
在 join
事件中,當有 client 加入時,會針對每個 client 註冊 broadcast
事件。當有人發送廣播訊息時 channel.emit('broadcast', ...)
,就會觸發 callback 事件。這個 callback 需要接收兩個參數:
senderId
:發送者的 id。message
:廣播的訊息。
如果該 client 不是訊息的發送者,則會使用 channel.clients[id].write(message)
將訊息送給這個 client。
最後每當 client 連線到 server 並傳送資料時,在 data
事件中 client.on('data', callback)
,這個 callback 就會被觸發。這個 callback 會透過我們前面新增的 broadcast
事件來廣播給所有人。
在 .on('data', callback)
收到的資料通常是 Buffer
,所以會先轉成 string。
我們來測試一下:
1 | # server |
先開一個 server,然後分別加入使用者並發話,記得最後要按 enter 才會發送,就可以看到發送的訊息會廣播到其他連線的 client。
如果你持續增加使用者,超過 10 個人訂閱同一個事件時,Node.js 會印出警告你,但可以透過 .setMaxListeners(50)
來增加上限。
1 | eventEmitter.setMaxListeners(50); |
離開群組 removeListener
現在我們的使用者可以加入群組,並且可以廣播訊息給所有人,看起來很不錯,但是使用者加入後卻沒有辦法離開。我們希望使用者輸入 leave 的時候,會將自己從群組中移除,並且斷開連線。
1 | channel.on('leave', (id) => { |
看起來功能已經完成了,但是有一個小問題。在 join
事件中,使用者訂閱的 broadcast
事件沒有清除。如果有人離開之後,就會在後續的廣播嘗試寫入不存在的連線,這會造成 Node.js 的錯誤。
因此我們在 leave
時需要用 .removeListener
來取消訂閱,注意 removeListener
帶入的 callback 必須跟 on
的 callback 一樣,
1 | // removeListener 帶入的 callback 需要跟 on 的 callback 一樣 |
所以我們需要修改一下 server.js
:
- 新增
channel.subscriptions
,並在join
事件中,每當有使用者加入時儲存每個 client 的 callback。 - 在
leave
事件中removeListener
將對應使用者的 broadcast callback 移除。
1 | const net = require('net'); |
在 Terminal 測試使用者能否正常離開群組:
1 | $ node server.js |
關閉伺服器 removeAllListeners
現在我們新增一個新功能,輸入 shutdown
指令讓所有人取消訂閱,並斷開所有連線。在前一章節所學的 removeListener
,我們可以使用迴圈將每個使用者從 broadcast
事件移除。然而,我們可以更間單的使用 .removeAllListeners('broadcast')
,來一次性移除所有訂閱 broadcast
事件上的使用者。最後我們的完整程式碼如下:
1 | const net = require('net'); |
測試一下,輸入 shutdown
後所有人將會斷開連線:
1 | # server |
只要 Event Loop 中還有待處理的非同步操作(例如未完成的 I/O、定時器、網絡請求等),Node.js process 就不會自動退出。這對於需要長期運行、持續監聽請求的 Web 伺服器來說是理想的行為;而對於命令行工具或一次性任務,則可能需要在所有非同步操作完成後主動退出。
Reference
Node.js Event Loop 是如何運作的
Node.js 是 Single Thread 的環境執行,是如何透過 Event Loop 的方式來實現 Non-blocking I/O 的操作。
Event Loop 運作方式
想像一個 HTTP Request 進入 Node.js http.Server 時,Event Loop 會觸發對應的 callback。假設該 callback 需要從資料庫拿到使用者資料,並從磁碟讀取電子郵件 template,然後將使用者資料填入 template 後,回傳 HTTP Response。
在這個過程中,Event Loop 會以先進先出 (FIFO) 的方式運行 Event Queue:
- 首先,Event Loop 包含一個 Timer,可以透過 setTimeout 或 setInterval 設定。
- 接下來,如果 Timeout 或是有新的 I/O Event 進來,則會進入 poll phase (輪詢階段),在此階段會安排執行 (schedule) 對應的 callback。
Event Loop 的概念不會太複雜,有 Event 進來,執行相對應的 callback。
那如果正在執行 poll phase 時,又有新的 I/O Event,Node.js 則會透過 setImmediate 設定,在當前 callback 執行完後立刻執行新進來的 I/O Event callback。
Library
Node.js 的 Non-blocking I/O Opertaion 是透過 libuv 實現的,libuv 實現 Node.js 的 Event Loop、Non-blocking I/O、網路、磁碟、檔案系統等功能,位於下圖的左下角:
原先在 Node.js 程式碼是透過 C++ Binding 綁定到 libuv 或其他 library 上,這些 library 會去跟 OS 來溝通。
之後 Google 的 V8 JavaScript 引擎的出現,讓 Node 更加強大。V8 引擎厲害之處就是直接繞過 C++ binding 變成 machine code,這大幅的提升了執行效率,如上圖右半側。
Reference
安裝 Windows 11 虛擬機過程與問題
很久沒安裝 Windows 11 VM 了,原本想說應該很順利,結果裝了一整個晚上,沒想到會踩了一堆雷,所以這篇來紀錄一下安裝 Windows 11 VM 的過程:
讀取 ISO
在 Windows 11 開機的時候,若出現以下提示文字:
Press any key to boot from CD or DVD…
此時,一定要按下鍵盤上的任意鍵,如果只用滑鼠輸入會失敗。
繞過 TPM 檢查
在安裝 Windows 11 時,系統會檢查硬體是否符合規格。如果是在虛擬機上安裝,得需要手動關閉 TPM 的檢查:
- 啟動命令提示字元:在安裝畫面按下
Shift + F10
,這將開啟命令提示字元 (cmd)。 - 開啟登錄編輯器:在命令提示字元中輸入
regedit
並按下 Enter 鍵。 - 新增 LabConfig 機碼:
- 找到
HKEY_LOCAL_MACHINE\SYSTEM\Setup
。 - 在此路徑下新增一個名為
LabConfig
的資料夾(機碼)。
- 繞過檢查的設定:在
HKEY_LOCAL_MACHINE\SYSTEM\Setup\LabConfig
機碼內新增以下三個 DWORD 32-bit 項目
BypassTPMCheck
,值設為 1。BypassRAMCheck
,值設為 1。BypassSecureBootCheck
,值設為 1。
完成這些設定後,關閉登錄編輯器,繼續安裝,即可成功繞過檢查。
帳號登入
在安裝過程的最後階段,Windows 可能會要求輸入帳號密碼,目前還不知道要略過此步驟。
建議
安裝完成後,建議馬上建立一個快照 (snapshot),這樣如果日後需要重置或修復系統,就不必重新安裝。
Reference
何時該使用 React Redux
今天這章會討論什麼時候要把資料放在 Redux 裡面,什麼時候要將資料放在 React 的 components 裡。
React 將 components 分成 Presentational Components (展示組件) 與 Container Components (容器組件)。
Presentational Components
Presentational Components 主要負責 UI 的部分,通常不會有複雜 application 狀態管理的資料,通過會由 props 傳入無狀態 (stateless) 的資料,像是:
- 跟 UI 相關的資料 e.g. 顏色的使用
- 只有在必要時才擁有自己的狀態 e.g. 下拉式選單的狀態
- 需要手動建立新的東西 e.g. new post 的資料儲存
Container Components
Container Components 主要負責 application 的狀態,通常會使用 Redux 來管理,然後在 render 的時候會資料傳給 Presentational Components,通常會儲存有狀態的資料:
- application data flow
- 使用者相關的資訊 e.g. 最喜歡的顏色
Compare
特性 | Presentational | Container |
---|---|---|
主要用途 | UI render | application state |
狀態 | 無狀態或是透過 props 獲得狀態 | 通常有狀態,透過 Redux 管理 |
類別 | smart | dumb |
儲存類型 | 儲存複雜的東西 | 儲存簡單的東西 |
Reference
React Redux Example App 範例程式碼
前一篇介紹 Redux 是參考 Flux 的架構而設計的,這篇要用 Redux 來寫一個簡單的 React Counter App。
在 Ubuntu 18.04 上安裝 Docker CE再提醒一下,React 跟 Redux 的差別是:React 是一個 front-end library;Redux 是一個架構,可以不用跟 React 一起使用,也可以跟 Vue 或 Angular 搭配。
Counter App
Install
建立一個 React App 然後安裝 Redux:
1 | npx create-react-app counter-app |
Actions
建立 Redux 的 Action,Action 一定要回傳 type 讓 Reducer 去做對應的資料更新,Middleware 會在送到 Reducer 之前執行。
1 | // actions.js |
Reducer
建立 Redux 的 Reducer,Reducer 不能修改原本的 state,必須複製一份,並且回傳最後 state 應該要的資料。
1 | // reducer.js |
Store
建立 Redux 的 Store,用來儲存 state 的資料
1 | // store.js |
Counter
建立 React Counter Element,而且注意這邊需要將 Action 傳入 Dispatcher 來讓後續的 Reducer 來更新狀態。
1 | // Counter.js |
App
最後 react-redux library 透過 Provider Component 來跟 React 串接,Provider 會提供 Store 的資料。
1 | // App.js |