关于javascript:实践指南网页生成PDF

49次阅读

共计 5014 个字符,预计需要花费 13 分钟才能阅读完成。

一、背景

开发工作中,须要实现网页生成 PDF 的性能,生成的 PDF 需上传至服务端,将 PDF 地址作为参数申请内部接口,这个转换过程及转换后的 PDF 不须要在前端展现给用户。

二、技术选型

该性能不须要在前端展现给用户,为节俭客户端资源,抉择在服务端实现网页生成 PDF 的性能。

1. Puppeteer

Puppeteer 是一个 Node 库,它提供了高级 API 来通过 DevTools 协定管制 ChromeChromium

在浏览器中手动执行的大多数操作都能够应用 Puppeteer 实现,比方:

  • 生成页面的屏幕截图和 PDF;
  • 爬取 SPA 并生成预渲染的内容(即 SSR);
  • 主动进行表单提交,UI 测试,键盘输入等;
  • 创立最新的自动化测试环境。应用最新的 JavaScript 和浏览器性能,间接在最新版本的 Chrome 中运行测试;
  • 捕捉工夫线跟踪网站,以帮忙诊断性能问题;
  • 测试 Chrome 扩大程序。

从上可见,Puppeteer 能够实现在 Node 端生成页面的 PDF 性能。

三、实现步骤

1. 装置

进入我的项目,装置 puppeteer 到本地。

$ npm install -g cnpm --registry=https://registry.npm.taobao.org
$ cnpm i puppeteer --save

需注意的是,装置 puppeteer 时,会下载与 API 一起应用的最新版本的 Chromium 浏览器,有以下办法能够批改默认设置,不下载浏览器:

  1. 在环境变量中设置 PUPPETEER_SKIP_CHROMIUM_DOWNLOAD
  2. puppeteer-core 代替 puppeteer

puppeteer-corepuppeteer 的轻量级版本,默认不下载浏览器,而是启动现有的浏览器或者连贯近程浏览器,应用 puppeteer-core 需注意本地有可连贯的浏览器,且装置的 puppeteer-core 版本与打算连贯的浏览器兼容。连贯本地浏览器办法如下:

const browser = await puppeteer.launch({executablePath: '/path/to/Chrome'});

本我的项目须要部署至服务端,没有可连贯的浏览器,因而抉择装置的是 puppeteer

2. 启动浏览器

const browser = await puppeteer.launch({
    headless: true,
    args: ['--no-sandbox', '--font-render-hinting=medium']
  })

headless 代表无头模式,在后端启动浏览器,前端不会有展现。

小倡议:本地调试时,倡议设置 headless: false,能够启动残缺版本的浏览器,间接在浏览器窗口查看内容。

3. 关上新页面

生成浏览器后,在浏览器中关上新页面。

const page = await browser.newPage()

4. 跳转到指定页面

跳转至要生成 PDF 的页面。

await page.goto(`${baseURL}/article/${id}`, {
    timeout: 60000,
    waitUntil: 'networkidle2', // networkidle2 会始终期待,直到页面加载后不存在 2 个以上的资源申请,这种状态继续至多 500 ms
  })

timeout 是最长的加载工夫,默认 30s,网页加载工夫长的状况下,倡议将 timeout 值改大,避免超时报错。

waitUntil 示意页面加载到什么水平能够开始生成 PDF 或其余操作了,当网页需加载的图片资源较多时,倡议设置为 networkidle2,有以下值可选:

  • load:当 load 事件触发时;
  • domcontentloaded:当 DOMContentLoaded 事件触发时;
  • networkidle0:页面加载后不存在 0 个以上的资源申请,这种状态继续至多 500 ms;
  • networkidle2:页面加载后不存在 2 个以上的资源申请,这种状态继续至多 500 ms。

5. 指定门路,生成 pdf

上述指定的页面加载实现后,将该页面生成 PDF。

  const ext = '.pdf'
  const key = randomFilename(title, ext)
  const _path = path.resolve(config.uploadDir, key)
  await page.pdf({path: _path, format: 'a4'})

path 示意将 PDF 保留到的文件门路,如果未提供门路,PDF 将不会保留至磁盘。

小倡议:不论 PDF 是不是须要保留到本地,倡议在调试的时候都设置一个 path,不便查看生成的 PDF 的款式,查看是否有问题。

format 示意 PDF 的纸张格局,a4 尺寸为 8.27 英寸 x 11.7 英寸,是传统的打印尺寸。

留神:目前仅反对 headless: true 无头模式下生成 PDF

6. 敞开浏览器

所有操作实现后,敞开浏览器,节约性能。

  await browser.close()

四、难点

1. 图片懒加载

因为需生成 PDF 的页面是文章类型的页面,蕴含大量图片,且图片引入了懒加载,导致生成的 PDF 会带有很多懒加载兜底图,成果如下图:

