Zanim zaczniesz, upewnij się, że rozumiesz, czego wymaga Google , zwłaszcza dotyczące używania ładnych i brzydkich adresów URL. Teraz zobaczmy implementację:
Strona klienta
Po stronie klienta masz tylko jedną stronę html, która dynamicznie współdziała z serwerem za pośrednictwem wywołań AJAX. o to chodzi w SPA. Wszystkie a
tagi po stronie klienta są tworzone dynamicznie w mojej aplikacji, później zobaczymy, jak sprawić, by te linki były widoczne dla bota Google na serwerze. Każdy taki a
tag musi mieć możliwość umieszczenia pretty URL
w href
tagu znaku, aby robot Google go zaindeksował. Nie chcesz, aby href
część była używana, gdy klient ją kliknie (nawet jeśli chcesz, aby serwer mógł ją przeanalizować, zobaczymy to później), ponieważ możemy nie chcieć załadować nowej strony, tylko po to, aby wykonać wywołanie AJAX, uzyskując dane do wyświetlenia w części strony i zmienić adres URL za pomocą javascript (np. używając HTML5 pushstate
lub with Durandaljs
). Tak więc mamy oba plikihref
atrybut dla google, a także onclick
który wykonuje zadanie, gdy użytkownik kliknie link. Teraz, ponieważ używam push-state
, nie chcę żadnego #
w adresie URL, więc typowy a
tag może wyglądać tak:
<a href="http://www.xyz.com/#!/category/subCategory/product111" onClick="loadProduct('category','subCategory','product111')>see product111...</a>
„kategoria” i „podkategoria” to prawdopodobnie inne wyrażenia, takie jak „komunikacja” i „telefony” lub „komputery” oraz „laptopy” dla sklepu z urządzeniami elektrycznymi. Oczywiście byłoby wiele różnych kategorii i podkategorii. Jak widać, link prowadzi bezpośrednio do kategorii, podkategorii i produktu, a nie jako dodatkowe parametry do określonej strony sklepu, takiej jak http://www.xyz.com/store/category/subCategory/product111
. Dzieje się tak, ponieważ wolę krótsze i prostsze linki. Oznacza to, że nie będzie kategorii o takiej samej nazwie jak jedna z moich „stron”, tj. „
Nie będę się zagłębiał w ładowanie danych przez AJAX ( onclick
część), przeszukuję go w google, jest wiele dobrych wyjaśnień. Jedyną ważną rzeczą, o której chcę tutaj wspomnieć, jest to, że kiedy użytkownik kliknie ten link, chcę, aby adres URL w przeglądarce wyglądał następująco:
http://www.xyz.com/category/subCategory/product111
. A to adres URL nie jest wysyłany do serwera! pamiętaj, jest to SPA, w którym cała interakcja między klientem a serwerem odbywa się za pośrednictwem AJAX, żadnych łączy! wszystkie `` strony '' są zaimplementowane po stronie klienta, a inny adres URL nie wywołuje serwera (serwer musi wiedzieć, jak obsługiwać te adresy URL, jeśli są używane jako linki zewnętrzne z innej witryny do Twojej witryny, zobaczymy to później w części po stronie serwera). Teraz Durandal wspaniale sobie z tym radzi. Gorąco polecam, ale możesz też pominąć tę część, jeśli wolisz inne technologie. Jeśli go wybierzesz, a także używasz MS Visual Studio Express 2012 for Web, tak jak ja, możesz zainstalować zestaw startowy Durandal , a tam, w programie shell.js
, użyć czegoś takiego:
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 });
}
};
});
Należy tu zwrócić uwagę na kilka ważnych rzeczy:
- Pierwsza trasa (z
route:''
) dotyczy adresu URL, który nie zawiera żadnych dodatkowych danych, tj http://www.xyz.com
. Na tej stronie ładujesz ogólne dane za pomocą AJAX. W rzeczywistości a
na tej stronie może nie być żadnych tagów. Będziemy chcieli, aby dodać następujący tag tak że Google jest bot będzie wiedział, co z nim zrobić:
<meta name="fragment" content="!">
. Ten tag sprawi, że bot google przekształci adres URL, do www.xyz.com?_escaped_fragment_=
którego zobaczymy później.
- Trasa „about” to tylko przykład odsyłacza do innych „stron”, które mogą znajdować się w aplikacji internetowej.
- Problem polega na tym, że nie ma trasy „kategorii” i może istnieć wiele różnych kategorii - z których żadna nie ma predefiniowanej trasy. I tu
mapUnknownRoutes
pojawia się. Mapuje te nieznane trasy na trasę „sklepu”, a także usuwa wszelkie „!” z adresu URL, jeśli jest pretty URL
wygenerowany przez wyszukiwarkę Google. Trasa „store” pobiera informacje z właściwości „fragment” i wykonuje wywołanie AJAX w celu pobrania danych, wyświetlenia ich i lokalnej zmiany adresu URL. W mojej aplikacji nie ładuję innej strony dla każdego takiego połączenia; Zmieniam tylko część strony, w której te dane są istotne, a także zmieniam lokalnie adres URL.
- Zwróć uwagę,
pushState:true
co instruuje Durandala, aby używał adresów URL stanu wypychania.
To wszystko, czego potrzebujemy po stronie klienta. Można to zaimplementować również z zahaszowanymi adresami URL (w Durandal po prostu usuwasz w tym pushState:true
celu). Bardziej złożoną częścią (przynajmniej dla mnie ...) była część serwerowa:
Po stronie serwera
Używam MVC 4.5
po stronie serwera z WebAPI
kontrolerami. Serwer faktycznie musi obsługiwać 3 rodzaje adresów URL: te generowane przez Google - zarówno pretty
a ugly
, a także „prosty” URL z takim samym formacie jak ten, który pojawia się w przeglądarce klienta. Zobaczmy, jak to zrobić:
Ładne adresy URL i „proste” są najpierw interpretowane przez serwer tak, jakby próbowały odwołać się do nieistniejącego kontrolera. Serwer widzi coś podobnego http://www.xyz.com/category/subCategory/product111
i szuka kontrolera o nazwie „kategoria”. Więc web.config
dodaję następujący wiersz, aby przekierować je do określonego kontrolera obsługi błędów:
<customErrors mode="On" defaultRedirect="Error">
<error statusCode="404" redirect="Error" />
</customErrors><br/>
Teraz, to przekształca URL do czegoś takiego: http://www.xyz.com/Error?aspxerrorpath=/category/subCategory/product111
. Chcę, aby adres URL był wysyłany do klienta, który będzie ładował dane przez AJAX, więc sztuczka polega na wywołaniu domyślnego kontrolera „indeksu”, jakby nie odwoływał się do żadnego kontrolera; Robię to, dodając skrót do adresu URL przed wszystkimi parametrami „category” i „subCategory”; zaszyfrowany adres URL nie wymaga żadnego specjalnego kontrolera z wyjątkiem domyślnego kontrolera „indeksu”, a dane są wysyłane do klienta, który następnie usuwa hash i używa informacji po hashu do załadowania danych przez AJAX. Oto kod kontrolera obsługi błędów:
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;
}
}
}
Ale co z brzydkimi adresami URL ? Są one tworzone przez bota Google i powinny zwracać zwykły kod HTML zawierający wszystkie dane, które użytkownik widzi w przeglądarce. Do tego używam phantomjs . Phantom to przeglądarka bezgłowa, która robi to, co przeglądarka robi po stronie klienta - ale po stronie serwera. Innymi słowy, fantom wie (między innymi), jak uzyskać stronę internetową za pośrednictwem adresu URL, przeanalizować ją, w tym uruchomić cały kod javascript (a także pobrać dane za pośrednictwem wywołań AJAX) i zwrócić kod HTML, który odzwierciedla DOM. Jeśli używasz MS Visual Studio Express, wielu z was chce zainstalować fantom za pomocą tego linku .
Ale najpierw, kiedy brzydki adres URL jest wysyłany do serwera, musimy go przechwycić; W tym celu dodałem do folderu „App_start” następujący plik:
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;
}
}
}
Jest to wywoływane z „filterConfig.cs” również w „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());
}
}
}
Jak widać, „AjaxCrawlableAttribute” kieruje brzydkie adresy URL do kontrolera o nazwie „HtmlSnapshot”, a oto ten kontroler:
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();
}
}
}
Skojarzony view
jest bardzo prosty, tylko jeden wiersz kodu:
@Html.Raw( ViewBag.result )
jak widać w kontrolerze, phantom ładuje plik javascript o nazwie createSnapshot.js
pod utworzonym przeze mnie folderem seo
. Oto ten plik 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);
Najpierw chciałbym podziękować Thomasowi Davisowi za stronę, z której otrzymałem podstawowy kod :-).
Zauważysz tutaj coś dziwnego: phantom ponownie ładuje stronę, dopóki checkLoaded()
funkcja nie zwróci true. Dlaczego? Dzieje się tak, ponieważ moje konkretne SPA wykonuje kilka wywołań AJAX, aby pobrać wszystkie dane i umieścić je w DOM na mojej stronie, a fantom nie może wiedzieć, kiedy wszystkie wywołania zostały zakończone, zanim zwróci mi odbicie HTML DOM. To, co tutaj zrobiłem, to po ostatnim wywołaniu AJAX, dodałem a <span id='compositionComplete'></span>
, więc jeśli ten tag istnieje, wiem, że DOM jest zakończony. Robię to w odpowiedzi na compositionComplete
wydarzenie Durandala , patrz tutajpo więcej. Jeśli tak się nie stanie w ciągu 10 sekund, poddaję się (maksymalnie powinno to zająć tylko sekundę). Zwrócony kod HTML zawiera wszystkie linki, które użytkownik widzi w przeglądarce. Skrypt nie będzie działał poprawnie, ponieważ <script>
tagi, które istnieją w migawce HTML, nie odwołują się do właściwego adresu URL. Można to również zmienić w pliku phantom javascript, ale nie sądzę, aby to było konieczne, ponieważ skrót HTML jest używany tylko przez Google do pobierania a
linków, a nie do uruchamiania javascript; Te linki zrobić odwoływać się ładny URL, a jeśli rzeczywistości, jeśli starają się zobaczyć zrzut HTML w przeglądarce, dostaniesz błędy JavaScript, ale wszystkie linki będą działać poprawnie i skieruje cię do serwera po raz kolejny z dość URL tego czasu uzyskanie w pełni działającej strony.
To jest to. Teraz serwer wie, jak obsługiwać zarówno ładne, jak i brzydkie adresy URL, z włączonym stanem wypychania zarówno na serwerze, jak i kliencie. Wszystkie brzydkie adresy URL są traktowane w ten sam sposób za pomocą fantomu, więc nie ma potrzeby tworzenia oddzielnego kontrolera dla każdego typu wywołania.
Jedna rzecz może wolisz do zmian nie jest do zwołania walnego 'kategorii / podkategorii / produktu, ale dodać „magazyn”, tak, że związek będzie wyglądać mniej więcej tak: http://www.xyz.com/store/category/subCategory/product111
. Pozwoli to uniknąć problemu w moim rozwiązaniu, że wszystkie nieprawidłowe adresy URL są traktowane tak, jakby były faktycznie wywołaniami kontrolera „indeksu” i przypuszczam, że można je wtedy obsłużyć w kontrolerze „sklepu” bez dodatku do web.config
pokazanego powyżej .