React项目实战(上)

618 阅读15分钟

笔记来源:拉勾教育 - 大前端就业集训营

文章内容:学习过程中的笔记、感悟、和经验

提示:项目实战类文章资源无法上传,仅供参考

拉勾教育图书电商项目实战 - 上

技术栈介绍

页面布局使用Ant Design组件库进行操作

客户端

  • 前端框架:React
  • 路由管理:react-router-dom
  • 用户界面:antd(UI组件库)
  • 全局状态管理:redux
  • 异步状态更新:redux-saga
  • 网络请求:axios
  • 状态调试工具:redux-devtools-extension

服务端

  • 脚本:node.js
  • 数据库:mongodb
  • 数据库可视化:mongodb-compass

搭建开发环境(服务端)

安装mongodb数据库(Mac端)

  1. 安装homebrew(mac系统软件包管理器)

    // 老师提供的方法有问题,可以使用这个命令安装,选择1 - 中科大源
    /bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)"
    
    // 安装之后可以输入下面命令常看是否安装成功
    brew 或者 brew -v
    
  2. 添加mongodb仓库源

    // 可能需要翻墙,我做的时候弄了好久
    brew tap mongodb/brew
    
  3. 安装mongodb

    // 执行命令安装
    brew install mongodb-community
    
    // 验证安装
    mongod --version
    

    如果安装mongodb报和xcode相关的错误,说明没有安装xcode,则需要安装xcode

    // 安装 xcode
    xcode-select --install
    
  4. 启动mongodb

    // 启动
    brew services run mongodb-community
    
    // 如果控制台不报错而且输出了 - Successfully ran `mongodb-community`....表示启动成功
    
  5. 停止mongodb

    // 停止
    brew services stop mongodb-community
    
    // 如果控制台不报错就代表停止成功了,应该会显示 - Stopping `mongodb-community`...
    
  6. 文件位置(一般不需要调整)

    • 1.数据库配置文件:/usr/oca/etc/mongod.conf
    • 2.数据库文件默认存放位置:/usr/local/var/mongodb
    • 3.日志存放位置:/usr/ocal/var/log/mongodb/mongo.log

安装mongodb数据库(Windows端)

没有win设备,所以上网找一下教程吧,安装一个软件就可以了,可能需要设置环境变量

安装mongodb数据库可视化软件 - Robo 3T

搭建开发环境

创建项目安装依赖

  1. 使用脚手架创建项目:npm install -g create-react-app@4.0.3

  2. 创建项目:npm init react-app 项目名称,课程里使用lagou-carefully-selected

  3. 安装项目依赖

    npm install antd@4.14.0 axios@0.21.1 moment@2.29.1 redux@4.0.5 react-redux@7.2.2 react-router-dom@5.2.0 redux-saga@1.1.3 redux-devtools-extension@2.13.9

    + moment@2.29.1
    + axios@0.21.1
    + redux@4.0.5
    + redux-saga@1.1.3
    + redux-devtools-extension@2.13.9
    + react-redux@7.2.2
    + react-router-dom@5.2.0
    + antd@4.14.0   // ant design - UI组件库
    
  4. 清理项目中不需要的文件、目录、代码

  5. antd CSS使用CDN,老师用的4.14.0版本,把这个标签复制到html模版文件public/index.htmlhead

    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/antd/4.14.0/antd.min.css"/>

根据开发环境切换服务端请求地址

在开发环境和生产环境中,要使用不同的接口地址,这一步实现不需要人为修改,程序可自动根据环境切换接口地址

  1. 使用create-react-app内置的dotenv通过代码的方式配置和使用环境变量

    // 跟目录创建文件 - .env
    // 生产(上线)环境接口地址
    REACT_APP_PRODUCTION_API_URL=http://fullstack.net.cn/api
    // 开发环境接口地址
    REACT_APP_DEVLOPMENT_API_URL=http://localhost/api
    
    // 注意:
    // 1、环境变量名字必须以 REACT_APP_ 开头
    // 2、千万不要写在一行,别问为什么
    
  2. 将请求地址写入配置中,根据环境决定使用哪个API地址

    // src/config.js  创建配置文件,用来根据环境使用不同的地址
    
    // 创建一个变量用来表示请求地址,默认使用开发环境地址
    export let API = process.env.REACT_APP_DEVLOPMENT_API_URL
    
    // 判断是否当前处于生产环境
    if (process.env.NODE_ENV === "production") {
      // 如果处于生产环境修改地址为生产环境地址
      API = process.env.REACT_APP_PRODUCTION_API_URL
    }
    

在项目中可以通过 process.env. REACT APPDEVLOPMENTAPIURL方式进行访问,但是这样会有弊端,其一是代码过长写起来不方便,其二是如果在代码中将环境写死,当切换环境时改起来也不方便。解决方案就是将AP地址写入配置中,根据环境决定使用哪个AP地址

安装浏览器扩展插件

  • React Developer Tools查看React组件层次结构、props信息等功能

    • 开发者模式中点击Components选项卡
  • Redux Devtools:监测Store中的状态变化

    • 开发者模式中点击Redux选项卡

    • 需要进行配置才能使用Redux Devtools

      // 引入 composeWithDevTools 包裹中间件得以使用 Redux Devtools 插件功能
      import { composeWithDevTools } from 'redux-devtools-extension'
      
      export const store = createStore(
        数据,
        // 使用方法包裹中间件即可
        composeWithDevTools(applyMiddleware(...))
      )
      

创建项目Layout(公共组件)和路由

rfce可以快速创建组件(再安装了插件的情况下)ES7 React/Redux/GraphQL/React-Native snippets插件可以实现快速创建结构参考

  • src目录创建Components目录防止所有组件,包含两个目录:
    • admin目录:放置后台管理页面相关组件(只有管理员可以访问)
    • core目录:放置前台页相关组件(常规用户访问)
      • Layout组件:每个页面组件中都有公共结构,其他组件作为公共组件的子组件
      • Home组件:首页
      • Shop组件:商品列表组件
    • Routes组件:路由规则组件,替代App组件成为根组件在index.js中使用
// src/Components/core/Home.js  首页组件

import React from 'react'
// 引入公共组件
import Layout from './Layout'

function Home() {
  return (
    // 最外层是公共组件,组件是公共组件的子组件
    <Layout>
      首页
    </Layout>
  )
}

export default Home

// src/Components/core/Shop.js  商品列表组件

import React from 'react'
// 引入公共组件
import Layout from './Layout'

function Shop() {
  return (
    // 最外层是公共组件,组件是公共组件的子组件
    <Layout>
      商品列表
    </Layout>
  )
}

export default Shop

// src/Components/Routes.js  路由组件

import React from 'react'
// 路由需要使用的方法: BrowserRouter类似于HashRouter,更推荐使用
import { Switch, BrowserRouter, Route } from 'react-router-dom'
// 引入路由组件
import Home from './core/Home'
import Shop from './core/Shop'

function Routes() {
  return (
    // 包裹开启路由功能
    <BrowserRouter>
      {/* 避免书写错误出现相同路由 */}
      <Switch>
        {/* 路由,第一个路由为默认路由,精准匹配 */}
        <Route path="/" component={Home} exact />
        <Route path="/shop" component={Shop} />
      </Switch>
    </BrowserRouter>
  )
}

export default Routes

//src/index.js  使用路由组件替换掉原本默认的App组件

