ASP.NET Core Blazor의 .NET 메서드에서 JavaScript 함수 호출

참고 항목

이 문서의 최신 버전은 아닙니다. 현재 릴리스는 이 문서의 .NET 8 버전을 참조 하세요.

Important

이 정보는 상업적으로 출시되기 전에 실질적으로 수정될 수 있는 시험판 제품과 관련이 있습니다. Microsoft는 여기에 제공된 정보에 대해 어떠한 명시적, 또는 묵시적인 보증을 하지 않습니다.

현재 릴리스는 이 문서의 .NET 8 버전을 참조 하세요.

이 문서에서는 .NET에서 JavaScript(JS) 함수를 호출하는 방법을 설명합니다.

JS에서 .NET 메서드를 호출하는 방법에 대한 자세한 내용은 ASP.NET Core Blazor의 JavaScript 함수에서 .NET 메서드 호출을 참조하세요.

JS 함수 호출

IJSRuntime은 Blazor 프레임워크에 의해 등록됩니다. .NET에서 JS를 호출하려면 IJSRuntime 추상화를 삽입하고 다음 메서드 중 하나를 호출합니다.

JS 함수를 호출하는 이전 .NET 메서드의 경우:

  • 함수 식별자(String)는 전역 범위(window)를 기준으로 합니다. window.someScope.someFunction을 호출하려면 식별자가 someScope.someFunction입니다. 호출되기 전에 함수를 등록할 필요는 없습니다.
  • Object[]에 있는 임의의 수의 JSON 직렬화 가능 인수를 JS 함수에 전달합니다.
  • 취소 토큰(CancellationToken)은 작업을 취소해야 한다는 알림을 전파합니다.
  • TimeSpan은 JS 작업의 시간 제한을 나타냅니다.
  • TValue 반환 형식은 또한 JSON 직렬화가 가능해야 합니다. TValue는 반환되는 JSON 형식에 가장 잘 매핑되는 .NET 형식과 일치해야 합니다.
  • 메서드에 대해 InvokeAsync A JS Promise 가 반환됩니다. InvokeAsyncPromise의 래핑을 해제하고 Promise가 대기한 값을 반환합니다.

서버 쪽 앱의 기본값인 미리 렌더링을 사용하도록 설정된 앱의 경우 Blazor 미리 렌더링하는 동안 호출 JS 할 수 없습니다. 자세한 내용은 미리 렌더링 섹션을 참조하세요.

다음 예제는 JS 기반 디코더인 TextDecoder를 기준으로 합니다. 이 예제에서는 C# 메서드에서 개발자 코드의 요구 사항을 기존 JS API로 오프로드하는 JS 함수를 호출하는 방법을 보여 줍니다. JS 함수는 C# 메서드에서 바이트 배열을 수신하고 배열을 디코딩하고 표시를 위해 구성 요소에 텍스트를 반환합니다.

<script>
  window.convertArray = (win1251Array) => {
    var win1251decoder = new TextDecoder('windows-1251');
    var bytes = new Uint8Array(win1251Array);
    var decodedArray = win1251decoder.decode(bytes);
    return decodedArray;
  };
</script>

참고 항목

위치 및 프로덕션 앱에 JS 대한 권장 사항에 대한 일반적인 지침은 ASP.NET Core Blazor 앱의 JavaScript 위치를 참조하세요.

다음 구성 요소는

  • 단추(Convert Array)를 선택할 때 InvokeAsync를 사용하여 convertArrayJS 함수를 호출합니다.
  • JS 함수를 호출한 후에는 전달된 배열이 문자열로 변환됩니다. 이 문자열은 표시를 위해 구성 요소로 반환됩니다(text).

CallJs1.razor:

@page "/call-js-1"
@inject IJSRuntime JS

<PageTitle>Call JS 1</PageTitle>

<h1>Call JS Example 1</h1>

<p>
    <button @onclick="ConvertArray">Convert Array</button>
</p>

<p>
    @text
</p>

<p>
    Quote ©2005 <a href="https://www.uphe.com">Universal Pictures</a>: 
    <a href="https://www.uphe.com/movies/serenity-2005">Serenity</a><br>
    <a href="https://www.imdb.com/name/nm0472710/">David Krumholtz on IMDB</a>
</p>

@code {
    private MarkupString text;

    private uint[] quoteArray = 
        new uint[]
        {
            60, 101, 109, 62, 67, 97, 110, 39, 116, 32, 115, 116, 111, 112, 32,
            116, 104, 101, 32, 115, 105, 103, 110, 97, 108, 44, 32, 77, 97,
            108, 46, 60, 47, 101, 109, 62, 32, 45, 32, 77, 114, 46, 32, 85, 110,
            105, 118, 101, 114, 115, 101, 10, 10,
        };

    private async Task ConvertArray()
    {
        text = new(await JS.InvokeAsync<string>("convertArray", quoteArray));
    }
}

CallJsExample1.razor:

@page "/call-js-example-1"
@inject IJSRuntime JS

<h1>Call JS <code>convertArray</code> Function</h1>

<p>
    <button @onclick="ConvertArray">Convert Array</button>
</p>

<p>
    @text
</p>

<p>
    Quote ©2005 <a href="https://www.uphe.com">Universal Pictures</a>: 
    <a href="https://www.uphe.com/movies/serenity-2005">Serenity</a><br>
    <a href="https://www.imdb.com/name/nm0472710/">David Krumholtz on IMDB</a>
</p>

@code {
    private MarkupString text;

    private uint[] quoteArray = 
        new uint[]
        {
            60, 101, 109, 62, 67, 97, 110, 39, 116, 32, 115, 116, 111, 112, 32,
            116, 104, 101, 32, 115, 105, 103, 110, 97, 108, 44, 32, 77, 97,
            108, 46, 60, 47, 101, 109, 62, 32, 45, 32, 77, 114, 46, 32, 85, 110,
            105, 118, 101, 114, 115, 101, 10, 10,
        };

    private async Task ConvertArray()
    {
        text = new(await JS.InvokeAsync<string>("convertArray", quoteArray));
    }
}

CallJsExample1.razor:

@page "/call-js-example-1"
@inject IJSRuntime JS

<h1>Call JS <code>convertArray</code> Function</h1>

<p>
    <button @onclick="ConvertArray">Convert Array</button>
</p>

<p>
    @text
</p>

<p>
    Quote ©2005 <a href="https://www.uphe.com">Universal Pictures</a>: 
    <a href="https://www.uphe.com/movies/serenity-2005">Serenity</a><br>
    <a href="https://www.imdb.com/name/nm0472710/">David Krumholtz on IMDB</a>
</p>

@code {
    private MarkupString text;

    private uint[] quoteArray = 
        new uint[]
        {
            60, 101, 109, 62, 67, 97, 110, 39, 116, 32, 115, 116, 111, 112, 32,
            116, 104, 101, 32, 115, 105, 103, 110, 97, 108, 44, 32, 77, 97,
            108, 46, 60, 47, 101, 109, 62, 32, 45, 32, 77, 114, 46, 32, 85, 110,
            105, 118, 101, 114, 115, 101, 10, 10,
        };

    private async Task ConvertArray()
    {
        text = new(await JS.InvokeAsync<string>("convertArray", quoteArray));
    }
}

CallJsExample1.razor:

@page "/call-js-example-1"
@inject IJSRuntime JS

<h1>Call JS <code>convertArray</code> Function</h1>

<p>
    <button @onclick="ConvertArray">Convert Array</button>
</p>

<p>
    @text
</p>

<p>
    Quote ©2005 <a href="https://www.uphe.com">Universal Pictures</a>: 
    <a href="https://www.uphe.com/movies/serenity-2005">Serenity</a><br>
    <a href="https://www.imdb.com/name/nm0472710/">David Krumholtz on IMDB</a>
</p>

@code {
    private MarkupString text;

    private uint[] quoteArray = 
        new uint[]
        {
            60, 101, 109, 62, 67, 97, 110, 39, 116, 32, 115, 116, 111, 112, 32,
            116, 104, 101, 32, 115, 105, 103, 110, 97, 108, 44, 32, 77, 97,
            108, 46, 60, 47, 101, 109, 62, 32, 45, 32, 77, 114, 46, 32, 85, 110,
            105, 118, 101, 114, 115, 101, 10, 10,
        };

    private async Task ConvertArray()
    {
        text = new(await JS.InvokeAsync<string>("convertArray", quoteArray));
    }
}

CallJsExample1.razor:

@page "/call-js-example-1"
@inject IJSRuntime JS

<h1>Call JS <code>convertArray</code> Function</h1>

<p>
    <button @onclick="ConvertArray">Convert Array</button>
</p>

<p>
    @text
</p>

<p>
    Quote ©2005 <a href="https://www.uphe.com">Universal Pictures</a>: 
    <a href="https://www.uphe.com/movies/serenity-2005">Serenity</a><br>
    <a href="https://www.imdb.com/name/nm0472710/">David Krumholtz on IMDB</a>
</p>

@code {
    private MarkupString text;

    private uint[] quoteArray = 
        new uint[]
        {
            60, 101, 109, 62, 67, 97, 110, 39, 116, 32, 115, 116, 111, 112, 32,
            116, 104, 101, 32, 115, 105, 103, 110, 97, 108, 44, 32, 77, 97,
            108, 46, 60, 47, 101, 109, 62, 32, 45, 32, 77, 114, 46, 32, 85, 110,
            105, 118, 101, 114, 115, 101, 10, 10,
        };

    private async Task ConvertArray()
    {
        text = new MarkupString(await JS.InvokeAsync<string>("convertArray", 
            quoteArray));
    }
}

사용자 제스처로 제한된 JavaScript API

이 섹션은 서버 쪽 구성 요소에 적용됩니다.

일부 브라우저 JavaScript(JS) API는 Fullscreen API(MDN 문서) 사용과 같은 사용자 제스처의 컨텍스트에서만 실행할 수 있습니다. 이러한 API는 UI 이벤트 처리가 비동기적으로 수행되고 일반적으로 사용자 제스처의 컨텍스트에서 더 이상 수행되지 않으므로 서버 쪽 구성 요소의 interop 메커니즘을 통해 JS 호출할 수 없습니다. 앱은 JavaScript에서 UI 이벤트를 완전히 처리해야 하므로 Blazor의 @onclick 지시문 특성 대신 onclick을 사용합니다.

반환된 값(InvokeVoidAsync)을 읽지 않고 JavaScript 함수를 호출

다음과 같은 경우 InvokeVoidAsync를 사용합니다.

  • .NET가 JS(JavaScript) 호출 결과를 읽을 필요가 없는 경우.
  • JS 함수가 void(0)/void 0 또는 undefined를 반환하는 경우

displayTickerAlert1JS 함수를 제공합니다. 이 함수는 InvokeVoidAsync를 사용하여 호출되며 값을 반환하지 않습니다.

<script>
  window.displayTickerAlert1 = (symbol, price) => {
    alert(`${symbol}: $${price}!`);
  };
</script>

참고 항목

위치 및 프로덕션 앱에 JS 대한 권장 사항에 대한 일반적인 지침은 ASP.NET Core Blazor 앱의 JavaScript 위치를 참조하세요.

구성 요소(.razor) 예제(InvokeVoidAsync)

TickerChanged 는 다음 구성 요소의 handleTickerChanged1 메서드를 호출합니다.

CallJs2.razor:

@page "/call-js-2"
@inject IJSRuntime JS

<PageTitle>Call JS 2</PageTitle>

<h1>Call JS Example 2</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
    private string? stockSymbol;
    private decimal price;

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + Random.Shared.Next(0, 26))}{(char)('A' + Random.Shared.Next(0, 26))}";
        price = Random.Shared.Next(1, 101);
        await JS.InvokeVoidAsync("displayTickerAlert1", stockSymbol, price);
    }
}

CallJsExample2.razor:

@page "/call-js-example-2"
@inject IJSRuntime JS

<h1>Call JS Example 2</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
    private string? stockSymbol;
    private decimal price;

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + Random.Shared.Next(0, 26))}{(char)('A' + Random.Shared.Next(0, 26))}";
        price = Random.Shared.Next(1, 101);
        await JS.InvokeVoidAsync("displayTickerAlert1", stockSymbol, price);
    }
}

CallJsExample2.razor:

@page "/call-js-example-2"
@inject IJSRuntime JS

<h1>Call JS Example 2</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
    private string? stockSymbol;
    private decimal price;

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + Random.Shared.Next(0, 26))}{(char)('A' + Random.Shared.Next(0, 26))}";
        price = Random.Shared.Next(1, 101);
        await JS.InvokeVoidAsync("displayTickerAlert1", stockSymbol, price);
    }
}

CallJsExample2.razor:

@page "/call-js-example-2"
@inject IJSRuntime JS

<h1>Call JS Example 2</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
    private Random r = new();
    private string stockSymbol;
    private decimal price;

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        await JS.InvokeVoidAsync("displayTickerAlert1", stockSymbol, price);
    }
}

CallJsExample2.razor:

@page "/call-js-example-2"
@inject IJSRuntime JS

<h1>Call JS Example 2</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol != null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
    private Random r = new Random();
    private string stockSymbol;
    private decimal price;

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        await JS.InvokeVoidAsync("displayTickerAlert1", stockSymbol, price);
    }
}

