游乐游手机版
首页/前端开发/文章详情

Angular SSR服务端渲染原理与实战解析

时间:2026-06-16 07:06
AngularUniversal在服务端渲染Angular应用,生成静态HTML,提升SEO友好性、移动端性能和首屏加载速度。通过CLI命令添加SSR支持,需替换浏览器API,使用绝对地址处理HTTP请求,支持预渲染和动态路由配置,并利用Title和Meta服务优化SEO。

你知道 Angular Universal 吗?这可是让网站获得更好 SEO 支持的一大利器。

深入浅析Angular SSR

通常情况下,Angular 应用是在浏览器中运行的,直接在 DOM 上渲染页面并与用户交互。而 Angular Universal 则是在服务端完成渲染(也就是我们常说的 Server-Side Rendering,SSR),生成静态的应用程序网页,再交给客户端展示。这么做的好处很明显——页面渲染速度更快,能在提供完整交互之前,先把内容展示给用户。

话说在前头,本文的示例是基于 Angular 14 环境完成的,新版本中的部分细节可能有所不同,建议结合 Angular 官方文档来参考。

使用 SSR 的好处

对 SEO 更加友好

虽然包括 Google 在内的某些搜索引擎和社交媒体,都声称能对由 JavaScript 驱动的单页应用(SPA)进行内容爬取,但从实际效果来看,结果往往差强人意。静态 HTML 网站的 SEO 表现,依然要明显优于依赖动态渲染的网站。这一点,Angular 官方也持有同样的看法——别忘了,Angular 可是 Google 亲生的。

通过 Universal,我们可以生成无 JavaScript 的静态版本应用,这对于搜索爬虫、外部链接导航来说,支持得更加到位。

提高移动端的性能

部分移动设备可能不支持或仅有限地支持 JavaScript,这直接导致网站访问体验大打折扣。在这样的场景下,提供一个无 JS 版本的应用,就显得尤为必要了。

更快地展示首页

用户对首屏加载速度的容忍度极低。根据 eBay 的数据,搜索结果展示速度每提升 100 毫秒,用户“加入购物车”的使用率就能提高 0.5%。这个数字足够说明问题了。

有了 Universal,应用的首页会以完整形态的纯 HTML 网页呈现给用户。即便浏览器不支持 JavaScript,页面内容也照样能展示出来。虽然此时网页还不能处理浏览器事件,但通过 routerLink 进行页面导航是没问题的。

这一方案的精妙之处在于:先用静态页面抓住用户的注意力,在他们浏览页面的同时,后台悄无声息地加载整个 Angular 应用,带给用户一种极速加载的体验。

为项目增加 SSR

Angular CLI 提供了一条捷径,能轻松地将一个普通 Angular 项目转换为支持 SSR 的版本。只需要一条命令:

ng add @nguniversal/express-engine

建议在运行这个命令之前,先把所有改动提交到版本控制里。这个命令会对项目做以下修改:

  • 添加服务端文件:

    • main.server.ts - 服务端主程序文件
    • app/app.server.module.ts - 服务端应用程序主模块
    • tsconfig.server.json - TypeScript 服务端配置文件
    • server.ts - Express web server 的运行文件
  • 修改的文件:

    • package.json - 添加 SSR 所需的依赖和运行脚本
    • angular.json - 添加开发和构建 SSR 应用所需的配置

替换浏览器 API

由于 Universal 应用并不在浏览器环境中执行,因此一些浏览器专属的 API 或功能会失效。最典型的就是服务端无法使用 windowdocumentnavigatorlocation 这些全局对象。

Angular 为此专门提供了两个可注入对象,用于在服务端环境中替代这些浏览器对象:LocationDOCUMENT

举个例子,在浏览器中我们通常通过 window.location.href 来获取当前地址。换成 SSR 之后,代码就变成这样:

import { Location } from '@angular/common';

export class AbmNavbarComponent implements OnInit{
  // ctor 中注入 Location
  constructor(private _location:Location){
    //...
  }

  ngOnInit() {
    // 打印当前地址
    console.log(this._location.path(true));
  }
}

同样的,如果在浏览器中使用 document.getElementById() 获取 DOM 元素,改成 SSR 之后是这样:

import { DOCUMENT } from '@angular/common';

export class AbmFoxComponent implements OnInit{
  // ctor 中注入 DOCUMENT
  constructor(@Inject(DOCUMENT) private _document: Document) { }

  ngOnInit() {
    // 获取 id 为 fox-container 的 DOM
    const container = this._document.getElementById('fox-container');
  }
}

