Angular Universal 服務(wù)端渲染

Angular Universal

Angular在服務(wù)端渲染方面提供一套前后端同構(gòu)解決方案,它就是 Angular Universal(統(tǒng)一平臺(tái)),一項(xiàng)在服務(wù)端運(yùn)行 Angular 應(yīng)用的技術(shù)。

標(biāo)準(zhǔn)的 Angular 應(yīng)用會(huì)執(zhí)行在瀏覽器中,它會(huì)在 DOM 中渲染頁面,以響應(yīng)用戶的操作。

而 Angular Universal 會(huì)在服務(wù)端通過一個(gè)被稱為服務(wù)端渲染(server-side rendering - SSR)的過程生成靜態(tài)的應(yīng)用頁面。

它可以生成這些頁面,并在瀏覽器請(qǐng)求時(shí)直接用它們給出響應(yīng)。 它也可以把頁面預(yù)先生成為 HTML 文件,然后把它們作為靜態(tài)文件供服務(wù)端使用。

工作原理

要制作一個(gè) Universal 應(yīng)用,就要安裝 platform-server 包。 platform-server 包提供了服務(wù)端的 DOM 實(shí)現(xiàn)、XMLHttpRequest 和其它底層特性,但不再依賴瀏覽器。

你要使用 platform-server 模塊而不是 platform-browser 模塊來編譯這個(gè)客戶端應(yīng)用,并且在一個(gè) Web 服務(wù)器上運(yùn)行這個(gè) Universal 應(yīng)用。

服務(wù)器(下面的示例中使用的是 Node Express 服務(wù)器)會(huì)把客戶端對(duì)應(yīng)用頁面的請(qǐng)求傳給 renderModuleFactory 函數(shù)。

renderModuleFactory 函數(shù)接受一個(gè)模板 HTML 頁面(通常是 index.html)、一個(gè)包含組件的 Angular 模塊和一個(gè)用于決定該顯示哪些組件的路由作為輸入。

該路由從客戶端的請(qǐng)求中傳給服務(wù)器。 每次請(qǐng)求都會(huì)給出所請(qǐng)求路由的一個(gè)適當(dāng)?shù)囊晥D。

renderModuleFactory 在模板中的 <app> 標(biāo)記中渲染出哪個(gè)視圖,并為客戶端創(chuàng)建一個(gè)完成的 HTML 頁面。

最后,服務(wù)器就會(huì)把渲染好的頁面返回給客戶端。

為什么要服務(wù)端渲染

三個(gè)主要原因:

  1. 幫助網(wǎng)絡(luò)爬蟲(SEO)
  2. 提升在手機(jī)和低功耗設(shè)備上的性能
  3. 迅速顯示出第首頁

幫助網(wǎng)絡(luò)爬蟲(SEO)

Google、Bing、百度、Facebook、Twitter 和其它搜索引擎或社交媒體網(wǎng)站都依賴網(wǎng)絡(luò)爬蟲去索引你的應(yīng)用內(nèi)容,并且讓它的內(nèi)容可以通過網(wǎng)絡(luò)搜索到。

這些網(wǎng)絡(luò)爬蟲可能不會(huì)像人類那樣導(dǎo)航到你的具有高度交互性的 Angular 應(yīng)用,并為其建立索引。

Angular Universal 可以為你生成應(yīng)用的靜態(tài)版本,它易搜索、可鏈接,瀏覽時(shí)也不必借助 JavaScript。它也讓站點(diǎn)可以被預(yù)覽,因?yàn)槊總€(gè) URL 返回的都是一個(gè)完全渲染好的頁面。

啟用網(wǎng)絡(luò)爬蟲通常被稱為搜索引擎優(yōu)化 (SEO)。

提升手機(jī)和低功耗設(shè)備上的性能

有些設(shè)備不支持 JavaScript 或 JavaScript 執(zhí)行得很差,導(dǎo)致用戶體驗(yàn)不可接受。 對(duì)于這些情況,你可能會(huì)需要該應(yīng)用的服務(wù)端渲染、無 JavaScript 的版本。 雖然有一些限制,不過這個(gè)版本可能是那些完全沒辦法使用該應(yīng)用的人的唯一選擇。

快速顯示首頁

快速顯示首頁對(duì)于吸引用戶是至關(guān)重要的。

