Monaco  是一个代码编辑器,大名鼎鼎的 VS Code 便是基于 Monaco 实现。
背景 在公司里做了一个 PC 端应用,应用名 RPCUI,开发语言 Objective-C。可以在该应用中发起 RPC 调用,调试接口。
RPCUI 实现了一个 JSON 编辑器,用于呈现 RPC 的输入和输出数据。这个编辑器实现原理很简单:使用 WKWebView 加载 Monaco,然后再将 webView 贴到原生视图上。
本文介绍 Monaco 使用的一些实用功能。
 
引入 package.json 1 2 3 4 5 6 7 8 9 10 11 12 {   "name" : "rpcui-editor" ,   "version" : "0.0.1" ,   "description" : "RPCUI 子项目,基于 Monaco Editor" ,   "main" : "index.js" ,   "scripts" : {},   "author" : "" ,   "license" : "MIT" ,   "dependencies" : {     "monaco-editor" : "^0.19.3"    } } 
 
初始化 index.html 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <!DOCTYPE html > <html > <head >   <title > browser-amd-editor</title >    <meta  http-equiv ="X-UA-Compatible"  content ="IE=edge"  />    <meta  http-equiv ="Content-Type"  content ="text/html;charset=utf-8"  >    <link  rel ="stylesheet"  href ="./index.css" >  </head > <body > <div  id ="container" > </div > <script  src ="../node_modules/monaco-editor/min/vs/loader.js" > </script > <script  src ="./index.js" > </script > </body > </html > 
 
index.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 let  editor;(function  (text )  {   require .config({     paths : {       'vs' : '../node_modules/monaco-editor/min/vs'      },   });   require (['vs/editor/editor.main' ], function ( )  {     editor = monaco.editor.create(document .getElementById('container' ), {       value : text,       language : option.language || 'json' ,       readOnly : !!option.readOnly,       lineNumbers : option.lineNumbers || 'on' ,       automaticLayout : true ,       wordWrap : 'on' ,       minimap : {         enabled : false ,       },       scrollBeyondLastLine : false ,     });   }); })('' ); 
 
常用功能 下面代码块是一些 util 函数。
index.js 1 2 3 4 5 6 7 8 9 10 11 12 13 function  isLastLine ( )  {  const  { lineNumber } = editor.getPosition() || {};   const  isLastLine = lineNumber === editor.getModel().getLineCount();   return  isLastLine; } function  scrollToBottom ( )  {  const  lineCount = editor.getModel().getLineCount();   editor.revealLine(lineCount);   editor.setPosition({ lineNumber : lineCount, column : 0  }); } 
 
更新 feature: 如果更新前编辑器定位在最后一行,那么内容更新后,编辑器仍然需要定位在最后一行。
index.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 function  updateText (text )  {  console .log(text);   if  (!editor) {     console .log('editor not instantiated' );     return ;   }      const  lastLine = isLastLine();      editor.setValue(text);   if  (option.scrollToBottom && lastLine) {          scrollToBottom();   } } 
 
追加 feature1: 如果追加内容前编辑器定位在最后一行,那么内容更新后,编辑器仍然需要定位在最后一行。
feature2: 即使编辑器是只读的,也能成功追加内容。
(RPCUI 中有一个打印日志的功能,追加接口主要给这个功能使用。)
index.js 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 function  appendText (text )  {  console .log(text);   if  (!editor) {     console .log('editor not instantiated' );     return ;   }      const  lastLine = isLastLine();      const  lineCount = editor.getModel().getLineCount();   const  lastLineLength = editor.getModel().getLineMaxColumn(lineCount);   const  range = new  monaco.Range(       lineCount,       lastLineLength,       lineCount,       lastLineLength   );      !!option.readOnly && editor.updateOptions({ readOnly : false  });   const  result = editor.executeEdits('' , [       { range, text, forceMoveMarkers : true  }   ])   !!option.readOnly && editor.updateOptions({ readOnly : true  });   if  (option.scrollToBottom && lastLine) {          scrollToBottom();   }   return  result; } 
 
获取 index.js 1 2 3 4 5 6 7 function  getText ( )  {  if  (editor) {     return  editor.getValue();   }   return  '' ; } 
 
原生调用 上文说到,Monaco 是先被加载到 WKWebView 中,再贴到原生视图上。本小节讲述,PC 原生应用如何与 Monaco 通信。
Monaco 初始化 的时候需要配置一些参数,可以通过跳转链接传递。
传递参数(OC代码) 1 2 3 4 5 6 7 8 9 10 11 12 13 14 NSString  *editorDir = [[NSBundle  mainBundle] pathForResource:@"rpcui-editor"  ofType:@"" ];NSURL  *editorDirURL = [NSURL  fileURLWithPath:editorDir];NSString  *editorPath = [editorDir stringByAppendingPathComponent:@"src/index.html" ];editorPath = [editorPath stringByAddingPercentEncodingWithAllowedCharacters:[NSCharacterSet  URLPathAllowedCharacterSet]]; NSString  *query = [NSString  stringWithFormat:    @"?language=%@&lineNumbers=%@&readOnly=%@&scrollToBottom=%@" ,     self .option.language,     self .option.lineNumbers,     self .option.readOnly ? @"1"  : @"" ,     self .option.scrollToBottom ? @"1"  : @"" ]; NSString  *editorUrlStr = [NSString  stringWithFormat:@"file://%@%@" , editorPath, query];NSURL  *url = [NSURL  URLWithString:editorUrlStr];[self .webView loadFileURL:url allowingReadAccessToURL:editorDirURL]; 
 
接收参数(js代码) 1 2 3 4 5 6 7 const  urlParams = new  URLSearchParams(window .location.search);const  option = {  language : urlParams.get('language' ),   readOnly : urlParams.get('readOnly' ),   lineNumbers : urlParams.get('lineNumbers' ),   scrollToBottom : urlParams.get('scrollToBottom' ), }; 
 
如上文所述,编辑器的接口已经直接暴露在了 H5 全局作用域中。原生应用调用编辑器的方式如下所示。
发起调用(OC代码) 1 2 3 4 5 6 7 8 9 10 - (void )_appendText:(NSString  *)str {     NSString  *res = [str copy ];     res = [res stringByReplacingOccurrencesOfString:@"\""  withString:@"\\\"" ];     res = [res stringByReplacingOccurrencesOfString:@"\n"  withString:@"\\n" ];     res = [res stringByReplacingOccurrencesOfString:@"\r"  withString:@"" ];     res = [res stringByReplacingOccurrencesOfString:@"'"  withString:@"\\'" ];     NSString  *jsStr = [NSString  stringWithFormat:@"appendText('%@');" , res];     [self .webView evaluateJavaScript:jsStr completionHandler:^(id  _Nullable res, NSError  * _Nullable error) {     }]; }