1.UMI #

Umi,中文发音为「乌米」,是可扩展的企业级前端应用框架。Umi 以路由为基础的,同时支持配置式路由和约定式路由,保证路由的功能完备,并以此进行功能扩展。然后配以生命周期完善的插件体系,覆盖从源码到构建产物的每个生命周期,支持各种功能扩展和业务需求。

2.项目初始化 #

2.1 创建项目 #

mkdir umi4project
cd umi4project
npm init -y 
npm install @ant-design/pro-components ahooks jsonwebtoken @umijs/max --save

3.约定式路由 #

3.1 生成页面 #

mdir src
max g

3.2 支持typescript #

max g

3.3 添加命令 #

{
  "scripts": {
    "dev": "max dev",
    "build": "max build"
  },
}

3.4 .gitignore #

/node_modules
/src/.umi
/dist

3.5 tsconfig.json #

tsconfig.json

{
  "extends": "./src/.umi/tsconfig.json",
+ "compilerOptions": {
+   "noImplicitAny": false
+ }
}

3.6 启动 #

npm run  dev

4.配置式路由 #

4.1 config.ts #

config\config.ts

import { defineConfig } from '@umijs/max';
+import routes from "./routes";
export default defineConfig({
  npmClient: 'npm',
+ routes
});

4.2 routes.ts #

config\routes.ts

export default [
  { path: '/', redirect: '/home' },
  { icon: 'HomeOutlined', name: '首页', path: '/home', component: './home/index' },
  { icon: 'ProfileOutlined', name: '个人中心', path: '/profile', component: './profile/index' },
]

4.3 home\index.tsx #

src\pages\home\index.tsx

import styles from './index.less';
+import { Link, history, useNavigate } from '@umijs/max';
+import { Button } from 'antd';
export default function Page() {
  let navigate = useNavigate();
  return (
    <div>
+     <h1 className={styles.title}>首页</h1>
+     <Link to="/profile">个人中心</Link>
+     <Button type='primary' onClick={() => history.push('/profile')}>个人中心</Button>
+     <Button type='dashed' onClick={() => navigate('/profile')}>个人中心</Button>
    </div>
  );
}

5.tailwindcss #

5.1 安装 #

max g

5.2 home\index.tsx #

src\pages\home\index.tsx

import { Link, history, useNavigate } from '@umijs/max';
import { Button } from 'antd';
export default function Page() {
  let navigate = useNavigate();
  return (
    <div>
+     <h1 className={`text-lg font-bold text-red-600`}>首页</h1>
      <Link to="/profile">个人中心</Link>
      <Button onClick={() => history.push('/profile')}>个人中心</Button>
      <Button onClick={() => navigate('/profile')}>个人中心</Button>
    </div>
  );
}

5.3 profile\index.tsx #

src\pages\profile\index.tsx

export default function Page() {
  return (
    <div>
+     <h1 className={`text-lg font-bold text-green-600`}>个人中心</h1>
    </div>
  );
}

6.支持布局 #

6.1 config\config.ts #

import { defineConfig } from "@umijs/max";
import routes from "./routes";
export default defineConfig({
  npmClient: "pnpm",
  routes,
  tailwindcss: {},
+ layout: {
+   title: "UMI",
+   locale: false
+ },
+ antd: {},
});

7.支持子路由 #

7.1 routes.ts #

config\routes.ts

export default [
  { path: '/', redirect: '/home' },
  { icon: 'HomeOutlined', name: '首页', path: '/home', component: './home/index' },
  { icon: 'ProfileOutlined', name: '个人中心', path: '/profile', component: './profile/index' },
+ {
+   icon: 'UserOutlined',
+   name: '用户管理',
+   path: '/user',
+   component: './user/index',
+   routes: [
+     { name: '添加用户', path: '/user/add', component: './user/add/index' },
+     { name: '用户列表', path: '/user/list', component: './user/list/index' },
+     { name: '用户详情', path: '/user/detail/:id', component: './user/detail/index', hideInMenu: true },
+   ],
+ },
]

7.2 user\index.tsx #

src\pages\user\index.tsx

import { PageContainer } from '@ant-design/pro-components';
import { Outlet } from '@umijs/max';
export default function () {
    return (
        <PageContainer>
            <Outlet />
        </PageContainer>
    );
}

7.3 list\index.tsx #

src\pages\user\list\index.tsx

import { List } from 'antd';
import { Link } from '@umijs/max';

export default function () {
    const data = {
        list: [
            { id: 1, username: 'root' },
            { id: 2, username: 'admin' },
            { id: 3, username: 'member' }
        ]
    }
    return (
        <List
            header={<div>用户列表</div>}
            footer={<div>共计{data?.list?.length}</div>}
            bordered
            dataSource={data?.list}
            renderItem={(user: any) => (
                <List.Item>
                    <Link to={`/user/detail/${user.id}`} state={user}> {user.username}</Link>
                </List.Item>
            )}
        />
    );
}

7.4 add\index.tsx #

src\pages\user\add\index.tsx

import { Form, Input, Button, Row, Col } from 'antd';
export default function () {
    const onFinish = (values: any) => {
        console.log(values);
    };
    return (
        <Row >
            <Col offset={8} span={8}>
                <Form
                    labelCol={{ span: 4 }}
                    wrapperCol={{ span: 20 }}
                    onFinish={onFinish}
                >
                    <Form.Item
                        label="用户名"
                        name="username"
                        rules={[{ required: true, message: '请输入用户名' }]}
                    >
                        <Input />
                    </Form.Item>
                    <Form.Item
                        label="密码"
                        name="password"
                        rules={[{ required: true, message: '请输入密码' }]}
                    >
                        <Input.Password />
                    </Form.Item>
                    <Form.Item
                        label="手机号"
                        name="phone"
                        rules={[{ required: true, message: '请输入手机号' }]}
                    >
                        <Input />
                    </Form.Item>
                    <Form.Item wrapperCol={{ offset: 8, span: 16 }}>
                        <Button type="primary" htmlType="submit" >
                            提交
                        </Button>
                    </Form.Item>
                </Form>
            </Col>
        </Row>

    );
}

7.5 detail\index.tsx #

src\pages\user\detail\index.tsx

import { Descriptions } from 'antd';
import { useLocation } from '@umijs/max';
export default function (props: any) {
    const location = useLocation();
    let user = location.state as API.User;
    return (
        <Descriptions title="用户信息">
            <Descriptions.Item label="用户名">{user?.username}</Descriptions.Item>
            <Descriptions.Item label="手机号">{user?.phone}</Descriptions.Item>
        </Descriptions>
    );
}

8.请求用户列表 #

8.1 支持请求 #

import { defineConfig } from "@umijs/max";
import routes from "./routes";
export default defineConfig({
  npmClient: "pnpm",
  routes,
  tailwindcss: {},
  layout: {
    title: "UMI",
    locale: false
  },
  antd: {},
+ request: {},
+ model: {},
+ initialState: {},
+ proxy: {
+   '/api/': {
+     target: 'http://127.0.0.1:7001/',
+     changeOrigin: true
+   }
+ }
});

