使用单页应用程序(Spa)预呈现无法通过IIS AspNetCoreModule返回正确的URL路径

时间:2019-02-08 18:54:50

标签: angular .net-core asp.net-core-2.0 single-page-application iis-10

我已成功使用.NET Core Prerendering设置了“单页应用程序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。

2 个答案:

答案 0 :(得分:1)

要使客户端路由正常工作,无论实际路径是什么,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("")
    });

答案 1 :(得分:0)

在开发模式下HttpContext.Request.Path属性确实是正确的,但是在生产HttpContext.Request.Path =“ /index.html”中。

但是,您实际上可以在HttpContext.Features.RawTarget私有属性中找到该url。它不是公开可用的,但是通过反思您可以得到它的价值:

var fc = context.Features.GetType();
var rt = fc.GetProperty("RawTarget");
var path = (string)rt.GetValue(context.Features);

我编写了一个程序包,以方便在服务器端渲染期间提供数据: