Blazor开发WEB程序

前言

对于后端程序员来说,前端看起来很简单,但是各种框架非常多,如流行的前端三大框架Vue.js、Angular.js、React.js。每种框架又对应了多种匹配的类库,想打造前端开发完整的知识体系是不容易的。而看起来高大上的一些技术如图技术、分布式计算、云渲染等都多少跟前端技术有一定的联系,就萌生了做全栈开发的想法。
做全栈,最好的是一个技术打天下,而不是完全不相关的技术体系,本人十多年C++研发经验,可以说对C++的各种场景的研发算是比较清楚的,也因此造成了对其他技术的一些偏见。最近做各种项目,才发现某些语言的开发是真香(C#的扩展方法、反射机制、Linq、包管理等极大的提升了开发效率;NODEJS前后端一起开发,一套代码解决一次编译解决多端应用;Vue框架让我不在关心Dom操作)。技术是为解决问题服务,本身没有什么好坏之分。在特定场景下用好特定的技术,能够短平快的解决项目中的问题就是好技术。做全栈开发,最重要的是打通各种语言的链路(以C++为基础,打通各个语言的障碍)。
Webassembly技术就是一种可以让后端人员开发前端程序的一种技术,在使用Blazor之前是通过C++的EMSDK技术开发C++的Webassembly程序(其难度对于项目开发来说是地狱级别,编译、调试、接口封装非常废时间)。所以最近转栈到其他语言的WebAssembly开发。才发现了微软的Blazor 的C# WebAssembly的解决方案。

什么是Blazor

Blazor 是微软在 .NET 里推出的一个 WEB 客户端 UI 交互的框架,使用 Blazor 你可以代替 JavaScript 来实现自己的页面交互逻辑,可以很大程度上进行 C# 代码的复用,Blazor 对于 .NET 开发人员来说是一个不错的选择。
Blazor 有两种托管模式,一种是 Blazor Server 模式,基于 asp.net core 部署,客户端和服务器的交互通过 SignalR 来完成,来实现客户端 UI 的更新和行为的交互。
另外一种是 Blazor WebAssembly 模式, 将 Blazor 应用、其依赖项以及 .NET 运行时下载到浏览器(所以有一个缺陷就是在不同的端中可能要重新编译), 应用将在浏览器线程中直接执行。具有以下优点:
Blazor WebAssembly 托管模型具有以下优点

没有 .NET 服务器端依赖,应用下载到客户端后即可正常运行。
可充分利用客户端资源和功能。
工作可从服务器转移到客户端。
无需 ASP.NET Core Web 服务器即可托管应用。 无服务器部署方案可行,例如通过内容分发网络 (CDN) 为应用提供服务的方案。

Blazor WebAssembly 托管模型具有以下局限性:

应用仅可使用浏览器功能。
需要可用的客户端硬件和软件(例如 WebAssembly 支持)。
下载项大小较大,应用加载耗时较长。
.NET 运行时和工具支持不够完善。 例如,.NET Standard 支持和调试方面存在限制。

本文主要介绍WebAssembly技术。

Blazor安装

  1. 安装Asp.net 环境 并确认勾选WebAssembly组件

image.pngimage.png

  1. 安装完成之后打开VS选择Blazor项目,选择Blazor WebAssembly应用,后续使用默认选项即可

image.png

  1. 创建项目之后如图

image.png

  1. 编译之后运行

image.png

Blazor项目结构

这是创建一个Blazor WebAssembly的大致模板
image.png

  • Counter 组件 (Counter.razor):实现“计数器”页面。
  • FetchData 组件 (FetchData.razor):实现“提取数据”页面。
  • Index 组件 (Index.razor):实现主页。
  • Properties/launchSettings.json:保留开发环境配置。
  • Shared 文件夹:包含以下共享组件和样式表:
  • MainLayout 组件 (MainLayout.razor):应用的布局组件
  • MainLayout.razor.css:应用主布局的样式表。
  • NavMenu 组件 (NavMenu.razor):实现边栏导航。 包括 NavLink 组件 (NavLink),该组件可向其他 Razor 组件呈现导航链接。 NavLink 组件会在系统加载其组件时自动指示选定状态,这有助于用户了解当前显示的组件。
  • NavMenu.razor.css:应用导航菜单的样式表。
  • SurveyPrompt 组件 (SurveyPrompt.razor):Blazor 调查组件(子组件)
  • wwwroot:应用的 Web 根目录文件夹,其中包含应用的公共静态资产,包括 appsettings.json 和配置设置的环境应用设置文件。 index.html 网页是实现为 HTML 页面的应用的根页面.

最初请求应用的任何页面时,都会呈现此页面并在响应中返回。
此页面指定根 App 组件的呈现位置。 使用 app 的 id (

Loading…
) 在 div DOM 元素的位置呈现组件。

  • _Imports.razor:包括要包含在应用组件 (.razor) 中的常见 Razor 指令,如用于命名空间的 @using 指令,相当于全局的引用组件的命令
  • App.razor:应用的根组件,用于使用 Router 组件来设置客户端路由。 Router 组件会截获浏览器导航并呈现与请求的地址匹配的页面。
  • Program.cs:应用入口点,用于设置 WebAssembly 主机:

App 组件是应用的根组件。 对于根组件集合 (builder.RootComponents.Add(“#app”)),使用 app 的 id(wwwroot/index.html 中的

Loading…
)将 App 组件指定为 div DOM 元素。
添加并配置了服务(例如,builder.Services.AddSingleton<IMyDependency, MyDependency>())。

编写第一个页面

Blazor的页面编写使用的是razor的语法,不是类似于前端的JS代码或者HTML写法,razor语法可参考相关文章

Blazor的页面结构有点类似于Vue的结构,有页面结构+逻辑代码组成。

@page "/counter"  //页面路由,如果是一个独立的页面需要加上这个页面标记,如果作为一个子组件则没有必要
    
    <PageTitle>Counter</PageTitle>  //
    
    <h1>Counter</h1>
    
    <p role="status">Current count: @currentCount</p> //@是razor的语法,用于绑定C#的属性或者方法来的
    
     <button class="btn btn-primary" @onclick="IncrementCount">Click me</button> //直接调用方法
   
   //C#代码块
   @code {
    
    
         private int currentCount = 0;

         private void IncrementCount()
         {
    
    
             currentCount++;
         }
      }

这里面一开始看的时候会比较迷惑,C#和页面代码写在一起了,而页面代码竟然可以调用C#的代码,也没有一个类的结构。要理解这个界面我们应该把这个组件理解为一个类对象来处理,页面和逻辑代码同属于一个类里面,虽然有的时候看起来在两个不同的文件里面,但就是一个类(继承ComponentBase类),只是分不同的地方编写而已。关于这块C#的组织方式可以参考
razor组件的C#代码组织形式 https://www.cnblogs.com/harrychinese/p/blazaor_CSharp_code.html
在这里面随便介绍下razor的相关指令

@code是Razor 组件专用,用于在@code代码块中定义页面类需要的字段,属性,方法等定义。@code是@functions的别名。但是建议在Razor组件中使用@code,而非@functions.

@implements 指令,类比如C# 类的implements.

@inherits: 类比C#的中继承,在Blazor的类的组织方式里面有说明

@inject: 从DI容器中引入需要的类,一个单例类的引入。在C#类中使用[inject]标记

@layout: 指定页面需要继承的布局模板。

@model: 专用于MVC 视图和Razor Page页面,用于指定model

@namespace: 用户指定生成的页面类所处的名称空间。

@page指令:用户razor page项目或者razor 组件,用于定义路由。

@preservewhitespace: 是否保留空白,默认是false.

@section: 用户razor page或者是mvc

使用Skia实现Blazor的绘图功能

在Blazor实现绘图可以使用类似于html的canvas功能,使用库 Blazor.Extensions.Canvas实现,内部有很多示例。由于在项目使用skia库比较多,所以对于Blazor绘图使用skia库来实现.

1. skia库安装

使用nuget安装SkiaSharp 和 SkiaSharp.Views.Blazor库,SkiaSharp是基础库,SkiaSharp.Views.Blazor 负责图面的显示。

2. 创建绘图页面

  1. 添加一个绘图页面,本人直接修改主页面 skiacanvas.razor
  2. 添加一个对应的页面的代码类 skiacanvas.razor.cs 实现与skiacanvas.razor页面进行绑定
  3. 在skiacanvas.razor中添加SKCanvasView标签,设置IgnorePixelScaling属性为true(表示为忽略按像素缩放,不然在鼠标事件中得到的偏移值与实际值是有偏差的,这个跟系统的屏幕缩放比例成一个正比关系),指定@ref=“canvasView”这个canvasView对象是在C#代码中定义的成员变量 SKCanvasView canvasView
<Layout ShowFooter="false">   // 这里面使用了BootstrapBlazor组件库进行页面布局
	<Header>
		<PageTitle>XDGraph WEB系统</PageTitle>
		<br/>
	</Header>
<Main>
		<SKCanvasView @ref="canvasView" IgnorePixelScaling=true style="@CursorType" OnPaintSurface="OnPaintSurface"  @onkeydown="OnCanvasKeyDown" 
		@onmousemove="OnCanvasMouseMove"  @onwheel="OnCanvasMouseWheel" @onmousedown="OnCanvasMouseDown" tabindex="0"
		@onmouseup="OnCanvasMouseUp"/>
</Main>
</Layout>

注意事项:

  1. 默认情况下SKCanvasView标签是不支持键盘事件的(与html5的canvas类似),要响应键盘事件需要给定 tabindex属性
  2. OnPaintSurface 事件是必须实现的,因为所有的绘图都是在OnPaintSurface事件中进行
  3. canvasView绑定对象的初始化并不是在对应的c#代码的构造函数中,而是在页面初始化完成之后。

3. SkiaCanvas.Razor.cs代码实现

以下是代码,实际代码在项目中不可直接贴出来.

using SkiaSharp;
using SkiaSharp.Views.Blazor;
public partial class SkiaCanvas
{
    
    
     protected SKCanvasView canvasView=null;

      
      public void OnCanvasKeyDown(KeyboardEventArgs e)
      {
    
    
          //键盘事件
      }
    public void OnCanvasMouseWheel(WheelEventArgs e)
    {
    
    
        
    }
     public void OnCanvasMouseMove(MouseEventArgs e)
    {
    
    
        
        var mouse_event = toEvent(e);

        
        _component.MouseMove(mouse_event);
        
        move_point =new Point(mouse_event.OffsetX,mouse_event.OffsetY);
    }
    public void OnPaintSurface(SKPaintSurfaceEventArgs e)
    {
    
    
        var canvas = e.Surface.Canvas;

        //SKBitmap bitmap = new SKBitmap(e.Info.Width, e.Info.Height);
        canvas.Clear(SKColors.White);

      //调用底层函数绘制
        _component.Graph.View.Redraw(canvas);

        //绘制坐标;
        using var paint = new SKPaint
        {
    
    
            Color = SKColors.Black,
            IsAntialias = true,
            Typeface = SkiaChinaFont.ChinaFont,
            TextSize = 24
        };

        if (move_point != null)
        {
    
    
            //move_point是mousemove事件中获取的,直接在渲染接口中好像没有办法获取到;
            var msg1 = $"x:{
      
      move_point.X.ToString("F2")} y:{
      
      move_point.Y.ToString("F2")}";
            canvas.DrawText(msg1, 0, e.Info.Height-30, paint);
        }
    }
    
}

4. 关于绘图界面的自适应问题

SKCanvasView的窗口大小使用浏览器窗口变化,组件中标签大小的设置通过style="width:900px;height=900px"类似的代码来实现,razor组件代码里面没有对应的事件响应,包括默认前端语言也没有,一般通过监听window.reszie事件来处理。这就涉及到C#与JS的交互问题。所幸的是有一个开源库帮我们封装好了 BlazorPro.BlazorSize这个库解决了大小改变的适应性问题。具体参考库的使用方法.

下面代码在原来的代码上增加以下代码实现.

    public static string DefaultStyle= "width:100%;heiht:100%;";
    [Parameter]
    public string CursorType {
    
     get; set; } = DefaultStyle;

   protected override void OnAfterRender(bool firstRender)
    {
    
    
    
        if (firstRender)
        {
    
    
            // Subscribe to the OnResized event. This will do work when the browser is resized.
            listener.OnResized += WindowResized;
        }
    }
    void WindowResized(object? sender,BrowserWindowSize window)
    {
    
    
         
        browser = window;
        int width = browser.Width;
        int height = browser.Height-100;
        DefaultStyle = $"width:100%;height:{
      
      height}px;";
        CursorType = DefaultStyle;
        StateHasChanged();
    }

