如何使用React和MonacoEditor实现网页版VSCode?
本项目是基于MonacoEditor使用React实现的WebVSCodeDemo。它的主要功能是让TypeScript/JavaScript可以直接在浏览器中编写和运行。此外,它还包括以下功能:支持部分语言服务,例如TS类型检查、代码补全、代码错误检查、代码格式化等;编辑器支持ES6模块语法导入/导出;可以添加和删除多个Tab项;标签页拖拽排序;控制台输出和显示;edithistoryrollback等。我们来看看它是如何工作的。使用MonacoEditorMonacoEditor是由ErichGamma领导的团队开发的网络编译器。关于MonacoEditor最早可以追溯到2011年,最早的Monaco是一款在微软内外部web产品中广泛使用的编辑器控件。众所周知的是早期的VisualStudioOnline。VSOnline是2013年推出的产品。界面与旧版本的VSCode非常相似。可以说是VSCode把VSOnline搬到了桌面,新的GithubCodespaces把它搬到了web。在React项目中使用MonacoEditor时,有两个比较成熟的组件库react-monaco-editor和@monaco-editor/react可供选择。@monaco-editor/react在这里推荐,因为它不需要额外的webpack(rollup/parcel/etc)配置或插件。#yarninstallyarnadd@monaco-editor/reactimportReactfrom'react';从'@monaco-editor/react'导入编辑器,{monaco};functionMonacoEditor(){return()}exportdefaultMonacoEditor;代码执行与输出MonacoEditor是一款文本编辑器(支持语法高亮、自动补全、悬停提示等),不具备代码执行功能,我们可以通过Function函数来模拟代码执行的效果。letuserCode='console.log("helloworld")'try{Function(userCode)()}catch(e){console.log(e)}直接调用Function([functionBody])动态创建函数返回是为functionBody创建的匿名函数。运行TypeScriptTypeScript不能直接在浏览器中运行,需要编译器将其编译成JavaScript然后运行。幸运的是,MonacoEditor提供了一个API,可以将TypeScript代码编译成JavaScript,通过获取编译后的代码就可以达到运行的目的。consttsClient=awaitmonaco.languages.typescript.getTypeScriptWorker().then(worker=>worker(runnerModel.uri));这将编译当前模型中的代码(在VSCode中,一个模型基本上就是一个文件),然后获取返回的JavaScript并运行它。注意:每个编辑器的代码内容等信息都保存在ITextModel中。模型中存储了文档内容、文档语言、文档路径等一系列信息。当编辑器关闭时,模型保留在内存中。consttsClient=awaitmonaco.languages.typescript.getTypeScriptWorker().then(worker=>worker(runnerModel.uri));constemittedJS=(awaittsClient.getEmitOutput(runnerModel.uri.toString()))try{函数(emittedJS)();}catch(e){...}这里显示的是控制台,我们可以在线在编辑器中执行TypeScript或者JavaScript代码,现在需要显示执行后的结果,需要实现控件A用于显示输出结果的平台组件。项目中使用了React组件console-feed,可以显示来自当前页面、iframe或跨服务器传输的控制台日志。importReact,{useState,useEffect}from'react'import{Console,Hook,Unhook}from'console-feed'constLogsContainer=()=>{const[logs,setLogs]=useState([])//运行一次!useEffect(()=>{Hook(window.console,(log)=>setLogs((currLogs)=>[...currLogs,log]),false)return()=>Unhook(window.console)},[])return}export{LogsContainer}支持多个控制台我们希望每个页面有多个编辑器,默认情况下,他们的控制台都会打印相同的消息,因为我们是从同一个控制台读取日志。我们如何将控制台消息与发送它们的编辑器隔离开来?我们让每个编辑器输出一个唯一的编辑器ID作为覆盖源的最后一个参数,以区分console.log消息来源。letconsoleOverride=`letconsole=(function(oldCons){return{...oldCons,log:function(...args){args.push("${editorId}");oldCons.log.apply(oldCons,args);},warn:function(...args){args.push("${editorId}");oldCons.warn.apply(oldCons,args);},错误:function(...args){args.push("${editorId}");oldCons.error.apply(oldCons,args);},};})(window.console);`;try{Function(consoleOverride+emittedJS)();}catch(e){...SupportmultiplefiletabsMonacoEditor没有自带tabs,这里加入tab功能,实现tabs的创建和删除。当点击“+”按钮时,会弹出一个输入框和一个文件类型的下拉框。下拉框预设了ts和js两种文件类型,我们可以选择编辑什么类型的文件。导出默认函数NewFileButton({plusModel}:newFileButtonProps){return(
setOpenMenu(true)}>{openMenu&&({if(e.key==='Enter'){createModelOnEnter();setOpenMenu(false);}}}>.ts.jsdiv>)} )}回车时会调用addNewModel函数,此时会在页面中添加一个Tab页,每个Tab页对应一个新的模型。导出默认函数TopBar({editorId,modelsInfo}:TopBarProps){...const[models,setModels]=useModels();constplusModel=(文件名:字符串,语言:'javascript'|'typescript'|'json')=>addNewModel(setModels);return(
{models&&models.filter(model=>!model.shown).map((model,index)=>())} );}拖拽排序tabs拖拽布局使用react-dnd,效果和VSCode一样。react-dnd是一组React高级组件。使用时,只需要用相应的API包装目标组件,即可实现拖动或接受被拖动元素的功能。项目使用useDrag和useDrop这两个Hooks的组合,来达到拖拽排序的目的。//下面只展示核心代码import{useDrag,useDrop}from'react-dnd';exportdefaultfunctionTab({model,index,dragTabMove,deleteTab,}:TabProps){//useDrag提供了一种将组件用作拖动动态源连接到DnD系统的方法。const[{isDragging},drag]=useDrag({item:{type:'moveIdx',index},collect:monitor=>({isDragging:!!monitor.isDragging(),}),});//useDrop提供一种将组件连接到DnD系统作为放置目标的方法。const[{isOver},drop]=useDrop({accept:'moveIdx',drop:(item:DragTabItem)=>{dragTabMove(item.index,index);},collect:monitor=>({isOver:!!monitor.isOver(),}),});返回(
{model.model.uri.path.substring(1)}deleteTab(index)}>x);}拖放也会更新对应模型的选中状态。函数dragTabMove(draggedIdx:number,draggedToIdx:number){if(models){letnewModels=[...models];//向左拖动if(draggedIdx>draggedToIdx){newModels.splice(draggedToIdx,0,models[draggedIdx]);newModels.splice(draggedIdx+1,1);}else{//向右拖动newModels.splice(draggedToIdx+1,0,models[draggedIdx]);newModels.splice(draggedIdx,1);}setModels(newModels);setSelectedIdx(draggedToIdx);}}支持ES6模块编辑器还支持ES6模块语法,可以使用import/export来导入/导出模块。首先,我们获取所有标签页对应的模型,从所选模型开始深度优先遍历(DFS),并使用正则表达式从每个模型的关联中生成依赖图。导出默认函数getModelsInOrder(currentModel,monaco){constallModels=monaco.editor.getModels();//从选择的模型开始,进行DFS(深度优先遍历)分析"']*)["']/gm;letimportIndices=(model.getValue().match(importRegex)??[])//获取导入字符串.map((s)=>s.match(/["']([^"']*)["']/)![1])//查找名称.map((s)=>allModels.findIndex((findImportModel)=>s===findImportModel.uri.path.substring(1).replace(/\.[^.]*$/,"")//将格式化导入并比较格式化后的文件名))).filter((index)=>index!==-1);返回导入索引;});然后对生成的依赖进行拓扑排序(这里使用LeetCode久经考验的代码),将文件堆叠在一起。//https://leetcode.com/problems/course-schedule-ii/discuss/146326/JavaScript-DFSconstTopoSort=function(ranFile:number,deps:number[][]){constres:number[]=[];constseeing=newSet();constseen=newSet<数字>();如果(!dfs(ranFile)){返回[];}返回资源;functiondfs(v:number){if(seen.has(v)){返回真;}if(seeing.has(v)){returnfalse;}seeing.add(v);for(letnvofdeps[v]){if(!dfs(nv)){returnfalse;}}seeing.delete(v);看到。添加(v);res.push(v);返回真;}};导出默认拓扑排序;摩纳哥模型在同一窗口中共享,因此可以在同一页面上导入来自不同编辑器的代码。蛮力做事的方式并不总是有效,如果你打开名为“0.ts”的文件,它会向你显示生成的代码,以便你可以诊断问题(这里我们得到重复语句的错误).自定义文件我为文件提供了一些不同的选项,您可以自定义这些选项以确定最初选择哪个选项卡、文件是否应为只读、文件是否应显示等。导出类型modelInfoType={notInitial?:boolean;显示?:布尔值;只读?:布尔值;测试?:布尔值;文件名:字符串;值:字符串;语言:“打字稿”|“javascript”|“JSON”;};编写交互式内容要为编辑器创建初始状态,您可以创建一个空编辑器,创建一个新文件,然后单击右上角的<>按钮,这会将modelsInfo配置复制到剪贴板。importReactfrom"react";importEditorfrom"react-run-code";functionApp(){return;}exportdefaultApp;现在你可以粘贴[{"value":"console.log(\"makeanewfile\")","filename":"new.ts","language":"typescript"}]来替换modelsInfo={[]中的源代码}of[]。(如上图)最后,我目前正在做一个浏览器支持C/C++语言服务(LSP之后)相关的项目(下图)。以后会总结这方面的知识,期待关注。原文参考:HowToEmbedVSCodeIntoABrowserWithReact