前言:鸿蒙与Web技术的完美结合

在当今多端开发的时代,开发者们一直在寻求更高效的开发方式。Electron框架之所以大受欢迎,正是因为它允许开发者使用熟悉的Web技术来构建桌面应用。那么,在鸿蒙生态中,我们能否实现类似的开发体验呢?

答案是肯定的!虽然鸿蒙没有官方的Electron框架,但通过Web组件 + ArkTS原生能力的组合,我们可以实现媲美Electron的开发模式。

一、鸿蒙混合开发架构解析

传统Electron vs 鸿蒙混合开发

Electron架构:

text

┌─────────────────┐    IPC通信    ┌─────────────────┐
│   HTML+CSS+JS   │ ◄───────────► │   Node.js API   │
│  (渲染进程)      │               │   (主进程)       │
└─────────────────┘               └─────────────────┘

鸿蒙混合架构:

text

┌─────────────────┐  JS Bridge   ┌─────────────────┐
│   HTML+CSS+JS   │ ◄───────────► │   ArkTS API     │
│  (Web组件)       │              │   (原生Ability)  │
└─────────────────┘              └─────────────────┘

核心技术组成

  • Web组件:鸿蒙内置的WebView,负责渲染Web内容

  • JavaScript Bridge:连接Web与原生层的通信桥梁

  • ArkTS:鸿蒙原生开发语言,提供系统能力调用

二、环境准备与项目搭建

开发环境要求

  • DevEco Studio 4.0或以上版本

  • HarmonyOS SDK API 9+

  • 配置好的鸿蒙开发环境

项目结构规划

text

HarmonyWebDemo/
├── entry/src/main/ets/
│   ├── entryability/
│   └── pages/
│       └── Index.ets              # 主页面
├── entry/src/main/resources/
│   └── rawfile/
│       ├── index.html            # Web页面
│       ├── css/
│       │   └── style.css         # 样式文件
│       └── js/
│           └── app.js            # 前端逻辑
└── module.json5                  # 模块配置

三、实战:构建文件管理器混合应用

下面我们通过一个完整的文件管理器案例,演示如何实现深度混合开发。

1. 原生层:ArkTS能力封装(Index.ets)

typescript

import webview from '@ohos.web.webview';
import fileio from '@ohos.file.fs';
import common from '@ohos.app.ability.common';
import promptAction from '@ohos.promptAction';

@Entry
@Component
struct FileManager {
  private webController: webview.WebviewController = new webview.WebviewController();
  private context: common.Context = getContext(this) as common.Context;

