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

如何让Angular 19 SSR与.NET 6协同工作?后端文件服务异常排查

Angular CSR转.NET 6 SSR:静态文件服务异常问题

我正把Angular CSR应用改成基于.NET 6的SSR应用,现在遇到.NET后端无法正常提供前端静态文件的问题。所有文件已构建完成并放到了正确目录,以下是相关配置片段:

angular.json 配置片段

"server": {
      "builder": "@angular-devkit/build-angular:server",
      "options": {
        "outputPath": "dist/my-app/server",
        "main": "src/main.server.ts",
        "tsConfig": "tsconfig.server.json"
      },
      "configurations": {
       ...
      }
    },

        "serve-ssr": {
      "builder": "@angular-devkit/build-angular:ssr-dev-server",
      "options": {
        "host": "localhost",
        "port": 44492,
        "ssl": true,
        "sslCert": "%APPDATA%\\ASP.NET\\https\\%npm_package_name%.pem",
        "sslKey": "%APPDATA%\\ASP.NET\\https\\%npm_package_name%.key",
        "proxyConfig": "proxy.conf.js"
      },
      "configurations": {
        "development": {
          "browserTarget": "my-app:build:development",
          "serverTarget": "my-app:server:development"
        },
        "production": {
          "browserTarget": "my-app:build:production",
          "serverTarget": "my-app:server:production"
        }
      },
      "defaultConfiguration": "development"
    },

tsconfig.server.json 配置

{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "./dist/my-app/server",
    "target": "es2022",
    "module": "es2022",
    "moduleResolution": "node",
    "types": ["node"],
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true
  },
  "files": [
    "src/main.server.ts"
  ],
  "angularCompilerOptions": {
    "enableIvy": true
  }
}

main.server.ts 代码

import { bootstrapApplication } from '@angular/platform-browser';
import { AppComponent } from './app/app.component';
import { serverAppConfig } from './app/app.config.server';

const bootstrap = () => bootstrapApplication(AppComponent, serverAppConfig);

export default bootstrap;

server.ts 代码

import 'zone.js/node';

import { APP_BASE_HREF } from '@angular/common';
import { CommonEngine } from '@angular/ssr/node';
import * as express from 'express';
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import AppServerModule from './src/main.server';

// Define paths
const distFolder = join(process.cwd(), 'dist', 'my-app', 'browser');
const serverFolder = join(process.cwd(), 'dist', 'my-app', 'server');
const indexHtml = existsSync(join(distFolder, 'index.original.html'))
  ? join(distFolder, 'index.original.html')
  : join(distFolder, 'index.html');

// Initialize CommonEngine for SSR
const commonEngine = new CommonEngine();

export function app(): express.Express {
  const server = express();

  server.set('view engine', 'html');
  server.set('views', distFolder);

  // 📚 Static Files Serving
  server.get('*.*', express.static(distFolder, {
    maxAge: '1y'
  }));

  // 📚 Angular SSR Rendering
  server.get('*', (req, res, next) => {
    const { protocol, originalUrl, baseUrl, headers } = req;

    commonEngine
      .render({
        bootstrap: AppServerModule,
        documentFilePath: indexHtml,
        url: `${protocol}://${headers.host}${originalUrl}`,
        publicPath: distFolder,
        providers: [{ provide: APP_BASE_HREF, useValue: baseUrl }],
      })
      .then((html) => res.send(html))
      .catch((err) => {
        console.error('SSR Error:', err);
        next(err);
      });
  });

  return server;
}

// 📚 Start the Server
function run(): void {
  const port = process.env['PORT'] || 44492;

  const server = app();
  server.listen(port, () => {
    console.log(`✅ Node Express server listening on http://localhost:${port}`);
  });
}

// 📚 Ensure Server Only Runs in Standalone Mode
declare const __non_webpack_require__: NodeRequire;
const mainModule = __non_webpack_require__.main;
const moduleFilename = mainModule && mainModule.filename || '';
console.log('✅ Module Filename:', moduleFilename);
console.log('✅ Current Filename:', __filename);
if (moduleFilename === __filename || moduleFilename.includes('iisnode')) {
  console.log('✅ Running the SSR server...');
  run();
}

export default AppServerModule;

Program.cs 配置(原CSR模式下正常运行)

app.UseStaticFiles(new StaticFileOptions
{
    OnPrepareResponse = ctx =>
    {
        ctx.Context.Response.Headers.Append("Cache-Control", "public,max-age=31536000");
    }
});

