一、当C#遇上JavaScript的那些事儿

咱们做Web开发的,经常遇到一个头疼的问题:前端用JavaScript,后端用C#,这俩语言怎么愉快地玩耍?Blazor WebAssembly给出了一套漂亮的解决方案。想象一下,你可以在C#里直接调用JavaScript函数,反过来JavaScript也能调用C#方法,就像邻居串门一样方便。

先看个简单例子(技术栈:.NET 6 + Blazor WebAssembly):

// 在C#中调用JavaScript的alert函数
[JSInvokable]
public static async Task ShowAlert(string message)
{
    // 通过IJSRuntime调用JS函数
    await JSRuntime.InvokeVoidAsync("alert", message);
}

// 对应的JavaScript代码
window.sayHello = function(name) {
    // 这个函数可以被C#调用
    return `Hello, ${name}!`;
};

看到没?三行代码就实现了跨语言调用。IJSRuntime是Blazor提供的桥梁,JSInvokable特性则让C#方法对JavaScript可见。

二、深度互操作的四种姿势

1. 基础调用:你呼我应

最基础的用法就是相互调用函数。比如要在C#里获取浏览器窗口大小:

public async Task GetWindowSize()
{
    // 调用JS的getWindowSize函数
    var size = await JSRuntime.InvokeAsync<WindowSize>("getWindowSize");
    Console.WriteLine($"Width: {size.Width}, Height: {size.Height}");
}

// JavaScript部分
window.getWindowSize = () => {
    return {
        width: window.innerWidth,
        height: window.innerHeight
    };
};

2. 回调函数:你中有我

有时候我们需要传递回调函数,比如处理按钮点击:

// 注册点击回调
await JSRuntime.InvokeVoidAsync("registerClickHandler", 
    DotNetObjectReference.Create(this));

// C#回调方法
[JSInvokable]
public void HandleClick(int x, int y)
{
    Console.WriteLine($"Clicked at ({x}, {y})");
}

// JavaScript事件处理
window.registerClickHandler = (dotNetHelper) => {
    document.addEventListener('click', (e) => {
        dotNetHelper.invokeMethodAsync('HandleClick', e.clientX, e.clientY);
    });
};

3. 对象传递:礼尚往来

复杂对象也能在两者间传递,比如处理JSON数据:

public class UserData
{
    public string Name { get; set; }
    public int Age { get; set; }
}

// C#调用JS处理数据
var user = new UserData { Name = "张三", Age = 30 };
var processed = await JSRuntime.InvokeAsync<UserData>("processUserData", user);

// JavaScript处理函数
window.processUserData = (user) => {
    user.age += 1; // 给年龄加1
    return user;
};

4. 模块化调用:高端玩法

对于大型项目,推荐使用JS模块:

// 加载JS模块
var module = await JSRuntime.InvokeAsync<IJSObjectReference>(
    "import", "./scripts/myModule.js");

// 调用模块方法
await module.InvokeVoidAsync("doSomething");

// myModule.js内容
export function doSomething() {
    console.log('模块化操作');
}

三、那些年我们踩过的坑

1. 类型转换的暗礁

JavaScript的弱类型和C#的强类型经常打架。比如:

// C#期望得到int
var count = await JSRuntime.InvokeAsync<int>("getCount");

// 但JS可能返回字符串"123"
window.getCount = () => "123"; // 这里会报错!

解决方案是做好类型校验:

window.getCount = () => {
    const count = localStorage.getItem('count');
    return parseInt(count) || 0; // 确保返回数字
};

2. 内存泄漏的陷阱

DotNetObjectReference必须手动释放:

public class MyService : IDisposable
{
    private DotNetObjectReference<MyService> _jsRef;
    
    public MyService()
    {
        _jsRef = DotNetObjectReference.Create(this);
    }
    
    [JSInvokable]
    public void Callback() { /*...*/ }
    
    public void Dispose()
    {
        _jsRef?.Dispose(); // 重要!
    }
}

3. 异步调用的时序问题

JS调用C#方法时要注意异步特性:

// 错误示范
dotNetHelper.invokeMethod('SyncMethod'); // 可能阻塞

// 正确做法
await dotNetHelper.invokeMethodAsync('AsyncMethod');

四、实战:打造一个文件预览组件

让我们用互操作技术实现一个图片预览功能:

// C#部分
public async Task PreviewImage(Stream imageStream)
{
    // 将流转换为Base64
    using var memoryStream = new MemoryStream();
    await imageStream.CopyToAsync(memoryStream);
    var base64 = Convert.ToBase64String(memoryStream.ToArray());
    
    // 调用JS显示图片
    await JSRuntime.InvokeVoidAsync("showImagePreview", 
        $"data:image/jpeg;base64,{base64}");
}

// JavaScript部分
window.showImagePreview = (base64Image) => {
    const preview = document.getElementById('image-preview');
    preview.src = base64Image;
    preview.style.display = 'block';
};

这个例子展示了如何:

  1. 在C#中处理文件流
  2. 转换为JS能识别的Base64格式
  3. 通过互操作更新DOM

五、性能优化小贴士

  1. 批量调用:减少跨语言调用次数
// 不好的做法
await JSRuntime.InvokeVoidAsync("updateName", name);
await JSRuntime.InvokeVoidAsync("updateAge", age);

// 优化方案
await JSRuntime.InvokeVoidAsync("updateUser", name, age);
  1. 使用内存缓存:对于频繁调用的JS方法
private IJSObjectReference? _cachedModule;

public async Task DoWork()
{
    _cachedModule ??= await JSRuntime.InvokeAsync<IJSObjectReference>(
        "import", "./scripts/work.js");
    
    await _cachedModule.InvokeVoidAsync("doWork");
}
  1. 避免大对象传递:超过1MB的数据考虑分块传输

六、应用场景大盘点

  1. 集成现有JS库:比如调用Chart.js绘图
  2. 浏览器API访问:地理位置、摄像头等
  3. DOM精细操作:JS更擅长处理动画效果
  4. Web Worker:用JS处理密集型任务
  5. 第三方服务:如支付SDK集成

七、技术选型的思考

优点:

  • 一套C#代码走天下,减少上下文切换
  • 强类型安全,编译时就能发现错误
  • 可以复用现有的.NET生态

缺点:

  • 首次加载较慢(WebAssembly特性)
  • 调试体验不如纯前端流畅
  • 某些浏览器API需要polyfill

注意事项:

  1. 生产环境记得开启压缩和缓存
  2. 考虑添加加载动画改善用户体验
  3. 对于复杂动画,还是交给CSS/JS处理更合适
  4. 注意浏览器兼容性问题

八、总结

Blazor的互操作就像给C#和JavaScript办了张结婚证,让它们能合法地在一起过日子。虽然偶尔会吵架(类型冲突),但只要掌握好沟通技巧(规范调用),就能组建幸福的家庭(稳定应用)。记住:没有银弹,根据场景选择最合适的交互方式才是王道。