7

使用 Puppeteer 导出声享 PPT

 3 years ago
source link: https://zhuanlan.zhihu.com/p/59365797
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

使用 Puppeteer 导出声享 PPT

现状

声享是一个基于 ThinkJS 开发的在线制作 PPT 平台。声享制作的 PPT 支持代码高亮、图片上传、神奇效果等功能,同时你可以在声享收藏自己喜欢的 PPT 、对自己的 PPT 进行分类管理。其中有一个 PDF 导出的功能,可以将自己制作的 PPT 导出成 PDF 保存到本地。

功能实现比较简单,只是提供了一个页面,用户需要手动去打印成 PDF。这个方案存在一些问题:

  1. 由于使用了 iframe 懒加载导致未加载的 iframe 无法正常显示。
  2. 该种方案只能打印所有页面的初始状态。如果页面中存在切换动画,可能会丢失部分 PPT 信息。
  3. 需要用户手动操作,提高了使用难度。

如果是前端来生成 PDF,这些问题基本可以得到解决,但是开发量比较大而且存在一个效率问题。如果 PPT 页面存在多个 iframe,PDF 的生成时间过长会让用户长时间等待,明显不太合适。最终还是决定服务端来生成 PDF,才有了后来 Puppeteer 的尝试。

Puppeteer

v2-28ec223998409b3a0f88908827693866_720w.jpg

什么是Puppeteer呢?官方给的解释是:

Puppeteer is a Node library which provides a high-level API to control Chrome or Chromium over the DevTools Protocol. Puppeteer runs headless by default, but can be configured to run full (non-headless) Chrome or Chromium.

简而言之,这货是一个提供高级 API 的 node 库,能够通过 devtool 控制 headless 模式的 Chrome 或者 Chromium,它可以在 headless 模式下模拟任何的人为操作。通过它我们可以实现:

  • 生成页面的截图或者 PDF。
  • 抓取 SPA(单页应用)并生成预渲染内容(即“SSR”(服务器端渲染))。
  • 自动提交表单,进行 UI 测试,键盘输入等。

通过 Puppeteer,我们可以直接使用 Chrome 把我们需要的内容导出为 PDF。对比以前的实现方式有以下优点:

  1. 不需要用户手动操作,服务端生成 PDF 后直接以邮件的方式发送给用户。
  2. PPT 中的动画可以模拟用户翻页的动作触发,然后以初始、结束两张 PDF 的方式展示,不会丢失 PPT 内容。
  3. 不需要考虑图片/ iframe 跨域等问题。

可以说 Puppeteer 完美的解决来我们一期 PDF 导出存在的问题。

解决方案

我们基本的实现思路是:

  1. 打开一个正常的 PPT 播放页,获取需要打印的 DOM 元素并翻页 。
  2. 重复第一步操作直至到最后一页。
  3. 清空页面内容并将前两步获得的页面内容依次填充到当前页面(为什么要依次填充会在后面解释)。

对应上述方案实现的部分代码如下:

  1. 通过 Puppeteer 打开指定的页面。
// 测试时建议headless设置为false,以便可以直观看到页面效果
this.browser = await puppeteer.launch({headless: this.isDebug});
this.page = await this.browser.newPage();
await this.page.goto('https://xxxxx.com', { waitUntil:'networkidle2' });

2. 打开页面后可以通过 Puppeteer 模拟用户翻页操作,每次翻页后缓存需要打印的 DOM 元素字符串。

let canNext;
let i = 0;
const content = {};
do {
    canNext = await this.page.$('.navigate-right.enabled');
    const iframes = await this.page.$$('.PluginPage.present iframe').length;
    content[i++] = {
        iframe: iframes,
        domStr: await this.page.$eval('.RevealViewPort', el => el.outerHTML)
    }
    if (canNext) {
        await this.page.click('.navigate-right');
        // 等待翻页动画
        await this.page.waitFor(1000);
    }
} while (canNext);

3. 获取到要打印的所有页面 DOM 后,替换掉原来的页面内容。因为 $evaluate 方法中不支持调用外部变量所以只能以传参的方式使用。

this.page.evaluate(domStr => document.body.innerHTML = domStr, content);

4. 调用生成 PDF 的 API。

this.page.pdf({
    path: path.join(think.ROOT_PATH, 'runtime/xxx.pdf'),
    format: 'A4',
    landscape: true,
    printBackground: true //如果要显示背景,此属性要设置为true
})

5. 使用 nodemailer 发送邮件给用户。这一步如果想使用本地的 SMTP 服务请用 nodemailer 的 2.7.5 的版本,此版本后这项功能被删除了。

let transporter = nodemailer.createTransport({
    host: 'smtp.ym.163.com',
    port: 994,
    secure: true,
    auth: {
        user: '[email protected]',
        pass: 'xxx'
    }
});
transporter.sendMail({
    from: '[email protected]',
    to: '[email protected]',,
    subject: '【声享】xxx',
    attachments: [{
        filename: 'xxx.pdf',
        path: path.join(think.ROOT_PATH, 'runtime/xxx.pdf'),
        contentType: 'application/pdf'
    }]
})

开发中需要注意的问题

用户登录

使用 Puppeteer 打开页面相当于你新启动了一个浏览器实例,页面中的 seession 和 cookie 是空的。而打印所用的页面需要用到用户信息,所以我们登录了一个超管帐号来执行打印操作。在 ThinkJS 中可以通过中间件来实现这项功能。在访问页面的时候通过参数校验判断是否是打印而打开的页面,如果是则登录超管帐号。

// 打开指定页面时通过校验后面参数判断是否以超管登录
module.exports = options => {
    return async (ctx, next) => {
        const { token, ctime } = ctx.query;
        const md5Str = tockenGenerator();
        if (md5Str === token) {
            await ctx.session('userInfo', adminUser);
        }
        return next();
    };
};

Puppeteer 启动

如果服务端是运行在 root 权限下,在启动 Puppeteer 时要添加 --no-sandbox 参数,否则 Chrome/Chromium 会启动失败。详情见 Running as root without — no-sandbox is not supported。这个权限问题在linux以root用户使用 Chrome 的时候同样适用。

this.browser = await puppeteer.launch({args:['--no-sandbox']});

iframe 无法加载

声享支持页面内嵌入 iframe,在打印的时候碰到一个问题。如果同时在页面上插入 iframe 过多,后面的 iframe 会直接卡住不再加载。所以 iframe 最好分批插入或者一个一个插入,同时设定10秒来加载iframe。 如果想精确控制 iframe 也可以使用 API 等待 iframe 完全加载再执行后续操作。

for (let i = 0; i < pages.length; i++) {
    const page = pages[i];
    await this.page.$evaluate(content => {
        const divDom = document.createElement('div');
        divDom.innerHTML = content;
        document.body.appendChild(divDom.childNodes[0])
    }, page.domStr);
    if (page.iframe) await this.page.waitFor(10000 * page.iframe);
}

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK