javascript技巧

关注公众号 jb51net

关闭
首页 > 网络编程 > JavaScript > javascript技巧 > JavaScript页面信息下载与打印

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页面信息下载与打印的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文