如果頁面加載超過了三秒中,那么 53% 的移動(dòng)網(wǎng)站會(huì)被放棄。 你的應(yīng)用需要啟動(dòng)的更快一點(diǎn),以便在用戶決定做別的事情之前吸引他們的注意力。

使用 Angular Universal,你可以為應(yīng)用生成“著陸頁”,它們看起來就和完整的應(yīng)用一樣。 這些著陸頁是純 HTML,并且即使 JavaScript 被禁用了也能顯示。 這些頁面不會(huì)處理瀏覽器事件,不過它們可以用 routerLink 在這個(gè)網(wǎng)站中導(dǎo)航。

在實(shí)踐中,你可能要使用一個(gè)著陸頁的靜態(tài)版本來保持用戶的注意力。 同時(shí),你也會(huì)在幕后加載完整的 Angular 應(yīng)用。 用戶會(huì)認(rèn)為著陸頁幾乎是立即出現(xiàn)的,而當(dāng)完整的應(yīng)用加載完之后,又可以獲得完全的交互體驗(yàn)。

示例解析

下面將基于我在GitHub上的示例項(xiàng)目 angular-universal-starter 來進(jìn)行講解。

這個(gè)項(xiàng)目與第一篇的示例項(xiàng)目一樣,都是基于 Angular CLI進(jìn)行開發(fā)構(gòu)建的,因此它們的區(qū)別只在于服務(wù)端渲染所需的那些配置上。

安裝工具

在開始之前,下列包是必須安裝的(示例項(xiàng)目均已配置好,只需 npm install 即可):

  • @angular/platform-server - Universal 的服務(wù)端元件。
  • @nguniversal/module-map-ngfactory-loader - 用于處理服務(wù)端渲染環(huán)境下的惰性加載。
  • @nguniversal/express-engine - Universal 應(yīng)用的 Express 引擎。
  • ts-loader - 用于對(duì)服務(wù)端應(yīng)用進(jìn)行轉(zhuǎn)譯。
  • express - Node Express 服務(wù)器

使用下列命令安裝它們:

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

項(xiàng)目配置

配置工作有:

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

1、創(chuàng)建服務(wù)端應(yīng)用模塊: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';

// 可以注冊(cè)那些在 Universal 環(huán)境下運(yùn)行應(yīng)用時(shí)特有的服務(wù)提供商
@NgModule({
    imports: [
        AppBrowserModule, // 客戶端應(yīng)用的 AppModule
        ServerModule, // 服務(wù)端的 Angular 模塊
        ModuleMapLoaderModule, // 用于實(shí)現(xiàn)服務(wù)端的路由的惰性加載
        ServerTransferStateModule, // 在服務(wù)端導(dǎo)入,用于實(shí)現(xiàn)將狀態(tài)從服務(wù)器傳輸?shù)娇蛻舳?    ],
    bootstrap: [AppComponent],
})
export class AppServerModule {
}

服務(wù)端應(yīng)用模塊(習(xí)慣上叫作 AppServerModule)是一個(gè) Angular 模塊,它包裝了應(yīng)用的根模塊 AppModule,以便 Universal 可以在你的應(yīng)用和服務(wù)器之間進(jìn)行協(xié)調(diào)。 AppServerModule 還會(huì)告訴 Angular 再把你的應(yīng)用以 Universal 方式運(yùn)行時(shí),該如何引導(dǎo)它。

2、修改客戶端應(yīng)用模塊:src/app/app.module.ts

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, // 用于實(shí)現(xiàn)服務(wù)器到客戶端的請(qǐng)求傳輸緩存,防止客戶端重復(fù)請(qǐng)求服務(wù)端已完成的請(qǐng)求
        BrowserTransferStateModule, // 在客戶端導(dǎo)入,用于實(shí)現(xiàn)將狀態(tài)從服務(wù)器傳輸?shù)娇蛻舳?        HttpClientModule
    ],
    declarations: [
        AppComponent,
        HomeComponent
    ],
    providers: [],
    bootstrap: [AppComponent]
})
export class AppBrowserModule {
    constructor(@Inject(PLATFORM_ID) private platformId: Object,
                @Inject(APP_ID) private appId: string) {

        // 判斷運(yùn)行環(huán)境為客戶端還是服務(wù)端
        const platform = isPlatformBrowser(platformId) ? 'in the browser' : 'on the server';
        console.log(`Running ${platform} with appId=${appId}`);
    }
}

