之前已經實現過Blazor
線上編譯了,現在我們 實現一個簡單的在瀏覽器執行的編輯器,並且讓他可以編譯我們的C#程式碼,
技術棧:
Roslyn 用於編譯c#
程式碼
[monaco](microsoft/monaco-editor: A browser based code editor (github.com)) 用於提供語法高亮和程式碼的智慧提示
問:在瀏覽器編譯有什麼用?我可以在電腦編譯還可以偵錯,為什麼要在瀏覽器中去編譯程式碼?
答:對比某些場景,比如一些Blazor元件庫,提供一個簡單的編輯框,在編輯框中可以編輯元件程式碼,並且實時看到元件動態渲染效果,這樣是不是會提高一些開發效率?或者說在某些學生,可能剛剛入門,還沒有開發裝置,想著熟悉c#,使用線上編輯是不是更簡單?
問:WebAssembly不是打包幾十MB嗎?那豈不是下載很久?
答: 可以參考這個部落格 如何將WebAssembly優化到1MB,Blazor WebAssembly的優化方案。最小可以到1MB,其實並不會很大
問:是否有範例專案?
答:Blazor 線上編輯器 這是一個可以在瀏覽器動態編譯Blazor的編輯器,
實現我們建立一個空的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的分享