我使用MVC与Webapi控制器, Phantomjs在服务器端,和 Durandal在客户端启用推状态;我也使用 Breezejs进行客户端 – 服务器数据交互,所有这些都强烈推荐,但我会尽量给出一个足够全面的解释,也将帮助人们使用其他平台。
客户端
在客户端,你只有一个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中使用这样的东西:
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 }); } }; });
这里有几个重要的事情需要注意:
>第一个路由(路由:”)用于没有额外数据的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中我添加以下行重定向这些到一个特定的错误处理控制器:
<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; } } }
但是,丑陋的URL呢?这些是由google的bot创建的,应该返回包含用户在浏览器中看到的所有数据的纯HTML。对于这个我使用phantomjs. Phantom是一个无头浏览器做浏览器在客户端做的 – 但在服务器端。换句话说,phantom知道(除其他事项外)如何通过URL获取网页,解析它,包括运行其中的所有javascript代码(以及通过AJAX调用获取数据),并返回HTML反映DOM。如果你使用MS Visual Studio Express,你很多人想通过这个link安装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(); } } }
相关的视图很简单,只是一行代码:
@ Html.Raw(ViewBag.result)
从控制器中可以看到,phantom在我创建的名为SEO的文件夹下加载一个名为createSnapshot.js的javascript文件。这里是这个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不能知道什么时候所有的调用完成之前,返回我的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我上面显示。