Blazor WebAssembly开发初体验

时间:12/06/2022 22:45:43   作者:ChenReal    阅读:303

近日,终于有机会将大名鼎鼎的WebAssembly开发框架Blazor体验了一把。Blazor是一个套基于ASP.NET的WebAssembly前端开发框架,仅用C#语言无需javascript便可以开发一套完整的Web前端应用,听起来都觉得相当之Cool。个人觉得,Blazor WebAssembly并不算什么非常新鲜的的技术。早在2-3年前,网上就有不少大V在不遗余力地“安利”它。但是,开发者对于Blazor或者WASM应用的热情似乎一直不温不火。究竟Blazor有什么硬伤呢?带着这个好奇心,我终于创建了一个Blazor应用一探究竟。

创建项目

添加一个Blazor WebAssembly项目,也就是那种纯前端的项目模板 blazor-0.png

目录结构说明

blazor-1.jpg

应用程序入口(program.cs)

这东东对于所有.net开发者都不陌生了吧。我们通常在此为依赖注入容器做注册。例如:

var builder = WebAssemblyHostBuilder.CreateDefault(args);
builder.RootComponents.Add<App>("#app"); // 注册前端根组件节点,类似Vue的app.mount('#app');
builder.RootComponents.Add<HeadOutlet>("head::after");

builder.Services.AddAntDesign(); // 注册 AntDesign Blazor组件库
builder.Services.AddHttpClient(); // 注册 HttpClientFactory
builder.Services.AddSingleton<LeanCloudService>(); // 注册自定义的类

Web静态资源

静态资源包括首页(index.html)、css、图片、字体、json数据等,这些内容与静态网页几乎一样。这里便不再展开赘述了。

  • 我的应用使用了AntDesign组件库,因此在首页(index.html)增加了相关的样式以及js脚本的引用。
    <link href="_content/AntDesign/css/ant-design-blazor.css" rel="stylesheet" />
    <script src="_content/AntDesign/js/ant-design-blazor.js"></script>
    

全局引用

_Imports.razor中设置全局引用,那么其他页面就可省事很多了。比如我这里的AntDesign。

@using System.Net.Http
@using System.Net.Http.Json
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.Web
@using Microsoft.AspNetCore.Components.Web.Virtualization
@using Microsoft.AspNetCore.Components.WebAssembly.Http
@using Microsoft.JSInterop
@using RM.Blazor.WASM
@using RM.Blazor.WASM.Shared
@using AntDesign

公共布局

根Blazor项目默认的布局不大一样:

  • 使用了AntDesign的布局控件
  • 使用了动态的菜单导航,内容从./data/menu.json配置文件获取。内容如下:
    [
    {
      "key": "home",
      "title": "首页",
      "routerUrl": "",
      "summary": ""
    },
    {
      "key": "vote",
      "title": "投票记录",
      "routerUrl": "votelogs",
      "summary": "HeiYan投票记录"
    },
    {
      "key": "account",
      "title": "用户账号",
      "routerUrl": "useraccount",
      "summary": "LeanCloud数据表TAccount管理"
    }
    ]
    
  • 使用PageHeader作为内页标题

MainLayout.razor 代码如下:

@inherits LayoutComponentBase
@inject HttpClient Http

<Layout Class="layout">
    <Header>
        <div class="logo" />
        <Menu Theme="MenuTheme.Dark" @ref="mainMenu" Mode="MenuMode.Horizontal" DefaultSelectedKeys=@(new[]{"home"}) OnMenuItemClicked=@(OnMenuClick)>
            @foreach (var menu in menus)
            {
                <MenuItem Key="@menu.Key" RouterLink="@menu.RouterUrl">@menu.Title</MenuItem>
            }
        </Menu>
    </Header>
    <Content Style="padding: 0 16px;">
        <PageHeader Class="site-page-header"  Title="@_title" Subtitle="@_summary" />
        <div class="site-layout-content">
            @Body
        </div>
    </Content>
    <Footer Style="text-align: center; ">Ant Design ©2018 Created by Ant UED</Footer>
</Layout>

@code {
    private MenuOption[]? menus = new MenuOption[] { };
    private string _title = "首页";
    private string _summary = "";
    protected override async Task OnInitializedAsync()
    {
        await base.OnInitializedAsync();
        menus = await Http.GetFromJsonAsync<MenuOption[]>("data/menu.json");
    }
    public Task OnMenuClick(MenuItem e)
    {
        var opt = menus.Where(x => x.Key == e.Key).FirstOrDefault();
        _title = e.Key == "home" ? opt.Title : "首页 / " + opt.Title;
        _summary = opt.Summary;
        return Task.CompletedTask;
    }

    Menu mainMenu;

    public class MenuOption
    {
        public string? Key { get; set; }

        public string? Title { get; set; }

        public string? RouterUrl { get; set; }

        public string? Summary { get; set; }
    }
}

