使用PhantomJS解决VUE的SEO方案

原理

通过在nginx里拦截判断ua的http_user_agent是否属于爬虫,如果不是,则正常返回页面,如果是则先通过PhantomJS渲染完整的HTML后再返回。

PS:只要两条路线返回的内容基本一致,就不会被搜索引擎处罚,认为是作弊的手段。

image.png

安装 PhantomJS

MAC本地:

brew update && brew cask install phantomjs

phantomjs -v 查看

2.1.1

编写脚本文件 spider.js

// spider.js
'use strict';

console.log('=====start=====');

// 单个资源等待时间,避免资源加载后还需要加载其他资源
var resourceWait = 500;
var resourceWaitTimer;

// 最大等待时间
var maxWait = 5000;
var maxWaitTimer;

// 资源计数
var resourceCount = 0;

// PhantomJS WebPage模块
var page = require('webpage').create();

// NodeJS 系统模块
var system = require('system');

// 从CLI中获取第二个参数为目标URL
var url = system.args[1];

// 设置PhantomJS视窗大小
page.viewportSize = {
	width: 1280,
	height: 1014,
};

// 获取镜像
var capture = function (errCode) {
	// 外部通过stdout获取页面内容
	console.log(page.content);

	// 清除计时器
	clearTimeout(maxWaitTimer);

	// 任务完成,正常退出
	phantom.exit(errCode);
};

// 资源请求并计数
page.onResourceRequested = function (req) {
	resourceCount++;
	clearTimeout(resourceWaitTimer);
};

// 资源加载完毕
page.onResourceReceived = function (res) {
	// chunk模式的HTTP回包,会多次触发resourceReceived事件,需要判断资源是否已经end
	if (res.stage !== 'end') {
		return;
	}

	resourceCount--;

	if (resourceCount === 0) {
		// 当页面中全部资源都加载完毕后,截取当前渲染出来的html
		// 由于onResourceReceived在资源加载完毕就立即被调用了,我们需要给一些时间让JS跑解析任务
		// 这里默认预留500毫秒
		resourceWaitTimer = setTimeout(capture, resourceWait);
	}
};

// 资源加载超时
page.onResourceTimeout = function (req) {
	resouceCount--;
};

// 资源加载失败
page.onResourceError = function (err) {
	resourceCount--;
};

// 打开页面
page.open(url, function (status) {
	if (status !== 'success') {
		phantom.exit(1);
	} else {
		// 当改页面的初始html返回成功后,开启定时器
		// 当到达最大时间(默认5秒)的时候,截取那一时刻渲染出来的html
		maxWaitTimer = setTimeout(function () {
			capture(2);
		}, maxWait);
	}
});

复制代码

本地测试百度

phantomjs spider.js 'www.baidu.com/'

返回HTML结构则说明成功,但确实访问速度明显降低

image.png

命令服务化

要做到响应搜索引擎爬虫的请求,我们需要将此命令服务化,通过node起个简单的web服务。

// server.js
// ExpressJS调用方式
var express = require('express');
var app = express();

// 引入NodeJS的子进程模块
var child_process = require('child_process');

app.get('*', function(req, res){

    // 完整URL
    var url = req.protocol + '://'+ req.hostname + req.originalUrl;

    // 预渲染后的页面字符串容器
    var content = '';

    // 开启一个phantomjs子进程
    var phantom = child_process.spawn('phantomjs', ['spider.js', url]);

    // 设置stdout字符编码
    phantom.stdout.setEncoding('utf8');

    // 监听phantomjs的stdout,并拼接起来
    phantom.stdout.on('data', function(data){
        content += data.toString();
    });

    // 监听子进程退出事件
    phantom.on('exit', function(code){
        switch (code){
            case 1:
                console.log('加载失败');
                res.send('加载失败');
                break;
            case 2:
                console.log('加载超时: '+ url);
                res.send(content);
                break;
            default:
                res.send(content);
                break;
        }
    });

});

app.listen(3000, function () {
  console.log('Spider app listening on port 3000!');
});
复制代码

运行node server.js,此时我们已经有了一个预渲染的web服务啦,接下来的工作便是将搜索引擎爬虫的请求转发到这个web服务,最终将渲染结果返回给爬虫。
为了防止node进程挂掉,可以使用nohup来启动,nohup node server.js &

通过Nginx配置,我们可以轻松的解决这个问题。

upstream spider_server {
  server localhost:3000;
}

server {
    listen       80;
    server_name  example.com;
    
    location / {
      proxy_set_header  Host            $host:$proxy_port;
      proxy_set_header  X-Real-IP       $remote_addr;
      proxy_set_header  X-Forwarded-For $proxy_add_x_forwarded_for;

      if ($http_user_agent ~* "Baiduspider|twitterbot|facebookexternalhit|rogerbot|linkedinbot|embedly|quora link preview|showyoubot|outbrain|pinterest|slackbot|vkShare|W3C_Validator|bingbot|Sosospider|Sogou Pic Spider|Googlebot|360Spider") {
        proxy_pass  http://spider_server;
      }
    }
}
复制代码

PhantomJS方案优缺点

优点:

  • 对VUE代码改动冲击小,甚至可以基本无改动,开发成本相对其它SEO方案最低
  • SEO方案可以抽离出来,供别的项目使用

缺点

  • 访问速度变慢
  • 部分ES6编译可能会出现问题,例如Set、Promise等
  • 高并发访问服务器压力大

参考

github.com/lengziyu/vu…

www.v2ex.com/t/302616

猜你喜欢

转载自juejin.im/post/7033661909756968973