8.2 list\index.tsx #

src\pages\user\list\index.tsx

import { List } from 'antd';
import { Link, useModel } from '@umijs/max';
export default function () {
+   const { data, loading } = useModel('user.model');
    return (
        <List
            header={<div>用户列表</div>}
            footer={<div>共计{data?.list?.length}</div>}
            bordered
            dataSource={data?.list}
            renderItem={(user: any) => (
                <List.Item>
                    <Link to={`/user/detail/${user.id}`} state={user}> {user.username}</Link>
                </List.Item>
            )}
        />
    );
}

8.3 user\model.ts #

src\pages\user\model.ts

import { useRequest } from 'ahooks';
import { getUser } from '@/services/user';

export default () => {
  const { data, loading, refresh } = useRequest(getUser);
  return {
    data,
    refresh,
    loading
  };
};

8.4 typings.d.ts #

src\services\typings.d.ts

declare namespace API {
    export interface ListResponse<T> {
        data?: ListData<T>;
        errorCode: string;
        errorMessage: string;
        errorType: number;
        success?: boolean;
    }

    export interface ListData<T> {
        list?: T[];
        total?: number;
    }

    export interface User {
        id?: number;
        password?: string;
        phone?: string;
        username?: string;
        role?: string;
    }
    export interface SigninUser {
        username?: string;
        password?: string;
    }
    export interface SignupUser {
        password?: string;
        phone?: string;
        username?: string;
        role_id?: number;
    }
}

8.5 user.ts #

src\services\user.ts

import { request } from '@umijs/max';
export async function getUser() {
  return request<API.ListData<API.User>>('/api/user', {
    method: 'GET'
  });
}

8.6 app.ts #

src\app.ts

import { notification, message } from 'antd';

enum ErrorShowType {
    SILENT = 0,
    WARN_MESSAGE = 1,
    ERROR_MESSAGE = 2,
    NOTIFICATION = 3
}

const errorHandler = (error: any) => {
    if (error.name === 'BizError') {
        const errorInfo = error.info;
        if (errorInfo) {
            const { errorMessage, errorCode, showType } = errorInfo;
            switch (errorInfo.showType) {
                case ErrorShowType.SILENT:
                    break;
                case ErrorShowType.WARN_MESSAGE:
                    message.warn(errorMessage);
                    break;
                case ErrorShowType.ERROR_MESSAGE:
                    message.error(errorMessage);
                    break;
                case ErrorShowType.NOTIFICATION:
                    notification.open({
                        description: errorMessage,
                        message: errorCode,
                    });
                    break;
                default:
                    message.error(errorMessage);
            }
        }
    } else if (error.response) {
        message.error(`响应状态码:${error.response.status}`);
    } else if (error.request) {
        message.error('没有收到响应');
    } else {
        message.error('请求发送失败');
    }
};
const errorThrower = (res: any) => {
    const { success, data, errorCode, errorMessage } = res;
    if (!success) {
        const error: any = new Error(errorMessage);
        error.name = 'BizError';
        error.info = { errorCode, errorMessage, data };
        throw error;
    }
}
export const request = {
    timeout: 3000,
    headers: {
        ['Content-Type']: 'application/json',
        ['Accept']: 'application/json',
        credentials: 'include'
    },
    errorConfig: {
        errorThrower,
        errorHandler
    },
    requestInterceptors: [(url, options) => {
        return { url, options }
    }],
    responseInterceptors: [(response, options) => {
        return response.data;
    }]
};

export const layout = () => {
    return {
        title: 'UMI4',
        logo: 'https://img.alicdn.com/tfs/TB1YHEpwUT1gK0jSZFhXXaAtVXa-28-27.svg',
    };
};

9.添加用户 #

9.1 add\index.tsx #

src\pages\user\add\index.tsx

import { Form, Input, Button, Row, Col } from 'antd';
+import { useRequest } from 'ahooks';
+import { addUser } from '@/services/user';
+import { useNavigate, useModel } from '@umijs/max';
+import { useEffect } from 'react';

export default function () {
+   const navigate = useNavigate();
+   const { refresh } = useModel('user.model');
+   const { data, loading, run } = useRequest(addUser, {
+       manual: true,
+       onSuccess: refresh
+   });
    const onFinish = (values: any) => {
+       run(values);
    };
+   useEffect(() => {
+       if (data) {
+           navigate('/user/list');
+       }
+   }, [data]);
    return (
        <Row >
            <Col offset={8} span={8}>
                <Form
                    labelCol={{ span: 4 }}
                    wrapperCol={{ span: 20 }}
                    onFinish={onFinish}
                >
                    <Form.Item
                        label="用户名"
                        name="username"
                        rules={[{ required: true, message: '请输入用户名' }]}
                    >
                        <Input />
                    </Form.Item>
                    <Form.Item
                        label="密码"
                        name="password"
                        rules={[{ required: true, message: '请输入密码' }]}
                    >
                        <Input.Password />
                    </Form.Item>
                    <Form.Item
                        label="手机号"
                        name="phone"
                        rules={[{ required: true, message: '请输入手机号' }]}
                    >
                        <Input />
                    </Form.Item>
                    <Form.Item wrapperCol={{ offset: 8, span: 16 }}>
                        <Button type="primary" htmlType="submit" >
                            提交
                        </Button>
                    </Form.Item>
                </Form>
            </Col>
        </Row>
    );
}

9.2 src\services\user.ts #

src\services\user.ts

import { request } from '@umijs/max';

export async function getUser() {
  return request<API.ListData<API.User>>('/api/user', {
    method: 'GET'
  });
}
+export async function addUser(
+  body?: API.User
+) {
+  return request<string>('/api/user', {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    data: body
+  });
+}

10.用户注册 #

10.1 routes.ts #

config\routes.ts

export default [
  { path: '/', redirect: '/home' },
  { icon: 'HomeOutlined', name: '首页', path: '/home', component: './home/index' },
  { icon: 'ProfileOutlined', name: '个人中心', path: '/profile', component: './profile/index' },
  {
    icon: 'UserOutlined',
    name: '用户管理',
    path: '/user',
    component: './user/index',
    routes: [
      { name: '添加用户', path: '/user/add', component: './user/add/index' },
      { name: '用户列表', path: '/user/list', component: './user/list/index' },
      { name: '用户详情', path: '/user/detail/:id', component: './user/detail/index', hideInMenu: true },
    ],
  },
+ {
+   name: '注册',
+   path: '/signup',
+   component: './signup/index',
+   hideInMenu: true,
+   layout: false
+ }
]

10.2 src\app.ts #

src\app.ts

import { notification, message } from 'antd';
+import { history } from '@umijs/max';

enum ErrorShowType {
    SILENT = 0,
    WARN_MESSAGE = 1,
    ERROR_MESSAGE = 2,
    NOTIFICATION = 3
}

