Angular服务器渲染

一、前言

​ 为什么需要服务端渲染?当我们要求渲染时间尽量快、页面响应速度快时(优点),才会采用服务器渲染,并且应该“按需”对页面进行渲染 ——“首次加载/首屏”。即服务端渲染的优势在于:由中间层( nodejs端 )为客户端请求初始数据、渲染好页面 ,有利于SEO优化。

二、实现

Angular2.x有个服务器渲染 Angular2-Universal ,但是在Angular4 中部分是合并到@angular/platform-server。使用也大大不同了,这里主要讲Angular4,如何使用服务器渲染。

1.新建一个Angular新项目

ng new Server-Render,这是localhost:4200返回的。没有经过服务器渲染,<body>只有<app-root></app-root>标签。

<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>ServerRender</title>
  <base href="/">

  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" type="image/x-icon" href="favicon.ico">
</head>
<body>
  <app-root></app-root>
<script type="text/javascript" src="inline.bundle.js"></script><script type="text/javascript" src="polyfills.bundle.js"></script><script type="text/javascript" src="styles.bundle.js"></script><script type="text/javascript" src="vendor.bundle.js"></script><script type="text/javascript" src="main.bundle.js"></script></body>
</html>

2.增加browser-app.module.tsserver-app.module.ts

增加两个文件browser-app.module.tsserver-app.module.ts一个是node渲染的Module,还有一个浏览器渲染的Module。

//browser-app.module.ts
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppComponent } from './app.component';
import { AppModule } from './app.module';

@NgModule({
  bootstrap: [ AppComponent ],
  imports: [
    BrowserModule.withServerTransition({
      appId: 'my-app-id' 
    }),
    AppModule
  ]
})
export class BrowserAppModule {}



//server-app.module.ts
import { NgModule, APP_BOOTSTRAP_LISTENER, ApplicationRef } from '@angular/core';
import { ServerModule } from '@angular/platform-server';
import { AppComponent } from './app.component';
import { AppModule } from './app.module';
import { BrowserModule } from '@angular/platform-browser';

@NgModule({
  bootstrap: [AppComponent],
  imports: [
    BrowserModule.withServerTransition({
      appId: 'my-app-id' //必须和browser-app.module.ts中的一样
    }),
    ServerModule,
    AppModule
  ]
})
export class ServerAppModule {

}

3.入口修改

Angular默认是用@angular/platform-browser-dynamic加载,只需要一个入口。而服务器渲染有两个入口,main.server.tsmain.browser.ts,一个是node运行的入口,一个是浏览器运行的入口。

  • main.browser.ts

用于加载BrowserAppModule,生成浏览器运行的js代码。

export function main() {
  return platformBrowserDynamic().bootstrapModule(BrowserAppModule);
}
//需要因为服务器渲染已经渲染好html,需要等待dom加载完成,绑定事件。
document.addEventListener('DOMContentLoaded', main, false);
  • main.server.ts

在服务器上面渲染js代码,加载ServerAppModule。这里使用express作为web服务器。使用html作为模板,更改@nguniversal/express-engine为模板引擎。这里把Angular路由注册成express路由,第一次加载都会经过服务器渲染,其他时候使用history API模拟跳转。

import 'zone.js/dist/zone-node';//使用zone的node版
import 'reflect-metadata';
import 'rxjs/Rx';
import * as express from 'express';
import { Request, Response } from 'express';
import { platformServer, renderModuleFactory } from '@angular/platform-server';
import { ServerAppModule } from './app/server-app.module';
import { ngExpressEngine } from '@nguniversal/express-engine';
import { routers } from './app/router'
import { enableProdMode } from '@angular/core';
enableProdMode();
const app = express();
const port = 8000;
const baseUrl = `http://localhost:${port}`;

app.engine('html', ngExpressEngine({
  bootstrap: ServerAppModule
}));

app.set('view engine', 'html');
app.set('views', 'src');

app.use('/', express.static('dist', { index: false }));

routers.forEach((route: any) => {
  app.get('/'+route.path, (req: Request, res: Response) => {
    console.time(`GET: ${req.originalUrl}`);
    res.render('../dist/index', {
      req: req,
      res: res
    });
    console.timeEnd(`GET: ${req.originalUrl}`);
  });
});

app.listen(port, () => {
  console.log(`Listening at ${baseUrl}`);
});

@nguniversal/express-engine是一个nodejs渲染Angular应用程序渲染引擎,它加载初始的html文件渲染出DOM结构,返回给浏览器。高级用法。比如,在渲染的时候接收到request参数,直接渲染到html中。如下:

import { Request } from 'express';
import { REQUEST } from '@nguniversal/express-engine/tokens';

@Injectable()
export class RequestService {
  constructor(@Inject(REQUEST) private request: Request) {}
}

三、服务器渲染陷阱

  • window, document, navigator, 等浏览器属性是不存在nodejs中的。因此涉及到dom结构的库如(jQuery)都不能工作。

因此你在项目中使用的时候,需要注意到当前运行在什么环境,如下:

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.
    ...
  }
}
  • 尽量少使用或避免避免setTimeout,它将等所有的setTimeout结束并渲染完,才会response

  • 记得关闭Rxjs的流。

  • 服务器渲染时不能直接操作nativeElement

    constructor(element: ElementRef, renderer: Renderer2) {

    renderer.setStyle(element.nativeElement, 'font-size', 'x-large');

    }

  • 在服务器渲染的时候发送了XHR请求,渲染出带数据的HTML,在浏览器中又会发送XHR请求,这次是多余的。可以使用缓存,把服务器端的数据通过生成<scrirpt>标签传递给浏览器,浏览器使用缓存即可。官方示例