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

接上篇css

防止從新渲染

其實說不對客戶端代碼作任何修改是忽悠人的。在咱們的Express 應用中,經過Puppteer加載頁面,提供給客戶端響應,可是這個過程是有一些問題的。html

js腳本在服務端的Headless Chrome 中執行過一次,可是等瀏覽器拿到真正的結果後,並不會阻止js再次執行,因此這種狀況下js會執行兩次(客戶端一次,服務端一次)git

針對咱們的例子,咱們能夠簡單的修復一下,咱們須要告訴頁面,須要的html已經生成了,不須要再次生成了,因此咱們能夠簡單的檢測<ul id="posts"> 是否在初始化時已存在,若是存在,說明在服務端已經渲染OK,沒有必要從新渲染了。代碼簡單修改以下:github

public/index.htmlchrome

 1 <html>
 2 <body>
 3   <div id="container">
 4     <!-- Populated by JS (below) or by prerendering (server). Either way,
 5          #container gets populated with the posts markup:
 6       <ul id="posts">...</ul>
 7     -->
 8   </div>
 9 </body>
10 <script>
11 ...
12 (async() => {
13   const container = document.querySelector('#container');
14 
15   // Posts markup is already in DOM if we're seeing a SSR'd.
16   // Don't re-hydrate the posts here on the client.
17   const PRE_RENDERED = container.querySelector('#posts');
18 //只有dom不存在時,纔會在客戶端渲染
19   if (!PRE_RENDERED) {
20     const posts = await fetch('/posts').then(resp => resp.json());
21     renderPosts(posts, container);
22   }
23 })();
24 </script>
25 </html>

 

優化

除了緩存預渲染後的結果以外,其實有不少有趣優化方案經過ssr()。有些優化方案是比較容易看到成效的,有的則須要細緻的思考才能看到成效,這主要根據應用頁面的類型以及應用的複雜度而定。express

終止非必須請求

當前,整個頁面(以及頁面中的全部資源)都是在無頭chrome中無條件加載。而後,咱們實際上只關注兩件事兒:json

1.渲染後的Html 標籤gulp

2.可以生成標籤的js請求api

因此不構建Dom結果的網絡請求都是浪費網絡資源。好比圖片、字體文件、樣式文件和媒體資並不實際參與構建HTML。樣式只是完整或者佈局DOM,可是並不會顯示的建立它,因此咱們應該告訴瀏覽器忽略掉這些資源!這樣作咱們能夠很大程度的節省帶寬提高預渲染的時間,尤爲對於包含了大量資源的頁面。瀏覽器

Devtools協議支持一個強大的特性,叫作網絡攔截,這種機制可讓咱們在瀏覽器真正發起請求以前修改請求對象。Puppteer經過開啓page.setRequestInterception(true)並設置page對象的請求事件, 來啓用網絡攔截機制。它容許咱們終止對某種資源的請求,放行咱們容許的請求。

ssr.mjs

 1 async function ssr(url) {
 2   ...
 3   const page = await browser.newPage();
 4 
 5   // 1. 啓用網絡攔截器.
 6   await page.setRequestInterception(true);
 7 
 8   page.on('request', req => {
 9     // 2.終止掉對不構建DOM的資源請求    // (images, stylesheets, media).
10     const whitelist = ['document', 'script', 'xhr', 'fetch'];
11     if (!whitelist.includes(req.resourceType())) {
12       return req.abort();
13     }
14 
15     // 3. 其餘請求正常放行
16     req.continue();
17   });
18 
19   await page.goto(url, {waitUntil: 'networkidle0'});
20   const html = await page.content(); // serialized HTML of page DOM.
21   await browser.close();
22 
23   return {html};
24 }

 

內聯資源文件內容

一般狀況下,咱們使用構建工具(如gulp等)在構建時直接把js、css等內聯到頁面中。這樣中能夠提高經過減小http請求來提高頁面初始化性能。

除了使用構建工具外,咱們也可使用瀏覽器作一樣的工做,咱們可使用Puppteer操做頁面DOM,內聯styles、Javascript以及其餘你想在預渲染以前內聯進去的資源。

這個列子展現了若是經過攔截響應對象,把本地css資源內聯到page的style標籤中:

import urlModule from 'url';
const URL = urlModule.URL;

