SPA网站SEO完美解决方案

作者: 北方蜘蛛 | 来源:发表于2017-02-24 21:09 被阅读5799次

郑重声明,此文只是提供了一个大概思路,程序代码还需要优化,思路大概正确,但是应用生产环境需要小心。

顺便推荐下自己广告业余项目,一个码农专用的搜索引擎
(https://www.1024ma.com/so/juejin?keyword=deno)

单页面SEO一直是让人比较头疼的问题,为了解决这个问题在网上搜到了大概几种方式,最终受到启发得出一个比较优秀的方案。

先说说网上的办法,有的是为了兼容谷歌,用的是#!的方式来给搜索引擎抓取,还有提交sitemap种种麻烦。

还有一种比较蛋疼的是在开发一个服务端渲染页面的应用,根据爬虫UA让nginx代理到后端渲染页面的服务器,这种似乎可以解决问题,然而比较蛋疼的你需要维护两套系统,给开发和维护都增加额外的工作非常这不建议。

下面是我个人认为最优的方案,简单来说,需要借助phantomjs,好像被这个问题困扰的码农似乎早就知道这个东西,不过我的用法与他们略有不同。当爬虫抓取页面,那我们就把他带到phantomjs渲染好的静态html,注意,这个渲染的方法是抓取原来SPA页面的代码并运行JS,生成一个与SPA一模一样的网站,并且url(要使用这种模式 html5 history api)保持与spa完全一致。是的就这么简单就解决了,代价是与需要腾出来一个服务器搭建一个nginx作为web服务器。

随着蜘蛛爬取的次数越来越多,随之产生的静态文件也越来多,不要怕,你只要硬盘足够大就可以了,弱这是你的网站文件已经达到已经的数量级,那时我想搜索引擎已经有了原生的解决方案,我想这不会等太久。当然纯的静态文件有一个弊端,就是网站改版之后会产生页面与SPA不一致的现象,从而可能被搜索降级,好在解决这个问题非常的简单,那就是每一个更新结点,我们写个任务定时更新所有的静态页面,就算你页面很多也没什么问题,跑个几个小时也就差不多了,毕竟网站的大规模改版并不是很频繁的事情。如果想要完全避免这个问题,也是有办法的,那就是从爬虫开始爬的时候使用phantomjs每一次都重新生成页面,也就解决了不一致的问题。然而虽然是解决了这个问题,又带来了另一个问题,从新生成页面是相对耗时的,访问速度下降,对seo多少产生了一点影响。所以还是推荐手动先生成的办法。

另外关于已经收录的网页,当用户从搜索引擎点过来的时候,如果是spa页面的话,ajax重新加载大量JS逻辑代码,并执行spa路由调取相应的页面,也是相对耗时的。此时我们可以通过入口的nginx转发到phantomjs的服务器去读那些已经生成好的html,那速度就快很多了。

到此这个问题就真的得到解决了。前端开发小伙伴就可以无痛愉快的开发写写单页面应用了。

流程图

流程图

入口机器Nginx

server {
        listen       80;
        server_name  www.abc.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;
            #web server
           #当UA里面含有Baiduspider的时候,流量Nginx以反向代理的形式,将流量传递给spider_server
            if ($http_user_agent ~* "Baidu") {
                   proxy_pass http://www.seo.com:82;
             }   
           proxy_pass http://www.aba.com.8:8080;  
        }
    }

web nginx

server {
        listen       8080;
        server_name  www.abc.com ;
        location / {
            root  E:\web;
            index  index.html index.htm;
            try_files $uri $uri/ /index.html;
                
        }
     location /api {
            proxy_pass http://192.1681.9:1335;  
        }
    }

SEO nginx

listen       82;
        server_name  www.seo.com ;
        location / {
           #先访问自己的静态页面如果有直接显示
            root  E:\seo;
            index  index.html index.htm;
            try_files  $uri $uri/index.html $uri.html @mongrel;
        }
        #如果没有文件就把url发给node
        location @mongrel{
             proxy_pass    http://192.1681.9:3000;
        }
    }

node express


