0%

最近在部署專案到 Vercel 時,遇到了一個讓我困擾的錯誤訊息:「Unexpected token ‘<’」。這個錯誤通常表示我們預期收到的是 JSON 格式的資料,但實際上卻收到了 HTML 頁面。本文將分享我遇到這個問題的背景、原因,以及兩種解決方案,幫助大家在使用 Vercel 部署時能夠順利取得 API 資料。

部署至 Vercel 遇到「Unexpected token ‘<’」錯誤:原因與解法

在將專案部署到 Vercel 時,當我透過瀏覽器或 extension background fetch 呼叫 API,卻出現以下錯誤:

1
取得資料夾失敗: Unexpected token '<', "<!doctype "... is not valid JSON

那代表請求並沒有真正拿到 JSON,而是得到了 HTML 頁面。本文將說明這個問題的原因、背後的背景,以及兩種完整的解法。

問題說明

這次在 Arc 瀏覽器 的開發過程中,我嘗試從部署於 Vercel 的 API 端點取得資料,但 Console 顯示:

1
index.iife_dev.js:24922 取得資料夾失敗: Unexpected token '<', "<!doctype "... is not valid JSON

進一步到 Arc 的 Service-Worker DevToolsarc://inspect/#service-workers)中,打開 background worker 的 Console,可以看到請求的回應實際上是一份 HTML 登入頁面,而非 JSON 資料。


背景補充:Vercel 的 Deployment Protection

這個錯誤並非來自 CORS,而是因為 Vercel 的 Deployment Protection 機制。
當專案開啟「Vercel Authentication」後,所有 Preview Deployments 都需要登入才能訪問。

因此,extension background 在發出請求時,被導向至登入頁面(HTML),導致解析 JSON 時出現:

Unexpected token '<'

Read more »

題目描述

You are given the heads of two sorted linked lists list1 and list2.

Merge the two lists into one sorted list. The list should be made by splicing together the nodes of the first two lists.

Return the head of the merged linked list.

給定兩個已排序鏈結串列的頭節點 list1 和 list2。
將兩個串列合併為一個已排序的串列。該串列應透過拼接前兩個串列的節點來建立。
回傳合併後鏈結串列的頭節點。

Example 1:
Input: list1 = [1,2,4], list2 = [1,3,4]
Output: [1,1,2,3,4,4]

Example 2:
Input: list1 = [], list2 = []
Output: []

Example 3:
Input: list1 = [], list2 = [0]
Output: [0]

解題思路

我一開始想法,會需要 2 迴圈去比較,然後組出一個新的 list node,最後再將新的 list node 回傳。
但是這樣的話,時間複雜度會是 O(n^2),因為每次都要從頭開始比較。
後來想說,因為兩個 list 都是已排序好的,所以可以用雙指標的方式,一次比較兩個 list 的頭節點,將較小的節點加入新的 list node,然後將該指標往後移動一位,這樣時間複雜度就會是 O(n),因為每個節點只會被比較一次。

範例程式碼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
* Definition for singly-linked list.
* function ListNode(val, next) {
* this.val = (val===undefined ? 0 : val)
* this.next = (next===undefined ? null : next)
* }
*/
/**
* @param {ListNode} list1
* @param {ListNode} list2
* @return {ListNode}
*/
var mergeTwoLists = function(list1, list2) {
let head = null
let nowNode = null
let list1pointer = list1;
let list2pointer = list2;
// 兩兩比較
if(list1 === null && list2 === null) return head
if(list1 === null && list2 !== null) return list2
if(list1 !== null && list2 === null) return list1


// 決定 head
// 因為當你選擇了 list1pointer 作為頭節點後,你應該:
// 移動 list1pointer 指針:list1pointer = list1pointer.next
// 設定 nowNode 為當前的尾節點:nowNode = head
if(list1pointer.val <= list2pointer.val ){
head = list1pointer
list1pointer = list1pointer.next
}else{
head = list2pointer
list2pointer = list2pointer.next
}
nowNode = head

//進入迴圈檢查
while(list1pointer !== null && list2pointer !== null){
if(list1pointer.val <= list2pointer.val){
nowNode.next = list1pointer
list1pointer = list1pointer.next
}else {
nowNode.next = list2pointer
// 移動指針
list2pointer = list2pointer.next
}
nowNode = nowNode.next;
}
// 最後要再檢查。其中一不為 null
if(list1pointer !== null){
nowNode.next = list1pointer
}else{
nowNode.next = list2pointer
}

return head

};
Read more »

