鸿蒙权限请求太麻烦?用装饰器装修一下你的代码

1,478 阅读3分钟

首先我们来看一下一个正常的权限请求流程

sequenceDiagram
  participant User
  participant App
  participant PermissionSystem

  User->>App: 打开 APP
  App->>User: 展示 APP 页面
  
  User->>App: 发起权限请求
  App->>PermissionSystem: APP 向系统请求
  PermissionSystem-->>App: 提供权限请求对话框
  
  alt 已授权
    App->>User: 展示相应功能
  else 已拒绝
    App->>User: 展示拒绝消息
  end

通常来说请求权限附带很多重复的模版代码,这个无疑会给我们带来很多不必要的工作。我们来看一个官方的示例(当前不感兴趣的可以直接跳过去)。

//检查权限
async function checkAccessToken(permission: Permissions): Promise<abilityAccessCtrl.GrantStatus> {
  let atManager = abilityAccessCtrl.createAtManager();
  let grantStatus: abilityAccessCtrl.GrantStatus;
   //.... 此处省略 x 字
  // 校验应用是否被授予权限
  try {
    grantStatus = await atManager.checkAccessToken(tokenId, permission);
  } catch (err) {
    console.error(`checkAccessToken failed, code is ${err.code}, message is ${err.message}`);
  }

  return grantStatus;
}
// 检查回调
async function checkPermissions(): Promise<void> {
  const permissions: Array<Permissions> = ['ohos.permission.READ_CALENDAR'];
  let grantStatus: abilityAccessCtrl.GrantStatus = await checkAccessToken(permissions[0]);
  if (grantStatus === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED) {
    // 已经授权,可以继续访问目标操作
  } else {
    // 申请日历权限
  }
}


// 请求动态权限
reqPermissionsFromUser(permissions: Array<Permissions>): void {
    let context = getContext(this) as common.UIAbilityContext;
    let atManager = abilityAccessCtrl.createAtManager();
    // requestPermissionsFromUser会判断权限的授权状态来决定是否唤起弹窗
    atManager.requestPermissionsFromUser(context, permissions).then((data) => {
      let grantStatus: Array<number> = data.authResults;
      let length: number = grantStatus.length;
      for (let i = 0; i < length; i++) {
        if (grantStatus[i] === 0) {
          // 用户授权,可以继续访问目标操作
        } else {
          // 用户拒绝授权,提示用户必须授权才能访问当前页面的功能,并引导用户到系统设置中打开相应的权限
          return;
        }
      }
      // 授权成功
    }).catch((err) => {
      console.error(`requestPermissionsFromUser failed, code is ${err.code}, message is ${err.message}`);
    })

上面的示例是不是又长又臭,纯纯的模板代码,有没有一种简单的方式来简化请求流程呢?答案当然是有啦,下面我就演示一下如何用TS 的装饰器来优化权限处理的,在此之前先来设计一下权限处理流程。

sequenceDiagram
  participant User
  participant App
  participant PermissionSystem

  User->>App: 打开应用
  App->>User: 显示应用界面
  
  User->>App: 触发方法调用
  App->>PermissionSystem: 检查权限状态
  PermissionSystem-->>App: 返回权限状态
  
  alt 用户有权限
    App->>PermissionSystem: 执行方法
    PermissionSystem-->>App: 返回方法执行结果
    App->>User: 显示方法执行结果
  else 用户无权限
    App->>PermissionSystem: 请求权限
    PermissionSystem-->>App: 返回权限请求结果
  
    alt 授权成功
      App->>PermissionSystem: 执行方法
      PermissionSystem-->>App: 返回方法执行结果
      App->>User: 显示方法执行结果
    else 授权失败
      App->>User: 显示权限请求失败提示
    end
  end

首先对外暴露的接口要最小化,这样才能让使用方不迷糊,实现 高内聚 低耦合的设计目的。我们对外暴露一个装饰器,使用方只需要用装饰器来描述需要的权限即可,是不是so easy,so cute.

@Permission( [...permissions])

第二个就是如何设计这个装饰器,来实现完整的权限处理流程。 首先这个装饰器是我们整个权限的入口,其次我们使用装饰器主要是为了解决模板代码的问题,那么我们可以把模板代码封装在一个方法,最后授权成功后调用原方法。在这里我们可以利用装饰器可以有他自身调用处的签名的特性来处理,通俗意义上的来讲,就是装饰器可以持有当前方法的对象。下面我们废话不多说上代码:

export function Permission(permissions: Permissions[]) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    // 获取调用处方法
    const originalMethod = descriptor.value;
    // 重新装饰当前方法
    descriptor.value = function (...args: any[]) {
      // 检查权限
      checkPermission(permissions)
        .then((grantStatusList: abilityAccessCtrl.GrantStatus[]) => {
          console.log('权限校验结果:', grantStatusList);
          const allPermissionsGranted = grantStatusList.every(status => status === abilityAccessCtrl.GrantStatus.PERMISSION_GRANTED);
          // 已经有授权
          if (allPermissionsGranted) {
            return originalMethod.apply(this, args);
          }
          // 如果没有权限,请求权限
          requestPermission(permissions)
            .then(() => {
              // 获取权限成功后重新调用方法
              return originalMethod.apply(this, args);
            })
            .catch(() => {
              // 获取权限失败,执行用户注入的处理函数或默认处理逻辑
              const errorHandler = target.permissionErrorHandler || defaultPermissionErrorHandler;
              errorHandler();
            });
        })
        .catch((error)=>{
          const errorHandler = target.permissionErrorHandler || defaultPermissionErrorHandler;
          errorHandler();
        })

    };

    return descriptor;
  };
}

具体处理逻辑在上面注释中有详细的说明。下面我们来展示一下工作成果:

@Component
struct Index {
  @State message: string = 'Hello World'

  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
          .onClick( (e)=>{
            this.permissionText()
          })
      }
      .width('100%')
    }
    .height('100%')
  }
  @Permission( ['ohos.permission.LOCATION', 'ohos.permission.APPROXIMATELY_LOCATION'])
  permissionText(){
    promptAction.showToast({message:"有权限啦"})
  }

  permissionErrorHandler(){
    promptAction.showToast({message:"请求权限失败"})
  }
}

是不是 so easy 。 下面附上代码传送地址,如果有需求欢迎提 pr,BTW 有 harmony next ide 可以分享一下Thanks♪(・ω・)ノ。今日愉快:)

后记,装饰器是面向切面编程(AOP)的一种方式,你可以用装饰器来实现一个方法的限流吗?有兴趣的可以尝试一下,当然我在仓库里面也写了一个示例 感兴趣的话可以看一下。