我已经设法使用.NET Core Prerendering设置了Single Page Application Angular项目,并且可以确认Angular应用程序在服务器端和客户端都可以工作。
我正在使用生产模式下内置的AspNetCoreModule处理程序在IIS 10中运行应用程序。再次,我可以确认这是可行的。
每次重新加载页面时,在SpaPrendering上下文中返回的内容,尤其是相对路径,我都有问题。它总是返回/index.html,而不是浏览器中的相对路径。
我已经在Visual Studio的IIS Express中尝试过此操作,并且相对路径返回正确的结果。因此,它似乎与IIS隔离并且使用AspNetCoreModule处理程序。
public class Program
{
public static void Main(string[] args)
{
CreateWebHostBuilder(args).Build().Run();
}
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
WebHost.CreateDefaultBuilder(args).UseKestrel(options =>
{
options.Listen(IPAddress.Loopback, 5443); //HTTP port
})
.UseStartup<Startup>();
}
public class Startup
{
public Startup(IConfiguration configuration)
{
Configuration = configuration;
}
public IConfiguration Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
// In production, the Angular files will be served from this directory
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/dist/ClientApp";
});
services.AddDbContext<DevResourceDbContext>(options => options.UseSqlServer(Configuration.GetConnectionString("DevResourceDbContext")));
services.AddScoped<ImageService>();
services.AddScoped<EnquiryService>();
services.AddScoped<CategoryService>();
services.AddSingleton<RouteBackgroundService>();
// Mapping
services.AddAutoMapper();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
try
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
//app.UseExceptionHandler("/Error");
//app.UseHsts();
}
//app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseSpaStaticFiles();
app.UseRewriter(new RewriteOptions().AddRedirect("index.html", "/"));
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller}/{action=Index}/{id?}");
});
app.UseSpa(spa =>
{
// To learn more about options for serving an Angular SPA from ASP.NET Core,
// see https://go.microsoft.com/fwlink/?linkid=864501
spa.Options.SourcePath = "ClientApp";
spa.UseSpaPrerendering(options =>
{
// If not working, make sure that npm install and npm install @angular-devkit/build-webpack have been ran.
options.BootModulePath = $"ClientApp/dist-server/ClientApp/main.js";
options.BootModuleBuilder = env.IsDevelopment()
? new AngularCliBuilder(npmScript: "build:ssr")
: null;
options.ExcludeUrls = new[] { "/sockjs-node" };
options.SupplyData = (context, data) =>
{
var routeBackgroundService = context.RequestServices.GetRequiredService<RouteBackgroundService>();
data["routes"] = JsonConvert.SerializeObject(new { Paths = routeBackgroundService.GetRouteData() }, Formatting.Indented);
// context.Request.Path always returns index.html, even if
GetSupplyData(context, new Uri(string.Format("{0}://{1}{2}{3}", context.Request.Scheme, context.Request.Host, context.Request.Path, context.Request.QueryString)), data);
};
});
spa.Options.StartupTimeout = new System.TimeSpan(0, 3, 0);
if (env.IsDevelopment())
{
spa.UseAngularCliServer(npmScript: "start");
}
});
}
catch (Exception ex)
{
throw ex;
}
}
protected void GetSupplyData(HttpContext context, Uri uri, IDictionary<string, object> data)
{
var routeBackgroundService = context.RequestServices.GetRequiredService<RouteBackgroundService>();
var path = uri.AbsolutePath;
// Remove forward slash from path.
if (path.Length > 0 && path.Substring(0, 1) == "/")
{
path = path.Substring(1, path.Length - 1);
}
// Does the path match a category?
var selectedCategory = routeBackgroundService.GetCategory(path);
if (selectedCategory != null)
{
data["selectedCategory"] = Map(context, selectedCategory);
// Filter the image categories
var imageCategories = routeBackgroundService.GetImageCategoryByCategory(selectedCategory.Id);
// Now get the images.
var images = routeBackgroundService.GetImageByCategory(imageCategories).Select(s => Map(context, s)).ToList();
if (images != null)
{
data["images"] = JsonConvert.SerializeObject(new PageList<Image>(images, uri, uri.ToPageNumber() ?? 1, 12, 5), Formatting.Indented);
}
}
// Ensure all querystring are added to the data.
var queryData = System.Web.HttpUtility.ParseQueryString(uri.Query);
foreach (var q in queryData.AllKeys)
{
data["querystring_" + q] = queryData[q];
}
}
protected Image Map(HttpContext context, ImageEntity entity)
{
var mapper = context.RequestServices.GetRequiredService<IMapper>();
return mapper.Map<ImageEntity, Image>(entity);
}
protected Category Map(HttpContext context, CategoryEntity entity)
{
var mapper = context.RequestServices.GetRequiredService<IMapper>();
return mapper.Map<CategoryEntity, Category>(entity);
}
}
在StartUp类内,没有Configure void。在Configure void内,有一个app.UseSpa,您可以在其中配置选项。在app.UseSpa内部,我正在使用spa.UsePreRendering。
从服务器加载页面后,页面将通过spa.UsePrerendering中的options.SupplyData运行。 options.SupplyData内部的参数之一是Context,它是HttpContext类型。
但是,从服务器加载页面时,不管在浏览器中输入的路径如何,context.Request.Path始终返回index.html。因此,如果我要输入http://domain/test,我希望context.Request.Path显示/ test,但它始终返回/index.html。我猜它正在这样做,因为Angular应用程序的默认HTML页面是/index.html。
这仅在使用AspNetCoreModule的IIS 10中发生,而在Visual Studio中不使用IIS Express。
为了使客户端路由正常工作,无论实际路径是什么,ASP.NET均应为所有请求返回index.html。否则,我们会收到404错误。但是对于预渲染的应用程序,我们实际上确实拥有所有需要的index.html文件。
因此请不要使用
services.AddSpaStaticFiles(configuration =>
{
configuration.RootPath = "ClientApp/dist/ClientApp";
});
您可以尝试在公共无效位置手动设置静态文件路由
Configure(IApplicationBuilder app, IHostingEnvironment env): {
...
}
客户端应用程序的路径:
var fileProvider = new PhysicalFileProvider(
Path.Combine(Directory.GetCurrentDirectory(), "ClientApp", "dist", "ClientApp"));
[我们还需要告诉我们的提供者,当我们在没有指定确切文件名的情况下使用斜杠结束index.html
时,应该提供/
。
var defOptions = new DefaultFilesOptions();
defOptions.FileProvider = fileProvider;
app.UseDefaultFiles(defOptions);
现在,我们应该告诉SPA文件应该是服务器,使用我们的提供程序来处理所有请求。 (如果存在现有的控制器或Web API请求,则它们可以正常运行)
app.UseStaticFiles(new StaticFileOptions
{
FileProvider = fileProvider,
RequestPath = new PathString("")
});