Angular Universal SSR预渲染阶段无法加载assets静态资源问题
Angular 14 SSR预渲染读取/assets静态资源失败解决方案
问题背景
- 技术栈:Angular 14 +
@nguniversal/express-engine开发SSR应用 - 异常表现:
- 执行
npm run prerender预渲染命令时,无法读取/assets目录下静态资源用于组件内容构建 - 执行
ssr:dev命令时,资源加载逻辑可正常运行
- 执行
- 已尝试无效方案:
- 服务端判断运行平台后,将资源相对路径转为
http://localhost:4200开头的绝对URL发起HTTP请求,预渲染阶段返回status: 0 Unknown Error - 业务代码中直接引入Node.js原生
fs模块读取本地文件,构建阶段抛出Can't resolve 'fs'模块找不到错误
- 服务端判断运行平台后,将资源相对路径转为
问题根因
- 预渲染属于构建期执行逻辑,运行时不会启动本地开发服务器监听4200端口,直接请求localhost地址必然无法建立连接,返回0状态错误
- 业务代码(
src/app目录下)会同时被浏览器端、服务端bundle打包,直接引入Node原生fs模块会被浏览器构建流程尝试解析,浏览器环境不存在该原生模块,因此构建报错
可行解决方案
方案1:依赖注入分层实现(推荐,适配全场景)
通过Angular依赖注入机制,为浏览器端、服务端(含预渲染环境)分别实现资源加载逻辑,服务端逻辑直接读取本地磁盘文件,不经过HTTP请求,从根源避免端口监听、模块打包问题。
- 定义资源加载服务抽象令牌,避免跨端模块耦合
// src/app/asset-loader.ts import { InjectionToken, Observable } from 'rxjs'; export interface AssetLoader { getAssetText(path: string): Observable<string>; } export const ASSET_LOADER = new InjectionToken<AssetLoader>('ASSET_LOADER'); - 实现浏览器端加载逻辑,走常规HTTP请求
// src/app/asset-loader.browser.ts import { Injectable } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; import { AssetLoader } from './asset-loader'; @Injectable() export class BrowserAssetLoader implements AssetLoader { constructor(private http: HttpClient) {} getAssetText(path: string): Observable<string> { const normalized = path.startsWith('./') ? path.replace('./', '/') : path; return this.http.get(normalized, { responseType: 'text' }); } } - 实现服务端加载逻辑,使用Node原生
fs模块直接读文件,该文件仅在服务端模块引入,不会被浏览器bundle打包// src/app/asset-loader.server.ts import { Injectable } from '@angular/core'; import { from, Observable } from 'rxjs'; import * as fs from 'fs/promises'; import * as path from 'path'; import { AssetLoader } from './asset-loader'; import { SERVER_DIST_ROOT } from '../app.server.module'; @Injectable() export class ServerAssetLoader implements AssetLoader { getAssetText(assetPath: string): Observable<string> { const normalized = assetPath.startsWith('./') ? assetPath.replace('./', '') : assetPath; // 预渲染阶段浏览器端资源已输出到dist/[项目名]/browser目录,直接拼接磁盘路径 const fullPath = path.join(SERVER_DIST_ROOT, 'browser', normalized); return from(fs.readFile(fullPath, 'utf-8')); } } - 在对应模块配置服务提供商
浏览器端根模块app.module.ts添加配置:
服务端模块@NgModule({ // ...其余原有配置 providers: [ { provide: ASSET_LOADER, useClass: BrowserAssetLoader } ] }) export class AppModule {}app.server.module.ts添加配置:import { NgModule } from '@angular/core'; import { ServerModule } from '@angular/platform-server'; import { AppModule } from './app.module'; import { AppComponent } from './app.component'; import * as path from 'path'; import { ASSET_LOADER } from './app/asset-loader'; import { ServerAssetLoader } from './app/asset-loader.server'; // 定义编译输出根目录常量,替换your-project-name为angular.json中定义的项目名 export const SERVER_DIST_ROOT = path.join(process.cwd(), 'dist', 'your-project-name'); @NgModule({ imports: [AppModule, ServerModule], bootstrap: [AppComponent], providers: [ { provide: ASSET_LOADER, useClass: ServerAssetLoader } ] }) export class AppServerModule {} - 改造原有业务代码,通过注入的抽象服务读取资源,无需手动判断平台、拼接URL
// 原有app.service.ts import { Inject, Injectable } from '@angular/core'; import { Observable } from 'rxjs'; import { ASSET_LOADER, AssetLoader } from './asset-loader'; @Injectable({ providedIn: 'root' }) export class AppService { constructor(@Inject(ASSET_LOADER) private assetLoader: AssetLoader) {} getContents(path: string): Observable<string> { return this.assetLoader.getAssetText(path); } }
方案2:临时静态服务(快速修复,改动量小)
如果不想重构服务逻辑,可在预渲染执行前临时启动静态资源服务,将编译后的资源目录挂载到4200端口,适配原有拼接localhost地址的逻辑。
- 安装依赖:
npm i -D concurrently wait-on http-server - 修改
package.json中的脚本配置:
脚本逻辑为先编译全量SSR产物,再启动静态服务挂载browser资源目录,等服务就绪后执行预渲染,流程结束后自动关闭临时服务。{ "scripts": { // ...其余原有脚本 "prerender:static": "http-server dist/your-project-name/browser -p 4200 -s", "prerender": "npm run build:ssr && concurrently -k -s first \"npm run prerender:static\" \"wait-on http://localhost:4200 && ng run your-project-name:prerender\"" } }
注意:该方案需确保执行预渲染命令前已清空旧的dist目录,避免旧版本资源被加载。
内容的提问来源于stack exchange,提问作者Alex Bush
相关产品推荐
相关产品推荐

