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) { }]; }