解决办法是跳转到页面后,将页面滚动到底部,所有图片资源都会失去申请,waitUntil 设置为 networkidle2,图片就能加载胜利了。

await autoScroll(page) // 因为文章图片引入了懒加载,所以须要把页面滑动到最底部,保障所有图片都加载进去

/**
 * 管制页面主动滚动
 * */
function autoScroll (page) {return page.evaluate(() => {
    return new Promise<void>(resolve => {
      let totalHeight = 0
      const distance = 100
      // 每 200 毫秒让页面下滑 100 像素的间隔
      const timer = setInterval(() => {
        const scrollHeight = document.body.scrollHeight
        window.scrollBy(0, distance)
        totalHeight += distance
        if (totalHeight >= scrollHeight) {clearInterval(timer)
          resolve()}
      }, 200)
    })
  })
}

这里用到了 page.evaluate() 办法,用来管制页面操作,比方应用内置的 DOM 选择器、应用 window 办法等等。

2. CSS 打印款式

依据官网阐明,page.pdf() 生成 PDF 文件的款式是通过 print css media 指定的,因而能够通过 css 来批改生成的 PDF 的款式,以本文需要为例,生成的 PDF 须要暗藏头部、底部,以及其余和文章主体无关的局部,代码如下:

@media print {
  .other_info,
  .authors,
  .textDetail_comment,
  .detail_recTitle,
  .detail_rec,
  .SuspensePanel {display: none !important;}

  .Footer,
  .HeaderSuctionTop {display: none;}
}

3. 登录态

因为存在一部分文章不对外部用户公开,须要鉴权用户身份,符合要求的用户能力看到文章内容,因而跳转到指定文章页后,须要在生成的浏览器窗口中注入登录态,符合条件的登录用户能力看到这部分文章的内容。

采纳注入 cookie 的形式来获取登录态,应用 page.evaluate() 设置 cookie,代码如下:


async function simulateLogin (page, cookies, domain) {return await page.evaluate((sig, sess, domain) => {let date = new Date()
    date = new Date(date.setDate(date.getDate() + 1))
    let expires = ''
    expires = `; expires=${date.toUTCString()}`
    document.cookie = `koa:sess.sig=${sig}${expires}; domain=${domain}; path=/`
    document.cookie = `koa:sess=${sess}=${expires}; domain=${domain}; path=/` // = 是这个 cookie 的 value
    document.cookie = `is_login=true${expires}; domain=${domain}; path=/`
  }, cookies['koa:sess.sig'], cookies['koa:sess'], domain)
}


await simulateLogin(page, cookies, config.domain.split('//')[1])

小倡议:Puppeteer 也有自带的 api 实现 cookie 注入,如 page.setCookie({name: name, value: value}),然而我用这个形式注入没能获取到登录态,没有找到具体起因,倡议还是间接用我下面这个办法来注入 cookie,留神除 namevalue 外,expiresdomainpath 也须要配置。

4. Docker 部署 Puppeteer

依据上文操作,本地曾经能够胜利将页面生成 PDF 了,本地体验没问题后,须要部署到服务端给到测试、上线。

没有批改 Dockerfile 时,部署后发现了如下谬误:

官网有给 Docker 配置阐明能够参考,最终实际可用的 ubuntu 零碎的 Dockerfile 如下:

# ... 省略...

# 装置 puppeteer 依赖
RUN apt-get update && \
    apt-get install -y libgbm-dev && \
    apt-get install gconf-service libasound2 libatk1.0-0 libatk-bridge2.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release xdg-utils wget build-essential libcairo2-dev libpango1.0-dev libjpeg-dev libgif-dev librsvg2-dev -y && \
    apt-get install -y fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-kacst fonts-freefont-ttf --no-install-recommends

# ... 省略...

只须要重点关注 装置 puppeteer 依赖 局部即可。

留神:在 v1.18.1 之前,Puppeteer 至多须要 Node v6.4.0。从 v1.18.1 到 v2.1.0 的版本都依赖于 Node 8.9.0+。从 v3.0.0 开始,Puppeteer 开始依赖于 Node 10.18.1+。配置 Dockerfile 时也须要留神服务端的 node 版本。

五、总结

本文讲述了实现在 Node 端将网页生成 PDF 文件的残缺过程,总结为以下 3 点:

  1. 技术选型,依据需要场景抉择适合的伎俩实现性能;
  2. 浏览官网文档,疾速过一遍文档能力少遇到些坑;
  3. 破解难点,应用一个未应用的工具,会遇到没有解决过的难题,遇招拆招吧 ^ ^。

参照 Demo 源码 可疾速上手上述性能,心愿本文能对你有所帮忙,感激浏览❤️


· 往期精彩 ·

【直播回顾·程序媛的成长变质】

【大规格文件的上传优化】

【JDR DESIGN 开发小结】

欢送关注凹凸实验室博客:aotu.io

或者关注凹凸实验室公众号(AOTULabs),不定时推送文章。

正文完
 0