Docker 中的瀏覽器平台、圖形與 Playwright

最後更新: 2022-08-21 22:50:08 +0800 +0800
原文以英文撰寫。您正在閱讀AI翻譯版本。

當測試依據文件載入、版面配置、網路與算繪建立模型,而非將頁面視為靜態字串時,瀏覽器自動化才能可靠執行。

測量瀏覽器與 DOM #

不同的瀏覽器尺寸回答不同的問題:

API測量物件
window.innerWidth and innerHeight版面配置檢視區,包括已算繪的捲軸
window.outerWidth and outerHeight瀏覽器視窗邊界
screen.width and screen.height以 CSS 像素表示的螢幕尺寸
screen.availWidth and availHeight排除作業系統保留區域後的螢幕空間
element.clientWidth and clientHeight內容框加內邊距,不包括邊框和捲軸
element.scrollWidth and scrollHeight可捲動內容的範圍
element.offsetWidth and offsetHeight邊框盒尺寸,四捨五入為整數像素
element.getBoundingClientRect()檢視區座標中的邊框盒幾何資訊,保留小數

對於沒有版面配置盒的行內元素,clientWidth 為零。在根元素上,它描述不含傳統捲軸的版面配置檢視區。除非加上捲動偏移量,否則不要混用 getBoundingClientRect() 傳回的檢視區座標與文件座標。

使用有效的 HTML 結構與資源提示 #

<link> 表示與外部資源的關係。其 rel 屬性是一組以空格分隔的標準連結型別:

html
<link rel="stylesheet" href="/site.css">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="icon" href="/favicon.svg" type="image/svg+xml">

preconnect 會預先建立傳輸連線;只應將其用於頁面將要連線的來源站。它不會下載樣式表、字型或指令碼。

<meta> 提供字元集、檢視區、描述和 HTTP 等效指令等文件中繼資料。應將 <meta charset="utf-8"> 放在 head 前段,讓解析器正確解碼後續位元組。

<noscript> 在指令碼停用或不受支援時提供備用內容。其解析行為取決於它出現在 head 還是 body 中,因此應使用 HTML 驗證器與瀏覽器測試產生的標記。

<p> 元素不能包含另一個段落。HTML 解析器會在內層段落之前關閉外層段落,使 DOM 與原始文字不同。範本程式碼應改為建構同層元素或語意容器:

html
<div class="notice">
  <p>First paragraph.</p>
  <p>Second paragraph.</p>
</div>

建模指令碼載入與模組依賴 #

不帶 async 或 defer 的傳統指令碼會在瀏覽器取得並執行指令碼期間阻塞 HTML 解析。因此,指令碼位置決定哪些節點與全域變數可用:

html
<script src="/jquery.js"></script>
<script src="/plugin-that-needs-jquery.js"></script>

該順序不可省略,因為第二個指令碼會使用第一個指令碼建立的全域變數。將相依指令碼移到其提供者之前會引發參考錯誤。

形式是否阻塞解析器執行順序DOM 可用性
傳統指令碼是文件順序標籤之前已解析的節點
defer擷取不阻塞解析後按文件順序完整的已解析文件
async擷取不阻塞先完成者先執行不與解析完成時機繫結
type="module"擷取不阻塞模組相依順序預設延遲執行

defer 適合有順序要求的應用指令碼。async 適合分析指令碼等獨立指令碼,這些指令碼不能依賴文件位置或其他指令碼。模組預設延遲執行,會解析 import 依賴,擁有自己的作用域,並且每個模組 URL 只執行一次。當傳統程式碼必須在解析後執行時,使用 DOMContentLoaded;僅當影像、樣式表和子框架也相關時才使用 load。

動態附加的指令碼預設非同步執行。只有當載入器刻意依序載入傳統指令碼時,才應在插入前設定 script.async = false。只要部署限制允許,應優先使用原生 ES 模組與明確的 import 相依關係,而不是自訂載入器。

根據限制設計回應式 CSS #

CSS 盒模型包括內容、內距、邊框和外距。box-sizing 決定宣告的寬度與高度是否包含內距與邊框:

css
*, *::before, *::after {
  box-sizing: border-box;
}

使用 border-box 時,width: 20rem 是邊框盒寬度。使用初始值 content-box 時,內距和邊框會增加算繪寬度。通用的 border-box 規則使元件尺寸得以組合。

Flexbox 對一維序列進行版面配置,Grid 則明確配置列與欄。工具列或線性內容流應使用 flex;項目需要沿兩個軸對齊時則應使用 grid:

css
.cards {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(min(18rem, 100%), 1fr));
  gap: 1rem;
}

