[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"blog-categories":3,"blog-category-frontend":78},[4,10,14,19,24,30,36,40,45,51,55,58,62,68,73],{"id":5,"databaseId":6,"name":7,"slug":8,"count":9},"dGVybTo1NQ==",55,"AI","ai",2,{"id":11,"databaseId":12,"name":13,"slug":13,"count":9},"dGVybTo2MQ==",61,"backend",{"id":15,"databaseId":16,"name":17,"slug":18,"count":16},"dGVybToyMA==",20,"Devops","devops",{"id":20,"databaseId":21,"name":22,"slug":22,"count":23},"dGVybToxNQ==",15,"frontend",25,{"id":25,"databaseId":26,"name":27,"slug":28,"count":29},"dGVybTo2MA==",60,"GAS","gas",1,{"id":31,"databaseId":32,"name":33,"slug":34,"count":35},"dGVybTo5",9,"HTML","html",4,{"id":37,"databaseId":38,"name":39,"slug":39,"count":29},"dGVybToyMg==",22,"mac",{"id":41,"databaseId":42,"name":43,"slug":43,"count":44},"dGVybTo2",6,"node",18,{"id":46,"databaseId":47,"name":48,"slug":49,"count":50},"dGVybTo2OQ==",69,"Nuxt","nuxt",3,{"id":52,"databaseId":53,"name":54,"slug":54,"count":50},"dGVybTo1NA==",54,"office",{"id":56,"databaseId":9,"name":57,"slug":57,"count":21},"dGVybToy","omekas",{"id":59,"databaseId":60,"name":61,"slug":61,"count":16},"dGVybToxNg==",16,"vue",{"id":63,"databaseId":64,"name":65,"slug":66,"count":67},"dGVybTo1",5,"WordPress","wordpress",13,{"id":69,"databaseId":29,"name":70,"slug":71,"count":72},"dGVybTox","無","uncategorized",null,{"id":74,"databaseId":75,"name":76,"slug":77,"count":9},"dGVybTo3MA==",70,"生活","life",[79,92,106,119,132,143,155,166,177,190,202,215,229,240,252,266,277,289,302,313,325,337,350,362,375,387,399,412,424,436,448,460,472],{"id":80,"slug":81,"title":82,"description":83,"content":84,"date":85,"image":86,"imageAlt":82,"author":87,"categories":89,"path":91,"minRead":9},3904,"checkmarx-client-dynamic-file-inclusion","[筆記] Checkmarx 中遇到 Client Dynamic File Inclusion 問題","前端專案中遇到 Client Dynamic File Inclusion 的高風險回報,出問題的地方是在這樣 […]","\u003Cp>前端專案中遇到 Client Dynamic File Inclusion 的高風險回報,出問題的地方是在這樣的地方,舉例來說是一個表單詢問:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-javascript\">async submitForm(name){\n\n  const sendData = new FormData()\n  formData.append('name', name)  \u002F\u002F 出問題\n  const response = await axios.post(\n    API_CONFIG.baseURL + '&lt;api endpoint&gt;',\n    sendData,\n    {\n      headers: {\n        'Content-Type': 'application\u002Fx-www-form-urlencoded'\n      }\n    }\n  )\n\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>其中的 \u003Ccode>append\u003C\u002Fcode> 被認為有 Client Dynamic File Inclusion 風險:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-javascript\">formData.append('name', name)\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2>先來看看他的說明\u003C\u002Fh2>\n\u003Ch3>可能發生什麼問題\u003C\u002Fh3>\n\u003Cblockquote>\n\u003Cp>If an attacker can select the name of the library, or the location of the code file that is loaded by the application, they would be able to cause the application to execute arbitrary code. This effectively allows the attacker to control the code run by the application. Execution of code in web application context may result with Cross-Site Scripting.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>可能發生的問題是:如果攻擊者可以選擇應用程式要載入的程式庫名稱或程式碼檔案位置,他們就能讓應用程式執行任意的惡意程式碼。這等於讓攻擊者能控制應用程式執行的內容。在網頁應用程式的執行環境中,這種情況可能導致跨網站指令碼攻擊(XSS, Cross-Site Scripting)。\u003C\u002Fp>\n\u003Ch3>如何發生\u003C\u002Fh3>\n\u003Cblockquote>\n\u003Cp>The application uses untrusted data to specify the library or code file, without proper sanitization. This causes the application to load any arbitrary code, as specified. The loaded code will then be executed.\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>發生的情況是:應用程式在指定要載入的程式庫或程式碼檔案時,使用了未受信任的資料(例如來自使用者的輸入),而且沒有進行適當的驗證或過濾。結果導致應用程式可能載入並執行任何由攻擊者指定的惡意程式碼。\u003C\u002Fp>\n\u003Ch3>如何避免\u003C\u002Fh3>\n\u003Cul>\n\u003Cli>不要動態載入程式庫或程式碼檔案,特別是不要根據使用者輸入動態載入。\u003C\u002Fli>\n\u003Cli>如果確實有必要根據未受信任的資料來選擇要載入的程式庫,應該驗證該資料是否屬於預先定義的白名單(whitelist)中的合法項目。或者,將使用者輸入作為「識別碼(identifier)」來對應選擇白名單內的安全程式庫。\u003C\u002Fli>\n\u003Cli>對所有未受信任的資料進行驗證與完整性檢查(integrity check),確保載入或處理的程式庫、檔案是可信任且未被竄改的。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>然後有一些範例如下:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-javascript\">&lt;script&gt;\nconst queryString = window.location.search;\nconst urlParams = new URLSearchParams(queryString);\nconst lib = urlParams.get('lib');\nvar script = document.createElement(\"script\");\nconst safeDomain = 'https:\u002F\u002F' + TRUST_DOMAIN_NAME + '\u002F';\nscript.src = safeDomain + lib;\nvar element = document.getElementById(DIV_ID);\nelement.appendChild(script);\n&lt;\u002Fscript&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>在 Stack Overflow 中可以找到類似的討論:\u003Cbr \u002F>\n\u003Ca href=\"https:\u002F\u002Fstackoverflow.com\u002Fquestions\u002F76356081\u002Fhow-to-mitigate-against-client-dynamic-file-inclusion-vulnerability-detected-by\" target=\"_blank\" rel=\"noopener\">How to mitigate against Client Dynamic File Inclusion vulnerability detected by Checkmarx\u003C\u002Fa>\u003C\u002Fp>\n\u003Ch2>心得\u003C\u002Fh2>\n\u003Cp>說明滿清楚的,但是很納悶為什麼是 \u003Ccode>append\u003C\u002Fcode> 出問題,合理懷疑是因為 Checkmarx 認為這邊的 \u003Ccode>append\u003C\u002Fcode> 跟 jQuery 的 \u003Ccode>append\u003C\u002Fcode> 是一樣的,但我的理解是 \u003Ccode>formData.append()\u003C\u002Fcode> 不會直接執行內容,不會造成動態載入的狀況。\u003C\u002Fp>\n\u003Cp>不過目前的解決方式就是不要用 \u003Ccode>append\u003C\u002Fcode> 來處理 formData,而是用一次性賦值的做法來打包 formData。\u003C\u002Fp>\n","2026-08-12T18:10:18","",{"name":88,"link":86},"橫隔膜",[90],{"name":22,"slug":22},"\u002Fblog\u002Fcheckmarx-client-dynamic-file-inclusion",{"id":93,"slug":94,"title":95,"description":96,"content":97,"date":98,"image":86,"imageAlt":95,"author":99,"categories":102,"path":105,"minRead":29},3818,"個人網站改版建置心得-headless-wordpress-nuxt","個人網站改版建置心得 Headless WordPress + Nuxt","一直聽過 WordPress 的無頭模式(Headless)架構，最近心一狠就把自己的網站轉移成無頭模式架構。 […]","\u003Cp>一直聽過 WordPress 的無頭模式(Headless)架構，最近心一狠就把自己的網站轉移成無頭模式架構。\u003C\u002Fp>\n\u003Cp>理由有幾點：\u003C\u002Fp>\n\u003Col>\n\u003Cli>想要更有彈性的在前台開發\u003C\u002Fli>\n\u003Cli>在前台實作一些想法\u003C\u002Fli>\n\u003Cli>體驗看看無頭模式的坑(?\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>記錄一下目前版本做了哪些事情\u003C\u002Fp>\n\u003Ch1>轉換流程\u003C\u002Fh1>\n\u003Ch2>1. 決定架構\u003C\u002Fh2>\n\u003Cp>我決定了前台採用 Nuxt ，一方面我是走 Vue 生態的開發者，二來需要有 SSR 來維持原本的 SEO 效果。\u003C\u002Fp>\n\u003Cp>簡單來說就是 Nuxt + WordPress\u003C\u002Fp>\n\u003Ch2>2. 開啟 WordPress 無頭模式\u003C\u002Fh2>\n\u003Cp>翻了一些文章，多半會推薦無頭模式要採用 Graphql ，主要是為了提高查詢效率，因此先安裝了 wpgraphql ，讓 WordPress 的 RestFul API  支援 Graphql。\u003C\u002Fp>\n\u003Ch2>3. 建置新的前台\u003C\u002Fh2>\n\u003Cp>用 Nuxt 建置了新的前台，目前版本只是先提供最基礎的內容：首頁、介紹、文章等，後續還有一些想法慢慢更新。這時實作串接原網站的 API，就能進行測試了！\u003C\u002Fp>\n\u003Cp>記得要開啟 SSR 模式，這部分除錯比較麻煩一些。\u003C\u002Fp>\n\u003Cp>另外也要處理 CORS 問題，這部分解完後，整個架構雛形就完成了。\u003C\u002Fp>\n\u003Ch2>4. 網域轉換 + SEO\u003C\u002Fh2>\n\u003Cp>將原本的 WordPress 轉換到另一個網域，並且將新前台掛在原本的網域。\u003C\u002Fp>\n\u003Cp>由於新架構的 route 已經改變，自然就會出現文章路徑的轉址問題，我另外寫了一個轉換器和對應表來處理 301 轉址。\u003C\u002Fp>\n\u003Cp>而 SEO 就要在 Nuxt 上全部處理了。\u003C\u002Fp>\n\u003Ch2>5. 關閉 WordPress 的前台模式\u003C\u002Fh2>\n\u003Cp>說是關閉，我採用的做法是強制轉址到前台網域billxu.net 啦.\u003C\u002Fp>\n\u003Cp>這些步驟做完，也差不多把整個架構調完了！\u003C\u002Fp>\n\u003Ch1>簡短心得\u003C\u002Fh1>\n\u003Cp>轉換完後有種舒爽的感覺，想到不用受到原本頁面架構的限制就覺得有種解放感！不過什麼都得重新來過，像是文章排版、資料解析等等。最近還在除錯，之後想在網站中放一些小遊戲之類的(? 到時候就再分享了XD\u003C\u002Fp>\n","2026-01-18T15:29:46",{"name":100,"link":101},"Chih Fan","http:\u002F\u002Fpost.billxu.net",[103,104],{"name":48,"slug":49},{"name":65,"slug":66},"\u002Fblog\u002F個人網站改版建置心得-headless-wordpress-nuxt",{"id":107,"slug":108,"title":109,"description":110,"content":111,"date":112,"image":113,"imageAlt":109,"author":114,"categories":115,"path":118,"minRead":9},3786,"理解與前端實作-recaptcha-v3","理解與前端實作 reCAPTCHA v3","理解與實作 recaptcha Google reCAPTCHA 是一種常見的機器人驗證技術，用來區分人類使用 […]","\u003Ch1>理解與實作 recaptcha\u003C\u002Fh1>\n\u003Cp>Google reCAPTCHA  是一種常見的機器人驗證技術，用來區分人類使用者和自動化程式。現在主流有 v2 和 v3 兩種版本，是擁有免費額度的付費服務。\u003Cbr \u002F>\n先前都只有透過打包好的 WordPress 外掛來實作，這次有機會在Nuxt3的前端專案中自己寫，就趁機記錄一下自己的理解與過程。\u003C\u002Fp>\n\u003Cp>本次使用的是 reCAPTCHA v3 ，特點在於不用跟介面互動就能取得辨識結果，好處就是網站對使用者的驗證只會在背景處理，不影響使用體驗。\u003C\u002Fp>\n\u003Cp>reCAPTCHA v3的技術原理大概可以這樣理解：\u003C\u002Fp>\n\u003Cul>\n\u003Cli>行為分析：評估使用者與頁面的互動方式，如滑鼠移動模式、點擊方式和速度。\u003C\u002Fli>\n\u003Cli>瀏覽器指紋識別：收集瀏覽器和設備資訊，如作業系統、瀏覽器類型、螢幕解析度等。\u003C\u002Fli>\n\u003Cli>Cookie 和歷史數據：分析用戶之前與 Google 服務的互動歷史。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>實作邏輯的話，其實就是跟 reCAPTCHA 服務取用資料這樣。\u003C\u002Fp>\n\u003Ch3>前置：取得金鑰\u003C\u002Fh3>\n\u003Cul>\n\u003Cli>網站金鑰 (Site Key)：公開的，嵌入在網頁前端代碼中\u003C\u002Fli>\n\u003Cli>密鑰 (Secret Key)：私密的，只在伺服器端使用\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch3>實作流程\u003C\u002Fh3>\n\u003Cp>以下是我對  reCAPTCHA 溝通的理解：\u003C\u002Fp>\n\u003Ch4>1. 前端向 Google reCAPTCHA 請求 token\u003C\u002Fh4>\n\u003Cp>首先註冊並申請 Google reCAPTCHA 服務。\u003Cbr \u002F>\n網站前端載入 reCAPTCHA SDK，當使用者執行需驗證的操作時，呼叫grecaptcha.execute()， reCAPTCHA 服務會分析使用者行為後回傳 token。\u003C\u002Fp>\n\u003Ch4>2. 前端向網站伺服器提交請求\u003C\u002Fh4>\n\u003Cp>前端將 reCAPTCHA token 連同表單數據或 API 請求一起發送給後端。\u003C\u002Fp>\n\u003Ch4>3. 伺服器向 Google reCAPTCHA 驗證 token\u003C\u002Fh4>\n\u003Cp>後端使用密鑰(secret key)向 Google reCAPTCHA API 發送驗證請求。\u003C\u002Fp>\n\u003Ch4>4. Google reCAPTCHA 返回驗證結果\u003C\u002Fh4>\n\u003Cp>JSON 格式的回應包含:\u003Cbr \u002F>\nsuccess: 驗證是否成功(true\u002Ffalse)\u003Cbr \u002F>\nscore: 闕值，人類可能性分數(0.0-1.0)\u003Cbr \u002F>\naction: 觸發驗證的行為名稱\u003Cbr \u002F>\nchallenge_ts: 時間戳記\u003Cbr \u002F>\nhostname: 網站域名\u003C\u002Fp>\n\u003Ch3>閾值判斷：\u003C\u002Fh3>\n\u003Cp>我這次使用的 reCAPTCHA v3 會回傳 0.0 到 1.0 之間的分數，此分數表示使用者行為的「人類可能性」，管理者可自行決定接受的閾值（例如 0.5）高於閾值就允許後續操作，低於則可能需要額外驗證或拒絕請求。\u003C\u002Fp>\n\u003Cp>\u003Ca href=\"https:\u002F\u002Fpost.billxu.net\u002Fwp-content\u002Fuploads\u002F2025\u002F04\u002Fdiagram.svg\">\u003Cimg decoding=\"async\" src=\"\u002Fapi\u002Fproxy-image?url=https%3A%2F%2Fpost.billxu.net%2Fwp-content%2Fuploads%2F2025%2F04%2Fdiagram.svg\" alt=\"\" \u002F>\u003C\u002Fa>\u003C\u002Fp>\n\u003Ch2>實作紀錄：\u003C\u002Fh2>\n\u003Cp>我使用 vue-recaptcha-v3 ，自行建立 plugin 管理與啟動 reCAPTCHA，因為只有前端實作，所以很快就處理好了。\u003C\u002Fp>\n\u003Cp>recaptcha.client.js\u003C\u002Fp>\n\u003Cpre>\u003Ccode>import { VueReCaptcha } from &#039;vue-recaptcha-v3&#039;\n\nexport default defineNuxtPlugin(async (nuxtApp) =&gt; {\n  const config = useRuntimeConfig()\n\n  \u002F\u002F 確保 siteKey 存在並輸出調試信息\n  console.log(&#039;Initializing reCAPTCHA with config:&#039;, {\n    siteKey: config.public.recaptchaSiteKey,\n    isDevelopment: process.env.NODE_ENV === &#039;development&#039;\n  })\n\n  if (!config.public.recaptchaSiteKey) {\n    console.error(&#039;reCAPTCHA site key is missing&#039;)\n    return\n  }\n\n  try {\n    await nuxtApp.vueApp.use(VueReCaptcha, {\n      siteKey: config.public.recaptchaSiteKey,\n      loaderOptions: {\n        useRecaptchaNet: true, \u002F\u002F 從 recaptcha.net 載入 script\n        autoHideBadge: false,\u002F\u002F 隱藏標籤\n        explicitRenderParameters: {\n          badge: &#039;bottomright&#039;,\n          size: &#039;invisible&#039;\n        }\n      }\n    })\n\n    \u002F\u002F 驗證 reCAPTCHA 是否正確載入\n    const recaptcha = await window.grecaptcha\n    if (!recaptcha) {\n      throw new Error(&#039;reCAPTCHA not loaded properly&#039;)\n    }\n\n    console.log(&#039;reCAPTCHA plugin initialized successfully&#039;)\n  } catch (error) {\n    console.error(&#039;Failed to initialize reCAPTCHA:&#039;, error)\n  }\n}) \u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>然後在 .env 和 nuxt.config 放好 API_KEY\u003C\u002Fp>\n\u003Cp>例如在登入時就宣告上述的流程即可\u003C\u002Fp>\n\u003Cpre>\u003Ccode>import { useReCaptcha } from &#039;vue-recaptcha-v3&#039;\nconst { executeRecaptcha, recaptchaLoaded } = useReCaptcha()\n\nconst handleLogin = async (e) =&gt; {\n\n\u002F\u002F 執行 reCAPTCHA 驗證\nconsole.log(&#039;開始執行 reCAPTCHA 驗證&#039;)\nconst recaptchaToken = await executeRecaptcha(&#039;login&#039;)\nconsole.log(&#039;recaptchaToken:&#039;, recaptchaToken)\n\n\u002F\u002F 發送登入請求\nconst response = await $fetch(`\u002Fapi\u002Flogin`, {\n    method: &#039;POST&#039;,\n        body: {\n        account: account.value,\n        password: password.value,\n        recaptchaToken: recaptchaToken\n        }\n    })\nif (response) {\n\u002F\u002F後端回傳闕值做後續處理\n}\n\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>後端就要另外寫一隻配合密鑰的請求去詢問結果。\u003C\u002Fp>\n\u003Cp>成功載入的話，網站會出現 reCAPTCHA 的標籤，預設在右下方。\u003C\u002Fp>\n","2025-04-02T12:29:05","\u002Fapi\u002Fproxy-image?url=https%3A%2F%2Fpost.billxu.net%2Fwp-content%2Fuploads%2F2024%2F09%2FNuxt_logo.svg.png",{"name":100,"link":101},[116,117],{"name":22,"slug":22},{"name":48,"slug":49},"\u002Fblog\u002F理解與前端實作-recaptcha-v3",{"id":120,"slug":121,"title":122,"description":123,"content":124,"date":125,"image":126,"imageAlt":122,"author":127,"categories":128,"path":131,"minRead":29},3775,"nuxt3-vue-3-動態取得圖片","Nuxt3 \u002F Vue 3 動態取得圖片","Nuxt3 \u002F Vue 3 動態取得圖片 參考資料 https:\u002F\u002Fwww.lichter.io\u002Farticl […]","\u003Ch1>Nuxt3 \u002F Vue 3 動態取得圖片\u003C\u002Fh1>\n\u003Cp>參考資料\u003C\u002Fp>\n\u003Cp>\u003Ca href=\"https:\u002F\u002Fwww.lichter.io\u002Farticles\u002Fnuxt3-vue3-dynamic-images\u002Fhttps:\u002F\u002Fclairechang.tw\u002F2023\u002F09\u002F12\u002Fnuxt3\u002Fnuxt-v3-assets-vs-public\u002F\">https:\u002F\u002Fwww.lichter.io\u002Farticles\u002Fnuxt3-vue3-dynamic-images\u002Fhttps:\u002F\u002Fclairechang.tw\u002F2023\u002F09\u002F12\u002Fnuxt3\u002Fnuxt-v3-assets-vs-public\u002F\u003C\u002Fa>\u003C\u002Fp>\n\u003Cp>把圖片放在assets中後，通常來說會使用這兩種寫法\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-jsx\">&lt;img src=&quot;@\u002Fassets\u002Fimage\u002Flogo.jpg&quot;&gt; \n&lt;img src=&quot;..\u002Fassets\u002Fimage\u002Flogo.jpg&quot;&gt;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>但只限於靜態圖片，如果想要動態取用圖片。\u003Cbr \u002F>\n例如有10張圖片，檔名是1~10，例如001.jpg ~ 010.jpg\u003C\u002Fp>\n\u003Cp>以前我偷懶會這樣處理：\u003Cbr \u002F>\n放在 public 中，直接取用\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-jsx\">&lt;img src=&quot;`\u002Fimage\u002F${index}.jpg`&quot;&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>但這種作法有些缺點，\u003C\u002Fp>\n\u003Cul>\n\u003Cli>圖片將被瀏覽器快取住，因此如果我們想要更改圖像，我們也必須更改檔案名稱。這不是最佳的，特別是如果我們想在多個地方使用相同的圖像。\u003C\u002Fli>\n\u003Cli>效能問題，圖片不會被Vite插件之類最佳化。\u003C\u002Fli>\n\u003Cli>圖片將以原始大小加載，如果尺寸很大，同樣也有瀏覽器效能問題。\u003C\u002Fli>\n\u003Cli>管理不一致，這是我的使用習慣，我不喜歡圖片要放在public裡面，比較偏好在assets中一起管理。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>如果換成assets中的話….\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-jsx\">&lt;img src=&quot;..\u002Fassets\u002Fimage\u002F${index}.jpg`&quot;&gt;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>會失效。代表取用不到資源\u003C\u002Fp>\n\u003Cp>如果找解決方式可能會找到 Vue2 時代的作法，加入 \u003Cstrong>require，\u003C\u002Fstrong>例如\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-jsx\">&lt;img :src=&quot;require(`...\u002Fassets\u002Fimage\u002F${index}.jpg`)&quot; :alt=&quot;selectedDog&quot;&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>但這個作法在Vue3 \u002F Nuxt3 不能用了，因為Vite不支援。\u003C\u002Fp>\n\u003Cp>這時要使用另一個Vite的API，\u003Ca href=\"https:\u002F\u002Fvitejs.dev\u002Fguide\u002Ffeatures.html\u002F#glob-import\">\u003Cstrong>\u003Ccode>import.meta.glob\u003C\u002Fcode>\u003C\u002Fstrong>\u003C\u002Fa>\u003C\u002Fp>\n\u003Cp>我的理解，\u003Ca href=\"https:\u002F\u002Fvitejs.dev\u002Fguide\u002Ffeatures.html\u002F#glob-import\">\u003Cstrong>\u003Ccode>import.meta.glob\u003C\u002Fcode>\u003C\u002Fstrong>\u003C\u002Fa>有點像是先整理所有引入資料夾中的檔案，建立成一個索引，然後透過key去對應檔名。\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-jsx\">&lt;script setup lang=&quot;ts&quot;&gt;\n\nconst glob = import.meta.glob(&#039;@\u002Fassets\u002Fimages\u002F*.jpg&#039;, { eager: true })\n&lt;\u002Fscript&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>去檢查 glob 可以看到以下類似資料\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-jsx\">{\n  &quot;\u002Fassets\u002Fimages\u002Flogo_1`.jpg&quot;: {\n    &quot;default&quot;: &quot;\u002F_nuxt\u002Fassets\u002Fimages\u002Flogo_1.jpg&quot;\n  },\n  &quot;\u002Fassets\u002Fimages\u002Flogo_2.jpg&quot;: {\n    &quot;default&quot;: &quot;\u002F_nuxt\u002Fassets\u002Fimages\u002Flogo_2.jpg&quot;\n  },\n  &quot;\u002Fassets\u002Fimages\u002Flogo_3.jpg&quot;: {\n    &quot;default&quot;: &quot;\u002F_nuxt\u002Fassets\u002Fimages\u002Flogo_3.jpg&quot;\n  }\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>這時我們用呼叫的圖檔名去尋找真實位置\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-jsx\">&lt;img :src=&quot;glob[`\u002Fassets\u002Fimages\u002F${index}.jpg`]&quot; alt=&quot;&quot; \u002F&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>當然也有一些缺點\u003C\u002Fp>\n\u003Cul>\n\u003Cli>圖片名稱會因為Vite快取關係被更改。\u003C\u002Fli>\n\u003Cli>同樣也可能有效能消耗，因為先處理了glob的API。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>就看專案目的取決要使用何種作法囉。\u003C\u002Fp>\n","2024-10-01T00:00:25","\u002Fapi\u002Fproxy-image?url=https%3A%2F%2Fpost.billxu.net%2Fwp-content%2Fuploads%2F2024%2F03%2Ficon-green.svg",{"name":100,"link":101},[129,130],{"name":22,"slug":22},{"name":48,"slug":49},"\u002Fblog\u002Fnuxt3-vue-3-動態取得圖片",{"id":133,"slug":134,"title":135,"description":136,"content":137,"date":138,"image":86,"imageAlt":135,"author":139,"categories":140,"path":142,"minRead":29},3772,"html2canvas-在-safari-中遇到錯誤0ms-starting-document-clone","html2canvas  在 safari 中遇到錯誤”0ms Starting document clone”","html2canvas 在 safari 中遇到錯誤\"0ms Starting document c […]","\u003Ch1>html2canvas  在 safari 中遇到錯誤&quot;0ms Starting document clone&quot;\u003C\u002Fh1>\n\u003Cp>記錄一下。\u003Cbr \u002F>\n如果使用 html2canvas 然後在 IOS 的 safari 上遇到卡住，經過查詢發現錯誤為 &quot;&quot;0ms Starting document clone...&quot; 之類的訊息，應該是因為有圖片元素宣告了 lazy loading，將 lazy 移除或改為 eager 大概就能正常運作了。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>&lt;img loading=&quot;eager&quot; src:&quot;xxx&quot;\u002F&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>我遇到問題的環境為 Nuxt3 + html2canvas。\u003C\u002Fp>\n\u003Cp>這個問題應該是因為 html2canvas 在某些狀況下會因為圖片的 lazy 宣告，載入延遲而造成資料卡住，可能新一點的 html2canvas 版本會解決。但我是移除了 lazy 就正常了。\u003C\u002Fp>\n\u003Cp>\u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fniklasvh\u002Fhtml2canvas\u002Fissues\u002F3053\" title=\"參考資料\">參考資料\u003C\u002Fa>\u003C\u002Fp>\n","2024-09-24T21:38:45",{"name":100,"link":101},[141],{"name":22,"slug":22},"\u002Fblog\u002Fhtml2canvas-在-safari-中遇到錯誤0ms-starting-document-clone",{"id":144,"slug":145,"title":146,"description":147,"content":148,"date":149,"image":150,"imageAlt":146,"author":151,"categories":152,"path":154,"minRead":29},3738,"vue3-建立輸入自動完成autocomplete","Vue3 建立輸入自動完成(autocomplete)","Vue 建立輸入自動完成(autocomplete) 參考資料：https:\u002F\u002Fstevencotterill […]","\u003Ch1>Vue 建立輸入自動完成(autocomplete)\u003C\u002Fh1>\n\u003Cp>參考資料：\u003Ca href=\"https:\u002F\u002Fstevencotterill.com\u002Farticles\u002Fhow-to-build-an-autocomplete-field-with-vue-3\">https:\u002F\u002Fstevencotterill.com\u002Farticles\u002Fhow-to-build-an-autocomplete-field-with-vue-3\u003C\u002Fa>\u003C\u002Fp>\n\u003Cp>紀錄在 Vue3 中建立自動完成(autocomplete)功能的基礎作法。我先假設是一個關於不同國家的輸入欄。\u003C\u002Fp>\n\u003Ch2>準備國家資料\u003C\u002Fh2>\n\u003Cp>首先，我們需要一個國家名稱的數。在這個例子中，我們將使用一個靜態的 JSON 文件 countries.json，其中包含了一系列國家名稱：\u003Cstrong>countries.json\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-json\">[\n  {&quot;name&quot;: &quot;Albania&quot;},\n  {&quot;name&quot;: &quot;Åland Islands&quot;},\n  {&quot;name&quot;: &quot;Algeria&quot;},\n  {&quot;name&quot;: &quot;American Samoa&quot;},\n  {&quot;name&quot;: &quot;Andorra&quot;},\n  {&quot;name&quot;: &quot;Angola&quot;},\n  {&quot;name&quot;: &quot;Anguilla&quot;},\n  {&quot;name&quot;: &quot;Antarctica&quot;},\n  {&quot;name&quot;: &quot;Antigua and Barbuda&quot;},\n  {&quot;name&quot;: &quot;Argentina&quot;},\n  {&quot;name&quot;: &quot;Armenia&quot;},\n  {&quot;name&quot;: &quot;Aruba&quot;},\n  {&quot;name&quot;: &quot;Australia&quot;},\n  {&quot;name&quot;: &quot;Austria&quot;},\n]\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2>建立輸入元件\u003C\u002Fh2>\n\u003Cp>接著，我們在 Vue 組件中建立一個輸入框，可以取得使用者輸入\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-html\">&lt;input\n  type=&quot;text&quot;\n  id=&quot;search&quot;\n  v-model=&quot;searchTerm&quot;\n  placeholder=&quot;Type here...&quot;\n  class=&quot;p-3 mb-0.5 w-full border border-gray-300 rounded&quot;\n>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2>實現自動完成下拉菜單\u003C\u002Fh2>\n\u003Cp>當使用者開始輸入時，我們希望顯示一個下拉選單列出文件中的國家名稱。\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-html\">&lt;ul\n  v-if=&quot;searchCountries.length&quot;\n  class=&quot;w-full rounded bg-white border border-gray-300 px-4 py-2 space-y-1 absolute z-10&quot;\n>\n  &lt;li class=&quot;px-1 pt-1 pb-2 font-bold border-b border-gray-200&quot;&gt;\n    Showing {{ searchCountries.length }} of {{ countries.length }} results\n  &lt;\u002Fli&gt;\n  &lt;li\n    v-for=&quot;country in searchCountries&quot;\n    :key=&quot;country.name&quot;\n    @click=&quot;selectCountry(country.name)&quot;\n    class=&quot;cursor-pointer hover:bg-gray-100 p-1&quot;\n  &gt;\n    {{ country.name }}\n  &lt;\u002Fli&gt;\n&lt;\u002Ful&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2>取得配對成功的國家\u003C\u002Fh2>\n\u003Cp>為了配對國家，使用一個函式 searchCountries，它根據目前的 searchTerm 篩選國家列表：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-javascript\">\u002F\u002F輸入的資料\nlet searchTerm = ref(&#039;&#039;)\n\n\u002F\u002F即時檢查輸入資料與偵測資料的 computed\nconst searchCountries = computed(() =&gt; {\n  if (searchTerm.value === &#039;&#039;) {\n    return []\n  }\n\n  let matches = 0\n\n  return countries.filter(country =&gt; {\n    if (country.name.toLowerCase().includes(searchTerm.value.toLowerCase()) &amp;&amp; matches &lt; 10) {\n      matches++\n      return country\n    }\n  })\n});\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2>完成\u003C\u002Fh2>\n\u003Cp>這樣基本上就可以建立一個有自動完成(autocomplete)功能的輸入欄囉～\u003C\u002Fp>\n","2024-04-22T10:34:29","\u002Fapi\u002Fproxy-image?url=https%3A%2F%2Fpost.billxu.net%2Fwp-content%2Fuploads%2F2022%2F11%2F256px-Vue.js_Logo_2.svg_.png",{"name":100,"link":101},[153],{"name":61,"slug":61},"\u002Fblog\u002Fvue3-建立輸入自動完成autocomplete",{"id":156,"slug":157,"title":158,"description":159,"content":160,"date":161,"image":150,"imageAlt":158,"author":162,"categories":163,"path":165,"minRead":9},3705,"在-github-page-上部署-vue-spa-時-vue-route-失效的問題","在 github page 上部署 vue SPA 時 vue-route 失效的問題","在 github page 上部署 vue SPA 時 vue-route 失效的問題 參考資料: Vue R […]","\u003Ch1>在 github page 上部署 vue SPA 時 vue-route 失效的問題\u003C\u002Fh1>\n\u003Cp>參考資料:\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\n\u003Cp>\u003Ca href=\"https:\u002F\u002Fstackoverflow.com\u002Fquestions\u002F65501787\u002Fvue-router-github-pages-and-custom-domain-not-working-with-routed-links\">Vue Router, GitHub Pages, and Custom Domain Not Working With Routed Links\u003C\u002Fa>\u003C\u002Fp>\n\u003C\u002Fli>\n\u003Cli>\n\u003Cp>\u003Ca href=\"https:\u002F\u002Fhuishun-medium-com.translate.goog\u002Fhow-to-deploy-a-vue-js-application-with-dynamic-routing-on-github-pages-3d36f4644e54?_x_tr_sl=en&amp;_x_tr_tl=zh-TW&amp;_x_tr_hl=zh-TW&amp;_x_tr_pto=sc&amp;_x_tr_hist=true\">如何在 GitHub 頁面上部署具有動態路由的 Vue.js 應用程式\u003C\u002Fa>\u003C\u002Fp>\n\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>跟上次部署在 AWS 的問題很類似=&gt;\u003Ca href=\"https:\u002F\u002Fpost.billxu.net\u002Fblog\u002F2024\u002F03\u002F19\u002Faws-cloudfront-%e9%83%a8%e7%bd%b2%e9%9d%9c%e6%85%8b%e7%b6%b2%e7%ab%99%e5%87%ba%e7%8f%be-x-cache-error-from-cloudfront-%e8%a8%8a%e6%81%af\u002F\" title=\"AWS Cloudfront 部署靜態網站出現 X-cache: Error from cloudfront 訊息\">AWS Cloudfront 部署靜態網站出現 X-cache: Error from cloudfront 訊息\u003C\u002Fa>，當 vue 的 SPA 部署在 github page ，並且設定自訂網域，如果是用 vue-route ，例如\u003Cdomain>\u002Fpage，可能會出現後端辨識錯誤，當訪問 \u002Fpage 時依然會導向到 \u002F 。\u003Cbr \u002F>\n找了幾種做法，這個解決法最簡易直覺，分享一下。\u003C\u002Fp>\n\u003Ch2>硬導向\u003C\u002Fh2>\n\u003Cp>這做法就是在部署後產生的 index.html 直接重新導向自己，包含 404.html。\u003Cbr \u002F>\n我的理解是，當訪問 \u003Cdomain>\u002Fpage 時，因為辨識不到實際的 \u002Fpage 中 index.html ，因此會跳錯誤轉向到 404.html ，因此 404.html 中需要導回到原來的  index.html 。\u003Cbr \u002F>\n記得加上檔案 public\u002F404.html \u003C\u002Fp>\n\u003Cpre>\u003Ccode>&lt;!DOCTYPE html&gt;\n&lt;html&gt;\n  &lt;head&gt;\n    &lt;meta charset=&quot;utf-8&quot;&gt;\n    &lt;title&gt;CHANGE THIS TO YOUR TITLE&lt;\u002Ftitle&gt;\n    &lt;script type=&quot;text\u002Fjavascript&quot;&gt;\n      var pathSegmentsToKeep = 0;\n      var l = window.location;\n      l.replace(\n        l.protocol + &#039;\u002F\u002F&#039; + l.hostname + (l.port ? &#039;:&#039; + l.port : &#039;&#039;) +\n        l.pathname.split(&#039;\u002F&#039;).slice(0, 1 + pathSegmentsToKeep).join(&#039;\u002F&#039;) + &#039;\u002F?\u002F&#039; +\n        l.pathname.slice(1).split(&#039;\u002F&#039;).slice(pathSegmentsToKeep).join(&#039;\u002F&#039;).replace(\u002F&amp;\u002Fg, &#039;~and~&#039;) +\n        (l.search ? &#039;&amp;&#039; + l.search.slice(1).replace(\u002F&amp;\u002Fg, &#039;~and~&#039;) : &#039;&#039;) +\n        l.hash\n      );\n\n    &lt;\u002Fscript&gt;\n  &lt;\u002Fhead&gt;\n  &lt;body&gt;\n  &lt;\u002Fbody&gt;\n&lt;\u002Fhtml&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>index.html 則需要再加上自我轉址\u003C\u002Fp>\n\u003Cpre>\u003Ccode>&lt;script type=&quot;text\u002Fjavascript&quot;&gt;\n\u002F\u002F MIT License\n\u002F\u002F https:\u002F\u002Fgithub.com\u002Frafgraph\u002Fspa-github-pages\n\u002F\u002F This script checks to see if a redirect is present in the query string,\n\u002F\u002F converts it back into the correct url and adds it to the\n\u002F\u002F browser&#039;s history using window.history.replaceState(...),\n\u002F\u002F which won&#039;t cause the browser to attempt to load the new url.\n\u002F\u002F When the single page app is loaded further down in this file,\n\u002F\u002F the correct url will be waiting in the browser&#039;s history for\n\u002F\u002F the single page app to route accordingly.\n(function(l) {\n    if (l.search[1] === &#039;\u002F&#039; ) {\n    var decoded = l.search.slice(1).split(&#039;&amp;&#039;).map(function(s) { \n        return s.replace(\u002F~and~\u002Fg, &#039;&amp;&#039;)\n    }).join(&#039;?&#039;);\n    window.history.replaceState(null, null,\n        l.pathname.slice(0, -1) + decoded + l.hash\n    );\n    }\n}(window.location))\n&lt;\u002Fscript&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>強制讓瀏覽器寫入原位置\u003Cbr \u002F>\n基本上就可以運作了，但由於這是取巧的做法，可能會影響 SEO ，使用時要考慮一下喔。\u003C\u002Fp>\n","2024-04-08T10:30:34",{"name":100,"link":101},[164],{"name":61,"slug":61},"\u002Fblog\u002F在-github-page-上部署-vue-spa-時-vue-route-失效的問題",{"id":167,"slug":168,"title":169,"description":170,"content":171,"date":172,"image":126,"imageAlt":169,"author":173,"categories":174,"path":176,"minRead":9},3700,"nuxt-3-的-pwa-設定方式","Nuxt 3 的 PWA 設定方式","Nuxt 3 的 PWA 設定方式 Nuxt 3 內就有基本的 PWA 設定，因為我只需要用到主畫面連結，因此 […]","\u003Ch1>Nuxt 3 的 PWA 設定方式\u003C\u002Fh1>\n\u003Cp>Nuxt 3 內就有基本的 PWA 設定，因為我只需要用到主畫面連結，因此用這做法就足夠了\u003C\u002Fp>\n\u003Cp>首先是設定基本的資訊\u003C\u002Fp>\n\u003Cp>nuxt.config.ts\u003C\u002Fp>\n\u003Cpre>\u003Ccode>import { defineNuxtConfig } from &#039;nuxt3&#039;\n\nexport default defineNuxtConfig({\n  meta: {\n    title: &#039;Nuxt3 PWA&#039;,\n    charset: &#039;utf-8&#039;,\n    meta: [\n      { name: &#039;theme-color&#039;, content: &#039;#ffdd67&#039; }\n    ],\n    link: [\n      { hid: &#039;icon&#039;, rel: &#039;icon&#039;, type: &#039;image\u002Fpng&#039;, href: &#039;\u002Flogo_x32.png&#039; },\n      { hid: &#039;apple-touch-icon&#039;, rel: &#039;apple-touch-icon&#039;, href: &#039;\u002Flogo_x180.png&#039; },\n      { rel: &#039;manifest&#039;, href: &#039;\u002Fmanifest.json&#039; }\n    ]\n  }\n})\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>再來於 public  裡面加入manifest.json 以及顯示的圖檔\u003C\u002Fp>\n\u003Cp>manifest.json\u003C\u002Fp>\n\u003Cpre>\u003Ccode>&quot;name&quot;: &quot;Nuxt3 PWA&quot;,\n    &quot;icons&quot;: [\n        {\n            &quot;src&quot;: &quot;\u002Flogo_x512.png&quot;,\n            &quot;types&quot;: &quot;img\u002Fpng&quot;,\n            &quot;sizes&quot;: &quot;512x512&quot;,\n            &quot;purpose&quot;: &quot;maskable&quot;\n        },\n        {\n            &quot;src&quot;: &quot;\u002Flogo_x384.png&quot;,\n            &quot;types&quot;: &quot;img\u002Fpng&quot;,\n            &quot;sizes&quot;: &quot;384x384&quot;,\n            &quot;purpose&quot;: &quot;maskable&quot;\n        },\n        {\n            &quot;src&quot;: &quot;\u002Flogo_x192.png&quot;,\n            &quot;types&quot;: &quot;img\u002Fpng&quot;,\n            &quot;sizes&quot;: &quot;192x192&quot;,\n            &quot;purpose&quot;: &quot;maskable&quot;\n        },\n        {\n            &quot;src&quot;: &quot;\u002Flogo_x128.png&quot;,\n            &quot;types&quot;: &quot;img\u002Fpng&quot;,\n            &quot;sizes&quot;: &quot;128x128&quot;,\n            &quot;purpose&quot;: &quot;maskable&quot;\n        },\n        {\n            &quot;src&quot;: &quot;\u002Flogo_x96.png&quot;,\n            &quot;types&quot;: &quot;img\u002Fpng&quot;,\n            &quot;sizes&quot;: &quot;96x96&quot;,\n            &quot;purpose&quot;: &quot;maskable&quot;\n        },\n        {\n            &quot;src&quot;: &quot;\u002Flogo_x72.png&quot;,\n            &quot;types&quot;: &quot;img\u002Fpng&quot;,\n            &quot;sizes&quot;: &quot;72x72&quot;,\n            &quot;purpose&quot;: &quot;maskable&quot;\n        },\n        {\n            &quot;src&quot;: &quot;\u002Flogo_x48.png&quot;,\n            &quot;types&quot;: &quot;img\u002Fpng&quot;,\n            &quot;sizes&quot;: &quot;48x48&quot;,\n            &quot;purpose&quot;: &quot;any&quot;\n        },\n        {\n            &quot;src&quot;: &quot;\u002Flogo_x180.png&quot;,\n            &quot;types&quot;: &quot;img\u002Fpng&quot;,\n            &quot;sizes&quot;: &quot;180x180&quot;,\n            &quot;purpose&quot;: &quot;any&quot;\n        },\n        {\n            &quot;src&quot;: &quot;\u002Flogo_x120.png&quot;,\n            &quot;types&quot;: &quot;img\u002Fpng&quot;,\n            &quot;sizes&quot;: &quot;120x120&quot;,\n            &quot;purpose&quot;: &quot;any&quot;\n        }\n    ],\n    &quot;start_url&quot;: &quot;\u002F?source=pwa&quot;,\n    &quot;display&quot;: &quot;fullscreen&quot;,\n    &quot;background_color&quot;: &quot;#000000&quot;,\n    &quot;theme_color&quot;: &quot;#000000&quot;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>app.vue 最好加個防呆\u003C\u002Fp>\n\u003Cpre>\u003Ccode>if (process.client) {\n  window.addEventListener(&#039;load&#039;, () =&gt; {\n    if (!(&#039;serviceWorker&#039; in navigator)) {\n      throw new Error(&#039;serviceWorker is not supported in current browser!&#039;)\n    }\n\n    navigator.serviceWorker.register(&#039;\u002Fsw.js&#039;)\n  })\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>大致上使用 PWA 時就吃到資料囉～\u003C\u002Fp>\n","2024-04-01T10:04:39",{"name":100,"link":101},[175],{"name":61,"slug":61},"\u002Fblog\u002Fnuxt-3-的-pwa-設定方式",{"id":178,"slug":179,"title":179,"description":180,"content":181,"date":182,"image":183,"imageAlt":179,"author":184,"categories":185,"path":189,"minRead":29},3664,"在手機上進行前端網頁的debug","在手機上進行前端網頁的debug 說來真是羞愧，最近才知道有這個神兵協助手機上的除錯XD 當專案越寫越多樣化時 […]","\u003Ch1>在手機上進行前端網頁的debug\u003C\u002Fh1>\n\u003Cp>說來真是羞愧，最近才知道有這個神兵協助手機上的除錯XD 當專案越寫越多樣化時，常常會遇到一種狀況，例如專案在手機的某個 app 內建瀏覽器上打開時才會出現錯誤、或是顯示異常，由於沒有萬能的 F12 可以看 console，通常得靠經驗通靈，其實效率滿差的。\u003C\u002Fp>\n\u003Cp>如果這種狀況依然能打開 console 的話，就能加速除錯時間了，最近才知道有這樣的神兵利器啊！那就是 vconsole ！\u003C\u002Fp>\n\u003Cp>以 vite + vue 為例，就照以下方式：\u003C\u002Fp>\n\u003Ch2>安裝\u003C\u002Fh2>\n\u003Cpre>\u003Ccode class=\"language-jsx\">npm install vconsole\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2>引入\u003C\u002Fh2>\n\u003Cp>我的作法有點偷懶，直接在 main.js 中引入\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-jsx\">import VConsole from &#039;vconsole&#039;;\n\n\u002F\u002F因為要限制只在開發期才顯示\nif (import.meta.env.MODE === &#039;development&#039;) {\n  const vConsole = new VConsole();\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>其實這樣就可以了\u003C\u002Fp>\n\u003Cp>正常來說會有個按鈕「VCONSOLE」\u003Cbr \u002F>\n\u003Cimg decoding=\"async\" src=\"\u002Fapi\u002Fproxy-image?url=https%3A%2F%2Fpost.billxu.net%2Fwp-content%2Fuploads%2F2024%2F03%2F%E6%88%AA%E5%9C%96-2024-03-07-%E6%99%9A%E4%B8%8A10.01.43.png\" alt=\"\" \u002F>\u003C\u002Fp>\n\u003Cp>打開來就有個 視窗可以看資訊囉～～\u003Cbr \u002F>\n\u003Cimg decoding=\"async\" src=\"\u002Fapi\u002Fproxy-image?url=https%3A%2F%2Fpost.billxu.net%2Fwp-content%2Fuploads%2F2024%2F03%2F%E6%88%AA%E5%9C%96-2024-03-07-%E6%99%9A%E4%B8%8A10.02.38-1024x349.png\" alt=\"\" \u002F>\u003C\u002Fp>\n","2024-03-10T10:04:14","\u002Fapi\u002Fproxy-image?url=https%3A%2F%2Fpost.billxu.net%2Fwp-content%2Fuploads%2F2023%2F01%2F640px-Node.js_logo.svg.png",{"name":100,"link":101},[186,187,188],{"name":22,"slug":22},{"name":43,"slug":43},{"name":61,"slug":61},"\u002Fblog\u002F在手機上進行前端網頁的debug",{"id":191,"slug":192,"title":193,"description":194,"content":195,"date":196,"image":197,"imageAlt":193,"author":198,"categories":199,"path":201,"minRead":29},3650,"vite-部署在-github-page","Vite 部署在 github page","Vite 部署在 github page 請參考官方文件：https:\u002F\u002Fvitejs.dev\u002Fguide\u002Fs […]","\u003Ch1>Vite 部署在 github page\u003C\u002Fh1>\n\u003Cp>請參考官方文件：\u003Ca href=\"https:\u002F\u002Fvitejs.dev\u002Fguide\u002Fstatic-deploy\">https:\u002F\u002Fvitejs.dev\u002Fguide\u002Fstatic-deploy\u003C\u002Fa>\u003C\u002Fp>\n\u003Cp>一開始因為 vue route 設定問題，部署到github page 上時不知道什麼原因，vue文檔中的\u003Crouter-view>會無法正確顯示在 index.html 上，幾番折騰後發現Vite官方就有最新版本的部署方式，這邊就記錄一下。(2024年)\u003C\u002Fp>\n\u003Cp>首先是 vite.config.js 中的需要先設定基礎位置，如果是使用https:\u002F\u002F\u003CUSERNAME>.github.io\u002F遠矢的位置，那\u003Ccode>base\u003C\u002Fcode>要設定為\u003Ccode>&#039;\u002F&#039;\u003C\u002Fcode>。如果是 https:\u002F\u002F\u003CUSERNAME>.github.io\u002F\u003CREPO>\u002F 有個 REPO位置的話，則\u003Ccode>base\u003C\u002Fcode> 設定為\u003Ccode>&#039;\u002F&lt;REPO&gt;\u002F&#039;\u003C\u002Fcode>。\u003C\u002Fp>\n\u003Cp>再來 setting 的 page 那邊， source 須要設定為 GitHub Actions。並且使用以下 workflow 的 yml\u003C\u002Fp>\n\u003Cpre>\u003Ccode># Simple workflow for deploying static content to GitHub Pages\nname: Deploy static content to Pages\n\non:\n  # Runs on pushes targeting the default branch\n  push:\n    branches: [&#039;main&#039;]\n\n  # Allows you to run this workflow manually from the Actions tab\n  workflow_dispatch:\n\n# Sets the GITHUB_TOKEN permissions to allow deployment to GitHub Pages\npermissions:\n  contents: read\n  pages: write\n  id-token: write\n\n# Allow one concurrent deployment\nconcurrency:\n  group: &#039;pages&#039;\n  cancel-in-progress: true\n\njobs:\n  # Single deploy job since we&#039;re just deploying\n  deploy:\n    environment:\n      name: github-pages\n      url: ${{ steps.deployment.outputs.page_url }}\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions\u002Fcheckout@v4\n      - name: Set up Node\n        uses: actions\u002Fsetup-node@v3\n        with:\n          node-version: 18\n          cache: &#039;npm&#039;\n      - name: Install dependencies\n        run: npm install\n      - name: Build\n        run: npm run build\n      - name: Setup Pages\n        uses: actions\u002Fconfigure-pages@v3\n      - name: Upload artifact\n        uses: actions\u002Fupload-pages-artifact@v2\n        with:\n          # Upload dist repository\n          path: &#039;.\u002Fdist&#039;\n      - name: Deploy to GitHub Pages\n        id: deployment\n        uses: actions\u002Fdeploy-pages@v2\u003C\u002Fcode>\u003C\u002Fpre>\n","2024-02-18T10:20:22","\u002Fapi\u002Fproxy-image?url=https%3A%2F%2Fpost.billxu.net%2Fwp-content%2Fuploads%2F2024%2F02%2F%E4%B8%8B%E8%BC%89.jpeg",{"name":100,"link":101},[200],{"name":61,"slug":61},"\u002Fblog\u002Fvite-部署在-github-page",{"id":203,"slug":204,"title":205,"description":206,"content":207,"date":208,"image":209,"imageAlt":205,"author":210,"categories":211,"path":214,"minRead":50},3630,"使用-github-gas-建立-liff-問卷","使用 Github + GAS 建立 LIFF 問卷","使用 Github + GAS 建立 LIFF 問卷 之前有個專案需求是透過 LINE bot + Googl […]","\u003Ch1>使用 Github + GAS 建立 LIFF 問卷\u003C\u002Fh1>\n\u003Cp>之前有個專案需求是透過 LINE bot + Google Form 建立一套可以進行線上預約的LINE OA(官方帳號)， 但為了進行 UID 的對應設計一套迂迴的流程，實在不爽快。現在有機會透過 LIFF 導入，能簡化這邊的流程。\u003C\u002Fp>\n\u003Cp>由於團隊適應關係，繼續沿用 Google sheet 來做管理，因此需要使用 GAS 來開發一組 API 。\u003C\u002Fp>\n\u003Cp>基礎概念很簡單：\u003C\u002Fp>\n\u003Col>\n\u003Cli>先部署 LIFF 頁面\u003C\u002Fli>\n\u003Cli>製作 LIFF 頁面的問卷\u003C\u002Fli>\n\u003Cli>部署 GAS 為基礎的 API\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch1>LIFF 頁面部署\u003C\u002Fh1>\n\u003Cp>這邊使用的開發環境為 Vite + Vue3.3 + Tailwind + axios。\u003C\u002Fp>\n\u003Ch3>1. 宣告 LIFF 導入\u003C\u002Fh3>\n\u003Cp>LIFF 的導入可以參考\u003Ca href=\"https:\u002F\u002Fpost.billxu.net\u002Fblog\u002F2023\u002F01\u002F10\u002Fvue%e5%88%9d%e6%8e%a2liff%e7%9a%84%e4%b8%80%e4%ba%9b%e7%ad%86%e8%a8%98-%e9%96%8b%e7%99%bc%e7%af%87\u002F\" title=\"這篇\">這篇\u003C\u002Fa>\u003Cbr \u002F>\n宣告 import liff 後，在 onMounted 時宣告 init，如下\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-jsx\">import liff from &quot;@line\u002Fliff&quot;;\n\nonMounted(async () =&gt; {\n  liff\n  .init({\n    liffId: &quot;&lt;LIFF ID&gt;&quot;, \u002F\u002F 從LIFF頁面中拿到的ID\n  })\n  .then(() =&gt; { \n    if(liff.isLoggedIn()){\n      \u002F\u002F做你想要做的\n      isLogin.value = liff.isLoggedIn();\n      userid.value = liff.getDecodedIDToken().sub; \u002F\u002F這是UID\n    }\n\n  })\n  .catch((err) =&gt; {\n    console.log(err.code, err.message);\n  });\n})\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>2.在問卷頁中加入送出表單\u003C\u002Fh3>\n\u003Cp>就是一個 submitForm 。裡面預留 API 的位置，已經撰寫問卷本體。\u003Cbr \u002F>\n到這邊 LIFF 大致上就做完了。接下來要部署到 Github Page 上\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-jsx\">&lt;form class=&quot;flex flex-col items-center&quot; @submit.prevent=&quot;submitForm&quot;&gt;\n&lt;label for=&quot;name&quot; class=&quot;block mt-4&quot;&gt;請問您的姓名：&lt;span class=&quot;text-red-500 text-sm&quot;&gt;＊（請輸入文字）&lt;\u002Fspan&gt;&lt;\u002Flabel&gt;\n  &lt;input type=&quot;text&quot; id=&quot;name&quot; v-model=&quot;name&quot; placeholder=&quot;姓名&quot; class=&quot;border border-gray-300 rounded-md px-2 py-1 mt-1&quot; pattern=&quot;[A-Za-z]+&quot; title=&quot;請輸入有效的姓名（只能包含字母）&quot; required \u002F&gt;\n\n  &lt;label for=&quot;phone&quot; class=&quot;block mt-4&quot;&gt;請問您的連絡電話：&lt;span class=&quot;text-red-500 text-sm&quot;&gt;＊（請輸入數字）&lt;\u002Fspan&gt;&lt;\u002Flabel&gt;\n  &lt;input type=&quot;text&quot; id=&quot;phone&quot; v-model=&quot;phone&quot; placeholder=&quot;連絡電話&quot; class=&quot;border border-gray-300 rounded-md px-2 py-1 mt-1&quot; pattern=&quot;[0-9]+&quot; title=&quot;請輸入有效的連絡電話（只能包含數字）&quot; required \u002F&gt;\n\n  &lt;label for=&quot;purchaseDate&quot; class=&quot;block mt-4&quot;&gt;請問您的購買時間：&lt;\u002Flabel&gt;\n  &lt;input type=&quot;date&quot; id=&quot;purchaseDate&quot; v-model=&quot;purchaseDate&quot; class=&quot;border border-gray-300 rounded-md px-2 py-1 mt-1&quot; required \u002F&gt;\n\n  &lt;label for=&quot;storeAddress&quot; class=&quot;block mt-4&quot;&gt;請問您購買的經銷門市地址：&lt;\u002Flabel&gt;\n  &lt;select id=&quot;storeAddress&quot; v-model=&quot;storeAddress&quot; class=&quot;border border-gray-300 rounded-md px-2 py-1 mt-1&quot; required&gt;\n      &lt;option value=&quot;&quot;&gt;請選擇&lt;\u002Foption&gt;\n      &lt;option value=&quot;address1&quot;&gt;A門市&lt;\u002Foption&gt;\n      &lt;option value=&quot;address2&quot;&gt;B門市&lt;\u002Foption&gt;\n      &lt;option value=&quot;address3&quot;&gt;C門市&lt;\u002Foption&gt;\n  &lt;\u002Fselect&gt;\n&lt;\u002Fform&gt;\n\nconst submitForm = () =&gt; {\n\n    fconst formData = new FormData();\n  formData.append(&#039;name&#039;, name.value);\n  formData.append(&#039;phone&#039;, phone.value);\n  formData.append(&#039;userid&#039;, user.userid);\n\n    axios({\n            method: &#039;post&#039;,\n            url: &#039;&lt;GAS api&gt;&#039;,\n            data: formData,\n            headers: { &#039;Content-Type&#039;: &#039;multipart\u002Fform-data&#039; }\n        })\n        .then(response =&gt; {\n            \u002F\u002F 成功\n            console.log(response);\n        })\n        .catch(error =&gt; {\n            \u002F\u002F 失敗\n\n        });\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2>使用  Github Actions 部署\u003C\u002Fh2>\n\u003Cp>為了省成本而部署在 Github Page 上，使用 Github Actions 來進行，請參考\u003Ca href=\"https:\u002F\u002Fpost.billxu.net\u002Fblog\u002F2024\u002F01\u002F08\u002F%e4%bd%bf%e7%94%a8-github-action-%e9%83%a8%e7%bd%b2-github-pages\u002F\" title=\"這篇\">這篇\u003C\u002Fa>。\u003Cbr \u002F>\ngh-pages-deploy.yml\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yaml\">name: Deploy\n\non:\n  push:\n    branches:\n      - main  # 將此處更改為你的默認分支名稱，例如：master\n\njobs:\n  build-and-deploy:\n    runs-on: ubuntu-latest\n\n    steps:\n    - name: Checkout\n      uses: actions\u002Fcheckout@v2\n\n    - name: Install &amp; Build # 執行指令\n      run: |\n        npm install\n        npm run build \n\n    - name: Deploy\n      uses: JamesIves\u002Fgithub-pages-deploy-action@4.1.5\n      with:\n        branch: gh-pages  # GitHub Pages 的分支\n        folder: dist  # build 後的檔案夾\n\n    - name: Debug git error\n      run: git status\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2>登記為 LIFF\u003C\u002Fh2>\n\u003Cp>部署完後，記得要去 LINE 開發者功能中登記 LIFF ，可以參考\u003Ca href=\"https:\u002F\u002Fpost.billxu.net\u002Fblog\u002F2023\u002F01\u002F10\u002Fline%e5%88%9d%e6%8e%a2liff%e7%9a%84%e4%b8%80%e4%ba%9b%e7%ad%86%e8%a8%98-%e7%94%b3%e8%ab%8b%e7%af%87\u002F\" title=\"這篇\">這篇\u003C\u002Fa>。\u003Cbr \u002F>\n就拿 Github Page 的 url 就可以。\u003C\u002Fp>\n\u003Ch1>GAS 部署\u003C\u002Fh1>\n\u003Cp>接著要製作 GAS 的 API ，請參考\u003Ca href=\"https:\u002F\u002Fwww.google.com\u002Fscript\u002Fstart\u002F\">官方文件\u003C\u002Fa>。\u003C\u002Fp>\n\u003Ch3>建立 POST 接口\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-jsx\">function doPost(e) {\n  var param = e.parameter;\n\n  const data = {\n    name: param.name,\n    phone: param.phone,\n    userID: param.userid\n  }\n  setData(data); \u002F\u002F將資料放進表單\n  return ContentService.createTextOutput(JSON.stringify({param}))\n                       .setMimeType(ContentService.MimeType.JSON) \u002F\u002F為了debug 先回傳資料\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>將資料放進表單\u003C\u002Fh3>\n\u003Cp>因為我是在別的文件開啟GAS，所以另外指定了 fileId 。簡單說就是看你的 Google sheet 位置，像這樣：\u003Ca href=\"https:\u002F\u002Fdocs.google.com\u002Fspreadsheets\u002Fd\u002F\">https:\u002F\u002Fdocs.google.com\u002Fspreadsheets\u002Fd\u002F\u003C\u002Fa>\u003CID>，取用\u003CID>那段就對了，記得要還要指定工作表喔。\u003C\u002Fp>\n\u003Cp>如果是同一個文件，可以使用\u003Ccode>getActiveSpreadsheet\u003C\u002Fcode>\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-jsx\">function setData(data){\n    var fileId = &#039;&lt;ID&gt;&#039;; \u002F\u002F 請用你的檔案ID替換此處\n    \u002F\u002F 透過ID開啟另一個檔案\n    var file = SpreadsheetApp.openById(fileId);\n    \u002F\u002F 獲取另一個檔案的工作表\n    var sheet = file.getSheetByName(&#039;&lt;工作表名稱&gt;&#039;);\n\n    const userID = data.userID;\n    var lastRow = sheet.getLastRow();\n\n        \u002F\u002F 如果是第一筆，先做標題\n    if (lastRow == 0) {\n      var headerRow = [&#039;name&#039;,&#039;phone&#039;,&#039;UID&#039;];\n      sheet.appendRow(headerRow);\n    }\n\n    \u002F\u002F 整理資料成陣列(注意是二維陣列)\n    var values = [[data.name,data.phone,userID]];\n\n    \u002F\u002F 放資料\n    sheet.getRange(lastRow + 1, 1, values.length, values[0].length).setValues(values);\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>部署位置\u003C\u002Fh3>\n\u003Cp>前面 LIFF 部署時，有個 \u003CGAS api> ，就是這時 GAS 部署完後會有組位置像這樣：\u003Ca href=\"https:\u002F\u002Fscript.google.com\u002Fmacros\u002Fs\u002Fxxxxxxxx\u002Fexec，將這串貼到\">https:\u002F\u002Fscript.google.com\u002Fmacros\u002Fs\u002Fxxxxxxxx\u002Fexec，將這串貼到\u003C\u002Fa> \u003CGAS api> 。\u003C\u002Fp>\n\u003Cp>這樣就可以建立一個可以取得 UID 的 LIFF 頁面，並且透過 GAS 存到 Google sheet 中，但記得 GAS API 有一些\u003Ca href=\"https:\u002F\u002Fdevelopers.google.com\u002Fapps-script\u002Fguides\u002Fservices\u002Fquotas?hl=zh-tw\">使用上限制\u003C\u002Fa>，用來建立微型服務絕對沒問題，但如果有用量上考量，建議不要這樣玩XD\u003C\u002Fp>\n","2024-01-16T16:20:22","\u002Fapi\u002Fproxy-image?url=https%3A%2F%2Fpost.billxu.net%2Fwp-content%2Fuploads%2F2024%2F01%2Fb40e6ed5-cbed-4701-b2c9-68e84f0754d4.jpeg",{"name":100,"link":101},[212,213],{"name":43,"slug":43},{"name":61,"slug":61},"\u002Fblog\u002F使用-github-gas-建立-liff-問卷",{"id":216,"slug":217,"title":218,"description":219,"content":220,"date":221,"image":222,"imageAlt":218,"author":223,"categories":224,"path":228,"minRead":29},3615,"使用-github-action-部署-github-pages","使用 Github Action 部署 Github Pages","使用 Github Action 部署 Github Pages (node) 記錄一下使用 Github A […]","\u003Ch1>使用 Github Action 部署 Github Pages (node)\u003C\u002Fh1>\n\u003Cp>記錄一下使用 Github Action 部署 Github Pages 的流程\u003Cbr \u002F>\n我這邊需求是 node + vue\u003C\u002Fp>\n\u003Cp>步驟很簡單：\u003C\u002Fp>\n\u003Col>\n\u003Cli>設定個人用的 token\u003C\u002Fli>\n\u003Cli>撰寫 Action 的 yml\u003C\u002Fli>\n\u003Cli>設定 repo\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>設定個人token\u003C\u002Fh2>\n\u003Cp>記得一兩年前 Github 就棄用輸入密碼的方式允許外部方式連線 repo，都必須要用 token。設定方式如下\u003C\u002Fp>\n\u003Cp>先在大頭像找到 setting\u003Cbr \u002F>\n\u003Cimg decoding=\"async\" src=\"\u002Fapi\u002Fproxy-image?url=https%3A%2F%2Fpost.billxu.net%2Fwp-content%2Fuploads%2F2024%2F01%2F%E6%88%AA%E5%9C%96-2024-01-05-%E4%B8%8A%E5%8D%8811.34.14-694x1024.png\" alt=\"\" \u002F>\u003C\u002Fp>\n\u003Cp>接著在左側欄找到 Developer Settings\u003Cbr \u002F>\n\u003Cimg decoding=\"async\" src=\"\u002Fapi\u002Fproxy-image?url=https%3A%2F%2Fpost.billxu.net%2Fwp-content%2Fuploads%2F2024%2F01%2F%E6%88%AA%E5%9C%96-2024-01-05-%E4%B8%8A%E5%8D%8811.34.34.png\" alt=\"\" \u002F>\u003C\u002Fp>\n\u003Cp>選擇 Token(classic)\u003Cbr \u002F>\n\u003Cimg decoding=\"async\" src=\"\u002Fapi\u002Fproxy-image?url=https%3A%2F%2Fpost.billxu.net%2Fwp-content%2Fuploads%2F2024%2F01%2F%E6%88%AA%E5%9C%96-2024-01-05-%E4%B8%8A%E5%8D%8811.35.09.png\" alt=\"\" \u002F>\u003C\u002Fp>\n\u003Cp>選擇 Generate new token (classic)\u003Cbr \u002F>\n\u003Cimg decoding=\"async\" src=\"\u002Fapi\u002Fproxy-image?url=https%3A%2F%2Fpost.billxu.net%2Fwp-content%2Fuploads%2F2024%2F01%2F%E6%88%AA%E5%9C%96-2024-01-05-%E4%B8%8A%E5%8D%8811.36.11-1024x232.png\" alt=\"\" \u002F>\u003C\u002Fp>\n\u003Cp>scopes 至少要勾以下範圍\u003Cbr \u002F>\n\u003Cimg decoding=\"async\" src=\"\u002Fapi\u002Fproxy-image?url=https%3A%2F%2Fpost.billxu.net%2Fwp-content%2Fuploads%2F2024%2F01%2F%E6%88%AA%E5%9C%96-2024-01-05-%E4%B8%8A%E5%8D%8811.37.21.png\" alt=\"\" \u002F>\u003C\u002Fp>\n\u003Ch2>設定 yml\u003C\u002Fh2>\n\u003Cp>這邊就不解釋 Action 的做法\u003Cbr \u002F>\n請參考 \u003Ca href=\"https:\u002F\u002Fdocs.github.com\u002Fen\u002Factions\" title=\"文件\">文件\u003C\u002Fa>\u003Cbr \u002F>\n我用的是這組 action ：JamesIves\u002Fgithub-pages-deploy-action@4.1.5\u003C\u002Fp>\n\u003Cp>相關參數寫在這邊:\u003Ca href=\"https:\u002F\u002Fgithub.com\u002FJamesIves\u002Fgithub-pages-deploy-action\">https:\u002F\u002Fgithub.com\u002FJamesIves\u002Fgithub-pages-deploy-action\u003C\u002Fa>\u003C\u002Fp>\n\u003Ch3>部署在自己的的專案\u003C\u002Fh3>\n\u003Cpre>\u003Ccode class=\"language-yaml\">name: Deploy\n\non:\n  push:\n    branches:\n      - main\n\njobs:\n  build-and-deploy:\n    runs-on: ubuntu-latest\n\n    steps:\n    - name: Checkout\n      uses: actions\u002Fcheckout@v2\n\n    - name: Install &amp; Build\n      run: |\n        npm install\n        npm run build \n\n      - name: Deploy\n        uses: JamesIves\u002Fgithub-pages-deploy-action@4.1.5\n        with:\n          branch: gh-pages\n          folder: dist\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>部署到其他專案\u003C\u002Fh3>\n\u003Cp>${{ secrets.ACCESS_TOKEN }} 就是從 ACCESS_TOKEN 的設定取值，不要將 token 暴露在明碼上\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yaml\">name: Deploy\n\non:\n  push:\n    branches:\n      - main\n\njobs:\n  build-and-deploy:\n    runs-on: ubuntu-latest\n\n    steps:\n    - name: Checkout\n      uses: actions\u002Fcheckout@v2\n\n    - name: Install &amp; Build\n      run: |\n        npm install\n        npm run build \n\n    - name: Deploy Antoher Repo\n      uses: JamesIves\u002Fgithub-pages-deploy-action@4.1.5\n      with:\n        branch: gh-pages\n        folder: dist\n        token: ${{ secrets.ACCESS_TOKEN }}\n        repository-name:  Sakasky\u002FSEZ\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>要另外在專案中設定 token\u003Cbr \u002F>\n\u003Cimg decoding=\"async\" src=\"\u002Fapi\u002Fproxy-image?url=https%3A%2F%2Fpost.billxu.net%2Fwp-content%2Fuploads%2F2024%2F01%2F%E6%88%AA%E5%9C%96-2024-01-05-%E4%B8%8A%E5%8D%8811.42.37.png\" alt=\"\" \u002F>\u003C\u002Fp>\n\u003Cp>\u003Cimg decoding=\"async\" src=\"\u002Fapi\u002Fproxy-image?url=https%3A%2F%2Fpost.billxu.net%2Fwp-content%2Fuploads%2F2024%2F01%2F%E6%88%AA%E5%9C%96-2024-01-05-%E4%B8%8A%E5%8D%8811.42.46-1024x612.png\" alt=\"\" \u002F>\u003C\u002Fp>\n\u003Cp>\u003Cimg decoding=\"async\" src=\"\u002Fapi\u002Fproxy-image?url=https%3A%2F%2Fpost.billxu.net%2Fwp-content%2Fuploads%2F2024%2F01%2F%E6%88%AA%E5%9C%96-2024-01-05-%E4%B8%8A%E5%8D%8811.42.53-1024x478.png\" alt=\"\" \u002F>\u003Cbr \u002F>\n以 yml 為例子\u003Cbr \u002F>\nName就設定 ACCESS_TOKEN\u003Cbr \u002F>\n然後將 token 放到 Secret 裡面\u003C\u002Fp>\n\u003Ch3>打開 Github Pages 設定\u003C\u002Fh3>\n\u003Cp>測試過要先設定或不設定都可以，若不先設定，而直接部署的話也可以\u003Cbr \u002F>\n\u003Cimg decoding=\"async\" src=\"\u002Fapi\u002Fproxy-image?url=https%3A%2F%2Fpost.billxu.net%2Fwp-content%2Fuploads%2F2024%2F01%2F%E6%88%AA%E5%9C%96-2024-01-05-%E4%B8%8A%E5%8D%8811.43.16.png\" alt=\"\" \u002F>\u003C\u002Fp>\n\u003Ch1>完成\u003C\u002Fh1>\n\u003Cp>只要每次 commit \u002F push 完，Action 就會自動幫部署網頁啦，超級方便！\u003C\u002Fp>\n","2024-01-08T10:03:34","\u002Fapi\u002Fproxy-image?url=https%3A%2F%2Fpost.billxu.net%2Fwp-content%2Fuploads%2F2024%2F01%2F7430fe6a-8f97-43ae-89ab-85f8bde95b18.jpeg",{"name":100,"link":101},[225,226,227],{"name":17,"slug":18},{"name":22,"slug":22},{"name":43,"slug":43},"\u002Fblog\u002F使用-github-action-部署-github-pages",{"id":230,"slug":231,"title":232,"description":233,"content":234,"date":235,"image":86,"imageAlt":232,"author":236,"categories":237,"path":239,"minRead":29},3556,"gasgoogle-app-script-上與資料表溝通的方式","GAS(google app script) 上與資料表溝通的方式","GAS(google app script) 上與資料表溝通的方式 上次使用 GAS 部署網頁後，進階做法就是 […]","\u003Ch1>GAS(google app script) 上與資料表溝通的方式\u003C\u002Fh1>\n\u003Cp>上次使用 GAS 部署網頁後，進階做法就是能透過 GAS 結合 google sheet 建置一個簡易的後端存取資料系統，最重要的問題就是---要如何溝通？\u003C\u002Fp>\n\u003Cp>我摸索出兩種做法，一種是使用 doPost 或是 doGet ，將 GAS 直接作為溝通工具。另一種做法是呼叫 google script，就看哪種方法剛好符合需求直接用就可以。\u003C\u002Fp>\n\u003Ch2>doPost\u003C\u002Fh2>\n\u003Cp>其實就如同名稱，使用 doPost 就會使用 POST 方式，如果換成 doGet 就會使用 GET 方式。使用方式很簡單：\u003Cbr \u002F>\n先在 gs 檔中加上\u003C\u002Fp>\n\u003Ch3>xxx.gs\u003C\u002Fh3>\n\u003Cpre>\u003Ccode>function doPost(e) {\n  var param = e.parameter;\n  var name = param.name;\n  var replyMessege = &#039;Your nane：&#039; + name ;\n  return ContentService.createTextOutput(replyMessege);\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>然後進行部署，之後會得到一個類似的位置\u003C\u002Fp>\n\u003Cpre>\u003Ccode>https:\u002F\u002Fscript.google.com\u002Fmacros\u002Fs\u002F&lt;token&gt;\u002Fexec\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>對這個位置發送 POST 就可以囉～大概會是這種感覺\u003C\u002Fp>\n\u003Cpre>\u003Ccode>$.post(&#039;部署網址&#039;,{\n    name:&#039;billxu&#039;,\n},function(e){\n    console.log(e); \u002F\u002F\u002F Your name : billxu\n});\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>這種方法就能將 GAS 視為一個簡易後端 API \u003C\u002Fp>\n\u003Ch2>google.script\u003C\u002Fh2>\n\u003Cp>這個方式比較粗暴，簡單來說就是使用部署後的網頁直接呼叫 gs 檔的函式，簡單來說像這樣寫\u003C\u002Fp>\n\u003Cpre>\u003Ccode>google.script.run\n          .withSuccessHandler()\n          .customFunction(input);\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>然後在 gs 檔裡宣告對應的函式名稱\u003C\u002Fp>\n\u003Ch3>xxx.gs\u003C\u002Fh3>\n\u003Cpre>\u003Ccode>function customFunction (input) {\n\u002F\u002Fdo something\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>這樣就可以在部署的網頁呼叫 GAS 中的功能囉～也可以傳入資料，缺點就是不像 POST 可以回傳執行結果，這點要注意。\u003C\u002Fp>\n","2023-10-15T10:11:51",{"name":100,"link":101},[238],{"name":22,"slug":22},"\u002Fblog\u002Fgasgoogle-app-script-上與資料表溝通的方式",{"id":241,"slug":242,"title":243,"description":244,"content":245,"date":246,"image":150,"imageAlt":243,"author":247,"categories":248,"path":251,"minRead":9},3543,"gasgoogle-app-script上實作-vue3-的網頁部署","GAS(google app script)上實作 vue3 的網頁部署","GAS(google app script)上實作 vue3 的網頁部署 GAS(google app scr […]","\u003Ch1>GAS(google app script)上實作 vue3 的網頁部署\u003C\u002Fh1>\n\u003Cp>GAS(google app script) 是 google 服務提供的腳本平台，最廣為熟知的就是可以透過 script 將 google sheet 擴充許多自動化功能，甚至可以部署網頁等。若有興趣可以查查看，可以多查查看，如果你有大量的 google 相依服務，就能考慮使用 GAS 提供工作效率。\u003C\u002Fp>\n\u003Cp>這次的需求就是將 google sheet 當作是簡易資料表，然後輸出到 GAS 的網頁做檢視，考慮到未來可能的擴充性，決定導入 Vue3 的 CDN 版本來實作。\u003C\u002Fp>\n\u003Ch2>啟用 GAS(google app script)\u003C\u002Fh2>\n\u003Cp>首先就是啟用 GAS ，這邊就不特別教學，請參考原本的\u003Ca href=\"https:\u002F\u002Fwww.google.com\u002Fscript\u002Fstart\u002F\" title=\"教學\">教學\u003C\u002Fa>。\u003C\u002Fp>\n\u003Ch2>部署網頁\u003C\u002Fh2>\n\u003Cp>首先新增一個網頁，我取名為 output.html 。\u003Cbr \u002F>\n\u003Cimg decoding=\"async\" src=\"\u002Fapi\u002Fproxy-image?url=https%3A%2F%2Fpost.billxu.net%2Fwp-content%2Fuploads%2F2023%2F10%2F%E6%88%AA%E5%9C%96-2023-10-01-%E4%B8%8B%E5%8D%8810.02.10-300x204.png\" alt=\"\" \u002F>\u003Cbr \u002F>\n再來新增一個 code.gs 。*.gs 是 GAS中專用的 script 副檔名。\u003Cbr \u002F>\n\u003Cimg decoding=\"async\" src=\"\u002Fapi\u002Fproxy-image?url=https%3A%2F%2Fpost.billxu.net%2Fwp-content%2Fuploads%2F2023%2F10%2F%E6%88%AA%E5%9C%96-2023-10-01-%E4%B8%8B%E5%8D%8810.03.41-300x237.png\" alt=\"\" \u002F>\u003C\u002Fp>\n\u003Cp>在 code.gs 中加上以下程式碼。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>function doGet(){\n    let template = HtmlService.createTemplateFromFile(&#039;output&#039;);\n    let dataObj = {};\n    template.data = dataObj;\n    return template.evaluate().setTitle(&quot;Vuejs Demo&quot;);\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Cem>HtmlService\u003C\u002Fem> 是 GAS 中建立網頁的函式，這樣就能將資料輸出至 output.html ，並且設定標題 \u003Cem>Vuejs Demo\u003C\u002Fem> 。一定要取名為doGet\u003Cbr \u002F>\n並且能把名為 dataObj 的物件丟到該文檔中。\u003C\u002Fp>\n\u003Cp>output.html 這隻檔案則就加入一般的 html 文件，例如\u003C\u002Fp>\n\u003Cpre>\u003Ccode>\u003C!DOCTYPE html>\n\u003Chtml>\n  \u003Chead>\n  ...\n  \u003C\u002Fhead>\n  \u003Cbody>\n  ...\n  \u003C\u002Fbody>\n\u003C\u002Fhtml>\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2>引入 Vue3\u003C\u002Fh2>\n\u003Cp>接下來就是建立 Vue3 的環境。我們使用 CDN 引入，可以直接像這樣\u003C\u002Fp>\n\u003Cpre>\u003Ccode>&lt;html&gt;\n  &lt;head&gt;\n    &lt;script src=&quot;https:\u002F\u002Fcdn.jsdelivr.net\u002Fnpm\u002Fvue@3.2.1\u002Fdist\u002Fvue.global.js&quot;&gt;&lt;\u002Fscript&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>接著要宣告 Vue 實體，為了方便管理我另外開了一隻檔案 app-js.html\u003Cbr \u002F>\n\u003Cimg decoding=\"async\" src=\"\u002Fapi\u002Fproxy-image?url=https%3A%2F%2Fpost.billxu.net%2Fwp-content%2Fuploads%2F2023%2F10%2F%E6%88%AA%E5%9C%96-2023-10-01-%E4%B8%8B%E5%8D%8810.13.03-300x264.png\" alt=\"\" \u002F>\u003Cbr \u002F>\n裡面其實就是宣告建立 Vue\u003C\u002Fp>\n\u003Cpre>\u003Ccode>&lt;script&gt;\n\n    const { createApp, ref } = Vue\n\n    const app = createApp({\n      components: {  },\n      data() {\n        return {\n\n        }\n      },\n      watch:{\n\n      },\n      methods: {\n\n      },\n      created: function(){\n\n    });\n    app.mount(&quot;#app&quot;);\n\n  &lt;\u002Fscript&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>然後記得 output.html 需要預留一個可以建立 Vue 的位置，並且還要引入剛剛那隻 app-js.html\u003C\u002Fp>\n\u003Cpre>\u003Ccode>&lt;body&gt;\n&lt;div class=&quot;&quot; id=&quot;app&quot;&gt;\n....\n\n    &lt;?!= include(&#039;app-js&#039;); ?&gt;\n&lt;\u002Fbody&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>這樣就快完成了，只需要部署就可以使用囉～\u003C\u002Fp>\n\u003Ch2>取用資料\u003C\u002Fh2>\n\u003Cp>如果需要取用 google sheet 內的資料，則需要在 code.gs 中另外使用函式與資料表溝通。以下為範例。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>function doGet(){\n    let template = HtmlService.createTemplateFromFile(&#039;output&#039;);\n    let dataObj = {};\n    dataObj.Data = getDB(); \u002F\u002F 這是取得資料的函式\n    template.data = dataObj;\n    return template.evaluate().setTitle(&quot;Vuejs and SpreadSheet Demo&quot;).setSandboxMode(HtmlService.SandboxMode.IFRAME);\n}\n\nfunction getDB(){\n    const activeSheet = SpreadsheetApp.getActiveSheet();\n    let dataItems = activeSheet.getDataRange().getValues().slice(1);\n    return dataItems;\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>這是最簡單的寫法， SpreadsheetApp 就是使用 GAS 跟 google sheet 溝通，有興趣就詳見 GAS 的各種應用。\u003C\u002Fp>\n\u003Ch2>部署\u003C\u002Fh2>\n\u003Cp>在 GAS 的管理介面中，右上角可以看到部署的按鈕。如果開發時可以使用「測試部署作業」，如果是正式使用則要「新增部署作業」，兩者差異在於「測試部署作業」會給予一個....\u002Fdev結尾的位置，並且每次修改儲存後會立即生效，「新增部署作業」的話會以該次程式碼版本部署，會給予一個....\u002Fexec的位置，但記得程式碼修改後不會立即生效，要使用每次部署後取得的位置才有用。\u003Cimg decoding=\"async\" src=\"\u002Fapi\u002Fproxy-image?url=https%3A%2F%2Fpost.billxu.net%2Fwp-content%2Fuploads%2F2023%2F10%2F%E6%88%AA%E5%9C%96-2023-10-01-%E4%B8%8B%E5%8D%8810.19.34-297x300.png\" alt=\"\" \u002F>\u003C\u002Fp>\n","2023-10-02T10:26:52",{"name":100,"link":101},[249,250],{"name":22,"slug":22},{"name":61,"slug":61},"\u002Fblog\u002Fgasgoogle-app-script上實作-vue3-的網頁部署",{"id":253,"slug":254,"title":255,"description":256,"content":257,"date":258,"image":259,"imageAlt":255,"author":260,"categories":261,"path":265,"minRead":9},3325,"vue初探liff的一些筆記-開發篇","[Vue]初探LIFF的一些筆記-開發篇","初探LIFF的一些筆記 最近因為chatGPT很紅因素，很多人將chatGPT與line整合一起，在玩這類專案 […]","\u003Ch1>初探LIFF的一些筆記\u003C\u002Fh1>\n\u003Cp>最近因為chatGPT很紅因素，很多人將chatGPT與line整合一起，在玩這類專案時剛好碰到了line的API服務於是認識到LIFF，今天花點時間整理關於開發LIFF遇到的狀況。\u003Cbr \u002F>\nLIFF ( LINE Front-end Framework ) 是Line推出的一種前端框架，可以將功能與Line直接整合，藉此取得使用者user資料、進行部分操作。\u003Cbr \u002F>\n用這玩意能完美的與Line bot連動，透過使用 Line Login 或 Account Link 的網站進行使用者資料串接，進而製作更全面性的整合行銷服務。\u003Cbr \u002F>\n我用Vue3進行串接開發，使用setup script。\u003C\u002Fp>\n\u003Ch2>使用LIFF\u003C\u002Fh2>\n\u003Cp>請參考\u003Ca href=\"https:\u002F\u002Fpost.billxu.net\u002Fline初探liff的一些筆記-申請篇\u002F\" title=\"liff的一些筆記-申請篇\">LIFF的一些筆記-申請篇\u003C\u002Fa>\u003C\u002Fp>\n\u003Ch2>載入\u003C\u002Fh2>\n\u003Cp>使用LIFF SDK可以直接用CDN引入\u003C\u002Fp>\n\u003Cpre>\u003Ccode>&lt;script src=&quot;https:\u002F\u002Fstatic.line-scdn.net\u002Fliff\u002Fedge\u002F2\u002Fsdk.js&quot;&gt;&lt;\u002Fscript&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>在Vue話，我是先透過npm安裝\u003C\u002Fp>\n\u003Cpre>\u003Ccode>npm install vue-router@next @line\u002Fliff\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>使用Vue3的setup script，載入SDK\u003C\u002Fp>\n\u003Cpre>\u003Ccode>&lt;script setup&gt;\nimport liff from &quot;@line\u002Fliff&quot;;\n&lt;\u002Fscript&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>後面就可以直接用囉。\u003C\u002Fp>\n\u003Ch2>初始化\u003C\u002Fh2>\n\u003Cp>啟動 LIFF 前需要先初始化(init)，在 Vue 的話就在 Mounted 實作這件事情。\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-javascript\">onMounted(async () =&gt; {\n  liff\n  .init({\n    liffId: &quot;&lt;LIFF ID&gt;&quot;, \u002F\u002F 從LIFF頁面中拿到的ID\n  })\n  .then(() =&gt; { \n  \u002F\u002F做你想要做的\n  })\n  .catch((err) =&gt; {\n    console.log(err.code, err.message);\n  });\n})\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2>登入與登出\u003C\u002Fh2>\n\u003Cp>登入後的redirectUri最好是跟LIFF頁面上設定的Callback URL相同，當然你可以依照需求轉到你要的地方。\u003Cbr \u002F>\n\u003Cimg decoding=\"async\" src=\"\u002Fapi\u002Fproxy-image?url=https%3A%2F%2Fpost.billxu.net%2Fwp-content%2Fuploads%2F2022%2F12%2F%E6%88%AA%E5%9C%96-2022-12-21-%E4%B8%8B%E5%8D%8812.17.53.png\" alt=\"\" \u002F>\u003C\u002Fp>\n\u003Cpre>\u003Ccode>if(!sysdata.isLoggedIn) {\n    liff.login({\n      redirectUri: window.location.href \u002F\u002F登入後轉址到需要的地方\n    });\n  }\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>登出就比較單純\u003C\u002Fp>\n\u003Cpre>\u003Ccode> if(sysdata.isLoggedIn) {\n    liff.logout();\n    window.location.reload(); \u002F\u002F 登出後重整一次頁面\n  }\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2>取得基本資訊\u003C\u002Fh2>\n\u003Cp>我將取得資訊跟初始化寫在一起，範例如下。\u003C\u002Fp>\n\u003Cul>\n\u003Cli>打開頁面中的使用語系（lang）\u003C\u002Fli>\n\u003Cli>LIFF SDK 的版本\u003C\u002Fli>\n\u003Cli>目前 LIFF 是否是在 LINE App 中開啟\u003C\u002Fli>\n\u003Cli>使用者有沒有登入 LINE 帳號\u003C\u002Fli>\n\u003Cli>使用者的作業系統：web、android、ios\u003C\u002Fli>\n\u003Cli>\n\u003Cp>使用者用的 LINE App 版本\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-javascript\">onMounted(async () =&gt; {\nliff\n.init({\nliffId: &quot;&quot;, \u002F\u002F 從LIFF頁面中拿到的ID\n})\n.then(() =&gt; {\nsysdata.language = liff.getLanguage(); \u002F\u002F String。引用 LIFF SDK 的頁面，頁面中的 lang 資料\nsysdata.version = liff.getVersion(); \u002F\u002F String。取得LIFF SDK 的版本\nsysdata.isInClient = liff.isInClient(); \u002F\u002F Boolean。回傳是否由 LINE App 存取\nsysdata.isLoggedIn = liff.isLoggedIn(); \u002F\u002F Boolean。檢查使用者是否登入 LINE 帳號狀態。true 時，可呼叫需要 Access Token 的 API\nsysdata.os = liff.getOS(); \u002F\u002F String。取得使用者的作業系統：ios、android、web\nsysdata.lineVersion = liff.getLineVersion(); \u002F\u002F 使用者的 LINE 版本\nconsole.log(sysdata.language,sysdata.version,sysdata.isInClient,sysdata.isLoggedIn,sysdata.os,sysdata.lineVersion);\n\n})\n.catch((err) =&gt; {\nconsole.log(err.code, err.message);\n});\n})\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>這邊要再講一下，申請完LIFF後會有個原來的Endpoint URL和LIFF上的LIFF URL，兩邊的LIFF SDK都會作用，但取得的權限不同(見下段落)，如果需要在LINE做一些操作，最好使用LIFF URL給使用者打開。\u003C\u002Fp>\n\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2>取得使用者資訊\u003C\u002Fh2>\n\u003Cp>取得使用者資訊使用這個api，取得後可以檢查使用者賦予的權限和一些資料，必須要使用者必須登入才能使用。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>userdata.context = liff.getContext();\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>回傳有以下幾種資料\u003C\u002Fp>\n\u003Cul>\n\u003Cli>type: utou(一對一聊天)、group(群組)、room(多人聊天室)、external(外部，通常是瀏覽器)、none(除以上外)。\u003C\u002Fli>\n\u003Cli>viewType：compact、tall、full。目前視窗的尺寸，可以在LIFF中設定。\u003C\u002Fli>\n\u003Cli>userId、groupId、roomId：不同的 type 會有不同的 ID。\u003C\u002Fli>\n\u003Cli>availability.shareTargetPicker.permission：能不能使用 shareTargetPicker 功能，可以在LIFF中打開。\u003C\u002Fli>\n\u003Cli>availability.shareTargetPicker.minVer：使用 shareTargetPicker 功能的需求版本是多少。\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>這邊就要提醒一下，Endpoint URL和LIFF URL打開來得到的使用者資訊是不同的，前者type為external後者為utou(如果是bot聊天)。\u003C\u002Fp>\n\u003Ch2>取得使用者公開資料\u003C\u002Fh2>\n\u003Cp>必須要使用者必須登入才能使用。回傳資料有四個：userId、暱稱、照片的圖片位置、狀態消息。除了userId外皆屬於完全公開資訊。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>liff.getProfile()\n    .then(function(profile) {\n    userdata.profile = profile;\n    console.log(userdata.profile);\n});\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>取得使用者 email\u003C\u002Fh3>\n\u003Cp>必須要使用者必須登入才能使用。也必須先在LIFF打開權限，並且使用者登入時需授權。\u003Cbr \u002F>\n\u003Cimg decoding=\"async\" src=\"\u002Fapi\u002Fproxy-image?url=https%3A%2F%2Fpost.billxu.net%2Fwp-content%2Fuploads%2F2022%2F12%2F%E6%88%AA%E5%9C%96-2022-12-21-%E4%B8%8B%E5%8D%8812.01.58.png\" alt=\"\" \u002F>\u003Cbr \u002F>\n還要打開LIFF的Scopes權限(email*)\u003Cbr \u002F>\n\u003Cimg decoding=\"async\" src=\"\u002Fapi\u002Fproxy-image?url=https%3A%2F%2Fpost.billxu.net%2Fwp-content%2Fuploads%2F2022%2F12%2F%E6%88%AA%E5%9C%96-2022-12-21-%E4%B8%8B%E5%8D%8812.05.21.png\" alt=\"\" \u002F>\u003C\u002Fp>\n\u003Cpre>\u003Ccode> const user = liff.getDecodedIDToken();\n    userdata.user = user\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>發送訊息\u003C\u002Fh3>\n\u003Cp>使用者必須登入才能使用，分為對自己和對其他人。要先打開LIFF的Scopes權限(chat_message.write)，並且使用者在登入時要授權。\u003Cbr \u002F>\n\u003Cimg decoding=\"async\" src=\"\u002Fapi\u002Fproxy-image?url=https%3A%2F%2Fpost.billxu.net%2Fwp-content%2Fuploads%2F2022%2F12%2F%E6%88%AA%E5%9C%96-2022-12-21-%E4%B8%8B%E5%8D%8812.05.21.png\" alt=\"\" \u002F>\u003C\u002Fp>\n\u003Ch3>對自己發送\u003C\u002Fh3>\n\u003Cp>只能在Line APP才能使用此功能，而且需要一對一聊天室，使用者資料的type須為utou，如果為group、room沒測試不知道能不能使用。external則不行。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>const sendMessegeToSelf = () =&gt; {\n  \u002F\u002F 傳送訊息給自己\n  \u002F\u002F type 的可用值說明：https:\u002F\u002Fdevelopers.line.biz\u002Fen\u002Freference\u002Fliff\u002F#send-messages\n  liff.sendMessages([\n    {\n      type: &#039;text&#039;,\n      text: &lt;發送訊息&gt;\n    }\n  ])\n  .then(res =&gt; window.alert(res.status))\n  .catch(error =&gt; window.alert(error));\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>對其他人發送\u003C\u002Fh3>\n\u003Cp>type為external也可以使用，但記得要先開啟\u003Cem>shareTargetPicker\u003C\u002Fem>。\u003Cbr \u002F>\n\u003Cimg decoding=\"async\" src=\"\u002Fapi\u002Fproxy-image?url=https%3A%2F%2Fpost.billxu.net%2Fwp-content%2Fuploads%2F2022%2F12%2F%E6%88%AA%E5%9C%96-2022-12-21-%E4%B8%8B%E5%8D%8812.08.03.png\" alt=\"\" \u002F>\u003C\u002Fp>\n\u003Cpre>\u003Ccode>const sendMessegeToOther = () =&gt; {\n  \u002F\u002F 傳送訊息給朋友\n  \u002F\u002F 發訊息的可用參數：https:\u002F\u002Fdevelopers.line.biz\u002Fen\u002Freference\u002Fliff\u002F#share-target-picker\n  if(sysdata.isLoggedIn &amp;&amp; liff.isApiAvailable(&#039;shareTargetPicker&#039;)) {\n    liff.shareTargetPicker([\n      {\n        type: &quot;text&quot;,\n        text: &lt;發送訊息&gt;\n      }\n    ])\n    .then(res =&gt; window.alert(res.status))\n    .catch(error =&gt; window.alert(error))\n  }\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2>QRcode\u003C\u002Fh2>\n\u003Cp>很多人都會介紹，但IOS目前不能用，所以先筆記一下\u003C\u002Fp>\n\u003Cpre>\u003Ccode>liff.scanCode()\n  .then(function(res) {\n    console.log(res);\n  })\n  .catch(function(error) {\n    console.log(error);\n  });\u003C\u002Fcode>\u003C\u002Fpre>\n","2023-01-10T12:05:31","\u002Fapi\u002Fproxy-image?url=https%3A%2F%2Fpost.billxu.net%2Fwp-content%2Fuploads%2F2022%2F12%2FLINE_logo.svg.png",{"name":100,"link":101},[262,263,264],{"name":22,"slug":22},{"name":33,"slug":34},{"name":61,"slug":61},"\u002Fblog\u002Fvue初探liff的一些筆記-開發篇",{"id":267,"slug":268,"title":269,"description":270,"content":271,"date":272,"image":259,"imageAlt":269,"author":273,"categories":274,"path":276,"minRead":29},3335,"line初探liff的一些筆記-申請篇","[Line]初探LIFF的一些筆記-申請篇","初探LIFF的一些筆記-申請篇 LIFF ( LINE Front-end Framework ) 是Line […]","\u003Ch1>初探LIFF的一些筆記-申請篇\u003C\u002Fh1>\n\u003Cp>LIFF ( LINE Front-end Framework ) 是Line推出的一種前端框架，可以將功能與Line直接整合，藉此取得使用者user資料、進行部分操作。\u003Cbr \u002F>\n用這玩意能完美的與Line bot連動，透過使用 Line Login 或 Account Link 的網站進行使用者資料串接，進而製作更全面性的整合行銷服務。\u003Cbr \u002F>\n在開發前須先申請LIFF服務。\u003C\u002Fp>\n\u003Ch2>建立LIFF\u003C\u002Fh2>\n\u003Cp>須先登入\u003Ca href=\"https:\u002F\u002Fdevelopers.line.biz\u002F\" title=\"LINE Developers\">LINE Developers\u003C\u002Fa>，新增 Providers，接著點進建立好的 Providers，按下「Create a new channel」，會看見有五種 channel 模式可選，選擇「Create a LINE Login channel」，之後 App types 要用 \u003Cstrong>\u003Cem>Web app\u003C\u002Fem>\u003C\u002Fstrong>\u003Cbr \u002F>\n\u003Cimg decoding=\"async\" src=\"\u002Fapi\u002Fproxy-image?url=https%3A%2F%2Fpost.billxu.net%2Fwp-content%2Fuploads%2F2022%2F12%2F%E6%88%AA%E5%9C%96-2022-12-21-%E4%B8%8B%E5%8D%8812.22.44.png\" alt=\"\" \u002F>\u003C\u002Fp>\n\u003Ch2>設定LIFF\u003C\u002Fh2>\n\u003Cp>建立好後，可以點進該 channel，就能看到一個分頁名為 LIFF 。點擊新增 Add 就能建立一個 LIFF 的服務。以下為幾點設定說明。\u003C\u002Fp>\n\u003Ch3>Size\u003C\u002Fh3>\n\u003Cp>有三個值：Full、Tall、Compact。這個設定就是當使用 LINE 打開 LIFF 時，會有一個大小框架。\u003Cbr \u002F>\n\u003Cimg decoding=\"async\" src=\"\u002Fapi\u002Fproxy-image?url=https%3A%2F%2Fpost.billxu.net%2Fwp-content%2Fuploads%2F2022%2F12%2F%E6%88%AA%E5%9C%96-2022-12-21-%E4%B8%8B%E5%8D%8812.28.08.png\" alt=\"\" \u002F>\u003Cbr \u002F>\n\u003Ca href=\"https:\u002F\u002Fdevelopers.line.biz\u002Fen\u002Fdocs\u002Fliff\u002Foverview\u002F#screen-size\" title=\"參考文件\">參考文件\u003C\u002Fa>\u003C\u002Fp>\n\u003Ch3>Endpoint URL\u003C\u002Fh3>\n\u003Cp>當我們點擊 LIFF url 後，會有個實際打開的位置，就是這邊設定，只能是 https 開頭的。\u003C\u002Fp>\n\u003Ch3>Scopes\u003C\u002Fh3>\n\u003Cp>Scopes指的是我們想要透過LIFF取哪些資料？但實際上使用者也需授權才能真的取得。有 chat_message.write, openid, profile, email，這在開發篇才會真的用到。\u003C\u002Fp>\n\u003Ch3>Scan QR\u003C\u002Fh3>\n\u003Cp>打開 QRcode 的權限，IOS不能用，所以我開了也沒用到。\u003C\u002Fp>\n\u003Ch3>Bot link feature\u003C\u002Fh3>\n\u003Cp>LINE Login Channel 中可以在使用者登入時詢問是否加好友的功能選項：\u003Cbr \u002F>\nOn (Normal)：預設將官方帳號加為好友\u003Cbr \u002F>\nOn (Aggressive)：另外跳出視窗詢問是否要加官方帳號為好友\u003Cbr \u002F>\nOff：不做\u003C\u002Fp>\n\u003Ch3>Module mode\u003C\u002Fh3>\n\u003Cp>似乎是可以透過按鈕或其他形式分享這個LIFF，沒有特別使用，\u003Ca href=\"https:\u002F\u002Fdevelopers.line.biz\u002Fen\u002Fdocs\u002Fliff\u002Foverview\u002F#screen-size\" title=\"參考文件\">參考文件\u003C\u002Fa>\u003C\u002Fp>\n\u003Ch2>關於開發\u003C\u002Fh2>\n\u003Cp>就參考\u003Ca href=\"https:\u002F\u002Fpost.billxu.net\u002Fvue初探liff的一些筆記-開發篇\u002F\" title=\"初探liff的一些筆記-開發篇\">初探liff的一些筆記-開發篇\u003C\u002Fa>囉！\u003C\u002Fp>\n","2023-01-10T12:00:44",{"name":100,"link":101},[275],{"name":22,"slug":22},"\u002Fblog\u002Fline初探liff的一些筆記-申請篇",{"id":278,"slug":279,"title":280,"description":281,"content":282,"date":283,"image":86,"imageAlt":280,"author":284,"categories":285,"path":288,"minRead":29},177,"javascriptswitch的條件式運算","[Javascript]switch的條件式運算","switch的條件式運算 Javascript中的if和switch想必在JS中都滿常使用，有時候有較多判斷時 […]","\u003Ch1>switch的條件式運算\u003C\u002Fh1>\n\u003Cp>Javascript中的if和switch想必在JS中都滿常使用，有時候有較多判斷時會使用switch，通常用法如下\u003C\u002Fp>\n\u003Cpre>\u003Ccode>function checkFruit(fruit){\n  switch(fruit){\n    case &#039;Apple&#039;:\n      return &#039;Apple&#039;\n    case &#039;Banana&#039;:\n      return &#039;Banana&#039;\n    case &#039;Orange&#039;:\n      return &#039;Orange&#039;\n    default: \n      return &#039;It is a fruit&#039;\n  }\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>原本我以為fruit作為變數的概念傳進switch，再透過不同case判斷值達到判斷效果。直到寫到了條件式才發現這個想法是錯的！沒錯，以下這種寫法就會失效。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>function checkYear(year){\n  switch(year){\n    case (year &gt; 2000):\n      console.log(&#039;young&#039;)\n      break\n    case (year &lt;= 2000 &amp;&amp; year &gt;= 1980):\n      console.log(&#039;old&#039;)\n      break\n    case (year &lt; 1980 &amp;&amp; year &gt;= 1950):\n      console.log(&#039;too old&#039;)\n      break\n    default:\n      console.log(&quot;year:&quot; + year)\n      break\n  }\n}\ncheckYear(2010) \u002F\u002F year:2010\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>居然不是預想的顯示\u003Cem>young\u003C\u002Fem>，仔細查了一下才發現使用switch時的理解錯了。其實在這個例子中switch 進行條件判斷的過程長這樣：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>year === (year &gt; 2000) \u002F\u002Fexpression === (value &gt; 2000) \u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>當然不會有作用，換成敘述來講，swtich長這樣：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>switch (expression) {\n    case value: statement\n        break;\n    case value: statement\n        break;\n    case value: statement\n        break;\n    default: statement\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>雖然都是宣告\u003Cem>year\u003C\u002Fem>但一個是expression一個是真正傳進去的value，意義上不同。如果要想要用switch進行條件式運算，就要將if~else的true反過來寫，如下：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>function checkYear(true){\n  switch(year){\n    case (year &gt; 2000):\n      console.log(&#039;young&#039;)\n      break\n    case (year &lt;= 2000 &amp;&amp; year &gt;= 1980):\n      console.log(&#039;old&#039;)\n      break\n    case (year &lt; 1980 &amp;&amp; year &gt;= 1950):\n      console.log(&#039;too old&#039;)\n      break\n    default:\n      console.log(&quot;year:&quot; + year)\n      break\n  }\n}\ncheckYear(2010) \u002F\u002F young\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>這樣就會是以下判斷，就能順利當if~else來用囉\u003C\u002Fp>\n\u003Cpre>\u003Ccode>true === (year &gt; 2000)\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2>結論\u003C\u002Fh2>\n\u003Cp>起初是看到了一些文章在講用不同方式處理降低使用if-else的依賴來達到判斷目的，但老實說就算使用switch，程式碼也不會減少太多，所以要不要用就見仁見智囉XD\u003C\u002Fp>\n","2023-01-03T20:00:02",{"name":100,"link":101},[286,287],{"name":22,"slug":22},{"name":43,"slug":43},"\u002Fblog\u002Fjavascriptswitch的條件式運算",{"id":290,"slug":291,"title":292,"description":293,"content":294,"date":295,"image":296,"imageAlt":292,"author":297,"categories":298,"path":301,"minRead":9},218,"vueionic的vue使用pwa","[Vue]ionic的vue使用PWA","Ionic的vue使用PWA 其實在Ionic中使用vue的方式在原本的技術文件中就有說明很清楚，可以先參考原 […]","\u003Ch1>Ionic的vue使用PWA\u003C\u002Fh1>\n\u003Cp>其實在Ionic中使用vue的方式在原本的技術文件中就有說明很清楚，可以先參考原本的\u003Ca href=\"https:\u002F\u002Fionicframework.com\u002Fdocs\u002Fvue\u002Fpwa\" title=\"說明\">說明\u003C\u002Fa>。\u003Cbr \u002F>\n簡單翻譯一下\u003C\u002Fp>\n\u003Ch2>首先於指令端加入PWA\u003C\u002Fh2>\n\u003Cpre>\u003Ccode>vue add pwa\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2>在main.js中宣告ServiceWorker\u003C\u002Fh2>\n\u003Cp>如果上面步驟成功，會在src中找到registerServiceWorker.js，如果使用TS則會看到registerServiceWorker.ts，這時於main.js中加上宣告。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>import { createApp } from &#039;vue&#039;;\nimport App from &#039;.\u002FApp.vue&#039;;\nimport router from &#039;.\u002Frouter&#039;;\n\u002F\u002F Added by the CLI\nimport &#039;.\u002FregisterServiceWorker&#039;;\n\ncreateApp(App).use(router).mount(&#039;#app&#039;);\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>registerServiceWorker.js裡面長這樣，文件有說明這是基於 Workbox's webpack 產生的。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>import { register } from &#039;register-service-worker&#039;;\n\nif (process.env.NODE_ENV === &#039;production&#039;) {\n  register(`${process.env.BASE_URL}service-worker.js`, {\n    ready() {\n      console.log(\n        &#039;App is being served from cache by a service worker.\\n&#039; + &#039;For more details, visit https:\u002F\u002Fgoo.gl\u002FAFskqB&#039;\n      );\n    },\n    registered() {\n      console.log(&#039;Service worker has been registered.&#039;);\n    },\n    cached() {\n      console.log(&#039;Content has been cached for offline use.&#039;);\n    },\n    updatefound() {\n      console.log(&#039;New content is downloading.&#039;);\n    },\n    updated() {\n      console.log(&#039;New content is available; please refresh.&#039;);\n    },\n    offline() {\n      console.log(&#039;No internet connection found. App is running in offline mode.&#039;);\n    },\n    error(error) {\n      console.error(&#039;Error during service worker registration:&#039;, error);\n    },\n  });\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>其實做到這步，PWA的設定大致上完成了，部署後就有PWA效果囉。\u003Cbr \u002F>\n但當然不能這樣水一篇\u003C\u002Fp>\n\u003Ch2>Manifest組態設定\u003C\u002Fh2>\n\u003Cp>文件中有提到可以找到一份Manifest，但我找半天發現寫在模組的基本設定中，總不可能要我改模組內容吧，查了一下，在vue裡如果要設定PWA的Manifest，可以寫在vue的config中，請在vue.config.js中加入\u003C\u002Fp>\n\u003Cpre>\u003Ccode>  pwa:{\n    &quot;name&quot;: &quot;一般稱呼&quot;,\n    &quot;short_name&quot;: &quot;短稱&quot;,\n    &quot;theme_color&quot;: &quot;#FFFFFF&quot;,\n    &quot;lang&quot;: &quot;zh-Hant-TW&quot;,\n    &quot;description&quot;: &quot;這是具有PWA設定的網站&quot;,\n    &quot;icons&quot;: [\n      {\n        &quot;src&quot;: &quot;.\u002Fimg\u002Ficons\u002Fandroid-chrome-512x512.png&quot;,\n        &quot;sizes&quot;: &quot;512x512&quot;,\n        &quot;type&quot;: &quot;image\u002Fpng&quot;\n      }\n    ],\n    &quot;start_url&quot;: &quot;.&quot;,\n    &quot;display&quot;: &quot;standalone&quot;,\n    &quot;background_color&quot;: &quot;#000000&quot;\n  }\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>寫法參考google一下就能找到很多，如果懶得找可以參考\u003Ca href=\"https:\u002F\u002Fwww.letswrite.tw\u002Fpwa-manifest\u002F\" title=\"這篇\">這篇\u003C\u002Fa>\u003C\u002Fp>\n\u003Ch2>觸發安裝PWA\u003C\u002Fh2>\n\u003Cp>如果希望主動觸發安裝效果，可以參考以下做法：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>setup() {\n    const states = reactive({\n      deferredPrompt: null,\n    });\n    onMounted(() =&gt; {\n      window.addEventListener(&quot;beforeinstallprompt&quot;, e =&gt; {\n        e.preventDefault();\n        states.deferredPrompt = e;\n      });\n      window.addEventListener(&quot;appinstalled&quot;, () =&gt; {\n        states.deferredPrompt = null;\n      });\n      document.querySelector(&quot;#app&quot;).addEventListener(&quot;click&quot;, () =&gt; { \n        if (states.deferredPrompt) {\n          states.deferredPrompt.prompt();\n          states.deferredPrompt = null;\n        }\n      });\n    });\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>這是在app的\u003Cstrong>onMounted\u003C\u002Fstrong>完成後，點擊畫面中id為app元素時觸發PWA安裝詢問。\u003Cbr \u002F>\n原理為\u003C\u002Fp>\n\u003Col>\n\u003Cli>監聽 \u003Cem>beforeinstallprompt\u003C\u002Fem> 這個事件\u003C\u002Fli>\n\u003Cli>監聽到後把事件存起來\u003Cbr \u002F>\n3.監聽id為app的元素，當點擊事件發生時告知使用者這個 App 可以被安裝\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>備註\u003C\u002Fh2>\n\u003Cp>以上應該就能讓PWA正常運作，但記得此方式在IOS\u002FAndroid會有所不同。\u003Cbr \u002F>\nIOS不支援安裝提示，且Chrome不支援PWA安裝，哈哈，只能使用safari &gt; 分享 &gt; 加入主畫面，來使用PWA\u003Cbr \u002F>\nAndroid則都可以用，如果是IOS使用者可以先透過PC的Chrome來測試。\u003Cbr \u002F>\n很多人都說PWA是未來，但搞了這麼多年IOS支援度還是半屌子真令人憂心啊\u003Cbr \u002F>\n如果不清楚PWA可以參考這\u003Ca href=\"https:\u002F\u002Fithelp.ithome.com.tw\u002Fusers\u002F20071512\u002Fironman\u002F1222\" title=\"系列\">系列\u003C\u002Fa>\u003C\u002Fp>\n","2022-12-25T10:00:27","\u002Fapi\u002Fproxy-image?url=https%3A%2F%2Fpost.billxu.net%2Fwp-content%2Fuploads%2F2022%2F09%2Fframework_ionic.jpeg",{"name":100,"link":101},[299,300],{"name":22,"slug":22},{"name":61,"slug":61},"\u002Fblog\u002Fvueionic的vue使用pwa",{"id":303,"slug":304,"title":305,"description":306,"content":307,"date":308,"image":86,"imageAlt":305,"author":309,"categories":310,"path":312,"minRead":29},179,"javascript使用javascript來實作post的submit","[Javascript]使用Javascript來實作POST的submit","使用Javascript來實作POST的submit 這個做法應該滿多人知道的，也算是一種老派做法了吧，現在前 […]","\u003Ch1>使用Javascript來實作POST的submit\u003C\u002Fh1>\n\u003Cp>這個做法應該滿多人知道的，也算是一種老派做法了吧，現在前端框架不太需要這樣處理XDDD沒關係就當作筆記。因為需求關係，想在流程最後加上一個submit行為以POST方式打資料到某個位置，左想右想在我遇到的狀況中還是以Javascript來實踐比較，於是翻找到這個老派作法。\u003C\u002Fp>\n\u003Cp>現在說到用Javascript來處理POST或GET，直接就會想到ajax或是$.post\u002F$.get之類，好啦，jQuery也算老了，還有axios等等方式，但我需要打一個資料到目標後執行一段php函式並且打開新分頁，因為不想用jQuery來實作，左思右想，找到了這個做法。哎呀，怎麼沒想到這麼簡單的方式勒XDD\u003C\u002Fp>\n\u003Cp>概念很簡單，用Javascript建立一個隱藏的form，然後填入資料執行他，就這麼簡單。範例如下\u003C\u002Fp>\n\u003Cpre>\u003Ccode>\u002F**\n * sends a request to the specified url from a form. this will change the window location.\n * @param {string} path the path to send the post request to\n * @param {object} params the parameters to add to the url\n * @param {string} [method=post] the method to use on the form\n *\u002F\n\nfunction post(path, params, method=&#039;post&#039;) {\n\n  const form = document.createElement(&#039;form&#039;);\n  form.method = method;\n  form.action = path;\n  form.target = &#039;&#039;_blank &#039;&#039;;  \u002F\u002F如果要開新分頁\n\n  for (const key in params) {\n    if (params.hasOwnProperty(key)) {\n      const hiddenField = document.createElement(&#039;input&#039;);\n      hiddenField.type = &#039;hidden&#039;;\n      hiddenField.name = key;\n      hiddenField.value = params[key];\n\n      form.appendChild(hiddenField);\n    }\n  }\n\n  document.body.appendChild(form);\n  form.submit();\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>最後記得要執行。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>post(&#039;\u002Ftarget\u002F&#039;, {email: &#039;xxxx@mail&#039;});\u003C\u002Fcode>\u003C\u002Fpre>\n","2022-12-20T18:00:12",{"name":100,"link":101},[311],{"name":22,"slug":22},"\u002Fblog\u002Fjavascript使用javascript來實作post的submit",{"id":314,"slug":315,"title":316,"description":317,"content":318,"date":319,"image":296,"imageAlt":316,"author":320,"categories":321,"path":324,"minRead":29},170,"vue透過ionic部署ios-app的幾點筆記","[Vue]透過ionic部署ios app的幾點筆記","透過ionic部署ios app的幾點筆記 ionic可以直接透過指令來部署android \u002F ios的app […]","\u003Ch1>透過ionic部署ios app的幾點筆記\u003C\u002Fh1>\n\u003Cp>ionic可以直接透過指令來部署android \u002F ios的app，這篇筆記一下部署過程遇到的一些問題。基本上還是透過Capacitor(似乎基還是cordova)實現跨平台部署，ionic裡面已經包你先包裝好了。流程指令可以參考\u003Ca href=\"https:\u002F\u002Fionicframework.com\u002Fdocs\u002Fvue\u002Fyour-first-app\u002Fdeploying-mobile\" title=\"此篇\">此篇\u003C\u002Fa>。\u003Cbr \u002F>\n首先先打包專案\u003C\u002Fp>\n\u003Cpre>\u003Ccode>ionic build\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>選擇要部署的裝置類型，因為這次是ios的實作所以使用了ios的指令\u003C\u002Fp>\n\u003Cpre>\u003Ccode>ionic cap add ios\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>注意喔，官方文件有說明\u003C\u002Fp>\n\u003Cblockquote>\n\u003Cp>Every time you perform a build (e.g. ionic build) that updates your web directory (default: build), you'll need to copy those changes into your native projects:\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>因為cordova會將你的「網頁」專案放一份到app專案裡面，所以有內容更新要下此指令\u003C\u002Fp>\n\u003Cpre>\u003Ccode>ionic cap copy\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cblockquote>\n\u003Cp>Note: After making updates to the native portion of the code (such as adding a new plugin), use the sync command:\u003C\u002Fp>\n\u003C\u002Fblockquote>\n\u003Cp>或是使用同步指令，老實說我不確定兩者有什麼差異，這部分還得研究一下\u003C\u002Fp>\n\u003Cpre>\u003Ccode>ionic cap sync\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>到這邊後，要開始設定ios上的細節，部署ios一定得透過xcode，輸入指令打開xcode\u003C\u002Fp>\n\u003Cpre>\u003Ccode>ionic cap open ios\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>在xcode中，找到以下畫面\u003Cbr \u002F>\n\u003Cimg decoding=\"async\" src=\"\u002Fapi\u002Fproxy-image?url=https%3A%2F%2Fpost.billxu.net%2Fwp-content%2Fuploads%2F2022%2F10%2F%E6%88%AA%E5%9C%96-2022-10-29-%E4%B8%8B%E5%8D%889.03.11.png\" alt=\"\" \u002F>\u003Cbr \u002F>\n\u003Cem>signing &amp; capabilities\u003C\u002Fem>中設定\u003Cem>Team\u003C\u002Fem>和\u003Cem>Bundle Identifer\u003C\u002Fem>要設定，這邊基本上有設定好，就可以按xcode左上角的三角形進行編譯看看有沒有bug，若有就看log來除錯\u003Cbr \u002F>\n還不太懂的話可以參考\u003Ca href=\"https:\u002F\u002Fwww.cnblogs.com\u002Fchunzhulovefeiyue\u002Fp\u002F7512853.html\" title=\"此篇\">此篇\u003C\u002Fa>\u003C\u002Fp>\n\u003Ch3>live demo\u003C\u002Fh3>\n\u003Cp>到這邊成功的話，會發現專案中出現了ios資料夾，進入ios資料夾，可以進行live demo，將ios裝置接上mac\u003C\u002Fp>\n\u003Cpre>\u003Ccode>ionic cap run ios -l --external\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>這時應該可以在指令中看到自己的ios裝置名稱來進行部署，成功的話就能在ios裝置上看到自己的app囉\u003C\u002Fp>\n\u003Ch3>遇到問題\u003C\u002Fh3>\n\u003Cp>\u003Cem>Cocoapods No  'Podfile' found in the project directory.\u003C\u002Fem>\u003Cbr \u002F>\nCocoapods應該是Objective-C與Swift上使用的一種什麼東西，但我目前還不懂他是什麼XD\u003Ca href=\"https:\u002F\u002Fwww.appcoda.com.tw\u002Fcocoapods\u002F\" title=\"參考\">參考\u003C\u002Fa>\u003C\u002Fp>\n\u003Cp>查了一下，解決方式很簡單，輸入以下三個指令就好\u003C\u002Fp>\n\u003Cpre>\u003Ccode>pod init\n\nopen Podfile\n\npod install\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>但沒這麼容易XD我遇到了一些錯誤，這邊推薦老方式，重裝cocoapods(啊哈)\u003C\u002Fp>\n\u003Cpre>\u003Ccode>gem uninstall cocoapods\nbrew install cocoapods \u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>到這邊我就順利成功建立了cocoapods的podfile\u003C\u002Fp>\n","2022-12-14T14:00:52",{"name":100,"link":101},[322,323],{"name":22,"slug":22},{"name":61,"slug":61},"\u002Fblog\u002Fvue透過ionic部署ios-app的幾點筆記",{"id":326,"slug":327,"title":328,"description":329,"content":330,"date":331,"image":150,"imageAlt":328,"author":332,"categories":333,"path":336,"minRead":29},165,"vuevue3重新渲染元件的方式-vue3-rerender-a-component","[Vue]Vue3重新渲染元件的方式-Vue3 rerender a component","Vue3重新渲染元件的方式-Vue3 rerender a component 研究了一下vue3中如果要重新 […]","\u003Ch1>Vue3重新渲染元件的方式-Vue3 rerender a component\u003C\u002Fh1>\n\u003Cp>研究了一下vue3中如果要重新讀取元件(component)的作法，有時候真的無法靠資料更新之類的達到目的時，就必須重新讀取\u002F渲染元件。一開始要實作這件事時還真的沒什麼頭緒，直覺想到用v-if來實作，大概長這樣。\u003C\u002Fp>\n\u003Ch3>v-if法\u003C\u002Fh3>\n\u003Cp>簡單來說就是用renderComponent變數來控制component的起動\u003C\u002Fp>\n\u003Cpre>\u003Ccode>&lt;template&gt;\n    &lt;my-component v-if=&quot;renderComponent&quot; \u002F&gt;\n&lt;\u002Ftemplate&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>在setup中設定，nextTick是當畫面渲染完後再執行\u003C\u002Fp>\n\u003Cpre>\u003Ccode>import { nextTick, ref } from &#039;vue&#039;;\nconst renderComponent = ref(true);\n\nconst forceRerender = async () =&gt; {\n  \u002F\u002F 移除component的DOM\n  renderComponent.value = false;\n\n    \u002F\u002F 等待主畫面\n    await nextTick();\n\n    \u002F\u002F 重新加入這個component\n  renderComponent.value = true;\n};\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>再找個地方執行forceRerender就可以，基本原理就是將component移除、重新創建、掛載。\u003Cbr \u002F>\n但查了一下資料後，發現有更漂亮的作法。\u003C\u002Fp>\n\u003Ch3>鍵值改變驅動Key-Changing\u003C\u002Fh3>\n\u003Cp>我們只要新增一個屬性在component上，透過屬性值的改變就能觸發vue更新component。\u003C\u002Fp>\n\u003Cpre>\u003Ccode>&lt;template&gt;\n    &lt;my-component :key=&quot;componentKey&quot; \u002F&gt;\n&lt;\u002Ftemplate&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>透過forceRerender來更改componentKey的值\u003C\u002Fp>\n\u003Cpre>\u003Ccode>import { ref } from &#039;vue&#039;;\nconst componentKey = ref(0);\n\nconst forceRerender = () =&gt; {\n  componentKey.value += 1;\n};\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>接下來只要觸發forceRerender，就能更新component。\u003Cbr \u002F>\n但老實說我其實還不太確定為什麼這個做法較為優秀，歡迎留言討論XD\u003C\u002Fp>\n\u003Cp>很推薦參考這篇\u003Ca href=\"https:\u002F\u002Fmichaelnthiessen.com\u002Fforce-re-render\u002F\">參考資料\u003C\u002Fa>\u003C\u002Fp>\n","2022-12-10T10:00:13",{"name":100,"link":101},[334,335],{"name":22,"slug":22},{"name":61,"slug":61},"\u002Fblog\u002Fvuevue3重新渲染元件的方式-vue3-rerender-a-component",{"id":338,"slug":339,"title":340,"description":341,"content":342,"date":343,"image":150,"imageAlt":340,"author":344,"categories":345,"path":349,"minRead":29},225,"vue使用vue3打造trello式的工作看板","[Vue]使用Vue3打造trello式的工作看板","使用vue3打造trello式的工作看板 今年參加了{Laravel x Vue}Conf Taiwan，第一 […]","\u003Ch1>使用vue3打造trello式的工作看板\u003C\u002Fh1>\n\u003Cp>今年參加了\u003Ca href=\"https:\u002F\u002Flaravelconf.tw\u002F\" title=\"{Laravel x Vue}Conf Taiwan\">{Laravel x Vue}Conf Taiwan\u003C\u002Fa>，第一天工作坊主題是使用Vue3打造類似trello的工作看板，藉此重新檢視自己在使用Vue3的習慣與詢問一些自學想不透的問題。\u003Cbr \u002F>\n很感謝Kuro的推廣與分享，幫忙打廣告一下，有興趣學Vue的人請務必購買「\u003Ca href=\"https:\u002F\u002Fwww.tenlong.com.tw\u002Fproducts\u002F9789864345687\" title=\"重新認識 Vue.js：008天絕對看不完的 Vue.js 3 指南\">重新認識 Vue.js：008天絕對看不完的 Vue.js 3 指南\u003C\u002Fa>」XDDDDD\u003Cbr \u002F>\n先附上本次工作坊的\u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fbillxu0521\u002Fvue-trello-app\" title=\"repo\">repo\u003C\u002Fa>與\u003Ca href=\"https:\u002F\u002Fvue-trello-app.vercel.app\" title=\"demo\">demo\u003C\u002Fa>。\u003C\u002Fp>\n\u003Ch2>專案目標\u003C\u002Fh2>\n\u003Cp>本次專案是打造一個類似trello的工作記錄看板，工作坊的學習目標為以下：\u003C\u002Fp>\n\u003Col>\n\u003Cli>認識 vite 與 Vue.js 開發環境設定\u003C\u002Fli>\n\u003Cli>熟悉 Vue.js 3.x Compontions 與 Setup script\u003C\u002Fli>\n\u003Cli>使用 Tailwind CSS 結合 Vue.js 開發網頁介面\u003C\u002Fli>\n\u003Cli>使用 Pinia 管理跨元件狀態\u003C\u002Fli>\n\u003Cli>使用 Vue Router 實作前端路由功能 (edited)\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>使用技術如下：\u003C\u002Fp>\n\u003Cul>\n\u003Cli>Vite 3.1.8\u003C\u002Fli>\n\u003Cli>Vue.js 3.2\u003C\u002Fli>\n\u003Cli>Vue Router 4.1.5\u003C\u002Fli>\n\u003Cli>Pinia 2.0\u003C\u002Fli>\n\u003Cli>Vue Use\u003C\u002Fli>\n\u003Cli>Tailwindcss 3.2.14\u003C\u002Fli>\n\u003Cli>Vue.draggable.next\u003C\u002Fli>\n\u003Cli>unplugin-vue-components\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>這邊就不做製作步驟，主要紀錄自己學到的重點，有問題的人可以留言給我。\u003C\u002Fp>\n\u003Ch2>工作坊筆記\u003C\u002Fh2>\n\u003Ch3>script setup 寫法\u003C\u002Fh3>\n\u003Cp>Vue3提供了setup的寫法，通常有兩種一種是傳統的export default\u003C\u002Fp>\n\u003Cpre>\u003Ccode>export default  {\n  setup() {\n  \u002F\u002F內容\n  }\n  }\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>另一種為script setup 寫法\u003C\u002Fp>\n\u003Cpre>\u003Ccode>&lt;script setup&gt;\n\u002F\u002F內容\n&lt;\u002Fscript&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>這邊建議是盡可能採用script setup 寫法，官方也較推薦這種撰寫方式，雖然要習慣一下，但好處是可以簡化程式碼，實際寫過真的感覺很清爽！\u003C\u002Fp>\n\u003Ch3>Vue Use\u003C\u002Fh3>\n\u003Cp>這是一個滿實用的Vue工作組，在這個專案中導入是為了 focus 與 onClickOutside這兩支api，可以協助我們快速處理元素的聚焦與脫離，像是在trello中，點擊某個卡片會跳出燈箱(lightbox)以及點擊燈箱後方的遮罩可以關閉燈箱這種流程。\u003C\u002Fp>\n\u003Ch3>v-bind傳遞物件到子元件\u003C\u002Fh3>\n\u003Cp>之前我會使用prop傳遞資料到子元件像是這樣\u003C\u002Fp>\n\u003Cpre>\u003Ccode>&lt;TaskItem\n          :id=&quot;element.id&quot;\n          :title=&quot;element.title&quot;\n          :tasks=&quot;element.tasks&quot;\n        \u002F&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>但其實物件可以用v-bind來傳遞寫法如下：\u003C\u002Fp>\n\u003Cpre>\u003Ccode> &lt;TaskItem\n          v-bind=&quot;element&quot;\n        \u002F&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>而在子元件中需要這樣接值\u003C\u002Fp>\n\u003Cpre>\u003Ccode>&lt;script setup&gt;\nimport { ref, computed } from &quot;vue&quot;;\nconst props = defineProps({\n  id: String,\n  title: String,\n  content: String,\n}); \n&lt;\u002Fscript&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>如果會對資料修改，那記得要再宣告一個變數來接避免污染父元件的物件喔\u003C\u002Fp>\n\u003Cpre>\u003Ccode>const title = ref(props.title);\n\u002F\u002F修改title\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>Pinia\u003C\u002Fh3>\n\u003Cp>類似Vuex的狀態管理套件，超好用啊，我其實沒用過Vuex，只有用過原生的依賴注入，Pinia的使用真的方便很多。\u003Cbr \u002F>\n這邊就不寫使用方法了，請參考\u003Ca href=\"https:\u002F\u002Fpinia.vuejs.org\u002F\" title=\"官方資料\">官方資料\u003C\u002Fa>\u003C\u002Fp>\n\u003Ch4>取用函式與值\u003C\u002Fh4>\n\u003Cp>要注要透過Pinia取用函式與值的作法\u003Cbr \u002F>\n如果closeEditTask是函式，那可以用解構方式來引用，但這樣的話就無法觀察到物件中的狀態或值\u003C\u002Fp>\n\u003Cpre>\u003Ccode>以下兩種相同\nconst { closeEditTask } = store;  \u002F\u002F物件解構\nconst { closeEditTask } = store.closeEditTask;  \n解構無法被觀察\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>如果是狀態或值，則需要computed\u003C\u002Fp>\n\u003Cpre>\u003Ccode>const currentEditTask = computed(() =&gt; store.currentEditTask); \u002F\u002FcurrentEditTask這是狀態\n因為要關注狀態所以用computed\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>在script中取的DOM\u003C\u002Fh3>\n\u003Cp>想取得DOM內容的話可以用ref，像這樣\u003C\u002Fp>\n\u003Cpre>\u003Ccode>const target = ref();\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>然後網頁元入記得宣告\u003C\u002Fp>\n\u003Cpre>\u003Ccode>&lt;textarea\n      ref=&quot;target&quot;    \n    &gt;&lt;\u002Ftextarea&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>這樣就可以透過target來操作textarea的內容\u003C\u002Fp>\n\u003Ch3>偵測自己的click\u003C\u002Fh3>\n\u003Cp>有點像以前寫jQuery的監聽，對某元素監聽時要加上this不然會監聽到所有同元素，vue的@click也是\u003C\u002Fp>\n\u003Cpre>\u003Ccode>&lt;div\n    @click.self=&quot;closeEditTask&quot;\n  &gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>src中的compoment與views命名習慣\u003C\u002Fh3>\n\u003Cp>這不是強制，習慣上只會在某地方特定使用一次的可以放在\u003Cstrong>views\u003C\u002Fstrong>中，例如首頁，如果是重複使用的元件則用\u003Cstrong>compoment\u003C\u002Fstrong>\u003C\u002Fp>\n\u003Ch3>Vue.draggable.next\u003C\u002Fh3>\n\u003Cp>支援拖曳效果的套件，附上\u003Ca href=\"https:\u002F\u002Fsortablejs.github.io\u002Fvue.draggable.next\u002F#\u002Fsimple\" title=\"官方說明\">官方說明\u003C\u002Fa>\u003C\u002Fp>\n\u003Ch3>unplugin-vue-components\u003C\u002Fh3>\n\u003Cp>超棒的玩意，可以直接以命名呼叫compoment，以往我們要呼叫compoment通常要先引入\u003C\u002Fp>\n\u003Cpre>\u003Ccode>import {xxx} from  &quot;xxx&quot;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>但這個套件可以直接省略import，以原名就能直接呼叫，太棒啦！\u003C\u002Fp>\n","2022-12-04T13:36:23",{"name":100,"link":101},[346,347,348],{"name":22,"slug":22},{"name":43,"slug":43},{"name":61,"slug":61},"\u002Fblog\u002Fvue使用vue3打造trello式的工作看板",{"id":351,"slug":352,"title":353,"description":354,"content":355,"date":356,"image":150,"imageAlt":353,"author":357,"categories":358,"path":361,"minRead":29},160,"vuevue3引入vue-meta的方式","[Vue]Vue3引入vue-meta的方式","Vue3引入vue-meta的方式 如果要在vue中動態引入Html meta tag，可以使用vue-met […]","\u003Ch2>Vue3引入vue-meta的方式\u003C\u002Fh2>\n\u003Cp>如果要在vue中動態引入Html meta tag，可以使用vue-meta，但vue3需要使用3.0.0，預設安裝會是2.4.0，這邊記錄一下實作過程。其實可以參考官方說明：\u003Ca href=\"https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fvue-meta\u002Fv\u002F3.0.0-alpha.10\" title=\"參考文件\">參考文件\u003C\u002Fa>，流程如下：\u003C\u002Fp>\n\u003Ch3>調整版本\u003C\u002Fh3>\n\u003Cp>調整vue-meta版本，於\u003Cem>package.json\u003C\u002Fem>中修改\u003C\u002Fp>\n\u003Cpre>\u003Ccode>- &quot;vue-meta&quot;: &quot;^2.4.0&quot;,\n+ &quot;vue-meta&quot;: &quot;^3.0.0-alpha.7&quot;,\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>修改完後記得重新安裝套件\u003C\u002Fp>\n\u003Cpre>\u003Ccode>npm install\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>為Vue App引入\u003C\u002Fh3>\n\u003Cp>在main.js中加入createMetaManager\u003C\u002Fp>\n\u003Cpre>\u003Ccode>import { createMetaManager } from &#039;vue-meta&#039;\n\nconst app = createApp(App)\n  .use(router)\n  .use(store)\n  .use(createMetaManager()) \u002F\u002F add this line\n\nawait router.isReady()\napp.mount(&#039;#app&#039;)\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>在App.vue中加入顯示tag\u003C\u002Fh3>\n\u003Cp>於App.vue中加入顯示tag，並且加上顯示tag的範本，以title為例子\u003C\u002Fp>\n\u003Cpre>\u003Ccode>&lt;template&gt;\n  &lt;metainfo&gt;\n    &lt;template v-slot:title=&quot;{ content }&quot;&gt;{{ content ? `${content} | SITE_NAME` : `SITE_NAME` }}&lt;\u002Ftemplate&gt;\n  &lt;\u002Fmetainfo&gt;\n  &lt;header \u002F&gt;\n  &lt;router-view \u002F&gt;\n  &lt;footer \u002F&gt;\n&lt;\u002Ftemplate&gt;\n\n&lt;script&gt;\nimport { useMeta } from &#039;vue-meta&#039;\n\nexport default {\n  setup () {\n    useMeta({\n      title: &#039;&#039;,\n      htmlAttrs: { lang: &#039;zh-tw&#039; }\n    })\n  }\n}\n&lt;\u002Fscript&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>到這邊應該可以看到預設顯示的字串了「SITE_NAME」。\u003C\u002Fp>\n\u003Ch3>為個別元件加上tag\u003C\u002Fh3>\n\u003Cp>在不同元件加上tag，切換過去後會顯示\u003C\u002Fp>\n\u003Cpre>\u003Ccode>import { useMeta } from &#039;vue-meta&#039;\n\nexport default {\n  setup () {\n    useMeta({ title: &#039;Single Page&#039; })\n  }\n}\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>設定成功的話，在個別元件中會看到標題已經是「Single Page｜SITE_NAME」。\u003Ca href=\"https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Fvue-meta\u002Fv\u002F3.0.0-alpha.10\" title=\"參考文件\">參考文件\u003C\u002Fa>\u003C\u002Fp>\n","2022-11-28T20:00:34",{"name":100,"link":101},[359,360],{"name":22,"slug":22},{"name":61,"slug":61},"\u002Fblog\u002Fvuevue3引入vue-meta的方式",{"id":363,"slug":364,"title":365,"description":366,"content":367,"date":368,"image":150,"imageAlt":365,"author":369,"categories":370,"path":374,"minRead":29},158,"vue使用vue-gtag串接google-analytics","[Vue]使用vue-gtag串接google analytics","使用vue-gtag串接google analytics 其實這篇只是暫時紀錄，因為我還是搞不動為什麼可以運作 […]","\u003Ch1>使用vue-gtag串接google analytics\u003C\u002Fh1>\n\u003Cp>其實這篇只是暫時紀錄，因為我還是搞不動為什麼可以運作....XD與其講Vue不如說是GTM和GA4串接上的一些狀況整理。\u003C\u002Fp>\n\u003Cp>總之呢，準備要在Vue專案上串接GA(google analytics)，找了一下，通常會推薦vue-gtag(vue-analytics已經不維護了，建議用vue-gtag)。於是參考了https:\u002F\u002Fmatteo-gabriele.gitbook.io\u002Fvue-gtag\u002F\u003Ca href=\"https:\u002F\u002Fmatteo-gabriele.gitbook.io\u002Fvue-gtag\u002F\" title=\"原作者說明\">原作者說明\u003C\u002Fa>就開始實作。\u003C\u002Fp>\n\u003Ch2>vue-gtag基本安裝\u003C\u002Fh2>\n\u003Cp>按照原說明文件，先安裝vue-gtag\u003C\u002Fp>\n\u003Cpre>\u003Ccode>$ npm add vue-gtag\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>然後在main.js引入vuegtag，\u003Cstrong>GA_MEASUREMENT_ID\u003C\u002Fstrong>就使用從GA服務開啟後取得的「評估ID」\u003C\u002Fp>\n\u003Cpre>\u003Ccode>import { createApp } from &quot;vue&quot;;\nimport App from &quot;.\u002FApp.vue&quot;;\nimport VueGtag from &quot;vue-gtag&quot;;\n\ncreateApp(App).use(VueGtag, {\n  config: { id: &quot;GA_MEASUREMENT_ID&quot; }\n}).mount(&quot;#app&quot;);\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>理論上這樣就完成了。然而GA4的串流提示老樣子可能會等數小時才有反應，於是我就等了.....48小時，然後依然沒反應，登愣\u003Cbr \u002F>\n由於GA4我還沒有用程式碼引入方式串接過，想說那誒啊捏，變這麼難接喔。\u003Cbr \u002F>\n稍微查了一下，結果....毫無收穫XDDD\u003Cbr \u002F>\n好吧，只好換方向來處理，既然GA4的很多說明跟GTM(google tag manager)有關係，那就用GTM來實作看看好了。\u003C\u002Fp>\n\u003Ch2>GTM串接GA4\u003C\u002Fh2>\n\u003Cp>基本流程如下：\u003C\u002Fp>\n\u003Col>\n\u003Cli>在GTM中開啟一個工作區\u003C\u002Fli>\n\u003Cli>加入代碼：選擇GA4設定\u003C\u002Fli>\n\u003Cli>評估ID填入GA中取得的「評估ID」\u003C\u002Fli>\n\u003Cli>觸發條件選擇：All page\u003C\u002Fli>\n\u003Cli>儲存設定，再來發布容器\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>回到Vue專案中，你申請完GTM工作區後應該會取的一組gtag，會長這樣：GTM-XXXXXX，將這個識別ID加回到到main.js，沒錯就是這串\u003C\u002Fp>\n\u003Cpre>\u003Ccode>import { createApp } from &quot;vue&quot;;\nimport App from &quot;.\u002FApp.vue&quot;;\nimport VueGtag from &quot;vue-gtag&quot;;\n\ncreateApp(App).use(VueGtag, {\n  config: { id: &quot;GA_MEASUREMENT_ID&quot; } &lt;---這個\n}).mount(&quot;#app&quot;);\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>設定完後，可以開啟GTM中的預覽來檢查，建議chrome可以裝一個擴充「Tag Assistant」以方便檢視是否有安裝順利。\u003C\u002Fp>\n\u003Ch3>遇到問題\u003C\u002Fh3>\n\u003Cp>其實上述做完後，我在GTM的預覽模式中還是會失敗，「Tag Assistant」提示我代碼有放，但會無法取得一個測試用的js，目前尚未找到原因，但在GA的即時模式中已經可以看到資料進來了，只好慢慢找問題與調整了，如果有人遇到類似問題歡迎一起討論。\u003C\u002Fp>\n","2022-11-20T10:00:15",{"name":100,"link":101},[371,372,373],{"name":22,"slug":22},{"name":43,"slug":43},{"name":61,"slug":61},"\u002Fblog\u002Fvue使用vue-gtag串接google-analytics",{"id":376,"slug":377,"title":378,"description":379,"content":380,"date":381,"image":150,"imageAlt":378,"author":382,"categories":383,"path":386,"minRead":29},153,"vuevue3中子元件傳資料給父元件的作法","[Vue]Vue3中子元件傳資料給父元件的作法","Vue3中子元件傳資料給父元件的作法 隨著使用Vue越來越深入，一定會遇到父子元件傳資料的需求，我想有稍微研究 […]","\u003Ch1>Vue3中子元件傳資料給父元件的作法\u003C\u002Fh1>\n\u003Cp>隨著使用Vue越來越深入，一定會遇到父子元件傳資料的需求，我想有稍微研究過一定知道這個口訣「Props in, Event out」XD，父傳子用props，子傳父用event。那今天就來記錄我實作時遇到的狀況。\u003C\u002Fp>\n\u003Cp>在運作上，子元件透過觸發「事件event」的作法來更新父元件的資料，Vue3中使用了\u003Cstrong>v-on\u003C\u002Fstrong>來實作，父元件需要有個監聽事件如下：\u003C\u002Fp>\n\u003Ch1>parent-component.js\u003C\u002Fh1>\n\u003Cpre>\u003Ccode>&lt;child-component\n    @listenevent=&quot;updateEvent&quot;\n>&lt;\u002Fchild-component&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cpre>\u003Ccode>&lt;script&gt;\nexport default defineComponent({\n   setup() {\n\n    },\n  methods: {\n    updateEvent(val) {\n      \u002F\u002F這邊會被觸發\n      \u002F\u002F做你想做的事\n      console.log(val)\n    }\n  }\n});\n&lt;\u002Fscript&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>先準備一個listenevent監聽事件，取名為updateEvent，當子元件打資料過來時會觸發updateEvent。\u003C\u002Fp>\n\u003Ch3>child-component.js\u003C\u002Fh3>\n\u003Cpre>\u003Ccode>&lt;script&gt;\nexport default defineComponent({\n   setup(_,{ emit }) {\n    const handleChange = (value) =&gt; {\n        emit(&quot;listenevent&quot;,value)\n    }\n    onMounted(() =&gt; {\n      handleChange(&quot;hello&quot;)\n      });\n    },\n});\n&lt;\u002Fscript&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>接著在子元件增加觸發\u003Cstrong>listenevent\u003C\u002Fstrong>事件的方式，如上\u003Cstrong>handleChange\u003C\u002Fstrong>，範例是onMounted時會觸發\u003Cstrong>handleChange\u003C\u002Fstrong>，這時會打&quot;hello&quot;回到父元件，這時父元件的\u003Cstrong>updateEvent\u003C\u002Fstrong>就會被觸發，應該就能看到&quot;hello&quot;囉。\u003C\u002Fp>\n\u003Cp>如果出現「...vue 3 emit warning &quot; Extraneous non-emits event listeners&quot;...」的警告提示，記得子元件要加上emits的宣告。\u003C\u002Fp>\n\u003Ch3>child-component.js\u003C\u002Fh3>\n\u003Cpre>\u003Ccode>&lt;script&gt;\nexport default defineComponent({\n   emits: [&quot;getGameitle&quot;], \u002F\u002F&lt;---加上宣告\n   setup(_,{ emit }) {\n    const handleChange = (value) =&gt; {\n        emit(&quot;listenevent&quot;,value)\n    }\n    onMounted(() =&gt; {\n      handleChange(&quot;hello&quot;)\n      });\n    },\n});\n&lt;\u002Fscript&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n","2022-11-14T20:00:08",{"name":100,"link":101},[384,385],{"name":22,"slug":22},{"name":61,"slug":61},"\u002Fblog\u002Fvuevue3中子元件傳資料給父元件的作法",{"id":388,"slug":389,"title":390,"description":391,"content":392,"date":393,"image":296,"imageAlt":390,"author":394,"categories":395,"path":398,"minRead":29},150,"vueionic中將資料與style綁定的方式","[Vue]ionic中將資料與style綁定的方式","ionic中將資料與style綁定的方式 在Vue中如果需要資料丟到style中，例如動態讀取背景圖片，一種寫 […]","\u003Ch1>ionic中將資料與style綁定的方式\u003C\u002Fh1>\n\u003Cp>在Vue中如果需要資料丟到style中，例如動態讀取背景圖片，一種寫法是這樣。\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-html\">&lt;template&gt;\n    &lt;div v-for=&quot;(item) in data&quot;&gt;\n        &lt;div class=&quot;image-container&quot; v-bind:style=&quot;{ backgroundImage: &#039;url(&#039; + item.image + &#039;)&#039; }&quot;&gt;\n    &lt;\u002Fdiv&gt;\n    &lt;\u002Fdiv&gt;\n&lt;\u002Ftemplate&gt;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>或是這樣\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-html\">&lt;template&gt;\n      &lt;div class=&quot;image-container&quot;&gt;\n        Image Container\n      &lt;\u002Fdiv&gt;\n&lt;\u002Ftemplate&gt;\n\n&lt;script &gt;\nconst props = {\n    image: &quot;xxxxxxx&quot;,\n  }\n&lt;\u002Fscript &gt;\n&lt;style&gt;\n.image-container{\n    background: v-bind(&quot;props.image&quot;) no-repeat left center fixed;\n}\n&lt;\u002Fstyle&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>第二種寫法在ionic中可以使用，但第一種則會判讀不到，需要改成這樣寫：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-html\">&lt;template&gt;\n    &lt;div v-for=&quot;(item) in data&quot;&gt;\n        &lt;div class=&quot;image-container&quot;  :style=&quot;`background-image: url(${image});`&quot;&gt;&lt;\u002Fdiv&gt;\n    &lt;\u002Fdiv&gt;\n&lt;\u002Ftemplate&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n","2022-11-08T13:00:29",{"name":100,"link":101},[396,397],{"name":22,"slug":22},{"name":61,"slug":61},"\u002Fblog\u002Fvueionic中將資料與style綁定的方式",{"id":400,"slug":401,"title":402,"description":403,"content":404,"date":405,"image":86,"imageAlt":402,"author":406,"categories":407,"path":411,"minRead":29},147,"nodeaxios自動加上網址","[Node]axios自動加上網址","axios自動加上網址 我使用了axios來實作vue中請求資料的部分，但網站中有許多地方要請求不同地方的資料 […]","\u003Ch1>axios自動加上網址\u003C\u002Fh1>\n\u003Cp>我使用了axios來實作vue中請求資料的部分，但網站中有許多地方要請求不同地方的資料時，就需要有個地方統一管理這個位置，這時就可以用到axios.baseURL\u003C\u002Fp>\n\u003Cp>只要在main.js中宣告axios後加上這行\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-javascript\">import axios from &#039;axios&#039;;\n\naxios.defaults.baseURL=&#039;http:\u002F\u002Flocalhost:3031&#039;;  \u002F\u002F宣告baseURL\n\napp.use(VueAxios, axios)\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>之後只要使用了axios，他會自動加上baseURL，例如\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-javascript\">let url = &#039;\u002Fapi&#039;\n axios.get(url)\n .then((res)=&gt;{\n\n })\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>他就會送出這個位置http:\u002F\u002Flocalhost:3031\u002Fapi\u003C\u002Fp>\n\u003Cp>通常還會搭配.env文件來使用，將環境變數先存在.env中，例如建立一個.env.dev的文件，裡面寫下\u003C\u002Fp>\n\u003Cpre>\u003Ccode>NODE_ENV=development\nAPI_URL=http:\u002F\u002Flocalhost:3031\u002F\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>然後在main.js中串上env的環境變數就可以了\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-javascript\">import axios from &#039;axios&#039;;\n\naxios.defaults.baseURL=process.env.API_URL\n\u003C\u002Fcode>\u003C\u002Fpre>\n","2022-11-02T10:00:27",{"name":100,"link":101},[408,409,410],{"name":22,"slug":22},{"name":43,"slug":43},{"name":61,"slug":61},"\u002Fblog\u002Fnodeaxios自動加上網址",{"id":413,"slug":414,"title":415,"description":416,"content":417,"date":418,"image":296,"imageAlt":415,"author":419,"categories":420,"path":423,"minRead":29},136,"vueionic中typescript改寫為javascript的一些做法","[Vue]ionic中TypeScript改寫為JavaScript的一些做法","ionic中TypeScript改寫為JavaScript的一些做法 好吧，這問題完全是個人還沒學TypeSc […]","\u003Ch1>ionic中TypeScript改寫為JavaScript的一些做法\u003C\u002Fh1>\n\u003Cp>好吧，這問題完全是個人還沒學TypeScript的關係所衍生出來的(遮臉)，使用ionic的vue版本時，預設會有大量TypeScript的寫法，官方有說他們偏好TypeScript，但實際使用時可以關閉TypeScript(\u003Ca href=\"https:\u002F\u002Fionicframework.com\u002Fdocs\u002Fvue\u002Fquickstart\" title=\"參考如下\">參考\u003C\u002Fa>)，由於個人對TypeScript沒那麼熟，所以這次開發就關光光了，但也出現這次得狀況。\u003C\u002Fp>\n\u003Cp>這次在\u003Ca href=\"https:\u002F\u002Fionicframework.com\u002Fdocs\u002Fapi\u002Finfinite-scroll\" title=\"ion-infinite-scroll\">ion-infinite-scroll\u003C\u002Fa>中遇到一些問題，像是(可以參考連結中的範例)：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-javascript\">const items = ref&lt;number[]&gt;([]);\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>對我來說這是一個陌生的寫法，一開始看到錯誤還在想到底哪邊出了問題，回想了一下發現這應該是個ts的寫法，從前後文推測這只是一個陣列宣告，於是改寫成這樣就通了。\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-javascript\">const items = [];\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>另外像是\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-javascript\">const loadData = (ev: InfiniteScrollCustomEvent) =&gt; {\n      setTimeout(() =&gt; {\n        pushData();\n        console.log(&#039;Loaded data&#039;);\n        ev.target.complete();\n        \u002F\u002F App logic to determine if all data is loaded\n        \u002F\u002F and disable the infinite scroll\n        if (items.value.length === 1000) {\n          ev.target.disabled = true;\n        }\n      }, 500);\n    }\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>其實也是一般的箭頭函式，ev宣告是ts的寫法，所以這樣改寫也就可以囉。\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-javascript\">const loadData = (event) =&gt; {\n      setTimeout(() =&gt; {\n        pushData();\n        console.log(&#039;Loaded data&#039;);\n        event.target.complete();\n\n        \u002F\u002F App logic to determine if all data is loaded\n        \u002F\u002F and disable the infinite scroll\n        if (skipitems === 2000) {\n          event.target.disabled = true;\n        }\n      }, 500);\n    }\u003C\u002Fcode>\u003C\u002Fpre>\n","2022-10-02T16:10:02",{"name":100,"link":101},[421,422],{"name":22,"slug":22},{"name":61,"slug":61},"\u002Fblog\u002Fvueionic中typescript改寫為javascript的一些做法",{"id":425,"slug":426,"title":427,"description":428,"content":429,"date":430,"image":86,"imageAlt":427,"author":431,"categories":432,"path":435,"minRead":29},133,"vueionic中取得route資訊的方式","[Vue]ionic中取得route資訊的方式","ionic中取得route資訊 一般在Vue中想取得url的參數或是網域位置，如果有使用vue-route的話 […]","\u003Ch1>ionic中取得route資訊\u003C\u002Fh1>\n\u003Cp>一般在Vue中想取得url的參數或是網域位置，如果有使用vue-route的話，可以直接取用，方法如下\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-vue\">const params = $route.params\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>或是在3.0要這要宣告\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-vue\">const params = this.$route.params\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>例如localhost\u002Ftest?username=橫隔膜，就可以以下面形式取出。\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-vue\">const params = this.$route.params\nconst username = params.username\n\u002F\u002Fparams : test\n\u002F\u002Fusername : 橫隔膜\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>但最近使用ionic做這這件時，這樣宣告會一直跳出錯誤，發現不能直接取用this.$route，甚至不能用this。找了一段時間終於在ionic的\u003Ca href=\"https:\u002F\u002Fionicframework.com\u002Fdocs\u002Fvue\u002Fnavigation\" title=\"說明文件\">說明文件\u003C\u002Fa>中找到正確寫法，其實只要拿掉&quot;$&quot;就可以囉。\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-vue\">const params= route.params;\u003C\u002Fcode>\u003C\u002Fpre>\n","2022-09-27T16:10:00",{"name":100,"link":101},[433,434],{"name":22,"slug":22},{"name":61,"slug":61},"\u002Fblog\u002Fvueionic中取得route資訊的方式",{"id":437,"slug":438,"title":439,"description":440,"content":441,"date":442,"image":86,"imageAlt":439,"author":443,"categories":444,"path":447,"minRead":29},130,"omkeas關於資源取用的xmlhttprequest問題","[OmkeaS]關於資源取用的XMLHttpRequest問題","關於資源取用的XMLHttpRequest問題 終於將機器從VM搬到Proxmox上了，也移植到docker中 […]","\u003Ch1>關於資源取用的XMLHttpRequest問題\u003C\u002Fh1>\n\u003Cp>終於將機器從VM搬到Proxmox上了，也移植到docker中，然後就發現到一個問題，條目的圖片居然無法從Viewer中讀取出來。怎麼會這樣勒，從console中會看到有個錯誤，寫著\u003Cem>This request has been blocked; the content must be served over HTTPS\u003C\u002Fem>，\u003Cem>會發現到有個關鍵字XMLHttpRequest\u003C\u002Fem>，看來是出現了Https取用Http的的混用問題。\u003Cbr \u002F>\n但說來慚愧，現在才注意到這個問題也是因為原本網站還沒有使用Https，所以一直沒發現到OmekaS存在這個狀況。\u003C\u002Fp>\n\u003Ch2>OmekaS的資源取用問題\u003C\u002Fh2>\n\u003Cp>我們會發生這個問題要先提到Universal viewer這個模組，經過檢查發現Universal viewer會呼叫配置檔以及IIIF的API，然後都是走\u003Cem>Http\u003C\u002Fem>，而這個取用路徑似乎包在Universal viewer的JS中，很難直接修改。\u003C\u002Fp>\n\u003Ch2>解決作法\u003C\u002Fh2>\n\u003Cp>遇到XMLHttpRequest問題其實滿好處理的，從Stockflow上可以找到好幾種建議，大致上會分三種：\u003C\u002Fp>\n\u003Cblockquote>\n\u003Col>\n\u003Cli>As Chrome suggests, change your API calls to use HTTPs (recommended)：將取用資源也改為Https\u003C\u002Fli>\n\u003Cli>Use HTTP instead of HTTPs：網站改為Http\u003C\u002Fli>\n\u003Cli>Add the following meta tag to your \u003Chead> element in your HTML：在head加入以下宣告\u003C\u002Fli>\n\u003C\u002Fol>\n\u003C\u002Fblockquote>\n\u003Cpre>\u003Ccode class=\"language-html\">&lt;meta http-equiv=&quot;Content-Security-Policy&quot; content=&quot;upgrade-insecure-requests&quot;&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>第二種很明顯在這個年代來說不適合，而第一種因為很難改動Universal viewer的呼叫方式變得很困難執行，雖然是最佳解。於是剩下第三種，直接允許瀏覽器接受混用狀態。\u003C\u002Fp>\n\u003Cp>實作方式有兩種：\u003C\u002Fp>\n\u003Col>\n\u003Cli>在theme的view&gt;layout.phtml中加入標頭\u003C\u002Fli>\n\u003Cli>在theme中的view&gt;universal viewer&gt;play.phtml中加入標頭\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>就看專案中怎麼取用Universal viewer工具囉。\u003C\u002Fp>\n","2022-09-20T12:00:42",{"name":100,"link":101},[445,446],{"name":33,"slug":34},{"name":57,"slug":57},"\u002Fblog\u002Fomkeas關於資源取用的xmlhttprequest問題",{"id":449,"slug":450,"title":451,"description":452,"content":453,"date":454,"image":86,"imageAlt":451,"author":455,"categories":456,"path":459,"minRead":29},113,"frontend筆記-使用selector尋找父元素的方式parent幾種相關做法","[Frontend]筆記-使用Selector尋找父元素的方式(parent)幾種相關做法","筆記-使用Selector尋找父元素的方式(parent)幾種相關做法 最近有個小需求得回去寫了點jquery […]","\u003Ch1>筆記-使用Selector尋找父元素的方式(parent)幾種相關做法\u003C\u002Fh1>\n\u003Cp>最近有個小需求得回去寫了點jquery，因為很久沒寫，也意外的發現以前沒使用過的一個api。\u003C\u002Fp>\n\u003Cp>通常要透過selector找特定父元素時都會使用\u003Cem>.parent('element')\u003C\u002Fem>吧，但\u003Cem>.parent('element')\u003C\u002Fem>只會往前一層，如果想要繼續往前找，可以使用\u003Cem>.parents('element')\u003C\u002Fem>。\u003Cbr \u002F>\n但\u003Cem>.parents('element')\u003C\u002Fem>會有個特性，會不斷的往前找，把所有符合條件的元素都撈出來。\u003Cbr \u002F>\n如果只想要往前撈到一個特定父元素的話，可以使用\u003Cem>closest()\u003C\u002Fem>，這樣就只會往前找但符合條件就會停止。\u003C\u002Fp>\n\u003Ch2>範例\u003C\u002Fh2>\n\u003Cpre>\u003Ccode class=\"language-html\">&lt;div class=&quot;parnet selected&quot;&gt;\n  Hello Parent\n  &lt;div class=&quot;child selected&quot;&gt;\n    &lt;div&gt;\n      &lt;p&gt;Hello &lt;\u002Fp&gt;\n    &lt;\u002Fdiv&gt;\n  &lt;\u002Fdiv&gt;\n&lt;\u002Fdiv&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>以上述解釋的話會有以下狀況\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-jquery\">$(&#039;p&#039;).parent(&#039;selected&#039;);\u002F\u002F找到&lt;div class=&quot;child selected&quot;&gt;\n$(&#039;p&#039;).parents(&#039;selected&#039;);\u002F\u002F找到&lt;div class=&quot;child selected&quot;&gt;與&lt;div class=&quot;parnet selected&quot;&gt;\n$(&#039;p&#039;).closest(&#039;selected&#039;);\u002F\u002F找到&lt;div class=&quot;child selected&quot;&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2>小結\u003C\u002Fh2>\n\u003Cul>\n\u003Cli>parent() 只往上找一層\u003C\u002Fli>\n\u003Cli>closest() 往上找，找到符合條件的就會停止\u003C\u002Fli>\n\u003Cli>parents() 一直往上找，找出所有符合條件的(會丟回陣列)\u003C\u002Fli>\n\u003C\u002Ful>\n","2022-09-10T20:00:53",{"name":100,"link":101},[457,458],{"name":22,"slug":22},{"name":33,"slug":34},"\u002Fblog\u002Ffrontend筆記-使用selector尋找父元素的方式parent幾種相關做法",{"id":461,"slug":462,"title":463,"description":464,"content":465,"date":466,"image":86,"imageAlt":463,"author":467,"categories":468,"path":471,"minRead":29},72,"vue在docker中hot-reloading失效的問題","[Vue]在docker中Hot-Reloading失效的問題","Vue在docker中hot reload失效的問題 這陣子在嘗試在開發流程中導入docker，有次寫Vue時 […]","\u003Ch1>Vue在docker中hot reload失效的問題\u003C\u002Fh1>\n\u003Cp>這陣子在嘗試在開發流程中導入docker，有次寫Vue時就發生了標題所述的狀況。\u003Cbr \u002F>\n通常我們在啟動node之後，修改文檔不用重啟服務，就能直接在網頁看到調整的結果，在Vue中是透過Hot-Reloading這個機制，偵測文檔是否變動過而重新載入到服務中。\u003Cbr \u002F>\n但掛載到docker後我發現這個機制失效了，一開始以為是設定問題，找了幾種方式來重啟Hot-Reloading結果都失敗，最後發現是docker掛在volumes時似乎無法偵測到host端的檔案修改裝態。\u003C\u002Fp>\n\u003Ch2>解決方式\u003C\u002Fh2>\n\u003Cp>解決方式很簡單，設定CHOKIDAR_USEPOLLING這個環境參數即可。\u003Cbr \u002F>\n因為我是用docker-compose，所以設定方式如下：\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-yml\">version: &quot;3.3&quot;\n\nservices: \n  my_app:\n    ....\n    ports:\n      - &quot;3030:3000&quot;\n    volumes:\n      - \u002Fpath\u002Fon\u002Flaptop:\u002Ftarget\u002Fin\u002Fcontainer\n    stdin_open: true\n    tty: true\n    environment:\n      - ...\n      - CHOKIDAR_USEPOLLING=true #加上這個\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>如果用Dockerfile則也可以用指令方式呼CHOKIDAR_USEPOLLING\u003Cbr \u002F>\n這樣就能在容器中偵測volumes的檔案變化囉！\u003C\u002Fp>\n\u003Cp>可以參考以下資料\u003Cbr \u002F>\n\u003Ca href=\"https:\u002F\u002Fdaten-und-bass.io\u002Fblog\u002Fenabling-hot-reloading-with-vuejs-and-vue-cli-in-docker\u002F\">https:\u002F\u002Fdaten-und-bass.io\u002Fblog\u002Fenabling-hot-reloading-with-vuejs-and-vue-cli-in-docker\u002F\u003C\u002Fa>\u003C\u002Fp>\n\u003Cp>\u003Ca href=\"https:\u002F\u002Fgithub.com\u002Fpaulmillr\u002Fchokidar\">https:\u002F\u002Fgithub.com\u002Fpaulmillr\u002Fchokidar\u003C\u002Fa>\u003C\u002Fp>\n","2022-07-15T14:00:10",{"name":100,"link":101},[469,470],{"name":22,"slug":22},{"name":61,"slug":61},"\u002Fblog\u002Fvue在docker中hot-reloading失效的問題",{"id":473,"slug":474,"title":475,"description":476,"content":477,"date":478,"image":86,"imageAlt":475,"author":479,"categories":480,"path":482,"minRead":29},48,"html在form表單上取得複數相同name的資料","[HTML]在form表單上取得複數相同name的資料","在form表單上取得複數相同name的資料 說來真慚愧，寫了好幾年的網頁現在才遇到這問題。 在HTML中使用f […]","\u003Ch1>在form表單上取得複數相同name的資料\u003C\u002Fh1>\n\u003Cp>說來真慚愧，寫了好幾年的網頁現在才遇到這問題。\u003C\u002Fp>\n\u003Cp>在HTML中使用form來做資料傳遞是很基礎的一個做法，例如像這樣：\u003Cbr \u002F>\n將name以POST方式傳到example.com的api\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-html\">&lt;form action=&quot;http:\u002F\u002Fexample.com&quot; method=&quot;post&quot;&gt;\n    &lt;input name=&quot;name&quot; value=&quot;val&quot;&gt;\n    &lt;input type=&quot;submit&quot;&gt;\n&lt;\u002Fform&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>在api那邊就可能會取得長得像這樣的JSON\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-json\">{&quot;name&quot;:&quot;val&quot;}\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>基本上一個name對應一個val，遇到欄位有相同的name則會取最後一個值。\u003C\u002Fp>\n\u003Cp>最近專案中有個表單，裡面會有個動態產生資料的需求(如下)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-html\">&lt;form action=&quot;http:\u002F\u002Fexample.com&quot; method=&quot;post&quot;&gt;\n    &lt;!--名為name的input動態產生--&gt;\n    &lt;input name=&quot;name&quot; value=&quot;val_1&quot;&gt; \n    &lt;input name=&quot;name&quot; value=&quot;val_2&quot;&gt;\n    &lt;input name=&quot;name&quot; value=&quot;val_3&quot;&gt;\n    &lt;input type=&quot;submit&quot;&gt;\n&lt;\u002Fform&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>照基本做法只會得到最後一個&quot;val_3&quot;，那麼要怎麼做呢？\u003C\u002Fp>\n\u003Ch2>將name陣列化\u003C\u002Fh2>\n\u003Cpre>\u003Ccode class=\"language-html\">&lt;form action=&quot;http:\u002F\u002Fexample.com&quot; method=&quot;post&quot;&gt;\n    &lt;input name=&quot;name[]&quot; value=&quot;val_1&quot;&gt;\n    &lt;input name=&quot;name[]&quot; value=&quot;val_2&quot;&gt;\n    &lt;input name=&quot;name[]&quot; value=&quot;val_3&quot;&gt;\n    &lt;input type=&quot;submit&quot;&gt;\n&lt;\u002Fform&gt;\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>答案很簡單，只要將name命名為陣列就可以，就能取得包起來的陣列囉。\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-json\">{&quot;name&quot;:[&quot;val_1&quot;,&quot;val_2&quot;,&quot;val_3&quot;]}\u003C\u002Fcode>\u003C\u002Fpre>\n","2022-06-10T16:00:36",{"name":100,"link":101},[481],{"name":33,"slug":34},"\u002Fblog\u002Fhtml在form表單上取得複數相同name的資料"]