Headless Chrome:服务端渲染JS站点的一个方案【中篇】【翻译】
接上篇
防止重新渲染
其实说不对客户端代码做任何修改是忽悠人的。在我们的Express 应用中,通过Puppteer加载页面,提供给客户端响应,但是这个过程是有一些问题的。
js脚本在服务端的Headless Chrome 中执行过一次,但是等浏览器拿到真正的结果后,并不会阻止js再次执行,所以这种情况下js会执行两次(客户端一次,服务端一次)
针对我们的例子,我们可以简单的修复一下,我们需要告诉页面,需要的html已经生成了,不需要再次生成了,所以我们可以简单的检测<ul id="posts"> 是否在初始化时已存在,如果存在,说明在服务端已经渲染OK,没有必要重新渲染了。代码简单修改如下:
public/index.html
<html>
<body>
<div id="container">
<!-- Populated by JS (below) or by prerendering (server). Either way,
#container gets populated with the posts markup:
<ul id="posts">...</ul>
-->
</div>
</body>
<script>
...
(async() => {
const container = document.querySelector('#container'); // Posts markup is already in DOM if we're seeing a SSR'd.
// Don't re-hydrate the posts here on the client.
const PRE_RENDERED = container.querySelector('#posts');
//只有dom不存在时,才会在客户端渲染
if (!PRE_RENDERED) {
const posts = await fetch('/posts').then(resp => resp.json());
renderPosts(posts, container);
}
})();
</script>
</html>
优化
除了缓存预渲染后的结果之外,其实有很多有趣优化方案通过ssr()。有些优化方案是比较容易看到成效的,有的则需要细致的思考才能看到成效,这主要根据应用页面的类型以及应用的复杂度而定。
终止非必须请求
当前,整个页面(以及页面中的所有资源)都是在无头chrome中无条件加载。然后,我们实际上只关注两件事儿:
1.渲染后的Html 标签
2.能够生成标签的js请求
所以不构建Dom结果的网络请求都是浪费网络资源。比如图片、字体文件、样式文件和媒体资并不实际参与构建HTML。样式只是完整或者布局DOM,但是并不会显示的创建它,所以我们应该告诉浏览器忽略掉这些资源!这样做我们可以很大程度的节省带宽提升预渲染的时间,尤其对于包含了大量资源的页面。
Devtools协议支持一个强大的特性,叫做网络拦截,这种机制可以让我们在浏览器真正发起请求之前修改请求对象。Puppteer通过开启page.setRequestInterception(true)并设置page对象的请求事件, 来启用网络拦截机制。它允许我们终止对某种资源的请求,放行我们允许的请求。
ssr.mjs
 async function ssr(url) {
   ...
   const page = await browser.newPage();
   // 1. 启用网络拦截器.
   await page.setRequestInterception(true);
   page.on('request', req => {
     // 2.终止掉对不构建DOM的资源请求    // (images, stylesheets, media).
     const whitelist = ['document', 'script', 'xhr', 'fetch'];
     if (!whitelist.includes(req.resourceType())) {
       return req.abort();
     }
     // 3. 其他请求正常放行
     req.continue();
   });
   await page.goto(url, {waitUntil: 'networkidle0'});
   const html = await page.content(); // serialized HTML of page DOM.
   await browser.close();
   return {html};
 }