minmax 運算式在維持欄寬可讀性的同時,允許狹窄檢視區使用單欄。gap 屬於版面配置容器,因而不會產生外距重疊或首尾子元素的例外處理。

媒體查詢根據環境特性選擇規則:

css
@media (width >= 48rem) {
  .sidebar-layout {
    grid-template-columns: minmax(16rem, 1fr) minmax(0, 3fr);
  }
}

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: 1ms; transition-duration: 1ms; }
}

當檢視區改變設計的可用空間時,使用檢視區中斷點。如果可重用元件可能出現在同一頁面中寬度不同的區域,容器查詢更合適。

CSS 長度採用不同的參考框。rem 參照根字型大小,em 參照目前字型大小,百分比取決於屬性,而 vw 或 vh 參照檢視區尺寸。當內容需要非拉丁字元時,應在字型堆疊中保留相應字形的涵蓋範圍:

css
.cjk-sample::before {
  content: "漢";
  font-family: "Noto Sans CJK SC", sans-serif;
}

該字形用於測試字型涵蓋範圍,並非文章內容。

尊重使用者的顏色偏好 #

先宣告支援兩種配色方案,再定義語義顏色變數:

css
:root {
  color-scheme: light dark;
  --surface: Canvas;
  --text: CanvasText;
  --accent: #2867c8;
}

