Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

使用 PhantomJS 实现服务端截图 #21

Open
ryancui92 opened this issue Feb 24, 2019 · 0 comments
Open

使用 PhantomJS 实现服务端截图 #21

ryancui92 opened this issue Feb 24, 2019 · 0 comments

Comments

@ryancui92
Copy link
Owner

ryancui92 commented Feb 24, 2019

服务端截图

最近需要实现服务端截图的需求,就是在 Server 开一个 Headless 的浏览器然后访问一个 URL,利用浏览器的截图功能来把整个网页保存成一张图片。由于整个渲染过程与用户在 Client 直接打开网页是一模一样的(只是不会真的渲染 UI),因此保存的图片也与用户实际在浏览器看到的完全一致。

本来想直接用 Puppeteer 就马上做完了,实在没想到 Puppeteer 并不支持在 CentOS 6 上使用。官网上写的 gtk3 的依赖只有 CentOS 7 才有,因此只能使用 PhantomJS 来做了。没想到这里面也有不少的问题。

PhantomJS

一开始我以为 PhantomJS 是一个跟 Puppeteer 类似的 Headless Browser 包,是一组操作浏览器网页的 API. 然而 PhantomJS 其实是一个 bin/script, 用于执行相应的 js 脚本。

这样跟 Puppeteer 相比实在是太麻烦了,所以我们选择了 phantomjs-node 这样一个包,他提供了与 Puppeteer 类似的基于 Promise 的 Node.js API, 我们就能顺利地从 Puppeteer 转到 PhantomJS 啦!

安装 phantomjs-node

$ npm i -S phantom

安装的时候会判断本地是否已经安装过 phantomjs-prebuilt, 如果没有就会帮你一并安装。

Github 上有完整的示例,这个 Demo 跟 Puppeteer 简直一模一样,换成百度官网就可以测试一下截图了。

const phantom = require('phantom')

(async function() {
  const instance = await phantom.create()
  const page = await instance.createPage()

  const status = await page.open('https://www.baidu.com/')
  await page.render('example')
  
  await instance.exit();
})()

不支持 Promise/const

PhantomJS 的 JS 引擎是真的旧的,保证要访问的网站的 JavaScript 都是 ES5 的语法,并且加上了各种 ES6 才有的语法的 Polyfill. 如果用了 babel 的话可以直接使用 babel-polyfills.

带 Cookie 访问网页

可以通过 page.addCookie 方法给网页添加 Cookie,这样访问时就不会跳回到登录页面了。这块的 API 可以参考 PhantomJS 的官方文档

await page.addCookie({
  name: 'xxx',
  value: 'xxx',
  path: '/',
  domain: 'localhost'
})

注意这个 domain 官方文档里没写是必须的,但实际测试中发现,domain 缺少或者不正确 Cookie 都不生效。

根据文档这个方法调用成功会返回 true,但实测调用成功后也会返回 false. 这是 v2.1.1 的一个 Bug.

等待异步请求结束

对于单页应用,页面会有很多的 XHR 请求来获取数据再进行渲染,而页面的 onload hook 跟这些异步请求并没有什么关系,因此 await page.open 后的截图,页面没有等待异步请求的返回,出来的结果也是有问题的。

对于 Puppeteer 来说,直接在 page.goto 的 API 提供一个 wait 选项,就能等待所有 XHR 结束了。沿着同样的思路,寻找 PhantomJS 的 API 时却没有发现,只有发请求和收请求的两个 Event:

  • onResourceReceived 页面收到 Resource 响应时触发
  • onResourceRequested 页面发出 Resource 请求时触发

通过一个数组的计数器来模拟这种等待 XHR 的功能:

// 等待 foo 返回 true
function waitUntil(foo) {
  return new Promise(resolve => {
    const interval = setInterval(() => {
      if (foo()) {
        clearInterval(interval)
        resolve()
      }
    }, 800)
  })
}

async () => {
  const requests = []
  await page.on('onResourceRequested', (requestData) => {
    requests.push(requestData.id)
  })

  await page.on('onResourceReceived', (response) => {
    const index = requests.indexOf(response.id)
    if (index > -1) {
      requests.splice(index, 1)
    }
  })
  
  await page.open('some url')

  // 等待所有请求完成
  await waitUntil(() => requests.length === 0)
  // 再等一会
  await waitUntil(() => true)
  
  // ...后续页面已经真正加载完毕
}

requestData.id 是请求资源的一个自增的索引,通过记录这个 ID 的一个数组并等待数组为空来确认请求已经全部返回。

但是这种方法并不是最「保险」的方法,或许会有下面几种情况的发生,在这些情况下,后续代码的执行并不能保证页面完全加载完毕。不过这样已经能保证绝大部分的页面能渲染完成才进行后续的截图。

1. received 事件会触发多次

文档上对 onResourceReceived 的描述写着:

If the resource is large and sent by the server in multiple chunks, onResourceReceived will be invoked for every chunk received by PhantomJS.

也就是说,received 事件是会被触发多次的,并带上同一个 response.id. 而按照我们的逻辑,在收到响应的第一个 chunk 时,数组里对应的 ID 就会被移除,这有可能导致数组为空。

2. 串行请求

如果页面的请求是串行的,就有可能出现一段很短的时间,请求 1 已经完成了但请求 2 还没发出去(比如在请求 1 和 2 之间做了很多费时的 UI Render),这时候数组有可能为空。

3. 请求完成但渲染未完成

即使的确请求全部已经返回,但也存在页面渲染仍未结束的问题。

页面色差

踩遍了前面的坑就能顺利地进行页面截图了,但有的时候发现明明网页上是有一个 BackgroundColor 的,但截出来的图片的 bgColor 变成了白色。

最后发现对于一些较浅的背景颜色,用 PhantomJS 渲染不出来= =目前这个问题被我们无视了,因为需要的图片质量也不用太高,只是用作缩略图而已。

Puppeteer 的部署问题

除了一开始提到的 CentOS 6 没有依赖的问题,使用 Puppeteer 的另一个令人头痛的问题就是部署。对于比较原始的部署方式,直接在本地把整个项目包括 node_modules 打包丢上服务器当然是没有太大的问题的,但如果部署方式稍微自动化一点,就不可避免地每次部署都需要在 Server 端安装 Puppeteer 的依赖,我国国情使这件事变得异常的困难,有几个方法可以解决。

使用淘宝源

淘宝不仅提供了 npm registry, 也提供了各种下载困难的包的源,戳这里浏览一下。

npm config set puppeteer_download_host https://npm.taobao.org/mirrors
npm install --production --registry=https://registry.npm.taobao.org

这样在 puppeteer 调用 node install.js 的时候就能使用 puppeteer_download_host 的 prefix,实际测试下来大概两三分钟就能下完了。

全局安装 Puppeteer

对生产环境有较大权力的话也可以尝试这种方法,但是 npm 默认的 require 是不能 require 到全局模块的,这意味着你不能这样用:

const puppeteer = require('puppeteer')	// 这里会报错 not found

// ...

这时你需要一个 requireg 的包,用了这个就能全局 require 了。如此一来,每次部署都不需要再拉一遍 Puppeteer 的依赖了,速度瞬间提升。

const requireg = require('requireg')
const puppeteer = requireg('puppeteer')

// ...

参考资料

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant