My Personal Blog

Published on

Vue 实现打印功能的两种方法(Vue 2 & Vue 3)

Authors
  • avatar
    Name
    Tian Haipeng

话不多说,先上代码

Vue 2 打印方法

function handleBatchPrint() {
    // 业务代码
    // ......
    // 需要打印的数据
    let data = {};
    
    // PrintTable 是打印模板组件
    const printOrderComponent = Vue.extend(PrintTable);

    // 获取组件的样式,用于打印时应用
    const styles = printOrderComponent.options.style;

    // 创建组件实例,并冻结数据,避免数据动态响应
    const instanceCom = new printOrderComponent({
        propsData: {
            data: Object.freeze(data),
        },
    });

    // 使用 Vue 的 nextTick 来确保 DOM 已更新
    this.$nextTick(() => {
        // 获取 iframe DOM 元素,作为打印区域
        const printArea = this.$refs.batchPrintArea;
        
        // 获取 iframe 内的文档对象
        const printContent = printArea.contentDocument || printArea.contentWindow.document;

        // 设置打印区域的 HTML 结构
        printContent.documentElement.innerHTML = `
          <html>
            <head></head>
            <body>
              <div id="print-box"></div>
            </body>
          </html>
        `;

        // 创建样式元素,将之前获取的样式插入到 iframe 的 head 中
        const style = document.createElement("style");
        style.innerHTML = styles;
        printContent.head.appendChild(style);

        // 将创建的组件实例挂载到 iframe 中的 #print-box 元素
        instanceCom.$mount(printContent.getElementById("print-box"));

        // 打印完成后执行自定义逻辑,比如更新打印状态
        printArea.contentWindow.onafterprint = () => {
            this.afterPrint(this.getOrderIds());
        };

        // 调用 iframe 的 print 方法,启动打印
        printArea.contentWindow.print();
    });
}

Vue 3 打印方法

const printClick = async (row: any, command: boolean) => {
    // 业务代码
    // ......
    // 准备打印数据
    let data = {};

    // 创建打印组件实例,将数据传递给组件
    const printOrderComponent = createApp(PrintTable, { data });

    // 等待 DOM 更新
    await nextTick(() => {
        // 获取 iframe DOM 元素,作为打印区域
        const printArea = batchPrintArea.value;

        // 获取 iframe 内的文档对象
        const printContent = printArea.contentDocument || printArea.contentWindow.document;

        // 获取当前页面所有样式,并将其转换为文本形式插入到打印页面中
        const stylesText = Array.from(document.styleSheets)
            .map((s) => {
                try {
                    // 将每个样式表的规则提取出来,拼接成字符串
                    return Array.from(s.cssRules)
                        .map((z) => z.cssText || "")
                        .join("\n");
                } catch (e) {
                    // 捕获跨域样式表引发的错误并跳过这些样式
                    console.warn("无法访问的样式表: ", s.href, e);
                    return "";
                }
            })
            .join("\n");

        // 设置 iframe 的 HTML 结构,并插入所有样式
        printContent.documentElement.innerHTML = `
            <html>
                <head>
                    <style>${stylesText}</style>
                </head>
                <body>
                    <div id="print-box"></div>
                </body>
            </html>
        `;

        // 将创建的打印组件挂载到 iframe 的 #print-box 元素
        printOrderComponent.mount(printContent.getElementById("print-box"));

        // 调用 iframe 的 print 方法,启动打印
        printArea.contentWindow.print();
    });
};

在这两个 Vue 打印功能实现中,核心原理是通过创建一个隐藏的 iframe 标签,将打印内容插入到 iframe 中,然后调用浏览器的 print() 方法从 iframe 中打印出内容。这是为了确保页面的打印内容独立于当前主页面,并且避免直接在主页面上操作 DOM 造成的副作用。

实现原理讲解

1. 打印模板的作用

打印模板是一个 Vue 组件,通常是为了定义需要打印的内容结构。这个模板可以根据需要动态生成,并且包含特定的样式和布局。

  • 在 Vue 2 中,打印模板使用 Vue.extend(PrintTable) 动态创建组件实例,便于将数据传入并生成对应的 DOM 元素。
  • 在 Vue 3 中,使用 createApp(PrintTable) 动态创建应用实例,然后挂载到指定的 DOM 元素。

这些模板组件和其他普通 Vue 组件类似,可以通过 propsdata 传入数据,生成想要展示的 HTML 结构。

2. iframe 的作用

iframe 是打印内容的容器。之所以使用 iframe,是为了隔离打印内容与当前页面的内容,避免主页面的样式或其他内容干扰打印效果。

  • iframe 元素是隐藏的,通常通过 refdocument.querySelector 来获取它。
  • 我们通过向 iframecontentDocumentcontentWindow.document 插入打印的 HTML 内容,确保在打印时,只打印 iframe 内的内容。

在实际应用中,我们通过 Vue 的 $refsref 属性获取到 iframe 元素。

3. printContent.documentElement.innerHTML 的作用

printContent.documentElement.innerHTML 是用来直接向 iframedocument 中插入 HTML 结构的关键操作。这个操作的原理是:

  • printContent:指向 iframe 内的文档对象,可以理解为 iframe 内的 DOM 树。
  • documentElement:是 iframe 文档的根节点,一般是 <html> 标签。
  • innerHTML:允许我们直接重写整个 iframe 的 HTML 内容。

具体步骤如下:

  1. 我们首先获取 iframe 的文档对象(即 printContent),然后通过 documentElement 来操作它的 HTML 结构。
  2. 使用 innerHTML 重写整个 iframe 的文档,包括 <html><head><body> 标签。这一步很重要,它让我们能够插入自定义的 HTML 结构和样式。
  3. 在这个 HTML 结构中,我们动态插入组件的容器(例如 <div id="print-box"></div>),然后将打印模板组件挂载到这个容器中。
  4. 通过 Vue 的 mountinstanceCom.$mount,将组件的内容渲染到 iframe 中的指定元素(即 #print-box 中),从而将打印的内容动态展示到 iframe 中。

4. 关联与执行流程

  • 打印模板(PrintTable):定义了要打印的内容布局和样式,这个模板将会被插入到 iframe 中作为打印内容。

  • iframe:作为一个独立的页面,用来承载要打印的 HTML 内容,并通过 print() 方法触发浏览器打印功能。

  • printContent.documentElement.innerHTML:将打印模板的 HTML 结构和样式插入到 iframe 中,使打印内容和主页面完全隔离。

    流程:

    1. 创建 iframe 标签并将其隐藏。
    2. iframe 中动态插入 HTML 结构,包括打印模板和必要的样式。
    3. 将打印模板挂载到 iframe 中的容器内。
    4. 调用 iframe.contentWindow.print() 打印 iframe 中的内容。

这样做的好处是保证打印内容的独立性和完整性,不会被主页面的样式或内容干扰。

5. 为什么不直接修改主页面打印?

直接在主页面上进行打印有几个问题:

  • 样式污染:如果打印的内容和主页面共享样式,打印时可能会出现不必要的样式,影响打印效果。
  • DOM 操作复杂:为了打印而修改主页面的 DOM 结构,会破坏主页面的原始布局,导致页面行为异常或错误。
  • 内容独立:通过 iframe 可以将打印内容与主页面内容完全隔离,使打印内容的控制更灵活。

总结:通过创建一个 iframe,我们可以在不影响主页面的情况下加载并打印所需内容。printContent.documentElement.innerHTML 用于将打印模板插入到 iframe 中,而 Vue 组件的动态挂载确保了打印内容的动态性和数据绑定。