import React from 'react'
import ReactDOM from 'react-dom'
// 引入路由组件
import Routes from './Components/Routes'


ReactDOM.render(
  <React.StrictMode>
    {/* 使用路由组件替代原本的APP */}
    <Routes />
  </React.StrictMode>,
  document.getElementById('root')
)

// src/Components/core/Layout.js  公共组件

import React from 'react'

// 公共组件,解构出子组件内容
// children即是子组件传递过来的内容
function Layout({children}) {
  console.log(children)
  return (
    <div>
      {/* 书写公共组件内容 */}
      公共组件
      {/* 直接使用子组件内容 */}
      {children}
    </div>
  )
}

export default Layout

全局Store初始化

  • src目录下新建Store目录
    • 创建index.js文件:创建store、导出store
    • 创建reducers目录:存放和导出reducers
      • 创建index.js文件,合并所有reducers并导出
      • 创建test.js文件,单个reducer

整个流程:创建reducer - 合并全部reducer - 使用reducer创建store - 把store传递给根组件启用仓库 - 组件使用方法获得状态(测试)

// src/store/reducers/text.js  创建一个reducer存放状态数据

// 创建一个reducer,并且导出
export default function testReducer(state = 0) {
  return state
}

// src/store/reducers/index.js  将全部reducer合并导出

// 导入 reducer 合并方法
import { combineReducers } from "redux";
// 引入要合并的 reducer
import testReducer from "./text";

// 合并
const rootReducer = combineReducers({
  test: testReducer
})

// 导出
export default rootReducer
// src/store/index.js  使用上面合并的reducer创建仓库

// 创建 store 方法
import { createStore } from "redux";
// 要使用的 reducer
import rootReducer from "./reducers";

// 创建 store 仓库
const store = createStore(rootReducer)

export default store
// src/index.js  正式传递并启用仓库

import React from 'react'
import ReactDOM from 'react-dom'
// 引入路由组件
import Routes from './Components/Routes'
// 引入方法传递 store 仓库
import { Provider } from 'react-redux'
// 引入 store
import store from './store'


ReactDOM.render(
  <React.StrictMode>
    {/* 把 store 仓库传递下去,正式启用 */}
    <Provider store={store}>
      {/* 使用路由组件替代原本的APP */}
      <Routes />
    </Provider>
  </React.StrictMode>,
  document.getElementById('root')
)

// src/Components/core/Home.js  组件从store中获得数据

import React from 'react'
// 引入公共组件
import Layout from './Layout'
// 引入获取状态方法
import {useSelector} from 'react-redux'

function Home() {
  // 创建变量获取状态,从Redux的store中提取数据(state)
  const state = useSelector(state => state)
  // 打印看一下
  console.log(state)
  return (
    // 最外层是公共组件,组件是公共组件的子组件
    <Layout>
      首页内容
    </Layout>
  )
}

export default Home

路由状态同步给全局store

将路由信息同步给全局store,这样不管是页面组件还是非页面组件都可以获取

使用connected-react-router第三方包

使用步骤

  1. 改造合并reducer代码,添加一个新的属性

  2. 创建store的时候添加路由中间件,这里要获取和使用history

  3. 改造传递和启用仓库的逻辑

    这里比较麻烦,直接写固定写法了,后面直接复制就可以了

// src/store/reducers/index.js

// 引入模块方法
import { connectRouter } from "connected-react-router";
// 导入 reducer 合并方法
import { combineReducers } from "redux";
// 引入要合并的 reducer
import testReducer from "./text";

// 合并全部 reducer
// createRootReducer是一个函数,不要更改名字
const createRootReducer = history => combineReducers({
  test: testReducer,
  // 路由信息
  router: connectRouter(history)
})

// 导出
export default createRootReducer
// src/store/index.js

// 创建 store 方法
import { createStore, applyMiddleware } from "redux";
// 要使用的 reducer
import createRootReducer from "./reducers";
import { createBrowserHistory } from 'history'
import { routerMiddleware } from 'connected-react-router'

// 导出 history
export const history = createBrowserHistory()

// 创建 store 仓库
const store = createStore(
  createRootReducer(history),
  // 中间件监控路由变化
  applyMiddleware(routerMiddleware(history))
)

export default store
// src/index.js

import React from 'react'
import ReactDOM from 'react-dom'
// 引入路由组件
import Routes from './Components/Routes'
// 引入方法传递 store 仓库
import { Provider } from 'react-redux'
// 引入 store
import store, { history }  from './store'
import { ConnectedRouter } from 'connected-react-router'


ReactDOM.render(
  <React.StrictMode>
    {/* 把 store 仓库传递下去,正式启用 */}
    <Provider store={store}>
      {/* 包装实现监控路由更改 */}
      <ConnectedRouter history={history}>
        {/* 使用路由组件替代原本的APP */}
        <Routes />
      </ConnectedRouter>
    </Provider>
  </React.StrictMode>,
  document.getElementById('root')
)

// src/Components/Routes.js  删除之前外层包裹的<BrowserRouter>,不是固定写法

import React from 'react'
// 路由需要使用的方法: BrowserRouter类似于HashRouter,更推荐使用
import { Switch, BrowserRouter, Route } from 'react-router-dom'
// 引入路由组件
import Home from './core/Home'
import Shop from './core/Shop'

function Routes() {
  return (
    // 包裹开启路由功能(这里因为处理路由到store的时候已经做了,这里不需要了)
      // {/* 避免书写错误出现相同路由 */}
      <Switch>
        {/* 路由,第一个路由为默认路由,精准匹配 */}
        <Route path="/" component={Home} exact />
        <Route path="/shop" component={Shop} />
      </Switch>
  )
}

export default Routes

创建导航菜单

  • 新建一个Navgation组件作为导航菜单组件,并把这个组件添加给公共组件中(Layout)
  • 使用ant design组件的导航菜单,如果发现没有样式可能是没有导入antd的样式文件(上面有写)
  • 设置导航为横向显示(默认纵向)
  • 开启允许选中,即点击导航显示选中效果,开启选中需要使用路由信息
// src/Components/core/Navgation.js  顶部导航组件

import React from 'react'
// 引入 导航组件
import { Menu } from 'antd'
// 引入获取 stre 状态方法
import { useSelector} from 'react-redux'
// 引入跳转路由方法
import { Link } from 'react-router-dom'

// 顶部导航组件
function Navgation() {
  // 获取状态 - 路由信息
  const state = useSelector(state => state.router)
  return (
    <div>
      {/* 使用导航组件,mode - 横向,selectedKeys - 设置选中(数组中使用当前路由信息) */}
      <Menu mode='horizontal' selectedKeys={[state.location.pathname]}>
        {/* key 对应着上面的 selectedKeys */}
        <Menu.Item key='/'>
          {/* 设置点击跳转路由 */}
          <Link to='/'>首页</Link>
        </Menu.Item>
        <Menu.Item key='/shop'>
          <Link to='/shop'>商品列表</Link>
        </Menu.Item>
        <Menu.Item key='/logIn'>
          <Link to='/logIn'>登录</Link>
        </Menu.Item>
        <Menu.Item key='/logUp'>
          <Link to='/logUp'>注册</Link>
        </Menu.Item>
      </Menu>
    </div>
  )
}

export default Navgation
// src/Components/core/Layout.js  布局容器添加公共组件

