.NET WebAssembly'de JavaScript [JSImport]/[JSExport] birlikte çalışma

Not

Bu, bu makalenin en son sürümü değildir. Geçerli sürüm için bu makalenin .NET 8 sürümüne bakın.

Uyarı

ASP.NET Core'un bu sürümü artık desteklenmiyor. Daha fazla bilgi için bkz . .NET ve .NET Core Destek İlkesi. Geçerli sürüm için bu makalenin .NET 8 sürümüne bakın.

Önemli

Bu bilgiler, ticari olarak piyasaya sürülmeden önce önemli ölçüde değiştirilebilen bir yayın öncesi ürünle ilgilidir. Burada verilen bilgilerle ilgili olarak Microsoft açık veya zımni hiçbir garanti vermez.

Geçerli sürüm için bu makalenin .NET 8 sürümüne bakın.

Tarafından Aaron Shumaker

Bu makalede, birlikte çalışma ( API) kullanarak JS/[JSImport][JSExport] istemci tarafı WebAssembly'de JavaScript (JSSystem.Runtime.InteropServices.JavaScript) ile nasıl etkileşim kurulacakları açıklanmaktadır.

[JSImport]/[JSExport] birlikte çalışma, aşağıdaki senaryolarda bir konakta JS .NET WebAssembly modülü çalıştırılırken geçerlidir:

Önkoşullar

.NET SDK (en son sürüm)

Aşağıdaki proje türlerinden herhangi biri:

Örnek uygulama

Örnek kodu görüntüleme veya indirme (indirme): Benimsediğiniz .NET sürümüyle eşleşen bir 8.0 veya üzeri sürüm klasörü seçin. sürüm klasöründe adlı WASMBrowserAppImportExportInteropörneğe erişin.

JS öznitelikleri kullanarak [JSImport]/[JSExport] birlikte çalışma

[JSImport] özniteliği, .NET yöntemi çağrıldığında karşılık gelen JS bir yöntemin çağrılması gerektiğini belirtmek için bir .NET yöntemine uygulanır. Bu, .NET geliştiricilerinin .NET kodunun çağrısı yapmasını sağlayan "içeri aktarmaları" tanımlamasına JSolanak tanır. Ayrıca, bir Action parametre olarak geçirilebilir ve JS geri çağırma veya olay aboneliği desenini desteklemek için eylemi çağırabilir.

[JSExport] Özniteliği koda göstermek için bir .NET yöntemine JS uygulanır. Bu, kodun .NET yöntemine çağrı başlatmasına olanak tanır JS .

yöntemleri içeri aktarma JS

Aşağıdaki örnek, standart bir yerleşik JS yöntemi (console.log) C# içine aktarır. [JSImport] genel olarak erişilebilir nesnelerin içeri aktarma yöntemleriyle sınırlıdır. Örneğin, log nesnesinde console tanımlanan ve genel olarak erişilebilir nesnesinde globalThistanımlanan bir yöntemdir. console.log yöntemi, ConsoleLoggünlük iletisi için bir dize kabul eden bir C# proxy yöntemiyle eşlenir:

public partial class GlobalInterop
{
    [JSImport("globalThis.console.log")]
    public static partial void ConsoleLog(string text);
}

içinde Program.Main, ConsoleLog günlüğe kaydedilecek iletiyle birlikte çağrılır:

GlobalInterop.ConsoleLog("Hello World!");

Çıkış, tarayıcının konsolunda görünür.

Aşağıda, içinde bildirilen bir yöntemi içeri aktarma işlemi gösterilmektedir JS.

Aşağıdaki özel JS yöntem (globalThis.callAlert), içinde iletinin iletildiğini textiçeren bir uyarı iletişim kutusu (window.alert) oluşturur:

globalThis.callAlert = function (text) {
  globalThis.window.alert(text);
}

globalThis.callAlert yöntemi, ileti için bir dize kabul eden bir C# proxy yöntemine ()CallAlert eşlenir:

using System.Runtime.InteropServices.JavaScript;

public partial class GlobalInterop
{
	[JSImport("globalThis.callAlert")]
	public static partial void CallAlert(string text);
}

CallAlert içinde Program.Mainçağrılır ve uyarı iletişim kutusu iletisinin metni geçirilir:

GlobalInterop.CallAlert("Hello World");

