JavaScript纯前端方式实现页面信息优雅下载与打印
作者:会联营的陆逊
前端下载 前端经常会遇到一些下载需求,常规的像打印、图片、word、PDF等等,以下是我对纯前端下载的一些整理 打印 打印是最简单的需求,也是最容易实现的 window.print() 可以直接调用
前端经常会遇到一些下载需求,常规的像打印、图片、word、PDF等等,以下是我对纯前端下载的一些整理
打印
打印是最简单的需求,也是最容易实现的
window.print()
可以直接调用 window.print() 方法,所有浏览器都支持 print():
但是这个方法会打印所有页面内容,无法指定打印那些内容,此时需要做一些额外工作
添加 css
function windowPrint(style) {
const styleElement = document.createElement("style");
const head = document.querySelector("head");
styleElement.onload = function () {
window.print();
setTimeout(() => {
head.removeChild(styleElement);
}, 1000);
};
styleElement.innerHTML = `@media print {
${style}
}`;
head.appendChild(styleElement);
}
如果需要分页处理可以增加样式
@media print .page-break-auto {
page-break-before: auto!important;
page-break-after: auto!important;
page-break-inside: avoid!important;
}
iframe.contentWindow.print()
将打印内容放入iframe,打印完成后移除
function iframePrint() {
const iframe = document.createElement("iframe");
let str = ''
const styles = document.querySelectorAll('style,link')
for (let i = 0; i < styles.length; i++) {
str += styles[i].outerHTML
}
const f = document.body.appendChild(iframe)
// 将iframe不可见
iframe.style = 'position:absolute;width:0;height:0;top:-10px;left:-10px;'
const frameWindow = f.contentWindow || f.contentDocument
const doc = f.contentDocument || f.contentWindow.document
doc.open()
doc.write(str + dom.outerHTML)
doc.close()
iframe.onload = function() {
frameWindow.focus()
frameWindow.print()
}
}
下载图片
下载图片需要借助第三方插件 html2canvas
function createCanvns(element: HTMLElement) {
const canvas = await html2canvas(element, {
useCORS: true,
imageTimeout: 0,
scale: 2,
onclone: (doc) => {},
ignoreElements: (element) => {
},
});
return canvas;
}
下载 PDF
下载 PDF 需要借助第三方插件 jspdf
import { jsPDF } from "jspdf";
import html2canvas from "html2canvas";
interface Blank {
height: number;
width: number;
x: number;
y: number;
}
class PdfBlank implements Blank {
height: number;
width: number;
x: number;
y: number;
constructor(blank: Blank) {
this.height = blank.height;
this.width = blank.width;
this.x = blank.x;
this.y = blank.y;
}
}
interface PdfPage {
height: number;
position: number;
blank?: Blank;
}
interface CanvasNode {
node: HTMLElement;
image: string;
offsetHeight: number;
offsetWidth: number;
imageHeight: number;
pages: PdfPage[];
position: number;
}
interface PDFExportOptions {
filename: string;
onProgress?: (progress: number) => void;
}
export class PDFExporter {
scale = 2;
width = 595.28;
height = 841.89;
async exportToPDF(element: HTMLElement, options: PDFExportOptions) {
this.updateProgress(0, options.onProgress);
console.time("exportToPDF");
const nodes = await this.getPageNode(element, options);
await new Promise((resolve) => setTimeout(resolve, 10));
await this.getNodeCanvas(nodes, options);
await this.createPdf(nodes, options);
console.timeEnd("exportToPDF");
this.updateProgress(100, options.onProgress);
}
async createCanvns(element: HTMLElement) {
const canvas = await html2canvas(element, {
useCORS: true,
imageTimeout: 0,
scale: this.scale,
onclone: (doc) => {},
ignoreElements: (element) => {
return (
element.classList.contains("canvas-ignore-this")
);
},
});
return canvas;
}
async getPageNode(element: HTMLElement, options: PDFExportOptions) {
const stack: { node: Node }[] = [{ node: element }];
const results: CanvasNode[] = [];
let position = 0;
// 更新进度
let pageProgress = 0;
while (stack.length > 0) {
if (pageProgress++ < 20) {
this.updateProgress(pageProgress++, options.onProgress);
}
const { node } = stack.pop()!;
if (node instanceof HTMLElement) {
const { createCanvasGroup, createCanvas } = node.dataset;
if (createCanvas != null) {
// 获取最小截图元素
const rect = node.getBoundingClientRect();
const offsetHeight = rect.height; // 包含小数部分的精确高度
const offsetWidth = rect.width; // 包含小数部分的精确高度
const imageHeight = (this.width / offsetWidth) * offsetHeight;
const canvasNode: CanvasNode = {
node,
image: "",
offsetHeight,
offsetWidth,
imageHeight,
position,
pages: [],
};
if (position + imageHeight < this.height) {
canvasNode.pages.push({
height: imageHeight,
position,
});
position += imageHeight;
} else {
// 获取分页
this.getNodePage(canvasNode);
position = canvasNode.position;
}
results.push(canvasNode);
} else if (createCanvasGroup != null) {
const children = Array.from(node.childNodes);
const childrenLength = children.length;
for (let i = childrenLength - 1; i >= 0; i--) {
stack.push({ node: children[i] });
}
}
}
}
return results;
}
async getNodeCanvas(canvasNodes: CanvasNode[], options: PDFExportOptions) {
// 更新进度 - 60
const canvasNodesLength = canvasNodes.length;
for (let i = 0; i < canvasNodesLength; i++) {
const pageProgress = 20 + (40 * i) / canvasNodesLength;
this.updateProgress(pageProgress, options.onProgress);
const canvasNode = canvasNodes[i];
const canvas = await this.createCanvns(canvasNode.node);
const image = canvas.toDataURL("image/jpeg", 1);
canvasNode.image = image;
}
}
getNodePage(canvasNode: CanvasNode) {
const { node, image } = canvasNode;
const stack: { node: Node }[] = [];
const results: PdfPage[] = [];
// 获取上一张图的position
let firstPosition = canvasNode.position;
const children = Array.from(node.childNodes);
const childrenLength = children.length;
for (let i = childrenLength - 1; i >= 0; i--) {
stack.push({ node: children[i] });
}
let imageHeight = 0;
let position = firstPosition;
while (stack.length > 0) {
const { node } = stack.pop()!;
if (node instanceof HTMLElement) {
const rect = node.getBoundingClientRect();
const offsetHeight = rect.height; // 包含小数部分的精确高度
const offsetWidth = rect.width; // 包含小数部分的精确高度
const height = (this.width / offsetWidth) * offsetHeight;
// 如果高度大于pdf高度,则需要递归子元素
if (height > this.height) {
const children = Array.from(node.childNodes);
if (children.length > 0) {
const childrenLength = children.length;
for (let i = childrenLength - 1; i >= 0; i--) {
stack.push({ node: children[i] });
}
continue;
}
}
if (firstPosition + imageHeight + height > this.height) {
const blankHeight = this.height - firstPosition - imageHeight;
const blank = new PdfBlank({
height: blankHeight,
width: this.width,
x: 0,
y: firstPosition + imageHeight,
});
results.push({ height: imageHeight, position, blank });
if (firstPosition) {
position -= firstPosition;
}
position -= imageHeight;
firstPosition = 0;
imageHeight = 0;
}
imageHeight += height;
}
}
if (imageHeight > 0) {
results.push({ height: imageHeight, position });
}
// 最后一页的图片高度
canvasNode.position = imageHeight;
canvasNode.pages = results;
}
async createPdf(nodes: CanvasNode[], options: PDFExportOptions) {
const pdf = new jsPDF("p", "pt", [this.width, this.height]);
const nodeLength = nodes.length;
const jProgress = 40 / nodeLength;
for (let i = 0; i < nodeLength; i++) {
const node = nodes[i];
const { image, pages, imageHeight } = node;
const pagesLength = pages.length;
for (let j = 0; j < pagesLength; j++) {
const pageProgress = 60 + (jProgress * j) / pagesLength;
this.updateProgress(pageProgress, options.onProgress);
const page = pages[j];
const { height, position, blank } = page;
if (height > 0) {
pdf.addImage(image, "JPEG", 0, position, this.width, imageHeight);
if (blank) {
this.addBlank(pdf, blank);
if (i !== pagesLength - 1 && j !== pagesLength - 1) {
pdf.addPage();
}
}
}
}
}
pdf.save("test.pdf");
}
addBlank(pdf: jsPDF, blank: PdfBlank) {
pdf.setFillColor(255, 255, 255);
pdf.rect(
blank.x,
blank.y,
Math.ceil(blank.width),
Math.ceil(blank.height),
"F"
);
}
// 更新进度
updateProgress(
progress: number,
onProgress?: (progress: number) => void
): void {
if (onProgress && typeof onProgress === "function") {
try {
onProgress(Math.min(100, Math.max(0, Math.round(progress))));
} catch (e) {
console.warn("进度回调错误:", e);
}
}
}
}
下载 word
下载 word 需要借助 docx 插件
import {
Document,
Paragraph,
TextRun,
Packer,
Table,
TableRow,
TableCell,
ImageRun,
type FileChild,
type ParagraphChild,
type IRunOptions,
type IParagraphOptions,
type ITableCellOptions,
type ITableOptions,
} from "docx";
import { saveAs } from "file-saver";
interface FileChildNode {
getNode(): FileChild;
}
interface ParagraphChildNode {
getNode(): ParagraphChild | null;
}
interface TableCellNode {
getNode(): TableCell;
}
class TextRunNode implements ParagraphChildNode {
options: IRunOptions;
constructor(options: IRunOptions) {
this.options = options;
}
getNode() {
return new TextRun(this.options);
}
}
class ImageRunNode implements ParagraphChildNode {
type: string;
data: Buffer | ArrayBuffer;
width: number = 100;
height: number = 100;
constructor() {}
getNode() {
if (!this.data) {
return null;
}
return new ImageRun({
data: this.data,
type: this.type as any,
transformation: { width: this.width, height: this.height },
});
}
getImageType(url: string) {
const noQueryUrl = url.split("?")[0];
const type = noQueryUrl.split(".").pop() || ("png" as any);
return type;
}
async getImageBuffer(localImage: string, width: number, height: number) {
const response = await fetch(localImage);
this.data = await response.arrayBuffer();
this.type = this.getImageType(localImage);
this.width = width;
this.height = height;
}
async getImageBufferFromUrl(url: string) {
try {
const response = await fetch(url);
this.data = await response.arrayBuffer();
this.type = this.getImageType(url);
const domElements = document.querySelectorAll(`[src="${url}"]`);
for (const domElement of Array.from(domElements)) {
const { width, height } = domElement.getBoundingClientRect();
if (width > 0 && height > 0) {
this.width = width;
this.height = height;
break;
}
}
const maxWidth = 595;
// 限制最大宽度
if (this.width > maxWidth) {
this.height = this.height * (maxWidth / this.width);
this.width = maxWidth;
}
} catch (error) {
console.error(error);
}
}
}
class ParagraphNode implements FileChildNode {
options: IParagraphOptions;
children: ParagraphChildNode[];
constructor(options: IParagraphOptions, children: ParagraphChildNode[] = []) {
this.options = options;
this.children = children;
}
push(child: ParagraphChildNode | ParagraphChildNode[]) {
if (Array.isArray(child)) {
this.children.push(...child);
} else {
this.children.push(child);
}
}
getNode() {
const children = this.children
.map((child) => child.getNode())
.filter(Boolean) as ParagraphChild[];
return new Paragraph({
...this.options,
children,
});
}
}
class TableNode implements FileChildNode {
children: TableCellNode[];
options: Partial<ITableOptions>;
constructor(options: Partial<ITableOptions>, children: TableCellNode[] = []) {
this.children = children;
this.options = options;
}
getNode() {
return new Table({
...this.options,
rows: [
new TableRow({
children: this.children.map((child) => child.getNode()),
}),
],
});
}
}
class TableCellNode implements TableCellNode {
options: Partial<ITableCellOptions>;
children: ParagraphNode[];
constructor(
options: Partial<ITableCellOptions>,
children: ParagraphNode[] = []
) {
this.options = options;
this.children = children;
}
push(child: ParagraphNode | ParagraphNode[]) {
if (Array.isArray(child)) {
this.children.push(...child);
} else {
this.children.push(child);
}
}
getNode() {
return new TableCell({
...this.options,
children: this.children.map((child) => child.getNode()),
});
}
}
class DocxDocument {
children: FileChildNode[] = [];
constructor() {}
push(child: FileChildNode | FileChildNode[]) {
if (Array.isArray(child)) {
this.children.push(...child);
} else {
this.children.push(child);
}
}
getChildren() {
return this.children.map((child) => child.getNode());
}
async downloadDocx(filename: string) {
const children = this.getChildren();
const doc = new Document({
sections: [
{
children,
},
],
});
const blob = await Packer.toBlob(doc);
saveAs(blob, filename);
}
/**
* 像素(px) 转 docx 的 size(半磅单位)
* @param {number} px - 像素值
* @returns {number} docx 的 size 值
*/
pxToSize(px: number | string) {
px = Number(px);
if (isNaN(px)) {
px = 14;
}
return Math.round(px * 1.5); // px × 1.5 并四舍五入
}
}
class HtmlNodes {
html: string;
color: string = "#262626";
size: number = 14;
constructor(html: string, size?: number, color?: string) {
this.html = html;
this.size = size || this.size;
this.color = color || this.color;
}
// 判断块级元素
isBlockElement(tagName: string) {
return (
tagName === "p" ||
tagName === "div" ||
tagName === "li" ||
tagName === "h1" ||
tagName === "h2" ||
tagName === "h3" ||
tagName === "h4" ||
tagName === "h5" ||
tagName === "h6" ||
tagName === "br"
);
}
rgbToHex(rgb: string): string {
const result = /rgb\((\d+),\s*(\d+),\s*(\d+)\)/.exec(rgb);
if (!result) {
return "000000";
}
const r = parseInt(result[1], 10);
const g = parseInt(result[2], 10);
const b = parseInt(result[3], 10);
return ((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1);
}
/**
* 像素(px) 转 docx 的 size(半磅单位)
* @param {number} px - 像素值
* @returns {number} docx 的 size 值
*/
pxToSize(px: number | string) {
px = Number(px);
if (isNaN(px)) {
px = 14;
}
return Math.round(px * 1.5); // px × 1.5 并四舍五入
}
checkPrecedingContent(node: HTMLElement) {
const previousSibling = node.previousSibling;
if (previousSibling && previousSibling.nodeType !== Node.ELEMENT_NODE) {
return false;
}
const previousElementSibling = node.previousElementSibling;
const isBlockElement =
previousElementSibling &&
this.isBlockElement(previousElementSibling?.tagName.toLowerCase());
if (isBlockElement) {
return true;
}
return false;
}
hasInlineUnderline(style: CSSStyleDeclaration) {
return (
style.textDecoration.includes("underline") ||
style.textDecorationLine.includes("underline")
);
}
hasInlineStrike(style: CSSStyleDeclaration) {
return (
style.textDecoration.includes("line-through") ||
style.textDecorationLine.includes("line-through")
);
}
isTextItalic(style: CSSStyleDeclaration) {
// 检查 font-style
if (style.fontStyle === "italic") return true;
return false;
}
getTextRunOptions(node: HTMLElement): IRunOptions {
const data: any = {};
const style = node.style;
// 换行
let breakValue;
const tagName = node.tagName.toLowerCase();
switch (tagName) {
case "p":
case "div":
case "li":
case "h1":
case "h2":
case "h3":
case "h4":
case "h5":
case "h6":
case "br":
breakValue = 1;
break;
case "b":
case "strong":
data.bold = true;
break;
case "i":
case "em":
data.italics = true;
break;
case "u":
data.underline = { type: "single" };
break;
case "a":
data.underline = { type: "single" };
data.color = "#00AD63";
break;
case "sub":
data.subscript = true;
break;
case "sup":
data.superscript = true;
break;
default:
if (this.checkPrecedingContent(node)) {
breakValue = 1;
}
break;
}
data.break = breakValue;
// 字体需要进行转换
const sizeMap = {
"xx-small": 8,
"x-small": 9,
small: 10,
medium: 12,
normal: 14,
large: 16,
"x-large": 18,
"xx-large": 24,
"xxx-large": 32,
"xxxx-large": 40,
"xxxxx-large": 48,
"xxxxxx-large": 64,
"xxxxxxx-large": 80,
};
const fontSize = style.fontSize || this.size || 14;
const sizeValue = sizeMap[fontSize as keyof typeof sizeMap] || fontSize;
const size = this.pxToSize(sizeValue);
data.size = size;
// 颜色 - rgb需要进行转换
let color = style.color || data.color || this.color || "#262626";
if (color.startsWith("rgb")) {
color = this.rgbToHex(color);
}
if (!color.startsWith("#")) {
color = `#${color}`;
}
data.color = color;
// 背景颜色
let backgroundColor = style.backgroundColor;
if (backgroundColor) {
if (backgroundColor.startsWith("rgb")) {
backgroundColor = this.rgbToHex(backgroundColor);
}
// 去掉前面的#
if (backgroundColor.startsWith("#")) {
backgroundColor = backgroundColor.slice(1);
}
data.shading = {
fill: backgroundColor,
};
console.log(style.backgroundColor, data.shading);
}
// 粗体
data.bold = style.fontWeight === "bold" || Number(style.fontWeight) >= 500;
// 下划线
if (this.hasInlineUnderline(style)) {
data.underline = { type: "single" };
}
// 斜体
if (this.isTextItalic(style)) {
data.italics = true;
}
// 删除线
if (this.hasInlineStrike(style)) {
data.strike = true;
}
// 检测DOM元素的上标/下标状态
if (style.verticalAlign === "sub") {
data.subscript = true;
}
if (style.verticalAlign === "super") {
data.superscript = true;
}
return data;
}
async getNodes() {
const parser = new DOMParser();
const htmlDom = parser.parseFromString(this.html, "text/html");
const rootNode = htmlDom.body;
const stack: { node: Node; options: IRunOptions }[] = [
{ node: rootNode, options: {} },
];
const results: (TextRunNode | ImageRunNode)[] = [];
while (stack.length > 0) {
const { node, options } = stack.pop()!;
if (node.nodeType === Node.TEXT_NODE) {
const text = node.textContent?.trim();
if (text) {
results.push(new TextRunNode({ text, ...options }));
}
continue;
}
if (node instanceof HTMLElement) {
// 图片处理
if (node.tagName === "IMG") {
const src = node.getAttribute("src");
if (src) {
const imageRun = new ImageRunNode();
await imageRun.getImageBufferFromUrl(src);
results.push(imageRun);
}
continue;
}
const children = Array.from(node.childNodes);
const options = this.getTextRunOptions(node);
for (let i = children.length - 1; i >= 0; i--) {
stack.push({ node: children[i], options });
}
}
}
return results;
}
}
export {
TextRunNode,
ImageRunNode,
HtmlNodes,
ParagraphNode,
TableNode,
TableCellNode,
DocxDocument,
type ParagraphChildNode,
};以上就是JavaScript纯前端方式实现页面信息优雅下载与打印的详细内容,更多关于JavaScript页面信息下载与打印的资料请关注脚本之家其它相关文章!