// ExpressJS调用方式
var express = require('express');
var app = express();
var fs = require('fs');
var path = require("path");
var mkdirsSync = function(dirname) {
    //console.log(dirname);
    if (fs.existsSync(dirname)) {
        return true;
    } else {
        if (mkdirsSync(path.dirname(dirname))) {
            fs.mkdirSync(dirname);
            return true;
        }
    }
};
// 引入NodeJS的子进程模块
var child_process = require('child_process');
app.get('*', function(req, res) {
    // 完整web服务器URL
    var url = req.protocol + '://' + '192.168.1.8:8080' + req.originalUrl;
    //console.log(req.originalUrl);
    var filePath = "";
    var fileName = "";
    if (req.originalUrl == '/') {
        fileName = 'index';
        filePath = "e:/seo/";
    } else {
        var pathArray = req.originalUrl.split('/');
        fileName = pathArray[pathArray.length - 1];
        filePath = "e:/seo/" + req.originalUrl.replace(fileName, "") + '/';
    }
    console.log(filePath + "....");
    // 预渲染后的页面字符串容器
    var content = '';
    // 开启一个phantomjs子进程
    //应当先蒋策seo服务器是否存在改文件如果有直接跳转
    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:
                var w_data = content;
                var w_data = new Buffer(w_data);
                /**
                 * filename, 必选参数,文件名
                 * data, 写入的数据,可以字符或一个Buffer对象
                 * [options],flag,mode(权限),encoding
                 * callback 读取文件后的回调函数,参数默认第一个err,第二个data 数据
                 */
                mkdirsSync(filePath, 0777);
                console.log(filePath + fileName + '.html');
                fs.writeFile(filePath + fileName + '.html', w_data, { flag: 'w' }, function(err) {
                    if (err) {
                        console.error(err);
                    } else {
                        console.log('写入成功');
                    }
                });
                res.send(content);
                break;
        }
    });
});
app.listen(3000);

phantomjs

/*global phantom*/
"use strict";

// 单个资源等待时间,避免资源加载后还需要加载其他资源
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);

    }

});

本机测试本机测试通过 可以使用postman等其他工具模拟爬虫

最近更新

最近发现本文还是有一定的浏览量,不过我们自己的项目还是使用一种更为简单的办法,就是启动另一个express应用,使用axios读取后端api,将页面从服务端渲染出来,然后利用nginx 判断user-agent 是否包含spider字符串,来转发到相同路径的 express应用,经过几个月的沉淀,从百度收录来看大概提高了2倍的量。这样做的好处是,可以在express应用的模板中简化html代码量,不会太多spa中过多的冗余标签,属性,使代码更简洁,对seo更友好,也有一定的劣势就是访问速度上可能会比真正的静态html慢一点点,如果缓存做的好,这点速度可以忽略。

