使用Headless Browser渲染页面

忙了很长一段时间,需要浮出水面来总结一工作了,不然做过的东西就像翻过一页完全没有记住的书,难免徒劳。

0. 起源

最近在做一个在线图片制作的项目,面向网站运营和自媒体作者,为他们提供素材库和在线的简单图片处理、合成的方案。这类工作当然最累的是前端了,画布组件组合、拖拽、变形、调色,图片裁剪、拼接,每一个单拿出来都够填好一阵子的。但今天我要说的不是前端(虽然这个颇具挑战的项目一度让我萌生了重拾前端的想法),而是后端。

在我们的界面中,画布是这样呈现在我们面前的:

很简单,它是一系列DOM元素的组合。然而当用户选择下载时,他们希望得到的是这样一张图片:

我们需要考虑的是,怎么把这一堆DOM扔到一张图片里?之前我曾经用过的方案就是前端渲染,通过把DOM元素写入canvas,再调用浏览器渲染引擎截图,html2canvas.js在这方面做得很好。然而省事的方法总伴随着一些麻烦:

  1. 浏览器分配到的资源有限。渲染速度慢,DOM转换canvas这个过程很费时间,特别是在DOM元素很多的情况下;
  2. 生成的图片要存储到文件系统,需要将图片转换为base64流走上传接口,而要保证图片质量的话,传输很占流量;
  3. 如果前端需要修改画布或素材样式,则需要将用户已保存的图片重新生成以完成同步,严重破坏数据一致性。

综上所述,一个神奇的解决方案——在后端渲染页面,就这么诞生了。

1. 敲定方案

在后端渲染页面,自己重新写个渲染引擎显然是不必要的,此时Headless Browser的概念开始进入我的视野。Headless Browser指的是一系列无界面的浏览器,一般用来配合爬虫生成网页的快照。它封装了某种浏览器内核,然后发起HTTP请求,对响应的内容进行渲染,输出图片。这跟我们的需求简直契合度100%。我考察了现在用的比较多的两种Headless Browser工具:

  • wkhtmltopdf/wkhtmltoimage
  • phantomjs

以上两个都是github上的开源项目,并且都是以Qt Webkit为内核的。经过一段时间的实际运用,也许是wkhtmltopdf的稳定版本Qt Webkit的引擎版本较低,对于一些web font的渲染支持并不是很好,与chrome等浏览器渲染效果有较大差异,于是我最终选择了phantomjs(中间省略数十万字踩坑血泪史)。

1.1. 抽象数据结构

有了Headless Browser后,我们需要得到页面的数据源来渲染页面,也就是为了得到和浏览器上显示一模一样的图片,后端必须拿到该页面所有的html、js、css代码。乍一看好像很麻烦,不过我们转念一想,我们需要渲染的也就只有画布这一个页面,那么我们参考前端的模板技术,定义好header、footer以及所有的js和css引用,把它们都放在服务器,到时候前端只需要把画布中的代码传过来不就好了吗?甚至我们还可以再进一步,把画布中的元素都抽象成数据结构,只需传输这些结构的实例,由服务器端根据预定义结构再拼装起来,岂不美哉?

以背景元素为例,它的类结构如下:

class Background extends Element {  
    // 标识属性
    var name;

    // 位置属性
    var width;
    var height;
    var top;
    var left;

    // 形变属性
    var rotate;
    var scale;
    var zIndex;

    // 颜色属性
    var color;
    var opacity;
}

当用户在画布上新建一个背景元素时,根据用户定义的参数生成背景的一个实例:

{
    "name": "Background1",
    "width": 900,
    "height": 500,
    "top": 0,
    "left": 0,
    "color": "rgba(255, 255, 255, 1)",
    "opacity": 100,
    "rotate": 0,
    "z-index": 0,
    "scale": 1,
}

1.2. 构建渲染模板

定义好数据结构之后,后台需要根据这些定义以及前端传输过来的上述元素实例来重新拼装出画布。为了达到这个目的,我们首先需要在服务器端建立一个用来渲染页面的模板。模板完成数据拼装后需要输出html代码给phantomjs,因此我们就将模板存成一个html文件。

部分示例代码如下,在这里我们使用Vue.js渲染数据,也可以根据需要使用其他渲染组件。

<html lang="en">  
<head>  
  <meta charset="UTF-8">
  <title>Document</title>
  <style>
    .elem-node-bg {
        z-index: 0;
    }
  </style>
  <script src="./js/vue.js"></script>
</head>  
<body>  
  <div id="app">
    <div>
      <template v-for="elem in elems">
        <!-- background -->
        <div class="elem-node elem-node-bg"
          :style="{'opacity':elem.opacity/100,'width': elem.width + 'px', 'height': elem.height + 'px', 'background-size': 'cover', 'background-color': elem.color}"></div>
      </template>
    </div>
</div>  
  </div>
  <script>
    var rawData = {$data}
    new Vue({
      el: '#app',
      data: {
          elems: rawData

      }
    })
  </script>
</body>  
</html>  

