欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Blazor(WebAssembly) + .NETCore 实现斗地主

程序员文章站 2023-10-28 19:17:40
之前群里大神发了一个 html5+ .NETCore的斗地主,刚好在看Blazor WebAssembly 就尝试重写试试。 还有就是有些标题党了,因为文章里几乎没有斗地主的相关实现:),这里主要介绍一些Blazor前端的一些方法实现而斗地主的实现总结来说就是获取数据绑定UI,语法上基本就是Razo ......

  之前群里大神发了一个 html5+ .netcore的斗地主,刚好在看blazor webassembly 就尝试重写试试。

  还有就是有些标题党了,因为文章里几乎没有斗地主的相关实现:),这里主要介绍一些blazor前端的一些方法实现而斗地主的实现总结来说就是获取数据绑定ui,语法上基本就是razor,页面上的注入语法等不在重复介绍,完整实现可以查看github:https://github.com/saber-wang/fightlandlord/tree/master/src/betgame.ddz.wasmclient,在线演示:

  另外强调一下blazor webassembly 是纯前端框架,所有相关组件等都会下载到浏览器运行,要和mvc、razor pages等区分开来

  当前是基于netcore3.1和blazor webassembly 3.1.0-preview4。

  blazor webassembly默认是没有安装的,在命令行执行下边的命令安装blazor webassembly模板。

dotnet new -i microsoft.aspnetcore.blazor.templates::3.1.0-preview4.19579.2

  Blazor(WebAssembly) + .NETCore 实现斗地主

  选择blazor应用,跟着往下就会看到blazor webassembly app模板,如果看不到就在asp.net core3.0和3.1之间切换一下。

  Blazor(WebAssembly) + .NETCore 实现斗地主

  新建后项目结构如下。

  Blazor(WebAssembly) + .NETCore 实现斗地主

  一眼看过去,大体和razor pages 差不多。program.cs也和asp.net core差不多,区别是返回了一个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的功能有限,另外就是目前运行时比较大,挂在羊毛机上启动下载都要一会才能下完,感受一下这个加载时间···

Blazor(WebAssembly) + .NETCore 实现斗地主