使用 `reply.raw.end` 时如何触发 fastify `onSend` 生命周期
How to trigger fastify `onSend` lifecycle when using `reply.raw.end`
简短版本:
如何让 fastify onSend
在使用 reply.raw.end
时触发?
这是必需的,以便我可以使用 fastify-session along side fastify-nextjs。
长版:
我目前正在尝试使用 fastify
设置自定义 NextJs
服务器。
fastify-nextjs
使用 reply.raw which is required by NextJs.
但是,似乎因为 NextJs
正在调用 reply.raw.end
,fastify
中的生命周期挂钩 onSend
从未被触发。
简答:
我创建了实现此行为的社区插件:@applicazza/fastify-nextjs
长答案:
由于 Next.js 直接操作 http.ServerResponse,无论它写入流什么都不会通过 fastify 的响应管道。
但是您可以使用 JavaScript Proxy 拦截对 NodeJs http.ServerResponse 的调用并将其传递给 Fastify。
import { FastifyPluginAsync, FastifyReply, FastifyRequest } from 'fastify';
import fastifyPlugin from 'fastify-plugin';
import { IncomingMessage, ServerResponse } from 'http';
import Next from 'next';
import { NextServer } from 'next/dist/server/next';
import fastifyStatic from 'fastify-static';
declare module 'fastify' {
interface FastifyInstance {
nextJsProxyRequestHandler: (request: FastifyRequest, reply: FastifyReply) => void;
nextJsRawRequestHandler: (request: FastifyRequest, reply: FastifyReply) => void;
nextServer: NextServer;
passNextJsRequests: () => void;
passNextJsDataRequests: () => void;
passNextJsDevRequests: () => void;
passNextJsImageRequests: () => void;
passNextJsPageRequests: () => void;
passNextJsStaticRequests: () => void;
}
}
declare module 'http' {
interface IncomingMessage {
fastify: FastifyRequest;
}
interface OutgoingMessage {
fastify: FastifyReply;
}
}
export interface FastifyNextJsOptions {
dev?: boolean;
basePath?: string;
}
const fastifyNextJs: FastifyPluginAsync<FastifyNextJsOptions> = async (fastify, { dev, basePath = '' }) => {
if (dev === undefined) {
dev = process.env.NODE_ENV !== 'production';
}
const nextServer = Next({
dev,
});
const nextJsProxyRequestHandler = function (request: FastifyRequest, reply: FastifyReply) {
nextServer.getRequestHandler()(proxyFastifyRawRequest(request), proxyFastifyRawReply(reply));
};
const nextJsRawRequestHandler = function (request: FastifyRequest, reply: FastifyReply) {
nextServer.getRequestHandler()(request.raw, reply.raw);
};
const passNextJsRequestsDecorator = () => {
fastify.passNextJsDataRequests();
fastify.passNextJsImageRequests();
if (dev) {
fastify.passNextJsDevRequests();
} else {
fastify.passNextJsStaticRequests();
}
fastify.passNextJsPageRequests();
};
const passNextJsDataRequestsDecorator = () => {
fastify.register((fastify, _, done) => {
fastify.route({
method: ['GET', 'HEAD', 'OPTIONS'],
url: '/data/*',
handler: nextJsProxyRequestHandler
});
done();
}, {
prefix: `${basePath}/_next`
});
};
const passNextJsDevRequestsDecorator = () => {
fastify.register((fastify, _, done) => {
fastify.route({
method: ['GET', 'HEAD', 'OPTIONS'],
url: '/static/*',
handler: nextJsRawRequestHandler
});
fastify.route({
method: ['GET', 'HEAD', 'OPTIONS'],
url: '/webpack-hmr',
handler: nextJsRawRequestHandler
});
done();
}, {
prefix: `${basePath}/_next`
});
};
const passNextJsImageRequestsDecorator = () => {
fastify.register((fastify, _, done) => {
fastify.route({
method: ['GET', 'HEAD', 'OPTIONS'],
url: '/image',
handler: nextJsRawRequestHandler
});
done();
}, {
prefix: `${basePath}/_next`
});
};
const passNextJsStaticRequestsDecorator = () => {
fastify.register(fastifyStatic, {
prefix: `${basePath}/_next/static/`,
root: `${process.cwd()}/.next/static`,
decorateReply: false,
});
};
const passNextJsPageRequestsDecorator = function () {
fastify.register((fastify, _, done) => {
fastify.route({
method: ['GET', 'HEAD', 'OPTIONS'],
url: '*',
handler: nextJsProxyRequestHandler,
});
done();
}, {
prefix: basePath || '/'
});
};
fastify.decorate('nextJsProxyRequestHandler', nextJsProxyRequestHandler);
fastify.decorate('nextJsRawRequestHandler', nextJsRawRequestHandler);
fastify.decorate('nextServer', nextServer);
fastify.decorate('passNextJsDataRequests', passNextJsDataRequestsDecorator);
fastify.decorate('passNextJsDevRequests', passNextJsDevRequestsDecorator);
fastify.decorate('passNextJsImageRequests', passNextJsImageRequestsDecorator);
fastify.decorate('passNextJsPageRequests', passNextJsPageRequestsDecorator);
fastify.decorate('passNextJsRequests', passNextJsRequestsDecorator);
fastify.decorate('passNextJsStaticRequests', passNextJsStaticRequestsDecorator);
await nextServer.prepare();
fastify.addHook('onClose', function () {
return nextServer.close();
});
};
const proxyFastifyRawRequest = (request: FastifyRequest) => {
return new Proxy(request.raw, {
get(target: IncomingMessage, property: string | symbol, receiver: unknown): unknown {
const value = Reflect.get(target, property, receiver);
if (typeof value === 'function') {
return value.bind(target);
}
if (property === 'fastify') {
return request;
}
return value;
}
});
};
const proxyFastifyRawReply = (reply: FastifyReply) => {
return new Proxy(reply.raw, {
get: function (target: ServerResponse, property: string | symbol, receiver: unknown): unknown {
const value = Reflect.get(target, property, receiver);
if (typeof value === 'function') {
if (value.name === 'end') {
return function () {
return reply.send(arguments[0]);
};
}
if (value.name === 'getHeader') {
return function () {
return reply.getHeader(arguments[0]);
};
}
if (value.name === 'hasHeader') {
return function () {
return reply.hasHeader(arguments[0]);
};
}
if (value.name === 'setHeader') {
return function () {
return reply.header(arguments[0], arguments[1]);
};
}
if (value.name === 'writeHead') {
return function () {
return reply.status(arguments[0]);
};
}
return value.bind(target);
}
if (property === 'fastify') {
return reply;
}
return value;
},
});
};
export default fastifyPlugin(fastifyNextJs, {
fastify: '3.x',
});
注册插件:
const dev = process.env.NODE_ENV !== 'production';
fastify.register(fastifyNextJs, { dev }).after(() => {
fastify.passNextJsImageRequests();
if (dev) {
fastify.passNextJsDevRequests();
} else {
fastify.passNextJsStaticRequests();
}
});
为会话创建上下文:
fastify.register(async (fastify) => {
fastify.register(fastifySession, {
// options
});
fastify.passNextJsDataRequests();
fastify.passNextJsPageRequests();
});
N.B。您必须在 Next.Js
中禁用压缩
module.exports = {
compress: false,
};
简短版本:
如何让 fastify onSend
在使用 reply.raw.end
时触发?
这是必需的,以便我可以使用 fastify-session along side fastify-nextjs。
长版:
我目前正在尝试使用 fastify
设置自定义 NextJs
服务器。
fastify-nextjs
使用 reply.raw which is required by NextJs.
但是,似乎因为 NextJs
正在调用 reply.raw.end
,fastify
中的生命周期挂钩 onSend
从未被触发。
简答:
我创建了实现此行为的社区插件:@applicazza/fastify-nextjs
长答案:
由于 Next.js 直接操作 http.ServerResponse,无论它写入流什么都不会通过 fastify 的响应管道。
但是您可以使用 JavaScript Proxy 拦截对 NodeJs http.ServerResponse 的调用并将其传递给 Fastify。
import { FastifyPluginAsync, FastifyReply, FastifyRequest } from 'fastify';
import fastifyPlugin from 'fastify-plugin';
import { IncomingMessage, ServerResponse } from 'http';
import Next from 'next';
import { NextServer } from 'next/dist/server/next';
import fastifyStatic from 'fastify-static';
declare module 'fastify' {
interface FastifyInstance {
nextJsProxyRequestHandler: (request: FastifyRequest, reply: FastifyReply) => void;
nextJsRawRequestHandler: (request: FastifyRequest, reply: FastifyReply) => void;
nextServer: NextServer;
passNextJsRequests: () => void;
passNextJsDataRequests: () => void;
passNextJsDevRequests: () => void;
passNextJsImageRequests: () => void;
passNextJsPageRequests: () => void;
passNextJsStaticRequests: () => void;
}
}
declare module 'http' {
interface IncomingMessage {
fastify: FastifyRequest;
}
interface OutgoingMessage {
fastify: FastifyReply;
}
}
export interface FastifyNextJsOptions {
dev?: boolean;
basePath?: string;
}
const fastifyNextJs: FastifyPluginAsync<FastifyNextJsOptions> = async (fastify, { dev, basePath = '' }) => {
if (dev === undefined) {
dev = process.env.NODE_ENV !== 'production';
}
const nextServer = Next({
dev,
});
const nextJsProxyRequestHandler = function (request: FastifyRequest, reply: FastifyReply) {
nextServer.getRequestHandler()(proxyFastifyRawRequest(request), proxyFastifyRawReply(reply));
};
const nextJsRawRequestHandler = function (request: FastifyRequest, reply: FastifyReply) {
nextServer.getRequestHandler()(request.raw, reply.raw);
};
const passNextJsRequestsDecorator = () => {
fastify.passNextJsDataRequests();
fastify.passNextJsImageRequests();
if (dev) {
fastify.passNextJsDevRequests();
} else {
fastify.passNextJsStaticRequests();
}
fastify.passNextJsPageRequests();
};
const passNextJsDataRequestsDecorator = () => {
fastify.register((fastify, _, done) => {
fastify.route({
method: ['GET', 'HEAD', 'OPTIONS'],
url: '/data/*',
handler: nextJsProxyRequestHandler
});
done();
}, {
prefix: `${basePath}/_next`
});
};
const passNextJsDevRequestsDecorator = () => {
fastify.register((fastify, _, done) => {
fastify.route({
method: ['GET', 'HEAD', 'OPTIONS'],
url: '/static/*',
handler: nextJsRawRequestHandler
});
fastify.route({
method: ['GET', 'HEAD', 'OPTIONS'],
url: '/webpack-hmr',
handler: nextJsRawRequestHandler
});
done();
}, {
prefix: `${basePath}/_next`
});
};
const passNextJsImageRequestsDecorator = () => {
fastify.register((fastify, _, done) => {
fastify.route({
method: ['GET', 'HEAD', 'OPTIONS'],
url: '/image',
handler: nextJsRawRequestHandler
});
done();
}, {
prefix: `${basePath}/_next`
});
};
const passNextJsStaticRequestsDecorator = () => {
fastify.register(fastifyStatic, {
prefix: `${basePath}/_next/static/`,
root: `${process.cwd()}/.next/static`,
decorateReply: false,
});
};
const passNextJsPageRequestsDecorator = function () {
fastify.register((fastify, _, done) => {
fastify.route({
method: ['GET', 'HEAD', 'OPTIONS'],
url: '*',
handler: nextJsProxyRequestHandler,
});
done();
}, {
prefix: basePath || '/'
});
};
fastify.decorate('nextJsProxyRequestHandler', nextJsProxyRequestHandler);
fastify.decorate('nextJsRawRequestHandler', nextJsRawRequestHandler);
fastify.decorate('nextServer', nextServer);
fastify.decorate('passNextJsDataRequests', passNextJsDataRequestsDecorator);
fastify.decorate('passNextJsDevRequests', passNextJsDevRequestsDecorator);
fastify.decorate('passNextJsImageRequests', passNextJsImageRequestsDecorator);
fastify.decorate('passNextJsPageRequests', passNextJsPageRequestsDecorator);
fastify.decorate('passNextJsRequests', passNextJsRequestsDecorator);
fastify.decorate('passNextJsStaticRequests', passNextJsStaticRequestsDecorator);
await nextServer.prepare();
fastify.addHook('onClose', function () {
return nextServer.close();
});
};
const proxyFastifyRawRequest = (request: FastifyRequest) => {
return new Proxy(request.raw, {
get(target: IncomingMessage, property: string | symbol, receiver: unknown): unknown {
const value = Reflect.get(target, property, receiver);
if (typeof value === 'function') {
return value.bind(target);
}
if (property === 'fastify') {
return request;
}
return value;
}
});
};
const proxyFastifyRawReply = (reply: FastifyReply) => {
return new Proxy(reply.raw, {
get: function (target: ServerResponse, property: string | symbol, receiver: unknown): unknown {
const value = Reflect.get(target, property, receiver);
if (typeof value === 'function') {
if (value.name === 'end') {
return function () {
return reply.send(arguments[0]);
};
}
if (value.name === 'getHeader') {
return function () {
return reply.getHeader(arguments[0]);
};
}
if (value.name === 'hasHeader') {
return function () {
return reply.hasHeader(arguments[0]);
};
}
if (value.name === 'setHeader') {
return function () {
return reply.header(arguments[0], arguments[1]);
};
}
if (value.name === 'writeHead') {
return function () {
return reply.status(arguments[0]);
};
}
return value.bind(target);
}
if (property === 'fastify') {
return reply;
}
return value;
},
});
};
export default fastifyPlugin(fastifyNextJs, {
fastify: '3.x',
});
注册插件:
const dev = process.env.NODE_ENV !== 'production';
fastify.register(fastifyNextJs, { dev }).after(() => {
fastify.passNextJsImageRequests();
if (dev) {
fastify.passNextJsDevRequests();
} else {
fastify.passNextJsStaticRequests();
}
});
为会话创建上下文:
fastify.register(async (fastify) => {
fastify.register(fastifySession, {
// options
});
fastify.passNextJsDataRequests();
fastify.passNextJsPageRequests();
});
N.B。您必须在 Next.Js
中禁用压缩module.exports = {
compress: false,
};