ก่อนเริ่มต้นโปรดตรวจสอบให้แน่ใจว่าคุณเข้าใจว่า Google ต้องการอะไรโดยเฉพาะการใช้URL ที่น่ารักและน่าเกลียด ตอนนี้ให้ดูการใช้งาน:
ด้านลูกค้า
ในฝั่งไคลเอ็นต์คุณมีหน้า html เดียวซึ่งโต้ตอบกับเซิร์ฟเวอร์แบบไดนามิกผ่านการโทร AJAX นั่นคือสิ่งที่สปาเป็นเรื่องเกี่ยวกับ a
แท็กทั้งหมดในฝั่งไคลเอ็นต์นั้นสร้างขึ้นในแอปพลิเคชันของฉันแบบไดนามิกในภายหลังเราจะเห็นวิธีที่จะทำให้ลิงก์เหล่านี้มองเห็นบอทของ Google ในเซิร์ฟเวอร์ แต่ละa
แท็กดังกล่าวจะต้องสามารถมีpretty URL
ในhref
แท็กเพื่อให้บอทของ Google จะรวบรวมข้อมูล คุณไม่ต้องการให้ชิ้นhref
ส่วนถูกใช้เมื่อไคลเอนต์คลิกที่มัน (แม้ว่าคุณต้องการให้เซิร์ฟเวอร์แยกวิเคราะห์ได้เราจะเห็นว่าในภายหลัง) เนื่องจากเราอาจไม่ต้องการให้โหลดหน้าใหม่ เพื่อโทร AJAX เพื่อรับข้อมูลบางส่วนที่จะแสดงในส่วนของหน้าและเปลี่ยน URL ผ่านทาง javascript (เช่นใช้ HTML5 pushstate
หรือด้วยDurandaljs
) ดังนั้นเราจึงมีทั้งhref
คุณลักษณะสำหรับ google เช่นเดียวกับonclick
ที่ทำงานเมื่อผู้ใช้คลิกที่ลิงค์ ขณะนี้เนื่องจากฉันใช้push-state
ฉันไม่ต้องการ#
URL ใด ๆดังนั้นa
แท็กทั่วไปอาจมีลักษณะเช่นนี้:
<a href="http://www.xyz.com/#!/category/subCategory/product111" onClick="loadProduct('category','subCategory','product111')>see product111...</a>
'หมวดหมู่' และ 'หมวดหมู่ย่อย' อาจเป็นวลีอื่น ๆ เช่น 'การสื่อสาร' และ 'โทรศัพท์' หรือ 'คอมพิวเตอร์' และ 'แล็ปท็อป' สำหรับร้านค้าเครื่องใช้ไฟฟ้า เห็นได้ชัดว่าจะมีหลายประเภทและหมวดย่อย ในขณะที่คุณสามารถดูการเชื่อมโยงโดยตรงกับหมวดหมู่หมวดหมู่ย่อยและผลิตภัณฑ์ที่ไม่เป็นพารามิเตอร์ที่เฉพาะเจาะจงเป็นพิเศษในหน้าเก็บ http://www.xyz.com/store/category/subCategory/product111
'เช่น นี่เป็นเพราะฉันต้องการลิงค์ที่สั้นกว่าและง่ายกว่า หมายความว่าฉันจะไม่มีหมวดหมู่ที่มีชื่อเดียวกับหนึ่งใน 'หน้า' ของฉันเช่น '
ฉันจะไม่เข้าไปดูวิธีการโหลดข้อมูลผ่าน AJAX ( onclick
ส่วนหนึ่ง) ค้นหาใน google มีคำอธิบายที่ดีมากมาย สิ่งสำคัญเพียงอย่างเดียวที่ฉันอยากพูดถึงคือเมื่อผู้ใช้คลิกที่ลิงค์นี้ฉันต้องการให้ URL ในเบราว์เซอร์มีลักษณะดังนี้:
http://www.xyz.com/category/subCategory/product111
. และนี่คือ URL จะไม่ถูกส่งไปยังเซิร์ฟเวอร์! โปรดจำไว้ว่านี่คือสปาที่การโต้ตอบทั้งหมดระหว่างไคลเอนต์และเซิร์ฟเวอร์ทำได้ผ่าน AJAX ไม่มีลิงก์เลย! 'หน้า' ทั้งหมดจะดำเนินการในฝั่งไคลเอ็นต์และ URL ที่แตกต่างไม่ได้โทรไปยังเซิร์ฟเวอร์ (เซิร์ฟเวอร์ไม่จำเป็นต้องรู้วิธีจัดการ URL เหล่านี้ในกรณีที่ใช้เป็นลิงก์ภายนอกจากไซต์อื่นไปยังเว็บไซต์ของคุณ เราจะเห็นในภายหลังในส่วนของเซิร์ฟเวอร์) ตอนนี้จัดการได้อย่างน่าอัศจรรย์โดย Durandal ฉันขอแนะนำอย่างยิ่ง แต่คุณสามารถข้ามส่วนนี้ได้หากคุณต้องการเทคโนโลยีอื่น ๆ หากคุณเลือกและคุณใช้ MS Visual Studio Express 2012 สำหรับเว็บอย่างฉันคุณสามารถติดตั้งชุดเริ่มต้น Durandalได้และshell.js
ใช้สิ่งต่อไปนี้ใน:
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 });
}
};
});
มีสิ่งสำคัญบางประการที่ควรสังเกตที่นี่:
- เส้นทางแรก (กับ
route:''
) สำหรับ URL http://www.xyz.com
ที่ยังไม่มีการเพิ่มข้อมูลในนั้นคือ ในหน้านี้คุณโหลดข้อมูลทั่วไปโดยใช้ AJAX อาจไม่มีa
แท็กเลยในหน้านี้ คุณจะต้องเพิ่มแท็กต่อไปนี้เพื่อให้บอทของ google รู้ว่าต้องทำอะไรกับมัน:
<meta name="fragment" content="!">
. แท็กนี้จะทำให้บอทของ Google เปลี่ยน URL www.xyz.com?_escaped_fragment_=
ที่เราจะเห็นในภายหลัง
- เส้นทาง 'เกี่ยวกับ' เป็นเพียงตัวอย่างของลิงก์ไปยัง 'หน้า' อื่น ๆ ที่คุณอาจต้องการในเว็บแอปพลิเคชันของคุณ
- ตอนนี้ส่วนที่ยุ่งยากก็คือไม่มีเส้นทาง 'หมวดหมู่' และอาจมีหมวดหมู่ที่แตกต่างกันมากมาย - ไม่มีส่วนใดที่มีเส้นทางที่กำหนดไว้ล่วงหน้า นี่คือที่
mapUnknownRoutes
มามันแผนที่เส้นทางที่ไม่รู้จักเหล่านี้ไปยังเส้นทาง 'เก็บ' และยังลบใด ๆ '!' จาก URL ในกรณีที่pretty URL
เครื่องมือค้นหาของ Google สร้างขึ้น เส้นทาง 'store' จะรับข้อมูลในคุณสมบัติ 'fragment' และทำการโทร AJAX เพื่อรับข้อมูลแสดงและเปลี่ยน URL ในเครื่อง ในแอปพลิเคชันของฉันฉันจะไม่โหลดหน้าอื่นสำหรับการโทรทุกครั้ง ฉันเปลี่ยนเฉพาะส่วนของหน้าที่ข้อมูลนี้มีความเกี่ยวข้องและเปลี่ยน URL ในเครื่อง
- ขอให้สังเกต
pushState:true
ว่าคำสั่งใดที่ Durandal ใช้ URL สถานะการพุช
นี่คือทั้งหมดที่เราต้องการในด้านลูกค้า สามารถนำไปใช้กับ URL ที่แฮช (ใน Durandal คุณสามารถลบได้อย่างง่ายดายpushState:true
) ส่วนที่ซับซ้อนมากขึ้น (อย่างน้อยสำหรับฉัน ... ) คือส่วนของเซิร์ฟเวอร์:
ฝั่งเซิร์ฟเวอร์
ฉันกำลังใช้MVC 4.5
ฝั่งเซิร์ฟเวอร์กับWebAPI
ตัวควบคุม เซิร์ฟเวอร์ต้องการจัดการ URL 3 ประเภทจริง ๆ : URL ที่สร้างขึ้นโดย google - ทั้งpretty
และugly
URL ที่ 'เรียบง่าย' ที่มีรูปแบบเดียวกับ URL ที่ปรากฏในเบราว์เซอร์ของลูกค้า ให้ดูที่วิธีการทำสิ่งนี้:
URL ที่ดีและเซิร์ฟเวอร์ที่ 'เรียบง่าย' จะถูกตีความเป็นครั้งแรกโดยเซิร์ฟเวอร์ราวกับว่าพยายามอ้างอิงคอนโทรลเลอร์ที่ไม่มีอยู่จริง เซิร์ฟเวอร์มองเห็นสิ่งที่ต้องการhttp://www.xyz.com/category/subCategory/product111
และค้นหาคอนโทรลเลอร์ที่มีชื่อว่า 'หมวดหมู่' ดังนั้นในweb.config
ฉันเพิ่มบรรทัดต่อไปนี้เพื่อเปลี่ยนเส้นทางเหล่านี้ไปยังตัวควบคุมการจัดการข้อผิดพลาดเฉพาะ:
<customErrors mode="On" defaultRedirect="Error">
<error statusCode="404" redirect="Error" />
</customErrors><br/>
ตอนนี้เปลี่ยน URL http://www.xyz.com/Error?aspxerrorpath=/category/subCategory/product111
เพื่อสิ่งที่ชอบ: ฉันต้องการให้ URL ถูกส่งไปยังไคลเอนต์ที่จะโหลดข้อมูลผ่าน AJAX ดังนั้นเคล็ดลับที่นี่คือการเรียกตัวควบคุม 'ดัชนี' เริ่มต้นราวกับว่าไม่ได้อ้างอิงตัวควบคุมใด ๆ ฉันทำได้โดยเพิ่มแฮชไปยัง URL ก่อนพารามิเตอร์ 'หมวดหมู่' และ 'หมวดหมู่ย่อย' ทั้งหมด URL ที่แฮชไม่ต้องการตัวควบคุมพิเศษใด ๆ ยกเว้นตัวควบคุม 'ดัชนี' เริ่มต้นและข้อมูลจะถูกส่งไปยังไคลเอนต์ซึ่งจะลบการแฮชและใช้ข้อมูลหลังจากแฮชเพื่อโหลดข้อมูลผ่าน AJAX นี่คือรหัสตัวจัดการข้อผิดพลาด:
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;
}
}
}
แต่แล้วURL ที่น่าเกลียดล่ะ สิ่งเหล่านี้สร้างขึ้นโดยบอทของ google และควรส่งคืน HTML ธรรมดาที่มีข้อมูลทั้งหมดที่ผู้ใช้เห็นในเบราว์เซอร์ สำหรับเรื่องนี้ผมใช้phantomjs Phantom เป็นเบราว์เซอร์ที่ไม่มีส่วนหัวทำสิ่งที่เบราว์เซอร์กำลังทำอยู่ในฝั่งไคลเอ็นต์ - แต่อยู่ฝั่งเซิร์ฟเวอร์ กล่าวอีกนัยหนึ่ง phantom รู้ (เหนือสิ่งอื่นใด) วิธีรับหน้าเว็บผ่าน URL แยกวิเคราะห์รวมถึงการเรียกใช้รหัสจาวาสคริปต์ทั้งหมดในนั้น (รวมถึงการรับข้อมูลผ่านการโทร AJAX) และให้ HTML ที่สะท้อนกลับมาให้คุณ DOM หากคุณใช้ MS Visual Studio Express คุณหลายคนต้องการติดตั้ง phantom ผ่านลิงค์นี้
แต่ก่อนอื่นเมื่อ URL ที่น่าเกลียดถูกส่งไปยังเซิร์ฟเวอร์เราต้องจับมัน สำหรับเรื่องนี้ฉันเพิ่มลงในโฟลเดอร์ 'App_start' ไฟล์ต่อไปนี้:
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;
}
}
}
สิ่งนี้ถูกเรียกจาก 'filterConfig.cs' ใน '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());
}
}
}
อย่างที่คุณเห็นเส้นทาง 'AjaxCrawlableAttribute' น่าเกลียด URL ไปยังตัวควบคุมชื่อ 'HtmlSnapshot' และนี่คือตัวควบคุมนี้:
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();
}
}
}
ที่เกี่ยวข้องview
เป็นเรื่องง่ายมากเพียงหนึ่งบรรทัดของรหัส:
@Html.Raw( ViewBag.result )
ในขณะที่คุณสามารถมองเห็นในการควบคุมโหลดภาพหลอนไฟล์จาวาสคริปต์ที่มีชื่อภายใต้โฟลเดอร์ที่ฉันสร้างขึ้นเรียกว่าcreateSnapshot.js
seo
นี่คือไฟล์จาวาสคริปต์:
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);
ฉันต้องการขอบคุณโทมัสเดวิสสำหรับหน้าที่ฉันได้รับรหัสพื้นฐานจาก :-)
คุณจะสังเกตเห็นสิ่งผิดปกติที่นี่: phantom โหลดซ้ำหน้าจนกว่าcheckLoaded()
ฟังก์ชันจะคืนค่าจริง ทำไมถึงเป็นอย่างนั้น? นี่เป็นเพราะ SPA เฉพาะของฉันทำการโทร AJAX หลายครั้งเพื่อรับข้อมูลทั้งหมดและวางไว้ใน DOM บนหน้าของฉันและ phantom ไม่ทราบว่าเมื่อการโทรทั้งหมดเสร็จสิ้นก่อนที่จะคืนค่าเงาสะท้อน HTML ของ DOM ให้ฉัน สิ่งที่ฉันทำที่นี่คือหลังจากการโทร AJAX สุดท้ายที่ฉันเพิ่ม<span id='compositionComplete'></span>
เพื่อว่าถ้าแท็กนี้มีอยู่ฉันรู้ว่า DOM เสร็จสมบูรณ์ ฉันทำสิ่งนี้เพื่อตอบสนองต่อcompositionComplete
เหตุการณ์ของ Durandal ดูที่นี่มากขึ้น หากสิ่งนี้ไม่เกิดขึ้นภายใน 10 วินาทีฉันก็ยอมแพ้ (มันน่าจะใช้เวลาเพียงเสี้ยววินาทีเท่านั้น) HTML ที่ส่งคืนมีลิงก์ทั้งหมดที่ผู้ใช้เห็นในเบราว์เซอร์ สคริปต์จะทำงานไม่ถูกต้องเนื่องจาก<script>
แท็กที่มีอยู่ในสแน็ปช็อต HTML ไม่ได้อ้างอิง URL ที่ถูกต้อง สิ่งนี้สามารถเปลี่ยนแปลงได้เช่นกันในไฟล์ phantom javascript แต่ฉันไม่คิดว่านี่เป็นสิ่งจำเป็นเพราะ google snapshort ถูกใช้โดย Google เพื่อรับa
ลิงก์และไม่ให้เรียกใช้ javascript เท่านั้น เชื่อมโยงเหล่านี้ทำอ้างอิง URL ที่สวยและถ้าเป็นจริงถ้าคุณลองไปดูภาพรวม HTML ในเบราว์เซอร์ที่คุณจะได้รับความผิดพลาด javascript แต่การเชื่อมโยงทั้งหมดจะทำงานได้อย่างถูกต้องและนำคุณไปยังเซิร์ฟเวอร์อีกครั้งกับ URL ที่สวยในครั้งนี้ รับหน้าทำงานอย่างเต็มที่
นี่ไง. ตอนนี้เซิร์ฟเวอร์รู้วิธีจัดการ URL ทั้งสวยและน่าเกลียดโดยเปิดใช้งานสถานะพุชบนทั้งเซิร์ฟเวอร์และไคลเอนต์ URL ที่น่าเกลียดทั้งหมดได้รับการปฏิบัติเช่นเดียวกันโดยใช้ phantom ดังนั้นจึงไม่จำเป็นต้องสร้างตัวควบคุมแยกต่างหากสำหรับการโทรแต่ละประเภท
สิ่งหนึ่งที่คุณอาจต้องการที่จะมีการเปลี่ยนแปลงไม่ได้ที่จะให้เป็นนายพล '/ หมวดหมู่หมวดหมู่รอง / ผลิตภัณฑ์' โทร แต่จะเพิ่มเก็บ http://www.xyz.com/store/category/subCategory/product111
'เพื่อให้การเชื่อมโยงจะมีลักษณะดังนี้: นี้จะหลีกเลี่ยงปัญหาที่เกิดขึ้นในการแก้ปัญหาของฉันว่า URL ที่ไม่ถูกต้องได้รับการปฏิบัติราวกับว่าพวกเขาเป็นจริงเรียกไป 'ดัชนี' ควบคุมและฉันคิดว่าสิ่งเหล่านี้สามารถจัดการได้แล้วภายในเก็บ 'ควบคุมโดยไม่ต้องไปweb.config
ฉันพบดังกล่าวข้างต้น .