0
  • 聊天消息
  • 系統(tǒng)消息
  • 評論與回復(fù)
登錄后你可以
  • 下載海量資料
  • 學(xué)習(xí)在線課程
  • 觀看技術(shù)視頻
  • 寫文章/發(fā)帖/加入社區(qū)
會員中心
創(chuàng)作中心

完善資料讓更多小伙伴認(rèn)識你,還能領(lǐng)取20積分哦,立即完善>

3天內(nèi)不再提示

鴻蒙開發(fā):應(yīng)用組件跨設(shè)備交互(流轉(zhuǎn))【多端協(xié)同】

jf_46214456 ? 來源:jf_46214456 ? 作者:jf_46214456 ? 2024-06-13 10:42 ? 次閱讀

多端協(xié)同

功能描述

多端協(xié)同主要包括如下場景:

  • [通過跨設(shè)備啟動UIAbility和ServiceExtensionAbility組件實現(xiàn)多端協(xié)同(無返回數(shù)據(jù))]
  • [通過跨設(shè)備啟動UIAbility組件實現(xiàn)多端協(xié)同(獲取返回數(shù)據(jù))]
  • [通過跨設(shè)備連接ServiceExtensionAbility組件實現(xiàn)多端協(xié)同]
  • [通過跨設(shè)備Call調(diào)用實現(xiàn)多端協(xié)同]

多端協(xié)同流程

多端協(xié)同流程如下圖所示。

圖1 多端協(xié)同流程圖 hop-multi-device-collaboration

約束限制

  • 由于“多端協(xié)同任務(wù)管理”能力尚未具備,開發(fā)者當(dāng)前只能通過開發(fā)系統(tǒng)應(yīng)用獲取設(shè)備列表,不支持三方應(yīng)用接入。
  • 多端協(xié)同需遵循[分布式跨設(shè)備組件啟動規(guī)則]。
  • 為了獲得最佳體驗,使用want傳輸?shù)臄?shù)據(jù)建議在100KB以下。
  • 開發(fā)前請熟悉鴻蒙開發(fā)指導(dǎo)文檔 :[gitee.com/li-shizhen-skin/harmony-os/blob/master/README.md]

通過跨設(shè)備啟動UIAbility和ServiceExtensionAbility組件實現(xiàn)多端協(xié)同(無返回數(shù)據(jù))

在設(shè)備A上通過發(fā)起端應(yīng)用提供的啟動按鈕,啟動設(shè)備B上指定的UIAbility與ServiceExtensionAbility。

接口說明

表1 跨設(shè)備啟動API接口功能介紹

接口名描述
startAbility(want: Want, callback: AsyncCallback): void;啟動UIAbility和ServiceExtensionAbility(callback形式)。
stopServiceExtensionAbility(want: Want, callback: AsyncCallback): void;退出啟動的ServiceExtensionAbility(callback形式)。
stopServiceExtensionAbility(want: Want): Promise;退出啟動的ServiceExtensionAbility(Promise形式)。

