Blazor (WebAssembly) + .NETCore achieve Landlords

  Great God before the group made a html5 + .NETCore Landlords, just look at Blazor WebAssembly  will try to rewrite try.

  There is some title of the party, because the article is almost no correlation Landlords realize :), here introduces some of the methods Blazor front-end implementation and realization of Landlords conclusion is to obtain data binding UI, the basic syntax is Razor, injection grammar on the page is not repeated presentation, you can view the complete implementation GitHub: https://github.com/saber-wang/FightLandlord/tree/master/src/BetGame.DDZ.WasmClient , online demo: HTTP: / /111.231.188.181:5000/

  Also emphasize Blazor WebAssembly is a pure front-end framework for all relevant components, etc. will be downloaded to the browser to run, and to MVC, Razor Pages and other distinguished

  Currently based NetCore3.1 and Blazor WebAssembly 3.1.0-preview4.

  Blazor WebAssembly default is not installed, the command line to run the following command to install Blazor WebAssembly template.

dotnet new -i Microsoft.AspNetCore.Blazor.Templates::3.1.0-preview4.19579.2

  

  Select Blazor application, follow down you will see Blazor WebAssembly App template, if you do not see it at between 3.1 and ASP.NET Core3.0 switch it.

  

  After the new project is structured as follows.

  

  A glance, and Razor Pages substantially similar. Program.cs and ASP.NET Core also similar, the difference is returned to a IWebAssemblyHostBuilder.

  

 public static void Main(string[] args)
        {
            CreateHostBuilder(args).Build().Run();
        }

public static IWebAssemblyHostBuilder CreateHostBuilder(string[] args) =>
            BlazorWebAssemblyHost.CreateDefaultBuilder()
                .UseBlazorStartup<Startup>();

  Startup.cs结构也和ASP.NET Core基本一致。Configure中接受的是IComponentsApplicationBuilder,并指定了启动组件

    public class Startup
    {
        public void ConfigureServices(IServiceCollection services)
        {
            //web api 请求
            services.AddScoped<ApiService>();
            //Js Function调用
            services.AddScoped<FunctionHelper>();
            //LocalStorage存储
            services.AddScoped<LocalStorage>();
            //AuthenticationStateProvider实现
            services.AddScoped<CustomAuthStateProvider>();
            services.AddScoped<AuthenticationStateProvider>(s => s.GetRequiredService<CustomAuthStateProvider>());
            //启用认证
            services.AddAuthorizationCore();
        }

        public void Configure(IComponentsApplicationBuilder app)
        {
            WebAssemblyHttpMessageHandlerOptions.DefaultCredentials = FetchCredentialsOption.Include;

            app.AddComponent<App>("app");
        }
    }

 

  Blazor WebAssembly 中也支持DI,注入方式与生命周期与ASP.NET Core一致,但是Scope生命周期不太一样,注册的服务的行为类似于 Singleton 服务。

  默认已注入了HttpClientIJSRuntimeNavigationManager,具体可以看官方文档介绍。

  App.razor中定义了路由和默认路由,修改添加AuthorizeRouteView和CascadingAuthenticationState以AuthorizeView、AuthenticationState级联参数用于认证和当前的身份验证状态。

  

<Router AppAssembly="@typeof(Program).Assembly">
    <Found Context="routeData">
        <AuthorizeRouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
    </Found>
    <NotFound>
        <CascadingAuthenticationState>
            <LayoutView Layout="@typeof(MainLayout)">
                <p>Sorry, there's nothing at this address.</p>
            </LayoutView>
        </CascadingAuthenticationState>
    </NotFound>
</Router>

  自定义AuthenticationStateProvider并注入为AuthorizeView和CascadingAuthenticationState组件提供认证。

            //AuthenticationStateProvider实现
            services.AddScoped<CustomAuthStateProvider>();
            services.AddScoped<AuthenticationStateProvider>(s => s.GetRequiredService<CustomAuthStateProvider>());

 