const errorHandler = (error: any) => {
    if (error.name === 'BizError') {
        const errorInfo = error.info;
        if (errorInfo) {
            const { errorMessage, errorCode, showType } = errorInfo;
            switch (errorInfo.showType) {
                case ErrorShowType.SILENT:
                    break;
                case ErrorShowType.WARN_MESSAGE:
                    message.warn(errorMessage);
                    break;
                case ErrorShowType.ERROR_MESSAGE:
                    message.error(errorMessage);
                    break;
                case ErrorShowType.NOTIFICATION:
                    notification.open({
                        description: errorMessage,
                        message: errorCode,
                    });
                    break;
                default:
                    message.error(errorMessage);
            }
        }
    } else if (error.response) {
        message.error(`响应状态码:${error.response.status}`);
    } else if (error.request) {
        message.error('没有收到响应');
    } else {
        message.error('请求发送失败');
    }
};
const errorThrower = (res: any) => {
    const { success, data, errorCode, errorMessage } = res;
    if (!success) {
        const error: any = new Error(errorMessage);
        error.name = 'BizError';
        error.info = { errorCode, errorMessage, data };
        throw error;
    }
}
export const request = {
    timeout: 3000,
    headers: {
        ['Content-Type']: 'application/json',
        ['Accept']: 'application/json',
        credentials: 'include'
    },
    errorConfig: {
        errorThrower,
        errorHandler
    },
    requestInterceptors: [(url, options) => {
        return { url, options }
    }],
    responseInterceptors: [(response, options) => {
        return response.data;
    }]
};
+interface InitialState {
+    currentUser: API.User | null
+}
+
+export async function getInitialState() {
+    let initialState: InitialState = {
+        currentUser: null
+    }
+    return initialState;
+}
export const layout = ({ initialState }) => {
    return {
        title: 'UMI4',
        logo: 'https://img.alicdn.com/tfs/TB1YHEpwUT1gK0jSZFhXXaAtVXa-28-27.svg',
+       onPageChange(location) {
+           const { currentUser } = initialState;
+           if (!currentUser && location.pathname !== '/signin') {
+               history.push('/signup');
+           }
+       }
    };
};

10.3 constants\index.ts #

src\constants\index.ts

enum RoleCodes {
    root = 'root',
    admin = 'admin',
    member = 'member'
}
const ROLES = [
    {
        code: RoleCodes.root,
        name: '超级管理员'
    },
    {
        code: RoleCodes.admin,
        name: '管理员'
    },
    {
        code: RoleCodes.member,
        name: '普通成员'
    }
]

export {
    RoleCodes,
    ROLES
}

10.4 signup\index.tsx #

src\pages\signup\index.tsx

import { Form, Input, Button, Card, Row, Col, Spin, Select } from 'antd';
import { useRequest } from 'ahooks';
import { signup } from '@/services/auth';
import { history, Link } from '@umijs/max';
import { ROLES } from '@/constants';

export default function () {
    const { loading, run } = useRequest(signup, {
        manual: true,
        onSuccess() {
            history.push('/signin')
        }
    });
    const onFinish = (values: any) => {
        run(values);
    };
    const onFinishFailed = (errorInfo: any) => {
        console.log('Failed:', errorInfo);
    };
    return (
        <Row className='h-screen bg-gray-200' align='middle'>
            <Col offset={8} span={8} >
                <Card title="请登录" extra={<Link to="/signin">去登录</Link>}>
                    <Spin spinning={loading}>
                        <Form
                            labelCol={{ span: 4 }}
                            wrapperCol={{ span: 20 }}
                            onFinish={onFinish}
                            onFinishFailed={onFinishFailed}
                        >
                            <Form.Item
                                label="用户名"
                                name="username"
                                rules={[{ required: true, message: '请输入用户名' }]}
                            >
                                <Input />
                            </Form.Item>
                            <Form.Item
                                label="密码"
                                name="password"
                                rules={[{ required: true, message: '请输入密码' }]}
                            >
                                <Input.Password />
                            </Form.Item>
                            <Form.Item
                                label="手机号"
                                name="phone"
                                rules={[{ required: true, message: '请输入手机号' }]}
                            >
                                <Input />
                            </Form.Item>
                            <Form.Item
                                label="角色"
                                name="role"
                                rules={[{ required: true, message: '请选择角色' }]}
                            >
                                <Select
                                >
                                    {
                                        ROLES.map(role => {
                                            return (
                                                <Select.Option value={role.code} key={role.code}>
                                                    {role.name}
                                                </Select.Option>
                                            )
                                        })
                                    }
                                </Select>
                            </Form.Item>
                            <Form.Item wrapperCol={{ offset: 8, span: 16 }}>
                                <Button type="primary" htmlType="submit" >
                                    提交
                                </Button>
                            </Form.Item>
                        </Form>
                    </Spin>
                </Card>
            </Col>
        </Row>
    );
}

10.5 auth.ts #

src\services\auth.ts

import { request } from '@umijs/max';

export async function signup(
  body?: API.SignupUser
) {
  return request<string>('/api/user', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    data: body
  });
}

11.用户登录 #

11.1 routes.ts #

config\routes.ts

export default [
  { path: '/', redirect: '/home' },
  { icon: 'HomeOutlined', name: '首页', path: '/home', component: './home/index' },
  { icon: 'ProfileOutlined', name: '个人中心', path: '/profile', component: './profile/index' },
  {
    icon: 'UserOutlined',
    name: '用户管理',
    path: '/user',
    component: './user/index',
    routes: [
      { name: '添加用户', path: '/user/add', component: './user/add/index' },
      { name: '用户列表', path: '/user/list', component: './user/list/index' },
      { name: '用户详情', path: '/user/detail/:id', component: './user/detail/index', hideInMenu: true },
    ],
  },
  {
    name: '注册',
    path: '/signup',
    component: './signup/index',
    hideInMenu: true,
    layout: false
  },
+ {
+   name: '登录',
+   path: '/signin',
+   component: './signin/index',
+   hideInMenu: true,
+   layout: false
+ }
]

11.2 app.ts #

src\app.ts

import { notification, message } from 'antd';
import { history } from '@umijs/max';
+import { decode } from 'jsonwebtoken';

enum ErrorShowType {
    SILENT = 0,
    WARN_MESSAGE = 1,
    ERROR_MESSAGE = 2,
    NOTIFICATION = 3
}

