本文中,我将会为大家分享一个如何用.NET技术开发“在浏览器端编译和运行C#代码的工具”,核心的技术就是用C#编写不依赖于Blazor框架的WebAssembly以及Roslyn技术。

一、 为什么要开发这样的工具?

对于编程初学者来讲,开发环境的安装配置是一个令人头疼的事情,如果能让初学者不用做任何的安装配置,直接打开浏览器就能编写、运行代码,那么这将会大大降低编程初学者的学习门槛。

目前已经有一些可以在线编写、运行C#代码的网站了,这些网站的实现思路有如下两种:

思路1把代码从前端提交到在后端服务器上,然后在服务器上进行编译、运行,然后把运行结果再显示到前端。这样做的缺点是无法完成复杂的输入输出、界面交互等。

思路2用Mono技术编写WebAssembly。这样做的缺点是对于C#语法的跟进不及时,一些新的C#语法不被支持。

因此,开发一个能在浏览器端编译运行C#代码,并且支持最新C#语法的工具就很重要了。要开发这样的工具,WebAssembly是一个绕不过去的技术。

二、 什么是WebAssembly

传统的前端开发都是使用JavaScript来编写逻辑,而WebAssembly让我们可以用其他编程序言编写在浏览器中运行的程序。由于WebAssembly属于现代浏览器的标准,所以在浏览器中运行WebAssembly程序并不需要安装额外的插件。现在Java、Go、Python等主流的编程语言都已经支持编译为WebAssembly。

三、 Blazor WebAssembly的缺点

.NET 中的Blazor WebAssembly技术可以把C#代码编译为WebAssembly运行在浏览器端。但是传统的Blazor WebAssembly是一个侵入性很强的框架,也就是整个系统都必须使用C#技术进行开发,而不能选择只是其中一个组件使用C#代码,其他地方仍然使用传统的JavaScript进行开发。当然,通过Microsoft.AspNetCore.Components.CustomElements,我们可以只把界面的一小块使用C#进行开发,但是这种方式仍然是“在页面上留一个用C#写的区域”,非常的重量级,而不能实现“只用C#写一个函数”这样轻量级的组件,也就是用C#写一个非侵入性、依赖性很低的轻量级WebAssembly组件。

四、 不用Blazor WebAssembly,用.NET技术开发WebAssembly

从.NET 6开始,我们可以使用C#编写轻量级的WebAssembly,生成的WebAssembly只需要使用Blazor提供的基础运行环境,而不需要引入整个Blazor WebAssembly技术。

下面,我将会通过一个简单的“用C#计算两个数的和”的例子来演示这个技术的用法。当然,这只是一个简单的演示,实际项目中肯定不会用C#完成这么简单的功能。下面的项目用.NET 7进行演示,其他版本使用可能会略有不同。

1、 创建一个.NET普通类库项目,然后通过Nuget安装如下两个组件:Microsoft.AspNetCore.Components.WebAssembly、Microsoft.AspNetCore.Components.WebAssembly.DevServer,然后把类库项目的csproj文件的根节点中Sdk属性的值修改为"Microsoft.NET.Sdk.BlazorWebAssembly"。

修改后的文件类似如代码 1所示。

代码 1  csproj项目文件

<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="7.0.2" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="7.0.2" />
</ItemGroup>
</Project>

2、 在类库项目中创建一个文件Program.cs,内容如代码 2所示。

代码 2 Program.cs

using Microsoft.JSInterop;

namespace Demo1
{
public class Program
{
private static async Task Main(string[] args)
{
} [JSInvokable]
public static int Add(int i,int j)
{
return i + j;
}
}
}

这里Main方法目前是空的,但是不能被省略。Add方法上的[JSInvokable]表示这个方法可以被JavaScript调用,也就是这个方法属于一个可以被调用的Web Assembly方法。

3、 编译项目,生成文件夹下的wwwroot文件夹中的_framework文件夹中就是生成的Web Assembly和相关文件。

4、 用任何你喜欢的前端技术创建一个前端项目。我这里不使用任何的前端框架,而是直接用普通的HTML+Javascript来编写前端项目。

首先,我们要把上一步生成的_framework文件夹复制到前端项目的根文件夹下。

