不定日拱卒-优雅地实现列权限

679 阅读5分钟

不定日拱卒:分享日常开发过程中的一些小技巧,为更多人提供类似问题的解决方案

背景

  • 表格是中后台系统最常见的数据呈现方式
  • 一般中后台系统都会有权限管控的需求,包括功能权限和数据权限
  • 对表格来说,数据权限包括行权限和列权限
    • 行权限:是否对单条数据有可见权限,例如:一个人只能看到自己创建的数据而看不到其他人的数据。行权限基本上是由服务端控制
    • 列权限:是否对某些特定的字段有可见权限,例如:普通用户看不到「成本价」这个字段,而管理员可以看到,尽管他们都能看到同样的 10 个商品。通常情况下,列权限需要前后端配合实现

遇到的问题

假设我们的角色是通过 role 存放在 window 对象中的

在无权限控制需求时,我们实现了一个表格页面如下

import React from 'react';
import { Button, Table as AntdTable } from 'antd';

// 模拟数据
const dataSource = new Array(8).fill(null).map((item, index) => ({
  id: index,
  columnA: `a${index}`,
  columnB: `b${index}`,
}));

IProps {}

const Table: React.FC<IProps> = () => {
  const columns = [
    {
      dataIndex: 'id',
    },
    {
      dataIndex: 'columnA',
    },
    {
      dataIndex: 'columnB',
    },
    {
      dataIndex: 'actions',
      render: () => (
        <React.Fragment>
          <Button>act1</Button>
          <Button>act2</Button>
        </React.Fragment>
      ),
    },
  ];
  return <AntdTable dataSource={dataSource} columns={columns} />;
};

export default Table;

某一天,PM 小姐姐告诉我们,这里的数据太敏感了,需要做权限区分:只有角色为 admin 的用户才能看到 columnB 这一列。

我们按着原来的代码,啃哧啃哧几下就搞定了,只修改了 columns 的实现

  const columns = [
    {
      dataIndex: 'id',
    },
    {
      dataIndex: 'columnA'
    }
  ];

  if (window.role === 'admin') {
    columns.push({ dataIndex: 'columnB' });
  }
  
  columns.push({
    dataIndex: 'actions',
    render: () => (
      <React.Fragment>
        <Button>act1</Button>
        <Button>act2</Button>
      </React.Fragment>
    ),
  });

日子安稳了没几天,PM 小姐姐又找上门来了,说 columnA columnB 的数据都有一定的敏感度,我们的系统三个角色包括 adminusersuper,其中

  • admin 可以看到所有列
  • user 可以看到 columnA,不能看 columnB
  • super 可以看到 columnB,不能看 columnA

没办法,我们只能再对上面的实现做些修改

  const columns = [
    {
      dataIndex: 'id',
    },
    {
      dataIndex: 'columnA'
    }
  ];

  if (window.role === 'user' || window.role === 'admin') {
    columns.push({ dataIndex: 'columnA' });
  }

  if (window.role === 'super' || window.role === 'admin') {
    columns.push({ dataIndex: 'columnB' });
  }
  
  columns.push({
    dataIndex: 'actions',
    render: () => (
      <React.Fragment>
        <Button>act1</Button>
        <Button>act2</Button>
      </React.Fragment>
    ),
  });

一顿操作下来,代码已经变得十分难看,最重要的是很难维护了,如果以后又多了一些角色,新的需求必然导致代码中充斥着难以理解的判断逻辑,我们必须找到合适的方案去处理掉这个问题

解决方案

我们不妨来思考一下刚刚的实现方案问题在哪里

  • 本来是一个表单页面的组件,却因为权限控制的需求,杂糅了「非 UI 实现」的逻辑,恰恰违背了「单一职责原则(SRP)」
  • 未来的需求,有可能是调整权限,也有可能是修改 UI 实现,都需要在同一个地方去处理,很容易产生意料之外的问题,这违反了「关注点分离(SoC)」 我们要找到一种方式,让「权限控制」和「UI 实现」解耦

没有什么是分层解决不了的

我们通过组件,把这个页面分成两层,分别实现权限控制和 UI 逻辑

对于 UI 逻辑部分,我们不需要关注权限,所以在这一层,我们先默认用户拥有所有权限,把所有 UI 逻辑部分都实现完整,再通过一个属性,来最终过滤出需要展示的列

interface IProps {
  displayColumns?: string[];
}

