针对jQuery页面做SSR服务器渲染方案

jq现在已经比较少出现在新项目的技术架构中了,但一些技术革新比较慢的团队依然采用jq在开发服务,那如何针对jq做服务器渲染呢,这里介绍一种koa2+puppeteer的解决方案。

正好最近有个PHP+jq的项目需要做服务器渲染来做SEO(PS:不想重构架构,先以最简单的方案解决问题),系统架构如下图所示:

在不更改原有代码结构的基础上最简单的方法就是加一层中间层来做这事,如下图所示:

  • (1)通过nginx将搜索引擎相关浏览引流到node服务器上;
  • (2)启动koa2作为web服务器接收请求,对Url进行重定向,发给Puppetter;
  • (3)puppeteer通过浏览器内核对页面的dom树进行渲染,返回给用户;

浏览器和服务端渲染流程差异对比

浏览器渲染主要会执行大量请求获取数据,而这是很耗时的,服务器渲染是指将数据获取在服务端拼接好直接发给浏览器,浏览器只要加载页面的数据就可以。

puppeteer 安装

puppeteer 在服务器安装其实还是挺多坑的,详细可以见:
https://github.com/puppeteer/puppeteer/blob/main/docs/troubleshooting.md#chrome-headless-doesnt-launch-on-unix

这里简单介绍下puppeteer在服务器安装:

npm i puppeteer

你以为这样就可以啦?其实大部分时候你运行将遇到崩溃,为啥?在安装目录下运行ldd检查依赖就知道:

$ cd ./node_modules/puppeteer/.local-chromium/linux-782078/chrome-linux/
$ ldd chrome | grep not
libatk-1.0.so.0 => not found
libatk-bridge-2.0.so.0 => not found
libXcomposite.so.1 => not found
libXcursor.so.1 => not found
libXdamage.so.1 => not found
libXfixes.so.3 => not found
libXi.so.6 => not found
libXtst.so.6 => not found
libcups.so.2 => not found
libgbm.so.1 => not found
libpangocairo-1.0.so.0 => not found
libpango-1.0.so.0 => not found
libcairo.so.2 => not found
libatspi.so.0 => not found
libXss.so.1 => not found
libgtk-3.so.0 => not found
libgdk-3.so.0 => not found
libgdk_pixbuf-2.0.so.0 => not found

一堆依赖库没装,毕竟大部分服务器环境不需要这些浏览器相关的动态库依赖。

Centos依赖包如下:

alsa-lib.x86_64
atk.x86_64
cups-libs.x86_64
gtk3.x86_64
ipa-gothic-fonts
libXcomposite.x86_64
libXcursor.x86_64
libXdamage.x86_64
libXext.x86_64
libXi.x86_64
libXrandr.x86_64
libXScrnSaver.x86_64
libXtst.x86_64
pango.x86_64
xorg-x11-fonts-100dpi
xorg-x11-fonts-75dpi
xorg-x11-fonts-cyrillic
xorg-x11-fonts-misc
xorg-x11-fonts-Type1
xorg-x11-utils

一个个安装好就可以了。有些服务器kernel不支持 sandbox 模式,可以设置关闭 sandbox 模式:

const browser = await puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox']});

puppeteer使用案例

// 导入包
const puppeteer = require('puppeteer');

(async () => {
// 因为服务器内核不支持sandbox,所以只能启用--no-sandbox
const browser = await puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox']});
const page = await browser.newPage();
let time1 = new Date().getTime();
await page.setJavaScriptEnabled(true);
// 由于只关心渲染后的dom树,所以对css,font,image等都做了屏蔽
await page.setRequestInterception(true);
page.on('request', (req) => {
if(req.resourceType() == 'stylesheet' || req.resourceType() == 'font' || req.resourceType() == 'image'){
req.abort();
}
else {
req.continue();
}
});
// waitUntil 主要包括四个值,'load','domcontentloaded','networkidle2','networkidle0'
// 分别表示在xx之后才确定为跳转完成
// load - 页面的load事件触发时
// domcontentloaded - 页面的 DOMContentLoaded 事件触发时
// networkidle2 - 只有2个网络连接时触发(至少500毫秒后)
// networkidle0 - 不再有网络连接时触发(至少500毫秒后)
await page.goto('https://developer.orbbec.com.cn/', { waitUntil: ['load','domcontentloaded','networkidle2'] });

console.log(await page.content());
let time2 = new Date().getTime();
console.log((time2-time1)/1000)
console.log("finish");
// 关闭浏览器
await browser.close();
})();

chrome命令参数:https://peter.sh/experiments/chromium-command-line-switches/
可以用于chrome启动项的优化,比如将Dom解析和渲染放到同一进程、禁止初始化的默认url、禁止GPU等。

koa2 使用案例

