Перед началом, пожалуйста, убедитесь, что вы понимаете, что требует Google , в частности, использование красивых и некрасивых URL-адреса. Теперь давайте посмотрим на реализацию:
Сторона клиента
На стороне клиента у вас есть только одна HTML-страница, которая динамически взаимодействует с сервером через вызовы AJAX. вот что такое SPA. Все a
теги на стороне клиента создаются динамически в моем приложении, позже мы увидим, как сделать эти ссылки видимыми для бота Google на сервере. Каждый такой a
тег должен иметь возможность иметь тег pretty URL
в href
теге, чтобы бот Google сканировал его. Вы не хотите, чтобы эта href
часть использовалась, когда клиент нажимает на нее (даже если вы действительно хотите, чтобы сервер мог ее проанализировать, мы увидим это позже), потому что мы можем не захотеть загружать новую страницу, только для вызова AJAX с получением некоторых данных для отображения на части страницы и изменения URL-адреса с помощью JavaScript (например,pushstate
Durandaljs
). Итак, у нас естьhref
Атрибут для Google, а также на onclick
котором выполняет свою работу, когда пользователь нажимает на ссылку. Теперь, поскольку я использую, push-state
я не хочу ничего #
в URL, поэтому типичный a
тег может выглядеть так:
<a href="http://www.xyz.com/#!/category/subCategory/product111" onClick="loadProduct('category','subCategory','product111')>see product111...</a>
«category» и «subCategory», вероятно, будут другими фразами, такими как «связь» и «телефоны» или «компьютеры» и «ноутбуки» для магазина электротоваров. Очевидно, будет много разных категорий и подкатегорий. Как видите, ссылка непосредственно на категорию, подкатегорию и продукт, а не в качестве дополнительных параметров для конкретной страницы «магазина», такой как 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 для 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_=
который мы увидим позже.
- Маршрут «about» - это просто пример ссылки на другие «страницы», которые вы можете захотеть использовать в своем веб-приложении.
- Сложность в том, что нет маршрута категории, и может быть много разных категорий, ни у одной из которых нет заранее определенного маршрута. Вот где
mapUnknownRoutes
приходит. Он сопоставляет эти неизвестные маршруты с маршрутом 'store', а также удаляет любые '!' с URL на случай, если он pretty URL
сгенерирован поисковым движком Google. Маршрут store хранит информацию в свойстве фрагмента и вызывает AJAX для получения данных, их отображения и локального изменения URL. В моем приложении я не загружаю разные страницы для каждого такого вызова; Я изменяю только ту часть страницы, где эти данные имеют отношение, а также меняю местный URL.
- Обратите внимание на то,
pushState:true
что Durandal указывает использовать URL-адреса push-состояний.
Это все, что нам нужно на стороне клиента. Это может быть реализовано также с хэшированными URL-адресами (в Durandal вы просто удаляете pushState:true
для этого). Более сложной частью (по крайней мере для меня ...) была серверная часть:
Сторона сервера
Я использую MVC 4.5
на стороне сервера с WebAPI
контроллерами. Сервер фактически должен обрабатывать 3 типа URL: сгенерированные google - оба, pretty
а ugly
также «простой» 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, поэтому хитрость здесь заключается в том, чтобы вызвать контроллер индекса по умолчанию, как если бы он не ссылался на какой-либо контроллер; Я делаю это, добавляя хеш к URL-адресу перед всеми параметрами 'category' и 'subCategory'; Для хешированного URL-адреса не требуется никакого специального контроллера, кроме контроллера по умолчанию «index», и данные отправляются клиенту, который затем удаляет хеш и использует информацию после хеша для загрузки данных через 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 и должны возвращать простой HTML, который содержит все данные, которые пользователь видит в браузере. Для этого я использую фантомы . Phantom - это безголовый браузер, который делает то же, что и браузер на стороне клиента, но на стороне сервера. Другими словами, фантом знает (среди прочего), как получить веб-страницу через URL-адрес, проанализировать ее, включая выполнение всего кода javascript (а также получение данных с помощью вызовов AJAX), и вернуть вам HTML, который отражает ДОМ. Если вы используете 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;
}
}
}
Это вызывается из 'filterConfig.cs' также в 'App_start':
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 )
как вы можете видеть в контроллере, фантом загружает файл javascript с именем createSnapshot.js
в созданной мной папке с именем 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);
Сначала я хочу поблагодарить Томаса Дэвиса за страницу, с которой я получил основной код :-).
Здесь вы заметите нечто странное: фантом продолжает перезагружать страницу, пока checkLoaded()
функция не вернет true. Это почему? это потому, что мой специальный SPA делает несколько AJAX-вызовов, чтобы получить все данные и поместить их в DOM на моей странице, и фантом не может знать, когда все вызовы завершены, прежде чем вернуть мне обратно HTML-отражение DOM. То, что я сделал здесь, после последнего вызова AJAX, я добавляю a <span id='compositionComplete'></span>
, так что если этот тег существует, я знаю, что DOM завершен. Я делаю это в ответ на compositionComplete
событие Дюрандаля , см. здесьдля большего. Если этого не произойдет в течение 10 секунд, я сдаюсь (это займет всего одну секунду, чтобы максимально). Возвращенный HTML-код содержит все ссылки, которые пользователь видит в браузере. Сценарий не будет работать должным образом, поскольку <script>
теги, которые существуют в снимке HTML, не ссылаются на правильный URL-адрес. Это также может быть изменено в фантомном файле javascript, но я не думаю, что это необходимо, потому что моментальный снимок HTML используется Google только для получения a
ссылок, а не для запуска javascript; эти ссылки делают ссылаться на довольно URL, и если самом деле, если вы пытаетесь увидеть HTML снимок в браузере, вы получите JavaScript ошибки , но все ссылки будут работать должным образом и направить вас на сервер еще раз с симпатичной URL на этот раз получить полностью рабочую страницу.
это все. Теперь сервер знает, как обрабатывать как красивые, так и некрасивые URL, с включенным push-состоянием как на сервере, так и на клиенте. Все уродливые URL обрабатываются одинаково с использованием фантома, поэтому нет необходимости создавать отдельный контроллер для каждого типа вызова.
Одна вещь , которую вы могли бы предпочесть изменения не сделать созвать общее «категорию / подкатегорию / продукт» , но , чтобы добавить «магазин» так , что ссылка будет выглядеть примерно так: http://www.xyz.com/store/category/subCategory/product111
. Это позволит избежать проблемы в моем решении, заключающейся в том, что все недопустимые URL-адреса обрабатываются так, как будто они на самом деле являются вызовами контроллера «index», и я предполагаю, что они могут быть обработаны затем в контроллере «store» без добавления к тому, что web.config
я показал выше. ,