UniApp自定义代码编辑器:从0到1打造专业开发工具

在移动应用开发领域,代码编辑器一直是开发者们不可或缺的工具。随着鸿蒙系统(HarmonyOS)的崛起,为其量身打造一款流畅、高效的代码编辑器变得尤为重要。本文将深入探讨如何使用UniApp框架开发一个专业级的代码编辑器,并重点关注鸿蒙系统的适配与优化。

技术选型与架构设计

在开始开发之前,我们需要慎重考虑技术栈的选择。经过实践,我们选择了以下核心技术:

  1. UniApp框架:提供跨平台能力
  2. Monaco Editor:微软开源的代码编辑器核心
  3. Prism.js:轻量级的代码语法高亮库
  4. Web Worker:用于代码分析和格式化
  5. IndexedDB:本地文件存储

项目依赖配置

首先,让我们配置必要的项目依赖:

// package.json
{
  "dependencies": {
    "monaco-editor": "^0.45.0",
    "prismjs": "^1.29.0",
    "@types/prismjs": "^1.26.3",
    "prettier": "^3.1.0",
    "localforage": "^1.10.0"
  }
}

编辑器核心实现

1. 编辑器状态管理

首先,我们需要创建一个编辑器状态管理类来处理编辑器的核心状态:

// utils/EditorState.ts
export class EditorState {
  private static instance: EditorState;
  private currentFile: string | null = null;
  private isModified: boolean = false;
  private history: string[] = [];
  private historyIndex: number = -1;

  private constructor() {
    // 单例模式
  }

  static getInstance(): EditorState {
    if (!EditorState.instance) {
      EditorState.instance = new EditorState();
    }
    return EditorState.instance;
  }

  setCurrentFile(filename: string): void {
    this.currentFile = filename;
    this.isModified = false;
    this.history = [];
    this.historyIndex = -1;
  }

  markAsModified(): void {
    this.isModified = true;
  }

  addToHistory(content: string): void {
    this.history = this.history.slice(0, this.historyIndex + 1);
    this.history.push(content);
    this.historyIndex++;
    
    // 限制历史记录大小
    if (this.history.length > 50) {
      this.history.shift();
      this.historyIndex--;
    }
  }

  undo(): string | null {
    if (this.historyIndex > 0) {
      this.historyIndex--;
      return this.history[this.historyIndex];
    }
    return null;
  }

  redo(): string | null {
    if (this.historyIndex < this.history.length - 1) {
      this.historyIndex++;
      return this.history[this.historyIndex];
    }
    return null;
  }
}

2. 编辑器组件实现

创建主编辑器组件,实现核心编辑功能:

<!-- components/CodeEditor.vue -->
<template>
  <view class="code-editor-container">
    <view class="editor-toolbar">
      <view class="toolbar-item" @tap="handleUndo">
        <text class="iconfont icon-undo"></text>
      </view>
      <view class="toolbar-item" @tap="handleRedo">
        <text class="iconfont icon-redo"></text>
      </view>
      <view class="toolbar-item" @tap="handleFormat">
        <text class="iconfont icon-format"></text>
      </view>
    </view>
    
    <view 
      class="editor-content"
      :style="{ height: editorHeight + 'px' }"
      @touchstart="handleTouchStart"
      @touchmove="handleTouchMove"
      @touchend="handleTouchEnd"
    >
      <view class="line-numbers">
        <text 
          v-for="line in lineNumbers" 
          :key="line"
          class="line-number"
        >{{ line }}</text>
      </view>
      
      <textarea
        class="code-area"
        v-model="code"
        :style="textareaStyles"
        @input="handleInput"
        @scroll="handleScroll"
        :show-confirm-bar="false"
        :auto-height="false"
        :cursor-spacing="20"
      ></textarea>
    </view>
  </view>
</template>

<script lang="ts">
import { defineComponent, ref, computed, onMounted, watch } from 'vue';
import { EditorState } from '@/utils/EditorState';
import { formatCode } from '@/utils/CodeFormatter';
import { SyntaxHighlighter } from '@/utils/SyntaxHighlighter';

