시작하기 전에, 당신이 구글에서 무엇을 이해하도록하십시오 필요 , 특히 사용을 꽤 하고 추한 URL을. 이제 구현을 보자.
고객 입장에서
클라이언트 측에는 AJAX 호출을 통해 서버와 동적으로 상호 작용하는 단일 html 페이지 만 있습니다. 그것이 바로 SPA의 문제입니다. a
클라이언트 측의 모든 태그는 내 응용 프로그램에서 동적으로 만들어지며 나중에 서버에서 Google 로봇에 이러한 링크를 표시하는 방법을 살펴 보겠습니다. 이러한 각 a
태그의 요구는이 할 수 pretty URL
에 href
구글의 로봇이 크롤링 있도록 태그입니다. 당신은 원하지 않는 href
우리가 부하에 새 페이지를 원하지 않을 수 있기 때문에, (우리가 나중에 보자, 서버가 구문 분석 할 수 있도록하려는에도 불구하고) 부분이 그것의 클라이언트 클릭 할 때 사용되는 AJAX 호출을 통해 일부 데이터가 페이지의 일부에 표시되도록하고 자바 스크립트를 통해 (예 : HTML5 사용 pushstate
또는 Durandaljs
) URL을 변경합니다 . 그래서, 우리는 둘 다href
onclick
사용자가 링크를 클릭 할 때 작업을 수행 할 뿐만 아니라 Google의 속성입니다 . 이제는 URL에 push-state
아무것도 원하지 #
않으므로 일반적인 a
태그는 다음과 같이 보일 수 있습니다.
<a href="http://www.xyz.com/#!/category/subCategory/product111" onClick="loadProduct('category','subCategory','product111')>see product111...</a>
'category'및 'subCategory'는 아마도 '통신'및 '전화'또는 '컴퓨터'와 같은 다른 문구 일 것입니다. 그리고 가전 제품 상점을위한 '노트북'. 분명히 많은 다른 범주와 하위 범주가있을 것입니다. 보다시피, 링크는과 같은 특정 '스토어'페이지에 대한 추가 매개 변수가 아닌 카테고리, 하위 카테고리 및 제품에 직접 연결됩니다 http://www.xyz.com/store/category/subCategory/product111
. 더 짧고 간단한 링크를 선호하기 때문입니다. 내 '페이지'중 하나와 이름이 같은 카테고리가 없습니다. 즉 '
AJAX ( onclick
부분) 를 통해 데이터를로드하는 방법에 대해서는 설명하지 않고 Google에서 검색하면 많은 좋은 설명이 있습니다. 여기서 언급하고 싶은 유일한 중요한 점은 사용자가이 링크를 클릭하면 브라우저의 URL이 다음과 같이 표시된다는 것입니다.
http://www.xyz.com/category/subCategory/product111
. 그리고 이것은 URL이 서버로 전송되지 않습니다! 이것은 클라이언트와 서버 간의 모든 상호 작용이 AJAX를 통해 이루어지고 전혀 링크가없는 SPA입니다. 모든 '페이지'는 클라이언트 측에서 구현되며 다른 URL은 서버를 호출하지 않습니다 (서버는 이러한 URL을 다른 사이트에서 사이트로 외부 링크로 사용하는 경우 이러한 URL을 처리하는 방법을 알아야합니다. 나중에 서버 측에서 볼 수 있습니다). 이제 이것은 Durandal에 의해 훌륭하게 처리됩니다. 나는 그것을 강력히 추천하지만 다른 기술을 선호한다면이 부분을 건너 뛸 수도 있습니다. 당신이 그것을 선택하고 나처럼 웹용 MS Visual Studio Express 2012를 사용 하고 있다면 Durandal Starter Kit를 설치할 수 있습니다 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의 봇이 www.xyz.com?_escaped_fragment_=
나중에 볼 URL을 변환하게합니다 .
- '정보'경로는 웹 애플리케이션에서 원하는 다른 '페이지'링크에 대한 예일뿐입니다.
- 이제 까다로운 부분은 '범주'경로가 없으며 여러 가지 범주가있을 수 있다는 것입니다. 사전 정의 된 경로가없는 범주는 없습니다. 이것은
mapUnknownRoutes
알려지지 않은 경로를 'store'경로에 매핑하고 '!'를 제거합니다. pretty URL
Google의 검색 엔진 에서 생성 된 경우 URL에서 'store'경로는 'fragment'속성의 정보를 가져와 AJAX 호출을 통해 데이터를 가져 와서 표시하고 URL을 로컬로 변경합니다. 내 응용 프로그램에서는 모든 호출에 대해 다른 페이지를로드하지 않습니다. 이 데이터와 관련된 페이지 부분 만 변경하고 URL을 로컬로 변경합니다.
pushState:true
Durandal이 푸시 상태 URL을 사용하도록 지시 하는 것을 주목하십시오 .
이것이 클라이언트 측에서 필요한 전부입니다. 해시 된 URL로도 구현할 수 있습니다 (Durandal에서는 간단히 제거하십시오 pushState:true
). 더 복잡한 부분 (적어도 나를 위해 ...)은 서버 부분이었습니다.
서버 측
내가 사용하고 MVC 4.5
있는 서버 측에서 WebAPI
컨트롤러. 서버는 실제로 3의 URL 종류 처리해야합니다 : 구글에 의해 생성 된 것들 - 모두 pretty
와 ugly
또한 클라이언트의 브라우저에 나타나는 것과 동일한 형식의 '간단한'URL. 이 작업을 수행하는 방법을 살펴 보겠습니다.
예쁜 URL과 '간단한'URL은 존재하지 않는 컨트롤러를 참조하려고하는 것처럼 서버에서 먼저 해석됩니다. 서버는 비슷한 것을보고 http://www.xyz.com/category/subCategory/product111
'category'라는 컨트롤러를 찾습니다. 그래서 web.config
다음 줄을 추가하여 특정 오류 처리 컨트롤러로 리디렉션합니다.
<customErrors mode="On" defaultRedirect="Error">
<error statusCode="404" redirect="Error" />
</customErrors><br/>
이제 URL을 다음과 같이 변환합니다 http://www.xyz.com/Error?aspxerrorpath=/category/subCategory/product111
. AJAX를 통해 데이터를로드 할 클라이언트로 URL을 전송하고 싶기 때문에 여기서 속임수는 컨트롤러를 참조하지 않는 것처럼 기본 '인덱스'컨트롤러를 호출하는 것입니다. 모든 'category'및 'subCategory'매개 변수 앞에 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은 어떻습니까? 이들은 구글의 봇에 의해 생성되며 사용자가 브라우저에서 보는 모든 데이터를 포함하는 일반 HTML을 반환해야합니다. 이를 위해 나는 phantomjs를 사용 합니다 . Phantom은 브라우저가 클라이언트 쪽에서 서버 쪽에서 수행하는 작업을 수행하는 헤드리스 브라우저입니다. 다시 말해, 팬텀은 URL을 통해 웹 페이지를 가져 오는 방법과 모든 자바 스크립트 코드 실행 (AJAX 호출을 통한 데이터 가져 오기 포함)을 분석하고 반영하는 HTML을 제공하는 방법을 알고 있습니다. DOM. MS Visual Studio Express를 사용하는 경우 많은 사람들이이 링크 를 통해 팬텀을 설치하려고합니다 .
그러나 먼저 못생긴 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;
}
}
}
이것은 'App_start'의 'filterConfig.cs'에서도 호출됩니다.
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 )
합니다. 컨트롤러에서 볼 수 있듯이 phantom 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);
먼저 :-)에서 기본 코드를 얻은 페이지 에 대해 Thomas Davis 에게 감사드립니다 .
여기서 이상한 점을 알 수 있습니다. 팬텀은 checkLoaded()
함수가 true를 반환 할 때까지 페이지를 계속 다시로드 합니다. 왜 그런 겁니까? 내 특정 SPA가 여러 데이터를 가져 와서 내 페이지의 DOM에 배치하기 위해 여러 AJAX 호출을 수행하고 팬텀은 DOM의 HTML 반영을 다시 반환하기 전에 모든 호출이 완료된 시점을 알 수 없기 때문입니다. 내가 여기에서 한 일은 마지막 AJAX 호출 후입니다. <span id='compositionComplete'></span>
이 태그가 있으면 DOM이 완료되었음을 알 수 있습니다. Durandal의 compositionComplete
이벤트 에 대한 응답으로이 작업을 수행합니다. 여기를 참조 하십시오.이상. 이것이 10 초 안에 일어나지 않으면 포기합니다 (최대 1 초가 걸립니다). 반환 된 HTML에는 사용자가 브라우저에서 볼 수있는 모든 링크가 포함되어 있습니다. <script>
HTML 스냅 샷에 존재 하는 태그가 올바른 URL을 참조하지 않기 때문에 스크립트가 제대로 작동 하지 않습니다. 이것은 자바 스크립트 팬텀 파일에서도 변경 될 수 있지만 HTML snapshort는 a
링크 를 가져 오고 자바 스크립트를 실행하지 않기 위해 Google에서만 사용하기 때문에 이것이 불필요하다고 생각하지 않습니다 . 이 링크 는 예쁜 URL을 참조하며, 사실 브라우저에서 HTML 스냅 샷을 보려고하면 자바 스크립트 오류가 발생하지만 모든 링크가 제대로 작동하고 이번에는 예쁜 URL로 서버로 다시 연결됩니다. 완전히 작동하는 페이지를 얻는 중입니다.
이거 야. 이제 서버는 서버와 클라이언트 모두에서 푸시 상태를 활성화하여 예쁘고 못생긴 URL을 처리하는 방법을 알고 있습니다. 모든 추악한 URL은 팬텀을 사용하여 동일한 방식으로 처리되므로 각 유형의 통화에 대해 별도의 컨트롤러를 만들 필요가 없습니다.
변경하려는 것을 선호하는 한 가지는 일반적인 '카테고리 / 서브 카테고리 / 제품'호출을하는 것이 아니라 링크를 다음과 같이 보이도록 '저장소'를 추가하는 것 http://www.xyz.com/store/category/subCategory/product111
입니다. 이것은 내 솔루션에서 모든 유효하지 않은 URL이 실제로 '인덱스'컨트롤러를 호출하는 것처럼 취급된다는 문제를 피할 수 있으며 web.config
위에서 언급 한 것 외에도 '저장소'컨트롤러 내에서 처리 할 수 있다고 가정합니다. .