前端打印踩坑小记

提起来前端打印,不得不说api太少了。可供参考的东西也是很少,我在项目上做前端打印有一年多时间,将打印遇到问题总结如下

打印插件选择
  • react项目建议使用react-to-print插件,也可以使用从插件中提炼出来的如下方法;
import { findDOMNode } from 'react-dom';
// import { delay } from 'core-js';

const printElement = (options) => {
  const {
    content,
    pageStyle,
    onBeforePrint,
    onAfterPrint,
    bodyClass = '',
    copyStyles = true,
  } = options;

  const contentEl = content;

  if (contentEl === undefined || contentEl === null) {
    console.error(
      "Refs are not available for stateless components. For 'react-to-print' to work only Class based components can be printed"
    ); // eslint-disable-line no-console
    return;
  }

  const printWindow = document.createElement('iframe');
  printWindow.style.position = 'absolute';
  printWindow.style.top = '-1000px';
  printWindow.style.left = '-1000px';

  // eslint-disable-next-line
  const contentNodes = findDOMNode(contentEl);
  const linkNodes = document.querySelectorAll('link[rel="stylesheet"]');

  const linkTotal = linkNodes.length || 0;
  const linksLoaded = [];
  const linksErrored = [];

  const removeWindow = (target) => {
    setTimeout(() => {
      target.parentNode.removeChild(target);
    }, 500);
  };

  const triggerPrint = (target) => {
    if (onBeforePrint) {
      onBeforePrint();
    }

    setTimeout(() => {
      target.contentWindow.focus();
      target.contentWindow.print();
      removeWindow(target);

      if (onAfterPrint) {
        onAfterPrint();
      }
    }, 500);
  };

  const markLoaded = (linkNode, loaded) => {
    if (loaded) {
      linksLoaded.push(linkNode);
    } else {
      console.error(
        "'react-to-print' was unable to load a link. It may be invalid. 'react-to-print' will continue attempting to print the page. The link the errored was:",
        linkNode
      ); // eslint-disable-line no-console
      linksErrored.push(linkNode);
    }

    // We may have errors, but attempt to print anyways - maybe they are trivial and the user will
    // be ok ignoring them
    if (linksLoaded.length + linksErrored.length === linkTotal) {
      triggerPrint(printWindow);
    }
  };

  printWindow.onload = () => {
    /* IE11 support */
    if (window.navigator && window.navigator.userAgent.indexOf('Trident/7.0') > -1) {
      printWindow.onload = null;
    }

    const domDoc = printWindow.contentDocument || printWindow.contentWindow.document;
    const srcCanvasEls = [...contentNodes.querySelectorAll('canvas')];

    domDoc.open();
    domDoc.write(contentNodes.outerHTML);
    domDoc.close();

    /* remove date/time from top */
    const defaultPageStyle =
      pageStyle === undefined
        ? '@page { size: auto;  margin: 0mm; } @media print { body { -webkit-print-color-adjust: exact; } } html,body { overflow: auto!important; height: auto!important; }'
        : pageStyle;

    const styleEl = domDoc.createElement('style');
    styleEl.appendChild(domDoc.createTextNode(defaultPageStyle));
    domDoc.head.appendChild(styleEl);

    if (bodyClass.length) {
      domDoc.body.classList.add(bodyClass);
    }

    const canvasEls = domDoc.querySelectorAll('canvas');

    if (Array.isArray(canvasEls)) {
      [...canvasEls].forEach((node, index) => {
        node.getContext('2d').drawImage(srcCanvasEls[index], 0, 0);
      });
    }

    if (copyStyles !== false) {
      const headEls = document.querySelectorAll('style, link[rel="stylesheet"]');

      [...headEls].forEach((node, index) => {
        if (node.tagName === 'STYLE') {
          const newHeadEl = domDoc.createElement(node.tagName);

          if (node.sheet) {
            let styleCSS = '';

            for (let i = 0; i < node.sheet.cssRules.length; i++) {
              // catch 'member not found' error on cssText
              if (typeof node.sheet.cssRules[i].cssText === 'string') {
                styleCSS += `${node.sheet.cssRules[i].cssText}\r\n`;
              }
            }

            newHeadEl.setAttribute('id', `react-to-print-${index}`);
            newHeadEl.appendChild(domDoc.createTextNode(styleCSS));
            domDoc.head.appendChild(newHeadEl);
          }
        } else {
          const attributes = [...node.attributes];

          const hrefAttr = attributes.filter((attr) => attr.nodeName === 'href');
          const hasHref = hrefAttr.length ? !!hrefAttr[0].nodeValue : false;

          // Many browsers will do all sorts of weird things if they encounter an empty `href`
          // tag (which is invalid HTML). Some will attempt to load the current page. Some will
          // attempt to load the page's parent directory. These problems can cause
          // `react-to-print` to stop  without any error being thrown. To avoid such problems we
          // simply do not attempt to load these links.
          if (hasHref) {
            const newHeadEl = domDoc.createElement(node.tagName);

            attributes.forEach((attr) => {
              newHeadEl.setAttribute(attr.nodeName, attr.nodeValue);
            });

            newHeadEl.onload = markLoaded.bind(null, newHeadEl, true);
            newHeadEl.onerror = markLoaded.bind(null, newHeadEl, false);
            domDoc.head.appendChild(newHeadEl);
          } else {
            console.warn(
              "'react-to-print' encountered a <link> tag with an empty 'href' attribute. In addition to being invalid HTML, this can cause problems in many browsers, and so the <link> was not loaded. The <link> is:",
              node
            ); // eslint-disable-line no-console
            markLoaded(node, true); // `true` because we've already shown a warning for this
          }
        }
      });
    }

    if (linkTotal === 0 || copyStyles === false) {
      triggerPrint(printWindow);
    }
  };

  document.body.appendChild(printWindow);

  // return delay(1000);
};

