开发准备

代码仓库:https://github.com/changeclass/react-admin/commits/

技术选型

前端路由

创建项目

  1. 基于脚手架创建项目

    1
    create-react-app react-admin_client
  2. 编辑基本结构

    入口文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    /**
    * 入口文件
    */
    import React from 'react';
    import ReactDOM from 'react-dom';

    import App from './App';

    ReactDOM.render(
    <App />,
    document.getElementById('root')
    );

    根组件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /**
    * 应用根组件
    */

    import React, { Component } from "react";

    export default class App extends Component {
    render () {
    return <div>ahha </div>
    }
    }

    其他目录结构

    image-20201116094719994

  3. 引入AntD

    安装插件

    1
    2
    3
    yarn add antd
    # 实现组件按需打包的依赖
    yarn add react-app-rewired customize-cra babel-plugin-import

    配置按需打包,在根目录创建config-overrides.js文件。

    1
    2
    3
    4
    5
    6
    7
    8
    const {override, fixBabelImports} = require('customize-cra');
    module.exports = override(
    fixBabelImports('import', {
    libraryName: 'antd',
    libraryDirectory: 'es',
    style: 'css',
    }),
    );

    修改package.json文件

    1
    2
    3
    4
    5
    6
    "scripts": {
    "start": "react-app-rewired start",
    "build": "react-app-rewired build",
    "test": "react-app-rewired test",
    "eject": "react-scripts eject"
    },
  4. 自定义主题

    安装

    1
    yarn add less less-loader

    修改config-overrides.js文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    const { override, fixBabelImports, addLessLoader } = require('customize-cra');
    module.exports = override(
    fixBabelImports('import', {
    libraryName: 'antd',
    libraryDirectory: 'es',
    style: true,
    }),
    addLessLoader({
    lessOptions: {
    javascriptEnabled: true,
    modifyVars: {
    '@primary-color': '#2daebf'
    }
    }
    })
    );
  5. 引入路由

    安装插件

    1
    yarn add react-router-dom

    入口文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import { BrowserRouter, Route, Switch } from 'react-router-dom'

    import Admin from './pages/admin/admin';
    import Login from './pages/login/login';

    export default class App extends Component {
    render () {
    return (
    <BrowserRouter>
    <Switch>
    <Route path='/login' component={Login}></Route>
    <Route path='/' component={Admin}></Route>
    </Switch>
    </BrowserRouter>
    )
    }
    }

    子组件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    /**
    * 管理的路由组件
    */
    import React, { Component } from 'react'

    export default class Admin extends Component {
    render() {
    return <div>Admin</div>
    }
    }

登录页面-布局

代码仓库:https://github.com/changeclass/react-admin/commits/dev

  1. HTML结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    export default class Login extends Component {
    render () {
    return (
    <div className='login'>
    <header className='login-header'>
    <img src={logo} />
    <h1>React项目:谷粒商城</h1>
    </header>
    <section className='login-content'>
    <h2>用户登录</h2>
    <div>Form组件标签</div>
    </section>
    </div>
    )
    }
    }
  2. login的样式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    // login组件样式
    .login {
    width: 100%;
    height: 100%;
    background-image: url('./images/bg.jpg');
    background-size: 100% 100%;
    .login-header {
    display: flex;
    align-items: center;
    height: 80px;
    background-color: rgba(21, 20, 13, 0.5);
    img {
    width: 40px;
    height: 40px;
    margin: 0 15px 0 50px;
    }
    h1 {
    font-size: 30px;
    color: white;
    }
    }
    .login-content {
    width: 400px;
    height: 300px;
    background-color: #fff;
    margin: 50px auto;
    padding: 20px 40px;
    h2 {
    text-align: center;
    font-size: 30px;
    font-weight: bolder;
    }
    }
    }
  3. 全局样式

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    html,
    body,
    p,
    ol,
    ul,
    li,
    dl,
    dt,
    dd,
    blockquote,
    figure,
    fieldset,
    legend,
    textarea,
    pre,
    iframe,
    hr,
    h1,
    h2,
    h3,
    h4,
    h5,
    h6 {
    margin: 0;
    padding: 0;
    }

    h1,
    h2,
    h3,
    h4,
    h5,
    h6 {
    font-size: 100%;
    font-weight: normal;
    }

    ul {
    list-style: none;
    }

    button,
    input,
    select,
    textarea {
    margin: 0;
    }

    html {
    box-sizing: border-box;
    }

    *,
    *::before,
    *::after {
    box-sizing: inherit;
    }

    img,
    video {
    height: auto;
    max-width: 100%;
    }

    iframe {
    border: 0;
    }

    table {
    border-collapse: collapse;
    border-spacing: 0;
    }

    td,
    th {
    padding: 0;
    }

    td:not([align]),
    th:not([align]) {
    text-align: left;
    }

    html,
    body {
    height: 100%;
    width: 100%;
    }
    #root {
    height: 100%;
    width: 100%;
    }