클래스(.cs) 예제(InvokeVoidAsync)

JsInteropClasses1.cs:

using Microsoft.JSInterop;

namespace BlazorSample;

public class JsInteropClasses1(IJSRuntime js) : IDisposable
{
    private readonly IJSRuntime js = js;

    public async ValueTask TickerChanged(string symbol, decimal price)
    {
        await js.InvokeVoidAsync("displayTickerAlert1", symbol, price);
    }

    public void Dispose()
    {
        // The following prevents derived types that introduce a
        // finalizer from needing to re-implement IDisposable.
        GC.SuppressFinalize(this);
    }
}
using Microsoft.JSInterop;

public class JsInteropClasses1 : IDisposable
{
    private readonly IJSRuntime js;

    public JsInteropClasses1(IJSRuntime js)
    {
        this.js = js;
    }

    public async ValueTask TickerChanged(string symbol, decimal price)
    {
        await js.InvokeVoidAsync("displayTickerAlert1", symbol, price);
    }

    public void Dispose()
    {
    }
}
using Microsoft.JSInterop;

public class JsInteropClasses1 : IDisposable
{
    private readonly IJSRuntime js;

    public JsInteropClasses1(IJSRuntime js)
    {
        this.js = js;
    }

    public async ValueTask TickerChanged(string symbol, decimal price)
    {
        await js.InvokeVoidAsync("displayTickerAlert1", symbol, price);
    }

    public void Dispose()
    {
    }
}
using System;
using System.Threading.Tasks;
using Microsoft.JSInterop;

public class JsInteropClasses1 : IDisposable
{
    private readonly IJSRuntime js;

    public JsInteropClasses1(IJSRuntime js)
    {
        this.js = js;
    }

    public async ValueTask TickerChanged(string symbol, decimal price)
    {
        await js.InvokeVoidAsync("displayTickerAlert1", symbol, price);
    }

    public void Dispose()
    {
    }
}
using System;
using System.Threading.Tasks;
using Microsoft.JSInterop;

public class JsInteropClasses1 : IDisposable
{
    private readonly IJSRuntime js;

    public JsInteropClasses1(IJSRuntime js)
    {
        this.js = js;
    }

    public async ValueTask TickerChanged(string symbol, decimal price)
    {
        await js.InvokeVoidAsync("displayTickerAlert1", symbol, price);
    }

    public void Dispose()
    {
    }
}

TickerChanged 는 다음 구성 요소의 handleTickerChanged1 메서드를 호출합니다.

CallJs3.razor:

@page "/call-js-3"
@implements IDisposable
@inject IJSRuntime JS

<PageTitle>Call JS 3</PageTitle>

<h1>Call JS Example 3</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
    private string? stockSymbol;
    private decimal price;
    private JsInteropClasses1? jsClass;

    protected override void OnInitialized()
    {
        jsClass = new(JS);
    }

    private async Task SetStock()
    {
        if (jsClass is not null)
        {
            stockSymbol = 
                $"{(char)('A' + Random.Shared.Next(0, 26))}{(char)('A' + Random.Shared.Next(0, 26))}";
            price = Random.Shared.Next(1, 101);
            await jsClass.TickerChanged(stockSymbol, price);
        }
    }

    public void Dispose() => jsClass?.Dispose();
}

CallJsExample3.razor:

@page "/call-js-example-3"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call JS Example 3</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
    private string? stockSymbol;
    private decimal price;
    private JsInteropClasses1? jsClass;

    protected override void OnInitialized()
    {
        jsClass = new(JS);
    }

    private async Task SetStock()
    {
        if (jsClass is not null)
        {
            stockSymbol = 
                $"{(char)('A' + Random.Shared.Next(0, 26))}{(char)('A' + Random.Shared.Next(0, 26))}";
            price = Random.Shared.Next(1, 101);
            await jsClass.TickerChanged(stockSymbol, price);
        }
    }

    public void Dispose() => jsClass?.Dispose();
}

CallJsExample3.razor:

@page "/call-js-example-3"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call JS Example 3</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
    private string? stockSymbol;
    private decimal price;
    private JsInteropClasses1? jsClass;

    protected override void OnInitialized()
    {
        jsClass = new(JS);
    }

    private async Task SetStock()
    {
        if (jsClass is not null)
        {
            stockSymbol = 
                $"{(char)('A' + Random.Shared.Next(0, 26))}{(char)('A' + Random.Shared.Next(0, 26))}";
            price = Random.Shared.Next(1, 101);
            await jsClass.TickerChanged(stockSymbol, price);
        }
    }

    public void Dispose() => jsClass?.Dispose();
}

CallJsExample3.razor:

@page "/call-js-example-3"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call JS Example 3</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
    private Random r = new();
    private string stockSymbol;
    private decimal price;
    private JsInteropClasses1 jsClass;

    protected override void OnInitialized()
    {
        jsClass = new(JS);
    }

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        await jsClass.TickerChanged(stockSymbol, price);
    }

    public void Dispose() => jsClass?.Dispose();
}

CallJsExample3.razor:

@page "/call-js-example-3"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call JS Example 3</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol != null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@code {
    private Random r = new Random();
    private string stockSymbol;
    private decimal price;
    private JsInteropClasses1 jsClass;

    protected override void OnInitialized()
    {
        jsClass = new JsInteropClasses1(JS);
    }

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        await jsClass.TickerChanged(stockSymbol, price);
    }

    public void Dispose() => jsClass?.Dispose();
}

JavaScript 함수를 호출하고 반환된 값(InvokeAsync) 읽기

.NET에서 JS(JavaScript) 호출 결과를 읽어야 하는 경우 InvokeAsync를 사용합니다.

displayTickerAlert2JS 함수를 제공합니다. 다음 예제에서는 호출자가 표시할 문자열을 반환합니다.

<script>
  window.displayTickerAlert2 = (symbol, price) => {
    if (price < 20) {
      alert(`${symbol}: $${price}!`);
      return "User alerted in the browser.";
    } else {
      return "User NOT alerted.";
    }
  };
</script>

참고 항목

위치 및 프로덕션 앱에 JS 대한 권장 사항에 대한 일반적인 지침은 ASP.NET Core Blazor 앱의 JavaScript 위치를 참조하세요.

구성 요소(.razor) 예제(InvokeAsync)

TickerChanged 는 메서드를 handleTickerChanged2 호출하고 반환된 문자열을 다음 구성 요소에 표시합니다.

CallJs4.razor:

@page "/call-js-4"
@inject IJSRuntime JS

<PageTitle>Call JS 4</PageTitle>

<h1>Call JS Example 4</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@if (result is not null)
{
    <p>@result</p>
}

@code {
    private string? stockSymbol;
    private decimal price;
    private string? result;

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + Random.Shared.Next(0, 26))}{(char)('A' + Random.Shared.Next(0, 26))}";
        price = Random.Shared.Next(1, 101);
        var interopResult = 
            await JS.InvokeAsync<string>("displayTickerAlert2", stockSymbol, price);
        result = $"Result of TickerChanged call for {stockSymbol} at " +
            $"{price.ToString("c")}: {interopResult}";
    }
}

CallJsExample4.razor:

@page "/call-js-example-4"
@inject IJSRuntime JS

<h1>Call JS Example 4</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@if (result is not null)
{
    <p>@result</p>
}

@code {
    private string? stockSymbol;
    private decimal price;
    private string? result;

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + Random.Shared.Next(0, 26))}{(char)('A' + Random.Shared.Next(0, 26))}";
        price = Random.Shared.Next(1, 101);
        var interopResult = 
            await JS.InvokeAsync<string>("displayTickerAlert2", stockSymbol, price);
        result = $"Result of TickerChanged call for {stockSymbol} at " +
            $"{price.ToString("c")}: {interopResult}";
    }
}

CallJsExample4.razor:

@page "/call-js-example-4"
@inject IJSRuntime JS

<h1>Call JS Example 4</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@if (result is not null)
{
    <p>@result</p>
}

@code {
    private string? stockSymbol;
    private decimal price;
    private string? result;

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + Random.Shared.Next(0, 26))}{(char)('A' + Random.Shared.Next(0, 26))}";
        price = Random.Shared.Next(1, 101);
        var interopResult = 
            await JS.InvokeAsync<string>("displayTickerAlert2", stockSymbol, price);
        result = $"Result of TickerChanged call for {stockSymbol} at " +
            $"{price.ToString("c")}: {interopResult}";
    }
}

CallJsExample4.razor:

@page "/call-js-example-4"
@inject IJSRuntime JS

<h1>Call JS Example 4</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@if (result is not null)
{
    <p>@result</p>
}

@code {
    private Random r = new();
    private string stockSymbol;
    private decimal price;
    private string result;

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        var interopResult = 
            await JS.InvokeAsync<string>("displayTickerAlert2", stockSymbol, price);
        result = $"Result of TickerChanged call for {stockSymbol} at " +
            $"{price.ToString("c")}: {interopResult}";
    }
}

CallJsExample4.razor:

@page "/call-js-example-4"
@inject IJSRuntime JS

<h1>Call JS Example 4</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol != null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@if (result != null)
{
    <p>@result</p>
}

@code {
    private Random r = new Random();
    private string stockSymbol;
    private decimal price;
    private string result;

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        var interopResult = 
            await JS.InvokeAsync<string>("displayTickerAlert2", stockSymbol, price);
        result = $"Result of TickerChanged call for {stockSymbol} at " +
            $"{price.ToString("c")}: {interopResult}";
    }
}

클래스(.cs) 예제(InvokeAsync)

JsInteropClasses2.cs:

using Microsoft.JSInterop;

namespace BlazorSample;

public class JsInteropClasses2(IJSRuntime js) : IDisposable
{
    private readonly IJSRuntime js = js;

    public async ValueTask<string> TickerChanged(string symbol, decimal price)
    {
        return await js.InvokeAsync<string>("displayTickerAlert2", symbol, price);
    }

    public void Dispose()
    {
        // The following prevents derived types that introduce a
        // finalizer from needing to re-implement IDisposable.
        GC.SuppressFinalize(this);
    }
}
using Microsoft.JSInterop;

public class JsInteropClasses2 : IDisposable
{
    private readonly IJSRuntime js;

    public JsInteropClasses2(IJSRuntime js)
    {
        this.js = js;
    }

    public async ValueTask<string> TickerChanged(string symbol, decimal price)
    {
        return await js.InvokeAsync<string>("displayTickerAlert2", symbol, price);
    }

    public void Dispose()
    {
    }
}
using Microsoft.JSInterop;

public class JsInteropClasses2 : IDisposable
{
    private readonly IJSRuntime js;

    public JsInteropClasses2(IJSRuntime js)
    {
        this.js = js;
    }

    public async ValueTask<string> TickerChanged(string symbol, decimal price)
    {
        return await js.InvokeAsync<string>("displayTickerAlert2", symbol, price);
    }

    public void Dispose()
    {
    }
}
using System;
using System.Threading.Tasks;
using Microsoft.JSInterop;

public class JsInteropClasses2 : IDisposable
{
    private readonly IJSRuntime js;

    public JsInteropClasses2(IJSRuntime js)
    {
        this.js = js;
    }

    public async ValueTask<string> TickerChanged(string symbol, decimal price)
    {
        return await js.InvokeAsync<string>("displayTickerAlert2", symbol, price);
    }

    public void Dispose()
    {
    }
}
using System;
using System.Threading.Tasks;
using Microsoft.JSInterop;

public class JsInteropClasses2 : IDisposable
{
    private readonly IJSRuntime js;

    public JsInteropClasses2(IJSRuntime js)
    {
        this.js = js;
    }

    public async ValueTask<string> TickerChanged(string symbol, decimal price)
    {
        return await js.InvokeAsync<string>("displayTickerAlert2", symbol, price);
    }

    public void Dispose()
    {
    }
}

TickerChanged 는 메서드를 handleTickerChanged2 호출하고 반환된 문자열을 다음 구성 요소에 표시합니다.

CallJs5.razor:

@page "/call-js-5"
@implements IDisposable
@inject IJSRuntime JS

<PageTitle>Call JS 5</PageTitle>

<h1>Call JS Example 5</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@if (result is not null)
{
    <p>@result</p>
}

@code {
    private string? stockSymbol;
    private decimal price;
    private JsInteropClasses2? jsClass;
    private string? result;

    protected override void OnInitialized()
    {
        jsClass = new(JS);
    }

    private async Task SetStock()
    {
        if (jsClass is not null)
        {
            stockSymbol = 
                $"{(char)('A' + Random.Shared.Next(0, 26))}{(char)('A' + Random.Shared.Next(0, 26))}";
            price = Random.Shared.Next(1, 101);
            var interopResult = await jsClass.TickerChanged(stockSymbol, price);
            result = $"Result of TickerChanged call for {stockSymbol} at " +
                $"{price.ToString("c")}: {interopResult}";
        }
    }

    public void Dispose() => jsClass?.Dispose();
}

CallJsExample5.razor:

@page "/call-js-example-5"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call JS Example 5</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@if (result is not null)
{
    <p>@result</p>
}

