Use Js Interop

作者 dotnet0608d8924cd3MIT5.5K 個星標收錄於 2026年10月8日更新於 2026年10月8日儲存庫今天更新

Add, review, or fix JavaScript interop in Blazor components. USE FOR: calling JavaScript from Blazor, calling .NET from JavaScript, collocated .razor.js modules, IJSRuntime, IJSObjectReference lifecycle, DotNetObjectReference, ElementReference, timing rules for when JS is available, IAsyncDisposable disposal of JS references, server-side JS interop safety. DO NOT USE FOR: general Blazor component authoring without JS interop needs (use author-component), forms (use collect-user-input).

AI 產生的概覽

為 Blazor 元件新增、審查與修正 JavaScript 互通性的指引。

功能
此技能提供 Blazor 中 JavaScript 互通性的規則與程式碼模式,涵蓋並置的 .razor.js 模組、IJSRuntime 用法、IJSObjectReference 生命週期、DotNetObjectReference 回呼、ElementReference、呼叫時機規則、互通性呼叫批次處理以及處置。它產出實際的 C# 與 JavaScript 範例,以及常見錯誤與修正方式的檢查清單。此技能僅含說明,沒有指令碼或資源。
適用情境
適用於從 Blazor 呼叫 JavaScript 或從 JavaScript 呼叫 .NET、設定並置 JS 模組、處理 JS 參照處置,或審查既有互通性程式碼中的時機、批次處理與伺服器端安全問題。不適用於無需互通性的一般 Blazor 元件撰寫或表單情境。
執行需求
此技能不隨附指令碼或套件,只需代理程式即可執行。其指引假設已有使用 .NET 與 JavaScript 工具的 Blazor 專案。

JS Interop in Blazor

1. Collocated JS Modules

Always use collocated .razor.js files with export — never global window.* functions or <script> tags.

javascript
// ChartPanel.razor.js — placed next to ChartPanel.razorexport function initialize(canvas, dotNetRef) { /* ... */ }export function updateData(points) { /* ... */ }export function dispose() { /* ... */ }

Import paths: same project = "./Components/ChartPanel.razor.js", RCL = "./_content/{AssemblyName}/...".

2. Lifecycle Timing

All JS interop must happen in OnAfterRenderAsync or event handlers — never in OnInitialized, OnParametersSet, or constructors. JS is not available during server prerendering.

Use a typed interop wrapper (see Section 4) — never call InvokeAsync/InvokeVoidAsync with raw string literals:

csharp
private ChartInterop? _chart;
protected override async Task OnAfterRenderAsync(bool firstRender){    if (firstRender)    {        _chart = new ChartInterop(JS);        await _chart.InitializeAsync(_canvasRef);    }}

Parameter changes: set a flag in OnParametersSet, apply in OnAfterRenderAsync:

csharp
private bool _dataChanged;
protected override void OnParametersSet() => _dataChanged = true;
protected override async Task OnAfterRenderAsync(bool firstRender){    if (firstRender) { /* init */ }    else if (_dataChanged && _chart is not null)    {        _dataChanged = false;        await _chart.UpdateDataAsync(DataPoints);    }}

3. Batch Related Operations

Each JS interop call crosses the .NET-to-JS boundary (and in Blazor Server, the SignalR circuit). Batching applies in both directions — .NET→JS and JS→.NET.

.NET → JS: merge consecutive calls

If the C# side makes two or more JS calls in a row, combine them into one JS function:

csharp
// ❌ Two round-trips — theme and locale are always applied togetherawait _module.InvokeVoidAsync("applyTheme", theme);await _module.InvokeVoidAsync("applyLocale", locale);
// ❌ Result of one call feeds into another — both can stay in JSvar token = await _module.InvokeAsync<string>("createAccessToken");await _module.InvokeVoidAsync("storeToken", token);
javascript
// ✅ One call applies both — no data dependency, no reason for two tripsexport function applyPreferences(theme, locale) {    document.documentElement.dataset.theme = theme;    document.documentElement.lang = locale;}
// ✅ Chain stays in JS — the token never needs to cross the boundaryexport function createAndStoreToken() {    const token = crypto.randomUUID();    sessionStorage.setItem('access-token', token);    return token;}

