MeteorCat / AntD的前端设计(一)

Created Sat, 13 Apr 2024 01:04:47 +0800 Modified Wed, 29 Oct 2025 23:24:45 +0800

AntD的前端设计(一)

之前搭建完了脚手架环境, 现在优先了解配置文件优先在哪配置:

// 这里可以查看到定义配置, 这里就是以项目做根目录
// 文件路径: .umirc.ts
import {defineConfig} from '@umijs/max';

export default defineConfig({
    antd: {},
    access: {},
    model: {},
    initialState: {},
    // 网络请求配置
    request: {
        // 获取消费服务端数据默认初始数据对象 key 为 data
        // { error:0, message:"success", data:{} }, 这样直接提取 data 而非初始 error/message/data
        dataField: 'data',
    },
    // layout 配置, title 就是标题名称
    // 具体配置说明查看: https://pro.ant.design/zh-CN/docs/title-landing
    layout: {
        // logo 标志
        logo: 'https://img.alicdn.com/tfs/TB1YHEpwUT1gK0jSZFhXXaAtVXa-28-27.svg',

        // 页面标题
        title: 'Mix管理后台',

        // 是否采用国际化, 如果不需要国际化直接关闭还能节省开发时间
        menu: false,
    },

    // 定义静态路由
    // 具体配置说明查看: https://pro.ant.design/zh-CN/docs/new-page
    routes: [
        {
            path: '/',
            redirect: '/home',
        },
        {
            name: '首页', // 权限名称
            path: '/home', // 路由路径
            component: './Home', // 路由部件文件, 实际指向 src/pages/Home/index.tsx
            // access: 'adminRouteFilter', // 这里是会调用 src/access.ts 中返回的 adminRouteFilter 进行鉴权
            // layout: false, // 默认都带有嵌套布局, 将该配置设置成 false 可以直接不嵌套在菜单内部, 用于单独配置登录页面
        },
        {
            name: '权限演示',
            path: '/access',
            component: './Access',
        },
        {
            name: ' CRUD 示例',
            path: '/table',
            component: './Table',
        },
    ],

    // npm 启动的二进制应用
    npmClient: 'npm',
});

这里就是最初始的配置, 这里先配置登录来搭建权限保护, 内部也有配置相关的时候默认优先匹配 .umirc.ts 文件.

权限配置文档: AntD权限

实际上 .umirc.ts 可能会被 git 忽略, 所以如果确定该配置要提交建议创建 config/config.ts 配置文件复制内容过去之后删除 .umirc.ts 文件.

后续配置文件都是采用 config/config.ts 处理, 但是需要注意 config.ts 是静态配置, 还有动态运行时配置需要在 app.ts 处理

这里从官网找到了登录页面的 具体配置, 我修改下先精简优化处理独立登陆页面:

// 文件路径: src/pages/Auth/Login/index.tsx
import {
    LockOutlined,
    UserOutlined,

} from '@ant-design/icons';
import {
    LoginForm,
    ProConfigProvider,
    ProFormCheckbox,
    ProFormText,
    setAlpha,
} from '@ant-design/pro-components';
import {message, theme} from 'antd';
import {history} from "@umijs/max";

export default () => {
    const {token} = theme.useToken();
    setAlpha(token.colorTextBase, 0.2);
    return (
        <ProConfigProvider hashed={false}>
            <div style={{backgroundColor: token.colorBgContainer}}>
                <LoginForm
                    title="Github"
                    subTitle="全球最大的代码托管平台"
                    onFinish={async (values) => {
                        // todo: 请求推送服务端登录逻辑
                        console.log(values);
                        message.success("登录成功", () => {
                            // 测试跳转
                            history.push("/home");
                        });
                    }}
                >
                    <>
                        <ProFormText
                            name="username"
                            fieldProps={{
                                size: 'large',
                                prefix: <UserOutlined className={'prefixIcon'}/>,
                            }}
                            placeholder={'用户名'}
                            rules={[
                                {
                                    required: true,
                                    message: '请输入用户名!',
                                },
                            ]}
                        />
                        <ProFormText.Password
                            name="password"
                            fieldProps={{
                                size: 'large',
                                prefix: <LockOutlined className={'prefixIcon'}/>,
                                strengthText:
                                    'Password should contain numbers, letters and special characters, at least 8 characters long.',
                                statusRender: (value) => {
                                    const getStatus = () => {
                                        if (value && value.length > 12) {
                                            return 'ok';
                                        }
                                        if (value && value.length > 6) {
                                            return 'pass';
                                        }
                                        return 'poor';
                                    };
                                    const status = getStatus();
                                    if (status === 'pass') {
                                        return (
                                            <div style={{color: token.colorWarning}}>
                                                强度:中
                                            </div>
                                        );
                                    }
                                    if (status === 'ok') {
                                        return (
                                            <div style={{color: token.colorSuccess}}>
                                                强度:强
                                            </div>
                                        );
                                    }
                                    return (
                                        <div style={{color: token.colorError}}>强度:弱</div>
                                    );
                                },
                            }}
                            placeholder={'密码'}
                            rules={[
                                {
                                    required: true,
                                    message: '请输入密码!',
                                },
                            ]}
                        />
                    </>
                    <div
                        style={{
                            marginBlockEnd: 24,
                        }}
                    >
                        <ProFormCheckbox noStyle name="logined" valuePropName="checked">
                            30天自动登录
                        </ProFormCheckbox>
                    </div>
                </LoginForm>
            </div>
        </ProConfigProvider>
    );
};