yöntemini belirten [JSImport] C# sınıfının bir uygulaması yok. Derleme zamanında, kaynak tarafından oluşturulan kısmi sınıf, ilgili yöntemi çağırmak JS için çağrının ve türlerin sıralamasını uygulayan .NET kodunu içerir. Visual Studio'da Tanıma Git veya Uygulamaya Git seçeneklerinin kullanılması sırasıyla kaynak tarafından oluşturulan kısmi sınıfa veya geliştirici tanımlı kısmi sınıfa gider.

Yukarıdaki örnekte, var olan JS kodu sarmak için ara globalThis.callAlertJS bildirim kullanılmıştır. Bu makale, ara JS bildirimi dolgu olarak gayri resmi olarak JSifade eder. JS dolgular, .NET uygulamasıyla var olan JS yetenekler/kitaplıklar arasındaki boşluğu doldurur. Önceki önemsiz örnek gibi çoğu durumda dolgu JS gerekli değildir ve önceki ConsoleLog örnekte gösterildiği gibi yöntemler doğrudan içeri aktarılabilir. Bu makalede de önümüzdeki bölümlerde gösterildiği gibi dolgu JS şunları yapabilir:

  • Ek mantığı kapsülleme.
  • El ile eşleme türleri.
  • Birlikte çalışma sınırını geçen nesne veya çağrı sayısını azaltın.
  • Statik çağrıları örnek yöntemlerine el ile eşleyin.

JavaScript bildirimleri yükleniyor

JS ile [JSImport] içeri aktarılması amaçlanan bildirimler genellikle .NET WebAssembly'yi yükleyen aynı sayfa veya JS konak bağlamında yüklenir. Bu, aşağıdakilerle gerçekleştirilebilir:

  • <script>...</script> Satır JSiçi bildirimde bulunan bir blok.
  • Bir dış JS dosya (src) yükleyen bir betik kaynağı () bildirimi<script src="./some.js"></script> (.js).
  • Bir JS ES6 modülü (<script type='module' src="./moduleName.js"></script>).
  • JS.NET WebAssembly'den kullanılarak JSHost.ImportAsync yüklenen bir ES6 modülü.

Bu makaledeki örneklerde kullanılır JSHost.ImportAsync. çağrılırken ImportAsync, istemci tarafı .NET WebAssembly parametresini moduleUrl kullanarak dosyayı ister ve bu nedenle dosyanın statik bir web varlığı olarak erişilebilir olmasını bekler; <script> aynı etiket URL'li src bir dosyayı alır. Örneğin, bir WebAssembly Browser Uygulaması projesindeki aşağıdaki C# kodu, dosyasını (.js) yolunda /wwwroot/scripts/ExampleShim.jstutarJS:

await JSHost.ImportAsync("ExampleShim", "/scripts/ExampleShim.js");

WebAssembly'yi yükleyen platforma bağlı olarak, gibi ./scripts/nokta ön ekli bir URL, webAssembly paketi altındaki /_framework/çerçeve betikleri tarafından başlatıldığından, gibi /_framework/scripts/yanlış bir alt dizine başvurabilir. Bu durumda URL'ye ../scripts/ ön ek eklemek doğru yola başvurur. ile /scripts/ ön ek özelliği, site etki alanının kökünde barındırılıyorsa çalışır. Tipik bir yaklaşım, belirli bir ortam için doğru temel yolu bir HTML <base> etiketiyle yapılandırmayı /scripts/ ve temel yola göre yola başvurmak için ön ekini kullanmayı içerir. Tilde gösterimi ~/ ön ekleri tarafından JSHost.ImportAsyncdesteklenmez.

Önemli

JavaScript modülünden yüklenirse JS öznitelikler [JSImport] ikinci parametre olarak modül adını içermelidir. Örneğin, [JSImport("globalThis.callAlert", "ExampleShim")] içeri aktarılan yöntemin "ExampleShim" adlı bir JavaScript modülünde bildirildiğini gösterir.

Tür eşlemeleri

Benzersiz bir eşleme destekleniyorsa, .NET yöntemi imzasında parametreler ve dönüş türleri çalışma zamanında uygun JS türlere veya bu türlerden otomatik olarak dönüştürülür. Bu, değerlerin bir ara sunucu türüne kaydırılan değere veya başvurulara göre dönüştürülmesiyle sonuçlanabilir. Bu işlem tür hazırlama olarak bilinir. İçeri aktarılan yöntem parametrelerinin ve dönüş türlerinin nasıl düzenlendiğini denetlemek için kullanın JSMarshalAsAttribute<T> .

