执行 npm create umi
创建 antd-pro
项目
路由
整个 umi 建立的项目可以自定义路由,也可以约定式路由,我就直接对自定义路由展开
配置可以写在 config/config.ts
下或者写在根目录下 .umirc.ts
的routes中
{
path: '/',
component: '../layouts/BlankLayout',
routes: [
{
path: '/',
component: '../layouts/UserLayout',
Routes: ['src/pages/Authorized'],
authority: ['admin', 'user'], // 权限角色
routes: [
{
name: '/test',
icon: 'smile',
path: '/test',
component: './test',
},
{
name: '/test2',
authority: ['admin', 'user'], // 权限角色
routes: [
{
name: '',
icon: 'smile',
path: '/test2/step1'
}
]
}
],
}
}
侧边栏/导航栏
注意:我们暂时不对它内置路由到底是怎么实现的,但我想的其原理就是对react-router
进行配置吧
路由设置完之后对导航栏进行配置, 文档
另外umi文档中的插件 也是可以进行导航栏的废纸的
@ant-design/pro-layout
<ProLayout
// 这里的route是在上面配置自动获取的,不配置那么就不会显示在导航栏中
route={props.route}
>
</ProLayout>
权限
说一下antd-pro的权限, 我们可以看到 utils/authority.ts
, 作用是获取/设置localstorage
中的用户角色,然后通过路由中的 Route
跟authority
判断其是否有准入条件。
v4.1.0貌似有bug,不会经过pages/Authority.tsx
判断权限
- 先通过
pages/Authority.tsx
文件判断用户登录以及权限路由- 用户已登录, 获取当前
location.pathname
在路由表中的权限,一直向上找,跟localstorage
获取的权限判断,没权限 跳转到403
- 用户没登录,跳转登录页
- 然后再经过路由下的
Component
, 或者说是他的Children
- 用户已登录, 获取当前
查看封装的 组件
项目多次使用该组件, 使用方法如下:
const notMatch = (
<div>not Match</div>
)
/**
* @param {string | string[] | Promise<boolean>} authority
* @param { React.ReactNode } notMatch
*
*/
<Authorized authority={authorized!.authority} noMatch={noMatch}>
{children}
</Authorized>
Authorized 实现
// 此时的 RenderAuthorize 来自 src/Authorithy/index
// 重新包装了一下Authority,使得 currentAuthority = ['admin', 'user'] 在整个周期都能使用
// getAuthority 就是获取当前权限
let Authorized = RenderAuthorize(getAuthority()); // 此时还是从新包装了一下Authorited
- RenderAuthorize
const RenderAuthorize = renderAuthorize(Authorized);
返回闭包,同时导出CURRENT, 当前current
也就是 getAuthority()
的值, 也可以理解成一个HOC
吧
/**
* use authority or getAuthority
* @param {string|()=>String} currentAuthority
*/
function renderAuthorize(Authorized) {
return function (currentAuthority) {
if (currentAuthority) {
if (typeof currentAuthority === 'function') {
CURRENT = currentAuthority();
}
if (Object.prototype.toString.call(currentAuthority) === '[object String]' || Array.isArray(currentAuthority))
{
CURRENT = currentAuthority;
}
} else {
CURRENT = 'NULL';
}
return Authorized;
};
}
export { CURRENT }
export default (Authorized) => renderAuthorize(Authorized);
这个组件逻辑很简单, 需要看check
函数,传入的authority
是 [‘admin’] 为例子
如果通过check
那么返回children
, 否则返回 <notMatch>
const Authorized: React.FunctionComponent<AuthorizedProps> = ({
children,
authority,
noMatch = (
<Result
status="403"
title="403"
subTitle="Sorry, you are not authorized to access this page."
/>
),
}) => {
const childrenRender: React.ReactNode = typeof children === 'undefined' ? null : children;
//
const dom = check(authority, childrenRender, noMatch);
return <>{dom}</>;
};
- 在来看看check函数
下面的CURRENT 就是上面的renderAuthorize
导出的CURRENT
/**
* @param {string| stirng[]} authority 这里的authority 就是 ['admin'], 也就是准入条件
* @param { React.ReactNode } target 也就是 children
* @param { React.ReactNode } Exception notMatch
*/
function check<T, K>(authority: IAuthorityType, target: T, Exception: K): T | K | React.ReactNode {
return checkPermissions<T, K>(authority, CURRENT, target, Exception);
}
- 再来看看 checkPermissions
- 如果准入权限为空,那么直接渲染
target
- 要是
authority
跟currentAuthority
都是数组的时候,那看看有没有交集有交集那就返回target
- 要是
authority
为字符串,那么看看我们的current中有没有这个权限就可以了 - 要是
authority
为promise
那么就进入promise的操作 - 要是
authority
为方法,那就执行一下, 参数为current
/**
* 通用权限检查方法
* Common check permissions method
* @param { 权限判定 | Permission judgment } authority
* @param { 你的权限 | Your permission description } currentAuthority
* @param { 通过的组件 | Passing components } target
* @param { 未通过的组件 | no pass components } Exception
*/
const checkPermissions = <T, K>(
authority: IAuthorityType,
currentAuthority: string | string[],
target: T,
Exception: K,
): T | K | React.ReactNode => {
// 没有判定权限.默认查看所有
// Retirement authority, return target;
if (!authority) {
return target;
}
// 数组处理
if (Array.isArray(authority)) {
if (Array.isArray(currentAuthority)) {
if (currentAuthority.some((item) => authority.includes(item))) {
return target;
}
} else if (authority.includes(currentAuthority)) {
return target;
}
return Exception;
}
// string 处理
if (typeof authority === 'string') {
if (Array.isArray(currentAuthority)) {
if (currentAuthority.some((item) => authority === item)) {
return target;
}
} else if (authority === currentAuthority) {
return target;
}
return Exception;
}
// Promise 处理
if (authority instanceof Promise) {
return <PromiseRender<T, K> ok={target} error={Exception} promise={authority} />;
}
// Function 处理
if (typeof authority === 'function') {
const bool = authority(currentAuthority);
// 函数执行后返回值是 Promise
if (bool instanceof Promise) {
return <PromiseRender<T, K> ok={target} error={Exception} promise={bool} />;
}
if (bool) {
return target;
}
return Exception;
}
throw new Error('unsupported parameters');
};
登录组件
pages/user/login
查看 index.tsx
Login
页面有一个 <Tab>
, <Tab>
下面又是一下Input组件,只是被重新包装了
// 我们可以看出 UserName, Password 等等都是LoginForm导出的
const { Tab, UserName, Password, Mobile, Captcha, Submit } = LoginForm;
我们看下 Login
这个页面的大体结构
- 用一个
<LoginForm />
包裹着两个<Tab>
,<Tab>
底下是登录按钮还有其他的登录方式 - 其中一个
<Tab>对应的是账号密码登录
, 另一个是短信验证码登录
- 下面的
status
,loginType
是redux
的状态,表示登录失败后会有报错信息 <LoginForm>
的activeKey
只有两个account
,mobile
代表账号登录跟短信登录
<LoginForm activeKey={type} onTabChange={setType} onSubmit={handleSubmit}>
<Tab key="account" tab="账户密码登录">
{status === 'error' && loginType === 'account' && !submitting && (
<LoginMessage content="账户或密码错误(admin/ant.design)" />
)}
<UserName/>
<Password/>
</Tab>
<Tab key="mobile" tab="手机号登录">
{status === 'error' && loginType === 'mobile' && !submitting && (
<LoginMessage content="验证码错误" />
)}
<Mobile/>
<Captcha/>
</Tab>
<div>
<Checkbox checked={autoLogin} onChange={(e) => setAutoLogin(e.target.checked)}>
自动登录
</Checkbox>
<a>忘记密码</a>
</div>
<Submit loading={submitting}>登录</Submit>
<div className={styles.other}>
其他登录方式
<AlipayCircleOutlined className={styles.icon} />
<TaobaoCircleOutlined className={styles.icon} />
<WeiboCircleOutlined className={styles.icon} />
<Link className={styles.register} to="/user/register">
注册账户
</Link>
</div>
</LoginForm>
- 首先先创建一个
Context
, 这个context 主要是给<LoginTab>
使用的
LoginTab 实现
简单看就是
// antd 官网
<Tabs defaultActiveKey="1" onChange={callback}>
<TabPane tab="Tab 1" key="1">
Content of Tab Pane 1
</TabPane>
<TabPane tab="Tab 2" key="2">
Content of Tab Pane 2
</TabPane>
<TabPane tab="Tab 3" key="3">
Content of Tab Pane 3
</TabPane>
</Tabs>
const LoginTab: React.FC<LoginTabProps> = (props) => {
useEffect(() => {
const uniqueId = generateId('login-tab-'); //治理生成唯一的tab id
const { tabUtil } = props;
if (tabUtil) {
tabUtil.addTab(uniqueId);
}
}, []);
const { children } = props;
return <TabPane {...props}>{props.active && children}</TabPane>;
};
const WrapContext: React.FC<TabPaneProps> & {
typeName: string;
} = (props) => (
<LoginContext.Consumer>
{(value) => <LoginTab tabUtil={value.tabUtil} {...props} />}
</LoginContext.Consumer>
);
我们看看provider 的用法
// 此时这里用上了hook然后加载的时候会加载一个tab,当切换的时候会添加另一个tab
const [tabs, setTabs] = useState<string[]>([]);
// 这个active貌似没什么用
const [active, setActive] = useState({});
// 此时声明当前的tab是哪一个 是 account 还是 mobile
const [type, setType] = useMergeValue('', {
value: props.activeKey,
onChange: props.onTabChange,
});
<LoginContext.Provider
value={{
tabUtil: {
addTab: (id) => {
setTabs([...tabs, id]);
},
removeTab: (id) => {
setTabs(tabs.filter((currentId) => currentId !== id));
},
},
updateActive: (activeItem) => {
if (!active) return;
if (active[type]) {
active[type].push(activeItem);
} else {
active[type] = [activeItem];
}
setActive(active);
},
}}
>
<LoginContext.Provider>