NextJS app router 實作 session 功能
NextJS 的 app router 所使用的 request 和 response 分別是 NextRequest 和 NextResponse 類型;又分別是 JS 裡 Request 和 Response 類型的延伸。而不像 NodeJS 裡的 IncomingMessage 和 ServerResponse,有著彈性的功能。
比較大的問題是,ExpressJS 裡的 request 和 response 是從頭「流」到尾的機制。我們可以很方便在 middlewares 處理完資料,掛載資料到 request 或 response.locals 的屬性上。
然而 NextJS 的 middleware 卻不是這樣運作的,request 物件可能是透過 clone 的方式再往下一個階段傳遞。所以在 middleware 取得的 request 和在 route.js 裡拿到的 request 是不相同(不同參照)的物件。如此就無法藉由 request 物件來傳遞資料。沒有接收 response 物件的情況下,更難使用 response 物件。
後來才找到 Passing data from middleware to API route,利用 middleware 的 response.rewrite(),將資料放在 cookie 裡,往 end-point 傳遞。
shin-session-obj.js 為實作取得 session 物件的 getSessionObj() 函式。回傳值中若 new_session_id 不為空字串,表示要設定新的 session-id。為了簡明,裡面用 Object 來實作 session 的功能,當然你也可以 DB 或 files 來實作,不過就不在此說明。
// modules/shin-session-obj.js
import { validate, v4 as uuidV4 } from "uuid";
let sessions;
export function getSessionObj(request) {
let new_session_id = "";
if (!request) return {}; // 沒有 request 時
let sid = request.cookies.get("shin-session-id")?.value;
// 沒有 session_id 時, 或 session_id 不是 uuid 時, 設定新的 session-id
if (!sid || !validate(sid)) {
new_session_id = uuidV4();
sid = new_session_id;
console.log({new_session_id})
}
// 初始化 sessions
if (!sessions) {
sessions = {};
}
// 初始化 session
if (!sessions[sid]) {
sessions[sid] = {};
}
return {
session: sessions[sid],
new_session_id,
};
}
在 middleware 會呼叫 getSessionObj() 以測試是否取得 new_session_id。若取得,則在複製的 request 物件裡設定 cookie 內容為我們要傳下傳遞的 session-id;同時,在 response 物件內設定要用戶端儲存的 session-id(儲存於 cookie)。
import { NextResponse } from "next/server";
import { getSessionObj } from "@/modules/shin-session-obj";
export async function middleware(nextRequest, nextFetchEvent) {
const {session, new_session_id} = getSessionObj(nextRequest);
if(new_session_id) {
const clonedRequest = nextRequest.clone();
clonedRequest.headers.append("Cookie", `shin-session-id=${new_session_id}`);
const response = NextResponse.rewrite(nextRequest.url.toString(), {
request: clonedRequest,
});
response.cookies.set({
name: "shin-session-id",
value: new_session_id,
path: "/",
});
return response;
}
}
在任何的 route.js 就可以直接使用 getSessionObj() 取得對應的 session 物件。
import { getSessionObj } from "@/modules/shin-session-obj";
export async function GET(request) {
const {session} = getSessionObj(request);
if(session){
session.count = session.count || 0;
session.count++;
}
const response = new Response(JSON.stringify({session}), {
status: 200,
headers: {
"Content-Type": "application/json",
},
});
return response;
}
要注意的是此實作,主要是為了說明實作方式,在正式環境使用記憶體存放 session 資料有許多問題。最好還是以 DB 或檔案來存放 session 資料。
沒有留言:
張貼留言