export default defineComponent({
  name: 'CodeEditor',
  
  props: {
    initialCode: {
      type: String,
      default: ''
    },
    language: {
      type: String,
      default: 'javascript'
    }
  },

  setup(props) {
    const code = ref(props.initialCode);
    const editorHeight = ref(0);
    const scrollTop = ref(0);
    const editorState = EditorState.getInstance();
    const syntaxHighlighter = new SyntaxHighlighter();

    // 计算行号
    const lineNumbers = computed(() => {
      const lines = code.value.split('\n').length;
      return Array.from({ length: lines }, (_, i) => i + 1);
    });

    // 编辑器样式
    const textareaStyles = computed(() => ({
      'font-family': 'Monaco, Consolas, monospace',
      'font-size': '14px',
      'line-height': '1.5',
      'tab-size': '2',
      'padding-left': '4em',
      'white-space': 'pre',
      'overflow-x': 'auto'
    }));

    // 处理输入
    const handleInput = (event: any) => {
      const newContent = event.detail.value;
      code.value = newContent;
      editorState.markAsModified();
      editorState.addToHistory(newContent);
      
      // 触发语法高亮
      syntaxHighlighter.highlight(newContent, props.language);
    };

    // 撤销操作
    const handleUndo = () => {
      const previousContent = editorState.undo();
      if (previousContent !== null) {
        code.value = previousContent;
      }
    };

    // 重做操作
    const handleRedo = () => {
      const nextContent = editorState.redo();
      if (nextContent !== null) {
        code.value = nextContent;
      }
    };

    // 格式化代码
    const handleFormat = async () => {
      try {
        const formatted = await formatCode(code.value, props.language);
        code.value = formatted;
        editorState.markAsModified();
        editorState.addToHistory(formatted);
      } catch (error) {
        uni.showToast({
          title: '格式化失败',
          icon: 'none'
        });
      }
    };

    // 处理滚动同步
    const handleScroll = (event: any) => {
      scrollTop.value = event.detail.scrollTop;
    };

    onMounted(() => {
      const sysInfo = uni.getSystemInfoSync();
      editorHeight.value = sysInfo.windowHeight - 44; // 减去工具栏高度
      
      // 初始化编辑器状态
      editorState.addToHistory(props.initialCode);
    });

    return {
      code,
      editorHeight,
      lineNumbers,
      textareaStyles,
      handleInput,
      handleUndo,
      handleRedo,
      handleFormat,
      handleScroll
    };
  }
});
</script>

<style lang="scss">
.code-editor-container {
  display: flex;
  flex-direction: column;
  height: 100%;
  background-color: #1e1e1e;
  color: #d4d4d4;

  .editor-toolbar {
    display: flex;
    height: 44px;
    padding: 0 10px;
    background-color: #2d2d2d;
    border-bottom: 1px solid #3d3d3d;

    .toolbar-item {
      width: 44px;
      height: 44px;
      display: flex;
      align-items: center;
      justify-content: center;
      
      &:active {
        background-color: #3d3d3d;
      }
    }
  }

  .editor-content {
    flex: 1;
    display: flex;
    position: relative;
    overflow: hidden;

    .line-numbers {
      position: absolute;
      left: 0;
      top: 0;
      width: 3em;
      padding: 8px 0;
      background-color: #2d2d2d;
      text-align: right;
      user-select: none;

      .line-number {
        display: block;
        padding: 0 8px;
        color: #858585;
        font-size: 12px;
        line-height: 1.5;
      }
    }

    .code-area {
      flex: 1;
      padding: 8px 0;
      background-color: transparent;
      color: inherit;
      font-size: inherit;
      line-height: inherit;
      border: none;
    }
  }
}
</style>

3. 语法高亮实现

为了提供更好的代码编辑体验,我们需要实现实时语法高亮:

// utils/SyntaxHighlighter.ts
import * as Prism from 'prismjs';

