Headless Chrome:服务端渲染JS站点的一个方案【上篇】【翻译】介绍Headless Chrome 预渲染页面

原文链接:https://developers.google.com/web/tools/puppeteer/articles/ssr

注:由于英文水平有限,没有逐字翻译,可以选择直接阅读原文

tips:Headless浏览器完全可以作为服务端渲染的一个替代方案,在服务端转化js 站点为静态html页面;在webserver 上运行Headless 浏览器完全可以预渲染现代js 模式的应用,增加响应速度,对SEO也更加友好

本篇涉及到的技术展示了如何通过Google Headless 框架(puppteer)向一个Express web server 添加服务端渲染能力,对应用对友好的是,基本上不需要修改任何代码;所有的工作基本都有puppteer承担,通过简单的几行代码你就可以在服务端渲染几乎所有页面。

下面是将要涉及到的一小段代码:

 1 import puppeteer from 'puppeteer';
 2 
 3 async function ssr(url) {
 4   const browser = await puppeteer.launch({headless: true});
 5   const page = await browser.newPage();
 6   await page.goto(url, {waitUntil: 'networkidle0'});
 7   const html = await page.content(); // 页面的html内容
 8   await browser.close();
 9   return html;
10 }

注意:本篇文章代码基于es modules,需要node 8.5+ 并开启--experimental-modules

介绍

      如果你需要seo,你登录进来阅读这篇文章无外乎两种原因:第一,你已创建了一个web 应用,但是它没有被搜索引擎索引到,你的应用可能是一个SPA、PWA应用。或者其实技术栈创建的应用,实际上你使用的技术栈也无关重要;重要的是,你花费了大量的时间创建了很棒应用,但是用户却无法发现它。第二,你可能是从其它网站注意到服务端渲染能提高一定的性能。你在这可以可以收获如何减少javascript 启动成本以及如何提高首屏渲染。

tips:一些框架如(Preact)已经支持服务端渲染了,如果你使用的框架有服务端渲染的解决方案,那么坚持使用就好了,没有必要引入一个新的工具。

爬取现代web应用

    搜索引擎主要是爬取静态html标签来工作,但是现代的web 应用已经进化的比较复杂了。基于Javascript的应用,内容对网络爬虫来说是透明的,因为其内容多是在客户端通过js渲染的。一些爬虫比如google的爬虫也开始变得聪明了,google的爬虫使用Chrome41 执行Javascript 来得到最终页面,但是这种方案还是不太成熟、完美。比如,比如一些ES6的新特性在旧的浏览器中还是会引起Js error的。对于其他的搜索引擎,鬼知道他们怎么做的?O(∩_∩)O哈!

Headless Chrome 预渲染页面

    所有爬虫都理解HTML,所以我们需要解决的是如何执行JS,来生成HTML。如果我告诉你有这样一个工具,你觉得如何?

  1.    这个工具知道如何运行所有类型的Javascript,然后产出静态的html
  2.    这个工具随着web添加新特性会持续更新
  3.    修改少量设置不需要修改任何代码,你可以快速把这个工具应用到已有应用之上

听起来很不错吧?这个工具就是浏览器!

Headless Chrome 不关心使用什么库、框架、或者工具链;它早饭吃进去Javascript,午饭就会吐出来静态的HTML。当然我们希望会比这个过程快很多--Eric

如果你使用Node,Puppteer是一种比较简单的方式来操作headless Chrome.它提供的API 是一个客户端应用支持服务端渲染功能。下面是一个简单的例子。

1.JS应用

我们以一个通过js动态生成HTML的动态页面的例子开始:

public/index.html

 1 <html>
 2 <body>
 3   <div id="container">
 4     <!-- Populated by the JS below. -->
 5   </div>
 6 </body>
 7 <script>
 8 function renderPosts(posts, container) {
 9   const html = posts.reduce((html, post) => {
10     return `${html}
11       <li class="post">
12         <h2>${post.title}</h2>
13         <div class="summary">${post.summary}</div>
14         <p>${post.content}</p>
15       </li>`;
16   }, '');
17 
18   // CAREFUL: assumes html is sanitized.
19   container.innerHTML = `<ul id="posts">${html}</ul>`;
20 }
21 
22 (async() => {
23   const container = document.querySelector('#container');
24   const posts = await fetch('/posts').then(resp => resp.json());
25   renderPosts(posts, container);
26 })();
27 </script>
28 </html>

2.SSR (Server Side Render)方法

接下来,简单实现一下ssr方法

ssr.mjs

import puppeteer from 'puppeteer';

//内存缓存,key:url value:html内容
const RENDER_CACHE = new Map();