@media (prefers-color-scheme: dark) {
  :root { --accent: #8ab4f8; }
}

prefers-color-scheme 反映作業系統或使用者代理程式的偏好。由應用程式控制的主題選擇器可以透過在根元素上設定屬性來覆寫變數,但應在已儲存偏好、使用者明確選擇與系統偏好之間維持清楚的優先順序規則。

嵌入的 SVG 或 iframe 是獨立文件。父文件的 CSS 自訂屬性不會跨越該邊界;嵌入資源需要自己的樣式,或經過明確設計的訊息傳遞與主題機制。

使用可預測的備用方案載入字型 #

一個字型家族包含標準、斜體、粗體與粗斜體等相關字體。字形是字元的可見表示。瀏覽器會依序查找 font-family,逐字元選擇字型:

css
body {
  font-family: "Example Sans", system-ui, sans-serif;
}

當目前字型缺少某個字形時,可以從後續字型家族取得備用字形。名稱中含空格的字型家族必須加上引號。

@font-face 將字型家族名稱與可下載資源關聯:

css
@font-face {
  font-family: "Example Sans";
  src: url("/fonts/example-sans.woff2") format("woff2");
  font-style: normal;
  font-weight: 400 700;
  font-display: swap;
}

WOFF2 是用於 OpenType 字型資料的壓縮 Web 字型容器。可變字型可以公開 wght、wdth、opsz 和 ital 等軸;宣告範圍應限制為網站實際提供的字體。font-display: swap 在 Web 字型載入期間顯示備用文字,因此應選擇字寬相容的備用字型,或使用字型度量覆寫來減少版面偏移。

window.queryLocalFonts() 需要權限,且瀏覽器支援有限。不要讓網站算繪依賴於列舉本機字型。

正確縮放 Canvas #

width 和 height HTML 屬性以裝置畫素設定 Canvas 繪圖緩衝區。CSS width 和 height 設定其顯示框。如果一個 400×800 的繪圖緩衝區以不同的 CSS 尺寸顯示,瀏覽器會縮放該點陣圖。

若要在高像素密度下清晰算繪,應根據 CSS 框與裝置像素比設定後端儲存區大小:

js
const rect = canvas.getBoundingClientRect();
const scale = window.devicePixelRatio;
canvas.width = Math.round(rect.width * scale);
canvas.height = Math.round(rect.height * scale);

const context = canvas.getContext("2d");
context.setTransform(scale, 0, 0, scale, 0, 0);

調整大小後,應在繪製前重設變換。如果大型 Canvas 的記憶體用量可能過高,則應限制縮放比例。

追蹤 CSS 規範與圖示 #

CSS 是一組模組,而不是單一的整體版本。CSS 快照描述某一時間點的一組成熟模組;隨後各模組獨立演進。應檢查模組的特性狀態和瀏覽器支援,而不是根據命名的 CSS 級別推斷支援情況。

不同圖示形式在無障礙功能、樣式與載入方面各有取捨:

形式優點限制
圖示字型繼承 color 和文字大小單色字形限制和字型載入
Unicode 字元無額外資源語意和外觀因字型與平台而異
行內 SVG無障礙結構和個別部分的樣式增加 HTML 或元件複雜度
外部 SVG 或點陣影像可快取的獨立資源設定內部細節的樣式需要明確設計

為僅含圖示的控制項提供無障礙名稱:

html
<button type="button" aria-label="Search">
  <svg aria-hidden="true" viewBox="0 0 24 24">...</svg>
</button>

aria-hidden="true" 只適用於裝飾性 SVG,而且按鈕必須透過文字或 aria-label 提供無障礙名稱。

透過 HTTP 傳輸影片 #

瀏覽器會為可跳轉媒體請求位元組範圍:

http
GET /movie.mp4 HTTP/1.1
Range: bytes=32243712-32955076
If-Range: "entity-tag"

符合規範的範圍回應具有 206 Partial Content 狀態和 Content-Range 標頭。伺服器應提供 Accept-Ranges: bytes、穩定的驗證器與正確的媒體類型。忽略範圍請求的伺服器可以傳回 200,但由於用戶端可能需要整個檔案,跳轉成本會很高。

MPEG-DASH 將呈現內容拆分為 MPD 資訊清單所描述的媒體片段。用戶端選擇具有不同位元速率或解析度的表示形式,並隨頻寬變化調整片段選擇。片段需要具備與切換相容的關鍵畫格邊界;跨越片段邊界編碼一個長 GOP 會妨礙獨立解碼。

HLS 使用播放清單格式,遵循相同的自適應分段原則。瀏覽器支援範圍不同,因此應根據目標平台選擇播放器與封裝格式,而非假定 <video src> 可以使用所有資訊清單。

在 TypeScript 中建模列舉值 #

接受一組固定值之一的 API 參數不需要數值型 TypeScript enum。字串常值聯集可以保留線路上的值:

ts
type Status = "queued" | "running" | "failed";

function retry(status: Status): boolean {
  return status === "failed";
}

需要在執行階段反覆運算並維護單一事實來源時,應使用常數物件:

ts
const Status = {
  queued: "queued",
  running: "running",
  failed: "failed",
} as const;

type Status = typeof Status[keyof typeof Status];

數值列舉適用於指定數值的協定,但其反向對應和產生的 JavaScript 都會帶來成本。對於結構更豐富的預先定義值,應使用具有判別欄位與明確行為的物件建模,而不是強行將商業邏輯放入列舉成員。

說明瀏覽器的下載與行內行為 #

HTTP 回應可以要求瀏覽器下載內容:

http
Content-Type: application/pdf
Content-Disposition: attachment; filename="guide.pdf"

attachment 要求下載,而 inline 允許在支援的檢視器中顯示。瀏覽器、使用者偏好、內容類型、沙箱策略和安全標頭都會影響最終操作。X-Content-Type-Options: nosniff 會阻止類型嗅探,因此正確的 Content-Type 非常重要。應透過瀏覽器自動化測試下載,而不是假定一個回應標頭會在不同瀏覽器中產生相同的 UI 行為。

透過 GPU 管線算繪畫格 #

應用程式透過 WebGL、WebGPU、Vulkan、Metal 或 Direct3D 等 API 提交緩衝區、紋理、著色器程式與繪製命令。驅動程式和 GPU 隨後執行概念上的管線:

  1. 頂點著色器變換每個頂點並產生插值屬性。
  2. 基元組合將頂點組成線、三角形或點。
  3. 裁切會移除或分割裁切空間之外的幾何圖形。
  4. 光柵化會產生覆蓋取樣位置的片段。
  5. 片段著色器計算每個片段的輸出。
  6. 深度、樣板和混合操作將輸出與畫格緩衝區組合。

在透視除法之前,會將裁切空間座標與齊次裁切邊界比較。跨越裁切平面的三角形會被分割,因此片段著色器不會接收到可見裁切基元之外的覆蓋。

GPU 介面會跨越 CPU—GPU 邊界傳輸資料。應將靜態幾何圖形儲存在裝置常駐緩衝區中,以有限的配置更新動態資料,並避免同步讀回,因為同步讀回會迫使 CPU 與 GPU 的執行進度交會。

定位攝影機並為幾何圖形著色 #

攝影機將世界座標轉換為視圖座標,再將其投影到裁切空間。透視攝影機會將遠處物體映射至較小的螢幕區域;正交攝影機則在不同深度維持平行線與表觀大小。在常見的 Three.js 慣例中,位於 (0, 0, 0) 且方向為單位變換的攝影機朝 Z 軸負方向觀看,Y 軸正方向朝上。

Lambert 漫反射光照會對霧面表面建模:

text
diffuse = albedo × lightColor × max(dot(normal, lightDirection), 0)

點積會編碼表面法線與光線方向之間夾角的餘弦。Blinn-Phong 使用光線方向與視線方向之間的半向量,加入一個經驗性的鏡面反射項。兩種模型都要求正確變換法線:對於非均勻縮放,應使用模型—視圖矩陣的反矩陣轉置。

Three.js 攝影機與材質會在更高層次公開這些概念。設定攝影機位置、呼叫 camera.lookAt(target)、在長寬比變化後更新投影矩陣,並在場景更新後算繪。不要假定預設攝影機方向適用於自訂座標系。

合成透明度與顏色 #

標準的來源覆蓋 alpha 合成會將來源與目的端組合:

text
out.rgb = source.rgb × source.a + destination.rgb × (1 - source.a)
out.a   = source.a + destination.a × (1 - source.a)

該公式假定來源 RGB 已預乘。直通 alpha 資源需要使用算繪器預期的轉換或混合因子。應在線性光空間而非經過 gamma 編碼的 sRGB 中混合,因為對編碼值執行算術運算會產生錯誤的變暗效果與漸層。

人類的顏色感知始於視錐細胞對彼此重疊波長範圍的反應。色彩空間將數值座標映射至定義明確的參考系統;脫離色彩空間的座標具有歧義。sRGB 定義了原色、白點、傳遞函式與編碼慣例。在光照或插值之前應將 sRGB 轉換為線性值,隨後再編碼以供顯示。

HSL 與 HSV 是方便的編輯座標,並非感知均勻空間。色相、飽和度或亮度中相同的數值步長不會產生相同的感知步長。對於需要符合物理意義之混合的漸層,應在線性 RGB 中插值。對於根據感知亮度設計的漸層,可以考慮 OKLab 等感知空間,再轉換回目標色域,並刻意裁切或映射超出色域的顏色。

洋紅色是一種感知顏色,由長波與短波視錐細胞同時反應而產生,不對應單一的光譜波長。加色混合不會平均成一個波長;視覺系統會解讀這些視錐細胞的組合訊號。

在容器中執行 Playwright #

Playwright 需要瀏覽器二進位檔及其作業系統程式庫。應將官方映像檔標籤固定為專案中的 @playwright/test 版本:

dockerfile
FROM mcr.microsoft.com/playwright:v1.58.2-noble

WORKDIR /work
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN chown -R pwuser:pwuser /work
USER pwuser
CMD ["npx", "playwright", "test"]

官方映像檔包含受支援的瀏覽器與相依套件。映像檔版本與套件版本應同步更新。建置映像檔,並透過 init 行程執行:

sh
docker build -t example-playwright-tests .
docker run --rm --init --ipc=host example-playwright-tests

--init 會回收瀏覽器子行程。在 --ipc=host 下,更大的共享記憶體區域有利於 Chromium;僅應在受信任的環境中使用該選項。自訂 Node 基礎映像檔必須在建置映像檔期間安裝瀏覽器相依套件與所選瀏覽器:

dockerfile
FROM node:22-bookworm
WORKDIR /work
COPY package.json package-lock.json ./
RUN npm ci && npx playwright install --with-deps chromium
COPY . .
CMD ["npx", "playwright", "test"]

不要在每次容器啟動時下載瀏覽器。應將報告、追蹤記錄、螢幕截圖與影片保存為 CI 成品,因為失敗往往取決於測試環境。

自動化虛擬化清單而不陷入無限迴圈 #

虛擬化清單會為可見項目建立節點,並使用佔位元素表示完整捲動範圍。DOM 中不存在的項目仍可能存在於資料集中,因此定位器必須驅動捲動並等待一輪算繪。

限制搜尋次數,並偵測是否停止前進:

ts
const scroller = page.locator('ul[role="listbox"] > div');
const option = page.getByRole("option", { name: targetAddress, exact: true });

for (let attempts = 0; attempts < 500; attempts += 1) {
  if (await option.count()) {
    await option.click();
    break;
  }

  const moved = await scroller.evaluate((element) => {
    const before = element.scrollTop;
    element.scrollBy(0, Math.max(1, element.clientHeight * 0.8));
    return element.scrollTop !== before;
  });
  if (!moved) throw new Error(`Option not found: ${targetAddress}`);
  await page.waitForTimeout(0);
}

waitForTimeout(0) 會將執行權交還瀏覽器事件迴圈,讓捲動事件監聽器或框架算繪能夠更新虛擬化節點。正式環境測試應優先使用應用程式的資料 API 或語意搜尋控制項(若有);只有在必須透過算繪介面測試行為時,才將類似像素的捲動作為備用方案。

評論

在 GitHub 上閱讀或評論(發表評論需要登入)