内联资源文件内容
通常情况下,我们使用构建工具(如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在预渲染阶段重写响应内容,具体如下代码:
 import fs from 'fs';
 async function ssr(url) {
   ...
   // 1. Intercept network requests.
   await page.setRequestInterception(true);
   page.on('request', req => {
     // 2. If request is for styles.css, respond with the minified version.
     if (req.url().endsWith('styles.css')) {
       return req.respond({
         status: 200,
         contentType: 'text/css',
         body: fs.readFileSync('./public/styles.min.css', 'utf-8')
       });
     }
     ...
     req.continue();
   });
   ...
   const html = await page.content();
   await browser.close();
   return {html};
 }
这里主要是使用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
Headless Chrome:服务端渲染JS站点的一个方案【中篇】【翻译】的更多相关文章
- Headless Chrome:服务端渲染JS站点的一个方案【上篇】【翻译】
		
原文链接:https://developers.google.com/web/tools/puppeteer/articles/ssr 注:由于英文水平有限,没有逐字翻译,可以选择直接阅读原文 tip ...
 - vuejs服务端渲染更好的SEO,SSR完全指南Nuxt.js静态站生成器
		
vuejs服务端渲染更好的SEO,SSR完全指南Nuxt.js静态站生成器SSR 完全指南https://cn.vuejs.org/v2/guide/ssr.html在 2.3 发布后我们发布了一份完 ...
 - Nuxt.js  vue服务端渲染
		
一.为什么要用Nuxt.js 原因其实不用多说,就是利用Nuxt.js的服务端渲染能力来解决Vue项目的SEO问题. 二.Nuxt.js和纯Vue项目的简单对比 1. build后目标产物不同 vue ...
 - Vue.js 服务端渲染业务入门实践
		
作者:威威(沪江前端开发工程师) 本文原创,转载请注明作者及出处. 背景 最近, 产品同学一如往常笑嘻嘻的递来需求文档, 纵使内心万般拒绝, 身体倒是很诚实. 接过需求,好在需求不复杂, 简单构思 后 ...
 - ASP.NET Core 与 Vue.js 服务端渲染
		
http://mgyongyosi.com/2016/Vuejs-server-side-rendering-with-aspnet-core/ 原作者:Mihály Gyöngyösi 译者:oop ...
 - 使用 PHP 来做 Vue.js 的 SSR 服务端渲染
		
对于客户端应用来说,服务端渲染是一个热门话题.然而不幸的是,这并不是一件容易的事,尤其是对于不用 Node.js 环境开发的人来说. 我发布了两个库让 PHP 从服务端渲染成为可能.spatie/se ...
 - Vue 爬坑之路(十一)—— 基于 Nuxt.js 实现服务端渲染(SSR)
		
直接使用 Vue 构建前端单页面应用,页面源码时只有简单的几行 html,这并不利于网站的 SEO,这时候就需要服务端渲染 2016 年 10 月 25 日,zeit.co 背后的团队对外发布了一个 ...
 - Vue.js与 ASP.NET Core 服务端渲染功能整合
		
http://mgyongyosi.com/2016/Vuejs-server-side-rendering-with-aspnet-core/ 原作者:Mihály Gyöngyösi 译者:oop ...
 - NET Core 与 Vue.js 服务端渲染
		
NET Core 与 Vue.js 服务端渲染 http://mgyongyosi.com/2016/Vuejs-server-side-rendering-with-aspnet-core/原作者: ...
 
随机推荐
- 修改GeoJson的网址
			
http://geojson.io 可以打开自己的json 然后修改
 - JAVA设计模式之【装饰者模式】
			
JAVA设计模式之[装饰者模式] 装饰模式 对新房进行装修并没有改变房屋的本质,但它可以让房子变得更漂亮.更温馨.更实用. 在软件设计中,对已有对象(新房)的功能进行扩展(装修). 把通用功能封装在装 ...
 - 20162311 实验二  Java面向对象程序设计  实验报告
			
实验二 Java面向对象程序设计 实验内容 1. 初步掌握单元测试和TDD 2. 理解并掌握面向对象三要素:封装.继承.多态 3. 初步掌握UML建模 4. 熟悉S.O.L.I.D原则 5. 了解设计 ...
 - 敏捷冲刺每日报告——Day4
			
1.情况简述 Alpha阶段第一次Scrum Meeting 敏捷开发起止时间 2017.10.28 00:00 -- 2017.10.29 00:00 讨论时间地点 2017.10.28晚9:30, ...
 - java第5章学习总结
			
学号20145336 <Java程序设计>第5周学习总结 教材学习内容总结 try catch JVM会先尝试执行try区块中的内容,若发生错误且与catch后面的类型相符,则执行catc ...
 - Scrum 冲刺 第六日
			
Scrum 冲刺 第六日 目录 要求 项目链接 燃尽图 问题 今日任务 明日计划 成员贡献量 要求 各个成员今日完成的任务(如果完成的任务为开发或测试任务,需给出对应的Github代码签入记录截图:如 ...
 - SQL的介绍及MySQL的安装
			
基础篇 - SQL 介绍及 MySQL 安装 SQL的介绍及MySQL的安装 课程介绍 本课程为实验楼提供的 MySQL 实验教程,所有的步骤都在实验楼在线实验环境中完成, ...
 - python 操作MongoDB
			
安装MongoDB 启动数据库:安装完成指定数据库存放路径 mongod.exe --dbpath c:\data\db进入目录后运行mongo.exe 成功 创建数据库 > use mydb ...
 - Environment.getExternalStorageDirectory()
			
Environment.getExternalStorageDirectory()得到的是storage/emulated/0
 - Node.js系列文章:编写自己的命令行界面程序(CLI)
			
CLI的全称是Command-line Interface(命令行界面),即在命令行接受用户的键盘输入并作出响应和执行的程序. 在Node.js中,全局安装的包一般都具有命令行界面的功能,例如我们用于 ...