Maison  >  Article  >  interface Web  >  Comment développer le rendu côté serveur avec Angular

Comment développer le rendu côté serveur avec Angular

php中世界最好的语言
php中世界最好的语言original
2018-05-11 13:47:411698parcourir

Cette fois, je vais vous montrer comment développer le rendu côté serveur dans Angular. Quelles sont les précautions pour qu'Angular développe le rendu côté serveur, jetons un coup d'oeil.

Angular Universal

Angular fournit un ensemble de solutions isomorphes front-end et back-end pour le rendu côté serveur. Il s'agit d'Angular Universal (plateforme unifiée). Technologie côté serveur pour exécuter des applications angulaires.

Une application Angular standard sera exécutée dans le navigateur et restituera la page dans le DOM en réponse aux opérations de l'utilisateur.

Angular Universal génère des pages d'application statiques sur le serveur via un processus appelé rendu côté serveur (SSR).

Il peut générer ces pages et y répondre directement à la demande du navigateur. Il peut également pré-générer des pages dans des fichiers HTML, puis les servir comme fichiers statiques pour le serveur.

Comment ça marche

Pour créer une application universelle, vous devez installer le package platform-server. Le package platform-server fournit une implémentation DOM côté serveur, XMLHttpRequest et d'autres fonctionnalités de bas niveau, mais ne repose plus sur le navigateur.

Vous devez utiliser le module platform-server au lieu du module platform-browser pour compiler l'application client et exécuter l'application universelle sur un serveur Web. Le serveur

(le serveur Node Express est utilisé dans l'exemple ci-dessous) transmettra la demande du client pour la page d'application à la fonction renderModuleFactory. La fonction

renderModuleFactory accepte en entrée un modèle de page HTML (généralement index.html), un module Angular contenant les composants et une route qui détermine quels composants doivent être affichés.

Cette route est transmise au serveur à partir de la demande du client. Chaque demande donnera une vue appropriée de l'itinéraire demandé.

renderModuleFactory restitue la vue dans la balise <app> du modèle et crée une page HTML terminée pour le client.

Enfin, le serveur renverra la page rendue au client.

Pourquoi le rendu côté serveur

Trois raisons principales :

  1. Aider les robots d'exploration du Web (SEO)

  2. Améliorez les performances sur les téléphones mobiles et les appareils à faible consommation

  3. Affichez rapidement la première page

Aide Robots d'exploration Web (SEO)

Google, Bing, Baidu, Facebook, Twitter et d'autres moteurs de recherche ou sites de médias sociaux s'appuient sur des robots d'exploration Web pour indexer le contenu de votre application et rendre le contenu pouvant être recherché via Internet. .

Ces robots d'exploration Web peuvent ne pas naviguer vers et indexer votre application Angular hautement interactive comme le ferait un humain.

Angular Universal peut générer une version statique de votre application consultable, pouvant être liée et parcourue sans avoir besoin de JavaScript. Il permet également de prévisualiser le site, puisque chaque URL renvoie une page entièrement rendue.

L'activation des robots d'exploration Web est souvent appelée optimisation des moteurs de recherche (SEO).

Amélioration des performances sur les téléphones mobiles et les appareils à faible consommation

Certains appareils ne prennent pas en charge JavaScript ou JavaScript est mal implémenté, ce qui entraîne une expérience utilisateur inacceptable. Dans ces cas, vous souhaiterez peut-être une version de l'application rendue par le serveur et sans JavaScript. Bien qu'il existe certaines limitations, cette version peut être la seule option pour ceux qui n'ont aucun moyen d'utiliser l'application.

Afficher rapidement la page d'accueil

Afficher rapidement la page d'accueil est crucial pour attirer les utilisateurs.

53% des sites Web mobiles sont abandonnés si une page met plus de trois secondes à se charger. Votre application doit se lancer plus rapidement pour attirer l'attention de l'utilisateur avant qu'il ne décide de faire autre chose.

En utilisant Angular Universal, vous pouvez générer des « pages de destination » pour votre application qui ressemblent à l'application complète. Ces pages de destination sont en HTML pur et s'affichent même si JavaScript est désactivé. Ces pages ne gèrent pas les événements du navigateur, mais elles peuvent être parcourues au sein du site à l'aide de routerLink.

En pratique, vous souhaiterez peut-être utiliser une version statique de la page de destination pour retenir l’attention de l’utilisateur. Dans le même temps, vous chargerez également l’application Angular complète en coulisses. Les utilisateurs s'attendront à ce que la page de destination apparaisse presque instantanément et, une fois l'application complète chargée, ils vivront une expérience entièrement interactive.

Exemple d'analyse

Ce qui suit sera expliqué en fonction de mon exemple de projet angulaire-universal-starter sur GitHub.

Ce projet, comme l'exemple de projet dans le premier article, est développé et construit sur la base d'Angular CLI, donc la seule différence entre eux réside dans la configuration requise pour le rendu côté serveur.

Outils d'installation

Avant de commencer, les packages suivants doivent être installés (les exemples de projets ont été configurés, juste npm install suffit) :

  1. @angular/platform-server - Composant serveur pour Universal.

  2. @nguniversal/module-map-ngfactory-loader - Utilisé pour gérer le chargement paresseux dans un environnement de rendu côté serveur.

  3. @nguniversal/express-engine - Moteur express pour applications universelles.

  4. ts-loader - utilisé pour traduire les applications côté serveur.

  5. express - Serveur Node Express

Installez-les à l'aide de la commande suivante :

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

Configuration du projet

Le travail de configuration comprend :

  1. Créer un module d'application serveur : src/app/app.server.module.ts

  2. Modifier Module d'application client : src/app/app.module.ts

  3. Créer le fichier bootstrap de l'application serveur : src/main.server.ts

  4. Modifier le fichier bootstrap de l'application client : src/main.ts

  5. Créer la configuration serveur de TypeScript : src/tsconfig.server.json

  6. Modifier le fichier de configuration de @angular/cli : .angular-cli.json

  7. Créer un programme de service Node Express : server.ts

  8. Créez le programme de pré-rendu côté serveur : prerender.ts

  9. Créez la configuration côté serveur de Webpack : webpack.server.config.js

1. Créez un module d'application serveur : 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 {
}

