Angular开发实践(六):服务端渲染

Angular Universal

Angular在服务端渲染方面提供一套先后端同构解决方案,它就是 Angular Universal(统一平台),一项在服务端运行 Angular 应用的技术。javascript

标准的 Angular 应用会执行在浏览器中,它会在 DOM 中渲染页面,以响应用户的操做。css

而 Angular Universal 会在服务端经过一个被称为服务端渲染(server-side rendering - SSR)的过程生成静态的应用页面。html

它能够生成这些页面,并在浏览器请求时直接用它们给出响应。 它也能够把页面预先生成为 HTML 文件,而后把它们做为静态文件供服务端使用。java

工做原理

要制做一个 Universal 应用,就要安装 platform-server 包。 platform-server 包提供了服务端的 DOM 实现、XMLHttpRequest 和其它底层特性,但再也不依赖浏览器。node

你要使用 platform-server 模块而不是 platform-browser 模块来编译这个客户端应用,而且在一个 Web 服务器上运行这个 Universal 应用。webpack

服务器(下面的示例中使用的是 Node Express 服务器)会把客户端对应用页面的请求传给 renderModuleFactory 函数。git

renderModuleFactory 函数接受一个模板 HTML 页面(一般是 index.html)、一个包含组件的 Angular 模块和一个用于决定该显示哪些组件的路由做为输入。github

该路由从客户端的请求中传给服务器。 每次请求都会给出所请求路由的一个适当的视图。web

renderModuleFactory 在模板中的 <app> 标记中渲染出哪一个视图,并为客户端建立一个完成的 HTML 页面。typescript

最后,服务器就会把渲染好的页面返回给客户端。

为何要服务端渲染

三个主要缘由:

  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 便可):

  • @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
复制代码

项目配置

配置工做有:

  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

一、建立服务端应用模块: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 方式运行时,该如何引导它。

二、修改客户端应用模块: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, // 用于实现服务器到客户端的请求传输缓存,防止客户端重复请求服务端已完成的请求
		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';
复制代码

四、修改客户端应用的引导程序文件: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);
});
复制代码

五、建立 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 编译器的选项:

    • entryModule - 服务端应用的根模块,其格式为 path/to/file#ClassName。

六、修改 @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"
    }
}
复制代码

七、建立 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}`);
});
复制代码
Universal 模板引擎

这个文件中最重要的部分是 ngExpressEngine 函数:

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

ngExpressEngine 是对 Universal 的 renderModuleFactory 函数的封装。它会把客户端请求转换成服务端渲染的 HTML 页面。若是你使用不一样于Node的服务端技术,你须要在该服务端的模板引擎中调用这个函数。

  • 第一个参数是你之前写过的 AppServerModule。 它是 Universal 服务端渲染器和你的应用之间的桥梁。

  • 第二个参数是 extraProviders。它是在这个服务器上运行时才须要的一些可选的 Angular 依赖注入提供商。当你的应用须要那些只有当运行在服务器实例中才须要的信息时,就要提供 extraProviders 参数。

ngExpressEngine 函数返回了一个会解析成渲染好的页面的承诺(Promise)。

接下来你的引擎要决定拿这个页面作点什么。 如今这个引擎的回调函数中,把渲染好的页面返回给了 Web 服务器,而后服务器经过 HTTP 响应把它转发给了客户端。

八、建立服务端预渲染的程序: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));
});
复制代码

九、建立 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 中导入了 ModuleMapLoaderModule,在 app.module.ts

ModuleMapLoaderModule 模块可使得懒加载的模块也能够在服务端进行渲染,而你要作也只是在 app.server.module.ts 中导入。

服务端到客户端的状态传输

在前面的介绍中,咱们在 app.server.module.ts 中导入了 ServerTransferStateModule,在 app.module.ts 中导入了 BrowserTransferStateModuleTransferHttpCacheModule

这三个模块都与服务端到客户端的状态传输有关:

  • 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 中根据当前是否客户端来决定是否将存储的数据进行删除

客户端与服务端渲染对比

最后,咱们分别经过这三个缘由来进行对比:

  1. 帮助网络爬虫(SEO)

  2. 提高在手机和低功耗设备上的性能

  3. 迅速显示出首页

帮助网络爬虫(SEO)

客户端渲染:

服务端渲染:

从上面能够看到,服务端提早将信息渲染到返回的页面上,这样网络爬虫就能直接获取到信息了(网络爬虫基本不会解析javascript的)。

提高在手机和低功耗设备上的性能

这个缘由经过上面就能够看出,对于一些低端的设备,直接显示页面总比要解析javascript性能高的多。

迅速显示出首页

一样在 Fast 3G 网络条件下进行测试

客户端渲染:

服务端渲染:

牢记几件事情

  • 对于服务器软件包,您可能须要将第三方模块包含到nodeExternals白名单中

  • window, document, navigator 以及其它的浏览器类型 - 不存在于服务端 - 若是你直接使用,在服务端将没法正常工做。 如下几种方法可让你的代码正常工做:

    • 能够经过PLATFORM_ID标记注入的Object来检查当前平台是浏览器仍是服务器,而后使用浏览器端特有的类型
    import { PLATFORM_ID } from '@angular/core';
     import { isPlatformBrowser, isPlatformServer } from '@angular/common';
     
     constructor(@Inject(PLATFORM_ID) private platformId: Object) { ... }
     
     ngOnInit() {
       if (isPlatformBrowser(this.platformId)) {
          // 仅运行在浏览器端的代码
          ...
       }
       if (isPlatformServer(this.platformId)) {
         // 仅运行在服务端的代码
         ...
       }
     }
    复制代码
    • 尽可能限制避免使用setTimeout。它会减慢服务器端的渲染过程。确保在组件的ngOnDestroy中删除它们

    • 对于RxJs超时,请确保在成功时 取消 它们的流,由于它们也会下降渲染速度。

  • 不要直接操做nativeElement,使用Renderer2,从而能够跨平台改变应用视图。

constructor(element: ElementRef, renderer: Renderer2) {
  this.renderer.setStyle(element.nativeElement, 'font-size', 'x-large');
}
复制代码
  • 解决应用程序在服务器上运行XHR请求,并在客户端再次运行的问题
    • 使用从服务器传输到客户端的缓存(TransferState)
  • 清楚了解与DOM相关的属性和属性之间的差别
  • 尽可能让指令无状态。对于有状态指令,您可能须要提供一个属性,以反映相应属性的初始字符串值,例如img标签中的url。对于咱们的native元素,src属性被反映为元素类型HTMLImageElement的src属性

转载请注明出处,谢谢!

相关文章
相关标签/搜索