async function ssr(url) {
  if (RENDER_CACHE.has(url)) {
    return {html: RENDER_CACHE.get(url), ttRenderMs: 0};
  }

  const start = Date.now();

  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  try {
    // networkidle0 waits 500ms 没有其他请求时.
    // The page's JS has likely produced markup by this point, but wait longer
    // if your site lazy loads, etc.
    await page.goto(url, {waitUntil: 'networkidle0'});
    await page.waitForSelector('#posts'); //等待并确认 #posts 已经存在于dom中,如果已经存在,则立即执行.
  } catch (err) {
    console.error(err);
    throw new Error('page.goto/waitForSelector timed out.');
  }

  const html = await page.content(); // 被序列化后的HTML内容
  await browser.close();

  const ttRenderMs = Date.now() - start;
  console.info(`Headless rendered page in: ${ttRenderMs}ms`);

  RENDER_CACHE.set(url, html); // cache rendered page.

  return {html, ttRenderMs};
}

export {ssr as default};

主要代码逻辑:

  1. 添加缓存。缓存渲染后的HTML是提高响应的最有效方法,当你再次请求的时候,避免再次运行headless chrome。后续会讨论其他方面的优化。
  2. 对页面加载超时添加异常处理
  3. 调用page.waitForSelector('#posts')方法,确保id为posts的元素在后续操作之前已经存在于DOM中(有多中waitForxxx方法)
  4. 添加计量统计,计算Headless渲染页面时间

3.WebServer 端代码

最后,通过一个Express server 把所有内容联系到一起。哎直接看代码吧,代码中加了注释。

server.mjs

import express from 'express';
import ssr from './ssr.mjs';

const app = express();

app.get('/', async (req, res, next) => {
//调用上面写好的ssr方法,传入url,通过headless chrome 渲染完毕后把渲染结果返回
  const {html, ttRenderMs} = await ssr(`${req.protocol}://${req.get('host')}/index.html`);
  // Add Server-Timing! See https://w3c.github.io/server-timing/.
  res.set('Server-Timing', `Prerender;dur=${ttRenderMs};desc="Headless render time (ms)"`);
  return res.status(200).send(html); // Serve prerendered page as response.
});

app.listen(8080, () => console.log('Server started. Press Ctrl+C to quit'));

那么,得到的响应HTML应该是这样的:

<html>
<body>
  <div id="container">
    <ul id="posts">
      <li class="post">
        <h2>Title 1</h2>
        <div class="summary">Summary 1</div>
        <p>post content 1</p>
      </li>
      <li class="post">
        <h2>Title 2</h2>
        <div class="summary">Summary 2</div>
        <p>post content 2</p>
      </li>
      ...
    </ul>
  </div>
</body>
<script>
...
</script>
</html>

上篇结束,后续中篇 和 下篇 请继续关注

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

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏CSDN技术头条

0基础开发小程序游戏

更有人戏称小程序是互联网的第五大发明。由于微信自身的流量庞大,所以很多开发者看好小程序。而小程序之所以这么火,是因为其自身的引流模式和盈利模式,毕竟老板都喜欢既...

2.4K50
来自专栏jessetalks

初识WEB:输入URL之后的故事

概述   为什么输入www.cnblogs.com之后敲一个回车,浏览器就会显示我们所看到的内容?这家伙在背后到底偷偷的干了哪些事情?今天我们就来挖掘一下这背...

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

JavaScript 性能优化技巧分享

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

我的网站的后台管理的结构图。

不知道这个能不能放在首页,不行的话我会测下来的。 另外这个算不算是一种架构呢? 欢迎大家多多批评指教! ? 说在前面: 1、 配置文件并不是 web.confi...

31050
来自专栏十月梦想

HTML基本与主要结构

11020
来自专栏数据星河

如何对第一个Vue.js组件进行单元测试 (上)

  单元测试是持续集成的关键。通过专注于小的、独立的实体,确保单元测试始终按预期运行,使代码更加可靠,你可以放心地迭代你的项目而不必担坏事儿。

16320
来自专栏腾讯移动品质中心TMQ的专栏

手把手教你搭建安卓自动化框架之UIAutomator

前言 谷歌对UI测试(UI Tetsting)的概念是:确保用户在一系列操作过程中(例如键盘输入、点击菜单、弹出对话框、图像显示以及其他UI控件的改变),你的应...

3.9K100
来自专栏张戈的专栏

DX-watermark插件无法预览及上传图片报imagesx()错误的解决办法

本文重新更新编辑于:2014 年 6 月 8 日 0 时 40 分. 这篇文章还是在 2014 年 2 月 12 日发布的,旧标题为:《不明问题让我折腾了一天!...

39160
来自专栏hbbliyong

WPF备忘录(4)打个勾画个叉娱乐下

<Path Grid.Column="2" Data="M43,5 L20,40 20,40 0,20 6,15 18,26 37,7 43,5 z" Fil...

34140
来自专栏一枝花算不算浪漫

[Java面试十]浏览器跨域问题.

497190

扫码关注云+社区

领取腾讯云代金券