146

我一直在研究如何根据谷歌的说明让谷歌可以抓取 SPA 。尽管有相当多的一般性解释,但我在任何地方都找不到更详尽的分步教程和实际示例。完成此操作后,我想分享我的解决方案,以便其他人也可以使用它并可能进一步改进它。
我使用MVC控制器Webapi,服务器端使用Phantomjs ,客户端启用Durandal ;push-state我还使用Breezejs进行客户端-服务器数据交互,我强烈推荐所有这些,但我会尝试给出一个足够通用的解释,这也将有助于使用其他平台的人们。

4

7 回答 7

121

在开始之前,请确保您了解 google的要求,尤其是漂亮丑陋的URL 的使用。现在让我们看看实现:

客户端

在客户端,您只有一个 html 页面,它通过 AJAX 调用与服务器动态交互。这就是SPA的意义所在。客户端中的所有a标签都是在我的应用程序中动态创建的,稍后我们将看到如何使这些链接对服务器中的 google 机器人可见。每个这样的a标签都需要能够pretty URLhref标签中有一个,以便谷歌的机器人可以抓取它。您不希望在href客户端单击时使用该部分(即使您确实希望服务器能够解析它,我们稍后会看到),因为我们可能不希望加载新页面,只是为了进行 AJAX 调用,获取一些要在页面的一部分中显示的数据并通过 javascript 更改 URL(例如使用 HTML5pushstate或 with Durandaljs)。所以,我们都有一个hrefgoogle 的属性以及onclick当用户点击链接时哪个工作。现在,由于我在 URL 上使用了push-stateI don't want any #,所以典型的a标签可能如下所示:
<a href="http://www.xyz.com/#!/category/subCategory/product111" onClick="loadProduct('category','subCategory','product111')>see product111...</a>

“category”和“subCategory”可能是其他短语,例如“communication”和“phones”或“computers”和电器商店的“笔记本电脑”。显然会有许多不同的类别和子类别。如您所见,链接直接指向类别、子类别和产品,而不是特定“商店”页面的额外参数,例如http://www.xyz.com/store/category/subCategory/product111. 这是因为我更喜欢更短更简单的链接。这意味着我不会有一个与我的“页面”同名的类别,即“
如何通过AJAX(部分)加载数据我就不说了onclick,在google上搜索一下,有很多很好的解释。这里我要提到的唯一重要的事情是,当用户单击此链接时,我希望浏览器中的 URL 看起来像这样:
http://www.xyz.com/category/subCategory/product111. 这是没有发送到服务器的 URL!请记住,这是一个 SPA,客户端和服务器之间的所有交互都是通过 AJAX 完成的,根本没有链接!所有“页面”都在客户端实现,并且不同的 URL 不会调用服务器(服务器确实需要知道如何处理这些 URL,以防它们被用作从另一个站点到您的站点的外部链接,我们稍后会在服务器端部分看到这一点)。现在,这被杜兰达尔处理得很好。我强烈推荐它,但如果您更喜欢其他技术,也可以跳过这部分。如果您确实选择了它,并且您也像我一样使用 MS Visual Studio Express 2012 for Web,您可以安装Durandal Starter Kit,然后在其中shell.js使用如下内容:

define(['plugins/router', 'durandal/app'], function (router, app) {
    return {
        router: router,
        activate: function () {
            router.map([
                { route: '', title: 'Store', moduleId: 'viewmodels/store', nav: true },
                { route: 'about', moduleId: 'viewmodels/about', nav: true }
            ])
                .buildNavigationModel()
                .mapUnknownRoutes(function (instruction) {
                    instruction.config.moduleId = 'viewmodels/store';
                    instruction.fragment = instruction.fragment.replace("!/", ""); // for pretty-URLs, '#' already removed because of push-state, only ! remains
                    return instruction;
                });
            return router.activate({ pushState: true });
        }
    };
});