Given an array of meeting time intervals where intervals[i] = [startᵢ, endᵢ], determine if a person could attend all meetings.

給定一個會議時間 intervals 陣列,其中 intervals[i] = [startᵢ, endᵢ],判斷一個人是否能夠參加所有會議。

Example 1:

Input:

intervals = [[0,30],[5,10],[15,20]]

Output:

false

Example 2:

Input:

intervals = [[7,10],[2,4]]

Output:

true

Constraints:

  • 0 <= intervals.length <= 10⁴
  • intervals[i].length == 2
  • 0 <= startᵢ < endᵢ <= 10⁶

解題思路

要判斷一個人是否能夠參加所有會議,關鍵在於檢查會議時間是否有重疊。若有任何兩個會議的時間區間重疊,則無法參加所有會議。

重疊情況分析

[1,5][2,4] 為範例:

  1. 假設會議時間按開始時間排序:將所有會議按照開始時間由早到晚排列
  2. 檢查相鄰會議:排序後,只需檢查相鄰的會議是否重疊
  3. 重疊條件:前一個會議的結束時間 > 下一個會議的開始時間

步驟

  1. 將所有會議按開始時間排序
  2. 遍歷排序後的會議陣列
  3. 檢查每個會議的結束時間是否晚於下一個會議的開始時間
  4. 若發現重疊,回傳 false;否則回傳 true

這樣的方法時間複雜度為 O(n log n)(主要是排序的時間),空間複雜度為 O(1)。

Read more »

列表的動態排序和插入為常見的需求。然而,當面對大量資料時,傳統的「刪除重建」策略容易會成為效能瓶頸。本文將分享我在專案中如何透過 SeqNo 管理機制,將列表插入效能提升,同時保持資料一致性和系統穩定性。

在專案內有一下拉列表的功能,當使用者選擇某個選項後,會將該選項插入到列表中,並且會依據使用者的操作順序來進行排序。最初的實作方式是每次插入新選項時,都會重新計算整個列表的排序,這在資料量較大時,導致效能明顯下降。為了解決這個問題,引入了 SeqNo 管理機制。

原有問題

當使用者要在列表中間插入新的資料時,使系統面臨挑戰,並且在沒有加入 seqNo 管理機制前,容易遇到排序衝突的問題。

1
2
3
4
5
6
7
// 原有狀態
[
{ id: 'A', seqNo: 1 },
{ id: 'B', seqNo: 2 }, // 要在 B 後面插入新項目
{ id: 'C', seqNo: 3 },
{ id: 'D', seqNo: 4 }
]

如果直接將新項目設為 seqNo: 3,會與現有的項目 C 發生衝突。

傳統解決方案的困境

直觀的解決方案是重新排序所有項目:

1
2
3
4
5
6
7
8
9
10
// 刪除重建
async function insertListData(afterId: string,
newListData: ListData) {
// 1. 刪除所有後續項目 (N-M 次刪除)
await deleteListDataAfter(afterId);

// 2. 重新建立所有項目 (N-M+1 次建立)
await recreateListDataWithNewSeqNo(newListData,
subsequentListData);
}
  • 操作複雜度: O(2N+1) - 其中 N 為列表總長度
  • 資料庫壓力: 大量刪除和建立操作
  • 交易風險: 操作步驟過多,失敗機率高
  • 併發問題: 長時間鎖定,容易產生競爭條件

SeqNo 管理機制

只更新真正需要變動的項目,將複雜度進行改善,是插入點之後的項目數量。

  1. 排序基礎函式
    確保所有項目能依照 seqNo 正確排序。