之后配置文件追加新路由, 这里路由文件独立出来 routes.ts 引入:

// 文件路径: config/routes.ts

// 路由配置单独移交给新文件处理
export default [
    {
        path: '/',
        redirect: '/auth/login', // 首页默认跳转登录先
    },
    {
        name: '用户登录',
        path: '/auth/login',
        component: './Auth/Login',
        hideInMenu: true,
        layout: false,
    },
    {
        name: '首页',
        path: '/home',
        component: './Home',
    },
    {
        name: '权限演示',
        path: '/access',
        component: './Access',
    },
    {
        name: ' CRUD 示例',
        path: '/table',
        component: './Table',
    },
]

// 在 config/config.ts 引入配置
import routes from "./routes";

export default defineConfig({
    // 定义静态路由
    routes: routes,
});

这里需要启动项目查看是否界面是否正常, 有没有在终端命令行打印对应输入数据, 之后就是测试调度数据推送服务端.

全局状态维护

这里推荐查看初始项目内的 src/models/global.ts 文件:

// 目录文件: src/models/global.ts
// 全局共享数据示例
import {useState} from 'react';

// 改写全局用户信息
const useUser = () => {
    // 登录全局写入的标识数据 { uid,nickname,username,token }
    const [uid, setUid] = useState<number>(0);
    const [nickname, setNickname] = useState<string>('');
    const [username, setUsername] = useState<string>('');
    const [token, setToken] = useState<string>('');
    const isLogin = () => uid !== 0;
    return {
        uid,
        setUid,
        nickname,
        setNickname,
        username,
        setUsername,
        token,
        setToken,
        isLogin
    };
};

export default useUser;

这里就是登录授权完成之后服务端返回的标识信息用于全局状态维护, 这里面类似于全局的 数据仓库; 所以在登录授权完成之后就可以写入内部:

// 文件路径: src/pages/Auth/Login/index.tsx
export default () => {
    const {token} = theme.useToken();
    const {setUsername, setUid, setToken} = useModel('global');

    setAlpha(token.colorTextBase, 0.2);
    return (
        <ProConfigProvider hashed={false}>
            <div style={{backgroundColor: token.colorBgContainer}}>
                <LoginForm
                    title="Github"
                    subTitle="全球最大的代码托管平台"
                    // 登录回调
                    onFinish={async (values) => {
                        // 网络请求, 略

                        // 假设登录完成直接写入到全局对象
                        // 后续只需要判断 isLogin() 即可
                        setUid(1);
                        setUsername(values.username || '');
                        setToken("this is token");
                    }}
                >
                    <!-- 其他略 -->
                </LoginForm>
            </div>
        </ProConfigProvider>
    );
};

这里就是拿之前的登录页面处理, 之后就是开始网络请求服务的搭建; 不过设想下开发流程当中过程, 服务端可能接口还没开发完成所以需要自己在客户端模拟推送, 这就是后续引入 mock 模拟数据接受推送的情况.

数据格式和测试

前端最终是要和服务端进行交互的, 所以需要先制定好数据返回格式用于开发预留, 这里用我日常返回格式:

{
  // 错误码, 默认0代表没有错误
  "error": 0,
  // 错误消息
  "message": "success",
  // 这个可有可无, 有时候消息需要前端弹窗提示, 而有时候又不需要弹出提示, 如果前后端都由一个人负责直接删除该配置项也行
  "alert": 0,
  // 数据格式, 这里不允许 null 且必须返回对象类型, 不能用 [] 替代
  "data": {},
}

在前后端联调在早年时候需要后端先给出接口才能联调, 但是现代化前端已经根据最开始制定的返回响应数据格式生成本地数据来调用本地数据, 之后等服务端也开发完成直接换成接口联通即可, 这就是新的 mock 开发.

mock 正式环境记得关闭, umi 已经集成 mock

这里官方还提供了网络的运行时 配置实例

首先配置好 app.ts 内部文件配置:

// 文件目录: src/app.ts
import {RequestConfig} from "@@/plugin-request/request";
import {message} from "antd";


// 错误处理方案: 错误类型
// 与后端约定的响应数据格式: { error,message,data }
interface ResponseStructure {
    error: number;
    message: string;
    data: any;
}


// 运行时配置
// 更多配置见: https://umijs.org/docs/max/request#%E8%BF%90%E8%A1%8C%E6%97%B6%E9%85%8D%E7%BD%AE%E7%A4%BA%E4%BE%8B
export const request: RequestConfig = {
    // 统一的请求设定
    timeout: 1000,
    headers: {'X-Requested-With': 'XMLHttpRequest'},

    // 错误处理: umi@3 的错误处理方案。
    errorConfig: {
        // 错误抛出
        errorThrower: (res: ResponseStructure) => {
            const {data, error, message} = res;
            if (error && error !== 0) {
                const error: any = new Error(message);
                error.name = 'BizError';
                error.info = {error, message, data};
                throw error; // 抛出自制的错误
            }
        },
        // 错误接收及处理
        errorHandler: (error: any, opts: any) => {
            if (opts?.skipErrorHandler) throw error;
            // 我们的 errorThrower 抛出的错误。
            if (error.name === 'BizError') {
                const errorInfo: ResponseStructure | undefined = error.info;
                if (errorInfo && errorInfo.error !== 0) {
                    message.error(errorInfo.message);
                }
            } else if (error.response) {
                // Axios 的错误
                // 请求成功发出且服务器也响应了状态码,但状态代码超出了 2xx 的范围
                message.error(`Response status:${error.response.status}`);
            } else if (error.request) {
                // 请求已经成功发起,但没有收到响应
                // \`error.request\` 在浏览器中是 XMLHttpRequest 的实例,
                // 而在node.js中是 http.ClientRequest 的实例
                message.error('None response! Please retry.');
            } else {
                // 发送请求时出了点问题
                message.error('Request error, please retry.');
            }
        },

    },

    // 请求拦截器
    requestInterceptors: [
        (config: any) => {
            // 拦截请求配置,进行个性化处理
            // 这里采用 OAuth2 认证
            let token = localStorage.getItem('token') || '';
            if (token.startsWith('"')) {
                token = JSON.parse(token);
            }
            if (token) {
                config.headers.Authorization = 'Bearer ' + token;
            }
            return config;
        },
        (error: any) => {
            return error;
        },
    ],

    // 响应拦截器
    responseInterceptors: [
        (response: any) => {
            // 拦截响应数据,进行个性化处理
            const {data} = response;
            // 返回错误不为0
            if (data.error !== 0) {
                message.error(data.message || "服务器错误");
            }
            return response;
        }
    ]
};


// 全局初始化数据配置,用于 Layout 用户信息和权限初始化
// 更多信息见文档:https://umijs.org/docs/api/runtime-config#getinitialstate
export async function getInitialState(): Promise<{ name: string }> {
    return {name: 'MeteorCat'};
}


// 运行时布局配置参考: https://procomponents.ant.design/components/layout#prolayout
export const layout = () => {
    return {
        // mix 即有头部,有左侧菜单等, 方便定制头像名称和退出
        layout: 'mix',

        // 退出方法, 要求 getInitialState 返回一个对象才可以显示(app.ts之中内置)
        logout: () => {
            console.log("退出登录")
        }
    };
};

这里 RequestConfig 配置项目已经完成初始化, 现在就是构建发起请求的设置, umi 需要知道以下内容:

后续开始编写网络 Mock, 结合前端的 状态管理, 从而才能维护全局登录状态