使用 Blazor 创建 DEV 的离线页面

2025-06-08

使用 Blazor 创建 DEV 的离线页面

我偶然看到了Ali Spittel关于创建DEV 离线页面的一篇有趣的帖子

鉴于我过去曾使用 WebAssembly 做过一些实验,我决定尝试在 WebAssembly 中实现我自己的功能,特别是使用Blazor

入门

警告:Blazor 是一个使用 .NET 技术栈(特别是 C# 语言)构建客户端 Web 应用程序的平台。它处于高度实验性阶段,因此在撰写本文时(我使用的是 build 方法3.0.0-preview6.19307.2)可能会有所变化。

首先,您需要按照Blazor 的设置指南进行操作,完成后在您最喜欢的编辑器中创建一个新项目(我使用了 VS Code)。

然后,我从PagesShared文件夹中删除了所有样板代码(除了任何_Imports.razor文件),并从css和 文件夹中删除了 Bootstrap sample-data。现在我们有一个完全空的 Blazor 项目。

创建布局

我们首先需要做的是创建布局文件。Blazor 与 ASP.NET MVC 类似,使用布局文件作为所有页面(当然,所有使用该布局的页面,你可以有多个布局)的基础模板。因此,在Shared名为 的文件中创建一个新文件MainLayout.razor并进行定义。假设我们希望它全屏显示,那么操作起来会非常简单

@inherits LayoutComponentBase

@Body
Enter fullscreen mode Exit fullscreen mode

此文件继承了 Blazor 提供的布局基类,LayoutComponentBase该基类允许我们访问@Body属性,从而将页面内容放置在任何我们想要的 HTML 中。我们不需要任何额外的设置,所以直接放在@Body页面中即可。

创建我们的离线页面

现在是时候制作离线页面了,我们首先在Pages文件夹中创建一个新文件,我们称之为Offline.html

@page "/"

<h3>Offline</h3>
Enter fullscreen mode Exit fullscreen mode

这是我们的起点,首先我们有一个@page指令,它告诉 Blazor 这是一个我们可以导航到的页面,并且它将响应的 URL 是"/"。我们这里有一些占位符 HTML,接下来我们将替换它们。

启动画布

离线页面本质上是一个可以绘制的大画布,我们需要创建它,让我们Offline.razor用画布元素进行更新:

@page "/"

<canvas></canvas>
Enter fullscreen mode Exit fullscreen mode

设置画布大小

我们需要将画布尺寸设置为全屏,但目前尺寸0x0并不理想。理想情况下,我们希望获取浏览器的innerWidthinnerHeight,为此我们需要使用Blazor 的JavaScript 互操作。

我们将快速创建一个新的 JavaScript 文件来进行互操作(调用它helper.js并将其放入wwwroot,同时更新index.htmlwwwroot引用它):

window.getWindowSize = () => {
    return { height: window.innerHeight, width: window.innerWidth };
};
Enter fullscreen mode Exit fullscreen mode

接下来我们将创建一个 C#struct来表示该数据(我WindowSize.cs在项目根目录中添加了一个名为的文件):

namespace Blazor.DevToOffline
{
    public struct WindowSize
    {
        public long Height { get; set; }
        public long Width { get; set; }
    }
}

Enter fullscreen mode Exit fullscreen mode

最后,我们需要在 Blazor 组件中使用它:

@page "/"
@inject IJSRuntime JsRuntime

<canvas height="@windowSize.Height" width="@windowSize.Width"></canvas>

@code {
    WindowSize windowSize;

    protected override async Task OnInitAsync()
    {
        windowSize = await JsRuntime.InvokeAsync<WindowSize>("getWindowSize");
    }
}
Enter fullscreen mode Exit fullscreen mode

这是添加的一点代码,所以让我们将其分解一下。

@inject IJSRuntime JsRuntime
Enter fullscreen mode Exit fullscreen mode

这里我们使用依赖注入来将其作为我们组件上IJSRuntime调用的属性进行注入。JsRuntime

<canvas height="@windowSize.Height" width="@windowSize.Width"></canvas>
Enter fullscreen mode Exit fullscreen mode

接下来,我们将元素height和属性设置为我们的 实例(名为 的实例)的字段值。注意前缀,这告诉编译器这指的是 C# 变量,而不是静态字符串。width<canvas>structwindowSize@

@code {
    WindowSize windowSize;

    protected override async Task OnInitAsync()
    {
        windowSize = await JsRuntime.InvokeAsync<WindowSize>("getWindowSize");
    }
}
Enter fullscreen mode Exit fullscreen mode

现在,我们在组件中添加了一个代码块。它包含变量windowSize(未初始化,但它是一个结构体,因此具有默认值),然后我们重写了Lifecycle 方法OnInitAsync在该方法中,我们调用 JavaScript 获取窗口大小并将其赋值给局部变量。

恭喜,你现在拥有了全屏画布!🎉

连接事件

我们的画布可能已经出现了,但它还没有做任何事情,所以让我们通过添加一些事件处理程序来解决这个问题:

@page "/"
@inject IJSRuntime JsRuntime

<canvas height="@windowSize.Height"
        width="@windowSize.Width"
        @onmousedown="@StartPaint"
        @onmousemove="@Paint"
        @onmouseup="@StopPaint"
        @onmouseout="@StopPaint" />

@code {
    WindowSize windowSize;

    protected override async Task OnInitAsync()
    {
        windowSize = await JsRuntime.InvokeAsync<WindowSize>("getWindowSize");
    }

    private void StartPaint(UIMouseEventArgs e)
    {
    }

    private async Task Paint(UIMouseEventArgs e)
    {
    }

    private void StopPaint(UIMouseEventArgs e)
    {
    }
}
Enter fullscreen mode Exit fullscreen mode

在 Blazor 中绑定事件时,需要在事件名称前添加前缀@,例如@onmousedown,然后提供事件发生时要调用的函数名称,例如@StartPaint。这些函数的签名是返回voidTask,具体取决于它是否是异步的。函数的参数需要是适当类型的事件参数,映射到 DOM 等效项(UIMouseEventArgsUIKeyboardEventArgs等)。

注意:如果你将此与 JavaScript 参考实现进行比较,你会注意到我没有使用touch事件。这是因为,在我今天的实验中,Blazor 中绑定触摸事件存在一个 bug。记住,这只是预览版!

获取画布上下文

注意:我将讨论如何设置与<canvas>Blazor 的交互,但在实际应用程序中,您更可能希望使用BlazorExtensions/Canvas,而不是自行使用。

由于我们需要使用画布的 2D 上下文,因此我们需要访问它。但问题是,这是一个 JavaScript API,而我们使用的是 C#/WebAssembly,这有点意思。

最终,我们必须在 JavaScript 中实现这一点,并依赖 Blazor 的 JavaScript 互操作功能,因此仍然需要编写一些 JavaScript!

让我们编写一个小的 JavaScript 模块来为我们提供可用的 API:

((window) => {
    let canvasContextCache = {};

    let getContext = (canvas) => {
        if (!canvasContextCache[canvas]) {
            canvasContextCache[canvas] = canvas.getContext('2d');
        }
        return canvasContextCache[canvas];
    };

    window.__blazorCanvasInterop = {
        drawLine: (canvas, sX, sY, eX, eY) => {
            let context = getContext(canvas);

            context.lineJoin = 'round';
            context.lineWidth = 5;
            context.beginPath();
            context.moveTo(eX, eY);
            context.lineTo(sX, sY);
            context.closePath();
            context.stroke();
        },

        setContextPropertyValue: (canvas, propertyName, propertyValue) => {
            let context = getContext(canvas);

            context[propertyName] = propertyValue;
        }
    };
})(window);
Enter fullscreen mode Exit fullscreen mode

我已经使用在匿名自执行函数中创建的闭包范围完成了此操作,这样,canvasContextCache我使用的避免不断获取上下文的就不会暴露。

该模块为我们提供了两个功能,第一个是在画布上的两点之间画一条线(我们需要它来涂鸦!),第二个是更新上下文的属性(我们需要它来改变颜色!)。

你可能还注意到,我根本没有调用document.getElementById,只是以某种方式“神奇地”获取了画布。这可以通过在 C# 中捕获组件引用并传递该引用来实现。

但这仍然是 JavaScript,我们在 C# 中做什么呢?好吧,我们创建一个 C# 包装类!

public class Canvas2DContext
{
    private readonly IJSRuntime jsRuntime;
    private readonly ElementRef canvasRef;

    public Canvas2DContext(IJSRuntime jsRuntime, ElementRef canvasRef)
    {
        this.jsRuntime = jsRuntime;
        this.canvasRef = canvasRef;
    }

    public async Task DrawLine(long startX, long startY, long endX, long endY)
    {
        await jsRuntime.InvokeAsync<object>("__blazorCanvasInterop.drawLine", canvasRef, startX, startY, endX, endY);
    }

    public async Task SetStrokeStyleAsync(string strokeStyle)
    {
        await jsRuntime.InvokeAsync<object>("__blazorCanvasInterop.setContextPropertyValue", canvasRef, "strokeStyle", strokeStyle);
    }
}
Enter fullscreen mode Exit fullscreen mode

这是一个通用类,它采用捕获的引用和 JavaScript 互操作 API,并为我们提供一个更好的编程接口。

连接我们的上下文

现在我们可以连接上下文并准备在画布上画线:

@page "/"
@inject IJSRuntime JsRuntime

<canvas height="@windowSize.Height"
        width="@windowSize.Width"
        @onmousedown="@StartPaint"
        @onmousemove="@Paint"
        @onmouseup="@StopPaint"
        @onmouseout="@StopPaint"
        @ref="@canvas" />

@code {
    ElementRef canvas;

    WindowSize windowSize;

    Canvas2DContext ctx;
    protected override async Task OnInitAsync()
    {
        windowSize = await JsRuntime.InvokeAsync<WindowSize>("getWindowSize");
        ctx = new Canvas2DContext(JsRuntime, canvas);
    }

    private void StartPaint(UIMouseEventArgs e)
    {
    }

    private async Task Paint(UIMouseEventArgs e)
    {
    }

    private void StopPaint(UIMouseEventArgs e)
    {
    }
}
Enter fullscreen mode Exit fullscreen mode

通过添加@ref="@canvas"我们的<canvas>元素,我们创建了我们需要的引用,然后在OnInitAsync函数中创建了我们Canvas2DContext将要使用的引用。

在画布上绘画

我们终于准备好在画布上进行一些绘图了,这意味着我们需要实现这些事件处理程序:

    bool isPainting = false;
    long x;
    long y;
    private void StartPaint(UIMouseEventArgs e)
    {
        x = e.ClientX;
        y = e.ClientY;
        isPainting = true;
    }

    private async Task Paint(UIMouseEventArgs e)
    {
        if (isPainting)
        {
            var eX = e.ClientX;
            var eY = e.ClientY;

            await ctx.DrawLine(x, y, eX, eY);
            x = eX;
            y = eY;
        }
    }

    private void StopPaint(UIMouseEventArgs e)
    {
        isPainting = false;
    }
Enter fullscreen mode Exit fullscreen mode

不可否认,这些与 JavaScript 实现并没有太大区别,它们所要做的就是从鼠标事件中获取坐标,然后将它们传递给画布上下文包装器,进而调用适当的 JavaScript 函数。

结论

🎉 完成了!你可以在这里看到它的运行情况,代码也在 GitHub 上。

GitHub 徽标 亚伦鲍威尔/ blazor-devto-offline

使用 Blazor 创建 DEV.to 离线页面的演示

这是对 Blazor 的快速了解,但更重要的是,我们如何在可能需要我们与 JavaScript 进行更多互操作的场景中使用 Blazor,而许多场景都需要这样做。

我希望你喜欢它并准备好进行你自己的 Blazor 实验!

奖金,颜色选择器

在上面的例子中,有一件事我们没有做,那就是实现颜色选择器!

我想将其作为通用组件来执行,以便我们可以这样做:

<ColourPicker OnClick="@SetStrokeColour"
              Colours="@colours" />
Enter fullscreen mode Exit fullscreen mode

ColourPicker.razor在一个名为(文件名很重要,因为这是组件的名称)的新文件中,我们将创建我们的组件:

<div class="colours">
    @foreach (var colour in Colours)
    {
        <button class="colour"
                @onclick="@OnClick(colour)"
                @key="@colour">
        </button>
    }
</div>

@code {
    [Parameter]
    public Func<string, Action<UIMouseEventArgs>> OnClick { get; set; }

    [Parameter]
    public IEnumerable<string> Colours { get; set; }
}

Enter fullscreen mode Exit fullscreen mode

我们的组件将包含两个可从父级设置的参数:颜色集合和点击按钮时调用的函数。我编写的事件处理程序会传入一个返回 action 的函数<button>,因此它是一个在元素创建时“绑定”到颜色名称的函数。

这意味着我们有这样的用法:

@page "/"
@inject IJSRuntime JsRuntime

<ColourPicker OnClick="@SetStrokeColour"
              Colours="@colours" />

// snip

@code {
    IEnumerable<string> colours = new[] { "#F4908E", "#F2F097", "#88B0DC", "#F7B5D1", "#53C4AF", "#FDE38C" };

    // snip

    private Action<UIMouseEventArgs> SetStrokeColour(string colour)
    {
        return async _ =>
        {
            await ctx.SetStrokeStyleAsync(colour);
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

现在,如果您单击顶部的颜色选择器,您将获得不同颜色的笔。

涂鸦快乐!

鏂囩珷鏉ユ簮锛�https://dev.to/azure/creating-dev-s-offline-page-using-blazor-29dl
PREV
调试 Angular 9:与组件交互
NEXT
使用 Node.js 和 Express 构建 Web API