export default printElement;
  • 使用该方法要将需要打印的ref传进去,比如:
import React, {useRef} from 'react;
import printElement from './myPrint';

import style from './index.module.less';
.
.
.
const printDOM=useRef(null);

printElement({content: printDOM.current});
.
.
.
<div ref={printDOM} className={style['transfer-card-print-result'}>
	<header>
		<span>我是要打印的东西</span>
	</header>
</div>
常常遇到的问题
(1)、我打印内容很多,怎么就显示一页,其他怎么没了?

打印时候要给body设置一个属性为height: auto,否则就只能看到一页了。

(2)、我写好的打印样式,怎么被别的功能影响了?
  • 造成这个现象原因有一下几点:
    没有使用样式模块化,导致不同功能可能会出现类名一样的问题,这样打包后,权重大的起作用。所以,就显示别人的样式了。
    所有样式都写在@print{}里了,哪怕你模块化了,所有内容都写在@print{}里也会导致打印的功能都会去媒体查询堆栈里找样式,不太友好。
(3)、我设置的A4纸横向打印没生效?

原因有两个:

  • 被别的功能@page{}干扰。
  • 没有设置对。
    正确做法是,开启样式模块化,并且需要用到媒体查询到地方,必须包裹在你的模块内容里边。修改纸张大小也要在你的模块里修改,否则就是全局的改变。打包后会全局污染。
@media print {
  body {
    .transfer-card-print-result {
      // 此处一定要加模块名,避免全局样式污染
      color: #000;
      display: block;
      margin: 0;
      box-sizing: border-box;
      overflow: hidden;
      @page {
        size: A4 landscape;
        margin: 40pt 0;
        overflow: hidden;
      }
      > header {
        page-break-before: always;
      }
      table {
        color: #000;
      }
    }
  }
}
.transfer-card-print-result {
  display: none;
  width: 100%;
  padding: 0 1.7cm;
  box-sizing: border-box;
  margin-top: 60px;
  color: #000;
  margin: 0 auto;
  overflow: hidden;
  header {
    display: flex;
    align-items: center;
    position: relative;
    margin-top: 10px;
    h1 {
      flex: 1;
      text-align: center;
    }
    > span {
      position: absolute;
      right: 0;
      top: 50%;
      transform: translateY(-50%);
    }
  }
}
(4)、打印如何换页?

在需要换页的地方只需要加入如下样式就行:

page-break-after: always; // 在此处之后换页
//或者
page-break-before: always; // 在此处之前换页
(5)、我的表格打印,有时候刚好某一行被上一张和下一张分割开,怎么办?
  • 如果不考虑上一张底部出现一条黑线,可以在tr中加入样式为:
page-break-inside: avoid;

也就是避免在此行插入换页符号,这样,当某行放不下时候就会自动跑到下一页去。

  • 如果考虑上一张出现黑色底边,影响用户体验,那就需要自己动手了。
    此时打印将会是偷梁换柱,需要将打印东西展示为两份,一份是模板,另外一份是真实打印的。(给tbody加避免换页属性不起作用,每行使用一个table就会导致上下多出一天线,如果不显示下边框,换页后上一页就会确实下边框)
    首先,说说模板。就是初次获取到后端数据后加载的页面,给这个设置样式为你需要打印纸张的尺寸宽度,高度不要设置。用useEffect检测渲染结束后,获取每行高度,将获取高度放入一个数组(建议一个头数据是一个组件,方便处理数据),将相邻数组求和,如果相邻之和超过纸张一页高度,将该长度截取一个新数组,保存起来。依次类推。
    假如需要高度超过400就换到下一张,获取到高度数据是arr=[60, 100, 300, 96];
    处理后数据为:[[60,100], [300, 96]];
    然后,将父组件传递过来的数据按照处理后数据格式切割行数据。
    将处理后数据返回给父元素,这样,整个页面渲染结束,父组件得到所有处理后的数据。
  • 真实打印数据,父组件最终得到想要数据结构,将分割行数据后的数据进行渲染展示,这样就达到自适应的目的。最终打印传递的ref也是渲染最后切割后的数据所在的内容。
    根据高度截取数据
function needSlice(arr, targetHeight) {
    let startIndex = 0;
    let endIndex = 0;
    let total = 0;
    const result = [];
    arr.forEach((itemHeight, j) => {
      if (total + itemHeight >= targetHeight) {
        const everyArry = arr.slice(startIndex, endIndex);
        result.push(everyArry);
        total = itemHeight;
        startIndex = j;
        if (j + 1 === arr.length) {
          const lastArray = arr.slice(startIndex, endIndex + 1);
          result.push(lastArray);
        }
      } else {
        total = itemHeight + total;
        endIndex = j + 1;
        if (j + 1 === arr.length) {
          const everyArry = arr.slice(startIndex, endIndex);
          result.push(everyArry);
        }
      }
    });
    return result;
  }

最终效果:
在这里插入图片描述
在这里插入图片描述

行数据一页放不下,就会自动跑到下一页,从数据层面来解决自动换页的问题。
最后,希望我遇到的问题总结,会帮助到你。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值