const errorHandler = (error: any) => {
    if (error.name === 'BizError') {
        const errorInfo = error.info;
        if (errorInfo) {
            const { errorMessage, errorCode, showType } = errorInfo;
            switch (errorInfo.showType) {
                case ErrorShowType.SILENT:
                    break;
                case ErrorShowType.WARN_MESSAGE:
                    message.warn(errorMessage);
                    break;
                case ErrorShowType.ERROR_MESSAGE:
                    message.error(errorMessage);
                    break;
                case ErrorShowType.NOTIFICATION:
                    notification.open({
                        description: errorMessage,
                        message: errorCode,
                    });
                    break;
                default:
                    message.error(errorMessage);
            }
        }
    } else if (error.response) {
        message.error(`响应状态码:${error.response.status}`);
    } else if (error.request) {
        message.error('没有收到响应');
    } else {
        message.error('请求发送失败');
    }
};
const errorThrower = (res: any) => {
    const { success, data, errorCode, errorMessage } = res;
    if (!success) {
        const error: any = new Error(errorMessage);
        error.name = 'BizError';
        error.info = { errorCode, errorMessage, data };
        throw error;
    }
}
export const request = {
    timeout: 3000,
    headers: {
        ['Content-Type']: 'application/json',
        ['Accept']: 'application/json',
        credentials: 'include'
    },
    errorConfig: {
        errorThrower,
        errorHandler
    },
    requestInterceptors: [(url, options) => {
+        const token = localStorage.getItem('token');
+        if (token) {
+            options.headers.authorization = token
+        }
+        return { url, options }
+    }],
    responseInterceptors: [(response, options) => {
        return response.data;
    }]
};
interface InitialState {
    currentUser: API.User | null
}

export async function getInitialState() {
    let initialState: InitialState = {
        currentUser: null
    }
+   const token = localStorage.getItem('token');
+   if (token) {
+       initialState.currentUser = decode(token)
+   }
    return initialState;
}
export const layout = ({ initialState }) => {
    return {
        title: 'UMI4',
        logo: 'https://img.alicdn.com/tfs/TB1YHEpwUT1gK0jSZFhXXaAtVXa-28-27.svg',
        onPageChange(location) {
            const { currentUser } = initialState;
+           if (!currentUser && location.pathname !== '/signin') {
+               history.push('/signin');
+           }
        }
    };
};

11.3 signin\index.tsx #

src\pages\signin\index.tsx

import { Form, Input, Button, Card, Row, Col, Spin } from 'antd';
import { useRequest } from 'ahooks';
import { signin } from '@/services/auth';
import { useModel, history, Link } from '@umijs/max';
import { useEffect } from 'react';
import { decode } from 'jsonwebtoken';

export default function () {
    const { initialState, setInitialState } = useModel('@@initialState');
    const { loading, run } = useRequest(signin, {
        manual: true,
        onSuccess(result) {
            localStorage.setItem('token', result);
            const currentUser = decode(result);
            setInitialState({ currentUser });
        }
    });
    useEffect(() => {
        if (initialState?.currentUser)
            history.push('/')
    }, [initialState]);
    const onFinish = (values: any) => {
        run(values);
    };
    const onFinishFailed = (errorInfo: any) => {
        console.log('Failed:', errorInfo);
    };
    return (
        <Row className='h-screen bg-gray-200' align='middle'>
            <Col offset={8} span={8} >
                <Card title="请登录" extra={<Link to="/signup">去注册</Link>}>
                    <Spin spinning={loading}>
                        <Form
                            labelCol={{ span: 4 }}
                            wrapperCol={{ span: 20 }}
                            onFinish={onFinish}
                            onFinishFailed={onFinishFailed}
                        >
                            <Form.Item
                                label="用户名"
                                name="username"
                                rules={[{ required: true, message: '请输入用户名' }]}
                            >
                                <Input />
                            </Form.Item>
                            <Form.Item
                                label="密码"
                                name="password"
                                rules={[{ required: true, message: '请输入密码' }]}
                            >
                                <Input.Password />
                            </Form.Item>
                            <Form.Item wrapperCol={{ offset: 8, span: 16 }}>
                                <Button type="primary" htmlType="submit" >
                                    提交
                                </Button>
                            </Form.Item>
                        </Form>
                    </Spin>
                </Card>
            </Col>
        </Row>
    );
}

11.4 auth.ts #

src\services\auth.ts

import { request } from '@umijs/max';

export async function signup(
  body?: API.SignupUser
) {
  return request<string>('/api/user', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    data: body
  });
}

+export async function signin(
+  body?: API.SigninUser
+) {
+  return request('/api/signin', {
+    method: 'POST',
+    headers: {
+      'Content-Type': 'application/json',
+    },
+    data: body
+  });
+}

12.用户退出 #

12.1 profile\index.tsx #

src\pages\profile\index.tsx

+import { PageContainer } from '@ant-design/pro-components';
+import HeaderMenu from '@/components/HeaderMenu';

export default function Page() {
  return (
+   <PageContainer ghost>
+     <HeaderMenu />
+   </PageContainer>
  );
}

12.2 HeaderMenu\index.tsx #

src\components\HeaderMenu\index.tsx

import { Dropdown, Menu } from 'antd'
import { LoginOutlined } from '@ant-design/icons';
import { useModel, history } from '@umijs/max';

export default function HeaderMenu() {
  const { initialState, setInitialState } = useModel('@@initialState');
  const logoutClick = () => {
    localStorage.removeItem('token');
    setInitialState({ currentUser: null });
    history.push('/signin');
  }
  const menu = (
    <Menu items={[
      {
        key: 'logout',
        label: (
          <span>退出</span>
        ),
        icon: <LoginOutlined />,
        onClick: logoutClick
      }
    ]} />
  )
  return (
    <Dropdown.Button overlay={menu}>
      {initialState?.currentUser?.username}
    </Dropdown.Button>
  )
}

13.路由权限 #

13.1 config.ts #

config\config.ts

import { defineConfig } from "@umijs/max";
import routes from "./routes";
export default defineConfig({
  npmClient: "pnpm",
  routes,
  tailwindcss: {},
  layout: {
    title: "UMI",
    locale: false
  },
  antd: {},
  request: {},
  model: {},
  initialState: {},
  proxy: {
    '/api/': {
      target: 'http://127.0.0.1:7001/',
      changeOrigin: true
    }
  },
+ access: {}
});

13.2 routes.ts #

config\routes.ts

export default [
  { path: '/', redirect: '/home' },
  { icon: 'HomeOutlined', name: '首页', path: '/home', component: './home/index' },
  { icon: 'ProfileOutlined', name: '个人中心', path: '/profile', component: './profile/index' },
  {
    icon: 'UserOutlined',
    name: '用户管理',
    path: '/user',
    component: './user/index',
    routes: [
+      { name: '添加用户', path: '/user/add', component: './user/add/index', access: 'adminCan' },
+      { name: '用户列表', path: '/user/list', component: './user/list/index', access: 'memberCan' },
+      { name: '用户详情', path: '/user/detail/:id', component: './user/detail/index', hideInMenu: true, access: 'memberCan' },
    ],
  },
  {
    name: '注册',
    path: '/signup',
    component: './signup/index',
    hideInMenu: true,
    layout: false
  },
  {
    name: '登录',
    path: '/signin',
    component: './signin/index',
    hideInMenu: true,
    layout: false
  },
]

13.3 access.ts #

src\access.ts

import { RoleCodes } from '@/constants';
export default function (initialState) {
    const role = initialState?.currentUser?.role;
    return {
        rootCan: role === RoleCodes.root,
        adminCan: [RoleCodes.root, RoleCodes.admin].includes(role),
        memberCan: [RoleCodes.root, RoleCodes.admin, RoleCodes.member].includes(role),
    };
}

