• Headless Chrome:服务端渲染JS站点的一个方案【中篇】【翻译】


    接上篇

    防止重新渲染

    其实说不对客户端代码做任何修改是忽悠人的。在我们的Express 应用中,通过Puppteer加载页面,提供给客户端响应,但是这个过程是有一些问题的。

    js脚本在服务端的Headless Chrome 中执行过一次,但是等浏览器拿到真正的结果后,并不会阻止js再次执行,所以这种情况下js会执行两次(客户端一次,服务端一次)

    针对我们的例子,我们可以简单的修复一下,我们需要告诉页面,需要的html已经生成了,不需要再次生成了,所以我们可以简单的检测<ul id="posts"> 是否在初始化时已存在,如果存在,说明在服务端已经渲染OK,没有必要重新渲染了。代码简单修改如下:

    public/index.html

     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()。有些优化方案是比较容易看到成效的,有的则需要细致的思考才能看到成效,这主要根据应用页面的类型以及应用的复杂度而定。

    终止非必须请求

    当前,整个页面(以及页面中的所有资源)都是在无头chrome中无条件加载。然后,我们实际上只关注两件事儿:

    1.渲染后的Html 标签

    2.能够生成标签的js请求

    所以不构建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};
    }

    对上述代码做一下简单说明:

    1、使用page.on("response") 事件监听网络响应。

    2、拦击对本地css资源的响应并暂存

    3、找到所有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

  • 相关阅读:
    xe5 android tts(Text To Speech) [转]
    xe5 android sample 中的 SimpleList 是怎样绑定的 [转]
    xe5 android 控制蓝牙[转]
    xe5 android 调用照相机获取拍的照片[转]
    XE5 Android 开发数据访问手机端[转]
    XE5 Android 开发实现手机打电话和发短信 [转]
    让VCL的皮肤用在手机程序里 让安桌程序不山寨[转]
    XE5 Android 开发数据访问server端[转]
    XE5 Android 开发实现手机打电话和发短信[转]
    Delphi XE5的Android开发平台搭建[转]
  • 原文地址:https://www.cnblogs.com/Johnzhang/p/8891366.html
Copyright © 2020-2023  润新知