然后,我们编写index.html文件,内容如代码 3所示。

代码 3 index.html

<html lang="en">
<head>
<meta charset="UTF-8" />
</head>
<body></body>
<script src="_framework/blazor.webassembly.js" autostart="false"></script>
<script>
window.onload = async function () {
await Blazor.start();
const r = await DotNet.invokeMethodAsync(
'Demo1',//程序集的名字
'Add',//要调用的标注了[JSInvokable]方法的名字
666,//若干参数
333
);
alert(r);
};
</script>
</html>

接下来解释一下上面的代码,<script src="_framework/blazor.webassembly.js" autostart="false"></script>用来引入相关的文件。Blazor.start();用来启动Blazor运行时环境; DotNet.invokeMethodAsync用来调用WebAssembly中的方法,第一个参数为被调用的程序集的名字,第二个参数为被调用的方法的名字,之后的参数为给被调用的方法传递的参数值。

可以看到,这里我们就是把WebAssembly当成一个组件在用,完全不对页面有其他特殊的要求。所以这个组件可以在任何前端框架中使用,也可以和其他前端的库一起使用。

最后,我们运行这个前端项目. 由于Blazor会生成blat、dll等不被Web服务器默认接受的文件类型,所以请确保在Web服务器上为如下格式的文件配置MimeType:.pdb、.blat、.dat、.dll、.json、.wasm、.woff、.woff2。我这里测试用的Web服务器是IIS,所以在网站根文件夹下创建如所示的Web.config文件即可,如代码 4所示,使用其他Web服务器的开发者请参考所使用的Web服务器的手册进行MimeType的配置。

代码 4 Web.config

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<staticContent>
<remove fileExtension=".pdb" />
<remove fileExtension=".blat" />
<remove fileExtension=".dat" />
<remove fileExtension=".dll" />
<remove fileExtension=".json" />
<remove fileExtension=".wasm" />
<remove fileExtension=".woff" />
<remove fileExtension=".woff2" />
<mimeMap fileExtension=".pdb" mimeType="application/octet-stream" />
<mimeMap fileExtension=".blat" mimeType="application/octet-stream" />
<mimeMap fileExtension=".dll" mimeType="application/octet-stream" />
<mimeMap fileExtension=".dat" mimeType="application/octet-stream" />
<mimeMap fileExtension=".json" mimeType="application/json" />
<mimeMap fileExtension=".wasm" mimeType="application/wasm" />
<mimeMap fileExtension=".woff" mimeType="application/font-woff" />
<mimeMap fileExtension=".woff2" mimeType="application/font-woff" />
</staticContent>
</system.webServer>
</configuration>

5、 在浏览器端访问Web服务器中的index.html,如果看到如Figure 1所示的弹窗,就说明Javascript成功了调用了C#编写的Add方法。

Figure 1 程序执行弹窗

五、 C#编写WebAssembly的应用场景

C#编写的WebAssembly默认占的流量比较大,大约要占到30MB。我们可以通过BlazorLazyLoad、启用Brotli算法等方式把流量降到5MB以下,具体用法请网上搜索相关资料。

在我看来,用C#编写WebAssembly有包含但不限于如下的场景。

场景1复用一些.NET组件或者C#代码。这些已经存在的.NET组件或者C#代码虽然也能用Javascript重写,但是这样增加了额外的工作量。而通过WebAssembly就可以直接重用这些组件。比如,我曾经在后端开发用到过一个PE文件解析的Nuget包,这个包采用的.NET Standard标准开发,而且全部是在内存中进行文件内容的处理,因此我就可以直接在WebAssembly中继续使用这个包在前端对PE文件进行处理。

场景2使用一些WebAssembly组件。因为C/C++等语言编写的程序可以移植为WebAssembly版本,因此很多经典的C/C++开发的软件也可以继续在前端使用。比如音视频处理的ffmpeg已经有了WebAssembly版本,因此我们就可以用C#调用它进行音视频的处理;再比如,著名的计算机视觉库OpenCV也被移植到了WebAssembly中,因此我们也可以使用C#在前端进行图像识别、图像处理等操作。WebAssembly非常适合开发“在线图像处理、在线音视频、在线游戏”等工具类应用的开发。

