开发准备

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

技术选型

前端路由

创建项目

  1. 基于脚手架创建项目

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

    入口文件

    /**
     * 入口文件
     */
    import React from 'react';
    import ReactDOM from 'react-dom';
    
    import App from './App';
    
    ReactDOM.render(
      <App />,
      document.getElementById('root')
    );

    根组件

    /**
     * 应用根组件
     */
    
    import React, { Component } from "react";
    
    export default class App extends Component {
      render () {
        return <div>ahha </div>
      }
    }

    其他目录结构

    image-20201116094719994

  3. 引入AntD

    安装插件

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

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

    const {override, fixBabelImports} = require('customize-cra');
    module.exports = override(
        fixBabelImports('import', {
            libraryName: 'antd',
            libraryDirectory: 'es',
            style: 'css',
        }),
    );

    修改package.json文件

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

    安装

    yarn add less less-loader

    修改config-overrides.js文件

    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. 引入路由

    安装插件

    yarn add react-router-dom

    入口文件

    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>
        )
      }
    }

    子组件

    /**
     * 管理的路由组件
     */
    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结构

    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的样式

    // 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. 全局样式

    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表单

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创建表单引用。

    
    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>
        )
      }
    }

完整代码

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. 声明式验证

    <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. 自定义验证

    <Form.Item
        name='password'
        rules={[{ validator: this.validatePwd }]}
        ></Form.Item>
    // 自定义验证-密码
    // 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 事件,该事件仅当校验通过后才会执行。

<Form.Item
    name='password'
    rules={[{ validator: this.validatePwd }]}
    ></Form.Item>
// 自定义验证-密码
// 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插件

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

    /**
     * 发送异步请求的模块
     * 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. 封装接口请求函数

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

    /**
     * 包含应用中所有接口请求的函数的模块
     * 每个函数的返回值都是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文件中加入代理字段即可。

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

    • 登录时

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

    • 进入登录页面时

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

    • 进入页面

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