在这里插入图片描述
在这里插入图片描述

项目概述

HTML 解析和提取是现代应用开发中的重要需求。无论是在网页爬虫、内容提取、数据采集还是网页自动化中,都需要进行各种 HTML 解析和数据提取操作。然而,不同的编程语言和平台对 HTML 处理的实现方式各不相同,这导致开发者需要在不同平台上重复编写类似的逻辑。

本文介绍一个基于 Kotlin Multiplatform (KMP) 和 OpenHarmony 平台的 HTML 解析和提取工具库。这个工具库提供了一套完整的 HTML 处理能力,包括标签提取、属性获取、文本内容提取、链接收集等功能。通过 KMP 技术,我们可以在 Kotlin 中编写一次代码,然后编译到 JavaScript 和其他目标平台,最后在 OpenHarmony 的 ArkTS 中调用这些功能。

技术架构

多平台支持

  • Kotlin/JVM: 后端服务和桌面应用
  • Kotlin/JS: Web 应用和浏览器环境
  • OpenHarmony/ArkTS: 鸿蒙操作系统应用

核心功能模块

  1. 标签提取: 提取 HTML 中的特定标签
  2. 属性获取: 获取标签的属性值
  3. 文本提取: 提取标签中的文本内容
  4. 链接收集: 收集页面中的所有链接
  5. 表格解析: 解析 HTML 表格数据
  6. 列表提取: 提取列表项
  7. 标签清理: 移除 HTML 标签
  8. 内容分析: 分析 HTML 结构

Kotlin 实现

核心 HTML 解析类

// 文件: src/commonMain/kotlin/HtmlParser.kt

/**
 * HTML 解析和提取工具类
 * 提供 HTML 解析、提取等功能
 */
class HtmlParser {
    
    /**
     * 提取所有指定标签
     * @param html HTML 字符串
     * @param tagName 标签名
     * @return 标签列表
     */
    fun extractTags(html: String, tagName: String): List<String> {
        val pattern = "<$tagName[^>]*>.*?</$tagName>".toRegex(RegexOption.DOT_MATCHES_ALL)
        return pattern.findAll(html).map { it.value }.toList()
    }
    
    /**
     * 提取标签的属性值
     * @param tag 标签字符串
     * @param attributeName 属性名
     * @return 属性值
     */
    fun getAttribute(tag: String, attributeName: String): String {
        val pattern = """$attributeName\s*=\s*["']([^"']*)["']""".toRegex()
        val match = pattern.find(tag)
        return match?.groupValues?.get(1) ?: ""
    }
    
    /**
     * 提取标签中的文本内容
     * @param tag 标签字符串
     * @return 文本内容
     */
    fun getTagContent(tag: String): String {
        val pattern = ">([^<]*)<".toRegex()
        val match = pattern.find(tag)
        return match?.groupValues?.get(1) ?: ""
    }
    
    /**
     * 提取所有链接
     * @param html HTML 字符串
     * @return 链接列表
     */
    fun extractLinks(html: String): List<String> {
        val pattern = """href\s*=\s*["']([^"']*)["']""".toRegex()
        return pattern.findAll(html).map { it.groupValues[1] }.toList()
    }
    
    /**
     * 提取所有图片 URL
     * @param html HTML 字符串
     * @return 图片 URL 列表
     */
    fun extractImages(html: String): List<String> {
        val pattern = """src\s*=\s*["']([^"']*)["']""".toRegex()
        return pattern.findAll(html).map { it.groupValues[1] }.toList()
    }
    
    /**
     * 提取所有标题
     * @param html HTML 字符串
     * @return 标题列表
     */
    fun extractHeadings(html: String): List<String> {
        val headings = mutableListOf<String>()
        for (level in 1..6) {
            val pattern = "<h$level[^>]*>([^<]*)</h$level>".toRegex()
            headings.addAll(pattern.findAll(html).map { it.groupValues[1] })
        }
        return headings
    }
    
    /**
     * 提取所有段落
     * @param html HTML 字符串
     * @return 段落列表
     */
    fun extractParagraphs(html: String): List<String> {
        val pattern = "<p[^>]*>([^<]*)</p>".toRegex()
        return pattern.findAll(html).map { it.groupValues[1] }.toList()
    }
    