场景3开发一些复杂度高的前端组件。我们知道,在开发复杂度高的项目的时候,Javascript经常是力不从心,即使是Typescript也并不会比Javascript有更根本性的改善。相比起来,C#等更适合工程化的开发,因此一些复杂度非常高的前端组件用C#编写为WebAssembly有可能更合适。

上面提到的这些场景下,我们可以只把部分组件用C#开发为WebAssembly,其他部分以及项目整体仍然可以继续用Javascript进行开发,这样各个语言可以发挥各自的特色。

六、 C#回调JavaScript中的方法

在编写WebAssembly的时候,我们可能需要在C#中调用Javascript中的方法。我们可以通过IJSRuntime、IJSInProcessRuntime分别调用Javascript中的异步方法和同步方法。

再创建一个类库项目Demo2,首先按照上面第四节中对项目进行配置,不再赘述。

index.html和Program.cs的代码如代码 5和代码 6所示。

代码 5 index.html

<html lang="en">
<head>
<meta charset="UTF-8" />
</head>
<body>
<div id="divDialog" style="display:none">
<div id="divMsg"></div>
<input type="text" id="txtValue" />
<button id="btnClose">close</button>
</div>
<ul id="ulMsgs">
</ul>
</body>
<script src="_framework/blazor.webassembly.js" autostart="false"></script>
<script>
function showMessage(msg) {
const divDialog = document.getElementById("divDialog");
divDialog.style.display = "block";
const divMsg = document.getElementById("divMsg");
divMsg.innerHTML = msg;
const txtValue = document.getElementById("txtValue");
const btnClose = document.getElementById("btnClose");
return new Promise(resolve => {
btnClose.onclick = function () {
divDialog.style.display = "none";
resolve(txtValue.value);
};
});
}
function appendMessage(msg) {
const li = document.createElement("li");
li.innerHTML = msg;
const ulMsgs = document.getElementById("ulMsgs");
ulMsgs.appendChild(li);
}
window.onload = async function () {
await Blazor.start();
await DotNet.invokeMethodAsync('Demo2','Count');
};
</script>
</html>

代码 6 Program.cs

using Microsoft.AspNetCore.Components.WebAssembly.Hosting;
using Microsoft.JSInterop; namespace Demo2; public class Program
{
private static IJSRuntime js; private static async Task Main(string[] args)
{
var builder = WebAssemblyHostBuilder.CreateDefault(args);
var host = builder.Build();
js = host.Services.GetRequiredService<IJSRuntime>();
await host.RunAsync();
} [JSInvokable]
public static async Task Count()
{
string strCount = await js.InvokeAsync<string>("showMessage","Input Count");
for(int i=0;i<int.Parse(strCount);i++)
{
((IJSInProcessRuntime)js).InvokeVoid("appendMessage",i);
}
((IJSInProcessRuntime)js).InvokeVoid("alert", "Done");
}
}

Index.html中定义的appendMessage方法是一个同步方法,用于把给定的消息附加到ul中;showMessage是一个异步方法,用于显示一个用html模拟的输入对话框,当用户点击【Close】按钮以后关闭对话框,并且返回用户输入的内容,这个操作涉及Javascript中的Promise相关概念,对这个不了解的请查看相关资料。

Program.cs中,在Main方法中获取用于调用Javascript代码的IJSRuntime服务。IJSRuntime接口中提供了InvokeAsync、InvokeVoidAsync两个方法,分别用于异步地调用JavaScript中的有返回值和无返回值的方法。如果想同步地调用Javascript中的方法,则需要把IJSRuntime类型的对象转型为IJSInProcessRuntime类型,然后调用它的InvokeVoid、Invoke方法。标注了[JSInvokable]的Count()方法是异步方法,在Javascript中调用C#中的异步方法的方式是一样的。

七、 运行时编译C#代码:Roslyn

.NET中的Roslyn用于在运行时编译C#代码,Roslyn支持在WebAssembly中使用,所以我们这里就用这个组件来完成C#代码的编译。Roslyn的用法在网上的资料很多,我这里不再详细讲解。

