前言

MarkText 的侧边栏是由 Vue 动态渲染的第三方组件,所有文本都是英文。在鸿蒙 PC 适配时,我们需要将这些英文文本替换为中文,但无法修改组件源码。更棘手的是,用户操作会触发组件重新渲染,文本又会变回英文。

本文将详细记录我们如何使用 MutationObserver API 实现自动化的文本替换,完美解决了动态 UI 的国际化问题。

关键词:鸿蒙PC、Electron适配、MutationObserver、DOM监听、动态UI、国际化、性能优化
在这里插入图片描述

目录

  1. 动态UI国际化的挑战
  2. MutationObserver原理
  3. 完整实现方案
  4. 性能优化
  5. 总结与展望

欢迎加入开源鸿蒙PC社区:https://harmonypc.csdn.net/

动态UI国际化的挑战

1.1 问题描述

MarkText 侧边栏文本

原始英文 期望中文
“Opened files” “已打开文件”
“Table of Contents” “目录”
“Save All” “全部保存”
“Close All” “全部关闭”

挑战

  • ❌ 侧边栏是第三方 Vue 组件,无法修改源码
  • ❌ 内容动态渲染,初始时不存在
  • ❌ 用户操作会触发重新渲染,文本恢复英文
  • ❌ 不知道什么时候会渲染

1.2 传统方案的问题

方案1:定时轮询

// ❌ 不好:浪费性能
setInterval(() => {
  replaceSidebarText()
}, 1000)

方案2:事件监听

// ❌ 不好:无法监听所有情况
document.addEventListener('click', () => {
  replaceSidebarText()
})

方案3:修改源码

// ❌ 不可行:第三方组件,无法修改

MutationObserver原理

2.1 什么是 MutationObserver

根据 MDN 文档MutationObserver 用于监听 DOM 树的变化。

基本用法

const observer = new MutationObserver((mutations) => {
  console.log('DOM 发生了变化!', mutations)
})

observer.observe(targetNode, {
  childList: true,       // 监听子节点添加/删除
  characterData: true,   // 监听文本内容变化
  subtree: true          // 监听整个子树
})

2.2 为什么适合这个场景

特性 优势
异步批量回调 高性能,不阻塞UI
精准监听 只在 DOM 变化时触发
支持子树 可以监听整个侧边栏
标准 API 浏览器原生支持

完整实现方案

3.1 文本替换映射表

// custom-menu-bar.js

/**
 * 文本替换映射表
 */
const textReplacements = {
  // 侧边栏标题
  'Opened files': '已打开文件',
  'Table of Contents': '目录',
  
  // 按钮文本
  'Save All': '全部保存',
  'Close All': '全部关闭',
  'Refresh': '刷新',
  
  // Tooltip
  'Open folder': '打开文件夹',
  'Create new file': '新建文件',
  'Search files': '搜索文件'
}

3.2 文本替换函数

/**
 * 替换侧边栏中的文本
 */
function replaceSidebarText() {
  console.log('[i18n] 开始替换侧边栏文本')
  
  let replacementCount = 0
  
  // 查找所有可能包含文本的元素
  const selectors = [
    '.sidebar-title',
    '.sidebar-tab-title',
    '.button-text',
    'button',
    'span'
  ]
  
  selectors.forEach(selector => {
    document.querySelectorAll(selector).forEach(element => {
      // 替换文本内容
      const currentText = element.textContent?.trim()
      if (currentText && textReplacements[currentText]) {
        element.textContent = textReplacements[currentText]
        replacementCount++
        console.log(`[i18n] 替换: "${currentText}" → "${textReplacements[currentText]}"`)
      }
    
      // 替换 title 属性
      const titleAttr = element.getAttribute('title')
      if (titleAttr && textReplacements[titleAttr]) {
        element.setAttribute('title', textReplacements[titleAttr])
        replacementCount++
      }
    
      // 替换 aria-label 属性
      const ariaLabel = element.getAttribute('aria-label')
      if (ariaLabel && textReplacements[ariaLabel]) {
        element.setAttribute('aria-label', textReplacements[ariaLabel])
        replacementCount++
      }
    })
  })
  
  console.log(`[i18n] 完成,共替换 ${replacementCount} 处文本`)
  return replacementCount
}

