使用 Angular5 进行服务器端渲染(从 AngularJS 迁移而来)



我们正在将我们的应用程序从AngularJS转换为Angular5。我正在尝试弄清楚如何使用 Angular5 复制某些行为 -即使用服务器端渲染来创建可注入的值。

在我们当前的 Angular1.6 应用程序中,我们有这个index.hbs文件:

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Collaborative Tool</title>
<link href="favicon.ico" rel="shortcut icon" type="image/x-icon">
</head>
<body class="aui content" ng-app="app">
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.6.5/angular.js"></script>
<script>
/* globals angular */
angular.module('app')
.value('USER', JSON.parse('{{{user}}}'))
.value('WORKSTREAM_ENUM', JSON.parse('{{{workStreamEnum}}}'))
.value('CATEGORY_ENUM', JSON.parse('{{{categoryEnum}}}'))
.value('ROLES_ENUM', JSON.parse('{{{roles}}}'))
.value('FUNCTIONAL_TEAM_ENUM', JSON.parse('{{{functionalTeams}}}'))
.value('CDT_ENV', '{{CDT_ENV}}')
.value('CDT_HOST', '{{CDT_HOST}}')
.value('CDT_LOGOUT_URL', '{{CDT_LOGOUT_URL}}');

</script>
</body>
</html>

所以我们要做的是在第一个脚本标签中加载角度,然后我们使用第二个脚本标签创建一些值/枚举/常量。本质上是使用服务器端渲染(把手)将数据发送到前端。

我的问题:有没有办法用Angular5做一些非常相似的事情?如何使用服务器端渲染在 Angular5 中创建可注入的模块/值?

在服务器端渲染依赖注入时,仍然可以在组件内部使用。

如果您打算在 Angular 5 中使用服务器端渲染,您应该考虑研究 Angular 通用,它提供了在服务器端呈现 Angular 单页应用程序的构建块(用于 SEO 友好的可索引内容)。

那里有很多很好的角度通用启动器项目。一个很好的例子是[universal-starter][2].它使用 ngExpressEngine 在请求的 url 处动态呈现您的应用程序。它使用 webpack 项目配置,其中包含一个预渲染任务,用于编译应用程序并预呈现应用程序文件。此任务如下所示:

// Load zone.js for the server.
import 'zone.js/dist/zone-node';
import 'reflect-metadata';
import {readFileSync, writeFileSync, existsSync, mkdirSync} from 'fs';
import {join} from 'path';
import {enableProdMode} from '@angular/core';
// Faster server renders w/ Prod mode (dev mode never needed)
enableProdMode();
// Import module map for lazy loading
import {provideModuleMap} from '@nguniversal/module-map-ngfactory-loader';
import {renderModuleFactory} from '@angular/platform-server';
import {ROUTES} from './static.paths';
// * NOTE :: leave this as require() since this file is built Dynamically from webpack
const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require('./dist/server/main.bundle');
const BROWSER_FOLDER = join(process.cwd(), 'browser');
// Load the index.html file containing referances to your application bundle.
const index = readFileSync(join('browser', 'index.html'), 'utf8');
let previousRender = Promise.resolve();
// Iterate each route path
ROUTES.forEach(route => {
var fullPath = join(BROWSER_FOLDER, route);
// Make sure the directory structure is there
if(!existsSync(fullPath)){
mkdirSync(fullPath);
}
// Writes rendered HTML to index.html, replacing the file if it already exists.
previousRender = previousRender.then(_ => renderModuleFactory(AppServerModuleNgFactory, {
document: index,
url: route,
extraProviders: [
provideModuleMap(LAZY_MODULE_MAP)
]
})).then(html => writeFileSync(join(fullPath, 'index.html'), html));
});

稍后,您可以运行一个快速服务器来呈现应用程序生成的 HTML:

app.engine('html', ngExpressEngine({
bootstrap: AppServerModuleNgFactory,
providers: [
provideModuleMap(LAZY_MODULE_MAP)
]
}));
app.set('view engine', 'html');
app.set('views', join(DIST_FOLDER, 'browser'));
// Server static files from /browser
app.get('*.*', express.static(join(DIST_FOLDER, 'browser'), {
maxAge: '1y'
}));
// All regular routes use the Universal engine
app.get('*', (req, res) => {
res.render('index', { req });
});
// Start up the Node server
app.listen(PORT, () => {
console.log(`Node Express server listening on http://localhost:${PORT}`);
});

您可以运行服务器端特定的代码,例如:

