之前群里大神发了一个 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应用,跟着往下就会看到blazor webassembly app模板,如果看不到就在asp.net core3.0和3.1之间切换一下。

  

  新建后项目结构如下。

  

  一眼看过去,大体和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 服务。

  默认已注入了httpclient,ijsruntime,navigationmanager,具体可以看介绍。

  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的功能有限,另外就是目前运行时比较大,挂在羊毛机上启动下载都要一会才能下完,感受一下这个加载时间···