Trước khi bắt đầu, hãy chắc chắn rằng bạn hiểu những gì google yêu cầu , đặc biệt là việc sử dụng các URL đẹp và xấu . Bây giờ hãy xem việc thực hiện:
Phía khách hàng
Về phía máy khách, bạn chỉ có một trang html duy nhất tương tác với máy chủ một cách linh hoạt thông qua các cuộc gọi AJAX. đó là những gì SPA nói về. Tất cả các a
thẻ ở phía máy khách được tạo động trong ứng dụng của tôi, sau này chúng ta sẽ xem cách làm cho các liên kết này hiển thị với bot của google trong máy chủ. Mỗi ví dụ a
nhu cầu thẻ để có thể có một pretty URL
trong các href
từ khóa để bot của google sẽ bóc tách nó. Bạn không muốn href
phần này được sử dụng khi khách hàng nhấp vào nó (mặc dù bạn muốn máy chủ có thể phân tích cú pháp, chúng ta sẽ thấy phần đó sau), vì chúng tôi có thể không muốn tải trang mới, chỉ để thực hiện cuộc gọi AJAX nhận được một số dữ liệu được hiển thị trong một phần của trang và thay đổi URL qua javascript (ví dụ: sử dụng HTML5 pushstate
hoặc với Durandaljs
). Vì vậy, chúng tôi có cả mộthref
thuộc tính cho google cũng như trên onclick
đó thực hiện công việc khi người dùng nhấp vào liên kết. Bây giờ, vì tôi sử dụng push-state
nên tôi không muốn bất kỳ #
URL nào, vì vậy một a
thẻ thông thường có thể trông như thế này:
<a href="http://www.xyz.com/#!/category/subCategory/product111" onClick="loadProduct('category','subCategory','product111')>see product111...</a>
'thể loại' và 'Danh mục con' có thể là các cụm từ khác, chẳng hạn như 'giao tiếp' và 'điện thoại' hoặc 'máy tính' và 'máy tính xách tay' cho một cửa hàng thiết bị điện. Rõ ràng sẽ có nhiều danh mục và danh mục phụ khác nhau. Như bạn có thể thấy, liên kết trực tiếp đến danh mục, danh mục phụ và sản phẩm, không phải là tham số phụ cho trang 'cửa hàng' cụ thể, chẳng hạn như http://www.xyz.com/store/category/subCategory/product111
. Điều này là do tôi thích các liên kết ngắn hơn và đơn giản hơn. Nó ngụ ý rằng tôi sẽ không có một danh mục có cùng tên với một trong những 'trang' của tôi, tức là '
Tôi sẽ không đi sâu vào cách tải dữ liệu qua AJAX ( onclick
phần), tìm kiếm nó trên google, có nhiều lời giải thích hay. Điều quan trọng duy nhất ở đây mà tôi muốn đề cập là khi người dùng nhấp vào liên kết này, tôi muốn URL trong trình duyệt trông như thế này:
http://www.xyz.com/category/subCategory/product111
. Và đây là URL không được gửi đến máy chủ! hãy nhớ rằng, đây là một SPA nơi tất cả các tương tác giữa máy khách và máy chủ được thực hiện thông qua AJAX, không có liên kết nào cả! tất cả 'trang' được triển khai ở phía máy khách và URL khác nhau không thực hiện cuộc gọi đến máy chủ (máy chủ cần biết cách xử lý các URL này trong trường hợp chúng được sử dụng làm liên kết ngoài từ trang web khác đến trang web của bạn, chúng ta sẽ thấy điều đó sau trong phần phía máy chủ). Bây giờ, điều này được Durandal xử lý tuyệt vời. Tôi thực sự khuyên bạn nên dùng nó, nhưng bạn cũng có thể bỏ qua phần này nếu bạn thích các công nghệ khác. Nếu bạn chọn nó và bạn cũng đang sử dụng MS Visual Studio Express 2012 cho Web như tôi, bạn có thể cài đặt Durandal Starter Kit , và ở đó shell.js
, sử dụng một cái gì đó như thế này:
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 });
}
};
});
Có một vài điều quan trọng cần chú ý ở đây:
- Tuyến đầu tiên (có
route:''
) dành cho URL không có dữ liệu bổ sung trong đó, nghĩa là http://www.xyz.com
. Trong trang này, bạn tải dữ liệu chung bằng AJAX. Thực tế có thể không có a
thẻ nào trong trang này. Bạn sẽ muốn thêm thẻ sau để bot của google sẽ biết phải làm gì với nó :
<meta name="fragment" content="!">
. Thẻ này sẽ khiến bot của google biến đổi URL www.xyz.com?_escaped_fragment_=
mà chúng ta sẽ thấy sau này.
- Lộ trình 'about' chỉ là một ví dụ cho một liên kết đến các 'trang' khác mà bạn có thể muốn trên ứng dụng web của mình.
- Bây giờ, phần khó khăn là không có tuyến đường 'loại' và có thể có nhiều loại khác nhau - không có loại nào có tuyến đường được xác định trước. Đây là nơi
mapUnknownRoutes
đến. Nó ánh xạ các tuyến đường không xác định này đến tuyến đường 'cửa hàng' và cũng loại bỏ bất kỳ '!' từ URL trong trường hợp nó pretty URL
được tạo bởi công cụ tìm kiếm của google. Tuyến đường 'cửa hàng' lấy thông tin trong thuộc tính 'mảnh' và thực hiện cuộc gọi AJAX để lấy dữ liệu, hiển thị nó và thay đổi URL cục bộ. Trong ứng dụng của mình, tôi không tải một trang khác cho mỗi cuộc gọi như vậy; Tôi chỉ thay đổi một phần của trang nơi dữ liệu này có liên quan và cũng thay đổi URL cục bộ.
- Lưu ý
pushState:true
hướng dẫn Durandal sử dụng URL trạng thái đẩy.
Đây là tất cả những gì chúng tôi cần ở phía khách hàng. Nó cũng có thể được thực hiện với các URL được băm (trong Durandal, bạn chỉ cần xóa phần pushState:true
đó). Phần phức tạp hơn (ít nhất là đối với tôi ...) là phần máy chủ:
Phía máy chủ
Tôi đang sử dụng MVC 4.5
ở phía máy chủ với WebAPI
bộ điều khiển. Các máy chủ thực sự cần phải xử lý 3 loại URL: những người được tạo ra bởi google - cả hai pretty
và ugly
và cũng là một 'đơn giản' URL với định dạng giống như một trong đó sẽ xuất hiện trong trình duyệt của khách hàng. Hãy xem cách làm điều này:
Các URL đẹp và 'đơn giản' trước tiên được máy chủ diễn giải như thể cố gắng tham chiếu bộ điều khiển không tồn tại. Máy chủ nhìn thấy một cái gì đó giống như http://www.xyz.com/category/subCategory/product111
và tìm kiếm một bộ điều khiển có tên 'thể loại'. Vì vậy, trong web.config
tôi thêm dòng sau để chuyển hướng chúng đến bộ điều khiển xử lý lỗi cụ thể:
<customErrors mode="On" defaultRedirect="Error">
<error statusCode="404" redirect="Error" />
</customErrors><br/>
Bây giờ, điều này biến đổi URL thành một cái gì đó như : http://www.xyz.com/Error?aspxerrorpath=/category/subCategory/product111
. Tôi muốn URL được gửi đến máy khách sẽ tải dữ liệu qua AJAX, vì vậy mẹo ở đây là gọi bộ điều khiển 'index' mặc định như thể không tham chiếu bất kỳ bộ điều khiển nào; Tôi làm điều đó bằng cách thêm một hàm băm vào URL trước tất cả các tham số 'thể loại' và 'danh mục con'; URL băm không yêu cầu bất kỳ bộ điều khiển đặc biệt nào ngoại trừ bộ điều khiển 'index' mặc định và dữ liệu được gửi đến máy khách sau đó loại bỏ hàm băm và sử dụng thông tin sau khi băm để tải dữ liệu qua AJAX. Đây là mã điều khiển xử lý lỗi:
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;
}
}
}
Nhưng những gì về các URL xấu xí ? Chúng được tạo bởi bot của google và sẽ trả về HTML đơn giản chứa tất cả dữ liệu mà người dùng nhìn thấy trong trình duyệt. Đối với điều này, tôi sử dụng ph Phantomjs . Phantom là một trình duyệt không đầu làm những gì trình duyệt đang làm ở phía máy khách - nhưng ở phía máy chủ. Nói cách khác, ph Phantom biết (trong số những thứ khác) làm thế nào để có được một trang web thông qua một URL, phân tích nó bao gồm chạy tất cả mã javascript trong đó (cũng như nhận dữ liệu qua các cuộc gọi AJAX) và cung cấp cho bạn HTML phản ánh DOM. Nếu bạn đang sử dụng MS Visual Studio Express, nhiều người muốn cài đặt ảo thông qua liên kết này .
Nhưng trước tiên, khi một URL xấu xí được gửi đến máy chủ, chúng ta phải bắt nó; Đối với điều này, tôi đã thêm vào thư mục 'App_start' tệp sau:
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;
}
}
}
Điều này được gọi từ 'filterConfig.cs' cũng trong '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());
}
}
}
Như bạn có thể thấy, 'AjaxCrawlableAttribution' định tuyến các URL xấu đến bộ điều khiển có tên 'HtmlSnapshot' và đây là bộ điều khiển này:
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ên kết view
rất đơn giản, chỉ cần một dòng mã:
@Html.Raw( ViewBag.result )
Như bạn có thể thấy trong bộ điều khiển, ph Phantom tải một tệp javascript có tên createSnapshot.js
trong thư mục mà tôi đã tạo được gọi seo
. Đây là tập tin javascript này:
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);
Trước tiên tôi muốn cảm ơn Thomas Davis cho trang mà tôi đã nhận được mã cơ bản từ :-).
Bạn sẽ nhận thấy một điều kỳ lạ ở đây: ph Phantom tiếp tục tải lại trang cho đến khi checkLoaded()
hàm trả về đúng. Tại sao vậy? điều này là do SPA cụ thể của tôi thực hiện một số cuộc gọi AJAX để lấy tất cả dữ liệu và đặt nó vào DOM trên trang của tôi và ảo không thể biết khi nào tất cả các cuộc gọi đã hoàn thành trước khi trả lại cho tôi phản ánh HTML của DOM. Những gì tôi đã làm ở đây là sau cuộc gọi AJAX cuối cùng tôi thêm một <span id='compositionComplete'></span>
, để nếu thẻ này tồn tại tôi biết DOM đã hoàn thành. Tôi làm điều này để đáp lại compositionComplete
sự kiện của Durandal , xem tại đâyđể biết thêm Nếu điều này không xảy ra trong 10 giây, tôi sẽ bỏ cuộc (chỉ mất một giây để có nhiều nhất). HTML được trả về chứa tất cả các liên kết mà người dùng nhìn thấy trong trình duyệt. Tập lệnh sẽ không hoạt động chính xác vì các <script>
thẻ tồn tại trong ảnh chụp nhanh HTML không tham chiếu đúng URL. Điều này cũng có thể được thay đổi trong tệp ảo javascript, nhưng tôi không nghĩ đây là cần thiết vì snapshort HTML chỉ được google sử dụng để lấy các a
liên kết và không chạy javascript; các liên kết này tham chiếu một URL đẹp và nếu thực tế, nếu bạn cố gắng xem ảnh chụp nhanh HTML trong trình duyệt, bạn sẽ gặp lỗi javascript nhưng tất cả các liên kết sẽ hoạt động bình thường và đưa bạn đến máy chủ một lần nữa với một URL đẹp nhận được trang làm việc đầy đủ.
Đây là nó. Bây giờ máy chủ đã biết cách xử lý cả URL đẹp và xấu, với trạng thái đẩy được bật trên cả máy chủ và máy khách. Tất cả các URL xấu được xử lý theo cùng một cách sử dụng ảo, do đó không cần phải tạo một bộ điều khiển riêng cho từng loại cuộc gọi.
Một điều bạn có thể muốn thay đổi không phải là thực hiện cuộc gọi 'danh mục / danh mục con / sản phẩm' chung chung mà là thêm một 'cửa hàng' để liên kết sẽ trông giống như : http://www.xyz.com/store/category/subCategory/product111
. Điều này sẽ tránh được vấn đề trong giải pháp của tôi là tất cả các URL không hợp lệ được xử lý như thể chúng thực sự được gọi đến bộ điều khiển 'index' và tôi cho rằng chúng có thể được xử lý sau đó trong bộ điều khiển 'store' mà không cần thêm vào trình web.config
bày ở trên .