async function ssr(url) {
  ...
  const stylesheetContents = {};

  // 1. Stash the responses of local stylesheets.
  page.on('response', async resp => {
    const responseUrl = resp.url();
    const sameOrigin = new URL(responseUrl).origin === new URL(url).origin;
    const isStylesheet = resp.request().resourceType() === 'stylesheet';
//對和頁面同一個域名的styles 暫存
    if (sameOrigin && isStylesheet) {
      stylesheetContents[responseUrl] = await resp.text();
    }
  });

  // 2. Load page as normal, waiting for network requests to be idle.
  await page.goto(url, {waitUntil: 'networkidle0'});

  // 3. Inline the CSS.
  // Replace stylesheets in the page with their equivalent <style>.
  await page.$$eval('link[rel="stylesheet"]', (links, content) => {
    links.forEach(link => {
      const cssText = content[link.href];
      if (cssText) {
        const style = document.createElement('style');
        style.textContent = cssText;
        link.replaceWith(style);
      }
    });
  }, stylesheetContents);

  // 4. Get updated serialized HTML of page.
  const html = await page.content();
  await browser.close();

  return {html};
}

對上述代碼作一下簡單說明:

一、使用page.on("response") 事件監聽網絡響應。

二、攔擊對本地css資源的響應並暫存

三、找到全部link標籤,替換爲style標籤,並設置textContent 爲上一步暫存的內容。

 

自動最小化資源

另一招你可使用網絡攔截器的是響應內容

好比,舉個例子來講,那你想在你的app中壓縮css資源,可是你同時但願在開發階段不作任何壓縮。那麼這時你也能夠經過在Puppteer在預渲染階段重寫響應內容,具體以下代碼:

 1 import fs from 'fs';
 2 
 3 async function ssr(url) {
 4   ...
 5 
 6   // 1. Intercept network requests.
 7   await page.setRequestInterception(true);
 8 
 9   page.on('request', req => {
10     // 2. If request is for styles.css, respond with the minified version.
11     if (req.url().endsWith('styles.css')) {
12       return req.respond({
13         status: 200,
14         contentType: 'text/css',
15         body: fs.readFileSync('./public/styles.min.css', 'utf-8')
16       });
17     }
18     ...
19 
20     req.continue();
21   });
22   ...
23 
24   const html = await page.content();
25   await browser.close();
26 
27   return {html};
28 }

這裏主要是使用request.respond方法,可直接查看接口說明文檔https://github.com/GoogleChrome/puppeteer/blob/master/docs/api.md#requestrespondresponse 

複用當個Chrome實例

每次預渲染都啓動一個browser實例會有很大的服務器負擔,因此更好的方法是,渲染不一樣頁面的時候或者說啓動不一樣渲染器的時候使用同一個實例,這樣能很大的程度的節省服務端的資源,增長預渲染的速度。

Puppteer能夠經過調用Puppteer.connect(url) 來鏈接到一個已經存在的實例,進而避免建立新的實例。爲了保持一個長期運行的browser實例,咱們能夠修改咱們的代碼,把啓動chrome的代碼從ssr()移動到Express Server入口文件中:

server.mjs

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

let browserWSEndpoint = null;
const app = express();

app.get('/', async (req, res, next) => {
  if (!browserWSEndpoint) {
    const browser = await puppeteer.launch();
    browserWSEndpoint = await browser.wsEndpoint();
  }

  const url = `${req.protocol}://${req.get('host')}/index.html`;
  const {html} = await ssr(url, browserWSEndpoint);

  return res.status(200).send(html);
});

 

ssr.mjs

import puppeteer from 'puppeteer';

/**
 * @param {string} url URL to prerender.
 * @param {string} browserWSEndpoint Optional remote debugging URL. If
 *     provided, Puppeteer's reconnects to the browser instance. Otherwise,
 *     a new browser instance is launched.
 */
async function ssr(url, browserWSEndpoint) {
  ...
  console.info('Connecting to existing Chrome instance.');
  const browser = await puppeteer.connect({browserWSEndpoint});

  const page = await browser.newPage();
  ...
  await page.close(); // Close the page we opened here (not the browser).

  return {html};
}

 

中篇結束,下篇爲最終篇(定時跑預渲染例子&其它注意事項)請持續關注

個人博客即將搬運同步至騰訊雲+社區,邀請你們一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=1v8oi9k363vog

相關文章
相關標籤/搜索