相关文章

  • SPA网站SEO完美解决方案

    郑重声明,此文只是提供了一个大概思路,程序代码还需要优化,思路大概正确,但是应用生产环境需要小心。 顺便推荐下自己...

  • [webpack]多页面打包工具

    根据公司需求,要做对SEO(Secrch enginner Optimization)友好的网站,SPA(Sing...

  • 单页面应用SEO方案

    最近希望在现有SPA网站的基础上,对网站进行SEO优化,所以列出了几种方案。 一、Rendertron Rende...

  • vue-cli3单页面项目SEO优化

    vue对SEO很不友好,现在网上已经给出了四种解决方案,主要说预渲染prerender-spa-plugin咋用,...

  • Nuxt 开发搭建博客

    众所周知,Vue SPA单页面应用对SEO不友好,当然也有相应的解决方案。 服务端渲染 (SSR) 就是常用的一种...

  • 网站优化选择关键词做百度排名的四大秘诀

    在网站建设的初期,我们不仅需要网站搭建环境、网站页面设计、网站布局完美,同时网站的内容、以及SEO优化都是必不可少...

  • vue 面试题

    介绍SPA 以及SPA 的缺点 spa 是单页面应用. 缺点: seo(讲究多页面,动态修改关键字) 优化不好.性...

  • 服务端渲染和客户端渲染区别?

    首先,介绍一下 SPA、SEO、SSR 三者的区别 SPA(single page application) 单页...

  • Vue-cli使用prerender-spa-plugin插件预

    前言:使用vue-cli打包项目一般为spa项目,众所周知单页面应用不利于SEO,有ssr和预渲染两种解决方案,这...

  • SPA、SEO、SSR

    1、SPA—单页面应用(single page application) SPA就是只有一张Web页面的应用。单页...

网友评论

  • ee4a255c13bf:如果内容更新比较快,要经常生成文件,这个有点不太现实
    北方蜘蛛:@ee4a255c13bf 不显示等着百度支持 spa吧,或者放弃spa 一样能很好的组织代码
  • 再见那卑微的再见:如果要spa就不要seo了啊,况且现在像NUXT和NEXT这类由SPA衍生的VUE、React SEO框架多得是,何必这么麻烦
    再见那卑微的再见:@北方蜘蛛 nuxt vue官方推荐的啊
    北方蜘蛛:@再见那卑微的再见 主要那时候还没出现,而且,nuxt 那种项目垃圾代码成倍,并不利于seo
  • 心谭:您好,请问phantomjs在爬取js代码中含有Promise代码的时候怎么处理?

    我的报错:Unhandled promise rejection ReferenceError: Can't find variable: Promise

    配置了polyfill(依然不行),可能是我配错了。
    北方蜘蛛:@godbmw 你是说你在写代码的时候,使用了promise 是吗? 目前这个方案我觉得可能不太可合适了,可以研究下 nuxt
  • goCan6:看了代码,你貌似没有将多个请求产生的phantom子进程进行缓存,所以seo一来,所有页面会同时开启子进程,把seo层全部放开到node去做,去掉ngnix的逻辑,估计这个问题好处理一点
    北方蜘蛛:@猫五号 可能是多了一步,nginx 这个一步主要是想让搜索引擎抓那些已经生成好的页面,不通过nodejs去找,文章写的很老了,感觉不是很实用,一直没时间整理,可能点击高了就被收录的比较好,我最后还是单独搭建了一个express做了手机版网页去让爬虫去爬
  • 67fd803a6726:直接把公司服务器搞挂了。。。还写了检讨,这个东西太吃硬件了,百度一秒请求三次, 不一会儿 cpu100% 内存100% 。。。
    67fd803a6726:@北方蜘蛛 我们是做XXX的,全国各个城市db里的数据库里差不多16万条,每个页面都是独立的详情页,类似淘宝的商品详情,所以蜘蛛抓取的重复率很小,基本上都是第一次访问,再就是上架下架很频繁。。本来以为蜘蛛爬取的频率很慢的,上次突然一下子一秒3-5个页面请求,服务器内存CPU直接爆了。。只能说自己太年轻了。。但是你这个方案真的挺好的,现在在研究chrome headless,如果这个也不能通过的话们只能nuxt.js单独做服务端渲染的方案了。。唉。。
    北方蜘蛛:你们其实客户跑一个任务,预先生成静态页面,这样百度就不会每次都访问node服务器了呀
    北方蜘蛛:抱歉了真狠抱歉这个东西非常不成熟,经常搞挂掉服务器,所以一定要要用心的服务器,然后,检测内存使用使用pm2 来重启项目,抱歉了
  • CallMeSoul:来个完整demo呗,最好一台服务器搞定
    CallMeSoul:@北方蜘蛛 好的,希望有更好的解决方案
    北方蜘蛛:一台服务器内存要够大,这个方案我现在倒是不太推荐了,我找时间优化下看看
  • 北方蜘蛛:这个方在window下面很容易内存不够,而且这个方案好像很重,但调试好,确实是可以解决全部页面的seo问题。
    远浅:保存下来的js里 有一段这个导致报错...<html><head></head><body><pre style="word-wrap: break-word; white-space: pre-wrap;">!
    北方蜘蛛:@冰凉泪 可能是服务器内存小,因为那个软件phantomjs 这个东西 linux也是可跑的,我测试的时候约到的一些问题,经常会挂掉,可能是把所有资源都处理了,如果是只是处理实事先定义好的,若干个页面或许没这么大压力吧
    不要打架:请问哪一块是需要windows支持的啊?还有为什么要在本地存成静态,搜索引擎爬虫带来的负担应该不大吧
  • iNiL0119:有demo吗
    北方蜘蛛:@brickspert 写的有点乱真的解决问题了吗?
    brickspert:谢谢~帮了我大忙
    北方蜘蛛:demo 就是你随意用一个 spa框架搭建一个程序,然后通过nginx配置好主机,都在window操作就好了

本文标题:SPA网站SEO完美解决方案

本文链接:https://www.haomeiwen.com/subject/aaawwttx.html