摘要:在服務(wù)端渲染方面提供一套前后端同構(gòu)解決方案,它就是統(tǒng)一平臺,一項(xiàng)在服務(wù)端運(yùn)行應(yīng)用的技術(shù)。這些著陸頁是純,并且即使被禁用了也能顯示。它會把客戶端請求轉(zhuǎn)換成服務(wù)端渲染的頁面。它是服務(wù)端渲染器和你的應(yīng)用之間的橋梁。
Angular Universal
Angular在服務(wù)端渲染方面提供一套前后端同構(gòu)解決方案,它就是 Angular Universal(統(tǒng)一平臺),一項(xiàng)在服務(wù)端運(yùn)行 Angular 應(yīng)用的技術(shù)。
標(biāo)準(zhǔn)的 Angular 應(yīng)用會執(zhí)行在瀏覽器中,它會在 DOM 中渲染頁面,以響應(yīng)用戶的操作。
而 Angular Universal 會在服務(wù)端通過一個(gè)被稱為服務(wù)端渲染(server-side rendering - SSR)的過程生成靜態(tài)的應(yī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ù)器)會把客戶端對應(yīng)用頁面的請求傳給 renderModuleFactory 函數(shù)。
renderModuleFactory 函數(shù)接受一個(gè)模板 HTML 頁面(通常是 index.html)、一個(gè)包含組件的 Angular 模塊和一個(gè)用于決定該顯示哪些組件的路由作為輸入。
該路由從客戶端的請求中傳給服務(wù)器。 每次請求都會給出所請求路由的一個(gè)適當(dāng)?shù)囊晥D。
renderModuleFactory 在模板中的
最后,服務(wù)器就會把渲染好的頁面返回給客戶端。
為什么要服務(wù)端渲染三個(gè)主要原因:
幫助網(wǎng)絡(luò)爬蟲(SEO)
提升在手機(jī)和低功耗設(shè)備上的性能
迅速顯示出第首頁
幫助網(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ò)爬蟲可能不會像人類那樣導(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)不可接受。 對于這些情況,你可能會需要該應(yīng)用的服務(wù)端渲染、無 JavaScript 的版本。 雖然有一些限制,不過這個(gè)版本可能是那些完全沒辦法使用該應(yīng)用的人的唯一選擇。
快速顯示首頁快速顯示首頁對于吸引用戶是至關(guān)重要的。
如果頁面加載超過了三秒中,那么 53% 的移動網(wǎng)站會被放棄。 你的應(yīng)用需要啟動的更快一點(diǎn),以便在用戶決定做別的事情之前吸引他們的注意力。
使用 Angular Universal,你可以為應(yīng)用生成“著陸頁”,它們看起來就和完整的應(yīng)用一樣。 這些著陸頁是純 HTML,并且即使 JavaScript 被禁用了也能顯示。 這些頁面不會處理瀏覽器事件,不過它們可以用 routerLink 在這個(gè)網(wǎng)站中導(dǎo)航。
在實(shí)踐中,你可能要使用一個(gè)著陸頁的靜態(tài)版本來保持用戶的注意力。 同時(shí),你也會在幕后加載完整的 Angular 應(yīng)用。 用戶會認(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 - 用于對服務(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)目配置
配置工作有:
創(chuàng)建服務(wù)端應(yīng)用模塊:src/app/app.server.module.ts
修改客戶端應(yīng)用模塊:src/app/app.module.ts
創(chuàng)建服務(wù)端應(yīng)用的引導(dǎo)程序文件:src/main.server.ts
修改客戶端應(yīng)用的引導(dǎo)程序文件:src/main.ts
創(chuàng)建 TypeScript 的服務(wù)端配置:src/tsconfig.server.json
修改 @angular/cli 的配置文件:.angular-cli.json
創(chuàng)建 Node Express 的服務(wù)程序:server.ts
創(chuàng)建服務(wù)端預(yù)渲染的程序:prerender.ts
創(chuàng)建 Webpack 的服務(wù)端配置:webpack.server.config.js
1、創(chuàng)建服務(wù)端應(yīng)用模塊:src/app/app.server.module.tsimport { 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 環(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 還會告訴 Angular 再把你的應(yīng)用以 Universal 方式運(yùn)行時(shí),該如何引導(dǎo)它。
2、修改客戶端應(yīng)用模塊:src/app/app.module.tsimport { 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ù)器到客戶端的請求傳輸緩存,防止客戶端重復(fù)請求服務(wù)端已完成的請求 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 會把 appId 值(它可以是任何字符串)添加到服務(wù)端渲染頁面的樣式名中,以便它們在客戶端應(yīng)用啟動時(shí)可以被找到并移除。
此時(shí),我們可以通過依賴注入(@Inject(PLATFORM_ID) 及 @Inject(APP_ID))取得關(guān)于當(dāng)前平臺和 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}`); });
這個(gè)文件中最重要的部分是 ngExpressEngine 函數(shù):
app.engine("html", ngExpressEngine({ bootstrap: AppServerModuleNgFactory, providers: [ provideModuleMap(LAZY_MODULE_MAP) ] }));
ngExpressEngine 是對 Universal 的 renderModuleFactory 函數(shù)的封裝。它會把客戶端請求轉(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è)會解析成渲染好的頁面的承諾(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 會幫我們處理它們。但是由于本例子的 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"), {} ) ] };測試配置
通過上面的配置,我們就制作完成一個(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)用程序,并啟動一個(gè)Node Express來為應(yīng)用程序提供服務(wù) http://localhost:4000
dist目錄:
執(zhí)行npm run prerender - 編譯應(yīng)用程序并預(yù)渲染應(yīng)用程序文件,啟動一個(gè)演示http服務(wù)器,以便您可以查看它 http://localhost:8080注意: 要將靜態(tài)網(wǎng)站部署到靜態(tài)托管平臺,您必須部署dist/browser文件夾, 而不是dist文件夾
dist目錄:
根據(jù)項(xiàng)目實(shí)際的路由信息并在根目錄的 static.paths.ts 中配置,提供給 prerender.ts 解析使用。
export const ROUTES = [ "/", "/lazy" ];
因此,從dist目錄可以看到,服務(wù)端預(yù)渲染會根據(jù)配置好的路由在 browser 生成對應(yīng)的靜態(tài)index.html。如 / 對應(yīng) /index.html,/lazy 對應(yīng) /lazy/index.html。
服務(wù)端的模塊懶加載在前面的介紹中,我們在 app.server.module.ts 中導(dǎo)入了 ModuleMapLoaderModule,在 app.module.ts。
ModuleMapLoaderModule 模塊可以使得懶加載的模塊也可以在服務(wù)端進(jìn)行渲染,而你要做也只是在 app.server.module.ts 中導(dǎo)入。
服務(wù)端到客戶端的狀態(tài)傳輸在前面的介紹中,我們在 app.server.module.ts 中導(dǎo)入了 ServerTransferStateModule,在 app.module.ts 中導(dǎo)入了 BrowserTransferStateModule 和 TransferHttpCacheModule。
這三個(gè)模塊都與服務(wù)端到客戶端的狀態(tài)傳輸有關(guān):
ServerTransferStateModule:在服務(wù)端導(dǎo)入,用于實(shí)現(xiàn)將狀態(tài)從服務(wù)端傳輸?shù)娇蛻舳?/p>
BrowserTransferStateModule:在客戶端導(dǎo)入,用于實(shí)現(xiàn)將狀態(tài)從服務(wù)端傳輸?shù)娇蛻舳?/p>
TransferHttpCacheModule:用于實(shí)現(xiàn)服務(wù)端到客戶端的請求傳輸緩存,防止客戶端重復(fù)請求服務(wù)端已完成的請求
使用這幾個(gè)模塊,可以解決 http請求在服務(wù)端和客戶端分別請求一次 的問題。
比如在 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{ 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ù)端請求并打印:
客戶端再一次請求并打?。?/p>
方法1:使用 TransferHttpCacheModule使用 TransferHttpCacheModule 很簡單,代碼不需要改動。在 app.module.ts 中導(dǎo)入之后,Angular自動會將服務(wù)端請求緩存到客戶端,換句話說就是服務(wù)端請求到數(shù)據(jù)會自動傳輸?shù)娇蛻舳耍蛻舳私邮盏綌?shù)據(jù)之后就不會再發(fā)送請求了。
方法2:使用 BrowserTransferStateModule該方法稍微復(fù)雜一些,需要改動一些代碼。
調(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ù)就在客戶端請求,如果已經(jīng)拿到數(shù)據(jù)就不發(fā)請求 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); // 存儲數(shù)據(jù) }); } } ngOnDestroy() { if (typeof window === "object") { this.state.set(KFCLIST_KEY, null as any); // 刪除數(shù)據(jù) } } poiSearch(text: string, city?: string): Observable{ 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)建儲存?zhèn)鬏敂?shù)據(jù)的 StateKey
在 HomeComponent 的構(gòu)造函數(shù)中注入 TransferState
在 ngOnInit 中根據(jù) this.state.get(KFCLIST_KEY, null as any) 判斷數(shù)據(jù)是否存在(不管是服務(wù)端還是客戶端),存在就不再請求,不存在則請求數(shù)據(jù)并通過 this.state.set(KFCLIST_KEY, data as any) 存儲傳輸數(shù)據(jù)
在 ngOnDestroy 中根據(jù)當(dāng)前是否客戶端來決定是否將存儲的數(shù)據(jù)進(jìn)行刪除
客戶端與服務(wù)端渲染對比最后,我們分別通過這三個(gè)原因來進(jìn)行對比:
幫助網(wǎng)絡(luò)爬蟲(SEO)
提升在手機(jī)和低功耗設(shè)備上的性能
迅速顯示出首頁
幫助網(wǎng)絡(luò)爬蟲(SEO)客戶端渲染:
服務(wù)端渲染:
從上面可以看到,服務(wù)端提前將信息渲染到返回的頁面上,這樣網(wǎng)絡(luò)爬蟲就能直接獲取到信息了(網(wǎng)絡(luò)爬蟲基本不會解析javascript的)。
提升在手機(jī)和低功耗設(shè)備上的性能這個(gè)原因通過上面就可以看出,對于一些低端的設(shè)備,直接顯示頁面總比要解析javascript性能高的多。
迅速顯示出首頁同樣在 Fast 3G 網(wǎng)絡(luò)條件下進(jìn)行測試
客戶端渲染:
服務(wù)端渲染:
牢記幾件事情對于服務(wù)器軟件包,您可能需要將第三方模塊包含到nodeExternals白名單中
window, document, navigator 以及其它的瀏覽器類型 - 不存在于服務(wù)端 - 如果你直接使用,在服務(wù)端將無法正常工作。 以下幾種方法可以讓你的代碼正常工作:
可以通過PLATFORM_ID標(biāo)記注入的Object來檢查當(dāng)前平臺是瀏覽器還是服務(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`。它會減慢服務(wù)器端的渲染過程。確保在組件的`ngOnDestroy`中刪除它們 - 對于RxJs超時(shí),請確保在成功時(shí) _取消_ 它們的流,因?yàn)樗鼈円矔档弯秩舅俣取?
不要直接操作nativeElement,使用Renderer2,從而可以跨平臺改變應(yīng)用視圖。
constructor(element: ElementRef, renderer: Renderer2) { this.renderer.setStyle(element.nativeElement, "font-size", "x-large"); }
解決應(yīng)用程序在服務(wù)器上運(yùn)行XHR請求,并在客戶端再次運(yùn)行的問題
使用從服務(wù)器傳輸?shù)娇蛻舳说木彺妫═ransferState)
清楚了解與DOM相關(guān)的屬性和屬性之間的差異
盡量讓指令無狀態(tài)。對于有狀態(tài)指令,您可能需要提供一個(gè)屬性,以反映相應(yīng)屬性的初始字符串值,例如img標(biāo)簽中的url。對于我們的native元素,src屬性被反映為元素類型HTMLImageElement的src屬性
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/93811.html
摘要:為了能夠支持跨平臺,通過抽象層封裝了不同平臺的差異。這樣我們就引出抽象類來對元素進(jìn)行設(shè)置樣式屬性插入子元素等操作。 在《Angular開發(fā)實(shí)踐(六):服務(wù)端渲染》這篇文章的最后,我們也提到了在服務(wù)端渲染中需要牢記的幾件事件,其中就包括不要使用window、 document、 navigator等瀏覽器特有的類型以及直接操作DOM元素。 這樣就引出了 Angular 主要特性之一:橫跨...
摘要:已被所有主流瀏覽器支持在過去幾周蘋果的瀏覽器與微軟的瀏覽器分別發(fā)布新版本,支持了,再加上早已支持的和,已得到所有主流瀏覽器支持。 showImg(https://segmentfault.com/img/remote/1460000012086220?w=1240&h=823); 前端每周清單第 40 期: JS 的 Core 與 Cost,Node 內(nèi)存溢出調(diào)試,Software 2...
摘要:五六月份推薦集合查看最新的請點(diǎn)擊集前端最近很火的框架資源定時(shí)更新,歡迎一下。蘇幕遮燎沈香宋周邦彥燎沈香,消溽暑。鳥雀呼晴,侵曉窺檐語。葉上初陽乾宿雨,水面清圓,一一風(fēng)荷舉。家住吳門,久作長安旅。五月漁郎相憶否。小楫輕舟,夢入芙蓉浦。 五、六月份推薦集合 查看github最新的Vue weekly;請::點(diǎn)擊::集web前端最近很火的vue2框架資源;定時(shí)更新,歡迎 Star 一下。 蘇...
摘要:五六月份推薦集合查看最新的請點(diǎn)擊集前端最近很火的框架資源定時(shí)更新,歡迎一下。蘇幕遮燎沈香宋周邦彥燎沈香,消溽暑。鳥雀呼晴,侵曉窺檐語。葉上初陽乾宿雨,水面清圓,一一風(fēng)荷舉。家住吳門,久作長安旅。五月漁郎相憶否。小楫輕舟,夢入芙蓉浦。 五、六月份推薦集合 查看github最新的Vue weekly;請::點(diǎn)擊::集web前端最近很火的vue2框架資源;定時(shí)更新,歡迎 Star 一下。 蘇...
閱讀 3222·2021-11-23 09:51
閱讀 3566·2021-11-09 09:46
閱讀 3673·2021-11-09 09:45
閱讀 2949·2019-08-29 17:31
閱讀 1867·2019-08-26 13:39
閱讀 2726·2019-08-26 12:12
閱讀 3624·2019-08-26 12:08
閱讀 2243·2019-08-26 11:31