小程序实践多角色下的菜单权限管理
写在前面
一直以来,权限管理都是 PC 后台的重要底层功能,但随着移动互联网的深入普及、客户运营的精细化管理要求,移动端的管理平台也成为了商家进行日常经营活动不可或缺的一端。
本文将从原生小程序实现菜单、按钮权限管理,介绍一种较简便、合理的技术实现方案,对于使用了其他开发框架的小程序来说,实现原理也是大同小异的。
前端管理菜单展示很简单?
这里先抛出一个问题,前端管理菜单展示很简单?答案确实是简单,甚至前端控制 DOM 有天然的原生 API 支持,特别是在 WEB 端,控制 DOM 几乎是可以随心所欲的事情。
先来撸一条最简单、直接的实现路径:
单从上面的路径来看,前端的处理就是在页面上请求接口,然后根据接口结果控制菜单展示,似乎 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>
如上所示,视图层中极少量的代码即可实现权限控制,主功能也基本完成了,剩下的是一些特殊场景下的兼容,这里就不多赘述了。
如果要进一步实现路由权限(转发、小程序码进来的)、接口权限(前端前置拦截),也可以基于以上的方案稍加调整以实现。
后记
小程序实践多角色下的菜单权限管理技术方案之旅,到此就结束了,如果有更好的实现方式,望不吝赐教。
转载自:https://juejin.cn/post/7170340281940705310