HarmonyOS APP开发Ability迁移实战

举报
Jack20 发表于 2026/06/19 23:22:20 2026/06/19
【摘要】 HarmonyOS APP开发Ability迁移实战 背景与动机上篇文章我们聊了分布式任务调度的基本原理,这篇来点实战的。Ability迁移听起来高大上,但真正落地时,细节才是魔鬼。想象这样一个场景:你在地铁上用手机写文档,到家后想把文档"甩"到电脑上继续编辑。理想情况下,手机上光标在哪,电脑上光标就在哪;手机上选中的文字,电脑上也选中;甚至连撤销历史都能保留。这就是"无缝切换"的真正含义...

HarmonyOS APP开发Ability迁移实战

背景与动机

上篇文章我们聊了分布式任务调度的基本原理,这篇来点实战的。Ability迁移听起来高大上,但真正落地时,细节才是魔鬼。

想象这样一个场景:你在地铁上用手机写文档,到家后想把文档"甩"到电脑上继续编辑。理想情况下,手机上光标在哪,电脑上光标就在哪;手机上选中的文字,电脑上也选中;甚至连撤销历史都能保留。这就是"无缝切换"的真正含义。

但现实往往骨感。迁移后页面跳转了、输入框内容丢了、滚动位置变了……这些问题每一个都能让用户体验大打折扣。今天我们就来逐一攻克这些难题。

核心原理

Ability迁移的完整生命周期

Ability迁移不是简单的"复制粘贴",而是一个精心设计的生命周期流程:
图片.png

状态分类与处理策略

迁移的状态数据可以分为三类,每类的处理策略不同:

1. 框架级状态:由系统自动处理,如Activity栈、Fragment状态
2. UI状态:需要手动序列化,如滚动位置、输入框内容、选中项
3. 业务数据:需要同步策略,如编辑中的文档、购物车内容

// 状态分类示例
interface MigrateState {
    // 框架级状态(系统处理)
    navigationState: NavigationState;
    
    // UI状态(需要序列化)
    uiState: {
        scrollPosition: number;
        inputText: string;
        selectedTabIndex: number;
        expandedItems: number[];
    };
    
    // 业务数据(需要同步策略)
    businessData: {
        documentContent: string;
        editHistory: EditAction[];
        unsavedChanges: boolean;
    };
}

代码实战

示例一:复杂UI状态迁移

这是一个文档编辑应用,需要迁移编辑器状态、侧边栏展开状态、工具栏配置等复杂UI状态。

// DocumentEditorAbility.ets
import UIAbility from '@ohos.app.ability.UIAbility';
import Want from '@ohos.app.ability.Want';
import AbilityConstant from '@ohos.app.ability.AbilityConstant';
import window from '@ohos.window';

export default class DocumentEditorAbility extends UIAbility {
    // 编辑器状态
    private editorState: EditorState = {
        documentId: '',
        content: '',
        cursorPosition: 0,
        selectionStart: -1,
        selectionEnd: -1,
        scrollOffset: 0,
        zoomLevel: 1.0
    };

    // UI配置状态
    private uiConfig: UIConfigState = {
        sidebarExpanded: true,
        activeToolGroup: 'format',
        showRuler: true,
        theme: 'light',
        fontSize: 14
    };

    // 编辑历史(用于撤销/重做)
    private editHistory: EditAction[] = [];
    private historyIndex: number = -1;

    // 窗口实例
    private mainWindow: window.Window | null = null;

    // 保存完整状态
    onSaveData(want: Want): boolean {
        console.info('[DocumentEditor] onSaveData');
        
        // 序列化编辑器状态
        const editorStateJson = JSON.stringify(this.editorState);
        
        // 序列化UI配置
        const uiConfigJson = JSON.stringify(this.uiConfig);
        
        // 序列化编辑历史(限制数量避免数据过大)
        const recentHistory = this.editHistory.slice(-50); // 最多保存50步历史
        const historyJson = JSON.stringify({
            actions: recentHistory,
            index: this.historyIndex
        });
        
        want.parameters = {
            ...want.parameters,
            // 编辑器核心状态
            'editorState': editorStateJson,
            // UI配置
            'uiConfig': uiConfigJson,
            // 编辑历史
            'editHistory': historyJson,
            // 迁移元数据
            'migrateMeta': JSON.stringify({
                timestamp: Date.now(),
                version: '2.0',
                sourceDevice: this.getDeviceId()
            })
        };
        
        return true;
    }

