Skip to content
This repository was archived by the owner on Nov 22, 2024. It is now read-only.

Commit 2deea86

Browse files
authored
feat(ng-aspnetcore-engine): adding initial new engine (#682)
* feat(ng-aspnetcore-engine): adding initial new engine WIP * update documentation & build * update documentation - startup.cs * passing data from .NET -> Angular * describe where data is passed from .NET * update api * separate out logic * updates & readme update * update readme * add docs about tokens
1 parent d6c4af7 commit 2deea86

File tree

10 files changed

+685
-0
lines changed

10 files changed

+685
-0
lines changed

modules/aspnetcore-engine/README.md

Lines changed: 342 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,342 @@
1+
# Angular & ASP.NET Core Engine
2+
3+
This is an ASP.NET Core Engine for running Angular Apps on the server for server side rendering.
4+
5+
---
6+
7+
## Example Application utilizing this Engine
8+
9+
#### [Asp.net Core & Angular advanced starter application](https://github.com/MarkPieszak/aspnetcore-angular2-universal)
10+
11+
# Usage
12+
13+
> Things have changed since the previous ASP.NET Core & Angular Universal useage. We're no longer using TagHelpers, but now invoking the **boot-server** file from the **Home Controller** *itself*, and passing all the data down to .NET.
14+
15+
Within our boot-server file, things haven't changed much, you still have your `createServerRenderer()` function that's being exported (this is what's called within the Node process) which is expecting a `Promise` to be returned.
16+
17+
Within that promise we simply call the ngAspnetCoreEngine itself, passing in our providers Array (here we give it the current `url` from the Server, and also our Root application, which in our case is just `<app></app>`).
18+
19+
20+
```ts
21+
// Polyfills
22+
import 'es6-promise';
23+
import 'es6-shim';
24+
import 'reflect-metadata';
25+
import 'zone.js';
26+
27+
import { enableProdMode } from '@angular/core';
28+
import { INITIAL_CONFIG } from '@angular/platform-server';
29+
import { createServerRenderer, RenderResult } from 'aspnet-prerendering';
30+
// Grab the (Node) server-specific NgModule
31+
import { AppServerModule } from './app/app.server.module';
32+
// ***** The ASPNETCore Angular Engine *****
33+
import { ngAspnetCoreEngine } from '@universal/ng-aspnetcore-engine';
34+
35+
enableProdMode(); // for faster server rendered builds
36+
37+
export default createServerRenderer(params => {
38+
39+
/*
40+
* How can we access data we passed from .NET ?
41+
* you'd access it directly from `params.data` under the name you passed it
42+
* ie: params.data.WHATEVER_YOU_PASSED
43+
* -------
44+
* We'll show in the next section WHERE you pass this Data in on the .NET side
45+
*/
46+
47+
// Platform-server provider configuration
48+
const setupOptions: IEngineOptions = {
49+
appSelector: '<app></app>',
50+
ngModule: ServerAppModule,
51+
request: params,
52+
providers: [
53+
/* Other providers you want to pass into the App would go here
54+
* { provide: CookieService, useClass: ServerCookieService }
55+
56+
* ie: Just an example of Dependency injecting a Class for providing Cookies (that you passed down from the server)
57+
(Where on the browser you'd have a different class handling cookies normally)
58+
*/
59+
]
60+
};
61+
62+
// ***** Pass in those Providers & your Server NgModule, and that's it!
63+
return ngAspnetCoreEngine(setupOptions).then(response => {
64+
65+
// Want to transfer data from Server -> Client?
66+
67+
// Add transferData to the response.globals Object, and call createTransferScript({}) passing in the Object key/values of data
68+
// createTransferScript() will JSON Stringify it and return it as a <script> window.TRANSFER_CACHE={}</script>
69+
// That your browser can pluck and grab the data from
70+
response.globals.transferData = createTransferScript({
71+
someData: 'Transfer this to the client on the window.TRANSFER_CACHE {} object',
72+
fromDotnet: params.data.thisCameFromDotNET // example of data coming from dotnet, in HomeController
73+
});
74+
75+
return ({
76+
html: response.html,
77+
globals: response.globals
78+
});
79+
80+
});
81+
});
82+
83+
```
84+
85+
# What about on the .NET side?
86+
87+
Previously, this was all done with TagHelpers and you passed in your boot-server file to it: `<app asp-prerender-module="dist/boot-server.js"></app>`, but this hindered us from getting the SEO benefits of prerendering.
88+
89+
Because .NET has control over the Html, using the ngAspnetCoreEngine, we're able to *pull out the important pieces*, and give them back to .NET to place them through out the View.
90+
91+
Below is how you can invoke the boot-server file which gets everything started:
92+
93+
> Hopefully in the future this will be cleaned up and less code as well.
94+
95+
### HomeController.cs
96+
97+
```csharp
98+
using System.Threading.Tasks;
99+
using Microsoft.AspNetCore.Mvc;
100+
101+
using Microsoft.AspNetCore.SpaServices.Prerendering;
102+
using Microsoft.AspNetCore.NodeServices;
103+
using Microsoft.Extensions.DependencyInjection;
104+
using Microsoft.AspNetCore.Hosting;
105+
using Microsoft.AspNetCore.Http.Features;
106+
107+
namespace WebApplicationBasic.Controllers
108+
{
109+
public class HomeController : Controller
110+
{
111+
public async Task<IActionResult> Index()
112+
{
113+
var nodeServices = Request.HttpContext.RequestServices.GetRequiredService<INodeServices>();
114+
var hostEnv = Request.HttpContext.RequestServices.GetRequiredService<IHostingEnvironment>();
115+
116+
var applicationBasePath = hostEnv.ContentRootPath;
117+
var requestFeature = Request.HttpContext.Features.Get<IHttpRequestFeature>();
118+
var unencodedPathAndQuery = requestFeature.RawTarget;
119+
var unencodedAbsoluteUrl = $"{Request.Scheme}://{Request.Host}{unencodedPathAndQuery}";
120+
121+
// *********************************
122+
// This parameter is where you'd pass in an Object of data you want passed down to Angular
123+
// to be used in the Server-rendering
124+
125+
// ** TransferData concept **
126+
// Here we can pass any Custom Data we want !
127+
128+
// By default we're passing down the REQUEST Object (Cookies, Headers, Host) from the Request object here
129+
TransferData transferData = new TransferData();
130+
transferData.request = AbstractHttpContextRequestInfo(Request); // You can automatically grab things from the REQUEST object in Angular because of this
131+
transferData.thisCameFromDotNET = "Hi Angular it's asp.net :)";
132+
// Add more customData here, add it to the TransferData class
133+
134+
// Prerender / Serialize application (with Universal)
135+
var prerenderResult = await Prerenderer.RenderToString(
136+
"/", // baseURL
137+
nodeServices,
138+
new JavaScriptModuleExport(applicationBasePath + "/ClientApp/dist/main-server"),
139+
unencodedAbsoluteUrl,
140+
unencodedPathAndQuery,
141+
// Our Transfer data here will be passed down to Angular (within the boot-server file)
142+
// Available there via `params.data.yourData`
143+
transferData,
144+
30000, // timeout duration
145+
Request.PathBase.ToString()
146+
);
147+
148+
// This is where everything is now spliced out, and given to .NET in pieces
149+
ViewData["SpaHtml"] = prerenderResult.Html;
150+
ViewData["Title"] = prerenderResult.Globals["title"];
151+
ViewData["Styles"] = prerenderResult.Globals["styles"];
152+
ViewData["Meta"] = prerenderResult.Globals["meta"];
153+
ViewData["Links"] = prerenderResult.Globals["links"];
154+
ViewData["TransferData"] = prerenderResult.Globals["transferData"]; // our transfer data set to window.TRANSFER_CACHE = {};
155+
156+
// Let's render that Home/Index view
157+
return View();
158+
}
159+
160+
private IRequest AbstractHttpContextRequestInfo(HttpRequest request)
161+
{
162+
163+
IRequest requestSimplified = new IRequest();
164+
requestSimplified.cookies = request.Cookies;
165+
requestSimplified.headers = request.Headers;
166+
requestSimplified.host = request.Host;
167+
168+
return requestSimplified;
169+
}
170+
171+
}
172+
173+
public class IRequest
174+
{
175+
public object cookies { get; set; }
176+
public object headers { get; set; }
177+
public object host { get; set; }
178+
}
179+
180+
public class TransferData
181+
{
182+
// By default we're expecting the REQUEST Object (in the aspnet engine), so leave this one here
183+
public dynamic request { get; set; }
184+
185+
// Your data here ?
186+
public object thisCameFromDotNET { get; set; }
187+
}
188+
}
189+
```
190+
191+
### Startup.cs : Make sure you add NodeServices to ConfigureServices:
192+
193+
```csharp
194+
public void ConfigureServices(IServiceCollection services)
195+
{
196+
// ... other things ...
197+
198+
services.AddNodeServices(); // <--
199+
}
200+
```
201+
202+
# What updates do our Views need now?
203+
204+
Now we have a whole assortment of SEO goodness we can spread around our .NET application. Not only do we have our serialized Application in a String...
205+
206+
We also have `<title>`, `<meta>`, `<link>'s`, and our applications `<styles>`
207+
208+
In our _layout.cshtml, we're going to want to pass in our different `ViewData` pieces and place these where they needed to be.
209+
210+
> Notice `ViewData[]` sprinkled through out. These came from our Angular application, but it returned an entire HTML document, we want to build up our document ourselves so .NET handles it!
211+
212+
```html
213+
<!DOCTYPE html>
214+
<html>
215+
<head>
216+
<base href="/" />
217+
<!-- Title will be the one you set in your Angular application -->
218+
<title>@ViewData["Title"]</title>
219+
220+
@Html.Raw(ViewData["Meta"]) <!-- <meta /> tags -->
221+
@Html.Raw(ViewData["Links"]) <!-- <link /> tags -->
222+
@Html.Raw(ViewData["Styles"]) <!-- <styles /> tags -->
223+
224+
</head>
225+
<body>
226+
<!-- Our Home view will be rendered here -->
227+
@RenderBody()
228+
229+
<!-- Here we're passing down any data to be used by grabbed and parsed by Angular -->
230+
@Html.Raw(ViewData["TransferData"])
231+
232+
@RenderSection("scripts", required: false)
233+
</body>
234+
</html>
235+
```
236+
237+
---
238+
239+
# Your Home View - where the App gets displayed:
240+
241+
You may have seen or used a TagHelper here in the past (that's where it used to invoke the Node process and everything), but now since we're doing everything
242+
in the **Controller**, we only need to grab our `ViewData["SpaHtml"]` and inject it!
243+
244+
This `SpaHtml` was set in our HomeController, and it's just a serialized string of your Angular application, but **only** the `<app>/* inside is all serialized */</app>` part, not the entire Html, since we split that up, and let .NET build out our Document.
245+
246+
```html
247+
@Html.Raw(ViewData["SpaHtml"]) <!-- magic -->
248+
249+
<!-- here you probably have your webpack vendor & main files as well -->
250+
<script src="~/dist/vendor.js" asp-append-version="true"></script>
251+
@section scripts {
252+
<script src="~/dist/main-client.js" asp-append-version="true"></script>
253+
}
254+
```
255+
256+
---
257+
258+
# What happens after the App gets server rendered?
259+
260+
Well now, your Client-side Angular will take over, and you'll have a fully functioning SPA. (With all these great SEO benefits of being server-rendered) !
261+
262+
:sparkles:
263+
264+
---
265+
266+
## Bootstrap
267+
268+
The engine also calls the ngOnBootstrap lifecycle hook of the module being bootstrapped, this is how the TransferData gets taken.
269+
Check [https://github.com/MarkPieszak/aspnetcore-angular2-universal/tree/master/Client/modules](https://github.com/MarkPieszak/aspnetcore-angular2-universal/tree/master/Client/modules) to see how to setup your Transfer classes.
270+
271+
```ts
272+
@NgModule({
273+
bootstrap: [AppComponent]
274+
})
275+
export class ServerAppModule {
276+
// Make sure to define this an arrow function to keep the lexical scope
277+
ngOnBootstrap = () => {
278+
console.log('bootstrapped');
279+
}
280+
}
281+
```
282+
283+
# Tokens
284+
285+
Along with the engine doing serializing and separating out the chunks of your Application (so we can let .NET handle it), you may have noticed we passed in the HttpRequest object from .NET into it as well.
286+
287+
This was done so that we could take a few things from it, and using dependency injection, "provide" a few things to the Angular application.
288+
289+
```typescript
290+
ORIGIN_URL
291+
// and
292+
REQUEST
293+
294+
// imported
295+
import { ORIGIN_URL, REQUEST } from '@ng-universal/ng-aspnetcore-engine';
296+
```
297+
298+
Make sure in your BrowserModule you provide these tokens as well, if you're going to use them!
299+
300+
```typescript
301+
@NgModule({
302+
...,
303+
providers: [
304+
{
305+
// We need this for our Http calls since they'll be using an ORIGIN_URL provided in main.server
306+
// (Also remember the Server requires Absolute URLs)
307+
provide: ORIGIN_URL,
308+
useFactory: (getOriginUrl)
309+
}, {
310+
// The server provides these in main.server
311+
provide: REQUEST,
312+
useFactory: (getRequest)
313+
}
314+
]
315+
} export class BrowserAppModule() {}
316+
```
317+
318+
Don't forget that the server needs Absolute URLs for paths when doing Http requests! So if your server api is at the same location as this Angular app, you can't just do `http.get('/api/whatever')` so use the ORIGIN_URL Injection Token.
319+
320+
```typescript
321+
import { ORIGIN_URL } from '@ng-universal/ng-aspnetcore-engine';
322+
323+
constructor(@Inject(ORIGIN_URL) private originUrl: string, private http: Http) {
324+
this.http.get(`${this.originUrl}/api/whatever`)
325+
}
326+
```
327+
328+
As for the REQUEST object, you'll find Cookies, Headers, and Host (from .NET that we passed down in our HomeController. They'll all be accessible from that Injection Token as well.
329+
330+
```typescript
331+
import { REQUEST } from '@ng-universal/ng-aspnetcore-engine';
332+
333+
constructor(@Inject(REQUEST) private request) {
334+
// this.request.cookies
335+
// this.request.headers
336+
// etc
337+
}
338+
339+
```
340+
341+
342+

modules/aspnetcore-engine/index.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
2+
export { ngAspnetCoreEngine } from './src/main';
3+
export { createTransferScript } from './src/create-transfer-script';
4+
5+
export { REQUEST, ORIGIN_URL } from './src/tokens';
6+
7+
export { IEngineOptions } from './src/interfaces/engine-options';
8+
export { IRequestParams } from './src/interfaces/request-params';

0 commit comments

Comments
 (0)