我一直在研究如何根据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 。因此,我们都有href
google的属性,以及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/来正确爬网单页应用程序。