    // 恢复完整状态
    onRestoreData(want: Want): boolean {
        console.info('[DocumentEditor] onRestoreData');
        
        if (!want.parameters) {
            console.error('[DocumentEditor] No parameters');
            return false;
        }
        
        try {
            // 恢复编辑器状态
            const editorStateJson = want.parameters['editorState'] as string;
            this.editorState = JSON.parse(editorStateJson);
            
            // 恢复UI配置
            const uiConfigJson = want.parameters['uiConfig'] as string;
            this.uiConfig = JSON.parse(uiConfigJson);
            
            // 恢复编辑历史
            const historyJson = want.parameters['editHistory'] as string;
            const historyData = JSON.parse(historyJson);
            this.editHistory = historyData.actions;
            this.historyIndex = historyData.index;
            
            // 验证迁移元数据
            const metaJson = want.parameters['migrateMeta'] as string;
            const meta = JSON.parse(metaJson);
            console.info('[DocumentEditor] Migrated from device:', meta.sourceDevice);
            
            return true;
        } catch (error) {
            console.error('[DocumentEditor] Failed to restore data:', error);
            return false;
        }
    }

    // 窗口创建时恢复UI
    async onWindowStageCreate(windowStage: window.WindowStage): Promise<void> {
        console.info('[DocumentEditor] onWindowStageCreate');
        
        // 获取主窗口
        this.mainWindow = await windowStage.getMainWindow();
        
        // 应用UI配置
        await this.applyUIConfig();
        
        // 加载页面,传递编辑器状态
        windowStage.loadContent('pages/DocumentEditor', (err) => {
            if (err.code) {
                console.error('[DocumentEditor] Failed to load content:', err);
                return;
            }
            
            // 页面加载完成后,通知恢复编辑器状态
            this.notifyEditorRestore();
        });
    }

    // 应用UI配置到窗口
    private async applyUIConfig(): Promise<void> {
        if (!this.mainWindow) return;
        
        try {
            // 设置窗口亮度
            await this.mainWindow.setWindowBrightness(1.0);
            
            // 根据主题设置窗口背景色
            const bgColor = this.uiConfig.theme === 'dark' ? '#1a1a1a' : '#ffffff';
            await this.mainWindow.setWindowBackgroundColor(bgColor);
            
            console.info('[DocumentEditor] UI config applied');
        } catch (error) {
            console.error('[DocumentEditor] Failed to apply UI config:', error);
        }
    }

    // 通知页面恢复编辑器状态
    private notifyEditorRestore(): void {
        // 通过AppStorage或Emitter通知页面
        AppStorage.setOrCreate('editorRestoreState', this.editorState);
        AppStorage.setOrCreate('uiConfigRestore', this.uiConfig);
        AppStorage.setOrCreate('editHistoryRestore', {
            actions: this.editHistory,
            index: this.historyIndex
        });
    }

    // 获取当前设备ID
    private getDeviceId(): string {
        // 实际实现需要调用设备管理API
        return 'current_device';
    }

    onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
        console.info('[DocumentEditor] onCreate, reason:', launchParam.launchReason);
        
        if (launchParam.launchReason === AbilityConstant.LaunchReason.CONTINUATION) {
            this.onRestoreData(want);
        }
    }
}

// 状态接口定义
interface EditorState {
    documentId: string;
    content: string;
    cursorPosition: number;
    selectionStart: number;
    selectionEnd: number;
    scrollOffset: number;
    zoomLevel: number;
}

interface UIConfigState {
    sidebarExpanded: boolean;
    activeToolGroup: string;
    showRuler: boolean;
    theme: string;
    fontSize: number;
}

interface EditAction {
    type: 'insert' | 'delete' | 'replace';
    position: number;
    content: string;
    length: number;
}

示例二:页面栈迁移与路由恢复

迁移时不仅要恢复当前页面状态,还要恢复整个页面导航栈。这样用户按返回键时,才能回到正确的页面。

// NavigationMigrateAbility.ets
import UIAbility from '@ohos.app.ability.UIAbility';
import Want from '@ohos.app.ability.Want';
import router from '@ohos.router';