開發(fā)步驟

  1. 需要申請ohos.permission.DISTRIBUTED_DATASYNC權(quán)限,配置方式請參見[聲明權(quán)限]。
  2. 同時需要在應(yīng)用首次啟動時彈窗向用戶申請授權(quán),使用方式請參見[向用戶申請授權(quán)]。
  3. 獲取目標(biāo)設(shè)備的設(shè)備ID。
    import hilog from '@ohos.hilog';
    import distributedDeviceManager from '@ohos.distributedDeviceManager';
    
    const TAG: string = '[Page_CollaborateAbility]';
    const DOMAIN_NUMBER: number = 0xFF00;
    
    let dmClass: distributedDeviceManager.DeviceManager;
    
    function initDmClass(): void {
      // 其中createDeviceManager接口為系統(tǒng)API
      try {
        dmClass = distributedDeviceManager.createDeviceManager('com.samples.stagemodelabilitydevelop');
        hilog.info(DOMAIN_NUMBER, TAG, JSON.stringify(dmClass) ?? '');
      } catch (err) {
        hilog.error(DOMAIN_NUMBER, TAG, 'createDeviceManager err: ' + JSON.stringify(err));
      };
    }
    
    function getRemoteDeviceId(): string | undefined {
      if (typeof dmClass === 'object' && dmClass !== null) {
        let list = dmClass.getAvailableDeviceListSync();
        hilog.info(DOMAIN_NUMBER, TAG, JSON.stringify(dmClass), JSON.stringify(list));
        if (typeof (list) === 'undefined' || typeof (list.length) === 'undefined') {
          hilog.info(DOMAIN_NUMBER, TAG, 'getRemoteDeviceId err: list is null');
          return;
        }
        if (list.length === 0) {
          hilog.info(DOMAIN_NUMBER, TAG, `getRemoteDeviceId err: list is empty`);
          return;
        }
        return list[0].networkId;
      } else {
        hilog.info(DOMAIN_NUMBER, TAG, 'getRemoteDeviceId err: dmClass is null');
        return;
      }
    }
    
  4. 設(shè)置目標(biāo)組件參數(shù),調(diào)用[startAbility()]接口,啟動UIAbility或ServiceExtensionAbility。
    import { BusinessError } from '@ohos.base';
    import hilog from '@ohos.hilog';
    import Want from '@ohos.app.ability.Want';
    import common from '@ohos.app.ability.common';
    import distributedDeviceManager from '@ohos.distributedDeviceManager';
    import promptAction from '@ohos.promptAction';
    
    const TAG: string = '[Page_CollaborateAbility]';
    const DOMAIN_NUMBER: number = 0xFF00;
    let dmClass: distributedDeviceManager.DeviceManager;
    
    function getRemoteDeviceId(): string | undefined {
      if (typeof dmClass === 'object' && dmClass !== null) {
        let list = dmClass.getAvailableDeviceListSync();
        hilog.info(DOMAIN_NUMBER, TAG, JSON.stringify(dmClass), JSON.stringify(list));
        if (typeof (list) === 'undefined' || typeof (list.length) === 'undefined') {
          hilog.info(DOMAIN_NUMBER, TAG, 'getRemoteDeviceId err: list is null');
          return;
        }
        if (list.length === 0) {
          hilog.info(DOMAIN_NUMBER, TAG, `getRemoteDeviceId err: list is empty`);
          return;
        }
        return list[0].networkId;
      } else {
        hilog.info(DOMAIN_NUMBER, TAG, 'getRemoteDeviceId err: dmClass is null');
        return;
      }
    };
    
    @Entry
    @Component
    struct Page_CollaborateAbility {
      private context = getContext(this) as common.UIAbilityContext;
    
      build() {
        Column() {
          //...
          List({ initialIndex: 0 }) {
            //...
            ListItem() {
              Row() {
                //...
              }
              .onClick(() = > {
                let want: Want = {
                  deviceId: getRemoteDeviceId(),
                  bundleName: 'com.samples.stagemodelabilityinteraction',
                  abilityName: 'CollaborateAbility',
                  moduleName: 'entry' // moduleName非必選
                };
                // context為發(fā)起端UIAbility的AbilityContext
                this.context.startAbility(want).then(() = > {
                  promptAction.showToast({
                    message: $r('app.string.SuccessfulCollaboration')
                  });
                }).catch((err: BusinessError) = > {
                  hilog.error(DOMAIN_NUMBER, TAG, `startAbility err: ` + JSON.stringify(err));
                });
              })
            }
            //...
          }
          //...
        }
        //...
      }
    }
    
  5. 當(dāng)設(shè)備A發(fā)起端應(yīng)用不需要設(shè)備B上的ServiceExtensionAbility時,可調(diào)用[stopServiceExtensionAbility]接口退出。(該接口不支持UIAbility的退出,UIAbility由用戶手動通過任務(wù)管理退出)
    import { BusinessError } from '@ohos.base';
    import hilog from '@ohos.hilog';
    import Want from '@ohos.app.ability.Want';
    import common from '@ohos.app.ability.common';
    import distributedDeviceManager from '@ohos.distributedDeviceManager';
    import promptAction from '@ohos.promptAction';
    
    const TAG: string = '[Page_CollaborateAbility]';
    const DOMAIN_NUMBER: number = 0xFF00;
    let dmClass: distributedDeviceManager.DeviceManager;
    
    function getRemoteDeviceId(): string | undefined {
      if (typeof dmClass === 'object' && dmClass !== null) {
        let list = dmClass.getAvailableDeviceListSync();
        hilog.info(DOMAIN_NUMBER, TAG, JSON.stringify(dmClass), JSON.stringify(list));
        if (typeof (list) === 'undefined' || typeof (list.length) === 'undefined') {
          hilog.info(DOMAIN_NUMBER, TAG, 'getRemoteDeviceId err: list is null');
          return;
        }
        if (list.length === 0) {
          hilog.info(DOMAIN_NUMBER, TAG, `getRemoteDeviceId err: list is empty`);
          return;
        }
        return list[0].networkId;
      } else {
        hilog.info(DOMAIN_NUMBER, TAG, 'getRemoteDeviceId err: dmClass is null');
        return;
      }
    };
    
    @Entry
    @Component
    struct Page_CollaborateAbility {
      private context = getContext(this) as common.UIAbilityContext;
    
      build() {
        // ...
        Button('stopServiceExtensionAbility')
          .onClick(() = > {
            let want: Want = {
              deviceId: getRemoteDeviceId(),
              bundleName: 'com.example.myapplication',
              abilityName: 'FuncAbility',
              moduleName: 'module1', // moduleName非必選
            }
            // 退出由startAbility接口啟動的ServiceExtensionAbility
            this.context.stopServiceExtensionAbility(want).then(() = > {
              hilog.info(DOMAIN_NUMBER, TAG, "stop service extension ability success")
            }).catch((err: BusinessError) = > {
              hilog.error(DOMAIN_NUMBER, TAG, `stop service extension ability err is ` + JSON.stringify(err));
            })
          })
      }
    }
    

通過跨設(shè)備啟動UIAbility組件實現(xiàn)多端協(xié)同(獲取返回數(shù)據(jù))

在設(shè)備A上通過應(yīng)用提供的啟動按鈕,啟動設(shè)備B上指定的UIAbility,當(dāng)設(shè)備B上的UIAbility退出后,會將返回值發(fā)回設(shè)備A上的發(fā)起端應(yīng)用。

接口說明

表2 跨設(shè)備啟動,返回結(jié)果數(shù)據(jù)API接口功能描述

接口名描述
startAbilityForResult(want: Want, callback: AsyncCallback): void;啟動UIAbility并在該Ability退出的時候返回執(zhí)行結(jié)果(callback形式)。
terminateSelfWithResult(parameter: AbilityResult, callback: AsyncCallback): void;停止UIAbility,配合startAbilityForResult使用,返回給接口調(diào)用方AbilityResult信息(callback形式)。
terminateSelfWithResult(parameter: AbilityResult): Promise;停止UIAbility,配合startAbilityForResult使用,返回給接口調(diào)用方AbilityResult信息(promise形式)。