CRUD示例页面

这个是个标准的CRUD用户账号管理的界面。看着跟Vue的开发体验也差不多。需要留意的一个细节就是如果要做显示控件与变量双向绑定,必须要加上@bind-才行,否者就是单向的。详细代码如下:

@page "/useraccount"
@using RM.Blazor.WASM.Services
@using RM.Blazor.WASM.Models
@using System.Text.Json;
@inject LeanCloudService Lean

<Button @onclick="OpenAdd">Add User Account</Button>
<br />
<Table @ref="table"
       TItem="LCAccount"
       DataSource="@listData"
       Size="TableSize.Small"
       Total="_total"
       @bind-PageIndex="_pageIndex"
       @bind-PageSize="_pageSize">
  <PropertyColumn Property="c=>c.phone" />
  <PropertyColumn Property="c=>c.points" />
  <PropertyColumn Title="tickets" Property="c=>c.remark" />
    <PropertyColumn Property="c=>c.status" />
    <ActionColumn>
        <Space>
            <SpaceItem><Button OnClick="()=>Edit(context.id)">Edit</Button></SpaceItem>
            <SpaceItem>
                <Popconfirm Title="Are you sure delete this task?"
                        OnConfirm="()=>Delete(context.id)"
                        OkText="Yes"
                        CancelText="No">
                    <Button Danger>Delete</Button>
                </Popconfirm></SpaceItem>
    </Space>
  </ActionColumn>
</Table>

<Drawer Closable="true" Placement="bottom" Height="600" Visible="showEdit" Title="@dialogTitle" OnClose="@(CloseEdit)">
    <Template style="height:90%">
        <Row Gutter="16">
            <AntDesign.Col Span="12">
                <Text>Name: @editItem.name</Text>
            </AntDesign.Col>
            <AntDesign.Col Span="12">
                <Text>Phone: @editItem.phone</Text>
            </AntDesign.Col>
        </Row>
        <br />
        <Row Gutter="16">
            <AntDesign.Col Span="12">
                <Text>Points: @editItem.points</Text>
            </AntDesign.Col>
            <AntDesign.Col Span="12">
                <Text>Tickets: @editItem.remark</Text>
            </AntDesign.Col>
        </Row>
        <br />
        <Row Gutter="16">
            <AntDesign.Col Span="24">
                <Text>Cookies</Text>
                <TextArea Rows="10" Placeholder="Please enter cookie" @bind-Value="@editItem.cookies"></TextArea>
            </AntDesign.Col>
        </Row>
        <br />
        <Row>
            <AntDesign.Col Span="12">
                <Text>Status</Text>
                <Switch @bind-Checked="@editStatus" />
            </AntDesign.Col>
            <AntDesign.Col Span="12">
                <Button Type="primary" OnClick="EditSave">Submit</Button>
            </AntDesign.Col>
        </Row>
    </Template>
</Drawer>
<Drawer Closable="true" Placement="bottom" Height="600" Visible="showAdd" Title="@dialogTitle" OnClose="@(CloseAdd)">
    <Template style="height:90%">
        <Row Gutter="16">
            <AntDesign.Col Span="12">
                <Text>Name: </Text>
                <Input Placeholder="input account name" @bind-Value="@editItem.name" />
            </AntDesign.Col>
            <AntDesign.Col Span="12">
                <Text>Phone: </Text>
                <Input Placeholder="input phone number" @bind-Value="@editItem.phone" />
            </AntDesign.Col>
        </Row>
        <br />
        <Row Gutter="16">
            <AntDesign.Col Span="12">
                <Text>Password:</Text>
                <Input Placeholder="input password" @bind-Value="@editItem.pwd" />
            </AntDesign.Col>
            <AntDesign.Col Span="12">
                <Text>User Id</Text>
                <Input Placeholder="input user id" @bind-Value="@editItem.userId" />
            </AntDesign.Col>
        </Row>
        <br />
        <Row Gutter="16">
            <AntDesign.Col Span="24">
                <Text>Cookies</Text>
                <TextArea Rows="10" Placeholder="Please enter cookie" @bind-Value="@editItem.cookies"></TextArea>
            </AntDesign.Col>
        </Row>
        <br />
        <Row>
            <AntDesign.Col Span="12">
                <Text>Status</Text>
                <Switch @bind-Checked="@editStatus" />
            </AntDesign.Col>
            <AntDesign.Col Span="12">
                <Button Type="primary" OnClick="AddSave">Submit</Button>
            </AntDesign.Col>
        </Row>
    </Template>
