You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在同一Blazor组件的多个实例间隔离JS变量?

解决Blazor组件中Fabric.js实例的隔离问题

问题背景

我有一个基于Fabric.js和Canvas的Blazor组件,采用传统JS隔离方案(导入模块、调用导出函数)。但当创建多个组件实例时,所有实例共享同一个canvas和data变量,无法实现真正的隔离。

组件代码示例

MyComponent.razor

<canvas id="@canvasId">
</canvas>

MyComponent.razor.cs

[Inject] private IJSRuntime _jsRuntime { get; init; }
private IJSObjectReference module;
private string canvasId ;

protected override async Task OnInitializedAsync()
{
    canvasId = $"canvas_MediaLabelEditor_{Guid.NewGuid().ToString()}";
    Reload();
}

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    ...
    string[] args = new string[] { canvasId };
    module = await _jsRuntime.InvokeAsync<IJSObjectReference>("import", "./js/fabricBboxLabel.js");
    await module.InvokeVoidAsync("loadCanvas", args);
}

protected async Task DoAmazingThings()
{
    string[] args = new string[] { ..., ..., ... };
    await module.InvokeVoidAsync("doAmazingThings", args);
}

./js/fabricBboxLabel.js

var canvas;
var data; //array
export function loadCanvas(id){
    canvas = new fabric.Canvas(id);
}
export function doAmazingThings(a,b,c){
    array.push( ... )
    ...
    canvas.renderAll();
}

问题复现

通过一个简单的求和组件可以复现该共享变量问题:

IsolationTest.razor

<button @onclick="@(async () => await OnClick())">点击累加</button>

IsolationTest.razor.cs

[Inject] private IJSRuntime _jsRuntime { get; init; }
private IJSObjectReference module;
   
protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        module = await _jsRuntime.InvokeAsync<IJSObjectReference>("import", "./js/isolationtest.js");
    }
}
public async Task OnClick()
{
   object[] args = new object[] { 5 };
   await module.InvokeVoidAsync("sumar", args);
}

isolationtest.js

var sum=0;

export function sumar(inte) {
    console.log("before sum="+sum)
    sum=sum+inte;
    console.log("after sum="+sum)
}
alert(sum);

Index.razor

<IsolationTest></IsolationTest>
<IsolationTest></IsolationTest>
<IsolationTest></IsolationTest>

输出结果(随机点击按钮)

before sum=0
after sum=5

before sum=5
after  sum=10

before sum=10
after sum=15

before sum=15
after sum=20

before sum=20
after sum=25

before sum=25
after sum=30

解决方案

问题根源在于ES模块是单例模式,模块内的顶层变量会被所有导入实例共享。要实现真正的隔离,需要为每个组件实例创建独立的状态上下文,以下是两种常用方案:

方案1:工厂函数创建独立实例

修改JS模块,通过工厂函数返回包含独立状态和方法的对象,每个Blazor组件实例调用工厂函数获取专属操作对象:

修改后的fabricBboxLabel.js

export function createCanvasInstance(id) {
    // 每个实例拥有独立的canvas和data
    const canvas = new fabric.Canvas(id);
    const data = [];

    return {
        doAmazingThings(a, b, c) {
            data.push(...);
            // 执行自定义操作
            canvas.renderAll();
        },
        // 新增获取数据的方法,用于回传C#
        getData() {
            return JSON.parse(JSON.stringify(data));
        }
    };
}

修改MyComponent.razor.cs

[Inject] private IJSRuntime _jsRuntime { get; init; }
private IJSObjectReference module;
private IJSObjectReference canvasInstance; // 保存当前组件的专属实例
private string canvasId ;

protected override async Task OnInitializedAsync()
{
    canvasId = $"canvas_MediaLabelEditor_{Guid.NewGuid().ToString()}";
    Reload();
}

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        module = await _jsRuntime.InvokeAsync<IJSObjectReference>("import", "./js/fabricBboxLabel.js");
        // 创建专属的canvas实例
        canvasInstance = await module.InvokeAsync<IJSObjectReference>("createCanvasInstance", canvasId);
    }
}

protected async Task DoAmazingThings()
{
    string[] args = new string[] { ..., ..., ... };
    // 调用专属实例的方法
    await canvasInstance.InvokeVoidAsync("doAmazingThings", args);
}

// 组件销毁时清理JS对象,避免内存泄漏
public async ValueTask DisposeAsync()
{
    if (canvasInstance != null)
    {
        await canvasInstance.DisposeAsync();
    }
    if (module != null)
    {
        await module.DisposeAsync();
    }
}

方案2:ES6类封装实例状态

通过类封装每个实例的状态和行为,适合逻辑复杂的场景:

修改后的fabricBboxLabel.js

export class CanvasEditor {
    constructor(id) {
        this.canvas = new fabric.Canvas(id);
        this.data = [];
    }

    doAmazingThings(a, b, c) {
        this.data.push(...);
        this.canvas.renderAll();
    }

    getData() {
        return JSON.parse(JSON.stringify(this.data));
    }
}

修改MyComponent.razor.cs

[Inject] private IJSRuntime _jsRuntime { get; init; }
private IJSObjectReference module;
private IJSObjectReference canvasEditor; // 保存当前组件的编辑器实例
private string canvasId ;

protected override async Task OnInitializedAsync()
{
    canvasId = $"canvas_MediaLabelEditor_{Guid.NewGuid().ToString()}";
    Reload();
}

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        module = await _jsRuntime.InvokeAsync<IJSObjectReference>("import", "./js/fabricBboxLabel.js");
        // 创建类实例
        canvasEditor = await module.InvokeAsync<IJSObjectReference>("new CanvasEditor", canvasId);
    }
}

protected async Task DoAmazingThings()
{
    string[] args = new string[] { ..., ..., ... };
    await canvasEditor.InvokeVoidAsync("doAmazingThings", args);
}

public async ValueTask DisposeAsync()
{
    if (canvasEditor != null)
    {
        await canvasEditor.DisposeAsync();
    }
    if (module != null)
    {
        await module.DisposeAsync();
    }
}

关键说明

  • ES模块在浏览器中是单例,顶层变量会被所有导入者共享,因此不能用顶层变量存储实例状态。
  • 每个Blazor组件实例需要持有自己的JS对象引用,调用该对象的方法而非模块的顶层函数。
  • 组件销毁时必须调用DisposeAsync()清理JS对象,避免内存泄漏。

内容的提问来源于stack exchange,提问作者Pablo Fuenzalida

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.25 02:45:34