14.按钮权限 #

14.1 list\index.tsx #

src\pages\user\list\index.tsx

+import { List, Button } from 'antd';
+import { Link, useModel, useAccess } from '@umijs/max';
+import { useRequest } from 'ahooks';
+import { deleteUser } from '@/services/user';

export default function () {
    const { data, loading, refresh } = useModel('user.model');
+   const { run } = useRequest(deleteUser, {
+       manual: true,
+       onSuccess: refresh
+   });
+   const access = useAccess();
    return (
        <List
            className='w-1/3'
            header={<div>用户列表</div>}
            footer={<div>共计{data?.list?.length}</div>}
            bordered
            dataSource={data?.list}
            renderItem={(user: any) => (
                <List.Item>
                    <Link to={`/user/detail/${user.id}`} state={user}> {user.username}</Link>
+                   <Button
+                       onClick={() => run(user.id)}
+                       size='small'
+                       type="primary"
+                       disabled={!access.adminCan}
+                       loading={loading}>
+                       删除
+                   </Button>
                </List.Item>
            )}
        />
    );
}

14.2 user.ts #

src\services\user.ts

import { request } from '@umijs/max';

export async function getUser() {
  return request<API.ListData<API.User>>('/api/user', {
    method: 'GET'
  });
}
export async function addUser(
  body?: API.User
) {
  return request<string>('/api/user', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
    },
    data: body
  });
}
+export async function deleteUser(id) {
+  return request<string>(`/api/user/${id}`, {
+    method: 'DELETE'
+  });
+}

15.用户管理 #

15.1 routes.ts #

config\routes.ts

[
+    {
+        icon:'UserOutlined',
+        name:'users',
+        path:'/users',
+        component:'./users/index'
+    },
]

15.2 user.ts #

src\services\user.ts

import { request } from '@umijs/max';
export async function getUser(params) {
    return request('/api/user', {
        method: 'GET',
        params,
    });
}

export async function addUser(user) {
    return request('/api/user', {
        method: 'POST',
        data: user,
    });
}

export async function updateUser(id, user) {
    return request(`/api/user/${id}`, {
        method: 'PUT',
        data: user,
    });
}

export async function deleteUser(ids) {
    return request(`/api/user/${ids[0]}`, {
        method: 'DELETE',
        data:ids
    });
}

15.3 users\index.tsx #

src\pages\users\index.tsx

import { useState, useEffect } from 'react';
import { Table, Button, Modal, Form, Input, message, Space, Popconfirm, Select,Row,Col } from 'antd';
import { getUser, addUser, updateUser, deleteUser } from '@/services/user';
const { Option } = Select;
const Users = () => {
  const [data, setData] = useState([]);
  const [loading, setLoading] = useState(true);
  const [visible, setVisible] = useState(false);
  const [current, setCurrent] = useState(null);
  const [selectedRowKeys, setSelectedRowKeys] = useState([]);
  const [searchUsername, setSearchUsername] = useState('');
  const [searchPhone, setSearchPhone] = useState('');
  const [pagination, setPagination] = useState({ current: 1, pageSize: 10, total: 0 });
  const [form] = Form.useForm();
  const fetchUsers = async (pageNum = 1, pageSize = 10, username = '', phone = '') => {
    setLoading(true);
    try {
      let params:any = { pageNum, pageSize };
      if (username) {
        params.username = username;
      }
      if (phone) {
        params.phone = phone;
      }
      const response = await getUser(params);
      setData(response.data.list);
      setPagination({ ...pagination, total: response.data.total, current: pageNum, pageSize });
      setLoading(false);
    } catch (error) {
      setLoading(false);
      message.error('获取用户列表失败');
    }
  };
  useEffect(() => {
    fetchUsers();
  }, []);
  const handleTableChange = (pagination) => {
    fetchUsers(pagination.current, pagination.pageSize);
  };
  const showModal = (record) => {
    form.resetFields();
    setCurrent(record);
    if (record) {
      form.setFieldsValue(record);
    }
    setVisible(true);
  };
  const handleOk = async () => {
    const values = await form.validateFields();
    if (current) {
      try {
        await updateUser(current.id, values);
        message.success('更新成功');
      } catch (error) {
        message.error('更新失败');
      }
    } else {
      try {
        await addUser(values);
        message.success('添加成功');
      } catch (error) {
        message.error('添加失败');
      }
    }
    setVisible(false);
    fetchUsers(pagination.current, pagination.pageSize);
  };
  const handleDelete = async (id) => {
    try {
      await deleteUser([id]);
      message.success('删除成功');
    } catch (error) {
      message.error('删除失败');
    }
    fetchUsers(pagination.current, pagination.pageSize);
  };
  const handleMultiDelete = async () => {
    try {
      await deleteUser(selectedRowKeys);
      message.success('删除成功');
    } catch (error) {
      message.error('删除失败');
    }
    setSelectedRowKeys([]);
    fetchUsers(pagination.current, pagination.pageSize);
  };
  const handleSearch = () => {
    fetchUsers(pagination.current, pagination.pageSize, searchUsername, searchPhone);
  };
  const columns = [
    {
      title: '用户名',
      dataIndex: 'username',
    },
    {
      title: '密码',
      dataIndex: 'password',
    },
    {
      title: '手机号',
      dataIndex: 'phone',
    },
    {
      title: '角色',
      dataIndex: 'access',
      render: (access) => {
        switch (access) {
          case 'root': return '超级管理员';
          case 'admin': return '管理员';
          default: return '普通用户';
        }
      }
    },
    {
      title: '操作',
      dataIndex: 'operation',
      render: (_, record) => (
        <Space size="middle">
          <a onClick={() => showModal(record)}>编辑</a>
          <Popconfirm title="确定要删除吗?" onConfirm={() => handleDelete(record.id)}>
            <a>删除</a>
          </Popconfirm>
        </Space>
      ),
    },
  ];
  const rowSelection = {
    selectedRowKeys,
    onChange: setSelectedRowKeys,
  };
  return (
    <div>
      <Row>
        <Col flex="auto">
          <Input
            placeholder="输入用户名搜索"
            value={searchUsername}
            onChange={e => setSearchUsername(e.target.value)}
            style={{ width: 200, marginRight: 16 }}
          />
          <Input
            placeholder="输入手机号搜索"
            value={searchPhone}
            onChange={e => setSearchPhone(e.target.value)}
            style={{ width: 200, marginRight: 16 }}
          />
          <Button type="primary" onClick={handleSearch}>
            搜索
          </Button>
        </Col>
        <Col flex="none">
          <Button type="primary" onClick={() => showModal(null)}>
            新增用户
          </Button>
          <Button type="primary" onClick={handleMultiDelete} style={{ marginLeft: 16 }}>
            批量删除
          </Button>
        </Col>
      </Row>
      <Table
        rowKey="id"
        columns={columns}
        dataSource={data}
        loading={loading}
        rowSelection={rowSelection}
        pagination={pagination}
        onChange={handleTableChange}
      />
      <Modal
        title={current ? '编辑用户' : '新增用户'}
        open={visible}
        onOk={handleOk}
        onCancel={() => setVisible(false)}
        destroyOnClose
      >
        <Form form={form} initialValues={current} layout="vertical">
          <Form.Item
            name="username"
            label="用户名"
            rules={[{ required: true, message: '请输入用户名' }]}
          >
            <Input />
          </Form.Item>
          <Form.Item
            name="password"
            label="密码"
            rules={[{ required: true, message: '请输入密码' }]}
          >
            <Input.Password />
          </Form.Item>
          <Form.Item
            name="phone"
            label="手机号"
            rules={[
              { required: true, message: '请输入手机号' },
              { pattern: /^1[3-9]\d{9}$/, message: '手机号格式不正确' },
            ]}
          >
            <Input />
          </Form.Item>
          <Form.Item
            name="access"
            label="角色"
            rules={[{ required: true, message: '请选择角色' }]}
          >
            <Select>
              <Option value="root">超级管理员</Option>
              <Option value="admin">管理员</Option>
              <Option value="member">普通用户</Option>
            </Select>
          </Form.Item>
        </Form>
      </Modal>
    </div>
  );
};
export default Users;

