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代码解决:
同时更新同文件中的NgModule: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({ 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对象,但无效:
推测ABP库在const domino = require('domino'); const win = domino.createWindow(indexHtml); // mock global['window'] = win; global['document'] = win.document; global['navigator'] = win.navigator;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
相关产品推荐
相关产品推荐