JS → .NET: batch callbacks

When JS needs to send multiple pieces of data back to .NET, send them in a single invokeMethodAsync call rather than making separate callbacks:

javascript
// ❌ Two .NET round-trips from JSawait dotNetRef.invokeMethodAsync(ON_VOLUME_CHANGED, volume);await dotNetRef.invokeMethodAsync(ON_PLAYBACK_CHANGED, isPlaying);
// ✅ One callback with all dataawait dotNetRef.invokeMethodAsync(ON_PLAYER_STATE_CHANGED, { volume, isPlaying });

Rule: if two interop calls always happen together from either side, merge them into one function.

4. Typed Interop Wrapper

Encapsulate interop for a feature in a plain class that owns the module lifecycle:

csharp
public sealed class ChartInterop : IAsyncDisposable{    internal const string ModulePath = "./Components/ChartPanel.razor.js";    internal const string InitMethod = "initialize";    internal const string UpdateMethod = "updateData";    internal const string DisposeMethod = "dispose";
    private readonly IJSRuntime _js;    private IJSObjectReference? _module;
    public ChartInterop(IJSRuntime js) => _js = js;
    private async ValueTask<IJSObjectReference> GetModuleAsync()        => _module ??= await _js.InvokeAsync<IJSObjectReference>("import", ModulePath);
    public async ValueTask InitializeAsync(ElementReference canvas)    {        var module = await GetModuleAsync();        await module.InvokeVoidAsync(InitMethod, canvas);    }
    public async ValueTask UpdateDataAsync(IReadOnlyList<DataPoint> points)    {        var module = await GetModuleAsync();        await module.InvokeVoidAsync(UpdateMethod, points);    }
    public async ValueTask DisposeAsync()    {        try        {            if (_module is not null)            {                await _module.InvokeVoidAsync(DisposeMethod);                await _module.DisposeAsync();            }        }        catch (JSDisconnectedException) { }    }}

The component creates and uses the wrapper with no magic strings:

razor
@inject IJSRuntime JS@implements IAsyncDisposable
<canvas @ref="_canvasRef" width="600" height="400"></canvas>
@code {    private ElementReference _canvasRef;    private ChartInterop? _chart;
    protected override async Task OnAfterRenderAsync(bool firstRender)    {        if (firstRender)        {            _chart = new ChartInterop(JS);            await _chart.InitializeAsync(_canvasRef);        }    }
    async ValueTask IAsyncDisposable.DisposeAsync()    {        if (_chart is not null)            await _chart.DisposeAsync();    }}

Prefer a concrete class over interface + implementation for interop wrappers. For unit testing, substitute IJSRuntime directly (it is already an interface).

5. DotNetObjectReference for JS-to-.NET Callbacks

csharp
_dotNetRef = DotNetObjectReference.Create(this);await _module.InvokeVoidAsync("initialize", _dotNetRef);

On the JS side, wrap the dotNetRef in a class. Use async/await with try/catch (not .catch()) to guard against circuit loss. Define .NET method name constants at the top:

javascript
const ON_CLIPBOARD_CHANGED = 'OnClipboardChanged';
class ClipboardMonitor {    #dotNetRef;    #abortController;
    constructor(dotNetRef) {        this.#dotNetRef = dotNetRef;        this.#abortController = new AbortController();    }
    start() {        document.addEventListener('copy', async () => {            try {                const text = await navigator.clipboard.readText();                await this.#dotNetRef.invokeMethodAsync(ON_CLIPBOARD_CHANGED, text);            } catch { /* circuit disconnected or clipboard denied */ }        }, { signal: this.#abortController.signal });    }
    dispose() {        this.#abortController.abort();    }}
let monitor;export function initialize(dotNetRef) {    monitor = new ClipboardMonitor(dotNetRef);    monitor.start();}
export function dispose() {    monitor?.dispose();}

Rules:

  • [JSInvokable] methods must be public — private/internal silently fails at runtime
  • Wrap StateHasChanged in InvokeAsync inside [JSInvokable] callbacks:
    csharp
    [JSInvokable]public async Task OnClipboardChanged(string text){    await InvokeAsync(() => { _lastClipboard = text; StateHasChanged(); });}
  • Always try/catch around invokeMethodAsync in JS — circuit loss throws
  • Use const for .NET method name strings in JS — prevents typo bugs that silently fail
  • Dispose DotNetObjectReference in DisposeAsync

6. Disposal and Server Safety

Always implement IAsyncDisposable. Call JS cleanup first, then dispose references. Catch JSDisconnectedException for Blazor Server circuit loss:

csharp
public async ValueTask DisposeAsync(){    try    {        if (_module is not null)        {            await _module.InvokeVoidAsync("dispose");            await _module.DisposeAsync();        }    }    catch (JSDisconnectedException) { }
    _dotNetRef?.Dispose();}

Never use sync IDisposable for JS interop cleanup — InvokeVoidAsync returns ValueTask and must be awaited.

7. ElementReference

Pass DOM elements via @ref, not string IDs:

razor
<canvas @ref="_canvasRef" width="600" height="400"></canvas>
csharp
await _chart.InitializeAsync(_canvasRef);

Checklist

  • JS is in collocated .razor.js with export — no window.* globals
  • All interop in OnAfterRenderAsync or event handlers — never during prerender
  • IAsyncDisposable catches JSDisconnectedException
  • DotNetObjectReference disposed in DisposeAsync; JS side has try/catch around invokeMethodAsync
  • [JSInvokable] methods are public and use await InvokeAsync(StateHasChanged)
  • InvokeVoidAsync used when no return value is needed
  • ElementReference instead of string IDs
  • Related operations batched into single interop calls (both .NET→JS and JS→.NET)

Common Mistakes Checklist

MistakeFix
Using JS for something achievable with CSSUse CSS custom properties, data- attributes, pseudo-classes
Many fine-grained interop callsBatch into coarse functions — both .NET→JS and JS→.NET
Component imports JS module directlyEncapsulate in a strongly typed interop class
Magic strings for method names / module pathsDefine internal const fields in the interop class
Interface + implementation for interop wrapperUse a plain class; mock IJSRuntime for tests instead
JS calls in OnInitializedAsyncMove to OnAfterRenderAsync(firstRender)
InvokeAsync<object> for void callsUse InvokeVoidAsync
IDisposable with fire-and-forget JSUse IAsyncDisposable with await
Global window.* JS functionsUse collocated .razor.js with export
String element IDs passed to JSUse ElementReference with @ref
[JSInvokable] on private methodMust be public — silently fails otherwise
DotNetObjectReference not disposedDispose in DisposeAsync — causes memory leak
StateHasChanged() without InvokeAsyncWrap in await InvokeAsync(() => { StateHasChanged(); })
JS invokeMethodAsync without error handlingWrap in try/catch — circuit loss throws
Bare dotNetRef in JS event handlersWrap in a class with #dotNetRef private field
Magic strings in JS invokeMethodAsync callsUse const at module top — typos silently fail at runtime
JS calls in OnParametersSetAsyncTrack changes, apply in OnAfterRenderAsync with guard
No null check before calling moduleCheck module is not null before use

來源與署名

來源:dotnet/skills位於plugins/dotnet-blazor/skills/use-js-interop提交0608d89

授權條款: MIT

內容歸原作者所有。SourceWeft 從公開儲存庫中收錄這些內容。

檢舉或申請下架