NgModule 的元數(shù)據(jù)中 BrowserModule 的導(dǎo)入改成 BrowserModule.withServerTransition({appId: 'my-app'}),Angular 會(huì)把 appId 值(它可以是任何字符串)添加到服務(wù)端渲染頁面的樣式名中,以便它們?cè)诳蛻舳藨?yīng)用啟動(dòng)時(shí)可以被找到并移除。

此時(shí),我們可以通過依賴注入(@Inject(PLATFORM_ID)@Inject(APP_ID))取得關(guān)于當(dāng)前平臺(tái)和 appId 的運(yùn)行時(shí)信息:

constructor(@Inject(PLATFORM_ID) private platformId: Object,
            @Inject(APP_ID) private appId: string) {

    // 判斷運(yùn)行環(huán)境為客戶端還是服務(wù)端
    const platform = isPlatformBrowser(platformId) ? 'in the browser' : 'on the server';
    console.log(`Running ${platform} with appId=${appId}`);
}

3、創(chuàng)建服務(wù)端應(yīng)用的引導(dǎo)程序文件:src/main.server.ts

該文件導(dǎo)出服務(wù)端模塊:

export { AppServerModule } from './app/app.server.module';

4、修改客戶端應(yīng)用的引導(dǎo)程序文件:src/main.ts

監(jiān)聽 DOMContentLoaded 事件,在發(fā)生 DOMContentLoaded 事件時(shí)運(yùn)行我們的代碼,以使 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 時(shí)運(yùn)行我們的代碼,以使 TransferState 正常工作
document.addEventListener('DOMContentLoaded', () => {
    platformBrowserDynamic().bootstrapModule(AppBrowserModule);
});

