写在前面
一直以来,权限办理都是 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万现金大奖」