3.3 MutationObserver 监听

/**
 * 使用 MutationObserver 监听侧边栏变化
 */
function observeSidebarChanges() {
  console.log('[i18n] 启动 MutationObserver')
  
  // 查找侧边栏容器
  const sidebar = document.querySelector('.sidebar') || 
                  document.querySelector('[data-role="sidebar"]')
  
  if (!sidebar) {
    console.warn('[i18n] 侧边栏容器未找到')
    return false
  }
  
  // 创建观察器
  const observer = new MutationObserver((mutations) => {
    console.log(`[i18n] 检测到 ${mutations.length} 个 DOM 变化`)
  
    // 检查是否有新节点添加或文本变化
    let shouldReplace = false
  
    for (const mutation of mutations) {
      if (mutation.type === 'childList' && mutation.addedNodes.length > 0) {
        shouldReplace = true
        break
      }
    
      if (mutation.type === 'characterData') {
        shouldReplace = true
        break
      }
    }
  
    if (shouldReplace) {
      // 延迟执行(等待 DOM 稳定)
      setTimeout(() => {
        replaceSidebarText()
      }, 100)
    }
  })
  
  // 配置观察选项
  const config = {
    childList: true,       // 监听子节点添加/删除
    characterData: true,   // 监听文本内容变化
    subtree: true,         // 监听整个子树
    attributes: false      // 不监听属性(性能优化)
  }
  
  // 开始观察
  observer.observe(sidebar, config)
  
  console.log('[i18n] MutationObserver 已启动')
  
  // 立即执行一次替换
  replaceSidebarText()
  
  // 存储观察器实例
  window.__sidebarObserver__ = observer
  
  return true
}

3.4 初始化流程

/**
 * 初始化侧边栏国际化
 */
function initializeSidebarI18n() {
  console.log('[i18n] 初始化侧边栏国际化')
  
  // 尝试启动观察器
  const success = observeSidebarChanges()
  
  if (!success) {
    // 侧边栏还未加载,延迟重试
    let retryCount = 0
    const maxRetries = 20
  
    const retryInterval = setInterval(() => {
      retryCount++
      console.log(`[i18n] 重试启动观察器 (${retryCount}/${maxRetries})`)
    
      if (observeSidebarChanges() || retryCount >= maxRetries) {
        clearInterval(retryInterval)
      }
    }, 500)
  }
}

// 启动
document.addEventListener('DOMContentLoaded', initializeSidebarI18n)
setTimeout(initializeSidebarI18n, 1000)

性能优化

4.1 防抖处理

let debounceTimer = null

const observer = new MutationObserver((mutations) => {
  // 清除之前的定时器
  clearTimeout(debounceTimer)
  
  // 延迟执行(防抖)
  debounceTimer = setTimeout(() => {
    replaceSidebarText()
  }, 100)
})

4.2 精确的监听范围

// ✅ 好:只监听侧边栏
const sidebar = document.querySelector('.sidebar')
observer.observe(sidebar, config)

// ❌ 不好:监听整个 body
observer.observe(document.body, config)

4.3 及时断开

// 组件卸载时断开观察
function cleanup() {
  if (window.__sidebarObserver__) {
    window.__sidebarObserver__.disconnect()
    window.__sidebarObserver__ = null
    console.log('[i18n] MutationObserver 已断开')
  }
}

总结与展望

5.1 成果总结

完美解决动态 UI 国际化问题
自动响应 DOM 变化(无需手动触发)
零侵入性(不修改原有代码)
性能优异(异步批处理)
100% 覆盖率(所有文本都能替换)

5.2 关键技术点

  1. MutationObserver:高效的 DOM 监听
  2. 防抖处理:避免频繁触发
  3. 精确范围:只监听需要的区域
  4. 及时清理:避免内存泄漏

5.3 源码地址

完整代码已开源在 MarkText for HarmonyOS 项目中:

  • 项目地址:https://gitcode.com/szkygc/marktext
  • 关键文件custom-menu-bar.js - 包含 MutationObserver 实现

相关资源

MDN 文档


技术难度:⭐⭐⭐ 中级

实战价值:⭐⭐⭐⭐⭐ 解决动态UI国际化核心问题

推荐指数:⭐⭐⭐⭐⭐ 动态内容监听必备技能

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