export class SyntaxHighlighter {
  private worker: Worker | null = null;
  private highlightQueue: Array<{
    code: string;
    language: string;
    resolve: (value: string) => void;
  }> = [];
  private isProcessing: boolean = false;

  constructor() {
    this.initializeWorker();
  }

  private initializeWorker(): void {
    // 鸿蒙平台特殊处理
    if (uni.getSystemInfoSync().platform === 'harmony') {
      const workerModule = uni.requireNativePlugin('worker');
      this.worker = workerModule.createWorker('highlighter.js');
    } else {
      this.worker = new Worker('/workers/highlighter.js');
    }

    this.worker.onmessage = (event) => {
      const { highlightedCode } = event.data;
      if (this.highlightQueue.length > 0) {
        const current = this.highlightQueue.shift();
        current?.resolve(highlightedCode);
      }
      this.processNextInQueue();
    };
  }

  private processNextInQueue(): void {
    if (this.highlightQueue.length === 0) {
      this.isProcessing = false;
      return;
    }

    this.isProcessing = true;
    const next = this.highlightQueue[0];
    this.worker?.postMessage({
      code: next.code,
      language: next.language
    });
  }

  highlight(code: string, language: string): Promise<string> {
    return new Promise((resolve) => {
      // 添加到队列
      this.highlightQueue.push({ code, language, resolve });

      // 如果没有正在处理的任务,开始处理
      if (!this.isProcessing) {
        this.processNextInQueue();
      }
    });
  }

  dispose(): void {
    this.worker?.terminate();
    this.worker = null;
    this.highlightQueue = [];
    this.isProcessing = false;
  }
}

4. 代码格式化工具

实现代码格式化功能:

// utils/CodeFormatter.ts
import prettier from 'prettier';
import parserBabel from 'prettier/parser-babel';
import parserTypescript from 'prettier/parser-typescript';
import parserHtml from 'prettier/parser-html';
import parserCss from 'prettier/parser-postcss';

export async function formatCode(code: string, language: string): Promise<string> {
  const options = {
    parser: getParserForLanguage(language),
    plugins: [parserBabel, parserTypescript, parserHtml, parserCss],
    semi: true,
    singleQuote: true,
    tabWidth: 2,
    printWidth: 80,
    trailingComma: 'es5'
  };

  try {
    return await prettier.format(code, options);
  } catch (error) {
    console.error('格式化失败:', error);
    throw error;
  }
}

function getParserForLanguage(language: string): string {
  switch (language.toLowerCase()) {
    case 'javascript':
    case 'js':
      return 'babel';
    case 'typescript':
    case 'ts':
      return 'typescript';
    case 'html':
    case 'vue':
      return 'html';
    case 'css':
    case 'scss':
    case 'less':
      return 'css';
    default:
      return 'babel';
  }
}

鸿蒙系统适配优化

在鸿蒙系统上运行代码编辑器需要特别注意以下几点:

1. 性能优化

// utils/HarmonyOptimizer.ts
export class HarmonyOptimizer {
  static setupPerformanceMonitor(): void {
    if (uni.getSystemInfoSync().platform === 'harmony') {
      const performance = uni.requireNativePlugin('performance');
      
      // 监控内存使用
      performance.startMonitoring({
        type: 'memory',
        callback: (data: any) => {
          if (data.usedMemory > data.totalMemory * 0.8) {
            this.performGC();
          }
        }
      });
    }
  }

  static performGC(): void {
    // 触发垃圾回收
    if (uni.getSystemInfoSync().platform === 'harmony') {
      const system = uni.requireNativePlugin('system');
      system.gc();
    }
  }

  static optimizeRendering(): void {
    if (uni.getSystemInfoSync().platform === 'harmony') {
      // 使用鸿蒙原生渲染引擎
      const render = uni.requireNativePlugin('render');
      render.setRenderMode({
        mode: 'hardware' // 启用硬件加速
      });
    }
  }
}

2. 输入法优化