  build() {
    Column() {
      // 原生顶部导航栏
      Row() {
        Text('文件管理器')
          .fontSize(22)
          .fontWeight(FontWeight.Bold)
          .fontColor('#FFFFFF')
      }
      .width('100%')
      .padding(20)
      .backgroundColor('#007DFF')

      // Web内容区域
      Web({
        src: $rawfile('index.html'),
        controller: this.webController
      })
        .width('100%')
        .height('100%')
        .onControllerAttached(() => {
          this.setupJavaScriptBridge();
        })
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }

  // 建立JavaScript与原生通信桥梁
  private setupJavaScriptBridge() {
    // 注册原生方法供Web调用
    this.webController.registerJavaScriptProxy({
      // 获取文件列表
      getFileList: async (dirPath: string) => {
        try {
          console.info(`正在读取目录: ${dirPath}`);
          let dir = fileio.opendirSync(dirPath);
          let fileList = [];
          let entry = dir.readSync();
          
          while (entry) {
            let fullPath = `${dirPath}/${entry.name}`;
            let stat = fileio.statSync(fullPath);
            
            fileList.push({
              name: entry.name,
              path: fullPath,
              isDirectory: entry.isDirectory,
              size: stat.size,
              mTime: stat.mtime,
              type: this.getFileType(entry.name, entry.isDirectory)
            });
            
            entry = dir.readSync();
          }
          
          dir.closeSync();
          return JSON.stringify({ 
            success: true, 
            data: fileList 
          });
        } catch (error) {
          console.error(`读取目录失败: ${error.message}`);
          return JSON.stringify({ 
            success: false, 
            error: error.message 
          });
        }
      },

      // 读取文件内容
      readFileContent: (filePath: string) => {
        try {
          let file = fileio.openSync(filePath, fileio.OpenMode.READ_ONLY);
          let stat = fileio.statSync(filePath);
          let arrayBuffer = new ArrayBuffer(stat.size);
          let readLen = fileio.readSync(file.fd, arrayBuffer);
          fileio.closeSync(file.fd);
          
          // 转换为文本
          let textDecoder = util.TextDecoder.create('utf-8');
          let content = textDecoder.decodeWithStream(arrayBuffer, { stream: false });
          
          return JSON.stringify({
            success: true,
            data: content.substring(0, 5000) // 限制内容长度
          });
        } catch (error) {
          return JSON.stringify({
            success: false,
            error: error.message
          });
        }
      },

      // 显示原生Toast
      showToast: (message: string) => {
        promptAction.showToast({
          message: message,
          duration: 2000
        });
      },

      // 获取存储信息
      getStorageInfo: () => {
        try {
          // 这里可以添加实际的存储统计逻辑
          return JSON.stringify({
            success: true,
            data: {
              total: '128 GB',
              used: '64.2 GB',
              available: '63.8 GB'
            }
          });
        } catch (error) {
          return JSON.stringify({
            success: false,
            error: error.message
          });
        }
      }
    }, 'harmonyNative', ['getFileList', 'readFileContent', 'showToast', 'getStorageInfo']);

    // 刷新Web组件使注入生效
    this.webController.refresh();
  }

  // 获取文件类型
  private getFileType(filename: string, isDirectory: boolean): string {
    if (isDirectory) return 'folder';
    
    const ext = filename.split('.').pop()?.toLowerCase() || '';
    const typeMap: { [key: string]: string } = {
      'txt': 'text',
      'pdf': 'pdf',
      'jpg': 'image',
      'png': 'image',
      'mp4': 'video',
      'mp3': 'audio'
    };
    
    return typeMap[ext] || 'file';
  }
}

2. 配置权限(module.json5)

json

{
  "module": {
    "requestPermissions": [
      {
        "name": "ohos.permission.READ_MEDIA",
        "reason": "需要读取文件信息"
      },
      {
        "name": "ohos.permission.WRITE_MEDIA", 
        "reason": "需要写入文件信息"
      }
    ]
  }
}

3. Web前端界面(index.html)

html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>鸿蒙文件管理器</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
            background: #f5f5f5;
            color: #333;
            line-height: 1.6;
        }

        .container {
            max-width: 100%;
            padding: 20px;
        }

        .header {
            text-align: center;
            margin-bottom: 30px;
        }

        .header h1 {
            color: #007DFF;
            margin-bottom: 8px;
            font-size: 28px;
        }

        .header .subtitle {
            color: #666;
            font-size: 14px;
        }

        .stats-card {
            background: white;
            border-radius: 12px;
            padding: 20px;
            margin-bottom: 20px;
            box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
        }

        .stats-grid {
            display: grid;
            grid-template-columns: repeat(3, 1fr);
            gap: 15px;
            margin-top: 15px;
        }

        .stat-item {
            text-align: center;
            padding: 15px;
            background: #f8f9fa;
            border-radius: 8px;
        }

        .stat-value {
            font-size: 18px;
            font-weight: bold;
            color: #007DFF;
            margin-bottom: 4px;
        }

        .stat-label {
            font-size: 12px;
            color: #666;
        }

        .file-list {
            background: white;
            border-radius: 12px;
            overflow: hidden;
            box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
        }

        .file-item {
            display: flex;
            align-items: center;
            padding: 16px 20px;
            border-bottom: 1px solid #f0f0f0;
            cursor: pointer;
            transition: background 0.2s;
        }

        .file-item:hover {
            background: #f8f9fa;
        }

        .file-item:last-child {
            border-bottom: none;
        }

        .file-icon {
            width: 40px;
            height: 40px;
            border-radius: 8px;
            display: flex;
            align-items: center;
            justify-content: center;
            margin-right: 15px;
            font-size: 18px;
        }

        .folder {
            background: #e3f2fd;
            color: #1976d2;
        }

        .file {
            background: #f3e5f5;
            color: #7b1fa2;
        }

        .image {
            background: #e8f5e8;
            color: #388e3c;
        }

        .file-info {
            flex: 1;
        }

        .file-name {
            font-weight: 500;
            margin-bottom: 4px;
        }

        .file-meta {
            font-size: 12px;
            color: #666;
        }

        .file-size {
            font-size: 12px;
            color: #999;
        }

        .loading {
            text-align: center;
            padding: 40px;
            color: #666;
        }

        .error {
            text-align: center;
            padding: 40px;
            color: #d32f2f;
            background: #ffebee;
            margin: 20px;
            border-radius: 8px;
        }

        .refresh-btn {
            background: #007DFF;
            color: white;
            border: none;
            padding: 12px 24px;
            border-radius: 8px;
            cursor: pointer;
            font-size: 14px;
            margin: 10px 0;
            transition: background 0.2s;
        }

        .refresh-btn:hover {
            background: #0056b3;
        }

        .path-bar {
            background: white;
            padding: 12px 20px;
            border-bottom: 1px solid #f0f0f0;
            font-size: 14px;
            color: #666;
        }
    </style>
