ajax – 如何使SPA SEO可爬行?

前端之家收集整理的这篇文章主要介绍了ajax – 如何使SPA SEO可爬行?前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。
我一直在努力如何使一个SPA可以爬行的谷歌google基于谷歌的 instructions.即使有很多一般的解释,我找不到任何地方更详细的逐步教程与实际的例子。完成后,我想分享我的解决方案,以便其他人也可以使用它,并可能进一步改善。
我使用MVC与Webapi控制器, Phantomjs在服务器端,和 Durandal在客户端启用推状态;我也使用 Breezejs进行客户端 – 服务器数据交互,所有这些都强烈推荐,但我会尽量给出一个足够全面的解释,也将帮助人们使用其他平台。
在开始之前,请确保您了解什么google requires,特别是使用漂亮和丑陋的URL。现在让我们看看实现:

客户端

在客户端,你只有一个html页面,通过AJAX调用动态地与服务器交互。这就是SPA是什么。客户端中的所有a标签都是在我的应用程序中动态创建的,稍后我们将看到如何使这些链接对服务器中的Googlebot可见。每个这样的代码都需要能够在href标记中有一个漂亮的网址,这样google的bot才能抓取它。您不希望在客户端点击它时使用href部分(即使您确实希望服务器能够解析它,我们稍后会看到),因为我们可能不希望加载新页面,只有使AJAX调用获取一些数据显示页面的一部分,并通过javascript更改URL(例如使用HTML5 pushstate或与Durandaljs)。因此,我们有google的href属性以及onclick,当用户点击链接时执行该作业。现在,由于我使用push状态,我不想在URL上的任何#,所以一个典型的标签可能看起来像这样:
< a href =“http://www.xyz.com/#!/category/subCategory/product111”onClick =“loadProduct('category','subCategory','product111')>请参阅product111 …< ; / a>
“类别”和“子类别”可能是其他短语,例如电器商店的“通信”和“电话”或“计算机”和“笔记本电脑”。显然会有很多不同的类别和子类别。如您所见,链接直接指向类别,子类别和产品,而不是特定“商店”页面的额外参数,例如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中使用这样的东西:

  1. define(['plugins/router','durandal/app'],function (router,app) {
  2. return {
  3. router: router,activate: function () {
  4. router.map([
  5. { route: '',title: 'Store',moduleId: 'viewmodels/store',nav: true },{ route: 'about',moduleId: 'viewmodels/about',nav: true }
  6. ])
  7. .buildNavigationModel()
  8. .mapUnknownRoutes(function (instruction) {
  9. instruction.config.moduleId = 'viewmodels/store';
  10. instruction.fragment = instruction.fragment.replace("!/",""); // for pretty-URLs,'#' already removed because of push-state,only ! remains
  11. return instruction;
  12. });
  13. return router.activate({ pushState: true });
  14. }
  15. };
  16. });

这里有几个重要的事情需要注意:

>第一个路由(路由:”)用于没有额外数据的URL,即http://www.xyz.com。在此页面中,使用AJAX加载常规数据。在此页面中实际上可能没有任何标签。您将需要添加以下标记,以便google的bot将知道该怎么做:
< Meta name =“fragment”content =“!”>。这个标记会让google的bot将网址转换为www.xyz.com?_escaped_fragment_=,我们稍后会看到。
>“关于”路线只是一个链接到您可能想要在您的Web应用程序上的其他“页面”的示例。
>现在,棘手的部分是没有“类别”路线,并且可能有许多不同的类别 – 没有一个具有预定义的路线。这是mapUnknownRoutes进来的地方。它将这些未知路由映射到’store’路由,并删除任何’!’。从URL,如果它是一个漂亮的URL生成的google的seach引擎。 “store”路由采用’fragment’属性中的信息,并进行AJAX调用获取数据,显示它,并在本地更改URL。在我的应用程序中,我不为每个这样的调用加载不同的页面;我只更改页面中与此数据相关的部分,并且还在本地更改URL。
>注意pushState:true,它指示Durandal使用推送状态URL。

这就是我们在客户端所需要的。它也可以实现与哈希的URL(在Durandal你简单删除pushState:true)。更复杂的部分(至少对我来说)是服务器部分:

服务器端

我在服务器端使用MVC 4.5与WebAPI控制器。服务器实际上需要处理3种类型的网址:google生成的网址 – 漂亮和丑陋的网址,以及与客户端浏览器中显示的网址格式相同的“简单”网址。让我们看看如何做:

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

  1. <customErrors mode="On" defaultRedirect="Error">
  2. <error statusCode="404" redirect="Error" />
  3. </customErrors><br/>

现在,这将URL转换为像:http://www.xyz.com/Error?aspxerrorpath=/category/subCategory/product111。我想要的URL发送到客户端将通过AJAX加载数据,所以这里的诀窍是调用默认的“索引”控制器,如果没有引用任何控制器;我通过在所有’category’和’subCategory’参数之前向URL添加一个散列;散列的URL不需要任何特殊的控制器,除了默认的“索引”控制器,数据发送到客户端,然后删除散列,并使用散列后的信息通过AJAX加载数据。这里是错误处理程序控制器代码

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Net;
  5. using System.Net.Http;
  6. using System.Web.Http;
  7.  
  8. using System.Web.Routing;
  9.  
  10. namespace eShop.Controllers
  11. {
  12. public class ErrorController : ApiController
  13. {
  14. [HttpGet,HttpPost,HttpPut,HttpDelete,HttpHead,HttpOptions,AcceptVerbs("PATCH"),AllowAnonymous]
  15. public HttpResponseMessage Handle404()
  16. {
  17. string [] parts = Request.RequestUri.OriginalString.Split(new[] { '?' },StringSplitOptions.RemoveEmptyEntries);
  18. string parameters = parts[ 1 ].Replace("aspxerrorpath=","");
  19. var response = Request.CreateResponse(HttpStatusCode.Redirect);
  20. response.Headers.Location = new Uri(parts[0].Replace("Error","") + string.Format("#{0}",parameters));
  21. return response;
  22. }
  23. }
  24. }

