实现一个简单的在浏览器运行Dotnet编辑器

之前已经实现过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 如图所示
实现一个简单的在浏览器运行Dotnet编辑器

然后删除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; } 

执行我们的项目,效果如图:
实现一个简单的在浏览器运行Dotnet编辑器

示例地址: GitHub

来着token的分享

发表评论

相关文章