三十三、【扩展工具篇】代码比对:集成 Monaco Editor 实现 Diff 功能

三十三、【扩展工具篇】代码比对:集成 Monaco Editor 实现 Diff 功能

前言

准备工作
第一部分:前端环境配置 (Vite + Monaco Editor)
第二部分:前端页面与组件实现

1. 添加路由和侧边栏菜单入口
2. 创建代码比对页面 (`src/views/tools/DiffCheckerView.vue`)

第三部分:全面测试与验证

总结

前言

在日常的测试开发工作中,我们经常需要比较两段代码或文本的差异。例如,比较接口返回的新旧 JSON、对比不同版本的配置文件、或者查看代码的细微修改。为了完成这些操作,通常需要借助外部的 Diff 工具。
为了提升效率,在测试平台中集成一个代码比对工具。用户可以在两个并排的编辑器中输入或粘贴内容,系统将实时、高亮地显示两者之间的差异。

本文目标:
在测试平台中新增一个“代码比对”工具页面。用户可以在两个并排的编辑器中粘贴或输入文本/代码,系统会实时、高亮地显示两者之间的差异。

准备工作

前端项目就绪: test-platform/frontend 项目可以正常运行 (npm run dev)。
Element Plus 集成完毕。
安装 Monaco Editor 库:
在前端项目根目录 (test-platform/frontend) 下打开终端,运行:

npm install monaco-editor@latest --save

第一部分:前端环境配置 (Vite + Monaco Editor)

配置 vite.config.ts
打开 test-platform/frontend/vite.config.ts,进行如下修改:

// test-platform/frontend/vite.config.ts
import {
               fileURLToPath, URL } from 'node:url'
import {
               defineConfig, loadEnv } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig(({
               mode }) => {
              
  const env = loadEnv(mode, process.cwd(), '')

  return {
              
    plugins: [vue()],
    resolve: {
              
      alias: {
              
        '@': fileURLToPath(new URL('./src', import.meta.url))
      }
    },
    server: {
              
      host: '0.0.0.0',
      port: parseInt(env.VITE_PORT) || 5173,
      proxy: {
              
        '/api': {
              
          target: 'http://127.0.0.1:8000',
          changeOrigin: true,
        }
      }
    },
    worker: {
              
      format: 'es',
    },
    build: {
              
      rollupOptions: {
              
        output: {
              
          manualChunks: {
              
            'monaco-editor': ['monaco-editor']
          }
        }
      }
    }
  }
})

关键配置解释:

build.rollupOptions.output.manualChunks:

这是一个非常重要的配置。它告诉 Vite 在生产构建时,将所有来自 monaco-editor 包的模块打包到一个名为 monaco-editor.js 的独立文件中。
这有助于优化应用的加载性能,因为 Monaco Editor 体积较大,将其分离出来可以实现按需加载,不会阻塞主应用的渲染。
如果没有这个配置,在生产构建后你可能会遇到 Monaco Editor 的 Web Worker 加载失败或路径错误的问题。

验证配置:

保存 vite.config.ts 后,重启你的 Vite 开发服务器 (npm run dev)。
如果服务器正常启动且没有报错,说明基本配置是正确的。

第二部分:前端页面与组件实现

1. 添加路由和侧边栏菜单入口

a. 路由 (frontend/src/router/index.ts):

// ... (在 Layout 的 children 中添加)
                {
            
                  path: '/tools',
                  name: 'tools',
                  redirect: '/tools/diff-checker',
                  meta: {
             title: '工具管理', requiresAuth: true, icon: 'Tools' },
                  children: [
                    {
            
                      path: 'diff-checker',
                      name: 'diffChecker',
                      component: () => import('../views/tools/DiffCheckerView.vue'),
                      meta: {
             title: '代码比对', requiresAuth: true, icon: 'DocumentCopy' }
                    }
                  ]
                },
// ...

注意: 将“代码比对”放在一个新的“工具管理”子菜单下,这样未来可以方便地添加更多工具。

b. 侧边栏入口 (frontend/src/layout/index.vue):

// ... 在 <el-sub-menu index="/system"> 之前
                <el-sub-menu index="/tools">
                  <template #title>
                    <el-icon><Tools /></el-icon> <!-- 使用 Tools 图标 -->
                    <span>工具管理</span>
                  </template>
                  <el-menu-item index="/tools/diff-checker">
                    <el-icon><DocumentCopy /></el-icon>
                    <span>代码比对</span>
                  </el-menu-item>
                  <!-- 未来可以添加更多工具的菜单项 -->
                </el-sub-menu>
