這次帶給大家Angular實作服務端渲染案列詳解,Angular實作服務端渲染的注意事項有哪些,以下就是實戰案例,一起來看一下。
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 伺服器
#使用下列指令安裝它們:
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.server.module.ts
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'; // 可以注册那些在 Universal 环境下运行应用时特有的服务提供商 @NgModule({ imports: [ AppBrowserModule, // 客户端应用的 AppModule ServerModule, // 服务端的 Angular 模块 ModuleMapLoaderModule, // 用于实现服务端的路由的惰性加载 ServerTransferStateModule, // 在服务端导入,用于实现将状态从服务器传输到客户端 ], bootstrap: [AppComponent], }) export class AppServerModule { }
src/app/app.module.ts#
@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 的執行時間資訊:
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}`); }
src/main.server.ts##該檔案匯出服務端模組:
export { AppServerModule } from './app/app.server.module';
4、修改客戶端應用程式的引導程式檔案:
src/main.ts監聽DOMContentLoaded 事件,在發生DOMContentLoaded 事件時執行我們的程式碼,以讓TransferState 正常運作
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(); } // 在 DOMContentLoaded 时运行我们的代码,以使 TransferState 正常工作 document.addEventListener('DOMContentLoaded', () => { platformBrowserDynamic().bootstrapModule(AppBrowserModule); });
5、建立TypeScript 的服務端設定:
src/tsconfig.server.json<div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">{
"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"
}
}</pre><div class="contentsignin">登入後複製</div></div>
與
的差異在於:module 屬性必須是commonjs,這樣它才能被require() 方法匯入你的服務端套用。
angularCompilerOptions 部分有一些面向 AOT 編譯器的選項:
6、修改 @angular/cli 的配置文件:.angular-cli.json
在 apps
下添加:
{ "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
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'; // Faster server renders w/ Prod mode (dev mode never needed) enableProdMode(); // Express server const app = express(); const PORT = process.env.PORT || 4000; const DIST_FOLDER = join(process.cwd(), 'dist'); // Our index.html we'll use as our template const template = readFileSync(join(DIST_FOLDER, 'browser', 'index.html')).toString(); // * NOTE :: leave this as require() since this file is built Dynamically from webpack const {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require('./dist/server/main.bundle'); // Express Engine import { ngExpressEngine } from '@nguniversal/express-engine'; // Import module map for lazy loading import { provideModuleMap } from '@nguniversal/module-map-ngfactory-loader'; // Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine) app.engine('html', ngExpressEngine({ bootstrap: AppServerModuleNgFactory, providers: [ provideModuleMap(LAZY_MODULE_MAP) ] })); app.set('view engine', 'html'); app.set('views', join(DIST_FOLDER, 'browser')); /* - Example Express Rest API endpoints - app.get('/api/**', (req, res) => { }); */ // 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}`); });
8、创建服务端预渲染的程序:prerender.ts
// 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 => { const 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)); });
9、创建 Webpack 的服务端配置:webpack.server.config.js
Universal 应用不需要任何额外的 Webpack 配置,Angular CLI 会帮我们处理它们。但是由于本例子的 Node Express 的服务程序是 TypeScript 应用(server.ts及prerender.ts),所以要使用 Webpack 来转译它。这里不讨论 Webpack 的配置,需要了解的移步 Webpack官网
// Work around for https://github.com/angular/angular-cli/issues/7200 const path = require('path'); const webpack = require('webpack'); module.exports = { entry: { server: './server.ts', // This is our Express server for Dynamic universal prerender: './prerender.ts' // This is an example of Static prerendering (generative) }, target: 'node', resolve: {extensions: ['.ts', '.js']}, externals: [/(node_modules|main\..*\.js)/,], // Make sure we include all node_modules etc output: { path: path.join(dirname, 'dist'), // Puts the output at the root of the dist folder filename: '[name].js' }, module: { rules: [ {test: /\.ts$/, loader: 'ts-loader'} ] }, plugins: [ new webpack.ContextReplacementPlugin( /(.+)?angular(\\|\/)core(.+)?/, // fixes WARNING Critical dependency: the request of a dependency is an expression path.join(dirname, 'src'), // location of your src {} // a map of your routes ), new webpack.ContextReplacementPlugin( /(.+)?express(\\|\/)(.+)?/, // fixes WARNING Critical dependency: the request of a dependency is an expression path.join(dirname, 'src'), {} ) ] };
测试配置
通过上面的配置,我们就制作完成一个可在服务端渲染的 Angular Universal 应用。
在 package.json 的 scripts 区配置 build 和 serve 有关的命令:
{ "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 解析使用。
export const ROUTES = [ '/', '/lazy' ];
因此,从dist目录可以看到,服务端预渲染会根据配置好的路由在 browser 生成对应的静态index.html。如 /
对应 /index.html
,/lazy
对应 /lazy/index.html
。
服务器到客户端的状态传输
在前面的介绍中,我们在 app.server.module.ts
中导入了 ServerTransferStateModule
,在 app.module.ts
中导入了 BrowserTransferStateModule
和 TransferHttpCacheModule
。
这三个模块都与服务器到客户端的状态传输有关:
ServerTransferStateModule
:在服务端导入,用于实现将状态从服务器传输到客户端
BrowserTransferStateModule
:在客户端导入,用于实现将状态从服务器传输到客户端
TransferHttpCacheModule
:用于实现服务器到客户端的请求传输缓存,防止客户端重复请求服务端已完成的请求
使用这几个模块,可以解决 http请求在服务端和客户端分别请求一次 的问题。
比如在 home.component.ts
中有如下代码:
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://restapi.amap.com/v3/place/text?keywords=${text}&city=${city}&offset=20&key=55f909211b9950837fba2c71d0488db9&extensions=all`)); } }
代码运行之后,
服务端请求并打印:
客户端再一次请求并打印:
方法1:使用 TransferHttpCacheModule
使用 TransferHttpCacheModule
很简单,代码不需要改动。在 app.module.ts
中导入之后,Angular自动会将服务端请求缓存到客户端,换句话说就是服务端请求到数据会自动传输到客户端,客户端接收到数据之后就不会再发送请求了。
方法2:使用 BrowserTransferStateModule
该方法稍微复杂一些,需要改动一些代码。
调整 home.component.ts
代码如下:
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://restapi.amap.com/v3/place/text?keywords=${text}&city=${city}&offset=20&key=55f909211b9950837fba2c71d0488db9&extensions=all`)); } }
使用 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
中根据当前是否客户端来决定是否将存储的数据进行删除
相信看了本文案例你已经掌握了方法,更多精彩请关注php中文网其它相关文章!
推荐阅读:
vue.js+element-ui树形控件修改iview步奏详解
以上是Angular實作服務端渲染案列詳解的詳細內容。更多資訊請關注PHP中文網其他相關文章!