Blazor(WebAssembly) + .NETCore 实现斗地主
之前群里大神发了一个 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的功能有限,另外就是目前运行时比较大,挂在羊毛机上启动下载都要一会才能下完,感受一下这个加载时间···
上一篇: 秦始皇修建长城召集了几百万的劳动力 所花费的钱换做现在是多少
下一篇: 大葱根的作用,一根大葱三味药