開發(fā)步驟

  1. 需要申請ohos.permission.DISTRIBUTED_DATASYNC權(quán)限,配置方式請參見[聲明權(quán)限]。
  2. 同時需要在應(yīng)用首次啟動時彈窗向用戶申請授權(quán),使用方式請參見[向用戶申請授權(quán)]。
  3. 在發(fā)起端設(shè)置目標(biāo)組件參數(shù),調(diào)用startAbilityForResult()接口啟動目標(biāo)端UIAbility,異步回調(diào)中的data用于接收目標(biāo)端UIAbility停止自身后返回給調(diào)用方UIAbility的信息。getRemoteDeviceId方法參照[通過跨設(shè)備啟動uiability和serviceextensionability組件實現(xiàn)多端協(xié)同無返回數(shù)據(jù)]。
    import { BusinessError } from '@ohos.base';
    import hilog from '@ohos.hilog';
    import Want from '@ohos.app.ability.Want';
    import common from '@ohos.app.ability.common';
    import distributedDeviceManager from '@ohos.distributedDeviceManager';
    import promptAction from '@ohos.promptAction';
    
    const DOMAIN_NUMBER: number = 0xFF00;
    const TAG: string = '[Page_CollaborateAbility]';
    let dmClass: distributedDeviceManager.DeviceManager;
    
    function getRemoteDeviceId(): string | undefined {
      if (typeof dmClass === 'object' && dmClass !== null) {
        let list = dmClass.getAvailableDeviceListSync();
        hilog.info(DOMAIN_NUMBER, TAG, JSON.stringify(dmClass), JSON.stringify(list));
        if (typeof (list) === 'undefined' || typeof (list.length) === 'undefined') {
          hilog.info(DOMAIN_NUMBER, TAG, 'getRemoteDeviceId err: list is null');
          return;
        }
        if (list.length === 0) {
          hilog.info(DOMAIN_NUMBER, TAG, `getRemoteDeviceId err: list is empty`);
          return;
        }
        return list[0].networkId;
      } else {
        hilog.info(DOMAIN_NUMBER, TAG, 'getRemoteDeviceId err: dmClass is null');
        return;
      }
    };
    
    @Entry
    @Component
    struct Page_CollaborateAbility {
      private context = getContext(this) as common.UIAbilityContext;
    
      build() {
        Column() {
          //...
          List({ initialIndex: 0 }) {
            //...
            ListItem() {
              Row() {
                //...
              }
              .onClick(() = > {
                let want: Want = {
                  deviceId: getRemoteDeviceId(),
                  bundleName: 'com.samples.stagemodelabilityinteraction',
                  abilityName: 'ServiceExtAbility',
                  moduleName: 'entry' // moduleName非必選
                };
                // 退出由startAbility接口啟動的ServiceExtensionAbility
                this.context.stopServiceExtensionAbility(want).then(() = > {
                  hilog.info(DOMAIN_NUMBER, TAG, 'stop service extension ability success')
                  promptAction.showToast({
                    message: $r('app.string.SuccessfullyStop')
                  });
                }).catch((err: BusinessError) = > {
                  hilog.error(DOMAIN_NUMBER, TAG, `stop service extension ability err is ` + JSON.stringify(err));
                });
              })
            }
            //...
          }
          //...
        }
        //...
      }
    }
    
  4. 在目標(biāo)端UIAbility任務(wù)完成后,調(diào)用terminateSelfWithResult()方法,將數(shù)據(jù)返回給發(fā)起端的UIAbility。
    import common from '@ohos.app.ability.common';
    import hilog from '@ohos.hilog';
    import { BusinessError } from '@ohos.base';
    
    const TAG: string = '[Page_CollaborateAbility]';
    const DOMAIN_NUMBER: number = 0xFF00;
    
    @Entry
    @Component
    struct Page_CollaborateAbility {
      private context = getContext(this) as common.UIAbilityContext;
    
      build() {
        Column() {
          //...
          List({ initialIndex: 0 }) {
            //...
            ListItem() {
              Row() {
                //...
              }
              .onClick(() = > {
                const RESULT_CODE: number = 1001;
                // context為目標(biāo)端UIAbility的AbilityContext
                this.context.terminateSelfWithResult(
                  {
                    resultCode: RESULT_CODE,
                    want: {
                      bundleName: 'ohos.samples.stagemodelabilitydevelop',
                      abilityName: 'CollaborateAbility',
                      moduleName: 'entry',
                      parameters: {
                        info: '來自Page_CollaborateAbility頁面'
                      }
                    }
                  },
                  (err: BusinessError) = > {
                    hilog.info(DOMAIN_NUMBER, TAG, `terminateSelfWithResult err: ` + JSON.stringify(err));
                  });
              })
            }
            //...
          }
          //...
        }
        //...
      }
    }
    
  5. 發(fā)起端UIAbility接收到目標(biāo)端UIAbility返回的信息,對其進行處理。
    import { BusinessError } from '@ohos.base';
    import hilog from '@ohos.hilog';
    import Want from '@ohos.app.ability.Want';
    import common from '@ohos.app.ability.common';
    import distributedDeviceManager from '@ohos.distributedDeviceManager';
    import promptAction from '@ohos.promptAction';
    
    const TAG: string = '[Page_CollaborateAbility]';
    const DOMAIN_NUMBER: number = 0xFF00;
    let dmClass: distributedDeviceManager.DeviceManager;
    
    function getRemoteDeviceId(): string | undefined {
      if (typeof dmClass === 'object' && dmClass !== null) {
        let list = dmClass.getAvailableDeviceListSync();
        hilog.info(DOMAIN_NUMBER, TAG, JSON.stringify(dmClass), JSON.stringify(list));
        if (typeof (list) === 'undefined' || typeof (list.length) === 'undefined') {
          hilog.info(DOMAIN_NUMBER, TAG, 'getRemoteDeviceId err: list is null');
          return;
        }
        if (list.length === 0) {
          hilog.info(DOMAIN_NUMBER, TAG, `getRemoteDeviceId err: list is empty`);
          return;
        }
        return list[0].networkId;
      } else {
        hilog.info(DOMAIN_NUMBER, TAG, 'getRemoteDeviceId err: dmClass is null');
        return;
      }
    };
    
    @Entry
    @Component
    struct Page_CollaborateAbility {
      private context = getContext(this) as common.UIAbilityContext;
    
      build() {
        Column() {
          //...
          List({ initialIndex: 0 }) {
            //...
            ListItem() {
              Row() {
                //...
              }
              .onClick(() = > {
                let want: Want = {
                  deviceId: getRemoteDeviceId(),
                  bundleName: 'com.samples.stagemodelabilityinteraction',
                  abilityName: 'CollaborateAbility',
                  moduleName: 'entry' // moduleName非必選
                };
                const RESULT_CODE: number = 1001;
                // context為調(diào)用方UIAbility的UIAbilityContext
                this.context.startAbilityForResult(want).then((data) = > {
                  if (data?.resultCode === RESULT_CODE) {
                    // 解析目標(biāo)端UIAbility返回的信息
                    let info = data.want?.parameters?.info;
                    hilog.info(DOMAIN_NUMBER, TAG, JSON.stringify(info) ?? '');
                    if (info !== null) {
                      promptAction.showToast({
                        message : JSON.stringify(info)
                      });
                    }
                  }
                }).catch((error: BusinessError) = > {
                  hilog.error(DOMAIN_NUMBER, TAG, `startAbilityForResult err: ` + JSON.stringify(error));
                });
              })
            }
            //...
          }
          //...
        }
        //...
      }
    }
    

通過跨設(shè)備連接ServiceExtensionAbility組件實現(xiàn)多端協(xié)同

系統(tǒng)應(yīng)用可以通過[connectServiceExtensionAbility()]跨設(shè)備連接一個服務(wù),實現(xiàn)跨設(shè)備遠(yuǎn)程調(diào)用。比如:分布式游戲場景,平板作為遙控器,智慧屏作為顯示器。

接口說明

表3 跨設(shè)備連接API接口功能介紹

