내 연락처 정보
우편메소피아@프로톤메일.com
2024-07-12
한어Русский языкEnglishFrançaisIndonesianSanskrit日本語DeutschPortuguêsΕλληνικάespañolItalianoSuomalainenLatina
2022년에는 상대적으로 제한된 팀 권한으로 인해 새 페이지나 새 서비스를 열지 않고도 이미지를 생성해야 하는 필요성도 있었고, 그 당시에는 Html2canvas의 프런트엔드 생성 솔루션이 채택되었습니다. 12개 이상의 핵심 비즈니스 모듈과 Web, H5, App, Electron PC App에서 300ms~2500ms 이내에 이미지를 생성하고 다운로드합니다.
올해도 같은 요구에 직면했지만 기업이 수십 개의 차트로 구성된 대형 화면 시각적 Iframe을 내장했기 때문에 Html2canvas는 Iframe 상황을 해결하는 데 약간의 어려움을 겪었습니다. 우리는 또한 팀 권한을 사용하여 스크린샷을 찍을 수 있는 솔루션을 고려했습니다. 서버 측에서 Puppeteer가 시작되었습니다.
Puppeteer를 사용하여 PDF를 생성하는 것은 작동하지 않으므로 솔루션은 Sharp를 사용하여 Puppeteer가 생성한 이미지를 PDF 페이지 크기에 비례하는 크기로 수동으로 자른 다음 pdfkit scale을 사용하여 잘라낸 이미지를 동일한 비율로 PDF에 채웁니다.
이 코드는 POC 솔루션이므로 프로덕션에 사용하기 전에 더 많은 안정성과 사용성 지원 작업이 필요합니다. 따라서 다음 코드는 학습 참고용일 뿐입니다.
프로세스 코드는 참조 의미가 거의 없습니다. 핵심 코드의 잘린 이미지는 자동으로 스크롤되고 지정된 Iframe이 로드될 때까지 기다린 후 참조에 더 의미 있는 지정된 요소의 속성 값을 얻습니다.
// 流程代码
import { ConsoleLogger } from '@nestjs/common';
import puppeteer, { Viewport } from 'puppeteer';
import { Browser, CookieParam } from 'puppeteer';
import * as helper from './helper';
const logger = new ConsoleLogger('WebViewer');
export enum TargetFileType {
png = 'png',
jpeg = 'jpeg',
webp = 'webp',
pdf = 'pdf',
}
export interface ShotParam {
type: TargetFileType;
url: string;
targetFile?: string;
cookies?: CookieParam[];
viewport?: Viewport;
timeout?: number;
}
export class WebViewer {
private browser: Browser | null = null;
private inited = false;
static defaultInstance = new WebViewer();
constructor(private readonly options?: WebViewerOptions) {
this.options = options || {
socketTimeout: 2 * 60 * 1000,
generateTimeout: 0,
};
}
async init(): Promise<void> {
this.browser = await puppeteer.launch({
// defaultViewport: { width: 1920, height: 1080 },
// headless: 'shell',
headless: false,
pipe: true,
args: [
'--disable-gpu',
'--disable-dev-shm-usage',
'--disable-setuid-sandbox',
'--no-first-run',
'--no-sandbox',
'--no-zygote',
'--full-memory-crash-report',
'--unlimited-storage',
],
});
this.inited = true;
}
async shot(param: ShotParam): Promise<Buffer> {
if (!this.inited) {
await this.init();
}
logger.log(
`Start to shot url: ${param.url}, type: ${
param.type
}, viewport: { heigth:${param.viewport?.height || 0}, width:${
param.viewport?.width || 0
}} `,
);
const page = await this.browser.newPage();
page.on('response', (response) => {
logger.debug(response.url());
});
page.on('close', () => {
logger.debug('Current page has been closed.');
});
if (param.cookies) {
await page.setCookie(...param.cookies);
}
await page.goto(param.url, {
timeout: param?.timeout || this.options.socketTimeout,
waitUntil: 'networkidle0',
});
await helper.waitForFrame(page);
const minWidth = 1920;
let width = await page.$eval('.html-table', (el) => el.scrollWidth + 36);
width = width > minWidth ? width : minWidth;
param.viewport = { width, height: 1080 };
await page.setViewport(param.viewport);
await helper.getValueFromElementDataset(
page,
'html',
'height',
async (value: string) => !Number.isNaN(Number(value)),
);
const bodyHandle = await page.$('body');
const { height: bodyHeight } = await bodyHandle.boundingBox();
param.viewport = { width, height: Math.floor(bodyHeight) + 1 };
await bodyHandle.dispose();
await page.setViewport(param.viewport);
await page.waitForSelector('#datart-rendered');
await helper.sleep(300);
let buffer = await page.screenshot();
if (param.type === 'pdf') {
buffer = await helper.generatePdf(buffer);
}
await helper.sleep(300);
await page.close();
return buffer;
}
async close(): Promise<void> {
await this.browser?.close();
}
}
export interface WebViewerOptions {
socketTimeout: number;
generateTimeout?: number;
}
// 核心代码
import { Page } from 'puppeteer';
import * as sharp from 'sharp';
import * as pdfkit from 'pdfkit';
import * as getStream from 'get-stream';
function waitForFrame(page: Page) {
let fulfill;
const promise = new Promise((resolve) => (fulfill = resolve));
checkFrame();
return promise;
function checkFrame() {
const frame = page.frames().find((f) => {
console.log(f.name());
return f.name() === 'datart';
});
if (frame) fulfill(frame);
else page.once('frameattached', checkFrame);
}
}
async function autoScroll(page: Page, selector: string) {
return page.evaluate((selector) => {
return new Promise((resolve) => {
//滚动的总高度
let totalHeight = 0;
//每次向下滚动的高度 100 px
const distance = 100;
const timer = setInterval(() => {
const dom = document.querySelector(selector);
if (!dom) {
return clearInterval(timer);
}
//页面的高度 包含滚动高度
const scrollHeight = dom.scrollHeight;
console.log(scrollHeight);
//滚动条向下滚动 distance
dom.scrollBy(0, distance);
totalHeight += distance;
//当滚动的总高度 大于 页面高度 说明滚到底了。也就是说到滚动条滚到底时,以上还会继续累加,直到超过页面高度
if (totalHeight >= scrollHeight) {
clearInterval(timer);
resolve(true);
}
}, 100);
});
}, selector);
}
async function getValueFromElementDataset(
page: Page,
selector: string,
key: string,
checkValue: (value: string) => Promise<boolean>,
) {
return new Promise((resolve) => {
const interval = setInterval(async () => {
const value = await page.$eval(
selector,
(el: HTMLElement, key: string) => {
return el.dataset[key];
},
key,
);
if (checkValue && !(await checkValue(value))) {
return;
} else {
clearInterval(interval);
resolve(value);
}
}, 500);
});
}
async function clipImage(
pageWidth: number,
pageHeight: number,
buffer: Buffer,
): Promise<{ images: Buffer[]; scale: number }> {
const imageOriginSharp = sharp(buffer);
const imageSharp = sharp(buffer).resize(pageWidth);
const imageBuffer = await imageSharp
.withMetadata()
.toBuffer({ resolveWithObject: true });
const imageOriginBuffer = await imageOriginSharp
.withMetadata()
.toBuffer({ resolveWithObject: true });
// const imageWidth = imageBuffer.info.width;
const imageHeight = imageBuffer.info.height;
const imageOriginWidth = imageOriginBuffer.info.width;
const imageOriginHeight = imageOriginBuffer.info.height;
const scale = imageOriginHeight / imageHeight;
const images: Buffer[] = [];
console.log({
imageOriginWidth,
imageOriginHeight,
scale,
});
let startY = 0;
while (startY < imageHeight) {
const height = Math.min(pageHeight, imageHeight - startY);
console.log(Math.ceil(height * scale), Math.ceil(startY * scale));
const imageOriginSharp = sharp(buffer);
const liteImage = await imageOriginSharp
.extract({
width: imageOriginWidth,
height: Math.ceil(height * scale),
left: 0,
top: Math.floor(startY * scale),
})
.toBuffer();
images.push(liteImage);
startY += height;
}
return { images, scale };
}
const generatePdf = (imageBuffer: Buffer) => {
return new Promise<Buffer>(async (resolve) => {
const doc = new pdfkit();
// 获取PDF页面的宽度和高度
const pageWidth = doc.page.width;
const pageHeight = doc.page.height;
// 图片按 page 高度等比放缩然后裁切为多份,然后通过 doc addpage 以及 doc.image 把每张图放入 pdf
const { images, scale } = await clipImage(
pageWidth,
pageHeight,
imageBuffer,
);
let index = 1;
for (const image of images) {
doc.image(image, 0, 0, {
width: pageWidth,
scale: 1 / scale,
});
if (index < images.length) {
doc.addPage();
doc.switchToPage(index);
index += 1;
}
}
doc.end();
const pdfBuffer = await getStream.buffer(doc);
resolve(pdfBuffer);
});
};
const sleep = (time: number) => new Promise((r) => setTimeout(r, time));
export {
sleep,
clipImage,
autoScroll,
generatePdf,
waitForFrame,
getValueFromElementDataset,
};