注意!!!
我不使用 puppeter 生成 PDF(无法使其在 WSL2 上运行),但您只需更改 RenderPDFInterceptor.generatePDFAttachment 方法即可完成!!!
如果您想尝试代码并且某些东西不起作用,请随时在 cmets 中问我?。
添加拦截器的自定义 RenderPDF 装饰器(版本 1):
用法(app.controller.ts):
将RenderPDF 添加到任何处理程序。此处理程序必须返回 PDFRenderOptions。
import { Controller, Get } from '@nestjs/common';
import { PDFRenderOptions, RenderPDF } from './render-pdf.decorator';
@Controller()
export class AppController {
@Get('/file')
@RenderPDF()
getFile(): PDFRenderOptions {
return {
templateFilename: 'template',
attachmentFilename: 'file.pdf',
locals: { message: 'Hi' }
}
}
}
实现(render-pdf.decorator.ts):
import { CallHandler, ExecutionContext, Injectable, NestInterceptor, StreamableFile, UseInterceptors } from '@nestjs/common';
import { Observable, mergeMap } from 'rxjs';
import { create as generatePDF } from 'html-pdf';
import { Application, Request } from 'express';
type NodeCallback<T> = (err: Error, val: T) => void;
type PromisifyCallback<T> = (cb: NodeCallback<T>) => void
const promise = <T>(cb: PromisifyCallback<T>) => {
return new Promise<T>((res, rej) => cb((err, val) => err ? rej(err) : res(val) ));
}
export interface PDFRenderOptions {
templateFilename: string;
attachmentFilename: string;
locals?: Record<string, any>;
}
@Injectable()
class RenderPDFInterceptor implements NestInterceptor {
intercept(ctx: ExecutionContext, next: CallHandler): Observable<any> {
const { app } = ctx.switchToHttp().getRequest() as Request;
return next.handle().pipe(mergeMap((opts: PDFRenderOptions) => this.generatePDFAttachment(app, opts)));
}
private async generatePDFAttachment(app: Application, options: PDFRenderOptions) {
const html = await promise<string>((cb) => app.render(options.templateFilename, options.locals, cb));
const pdf = await promise<Buffer>((cb) => generatePDF(html).toBuffer(cb));
return new StreamableFile(pdf, { disposition: `attachment; filename="${options.attachmentFilename}"` })
}
}
export const RenderPDF = () => {
return UseInterceptors(RenderPDFInterceptor);
}
添加拦截器的自定义 RenderPDF 装饰器 + 自定义 AddRenderPDFOptions 装饰器(版本 2)
用法(app.controller.ts)
将RenderPDF 应用于某个处理程序或整个控制器,并在那里传递一些默认选项。现在您可以使用 AddRenderPDFOptions 装饰器来更改控制器级别的选项和/或从处理程序返回选项以更改处理程序级别的选项。
import { Controller, Get } from '@nestjs/common';
import { PDFRenderOptions, RenderPDF, AddRenderPDFOptions } from './render-pdf.interceptor';
@Controller()
@RenderPDF({
templateFilename: 'default-template',
attachmentFilename: 'default-filename.pdf',
locals: { message: 'DEFAULT MESSAGE' },
})
export class AppController {
@Get('/default')
getDefaultFile() {
return {};
}
@Get('/different')
@AddRenderPDFOptions({
templateFilename: 'different-template',
locals: { message: 'DIFFERENT MESSAGE' }
})
getDifferentFile(): PDFRenderOptions {
return {
attachmentFilename: Math.random() > 0.5
? 'first-different-filename.pdf'
: 'second-different-filename.pdf',
}
}
@Get('/random')
@AddRenderPDFOptions({
templateFilename: 'template-for-random',
})
getRandomFile() {
return {
locals: { message: Math.random().toString() }
}
}
}
@Controller()
export class DifferentAppController {
@Get('something-but-not-pdf')
getSomethingButNotPDF() {
return { something: true };
}
@Get('pdf')
@RenderPDF({ templateFilename: 'pdf', attachmentFilename: 'pdf.pdf' })
getPDF(): PDFRenderOptions {
return { locals: { message: "pdf" } }
}
}
实施:
import { applyDecorators, CallHandler, ExecutionContext, Injectable, NestInterceptor, SetMetadata, StreamableFile, UseInterceptors } from '@nestjs/common';
import { Observable, mergeMap } from 'rxjs';
import { create as generatePDF } from 'html-pdf';
import { Application, Request } from 'express';
import { Reflector } from '@nestjs/core';
type NodeCallback<T> = (err: Error, val: T) => void;
type PromisifyCallback<T> = (cb: NodeCallback<T>) => void
const promise = <T>(cb: PromisifyCallback<T>) => {
return new Promise<T>((res, rej) => cb((err, val) => err ? rej(err) : res(val) ));
}
const PDF_RENDER_OPTIONS_METADATA_KEY = Symbol('PDF render options');
export interface PDFRenderOptions {
templateFilename?: string;
attachmentFilename?: string;
locals?: Record<string, any>;
}
@Injectable()
class RenderPDFInterceptor implements NestInterceptor {
constructor(private reflector: Reflector) {}
intercept(ctx: ExecutionContext, next: CallHandler): Observable<any> {
const { app } = ctx.switchToHttp().getRequest() as Request;
return next.handle().pipe(
mergeMap((responseOptions: PDFRenderOptions) => (
this.generatePDFAttachment(app, this.getFinalRenderOptions(ctx, responseOptions))
))
);
}
private async generatePDFAttachment(app: Application, options: PDFRenderOptions) {
console.log("FINAL OPTIONS", options)
const html = await promise<string>((cb) => app.render(options.templateFilename, options.locals, cb));
const pdf = await promise<Buffer>((cb) => generatePDF(html).toBuffer(cb));
return new StreamableFile(pdf, { disposition: `attachment; filename="${options.attachmentFilename}"` })
}
private getFinalRenderOptions(ctx: ExecutionContext, responseOptions: PDFRenderOptions) {
const controllerOptions = this.reflector.get<PDFRenderOptions>(PDF_RENDER_OPTIONS_METADATA_KEY, ctx.getClass()) || {};
const handlerOptions = this.reflector.get<PDFRenderOptions>(PDF_RENDER_OPTIONS_METADATA_KEY, ctx.getHandler()) || {};
return {
...controllerOptions,
...handlerOptions,
...responseOptions,
locals: {
...controllerOptions.locals,
...handlerOptions.locals,
...responseOptions.locals,
}
}
}
}
export const AddRenderPDFOptions = (options: PDFRenderOptions = {}) => {
return SetMetadata(PDF_RENDER_OPTIONS_METADATA_KEY, { locals: {}, ...options });
}
export const RenderPDF = (options: PDFRenderOptions = {}) => {
return applyDecorators(
AddRenderPDFOptions(options),
UseInterceptors(RenderPDFInterceptor),
)
}