You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ABP Angular集成Angular Universal遇问题求助(7.0.1+Angular15)

ABP Angular 集成 Angular Universal 问题与解决方案

问题背景

我尝试为ABP Angular项目添加Angular Universal,执行命令ng add @nguniversal/express-engine后自动生成相关文件及server.ts,build:ssr构建成功,但运行serve:ssr时出现一系列问题:

  • 首次报错localStorage is not defined,我通过修改app.module.ts代码解决:
    export class AuthModule {
      static forRoot(): ModuleWithProviders<AbpOAuthModule> {
        return {
          ngModule: AuthModule,
          providers: [
            { provide: OAuthStorage, useFactory: storageFactory },
            { provide: "windowObject", useValue: window}
          ]
        };
      }
    } 
    export function storageFactory() : OAuthStorage {
      return localStorage
    }
    
    同时更新同文件中的NgModule:
    @NgModule({
     declarations: [AppComponent],
     imports: [
       BrowserModule.withServerTransition({ appId: 'serverApp' }),
       OAuthModule.forRoot(),
    
  • 之后出现新报错:./node_modules/@volo/ngx-lepton-x.core/fesm2020/volo-ngx-lepton-x.core.mjs:1235 return { provide: WINDOW, useValue: windowObj || window };
    尝试在server.ts中添加domino模拟window对象,但无效:
    const domino = require('domino');
    const win = domino.createWindow(indexHtml);
    // mock
    global['window'] = win;
    global['document'] = win.document;
    global['navigator'] = win.navigator;
    
    推测ABP库在server.ts之前调用了localStorage和window对象,随后尝试安装jsdom、global-jsdom,又出现新错误:
    • ./node_modules/jsdom/lib/jsdom/utils.js:139:2-27 - Error: Module not found: Error: Can't resolve 'canvas' in \AngularUniversal\angular\node_modules\jsdom\lib\jsdom
    • 安装canvas后报错:./node_modules/canvas/build/Release/canvas.node:1:2 - Error: Module parse failed: Unexpected character '�' (1:2)

现咨询:ABP Angular是否支持Angular Universal?若支持,正确的集成步骤是什么?

环境信息:

  • ABP Framework版本:7.0.1
  • Angular Framework版本:15.*
  • UI类型:Angular

解决方案

1. ABP Angular 对 Angular Universal 的支持情况

ABP Framework 7.x版本官方部分支持Angular Universal,但需要针对ABP核心依赖(如认证模块、Lepton X UI组件)做特殊适配——这类模块默认依赖浏览器环境的window、localStorage等API,服务端渲染时会直接报错。

2. 正确集成步骤

步骤1:基础安装与初始化

执行官方初始化命令:

ng add @nguniversal/express-engine

步骤2:适配认证模块的存储与Window依赖

修改app.module.ts,将OAuthStorage和WINDOW提供器改为服务端兼容的工厂函数,避免直接依赖浏览器对象:

import { isPlatformBrowser } from '@angular/common';
import { Inject, InjectionToken, ModuleWithProviders, NgModule, PLATFORM_ID } from '@angular/core';
import { OAuthModule, OAuthStorage } from 'angular-oauth2-oidc';

// 定义WINDOW注入令牌
export const WINDOW = new InjectionToken<Window>('WindowToken');

export function windowFactory(@Inject(PLATFORM_ID) platformId: object): Window | undefined {
  return isPlatformBrowser(platformId) ? window : undefined;
}

export function storageFactory(@Inject(PLATFORM_ID) platformId: object): OAuthStorage {
  return isPlatformBrowser(platformId) ? localStorage : {
    getItem: () => null,
    setItem: () => {},
    removeItem: () => {},
    clear: () => {},
    key: () => null,
    length: 0
  } as OAuthStorage;
}

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule.withServerTransition({ appId: 'serverApp' }),
    OAuthModule.forRoot(),
    // 其他模块导入
  ],
  providers: [
    { provide: WINDOW, useFactory: windowFactory, deps: [PLATFORM_ID] },
    { provide: OAuthStorage, useFactory: storageFactory, deps: [PLATFORM_ID] },
    // 其他服务提供者
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

步骤3:适配Lepton X UI的Window依赖

针对@volo/ngx-lepton-x.core模块的WINDOW注入问题,在app.server.module.ts中覆盖其提供器:

import { NgModule } from '@angular/core';
import { ServerModule } from '@angular/platform-server';
import { WINDOW } from './app.module';
import { AppModule } from './app.module';
import { AppComponent } from './app.component';

export function serverWindowFactory() {
  return {
    innerWidth: 1200, // 模拟浏览器窗口宽度
    innerHeight: 800,
    // 按需添加Lepton X需要的其他window属性
  };
}

@NgModule({
  imports: [
    AppModule,
    ServerModule,
  ],
  providers: [
    { provide: WINDOW, useFactory: serverWindowFactory },
  ],
  bootstrap: [AppComponent],
})
export class AppServerModule {}

步骤4:修改server.ts,避免提前加载浏览器依赖

确保在导入任何ABP模块之前,先完成domino的全局对象模拟:

import 'zone.js/node';
import { ngExpressEngine } from '@nguniversal/express-engine';
import * as express from 'express';
import { join } from 'path';
import { AppServerModule } from './src/main.server';
import { APP_BASE_HREF } from '@angular/common';
import { existsSync } from 'fs';

// 提前初始化domino
const domino = require('domino');
const indexHtml = existsSync(join(__dirname, '..', 'browser', 'index.html')) ? 
  join(__dirname, '..', 'browser', 'index.html') : 
  join(__dirname, '..', 'public', 'index.html');
const win = domino.createWindow(indexHtml);
global['window'] = win;
global['document'] = win.document;
global['navigator'] = win.navigator;
global['localStorage'] = {
  getItem: () => null,
  setItem: () => {},
  removeItem: () => {},
  clear: () => {},
};

// 之后再导入其他依赖
const app = express();

// ... 剩余server.ts代码保持不变

步骤5:构建与运行

执行构建命令:

npm run build:ssr

运行服务端渲染:

npm run serve:ssr

3. 注意事项

  • 所有涉及window、document的代码都要通过PLATFORM_ID判断环境,避免在服务端执行依赖浏览器API的逻辑
  • Lepton X UI部分组件可能仍存在服务端兼容问题,若遇到特定组件报错,可考虑在服务端渲染时跳过该组件(使用isPlatformBrowser判断后动态渲染)
  • 不建议使用jsdom替代domino,因为jsdom依赖的canvas等原生模块在Angular Universal的webpack构建中存在兼容性问题

内容的提问来源于stack exchange,提问作者Morshed Alomary

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 20:39:19