不用Blazor WebAssembly,開發在瀏覽器端編譯和運行C#代碼的網站

来源:https://www.cnblogs.com/rupeng/archive/2023/02/10/17107662.html
-Advertisement-
Play Games

本文中,我將會為大家分享一個如何用.NET技術開發“在瀏覽器端編譯和運行C#代碼的工具”,核心的技術就是用C#編寫不依賴於Blazor框架的WebAssembly以及Roslyn技術。 一、 為什麼要開發這樣的工具? 對於編程初學者來講,開發環境的安裝配置是一個令人頭疼的事情,如果能讓初學者不用做任 ...


本文中,我將會為大家分享一個如何用.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#開發的開源項目。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Rust 語言由 Mozilla 開發,最早發佈於 2014 年 9 月,是一種高效、可靠的通用高級語言。其高效不僅限於開發效率,它的執行效率也是令人稱贊的,是一種少有的兼顧開發效率和執行效率的語言。 ...
  • Python裝飾器實例講解(二) Python裝飾器實例講解(一) 你最好去看下第一篇,雖然也不是緊密的鏈接在一起 參考B站碼農高天的視頻,大家喜歡看視頻可以跳轉忽略本文:https://www.bilibili.com/video/BV19U4y1d79C 一鍵三連哦 本文的知識點主要是 ​ 類裝 ...
  • Mybatis 作為國內開發中常用到的半自動 orm 框架,相信大家都很熟悉,它提供了簡單靈活的xml映射配置,方便開發人員編寫簡單、複雜SQL,在國內互聯網公司使用眾多。 本文針對筆者日常開發中對 Mybatis 占位符 #{} 和 ${} 使用時機結合源碼,思考總結而來 Mybatis 版本 3 ...
  • 安裝Rust 參考文檔,指定安裝目錄和鏡像配置。 rustc --version 檢查是否安裝成功。 構建程式 使用rustc編譯運行 rustc 編譯:rustc main.js 運行: Windows:.\main.exe 或 main Linux:./main 註意: 編譯成功後會生成一個二進 ...
  • java中線程的實現方式 繼承Thread類 代碼 : package ThreadImplamen; import static java.lang.Thread.sleep; public class threadImp { public static void main(String[] ar ...
  • Maven 用的好好的,gradle這個程咬金冒出來了! 沒法子,咱們乾! Gradle 目錄結構: 各文件作用 build.gradle | 相當於maven的pom,主要定義依賴文件,編譯版本等信息 gradlew | Linux下的shell腳本,內部調用的是本項目中的gradle 目錄下的g ...
  • 教程簡介 初學者敏捷數據科學教程 - 從簡單和簡單的步驟開始學習敏捷數據科學,包括簡介,方法概念,數據科學過程,敏捷工具和安裝,敏捷數據處理,SQL與NoSQL,NoSQL和數據流編程,收集和顯示,數據可視化,數據豐富,使用報告,預測的作用,使用PySpark提取功能,構建回歸模型,部署預測系統,S ...
  • ​本文在CSDN"彭_Yu的博客"同步發表 目錄 1.要點 2.運行原理 3.異或演算法簡介 4.運行效果 5.實現過程 5.1文件結構 5.2建立資料庫 5.3 Python代碼 ​ 編輯 註:程式實例可到文末下載 1.要點 1.tkinter界面設計 2.SQLite資料庫操作 3.字元串異或運算 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...