使用 `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.endfastify 中的生命周期挂钩 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,
};