2023-09-04

NextJS app router 實作 body-parser 功能

NextJS app router 實作 body-parser 功能

承上篇,NextJS 的 app router 所使用的 request 和 response 分別是 NextRequest 和 NextResponse 類型;又分別是 JS 裡 Request 和 Response 類型的延伸。所以解析 http body 要遵循 Request 和 Response 類型的 api 用法。

底下的工具,借用 qs 套件的功能解析 urlencoded 的格式。實作了三個功能:

  1. getQueryString(): 解析 query string parameters
  2. getBodyData(): 解析表單資料(包含 multipart 格式)
  3. getMultipartData(): 解析上傳的表單
// *** request-parser.js import qs from "qs"; import fs from "node:fs/promises"; import { v4 as uuidV4 } from "uuid"; /** 解析 query string */ export function getQueryString(request) { return qs.parse(request.url.split("?")[1]); } /** 解析 urlencoded, json, multipart */ export async function getBodyData(request) { let result = {}; const method = request.method.toUpperCase(); const type = request.headers.get("Content-Type"); if (method !== "GET") { if (type === "application/x-www-form-urlencoded") { const txt = await request.text(); result = txt.length ? qs.parse(txt) : {}; } else if (type === "application/json") { try { result = await request.json(); } catch (ex) {} } else { const { fields } = await getMultipartData(request); if (fields) result = { ...fields }; } } return result; } /** 解析 multipart */ export async function getMultipartData( request, // NextRequest 物件 acceptedMimetypes = [], // 篩選類型設定,預設為不篩選 useUuidFilename = true, // 使用隨機 uuid 為主檔名 uploadDir = "./tmp" // 上傳的資料夾 ) { let result = { fields: undefined, files: undefined, error: undefined, }; const method = request.method.toUpperCase(); let type = request.headers.get("Content-Type"); if (!type) return result; type = type.split(";")[0]; // 取得 mimetype if (method !== "GET") { if (type === "multipart/form-data") { const formData = await request.formData(); try { await fs.access(uploadDir, fs.constants.F_OK); } catch (ex) { // 建立上傳的資料夾 await fs.mkdir(uploadDir, { recursive: true }); } for (const [k, v] of formData.entries()) { if (typeof v === "string") { // 處理文字欄位 result.fields = result.fields || {}; if (!result.fields[k]) { result.fields[k] = v; } else { if (result.fields[k] instanceof Array) { result.fields[k].push(v); } else { result.fields[k] = [result.fields[k], v]; } } } else if (v instanceof Blob) { // 處理檔案欄位 result.files = result.files || {}; const { size, type, name, lastModified } = v; if (acceptedMimetypes.length) { if (!acceptedMimetypes.includes(type)) { continue; } let filename = name; if (useUuidFilename) { let tmpName = name.toLowerCase(); let mainName = uuidV4(); let extName = ""; if (tmpName.indexOf(".") !== -1) { const frs = tmpName.split("."); extName = "." + frs[frs.length - 1]; } filename = mainName + extName; } const path = uploadDir + "/" + filename; const fileData = { size, type, lastModified, originalName: name, filename, path, }; if (!result.files[k]) { result.files[k] = fileData; } else { if (result.files[k] instanceof Array) { result.files[k].push(fileData); } else { result.files[k] = [result.files[k], fileData]; } } await fs.writeFile(path, v.stream()); } } } } } return result; }

以下是在 route.js 中的用法範例。

import { getBodyData, getQueryString, getMultipartData, } from "@/modules/request-parser"; const responseInit = { status: 200, headers: { "Content-Type": "application/json", }, } export async function GET(request) { let output = getQueryString(request); return new Response(JSON.stringify(output), responseInit); } export async function POST(request) { let body = await getBodyData(request); return new Response(JSON.stringify(body), responseInit); } export async function PUT(request) { let multi = await getMultipartData(request, ["image/jpeg"]); return new Response(JSON.stringify(multi), responseInit); }

2023-09-03

NextJS app router 實作 session 功能

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 資料。

FB 留言