但是,丑陋的URL呢?这些是由google的bot创建的,应该返回包含用户在浏览器中看到的所有数据的纯HTML。对于这个我使用phantomjs. Phantom是一个无头浏览器做浏览器在客户端做的 – 但在服务器端。换句话说,phantom知道(除其他事项外)如何通过URL获取网页,解析它,包括运行其中的所有javascript代码(以及通过AJAX调用获取数据),并返回HTML反映DOM。如果你使用MS Visual Studio Express,你很多人想通过这个link安装phantom。
但首先,当一个丑陋的URL发送到服务器,我们必须抓住它;为此,我在“App_start”文件夹中添加了以下文件

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Diagnostics;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Reflection;
  7. using System.Web;
  8. using System.Web.Mvc;
  9. using System.Web.Routing;
  10.  
  11. namespace eShop.App_Start
  12. {
  13. public class AjaxCrawlableAttribute : ActionFilterAttribute
  14. {
  15. private const string Fragment = "_escaped_fragment_";
  16.  
  17. public override void OnActionExecuting(ActionExecutingContext filterContext)
  18. {
  19. var request = filterContext.RequestContext.HttpContext.Request;
  20.  
  21. if (request.QueryString[Fragment] != null)
  22. {
  23.  
  24. var url = request.Url.ToString().Replace("?_escaped_fragment_=","#");
  25.  
  26. filterContext.Result = new RedirectToRouteResult(
  27. new RouteValueDictionary { { "controller","HtmlSnapshot" },{ "action","returnHTML" },{ "url",url } });
  28. }
  29. return;
  30. }
  31. }
  32. }

这也从’app_start’中的’filterConfig.cs’调用

  1. using System.Web.Mvc;
  2. using eShop.App_Start;
  3.  
  4. namespace eShop
  5. {
  6. public class FilterConfig
  7. {
  8. public static void RegisterGlobalFilters(GlobalFilterCollection filters)
  9. {
  10. filters.Add(new HandleErrorAttribute());
  11. filters.Add(new AjaxCrawlableAttribute());
  12. }
  13. }
  14. }

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

  1. using System;
  2. using System.Collections.Generic;
  3. using System.Diagnostics;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Web;
  7. using System.Web.Mvc;
  8.  
  9. namespace eShop.Controllers
  10. {
  11. public class HtmlSnapshotController : Controller
  12. {
  13. public ActionResult returnHTML(string url)
  14. {
  15. string appRoot = Path.GetDirectoryName(AppDomain.CurrentDomain.BaseDirectory);
  16.  
  17. var startInfo = new ProcessStartInfo
  18. {
  19. 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
  20. };
  21. var p = new Process();
  22. p.StartInfo = startInfo;
  23. p.Start();
  24. string output = p.StandardOutput.ReadToEnd();
  25. p.WaitForExit();
  26. ViewData["result"] = output;
  27. return View();
  28. }
  29.  
  30. }
  31. }

相关的视图很简单,只是一行代码
@ Html.Raw(ViewBag.result)
从控制器中可以看到,phantom在我创建的名为SEO文件夹下加载一个名为createSnapshot.js的javascript文件。这里是这个javascript文件

  1. var page = require('webpage').create();
  2. var system = require('system');
  3.  
  4. var lastReceived = new Date().getTime();
  5. var requestCount = 0;
  6. var responseCount = 0;
  7. var requestIds = [];
  8. var startTime = new Date().getTime();
  9.  
  10. page.onResourceReceived = function (response) {
  11. if (requestIds.indexOf(response.id) !== -1) {
  12. lastReceived = new Date().getTime();
  13. responseCount++;
  14. requestIds[requestIds.indexOf(response.id)] = null;
  15. }
  16. };
  17. page.onResourceRequested = function (request) {
  18. if (requestIds.indexOf(request.id) === -1) {
  19. requestIds.push(request.id);
  20. requestCount++;
  21. }
  22. };
  23.  
  24. function checkLoaded() {
  25. return page.evaluate(function () {
  26. return document.all["compositionComplete"];
  27. }) != null;
  28. }
  29. // Open the page
  30. page.open(system.args[1],function () { });
  31.  
  32. var checkComplete = function () {
  33. // We don't allow it to take longer than 5 seconds but
  34. // don't return until all requests are finished
  35. if ((new Date().getTime() - lastReceived > 300 && requestCount === responseCount) || new Date().getTime() - startTime > 10000 || checkLoaded()) {
  36. clearInterval(checkCompleteInterval);
  37. var result = page.content;
  38. //result = result.substring(0,10000);
  39. console.log(result);
  40. //console.log(results);
  41. phantom.exit();
  42. }
  43. }
  44. // Let us check to see if the page is finished rendering
  45. var checkCompleteInterval = setInterval(checkComplete,300);

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

猜你在找的Ajax相关文章