import React from 'react'
// 引入公共使组件
import Navgation from './Navgation'

// 公共组件(布局)容器,解构出子组件内容,children即是子组件传递过来的内容
function Layout({children}) {
  return (
    <div>
      {/* 公共组件 */}
      <Navgation />
      {/* 直接使用子组件内容 */}
      {children}
    </div>
  )
}

export default Layout

创建页头

使用页头组件

同样是是每个页面都有的,所以放置到Layout公共组件中

每个页面在展示的时候要传递给layout组件要显示的title和subtitle

可以使用老师提供的css文件进行样式调整,设置页头类名即可

// src/Components/core/Layout.js  添加页头

import React from 'react'
// 引入公共使组件
import Navgation from './Navgation'
// 引入UI组件 - 页头
import { PageHeader } from 'antd'
// 引入css样式
import '../../style.css'

// 公共组件(布局)容器,解构出子组件内容,children即是子组件传递过来的内容
// title, subTitle 由其他页面组件传递过来
function Layout({children, title, subTitle}) {
  return (
    <div>
      {/* 公共组件 */}
      <Navgation />
      {/* 页头,设置类名、使用title 和 subTitle */}
      <PageHeader className="jumbotron" title={title} subTitle={subTitle} />
      {/* 直接使用子组件内容 */}
      {children}
    </div>
  )
}

export default Layout
// src/Components/core/Home.js  传递页头需要使用的文本

import React from 'react'
// 引入公共组件
import Layout from './Layout'
// 引入获取状态方法
// import {useSelector} from 'react-redux'

function Home() {
  // 创建变量获取状态,从Redux的store中提取数据(state)
  // const state = useSelector(state => state)
  return (
    // 最外层是公共组件,组件是公共组件的子组件
    // 传递 title, subTitle
    <Layout title='首页' subTitle='开始你的选购之旅吧'>
      首页内容
    </Layout>
  )
}

export default Home
// src/Components/core/Shop.js  传递页头需要使用的文本

import React from 'react'
// 引入公共组件
import Layout from './Layout'

function Shop() {
  return (
    // 最外层是公共组件,组件是公共组件的子组件
    // 传递 title, subTitle
    <Layout  title='商品列表' subTitle='开始选购吧'>
      商品列表
    </Layout>
  )
}

export default Shop

构建注册和登录表单

core目录新建登录和注册两个组件

同样登录和注册也需要使用公共组件Layout,同样要传入页头信息

将两个新页面的路由规则添加进路由规则组件

导航菜单也需要添加两个组件的导航(前面添加过了)

登录和注册都使用From表单组件构建

书写完发现不居中于页面,因为每个页面都要剧中与页面,所以在Layout中设置一下页面内容居中

// src/Components/core/Login.js  新建登录组件

import React from 'react'
// 引入公共布局容器
import Layout from './Layout'
// 引入UI组件
import { Form, Input, Button } from 'antd'

// 登录组件
function Login() {
  return (
    // 导出布局容器,传递 title 和 subTitle
    <Layout  title='登录' subTitle='请登录您的账户'>
      {/* 表单 */}
      <Form>
        <Form.Item
          label="电子邮箱"
          name="email"
          // 校验
          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>
    </Layout>
  )
}

export default Login
// src/Components/core/Logup.js  新建注册组件

import React from 'react'
// 导入布局容器
import Layout from './Layout'
// 导入UI组件
import { Form, Input, Button } from 'antd'

// 注册组件
function Logup() {
  return (
    // 导出布局容器,传递 title 和 subTitle
    <Layout  title='注册' subTitle='注册一个账户吧'>
      {/* 表单 */}
      <Form>
        <Form.Item
          label="登录昵称"
          name="name"
          // 校验
          rules={[{ required: true, message: '请输入用户名!' }]}>
          <Input />
        </Form.Item>
        <Form.Item
          label='登录密码'
          name="password"
          rules={[{ required: true, message: '请输入密码!' }]}>
          <Input.Password />
        </Form.Item>
        <Form.Item
          label="电子邮箱"
          name="email"
          rules={[{ required: true, message: '请输入电子邮箱!' }]}>
          <Input />
        </Form.Item>
        <Form.Item wrapperCol={{ offset: 8, span: 16 }}>
          <Button type="primary" htmlType="submit">注册</Button>
        </Form.Item>
      </Form>
    </Layout>
  )
}

export default Logup
// src/Components/Routes.js  路由组件添加登录和注册两个路由

import React from 'react'
// 路由需要使用的方法: BrowserRouter类似于HashRouter,更推荐使用
import { Switch, Route } from 'react-router-dom'
// 引入路由组件
import Home from './core/Home'
import Login from './core/Login'
import Logup from './core/Logup'
import Shop from './core/Shop'

function Routes() {
  return (
    // 包裹开启路由功能(这里因为处理路由到store的时候已经做了,这里不需要了)
      // {/* 避免书写错误出现相同路由 */}
      <Switch>
        {/* 路由,第一个路由为默认路由,精准匹配 */}
        <Route path="/" component={Home} exact />
        <Route path="/shop" component={Shop} />
        <Route path="/login" component={Login} />
        <Route path="/logup" component={Logup} />
      </Switch>
  )
}

export default Routes
// src/Components/core/Layout.js  布局容器设置整体左右边距

import React from 'react'
// 引入公共使组件
import Navgation from './Navgation'
// 引入UI组件 - 页头
import { PageHeader } from 'antd'
// 引入css样式
import '../../style.css'

// 公共组件(布局)容器,解构出子组件内容,children即是子组件传递过来的内容
// title, subTitle 由其他页面组件传递过来
function Layout({children, title, subTitle}) {
  return (
    <div>
      {/* 公共组件 */}
      <Navgation />
      {/* 页头,设置类名、使用title 和 subTitle */}
      <PageHeader className="jumbotron" title={title} subTitle={subTitle} />
      {/* 直接使用子组件内容,设置两侧边距 */}
      <div style={{width: '85%', margin: '0 auto'}}>{children}</div>
    </div>
  )
}

export default Layout

实现注册Redux工作流

创建action和React

安装redux-actions@2.6.5简化代码 - npm i redux-actions@2.6.5

store目录下创建actions目录容纳action指令

actions目录下单独创建一个文件书写注册相关指令

创建四条指令(cerateAction):发起注册请求、注册成功、注册失败、重置注册状态(离开注册状态时使用)

reducers目录下新建用于注册的reducer

创建注册相关状态数据{是否正在发送请求,请求是否完成,请求是否成功,请求失败原因}

将注册相关reducer进行合并

// src/store/actions/logup.action.js 新建文件书写注册相关指令

// 引入方法简化 Redex 书写
import { createAction } from 'redux-actions'

// 四条指令
// 发起注册请求
export const logup = createAction('logup')
// 注册成功
export const logup_success = createAction('logup_success')
// 注册失败
export const logup_fail = createAction('logup_fail')
// 重置注册状态(离开注册页面时恢复注册状态)
export const logup_reset = createAction('logup_reset')
// src/store/reducers/logup.reducer.js 新建文件书写注册相关状态reducer

// 引入方法简化 Redux
import { handleActions } from 'redux-actions'
// 引入需要的指令
import { logup, logup_fail, logup_reset, logup_success } from '../actions/logup.action'