@code {
    private string? stockSymbol;
    private decimal price;
    private JsInteropClasses2? jsClass;
    private string? result;

    protected override void OnInitialized()
    {
        jsClass = new(JS);
    }

    private async Task SetStock()
    {
        if (jsClass is not null)
        {
            stockSymbol = 
                $"{(char)('A' + Random.Shared.Next(0, 26))}{(char)('A' + Random.Shared.Next(0, 26))}";
            price = Random.Shared.Next(1, 101);
            var interopResult = await jsClass.TickerChanged(stockSymbol, price);
            result = $"Result of TickerChanged call for {stockSymbol} at " +
                $"{price.ToString("c")}: {interopResult}";
        }
    }

    public void Dispose() => jsClass?.Dispose();
}

CallJsExample5.razor:

@page "/call-js-example-5"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call JS Example 5</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@if (result is not null)
{
    <p>@result</p>
}

@code {
    private string? stockSymbol;
    private decimal price;
    private JsInteropClasses2? jsClass;
    private string? result;

    protected override void OnInitialized()
    {
        jsClass = new(JS);
    }

    private async Task SetStock()
    {
        if (jsClass is not null)
        {
            stockSymbol = 
                $"{(char)('A' + Random.Shared.Next(0, 26))}{(char)('A' + Random.Shared.Next(0, 26))}";
            price = Random.Shared.Next(1, 101);
            var interopResult = await jsClass.TickerChanged(stockSymbol, price);
            result = $"Result of TickerChanged call for {stockSymbol} at " +
                $"{price.ToString("c")}: {interopResult}";
        }
    }

    public void Dispose() => jsClass?.Dispose();
}

CallJsExample5.razor:

@page "/call-js-example-5"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call JS Example 5</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol is not null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@if (result is not null)
{
    <p>@result</p>
}

@code {
    private Random r = new();
    private string stockSymbol;
    private decimal price;
    private JsInteropClasses2 jsClass;
    private string result;

    protected override void OnInitialized()
    {
        jsClass = new(JS);
    }

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        var interopResult = await jsClass.TickerChanged(stockSymbol, price);
        result = $"Result of TickerChanged call for {stockSymbol} at " +
            $"{price.ToString("c")}: {interopResult}";
    }

    public void Dispose() => jsClass?.Dispose();
}

CallJsExample5.razor:

@page "/call-js-example-5"
@implements IDisposable
@inject IJSRuntime JS

<h1>Call JS Example 5</h1>

<p>
    <button @onclick="SetStock">Set Stock</button>
</p>

@if (stockSymbol != null)
{
    <p>@stockSymbol price: @price.ToString("c")</p>
}

@if (result != null)
{
    <p>@result</p>
}

@code {
    private Random r = new Random();
    private string stockSymbol;
    private decimal price;
    private JsInteropClasses2 jsClass;
    private string result;

    protected override void OnInitialized()
    {
        jsClass = new JsInteropClasses2(JS);
    }

    private async Task SetStock()
    {
        stockSymbol = 
            $"{(char)('A' + r.Next(0, 26))}{(char)('A' + r.Next(0, 26))}";
        price = r.Next(1, 101);
        var interopResult = await jsClass.TickerChanged(stockSymbol, price);
        result = $"Result of TickerChanged call for {stockSymbol} at " +
            $"{price.ToString("c")}: {interopResult}";
    }

    public void Dispose() => jsClass?.Dispose();
}

동적 콘텐츠 생성 시나리오

BuildRenderTree를 사용하여 동적 콘텐츠를 생성하려면 [Inject] 특성을 사용합니다.

[Inject]
IJSRuntime JS { get; set; }

미리 렌더링

이 섹션은 구성 요소를 미리 렌더링 Razor 하는 서버 쪽 앱에 적용됩니다. 사전 렌더링은 Prerender ASP.NET Core Razor 구성 요소에서 다룹니다.

참고 항목

Web Apps에서 대화형 라우팅을 Blazor 위한 내부 탐색에는 서버에서 새 페이지 콘텐츠를 요청하는 작업이 포함되지 않습니다. 따라서 내부 페이지 요청에 대해 미리 렌더링이 발생하지 않습니다. 앱이 대화형 라우팅을 채택하는 경우 사전 렌더링 동작을 보여 주는 구성 요소 예제에 대해 전체 페이지 다시 로드를 수행합니다. 자세한 내용은 Prerender ASP.NET Core Razor 구성 요소를 참조하세요.

이 섹션은 구성 요소를 미리 렌더링 Razor 하는 서버 쪽 앱 및 호스트 Blazor WebAssembly 된 앱에 적용됩니다. 미리 렌더링은 ASP.NET Core Razor 구성 요소 미리 렌더링 및 통합에서 다룹니다.

앱이 미리 렌더링하는 동안에는 JavaScript(JS) 호출 같은 특정 작업은 불가능합니다.

다음 예제에서는 함수가 setElementText1 호출 JSRuntimeExtensions.InvokeVoidAsync 되고 값을 반환하지 않습니다.

참고 항목

위치 및 프로덕션 앱에 JS 대한 권장 사항에 대한 일반적인 지침은 ASP.NET Core Blazor 앱의 JavaScript 위치를 참조하세요.

<script>
  window.setElementText1 = (element, text) => element.innerText = text;
</script>

Warning

앞의 예제에서는 데모용으로만 DOM을 직접 수정합니다. JS를 사용하여 DOM을 직접 수정할 경우 JS가 Blazor의 변경 내용 추적을 방해할 수 있으므로 대부분의 시나리오에서 권장되지 않습니다. 자세한 내용은 ASP.NET Core Blazor JavaScript 상호 운용성(JS)을 참조하세요.

OnAfterRender{Async} 수명 주기 이벤트는 서버에서 미리 렌더링 프로세스가 진행 중일 때는 호출되지 않습니다 구성 요소가 렌더링되고 미리 렌더링된 후 클라이언트에서 대화형이 될 때까지 JS interop 호출을 지연하도록 OnAfterRender{Async} 메서드를 재정의합니다.

PrerenderedInterop1.razor:

@page "/prerendered-interop-1"
@using Microsoft.JSInterop
@inject IJSRuntime JS

<PageTitle>Prerendered Interop 1</PageTitle>

<h1>Prerendered Interop Example 1</h1>

<div @ref="divElement">Text during render</div>

@code {
    private ElementReference divElement;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            await JS.InvokeVoidAsync(
                "setElementText1", divElement, "Text after render");
        }
    }
}

참고 항목

앞의 예제에서는 전역 함수를 사용하여 클라이언트를 오염합니다. 프로덕션 앱에서의 더 나은 접근 방식은 JavaScript 모듈에서의 JavaScript 격리를 참조하세요.

예시:

export setElementText1 = (element, text) => element.innerText = text;

다음 구성 요소는 미리 렌더링과 호환되는 방식으로 구성 요소의 초기화 논리의 일부로서 JS interop를 사용하는 방법을 보여 줍니다. 이 구성 요소는 OnAfterRenderAsync 내부에서 렌더링 업데이트를 트리거하는 것이 가능함을 보여 줍니다. 개발자는 이 시나리오에서 무한 루프를 만들지 않도록 주의해야 합니다.

다음 예제에서는 함수를 setElementText2 호출 IJSRuntime.InvokeAsync 하고 값을 반환합니다.

참고 항목

위치 및 프로덕션 앱에 JS 대한 권장 사항에 대한 일반적인 지침은 ASP.NET Core Blazor 앱의 JavaScript 위치를 참조하세요.

<script>
  window.setElementText2 = (element, text) => {
    element.innerText = text;
    return text;
  };
</script>

Warning

앞의 예제에서는 데모용으로만 DOM을 직접 수정합니다. JS를 사용하여 DOM을 직접 수정할 경우 JS가 Blazor의 변경 내용 추적을 방해할 수 있으므로 대부분의 시나리오에서 권장되지 않습니다. 자세한 내용은 ASP.NET Core Blazor JavaScript 상호 운용성(JS)을 참조하세요.

호출 ElementReference 되는 위치 JSRuntime.InvokeAsync 는 구성 요소가 렌더링될 때까지 HTML DOM 요소가 없기 때문에 이전의 수명 주기 메서드에서만 사용 OnAfterRenderAsync 되며 사용되지 않습니다.

JS interop 호출에서 얻은 새 상태로 구성 요소를 다시 렌더링하도록 StateHasChanged가 호출됩니다(자세한 내용은 ASP.NET Core Razor 구성 요소 렌더링 참조). StateHasChangeddatanull인 경우에만 호출되기 때문에 이 코드는 무한 루프를 만들지 않습니다.

PrerenderedInterop2.razor:

@page "/prerendered-interop-2"
@using Microsoft.AspNetCore.Components
@using Microsoft.JSInterop
@inject IJSRuntime JS

<PageTitle>Prerendered Interop 2</PageTitle>

<h1>Prerendered Interop Example 2</h1>

<p>
    Get value via JS interop call:
    <strong id="val-get-by-interop">@(infoFromJs ?? "No value yet")</strong>
</p>

<p>
    Set value via JS interop call: 
    <strong id="val-set-by-interop" @ref="divElement"></strong>
</p>



@code {
    private string? infoFromJs;
    private ElementReference divElement;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender && infoFromJs == null)
        {
            infoFromJs = await JS.InvokeAsync<string>(
                "setElementText2", divElement, "Hello from interop call!");

            StateHasChanged();
        }
    }
}

참고 항목

앞의 예제에서는 전역 함수를 사용하여 클라이언트를 오염합니다. 프로덕션 앱에서의 더 나은 접근 방식은 JavaScript 모듈에서의 JavaScript 격리를 참조하세요.

예시:

export setElementText2 = (element, text) => {
  element.innerText = text;
  return text;
};

클라이언트 쪽 구성 요소의 동기 JS interop

이 섹션은 클라이언트 쪽 구성 요소에만 적용됩니다.

JS interop 호출은 호출된 코드가 동기인지 비동기인지와 관계없이 기본적으로 비동기적입니다. 기본적으로 호출은 비동기적이므로 구성 요소가 서버 쪽 및 클라이언트 쪽 렌더링 모드에서 호환되도록 합니다. 서버에서 모든 JS interop 호출은 네트워크 연결을 통해 전송되므로 비동기적이어야 합니다.

구성 요소가 WebAssembly에서만 실행된다는 것을 확실히 알고 있는 경우 동기 JS interop 호출을 하도록 선택할 수 있습니다. 이렇게 하면 비동기 호출을 수행하는 것보다 오버헤드가 약간 감소하며 결과를 기다리는 동안 중간 상태가 없기 때문에 렌더링 주기가 감소할 수 있습니다.

클라이언트 쪽 구성 요소에서 .NET에서 JavaScript로 동기 호출을 수행하려면 interop 호출을 수행 JS 하도록 IJSInProcessRuntime 캐스팅 IJSRuntime 합니다.

@inject IJSRuntime JS

...

@code {
    protected override void HandleSomeEvent()
    {
        var jsInProcess = (IJSInProcessRuntime)JS;
        var value = jsInProcess.Invoke<string>("javascriptFunctionIdentifier");
    }
}

ASP.NET Core 5.0 이상 클라이언트 쪽 구성 요소에서 작업 IJSObjectReference 하는 경우 대신 동기적으로 사용할 IJSInProcessObjectReference 수 있습니다. IJSInProcessObjectReferenceIAsyncDisposable/IDisposable 는 다음 예제와 같이 메모리 누수 방지를 위해 가비지 수집을 구현하고 삭제해야 합니다.

@inject IJSRuntime JS
@implements IAsyncDisposable

...

@code {
    ...
    private IJSInProcessObjectReference? module;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSInProcessObjectReference>("import", 
            "./scripts.js");
        }
    }

    ...

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            await module.DisposeAsync();
        }
    }
}

JavaScript 위치

JavaScript 위치에 대한 문서에서 설명하는 방법을 사용하여 JavaScript(JS) 코드를 로드합니다.

JS 모듈에서 스크립트를 분리하는 방법에 대한 내용은 Javascript 모듈의 Javascript 격리 섹션을 참조하세요.

Warning

태그를 <script> 동적으로 업데이트할 수 없으므로 구성 요소가 정적 서버 쪽 렌더링(정적 SSR)을 채택하도록 보장되는 경우에만 구성 요소 파일.razor()에 태그를 <script> 배치합니다.

Warning

<script> 태그를 동적으로 업데이트할 수 없으므로 구성 요소 파일(.razor)에 <script> 태그를 넣지 마세요.

JavaScript 모듈에서의 JavaScript 격리

Blazor에서는 표준 JavaScript 모듈(ECMAScript 사양)에서 JavaScript(JS) 격리를 사용하도록 설정합니다. JavaScript 모듈 로드는 다른 유형의 웹앱과 Blazor 동일한 방식으로 작동하며, 앱에서 모듈을 정의하는 방법을 자유롭게 사용자 지정할 수 있습니다. JavaScript 모듈을 사용하는 방법에 대한 가이드는 MDN 웹 문서: JavaScript 모듈을 참조 하세요.