5. 中文文字显示

默认情况下Skia绘制中文字符显示不出来,这个时候需要添加对中文字体的显示.

 public static class SkiaChinaFont
    {
    
    
        public static SKTypeface ChinaFont {
    
     get; private set; }

        static SkiaChinaFont()
        {
    
    
            //加载资源方案,设置字体文件属性为嵌入的资源(需要注意)
            try
            {
    
    
                //嵌入资源的访问是应用程序名加上路径,路径使用.而不是使用/。
                //如XDGraphWeb.res.DroidSansFallback.ttf 表示的是应用程序名为XDGraphWeb 在当前项目的res文件夹下的DroidSansFallback.ttf文件.
                var fontStream = Assembly.GetExecutingAssembly().GetManifestResourceStream("XDGraphWeb.res.DroidSansFallback.ttf");
                ChinaFont = SKTypeface.FromStream(fontStream);
            }
            catch(Exception e)
            {
    
    
                var error=e.Message;
                int c = 0;
            }
            
        }
    }

6. 成果

image.png

Blazor之静态资源的访问

在WebAssembly中的静态资源有两种一种是C#的内嵌资源,还有一种是wwwroot目录下的文件访问。

1. 内嵌资源

我们平时接触的更多的是本地文件系统,或者是 FTP 、对象存储这类运行在远程服务器上的文件系统,这些都是非内嵌资源,所以,内嵌资源主要是指那些没有目录层级的文件资源,因为它会在编译的时候“嵌入”到动态链接库(DLL)中。像之前引入skia中的资源文件就是属于内嵌资源的访问。

  1. 在 Visual Studio 中选中指定文件,在其属性窗口中选择生成操作为嵌入的资源

