如何在同一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
相关产品推荐
相关产品推荐

