目的:服务端渲染——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; } })