image.png
这样,我们就完成了内嵌资源的定义。而定义内嵌资源,本质上还是为了在运行时期间去读取和使用,那么,自然而然地,我们不禁要问,该怎么读取这些内嵌资源呢?在Assembly类中,微软为我们提供了下列接口来处理内嵌资源。

public virtual ManifestResourceInfo GetManifestResourceInfo(string resourceName);
public virtual string[] GetManifestResourceNames();//返回资源名称
public virtual Stream GetManifestResourceStream(Type type, string name);//获取资源中的留数据
public virtual Stream GetManifestResourceStream(string name);

通过GetManifestResourceNames这个方法我们发现其资源是通过A.B.C的路径方式来访问的.

var assembly = Assembly.GetExecutingAssembly();
var resources = assembly.GetManifestResourceNames();
resources.ToList().ForEach(x => Console.WriteLine(x));
var fileInfo = assembly.GetManifestResourceInfo(resources[0]);
var fileStream = assembly.GetManifestResourceStream(resources[0]);//获取文件的字节流

2. httpClient

HttpClient 类所在库为 System.Net.Http,Blazor webassembly 默认模版已经自动将HttpClient 注册到DI 容器中了, 使用起来非常方便. Program.Main 函数注册DI容器代码

builder.Services.AddScoped(sp => new HttpClient {
    
     BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

直接使用 HttpClient 问题有:

  • HttpClient 主要问题是, 即使 Dispose 之后, 也不能即时关闭 socket 连接, 在 windows 下, 默认需要等 240秒之后才能关闭 socket. 短时大量使用 HttpClient, 会将客户端和服务器端 socket 连接消耗殆尽, 详见参考文档1的分析. 所以, 客户端应用程序一般使用单例模式使用 HttpClient 类. Blazor webassembly 也是如此.
  • 如果使用单例模式, 如要为 不同url 设置不同的 header, 就很不方便.
  • HttpClient 还会缓存 IP, 如果 DNS 之后有更新, HttpClient 仍会使用老的 IP

所以一般情况下都是将 HttpClient注册为单例类,注册为单例之后在Razor.cs代码为

 //直接获取单例类;
[Inject]
private HttpClient Http {
    
     get; set; }  //一定要设置为get;set 否则会崩溃。HttpClient只能设置在这样的组件类中设置,普通类设置无效还有可能出问题。

HttpClient的使用

// 读取html数据,在wwwroot目录下的数据
var xml_string= await Http.GetStringAsync("/sample-data/defaultConfig.Xml"); 
//获取json数据
 protected override async Task OnInitializedAsync()
{
    
    
    Items = await Http.GetFromJsonAsync<MenuItem[]>("sample-data/menu.json");  
    return;
}

Blazor之BootstrapBlazor界面库

https://www.blazor.zone/introduction BootstrapBlazor界面库提供了近200个组件,基本涵盖了前端开发的大多数界面使用。
image.png

  1. 对话框的使用
 
@using System.ComponentModel.DataAnnotations
@using Microsoft.Extensions.Localization
@using System.Xml 
<div>
 
    <ValidateForm Model="Project" OnValidSubmit="OnSubmit">
        <div class="row g-3">
            
            <div class="col-12">
                <InputUpload @bind-Value="@Project.Mxe" DisplayText="工程文件名"  />
            </div>
            <div class="col-12">
                <Button ButtonType="@ButtonType.Submit" Text="提交"></Button>
            </div>
        </div>
    </ValidateForm>
</div>
@code
{
    
    
   //表单验证设置
    private class PersonInfo
    {
    
    
        [Required]
        [FileValidation(Extensions = new string[] {
      
       ".mxe" }, FileSize = 2*1024 * 1024)]
        public IBrowserFile? Mxe {
    
     get; set; }
    }

    private PersonInfo Project {
    
     get; set; } = new PersonInfo();
    //对话框的参数传递,返回值必须是object
    [CascadingParameter(Name = "BodyContext")]
    public object? Graph {
    
     get; set; }

    //设置回调函数参数;
    [Parameter]
    public Action? OnClose {
    
     get; set; }

    //执行提交功能,打开本地文件,相当于实现了文件上传功能
    private async Task OnSubmit(EditContext context)
    {
    
    
        var graph=Graph as XDGraph;
        if(graph==null || Project.Mxe==null)
        {
    
    
            return;
        }
        MemoryStream ms = new MemoryStream();

        await  Project.Mxe.OpenReadStream().CopyToAsync(ms);
        //从内存数据流中读取文件;
        XmlDocument docxml = new XmlDocument();
       
        byte[] b = ms.ToArray();
        string s = System.Text.Encoding.UTF8.GetString(b, 0, b.Length);

        docxml.LoadXml(s);

      
        //删除历史;
        graph.Editor().UndoManager().Clear();

        var codec = new XDCodec();
        graph.SetModel(codec.DecodeGraphModel(docxml, graph.Model));
        graph.Repaint();

        if(OnClose!=null)
        {
    
    
            OnClose.Invoke();
        }
         return ;
    }
}
 

使用对话框功能


 public async Task OpenFile()
 {
    
    
    var option = new DialogOption()
    {
    
    
        Title = "打开本地工程文件",
        IsKeyboard=true,
        ShowHeaderCloseButton=true
        
    };
    //也可以当成参数传递;
    option.BodyContext = MainGrpah;
    
    option.BodyTemplate = BootstrapDynamicComponent.CreateComponent<OpenDialogComponent>(new Dictionary<string, object?>
    {
    
    
        [nameof(OpenDialogComponent.OnClose)] = new Action(async () => await option.Dialog.Close())
    }).Render();
    await DialogService.Show(option);
 }

Blazor 相关学习网站

Github上关于Blazor的例子 https://github.com/syncfusion/blazor-samples
Blazor.Extensions.Canvas https://github.com/mizrael/BlazorCanvas
SkiaSharp在网页上绘图 https://www.cnblogs.com/sunnytrudeau/p/15574467.html
bibi视频教程 https://www.bilibili.com/video/BV19K4y1e7kd?p=1
Blazor官网 https://docs.microsoft.com/zh-cn/aspnet/core/blazor/?view=aspnetcore-6.0
https://github.com/AdrienTorris/awesome-blaz
blazor界面库 https://www.blazor.zone/introduction
blazor的C#组织方式 https://www.cnblogs.com/harrychinese/p/blazaor_CSharp_code.html
Blazor的C#和JS互操作 https://www.cnblogs.com/functionMC/p/16552500.html

猜你喜欢

转载自blog.csdn.net/qq_33377547/article/details/126935824