// ...

// 导入图标
import { /* ..., */ Tools  } from '@element-plus/icons-vue'
2. 创建代码比对页面 (src/views/tools/DiffCheckerView.vue)

a. 创建 src/views/tools/DiffCheckerView.vue 文件:

b. 编写 DiffCheckerView.vue

<!-- test-platform/frontend/src/views/tools/DiffCheckerView.vue -->
<template>
  <div class="diff-checker-view">
    <el-card class="box-card">
      <template #header>
        <div class="card-header">
          <span>代码比对工具</span>
          <div class="controls">
            <!-- 语言选择 -->
            <el-select v-model="language" placeholder="选择语言">
              <el-option v-for="lang in supportedLanguages" :key="lang" :label="lang.toUpperCase()" :value="lang" />
            </el-select>
            <!-- 主题切换 -->
            <el-switch
              v-model="theme"
              active-value="vs-dark"
              inactive-value="vs"
              active-text="深色模式"
              inactive-text="浅色模式"
              inline-prompt
             
            />
            <el-button-group>
              <el-button :icon="Switch" @click="swapContent">交换内容</el-button>
              <el-button :icon="Delete" @click="clearContent">清空内容</el-button>
            </el-button-group>
          </div>
        </div>
      </template>

      <!-- Monaco Diff Editor 组件 -->
      <div class="editor-container" ref="editorContainer"></div>
    </el-card>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive, onMounted, onBeforeUnmount, watch } from 'vue';
import { ElMessage } from 'element-plus';
import { Switch, Delete } from '@element-plus/icons-vue';
import * as monaco from 'monaco-editor';
import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
import jsonWorker from 'monaco-editor/esm/vs/language/json/json.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 tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker';

// 设置 Monaco Editor 的 workers
self.MonacoEnvironment = {
  getWorker(_, label) {
    if (label === 'json') {
      return new jsonWorker();
    }
    if (label === 'css' || label === 'scss' || label === 'less') {
      return new cssWorker();
    }
    if (label === 'html' || label === 'handlebars' || label === 'razor') {
      return new htmlWorker();
    }
    if (label === 'typescript' || label === 'javascript') {
      return new tsWorker();
    }
    return new editorWorker();
  }
};

const editorContainer = ref<HTMLElement | null>(null);
let diffEditor: monaco.editor.IStandaloneDiffEditor | null = null;

// 左侧编辑器内容
const originalCode = ref(`{
    "id": 1,
    "name": "Leanne Graham",
    "username": "Bret",
    "email": "Sincere@april.biz",
    "address": {
        "street": "Kulas Light",
        "suite": "Apt. 556",
        "city": "Gwenborough",
        "zipcode": "92998-3874",
        "geo": {
            "lat": "-37.3159",
            "lng": "81.1496"
        }
    },
    "phone": "1-770-736-8031 x56442",
    "website": "hildegard.org"
}`);

// 右侧编辑器内容
const modifiedCode = ref(`{
    "id": 1,
    "name": "Leanne Graham",
    "username": "Bret",
    "email": "Sincere@april.com",
    "address": {
        "street": "Kulas Light",
        "suite": "Apt. 556",
        "city": "Gwenborough",
        "zipcode": "92998-3874"
    },
    "phone": "1-770-736-8031 x56442",
    "website": "hildegard.org",
    "company": {
        "name": "Romaguera-Crona"
    }
}`);

const language = ref('json'); // 默认语言
const theme = ref('vs'); // 默认主题 'vs' (light), 可选 'vs-dark'

const supportedLanguages = [
  'json', 'javascript', 'typescript', 'html', 'css', 'python', 'yaml', 'xml', 'sql', 'shell', 'markdown'
];

// Monaco Editor 的配置项
const editorOptions = reactive({
  automaticLayout: true, // 自动布局
  readOnly: false,       // 允许编辑
  renderSideBySide: true, // 并排显示
  minimap: { enabled: true }, // 显示小地图
  scrollBeyondLastLine: false, // 禁止滚动超过最后一行
  wordWrap: 'on', // 自动换行
  fontSize: 14,
  originalEditable: true, // 允许编辑原始代码
  enableSplitViewResizing: true,
});

// 初始化编辑器
const initEditor = () => {
  if (editorContainer.value) {
    diffEditor = monaco.editor.createDiffEditor(editorContainer.value, {
      ...editorOptions,
      theme: theme.value
    });

    // 设置初始内容
    diffEditor.setModel({
      original: monaco.editor.createModel(originalCode.value, language.value),
      modified: monaco.editor.createModel(modifiedCode.value, language.value)
    });

    // 监听修改后的内容变化
    const modifiedEditor = diffEditor.getModifiedEditor();
    modifiedEditor.onDidChangeModelContent(() => {
      modifiedCode.value = modifiedEditor.getValue();
    });
  }
};

