• 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

  • 相关阅读:
    使用MobaXterm远程连接Ubuntu,启动Octave,界面不能正常显示
    ABP .Net Core 日志组件集成使用NLog
    ABP .Net Core Entity Framework迁移使用MySql数据库
    ABP前端使用阿里云angular2 UI框架NG-ZORRO分享
    阿里云 Angular 2 UI框架 NG-ZORRO介绍
    Visual Studio 2019 Window Form 本地打包发布猫腻
    VS Code + NWJS(Node-Webkit)0.14.7 + SQLite3 + Angular6 构建跨平台桌面应用
    ABP .Net Core 调用异步方法抛异常A second operation started on this context before a previous asynchronous operation completed
    ABP .Net Core To Json序列化配置
    .Net EF Core数据库使用SQL server 2008 R2分页报错How to avoid the “Incorrect syntax near 'OFFSET'. Invalid usage of the option NEXT in the FETCH statement.”
  • 原文地址:https://www.cnblogs.com/Johnzhang/p/8891366.html
Copyright © 2020-2023  润新知