NextJS app router 實作 body-parser 功能
承上篇,NextJS 的 app router 所使用的 request 和 response 分別是 NextRequest 和 NextResponse 類型;又分別是 JS 裡 Request 和 Response 類型的延伸。所以解析 http body 要遵循 Request 和 Response 類型的 api 用法。
底下的工具,借用 qs 套件的功能解析 urlencoded 的格式。實作了三個功能:
- getQueryString(): 解析 query string parameters
- getBodyData(): 解析表單資料(包含 multipart 格式)
- 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);
}
沒有留言:
張貼留言