5、創(chuàng)建 TypeScript 的服務(wù)端配置: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() 方法導(dǎo)入你的服務(wù)端應(yīng)用。

  • angularCompilerOptions 部分有一些面向 AOT 編譯器的選項(xiàng):

    • entryModule - 服務(wù)端應(yīng)用的根模塊,其格式為 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、創(chuàng)建 Node Express 的服務(wù)程序: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}`);
});
Universal 模板引擎

這個(gè)文件中最重要的部分是 ngExpressEngine 函數(shù):

app.engine('html', ngExpressEngine({
    bootstrap: AppServerModuleNgFactory,
    providers: [
        provideModuleMap(LAZY_MODULE_MAP)
    ]
}));

ngExpressEngine 是對(duì) Universal 的 renderModuleFactory 函數(shù)的封裝。它會(huì)把客戶端請(qǐng)求轉(zhuǎn)換成服務(wù)端渲染的 HTML 頁面。如果你使用不同于Node的服務(wù)端技術(shù),你需要在該服務(wù)端的模板引擎中調(diào)用這個(gè)函數(shù)。

  • 第一個(gè)參數(shù)是你以前寫過的 AppServerModule。 它是 Universal 服務(wù)端渲染器和你的應(yīng)用之間的橋梁。
  • 第二個(gè)參數(shù)是 extraProviders。它是在這個(gè)服務(wù)器上運(yùn)行時(shí)才需要的一些可選的 Angular 依賴注入提供商。當(dāng)你的應(yīng)用需要那些只有當(dāng)運(yùn)行在服務(wù)器實(shí)例中才需要的信息時(shí),就要提供 extraProviders 參數(shù)。

ngExpressEngine 函數(shù)返回了一個(gè)會(huì)解析成渲染好的頁面的承諾(Promise)。

接下來你的引擎要決定拿這個(gè)頁面做點(diǎn)什么。 現(xiàn)在這個(gè)引擎的回調(diào)函數(shù)中,把渲染好的頁面返回給了 Web 服務(wù)器,然后服務(wù)器通過 HTTP 響應(yīng)把它轉(zhuǎn)發(fā)給了客戶端。

8、創(chuàng)建服務(wù)端預(yù)渲染的程序: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、創(chuàng)建 Webpack 的服務(wù)端配置:webpack.server.config.js

Universal 應(yīng)用不需要任何額外的 Webpack 配置,Angular CLI 會(huì)幫我們處理它們。但是由于本例子的 Node Express 的服務(wù)程序是 TypeScript 應(yīng)用(server.ts及prerender.ts),所以要使用 Webpack 來轉(zhuǎn)譯它。這里不討論 Webpack 的配置,需要了解的移步 Webpack官網(wǎng)

// 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'),
            {}
        )
    ]
};

測(cè)試配置

通過上面的配置,我們就制作完成一個(gè)可在服務(wù)端渲染的 Angular Universal 應(yīng)用。

在 package.json 的 scripts 區(qū)配置 build 和 serve 有關(guān)的命令:

{
    "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"
    }
}

開發(fā)只需運(yùn)行 npm run start

執(zhí)行 npm run ssr 編譯應(yīng)用程序,并啟動(dòng)一個(gè)Node Express來為應(yīng)用程序提供服務(wù) http://localhost:4000

dist目錄:

執(zhí)行npm run prerender - 編譯應(yīng)用程序并預(yù)渲染應(yīng)用程序文件,啟動(dòng)一個(gè)演示http服務(wù)器,以便您可以查看它 http://localhost:8080

注意: 要將靜態(tài)網(wǎng)站部署到靜態(tài)托管平臺(tái),您必須部署dist/browser文件夾, 而不是dist文件夾

dist目錄:


根據(jù)項(xiàng)目實(shí)際的路由信息并在根目錄的 static.paths.ts 中配置,提供給 prerender.ts 解析使用。

export const ROUTES = [
    '/',
    '/lazy'
];

因此,從dist目錄可以看到,服務(wù)端預(yù)渲染會(huì)根據(jù)配置好的路由在 browser 生成對(duì)應(yīng)的靜態(tài)index.html。如 / 對(duì)應(yīng) /index.html,/lazy 對(duì)應(yīng) /lazy/index.html。

服務(wù)端的模塊懶加載

在前面的介紹中,我們?cè)?app.server.module.ts 中導(dǎo)入了 ModuleMapLoaderModule,在 app.module.ts。

ModuleMapLoaderModule 模塊可以使得懶加載的模塊也可以在服務(wù)端進(jìn)行渲染,而你要做也只是在 app.server.module.ts中導(dǎo)入。

服務(wù)端到客戶端的狀態(tài)傳輸

在前面的介紹中,我們?cè)?app.server.module.ts 中導(dǎo)入了 ServerTransferStateModule,在 app.module.ts 中導(dǎo)入了 BrowserTransferStateModuleTransferHttpCacheModule

這三個(gè)模塊都與服務(wù)端到客戶端的狀態(tài)傳輸有關(guān):

  • ServerTransferStateModule:在服務(wù)端導(dǎo)入,用于實(shí)現(xiàn)將狀態(tài)從服務(wù)端傳輸?shù)娇蛻舳?/li>
  • BrowserTransferStateModule:在客戶端導(dǎo)入,用于實(shí)現(xiàn)將狀態(tài)從服務(wù)端傳輸?shù)娇蛻舳?/li>
  • TransferHttpCacheModule:用于實(shí)現(xiàn)服務(wù)端到客戶端的請(qǐng)求傳輸緩存,防止客戶端重復(fù)請(qǐng)求服務(wù)端已完成的請(qǐng)求

使用這幾個(gè)模塊,可以解決 http請(qǐng)求在服務(wù)端和客戶端分別請(qǐng)求一次 的問題。

比如在 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`));
    }
}

代碼運(yùn)行之后,

服務(wù)端請(qǐng)求并打印:



客戶端再一次請(qǐng)求并打?。?/p>

方法1:使用 TransferHttpCacheModule
使用 TransferHttpCacheModule 很簡單,代碼不需要改動(dòng)。在 app.module.ts 中導(dǎo)入之后,Angular自動(dòng)會(huì)將服務(wù)端請(qǐng)求緩存到客戶端,換句話說就是服務(wù)端請(qǐng)求到數(shù)據(jù)會(huì)自動(dòng)傳輸?shù)娇蛻舳耍蛻舳私邮盏綌?shù)據(jù)之后就不會(huì)再發(fā)送請(qǐng)求了。

方法2:使用 BrowserTransferStateModule
該方法稍微復(fù)雜一些,需要改動(dòng)一些代碼。

調(diào)整 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() {

    // 采用一個(gè)標(biāo)記來區(qū)分服務(wù)端是否已經(jīng)拿到了數(shù)據(jù),如果沒拿到數(shù)據(jù)就在客戶端請(qǐng)求,如果已經(jīng)拿到數(shù)據(jù)就不發(fā)請(qǐng)求
    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); // 存儲(chǔ)數(shù)據(jù)
        });
    }
}