koa2 是一个js的web服务器,安装很简单,只要node版本大于7.6.0即可,安装koa:

$ npm i koa

下面是个简单koa的demo:

const Koa = require('koa');
const app = new Koa();

app.use(async ctx => {
console.log(ctx.url);
ctx.body = 'Hello World';
});

console.log("running");
app.listen(10311);

测试运行:

$ curl -i 127.0.0.1:10311
HTTP/1.1 200 OK
Content-Type: text/plain; charset=utf-8
Content-Length: 11
Date: Sat, 12 Sep 2020 20:05:45 GMT
Connection: keep-alive

hello world

这里我们主要用到 koa context,ctx 封装了 request 和 response 对象,详细参考:https://koa.bootcss.com/#context

完整例子

下面将 koa 和 puppeteer 结合做一个 SSR 渲染服务器:

// 导入包
const puppeteer = require('puppeteer');

const Koa = require('koa');
const app = new Koa();

app.use(async ctx =>{
console.log(ctx.url);
let url = 'https://developer.orbbec.com.cn' + ctx.url
console.log(ctx.url);
// 因为服务器内核不支持sandbox,所以只能启用--no-sandbox
const browser = await puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox']});

const page = await browser.newPage();
let time1 = new Date().getTime();
await page.setJavaScriptEnabled(true);
// 由于只关心渲染后的dom树,所以对css,font,image等都做了屏蔽
await page.setRequestInterception(true);
page.on('request', (req) => {
if(req.resourceType() == 'stylesheet' || req.resourceType() == 'font' || req.resourceType() == 'image'){
req.abort();
}
else {
req.continue();
}
});
// waitUntil 主要包括四个值,'load','domcontentloaded','networkidle2','networkidle0'
// 分别表示在xx之后才确定为跳转完成
// load - 页面的load事件触发时
// domcontentloaded - 页面的 DOMContentLoaded 事件触发时
// networkidle2 - 只有2个网络连接时触发(至少500毫秒后)
// networkidle0 - 不再有网络连接时触发(至少500毫秒后)
await page.goto(url, { waitUntil: ['load','domcontentloaded','networkidle2'] });

ctx.body = await page.content();

let time2 = new Date().getTime();
console.log((time2-time1)/1000)
console.log("finish");

// 关闭浏览器
await browser.close();
});

app.listen(10133);

优化

// 导入包
const puppeteer = require('puppeteer');

const Koa = require('koa');
const app = new Koa();

// 存储browserWSEndpoint列表
let WSE_LIST = [];

// browser 初始化,将bwse存储复用
// 我这里只起了一个进程,如果机器性能好的话可以启多几个,不过切记不要太多
(async() =>{
// 因为服务器内核不支持sandbox,所以只能启用--no-sandbox
// --no-first- run 可以减少第一个标签页开启的时间
// 更多参数可以参考https://peter.sh/experiments/chromium-command-line-switches/
const browser = await puppeteer.launch({args: ['--no-sandbox', '--disable-setuid-sandbox','--no-first-run']});
// 存储节点以便能重新连接到 Chromium
const browserWSEndpoint = await browser.wsEndpoint();
WSE_LIST = [browserWSEndpoint]
})();

app.use(async ctx =>{
console.log(ctx.url);
let time1 = new Date().getTime();
let url = 'https://developer.orbbec.com.cn' + ctx.url
console.log(url);

// 恢复节点
let browserWSEndpoint = WSE_LIST[0]
console.log(browserWSEndpoint)
const browser = await puppeteer.connect({browserWSEndpoint});

// 开启新的标签页
let page = await browser.newPage();
await page.setJavaScriptEnabled(true);
// 由于只关心渲染后的dom树,所以对css,font,image等都做了屏蔽
await page.setRequestInterception(true);
page.on('request', (req) => {
if(req.resourceType() == 'stylesheet' || req.resourceType() == 'font' || req.resourceType() == 'image'){
req.abort();
}
else {
req.continue();
}
});


// waitUntil 主要包括四个值,'load','domcontentloaded','networkidle2','networkidle0'
// 分别表示在xx之后才确定为跳转完成
// load - 页面的load事件触发时
// domcontentloaded - 页面的 DOMContentLoaded 事件触发时
// networkidle2 - 只有2个网络连接时触发(至少500毫秒后)
// networkidle0 - 不再有网络连接时触发(至少500毫秒后)
await page.goto(url, { waitUntil: ['load','domcontentloaded','networkidle2'] });

ctx.body = await page.content();
// 关闭标签页
await page.close();

// 断开连接
await browser.disconnect();

let time2 = new Date().getTime();
console.log((time2-time1)/1000)
console.log("finish");
// 关闭浏览器
// await browser.close();
});

app.listen(10133);
shikanon wechat
欢迎您扫一扫,订阅我滴↑↑↑的微信公众号!