【Next.js】Day 5 – 壓縮 data 並透過 URL 查詢參數傳遞

以往資料太大超過 URL 的長度 2048 限制,都會改使用 POST 處理。
沒想到 Next.js 處理 POST 居然卡關了!!!!

先說明一下情境:
舊網站輸入資料,需用開新分頁的方式呈現結果,新分頁開啟 Next.js 新網站

最一開始是這樣處理

  1. 舊網站輸入資料後 submit 到 Next.js 伺服器端
  2. 伺服器端接收到資料存在全域變數,並轉址到新的頁面
  3. 客戶端發 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 的資料壓縮再傳送。

嘗試了兩個套件

lz-string

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>
  );
}

結果


pako

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 的效能會比較好,這個倒是很有感。

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *