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

相关文章
相关标签/搜索