Angular Universal
Angular在服務端渲染方面提供一套前後端同構解決方案,它就是 Angular Universal(統一平台),一項在服務端運行 Angular 應用的技術。
標準的 Angular 應用程式會執行在瀏覽器中,它會在 DOM 中渲染頁面,以回應使用者的操作。
而 Angular Universal 會在服務端透過一個被稱為服務端渲染(server-side rendering - SSR)的過程產生靜態的應用頁面。
它可以產生這些頁面,並在瀏覽器請求時直接用它們給予回應。 它也可以把頁面預先先生變成 HTML 文件,然後把它們當作靜態文件供服務端使用。
工作原理
要製作一個 Universal 應用,就要安裝 platform-server
套件。 platform-server 套件提供了服務端的 DOM 實作、XMLHttpRequest 和其它底層特性,但不再依賴瀏覽器。
你要使用 platform-server
模組而不是 platform-browser
模組來編譯這個客戶端應用,並且在一個 Web 伺服器上執行這個 Universal 應用程式。
伺服器(下面的範例中使用的是 Node Express 伺服器)會把客戶端對應用程式頁面的請求傳給 renderModuleFactory
函數。
renderModuleFactory 函數接受一個模板 HTML 頁面(通常是 index.html)、一個包含元件的 Angular 模組和一個用於決定該顯示哪些元件的路由作為輸入。
該路由從客戶端的請求中傳給伺服器。 每次請求都會給出所請求路由的一個適當的視圖。
renderModuleFactory 在範本中的 <app>
標籤中渲染出哪個視圖,並為客戶端建立一個完成的 HTML 頁面。
最後,伺服器就會把渲染好的頁面回傳給客戶端。
為什麼要服務端渲染
三個主要原因:
#幫助網路爬蟲(SEO)
提昇在手機和低功耗設備上的效能
迅速顯示出第首頁
幫助網路爬蟲(SEO)
Google、Bing、百度、Facebook、Twitter 和其它搜尋引擎或社群媒體網站都依賴網路爬蟲來索引你的應用程式內容,並且讓它的內容可以透過網路搜尋。
這些網路爬蟲可能不會像人類那樣導航到你的具有高度互動性的 Angular 應用,並為其建立索引。
Angular Universal 可以為你產生應用的靜態版本,它易搜尋、可鏈接,瀏覽時也不必借助 JavaScript。它也讓網站可以被預覽,因為每個 URL 返回的都是一個完全渲染好的頁面。
啟用網路爬蟲通常被稱為搜尋引擎最佳化 (SEO)。
提升手機和低功耗裝置上的效能
有些裝置不支援 JavaScript 或 JavaScript 執行得很差,導致使用者體驗不可接受。 對於這些情況,你可能需要該應用程式的服務端渲染、無 JavaScript 的版本。 雖然有一些限制,不過這個版本可能是那些完全沒辦法使用該應用程式的人的唯一選擇。
快速顯示首頁
快速顯示首頁對於吸引使用者是至關重要的。
如果頁面載入超過了三秒鐘中,那麼 53% 的行動網站會被放棄。 你的應用程式需要啟動的更快一點,以便在用戶決定做別的事情之前吸引他們的注意力。
使用 Angular Universal,你可以為應用程式產生“著陸頁”,它們看起來就和完整的應用程式一樣。 這些著陸頁是純 HTML,並且即使 JavaScript 被禁用了也能顯示。 這些頁面不會處理瀏覽器事件,不過它們可以用 routerLink 在這個網站中導航。
在實踐中,你可能要使用一個登陸頁面的靜態版本來保持使用者的注意力。 同時,你也會在幕後載入完整的 Angular 應用程式。 使用者會認為著陸頁幾乎是立即出現的,而當完整的應用程式載入完畢後,又可以獲得完全的互動體驗。
範例解析
下面將基於我在GitHub上的範例專案 angular-universal-starter 來進行解說。
這個專案與第一篇的範例專案一樣,都是基於 Angular CLI進行開發建構的,因此它們的差異只在於服務端渲染所需的那些配置上。
安裝工具
在開始之前,下列套件是必須安裝的(範例項目都已配置好,只需npm install
即可):
@angular/platform-server
- Universal 的服務端元件。
@nguniversal/module-map-ngfactory-loader
- 用於處理服務端渲染環境下的惰性載入。
@nguniversal/express-engine
- Universal 應用的 Express 引擎。
ts-loader
- 用於對服務端應用進行轉譯。
express
- Node Express 伺服器
#使用下列指令安裝它們:
1 | npm install --save @angular/platform-server @nguniversal/module-map-ngfactory-loader ts-loader @nguniversal/express-engine express
|
登入後複製
專案配置
設定工作有:
创建服务端应用模块:src/app/app.server.module.ts
修改客户端应用模块:src/app/app.module.ts
创建服务端应用的引导程序文件:src/main.server.ts
修改客户端应用的引导程序文件:src/main.ts
创建 TypeScript 的服务端配置:src/tsconfig.server.json
修改 @angular/cli 的配置文件:.angular-cli.json
创建 Node Express 的服务程序:server.ts
创建服务端预渲染的程序:prerender.ts
创建 Webpack 的服务端配置:webpack.server.config.js
1、创建服务端应用模块:src/app/app.server.module.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import { NgModule } from '@angular/core' ;
import { ServerModule, ServerTransferStateModule } from '@angular/platform-server' ;
import { ModuleMapLoaderModule } from '@nguniversal/module-map-ngfactory-loader' ;
import { AppBrowserModule } from './app.module' ;
import { AppComponent } from './app.component' ;
@NgModule({
imports: [
AppBrowserModule,
ServerModule,
ModuleMapLoaderModule,
ServerTransferStateModule,
],
bootstrap: [AppComponent],
})
export class AppServerModule {
}
|
登入後複製
服务端应用模块(习惯上叫作 AppServerModule)是一个 Angular 模块,它包装了应用的根模块 AppModule,以便 Universal 可以在你的应用和服务器之间进行协调。 AppServerModule 还会告诉 Angular 再把你的应用以 Universal 方式运行时,该如何引导它。
2、修改客户端应用模块:src/app/app.module.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | import { BrowserModule, BrowserTransferStateModule } from '@angular/platform-browser' ;
import { HttpClientModule } from '@angular/common/http' ;
import { APP_ID, Inject, NgModule, PLATFORM_ID } from '@angular/core' ;
import { AppComponent } from './app.component' ;
import { HomeComponent } from './home/home.component' ;
import { TransferHttpCacheModule } from '@nguniversal/common' ;
import { isPlatformBrowser } from '@angular/common' ;
import { AppRoutingModule } from './app.routes' ;
@NgModule({
imports: [
AppRoutingModule,
BrowserModule.withServerTransition({appId: 'my-app' }),
TransferHttpCacheModule,
BrowserTransferStateModule,
HttpClientModule
],
declarations: [
AppComponent,
HomeComponent
],
providers: [],
bootstrap: [AppComponent]
})
export class AppBrowserModule {
constructor(@Inject(PLATFORM_ID) private platformId: Object,
@Inject(APP_ID) private appId: string) {
const platform = isPlatformBrowser(platformId) ? 'in the browser' : 'on the server' ;
console.log(`Running ${platform} with appId=${appId}`);
}
}
|
登入後複製
将 NgModule
的元数据中 BrowserModule 的导入改成 BrowserModule.withServerTransition({appId: 'my-app'}),Angular 会把 appId 值(它可以是任何字符串)添加到服务端渲染页面的样式名中,以便它们在客户端应用启动时可以被找到并移除。
此时,我们可以通过依赖注入(@Inject(PLATFORM_ID)
及 @Inject(APP_ID)
)取得关于当前平台和 appId 的运行时信息:
1 2 3 4 5 6 7 | constructor(@Inject(PLATFORM_ID) private platformId: Object,
@Inject(APP_ID) private appId: string) {
const platform = isPlatformBrowser(platformId) ? 'in the browser' : 'on the server' ;
console.log(`Running ${platform} with appId=${appId}`);
}
|
登入後複製
3、创建服务端应用的引导程序文件:src/main.server.ts
该文件导出服务端模块:
1 | export { AppServerModule } from './app/app.server.module' ;
|
登入後複製
4、修改客户端应用的引导程序文件:src/main.ts
监听 DOMContentLoaded 事件,在发生 DOMContentLoaded 事件时运行我们的代码,以使 TransferState 正常工作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import { enableProdMode } from '@angular/core' ;
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic' ;
import { AppBrowserModule } from './app/app.module' ;
import { environment } from './environments/environment' ;
if (environment.production) {
enableProdMode();
}
document.addEventListener( 'DOMContentLoaded' , () => {
platformBrowserDynamic().bootstrapModule(AppBrowserModule);
});
|
登入後複製
5、创建 TypeScript 的服务端配置:src/tsconfig.server.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | {
"extends" : "../tsconfig.json" ,
"compilerOptions" : {
"outDir" : "../out-tsc/app" ,
"baseUrl" : "./" ,
"module" : "commonjs" ,
"types" : [
"node"
]
},
"exclude" : [
"test.ts" ,
"**/*.spec.ts"
],
"angularCompilerOptions" : {
"entryModule" : "app/app.server.module#AppServerModule"
}
}
|
登入後複製
与 tsconfig.app.json
的差异在于:
6、修改 @angular/cli 的配置文件:.angular-cli.json
在 apps
下添加:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | {
"platform" : "server" ,
"root" : "src" ,
"outDir" : "dist/server" ,
"assets" : [
"assets" ,
"favicon.ico"
],
"index" : "index.html" ,
"main" : "main.server.ts" ,
"test" : "test.ts" ,
"tsconfig" : "tsconfig.server.json" ,
"testTsconfig" : "tsconfig.spec.json" ,
"prefix" : "" ,
"styles" : [
"styles.scss"
],
"scripts" : [],
"environmentSource" : "environments/environment.ts" ,
"environments" : {
"dev" : "environments/environment.ts" ,
"prod" : "environments/environment.prod.ts"
}
}
|
登入後複製
7、创建 Node Express 的服务程序:server.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | import 'zone.js/dist/zone-node' ;
import 'reflect-metadata' ;
import { enableProdMode } from '@angular/core' ;
import * as express from 'express' ;
import { join } from 'path' ;
import { readFileSync } from 'fs' ;
enableProdMode();
const app = express();
const PORT = process.env.PORT || 4000;
const DIST_FOLDER = join(process.cwd(), 'dist' );
const template = readFileSync(join(DIST_FOLDER, 'browser' , 'index.html' )).toString();
const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require ( './dist/server/main.bundle' );
import { ngExpressEngine } from '@nguniversal/express-engine' ;
import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader' ;
app.engine( 'html' , ngExpressEngine({
bootstrap: AppServerModuleNgFactory,
providers: [
provideModuleMap(LAZY_MODULE_MAP)
]
}));
app.set( 'view engine' , 'html' );
app.set( 'views' , join(DIST_FOLDER, 'browser' ));
app.get( '*.*' , express. static (join(DIST_FOLDER, 'browser' ), {
maxAge: '1y'
}));
app.get( '*' , (req, res) => {
res.render( 'index' , {req});
});
app.listen(PORT, () => {
console.log(`Node Express server listening on http:
});
|
登入後複製
Universal 模板引擎
这个文件中最重要的部分是 ngExpressEngine 函数:
1 2 3 4 5 6 | app.engine( 'html' , ngExpressEngine({
bootstrap: AppServerModuleNgFactory,
providers: [
provideModuleMap(LAZY_MODULE_MAP)
]
}));
|
登入後複製
ngExpressEngine 是对 Universal 的 renderModuleFactory 函数的封装。它会把客户端请求转换成服务端渲染的 HTML 页面。如果你使用不同于Node的服务端技术,你需要在该服务端的模板引擎中调用这个函数。
ngExpressEngine 函数返回了一个会解析成渲染好的页面的承诺(Promise)。
接下来你的引擎要决定拿这个页面做点什么。 现在这个引擎的回调函数中,把渲染好的页面返回给了 Web 服务器,然后服务器通过 HTTP 响应把它转发给了客户端。
8、创建服务端预渲染的程序:prerender.ts
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | 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' ;
enableProdMode();
import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader' ;
import { renderModuleFactory } from '@angular/platform-server' ;
import { ROUTES } from './static.paths' ;
const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require ( './dist/server/main.bundle' );
const BROWSER_FOLDER = join(process.cwd(), 'browser' );
const index = readFileSync(join( 'browser' , 'index.html' ), 'utf8' );
let previousRender = Promise.resolve();
ROUTES.forEach(route => {
const fullPath = join(BROWSER_FOLDER, route);
if (!existsSync(fullPath)) {
mkdirSync(fullPath);
}
previousRender = previousRender.then(_ => renderModuleFactory(AppServerModuleNgFactory, {
document: index,
url: route,
extraProviders: [
provideModuleMap(LAZY_MODULE_MAP)
]
})).then(html => writeFileSync(join(fullPath, 'index.html' ), html));
});
|
登入後複製
9、创建 Webpack 的服务端配置:webpack.server.config.js
Universal 应用不需要任何额外的 Webpack 配置,Angular CLI 会帮我们处理它们。但是由于本例子的 Node Express 的服务程序是 TypeScript 应用(server.ts及prerender.ts),所以要使用 Webpack 来转译它。这里不讨论 Webpack 的配置,需要了解的移步 Webpack官网
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | const path = require ( 'path' );
const webpack = require ( 'webpack' );
module.exports = {
entry: {
server: './server.ts' ,
prerender: './prerender.ts'
},
target: 'node' ,
resolve: {extensions: [ '.ts' , '.js' ]},
externals: [/(node_modules|main\..*\.js)/,],
output: {
path: path.join(__dirname, 'dist' ),
filename: '[name].js'
},
module: {
rules: [
{test: /\.ts$/, loader: 'ts-loader' }
]
},
plugins: [
new webpack.ContextReplacementPlugin(
/(.+)?angular(\\|\/)core(.+)?/,
path.join(__dirname, 'src' ),
{}
),
new webpack.ContextReplacementPlugin(
/(.+)?express(\\|\/)(.+)?/,
path.join(__dirname, 'src' ),
{}
)
]
};
|
登入後複製
测试配置
通过上面的配置,我们就制作完成一个可在服务端渲染的 Angular Universal 应用。
在 package.json 的 scripts 区配置 build 和 serve 有关的命令:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | {
"scripts" : {
"ng" : "ng" ,
"start" : "ng serve -o" ,
"ssr" : "npm run build:ssr && npm run serve:ssr" ,
"prerender" : "npm run build:prerender && npm run serve:prerender" ,
"build" : "ng build" ,
"build:client-and-server-bundles" : "ng build --prod && ng build --prod --app 1 --output-hashing=false" ,
"build:prerender" : "npm run build:client-and-server-bundles && npm run webpack:server && npm run generate:prerender" ,
"build:ssr" : "npm run build:client-and-server-bundles && npm run webpack:server" ,
"generate:prerender" : "cd dist && node prerender" ,
"webpack:server" : "webpack --config webpack.server.config.js --progress --colors" ,
"serve:prerender" : "cd dist/browser && http-server" ,
"serve:ssr" : "node dist/server"
}
}
|
登入後複製
开发只需运行 npm run start
执行 npm run ssr
编译应用程序,并启动一个Node Express来为应用程序提供服务 http://localhost:4000
dist目录:

执行npm run prerender - 编译应用程序并预渲染应用程序文件,启动一个演示http服务器,以便您可以查看它 http://localhost:8080
注意: 要将静态网站部署到静态托管平台,您必须部署dist/browser文件夹, 而不是dist文件夹
dist目录:

根据项目实际的路由信息并在根目录的 static.paths.ts
中配置,提供给 prerender.ts 解析使用。
1 2 3 4 | export const ROUTES = [
'/' ,
'/lazy'
];
|
登入後複製
因此,从dist目录可以看到,服务端预渲染会根据配置好的路由在 browser 生成对应的静态index.html。如 /
对应 /index.html
,/lazy
对应 /lazy/index.html
。
服务端的模块懒加载
在前面的介绍中,我们在 app.server.module.ts
中导入了 ModuleMapLoaderModule,在 app.module.ts
。
ModuleMapLoaderModule
模块可以使得懒加载的模块也可以在服务端进行渲染,而你要做也只是在 app.server.module.ts
中导入。
服务端到客户端的状态传输
在前面的介绍中,我们在 app.server.module.ts
中导入了 ServerTransferStateModule
,在 app.module.ts
中导入了 BrowserTransferStateModule
和 TransferHttpCacheModule。
这三个模块都与服务端到客户端的状态传输有关:
ServerTransferStateModule
:在服务端导入,用于实现将状态从服务端传输到客户端
BrowserTransferStateModule
:在客户端导入,用于实现将状态从服务端传输到客户端
TransferHttpCacheModule
:用于实现服务端到客户端的请求传输缓存,防止客户端重复请求服务端已完成的请求
使用这几个模块,可以解决 http请求在服务端和客户端分别请求一次 的问题。
比如在 home.component.ts
中有如下代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | import { Component, OnDestroy, OnInit } from '@angular/core' ;
import { HttpClient } from '@angular/common/http' ;
import { Observable } from 'rxjs/Observable' ;
@Component({
selector: 'app-home' ,
templateUrl: './home.component.html' ,
styleUrls: [ './home.component.scss' ]
})
export class HomeComponent implements OnInit, OnDestroy {
constructor( public http: HttpClient) {
}
ngOnInit() {
this.poiSearch(this.keyword, '北京市' ).subscribe((data: any) => {
console.log(data);
});
}
ngOnDestroy() {
}
poiSearch(text: string, city?: string): Observable<any> {
return this.http.get(encodeURI(`http:
}
}
|
登入後複製
代码运行之后,
服务端请求并打印:

客户端再一次请求并打印:

方法1:使用 TransferHttpCacheModule
使用 TransferHttpCacheModule
很简单,代码不需要改动。在 app.module.ts
中导入之后,Angular自动会将服务端请求缓存到客户端,换句话说就是服务端请求到数据会自动传输到客户端,客户端接收到数据之后就不会再发送请求了。
方法2:使用 BrowserTransferStateModule
该方法稍微复杂一些,需要改动一些代码。
调整 home.component.ts
代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | import { Component, OnDestroy, OnInit } from '@angular/core' ;
import { makeStateKey, TransferState } from '@angular/platform-browser' ;
import { HttpClient } from '@angular/common/http' ;
import { Observable } from 'rxjs/Observable' ;
const KFCLIST_KEY = makeStateKey( 'kfcList' );
@Component({
selector: 'app-home' ,
templateUrl: './home.component.html' ,
styleUrls: [ './home.component.scss' ]
})
export class HomeComponent implements OnInit, OnDestroy {
constructor( public http: HttpClient,
private state: TransferState) {
}
ngOnInit() {
const kfcList:any[] = this.state.get(KFCLIST_KEY, null as any);
if (!this.kfcList) {
this.poiSearch(this.keyword, '北京市' ).subscribe((data: any) => {
console.log(data);
this.state.set(KFCLIST_KEY, data as any);
});
}
}
ngOnDestroy() {
if (typeof window === 'object' ) {
this.state.set(KFCLIST_KEY, null as any);
}
}
poiSearch(text: string, city?: string): Observable<any> {
return this.http.get(encodeURI(`http:
}
}
|
登入後複製
使用 const KFCLIST_KEY = makeStateKey('kfcList')
创建储存传输数据的 StateKey
在 HomeComponent
的构造函数中注入 TransferState
在 ngOnInit
中根据 this.state.get(KFCLIST_KEY, null as any)
判断数据是否存在(不管是服务端还是客户端),存在就不再请求,不存在则请求数据并通过 this.state.set(KFCLIST_KEY, data as any)
存储传输数据
在 ngOnDestroy
中根据当前是否客户端来决定是否将存储的数据进行删除
客户端与服务端渲染对比
最后,我们分别通过这三个原因来进行对比:
帮助网络爬虫(SEO)
提升在手机和低功耗设备上的性能
迅速显示出首页
帮助网络爬虫(SEO)
客户端渲染:

服务端渲染:

从上面可以看到,服务端提前将信息渲染到返回的页面上,这样网络爬虫就能直接获取到信息了(网络爬虫基本不会解析javascript的)。
提升在手机和低功耗设备上的性能
这个原因通过上面就可以看出,对于一些低端的设备,直接显示页面总比要解析javascript性能高的多。
迅速显示出首页
同样在 Fast 3G 网络条件下进行测试
客户端渲染:

服务端渲染:

牢记几件事情
对于服务器软件包,您可能需要将第三方模块包含到nodeExternals
白名单中
window
, document
, navigator
以及其它的浏览器类型 - 不存在于服务端 - 如果你直接使用,在服务端将无法正常工作。 以下几种方法可以让你的代码正常工作:
1 2 3 | - 尽量**限制**或**避免**使用`setTimeout`。它会减慢服务器端的渲染过程。确保在组件的`ngOnDestroy`中删除它们
- 对于RxJs超时,请确保在成功时 _取消_ 它们的流,因为它们也会降低渲染速度。
|
登入後複製
1 2 3 | constructor(element: ElementRef, renderer: Renderer2) {
this.renderer.setStyle(element.nativeElement, 'font-size' , 'x-large' );
}
|
登入後複製
相关推荐:
Angular开发实践(五):深入解析变化监测
Angular开发实践(四):组件之间的交互
以上是Angular開發實踐(六):服務端渲染的詳細內容。更多資訊請關注PHP中文網其他相關文章!