export default class NavigationMigrateAbility extends UIAbility {
    // 导航栈状态
    private navigationStack: NavStackItem[] = [];
    private currentTabIndex: number = 0;

    // 保存导航状态
    onSaveData(want: Want): boolean {
        // 构建导航栈快照
        const navSnapshot: NavigationSnapshot = {
            stack: this.navigationStack.map(item => ({
                pagePath: item.pagePath,
                params: item.params,
                timestamp: item.timestamp
            })),
            currentTab: this.currentTabIndex,
            stackLength: router.getLength()
        };
        
        want.parameters = {
            ...want.parameters,
            'navigationSnapshot': JSON.stringify(navSnapshot)
        };
        
        console.info('[Navigation] Saved navigation stack, length:', navSnapshot.stack.length);
        return true;
    }

    // 恢复导航状态
    onRestoreData(want: Want): boolean {
        if (!want.parameters?.['navigationSnapshot']) {
            return false;
        }
        
        try {
            const navSnapshot: NavigationSnapshot = 
                JSON.parse(want.parameters['navigationSnapshot'] as string);
            
            // 恢复导航栈
            this.navigationStack = navSnapshot.stack;
            this.currentTabIndex = navSnapshot.currentTab;
            
            console.info('[Navigation] Restored navigation stack, length:', 
                this.navigationStack.length);
            
            return true;
        } catch (error) {
            console.error('[Navigation] Failed to restore:', error);
            return false;
        }
    }

    // 在页面加载后重建导航栈
    async rebuildNavigationStack(): Promise<void> {
        if (this.navigationStack.length === 0) return;
        
        // 从后往前重建导航栈
        // 最后一个元素是当前页面,不需要push
        const stackToRebuild = this.navigationStack.slice(0, -1);
        
        for (const item of stackToRebuild) {
            try {
                // 使用replace模式重建,避免产生额外的历史记录
                await router.replaceNamedRoute({
                    name: item.pagePath,
                    params: item.params
                });
            } catch (error) {
                console.error('[Navigation] Failed to rebuild page:', item.pagePath, error);
            }
        }
        
        // 最后push当前页面
        const currentPage = this.navigationStack[this.navigationStack.length - 1];
        if (currentPage) {
            await router.pushUrl({
                url: currentPage.pagePath,
                params: currentPage.params
            });
        }
    }

    // 记录页面跳转
    recordNavigation(pagePath: string, params: Record<string, Object>): void {
        this.navigationStack.push({
            pagePath,
            params,
            timestamp: Date.now()
        });
        
        // 限制栈深度
        if (this.navigationStack.length > 20) {
            this.navigationStack.shift();
        }
    }

    // 处理返回键
    async handleBackPress(): Promise<boolean> {
        if (this.navigationStack.length > 1) {
            this.navigationStack.pop();
            const prevPage = this.navigationStack[this.navigationStack.length - 1];
            
            await router.replaceUrl({
                url: prevPage.pagePath,
                params: prevPage.params
            });
            
            return true;
        }
        
        return false;
    }
}

interface NavStackItem {
    pagePath: string;
    params: Record<string, Object>;
    timestamp: number;
}

interface NavigationSnapshot {
    stack: NavStackItem[];
    currentTab: number;
    stackLength: number;
}

示例三:实时状态同步迁移

对于实时协作类应用(如在线白板、协同编辑),迁移时需要考虑实时状态的同步。

// RealtimeSyncMigrateAbility.ets
import UIAbility from '@ohos.app.ability.UIAbility';
import Want from '@ohos.app.ability.Want';
import webSocket from '@ohos.net.webSocket';

export default class RealtimeSyncMigrateAbility extends UIAbility {
    // WebSocket连接
    private wsClient: webSocket.WebSocket | null = null;
    
    // 实时数据状态
    private realtimeState: RealtimeState = {
        sessionId: '',
        lastSyncSeq: 0,
        pendingOperations: [],
        collaborators: []
    };

    // 画布状态
    private canvasState: CanvasState = {
        elements: [],
        viewport: { x: 0, y: 0, zoom: 1 },
        selectedIds: [],
        clipboard: null
    };

