• 作者:老汪软件技巧
  • 发表时间: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,服务端渲染会报错,因此在客户端的时候,动态引入使用:


<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 即可:


<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、对比色、背景色修改

编辑器默认的对比背景色,大多时候都不符合我们项目的要求,因此需要进行修改,但是修改对比背景色并不容易,尝试了几种方法,才顺利解决。

_app接入在线客服功能_接入翻译

第一种方法:使用 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


<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,
})

注意: 大家在修改编辑器的样式时,一定要谨慎,最好通过具体的配置或者主题修改,而不是直接操作样式,我就遇到过,修改某部分的宽度(或者加个边距)会影响编辑器功能的使用,可能是编辑器的各个部分的样式都是规定好的,而且会相互影响。