    /**
     * 提取列表项
     * @param html HTML 字符串
     * @return 列表项列表
     */
    fun extractListItems(html: String): List<String> {
        val pattern = "<li[^>]*>([^<]*)</li>".toRegex()
        return pattern.findAll(html).map { it.groupValues[1] }.toList()
    }
    
    /**
     * 移除所有 HTML 标签
     * @param html HTML 字符串
     * @return 纯文本
     */
    fun stripHtmlTags(html: String): String {
        return html.replace(Regex("<[^>]*>"), "")
    }
    
    /**
     * 解码 HTML 实体
     * @param html HTML 字符串
     * @return 解码后的字符串
     */
    fun decodeHtmlEntities(html: String): String {
        return html
            .replace("&lt;", "<")
            .replace("&gt;", ">")
            .replace("&amp;", "&")
            .replace("&quot;", "\"")
            .replace("&#39;", "'")
            .replace("&nbsp;", " ")
    }
    
    /**
     * 编码 HTML 实体
     * @param text 文本
     * @return 编码后的字符串
     */
    fun encodeHtmlEntities(text: String): String {
        return text
            .replace("&", "&amp;")
            .replace("<", "&lt;")
            .replace(">", "&gt;")
            .replace("\"", "&quot;")
            .replace("'", "&#39;")
    }
    
    /**
     * 提取表格数据
     * @param html HTML 字符串
     * @return 表格行列表
     */
    fun extractTableData(html: String): List<List<String>> {
        val rows = mutableListOf<List<String>>()
        val rowPattern = "<tr[^>]*>(.*?)</tr>".toRegex(RegexOption.DOT_MATCHES_ALL)
        val cellPattern = "<t[dh][^>]*>([^<]*)</t[dh]>".toRegex()
        
        rowPattern.findAll(html).forEach { rowMatch ->
            val cells = mutableListOf<String>()
            cellPattern.findAll(rowMatch.groupValues[1]).forEach { cellMatch ->
                cells.add(cellMatch.groupValues[1])
            }
            if (cells.isNotEmpty()) {
                rows.add(cells)
            }
        }
        
        return rows
    }
    
    /**
     * 获取 HTML 统计信息
     * @param html HTML 字符串
     * @return 统计信息映射
     */
    fun getHtmlStatistics(html: String): Map<String, Any> {
        return mapOf(
            "totalLength" to html.length,
            "tagCount" to Regex("<[^>]+>").findAll(html).count(),
            "linkCount" to extractLinks(html).size,
            "imageCount" to extractImages(html).size,
            "headingCount" to extractHeadings(html).size,
            "paragraphCount" to extractParagraphs(html).size,
            "listItemCount" to extractListItems(html).size,
            "textLength" to stripHtmlTags(html).length
        )
    }
    
    /**
     * 验证 HTML 是否有效
     * @param html HTML 字符串
     * @return 是否有效
     */
    fun isValidHtml(html: String): Boolean {
        val openTags = Regex("<([a-z][a-z0-9]*)").findAll(html).map { it.groupValues[1] }.toList()
        val closeTags = Regex("</([a-z][a-z0-9]*)>").findAll(html).map { it.groupValues[1] }.toList()
        return openTags.size >= closeTags.size
    }
}

Kotlin 实现的核心特点

Kotlin 实现中的 HTML 解析功能充分利用了 Kotlin 标准库的正则表达式能力。标签提取使用了正则表达式来匹配 HTML 标签。属性获取使用了命名分组来提取属性值。

文本提取使用了正则表达式来获取标签之间的内容。链接和图片提取使用了特定的属性模式。标题和段落提取使用了循环和正则表达式的组合。

HTML 实体编码和解码使用了字符串替换。表格解析使用了嵌套的正则表达式来提取行和单元格。统计功能使用了正则表达式计数。

JavaScript 实现

编译后的 JavaScript 代码

// 文件: build/js/packages/kmp_openharmony-js/kotlin/kmp_openharmony.js
// (由 Kotlin 编译器自动生成)

/**
 * HtmlParser 类的 JavaScript 版本
 * 通过 Kotlin/JS 编译器从 Kotlin 源代码生成
 */