JS 격리는 다음과 같은 이점을 제공합니다.

  • 가져온 JS는 전역 네임스페이스를 더 이상 오염시키지 않습니다.
  • 라이브러리 및 구성 요소의 소비자는 관련 JS를 가져올 필요가 없습니다.

연산자를 사용하여 import() 동적 가져오기는 ASP.NET Core 및 Blazor다음에서 지원됩니다.

if ({CONDITION}) import("/additionalModule.js");

앞의 예제 {CONDITION} 에서 자리 표시자는 모듈을 로드해야 하는지 여부를 결정하는 조건부 검사 나타냅니다.

브라우저 호환성을 보려면 다음을 사용할 수 있습니다. JavaScript 모듈: 동적 가져오기를 참조하세요.

예를 들어 다음 JS 모듈은 브라우저 창 프롬프트를 표시하기 위해 JS 함수를 내보냅니다. 외부 JS 파일에 다음 JS 코드를 배치합니다.

wwwroot/scripts.js:

export function showPrompt(message) {
  return prompt(message, 'Type anything here');
}

앞의 JS 모듈을 wwwroot 폴더의 정적 웹 자산으로 앱 또는 클래스 라이브러리에 추가한 다음, IJSRuntime 인스턴스에서 InvokeAsync를 호출하여 .NET 코드로 모듈을 가져옵니다.

IJSRuntime은 모듈을 IJSObjectReference로서 가져오는데, 이는 .NET 코드에서 JS 개체에 대한 참조를 나타냅니다. IJSObjectReference를 사용하여 모듈에서 내보낸 JS 함수를 호출합니다.

CallJs6.razor:

@page "/call-js-6"
@implements IAsyncDisposable
@inject IJSRuntime JS

<PageTitle>Call JS 6</PageTitle>

<h1>Call JS Example 6</h1>

<p>
    <button @onclick="TriggerPrompt">Trigger browser window prompt</button>
</p>

<p>
    @result
</p>

@code {
    private IJSObjectReference? module;
    private string? result;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSObjectReference>("import",
                "./scripts.js");
        }
    }

    private async Task TriggerPrompt()
    {
        result = await Prompt("Provide some text");
    }

    public async ValueTask<string?> Prompt(string message) =>
        module is not null ? 
            await module.InvokeAsync<string>("showPrompt", message) : null;

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            await module.DisposeAsync();
        }
    }
}

CallJsExample6.razor:

@page "/call-js-example-6"
@implements IAsyncDisposable
@inject IJSRuntime JS

<h1>Call JS Example 6</h1>

<p>
    <button @onclick="TriggerPrompt">Trigger browser window prompt</button>
</p>

<p>
    @result
</p>

@code {
    private IJSObjectReference? module;
    private string? result;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSObjectReference>("import", 
                "./scripts.js");
        }
    }

    private async Task TriggerPrompt()
    {
        result = await Prompt("Provide some text");
    }

    public async ValueTask<string?> Prompt(string message) =>
        module is not null ? 
            await module.InvokeAsync<string>("showPrompt", message) : null;

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            await module.DisposeAsync();
        }
    }
}

CallJsExample6.razor:

@page "/call-js-example-6"
@implements IAsyncDisposable
@inject IJSRuntime JS

<h1>Call JS Example 6</h1>

<p>
    <button @onclick="TriggerPrompt">Trigger browser window prompt</button>
</p>

<p>
    @result
</p>

@code {
    private IJSObjectReference? module;
    private string? result;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSObjectReference>("import", 
                "./scripts.js");
        }
    }

    private async Task TriggerPrompt()
    {
        result = await Prompt("Provide some text");
    }

    public async ValueTask<string?> Prompt(string message) =>
        module is not null ? 
            await module.InvokeAsync<string>("showPrompt", message) : null;

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            await module.DisposeAsync();
        }
    }
}

CallJsExample6.razor:

@page "/call-js-example-6"
@implements IAsyncDisposable
@inject IJSRuntime JS

<h1>Call JS Example 6</h1>

<p>
    <button @onclick="TriggerPrompt">Trigger browser window prompt</button>
</p>

<p>
    @result
</p>

@code {
    private IJSObjectReference module;
    private string result;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSObjectReference>("import", 
                "./scripts.js");
        }
    }

    private async Task TriggerPrompt()
    {
        result = await Prompt("Provide some text");
    }

    public async ValueTask<string> Prompt(string message)
    {
        return await module.InvokeAsync<string>("showPrompt", message);
    }

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            await module.DisposeAsync();
        }
    }
}

앞의 예에서:

  • 규칙에 따라, import 식별자는 JS 모듈을 가져오기 위해 특별히 사용되는 특수 식별자입니다.
  • 안정적인 정적 웹 자산 경로를 사용하여 모듈의 외부 JS 파일을 지정합니다. ./{SCRIPT PATH AND FILE NAME (.js)}여기서 다음을 수행합니다.
    • JS 파일의 올바른 정적 자산 경로를 만들려면 현재 디렉터리의 패스 세그먼트(./)가 필요합니다.
    • {SCRIPT PATH AND FILE NAME (.js)} 자리 표시자는 wwwroot 아래의 경로 및 파일 이름입니다.
  • IAsyncDisposable.DisposeAsync에서 가비지 수집을 위해 IJSObjectReference를 삭제합니다.

모듈을 동적으로 가져오려면 네트워크 요청이 필요하므로 InvokeAsync를 호출하여 비동기적으로 수행해야 합니다.

IJSInProcessObjectReference 는 클라이언트 쪽 구성 요소에서 함수를 동기적으로 호출할 수 있는 개체에 대한 참조 JS 를 나타냅니다. 자세한 내용은 클라이언트 쪽 구성 요소의 동기 JS interop 섹션을 참조하세요.

참고 항목

Razor 클래스 라이브러리에서 외부 JS 파일을 제공하는 경우, 안정적인 정적 웹 자산 경로(./_content/{PACKAGE ID}/{SCRIPT PATH AND FILE NAME (.js)})를 사용하여 모듈의 JS 파일을 지정합니다.

  • JS 파일의 올바른 정적 자산 경로를 만들려면 현재 디렉터리의 패스 세그먼트(./)가 필요합니다.
  • {PACKAGE ID} 자리 표시자는 라이브러리의 패키지 ID입니다. <PackageId>가 프로젝트 파일에 지정되지 않은 경우 패키지 ID는 기본적으로 프로젝트의 어셈블리 이름으로 설정됩니다. 다음 예제에서 라이브러리의 어셈블리 이름은 ComponentLibrary이고 라이브러리의 프로젝트 파일은 <PackageId>를 지정하지 않습니다.
  • {SCRIPT PATH AND FILE NAME (.js)} 자리 표시자는 wwwroot 아래의 경로 및 파일 이름입니다. 다음 예제에서는 외부 JS 파일(script.js)이 클래스 라이브러리의 wwwroot 폴더에 배치됩니다.
  • module는 구성 요소 클래스(private IJSObjectReference? module;)의 프라이빗 nullable IJSObjectReference 입니다.
module = await js.InvokeAsync<IJSObjectReference>(
    "import", "./_content/ComponentLibrary/scripts.js");

자세한 내용은 RCL(Razor 클래스 라이브러리)에서 ASP.NET Core Razor 구성 요소 사용을 참조하세요.

설명서 전체에서 Blazor 예제에서는 최신 .mjs 파일 확장명(RFC 9239)이 아닌 모듈 파일에 파일 확장자를 사용합니다.js. 이 설명서에서는 Mozilla Foundation 설명서에서 파일 확장자를 계속 사용하는 것과 같은 이유로 파일 확장자를 계속 사용합니다 .js.js . 자세한 내용은 MDN 설명서와 함께 .mjs 및 .js(MdN 설명서)를 참조하세요.

요소에 대한 참조 캡처

일부 JS(JavaScript) interop 시나리오에는 HTML 요소에 대한 참조가 필요합니다. 예를 들어, UI 라이브러리에는 초기화를 위해 요소 참조가 필요하거나, click 또는 play와 같은 요소에서 명령 같은 API를 호출해야 할 수 있습니다.

다음 방법을 사용하여 구성 요소의 HTML 요소에 대한 참조를 캡처합니다.

  • HTML 요소에 @ref 특성을 추가합니다.
  • 해당 이름이 @ref 특성 값과 일치하는 ElementReference 형식의 필드를 정의합니다.

다음 예에서는 username<input> 요소에 대한 참조를 캡처하는 방법을 보여 줍니다.

<input @ref="username" ... />

@code {
    private ElementReference username;
}

Warning

요소 참조만 사용하여 Blazor와 상호 작용하지 않는 빈 요소의 내용을 변경합니다. 이 시나리오는 타사 API가 요소에 콘텐츠를 제공하는 경우에 유용합니다. Blazor는 요소와 상호 작용하지 않으므로 요소의 Blazor 표시와 DOM 간에 충돌이 발생할 가능성이 없습니다.

다음 예제에서는 DOM과 상호 작용하여 개체에서 Todos 이 요소의 목록 항목(ul)을 채우기 때문에 Blazor interop을 통해 MyListJS 정렬되지 않은 목록(<li>)의 내용을 변경하면 위험합니다.

<ul @ref="MyList">
    @foreach (var item in Todos)
    {
        <li>@item.Text</li>
    }
</ul>

MyList 단지 DOM 콘텐츠를 읽거나 이벤트를 트리거하기 위해 요소 참조를 사용하는 것이 지원됩니다.

JS interop이 요소의 내용을 변경하고 Blazor 요소 MyList 에 diffs를 적용하려고 하면 diffs가 DOM과 일치하지 않습니다. 요소 참조와의 interop을 통해 JS 목록의 내용을 수정하는 MyList 것은 지원되지 않습니다.

자세한 내용은 ASP.NET Core Blazor JavaScript 상호 운용성(JS)을 참조하세요.

ElementReference는 JS interop을 통해 JS 코드로 전달됩니다. JS 코드는 일반적인 DOM API에서 사용할 수 있는 HTMLElement 인스턴스를 수신합니다. 예를 들어 다음 코드는 요소에 마우스 클릭을 전송할 수 있도록 하는 .NET 확장 메서드(TriggerClickEvent)를 정의합니다.

JS 함수 clickElement는 전달된 HTML 요소(element)에 click 이벤트를 만듭니다.

window.interopFunctions = {
  clickElement : function (element) {
    element.click();
  }
}

값을 반환하지 않는 JS 함수를 호출하려면 JSRuntimeExtensions.InvokeVoidAsync를 사용합니다. 다음 코드는 캡처된 ElementReference로 이전 JS 함수를 호출하여 클라이언트 쪽 click 이벤트를 트리거합니다.

@inject IJSRuntime JS

<button @ref="exampleButton">Example Button</button>

<button @onclick="TriggerClick">
    Trigger click event on <code>Example Button</code>
</button>

@code {
    private ElementReference exampleButton;

    public async Task TriggerClick()
    {
        await JS.InvokeVoidAsync(
            "interopFunctions.clickElement", exampleButton);
    }
}

확장 메서드를 사용하려면 IJSRuntime 인스턴스를 수신하는 정적 확장 메서드를 만듭니다.

public static async Task TriggerClickEvent(this ElementReference elementRef, 
    IJSRuntime js)
{
    await js.InvokeVoidAsync("interopFunctions.clickElement", elementRef);
}

clickElement 메서드는 개체에서 직접 호출됩니다. 다음 예제에서는 TriggerClickEvent 메서드를 JsInteropClasses 네임스페이스에서 사용할 수 있다고 가정합니다.

@inject IJSRuntime JS
@using JsInteropClasses

<button @ref="exampleButton">Example Button</button>

<button @onclick="TriggerClick">
    Trigger click event on <code>Example Button</code>
</button>

@code {
    private ElementReference exampleButton;

    public async Task TriggerClick()
    {
        await exampleButton.TriggerClickEvent(JS);
    }
}

Important

exampleButton 변수는 구성 요소가 렌더링된 후에만 채워집니다. JS 코드에 채워지지 않은 ElementReference가 전달되면 JS 코드는 null 값을 받습니다. 구성 요소에서 렌더링을 완료한 후에 구성 요소 참조를 조작하려면 OnAfterRenderAsync 또는 OnAfterRender 구성 요소 수명 주기 메서드를 사용합니다.

제네릭 형식으로 작업하고 값을 반환하는 경우 ValueTask<TResult>를 사용합니다.

public static ValueTask<T> GenericMethod<T>(this ElementReference elementRef, 
    IJSRuntime js)
{
    return js.InvokeAsync<T>("{JAVASCRIPT FUNCTION}", elementRef);
}

{JAVASCRIPT FUNCTION} 자리 표시자는 JS 함수 식별자입니다.

GenericMethod는 형식이 있는 개체에 대해 직접 호출됩니다. 다음 예제에서는 GenericMethodJsInteropClasses 네임스페이스에서 사용할 수 있다고 가정합니다.

@inject IJSRuntime JS
@using JsInteropClasses

<input @ref="username" />

<button @onclick="OnClickMethod">Do something generic</button>

<p>
    returnValue: @returnValue
</p>

@code {
    private ElementReference username;
    private string? returnValue;

    private async Task OnClickMethod()
    {
        returnValue = await username.GenericMethod<string>(JS);
    }
}
@inject IJSRuntime JS
@using JsInteropClasses