// 创建注册组件状态
const logupDate = {
  loadering: false,  // 是否正在发起请求
  loader: false,  // 是否请求完毕
  success: false,  // 请求是否成功
  message: ''  // 请求失败提示结果
}

// 使用方法 简化 Redux
const logupReducer = handleActions({
  // 不同结果返回不同的状态
  [logup]: (state, action) => ({loadering: true, loader: false, success: false, message: '' }),
  [logup_success]: (state, action) => ({loadering: false, loader: true, success: true, message: '' }),
  // 请求失败需要给指令传递一条数据放在 action.payload 中
  [logup_fail]: (state, action) => ({loadering: false, loader: true, success: false, message: action.payload }),
  [logup_reset]: (state, action) => ({loadering: false, loader: false, success: false, message: '' })
}, logupDate)

export default logupReducer
// src/store/reducers/index.js  将注册相关指令合并进去

// 引入模块方法
import { connectRouter } from "connected-react-router"
// 导入 reducer 合并方法
import { combineReducers } from "redux"
// 引入要合并的 reducer
import testReducer from "./text"
import logupReducer from './logup.reducer'

// 合并全部 reducer
// createRootReducer是一个函数,不要更改名字
const createRootReducer = history => combineReducers({
  test: testReducer,
  // 路由信息
  router: connectRouter(history),
  logup: logupReducer
})

// 导出
export default createRootReducer

启动后端服务器

数据库文件夹存储在课程文件夹 - server中

  • 安装依赖:npm i
  • 启动mongodb
  • 启动服务器:npm start

接口文档存放在课程文件夹的PPT文件夹中

配置saga中间件

store目录中新建sagas目录存放saga文件

sagas目录新建注册相关的saga文件

拦截发起注册请求指令,进行axios请求,利用try-catch进行分支判断,请求成功触发请求成功指令,请求失败触发请求失败指令

sagas目录新建rootsaga文件,用来合并saga(all方法)

在store下的index.js中注册中间件

// src/store/sagas/logup.saga.js  新建文件书写注册相关saga

// 注册组件 saga 引入方法
import {takeEvery ,put} from 'redux-saga/effects'
// 引入所有需要的指令
import { logup, logup_fail, logup_success } from '../actions/logup.action'
// 引入 axios 发情请求
import axios from 'axios'
// 引入当前环境请求地址 前缀
import { API } from '../../config'

// 拦截到请求指令后触发回调函数
function* handleLogup(action) {
  // 利用 try-catch 进行条件判断
  try {
    // axios 发起注册请求
    yield axios.post(`${API}/signup`, action.payload)
    console.log('注册成功')
    // 请求成功触发请求成功指令
    yield put(logup_success())
  } catch (error) {
    console.log('注册失败')
    // 请求失败触发请求失败指令
    yield put(logup_fail({mesage: '注册失败'}))
  }
}

// saga拦截请求发起指令,并触发相应函数
export default function* logupSaga() {
  yield takeEvery(logup, handleLogup)
}
// src/store/sagas/root.saga.js  合并saga

// 引入 all 方法合并saga
import { all } from 'redux-saga/effects'
// 引入需要合并的 saga
import logupSaga from './logup.saga'

// 调用方法合并 saga 并导出
export default function* rootSaga() {
  yield all([logupSaga()])
}
// src/store/index.js  启用saga中间件并且实现浏览器插件功能

// 创建 store 方法
import { createStore, applyMiddleware } from "redux";
// 要使用的 reducer
import createRootReducer from "./reducers"
import { createBrowserHistory } from 'history'
import { routerMiddleware } from 'connected-react-router'
// 引入 saga 中间件
import reduxSaga from 'redux-saga'
// 引入合并后的 saga
import rootSaga from "./sagas/root.saga"
// redux-devtools-extension 浏览器插件配置方法
import { composeWithDevTools } from 'redux-devtools-extension'

// 创建 saga
const saga = reduxSaga()
// 导出 history
export const history = createBrowserHistory()

// 创建 store 仓库
const store = createStore(
  createRootReducer(history),
  // 中间件监控路由变化
  // composeWithDevTools 包裹着中间件实现浏览器插件功能
  composeWithDevTools(applyMiddleware(routerMiddleware(history), saga))
)
// saga执行
saga.run(rootSaga)

export default store

验证注册reducer流程是否能够跑通

在注册组件中验证(useDispatch)

Form组件触发onFinish事件后触发发起注册指令

触发指令的时候传递用户填写的数据

这可能需要使用MongoDB Compass软件查看

如果浏览器插件redux-devtools-extension无法查看redux状态,需要在store中进行相关设置

// 引入 composeWithDevTools 包裹中间件得以使用 Redux Devtools 插件功能
import { composeWithDevTools } from 'redux-devtools-extension'

export const store = createStore(
  数据,
  // 使用方法包裹中间件即可
  composeWithDevTools(applyMiddleware(...))
)
// src/Components/core/Logup.js  注册组件获取指令并在提交时触发注册请求指令

import React from 'react'
// 导入布局容器
import Layout from './Layout'
// 导入UI组件
import { Form, Input, Button } from 'antd'
// 引入方法用来获取指令
import { useDispatch } from 'react-redux'
// 引入指令名称
import { logup } from '../../store/actions/logup.action'

// 注册组件
function Logup() {
  // 获取所有指令
  const dispatch = useDispatch()
  // 表单提交方法,表单会自动把用户填写的数据作为参数传递过来 - value
  const handleOnFinish = (value) => {
    // 直接执行请求注册指令,并把数据传递给指令
    dispatch(logup(value))
  }
  return (
    // 导出布局容器,传递 title 和 subTitle
    <Layout  title='注册' subTitle='注册一个账户吧'>
      {/* 表单,添加表单提交事件 */}
      <Form onFinish={handleOnFinish}>
        <Form.Item
          label="登录昵称"
          name="name"
          // 校验
          rules={[{ required: true, message: '请输入用户名!' }]}>
          <Input />
        </Form.Item>
        <Form.Item
          label='登录密码'
          name="password"
          rules={[{ required: true, message: '请输入密码!' }]}>
          <Input.Password />
        </Form.Item>
        <Form.Item
          label="电子邮箱"
          name="email"
          rules={[{ required: true, message: '请输入电子邮箱!' }]}>
          <Input />
        </Form.Item>
        <Form.Item wrapperCol={{ offset: 8, span: 16 }}>
          <Button type="primary" htmlType="submit">注册</Button>
        </Form.Item>
      </Form>
    </Layout>
  )
}

export default Logup

状态和注册组件结合

目标:

  • 正在发送注册请求 - 显示loading
  • 注册成功 - 清空表单、显示成功提示信息
  • 注册失败 - 显示失败提示信息
  • 离开页面 - 重置注册状态

在组件中获取注册状态信息(useSelector)

设置一个信号值,用来判断当前是否正在请求注册,利用加载中组件实现loading效果

清空表单需要获取表单对象 ,调用相关方法(From.useFrom),在from上添加from属性(from)

重置表单(from.resetFields)

注册成功需要监控两个属性(useEffect)

注册成功调用方法重置表单,然后创建方法弹出提示,使用结果组件,结果组件中使用按钮,点击后可跳转登录界面(Link)

如果注册失败同样需要使用结果组件,注册失败信息从状态里拿,这里把服务端返回信息填进去

把整个表单都放在一个单独的函数中,然后返回表单即可

当离开注册页面如果不重置注册状态会导致上次注册的状态影响当前注册

