聲享是一個基於 ThinkJS 開發的在線製做 PPT 平臺。聲享製做的 PPT 支持代碼高亮、圖片上傳、神奇效果等功能,同時你能夠在聲享收藏本身喜歡的 PPT 、對本身的 PPT 進行分類管理。其中有一個 PDF 導出的功能,能夠將本身製做的 PPT 導出成 PDF 保存到本地。前端
功能實現比較簡單,只是提供了一個頁面,用戶須要手動去打印成 PDF。這個方案存在一些問題:node
若是是前端來生成 PDF,這些問題基本能夠獲得解決,可是開發量比較大並且存在一個效率問題。若是 PPT 頁面存在多個 iframe,PDF 的生成時間過長會讓用戶長時間等待,明顯不太合適。最終仍是決定服務端來生成 PDF,纔有了後來 Puppeteer 的嘗試。linux
什麼是Puppeteer呢?官方給的解釋是:git
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 模式下模擬任何的人爲操做。經過它咱們能夠實現:github
經過 Puppeteer,咱們能夠直接使用 Chrome 把咱們須要的內容導出爲 PDF。對比之前的實現方式有如下優勢:web
能夠說 Puppeteer 完美的解決來咱們一期 PDF 導出存在的問題。chrome
咱們基本的實現思路是:跨域
對應上述方案實現的部分代碼以下:瀏覽器
// 測試時建議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' });
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);
this.page.evaluate(domStr => document.body.innerHTML = domStr, content);
this.page.pdf({ path: path.join(think.ROOT_PATH, 'runtime/xxx.pdf'), format: 'A4', landscape: true, printBackground: true //若是要顯示背景,此屬性要設置爲true })
let transporter = nodemailer.createTransport({ host: 'smtp.ym.163.com', port: 994, secure: true, auth: { user: 'xxx@xxx.com', pass: 'xxx' } }); transporter.sendMail({ from: 'xxx@xxx.com', to: 'xxx@xxx.com',, 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(); }; };
若是服務端是運行在 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 最好分批插入或者一個一個插入,同時設定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); }