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

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请求,从根源避免端口监听、模块打包问题。

  1. 定义资源加载服务抽象令牌,避免跨端模块耦合
    // 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');
    
  2. 实现浏览器端加载逻辑,走常规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' });
      }
    }
    
  3. 实现服务端加载逻辑,使用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'));
      }
    }
    
  4. 在对应模块配置服务提供商
    浏览器端根模块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 {}
    
  5. 改造原有业务代码,通过注入的抽象服务读取资源,无需手动判断平台、拼接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地址的逻辑。

  1. 安装依赖:npm i -D concurrently wait-on http-server
  2. 修改package.json中的脚本配置:
    {
      "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\""
      }
    }
    
    脚本逻辑为先编译全量SSR产物,再启动静态服务挂载browser资源目录,等服务就绪后执行预渲染,流程结束后自动关闭临时服务。

注意:该方案需确保执行预渲染命令前已清空旧的dist目录,避免旧版本资源被加载。


内容的提问来源于stack exchange,提问作者Alex Bush

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 16:03:34