唯一需要注意的就是:Roslyn编译的默认是并行编译的,因为这样可以提高编译速度。但是受限于浏览器沙盒环境的限制,WebAssembly不支持并行操作,因此如果用默认的Roslyn编译设置,在执行编译操作的时候,Roslyn会抛出“System.PlatformNotSupportedException: Cannot wait on monitors on this runtime”这个错误信息。因此,需要在CSharpCompilationOptions中设置concurrentBuild=false,如代码 7所示。

代码 7 关闭concurrentBuild

var compilationOptions = new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, concurrentBuild: false);
var scriptCompilation = CSharpCompilation.CreateScriptCompilation("main.dll", syntaxTree, options: compilationOptions).AddReferences(references);

八、 替换默认Console类的实现

由于浏览器运行环境的限制,并不是所有.NET类都可以调用的,或者其功能是受限的。比如WebAssembly中调用IO类的时候,并不能随意的读写用户磁盘上的文件,只能受限于浏览器沙盒环境的安全限制。再比如WebAssembly中可以调用HttpClient类发出Http请求,但是同样受浏览器的跨域访问的限制。WebAssembly很强大,但是再强大也跳不出浏览器的限制。

在这个在线编译、运行C#代码的工具中,我希望用户可以使用Console.WriteLine()、Console.ReadLine()来和用户进行输出和输入的交互。然而,在Web Assembly中,Console.WriteLine()是在开发人员工具的控制台中输出,相当于执行JavaScript中的console.log();在Web Assembly中,Console.ReadLine()无法使用。因此,我编写了一个同名的Console类,并且提供了WriteLine()、ReadLine()方法的实现,把它们分别用JavaScript中的alert()、prompt()两个函数来实现。在使用Roslyn编译用户编写的代码的时候,使用我这个自定义的Console类的程序集来代替System.Console.dll,这样用户编写的代码中的Console类就调用我自定义的类了。

九、 项目的演示和代码地址

我把这个项目部署到了互联网上,大家可以访问https://block.youzack.com/editor.html来使用它。效果如Figure 2所示。

Figure 2运行效果

在代码编辑器中编写C#代码,然后点击【Run】按钮就可以看到代码的编译、运行效果。如果代码有编译问题,界面也会显示出来详细的编译错误信息。

项目的开源地址为:https://github.com/yangzhongke/WebCSC

十、 总结

自从.NET 6开始,我们可以脱离传统的侵入性强的Blazor WebAssembly框架,从而使用C#编写轻量级、无侵入的WebAssembly程序,从而和Javascript一起协同开发,让项目在开发效率、工程化管理等方面取得更好的效果。

本文介绍了用C#开发无侵入的WebAssembly组件,而且分享了一个在浏览器端编写、编译和运行C#开发的开源项目。