15.4 fields.tsx #

fields.tsx

import { Table, Row, Col, Button, Modal, Form, Input, Select, message, Space, Popconfirm } from 'antd';
const fields = [
    {
      props: {
        name: 'username',
        label: '用户名',
        rules: [{ required: true, message: '请输入用户名' }],
      },
      component: Input,
    },
    {
      props: {
        name: 'password',
        label: '密码',
        rules: [{ required: true, message: '请输入密码' }],
      },
      component: Input.Password,
    },
    {
      props: {
        name: 'email',
        label: '邮箱',
        rules: [{ required: true, message: '请输入邮箱' }],
      },
      component: Input,
    },
    {
      props: {
        name: 'phone',
        label: '手机号',
        rules: [{ required: true, message: '请输入手机号' }],
      },
      component: Input,
    },
    {
      props: {
        name: 'gender',
        label: '性别',
        rules: [{ required: true, message: '请选择性别' }],
      },
      component: Select,
      children: [
        <Select.Option key="1" value={1}>男</Select.Option>,
        <Select.Option key="0" value={0}>女</Select.Option>,
      ],
    },
    {
      props: {
        name: 'access',
        label: '角色',
        rules: [{ required: true, message: '请输入角色' }],
      },
      component: Select,
      children: [
        <Select.Option key="root" value="root">超级管理员</Select.Option>,
        <Select.Option key="admin" value="admin">管理员</Select.Option>,
        <Select.Option key="member" value="member">普通会员</Select.Option>,
      ],
    },
  ];
export default fields;

15.5 ConfigForm.tsx #

ConfigForm.tsx

import React, { useCallback, useState } from 'react';
import { Form, Button } from 'antd';
const toConfigForm = (Form) => {
  return ({ form,handleSubmit, handleReset, fields, submitText = '提交', resetText = '重置', ...rest }) => {
    const [submitting, setSubmitting] = useState(false);
    const onSubmit = useCallback(async () => {
      try {
        setSubmitting(true);
        const values = await form.validateFields();
        handleSubmit?.(values);
      } catch (error) {
        console.error('Failed:', error);
        // Add your custom user-friendly error message here
      } finally {
        setSubmitting(false);
      }
    }, [form, handleSubmit]);

    const onReset = useCallback(() => {
      form.resetFields();
      handleReset?.();
    }, [form, handleReset]);

    return (
      <Form form={form} onFinish={onSubmit} {...rest}>
        {fields.map(({ props, component, componentProps, children }) => (
          <Form.Item key={props.name} {...props}>
            {React.createElement(component, componentProps, children)}
          </Form.Item>
        ))}
        <Form.Item>
          <Button type="primary" htmlType="submit" loading={submitting}>{submitText}</Button>
          <Button type="default" onClick={onReset}>{resetText}</Button>
        </Form.Item>
      </Form>
    );
  };
};
const ConfigForm = toConfigForm(Form);
export default ConfigForm;

15.tailwindcss #

1. 什么是 Tailwind CSS? #

Tailwind CSS 是一个高度可定制的、工具类优先的 CSS 框架,它使你可以在开发过程中迅速构建现代化的用户界面。与其他 CSS 框架(如 Bootstrap 和 Foundation)不同,Tailwind 不提供预设的 UI 组件,而是提供一套底层工具类,这些工具类可以通过组合来创建自定义的设计。

2. 特点 #

3. 安装 #

使用 npm 或 yarn 安装:

npm install tailwindcss
# or
yarn add tailwindcss

4. 基础使用 #

  1. 初始化配置文件

    运行下面的命令来生成一个 tailwind.config.js 文件:

    npx tailwindcss init
    
  2. 引入 Tailwind

    在你的 CSS 文件中:

    @import 'tailwindcss/base';
    @import 'tailwindcss/components';
    @import 'tailwindcss/utilities';
    
  3. 编写 HTML

    使用工具类来定义元素的样式:

    <button class="bg-blue-500 text-white rounded-lg px-4 py-2 hover:bg-blue-600">
      Click Me
    </button>
    

16.useRequest #

useRequest 是一个常见的 React Hook,用于简化 API 调用或异步操作的处理。尽管多个库提供了类似的功能,但 useRequest 通常与 UmiJSahooks 这样的库关联在一起。

useRequest 的主要目标是:

  1. 简化 API 调用和异步操作的代码。
  2. 提供一致的错误处理和加载状态管理。

基本用法 #

// 从 "ahooks" 库中引入 "useRequest" Hook。
import { useRequest } from 'ahooks';

// 从 "react" 库中引入 "React"。
import React from 'react';

// 定义一个函数 "getName",它返回一个 Promise。
// 这个 Promise 在1秒后解析为字符串 "zhufeng"。
function getName() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('zhufeng');
    }, 1000);
  });
}

// 定义一个 React 函数组件 "App"。
function App() {
  // 使用 "useRequest" Hook 来调用 "getName" 函数。
  // "data" 会在 Promise 解析后获得其值,而 "loading" 用于表示当前是否在加载状态。
  const { data, loading } = useRequest(getName);
  // 如果处于加载状态,显示 "加载中..."。
  if (loading) {
    return <div>加载中...</div>;
  }
  // 当数据加载完成时,显示 "用户名: " 后面跟着加载的数据。
  return <div>用户名: {data}</div>;
};
// 导出 "App" 组件,使其可以在其他地方被引用。
export default App;

17. styled-components #

styled-components 是一种流行的 CSS-in-JS 库,允许你使用 JavaScript 中的模板字符串来定义组件的样式。使用 styled-components 可以将样式与组件逻辑紧密地结合在一起,从而使得组件样式更加模块化和可重用。

