[{"data":1,"prerenderedAt":1511},["ShallowReactive",2],{"search-dialog-posts":3,"post-\u002Fposts\u002Fjs-encoding-today":316,"post-surround-\u002Fposts\u002Fjs-encoding-today":1504},[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":87,"body":318,"category":89,"description":88,"draft":1490,"extension":1491,"image":1492,"meta":1493,"navigation":440,"path":86,"publishedAt":1494,"relatedExperiments":1495,"seo":1499,"stem":1500,"tags":1501,"updatedAt":1502,"__hash__":1503},"posts\u002Fposts\u002F20260325.js-encoding-today.md",{"type":319,"value":320,"toc":1479},"minimark",[321,325,365,376,379,384,387,395,398,449,452,482,491,495,501,576,579,727,730,734,737,740,746,753,756,794,797,822,826,829,832,838,844,906,912,994,997,1001,1004,1052,1055,1069,1072,1102,1108,1112,1118,1121,1228,1240,1243,1353,1356,1359,1419,1422,1428,1433,1436,1475],[322,323,324],"p",{},"以前處理中文 Base64 時，常看到這種寫法：",[326,327,332],"pre",{"className":328,"code":329,"language":330,"meta":331,"style":331},"language-js shiki shiki-themes github-light github-dark-default","btoa(unescape(encodeURIComponent('中文')))\n","js","",[333,334,335],"code",{"__ignoreMap":331},[336,337,340,344,348,351,353,356,358,362],"span",{"class":338,"line":339},"line",1,[336,341,343],{"class":342},"sVjZL","btoa",[336,345,347],{"class":346},"szmH1","(",[336,349,350],{"class":342},"unescape",[336,352,347],{"class":346},[336,354,355],{"class":342},"encodeURIComponent",[336,357,347],{"class":346},[336,359,361],{"class":360},"sNbzA","'中文'",[336,363,364],{"class":346},")))\n",[322,366,367,368,371,372,375],{},"現在不建議再使用這種方式，",[333,369,370],{},"escape()"," 與 ",[333,373,374],{},"unescape()"," 已經 deprecated。",[322,377,378],{},"比較清楚的做法是先把文字轉成 UTF-8 位元組，再處理 Base64。",[380,381,383],"h2",{"id":382},"字串-位元組","字串 ↔ 位元組",[322,385,386],{},"先記住這個流程：",[326,388,393],{"className":389,"code":391,"language":392,"meta":331},[390],"language-text","字串 \"中文\"\n\n  ↕ TextEncoder \u002F TextDecoder\n\nUint8Array\n\n  ↕ toBase64() \u002F fromBase64()\n\nBase64 \"5Lit5paH\"\n","text",[333,394,391],{"__ignoreMap":331},[322,396,397],{},"文字轉成位元組：",[326,399,401],{"className":328,"code":400,"language":330,"meta":331,"style":331},"const bytes = new TextEncoder().encode('中文')\n\n\u002F\u002F Uint8Array(6) [228, 184, 173, 230, 150, 135]\n",[333,402,403,435,442],{"__ignoreMap":331},[336,404,405,409,413,416,419,422,425,428,430,432],{"class":338,"line":339},[336,406,408],{"class":407},"sDgYj","const",[336,410,412],{"class":411},"sPgVT"," bytes",[336,414,415],{"class":407}," =",[336,417,418],{"class":407}," new",[336,420,421],{"class":342}," TextEncoder",[336,423,424],{"class":346},"().",[336,426,427],{"class":342},"encode",[336,429,347],{"class":346},[336,431,361],{"class":360},[336,433,434],{"class":346},")\n",[336,436,438],{"class":338,"line":437},2,[336,439,441],{"emptyLinePlaceholder":440},true,"\n",[336,443,445],{"class":338,"line":444},3,[336,446,448],{"class":447},"sPyeA","\u002F\u002F Uint8Array(6) [228, 184, 173, 230, 150, 135]\n",[322,450,451],{},"轉回文字：",[326,453,455],{"className":328,"code":454,"language":330,"meta":331,"style":331},"new TextDecoder().decode(bytes)\n\n\u002F\u002F '中文'\n",[333,456,457,473,477],{"__ignoreMap":331},[336,458,459,462,465,467,470],{"class":338,"line":339},[336,460,461],{"class":407},"new",[336,463,464],{"class":342}," TextDecoder",[336,466,424],{"class":346},[336,468,469],{"class":342},"decode",[336,471,472],{"class":346},"(bytes)\n",[336,474,475],{"class":338,"line":437},[336,476,441],{"emptyLinePlaceholder":440},[336,478,479],{"class":338,"line":444},[336,480,481],{"class":447},"\u002F\u002F '中文'\n",[322,483,484,486,487,490],{},[333,485,92],{}," 使用 UTF-8；",[333,488,489],{},"TextDecoder"," 則負責將位元組重新轉回文字。",[380,492,494],{"id":493},"位元組-base64","位元組 ↔ Base64",[322,496,497,498,500],{},"現在 ",[333,499,95],{}," 可以直接處理 Base64：",[326,502,504],{"className":328,"code":503,"language":330,"meta":331,"style":331},"const bytes = new TextEncoder().encode('中文')\n\nbytes.toBase64()\n\u002F\u002F '5Lit5paH'\n\nUint8Array.fromBase64('5Lit5paH')\n\u002F\u002F Uint8Array(6) [...]\n",[333,505,506,528,532,543,549,554,570],{"__ignoreMap":331},[336,507,508,510,512,514,516,518,520,522,524,526],{"class":338,"line":339},[336,509,408],{"class":407},[336,511,412],{"class":411},[336,513,415],{"class":407},[336,515,418],{"class":407},[336,517,421],{"class":342},[336,519,424],{"class":346},[336,521,427],{"class":342},[336,523,347],{"class":346},[336,525,361],{"class":360},[336,527,434],{"class":346},[336,529,530],{"class":338,"line":437},[336,531,441],{"emptyLinePlaceholder":440},[336,533,534,537,540],{"class":338,"line":444},[336,535,536],{"class":346},"bytes.",[336,538,539],{"class":342},"toBase64",[336,541,542],{"class":346},"()\n",[336,544,546],{"class":338,"line":545},4,[336,547,548],{"class":447},"\u002F\u002F '5Lit5paH'\n",[336,550,552],{"class":338,"line":551},5,[336,553,441],{"emptyLinePlaceholder":440},[336,555,557,560,563,565,568],{"class":338,"line":556},6,[336,558,559],{"class":346},"Uint8Array.",[336,561,562],{"class":342},"fromBase64",[336,564,347],{"class":346},[336,566,567],{"class":360},"'5Lit5paH'",[336,569,434],{"class":346},[336,571,573],{"class":338,"line":572},7,[336,574,575],{"class":447},"\u002F\u002F Uint8Array(6) [...]\n",[322,577,578],{},"完整寫法：",[326,580,582],{"className":328,"code":581,"language":330,"meta":331,"style":331},"function encodeBase64(text) {\n  return new TextEncoder().encode(text).toBase64()\n}\n\nfunction decodeBase64(base64) {\n  return new TextDecoder().decode(\n    Uint8Array.fromBase64(base64)\n  )\n}\n\nencodeBase64('中文 👍')\n\u002F\u002F '5Lit5paHIPCfkY0='\n\ndecodeBase64('5Lit5paHIPCfkY0=')\n\u002F\u002F '中文 👍'\n",[333,583,584,600,620,625,629,643,658,668,674,679,684,697,703,708,721],{"__ignoreMap":331},[336,585,586,589,592,594,597],{"class":338,"line":339},[336,587,588],{"class":407},"function",[336,590,591],{"class":342}," encodeBase64",[336,593,347],{"class":346},[336,595,392],{"class":596},"sjlk-",[336,598,599],{"class":346},") {\n",[336,601,602,605,607,609,611,613,616,618],{"class":338,"line":437},[336,603,604],{"class":407},"  return",[336,606,418],{"class":407},[336,608,421],{"class":342},[336,610,424],{"class":346},[336,612,427],{"class":342},[336,614,615],{"class":346},"(text).",[336,617,539],{"class":342},[336,619,542],{"class":346},[336,621,622],{"class":338,"line":444},[336,623,624],{"class":346},"}\n",[336,626,627],{"class":338,"line":545},[336,628,441],{"emptyLinePlaceholder":440},[336,630,631,633,636,638,641],{"class":338,"line":551},[336,632,588],{"class":407},[336,634,635],{"class":342}," decodeBase64",[336,637,347],{"class":346},[336,639,640],{"class":596},"base64",[336,642,599],{"class":346},[336,644,645,647,649,651,653,655],{"class":338,"line":556},[336,646,604],{"class":407},[336,648,418],{"class":407},[336,650,464],{"class":342},[336,652,424],{"class":346},[336,654,469],{"class":342},[336,656,657],{"class":346},"(\n",[336,659,660,663,665],{"class":338,"line":572},[336,661,662],{"class":346},"    Uint8Array.",[336,664,562],{"class":342},[336,666,667],{"class":346},"(base64)\n",[336,669,671],{"class":338,"line":670},8,[336,672,673],{"class":346},"  )\n",[336,675,677],{"class":338,"line":676},9,[336,678,624],{"class":346},[336,680,682],{"class":338,"line":681},10,[336,683,441],{"emptyLinePlaceholder":440},[336,685,687,690,692,695],{"class":338,"line":686},11,[336,688,689],{"class":342},"encodeBase64",[336,691,347],{"class":346},[336,693,694],{"class":360},"'中文 👍'",[336,696,434],{"class":346},[336,698,700],{"class":338,"line":699},12,[336,701,702],{"class":447},"\u002F\u002F '5Lit5paHIPCfkY0='\n",[336,704,706],{"class":338,"line":705},13,[336,707,441],{"emptyLinePlaceholder":440},[336,709,711,714,716,719],{"class":338,"line":710},14,[336,712,713],{"class":342},"decodeBase64",[336,715,347],{"class":346},[336,717,718],{"class":360},"'5Lit5paHIPCfkY0='",[336,720,434],{"class":346},[336,722,724],{"class":338,"line":723},15,[336,725,726],{"class":447},"\u002F\u002F '中文 👍'\n",[322,728,729],{},"中文與 emoji 都不需要再另外處理。",[380,731,733],{"id":732},"base64url","Base64URL",[322,735,736],{},"JWT 或放在 URL 裡的 token 常使用 Base64URL。",[322,738,739],{},"它與一般 Base64 的主要差異是：",[326,741,744],{"className":742,"code":743,"language":392,"meta":331},[390],"+ → -\n\u002F → _\n",[333,745,743],{"__ignoreMap":331},[322,747,748,749,752],{},"通常也會省略結尾的 ",[333,750,751],{},"=","。",[322,754,755],{},"現在可以直接指定：",[326,757,759],{"className":328,"code":758,"language":330,"meta":331,"style":331},"bytes.toBase64({\n  alphabet: 'base64url',\n  omitPadding: true\n})\n",[333,760,761,770,781,789],{"__ignoreMap":331},[336,762,763,765,767],{"class":338,"line":339},[336,764,536],{"class":346},[336,766,539],{"class":342},[336,768,769],{"class":346},"({\n",[336,771,772,775,778],{"class":338,"line":437},[336,773,774],{"class":346},"  alphabet: ",[336,776,777],{"class":360},"'base64url'",[336,779,780],{"class":346},",\n",[336,782,783,786],{"class":338,"line":444},[336,784,785],{"class":346},"  omitPadding: ",[336,787,788],{"class":411},"true\n",[336,790,791],{"class":338,"line":545},[336,792,793],{"class":346},"})\n",[322,795,796],{},"解碼：",[326,798,800],{"className":328,"code":799,"language":330,"meta":331,"style":331},"Uint8Array.fromBase64(token, {\n  alphabet: 'base64url'\n})\n",[333,801,802,811,818],{"__ignoreMap":331},[336,803,804,806,808],{"class":338,"line":339},[336,805,559],{"class":346},[336,807,562],{"class":342},[336,809,810],{"class":346},"(token, {\n",[336,812,813,815],{"class":338,"line":437},[336,814,774],{"class":346},[336,816,817],{"class":360},"'base64url'\n",[336,819,820],{"class":338,"line":444},[336,821,793],{"class":346},[380,823,825],{"id":824},"query-stringurlsearchparams","Query string：URLSearchParams",[322,827,828],{},"Base64 與 URL encoding 是不同的東西。",[322,830,831],{},"如果要建立 query string，例如：",[326,833,836],{"className":834,"code":835,"language":392,"meta":331},[390],"\u002Fsearch?q=中文&page=1\n",[333,837,835],{"__ignoreMap":331},[322,839,840,841,843],{},"可以使用 ",[333,842,94],{},"：",[326,845,847],{"className":328,"code":846,"language":330,"meta":331,"style":331},"const params = new URLSearchParams({\n  q: 'A&B 公司',\n  page: '1'\n})\n\nparams.toString()\n\u002F\u002F 'q=A%26B+%E5%85%AC%E5%8F%B8&page=1'\n",[333,848,849,865,875,883,887,891,901],{"__ignoreMap":331},[336,850,851,853,856,858,860,863],{"class":338,"line":339},[336,852,408],{"class":407},[336,854,855],{"class":411}," params",[336,857,415],{"class":407},[336,859,418],{"class":407},[336,861,862],{"class":342}," URLSearchParams",[336,864,769],{"class":346},[336,866,867,870,873],{"class":338,"line":437},[336,868,869],{"class":346},"  q: ",[336,871,872],{"class":360},"'A&B 公司'",[336,874,780],{"class":346},[336,876,877,880],{"class":338,"line":444},[336,878,879],{"class":346},"  page: ",[336,881,882],{"class":360},"'1'\n",[336,884,885],{"class":338,"line":545},[336,886,793],{"class":346},[336,888,889],{"class":338,"line":551},[336,890,441],{"emptyLinePlaceholder":440},[336,892,893,896,899],{"class":338,"line":556},[336,894,895],{"class":346},"params.",[336,897,898],{"class":342},"toString",[336,900,542],{"class":346},[336,902,903],{"class":338,"line":572},[336,904,905],{"class":447},"\u002F\u002F 'q=A%26B+%E5%85%AC%E5%8F%B8&page=1'\n",[322,907,908,909,843],{},"也可以直接搭配 ",[333,910,911],{},"URL",[326,913,915],{"className":328,"code":914,"language":330,"meta":331,"style":331},"const url = new URL('https:\u002F\u002Fexample.com\u002Fsearch')\n\nurl.searchParams.set('q', 'A&B 公司')\nurl.searchParams.append('tag', '前端')\n\nurl.toString()\n",[333,916,917,938,942,962,981,985],{"__ignoreMap":331},[336,918,919,921,924,926,928,931,933,936],{"class":338,"line":339},[336,920,408],{"class":407},[336,922,923],{"class":411}," url",[336,925,415],{"class":407},[336,927,418],{"class":407},[336,929,930],{"class":342}," URL",[336,932,347],{"class":346},[336,934,935],{"class":360},"'https:\u002F\u002Fexample.com\u002Fsearch'",[336,937,434],{"class":346},[336,939,940],{"class":338,"line":437},[336,941,441],{"emptyLinePlaceholder":440},[336,943,944,947,950,952,955,958,960],{"class":338,"line":444},[336,945,946],{"class":346},"url.searchParams.",[336,948,949],{"class":342},"set",[336,951,347],{"class":346},[336,953,954],{"class":360},"'q'",[336,956,957],{"class":346},", ",[336,959,872],{"class":360},[336,961,434],{"class":346},[336,963,964,966,969,971,974,976,979],{"class":338,"line":545},[336,965,946],{"class":346},[336,967,968],{"class":342},"append",[336,970,347],{"class":346},[336,972,973],{"class":360},"'tag'",[336,975,957],{"class":346},[336,977,978],{"class":360},"'前端'",[336,980,434],{"class":346},[336,982,983],{"class":338,"line":551},[336,984,441],{"emptyLinePlaceholder":440},[336,986,987,990,992],{"class":338,"line":556},[336,988,989],{"class":346},"url.",[336,991,898],{"class":342},[336,993,542],{"class":346},[322,995,996],{},"這樣就不用自己手動拼 query string。",[380,998,1000],{"id":999},"urlsearchparams-與-encodeuricomponent","URLSearchParams 與 encodeURIComponent",[322,1002,1003],{},"兩者最容易注意到的差異是空白：",[326,1005,1007],{"className":328,"code":1006,"language":330,"meta":331,"style":331},"encodeURIComponent('a b')\n\u002F\u002F 'a%20b'\n\nnew URLSearchParams({ q: 'a b' }).toString()\n\u002F\u002F 'q=a+b'\n",[333,1008,1009,1020,1025,1029,1047],{"__ignoreMap":331},[336,1010,1011,1013,1015,1018],{"class":338,"line":339},[336,1012,355],{"class":342},[336,1014,347],{"class":346},[336,1016,1017],{"class":360},"'a b'",[336,1019,434],{"class":346},[336,1021,1022],{"class":338,"line":437},[336,1023,1024],{"class":447},"\u002F\u002F 'a%20b'\n",[336,1026,1027],{"class":338,"line":444},[336,1028,441],{"emptyLinePlaceholder":440},[336,1030,1031,1033,1035,1038,1040,1043,1045],{"class":338,"line":545},[336,1032,461],{"class":407},[336,1034,862],{"class":342},[336,1036,1037],{"class":346},"({ q: ",[336,1039,1017],{"class":360},[336,1041,1042],{"class":346}," }).",[336,1044,898],{"class":342},[336,1046,542],{"class":346},[336,1048,1049],{"class":338,"line":551},[336,1050,1051],{"class":447},"\u002F\u002F 'q=a+b'\n",[322,1053,1054],{},"簡單記：",[1056,1057,1058,1064],"ul",{},[1059,1060,1061,1062],"li",{},"query string → ",[333,1063,94],{},[1059,1065,1066,1067],{},"URL 路徑片段 → ",[333,1068,355],{},[322,1070,1071],{},"例如：",[326,1073,1075],{"className":328,"code":1074,"language":330,"meta":331,"style":331},"const path = `\u002Fposts\u002F${encodeURIComponent(slug)}`\n",[333,1076,1077],{"__ignoreMap":331},[336,1078,1079,1081,1084,1086,1089,1091,1093,1096,1099],{"class":338,"line":339},[336,1080,408],{"class":407},[336,1082,1083],{"class":411}," path",[336,1085,415],{"class":407},[336,1087,1088],{"class":360}," `\u002Fposts\u002F${",[336,1090,355],{"class":342},[336,1092,347],{"class":360},[336,1094,1095],{"class":346},"slug",[336,1097,1098],{"class":360},")",[336,1100,1101],{"class":360},"}`\n",[322,1103,1104,1105,1107],{},"不要把 ",[333,1106,94],{}," 拿來處理 URL path。",[380,1109,1111],{"id":1110},"textencoder-與-cryptosubtle","TextEncoder 與 crypto.subtle",[322,1113,1114,1117],{},[333,1115,1116],{},"crypto.subtle"," 處理的是二進位資料。",[322,1119,1120],{},"如果要計算文字的 SHA-256，需要先把字串轉成 UTF-8 位元組：",[326,1122,1124],{"className":328,"code":1123,"language":330,"meta":331,"style":331},"async function sha256(text) {\n  const bytes = new TextEncoder().encode(text)\n\n  const digest = await crypto.subtle.digest(\n    'SHA-256',\n    bytes\n  )\n\n  return new Uint8Array(digest).toHex()\n}\n",[333,1125,1126,1143,1163,1167,1187,1194,1199,1203,1207,1224],{"__ignoreMap":331},[336,1127,1128,1131,1134,1137,1139,1141],{"class":338,"line":339},[336,1129,1130],{"class":407},"async",[336,1132,1133],{"class":407}," function",[336,1135,1136],{"class":342}," sha256",[336,1138,347],{"class":346},[336,1140,392],{"class":596},[336,1142,599],{"class":346},[336,1144,1145,1148,1150,1152,1154,1156,1158,1160],{"class":338,"line":437},[336,1146,1147],{"class":407},"  const",[336,1149,412],{"class":411},[336,1151,415],{"class":407},[336,1153,418],{"class":407},[336,1155,421],{"class":342},[336,1157,424],{"class":346},[336,1159,427],{"class":342},[336,1161,1162],{"class":346},"(text)\n",[336,1164,1165],{"class":338,"line":444},[336,1166,441],{"emptyLinePlaceholder":440},[336,1168,1169,1171,1174,1176,1179,1182,1185],{"class":338,"line":545},[336,1170,1147],{"class":407},[336,1172,1173],{"class":411}," digest",[336,1175,415],{"class":407},[336,1177,1178],{"class":407}," await",[336,1180,1181],{"class":346}," crypto.subtle.",[336,1183,1184],{"class":342},"digest",[336,1186,657],{"class":346},[336,1188,1189,1192],{"class":338,"line":551},[336,1190,1191],{"class":360},"    'SHA-256'",[336,1193,780],{"class":346},[336,1195,1196],{"class":338,"line":556},[336,1197,1198],{"class":346},"    bytes\n",[336,1200,1201],{"class":338,"line":572},[336,1202,673],{"class":346},[336,1204,1205],{"class":338,"line":670},[336,1206,441],{"emptyLinePlaceholder":440},[336,1208,1209,1211,1213,1216,1219,1222],{"class":338,"line":676},[336,1210,604],{"class":407},[336,1212,418],{"class":407},[336,1214,1215],{"class":342}," Uint8Array",[336,1217,1218],{"class":346},"(digest).",[336,1220,1221],{"class":342},"toHex",[336,1223,542],{"class":346},[336,1225,1226],{"class":338,"line":681},[336,1227,624],{"class":346},[322,1229,1230,1231,1234,1235,1237,1238,752],{},"如果原本拿到的就是 ",[333,1232,1233],{},"ArrayBuffer"," 或 ",[333,1236,95],{},"，就不需要再經過 ",[333,1239,92],{},[380,1241,1242],{"id":1242},"簡單對照",[1244,1245,1246,1259],"table",{},[1247,1248,1249],"thead",{},[1250,1251,1252,1256],"tr",{},[1253,1254,1255],"th",{},"要做的事",[1253,1257,1258],{},"現在建議",[1260,1261,1262,1273,1283,1293,1303,1312,1322,1331,1341],"tbody",{},[1250,1263,1264,1268],{},[1265,1266,1267],"td",{},"字串 → UTF-8 位元組",[1265,1269,1270],{},[333,1271,1272],{},"new TextEncoder().encode(str)",[1250,1274,1275,1278],{},[1265,1276,1277],{},"UTF-8 位元組 → 字串",[1265,1279,1280],{},[333,1281,1282],{},"new TextDecoder().decode(bytes)",[1250,1284,1285,1288],{},[1265,1286,1287],{},"字串 → Base64",[1265,1289,1290],{},[333,1291,1292],{},"new TextEncoder().encode(str).toBase64()",[1250,1294,1295,1298],{},[1265,1296,1297],{},"Base64 → 字串",[1265,1299,1300],{},[333,1301,1302],{},"new TextDecoder().decode(Uint8Array.fromBase64(base64))",[1250,1304,1305,1307],{},[1265,1306,733],{},[1265,1308,1309],{},[333,1310,1311],{},"{ alphabet: 'base64url' }",[1250,1313,1314,1317],{},[1265,1315,1316],{},"位元組 → Hex",[1265,1318,1319],{},[333,1320,1321],{},"bytes.toHex()",[1250,1323,1324,1327],{},[1265,1325,1326],{},"Query string",[1265,1328,1329],{},[333,1330,94],{},[1250,1332,1333,1336],{},[1265,1334,1335],{},"URL 路徑片段",[1265,1337,1338],{},[333,1339,1340],{},"encodeURIComponent()",[1250,1342,1343,1350],{},[1265,1344,1345,1347,1348],{},[333,1346,370],{}," \u002F ",[333,1349,374],{},[1265,1351,1352],{},"不再使用",[380,1354,1355],{"id":1355},"小結",[322,1357,1358],{},"現在處理文字 Base64，可以先記住這兩個方向：",[326,1360,1362],{"className":328,"code":1361,"language":330,"meta":331,"style":331},"\u002F\u002F 文字 → Base64\nnew TextEncoder().encode(text).toBase64()\n\n\u002F\u002F Base64 → 文字\nnew TextDecoder().decode(\n  Uint8Array.fromBase64(base64)\n)\n",[333,1363,1364,1369,1385,1389,1394,1406,1415],{"__ignoreMap":331},[336,1365,1366],{"class":338,"line":339},[336,1367,1368],{"class":447},"\u002F\u002F 文字 → Base64\n",[336,1370,1371,1373,1375,1377,1379,1381,1383],{"class":338,"line":437},[336,1372,461],{"class":407},[336,1374,421],{"class":342},[336,1376,424],{"class":346},[336,1378,427],{"class":342},[336,1380,615],{"class":346},[336,1382,539],{"class":342},[336,1384,542],{"class":346},[336,1386,1387],{"class":338,"line":444},[336,1388,441],{"emptyLinePlaceholder":440},[336,1390,1391],{"class":338,"line":545},[336,1392,1393],{"class":447},"\u002F\u002F Base64 → 文字\n",[336,1395,1396,1398,1400,1402,1404],{"class":338,"line":551},[336,1397,461],{"class":407},[336,1399,464],{"class":342},[336,1401,424],{"class":346},[336,1403,469],{"class":342},[336,1405,657],{"class":346},[336,1407,1408,1411,1413],{"class":338,"line":556},[336,1409,1410],{"class":346},"  Uint8Array.",[336,1412,562],{"class":342},[336,1414,667],{"class":346},[336,1416,1417],{"class":338,"line":572},[336,1418,434],{"class":346},[322,1420,1421],{},"核心概念就是：",[326,1423,1426],{"className":1424,"code":1425,"language":392,"meta":331},[390],"文字\n↕\nUTF-8 位元組\n↕\nBase64\n",[333,1427,1425],{"__ignoreMap":331},[322,1429,1430,1432],{},[333,1431,94],{}," 則是用來處理 URL query string，和 Base64 是不同用途。",[380,1434,1435],{"id":1435},"參考資料",[1056,1437,1438,1447,1454,1461,1468],{},[1059,1439,1440],{},[1441,1442,1446],"a",{"href":1443,"rel":1444},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FTextEncoder",[1445],"nofollow","MDN：TextEncoder",[1059,1448,1449],{},[1441,1450,1453],{"href":1451,"rel":1452},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FTextDecoder",[1445],"MDN：TextDecoder",[1059,1455,1456],{},[1441,1457,1460],{"href":1458,"rel":1459},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FJavaScript\u002FReference\u002FGlobal_Objects\u002FUint8Array\u002FtoBase64",[1445],"MDN：Uint8Array.prototype.toBase64",[1059,1462,1463],{},[1441,1464,1467],{"href":1465,"rel":1466},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FURLSearchParams",[1445],"MDN：URLSearchParams",[1059,1469,1470],{},[1441,1471,1474],{"href":1472,"rel":1473},"https:\u002F\u002Fdeveloper.mozilla.org\u002Fen-US\u002Fdocs\u002FWeb\u002FAPI\u002FSubtleCrypto\u002Fdigest",[1445],"MDN：SubtleCrypto.digest",[1476,1477,1478],"style",{},"html pre.shiki code .sVjZL, html code.shiki .sVjZL{--shiki-default:#6F42C1;--shiki-dark:#D2A8FF}html pre.shiki code .szmH1, html code.shiki .szmH1{--shiki-default:#24292E;--shiki-dark:#E6EDF3}html pre.shiki code .sNbzA, html code.shiki .sNbzA{--shiki-default:#032F62;--shiki-dark:#A5D6FF}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 .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}html pre.shiki code .sPyeA, html code.shiki .sPyeA{--shiki-default:#6A737D;--shiki-dark:#8B949E}html pre.shiki code .sjlk-, html code.shiki .sjlk-{--shiki-default:#E36209;--shiki-dark:#FFA657}",{"title":331,"searchDepth":444,"depth":444,"links":1480},[1481,1482,1483,1484,1485,1486,1487,1488,1489],{"id":382,"depth":437,"text":383},{"id":493,"depth":437,"text":494},{"id":732,"depth":437,"text":733},{"id":824,"depth":437,"text":825},{"id":999,"depth":437,"text":1000},{"id":1110,"depth":437,"text":1111},{"id":1242,"depth":437,"text":1242},{"id":1355,"depth":437,"text":1355},{"id":1435,"depth":437,"text":1435},false,"md",null,{},"2026-03-25",[1496,1497,1498],"base64-converter","url-encoder","jwt-decoder",{"title":87,"description":88},"posts\u002F20260325.js-encoding-today",[91,92,93,94,95,18],"2026-09-08","iGobSyWIwtqSzVS0xq06QypgIqvdhdtfLyVg9VwitCE",[1505,1508],{"title":98,"path":97,"stem":1506,"description":99,"category":16,"publishedAt":1507,"children":-1},"posts\u002F20260407.dart-sass-migration","2026-04-07",{"title":79,"path":78,"stem":1509,"description":80,"category":16,"publishedAt":1510,"children":-1},"posts\u002F20250808.playwright-vs-cypress","2025-08-08",1791786901369]