使用 URL 绝对地址

在 Angular SSR 应用中,HTTP 请求的 URL 地址必须是绝对地址(即以 http/https 开头,不能是 /api/heros 这样的相对地址)。官方推荐的做法是把请求的完整 URL 路径设置到 renderModule()renderModuleFactory()options 参数中。不过在 v14 自动生成的代码里,并没有显式调用这两个方法的代码。但通过拦截 HTTP 请求,也能达到同样的效果。

下面先准备一个拦截器,假设这个文件位于项目的 shared/universal-relative.interceptor.ts 路径:

import { HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http';
import { Inject, Injectable, Optional } from '@angular/core';
import { REQUEST } from '@nguniversal/express-engine/tokens';
import { Request } from 'express';

// 忽略大小写检查
const startsWithAny = (arr: string[] = []) => (value = '') => {
    return arr.some(test => value.toLowerCase().startsWith(test.toLowerCase()));
};

// http, https, 相对协议地址
const isAbsoluteURL = startsWithAny(['http', '//']);

@Injectable()
export class UniversalRelativeInterceptor implements HttpInterceptor {
    constructor(@Optional() @Inject(REQUEST) protected request: Request) { }

    intercept(req: HttpRequest, next: HttpHandler) {
        // 不是绝对地址的 URL
        if (!isAbsoluteURL(req.url)) {
            let protocolHost: string;
            if (this.request) {
                // 如果注入的 REQUEST 不为空,则从注入的 SSR REQUEST 中获取协议和地址
                protocolHost = `${this.request.protocol}://${this.request.get('host')}`;
            } else {
                // 如果注入的 REQUEST 为空,比如在进行 prerender build:
                // 这里需要根据实际情况添加自定义的地址前缀
                protocolHost = 'https://www.example.com';
            }
            const pathSeparator = !req.url.startsWith('/') ? '/' : '';
            const url = protocolHost + pathSeparator + req.url;
            const serverRequest = req.clone({ url });
            return next.handle(serverRequest);

        } else {
            return next.handle(req);
        }
    }
}

然后在 app.server.module.ts 文件中把它 provide 出来:

import { UniversalRelativeInterceptor } from './shared/universal-relative.interceptor';
// ... 其他 imports

@NgModule({
  imports: [
    AppModule,
    ServerModule,
    // 如果你用了 @angular/flext-layout,这里也需要引入服务端模块
    FlexLayoutServerModule, 
  ],
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: UniversalRelativeInterceptor,
      multi: true
    }
  ],
  bootstrap: [AppComponent],
})
export class AppServerModule { }

这样一来,任何针对相对地址的请求都会被自动转换为绝对地址请求,在 SSR 场景下就不会再出问题了。

Prerender 预渲染静态 HTML

经过上面的步骤,如果我们通过 npm run build:ssr 构建项目,你会发现 dist//browser 下面只有一个 index.html 文件。打开这个文件,会发现它里面仍然有 这样的元素,页面内容并没有直接在 HTML 中生成。这是因为 Angular 使用的是动态路由,比如 /product/:id 这种形式,页面的渲染结果需要在执行 JS 之后才能确定。因此,Angular 使用 Express 作为 Web 服务器,在运行时根据用户请求(比如爬虫请求),通过模板引擎动态生成静态 HTML 界面。

prerender(通过 npm run prerender)则会在构建时就生成静态 HTML 文件。比如做一个企业官网,页面数量不多,那就很适合用预渲染技术,把这几个页面的静态 HTML 文件提前生成好,避免运行时再做动态生成,从而进一步提升网页的访问速度和用户体验。

预渲染路径配置

需要进行预渲染的网页路径,有几种提供方式:

1. 通过命令行的附加参数:

ng run :prerender --routes /product/1 /product/2

2. 如果路径比较多,比如针对 product/:id 这种动态路径,可以准备一个路径文件:

routes.txt

/products/1
/products/23
/products/145
/products/555

然后在命令行参数中指定该文件:

ng run :prerender --routes-file routes.txt