</head>
<body>
    <div class="container">
        <div class="header">
            <h1>📁 鸿蒙文件管理器</h1>
            <p class="subtitle">基于Web组件 + ArkTS混合开发</p>
        </div>

        <div class="stats-card">
            <h3>存储概览</h3>
            <div class="stats-grid" id="storageStats">
                <div class="loading">加载中...</div>
            </div>
        </div>

        <div class="path-bar">
            当前路径: <span id="currentPath">/</span>
        </div>

        <button class="refresh-btn" onclick="loadFileList()">
            🔄 刷新文件列表
        </button>

        <div class="file-list" id="fileList">
            <div class="loading">正在加载文件列表...</div>
        </div>
    </div>

    <script src="js/app.js"></script>
</body>
</html>

4. 前端交互逻辑(app.js)

javascript

class FileManager {
    constructor() {
        this.currentPath = '/';
        this.init();
    }

    async init() {
        await this.loadStorageInfo();
        await this.loadFileList();
    }

    // 加载存储信息
    async loadStorageInfo() {
        try {
            const result = await window.harmonyNative.getStorageInfo();
            const data = JSON.parse(result);
            
            if (data.success) {
                this.renderStorageInfo(data.data);
            } else {
                this.showError('获取存储信息失败');
            }
        } catch (error) {
            console.error('获取存储信息失败:', error);
            this.showError('获取存储信息失败');
        }
    }

    // 渲染存储信息
    renderStorageInfo(storageData) {
        const statsContainer = document.getElementById('storageStats');
        statsContainer.innerHTML = `
            <div class="stat-item">
                <div class="stat-value">${storageData.total}</div>
                <div class="stat-label">总空间</div>
            </div>
            <div class="stat-item">
                <div class="stat-value">${storageData.used}</div>
                <div class="stat-label">已使用</div>
            </div>
            <div class="stat-item">
                <div class="stat-value">${storageData.available}</div>
                <div class="stat-label">可用空间</div>
            </div>
        `;
    }

    // 加载文件列表
    async loadFileList(path = '/') {
        try {
            this.showLoading();
            this.currentPath = path;
            document.getElementById('currentPath').textContent = path;

            const result = await window.harmonyNative.getFileList(path);
            const data = JSON.parse(result);
            
            if (data.success) {
                this.renderFileList(data.data);
                window.harmonyNative.showToast(`已加载 ${data.data.length} 个项目`);
            } else {
                this.showError(`加载失败: ${data.error}`);
            }
        } catch (error) {
            console.error('加载文件列表失败:', error);
            this.showError('加载文件列表失败');
        }
    }

    // 渲染文件列表
    renderFileList(files) {
        const fileListContainer = document.getElementById('fileList');
        
        if (files.length === 0) {
            fileListContainer.innerHTML = '<div class="loading">目录为空</div>';
            return;
        }

        const filesHTML = files.map(file => `
            <div class="file-item" onclick="handleFileClick('${file.path}', ${file.isDirectory})">
                <div class="file-icon ${file.type}">
                    ${this.getFileIcon(file.type)}
                </div>
                <div class="file-info">
                    <div class="file-name">${this.escapeHtml(file.name)}</div>
                    <div class="file-meta">
                        修改时间: ${new Date(file.mTime).toLocaleString()}
                    </div>
                </div>
                <div class="file-size">
                    ${file.isDirectory ? '' : this.formatFileSize(file.size)}
                </div>
            </div>
        `).join('');

        fileListContainer.innerHTML = filesHTML;
    }