接口名描述
connectServiceExtensionAbility(want: Want, options: ConnectOptions): number;連接ServiceExtensionAbility。
disconnectServiceExtensionAbility(connection: number, callback:AsyncCallback): void;斷開連接(callback形式)。
disconnectServiceExtensionAbility(connection: number): Promise;斷開連接(promise形式)。

開發(fā)步驟

  1. 需要申請ohos.permission.DISTRIBUTED_DATASYNC權(quán)限,配置方式請參見[聲明權(quán)限]。
  2. 同時需要在應(yīng)用首次啟動時彈窗向用戶申請授權(quán),使用方式請參見[向用戶申請授權(quán)]。
  3. 如果已有后臺服務(wù),請直接進入下一步;如果沒有,則[實現(xiàn)一個后臺服務(wù)(僅對系統(tǒng)應(yīng)用開放)]。
  4. 連接一個后臺服務(wù)。
    • 實現(xiàn)IAbilityConnection接口。IAbilityConnection提供了以下方法供開發(fā)者實現(xiàn):onConnect()是用來處理連接Service成功的回調(diào),onDisconnect()是用來處理Service異常終止的回調(diào),onFailed()是用來處理連接Service失敗的回調(diào)。

    • 設(shè)置目標(biāo)組件參數(shù),包括目標(biāo)設(shè)備ID、Bundle名稱、Ability名稱。

    • 調(diào)用connectServiceExtensionAbility發(fā)起連接。

    • 連接成功,收到目標(biāo)設(shè)備返回的服務(wù)句柄。

    • 進行跨設(shè)備調(diào)用,獲得目標(biāo)端服務(wù)返回的結(jié)果。

      import { BusinessError } from '@ohos.base';
      import hilog from '@ohos.hilog';
      import Want from '@ohos.app.ability.Want';
      import common from '@ohos.app.ability.common';
      import distributedDeviceManager from '@ohos.distributedDeviceManager';
      import rpc from '@ohos.rpc';
      
      const TAG: string = '[Page_CollaborateAbility]';
      const DOMAIN_NUMBER: number = 0xFF00;
      const REQUEST_CODE = 1;
      let dmClass: distributedDeviceManager.DeviceManager;
      let connectionId: number;
      let options: common.ConnectOptions = {
        onConnect(elementName, remote): void {
          hilog.info(DOMAIN_NUMBER, TAG, 'onConnect callback');
          if (remote === null) {
            hilog.info(DOMAIN_NUMBER, TAG, `onConnect remote is null`);
            return;
          }
          let option = new rpc.MessageOption();
          let data = new rpc.MessageSequence();
          let reply = new rpc.MessageSequence();
          data.writeInt(99); // 開發(fā)者可發(fā)送data到目標(biāo)端應(yīng)用進行相應(yīng)操作
          // @param code 表示客戶端發(fā)送的服務(wù)請求代碼。
          // @param data 表示客戶端發(fā)送的{@link MessageSequence}對象。
          // @param reply 表示遠(yuǎn)程服務(wù)發(fā)送的響應(yīng)消息對象。
          // @param options 指示操作是同步的還是異步的。
          //
          // @return 如果操作成功返回{@code true}; 否則返回 {@code false}。
          remote.sendMessageRequest(REQUEST_CODE, data, reply, option).then((ret: rpc.RequestResult) = > {
            let errCode = reply.readInt(); // 在成功連接的情況下,會收到來自目標(biāo)端返回的信息(100)
            let msg: number = 0;
            if (errCode === 0) {
              msg = reply.readInt();
            }
        // 成功連接后臺服務(wù)
            hilog.info(DOMAIN_NUMBER, TAG, `sendRequest msg:${msg}`);
          }).catch((error: BusinessError) = > {
            hilog.info(DOMAIN_NUMBER, TAG, `sendRequest failed, ${JSON.stringify(error)}`);
          });
        },
        onDisconnect(elementName): void {
          hilog.info(DOMAIN_NUMBER, TAG, 'onDisconnect callback');
        },
        onFailed(code) void {
          hilog.info(DOMAIN_NUMBER, TAG, 'onFailed callback');
        }
      };
      
      function getRemoteDeviceId(): string | undefined {
        if (typeof dmClass === 'object' && dmClass !== null) {
          let list = dmClass.getAvailableDeviceListSync();
          hilog.info(DOMAIN_NUMBER, TAG, JSON.stringify(dmClass), JSON.stringify(list));
          if (typeof (list) === 'undefined' || typeof (list.length) === 'undefined') {
            hilog.info(DOMAIN_NUMBER, TAG, 'getRemoteDeviceId err: list is null');
            return;
          }
          if (list.length === 0) {
            hilog.info(DOMAIN_NUMBER, TAG, `getRemoteDeviceId err: list is empty`);
            return;
          }
          return list[0].networkId;
        } else {
          hilog.info(DOMAIN_NUMBER, TAG, 'getRemoteDeviceId err: dmClass is null');
          return;
        }
      }
      
      @Entry
      @Component
      struct Page_CollaborateAbility {
        private context = getContext(this) as common.UIAbilityContext;
        build() {
          Column() {
            //...
            List({ initialIndex: 0 }) {
              //...
              ListItem() {
                Row() {
                  //...
                }
                .onClick(() = > {
                  let want: Want = {
                    'deviceId': getRemoteDeviceId(),
                    'bundleName': 'com.samples.stagemodelabilityinteraction',
                    'abilityName': 'ServiceExtAbility'
                  };
                  // 建立連接后返回的Id需要保存下來,在解綁服務(wù)時需要作為參數(shù)傳入
                  connectionId = this.context.connectServiceExtensionAbility(want, options);
                })
              }
              //...
            }
            //...
          }
          //...
        }
      }
      

      getRemoteDeviceId方法參照[通過跨設(shè)備啟動uiability和serviceextensionability組件實現(xiàn)多端協(xié)同無返回數(shù)據(jù)]。

  5. 斷開連接。調(diào)用disconnectServiceExtensionAbility()斷開與后臺服務(wù)的連接。
    import { BusinessError } from '@ohos.base';
    import hilog from '@ohos.hilog';
    import Want from '@ohos.app.ability.Want';
    import common from '@ohos.app.ability.common';
    import rpc from '@ohos.rpc';
    import IdlServiceExtProxy from '../IdlServiceExt/idl_service_ext_proxy';
    import promptAction from '@ohos.promptAction';
    
    let connectionId: number;
    const TAG: string = '[Page_CollaborateAbility]';
    const DOMAIN_NUMBER: number = 0xFF00;
    
    @Entry
    @Component
    struct Page_CollaborateAbility {
      private context = getContext(this) as common.UIAbilityContext;
    
      build() {
        Column() {
          //...
          List({ initialIndex: 0 }) {
            //...
            ListItem() {
              Row() {
                //...
              }
              .onClick(() = > {
                this.context.disconnectServiceExtensionAbility(connectionId).then(() = > {
                  hilog.info(DOMAIN_NUMBER, TAG, 'disconnectServiceExtensionAbility success');
                  // 成功斷連后臺服務(wù)
                  promptAction.showToast({
                    message: $r('app.string.SuccessfullyDisconnectBackendService')
                  })
                }).catch((error: BusinessError) = > {
                  hilog.error(DOMAIN_NUMBER, TAG, 'disconnectServiceExtensionAbility failed');
                });
              })
            }
            //...
          }
          //...
        }
        //...
      }
    }
    