Bazı türlerin varsayılan tür eşlemesi yoktur. Örneğin, bir long veya System.Runtime.InteropServices.JavaScript.JSType.BigIntolarak System.Runtime.InteropServices.JavaScript.JSType.Number sıralanabilir, bu nedenle JSMarshalAsAttribute<T> derleme zamanı hatasını önlemek için gereklidir.

Aşağıdaki tür eşleme senaryoları desteklenir:

  • Action Func<TResult> Çağrılabilir JS yöntemler olarak sıralanmış veya parametre olarak geçirme. Bu, .NET kodunun geri çağırmalara veya olaylara yanıt olarak dinleyicileri çağırmasına JS olanak tanır.
  • JS Başvuruları ve .NET yönetilen nesne başvurularını her iki yönde de geçirme. Bu, ara sunucu nesneleri olarak sıralanmış ve ara sunucu çöp toplanana kadar birlikte çalışma sınırında canlı tutulmşdur.
  • Zaman uyumsuz JS yöntemleri veya JS Promise bir sonuçla Task birlikte (veya tam tersi) hazırlama.

Sıralanmış türlerin çoğu, hem içeri hem de dışarı aktarılan yöntemler üzerinde parametre olarak ve dönüş değerleri olarak her iki yönde de çalışır.

Aşağıdaki tabloda desteklenen tür eşlemeleri gösterilir.

.NET JavaScript Nullable Task➔Hedef Promise JSMarshalAs opsiyonel Array of
Boolean Boolean Destekleniyor Destekleniyor Destekleniyor Desteklenmiyor
Byte Number Destekleniyor Destekleniyor Destekleniyor Destekleniyor
Char String Destekleniyor Destekleniyor Destekleniyor Desteklenmiyor
Int16 Number Destekleniyor Destekleniyor Destekleniyor Desteklenmiyor
Int32 Number Destekleniyor Destekleniyor Destekleniyor Destekleniyor
Int64 Number Destekleniyor Destekleniyor Desteklenmiyor Desteklenmiyor
Int64 BigInt Destekleniyor Destekleniyor Desteklenmiyor Desteklenmiyor
Single Number Destekleniyor Destekleniyor Destekleniyor Desteklenmiyor
Double Number Destekleniyor Destekleniyor Destekleniyor Destekleniyor
IntPtr Number Destekleniyor Destekleniyor Destekleniyor Desteklenmiyor
DateTime Date Destekleniyor Destekleniyor Desteklenmiyor Desteklenmiyor
DateTimeOffset Date Destekleniyor Destekleniyor Desteklenmiyor Desteklenmiyor
Exception Error Desteklenmiyor Destekleniyor Destekleniyor Desteklenmiyor
JSObject Object Desteklenmiyor Destekleniyor Destekleniyor Destekleniyor
String String Desteklenmiyor Destekleniyor Destekleniyor Destekleniyor
Object Any Desteklenmiyor Destekleniyor Desteklenmiyor Destekleniyor
Span<Byte> MemoryView Desteklenmiyor Desteklenmiyor Desteklenmiyor Desteklenmiyor
Span<Int32> MemoryView Desteklenmiyor Desteklenmiyor Desteklenmiyor Desteklenmiyor
Span<Double> MemoryView Desteklenmiyor Desteklenmiyor Desteklenmiyor Desteklenmiyor
ArraySegment<Byte> MemoryView Desteklenmiyor Desteklenmiyor Desteklenmiyor Desteklenmiyor
ArraySegment<Int32> MemoryView Desteklenmiyor Desteklenmiyor Desteklenmiyor Desteklenmiyor
ArraySegment<Double> MemoryView Desteklenmiyor Desteklenmiyor Desteklenmiyor Desteklenmiyor
Task Promise Desteklenmiyor Desteklenmiyor Destekleniyor Desteklenmiyor
Action Function Desteklenmiyor Desteklenmiyor Desteklenmiyor Desteklenmiyor
Action<T1> Function Desteklenmiyor Desteklenmiyor Desteklenmiyor Desteklenmiyor
Action<T1, T2> Function Desteklenmiyor Desteklenmiyor Desteklenmiyor Desteklenmiyor
Action<T1, T2, T3> Function Desteklenmiyor Desteklenmiyor Desteklenmiyor Desteklenmiyor
Func<TResult> Function Desteklenmiyor Desteklenmiyor Desteklenmiyor Desteklenmiyor
Func<T1, TResult> Function Desteklenmiyor Desteklenmiyor Desteklenmiyor Desteklenmiyor
Func<T1, T2, TResult> Function Desteklenmiyor Desteklenmiyor Desteklenmiyor Desteklenmiyor
Func<T1, T2, T3, TResult> Function Desteklenmiyor Desteklenmiyor Desteklenmiyor Desteklenmiyor