这里有一些重要的事情需要注意:

  1. 第一个路由(带有route:'')用于其中没有额外数据的 URL,即http://www.xyz.com. 在此页面中,您使用 AJAX 加载一般数据。这个页面实际上可能根本没有a标签。您需要添加以下标签,以便 google 的机器人知道如何处理它:
    <meta name="fragment" content="!">. 这个标签将使 google 的 bot 转换www.xyz.com?_escaped_fragment_=我们稍后会看到的 URL。
  2. 'about' 路由只是一个链接到您可能希望在您的 Web 应用程序上使用的其他“页面”的示例。
  3. 现在,棘手的部分是没有“类别”路线,并且可能有许多不同的类别 - 没有一个具有预定义的路线。这就是mapUnknownRoutes进来的地方。它将这些未知路线映射到“商店”路线,并删除任何“!” pretty URL如果它是由谷歌的搜索引擎生成的,则来自 URL 。'store' 路由获取 'fragment' 属性中的信息并进行 AJAX 调用以获取数据、显示数据并在本地更改 URL。在我的应用程序中,我不会为每个此类调用加载不同的页面;我只更改与此数据相关的页面部分,并在本地更改 URL。
  4. 注意pushState:true指示 Durandal 使用推送状态 URL。

这就是我们在客户端所需要的。它也可以使用散列 URL 实现(在 Durandal 中,您只需删除pushState:true它)。更复杂的部分(至少对我来说......)是服务器部分:

服务器端

MVC 4.5在服务器端使用WebAPI控制器。服务器实际上需要处理 3 种类型的 URL:由 google 生成的 URL - 两者pretty以及ugly与客户端浏览器中显示的格式相同的“简单”URL。让我们看看如何做到这一点:

漂亮的 URL 和“简单”的 URL 首先被服务器解释为试图引用一个不存在的控制器。服务器看到类似的东西http://www.xyz.com/category/subCategory/product111并寻找一个名为“类别”的控制器。因此,在web.config我添加以下行以将它们重定向到特定的错误处理控制器:

<customErrors mode="On" defaultRedirect="Error">
    <error statusCode="404" redirect="Error" />
</customErrors><br/>

现在,这会将 URL 转换为以下内容:http://www.xyz.com/Error?aspxerrorpath=/category/subCategory/product111。我希望将 URL 发送到将通过 AJAX 加载数据的客户端,所以这里的技巧是调用默认的“索引”控制器,就好像不引用任何控制器一样;我通过在所有 'category' 和 'subCategory' 参数之前向 URL添加一个哈希来做到这一点;散列 URL 不需要任何特殊控制器,除了默认的“索引”控制器,数据被发送到客户端,然后删除散列并使用散列后的信息通过 AJAX 加载数据。这是错误处理程序控制器代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Web.Http;

using System.Web.Routing;

namespace eShop.Controllers
{
    public class ErrorController : ApiController
    {
        [HttpGet, HttpPost, HttpPut, HttpDelete, HttpHead, HttpOptions, AcceptVerbs("PATCH"), AllowAnonymous]
        public HttpResponseMessage Handle404()
        {
            string [] parts = Request.RequestUri.OriginalString.Split(new[] { '?' }, StringSplitOptions.RemoveEmptyEntries);
            string parameters = parts[ 1 ].Replace("aspxerrorpath=","");
            var response = Request.CreateResponse(HttpStatusCode.Redirect);
            response.Headers.Location = new Uri(parts[0].Replace("Error","") + string.Format("#{0}", parameters));
            return response;
        }
    }
}


但是丑陋的 URLs呢?这些是由谷歌的机器人创建的,应该返回包含用户在浏览器中看到的所有数据的纯 HTML。为此,我使用phantomjs。Phantom 是一个无头浏览器,做浏览器在客户端所做的事情——但在服务器端。换句话说,Phantom 知道(除其他外)如何通过 URL 获取网页,解析它,包括运行其中的所有 javascript 代码(以及通过 AJAX 调用获取数据),并将反映的 HTML 返回给您DOM。如果您使用的是 MS Visual Studio Express,您可能希望通过此链接安装 phantom 。
但首先,当一个丑陋的 URL 被发送到服务器时,我们必须捕获它;为此,我在“App_start”文件夹中添加了以下文件:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Web;
using System.Web.Mvc;
using System.Web.Routing;

namespace eShop.App_Start
{
    public class AjaxCrawlableAttribute : ActionFilterAttribute
    {
        private const string Fragment = "_escaped_fragment_";

        public override void OnActionExecuting(ActionExecutingContext filterContext)
        {
            var request = filterContext.RequestContext.HttpContext.Request;

            if (request.QueryString[Fragment] != null)
            {

                var url = request.Url.ToString().Replace("?_escaped_fragment_=", "#");

                filterContext.Result = new RedirectToRouteResult(
                    new RouteValueDictionary { { "controller", "HtmlSnapshot" }, { "action", "returnHTML" }, { "url", url } });
            }
            return;
        }
    }
}

这也是在“App_start”中从“filterConfig.cs”调用的:

using System.Web.Mvc;
using eShop.App_Start;