登录页面-Form表单

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import { Form, Input, Button, Checkbox } from 'antd'
import { UserOutlined, LockOutlined } from '@ant-design/icons'
<Form
name='normal_login'
className='login-form'
onSubmit={this.handleSubmit}
>
<Form.Item>
<Input
prefix={
<UserOutlined
className='site-form-item-icon'
style={{ color: 'rgba(0,0,0,.25)' }}
/>
}
placeholder='Username'
/>
</Form.Item>
<Form.Item>
<Input
prefix={
<LockOutlined
className='site-form-item-icon'
style={{ color: 'rgba(0,0,0,.25)' }}
/>
}
type='password'
placeholder='Password'
/>
</Form.Item>
<Form.Item>
<Button
type='primary'
htmlType='submit'
className='login-form-button'
>
登录
</Button>
</Form.Item>
</Form>

登录页面-表单收集

视频中使用了AntD3的获取方式,然而AntD4与3版本获取表单实例略有差异。

  • 获取表单实例

    Form组件添加属性ref,在类通过React.createRef创建表单引用。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32

    export default class Login extends Component {
    // 创建表单实例
    formRef = React.createRef()
    // 表单提交事件
    handleSubmit = () => {
    console.log(this.formRef)
    }

    render() {
    return (
    <div className='login'>
    <header className='login-header'>
    <img src={logo} />
    <h1>React项目:谷粒商城</h1>
    </header>
    <section className='login-content'>
    <h2>用户登录</h2>
    <Form
    name='normal_login'
    className='login-form'
    // 为表单添加引用
    ref={this.formRef}
    // 为表单添加校检完成的事件
    onFinish={this.handleSubmit}
    >
    </Form>
    </section>
    </div>
    )
    }
    }

完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
import React, { Component } from 'react'
import { Form, Input, Button } from 'antd'
import { UserOutlined, LockOutlined } from '@ant-design/icons'

import './login.less'
import logo from './images/logo.png'
/**
* 登录的路由组件
*/

export default class Login extends Component {
// 创建表单实例
formRef = React.createRef()
// 表单提交事件
handleSubmit = () => {
console.log(this.formRef)
}

render() {
return (
<div className='login'>
<header className='login-header'>
<img src={logo} />
<h1>React项目:谷粒商城</h1>
</header>
<section className='login-content'>
<h2>用户登录</h2>
<Form
name='normal_login'
className='login-form'
// 为表单添加引用
ref={this.formRef}
// 为表单添加校检完成的事件
onFinish={this.handleSubmit}
>
<Form.Item>
<Input
prefix={
<UserOutlined
className='site-form-item-icon'
style={{ color: 'rgba(0,0,0,.25)' }}
/>
}
placeholder='Username'
/>
</Form.Item>
<Form.Item>
<Input
prefix={
<LockOutlined
className='site-form-item-icon'
style={{ color: 'rgba(0,0,0,.25)' }}
/>
}
type='password'
placeholder='Password'
/>
</Form.Item>
<Form.Item>
<Button
type='primary'
htmlType='submit'
className='login-form-button'
>
登录
</Button>
</Form.Item>
</Form>
</section>
</div>
)
}
}