Tür eşlemesi ve marshalled değerleri için aşağıdaki koşullar geçerlidir:

  • Sütun, Array of .NET türünün olarak JSArraysıralanabilir olup olmadığını gösterir. Örnek: C# int[] (Int32) ile eşlenen JSArray Numbers.
  • C# değerine yanlış türde bir JS değer geçirirken, çerçeve çoğu durumda bir özel durum oluşturur. Çerçeve, içinde JSderleme zamanı türü denetimi gerçekleştirmez.
  • JSObject, Exceptionve Task ArraySegment oluşturun GCHandle ve bir ara sunucu oluşturun. Geliştirici kodunda elden çıkarmayı tetikleyebilir veya .NET çöp toplamanın (GC) nesneleri daha sonra atmasına izin vekleyebilirsiniz. Bu türler önemli performans yükü taşır.
  • Array: Dizi hazırlama, veya .NET'te JS dizinin bir kopyasını oluşturur.
  • MemoryView
    • MemoryView, ve ArraySegmentsıralamak için .NET WebAssembly çalışma zamanına Span yönelik bir JS sınıftır.
    • Bir diziyi hazırlamanın aksine, veya Span ArraySegment sıralamak temel belleğin bir kopyasını oluşturmaz.
    • MemoryView yalnızca .NET WebAssembly çalışma zamanı tarafından düzgün bir şekilde örneklenebilir. Bu nedenle, bir JS yöntemi veya ArraySegmentparametresine Span sahip bir .NET yöntemi olarak içeri aktarmak mümkün değildir.
    • MemoryView için Span oluşturulan yalnızca birlikte çalışma çağrısı süresi için geçerlidir. Birlikte çalışma çağrısından sonra kalıcı olmayan çağrı yığınında ayrıldığı gibi Span , döndüren Spanbir .NET yöntemini dışarı aktarmak mümkün değildir.
    • MemoryView bir ArraySegment için oluşturulan birlikte çalışma çağrısından sonra hayatta kalır ve bir arabelleği paylaşmak için yararlıdır. bir için ArraySegment oluşturulan üzerinde MemoryView çağrısı dispose() proxy'yi atıp temel alınan .NET dizisini kaldırıyor. için MemoryViewbir try-finally blokta çağırmanızı dispose() öneririz.

içinde iç içe genel türler JSMarshalAs gerektiren tür eşlemelerinin bazı bileşimleri şu anda desteklenmemektedir. Örneğin, gibi [return: JSMarshalAs<JSType.Promise<JSType.Array<JSType.Number>>>()] bir diziyi Promise gerçekleştirmeye çalışmak bir derleme zamanı hatası oluşturur. Senaryoya bağlı olarak uygun bir geçici çözüm farklılık gösterir, ancak bu senaryo Tür eşleme sınırlamaları bölümünde daha ayrıntılı olarak incelenmiş olur.

JS Ilkel

Aşağıdaki örnek, çeşitli temel JS türlerin tür eşlemelerinden yararlanmayı ve derleme zamanında açık eşlemelerin gerekli olduğu kullanımını JSMarshalAsgösterir[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 içinde:

await PrimitivesUsage.Run();

Yukarıdaki örnek, tarayıcının hata ayıklama konsolunda aşağıdaki çıkışı görüntüler:

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 Nesne

Bu bölümdeki örnekte, dönüş veya parametresi olarak nesnesi JS Date olan içeri aktarma yöntemleri gösterilmektedir. Tarihler birlikte çalışmada değere göre sıralanır, yani bunlar ilkel öğelerle aynı şekilde JS kopyalanır.

Nesne Date saat dilimi belirsizdir. .NETDateTime, öğesine göre düzenlendiğinde Datebuna göre DateTimeKind ayarlanır, ancak saat dilimi bilgileri korunmaz. ile veya DateTimeKind.Local temsil ettiği değerle tutarlı bir DateTime DateTimeKind.Utc başlatmayı göz önünde bulundurun.

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 içinde:

await DateUsage.Run();

Yukarıdaki örnek, tarayıcının hata ayıklama konsolunda aşağıdaki çıkışı görüntüler:

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)