</Drawer>

@code {
    ITable table;
    int _pageIndex = 1;
    int _pageSize = 10;
    int _total = 0;
    private List<LCAccount> listData;
    LCAccount editItem = new LCAccount();
    bool showEdit = false;
    bool showAdd = false;
    bool editStatus = false;
    string dialogTitle = "Edit User Account";
    string tableName = LCAccount.TABLE;

    protected override async Task OnInitializedAsync()
    {
        await LoadData();
    }

    private async Task LoadData(){

        listData = await Lean.FindAll<LCAccount>(tableName) ;
        _total = listData.Count;
    }

    private void Edit(int id)
    {
        var item = listData.Where(x => x.id == id).FirstOrDefault();
        if(item != null){
            editItem = item.Clone();
            editStatus = (editItem.status == 1);
            showEdit = true;
            dialogTitle = "Edit User Account";
        }
    }
    private async Task EditSave()
    {        
        if (editItem.id>0)
        {
            var data = new { cookies = editItem.cookies, status = editStatus ? 1 : 0 };
            var res = await Lean.Update(tableName, JsonSerializer.Serialize(data), editItem.objectId);
            if (res)
            {
                var item = listData.Where(x => x.id == editItem.id).FirstOrDefault();
                item.cookies = editItem.cookies;
                item.status = editStatus ? 1 : 0;
            }
            showEdit = false;
        }
    }
    private async Task Delete(int id)
    {
        var item = listData.Where(x => x.id == editItem.id).FirstOrDefault();
        if (item != null)
        {
            var res = await Lean.Delete(tableName, new string[] { item.objectId });
            listData = listData.Where(x => x.id != id).ToList();
            _total = listData.Count;
        }
    }
    private void CloseEdit(){
        showEdit = false;
    }
    private void OpenAdd(){
        dialogTitle = "Add User Account";
        editItem = new LCAccount();
        editStatus = true;
        showAdd = true;
    }
    private void CloseAdd(){
        showAdd = false;
    }
    private async Task AddSave()
    {        
        var data = new { 
            editItem.cookies, 
            status = editStatus ? 1 : 0 ,
            editItem.pwd,
            editItem.name,
            editItem.phone,
            remark= "",
            points = 0,                
            editItem.userId
        };
        var res = await Lean.Insert(tableName, JsonSerializer.Serialize(data));
        if (res)
        {
            await LoadData();
            CloseAdd();
        }
    }
}

运行界面

  • 列表页面

wasm-5.png

  • 添加表单

wasm4.png

总结

  • 首先,开发过程来说是比较友好的,对于一个有ASP.NET项目开发经验的人来说上手还是相对平顺的,总体坑不算太多。AntDesign这类开源组件的支持,界面的表现力也是可圈可点,不逊于 vue/ react这些主流的框架。对于完全没有兴趣折腾前端技术的.NET开发者来说,或许是个不错的选择(我的demo代码里 0 javascript哦)。
  • 接下来篇幅,我是准备用来吐槽的!

1、加载实在太慢了

首次加载页面loading时间超过90秒,浏览器在疯狂地加载一堆dll总共计11.2M。

wasm-2.png

这速度瞬间能把95%的人耐心给耗尽。个人觉得Blazor WebAssembly技术来做互联网应用并非是它的用武之地,而在企业应用领域的表现也会差强人意,估计跟当年的Silverlight半斤八两。反而可能会在桌面应用领域能够找到其合适的定位。AntDesign的文档就是推荐大家这么玩的。

wasm-6.png

Winform+WebView2+Blazor WebAssembly,这样loading起来感觉就平顺很多。至少用户对于内容加载速度的心理预期,会大大降低。亲测之,果然非常顺滑!有机会的话,针对套解决方案我再补充一篇文章。

wasm-7.png

2、代码安全问题

Blazor WebAssembly是作为运行在浏览器的运行的客户端代码,起始就跟javascript一样,完全可以把编译后的dll下载到本地,然后用反编译工具进行逆向反编。

wasm-3.png

所以,跟发布服务端应用不用,想要确保代码安全,不被有心人窃取或窥视的话,有两招:

  • NativeAOT发布直接弄成机器码,但是换取的代价就是发布的包容量可能会膨胀一倍之多
  • 做代码混淆,将代码可读性降低,即便让人家反编译出来,也像天书一样难懂。

3、网络安全问题

Blazor WebAssembly应用中HttpClient请求是通过Ajax发送,因此简单的抓包就可以拿到整个请求内容。如果有涉及安全级别较高的接口请求,比如支付什么的,不建议直接在客户端写。而是通过服务端转接会更好。

 

评论
0/200