app.UseRouting();
app.UseAuthorization();

app.MapControllerRoute(
    name: "default",
    pattern: "{controller}/{action=Index}/{id?}");

app.MapFallbackToFile("index.html");

.csproj 配置

<PropertyGroup>
    <TargetFramework>net6.0</TargetFramework>
    <Nullable>enable</Nullable>
    <IsPackable>false</IsPackable>
    <SpaRoot>ClientApp\</SpaRoot>
    <SpaProxyServerUrl>https://localhost:44492</SpaProxyServerUrl>
    <SpaProxyLaunchCommand>npm run dev:ssr</SpaProxyLaunchCommand>
    <ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<!-- Copy Browser and Server Builds to wwwroot -->
<Target Name="CopySpaFiles" AfterTargets="PublishRunWebpack">
    <ItemGroup>
      <DistBrowserFiles Include="$(SpaRoot)dist\my-app\browser\**" />
      <DistServerFiles Include="$(SpaRoot)dist\my-app\server\**" />
    </ItemGroup>

    <Message Importance="high" Text="Copying Angular SSR files to wwwroot..." />
    <Copy SourceFiles="@(DistBrowserFiles)" DestinationFolder="wwwroot\%(RecursiveDir)" />
    <Copy SourceFiles="@(DistServerFiles)" DestinationFolder="wwwroot\%(RecursiveDir)" />
</Target>

问题排查与解决

1. 静态文件路由逻辑错误

CSR模式下MapFallbackToFile("index.html")直接返回静态文件,但SSR场景需要让非静态文件请求转发给Angular SSR引擎处理。修改Program.cs:

using Microsoft.Extensions.FileProviders;
using System.IO;

// 保留静态文件服务
app.UseStaticFiles(new StaticFileOptions
{
    OnPrepareResponse = ctx =>
    {
        ctx.Context.Response.Headers.Append("Cache-Control", "public,max-age=31536000");
    },
    // 明确指向wwwroot目录
    FileProvider = new PhysicalFileProvider(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot"))
});

app.UseRouting();
app.UseAuthorization();

// 保留原有API路由
app.MapControllerRoute(
    name: "default",
    pattern: "{controller}/{action=Index}/{id?}");

// 修改fallback逻辑:静态文件直接返回,其他请求交给SSR处理
app.MapFallback(async (context) =>
{
    // 这里可以整合Angular SSR的Node服务,或者使用SpaServices调用渲染逻辑
    // 开发环境下SpaProxy会自动转发请求到Angular SSR服务
    await context.Response.SendFileAsync(Path.Combine(Directory.GetCurrentDirectory(), "wwwroot", "index.html"));
});

2. .csproj 文件复制路径冲突

当前配置把server和browser目录文件都复制到wwwroot根目录,会导致文件覆盖。修改复制逻辑,分开存放:

<Target Name="CopySpaFiles" AfterTargets="PublishRunWebpack">
    <ItemGroup>
      <DistBrowserFiles Include="$(SpaRoot)dist\my-app\browser\**" />
      <DistServerFiles Include="$(SpaRoot)dist\my-app\server\**" />
    </ItemGroup>

    <Message Importance="high" Text="Copying Angular SSR files to wwwroot..." />
    <!-- 前端静态文件放到wwwroot根目录 -->
    <Copy SourceFiles="@(DistBrowserFiles)" DestinationFolder="wwwroot\%(RecursiveDir)" />
    <!-- SSR服务端文件放到wwwroot/server子目录 -->
    <Copy SourceFiles="@(DistServerFiles)" DestinationFolder="wwwroot\server\%(RecursiveDir)" />
</Target>

3. server.ts 路径适配.NET环境

.NET部署环境中process.cwd()指向发布目录,需调整路径指向wwwroot:

// 修改路径定义
const distFolder = join(process.cwd(), 'wwwroot');
const serverFolder = join(process.cwd(), 'wwwroot', 'server');
const indexHtml = existsSync(join(distFolder, 'index.original.html'))
  ? join(distFolder, 'index.original.html')
  : join(distFolder, 'index.html');

4. 开发环境代理配置

确保proxy.conf.js正确转发API请求到.NET后端:

module.exports = {
    "/api": {
        "target": "https://localhost:5001", // 替换为你的.NET API端口
        "secure": false,
        "changeOrigin": true
    }
};

内容的提问来源于stack exchange,提问作者Josh Hick

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.15 03:28:11