我一直致力于如何根据谷歌的instructions谷歌进行SPA抓取。尽管有很多一般性的解释,但我找不到更详尽的逐步教程和实际示例。完成此操作后,我想分享我的解决方案,以便其他人也可以使用它,并可能进一步改进它。
我在MVC
控制器上使用Webapi
,在服务器端使用Phantomjs,在客户端使用Durandal启用push-state
;我还使用Breezejs进行客户端 - 服务器数据交互,我强烈推荐所有这些,但我会尝试给出足够的解释,以帮助人们使用其他平台。
答案 0 :(得分:122)
在开始之前,请确保您了解Google requires的内容,尤其是漂亮和丑陋网址的使用情况。现在让我们看一下实现:
在客户端,您只有一个html页面,它通过AJAX调用动态地与服务器交互。这就是SPA的意义所在。客户端中的所有a
标记都是在我的应用程序中动态创建的,我们稍后会看到如何在服务器中将这些链接显示给谷歌的僵尸程序。每个此类a
代码都需要在pretty URL
代码中包含href
,以便Google的机器人抓取它。当客户点击它时,您不希望使用href
部分(即使您确实希望服务器能够解析它,我们稍后会看到它),因为我们可能不希望加载新页面,只是进行AJAX调用以获得一些数据显示在页面的一部分并通过javascript更改URL(例如使用HTML5 pushstate
或Durandaljs
)。因此,我们同时拥有谷歌和href
的{{1}}属性,当用户点击链接时,该属性可以完成工作。现在,由于我使用onclick
,我不希望在网址上显示任何push-state
,因此典型的#
代码可能如下所示:
a
&#39;类别&#39;和&#39; subCategory&#39;可能是其他短语,例如&#39;沟通&#39;和&#39;手机&#39;或者&#39;计算机&#39;和笔记本电脑&#39;对于电器商店。显然会有许多不同的类别和子类别。如您所见,该链接直接指向类别,子类别和产品,而不是特定“商店”的额外参数。 <a href="http://www.xyz.com/#!/category/subCategory/product111" onClick="loadProduct('category','subCategory','product111')>see product111...</a>
等网页。这是因为我更喜欢更短更简单的链接。这意味着我不会出现与我的某个页面同名的类别,即&#39; about&#39;。
我不会介绍如何通过AJAX(http://www.xyz.com/store/category/subCategory/product111
部分)加载数据,在谷歌上搜索,有很多很好的解释。我想要提到的唯一重要的事情是,当用户点击此链接时,我希望浏览器中的URL看起来像这样:
onclick
。这是URL不发送到服务器!记住,这是一个SPA,客户端和服务器之间的所有交互都是通过AJAX完成的,根本没有链接!所有页面&#39;在客户端实现,并且不同的URL不调用服务器(服务器确实需要知道如何处理这些URL,以防它们被用作从另一个站点到您的站点的外部链接,我们&#39;稍后将在服务器端部分看到)。现在,Durandal精彩地处理了这个问题。我强烈推荐它,但如果您更喜欢其他技术,也可以跳过此部分。如果您选择它,并且您也像我一样使用MS Visual Studio Express 2012 for Web,则可以安装Durandal Starter Kit,并在http://www.xyz.com/category/subCategory/product111
中使用以下内容:< BR />
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:''
。在此页面中,您可以使用AJAX加载常规数据。此页面中可能根本没有http://www.xyz.com
个标签。您需要添加以下标记,以便Google的机器人知道如何处理它:a
。此标记会让Google的机器人将网址转换为我们稍后会看到的<meta name="fragment" content="!">
。www.xyz.com?_escaped_fragment_=
的用武之地。它将这些未知路线映射到&#39;商店&#39;路线并删除任何&#39;!&#39;如果它是由谷歌搜索引擎生成的mapUnknownRoutes
,则来自网址。该商店&#39;路由获取&#39;片段中的信息&#39;属性并进行AJAX调用以获取数据,显示数据并在本地更改URL。在我的申请中,我没有为每个这样的电话加载不同的页面;我只更改与此数据相关的页面部分,并在本地更改URL。pretty URL
指示Durandal使用推送状态URL。 这是我们在客户端需要的全部内容。它也可以使用散列URL实现(在Durandal中,您可以简单地删除pushState:true
)。更复杂的部分(至少对我而言......)是服务器部分:
我在服务器端使用pushState:true
MVC 4.5
个控制器。服务器实际上需要处理3种类型的URL:谷歌生成的URL - WebAPI
和pretty
以及简单的&#39;网址格式与客户端浏览器中显示的格式相同。让我们看看如何做到这一点:
漂亮的网址和简单的&#39;这些服务器首先被服务器解释为尝试引用不存在的控制器。服务器看到类似ugly
的内容,并查找名为&#39; category&#39;的控制器。因此,在http://www.xyz.com/category/subCategory/product111
中,我添加以下行以将这些行重定向到特定的错误处理控制器:
web.config
现在,这会将URL转换为:<customErrors mode="On" defaultRedirect="Error">
<error statusCode="404" redirect="Error" />
</customErrors><br/>
。我希望将URL发送到将通过AJAX加载数据的客户端,因此这里的诀窍是调用默认的&#39;索引&#39;控制器好像没有引用任何控制器;我通过在所有&#39;类别之前添加哈希到URL来做到这一点。和&#39; subCategory&#39;参数;散列URL不需要任何特殊控制器,除了默认的&#39;索引&#39;控制器和数据被发送到客户端,然后客户端删除哈希并使用哈希后的信息通过AJAX加载数据。这是错误处理程序控制器代码:
http://www.xyz.com/Error?aspxerrorpath=/category/subCategory/product111
但是 Ugly URL 呢?这些是由谷歌的机器人创建的,应返回包含用户在浏览器中看到的所有数据的纯HTML。为此我使用phantomjs。 Phantom是一个无头浏览器,可以在客户端执行浏览器操作 - 但在服务器端。换句话说,幻影知道(除其他事项外)如何通过URL获取网页,解析它包括运行其中的所有javascript代码(以及通过AJAX调用获取数据),并返回反映的HTML DOM。如果您正在使用MS Visual Studio Express,则许多人希望通过此link安装幻像。
但首先,当一个丑陋的URL被发送到服务器时,我们必须抓住它;为此,我添加了“App_start”&#39;文件夹中的以下文件:
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;
}
}
}
这是从&#39; filterConfig.cs&#39;也在&#39; App_start&#39;:
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;
}
}
}
正如您所看到的,&#39; AjaxCrawlableAttribute&#39;将丑陋的URL路由到名为&#39; HtmlSnapshot&#39;的控制器,这里是这个控制器:
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());
}
}
}
关联的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文件。这是这个javascript文件:
seo
我首先要感谢Thomas Davis获取基本代码的页面:-)。
你会发现一些奇怪的事情:幻像不断重新加载页面,直到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);
函数返回true。这是为什么?这是因为我的特定SPA进行了几次AJAX调用以获取所有数据并将其放在我页面上的DOM中,并且幻象无法知道所有调用何时完成,然后再返回DOM的HTML反射。我在这里做的是在最后的AJAX调用之后添加checkLoaded()
,这样如果这个标记存在,我就知道DOM已经完成了。我这样做是为了回应Durandal的<span id='compositionComplete'></span>
事件,请参阅here了解更多信息。如果10秒钟没有发生这种情况,我会放弃(最多只需要一秒钟)。返回的HTML包含用户在浏览器中看到的所有链接。该脚本无法正常运行,因为HTML快照中存在的compositionComplete
标记未引用正确的URL。这也可以在javascript幻像文件中更改,但我不认为这是必要的,因为HTML快照只用于谷歌获取<script>
链接而不是运行javascript;这些链接做引用一个漂亮的URL,如果你试图在浏览器中看到HTML快照,你会得到javascript错误,但所有的链接都能正常工作,并引导你到服务器一次再次使用漂亮的URL,这次获得完整工作页面
就是这个。现在,服务器知道如何处理漂亮和丑陋的URL,并在服务器和客户端上启用了push-state。所有丑陋的URL都使用幻像处理相同的方式,因此无需为每种类型的呼叫创建单独的控制器。
您可能更愿意改变的一件事是不要制作一般的&#39;类别/子类别/产品&#39;请致电,但要添加商店&#39;以便链接看起来像:a
。这样可以避免我的解决方案中的问题,即所有无效的网址都被视为实际调用了&#39;索引&#39;控制器,我想这些可以在&#39;商店内处理。没有添加到上面显示的http://www.xyz.com/store/category/subCategory/product111
的控制器。
答案 1 :(得分:32)
Google现在能够呈现SPA页面: Deprecating our AJAX crawling scheme
答案 2 :(得分:4)
这是我8月14日在伦敦举办的Ember.js培训班的截屏视频链接。它概述了客户端应用程序和服务器端应用程序的策略,并且实时演示了实现这些功能如何为JavaScript单页面应用程序提供优雅降级,即使对于关闭JavaScript的用户也是如此。
它使用PhantomJS来帮助抓取您的网站。
简而言之,所需的步骤是:
完成此步骤后,将由您的后端提供HTML的静态版本作为该页面上noscript-tag的一部分。这将允许Google和其他搜索引擎抓取您网站上的每个网页,即使您的应用最初是单页应用。
使用详细信息链接到截屏视频:
答案 3 :(得分:0)
您可以使用或创建自己的服务,使用名为prerender的服务预呈现您的SPA。您可以在他的网站prerender.io和他的github project上查看它(它使用PhantomJS并为您渲染您的网站)。
这很容易入手。您只需将抓取工具请求重定向到服务,他们就会收到呈现的html。
答案 4 :(得分:0)
您可以使用http://sparender.com/来正确抓取单页应用程序。