直接给页面添加监控,当页面加载的时候直接触发重置指令,这样就会让状态重置

// src/Components/core/Logup.js  注册组件,与注册状态进行绑定结合,根据状态实现不同效果

import React, { useEffect } from 'react'
// 导入布局容器
import Layout from './Layout'
// 导入UI组件
import { Form, Input, Button, Spin, Result } from 'antd'
// 引入方法用来获取指令
import { useDispatch, useSelector } from 'react-redux'
// 引入指令名称
import { logup, logup_reset } from '../../store/actions/logup.action'
// 引入方法: 路由跳转
import { Link } from 'react-router-dom'

// 注册组件
function Logup() {
  // 获取所有指令
  const dispatch = useDispatch()
  // 获取状态
  const {loadering, loader, success, message} = useSelector(state => state.logup)
  // 获取表单元素
  const [form] = Form.useForm()
  // 生命周期函数
  useEffect(() => {
    // 如果注册成功 - 重置表单
    if (loader && success) form.resetFields()
  }, [loader, success]) // eslint-disable-line 
  // 生命周期函数
  useEffect(() => {
    return () => {
      // 组件销毁的时候触发重置状态指令
      dispatch(logup_reset())
    }
  }, [])  // eslint-disable-line 
  // 请求注册过程中的等待中效果
  const loadingShow = () => {
    if (loadering) return <Spin />
  }
  // 注册成功函数
  const successShow = () => {
    // 注册成功显示结果
    if (loader && success) return (
      <Result
        status="success"
        title="注册成功"
        extra={[
          // 添加登录按钮跳转登录
          <Button type="primary" key="console"><Link to='/login'>登录</Link></Button>
        ]}
      />
    )
  }
  // 注册失败函数
  const FailShow = () => {
    // 注册失败显示结果
    if (loader && !success) return (
      <Result
        status="warning"
        title="注册失败"
        subTitle={message.message}
      />
    )
  }
  // 表单提交方法,表单会自动把用户填写的数据作为参数传递过来 - value
  const handleOnFinish = (value) => {
    // 直接执行请求注册指令,并把数据传递给指令
    dispatch(logup(value))
  }
  // 将整个表单单独封装起来
  const jsx = () => (
    <Form form={form} onFinish={handleOnFinish}>
      {/* 表单初始化的时候执行三个函数 */}
      {loadingShow()}
      {successShow()}
      {FailShow()}
      <Form.Item
        label="登录昵称"
        name="name"
        // 校验
        rules={[{ required: true, message: '请输入用户名!' }]}>
        <Input />
      </Form.Item>
      <Form.Item
        label='登录密码'
        name="password"
        rules={[{ required: true, message: '请输入密码!' }]}>
        <Input.Password />
      </Form.Item>
      <Form.Item
        label="电子邮箱"
        name="email"
        rules={[{ required: true, message: '请输入电子邮箱!' }]}>
        <Input />
      </Form.Item>
      <Form.Item wrapperCol={{ offset: 8, span: 16 }}>
        <Button type="primary" htmlType="submit">注册</Button>
      </Form.Item>
    </Form>
  )
  return (
    // 导出布局容器,传递 title 和 subTitle
    <Layout  title='注册' subTitle='注册一个账户吧'>
      {/* 调用方法插入Form表单 */}
      {jsx()}
    </Layout>
  )
}

export default Logup

// src/store/sagas/logup.saga.js  修改注册saga,让状态可以获取到失败的信息

// 注册组件 saga 引入方法
import {takeEvery ,put} from 'redux-saga/effects'
// 引入所有需要的指令
import { logup, logup_fail, logup_success } from '../actions/logup.action'
// 引入 axios 发情请求
import axios from 'axios'
// 引入当前环境请求地址 前缀
import { API } from '../../config'

// 拦截到请求指令后触发回调函数
function* handleLogup(action) {
  // 利用 try-catch 进行条件判断
  try {
    // axios 发起注册请求
    yield axios.post(`${API}/signup`, action.payload)
    console.log('注册成功')
    // 请求成功触发请求成功指令
    yield put(logup_success())
  } catch (error) {
    // 请求失败触发请求失败指令,指令把请求失败返回的错误信息传递过去
    yield put(logup_fail({message: error.response.data.error}))
  }
}

// saga拦截请求发起指令,并触发相应函数
export default function* logupSaga() {
  yield takeEvery(logup, handleLogup)
}

用户登录

实现用户登录,并在登录后存储用户信息(localStorage.setItem),需要转换字符串

表单发生提交行为(onFinish)向接口发送登录请求

登录成功后需要跳转,判断用户角色,普通用户 - 0,管理员 - 1跳转不同页面

新建跳转实例(useHistory),最终使用push方法进行跳转

// src/Components/core/Login.js  登录组件进行登录验证、数据本地化、路由跳转

import React from 'react'
// 引入公共布局容器
import Layout from './Layout'
// 引入UI组件
import { Form, Input, Button, notification } from 'antd'
// 引入axios
import axios from 'axios'
// 引入 axios 前缀
import { API } from '../../config'
// 引入方法 路由跳转
import { useHistory } from 'react-router-dom'

// 登录组件
function Login() {
  // 创建路由对象
  const history = useHistory()
  // 登录表单提交方法
  const onFinish = async (value) => {
    // 使用 try - catch 进行错误捕捉
    try {
      // 请求登录接口,传递参数
      const { data } = await axios.post(`${API}/signin`, value)
      // 将数据存储到浏览器
      localStorage.setItem('lagou', JSON.stringify(data))
      // 根据返回用户角色跳转不同路由
      if (data.user.role === 0) {
        history.push('./user/dashboard')
      } else if (data.user.role === 1) {
        history.push('./admin/dashboard')
      }
    } catch (error) {
      // 失败则弹出提示信息
      notification.open({
        message: '登录失败',
        description: error.response.data.error
      })
    }
  }
  return (
    // 导出布局容器,传递 title 和 subTitle
    <Layout  title='登录' subTitle='请登录您的账户'>
      {/* 表单 */}
      <Form onFinish={onFinish}>
        <Form.Item
          label="电子邮箱"
          name="email"
          // 校验
          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>
    </Layout>
  )
}

export default Login

根据用户登录状态显示菜单栏选项

当前用户没有登录显示登录和注册,否则不显示

在导航菜单组件中新建两个方法

用来返回登录注册组件

用来返回dashboard组件

在src目录下创建一个专门存储公共方法的目录,新建文件书写哦判断用户是否登录方法

标题根据方法返回结果判断是否登录,从而执行不同方法返回不同结构

另外根据返回用户角色的不同,从而修改dashboard路由地址

// src/method/loginOrNot.js  新建文件存储公共方法,添加判断是否登录方法

// 判读是否登陆
export const loginOrNot = () => {
  // 获取本地登录信息
  const data = JSON.parse(localStorage.getItem('lagou'))
  // 如果有登录信息表示登录,直接返回登录信息
  if (data) return data
  // 没有登录信息返回 false
  return false
}
// src/Components/core/Navgation.js  引入上面的方法根据方法动态添加不同的结构

import React from 'react'
// 引入 导航组件
import { Menu } from 'antd'
// 引入获取 stre 状态方法
import { useSelector} from 'react-redux'
// 引入跳转路由方法
import { Link } from 'react-router-dom'
// 引入方法 - 判断是否登陆
import { loginOrNot } from '../../method/loginOrNot'