class HtmlParser {
  /**
   * 提取所有指定标签
   * @param {string} html - HTML 字符串
   * @param {string} tagName - 标签名
   * @returns {string[]} 标签列表
   */
  extractTags(html, tagName) {
    const pattern = new RegExp(`<${tagName}[^>]*>.*?</${tagName}>`, 'gs');
    const matches = html.match(pattern);
    return matches || [];
  }

  /**
   * 提取标签的属性值
   * @param {string} tag - 标签字符串
   * @param {string} attributeName - 属性名
   * @returns {string} 属性值
   */
  getAttribute(tag, attributeName) {
    const pattern = new RegExp(`${attributeName}\\s*=\\s*["']([^"']*)["']`);
    const match = tag.match(pattern);
    return match ? match[1] : '';
  }

  /**
   * 提取标签中的文本内容
   * @param {string} tag - 标签字符串
   * @returns {string} 文本内容
   */
  getTagContent(tag) {
    const pattern = />([^<]*)</;
    const match = tag.match(pattern);
    return match ? match[1] : '';
  }

  /**
   * 提取所有链接
   * @param {string} html - HTML 字符串
   * @returns {string[]} 链接列表
   */
  extractLinks(html) {
    const pattern = /href\s*=\s*["']([^"']*)["']/g;
    const links = [];
    let match;
    while ((match = pattern.exec(html)) !== null) {
      links.push(match[1]);
    }
    return links;
  }

  /**
   * 提取所有图片 URL
   * @param {string} html - HTML 字符串
   * @returns {string[]} 图片 URL 列表
   */
  extractImages(html) {
    const pattern = /src\s*=\s*["']([^"']*)["']/g;
    const images = [];
    let match;
    while ((match = pattern.exec(html)) !== null) {
      images.push(match[1]);
    }
    return images;
  }

  /**
   * 提取所有标题
   * @param {string} html - HTML 字符串
   * @returns {string[]} 标题列表
   */
  extractHeadings(html) {
    const headings = [];
    for (let level = 1; level <= 6; level++) {
      const pattern = new RegExp(`<h${level}[^>]*>([^<]*)</h${level}>`, 'g');
      let match;
      while ((match = pattern.exec(html)) !== null) {
        headings.push(match[1]);
      }
    }
    return headings;
  }

  /**
   * 提取所有段落
   * @param {string} html - HTML 字符串
   * @returns {string[]} 段落列表
   */
  extractParagraphs(html) {
    const pattern = /<p[^>]*>([^<]*)<\/p>/g;
    const paragraphs = [];
    let match;
    while ((match = pattern.exec(html)) !== null) {
      paragraphs.push(match[1]);
    }
    return paragraphs;
  }

  /**
   * 提取列表项
   * @param {string} html - HTML 字符串
   * @returns {string[]} 列表项列表
   */
  extractListItems(html) {
    const pattern = /<li[^>]*>([^<]*)<\/li>/g;
    const items = [];
    let match;
    while ((match = pattern.exec(html)) !== null) {
      items.push(match[1]);
    }
    return items;
  }

  /**
   * 移除所有 HTML 标签
   * @param {string} html - HTML 字符串
   * @returns {string} 纯文本
   */
  stripHtmlTags(html) {
    return html.replace(/<[^>]*>/g, '');
  }

