我一直在研究如何根据Google的说明使SPA可被Google抓取。即使有很多一般性的解释,我也找不到任何包含实际示例的更详尽的分步教程。完成此操作后,我想分享我的解决方案,以便其他人也可以使用它,并可能进一步改进它。
 
我使用MVC与Webapi控制器和Phantomjs在服务器端,并迪朗达尔与客户端push-state启用; 我还使用Breezejs进行客户端-服务器数据交互,我强烈建议所有这些操作,但是我将尝试给出足够概括的解释,这也将有助于使用其他平台的人们。
我一直在研究如何根据Google的说明使SPA可被Google抓取。即使有很多一般性的解释,我也找不到任何包含实际示例的更详尽的分步教程。完成此操作后,我想分享我的解决方案,以便其他人也可以使用它,并可能进一步改进它。
 
我使用MVC与Webapi控制器和Phantomjs在服务器端,并迪朗达尔与客户端push-state启用; 我还使用Breezejs进行客户端-服务器数据交互,我强烈建议所有这些操作,但是我将尝试给出足够概括的解释,这也将有助于使用其他平台的人们。
Answers:
在开始之前,请确保您了解什么谷歌需要,尤其是使用的漂亮与丑陋的URL。现在让我们看一下实现:
在客户端,您只有一个HTML页面,该页面通过AJAX调用与服务器动态交互。这就是SPA的目的。a客户端中的所有标记都是在我的应用程序中动态创建的,稍后我们将看到如何使这些链接对服务器中的Google bot可见。每个此类a标签都必须能够pretty URL在href标签中包含,以便Google的漫游器抓取它。您不希望该href部分在客户端单击时使用(即使您确实希望服务器能够对其进行解析,但我们稍后会看到),因为我们可能不希望加载新页面,只是进行AJAX调用,以使某些数据显示在页面的一部分中,并通过javascript(例如,使用HTML5 pushstate或Durandaljs)更改URL 。因此,我们都有hrefgoogle的属性,以及onclick当用户单击链接时执行的工作。现在,由于使用了URL,因此push-state不需要任何#URL,因此典型的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,以防它们用作从另一个站点到您站点的外部链接,我们稍后会在服务器端看到)。现在,杜兰达尔(Durandal)很好地处理了这一问题。我强烈建议您这样做,但是如果您喜欢其他技术,也可以跳过此部分。如果您选择了它,并且还像我一样使用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 });
        }
    };
});
这里有一些重要的注意事项:
route:'')用于其中没有多余数据的URL,即http://www.xyz.com。在此页面中,您将使用AJAX加载常规数据。a该页面实际上可能根本没有标签。您将需要添加以下标记,以便Google的漫游器知道如何处理它:<meta name="fragment" content="!">。此标记将使Google的漫游器将URL转换为www.xyz.com?_escaped_fragment_=以后将看到的URL 。mapUnknownRoutes来源。它将这些未知路由映射到“商店”路由,并删除所有“!” 如果pretty URL是Google的搜索引擎生成的,则从URL中获取。“存储”路由在“ fragment”属性中获取信息,并进行AJAX调用以获取数据,显示数据并在本地更改URL。在我的应用程序中,我不会为每个此类调用加载不同的页面。我只更改页面中与该数据相关的部分,并在本地更改URL。pushState:true该指令指示Durandal使用推送状态URL。  这就是我们在客户端需要的。它也可以使用哈希URL来实现(在Durandal中,您可以简单地删除pushState:true它)。更复杂的部分(至少对我来说...)是服务器部分:
我MVC 4.5在服务器端使用WebAPI控制器。服务器实际上需要处理3种类型的URL:由google生成的URL — pretty以及由URL生成ugly的“简单” URL,其格式与客户端浏览器中显示的URL相同。让我们来看看如何做到这一点:
漂亮的URL和“简单”的URL首先由服务器解释,就好像试图引用不存在的控制器一样。服务器看到类似的内容,http://www.xyz.com/category/subCategory/product111并寻找一个名为“ category”的控制器。因此,在其中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加载数据的客户端,因此这里的技巧是调用默认的“索引”控制器,就好像未引用任何控制器一样。我通过在所有“类别”和“ subCategory”参数之前在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;
        }
    }
}
但是丑陋的网址呢?这些是由Google的漫游器创建的,应返回纯HTML,其中包含用户在浏览器中看到的所有数据。为此,我使用phantomjs。Phantom是一种无头浏览器,它在客户端-但在服务器端执行浏览器的工作。换句话说,phantom知道(其中包括)如何通过URL获取网页,解析包括运行其中的所有javascript代码(以及通过AJAX调用获取数据)以及如何返回反映DOM。如果您使用的是MS Visual Studio Express,则许多人都希望通过此链接安装幻像。  
但是首先,当一个丑陋的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 ) 
正如您在控制器中看到的那样,幻影会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中,而phantom在返回DOM的HTML反射之前无法知道所有调用何时完成。我在这里做的是在最后一个AJAX调用之后添加了<span id='compositionComplete'></span>,这样,如果存在此标记,我就知道DOM已完成。我这样做是为了响应杜兰达尔的compositionComplete活动,请参见此处更多。如果在10秒钟内仍没有发生这种情况,我会放弃(最多应该花一秒钟)。返回的HTML包含用户在浏览器中看到的所有链接。该脚本将无法正常运行,因为<script>HTML快照中确实存在的标记未引用正确的URL。这也可以在javascript幻象文件中更改,但是我认为这不是必需的,因为HTML snapshort仅由Google用于获取a链接而不是运行javascript;这些链接确实引用了漂亮的URL,如果实际上,如果您尝试在浏览器中查看HTML快照,则会收到JavaScript错误,但所有链接都将正常工作,并再次使用漂亮的URL将您定向到服务器获取完整的工作页面。
就是这个。现在,服务器知道如何处理漂亮的URL和丑陋的URL,并且在服务器和客户端上都启用了推状态。使用幻像对所有丑陋的URL进行相同的处理,因此无需为每种呼叫类型创建单独的控制器。
您可能希望更改的一件事不是发出常规的“ category / subCategory / product”调用,而是添加“ store”,以便链接看起来像:http://www.xyz.com/store/category/subCategory/product111。这样可以避免我的解决方案出现的问题,即所有无效的URL都将被视为实际上是对“索引”控制器的调用,并且我认为这些URL可以在“存储”控制器中进行处理,而无需添加web.config上面显示的内容。 。
Google现在可以呈现SPA页面: 不推荐使用我们的AJAX抓取方案
这是我8月14日在伦敦举办的Ember.js培训班的截屏录像的链接。它概述了客户端应用程序和服务器端应用程序的策略,并现场演示了如何实现这些功能,即使关闭了JavaScript的用户,也能为您的JavaScript单页应用程序带来优美的降级效果。
它使用PhantomJS辅助爬网您的网站。
简而言之,所需的步骤是:
完成此步骤后,由您的后端决定是否将HTML的静态版本作为该页面上noscript-tag的一部分提供。即使您的应用程序最初是单页应用程序,这也将允许Google和其他搜索引擎抓取您网站上的每个页面。
链接到截屏,具有完整的详细信息:
您可以使用或创建自己的服务来通过称为prerender的服务来预渲染SPA。您可以在他的网站prerender.io和他的github项目(使用PhantomJS并为您呈现网站)中进行检查。
开始很容易。您只需要将搜寻器请求重定向到服务,它们就会收到呈现的html。
您可以使用http://sparender.com/来正确爬网单页应用程序。