// 顶部导航组件
function Navgation() {
  // 调用方法返回信息(如果登录会返回登录信息,未登录返回false)
  const data = loginOrNot()
  // 获取状态 - 路由信息
  const state = useSelector(state => state.router)
  // 返回注册、登录结构
  const loginRegisterShow = () => {
    return (
      <>
        <Menu.Item key='/logIn'>
          <Link to='/logIn'>登录</Link>
        </Menu.Item>
        <Menu.Item key='/logUp'>
          <Link to='/logUp'>注册</Link>
        </Menu.Item>
      </>
    )
  }
  // 返回登录后的 dashboard 结构
  const DashboardShow = () => {
    // 根据返回的用户类型返设置不同的值
    const keyAndTo = data.user.role ? '/admin/dashboard' : '/user/dashboard'
    return (
      <>
        {/* 直接使用动态参数 */}
        <Menu.Item key={keyAndTo}>
          <Link to={keyAndTo}>仪表盘</Link>
        </Menu.Item>
      </>
    )
  }
  return (
    <div>
      {/* 使用导航组件,mode - 横向,selectedKeys - 设置选中(数组中使用当前路由信息) */}
      <Menu mode='horizontal' selectedKeys={[state.location.pathname]}>
        {/* key 对应着上面的 selectedKeys */}
        <Menu.Item key='/'>
          {/* 设置点击跳转路由 */}
          <Link to='/'>首页</Link>
        </Menu.Item>
        <Menu.Item key='/shop'>
          <Link to='/shop'>商品列表</Link>
        </Menu.Item>
        {/* 动态结构,根据是否登陆判断 */}
        { data ? DashboardShow() : loginRegisterShow()}
      </Menu>
    </div>
  )
}

export default Navgation

创建受保护的dashboard组件

用户未登录情况下不允许访问

  • 在admin目录下新建两个dashboard组件,分别对应普通用户和管理员
  • 给两个组件添加路由配置
  • 新建一个组件,作为路由守卫包裹组件(记得传递路由信息),如果没登录调准到登录页
  • 两个组件都同理,都要新建一个路由守卫,admindashboard需要判断角色是否为管理员
// src/Components/admin/UserDashboard.js  新建用户信息组件

// 布局容器
import Layout from '../core/Layout'
import React from 'react'

// 用户信息组件 - 仪表盘
function UserDashboard() {
  return (
    // 使用布局容器,传递数据
    <Layout title='用户信息' subTitle='欢迎您,亲爱的用户'>
      普通用户
    </Layout>
  )
}

export default UserDashboard
// src/Components/admin/AdminDashboard.js  新建管理员信息组件

import React from 'react'
// 布局容器
import Layout from '../core/Layout'

// 管理员信息组件 - 仪表盘
function AdminDashboard() {
  return (
    // 使用布局容器,传递数据
    <Layout title='用户信息' subTitle='辛苦了,管理员同学'>
      管理员
    </Layout>
  )
}

export default AdminDashboard
// src/Components/admin/GuardUserDashboard.js  用户信息组件路由守卫

import React from 'react'
import { Redirect, Route } from 'react-router-dom/cjs/react-router-dom.min'
import { loginOrNot } from '../../method/loginOrNot'

// 用户信息路由守卫组件,参数使用解构赋值
function GuardUserDashboard({component: Component, ...test}) {
  // 获取登录信息
  const data = loginOrNot()
  return (
    <>
      {/* 路由守卫 */}
      <Route {...test} render={ (props) => {
        // 如果登录成功并且角色为普通用户才能继续跳转
        if(data && data.user.role === 0) {
          return <Component {...props} />
        }
        // 否色跳转登录
        return <Redirect to='/login' />
      }} />
    </>
  )
}

export default GuardUserDashboard
// src/Components/admin/GuardAdminDashboard.js  管理员信息路由守卫

import React from 'react'
import { Redirect, Route } from 'react-router-dom/cjs/react-router-dom.min'
import { loginOrNot } from '../../method/loginOrNot'

// 管理员信息路由守卫组件
function GuardAdminDashboard({component: Component, ...test}) {
  // 获取登录信息
  const data = loginOrNot()
  return (
    <>
    {/* 路由守卫 */}
      <Route {...test} render={ (props) => {
        // 如果登录并且角色为管理员才能继续跳转
        if(data && data.user.role) return <Component {...props} />
        // 否则返回登录
        return <Redirect to='/login' />
      }} />
    </>
  )
}

export default GuardAdminDashboard
// src/Components/Routes.js  路由组件添加两个用户信息组件并使用路由守卫

import React from 'react'
// 路由需要使用的方法: BrowserRouter类似于HashRouter,更推荐使用
import { Switch, Route } from 'react-router-dom'
import AdminDashboard from './admin/AdminDashboard'
import GuardAdminDashboard from './admin/GuardAdminDashboard'
import GuardUserDashboard from './admin/GuardUserDashboard'
import UserDashboard from './admin/UserDashboard'
// 引入路由组件
import Home from './core/Home'
import Login from './core/Login'
import Logup from './core/Logup'
import Shop from './core/Shop'

function Routes() {
  return (
    // 包裹开启路由功能(这里因为处理路由到store的时候已经做了,这里不需要了)
      // {/* 避免书写错误出现相同路由 */}
      <Switch>
        {/* 路由,第一个路由为默认路由,精准匹配 */}
        <Route path="/" component={Home} exact />
        <Route path="/shop" component={Shop} />
        <Route path="/login" component={Login} />
        <Route path="/logup" component={Logup} />
        {/* 使用路由守卫 */}
        <GuardUserDashboard path="/user/dashboard" component={UserDashboard} />
        <GuardAdminDashboard path="/admin/dashboard" component={AdminDashboard} />
      </Switch>
  )
}

export default Routes

管理员组件添加链接和管理员信息

  • 使用组件库栅格系统,左侧4右侧20,左右可以设置间隙(但这里不需要设置)
  • 左侧使用导航菜单添加三个链接:添加分类、添加商品、订单列表(使用函数导出)
  • 给链接添加图标,使用组件库
  • 在连接上面添加标题,使用组件库的排版功能
  • 右侧使用描述列表组件展示管理员信息(使用函数导出)
// src/Components/admin/AdminDashboard.js  填充管理员组件内容

import { Col, Row, Menu, Typography, Descriptions } from 'antd'
import React from 'react'
// 布局容器
import Layout from '../core/Layout'
import { PlusOutlined, AppstoreAddOutlined, UnorderedListOutlined } from '@ant-design/icons'
import { loginOrNot } from '../../method/loginOrNot'
const { Title } = Typography

// 管理员信息组件 - 仪表盘
function AdminDashboard() {
  // 左侧展示函数
  const leftShow = () => {
    return (
      <>
        <Title level={5}>功能管理</Title>
        <Menu>
          <Menu.Item><PlusOutlined />添加分类</Menu.Item>
          <Menu.Item><AppstoreAddOutlined />添加商品</Menu.Item>
          <Menu.Item><UnorderedListOutlined />订单列表</Menu.Item>
        </Menu>
      </>
    )
  }
  // 右侧展示函数
  const rightShow = () => {
    // 获取用户登录信息
    const {user:{name, email}} = loginOrNot()
    return (
      <Descriptions title="个人信息" bordered>
        <Descriptions.Item label="昵称">{name}</Descriptions.Item>
        <Descriptions.Item label="电子邮箱">{email}</Descriptions.Item>
        <Descriptions.Item label="角色">管理员</Descriptions.Item>
      </Descriptions>
    )
  }
  return (
    // 使用布局容器,传递数据
    <Layout title='用户信息' subTitle='辛苦了,管理员同学'>
      {/* 行 */}
      <Row>
        {/* 列,调用函数渲染 */}
        <Col span={4}>{leftShow()}</Col>
        <Col span={20}>{rightShow()}</Col>
      </Row>
    </Layout>
  )
}