namespace eShop
{
    public class FilterConfig
    {
        public static void RegisterGlobalFilters(GlobalFilterCollection filters)
        {
            filters.Add(new HandleErrorAttribute());
            filters.Add(new AjaxCrawlableAttribute());
        }
    }
}

如您所见,“AjaxCrawlableAttribute”将丑陋的 URL 路由到名为“HtmlSnapshot”的控制器,这是这个控制器:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Web;
using System.Web.Mvc;

namespace eShop.Controllers
{
    public class HtmlSnapshotController : Controller
    {
        public ActionResult returnHTML(string url)
        {
            string appRoot = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory);

            var startInfo = new ProcessStartInfo
            {
                Arguments = String.Format("{0} {1}", Path.Combine(appRoot, "seo\\createSnapshot.js"), url),
                FileName = Path.Combine(appRoot, "bin\\phantomjs.exe"),
                UseShellExecute = false,
                CreateNoWindow = true,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                RedirectStandardInput = true,
                StandardOutputEncoding = System.Text.Encoding.UTF8
            };
            var p = new Process();
            p.StartInfo = startInfo;
            p.Start();
            string output = p.StandardOutput.ReadToEnd();
            p.WaitForExit();
            ViewData["result"] = output;
            return View();
        }

    }
}

关联view的很简单,就一行代码:
@Html.Raw( ViewBag.result )
正如你在控制器中看到的那样,phantom 加载了一个名为 .js的文件createSnapshot.js夹下的 javascript 文件seo。这是这个 javascript 文件:

var page = require('webpage').create();
var system = require('system');

var lastReceived = new Date().getTime();
var requestCount = 0;
var responseCount = 0;
var requestIds = [];
var startTime = new Date().getTime();

page.onResourceReceived = function (response) {
    if (requestIds.indexOf(response.id) !== -1) {
        lastReceived = new Date().getTime();
        responseCount++;
        requestIds[requestIds.indexOf(response.id)] = null;
    }
};
page.onResourceRequested = function (request) {
    if (requestIds.indexOf(request.id) === -1) {
        requestIds.push(request.id);
        requestCount++;
    }
};

function checkLoaded() {
    return page.evaluate(function () {
        return document.all["compositionComplete"];
    }) != null;
}
// Open the page
page.open(system.args[1], function () { });

var checkComplete = function () {
    // We don't allow it to take longer than 5 seconds but
    // don't return until all requests are finished
    if ((new Date().getTime() - lastReceived > 300 && requestCount === responseCount) || new Date().getTime() - startTime > 10000 || checkLoaded()) {
        clearInterval(checkCompleteInterval);
        var result = page.content;
        //result = result.substring(0, 10000);
        console.log(result);
        //console.log(results);
        phantom.exit();
    }
}
// Let us check to see if the page is finished rendering
var checkCompleteInterval = setInterval(checkComplete, 300);

我首先要感谢Thomas Davis提供了我从中获得基本代码的页面:-)。
你会注意到这里有一些奇怪的地方:phantom 不断地重新加载页面,直到checkLoaded()函数返回 true。这是为什么?这是因为我的特定 SPA 进行了多次 AJAX 调用以获取所有数据并将其放置在我页面上的 DOM 中,并且在将 DOM 的 HTML 反射返回给我之前,幻影无法知道所有调用何时完成。我在这里所做的是在最后的 AJAX 调用之后添加一个<span id='compositionComplete'></span>,这样如果这个标签存在,我就知道 DOM 已经完成。我这样做是为了响应 Durandal 的compositionComplete事件,请参见此处更多。如果这在 10 秒内没有发生,我放弃(最多只需要一秒钟)。返回的 HTML 包含用户在浏览器中看到的所有链接。该脚本将无法正常工作,因为<script>HTML 快照中确实存在的标签未引用正确的 URL。这也可以在 javascript 幻像文件中进行更改,但我不认为这是必要的,因为 HTML snapshort 仅由 google 用于获取a链接而不是运行 javascript;这些链接确实引用了一个漂亮的 URL,如果事实上,如果您尝试在浏览器中查看 HTML 快照,您将收到 javascript 错误,但所有链接都将正常工作,并再次使用漂亮的 URL 将您定向到服务器获得完整的工作页面。
就是这个。现在服务器知道如何处理漂亮和丑陋的 URL,在服务器和客户端都启用了推送状态。所有丑陋的 URL 都使用 phantom 以相同的方式处理,因此无需为每种类型的调用创建单独的控制器。
您可能希望更改的一件事是不要进行一般的“类别/子类别/产品”调用,而是添加一个“商店”,以便链接看起来像:http://www.xyz.com/store/category/subCategory/product111. 这将避免我的解决方案中的问题,即所有无效的 URL 都被视为实际上是对“索引”控制器的调用,我想这些可以在“商店”控制器中处理,而不需要添加web.config上面显示的内容.