// 更新编辑器内容
const updateEditor = () => {
  if (diffEditor) {
    diffEditor.setModel({
      original: monaco.editor.createModel(originalCode.value, language.value),
      modified: monaco.editor.createModel(modifiedCode.value, language.value)
    });
  }
};

// 监听主题变化
watch(theme, (newTheme) => {
  monaco.editor.setTheme(newTheme);
});

// 监听语言变化
watch(language, () => {
  updateEditor();
});

// 交换左右两侧内容
const swapContent = () => {
  const temp = originalCode.value;
  originalCode.value = modifiedCode.value;
  modifiedCode.value = temp;
  updateEditor();
  ElMessage.success('已交换左右两侧内容');
};

// 清空所有内容
const clearContent = () => {
  originalCode.value = '';
  modifiedCode.value = '';
  updateEditor();
  ElMessage.info('已清空所有内容');
};

// 组件挂载时初始化编辑器
onMounted(() => {
  initEditor();
});

// 组件卸载前销毁编辑器
onBeforeUnmount(() => {
  if (diffEditor) {
    diffEditor.dispose();
  }
});
</script>

<style scoped lang="scss">
.diff-checker-view {
  padding: 20px;
  height: calc(100vh - 50px - 40px); // 减去顶部导航和页面 padding
  display: flex;
  flex-direction: column;

  .box-card {
    flex-grow: 1;
    display: flex;
    flex-direction: column;

    :deep(.el-card__header) {
      padding: 15px 20px;
    }

    .card-header {
      display: flex;
      justify-content: space-between;
      align-items: center;
      font-size: 1.1em;
      font-weight: bold;
    }

    :deep(.el-card__body) {
      padding: 0; // 移除卡片 body 的内边距,让编辑器占满
      flex-grow: 1;
      height: 0; // 关键:让 flex-grow 生效
    }

    .editor-container {
      height: 100%; // 编辑器容器占满 Card Body
      border: 1px solid #dcdfe6; // 给编辑器一个边框
      border-radius: 4px;
    }
  }
}
</style>

第三部分:全面测试与验证

环境验证:

重启 Vite 开发服务器。
在浏览器中打开 http://127.0.0.1:5173/tools/diff-checker
按 F12 打开开发者工具,切换到 “Network” (网络) 标签页。
刷新页面。确认没有与 monaco-editor 相关的 404 错误。你应该能看到一些 *.worker.js 文件被成功加载。

功能测试:

默认加载: 页面应能正常显示,左右编辑器中应有我们的示例 JSON,并且差异部分(emailaddress.geocompany)应被高亮显示。

实时比对:

在任一编辑器中进行修改、删除或添加内容。
确认差异高亮会实时更新。

语法高亮:

通过顶部的语言选择器,切换语言为 “Python”。
在编辑器中粘贴一些 Python 代码,确认语法高亮正确。
切换回 “JSON”,语法高亮也应恢复。

主题切换: 点击“深色模式”开关,确认编辑器主题变为深色。

操作按钮:

测试“交换内容”按钮。
测试“清空内容”按钮。

布局与滚动:

粘贴大量代码,确认编辑器内部可以正常滚动。
调整浏览器窗口大小,确认编辑器布局自适应。

总结

在本文中,测试平台集成了一个专业级的代码比对工具:

安装了 monaco-editor 核心库及其 Vue3 集成组件 monaco-editor-vue3
配置了 Vite (vite.config.ts),通过 manualChunks 确保 Monaco Editor 在生产构建时被正确打包,优化了加载性能。
创建了新的“工具管理”模块,并添加了“代码比对”页面的路由和侧边栏菜单入口。
实现了 DiffCheckerView.vue 页面

使用 monaco-editor-vue3 提供的 MonacoDiffEditor 组件作为核心。
实现了左右两侧内容的双向数据绑定。
提供了动态切换语言(语法高亮)和主题(浅色/深色模式)的功能。
添加了“交换内容”和“清空内容”等便捷操作。
通过 CSS Flexbox 和 :deep 选择器,实现了编辑器在 ElCard 中自适应高度的良好布局。

指导了如何全面测试和验证该工具的各项功能。

© 版权声明
THE END
如果内容对您有所帮助,就支持一下吧!
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容