3. 在项目的 angular.json 文件中直接配置路径:

 "prerender": {
   "builder": "@nguniversal/builders:prerender",
   "options": {
     "routes": [ // 这里配置
       "/",
       "/main/home",
       "/main/service",
       "/main/team",
       "/main/contact"
     ]
   },

配置完成后,重新执行预渲染命令(npm run prerender 或上面提到的带命令行参数的方式)。编译完成后,再打开 dist//browser 下的 index.html,你会发现 消失了,取而代之的是主页的实际内容。同时也会生成相应的路径目录以及各个目录下的 index.html 子页面文件。

SEO 优化

SEO 的关键在于网页的 titlekeywordsdescription。对于希望被搜索引擎收录的页面,我们需要在代码中提供这些内容。

在 Angular 14 中,如果路由界面是通过 Routes 配置的,可以直接将网页的静态 title 写在路由配置里:

{ path: 'home', component: AbmHomeComponent, title: '<你想显示在浏览器 tab 上的标题>' },

另外,Angular 也提供了可注入的 TitleMeta 服务,用于动态修改网页的标题和 meta 信息:

import { Meta, Title } from '@angular/platform-browser';

export class AbmHomeComponent implements OnInit {

  constructor(
    private _title: Title,
    private _meta: Meta,
  ) { }

  ngOnInit() {
    this._title.setTitle('<此页的标题>');
    this._meta.addTags([
      { name: 'keywords', content: '<此页的 keywords,以英文逗号隔开>' },
      { name: 'description', content: '<此页的描述>' }
    ]);
  }
}

总结

Angular 作为一款企业级 SPA 开发框架,在模块化组织和团队协作开发方面,有着自己独特的优势。发展到 v14 这个版本,更是提供了不依赖 NgModule 的独立 Component 功能,进一步简化了模块化的架构。

Angular Universal 的核心目标,是将 Angular 应用进行服务端渲染和生成静态 HTML。对于用户交互复杂的 SPA,其实并不推荐使用 SSR。但对于页面数量较少、又有 SEO 需求的网站或系统,用上 Universal 和 SSR 技术,绝对是一个明智的选择。

来源:https://www.jb51.net/article/266803.htm
上一篇Angular变更检测机制实现原理详解 下一篇Angular FormArray与模态框结合使用实例详解
本站内容用于信息整理与展示,如有侵权或内容问题请及时联系处理。

相关推荐

补充同频道和同主题内容,方便继续浏览更多相关内容。

同类最新

继续查看同栏目最近更新的文章。

更多
HTML双英雄图精准居中与并排对齐实战指南
前端开发 · 2026-07-04

HTML双英雄图精准居中与并排对齐实战指南

本文详解如何使用CSS Flexbox将两个英雄图在页面中水平居中、等高对齐,并保持50px间距,解决justify-content align-items单独作用于子元素无效的问题。 想让两个视觉冲击力十足的英雄图在首页并排居中,是提升首屏吸引力的经典设计。但很多开发者都踩过同一个坑:直接在 `

Flexbox实现div水平垂直居中的方法
前端开发 · 2026-07-04

Flexbox实现div水平垂直居中的方法

使用 Flexbox 实现 div 的水平垂直居中,推荐在父容器上设置 display: flex,并配合 justify-content: center(控制主轴居中)与 align-items: center(控制交叉轴居中),同时确保父容器拥有明确高度,例如 min-height: 100vh

React循环中正确管理多个独立Modal实例的方法
前端开发 · 2026-07-04

React循环中正确管理多个独立Modal实例的方法

在 React 开发中,我们常常会遇到这样的场景:需要在一个列表循环里渲染多个弹窗(Modal)。如果处理不当,点击任何一个按钮,都会导致所有的弹窗同时打开或关闭,这显然不是我们想要的效果。问题的根源在于状态管理:当多个 Modal 实例共享同一份控制其显示隐藏的状态时,它们的行为就被捆绑在了一起。

鼠标滚动切换图片与7秒无操作自动轮播完整教程
前端开发 · 2026-07-04

鼠标滚动切换图片与7秒无操作自动轮播完整教程

本文介绍如何结合鼠标滚轮交互与定时器机制,实现图片在用户滚动时手动切换、7秒无操作后自动轮播的双重功能,并提供可复用、多实例支持的现代化 JavaScript 解决方案。 在网页开发中,图片轮播组件虽然常见,但许多实现方案在用户体验上仍存遗憾。例如,完全依赖用户滚动切换的轮播,当用户停止操作专注查看

输入新城市自动清除旧天气数据实现方法
前端开发 · 2026-07-04

输入新城市自动清除旧天气数据实现方法

本文详解如何借助 JavaScript 在用户切换查询城市时,自动清空先前展示的天气信息,避免新旧数据混杂叠加,从而优化单页应用的交互体验。 在基于 OpenWeather API 打造天气查询工具时,很多开发者都会遇到一个颇为棘手的小问题:用户查完一个城市后,紧接着输入另一个城市名称,页面上新旧天