// utils/InputMethodManager.ts
export class InputMethodManager {
  static adjustInputMethod(): void {
    if (uni.getSystemInfoSync().platform === 'harmony') {
      const input = uni.requireNativePlugin('input');
      
      // 设置输入法模式
      input.setInputMode({
        type: 'text',
        returnKeyType: 'done',
        options: {
          autoCapitalize: false,
          autoCorrect: false
        }
      });
    }
  }

  static handleKeyboardHeight(callback: (height: number) => void): void {
    uni.onKeyboardHeightChange((res) => {
      callback(res.height);
    });
  }
}

实际应用案例

下面是一个完整的代码编辑器页面示例:

<!-- pages/editor/index.vue -->
<template>
  <view class="editor-page">
    <CodeEditor
      :initial-code="initialCode"
      :language="currentLanguage"
      @change="handleCodeChange"
    />
    
    <view class="floating-menu" v-if="showMenu">
      <view class="menu-item" @tap="handleSave">
        <text class="iconfont icon-save"></text>
        <text>保存</text>
      </view>
      <view class="menu-item" @tap="handleShare">
        <text class="iconfont icon-share"></text>
        <text>分享</text>
      </view>
    </view>
  </view>
</template>

<script lang="ts">
import { defineComponent, ref, onMounted, onUnmounted } from 'vue';
import CodeEditor from '@/components/CodeEditor.vue';
import { HarmonyOptimizer } from '@/utils/HarmonyOptimizer';
import { InputMethodManager } from '@/utils/InputMethodManager';

export default defineComponent({
  components: {
    CodeEditor
  },

  setup() {
    const initialCode = ref('');
    const currentLanguage = ref('javascript');
    const showMenu = ref(false);

    onMounted(() => {
      // 初始化优化
      HarmonyOptimizer.setupPerformanceMonitor();
      HarmonyOptimizer.optimizeRendering();
      InputMethodManager.adjustInputMethod();

      // 处理键盘高度变化
      InputMethodManager.handleKeyboardHeight((height) => {
        // 调整编辑器布局
      });
    });

    const handleCodeChange = (newCode: string) => {
      // 处理代码变更
    };

    const handleSave = async () => {
      try {
        // 实现保存逻辑
        await saveCode();
        uni.showToast({
          title: '保存成功',
          icon: 'success'
        });
      } catch (error) {
        uni.showToast({
          title: '保存失败',
          icon: 'none'
        });
      }
    };

    const handleShare = () => {
      // 实现分享逻辑
    };

    return {
      initialCode,
      currentLanguage,
      showMenu,
      handleCodeChange,
      handleSave,
      handleShare
    };
  }
});
</script>

<style lang="scss">
.editor-page {
  height: 100vh;
  position: relative;
}

.floating-menu {
  position: fixed;
  right: 20px;
  bottom: 20px;
  background-color: #2d2d2d;
  border-radius: 8px;
  padding: 8px;
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);

  .menu-item {
    display: flex;
    align-items: center;
    padding: 8px 12px;
    
    text {
      margin-left: 8px;
      color: #d4d4d4;
    }
    
    &:active {
      background-color: #3d3d3d;
    }
  }
}
</style>

总结与展望

通过本文的实践,我们实现了一个功能完整的代码编辑器,并针对鸿蒙系统进行了深度优化。主要特点包括:

  1. 完整的编辑功能:语法高亮、代码格式化、撤销/重做等
  2. 鸿蒙系统特别优化:性能监控、输入法适配、渲染优化
  3. 良好的用户体验:流畅的输入响应、合理的布局设计

未来的优化方向:

  1. 支持更多编程语言和框架
  2. 实现代码自动补全
  3. 集成代码调试功能
  4. 优化大文件处理性能
  5. 增强与鸿蒙系统的集成度

随着鸿蒙生态的不断发展,相信会有更多优秀的开发工具在这个平台上涌现。我们也将持续关注新特性的支持情况,不断优化和改进我们的代码编辑器。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