實現一個簡單的在瀏覽器執行Dotnet編輯器

2023-02-10 21:01:16

之前已經實現過Blazor線上編譯了,現在我們 實現一個簡單的在瀏覽器執行的編輯器,並且讓他可以編譯我們的C#程式碼,

技術棧:

Roslyn 用於編譯c#程式碼

[monaco](microsoft/monaco-editor: A browser based code editor (github.com)) 用於提供語法高亮和程式碼的智慧提示

WebAssembly線上編譯使用場景

問:在瀏覽器編譯有什麼用?我可以在電腦編譯還可以偵錯,為什麼要在瀏覽器中去編譯程式碼?

答:對比某些場景,比如一些Blazor元件庫,提供一個簡單的編輯框,在編輯框中可以編輯元件程式碼,並且實時看到元件動態渲染效果,這樣是不是會提高一些開發效率?或者說在某些學生,可能剛剛入門,還沒有開發裝置,想著熟悉c#,使用線上編輯是不是更簡單?

問:WebAssembly不是打包幾十MB嗎?那豈不是下載很久?

答: 可以參考這個部落格 如何將WebAssembly優化到1MB,Blazor WebAssembly的優化方案。最小可以到1MB,其實並不會很大

問:是否有範例專案?

答:Blazor 線上編輯器 這是一個可以在瀏覽器動態編譯Blazor的編輯器,

建立WebAssembly

實現我們建立一個空的Blazor WebAssembly的專案 ,並且命名為WebEditor 如圖所示

然後刪除Pages\Index.razor_Imports.razor,App.razor,MainLayout.razor檔案

專案新增包參照,將以下程式碼copy到專案檔案中新增參照

<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" />

建立Compile.cs,用於編寫編譯工具,新增以下程式碼,在這裡我們使用Roslyn編譯我們的C#程式碼,並且執行,這裡還會提供 Execute方法供js呼叫


public class Compile
{
    /// <summary>
    /// 定義需要載入的程式集,相當於專案參照第三方程式集
    /// </summary>
    static List<string> ReferenceAssembly = new(){
        "/_framework/System.dll",
        "/_framework/System.Buffers.dll",
        "/_framework/System.Collections.dll",
        "/_framework/System.Core.dll",
        "/_framework/System.Linq.Expressions.dll",
        "/_framework/System.Linq.Parallel.dll",
        "/_framework/mscorlib.dll",
        "/_framework/System.Linq.dll",
        "/_framework/System.Console.dll",
        "/_framework/System.Private.CoreLib.dll",
        "/_framework/System.Runtime.dll"
    };

    private static IEnumerable<MetadataReference>? _references;
    private static CSharpCompilation _previousCompilation;

    private static object[] _submissionStates = { null, null };
    private static int _submissionIndex = 0;

    /// <summary>
    /// 注入的HttpClient
    /// </summary>
    private static HttpClient Http;

    /// <summary>
    /// 初始化Compile
    /// </summary>
    /// <param name="http"></param>
    /// <returns></returns>
    public static void Init(HttpClient http)
    {
        Http = http;
    }

    [JSInvokable("Execute")]
    public static async Task<string> Execute(string code)
    {
        return await RunSubmission(code);
    }

    private static bool TryCompile(string source, out Assembly? assembly, out IEnumerable<Diagnostic> errorDiagnostics)
    {
        assembly = null;

        var scriptCompilation = CSharpCompilation.CreateScriptCompilation(
            Path.GetRandomFileName(),
            CSharpSyntaxTree.ParseText(source, CSharpParseOptions.Default.WithKind(SourceCodeKind.Script)
                .WithLanguageVersion(LanguageVersion.Preview)), _references,
            // 預設參照的程式集
            new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, usings: new[]
            {
                "System",
                "System.Collections.Generic",
                "System.Console",
                "System.Diagnostics",
                "System.Dynamic",
                "System.Linq",
                "System.Linq.Expressions",
                "System.Text",
                "System.Threading.Tasks"
            }, concurrentBuild: false), // 需要注意,目前由於WebAssembly不支援多執行緒,這裡不能使用並行編譯
            _previousCompilation
        );

        errorDiagnostics = scriptCompilation.GetDiagnostics().Where(x => x.Severity == DiagnosticSeverity.Error);
        if (errorDiagnostics.Any())
        {
            return false;
        }

        using var peStream = new MemoryStream();
        var emitResult = scriptCompilation.Emit(peStream);
        if (emitResult.Success)
        {
            _submissionIndex++;
            _previousCompilation = scriptCompilation;
            assembly = Assembly.Load(peStream.ToArray());
            return true;
        }