  /**
   * 解码 HTML 实体
   * @param {string} html - HTML 字符串
   * @returns {string} 解码后的字符串
   */
  decodeHtmlEntities(html) {
    return html
      .replace(/&lt;/g, '<')
      .replace(/&gt;/g, '>')
      .replace(/&amp;/g, '&')
      .replace(/&quot;/g, '"')
      .replace(/&#39;/g, "'")
      .replace(/&nbsp;/g, ' ');
  }

  /**
   * 编码 HTML 实体
   * @param {string} text - 文本
   * @returns {string} 编码后的字符串
   */
  encodeHtmlEntities(text) {
    return text
      .replace(/&/g, '&amp;')
      .replace(/</g, '&lt;')
      .replace(/>/g, '&gt;')
      .replace(/"/g, '&quot;')
      .replace(/'/g, '&#39;');
  }

  /**
   * 提取表格数据
   * @param {string} html - HTML 字符串
   * @returns {string[][]} 表格行列表
   */
  extractTableData(html) {
    const rows = [];
    const rowPattern = /<tr[^>]*>(.*?)<\/tr>/gs;
    const cellPattern = /<t[dh][^>]*>([^<]*)<\/t[dh]>/g;
    
    let rowMatch;
    while ((rowMatch = rowPattern.exec(html)) !== null) {
      const cells = [];
      let cellMatch;
      while ((cellMatch = cellPattern.exec(rowMatch[1])) !== null) {
        cells.push(cellMatch[1]);
      }
      if (cells.length > 0) {
        rows.push(cells);
      }
    }
    
    return rows;
  }

  /**
   * 获取 HTML 统计信息
   * @param {string} html - HTML 字符串
   * @returns {Object} 统计信息
   */
  getHtmlStatistics(html) {
    return {
      totalLength: html.length,
      tagCount: (html.match(/<[^>]+>/g) || []).length,
      linkCount: this.extractLinks(html).length,
      imageCount: this.extractImages(html).length,
      headingCount: this.extractHeadings(html).length,
      paragraphCount: this.extractParagraphs(html).length,
      listItemCount: this.extractListItems(html).length,
      textLength: this.stripHtmlTags(html).length
    };
  }

  /**
   * 验证 HTML 是否有效
   * @param {string} html - HTML 字符串
   * @returns {boolean} 是否有效
   */
  isValidHtml(html) {
    const openTags = (html.match(/<([a-z][a-z0-9]*)/gi) || []).length;
    const closeTags = (html.match(/<\/([a-z][a-z0-9]*)>/gi) || []).length;
    return openTags >= closeTags;
  }
}

JavaScript 实现的特点

JavaScript 版本完全由 Kotlin/JS 编译器自动生成,确保了与 Kotlin 版本的行为完全一致。JavaScript 的正则表达式提供了强大的模式匹配能力。

exec 方法用于循环匹配。match 方法用于一次性获取所有匹配。replace 方法用于字符串替换。

ArkTS 调用代码

OpenHarmony 应用集成

// 文件: kmp_ceshiapp/entry/src/main/ets/pages/HtmlParserPage.ets

import { HtmlParser } from '../../../../../../../build/js/packages/kmp_openharmony-js/kotlin/kmp_openharmony';

@Entry
@Component
struct HtmlParserPage {
  @State selectedOperation: string = 'stripTags';
  @State inputHtml: string = '';
  @State result: string = '';
  @State resultTitle: string = '';

  private parser = new HtmlParser();

  private operations = [
    { name: '移除标签', value: 'stripTags' },
    { name: '提取链接', value: 'links' },
    { name: '提取图片', value: 'images' },
    { name: '提取标题', value: 'headings' },
    { name: '提取段落', value: 'paragraphs' },
    { name: '提取列表', value: 'lists' },
    { name: '统计信息', value: 'statistics' },
    { name: '验证 HTML', value: 'validate' },
    { name: '解码实体', value: 'decode' },
    { name: '提取表格', value: 'table' }
  ];

  build() {
    Column() {
      // 标题
      Text('🔍 HTML解析和提取工具库')
        .fontSize(28)
        .fontWeight(FontWeight.Bold)
        .fontColor('#FFFFFF')
        .width('100%')
        .padding(20)
        .backgroundColor('#1A237E')
        .textAlign(TextAlign.Center)

      Scroll() {
        Column() {
          // 操作选择
          Column() {
            Text('选择操作')
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor('#333333')
              .margin({ bottom: 12 })

            Flex({ wrap: FlexWrap.Wrap }) {
              ForEach(this.operations, (op: { name: string; value: string }) => {
                Button(op.name)
                  .layoutWeight(1)
                  .height(40)
                  .margin({ right: 8, bottom: 8 })
                  .backgroundColor(this.selectedOperation === op.value ? '#1A237E' : '#E0E0E0')
                  .fontColor(this.selectedOperation === op.value ? '#FFFFFF' : '#333333')
                  .fontSize(11)
                  .onClick(() => {
                    this.selectedOperation = op.value;
                    this.result = '';
                    this.resultTitle = '';
                  })
              })
            }
            .width('100%')
          }
          .width('95%')
          .margin({ top: 16, left: '2.5%', right: '2.5%', bottom: 16 })
          .padding(12)
          .backgroundColor('#FFFFFF')
          .borderRadius(6)

          // 输入区域
          Column() {
            Text('输入 HTML')
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .fontColor('#333333')
              .margin({ bottom: 8 })

            TextInput({ placeholder: '输入 HTML 代码', text: this.inputHtml })
              .onChange((value) => this.inputHtml = value)
              .width('100%')
              .height(120)
              .padding(12)
              .border({ width: 1, color: '#4DB6AC' })
              .borderRadius(6)
              .fontSize(12)
          }
          .width('95%')
          .margin({ left: '2.5%', right: '2.5%', bottom: 16 })
          .padding(12)
          .backgroundColor('#FFFFFF')
          .borderRadius(6)

          // 操作按钮
          Row() {
            Button('✨ 解析')
              .layoutWeight(1)
              .height(44)
              .backgroundColor('#1A237E')
              .fontColor('#FFFFFF')
              .fontSize(14)
              .fontWeight(FontWeight.Bold)
              .borderRadius(6)
              .onClick(() => this.executeOperation())

            Blank()
              .width(12)

            Button('🔄 清空')
              .layoutWeight(1)
              .height(44)
              .backgroundColor('#F5F5F5')
              .fontColor('#1A237E')
              .fontSize(14)
              .border({ width: 1, color: '#4DB6AC' })
              .borderRadius(6)
              .onClick(() => {
                this.inputHtml = '';
                this.result = '';
                this.resultTitle = '';
              })
          }
          .width('95%')
          .margin({ left: '2.5%', right: '2.5%', bottom: 16 })

          // 结果显示
          if (this.resultTitle) {
            Column() {
              Text(this.resultTitle)
                .fontSize(16)
                .fontWeight(FontWeight.Bold)
                .fontColor('#FFFFFF')
                .width('100%')
                .padding(12)
                .backgroundColor('#1A237E')
                .borderRadius(6)
                .textAlign(TextAlign.Center)
                .margin({ bottom: 12 })

              Scroll() {
                Text(this.result)
                  .fontSize(12)
                  .fontColor('#333333')
                  .fontFamily('monospace')
                  .textAlign(TextAlign.Start)
                  .width('100%')
                  .padding(12)
                  .selectable(true)
              }
              .width('100%')
              .height(300)
              .backgroundColor('#F9F9F9')
              .border({ width: 1, color: '#4DB6AC' })
              .borderRadius(6)
            }
            .width('95%')
            .margin({ left: '2.5%', right: '2.5%', bottom: 16 })
            .padding(12)
            .backgroundColor('#FFFFFF')
            .borderRadius(6)
          }
        }
        .width('100%')
      }
      .layoutWeight(1)
      .width('100%')
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }

  private executeOperation() {
    if (!this.inputHtml.trim()) {
      this.resultTitle = '❌ 错误';
      this.result = '请输入 HTML 代码';
      return;
    }

    try {
      switch (this.selectedOperation) {
        case 'stripTags':
          const plainText = this.parser.stripHtmlTags(this.inputHtml);
          this.resultTitle = '📝 移除标签结果';
          this.result = plainText;
          break;

        case 'links':
          const links = this.parser.extractLinks(this.inputHtml);
          this.resultTitle = '🔗 提取链接';
          this.result = links.length > 0 ? links.join('\n') : '没有找到链接';
          break;

        case 'images':
          const images = this.parser.extractImages(this.inputHtml);
          this.resultTitle = '🖼️ 提取图片';
          this.result = images.length > 0 ? images.join('\n') : '没有找到图片';
          break;

        case 'headings':
          const headings = this.parser.extractHeadings(this.inputHtml);
          this.resultTitle = '📋 提取标题';
          this.result = headings.length > 0 ? headings.join('\n') : '没有找到标题';
          break;

        case 'paragraphs':
          const paragraphs = this.parser.extractParagraphs(this.inputHtml);
          this.resultTitle = '📄 提取段落';
          this.result = paragraphs.length > 0 ? paragraphs.join('\n') : '没有找到段落';
          break;

        case 'lists':
          const items = this.parser.extractListItems(this.inputHtml);
          this.resultTitle = '📑 提取列表';
          this.result = items.length > 0 ? items.join('\n') : '没有找到列表项';
          break;

        case 'statistics':
          const stats = this.parser.getHtmlStatistics(this.inputHtml);
          this.resultTitle = '📊 HTML 统计信息';
          this.result = `总长度: ${stats.totalLength}\n标签数: ${stats.tagCount}\n链接数: ${stats.linkCount}\n图片数: ${stats.imageCount}\n标题数: ${stats.headingCount}\n段落数: ${stats.paragraphCount}\n列表项数: ${stats.listItemCount}\n文本长度: ${stats.textLength}`;
          break;

        case 'validate':
          const isValid = this.parser.isValidHtml(this.inputHtml);
          this.resultTitle = isValid ? '✅ HTML 有效' : '❌ HTML 无效';
          this.result = `HTML 结构: ${isValid ? '有效' : '无效'}`;
          break;

        case 'decode':
          const decoded = this.parser.decodeHtmlEntities(this.inputHtml);
          this.resultTitle = '🔓 解码实体';
          this.result = decoded;
          break;

        case 'table':
          const tableData = this.parser.extractTableData(this.inputHtml);
          this.resultTitle = '📋 提取表格';
          if (tableData.length > 0) {
            this.result = tableData.map((row, idx) => `${idx + 1}: ${row.join(' | ')}`).join('\n');
          } else {
            this.result = '没有找到表格数据';
          }
          break;
      }
    } catch (e) {
      this.resultTitle = '❌ 解析出错';
      this.result = `错误: ${e}`;
    }
  }
}

ArkTS 集成的关键要点

在 OpenHarmony 应用中集成 HTML 解析工具库需要考虑多种操作类型和用户体验。我们设计了一个灵活的 UI,能够支持不同的 HTML 解析操作。

操作选择界面使用了 Flex 布局和 FlexWrap 来实现响应式的按钮排列。当用户选择不同的操作时,结果会相应更新。

输入区域使用了较大的高度以容纳多行 HTML 代码。结果显示使用了可选择的文本,这样用户可以轻松复制解析结果。

对于列表类型的结果(如链接、图片等),我们使用换行符分隔显示。对于表格数据,我们使用管道符分隔列。

工作流程详解

HTML 解析的完整流程

  1. 操作选择: 用户在 ArkTS UI 中选择要执行的 HTML 解析操作
  2. HTML 输入: 用户输入要解析的 HTML 代码
  3. 解析执行: 调用 HtmlParser 的相应方法
  4. 结果展示: 将解析结果显示在 UI 中

跨平台一致性

通过 KMP 技术,我们确保了在所有平台上的行为一致性。无论是在 Kotlin/JVM、Kotlin/JS 还是通过 ArkTS 调用,HTML 解析的逻辑和结果都是完全相同的。

实际应用场景

网页爬虫

在网页爬虫中,需要解析 HTML 并提取数据。这个工具库提供了必要的 HTML 解析功能。

内容提取

在内容管理系统中,需要从 HTML 中提取有用的内容。这个工具库提供了提取功能。

数据采集

在数据采集应用中,需要从网页中收集特定的数据。这个工具库提供了数据提取能力。

网页分析

在网页分析工具中,需要分析 HTML 结构。这个工具库提供了分析功能。

性能优化

缓存解析结果

在频繁解析相同 HTML 时,可以缓存解析结果以避免重复计算。

流式处理

在处理大型 HTML 文件时,应该考虑使用流式处理的方式以提高效率。

安全性考虑

XSS 防护

在处理用户输入的 HTML 时,应该进行清洗以防止 XSS 攻击。

输入验证

在解析 HTML 时,应该进行验证以确保 HTML 的有效性。

总结

这个 KMP OpenHarmony HTML 解析和提取工具库展示了如何使用现代的跨平台技术来处理常见的 HTML 处理任务。通过 Kotlin Multiplatform 技术,我们可以在一个地方编写业务逻辑,然后在多个平台上使用。

HTML 解析是网页处理中的基础功能。通过使用这样的工具库,开发者可以快速、可靠地处理各种 HTML 解析操作,从而提高开发效率和代码质量。

在实际应用中,建议根据具体的需求进行定制和扩展,例如添加更复杂的 HTML 解析功能、实现 CSS 选择器支持等高级特性。同时,定期进行安全审计和性能测试,确保应用的 HTML 处理功能始终处于最佳状态。欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

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

更多推荐