public class CustomAuthStateProvider : AuthenticationStateProvider
    {
        ApiService _apiService;
        Player _playerCache;
        public CustomAuthStateProvider(ApiService apiService)
        {
            _apiService = apiService;
        }

        public override async Task<AuthenticationState> GetAuthenticationStateAsync()
        {
            var player = _playerCache??= await _apiService.GetPlayer();

            if (player == null)
            {
                return new AuthenticationState(new ClaimsPrincipal());
            }
            else
            {
                //认证通过则提供ClaimsPrincipal
                var user = Utils.GetClaimsIdentity(player);
                return new AuthenticationState(user);
            }

        }
        /// <summary>
        /// 通知AuthorizeView等用户状态更改
        /// </summary>
        public void NotifyAuthenticationState()
        {
            NotifyAuthenticationStateChanged(GetAuthenticationStateAsync());
        }
        /// <summary>
        /// 提供Player并通知AuthorizeView等用户状态更改
        /// </summary>
        public void NotifyAuthenticationState(Player player)
        {
            _playerCache = player;
            NotifyAuthenticationState();
        }
    }

  我们这个时候就可以在组件上添加AuthorizeView根据用户是否有权查看来选择性地显示 UI,该组件公开了一个 AuthenticationState 类型的 context 变量,可以使用该变量来访问有关已登录用户的信息。

  

<AuthorizeView>
    <Authorized>
     //认证通过 @context.User
    </Authorized>
    <NotAuthorized>
     //认证不通过
    </NotAuthorized>
</AuthorizeView>

   使身份验证状态作为级联参数

[CascadingParameter]
private Task<AuthenticationState> authenticationStateTask { get; set; }

  获取当前用户信息

    private async Task GetPlayer()
    {
        var user = await authenticationStateTask;
        if (user?.User?.Identity?.IsAuthenticated == true)
        {
            player = new Player
            {
                Balance = Convert.ToInt32(user.User.FindFirst(nameof(Player.Balance)).Value),
                GameState = user.User.FindFirst(nameof(Player.GameState)).Value,
                Id = user.User.FindFirst(nameof(Player.Id)).Value,
                IsOnline = Convert.ToBoolean(user.User.FindFirst(nameof(Player.IsOnline)).Value),
                Nick = user.User.FindFirst(nameof(Player.Nick)).Value,
                Score = Convert.ToInt32(user.User.FindFirst(nameof(Player.Score)).Value),
            };
            await ConnectWebsocket();
        }
    }

  注册用户并通知AuthorizeView状态更新

    private async Task GetOrAddPlayer(MouseEventArgs e)
    {
        GetOrAddPlayering = true;
        player = await ApiService.GetOrAddPlayer(editNick);
        this.GetOrAddPlayering = false;

        if (player != null)
        {
            CustomAuthStateProvider.NotifyAuthenticationState(player);
            await ConnectWebsocket();
        }

    }

 

  JavaScript 互操作,虽然很希望完全不操作JavaScript,但目前版本的Web WebAssembly不太现实,例如弹窗、WebSocket、本地存储等,Blazor中操作JavaScript主要靠IJSRuntime 抽象。

  从Blazor操作JavaScript比较简单,操作的JavaScript需要是公开的,这里实现从Blazor调用alert和localStorage如下

  

public class FunctionHelper
    {
        private readonly IJSRuntime _jsRuntime;

        public FunctionHelper(IJSRuntime jsRuntime)
        {
            _jsRuntime = jsRuntime;
        }

        public ValueTask Alert(object message)
        {
            //无返回值使用InvokeVoidAsync
            return _jsRuntime.InvokeVoidAsync("alert", message);
        }
    }
