核心做法

1function setupSearchShell(shell) {
2  var ui     = shell.querySelector('.pagefind-ui');
3  var input  = shell.querySelector('.pagefind-ui__search-input');
4  var drawer = shell.querySelector('.pagefind-ui__drawer');
5  // ... 所有 query 從參數 shell 開始
6}
7
8document.querySelectorAll('.search-shell').forEach(setupSearchShell);

元件根不在函式內 query、由呼叫者傳入。函式支援任意數量的元件實例。


這個做法存在的價值

兩件事:

  1. 多實例支援免費forEach(setup) 自動處理多個 shell
  2. 純函式特性:函式行為只依賴參數、不依賴外部狀態 — 可單獨測試、可重用、副作用集中

元件根變數的關鍵差異:那個 pattern 假設「shell 唯一」、本 pattern 把這個假設外移到呼叫端、函式本身不假設。


適合的情境

情境為什麼合理
同頁面有多個元件實例(多語切換、相關搜尋)forEach 自動覆蓋全部
元件設計成可被重用到其他頁面沒有 hardcoded 依賴、容易移植
寫成函式庫 / 第三方 component使用者可以對任意根節點呼叫
想單元測試函式行為傳入 mock root 即可測試

核心特徵:把「shell 從哪來」這個責任明確交給呼叫端、函式自己不關心。


不適合的情境

情境為什麼過度工程改用
確定全站只有一個元件實例每函式多一個參數、收益不明顯元件根變數
元件動態增減、生命週期不可預測forEach 只跑一次、無法捕捉後加的元件closest 反向找根
一次性探索程式碼純函式設計成本不值得document query

設計細節

函式簽名的設計

 1// 好:shell 是必填參數
 2function setupSearchShell(shell) { ... }
 3
 4// 較差:依賴外部變數
 5var shell;  // module scope
 6function setupSearchShell() {
 7  // 用了外部 shell
 8}
 9
10// 更差:mega object
11function setupSearchShell(allElements) {
12  var shell = allElements.shell;  // 不知道實際依賴什麼
13  // ...
14}

明確參數 = 明確依賴 = 容易測試、容易讀。

內部子函式也接受 shell

 1function setupSearchShell(shell) {
 2  syncScopeHeight(shell);
 3  setupFilterSlot(shell);
 4  setupScopeFilter(shell);
 5}
 6
 7function syncScopeHeight(shell) {
 8  var scope = shell.querySelector('.search-scope');
 9  // ...
10}

每層都明確接受 shell — 不依賴外層 closure。整套函式族都是純函式。

預先抓子節點 vs 每次重 query

 1// 方式 A:函式入口抓所有子節點
 2function setupSearchShell(shell) {
 3  var els = {
 4    ui:     shell.querySelector('.pagefind-ui'),
 5    input:  shell.querySelector('.pagefind-ui__search-input'),
 6    drawer: shell.querySelector('.pagefind-ui__drawer'),
 7  };
 8  // 後續用 els.ui / els.input / els.drawer
 9}
10
11// 方式 B:各子函式自己 query
12function setupSearchShell(shell) {
13  syncScopeHeight(shell);  // 內部自己 querySelector
14  setupFilterSlot(shell);
15}

A 比較有效率(只 query 一次)、B 比較解耦(子函式自包含)。選 B 為預設、效能瓶頸時才考慮 A


跟其他起點做法的關係

#14 Selector 精準度 的「起點」維度有四種做法:

做法比較
document query比本卡片簡潔、無多實例支援
元件根變數比本卡片少一個參數、無多實例支援
本卡片:起點當參數多實例支援、純函式、設計成本前移
closest 反向找根比本卡片更動態、不依賴 forEach 時機

升級階梯:document → 元件根變數 → 起點當參數 → closest。複雜度遞增、能處理的情境也遞增。


應用範例:多實例 setup

1// 頁面有 N 個 search-shell(例如多語版面切換)
2document.querySelectorAll('.search-shell').forEach(setupSearchShell);
3
4// 跑完之後:每個 shell 各自獨立 setup、互不干擾

當前頁只一個 shell、上面這行也適用 —forEach 對 1 個元素跑一次、跟 hardcode 單例沒差。做了多實例設計、未來不需要重寫


應用範例:單元測試

純函式可以對 mock DOM 測試:

1test('setupSearchShell 把 filter 移到 sidebar', function () {
2  var shell = createMockShell();  // 建立測試用 DOM
3  setupSearchShell(shell);
4
5  expect(shell.querySelector('.search-filter-slot').children.length).toBe(1);
6});

不需要全頁面 mount、只需要 mock 一個 shell — 測試成本低。


判讀徵兆

訊號該套用本 pattern 嗎?
同頁要支援多個元件實例是 — 直接的好處
想對函式寫單元測試是 — 純函式才好測
函式內讀 module scope 變數是 — 改成參數讓依賴顯式
確定永遠只一個實例、且不寫測試否 — 元件根變數 已夠
元件實例 runtime 動態增減否 — 升級到 closest

核心原則:本 pattern 把「我從哪取得 shell」的答案從函式內搬到呼叫端 — 換到「函式可重用」+「測試容易」+「多實例免費」三個收益、代價是函式簽名多一個參數。當前情境只一個實例也適用、未來擴展不需重寫。