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

沒有留言:

FB 留言