国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 編程 > JavaScript > 正文

Angular開發實踐之服務端渲染

2019-11-19 14:05:28
字體:
來源:轉載
供稿:網友

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 頁面。

最后,服務器就會把渲染好的頁面返回給客戶端。

為什么要服務端渲染

三個主要原因:

  1. 幫助網絡爬蟲(SEO)
  2. 提升在手機和低功耗設備上的性能
  3. 迅速顯示出第一個頁面

幫助網絡爬蟲(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 即可):

  1. @angular/platform-server - Universal 的服務端元件。
  2. @nguniversal/module-map-ngfactory-loader - 用于處理服務端渲染環境下的惰性加載。
  3. @nguniversal/express-engine - Universal 應用的 Express 引擎。
  4. ts-loader - 用于對服務端應用進行轉譯。
  5. express - Node Express 服務器

使用下列命令安裝它們:

npm install --save @angular/platform-server @nguniversal/module-map-ngfactory-loader ts-loader @nguniversal/express-engine express

項目配置

配置工作有:

  1. 創建服務端應用模塊:src/app/app.server.module.ts
  2. 修改客戶端應用模塊:src/app/app.module.ts
  3. 創建服務端應用的引導程序文件:src/main.server.ts
  4. 修改客戶端應用的引導程序文件:src/main.ts
  5. 創建 TypeScript 的服務端配置:src/tsconfig.server.json
  6. 修改 @angular/cli 的配置文件:.angular-cli.json
  7. 創建 Node Express 的服務程序:server.ts
  8. 創建服務端預渲染的程序:prerender.ts
  9. 創建 Webpack 的服務端配置:webpack.server.config.js

1、創建服務端應用模塊: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 {}

服務端應用模塊(習慣上叫作 AppServerModule)是一個 Angular 模塊,它包裝了應用的根模塊 AppModule,以便 Universal 可以在你的應用和服務器之間進行協調。 AppServerModule 還會告訴 Angular 再把你的應用以 Universal 方式運行時,該如何引導它。

2、修改客戶端應用模塊: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}`);}

3、創建服務端應用的引導程序文件: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

{ "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 的差異在于:

module 屬性必須是 commonjs,這樣它才能被 require() 方法導入你的服務端應用。

angularCompilerOptions 部分有一些面向 AOT 編譯器的選項:

  1. entryModule - 服務端應用的根模塊,其格式為 path/to/file#ClassName。

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 serverconst app = express();const PORT = process.env.PORT || 4000;const DIST_FOLDER = join(process.cwd(), 'dist');// Our index.html we'll use as our templateconst template = readFileSync(join(DIST_FOLDER, 'browser', 'index.html')).toString();// * NOTE :: leave this as require() since this file is built Dynamically from webpackconst {AppServerModuleNgFactory, LAZY_MODULE_MAP} = require('./dist/server/main.bundle');// Express Engineimport { ngExpressEngine } from '@nguniversal/express-engine';// Import module map for lazy loadingimport { 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 /browserapp.get('*.*', express.static(join(DIST_FOLDER, 'browser'), {  maxAge: '1y'}));// ALl regular routes use the Universal engineapp.get('*', (req, res) => {  res.render('index', {req});});// Start up the Node serverapp.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 loadingimport { 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 webpackconst {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 pathROUTES.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/7200const 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 中導入了 BrowserTransferStateModuleTransferHttpCacheModule

這三個模塊都與服務器到客戶端的狀態傳輸有關:

  1. ServerTransferStateModule:在服務端導入,用于實現將狀態從服務器傳輸到客戶端
  2. BrowserTransferStateModule:在客戶端導入,用于實現將狀態從服務器傳輸到客戶端
  3. 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`));  }}
  1. 使用 const KFCLIST_KEY = makeStateKey('kfcList') 創建儲存傳輸數據的 StateKey
  2. HomeComponent 的構造函數中注入 TransferState
  3. ngOnInit 中根據 this.state.get(KFCLIST_KEY, null as any) 判斷數據是否存在(不管是服務端還是客戶端),存在就不再請求,不存在則請求數據并通過 this.state.set(KFCLIST_KEY, data as any) 存儲傳輸數據
  4. ngOnDestroy 中根據當前是否客戶端來決定是否將存儲的數據進行刪除

以上就是本文的全部內容,希望對大家的學習有所幫助,也希望大家多多支持武林網。

發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 清水河县| 新津县| 荣成市| 竹北市| 斗六市| 松江区| 洪湖市| 都江堰市| 精河县| 工布江达县| 黔西| 南投县| 宣汉县| 淮滨县| 成都市| 夹江县| 双城市| 白水县| 梁平县| 石景山区| 墨脱县| 象山县| 平谷区| 淅川县| 三穗县| 从江县| 梧州市| 沐川县| 蓬莱市| 大冶市| 迁安市| 淄博市| 依兰县| 台南市| 元谋县| 澄城县| 瓦房店市| 怀安县| 延津县| 达州市| 曲阜市|