[{"data":1,"prerenderedAt":1160},["ShallowReactive",2],{"search-dialog-posts":3,"post-\u002Fposts\u002Fmobile-debugging-today":316,"post-surround-\u002Fposts\u002Fmobile-debugging-today":1153},[4,12,21,29,37,47,57,67,77,85,96,104,112,121,129,137,147,155,164,173,181,188,195,207,215,224,236,241,247,254,260,266,272,279,286,291,298,304,310],{"path":5,"title":6,"description":7,"category":8,"tags":9},"\u002Fposts\u002Fgit-commands-cheatsheet","Git 常用指令與終端機筆記","2018 年整理的 Git 學習筆記，包含基本終端機指令、Git 操作、分支、reset、SourceTree、Vim 與一些常用設定。","notes",[10,11],"Git","版本控制",{"path":13,"title":14,"description":15,"category":16,"tags":17},"\u002Fposts\u002Fjs-prototype-inheritance","JavaScript 物件導向：建構函式、prototype 與繼承三步驟","整理 ES5 時代用建構函式加 prototype 手刻物件導向的完整流程——封裝屬性與方法、共用函式為什麼要掛在 prototype 上、以及繼承的三個步驟（call 借用母類別建構子、Object.create 接上原型鏈、修正 constructor 指向），並附上物件縮寫與計算屬性這兩個順便學到的 ES6 寫法。","frontend",[18,19,20],"JavaScript","物件導向","prototype",{"path":22,"title":23,"description":24,"category":16,"tags":25},"\u002Fposts\u002Fflexbox-notes","Flexbox 對齊與彈性尺寸筆記","記錄 Flexbox 的主軸與交錯軸對齊、flex-direction、flex-wrap，以及 flex-grow、flex-shrink、flex-basis 等常用屬性。",[26,27,28],"CSS","Flexbox","版面",{"path":30,"title":31,"description":32,"category":16,"tags":33},"\u002Fposts\u002Fscreen-orientation-api","screen.orientation：現在判斷與鎖定螢幕方向的做法","整理現在 JavaScript 判斷螢幕方向的方式，包括 screen.orientation、change 事件與 lock()，並說明 RWD 使用 matchMedia 或 CSS orientation 會更適合。",[18,34,35,36],"RWD","螢幕方向","PWA",{"path":38,"title":39,"description":40,"category":16,"tags":41},"\u002Fposts\u002Fpinia-vuex-state-management","Pinia \u002F Vuex 狀態管理及差異","紀錄 Vuex、Pinia 的狀態管理概念、差異與用法。",[42,43,44,45,46],"Pinia","Vuex","Vue3","狀態管理","Nuxt",{"path":48,"title":49,"description":50,"category":8,"tags":51},"\u002Fposts\u002Fpyenv-python-version-management","Python 多版本管理 pyenv","紀錄 macOS 與 Windows 使用 pyenv 管理 Python 版本的方式。",[52,53,54,55,56],"Python","pyenv","macOS","Windows","版本管理",{"path":58,"title":59,"description":60,"category":8,"tags":61},"\u002Fposts\u002Fnpm-publish-vue2-component","npm 發布流程","紀錄 Vue 2 元件的 npm 發布、dist-tag 與打包流程。",[62,63,64,65,66],"npm","Vue2","Vue CLI","套件發布","MIT",{"path":68,"title":69,"description":70,"category":8,"tags":71},"\u002Fposts\u002Fmobile-debugging-today","現在怎麼 debug 手機網頁：從裝一堆工具到幾乎不用裝","對照 2022 年那套 scoop、iTunes、remotedebug-ios-webkit-adapter 的繁瑣設定，整理現在偵錯手機網頁的做法：Android 無線偵錯、iOS 的實際選項、on-device console、Playwright 裝置模擬，以及本機 HTTPS 的處理。",[72,73,74,75,76],"debug","iOS","Android","Chrome DevTools","Playwright",{"path":78,"title":79,"description":80,"category":16,"tags":81},"\u002Fposts\u002Fplaywright-vs-cypress","E2E 測試現在選什麼：Playwright 與 Cypress 的實際差異","對照幾年前用 Cypress 的做法，說明 Playwright 為什麼變成多數新專案的預設選擇：真正的多瀏覽器支援、免費的平行執行、trace viewer 除錯，以及兩者在架構上的根本差異與各自仍然適合的場景。",[76,82,83,84],"Cypress","E2E","測試",{"path":86,"title":87,"description":88,"category":89,"tags":90},"\u002Fposts\u002Fjs-encoding-today","現在處理編碼：TextEncoder、Uint8Array 的 Base64 方法與 URLSearchParams","整理 JavaScript 現代編碼方式，使用 TextEncoder、TextDecoder 與 Uint8Array 處理 UTF-8 與 Base64，並說明 Base64URL、URLSearchParams 與 encodeURIComponent 的差異。","research",[91,92,93,94,95,18],"編碼","TextEncoder","Base64","URLSearchParams","Uint8Array",{"path":97,"title":98,"description":99,"category":16,"tags":100},"\u002Fposts\u002Fdart-sass-migration","Dart Sass 遷移筆記：node-sass 之後怎麼裝 Sass","node-sass 與 LibSass 已停止維護，現在改用 Dart Sass。整理 webpack 與 Vite 的安裝方式，以及為什麼不再像 node-sass 一樣容易遇到 Node.js 版本與原生編譯問題。",[101,102,103],"Sass","Webpack","Vite",{"path":105,"title":106,"description":107,"category":16,"tags":108},"\u002Fposts\u002Fdayjs-vue3-computed","day.js 搭配 Vue 3 的日期格式化","整理 Vue 3 日期格式化的常見做法，使用 day.js 取代舊專案常見的 moment.js，並以 method 或 computed 取代 Vue 2 filter，也補充原生 Intl.DateTimeFormat 的使用方式。",[44,109,110,111],"day.js","日期","Intl",{"path":113,"title":114,"description":115,"category":16,"tags":116},"\u002Fposts\u002Fes-modules-today","ES Module 現在的樣子：Node ESM、exports 與 import maps","整理現代 JavaScript ES Module 的常見用法，包括 Node.js 原生 ESM、package.json exports、瀏覽器 import maps、JSON import attributes，以及 Vite 動態載入的注意事項。",[117,118,119,120,103,18],"ES Module","Node.js","import maps","exports",{"path":122,"title":123,"description":124,"category":89,"tags":125},"\u002Fposts\u002Fstructured-clone","structuredClone()：終於有內建的深層拷貝","整理 JavaScript structuredClone() 的深層拷貝用法，說明 Date、Map、Set、循環參考的處理方式，以及 Function、class 與 Vue reactive Proxy 的注意事項。",[18,126,127,128],"structuredClone","深拷貝","Vue",{"path":130,"title":131,"description":132,"category":16,"tags":133},"\u002Fposts\u002Fdynamic-import","JavaScript 動態載入：import() 與 Vue 非同步元件","整理靜態 import、動態 import() 與 Vue defineAsyncComponent 的使用方式，並簡單補充 Vite import.meta.glob 的用途。",[18,134,135,103,136],"ES Modules","Vue 3","Code Splitting",{"path":138,"title":139,"description":140,"category":16,"tags":141},"\u002Fposts\u002Fscreen-wake-lock-api","Screen Wake Lock API：不用套件也能讓頁面保持亮著","整理 Screen Wake Lock API 的基本用法，說明 HTTPS、頁面可見性、切換分頁後重新取得 wake lock，以及 Vue／Nuxt 計時器使用時的注意事項。",[142,128,143,144,145,146],"Wake Lock","Nuxt4","行動裝置","計時器","Web API",{"path":148,"title":149,"description":150,"category":16,"tags":151},"\u002Fposts\u002Fhtml-meta-boilerplate-today","現在的 HTML meta 標籤怎麼寫","整理現在一個新頁面該有的 meta 標籤骨架——viewport 補上 viewport-fit=cover 因應瀏海與圓角螢幕、color-scheme 讓表單控制項跟著深色模式換色、theme-color 決定行動瀏覽器工具列顏色，以及基本的 SEO 與 Open Graph 標籤。",[152,153,154],"HTML","meta","SEO",{"path":156,"title":157,"description":158,"category":8,"tags":159},"\u002Fposts\u002Fdocker-node-postgres","把 Node + PostgreSQL 的 backend 裝進 Docker：Dockerfile、.dockerignore 與 compose","從「本機裝了就跑」到打包成容器的通用做法——Dockerfile 每一行的理由與 layer cache 為什麼決定了指令順序、.dockerignore 漏掉 node_modules 對原生模組是災難級的問題、容器之間為什麼不能用 localhost 互相找到對方，以及怎麼驗證資料真的寫進了 volume。",[160,161,118,162,163],"Docker","Docker Compose","PostgreSQL","Express",{"path":165,"title":166,"description":167,"category":16,"tags":168},"\u002Fposts\u002Fnuxt-server-proxy","Nuxt 3 \u002F 4 用 Server API 做代理與轉發","整理 Nuxt 3 與 Nuxt 4 使用 server\u002Fapi 轉發第三方 API 的方式，可以避免瀏覽器 CORS 問題、隱藏 API Key，也能在伺服器端加入快取。",[169,143,170,171,172],"Nuxt3","CORS","Nitro","API",{"path":174,"title":175,"description":176,"category":16,"tags":177},"\u002Fposts\u002Fnuxt4-project-setup","Nuxt 4 建立專案：和 Nuxt 3 有哪些不同","整理 Nuxt 4 建立專案的基本流程與目錄結構，並對照 Nuxt 3 的差異，包括 app\u002F 目錄、路徑別名、TypeScript，以及目前常用的 ESLint 與 Tailwind CSS 4 設定。",[143,44,178,179,180],"TypeScript","ESLint","Tailwind CSS 4",{"path":182,"title":183,"description":184,"category":16,"tags":185},"\u002Fposts\u002Fnuxt3-route-middleware","Nuxt 3 \u002F 4 的 Route Middleware：進頁面前先做判斷","整理 Nuxt 3 與 Nuxt 4 的 Route Middleware，用 defineNuxtRouteMiddleware 在進入頁面前檢查登入狀態，搭配 navigateTo 重新導向，並說明一般 middleware、global middleware 與 definePageMeta 的用法。",[169,143,186,187],"Middleware","驗證",{"path":189,"title":190,"description":191,"category":16,"tags":192},"\u002Fposts\u002Fnuxt-route-rules-redirect","Nuxt 3 \u002F 4 的 routeRules：重新導向與頁面設定","整理 Nuxt 3 與 Nuxt 4 的 routeRules 用法，可以依照不同網址設定重新導向、預渲染、SSR 與快取，並簡單對照 Nuxt 2 的舊做法。",[169,143,193,194],"routeRules","重新導向",{"path":196,"title":197,"description":198,"category":16,"tags":199},"\u002Fposts\u002Fios-iframe-google-login-blank-page","iOS iframe 串接 Google 登入出現空白頁：Popup 與頁面回收問題排查紀錄","記錄 iframe 內串接 Google 第三方登入時，部分 iOS 裝置因背景頁面遭到回收並重新載入，導致 Popup 登入流程中斷，以及最後透過 Redirect 分流與登入狀態接回機制處理的過程。",[73,200,201,202,203,204,205,206],"Safari","iframe","Google Login","GIS","OAuth","Popup","Redirect",{"path":208,"title":209,"description":210,"category":16,"tags":211},"\u002Fposts\u002Fios-login-tab-return-behavior","iOS 第三方登入後回不到原頁？分頁開啟關係與回頁行為實測","記錄 iOS Safari 與 Chrome 在多分頁第三方登入流程中，登入頁關閉後不一定回到原頁的實測，以及分頁開啟關係、window.close 與 window.focus 的限制。",[73,200,212,202,204,201,213,214],"Chrome","window.close","window.focus",{"path":216,"title":217,"description":218,"category":16,"tags":219},"\u002Fposts\u002Fgoogle-app-about-blank-placeholder","Google App 內建瀏覽器：about:blank 佔位頁被關閉的排查紀錄","記錄 iOS Google App 內建瀏覽器在非同步開啟外部頁面時，about:blank 佔位分頁被自動關閉，導致第三方登入或外部導轉失敗的排查與解法。",[73,220,221,222,204,223],"Google App","window.open","about:blank","WebView",{"path":225,"title":226,"description":227,"category":228,"tags":229},"\u002Fposts\u002Fclaude-code-codex-cli-init-basics","AI 相關紀錄 v1｜Claude Code \u002F Codex CLI 初始化與基本概念","從 Claude Code 與 Codex CLI 開始，整理 CLI、Coding Agent、Context、\u002Finit 與 CLAUDE.md／AGENTS.md 這幾個一開始最容易搞混的概念，以及在既有專案中的安裝與啟動方式。","ai",[230,231,232,233,234,235],"AI","Claude Code","Codex CLI","Coding Agent","CLAUDE.md","AGENTS.md",{"path":237,"title":238,"description":239,"category":228,"tags":240},"\u002Fposts\u002Fclaude-agents-md-project-guidelines","AI 相關紀錄 v2｜CLAUDE.md \u002F AGENTS.md 怎麼寫？專案規範與實際範例","整理 CLAUDE.md 與 AGENTS.md 的用途、適合記錄的專案規範，以及可直接參考的設定範例。",[230,231,232,234,235],{"path":242,"title":243,"description":244,"category":228,"tags":245},"\u002Fposts\u002Fclaude-codex-config-locations","AI 相關紀錄 v3｜Claude Code `.claude\u002F` \u002F Codex CLI 設定位置：全域與專案設定基本概要介紹","整理 Claude Code 與 Codex CLI 的使用者層級、專案層級設定位置，以及 rules、skills、agents 等目錄的用途。",[230,231,232,246],"設定檔",{"path":248,"title":249,"description":250,"category":228,"tags":251},"\u002Fposts\u002Fclaude-settings-codex-config","AI 相關紀錄 v4｜Claude Code settings \u002F Codex config 基本設定","整理 Claude Code settings.json 與 Codex config.toml 的設定層級、權限控制和常見基本設定。",[230,231,232,252,253],"settings.json","config.toml",{"path":255,"title":256,"description":257,"category":228,"tags":258},"\u002Fposts\u002Fclaude-codex-rules-structure","AI 相關紀錄 v5｜Rules：專案規則多了之後怎麼拆？","整理 Claude Code Rules 與 Codex AGENTS.md 的拆分方式，說明何時該拆、怎麼依目錄管理專案規則。",[230,231,232,259,235],"Rules",{"path":261,"title":262,"description":263,"category":228,"tags":264},"\u002Fposts\u002Fclaude-codex-slash-commands","AI 相關紀錄 v6｜Commands \u002F Slash Commands：CLI 裡的 `\u002F` 指令是什麼？","介紹 Claude Code 與 Codex CLI 的 Slash Commands，整理常用內建指令、自訂指令與 Skills 的差異。",[230,231,232,265],"Slash Commands",{"path":267,"title":268,"description":269,"category":228,"tags":270},"\u002Fposts\u002Fclaude-codex-skills","AI 相關紀錄 v7｜Skills：把重複工作流程整理起來","介紹 Claude Code 與 Codex Skills，整理適合保存成 Skill 的重複流程、基本結構與使用方式。",[230,231,232,271],"Skills",{"path":273,"title":274,"description":275,"category":228,"tags":276},"\u002Fposts\u002Fclaude-codex-agents-subagents","AI 相關紀錄 v8｜Agent \u002F Subagent：什麼時候真的需要分工？","整理 Agent、Subagent 與 Custom Agent 的用途，說明何時適合分工，以及它們和 Rules、Skills 的差異。",[230,231,232,277,278],"Agent","Subagent",{"path":280,"title":281,"description":282,"category":228,"tags":283},"\u002Fposts\u002Fmcp-basics","AI 相關紀錄 v9｜MCP：讓 AI 使用其他工具與資料","介紹 MCP 如何讓 AI 使用外部工具與資料，並以 Pencil MCP 說明實際用途、權限風險及與 Skill 的差異。",[230,284,231,232,285],"MCP","Pencil",{"path":287,"title":288,"description":289,"category":228,"tags":290},"\u002Fposts\u002Fai-coding-agent-concepts-summary","AI 相關紀錄 v10｜基本概念小結論","用一篇快速整理 CLAUDE.md、AGENTS.md、設定檔、Rules、Commands、Skills、Agent 與 MCP 的角色和差異。",[230,231,232,284,271,277],{"path":292,"title":293,"description":294,"category":228,"tags":295},"\u002Fposts\u002Fclaude-code-codex-model-selection","Claude Code 與 Codex：不同模型怎麼選？","比較 Claude Code 與 Codex 不同模型的速度、能力與成本取捨，依工作難度整理實用的選擇方向。",[230,231,296,297],"Codex","模型選擇",{"path":299,"title":300,"description":301,"category":228,"tags":302},"\u002Fposts\u002Fclaude-model-selection","Claude 模型簡單怎麼選？","整理 Claude 各模型在速度、能力與使用情境上的差異，提供從輕量工作到複雜任務的選擇方向。",[230,231,303,297],"Claude",{"path":305,"title":306,"description":307,"category":228,"tags":308},"\u002Fposts\u002Fcodex-model-selection","Codex 模型簡單怎麼選？","整理 Codex 各模型在速度、能力與使用情境上的差異，提供從簡單修改到大型端到端任務的選擇方向。",[230,296,309,297],"OpenAI",{"path":311,"title":312,"description":313,"category":228,"tags":314},"\u002Fposts\u002Fclaude-code-codex-cli-command-cheatsheet","Claude Code \u002F Codex CLI 常用指令速查表","整理 Claude Code 與 Codex CLI 的常用斜線指令、快捷操作與進階功能，方便需要時快速查找。",[230,231,232,265,315],"速查表",{"id":317,"title":69,"body":318,"category":8,"description":70,"draft":1142,"extension":1143,"image":1144,"meta":1145,"navigation":412,"path":68,"publishedAt":1146,"relatedExperiments":1147,"seo":1148,"stem":1149,"tags":1150,"updatedAt":1151,"__hash__":1152},"posts\u002Fposts\u002F20250725.mobile-debugging-today.md",{"type":319,"value":320,"toc":1127},"minimark",[321,325,338,343,349,354,375,425,434,441,445,483,487,494,498,501,522,525,529,532,562,568,572,587,642,645,651,726,729,732,735,742,745,889,895,914,917,921,928,931,973,976,1080,1083,1123],[322,323,324],"p",{},"2020 到 2022 年間，在 Windows 上遠端 debug iPhone 網頁，常見流程是安裝 scoop、ios-webkit-debug-proxy、libimobiledevice、iTunes，再透過轉接器把 iOS 的 WebKit Debug 資訊接到 Chrome DevTools。工具與裝置必須按順序啟動，少一個環節就可能連不上。",[322,326,327,328,332,333,337],{},"那套現在",[329,330,331],"strong",{},"不建議再用了","——",[334,335,336],"code",{},"remotedebug-ios-webkit-adapter"," 已經多年未維護。這篇講現在實際會怎麼做。",[339,340,342],"h2",{"id":341},"android變得更簡單了","Android：變得更簡單了",[322,344,345,348],{},[334,346,347],{},"chrome:\u002F\u002Finspect"," 那套沒變，但現在多了無線偵錯，不用一直插著線。",[350,351,353],"h3",{"id":352},"無線偵錯android-11","無線偵錯（Android 11+）",[355,356,357,365,372],"ol",{},[358,359,360,361,364],"li",{},"手機開啟「開發人員選項」→「",[329,362,363],{},"無線偵錯","」",[358,366,367,368,371],{},"點「",[329,369,370],{},"使用配對碼配對裝置","」，會顯示一組配對碼與 IP:port",[358,373,374],{},"電腦執行：",[376,377,382],"pre",{"className":378,"code":379,"language":380,"meta":381,"style":381},"language-bash shiki shiki-themes github-light github-dark-default","adb pair 192.168.1.100:37251\n# 輸入手機上顯示的配對碼\n\nadb connect 192.168.1.100:5555\n","bash","",[334,383,384,400,407,414],{"__ignoreMap":381},[385,386,389,393,397],"span",{"class":387,"line":388},"line",1,[385,390,392],{"class":391},"sYRkA","adb",[385,394,396],{"class":395},"sNbzA"," pair",[385,398,399],{"class":395}," 192.168.1.100:37251\n",[385,401,403],{"class":387,"line":402},2,[385,404,406],{"class":405},"sPyeA","# 輸入手機上顯示的配對碼\n",[385,408,410],{"class":387,"line":409},3,[385,411,413],{"emptyLinePlaceholder":412},true,"\n",[385,415,417,419,422],{"class":387,"line":416},4,[385,418,392],{"class":391},[385,420,421],{"class":395}," connect",[385,423,424],{"class":395}," 192.168.1.100:5555\n",[355,426,427],{"start":416},[358,428,429,430,433],{},"開 ",[334,431,432],{},"chrome:\u002F\u002Finspect\u002F#devices","，裝置就會出現",[322,435,436,437,440],{},"配對只需要做一次，之後同一個網路下 ",[334,438,439],{},"adb connect"," 就能連上。手機可以拿在手上測捲動、測橫置，不會被線扯住。",[350,442,444],{"id":443},"埠轉發讓手機連到你的-localhost","埠轉發：讓手機連到你的 localhost",[446,447,448,455,470,477],"ul",{},[358,449,450,451,454],{},"這個功能很多人不知道，但幾乎每次都會用到。手機沒辦法直接開 ",[334,452,453],{},"localhost:3000","，因為那是電腦的 localhost。",[358,456,457,459,460,463,464,459,467,469],{},[334,458,432],{}," → ",[329,461,462],{},"Port forwarding"," → 加一條 ",[334,465,466],{},"3000",[334,468,453],{},"。",[358,471,472,473,476],{},"之後手機的 Chrome 直接開 ",[334,474,475],{},"http:\u002F\u002Flocalhost:3000"," 就會連到你電腦上的開發伺服器。",[358,478,479,482],{},[329,480,481],{},"而且它算在安全來源裡","，所以 Service Worker、getUserMedia、Web Crypto 這些需要安全環境的 API 都能正常測試，不用另外弄憑證。",[339,484,486],{"id":485},"ios老實說選項還是不多","iOS：老實說，選項還是不多",[322,488,489,490,493],{},"核心限制沒變：",[329,491,492],{},"Safari 的偵錯要靠 Safari","，而 Safari 只有 macOS 有。",[350,495,497],{"id":496},"有-mac-的話","有 Mac 的話",[322,499,500],{},"這是唯一順暢的路：",[355,502,503,510,516,519],{},[358,504,505,506,509],{},"iPhone：設定 → Apps → Safari → 進階 → ",[329,507,508],{},"網頁檢閱器","開啟",[358,511,512,513,364],{},"Mac 的 Safari：設定 → 進階 → 勾選「",[329,514,515],{},"顯示網頁開發者功能",[358,517,518],{},"USB 接上（或同一個 Apple ID 下用無線）",[358,520,521],{},"Mac 的 Safari → 開發 → 選你的 iPhone → 選要偵錯的分頁",[322,523,524],{},"功能跟桌機的 Web Inspector 一樣。",[350,526,528],{"id":527},"沒有-mac-的話","沒有 Mac 的話",[322,530,531],{},"老實講，沒有好答案。實務上的選擇是：",[446,533,534,550,556],{},[358,535,536,541,542,549],{},[329,537,538],{},[334,539,540],{},"ios-webkit-debug-proxy","：這個專案本身還在維護，配合 ",[543,544,548],"a",{"href":545,"rel":546},"https:\u002F\u002Fgithub.com\u002FRemoteDebug\u002Fremotedebug-ios-webkit-adapter",[547],"nofollow","RemoteDebug iOS WebKit Adapter"," 的老路子有時候還能通，但相容性看 iOS 版本，會踩到什麼很難預期。",[358,551,552,555],{},[329,553,554],{},"雲端裝置服務","：BrowserStack、LambdaTest 這類，提供真機加上 DevTools。要付費，但對「只是要確認某個 iOS 版本上會不會壞」很有效率。",[358,557,558,561],{},[329,559,560],{},"on-device console","（見下一節）：不能設中斷點，但看 log 與網路請求夠用。",[322,563,564,565],{},"要接受的現實是：",[329,566,567],{},"在 Windows 上完整偵錯 iOS Safari，至今沒有輕鬆的解法。",[339,569,571],{"id":570},"on-device-console塞一段程式就有-devtools","on-device console：塞一段程式就有 DevTools",[322,573,574,575,580,581,586],{},"不管什麼平台都能用的土法煉鋼，但意外好用。",[543,576,579],{"href":577,"rel":578},"https:\u002F\u002Fgithub.com\u002Fliriliri\u002Feruda",[547],"Eruda"," 或 ",[543,582,585],{"href":583,"rel":584},"https:\u002F\u002Fgithub.com\u002FTencent\u002FvConsole",[547],"vConsole"," 會在頁面上疊一個浮動按鈕，點開就是簡化版的 DevTools。",[376,588,592],{"className":589,"code":590,"language":591,"meta":381,"style":381},"language-html shiki shiki-themes github-light github-dark-default","\u003Cscript src=\"https:\u002F\u002Fcdn.jsdelivr.net\u002Fnpm\u002Feruda\">\u003C\u002Fscript>\n\u003Cscript>eruda.init();\u003C\u002Fscript>\n","html",[334,593,594,622],{"__ignoreMap":381},[385,595,596,600,604,608,611,614,617,619],{"class":387,"line":388},[385,597,599],{"class":598},"szmH1","\u003C",[385,601,603],{"class":602},"sXQ1o","script",[385,605,607],{"class":606},"syv4q"," src",[385,609,610],{"class":598},"=",[385,612,613],{"class":395},"\"https:\u002F\u002Fcdn.jsdelivr.net\u002Fnpm\u002Feruda\"",[385,615,616],{"class":598},">\u003C\u002F",[385,618,603],{"class":602},[385,620,621],{"class":598},">\n",[385,623,624,626,628,631,635,638,640],{"class":387,"line":402},[385,625,599],{"class":598},[385,627,603],{"class":602},[385,629,630],{"class":598},">eruda.",[385,632,634],{"class":633},"sVjZL","init",[385,636,637],{"class":598},"();\u003C\u002F",[385,639,603],{"class":602},[385,641,621],{"class":598},[322,643,644],{},"看得到 Console、Network、Elements、localStorage。設不了中斷點，但「這支手機上到底噴什麼錯」這個問題它答得出來。",[322,646,647,650],{},[329,648,649],{},"只在開發環境載入","，不要進正式站：",[376,652,656],{"className":653,"code":654,"language":655,"meta":381,"style":381},"language-javascript shiki shiki-themes github-light github-dark-default","if (import.meta.env.DEV) {\n  const eruda = await import('eruda');\n  eruda.default.init();\n}\n","javascript",[334,657,658,685,711,721],{"__ignoreMap":381},[385,659,660,664,667,670,673,676,679,682],{"class":387,"line":388},[385,661,663],{"class":662},"sDgYj","if",[385,665,666],{"class":598}," (",[385,668,669],{"class":662},"import",[385,671,672],{"class":598},".",[385,674,153],{"class":675},"sPgVT",[385,677,678],{"class":598},".env.",[385,680,681],{"class":675},"DEV",[385,683,684],{"class":598},") {\n",[385,686,687,690,693,696,699,702,705,708],{"class":387,"line":402},[385,688,689],{"class":662},"  const",[385,691,692],{"class":675}," eruda",[385,694,695],{"class":662}," =",[385,697,698],{"class":662}," await",[385,700,701],{"class":662}," import",[385,703,704],{"class":598},"(",[385,706,707],{"class":395},"'eruda'",[385,709,710],{"class":598},");\n",[385,712,713,716,718],{"class":387,"line":409},[385,714,715],{"class":598},"  eruda.default.",[385,717,634],{"class":633},[385,719,720],{"class":598},"();\n",[385,722,723],{"class":387,"line":416},[385,724,725],{"class":598},"}\n",[322,727,728],{},"這招在 App 內建的 WebView（LINE、Facebook 內開的頁面）特別有價值——那些環境根本接不上任何遠端偵錯工具，但問題偏偏常常只在裡面發生。",[339,730,731],{"id":731},"先用模擬器排除掉一半的問題",[322,733,734],{},"真機偵錯很麻煩，所以先確認問題是不是真的跟裝置有關。",[322,736,737,738,741],{},"Chrome DevTools 的 Device Mode（Ctrl+Shift+M）可以模擬螢幕尺寸、觸控事件、",[334,739,740],{},"prefers-reduced-motion"," 與網路節流。版面破掉、觸控目標太小這類問題，在這裡就看得出來，不用接手機。",[322,743,744],{},"Playwright 更適合放進自動化流程：",[376,746,748],{"className":653,"code":747,"language":655,"meta":381,"style":381},"import { chromium, devices } from '@playwright\u002Ftest';\n\nconst browser = await chromium.launch();\nconst context = await browser.newContext({\n  ...devices['iPhone 15'],\n});\nconst page = await context.newPage();\nawait page.goto('http:\u002F\u002Flocalhost:3000');\nawait page.screenshot({ path: 'mobile.png' });\n",[334,749,750,766,770,790,810,825,831,851,870],{"__ignoreMap":381},[385,751,752,754,757,760,763],{"class":387,"line":388},[385,753,669],{"class":662},[385,755,756],{"class":598}," { chromium, devices } ",[385,758,759],{"class":662},"from",[385,761,762],{"class":395}," '@playwright\u002Ftest'",[385,764,765],{"class":598},";\n",[385,767,768],{"class":387,"line":402},[385,769,413],{"emptyLinePlaceholder":412},[385,771,772,775,778,780,782,785,788],{"class":387,"line":409},[385,773,774],{"class":662},"const",[385,776,777],{"class":675}," browser",[385,779,695],{"class":662},[385,781,698],{"class":662},[385,783,784],{"class":598}," chromium.",[385,786,787],{"class":633},"launch",[385,789,720],{"class":598},[385,791,792,794,797,799,801,804,807],{"class":387,"line":416},[385,793,774],{"class":662},[385,795,796],{"class":675}," context",[385,798,695],{"class":662},[385,800,698],{"class":662},[385,802,803],{"class":598}," browser.",[385,805,806],{"class":633},"newContext",[385,808,809],{"class":598},"({\n",[385,811,813,816,819,822],{"class":387,"line":812},5,[385,814,815],{"class":662},"  ...",[385,817,818],{"class":598},"devices[",[385,820,821],{"class":395},"'iPhone 15'",[385,823,824],{"class":598},"],\n",[385,826,828],{"class":387,"line":827},6,[385,829,830],{"class":598},"});\n",[385,832,834,836,839,841,843,846,849],{"class":387,"line":833},7,[385,835,774],{"class":662},[385,837,838],{"class":675}," page",[385,840,695],{"class":662},[385,842,698],{"class":662},[385,844,845],{"class":598}," context.",[385,847,848],{"class":633},"newPage",[385,850,720],{"class":598},[385,852,854,857,860,863,865,868],{"class":387,"line":853},8,[385,855,856],{"class":662},"await",[385,858,859],{"class":598}," page.",[385,861,862],{"class":633},"goto",[385,864,704],{"class":598},[385,866,867],{"class":395},"'http:\u002F\u002Flocalhost:3000'",[385,869,710],{"class":598},[385,871,873,875,877,880,883,886],{"class":387,"line":872},9,[385,874,856],{"class":662},[385,876,859],{"class":598},[385,878,879],{"class":633},"screenshot",[385,881,882],{"class":598},"({ path: ",[385,884,885],{"class":395},"'mobile.png'",[385,887,888],{"class":598}," });\n",[322,890,891,894],{},[334,892,893],{},"devices"," 內建各種裝置的視窗大小、DPR、UA 與觸控設定。",[322,896,897,898,905,906,909,910,913],{},"但要講清楚它的界線：",[329,899,900,901,904],{},"Playwright 的 ",[334,902,903],{},"devices['iPhone 15']"," 跑的還是 Chromium","，只是把外觀參數換掉。真正的 Safari bug（WebKit 的 CSS 差異、日期解析、",[334,907,908],{},"100vh"," 行為）它抓不到。要測真的 WebKit 就得用 ",[334,911,912],{},"webkit"," 這個 browser type，那比較接近，但仍然不等於 iOS 上的 Safari。",[322,915,916],{},"模擬器負責排除版面問題，平台專屬的 bug 還是得上真機。",[339,918,920],{"id":919},"本機開發用-https","本機開發用 HTTPS",[322,922,923,924,927],{},"現在很多 API 只在安全來源下可用（Service Worker、getUserMedia、Web Bluetooth、剪貼簿）。手機要連你電腦的開發伺服器測這些功能時，",[334,925,926],{},"http:\u002F\u002F192.168.1.x:3000"," 會被擋。",[322,929,930],{},"三個做法：",[355,932,933,943,959],{},[358,934,935,938,939,942],{},[329,936,937],{},"Chrome 的埠轉發","（前面提過）——最省事，",[334,940,941],{},"localhost"," 天生算安全來源。只適用 Android。",[358,944,945,948,949,954,955,958],{},[329,946,947],{},"本機憑證","：用 ",[543,950,953],{"href":951,"rel":952},"https:\u002F\u002Fgithub.com\u002FFiloSottile\u002Fmkcert",[547],"mkcert"," 產生自簽憑證，把根憑證裝進手機。Vite 設定 ",[334,956,957],{},"server.https"," 帶入憑證即可。",[358,960,961,964,965,968,969,972],{},[329,962,963],{},"對外通道","：",[334,966,967],{},"cloudflared tunnel"," 或 ngrok，給你一個真的 HTTPS 網址。方便，但",[329,970,971],{},"流量會經過第三方","，測試含敏感資料的頁面前先想清楚。",[339,974,975],{"id":975},"對照表",[977,978,979,995],"table",{},[980,981,982],"thead",{},[983,984,985,989,992],"tr",{},[986,987,988],"th",{},"情境",[986,990,991],{},"2022",[986,993,994],{},"現在",[996,997,998,1014,1025,1036,1047,1058,1069],"tbody",{},[983,999,1000,1004,1007],{},[1001,1002,1003],"td",{},"Android 連線",[1001,1005,1006],{},"USB 線 + chrome:\u002F\u002Finspect",[1001,1008,1009,1010,1013],{},"無線偵錯（",[334,1011,1012],{},"adb pair","）",[983,1015,1016,1019,1022],{},[1001,1017,1018],{},"手機連 localhost",[1001,1020,1021],{},"改用區網 IP",[1001,1023,1024],{},"Chrome 埠轉發",[983,1026,1027,1030,1033],{},[1001,1028,1029],{},"iOS（有 Mac）",[1001,1031,1032],{},"Safari 網頁檢閱器",[1001,1034,1035],{},"沒變，仍是最佳解",[983,1037,1038,1041,1044],{},[1001,1039,1040],{},"iOS（只有 Windows）",[1001,1042,1043],{},"scoop + iTunes + adapter",[1001,1045,1046],{},"沒有好方法，考慮雲端真機或 on-device console",[983,1048,1049,1052,1055],{},[1001,1050,1051],{},"快速看 log",[1001,1053,1054],{},"無",[1001,1056,1057],{},"Eruda \u002F vConsole",[983,1059,1060,1063,1066],{},[1001,1061,1062],{},"版面檢查",[1001,1064,1065],{},"真機",[1001,1067,1068],{},"Device Mode 或 Playwright",[983,1070,1071,1074,1077],{},[1001,1072,1073],{},"本機 HTTPS",[1001,1075,1076],{},"少有需求",[1001,1078,1079],{},"mkcert 或埠轉發",[339,1081,1082],{"id":1082},"參考資料",[446,1084,1085,1092,1099,1106,1111,1118],{},[358,1086,1087],{},[543,1088,1091],{"href":1089,"rel":1090},"https:\u002F\u002Fdeveloper.chrome.com\u002Fdocs\u002Fdevtools\u002Fremote-debugging\u002F",[547],"Chrome DevTools：Remote debugging Android devices",[358,1093,1094],{},[543,1095,1098],{"href":1096,"rel":1097},"https:\u002F\u002Fdeveloper.android.com\u002Ftools\u002Fadb#wireless-android11-command-line",[547],"Android 無線偵錯",[358,1100,1101],{},[543,1102,1105],{"href":1103,"rel":1104},"https:\u002F\u002Fwebkit.org\u002Fweb-inspector\u002F",[547],"Safari Web Inspector",[358,1107,1108],{},[543,1109,579],{"href":577,"rel":1110},[547],[358,1112,1113],{},[543,1114,1117],{"href":1115,"rel":1116},"https:\u002F\u002Fplaywright.dev\u002Fdocs\u002Femulation",[547],"Playwright 裝置模擬",[358,1119,1120],{},[543,1121,953],{"href":951,"rel":1122},[547],[1124,1125,1126],"style",{},"html pre.shiki code .sYRkA, html code.shiki .sYRkA{--shiki-default:#6F42C1;--shiki-dark:#FFA657}html pre.shiki code .sNbzA, html code.shiki .sNbzA{--shiki-default:#032F62;--shiki-dark:#A5D6FF}html pre.shiki code .sPyeA, html code.shiki .sPyeA{--shiki-default:#6A737D;--shiki-dark:#8B949E}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .szmH1, html code.shiki .szmH1{--shiki-default:#24292E;--shiki-dark:#E6EDF3}html pre.shiki code .sXQ1o, html code.shiki .sXQ1o{--shiki-default:#22863A;--shiki-dark:#7EE787}html pre.shiki code .syv4q, html code.shiki .syv4q{--shiki-default:#6F42C1;--shiki-dark:#79C0FF}html pre.shiki code .sVjZL, html code.shiki .sVjZL{--shiki-default:#6F42C1;--shiki-dark:#D2A8FF}html pre.shiki code .sDgYj, html code.shiki .sDgYj{--shiki-default:#D73A49;--shiki-dark:#FF7B72}html pre.shiki code .sPgVT, html code.shiki .sPgVT{--shiki-default:#005CC5;--shiki-dark:#79C0FF}",{"title":381,"searchDepth":409,"depth":409,"links":1128},[1129,1133,1137,1138,1139,1140,1141],{"id":341,"depth":402,"text":342,"children":1130},[1131,1132],{"id":352,"depth":409,"text":353},{"id":443,"depth":409,"text":444},{"id":485,"depth":402,"text":486,"children":1134},[1135,1136],{"id":496,"depth":409,"text":497},{"id":527,"depth":409,"text":528},{"id":570,"depth":402,"text":571},{"id":731,"depth":402,"text":731},{"id":919,"depth":402,"text":920},{"id":975,"depth":402,"text":975},{"id":1082,"depth":402,"text":1082},false,"md",null,{},"2025-07-25",[],{"title":69,"description":70},"posts\u002F20250725.mobile-debugging-today",[72,73,74,75,76],"2026-09-08","OCK_vznrA9B49wp60V5r67Vs7El-MbMdx4RexfEn7rM",[1154,1157],{"title":79,"path":78,"stem":1155,"description":80,"category":16,"publishedAt":1156,"children":-1},"posts\u002F20250808.playwright-vs-cypress","2025-08-08",{"title":59,"path":58,"stem":1158,"description":60,"category":8,"publishedAt":1159,"children":-1},"posts\u002F20250122.npm-publish-vue2-component","2025-01-22",1791786901434]