以下是 styled-components 的基础讲解:

  1. 安装

    在你的项目中安装 styled-components:

    npm install styled-components
    
  2. 基本使用

    创建一个 styled component:

    import styled from 'styled-components';
    
    const Button = styled.button`
      background: palevioletred;
      border-radius: 3px;
      border: none;
      color: white;
      padding: 0.5rem 1rem;
    `;
    
    function App() {
      return <Button>点击我</Button>;
    }
    

    在上述代码中,我们定义了一个名为 Button 的 styled component,该组件将渲染为一个带有特定样式的 <button> 元素。

  3. 传递属性

    你可以根据组件的属性来调整样式:

    const Button = styled.button`
      background: ${props => props.primary ? 'palevioletred' : 'white'};
      color: ${props => props.primary ? 'white' : 'palevioletred'};
      /* ...其他样式... */
    `;
    
    function App() {
      return (
        <div>
          <Button primary>主要按钮</Button>
          <Button>次要按钮</Button>
        </div>
      );
    }
    
  4. 扩展样式

    你可以在一个 styled component 的基础上扩展样式:

    const Button = styled.button`
      background: palevioletred;
      color: white;
      /* ... */
    `;
    
    const LargeButton = styled(Button)`
      padding: 1rem 2rem;
    `;
    
    function App() {
      return <LargeButton>更大的按钮</LargeButton>;
    }
    
  5. 全局样式

    使用 createGlobalStyle 创建全局样式:

    import styled, { createGlobalStyle } from 'styled-components';
    
    const GlobalStyle = createGlobalStyle`
      body {
        margin: 0;
        font-family: Arial, sans-serif;
      }
    `;
    
    function App() {
      return (
        <>
          <GlobalStyle />
          {/* 其他组件 */}
        </>
      );
    }
    
  6. 主题

    使用 ThemeProvider 组件来为你的应用提供主题:

    import styled, { ThemeProvider } from 'styled-components';
    
    const theme = {
      primaryColor: 'palevioletred',
      secondaryColor: 'white'
    };
    
    const Button = styled.button`
      background: ${props => props.theme.primaryColor};
      color: ${props => props.theme.secondaryColor};
    `;
    
    function App() {
      return (
        <ThemeProvider theme={theme}>
          <Button>使用主题的按钮</Button>
        </ThemeProvider>
      );
    }
    

18. react-query #

react-query 是一个数据获取库,用于 React。与 Redux 和 MobX 等状态管理库不同,react-query 主要关注远程数据的获取、缓存、同步和更新。它提供了自动数据同步、后台数据获取、缓存管理、无限查询等功能。

以下是 react-query 的核心概念:

  1. 安装

    你首先需要安装 react-query:

     npm install react-query
    
  2. 基本使用

    使用 useQuery hook 来进行数据获取:

     import { useQuery } from 'react-query';
    
     function FetchData() {
       const { data, error, isLoading } = useQuery('repoData', () =>
         fetch('https://api.github.com/repos/tannerlinsley/react-query').then(res => res.json())
       );
    
       if (isLoading) return 'Loading...';
    
       if (error) return `An error occurred: ${error.message}`;
    
       return (
         <div>
           <h1>{data.name}</h1>
           <p>{data.description}</p>
         </div>
       );
     }
    

    在上述示例中,useQuery 接受两个参数:查询键和获取函数。查询键用于唯一标识此查询。

  3. 配置

    你可以为 useQuery 提供配置对象以改变其默认行为:

     const { data } = useQuery('repoData', fetchFunction, {
       staleTime: 1000 * 60 * 5,  // 5 minutes
       cacheTime: 1000 * 60 * 60,  // 1 hour
       retry: 3
     });
    
    • staleTime: 数据在多少时间后被视为过时,此后将在背景中重新获取。
    • cacheTime: 未使用的数据在多长时间后从缓存中被清除。
    • retry: 如果查询失败,尝试的次数。
  4. QueryClient 和 QueryClientProvider

    要使用 react-query, 你需要创建一个 QueryClient 并使用 QueryClientProvider 将其提供给你的应用程序:

     import { QueryClient, QueryClientProvider } from 'react-query';
    
     const queryClient = new QueryClient();
    
     function App() {
       return (
         <QueryClientProvider client={queryClient}>
           <FetchData />
           {/* 其他组件 */}
         </QueryClientProvider>
       );
     }
    

现在,我们将上述概念组合在一起,创建一个完整的 react-query 示例:

import React from 'react';
import { useQuery, QueryClient, QueryClientProvider } from 'react-query';

// 数据获取函数
function fetchRepoData() {
  return fetch('https://api.github.com/repos/tannerlinsley/react-query').then(res => res.json());
}

function RepoInfo() {
  const { data, error, isLoading } = useQuery('repoData', fetchRepoData);

  if (isLoading) return 'Loading...';
  if (error) return `An error occurred: ${error.message}`;

  return (
    <div>
      <h1>{data.name}</h1>
      <p>{data.description}</p>
    </div>
  );
}

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <RepoInfo />
      {/* 其他组件 */}
    </QueryClientProvider>
  );
}

export default App;

19. valtio #

valtio 是一个简单的状态管理库,用于 React。其主要思想是使状态管理变得简单而不牺牲反应性。与其他状态管理解决方案如 Redux 或 MobX 相比,valtio 提供了一个更直观和轻量级的方法来管理和响应状态变化。

以下是 valtio 的基础讲解:

  1. 安装

    你首先需要安装 valtio:

     npm install valtio
    
  2. 创建状态

    使用 proxy 函数创建一个代理状态:

     import { proxy } from 'valtio';
    
     const state = proxy({
       count: 0,
     });
    

    上述代码创建了一个有 count 属性的状态对象。

  3. 在组件中使用状态

    使用 useSnapshot hook 来在组件中访问和响应状态变化:

     import { useSnapshot } from 'valtio';
    
     function Counter() {
       const snapshot = useSnapshot(state);
    
       return (
         <div>
           <p>Count: {snapshot.count}</p>
           <button onClick={() => state.count++}>Increment</button>
         </div>
       );
     }
    

    state.count 改变时,组件将自动重新渲染。

现在,我们将上述概念组合在一起,创建一个完整的 valtio 示例:

import React from 'react';
import { proxy, useSnapshot } from 'valtio';

// 创建一个状态代理
const state = proxy({
  count: 0,
});

function Counter() {
  // 获取状态的快照
  const snapshot = useSnapshot(state);

  return (
    <div>
      <p>Count: {snapshot.count}</p>
      <button onClick={() => state.count++}>Increment</button>
      <button onClick={() => state.count--}>Decrement</button>
    </div>
  );
}

function App() {
  return (
    <div>
      <h1>Valtio Counter</h1>
      <Counter />
    </div>
  );
}

export default App;

20. UnoCSS #

当然可以。

UnoCSS 是什么?

