Antes de começar, entenda o que o Google exige , principalmente o uso de URLs bonitos e feios . Agora vamos ver a implementação:
Lado do Cliente
No lado do cliente, você possui apenas uma única página html que interage dinamicamente com o servidor por meio de chamadas AJAX. é disso que se trata o SPA. Todas as a
tags no lado do cliente são criadas dinamicamente no meu aplicativo. Mais adiante, veremos como tornar esses links visíveis ao bot do google no servidor. Cada uma dessas a
tags precisa ter um pretty URL
na href
tag para que o bot do Google o rastreie. Você não deseja que a href
peça seja usada quando o cliente clicar nela (mesmo que você queira que o servidor possa analisá-la, veremos isso mais tarde), porque talvez não desejemos que uma nova página seja carregada, apenas para fazer uma chamada AJAX, exibindo alguns dados em parte da página e alterar o URL via javascript (por exemplo, usando HTML5 pushstate
ou com Durandaljs
). Então, nós temos umhref
atributo para o google, bem como sobre onclick
qual faz o trabalho quando o usuário clica no link. Agora, como eu push-state
não quero usar nenhum #
URL, uma a
tag típica pode ficar assim:
<a href="http://www.xyz.com/#!/category/subCategory/product111" onClick="loadProduct('category','subCategory','product111')>see product111...</a>
'category' e 'subCategory' provavelmente seriam outras frases, como 'comunicação' e 'telefones' ou 'computadores' e 'laptops' para uma loja de eletrodomésticos. Obviamente, haveria muitas categorias e subcategorias diferentes. Como você pode ver, o link é diretamente para a categoria, subcategoria e produto, não como parâmetros extras para uma página específica da "loja", como http://www.xyz.com/store/category/subCategory/product111
. Isso ocorre porque eu prefiro links mais curtos e simples. Isso implica que não haverá uma categoria com o mesmo nome que uma das minhas 'páginas', ou seja, '
Não vou entrar em como carregar os dados via AJAX (a onclick
parte), pesquisá-los no google, há muitas boas explicações. A única coisa importante aqui que quero mencionar é que, quando o usuário clica neste link, quero que o URL no navegador fique assim:
http://www.xyz.com/category/subCategory/product111
. E este URL não é enviado para o servidor! lembre-se, este é um SPA onde toda a interação entre o cliente e o servidor é feita via AJAX, sem links! todas as 'páginas' são implementadas no lado do cliente e o URL diferente não faz uma chamada para o servidor (o servidor precisa saber como lidar com esses URLs caso sejam usados como links externos de outro site para o site, veremos isso mais tarde na parte do servidor). Agora, isso é tratado maravilhosamente por Durandal. Eu recomendo fortemente, mas você também pode pular esta parte se preferir outras tecnologias. Se você escolher, e também estiver usando o MS Visual Studio Express 2012 para Web como eu, poderá instalar o Durandal Starter Kit e, em seguida shell.js
, usar algo como isto:
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 });
}
};
});
Há algumas coisas importantes a serem observadas aqui:
- A primeira rota (com
route:''
) é para a URL que não possui dados extras, ou seja http://www.xyz.com
. Nesta página, você carrega dados gerais usando o AJAX. Na verdade, pode não haver a
tags nesta página. Você vai querer adicionar a seguinte tag de modo bot que o Google vai saber o que fazer com ele:
<meta name="fragment" content="!">
. Essa tag fará o bot do google transformar o URL para o www.xyz.com?_escaped_fragment_=
qual veremos mais adiante.
- A rota 'about' é apenas um exemplo para um link para outras 'páginas' que você pode querer no seu aplicativo da web.
- Agora, a parte complicada é que não há rota de 'categoria' e pode haver muitas categorias diferentes - nenhuma das quais tem uma rota predefinida. É aqui que
mapUnknownRoutes
entra. Ele mapeia essas rotas desconhecidas para a rota 'store' e também remove qualquer '!' do URL, caso seja pretty URL
gerado pelo mecanismo de pesquisa do Google. A rota 'store' pega as informações na propriedade 'fragment' e faz a chamada AJAX para obter os dados, exibi-los e alterar o URL localmente. No meu aplicativo, não carrego uma página diferente para cada chamada; Apenas altero a parte da página em que esses dados são relevantes e também altero o URL localmente.
- Observe o
pushState:true
que instrui Durandal a usar URLs de estado de envio.
É tudo o que precisamos no lado do cliente. Ele pode ser implementado também com URLs com hash (em Durandal, você simplesmente remove o pushState:true
para isso). A parte mais complexa (pelo menos para mim ...) foi a parte do servidor:
Lado do servidor
Estou usando MVC 4.5
no lado do servidor com WebAPI
controladores. O servidor realmente precisa lidar com três tipos de URLs: os gerados pelo google - ambos pretty
e ugly
também um URL 'simples' com o mesmo formato que o que aparece no navegador do cliente. Vamos ver como fazer isso:
URLs bonitas e 'simples' são primeiro interpretadas pelo servidor como se tentassem fazer referência a um controlador inexistente. O servidor vê algo parecido http://www.xyz.com/category/subCategory/product111
e procura um controlador chamado 'categoria'. Então web.config
, adiciono a seguinte linha para redirecioná-las para um controlador de tratamento de erros específico:
<customErrors mode="On" defaultRedirect="Error">
<error statusCode="404" redirect="Error" />
</customErrors><br/>
Agora, isso transforma o URL para algo como: http://www.xyz.com/Error?aspxerrorpath=/category/subCategory/product111
. Quero que a URL seja enviada ao cliente que carregará os dados via AJAX, portanto, o truque aqui é chamar o controlador 'index' padrão como se não estivesse fazendo referência a nenhum controlador; Eu faço isso adicionando um hash ao URL antes de todos os parâmetros 'category' e 'subCategory'; o URL do hash não requer nenhum controlador especial, exceto o controlador 'index' padrão e os dados são enviados ao cliente, que remove o hash e usa as informações após o hash para carregar os dados via AJAX. Aqui está o código do controlador do manipulador de erros:
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;
}
}
}
Mas e os URLs feios ? Eles são criados pelo bot do Google e devem retornar HTML simples que contém todos os dados que o usuário vê no navegador. Para isso eu uso phantomjs . O Phantom é um navegador sem cabeça que faz o que o navegador está fazendo no lado do cliente - mas no lado do servidor. Em outras palavras, o phantom sabe (entre outras coisas) como obter uma página da Web por meio de uma URL, analisá-la, incluindo a execução de todo o código javascript (e também a obtenção de dados por chamadas AJAX) e devolver o HTML que reflete o DOM. Se você estiver usando o MS Visual Studio Express, muitos desejam instalar o phantom através deste link .
Mas primeiro, quando uma URL feia é enviada para o servidor, precisamos capturá-la; Para isso, adicionei à pasta 'App_start' o seguinte arquivo:
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;
}
}
}
Isso é chamado de 'filterConfig.cs' também em '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());
}
}
}
Como você pode ver, 'AjaxCrawlableAttribute' roteia URLs feias para um controlador chamado 'HtmlSnapshot' e aqui está este controlador:
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();
}
}
}
O associado view
é muito simples, apenas uma linha de código:
@Html.Raw( ViewBag.result )
Como você pode ver no controlador, o phantom carrega um arquivo javascript chamado createSnapshot.js
em uma pasta que criei chamadaseo
. Aqui está este arquivo 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);
Em primeiro lugar, quero agradecer a Thomas Davis pela página em que obtive o código básico de :-).
Você notará algo estranho aqui: o fantasma continua recarregando a página até mais. Se isso não acontecer dentro de 10 segundos, desisto (levará apenas um segundo para o máximo). O HTML retornado contém todos os links que o usuário vê no navegador. O script não funcionará corretamente porque as tags existentes no instantâneo HTML não fazem referência ao URL correto. Isso também pode ser alterado no arquivo fantasma javascript, mas não acho necessário, porque o snapshort HTML é usado apenas pelo google para obter os links e não para executar o javascript; esses linkscheckLoaded()
função retorne verdadeira. Por que é que? isso ocorre porque meu SPA específico faz várias chamadas AJAX para obter todos os dados e colocá-los no DOM na minha página, e o phantom não pode saber quando todas as chamadas foram concluídas antes de retornar o reflexo HTML do DOM. O que fiz aqui é após a chamada final do AJAX, adiciono um <span id='compositionComplete'></span>
, para que, se essa tag existir, eu saiba que o DOM está concluído. Eu faço isso em resposta ao compositionComplete
evento de Durandal , veja aqui<script>
a
fazem referência a um URL bonito e, se for o caso, se você tentar ver o instantâneo HTML em um navegador, você receberá erros de javascript, mas todos os links funcionarão corretamente e direcionarão você ao servidor mais uma vez com um URL bonito dessa vez obtendo a página totalmente funcional.
É isso. Agora, o servidor sabe como lidar com URLs bonitas e feias, com o estado push ativado no servidor e no cliente. Todos os URLs feios são tratados da mesma maneira usando o phantom; portanto, não há necessidade de criar um controlador separado para cada tipo de chamada.
Uma coisa que você pode preferir a mudança não é para fazer a chamada uma 'categoria / subcategoria / produto' geral, mas para adicionar um 'loja' para que o link será algo parecido com: http://www.xyz.com/store/category/subCategory/product111
. Isso evitará o problema na minha solução de que todos os URLs inválidos são tratados como se fossem realmente chamadas para o controlador 'index', e suponho que eles possam ser tratados no controlador 'store' sem a adição web.config
mostrada acima. .