    // 保存实时状态
    onSaveData(want: Want): boolean {
        // 先暂停实时同步
        this.pauseRealtimeSync();
        
        // 等待待处理操作完成
        this.flushPendingOperations();
        
        want.parameters = {
            ...want.parameters,
            'realtimeState': JSON.stringify(this.realtimeState),
            'canvasState': JSON.stringify(this.canvasState),
            'connectionInfo': JSON.stringify({
                serverUrl: this.getServerUrl(),
                reconnectToken: this.generateReconnectToken()
            })
        };
        
        return true;
    }

    // 恢复实时状态
    onRestoreData(want: Want): boolean {
        if (!want.parameters) return false;
        
        try {
            // 恢复画布状态
            const canvasStateJson = want.parameters['canvasState'] as string;
            this.canvasState = JSON.parse(canvasStateJson);
            
            // 恢复实时状态
            const realtimeStateJson = want.parameters['realtimeState'] as string;
            this.realtimeState = JSON.parse(realtimeStateJson);
            
            // 恢复连接信息
            const connInfoJson = want.parameters['connectionInfo'] as string;
            const connInfo = JSON.parse(connInfoJson);
            
            // 异步重建实时连接
            this.rebuildRealtimeConnection(connInfo);
            
            return true;
        } catch (error) {
            console.error('[RealtimeSync] Failed to restore:', error);
            return false;
        }
    }

    // 暂停实时同步
    private pauseRealtimeSync(): void {
        if (this.wsClient) {
            // 发送暂停消息
            this.wsClient.send(JSON.stringify({
                type: 'pause',
                sessionId: this.realtimeState.sessionId
            }));
        }
    }

    // 刷新待处理操作
    private flushPendingOperations(): void {
        if (!this.wsClient || this.realtimeState.pendingOperations.length === 0) {
            return;
        }
        
        // 批量发送待处理操作
        const batchOps = {
            type: 'batch',
            operations: this.realtimeState.pendingOperations,
            lastSeq: this.realtimeState.lastSyncSeq
        };
        
        this.wsClient.send(JSON.stringify(batchOps));
        this.realtimeState.pendingOperations = [];
    }

    // 重建实时连接
    private async rebuildRealtimeConnection(connInfo: ConnectionInfo): Promise<void> {
        try {
            // 创建新的WebSocket连接
            this.wsClient = webSocket.createWebSocket();
            
            await this.wsClient.connect(connInfo.serverUrl, {
                header: {
                    'Reconnect-Token': connInfo.reconnectToken,
                    'Session-Id': this.realtimeState.sessionId
                }
            });
            
            // 监听消息
            this.wsClient.on('message', (err, value) => {
                this.handleRealtimeMessage(value);
            });
            
            // 发送同步请求,获取迁移期间的数据更新
            this.wsClient.send(JSON.stringify({
                type: 'sync',
                fromSeq: this.realtimeState.lastSyncSeq,
                sessionId: this.realtimeState.sessionId
            }));
            
            console.info('[RealtimeSync] Connection rebuilt');
        } catch (error) {
            console.error('[RealtimeSync] Failed to rebuild connection:', error);
            // 重试逻辑
            setTimeout(() => this.rebuildRealtimeConnection(connInfo), 3000);
        }
    }

    // 处理实时消息
    private handleRealtimeMessage(message: string): void {
        try {
            const data = JSON.parse(message);
            
            switch (data.type) {
                case 'sync':
                    // 应用同步数据
                    this.applySyncData(data);
                    break;
                case 'operation':
                    // 应用远程操作
                    this.applyRemoteOperation(data.operation);
                    break;
                case 'collaborator_join':
                    this.realtimeState.collaborators.push(data.collaborator);
                    break;
                case 'collaborator_leave':
                    this.realtimeState.collaborators = 
                        this.realtimeState.collaborators.filter(
                            c => c.id !== data.collaboratorId
                        );
                    break;
            }
        } catch (error) {
            console.error('[RealtimeSync] Failed to handle message:', error);
        }
    }

    // 应用同步数据
    private applySyncData(syncData: any): void {
        // 合并远程更新
        for (const op of syncData.operations) {
            this.applyRemoteOperation(op);
        }
        
        // 更新同步序列号
        this.realtimeState.lastSyncSeq = syncData.toSeq;
        
        // 通知UI更新
        this.notifyCanvasUpdate();
    }

