.NET WebAssembly'de JavaScript [JSImport]
/[JSExport]
birlikte çalışma
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.
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:
- JavaScript '[JSImport]'/'[JSExport]' bir WebAssembly Browser Uygulaması projesiyle birlikte çalışabilir.
- ASP.NET Core Blazorile JavaScript JSImport/JSExport birlikte çalışma.
- Birlikte çalışma destekleyen
[JSImport]
/[JSExport]
diğer .NET WebAssembly platformları.
Önkoşullar
Aşağıdaki proje türlerinden herhangi biri:
- '[JSImport]'/'[JSExport]' JavaScript'e göre oluşturulan bir WebAssembly Browser Uygulaması projesi, bir WebAssembly Browser Uygulaması projesiyle birlikte çalışabilir.
- Blazor ASP.NET Core Blazorile JavaScript JSImport/JSExport birlikte çalışmasına göre oluşturulan bir istemci tarafı projesi.
- Birlikte çalışma (System.Runtime.InteropServices.JavaScriptAPI) destekleyen
[JSExport]
[JSImport]
/ticari veya açık kaynak platform için oluşturulan bir proje.
Ö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 globalThis
tanımlanan bir yöntemdir. console.log
yöntemi, ConsoleLog
gü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 text
iç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.callAlert
JS 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.js
tutarJS:
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 JSArray
sıralanabilir olup olmadığını gösterir. Örnek: C#int[]
(Int32
) ile eşlenen JSArray
Number
s. - 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
,Exception
veTask
ArraySegment
oluşturunGCHandle
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
, veArraySegment
sıralamak için .NET WebAssembly çalışma zamanınaSpan
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 veyaArraySegment
parametresineSpan
sahip bir .NET yöntemi olarak içeri aktarmak mümkün değildir.MemoryView
içinSpan
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ığı gibiSpan
, döndürenSpan
bir .NET yöntemini dışarı aktarmak mümkün değildir.MemoryView
birArraySegment
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çinArraySegment
oluşturulan üzerindeMemoryView
çağrısıdispose()
proxy'yi atıp temel alınan .NET dizisini kaldırıyor. içinMemoryView
birtry-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ı JSMarshalAs
gö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 Date
buna 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 Promise
veya 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 async
iş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 Promise
bir geri çağırma sarmalama Wait2Seconds
yalnı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 Promise
dö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:
ASP.NET Core