UnoCSS 是一个实时 (JIT) 原子 CSS 框架。原子 CSS 的理念是将样式分解为原子性(小型、单一目的)的类名,每个类名只做一件事。这样,你可以通过组合这些原子类来构建复杂的用户界面。

UnoCSS 的主要特点:

  1. 实时编译:在开发过程中,UnoCSS 实时生成样式。这意味着你在 HTML 中实际使用的样式才会在最终的 CSS 输出中被编译和包含,从而产生更小的 CSS 包。

  2. 基于插件的架构:它基于一组插件来操作,这意味着你可以使用插件扩展其功能,或者编写自己的插件来生成自定义样式。

  3. 预设:UnoCSS 为常见的框架如 TailwindCSS 提供预设,允许你实现类似的实用程序为基础的工作流程,但可能提供更多的自定义和效率。

  4. 动态实用程序:除了静态实用程序,UnoCSS 还允许你创建动态实用程序。例如,你可以使用特定的类名语法动态生成颜色的阴影。

UnoCSS 如何工作?

  1. 扫描和清除:在构建过程中,UnoCSS 扫描你的文件(如 Vue 或 React 组件)以获取类名。然后处理这些类名,生成必要的样式。

  2. 生成样式:根据你使用的实用程序生成样式。例如,如果你使用了像 p-1 这样的实用程序(它可能代表 padding: 0.25rem),UnoCSS 就会生成该特定样式。

  3. 输出:一旦生成了所有实际使用的样式,UnoCSS 就会输出一个包含你项目中实际使用的样式的 CSS 文件。

使用 UnoCSS 的优点:

  1. 性能:由于它只包括实际使用的样式,所以生成的 CSS 通常较小。

  2. 灵活性:借助于 UnoCSS 的插件扩展能力,你可以完美地定制框架来满足你的需求。

  3. 快速开发:一旦熟悉了实用程序类,开发人员可以在不频繁跳转 HTML 和 CSS 文件之间的情况下快速进行原型设计和构建用户界面。

可能的缺点:

  1. 学习曲线:如果你不熟悉原子/实用程序优先的 CSS,可能需要一段时间来熟悉。

  2. 冗长的 HTML:由于大量的实用程序类,你的 HTML 可能会变得很冗长。

  3. 自定义:尽管 UnoCSS 提供了广泛的自定义功能,但要实现某些设计可能需要额外的配置或自定义实用程序。

集成:

UnoCSS 可与 Vue、React、Vite 和 Webpack 等流行的框架和构建工具集成。

UMI4使用 UnoCSS

npm i unocss @unocss/cli

config\config.js

export default {
  // 添加插件列表
  plugins: [
    // 使用 Umi 的 unoCSS 插件
    require.resolve('@umijs/plugins/dist/unocss')
  ],
  // unoCSS 的配置
  unocss: {
    // 观察并处理以下路径中的文件
    watch: ['src/**/*.tsx']
  },
};

// unocss.config.ts

// 导入来自'unocss'的相关方法和预设
import {defineConfig, presetAttributify, presetUno} from 'unocss';
// 定义一个函数,根据提供的参数来创建配置
export function createConfig({strict = true, dev = true} = {}) {
  // 使用定义的配置,其中包括环境模式和所使用的预设
  return defineConfig({
    envMode: dev ? 'dev' : 'build',
    presets: [presetAttributify({strict}), presetUno()],
  });
}
// 默认导出创建的配置
export default createConfig();
  1. presetUno

presetUnoUnoCSS 的默认预设。UnoCSS 的核心思想是为常用的 CSS 属性和值提供缩写,从而让开发者可以以更快、更简洁的方式书写样式。例如,你可以用 .m-1 来表示 margin: 0.25rem,或 .bg-red 来表示背景色为红色。

这种方法与 TailwindCSS 类似,但 UnoCSS 提供了一种更自定义和轻量级的方法来实现这些效果。

使用 presetUno,你就启用了这些原子类样式的功能。

  1. presetAttributify

presetAttributify 是另一个功能强大的预设,允许你直接在 HTML 标签的属性中书写样式。

传统的 UnoCSS 或类似的工具类 CSS 方法要求你在 class 属性中列出所需的工具类。但使用 attributify,你可以直接在元素上使用属性作为工具类。

举例来说:

传统的方式:

<div class="bg-red text-white">Hello, world!</div>

使用 attributify:

<div bg="red" text="white">Hello, world!</div>

如你所见,attributify 预设允许你使用属性来代替 class 来定义样式。这提供了一种更直观、更紧凑的方式来表示样式。

  <div className="p-4 bg-blue-200 text-center">
    <button className="px-4 py-2 bg-blue-500 text-white rounded">Click Me</button>
  </div>

21 UI 组件的二次封装 #

UI组件的二次封装是一个常见的开发模式,特别是在大型项目和团队中。这意味着我们基于一个现有的 UI 组件库(如 Ant Design、Material-UI、Bootstrap 等)创建自己的组件集,以满足项目特定的需求或设计规范。

二次封装有以下好处

  1. 统一的设计语言:通过定制和封装,可以确保整个应用或多个应用之间有一致的外观和感觉。
  2. 提高开发速度:组件是可重用的,这意味着开发者不需要从零开始编写组件,而是可以使用预先设计好的组件。
  3. 简化代码维护:当原始 UI 库中的组件有更新或者项目需求发生变化时,只需要在封装的组件中进行更改,而不是在整个应用的多个地方进行更改。
  4. 增强功能:封装允许你为组件添加额外的功能,这些功能可能不是原始库中提供的。

如何进行二次封装: #

  1. 确定需求:首先确定你需要哪些组件,并了解它们需要满足的特定需求和设计规范。

  2. 创建基础组件:基于原始组件库,创建你自己的基础组件。例如,你可以创建一个自定义的按钮,该按钮具有特定的大小、颜色和行为。

  3. 增加额外功能:为封装的组件增加额外的功能。例如,如果你封装了一个输入组件,你可以添加自动完成、字符计数等功能。

  4. 封装复杂组件:一些组件可能涉及多个子组件的组合。例如,一个带有标签、图标和工具提示的按钮。确保所有的子组件都能很好地一起工作。

  5. 测试:在使用封装的组件之前,确保对其进行充分的测试,包括单元测试、集成测试和可视化测试。

  6. 文档:为你的封装的组件编写文档是非常重要的。确保开发团队知道如何使用它们,以及它们的功能和限制。

示例

假设我们要在 Ant Design 的基础上封装一个按钮:

import { Button as AntButton } from 'antd';
import PropTypes from 'prop-types';

const CustomButton = ({ label, variant, ...props }) => {
  let color;
  switch(variant) {
    case 'primary':
      color = 'blue';
      break;
    case 'secondary':
      color = 'gray';
      break;
    default:
      color = 'blue';
      break;
  }

  return <AntButton style={{ backgroundColor: color }} {...props}>{label}</AntButton>;
}

CustomButton.propTypes = {
  label: PropTypes.string.isRequired,
  variant: PropTypes.oneOf(['primary', 'secondary'])
};

export default CustomButton;