如何用js实现HTML转PDF

5,450 阅读4分钟

这是我参与2022首次更文挑战的第6天,活动详情查看:2022首次更文挑战

介绍

我们经常在开发过程中遇到要把网页某一部分打印到A4纸上的需求,但有时候效果或许并不尽人意,有时候会样式错乱,有时候出现浏览器或打印机的兼容问题,再或者运营想截取某部分大块内容存到本地这样的需求,这时我们可能就会想到一种解决方案,对,就是把html那部分内容转成PDF文件,这样在PDF上就可以实现打印和截取存储。

本期我们就来用js实现一个把HTML转成PDF文件的类,好让运营和产品给咱们加个鸡腿。其实实现这个也非常的简单,就是要把需要转的内容先生成canvas,再用canvas生成图片,最后用图片生成PDF文件。

正文

1.基础结构

我们先来安装俩个核心依赖分别是把html变成canvas的 html2Canvas 和 把图片变成PDF文件的 jspdf

# NPM
npm i -S html2Canvas jspdf
# YARN
yarn add html2Canvas jspdf

因为大多数需求是打印到A4至上,先定义一下,打印的类型数据,在设定的分辨率是72像素/英寸时,A4纸的尺寸的图像的像素是595×842。

// html2Pdf.js
const TYPES = {
    "a4": [595,842]
}

这里变成对象返回的目的是方便后期扩展,至于后面的想加A3,A5还是信纸的都可以加进去。

// html2Pdf.js
class Html2Pdf {
    constructor(option) {
        this.title = "";                // 文件标题
        this.noClass = ["no2canvas"];   // 需要忽略的class名
        this.type = "a4";               // 生成类型默认为A4纸张
        this.el;                        // 生成的目标元素
        this.beforeFn;                  // 生成图像前可执行
        this.afterFn;                   // 生成图像后可执行
        Object.assign(this, option);
        return this;
    }
    async start() {
        const { title, type, el, beforeFn, afterFn, noClass } = this;
        const [w, h] = TYPES[type];
        if (!el) throw new Error("Missing target element!")
    }
}
export default Html2Pdf;

上面是要传入一些必要参数,比如下载文件的标题,哪部分内容要转,转成什么纸张,内容中有哪些元素不需要转,还有生成图片前后需要的钩子函数来方便我们后期操作都要考虑进去。至于,start方法要执行转的函数,这里就简单判断了一下有没有相应的目标内容,有才会往下执行。

2.html转图片

// html2Pdf.js
class Html2Pdf {
    // ...
    async start() {
        const { title, type, el, beforeFn, afterFn, noClass } = this;
        const [w, h] = TYPES[type];
        if (!el) throw new Error("Missing target element!")
        beforeFn && await beforeFn.call(this);
        const canvas = await _createCanvas(el, noClass);
        let dataURL =await canvas.toDataURL('image/jpeg', 1.0);
        afterFn && await afterFn.call(this);
    }
}
const _createCanvas = async function (el, noClass) {
    return html2Canvas(el, {
        backgroundColor: '#ffffff',
        allowTaint: false,
        useCORS: true,
        ignoreElements: (element) => {
            for (let i = 0, len = noClass.length; i < len; ++i) {
                if ([].includes.call(element.classList, noClass[i])) {
                    return true;
                }
            }
        }
    })
}

这里我们期望的是 beforeFnafterFn 分别作为生成图像前后的钩子后期方便我们处理,中间那个 _createCanvas 后面我们将会用html2Canvas处理成canvas,再用canvas.toDataURL方法把画布处理成图片。

下图是做实例化测试的时候,打印处理的结果。

微信截图_20220123162011.png

3.图片转PDF

class Html2Pdf {
    // ...
    async start() {
        const { title, type, el, beforeFn, afterFn, noClass } = this;
        const [w, h] = TYPES[type];
        if (!el) throw new Error("Missing target element!")
        beforeFn && await beforeFn.call(this);
        const canvas = await _createCanvas(el, noClass);
        let dataURL =await canvas.toDataURL('image/jpeg', 1.0);
        afterFn && await afterFn.call(this);
        // 生成PDF
        const {width,height} = canvas
        let pageHeight = width / w * h;
        let leftHeight = height;
        let position = 0;
        let imgHeight = w / width * height;
        let PDF = new JsPDF('', 'pt', type);
        if (leftHeight < pageHeight) {
            PDF.addImage(dataURL, 'JPEG', 0, 0, w, imgHeight)
        } else {
            while (leftHeight > 0) {
                PDF.addImage(dataURL, 'JPEG', 0, position, w, imgHeight);
                leftHeight -= pageHeight;
                position -= h;
                if (leftHeight > 0) PDF.addPage()
            }
        }
        PDF.save(title + '.pdf')
    }
}

这一部分其实就把刚刚的生成的图像,将大小缩成跟A4纸统一,再用JsPDF转PDF文件下载下来,但可能页面内容很多,所以会出现分页,就判断一下他超没超出一页,如果超过不断记录其位置把图片的每一段都保存到PDF中,直至长度用尽。

4.使用

为了测试准备了一个index.html ,需要转的内容是div#content,它里面还有个button#pdf-btn按钮,点击手动触发转换PDF文件操作。

微信截图_20220123190149.png

// app.js
import Html2Pdf from "./src/html2Pdf"
const html2pdf = new Html2Pdf({
    title: "本土确诊,新增19例!北京丰台全区核酸检测!又两省出现本土疫情,均与北京病例关联",
    el: document.querySelector("#content"),
    type: "a4",
    beforeFn() {
        console.log("before")
    },
    afterFn() {
        console.log("after")
    }
})
document.getElementById("pdf-btn").addEventListener("click",e=>{
    html2pdf.start();
})

我们实例化一下刚才写的类,绑定触发HTML转PDF文件,我们就发现大功告成了~

微信截图_20220123190241.png

结语

我们上面的方案简单实现了一个HTML转PDF的类。当然其实里面并不完美,比如说清晰度并不是太过清晰,当然我们canvas.getContext("2d")用扩大的方法去弥补。另外,如果页面内容太长时,容易出现无法生成的问题,这里还需要做切片批量处理,如有需要我们可以自己来动动手扩展他。当然。但是凡事有利皆有弊。还有一个缺点就是因为内容先生成的图片,所以里面的内容会被截断,内容出现了割裂体验不是很好。所以,建议做这方面有需求的中小页面生成PDF还是很管用的,如果内容过长还需要找寻Window.print中相关的方案来解决,虽然体验和兼容有这些方面的问题,看自己取舍了。