写在前面

一直以来,权限办理都是 PC 后台的重要底层功用,但随着移动互联网的深化普及、客户运营的精细化办理要求,移动端的办理平台也成为了商家进行日常经营活动不可或缺的一端。

本文将从原生小程序完成菜单、按钮权限办理,介绍一种较简洁、合理的技术完成方案,关于运用了其他开发结构的小程序来说,完成原理也是大同小异的。

前端办理菜单展现很简略?

这儿先抛出一个问题,前端办理菜单展现很简略?答案确实是简略,甚至前端操控 DOM 有天然的原生 API 支撑,特别是在 WEB 端,操控 DOM 几乎是能够为所欲为的工作。

先来撸一条最简略、直接的完成途径:

graph LR
id3((菜单数据)) --> AJAX恳求 --> 匹配菜单 --> if/else展现

单从上面的途径来看,前端的处理就是在页面上恳求接口,然后依据接口成果操控菜单展现,好像 10 行左右代码就能够搞定了。

然尔,这是没有从项目全体架构做考虑的成果,简略的问题在很多重复的场景下,也会变成费事的问题。比如当页面存在几百个时,上述简略的处理就会呈现很多重复的代码,而且违反了软件设计的单一职责原则,不利扩展,也不方便运用。

由此带来了几个关于小程序完成菜单操控的现实考虑:

  1. 怎么提高代码复用性,将菜单处理逻辑尽可能地从页面中抽离出去?
  2. 怎么高效地将接口菜单数据匹配实践的 DOM 菜单,经过仅有标识,仍是父子菜单层叠式的标识来匹配?
  3. 在很多的视图层的调用上,怎么简洁操作?

运用场景剖析

功用完成之前先来看看背面的制约条件、运用场景,以下展现菜单的组织方法、数据结构,以及小程序运用场景。

菜单组织方法

小程序实践多人物下的菜单权限办理

菜单数据结构

[
  {
    "identifier": "home",
    "url": "/pages/tabs?selectedTabType=home",
    "name": "首页",
    "isMenu": 1,
    "childMenu": [
      {
        "identifier": "customer-pool",
        "url": "/pages/common/message-reminder-list/index",
        "name": "公共客户池",
        "isMenu": 1,
        "childMenu": [
          {
            "identifier": "more",
            "name": "查看更多",
            "isMenu": 0
          },
          {
            "identifier": "get-phone",
            "name": "获取电话",
            "isMenu": 0
          }
        ]
      }
    ]
  }
]

运用场景

小程序实践多人物下的菜单权限办理

能够从组织方法看出来,菜单有类型,支撑菜单和按钮,而且支撑装备标识、路由;数据结构上是树形结构,且层级不定,需求递归查找指定的数据。

而在小程序运用场景上,几乎没有章法可言,有的当地排列菜单、有点的当地放置按钮;有的当地是单个的方法,有点当地是一组的,这就需求从树形数据中抽象出规律来,以适配不规律的运用场景,简化视图操作。

解决问题

先来看看第二节留传的三个问题,并考虑解决方法。

  1. 代码复用

因为在小程序中操作 DOM 没有 WEB 端那么方便,例如能够像 Vue 一样把方法提取为指令、过滤器,来操控元素的展现。但在小程序中能够运用类似mixins的behaviors来提高代码复用。

  1. 菜单匹配

这看起来好像不是件难事,经过数据里的权限标识去匹配就好,然而这仍然是很繁琐、不利于保护的。给数千以上的元素取仅有标识,光想想就知道不太靠谱了,其次是经过父标识加子标识的方法匹配,比如home:btn,笔者见过上十个标识的叠加匹配,这是件很繁琐、容易犯错的工作。

那么要怎么处理菜单匹配呢?答案是经过 路由地址+权限标识 匹配,因为路由地址一般是仅有的(共用页面能够加参区别),用于查找当时菜单是比较方便的,而当时菜单的子菜单、子按钮,再经过标识去匹配就好。

  1. 操作简洁

因为wxml支撑的表达式有限,最好是在behaviors中把当时菜单设置为目标方法的data,然后wxml中直接类似运用wx:if="{{p['home']}}"wx:for="{{p['subButtonList']}}"方法。

示例代码

初始化恳求

/**
 * 小程序初始化时恳求菜单列表,而且把菜单拼装为目标方法,便于查找
 */