        return false;
    }

    /// <summary>
    /// 執行Code
    /// </summary>
    /// <param name="code"></param>
    /// <returns></returns>
    private static async Task<string> RunSubmission(string code)
    {
        var diagnostic = string.Empty;
        try
        {
            if (_references == null)
            {
                // 定義零時集合
                var references = new List<MetadataReference>(ReferenceAssembly.Count);
                foreach (var reference in ReferenceAssembly)
                {
                    await using var stream = await Http.GetStreamAsync(reference);

                    references.Add(MetadataReference.CreateFromStream(stream));
                }

                _references = references;
            }

            if (TryCompile(code, out var script, out var errorDiagnostics))
            {
                var entryPoint = _previousCompilation.GetEntryPoint(CancellationToken.None);
                var type = script.GetType($"{entryPoint.ContainingNamespace.MetadataName}.{entryPoint.ContainingType.MetadataName}");
                var entryPointMethod = type.GetMethod(entryPoint.MetadataName);

                var submission = (Func<object[], Task>)entryPointMethod.CreateDelegate(typeof(Func<object[], Task>));

                // 如果不進行新增會出現超出索引
                if (_submissionIndex >= _submissionStates.Length)
                {
                    Array.Resize(ref _submissionStates, Math.Max(_submissionIndex, _submissionStates.Length * 2));
                }
                // 執行程式碼
                _ = await ((Task<object>)submission(_submissionStates));

            }

            diagnostic = string.Join(Environment.NewLine, errorDiagnostics);

        }
        catch (Exception ex)
        {
            diagnostic += Environment.NewLine + ex;
        }
        return diagnostic;
    }
}

修改Program.cs檔案,在這裡我們注入了HttpClient,並且傳遞到了Compile.Init中;


var builder = WebAssemblyHostBuilder.CreateDefault(args);

builder.RootComponents.Add<HeadOutlet>("head::after");

builder.Services.AddScoped(sp => new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) });

var app = builder.Build();

// 獲取HttpClient傳遞到初始化編譯
Compile.Init(app.Services.GetRequiredService<HttpClient>());

await app.RunAsync();

編寫介面

建立wwwroot/index.html 我們將使用monaco建立我們的編輯框,通過參照cdn載入monaco的js

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="utf-8" />
    <title>WebEditor</title>
    <base href="/" />
    <link href="web-editor.css" rel="stylesheet" />
</head>

<body>
    <div>
        <div class="web-editor" id="monaco">
        </div>
        <div class="web-editor console" id="console">
        </div>
        <div class="clear" id="clear">
            清空偵錯
        </div>
        <div class="run" id="run">
            執行
        </div>
    </div>
    <!-- 設定autostart="false" 將不會自動載入web Assembly程式集 -->
    <script src="_framework/blazor.webassembly.js"></script>
    <script>
        var require = { paths: { 'vs': 'https://cdn.masastack.com/npm/monaco-editor/0.34.1/min/vs' } };
    </script>
    <script src="https://cdn.masastack.com/npm/monaco-editor/0.34.1/min/vs/loader.js"></script>
    <script src="https://cdn.masastack.com/npm/monaco-editor/0.34.1/min/vs/editor/editor.main.nls.js"></script>
    <script src="https://cdn.masastack.com/npm/monaco-editor/0.34.1/min/vs/editor/editor.main.js"></script>
    <script>
        // 等待dom載入完成
        window.addEventListener('load', function () {
            // 建立Monaco物件儲存在window中
            window.webeditor = monaco.editor.create(document.getElementById('monaco'), {
                value: `Console.WriteLine("歡迎使用Token線上編輯器");`, // 設定初始值
                language: 'csharp', // 設定monaco 語法提示
                automaticLayout: true, // 跟隨父容器大小
                theme: "vs-dark" // 主題
            });
            
            document.getElementById("run").onclick = () => {
                // 呼叫封裝的方法將編輯器的程式碼傳入
                execute(window.webeditor.getValue());
            };

            // 清空偵錯區
            document.getElementById('clear').onclick = () => {
                document.getElementById("console").innerText = '';
            }

            async function execute(code) {
                // 使用js互操呼叫WebEditor程式集下的Execute靜態方法,並且傳送引數
                code = await DotNet.invokeMethodAsync('WebEditor', 'Execute', code);
                document.getElementById("console").innerText += code;
            }
        })

    </script>
</body>

</html>

建立web-editor.css樣式檔案


/*通用樣式*/
.web-editor {
    height: 98vh; /*可見高度*/
    width: 50%;/*區塊寬度*/
    float: left; 
}

/*執行按鈕*/
.run {
    position: fixed; /*懸浮*/
    height: 23px;
    width: 34px;
    right: 8px; /*靠右上角*/
    cursor: pointer; /*顯示手指*/
    background: #3d5fab; /*背景顏色*/
    border-radius: 6px; 
    user-select: none; /*禁止選擇*/
}

/*清除按鈕*/
.clear {
    position: fixed;
    height: 23px;
    width: 69px;
    right: 45px;
    cursor: pointer;
    background: #fd0707;
    border-radius: 6px;
    user-select: none;
}

.console {
    background-color: dimgray;
    color: aliceblue;
}

執行我們的專案,效果如圖:

範例地址: GitHub

來著token的分享