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

时间:2018-04-23 08:56:02

标签: angularjs angular5

我们正在将我们的应用程序从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中创建可注入的模块/值?

3 个答案:

答案 0 :(得分:4)

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

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

其中JSON_SERIALIZED_OBJECT是实际的序列化对象。我们使用.NET(非核心,因此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);
    }
}

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

答案 1 :(得分:4)

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

如果你计划在Angular 5中使用服务器端渲染,你应该考虑查看Angular Universal它提供了在服务器端呈现Angular单页面应用程序的构建块(对于SEO友好的可索引)含量)。

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

// 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.
     ...
   }
 }

但要注意窗口,文档,导航器和其他浏览器类型 - 在服务器上不存在。因此,任何可能使用它们的库都可能无效。

答案 2 :(得分:4)

创建文件: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中几乎一样。剩下的唯一事情就是在角度脚本之前将变量添加到index.html(甚至可以将它放在head中):

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