import { PLATFORM_ID } from '@angular/core';
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
constructor(@Inject(PLATFORM_ID) private platformId: Object) { ... }
ngOnInit() {
if (isPlatformBrowser(this.platformId)) {
// Client only code.
...
}
if (isPlatformServer(this.platformId)) {
// Server only code.
...
}
}

但请注意,窗口中,文档,导航器和其他浏览器类型 - 服务器上不存在。因此,任何可能使用这些库的库都可能无法正常工作。

我的团队在从 AngularJS 过渡到 Angular(v2 的早期候选版本)时遇到了同样的问题。我们想出了一个我们仍在使用的解决方案,我不知道有任何更新可以使其更容易(至少在不使用 Angular Universal 时 - 如果您正在使用它,那么内置了一些东西来引导初始数据)。我们通过序列化 JSON 对象并将其设置为我们 HTML 中应用程序根 Angular 组件上的属性来将数据传递给我们的 Angular 应用程序:

<app-root [configuration]="JSON_SERIALIZED_OBJECT"></app-root>

其中JSON_SERIALIZED_OBJECT是实际序列化的对象。我们使用.NET(非Core,所以Angular Universal不是一个真正的选择)来呈现我们的页面(做[configuration]="@JsonConvert.SerializeObject(Model.Context)"),所以不知道你需要做什么,但看起来你应该能够做你以前做过的同样的事情来序列化它。

设置完成后,我们必须在主应用程序组件中手动JSON.parse(...)该对象,但我们将其视为 Angular 输入。这就是我们的组件看起来的样子:

import { Component, ElementRef } from '@angular/core';
import { ConfigurationService } from 'app/core';
@Component(...)
export class AppComponent {
constructor(private element: ElementRef, private configurationService: ConfigurationService) {
this.setupConfiguration();
}
private setupConfiguration() {
const value = this.getAttributeValue('[configuration]');
const configuration = value ? JSON.parse(value) : {};
this.configurationService.setConfiguration(configuration);
}
private getAttributeValue(attribute: string) {
const element = this.element.nativeElement;
return element.hasAttribute(attribute) ? element.getAttribute(attribute) : null;
}
}

如图所示,我们使用服务在系统周围共享数据。它可以是这样简单的事情:

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Configuration } from './configuration.model';
@Injectable()
export class ConfigurationService {
private readonly configurationSubject$ = new BehaviorSubject<Configuration>(null);
readonly configuration$ = this.configurationSubject$.asObservable();
setConfiguration(configuration: Configuration) {
this.configurationSubject$.next(configuration);
}
}

然后在需要配置数据的组件中,我们注入此服务并观察更改。

import { Component, OnDestroy } from '@angular/core';
import { Subject } from 'rxjs/Subject';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/takeUntil';
import { ConfigurationService } from 'app/core';
@Component(...)
export class ExampleThemedComponent implements OnDestroy {
private readonly destroy$ = new Subject<boolean>();
readonly theme$: Observable<string> = this.configurationService.configuration$
.takeUntil(this.destroy$.asObservable())
.map(c => c.theme);
constructor(private configurationService: ConfigurationService) {
}
ngOnDestroy() {
this.destroy$.next(true);
}
}

注意:我们有时会在运行时更改配置,这就是我们使用主题和可观察量的原因。如果您的配置不会更改,则可以跳过这些示例的所有部分。

创建文件:data.ts。在此文件中声明变量及其类型(我将只显示一个)并为每个变量创建 InjectionToken:

import { InjectionToken } from '@angular/core';
// describes the value of the variable
export interface EmbeddedUserData {
userId: string;
// etc
}
// tells the app that there will be a global variable named EMBEDDED_USER_DATA (from index.html)
export declare const EMBEDDED_USER_DATA: EmbeddedUserData;
// creates injection token for DI that you can use it as a provided value (like value or constant in angular 1)
export UserData = new InjectionToken<EmbeddedUserData>('EmbeddedUserData');

然后来到你的app.module.ts并提供这个令牌:

// ...
providers: [
{ provide: UserData, useValue: EMBEDDED_USER_DATA }
],
// ...

最后将其用作任何正常服务/注入值:

// ...
constructor(@Inject(UserData) userData: EmbeddedUserData) {}
// ...

或将其用作简单的导入变量(在这种情况下甚至不需要提供/注入任何东西):

import { EMBEDDED_USER_DATA } from './data.ts';

因此,你几乎拥有与angularjs相同的功能。剩下的唯一事情就是在角度脚本之前将变量添加到索引.html(也许将其放在head中甚至有意义):

<script>var EMBEDDED_USER_DATA = JSON.parse({ ... })</script>

最新更新