在 NestJS 的服务中渲染 hbs 模板
Render hbs template inside a service in NestJS
我需要从 HBS 模板生成 HTML 内容并将此内容传递到 puppeteer 页面并导出 PDF。
@UseGuards(JwtAuthGuard)
@Get('minimal')
async minimal(
@Param('id') projectId: string,
@Res() res: Response
) {
const project = await this.projectService.get(projectId);
const articles = await this.articleService.getAll(projectId);
const buffer: Buffer = await this.katalog.minimal(res, project, articles);
res.set({
'Content-Type': 'application/pdf',
'Content-Disposition': 'attachment; filename=katalog.pdf',
'Content-Length': buffer.length,
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': 0
});
res.end(buffer);
}
我的服务是这样的:
@Injectable()
export class KatalogService {
async minimal(res: Response, project: Project, articles: Article[]): Promise<Buffer> {
const browser = await puppeteer.launch();
const page = await browser.newPage();
const content = await this.render(res, project, articles);
await page.setContent(content);
const pdfBuffer = await page.pdf();
await page.close();
await browser.close();
return pdfBuffer;
}
async render(res: Response, project: Project, articles: Article[]): Promise<string> {
return new Promise((resolve, reject) => {
res.render('katalog', {}, (err, html) => {
if(err) return reject(err);
return resolve(html);
})
});
}
}
我的代码工作正常,我只是想知道是否有任何方法可以在不将 res
对象作为参数传递的情况下实现这一点。
注意!!!
我不使用 puppeter 生成 PDF(无法使其在 WSL2 上运行),但您可以通过简单地更改 RenderPDFInterceptor.generatePDFAttachment
方法来完成!!!
如果你想尝试代码但有些东西不起作用,请随时在评论中问我。
添加拦截器的自定义 RenderPDF
装饰器(版本 1):
用法(app.controller.ts
):
将 RenderPDF
添加到任何处理程序。此处理程序必须 return 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
装饰器来更改 controller-level 选项 and/or return 选项从处理程序更改 handler-level 选项。
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),
)
}
我需要从 HBS 模板生成 HTML 内容并将此内容传递到 puppeteer 页面并导出 PDF。
@UseGuards(JwtAuthGuard)
@Get('minimal')
async minimal(
@Param('id') projectId: string,
@Res() res: Response
) {
const project = await this.projectService.get(projectId);
const articles = await this.articleService.getAll(projectId);
const buffer: Buffer = await this.katalog.minimal(res, project, articles);
res.set({
'Content-Type': 'application/pdf',
'Content-Disposition': 'attachment; filename=katalog.pdf',
'Content-Length': buffer.length,
'Cache-Control': 'no-cache, no-store, must-revalidate',
'Pragma': 'no-cache',
'Expires': 0
});
res.end(buffer);
}
我的服务是这样的:
@Injectable()
export class KatalogService {
async minimal(res: Response, project: Project, articles: Article[]): Promise<Buffer> {
const browser = await puppeteer.launch();
const page = await browser.newPage();
const content = await this.render(res, project, articles);
await page.setContent(content);
const pdfBuffer = await page.pdf();
await page.close();
await browser.close();
return pdfBuffer;
}
async render(res: Response, project: Project, articles: Article[]): Promise<string> {
return new Promise((resolve, reject) => {
res.render('katalog', {}, (err, html) => {
if(err) return reject(err);
return resolve(html);
})
});
}
}
我的代码工作正常,我只是想知道是否有任何方法可以在不将 res
对象作为参数传递的情况下实现这一点。
注意!!!
我不使用 puppeter 生成 PDF(无法使其在 WSL2 上运行),但您可以通过简单地更改 RenderPDFInterceptor.generatePDFAttachment
方法来完成!!!
如果你想尝试代码但有些东西不起作用,请随时在评论中问我。
添加拦截器的自定义 RenderPDF
装饰器(版本 1):
用法(app.controller.ts
):
将 RenderPDF
添加到任何处理程序。此处理程序必须 return 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
装饰器来更改 controller-level 选项 and/or return 选项从处理程序更改 handler-level 选项。
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),
)
}