<input @ref="username" />

<button @onclick="OnClickMethod">Do something generic</button>

<p>
    returnValue: @returnValue
</p>

@code {
    private ElementReference username;
    private string? returnValue;

    private async Task OnClickMethod()
    {
        returnValue = await username.GenericMethod<string>(JS);
    }
}
@inject IJSRuntime JS
@using JsInteropClasses

<input @ref="username" />

<button @onclick="OnClickMethod">Do something generic</button>

<p>
    returnValue: @returnValue
</p>

@code {
    private ElementReference username;
    private string returnValue;

    private async Task OnClickMethod()
    {
        returnValue = await username.GenericMethod<string>(JS);
    }
}

구성 요소 간 참조 요소

다음과 같은 이유로 구성 요소 간에 ElementReference를 전달할 수 없습니다.

부모 구성 요소는 다른 구성 요소에서 요소 참조를 사용할 수 있도록 하기 위해 다음을 수행할 수 있습니다.

  • 자식 구성 요소가 콜백을 등록할 수 있도록 허용합니다.
  • 전달된 요소 참조를 사용하여 OnAfterRender 이벤트 중에 등록된 콜백을 호출합니다. 간접적으로 이 방법을 사용하면 자식 구성 요소가 부모의 요소 참조와 상호 작용할 수 있습니다.
<style>
    .red { color: red }
</style>
<script>
  function setElementClass(element, className) {
    var myElement = element;
    myElement.classList.add(className);
  }
</script>

참고 항목

위치 및 프로덕션 앱에 JS 대한 권장 사항에 대한 일반적인 지침은 ASP.NET Core Blazor 앱의 JavaScript 위치를 참조하세요.

CallJs7.razor(부모 구성 요소):

@page "/call-js-7"

<PageTitle>Call JS 7</PageTitle>

<h1>Call JS Example 7</h1>

<h2 @ref="title">Hello, world!</h2>

Welcome to your new app.

<SurveyPrompt Parent="this" Title="How is Blazor working for you?" />

CallJsExample7.razor(부모 구성 요소):

@page "/call-js-example-7"

<h1>Call JS Example 7</h1>

<h2 @ref="title">Hello, world!</h2>

Welcome to your new app.

<SurveyPrompt Parent="this" Title="How is Blazor working for you?" />

CallJsExample7.razor(부모 구성 요소):

@page "/call-js-example-7"

<h1>Call JS Example 7</h1>

<h2 @ref="title">Hello, world!</h2>

Welcome to your new app.

<SurveyPrompt Parent="this" Title="How is Blazor working for you?" />

CallJsExample7.razor(부모 구성 요소):

@page "/call-js-example-7"

<h1>Call JS Example 7</h1>

<h2 @ref="title">Hello, world!</h2>

Welcome to your new app.

<SurveyPrompt Parent="this" Title="How is Blazor working for you?" />

CallJsExample7.razor(부모 구성 요소):

@page "/call-js-example-7"

<h1>Call JS Example 7</h1>

<h2 @ref="title">Hello, world!</h2>

Welcome to your new app.

<SurveyPrompt Parent="this" Title="How is Blazor working for you?" />

CallJs7.razor.cs:

using Microsoft.AspNetCore.Components;

namespace BlazorSample.Pages;

public partial class CallJs7 : 
    ComponentBase, IObservable<ElementReference>, IDisposable
{
    private bool disposing;
    private readonly List<IObserver<ElementReference>> subscriptions = [];
    private ElementReference title;

    protected override void OnAfterRender(bool firstRender)
    {
        base.OnAfterRender(firstRender);

        foreach (var subscription in subscriptions)
        {
            try
            {
                subscription.OnNext(title);
            }
            catch (Exception)
            {
                throw;
            }
        }
    }

    public void Dispose()
    {
        disposing = true;

        foreach (var subscription in subscriptions)
        {
            try
            {
                subscription.OnCompleted();
            }
            catch (Exception)
            {
            }
        }

        subscriptions.Clear();

        // The following prevents derived types that introduce a
        // finalizer from needing to re-implement IDisposable.
        GC.SuppressFinalize(this);
    }

    public IDisposable Subscribe(IObserver<ElementReference> observer)
    {
        if (disposing)
        {
            throw new InvalidOperationException("Parent being disposed");
        }

        subscriptions.Add(observer);

        return new Subscription(observer, this);
    }

    private class Subscription(IObserver<ElementReference> observer,
        CallJs7 self) : IDisposable
    {
        public IObserver<ElementReference> Observer { get; } = observer;
        public CallJs7 Self { get; } = self;

        public void Dispose() => Self.subscriptions.Remove(Observer);
    }
}

CallJsExample7.razor.cs:

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Components;

namespace BlazorSample.Pages;

public partial class CallJsExample7 : 
    ComponentBase, IObservable<ElementReference>, IDisposable
{
    private bool disposing;
    private IList<IObserver<ElementReference>> subscriptions = 
        new List<IObserver<ElementReference>>();
    private ElementReference title;

    protected override void OnAfterRender(bool firstRender)
    {
        base.OnAfterRender(firstRender);

        foreach (var subscription in subscriptions)
        {
            try
            {
                subscription.OnNext(title);
            }
            catch (Exception)
            {
                throw;
            }
        }
    }

    public void Dispose()
    {
        disposing = true;

        foreach (var subscription in subscriptions)
        {
            try
            {
                subscription.OnCompleted();
            }
            catch (Exception)
            {
            }
        }

        subscriptions.Clear();
    }

    public IDisposable Subscribe(IObserver<ElementReference> observer)
    {
        if (disposing)
        {
            throw new InvalidOperationException("Parent being disposed");
        }

        subscriptions.Add(observer);

        return new Subscription(observer, this);
    }

    private class Subscription : IDisposable
    {
        public Subscription(IObserver<ElementReference> observer, 
            CallJsExample7 self)
        {
            Observer = observer;
            Self = self;
        }

        public IObserver<ElementReference> Observer { get; }
        public CallJsExample7 Self { get; }

        public void Dispose()
        {
            Self.subscriptions.Remove(Observer);
        }
    }
}

CallJsExample7.razor.cs:

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Components;

namespace BlazorSample.Pages;

public partial class CallJsExample7 : 
    ComponentBase, IObservable<ElementReference>, IDisposable
{
    private bool disposing;
    private IList<IObserver<ElementReference>> subscriptions = 
        new List<IObserver<ElementReference>>();
    private ElementReference title;

    protected override void OnAfterRender(bool firstRender)
    {
        base.OnAfterRender(firstRender);

        foreach (var subscription in subscriptions)
        {
            try
            {
                subscription.OnNext(title);
            }
            catch (Exception)
            {
                throw;
            }
        }
    }

    public void Dispose()
    {
        disposing = true;

        foreach (var subscription in subscriptions)
        {
            try
            {
                subscription.OnCompleted();
            }
            catch (Exception)
            {
            }
        }

        subscriptions.Clear();
    }

    public IDisposable Subscribe(IObserver<ElementReference> observer)
    {
        if (disposing)
        {
            throw new InvalidOperationException("Parent being disposed");
        }

        subscriptions.Add(observer);

        return new Subscription(observer, this);
    }

    private class Subscription : IDisposable
    {
        public Subscription(IObserver<ElementReference> observer, 
            CallJsExample7 self)
        {
            Observer = observer;
            Self = self;
        }

        public IObserver<ElementReference> Observer { get; }
        public CallJsExample7 Self { get; }

        public void Dispose()
        {
            Self.subscriptions.Remove(Observer);
        }
    }
}

CallJsExample7.razor.cs:

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Components;

namespace BlazorSample.Pages
{
    public partial class CallJsExample7 : 
        ComponentBase, IObservable<ElementReference>, IDisposable
    {
        private bool disposing;
        private IList<IObserver<ElementReference>> subscriptions = 
            new List<IObserver<ElementReference>>();
        private ElementReference title;

        protected override void OnAfterRender(bool firstRender)
        {
            base.OnAfterRender(firstRender);

            foreach (var subscription in subscriptions)
            {
                try
                {
                    subscription.OnNext(title);
                }
                catch (Exception)
                {
                    throw;
                }
            }
        }

        public void Dispose()
        {
            disposing = true;

            foreach (var subscription in subscriptions)
            {
                try
                {
                    subscription.OnCompleted();
                }
                catch (Exception)
                {
                }
            }

            subscriptions.Clear();
        }

        public IDisposable Subscribe(IObserver<ElementReference> observer)
        {
            if (disposing)
            {
                throw new InvalidOperationException("Parent being disposed");
            }

            subscriptions.Add(observer);

            return new Subscription(observer, this);
        }

        private class Subscription : IDisposable
        {
            public Subscription(IObserver<ElementReference> observer, 
                CallJsExample7 self)
            {
                Observer = observer;
                Self = self;
            }

            public IObserver<ElementReference> Observer { get; }
            public CallJsExample7 Self { get; }

            public void Dispose()
            {
                Self.subscriptions.Remove(Observer);
            }
        }
    }
}

CallJsExample7.razor.cs:

using System;
using System.Collections.Generic;
using Microsoft.AspNetCore.Components;

namespace BlazorSample.Pages
{
    public partial class CallJsExample7 : 
        ComponentBase, IObservable<ElementReference>, IDisposable
    {
        private bool disposing;
        private IList<IObserver<ElementReference>> subscriptions = 
            new List<IObserver<ElementReference>>();
        private ElementReference title;

        protected override void OnAfterRender(bool firstRender)
        {
            base.OnAfterRender(firstRender);

            foreach (var subscription in subscriptions)
            {
                try
                {
                    subscription.OnNext(title);
                }
                catch (Exception)
                {
                    throw;
                }
            }
        }

        public void Dispose()
        {
            disposing = true;

            foreach (var subscription in subscriptions)
            {
                try
                {
                    subscription.OnCompleted();
                }
                catch (Exception)
                {
                }
            }

            subscriptions.Clear();
        }

        public IDisposable Subscribe(IObserver<ElementReference> observer)
        {
            if (disposing)
            {
                throw new InvalidOperationException("Parent being disposed");
            }

            subscriptions.Add(observer);

            return new Subscription(observer, this);
        }

        private class Subscription : IDisposable
        {
            public Subscription(IObserver<ElementReference> observer, 
                CallJsExample7 self)
            {
                Observer = observer;
                Self = self;
            }

            public IObserver<ElementReference> Observer { get; }
            public CallJsExample7 Self { get; }

            public void Dispose()
            {
                Self.subscriptions.Remove(Observer);
            }
        }
    }
}

앞의 예제에서 앱의 네임스페이스는 .입니다 BlazorSample. 코드를 로컬로 테스트하는 경우 네임스페이스를 업데이트합니다.

SurveyPrompt.razor(자식 구성 요소):

<div class="alert alert-secondary mt-4">
    <span class="oi oi-pencil me-2" aria-hidden="true"></span>
    <strong>@Title</strong>

    <span class="text-nowrap">
        Please take our
        <a target="_blank" class="font-weight-bold link-dark" href="https://go.microsoft.com/fwlink/?linkid=2186158">brief survey</a>
    </span>
    and tell us what you think.
</div>

@code {
    // Demonstrates how a parent component can supply parameters
    [Parameter]
    public string? Title { get; set; }
}
<div class="alert alert-secondary mt-4">
    <span class="oi oi-pencil me-2" aria-hidden="true"></span>
    <strong>@Title</strong>

    <span class="text-nowrap">
        Please take our
        <a target="_blank" class="font-weight-bold link-dark" href="https://go.microsoft.com/fwlink/?linkid=2186157">brief survey</a>
    </span>
    and tell us what you think.
</div>

@code {
    // Demonstrates how a parent component can supply parameters
    [Parameter]
    public string? Title { get; set; }
}
<div class="alert alert-secondary mt-4" role="alert">
    <span class="oi oi-pencil mr-2" aria-hidden="true"></span>
    <strong>@Title</strong>

    <span class="text-nowrap">
        Please take our
        <a target="_blank" class="font-weight-bold" 
            href="https://go.microsoft.com/fwlink/?linkid=2109206">brief survey</a>
    </span>
    and tell us what you think.
</div>

@code {
    [Parameter]
    public string? Title { get; set; }
}
<div class="alert alert-secondary mt-4" role="alert">
    <span class="oi oi-pencil mr-2" aria-hidden="true"></span>
    <strong>@Title</strong>

    <span class="text-nowrap">
        Please take our
        <a target="_blank" class="font-weight-bold" 
            href="https://go.microsoft.com/fwlink/?linkid=2109206">brief survey</a>
    </span>
    and tell us what you think.
</div>

@code {
    [Parameter]
    public string Title { get; set; }
}
<div class="alert alert-secondary mt-4" role="alert">
    <span class="oi oi-pencil mr-2" aria-hidden="true"></span>
    <strong>@Title</strong>

    <span class="text-nowrap">
        Please take our
        <a target="_blank" class="font-weight-bold" 
            href="https://go.microsoft.com/fwlink/?linkid=2109206">brief survey</a>
    </span>
    and tell us what you think.
</div>

@code {
    [Parameter]
    public string Title { get; set; }
}