    // 应用远程操作
    private applyRemoteOperation(operation: any): void {
        // 根据操作类型更新画布状态
        switch (operation.type) {
            case 'add':
                this.canvasState.elements.push(operation.element);
                break;
            case 'update':
                const idx = this.canvasState.elements.findIndex(
                    e => e.id === operation.elementId
                );
                if (idx >= 0) {
                    this.canvasState.elements[idx] = operation.element;
                }
                break;
            case 'delete':
                this.canvasState.elements = this.canvasState.elements.filter(
                    e => e.id !== operation.elementId
                );
                break;
        }
    }

    // 通知画布更新
    private notifyCanvasUpdate(): void {
        AppStorage.setOrCreate('canvasElements', this.canvasState.elements);
        AppStorage.setOrCreate('viewport', this.canvasState.viewport);
    }

    private getServerUrl(): string {
        return 'wss://collab.example.com/ws';
    }

    private generateReconnectToken(): string {
        return `reconnect_${Date.now()}_${Math.random().toString(36).substr(2)}`;
    }
}

interface RealtimeState {
    sessionId: string;
    lastSyncSeq: number;
    pendingOperations: any[];
    collaborators: Collaborator[];
}

interface CanvasState {
    elements: CanvasElement[];
    viewport: { x: number; y: number; zoom: number };
    selectedIds: string[];
    clipboard: any;
}

interface Collaborator {
    id: string;
    name: string;
    color: string;
}

interface CanvasElement {
    id: string;
    type: string;
    x: number;
    y: number;
    width: number;
    height: number;
    props: Record<string, any>;
}

interface ConnectionInfo {
    serverUrl: string;
    reconnectToken: string;
}

踩坑与注意事项

坑一:异步操作未完成就迁移

问题描述:迁移时如果有未完成的异步操作(如网络请求、文件写入),可能导致数据不一致。

解决方案:迁移前等待关键操作完成

// 添加迁移锁
private migrateLock: boolean = false;
private pendingOperations: Promise<any>[] = [];

async onSaveData(want: Want): Promise<boolean> {
    // 设置迁移锁,阻止新的异步操作
    this.migrateLock = true;
    
    try {
        // 等待所有待处理操作完成
        await Promise.all(this.pendingOperations);
        
        // 执行状态保存
        return this.doSaveData(want);
    } finally {
        this.migrateLock = false;
    }
}

// 包装异步操作
async wrapOperation<T>(op: Promise<T>): Promise<T> {
    if (this.migrateLock) {
        throw new Error('Migration in progress');
    }
    
    this.pendingOperations.push(op);
    try {
        return await op;
    } finally {
        this.pendingOperations = this.pendingOperations.filter(p => p !== op);
    }
}

坑二:资源未正确释放

问题描述:迁移后源设备的资源(如文件句柄、传感器监听)未释放,导致资源泄漏。

解决方案

onContinuationDone(result: number): void {
    if (result === AbilityConstant.OnContinuationResult.CONTINUATION_SUCCESS) {
        // 迁移成功,释放资源
        this.releaseResources();
    }
}

private releaseResources(): void {
    // 关闭文件句柄
    if (this.fileHandle) {
        this.fileHandle.close();
        this.fileHandle = null;
    }
    
    // 取消传感器监听
    if (this.sensorSubscriber) {
        sensor.off(this.sensorSubscriber);
        this.sensorSubscriber = null;
    }
    
    // 关闭网络连接
    if (this.wsClient) {
        this.wsClient.close();
        this.wsClient = null;
    }
}

坑三:时区差异导致时间计算错误

问题描述:源设备和目标设备可能处于不同时区,导致时间相关的逻辑出错。

解决方案:统一使用UTC时间戳

// 错误示例
const localTime = new Date().getTime(); // 本地时间戳

// 正确示例
const utcTime = Date.now(); // UTC时间戳,与时区无关

// 如果需要显示本地时间,在显示时转换
function formatLocalTime(utcTimestamp: number): string {
    const date = new Date(utcTimestamp);
    return date.toLocaleString(); // 根据设备时区显示
}

坑四:迁移后UI闪烁

问题描述:迁移后页面先显示默认状态,再恢复保存的状态,导致UI闪烁。

解决方案:使用骨架屏或加载状态