1
2
3
4
5
6
7
8
// 排序工具
export function sortListDataBySeqNo(listData: ListData[]): ListData[] {
return [...listData].sort((a, b) => {
const aSeqNo = a.seqNo || 0;
const bSeqNo = b.seqNo || 0;
return aSeqNo - bSeqNo;
});
}
  1. 插入策略計算

只計算「插入點之後」需要調整的項目,並產生更新操作清單。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
 export function calculateInsertStrategy(
existingListData: ListData[],
afterListDataId: string
): {
insertSeqNo: number;
affectedListData: ListData[];
updateOperations: Array<{ listDataId: string; newSeqNo: number }>;
} {
const sortedListData = sortListDataBySeqNo(existingListData);
const afterIndex = sortedListData.findIndex(data => data.id === afterListDataId);

if (afterIndex === -1) {
throw new Error('afterListDataId not found');
}

// 關鍵:插入點的 seqNo + 1
const insertSeqNo = sortedListData[afterIndex].seqNo! + 1;
// 只影響插入點之後的項目
const affectedListData = sortedListData.slice(afterIndex + 1);

// 產生更新操作清單
const updateOperations = affectedListData.map(data => ({
listDataId: data.id,
newSeqNo: data.seqNo! + 1
}));

return { insertSeqNo, affectedListData, updateOperations };
}

  1. 交易執行
    確保所有更新操作能在同一個 Transaction 中完成。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
export async function executeSeqNoUpdates(
transaction: Transaction,
operations: Array<{ listDataId: string; newSeqNo: number }>
): Promise<void> {
for (const operation of operations) {
// 更新每個受影響的項目
const listDataRef = adminDb.collection('listData').doc(operation.listDataId);
transaction.update(listDataRef, {
seqNo: operation.newSeqNo,
updatedAt: new Date()
});
}
}

API 實現範例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
  // POST /api/v1/listData
export async function POST(req: Request) {
// ... 驗證邏輯

if (afterListDataId) {
try {
// 步驟1: 計算最小更新策略
const { updateOperations, insertSeqNo } =
calculateInsertStrategy(existingListData, afterListDataId);

// 步驟2: Transaction 執行
const result = await adminDb.runTransaction(async (transaction) => {
await executeSeqNoUpdates(transaction, updateOperations);

// 插入新項目
const listDataRef = adminDb.collection('listData').doc();
transaction.set(listDataRef, {
...newListData,
seqNo: insertSeqNo
});

return { id: listDataRef.id, seqNo: insertSeqNo };
});

return NextResponse.json(result, { status: 201 });
} catch (error) {
if (error.message === 'afterListDataId not found') {
return NextResponse.json({ message: 'afterListDataId not found' }, { status: 404 });
}
throw error;
}
}
}

此專案的應用回顧與心得

這個問題最初是在「插入列表中間」時被發現的。
當時經常出現排序錯亂,追查後才發現是因為缺乏 SeqNo 管理機制,導致插入時 seqNo 發生衝突。

一開始的解法是 刪除重建,但這帶來兩個明顯的問題:
1.當資料量大時,對後端資料庫造成極大負擔。
2.使用者操作無法預測,若頻繁在列表中間插入或刪除,效能會快速下降。

引入 SeqNo 管理機制 後:

  • 插入效能明顯提升
  • 保持了資料一致性
  • 系統穩定性也大幅改善
    這樣的設計不僅解決了排序衝突,也大幅減少了資料庫的操作次數,讓整體效能更加穩定可

在開發 SaaS 或工具型應用時,「使用者驗證系統」往往是關鍵基礎設施。這篇文章將分享我如何使用 Next.js(App Router)+ MongoDB + NextAuth,同時支援 Google OAuth 和 自定義帳密登入,並分享專案 mongoDB 資料結構與 API 架構。

MongoDB 資料模型設計

為了支援多元登入方式,我設計了以下幾個集合(Collections):

  1. users - 使用者基本資訊
1
2
3
4
5
6
7
8
{
_id: ObjectId,
email: String,
name: String,
createdAt: Date,
updatedAt: Date
}

  1. authProviders - 自定義驗證方式(帳密)
Read more »

LeetCode 題目多樣,尤其是「二元樹(Binary Tree)」相關題型,容易讓人看了發愣。