Yukarıdaki saat dilimi bilgileri (GMT-0500 (Eastern Standard Time)), bilgisayarınızın/tarayıcınızın yerel saat dilimine bağlıdır.

JS nesne başvuruları

Bir JS yöntem bir nesne başvurusu döndürdüğünde, .NET'te olarak JSObjectgösterilir. Özgün JS nesne, sınır içinde JS yaşam süresine devam ederken .NET kodu aracılığıyla başvuru yoluyla nesneye erişebilir ve bu nesnede JSObjectdeğişiklik yapabilir. Türün kendisi sınırlı bir API'yi kullanıma sunarken, nesne JS başvurusunu tutma ve bunu birlikte çalışma sınırından döndürme veya geçirme olanağı, çeşitli birlikte çalışma senaryoları için destek sağlar.

JSObject özelliklerine erişmek için yöntemler sağlar, ancak örnek yöntemlerine doğrudan erişim sağlamaz. Aşağıdaki Summarize yöntemin gösterdiği gibi, örneği parametre olarak alan statik bir yöntem uygulanarak örnek yöntemlerine dolaylı olarak erişilebilir.

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 içinde:

await JSObjectUsage.Run();

Yukarıdaki örnek, tarayıcının hata ayıklama konsolunda aşağıdaki çıkışı görüntüler:

