bea*_*ish 142 ajax seo phantomjs single-page-application durandal
我一直在研究如何根据谷歌的指示谷歌可以抓取SPA .尽管有很多一般性的解释,但我找不到更详尽的逐步教程和实际示例.完成此操作后,我想分享我的解决方案,以便其他人也可以使用它,并可能进一步改进它.
我使用MVC
与Webapi
控制器和Phantomjs在服务器端,并迪朗达尔与客户端push-state
启用; 我还使用Breezejs进行客户端 - 服务器数据交互,我强烈推荐所有这些,但我会尝试给出一个足够的解释,也可以帮助人们使用其他平台.
bea*_*ish 121
在开始之前,请确保您了解谷歌需要什么,特别是使用漂亮和丑陋的 URL.现在让我们看一下实现:
在客户端,您只有一个html页面,它通过AJAX调用动态地与服务器交互.这就是SPA的意义所在.a
客户端中的所有标签都是在我的应用程序中动态创建的,稍后我们将看到如何在服务器中将这些链接显示为google的bot.每个此类a
标记都需要能够pretty URL
在href
标记中包含一个标记,以便google的bot会抓取它.您不希望在href
客户端点击它时使用该部件(即使您确实希望服务器能够解析它,我们稍后会看到),因为我们可能不希望加载新页面,只做一个AJAX调用,让一些数据显示在页面的一部分,并通过javascript更改URL(例如使用HTML5 pushstate
或with Durandaljs
).因此,我们既有href
谷歌的属性,也有onclick
用户点击链接时的工作.现在,因为我使用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
部分)加载数据,在谷歌上搜索,有很多很好的解释.我想要提到的唯一重要的事情是,当用户点击此链接时,我希望浏览器中的URL看起来像这样:
http://www.xyz.com/category/subCategory/product111
.这是URL不发送到服务器!记住,这是一个SPA,客户端和服务器之间的所有交互都是通过AJAX完成的,根本没有链接!所有"页面"都在客户端实现,不同的URL不会调用服务器(服务器确实需要知道如何处理这些URL,以防它们被用作从另一个站点到您站点的外部链接,我们稍后会在服务器端部分看到).现在,Durandal精彩地处理了这个问题.我强烈推荐它,但如果您更喜欢其他技术,也可以跳过此部分.如果您选择它,并且您也使用像我这样的MS Visual Studio Express 2012 for Web,您可以安装Durandal入门套件,在那里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 });
}
};
});
Run Code Online (Sandbox Code Playgroud)
这里有一些重要的事情需要注意:
route:''
)用于没有额外数据的URL,即http://www.xyz.com
.在此页面中,您可以使用AJAX加载常规数据.a
此页面中实际上可能根本没有标签.您将需要添加以下标记,以便谷歌的机器人知道如何处理它:<meta name="fragment" content="!">
.此标记会让谷歌的机器人转换为www.xyz.com?_escaped_fragment_=
我们稍后会看到的网址.mapUnknownRoutes
用武之地.它将这些未知路线映射到"商店"路线,并删除任何"!" 来自URL,以防它是pretty URL
谷歌的搜索引擎生成的.'store'路径获取'fragment'属性中的信息,并进行AJAX调用以获取数据,显示数据并在本地更改URL.在我的应用程序中,我没有为每个这样的调用加载不同的页面; 我只更改了与此数据相关的页面部分,并在本地更改了URL.pushState:true
它指示Durandal使用推送状态URL. 这就是我们在客户端所需要的一切.它也可以用散列URL实现(在Durandal中你可以简单地删除pushState:true
它).更复杂的部分(至少对我而言......)是服务器部分:
我MVC 4.5
在服务器端使用WebAPI
控制器.服务器实际上需要处理3种类型的URL:谷歌生成的URL - 两者pretty
以及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/>
Run Code Online (Sandbox Code Playgroud)
现在,这会将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;
}
}
}
Run Code Online (Sandbox Code Playgroud)
但丑陋的URL怎么样?这些是由google的bot创建的,应该返回包含用户在浏览器中看到的所有数据的纯HTML.为此我使用phantomjs.Phantom是一个无头浏览器,可以在客户端执行浏览器操作 - 但在服务器端.换句话说,幻影知道(除其他事项外)如何通过URL获取网页,解析它包括运行其中的所有javascript代码(以及通过AJAX调用获取数据),并返回反映的HTML 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;
}
}
}
Run Code Online (Sandbox Code Playgroud)
这也是从'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());
}
}
}
Run Code Online (Sandbox Code Playgroud)
如您所见,'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();
}
}
}
Run Code Online (Sandbox Code Playgroud)
关联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);
Run Code Online (Sandbox Code Playgroud)
我首先要感谢Thomas Davis,我从中获得了基本代码的页面:-).
你会发现一些奇怪的事情:幻像不断重新加载页面,直到checkLoaded()
函数返回true.这是为什么?这是因为我的特定SPA进行了几次AJAX调用以获取所有数据并将其放在我页面上的DOM中,并且幻像无法知道所有调用何时完成,然后再返回DOM的HTML反射.我在这里做的是在我添加a的最终AJAX调用之后<span id='compositionComplete'></span>
,如果这个标签存在,我知道DOM已经完成.我这样做是为了回应Durandal的compositionComplete
活动,请点击此处了解更多信息.如果在10秒钟内没有发生这种情况,我会放弃(最多只需要一秒钟).返回的HTML包含用户在浏览器中看到的所有链接.该脚本无法正常运行,因为<script>
HTML快照中存在的标记不会引用正确的URL.这也可以在javascript幻像文件中更改,但我不认为这是必要的,因为HTML snapshort只用于谷歌获取a
链接而不是运行javascript; 这些链接确实引用了一个漂亮的URL,如果你试图在浏览器中看到HTML快照,你会得到javascript错误,但是所有链接都能正常工作,并且这次使用漂亮的URL再次引导你到服务器获得完整工作的页面.
就是这个.现在,服务器知道如何处理漂亮和丑陋的URL,并在服务器和客户端上启用了推送状态.所有丑陋的URL都使用幻像以相同的方式处理,因此不需要为每种类型的调用创建单独的控制器.
您可能更喜欢改变的一件事是不要进行一般的'category/subCategory/product'调用,而是添加一个'store',以便链接看起来像:http://www.xyz.com/store/category/subCategory/product111
.这将避免我的解决方案中的问题,即所有无效的URL都被视为实际上是对"索引"控制器的调用,并且我认为这些可以在"存储"控制器中处理,而无需添加到web.config
上面显示的I .