0

    小程序实践多角色下的菜单权限管理

    2023.07.14 | admin | 127次围观

    写在前面

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

    本文将从原生小程序实现菜单、按钮权限管理,介绍一种较简便、合理的技术实现方案,对于使用了其他开发框架的小程序来说,实现原理也是大同小异的。

    前端管理菜单展示很简单?

    这里先抛出一个问题,前端管理菜单展示很简单?答案确实是简单,甚至前端控制 DOM 有天然的原生 API 支持微信小程序应用场景,特别是在 WEB 端,控制 DOM 几乎是可以随心所欲的事情。

    先来撸一条最简单、直接的实现路径:

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

    单从上面的路径来看,前端的处理就是在页面上请求接口,然后根据接口结果控制菜单展示,似乎 10 行左右代码就可以搞定了。

    然尔,这是没有从项目整体架构做思考的结果,简单的问题在大量重复的场景下,也会变成麻烦的问题。比如当页面存在几百个时,上述简单的处理就会出现大量重复的代码,并且违背了软件设计的单一职责原则,不利扩展,也不方便使用。

    由此带来了几个关于小程序实现菜单控制的现实思考:

    如何提高代码复用性,将菜单处理逻辑尽可能地从页面中抽离出去?如何高效地将接口菜单数据匹配实际的 DOM 菜单,通过唯一标识,还是父子菜单层叠式的标识来匹配?在大量的视图层的调用上,如何简便操作?应用场景分析

    功能实现之前先来看看背后的制约条件、应用场景,以下展示菜单的组织形式、数据结构,以及小程序应用场景。

    菜单组织形式

    菜单数据结构

    [
      {
        "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
              }
            ]
          }
        ]
      }
    ]
    复制代码

    应用场景

    可以从组织形式看出来,菜单有类型,支持菜单和按钮,并且支持配置标识、路由;数据结构上是树形结构,且层级不定,需要递归查找指定的数据。

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

    解决问题

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

    代码复用

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

    菜单匹配

    这看起来似乎不是件难事,通过数据里的权限标识去匹配就好,然而这仍然是很繁琐、不利于维护的。给数千以上的元素取唯一标识,光想想就知道不太靠谱了,其次是通过父标识加子标识的方式匹配,比如home:btn,笔者见过上十个标识的叠加匹配,这是件很繁琐、容易出错的事情。

    那么要如何处理菜单匹配呢?答案是通过 路由地址+权限标识 匹配,由于路由地址一般是唯一的(共用页面可以加参区分),用于查找当前菜单是比较方便的,而当前菜单的子菜单、子按钮,再通过标识去匹配就好。

    操作简便

    由于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万现金大奖」

    版权声明

    本文仅代表作者观点。
    本文系作者授权发表,未经许可,不得转载。

    标签: 小程序前端
    发表评论