Перш ніж почати, переконайтеся, що ви розумієте, що вимагає google , зокрема використання красивих та некрасивих URL-адрес. Тепер переглянемо реалізацію:
Сторона клієнта
На стороні клієнта у вас є лише одна HTML-сторінка, яка динамічно взаємодіє з сервером за допомогою AJAX-дзвінків. ось що стосується SPA. Всі a
теги на стороні клієнта створюються динамічно в моєму додатку, пізніше ми побачимо, як зробити ці посилання видимими для бота google на сервері. Кожен такий a
потреби тегів , щоб мати можливість мати pretty URL
в href
тезі так бот Google буде сканувати його. Ви не хочете, щоб href
частина використовувалася, коли клієнт натискає на неї (навіть якщо ви хочете, щоб сервер міг її розібрати, ми побачимо це пізніше), оскільки ми можемо не хотіти, щоб нова сторінка завантажувалася, лише для здійснення дзвінка AJAX, який отримує деякі дані, які відображатимуться на частині сторінки та змінює URL-адресу через javascript (наприклад, за допомогою HTML5 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>
"категорія" та "підкатегорія", ймовірно, будуть іншими фразами, такими як "зв'язок" і "телефони" або "комп'ютери" та "ноутбуки" для магазину електроприладів. Очевидно, було б багато різних категорій та підкатегорій. Як бачите, посилання знаходиться безпосередньо на категорії, підкатегорії та продукті, а не як додаткові параметри до певної сторінки "магазину", такої як 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_=
якої ми побачимось пізніше.
- Маршрут "про" - це лише приклад посилання на інші "сторінки", які ви можете захотіти у своєму веб-додатку.
- Тепер складною частиною є те, що маршруту "категорії" немає, і може бути багато різних категорій, жодна з яких не має заздалегідь визначеного маршруту. Це місце, де
mapUnknownRoutes
надходить. Він відображає ці невідомі маршрути до маршруту "магазин", а також видаляє будь-які "!" від URL-адреси на випадок, якщо це pretty URL
генерується пошуковим механізмом google. Маршрут "зберігати" приймає інформацію у властивості "фрагмент" і здійснює дзвінок 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
і шукає контролер з назвою "категорія". Отже, 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-адреси перед усіма параметрами "категорії" та "підкатегорії"; 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 і повинні повертати звичайний HTML, який містить усі дані, які користувач бачить у браузері. Для цього я використовую phantomjs . Phantom - це безголовий браузер, який робить те, що робить браузер на стороні клієнта - але на стороні сервера. Іншими словами, фантом знає (серед іншого), як отримати веб-сторінку за допомогою URL-адреси, проаналізувати її, включаючи запуск у неї всього коду javascript (а також отримання даних за допомогою дзвінків AJAX) та повернути HTML, який відображає ДОМ. Якщо ви використовуєте MS Visual Studio Express, багато хто хоче встановити 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;
}
}
}
Це називається з "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);
Я спершу хочу подякувати Томасу Девісу за сторінку, на якій я отримав основний код від :-).
Тут ви помітите щось дивне: phantom продовжує повторно завантажувати сторінку, поки checkLoaded()
функція не повернеться true. Чому так? це тому, що мій конкретний SPA здійснює кілька дзвінків AJAX, щоб отримати всі дані та розмістити їх у DOM на моїй сторінці, а фантом не може знати, коли всі виклики завершилися, перш ніж повернути мені HTML-відображення DOM. Що я тут зробив - це після остаточного дзвінка AJAX, який я додаю <span id='compositionComplete'></span>
, так що якщо цей тег існує, я знаю, що DOM завершений. Я роблю це у відповідь на compositionComplete
подію Дурандала , дивіться тутдля більш. Якщо цього не відбувається за 10 секунд, я здаюсь (на це найбільше знадобиться секунда). Повернений HTML містить усі посилання, які бачить користувач у браузері. Сценарій не працюватиме належним чином, оскільки <script>
теги, які існують у знімку HTML, не посилаються на правильну URL-адресу. Це також можна змінити у фантомному файлі javascript, але я не думаю, що це не обов'язково, оскільки HTML-знімок використовується лише Google для отримання a
посилань та не для запуску javascript; ці посилання роблять посилатися на досить URL, і якщо насправді, якщо ви намагаєтеся побачити HTML знімок в браузері, ви отримаєте JavaScript помилки , але все посилання працюватимуть належним чином і направити вас на сервер ще раз з симпатичною URL на цей раз отримання повністю працюючої сторінки.
Це воно. Тепер сервер знає, як обробляти як гарні, так і некрасиві URL-адреси, з включеним push-state і на сервері, і на клієнті. Усі потворні URL-адреси обробляються однаково за допомогою фантома, тому немає необхідності створювати окремий контролер для кожного типу виклику.
Одна річ , яку ви могли б віддати перевагу зміни не зробити скликати загальні «категорію / підкатегорію / продукт» , але , щоб додати «магазин» так , що посилання буде виглядати приблизно так: http://www.xyz.com/store/category/subCategory/product111
. Це дозволить уникнути проблеми в моєму рішенні, що всі недійсні URL-адреси обробляються так, ніби вони насправді є викликами до контролера "індексу", і я вважаю, що з ними можна обробляти потім в контролері "store" без додавання до web.config
показаного вище .