export const getMenuList = async () => {
  // 接口回来的菜单,恳求代码省略
  // ...
  const menuList = [];
  const menuSets = {};
  // 拼装成[path]:value方法
  const dfs = list => {
    for (const item of list) {
      if (!item.isMenu) continue;
      if (item.url) {
        menuSets[item.url] = { ...item };
      }
      item.childMenu?.length && dfs(item.childMenu);
    }
  };
  dfs(menuList);
  App.menuSets = menuSets;
};

permission.js(behaviors)

// 内部扩展方法,这儿也能够不运用
import wxApi from '../utils/wxApi';
/**
 * 经过原生菜单拼装页面所需的菜单权限组
 *
 * 判别规则:
 * 经过当时页面途径(或许途径传入:permissionPath)拼装数据
 *
 * 回来格局:
 * {
    // 菜单
    'customer-pool':{
      //...菜单信息
    },
    // 按钮
    'search':{
      //...按钮信息
    },
    // 子菜单数组
    subMenuList:[
      //...菜单信息
    ],
    // 子按钮数组
    subButtonList:[
      //...按钮信息
    ],
  }
 *
 */
module.exports = Behavior({
  data: {
    /**
     * 因为getCurrentPages的缺点,permission是异步设置的
     * 如果需求在js中较早地获取permission,可经过observers监听
     */
    p: {},
  },
  attached: function () {
    this.assembleMenu();
  },
  methods: {
    async assembleMenu() {
      const { permissionPath } = this.data;
      const currentPath = await wxApi.$getCurrentPageUrl(true);
      const $path = permissionPath || currentPath;
      const currentMenu = this.findMenu($path);
      if (!currentMenu) {
        return this.setData({
          p: {},
        });
      }
      const p = this.menuCombination(currentMenu);
      console.log('p', `${$path}n`, p);
      this.setData({
        p,
      });
    },
    // 查找当时菜单装备
    findMenu(path) {
      const hitKey = Object.keys(App.menuSets)
        .filter(key => path.includes(key))
        .reduce((a, b) => (a.length > b.length ? a : b), '');
      return App.menuSets[hitKey];
    },
    // 拼装数据
    menuCombination(menu) {
      const p = {
        name: menu.name,
        identifier: menu.identifier,
      };
      const dfs = (list, _p) => {
        for (const item of list) {
          const newItem = {
            name: item.name,
            identifier: item.identifier,
          };
          if (item.url) newItem.url = item.url;
          _p[item.identifier] = { ...newItem };
          if (item.isMenu) {
            _p.subMenuList = (_p.subMenuList || []).concat({ ...newItem });
          } else {
            _p.subButtonList = (_p.subButtonList || []).concat({ ...newItem });
          }
          if (item.childMenu?.length) dfs(item.childMenu, _p[item.identifier]);
        }
      };
      dfs(menu.childMenu || [], p);
      return p;
    },
    /**
     * 获取指定页面的菜单权限
     * 场景:需求跨页面获取权限
     * 运用示例:const p = this.getPermission('/pages/tabs?selectedTabType=analysis')
     */
    getPermission(path) {
      if (!path) return {};
      const menu = this.findMenu(path);
      if (!menu) return {};
      return this.menuCombination(menu);
    },
  },
});

运用方法(index.js、index.wxml)

Component({
  // behaviors 挂载到App中,不需求每次import
  behaviors: [App.behaviors.permission],
  data: {},
  methods: {},
});
<!-- 按钮 -->
<button wx:if="{{p['btn']}}">按钮</button>
<!-- 按钮组 -->
<button wx:for="{{p['subButtonList']}}">按钮</button>
<!-- 菜单 -->
<view wx:if="{{p['menu']}}">菜单</view>
<!-- 菜单组 -->
<view wx:for="{{p['subMenuList']}}">菜单</view>
<!-- 多层获取 -->
<button wx:if="{{p['menu']['btn']}}">按钮</button>
<view wx:for="{{p['menu']['subMenuList']}}">}}">菜单</view>

如上所示,视图层中极少数的代码即可完成权限操控,主功用也基本完成了,剩余的是一些特殊场景下的兼容,这儿就不多赘述了。

如果要进一步完成路由权限(转发、小程序码进来的)、接口权限(前端前置阻拦),也能够根据以上的方案稍加调整以完成。

后记

小程序实践多人物下的菜单权限办理技术方案之旅,到此就结束了,如果有更好的完成方法,望不吝赐教。

本文正在参加「金石计划 . 分割6万现金大奖」