Prima di iniziare, assicurati di capire cosa richiede Google , in particolare l'uso di URL belli e brutti . Ora vediamo l'implementazione:
Dalla parte del cliente
Sul lato client hai solo una singola pagina html che interagisce dinamicamente con il server tramite chiamate AJAX. ecco di cosa parla SPA. Tutti i a
tag sul lato client vengono creati dinamicamente nella mia applicazione, vedremo in seguito come rendere questi collegamenti visibili al bot di Google nel server. Ognuno di tali a
esigenze tag siano in grado di avere una pretty URL
nel href
tag in modo che il bot di Google esegue la scansione di esso. Non vuoi che la href
parte venga utilizzata quando il client fa clic su di essa (anche se desideri che il server sia in grado di analizzarla, lo vedremo più avanti), perché potremmo non voler caricare una nuova pagina, solo per effettuare una chiamata AJAX ottenendo alcuni dati da visualizzare in una parte della pagina e modificare l'URL tramite javascript (ad esempio utilizzando HTML5 pushstate
o con Durandaljs
). Quindi, abbiamo entrambi unhref
attributo per google e su onclick
quale svolge il lavoro quando l'utente fa clic sul collegamento. Ora, poiché uso push-state
non ne voglio alcuno #
sull'URL, quindi un tipico a
tag potrebbe apparire così:
<a href="http://www.xyz.com/#!/category/subCategory/product111" onClick="loadProduct('category','subCategory','product111')>see product111...</a>
'categoria' e 'sottoCategoria' probabilmente sarebbero altre frasi, come 'comunicazione' e 'telefoni' o 'computer' e "laptop" per un negozio di elettrodomestici. Ovviamente ci sarebbero molte diverse categorie e sottocategorie. Come puoi vedere, il link è direttamente alla categoria, alla sottocategoria e al prodotto, non come parametri extra a una specifica pagina 'store' come http://www.xyz.com/store/category/subCategory/product111
. Questo perché preferisco collegamenti più brevi e più semplici. Implica che non ci sarà una categoria con lo stesso nome di una delle mie "pagine", cioè "
Non entrerò nel modo in cui caricare i dati tramite AJAX (la onclick
parte), cercarli su google, ci sono molte buone spiegazioni. L'unica cosa importante qui che voglio menzionare è che quando l'utente fa clic su questo link, voglio che l'URL nel browser assomigli a questo:
http://www.xyz.com/category/subCategory/product111
. E questo è l'URL non viene inviato al server! ricorda, questa è una SPA in cui tutta l'interazione tra client e server avviene tramite AJAX, nessun collegamento! tutte le "pagine" sono implementate sul lato client e il diverso URL non effettua una chiamata al server (il server deve sapere come gestire questi URL nel caso in cui vengano utilizzati come collegamenti esterni da un altro sito al tuo sito, lo vedremo più avanti nella parte lato server). Ora, questo è gestito meravigliosamente da Durandal. Lo consiglio vivamente, ma puoi anche saltare questa parte se preferisci altre tecnologie. Se lo scegli, e stai anche utilizzando MS Visual Studio Express 2012 per Web come me, puoi installare Durandal Starter Kit e lì, in shell.js
, utilizzare qualcosa del genere:
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 });
}
};
});
Ci sono alcune cose importanti da notare qui:
- Il primo percorso (con
route:''
) è per l'URL che non contiene dati extra, ad es http://www.xyz.com
. In questa pagina carichi i dati generali usando AJAX. In a
questa pagina potrebbero non esserci tag. Si vuole aggiungere il seguente tag in modo che il bot di Google saprà che cosa fare con esso:
<meta name="fragment" content="!">
. Questo tag consentirà al bot di Google di trasformare l'URL in www.xyz.com?_escaped_fragment_=
cui vedremo più avanti.
- Il percorso 'about' è solo un esempio di un collegamento ad altre 'pagine' che potresti desiderare sulla tua applicazione web.
- Ora, la parte difficile è che non esiste un percorso 'categoria' e ci possono essere molte categorie diverse, nessuna delle quali ha un percorso predefinito. È qui che
mapUnknownRoutes
entra in gioco. Mappa questi percorsi sconosciuti sul percorso 'store' e rimuove anche qualsiasi '!' dall'URL nel caso sia pretty URL
generato dal motore di ricerca di Google. Il percorso 'store' prende le informazioni nella proprietà 'fragment' ed effettua la chiamata AJAX per ottenere i dati, visualizzarli e modificare l'URL localmente. Nella mia applicazione, non carico una pagina diversa per ogni chiamata di questo tipo; Cambio solo la parte della pagina in cui questi dati sono rilevanti e cambio anche l'URL localmente.
- Si noti
pushState:true
che indica a Durandal di utilizzare gli URL di stato push.
Questo è tutto ciò di cui abbiamo bisogno nel lato client. Può essere implementato anche con URL con hash (in Durandal è sufficiente rimuoverlo pushState:true
per quello). La parte più complessa (almeno per me ...) era la parte del server:
Lato server
Sto usando MVC 4.5
sul lato server con i WebAPI
controller. Il server è in realtà ha bisogno di gestire 3 tipi di URL: quelli generati da Google - sia pretty
ed ugly
e anche un URL 'semplice' con lo stesso formato di quello che appare nel browser del client. Vediamo come fare:
Gli URL graziosi e quelli "semplici" vengono prima interpretati dal server come se provassero a fare riferimento a un controller inesistente. Il server vede qualcosa di simile http://www.xyz.com/category/subCategory/product111
e cerca un controller chiamato 'categoria'. Quindi web.config
aggiungo la seguente riga per reindirizzarli a un controller di gestione degli errori specifico:
<customErrors mode="On" defaultRedirect="Error">
<error statusCode="404" redirect="Error" />
</customErrors><br/>
Ora, questo trasforma l'URL a qualcosa come: http://www.xyz.com/Error?aspxerrorpath=/category/subCategory/product111
. Voglio che l'URL venga inviato al client che caricherà i dati tramite AJAX, quindi il trucco qui è chiamare il controller 'index' predefinito come se non si riferisse a nessun controller; Lo faccio aggiungendo un hash all'URL prima di tutti i parametri "categoria" e "sottocategoria"; l'URL con hash non richiede alcun controller speciale tranne il controller "indice" predefinito e i dati vengono inviati al client che quindi rimuove l'hash e utilizza le informazioni dopo l'hash per caricare i dati tramite AJAX. Ecco il codice del controller del gestore errori:
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;
}
}
}
Ma per quanto riguarda gli URL brutti ? Questi sono creati dal bot di Google e dovrebbero restituire un semplice HTML che contiene tutti i dati che l'utente vede nel browser. Per questo uso phantomjs . Phantom è un browser senza testa che fa ciò che fa il browser sul lato client, ma sul lato server. In altre parole, il fantasma sa (tra le altre cose) come ottenere una pagina Web tramite un URL, analizzarla includendo tutto il codice javascript in essa contenuto (nonché ottenere dati tramite chiamate AJAX) e restituirti l'HTML che riflette il DOM. Se si utilizza MS Visual Studio Express, molti desiderano installare Phantom tramite questo collegamento .
Ma prima, quando un brutto URL viene inviato al server, dobbiamo prenderlo; Per questo, ho aggiunto alla cartella 'App_start' il seguente file:
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;
}
}
}
Questo è chiamato da 'filterConfig.cs' anche in '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());
}
}
}
Come puoi vedere, "AjaxCrawlableAttribute" indirizza brutti URL a un controller chiamato "HtmlSnapshot", ed ecco questo controller:
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();
}
}
}
L'associato view
è molto semplice, solo una riga di codice:
@Html.Raw( ViewBag.result )
come puoi vedere nel controller, phantom carica un file javascript chiamato createSnapshot.js
sotto una cartella che ho creato chiamato seo
. Ecco questo file 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);
Voglio prima ringraziare Thomas Davis per la pagina da cui ho ricevuto il codice di base da :-).
Noterai qualcosa di strano qui: phantom continua a ricaricare la pagina fino a quando la checkLoaded()
funzione non ritorna vera. Perché? questo perché la mia SPA specifica effettua diverse chiamate AJAX per ottenere tutti i dati e inserirli nel DOM sulla mia pagina, e il fantasma non può sapere quando tutte le chiamate sono state completate prima di restituirmi il riflesso HTML del DOM. Quello che ho fatto qui è dopo l'ultima chiamata AJAX aggiungo un <span id='compositionComplete'></span>
, in modo che se esiste questo tag so che il DOM è completato. Lo faccio in risposta compositionComplete
all'evento di Durandal , vedi quiper più. Se ciò non accade entro 10 secondi, mi arrendo (dovrebbe impiegare solo un secondo per farlo). L'HTML restituito contiene tutti i collegamenti che l'utente vede nel browser. Lo script non funzionerà correttamente perché i <script>
tag presenti nell'istantanea HTML non fanno riferimento all'URL corretto. Questo può essere modificato anche nel file phantom javascript, ma non credo sia necessario perché lo snapshot HTML viene utilizzato solo da Google per ottenere i a
collegamenti e non eseguire javascript; questi collegamenti fanno riferimento a un URL grazioso e, in realtà, se provi a vedere l'istantanea HTML in un browser, otterrai errori javascript ma tutti i collegamenti funzioneranno correttamente e ti indirizzeranno di nuovo al server con un URL grazioso questa volta ottenere la pagina completamente funzionante.
Questo è. Ora il server sa come gestire URL belli e brutti, con push-state abilitato sia sul server che sul client. Tutti gli URL brutti vengono trattati allo stesso modo utilizzando Phantom, quindi non è necessario creare un controller separato per ogni tipo di chiamata.
Una cosa che si potrebbe preferire di cambiamento non è di fare chiamate un generale 'categoria / sottocategoria / prodotto', ma di aggiungere un 'negozio' in modo che il link sarà simile: http://www.xyz.com/store/category/subCategory/product111
. Ciò eviterà il problema nella mia soluzione che tutti gli URL non validi vengano trattati come se fossero effettivamente chiamate al controller "indice" e suppongo che questi possano essere gestiti quindi all'interno del controller "store" senza l'aggiunta a quella web.config
mostrata sopra .