SurveyPrompt.razor.cs:

using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;

namespace BlazorSample.Components;

public partial class SurveyPrompt : 
    ComponentBase, IObserver<ElementReference>, IDisposable
{
    private IDisposable? subscription = null;

    [Parameter]
    public IObservable<ElementReference>? Parent { get; set; }

    [Inject]
    public IJSRuntime? JS {get; set;}

    protected override void OnParametersSet()
    {
        base.OnParametersSet();

        subscription?.Dispose();
        subscription = Parent?.Subscribe(this);
    }

    public void OnCompleted()
    {
        subscription = null;
    }

    public void OnError(Exception error)
    {
        subscription = null;
    }

    public void  OnNext(ElementReference value)
    {
        _ = (JS?.InvokeAsync<object>(
            "setElementClass", [value, "red"]));
    }

    public void Dispose()
    {
        subscription?.Dispose();

        // The following prevents derived types that introduce a
        // finalizer from needing to re-implement IDisposable.
        GC.SuppressFinalize(this);
    }
}
using System;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;

namespace BlazorSample.Shared;

public partial class SurveyPrompt : 
    ComponentBase, IObserver<ElementReference>, IDisposable
{
    private IDisposable? subscription = null;

    [Parameter]
    public IObservable<ElementReference>? Parent { get; set; }

    [Inject]
    public IJSRuntime? JS {get; set;}

    protected override void OnParametersSet()
    {
        base.OnParametersSet();

        subscription?.Dispose();
        subscription = 
            Parent is not null ? Parent.Subscribe(this) : null;
    }

    public void OnCompleted()
    {
        subscription = null;
    }

    public void OnError(Exception error)
    {
        subscription = null;
    }

    public void OnNext(ElementReference value)
    {
        JS.InvokeAsync<object>(
            "setElementClass", new object[] { value, "red" });
    }

    public void Dispose()
    {
        subscription?.Dispose();
    }
}
using System;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;

namespace BlazorSample.Shared;

public partial class SurveyPrompt : 
    ComponentBase, IObserver<ElementReference>, IDisposable
{
    private IDisposable? subscription = null;

    [Parameter]
    public IObservable<ElementReference>? Parent { get; set; }

    [Inject]
    public IJSRuntime? JS {get; set;}

    protected override void OnParametersSet()
    {
        base.OnParametersSet();

        subscription?.Dispose();
        subscription = 
            Parent is not null ? Parent.Subscribe(this) : null;
    }

    public void OnCompleted()
    {
        subscription = null;
    }

    public void OnError(Exception error)
    {
        subscription = null;
    }

    public void OnNext(ElementReference value)
    {
        JS.InvokeAsync<object>(
            "setElementClass", new object[] { value, "red" });
    }

    public void Dispose()
    {
        subscription?.Dispose();
    }
}
using System;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;

namespace BlazorSample.Shared
{
    public partial class SurveyPrompt : 
        ComponentBase, IObserver<ElementReference>, IDisposable
    {
        private IDisposable subscription = null;

        [Parameter]
        public IObservable<ElementReference> Parent { get; set; }

        [Inject]
        public IJSRuntime JS {get; set;}

        protected override void OnParametersSet()
        {
            base.OnParametersSet();

            subscription?.Dispose();
            subscription = Parent.Subscribe(this);
        }

        public void OnCompleted()
        {
            subscription = null;
        }

        public void OnError(Exception error)
        {
            subscription = null;
        }

        public void OnNext(ElementReference value)
        {
            JS.InvokeAsync<object>(
                "setElementClass", new object[] { value, "red" });
        }

        public void Dispose()
        {
            subscription?.Dispose();
        }
    }
}
using System;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;

namespace BlazorSample.Shared
{
    public partial class SurveyPrompt : 
        ComponentBase, IObserver<ElementReference>, IDisposable
    {
        private IDisposable subscription = null;

        [Parameter]
        public IObservable<ElementReference> Parent { get; set; }

        [Inject]
        public IJSRuntime JS {get; set;}

        protected override void OnParametersSet()
        {
            base.OnParametersSet();

            subscription?.Dispose();
            subscription = Parent.Subscribe(this);
        }

        public void OnCompleted()
        {
            subscription = null;
        }

        public void OnError(Exception error)
        {
            subscription = null;
        }

        public void OnNext(ElementReference value)
        {
            JS.InvokeAsync<object>(
                "setElementClass", new object[] { value, "red" });
        }

        public void Dispose()
        {
            subscription?.Dispose();
        }
    }
}

앞의 예제에서 앱의 네임스페이스는 Shared 폴더에 공유 구성 요소가 있는 BlazorSample입니다. 코드를 로컬로 테스트하는 경우 네임스페이스를 업데이트합니다.

JavaScript interop 호출 강화

이 섹션은 대화형 서버 구성 요소에만 적용되지만 조건이 보증하는 경우 클라이언트 쪽 구성 요소는 interop 시간 제한을 설정할 JS 수도 있습니다.

서버 대화형 작업이 있는 서버 쪽 앱에서 JavaScript(JS) interop은 네트워킹 오류로 인해 실패할 수 있으며 신뢰할 수 없는 것으로 처리되어야 합니다. 기본적으로 Blazor 앱은 JS interop 호출에 1분의 시간 제한을 사용합니다. 앱에서 시간 제한을 좀 더 적극적으로 허용할 수 있는 경우 다음 접근 방식 중 하나를 사용하여 시간 제한을 설정합니다.

CircuitOptions.JSInteropDefaultCallTimeout을 사용하여 Program.cs에서 전역 시간 제한을 설정합니다.

builder.Services.AddRazorComponents()
    .AddInteractiveServerComponents(options => 
        options.JSInteropDefaultCallTimeout = {TIMEOUT});
builder.Services.AddServerSideBlazor(
    options => options.JSInteropDefaultCallTimeout = {TIMEOUT});

CircuitOptions.JSInteropDefaultCallTimeout을 사용하여 Startup.csStartup.ConfigureServices 메서드에서 전역 시간 제한을 설정합니다.

services.AddServerSideBlazor(
    options => options.JSInteropDefaultCallTimeout = {TIMEOUT});

{TIMEOUT} 자리 표시자는 (예: TimeSpan.FromSeconds(80))입니다 TimeSpan .

구성 요소 코드에서 호출당 시간 제한을 설정합니다. 지정된 시간 제한은 JSInteropDefaultCallTimeout에 의해 설정된 전역 시간 제한을 재정의합니다.

var result = await JS.InvokeAsync<string>("{ID}", {TIMEOUT}, new[] { "Arg1" });

앞의 예에서:

  • {TIMEOUT} 자리 표시자는 (예: TimeSpan.FromSeconds(80))입니다 TimeSpan .
  • {ID} 자리 표시자는 호출할 함수의 식별자입니다. 예를 들어 someScope.someFunction 값은 window.someScope.someFunction 함수를 호출합니다.

interop 오류의 JS 일반적인 원인은 서버 쪽 구성 요소의 네트워크 오류이지만 클라이언트 쪽 구성 요소에 대한 interop 호출에 대해 JS 호출당 시간 제한을 설정할 수 있습니다. 클라이언트 쪽 구성 요소 JS 에 대한 회로가 없 SignalR 더라도 적용되는 다른 이유로 interop 호출이 실패할 수 있습니다.

리소스 소모에 대한 자세한 내용은 ASP.NET Core Blazor 대화형 서버 쪽 렌더링에 대한 위협 완화 지침을 참조하세요.

순환 개체 참조 방지

순환 참조를 포함하는 개체는 다음 중 하나에 대해 클라이언트에서 직렬화될 수 없습니다.

  • .NET 메서드 호출
  • 반환 형식에 순환 참조가 있는 경우 C#에서 JavaScript 메서드 호출

UI를 렌더링하는 JavaScript 라이브러리

브라우저 DOM 내에서 표시되는 사용자 인터페이스 요소를 생성하는 JavaScript(JS) 라이브러리를 사용할 수도 있습니다. 처음에는, Blazor의 diff 시스템은 DOM 요소의 트리를 제어해야 하는데 일부 외부 코드가 DOM 트리를 변경하고 diff 적용 메커니즘을 무효화하면 오류가 발생하기 때문에 이것이 어려워 보일 수 있습니다. 이는 Blazor 특정 제한 사항이 아닙니다. 동일한 문제가 모든 diff 기반 UI 프레임워크에서 발생합니다.

다행히도 Razor 구성 요소 UI 내에 외부에서 생성된 UI를 안정적으로 포함하는 것은 간단합니다. 구성 요소 코드(.razor 파일)가 빈 요소를 생성하도록 하는 것이 좋습니다. Blazor의 diff 시스템이 관련되는 한 요소는 항상 비어 있으므로 렌더러는 요소로 재귀되지 않으며 대신 콘텐츠를 그대로 둡니다. 이렇게 하면 외부 관리형 임의 콘텐츠로 요소를 안전하게 채울 수 있습니다.

다음 예제에서는 개념을 보여 줍니다. firstRendertrue인 경우 if 문 내에서, JS interop을 사용하여 Blazor 외부에서 unmanagedElement와 상호 작용합니다. 예를 들어 외부 JS 라이브러리를 호출하여 요소를 채웁니다. 이 구성 요소가 제거될 때까지 Blazor는 요소 콘텐츠를 그대로 둡니다. 구성 요소가 제거되면 구성 요소의 전체 DOM 하위 트리도 제거됩니다.

<h1>Hello! This is a Razor component rendered at @DateTime.Now</h1>

<div @ref="unmanagedElement"></div>

@code {
    private ElementReference unmanagedElement;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            ...
        }
    }
}

오픈 소스 Mapbox API를 사용하여 대화형 맵을 렌더링하는 다음 예제를 살펴보겠습니다.

다음 JS 모듈은 앱에 배치되거나 Razor 클래스 라이브러리에서 사용할 수 있게 됩니다.

참고 항목

Mapbox 맵을 만들려면 Mapbox Sign in(Mapbox 로그인)에서 액세스 토큰을 가져와, 다음 코드에서 {ACCESS TOKEN}이 나타나는 곳에 제공합니다.

wwwroot/mapComponent.js:

import 'https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.js';

mapboxgl.accessToken = '{ACCESS TOKEN}';

export function addMapToElement(element) {
  return new mapboxgl.Map({
    container: element,
    style: 'mapbox://styles/mapbox/streets-v11',
    center: [-74.5, 40],
    zoom: 9
  });
}

export function setMapCenter(map, latitude, longitude) {
  map.setCenter([longitude, latitude]);
}

올바른 스타일을 생성하려면 호스트 HTML 페이지에 다음 스타일시트 태그를 추가합니다.

다음 <link> 요소를 <head> 요소 태그(<head> 콘텐츠의 위치)에 추가합니다.

<link href="https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.css" 
    rel="stylesheet" />

CallJs8.razor:

@page "/call-js-8"
@implements IAsyncDisposable
@inject IJSRuntime JS

<PageTitle>Call JS 8</PageTitle>

<HeadContent>
    <link href="https://api.mapbox.com/mapbox-gl-js/v1.12.0/mapbox-gl.css" 
        rel="stylesheet" />
</HeadContent>

<h1>Call JS Example 8</h1>

<div @ref="mapElement" style='width:400px;height:300px'></div>

<button @onclick="() => ShowAsync(51.454514, -2.587910)">Show Bristol, UK</button>
<button @onclick="() => ShowAsync(35.6762, 139.6503)">Show Tokyo, Japan</button>

@code
{
    private ElementReference mapElement;
    private IJSObjectReference? mapModule;
    private IJSObjectReference? mapInstance;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            mapModule = await JS.InvokeAsync<IJSObjectReference>(
                "import", "./mapComponent.js");
            mapInstance = await mapModule.InvokeAsync<IJSObjectReference>(
                "addMapToElement", mapElement);
        }
    }

    private async Task ShowAsync(double latitude, double longitude)
    {
        if (mapModule is not null && mapInstance is not null)
        {
            await mapModule.InvokeVoidAsync("setMapCenter", mapInstance, 
                latitude, longitude).AsTask();
        }
    }

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (mapInstance is not null)
        {
            await mapInstance.DisposeAsync();
        }

        if (mapModule is not null)
        {
            await mapModule.DisposeAsync();
        }
    }
}

CallJsExample8.razor:

@page "/call-js-example-8"
@implements IAsyncDisposable
@inject IJSRuntime JS

<h1>Call JS Example 8</h1>

<div @ref="mapElement" style='width:400px;height:300px'></div>

<button @onclick="() => ShowAsync(51.454514, -2.587910)">Show Bristol, UK</button>
<button @onclick="() => ShowAsync(35.6762, 139.6503)">Show Tokyo, Japan</button>

@code
{
    private ElementReference mapElement;
    private IJSObjectReference? mapModule;
    private IJSObjectReference? mapInstance;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            mapModule = await JS.InvokeAsync<IJSObjectReference>(
                "import", "./mapComponent.js");
            mapInstance = await mapModule.InvokeAsync<IJSObjectReference>(
                "addMapToElement", mapElement);
        }
    }

    private async Task ShowAsync(double latitude, double longitude)
    {
        if (mapModule is not null && mapInstance is not null)
        {
            await mapModule.InvokeVoidAsync("setMapCenter", mapInstance, 
                latitude, longitude).AsTask();
        }
    }

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (mapInstance is not null)
        {
            await mapInstance.DisposeAsync();
        }

        if (mapModule is not null)
        {
            await mapModule.DisposeAsync();
        }
    }
}

