之前已经实现过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
如图所示
然后删除PagesIndex.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的分享