不用Blazor WebAssembly,开发在浏览器端编译和运行C#代码的网站的更多相关文章

  1. 学习编译并运行C代码

    以<UNIX网络编程>中的代码为例,学习如何编译并运行C代码. 根据 UNIX网络编程(第3版)环境搭建——使用MAC OSX10.10,能够成功运行 1.下载本书的头文件及示例源码原书地 ...

  2. 一起学Blazor WebAssembly 开发(1)

    最近blazor的WebAssembly 正式版出来了,正好手头有一个项目采用的前后端分离模式做的,后端用的abp vnext(.net core 的一个很著名的框架)框架开发的,其实前端之前考虑的使 ...

  3. 一起学Blazor WebAssembly 开发(3)

    接着上篇,本篇开始讲下实现登录窗口,先看下大概的效果图: 打开的效果,没有美工美化 点登录校验得到不能为空 我在做blazor时用到了一个ui框架,这个框架名叫Ant Design blazor(ht ...

  4. 一起学Blazor WebAssembly 开发(2)

    上篇文章讲了Blazor的两种模式的区别及各自的使用场景,本篇就开始学习WebAssembly模式,本篇主要学习的是创建项目及认识项目结构: 创建项目 选择Blazor应用 选择WebAssembly ...

  5. 从零开始一起学Blazor WebAssembly 开发(4)

    登录模块基本完成了,登录主要用了以下几个点: 1.后端采用的Abp Vnext 框架,这个框架自带的IdentityServer4用户角色权限控制,这个框架登录研究了好一阵子,有几个坑这里说下: 1) ...

  6. less学习-浏览器端编译(一)

    demo地址 http://www.qq210.com/shoutu/android 1.下载less包,官网 2.引入less文件 <link rel="stylesheet/les ...

  7. iOS开发:记录开发中遇到的编译或运行异常以及解决方案

    1.部署到真机异常 dyld`dyld_fatal_error: ->  0x120015088 <+0>: brk    #0x3 dyld: Library not loaded ...

  8. 编写一个shell脚本来编译并运行java代码

    概述 编译和运行java分别要用到javac命令和java命令,虽然可以使用IDE(比如eclipse,InteliJ,NetBean...),按一下快捷键就可以实现编译并运行,但是,在之前还要配置一 ...

  9. Blazor WebAssembly 候选版迁移手记

    前言 之前我写过一篇关于 Blazor WebAssembly 的文章浏览器中的 .Net Core —— Blazor WebAssembly 初体验,如今已经更新到 RC-1,与预览版有着较大的差 ...

  10. ASP.NET Core Blazor WebAssembly 之 .NET JavaScript互调

    Blazor WebAssembly可以在浏览器上跑C#代码,但是很多时候显然还是需要跟JavaScript打交道.比如操作dom,当然跟angular.vue一样不提倡直接操作dom:比如浏览器的后 ...

随机推荐

  1. JVM调优工具使用手册

    ​ 作为Java开发人员,我们肯定知道JDK的bin目录下有"java.exe"."javac.exe"这两个命令工具,这也是我们平时用得最多的工具.但其实bi ...

  2. typora实现多平台发布文章

    源码下载 前言 之前写过一片文章,typora 使用CSDN作为图床,用来存储 markdown 文章的图片资源文件.后来发现 typora 还可以自定义导出命令,那么也可以利用这个功能实现直接发布到 ...

  3. mingw编译opencv动态链接库和静态链接库及使用方法

    前言 我一直不知道编译的过程以及cmake, make 这些工具是干什么的,所有抽时间研究了一下. 简单来说就是 cmake 是根据 CMakeLists.txt 用来生成 makefile文件的.而 ...

  4. Golang反射获得变量类型和值

    1. 什么是反射 反射是程序在运行期间获取变量的类型和值.或者执行变量的方法的能力. Golang反射包中有两对非常重要的函数和类型,两个函数分别是: reflect.TypeOf 能获取类型信息re ...

  5. 【Java SE】Day05数组

    一.数组的定义和访问 1.初始化 动态new int[10];--默认值 静态new int[]{1,2,3};,省略为{1,2,3}; 2.访问 长度arr.length属性(数组的属性) 打印数组 ...

  6. 【Java EE】Day06 JDBC连接池介绍、C3P0连接池实现、Druid连接池实现、JDBCTemplate

    一.数据库连接池介绍 1.引入 之前:每次都要获取连接释放连接 现在:连接重复使用 2.概念: 存放数据库连接的容器 3.实现 DataSource接口 三种实现 标准实现 连接池实现 C3P0 Dr ...

  7. 【大数据面试】sqoop:空值、数据一致性、列式存储导出、数据量、数据倾斜

    一.有没有遇到过问题,怎么进行解决的 1.空值问题 本质:hive底层存储空数据使用\n<==>MySQL存储空数据使用null 解决:双向导入均分别使用两个参数☆,之前讲过 2.数据一致 ...

  8. 【JUC】交换器Exchanger详解

    欢迎关注专栏[JAVA并发] 前言 JDK中提供了不少的同步工具,现在分享一个相对比较冷门的同步工具--交换器(Exchanger).你知道Exchanger的作用是什么吗?实现机制是什么?可以用来做 ...

  9. Jgit的使用笔记

    原文:Jgit的使用笔记 - Stars-One的杂货小窝 之前整的一个系统,涉及到git代码的推送,是通过cmd命令去推送的,然后最近在产品验收的时候,测试部门随意填了个git仓库,然后导致仓库代码 ...

  10. js 定时器(setInterval)

    js 定时器 分类 编程技术 js 定时器有以下两个方法: setInterval() :按照指定的周期(以毫秒计)来调用函数或计算表达式.方法会不停地调用函数,直到 clearInterval() ...