登录页面-Form的验证方式

  1. 声明式验证

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    <Form.Item
    name='username'
    // 声明式验证
    rules={[
    { required: true, whitespace: true, message: '请输入用户名' },
    { min: 4, message: '用户名最少4位' },
    { max: 12, message: '用户名最多12位' },
    {
    pattern: /^[a-zA-Z0-9_]+$/,
    message: '用户名必须是大写字母、小写字母或下划线组成'
    }
    ]}
    >
    </Form.Item>
  2. 自定义验证

    1
    2
    3
    4
    <Form.Item
    name='password'
    rules={[{ validator: this.validatePwd }]}
    ></Form.Item>
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    // 自定义验证-密码
    // AntD4中已经没有callback回调函数了,而是返回Promise对象
    validatePwd = (rule, value) => {
    // value 表示当前输入框传入的值
    console.log(rule, value)
    if (!value) {
    return Promise.reject('密码必须输入')
    } else if (value.length < 5) {
    return Promise.reject('密码长度不能小于4')
    } else if (value.length > 12) {
    return Promise.reject('密码长度不能大于12')
    } else if (!/^[a-zA-Z0-9_]+$/.test(value)) {
    return Promise.reject('密码必须是大写字母、小写字母或下划线组成')
    } else {
    return Promise.resolve()
    }
    }

登录页面-Form的统一验证

新版直接使用 onFinish 事件,该事件仅当校验通过后才会执行。

1
2
3
4
<Form.Item
name='password'
rules={[{ validator: this.validatePwd }]}
></Form.Item>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 自定义验证-密码
// AntD4中已经没有callback回调函数了,而是返回Promise对象
validatePwd = (rule, value) => {
// value 表示当前输入框传入的值
console.log(rule, value)
if (!value) {
return Promise.reject('密码必须输入')
} else if (value.length < 5) {
return Promise.reject('密码长度不能小于4')
} else if (value.length > 12) {
return Promise.reject('密码长度不能大于12')
} else if (!/^[a-zA-Z0-9_]+$/.test(value)) {
return Promise.reject('密码必须是大写字母、小写字母或下划线组成')
} else {
return Promise.resolve()
}
}

发送Ajax请求

  1. 安装axios插件

    1
    yarn add axios
  2. api/ajax.js文件中封装ajax请求,并处理错误请求

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    /**
    * 发送异步请求的模块
    * 1. 统一处理请求异常
    */
    import axios from 'axios'
    import { message } from 'antd'
    export default function ajax (url, data = {}, method = 'GET') {
    return new Promise((resolve, reject) => {
    let promise
    // 1. 执行异步请求
    if (method === 'GET') {
    promise = axios.get(url, {
    params: data
    })
    } else {
    promise = axios.post(url, data)
    }
    promise
    .then(response => {
    // 2. 成功调用resolve
    resolve(response)
    })
    .catch(error => {
    // 3. 失败不调用reject,而是提示异常信息
    message.error("请求出错了:" + error.message);

    })
    })
    };
  3. 封装接口请求函数

    为了方便请求接口,将请求的重心放在数据传输上,而将各种接口封装成一个函数,只需要向此函数传递参数即可。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    /**
    * 包含应用中所有接口请求的函数的模块
    * 每个函数的返回值都是Promise
    */
    import ajax from './ajax'
    // 登录
    export const reqLogin = (username, password) => ajax('/login', { username, password }, "POST")
    // 添加用户
    export const reqAddUser = (user) => ajax('/manage/user/add', user, "POST")
  4. 跨域问题

    使用脚手架创建的项目只需要在package.json文件中加入代理字段即可。

    1
    2
    3
    {
    "proxy":"http://127.0.0.1:5000"
    }
  5. 数据持久化

    • 登录时

      将数据保存到local中,并同时放入内存中。

    • 进入登录页面时

      获取内存中是否有登录字段,如果有则重定向管理页面

    • 进入页面

      一进入页面将local中的用户数据存储到内存中。