通過跨設(shè)備Call調(diào)用實現(xiàn)多端協(xié)同

跨設(shè)備Call調(diào)用的基本原理與設(shè)備內(nèi)Call調(diào)用相同,請參見[通過Call調(diào)用實現(xiàn)UIAbility交互(僅對系統(tǒng)應(yīng)用開放)]。

下面介紹跨設(shè)備Call調(diào)用實現(xiàn)多端協(xié)同的方法。

接口說明

表4 Call API接口功能介紹

接口名描述
startAbilityByCall(want: Want): Promise;啟動指定UIAbility至前臺或后臺,同時獲取其Caller通信接口,調(diào)用方可使用Caller與被啟動的Ability進行通信。
on(method: string, callback: CalleeCallBack): void通用組件Callee注冊method對應(yīng)的callback方法。
off(method: string): void通用組件Callee解注冊method的callback方法。
call(method: string, data: rpc.Parcelable): Promise向通用組件Callee發(fā)送約定序列化數(shù)據(jù)。
callWithResult(method: string, data: rpc.Parcelable): Promise向通用組件Callee發(fā)送約定序列化數(shù)據(jù), 并將Callee返回的約定序列化數(shù)據(jù)帶回。
release(): void釋放通用組件的Caller通信接口。
on(type: "release", callback: OnReleaseCallback): void注冊通用組件通信斷開監(jiān)聽通知。

