目的:服务端渲染——SSR
- 环境及目的
测试服务器是 ubuntuserver18.04。网站运行环境是 lnmp。网站程序基本都是 spa,所以想通过 ssr 实现 seo。不想改代码,不想改代码,不想改代码~~ - 安装 node 和 npm
因为很久之前安装的,忘了步骤,大概和安装一般软件没区别吧。 - 建立服务文件夹
在准备运行服务的地方建立文件夹。比如:
cd /home/wwwroot mkdir node-render
- 进入文件夹安装 puppeteer
cd ./node-render npm i puppeteer -s
- 测试 node
vim test.js // 键入下面文字并保存 let a = 2 let b = 3 console.log(a * b) // 保存并运行 node test.js // 结果: 6
- 尝试通过 puppeteer 进行网站截图
首先安装一些依赖:
sudo apt-get install -yq --no-install-recommends libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 libgcc1 libgconf-2-4 libgdk-pixbuf2.0-0 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libpangocairo-1.0-0 libstdc++6 libx11-6 libx11-xcb1 libxcb1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 libxss1 libxtst6 libnss3
新建文件并键入如下内容:
// screenshoot.js
const puppeteer = require('puppeteer');
const url = process.argv[2];
if (!url) {
throw "Please provide URL as a first argument";
}
async function run () {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto(url);
await page.screenshot({path: 'screenshot.png'});
browser.close();
}
run();
运行:
node screenshot.js http://www.baidu.com
如果是root用户,可能报错:
Running as root without --no-sandbox is not supported.
修改 screenshot.js 代码:
...
const browser = await puppeteer.launch({headless: true, args:['--no-sandbox']});
...
再次运行:
node screenshot.js http://www.baidu.com
稍等一会,文件夹下会多一个 screenshot.png 的图片文件。截图成功。
- 尝试服务端渲染(ssr)
https://developers.google.com/web/tools/puppeteer/articles/ssr
其实我想找一种简单的方式,比如服务器上安装一个程序,nginx 配置一下反向代理,然后就可以了。所以并没有直接按照这篇文章中的内容实现。后来实在找不到,就从 github 上下载了一个实现。
https://github.com/wayou/ssr-demo
这个 demo 也是根据上面的文章写的。安装好后启动。192.168.0.110:8080。访问成功。然后就是改造了。
`${req.protocol}://${req.get("host")}/public/index.html`
// 改成下面。shici.tony93-dev.top 是虚拟机中原 spa 应用的地址
`${req.protocol}://shici.tony93-dev.top/`
nginx 新增虚机并反向代理。
server {
listen 80;
server_name poemssr.tony93-dev.top;
access_log logs/poem.access.log;
error_log logs/poem.error.log;
root /home/wwwroot/poem/;
index index.html index.htm index.php;
location ~ .*\.(js|css|ico)$ {
proxy_pass http://shici.tony93-dev.top;
}
location / {
proxy_pass http://localhost:8080;
#Proxy Settings
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
proxy_max_temp_file_size 0;
proxy_connect_timeout 90;
proxy_send_timeout 90;
proxy_read_timeout 90;
proxy_buffer_size 4k;
proxy_buffers 4 32k;
proxy_busy_buffers_size 64k;
proxy_temp_file_write_size 64k;
}
}
新增网址:poemssr.tony93-dev.top并进行反向代理。代理服务器地址为:http://localhost:8080。但是静态资源依然要访问原地址。所以,后缀名为js|css|ico的文件访问地址为http://shici.tony93-dev.top。
这里没有修改应用源码,所以,服务端渲染时和客户端加载后都会发起 ajax 请求,如果有随机生成的内容,那么渲染结果和加载结果将不一致。此外,只代理了首页。准确来说是所有页面请求都返回的首页。懒得改了,因为 spa 用的 hash 模式,要想每个页面都代理需要改为 history 模式。
听说百度已经可以理解 JavaScript 了。百度,加油,以后要不要服务端渲染就看你的了。
更新一下 router 采用 history 模式的配置。采用 hash 模式的缺点是 sharp 符号(#)后面的内容无法发送到服务器端,无法渲染。采用 history 模式可以解决这个问题,但服务器端需要配置一下,解决没有与路径对于的静态资源而报错的问题。关于配置,vue 官网是有例子的。
https://router.vuejs.org/zh/guide/essentials/history-mode.html
我这边是 nginx,所以配置如下:
location / {
try_files $uri $uri/ index.html;
}
部分优化。
- 重用 chrome 实例实现交叉渲染。
- 中止不必要的请求。
- 缓存页面。
以上三点可以在https://www.jianshu.com/p/ce84c77ecd53文章中找到。
关于缓存页面,我做了些改变,可以根据时间决定是否调用缓存。
\\ ssr.js
……
if (RENDER_CACHE.has(url)) {
// 在缓存时间内,则用缓存,否则重新渲染
if (Date.now() - RENDER_CACHE.get(url).time < 1000 * 10) {
console.info(`hasCached`);
return { html: RENDER_CACHE.get(url).html, ttRenderMs: 0 };
}
}
……
RENDER_CACHE.set(url, {html, time: Date.now()}); // cache rendered page.
……
项目地址只用一个域名。实际上这个算不上优化,只是之前没做好,防止 puppeteer 访问原页面地址造成死循环,所以用了两个域名,一个域名可以直接访问,另一个需要代理到 ssr 服务,然后 ssr 访问真实域名,渲染页面。
现在进行了修改,只用一个域名,通过 nginx 来区分访问者。如果是 ssr 中的
headless chrome,不进行反向代理,否则反向代理到 ssr 。
# 这些资源不进行反向代理
location ~ .*\.(js|css|ico|json|map)$ {
allow all;
}
# 对页面进行反向代理
location / {
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
proxy_max_temp_file_size 0;
proxy_connect_timeout 90;
proxy_send_timeout 90;
proxy_read_timeout 90;
proxy_buffer_size 4k;
proxy_buffers 4 32k;
proxy_busy_buffers_size 64k;
proxy_temp_file_write_size 64k;
set $notHeadless yes;
if ($http_user_agent ~* "HeadlessChrome" )
{
set $notHeadless no;
}
if ($notHeadless = yes) {
proxy_pass http://localhost:8080;
}
try_files $uri $uri/ index.html;
index index.html;
autoindex on;
autoindex_localtime on;
autoindex_exact_size off;
}
顺带说一下 nginx 的坑。nginx 的变量类型只有字符串。set $notHeadless false;. 这里的 false 是字符串,不是布尔值。所以,if ($notHeadless). 是 true,会进入相应代码块。正确的使用方式是if ($notHeadless = true). 这样才能得到预期的结果,不进入相应代码块。
另外,nginx 没有与或非的逻辑判断。
这一条不可用。
nginx的if会导致try_files失效。所以还是根据不同的请求进行代理。如果是爬虫,则代理到渲染服务,否则代理到另一个端口/域名。
# 目前有效的nginx配置
server
{
listen 80;
#listen [::]:80 default_server ipv6only=on;
server_name xxx.xxx.com;
root /home/wwwroot/xxxx/;
// 默认代理到渲染服务,可以根据 userAgent 重置 $proxy
set $proxy http://localhost:8080;
location ~ /\.
{
deny all;
}
# 这些资源不进行反向代理
location ~ .*\.(js|css|ico|json|map)$ {
allow all;
}
# 对页面进行反向代理
location / {
proxy_redirect off;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_next_upstream error timeout invalid_header http_500 http_502 http_503 http_504;
proxy_max_temp_file_size 0;
proxy_connect_timeout 90;
proxy_send_timeout 90;
proxy_read_timeout 90;
proxy_buffer_size 4k;
proxy_buffers 4 32k;
proxy_busy_buffers_size 64k;
proxy_temp_file_write_size 64k;
proxy_pass $proxy;
# try_files $uri $uri/ /index.html; # 这句不要,否则路径不对
}
access_log /home/wwwlogs/poem.tony93-dev.top.log;
error_log /home/wwwlogs/poem.tony93-dev.top.log;
}
server
{
listen 8081;
server_name xxx.xxx.com; # 和上面相同,只是在不同的端口下。也可以直接设置不同的域名。渲染服务那改个地址。
root /home/wwwroot/xxxx/;
location ~ /\.
{
deny all;
}
location / {
try_files $uri $uri/ /index.html;
}
access_log /home/wwwlogs/poem.tony93-dev.top.log;
error_log /home/wwwlogs/poem.tony93-dev.top.log;
}
闪屏问题。
不管是 ssr 还是 预渲染,客户端接收到页面后会出现闪屏现象。因该是客户端再次渲染导致的。官网教程也提供了解决方法:https://ssr.vuejs.org/zh/guide/hydration.html
SSR:
// ssr(node.js + pupeteer, 不是 vue 官网的 ssr):
……
// 将 const 改为 let
let html = await page.content(); // serialized HTML of page DOM.
……
// 解决闪屏
html = html.replace('id="app"', 'id="app" data-server-rendered="true"')
……
预渲染:
// 预渲染
new PrerenderSpaPlugin({
staticDir: config.build.assetsRoot,
routes: ['/','/home','/product'],
postProcess(context) {
context.html = context.html.replace('id="app"', 'id="app" data-server-rendered="true"');
return context;
}
})