Headless Chrome:服務端渲染JS站點的一個方案【上篇】【翻譯】

原文連接:https://developers.google.com/web/tools/puppeteer/articles/ssr
注:因爲英文水平有限,沒有逐字翻譯,能夠選擇直接閱讀原文

 

tips:Headless瀏覽器徹底能夠做爲服務端渲染的一個替代方案,在服務端轉化js 站點爲靜態html頁面;在webserver 上運行Headless 瀏覽器徹底能夠預渲染現代js 模式的應用,增長響應速度,對SEO也更加友好javascript

 

本篇涉及到的技術展現瞭如何經過Google Headless 框架(puppteer)向一個Express web server 添加服務端渲染能力,對應用對友好的是,基本上不須要修改任何代碼;全部的工做基本都有puppteer承擔,經過簡單的幾行代碼你就能夠在服務端渲染幾乎全部頁面。html

下面是將要涉及到的一小段代碼:java

 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-modulesnode

介紹

      若是你須要seo,你登陸進來閱讀這篇文章無外乎兩種緣由:第一,你已建立了一個web 應用,可是它沒有被搜索引擎索引到,你的應用多是一個SPA、PWA應用。或者其實技術棧建立的應用,實際上你使用的技術棧也無關重要;重要的是,你花費了大量的時間建立了很棒應用,可是用戶卻沒法發現它。第二,你多是從其它網站注意到服務端渲染能提升必定的性能。你在這能夠能夠收穫如何減小javascript 啓動成本以及如何提升首屏渲染。react

tips:一些框架如(Preact)已經支持服務端渲染了,若是你使用的框架有服務端渲染的解決方案,那麼堅持使用就行了,沒有必要引入一個新的工具。git

爬取現代web應用

    搜索引擎主要是爬取靜態html標籤來工做,可是現代的web 應用已經進化的比較複雜了。基於Javascript的應用,內容對網絡爬蟲來講是透明的,由於其內容可能是在客戶端經過js渲染的。一些爬蟲好比google的爬蟲也開始變得聰明瞭,google的爬蟲使用Chrome41 執行Javascript 來獲得最終頁面,可是這種方案仍是不太成熟、完美。好比,好比一些ES6的新特性在舊的瀏覽器中仍是會引發Js error的。對於其餘的搜索引擎,鬼知道他們怎麼作的?O(∩_∩)O哈!github

Headless Chrome 預渲染頁面

    全部爬蟲都理解HTML,因此咱們須要解決的是如何執行JS,來生成HTML。若是我告訴你有這樣一個工具,你以爲如何?web

  1.    這個工具知道如何運行全部類型的Javascript,而後產出靜態的html
  2.    這個工具隨着web添加新特性會持續更新
  3.    修改少許設置不須要修改任何代碼,你能夠快速把這個工具應用到已有應用之上

聽起來很不錯吧?這個工具就是瀏覽器!chrome

Headless Chrome 不關心使用什麼庫、框架、或者工具鏈;它早飯吃進去Javascript,午餐就會吐出來靜態的HTML。固然咱們但願會比這個過程快不少--Ericexpress

若是你使用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>

 

上篇結束,後續中篇 和 下篇 請繼續關注

相關文章
相關標籤/搜索