.NET WebAssembly의 JavaScript [JSImport]
/[JSExport]
interop
참고 항목
이 문서의 최신 버전은 아닙니다. 현재 릴리스는 이 문서의 .NET 9 버전을 참조 하세요.
Important
이 정보는 상업적으로 출시되기 전에 실질적으로 수정될 수 있는 시험판 제품과 관련이 있습니다. Microsoft는 여기에 제공된 정보에 대해 어떠한 명시적, 또는 묵시적인 보증을 하지 않습니다.
현재 릴리스는 이 문서의 .NET 9 버전을 참조 하세요.
아론 슈메이커
이 문서에서는 API(interop)를 사용하여 JS/[JSImport]
[JSExport]
클라이언트 쪽 WebAssembly에서 JavaScript(JSSystem.Runtime.InteropServices.JavaScript)와 상호 작용하는 방법을 설명합니다.
[JSImport]
/[JSExport]
interop은 다음 시나리오에서 호스트에서 .NET WebAssembly 모듈을 JS 실행할 때 적용됩니다.
- WebAssembly 브라우저 앱 프로젝트와 JavaScript '[JSImport]'/'[JSExport]' interop입니다.
- ASP.NET Core Blazor와 JavaScript JSImport/JSExport interop.
- interop을 지원하는
[JSImport]
/[JSExport]
기타 .NET WebAssembly 플랫폼입니다.
필수 조건
다음 프로젝트 유형 중 어느 것이든지:
- WebAssembly 브라우저 앱 프로젝트와 JavaScript '[JSImport]'/'[JSExport]' interop에 따라 만들어진 WebAssembly 브라우저 앱 프로젝트입니다.
- Blazor ASP.NET CoreBlazor와 JavaScript JSImport/JSExport interop에 따라 만들어진 클라이언트 쪽 프로젝트입니다.
- interop(System.Runtime.InteropServices.JavaScriptAPI)를 지원하는
[JSExport]
[JSImport]
/상용 또는 오픈 소스 플랫폼용으로 만든 프로젝트입니다.
샘플 앱
샘플 코드 보기 또는 다운로드(다운로드 방법): 채택 중인 .NET 버전과 일치하는 8.0 이상 버전 폴더를 선택합니다. 버전 폴더 내에서 이름이 인 WASMBrowserAppImportExportInterop
샘플에 액세스합니다.
JS 특성을 사용하는 [JSImport]
/[JSExport]
interop
[JSImport]
.NET 메서드가 호출될 때 해당 JS 메서드를 호출해야 함을 나타내기 위해 .NET 메서드에 특성이 적용됩니다. 이렇게 하면 .NET 개발자가 .NET 코드를 호출할 수 있는 "가져오기"를 정의할 수 있습니다 JS. Action 또한 매개 변수로 전달될 수 있으며 JS 콜백 또는 이벤트 구독 패턴을 지원하는 작업을 호출할 수 있습니다.
특성은 [JSExport]
.NET 메서드에 적용되어 코드에 노출됩니다 JS . 이렇게 하면 코드에서 JS .NET 메서드에 대한 호출을 시작할 수 있습니다.
메서드 가져오기 JS
다음 예제에서는 표준 기본 제공 JS 메서드(console.log
)를 C#으로 가져옵니다. [JSImport]
는 전역적으로 액세스할 수 있는 개체의 메서드를 가져오는 것으로 제한됩니다. 예를 들어 전역 log
적으로 액세스할 수 있는 개체에 console
정의된 개체 globalThis
에 정의된 메서드입니다. 이 console.log
메서드는 로그 메시지에 대한 문자열을 허용하는 C# 프록시 메서드 ConsoleLog
에 매핑됩니다.
public partial class GlobalInterop
{
[JSImport("globalThis.console.log")]
public static partial void ConsoleLog(string text);
}
ConsoleLog
에서 Program.Main
로깅할 메시지와 함께 호출됩니다.
GlobalInterop.ConsoleLog("Hello World!");
출력이 브라우저의 콘솔에 나타납니다.
다음은 에 선언된 메서드를 가져오는 방법을 보여 줍니다 JS.
다음 사용자 지정 JS 메서드(globalThis.callAlert
)는 메시지가 전달된 경고 대화 상자(window.alert
)를 생성합니다.text
globalThis.callAlert = function (text) {
globalThis.window.alert(text);
}
이 globalThis.callAlert
메서드는 메시지에 대한 문자열을 허용하는 C# 프록시 메서드(CallAlert
)에 매핑됩니다.
using System.Runtime.InteropServices.JavaScript;
public partial class GlobalInterop
{
[JSImport("globalThis.callAlert")]
public static partial void CallAlert(string text);
}
에서 Program.Main
경고 CallAlert
대화 상자 메시지의 텍스트를 전달하여 호출됩니다.
GlobalInterop.CallAlert("Hello World");
메서드를 선언하는 [JSImport]
C# 클래스에는 구현이 없습니다. 컴파일 시 소스 생성 부분 클래스에는 해당 JS 메서드를 호출하기 위해 호출 및 형식의 마샬링을 구현하는 .NET 코드가 포함됩니다. Visual Studio에서 정의로 이동 또는 구현으로 이동 옵션을 사용하면 각각 원본에서 생성된 partial 클래스 또는 개발자 정의 partial 클래스로 이동합니다.
앞의 예제에서 중간 globalThis.callAlert
JS 선언은 기존 코드를 래핑하는 JS 데 사용됩니다. 이 문서에서는 비공식적으로 중간 JS 선언을 shim이라고 JS 합니다. JS shim은 .NET 구현과 기존 기능/라이브러리 간의 간격을 JS 채웁니다. 앞의 간단한 예제와 같은 많은 경우 shim은 필요하지 않으며 이전 ConsoleLog
예제 JS 에서 설명한 대로 메서드를 직접 가져올 수 있습니다. 이 문서에서 설명한 대로 향후 섹션에서 설명한 것처럼 shim은 다음을 JS 수행할 수 있습니다.
- 추가 논리를 캡슐화합니다.
- 수동으로 형식을 매핑합니다.
- interop 경계를 통과하는 개체 또는 호출 수를 줄입니다.
- 인스턴스 메서드에 정적 호출을 수동으로 매핑합니다.
JavaScript 선언 로드
JS 가져올 선언은 [JSImport]
일반적으로 .NET WebAssembly를 로드한 동일한 페이지 또는 JS 호스트의 컨텍스트에서 로드됩니다. 이 작업은 다음을 사용하여 수행할 수 있습니다.
<script>...</script>
인라인을 선언하는 블록입니다JS.- 외부 파일(
src
)을 로드하는 스크립트 소스(<script src="./some.js"></script>
) 선언(.js
)JS입니다. - JS ES6 모듈(
<script type='module' src="./moduleName.js"></script>
). - JS.NET WebAssembly에서 사용하여 JSHost.ImportAsync 로드된 ES6 모듈입니다.
이 문서의 예제에서는 .를 사용합니다 JSHost.ImportAsync. 호출ImportAsync할 때 클라이언트 쪽 .NET WebAssembly는 매개 변수를 사용하여 moduleUrl
파일을 요청하므로 태그가 URL이 있는 파일을 src
검색하는 것과 거의 동일한 방식으로 파일에 정적 웹 자산으로 <script>
액세스할 수 있어야 합니다. 예를 들어 WebAssembly 브라우저 앱 프로젝트 내의 다음 C# 코드는 경로/wwwroot/scripts/ExampleShim.js
에 JS 파일(.js
)을 유지 관리합니다.
await JSHost.ImportAsync("ExampleShim", "/scripts/ExampleShim.js");
WebAssembly를 로드하는 플랫폼에 따라 WebAssembly 패키지가 아래의 프레임워크 스크립트/_framework/
에 의해 초기화되기 때문에 점 접두사 URL(예./scripts/
: /_framework/scripts/
)이 잘못된 하위 디렉터리를 참조할 수 있습니다. 이 경우 URL ../scripts/
접두사를 올바른 경로를 참조합니다. 사이트가 도메인의 루트에서 호스트되는 경우 접두 /scripts/
사를 사용할 수 있습니다. 일반적인 방법은 HTML <base>
태그를 사용하여 지정된 환경에 대한 올바른 기본 경로를 구성하고 접두사를 사용하여 /scripts/
기본 경로를 기준으로 경로를 참조하는 것입니다. 타일드 표기법 ~/
접두사는 .에서 JSHost.ImportAsync지원되지 않습니다.
Important
JavaScript 모듈 [JSImport]
에서 로드되는 경우 JS 특성은 모듈 이름을 두 번째 매개 변수로 포함해야 합니다. 예를 들어 가져온 [JSImport("globalThis.callAlert", "ExampleShim")]
메서드가 "ExampleShim
"라는 JavaScript 모듈에서 선언되었음을 나타냅니다.
형식 매핑
고유한 매핑이 지원되는 경우 .NET 메서드 시그니처의 매개 변수 및 반환 형식은 런타임 시 적절한 JS 형식으로 자동 변환됩니다. 이로 인해 값 또는 참조가 프록시 형식으로 래핑된 값으로 변환될 수 있습니다. 이 프로세스를 형식 마샬링이라고 합니다. 가져온 메서드 매개 변수 및 반환 형식을 마샬링하는 방법을 제어하는 데 사용합니다 JSMarshalAsAttribute<T> .
일부 형식에는 기본 형식 매핑이 없습니다. 예를 들어 long
컴파일 시간 오류를 방지하려면 마샬링할 System.Runtime.InteropServices.JavaScript.JSType.BigIntSystem.Runtime.InteropServices.JavaScript.JSType.Number 수 있습니다JSMarshalAsAttribute<T>.
지원되는 형식 매핑 시나리오는 다음과 같습니다.
- 전달 Action 또는 Func<TResult> 매개 변수로, 호출 가능한 JS 메서드로 마샬링됩니다. 이렇게 하면 .NET 코드가 콜백 또는 이벤트에 대한 응답으로 수신기를 호출할 JS 수 있습니다.
- 프록시 개체로 마샬링되고 프록시가 가비지 수집될 때까지 interop 경계를 넘어 활성 상태로 유지되는 참조 및 .NET 관리 개체 참조를 어느 방향으로든 전달 JS 합니다.
- 비동 JS 기 메서드 또는 JS
Promise
결과와 함께 Task 마샬링하고 그 반대의 경우도 마찬가지입니다.
마샬링된 형식의 대부분은 가져온 메서드와 내보낸 메서드 모두에서 매개 변수 및 반환 값으로 양방향으로 작동합니다.
다음 표에서는 지원되는 형식 매핑을 보여줍니다.
.NET | JavaScript | Nullable |
Task ➔받는 사람 Promise |
JSMarshalAs 선택사항 |
Array of |
---|---|---|---|---|---|
Boolean |
Boolean |
지원됨 | 지원됨 | 지원됨 | 지원되지 않음 |
Byte |
Number |
지원됨 | 지원됨 | 지원됨 | 지원됨 |
Char |
String |
지원됨 | 지원됨 | 지원됨 | 지원되지 않음 |
Int16 |
Number |
지원됨 | 지원됨 | 지원됨 | 지원되지 않음 |
Int32 |
Number |
지원됨 | 지원됨 | 지원됨 | 지원됨 |
Int64 |
Number |
지원됨 | 지원됨 | 지원되지 않음 | 지원되지 않음 |
Int64 |
BigInt |
지원됨 | 지원됨 | 지원되지 않음 | 지원되지 않음 |
Single |
Number |
지원됨 | 지원됨 | 지원됨 | 지원되지 않음 |
Double |
Number |
지원됨 | 지원됨 | 지원됨 | 지원됨 |
IntPtr |
Number |
지원됨 | 지원됨 | 지원됨 | 지원되지 않음 |
DateTime |
Date |
지원됨 | 지원됨 | 지원되지 않음 | 지원되지 않음 |
DateTimeOffset |
Date |
지원됨 | 지원됨 | 지원되지 않음 | 지원되지 않음 |
Exception |
Error |
지원되지 않음 | 지원됨 | 지원됨 | 지원되지 않음 |
JSObject |
Object |
지원되지 않음 | 지원됨 | 지원됨 | 지원됨 |
String |
String |
지원되지 않음 | 지원됨 | 지원됨 | 지원됨 |
Object |
Any |
지원되지 않음 | 지원됨 | 지원되지 않음 | 지원됨 |
Span<Byte> |
MemoryView |
지원되지 않음 | 지원되지 않음 | 지원되지 않음 | 지원되지 않음 |
Span<Int32> |
MemoryView |
지원되지 않음 | 지원되지 않음 | 지원되지 않음 | 지원되지 않음 |
Span<Double> |
MemoryView |
지원되지 않음 | 지원되지 않음 | 지원되지 않음 | 지원되지 않음 |
ArraySegment<Byte> |
MemoryView |
지원되지 않음 | 지원되지 않음 | 지원되지 않음 | 지원되지 않음 |
ArraySegment<Int32> |
MemoryView |
지원되지 않음 | 지원되지 않음 | 지원되지 않음 | 지원되지 않음 |
ArraySegment<Double> |
MemoryView |
지원되지 않음 | 지원되지 않음 | 지원되지 않음 | 지원되지 않음 |
Task |
Promise |
지원되지 않음 | 지원되지 않음 | 지원됨 | 지원되지 않음 |
Action |
Function |
지원되지 않음 | 지원되지 않음 | 지원되지 않음 | 지원되지 않음 |
Action<T1> |
Function |
지원되지 않음 | 지원되지 않음 | 지원되지 않음 | 지원되지 않음 |
Action<T1, T2> |
Function |
지원되지 않음 | 지원되지 않음 | 지원되지 않음 | 지원되지 않음 |
Action<T1, T2, T3> |
Function |
지원되지 않음 | 지원되지 않음 | 지원되지 않음 | 지원되지 않음 |
Func<TResult> |
Function |
지원되지 않음 | 지원되지 않음 | 지원되지 않음 | 지원되지 않음 |
Func<T1, TResult> |
Function |
지원되지 않음 | 지원되지 않음 | 지원되지 않음 | 지원되지 않음 |
Func<T1, T2, TResult> |
Function |
지원되지 않음 | 지원되지 않음 | 지원되지 않음 | 지원되지 않음 |
Func<T1, T2, T3, TResult> |
Function |
지원되지 않음 | 지원되지 않음 | 지원되지 않음 | 지원되지 않음 |
다음 조건은 형식 매핑 및 마샬링된 값에 적용됩니다.
Array of
열은 .NET 형식을 JSArray
로 마샬링할 수 있는지를 나타냅니다. 예:Number
의 JSArray
에 매핑된C#int[]
(Int32
).- 잘못된 형식의 값을 사용하여 C#에 JS 값을 전달할 때, 프레임워크는 대부분의 경우 예외를 throw합니다. 프레임워크는 JS에서 컴파일 시간 형식 검사를 수행하지 않습니다.
JSObject
,Exception
,Task
,ArraySegment
는GCHandle
및 프록시를 만듭니다. 개발자 코드에서 삭제를 트리거하거나 .NET GC(가비지 수집)가 나중에 개체를 삭제하도록 허용할 수 있습니다. 이러한 형식은 상당한 성능 오버헤드를 수행합니다.Array
: 배열을 마샬링하면 JS 또는 .NET에서 배열의 복사본이 만들어집니다.MemoryView
MemoryView
는Span
및ArraySegment
를 마샬링하는 .NET WebAssembly 런타임에 대한 JS 클래스입니다.- 배열 마샬링과 달리
Span
또는ArraySegment
를 마샬링해도 기본 메모리의 복사본이 만들어지지 않습니다. MemoryView
는 .NET WebAssembly 런타임에서만 올바르게 인스턴스화할 수 있습니다. 따라서 매개 변수Span
가 있는ArraySegment
.NET 메서드로 메서드를 가져올 JS 수 없습니다.Span
에 대해 만들어진MemoryView
는 interop 호출 기간 동안에만 유효합니다. interop 호출 후에도 유지되지 않는 호출 스택에Span
이 할당되기 때문에,Span
를 반환 하는 .NET 메서드를 내보낼 수 없습니다.ArraySegment
에 대해 만들어진MemoryView
는 interop 호출 후에 유지되며 버퍼를 공유하는 데 유용합니다.ArraySegment
에 대해 만들어진MemoryView
에서dispose()
를 호출하면 프록시가 삭제되고 기본 .NET 배열의 고정이 해제됩니다.MemoryView
에 대한try-finally
블록에서dispose()
를 호출하는 것이 좋습니다.
중첩된 제네릭 형식 JSMarshalAs
이 필요한 형식 매핑의 일부 조합은 현재 지원되지 않습니다. 예를 들어 다음과 같은 [return: JSMarshalAs<JSType.Promise<JSType.Array<JSType.Number>>>()]
배열을 Promise
구체화하려고 시도하면 컴파일 시간 오류가 생성됩니다. 적절한 해결 방법은 시나리오에 따라 다르지만 이 특정 시나리오는 형식 매핑 제한 섹션에서 자세히 설명 합니다 .
JS 프리미티브
다음 예제에서는 여러 기본 JS 형식의 형식 매핑을 활용하고 컴파일 시간에 명시적 매핑이 필요한 경우의 JSMarshalAs
사용을 보여 [JSImport]
줍니다.
PrimitivesShim.js
:
globalThis.counter = 0;
// Takes no parameters and returns nothing.
export function incrementCounter() {
globalThis.counter += 1;
};
// Returns an int.
export function getCounter() { return globalThis.counter; };
// Takes a parameter and returns nothing. JS doesn't restrict the parameter type,
// but we can restrict it in the .NET proxy, if desired.
export function logValue(value) { console.log(value); };
// Called for various .NET types to demonstrate mapping to JS primitive types.
export function logValueAndType(value) { console.log(typeof value, value); };
PrimitivesInterop.cs
:
using System;
using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;
public partial class PrimitivesInterop
{
// Importing an existing JS method.
[JSImport("globalThis.console.log")]
public static partial void ConsoleLog([JSMarshalAs<JSType.Any>] object value);
// Importing static methods from a JS module.
[JSImport("incrementCounter", "PrimitivesShim")]
public static partial void IncrementCounter();
[JSImport("getCounter", "PrimitivesShim")]
public static partial int GetCounter();
// The JS shim method name isn't required to match the C# method name.
[JSImport("logValue", "PrimitivesShim")]
public static partial void LogInt(int value);
// A second mapping to the same JS method with compatible type.
[JSImport("logValue", "PrimitivesShim")]
public static partial void LogString(string value);
// Accept any type as parameter. .NET types are mapped to JS types where
// possible. Otherwise, they're marshalled as an untyped object reference
// to the .NET object proxy. The JS implementation logs to browser console
// the JS type and value to demonstrate results of marshalling.
[JSImport("logValueAndType", "PrimitivesShim")]
public static partial void LogValueAndType(
[JSMarshalAs<JSType.Any>] object value);
// Some types have multiple mappings and require explicit marshalling to the
// desired JS type. A long/Int64 can be mapped as either a Number or BigInt.
// Passing a long value to the above method generates an error at runtime:
// "ToJS for System.Int64 is not implemented." ("ToJS" means "to JavaScript")
// If the parameter declaration `Method(JSMarshalAs<JSType.Any>] long value)`
// is used, a compile-time error is generated:
// "Type long is not supported by source-generated JS interop...."
// Instead, explicitly map the long parameter to either a JSType.Number or
// JSType.BigInt. Note that runtime overflow errors are possible in JS if the
// C# value is too large.
[JSImport("logValueAndType", "PrimitivesShim")]
public static partial void LogValueAndTypeForNumber(
[JSMarshalAs<JSType.Number>] long value);
[JSImport("logValueAndType", "PrimitivesShim")]
public static partial void LogValueAndTypeForBigInt(
[JSMarshalAs<JSType.BigInt>] long value);
}
public static class PrimitivesUsage
{
public static async Task Run()
{
// Ensure JS module loaded.
await JSHost.ImportAsync("PrimitivesShim", "/PrimitivesShim.js");
// Call a proxy to a static JS method, console.log().
PrimitivesInterop.ConsoleLog("Printed from JSImport of console.log()");
// Basic examples of JS interop with an integer.
PrimitivesInterop.IncrementCounter();
int counterValue = PrimitivesInterop.GetCounter();
PrimitivesInterop.LogInt(counterValue);
PrimitivesInterop.LogString("I'm a string from .NET in your browser!");
// Mapping some other .NET types to JS primitives.
PrimitivesInterop.LogValueAndType(true);
PrimitivesInterop.LogValueAndType(0x3A); // Byte literal
PrimitivesInterop.LogValueAndType('C');
PrimitivesInterop.LogValueAndType((Int16)12);
// JS Number has a lower max value and can generate overflow errors.
PrimitivesInterop.LogValueAndTypeForNumber(9007199254740990L); // Int64/Long
// Next line: Int64/Long, JS BigInt supports larger numbers.
PrimitivesInterop.LogValueAndTypeForBigInt(1234567890123456789L);//
PrimitivesInterop.LogValueAndType(3.14f); // Single floating point literal
PrimitivesInterop.LogValueAndType(3.14d); // Double floating point literal
PrimitivesInterop.LogValueAndType("A string");
}
}
Program.Main
의 경우
await PrimitivesUsage.Run();
앞의 예제에서는 브라우저의 디버그 콘솔에 다음 출력을 표시합니다.
Printed from JSImport of console.log()
1
I'm a string from .NET in your browser!
boolean true
number 58
number 67
number 12
number 9007199254740990
bigint 1234567890123456789n
number 3.140000104904175
number 3.14
string A string
JSDate
개체입니다.
이 섹션의 예제에서는 개체를 반환 또는 매개 변수로 사용하는 JS Date
메서드를 가져오는 방법을 보여 줍니다. 날짜는 interop에서 값별로 마샬링되므로 기본 형식과 JS 거의 동일한 방식으로 복사됩니다.
Date
개체는 표준 시간대에 구애받지 않습니다. .NET DateTime 은 마샬링할 DateTimeKind 때 상대적으로 Date
조정되지만 표준 시간대 정보는 보존되지 않습니다. 나타내는 값으로 DateTimeKind.Utc 초기화하거나 DateTimeKind.Local 일치하는 값을 초기화하는 DateTime 것이 좋습니다.
DateShim.js
:
export function incrementDay(date) {
date.setDate(date.getDate() + 1);
return date;
}
export function logValueAndType(value) {
console.log("Date:", value)
}
DateInterop.cs
:
using System;
using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;
public partial class DateInterop
{
[JSImport("incrementDay", "DateShim")]
[return: JSMarshalAs<JSType.Date>] // Explicit JSMarshalAs for a return type
public static partial DateTime IncrementDay(
[JSMarshalAs<JSType.Date>] DateTime date);
[JSImport("logValueAndType", "DateShim")]
public static partial void LogValueAndType(
[JSMarshalAs<JSType.Date>] DateTime value);
}
public static class DateUsage
{
public static async Task Run()
{
// Ensure JS module loaded.
await JSHost.ImportAsync("DateShim", "/DateShim.js");
// Basic examples of interop with a C# DateTime and JS Date.
DateTime date = new(1968, 12, 21, 12, 51, 0, DateTimeKind.Utc);
DateInterop.LogValueAndType(date);
date = DateInterop.IncrementDay(date);
DateInterop.LogValueAndType(date);
}
}
Program.Main
의 경우
await DateUsage.Run();
앞의 예제에서는 브라우저의 디버그 콘솔에 다음 출력을 표시합니다.
Date: Sat Dec 21 1968 07:51:00 GMT-0500 (Eastern Standard Time)
Date: Sun Dec 22 1968 07:51:00 GMT-0500 (Eastern Standard Time)
앞의 표준 시간대 정보(GMT-0500 (Eastern Standard Time)
)는 컴퓨터/브라우저의 현지 표준 시간대에 따라 달라집니다.
JS 개체 참조
메서드가 JS 개체 참조를 반환할 때마다 .NET에서 .NET으로 JSObject표시됩니다. 원래 JS 개체는 경계 내에서 JS 수명을 계속하지만 .NET 코드는 .NET 코드를 통해 참조를 통해 JSObject액세스하고 수정할 수 있습니다. 형식 자체는 제한된 API를 노출하지만 개체 참조를 유지하고 JS interop 경계를 넘어 반환하거나 전달하는 기능을 통해 여러 interop 시나리오를 지원할 수 있습니다.
속성 JSObject 에 액세스하는 메서드를 제공하지만 인스턴스 메서드에 대한 직접 액세스는 제공하지 않습니다. 다음 Summarize
메서드에서 알 수 있듯이 인스턴스를 매개 변수로 사용하는 정적 메서드를 구현하여 인스턴스 메서드에 간접적으로 액세스할 수 있습니다.
JSObjectShim.js
:
export function createObject() {
return {
name: "Example JS Object",
answer: 41,
question: null,
summarize: function () {
return `Question: "${this.question}" Answer: ${this.answer}`;
}
};
}
export function incrementAnswer(object) {
object.answer += 1;
// Don't return the modified object, since the reference is modified.
}
// Proxy an instance method call.
export function summarize(object) {
return object.summarize();
}
JSObjectInterop.cs
:
using System;
using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;
public partial class JSObjectInterop
{
[JSImport("createObject", "JSObjectShim")]
public static partial JSObject CreateObject();
[JSImport("incrementAnswer", "JSObjectShim")]
public static partial void IncrementAnswer(JSObject jsObject);
[JSImport("summarize", "JSObjectShim")]
public static partial string Summarize(JSObject jsObject);
[JSImport("globalThis.console.log")]
public static partial void ConsoleLog([JSMarshalAs<JSType.Any>] object value);
}
public static class JSObjectUsage
{
public static async Task Run()
{
await JSHost.ImportAsync("JSObjectShim", "/JSObjectShim.js");
JSObject jsObject = JSObjectInterop.CreateObject();
JSObjectInterop.ConsoleLog(jsObject);
JSObjectInterop.IncrementAnswer(jsObject);
// An updated object isn't retrieved. The change is reflected in the
// existing instance.
JSObjectInterop.ConsoleLog(jsObject);
// JSObject exposes several methods for interacting with properties.
jsObject.SetProperty("question", "What is the answer?");
JSObjectInterop.ConsoleLog(jsObject);
// We can't directly JSImport an instance method on the jsObject, but we
// can pass the object reference and have the JS shim call the instance
// method.
string summary = JSObjectInterop.Summarize(jsObject);
Console.WriteLine("Summary: " + summary);
}
}
Program.Main
의 경우
await JSObjectUsage.Run();
앞의 예제에서는 브라우저의 디버그 콘솔에 다음 출력을 표시합니다.
{name: 'Example JS Object', answer: 41, question: null, Symbol(wasm cs_owned_js_handle): 5, summarize: ƒ}
{name: 'Example JS Object', answer: 42, question: null, Symbol(wasm cs_owned_js_handle): 5, summarize: ƒ}
{name: 'Example JS Object', answer: 42, question: 'What is the answer?', Symbol(wasm cs_owned_js_handle): 5, summarize: ƒ}
Summary: Question: "What is the answer?" Answer: 42
비동기 interop
많은 JS API는 콜백, Promise
또는 비동기 메서드를 통해 비동기 및 신호 완성입니다. 비동기 기능 무시는 종종 옵션이 아닙니다. 이후 코드는 비동기 작업의 완료에 따라 달라질 수 있으므로 대기해야 합니다.
JS를 반환하는 Promise
메서드를 async
통해 C#에서 키워드를 사용하거나 반환하는 Task메서드를 대기할 수 있습니다. 아래에 async
설명된 것처럼 키워드는 C# 메서드에서 특성과 함께 [JSImport]
사용되지 않습니다. 키워드는 해당 키워드를 사용하지 await
않기 때문입니다. 그러나 메서드를 호출하는 코드를 사용하면 일반적으로 키워드를 await
사용하고 예제에 PromisesUsage
설명된 대로 키워드로 async
표시됩니다.
JS콜백(예: 콜백)을 사용하여 setTimeout
반환JS하기 전에 래 Promise
핑할 수 있습니다. 콜백을 Promise
할당된 함수에 설명된 대로 콜백에 래핑하는 Wait2Seconds
것은 콜백이 정확히 한 번 호출될 때만 적합합니다. 그렇지 않으면 이벤트 구독 섹션에서 보여 주는 0번 또는 여러 번 호출될 수 있는 콜백을 수신 대기하기 위해 JS C# Action 을 전달할 수 있습니다.
PromisesShim.js
:
export function wait2Seconds() {
// This also demonstrates wrapping a callback-based API in a promise to
// make it awaitable.
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(); // Resolve promise after 2 seconds
}, 2000);
});
}
// Return a value via resolve in a promise.
export function waitGetString() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve("String From Resolve"); // Return a string via promise
}, 500);
});
}
export function waitGetDate() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(new Date('1988-11-24')); // Return a date via promise
}, 500);
});
}
// Demonstrates an awaitable fetch.
export function fetchCurrentUrl() {
// This method returns the promise returned by .then(*.text())
// and .NET awaits the returned promise.
return fetch(globalThis.window.location, { method: 'GET' })
.then(response => response.text());
}
// .NET can await JS methods using the async/await JS syntax.
export async function asyncFunction() {
await wait2Seconds();
}
// A Promise.reject can be used to signal failure and is bubbled to .NET code
// as a JSException.
export function conditionalSuccess(shouldSucceed) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (shouldSucceed)
resolve(); // Success
else
reject("Reject: ShouldSucceed == false"); // Failure
}, 500);
});
}
C# 메서드 서명에는 키워드를 사용하지 async
마세요. 반환하거나 Task Task<TResult> 충분합니다.
비동 JS 기 메서드를 호출할 때 메서드 실행이 JS 완료될 때까지 기다리는 경우가 많습니다. 리소스를 로드하거나 요청하는 경우 다음 코드에서 작업이 완료된 것으로 가정할 수 있습니다.
shim이 JS 반환되는 Promise
경우 C#은 이를 대기 가능한 Task/Task<TResult>것으로 처리할 수 있습니다.
PromisesInterop.cs
:
using System;
using System.Diagnostics;
using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;
public partial class PromisesInterop
{
// For a promise with void return type, declare a Task return type:
[JSImport("wait2Seconds", "PromisesShim")]
public static partial Task Wait2Seconds();
[JSImport("waitGetString", "PromisesShim")]
public static partial Task<string> WaitGetString();
// Some return types require a [return: JSMarshalAs...] declaring the
// Promise's return type corresponding to Task<T>.
[JSImport("waitGetDate", "PromisesShim")]
[return: JSMarshalAs<JSType.Promise<JSType.Date>>()]
public static partial Task<DateTime> WaitGetDate();
[JSImport("fetchCurrentUrl", "PromisesShim")]
public static partial Task<string> FetchCurrentUrl();
[JSImport("asyncFunction", "PromisesShim")]
public static partial Task AsyncFunction();
[JSImport("conditionalSuccess", "PromisesShim")]
public static partial Task ConditionalSuccess(bool shouldSucceed);
}
public static class PromisesUsage
{
public static async Task Run()
{
await JSHost.ImportAsync("PromisesShim", "/PromisesShim.js");
Stopwatch sw = new();
sw.Start();
await PromisesInterop.Wait2Seconds(); // Await Promise
Console.WriteLine($"Waited {sw.Elapsed.TotalSeconds:#.0}s.");
sw.Restart();
string str =
await PromisesInterop.WaitGetString(); // Await promise (string return)
Console.WriteLine(
$"Waited {sw.Elapsed.TotalSeconds:#.0}s for WaitGetString: '{str}'");
sw.Restart();
// Await promise with string return.
DateTime date = await PromisesInterop.WaitGetDate();
Console.WriteLine(
$"Waited {sw.Elapsed.TotalSeconds:#.0}s for WaitGetDate: '{date}'");
// Await a JS fetch.
string responseText = await PromisesInterop.FetchCurrentUrl();
Console.WriteLine($"responseText.Length: {responseText.Length}");
sw.Restart();
await PromisesInterop.AsyncFunction(); // Await an async JS method
Console.WriteLine(
$"Waited {sw.Elapsed.TotalSeconds:#.0}s for AsyncFunction.");
try
{
// Handle a promise rejection. Await an async JS method.
await PromisesInterop.ConditionalSuccess(shouldSucceed: false);
}
catch (JSException ex) // Catch JS exception
{
Console.WriteLine($"JS Exception Caught: '{ex.Message}'");
}
}
}
Program.Main
의 경우
await PromisesUsage.Run();
앞의 예제에서는 브라우저의 디버그 콘솔에 다음 출력을 표시합니다.
Waited 2.0s.
Waited .5s for WaitGetString: 'String From Resolve'
Waited .5s for WaitGetDate: '11/24/1988 12:00:00 AM'
responseText.Length: 582
Waited 2.0s for AsyncFunction.
JS Exception Caught: 'Reject: ShouldSucceed == false'
형식 매핑 제한 사항
정의에 중첩된 제네릭 형식이 필요한 일부 형식 JSMarshalAs
매핑은 현재 지원되지 않습니다. 예를 들어 배열에 Promise
대한 [return: JSMarshalAs<JSType.Promise<JSType.Array<JSType.Number>>>()]
반환은 컴파일 시간 오류를 생성합니다. 적절한 해결 방법은 시나리오에 따라 다르지만 한 가지 옵션은 배열을 참조로 JSObject 나타내는 것입니다. .NET 내의 개별 요소에 액세스할 필요가 없고 배열에 대해 작동하는 다른 JS 메서드에 참조를 전달할 수 있으면 충분할 수 있습니다. 또는 전용 메서드는 다음 UnwrapJSObjectAsIntArray
예제와 같이 참조를 JSObject 매개 변수로 사용하고 구체화된 배열을 반환할 수 있습니다. 이 경우 JS 메서드에는 형식 검사가 없으며 개발자는 적절한 배열 형식 래핑이 전달되는지 확인해야 JSObject 합니다.
export function waitGetIntArrayAsObject() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve([1, 2, 3, 4, 5]); // Return an array from the Promise
}, 500);
});
}
export function unwrapJSObjectAsIntArray(jsObject) {
return jsObject;
}
// Not supported, generates compile-time error.
// [JSImport("waitGetArray", "PromisesShim")]
// [return: JSMarshalAs<JSType.Promise<JSType.Array<JSType.Number>>>()]
// public static partial Task<int[]> WaitGetIntArray();
// Workaround, take the return the call and pass it to UnwrapJSObjectAsIntArray.
// Return a JSObject reference to a JS number array.
[JSImport("waitGetIntArrayAsObject", "PromisesShim")]
[return: JSMarshalAs<JSType.Promise<JSType.Object>>()]
public static partial Task<JSObject> WaitGetIntArrayAsObject();
// Takes a JSObject reference to a JS number array, and returns the array as a C#
// int array.
[JSImport("unwrapJSObjectAsIntArray", "PromisesShim")]
[return: JSMarshalAs<JSType.Array<JSType.Number>>()]
public static partial int[] UnwrapJSObjectAsIntArray(JSObject intArray);
//...
Program.Main
의 경우
JSObject arrayAsJSObject = await PromisesInterop.WaitGetIntArrayAsObject();
int[] intArray = PromisesInterop.UnwrapJSObjectAsIntArray(arrayAsJSObject);
성능 고려 사항
호출 마샬링 및 interop 경계를 넘어 개체를 추적하는 오버헤드는 네이티브 .NET 작업보다 비용이 많이 들지만, 여전히 수요가 보통인 일반적인 웹앱에 대해 허용 가능한 성능을 보여 주어야 합니다.
interop 경계를 넘어 참조를 유지하는 개체 JSObject프록시는 추가 메모리 오버헤드를 가지며 가비지 수집이 이러한 개체에 미치는 영향에 영향을 줍니다. 또한 일부 시나리오에서 가비지 수집을 트리거하지 않고 사용 가능한 메모리 JS 가 소진될 수 있습니다. 이 위험은 상대적으로 작은 JS 개체에 의해 interop 경계를 넘어 과도한 수의 큰 개체를 참조하거나 프록시에서 큰 .NET 개체를 참조 JS 하는 경우도 마찬가지입니다. 이러한 경우 개체에 대한 JS 인터페이스를 활용하는 IDisposable 범위가 있는 using
결정적 삭제 패턴을 따르는 것이 좋습니다.
앞의 예제 코드를 활용하는 다음 벤치마크는 interop 작업이 .NET 경계 내에 남아 있지만 interop 작업은 상대적으로 빠르게 유지되는 작업보다 대략 더 느린 순서임을 보여 줍니다. 또한 사용자의 디바이스 기능이 성능에 영향을 미친다는 점을 고려합니다.
JSObjectBenchmark.cs
:
using System;
using System.Diagnostics;
public static class JSObjectBenchmark
{
public static void Run()
{
Stopwatch sw = new();
var jsObject = JSObjectInterop.CreateObject();
sw.Start();
for (int i = 0; i < 1000000; i++)
{
JSObjectInterop.IncrementAnswer(jsObject);
}
sw.Stop();
Console.WriteLine(
$"JS interop elapsed time: {sw.Elapsed.TotalSeconds:#.0000} seconds " +
$"at {sw.Elapsed.TotalMilliseconds / 1000000d:#.000000} ms per " +
"operation");
var pocoObject =
new PocoObject { Question = "What is the answer?", Answer = 41 };
sw.Restart();
for (int i = 0; i < 1000000; i++)
{
pocoObject.IncrementAnswer();
}
sw.Stop();
Console.WriteLine($".NET elapsed time: {sw.Elapsed.TotalSeconds:#.0000} " +
$"seconds at {sw.Elapsed.TotalMilliseconds / 1000000d:#.000000} ms " +
"per operation");
Console.WriteLine($"Begin Object Creation");
sw.Restart();
for (int i = 0; i < 1000000; i++)
{
var jsObject2 = JSObjectInterop.CreateObject();
JSObjectInterop.IncrementAnswer(jsObject2);
}
sw.Stop();
Console.WriteLine(
$"JS interop elapsed time: {sw.Elapsed.TotalSeconds:#.0000} seconds " +
$"at {sw.Elapsed.TotalMilliseconds / 1000000d:#.000000} ms per " +
"operation");
sw.Restart();
for (int i = 0; i < 1000000; i++)
{
var pocoObject2 =
new PocoObject { Question = "What is the answer?", Answer = 0 };
pocoObject2.IncrementAnswer();
}
sw.Stop();
Console.WriteLine(
$".NET elapsed time: {sw.Elapsed.TotalSeconds:#.0000} seconds at " +
$"{sw.Elapsed.TotalMilliseconds / 1000000d:#.000000} ms per operation");
}
public class PocoObject // Plain old CLR object
{
public string Question { get; set; }
public int Answer { get; set; }
public void IncrementAnswer() => Answer += 1;
}
}
Program.Main
의 경우
JSObjectBenchmark.Run();
앞의 예제에서는 브라우저의 디버그 콘솔에 다음 출력을 표시합니다.
JS interop elapsed time: .2536 seconds at .000254 ms per operation
.NET elapsed time: .0210 seconds at .000021 ms per operation
Begin Object Creation
JS interop elapsed time: 2.1686 seconds at .002169 ms per operation
.NET elapsed time: .1089 seconds at .000109 ms per operation
이벤트 구독 JS
.NET 코드는 처리기 역할을 하는 JS 함수에 C# Action JS 을 전달하여 이벤트를 구독하고 이벤트를 처리 JS 할 수 있습니다. shim 코드는 JS 이벤트 구독을 처리합니다.
Warning
이 섹션의 지침에 따르면 interop을 통해 JS DOM의 개별 속성과 상호 작용하는 것은 비교적 느리며 가비지 수집 압력이 높은 많은 프록시를 만들 수 있습니다. 다음 패턴은 일반적으로 권장되지 않습니다. 몇 개 이하의 요소에 대해 다음 패턴을 사용합니다. 자세한 내용은 성능 고려 사항 섹션을 참조하세요 .
뉘앙스는 removeEventListener
이전에 전달된 함수에 대한 참조가 필요하다는 것입니다 addEventListener
. C# Action 이 interop 경계를 넘어 전달되면 프록시 개체에 JS 래핑됩니다. 따라서 동일한 C# Action 을 둘 다 addEventListener
전달하면 removeEventListener
두 개의 서로 다른 JS 프록시 개체가 래핑됩니다 Action. 이러한 참조는 다르므로 removeEventListener
제거할 이벤트 수신기를 찾을 수 없습니다. 이 문제를 해결하기 위해 다음 예제에서는 함수에서 JS C# Action 을 래핑하고 구독 호출에서 참조를 JSObject 반환하여 나중에 구독 취소 호출에 전달합니다. C# Action 이 반환되고 전달되므로 JSObject두 호출 모두에 동일한 참조가 사용되며 이벤트 수신기를 제거할 수 있습니다.
EventsShim.js
:
export function subscribeEventById(elementId, eventName, listenerFunc) {
const elementObj = document.getElementById(elementId);
// Need to wrap the Managed C# action in JS func (only because it is being
// returned).
let handler = function (event) {
listenerFunc(event.type, event.target.id); // Decompose object to primitives
}.bind(elementObj);
elementObj.addEventListener(eventName, handler, false);
// Return JSObject reference so it can be used for removeEventListener later.
return handler;
}
// Param listenerHandler must be the JSObject reference returned from the prior
// SubscribeEvent call.
export function unsubscribeEventById(elementId, eventName, listenerHandler) {
const elementObj = document.getElementById(elementId);
elementObj.removeEventListener(eventName, listenerHandler, false);
}
export function triggerClick(elementId) {
const elementObj = document.getElementById(elementId);
elementObj.click();
}
export function getElementById(elementId) {
return document.getElementById(elementId);
}
export function subscribeEvent(elementObj, eventName, listenerFunc) {
let handler = function (e) {
listenerFunc(e);
}.bind(elementObj);
elementObj.addEventListener(eventName, handler, false);
return handler;
}
export function unsubscribeEvent(elementObj, eventName, listenerHandler) {
return elementObj.removeEventListener(eventName, listenerHandler, false);
}
export function subscribeEventFailure(elementObj, eventName, listenerFunc) {
// It's not strictly required to wrap the C# action listenerFunc in a JS
// function.
elementObj.addEventListener(eventName, listenerFunc, false);
// If you need to return the wrapped proxy object, you will receive an error
// when it tries to wrap the existing proxy in an additional proxy:
// Error: "JSObject proxy of ManagedObject proxy is not supported."
return listenerFunc;
}
EventsInterop.cs
:
using System;
using System.Runtime.InteropServices.JavaScript;
using System.Threading.Tasks;
public partial class EventsInterop
{
[JSImport("subscribeEventById", "EventsShim")]
public static partial JSObject SubscribeEventById(string elementId,
string eventName,
[JSMarshalAs<JSType.Function<JSType.String, JSType.String>>]
Action<string, string> listenerFunc);
[JSImport("unsubscribeEventById", "EventsShim")]
public static partial void UnsubscribeEventById(string elementId,
string eventName, JSObject listenerHandler);
[JSImport("triggerClick", "EventsShim")]
public static partial void TriggerClick(string elementId);
[JSImport("getElementById", "EventsShim")]
public static partial JSObject GetElementById(string elementId);
[JSImport("subscribeEvent", "EventsShim")]
public static partial JSObject SubscribeEvent(JSObject htmlElement,
string eventName,
[JSMarshalAs<JSType.Function<JSType.Object>>]
Action<JSObject> listenerFunc);
[JSImport("unsubscribeEvent", "EventsShim")]
public static partial void UnsubscribeEvent(JSObject htmlElement,
string eventName, JSObject listenerHandler);
}
public static class EventsUsage
{
public static async Task Run()
{
await JSHost.ImportAsync("EventsShim", "/EventsShim.js");
Action<string, string> listenerFunc = (eventName, elementId) =>
Console.WriteLine(
$"In C# event listener: Event {eventName} from ID {elementId}");
// Assumes two buttons exist on the page with ids of "btn1" and "btn2"
JSObject listenerHandler1 =
EventsInterop.SubscribeEventById("btn1", "click", listenerFunc);
JSObject listenerHandler2 =
EventsInterop.SubscribeEventById("btn2", "click", listenerFunc);
Console.WriteLine("Subscribed to btn1 & 2.");
EventsInterop.TriggerClick("btn1");
EventsInterop.TriggerClick("btn2");
EventsInterop.UnsubscribeEventById("btn2", "click", listenerHandler2);
Console.WriteLine("Unsubscribed btn2.");
EventsInterop.TriggerClick("btn1");
EventsInterop.TriggerClick("btn2"); // Doesn't trigger because unsubscribed
EventsInterop.UnsubscribeEventById("btn1", "click", listenerHandler1);
// Pitfall: Using a different handler for unsubscribe silently fails.
// EventsInterop.UnsubscribeEventById("btn1", "click", listenerHandler2);
// With JSObject as event target and event object.
Action<JSObject> listenerFuncForElement = (eventObj) =>
{
string eventType = eventObj.GetPropertyAsString("type");
JSObject target = eventObj.GetPropertyAsJSObject("target");
Console.WriteLine(
$"In C# event listener: Event {eventType} from " +
$"ID {target.GetPropertyAsString("id")}");
};
JSObject htmlElement = EventsInterop.GetElementById("btn1");
JSObject listenerHandler3 = EventsInterop.SubscribeEvent(
htmlElement, "click", listenerFuncForElement);
Console.WriteLine("Subscribed to btn1.");
EventsInterop.TriggerClick("btn1");
EventsInterop.UnsubscribeEvent(htmlElement, "click", listenerHandler3);
Console.WriteLine("Unsubscribed btn1.");
EventsInterop.TriggerClick("btn1");
}
}
Program.Main
의 경우
await EventsUsage.Run();
앞의 예제에서는 브라우저의 디버그 콘솔에 다음 출력을 표시합니다.
Subscribed to btn1 & 2.
In C# event listener: Event click from ID btn1
In C# event listener: Event click from ID btn2
Unsubscribed btn2.
In C# event listener: Event click from ID btn1
Subscribed to btn1.
In C# event listener: Event click from ID btn1
Unsubscribed btn1.
JS[JSImport]
/[JSExport]
interop 시나리오
다음 문서에서는 브라우저와 같은 호스트에서 .NET WebAssembly 모듈을 JS 실행하는 데 중점을 줍니다.
ASP.NET Core