export default AdminDashboard

创建添加分类组件

admin目录新建添加分类组件,引入布局容器,添加路由规则,使用路由守卫

添加点击跳转添加分类路由

添加form表单,表单添加提交事件

下面一起实现

实现添加分类功能

使用axios发起请求,先判断是否存在数据

注意这里需要传递token和用户ID

添加成功提示用户 - message

添加一个按钮可以返回管理员页面

// src/Components/Routes.js  路由添加添加分类,使用路由守卫

import React from 'react'
// 路由需要使用的方法: BrowserRouter类似于HashRouter,更推荐使用
import { Switch, Route } from 'react-router-dom'
import AddSort from './admin/AddSort'
import AdminDashboard from './admin/AdminDashboard'
import GuardAdminDashboard from './admin/GuardAdminDashboard'
import GuardUserDashboard from './admin/GuardUserDashboard'
import UserDashboard from './admin/UserDashboard'
// 引入路由组件
import Home from './core/Home'
import Login from './core/Login'
import Logup from './core/Logup'
import Shop from './core/Shop'

function Routes() {
  return (
    // 包裹开启路由功能(这里因为处理路由到store的时候已经做了,这里不需要了)
      // {/* 避免书写错误出现相同路由 */}
      <Switch>
        {/* 路由,第一个路由为默认路由,精准匹配 */}
        <Route path="/" component={Home} exact />
        <Route path="/shop" component={Shop} />
        <Route path="/login" component={Login} />
        <Route path="/logup" component={Logup} />
        {/* 使用路由守卫 */}
        <GuardUserDashboard path="/user/dashboard" component={UserDashboard} />
        <GuardAdminDashboard path="/admin/dashboard" component={AdminDashboard} />
        <GuardAdminDashboard path="/admin/addsort" component={AddSort} />
      </Switch>
  )
}

export default Routes
// src/Components/admin/AdminDashboard.js  仪表盘组件点击添加分类跳转路由

import { Col, Row, Menu, Typography, Descriptions } from 'antd'
import React from 'react'
// 布局容器
import Layout from '../core/Layout'
import { PlusOutlined, AppstoreAddOutlined, UnorderedListOutlined } from '@ant-design/icons'
import { loginOrNot } from '../../method/loginOrNot'
import { Link } from 'react-router-dom/cjs/react-router-dom.min'
const { Title } = Typography

// 管理员信息组件 - 仪表盘
function AdminDashboard() {
  // 左侧展示函数
  const leftShow = () => {
    return (
      <>
        <Title level={5}>功能管理</Title>
        <Menu>
          {/* 点击跳转添加分类组件 */}
          <Menu.Item><Link to='/admin/addsort'><PlusOutlined />添加分类</Link></Menu.Item>
          <Menu.Item><AppstoreAddOutlined />添加商品</Menu.Item>
          <Menu.Item><UnorderedListOutlined />订单列表</Menu.Item>
        </Menu>
      </>
    )
  }
  // 右侧展示函数
  const rightShow = () => {
    // 获取用户登录信息
    const {user:{name, email}} = loginOrNot()
    return (
      <Descriptions title="个人信息" bordered>
        <Descriptions.Item label="昵称">{name}</Descriptions.Item>
        <Descriptions.Item label="电子邮箱">{email}</Descriptions.Item>
        <Descriptions.Item label="角色">管理员</Descriptions.Item>
      </Descriptions>
    )
  }
  return (
    // 使用布局容器,传递数据
    <Layout title='用户信息' subTitle='辛苦了,管理员同学'>
      {/* 行 */}
      <Row>
        {/* 列,调用函数渲染 */}
        <Col span={4}>{leftShow()}</Col>
        <Col span={20}>{rightShow()}</Col>
      </Row>
    </Layout>
  )
}

export default AdminDashboard
// src/Components/admin/AddSort.js  新建添加分类组件

import { Form, Input, Button, message } from 'antd'
import axios from 'axios'
import React from 'react'
import { Link } from 'react-router-dom'
import { API } from '../../config'
import { loginOrNot } from '../../method/loginOrNot'
import Layout from '../core/Layout'

// 添加分类组件
function AddSort() {
  // 点击添加分类按钮事件
  const onFinish = value => {
    // 获取用户信息
    const {token, user:{_id}} = loginOrNot()
    // 判断用户是否填写了数据
    if(value.name) {
      // 请求接口,传递参数
      axios.post(`${API}/category/create/${_id}`, value, {
        headers: {
          Authorization: `Bearer ${token}`
        }
      }).then(rse => {
        // 成功弹出提示信息
        message.success(`添加 “${rse.data.name}” 分类成功`)
      })
    }
  }
  return (
    <Layout title='添加分类'>
      {/* 添加提交事件 */}
      <Form onFinish={onFinish}>
        <Form.Item label='分类名称' name='name'>
          <Input />
        </Form.Item>
        <Form.Item>
          <Button type="primary" htmlType="submit">添加分类</Button>
        </Form.Item>
      </Form>
      {/* 返回仪表盘按钮 */}
      <Button><Link to='/admin/dashboard'>返回仪表盘</Link></Button>
    </Layout>
  )
}

export default AddSort

创建添加商品组件

admin目录新建添加商品组件,引入布局容器,添加路由规则,使用路由守卫

添加Form组件,组件中要添加商品封面、商品名称、商品描述、价格、商品分类、数量、书否需要运输

上传产品封面使用上传组件(先写结构后面处理)

获取分类列表

在页面加载后获取分类数据,需要使用Hook钩子函数

还需要存储状态,使用钩子函数

使用axios获取分类,注意需要使用async设置异步函数

在商品分类列表中遍历获取到的列表,添加结构

从老师给的文件中手动倒入一些分类

设置第一个请选择分类为默认显示

实现添加商品功能

处理上传商品封面,点击选择之后会立即提交,我们要阻止立即提交行为(beforUpload - false)

提交之前需要获取用户封面(beforUpload参数)和表单提交的数据(onfinish)

提交之前把所有数据添加进一个formData对象中一起提交

上传提交数据,需要使用登录数据

添加成功后显示提示

清空表单

自动跳转回用户页面

// src/index.js  关闭严格模式,避免某些报错

import React from 'react'
import ReactDOM from 'react-dom'
// 引入路由组件
import Routes from './Components/Routes'
// 引入方法传递 store 仓库
import { Provider } from 'react-redux'
// 引入 store
import store, { history }  from './store'
import { ConnectedRouter } from 'connected-react-router'