const Table: React.FC<IProps> = ({ displayColumns }) => {
  // 定义所有的列
  const columns = [
    {
      dataIndex: 'id',
    },
    {
      dataIndex: 'columnA',
    },
    {
      dataIndex: 'columnB',
    },
    {
      dataIndex: 'actions',
      render: () => (
        <React.Fragment>
          <Button>act1</Button>
          <Button>act2</Button>
        </React.Fragment>
      ),
    },
  ];

  const finalColumns = displayColumns
    ? columns.filter(item => displayColumns.join(',').includes(item.dataIndex))
    : columns;

  return <AntdTable dataSource={dataSource} columns={finalColumns} />;
};

在权限控制部分,我们根据角色判断需要展示的列之后,通过 props 传递给 UI 层

import Table from './Table';

declare global {
  interface Window {
    role: any;
  }
}

const getDisplayColumns = () => {
  if (window.role === 'user') {
    return ['id', 'columnA', 'actions'];
  }
  if (window.role === 'super') {
    return ['id', 'columnB', 'actions'];
  }
  if (window.role === 'admin') {
    return ['id', 'columnA', 'columnB', 'actions'];
  }
  return [];
};

const TablePage: React.FC = () => {
  return (
    <Table
      displayColumns={getDisplayColumns()}
    />
  );
};

export default TablePage;

我们再将上面的 getDisplayColumns 方法优化一下,毕竟过多的 if 条件会让代码显得很 low

const roleColumnsMap: any = {
  user: ['id', 'columnA', 'actions'],
  super: ['id', 'columnB', 'actions'],
  admin: ['id', 'columnA', 'columnB', 'actions'],
};

const getDisplayColumns = () => roleColumnsMap[window.role] || [];

分层,帮我们实现了「关注点分离」,降低了修改的「心智负担」

又过了几天,PM 小姐姐提出了新的需求,这次不是数据权限的问题,是操作权限的问题,即

  • admin 可以看到所有操作
  • user 可以看到 act1,不能看 act2
  • super 可以看到 act2,不能看 act1

同样的,我们完全可以按照上面的思路来处理

  • 权限层
import Table from './Table';

declare global {
  interface Window {
    role: any;
  }
}

const roleColumnsMap: any = {
  user: ['id', 'columnA', 'actions'],
  super: ['id', 'columnB', 'actions'],
  admin: ['id', 'columnA', 'columnB', 'actions'],
};

const roleActionsMap: any = {
  user: ['act1'],
  super: ['act2'],
  admin: ['act1', 'act2'],
};

window.role = 'admin';

const getDisplayColumns = () => roleColumnsMap[window.role] || [];

const getDisplayActions = () => roleActionsMap[window.role] || [];

const TablePage: React.FC = () => {
  return (
    <Table
      displayColumns={getDisplayColumns()}
      displayActions={getDisplayActions()}
    />
  );
};

export default TablePage;
  • UI 层
import React from 'react';
import { Button, Table as AntdTable } from 'antd';

interface IProps {
  displayColumns?: string[];
  displayActions?: string[];
}

// 模拟数据
const dataSource = new Array(8).fill(null).map((item, index) => ({
  id: index,
  columnA: `a${index}`,
  columnB: `b${index}`,
}));

const Table: React.FC<IProps> = ({ displayColumns, displayActions }) => {
  // 定义所有的操作
  const actions = [
    {
      key: 'act1',
      rc: <Button>act1</Button>,
    },
    {
      key: 'act2',
      rc: <Button>act2</Button>,
    },
  ];

  const finalActions = displayActions
    ? actions.filter(item => displayActions.join(',').includes(item.key))
    : actions;

  // 定义所有的列
  const columns = [
    {
      dataIndex: 'id',
    },
    {
      dataIndex: 'columnA',
    },
    {
      dataIndex: 'columnB',
    },
    {
      dataIndex: 'actions',
      render: () => finalActions.map(action => action.rc),
    },
  ];

  const finalColumns = displayColumns
    ? columns.filter(item => displayColumns.join(',').includes(item.dataIndex))
    : columns;

  return <AntdTable dataSource={dataSource} columns={finalColumns} />;
};

export default Table;

至此,任何的权限调整,都被限定在了与 UI 实现无关的地方。未来,如果服务端支持,还可以将每个角色所对应的权限通过 API 的方式获取,从而加大灵活程度,可以在不用改代码的情况下增加角色、调整权限。

有时候,我们可以适当转变一下思路,采用一些「模式」来优雅地解决问题