CallJsExample8.razor:

@page "/call-js-example-8"
@implements IAsyncDisposable
@inject IJSRuntime JS

<h1>Call JS Example 8</h1>

<div @ref="mapElement" style='width:400px;height:300px'></div>

<button @onclick="() => ShowAsync(51.454514, -2.587910)">Show Bristol, UK</button>
<button @onclick="() => ShowAsync(35.6762, 139.6503)">Show Tokyo, Japan</button>

@code
{
    private ElementReference mapElement;
    private IJSObjectReference? mapModule;
    private IJSObjectReference? mapInstance;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            mapModule = await JS.InvokeAsync<IJSObjectReference>(
                "import", "./mapComponent.js");
            mapInstance = await mapModule.InvokeAsync<IJSObjectReference>(
                "addMapToElement", mapElement);
        }
    }

    private async Task ShowAsync(double latitude, double longitude)
    {
        if (mapModule is not null && mapInstance is not null)
        {
            await mapModule.InvokeVoidAsync("setMapCenter", mapInstance, 
                latitude, longitude).AsTask();
        }
    }

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (mapInstance is not null)
        {
            await mapInstance.DisposeAsync();
        }

        if (mapModule is not null)
        {
            await mapModule.DisposeAsync();
        }
    }
}

앞의 예제는 대화형 맵 UI를 생성합니다. 사용자는:

  • 끌어서 스크롤하거나 확대/축소할 수 있습니다.
  • 단추를 선택하여 미리 정의된 위치로 이동합니다.

영국 브리스틀 및 일본 도쿄를 선택하는 단추가 포함된 일본 도쿄의 Mapbox 거리 지도

앞의 예에서:

  • Blazor가 관련되는 한 @ref="mapElement"가 있는 <div>는 비어 있습니다. mapbox-gl.js 스크립트는 요소를 안전하게 채우고 콘텐츠를 수정할 수 있습니다. UI를 렌더링하는 JS 라이브러리와 함께 이 방법을 사용합니다. 페이지의 다른 부분에 연결하고 해당 부분을 수정하려고 시도하지 않는 한 Razor 구성 요소 내부에 타사 JS SPA 프레임워크의 구성 요소를 포함할 수 있습니다. 외부 JS 코드는 Blazor가 비어 있는 것으로 간주하지 않는 요소를 안전하게 수정할 수 없습니다.
  • 이 접근 방식을 사용하는 경우 Blazor가 DOM 요소를 유지하거나 제거하는 방법에 대한 규칙에 고려해야 합니다. 구성 요소는 단추 클릭 이벤트를 안전하게 처리하며 기본적으로 가능한 경우 DOM 요소가 유지되므로 기존 맵 인스턴스를 업데이트합니다. @foreach 루프 내부에서 맵 요소 목록을 렌더링한 경우 @key를 사용하여 구성 요소 인스턴스를 유지할 수 있습니다. 그렇지 않으면 목록 데이터의 변경으로 인해 안타깝게도 구성 요소 인스턴스가 이전 인스턴스의 상태를 유지할 수 있습니다. 자세한 내용은 지시문 특성을 사용하여 @key 요소, 구성 요소 및 모델 개체 간의 관계를 유지하는 방법을 참조하세요.
  • 이 예제는 ES6 모듈 내에서 JS 논리와 종속성을 캡슐화하고 import 식별자를 사용하여 동적으로 모듈을 로드합니다. 자세한 내용은 JavaScript 모듈에서의 JavaScript 격리를 참조하세요.

바이트 배열 지원

Blazor는 최적화된 바이트 배열 JS(JavaScript) interop을 지원하여 바이트 배열이 Base64로 인코딩/디코딩되는 것을 방지합니다. 다음 예제에서는 JS interop을 사용하여 바이트 배열을 JavaScript에 전달합니다.

receiveByteArrayJS 함수를 제공합니다. 이 함수는 InvokeVoidAsync를 사용하여 호출되며 값을 반환하지 않습니다.

<script>
  window.receiveByteArray = (bytes) => {
    let utf8decoder = new TextDecoder();
    let str = utf8decoder.decode(bytes);
    return str;
  };
</script>

참고 항목

위치 및 프로덕션 앱에 JS 대한 권장 사항에 대한 일반적인 지침은 ASP.NET Core Blazor 앱의 JavaScript 위치를 참조하세요.

CallJs9.razor:

@page "/call-js-9"
@inject IJSRuntime JS

<h1>Call JS Example 9</h1>

<p>
    <button @onclick="SendByteArray">Send Bytes</button>
</p>

<p>
    @result
</p>

<p>
    Quote &copy;2005 <a href="https://www.uphe.com">Universal Pictures</a>:
    <a href="https://www.uphe.com/movies/serenity-2005">Serenity</a><br>
    <a href="https://www.imdb.com/name/nm0821612/">Jewel Staite on IMDB</a>
</p>

@code {
    private string? result;

    private async Task SendByteArray()
    {
        var bytes = new byte[] { 0x45, 0x76, 0x65, 0x72, 0x79, 0x74, 0x68, 0x69,
            0x6e, 0x67, 0x27, 0x73, 0x20, 0x73, 0x68, 0x69, 0x6e, 0x79, 0x2c,
            0x20, 0x43, 0x61, 0x70, 0x74, 0x69, 0x61, 0x6e, 0x2e, 0x20, 0x4e,
            0x6f, 0x74, 0x20, 0x74, 0x6f, 0x20, 0x66, 0x72, 0x65, 0x74, 0x2e };

        result = await JS.InvokeAsync<string>("receiveByteArray", bytes);
    }
}

CallJsExample9.razor:

@page "/call-js-example-9"
@inject IJSRuntime JS

<h1>Call JS Example 9</h1>

<p>
    <button @onclick="SendByteArray">Send Bytes</button>
</p>

<p>
    @result
</p>

<p>
    Quote &copy;2005 <a href="https://www.uphe.com">Universal Pictures</a>:
    <a href="https://www.uphe.com/movies/serenity-2005">Serenity</a><br>
    <a href="https://www.imdb.com/name/nm0821612/">Jewel Staite on IMDB</a>
</p>

@code {
    private string? result;

    private async Task SendByteArray()
    {
        var bytes = new byte[] { 0x45, 0x76, 0x65, 0x72, 0x79, 0x74, 0x68, 0x69,
            0x6e, 0x67, 0x27, 0x73, 0x20, 0x73, 0x68, 0x69, 0x6e, 0x79, 0x2c,
            0x20, 0x43, 0x61, 0x70, 0x74, 0x69, 0x61, 0x6e, 0x2e, 0x20, 0x4e,
            0x6f, 0x74, 0x20, 0x74, 0x6f, 0x20, 0x66, 0x72, 0x65, 0x74, 0x2e };

        result = await JS.InvokeAsync<string>("receiveByteArray", bytes);
    }
}

JavaScript에서 .NET을 호출할 때 바이트 배열을 사용하는 방법에 대한 자세한 내용은 ASP.NET Core Blazor의 JavaScript 함수에서 .NET 메서드 호출을 참조하세요.

.NET에서 JavaScript로 스트리밍

Blazor는 .NET에서 JavaScript로 직접 데이터를 스트리밍하는 것을 지원합니다. DotNetStreamReference를 사용하여 스트림이 만들어집니다.

DotNetStreamReference는 .NET 스트림을 나타내며 다음 매개 변수를 사용합니다.

  • stream: JavaScript로 전송되는 스트림입니다.
  • leaveOpen: 전송 후 스트림을 열어 둘지를 결정합니다. 값을 제공하지 않으면 leaveOpen은 기본적으로 false로 설정됩니다.

JavaScript에서 배열 버퍼 또는 읽기 가능한 스트림을 사용하여 데이터를 받습니다.

  • ArrayBuffer 사용:

    async function streamToJavaScript(streamRef) {
      const data = await streamRef.arrayBuffer();
    }
    
  • ReadableStream 사용:

    async function streamToJavaScript(streamRef) {
      const stream = await streamRef.stream();
    }
    

C# 코드에서:

using var streamRef = new DotNetStreamReference(stream: {STREAM}, leaveOpen: false);
await JS.InvokeVoidAsync("streamToJavaScript", streamRef);

앞의 예에서:

  • {STREAM} 자리 표시자는 JavaScript로 전송되는 Stream을 나타냅니다.
  • JS는 삽입된 IJSRuntime 인스턴스입니다.

ASP.NET Core Blazor의 JavaScript 함수에서 .NET 메서드 호출에서는 JavaScript에서 .NET으로 스트리밍하는 역방향 작업을 설명합니다.

ASP.NET Core Blazor 파일 다운로드에서는 Blazor에 파일을 다운로드하는 방법을 다룹니다.

JavaScript 예외 Catch

JS 예외를 catch하려면 JS interop을 try-catch 블록으로 래핑하고 JSException을 catch합니다.

다음 예제에는 nonFunctionJS 함수가 존재하지 않습니다. 함수가 없는 경우에는 JSException이 다음 오류를 나타내는 Message로 트래핑됩니다.

Could not find 'nonFunction' ('nonFunction' was undefined).

CallJs11.razor:

@page "/call-js-11"
@inject IJSRuntime JS

<PageTitle>Call JS 11</PageTitle>

<h1>Call JS Example 11</h1>

<p>
    <button @onclick="CatchUndefinedJSFunction">Catch Exception</button>
</p>

<p>
    @result
</p>

<p>
    @errorMessage
</p>

@code {
    private string? errorMessage;
    private string? result;

    private async Task CatchUndefinedJSFunction()
    {
        try
        {
            result = await JS.InvokeAsync<string>("nonFunction");
        }
        catch (JSException e)
        {
            errorMessage = $"Error Message: {e.Message}";
        }
    }
}

CallJsExample11.razor:

@page "/call-js-example-11"
@inject IJSRuntime JS

<h1>Call JS Example 11</h1>

<p>
    <button @onclick="CatchUndefinedJSFunction">Catch Exception</button>
</p>

<p>
    @result
</p>

<p>
    @errorMessage
</p>

@code {
    private string? errorMessage;
    private string? result;

    private async Task CatchUndefinedJSFunction()
    {
        try
        {
            result = await JS.InvokeAsync<string>("nonFunction");
        }
        catch (JSException e)
        {
            errorMessage = $"Error Message: {e.Message}";
        }
    }
}

CallJsExample11.razor:

@page "/call-js-example-11"
@inject IJSRuntime JS

<h1>Call JS Example 11</h1>

<p>
    <button @onclick="CatchUndefinedJSFunction">Catch Exception</button>
</p>

<p>
    @result
</p>

<p>
    @errorMessage
</p>

@code {
    private string? errorMessage;
    private string? result;

    private async Task CatchUndefinedJSFunction()
    {
        try
        {
            result = await JS.InvokeAsync<string>("nonFunction");
        }
        catch (JSException e)
        {
            errorMessage = $"Error Message: {e.Message}";
        }
    }
}

CallJsExample11.razor:

@page "/call-js-example-11"
@inject IJSRuntime JS

<h1>Call JS Example 11</h1>

<p>
    <button @onclick="CatchUndefinedJSFunction">Catch Exception</button>
</p>

<p>
    @result
</p>

<p>
    @errorMessage
</p>

@code {
    private string errorMessage;
    private string result;

    private async Task CatchUndefinedJSFunction()
    {
        try
        {
            result = await JS.InvokeAsync<string>("nonFunction");
        }
        catch (JSException e)
        {
            errorMessage = $"Error Message: {e.Message}";
        }
    }
}

CallJsExample11.razor:

@page "/call-js-example-11"
@inject IJSRuntime JS

<h1>Call JS Example 11</h1>

<p>
    <button @onclick="CatchUndefinedJSFunction">Catch Exception</button>
</p>

<p>
    @result
</p>

<p>
    @errorMessage
</p>

@code {
    private string errorMessage;
    private string result;

    private async Task CatchUndefinedJSFunction()
    {
        try
        {
            result = await JS.InvokeAsync<string>("nonFunction");
        }
        catch (JSException e)
        {
            errorMessage = $"Error Message: {e.Message}";
        }
    }
}

장기 실행 JavaScript 함수 중단

JSAbortController를 구성 요소의 CancellationTokenSource와 함께 사용하여 C# 코드에서 장기 실행 JavaScript 함수를 중단합니다.

다음 JSHelpers 클래스에는 장기 실행 함수인 longRunningFn이 포함되어 있어, AbortController.signalAbortController.abort가 호출되었다고 표시될 때까지 계속 카운트합니다. sleep 함수는 장기 실행 함수의 느린 실행을 시뮬레이션하기 위한 데모용이며, 프로덕션 코드에는 존재하지 않습니다. 구성 요소에서 stopFn을 호출하면 longRunningFnAbortSignal.aborted에서의 while 루프 조건부 검사를 통해 중단 신호를 받습니다.

