以往資料太大超過 URL 的長度 2048 限制,都會改使用 POST 處理。
沒想到 Next.js 處理 POST 居然卡關了!!!!
先說明一下情境:
舊網站輸入資料,需用開新分頁的方式呈現結果,新分頁開啟 Next.js 新網站
最一開始是這樣處理
- 舊網站輸入資料後 submit 到 Next.js 伺服器端
- 伺服器端接收到資料存在全域變數,並轉址到新的頁面
- 客戶端發 Request 給伺服器端取得全域變數的值渲染
//app/api/getFormData/route.js
let formData = null;
export async function POST(request) {
formData = await request.text();//存到全域變數
return new Response(null, {
status: 302,
headers: {
'Location': '/event/history',
},
});
}
export async function GET() {
if (formData) {
//雖然這裡是GET,但這裡是直接回傳數據,不受 URL 長度限制
return new Response(JSON.stringify({ data: formData }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
} else {
return new Response(JSON.stringify({ message: 'No data' }), {
status: 200,
headers: { 'Content-Type': 'application/json' },
});
}
}
//app/event/history/page.tsx
'use client';
import { useState, useEffect } from 'react';
export default function EventHistory() {
const [data, setData] = useState('');
useEffect(() => {
const getInitData = async () => {
const res = await fetch('/api/getFormData');
const result = await res.json();
const formData = new URLSearchParams(result.data);
setData(formData.getAll('data').join(','));
};
getInitData();
}, []);
return (
<div>
<h1>Data</h1>
{data ? (
<pre>{JSON.stringify(data, null, 2)}</pre>
) : (
<p>Loading...</p>
)}
</div>
);
}
結果上線被 user 反映輸入的數值跟呈現的結果不一樣。
燈愣!!!!What happen???!!!
原來是因為 Next.js 伺服器端全域變數的生命週期不是 per request,而是服務啟動時就會一直存在的。
let formData = null;
後來有嘗試跟想了幾種方法,但都不是很符合我的需求
- POST 到伺服器端之後直接回傳 HTML 渲染
- 無法開新分頁
- 要在新網站 POST 才可以
- 重新刷新頁面就沒有資料了
- 全域變數加上 UUID
- 全域變數的資料不清除的話,記憶體會一直不斷往上成長,找不到清除資料的時機
- 採用負載平衡,存在記憶體資料不共享
- 存 DB/Redis
- 殺雞用牛刀的感覺
- 存 Localstorage
- 可能面臨儲存上限問題(5MB),一樣有資料清理的問題
最後的取捨下,決定將 POST 資料轉為查詢字串(QueryString),這樣客戶端就可以透過 URL 取得資料。
先前有提到用 POST 是因為資料量大,轉成查詢字串的話可能面臨超過 URL 長度 2048 的問題,因此開始試著將 POST 的資料壓縮再傳送。
嘗試了兩個套件
npm i lz-string
//app/api/getFormData/route.js
import LZString from 'lz-string';//import lz-string
export async function POST(request) {
let formData = await request.text();
const compressData = LZString.compressToBase64(formData);//壓縮
const encodedData = encodeURIComponent(compressData);
console.log('origin/compress/encode length:', formData.length, 'to', compressData.length, 'to', encodedData.length);
return new Response(null, {
status: 302,
headers: {
'Location': `/event/history?data=${encodedData}`,
},
});
}
//app/event/history/page.tsx
'use client';
import { useSearchParams } from 'next/navigation';
import { useState, useEffect } from 'react';
import LZString from 'lz-string';//import lz-string
export default function EventHistory() {
const searchParams = useSearchParams();
const [data, setData] = useState('');
useEffect(() => {
const encodedData = searchParams.get('data');
if (encodedData) {
const decodedData = LZString.decompressFromBase64(decodeURIComponent(encodedData));//解壓縮
setData(decodedData);
}
}, [searchParams]);
return (
<div>
<h1>Data</h1>
{data ? (
<pre>{JSON.stringify(data, null, 2)}</pre>
) : (
<p>Loading...</p>
)}
</div>
);
}
結果
npm i pako
//app/api/getFormData/route.js
const zlib = require('zlib');
export async function POST(request) {
let formData = await request.text();
//const compressData = Buffer.from(pako.deflate(formData, { to: 'string' })).toString('base64');
const compressData = zlib.deflateSync(formData).toString('base64');//壓縮
const encodedData = encodeURIComponent(compressData);
console.log('origin/compress/encode length:', formData.length, 'to', compressData.length, 'to', encodedData.length);
return new Response(null, {
status: 302,
headers: {
'Location': `/event/history?data=${encodedData}`,
},
});
}
//app/event/history/page.tsx
'use client';
import { useSearchParams } from 'next/navigation';
import { useState, useEffect } from 'react';
const pako = require('pako');
export default function EventHistory() {
const searchParams = useSearchParams();
const [data, setData] = useState('');
useEffect(() => {
const encodedData = searchParams.get('data');
if (encodedData) {
const decodedData = pako.inflate(Uint8Array.from(atob(decodeURIComponent(encodedData)), c => c.charCodeAt(0)), { to: 'string' });//解壓縮
setData(decodedData);
}
}, [searchParams]);
return (
<div>
<h1>Data</h1>
{data ? (
<pre>{JSON.stringify(data, null, 2)}</pre>
) : (
<p>Loading...</p>
)}
</div>
);
}
結果
可能因為我的資料量還是很少,所以雖然網路上說 LZ-String 的速度比較快,但我兩個測起來的速度沒什麼特別的差異。
Pako 因為是使用 DEFLATE 的壓縮算法,而 LZ-String 使用 LZW 的算法,因算法的關係,Pako 的效能會比較好,這個倒是很有感。