Implement cache priming prototype

This commit is contained in:
SteveSandersonMS
2015-12-10 20:27:15 +00:00
parent 5ff3e9a051
commit 7924a6527a
7 changed files with 114 additions and 3 deletions

View File

@@ -0,0 +1,44 @@
using System;
using System.Net;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.AspNet.Mvc.Rendering;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
namespace Microsoft.AspNet.AngularServices {
public static class PrimeCacheHelper {
public static async Task<HtmlString> PrimeCache(this IHtmlHelper html, string url) {
// TODO: Consider deduplicating the PrimeCache calls (that is, if there are multiple requests to precache
// the same URL, only return nonempty for one of them). This will make it easier to auto-prime-cache any
// HTTP requests made during server-side rendering, without risking unnecessary duplicate requests.
if (string.IsNullOrEmpty(url)) {
throw new ArgumentException("Value cannot be null or empty", "url");
}
try {
var request = html.ViewContext.HttpContext.Request;
var baseUri = new Uri(string.Concat(request.Scheme, "://", request.Host.ToUriComponent(), request.PathBase.ToUriComponent(), request.Path.ToUriComponent(), request.QueryString.ToUriComponent()));
var fullUri = new Uri(baseUri, url);
var response = await new HttpClient().GetAsync(fullUri.ToString());
var responseBody = await response.Content.ReadAsStringAsync();
return new HtmlString(FormatAsScript(url, response.StatusCode, responseBody));
} catch (Exception ex) {
var logger = (ILogger)html.ViewContext.HttpContext.ApplicationServices.GetService(typeof (ILogger));
if (logger != null) {
logger.LogWarning("Error priming cache for URL: " + url, ex);
}
return new HtmlString(string.Empty);
}
}
private static string FormatAsScript(string url, HttpStatusCode responseStatusCode, string responseBody)
{
return string.Format(@"<script>window.__preCachedResponses = window.__preCachedResponses || {{}}; window.__preCachedResponses[{0}] = {1};</script>",
JsonConvert.SerializeObject(url),
JsonConvert.SerializeObject(new { statusCode = responseStatusCode, body = responseBody })
);
}
}
}

View File

@@ -12,7 +12,8 @@ builder.config({
'angular2-aspnet/*': 'dist/*'
},
meta: {
'angular2/*': { build: false }
'angular2/*': { build: false },
'@reactivex/*': { build: false }
}
});

View File

@@ -1,6 +1,6 @@
{
"name": "angular2-aspnet",
"version": "0.0.1",
"version": "0.0.3",
"description": "Helpers for Angular 2 apps built on ASP.NET",
"main": "./dist/Exports",
"scripts": {
@@ -15,7 +15,7 @@
"author": "Microsoft",
"license": "Apache-2.0",
"peerDependencies": {
"angular2": "^2.0.0-alpha.44"
"angular2": "2.0.0-alpha.44"
},
"devDependencies": {
"systemjs-builder": "^0.14.11",

View File

@@ -0,0 +1,63 @@
import { provide, Injectable, Provider } from 'angular2/core';
import { Connection, ConnectionBackend, Http, XHRBackend, RequestOptions, Request, RequestMethods, Response, ResponseOptions, ReadyStates } from 'angular2/http';
@Injectable()
export class CachePrimedConnectionBackend extends ConnectionBackend {
private _preCachedResponses: PreCachedResponses;
constructor(private _underlyingBackend: ConnectionBackend, private _baseResponseOptions: ResponseOptions) {
super();
this._preCachedResponses = (<any>window).__preCachedResponses || {};
}
public createConnection(request: Request): Connection {
let cacheKey = request.url;
if (request.method === RequestMethods.Get && this._preCachedResponses.hasOwnProperty(cacheKey)) {
return new CacheHitConnection(request, this._preCachedResponses[cacheKey], this._baseResponseOptions);
} else {
return this._underlyingBackend.createConnection(request);
}
}
}
class CacheHitConnection implements Connection {
readyState: ReadyStates;
request: Request;
response: any;
constructor (req: Request, cachedResponse: PreCachedResponse, baseResponseOptions: ResponseOptions) {
this.request = req;
this.readyState = ReadyStates.Done;
// Workaround for difficulty consuming CommonJS default exports in TypeScript. Note that it has to be a dynamic
// 'require', and not an 'import' statement, because the module isn't available on the server.
// All this badness goes away with the next update of Angular 2, as it exposes Observable directly from angular2/core.
// --
// The current version of Angular exposes the following SystemJS module directly (it is *not* coming from the
// @reactivex/rxjs NPM package - it's coming from angular2).
let obsCtor: any = require('@reactivex/rxjs/dist/cjs/Observable');
this.response = new obsCtor(responseObserver => {
let response = new Response(new ResponseOptions({ body: cachedResponse.body, status: cachedResponse.statusCode }));
responseObserver.next(response);
responseObserver.complete();
});
}
}
declare var require: any; // Part of the workaround mentioned below. Can remove this after updating Angular.
interface PreCachedResponses {
[url: string]: PreCachedResponse;
}
interface PreCachedResponse {
statusCode: number;
body: string;
}
export const CACHE_PRIMED_HTTP_PROVIDERS = [
provide(Http, {
useFactory: (xhrBackend, requestOptions, responseOptions) => new Http(new CachePrimedConnectionBackend(xhrBackend, responseOptions), requestOptions),
deps: [XHRBackend, RequestOptions, ResponseOptions]
}),
];

View File

@@ -1 +1,2 @@
export * from './CachePrimedHttp';
export * from './Validation';

View File

@@ -4,6 +4,7 @@
"target": "es5",
"sourceMap": false,
"declaration": true,
"experimentalDecorators": true,
"noLib": false,
"outDir": "./dist"
},

View File

@@ -19,6 +19,7 @@
"Microsoft.CSharp": "4.0.1-beta-*",
"System.Collections": "4.0.11-beta-*",
"System.Linq": "4.0.1-beta-*",
"System.Net.Http": "4.0.1-beta-*",
"System.Runtime": "4.0.21-beta-*",
"System.Threading": "4.0.11-beta-*"
}