使用 pyppeteer 预渲染对 vue 单页应用进行 SEO
问题背景
由于 我的博客 使用 vue 开发,是一个 SPA 单页应用,因此在不进行特殊优化的默认状态下,加载界面时只会返回一个空白 HTML 文件。在加载 HTML 文件中的 js 文件后,才会进行构建 DOM、拉取数据并显示等操作。而目前的爬虫(百度爬虫、Google爬虫等等)并不支持这种先加载 HTML 文档、后拉取数据的异步操作。这就对 SEO 带来了很大的困难。
通常来说,SPA 对此的应对办法是 ssr (服务器端渲染)或是预渲染。由于我的博客是不怎么变化的,因此预渲染应该比服务器端渲染更加符合我的要求。下面讲一下我是怎么进行预渲染的。
问题解决
node.js 编译期预渲染——放弃
通常来说预渲染可以在 vue 打包的时候就进行,使用包 prerender-spa-plugin。但是这对我的需求来说不是特别符合,原因有:
- 在 vue 打包的时候进行预渲染,意味着我每发布一篇博文,都需要重新打包一次 vue 并部署。而我从 hexo 迁移到目前这套自写的博客系统的原因之一正是不喜欢每次写博文都要重新编译并打包部署(虽然可以 CI 自动化)。
- 我的博客使用了 CDN 进行部署,静态资源全部由配置中的
publicPath
配置为 CDN 分发。这也就代表着,编译的时候全部静态资源都是以https://cdn.com/
开头的。而预渲染会尝试加载这些还没有被上传到 CDN 的资源,因此无法正常加载 js,从而无法进行预渲染。通常的解决方法有:- 采用多次编译,先预编译并部署静态文件到 CDN,再进行预渲染。
- 使用本地路径,编写脚本对预渲染输出文件中的路径进行替换。
- 采用 DNS 胁持或是中间人攻击,替换静态文件。
考虑到这两点,我需要在博客的网页端完成部署后,独立地进行完整的预渲染过程;即使用无头浏览器加载 js 并保存渲染后的网页为静态文件。
使用 python+Puppeteer 在部署后预渲染
无头浏览器有很多选择,在这里我选择了非常友好的 Puppeteer。操作的语言也有很多选择,人生苦短,我选 python。
python 的 puppeteer 包可以选择 pyppeteer。
pyppeteer 的安装 (python 3.6+)
pip install pyppeteer
chromium 浏览器下载的解决
pyppeteer 基于 chromium 进行渲染,因此需要下载 chromium。我们可以在第一次运行时进行下载,也可以手动完成这个下载过程:
pyppeteer-install
默认情况下, pyppeteer 会从官方地址进行下载。在中国,由于众所周知的问题,这个下载是受阻的。这里,我们可以手动进行下载。
通过下面这个命令获取当前 pyppeteer 使用的 chromium zip 包地址:
>>> import pyppeteer
>>> print(pyppeteer.chromium_downloader.get_url())
https://storage.googleapis.com/chromium-browser-snapshots/Win/575458/chrome-win32.zip
下载完后,将其解压到如下路径:
>>> d = pyppeteer.chromium_downloader
>>> print(d.DOWNLOADS_FOLDER / d.REVISION)
C:\Users\(your username)\AppData\Local\pyppeteer\pyppeteer\local-chromium\575458
解压后,目录结构应该是这样的:
再运行一遍 pyppeteer-install
——它应该告诉你,chromium 已经安装好了。
预渲染脚本编写
预渲染脚本按照 pyppeteer
的文档很容易编写:
PAGE_URL = 'https://gwy15.com'
ROUTES = [
['/', 'index'],
['/blog', 'blog']
]
browser = await launch()
for route, name in ROUTES:
page = await browser.newPage()
await page.goto(PAGE_URL + route)
content = await page.content()
await save(content, OUTPUT_PATH / (name + '.html'))
print(f'Page {name} generated.')
await page.close()
await asyncio.sleep(0.1)
await browser.close()
对预渲染的阅读计数器进行优化
渲染问题解决了,但是新的问题出现了——渲染的时候会请求博客正文,而这会将博文的阅读数计数器 +1,而这并不是真正的阅读数。理想来说,我希望这种请求不被计入计数器。
解决方法也很简单,我们可以自定义预渲染时无头浏览器的 User-Agent,将其标注为预渲染器,并在服务器的代码中进行适配即可。这部分就略过了。
部署
将预编译后的一堆 HTML 打包并传输到服务器上的静态文件目录,进行部署。
Nginx 设置
因为我们希望对 SEO 进行优化而不影响用户的体验,因此可以这样写,在检测到是爬虫或 bot 时返回预渲染网页:
map $http_user_agent $html_root {
default /path/to/html/;
~*baiduspider|googlebot|spider|bot /path/to/prerender/;
}
location / {
gzip_static on;
add_header Cache-Control no-cache;
expires 0;
root $html_root;
try_files $uri $uri.html $uri/ /index.html =404;
}
测试验证
我们可以对预渲染的网页进行验证:
curl https://gwy15.com | less # 只返回空白 html 和一堆 js
curl https://gwy15.com -A "Baiduspider" | less # 带有正文内容的 html
下一步(咕咕咕)
自动渲染、自动部署
完成手动预渲染后,可以部署到服务器上,在新发布博文时自动进行预渲染并发布到静态文件路径。