ngOnDestroy() {
    if (typeof window === 'object') {
        this.state.set(KFCLIST_KEY, null as any); // 刪除數(shù)據(jù)
    }
}

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') 創(chuàng)建儲(chǔ)存?zhèn)鬏敂?shù)據(jù)的 StateKey
在 HomeComponent 的構(gòu)造函數(shù)中注入 TransferState
在 ngOnInit 中根據(jù) this.state.get(KFCLIST_KEY, null as any) 判斷數(shù)據(jù)是否存在(不管是服務(wù)端還是客戶端),存在就不再請(qǐng)求,不存在則請(qǐng)求數(shù)據(jù)并通過 this.state.set(KFCLIST_KEY, data as any) 存儲(chǔ)傳輸數(shù)據(jù)
在 ngOnDestroy 中根據(jù)當(dāng)前是否客戶端來決定是否將存儲(chǔ)的數(shù)據(jù)進(jìn)行刪除
客戶端與服務(wù)端渲染對(duì)比
最后,我們分別通過這三個(gè)原因來進(jìn)行對(duì)比:

幫助網(wǎng)絡(luò)爬蟲(SEO)
提升在手機(jī)和低功耗設(shè)備上的性能
迅速顯示出首頁
幫助網(wǎng)絡(luò)爬蟲(SEO)
客戶端渲染:



服務(wù)端渲染:
從上面可以看到,服務(wù)端提前將信息渲染到返回的頁面上,這樣網(wǎng)絡(luò)爬蟲就能直接獲取到信息了(網(wǎng)絡(luò)爬蟲基本不會(huì)解析javascript的)。

提升在手機(jī)和低功耗設(shè)備上的性能
這個(gè)原因通過上面就可以看出,對(duì)于一些低端的設(shè)備,直接顯示頁面總比要解析javascript性能高的多。

迅速顯示出首頁
同樣在 Fast 3G 網(wǎng)絡(luò)條件下進(jìn)行測(cè)試

客戶端渲染:


服務(wù)端渲染:


牢記幾件事情

  • 對(duì)于服務(wù)器軟件包,您可能需要將第三方模塊包含到nodeExternals白名單中

  • window, document, navigator 以及其它的瀏覽器類型 - 不存在于服務(wù)端 - 如果你直接使用,在服務(wù)端將無法正常工作。 以下幾種方法可以讓你的代碼正常工作:

    • 可以通過PLATFORM_ID標(biāo)記注入的Object來檢查當(dāng)前平臺(tái)是瀏覽器還是服務(wù)器,然后使用瀏覽器端特有的類型

       import { PLATFORM_ID } from '@angular/core';
       import { isPlatformBrowser, isPlatformServer } from '@angular/common';
      
       constructor(@Inject(PLATFORM_ID) private platformId: Object) { ... }
      
       ngOnInit() {
         if (isPlatformBrowser(this.platformId)) {
            // 僅運(yùn)行在瀏覽器端的代碼
            ...
         }
         if (isPlatformServer(this.platformId)) {
           // 僅運(yùn)行在服務(wù)端的代碼
           ...
         }
       }
      

 - 盡量**限制**或**避免**使用`setTimeout`。它會(huì)減慢服務(wù)器端的渲染過程。確保在組件的`ngOnDestroy`中刪除它們

 - 對(duì)于RxJs超時(shí),請(qǐng)確保在成功時(shí) _取消_ 它們的流,因?yàn)樗鼈円矔?huì)降低渲染速度。

  • 不要直接操作nativeElement,使用Renderer2,從而可以跨平臺(tái)改變應(yīng)用視圖。
constructor(element: ElementRef, renderer: Renderer2) {
  this.renderer.setStyle(element.nativeElement, 'font-size', 'x-large');
}
  • 解決應(yīng)用程序在服務(wù)器上運(yùn)行XHR請(qǐng)求,并在客戶端再次運(yùn)行的問題

    • 使用從服務(wù)器傳輸?shù)娇蛻舳说木彺妫═ransferState)
  • 清楚了解與DOM相關(guān)的屬性和屬性之間的差異

  • 盡量讓指令無狀態(tài)。對(duì)于有狀態(tài)指令,您可能需要提供一個(gè)屬性,以反映相應(yīng)屬性的初始字符串值,例如img標(biāo)簽中的url。對(duì)于我們的native元素,src屬性被反映為元素類型HTMLImageElement的src屬性

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容