如何在带预渲染的Angular模板中使用Razor语法?
首先得明确:直接把Razor页面路径设为Angular组件的templateUrl在预渲染场景下确实不可行,你遇到的Cannot find module错误本质是Webpack打包逻辑和ASP.NET Razor渲染逻辑的冲突——我来拆解原因和给出可行方案:
为什么预渲染时会报错?
服务器端预渲染(SSR)是靠Node.js环境运行Angular的服务器端bundle,而Webpack在打包这个bundle时,会把templateUrl的路径当作Node.js模块去查找解析。但Razor页面是ASP.NET Core在.NET环境下处理的动态资源,不在Webpack的打包范围内,自然会触发模块找不到的错误。
可行的解决思路
方案1:通过API获取Razor渲染后的HTML,动态渲染组件
这种方式最直观,把Razor页面的内容通过API暴露出来,Angular组件不管是客户端还是预渲染阶段,都通过HTTP请求获取HTML内容并渲染:
创建Razor内容API控制器
在ASP.NET Core项目中添加一个控制器,负责渲染Razor页面并返回HTML:using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Razor; using System.IO; using System.Threading.Tasks; [Route("api/razor-content")] public class RazorContentController : Controller { private readonly IRazorViewEngine _viewEngine; private readonly ITempDataProvider _tempDataProvider; public RazorContentController(IRazorViewEngine viewEngine, ITempDataProvider tempDataProvider) { _viewEngine = viewEngine; _tempDataProvider = tempDataProvider; } [HttpGet("test")] public async Task<IActionResult> GetTestPage() { var actionContext = new ActionContext(HttpContext, RouteData, new ActionDescriptor()); var viewResult = _viewEngine.FindView(actionContext, "/Pages/Test", isMainPage: true); if (!viewResult.Success) { return NotFound("Razor页面未找到"); } using var sw = new StringWriter(); var viewContext = new ViewContext( actionContext, viewResult.View, ViewData, new TempDataDictionary(HttpContext, _tempDataProvider), sw, new HtmlHelperOptions() ); await viewResult.View.RenderAsync(viewContext); return Content(sw.ToString(), "text/html"); } }修改Angular组件动态获取并渲染HTML
调整fetchdata.component.ts,去掉templateUrl,改用动态获取HTML并渲染:import { Component, OnInit, Inject, PLATFORM_ID } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; import { isPlatformServer } from '@angular/common'; import { TransferState, makeStateKey } from '@angular/platform-browser'; const TEST_PAGE_KEY = makeStateKey<string>('testPageHtml'); @Component({ selector: 'app-fetch-data', template: '<div [innerHTML]="safeRazorHtml"></div>' }) export class FetchDataComponent implements OnInit { safeRazorHtml: SafeHtml; constructor( private http: HttpClient, private sanitizer: DomSanitizer, private transferState: TransferState, @Inject(PLATFORM_ID) private platformId: Object ) { } ngOnInit() { // 优先从TransferState取预渲染时的数据,避免客户端重复请求 const cachedHtml = this.transferState.get(TEST_PAGE_KEY, ''); if (cachedHtml) { this.safeRazorHtml = this.sanitizer.bypassSecurityTrustHtml(cachedHtml); return; } // 预渲染时需要用完整API地址,Node环境无法识别相对路径 const apiUrl = isPlatformServer(this.platformId) ? 'http://localhost:64066/api/razor-content/test' : '/api/razor-content/test'; this.http.get(apiUrl, { responseType: 'text' }).subscribe(html => { this.safeRazorHtml = this.sanitizer.bypassSecurityTrustHtml(html); // 把数据存入TransferState,客户端复用 if (isPlatformServer(this.platformId)) { this.transferState.set(TEST_PAGE_KEY, html); } }); } }
方案2:预渲染阶段直接把Razor内容传递给Angular
这种方式更高效,在ASP.NET的预渲染中间件中提前渲染Razor页面,把内容通过SupplyData传递给Angular的服务器端代码:
修改Startup.cs配置预渲染数据
using Microsoft.AspNetCore.SpaServices.Prerendering; // 其他必要using... public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { // ...其他中间件配置 app.UseSpa(spa => { spa.Options.SourcePath = "ClientApp"; if (env.IsDevelopment()) { spa.UseAngularCliServer(npmScript: "start"); } else { spa.UseSpaPrerendering(options => { options.BootModulePath = $"{spa.Options.SourcePath}/dist/main-server.js"; options.BootModuleBuilder = env.IsDevelopment() ? new AngularCliBuilder(npmScript: "build:ssr") : null; options.ExcludeUrls = new[] { "/sockjs-node" }; // 预渲染时传递Razor页面内容 options.SupplyData = async (context, data) => { var viewEngine = context.RequestServices.GetRequiredService<IRazorViewEngine>(); var tempDataProvider = context.RequestServices.GetRequiredService<ITempDataProvider>(); var actionContext = new ActionContext(context, context.GetRouteData(), new ActionDescriptor()); var viewResult = viewEngine.FindView(actionContext, "/Pages/Test", isMainPage: true); if (viewResult.Success) { using var sw = new StringWriter(); var viewContext = new ViewContext( actionContext, viewResult.View, new ViewDataDictionary(new EmptyModelMetadataProvider(), new ModelStateDictionary()), new TempDataDictionary(context, tempDataProvider), sw, new HtmlHelperOptions() ); await viewResult.View.RenderAsync(viewContext); data["testRazorHtml"] = sw.ToString(); } }; }); } }); }在Angular服务器端模块接收数据并传递给组件
找到main-server.ts,修改createServerRenderer的逻辑,把传递过来的Razor内容注入到组件中(或通过服务传递),示例简化版:import { createServerRenderer, RenderResult } from 'aspnet-prerendering'; import { AppServerModule } from './app/app.server.module'; import { NgModuleRef, NgZone } from '@angular/core'; import { TransferState } from '@angular/platform-browser'; export default createServerRenderer(params => { const { AppServerModuleNgFactory, LAZY_MODULE_MAP } = require('./dist/server/main'); const transferState = params.data.root.injector.get(TransferState); // 把预渲染时的Razor内容存入TransferState transferState.set(makeStateKey<string>('testRazorHtml'), params.data.testRazorHtml); return new Promise<RenderResult>((resolve, reject) => { const zone = NgZone.current; zone.run(() => { // ...原有渲染逻辑 }); }); });之后组件就可以像方案1那样,从
TransferState中直接获取Razor内容渲染了。
关键注意事项
- 预渲染阶段是Node.js环境,HTTP请求需要使用完整的服务器地址,不能用相对路径;
- 使用
TransferState避免客户端重复请求相同内容,提升性能; - 如果Razor页面需要用户上下文(比如登录状态),要确保预渲染时的
HttpContext包含正确的请求信息。
内容的提问来源于stack exchange,提问作者pbristow

