開始する前に、Google が必要とするものを理解していることを確認してください、特に使用かわいいと醜い URLを。実装を見てみましょう:
クライアント側
クライアント側では、AJAX呼び出しを介してサーバーと動的に対話する単一のhtmlページしかありません。それがSPAの目的です。a
クライアント側のすべてのタグはアプリケーションで動的に作成されます。これらのリンクをサーバーのGoogleのボットに表示する方法については後で説明します。このような各a
タグは、Google pretty URL
の属性にを含めることができる必要があります。また、ユーザーがリンクをクリックしたときに、タグがジョブを実行できる必要があります。ここで使用するので、URLには何も必要ないため、一般的なタグは次のようhref
、Googleのボットがタグをクロールタグにを含める。href
クライアントがクリックしたときにその部分が使用されないようにします(サーバーがそれを解析できるようにしたい場合でも、後で確認します)。新しいページをロードしたくない場合があるためです。ページの一部に表示されるデータを取得するAJAX呼び出しを行い、JavaScriptを介してURLを変更する(たとえば、HTML5 pushstate
またはを使用するDurandaljs
)ためだけです。ですから、href
onclick
push-state
#
a
<a href="http://www.xyz.com/#!/category/subCategory/product111" onClick="loadProduct('category','subCategory','product111')>see product111...</a>
「category」と「subCategory」は、おそらく「communication」と「phones」または「computers」などの他のフレーズになります。と電化製品店の「ラップトップ」。明らかに、多くの異なるカテゴリーとサブカテゴリーがあります。ご覧のとおり、リンクはカテゴリ、サブカテゴリ、および製品への直接のリンクであり、などの特定の「ストア」ページへの追加パラメータではありませんhttp://www.xyz.com/store/category/subCategory/product111
。これは、私がより短くてシンプルなリンクを好むからです。これは、「ページ」の1つと同じ名前のカテゴリがないことを意味します。
AJAX(onclick
一部)を介してデータをロードする方法については説明しません。Google で検索します。多くの良い説明があります。ここで言及したい唯一の重要なことは、ユーザーがこのリンクをクリックしたときに、ブラウザーのURLが次のようになるようにすることです。
http://www.xyz.com/category/subCategory/product111
。そして、これはサーバーに送信されないURLです!これは、クライアントとサーバー間のすべての対話がAJAXを介して行われるSPAであり、リンクはまったくないことに注意してください。すべての「ページ」はクライアント側に実装されており、異なるURLがサーバーを呼び出しません(サーバーは、これらのURLを別のサイトからサイトへの外部リンクとして使用する場合に、これらのURLを処理する方法を知る必要があります。後でサーバー側の部分で確認します)。現在、これはDurandalによって見事に処理されています。私はそれを強くお勧めしますが、他のテクノロジーを好むなら、この部分をスキップすることもできます。それを選択し、私と同じようにMS Visual Studio Express 2012 for Webも使用している場合は、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を変換させます。
- 「about」ルートは、Webアプリケーションに必要な他の「ページ」へのリンクの例にすぎません。
- ここで注意が必要なのは、「カテゴリ」ルートがなく、多くの異なるカテゴリが存在する可能性があることです。いずれにも事前定義されたルートはありません。これが登場
mapUnknownRoutes
です。これらの不明なルートを「ストア」ルートにマッピングし、「!」も削除します。pretty URL
googleの検索エンジンで生成された場合は、URLから。「store」ルートは「fragment」プロパティの情報を受け取り、AJAX呼び出しを行ってデータを取得し、表示して、URLをローカルで変更します。私のアプリケーションでは、そのような呼び出しごとに別のページをロードしません。このデータに関連するページの部分のみを変更し、ローカルでURLも変更します。
- が
pushState:true
Durandalにプッシュ状態のURLを使用するように指示することに注意してください。
これがクライアント側で必要なすべてです。ハッシュ化されたURLでも実装できます(Durandalでは、pushState:true
、そのを)。より複雑な部分(少なくとも私にとっては...)はサーバー部分でした:
サーバ側
MVC 4.5
サーバー側でWebAPI
コントローラーを使用しています。サーバーは実際には3種類のURLを処理する必要があります。Googleによって生成されたもの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を介してWebページを取得し、その中ですべてのJavaScriptコードの実行(および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
は非常にシンプルで、コードは1行だけです。
@Html.Raw( ViewBag.result )
コントローラーで確認できるように、ファントムは、createSnapshot.js
私が作成したフォルダーの下にという名前のjavascriptファイルをロードしますseo
。これがこの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);
:-)から基本的なコードを取得したページについて、Thomas Davisに最初に感謝します。
ここで奇妙なことに気づくでしょう。ファントムは、checkLoaded()
関数がtrueを返すまでページをリロードし続けます。何故ですか?これは、私の特定のSPAがすべてのデータを取得してページのDOMに配置するためにAJAX呼び出しをいくつか行っており、ファントムはDOMのHTMLリフレクションを返す前にすべての呼び出しが完了したことを認識できないためです。ここで行ったのは、最後のAJAX呼び出しの後にを追加した<span id='compositionComplete'></span>
ため、このタグが存在する場合、DOMが完了したことがわかります。DurandalのcompositionComplete
イベントに応じてこれを行います。ここを参照してください多くのための。これが10秒以内に起こらない場合、私はあきらめます(1秒で終わります)。返されるHTMLには、ユーザーがブラウザに表示するすべてのリンクが含まれています。<script>
HTMLスナップショットに存在するタグが正しいURLを参照していないため、スクリプトは正しく機能しません。これはJavaScriptファントムファイルでも変更できますが、HTMLスナップショットはa
リンクを取得してJavaScriptを実行するためではなく、Googleでのみ使用されるため、これが必要であるとは思いません。これらのリンクはかなりのURLを参照しています。実際、ブラウザーでHTMLスナップショットを表示しようとすると、JavaScriptエラーが発生しますが、すべてのリンクは正しく機能し、今回はかなりのURLを使用してサーバーに再度アクセスします。完全に機能するページを取得します。
これだよ。これで、サーバーはきれいなURLと醜いURLの両方を処理する方法を認識し、サーバーとクライアントの両方でプッシュ状態が有効になります。すべての醜いURLはファントムを使用して同じ方法で処理されるため、呼び出しのタイプごとに個別のコントローラーを作成する必要はありません。
変更することをお勧めするのは、一般的な「category / subCategory / product」呼び出しを行うのではなく、「store」を追加してリンクが次のようになるようにすることですhttp://www.xyz.com/store/category/subCategory/product111
。これにより、すべての無効なURLが実際に「インデックス」コントローラーへの呼び出しであるかのように扱われるという私のソリューションの問題が回避されます。これらは、web.config
上記で示したものに追加することなく、「ストア」コントローラー内で処理できると思います。