在后端可用字符串占位符替换的方法将数据实例塞进去,模板拼装这一步就完成了。得到的结果即将转入最后阶段:生成图片,

1.3. 生成图片

获取到拼装完成的html代码字符串后,我们可以开始使用phantomjs来渲染图片。在此之前,我选择先将这段代码写入到临时文件备用。随后,我们准备调用phantomjs的ScreenCapture方法,它的原理是在本地调起Webkit内核渲染指定页面,然后根据参数截取屏幕显示内容,生成图片。具体使用详见:http://phantomjs.org/screen-capture.html

新建文件render.js

var page = require('webpage').create();

// 在这里定义请求头,如访问目标对Referer、UserAgent有过滤机制的话可以加上
page.customHeaders = {  
    'Referer': 'http://www.xx.com',
};

// render.html即存储拼装数据的缓存文件
page.open('render.html', function() {  
  // 这里定义浏览器视窗宽高
  page.viewportSize = { width: 900, height: 500 };
  // 这里定义裁剪窗口坐标
  page.clipRect = { top: 0, left: 0, width: 900, height: 500 };

  page.evaluate(function() {
    // 在这里可以复写dom元素,此处在最底层添加了一个背景层
    var style = document.createElement('style'),
        text = document.createTextNode('body { background: #fff }');
    style.setAttribute('type', 'text/css');
    style.appendChild(text);
    document.head.insertBefore(style, document.head.firstChild);
  });

  // 指定输出文件名
  page.render('render.jpg');
  phantom.exit();
});

最后执行命令

phantomjs render.js  

图片生成成功。

2. 一些不足

这个方案简单易操作,当然也还会存在很多问题。

  1. 与其他浏览器渲染细节上会有差异(具体需要看浏览器内核版本)。这个需要不断测试,尽量避免一些兼容性差的样式写法;
  2. 服务器如果非Windows,在字体的渲染上生成的图片会与Windows上浏览器显示的画布元素有差别。这涉及到Linux字体渲染引擎,需要深入研究,甚至自己对浏览器内核有一些改造;
  3. 渲染过程比较耗时,会对前端响应造成一定的影响。可以考虑后台用异步的方式生成图片,前端保存图片后不等待直接返回,减少用户交互上的不适。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏ThoughtWorks

使用Enzyme测试React(Native)组件|洞见

组件化与UI测试 在组件化出现之前,我们不谈UI的单元测试,哪怕是对于UI页面进行测试都是一件非常困难的事情。其实组件化并不完全是为了复用,很多情况下也恰恰是为...

3334
来自专栏Material Design组件

后台系统设计(上篇:选择)

在单个选项下,存在多组互斥选项,且互斥选项组之间存在一定关系,可以考虑混用分段控件和常规按钮,由于分段控件在视觉上占用更大的面积,故给人在层级上更加置前。

4962
来自专栏cnblogs

如何写好css系列之button

      现代前端行业的发展,如果你在css的时候,还没有利用一些预编译工具,是否觉得自己太low了。但你是否考虑过搭建一套自己前端框架。可能你会想这是否有必...

2397
来自专栏葡萄城控件技术团队

程序员Web面试之jQuery

又到了一年一度的毕业季了,青春散场,却等待下一场开幕。 ? 在求职大军中,IT行业的程序员、码农是工科类大学生的热门选择之一, 尤其是近几年Web的如火如荼,更...

25710
来自专栏更流畅、简洁的软件开发方式

分享一个基于jQuery的锁定表格行列的js脚本。

  网上也有很多锁定行列的方法,一个是使用css,另一个就是专门的控件附带有锁定的功能。css的大多数锁定行,而不能锁定列。好像看过园子里的司徒正美,写过一个用...

3026
来自专栏互联网杂技

开始学习React js

现在最热门的前端框架有AngularJS、React、Bootstrap等。自从接触了ReactJS,ReactJs的虚拟DOM(Virtual DOM)和组件...

4356
来自专栏企鹅号快讯

常见的前端面试题,总有一点让你涨知识

首先在面试时,我会大声说:"本人擅长Ai、Fw、Fl、Br、Ae、Pr、Id、Ps等软件的安装与卸载,精通CSS、PHP、ASP、C、C++、C#、Java、R...

2307
来自专栏前端说吧

CSS-自定义高度的元素背景图如何自适应以及after伪类在ie下的处理

3338
来自专栏pangguoming

svg矢量图绘制以及转换为Android可用的VectorDrawable资源

项目需要 要在快速设置面板里显示一个VoWiFi图标(为了能够区分出来图形,我把透明的背景填充为黑色了) ? 由于普通图片放大后容易失真,这里我们最好用矢量图(...

5109
来自专栏Modeng的专栏

Vue一个案例引发的动态组件与全局事件绑定总结

最近在自学 Vue 也了解了一些基本用法,也记录了一些笔记有兴趣的朋友可以去查看我的其他文章,技术这东西真的不能光靠看,看是没有的,你必须要动手实践,只有在实战...

1390

扫码关注云+社区

领取腾讯云代金券