<script>
  class Helpers {
    static #controller = new AbortController();

    static async #sleep(ms) {
      return new Promise(resolve => setTimeout(resolve, ms));
    }

    static async longRunningFn() {
      var i = 0;
      while (!this.#controller.signal.aborted) {
        i++;
        console.log(`longRunningFn: ${i}`);
        await this.#sleep(1000);
      }
    }

    static stopFn() {
      this.#controller.abort();
      console.log('longRunningFn aborted!');
    }
  }

  window.Helpers = Helpers;
</script>

참고 항목

위치 및 프로덕션 앱에 JS 대한 권장 사항에 대한 일반적인 지침은 ASP.NET Core Blazor 앱의 JavaScript 위치를 참조하세요.

다음 구성 요소는

CallJs12.razor:

@page "/call-js-12"
@inject IJSRuntime JS

<h1>Cancel long-running JS interop</h1>

<p>
    <button @onclick="StartTask">Start Task</button>
    <button @onclick="CancelTask">Cancel Task</button>
</p>

@code {
    private CancellationTokenSource? cts;

    private async Task StartTask()
    {
        cts = new CancellationTokenSource();
        cts.Token.Register(() => JS.InvokeVoidAsync("Helpers.stopFn"));

        await JS.InvokeVoidAsync("Helpers.longRunningFn");
    }

    private void CancelTask()
    {
        cts?.Cancel();
    }

    public void Dispose()
    {
        cts?.Cancel();
        cts?.Dispose();
    }
}

CallJsExample12.razor:

@page "/call-js-example-12"
@inject IJSRuntime JS

<h1>Cancel long-running JS interop</h1>

<p>
    <button @onclick="StartTask">Start Task</button>
    <button @onclick="CancelTask">Cancel Task</button>
</p>

@code {
    private CancellationTokenSource? cts;

    private async Task StartTask()
    {
        cts = new CancellationTokenSource();
        cts.Token.Register(() => JS.InvokeVoidAsync("Helpers.stopFn"));

        await JS.InvokeVoidAsync("Helpers.longRunningFn");
    }

    private void CancelTask()
    {
        cts?.Cancel();
    }

    public void Dispose()
    {
        cts?.Cancel();
        cts?.Dispose();
    }
}

브라우저의 개발자 도구 콘솔에는 Start Task 단추가 선택되면 장기 실행 JS 함수의 실행이 표시됩니다. Cancel Task 단추가 선택된 후 함수가 중단되면

longRunningFn: 1
longRunningFn: 2
longRunningFn: 3
longRunningFn aborted!

JavaScript [JSImport]/[JSExport] interop

이 섹션은 클라이언트 쪽 구성 요소에 적용됩니다.

인터페이스를 기반으로 IJSRuntime 하는 'interop 메커니즘을 JS 사용하여 Blazor클라이언트 쪽 구성 요소에서 JavaScript(JS)와 상호 작용하는 대신.JS[JSImport]/[JSExport]NET 7 이상을 대상으로 하는 앱에서 interop API를 사용할 수 있습니다.

자세한 내용은 JavaScript JSImport/JSExport interop 및 ASP.NET Core Blazor를 참조하세요.

역마샬링된 JavaScript interop

이 섹션은 클라이언트 쪽 구성 요소에 적용됩니다.

IJSUnmarshalledRuntime인터페이스를 사용하는 역마샬링된 interop는 사용되지 않으며 JavaScript [JSImport]/[JSExport] interop로 대체되어야 합니다.

자세한 내용은 JavaScript JSImport/JSExport interop 및 ASP.NET Core Blazor를 참조하세요.

역마샬링된 JavaScript interop

.NET 개체가 JS(JavaScript) interop에 대해 직렬화되고 다음 중 하나가 참인 경우 Blazor WebAssembly 구성 요소의 성능이 저하될 수 있습니다.

  • 많은 양의 .NET 개체가 빠르게 직렬화됩니다. 예를 들어, JS interop 호출이 마우스 휠 회전과 같은 입력 디바이스의 이동을 기반으로 이루어질 경우 성능이 저하될 수 있습니다.
  • 많은 양의 .NET 개체 또는 여러 .NET 개체가 JS interop에 대해 직렬화되어야 합니다. 예를 들어, JS interop 호출이 수십 개의 파일을 직렬화해야 하는 경우 성능이 저하될 수 있습니다.

IJSUnmarshalledObjectReference는 .NET 데이터를 직렬화하는 오버헤드 없이 함수를 호출할 수 있는 JS 개체에 대한 참조를 나타냅니다.

다음 예제에서

  • 문자열과 정수를 포함하는 구조체가 직렬화되지 않은 상태로 JS로 전달됩니다.
  • JS 함수가 데이터를 처리하고 부울 또는 문자열을 호출자에게 반환합니다.
  • string 문자열은 .NET JS 개체로 직접 변환할 수 없습니다. unmarshalledFunctionReturnString 함수가 JS 문자열의 변환을 관리하기 위해 BINDING.js_string_to_mono_string을 호출합니다.

참고 항목

JS로 전달된 구조체의 결과로 구성 요소 성능이 저하되지 않기 때문에 다음 예제는 이 시나리오의 일반적인 사용 사례가 아닙니다. 예제에서는 직렬화되지 않은 .NET 데이터의 전달이라는 개념을 보여 주기 위한 용도로 작은 개체를 사용합니다.

<script>
  window.returnObjectReference = () => {
    return {
      unmarshalledFunctionReturnBoolean: function (fields) {
        const name = Blazor.platform.readStringField(fields, 0);
        const year = Blazor.platform.readInt32Field(fields, 8);

        return name === "Brigadier Alistair Gordon Lethbridge-Stewart" &&
            year === 1968;
      },
      unmarshalledFunctionReturnString: function (fields) {
        const name = Blazor.platform.readStringField(fields, 0);
        const year = Blazor.platform.readInt32Field(fields, 8);

        return BINDING.js_string_to_mono_string(`Hello, ${name} (${year})!`);
      }
    };
  }
</script>

참고 항목

위치 및 프로덕션 앱에 JS 대한 권장 사항에 대한 일반적인 지침은 ASP.NET Core Blazor 앱의 JavaScript 위치를 참조하세요.

Warning

js_string_to_mono_string 함수 이름, 동작 및 존재는 향후 .NET 릴리스에서 변경될 수 있습니다. 예시:

  • 함수의 이름이 바뀔 수 있습니다.
  • 함수가 제거되고 프레임워크에 의한 자동 문자열 변환 기능이 사용될 수 있습니다.

CallJsExample10.razor:

@page "/call-js-example-10"
@using System.Runtime.InteropServices
@using Microsoft.JSInterop
@inject IJSRuntime JS

<h1>Call JS Example 10</h1>

@if (callResultForBoolean)
{
    <p>JS interop was successful!</p>
}

@if (!string.IsNullOrEmpty(callResultForString))
{
    <p>@callResultForString</p>
}

<p>
    <button @onclick="CallJSUnmarshalledForBoolean">
        Call Unmarshalled JS & Return Boolean
    </button>
    <button @onclick="CallJSUnmarshalledForString">
        Call Unmarshalled JS & Return String
    </button>
</p>

<p>
    <a href="https://www.doctorwho.tv">Doctor Who</a>
    is a registered trademark of the <a href="https://www.bbc.com/">BBC</a>.
</p>

@code {
    private bool callResultForBoolean;
    private string? callResultForString;

    private void CallJSUnmarshalledForBoolean()
    {
        var unmarshalledRuntime = (IJSUnmarshalledRuntime)JS;

        var jsUnmarshalledReference = unmarshalledRuntime
            .InvokeUnmarshalled<IJSUnmarshalledObjectReference>(
                "returnObjectReference");

        callResultForBoolean = 
            jsUnmarshalledReference.InvokeUnmarshalled<InteropStruct, bool>(
                "unmarshalledFunctionReturnBoolean", GetStruct());
    }

    private void CallJSUnmarshalledForString()
    {
        var unmarshalledRuntime = (IJSUnmarshalledRuntime)JS;

        var jsUnmarshalledReference = unmarshalledRuntime
            .InvokeUnmarshalled<IJSUnmarshalledObjectReference>(
                "returnObjectReference");

        callResultForString = 
            jsUnmarshalledReference.InvokeUnmarshalled<InteropStruct, string>(
                "unmarshalledFunctionReturnString", GetStruct());
    }

    private InteropStruct GetStruct()
    {
        return new InteropStruct
        {
            Name = "Brigadier Alistair Gordon Lethbridge-Stewart",
            Year = 1968,
        };
    }

    [StructLayout(LayoutKind.Explicit)]
    public struct InteropStruct
    {
        [FieldOffset(0)]
        public string Name;

        [FieldOffset(8)]
        public int Year;
    }
}
@page "/call-js-example-10"
@using System.Runtime.InteropServices
@using Microsoft.JSInterop
@inject IJSRuntime JS

<h1>Call JS Example 10</h1>

@if (callResultForBoolean)
{
    <p>JS interop was successful!</p>
}

@if (!string.IsNullOrEmpty(callResultForString))
{
    <p>@callResultForString</p>
}

<p>
    <button @onclick="CallJSUnmarshalledForBoolean">
        Call Unmarshalled JS & Return Boolean
    </button>
    <button @onclick="CallJSUnmarshalledForString">
        Call Unmarshalled JS & Return String
    </button>
</p>

<p>
    <a href="https://www.doctorwho.tv">Doctor Who</a>
    is a registered trademark of the <a href="https://www.bbc.com/">BBC</a>.
</p>

@code {
    private bool callResultForBoolean;
    private string callResultForString;

    private void CallJSUnmarshalledForBoolean()
    {
        var unmarshalledRuntime = (IJSUnmarshalledRuntime)JS;

        var jsUnmarshalledReference = unmarshalledRuntime
            .InvokeUnmarshalled<IJSUnmarshalledObjectReference>(
                "returnObjectReference");

        callResultForBoolean = 
            jsUnmarshalledReference.InvokeUnmarshalled<InteropStruct, bool>(
                "unmarshalledFunctionReturnBoolean", GetStruct());
    }

    private void CallJSUnmarshalledForString()
    {
        var unmarshalledRuntime = (IJSUnmarshalledRuntime)JS;

        var jsUnmarshalledReference = unmarshalledRuntime
            .InvokeUnmarshalled<IJSUnmarshalledObjectReference>(
                "returnObjectReference");

        callResultForString = 
            jsUnmarshalledReference.InvokeUnmarshalled<InteropStruct, string>(
                "unmarshalledFunctionReturnString", GetStruct());
    }

    private InteropStruct GetStruct()
    {
        return new InteropStruct
        {
            Name = "Brigadier Alistair Gordon Lethbridge-Stewart",
            Year = 1968,
        };
    }

    [StructLayout(LayoutKind.Explicit)]
    public struct InteropStruct
    {
        [FieldOffset(0)]
        public string Name;

        [FieldOffset(8)]
        public int Year;
    }
}

IJSUnmarshalledObjectReference 인스턴스가 C# 코드에서 삭제되지 않을 경우 JS에서 삭제될 수 있습니다. 다음 dispose 함수는 JS에서 호출된 경우 개체 참조를 삭제합니다.

window.exampleJSObjectReferenceNotDisposedInCSharp = () => {
  return {
    dispose: function () {
      DotNet.disposeJSObjectReference(this);
    },

    ...
  };
}

배열 형식이 js_typed_array_to_array를 사용하여 JS 개체에서 .NET으로 변환될 수 있지만 JS 배열은 형식화된 배열이어야 합니다. JS로부터의 배열은 C# 코드에서 .NET 개체 배열(object[])로 읽힐 수 있습니다.

문자열 배열과 같은 기타 데이터 형식은 변환될 수 있지만 이를 위해서는 새 Mono 배열 개체(mono_obj_array_new)를 만들고 값을 설정(mono_obj_array_set)해야 합니다.

Warning

Blazor 프레임워크에서 제공하는 JS 함수(예: js_typed_array_to_array, mono_obj_array_new, mono_obj_array_set)는 .NET의 향후 릴리스에서 이름이 바뀌거나 동작이 변경되거나 제거될 수 있습니다.

JavaScript interop 개체 참조 삭제

JavaScript(JS) interop 문서의 예제는 일반적인 개체 삭제 패턴을 보여 줍니다.

JS interop 개체 참조는 참조를 만드는 interop 호출의 측면에 JS 있는 식별자에 의해 키가 지정된 맵으로 구현됩니다. .NET 또는 JS 쪽 Blazor 에서 개체 삭제가 시작되면 맵에서 항목을 제거하고 개체에 대한 다른 강력한 참조가 없는 한 개체를 가비지 수집할 수 있습니다.

최소한 .NET 관리 메모리 누수 방지를 위해 항상 .NET 쪽에서 만든 개체를 삭제합니다.

구성 요소 삭제 중 DOM 클린up 작업

자세한 내용은 ASP.NET Core Blazor JavaScript 상호 운용성(JS)을 참조하세요.

회로가 없는 JavaScript interop 호출

자세한 내용은 ASP.NET Core Blazor JavaScript 상호 운용성(JS)을 참조하세요.

추가 리소스