-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcontent.json
More file actions
1 lines (1 loc) · 377 KB
/
Copy pathcontent.json
File metadata and controls
1 lines (1 loc) · 377 KB
1
{"posts":[{"title":"【PHP】4種判斷變數或陣列是否存在的方法","text":"本篇重點 使用 empty()、is_null()、isset() 或是直接使用if ($variable) 判斷式來判斷變數或陣列是否存在 資料檢查甚麼時候會有錯誤訊息(php error) 先了解這些會幫助你更快理解唷😁 【PHP】解析邏輯運算子 and、or、&&、|| 之間的差異【PHP、JavaScript】三種實用的符號運算子 ?:、??、??= 在開發動態網頁時,後端程式扮演了一個重要的角色,可能會處理來自前端程式的資料,也有可能處理來自資料庫的資料,在這種情況下,我們必須嚴謹地檢查數據的完整性,以確保後續的程式能夠順暢運行。那麼有哪些函數能夠進行這樣的檢查? 首先先了解各種類型的判斷,判斷的類型有 無效值(null) 布林值(boolean) 字串(string) 數值(integer) 陣列(array) 判斷差異表比較不同的function在各種情況下的判斷 型態 if ($x) empty() isset() is_null() $x = null null false true false true $x is undefined null false true false true $x = true boolean true false true false $x = false boolean false true true false $x = ‘’ string false true true false $x = ‘1’ string true false true false $x = ‘0’ string false true true false $x = ‘-1’ string true false true false $x = ‘hello’ string true false true false $x = ‘true’ string true false true false $x = ‘false’ string true false true false $x = 1 integer true false true false $x = 0 integer false true true false $x = -1 integer true false true false $x = 666 integer true false true false $x = array() array false true true false $x = array(‘a’, ‘b’) array true false true false if ($x) 和 empty() 的判斷結果相反 isset() 和 is_null() 的判斷結果相反 螢光標記是我覺得特別要注意的判斷結果 範例 用戶填寫表格資料並送出資料,這些資料會從前端傳到後端程式,此時我們需要驗證是否有填入資料或是資料是否正確 php12345678<?php// 使用 isset() 搭配 intval() 確認是否有參數傳入且為大於 0 的數值$age = isset($_POST['age']) && intval($_POST['age']) > 0 ? intval($_POST['age']) : 0;// 初步整理 $age 值後,檢查是否需要返回前端重新填寫年齡if ($age == 0) { // 返回前端請用戶重新填寫年齡} 情境一,填寫年齡 30 傳入後端,isset() 為 true,且 intval() 後數值大於零,$age = 30 往下接續處理 情境二,填寫年齡 0 傳入後端,isset() 為 true,但 intval() 後數值等於零,$age = 0 返回前端處理 情境三,無填寫年齡傳入後端,isset() 為 false,$age = 0 返回前端處理 健忘筆記 ? : 是一種條件運算符,稱為三元運算符,可以用來做因果判斷,詳細可參考 【PHP、JavaScript】三種實用的符號運算子 ?:、??、??= 動手做做看 前端資料傳入後端後做資料檢查 情境一,填寫年齡 30 傳入後端,isset() 為 true,且 intval() 後數值大於零,$age = 30 往下接續處理情境二,填寫年齡 0 傳入後端,isset() 為 true,但 intval() 後數值等於零,$age = 0 返回前端處理情境三,無填寫年齡傳入後端,isset() 為 false,$age = 0 返回前端處理 動手做做看 從資料庫撈取資料,並檢測是否有資料,如果為無資料需要做處理 情境一,從資料庫撈取會員資訊,並取得完整資料 php12345678910111213141516171819<?php$user_data = array( 'name' => '小明', 'gender' => 'Male',);// 使用 if (!$user_data) 確認變數內是否有資料if (!$user_data) { // 無資料時的處理}// 結果:if (!$user_data) 判斷為 false,原本 if ($user_data) 為 true,但碰到驚嘆號反轉布林值結果,因此為 false 跳過判斷// 使用 !is_array($user_data) 搭配 empty($user_data) 確認變數內是否有資料if (!is_array($user_data) || empty($user_data)) { // 無資料時的處理}// 結果:!is_array($user_data) 為 false,empty($user_data) 也為 false,因此跳過判斷 動手做做看 情境一,從資料庫撈取會員資訊,並取得完整資料 資料檢查1:if (!$user_data) 判斷為 false,原本 if ($user_data) 為 true,但碰到驚嘆號反轉布林值結果,因此為 false 跳過判斷資料檢查2:!is_array($user_data) 為 false,empty($user_data) 也為 false,因此跳過判斷 動手做做看 情境二,從資料庫撈取會員資訊,但沒有撈到資料寫入空陣列 php12345678910111213141516<?php$user_data = array();// 使用 if (!$user_data) 確認變數內是否有資料if (!$user_data) { // 無資料時的處理}// 結果:if (!$user_data) 判斷為 true 進入無資料處理// 使用 !is_array($user_data) 搭配 empty($user_data) 確認變數內是否有資料if (!is_array($user_data) || empty($user_data)) { // 無資料時的處理}// 結果:!is_array($user_data)為 false,但 empty($user_data) 為 true,因此進入無資料處理 動手做做看 情境二,從資料庫撈取會員資訊,但沒有撈到資料寫入空陣列 資料檢查1:if (!$user_data) 判斷為 true 進入無資料處理資料檢查2:!is_array($user_data)為 false,但 empty($user_data) 為 true,因此進入無資料處理 動手做做看 情境三,從資料庫撈取會員資訊,但沒有撈到資料寫入字串 php1234567891011121314<?php$user_data = 'no_data';if (!$user_data) { // 無資料時的處理}// 結果:if (!$user_data) 判斷為 false 跳過判斷if (!is_array($user_data) || empty($user_data)) { // !is_array()為 true,因此進入無資料處理 // 無資料時的處理}// 結果:!is_array($user_data) 判斷為 true,因此進入無資料處理 健忘筆記 如果查找資料庫,沒有取得資料會寫入 array 以外的型態,那使用 !is_array($x) 搭配 empty() 來檢查資料會是更謹慎的做法 動手做做看 情境三,從資料庫撈取會員資訊,但沒有撈到資料寫入字串 資料檢查1:if (!$user_data) 判斷為 false 跳過判斷資料檢查2:!is_array($user_data) 判斷為 true,因此進入無資料處理 動手做做看 錯誤訊息在進行資料完整性檢查時,要確保資料的完整性,但又不希望出現錯誤訊息,那麼應該採用哪種函數? 這四種驗證方式除了兩兩相反外,還有一個很重要的差異 “錯誤訊息” 也就是PHP error。當變數 $x 沒有被賦值時,使用 if ($x) 和 is_null($x) 會引發錯誤訊息,但使用 isset($x) 和 empty($x) 不會,不過這些錯誤屬於較輕微的 E_NOTICE 級別,所以在沒有開啟PHP的所有錯誤提示的情況下是不會對網頁造成影響,但還是應該盡量處理這些錯誤。 範例 情境一,檢查陣列中不存在的索引 php12345<?php$a = [];var_dump($a[1]); // NULL,錯誤訊息 Notice: Undefined offset: 1var_dump(isset($a[1])); // bool(false),無錯誤訊息 $a 設定為一個空陣列,卻指定不存在的索引1,因此在 var_dump($a[1]) 的情況下會出現錯誤訊息 動手做做看 情境一,檢查陣列中不存在的索引 $a 設定為一個空陣列,卻指定不存在的索引1,因此在 var_dump($a[1]) 的情況下會出現錯誤訊息 動手做做看 情境二,判斷不存在的變數 php1234<?php// $b 沒有被賦值var_dump(is_null($b)); // bool(true),Notice: Undefined variable: b var_dump(empty($b)); // bool(true),無錯誤訊息 $b 沒有被賦值,因此在 is_null($b) 的情況下會出現錯誤訊息。 動手做做看 情境二,檢查不存在的變數 $b 沒有被賦值,因此在 is_null($b) 的情況下會出現錯誤訊息 動手做做看 情境三,有個陣列內的值需要做處理,但這個值不一定存在,如果不存在就不需要處理 php123456789101112131415161718192021222324252627282930<?php// 如果$user內沒有'age'$user = array( 'name' => '小明');if ($user['age']) { $user['adult'] = ($user['age'] >= 18) ? true : false;}// 結果:出現錯誤訊息 Notice: Undefined index: age 並跳出判斷,'adult'沒有寫入資料// 為了不跳出錯誤訊息且正常判斷,可以改成!empty($user['age'])if (!empty($user['age'])) { $user['adult'] = ($user['age'] >= 18) ? true : false;}// 結果:無錯誤訊息跳出判斷// 如果$user內有'age'$user = array( 'name' => '小明', 'age' => 10);if (!empty($user['age'])) { $user['adult'] = ($user['age'] >= 18) ? true : false;}// 結果:!empty($user['age'])判斷為true,$user['adult'] 寫入 false 判斷用戶是否成年,而 $user 內 ‘age’ 的值不一定存在,因此需要檢查 ‘age’ 是否存在再做處理 健忘筆記 在不確定資料內部狀況的情況下,可以使用 empty() 來讓錯誤訊息消失 動手做做看 情境三,有個陣列內的值需要做處理,但這個值不一定存在,如果不存在就不需要處理 資料檢查1:出現錯誤訊息 Notice: Undefined index: age 並跳出判斷,'adult'沒有寫入資料資料檢查2:無錯誤訊息跳出判斷資料檢查3:!empty($user['age'])判斷為true,$user['adult'] 寫入 false 動手做做看 結論依不同的需求,使用function 來判斷變數或陣列的完整性。 if ($x) 和 empty($x) 的判斷結果相反 isset($x) 和 is_null($x) 的判斷結果相反 無設定 $x 值的狀況下 if ($x) 和 is_null($x) 會跳錯誤訊息 isset($x) 和 empty($x) 則不會跳錯誤訊息 檢查完整性可以減少 bug 的發生,不用修 bug 就是開心😆~ 你平常會檢查變數或陣列的資料完整性嗎? 是用甚麼方法處理呢? 歡迎留言跟大家分享! 動手做做看 PHP 線上編輯器 參考資料 【官方文件】PHP類型比較表 延伸閱讀 【官方文件】資料類型的介紹 【官方文件】判斷式(if)介紹 【官方文件】布林值false的定義 【官方文件】PHP error 介紹","link":"/Backend/PHP/4-Ways-to-Check-If-a-Variable-or-Array-Exists/"},{"title":"【VSCode】在 VSCode terminal 使用 git bash","text":"本篇重點 為什麼要使用 git bash VSCode terminal 選項新增 git bash 將 git bash 設定為 VSCode 的預設終端機 我本身是使用 Windows 系統,為了熟悉 Linux 環境和 Git 的相關指令,嘗試了一些方法 虛擬環境 - VirtualBox、docker windows 子系統 - WSL git for windows - git bash 剛開始是使用 VirtualBox 創建 ubuntu 來熟悉 linux 的指令,後來覺得每次都要啟動虛擬機很麻煩,最後選擇 git bash 來在本機實做。 為什麼要使用 git bash在 windows 環境下: 可以使用大部分的 linux 系統指令,例如 ls、cp、mv、rm 等 可以使用 git 版本控制工具 無須安裝虛擬機或 Windows 子系統 在 VSCode terminal 新增 git bash 選項一直都是有需要使用 git 的時候才叫出 git bash 執行指令,但這樣其實有點麻煩,而且還需要不斷切換視窗。 隨著對 VSCode 的了解,才知道 VSCode 有內建 terminal,windows 預設的 terminal 是 cmd 或是 powersell。接下來會把 git bash 設定到 VSCode 的 terminal 選項,這樣就可以直接在 VSCode 中使用了! 1. 開啟 VSCode 設定檔 (JSON檔)方法一、使用快捷鍵開啟 使用按鍵 F1 或是 ctrl + shift + p 顯示所有命令 輸入 open settings 選取 Preferences: Open User Settings (Json) 3. 即可開啟設定檔 settings.json 方法二、從設定中開啟 點擊VSCode 左下角齒輪裡的設定 (快捷鍵 ctrl + ,) 2. 點擊右上角的 開啟設定(json) 3. 即可開啟設定檔 settings.json 2. 新增 git bash 到 terminal 選項 當前使用的 VSCode 版本為 1.84 將設定值寫入 settings.json settings.json12345"terminal.integrated.profiles.windows": { "Git Bash": { "path": "C:\\\\Program Files\\\\Git\\\\bin\\\\bash.exe", }} 健忘筆記 "path" 請設定自己 git bash 的路徑,以及這邊的檔案是 bash.exe 不是 git-bash.exe。 "terminal.integrated.profiles 的其他設定可以到官方文件查看。 設定後存檔,重新開啟 VSCode,使用按鍵 ctrl + ‵ ( ‵ 就是 ~ 鍵) 開啟終端機,就可以在選項中看到 git bash 了! 終端機名稱設定 "Git Bash" 如果顯示不行使用或是終端機選項中沒有出現,嘗試修改名稱,使用 "GitBash" 或 "Git_Bash" 等,我的兩台電腦一台可以使用 "Git Bash" 一台則不行,爬文的結果此問題應該是 VSCode 版本導致,建議更新版本後再設定。 將 git bash 設定為 VSCode 的預設終端機terminal 增加 git bash 選項後,將其設為預設終端機,之後每次開終端機就都是 git bash 了 將設定值寫入 settings.json settings.json1"terminal.integrated.defaultProfile.windows": "Git Bash" 設定後存檔重新開啟 VSCode,終端機就會自動選擇 git bash 開啟了! 健忘筆記 "terminal.integrated.profiles.windows" 和 "terminal.integrated.defaultProfile.windows" 是最新的設定參數,"terminal.integrated.shell.windows" 已是舊設定值無法使用。 快速切換 shell在終端機命令列打上 shell 名稱 bash / powershell / cmd 就可以快速切換。 切換 powershell 切換 cmd 切換 git bash 結論對我來說,能夠在同一個視窗內完成所有工作,直接在 VSCode 中查看程式並執行指令是最方便的方式。 這邊整理需要注意的地方: 查看 VSCode 版本,有些版本設定會有問題,當前使用的版本為 1.84 設定 git bash 的路徑 “path”,請填寫自己 bash.exe 的檔案位置 \"terminal.integrated.shell.windows\" 已棄用,請使用 \"terminal.integrated.profiles.windows\" 和 \"terminal.integrated.defaultProfile.windows\" 你們覺得這樣方便嗎? 歡迎分享你的看法~ 延伸閱讀 [官方文件] VSCode terminal 介紹 [官方文件] VSCode terminal 設定檔 [為你自己學Git] 終端機是甚麼? [為你自己學Git] 什麼是Git?為什麼要學習它?","link":"/Other/VSCode/Using-Git-Bash-in-VSCode/"},{"title":"【MySQL】解析資料庫語言類型 DDL、DML、DCL、DQL","text":"本篇重點 了解 DML、DDL、DCL 和 DQL 分別是資料庫裡的哪些功能 以下說明會以 MySQL 的指令做說明 在資料庫管理中,DML、DDL、DCL 和 DQL 是資料庫領域中的四種類型,它們各自負責著不同的任務。 舉例的中括號 [ ] 為替換字串 DDL (Data Definition Language) 數據定義語言DDL用於定義和管理資料庫結構。 包括創建、修改和刪除資料庫、資料表、索引等對象的操作。 常見指令 CREATE:建立資料庫的物件 (資料表、索引等) DESCRIBE:查詢資料表的結構資訊 ALTER:修改現有資料庫對象的結構 (新增欄位、修改資料型別等) TRUNCATE:刪除資料表中的所有資料,但保留資料表的結構 DROP:刪除資料庫的物件 (資料表、索引等) 實例搶先看 DDL 數據定義語言 CREATE:建立資料庫的物件 (資料表、索引等)DESCRIBE:查詢資料表的結構資訊ALTER:修改現有資料庫對象的結構 (新增欄位、修改資料型別等)DROP:刪除資料庫的物件 (資料表、索引等) 實例搶先看 舉例創建資料庫 mysql1234CREATE DATABASE [資料庫名稱];// 創建 "mydatabase" 資料庫CREATE DATABASE mydatabase; 創建資料表 mysql12345678CREATE TABLE [資料表名稱];// 創建 "customers" 資料表CREATE TABLE customers (id INT AUTO_INCREMENT PRIMARY KEY,name VARCHAR(50),email VARCHAR(100)); 查詢資料表的結構資訊 mysql1234DESCRIBE [資料表名稱];// 查看 "customers" 資料表結構資訊DESCRIBE customers; 修改資料表結構 mysql1234567ALTER TABLE [資料表名稱] [修改事項];// "customers" 資料表新增一個欄位 "phone"ALTER TABLE customers ADD phone VARCHAR(20);// 刪除 "customers" 資料表的 "phone" 欄位ALTER TABLE customers DROP COLUMN phone; 清空資料表所有資料 mysql1234TRUNCATE TABLE [資料表名稱];// 清空 "customers" 資料表內所有內容TRUNCATE TABLE customers; 刪除資料表 mysql1234DROP TABLE [資料表名稱];// 刪除 "customers" 資料表DROP TABLE customers; 健忘筆記 刪除資料功能 (TRUNCATE 指令或刪除資料表的欄位等) 無法回滾(Rollback),一旦執行就無法還原被刪除的資料。在使用刪除功能時,請再三確認要刪除的資料。 動手做做看 動手做做看 DML (Data Manipulation Language) 數據操作語言DML 是用於操作和處理資料。 包括對資料進行插入、更新和刪除等操作。 常見指令 INSERT INTO:新增資料到資料表中 UPDATE:更改資料表中的資料 DELETE:刪除資料表中的資料 健忘筆記 在一些資料庫系統中,SELECT 指令也可能被視為 DML 指令的一部分,這取決於特定資料庫的標準。但在 MySQL 中,通常將 SELECT 指令歸類為 DQL 指令。 實例搶先看 DML 數據操作語言 INSERT INTO:新增資料到資料表中SELECT:查詢資料庫中的資料UPDATE:更改資料表中的資料DELETE:刪除資料表中的資料 實例搶先看 舉例新增資料 mysql1234INSERT INTO [資料庫名稱] ([欄位]) VALUES ([值]);// 在 "customers" 資料表新增一筆客戶資料INSERT INTO customers (name, email) VALUES ('John', 'john@example.com'); 更新資料 mysql1234UPDATE [資料庫名稱] SET [修改值] WHERE [條件];// 在 "customers" 資料表更新一筆客戶資料UPDATE customers SET email = 'newJohn@example.com' WHERE name = 'John'; 刪除資料 mysql1234DELETE FROM [資料庫名稱] WHERE [條件];// 在 "customers" 資料表刪除一筆客戶資料DELETE FROM customers WHERE name = 'John'; 動手做做看 動手做做看 DQL (Data Query Language) 數據查詢語言DQL 用於查詢資料庫的資料。 主要目的是檢索所需的數據,而不涉及對數據進行修改。 常見指令 SELECT:查詢資料庫中的資料 WHERE:條件篩選資料 DISTINCT:檢索唯一的資料值 ORDER BY:依指定的欄位排序資料 GROUP BY:依指定的欄位分組 想了解指令的執行順序,可以參考 【MySQL】DQL 指令的執行順序與效能優化 實例搶先看 DQL 數據查詢語言 SELECT:查詢資料庫中的資料WHERE:條件篩選資料DISTINCT:檢索唯一的資料值ORDER BY:按照指定的欄位排序資料GROUP BY:依指定的欄位分組 實例搶先看 舉例查詢資料 mysql1234SELECT [欄位名稱] FROM [資料表名稱];// 搜索 table_name 資料表內所有欄位的資料SELECT * FROM table_name; 篩選條件 mysql1234SELECT [欄位名稱] FROM [資料表名稱] WHERE [篩選條件];// 搜索 table_name 資料表內 uid = 123 的資料SELECT * FROM table_name WHERE uid = 123; 搜索唯一值 mysql1234SELECT DISTINCT [欄位名稱] FROM [資料表名稱];// 搜索 order_list 資料表內 uid 唯一值的資料SELECT DISTINCT uid FROM order_list; 依指定欄位排序資料 mysql1234567SELECT [欄位名稱] FROM [資料表名稱] ORDER BY [欄位名稱] (ASC/DESC);// 搜索 order_list 資料表內所有資料並以uid做排序SELECT * FROM order_list ORDER BY uid;// 設定順序由大排到小SELECT * FROM order_list ORDER BY uid DESC; 健忘筆記 ORDER BY 未指定排序,預設使用 ASC (升冪),順序由小排到大,如果想要由大排到小,可以設定 ORDER BY [欄位名稱] DESC; 依指定的欄位分組 mysql1234SELECT [欄位名稱] FROM [資料表名稱] GROUP BY [欄位名稱];// 依 team 去分組,計算每組的人數SELECT team, COUNT(*) FROM table_name GROUP BY team; 動手做做看 動手做做看 DCL (Data Control Language) 數據控制語言DCL 用於控制資料庫的使用權限和訪問權限。 包括授予、撤銷用戶的權限,以及確保資料庫的安全性和完整性。 常見指令 CREATE USER:創建新的使用者 DROP USER:刪除現有的使用者 GRANT:授予使用者特定的權限和許可權 REVOKE:撤銷使用者的特定權限和許可權 舉例創建新的使用者 mysql1234CREATE USER [使用者名稱]@[IP位置或主機名] IDENTIFIED BY 'password';// 創建新使用者 "mary",並設定密碼CREATE USER 'mary'@'localhost' IDENTIFIED BY 'password'; 健忘筆記 IDENTIFIED BY 是用於設定使用者密碼的指令,可以省略這個指令讓使用者直接進入資料庫,但這樣就有安全性的問題 刪除現有的使用者 mysql1234DROP USER [使用者名稱]@[IP位置或主機名];// 刪除資料庫使用者 "mary"DROP USER 'mary'@'localhost'; 授予權限 mysql1234GRANT [權限] ON [資料庫名稱].* TO [使用者名稱]@[IP位置或主機名];// 授予 "john" 在 my_database 搜尋和寫入和刪除的權限GRANT SELECT, INSERT, DELETE ON my_database.* TO 'john'@'localhost'; 健忘筆記 ‘john‘@’localhost’ 是用來指定使用者帳號和使用者連接的 IP,指定連接 IP 確保資料庫的安全性my_database.* 是指定 my_database 資料庫的所有資料表,也可以指定特定資料表的權限,例如 my_database.my_table 收回權限 mysql1234REVOKE [權限] ON [資料庫名稱].* FROM [使用者名稱]@[IP位置或主機名];// 收回 "john" 在 my_database 刪除的權限REVOKE DELETE ON my_database.* FROM 'john'@'localhost'; 結論有一次在看 MySQL 文件的時候裡面提到 DQL,但也沒有特別解釋什麼是 DQL,後來去查才知道這是資料庫語言類型,感覺很容易忘記所以有了這篇😆 DDL:定義和管理資料庫結構 DML:操作和處理資料 DQL:查詢資料庫的資料 DCL:控制資料庫的使用權限和訪問權限。 動手做做看 mysql 線上編輯器","link":"/Backend/MySQL/Analyzing-4-Types-of-Database-Language/"},{"title":"【PHP】解析邏輯運算子 and、or、&&、|| 之間的差異","text":"本篇重點 and 和 &&之間有什麼差別,它們分別代表什麼意思? or 和 || 之間有什麼差別,它們分別代表什麼意思? 邏輯運算子的執行順序 邏輯運算子使用建議 先了解這些會幫助你更快理解唷😁 【PHP】4種判斷變數或陣列是否存在的方法【PHP、JavaScript】三種實用的符號運算子 ?:、??、??= 「分數有及格嗎?」、「考卷的姓名和學號都是正確的嗎?」、「這張考卷是 john 還是tom 的?」,在日常生活中很常用到的條件判斷,在程式中也很常出現,PHP 提供了很多符號來輔助我們,使用邏輯運算子來達到目的。 &&、AND vs || 、OR &&、AND 是「且、和」的意思,符號兩邊的條件都達成,結果為 true,反之為 false ||、OR 是「或」的意思,符號兩邊依左至右判斷,如果有條件達成,結果為 true,反之為 false 範例依學生的資訊做條件處理 實例搶先看 &&、AND vs || 、OR &&、AND:「且、和」的意思,符號兩邊的條件都達成,結果為 true,反之為 false||、OR:「或」的意思,符號兩邊依左至右判斷,如果有條件達成,結果為 true,反之為 false 實例搶先看 php12345678910111213141516171819202122232425262728293031323334353637383940414243<?php// 設定學生 'john' 的資訊$name = 'john';$score = 60;$gender = 'male';// &&、AND 範例// 判斷學生是 john,且分數是及格if ($name == 'john' && $score >= 60) { echo '判斷為true,學生是 john,且分數是及格';} else { echo '判斷為false';}// 結果:if ($name == 'john' && $score >= 60) 判斷為true,「&&」兩邊的條件皆正確// 判斷學生不是john,且性別是男生if ($name != 'john' AND $gender == 'male') { echo '判斷為true,學生不是john,且性別是男生';} else { echo '判斷為false';}// 結果:if ($name != 'john' AND $gender == 'male') 判斷為 false,第一個條件學生不是 john 是錯誤的,因此也不用看第二個條件了,整個判斷式直接錯誤// ||、OR 範例// 判斷學生是john或是學生是女生if ($name == 'john' || $gender == 'female') { echo '判斷為true,學生是john,或是學生是女生';} else { echo '判斷為false';}// 結果:if ($name == 'john' || $gender == 'female') 判斷為 true,第一個條件學生是 john 是正確的,因此也不用看第二個條件了,整個判斷式直接正確// 判斷學生分數不及格或是這位學生不是男生if ($score < 60 OR $gender != 'male') { echo '判斷為true,學生分數不及格或是這位學生不是男生';} else { echo '判斷為false';}// 結果:if ($name == 'john' || $gender == 'female') 判斷為 false,第一個條件不正確,接續判斷第二個條件,第二個條件也不正確 動手做做看 動手做做看 && vs AND 以及 || vs OR接著來比較 and 和 && 之間以及 or 和 || 之間的差異 簡單來說它們的差異就是執行的優先順序(Operator Precedence)不同 「&&」的執行順序比「=」高,而「=」的執行順序比「AND」高 「||」的執行順序比「=」高,而「=」的執行順序比「OR」高 範例依學生的資訊做條件處理 實例搶先看 && vs AND 以及 || vs OR 「&&」的執行順序比「=」高,而「=」的執行順序比「AND」高「||」的執行順序比「=」高,而「=」的執行順序比「OR」高 實例搶先看 php123456789101112131415161718192021222324252627282930313233<?php// 設定學生 'Tom' 的資訊$student_id = 123;$name = 'tom';$gender = 'male';$score = 49;// &&、AND 範例// 判斷學生學號是123,且分數是及格$answer = $student_id == 123 && $score >= 60;// 結果:判斷結果為 fasle,第二個條件錯誤$answer = $student_id == 123 AND $score >= 60;// 結果:判斷結果為 true,因為「=」的執行順序比「AND」高,因此變成先執行「$answer = $student_id== 123」,導致 $answer 先賦值$answer = ($student_id == 123 AND $score >= 60);// 結果:判斷結果為 false,因為加了括號改變執行順序,先執行「$student_id == 123 AND $score >= 60」// 判斷這位學生是 john 還是 tom$answer = $name == 'john' || $name == 'tom';// 結果:判斷結果為 true,第一個條件正確$answer = $name == 'john' OR $name == 'tom';// 結果:判斷結果為 false,因為「=」的執行順序比「OR」高,因此變成先執行「$answer = $name == 'john'」,導致 $answer 先賦值$answer = ($name == 'john' OR $name == 'tom');// 結果:判斷結果為 true,因為加了括號改變執行順序,先執行「$name == 'john' OR $name == 'tom'」 健忘筆記 判斷式建議使用 && 和 || ,如果要使用 AND 和 OR 記得加上括號減少錯誤發生~詳細的執行順序看這裡! 動手做做看 動手做做看 結論條件判斷時,依需求使用不同的邏輯運算子幫助我們達到目的。 「&&」、「AND」 是「且」的意思,符號兩邊的條件皆須達成時使用 「||」、「OR」 是「或」的意思,符號兩邊的條件只需要有一個條件達成時使用 「&&」的執行順序比「=」高,而「=」的執行順序比「AND」高 「||」的執行順序比「=」高,而「=」的執行順序比「OR」高 運算子的執行優先順序大大的影響判斷的結果,所以在使用時要多加注意,不然會得到意想不到的結果😆,建議使用「&&」和「||」可以減少錯誤的發生~ 動手做做看 PHP 線上編輯器 延伸閱讀 【官方文件】邏輯運算子介紹 【官方文件】運算子優先級","link":"/Backend/PHP/Comparing-the-Differences-Between-Logical-Operators/"},{"title":"【jQuery、css】解析六種組合選擇器:相連、+、~、>、,、空格","text":"本篇重點 組合選擇器是甚麼 選擇器內的符號( +、~、> 、, 等)有甚麼含意 jQuery 要使用組合選擇器還是原生的函式 設定 CSS 樣式或是 jQuery 要操作 DOM 元素時,會用到選擇器選取元素,兩者的選擇器類似,你可能看過選擇器中有一些符號,例如 +、~、> 或是空格,這些將兩個元素組合起來的選擇器就稱為「組合選擇器 Combinator」,熟悉符號的意思可以加速抓到想要的元素,甚至提升整體效能,那來了解這些符號分別代表甚麼意思吧! 標籤之間的關係先了解 HTML 階層間的關係,可以用家族的關係來了解每個階層。 舉例來說,以 <div id="john"> 角度出發,看看下面標籤之間的關係 html12345678<div>john的父親(父層) <p>john的哥哥或姊姊</p> <div id="john"> <p>john的孩子(子層)</p> </div> <p>john的弟弟或妹妹</p></div><div>john父親的弟弟或妹妹</div> 接下來會使用家族關係來解釋各個組合選擇器的使用方法。 健忘筆記 可以使用 tag、id、class 等選擇器做組合選擇器 相連的選擇器選擇器相連且中間沒有空格或是符號,表示同一個 tag 要同時符合所有條件 範例 實例搶先看 相連的選擇器 選擇器相連且中間沒有空格或是符號,表示同一個 tag 要同時符合所有條件 html123456<div id="father"> john的父親(父層) <div class="select" id="tom">tom (john的哥哥)</div> <p class="select" id="john">john</p> <p>john的弟弟或妹妹</p></div> css1234567#john.select { background-color: orange;}div.select#tom { background-color: gray;} jQuery1234567$("#john.select").click(function () { alert("john");});$("div#tom.select").click(function () { alert("tom");}); 空格相連的選擇器指定元素層內的所有指定元素,表示同一家族內的指定元素都會選擇 範例 實例搶先看 空格相連的選擇器 指定元素層內的所有指定元素,表示同一家族內的指定元素都會選擇 html12345678910<p class="select">class="select"</p><div id="father"> john的父親(父層) <p class="find">class="find"</p> <div class="select" id="tom">tom (john的哥哥)</div> <p class="select" id="john">john</p> <p id="joy">joy (john的<b class="select">弟弟或妹妹</b>)</p> <p class="find">class="find"</p></div><p class="select">class="select"</p> css1234567#father .select { background-color: orange;}div #joy { background-color: gray;} 健忘筆記 使用空格只會選取 <div id="father"> 內層的元素,因此在 <div id="father"> 外層上下的 class="select" 元素不會有反應 jQuery1234567$("#father .select").click(function () { alert("orange");});$("div #joy").click(function () { alert("gray");}); 實作這個例子時你會發現,點擊文字”弟弟或妹妹” 會發現先跳出 orange 再跳出 gray,這是JS的事件冒泡,如果要解決這個問題可以使用 、event.stopPropagation() 來處理。 jQuery1234$("#father .select").click(function() { alert("orange"); event.stopPropagation()}); 健忘筆記 事件冒泡:指內層元素觸發事件,外層元素也跟著觸發 選取同一家族內的指定元素,也可以使用 jquery 函數的 .find() 來選取 jQuery123$("#father").find('.find').click(function () { alert("find");}); 「+」相連的選擇器同一層後的第一個元素,表示會選取大弟或大妹 範例 實例搶先看 「+」相連的選擇器 同一層後的第一個元素,表示會選取大弟或大妹 html1234567<p class="select">father的大哥或大姐</p><div id="father">father <p id="john">john</p> <p id="joy">joy (john的弟弟或妹妹)</p></div><p class="select" id="first">father的大弟或大妹</p><p class="select">father的二弟或二妹</p> css123#father + .select { background-color: orange;} 健忘筆記 使用「+」只會選取 <div id="father"> 同層後的第一個元素,所以只有 <p class="select" id="first"> 會被選取 jQuery123$("#father + .select").click(function() { alert("click");}); 同層後的第一個元素,也可以使用 jquery 函數的 .next() 來選取 jQuery12345678$("#john").next().click(function () { alert(".next()");});// 指定同層後的第一個元素$("#first").next(".select").click(function () { alert(".next('.select')");}); 健忘筆記 如果 .next() 有指定元素例如 .next(".select"),表示會選取同一層後第一個是 class="select" 的元素,如果第一個元素不是則不會選取 「~」相連的選擇器同一層後的所有指定元素,表示會選取所有指定弟弟或妹妹 範例 實例搶先看 「~」相連的選擇器 同一層後的所有指定元素,表示會選取所有指定弟弟或妹妹 html12345678910<p class="select">father的大哥或大姐</p><div id="father">father <p id="john">john</p> <p id="joy" class="nextAll">joy (john的弟弟或妹妹)</p> <p id="betty">betty (joy的弟弟或妹妹)</p> <p id="tom" class="nextAll">tom (joy的弟弟或妹妹)</p></div><p class="select" id="first">father的大弟或大妹</p><p id="second">father的二弟或二妹</p><p class="select" id="second">father的三弟或三妹</p> css123#father ~ .select { background-color: orange;} 健忘筆記 使用「~」只會選取 <div id="father"> 同層後的所有指定元素,所以 <p class="select">father的大哥或大姐</p> 不會反應 jQuery123$("#father ~ .select").click(function () { alert("click");}); 同層後的所有元素,也可以使用 jquery 函數的 .nextAll() 來選取 jQuery12345678$("#john").nextAll().click(function(){ alert('nextAll');})// 指定同層後的所有指定元素$("#john").nextAll(".nextAll").click(function(){ alert('nextAll(".nextAll")');}) 健忘筆記 如果 .nextAll() 有指定元素例如 .nextAll(".nextAll"),表示會選取同一層後所有有 class="nextAll" 的元素,如果沒有則不會選取 「>」相連的選擇器剛好在下一層的所有指定元素,表示會選取親生的指定小孩 範例 實例搶先看 「>」相連的選擇器 剛好在下一層的所有指定元素,表示會選取親生的指定小孩 html1234567891011121314<p class="select">father的哥哥或姐姐</p><div id="father">father <p id="john" class="select">john</p> <div id="joy">joy <p id="tim" class="select">tim (joy的小孩)</p> </div> <p id="tom" class="select">father的子層</p></div><p class="select">father的大弟或大妹</p><div id="mother">mother <p id="ken" class="select">ken</p> <p id="eason">eason</p> <p id="kevin" class="select">kevin</p></div> 這邊比較以空格和以「>」相連的差別 css1234567#father .select { color: green;}#father > .select { background-color: orange;} jQuery123$("#father > .select").click(function () { alert("click");}); 健忘筆記 使用「>」只會選取 <div id="father"> 下一層的所有指定元素,因此不會選取到 <p id="tim" class="select">tim (joy的小孩)</p><div> 下一層的所有元素,也可以使用 jquery 函數的 .children() 來選取 jQuery12345678$("#mother").children().click(function(){ alert('children()');})// 指定元素下一層的所有指定元素$("#mother").children(".select").click(function(){ alert('children(".select")');}) 健忘筆記 如果 .children() 有指定元素例如 .children(".select"),表示會選取下一層中所有有 class="select" 的元素,如果沒有則不會選取 「,」相連的選擇器需要相同樣式或是相同行為的元素綁在一起 範例 實例搶先看 「,」相連的選擇器 需要相同樣式或是相同行為的元素綁在一起 html1234567<p class="example1">example1</p><p class="example2">example2</p><p class="example3">example3</p><div>div <p class="example4">example4</p> <span class="example5">example5</span></div> css12345.example1,.example3,div > span { background-color: orange;} jQuery123$(".example1, .example3, div > span").click(function () { alert("click");}); 健忘筆記 可以使用各種選擇器選取元素,再用「,」綁在一起 jQuery 要使用組合選擇器還是原生的函式jQuery 選取元素有很多方法,這邊比較組合選擇器和使用 jQuery 的函數的差異 例如:選取 id=”father” 層底下的所有 class=”select” 元素 jQuery12345/* 組合選擇器 */$("#father .select")/* jQuery 的函數 */$("#father").find(".select") 兩種方式都可以達到同樣的效果。在選取元素數量較少的情況下,兩者的差異非常微小,因為目前的瀏覽器及 jQuery 有對此進行優化。但如果選取的元素數量龐大,使用組合選擇器是較有效率的。 總結來說,如果不用考慮效能,則根據個人喜好和代碼的可讀性選擇一種方法來撰寫。覺得使用原生函式較容易閱讀及維護,那就直接使用原生函式吧! 結論 相連的選擇器:選擇器中間沒有空格或是符號,表示同一個 tag 要同時符合所有條件 空格相連的選擇器:指定元素層內的所有指定元素,表示同一家族內的指定元素都會選擇 「+」相連的選擇器:同一層後的第一個元素,表示會選取大弟或大妹 「~」相連的選擇器:同一層後的所有指定元素,表示會選取所有指定弟弟或妹妹 「>」相連的選擇器:剛好在下一層的所有指定元素,表示會選取親生的指定小孩 「,」相連的選擇器:需要相同樣式或是相同行為的元素綁在一起 選擇器用的好,書寫方面會更方便、選取某個元素時會更快速,也可以降低程式的複雜度,閱讀起來更舒服! 你有什麼常用的選擇器嗎?歡迎跟大家分享~ 動手做做看 codepen 線上編輯器 延伸閱讀 [官方文件] MDN CSS selectors [iT邦幫忙] JQuery 選擇器的補充 [iT邦幫忙] 事件冒泡","link":"/Frontend/JQuery/Analyzing-Six-Types-of-Combinator/"},{"title":"【Git】了解 git config 設定","text":"本篇重點 了解設定區域:--system, --global, --local git config 常見設定 查詢 git config 設定 刪除 git config 設定 設定的區域依需求做不同範圍的設定,在不同區域的設定影響 git 操作的範圍 當前儲存庫設定 --local在資料夾建立 git 環境後,這個資料夾就是當前儲存庫的設定區域 設定檔文件通常位於當前資料夾的 .git/config 用戶級別設定 --global系統用戶的設定區域,會影響當前用戶所建立的所有儲存庫區域。 設定檔文件通常位於用戶家目錄下的 .gitconfig 或 .config/git/config 系統級別設定 --system系統管理員的設定區域,會影響所有用戶所建立的所有儲存庫區域。 設定檔文件通常位於 /etc/gitconfig 健忘筆記 如果想查詢各個區域的設定檔,可以使用 git config --list --show-origin [區域],例如 git config --list --show-origin --global 設定區域優先級git config 會依照固定的優先順序進行覆蓋。優先順序如下: --local:對當前資料夾的 Git 儲存庫有效,優先級最高,會覆蓋 --global 和 --system 的相同設定。 --global:對當前用戶的所有 Git 儲存庫有效,優先級次於 --local ,會覆蓋 --system 的相同設定,但不會覆蓋 --local 的。 --system:對系統上所有 Git 的儲存庫有效,優先級最低,會被 --global 和 --local 覆蓋相同的設定。 健忘筆記 優先級順序: --local > --global > --system git config 常見設定各個設定都可以依據需求來做不同區域的設定 舉例的中括號 [ ] 為替換字串 git1234567git config [要設定的區域] [甚麼設定] [設定值]// 設定用戶級別的區域git config --global user.name "forgetfulengineer"// 設定當前儲存庫的區域git config --local user.email "thatforgetfulengineer@gmail.com" 健忘筆記 如果沒有指定設定位置,例如:git config user.name "forgetfulengineer"git config 的預設位置為 -- local 設定使用者名稱和郵件地址git123456789// 設定使用者的名稱git config --global user.name "[名字]"git config --global user.name "forgetfulengineer"// 設定使用者信箱git config --local user.email "[信箱]"git config --local user.email "thatforgetfulengineer@gmail.com" 設定 Git 指令縮寫git1234567891011git config --global alias.[縮寫] [指令]// 設定 st 等於 statusgit config --global alias.st status// 設定完後使用 git st 等於 git status 的效果// 設定 br 等於 branchgit config --global alias.br branch// 設定完後使用 git br 等於 git branch 的效果 設定 Git 使用的編輯器git config –global core.editor “vim” git1234git config --global core.editor "[編輯器]"// 設定 vim 為 git 的編輯器git config --global core.editor "vim" 設定 git log 顯示各分支進度git12345// 開啟顯示各分支進度git config --global log.decorate auto// 關閉顯示各分支進度git config --global log.decorate no log.decorate 設置有幾個選項: auto:根據終端檢測決定是否顯示各分支進度。 short:顯示簡短的各分支進度。 full:顯示完整的各分支進度。 no:不顯示各分支進度。 健忘筆記 HEAD -> dev 表示當前 branch 的進度,當前在 dev 這個 branch 上origin/dev 表示遠端 branch 的進度,當前 github 上的 dev branch 在 “無障礙功能優化” 這個 commit 上 當前本地 branch 還沒跟遠端 branch 的進度同步 設定 git 追蹤檔案權限預設情況下修改檔案的權限(chmod) 會影響 git 的紀錄 git12345// 開啟追蹤檔案權限git config --global core.filemode true// 關閉追蹤檔案權限git config --global core.filemode false 範例 command12345678910111213141516// chmod_test.txt 檔案權限是 644touch chmod_test.txt// 用 chmod 修改檔案權限chmod 755 chmod_test.txt// git diff 查看檔案更動紀錄git diff chmod_test.txt// git diff 結果diff --git a/chmod_test.txt b/chmod_test.txtold mode 100644new mode 100755// 不想記錄檔權限,關閉追蹤檔案權限git config --global core.filemode false 健忘筆記 三位數的檔案權限數字用來說明三種身分user(自己)、group(組別)、other(其它人)的權限,每一位數是三種權限read(4)、write(2)、execute(1)的加總。詳細說明看這裡! 設定 git 是否忽略檔名大小寫預設情況下 git 是忽略檔名大小寫的,所以修改檔名大小寫 git 不會紀錄 git12345// 設定忽略檔名大小寫 (預設)git config core.ignorecase true// 設定追蹤檔名大小寫git config core.ignorecase false git 忽略檔名大小寫引發 github page 404 錯誤 查詢 git config 設定值及刪除設定查詢單一設定git12345// 查詢 --global 的使用者名稱git config --get --global user.name// 查詢 --local 的 log 設定git config --get --local log.decorate 查詢所有設定git12345// 查詢 --global 的所有設定git config --global --list// 也可以用縮寫 -lgit config --global -l 健忘筆記 如果沒有指定設定位置,例如:git config --list,會列出當前存儲庫的 Git 配置 --local 和當前用戶級別的 Git 配置 --global,但不包括系統級別的 Git 配置 --system 刪除設定使用以下命令來刪除 Git 的現有設定: git12345// 刪除 --global 的使用者名稱git config --unset --global user.name// 刪除 --local 的 log 設定git config --unset --local log.decorate 結論我認為 git config 中最基本也最重要的兩種設定是 user.name 和 user.email,因為使用 git commit 時會記錄名字和信箱,如果沒有設定且嘗試使用 git commit,Git 通常會給出錯誤提示。如果仍然嘗試進行提交,Git 可能會使用空值或電腦的使用者名稱作為提交者資訊,獨自開發的情況下影響不大,但多人開發專案時提交資訊就非常重要了。(才有辦法追朔誰開發出 bug 🤣) 增加一些設定也可以讓我們更加便利,像是設定快捷鍵簡短指令長度,加快打指令的速度,或是讓 git log 顯示分支進度,讓資訊更一目了然。 可能還有很多方便的設定,如果你知道就分享一下吧~ 延伸閱讀 【官方文件】git init 創建 git 倉儲環境 【官方文件】git config 介紹 【官方文件】git commit 介紹","link":"/Other/Git/Understanding-Git-Config-Settings/"},{"title":"【Linux】解析資料重定向","text":"本篇重點 基本重定向用法:<、<<、>、>>、 2>、2>> 重定向的判讀方式 重定向符號的前後順序不影響命令的結果 特殊重定向用法:/dev/null、&>、 2>&1 、 1>&2 在 Linux 中,資料重定向是一個非常重要且強大的功能。它可以將命令的輸入或輸出重定向到不同的文件或設備,讓我們能更靈活地處理資料。 基本重定向用法標準輸入 stdin,代號:0 < 輸入重定向將文件內容作為命令輸入 範例 number.txt1234564638511449 command12345678910# 將 number.txt 文件的內容作為 sort 命令的輸入,並對內容進行排序sort -n < number.txt# 終端顯示4911446385 健忘筆記 sort -n < number.txt ,-n 選項是指定對數字進行排序 << 多行輸入重定向將多行文字作為命令的輸入,直到遇到指定的結束標記 範例 command123456789# 將多行文字作為 cat 命令的輸入,並將這些文字輸出到終端cat << EOFHelloWorldEOF# 終端顯示HelloWorld 健忘筆記 EOF 是一個告訴 linux 開始與結束的標記,用於長文本段落或多行命令的輸入。 EOF 本身並沒有特別的含義,可以用其他任意的字串來代替,只要確保開始標記和結束標記一致即可 動手做做看 標準輸出 stdout,代號:1 實例搶先看 標準輸出 (stdout,代號:1) > 輸出重定向:將命令的標準輸出重定向到文件,會覆蓋文件的原有內容 >> 附加重定向:將命令的標準輸出附加到文件末尾 實例搶先看 > 輸出重定向將命令的標準輸出重定向到文件,會覆蓋文件的原有內容 範例 command12345678# echo 命令的輸出寫入到 output.txt 文件中,如果文件已經存在,會覆蓋原有內容。echo hello world > output.txt# 查看 output.txt 的內容cat output.txt# 終端顯示hello world >> 附加重定向將命令的標準輸出附加到文件末尾,不會覆蓋文件的原有內容 範例 command123456789# 將 echo 命令的輸出附加到 output.txt 文件的末尾 (延續上一個範例)echo hello world >> output.txt# 查看 output.txt 的內容cat output.txt# 終端顯示hello worldhello world 動手做做看 錯誤輸出 stderr,代號:2 實例搶先看 錯誤輸出 (stderr,代號:2) 2> 錯誤輸出重定向:將命令的錯誤輸出重定向到文件,會覆蓋文件的原有內容 2>> 錯誤輸出附加重定向:將命令的錯誤輸出附加到文件末尾,不會覆蓋文件的原有內容 實例搶先看 2> 錯誤輸出重定向將命令的錯誤輸出重定向到文件,會覆蓋文件的原有內容 範例 command12345678910# non_existing_file 是一個不存在的檔案# 嘗試列出檔案資訊,並將錯誤訊息寫入到 error.logls non_existing_file 2> error.log# 查看 error.log 的內容cat error.log# 因為 non_existing_file 檔案不存在,系統給予錯誤訊息,並錯誤輸出重定向到 error.log# 終端顯示ls: cannot access 'non_existing_file': No such file or directory 2>> 錯誤輸出附加重定向將命令的錯誤輸出附加到文件末尾,不會覆蓋文件的原有內容 範例 command1234567891011# non_existing_file 是一個不存在的檔案# 嘗試列出檔案資訊,並將錯誤訊息附加到 error.log 的末尾(延續上一個範例)ls non_existing_file 2>> error.log# 查看 error.log 的內容cat error.log# 因為 non_existing_file 檔案不存在,系統給予錯誤訊息,並錯誤輸出重定向到 error.log 的末尾# 終端顯示ls: cannot access 'non_existing_file': No such file or directoryls: cannot access 'non_existing_file': No such file or directory 動手做做看 綜合應用標準輸入結合標準輸出command1234567891011121314151617181920212223# 使用多行輸入重定向搭配輸出重定向# 製作 input.txtcat << EOF > input.txthellotest 123EOFcat input.txt# 終端顯示hellotest 123# 將 input.txt 的內容作為 cat 命令的輸入,在輸出到 output.txtcat < input.txt > output.txt# 查看 output.txt 的內容cat output.txt# 得到跟 input.txt 一模一樣的 output.txt# 終端顯示hellotest 123 標準輸出結合錯誤輸出command1234567891011121314# non_existing_file 是一個不存在的檔案# 嘗試列出檔案資訊,並標準輸出到 output.log,錯誤輸出到 error.logcat non_existing_file > output.log 2> error.log# 查看 output.log 的內容cat output.log# 終端無顯示任何東西,output.log 是空的# 查看 error.log 的內容cat error.log# 終端顯示cat: non_existing_file: No such file or directory 動手做做看 重定向的判讀方式[重定向符號] + [文件或字串] 根據重定向符號和文件或字串來判斷,因此符號的前後順序不影響命令的結果 健忘筆記 Shell 在執行命令之前會先解析所有的重定向符號,再執行命令。 Shell 是用來和操作系統進行溝通的命令行界面(CLI),允許用戶通過輸入命令來控制系統執行程序、管理文件系統等 cat < input.txt > output.txt 可以分解為: < input.txt:這部分將 input.txt 的內容重定向到 cat 命令的標準輸入 > output.txt:這部分將 cat 命令的標準輸出重定向到 output.txt Shell 會先解析重定向符號,再執行 cat 命令,從 input.txt 讀取內容並將其寫入 output.txt 因此不管是 cat < input.txt > output.txt 或是 cat > output.txt < input.txt 結果都是一樣的!符號的前後順序不影響命令的結果。 特殊重定向用法/dev/null可以視為垃圾桶,任何資料重定向給它就會丟棄 範例 command12345# non_existing_file 是一個不存在的檔案# 嘗試列出檔案資訊,並將錯誤訊息重定向到 /dev/nullls non_existing_file 2> /dev/null# 終端不顯示訊息,錯誤訊息直接丟棄 健忘筆記 重定向到 /dev/null 表示丟棄所有寫入它的數據,因此無法復原要謹慎使用! 動手做做看 &>將命令的標準輸出和錯誤輸出都重定向到同一個文件 範例 command12345678910111213141516171819202122# non_existing_file 是不存在的檔案# 嘗試列出檔案資訊,並將標準輸出和錯誤輸出都重定向到 output.txtcat non_existing_file &> output.txt# 查看 output.txt 的內容cat output.txt# 終端顯示cat: cannot access 'non_existing_file': No such file or directory# 建立 test.txtecho test > test.txt# test.txt 是已存在的檔案# 嘗試列出檔案資訊,並將標準輸出和錯誤輸出都重定向到 output.txtcat test.txt &> output.txt# 查看 output.txt 的內容cat output.txt# 終端顯示test 健忘筆記 需要重複輸出到同一個檔案時,可以使用 &>>,將輸出附加到檔案末尾 動手做做看 2>&1、1>&2 2>&1 表示將標準錯誤輸出重定向到標準輸出 1>&2 表示將標準輸出輸出重定向到錯誤輸出 範例 test.txt12hellotest 123 2>&1 command123456789101112131415161718# non_existing_file 是不存在的檔案# 列出 test.txt 和 non_existing_file 檔案資訊,並標準輸出重定向到 output.txt,最後要求標準錯誤輸出重定向到標準輸出ls test.txt non_existing_file > output.txt 2>&1# 查看 output.txt 的內容cat output.txt# 終端顯示ls: cannot access 'non_existing_file': No such file or directorytest.txt# 如果使用 2> 會導致訊息消失,因為錯誤輸出已經重定向到標準輸出了ls test.txt non_existing_file 2> output.txt 2>&1# 查看 output.txt 的內容cat output.txt# 終端無顯示任何東西,output.txt 是空的 1>&2 command123456789101112131415161718# non_existing_file 是不存在的檔案# 列出 test.txt 和 non_existing_file 檔案資訊,並錯誤輸出重定向到 output.txt,最後要求標準輸出輸出重定向到錯誤輸出ls test.txt non_existing_file 2> output.txt 1>&2# 查看 output.txt 的內容cat output.txt# 終端顯示ls: cannot access 'non_existing_file': No such file or directorytest.txt# 如果使用 > 會導致訊息消失,因為標準輸出已經重定向到錯誤輸出了ls test.txt non_existing_file > output.txt 1>&2# 查看 output.txt 的內容cat output.txt# 終端無顯示任何東西,output.txt 是空的 健忘筆記 同時重定向標準輸出和標準錯誤輸出,可以使用 command &> output.log command > output.log 2>&1 command 2> output.log 1>&2 三種方法都有一樣的效果 動手做做看 結論了解並熟悉資料輸出重定向的用法,能夠讓我們更靈活地處理和管理命令的輸入輸出。以下是一些需要注意的地方: 依不同的需求,使用不同的輸出方式 > 和 2> 會覆蓋輸出檔案原本的內容。 >> 和 2>> 會將輸出附加到檔案原本的內容末尾。 重定向符號的前後順序不影響命令的結果 如果你有其他有趣的用法或技巧,歡迎在下方留言分享! 動手做做看 Linux 線上模擬器","link":"/Backend/Linux/Understanding-Data-Redirection/"},{"title":"【Git、Hexo】deploy github 檔名大小寫問題","text":"本篇重點 hexo deploy 檔名大小寫不同,導致網頁404 git 忽略檔名大小寫的原因 解決 hexo deploy github 檔名大小寫問題 hexo deploy github page 檔名大小寫不同,導致網頁404有一次無意間發現網站中的某幾個頁面會404 error,但是在本地 hexo s 測試卻是正常的,重新 deploy 還是沒有修正這個問題,仔細比對檔案和網址後發現 github 上的檔名和網址路徑不同 造成錯誤的原因是剛開始設計的 tag 名稱是大寫,但後來修改為小寫命名,deploy 到 github 並沒有更新為小寫,導致 github page 404 錯誤,也就是說 git 並未追蹤檔名的大小寫。 健忘筆記 404 錯誤是 HTTP 的其中一種「標準回應訊息」(HTTP狀態碼),表示網頁不存在 git 忽略檔名大小寫的原因git 預設是忽略追蹤檔名大小寫,所以檔名做大小寫變更時並不會記錄。原因是許多作業系統(例如 Windows 和 macOS)的檔案系統,檔名大小寫是不敏感的。例如:建立 test 資料夾後,再建立 TEST 資料夾,系統會詢問是否要取代檔案,認為兩個檔案名稱是相同的 解決 hexo deploy github 檔名大小寫問題修改檔名的當下,git 設定是忽略檔名大小寫的,因此之後的更新沒辦法改變 github 上的檔名,解決這個問題必須清空 github 上的檔案並且重新上傳。 1. 設定 git 追蹤檔名大小寫執行 hexo d 的時候,會從 .deploy_git 資料夾將檔案更新至設定好的 github,所以需要修改 .deploy_git 的 git 設定,讓 git 追蹤檔名的大小寫,以便之後修改檔名能正確更新。 command12345// 進入 .deploy_git 資料夾cd .deploy_git// 設定追蹤檔名大小寫git config --local core.ignorecase false 查看更多 git 設定 2. 清空 github 上的檔案command12345678// 移除當前目錄及其子目錄中的所有文件和文件夾,新增至暫存區git rm -rf *// 提交變更git commit -m 'clean all file'// 更新至 githubgit push https://github.com/forgetfulengineer/forgetfulengineer.github.io.git HEAD:main 健忘筆記 git push https://github.com/forgetfulengineer/forgetfulengineer.github.io.git HEAD:main https://github.com/forgetfulengineer/forgetfulengineer.github.io.git 為遠端名稱 HEAD 為當前所在的分支 main 為 github 上要更新的分支 意思是把當前所在的分支更新到遠端的 main 分支。遠端名稱可以透過 git config --local -l 查詢 3. hexo 重新生成檔案並佈署command12345678// 回到 hexo 資料夾cd ..// 清除 hexo 生成的所有檔案hexo clean// 生成檔案並佈署hexo d -g 結論無意間發現的錯誤讓我更了解 git ,原來 git 預設是忽略追蹤檔名大小寫的。檔名修正更新後 php 標籤頁就恢復正常啦😁~ 快回想看看你的 github page 有沒有曾經更改過檔名大小,如果有,可能也會遇到 404 的問題,趕快清空 github 上的檔案重新佈署吧! 延伸閱讀 【官方文件】 Hexo 是甚麼 【官方文件】 github page 是甚麼","link":"/Other/Hexo/Handling-Case-Sensitivity-Issues-in-GitHub-Deployment/"},{"title":"【Hexo】多台電腦佈署 GitHub page 的問題","text":"本篇重點 了解 Hexo 佈署的流程 多台電腦佈署網站,導致 git 紀錄被覆蓋 使用腳本解決 git 紀錄被覆蓋的問題 Hexo 佈署的流程演示佈署網站的流程 command12345# 先移除原本已生成的檔案hexo clean# 生成檔案並且佈署到 githubhexo d -g 使用 hexo d 前要先到 _config.yml 中配置 GitHub 的設定 hexo d 的時候會將 public 目錄下的檔案複製到 .deploy_git 中,再從這個目錄強制推送 git push -f 到 repository,接著 github page 服務會自動偵測到推送的更新,並在一段時間內更新佈署到網站上 多台電腦佈署網站,導致 git 紀錄被覆蓋當使用 Hexo 在多台電腦上進行佈署時,可能會遇到 git 紀錄被覆蓋的問題。這是因為 hexo d 會從 .deploy_git 強制推送更新到 github,而不會先拉取最新的紀錄回 .deploy_git。因此,如果在不同的電腦上進行佈署,git 紀錄就可能會被覆蓋。 舉例來說,如果 A 電腦在 7 月 1 日更新了網站,隨後 B 電腦在 7 月 3 日也進行了更新,但由於 B 電腦的 .deploy_git 資料夾沒有拉取最新的記錄,因此 GitHub 上的記錄不會包含 7 月 1 日的更新。 雖然這對網站內容本身沒有任何影響,但版本控制的主要目的是記錄每一次的更改。因此,保留每次更新的記錄是必要的! 解決 git 紀錄被覆蓋的問題Hexo 官方沒有針對多台電腦佈署的具體方案,因此每次佈署網站時,都需要先進入 .deploy_git 更新進度再執行 hexo d,這樣非常不方便。為了解決這個問題,我編寫了一個腳本來自動化更新進度並佈署網站。 1. 製作自動化腳本 deploy.sh在每次佈署前先檢查 .deploy_git 資料夾是否存在,並拉取最新的進度。這樣可以確保佈署過程中的進度同步,避免覆蓋提交記錄的問題。 以下是完整的腳本示例: deploy.sh >folded1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253#!/bin/bash# GitHub Pages repository 的 URLREPO_URL="https://github.com/forgetfulengineer/forgetfulengineer.github.io.git"USER_NAME="forgetfulengineer"USER_EMAIL="thatforgetfulengineer@gmail.com"FIRST_DEPLOY=0# 檢查 .deploy_git 資料夾是否存在echo -e "\\n檢查 .deploy_git 資料夾是否存在..."if [ -d ".deploy_git" ]; then # 切換到 .deploy_git 資料夾並拉取最新進度 cd .deploy_git || { echo -e "\\n切換到 .deploy_git 資料夾失敗"; exit 1; } echo -e "\\n拉取 github page 最新進度..." git pull || { echo -e "\\n拉取進度失敗"; exit 1; } cd .. || { echo -e "\\n返回 Hexo 資料夾失敗"; exit 1; }else # 檢查 GitHub Pages repository 是否已有 commit echo -e "\\n檢查 GitHub Pages repository 是否已有 commit..." if git ls-remote --exit-code "$REPO_URL" HEAD > /dev/null 2>&1; then echo -e "\\nClone repository 到 .deploy_git 資料夾..." git clone "$REPO_URL" .deploy_git || { echo -e "\\nClone repository 失敗"; exit 1; } # 設置 git config cd .deploy_git || { echo -e "\\n切換到 .deploy_git 資料夾失敗"; exit 1; } git config --local user.name "$USER_NAME" git config --local user.email "$USER_EMAIL" git config --local core.ignorecase false cd .. || { echo -e "\\n返回 Hexo 資料夾失敗"; exit 1; } else echo -e "\\nGitHub Pages repository 尚無 commit,進行首次佈署" FIRST_DEPLOY=1 fifi# 清理舊的生成文件echo -e "\\n清理舊的生成文件..."hexo clean || { echo -e "\\nHexo 清理失敗"; exit 1; }# 生成新的靜態文件並佈署網站echo -e "\\n生成新的靜態文件並佈署網站..."hexo g || { echo -e "\\nHexo 生成失敗"; exit 1; }hexo d || { echo -e "\\nHexo 佈署失敗"; exit 1; }# 如果是首次部署,進入 .deploy_git 設置 git configif [ "$FIRST_DEPLOY" -eq 1 ]; then echo -e "\\n首次佈署,設置 git config --local core.ignorecase false" cd .deploy_git || { echo -e "\\n切換到 .deploy_git 資料夾失敗"; exit 1; } git config --local core.ignorecase false cd .. || { echo -e "\\n返回 Hexo 資料夾失敗"; exit 1; }fiecho -e "\\n佈署完成" 健忘筆記 生成檔案並佈署其實可以使用 hexo d -g 達到一樣的效果,但因為有些網站外掛套件(例如:hexo-minify)有綁定在 hexo g 或 hexo generate 這個階段啟動,所以生成和佈署分開才能確保套件正確啟動! 2. 執行 deploy.sh 腳本完成 deploy.sh 腳本後,將其放在執行 Hexo 的資料夾中。佈署網站時,只需執行 deploy.sh,神奇的事情就會發生囉~😝 command12# 在 hexo 資料夾的路徑執行 deploy.sh./deploy.sh 補充說明檢查 .deploy_git 資料夾是否存在使用 if [ -d ".deploy_git" ] 檢查 .deploy_git 資料夾是否存在。在 Shell 中,-d 選項常用來檢查資料夾是否存在。如果要檢查檔案是否存在,可以使用 -f 選項。若要檢查資料夾或是檔案是否存在,則可以使用 -e 選項。 使用 || 和 {} 處理命令執行失敗的情況|| 是邏輯 OR 操作符。它表示當前面的命令執行失敗(返回非零退出狀態碼)時,執行後面的命令。例如: command1command1 || command2 如果 command1 執行成功,那麼 command2 不會被執行。如果 command1 執行失敗,那麼 command2 會被執行。 {} 用於將多條命令組合成一個命令塊。命令塊中的命令會按順序執行。通常與邏輯操作符(如 || 或 &&)結合使用。例如: command1command1 || { command2; command3; } 如果 command1 執行失敗,則執行命令塊 { command2; command3; } 中的命令。 使用 echo -e 啟用反斜杠特殊字符轉義告訴 echo 啟用反斜杠特殊字符轉義,例如 \\n、\\t、\\r 。如果沒有使用 -e ,字串會直接顯示不會有特殊符號的效果。 檢查 GitHub Pages repository 是否已有 commit使用 git ls-remote --exit-code "$REPO_URL" HEAD 檢查 repository 。 ls-remote 用於檢查遠端儲存庫是否存在特定分支的命令,執行後終端顯示該分支最新的進度。 command1234git ls-remote --exit-code https://github.com/forgetfulengineer/forgetfulengineer.github.io.git# 終端顯示4w727c14a1260cf59a65e34de3b643d3f3bee83f HEAD --exit-code 選項 如果找到指定的引用(例如 HEAD),命令會成功執行並返回退出碼 0。 如果未找到指定的引用,命令會返回退出碼 2 不使用 -exit-code 的情況 無論是否找到指定的引用,命令都會成功執行並返回退出碼 0 > /dev/null 2>&1; 表示不顯示 command 的輸出結果將標準輸出和錯誤輸出重定向到垃圾桶 (/dev/null),這樣執行的結果就不會顯示在終端。 想了解更多重定向符號的使用方法,可以到【Linux】解析資料重定向 查看 if [ -d ".deploy_git" ]; then 和 if git ls-remote --exit-code "$REPO_URL" HEAD > /dev/null 2>&1; then 兩種判斷式的差異[ ... ] 是 POSIX 標準中的測試命令,又稱 test 命令,用來檢查條件表達式是否為真,根據條件的結果返回不同的退出狀態碼。退出狀態碼為 0 表示條件為 true,非 0 表示條件為fasle,通常用於檔案或目錄的存在性、字串比較等操作。 if COMMAND; then 是基於命令執行後的退出狀態碼判斷。退出狀態碼為 0 表示 true,非 0 表示 false,通常用於判斷一個命令是否成功執行。 git config --local core.ignorecase false 設定 git 追蹤檔名大小寫有兩種情況會首次建立 .deploy_git 首次佈署,執行 hexo d 後建立 .deploy_git GitHub Pages repository 已有 commit,使用 git clone 建立 .deploy_git 建立後建議進入 .deploy_git 設定 git 追蹤檔名大小寫,不然有機會遇到 git 忽略檔名大小寫導致 github page 404 的錯誤 想了解更多 git config 的設定,可以到【Git】了解 git config 設定 查看 結論透過上述腳本,我們可以確保在多台電腦上進行 Hexo 佈署時,不會覆蓋 GitHub 上的提交記錄。這樣的解決方案不僅簡單且有效,同時也考慮到首次佈署的情況,確保每次佈署順利進行。 另外也可以使用 GitHub Actions 解決,GitHub Actions 可以設定自動化工作流程,將 Hexo 生成的靜態網站自動佈署到 GitHub Pages,但跟我的佈署習慣不同,所以就沒有使用了,使用方式可以看這篇教學。 希望這篇文章對遇到相同問題的你有所幫助~😁","link":"/Other/Hexo/Multi-Computer-Deployment-Issues-on-GitHub-Page/"},{"title":"【HTML】button 在 form 裡和在 form 以外的差別","text":"本篇重點 了解 <button> 在 <form> 裡與 <form> 以外的差別 了解 <form> 的預設 method 和預設 action <button> 在 <form> 裡與 <form> 以外的差別<button> 在 <form> 裡 當 <button> 放在 <form> 裡且沒有指定 type 屬性時,它的預設 type 是 submit,會提交整個表單到指定的 action URL 如果指定了 type="button",按鈕就不會觸發表單提交,只是單純的按鈕,可以透過 JavaScript 綁定其他行為 html12345<!-- 按下提交按鈕會觸發表單提交,並將資料發送到 "example.php" --><form method="post" action="example.php"> <input type="text" name="username"> <button>提交</button> <!-- 相當於 <button type="submit">提交</button> --></form> <button> 在 <form> 以外 在 <form> 以外的 <button> 元素,預設 type="button",按下時不會觸發表單提交 這種 <button> 通常用於執行與表單提交無關的操作,例如觸發 JavaScript 函數等 html12345<!-- 按下提交按鈕甚麼事都不會發生 --><form method="post" action="example.php"> <input type="text" name="username"></form><button>提交</button> <!-- 相當於 <button type="button">提交</button> --> 案例說明在處理表單返回按鈕的功能時,發現一個問題,當 <button> 沒有設定 type 屬性時,點擊按鈕會自動送出表單,這和原本預設的行為不符。 以下範例的樣式使用 bulma html1234567891011121314151617181920212223<!-- 建立一個表單,底部有兩個按鈕,一個是返回指定頁面,一個是提交表單 --><form> <div class="field is-horizontal"> <div class="field-label is-normal"> <label class="label">Name</label> </div> <div class="field-body"> <div class="field"> <div class="control"> <input class="input" type="text" name="username" placeholder="Text input"> </div> </div> </div> </div> <div class="field is-grouped is-grouped-centered"> <a href="/" class="control"> <button class="button is-link">返回</button> <!-- 預期是返回首頁 --> </a> <div class="control"> <button class="button is-link">提交</button> <!-- 預期是提交表單 --> </div> </div></form> 實例 Name 返回 提交 範例中,<button> 位於 <form> 內部,當按鈕沒有設定 type 屬性時,瀏覽器會將其視為 type="submit"。所以點擊「返回」按鈕時,會觸發表單的提交操作,而不是前往 href 指定的首頁。 為了正確的前往指定連結,就要將「返回」按鈕的 type 設定為 button html1234<!-- <button> 的 type 設定為 button --><a href="/" class="control"> <button type="button" class="button is-link">返回</button></a> 實例 Name 返回 提交 設定後,點擊「返回」按鈕會前往首頁,而不會提交表單。 健忘筆記 <button> 位於 <form> 內且未設置 type 時,默認為 type="submit",會提交表單。 <form> 的預設 method 和預設 action 預設 method:沒有設定 method 屬性,<form> 元素的預設提交方法是 GET。表單數據會被附加到 URL 中進行提交。 預設 action:沒有設定 action 屬性,表單數據會提交到當前頁面本身的 URL。 html12345678910111213141516171819202122<form> <div class="field is-horizontal"> <div class="field-label is-normal"> <label class="label">Name</label> </div> <div class="field-body"> <div class="field"> <div class="control"> <input class="input" type="text" name="username" placeholder="Text input"> </div> </div> </div> </div> <div class="field is-grouped is-grouped-centered"> <a href="/" class="control"> <button type="button" class="button is-link">返回</button> </a> <div class="control"> <button class="button is-link">提交</button> </div> </div></form> 實例 Name 返回 提交 提交表單後會以 GET 的方式送到原網址,因此可以看到網址列最後面會加上 ?username=[填入的字串] 動手做做看 html 線上編輯器 結論這個 <button> 小細節是在我製作表單時不小心踩到的坑。當時因為沒有指定屬性,結果 <button> 在不同情境下的預設值竟然不一樣,讓我花了不少時間才找到問題。標籤的預設屬性在不同情境下可能會有很大的差異,還有甚麼在不同情境下有不同預設屬性的標籤嗎?歡迎分享給我~ 延伸閱讀 【MDN】<button> 介紹 【MDN】<form> 介紹 【medium】HTML5 新增的表單元件 & 屬性","link":"/Frontend/Html/Difference-Between-button-Inside-and-Outside-of-a-form/"},{"title":"【PHP】陣列合併應用:array + array、array_merge()、array_replace()","text":"本篇重點 陣列的自定義鍵值和流水號鍵值 array + array、array_merge()、array_replace() 的使用方式和差異 陣列的自定義鍵值和流水號鍵值陣列的鍵值可以是自定義的字符串或整數,也可以是預設的流水號鍵值 實例搶先看 陣列的自定義鍵值和流水號鍵值 自定義鍵值:使用有意義的鍵來標識陣列中的元素 流水號鍵值:從 0 開始遞增的整數鍵值,沒有指定鍵時,PHP 會自動生成預設的流水號鍵值 實例搶先看 自定義鍵值使用有意義的鍵來標識陣列中的元素 12345$customArray = [ 'name' => 'Alice', 'age' => 25, 'city' => 'New York']; 流水號鍵值從 0 開始遞增的整數鍵值。沒有指定鍵時,PHP 會自動生成預設的流水號鍵值 1$defaultArray = ['Apple', 'Banana', 'Cherry']; 同時使用自定義鍵值和流水號鍵值1234567$mixedArray = [ 'name' => 'Bob', 'age' => 30, 'colors' => ['Red', 'Green', 'Blue'], '2' => 'two', 1 => 1]; 鍵值重複同一個陣列中使用相同的鍵值,後面定義的值會覆蓋前面的值。 12345$duplicateArray = [ 'name' => 'Bob', 'name' => 'Tom', 'age' => 30,]; 動手做做看 動手做做看 陣列合併應用array + array 保留第一組陣列的所有鍵值對 添加第二組陣列中不在第一組陣列的鍵值對 如果兩個陣列有相同的鍵值(不管字串或數字的鍵值),則保留第一組陣列的鍵值對 如果合併的時候有流水號鍵值,則保持原順序,不會重新排序 範例 實例搶先看 array + array 保留第一組陣列的所有鍵值對 添加第二組陣列中不在第一組陣列的鍵值對 如果兩個陣列有相同的鍵值,則保留第一組陣列的鍵值對 流水號鍵值保持原順序,不會重新排序 實例搶先看 12345678910111213141516171819202122232425262728293031323334353637383940414243// 自定義鍵值$array1 = ['a' => 1, 'b' => 2];$array2 = ['b' => 3, 'c' => 4];$result1 = $array1 + $array2;// 輸出結果print_r($result1);Array( [a] => 1 [b] => 2 [c] => 4)// 流水號鍵值$array3 = [0 => 'Apple', 3 => 'Banana'];$array4 = [0 => 'Cherry', 2 => 'Grap'];$result2 = $array3 + $array4;// 輸出結果print_r($result2);Array( [0] => Apple [3] => Banana [2] => Grap)// 同時使用自定義鍵值和流水號鍵值$array5 = [0 => 'Apple', 'b' => 23, 3 => 'Banana'];$array6 = ['e' => 66, 'Cherry', 'Grap'];$result3 = $array5 + $array6;// 輸出結果print_r($result3);Array( [0] => Apple ['b'] => 23 [3] => Banana ['e'] => 66 [1] => Grap) 動手做做看 動手做做看 array_merge() 合併一組或多組陣列 保留所有參與合併陣列的鍵值對 如果有相同的鍵值 (不是數字的鍵值),後面陣列的鍵值對會覆蓋前面的鍵值對 如果合併的時候有數字或流水號鍵值,則重新排序 (從0開始遞增) 範例 實例搶先看 array_merge() 合併一個或多個陣列 保留所有參與合併陣列的鍵值對 如果有相同的鍵值 (不是數字的鍵值),後面陣列的鍵值對會覆蓋前面的鍵值對 如果合併的時候有數字或流水號鍵值,則重新排序 (從0開始遞增) 實例搶先看 12345678910111213141516171819202122232425262728293031323334353637383940414243444546// 自定義鍵值$array1 = ['a' => 1, 'b' => 2];$array2 = ['b' => 3, 'c' => 4];$result1 = array_merge($array1, $array2);// 輸出結果print_r($result1);Array( [a] => 1 [b] => 3 [c] => 4)// 流水號鍵值$array3 = [0 => 'Apple', 3 => 'Banana'];$array4 = [0 => 'Cherry', 2 => 'Grap'];$result2 = array_merge($array3, $array4);// 輸出結果// 陣列都是數字鍵值,合併後鍵值重新排序print_r($result2);Array( [0] => Apple [1] => Banana [2] => Cherry [3] => Grap)// 同時使用自定義鍵值和流水號鍵值$array5 = [0 => 'Apple', 'b' => 23, 3 => 'Banana'];$array6 = ['e' => 66, 'Cherry', 'Grap'];$result3 = array_merge($array5, $array6);// 輸出結果print_r($result3);Array( [0] => Apple [b] => 23 [1] => Banana [e] => 66 [2] => Cherry [3] => Grap) 動手做做看 動手做做看 array_replace() 合併一組或多組陣列 添加後面陣列中不在第一組陣列的鍵值對 如果有相同的鍵值 (不管字串或數字的鍵值),後面陣列的鍵值對會覆蓋前面的鍵值對 如果合併的時候有流水號鍵值,則保持原順序,不會重新排序 範例 實例搶先看 array_replace() 合併一個或多個陣列 添加後面陣列中不在第一組陣列的鍵值對 如果有相同的鍵值 (不管字串或數字的鍵值),後面陣列的鍵值對會覆蓋前面的鍵值對 如果合併的時候有流水號鍵值,則保持原順序,不會重新排序 實例搶先看 12345678910111213141516171819202122232425262728293031323334353637383940414243// 自定義鍵值$array1 = ['a' => 1, 'b' => 2];$array2 = ['b' => 3, 'c' => 4];$result1 = array_replace($array1, $array2);// 輸出結果print_r($result1);Array( [a] => 1 [b] => 3 [c] => 4)// 流水號鍵值$array3 = [0 => 'Apple', 3 => 'Banana'];$array4 = [0 => 'Cherry', 2 => 'Grap'];$result2 = array_replace($array3, $array4);// 輸出結果print_r($result2);Array( [0] => Cherry [3] => Banana [2] => Grap)// 同時使用自定義鍵值和流水號鍵值$array5 = [0 => 'Apple', 'b' => 23, 3 => 'Banana'];$array6 = ['e' => 66, 'Cherry', 'Grap'];$result3 = array_replace($array5, $array6);// 輸出結果print_r($result3);Array( [0] => Cherry [b] => 23 [3] => Banana [e] => 66 [1] => Grap) 動手做做看 動手做做看 array + array、array_merge()、array_replace() 相互比較合併組數 array + array:只能合併兩組陣列 1$result = $array1 + $array2; array_merge():可合併多組陣列 1$result = array_merge($array1, $array2, $array3, ...); array_replace():可合併多組陣列 1$result = array_replace($array1, $array2, $array3, ...); 相同鍵值時,鍵值對的覆蓋方式 array + array: 不論是字符串鍵值還是數字鍵值,保留第一組陣列的鍵值對,不會被後面的鍵值對覆蓋 array_merge(): 字符串鍵值:後面陣列的鍵值對會覆蓋前面的鍵值對 數字鍵值:後面陣列的值會追加到結果陣列中,不覆蓋前面的鍵值對 array_replace(): 不論是字符串鍵值還是數字鍵值,後面陣列的鍵值對都會覆蓋前面的鍵值對 合併數字鍵值時,鍵值排序方式 array + array:保持數字鍵值的原順序,不會重新排序 123456789101112$array1 = [0 => 'Apple', 2 => 'Banana'];$array2 = [2 => 'Cherry', 4 => 'Grap'];$result = $array1 + $array2;// 輸出結果:print_r($result);Array( [0] => Apple [2] => Banana [4] => Grap) array_merge():數字鍵值會重新索引,從0開始遞增 12345678910111213$array1 = [0 => 'Apple', 2 => 'Banana'];$array2 = [2 => 'Cherry', 4 => 'Grap'];$result = array_merge($array1, $array2);// 輸出結果:print_r($result);Array( [0] => Apple [1] => Banana [2] => Cherry [3] => Grap) array_replace():保持數字鍵值的原順序,不會重新排序 123456789101112$array1 = [0 => 'Apple', 2 => 'Banana'];$array2 = [2 => 'Cherry', 4 => 'Grap'];$result = array_replace($array1, $array2);// 輸出結果:print_r($result);Array( [0] => Apple [2] => Cherry [4] => Grap) 動手做做看 PHP 線上編輯器 總結 array + array array_merge() array_replace() 合併陣列組數 兩組 多組 多組 相同鍵值的覆蓋方式 不論字符串鍵值還是數字鍵值,保留第一組陣列的鍵值對,不覆蓋 字符串鍵值:後者覆蓋前者數字鍵值:追加不覆蓋 不論字符串鍵值還是數字鍵值,後者覆蓋前者 合併數字鍵值時的排序方式 保持原順序,不重新排序 重新排序,從0開始遞增 保持原順序,不重新排序 array + array、array_merge()、array_replace() 三種合併陣列的方式,各自有相似的地方,但是執行結果有些許差異。 我很常會忘記它們各自的差異,所以需要陣列合併的時候會不知道要使用哪個方式,希望透過這篇整理可以幫助到我跟有同樣困擾的人😂","link":"/Backend/PHP/Analyzing-Three-Methods-of-Merging-Array/"},{"title":"【jQuery】click 事件綁定方式比較","text":"本篇重點 $('選擇器').click() 的應用和使用時機 $(document).on('click', '選擇器', function(){}) 的應用和使用時機 .on('click', function() {}) 不是委派給 document 就無法適用在動態生成的元素 先了解這些會幫助你更快理解唷😁 【jQuery、css】解析六種組合選擇器:相連、+、~、>、,、空格 在前端開發中,經常需要偵測使用者對於網頁上元素的點擊事件,jQuery 中,可以使用 .click() 或 .on('click') 事件來偵測元素的點擊事件,雖然都是偵測點擊事件,但兩者有些差異。 $(選擇器).click()應用重點 直接綁定點擊事件到指定的元素 只適用於當前 DOM 中已存在的元素 當指定元素被刪除,綁定的事件也會被刪除 使用時機 用於靜態元素,這些元素在 DOM 加載時已經存在且不會動態變更 適合簡單的事件綁定,不需要過多處理 健忘筆記 DOM(Document Object Model)是瀏覽器用來解讀和操作 HTML 或 XML 文件的結構化模型,它將 HTML 文件中的各種元素(如標籤、屬性、文本等)轉換為 JavaScript 可以操作的物件,讓開發者可以動態修改、刪除或新增內容,並控制網頁的行為。 範例 實例搶先看 $(選擇器).click() 直接綁定點擊事件到指定的元素 只適用於當前 DOM 中已存在的元素 當指定元素被刪除,綁定的事件也會被刪除 html123<button class="button is-link add_button"> 新增按鈕</button> js12345678let num = 1;$('.add_button').click(function() { let new_button = `<button class="button is-success add_button"> 新增按鈕 ${num} </button>`; $(this).after(new_button); num ++;}); 點擊 “新增按鈕” 產生新按鈕,新的按鈕雖然 class 也有 .add_button,但無法產生新的按鈕 $(document).on(‘click’, ‘選擇器’, function(){})應用重點 委派事件處理給 document,再綁定點擊事件到指定的元素 適用於當前或未來新增的元素 當指定元素被刪除,綁定的事件依然存在 使用時機 適用於動態生成的指定元素,確保事件觸發 範例 實例搶先看 $(document).on('click', '選擇器', function(){}) 委派事件處理給 document,再綁定點擊事件到指定的元素 適用於當前或未來新增的元素 當指定元素被刪除,綁定的事件依然存在 html123<button class="button is-link add_button"> 新增按鈕</button> js12345678let num = 1;$(document)on('click', '.add_button', function() { let new_button = `<button class="button is-success add_button"> 新增按鈕 ${num} </button>`; $(this).after(new_button); num ++;}); 點擊 “新增按鈕” 產生新的按鈕,新按鈕也可以產生新的按鈕 健忘筆記 早期的 jquery 是使用 .live(),但這個函數因為一些漏洞已在 jQuery 1.9 被刪除,所以請將 .live() 替換成 .on()詳情請看【geeksforgeeks】Replace live() with on() in jQuery 如果 .on('click', function() {}) 不是委派給 document 會發生甚麼事.on('click', function() {}) 不是委派給 document 就無法適用在動態生成的元素,只能綁定到當前 DOM 中已存在的元素 例如 實例搶先看 如果 .on('click', function() {}) 不是委派給 document 會發生甚麼事 不是委派給 document 就無法適用在動態生成的元素,只能綁定到當前 DOM 中已存在的元素 html123<button class="button is-link add_button"> 新增按鈕</button> js1234567891011121314151617let num = 1;$('.add_button').on('click', function() { let new_button = `<button class="button is-success add_button"> 新增按鈕 ${num} </button>`; $(this).after(new_button); num ++;});$('.add_button').click(function() { let new_button = `<button class="button is-success add_button"> 新增按鈕 ${num} </button>`; $(this).after(new_button); num ++;}); $('.add_button').on('click', function() {}) 和 $('.add_button').click(function() {}) 寫法不同,但兩者的效果是一樣的,都是綁定 click 事件到當前 DOM 中已存在的 .add_button 元素,動態生成的 .add_button 元素無法觸發事件 這兩種寫法的效果相同,但 .click(function() {}) 僅能偵測點擊事件;相比之下,使用 .on('click', function() {}) 更具彈性。除了偵測點擊事件,.on() 還能同時綁定多個事件,例如:.on('click mouseenter change', function() {}),因此在事件處理上提供了更多的靈活性。 結論 click(function() {}) 是最簡單的事件綁定方式,適合靜態元素的單一點擊事件。 $(document).on('click', function() {}) 是較複雜但更具彈性的方式,特別適合處理動態生成的元素或需要更靈活事件處理的情況。 根據具體使用場景選擇最適合的綁定方式,才能確保達到預期效果。如果你需要偵測點擊事件,不妨試試這兩種方式,看哪一種更符合你的需求! 動手做做看 codepen 線上編輯器 延伸閱讀【jQuery 官方文件】.click() 介紹【jQuery 官方文件】.on(‘click’, function() {}) 介紹【天矽科技】DOM 是什麼?","link":"/Frontend/JQuery/Comparison-of-Click-Event-Binding-Methods/"},{"title":"【jQuery】尋找父元素、同層元素和子元素的方法","text":"本篇重點 尋找父元素的常用方法:.parent() 、.parents() 、.closest() 尋找同層元素的常用方法:.siblings() 、.next() 、.prev() 尋找子元素的常用方法:.children() 、.find() 每個尋找元素方法的具體用途和應用場景 先了解這些會幫助你更快理解唷😁 【jQuery、css】解析六種組合選擇器:相連、+、~、>、,、空格 尋找父元素.parent()只尋找當前元素上一層匹配的元素,也就是尋找親身父母 範例 實例搶先看 .parent() 尋找親身父親,也就是尋找當前元素上一層元素 html123456789101112131415161718192021<div class="box"> <div class="content"> <h2>.parent() 的效果</h2> <h3>使用方式一</h3> <ul> <li class="target-1 target-2">取得所有 li 元素的直接父元素</li> <li>設定父元素的文字顏色</li> </ul> <h3>使用方式二</h3> <ul class='selected'> <li class="target-1 target-2">取得所有 li 元素的直接父元素,並且父元素的 class 有 "selected"</li> <li>設定父元素的文字顏色</li> <li class="selected">沒有 "selected" 的元素不會被設定</li> </ul> </div> <div class="field is-grouped"> <button class="button is-link example-1">使用方式一</button> <button class="button is-info example-2">使用方式二</button> <button class="button reset">reset</button> </div></div> js1234567891011121314// 使用方式一$('.example-1').on('click', function() { $('.target-1').parent().css('color', 'DodgerBlue');});// 使用方式二$('.example-2').on('click', function() { $('.target-2').parent('.selected').css('color', 'LightSkyBlue');});// reset$('.reset').on('click', function() { $('*').css('color', '');}); .parents()向上尋找所有匹配的祖先元素,直到根元素為止 範例 實例搶先看 .parents() 向上尋找所有祖先元素,直到根元素為止 html123456789101112131415161718192021<div class="box"> <div class="content selected"> <h2>.parents() 的效果</h2> <h3>使用方式一</h3> <ul> <li>取得所有 li 元素的所有匹配的祖先元素,直到根元素</li> <li class="target-1">設定所有祖先元素的背景顏色</li> </ul> <h3>使用方式二</h3> <ul class="selected"> <li>取得所有 li 元素的祖先元素,並且元素的 class 有 "selected" </li> <li>設定匹配的祖先元素背景顏色</li> <li class="target-1 target-2">只設定匹配的元素,因此使用方法一的ul不會被設定背景顏色</li> </ul> </div> <div class="field is-grouped"> <button class="button is-link example-1">使用方式一</button> <button class="button is-info example-2">使用方式二</button> <button class="button reset">reset</button> </div></div> js1234567891011121314// 使用方式一$('.example-1').on('click', function() { $('.target-1').parents().css('background-color', 'DodgerBlue');});// 使用方式二$('.example-2').on('click', function() { $('.target-2').parents('.selected').css('background-color', 'LightSkyBlue');});// reset$('.reset').on('click', function() { $('*').css('background-color', '');}); .closest() 向上尋找所有祖先元素,直到第一個匹配的元素停止 .closest() 方法必須代入參數 (選擇器) 範例 實例搶先看 .closest() 向上尋找所有祖先元素,直到第一個匹配的元素停止 .closest() 方法必須代入參數 (選擇器) html123456789101112131415<div class="box"> <div class="content selected"> <h2>.closest() 的效果</h2> <h3>使用方式</h3> <ul class="selected"> <li>取得所有 li 元素向上尋找祖先元素,直到第一個 class 有 "selected" 的元素停止</li> <li class="target-1">設定匹配元素的邊框</li> <li>只設定第一個匹配的元素,因此上層的 div class 有 "selected",但沒有被設定邊框</li> </ul> </div> <div class="field is-grouped"> <button class="button is-link example-1">使用方式</button> <button class="button reset">reset</button> </div></div> js123456789// 使用方式一$('.example-1').on('click', function() { $('.target-1').closest('.selected').css('border', '1px solid red');});// reset$('.reset').on('click', function() { $('*').css('border', '');}); 尋找同層元素.siblings()尋找當前元素的所有同層元素 (排除自己),也就是元素的兄弟姐妹 範例 實例搶先看 .siblings() 尋找當前元素的所有同層元素,也就是元素的兄弟姐妹 html12345678910111213141516171819202122<div class="box"> <div class="content selected"> <h2 class="selected">.siblings() 的效果</h2> <h3>使用方式一</h3> <ul class="target-1 selected"> <li>取得 class 有 "target-1" 的元素再匹配所有同層元素</li> <li>設定所有同層元素的文字顏色</li> <li>設定除了自己的同層元素</li> </ul> <h3 class="target-2">使用方式二</h3> <ul class="selected"> <li>取得 class 有 "target-2" 的元素再匹配所有同層 class 有 "selected" 元素</li> <li>設定所有匹配同層元素的文字顏色</li> <li>設定除了自己的同層元素</li> </ul> </div> <div class="field is-grouped"> <button class="button is-link example-1">使用方式一</button> <button class="button is-info example-2">使用方式二</button> <button class="button reset">reset</button> </div></div> js1234567891011121314// 使用方式一$('.example-1').on('click', function() { $('.target-1').siblings().css('color', 'DodgerBlue');});// 使用方式二$('.example-2').on('click', function() { $('.target-2').siblings('.selected').css('color', 'LightSkyBlue');});// reset$('.reset').on('click', function() { $('ul').siblings().css('color', '');}); .next()尋找當前元素同層的下一個元素,也就是元素最大的弟弟或妹妹 範例 實例搶先看 .next() 尋找當前元素同層的下一個元素,也就是元素最大的弟弟或妹妹 html123456789101112131415161718192021<div class="box"> <div class="content selected"> <h2>.next() 的效果</h2> <h3 class="target-1">使用方式一</h3> <ul> <li>取得 class 有 "target-1" 的元素再匹配同層的下一個元素</li> <li>設定同層的下一個元素的文字顏色</li> </ul> <h3 class="target-2">使用方式二</h3> <ul class="selected"> <li>取得 class 有 "target-2" 的元素再匹配同層下一個 class 有 "selected" 的元素</li> <li>設定同層下一個匹配元素的文字顏色</li> <li>如果同層下一個元素 class 沒有 "selected" 就無法設定文字顏色</li> </ul> </div> <div class="field is-grouped"> <button class="button is-link example-1">使用方式一</button> <button class="button is-info example-2">使用方式二</button> <button class="button reset">reset</button> </div></div> js1234567891011121314// 使用方式一$('.example-1').on('click', function() { $('.target-1').next().css('color', 'DodgerBlue');});// 使用方式二$('.example-2').on('click', function() { $('.target-2').next('.selected').css('color', 'LightSkyBlue');});// reset$('.reset').on('click', function() { $('*').css('color', '');}); .prev()尋找當前元素同層的上一個元素,也就是元素最小的哥哥或姐姐 範例 實例搶先看 .prev() 尋找當前元素同層的上一個元素,也就是元素最小的哥哥或姐姐 js123456789101112131415161718192021<div class="box"> <div class="content selected"> <h2>.prev() 的效果</h2> <h3 class="target-1">使用方式一</h3> <ul> <li>取得 class 有 "target-1" 的元素再匹配同層的上一個元素</li> <li>設定同層的上一個元素的文字顏色</li> </ul> <h3 class="selected">使用方式二</h3> <ul class="target-2"> <li>取得 class 有 "target-2" 的元素再匹配同層上一個 class 有 "selected" 的元素</li> <li>設定同層上一個匹配元素的文字顏色</li> <li>如果同層上一個元素 class 沒有 "selected" 就無法設定文字顏色</li> </ul> </div> <div class="field is-grouped"> <button class="button is-link example-1">使用方式一</button> <button class="button is-info example-2">使用方式二</button> <button class="button reset">reset</button> </div></div> js1234567891011121314// 使用方式一$('.example-1').on('click', function() { $('.target-1').prev().css('color', 'DodgerBlue');});// 使用方式二$('.example-2').on('click', function() { $('.target-2').prev('.selected').css('color', 'LightSkyBlue');});// reset$('.reset').on('click', function() { $('*').css('color', '');}); 這段代碼會將類別為 .current 的元素的上一個同層元素字體加粗。 尋找子元素.children()只尋找當前元素下一層匹配的元素,也就是尋找元素的親生小孩 範例 實例搶先看 .children() 只尋找當前元素下一層匹配的元素,也就是尋找元素的親生小孩 html123456789101112131415161718192021<div class="box"> <div class="content"> <h2>.children() 的效果</h2> <h3>使用方式一</h3> <ul class="target-1"> <li>取得 class 有 "target-1" 的元素再匹配所有下一層的元素</li> <li>設定所有下一層元素的文字顏色</li> </ul> <h3>使用方式二</h3> <ul class="target-2"> <li>取得 class 有 "target-2" 的元素再匹配所有下一層 class 有 "selected" 的元素</li> <li class="selected">設定所有下一層元素的文字顏色</li> <li class="selected">沒有 "selected" 的元素不會被設定</li> </ul> </div> <div class="field is-grouped"> <button class="button is-link example-1">使用方式一</button> <button class="button is-info example-2">使用方式二</button> <button class="button reset">reset</button> </div></div> js1234567891011121314// 使用方式一$('.example-1').on('click', function() { $('.target-1').children().css('color', 'DodgerBlue');});// 使用方式二$('.example-2').on('click', function() { $('.target-2').children('.selected').css('color', 'LightSkyBlue');});// reset$('.reset').on('click', function() { $('*').css('color', '');}); .find() 向下尋找所有匹配的後代元素 .find() 方法必須代入參數 (選擇器) 範例 實例搶先看 .find() 向下尋找所有匹配的後代元素 .find() 方法必須代入參數 (選擇器) html123456789101112131415<div class="box"> <div class="content target-1"> <h2 class="selected">.find() 的效果</h2> <h3>使用方式</h3> <ul> <li class="selected">取得 class 有 "target-1" 的元素再向下匹配所有 class 有 "selected" 的元素</li> <li class="selected">設定匹配元素的文字顏色</li> <li>如果元素的 class 沒有 "selected" 就無法設定文字顏色</li> </ul> </div> <div class="field is-grouped"> <button class="button is-link example-1">使用方式</button> <button class="button reset">reset</button> </div></div> js123456789// 使用方式一$('.example-1').on('click', function() { $('.target-1').find('.selected').css('color', 'DodgerBlue');});// reset$('.reset').on('click', function() { $('*').css('color', '');}); 結論jQuery 提供了強大的工具,能快速尋找父元素、同層元素與子元素,讓操作 DOM 結構更加靈活且高效。 尋找父元素 .parent():尋找最近的父元素 .parents():尋找所有祖先元素 .closest():尋找最近且符合條件的祖先元素 尋找同層元素 .siblings():尋找所有同層元素(不包括自身) .next():尋找下一個同層元素 .prev():尋找上一個同層元素 尋找子元素 .children():尋找所有直接子元素 .find():尋找所有符合條件的子元素 除了這些常用方法外,還有一些進階方法,如 .parentsUntil() 和 .offsetParent() 等,可以根據需求選擇最適合的方法~ 動手做做看 html 線上編輯器 延伸閱讀【天矽科技】DOM 是什麼?【jQuery 官方文件】tree-traversal 介紹","link":"/Frontend/JQuery/Methods-for-Finding-Parent-and-Sibling-and-Child-Elements/"},{"title":"【HTML】解析 <a> 的 rel 屬性","text":"本篇重點 了解 <a> 的 rel 屬性 <a> 常見的 rel 屬性值和用途 不同情境下 <a> rel 屬性的使用方式 現代瀏覽器在大部分情況下已經自動為帶有 target="_blank" 的連結加上 rel="noopener" 調用 window.opener 的方法 了解 <a> 的 rel 屬性<a> 元素常用於創建超連結,連結其他頁面或網站。而 rel 全名 “relationship” 用來定義當前文件和連結資源之間的關係,這個屬性不僅能夠影響 SEO,還能提升網站的安全性。 <a> 常見的 rel 屬性值和用途noopener當使用 target="_blank" 開啟新頁面時,使用此屬性可以防止新視窗操作原本的網站,避免潛在的安全風險。noopener 和 Javascript 有關,新視窗可以使用 window.opener 來控制原視窗,藉此執行惡意行為。 健忘筆記 現代瀏覽器在大部分情況下已經自動為帶有 target="_blank" 的連結加上 rel="noopener",所以有沒有設定 noopener 屬性,在新視窗使用 window.opener 都會得到 null,但為了確保相容性和覆蓋到所有情況,依然建議明確地設定 rel="noopener"。【Chrome Platform Status】自動為所有使用 target=”_blank” 的連結加上 rel=”noopener” 例如js12345678// 原視窗重新導向到首頁window.opener.location.href = "https://forgetfulengineer.github.io/";// 開啟原視窗的 alertwindow.opener.alert('你點到惡意連結了');// 操作原視窗的 DOMwindow.opener.document.querySelector('body').innerHTML='<b style="color: red; font-size: 100px; margin: auto;">你點到惡意連結了</b>'; noreferrer阻止瀏覽器在新頁面中傳送 Referer 標頭,從而保護當前頁面 URL 不被暴露給目標網站。這個屬性也會自動包含 noopener 的效果。 健忘筆記 網站內部連結時,千萬別加上 noreferrer 這個屬性值,否則會影響 SEO! nofollow告訴搜尋引擎忽視兩個網站間的關聯,不要追蹤此連結,對於原視窗沒有影響,但會影響新視窗的流量分析和 SEO。這個屬性通常應用在廣告或不信任的外部連結上,以防止影響網站的 SEO 排名。 健忘筆記 backlink 是 SEO 計算排名的指標之一,如果某知名網站推廣了健忘工程師網站,並且沒有使用 nofollow,就可能提高網站的 SEO 分數。 external告訴瀏覽器或一些輔助工具(如 SEO 工具或網頁分析工具),這個連結指向的是一個外部網站。 ugc(User Generated Content)用於標示用戶生成的內容,常見於評論、留言等。 sponsored表示該連結為付費推廣或廣告內容,這有助於透明地告知搜尋引擎該連結的性質。 不同情境下 <a> rel 屬性的使用方式 rel 屬性可以疊加使用 外部連結針對外部連結可以使用 rel="noopener noreferrer nofollow external" html1<a href="<https://www.google.com>" target="_blank" rel="noopener noreferrer nofollow external">google</a> 提升 SEO 針對付費推廣連結時可以使用rel="sponsored" 針對用戶自行提交的連結可以使用 rel="ugc nofollow" html123<a href="<https://ads.com>" rel="sponsored">Ads</a><a href="<https://user-content.com>" rel="ugc nofollow">User Content</a> 調用 window.opener 的方法隨著瀏覽器安全性的提升,現代瀏覽器已經自動為所有帶有 target="_blank" 的連結加上 rel="noopener",這是為了防止新視窗通過 window.opener 對原視窗進行操作,避免可能的安全漏洞(如釣魚攻擊)。 如果在某些情境下需要測試或使用 window.opener,這邊提供兩種方法~ 設置 rel="opener" 使用 window.open() 開啟新連結 需要注意的是,這只能在原視窗與新視窗位於相同域名的情況下使用,如果是跨域情況,則無法操作 window.opener。 html12345<!-- 設置 rel="opener" --><a href="https://example.com" target="_blank" rel="opener">測試 window.opener</a><!-- 使用 window.open() 開啟新連結 --><button onclick="javascript:window.open('https://example.com');">測試 window.opener</button> 😈 惡意連結實例 原視窗重新導向到首頁 開啟原視窗的 alert 操作原視窗的 DOM 結論rel 屬性雖然簡單,但在網站的安全性、SEO 扮演著重要角色。 noopener:阻擋新視窗操作原視窗 (現代瀏覽器已經自動為所有帶有 target="_blank" 的連結加上 rel="noopener") noreferrer:阻止瀏覽器在新頁面中傳送 Referer 標頭,影響新視窗的 SEO nofollow:告訴搜尋引擎忽視兩個網站間的關聯,影響新視窗的 SEO external:告訴瀏覽器此連結是外部連結 ugc:標示用戶生成的內容 sponsored:標示此連結為付費推廣或廣告內容 延伸閱讀【MDN】rel 屬性介紹【MDN】Window: opener 屬性介紹【Chrome Platform Status】自動為所有使用 target=”_blank” 的連結添加 rel=”noopener”","link":"/Frontend/Html/Understanding-the-rel-Attribute-of-the-a-Tag/"},{"title":"【Linux】使用 less 查看檔案","text":"本篇重點 less 基本使用方法 less 的實用選項:-N、-S、+F、-p、-i、-m less 檢視模式內的快捷鍵:/pattern、?pattern、&pattern、n、N、g、G、F、q、h 比較 less 和 more 的差異 在 Linux 中,經常需要查看文字檔案的內容,例如系統日誌、設定檔等。當查看大檔案時,使用 cat 會一次顯示全部內容,這不僅會造成閱讀困難,也可能因為資料龐大而影響載入速度。這時,less 就是一個非常實用的工具,能夠讓使用者輕鬆瀏覽檔案內容,不會一次性載入全部資料,並按需求檢視,接下來介紹 less 命令的使用方法。 less 基本使用方法在終端輸入 less 加上檔案名稱即可檢視檔案 command123# less [檔案名稱]less example.txt less 的實用選項當使用 less 查看檔案時,可以透過不同的選項來提升檢視效率 動手做做看 -N:顯示行號讓每一行都顯示行號,方便定位具體行數。 command123# less -N [檔案名稱]less -N example.txt -S:長行單行顯示預設情況下,超過螢幕寬度的長行會自動換行,使用此選項可以保持單行顯示。 command123# less -S [檔案名稱]less -S example.txt +F:追蹤檔案最後一行的變動類似 tail -f,適合查看動態更新的日誌文件,會自動顯示新增的內容。 command123# less +F [檔案名稱]less +F example.txt 健忘筆記 less +F 是追蹤檔案最後一行的變動,如果透過編輯器修改文件的中間部分或重寫文件的某些區塊,可能會導致 less +F 無法正確解析或錯誤更新,因為它只關注尾端的變化。 -p "pattern": 直接搜尋特定字串打開檔案的同時,直接搜尋特定字串,並跳轉到該字串的位置。 command123# less -p "pattern" [檔案名稱]less -p "test" example.txt -i:搜尋時忽略大小寫查看文件時,搜尋關鍵字忽略大小寫。 command123# less -i [檔案名稱]less -i example.txt -m:顯示查看進度查看文件時,底部顯示當前進度。 command123# less -m [檔案名稱]less -m example.txt 選項疊加使用各選項可以疊加使用,增加靈活度 command12# 查看文件時,顯示行號、長行單行顯示、顯示查看進度less -NSm example.txt 檢視模式內的快捷鍵進入 less 的檢視模式後,可以使用一些快捷鍵來操作檔案。 動手做做看 /pattern:向下搜尋字串,輸入 / 後跟著填入要搜尋的字串,less 會跳轉到該字串第一次出現的地方,按 n 可以跳到下一個匹配 ?pattern:向上搜尋字串,? 會向上搜尋,跟 / 搜尋相反 &pattern:只顯示匹配的字串行 n:跳轉到下一個匹配結果 N (shift + n):跳轉到上一個匹配結果 g:跳轉到檔案開頭 G (shift + g):跳轉到檔案結尾 F (shift + f):追蹤檔案最後一行的變動,和 less +F 的效果相同 q:退出 less h:顯示幫助頁面,列出所有快捷鍵 比較 less 和 more 的差異more 較早的工具,功能相對簡單 一次性加載整個檔案,當檔案很大時,可能會花較長時間加載 關閉 more 後,終端會保留顯示過的檔案內容 less more 的增強版本,具備更靈活的操作功能 分段加載檔案,僅顯示需要的部分 關閉 less 後,終端不會顯示檔案內容,回到執行 less 前的狀態 結論less 透過選項與快捷鍵可以靈活地檢視大檔案、日誌文件或程式碼,如果有其他有趣的選項或快捷鍵,歡迎在下方留言分享! 動手做做看 Linux 線上模擬器","link":"/Backend/Linux/Using-less-to-View-Files/"},{"title":"【GitHub】追蹤檔案修改紀錄","text":"本篇重點 查看檔案的 commit 紀錄 依作者或時間查詢 commit 紀錄 使用 blame 查看檔案的逐行修改紀錄 blame 模式下,還原檔案至指定 commit 前的狀態,查看修改前的內容 Git 的 git blame 查看檔案的逐行修改紀錄 在軟體開發中,版本控制是用來追蹤檔案修改紀錄的重要工具,不僅能掌握專案的歷史變更,還能快速查找特定的修改。GitHub 提供了多種實用工具來幫助開發者追蹤檔案變更,blame 功能就是其中一個非常實用的功能,可以逐行檢視修改詳情,接下來來了解 GitHub 上幾種查看檔案修改紀錄的方法。 查看檔案的 commit 紀錄每當檔案修改並提交 commit 後,會記錄修改人、修改時間以及提交內容說明。 1. 查找檔案透過目錄尋找檔案 透過路徑或檔名尋找檔案 2. 點擊檔案右上方的 History 顯示檔案的所有 commit 歷程(新到舊) 此頁面顯示所有與該檔案相關的提交紀錄,包括每次提交的 SHA 值、提交者、提交時間及提交說明,並且可以點擊 commit,進一步檢視 commit 的修改內容。 依作者或時間查詢 commit 紀錄在 commit 歷史頁面,GitHub 提供了篩選功能,可以依據作者、日期查詢 commit 紀錄。 依作者查詢因為這個專案只有我在維護,所以能查詢的作者只有 forgetfulengineer 依日期查詢 查詢功能能快速找到特定修改者或在特定時間範圍內更動紀錄,特別適合用於大型專案或長期維護的專案。 使用 blame 查看檔案的逐行修改紀錄blame 是 GitHub 上一項強大的功能,用於查看檔案每一行的修改紀錄,能精確地了解每一行的最新修改者及其修改時間,對於排查問題和追溯修改原因很有幫助。 1. 查找檔案 2. 點擊檔案左方的 Blame 3. 查看檔案的逐行修改紀錄可以看到每一行左側顯示了最新修改此行的 commit 記錄、作者和時間 在 blame 模式下,可以直接點擊每行的 commit 說明來檢視該提交的詳細內容,進一步追蹤更動的原因和細節。 還原檔案至指定 commit 前的狀態,查看修改前的內容有時可能想查看檔案某一行或某一個 commit 前的檔案內容,例如確認修改前的檔案狀況或排查錯誤。在 blame 模式下,可以點擊每行 commit ID 旁邊的按鈕達到還原的效果。 1. 點擊檔案左方的 Blame 2. 點擊 commit ID 旁邊的按鈕 點擊後,檔案將會還原到指定 commit 前的狀態! Git 的 git blame 查看檔案的逐行修改紀錄Git 提供 git blame 指令,追蹤檔案每一行的修改紀錄,這個指令會顯示出每行程式碼的最新修改資訊,包括提交的作者、日期和 commit ID。 123git blame [檔案名稱]git blame example.txt 結論blame 功能非常實用,尤其在排查問題和追溯修改原因時效果顯著。我個人在追蹤檔案的修改紀錄時經常使用 blame,不僅更清晰地了解修改細節,也提升了整體追蹤效率。如果 GitHub 有其他方便的功能,歡迎在下方留言分享! 延伸閱讀 【GitHub 官方文件】Viewing a file 【git 官方文件】git blame 說明","link":"/Other/GitHub/Tracking-File-Modification-History/"},{"title":"【Css】解析五種常見的相對單位","text":"本篇重點 常見的相對單位:%、em、rem、vh、vw 相對單位的使用方式和時機 在 CSS 中,除了固定的像素單位(px)之外,還有許多相對單位可以根據父元素比例或視窗大小或元素字體自動調整,是現代響應式設計(RWD)不可或缺的工具。 %:父元素的相對比例最基本的相對單位,常用來設定元素尺寸(例如 width、height、margin、padding 等),其值是相對於父元素對應屬性的比例,表示當父元素的尺寸改變時,使用百分比的子元素會隨之縮放。 範例利用百分比設計圖片區塊,讓圖片隨著不同的螢幕寬度自適應 實例搶先看 %:父元素的相對比例 相對於父元素對應屬性的比例 html123<div class="responsive-wrapper"> <img src="https://forgetfulengineer.github.io/gallery/covers/Understanding-five-Common-Relative-Units.png"></div> css123456789/* 寬度預設為 width: 100%,寬度隨螢幕寬度改變 */.responsive-wrapper { background-color: blue;}.responsive-wrapper img { width: 80%; /* 圖片佔父容器 80% */ height: auto; /* 維持圖片比例 */} em:字體大小的相對單位相對於當前元素或父元素字體大小的單位,常用於需要隨字體大小變化調整元素大小的排版,例如調整字體和內邊距,讓內、外邊距跟隨字體大小調整,保持整體視覺一致,或是設置多層次樣式,呈現層級關係時使用。 假設一個父容器的字體大小為 20px,子元素的 padding 設為 1.5em,則子元素實際的 padding 值為 30px,因為 1.5em * 20px = 30px。 健忘筆記 em 的參照順序: 當前元素有設置 font-size,則 em 以當前元素的字體大小為基準。 若當前元素沒有設定 font-size,會沿著 DOM 樹逐層向上找,直到找到最近一個有 font-size 的父元素。 如果沒有找到設置的字體大小,則最終會參照根元素 <html> 的 font-size。根元素默認值通常是 16px(但可透過 CSS 調整,例如 html { font-size: 10px; })。 範例 實例搶先看 em:字體大小的相對單位 相對於當前元素或父元素字體大小的單位 html1234<div class="parent"> 測試內容 <button class="child">我是按鈕</button></div> css12345678.parent { font-size: 30px;}button { font-size: 0.5em; padding: 0.5em 1em;} 健忘筆記 em 是相對於當前元素或父元素字體大小的單位,如果父元素和當前元素同時使用 em,則當前元素屬性的大小會隨著層級增加而累積 rem:根元素字體大小的相對單位rem(root em)是相對於根元素(通常是 <html>)字體大小的單位,統一的相對大小能確保在頁面中一致性設計。 假設根元素的字體大小為 20px,其子元素的 padding 設為 1.5rem,則子元素實際的 padding 值為 30px,因為 1.5rem * 20px = 30px。 範例 實例搶先看 rem:根元素字體大小的相對單位 相對於根元素(通常是 <html>)字體大小的單位 html1234<div class="container"> 容器文字大小為 24px <div class="child">子元素文字大小為 32px</div></div> css1234567/* 根元素 font-size 預設 16px */.container { font-size: 1.5rem; /* 1.5 * 16px = 24px */}.child { font-size: 2rem; /* 2 * 16px = 32px */} vh 和 vw:用戶視窗高和寬的比例相對於用戶視窗高度和寬度的單位,適合設計需要相對於視窗尺寸而調整的布局,讓元素能隨著不同裝置和瀏覽器窗口大小動態縮放,常用於設置全螢幕背景或全螢幕容器,例如:lightbox。 1vh 表示視窗高度的 1% 20vw 表示視窗寬度的 20% 範例 實例搶先看 vh 和 vw:用戶視窗高和寬的比例 相對於用戶視窗高度和寬度的單位 html1<img src="https://forgetfulengineer.github.io/gallery/covers/Understanding-five-Common-Relative-Units.png"> css1234img { height: 30vh; /* 視窗高度的 30% */ width: 50vw; /* 視窗寬度的 50% */} 健忘筆記 裝置旋轉的影響:vh 和 vw 會根據視窗實際顯示區的高度和寬度來計算,因此旋轉裝置時,元素可能會突然變大或縮小。 捲軸條影響:若瀏覽器視窗出現垂直或水平捲軸條,會佔據一定的寬度或高度,影響到 vw 和 vh 的實際計算結果。 本網站的 lightbox 使用 vw 來控制寬度,可以點擊 實例搶先看 或 文章圖片 查看元素的寬度~ 結論這些相對單位可以更靈活得設計響應式佈局,並確保網頁在不同設備和視窗大小上都能有適當的呈現。 %:父元素的相對比例 em:當前元素或父元素字體大小的相對單位 rem:根元素字體大小的相對單位 vh 和 vw:用戶視窗高和寬的比例 動手做做看 codepen 線上編輯器 延伸閱讀【W3Schools】單位大全【愛貝斯網路】RWD是甚麼?","link":"/Frontend/Css/Understanding-five-Common-Relative-Units/"},{"title":"【PHP、JavaScript】陣列自定義排序","text":"本篇重點 PHP 的 usort() 陣列自定義排序 JavaScript 的 .sort() 陣列自定義排序 自定義排序比較函數的規則 PHP usort() 和 JavaScript .sort() 的反向排序 陣列自定義排序應用範例 PHP 的 usort()根據自定義的邏輯對陣列進行排序,不保留鍵名,僅針對值進行排序,並且直接修改原陣列 usort($array, $callback); php123usort($array, function($a, $b) { return $a <=> $b;}); $array 為欲排序的陣列 $callback 為比較函數 比較函數的規則 return < 0:$a 排在 $b 前 return > 0:$a 排在 $b 後 return === 0:$a 和 $b 順序保持不變 <=> 介紹<=> 是 PHP 7 引入的「太空船運算子」(Spaceship Operator),用來比較兩個值的大小,返回三種結果 返回 -1:左邊的值小於右邊的值 返回 0:左邊的值等於右邊的值 返回 1:左邊的值大於右邊的值 範例 情境一,對一組隨機亂數的陣列,依小到大做排序 (升序) php1234567891011121314151617$array = [5, 2, 9, 1, 3];// 方法一usort($array, function($a, $b) { return $a - $b; // 差值決定排序順序});// 方法二usort($array, function($a, $b) { return $a <=> $b;});// 方法三 (單層的陣列可以直接使用 sort() 排序,預設排序是小到大)sort($array);print_r($array);// 輸出: [1, 2, 3, 5, 9] 動手做做看 對一組隨機亂數的陣列,依小到大做排序 動手做做看 情境二,假設得到一組陣列資料,裡面有各個活動的資訊,依需求對陣列排序。 整體順序是 1.未到期活動 2.已到期活動,且皆以最新到最舊活動排序,執行方式為判斷活動結束時間 end_date 是否小於現在時間 (是否到期),未到期活動排在已到期活動前面,再以活動開始時間 start_date 大到小排序 實例搶先看 假設得到一組陣列資料,裡面有各個活動的資訊,對陣列做排序 整體順序是 1. 未到期活動 2.已到期活動,且皆以最新到最舊活動排序 php1234567891011121314151617181920212223242526272829303132333435363738394041424344454647// 活動資訊陣列$array = [ [ "id" => 1, "event_time" => [ "start_date" => 1730084400, // 2024-10-28 11:00:00 "end_date" => 1731513659 // 2024-11-14 00:00:59 ] ], [ "id" => 2, "event_time" => [ "start_date" => 1729512000, // 2024-10-21 20:00:00 "end_date" => 1731254400 // 2024-11-11 00:00:00 ] ], [ "id" => 3, "event_time" => [ "start_date" => 1730102400, // 2024-10-28 16:00:00 "end_date" => 1731686400 // 2024-11-16 00:00:00 ] ]];// 排序函數usort($array, function ($a, $b) { $current_time = time(); $a_end_date = $a['event_time']['end_date']; $b_end_date = $b['event_time']['end_date']; $a_start_date = $a['event_time']['start_date']; $b_start_date = $b['event_time']['start_date']; // 首先判斷是否過期 $a_is_expired = $a_end_date < $current_time; $b_is_expired = $b_end_date < $current_time; // 如果一個過期一個未過期,將未過期的排前面 if ($a_is_expired !== $b_is_expired) { return $a_is_expired <=> $b_is_expired; } // 否則依據 start_date 從大到小排序 return $b_start_date <=> $a_start_date;});print_r($array); 動手做做看 動手做做看 JavaScript 的 .sort().sort() 是 JavaScript 提供的陣列排序方法,與 PHP 的 usort() 規則非常相似,允許傳入自定義的比較函數,並且直接修改原陣列。 array.sort([compareFunction]) js123array.sort(function (a, b) { return a - b;}); array 為欲排序的陣列 compareFunction 為比較函數 比較函數的規則: return < 0:a 排在 b 前 return > 0:a 排在 b 後 return === 0:順序保持不變 健忘筆記 JavaScript 沒有 <=> 運算子 範例 情境一,對一組隨機亂數的陣列,依小到大做排序 (升序) js123456789101112let array = [5, 2, 9, 1, 3];// 方法一,.sort() 預設就是以小排到大array.sort();// 方法二,差值決定排序順序array.sort(function (a, b) { return a - b;});console.log(array);// 輸出: [1, 2, 3, 5, 9] 情境二,假設需要依照指定順序照片排序 陣列內是指定順序的照片編號 實例搶先看 假設需要依照指定順序照片排序 陣列內是指定順序的照片編號 html12345<div class="images_container"> <img data-id="1" src="https://forgetfulengineer.github.io/gallery/covers/Tracking-File-Modification-History.png"></img> <img data-id="2" src="https://forgetfulengineer.github.io/gallery/covers/Understanding-five-Common-Relative-Units.png"></img> <img data-id="3" src="https://forgetfulengineer.github.io/gallery/covers/Using-less-to-View-Files.png"></img></div> js1234567891011let images_sorting_arr = [3, 1, 2];let images_container = $(".images_container");let images = $(".images_container [data-id]").toArray();category_news.sort(function (a, b) { return ( images_sorting_arr.indexOf($(a).data("id")) - images_sorting_arr.indexOf($(b).data("id")) );});images_container.empty();$(images).appendTo(images_container); PHP usort() 的反向排序php12345678910111213141516171819202122$array = [5, 2, 9, 1, 3];// 方法一usort($array, function($a, $b) { return $b - $a;});// 方法二usort($array, function($a, $b) { return $b <=> $a;});// 方法三 (使用負號來反轉比較結果)usort($array, function($a, $b) { return -($a <=> $b);});// 方法四 (單層的陣列可以直接使用 rsort() 排序)rsort($array);print_r($array);// 輸出: [9, 5, 3, 2, 1] 動手做做看 得到一組隨機亂數的陣列,依小到大做排序 動手做做看 JavaScript .sort() 的反向排序js12345678910111213let array = [5, 2, 9, 1, 3];// 方法一,先用 .sort() 小排到大,再用 .reverse() 把陣列順序反轉array.sort();array.reverse();// 方法二,差值決定排序順序array.sort(function (a, b) { return b - a;});console.log(array);// 輸出: [9, 5, 3, 2, 1] 結論PHP 的 usort() 和 JavaScript 的 .sort() 是功能非常相似的自定義排序工具,並遵循相同的比較函數邏輯判斷 return < 0:$a 排在 $b 前 return > 0:$a 排在 $b 後 return === 0:$a 和 $b 順序保持不變 自定義排序可以更靈活的依照需求得到想要的陣列,在開發上很有幫助! 延伸閱讀 【PHP 官方文件】usort 介紹 【w3schools】PHP Sorting Arrays 【w3schools】PHP Operators (邏輯運算子) 【w3schools】JavaScript Sorting Arrays","link":"/Backend/PHP/Custom-Sorting-an-Array/"},{"title":"【Git】強制更新遠端分支進度到本地分支","text":"本篇重點 強制更新遠端分支進度到本地分支的方法 git checkout -B 強制更新本地分支 刪除並重新建立本地分支 git reset --hard 強制更新本地分支 強制更新本地分支進度到遠端分支的方法 git push -f 強制更新遠端分支 在版本控制的過程中,有時會遇到本地分支的進度與遠端分支不同步的情況,而當遠端分支的進度才是正確版本時,需要強制更新本地分支來與遠端保持同步。以下提供三種方法來完成本地分支的強制更新! 強制更新遠端分支進度到本地分支方法一:git checkout -B(推薦)git12git fetch origingit checkout -B local-branch origin/remote-branch 使用 git fetch 更新遠端分支的資訊。 接著執行 git checkout -B,該指令會執行以下操作: 切換到指定的本地分支(若不存在,會自動建立)。 強制將本地分支重置為遠端分支的最新進度。 不管當前位於哪個分支,都可以直接使用 git checkout -B 來切換並更新指定分支 範例 遠端分支為 origin/develop,本地分支為 develop git12git fetch origingit checkout -B develop origin/develop 方法二:刪除並重新建立分支git123git fetch origingit branch -D local-branchgit checkout -b local-branch origin/remote-branch 使用 git fetch 更新遠端分支的資訊。 使用 git branch -D 刪除本地分支。 透過 git checkout -b 重新建立一個與遠端分支同步的本地分支。 範例遠端分支為 origin/develop,本地分支為 develop git1234git fetch origingit checkout mastergit branch -D developgit checkout -b develop origin/develop 健忘筆記 使用 git branch -D 刪除分支時,要先離開欲刪除的分支 git checkout -b 只能建立當前不存在的分支 方法三:git reset --hardgit123git fetch origingit checkout local-branchgit reset --hard origin/remote-branch 使用 git fetch 更新遠端分支的資訊。 使用 git checkout -b 切換到欲更新的分支。 使用 git reset --hard 將當前的本地分支強制重置為遠端分支的最新進度。 範例遠端分支為 origin/develop,本地分支為 develop git123git fetch origingit checkout developgit reset --hard origin/develop 健忘筆記 git reset --hard 會丟失本地的變更,本地未提交的更改將會消失 強制更新本地分支進度到遠端分支git push -fgit12git checkout local-branchgit push -f origin remote-branch 使用 git checkout -b 切換到要推送的分支。 使用 git push -f 強制更新遠端分支進度。 範例遠端分支為 origin/develop,本地分支為 develop git12git checkout developgit push -f origin develop 健忘筆記 如果遠端分支的名稱和本地分支的名稱不同(遠端分支為 origin/develop,本地分支為 dev),可以使用 git push -f origin dev:develop 做更新 結論我認為使用 git checkout -B 是最方便且直觀的方式,能快速完成本地分支與遠端分支的同步,且無需額外刪除或重建分支,適合大多數情況。不過,選擇哪種方式還是取決於當前分支的狀況與需求。需要特別注意的是,強制更新可能會無法復原,所以執行時要特別注意,確認無誤再更新,避免重要紀錄遺失。 延伸閱讀【Git 官方文件】git checkout -B 介紹【Git 官方文件】git push -f 介紹【Git 官方文件】git reset –hard 介紹【Git 官方文件】git fetch 介紹","link":"/Other/Git/Force-Update-Local-Branch-with-Remote-Branch-Progress/"},{"title":"【Git】使用 git commit 修改提交(commit)的資訊或是內容","text":"本篇重點 git commit --amend 修改最新一次提交的資訊或是內容 git commit --amend --author 修改提交的作者和信箱 git commit --amend --reset-author 重置提交的作者和信箱 git commit --amend -m 修改提交訊息 git commit --amend --no-editor 提交修改但不修改提交訊息 每次修改提交 git commit --amend 都會變更提交 ID 在使用 Git 時,提交(commit)後才發現需要修改提交的資訊或內容。Git 提供了 git commit --amend 指令,能夠修改最新一次提交的內容或資訊,這樣能夠避免新增額外的提交,保持乾淨的提交歷史。 修改提交資訊修改提交的作者和信箱提交的作者和信箱是透過 git config 指令來設定的 git git12git config --global user.name "你的名稱"git config --global user.email "你的信箱" 詳細設定方式可查看 【Git】了解 git config 設定 修改最新一次提交的作者名稱和信箱方法一,git commit --amend --reset-author 設定正確的作者名稱和信箱資訊 git12git config --local user.name "forgetfulengineer"git config --local user.email "thatforgetfulengineer@gmail.com" 重置作者名稱和信箱資訊 git1git commit --amend --reset-author 方法二,git commit --amend --author git1234git commit --amend --author="新作者名稱 <新信箱>"# 修改最新一次提交的作者名稱和信箱git commit --amend --author="forgetfulengineer <thatforgetfulengineer@gmail.com>" 健忘筆記 信箱的 <> 一定要加上 如果要修改多個提交的資訊,可以使用 git rebase -i 加上操作指令 e, edit 達到目的,詳細可查看 【Git】使用 git rebase 整理提交歷史 修改提交訊息修改最新一次提交的訊息 git1git commit --amend -m "修改後的提交訊息" 範例原本的提交訊息為「修正錯誤」,修改改成「修正登入功能錯誤」 git1git commit --amend -m "修正登入功能錯誤" 如果要修改多個提交訊息,可以使用 git rebase -i 加上操作指令 r, reword 達到目的,詳細可查看 【Git】使用 git rebase 整理提交歷史 修改提交內容使用 Git 時,我的提交習慣是根據遠端分支的狀態來決定處理方式。如果提交尚未推送到無法強制修改的遠端分支(例如 master 或 main),我會保持每個提交精簡且整潔,避免有修正型的提交。例如,在設計網站的樣式並進行提交時,如果尚未更新到這些重要的遠端分支,仍處於自己的開發分支中,發現有需要修正的部分時,我不會新增一個修正提交,而是直接修正後再合併到原提交中,確保分支保持整潔。 但是當提交已經推送到無法強制修改的遠端分支時,為了避免影響該分支的穩定性,我會選擇透過提交一個新的修正分支,保留修改記錄的完整性。 新增內容到最新一次的提交在最新一次提交中加入新的修改,並使用 git commit --amend --no-edit 更新提交,這樣會將新添加的內容更新到最新一次的提交中,並保留原提交訊息跟資訊(作者、信箱) git12git add example.jsgit commit --amend --no-edit 如果要修改多個提交的內容,可以使用 git rebase -i 加上操作指令 e, edit 達到目的,詳細可查看 【Git】使用 git rebase 整理提交歷史 不更改提交訊息如果不需要修改提交訊息,可以加上 --no-edit 選項,保持原提交訊息不變: git12345# 提交當前檔案git commit --amend --no-edit# 提交當前檔案並且修改作者名稱和信箱git commit --amend --no-edit --author="forgetfulengineer <thatforgetfulengineer@gmail.com>" 結論git commit --amend 是非常靈活的指令,用於修正最新一次提交的錯誤資訊或內容。然而需要注意的是,每次執行 git commit --amend 都會產生一個新的提交 ID,因此在使用時務必確認不會影響遠端分支的進度,尤其是在多人開發的專案中更需謹慎,以免造成分支衝突或其他協作問題。 延伸閱讀 【Git 官方文件】git commit --amend 介紹 【Git 官方文件】git commit --amend --no-edit 介紹 【Git 官方文件】git config 介紹","link":"/Other/Git/Using-git-commit-to-Modify-Commit-Data-or-Content/"},{"title":"【NPM、GitHub】解決 npm 版本落後 GitHub 版本的問題","text":"本篇重點 如果發現 npm 套件的版本有 bug,但 GitHub 上的版本已修正,應該如何安裝修正版套件? 怎麼處理 npm 版本落後於 GitHub 版本的狀況 怎麼使用 npm 安裝 GitHub 套件的指定版本? 怎麼使用 npm 更新 GitHub 套件? hexo-generator-sitemap 的 3.0.1 版本存在 bug,導致沒有指定 sitemap 檔案,影響網站 seo 本網站使用 hexo-generator-sitemap 自動生成站點地圖(sitemap.xml),但透過 npm 安裝後,套件沒有正常運作,檢查後發現 npm 套件的版本存在 bug,而套件的 GitHub 儲存庫已修正問題,但修正版尚未發佈至 npm。 本篇文章將以 hexo-generator-sitemap 為例,分享如何處理 npm 版本落後於 GitHub 版本的狀況。 使用 npm 安裝 GitHub 套件指定版本在 GitHub 找到相對應的套件,查找正確的版本(特定分支或 commit id 或 tag 版號) 方法一:使用 npm 指令安裝套件npm12345678# 安裝指定 branch 版本npm install <github-username>/<repository-name>#<branch># 安裝指定 commit 版本npm install <github-username>/<repository-name>#<commit-hash># 安裝指定 tag 版本npm install <github-username>/<repository-name>#<tag> 健忘筆記 也可以使用 npm install git+https://github.com/<github-username>/<repository-name>.git,這同等於 npm install <github-username>/<repository-name>。會使用 git+https://... 的情境可能是如果儲存庫在自建 Git 伺服器上或非 GitHub 平台,需要更高的靈活性或精確指定完整 URL。 範例npm12345678# 安裝最新的 master 分支版本npm install hexojs/hexo-generator-sitemap#master# 安裝指定 commit 版本npm install hexojs/hexo-generator-sitemap#3ac80ce7a3c2506e99b7bf69010ac00b363e6222# 安裝指定 tag 版本npm install hexojs/hexo-generator-sitemap#v3.1.0 健忘筆記 如果沒有指定版本 npm install hexojs/hexo-generator-sitemap,預設安裝該 GitHub 儲存庫 default branch 的最新提交(通常是 main 或 master 分支) 方法二:修改 package.json 檔案並安裝將套件的寫入 package.json 並指定版本1234// 安裝指定 branch 或 tag 或 commit 版本"dependencies": { "<package-name>": "github:<username>/<repository>#<tag-or-commit>"} 執行 npm install 安裝套件npm1npm install 範例1234// 安裝指定 branch 或 tag 或 commit 版本"dependencies": { "hexo-generator-sitemap": "github:hexojs/hexo-generator-sitemap#master"} npm1npm install 健忘筆記 如果沒有指定版本 "hexo-generator-sitemap": "github:hexojs/hexo-generator-sitemap#master",預設安裝該 GitHub 儲存庫的 default branch 的最新提交(通常是 main 或 master 分支) 使用 npm 更新 GitHub 套件重新安裝覆蓋原本的版本npm12# 安裝儲存庫預設分支的最新版本npm install <github-username>/<repository-name> 範例npm12# 安裝 hexojs/hexo-generator-sitemap 的最新版本npm install hexojs/hexo-generator-sitemap 如果 package.json 的設定是 "hexo-generator-sitemap": "github:hexojs/hexo-generator-sitemap",可以直接使用 npm install 就會安裝套件的最新版本! 健忘筆記 npm update 只針對 package.json 中指向 NPM 註冊表的套件進行檢查與更新,所以無法使用 npm update 來更新 GitHub 安裝的套件。 hexo-generator-sitemap 3.0.1 版本的 bug 【NPM】hexo-generator-sitemap 3.0.1 套件 錯誤說明套件的 lib/rel.js 負責將 <link rel="sitemap" href="..." /> 插入到 HTML 檔案的 <head> 標籤內。 lib/rel.jsnpm套件123// 將 sitemap 設定插到 <head> 的最後面const relSitemap = `<link rel="sitemap" type="application/xml" title="Sitemap" href="${url_for.call(this, path)}">`;data.replace(/<head>(?!<\\/head>).+?<\\/head>/, str => str.replace('</head>', `${relSitemap}</head>`)); 當前正規表達式只能匹配到 <head> 標籤單行存在的狀況 html1<head>...</head> 如果 <head> 標籤是多行排列時,正規表達式無法正確匹配,導致 sitemap 設定未寫入,影響網站 seo html1234// 正規表達式無法匹配<head> <meta charset="utf-8"></head> 修正錯誤hexo-generator-sitemap 的 GitHub 儲存庫已修正 <head> 標籤多行排列,正規表達式無法正確匹配的錯誤。 lib/rel.jsGitHub修改紀錄12- return data.replace(/<head>(?!<\\/head>).+?<\\/head>/, str => str.replace('</head>', `${relSitemap}</head>`));+ return data.replace(/<head>(?!<\\/head>).+?<\\/head>/s, str => str.replace('</head>', `${relSitemap}</head>`)); 啟用修飾詞 s 讓特殊字元 . 可以匹配所有字元,包括換行符(\\n 或 \\r\\n)。 結論在開發過程中發現 hexo-generator-sitemap 的錯誤,進一步了解到 NPM 套件版本有時無法完全跟上 GitHub 上的最新版本。為了解決這個問題,可以直接從 GitHub 安裝最新修正的版本,讓套件正常運行。但需要注意的是這種方法只是暫時性的解決方案,未來還是需要關注 NPM 的正式版本更新,確保專案的長期穩定性和可維護性。如果你有其他解決方法或建議,也歡迎在下方留言分享~ 延伸閱讀【NPM 官方文件】npm install 介紹【NPM】hexo-generator-sitemap 套件【GitHub】hexo-generator-sitemap 套件【Fooish 程式技術】Regex 正規表示法 - 修飾詞","link":"/Other/NPM/Resolving-the-Issue-of-npm-Version-Lagging-Behind-the-GitHub-Version/"},{"title":"【Tool】Unix 時間戳(Unix timestamp)轉換","text":"本篇重點 Unix timestamp 轉換工具 什麼是 Unix 時間戳(Unix timestamp) JavaScript 的 Date() 應用 如何建立日期物件、日期格式化、計算日期差 取得日期與時間的資訊、當前時區偏移量 #timestamp::-webkit-inner-spin-button, #timestamp::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; } #timestamp { -moz-appearance: textfield; } .field .icon { z-index: 5 !important; } .control.has-icons-left .input:focus~.icon { color: rgb(146, 127, 108); } .icon.is-right .icon-text { display: none; } .timestamp_help { visibility: hidden; } .card { transition: opacity 0.3s ease-out, transform 0.3s ease-out, box-shadow 0.3s ease-in-out; opacity: 1; transform-origin: center top; } Unix 時間戳(Unix timestamp)轉換工具 時間戳(timestamp) 秒 毫秒 timestamp invalid 日期時間(date) 直接輸入時間或點擊右方月曆選取時間 $(document).ready(function () { let timeout; // 設定 #timestamp 預設為當下時間戳(秒) const now = Math.floor(Date.now() / 1000); $(\"#timestamp\").val(now); // 顯示當前時區 const timezoneOffset = -new Date().getTimezoneOffset() / 60; const gmtString = `GMT ${timezoneOffset >= 0 ? \"+\" : \"\"}${timezoneOffset}`; $(\"#gmt\").text(gmtString); // 監聽 #timestamp 和 #unit 的變化 $(\"#timestamp\").on(\"input\", updateCalendar); $(\"#unit\").on(\"change\", updateCalendar); // 監聽 #calendar 的變化 $(\"#calendar\").on(\"input\", updateTimestamp); // 手動觸發一次輸入事件,顯示當下日期時間 $(\"#timestamp\").trigger(\"input\"); // 更新 #calendar 的日期 function updateCalendar() { const timestamp = parseInt($(\"#timestamp\").val(), 10); const unit = parseInt($(\"#unit\").val(), 10); const date = new Date(timestamp * unit); if (!isNaN(date.getTime())) { const formattedDate = date.getFullYear() + \"-\" + String(date.getMonth() + 1).padStart(2, \"0\") + \"-\" + String(date.getDate()).padStart(2, \"0\") + \"T\" + String(date.getHours()).padStart(2, \"0\") + \":\" + String(date.getMinutes()).padStart(2, \"0\") + \":\" + String(date.getSeconds()).padStart(2, \"0\"); $(\"#calendar\").val(formattedDate); timestampFeedback(true); } else { timestampFeedback(false); } } // 更新 #timestamp 的數值 function updateTimestamp() { const dateStr = $(\"#calendar\").val(); const date = new Date(dateStr); var timestamp = date.getTime(); if (!isNaN(timestamp)) { const unit = parseInt($(\"#unit\").val(), 10); timestamp = Math.floor(timestamp / unit); $(\"#timestamp\").val(timestamp); } } function timestampFeedback(isValid) { let timestamp = $(\"#timestamp\"), help = $(\".timestamp_help\"), check = timestamp.siblings(\".icon.is-right\").find(\".has-text-success\"), error = timestamp.siblings(\".icon.is-right\").find(\".has-text-danger\"); clearTimeout(timeout); if (isValid) { check.fadeIn(); error.hide(); help.fadeTo(300, 0, function () { $(this).css(\"visibility\", \"hidden\"); }); } else { error.fadeIn(); check.hide(); help.css({\"visibility\":\"visible\", \"opacity\":\"1\"}); } timeout = setTimeout(function() { check.fadeOut(300); error.fadeOut(300); help.fadeTo(300, 0, function () { $(this).css(\"visibility\", \"hidden\"); }); }, 1500); } }); 查看原始碼 Unix 時間戳(Unix timestamp)轉換工具 健忘筆記 input 元素可以使用 type="datetime-local" 產生 HTML5 內建的日期時間選擇器,想了解可以查看 【Html】實用的 <input> 表單屬性 什麼是 Unix 時間戳(Unix timestamp)Unix 時間戳(Unix timestamp)是一種時間表示方式,定義為從格林威治時間 1970 年 01 月 01 日 00 時 00 分 00 秒起到現在的總秒數。時間戳的特性是,不管在哪個時區,當下算出的時間戳都會是同樣的數值。這點對於應用程式非常有用,不管是要追蹤時間、計算時間差或是對日期進行排序,都可以利用時間戳來進行。在將時間戳轉換為時間時,也可以根據時區計算出當地的時間,是一種在程式中廣泛應用的技術。 JavaScript 的 Date() 應用Date 是 JavaScript 中內建的物件,專門用來處理日期和時間的工具。 建立日期物件Date 可以根據不同的方式建立日期物件 當前日期與時間javascript12const now = new Date();console.log(now); // Mon Jan 27 2025 16:20:45 GMT+0800 (台北標準時間) 指定日期javascript123// 2025 年 1 月 15 日const specificDate = new Date(2025, 0, 15);console.log(specificDate); // Wed Jan 15 2025 00:00:00 GMT+0800 (台北標準時間) 健忘筆記 JavaScript 的 Date() 月份(Month)是從 0 到 11 進行編號,0 表示 1 月、1 表示 2 月以此類推,因此指定月份的時候要手動減一 指定日期與時間javascript123// 2025/01/15 14:30:00const dateTime = new Date(2025, 0, 15, 14, 30, 0);console.log(dateTime); // Wed Jan 15 2025 14:30:00 GMT+0800 (台北標準時間) 透過字串建立javascript123// 解析 ISO 格式日期字串const fromString = new Date("2025-01-15T14:30:00");console.log(fromString); // Wed Jan 15 2025 14:30:00 GMT+0800 (台北標準時間) 透過時間戳建立javascript12const fromTimestamp = new Date(1673820000000);console.log(fromTimestamp); // Mon Jan 16 2023 06:00:00 GMT+0800 (台北標準時間) 健忘筆記 直接寫入時間戳的話,要以毫秒為單位 動手做做看 取得日期與時間的資訊Date 提供了多個方法來獲取特定的日期或時間資訊 javascript123456789101112131415const now = new Date();// 取得年份console.log("年:", now.getFullYear()); // 年: 2025// 取得月份(0-11,需要 +1)console.log("月:", now.getMonth() + 1); // 月: 1// 取得日期console.log("日:", now.getDate()); // 日: 27// 取得小時console.log("時:", now.getHours()); // 時: 16// 取得分鐘console.log("分:", now.getMinutes()); // 分: 26// 取得秒數console.log("秒:", now.getSeconds()); // 秒: 9// 取得星期幾(0 = 星期日)console.log("星期:", now.getDay()); // 星期: 1 健忘筆記 JavaScript 的 .getDay() 取得星期幾是從 0 到 6 進行編號,0 表示 星期日、1 表示 星期一以此類推。 *PHP 的 date('w') 也是如此,想了解 date('w') 可以查看 【PHP】五種實用的日期時間函數 動手做做看 時間戳應用Date 中的時間戳是以毫秒為單位,如果要取得以秒為單位的時間戳需要做轉換 javascript12345const now = new Date();// 取得毫秒時間戳console.log("毫秒時間戳:", now.getTime()); // 毫秒時間戳: 1737967042147// 換算成秒console.log("秒時間戳:", Math.floor(now.getTime() / 1000)); // 秒時間戳: 1737967042 動手做做看 日期格式化JavaScript 的 Date 沒有提供原生的格式化方法,但可以用字串拼接的方式實現 javascript12345678910111213141516// Date-Time Format:YYYY/MM/DD HH:MM:SSconst now = new Date();const formattedDate = now.getFullYear() + "/" + String(now.getMonth() + 1).padStart(2, "0") + "/" + String(now.getDate()).padStart(2, "0") + " " + String(now.getHours()).padStart(2, "0") + ":" + String(now.getMinutes()).padStart(2, "0") + ":" + String(now.getSeconds()).padStart(2, "0");console.log("格式化日期:", formattedDate); // 格式化日期: 2025/01/27 16:43:21 健忘筆記 padStart() 用來在字串的開頭填充指定的字元,以確保字串達到特定的長度。 str.padStart(targetLength , padString) targetLength:目標字串的長度,如果原字串長度已經大於或等於 targetLength,則不會進行填充。 padString:用來填充的字串。如果未指定,預設會用空白字元 (“ “) 填充。 動手做做看 取得當前時區偏移量new Date().getTimezoneOffset() 回傳當前時區的偏移量(以分鐘為單位),偏移量表示本地時間與 GMT 的差異,且為相反數。 如果是 UTC+8,則偏移量為 -480(本地時間比 GMT 快 480 分鐘)。 如果是 UTC-5,則偏移量為 300(本地時間比 GMT 慢 300 分鐘)。 javascript123456// 計算當前時區相對於 GMT 的時差(以小時為單位)const timezoneOffset = -new Date().getTimezoneOffset() / 60; // 根據時差生成對應的 GMT 字串const gmtString = `GMT ${timezoneOffset >= 0 ? "+" : ""}${timezoneOffset}`;console.log(gmtString); // GMT +8 動手做做看 計算日期差利用 Date 的時間戳,計算兩個日期的差值 javascript1234567const date1 = new Date("2025-01-01");const date2 = new Date("2025-01-15");const diffInMilliseconds = date2 - date1;const diffInDays = diffInMilliseconds / (1000 * 60 * 60 * 24);console.log(`日期相差 ${diffInDays} 天`); // 日期相差 14 天 動手做做看 延伸閱讀 【wikipedia】UNIX 時間 【MDN】Date() 介紹","link":"/Other/Tool/Unix-imestamp-Conversion/"},{"title":"【Git】使用 git rebase 整理提交歷史","text":"本篇重點 如果有尚未提交的變更,無法使用 git rebase git rebase 合併不會產生額外的合併提交,但是會變更原本的提交 ID git rebase -i(互動式 rebase)編輯功能 使用 git rebase -i 調整提交順序、修改提交訊息、修改提交內容、合併提交、刪除提交 git rebase 的注意事項 動手做做看 我建了一個範例 repository,可以 clone 下來後跟著文章操作!git clone https://github.com/forgetfulengineer/git-rebase-example.git Git 拒絕執行 rebase使用 git rebase 的前提是工作區(working directory)必須是乾淨的,也就是不能有尚未提交的變更,否則 Git 會拒絕執行 rebase,並顯示錯誤訊息 git error12Cannot rebase: You have unstaged changes.Please commit or stash them. 因為 git rebase 會重寫提交歷史,而未提交的變更可能會導致衝突或遺失,因此 Git 會強制要求先處理這些變更。 解決方法1. 使用 git stash 暫存變更如果目前的變更還不想提交,可以使用 git stash 暫存起來,執行 rebase 後再取回,適合暫時性變更,不影響提交歷史 git12345678# 暫存變更git stash# 進行 rebasegit rebase ...# 取回變更git stash pop 2. 使用 git rebase --autostash(自動暫存變更)可以使用 --autostash 讓 Git 在 rebase 前自動暫存變更,完成後再自動取回,適合經常遇到這個問題的人,減少手動 stash 的步驟 git1git rebase --autostash ... 3. 先提交變更暫時提交變更,執行 rebase 後再撤銷提交,適合暫時性變更,不影響提交歷史 git123git add .git commit -m "暫存變更以執行 rebase"git rebase ... 如果不想保留暫存提交,可以在 rebase 後使用 git reset HEAD~1 撤銷提交,讓變更回到工作區 git1git reset HEAD~1 git rebase 的基本功能將當前分支的提交銜接到另一個分支的最新進度上,與 git merge 不同,git rebase 不會產生額外的合併提交(merge commit),讓提交歷史保持乾淨且條理。 健忘筆記 如果兩分支變更的檔案有衝突,沒辦法 Fast-forward 合併,就需要重新調整提交內容 範例當前 main 分支最新進度開發到 C 節點,但 dev 分支是從 main 分支 B 節點開始開發,因此出現分支衝突的狀況。 git tree123A --- B --- C (main) \\ D (dev) 使用 git rebase 合併 main 分支進度,將 dev 分支的提交銜接到 main 的最新進度上 git12git checkout devgit rebase main git tree12# D 的提交 ID 會和原本不一樣A --- B --- C --- D' (dev) 這樣可以確保 dev 基於 main 的最新進度,並且提交記錄保持清晰,不產生額外的合併提交。 健忘筆記 git rebase 合併分支,銜接到最新進度的提交(commit),提交 ID 會和原本不一樣 git rebase -i(互動式 rebase)git rebase -i 用於編輯提交歷史,可以整理提交順序、修改提交內容或合併提交等,-i 是 interactive 的意思。 git1234567891011# 對最近的 N 次提交執行 rebase (N 替換成數字)git rebase -i HEAD~[N]# 對最近的 3 次提交執行 rebasegit rebase -i HEAD~3# 對 commit id 之後的提交執行 rebasegit rebase -i [commit id]# 對 a1b2c3d 之後的提交執行 rebasegit rebase -i a1b2c3d 健忘筆記 如果要修改 Git 儲存庫中的第一個 commit(即最早的根 commit),可以使用 git rebase -i --root 來從最根源的 commit 開始進行互動 互動式 rebase 編輯介面執行 git rebase -i [指定提交] 後會進入編輯介面,上到下以最舊到最新列出提交資訊,並且每個提交的前面會顯示操作指令 git rebae -i123pick a1b2c3d 初始提交pick d4e5f6g 修正錯誤pick h7i8j9k 增加新功能 git rebae -i1234# 設定操作方式可以直接使用縮寫指令也可以使用完整指令r a1b2c3d 初始提交reword d4e5f6g 修正錯誤d h7i8j9k 增加新功能 這些是我覺得比較重要的選項: 選項 功能描述 p(pick) 保留提交 r(reword) 修改提交訊息 e(edit) 修改提交內容 s(squash) 與前一個提交合併,並且設定提交訊息 f(fixup) 與前一個提交合併,並且直接使用前一個提交的提交訊息 d(drop) 刪除提交 完整操作指令說明可以在編輯介面查看 git rebae -i123456789101112131415161718# Commands:# p, pick <commit> = use commit# r, reword <commit> = use commit, but edit the commit message# e, edit <commit> = use commit, but stop for amending# s, squash <commit> = use commit, but meld into previous commit# f, fixup [-C | -c] <commit> = like "squash" but keep only the previous# commit's log message, unless -C is used, in which case# keep only this commit's message; -c is same as -C but# opens the editor# x, exec <command> = run command (the rest of the line) using shell# b, break = stop here (continue rebase later with 'git rebase --continue')# d, drop <commit> = remove commit# l, label <label> = label current HEAD with a name# t, reset <label> = reset HEAD to a label# m, merge [-C <commit> | -c <commit>] <label> [# <oneline>]# . create a merge commit using the original merge commit's# . message (or the oneline, if no original merge commit was# . specified); use -c <commit> to reword the commit message 調整提交順序如果提交的順序不符合邏輯,或希望讓提交記錄更清晰,可以調整提交的順序。 範例git rebae -i1234pick a1b2c3d test4pick d4e5f6g test2pick h7i8j9k test1pick y3i1j7r test3 重新調整提交歷史的順序,讓提交是 test1 到 test4 git rebae -i1234pick h7i8j9k test1pick d4e5f6g test2pick y3i1j7r test3pick a1b2c3d test4 調整完成後儲存,Git 就會按照新的順序排列。 健忘筆記 我的編輯介面是使用 vim 做編輯器,所以直接使用 :x(:x 等於 :wq)即可儲存後退出 git rebase -i,如果想把編輯器設定成 vim,可以參考 【Git】了解 git config 設定 修改提交訊息如果某次提交的訊息不夠清楚或有錯誤,可以使用 r, reword 來修改提交訊息。 範例git rebae -i1234pick h7i8j9k test1pick d4e5f6g test2pick y3i1j7r test3r a1b2c3d test4 儲存後,Git 會提示你輸入新的提交訊息 reword12# test4 -> test4 & test5test4 & test5 修改完成後儲存,就會更新成新的提交訊息 git12345$ git logh7i8j9k test1d4e5f6g test2y3i1j7r test3a1b413d test4 & test *補充:也可以一次修改多個提交訊息 git rebae -i1234r h7i8j9k test1r d4e5f6g test2pick y3i1j7r test3r a1b2c3d test4 健忘筆記 如果只是要修改當前最新的提交訊息,可以使用 git commit --amend -m "提交訊息" 做調整,詳細可以參考 【Git】使用 git commit 修改提交(commit)的資訊或是內容 修改提交內容如果某個提交有錯誤,或想要補充內容,或是想修改提交的資訊(作者、信箱等),可以使用 e, edit 來修改提交內容。 git rebae -i1234pick h7i8j9k test1e d4e5f6g test2pick y3i1j7r test3pick a1b2c3d test4 儲存後,Git 會暫停在該提交,即可對該提交進行修改 edit1234567891011121314# 修改檔案vim example.txt# 加入修改內容git add example.txt# 重新提交(覆蓋最新一次提交)git commit --amend --no-edit# 修改最新一次提交的作者名稱和信箱git commit --amend --author="forgetfulengineer <thatforgetfulengineer@gmail.com>"# 繼續 rebasegit rebase --continue *補充:也可以一次修改多個提交 git rebae -i1234pick h7i8j9k test1e d4e5f6g test2e y3i1j7r test3e a1b2c3d test4 健忘筆記 如果只是要修改當前最新的提交資訊,可以使用 git commit --amend 做調整,詳細可以參考 【Git】使用 git commit 修改提交(commit)的資訊或是內容 合併提交如果多個提交的內容屬於同一個邏輯並且想整理成一個提交,可以使用 s, squash 或是 f, fixup 來合併提交,使歷史記錄更加簡潔。 git rebae -i1234pick h7i8j9k test1s d4e5f6g test2pick y3i1j7r test3f a1b2c3d test4 儲存後,Git 會讓你選擇合併後的提交訊息,或是自行修改 squash12# 設定合併後的提交訊息add test1 & test2 git123$ git logh7i8j7k add test1 & test2d4e2f6g test3 完成後,提交將合併成一個 健忘筆記 s, squash 與前一個提交合併,並且設定提交訊息f, fixup 與前一個提交合併,並且直接使用前一個提交的提交訊息 刪除提交如果提交已經不需要或是有錯誤,可以使用 d, drop 來刪除 git rebae -i1234pick h7i8j9k test1pick d4e5f6g test2d y3i1j7r test3pick a1b2c3d test4 儲存後,Git 會該提交從歷史中刪除 git1234$ git logpick h7i8j9k test1pick d4e5f6g test2pick a1b2c3d test4 結論git rebase -i 提供了很多功能整理歷史提交,讓紀錄更清晰、整潔,這邊整理我常使用到的功能: 調整提交順序 修改提交訊息(r, reword) 修改提交內容(e, edit) 合併提交(s, squash 或 f, fixup) 刪除提交(d, drop) 雖然 git rebase 很好用,但使用時有一些需要注意的地方: 工作區不能有尚未提交的變更,否則 Git 會拒絕執行 rebase git rebase 的操作可能會改變歷史提交的提交 ID,導致發生分支衝突 多人開發時,要避免直接在公共分支使用 git rebase,可能導致和其他人分支衝突 熟悉需要注意的地方後,善用 git rebase 就可以讓 Git 提交歷史更整潔,提升專案的可讀性與維護性! 動手做做看 我建了一個範例 repository,可以 clone 下來後跟著文章操作!git clone https://github.com/forgetfulengineer/git-rebase-example.git 延伸閱讀 【Git 官方文件】git rebase 介紹 【為你自己學 Git】合併發生衝突了,怎麼辦? 【Git 官方文件】git stash 介紹 【Git 官方文件】git reset 介紹","link":"/Other/Git/Using-Git-Rebase-to-Organize-Commit-History/"},{"title":"【PHP】五種實用的日期時間函數","text":"本篇重點 time() 取得當前時間戳記 date() 格式化時間 strtotime() 將字串轉換為時間戳記 mktime() 建立時間戳記 checkdate() 驗證日期是否有效 如果想把日期時間換成時間戳或是時間戳換成日期時間,歡迎使用本站的 timestamp 轉換工具 time() - 取得當前時間戳記回傳從 Unix 紀元(1970-01-01 00:00:00 UTC)至今的秒數。 範例: php123// 輸出當前的 Unix 時間戳記$timestamp = time();echo "目前的 Unix 時間戳記:" . $timestamp; 動手做做看 time() - 取得當前時間戳記 回傳從 Unix 紀元(1970-01-01 00:00:00 UTC)至今的秒數。 動手做做看 date() - 格式化時間可用來格式化時間戳記,讓時間以可讀的方式顯示。 實例搶先看 date() - 格式化時間 可用來格式化時間戳記,讓時間以可讀的方式顯示。 實例搶先看 php12// 預設為輸出當下的格式化時間,可指定需格式化的時間點date(string $format, int $timestamp = time()) 常用格式: 格式 說明 範例 Y 完整四位數西元年份,使用 - 表示公元前 2025、-0055、0787 m 兩位數月份,沒有十位數會補零 02、07、12 d 兩位數日期,沒有十位數會補零 14、25、06 H 24 小時制兩位數小時,沒有十位數會補零 00 到 23 i 分鐘,沒有十位數會補零 00 到 59 s 秒,沒有十位數會補零 00 到 59 w 星期幾,數字表示 0(表示星期天) 到 6(表示星期六) 健忘筆記 PHP 的 date('w') 取得星期幾是從 0 到 6 進行編號,0 表示 星期日、1 表示 星期一以此類推。 *JavaScript 的 .getDay() 也是如此,想了解 .getDay() 可以查看 【Tool】Unix 時間戳(Unix timestamp) 轉換 範例:php123456// 以 YYYY-MM-DD HH:MM:SS 格式輸出當下時間echo date('Y-m-d H:i:s');// 以 YYYY/MM/DD HH:MM:SS 格式輸出時間戳 1739690772echo date('Y/m/d H:i:s', 1739690772);// 2025/02/16 15:26:12 查看所有格式設定 動手做做看 動手做做看 strtotime() - 將字串轉換為時間戳記將各種時間字串(如 yesterday、+1 week)轉換為 Unix 時間戳。 健忘筆記 如果執行日期運算的環境沒有設定自己的時區或是想指定時區,可以使用 date_default_timezone_set() 調整。 實例搶先看 strtotime() - 將字串轉換為時間戳記 1. 將各種時間字串(如 yesterday、+1 week)轉換為 Unix 時間戳 2. 如果執行日期運算的環境沒有設定自己的時區或是想指定時區,可以使用 date_default_timezone_set() 調整。 3. 具體的日期時間取得時間戳不受 strtotime() 的第二個參數 $baseTimestamp 影響 4. 相對時間的時間字串會受 strtotime() 的第二個參數 $baseTimestamp 影響 實例搶先看 php12// 預設基礎時間點為當下的時間戳,可指定基礎時間點strtotime(string $datetime, int $baseTimestamp = time()) 常見日期時間字串應用:絕對時間(Absolute Dates)具體的日期時間,這個類型的關鍵字會忽略 strtotime() 的第二個參數 $baseTimestamp 時間字串 解析時間(假設今天是 2025-02-14) 2025-01-01 2025-01-01 00:00:00 2025-04-31 08:11 2025-04-31 08:11:00 2025-12-06 noon 2025-12-06 12:00:00 範例: php1234567891011// 取得 2025-01-01 的時間戳echo strtotime('2025-01-01');// 1735660800 - 2025-01-01 00:00:00// 取得 2025-04-31 08:11 的時間戳echo strtotime('2025-04-31 08:11');// 1746058260 - 2025-05-01 08:11:00// 取得 2025-12-06 12:00:00 的時間戳 (不受 1740047219 影響)echo strtotime('2025-12-06 noon', 1740047219);// 1764993600 - 2025-12-06 12:00:00 健忘筆記 具體的日期時間取得時間戳不受 strtotime() 的第二個參數 $baseTimestamp 影響 相對時間(Relative Dates)基於 $baseTimestamp 進行計算,預設為當下的時間(time()) 時間字串 解析時間(假設現在是 2025-02-14 15:30:45) +1 day 2025-02-15 15:30:45 -1 day 2025-02-13 15:30:45 +2 weeks 2025-02-28 15:30:45 -3 months 2024-11-14 15:30:45 +5 years 2030-02-14 15:30:45 today 2025-02-14 00:00:00 yesterday 2025-02-13 00:00:00 tomorrow 2025-02-15 00:00:00 now 2025-02-14 15:30:45 midnight 2025-02-14 00:00:00 noon 2025-02-14 12:00:00 範例: php123456789101112131415161718192021// 假設現在是 2025-02-21 17:54:45// 取得當下時間兩年後前一天的時間戳echo strtotime('+2 years -1 day');// 1803117258 - 2027-02-20 17:54:18// 取得明天的 00:00:00 的時間戳echo strtotime('tomorrow');// 1740153600 - 2025-02-22 00:00:00// 以 1740047219 (2025/02/20 18:26:59) 時間戳為基準點,取得兩年又兩星期後的時間戳echo strtotime('+2 weeks +1 years', 1740047219);// 1772792819 - 2026-03-06 18:26:59// 以 1737368819 (2025/01/20 18:26:59) 時間戳為基準點,取得四天後正中午的時間戳echo strtotime('+4 days noon', 1737368819);// 1737691200 - 2025-01-24 12:00:00// 以 1739960316 (2025/02/19 18:18:36) 時間戳為基準點,取得當天 00:00:00 的時間戳echo strtotime('today', 1739960316)// 1739894400 - 2025-02-19 00:00:00 健忘筆記 相對時間的時間字串會受 strtotime() 的第二個參數 $baseTimestamp 影響 查看所有可解析的日期時間字串 動手做做看 動手做做看 mktime() - 建立時間戳記建立特定時間的 Unix 時間戳。 健忘筆記 如果執行日期運算的環境沒有設定自己的時區或是想指定時區,可以使用 date_default_timezone_set() 調整。 php1mktime(int $hour, int $minute, int $second, int $month, int $day, int $year) 範例: php123456789// 取得 2025-02-14 00:00:00 的時間戳echo mktime(0, 0, 0, 2, 14, 2025);// 1739462400// 取得當前日期 20 天後 12:00:00 的時間戳echo mktime(12, 0, 0, date('m'), date('d') + 20, date('Y'));// 取得當前日期 3 個月前 14:03:00 的時間戳echo mktime(14, 3, 0, date('m') - 3, date('d'), date('Y')); mktime() - 建立時間戳記 建立特定時間的 Unix 時間戳。 動手做做看 動手做做看 checkdate() - 驗證日期是否有效檢查一個日期是否有效,回傳布林值(boolean)。 php123456789101112131415// 2024-02-29 是否為有效日期var_dump(checkdate(2, 29, 2024));// true,閏年 2024,2 月有 29 天// 2024-02-30 是否為有效日期var_dump(checkdate(2, 30, 2024));// false,2 月最多 29 天// 2023-04-31 是否為有效日期var_dump(checkdate(4, 31, 2023));// false,4 月只有 30 天// 2023-12-25 是否為有效日期var_dump(checkdate(12, 25, 2023));// true checkdate() - 驗證日期是否有效 檢查一個日期是否有效,回傳布林值(boolean)。 動手做做看 動手做做看 結論這邊整理了我常用的的日期時間函數,後端記錄資料的時候很常需要日期時間、timestamp 的轉換,或是執行排程可能也會需要計算時間來撈取資料等。 time() - 取得當前時間戳記 date() - 格式化時間 strtotime() - 將字串轉換為時間戳記 mktime() - 建立時間戳記 checkdate() - 驗證日期是否有效 平常也會使用日期時間函數嗎?有甚麼更方便的函數嗎?歡迎留言跟大家分享! 如果想把日期時間換成時間戳或是時間戳換成日期時間,歡迎使用本站的 timestamp 轉換工具 延伸閱讀 【PHP官方文件】日期時間格式介紹 【PHP官方文件】可解析的日期時間字串 【PHP官方文件】Date/Time 函数介紹","link":"/Backend/PHP/Five-Useful-Date-and-Time-Functions/"},{"title":"【MySQL】DQL 指令的執行順序與效能優化","text":"本篇重點 了解 DQL 指令的邏輯執行順序以及實際執行順序 每個步驟會產生一張虛擬表供下一步驟使用,但中間結果是不可見的,使用者只能取得最終查詢結果 各個 DQL 指令的使用重點 MySQL 實際運作時,可能會有查詢優化器影響執行順序 必須使用 EXPLAIN 指令才有辦法查看執行順序,沒辦法單純依靠邏輯順序推斷 在 MySQL 查詢語言(DQL,Data Query Language)中,每條 SELECT 查詢語句包含多個不同的指令。了解執行順序,能幫助理解查詢如何處理,並且優化 SQL 查詢,減少不必要的篩選與計算,提高效能。 想了解其他資料庫語言類型,可以參考 【MySQL】解析資料庫語言類型 DDL、DML、DCL、DQL DQL 指令的執行順序SQL 查詢通常按照 SELECT … FROM … WHERE … GROUP BY … HAVING … ORDER BY … LIMIT … 的語法順序撰寫,但這只是是語法結構上的安排,並非資料實際被處理的先後順序。 邏輯執行順序資料庫引擎依照以下邏輯步驟處理查詢: FROM / JOIN / ON:鎖定資料來源,進行資料表連接。 WHERE:對原始資料進行列(Row)層級的過濾。 GROUP BY:將資料集依照指定欄位進行分組。 AGG_FUNC:執行聚合函數計算(如 SUM、COUNT)。 HAVING:對分組後的數據進行過濾。 SELECT:選取顯示的欄位或計算表達式。 DISTINCT:去除重複的資料列。 ORDER BY:對結果集進行排序。 LIMIT / OFFSET:限制回傳的筆數與偏移量。 此邏輯順序用來說明查詢語意如何逐步運作,例如資料來源的組合、條件過濾、分組、聚合與最終輸出結果。每個步驟都會產生一個中間結果(可視為虛擬表),再交由下一個步驟處理,但這些中間結果在實際執行中是不可見的,使用者只能取得最終查詢結果。 健忘筆記 邏輯順序定義了查詢的語意與結果正確性。但 MySQL 實際運作時,會有查詢優化器(Query Optimizer)根據索引、統計資訊重新編排執行順序以提升效能(例如:將 WHERE 條件提前執行以減少 JOIN 的資料量),因此如果想知道 MySQL 查詢的實際執行順序,必須使用 EXPLAIN 指令查看執行計畫(Execution Plan),沒辦法單純依靠邏輯順序推斷。 範例資料mysql1234567891011121314151617181920212223242526-- 創建資料表CREATE TABLE users (uid INT AUTO_INCREMENT PRIMARY KEY,name VARCHAR(50),age VARCHAR(50),team VARCHAR(50),email VARCHAR(100));CREATE TABLE orders (order_id INT AUTO_INCREMENT PRIMARY KEY,uid INT,commodity_id INT,price INT);-- 新增資料INSERT INTO users (name, age, team, email) VALUES ('John', 17, 'a', 'john@example.com');INSERT INTO users (name, age, team, email) VALUES ('tom', 22, 'b', 'tom@example.com');INSERT INTO users (name, age, team, email) VALUES ('cindy', 35, 'a', 'cindy@example.com');INSERT INTO users (name, age, team, email) VALUES ('tom', 30, 'a', 'tom2@example.com');INSERT INTO orders (uid, commodity_id, price) VALUES (2, 5, 50);INSERT INTO orders (uid, commodity_id, price) VALUES (2, 8, 30);INSERT INTO orders (uid, commodity_id, price) VALUES (3, 1, 100);INSERT INTO orders (uid, commodity_id, price) VALUES (4, 16, 50); (1) FROMmysql1SELECT [欄位名稱] FROM [表格名稱]; 來源資料表的讀取 範例: mysql123SELECT * FROM users;SELECT * FROM orders; (1) FROM 來源資料表的讀取 動手做做看 動手做做看 (2) JOIN … ONmysql12SELECT [欄位名稱] FROM [表格1]JOIN [表格2] ON [表格1].[欄位] = [表格2].[欄位]; 當有 JOIN 操作時,MySQL 會先依據 ON 條件來篩選需要 JOIN 的資料,這有助於減少不必要的數據傳輸,提升查詢效能 JOIN … ON 比 WHERE 更早執行,因此 ON 條件能過濾掉不必要的資料,提高效能 JOIN 還有分 LEFT JOIN、RIGHT JOIN,可以參考 【w3schools】SQL Joins 範例: mysql123SELECT u.uid, u.name, o.order_id, o.commodity_id, o.priceFROM users uJOIN orders o ON u.uid = o.uid AND o.price >= 50; -- 在 JOIN 的時候先做篩選 (2) JOIN … ON 當有 JOIN 操作時,MySQL 會先依據 ON 條件來篩選需要 JOIN 的資料,這有助於減少不必要的數據傳輸,提升查詢效能 這比 WHERE 更早執行,因此 ON 條件能過濾掉不必要的資料,提高效能 動手做做看 動手做做看 (3) WHEREmysql1SELECT [欄位名稱] FROM [表格名稱] WHERE [篩選條件]; 在 FROM 抓取完表之後,會根據 WHERE 條件來篩選符合條件的資料 WHERE 會在 GROUP BY 之前執行,因此 WHERE 不能對聚合函數 (SUM、COUNT) 進行篩選 範例: mysql12345678910SELECT u.uid, u.name, o.order_id, o.commodity_id, o.priceFROM users uJOIN orders o ON u.uid = o.uidWHERE u.uid = 2;-- 因為 JOIN … ON 比 WHERE 更早執行,所以執行 WHERE u.uid = 2 的時候,已經只剩 price >= 50 的資料SELECT u.uid, u.name, o.order_id, o.commodity_id, o.priceFROM users uJOIN orders o ON u.uid = o.uid AND o.price >= 50WHERE u.uid = 2; (3) WHERE 在 FROM 抓取完表之後,會根據 WHERE 條件來篩選符合條件的資料 WHERE 會在 GROUP BY 之前執行,因此 WHERE 不能對聚合函數 (SUM、COUNT) 進行篩選 動手做做看 動手做做看 (4) GROUP BYmysql12SELECT [分組欄位], [聚合函數] FROM [表格名稱]GROUP BY [分組欄位]; 將符合 WHERE 條件的資料分組 GROUP BY 後只能 SELECT 分組的欄位或聚合函數 (如 SUM、COUNT、AVG),如果 SELECT 分組以外的欄位會是同一分組中任意一行的值,因此可能會與預期結果不符 範例: 實例搶先看 mysql123456789SELECT teamFROM usersGROUP BY team;-- 因為 WHERE 比 GROUP BY 更早執行,所以執行 GROUP BY team 的時候,已經只剩 age >= 30 的資料SELECT teamFROM usersWHERE age >= 30GROUP BY team; 在查詢每個群組中的最新或特定條件的資料時,可以使用 GROUP BY 搭配聚合函數來篩選出所需的關鍵值(例如最新的 ID 或日期),再透過 JOIN 將篩選出的關鍵值與原表進行關聯,確保獲取完整的詳細資訊。這種方式適用於各種情境,例如查詢每位使用者的最新留言、每個產品的最高價格紀錄、每個客戶的最近一次購買紀錄等。 範例: 查詢每個 uid 的最新訂單可以使用 GROUP BY 搭配 MAX() 先找出 orders 表中每個 uid 最新的 order_id(假設 order_id 越大代表越新) 然後 JOIN 回 orders 表,獲取完整的訂單資訊 mysql12345678SELECT o.*FROM orders oJOIN ( SELECT uid, MAX(order_id) AS latest_order FROM orders GROUP BY uid) latest_ordersON o.uid = latest_orders.uid AND o.order_id = latest_orders.latest_order; (4) GROUP BY 將符合 WHERE 條件的資料分組 GROUP BY 後只能 SELECT 分組的欄位或聚合函數 (如 SUM、COUNT、AVG),如果 SELECT 分組以外的欄位會是同一分組中任意一行的值,因此可能會與預期結果不符 動手做做看 動手做做看 (5) AGG_FUNCmysql12SELECT [分組欄位], [聚合函數]([欄位]) FROM [表格名稱]GROUP BY [分組欄位]; 聚合函數計算(如 SUM、AVG、COUNT) 搭配 GROUP BY 可針對每組資料進行聚合計算 如果沒有 GROUP BY,是將整張資料表當作一組來計算 可使用 AS 為結果欄位命名,增加可讀性 範例: mysql1234567891011121314SELECT team, COUNT(*) AS team_count, AVG(age) AS age_avgFROM usersGROUP BY team;-- 因為 WHERE 比 GROUP BY 更早執行,所以執行 GROUP BY team 的時候,已經只剩 age >= 30 的資料SELECT team, COUNT(*) AS team_count, AVG(age) AS age_avgFROM usersWHERE age >= 30GROUP BY team;-- age >= 30 的使用者總數SELECT COUNT(*) AS user_countFROM usersWHERE age >= 30; (5) AGG_FUNC 聚合函數計算(如 SUM、AVG、COUNT) GROUP BY 分組後,才能使用聚合函數 可以使用 AS 將欄位重新命名 動手做做看 動手做做看 (6) HAVINGmysql123SELECT [分組欄位], [聚合函數]([欄位]) FROM [表格名稱]GROUP BY [分組欄位]HAVING [條件]; 針對聚合函數的結果篩選 範例: mysql12345-- 針對聚合函數的結果篩選 team_count > 1 的資料SELECT team, COUNT(*) AS team_countFROM usersGROUP BY teamHAVING team_count > 1; (6) HAVING 針對聚合函數的結果篩選 動手做做看 動手做做看 (7) SELECTmysql1SELECT [欄位名稱] FROM [表格名稱]; 選擇要顯示的欄位(* 為選擇所有欄位) 所有條件已經套用,查詢結果已經確定 範例: mysql123456789101112131415SELECT * FROM users;SELECT * FROM orders;SELECT u.uid, u.name, o.order_id, o.commodity_id, o.priceFROM users uJOIN orders o ON u.uid = o.uid AND o.price >= 50;SELECT u.uid, u.name, o.order_id, o.commodity_id, o.priceFROM users uJOIN orders o ON u.uid = o.uidWHERE u.uid = 2;SELECT team, COUNT(*) AS team_count, AVG(age) AS age_avgFROM usersGROUP BY team; (7) SELECT 選擇要顯示的欄位(* 為選擇所有欄位) 所有條件已經套用,查詢結果已經確定 動手做做看 動手做做看 (8) DISTINCTmysql1SELECT DISTINCT [欄位名稱] FROM [表格名稱]; 和 SELECT 搭配使用,在 SELECT 完成後過濾掉重複的資料 DISTINCT 是對整個查詢結果去除重複資料 DISTINCT 用於多個欄位是根據這些欄位的組合值來判斷是否重複,不是單獨判斷某一欄位 範例: mysql123456SELECT DISTINCT uid FROM orders;SELECT DISTINCT name FROM users;-- 根據多欄位去除重複項SELECT DISTINCT name, age, email FROM users; (8) DISTINCT 和 SELECT 搭配使用,在 SELECT 完成後過濾掉重複的資料 DISTINCT 用於多個欄位是根據這些欄位的組合值來判斷是否重複,不是單獨判斷某一欄位 DISTINCT 是對整個查詢結果去除重複資料 動手做做看 動手做做看 (9) ORDER BYmysql12SELECT [欄位名稱] FROM [表格名稱]ORDER BY [欄位名稱] [ASC|DESC]; 在 SELECT 完成後,結果會按照 ORDER BY 進行排序 未指定排序,預設為 ASC 升冪,順序由小排到大,使用 DESC 為降冪,順序由大排到小 排序可能會影響效能,特別是在大量數據時,要確保排序欄位有索引 範例: mysql12345SELECT * FROM orders ORDER BY price;SELECT * FROM users ORDER BY team ASC;SELECT * FROM users ORDER BY age DESC; (9) ORDER BY 在 SELECT 完成後,結果會按照 ORDER BY 進行排序 未指定排序,預設為升冪 ASC,順序由小排到大,使用 DESC 為降冪,順序由大排到小 排序可能會影響效能,特別是在大量數據時,要確保排序欄位有索引 動手做做看 動手做做看 (10) LIMIT / OFFSETmysql12SELECT [欄位名稱] FROM [表格名稱]LIMIT [筆數] OFFSET [跳過筆數]; 取出特定範圍的資料 LIMIT 用於限制返回的筆數,如果 LIMIT 使用兩個參數,第一個參數為資料跳過的筆數,第二個參數為跳過後選取的筆數 OFFSET 用於跳過前幾筆資料,不能單獨使用,必須與 LIMIT 搭配使用 健忘筆記 LIMIT 使用兩個參數,例如 LIMIT 20, 10,解讀為跳過 20 筆記錄後,再取 10 筆記錄 範例: mysql123456SELECT * FROM users LIMIT 3;-- 以下兩個查詢的結果相同SELECT * FROM users LIMIT 1, 2;SELECT * FROM users LIMIT 2 OFFSET 1; (10) OFFSET / LIMIT 取出特定範圍的資料 LIMIT 用於限制返回的筆數,如果 LIMIT 使用兩個參數,第一個參數為資料跳過的筆數,第二個參數為跳過後選取的筆數 OFFSET 用於跳過前幾筆資料,不能單獨使用,必須與 LIMIT 搭配使用 動手做做看 動手做做看 提升查詢效能的方式建立適當的索引 WHERE、JOIN、ORDER BY 相關的欄位應建立索引,減少資料庫掃描時間。 例如: mysql12-- 對 users 表的 age 欄位建立索引CREATE INDEX idx_users_age ON users(age); 先篩選再分組 WHERE 會在 GROUP BY 之前執行,因此應確保 WHERE 先過濾掉不必要的數據,減少 GROUP BY 的負擔。 避免 SELECT *,只選取需要的欄位 只選擇必要的欄位可以減少 I/O 負擔,提高查詢效能。 結論了解 DQL 指令的執行順序能編寫更高效的 SQL 查詢。透過優化 WHERE 篩選順序、善用索引、避免 SELECT * 等方式,可以顯著提升查詢效能。","link":"/Backend/MySQL/DQL-Execution-Order/"},{"title":"【Html】自定義屬性:data-* 的命名規則和取值方法","text":"本篇重點 屬性必須以 data- 為前綴 建議使用 kebab-case 命名 data-* 的常見應用時機 JavaScript 使用 dataset. 取值 jQuery 使用 .data() 或是 .attr() 取值 jQuery 的 .data() 和 .attr() 不可混用 HTML5 引入了 data-* 屬性,讓開發者可以在標籤上自訂屬性,儲存額外的資訊,並能夠被 JavaScript 獲取與操作。 命名規則html1<div id="example" data-user-id="12345" data-role="admin" data-info='{"name": "Alice", "age": 25}'></div> 必須以 data- 為前綴 data- 是固定的屬性前綴,用於告訴瀏覽器這是一個自定義數據屬性。 後續名稱只能包含 小寫字母(a-z) 數字(0-9) 中橫線(-) 建議使用「kebab-case(短橫線命名法)」,例如 data-user-id,避免使用 data_user_id 或 dataUserId,這可能會影響取值的操作 data-* 屬性值可以是 字串 數字 JSON 格式 常見應用時機 前端與後端溝通的小型數據 html1<li data-product-id="101" data-price="25.99">商品名稱</li> 控制前端行為 html1<button data-toggle="modal">開啟模態窗</button> 統計與分析 html1<a href="example.html" data-track="nav-link">點擊我</a> 讀取 data-* 屬性的方法Javascript使用 dataset 物件讀取 data-* 屬性: dataset 會將 data-* 的 kebab-case 轉換為 camelCase(駝峰式命名法),例如 data-user-id 使用 dataset.userId 讀取 如果屬性名稱包含大寫(不建議這樣做),JavaScript 仍然會轉為小寫處理。例如 data-User-Name 使用 dataset.userName 讀取 實例搶先看 Javascript 讀取 data 屬性的方法 dataset 會將 data-* 的 kebab-case 轉換為 camelCase(駝峰式命名法),例如 data-user-id 變成 dataset.userId 如果屬性名稱包含大寫(不建議這樣做),JavaScript 仍然會轉為小寫處理。例如 data-User-Name 使用 dataset.userName 讀取 html12345678<div id="example" data-user-id="12345" data-User-Name="Alice" data-role="admin" data-info='{"name": "Alice", "age": 25}'></div><p id="output"></p> js123456789101112131415const div = document.getElementById("example");const output = document.getElementById("output");// 取得 data-* 屬性值const userId = div.dataset.userId;const userName = div.dataset.userName;const role = div.dataset.role;const info = JSON.parse(div.dataset.info); // 解析 JSON 字串output.innerHTML = ` 使用者 ID: ${userId} <br> 使用者名稱: ${userName} <br> 角色: ${role} <br> 額外資訊: 名字 - ${info.name}, 年齡 - ${info.age}`; 健忘筆記 駝峰式命名(Camel case)是一種將單字相連的命名方式,並根據開頭字母的大小寫進一步細分為兩種類型。小駝峰命名(lower camel case)的開頭字母為小寫,例如 userId,而大駝峰命名(upper camel case)的開頭字母則為大寫,例如 UserId。 烤肉串命名(Kebab case)是一種使用 - 來分隔單字的命名方式,例如:user-id。這種命名方式常見於網頁開發中,特別是在 CSS 類別名稱與部分 URL 結構中。 JQuery可以使用 .data() 或是 attr() 存取 data-* 屬性: 使用 .data() 讀取 data-*,無論屬性名稱使用 kebab-case 或 camelCase,都能正確獲取對應的值 如果 data-* 的值是有效的 JSON,且在 HTML 載入時已經存在,.data() 會自動將其解析為 JavaScript 物件 實例搶先看 JQuery 讀取 data 屬性的方法 使用 .data() 讀取 data-*,無論屬性名稱使用 kebab-case 或 camelCase,都能正確獲取對應的值 如果 data-* 的值是有效的 JSON,且在 HTML 載入時已經存在,.data() 會自動將其解析為 JavaScript 物件 html123456789<div id="example" data-user-id="12345" data-User-Name="Alice" data-role="admin" data-info='{"name": "Alice", "age": 25}'></div><p id="output"></p> js1234567891011121314151617const div = $("#example");const output = $("#output");// 取得 data-* 屬性值const userId = div.data('user-id'); // kebab-case 也可讀取const userId2 = div.data('userId');const userName = div.data('userName');const role = div.attr('data-role');const info = div.data('info');$("#output").html(` 使用者 ID: ${userId} <br> 使用者 ID 2: ${userId2} <br> 使用者名稱: ${userName} <br> 角色: ${role} <br> 額外資訊: 名字 - ${info.name}, 年齡 - ${info.age}`); .data() 和 .attr() 不可混用可以使用 .data() 或 .attr() 存取 data-* 屬性,但兩者的機制不同,賦值與取值不可混用。 .data() 存取的值會被緩存在 jQuery 物件內部,不會即時反映到 DOM .attr() 存取是直接對應 HTML 屬性本身 若使用 .attr() 設定屬性後,再用 .data() 取值,會得到 undefined,因為 .data() 只會讀取在 DOM 初始載入時已存在的屬性資料。 實例搶先看 .data() 和 .attr() 不可混用 .data() 存取的值會被緩存在 jQuery 物件內部,不會即時反映到 DOM .attr() 存取是直接對應 HTML 屬性本身 html12<div id="example" data-user-id="12345"></div><p id="output"></p> js12345678910111213141516171819202122const div = $("#example");const output = $("#output");// 初始 data-* 屬性值const initialData = div.data("userId");// 使用 .attr() 新增屬性div.attr("data-userName", "Alice");// 嘗試以 .data() 讀取剛新增的屬性const mixedRead = div.data("userName"); // undefined// 改用 .attr() 讀取即可獲得正確值const attrRead = div.attr("data-userName");// 輸出結果output.html(` 初始 data-user-id(使用 .data 讀取):${initialData}<br> 使用 .attr() 設定 data-userName="Alice"<br> 使用 .data("userName") 取值:${mixedRead}<br> 使用 .attr("data-userName") 取值:${attrRead}`); 結論HTML 的 data-* 屬性是一種靈活且易用的方式,用於存儲自定義數據,方便前端操作,但有幾個需要注意的地方: 避免存放敏感信息data-* 屬性數據是公開的,任何人都可以通過檢視原始碼或瀏覽器開發工具查看,避免存放密碼、API 金鑰等敏感資訊。 適度使用data-* 屬性適合存放小型且靜態的數據。如果數據過於龐大或動態,建議改用其他方式,例如 API 或 JavaScript 物件。 了解後能夠大幅提升程式碼的可讀性與維護性! 延伸閱讀 【MDN】data-* 介紹 【MDN】dataset 介紹 【jQuery 官方文件】.data() 介紹 【Camel ‘s blog】常見的命名規則","link":"/Frontend/Html/Data-Attribute-Naming-Conventions-and-Retrieval-Methods/"},{"title":"【HTML】解決 Lazy loading 導致滾動定位不準的問題","text":"本篇重點 解決 Lazy loading 導致 .scrollIntoView() 滾動定位不準的問題 檢查目標元素是否進入視窗的方式 .getBoundingClientRect()、window.innerHeight、.clientHeight 的應用 在開發「點擊回覆文章按鈕後自動滾動至留言區」的功能時,原本使用 .scrollIntoView({ behavior: 'smooth' }) 來實現平滑滾動的效果。但在實際測試時發現,滾動的位置沒有準確對齊留言區。檢查後發現,因為頁面中的圖片使用了 loading="lazy" 機制,導致圖片尚未載入時不會佔據實際高度,進而影響瀏覽器對滾動位置的計算,造成滾動偏移的問題。 js1234$('#reply').click(function scrollToComment() { const commentSection = document.getElementById("comment"); commentSection.scrollIntoView({ behavior: 'smooth' });}); 動手做做看 Lazy loading 導致滾動定位不準的問題 動手做做看 健忘筆記 以前都是使用套件來實現 Lazy loading 機制,現在 HTML 已經內建此機制,詳細可查看 【MDN】Lazy loading 介紹 解決方法檢查目標是否進入視窗在滾動後使用 setTimeout,並檢查目標元素是否進入視窗。如果未進入視窗範圍,則重新執行滾動,確保用戶能夠正確移動到留言區。 js123456789101112131415161718$('#reply').click(function scrollToComment() { const commentSection = document.getElementById("comment"); commentSection.scrollIntoView({ behavior: 'smooth' }); setTimeout(() => { if (!isElementInViewport(commentSection)) { scrollToComment(); } }, 300);});function isElementInViewport(el) { const rect = el.getBoundingClientRect(); return ( rect.top >= 0 && rect.top <= (window.innerHeight || document.documentElement.clientHeight) );} 初次滾動:scrollIntoView({ behavior: 'smooth' }) 觸發滾動效果。 延遲檢查:使用 setTimeout 延遲 300ms,讓瀏覽器有時間載入 Lazy Load 圖片並重新計算頁面高度。 可視範圍判斷:透過 isElementInViewport() 檢查 comment 是否確實進入視窗範圍。 二次滾動:如果 comment 仍未進入視窗,則再次執行 scrollToComment(),確保滾動到正確位置。 動手做做看 Lazy loading 導致滾動定位不準的問題 動手做做看 健忘筆記 .getBoundingClientRect():取得某個元素相對於視窗的位置和元素大小(單位:px),可用來判斷元素是否出現在畫面中 top:元素頂部與視窗頂部的距離 bottom:元素底部與視窗頂部的距離 left / right:元素左右邊與視窗邊界的距離 width / height:元素的寬度與高度 window.innerHeight:取得目前瀏覽器「視窗可視區域」的高度(單位:像素 px),包括了內容區域 + 捲軸的高度(如果有捲軸),無論有沒有捲動網頁值都不會變 document.documentElement.clientHeight:取得目前瀏覽器「視窗可視區域」的高度(單位:像素 px),和 window.innerHeight 最大的差異是不包含捲軸的厚度及相容性較好 固定圖片的高度固定圖片的高度後,瀏覽器在計算滾動位置能提前保留圖片所佔的空間,避免圖片載入後高度變化影響整體排版和捲動位置,進而解決滑動定位不準確的問題。但這個方法也有問題存在,若網站中的圖片尺寸不一(例如橫圖、直圖混排),全部統一設定固定高度可能造成畫面留白、不均衡,影響整體版面;若在 RWD(響應式網頁設計)中,圖片的顯示尺寸通常會根據螢幕寬度自動調整,強制設定固定高度(例如 height: 300px),會讓圖片失去原有比例,產生壓縮或拉伸的問題。 結論我目前是用 setTimeout 搭配檢查目標元素是否進入視窗的方式,來解決 Lazy loading 導致滾動定位不準的問題。雖然這方法暫時能用,但感覺不是最佳解😅 如果有更聰明、簡單、穩定的解法,麻煩一定要留言分享給我!🙏🏻 延伸閱讀 【MDN】Lazy loading 介紹 【MDN】getBoundingClientRect() 介紹 【MDN】window.innerHeight 介紹 【MDN】.clientHeight 介紹","link":"/Frontend/Html/Fixing-Scroll-Effect-Misalignment-Caused-by-Lazy-Loading/"},{"title":"【PHP、JavaScript】三種實用的符號運算子 ?:、??、??=","text":"本篇重點 介紹 ?: 、??、??= 的使用方式 JavaScript 沒有 ?: 符號相鄰的簡化用法,但可以使用 || 來達到類似效果 PHP 和 JavaScript 判斷假值標準不完全相同 了解符號運算子的可使用版本 先了解這些會幫助你更快理解唷😁 【PHP】4種判斷變數或陣列是否存在的方法【PHP】解析邏輯運算子 and、or、&&、|| 之間的差異 在開發中,常常會遇到要檢查變數是否有值、是否被設定的情況。為了讓程式碼更簡潔,PHP 和 JavaScript 提供了三種「簡化條件運算子」: ?:(三元運算子) ??(Null 合併運算子) ??=(Null 合併賦值運算子) ?: 三元運算子PHP常見用法(PHP 4+)?: 是條件運算子(if...else...)的簡寫 php1條件式 ? 成立時的值 : 不成立時的值; 範例: 動手做做看 ?: 三元運算子 常見用法:使用 ?: 符號間隔達到 if-else 的效果 簡化用法:當「條件」與「成立時的值」相同時可省略重複語句 動手做做看 php1$age = isset($_POST['age']) && intval($_POST['age']) > 0 ? intval($_POST['age']) : 0; 如果使用者有傳入 age,且值大於 0,就使用該值 否則預設為 0 簡化用法(PHP 5.3+)當「條件」與「成立時的值」相同時可省略重複語句 php1條件式 ?: 不成立時的值; 範例: 動手做做看 動手做做看 php123456789// $nickname 未設定值$nickname = '';$name = $nickname ?: '訪客';echo $name; // 訪客// $nickname 有設定值$nickname = 'John';$name = $nickname ?: '訪客';echo $name; // John 如果 $nickname 的值存在,返回 $nickname 如果 $nickname 的值為假值(falsy,如空字串、0、null、false),返回後方的 '訪客' 等同於: php1$name = $nickname ? $nickname : '訪客' 判斷條件是否成立、是否為假值,可參考 【PHP】4種判斷變數或陣列是否存在的方法 判斷差異表 if ($x) 的那一行 JavaScript?: 是條件運算子(if...else...)的簡寫 js1條件式 ? 成立時的值 : 不成立時的值; 範例: js1234567let inputAge = null;let age = inputAge > 0 ? inputAge : 0;console.log(age); // 0inputAge = 10;age = inputAge > 0 ? inputAge : 0;console.log(age); // 10 如果使用者有傳入 age,且值大於 0,就使用該值 否則預設為 0 健忘筆記 JavaScript 沒有 ?: 符號相鄰的簡化用法,但可以使用 || 來達到類似效果 let nickname = inputNickname || '訪客'; 不過 JavaScript 假值的定義和 PHP 不完全相同,詳情可見【MDN】JavaScript 假值介紹 動手做做看 ?? Null 合併運算子PHP( 7+)用來檢查變數是否「被設定」且「不為 null」,如果條件不符,則使用後方預設值,和 isset() 的判斷結果相同 php1$變數 ?? $預設值; 範例: 動手做做看 ?? Null 合併運算子 用來檢查變數是否有「設定」,且不是 null,和 isset() 的判斷結果相同 動手做做看 php12$user = $_GET['user_name'] ?? 'noName';echo $user; 如果 $_GET['user_name'] 存在且不為 null,就返回 $_GET['user_name'] 否則返回 'noName' 等同於: php1$user = isset($_GET['user']) ? $_GET['user'] : '預設使用者'; 判斷是否存在且不為 null,可參考 【PHP】4種判斷變數或陣列是否存在的方法 判斷差異表 isset($x) 的那一行 ?: 和 ?? 的差異比較?: 和 ?? 兩者都有檢查變數是否有值、是否被設定的功用,但判斷標準不完全相同 php12345678$value1 = 0 ?: 100; // 100 (因為 0 是假值)$value2 = 0 ?? 100; // 0 (因為 0 不是 null)$value1 = '' ?: 100; // 100 (因為 '' 是假值)$value2 = '' ?? 100; // '' (因為 '' 不是 null)$value1 = null ?: 100; // 100 (因為 null 是假值)$value2 = null ?? 100; // 100 (因為是 null) 動手做做看 ?: 和 ?? 的差異比較 ?: 和 ?? 兩者都有檢查變數是否有值、是否被設定的功用,但判斷標準不完全相同 動手做做看 JavaScript(ES2020+)用來檢查變數是否「被設定」且「不為 null 或 undefined」,如果條件不符,則使用後方預設值 js1$變數 ?? $預設值; 範例: js1234567let inputUserName = null;let user = inputUserName ?? "noName";console.log(user); // noNameinputUserName = "John";user = inputUserName ?? "noName";console.log(user); // John 如果 inputUserName 存在且不為 null 或 undefined,就返回 inputUserName 否則返回 'noName' 動手做做看 ??= Null 合併賦值運算子PHP(7.4+)?? 的延伸寫法,如果變數未設定或為 null 自動賦予預設值,否則賦予原值 php1$變數 ??= $預設值; 範例: php12$_GET['user_name'] ??= 'noName';echo $_GET['user_name']; 如果 $_GET['user_name'] 未設定或為 null,就賦值為 'noName' 如果已經有設定值(例如 'John'),則不做任何改變 等同於: php123if (!isset($_GET['user_name'])) { $_GET['user_name'] = 'noName';} 動手做做看 ??= Null 合併賦值運算子 ?? 的進階寫法,若變數未設定或為 null,進行賦值,否則保留原本的值 動手做做看 JavaScript(ES2021+)?? 的延伸寫法,如果變數為 null 或 undefined 自動賦予預設值,否則賦予原值 js1$變數 ??= $預設值; 範例: js1234567let user_name = null;user_name ??= "noName";console.log(user_name);user_name = "John";user_name ??= "noName";console.log(user_name); 如果 user_name 為 null 或 undefined,就賦值為 'noName' 如果已經有設定值(例如 'John'),則不做修改 動手做做看 結論在 PHP 和 JavaScript 中,可以使用 ?:、??、??= 提升程式的簡潔性和可讀性,適合應用在表單接收、API 處理等場景! 延伸閱讀 【PHP 官方文件】三元運算子介紹 【PHP 官方文件】Null 合併運算子介紹 【MDN】JavaScript 三元運算子介紹 【MDN】JavaScript Null 合併運算子介紹 【MDN】JavaScript 假值介紹","link":"/Backend/PHP/Three-Useful-Symbol-Operators/"},{"title":"【jQuery】解析六種添加元素方法","text":"本篇重點 常見的添加元素方法 添加到指定元素內部:.append()、.prepend()、.appendTo()、.prependTo() 添加到指定元素外部:.after()、.before() 介紹各方法的使用效果及適用情境 開發網頁互動功能或動態內容時,動態新增 HTML 元素是常見的需求,例如:點擊按鈕時添加圖片、在特定區塊加入提示訊息等。 在 jQuery 中,為了讓這些操作更簡潔有效率,提供了多種用來「新增元素」的方法,如 .append()、.prepend()、.after()、.before()、.appendTo() 與 .prependTo(),雖然功能相似,但添加的位置與語法的寫法略有不同! 添加到指定元素內部.append()在選取元素內部的最後方添加新內容 js1$(目標元素).append(要添加的內容); 範例: 實例搶先看 .append() 在選取元素內部的最後方添加新內容 html123456789<div class="box"> <div class="content"> <div id="box">Hello</div> </div> <div class="field is-grouped"> <button class="button is-link example">.append() 添加元素</button> <button class="button reset">reset</button> </div></div> js123456789// .append()$('.example').on('click', function() { $('#box').append('<p>World</p>');});// reset$('.reset').on('click', function() { $('.content').html('<div id="box">Hello</div>');}); 結果: html1234<div id="box"> Hello <p>World</p></div> .prepend()在選取元素內部的最前方添加新內容 js1$(目標元素).prepend(要添加的內容); 範例: 實例搶先看 .prepend() 在選取元素內部的最後方添加新內容 html123456789<div class="box"> <div class="content"> <div id="box">World</div> </div> <div class="field is-grouped"> <button class="button is-link example">.prepend() 添加元素</button> <button class="button reset">reset</button> </div></div> js123456789// prepend()$('.example').on('click', function() { $('#box').prepend('<p>Hello</p>');});// reset$('.reset').on('click', function() { $('.content').html('<div id="box">World</div>');}); 結果: html1234<div id="box"> <p>Hello</p> World</div> .appendTo()把新內容添加到目標元素內部的最後方 js1$(要添加的內容).appendTo(目標元素); 範例: 實例搶先看 .appendTo() 把新內容添加到目標元素內部的最後方 html123456789<div class="box"> <div class="content"> <div id="box">Hello</div> </div> <div class="field is-grouped"> <button class="button is-link example">.appendTo() 添加元素</button> <button class="button reset">reset</button> </div></div> js12345678910// .appendTo()$('.example').on('click', function() { $('<p>World</p>').appendTo('#box'); // 效果和 $('#box').append('<p>World</p>'); 相同});// reset$('.reset').on('click', function() { $('.content').html('<div id="box">Hello</div>');}); 結果: html1234<div id="box"> Hello <p>World</p></div> .prependTo()把新內容添加到目標元素內部的最前方 js1$(要添加的內容).prependTo(目標元素); 範例: 實例搶先看 .prependTo() 把新內容添加到目標元素內部的最前方 html123456789<div class="box"> <div class="content"> <div id="box">World</div> </div> <div class="field is-grouped"> <button class="button is-link example">.prependTo() 添加元素</button> <button class="button reset">reset</button> </div></div> js12345678910// .prependTo()$(".example").on("click", function () { $("<p>Hello</p>").prependTo("#box"); // 效果和 $('#box').prepend('<p>Hello</p>'); 相同});// reset$(".reset").on("click", function () { $(".content").html('<div id="box">World</div>');}); 結果: html1234<div id="box"> <p>Hello</p> World</div> 添加到指定元素外部.after()在選取元素外部的後面添加新內容 js1$(目標元素).after(要添加的內容); 範例: 實例搶先看 .after() 在選取元素外部的後面添加新內容 html123456789<div class="box"> <div class="content"> <div id="box">Hello</div> </div> <div class="field is-grouped"> <button class="button is-link example">.after() 添加元素</button> <button class="button reset">reset</button> </div></div> js123456789// .after()$('.example').on('click', function() { $('#box').after('<p>World</p>');});// reset$('.reset').on('click', function() { $('.content').html('<div id="box">Hello</div>');}); 結果: html1234<div id="box"> Hello</div><p>World</p> .before()在選取元素外部的前面添加新內容 語法: js1$(目標元素).before(要添加的內容); 範例: 實例搶先看 .before() 在選取元素外部的前面添加新內容 html123456789<div class="box"> <div class="content"> <div id="box">World</div> </div> <div class="field is-grouped"> <button class="button is-link example">.before() 添加元素</button> <button class="button reset">reset</button> </div></div> js123456789// .before()$('.example').on('click', function() { $('#box').before('<p>Hello</p>');});// reset$('.reset').on('click', function() { $('.content').html('<div id="box">World</div>');}); 結果: html12<p>Hello</p><div id="box">World</div> 添加位置比較 方法 添加位置 .append() 目標元素內部最後 .prepend() 目標元素內部最前 .appendTo() 目標元素內部最後 .prependTo() 目標元素內部最前 .after() 目標元素外部後面 .before() 目標元素外部前面 結論我常常忘記哪些方法是添加到元素內部,哪些是添加到元素外部,所以用這篇文章來做個整理,方便自己記憶😆。這些添加元素的方法在做前端互動時非常實用,如果有其他更簡單、方便的添加方法,歡迎留言分享~ 延伸閱讀 【jQuery 官方文件】.append() 介紹 【jQuery 官方文件】.prepend() 介紹 【jQuery 官方文件】.appendTo() 介紹 【jQuery 官方文件】.prependTo() 介紹 【jQuery 官方文件】.after() 介紹 【jQuery 官方文件】.before() 介紹","link":"/Frontend/JQuery/Analyzing-Six-Methods-for-Adding-Elements/"},{"title":"【Html】實用的 <input> 表單屬性","text":"本篇重點 了解各個場景適合的 <input> 屬性 移除 <input type="number"> 上下箭頭的方法 如何依需求控制行動裝置的鍵盤類型 開發表單時,善用 <input> 的屬性可以讓輸入更直覺,並且提升使用者體驗,特別是行動裝置效果顯著 type="number":限制只能輸入數字限制輸入數值,可搭配 min、max、step 控制範圍與增減幅度,部分瀏覽器會顯示上下箭頭,可控制輸入的數值,適合用於金額、數量等欄位。 html1234<label >type="number": <input type="number" min="1" max="100" step="5" /></label> 移除上下箭頭的方法如果不想要有上下箭頭,可以使用 css 移除 html1234<label >移除上下箭頭: <input class="remove_arrow" type="number" min="1" max="100" step="5" /></label> css123456789input[type="number"].remove_arrow::-webkit-outer-spin-button,input[type="number"].remove_arrow::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0;}input[type="number"].remove_arrow { -moz-appearance: textfield;} type="tel":在手機上顯示數字鍵盤不會驗證格式,但在行動裝置上會顯示數字輸入鍵盤,適合輸入電話。 html1234<label >type="tel": <input type="tel"/></label> type="email":基本 Email 格式驗證不會主動驗證格式,執行送出表單才會驗證信箱格式,適合輸入信箱。 html1234<label>type="email": <input type="email" /></label><button>送出</button> type="url":基本 url 格式驗證不會主動驗證格式,執行送出表單才會驗證網址格式,適合輸入網址。 html12345<label >type="url": <input type="url"/></label><button>送出</button> type="date":日期選擇器提供內建的月曆選擇器,可搭配 min、max 控制日期範圍,適合輸入日期。 html1234<label >type="date": <input type="date" min="2023-01-01" max="2025-12-31"/></label> 日期選擇器的特性 日期單位操作:編輯日期單位(例如:年、月)通常會被視為一個整體,使用者點擊某個時間單位後,該單位會被選取並允許直接輸入數字或使用上下箭頭或從選擇器調整。 範圍限制:瀏覽器會強制執行範圍限制。使用者無法手動輸入超出時間範圍或超出 min/max 屬性定義範圍的數字。 空值表示:如果使用者手動刪除時間數字,該位置會自動替換為單位(例如 日 或 月),表示該欄位處於未完成或無效的狀態。 type="month":月份選擇器提供內建的月份選擇器,可搭配 min、max 控制日期範圍,適合輸入月份。 html1234<label >type="month": <input type="month" min="2024-01" max="2025-12"/></label> 月份選擇器的特性 日期單位操作:編輯日期單位(例如:年、月)通常會被視為一個整體,使用者點擊某個時間單位後,該單位會被選取並允許直接輸入數字或使用上下箭頭或從選擇器調整。 範圍限制:瀏覽器會強制執行範圍限制。使用者無法手動輸入超出時間範圍或超出 min/max 屬性定義範圍的數字。 空值表示:如果使用者手動刪除時間數字,該位置會自動替換為佔位符(通常是 – 或 -),表示該欄位處於未完成或無效的狀態。 type="datetime-local":日期時間選擇器提供內建的日期時間選擇器,可搭配 min、max 控制日期範圍,step 增加秒數選項,適合輸入日期時間。 html1234<label >type="datetime-local": <input type="datetime-local" min="2025-01-01T08:00" max="2025-12-31T18:00"/></label> 增加秒數選項html1234<label >type="datetime-local": <input type="datetime-local" step="1"/></label> 日期時間選擇器的特性 時間單位操作:編輯時間單位(例如:小時、分鐘)通常會被視為一個整體,使用者點擊某個時間單位後,該單位會被選取並允許直接輸入數字或使用上下箭頭或從選擇器調整。 範圍限制:瀏覽器會強制執行範圍限制。使用者無法手動輸入超出時間範圍或超出 min/max 屬性定義範圍的數字。 空值表示:如果使用者手動刪除時間或日期數字,該位置會自動替換為佔位符(通常是 – 或 –),表示該欄位處於未完成或無效的狀態。 時間的 12/24 小時制格式:取決於作業系統的時間或瀏覽器的語系格式設定,無法透過 HTML 屬性或 CSS 進行控制。 type="time":時間選擇器提供內建的時間選擇器,可搭配 step 增加秒數選項,適合輸入時間。 html1234<label >type="time": <input type="time" /></label> 增加秒數選項html1234<label >type="time": <input type="time" step="1"/></label> 時間選擇器的特性 時間單位操作:編輯時間單位(例如:小時、分鐘)通常會被視為一個整體,使用者點擊某個時間單位後,該單位會被選取並允許直接輸入數字或使用上下箭頭或從選擇器調整。 範圍限制:瀏覽器會強制執行範圍限制。使用者無法手動輸入超出時間範圍。 空值表示:如果使用者手動刪除時間,該位置會自動替換為佔位符(通常是 – 或 –),表示該欄位處於未完成或無效的狀態。 時間的 12/24 小時制格式:取決於作業系統的時間或瀏覽器的語系格式設定,無法透過 HTML 屬性或 CSS 進行控制。 type="range":滑桿式數字輸入提供內建的滑桿,搭配 min、max 控制數值範圍,適合用在音量、亮度、分數等場景。 html1234<label >type="range": <input type="range" min="0" max="100" step="10" /></label> type="color":色彩選擇器提供內建的色彩選擇工具,回傳 hex 色碼,適合輸入色彩。 html1234<label >type="color": <input type="color"/></label> inputmode:行動裝置上切換適合的鍵盤不改變輸入型態(type),使用 inputmode 也可以在行動裝置顯示對應的鍵盤類型,例如 numeric、email 鍵盤等。 html12345678<label >inputmode="numeric": <input type="text" inputmode="numeric" /></label><label >inputmode="email": <input type="text" inputmode="email" /></label> 健忘筆記 使用 Can I use 查詢屬性在每個瀏覽器的相容性 結論HTML5 提供了多種實用的 <input> 屬性,可以減少 JavaScript 驗證的需求,並且提升使用者操作上的便利性。特別是在行動裝置上,不同的 type 搭配 inputmode 能提供對應的輸入鍵盤,打造更直覺、流暢的表單體驗。 延伸閱讀 【MDN】input 屬性介紹 【Can I use】查詢瀏覽器相容性","link":"/Frontend/Html/Useful-Attributes-for-Input-Elements/"},{"title":"【Tool】cron 表達式(排程時間)轉換","text":"本篇重點 cron 表達式(排程時間)生成工具 cron 表達式轉換工具 介紹 cron 和 cron 表達式 cron 表達式符號語法使用方式 查詢所有/指定排程(crontab)的方法 新增/修改/刪除排程(crontab)的方法 .card { transition: opacity 0.3s ease-out, transform 0.3s ease-out, box-shadow 0.3s ease-in-out; opacity: 1; transform-origin: center top; } input[type=number]::-webkit-outer-spin-button, input[type=number]::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; } input[type=number] { -moz-appearance: textfield; } cron 表達式(排程時間)生成工具 從每個時間分類頁籤挑選要設定時間 依分類規則輸入 顯示 5 欄位格式的 cron 表達式,分別是分鐘(minute)、小時(hour)、日期(DOM - dayOfMonth)、月份(month)、星期(DOW - dayOfWeek),例如:5 0 * * 1 分鐘 小時 日期 月份 星期 固定間隔 區間範圍 指定時刻 設定「從哪個時間點開始」每隔多少單位執行一次,產生 起始值/間隔 的 cron 語法 * 表示每個可能的時間點,如果設定 * 並輸入執行間隔,* 表示從最小值開始 某時刻 執行間隔 設定一個區間,表示排程任務會在這段時間內「每個單位都執行一次」,產生 最小值-最大值 的 cron 語法 最小值 最大值 設定執行任務的「具體時間點」,可使用「逗號 ,」同時選擇多個時間,產生 時間點,時間點,時間點 的 cron 語法 某時刻 可使用 \",\" 選擇多個時間 固定間隔 區間範圍 指定時刻 設定「從哪個時間點開始」每隔多少單位執行一次,產生 起始值/間隔 的 cron 語法 * 表示每個可能的時間點,如果設定 * 並輸入執行間隔,* 表示從最小值開始 某時刻 執行間隔 設定一個區間,表示排程任務會在這段時間內「每個單位都執行一次」,產生 最小值-最大值 的 cron 語法 最小值 最大值 設定執行任務的「具體時間點」,可使用「逗號 ,」同時選擇多個時間,產生 時間點,時間點,時間點 的 cron 語法 某時刻 可使用 \",\" 選擇多個時間 固定間隔 區間範圍 指定時刻 設定「從哪個時間點開始」每隔多少單位執行一次,產生 起始值/間隔 的 cron 語法 * 表示每個可能的時間點,如果設定 * 並輸入執行間隔,* 表示從最小值開始 某時刻 執行間隔 設定一個區間,表示排程任務會在這段時間內「每個單位都執行一次」,產生 最小值-最大值 的 cron 語法 最小值 最大值 設定執行任務的「具體時間點」,可使用「逗號 ,」同時選擇多個時間,產生 時間點,時間點,時間點 的 cron 語法 某時刻 可使用 \",\" 選擇多個時間 固定間隔 區間範圍 指定時刻 設定「從哪個時間點開始」每隔多少單位執行一次,產生 起始值/間隔 的 cron 語法 * 表示每個可能的時間點,如果設定 * 並輸入執行間隔,* 表示從最小值開始 某時刻 執行間隔 設定一個區間,表示排程任務會在這段時間內「每個單位都執行一次」,產生 最小值-最大值 的 cron 語法 最小值 最大值 設定執行任務的「具體時間點」,可使用「逗號 ,」同時選擇多個時間,產生 時間點,時間點,時間點 的 cron 語法 某時刻 可使用 \",\" 選擇多個時間 固定間隔 區間範圍 指定時刻 設定「從哪個時間點開始」每隔多少單位執行一次,產生 起始值/間隔 的 cron 語法 * 表示每個可能的時間點,如果設定 * 並輸入執行間隔,* 表示從最小值開始 0 表示星期日、1 表示星期一以此類推 某時刻 執行間隔 設定一個區間,表示排程任務會在這段時間內「每個單位都執行一次」,產生 最小值-最大值 的 cron 語法 0 表示星期日、1 表示星期一以此類推 最小值 最大值 設定執行任務的「具體時間點」,可使用「逗號 ,」同時選擇多個時間,產生 時間點,時間點,時間點 的 cron 語法 0 表示星期日、1 表示星期一以此類推 某時刻 可使用 \",\" 選擇多個時間 cron 表達式 * * * * * 說明 中文 English 下次執行時間 cron 表達式轉換工具 支援基本 5 欄位格式,分別是分鐘(minute)、小時(hour)、日期(DOM - dayOfMonth)、月份(month)、星期(DOW - dayOfWeek),例如:5 0 * * 1 星期欄位(第 5 個欄位),0 表示星期日、1 表示星期一以此類推 cron 表達式 說明 中文 English 下次執行時間 $(function() { let timeout; const userLang = (navigator.language || navigator.userLanguage || '').toLowerCase(); if (userLang.startsWith('en')) { $('#translation').val('en'); $('#translation_input').val('en'); cronParser(); } else { cronParser(); } $(\"#translation, #translation_input\").on(\"change\", function(e) { if (e.target.id == 'translation') { cronParser(); } else { cronParser(true); } }); const tabContents = document.querySelectorAll('.tabs ~ .tab-content'); const observer = new MutationObserver((mutationsList) => { for (const mutation of mutationsList) { if (mutation.type === 'attributes' && mutation.attributeName === 'class') { const target = mutation.target; if (!target.classList.contains('is-hidden')) { update_cron_expression(target.id); } } } }); tabContents.forEach(el => { observer.observe(el, { attributes: true, attributeFilter: ['class'] }); }); $('#cron_expression_input').on('input', function () { const currentCron = $('#cron_expression_input').val().trim(); let cronParts = currentCron.split(' '); if (cronParts.length != 5) { return; } cronParser(true); }); $('[data-unit]').on('input', function () { const unit = $(this).data('unit'); const activeTabId = $(`#${unit} > .tab-content:not(.is-hidden)`).attr('id'); update_cron_expression(activeTabId); }); later.date.localTime(); function update_cron_expression(tabId) { const currentCron = $('#cron_expression').text().trim() || '* * * * *'; let cronParts = currentCron.split(' '); if (tabId.substr(0, 3) === 'min') { cronParts = update_expression_unit(tabId, cronParts, 0, 'min_', validMin) } else if (tabId.substr(0, 1) === 'h') { cronParts = update_expression_unit(tabId, cronParts, 1, 'h_', validHour) } else if (tabId.substr(0, 1) === 'd') { cronParts = update_expression_unit(tabId, cronParts, 2, 'd_', validDay) } else if (tabId.substr(0, 3) === 'mon') { cronParts = update_expression_unit(tabId, cronParts, 3, 'mon_', validMon) } else if (tabId.substr(0, 1) === 'w') { cronParts = update_expression_unit(tabId, cronParts, 4, 'w_', validWeek) } $('#cron_expression').text(cronParts.join(' ')); cronParser(); } function update_expression_unit(tabId, cronParts, unitIndex, unitPrefix, validator) { let value = cronParts[unitIndex]; if (tabId === `${unitPrefix}step`) { const every = $(`#${unitPrefix}step_every`).val().trim(); const step = $(`#${unitPrefix}step_step`).val().trim(); if (every == '') { value = '*'; } else if (every === '*' || validator(every)) { value = validator(step) && Number(step) > 0 ? `${every}/${step}` : every; } } else if (tabId === `${unitPrefix}range`) { const min = $(`#${unitPrefix}range_min`).val().trim(); const max = $(`#${unitPrefix}range_max`).val().trim(); if (min == '' || max == '') { value = '*'; } else if (validator(min) && validator(max) && Number(min) < Number(max)) { value = `${min}-${max}`; } } else if (tabId === `${unitPrefix}choices`) { const raw = $(`#${unitPrefix}choices_choices`).val().trim(); const parts = raw.split(','); const valid = parts.every(v => validator(v)); if (raw == '') { value = '*'; } else if (raw && valid) { value = raw; } } cronParts[unitIndex] = value ? value : '*'; return cronParts; } function validMin(val) { return /^\\d+$/.test(val) && val >= 0 && val = 0 && val = 1 && val = 1 && val = 0 && val { $('').text(date.toString()).appendTo(ol); }); $(next_times).empty().append(ol) } catch (err) { if (input) { check = $(\".icon-text.has-text-success\"); error = $(\".icon-text.has-text-danger\"); clearTimeout(timeout); error.fadeIn(); check.hide(); timeout = setTimeout(function() { check.fadeOut(300); error.fadeOut(300); }, 1500); } $(cron_parser).text(err).addClass('has-text-danger'); $(next_times).html('無效的 Cron 表達式'); } } }); cron 表達式工具有錯誤或建議歡迎留言告訴我😁 cron 表達式工具使用範例這邊示範如何使用 cron 表達式生成工具~ 每天十二點半執行排程 切換到分鐘頁籤的固定間隔,某時刻輸入 30 切換到小時頁籤的固定間隔,某時刻輸入 12 每個星期六,每小時每15分鐘執行排程 切換到分鐘頁籤的固定間隔,某時刻輸入 *,執行間隔輸入 15 切換到星期頁籤的固定間隔,輸入 6 一到三月的一號和十五號,00:00 執行排程 切換到分鐘頁籤的固定間隔,某時刻輸入 0 切換到小時頁籤的固定間隔,某時刻輸入 0 切換到日期頁籤的指定時刻,某時刻輸入 1,15 切換到月份頁籤的區間範圍,最小值輸入 1,最大值輸入 3 什麼是 cron?cron 是 Linux/Unix 系統中的一種排程工具,用來自動執行指定時間的任務,例如定時備份資料、寄送報表、執行維護腳本等。而 cron 的排程是透過 cron 表達式來定義執行的時間與頻率 cron 表達式的基本格式bash12345678* * * * * <command>- - - - -| | | | || | | | +---- 星期幾 (0 - 6)(0 是星期日)| | | +------ 月份 (1 - 12)| | +-------- 日 (1 - 31)| +---------- 時 (0 - 23)+------------ 分 (0 - 59) 星號(*)表示每個可能的時間點。 例如 * * * * * 表示每分鐘都執行。 逗號(,)表示多個時間點。 例如:0 9,18 * * * 表示每天 9 點與 18 點各執行一次。 減號(-)表示時間的範圍。 例如:0 8-10 * * * 表示每天 8 點、9 點、10 點執行一次。 斜線(/)表示每個時間點間隔多少單位。 例如:0 */3 * * * 表示每 3 小時整點執行一次。 範例 cron 表達式 說明 0 10 * * * 每天早上 10:00 執行一次 */15 * * * * 每 15 分鐘執行一次 0 */2 * 1,10 * 一月和十月每兩小時整點執行一次 0 12 * * 1-5 每週一到五中午 12:00 執行 (平日) 0 0 1 * * 每月 1 號的午夜 00:00 執行 0 22 * * 0,6 每週六、日晚上 10:00 執行 (假日) 查詢 crontab 排程查詢所有排程bash12345crontab -l# 結果0 10 * * 0,6 /home/user/scripts/backup.sh0 8 * * * /home/user/scripts/deploy.sh 查詢指定排程bash1234crontab -l | grep deploy# 結果0 8 * * * /home/user/scripts/deploy.sh 新增/修改/刪除 crontab 排程使用指令後會打開編輯器,直接在編輯器裡面新增/修改/刪除排程 bash1crontab -e 延伸閱讀 【wikipedia】cron 介紹 【bradymholt/cRonstrue】cron 表達式解析工具 【bunkat/later】計算排程的執行時間","link":"/Other/Tool/Cron-Expression-Conversion/"},{"title":"【PHP】解析 break 和 continue 對迴圈的控制","text":"本篇重點 認識 break 和 continue 可用在哪些地方 說明 break 和 continue 的基本語法 介紹 break n; 和 continue n; 在巢狀迴圈中的應用 終止或跳過的層數不能超過實際的巢狀層數,否則會有錯誤訊息 在 switch 裡控制外層迴圈 可用在哪些地方? foreach 迴圈 for 迴圈 while 迴圈 do...while 迴圈 這些結構都可以透過 break 來跳出整個迴圈,或是透過 continue 來略過本次執行,直接進入下一輪 基本語法break立即結束目前的迴圈,從迴圈區塊外繼續執行 php12345678for ($i = 0; $i < 5; $i++) { // 當 $i 為 3 時跳出迴圈 if ($i == 3) { break; } echo $i . "\\n";}echo "迴圈結束"; result1234012迴圈結束 動手做做看 break 基本語法 立即結束目前的迴圈,從迴圈區塊外繼續執行 動手做做看 continue跳過目前迴圈的後續內容,直接進入下一輪 php12345678for ($i = 0; $i < 5; $i++) { // 當 $i 為 3 時略過輸出 if ($i == 3) { continue; } echo $i . "\\n";}echo "迴圈結束"; result123450124迴圈結束 動手做做看 continue 基本語法 跳過目前迴圈的後續內容,直接進入下一輪 動手做做看 在巢狀迴圈中使用 break n; 與 continue n;多層迴圈(巢狀結構)中可以在 break 或 continue 後加上數字,控制要跳出或跳過幾層迴圈 break n;多層迴圈(巢狀結構)中,在 break 後加上數字,立即結束 n 層的迴圈,從迴圈區塊外繼續執行 php123456789for ($i = 0; $i < 3; $i++) { for ($j = 0; $j < 3; $j++) { echo "i = $i, j = $j\\n"; if ($j == 1) { break 2; // 跳出 $j 和 $i 這兩層迴圈 } }}echo "迴圈結束"; result123i = 0, j = 0i = 0, j = 1迴圈結束 動手做做看 巢狀迴圈中使用 break n; 多層迴圈(巢狀結構)中,在 break 後加上數字,立即結束 n 層的迴圈,從迴圈區塊外繼續執行 動手做做看 continue n;多層迴圈(巢狀結構)中,在 continue 後加上數字,跳過 n 層迴圈的後續內容,直接進入下一輪 php123456789for ($i = 0; $i < 3; $i++) { for ($j = 0; $j < 3; $j++) { echo "i = $i, j = $j\\n"; if ($j == 1) { continue 2; // 跳過目前 $i 的迴圈,直接到 $i++ } }}echo "迴圈結束"; result1234567i = 0, j = 0i = 0, j = 1i = 1, j = 0i = 1, j = 1i = 2, j = 0i = 2, j = 1迴圈結束 動手做做看 巢狀迴圈中使用 continue n; 多層迴圈(巢狀結構)中,在 continue 後加上數字,跳過 n 層迴圈的後續內容,直接進入下一輪 動手做做看 在 switch 裡控制外層迴圈switch 通常搭配 break 使用來結束 case 區塊(不能使用 continue)。但如果 switch 是寫在迴圈(如 foreach 或 for)裡,有時會希望在 switch 中直接跳出外層的迴圈,這時可以使用 break 2 或 continue 2。 switch 基本用法正確範例 動手做做看 錯誤跳出 switch switch 不是迴圈,因此不能直接使用 continue 或 break 動手做做看 php12345678910$switch = 'A'; switch ($switch) { case 'A': echo "處理 $switch\\n"; break; default: echo "處理 $switch\\n"; break; }echo "switch 結束\\n"; result12處理 Aswitch 結束 錯誤範例 動手做做看 錯誤跳出 switch switch 不是迴圈,因此不能直接使用 continue 或 break 動手做做看 php12345$x = 1;switch ($x) { case 1: continue;} result1PHP Warning: "continue" targeting switch is equivalent to "break" 控制外層迴圈break 2 動手做做看 switch 控制外層迴圈 - break 2 如果 switch 是包在一個迴圈裡,就可以用 break 2; 跳出 switch 及外層迴圈,繼續往下執行 動手做做看 php12345678910$list = ['A', 'break', 'B'];foreach ($list as $item) { switch ($item) { case 'break': break 2; default: echo "處理 $item\\n"; }}echo "break 2"; result12處理 Abreak 2 continue 2 動手做做看 switch 控制外層迴圈 - continue 2 如果 switch 是包在一個迴圈裡,就可以用 continue 2; 跳出 switch 並到外層迴圈的下一輪 動手做做看 php12345678910$list = ['A', 'continue', 'B'];foreach ($list as $item) { switch ($item) { case 'continue': continue 2; default: echo "處理 $item\\n"; }}echo "continue 2"; result123處理 A處理 Bcontinue 2 注意事項 break n; 和 continue n; 中的 n 不能超過實際的巢狀層數,否則會產生錯誤: error1Fatal error: Cannot break/continue N levels 動手做做看 不能超過實際的巢狀層數 break n; 和 continue n; 中的 n 不能超過實際的巢狀層數,否則會產生錯誤 動手做做看 break 或 continue 僅適用於「可中斷」的控制結構,因此不能用在 if 等條件式中 結論 continue; 與 continue 1; 一樣,僅跳過當前迴圈的剩餘程式碼,進入下一次迴圈。 continue n; 跳過 n 層迴圈,直接讓更外層迴圈的當前次數終止並開始下一次迴圈。 避免濫用 continue n;,特別是在多層迴圈,程式的可讀性會降低。 延伸閱讀 【PHP 官方文件】continue 介紹 【PHP 官方文件】break 介紹","link":"/Backend/PHP/How-break-and-continue-Control-the-Flow-of-Loops/"},{"title":"【API】使用 Google Analytics Data API 架設網站流量查詢 API","text":"本篇重點 如何使用 Google Analytics Data API 在 GA 中新增服務帳號,授權 API 存取 GA 數據 Fork GitHub 專案並部署至 Netlify,快速建立網站流量查詢 API 掌握 forgetfulengineer/google-analytics-data-api-netlify 專案使用方式 本篇紀錄如何使用 forgetfulengineer/google-analytics-data-api-netlify 網站流量查詢 API,取得網站的 總瀏覽量(PV)、不重複訪客(UV) 以及 單頁瀏覽量(page PV),並且部署在 Netlify 無伺服器環境中。 GA 設定與取得 API 金鑰啟用 Google Analytics Data API前往 Google Cloud Platform 控制台: 建立一個專案 前往「API 與服務 > 程式庫」,搜尋並啟用:Google Analytics Data API 建立服務帳號 前往「IAM & 管理 > 服務帳號」建立新帳號 權限選擇:擁有者 建立金鑰(JSON) 為服務帳號新增金鑰,格式選擇:JSON 儲存該 JSON 檔案內容,等等會用在 API 認證 加入 GA 資源前往 Google Analytics 控制台: 前往 GA 管理後台:管理 > 資源設定 > 資源存取管理 新增剛才建立的服務帳號信箱(xxx@xxx.iam.gserviceaccount.com),授權 分析人員 取得 GA 的 Property ID GA 的資源 ID 可於 管理 > 資源設定 > 資源詳細資料 取得 Fork 專案並部署到 NetlifyFork GitHub 專案 fork forgetfulengineer/google-analytics-data-api-netlify 到自己的 github 將專案部署到 Netlify 前往 Netlify 建立新專案 選擇「Projects > Add new project > Import an existing project」,並連結剛剛 fork 的 repo 設定環境變數 選擇「Add environment variables > Import from a .env file」 設定 API 所需的環境變數 名稱 說明 PROPERTYID GA Property ID STARTDATE 開始統計的日期,如 2023-12-31 ENDDATE 結束統計的日期,如 today GOOGLE_APPLICATION_CREDENTIALS_JSON GA 服務帳號金鑰內容(請勿公開) .env file1234PROPERTYID=123456789STARTDATE=2023-12-31ENDDATE=todayGOOGLE_APPLICATION_CREDENTIALS_JSON={"type":"service_account",...} 健忘筆記 日期格式為 YYYY-MM-DD,系統也接受 NdaysAgo、yesterday 或 today 格式,系統會根據資源的回報時區推斷日期,詳細可查看文件 如果環境變數未被正確讀取,可以查看【Netlify】環境變數更新無效的除錯流程 API 使用方式參數說明 參數名稱 是否必填 說明 範例 path 否 欲查詢的單篇頁面路徑 /Backend/MySQL/DQL-Execution-Order/ 查詢全站 PV/UVGET https://{your-site}.netlify.app/.netlify/functions/pageview 回傳格式 json1234{ "pv": string, "uv": string} 範例GET https://ga-api-demo.netlify.app/.netlify/functions/pageview 回傳格式 json1234{ "pv": "8888", "uv": "1234"} 查詢特定路徑 PVGET https://{your-site}.netlify.app/.netlify/functions/pageview?path={your-path} 回傳格式 json123456{ "pv": string, "uv": string, "path": string, "pageViews": string} 範例GET https://ga-api-demo.netlify.app/.netlify/functions/pageview?path=/Backend/MySQL/DQL-Execution-Order/ 回傳格式 json123456{ "pv": "8888", "uv": "1234", "path": "/Backend/MySQL/DQL-Execution-Order/", "pageViews": "100"} 補充:專案說明 部署在 Netlify Functions,採用 Node.js 撰寫,並且 .js 檔案會被 Netlify 包裝成可被呼叫的 API 端點(Function Endpoint),當收到請求時,自動呼叫匯出的 handler 函式,不需要額外設定伺服器或手動執行函式。 使用 npm 的 @google-analytics/data 套件串接 Google Analytics Data API,使用套件的 batchRunReports 搭配環境變數讀取設定來查詢 GA 的資料。 使用 async/await 語法來處理非同步資料請求。await 可暫停函式執行直到 Promise 結果回傳,讓程式接近同步流程。 回應中包含 HTTP 標頭 { 'Access-Control-Allow-Origin': '*' },用來允許跨來源請求(CORS),這樣能夠從不同網域(例如部署在 GitHub Pages 的前端網站)向此 API 發送請求,否則瀏覽器會因同源政策(Same-Origin Policy)而阻擋請求。 結論從 google analytics data api 的使用、 GA 的設定、Fork 專案到 Netlify 上線,打造自己的 網站流量查詢 API,支援全站 PV/UV 與單頁 PV 查詢。 可以將這支 API 應用在: 網頁上即時顯示瀏覽量 自訂網站儀表板 自動化分析與報表工具 如果對 API 有甚麼建議歡迎留言或是開 issue 讓我知道~ Google Analytics Data API 的應用 【API】使用 Google Analytics Data API 架設網站流量查詢 API【Hexo】Icarus 主題串接 GA 顯示網站 PV 和 UV 延伸閱讀 【Google 官方文件】Google Analytics Data API 總覽 【Netlify 官方文件】Netlify Functions 介紹","link":"/Backend/API/Building-a-Traffic-API-with-GA-Data-API/"},{"title":"【Hexo】Icarus 主題串接 GA 顯示網站 PV 和 UV","text":"本篇重點 如何修改 Icarus 主題、新增自訂插件 Icarus 主題串接 Google Analytics 顯示網站 PV 和 UV 使用 forgetfulengineer/google-analytics-data-api-netlify API 顯示網站數據 使用 countUp.js 建立數字的動畫效果 先了解API的使用可以幫助你更快理解唷😁 【API】使用 Google Analytics Data API 架設網站流量查詢 API Icarus 主題原生僅支援不蒜子提供的 PV 計數功能,無法串接 Google Analytics 的瀏覽數據。為了顯示來自 GA 的數據,我建立了 forgetfulengineer/google-analytics-data-api-netlify 作為 API 接口,並修改 Icarus 主題,使其能透過該 API 顯示網站數據。本文紀錄如何修改 Icarus 主題、新增自訂插件,並串接 API 顯示網站的 PV 與 UV。 1. 架設網站 PV/UV 查詢 APIfork forgetfulengineer/google-analytics-data-api-netlify 並部署到 Netlify,建立一個可查詢網站 PV/UV 的 API,詳細部屬流程請查看【API】使用 Google Analytics Data API 架設網站流量查詢 API。 2. 添加自定義插件Icarus 主題大部分的通用布局文件已被移至 repository hexo-component-inferno,因此可以在不影響主題更新的情況下添加自定義插件,布局文件的詳細說明可查看官方文件。 建立 ga_count 的 schema新增 icarus 設定檔參數的概要 檔案路徑12345678themes/├── icarus/│ └── include/│ └── schema/│ └── plugin/│ └── ga_count.json ← 新增參數概要│ └── common/│ └── plugins.json ← 增加引用路徑 新增參數概要 /include/schema/plugin/ga_count.json查看123456789101112131415{ "$schema": "http://json-schema.org/draft-07/schema#", "$id": "/plugin/ga_count.json", "description": "Ga count plugin", "type": "object", "properties": { "ga_count_api": { "type": "string", "description": "Google Analytics Data API", "nullable": true, "examples": ["https://ga-api-demo.netlify.app/.netlify/functions/pageview"] } }, "required": ["ga_count_api"]} 增加引用路徑 /include/schema/common/plugins.json查看12345{ ... "ga_count": "plugin/ga_count.json" ...} 主題設定啟用 ga_count填入 API 路徑 https://{your-site}.netlify.app/.netlify/functions/pageview _config.icarus.yml123plugins: ga_count: ga_count_api: https://ga-api-demo.netlify.app/.netlify/functions/pageview 建立 ga_count 元件新增 /layout/plugin/ga_count.jsx 作為 Hexo 客製化插件,用於前端呼叫 GA API,並顯示瀏覽數。 layout/plugin/ga_count.jsx >folded查看檔案123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869const { Component, Fragment } = require('inferno');const { cacheComponent } = require('hexo-component-inferno/lib/util/cache');class GACount extends Component { render() { const { head, apiUrl, countUpJs } = this.props; if (head) return null; let gaCountJs = ` async function fetchAndRenderGAData() { const url = "${apiUrl}"; try { const response = await fetch(url); if (!response.ok) throw new Error('API 回應錯誤'); const json = await response.json(); const initCountUp = (elementId, targetValue) => { const el = document.getElementById(elementId); if (el && window.countUp) { new countUp.CountUp(elementId, targetValue, { enableScrollSpy: true, scrollSpyOnce: true }); } }; requestAnimationFrame(() => { initCountUp('ga_value_site_pv', json.pv); initCountUp('ga_value_site_uv', json.uv); initCountUp('ga_value_page_pv', json.pageViews); }); } catch (error) { console.error('獲取 GA 數據失敗:', error); } } if ('requestIdleCallback' in window) { window.requestIdleCallback(fetchAndRenderGAData, { timeout: 2000 }); } else { window.addEventListener('load', () => { fetchAndRenderGAData(); }); } `; return <Fragment> <script src={countUpJs} defer></script> <script dangerouslySetInnerHTML={{ __html: gaCountJs }}></script> </Fragment>; }}GACount.Cacheable = cacheComponent(GACount, 'plugin.ga_count', props => { const { helper, head, page, plugin } = props; const { url_for, cdn } = helper; const apiBase = plugin.ga_count_api; const path = url_for(page.path); const apiUrl = (page.layout == 'post') ? `${apiBase}?path=${encodeURIComponent(path)}` : apiBase; const countUpJs = cdn('countup.js', '2.9.0', 'dist/countUp.umd.js'); return { head, apiUrl, countUpJs };});module.exports = GACount; 元件快取機制 cacheComponent: Icarus 主題特有的效能優化方式,避免不必要的重複渲染,每個組件會依據頁面內容快取。 API 路徑動態處理: 根據是否為文章頁,決定是否要加上 ?path= 查詢字串,讓後端能計算單篇 PV。 jsx查看1const apiUrl = (page.layout == 'post') ? `${apiBase}?path=${encodeURIComponent(path)}` : apiBase; 使用 countUp.js 呈現數字動畫: 利用 cdn() 方法載入 CountUp 套件,再透過 enableScrollSpy 與 scrollSpyOnce 讓動畫僅在畫面滾動到數字時觸發一次。 jsx查看1new countUp.CountUp('ga_value_page_pv', json.pageViews, { enableScrollSpy: true, scrollSpyOnce: true }) plugin 載入處理: Icarus 主題中,layout/common/head.jsx 和 layout/common/scripts.jsx 都會載入 plugin,但 ga_count.jsx 只需載入一次,且必須等頁面元素渲染完成後再執行,避免抓不到 PV 元素,因此加入判斷,當處於 <head> 區塊時跳過渲染: jsx查看1if (head) return null; 資料顯示區塊:透過 getElementById 取得 3 個容器(site PV、site UV、單篇 PV),再用 CountUp 顯示動畫數字。 詳細修改紀錄 3. 修改模板:顯示 PV / UV 數字修改 article.jsx(文章 PV)在文章內容(layout/common/article.jsx)中新增顯示該篇文章瀏覽數(PV)的區塊。 jsx查看12345678{/* Visitor counter */}{!index && plugins && plugins.ga_count.ga_count_api ? ( <span class="level-item" id="ga_container_page_pv"> <i class="far fa-eye"></i> <span id="ga_value_page_pv">-</span> </span> ) : null;} 修改 footer.jsx (全站 PV/UV)在頁尾(layout/common/footer.jsx)中新增顯示全站瀏覽及訪客數(PV/UV)的區塊。 取得 ga_count 設定並修改瀏覽數據顯示內容 jsx查看1234- showVisitorCounter: plugins && plugins.busuanzi === true,- visitorCounterTitle: _p('plugin.visitor_count', '<span id="busuanzi_value_site_uv">0</span>')+ showVisitorCounter: plugins && plugins.ga_count.ga_count_api,+ visitorCounterTitle: '<i class="far fa-eye"></i>總瀏覽數:<span id="ga_value_site_pv">-</span>&nbsp;&nbsp;<i class="fa-solid fa-person-walking"></i>總訪客數:<span id="ga_value_site_uv">-</span>' 將原本 Busuanzi 使用的容器 ID 改為 GA 專用的容器 jsx查看12- {showVisitorCounter ? <span id="busuanzi_container_site_uv"+ {showVisitorCounter ? <span id="ga_container_site_pvuv" 詳細修改紀錄 結論透過修改 Icarus 主題與串接 forgetfulengineer/google-analytics-data-api-netlify,就能在網站中顯示來自 Google Analytics 的瀏覽數據,即時掌握網站的流量情況。 如果在串接過程中遇到問題,或有任何建議,歡迎留言讓我知道~接下來計畫新增「熱門文章排行榜」,敬請期待! Google Analytics Data API 的應用 【API】使用 Google Analytics Data API 架設網站流量查詢 API【Hexo】Icarus 主題串接 GA 顯示網站 PV 和 UV 延伸閱讀 【健忘工程師網站主題】forgetfulengineer/hexo-theme-icarus repository 【網站流量查詢 API】forgetfulengineer/google-analytics-data-api-netlify repository 【數字動畫套件】inorganik/countUp.js repository","link":"/Other/Hexo/Icarus-Integrate-Ga-Api-Pv-Uv/"},{"title":"【JavaScript】字串比大小的型別陷阱","text":"本篇重點 JavaScript 中「字串」與「數字」如何進行比較? 字串之間的大小比較是依 Unicode 編碼,不是數值 字串與數字混合使用進行比較 避免型別產生的錯誤,提供處理方式 字串與數字在加減乘除的差異 字串比大小結果「不符合預期」在 JavaScript 中,當你使用 < 、 > 、 <= 、 >= 比較兩個字串時,並不會把字串當作數字來比較,而是根據字串的 Unicode 編碼值 進行逐字比較。 範例: js123console.log("2".charCodeAt(0)); // 50console.log("10".charCodeAt(0)); // 49console.log("2" < "10"); // false “2” 的 Unicode 是 50 “1” 的 Unicode 是 49 字串逐字比較,因此比較的是 "2" vs "1" → 50 > 49,所以 "2" < "10" 結果是 false 健忘筆記 使用 .charCodeAt() 可以查詢字串的 Unicode 動手做做看 字串與數字混合使用進行比較當你用 <, > 比較 一個字串和一個數字 時,JavaScript 會嘗試將字串轉成數字再比較。 範例: js12console.log("2" < 10); // true → 字串 "2" 被轉成數字 2console.log("10" < 2); // false → 字串 "10" 被轉成數字 10 動手做做看 避免型別產生的錯誤確保 <、> 的兩邊是相同型別,使用 parseInt()、Number() 將字串轉為數字。 範例: js1234567891011let str1 = '2';let str2 = '10';// 字串轉為數字後再做比較console.log(typeof parseInt(str1, 10)); // numberconsole.log(typeof parseInt(str2, 10)); // numberconsole.log(parseInt(str1, 10) < parseInt(str2, 10)); // trueconsole.log(typeof Number(str1)); // numberconsole.log(typeof Number(str2)); // numberconsole.log(Number(str1) < Number(str2)); // true 動手做做看 補充:字串與數字在加減乘除的差異不同型別運算時會進行 隱式型態轉換 + 運算子在「有字串」的情況下會是 字串串接。 -、*、/ 等運算子不做字串串接,而是會嘗試把字串轉成數字再計算。 如果轉換失敗,就會得到 NaN(Not-a-Number)。 字串 + 字串 運算 範例 結果 原因 + "2" + "3" "23" + 觸發字串串接 - "5" - "2" 3 兩個字串都可轉數字 * "4" * "3" 12 兩個字串都可轉數字 / "8" / "2" 4 兩個字串都可轉數字 - "a" - "1" NaN "a" 無法轉為數字 字串 + 數字 運算 範例 結果 原因 + "2" + 3 "23" 有字串 → 轉成字串串接 - "2" - 1 1 "2" 轉成數字 2 * "3" * 2 6 "3" 轉成數字 3 / "6" / 3 2 "6" 轉成數字 6 js1234567891011// 字串 + 數字console.log("2" + 3); // "23" → 字串串接console.log("2" - 1); // 1 → 轉成數字計算console.log("3" * 2); // 6 → 轉成數字計算console.log("6" / 3); // 2 → 轉成數字計算// 字串 + 字串console.log("5" + "2"); // "52" → 字串串接console.log("5" - "2"); // 3 → 兩個字串轉成數字計算console.log("4" * "3"); // 12 → 兩個字串轉成數字計算console.log("a" - "1"); // NaN → "a" 無法轉為數字 健忘筆記 為了避免運算產生的型別錯誤,請把字串轉為數字再做運算 動手做做看 結論JavaScript 在進行大小比較時,如果是字串會以 Unicode 字元逐個比較,可能會導致意想不到的結果。 養成明確轉型的習慣。 避免字串與數字混合比較。 不確定的地方,可以用 typeof 檢查變數型別。 這樣就能避免很多難以追蹤的錯誤行為,有遇過意想不到的比較結果嗎?歡迎留言分享~","link":"/Frontend/JavaScript/String-Comparison-Type-Gotchas/"},{"title":"【MySQL】欄位編碼設定影響查詢的大小寫敏感","text":"本篇重點 不同 collation 對查詢的影響 如何確認欄位 collation 設定 collation 的方法 查詢時強制區分大小寫 操作 MySQL 查詢時,有時候會發現輸入的條件大小寫不同卻能查到相同的結果,例如 WHERE name = 'Tom' 與 WHERE name = 'tom' 回傳的內容一樣。這並不是 MySQL 的「錯誤」,而是因為欄位的 collation 可能被設定為大小寫不敏感 (case-insensitive),導致查詢忽略了大小寫差異。 不同 Collation 對查詢的影響MySQL 中,collation (編碼、排序規則) 影響字串的比較與排序方式,以下是 collation 的重要特性: 是否區分大小寫 (case sensitivity)、是否區分重音 (accent sensitivity) ci → case-insensitive (不區分大小寫),查詢時 Tom 與 tom 視為相同 bin → binary (以二進位比較,嚴格區分內容),查詢時 Tom 與 tom 或是 Eric 與 Éric 會被視為不同 語言排序規則 utf8mb4_general_ci → 簡單快速,但會犧牲部分語言正確性 utf8mb4_unicode_ci → 依 Unicode 規範排序,更精準,適合多語言 範例: 不同的 collation 得到不同的結果 sql1234567891011-- utf8mb4_general_ci 大小寫不敏感SELECT * FROM customers WHERE name_ci = 'tom';SELECT * FROM customers WHERE name_ci = 'TOm';-- utf8mb4_bin 區分大小寫SELECT * FROM customers WHERE name_bin = 'tom';SELECT * FROM customers WHERE name_bin = 'Tom';-- 區分重音SELECT * FROM customers WHERE name_ci = 'Eric';SELECT * FROM customers WHERE name_bin = 'Eric'; 動手做做看 不同的 collation 得到不同的結果 是否區分大小寫(case sensitivity) 是否區分重音(accent sensitivity) 動手做做看 如何確認欄位 Collation透過指令檢查欄位的 collation sql12345678-- 查看資料表欄位的 collationSHOW FULL COLUMNS FROM customers;-- 或從 information_schema 查詢SELECT COLUMN_NAME, COLLATION_NAMEFROM information_schema.COLUMNSWHERE TABLE_SCHEMA = 'your_database' AND TABLE_NAME = 'customers'; 動手做做看 透過指令檢查欄位的 collation 動手做做看 設定 Collation哪些欄位需要大小寫敏感?不區分大小寫 (使用 ci) 一般文字內容(標題、文章、備註) 使用者暱稱、搜尋功能中的關鍵字 多語言資料 區分大小寫 (使用 cs 或 bin) 使用者帳號 密碼欄位 Token、驗證碼、序號等需嚴格比對的字串 建立欄位或資料表建立整張資料表指定 Collation sql12345CREATE TABLE customers ( id INT AUTO_INCREMENT PRIMARY KEY, name_ci VARCHAR(50) COLLATE utf8mb4_general_ci, name_bin VARCHAR(50) COLLATE utf8mb4_bin); 新增欄位指定 Collation sql123ALTER TABLE customersADD name_bin2 VARCHAR(50) CHARACTER SET utf8mb4COLLATE utf8mb4_bin; CHARACTER SET 與 COLLATE 要一起指定 動手做做看 建立欄位或資料表設定 collation 建立整張資料表指定 Collation 新增欄位指定 Collation 動手做做看 修改欄位或資料表修改整張資料表指定 Collation sql123ALTER TABLE customersCONVERT TO CHARACTER SET utf8COLLATE utf8_bin; 修改資料表所有文字欄位(VARCHAR、TEXT 等)的 Character Set 與 Collation 修改欄位指定 Collation sql123ALTER TABLE customersMODIFY name_ci VARCHAR(100)COLLATE utf8mb4_general_ci; 動手做做看 修改欄位或資料表設定 collation 修改整張資料表指定 Collation 修改欄位指定 Collation 動手做做看 查詢時強制區分大小寫sql12SELECT * FROM customersWHERE name_ci COLLATE utf8mb4_bin LIKE 'Tom'; 結論MySQL 查詢是否區分大小寫,取決於 collation 的設定。 ci 適合大部分一般文字內容 bin 或 cs 適合帳號、密碼、Token 等需嚴格比對的欄位 不用每個欄位都個別設定,建議在資料庫層級先指定一個通用的 collation,再針對需要區分大小寫的欄位額外調整。 延伸閱讀 【官方文件】命名慣例介紹","link":"/Backend/MySQL/column-collation-case-sensitivity/"},{"title":"【Git】使用 git revert 一次性還原多個提交","text":"本篇重點 git revert 的用途以及使用情境 還原指定或多個提交的方法 調整容易混淆的還原範圍寫法 多個還原合併成一次提交 git revert 和 git reset 的差異 動手做做看 我建了一個範例 repository,可以 clone 下來後跟著文章操作!git clone https://github.com/forgetfulengineer/git-revert-example.git 使用 Git 做版本控制,偶爾會遇到需要「還原某些修改」的情境,這時候除了常聽到的 git reset,還有一個更安全、常被忽略的指令:git revert。 git revert用途建立一個新的 commit,將指定 commit 的變更「還原」回去,換句話說,它不會直接刪除歷史紀錄,而是新增一個「還原某次修改」的提交。 使用情境 誤把有問題的功能合併進主要分支,需要「還原」但不能破壞歷史。 在多人協作的專案裡,避免因為 git reset 改寫歷史而造成衝突。 還原指定提交git1git revert <commit-hash> 範例: git12345git log --onelinec3d9e28 修正 README 錯字b71c5a2 新增登入驗證功能a84f0d1 新增會員註冊功能9d2a6ff 初始化專案 發現登入驗證功能 (b71c5a2) 有 bug,需要先移除功能 git1git revert b71c5a2 Git 會自動進入編輯器,讓你修改 commit message,通常預設會像這樣: git123Revert "新增登入驗證功能"This reverts commit b71c5a2. 儲存後就會新增一個還原修改的提交了 git123456git log --onelinegak451r Revert "新增登入驗證功能"c3d9e28 修正 README 錯字b71c5a2 新增登入驗證功能a84f0d1 新增會員註冊功能9d2a6ff 初始化專案 健忘筆記 直接使用預設提交訊息,不做任何修改,可以使用 git revert <commit-hash> --no-edit 還原多個提交還原連續提交容易混淆的還原範圍git1git revert <oldest-commit>..<latest-commit> 從 <oldest-commit> 的下一個提交開始到 <latest-commit> 為止,並依序產生「反向 commit」,<oldest-commit> 本身的內容並不會被還原。 範例 git12345git log --onelinec3d9e28 修正 README 錯字b71c5a2 新增登入驗證功能a84f0d1 新增會員註冊功能9d2a6ff 初始化專案 如果執行 git revert a84f0d1..c3d9e28,實際被還原的會是 b71c5a2 和 c3d9e28,而 a84f0d1 並不會被包含在內。 調整還原範圍的寫法因為 <oldest-commit>..<latest-commit> 的範圍並不包含 <oldest-commit>,在實際使用時可能會造成混淆。 如果希望範圍正好是從 <oldest-commit> 到 <latest-commit>(包含 <oldest-commit> 本身),就需要在 <oldest-commit> 後面加上 ^,代表「從這個提交的前一個開始」。 git1git revert <oldest-commit>^..<latest-commit> 範例 git12345git log --onelinec3d9e28 修正 README 錯字b71c5a2 新增登入驗證功能a84f0d1 新增會員註冊功能9d2a6ff 初始化專案 如果執行 git revert a84f0d1^..c3d9e28,實際被還原的就是 a84f0d1 到 c3d9e28。 多個還原合併成一次提交還原多個提交會逐一產生「反向 commit」,如果不希望產生多個 commit,而是想把所有變更合併到一次提交中,可以使用 --no-commit 參數,然後再手動下 git commit git12git revert --no-commit <oldest-commit>^..<latest-commit>git commit -m "Revert commits from <oldest-commit> to <latest-commit>" 範例 git12345git log --onelinec3d9e28 修正 README 錯字b71c5a2 新增登入驗證功能a84f0d1 新增會員註冊功能9d2a6ff 初始化專案 git12git revert --no-commit a84f0d1^..c3d9e28git commit -m "還原到專案初始化的狀態" -m "Revert commits from a84f0d1 to c3d9e28" 健忘筆記 第一個 -m 是提交訊息,第二個 -m 是提交的補充說明,多個 -m 選項會串連成個別段落,詳細說明可察看官方文件 還原非連續提交如果要還原的提交非連續,無法使用範圍操作 git12git revert --no-commit <commit1> <commit2> <commit3>git commit -m "Revert selected commits" 範例 git12345git log --onelineb71c5a2 新增登入驗證功能c3d9e28 修正 README 錯字a84f0d1 新增會員註冊功能9d2a6ff 初始化專案 git12git revert --no-commit a84f0d1 b71c5a2git commit -m "移除會員註冊、登入驗證功能" -m "Revert commits a84f0d1, b71c5a2" git revert 和 git reset 的差異 指令 行為 是否保留歷史 適合場合 git revert 新增一個「撤銷指定 commit」的提交 保留歷史 公開分支、多人協作 git reset 將分支指標移動到指定 commit 改寫歷史 私人分支、尚未 push 的情況 範例 git revert git tree12A - B - C - D (main) ↑ (HEAD) 假設要還原 C,git revert C 後會變成: git tree12A - B - C - D - E (main) ↑ (HEAD) E 這個新 commit 就是「還原 C 的內容」 git reset git tree12A - B - C - D (main) ↑ (HEAD) 執行 git reset B git tree12A - B (main) ↑ (HEAD) 原本的 C 和 D 會直接消失(歷史被改寫) 健忘筆記 還原指令建議 如果專案已經 push 到 GitHub,建議使用 git revert 如果還在本地測試、未 push,可以用 git reset 來清理歷史 結論 語法 作用 git revert --no-commit A..D 還原 B、C、D(不包含 A) git revert --no-commit A^..D 還原 A、B、C、D git revert --no-commit A B C D 手動指定還原 A, B, C, D git revert 適合在多人協作、公開分支上使用,能保留完整歷史並安全還原修改。 如果要還原多個提交,可以 一次指定多個 commit 或 使用範圍語法。 git revert 和 git reset 最大的差異在於是否「改寫歷史」: revert 會新增 commit,保留歷史。 reset 則會刪掉歷史,不適合公開分支。 動手做做看 我建了一個範例 repository,可以 clone 下來後跟著文章操作!git clone https://github.com/forgetfulengineer/git-revert-example.git 延伸閱讀 【Git 官方文件】git revert 介紹 【Git 官方文件】git commit -m 介紹 【Git 官方文件】git reset 介紹","link":"/Other/Git/Git-Revert-Multiple-Commits/"},{"title":"【Netlify】環境變數更新無效的除錯流程","text":"本篇重點 Netlify function 更新程式碼後,新設定的環境變數未被正確讀取 檢查環境變數是否設定在正確的環境 (Production / Deploy Preview / Branch deploys) 確認環境變數名稱是否正確 透過 Deploy project without cache 解決問題 問題情境專案架設於 Netlify,並透過 GitHub 連動自動佈署。 更新程式碼後,function 使用新的環境變數,雖然已在 Netlify 後台新增並儲存變數,但程式始終沒有正確讀取。 排查流程確認環境是否正確Netlify 提供不同環境: Production:主要佈署來源,與正式網域綁定 Deploy Preview:針對 PR 產生的測試環境 Branch deploys:針對指定分支的佈署 若環境變數僅設定在 Deploy Preview 或 Branch deploys,Production 可能無法取得。 檢查後確認 Production 已正確設定,並且如果是免費方案的 Netlify 會部屬在全區域,無法設定環境變數部屬區域,只能指定各區域的變數值。 確認環境變數名稱拼字常見錯誤包含: 拼錯大小寫(環境變數區分大小寫) 變數前後多餘空格 後台儲存時沒有正確提交 檢查後確認名稱正確無誤,程式碼與後台設定的環境變數名稱一致。 移除快取重新佈署雖然後台顯示環境變數已更新,但 function 仍未讀到新值。嘗試在佈署頁面選擇:「Trigger deploy → Deploy project without cache」,清除快取後重新佈署。 重新部署並清除快取後,function 成功取得最新環境變數。 為什麼會發生?Netlify 在部署過程中會建立建構快取 (build cache),其中可能包含舊的環境設定或編譯結果。 當環境變數更新後,如果 Netlify 沒有完全清除快取,舊的變數值可能仍會被沿用,導致新值無法被讀取。 結論這次的問題並非環境變數設定錯誤,是 Netlify function 使用了快取的舊設定。透過清除快取並重新佈署後,function 才正確讀取到新環境變數。 未來遇到類似狀況,可以依照流程依序排查: 確認環境設定位置是否正確 確認拼字無誤 清除快取重新佈署 延伸閱讀 【Netlify 官方文件】環境變數介紹","link":"/Backend/Netlify/Netlify-Env-Variables-Debugging/"},{"title":"【Html】解析 preconnect、preload、Fetch Priority API","text":"本篇重點 資源連線與載入的時間線 縮短首次繪製時間(First Paint)的方法 rel="preconnect"、rel="preload"、fetchpriority 的用意與差異 有/無用這些屬性時,載入時間的差異 fetchpriority 與 preload 的差異 crossorigin 屬性的用途與使用時機 onload="this.rel='stylesheet'" 的動態載入樣式 資源連線與載入的時間線瀏覽器載入網頁時,通常會經歷以下階段: DNS 查詢:查詢網域名稱對應的 IP。 TCP 連線:建立與伺服器的連線。 TLS 握手(HTTPS 時):建立安全通道。 HTTP 請求與回應:正式請求與接收資源。 解析 HTML → 載入資源(CSS、JS、字型、圖片等)。 若外部資源(例如字型、CSS、圖片)位於不同網域,這三個階段(DNS、TCP、TLS)都要重新建立一次。 因此,若能「提前建立連線」或「提前載入關鍵資源」,就能減少等待時間,縮短「首次繪製時間(First Paint)」。 rel=”preconnect”preconnect 可讓瀏覽器先幫該網域建立連線(包含 DNS、TCP、TLS),後續正式請求資源時,可以直接跳過連線階段,加速載入。 不會下載資源,只提前連線 不會影響 HTML 解析與 DOM 建立流程 適合第三方資源(CDN、字型、API 域名) 差異說明 無使用:瀏覽器碰到真正要下載資源的 HTML/CSS/JS(例如 <link href="...">、@font-face 的字型 URL)時,才開始 DNS → TCP → TLS 等連線流程。 有使用:瀏覽器解析到 <link rel="preconnect" href="...">,就會在此階段開始 DNS → TCP → TLS。之後當資源被引用,連線步驟已完成,可以立即發送請求。 範例 Google Fonts 官方載入範例 html12<link rel="preconnect" href="https://fonts.googleapis.com"><link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 健忘筆記 僅對「關鍵第三方域名」使用 preconnect(如字型、主要 CDN、關鍵 API);濫用會浪費連線資源與瀏覽器配額。 preconnect 對縮短首次網路握手時間最有效,但不會下載任何檔案。 rel=”preload”preload 告訴瀏覽器「這個資源很重要,請優先載入」,即使該資源稍後才會在 CSS 或 JS 中被引用。下載後並不會直接執行資源,只是讓瀏覽器先行下載。 控制載入順序、避免延遲渲染 不會影響 HTML 解析與 DOM 建立流程 適合關鍵字型、首屏圖片等 差異說明 無使用:瀏覽器會在解析到資源引用(例如在 CSS 裡發現 @font-face 或 HTML 中發現 <img> / <link rel="stylesheet">)時才下載;若 CSS 在較後才解析,資源下載會被延後。 有使用:在瀏覽器解析 <link rel="preload" href="..." as="..."> 那一刻就開始下載該資源,因此能顯著縮短關鍵資源的可用時間。 範例 html html1<link rel="preload" href="https://fonts.gstatic.com/s/foo.woff2" as="font" type="font/woff2" crossorigin /> 健忘筆記 preload 需要正確的 as(例如 as=”font”、as=”style”、as=”image”),錯誤會導致瀏覽器無法正確處理緩存與優先權。 跨來源的字型資源通常需要 crossorigin 才能成功載入並使用 Fetch Priority APIHTML5 新增的屬性,允許開發者為單一資源標記優先順序(high、low、auto),讓瀏覽器在其內部的排程中更傾向先下載或後下載該資源,但瀏覽器可能根據自身策略(例如當前網路情況、連線數量、瀏覽器內部優化、節能模式等)決定是否完全遵從 。 控制資源載入優先順序 適用於關鍵圖片、主樣式表、首屏腳本等 常用於 <img>、<link>、<script> 瀏覽器載入優先級(Chrome) 優先度 資源類型 備註 Highest HTML、CSS、fonts 首屏必要資源 High 主程式 script、defer script、首屏圖片(in-viewport) 主要邏輯與畫面 Medium 一般 script(例如次要互動) 非立即需要 Low async script、lazyload 圖片/影音 非同步載入、不影響首屏 Lowest prefetch、非當前頁面資源 未來可能用到 健忘筆記 優先度決定「當多個資源在候補要下載時,哪個先被拿到網路資源」,詳細可查看【wev.dev】了解 Fetch Priority API 差異說明 無使用:照原瀏覽器載入順序下載資源 有使用:影響該資源在下載排程的優先順序 範例 html12<img src="/hero.webp" fetchpriority="high" alt="Main Banner"><link rel="stylesheet" href="/main.css" fetchpriority="high"> 提醒瀏覽器「這個資源對渲染很重要」,應該盡早下載 相反地,背景圖片、懸浮區塊可標註為 fetchpriority="low",節省頻寬 與 preload 差異: preload 是「強制提前下載」 fetchpriority 是「建議調整優先權」 項目 fetchpriority <link rel="preload"> 是否強制下載 不會強制(僅建議) 強制瀏覽器預先載入資源 適用資源 任何可下載資源(img, script, css, font) 通常用於關鍵字體、CSS、首屏圖片 時機差異 等瀏覽器解析到資源時才決定下載順序 在解析 HTML 初期就主動開始下載 是否影響渲染 不直接影響渲染,只影響載入排序 可顯著縮短首屏渲染時間 動態載入樣式可讓瀏覽器延遲樣式表的應用,避免同步的 CSS 阻塞初始渲染。 使用 rel="preload" 先下載 CSS,但不立即套用 當 onload 觸發後,再將 rel 改為 "stylesheet",瀏覽器才正式套用樣式 避免 render-blocking 常用於「非關鍵 CSS」 範例: html12<link rel="preload" href="/css/print.css" as="style" onload="this.onload=null;this.rel='stylesheet'"><noscript><link rel="stylesheet" href="/css/print.css"></noscript> 說明: onload=null 避免載入後重複觸發事件 <noscript> 提供給沒有 JS 環境的用戶 可避免 CSS 阻塞渲染(Render-blocking),提升 LCP(Largest Contentful Paint) crossorigin 屬性的用途與時機給需要使用 CORS(Cross-Origin Resource Sharing)驗證的資源,不是所有跨源資源都需要使用 crossorigin。 值 說明 anonymous 不帶憑證(cookies、授權資訊)。 use-credentials 帶憑證發送請求。 使用時機: 字型、圖片、影片、JS 來自不同網域時。 需要通過 CORS 驗證的資源(否則會被視為失敗)。 範例: html12// 字型檔屬於「需要 CORS 驗證」的資源<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 結論preconnect、preload、fetchpriority 是優化前端中互補的三個屬性: preconnect:提前連線,解決「網路握手延遲」,不下載資源,適合重要的第三方域名。 preload:提前下載,解決「關鍵資源延後載入」,適合關鍵字型/圖片效果顯著。 fetchpriority:精細調整優先權,解決「資源搶占頻寬問題」,是建議而非強制。 整體而言,這些屬性的目的都是讓「瀏覽器提早行動」,減少等待,提升用戶的載入體驗,但也要控制使用數量與範圍,避免造成頻寬壅塞或預載過多無效資源的問題。 延伸閱讀 【MDN】rel=preconnect 介紹 【MDN】rel=preload 介紹 【wev.dev】了解 rel=preconnect 和 rel=dns-prefetch 【wev.dev】了解 Fetch Priority API 【MDN】瀏覽器渲染流程","link":"/Frontend/Html/Analyzing-Preconnect-Preloading-Fetch-Priority/"},{"title":"【Html】解析 <script> 載入機制與 defer、async 屬性","text":"本篇重點 <script> 的載入與執行特性 defer 和 async 的功能與特性 type="module" 與 defer 的關係 <script> 的載入與執行機制HTML 解析器在遇到 <script src="xxx.js"> 時,預設行為是: 暫停 HTML 解析 下載 script 檔案 立即執行 script 繼續解析 HTML 因此若未加上任何屬性(即同步載入),每個 script 標籤都可能造成主執行緒中斷,延後頁面渲染。 無屬性(同步載入)功能與特性 預設為同步模式,遇到 <script> 時會暫停 HTML 解析,等待下載並執行完成後才繼續解析。 若放在 <head>,可能導致白屏延遲(render-blocking)。 使用時機適合需要立即執行或初始化頁面邏輯的腳本。 在 DOM 尚未完全建立前,就必須修改或偵測初始狀態的功能(例如 polyfill 引入)。 須確保執行順序固定的腳本。 範例: polyfill.js 的執行會阻塞後續的 HTML 載入。 html123<head> <script src="polyfill.js"></script></head> 健忘筆記 polyfill 能夠產生舊瀏覽器所支援的程式版本,讓舊瀏覽器可以使用新的功能。 defer 屬性功能與特性 表示 script 延遲執行,直到整份 HTML 解析完成。 在解析 HTML 的同時開始非同步下載腳本,等到 DOM 結構建立完畢後才執行。 如果有多個帶 defer 的 script,會依照在文件中的出現順序執行。 執行時機在 DOMContentLoaded 事件前。 健忘筆記 DOMContentLoaded 事件和 Load 事件 DOMContentLoaded 執行時機在整個 DOM 樹建構完成後(不含圖片、CSS、iframe 等外部資源) load 執行時機在所有資源都載入完成(包含圖片、CSS、iframe、JS等) 使用時機適合需要存取或操作 DOM 元素的程式 範例 DOM 已解析完成,main.js 可以安全地操作 DOM。 html1<script defer src="main.js"></script> 執行順序 html1234<p>Start parsing...</p><script defer src="a.js"></script><script defer src="b.js"></script><p>Parsing continues...</p> HTML 解析不中斷 a.js、b.js 同步下載 HTML 完成解析後,依序執行 a.js → b.js async 屬性功能與特性 表示該 script 非同步載入與執行,不會阻塞 HTML 解析。 載入完成立即執行,不會等待 DOM 完成。 如果有多個帶 async 的 script,執行順序不保證和文件檔案順序一致,取決於哪個 script 先下載完。 使用時機適合與頁面內容無依賴關係的外部腳本,例如: 網站統計工具(例如:Google Analytics) 廣告追蹤腳本 第三方 SDK 範例: adsbygoogle.js 會在下載完成的那一刻立刻執行,不干擾主要頁面邏輯。 html1<script data-ad-client="ca-pub-11111" src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js" async></script> type="module" 與 defer 的關係當使用 <script type="module"> 時,模組腳本預設具有 defer 的行為: html1<script type="module" src="app.js"></script> 因此模組不會阻塞 HTML 解析,會在 DOM 解析完成後執行。 結論 屬性 是否阻塞 HTML 解析 下載時機 執行時機 多腳本執行順序 適用情境 無屬性 是 立即下載 下載完成即執行 順序固定 需立即執行、初始化頁面邏輯 defer 否 與解析並行 DOM 完成後執行 順序固定 需操作 DOM 的主程式 async 否 與解析並行 下載完成即執行 順序不定 無依賴、獨立腳本 defer 與 async 都是為了避免傳統 <script> 阻塞頁面載入所設計,根據腳本間的相依性與執行時機選擇: 若腳本需操作 DOM 或依賴其他腳本 → 使用 defer 若腳本獨立且不影響頁面主流程 → 使用 async 若需立即執行初始化邏輯 → 可保留同步載入 延伸閱讀 【MDN】<script> 介紹 【web.dev】defer 與 async 介紹","link":"/Frontend/Html/script-loading-defer-async/"},{"title":"【Regex】解析非捕獲分組與斷言語法","text":"本篇重點 說明擴充型分組的用途 說明非捕獲分組 ?: 的用途 說明先行斷言 ?=、?!、後行斷言 ?<=、?<! 的用途與差異 非捕獲分組有匹配但不記錄分組結果 斷言只檢查條件不匹配任何內容 擴充型分組以 (? 開頭,屬於正規表達式的「擴充型分組(Extended Group)」語法,主要用途是改變群組行為或設定匹配條件。 類型 語法 名稱 是否匹配字元 是否捕獲內容 一般分組 (pattern) capturing group ✅ ✅ 非捕獲分組 (?:pattern) Non-capturing group ✅ ❌ 斷言(先行/後行) (?=...)、(?!...)、(?<=...)、(?<!...) Lookaround assertion ❌ ❌ 非捕獲分組非捕獲分組(Non-capturing Group)是讓正規式結構化或重複某段規則,但不捕獲分組結果,仍然會參與實際的匹配過程,只是不會佔用群組索引或儲存結果。 使用時機(?:pattern) 建立條件組合但不希望影響 $1、$2 群組索引。 群組僅為結構性用途(如限定重複次數或選擇): 限制重複次數 你需要讓一段模式重複多次時,必須用括號把那段包起來: js12const regex = /(?:ab){2}/;console.log("abab".match(regex)); // ["abab"] 這裡 (ab){2} 表示「ab 重複兩次」,若不用括號 /ab{2}/ 則表示「a 後面接兩個 b」→ “abb”,使用 (?:ab){2} 而不是 (ab){2} 是因為不需要捕獲 “ab”,只是想讓整段重複。 選擇(alternation)用途 當你用 |(或條件)時,常需要把多個選項包起來: js123const regex = /^(?:cat|dog)s?$/;console.log("cats".match(regex)); // ["cats"]console.log("dog".match(regex)); // ["dog"] (?:cat|dog) 表示「cat 或 dog」,s? 表示可選的複數 s,這樣寫是為了讓正則語法正確運作,並且不需捕獲 “cat” 或 “dog”。 範例 js12345const str = "2025-11-05, 2024-07-20";const regex = /(?:\\d{4})-(\\d{2})-(\\d{2})/;const match = str.match(regex);console.log(match); // ["2025-11-05", "11", "05"] 說明: 年份以 (?:...) 包起來,只是為了表達結構,不需要在結果中取出,因此不建立捕獲群組,但有匹配字元。 動手做做看 斷言斷言(Assertions, Lookaround)屬於零寬度匹配(zero-width match),也就是只檢查字串中「某個位置的前後條件」,不消耗字元、也不會出現在結果中。 斷言可分為四種: 類型 語法 檢查方向 條件 正向先行斷言 (?=pattern) 後方 必須符合 負向先行斷言 (?!pattern) 後方 不得符合 正向後行斷言 (?<=pattern) 前方 必須符合 負向後行斷言 (?<!pattern) 前方 不得符合 正向先行斷言(?=pattern) 檢查某位置後方是否符合指定條件。 若符合,該位置可被匹配,但 pattern 本身不會納入結果。 範例 js123const str = "商品A 100元, 商品B 200元, 商品C 300日圓";const regex = /\\d+(?=元)/g;console.log(str.match(regex)); // ["100", "200"] 說明 匹配「後面接著『元』」的數字,排除「日圓」的數字。 動手做做看 負向先行斷言(?!pattern) 檢查某位置後方不得符合指定條件。 範例 js123const str = "apple pie, apple tart, apple juice";const regex = /apple(?!\\start)/g;console.log(str.match(regex)); // ["apple", "apple"] 說明 /apple(?!\\start)/g 的 \\s 是空白的意思。 僅匹配後方不是 “ tart” 的 “apple”。 動手做做看 正向後行斷言(?<=pattern) 檢查某位置前方是否符合指定條件,若符合,即可從該位置開始匹配。 範例 js123const str = "JPY300, USD120, USD500";const regex = /(?<=USD)\\d+/g;console.log(str.match(regex)); // ["120", "500"] 說明 只取出「前方是 USD」的數字。 動手做做看 負向後行斷言(?<!pattern) 檢查某位置前方不得符合指定條件。 範例 js123const str = "VIP123, GUEST456, USER789";const regex = /(?<!VIP)\\d+/g;console.log(str.match(regex)); // ["23", "456", "789"] 說明 斷言只檢查匹配開頭的位置,並不會讓整段文字「整體排除」,因此會在 "VIP123" 的第 4 個位置(2)發現條件成立(前方不等於 VIP),開始匹配數字,得到 "23"。 動手做做看 非捕獲分組與斷言的差異 比較項目 非捕獲分組 (?:pattern) 斷言 ( ?= / ?! / ?<= / ?<! ) 是否參與匹配 ✅ 有實際匹配字元 ❌ 不匹配字元(零寬度) 是否捕獲內容 ❌ 不捕獲 ❌ 不捕獲 主要功能 結構化表達式、不記錄群組 檢查前後條件 實際應用範例 建立非捕獲群組以控制重複或選擇 根據上下文條件限制匹配範圍 範例 js1234567const str = "Price: $100, €200, $300";// 非捕獲分組console.log(str.match(/(?:\\$)\\d+/g)); // ['$100', '$300']// 斷言console.log(str.match(/(?<=\\$)\\d+/g)); // ['100', '300'] 非捕獲分組 → 有匹配、但不記錄分組結果。 斷言 → 只檢查條件、不匹配任何內容。 動手做做看 結論非捕獲分組與四種斷言語法是讓正規表達式更具彈性的重要工具: (?:pattern):非捕獲分組,用於結構化匹配但不取值。 (?=pattern)、(?!pattern):先行斷言,用於判斷後方條件。 (?<=pattern)、(?<!pattern):後行斷言,用於判斷前方條件。 延伸閱讀 【regex101】regex 正規表達式查詢工具 【MDN】regex 正規表達式介紹","link":"/Other/regex-non-capturing-and-assertions/"},{"title":"【Develop】解析 CSR / SSR / SSG / ISR 網頁渲染架構","text":"本篇重點 了解客戶端渲染 (CSR)、伺服器端渲染 (SSR)、靜態網站生成 (SSG) 以及增量靜態再生(ISR) 的工作流程與渲染機制 了解 ISR 的生成範圍、再生成觸發條件 網頁渲染趨勢朝向混合渲染 (Hybrid Rendering) 發展 渲染架構決策樹 網頁應用程式的渲染(Rendering)架構,決定了內容從伺服器到使用者瀏覽器呈現的流程。每個模式各有其設計目的、工作流程與性能特性,選擇合適的架構對於網頁的載入速度、使用者體驗與搜尋引擎優化 (SEO) 非常重要。 CSR 客戶端渲染Client-Side Rendering 概念伺服器回傳最精簡的 HTML(無內容),瀏覽器下載 JS 後於前端渲染 DOM(生成內容),將內容的生成任務交給瀏覽器。 工作流程 使用者發送請求。 回傳 -> 載入精簡 HTML。 JS bundle 下載。 前端執行 API 請求。 完成渲染。 優點 首次加載後,應用程式內頁面切換極快,因為瀏覽器不需重新請求整個頁面,僅需獲取數據。 低伺服器負載,伺服器只需提供 API 服務。 缺點 首次內容繪製 (FCP) 較慢,因為瀏覽器需要等待 JavaScript 載入並執行才能看到內容。 SEO 不佳,因為搜尋引擎爬蟲可能無法完整抓取渲染後的內容。 使用時機適用於高度動態、需要大量使用者互動且內容更新頻繁的應用(SPA 型應用),例如:儀表板應用、企業內部系統、SaaS 服務的管理介面,需要大量互動的應用。 SSR 伺服器端渲染Server-Side Rendering 概念每次請求都由伺服器即時執行完整的應用程式邏輯、獲取數據,並將渲染好的完整 HTML 返回瀏覽器,再載入並執行 JavaScript,讓頁面可以互動。 工作流程 使用者發送請求。 伺服器讀取資料。 渲染 HTML。 回傳頁面。 前端執行 JavaScript。 優點 可即時取得動態資料。 SEO 友善,搜尋引擎爬蟲能直接讀取完整的 HTML 內容。 缺點 互動有延遲,使用者雖然看到內容,但 JavaScript 執行前無法互動。 伺服器負載重,每次請求都需要重新渲染。 使用時機適用於內容是核心、強調 SEO 且需要即時數據的網站,例如:電子商務網站的商品頁面(價格與庫存需即時更新)、新聞網站、需要強大 SEO 表現的行銷網站。 SSG 靜態網站生成Static Site Generation 概念在構建 (build) 階段渲染生成對應的完整 HTML 文件、CSS 和 JavaScript 資源,部署到 CDN 後內容固定。當使用者請求頁面時,CDN 直接返回預先生成好的靜態文件。 工作流程 Build 時取得資料。 產生靜態 HTML、CSS 和 JavaScript。 使用者從 CDN 直接下載。 優點 SEO 表現優秀。 回應速度最快。 伺服器幾乎無運算負載。 缺點 內容更新需要重新構建 (Rebuild) 整個網站並重新部署。 頁面量大時 build 時間增加。 使用時機適用於內容不常變動且追求高性能和低成本的網站,例如:資訊穩定的文件、部落格文章頁面。 健忘筆記 CDN(內容傳遞網路,Content Delivery Network)是一種分散式伺服器網路系統,其核心目的是加速網站內容的傳輸速度,讓全球各地的使用者都能快速存取網站內容。 ISR 增量靜態再生Incremental Static Regeneration 概念SSG 的擴充機制,解決 SSG 內容更新需要全局重新構建的問題,設定重新驗證間隔 (Revalidation Time) 後,可在背景更新靜態頁面。 工作流程 Build 時取得資料。 產生靜態 HTML、CSS 和 JavaScript。 首次請求取得舊的靜態檔案。 若超過設定時間(revalidate),伺服器在背景重新生成頁面。 新版本靜態檔案生成後替換舊版本。 下次請求開始使用最新靜態檔案。 生成範圍 單頁或局部區塊。只重新生成需要更新的特定頁面或區塊,而不是重新構建整個網站,這大幅減少了構建時間和資源消耗。 Regenerate 觸發方式 基於時間 (Time-Based):透過設定時間參數 revalidate,頁面在首次生成後會在指定的時間內(例如 60 秒)保持靜態。當時間到期後,下一次請求該頁面時,系統會在後臺觸發重新生成,並將更新的版本提供給後續使用者。若在時間到期後一直沒有使用者訪問該頁面,那麼頁面就會持續保持舊版本,直到下一次請求出現為止。 基於事件 (On-Demand):通過 Webhook 或 API 呼叫,由 CMS 或後端服務主動通知應用程式進行特定頁面的再生。 健忘筆記 Regenerate 過程是在背景執行,使用者體驗不會因內容更新而中斷,能持續正常使用。 優點 保留 SSG 的快取速度與 CDN 優勢。 可定期更新資料(如 10 秒、1 分鐘)。 無需全局重新部署。 缺點 更新速度不是即時。 有機會出現短暫的舊資料。 使用時機適用於內容會定期變更,但不需要即時更新,且需要保持高性能的網站,例如:大型內容網站 (數萬頁面但不需立即渲染)。 渲染模式比較 特性 CSR SSR SSG ISR 何時產生 HTML 客戶端產生 每次請求 build build + 背景重建 首次顯示速度 慢 中 最快 快 SEO 弱 強 強 強 更新能力 即時 即時 無 定期/手動 特點 高互動 SPA 適合動態資料 適合穩定內容 大量頁面+需更新 混合渲染概念當前的網頁渲染趨勢並非單一地偏向某一種架構,而是朝向混合渲染 (Hybrid Rendering) 發展,現代 Web 框架(Next.js、Nuxt)強調依頁面需求混合使用多種渲染方式。 同一個網站可同時包含: 高互動 SPA 區塊(CSR) 即時內容頁面(SSR) 靜態頁面(SSG) 定期更新頁面(ISR) 這種方式稱為「混合渲染」。 優點 效能最佳化,為每個頁面選擇最適合的渲染方式。 提升使用者體驗,關鍵頁面(如行銷頁、登陸頁)載入快,而互動性強的頁面(如儀表板)保持流暢。 確保需要 SEO 的內容得到有效索引 降低伺服器負載 缺點 架構複雜,必須理解多種渲染方式的差異,部署流程也更複雜。 需要更多觀察與監控,ISR 生成是否失敗,SSR 是否有延遲,CSR 客戶端 JS 是否過大 使用時機 Blog / 行銷頁:SSG 搭配 CDN 以提升首次載入速度,少數需要更新的頁面再以 ISR 生成。 電商平台:商品列表與 SEO 重要頁面用 SSG 或 ISR,而購物車與結帳流程因涉及個人化與即時資料,多以 CSR 或 SSR 處理。 新聞網站:SSR 或 ISR 生成首頁以快速回應流量高峰,文章頁面則透過 ISR 兼顧速度與更新頻率。 使用者儀表板:若需要搜尋引擎可見性可選擇 SSR,若僅作為登入後內部使用則以 CSR 搭配 API 載入資料即可。 文件站(Docs):SSG,提供高效、穩定且易於部署的架構。 渲染架構決策樹 是否需要 SEO 或首次可見內容非常重要? SSG / ISR / SSR(若需要更新即時,使用 SSR 或 ISR)。 內容是否高度個人化? SSR 或 CSR(若 SEO 重要 → SSR;若僅內部 app → CSR)。 內容更新頻率高,但想要靜態快取的好處? ISR。 頁面數量超多(數萬頁)且不能每次 rebuild? ISR。 管理後台、內部工具或高度互動的 UI? CSR。 結論依需求使用最適渲染策略,透過靜態生成、背景再生、伺服器渲染與客戶端渲染的搭配,讓網站能同時達到速度、SEO、維運彈性與互動性。 延伸閱讀 【web.dev】Rendering on the Web 【MDN】Client-side Rendering / Server-side Rendering","link":"/Frontend/ssr-ssg-isr-csr-rendering-modes/"},{"title":"【GitHub、Hexo】jsDelivr CDN 加速 GitHub Pages 資源及 Hexo 實作","text":"本篇重點 使用 jsDelivr CDN 串接 GitHub Pages 資源,延長快取時長、加速載入 了解 GitHub Pages 與 jsDelivr 在快取、全球節點、資源更新速度上的差異 了解 jsDelivr CDN 串接原理與版本控制 使用 Hexo 過濾器實現本地資源重寫 使用自動化佈署腳本與 CDN Tag 機制實現佈署後強制更新快取 jsDelivr CDN一個免費、開源的公共 CDN,專門用於託管 GitHub 倉庫、npm 包等資源,並提供全球多個節點 + 彈性快取 + fallback 機制。 健忘筆記 CDN(內容傳遞網路,Content Delivery Network)是一種分散式伺服器網路系統,其核心目的是加速網站內容的傳輸速度,讓全球各地的使用者都能快速存取網站內容。 串接 GitHub Pages 的運作方式支援透過特定的 URL 結構來直接存取 GitHub 倉庫中的文件。 URL 結構: jsdelivr cdn123456789https://cdn.jsdelivr.net/gh/使用者名稱/倉庫名稱@版本號/文件路徑// 範例https://cdn.jsdelivr.net/gh/forgetfulengineer/forgetfulengineer.github.io@v-20251208-172935/gallery/covers/Handling-Case-Sensitivity-Issues-in-GitHub-Deployment.webphttps://cdn.jsdelivr.net/gh/forgetfulengineer/forgetfulengineer.github.io@lateset/gallery/covers/Handling-Case-Sensitivity-Issues-in-GitHub-Deployment.webp// 無使用版本號,會自動存取主分支的資源,例如:@mainhttps://cdn.jsdelivr.net/gh/forgetfulengineer/forgetfulengineer.github.io/gallery/covers/Handling-Case-Sensitivity-Issues-in-GitHub-Deployment.webp @版本號 參數可為 Git 分支名、Commit SHA 或 Tag 名稱。此參數用於鎖定特定版本的資源,是實現 CDN 快取強制更新(Cache Busting)的核心機制。 特性: 從 GitHub API / 原始檔案擷取資料。 調用的是 GitHub 資源,但由 jsDelivr 分發,避免 GitHub 本機延遲。 若使用 tag 或 commit hash,jsDelivr 視為不可變版本,會使用長效快取(例如 max-age 1 年)。 若使用分支(如 main、latest),屬於「可變內容」,會使用短效快取(例如 max-age 600 秒)。 整合多家 CDN,若某一節點故障,自動 fallback 至其他節點,提升可用性與抗故障能力。 適合用於靜態資源 (CSS, JS, 圖片),利用 Multi-CDN 優勢最大化下載速度。 詳細的 jsDelivr CDN 文件可查看【jsDelivr 官方文件】Open Source CDN 簡介 GitHub Pages 資源 vs jsDelivr CDN GitHub Pages jsDelivr CDN CDN 架構 單一主要供應商 (Fastly)。 Multi-CDN (多重 CDN 聚合)。整合了 Cloudflare、Fastly、Bunny 等多個供應商。 節點覆蓋 Fastly POP(edge location)服務,依訪問者地理位置優化。 整合多家 CDN(Cloudflare、Fastly、Bunny 等),提高緩存命中率與可用性。 快取政策 快取策略較短(例如 max-age 600秒)。 依 CDN 版本號決定快取策略,支援 immutable tag 機制對應長效快取(例如 max-age 1 年),對開源專案資源尤其友好。 版本更新 靜態內容更新後由預設機制重新佈署與快取刷新。 透過更新資源 URL(如新的 tag)實現版本鎖定與 cache busting。 故障轉移 無。 支援自動 Failover(Cloudflare + Fastly + Bunny 等)提供高穩定性。 適用場景 網站主體 HTML 文件、動態更新頻繁的內容。 靜態資源(圖片、CSS、JS)。利用 Multi-CDN 優勢最大化下載速度。 健忘筆記 GitHub Pages 本身透過 Fastly 作為 CDN 層提供全球分發與快取,因此具備一定的延遲優化與邊緣緩存。jsDelivr CDN 的優勢在於提供「版本鎖定 + 公共 CDN」、「多供應商 fallback」、「immutable 快取」等特性,並且台灣用戶因 jsDelivr 結合 Cloudflare、Bunny 節點能增加命中率與載入速度。 【jsDelivr】Failover 備緩機制 【jsDelivr】快取政策說明 【jsDelivr】多 CDN 供應商整合 Hexo 實作方式 目標是透過 Hexo 過濾器(Filter)在網站生成時,將 HTML 文件中指向本地靜態資源的相對路徑替換為 jsdelivr CDN 的絕對路徑。 過濾器重寫資源 URL_config.yml 設定檔新增 rewrite_local_asset 設定。 _config.yml >folded123rewrite_local_asset: enable: true prefix: 'https://cdn.jsdelivr.net/gh/使用者名稱/倉庫名稱' 新增腳本(rewrite_local_asset.js,以下稱 “重寫腳本”)放在 scripts 資料夾下,讓 Hexo 初始化時自動執行。 rewrite_local_asset.js >folded123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596/** * 本地資源重寫腳本 * 使用 Cheerio 解析 HTML,將 / 開頭的路徑改寫為 jsdelivr CDN prefix */'use strict';const cheerio = require('cheerio');hexo.extend.filter.register('after_render:html', function (html) { // 本地開發模式不重寫 if (['s', 'server'].includes(hexo.env.cmd)) return html; const config = hexo.config.rewrite_local_asset || {}; if (!config.enable || !config.prefix) return html; let prefix = config.prefix.replace(/\\/+$/, ''); if (process.env.CDN_TAG) { prefix = prefix.includes('@') ? prefix.replace(/@([^\\/]+)/, `@${process.env.CDN_TAG}`) : `${prefix}@${process.env.CDN_TAG}`; } if (!prefix) return html; const $ = cheerio.load(html, { decodeEntities: false }); /** * 通用重寫與 Fallback 邏輯 * @param {object} el - Cheerio 元素物件 * @param {string} attrName - 屬性名稱 (src 或 href) */ const rewriteAndSetFallback = (el, attrName) => { const $el = $(el); const originalUrl = $el.attr(attrName); // 如果 URL 為空或不是以 / 開頭的 URL均不處理 if (!originalUrl || !originalUrl.startsWith('/')) return; // 將本地路徑轉換為 CDN 路徑 const cdnUrl = prefix + originalUrl; // Fallback 設置 // 當 CDN 載入失敗時(onerror handler),將 src/href 切換回本地資源 $el.attr('onerror', `this.onerror=null;this.setAttribute('${attrName}', '${originalUrl}');`); // 設置重寫後的 CDN URL $el.attr(attrName, cdnUrl); }; /** * 處理腳本 (Script) 的 Fallback 邏輯 */ const handleScriptFallback = (el) => { const $el = $(el); const originalUrl = $el.attr('src'); if (!originalUrl || !originalUrl.startsWith('/')) return; const cdnUrl = prefix + originalUrl; const fallbackJs = [ "this.onerror=null;", "var s=document.createElement('script');", `s.src='${originalUrl}';`, "document.head.appendChild(s);", "this.remove();" ].join(''); $el.attr('onerror', fallbackJs); $el.attr('src', cdnUrl); }; $('img').each((i, el) => rewriteAndSetFallback(el, 'src')); $('link').each((i, el) => { const $el = $(el); const rel = $el.attr('rel'); // 排除 Favicon、Icons、Sitemap、Alternate 等特定 rel 屬性 if (rel && /icon|apple-touch-icon|sitemap|manifest|alternate/i.test(rel)) { return; } rewriteAndSetFallback(el, 'href'); }); $('script').each((i, el) => { // 排除沒有 src 的 Inline Script if ($(el).attr('src')) { handleScriptFallback(el); } }); return $.html();}); 說明 從 _config.yml (hexo.config.rewrite_local_asset) hexo.config.rewrite_local_asset 取得重寫資源設定。 重寫腳本註冊 after_render:html 過濾器,在 Hexo 將 Markdown 轉換為 HTML 之後執行。 重寫腳本解析生成的 HTML(使用 Cheerio),針對所有以 / 開頭 (root-relative) 的資源 (例如 <link href="/css/style.css">) 進行 rewrite。 注入 CDN Tag: 腳本首先檢查環境變數 process.env.CDN_TAG。如果存在,它會將這個 Tag 名稱(例如 v-20251208-140000)注入到 CDN 的 URL Prefix 中,實現版本控制。 CDN URL 生成: 對於符合條件的本地路徑 originalUrl,新的 CDN URL cdnUrl 會被構建為 prefix + originalUrl。 範例: 如果 prefix 為 https://cdn.jsdelivr.net/gh/user/repo@v-latest,originalUrl 為 /images/logo.png,那 cdnUrl 會是 https://cdn.jsdelivr.net/gh/user/repo@v-latest/images/logo.png。 避免 <img> 或 <link> CDN 故障 (cdn 回傳 404 或 downtime),對所有被 rewrite 的元素加上 onerror fallback: 1$el.attr('onerror', `this.onerror=null;this.setAttribute('${attrName}', '${originalUrl}');`); this.onerror=null 防止在回退到原始路徑後若仍失敗導致無限循環。 將元素的 src 或 href 屬性切換回原始的本地相對路徑 (originalUrl),確保網站的可用性。 排除特定 rel 屬性的 <link> 元素是為了減少 SEO、快取策略以及跨來源安全限制(CORS)的問題。 避免 <script> CDN 故障 (cdn 回傳 404 或 downtime),對所有被 rewrite 的元素加上 onerror fallback: 和 <img> 或 <link> 不同,當瀏覽器解析到 <script> 後,無論下載成功還是失敗(觸發 onerror),該標籤的生命週期在瀏覽器眼裡已經「結束」了,因此用 document.createElement('script') 的方式,讓瀏覽器將其視為全新的任務處理。 自動化佈署腳本與 CDN 版本控制新增腳本(deploy.sh,以下稱 ‘佈署腳本’)放在 Hexo 專案資料夾下,負責網站的清理、生成、佈署,以及最重要的 CDN 快取強制更新機制。 deploy.sh >folded12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576#!/bin/bash# GitHub Pages repository 的 URLREPO_URL="https://github.com/使用者名稱/倉庫名稱"USER_NAME="使用者名稱"USER_EMAIL="使用者信箱"FIRST_DEPLOY=0# 產生本次佈署的 Tag 名稱 (格式: v-YYYYMMDD-HHMMSS)CDN_TAG="v-$(date +'%Y%m%d-%H%M%S')"# 檢查 .deploy_git 資料夾是否存在echo -e "\\n檢查 .deploy_git 資料夾是否存在..."if [ -d ".deploy_git" ]; then # 切換到 .deploy_git 資料夾並拉取最新進度 cd .deploy_git || { echo -e "\\n切換到 .deploy_git 資料夾失敗"; exit 1; } echo -e "\\n拉取 github page 最新進度..." git pull || { echo -e "\\n拉取進度失敗"; exit 1; } cd .. || { echo -e "\\n返回 Hexo 資料夾失敗"; exit 1; }else # 檢查 GitHub Pages repository 是否已有 commit echo -e "\\n檢查 GitHub Pages repository 是否已有 commit..." if git ls-remote --exit-code "$REPO_URL" HEAD > /dev/null 2>&1; then echo -e "\\nClone repository 到 .deploy_git 資料夾..." git clone "$REPO_URL" .deploy_git || { echo -e "\\nClone repository 失敗"; exit 1; } # 設置 git config cd .deploy_git || { echo -e "\\n切換到 .deploy_git 資料夾失敗"; exit 1; } git config --local user.name "$USER_NAME" git config --local user.email "$USER_EMAIL" git config --local core.ignorecase false cd .. || { echo -e "\\n返回 Hexo 資料夾失敗"; exit 1; } else echo -e "\\nGitHub Pages repository 尚無 commit,進行首次佈署" FIRST_DEPLOY=1 fifi# 清理舊的生成文件echo -e "\\n清理舊的生成文件..."hexo clean || { echo -e "\\nHexo 清理失敗"; exit 1; }# 將 CDN_TAG 注入環境變數,讓 rewrite_local_asset.js 讀取export CDN_TAG="$CDN_TAG"# 生成新的靜態文件並佈署網站echo -e "\\n生成新的靜態文件並佈署網站..."hexo g || { echo -e "\\nHexo 生成失敗"; exit 1; }hexo d || { echo -e "\\nHexo 佈署失敗"; exit 1; }# 對佈署的 Commit 打 Tagecho -e "\\n正在對 GitHub Pages 建立 Tag: ${CDN_TAG}..."cd .deploy_git || { echo -e "\\n無法進入 .deploy_git,Tag 建立失敗"; exit 1; }# 確保在最新的 commit 上LATEST_COMMIT=$(git rev-parse HEAD)echo -e "\\n最新 Commit: ${LATEST_COMMIT}"# 建立 Taggit tag -a "$CDN_TAG" $LATEST_COMMIT -m "Release: $LATEST_COMMIT"# 推送 Tag 到遠端echo -e "\\n推送 Tag 到 GitHub..."git push origin "$CDN_TAG"cd ..# 如果是首次佈署,進入 .deploy_git 設置 git configif [ "$FIRST_DEPLOY" -eq 1 ]; then echo -e "\\n首次佈署,設置 git config --local core.ignorecase false" cd .deploy_git || { echo -e "\\n切換到 .deploy_git 資料夾失敗"; exit 1; } git config --local core.ignorecase false cd .. || { echo -e "\\n返回 Hexo 資料夾失敗"; exit 1; }fiecho -e "\\n佈署完成" 說明 產生唯一的 CDN_TAG:腳本在佈署開始時,自動生成唯一且代表當前版本的 CDN_TAG,格式為 v-YYYYMMDD-HHMMSS。 注入環境變數:export CDN_TAG="$CDN_TAG" 將 Tag 注入到環境變數中,供重寫腳本讀取並整合進 CDN URL。 將該 CDN_TAG 設為環境變數,供重寫腳本使用,以產生對應版本的 CDN URL。 生成並佈署網站:hexo g (Generate) 執行時,重寫腳本會使用最新的 CDN_TAG 重寫所有本地資源路徑。 建立並推送 Git Tag:在 Hexo 成功佈署,將靜態文件推送到 GitHub Pages 倉庫後,腳本在 .deploy_git 資料夾內,對最新的 Commit 建立並推送該 CDN_TAG 到遠端 GitHub 倉庫。 CDN URL 隨版本改變(tag 不同),實現「Cache Busting」策略:舊版資源仍可被 cache,但新版資源 URL 不同,使用者會重新拉取最新資源,實現強制更新,避免被舊資源覆蓋或快取污染。 只說明 deploy.sh Tag 邏輯與 CDN 快取更新的部分,其他流程說明可以到【Hexo】多台電腦佈署 GitHub page 的問題。 查看 結論雖然 GitHub Pages 本身已透過 Fastly 提供優秀的 CDN 服務,但透過 jsdelivr 的 Multi-CDN 架構與 Hexo 自動化腳本的整合,可以進一步優化靜態資源的交付策略。 此方案的核心價值在於「資源版本控制」:利用自動化的 Git Tag 機制,確保每次佈署的資源都擁有唯一的 URL,解決了靜態資源的快取更新問題,更讓網站能享受到 jsdelivr 針對資源提供的長效快取,同時配合 onerror 回退機制確保了服務的高可用性。 延伸閱讀 【jsDelivr 官方文件】Open Source CDN 簡介 【Fastly 官方文件】GitHub 與 Fastly 合作提供 CDN 的說明 【Hexo 官方文件】filter 過濾器介紹 【MDN】onerror 事件","link":"/Other/GitHub/jsdelivr-cdn-github-pages-hexo-acceleration-tutorial/"},{"title":"【Develop】解析 Reverse Proxy 與 Forward Proxy","text":"本篇重點 Reverse Proxy 反向代理是在保護與加速後端伺服器,扮演對外單一入口。 Forward Proxy 正向代理是用於存取控制與匿名化,扮演用戶端請求的單一出口。 解析兩種代理伺服器的工作流程、用途、使用時機。 了解 Reverse Proxy 和 Load Balancer 的關係。 了解 Forward Proxy 和 VPN 的相似之處。 Reverse Proxy反向代理 用途部署在網站伺服器之前,作為用戶端與內部伺服器群之間的單一對外入口。它的核心價值在於提供安全性、加速效能以及集中式管理。對於用戶端而言,所有請求都只與反向代理溝通,沒辦法得知後端伺服器真實的位址、數量與架構細節。 隱藏後端伺服器 IP 集中處理 SSL/TLS(HTTPS) 進行快取以減少後端負擔 壓縮、控制與檢查請求 作為後端服務的流量入口(Gateway) 工作流程 外部請求安全、高效得導向內部資源: 用戶端發送請求至 Reverse Proxy 的公網 IP 地址(例如:api.example.com)。 Reverse Proxy 接收請求,根據預設的規則或負載平衡算法,決定將該請求導向至哪一個後端 Web 伺服器。 後端 Web 伺服器處理請求並生成回應。 後端 Web 伺服器將回應傳送回 Reverse Proxy。 Reverse Proxy 將回應傳送回原始的用戶端。 健忘筆記 用戶端永遠只看到 Reverse Proxy 的 IP 地址,後端伺服器的 IP 地址被隱藏。 使用時機 應用情境 核心功能 具體範例 安全性強化 屏蔽後端伺服器,過濾惡意請求。 在 Reverse Proxy 上配置 WAF (Web Application Firewall) 來抵禦 SQL 注入或 DDoS 攻擊。 負載平衡與擴展 將流量平均分配給多個後端伺服器,實現高可用性。 使用 Nginx 將流量平均分散到三台處理應用程式邏輯的 App Server 上。 內容快取與加速 快取靜態或常用內容,減少對後端伺服器的負載。 快取網站的 CSS、JavaScript 文件或不常變動的產品頁面 HTML,以提升首次載入速度。 和 Load Balancer 的關係Load Balancer(負載平衡器)是專門做「流量分配」的工具,用於在多台伺服器之間平均分散請求,並確保應用程式的高可用性,是 Reverse Proxy 的子集概念,專門聚焦在「流量調度」。 防止單一伺服器過載 依照策略選擇後端(Round Robin、Least Connection 等) 自動把流量導到正常的伺服器(故障時跳過壞掉的) 健忘筆記 反向代理和負載均衡器可以同時配置在同一台機器上。 Forward Proxy正向代理 用途部署在用戶端的網路環境中,作為內部網路發出請求時的單一出口,進行存取控制、過濾以及提供匿名性。對於外部的目標伺服器而言,所有請求都來自 Forward Proxy 的 IP 地址,無法知道實際發起請求的用戶端資訊。 控制用戶訪問特定外部服務 實作網路管控 隱藏用戶端 IP 工作流程 用戶端透過代理存取外部資源: 用戶端配置代理伺服器地址,發送請求至 Forward Proxy。(請求中包含完整的目標資源地址,例如:GET https://www.google.com/) Forward Proxy 接收請求,進行策略檢查(例如:檢查是否在黑名單網站上)。 檢查通過後,Forward Proxy 以自身的身份向目標伺服器發送請求。 目標伺服器處理請求並將回應傳送回 Forward Proxy。 Forward Proxy 接收回應並將其傳送回原始的用戶端。 使用時機 應用情境 核心功能 具體範例 網路存取控制 限制用戶只能存取特定的外部網站或服務。 企業網路禁止員工存取社群媒體或非業務相關的網站。 地理限制繞過 隱藏真實的地理位置 IP,用於存取區域受限的內容。 位於 A 國家的用戶端透過位於 B 國家的 Forward Proxy 存取僅限 B 國家的內容。 流量監控與日誌 集中記錄並審查所有出站的網路流量,用於安全審計。 網路安全團隊透過代理伺服器日誌追蹤是否有資料被洩露到外部伺服器。 和 VPN 的關係VPN (虛擬私人網路) 的核心概念是建立一條加密的私人隧道,將用戶端的網路流量安全地導向遠端網路。 建立加密隧道 將所有流量包在隧道內傳輸 保護封包內容不被攔截 對外呈現為 VPN 伺服器的出口 IP 兩者都可用於繞過地理限制和隱藏 IP 地址,但 VPN 提供了更全面的加密保護,涵蓋了設備上所有的網路活動,而不僅僅是瀏覽器或單一應用程式的流量。 健忘筆記 VPN 強調安全隧道;Forward Proxy 強調代理與控制。 結論Reverse Proxy 和 Forward Proxy 雖然都是「代理伺服器」,但網路架構中的角色與核心目的完全相反。 Reverse Proxy (反向代理):保護後端伺服器、加速內容載入、實現負載平衡。 Forward Proxy (正向代理):隱藏用戶端身份、存取控制與過濾、繞過地理限制。 延伸閱讀 【Nginx 文件】 Reverse Proxy 介紹 【Nginx 文件】 load balance 介紹 【Nginx 文件】 使用第三方 forward proxy","link":"/Backend/reverse-proxy-forward-proxy/"},{"title":"【NPM】常用指令速查表","text":"本篇重點 npm 常用指令速查表及各指令的詳細說明 說明 npm 從初始化到安裝到更新到移除等等實用指令 npm install 和 npm ci 的差異 npm(Node Package Manager)是 Node.js 官方提供的套件管理工具,用於安裝、更新、移除與管理 JavaScript 套件。隨著前端與全端生態發展,npm 已成為專案初始化與依賴管理的標準配備。 npm 指令速查表 指令 (縮寫) 核心用途 使用時機 init 初始化專案 建立新專案並產生 package.json 時。 install (i) 安裝套件 初次下載專案或新增第三方函式庫時。 uninstall (un, rm) 移除套件 專案不再需要特定函式庫時。 update (up) 更新套件 需要升級套件版本以獲取新功能或修復時。 run 執行腳本 啟動伺服器、打包建置或執行測試時。 ci 潔淨安裝 在 CI/CD 流程中確保依賴版本與 Lock 檔完全一致時。 ls 檢視套件樹 檢查當前安裝的套件列表與版本結構時。 outdated 檢查過期套件 確認有哪些套件存在新版本可供升級時。 audit 安全稽核 掃描專案依賴是否存在已知的資安漏洞時。 npm init專案初始化 用途與建議 建立 package.json 檔案。 定義專案基本資訊與相依結構。 新專案第一步必用。 已存在 package.json 時不需重複執行。 常用指令 npm init:互動式建立,需逐一回答問題。 npm init -y:跳過問答,直接以預設值建立檔案。 npm install安裝套件(縮寫:npm i) 用途與建議 解析並安裝 package.json 中定義的依賴套件至 node_modules 目錄。 如果 package.json + package-lock.json 都存在,會直接安裝 lock 裡「精確鎖定」的版本,忽略 package.json 的版本設定。 開發工具類套件務必使用 -D,避免增加生產環境的部署體積。 常用指令 npm i:讀取 package.json 安裝所有依賴(預設情況下會同時安裝 dependencies 和 devDependencies 的套件)。 npm i --omit=dev:讀取 package.json 單獨安裝 dependencies 內的套件。 npm i <package>:安裝特定套件並寫入 dependencies(等同於 npm i <package> --save)。 npm i <package> -D:安裝開發專用套件(如 webpack, jest),更新 package.json 寫入 devDependencies。 npm i <package> -g:安裝套件到全域環境(npm 的 global 目錄),通常用來安裝「指令型工具(CLI)」(如 nodemon, npm-check-updates),package.json 和 node_modules 目錄皆不會更新。 npm i <package> --no-save:只安裝到 node_modules 目錄,不會修改 package.json,dependencies / devDependencies 也都不會新增。 npm i <package> -E:鎖定精確版本號(移除 ^ 符號),確保版本完全固定。 健忘筆記 當前的預設行為 (npm v5+)自 2017 年 (v5.0.0) 起,npm install 採取「自動儲存」機制: 自動寫入:套件會自動加進 package.json 的 dependencies 列表。 版本鎖定:同步更新 package-lock.json,確保團隊安裝的版本一致。 目的:防止開發者忘了存檔,導致其他人下載專案後因缺少套件而無法執行。 過去的行為 (npm v5 以前)在舊版本中,npm install 預設「只下載,不紀錄」: 手動存檔:你必須明確加上 --save(或 -S),npm 才會把套件寫進 package.json。 容易出錯:如果忘了加參數,套件只會存在於你電腦的 node_modules 裡;當專案上傳到 Git 後,其他成員執行 npm install 會抓不到該套件,造成開發環境不統一。 npm ci自動化環境安裝 用途與建議 Clean Install,專為自動化環境設計的安裝指令(預設情況下會同時安裝 dependencies 和 devDependencies 的套件)。 嚴格按照 package-lock.json 安装,不會修改 package-lock.json 或 package.json。 版本解析與安裝只看 package-lock.json,所以 package-lock.json 和 package.json 不一致可能導致安裝失敗。 安裝前會先刪除 node_modules。 在 CI/CD Pipeline(如 GitHub Actions, Jenkins)中,應使用 npm ci 取代 npm install,確保部署環境與開發環境完全一致,避免因版本浮動導致的建置失敗。 常用指令 npm ci:先刪除 node_modules,並嚴格依照 package-lock.json 安裝。 安裝時排除 devDependencies在部署生產環境時,應排除 devDependencies,只需安裝 dependencies 的套件就好。 npm12345# 只安裝 dependencies 的套件npm ci --omit=dev# 設定環境變數NODE_ENV=production npm ci 和 npm install 的差異 項目 npm i / npm install npm ci 主要用途 日常開發、新增或更新套件 持續整合 (CI)、部署、自動化測試 是否需要 package-lock.json 不一定需要 一定需要 是否會更新 package-lock.json 可能會更新 不會更新 版本解析方式 依 package.json 版本範圍重新解析 完全依照 package-lock.json node_modules 行為 僅更新差異部分,不會刪除現有資料 先刪除再重裝 安裝速度 較慢(需解析相依) 通常較快 適合環境 本地開發 CI / CD、正式建置 健忘筆記 開發用 npm i,自動化與部署用 npm ci。 npm run執行專案腳本 用途與建議 用於執行 package.json 中 scripts 定義的指令。 將常用指令(如 build、dev、test)標準化,避免手動輸入完整指令。 適合專案開發、建置、測試與部署流程。 若指令名稱為 start、test、restart 或 stop,可省略 run 直接執行(如 npm start)。 常用指令 npm run <script>:執行指定的腳本(如 npm run build)。 npm run:列出目前專案中所有可用的 scripts。 npm uninstall移除套件(縮寫:npm un, npm rm) 用途與建議 從專案中移除已安裝套件。 同步更新 package.json 與 package-lock.json 檔案。 移除前需先搜尋專案是否仍有引用。 常用指令 npm uninstall <package>:移除指定套件 npm uninstall -g <package>:移除指定全域環境套件 健忘筆記 可使用 npm list -g 來查看當前有哪些全域套件。 npm outdated檢查過期套件 用途與建議 檢查目前安裝套件是否有可更新版本。 列出所有有新版本的套件,包含 Current(當前安裝)、Wanted(符合語意化版本範圍的最高版)、Latest(最新版)。 定期維護專案時使用。 常用指令 npm outdated:確認哪些套件存在新版本可升級。 健忘筆記 語意化版本(Semantic Versioning, SemVer)SemVer 格式:MAJOR.MINOR.PATCH MAJOR:不相容的重大變更 MINOR:向下相容的新功能 PATCH:向下相容的錯誤修正 範例:2.4.1 版本符號 ^:允許更新不變更主版本的最新版 ~:允許更新修補版本 *:接受任何版本 範例:^2.4.1、~2.4.1 npm update更新套件(縮寫:npm up) 用途與建議 將套件更新至 Wanted 版本,並修改 package-lock.json(不修改 package.json)。 可使用 npm outdated 查詢 Wanted 版本。 常用指令 npm update:更新套件 npm update <package>:更新指定套件 npm ls檢視套件樹 用途與建議 顯示目前專案的套件相依樹。 用於檢查實際安裝版本與巢狀關係。 套件衝突或版本疑慮時使用。 常用指令 npm ls:列出完整的依賴樹狀圖 npm ls <package>:列出指定套件的依賴樹狀圖 npm ls --depth=0:僅列出第一層(頂層)依賴,可以快速查看專案直接引用的套件版本。 npm audit安全性修復 用途與建議 掃描依賴套件的已知漏洞。 常用指令 npm audit:產出安全性報告。 npm audit fix:嘗試自動修復可相容的漏洞版本。 延伸閱讀 【npm 官方文件】 npm 指令大全 【npm 官方文件】package.json 說明 【semver】語意化版本說明","link":"/Other/NPM/common-commands-cheat-sheet/"},{"title":"【VSCode】自動移除行尾空白","text":"本篇重點 VSCode 可以在存檔時自動移除行尾空白(Trim Trailing Whitespace) 基礎設定為全域生效 settings.json 可進行語言層級的進階控制 Render Whitespace 設定可以輔助檢視空白位置 行尾空白(trailing whitespace)指一行文字結尾處多餘的空白字元(例如:space 或 tab)。 常見來源包含: 編輯時不小心多按空白 貼上程式碼或文件時殘留 自動排版或格式化工具產生 選擇自動移除的原因: 不影響程式行為 容易造成版本控制的無意義 diff 編輯習慣層級的清理設定 透過圖形介面啟用操作步驟: 點擊左下角齒輪裡的設定 搜尋 trim trailing whitespace 勾選 Files: Trim Trailing Whitespace 啟用後效果: 每次存檔時自動移除行尾空白 為全域設定,預設會套用至所有檔案類型 透過 settings.json 設定開啟 VSCode 設定檔 (JSON檔)方法一、使用快捷鍵開啟 使用按鍵 F1 或是 ctrl + shift + p 顯示所有命令 輸入 open settings 選取 Preferences: Open User Settings (Json) 即可開啟設定檔(settings.json) 方法二、從設定中開啟 點擊左下角齒輪裡的設定 點擊右上角的 開啟設定(json) 即可開啟設定檔(settings.json) 新增基礎全域設定在 settings.json 中新增設定,效果和在圖形介面勾選完全相同 settings.json123{ "files.trimTrailingWhitespace": true} 進階應用:針對特定語言停用當全域設定與特定語言需求衝突時,可利用範圍定義功能(Scoped settings)來覆蓋全域行為。 settings.json123456{ "files.trimTrailingWhitespace": true, "[指定語言]": { "files.trimTrailingWhitespace": false }} 全域仍維持自動清理 只會在指定語言檔案中停用 範例 在 Markdown 中,使用者可以根據撰寫習慣,決定是不是要停用自動移除行尾空白的功能。 使用雙換行(Double Enter)控制文章換行:如果習慣用兩次換行符號產生新段落,則可以移除行尾空格,維持全域自動移除設定。 使用行尾雙空格控制文章換行:如果習慣在行末輸入兩個空格來達成「段落內強制換行」,那就必須停用自動移除,以免版面跑版。 settings.json123456{ "files.trimTrailingWhitespace": true, "[markdown]": { "files.trimTrailingWhitespace": false }} 輔助檢視:Render Whitespace用途: 顯示 space、tab 等不可見字元 用來確認行尾是否存在空白 視覺輔助,不影響實際內容或存檔行為 透過圖形介面啟用 開啟設定,搜尋 Render Whitespace 調整空格呈現方式的選項 boundary:僅在行首、行尾、文字間單一空格以上會顯示。 all:顯示檔案中所有的空格點。 透過 settings.json 設定 開啟 VSCode 設定檔 (JSON檔) 新增 Render Whitespace 設定 settings.json123{ "editor.renderWhitespace": "boundary" // 或是 "all"} 延伸閱讀 【VSCode 官方文件】setting 說明 【VSCode 官方文件】markdown 調整說明","link":"/Other/VSCode/trim-trailing-whitespace/"},{"title":"【NPM】解析 npm 安裝和更新的行為","text":"本篇重點 npm install 有無指定版本的差異 npm install 已安裝套件再次安裝的情境 npm 設定檔的分工設計 npm 相依套件的分類 語意化版本(SemVer)、符號(^、~)的實際安裝範圍 npm update 是一個依版本規則自動前進的更新工具 npm update 有無指定套件的差異 npm 降版本需求使用 npm install,不能使用 npm update 先了解這些會幫助你更快理解唷😁 【NPM】常用指令速查表 npm install 行為機制有無指定版本的差異當執行 npm i <package> 指令時,npm 會根據指令後方是否帶有版本號,決定下載策略及對設定檔的變更方式。 未指定版本:預設下載該套件在 npm registry 標記為 latest 的版本(通常是最新穩定版)。安裝後,package.json 預設會在版本號前加上 ^ 符號(例如 "lodash": "^4.17.21"),表示未來允許更新至該大版本下的最新小版本。 指定版本:使用 npm i <package>@<version> 指定精確版本(如 1.2.3),package.json 會移除版本符號,直接記錄該固定數值。若指定版本範圍(如 @1.x),則安裝該範圍內的最新版。 npm1234567# 未指定套件版本npm i lodash# 指定套件版本npm i lodash@4.17.20npm i lodash@4.xnpm i lodash@~4.17 已安裝套件再次安裝的情境 版本一致:node_modules 中的套件已完整符合 package-lock.json 描述的狀態(包含版本、來源與完整性),npm 會續用現有套件不重新下載。 版本不一致(升降級):宣告版本和現有 node_modules 、package-lock.json 不同,npm 會移除舊版並安裝新版,同時更新 package-lock.json。 健忘筆記 有可能出現多版本共存的狀況,這會發生在不同主套件各自依賴不同版本的同名套件。 檔案間的連動關係npm 專案中,透過以下三部分來維持狀態同步: package.json:定義專案「允許使用」的套件版本範圍。 package-lock.json:鎖定專案「實際安裝」的確切版本與依賴樹。 node_modules:存放套件原始碼的實體資料夾。 健忘筆記 通常只會修改 package.json 內容或透過 npm 指令來管理套件,不會去更動 package-lock.json 和 node_modules 的內容。 安裝過程中,npm 會根據 package-lock.json 的狀態決定執行行為。 Lock 檔不存在時: npm 依據 package.json 的版本範圍(如 ^1.0.0)向 Registry 查詢符合條件的最新版本(例如 1.5.0)。安裝後,此確切版本會被記錄在 package-lock.json,但 package.json 的宣告範圍維持不變。 Lock 檔存在時: 範圍一致:若 package.json 的描述與 Lock 檔記錄一致,npm 優先依照 Lock 檔進行安裝,確保環境穩定。 範圍衝突:若 package.json 導致範圍與 Lock 檔不符,npm 會以 package.json 為準下載新版,再回頭更新 package-lock.json 以維持同步。 取得實際套件版本的方法 查看 package-lock.json 直接在 package-lock.json 中搜尋套件名稱,查看 version 欄位。 package-lock.json123"node_modules/some-lib": { "version": "1.5.0",} 使用 npm 指令查詢 查詢指定套件版本,輸出會顯示目前專案中實際安裝的版本。 npm12345# 顯示巢狀依賴,輸出有時較雜npm list some-lib# 僅顯示專案直接依賴的版本npm list some-lib --depth=0 透過 node_modules 直接確認 查看 node_modules/some-lib/package.json 檔案。 package.json1234{ "name": "some-lib", "version": "1.5.0"} 相依套件分類dependencies vs devDependencies 在實務中,區分套件存放位置對於建置(Build)後的產品體積非常重要。 類型 適用套件範例 說明 dependencies react, lodash, axios 執行環境(Production)必須具備的套件。 devDependencies typescript, jest, eslint 僅在開發、測試、建置階段需要的工具。 範例 package.json12345678910{ "dependencies": { "axios": "^1.13.2", "lodash": "^4.17.21" }, "devDependencies": { "eslint": "^9.39.2", "typescript": "^5.9.3" }} 語意化版本SemVer 遵循 MAJOR.MINOR.PATCH 格式,分別代表主版本號、次版本號與修補版本號。 MAJOR:不相容的重大變更 MINOR:向下相容的新功能 PATCH:向下相容的錯誤修正 版本符號 ^:允許更新不變更主版本的最新版(不更動最左側非零數字) ~:允許更新修補版本(僅允許更新最右側非零數字) 符號名稱 範例 實際安裝範圍 Caret (^) 插入符號 ^1.2.3 >=1.2.3 且 <2.0.0 Tilde (~) 波浪符號 ~1.2.3 >=1.2.3 且 <1.3.0 Exact 精確版本 1.2.3 僅安裝 1.2.3 Wildcard (*) 萬用字元 * 接受任何版本,不建議使用 健忘筆記 特別注意 0.x.x 版本的符號行為,在語意化版本中,0.x.x 被視為不穩定階段的開發中版本,npm 會採用更保守的更新策略,MINOR 會被視為重大變更,因此升級行為有所不同。例如:^0.2.3 的實際範圍為 >=0.2.3 <0.3.0,不會升級到 0.3.0 以上的版本。 詳細的語意化版本說明可到【npm 官方文件】查看。 npm update 行為機制依循規範自動化升級 適用於將套件升級至符合 package.json 範圍內的最新版本,並同步更新 node_modules 與 package-lock.json。 無法指定版本號進行更新,版本規則全權由 package.json 決定。 健忘筆記 如果有降版本的需求,需 使用 npm install <package>@<version> 達到目的。 有無指定套件的差異當未指定套件名稱時: npm1npm update 更新所有依賴套件,只要該套件有新版本且符合版本範圍。 適合情境: 例行套件維護 小版本或 patch 更新 專案整體升級 當指定套件名稱時: npm1npm update some-lib 更新指定套件和其子套件,指定套件以外的直接依賴不會變動。 適合情境: 只想升級特定套件 希望控制變動影響範圍 範例 package.json123"dependencies":{ "some-lib":"^1.2.0"} 此設定代表允許更新至 1.x.x 的最新版本。 npm1npm update some-lib npm 會自動解析可用版本,並更新: npm1some-lib@1.9.3 健忘筆記 npm update 是一個「依版本規則自動前進」的更新工具,而版本的精準控制(指定或回退)必須使用 npm install 處理。 結論npm 的核心邏輯在於提供一套可重現的依賴套件解析機制,把「版本範圍」與「確切版本」明確分離,透過 package.json 控制版本範圍、 package-lock.json 穩定專案確切版本,理解 npm 安裝和更新的實際行為,有助於解讀版本變動、維護專案、多人開發協作,降低不可預期的風險。 延伸閱讀 【npm 官方文件】npm install 說明 【npm 官方文件】package.json 說明 【npm 官方文件】package-lock.json 說明 【npm 官方文件】SemVer 說明 【npm 官方文件】npm update 說明","link":"/Other/NPM/analyzing-npm-install-update-mechanism/"},{"title":"【Develop】解析 URL Text Fragments #:~:text=","text":"本篇重點 Text Fragments 允許透過 URL 直接定位並 highlight 網頁中的特定文字,無需依賴 HTML ID 屬性 了解 Text Fragments 和傳統錨點的差異 語法結構支援單詞、範圍及上下文定位 要求導航必須由使用者觸發(例如點擊連結或在網址列輸入),禁止透過腳本或是 iframe 加載 可透過 ::target-text 客製化反白樣式,不影響 DOM 結構 Text Fragments(文字片段)全名為 Scroll to Text Fragment。此功能允許使用者透過特定的 URL 格式,直接連結到網頁中某段特定的文字,而不是僅限於連結到頁面頂部或具有特定 ID 的元素。 和傳統錨點 (#id) 的差異: 傳統 #id: 依賴網頁開發者在 HTML 元素上設置 id 屬性(例如 <h2 id="section1">),若目標位置沒有 ID,則無法精確連結。 Text Fragments: 不需要任何 HTML 標記或屬性,只要網頁上有該段文字,就可透過 URL 參數進行定位和 highlight。 運作方式當瀏覽器接收到包含 Text Fragment 的 URL 時 解析 URL:瀏覽器識別 URL 尾端的片段指令 #:~:text=(不會被送至伺服器,僅影響用戶端行為)。 內容比對:瀏覽器在 DOM 載入後,搜尋和參數相符的文字內容。 滾動和 highlight: 若找到匹配文字,瀏覽器會自動滾動頁面,將該段文字垂直置中於視窗的可見範圍內。 瀏覽器會對該段文字套用 User-Agent 預設的 highlight 樣式(CSS 偽類為 ::target-text)。 隱私保護:嚴格防止攻擊者利用此功能探測頁面內容(如跨站搜索攻擊),該功能要求導航必須由使用者觸發(例如點擊連結或在網址列輸入),無法透過腳本或是 iframe 加載。 健忘筆記 跨站搜索攻擊(Cross-Site Search)是側鏈攻擊(Side-channel attack)的一種。攻擊者的目標不是直接偷取你的密碼,而是透過「詢問問題」來推斷出你在另一個受信任網站(如 Gmail、銀行或私人筆記)中的敏感資料。 語法結構Text Fragment 的語法附加在 URL 的 hash (#) 之後,使用特殊分隔符 :~: 開頭,並且內容必須進行 URL Encode(中文字串現代瀏覽器會自動處理),確保連結在所有平台(例如 Line、Discord、Slack 或 Email 軟體)都正確運作。 Text Fragment 語法結構1URL#:~:text=[prefix-,]textStart[,textEnd][,-suffix] 參數 prefix-(前綴內容):選填,用於指定目標文字「前面」的文字,協助瀏覽器在重複詞彙中定位正確段落。語法必須以連字號 - 結尾,並以逗號 , 和 textStart 分隔。 textStart(起始文字):必填,要 highlight 顯示的文字開頭。若未指定 textEnd,則只 highlight 此部分。 textEnd(結束文字):選填,要 highlight 顯示的文字結尾。若指定此項,瀏覽器會 highlight 從 textStart 到 textEnd 之間的所有內容。 -suffix(後綴內容):選填,用於指定目標文字「後面」的文字,協助精確定位。語法必須以連字號 - 開頭,並以逗號 , 和前面的參數分隔。 健忘筆記 URL Encode 是一種將「特殊字元」轉換為網頁瀏覽器與伺服器都能理解的格式的技術,因為網址只允許使用 ASCII 字元集(英文字母、數字與少數符號)。 範例 1. 單詞搜尋 定位並 highlight “defer 屬性“ 一詞。 demo:https://forgetfulengineer.github.io/Frontend/Html/script-loading-defer-async/#:~:text=defer%20%E5%B1%AC%E6%80%A7 2. 跨多句範圍搜尋 定位從 “表示” 開始到 “事件前” 結束的句子片段。 demo:https://forgetfulengineer.github.io/Frontend/Html/script-loading-defer-async/#:~:text=%E8%A1%A8%E7%A4%BA,%E4%BA%8B%E4%BB%B6%E5%89%8D 3. 上下文消搜尋 網頁中有三個 “DOMContentLoaded”,要指定定位在第三個(後面接著 “執行時機” 的那個)。 demo:https://forgetfulengineer.github.io/Frontend/Html/script-loading-defer-async/#:~:text=DOMContentLoaded,-%E5%9F%B7%E8%A1%8C%E6%99%82%E6%A9%9F 客製化反白的背景顏色::target-text 偽元素 瀏覽器為 Text Fragment 提供專用偽元素 不影響一般文字選取樣式 範例 css 設定1234::target-text { background-color: #ffe58f; /* 反白背景色 */ color: #000; /* 反白文字色 */} 注意事項相容性狀態: Chromium 核心瀏覽器(Chrome, Edge, Opera):完全支援。 Safari:支援(自 Safari 16.1 起)。 Firefox:支援(現代版本已預設開啟)。 URL Text Fragments 的 Can i use 限制和規範: 瀏覽器只會搜尋可見文字,隱藏元素(display: none)中的文字無法被定位。 同頁多次出現相同文字時,可能命中非預期位置。 結論HTML URL Text Fragments 提供了一種不需要修改後端或 HTML 結構就可以標記網頁內容的方法,透過 textStart、textEnd 以及前後文 prefix、suffix 的語法組合,可以更有效得分享內容。 延伸閱讀 【web.dev】Text Fragments 【MDN】Text fragments 【MDN】::target-text 【Can I use】URL Text Fragments 瀏覽器相容性","link":"/Other/analyzing-url-text-fragments/"},{"title":"【Develop】解析五大雲端服務模型","text":"本篇重點 分析五大雲端服務模型(IaaS、CaaS、PaaS、FaaS、SaaS)的核心架構 區分各服務硬體、虛擬化、作業系統、應用程式等層級的管理歸屬 各服務模型的代表性產品、管理複雜度及適用場景 從實體硬體到軟體應用,雲端服務的核心在於將不同層級的技術資源根據使用者的需求提供不同程度的管理權限。 IaaS 基礎設施即服務Infrastructure as a Service 提供最基礎的運算資源,供應商負責實體資料中心、網路、伺服器硬體及虛擬化技術,用戶則擁有虛擬機以上的完整控制權,需要自行管理,相當於租用一台遠端伺服器。 管理範圍 雲端廠商負責實體資料中心、網路、儲存設備、虛擬化技術。 用戶負責安裝與維護作業系統(OS)、中間件(Middleware)、執行環境(Runtime)以及所有應用程式與數據。 代表產品Amazon EC2、Google Compute Engine (GCE)、Azure Virtual Machines。 適用對象需要高度客製化環境、運行遺留系統(Legacy Systems)或具備強大運維團隊的企業。 例如 自行管理 Kubernetes cluster 高度客製化企業系統 CaaS 容器即服務Container as a Service 介於 IaaS 與 PaaS 之間,專注於容器的部署與編排,簡化了基礎設施的管理,讓開發者專注於容器映像檔(Container Images),是專門為容器化應用(Containerized Application) 設計的雲端服務。 管理範圍 雲端廠商負責管理叢集(Cluster)底層基礎設施、容器執行環境。 用戶負責容器映像檔、應用程式碼及容器編排設定。 代表產品Google Kubernetes Engine (GKE)、Amazon EKS、Azure AKS。 適用對象採用微服務架構、需跨雲平台移植(Portability)、CI/CD 容器部署的開發團隊。 例如 以 Docker + Kubernetes 部署 API 服務 DevOps 團隊進行自動化部署 PaaS 平台即服務Platform as a Service 消除作業系統與中間件的管理負擔,提供完整的應用開發、測試與部署平台。 管理範圍 雲端廠商負責管理作業系統(OS)、執行環境(Runtime,如 Java、Node.js、Python runtime)、自動部署、自動縮放機制(Auto Scaling)。 用戶僅需上傳程式碼並配置相關設定。 代表產品Google App Engine、Netlify、Heroku、AWS Elastic Beanstalk。 適用對象Web 應用開發、API Server,希望將資源集中於產品邏輯而非基礎架構的開發者。 例如 Node.js API Python Web Application RESTful Service FaaS 函數即服務Function as a Service 「無伺服器運算」(Serverless)架構的一種實作方式,簡化成單一函數層級,僅在特定事件觸發時才執行。 管理範圍 雲端廠商負責底層資源調度、執行環境與擴展。 用戶僅撰寫並上傳特定邏輯的函數代碼。 代表產品AWS Lambda、Google Cloud Functions、Azure Functions。 適用對象事件驅動型任務、非同步處理、API 後端處理、低頻率或突發性流量的任務。 例如 圖片上傳後自動壓縮 webhook 事件處理 IoT 資料處理 SaaS 軟體即服務Software as a Service 用戶直接透過網路瀏覽器或 API 使用完整的應用軟體。 管理範圍 雲端廠商負責從硬體到軟體的所有層級。 用戶僅負責設定軟體參數與使用功能。 代表產品Salesforce、Microsoft 365、Slack、Gmail。 適用對象企業通用辦公需求、客戶關係管理或不具備軟體開發能力的一般企業用戶。 雲端服務模型分析各服務模型在控管能力與複雜度(客製化能力)上的差異: 服務類型 核心控管層級 管理複雜度 IaaS 虛擬主機與網路 最高 CaaS 容器與編排 高 PaaS 應用程式碼 中 FaaS 函數代碼 低 SaaS 終端軟體設定 最低 結論 雲端服務模型反映了系統管理責任在廠商與使用者之間的分工差異。 工程團隊在選擇雲端服務時,通常可以根據以下因素決定: 系統架構(Monolith / Microservices) DevOps 能力 維運成本 擴展需求 延伸閱讀 【MDN】Cloud computing models 介紹 【google】Cloud computing models 介紹 【AWS】 Cloud Computing Models 介紹","link":"/Other/analyzing-five-cloud-service-models/"},{"title":"【Tool】使用 Gemini Canvas 製作跑步配速 APP","text":"本篇重點 簡潔、無需付費的跑步配速工具 自訂配速目標與語音回報 即時配速與時間儀表板 訓練總結,平均配速分、每公里分段數據表格 跑步配速助手使用教學 web app 的背景執行限制 當初在練習跑步時,想找一款能夠單純專注在配速和語音回報的工具作輔助,但實際使用市面上的跑步 APP 後,發現幾個問題: 語音播報內容與節奏不符合需求 功能過於複雜,例如社群、課程、排行榜等元素 部分關鍵功能需要付費才能使用 所以決定自行製作一個專注於「配速控制」的 Web App,並嘗試使用 Gemini Canvas 進行開發,打造一個簡潔、可安裝、無需付費的跑步配速工具。 跑步配速助手 搶先體驗 功能介紹 功能 說明 自訂目標配速 設定每跑 1 公里需要花多少時間 自訂語音回報間隔 設定語音回報距離間隔 即時配速儀表板 顯示時間、距離、平均配速 語音回報 依距離間隔設定自動播報目前成績 訓練總結 提供總里程、平均配速及每公里分段數據分析 使用步驟設定跑步目標 設定目標配速 設定語音回報間隔,設定後會依距離間隔自動播報目前成績 開始跑步 點擊 開始跑步 開啟 GPS 定位權限 即時儀表板開始計算時間與距離 語音播報 每完成指定距離自動語音回報 播報內容包含: 目前總距離 平均配速 與目標配速的差距(領先或落後幾秒) 訓練總結跑步結束後,會產生完整統計資料: 第一部分:總覽數據 🏃 總公里 ⏱ 訓練時間 ⚡ 平均配速 第二部分:每公里分段數據分析 每公里的時間 每公里的平均配速 每公里與目標配速的差距 背景執行的限制關閉螢幕或離開瀏覽器後無法持續計算距離與時間 關閉螢幕 切換到其他 App 最小化瀏覽器 原因: 行動瀏覽器會暫停背景分頁的 JavaScript 執行 Web App 無法在背景取得 GPS 權限 iOS 與 Android 對背景定位有安全限制 這也是 Web App 與原生 App 的核心差異。 PWA 應用目前版本屬於 PWA,優勢在於: 無需下載 無需註冊 可快速迭代 可使用 Chrome 或 Safari 開啟,再點選「加入主畫面」獲得最佳體驗。 項目 Web App (PWA) 原生 App 安裝方式 可直接透過瀏覽器安裝 需透過 App Store / Play 商店 開發成本 單一前端即可完成 需分平台開發 背景運作 受瀏覽器限制 完整權限,背景執行最穩定 更新方式 即時更新 需重新上架審核 語音與硬體控制 支援有限 完整支援 未來優化方向後續若有時間,會考慮以下改進: 訓練結算:增加訓練步數、消耗的卡路里數的數據。 數據圖表:使用 SVG 繪製配速曲線,數據視覺化。 多語系支持:增加多語言的配置。 線上體驗建議在行動裝置上開啟,並允許定位權限,獲得完整使用體驗。 跑步配速助手:https://forgetfulengineer.github.io/run-pace-assistant/ github 專案:https://github.com/forgetfulengineer/run-pace-assistant","link":"/Other/Tool/build-running-pace-app-with-gemini-canvas/"},{"title":"【Develop】解析 DevOps 與 CI/CD 自動化流程","text":"本篇重點 定義 DevOps、CI 與 CD 核心概念 解析 CI 與 CD 的差異 解析 CI/CD 流水線標準流程及應用場景 說明 CI 與 CD 的異常處理 說明 DevOps 與 CI/CD 的關係 DevOps 開發與營運Development & Operations 一種強調開發(Development)與營運(Operations)團隊協作的概念,並非單一的工具或是職位。 核心概念 跨團隊協作(開發 + 營運) 自動化流程(建置、測試、部署) 提高部署頻率 確保系統穩定性 即時監控 範例 傳統流程:開發完成 → 手動交給營運部署 → 問題回報慢 DevOps:開發提交程式碼 → 自動測試 → 自動部署 → 即時監控 CI 持續整合Continuous Integration 開發人員將程式碼整合到主分支,系統會自動啟動建置與測試,確保新程式碼不會破壞既有功能,提高穩定性。 核心目標 保持主分支穩定 提早發現錯誤 降低整合風險 範例 每次 push / pull request 都會觸發流程,自動執行: Build(編譯 / 打包) Test(單元測試) CD 持續交付 / 部署Continuous Delivery / Deployment CD 分為兩種模式: 持續交付 (Continuous Delivery) :程式碼通過測試後,自動進入可發布狀態,但部署至正式環境需要手動觸發。 持續部署 (Continuous Deployment) :通過所有測試的程式碼會自動部署到生產環境,完全不需人工介入。 範例 CI 測試通過後: Continuous Delivery 觸發 deploy job,但需手動批准 Continuous Deployment 自動部署到 production CI 與 CD 的差異總結持續整合、持續交付與持續部署在操作層級的差異: 階段特性 持續整合 (CI) 持續交付 (CD) 持續部署 (CD) 主要目的 驗證程式碼正確性 確保隨時可發布 自動化上線流程 觸發方式 代碼推送 (Push) 代碼合併 (Merge) 通過測試後自動執行 人工介入 無 需要 (最終部署) 無 CI/CD Pipeline標準流程標準的 Pipeline 包含一系列自動化步驟,確保程式碼從提交到上線的品質。 原始碼階段 (Source) :開發者將程式碼提交至 Git 倉庫,觸發 Pipeline。 建置階段 (Build) :編譯程式碼、下載依賴套件,並將其打包成 Docker 鏡像或執行檔。 測試階段 (Test) :執行單元測試 (Unit Test)、整合測試 (Integration Test) 與代碼掃描 (Linting)。 部署階段 (Deploy) :將產出物發布至測試、預發布或正式環境。 應用場景 全端專案交付整合前後端的自動化流程。當開發者提交程式碼時,系統會自動執行 API 整合測試與前端資源打包(Build),並將結果部署至測試環境或 CDN,確保在正式上線前完成嚴謹的驗證與檢查。 微服務架構管理針對解耦的大型系統,為數十甚至數百個微服務建立「各自獨立」的 CI/CD Pipeline。這讓團隊能針對單一模組進行快速更新或錯誤回滾,避免一次更新全部的功能,大幅提升系統的擴充性與容錯能力。 多人協作與持續整合開發階段的品質把關和分支管理。在多人團隊同時開發不同功能時,CI/CD 會即時偵測分支合併可能產生的衝突與邏輯錯誤,確保主分支(Main Branch)的程式碼永遠維持在「隨時可部署」的高品質狀態。 高頻發布需求為需要快速反應市場的電商或社交平台設計。CI/CD 能將傳統冗長、需要人工介入的發布流程壓縮,讓團隊具備每日多次穩定上線的能力,快速推出新功能或修復緊急 Bug。 健忘筆記 「解耦」(Decoupling) 是一個技術概念,將原本緊密連結、互相牽制的系統,拆解成多個「獨立且不依賴」的小單元,解耦後的營運優勢有 1.各自獨立的 CI/CD Pipeline 2.單一模組可以快速更新或是回滾 3.提升擴充性 CI/CD 異常處理在 Pipeline 執行過程中,錯誤發生後的「中止與否」取決於觸發事件(push 或 PR)以及 pipeline 設計,但多數情況會停止後續步驟並回報狀態。 不同情境下流程失敗的處理邏輯: 項目 直接 Push Pull Request 流程是否終止 是,後續階段中斷 是,禁止合併 對分支的影響 分支狀態顯示為 Fail 目標分支受保護,不受影響 恢復手段 提交新 Commit 修復或 Revert 修正程式碼重新提交 CI/CD 不會阻止 commit 寫入 Git,因為 push 已完成,CI 僅影響「狀態」,不影響 Git 結構 CI/CD 主要作用在「驗證與阻擋後續流程(如 deploy / merge)」 PR 流程可有效避免錯誤進入主分支 push 進主分支需搭配保護機制,否則可能引入不穩定版本 在有 CI/CD 架構的專案中,建議使用 PR(Pull Request)做更新,而不是直接 Push 到主分支,確保錯誤的內容影響 git 分支、影響生產環境。 健忘筆記 在 github 中可以設定 repositery 開啟分支保護和禁止 Force Push 的相關設定,達到防呆的效果。 DevOps 與 CI/CD 的關係DevOps 是開發與營運的整體概念,而 CI/CD 是實現 DevOps 的實作技術。 DevOps 提供目標:例如提升交付速度、強化系統穩定性等。 CI/CD 提供工具:透過自動化工具鏈(如 GitHub Actions, Jenkins, GitLab CI)執行重複性任務,達到自動化流程、減少人為操作等效果。 結論CI/CD 是現代軟體開發的核心基礎設施,透過 CI 的持續整合與 CD 的持續交付或部署落實 DevOps 的概念。藉由嚴謹的 Pipeline 監控與異常阻斷機制,開發團隊能在追求交付速度的同時,強化系統穩定性與品質控管。 延伸閱讀 【Red Hat】Understanding CI/CD 【GitLab】CI/CD Concepts 【Github】Understanding GitHub Actions","link":"/Other/devops-ci-cd-pipeline/"},{"title":"【Hexo】自訂 Icarus 主題的 JSON-LD 優化 SEO","text":"本篇重點 覆寫 Icarus 預設 Structured Data,改為自訂元件控制 建立 structured_data.jsx,依頁面類型動態產生不同的 JSON-LD,提升 SEO 表現 Icarus 主題原生提供 head.structured_data 設定,可以在 _config.icarus.yml 定義基本欄位(如 title、description 等),並自動產生對應的 JSON-LD。但屬於靜態配置,生成內容較為簡化,導致結構資訊不夠完整,無法充分發揮 Structured Data 在搜尋引擎解析的效果。 因此決定將 Structured Data 改為由自訂元件統一生成,透過程式邏輯依頁面類型動態輸出對應的 JSON-LD(如 WebSite、TechArticle、CollectionPage、BreadcrumbList 等),並讓 JSON-LD 結構更完整且具擴展性,同時強化搜尋引擎對內容的理解,進一步提升整體 SEO 表現。 目標透過修改 Hexo Icarus 主題原始碼,覆蓋預設的 SEO 輸出,建立並套用專屬於個人網站的 JSON-LD 結構化資料。 覆寫 Icarus 預設 Structured DataIcarus 預設使用 hexo-component-inferno 提供的 Structured Data,替換掉此檔案的引用路徑,改為引入自定義的結構化資料元件,並修改元件 props 以利後續新增的 structured_data.jsx。 修改內容 layout/common/head.jsx >folded查看檔案12345678910111213141516171819202122...- const StructuredData = require('hexo-component-inferno/lib/view/misc/structured_data');+ const StructuredData = require('../misc/structured_data');...{typeof structured_data === 'object' && structured_data !== null ? <StructuredData- title={structured_data.title || page.title || config.title}- description={structured_data.description || page.description || page.excerpt || page.content || config.description}- url={structured_data.url || page.permalink || url}- author={structured_data.author || config.author}- publisher={structured_data.publisher || config.title}- publisherLogo={structured_data.publisher_logo || config.logo}- date={page.date}- updated={page.updated}+ page={page}+ config={config}+ helper={helper} images={structuredImages} /> : null}... 移除原本套件輸出,導入自定義 structured_data.jsx 修改元件 props 建立自定義 Structured Data 元件在主題資料夾內新增 layout/misc/structured_data.jsx,此檔案將覆蓋 Icarus 預設的 JSON-LD 生成邏輯,內容可依個人需求對結構化資料做調整。 產出結構 透過 Hexo 的 Helper 函式(如 is_home(), is_post(), is_category()),根據當前頁面類型,動態產出不同的結構化資料。 首頁:輸出 WebSite 結構。 文章頁:輸出 TechArticle 與 BreadcrumbList 結構。 分類頁:輸出 CollectionPage 與 BreadcrumbList 結構。 關於頁:輸出 AboutPage 與 Person 結構。 其他頁面:輸出基礎 WebPage 結構。 替換自定義內容 將所有含有 example 的相關網址,全面替換為個人專屬的網站資訊。 新增內容 layout/misc/structured_data.jsx >folded查看檔案123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222/** * A JSX component that renders simple Google structured data. * @module layout/misc/structured_data */const { Component } = require('inferno');const { stripHTML, escapeHTML } = require('hexo-util');/** * A JSX component that renders simple Google structured data. * * @name StructuredData */module.exports = class extends Component { render() { const { page, config, helper, images } = this.props; const { full_url_for, is_home, is_post, is_category, is_page } = helper; const { structured_data = {} } = config.head; const siteUrl = config.url; const language = config.language; const title = structured_data.title || page.title || config.title; let description = structured_data.description || page.description || page.excerpt || page.content || config.description; const canonical = full_url_for(structured_data.url || page.permalink || page.current_url || config.url); const author = structured_data.author || config.author; const publisher = structured_data.publisher || title; const publisherLogo = structured_data.publisher_logo || config.logo; const schemaData = []; const person = { '@type': 'Person', '@id': 'https://example.github.io/about/#person', name: author, image: 'https://example.github.io/img/og_image.png', url: 'https://example.github.io/about/', jobTitle: "Software Engineer", sameAs: [ "https://github.com/example", ] }; const organization = { '@type': 'Organization', '@id': `${siteUrl}#organization`, name: publisher, url: siteUrl, logo: { '@type': 'ImageObject', url: full_url_for(publisherLogo) } }; const breadcrumbList = { '@type': 'BreadcrumbList', '@id': `${canonical}#breadcrumb`, itemListElement: [ { '@type': 'ListItem', position: 1, name: '分類', item: 'https://example.github.io/categories/' } ] }; if (description) { description = escapeHTML( stripHTML(description) .replace(/\\n/g, ' ') .slice(0, 210) .trim() ); } // 首頁(WebSite) if (is_home()) { schemaData.push({ '@type': 'WebSite', '@id': `${siteUrl}#website`, url: siteUrl, name: title, alternateName: "example", inLanguage: language, author: person, publisher: organization, description: description, }); } // 文章頁 (TechArticle + BreadcrumbList) if (is_post()) { schemaData.push({ '@type': 'TechArticle', '@id': `${canonical}#article`, mainEntityOfPage: { '@type': 'WebPage', '@id': canonical }, url: canonical, headline: title, description: description, image: [full_url_for(images)], datePublished: page.date ? page.date.toISOString() : undefined, dateModified: page.updated ? page.updated.toISOString() : undefined, inLanguage: language, author: person, publisher: organization, keywords: page.tags ? page.tags.map(t => t.name).join(',') : undefined, articleSection: page.categories ? page.categories.map(cat => cat.name) : undefined }); let position = 2; if (page.categories && page.categories.length > 0) { page.categories.forEach((cat) => { breadcrumbList.itemListElement.push({ '@type': 'ListItem', position: position++, name: cat.name, item: full_url_for(cat.path) }); }); } breadcrumbList.itemListElement.push({ '@type': 'ListItem', position: position, name: title, item: canonical }); schemaData.push(breadcrumbList); } // 分類頁 (CollectionPage) if (is_category()) { const categoryName = page.category || title; schemaData.push({ '@type': 'CollectionPage', '@id': `${canonical}#collection`, name: categoryName + ' 技術文章列表', headline: categoryName + ' 技術文章列表', url: canonical, description: `收錄 ${categoryName} 相關技術文章、經驗分享整理。`, inLanguage: language, mainEntity: { '@type': 'ItemList', '@id': `${canonical}#itemlist`, numberOfItems: page.posts.length, itemListElement: (page.posts).slice(0, 20).map((post, index) => ({ '@type': 'ListItem', position: index + 1, url: full_url_for(post.link || post.path), name: post.title })) } }); let position = 2; if (page.parents && page.parents.length > 0) { page.parents.forEach((cat) => { breadcrumbList.itemListElement.push({ '@type': 'ListItem', position: position++, name: cat.name, item: full_url_for(cat.path) }); }); } breadcrumbList.itemListElement.push({ '@type': 'ListItem', position: position++, name: page.category, item: canonical }); schemaData.push(breadcrumbList); } if (!is_home() && !is_post() && !is_category()) { if (is_page() && page.path.startsWith('about/')) { schemaData.push({ '@type': 'AboutPage', '@id': `${canonical}#aboutpage`, url: canonical, name: '關於 ' + author, headline: '關於 ' + author, description: description, inLanguage: language, mainEntity: { "@id": "https://example.github.io/about/#person" } }); schemaData.push(person); } else { schemaData.push({ '@type': 'WebPage', '@id': `${canonical}#webpage`, url: canonical, name: title, headline: title, description: description, inLanguage: language, author: person, publisher: organization, }); } } if (schemaData.length === 0) return null; const finalSchema = { '@context': 'https://schema.org', '@graph': schemaData }; return ( <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(finalSchema) }} ></script> ); }}; 詳細修改紀錄 延伸閱讀 【健忘工程師網站主題】forgetfulengineer/hexo-theme-icarus repository 【Google Search Central】Structured Data 【Schema.org 官方文件】 【Hexo 官方文件】Helpers 介紹 【Github】hexo-component-inferno repo","link":"/Other/Hexo/icarus-json-ld-custom-structured-data-seo/"},{"title":"【GitHub】User Page 與 Project Page 路徑解析優先級","text":"本篇重點 GitHub Pages 分為 User Page 與 Project Page 兩種類型 User Page 與 Project Page 路徑重疊時,優先解析 Project Page 的內容 GitHub Pages 機制User Page repository 名稱固定為:[username].github.io(例如:forgetfulengineer.github.io) 對應網址:https://[username].github.io/(例如:https://forgetfulengineer.github.io/) 特性: 作為整個網域的根 可包含任意資料夾(例如:/blog/、/project/) 必須是 public repository Project Page 任意 repository 名稱(例如:run-pace-assistant) 對應網址:https://[username].github.io/repository-name/(例如:https://forgetfulengineer.github.io/run-pace-assistant/) 特性: 必須是 public repository 必須在 repository 設定中啟用 GitHub Pages /repository-name/ 路徑規則當請求 https://username.github.io/<repository-name>/,GitHub Pages 會依以下順序判斷: 是否存在同名 repository <repository-name> 該 repository 是否啟用 GitHub Pages 判斷結果 若「存在 + 已啟用」→ 直接使用 Project Page 若「存在但未啟用」或「不存在」→ 回退至 User Page 的 /repository-name/ 資料夾 若以上接失敗,則導向 404 Not Found error 健忘筆記 GitHub Pages 的路由系統優先匹配 Project Page 的路徑 範例 假設使用者帳號為 forgetfulengineer,並同時存在兩個儲存庫。其一是 User Page repository forgetfulengineer.github.io,該 repository 內部包含一個名為 /run-pace-assistant/ 的資料夾;其二是獨立建立的 Project Page repository run-pace-assistant,且已啟用 GitHub Pages 功能。 當存取網址 https://forgetfulengineer.github.io/run-pace-assistant/,GitHub Pages 會先依據路徑 /run-pace-assistant/ 判斷是否存在對應名稱的 repository。由於確實存在 run-pace-assistant 這個 repository,且該 repository 已啟用 GitHub Pages,因此系統會將此路徑直接映射至該 Project Page,而不是 User Page 中的同名資料夾。 結論 GitHub Pages 對獨立儲存庫(Project Page)的優先權高於主儲存庫(User Page)內的子目錄。 延伸閱讀 【GitHub 官方文件】GitHub Pages 介紹 【GitHub 官方文件】Project Pages 和 User Pages 說明 【stackoverflow】探討 User Page 與 Project Page 路徑重疊問題","link":"/Other/GitHub/github-pages-user-project-page-priority/"},{"title":"【Antigravity】自動更新後編輯器因此消失,如何恢復原本的編輯器與設定","text":"本篇重點 Antigravity 2.0 不再以「AI IDE」為核心,而是改為 autonomous AI agent 平台 如何找回原本的 Antigravity IDE 如何恢復原本 IDE 設定與 AI 對話紀錄 Antigravity 2.0 改版重點Google 於 I/O 2026(2026 年 5 月 20 日)宣布 Antigravity 2.0 正式上線。此次更新將系統從單純的「輔助程式碼編輯工具」轉型為「自主代理人(Autonomous AI Agent)開發與管理平台」。 官方指出,將程式碼編輯器與 AI 代理綁定在一起的設計對於非工程背景使用者不夠直覺,因此 2.0 版本進行了核心功能剝離,轉化為純 AI 代理聊天介面的獨立桌面應用程式。 Antigravity 2.0 不再以「AI IDE」為核心,而是改為 autonomous AI agent 平台,主打: 多 AI agent 協作 背景任務執行 長時間 autonomous workflow Agent orchestration 管理 因此更新後開啟的 Antigravity,其實已不是原本的 IDE,而是新的 AI agent 管理介面。 如何找回原本的 Antigravity IDE新版 Antigravity 不再內建舊版 IDE,需要額外重新安裝。 先移除當前的 Antigravity,再前往 Antigravity 官方下載頁面,滾到頁面最下方找到「Antigravity IDE」進行下載。 安裝完成後重新開啟 IDE,會發現: 原本的編輯器設定消失 AI 對話紀錄消失 已開啟過的專案紀錄消失 工作區設定被重置 原因是新版與舊版 IDE 使用不同的資料夾名稱,因此新版 Antigravity 無法抓到原本的設定。 如何恢復原本 IDE 設定與 AI 對話紀錄 此說明為 windows 作業系統環境 舊版與新版 Antigravity 使用的資料夾名稱不同: 類型 資料夾名稱 舊 AI IDE Antigravity 新 Agent 平台 Antigravity IDE 舊版 IDE 資料通常仍保留在 Windows 使用者資料夾內。 資料位置為 C:\\Users\\[使用者名稱]\\AppData\\Roaming\\,可在此找到 Antigravity 資料夾,裡面通常包含: IDE 設定 Workspace 設定 專案紀錄 AI 對話紀錄 快取資料 還原舊 IDE 設定的方法 關閉 Antigravity IDE:避免檔案被占用 前往 Roaming 資料夾:路徑為 C:\\Users\\[使用者名稱]\\AppData\\Roaming\\ 備份新的資料夾或刪除:將 Antigravity IDE 改為 Antigravity IDE backup 或直接刪除 將舊資料夾重新命名:將 Antigravity 改為 Antigravity IDE 重新開啟 Antigravity IDE 完成後即可恢復: 原本設定 AI 對話紀錄 Workspace 最近專案 編輯器環境 結論如果你也對這突如其來的更新感到困擾,希望找回原本的 IDE,希望這篇紀錄有幫助到你 😁 延伸閱讀【Antigravity 官方文件】Antigravity 2.0 更新介紹【google I/O 20226】Build next-gen AI experiences","link":"/Other/ide-disappeared-after-recovery/"},{"title":"【Node.js】解析 CJS 與 ESM 核心差異和專案轉換","text":"本篇重點 解析 CommonJS(CJS)與 ES Modules(ESM)模組語法差異 CJS 使用 require() 與 module.exports,ESM 使用 import 與 export CJS 採同步載入,ESM 採靜態分析與非同步初始化 ESM 天生支援 Tree Shaking,利於效能優化 整理專案 CJS 到 ESM 的轉換,通常需要調整載入、輸出、路徑、副檔名、全域變數等寫法 早期 Node.js 主要使用 CommonJS(CJS)作為模組系統,其設計目標偏向伺服器端同步執行環境。 後來 JavaScript 官方推出 ES Modules(ESM),作為語言標準模組系統。瀏覽器、Node.js、打包工具與現代前端框架逐漸全面採用 ESM。 CJS 與 ESM 的語法差異CJS:require() 和 exportsCJS 使用 require() 載入模組,並透過 module.exports 或 exports 輸出內容。 範例 math.js1234567891011function add(a, b) { return a + b;}// module.exports 寫法module.exports = { add};// exports 寫法exports.add = add; app.js123456789// 整包引入成一個物件const math = require('./math');console.log(math.add(1, 2));// 使用解構賦值(Destructuring)只拿取 addconst { add } = require('./math');console.log(add(1, 2)); ESM:import 和 exportESM 使用 import 載入模組,並透過 export (Named Exports) 或是 export default (Default Export) 輸出內容。 範例一個檔案中只能有一個 export default,但可以同時擁有多個具名匯出(Named exports,export) math.js1234567891011// 1. 預設匯出 (只能有一個) -> 主角export default function add(a, b) { return a + b;}// 2. 具名匯出 (可以有很多個) -> 配角們export function subtract(a, b) { return a - b;}export const PI = 3.14159; app.js123456// 主角不用大括號,配角們要放進大括號裡import add, { subtract, PI } from './math.js';console.log(add(5, 3)); // 8console.log(subtract(5, 3)); // 2console.log(PI); // 3.14159 健忘筆記 以 export default 匯出,匯入時可以自己改名,如果是以 export 具名匯出,匯入時須使用匯出名稱命名,但可以使用 as 關鍵字改名。 加載機制差異CJS:同步載入Node.js 會在程式碼執行到 require() 時才進行讀取與解析,這種方式的優點是簡單直接並且可以動態載入,但缺點是: 無法在編譯階段分析依賴 動態特性也會增加分析難度 不利於最佳化 不適合瀏覽器環境 範例 sync.js123456const moduleA = require('./moduleA');if (condition) { // 允許在條件碼區塊中動態載入 const dynamicModule = require('./dynamic');} ESM:靜態分析與非同步初始化import 必須在檔案頂層,Node.js 會在程式執行前先建立依賴圖(Dependency Graph),再進行模組初始化。 可提前分析依賴 可進行最佳化 可支援 Tree Shaking 更適合瀏覽器與現代打包工具 範例 sync.js1234567import path from 'path';import myModule from './my-module.js';// 若需動態載入,必須使用非同步的 import() 函式if (condition) { const dynamicModule = await import('./dynamic.js');} 副檔名與專案設定Node.js 必須明確知道檔案使用 CJS 還是 ESM,預設將 .js 檔案視為 CJS 模組。若要啟用 ESM,需透過更改副檔名或修改專案的 package.json 設定來實現切換。 副檔名區分:強制指定 .mjs 代表 ESM,.cjs 代表 CJS。 專案級別設定:在 package.json 中加入 "type": "module",此後專案內所有的 .js 檔案都會被解析為 ESM。 package.json123456{ "name": "my-project", "version": "1.0.0", "type": "module", // 此設定會讓所有的 .js 檔案都會被解析為 ESM "main": "app.js"} Tree ShakingTree Shaking 是現代打包工具的重要最佳化機制,一種 Dead Code Elimination 技術,主要用於移除程式碼中未被實際呼叫的程式碼。 範例 CJS 的 require() 為動態執行,通常無法安全分析實際使用內容,因此 Tree Shaking 能力較差。 ESM 天生支援靜態分析,因此工具可以知道哪些 export 未被使用。 math.js12345export function subtract(a, b) { return a - b;}export const PI = 3.14159; app.js123import { subtract, PI } from './math.js';console.log(subtract(5, 3)); app.js 有輸入 PI 但未使用,在打包時 PI 會被移除。 __dirname 與 __filename 差異在 CJS 中,Node.js 會自動注入 __dirname (當前目錄路徑) 與 __filename (當前檔案路徑) 等全域變數。 例如 variable.js123const path = require('path');const configPath = path.join(__dirname, 'config.json'); 在 ESM 中不提供這些全域變數,需引用模組來取得路徑。 範例 variable.js12345import { fileURLToPath } from 'node:url';import path from 'node:path';const __filename = fileURLToPath(import.meta.url);const __dirname = path.dirname(__filename); 互操作性在過渡時期,專案中經常會遇到 CJS 與 ESM 混用的情況,這會帶來一些限制與痛點: ESM 載入 CJS:可以直接使用 import 載入 CJS 模組,但通常只能取得 Default Export 的形式,無法依賴 Named Exports 的靜態分析。 CJS 載入 ESM:無法使用同步的 require() 載入 ESM 模組,這會引發 ERR_REQUIRE_ESM 錯誤。必須使用非同步的 import() 函式進行動態載入。 從 CJS 轉換至 ESM 更新 package.json在 package.json 加入 "type": "module" 設定,宣告專案全面採用 ESM 規範。 重構載入與輸出語法全面檢視檔案,將所有的 require() 替換為 import,並將 module.exports 與 exports 替換為 export / export default。 補齊本地檔案的副檔名在 ESM 中,匯入本地端相對路徑的檔案時,必須寫出完整的副檔名(例如:import { helper } from './utils.js')。不寫副檔名或依賴 index.js 省略寫法的機制在 ESM 中預設不支援。 替換全域變數找出專案中使用的 __dirname、__filename 以及 require.resolve 等 CJS 專有變數,使用 import.meta.url 配合 fileURLToPath 進行替換。 處理第三方依賴套件檢查 node_modules 中是否有不相容的舊套件。 修正 JSON 檔案載入在 ESM 中不能直接 import data from './data.json',必須加上 Import Assertions (或 Import Attributes) 語法,或是改用 fs.readFileSync 讀取。 結論CJS 與 ESM 最大差異在於: 模組載入方式 靜態分析能力 Node.js 執行模型 CJS 偏向 Node.js 傳統伺服器架構,ESM 則是現代 JavaScript 生態。 ESM 帶來: Tree Shaking Top-level await 更佳的依賴分析 更一致的跨平台模組標準 延伸閱讀 Node.js ECMAScript Modules 官方文件 Node.js CommonJS Modules 官方文件 MDN JavaScript Modules","link":"/Backend/Nodejs/-cjs-vs-esm-migration-guide/"},{"title":"【Develop】介接搜尋引擎來提升查詢效率!","text":"本篇重點 搜尋引擎透過建立倒排索引(Inverted Index)來提升查詢效率 一般資料庫擅長資料儲存與交易處理,搜尋引擎則專注於資料檢索 搜尋引擎的優勢是全文檢索、模糊搜尋與相關性排序 適合導入搜尋引擎的場景有哪些 搜尋引擎可作為系統架構中的檢索層,降低資料庫查詢壓力 什麼是搜尋引擎搜尋引擎(Search Engine)是一種專門用於資料檢索的系統,其核心目標是在大量資料中快速找到最符合需求的內容。 常見的搜尋引擎包括 Elasticsearch:功能強大,生態系豐富,適合海量資料與複雜分析。 OpenSearch:Elasticsearch 的開源分支(由 AWS 維護),功能高度相似,適合偏好開源架構者。 Apache Solr:老牌、極其穩定,在企業級內容搜尋上有深厚根基(如大型網站文件檢索)。 Meilisearch:以「極致效能」與「易用性」為目標,設定簡單,非常適合小型專案或即時搜尋體驗。 這些是「應用軟體」或「中間件(Middleware)」,並非「打開即用」的應用程式,需部署、資料同步後再串接。如果希望找開箱即用產品,可以使用 Algolia 這樣的 SaaS 服務。 健忘筆記 SaaS 指的是軟體即服務,用戶可以直接透過網路瀏覽器或 API 使用完整的應用軟體。更多相關資訊可以查看【Develop】解析五大雲端服務模型 搜尋引擎與資料庫的差異 與直接從資料庫查詢不同,搜尋引擎會先將資料建立成索引(Index),以解決「巨量文字檢索」與「模糊查詢效能」。 資料庫在處理精準匹配(例如 WHERE user_id = 100)或範圍查詢時效率極高,但在面對非結構化文字的全文檢索(例如 WHERE content LIKE '%關鍵字%')時,通常需要進行全表掃描(Full Table Scan),導致查詢效能隨著資料量增加而呈線性下降。 搜尋引擎的核心在於倒排索引(Inverted Index)技術。傳統資料庫是以「文件/資料列」為核心記錄包含哪些詞彙;倒排索引則是相反,先將所有文字進行「分詞(Tokenization)」處理,記錄「每個詞彙出現在哪些文件之中」,從而將查詢時間複雜度降低。 資料庫和搜尋引擎通常會同時存在,資料庫作為資料來源,搜尋引擎則同步資料後提供搜尋服務。 項目 資料庫 搜尋引擎 主要用途 資料儲存與交易處理 資料搜尋與檢索 查詢方式 SQL 查詢 關鍵字查詢 全文搜尋 能力有限 原生支援 模糊搜尋 成本較高 原生支援 排序機制 依欄位排序 依相關性排序 聚合分析 可實作但較複雜 原生支援 適用場景 CRUD、交易系統 搜尋與推薦系統 搜尋引擎的優勢全文檢索能力搜尋引擎利用分詞器(Tokenizer)將文字內容建立倒排索引(Inverted Index),快速找出包含指定關鍵字的文件。 例如搜尋 docker network,即使關鍵字出現在文章中間,也能快速找到相關內容。 模糊搜尋使用者輸入內容未必完全正確。 例如搜尋 Elasticsarch,實際想搜尋 Elasticsearch 搜尋引擎可透過模糊匹配(Fuzzy Search)找出最接近的結果,這種能力在電商與知識庫系統中特別重要。 搜尋效能提升搜尋引擎透過索引結構進行查詢,當資料量從數萬筆成長到數百萬筆時,仍能維持相對穩定的搜尋速度,因此大型網站通常不會直接使用資料庫承擔所有搜尋需求。 關鍵字權重排序搜尋結果不只是找到資料,更重要的是找到最相關的資料。 例如搜尋 docker,搜尋引擎可能依據條件計算相關性: 標題是否包含關鍵字 關鍵字出現次數 更新時間 點閱數 使用者行為 讓最符合需求、最相關的內容排在前面。 篩選與聚合分析搜尋引擎除了搜尋,也能進行即時統計分析。 例如電商網站常見的篩選功能: 12345手機├─ Apple (523)├─ Samsung (412)├─ ASUS (298)└─ Google (86) 這類統計結果通常稱為 Aggregation(聚合分析),搜尋與分析可以同時完成,而不需要額外執行複雜統計查詢。 適合導入搜尋引擎的場景並非所有系統都需要搜尋引擎,若系統只有少量資料且以 CRUD 為主,資料庫通常已足夠,但是當查詢需求超越「精準比對」時,導入搜尋引擎能顯著提升系統的 I/O 與使用者體驗。 電商商品搜尋電商平台的商品品項繁多,且使用者行為高度依賴關鍵字查詢,搜尋引擎能提供商品名稱、描述、規格的即時檢索,並同時計算出分類、品牌、價格區間的聚合統計,透過權重調整,營運團隊還能人為提高特定贊助商品或促銷商品的排序得分。 文章或文件搜尋技術部落格、新聞網站或文件系統通常需要全文檢索。 例如搜尋 Kubernetes Ingress,搜尋引擎能直接從大量文章內容中找到相關段落,而不僅限於標題比對。 AI RAG 應用在現代 LLM 應用中,為了避免模型產生幻覺(Hallucination)並提供即時資訊,通常會採用 RAG 架構。搜尋引擎在其中擔任「檢索器(Retriever)」的角色,透過傳統文本搜尋(Lexical Search)或結合向量空間的語義搜尋,快速篩選出最相關的資料提供給 LLM 參考。 實務範例 以 AI RAG(檢索增強生成) 系統為例,搜尋引擎在此架構中作為知識檢索的核心節點。 資料同步與索引建立:企業內部的非結構化文件(如產品手冊、技術文件)定期經過清理、切塊(Chunking),並將文本內容與對應的向量資料寫入搜尋引擎中建立索引。 使用者提問輸入:提出具體問題,例如:「2026年最新款伺服器的散熱規格與安裝步驟是什麼?」。 相關性檢索:系統將提問轉化為查詢條件,向搜尋引擎發起檢索。搜尋引擎利用全文檢索與向量檢索技術(Hybrid Search),在毫秒內從數百萬筆文件段落中,篩選出關聯度最高的前 3 至 5 筆區塊。 提示詞組裝:系統將使用者的原始提問,與搜尋引擎檢索出來的精準文本區塊,共同組裝成一個豐富的提示詞(Prompt)。 模型生成回答:LLM 基於提示詞中提供的正確參考資料,生成具備事實根據、無幻覺且邏輯通順的專業回答。 google 的 NotebookLM 就是軟體即服務的「AI RAG(檢索增強生成)應用程式」 結論搜尋引擎的核心價值在於建立高效率的資料檢索能力,提升全文搜尋、模糊匹配、相關性排序、大量資料查詢的效能,解決關聯式資料庫在非結構化資料的效能瓶頸,對於現代電商平台、內容網站、企業知識庫與 AI 應用而言,搜尋引擎已逐漸成為系統架構中的重要元件之一。 延伸閱讀 Elasticsearch 官方文件 OpenSearch 官方文件 Apache Solr 官方文件 Meilisearch 官方文件","link":"/Backend/Develop/search-engine-integration-for-better-performance/"},{"title":"【Node.js】使用 nvm 管理 Node.js 版本重點整理","text":"本篇重點 透過 nvm 可在單一系統中無縫切換多種 Node.js 版本,解決全域套件衝突 了解 Windows 安裝 nvm 的流程 相比直接安裝,nvm 提供更彈性的環境隔離與版本降級、升級能力 建議安裝 nvm 前先移除既有 Node.js,避免檔案或路徑衝突 適合維護舊專案、測試新版 Node.js、團隊協作與 CI/CD 開發流程 nvm(Node Version Manager)是一套用來管理 Node.js 多個版本 的工具,可在同一台電腦安裝多個 Node.js,並快速切換目前使用的版本。 需要注意的是,Windows 所使用的是 nvm-windows,並不是 Linux 或 macOS 常見的 Shell 版本 nvm,兩者雖然用途相同,但屬於不同專案,指令與實作方式也略有差異。 Windows 常用的是 Corey Butler 維護的 nvm-windows。 Windows 安裝流程步驟一:先移除既有 Node.js若電腦已直接安裝 Node.js,建議先移除。 原因包括: 避免 PATH 衝突 避免 node.exe 指向錯誤位置 避免 npm 執行異常 移除後可重新開機一次。 步驟二:下載 nvm-windows到專案的 GitHub Releases 下載最新版安裝程式 nvm-setup.exe 步驟三:執行安裝雙擊 nvm-setup.exe 後,即可跟著流程完成安裝。 步驟四:確認安裝到終端機輸入 nvm -v 確認回傳版本表示安裝成功 nvm vs 直接安裝 Node.js 比較項目 nvm 直接安裝 Node.js 版本切換 指令即時動態切換,耗時極短 需手動移除舊版再安裝新版,耗時長 全域套件狀態 依 Node.js 版本獨立存放,相互隔離 全域共用,多專案易產生套件衝突 多版本管理 可同時安裝多版本 僅能保留一個版本 環境變數管理 自動修改與維護 Symlink 路徑 需依賴單一安裝檔覆寫或手動調整系統變數 維護與升降級 可一鍵安裝或移除指定版本 升降級過程容易殘留舊環境設定 健忘筆記 nvm 將不同版本分別存放在各自的資料夾,並建立捷徑(Symlink)供系統存取,當執行 nvm use <版本號> 時,nvm 會將「捷徑」指向對應的版本資料夾,即可瞬間切換。 實務應用情境 維護舊有專案(Legacy Projects):舊型專案可能依賴較舊的 Node.js 版本(如 v14 或 v16)或是特定的舊版套件,強行以最新版 Node.js 執行易引發語法或套件不相容的錯誤,因此需要調整成專案適合的版本。 平行開發多個專案:同時參與前端現代化專案(需 Node.js v20+)與後端微服務專案(需 Node.js v18),需頻繁於不同環境間切換。 套件升級相容性測試:在將專案的 Node.js 基底版本升級前,需在本地端快速切換至目標版本進行回歸測試,確認編譯與執行無誤。 常用指令 指令語法 功能說明 nvm version 查看 nvm 版本 nvm list 或 nvm ls 列出目前本機端已安裝的所有 Node.js 版本,並標示當前使用中的版本。 nvm list available 列出官方伺服器上可供下載的 Node.js 版本清單(包含 LTS 與 Current)。 nvm install <version> 下載並安裝指定的 Node.js 版本(例如:nvm install 24.18.0 或 nvm install latest)。 nvm use <version> 切換當前系統使用的 Node.js 版本。需具備系統管理員權限以修改 Symlink。 nvm uninstall <version> 移除指定的 Node.js 版本及其關聯的全域套件。 nvm current 顯示當前正在使用的 Node.js 版本號。 nvm 實作流程以下示範從安裝特定版本到模擬專案切換的操作流程。 步驟一:安裝專案所需的不同 Node.js 版本假設目前有兩個專案,分別需要 Node.js v22.23.1 與 v24.18.0。 bash12345# 安裝舊版 Node.js 供專案 A 使用nvm install 22.23.1# 安裝新版 Node.js 供專案 B 使用nvm install 24.18.0 步驟二:驗證安裝列表確認版本皆已正確安裝於本機。 bash123nvm list# 24.18.0# 22.23.1 步驟三:切換並驗證環境(專案 A 開發情境)進入專案 A 目錄前,切換至對應的環境。 bash12345nvm use 22.23.1# Now using node v22.23.1 (64-bit)node -v# v22.23.1 步驟四:切換並驗證環境(專案 B 開發情境)完成專案 A 開發後,準備進行專案 B 的作業。 bash12345nvm use 24.18.0# Now using node v24.18.0 (64-bit)node -v# v24.18.0 結論透過 nvm 預先安裝多個 Node.js 版本並以 nvm use 快速切換,可降低重新安裝的成本,提升專案維護效率。Windows 環境的用戶也可以使用 nvm-windows 達到一樣的效果! 延伸閱讀 【Github repo】nvm-windows(Corey Butler) Node.js 官方網站","link":"/Backend/Nodejs/use-nvm-to-manage-nodejs-version/"},{"title":"【GitHub】解析 Reference System:Issue、Pull Request、Commit 如何建立關聯","text":"本篇重點 Reference System 透過簡單的語法讓 Issue、Pull Request、Commit 建立關聯 同一個 Repository 內的所有 Issue 和 PR 共用同一組遞增的流水號 跨儲存庫參照使用 owner/repo#number 支援文字輸入的 GitHub 介面,皆可解析並觸發參照語法 雙向追蹤機制自動建立雙向提交紀錄和通知 利用 Closing Keywords 關閉 issue 在多人協作的專案中,Issue、Pull Request、Commit 常常是彼此相關的,如果只依靠網址或文字描述,這樣閱讀成本較高,也很難追蹤整個開發流程。 GitHub 提供的 Reference System(參照系統),可以透過簡單的語法建立不同物件之間的關聯,讓 Issue、Pull Request、Commit 等內容彼此串聯。 PR 和 Issue 的關係在 GitHub 的資料結構中,Pull Request(PR)與 Issue 被歸類為同類型的「溝通物件」,PR 在本質上可視為一個「夾帶了 Git 分支和變更紀錄的 Issue」,並且同一個儲存庫(Repository)內的所有 Issue 和 PR 共用同一組遞增的流水號。 此規則確保了編號的絕對唯一性,在單一專案中不會出現「Issue #5」與「PR #5」互相衝突的情況,兩者在系統底層對應的是同一個編號序列。 常見 Reference 語法同一個 Repository用於參照目前 Repository 中的 Issue 或 Pull Request,格式為 #number,範例:#123。 跨 Repository用於參照其他 Repository 的 Issue 或 Pull Request,格式為 owner/repo#number,範例:python/project#123。 適合在多個 Repository 共同開發時建立關聯。 健忘筆記 如果目標 Repository 為 Private 或目前帳號沒有存取權限,GitHub 將無法建立有效的 Reference。 Commit用於參照指定 Commit,格式為 owner/repo@sha,範例:facebook/react@a1b2c3d。 支援的參照場景GitHub 參照系統的應用範圍廣泛,只要是支援文字輸入的 GitHub 介面,皆可解析並觸發參照語法。主要應用場景包含: Issue 與 PR 的內容與評論:用於串聯相關討論上下文,讓對話歷程更具追溯性。 PR 的描述(Description):設定變更請求的依賴目標,或提供相關背景資訊,方便程式碼審查者理解脈絡。 Commit 訊息:在推送程式碼時,將參照直接寫入 Commit 訊息,讓單次變更與追蹤系統產生直接關聯。 雙向追蹤機制參照功能不僅產生靜態的超連結,更具備系統層級的互動與自動化能力,形成完整的雙向追蹤: 自動建立 Referenced:當 Commit、Comment 或 Pull Request 參照某個 Issue 時,Issue 頁面會自動出現 Referenced 紀錄,方便追蹤有哪些開發工作與此 Issue 有關。 自動通知:被參照的 Issue 或 Pull Request 相關參與者,可能會收到 GitHub 的通知,提高協作效率。 Closing Keywords:透過特定關鍵字(如 Fixes、Closes、Resolves 等)關閉 issue,這些關鍵字除了可寫在 Pull Request Description,也能出現在 Commit Message。當變更合併至 Default Branch 後,GitHub 會自動關閉對應的 Issue。 專案參照範例情境假設:在 project A 發現問題並建立 Issue,隨後於 project B 提交修正 PR,並透過 Closing Keywords 讓參照系統自動關閉 project A 的 Issue。 Step 1:建立 issue 在 project A 建立新的 Issue 描述問題,系統配發編號為 #1。 Step 2:開發與提交變更 在 project B 進行程式碼修正,並於 Commit 訊息中記錄關聯(change README forgetfulengineer/projectA#1)。 project A 的 issue 自動出現 Referenced 紀錄 Step 3:發起變更請求與狀態綁定 於 project B 建立 PR,並在 PR 描述區塊寫入 Closing Keywords 關鍵字: forgetfulengineer/projectA#1。 project A 的 issue 自動出現 Referenced 紀錄 Step 4:合併後自動觸發關閉 project B 的 PR 通過審查並執行 merge,系統自動觸發狀態同步。 project A 的 Issue #1 隨即自動轉換為 Closed 狀態。 參照語法總整理 參照目標類型 語法結構 情境說明 範例 同儲存庫 Issue/PR #編號 建立當前專案關聯 #123 跨儲存庫 Issue/PR 擁有者/儲存庫#編號 建立跨專案關聯 facebook/react#123 特定 Commit 擁有者/儲存庫@雜湊值 建立專案特定程式碼變更紀錄 facebook/react@a1b2c3d Closing Keywords Fixes #編號 PR 合併時自動關閉關聯 Issue Fixes #123 User @username Mention 使用者 @forgetful_engineer 結論GitHub Reference System 將 Issue、Pull Request、Commit 和 Repository 串聯成完整的協作紀錄,透過簡單的 Reference 語法和 Closing Keywords,能提升程式碼和需求的可追溯性,也能讓 Issue 的生命週期從建立、開發、Code Review 到 Merge 都保持清楚且一致,是 GitHub 協作中實用且不可或缺的重要功能! 延伸閱讀 【GitHub Docs】Autolinked references and URLs 【GitHub Docs】Linking a pull request to an issue 【GitHub Docs】Using keywords in issues and pull requests","link":"/Other/GitHub/github-reference-system/"}],"tags":[{"name":"php","slug":"php","link":"/tags/php/"},{"name":"backend","slug":"backend","link":"/tags/backend/"},{"name":"vscode","slug":"vscode","link":"/tags/vscode/"},{"name":"git bash","slug":"git-bash","link":"/tags/git-bash/"},{"name":"terminal","slug":"terminal","link":"/tags/terminal/"},{"name":"mysql","slug":"mysql","link":"/tags/mysql/"},{"name":"database","slug":"database","link":"/tags/database/"},{"name":"db","slug":"db","link":"/tags/db/"},{"name":"css","slug":"css","link":"/tags/css/"},{"name":"jquery","slug":"jquery","link":"/tags/jquery/"},{"name":"frontend","slug":"frontend","link":"/tags/frontend/"},{"name":"git","slug":"git","link":"/tags/git/"},{"name":"github","slug":"github","link":"/tags/github/"},{"name":"linux","slug":"linux","link":"/tags/linux/"},{"name":"system","slug":"system","link":"/tags/system/"},{"name":"hexo","slug":"hexo","link":"/tags/hexo/"},{"name":"html","slug":"html","link":"/tags/html/"},{"name":"seo","slug":"seo","link":"/tags/seo/"},{"name":"security","slug":"security","link":"/tags/security/"},{"name":"javascript","slug":"javascript","link":"/tags/javascript/"},{"name":"js","slug":"js","link":"/tags/js/"},{"name":"npm","slug":"npm","link":"/tags/npm/"},{"name":"nodejs","slug":"nodejs","link":"/tags/nodejs/"},{"name":"tool","slug":"tool","link":"/tags/tool/"},{"name":"timestamp","slug":"timestamp","link":"/tags/timestamp/"},{"name":"api","slug":"api","link":"/tags/api/"},{"name":"google","slug":"google","link":"/tags/google/"},{"name":"ga","slug":"ga","link":"/tags/ga/"},{"name":"netlify","slug":"netlify","link":"/tags/netlify/"},{"name":"icarus","slug":"icarus","link":"/tags/icarus/"},{"name":"regex","slug":"regex","link":"/tags/regex/"},{"name":"develop","slug":"develop","link":"/tags/develop/"},{"name":"jsDelivr","slug":"jsDelivr","link":"/tags/jsDelivr/"},{"name":"ai","slug":"ai","link":"/tags/ai/"},{"name":"gemini","slug":"gemini","link":"/tags/gemini/"},{"name":"devops","slug":"devops","link":"/tags/devops/"},{"name":"ci/cd","slug":"ci-cd","link":"/tags/ci-cd/"},{"name":"json-ld","slug":"json-ld","link":"/tags/json-ld/"},{"name":"structured data","slug":"structured-data","link":"/tags/structured-data/"},{"name":"antigravity","slug":"antigravity","link":"/tags/antigravity/"},{"name":"ide","slug":"ide","link":"/tags/ide/"},{"name":"cjs","slug":"cjs","link":"/tags/cjs/"},{"name":"esm","slug":"esm","link":"/tags/esm/"},{"name":"search engine","slug":"search-engine","link":"/tags/search-engine/"},{"name":"ai rag","slug":"ai-rag","link":"/tags/ai-rag/"},{"name":"nvm","slug":"nvm","link":"/tags/nvm/"}],"categories":[{"name":"Backend","slug":"Backend","link":"/categories/Backend/"},{"name":"Other","slug":"Other","link":"/categories/Other/"},{"name":"PHP","slug":"Backend/PHP","link":"/categories/Backend/PHP/"},{"name":"Frontend","slug":"Frontend","link":"/categories/Frontend/"},{"name":"Git","slug":"Other/Git","link":"/categories/Other/Git/"},{"name":"VSCode","slug":"Other/VSCode","link":"/categories/Other/VSCode/"},{"name":"Linux","slug":"Backend/Linux","link":"/categories/Backend/Linux/"},{"name":"MySQL","slug":"Backend/MySQL","link":"/categories/Backend/MySQL/"},{"name":"Hexo","slug":"Other/Hexo","link":"/categories/Other/Hexo/"},{"name":"Html","slug":"Frontend/Html","link":"/categories/Frontend/Html/"},{"name":"Css","slug":"Frontend/Css","link":"/categories/Frontend/Css/"},{"name":"GitHub","slug":"Other/GitHub","link":"/categories/Other/GitHub/"},{"name":"JavaScript","slug":"Frontend/JavaScript","link":"/categories/Frontend/JavaScript/"},{"name":"NPM","slug":"Other/NPM","link":"/categories/Other/NPM/"},{"name":"Tool","slug":"Other/Tool","link":"/categories/Other/Tool/"},{"name":"JQuery","slug":"Frontend/JQuery","link":"/categories/Frontend/JQuery/"},{"name":"API","slug":"Backend/API","link":"/categories/Backend/API/"},{"name":"Netlify","slug":"Backend/Netlify","link":"/categories/Backend/Netlify/"},{"name":"Nodejs","slug":"Backend/Nodejs","link":"/categories/Backend/Nodejs/"},{"name":"Develop","slug":"Backend/Develop","link":"/categories/Backend/Develop/"}],"pages":[{"title":"404 - 網頁不存在","text":"var redirects = { \"/PHP/4-Ways-to-Check-If-a-Variable-or-Array-Exists/\": \"/Backend/PHP/4-Ways-to-Check-If-a-Variable-or-Array-Exists/\", \"/VSCode/Using-Git-Bash-in-VSCode/\": \"/Other/VSCode/Using-Git-Bash-in-VSCode/\", \"/Backend/MySQL/Execution-Order-and-Performance-Optimization-of-DQL-Commands/\": \"/Backend/MySQL/DQL-Execution-Order/\", }; var path = window.location.pathname; if (redirects[path]) { window.location.replace(redirects[path]); } 這是一個不存在的頁面 抱歉,找不到你要的頁面 😢將在 5 秒後返回首頁。 let countTime = 5; function count() { document.getElementById('timeout').textContent = countTime; countTime -= 1; if (countTime === 0) { location.href = 'https://forgetfulengineer.github.io'; } setTimeout(() => { count(); }, 1000); } count();","link":"/404.html"},{"title":"Cookie Policy","text":"甚麼是 cookie?Cookie 是由網站儲存在使用者瀏覽器中的小型文字檔案,用於在不同的頁面或訪問之間存儲數據。它們主要用來保存使用者資訊,以便在使用者再次訪問網站時提供個性化的體驗。 更詳細的說明可查看 【資安趨勢部落格】cookie 介紹 在哪裡使用 cookie本網站使用了一些第三方服務 Google Analytics : 用來紀錄訪客數據 Twikoo 評論系統 : 用來讓訪客回覆,紀錄訪客資訊 (名稱、信箱等) 禁用 cookie您可以經由瀏覽器的設定,取消或限制 cookie (操作方式請至瀏覽器幫助查詢),但我並不建議您禁用 cookie,禁用可能會導致您喪失網站的一些功能。","link":"/cookie-policy/index.html"},{"title":"About Me","text":"嗨,我是健忘工程師!建立這個網站的原因其實很單純,就是用來紀錄我的學習筆記。它對我來說,就像一個整理過的備忘錄。 在這個有搜尋引擎與 LLM 的時代,我認為不需要把所有事情都牢牢記住,真正重要的是理解和實作,而不是死記硬背。常用的觀念自然會熟悉,不常用、容易忘記的內容,就記錄下來,等需要的時候,再回來查閱就好😆 與其強迫自己記住一切,不如建立一套屬於自己的知識索引!健忘沒關係,努力有痕跡,健忘沒關係,好險有筆記~ 本站使用 Hexo 作為靜態網站框架,主題為 Icarus 的個人修改版。","link":"/about/index.html"},{"title":"【HTML】解析 <a> 的 rel 屬性 - window.opener 惡意連結實例","text":"原視窗重新導向到首頁如果帶有 target="_blank" 的連結沒有加上 rel 的 noopener 屬性,新視窗可以使用 window.opener 控制原視窗重新導向指定網址。 js12// 原視窗重新導向到首頁window.opener.location.href = "https://forgetfulengineer.github.io/"; 開啟原視窗的 alert如果帶有 target="_blank" 的連結沒有加上 rel 的 noopener 屬性,新視窗可以使用 window.opener 開啟原視窗的 alert。 js12// 開啟原視窗的 alertwindow.opener.alert('你點到惡意連結了'); 操作原視窗的 DOM如果帶有 target="_blank" 的連結沒有加上 rel 的 noopener 屬性,新視窗可以使用 window.opener 操作原視窗的 DOM。 js12// 操作原視窗的 DOMwindow.opener.document.querySelector('body').innerHTML='<b style="color: red; font-size: 100px; margin: auto;">你點到惡意連結了</b>'; 健忘筆記 現代瀏覽器在大部分情況下已經自動為帶有 target="_blank" 的連結加上 rel="noopener",所以有沒有設定 noopener 使用 window.opener 都會得到 null,但為了確保相容性和覆蓋到所有情況,依然建議明確地設定 rel="noopener"。 😈 惡意連結實例 原視窗重新導向到首頁 開啟原視窗的 alert 操作原視窗的 DOM 回到原文章 let urlParams = new URLSearchParams(window.location.search); let example = (['1', '2', '3'].includes(urlParams.get('example'))) ? urlParams.get('example') : 1; let element = document.querySelector(\".example-\" + example); element.classList.remove(\"is-hidden\"); switch (example) { case '1': default: window.opener.location.href = \"https://forgetfulengineer.github.io/\"; break; case '2': window.opener.alert('你點到惡意連結了'); break; case '3': window.opener.document.querySelector('body').innerHTML='你點到惡意連結了'; break; }","link":"/example/Malicious-Links-Using-window-opener.html"}]}