Başlamadan önce lütfen google'ın neye ihtiyacı olduğunu , özellikle de güzel ve çirkin URL'leri kullandığınızdan emin olun . Şimdi uygulamayı görelim:
Müşteri Tarafı
İstemci tarafında, sunucu ile AJAX çağrıları aracılığıyla dinamik olarak etkileşime giren tek bir html sayfanız vardır. SPA bununla ilgili. a
İstemci tarafındaki tüm etiketler uygulamamda dinamik olarak oluşturulur, daha sonra bu bağlantıların google'ın sunucudaki botuna nasıl görünür hale getirileceğini göreceğiz. Bu tür a
her bir etiketin, google'ın botunun onu taraması pretty URL
için href
etikette bir olması gerekir. Sen istemiyoruz href
biz yüke yeni bir sayfa istemeyebilir, çünkü (daha sonra göreceksiniz, sunucu bunu ayrıştırmak mümkün istiyoruz olsa bile) kısmı üzerinde istemci tıklama kullanılacak yalnızca sayfanın bir bölümünde görüntülenecek bazı verileri almak üzere bir AJAX çağrısı yapmak ve URL'yi javascript (ör . HTML5 pushstate
veya ile Durandaljs
) kullanarak değiştirmek . Yani, ikimiz dehref
google özelliği onclick
ve kullanıcı bağlantıyı tıkladığında işi yapan özelliktir . Şimdi, kullandığım için URL'de push-state
hiçbir şey istemiyorum #
, bu yüzden tipik bir a
etiket şöyle görünebilir:
<a href="http://www.xyz.com/#!/category/subCategory/product111" onClick="loadProduct('category','subCategory','product111')>see product111...</a>
'kategori' ve 'altKategori' muhtemelen 'iletişim' ve 'telefonlar' veya 'bilgisayarlar' gibi diğer ifadeler olacaktır ve elektrikli ev aletleri mağazası için 'dizüstü bilgisayarlar'. Açıkçası birçok farklı kategori ve alt kategori olacaktır. Gördüğünüz gibi, bağlantı doğrudan kategori, alt kategori ve ürüne yöneliktir, gibi belirli bir 'mağaza' sayfasına ekstra parametre olarak değil http://www.xyz.com/store/category/subCategory/product111
. Çünkü daha kısa ve daha basit bağlantıları tercih ediyorum. 'Sayfalarımdan biriyle aynı ada sahip bir kategori olmayacağımı, yani'
Ben AJAX ( onclick
bölüm) üzerinden veri yüklemek için google üzerinde arama içine girmeyeceğim , birçok iyi açıklamalar vardır. Burada bahsetmek istediğim tek önemli şey, kullanıcı bu bağlantıyı tıkladığında, tarayıcıdaki URL'nin şöyle görünmesini istediğidir:
http://www.xyz.com/category/subCategory/product111
. Ve bu URL sunucuya gönderilmez! Unutmayın, bu istemci ve sunucu arasındaki tüm etkileşimin AJAX aracılığıyla yapıldığı, hiç bağlantı olmadığı bir SPA! tüm 'sayfalar' istemci tarafında uygulanır ve farklı URL sunucuya çağrı yapmaz (sunucunun başka bir siteden sitenize harici bağlantılar olarak kullanılması durumunda bu URL'lerin nasıl ele alınacağını bilmesi gerekir, bunu daha sonra sunucu tarafında göreceğiz). Şimdi, bu Durandal tarafından harika bir şekilde ele alınmaktadır. Şiddetle tavsiye ederim, ancak diğer teknolojileri tercih ediyorsanız bu bölümü de atlayabilirsiniz. Bunu seçerseniz ve aynı zamanda benim gibi Web için MS Visual Studio Express 2012 kullanıyorsanız, Durandal Başlangıç Seti'ni yükleyebilir ve burada şöyle bir shell.js
şey kullanabilirsiniz:
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 });
}
};
});
Burada dikkat edilmesi gereken birkaç önemli nokta var:
- İlk rota (ile
route:''
) içinde fazladan veri olmayan URL içindir, yani http://www.xyz.com
. Bu sayfada, AJAX kullanarak genel verileri yüklersiniz. Aslında a
bu sayfada hiç etiket olmayabilir . O google'ın bot onunla ne yapacağını bilecek böylece aşağıdaki etiketi eklemek isteyeceksiniz:
<meta name="fragment" content="!">
. Bu etiket, Google'ın botunun www.xyz.com?_escaped_fragment_=
daha sonra göreceğimiz URL'yi dönüştürmesini sağlayacaktır .
- 'Yaklaşık' rota, web uygulamanızda isteyebileceğiniz diğer 'sayfalara' bağlantıya sadece bir örnektir.
- Şimdi, zor olan kısım 'kategori' güzergahı olmaması ve hiçbiri önceden tanımlanmış bir güzergahı olmayan birçok farklı kategori olabilir. Bu, devreye girdiği yerdir
mapUnknownRoutes
. Bu bilinmeyen rotaları 'mağaza' yoluna eşler ve ayrıca '!' pretty URL
Google'ın arama motoru tarafından oluşturulması durumunda URL'den . 'Store' yolu 'fragment' özelliğindeki bilgileri alır ve verileri almak, görüntülemek ve URL'yi yerel olarak değiştirmek için AJAX çağrısı yapar. Uygulamamda, bu tür her arama için farklı bir sayfa yüklemiyorum; Sayfanın yalnızca bu verilerin alakalı olduğu bölümünü değiştiriyorum ve URL'yi yerel olarak değiştiriyorum.
pushState:true
Hangisinin Durandal'a push state URL'lerini kullanma talimatı verdiğine dikkat edin .
Müşteri tarafında ihtiyacımız olan bu. Karma URL'lerle de uygulanabilir (Durandal'da bunun pushState:true
için basitçe kaldırın ). Daha karmaşık olan kısım (en azından benim için ...) sunucu kısmıydı:
Sunucu Tarafı
Ben kullanıyorum MVC 4.5
ile sunucu tarafında WebAPI
kontrolörleri. Sunucu aslında 3 URL'lerin türlerini işlemek gerekir: google tarafından oluşturulan olanları - her ikisi pretty
ve ugly
hem de müşterinin tarayıcısında göründüğünü o aynı biçimde bir 'basit' URL. Bunu nasıl yapacağımıza bakalım:
Güzel URL'ler ve 'basit' olanlar önce sunucu tarafından varolmayan bir denetleyiciye başvurmaya çalışıyormuş gibi yorumlanır. Sunucu benzer bir şey görür http://www.xyz.com/category/subCategory/product111
ve 'kategori' adlı bir denetleyici arar. Bu yüzden web.config
bunları belirli bir hata işleme denetleyicisine yönlendirmek için aşağıdaki satırı ekliyorum:
<customErrors mode="On" defaultRedirect="Error">
<error statusCode="404" redirect="Error" />
</customErrors><br/>
Şimdi, böyle bir şey URL'yi dönüştürür: http://www.xyz.com/Error?aspxerrorpath=/category/subCategory/product111
. URL AJAX üzerinden veri yükleyecek istemciye gönderilmesini istiyorum, bu yüzden burada hile herhangi bir denetleyiciye başvuruyormuş gibi varsayılan 'dizin' denetleyicisi çağırmak için; Tüm 'kategori' ve 'altKategori' parametrelerinden önce URL'ye bir karma ekleyerek bunu yaparım ; karma URL, varsayılan 'index' denetleyicisi dışında herhangi bir özel denetleyici gerektirmez ve veriler istemciye gönderilir; bu, daha sonra karma değerini kaldırır ve verileri AJAX aracılığıyla yüklemek için karma işleminden sonra bilgileri kullanır. Hata işleyici denetleyici kodu:
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;
}
}
}
Peki ya Çirkin URL'ler ? Bunlar, Google'ın botu tarafından oluşturulur ve kullanıcının tarayıcıda gördüğü tüm verileri içeren düz HTML döndürmelidir. Bunun için phantomjs kullanıyorum . Phantom, tarayıcının istemci tarafında - ancak sunucu tarafında - yaptığı işi yapan başsız bir tarayıcıdır. Başka bir deyişle, fantom (diğer şeylerin yanı sıra) bir URL aracılığıyla bir web sayfasını nasıl alacağınızı bilir, içindeki tüm javascript kodunu çalıştırmayı (AJAX çağrıları yoluyla veri almayı da içerir) dahil eder ve size yansıtan HTML'yi geri verir. DOM. MS Visual Studio Express kullanıyorsanız birçoğu bu bağlantı üzerinden hayalet yüklemek istersiniz .
Ancak önce sunucuya çirkin bir URL gönderildiğinde onu yakalamamız gerekir; Bunun için, 'App_start' klasörüne aşağıdaki dosyayı ekledim:
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;
}
}
}
Bu, 'App_start' öğesinde de 'filterConfig.cs' den çağrılır:
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());
}
}
}
Gördüğünüz gibi, 'AjaxCrawlableAttribute' çirkin URL'leri 'HtmlSnapshot' adlı bir denetleyiciye yönlendirir ve işte bu denetleyici:
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();
}
}
}
İlişkili view
çok basit, sadece bir kod satırı:
@Html.Raw( ViewBag.result )
Denetleyicide görebileceğiniz gibi, fantom, createSnapshot.js
oluşturduğum bir klasörün altında adlandırılan bir javascript dosyası yükler seo
. İşte bu javascript dosyası:
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);
İlk önce :-) temel kodu aldığım sayfa için Thomas Davis'e teşekkür etmek istiyorum .
Burada tuhaf bir şey fark edeceksiniz: phantom, checkLoaded()
işlev true olana kadar sayfayı yeniden yüklemeye devam eder . Neden? Bunun nedeni, özel SPA'mın tüm verileri almak ve sayfamdaki DOM'a yerleştirmek için birkaç AJAX çağrısı yapması ve fantom, DOM'un HTML yansımasını geri döndürmeden önce tüm çağrıların ne zaman tamamlandığını bilememesi. Burada yaptığım son AJAX çağrısından sonra ekliyorum <span id='compositionComplete'></span>
, bu etiket varsa DOM'un tamamlandığını biliyorum. Bunu Durandal'ın compositionComplete
olayına yanıt olarak yapıyorum , buraya bakındaha fazlası için. Eğer bu 10 saniye içinde olmazsa, vazgeçerim (en fazla sadece bir saniye sürmelidir). Döndürülen HTML, kullanıcının tarayıcıda gördüğü tüm bağlantıları içerir. <script>
HTML anlık görüntüsünde bulunan etiketler doğru URL'ye başvurmadığından komut dosyası düzgün çalışmaz . Bu çok javascript phantom dosyasında değiştirilebilir, ancak HTML snapshort sadece google tarafından a
bağlantıları almak ve javascript çalıştırmak için kullanılan çünkü bu gerekli olduğunu sanmıyorum ; bu bağlantıları yapmak referansı oldukça URL ve bir tarayıcıda HTML anlık görmeye çalışırsanız aslında, eğer javascript hatası alacağını ancak tüm bağlantılar düzgün çalışması ve güzel bir URL'ye bu kez bir kez daha sunucuya yönlendirir tam çalışma sayfası elde.
Budur. Artık sunucu, hem sunucuda hem de istemcide push-state etkinken hem güzel hem de çirkin URL'lerin nasıl işleneceğini biliyor. Tüm çirkin URL'ler, fantom kullanılarak aynı şekilde ele alınır, böylece her arama türü için ayrı bir denetleyici oluşturmaya gerek yoktur.
Değiştirmeyi tercih edebileceğiniz bir şey, genel bir 'category / subCategory / product' çağrısı yapmak değil, bağlantının şöyle görünmesi için bir 'mağaza' eklemektir http://www.xyz.com/store/category/subCategory/product111
. Bu, çözümümdeki tüm geçersiz URL'lerin gerçekten 'dizin' denetleyicisine çağrı yapıyormuş gibi ele alınmasını önler ve bunların web.config
yukarıda gösterilen I eki olmadan daha sonra 'mağaza' denetleyicisi içinde ele alınabileceğini düşünüyorum .