Monaco Editor实现diff对比差异,vue3

0 阅读2分钟

背景

Monaco Editor 实现差异对比工具,理由是他是微软开源的,功能强大,并且他是为 VS Code 提供动力的代码编辑器。

image.png

演练场:Monaco Editor Playground

代码封装

注意点

  • worker需要引入,否则会有个toUrl的警告
  • 高度需要默认指定,所以最好设置alwaysConsumeMouseWheel: true,否则浏览器默认滚动行为会被禁止,比如说,你在这个编辑器滚动底部时,不会触发父容器滚动
  • 页面卸载时,需要释放编辑器实例
  • watch监听到数据变化时,需要重新setModel,并且还需要释放实例
<script setup lang="ts">
import * as monaco from 'monaco-editor'
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'
import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker'
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'

// 设置 Monaco worker
self.MonacoEnvironment = {
  getWorker(_: string, label: string) {
    switch (label) {
      case 'json':
        return new jsonWorker()
      case 'css':
      case 'scss':
      case 'less':
        return new cssWorker()
      case 'html':
      case 'handlebars':
      case 'razor':
        return new htmlWorker()
      case 'typescript':
      case 'javascript':
        return new tsWorker()
      default:
        return new editorWorker()
    }
  },
}

interface Props {
  oldData: unknown
  newData: unknown
  language?: string
  height?: string
  theme?: string
}

const props = withDefaults(defineProps<Props>(), {
  language: 'json',
  height: '300px',
  theme: 'vs', // vs vs-dark hc-black
})

interface EditorOptions extends monaco.editor.IDiffEditorConstructionOptions {
  theme: string
}

const editorOptions: EditorOptions = {
  theme: props.theme,
  automaticLayout: true, // 自动布局
  readOnly: true, // 只读模式
  renderSideBySide: true, // false: 内联对比
  scrollBeyondLastLine: false, // 不允许滚动到最后一行之后
  hideUnchangedRegions: {
    enabled: true, // 折叠
    revealLineCount: 40,
    minimumLineCount: 1,
    contextLineCount: 1,
  },
  scrollbar: {
    alwaysConsumeMouseWheel: false, // 允许鼠标滚轮滚动
  },
}

const diffEditorContainer = ref<HTMLElement | null>(null)
let diffEditorInstance: monaco.editor.IStandaloneDiffEditor | null = null

const oldDataStr = computed(() => simpleStringify(props.oldData))
const newDataStr = computed(() => simpleStringify(props.newData))

function createModels(original: string, modified: string, language: string) {
  const originalModel = monaco.editor.createModel(original, language)
  const modifiedModel = monaco.editor.createModel(modified, language)
  return { originalModel, modifiedModel }
}

function simpleStringify(value: unknown): string {
  if (value === null || value === undefined) return ''

  const valueType = typeof value
  if (['number', 'boolean', 'string'].includes(valueType)) return String(value)

  // JSON.stringify 会跳过函数和 symbol;若存在循环引用则会抛错。
  // 这里不考虑循环引用情况,因为json配置不会有这种情况

  try {
    return JSON.stringify(value, null, 2)
  } catch {
    return typeof value === 'object' ? '[Object parse error]' : String(value)
  }
}

watch([oldDataStr, newDataStr], ([newOriginal, newModified]) => {
  if (!diffEditorInstance) return

  const currentModel = diffEditorInstance.getModel()
  if (
    currentModel?.original.getValue() === newOriginal &&
    currentModel.modified.getValue() === newModified
  ) {
    return
  }

  currentModel?.original?.dispose()
  currentModel?.modified?.dispose()

  const { originalModel, modifiedModel } = createModels(newOriginal, newModified, props.language)
  diffEditorInstance.setModel({ original: originalModel, modified: modifiedModel })
})

onMounted(() => {
  if (!diffEditorContainer.value) return

  diffEditorInstance = monaco.editor.createDiffEditor(diffEditorContainer.value, editorOptions)

  const { originalModel, modifiedModel } = createModels(
    oldDataStr.value,
    newDataStr.value,
    props.language
  )
  diffEditorInstance.setModel({ original: originalModel, modified: modifiedModel })
})

onBeforeUnmount(() => {
  if (diffEditorInstance) {
    const model = diffEditorInstance.getModel()
    model?.original?.dispose()
    model?.modified?.dispose()
    diffEditorInstance.dispose()
    diffEditorInstance = null
  }
})
</script>

<template>
  <div ref="diffEditorContainer" class="monaco-diff-editor" :style="{ height: height }" />
</template>

<style lang="scss" scoped>
.monaco-diff-editor {
  width: 100%;
}
</style>

具体使用

<MonacoDiff old-data="111" new-Data="222" />