public class LocalStorage
    {
        private readonly IJSRuntime _jsRuntime;
        private readonly static JsonSerializerOptions SerializerOptions = new JsonSerializerOptions();

        public LocalStorage(IJSRuntime jsRuntime)
        {
            _jsRuntime = jsRuntime;
        }
        public ValueTask SetAsync(string key, object value)
        {

            if (string.IsNullOrEmpty(key))
            {
                throw new ArgumentException("Cannot be null or empty", nameof(key));
            }

            var json = JsonSerializer.Serialize(value, options: SerializerOptions);

            return _jsRuntime.InvokeVoidAsync("localStorage.setItem", key, json);
        }
        public async ValueTask<T> GetAsync<T>(string key)
        {

            if (string.IsNullOrEmpty(key))
            {
                throw new ArgumentException("Cannot be null or empty", nameof(key));
            }

            //有返回值使用InvokeAsync
            var json =await _jsRuntime.InvokeAsync<string>("localStorage.getItem", key);
            if (json == null)
            {
                return default;
            }

            return JsonSerializer.Deserialize<T>(json, options: SerializerOptions);
        }
        public ValueTask DeleteAsync(string key)
        {
            return _jsRuntime.InvokeVoidAsync(
                $"localStorage.removeItem",key);
        }
    }

  从JavaScript调用C#方法则需要把C#方法使用[JSInvokable]特性标记且必须为公开的。调用C#静态方法看这里,这里主要介绍调用C#的实例方法。

  因为Blazor Wasm暂时不支持ClientWebSocket,所以我们用JavaScript互操作来实现WebSocket的链接与C#方法的回调。

  使用C#实现一个调用JavaScript的WebSocket,并使用DotNetObjectReference.Create包装一个实例传递给JavaScript方法的参数(dotnetHelper),这里直接传递了当前实例。

    [JSInvokable]
    public async Task ConnectWebsocket()
    {
        Console.WriteLine("ConnectWebsocket");
        var serviceurl = await ApiService.ConnectWebsocket();
        //TODO ConnectWebsocket
        if (!string.IsNullOrWhiteSpace(serviceurl))
            await _jsRuntime.InvokeAsync<string>("newWebSocket", serviceurl, DotNetObjectReference.Create(this));
    }

  JavaScript代码里使用参数(dotnetHelper)接收的实例调用C#方法(dotnetHelper.invokeMethodAsync('方法名',方法参数...))。

 