本篇文章將教你如何釐清解法邏輯,並將二元樹題型拆解為兩大類解題策略,搭配範例與模板,幫助你用 DFS(深度優先搜尋)更系統地解題

類型一:遍歷型(Traversal Type)

這類題目需要「走過整棵樹的所有節點」,通常是為了計算某種總量,例如:

  • 節點總數
  • 最大深度
  • 所有路徑總和
  • 葉子節點的數量

解題思路

可根據需求使用:

  • 後序遍歷 + 回傳值 return → 適合需要組合子樹結果的情況
  • 前序遍歷 + 狀態變數追蹤 → 適合需要累加或記錄路徑狀態的情況

模板(後序 + return)

1
2
3
4
5
6
function dfs(node) {
if (node === null) return baseValue;
const left = dfs(node.left);
const right = dfs(node.right);
return combine(left, right, node);
}

模板(前序 + 狀態變數)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
let result = 0;
let pathDepth = 0;

function traverse(node) {
if (node === null) return;

pathDepth++; // 遞迴深入,深度 +1
if (node.left === null && node.right === null) {
result = Math.max(result, pathDepth); // 若為葉子,更新最大深度
}

traverse(node.left);
traverse(node.right);

pathDepth--; // 回歸
}
Read more »

在前端開發中,動畫與互動是提升用戶體驗的關鍵元素。然而,當動畫與元件狀態、生命周期交織在一起時,實作上就可能遇到各種挑戰。

這篇文章記錄我在一個 React 專案中,解決「動畫重複觸發與重置」問題的過程,深入比較了使用 key 重置與 CSS Reflow(offsetWidth) 重置的方式

問題概述

在這個專案中,我有兩個自訂的元件,各自內部包含一個相同結構的 <input>。當使用者點擊某個操作後,我希望這些 input:

Read more »

練習來自 labuladong 的演算法筆記 的滑動視窗演算法核心程式碼模板。

基本設置:

用兩個指針來界定一個區間,也就是一個「窗口」:

  • 左指針 left:表示窗口的起始位置。
  • 右指針 right:表示窗口的尾部(通常採用左閉右開區間 [left, right))。

窗口內的資料會隨著 left 和 right 的移動而動態改變。解題的關鍵在於:

  • 擴大窗口:不斷將新的元素加入窗口,並更新窗口內的狀態或統計(例如字元出現次數)。
  • 檢查窗口是否滿足條件:根據題目要求檢查當前窗口是否為合法解。
  • 收縮窗口:如果當前窗口已滿足條件,嘗試從左側移除元素以找到更優(例如更短)的解,同時更新狀態。

模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var slidingWindow = function(s) {
// 使用合適的資料結構記錄視窗中的資料,根據具體題目變通。
// 例如:記錄字元出現的次數,或累加窗口內元素的和。
let window = {};

let left = 0, right = 0;
while (right < s.length) {
// 取出將要加入視窗的字元
let c = s[right];
right++; // 擴大窗口

// 更新窗口內資料
// 例如:window[c] = (window[c] || 0) + 1;
// 根據具體題目邏輯進行更新

// *** Debug 輸出位置 ***
// console.log("窗口: [%d, %d)", left, right);

// 當窗口不再滿足題目要求時,收縮窗口
while (/* 判斷窗口需要收縮的條件 */) {
// 取得將要移除的字元(為使變數名稱更具描述性可用 removedChar)
let removedChar = s[left];
left++; // 縮小窗口

// 更新移除字元對窗口資料的影響
// 例如:if (window[removedChar] ...) { ... }
}
}
};

題目

Given two strings s1 and s2, return true if s2 contains a permutation of s1, or false otherwise.

In other words, return true if one of s1’s permutations is the substring of s2.

給定兩個字串 s1 和 s2,若 s2 中包含 s1 的某個排列,則返回 true;否則返回 false。

換句話說,若 s1 的某個排列是 s2 的子字串,則返回 true。

Example 1:

Input: s1 = “ab”, s2 = “eidbaooo”
Output: true
Explanation: s2 contains one permutation of s1 (“ba”).

Read more »