{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

Zaman uyumsuz birlikte çalışma

Birçok JS API zaman uyumsuzdur ve bir geri çağırma, bir Promiseveya zaman uyumsuz bir yöntem aracılığıyla tamamlanma sinyali gönderir. Sonraki kod zaman uyumsuz işlemin tamamlanmasına bağlı olabileceğinden ve beklenmesi gerektiğinden, zaman uyumsuz özellikleri yoksaymak genellikle bir seçenek değildir.

JS anahtar sözcüğünü async kullanan veya döndüren yöntemler, bir döndüren Promise Taskyöntem tarafından C# dilinde beklenebilir. Aşağıda gösterildiği gibi anahtar sözcüğü içinde async anahtar sözcüğü kullanmadığından C# yönteminde await özniteliğiyle [JSImport] birlikte kullanılmaz. Ancak, yöntemini çağıran kodun kullanılması genellikle anahtar sözcüğünü await kullanır ve örnekte gösterildiği PromisesUsage gibi olarak asyncişaretlenir.

JSgibi bir geri setTimeoutçağırma ile, uygulamasından JSdönmeden önce içinde Promise sarmalanabilir. öğesine atanan işlevde gösterildiği gibi içinde Promisebir geri çağırma sarmalama Wait2Secondsyalnızca geri çağırma tam olarak bir kez çağrıldığında uygundur. Aksi takdirde, olaylara abone JS olma bölümünde gösterilen sıfır veya birçok kez çağrılabilen bir geri çağırmayı dinlemek için bir C# Action geçirilebilir.

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# yöntemi imzasında anahtar sözcüğünü kullanmayın async . geri dönmek Task veya Task<TResult> yeterlidir.

Zaman uyumsuz JS yöntemleri çağırırken genellikle yöntem yürütmeyi tamamlayana JS kadar beklemek isteriz. Bir kaynağı yüklüyor veya istekte bulunuyorsanız, büyük olasılıkla aşağıdaki kodun eylemin tamamlandığını varsaymasını istiyoruz.

JS Dolgu bir Promisedöndürürse, C# bunu beklenebilir Task/Task<TResult>olarak değerlendirebilir.

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 içinde:

await PromisesUsage.Run();

Yukarıdaki örnek, tarayıcının hata ayıklama konsolunda aşağıdaki çıkışı görüntüler:

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'

Tür eşleme sınırlamaları

Tanımda JSMarshalAs iç içe genel türler gerektiren bazı tür eşlemeleri şu anda desteklenmemektedir. Örneğin, gibi [return: JSMarshalAs<JSType.Promise<JSType.Array<JSType.Number>>>()] bir dizi için bir Promise döndürülmesi derleme zamanı hatası oluşturur. Senaryoya bağlı olarak uygun bir geçici çözüm farklılık gösterir, ancak bir seçenek de diziyi başvuru JSObject olarak göstermektir. .NET içindeki tek tek öğelere erişmek gerekli değilse ve başvuru dizi üzerinde çalışan diğer JS yöntemlere geçirilebilirse bu yeterli olabilir. Alternatif olarak, ayrılmış bir yöntem başvuruyu JSObject parametre olarak alabilir ve aşağıdaki UnwrapJSObjectAsIntArray örnekte gösterildiği gibi gerçekleştirilmiş diziyi döndürebilir. Bu durumda, yöntemin JS tür denetimi yoktur ve geliştirici uygun dizi türünün sarmalanmasını sağlama JSObject sorumluluğuna sahiptir.

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 içinde:

JSObject arrayAsJSObject = await PromisesInterop.WaitGetIntArrayAsObject();
int[] intArray = PromisesInterop.UnwrapJSObjectAsIntArray(arrayAsJSObject);

Performans değerlendirmeleri

Çağrıların sıralanması ve birlikte çalışma sınırındaki nesneleri izleme yükü yerel .NET işlemlerinden daha pahalıdır, ancak orta düzeyde talep içeren tipik bir web uygulaması için kabul edilebilir performans göstermeye devam etmelidir.

Birlikte çalışma sınırında başvuruları koruyan gibi JSObjectnesne ara sunucuları ek bellek yüküne sahiptir ve çöp toplamanın bu nesneleri nasıl etkilediğini etkiler. Ayrıca ve .NET'ten JS gelen bellek baskısı paylaşılmadığından bazı senaryolarda atık toplama tetiklemeden kullanılabilir bellek tükenebilir. Bu risk, göreli olarak küçük JS nesneler tarafından birlikte çalışma sınırında aşırı sayıda büyük nesneye başvurulduğunda veya proxy'ler tarafından JS büyük .NET nesnelerine başvurulduğunda önemlidir. Bu gibi durumlarda, nesneler üzerindeki JS arabirimden yararlanan IDisposable kapsamlarla belirlenimici elden çıkarma desenlerinin using izlenmesini öneririz.

Önceki örnek koddan yararlanan aşağıdaki karşılaştırmalar, birlikte çalışma işlemlerinin .NET sınırı içinde kalanlardan kabaca daha yavaş bir büyüklük sırası olduğunu, ancak birlikte çalışma işlemlerinin görece hızlı kaldığını göstermektedir. Ayrıca, bir kullanıcının cihaz özelliklerinin performansı etkilediğini de göz önünde bulundurun.

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 içinde:

JSObjectBenchmark.Run();

Yukarıdaki örnek, tarayıcının hata ayıklama konsolunda aşağıdaki çıkışı görüntüler:

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

Olaylara abone olma JS

.NET kodu olaylara abone olabilir ve işleyici olarak çalışmak üzere JS bir JS işleve C# Action geçirerek olayları işleyebilirJS. Dolgu JS kodu, olaya abone olunması işlemini işler.

Uyarı

Bu bölümdeki kılavuzda gösterildiği gibi birlikte çalışma yoluyla JS DOM'un tek tek özellikleriyle etkileşim kurmak nispeten yavaştır ve yüksek çöp toplama baskısı oluşturan birçok ara sunucu oluşturulmasına yol açabilir. Aşağıdaki desen genellikle önerilmez. En fazla birkaç öğe için aşağıdaki deseni kullanın. Daha fazla bilgi için Performansla ilgili dikkat edilmesi gerekenler bölümüne bakın.

bir nüans removeEventListener , daha önce öğesine geçirilen işleve başvuru gerektirmesidir addEventListener. Birlikte çalışma sınırı boyunca bir C# Action geçirildiğinde, bir JS ara sunucu nesnesine sarmalanır. Bu nedenle, aynı C# Action öğesinin her ikisine de addEventListener geçirilmesi ve removeEventListener iki farklı JS proxy nesnesinin sarmalanmasıyla sonuçlanmaktadır Action. Bu başvurular farklıdır, bu nedenle removeEventListener kaldırılacak olay dinleyicisini bulamazsınız. Bu sorunu gidermek için aşağıdaki örnekler C# Action öğesini bir JS işleve sarmalar ve daha sonra abonelikten çıkma çağrısına geçirmek için başvuruyu abone olun çağrısından bir JSObject olarak döndürür. C# Action döndürülür ve olarak JSObjectgeçirildiğinden, her iki çağrı için de aynı başvuru kullanılır ve olay dinleyicisi kaldırılabilir.

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 içinde:

await EventsUsage.Run();

Yukarıdaki örnek, tarayıcının hata ayıklama konsolunda aşağıdaki çıkışı görüntüler:

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] birlikte çalışma senaryoları

Aşağıdaki makaleler tarayıcı gibi bir JS konakta .NET WebAssembly modülünü çalıştırmaya odaklanır: