让Blazor Server模式支持IE11和老版Edge浏览器

起因

Blazor分(Client,或者叫WebAssembly)和Server两种模式,所以在Blazor Client在IE 11浏览器上运行是不可能的,是因为微软对IE 11的支持已经改变为补丁更新了,已经主推新版Edge(基本Chromium).所以本文主要支持IE 11和老版Edge也是要通过插件进行支持的.

*如果使用Blazor,一定要使用.Net 5,在.Net 5对Blazor进行不少优化,性能提高了不少,如果.Net 6出来了,则优先使用.Net 6.从github看.Net 和 asp.net core最近提交的源码还在对Blazor进行大量的改进.

Blazor支持的浏览器

Blazor Server默认模版新建的项目.在IE 11看看效果

因为Blazor项目模版默认在调试开启IIS Express,在Asp.Net Core中IIS Express调试的时候,个人觉得没什么用,主要是在调试的时候笔记本的风扇呼呼的响,所以基本在建好项目之后,都会手动在Properties/launchSettings.json配置中进行删除.
删除配置在调试的时候启动IIS Express
启动项目,在IE 11中,查看运行效果,样式没什么问题(Blazor中使用UI是bootstrap)
Blazor Server模式在IE 11中运行,样式没问题.
在Counter Component(组件,和WebForm时代的控件差不多,可以看看 学习Asp.Net Core Blazor ),Counter组件功能是没法使用的,按钮单击之后,并没有进行计数(这一块是WebSocket进行通信的)

在IE 11中,要像正常使用,还是需要第三方插件进行兼容支持.

引入BlazorPolyfill.Server组件,兼容IE 11

BlazorPolyfill.Server是以Asp.Net Core中间件进行IE 11的功能支持.该组件代码是开源的,源码地址: https://github.com/Daddoon/Blazor.Polyfill 
在NuGet管理器中,搜索 BlazorPolyfill.Server ,并进行安装,依赖有点多,安装有点慢.

这里主要是在.Net 5使用,和在.Net Core 3.1的时候不同,是否加载blazor.polyfill.min.js在中间件处理
Blazor 安装BlazorPolyfill.Server插件
查看所依赖的库
Blazor 安装BlazorPolyfill.Server依赖的库
在安装BlazorPolyfill.Server之后,在项目代码中注册该中间件和使用该中间件.
// This method gets called by the runtime. Use this method to add services to the container.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
    services.AddRazorPages();
    services.AddServerSideBlazor();
    services.AddSingleton<WeatherForecastService>();

    //注册BlazorPolyfill
    services.AddBlazorPolyfill();
}

// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
    }

    //启用BlazorPolyfill,一定要在静态文件中间件之前启用,启用BlazorPolyfill中间件对blazor.polyfill.min.js和blazor.server.js文件请求,进行了拦截处理
    app.UseBlazorPolyfill();

    //
    //ForceES5Fallback默认为false,会自动判断浏览器是否支持es 5,如果该浏览器不支持的es5,则需要加载blazor.polyfill.min.js文件,如果该浏览器支持es 5则把blazor.polyfill.min.js请求返回返回_fakeBlazorPolyfill={}
    //app.UseBlazorPolyfill((options) =>
    //{
    //    options.ForceES5Fallback = true;
    //});



    app.UseStaticFiles();

    app.UseRouting();

    app.UseEndpoints(endpoints =>
    {
        endpoints.MapBlazorHub();
        endpoints.MapFallbackToPage("/_Host");
    });
}


在_Host.cshtml中

@page "/"
@namespace BlazorServerApp.Pages
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@{
    Layout = null;
}

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>BlazorServerApp</title>
    <base href="~/" />
    <link rel="stylesheet" href="css/bootstrap/bootstrap.min.css" />
    <link href="css/site.css" rel="stylesheet" />
    <link href="BlazorServerApp.styles.css" rel="stylesheet" />
</head>
<body>
    <component type="typeof(App)" render-mode="ServerPrerendered" />

    <div id="blazor-error-ui">
        <environment include="Staging,Production">
            An error has occurred. This application may no longer respond until reloaded.
        </environment>
        <environment include="Development">
            An unhandled exception has occurred. See browser dev tools for details.
        </environment>
        <a href="" class="reload">Reload</a>
        <a class="dismiss">🗙</a>
    </div>
    @*引入支持IE 11blazor.polyfill.min.js脚本文件,该脚本文件是嵌入BlazorPolyfill.Server.dll的资源中  *@    <script src="_framework/blazor.polyfill.min.js"></script>
    <script src="_framework/blazor.server.js"></script>

</body>
</html>
对比在新版Edge和IE 11浏览器中,blazor.polyfill.min.js请求响应大小:
blazor.polyfill.min.js在edge和ie11请求响应大小对比

BlazorPolyfill.Server中间件源码

看看BlazorPolyfill.Server目录:
BlazorPolyfill.Server  代码目录
public static IApplicationBuilder UseBlazorPolyfill(
    this IApplicationBuilder builder, BlazorPolyfillOptions options)
{
    if (builder is null)
    {
        throw new ArgumentNullException(nameof(builder));
    }

    if (options is null)
    {
        throw new ArgumentNullException(nameof(options));
    }

    if (options.ForceES5Fallback)
    {
        HttpRequestExtensions.ForceES5FallbackFlag();
    }

    InitReact(builder);

    //This is a kind of hack for caching the data at boot
    //It's better to prevent anything at first request by caching instead of
    //making the user wait the file generation
    builder.Use((context, next) =>
    {
        if (!IsBlazorPolyfillLibCached())
        {
            //Avoiding first client to await the file generation
            CacheBlazorPolyfillLib();
        }

        //Normal behavior
        return next();
    });

    builder.MapWhen(ctx =>
    ctx.Request.BrowserNeedES5Fallback()
    && ctx.Request.Path.StartsWithSegments("/_framework")
    && ctx.Request.Path.StartsWithSegments("/_framework/blazor.server.js"),
    subBuilder =>
    {
        subBuilder.Run(async (context) =>
        {
            var fileContent = GetPatchedBlazorServerFile();
            await HttpRequestManager.ManageRequest(context, fileContent);
        });
    });

    //拦截blazor.polyfill.js或者blazor.polyfill.min.js请求
    //Should return the resource file if IE11
    builder.MapWhen(ctx =>
    ctx.Request.Path.StartsWithSegments("/_framework")
    && (
        ctx.Request.Path.StartsWithSegments("/_framework/blazor.polyfill.js")
        || ctx.Request.Path.StartsWithSegments("/_framework/blazor.polyfill.min.js")
    ),
    subBuilder =>
    {
        subBuilder.Run(async (context) =>
        {
            //Eval if the requested file is the minified version or not
            bool isMinified = context.Request.Path.StartsWithSegments("/_framework/blazor.polyfill.min.js");
            //判断是否兼容ie11
            var fileContent = GetIE11BlazorPolyfill(context.Request.BrowserNeedES5Fallback(), isMinified);
            //将blazor.polyfill.min.js进行响应
            await HttpRequestManager.ManageRequest(context, fileContent);
        });
    });

    return builder;
}


private static FileContentReference _ie11Polyfill = null;
private static FileContentReference _ie11PolyfillMin = null;

/// <summary>
/// Used to return an empty file but hashed with some dummy valid values in order to generate a usable Hash/ETag for browser caching
/// </summary>
private static FileContentReference _fakeie11Polyfill = null;

private static FileContentReference GetIE11BlazorPolyfill(bool isIE11, bool isMinified)
{
    if (!isIE11)
    {
        if (_fakeie11Polyfill == null)
        {
            string fakeContent = "var _fakeBlazorPolyfill = { };";

            //Computing ETag. Should be computed last !
            string Etag = EtagGenerator.GenerateEtagFromString(fakeContent);

            //Computing Build time for the Last-Modified Http Header
            DateTime buildTime = GetBlazorPolyfillServerBuildDate();

            _fakeie11Polyfill = new FileContentReference()
            {
                Value = fakeContent,
                ETag = Etag,
                LastModified = buildTime,
                ContentLength = System.Text.UTF8Encoding.UTF8.GetByteCount(fakeContent).ToString(CultureInfo.InvariantCulture)
            };
        }

        return _fakeie11Polyfill;
    }
    else
    {
        if (isMinified)
        {
            if (_ie11PolyfillMin == null)
            {
                //获取Blazor.Polyfill.Server程序集
                var assembly = GetBlazorPolyfillAssembly();

                var resources = assembly.GetManifestResourceNames();
                var resourceName = resources.Single(str => str.EndsWith("blazor.polyfill.min.js"));
                //从程序集的资源中获取blazor.polyfill.min.js文件,并读取到string中
                using (Stream stream = assembly.GetManifestResourceStream(resourceName))
                using (StreamReader reader = new StreamReader(stream))
                {
                    string js = reader.ReadToEnd();

                    //Computing ETag. Should be computed last !
                    string Etag = EtagGenerator.GenerateEtagFromString(js);

                    //Computing Build time for the Last-Modified Http Header
                    DateTime buildTime = GetBlazorPolyfillServerBuildDate();

                    _ie11PolyfillMin = new FileContentReference()
                    {
                        Value = js,
                        ETag = Etag,
                        LastModified = buildTime,
                        ContentLength = System.Text.UTF8Encoding.UTF8.GetByteCount(js).ToString(CultureInfo.InvariantCulture)
                    };
                }
            }

            return _ie11PolyfillMin;
        }
        else
        {
            if (_ie11Polyfill == null)
            {
                var assembly = GetBlazorPolyfillAssembly();

                var resources = assembly.GetManifestResourceNames();
                var resourceName = resources.Single(str => str.EndsWith("blazor.polyfill.js"));

                using (Stream stream = assembly.GetManifestResourceStream(resourceName))
                using (StreamReader reader = new StreamReader(stream))
                {
                    string js = reader.ReadToEnd();

                    //Computing ETag. Should be computed last !
                    string Etag = EtagGenerator.GenerateEtagFromString(js);

                    //Computing Build time for the Last-Modified Http Header
                    DateTime buildTime = GetBlazorPolyfillServerBuildDate();

                    _ie11Polyfill = new FileContentReference()
                    {
                        Value = js,
                        ETag = Etag,
                        LastModified = buildTime,
                        ContentLength = System.Text.UTF8Encoding.UTF8.GetByteCount(js).ToString(CultureInfo.InvariantCulture)
                    };
                }
            }

            return _ie11Polyfill;
        }
    }
}
秋风 2021-01-09