于 2013-08-30T10:05:17.537 回答
33

Google 现在能够呈现 SPA 页面: 弃用我们的 AJAX 抓取方案

于 2016-03-05T14:14:28.817 回答
4

这是我 8 月 14 日在伦敦主持的 Ember.js 培训课程的截屏视频链接。它为您的客户端应用程序和服务器端应用程序概述了一种策略,并现场演示了如何实现这些功能将为您的 JavaScript 单页应用程序提供优雅的降级,即使对于关闭 JavaScript 的用户也是如此.

它使用 PhantomJS 来帮助抓取您的网站。

简而言之,所需的步骤是:

  • 拥有您要抓取的 Web 应用程序的托管版本,该站点需要拥有您在生产中拥有的所有数据
  • 编写一个 JavaScript 应用程序(PhantomJS 脚本)来加载您的网站
  • 将 index.html(或“/”)添加到要抓取的 URL 列表中
    • 弹出添加到爬虫列表的第一个 URL
    • 加载页面并渲染其 DOM
    • 在加载的页面上查找链接到您自己网站的任何链接(URL 过滤)
    • 将此链接添加到“可抓取”网址列表(如果尚未抓取)
    • 将渲染的 DOM 存储到文件系统上的文件中,但首先剥离所有脚本标签
    • 最后,使用抓取的 URL 创建一个 Sitemap.xml 文件

完成此步骤后,由您的后端将 HTML 的静态版本作为该页面上 noscript-tag 的一部分提供。这将允许 Google 和其他搜索引擎抓取您网站上的每个页面,即使您的应用最初是单页应用。

链接到包含完整详细信息的截屏视频:

http://www.devcasts.io/p/spas-phantomjs-and-seo/#

于 2014-08-16T14:50:24.730 回答
2

我曾经在客户端和 AngularRendertron中解决 SEO 问题ASP.net core,它是一个中间件,可以根据爬虫或客户端来区分请求,因此当请求来自爬虫端时,会快速快速地生成响应。

  • 为普通客户呈现的网站:图像1

  • 为 Crawlers 渲染的站点:图2

Startup.cs

配置 rendertron 服务:

public void ConfigureServices(IServiceCollection services)
{
    // Add rendertron services
    services.AddRendertron(options =>
    {
        // rendertron service url
        options.RendertronUrl = "http://rendertron:3000/render/";

        // proxy url for application
        options.AppProxyUrl = "http://webapplication";

        // prerender for firefox
        //options.UserAgents.Add("firefox");

        // inject shady dom
        options.InjectShadyDom = true;
        
        // use http compression
        options.AcceptCompression = true;
    });
}

确实,这种方法有点不同,需要很短的代码来生成特定于爬虫的内容,但它对于 CMS 或门户网站等小型项目很有用。

这种方法可以在大多数编程语言或服务器端框架中完成,例如ASP.net core, Python (Django), Express.js, Firebase.

要查看源代码和更多详细信息:https ://github.com/GoogleChrome/rendertron

于 2021-12-05T09:28:20.270 回答
1

2021 年更新

  • SPA 应该使用History API以便对 SEO 友好。

    SPA 页面之间的转换通常通过history.pushState(path)调用来实现。接下来发生的事情取决于框架。在使用 React 的情况下,一个名为 React Router 的组件会监视history并显示/呈现为所path使用的配置的 React 组件。

  • 为一个简单的 SPA 实现 SEO 很简单

  • 如文章所示,为更高级的 SPA(使用选择性预渲染以获得更好的性能)实现 SEO 涉及更多。我是作者。

于 2021-12-15T07:36:08.157 回答
0

您可以使用或创建自己的服务来使用名为 prerender 的服务来预渲染您的 SPA。你可以在他的网站prerender.io和他的github 项目上查看它(它使用 PhantomJS 并为你渲染你的网站)。

这很容易开始。您只需要将爬虫请求重定向到服务,它们就会收到呈现的 html。

于 2016-01-07T10:36:51.323 回答
-1

您可以使用http://sparender.com/来正确抓取单页应用程序。

于 2017-10-26T18:47:22.950 回答