開發(fā)步驟

  1. 需要申請ohos.permission.DISTRIBUTED_DATASYNC權(quán)限,配置方式請參見[聲明權(quán)限]。
  2. 同時需要在應(yīng)用首次啟動時彈窗向用戶申請授權(quán),使用方式請參見[向用戶申請授權(quán)]。
  3. 創(chuàng)建被調(diào)用端UIAbility。 被調(diào)用端UIAbility需要實現(xiàn)指定方法的數(shù)據(jù)接收回調(diào)函數(shù)、數(shù)據(jù)的序列化及反序列化方法。在需要接收數(shù)據(jù)期間,通過on接口注冊監(jiān)聽,無需接收數(shù)據(jù)時通過off接口解除監(jiān)聽。
    1. 配置UIAbility的啟動模式。 配置module.json5,將CalleeAbility配置為單實例"singleton"。

      Json字段字段說明
      “l(fā)aunchType”Ability的啟動模式,設(shè)置為"singleton"類型。

      UIAbility配置標(biāo)簽示例如下:

      "abilities":[{
          "name": ".CalleeAbility",
          "srcEntry": "./ets/CalleeAbility/CalleeAbility.ts",
          "launchType": "singleton",
          "description": "$string:CalleeAbility_desc",
          "icon": "$media:icon",
          "label": "$string:CalleeAbility_label",
          "exported": true
      }]
      
    2. 導(dǎo)入UIAbility模塊。

      import UIAbility from '@ohos.app.ability.UIAbility';
      
    3. 定義約定的序列化數(shù)據(jù)。 調(diào)用端及被調(diào)用端發(fā)送接收的數(shù)據(jù)格式需協(xié)商一致,如下示例約定數(shù)據(jù)由number和string組成。

      import type rpc from '@ohos.rpc';
      class MyParcelable {
        num: number = 0;
        str: string = '';
      
        constructor(num: number, string: string) {
          this.num = num;
          this.str = string;
        }
      
        mySequenceable(num: number, string: string): void {
          this.num = num;
          this.str = string;
        }
      
        marshalling(messageSequence: rpc.MessageSequence): boolean {
          messageSequence.writeInt(this.num);
          messageSequence.writeString(this.str);
          return true;
        };
      
        unmarshalling(messageSequence: rpc.MessageSequence): boolean {
          this.num = messageSequence.readInt();
          this.str = messageSequence.readString();
          return true;
        };
      };
      
    4. 實現(xiàn)Callee.on監(jiān)聽及Callee.off解除監(jiān)聽。 如下示例在Ability的onCreate注冊MSG_SEND_METHOD監(jiān)聽,在onDestroy取消監(jiān)聽,收到序列化數(shù)據(jù)后作相應(yīng)處理并返回。應(yīng)用開發(fā)者根據(jù)實際業(yè)務(wù)需要做相應(yīng)處理。

      import type AbilityConstant from '@ohos.app.ability.AbilityConstant';
      import UIAbility from '@ohos.app.ability.UIAbility';
      import type Want from '@ohos.app.ability.Want';
      import type { Caller } from '@ohos.app.ability.UIAbility';
      import hilog from '@ohos.hilog';
      import type rpc from '@ohos.rpc';
      
      
      const TAG: string = '[CalleeAbility]';
      const MSG_SEND_METHOD: string = 'CallSendMsg';
      const DOMAIN_NUMBER: number = 0xFF00;
      
      class MyParcelable {
        num: number = 0;
        str: string = '';
      
        constructor(num: number, string: string) {
          this.num = num;
          this.str = string;
        };
      
        mySequenceable(num: number, string: string): void {
          this.num = num;
          this.str = string;
        };
      
        marshalling(messageSequence: rpc.MessageSequence): boolean {
          messageSequence.writeInt(this.num);
          messageSequence.writeString(this.str);
          return true;
        };
      
        unmarshalling(messageSequence: rpc.MessageSequence): boolean {
          this.num = messageSequence.readInt();
          this.str = messageSequence.readString();
          return true;
        };
      };
      
      function sendMsgCallback(data: rpc.MessageSequence): rpc.Parcelable {
        hilog.info(DOMAIN_NUMBER, TAG, '%{public}s', 'CalleeSortFunc called');
      
        // 獲取Caller發(fā)送的序列化數(shù)據(jù)
        let receivedData: MyParcelable = new MyParcelable(0, '');
        data.readParcelable(receivedData);
        hilog.info(DOMAIN_NUMBER, TAG, '%{public}s', `receiveData[${receivedData.num}, ${receivedData.str}]`);
        let num: number = receivedData.num;
      
        // 作相應(yīng)處理
        // 返回序列化數(shù)據(jù)result給Caller
        return new MyParcelable(num + 1, `send ${receivedData.str} succeed`) as rpc.Parcelable;
      };
      
      export default class CalleeAbility extends UIAbility {
        caller: Caller | undefined;
      
        onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
          try {
            this.callee.on(MSG_SEND_METHOD, sendMsgCallback);
          } catch (error) {
            hilog.error(DOMAIN_NUMBER, TAG, '%{public}s', `Failed to register. Error is ${error}`);
          };
        };
        //...
        releaseCall(): void {
          try {
            if (this.caller) {
              this.caller.release();
              this.caller = undefined;
            }
            hilog.info(DOMAIN_NUMBER, TAG, 'caller release succeed');
          } catch (error) {
            hilog.info(DOMAIN_NUMBER, TAG, `caller release failed with ${error}`);
          };
        };
        //...
        onDestroy(): void {
          try {
            this.callee.off(MSG_SEND_METHOD);
            hilog.info(DOMAIN_NUMBER, TAG, '%{public}s', 'Callee OnDestroy');
            this.releaseCall();
          } catch (error) {
            hilog.error(DOMAIN_NUMBER, TAG, '%{public}s', `Failed to register. Error is ${error}`);
          };
        };
      };
      
  4. 獲取Caller接口,訪問被調(diào)用端UIAbility。
    1. 導(dǎo)入UIAbility模塊。

      import UIAbility from '@ohos.app.ability.UIAbility';
      
    2. 獲取Caller通信接口。 Ability的context屬性實現(xiàn)了startAbilityByCall方法,用于獲取指定通用組件的Caller通信接口。如下示例通過this.context獲取Ability實例的context屬性,使用startAbilityByCall拉起Callee被調(diào)用端并獲取Caller通信接口,注冊Caller的onRelease和onRemoteStateChange監(jiān)聽。應(yīng)用開發(fā)者根據(jù)實際業(yè)務(wù)需要做相應(yīng)處理。

      import { BusinessError } from '@ohos.base';
      import { Caller } from '@ohos.app.ability.UIAbility';
      import common from '@ohos.app.ability.common';
      import hilog from '@ohos.hilog';
      import distributedDeviceManager from '@ohos.distributedDeviceManager';
      
      const TAG: string = '[Page_CollaborateAbility]';
      const DOMAIN_NUMBER: number = 0xFF00;
      let caller: Caller | undefined;
      let dmClass: distributedDeviceManager.DeviceManager;
      
      function getRemoteDeviceId(): string | undefined {
        if (typeof dmClass === 'object' && dmClass !== null) {
          let list = dmClass.getAvailableDeviceListSync();
          hilog.info(DOMAIN_NUMBER, TAG, JSON.stringify(dmClass), JSON.stringify(list));
          if (typeof (list) === 'undefined' || typeof (list.length) === 'undefined') {
            hilog.info(DOMAIN_NUMBER, TAG, 'getRemoteDeviceId err: list is null');
            return;
          }
          if (list.length === 0) {
            hilog.info(DOMAIN_NUMBER, TAG, `getRemoteDeviceId err: list is empty`);
            return;
          }
          return list[0].networkId;
        } else {
          hilog.info(DOMAIN_NUMBER, TAG, 'getRemoteDeviceId err: dmClass is null');
          return;
        }
      };
      
      @Entry
      @Component
      struct Page_CollaborateAbility {
        private context = getContext(this) as common.UIAbilityContext;
      
        build() {
          Column() {
            //...
            List({ initialIndex: 0 }) {
              //...
              ListItem() {
                Row() {
                  //...
                }
                .onClick(() = > {
                  let caller: Caller | undefined;
                  let context = this.context;
      
                  context.startAbilityByCall({
                    deviceId: getRemoteDeviceId(),
                    bundleName: 'com.samples.stagemodelabilityinteraction',
                    abilityName: 'CalleeAbility'
                  }).then((data) = > {
                    if (data !== null) {
                      caller = data;
                      hilog.info(DOMAIN_NUMBER, TAG, 'get remote caller success');
                      // 注冊caller的release監(jiān)聽
                      caller.onRelease((msg) = > {
                        hilog.info(DOMAIN_NUMBER, TAG, `remote caller onRelease is called ${msg}`);
                      });
                      hilog.info(DOMAIN_NUMBER, TAG, 'remote caller register OnRelease succeed');
                      promptAction.showToast({
                        message: $r('app.string.CallerSuccess')
                      });
                      // 注冊caller的協(xié)同場景下跨設(shè)備組件狀態(tài)變化監(jiān)聽通知
                      try {
                        caller.onRemoteStateChange((str) = > {
                          hilog.info(DOMAIN_NUMBER, TAG, 'Remote state changed ' + str);
                        });
                      } catch (error) {
                        hilog.info(DOMAIN_NUMBER, TAG, `Caller.onRemoteStateChange catch error, error.code: ${JSON.stringify(error.code)}, error.message: ${JSON.stringify(error.message)}`);
                      };
                    }
                  }).catch((error: BusinessError) = > {
                    hilog.error(DOMAIN_NUMBER, TAG, `get remote caller failed with ${error}`);
                  });
                })
              }
              //...
            }
            //...
          }
          //...
        }
      }
      

      getRemoteDeviceId方法參照[通過跨設(shè)備啟動uiability和serviceextensionability組件實現(xiàn)多端協(xié)同無返回數(shù)據(jù)]。

  5. 向被調(diào)用端UIAbility發(fā)送約定序列化數(shù)據(jù)。
    1. 向被調(diào)用端發(fā)送Parcelable數(shù)據(jù)有兩種方式,一種是不帶返回值,一種是獲取被調(diào)用端返回的數(shù)據(jù),method以及序列化數(shù)據(jù)需要與被調(diào)用端協(xié)商一致。如下示例調(diào)用Call接口,向Callee被調(diào)用端發(fā)送數(shù)據(jù)。

      import UIAbility from '@ohos.app.ability.UIAbility';
      import type { Caller } from '@ohos.app.ability.UIAbility';
      import type rpc from '@ohos.rpc';
      import hilog from '@ohos.hilog';
      
      const TAG: string = '[CalleeAbility]';
      const DOMAIN_NUMBER: number = 0xFF00;
      const MSG_SEND_METHOD: string = 'CallSendMsg';
      class MyParcelable {
        num: number = 0;
        str: string = '';
      
        constructor(num: number, string: string) {
          this.num = num;
          this.str = string;
        };
      
        mySequenceable(num: number, string: string): void {
          this.num = num;
          this.str = string;
        };
      
        marshalling(messageSequence: rpc.MessageSequence): boolean {
          messageSequence.writeInt(this.num);
          messageSequence.writeString(this.str);
          return true;
        };
      
        unmarshalling(messageSequence: rpc.MessageSequence): boolean {
          this.num = messageSequence.readInt();
          this.str = messageSequence.readString();
          return true;
        };
      };
      
      export default class EntryAbility extends UIAbility {
        // ...
        caller: Caller | undefined;
        async onButtonCall(): Promise< void > {
          try {
            let msg: MyParcelable = new MyParcelable(1, 'origin_Msg');
            if (this.caller) {
              await this.caller.call(MSG_SEND_METHOD, msg);
            }
          } catch (error) {
            hilog.info(DOMAIN_NUMBER, TAG, `caller call failed with ${error}`);
          };
        }
        // ...
      }
      
    2. 如下示例調(diào)用CallWithResult接口,向Callee被調(diào)用端發(fā)送待處理的數(shù)據(jù)originMsg,并將’CallSendMsg’方法處理完畢的數(shù)據(jù)賦值給backMsg。

      import UIAbility from '@ohos.app.ability.UIAbility';
      import type { Caller } from '@ohos.app.ability.UIAbility';
      import type rpc from '@ohos.rpc';
      import hilog from '@ohos.hilog';
      
      const TAG: string = '[CalleeAbility]';
      const DOMAIN_NUMBER: number = 0xFF00;
      
      const MSG_SEND_METHOD: string = 'CallSendMsg';
      let originMsg: string = '';
      let backMsg: string = '';
      
      class MyParcelable {
        num: number = 0;
        str: string = '';
      
        constructor(num: number, string: string) {
          this.num = num;
          this.str = string;
        };
      
        mySequenceable(num: number, string: string): void {
          this.num = num;
          this.str = string;
        };
      
        marshalling(messageSequence: rpc.MessageSequence): boolean {
          messageSequence.writeInt(this.num);
          messageSequence.writeString(this.str);
          return true;
        };
      
        unmarshalling(messageSequence: rpc.MessageSequence): boolean {
          this.num = messageSequence.readInt();
          this.str = messageSequence.readString();
          return true;
        };
      };
      
      export default class EntryAbility extends UIAbility {
        // ...
        caller: Caller | undefined;
        async onButtonCallWithResult(originMsg: string, backMsg: string): Promise< void > {
          try {
            let msg: MyParcelable = new MyParcelable(1, originMsg);
            if (this.caller) {
              const data = await this.caller.callWithResult(MSG_SEND_METHOD, msg);
              hilog.info(DOMAIN_NUMBER, TAG, 'caller callWithResult succeed');
              let result: MyParcelable = new MyParcelable(0, '');
              data.readParcelable(result);
              backMsg = result.str;
              hilog.info(DOMAIN_NUMBER, TAG, `caller result is [${result.num}, ${result.str}]`);
            }
          } catch (error) {
            hilog.info(DOMAIN_NUMBER, TAG, `caller callWithResult failed with ${error}`);
          };
        }
        // ...
      }
      
      `HarmonyOSOpenHarmony鴻蒙文檔籽料:mau123789是v直接拿`
      

      搜狗高速瀏覽器截圖20240326151450.png

  6. 釋放Caller通信接口。 Caller不再使用后,應(yīng)用開發(fā)者可以通過release接口釋放Caller。
    import UIAbility from '@ohos.app.ability.UIAbility';
    import type { Caller } from '@ohos.app.ability.UIAbility';
    import hilog from '@ohos.hilog';
    
    export default class EntryAbility extends UIAbility {
      caller: Caller | undefined;
      releaseCall(): void {
        try {
          if (this.caller) {
            this.caller.release();
            this.caller = undefined;
          }
          hilog.info(DOMAIN_NUMBER, TAG, 'caller release succeed');
        } catch (error) {
          hilog.info(DOMAIN_NUMBER, TAG, `caller release failed with ${error}`);
        };
      }
    }
    