ReactDOM.render(
  // 取消严格模式,避免报错
  // <React.StrictMode>
    // {/* 把 store 仓库传递下去,正式启用 */}
    <Provider store={store}>
      {/* 包装实现监控路由更改 */}
      <ConnectedRouter history={history}>
        {/* 使用路由组件替代原本的APP */}
        <Routes />
      </ConnectedRouter>
    </Provider>,
  // </React.StrictMode>,
  document.getElementById('root')
)
// src/Components/Routes.js  路由组件添加新配置信息 - 添加商品

import React from 'react'
// 路由需要使用的方法: BrowserRouter类似于HashRouter,更推荐使用
import { Switch, Route } from 'react-router-dom'
import AddSort from './admin/AddSort'
import AddWares from './admin/AddWares'
import AdminDashboard from './admin/AdminDashboard'
import GuardAdminDashboard from './admin/GuardAdminDashboard'
import GuardUserDashboard from './admin/GuardUserDashboard'
import UserDashboard from './admin/UserDashboard'
// 引入路由组件
import Home from './core/Home'
import Login from './core/Login'
import Logup from './core/Logup'
import Shop from './core/Shop'

function Routes() {
  return (
    // 包裹开启路由功能(这里因为处理路由到store的时候已经做了,这里不需要了)
      // {/* 避免书写错误出现相同路由 */}
      <Switch>
        {/* 路由,第一个路由为默认路由,精准匹配 */}
        <Route path="/" component={Home} exact />
        <Route path="/shop" component={Shop} />
        <Route path="/login" component={Login} />
        <Route path="/logup" component={Logup} />
        {/* 使用路由守卫 */}
        <GuardUserDashboard path="/user/dashboard" component={UserDashboard} />
        <GuardAdminDashboard path="/admin/dashboard" component={AdminDashboard} />
        <GuardAdminDashboard path="/admin/addsort" component={AddSort} />
        <GuardAdminDashboard path="/admin/addwares" component={AddWares} />
      </Switch>
  )
}

export default Routes
// src/Components/admin/AdminDashboard.js  添加点击跳转到添加商品路由

import { Col, Row, Menu, Typography, Descriptions } from 'antd'
import React from 'react'
// 布局容器
import Layout from '../core/Layout'
import { PlusOutlined, AppstoreAddOutlined, UnorderedListOutlined } from '@ant-design/icons'
import { loginOrNot } from '../../method/loginOrNot'
import { Link } from 'react-router-dom/cjs/react-router-dom.min'
const { Title } = Typography

// 管理员信息组件 - 仪表盘
function AdminDashboard() {
  // 左侧展示函数
  const leftShow = () => {
    return (
      <>
        <Title level={5}>功能管理</Title>
        <Menu>
          {/* 点击跳转添加分类组件 */}
          <Menu.Item><Link to='/admin/addsort'><PlusOutlined />添加分类</Link></Menu.Item>
          {/* 点击跳转添加商品组件 */}
          <Menu.Item><Link to='/admin/addwares'><AppstoreAddOutlined />添加商品</Link></Menu.Item>
          <Menu.Item><UnorderedListOutlined />订单列表</Menu.Item>
        </Menu>
      </>
    )
  }
  // 右侧展示函数
  const rightShow = () => {
    // 获取用户登录信息
    const {user:{name, email}} = loginOrNot()
    return (
      <Descriptions title="个人信息" bordered>
        <Descriptions.Item label="昵称">{name}</Descriptions.Item>
        <Descriptions.Item label="电子邮箱">{email}</Descriptions.Item>
        <Descriptions.Item label="角色">管理员</Descriptions.Item>
      </Descriptions>
    )
  }
  return (
    // 使用布局容器,传递数据
    <Layout title='用户信息' subTitle='辛苦了,管理员同学'>
      {/* 行 */}
      <Row>
        {/* 列,调用函数渲染 */}
        <Col span={4}>{leftShow()}</Col>
        <Col span={20}>{rightShow()}</Col>
      </Row>
    </Layout>
  )
}

export default AdminDashboard
// src/Components/admin/AddWares.js  新建添加商品组件

import { Form, Input, Button, Upload, Select, Radio, message } from 'antd'
import { UploadOutlined } from '@ant-design/icons'
import { useEffect, useState } from 'react'
import Layout from '../core/Layout'
import axios from 'axios'
import { API } from '../../config'
import { loginOrNot } from '../../method/loginOrNot'
import { useHistory } from 'react-router-dom/cjs/react-router-dom.min'

// 添加商品组件
function AddWares() {
  // 商品分类列表
  let [list, listSet] = useState([])
  // 上传封面
  let [flie, flieSet] = useState()
  // 获取表单
  const [form] = Form.useForm()
  // 获取路由跳转方法
  const history = useHistory()
  // 钩子函数
  useEffect(() => {
    // 处理异步函数,这里把异步过程写成一个单独的函数
    async function loadList() {
      // 发起请求获取全部分类列表
      const { data } =  await axios.get(`${API}/categories`)
      // 将获取到的列表放进状态
      listSet(data)
    }
    // 调用函数执行
    loadList()
  }, [])
  // 上传组件配置
  const props = {
    // 上传相关,参数为上传图片本身
    beforeUpload(e) {
      // 将图片信息存储起来
      flieSet(e)
      // 返回false避免自动上传
      return false
    }
  }
  // 点击提交事件
  const onFinish = value => {
    // 获取用户信息
    const {token, user:{_id}} = loginOrNot()
    // 创建formdata实例,接口要求的
    const formData = new FormData()
    // 将表单数据添加进实例对象
    for(let key in value) {
      formData.append(key, value[key])
    }
    // 将图片信息添加进实例对象
    formData.append('photo', flie)
    // 发起请求添加商品
    axios.post(`${API}/product/create/${_id}`, formData, {
      headers: {
        Authorization: `Bearer ${token}`
      }
    }).then(rse => {
      // 成功弹出提示信息
      message.success(`添加 “${rse.data.name}” 商品成功`)
      // 清空表单
      form.resetFields()
      // 自动跳转回仪表盘
      history.push('/admin/dashboard')
    })
  }
  return (
    <Layout title='添加商品'>
      {/* 设置 form用来获取From组件,initialValues设置默认值,onFinish提交是件憾事 */}
      <Form form={form} initialValues={{category: '-1'}} onFinish={onFinish} >
        <Form.Item >
          {/* 添加props设置参数 */}
          <Upload {...props}>
            <Button icon={<UploadOutlined />}>上传封面</Button>
          </Upload>
        </Form.Item>
        <Form.Item label='名称' name='name'>
          <Input />
        </Form.Item>
        <Form.Item label='描述' name='description'>
          <Input />
        </Form.Item>
        <Form.Item label='价格' name='price'>
          <Input />
        </Form.Item>
        <Form.Item label='所属分类' name='category'>
          <Select>
            <Select.Option value='-1'>请选择分类</Select.Option>
            {/* 遍历列表,创建结构 */}
            {list.map(item => {
              return (<Select.Option key={item._id} value={item._id}>{item.name}</Select.Option>)
            })}
          </Select>
        </Form.Item>
        <Form.Item label='数量' name='quantity'>
          <Input />
        </Form.Item>
        <Form.Item label='是否需要运输' name='shipping'>
          <Radio.Group>
            <Radio value={true}></Radio>
            <Radio value={false}></Radio>
          </Radio.Group>
        </Form.Item>
        <Form.Item>
          <Button type="primary" htmlType="submit">添加商品</Button>
        </Form.Item>
      </Form>
    </Layout>
  )
}

export default AddWares