var gsocket = null;
var gsocketTimeId = null;
function newWebSocket(url, dotnetHelper)
{
    console.log('newWebSocket');
    if (gsocket) gsocket.close();
    gsocket = null;
    gsocket = new WebSocket(url);
    gsocket.onopen = function (e) {
        console.log('websocket connect');
        //调用C#的onopen();
        dotnetHelper.invokeMethodAsync('onopen')
    };
    gsocket.onclose = function (e) {
        console.log('websocket disconnect');
        dotnetHelper.invokeMethodAsync('onclose')
        gsocket = null;
        clearTimeout(gsocketTimeId);
        gsocketTimeId = setTimeout(function () {
            console.log('websocket onclose ConnectWebsocket');
            //调用C#的ConnectWebsocket();
            dotnetHelper.invokeMethodAsync('ConnectWebsocket');
            //_self.ConnectWebsocket.call(_self);
        }, 5000);
    };
    gsocket.onmessage = function (e) {
        try {
            console.log('websocket onmessage');
            var msg = JSON.parse(e.data);
            //调用C#的onmessage();
            dotnetHelper.invokeMethodAsync('onmessage', msg);
            //_self.onmessage.call(_self, msg);
        } catch (e) {
            console.log(e);
            return;
        }
    };
    gsocket.onerror = function (e) {
        console.log('websocket error');
        gsocket = null;
        clearTimeout(gsocketTimeId);
        gsocketTimeId = setTimeout(function () {
            console.log('websocket onerror ConnectWebsocket');
            dotnetHelper.invokeMethodAsync('ConnectWebsocket');
            //_self.ConnectWebsocket.call(_self);
        }, 5000);
    };
}

 

  从JavaScript回调的onopen,onclose,onmessage实现

 [JSInvokable]
    public async Task onopen()
    {

        Console.WriteLine("websocket connect");
        wsConnectState = 1;
        await GetDesks();
        StateHasChanged();
    }
    [JSInvokable]
    public void onclose()
    {
        Console.WriteLine("websocket disconnect");
        wsConnectState = 0;
        StateHasChanged();
    }

    [JSInvokable]
    public async Task onmessage(object msgobjer)
    {
        try
        {
            var jsonDocument = JsonSerializer.Deserialize<object>(msgobjer.ToString());
            if (jsonDocument is JsonElement msg)
            {
                if (msg.TryGetProperty("type", out var element) && element.ValueKind == JsonValueKind.String)
                {
                    Console.WriteLine(element.ToString());
                    if (element.GetString() == "Sitdown")
                    {
                        Console.WriteLine(msg.GetProperty("msg").GetString());
                        var deskId = msg.GetProperty("deskId").GetInt32();
                        foreach (var desk in desks)
                        {
                            if (desk.Id.Equals(deskId))
                            {
                                var pos = msg.GetProperty("pos").GetInt32();
                                Console.WriteLine(pos);
                                var player = JsonSerializer.Deserialize<Player>(msg.GetProperty("player").ToString());
                                switch (pos)
                                {
                                    case 1:
                                        desk.player1 = player;
                                        break;
                                    case 2:
                                        desk.player2 = player;
                                        break;
                                    case 3:
                                        desk.player3 = player;
                                        break;

                                }
                                break;
                            }
                        }
                    }
                    else if (element.GetString() == "Standup")
                    {
                        Console.WriteLine(msg.GetProperty("msg").GetString());
                        var deskId = msg.GetProperty("deskId").GetInt32();
                        foreach (var desk in desks)
                        {
                            if (desk.Id.Equals(deskId))
                            {
                                var pos = msg.GetProperty("pos").GetInt32();
                                Console.WriteLine(pos);
                                switch (pos)
                                {
                                    case 1:
                                        desk.player1 = null;
                                        break;
                                    case 2:
                                        desk.player2 = null;
                                        break;
                                    case 3:
                                        desk.player3 = null;
                                        break;

                                }
                                break;
                            }
                        }
                    }
                    else if (element.GetString() == "GameStarted")
                    {
                        Console.WriteLine(msg.GetProperty("msg").GetString());
                        currentChannel.msgs.Insert(0, msg);
                    }
                    else if (element.GetString() == "GameOvered")
                    {
                        Console.WriteLine(msg.GetProperty("msg").GetString());
                        currentChannel.msgs.Insert(0, msg);
                    }
                    else if (element.GetString() == "GamePlay")
                    {

                        ddzid = msg.GetProperty("ddzid").GetString();
                        ddzdata = JsonSerializer.Deserialize<GameInfo>(msg.GetProperty("data").ToString());
                        Console.WriteLine(msg.GetProperty("data").ToString());
                        stage = ddzdata.stage;
                        selectedPokers = new int?[55];
                        if (playTips.Any())
                            playTips.RemoveRange(0, playTips.Count);
                        playTipsIndex = 0;

                        if (this.stage == "游戏结束")
                        {
                            foreach (var ddz in this.ddzdata.players)
                            {
                                if (ddz.id == player.Nick)
                                {
                                    this.player.Score += ddz.score;
                                    break;
                                }
                            }

                        }

                        if (this.ddzdata.operationTimeoutSeconds > 0 && this.ddzdata.operationTimeoutSeconds < 100)
                            await this.operationTimeoutTimer();
                    }
                    else if (element.GetString() == "chanmsg")
                    {
                        currentChannel.msgs.Insert(0, msg);
                        if (currentChannel.msgs.Count > 120)
                            currentChannel.msgs.RemoveRange(100, 20);
                    }

                }
                //Console.WriteLine("StateHasChanged");
                StateHasChanged();
                Console.WriteLine("onmessage_end");
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine($"onmessage_ex_{ex.Message}_{msgobjer}");
        }

    }

  因为是回调函数所以这里我们使用 StateHasChanged()来通知UI更新。

  在html5版中有使用setTimeout来刷新用户的等待操作时间,我们可以通过一个折中方法实现

  

    private CancellationTokenSource timernnnxx { get; set; }

    //js setTimeout
    private async Task setTimeout(Func<Task> action, int time)
    {
        try
        {
            timernnnxx = new CancellationTokenSource();
            await Task.Delay(time, timernnnxx.Token);
            await action?.Invoke();
        }
        catch (Exception ex)
        {
            Console.WriteLine($"setTimeout_{ex.Message}");
        }

    }
    private async Task operationTimeoutTimer()
    {
        Console.WriteLine("operationTimeoutTimer_" + this.ddzdata.operationTimeoutSeconds);
        if (timernnnxx != null)
        {
            timernnnxx.Cancel(false);
            Console.WriteLine("operationTimeoutTimer 取消");
        }
        this.ddzdata.operationTimeoutSeconds--;
        StateHasChanged();
        if (this.ddzdata.operationTimeoutSeconds > 0)
        {
            await setTimeout(this.operationTimeoutTimer, 1000);
        }
    }

  其他组件相关如数据绑定,事件处理,组件参数等等推荐直接看文档也没必要在复制一遍。

  完整实现下来感觉和写MVC似的就是一把梭,各种C#语法,函数往上怼就行了,目前而言还是Web WebAssembly的功能有限,另外就是目前运行时比较大,挂在羊毛机上启动下载都要一会才能下完,感受一下这个加载时间···

Guess you like

Origin www.cnblogs.com/nasha/p/12040520.html