審核編輯 黃宇

聲明:本文內(nèi)容及配圖由入駐作者撰寫或者入駐合作網(wǎng)站授權(quán)轉(zhuǎn)載。文章觀點僅代表作者本人,不代表電子發(fā)燒友網(wǎng)立場。文章及其配圖僅供工程師學(xué)習(xí)之用,如有內(nèi)容侵權(quán)或者其他違規(guī)問題,請聯(lián)系本站處理。 舉報投訴
  • 組件
    +關(guān)注

    關(guān)注

    1

    文章

    498

    瀏覽量

    17771
  • 鴻蒙
    +關(guān)注

    關(guān)注

    57

    文章

    2287

    瀏覽量

    42629
收藏 人收藏

    評論

    相關(guān)推薦

    鴻蒙千帆起】《開心消消樂》完成鴻蒙原生應(yīng)用開發(fā),創(chuàng)新多端聯(lián)動用戶體驗

    更優(yōu)質(zhì)的用戶體驗,基于強大的 AI 能力還可以實現(xiàn)精準(zhǔn)識別、精確投放,為伙伴提供新流量,并共享全場景服務(wù)分發(fā),助力生態(tài)共贏。數(shù)據(jù)顯示,截至今年 8 月份,鴻蒙生態(tài)的設(shè)備數(shù)已超過 7 億,鴻蒙生態(tài)
    發(fā)表于 01-03 10:22

    HarmonyOS NEXT Developer Beta1最新術(shù)語表

    郵件設(shè)備編輯、多設(shè)備協(xié)同健身、多屏游戲等分布式業(yè)務(wù)。 流轉(zhuǎn)開發(fā)者提供更廣的使用場景和更新的產(chǎn)
    發(fā)表于 06-27 16:16

    為滿足全場景智慧體驗 鴻蒙系統(tǒng)一四大技術(shù)特性!

    ,分布式架構(gòu)Kit提供屏幕布局控件以及交互的自動適配,支持控件拖拽,面向預(yù)覽的可視化編程,從而使開發(fā)者可以基于同一工程高效構(gòu)建多端自動運行App,實現(xiàn)真正的一次開發(fā),
    發(fā)表于 09-03 21:09

    HarmonyOS學(xué)習(xí)之八:鴻蒙系統(tǒng)從1.0到2.0的提升

    協(xié)同優(yōu)化,將搭載于音箱、耳機等產(chǎn)品。今年,HarmonyOS2.0 進行了全新定義,即全面使能全場景生態(tài),包括設(shè)備、服務(wù)流轉(zhuǎn)、極速直達(dá)、語音視頻、隱私安全五個方面能力。 通過
    發(fā)表于 11-27 09:40

    華為鴻蒙系統(tǒng) HarmonyOS 2.0 京東、美團、優(yōu)酷等軟件,分布式設(shè)備交互演示及簡單說明

    華為鴻蒙系統(tǒng) HarmonyOS 2.0京東、美團、優(yōu)酷等軟件,分布式設(shè)備交互演示及簡單說明
    發(fā)表于 01-14 09:52

    HarmonyOS應(yīng)用框架如何解決多設(shè)備交互問題?

    應(yīng)用框架中提供了兩種基礎(chǔ)功能,分別是:多端協(xié)同端遷移。接下來我們將介紹HarmonyOS的上述兩個能力是如何解決多設(shè)備交互問題的。二、H
    發(fā)表于 08-12 11:03

    HarmonyOS原子化服務(wù)卡片開發(fā)-分布式體驗學(xué)習(xí)

    1.原子化服務(wù)流轉(zhuǎn)在HarmonyOS中泛指涉及多端的分布式操作。流轉(zhuǎn)能力打破設(shè)備界限,多設(shè)備聯(lián)動,使用戶應(yīng)用程序可分可合、可
    發(fā)表于 09-07 09:38

    HDC2021技術(shù)分論壇:廣發(fā)證券攜手HarmonyOS打造智慧金融服務(wù)

    ,點擊確認(rèn)后,即可將手機中自選股票同步流轉(zhuǎn)至手表,整體流程便捷流暢。圖4 協(xié)同豐富的服務(wù)卡片和終端的互聯(lián)互通,組成一個虛擬的超級終端,將人、設(shè)
    發(fā)表于 11-23 17:02

    新能力讓數(shù)據(jù)多端協(xié)同更便捷,數(shù)據(jù)端遷移更高效!

    文檔演示場景中,文檔演示狀態(tài)(比如翻頁、頁面放大、頁面縮小和涂鴉等)其實就是變量。如果這些變量支持“全局”訪問,那么開發(fā)設(shè)備訪問這些變量就能像操作本地變量一樣,數(shù)據(jù)就能夠自動高效、便捷地
    發(fā)表于 01-11 10:41

    DevEco Studio 2.1平臺設(shè)備交互使用示例

    DevEco Studio 2.1新增開發(fā)者使用的平臺設(shè)備交互界面(超級用戶可以使用該設(shè)備
    發(fā)表于 03-31 15:02

    OpenHarmony應(yīng)用模型的構(gòu)成要素與Stage優(yōu)勢

    ,便捷實現(xiàn)端遷移。 在多端協(xié)同場景下,應(yīng)用組件具備組件間通信的RPC調(diào)用能力,天然支持
    發(fā)表于 09-26 16:48

    華為發(fā)布HarmonyOS2,全新桌面支持應(yīng)用設(shè)備流轉(zhuǎn)

    華為正式發(fā)布了HarmonyOS 2,鴻蒙系統(tǒng)新亮點先知道,不僅有全新桌面,還支持應(yīng)用設(shè)備流轉(zhuǎn),新奇玩法你值得期待。
    的頭像 發(fā)表于 06-02 20:43 ?1.1w次閱讀

    鴻蒙ArkUI開發(fā)-Tabs組件的使用

    鴻蒙ArkUI開發(fā)-Tabs組件的使用
    的頭像 發(fā)表于 01-19 16:01 ?1577次閱讀
    <b class='flag-5'>鴻蒙</b>ArkUI<b class='flag-5'>開發(fā)</b>-Tabs<b class='flag-5'>組件</b>的使用

    鴻蒙開發(fā):應(yīng)用組件設(shè)備交互流轉(zhuǎn))【概述】

    隨著全場景多設(shè)備的生活方式不斷深入,用戶擁有的設(shè)備越來越多,不同設(shè)備都能在適合的場景下提供良好的體驗,例如手表可以提供及時的信息查看能力,電視可以帶來沉浸的觀影體驗。但是,每個設(shè)備也有
    的頭像 發(fā)表于 06-11 15:40 ?550次閱讀
    <b class='flag-5'>鴻蒙</b><b class='flag-5'>開發(fā)</b>:應(yīng)用<b class='flag-5'>組件</b><b class='flag-5'>跨</b><b class='flag-5'>設(shè)備</b><b class='flag-5'>交互</b>(<b class='flag-5'>流轉(zhuǎn)</b>)【概述】

    鴻蒙開發(fā):應(yīng)用組件設(shè)備交互流轉(zhuǎn))【端遷移】

    端遷移的核心任務(wù)是將應(yīng)用的當(dāng)前狀態(tài)(包括頁面控件、狀態(tài)變量等)無縫遷移到另一設(shè)備,從而在新設(shè)備上無縫接續(xù)應(yīng)用體驗。這意味著用戶在一臺設(shè)備上進行的操作可以在另一臺
    的頭像 發(fā)表于 06-11 17:10 ?945次閱讀
    <b class='flag-5'>鴻蒙</b><b class='flag-5'>開發(fā)</b>:應(yīng)用<b class='flag-5'>組件</b><b class='flag-5'>跨</b><b class='flag-5'>設(shè)備</b><b class='flag-5'>交互</b>(<b class='flag-5'>流轉(zhuǎn)</b>)【<b class='flag-5'>跨</b>端遷移】