    // 获取文件图标
    getFileIcon(fileType) {
        const icons = {
            'folder': '📁',
            'text': '📄',
            'image': '🖼️',
            'pdf': '📕',
            'video': '🎬',
            'audio': '🎵',
            'file': '📎'
        };
        return icons[fileType] || '📎';
    }

    // 处理文件点击
    async handleFileClick(filePath, isDirectory) {
        if (isDirectory) {
            await this.loadFileList(filePath);
        } else {
            await this.readFileContent(filePath);
        }
    }

    // 读取文件内容
    async readFileContent(filePath) {
        try {
            const result = await window.harmonyNative.readFileContent(filePath);
            const data = JSON.parse(result);
            
            if (data.success) {
                window.harmonyNative.showToast(`已读取文件内容 (${data.data.length} 字符)`);
                // 这里可以显示文件内容预览
                console.log('文件内容:', data.data);
            } else {
                this.showError(`读取文件失败: ${data.error}`);
            }
        } catch (error) {
            console.error('读取文件失败:', error);
            this.showError('读取文件失败');
        }
    }

    // 显示加载状态
    showLoading() {
        document.getElementById('fileList').innerHTML = 
            '<div class="loading">正在加载...</div>';
    }

    // 显示错误信息
    showError(message) {
        document.getElementById('fileList').innerHTML = `
            <div class="error">
                ❌ ${message}
                <br>
                <button class="refresh-btn" onclick="loadFileList()" style="margin-top: 10px;">
                    重试
                </button>
            </div>
        `;
    }

    // 格式化文件大小
    formatFileSize(bytes) {
        if (bytes === 0) return '0 B';
        const k = 1024;
        const sizes = ['B', 'KB', 'MB', 'GB'];
        const i = Math.floor(Math.log(bytes) / Math.log(k));
        return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
    }

    // HTML转义
    escapeHtml(unsafe) {
        return unsafe
            .replace(/&/g, "&amp;")
            .replace(/</g, "&lt;")
            .replace(/>/g, "&gt;")
            .replace(/"/g, "&quot;")
            .replace(/'/g, "&#039;");
    }
}

// 全局函数供HTML调用
async function loadFileList(path = '/') {
    await fileManager.loadFileList(path);
}

async function handleFileClick(filePath, isDirectory) {
    await fileManager.handleFileClick(filePath, isDirectory);
}

// 初始化应用
let fileManager;
document.addEventListener('DOMContentLoaded', function() {
    fileManager = new FileManager();
});

四、开发技巧与最佳实践

1. 通信安全设计

typescript

// 参数验证示例
private validatePath(path: string): boolean {
    // 防止路径遍历攻击
    return !path.includes('../') && path.startsWith('/approved/path');
}

2. 性能优化建议

  • 使用虚拟滚动处理大量文件

  • 实现文件列表缓存机制

  • 减少不必要的原生调用

3. 错误处理策略

javascript

// 统一的错误处理
class ErrorHandler {
    static handleBridgeError(error) {
        console.error('Bridge Error:', error);
        // 显示用户友好的错误信息
        // 重试机制
        // 错误上报
    }
}

五、总结与展望

通过本文的实战演示,我们看到了在鸿蒙平台上实现Electron式开发的完整流程。这种混合开发模式结合了Web技术的灵活性和原生应用的强大能力,为开发者提供了全新的选择。

核心优势:

  • 🚀 开发效率:利用丰富的Web生态快速构建UI

  • 💪 性能体验:原生能力保障应用性能

  • 🔄 技术复用:现有Web团队可快速上手

  • 🎯 动态更新:Web资源支持热更新

适用场景:

  • 内容管理类应用

  • 数据可视化大屏

  • 企业内部工具

  • 快速原型开发

随着鸿蒙生态的不断发展,相信这种混合开发模式会越来越成熟,为开发者带来更多可能性!

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