// 页面代码
@Entry
@Component
struct EditorPage {
    @State isRestoring: boolean = true;
    @State editorState: EditorState | null = null;

    aboutToAppear(): void {
        // 检查是否有迁移状态
        const restoreState = AppStorage.get<EditorState>('editorRestoreState');
        if (restoreState) {
            this.editorState = restoreState;
            this.isRestoring = false;
        } else {
            // 正常启动,加载默认数据
            this.loadDefaultData();
        }
    }

    build() {
        if (this.isRestoring) {
            // 显示骨架屏
            this.buildSkeleton();
        } else {
            // 显示实际内容
            this.buildContent();
        }
    }

    @Builder
    buildSkeleton() {
        Column() {
            // 骨架屏UI
            Skeleton()
        }
    }

    @Builder
    buildContent() {
        Column() {
            // 实际内容
            Editor({ state: this.editorState })
        }
    }
}

HarmonyOS 6适配

新增迁移预览功能

HarmonyOS 6支持迁移前的预览功能,可以在目标设备上先显示预览,用户确认后再正式迁移。

// HarmonyOS 6新增API
interface ContinuationPreview {
    // 生成预览数据
    generatePreview(): Want;
    
    // 预览确认回调
    onPreviewConfirmed(): void;
    
    // 预览取消回调
    onPreviewCancelled(): void;
}

// 使用示例
async previewAndMigrate(targetDeviceId: string): Promise<void> {
    // 生成预览数据
    const previewWant = this.generatePreview();
    
    // 发送预览到目标设备
    await this.context.continueAbilityPreview({
        deviceId: targetDeviceId,
        previewWant: previewWant,
        onConfirm: () => {
            // 用户确认,执行正式迁移
            this.context.continueAbility({ deviceId: targetDeviceId });
        },
        onCancel: () => {
            // 用户取消,清理预览
            this.cleanupPreview();
        }
    });
}

增强的状态压缩

HarmonyOS 6内置了状态压缩功能,可以自动压缩大型状态数据:

// 配置状态压缩
const migrateOptions: AbilityConstant.ContinuationOptions = {
    deviceId: targetDeviceId,
    compressData: true,           // 启用压缩
    compressionThreshold: 102400, // 超过100KB时压缩
    compressionAlgorithm: 'gzip'  // 使用gzip算法
};

迁移进度通知

HarmonyOS 6新增了迁移进度回调,可以实时反馈迁移状态:

// 监听迁移进度
this.context.on('continuationProgress', (progress: ContinuationProgress) => {
    console.info(`Migration progress: ${progress.stage} - ${progress.percent}%`);
    
    switch (progress.stage) {
        case 'preparing':
            this.showProgress('正在准备迁移...');
            break;
        case 'transferring':
            this.showProgress(`正在传输数据... ${progress.percent}%`);
            break;
        case 'restoring':
            this.showProgress('正在恢复状态...');
            break;
        case 'completed':
            this.hideProgress();
            break;
    }
});

interface ContinuationProgress {
    stage: 'preparing' | 'transferring' | 'restoring' | 'completed';
    percent: number;
    transferredBytes: number;
    totalBytes: number;
}

总结

Ability迁移的实战要点在于"细节"。原理大家都懂,但真正落地时,滚动位置、光标位置、选中状态、编辑历史……每一个细节都需要精心处理。

关键实战经验

  1. 状态分类处理:框架状态、UI状态、业务数据采用不同的策略
  2. 导航栈恢复:完整重建页面栈,保证返回键行为正确
  3. 实时同步衔接:迁移前后保证实时数据不丢失、不冲突
  4. 异步操作管理:迁移前等待关键操作完成,避免数据不一致
  5. 资源正确释放:迁移成功后清理源设备资源
  6. UI无闪烁:使用骨架屏或加载状态,避免状态切换时的视觉跳动

性能优化建议

  • 控制序列化数据大小,大数据使用分布式存储
  • 合理设置迁移超时时间
  • 使用增量同步减少数据传输量
  • 善用HarmonyOS 6的压缩和预览功能

Ability迁移是分布式能力的核心应用场景,掌握了这些实战技巧,你就能构建出真正"无缝"的多设备体验。下一篇我们将深入Continuation开发,探讨更灵活的跨设备流转能力。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。