- 作者:老汪软件技巧
- 发表时间:2024-09-01 17:03
- 浏览量:
一、需求背景
最近在负责公司翻译网站的开发,需要实现原文和译文之间的对比功能,这个对比功能主要有三点:
行保持:原文和译文同一段高度需要保持一致,以最高的为准,假设同一段(行)文本,原文是 2 行的高度,译文是 3 行,那么原文此段下方需要填充空行,保持左右两边高度一致;差异高亮显示:译文相对于原文,不同的字词需要高亮显示;同步滚动:类似于 MD 编辑器的同步滚动。
刚开始是想自己去实现这几个功能,在 MD 编辑器的基础上实现,同步滚动倒是没什么问题,关键是另外两点很难实现,初始的想法是:行保持采用手动操作 DOM 实现,差异高亮显示使用 jsdiff 实现,但是效果以及性能特别差,也许是自己的能力不行,无奈之下只能放弃,另寻出路。
后面同事给我推荐了一款编辑器 Monaco Editor,这款编辑器功能十分强大,自带差异编辑模式。
二、Monaco Editor
Monaco Editor 由微软开发,它和 VSCode 在编辑代码,交互以及 UI 上几乎是一模一样,Monaco Editor (基于浏览器)可以看作是一个编辑器控件,只提供了基础的编辑器与语言相关的接口,可以被用于任何基于 Web 技术构建的项目中(不支持移动浏览器)。
普通的编辑器模式:
差异编辑器模式:
Monaco Editor 的应用是十分广泛的,很多平台都用到它作为编辑器,举几个常见的例子:
Github在线编辑器:
在线WebIDE CodeSandBox:
通过官网的 DEMO,我简单测试了下,发现该编辑器已经提供了上面所说的 3 个对比功能点,因此采用这款编辑器进行开发,但是需要去掉默认的许多配置,以及修改样式,将其进行彻底的改造!
Monaco Editor 提供了很多的配置以及 API,大家可以根据需要自行查阅:microsoft.github.io/monaco-edit…
三、最终效果展示
四、在 Nuxt3 项目中接入
在 Nuxt3 项目中接入 Monaco Editor 编辑器的过程中踩了很多的坑,网站很多文章都是关于 webpack 项目接入的,而 Nuxt3 内部的构建打包工具用的是 Vite,在这里和大家分享接入的过程!
首先项目的 Nuxt 版本:"^3.7.0",安装 monaco-editor 的版本为:0.50.0
npm install monaco-editor@0.50.0
首先封装个组件,这里不能直接使用 import 语句导入 monaco-editor,服务端渲染会报错,因此在客户端的时候,动态引入使用:
<section id="monacoEditorDiff" class="monaco-editor-box">section>
<script setup lang="ts">
import { editorOptions } from './editor-options';
const props = defineProps({
options: {
type: Object,
default: () => ({
theme: 'vs',
}),
},
oldValue: {
type: String,
},
newValue: {
type: String,
},
});
let editor: any;
let originalModel: any;
let modifiedModel: any;
let editorWorker: any;
let monaco: any;
let modifiedEditor: any;
let originalEditor: any;
const emits = defineEmits(['originalChange']);
const init = () => {
editor = monaco.editor.createDiffEditor(document.getElementById('monacoEditorDiff'), {
...editorOptions,
...props.options,
});
//创建左右编辑器
originalModel = monaco.editor.createModel(props.oldValue, 'plaintext');
modifiedModel = monaco.editor.createModel(props.newValue, 'plaintext');
editor.setModel({
original: originalModel,
modified: modifiedModel,
});
originalEditor = editor.getOriginalEditor();
modifiedEditor = editor.getModifiedEditor();
// 原始模型的内容发生变化时触发的操作
originalModel.onDidChangeContent(() => {
const OriginalText = originalModel.getValue();
emits('originalChange', OriginalText);
});
modifiedModel.onDidChangeContent(() => {});
originalEditor.onDidBlurEditorText(() => {});
};
const asyncFn = async () => {
monaco = await import('monaco-editor');
editorWorker = await import('monaco-editor/esm/vs/editor/editor.worker?worker');
//解决 Monaco Editor 无法正确加载其所需的 Web Worker
self.MonacoEnvironment = {
getWorker(workerId, label) {
return new editorWorker();
},
};
//初始化编辑器
init();
};
onMounted(() => {
asyncFn();
});
onBeforeUnmount(() => {
editor && editor.dispose();
});
script>
使用组件,传入左右编辑器对应的 value 即可:
<section class="monaco-editor-container">
<MonacoEditor
:old-value="sourceValue"
:new-value="targetValue"
@original-change="originalChange"
/>
section>
<script setup lang="ts">
import MonacoEditor from './components/monaco-editor.vue';
const sourceValue = ref('123')
const targetValue = ref('123aaaaa')
const originalChange = (val: string) => {
sourceValue.value = val
}
script>
五、编辑器的使用及改造5.1、常用的配置项
下面这些是我在项目里用到的编辑器配置,因为用不到编辑器里的很多默认功能,所以我把它们都去掉了,大家可以根据项目的需要进行设置:
export const editorOptions = {
value: '', // 编辑器初始显示文字
autoIndex: false, // 控制是否开启自动索引。当开启时,编辑器会自动创建索引以加速搜索和语义高亮。
fontSize: 16, // 字体大小
language: 'plaintext', //语言
readOnly: true, // 是否只读 只影响右侧
overviewRulerBorder: false, // 滚动是否有边框
cursorSmoothCaretAnimation: false, // 控制光标平滑动画的开启与关闭。当开启时,光标移动会有平滑的动画效果。
mouseWheelZoom: false, //设置是否开启鼠标滚轮缩放功能
folding: false, //控制是否开启代码折叠功能
automaticLayout: true, // 控制编辑器是否自动调整布局以适应容器大小的变化
minimap: {
// 关闭代码缩略图
enabled: false, // 是否启用预览图
},
scrollbar: {
verticalScrollbarSize: 5, // 垂直滚动条宽度,默认px
horizontalScrollbarSize: 2, // 水平滚动条高度
},
wordWrap: 'on', // 开启自动换行
roundedSelection: true, // 右侧不显示编辑器预览框
originalEditable: true, // 是否允许修改原始文本
scrollBeyondLastLine: false, // 启用该功能,滚动可以在最后一行之后增加一个屏幕大小,默认值为 true
renderLineHighlight: 'none', // 启用当前行高亮显示的呈现
contextmenu: false, // 双击菜单
columnSelection: false, // 启用列编辑 按下shift键位然后按↑↓键位可以实现列选择 然后实现列编辑
copyWithSyntaxHighlighting: false, // 是否应将语法突出显示复制到剪贴板中 即 当你复制到word中是否保持文字高亮颜色
links: false, // 是否点击链接
selectionHighlight: true,
lineNumbers: false, //显示行号
autoClosingBrackets: 'never', // 禁用自动闭合括号功能
renderIndentGuides: false, // 缩进线的呈现
suggestOnTriggerCharacters: false, // 用于控制是否在输入触发字符时显示建议
glyphMargin: false, // 设置是否显示装饰符号边距
unicodeHighlight: {
ambiguousCharacters: false, // 禁用 unicode 字符的高亮显示
},
stickyScroll: {
enabled: false, // 取消顶部吸附
},
};
这里比较坑的一点是:默认情况下编辑器最后会有一段大留白,极大影响了页面的样式效果,需要设置 scrollBeyondLastLine: false 将其去掉。
更加完整的配置可以参考这篇博客:/qq252320847… 或者直接去官方文档中查找。
5.2、对比色、背景色修改
编辑器默认的对比背景色,大多时候都不符合我们项目的要求,因此需要进行修改,但是修改对比背景色并不容易,尝试了几种方法,才顺利解决。
第一种方法:使用 CSS 修改对应盒子的样式,颜色倒是可以生效
但是会影响其他的颜色,比如鼠标选中文本的颜色,对比色均无法正常显示,没法解决这个问题,只能尝试其他的方式。
第二种方法:自定义主题,查阅了许多文档才知道可以通过定义主题的方式去修改颜色
monaco.editor.defineTheme('vs', {
base: 'vs', // 要继承的基础主题,即内置的三个:vs、vs-dark、hc-black
inherit: false, // 是否继承
rules: [
// 高亮规则,即给代码里不同token类型的代码设置不同的显示样式
{ token: '', foreground: '000000', background: 'fffffe' },
],
colors: {
// 非代码部分的其他部分的颜色,比如背景、滚动条等
},
});
但是需要找到对应的属性名称才行,正好在浏览器元素面板中可以看到 Vscode 定义的许多主题属性:
对上面的代码进行修改:
monaco.editor.defineTheme('vs', {
......
colors: {
'editor-inactiveSelectionBackground': '#ADD6FF'
'diffEditor-removedTextBackground': '#ADD6FF'
'diffEditor-insertedTextBackground': '#ADD6FF'
'editor-selectionHighlightBackground': '#ADD6FF'
},
});
遗憾的是,还是有问题,跟第一种情况是一样的,会影响其他的颜色正常显示
第三种方法:受第二种方法的启发,决定尝试修改 css 变量的值,新建 monaco-editor.less 文件,在里面覆盖默认的 CSS 变量的值
@custom-monaco-theme: {
--vscode-diffEditor-removedTextBackground: none;
--vscode-diffEditor-insertedTextBackground: #F4F8F9;
--vscode-diffEditor-insertedLineBackground: none;
--vscode-diffEditor-removedLineBackground: none;
--vscode-editor-inactiveSelectionBackground: #ADD6FF;
--vscode-editor-selectionHighlightBackground: #ADD6FF;
--vscode-diffEditor-diagonalFill: none;
--vscode-focusBorder: #ffffff;
};
.monaco-editor {
@custom-monaco-theme();
}
这种方法终于完美的修改了对比色、背景色,同时不会影响到别的东西。
5.3、鼠标光标定位到最后
在项目里翻译功能有个需求,由于翻译后的内容是流式输出的,因此在输出的时候需要滚动到编辑器的最底部。
这个听起来是很简单的,只要找到滚动的元素,然后一行代码便可以搞定:
scrollRef.value.scrollTop = scrollRef.value.scrollHeight;
但是效果不佳,存在许多问题,比如在对比编辑器中,如果左侧的内容非常多,右侧目前是空的,此时两侧的滚动条其实是同步的:
如果这个时候执行滚动到底部的操作,那么上面输出的内容就无法看到了,虽然说可以解决这个问题,但是尝试了下,会造成其他的问题,所以便放弃了这种方式。
最终采用控制鼠标光标的位置来实现平滑滚动到内容的最后:
editor = monaco.editor.createDiffEditor(document.getElementById('monacoEditorDiff'), {
...editorOptions,
});
modifiedEditor = editor.getModifiedEditor();
modifiedModel.onDidChangeContent(() => {
// 内容变化时,定位光标到最后实现平滑滚动到最后一行
const lineCount = modifiedEditor.getModel().getLineCount();
const lastLineColumn = modifiedEditor.getModel().getLineMaxColumn(lineCount);
const lastLinePosition = { lineNumber: lineCount, column: lastLineColumn };
modifiedEditor.revealPositionInCenterIfOutsideViewport(lastLinePosition);
});
5.4、预加载编辑器
由于这个编辑器的体积比较大,加载耗时,会影响用户的体验,所以我在项目中做了优化:预加载编辑器
其实就是在用户见到编辑器之前,先加载渲染一遍编辑器相关的东西,我是在 loading 页面中预加载的,进入主页面之后编辑器几乎是秒出。
首先封装个简单的组件 PrefetchMonaco.vue,与上面的 Nuxt3项目接入 中的封装是类似的,只不过将多余的代码全都去掉,给组件设置定位移出屏幕可视区域:
PrefetchMonaco.vue
<section id="monacoEditorDiffPrefetch" class="prefetch-monaco-editor">section>
<script setup lang="ts">
import { editorOptions } from './editor-options';
let editor: any;
let originalModel: any;
let modifiedModel: any;
let editorWorker: any;
let monaco: any;
const oldValue = ref('11');
const newValue = ref('11');
const init = () => {
editor = monaco.editor.createDiffEditor(document.getElementById('monacoEditorDiffPrefetch'), {
...editorOptions,
theme: 'vs',
});
//创建左右编辑器
originalModel = monaco.editor.createModel(oldValue.value, 'plaintext');
modifiedModel = monaco.editor.createModel(newValue.value, 'plaintext');
editor.setModel({
original: originalModel,
modified: modifiedModel,
});
};
const asyncFn = async () => {
monaco = await import('monaco-editor');
editorWorker = await import('monaco-editor/esm/vs/editor/editor.worker?worker');
//解决 Monaco Editor 无法正确加载其所需的 Web Worker
self.MonacoEnvironment = {
getWorker(workerId, label) {
return new editorWorker();
},
};
//初始化编辑器
init();
};
const disposeEditor = () => {
editor && editor.dispose();
};
defineExpose({ disposeEditor });
onMounted(() => {
console.log('prefetch monaco');
asyncFn();
});
onBeforeUnmount(() => {
editor && editor.dispose();
});
script>
<style scoped lang="less">
.prefetch-monaco-editor {
position: relative;
left: -9999px;
top: -9999px;
opacity: 0;
}
style>
在 loading 页面中引入使用,需要根据实际情况处理,要记得销毁编辑器实例!
<div v-if="prefetch">
<PrefetchMonaco ref="prefetchMonaco" />
div>
const prefetchMonaco = ref<InstanceType<typeof PrefetchMonaco> | null>(null);
prefetchMonaco.value?.disposeEditor();
5.5、光标错位的问题
使用 Monaco Editor 编辑器可能会遇到的一个问题:点击编辑内容某个位置光标不会在该位置定位,而是发生偏移,有时候当输入的内容越多,鼠标光标错位的就越明显。
具体可以参考:/microsoft/m…
这主要是字体导致的,因为编辑框里面的字体用了非等宽字体,导致计算不准确,Windows 下可以使用Consolas字体,编辑器默认的字体就是Consolas。
还有,设置编辑框的字体大小最好不要直接通过样式去修改,可能会影响编辑器的一些功能,可以使用配置项去指定字体大小:
monaco.editor.create(elm, {
fontSize: 14,
})
注意: 大家在修改编辑器的样式时,一定要谨慎,最好通过具体的配置或者主题修改,而不是直接操作样式,我就遇到过,修改某部分的宽度(或者加个边距)会影响编辑器功能的使用,可能是编辑器的各个部分的样式都是规定好的,而且会相互影响。