Le module d'application serveur (habituellement appelé AppServerModule) est un module Angular qui enveloppe l'application La racine module AppModule afin qu'Universal puisse se coordonner entre votre application et le serveur. AppServerModule indique également à Angular comment amorcer votre application lorsqu'elle est exécutée en tant qu'Universal.

2. Modifiez le module de l'application client : 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}`);
  }
}

Modifiez l'import de BrowserModule dans les métadonnées de NgModule en BrowserModule.withServerTransition({appId: 'my-app '}), Angular ajoutera la valeur appId (qui peut être n'importe quelle chaîne) au nom de style de la page rendue par le serveur afin qu'elle puisse être trouvée et supprimée au démarrage de l'application client.

À ce stade, nous pouvons obtenir des informations d'exécution sur la plate-forme actuelle et l'appId via Injection de dépendances (@Inject(PLATFORM_ID) et @Inject(APP_ID)) :

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 , Créez le fichier bootstrap de l'application serveur : src/main.server.ts

Ce fichier exporte le module serveur :

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

4. Modifiez le fichier bootstrap de l'application client : src/main.ts

Écoutez l'événement DOMContentLoaded et exécutez notre code lorsque l'événement DOMContentLoaded se produit pour que TransferState fonctionne correctement

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. Créer une configuration côté serveur TypeScript : src/tsconfig.server.json

La différence entre
{
 "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"
 }
}
et

est la suivante : tsconfig.app.json

l'attribut du module doit être commonjs, afin qu'il puisse être importé dans votre application serveur par la méthode require(). La section

angularCompilerOptions contient quelques options pour le compilateur AOT :

  1. entryModule - le module racine de l'application serveur, avec le format chemin/vers/fichier#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 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 中导入了 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 中根据当前是否客户端来决定是否将存储的数据进行删除

相信看了本文案例你已经掌握了方法,更多精彩请关注php中文网其它相关文章!

推荐阅读:

在vue里使用axios步骤详解

axios+post方法提交formdata步骤详解

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration:
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn