React框架在作业当中的应用

React框架在作业当中的应用

本篇博客随时会引用这四篇博客中实现的功能与技术。

鉴于基础博客的篇幅都比较长,为了有良好的阅读体验,本文所有的链接点击即可跳转至相应章节

react基础1组件与组合组件

react基础2分页排序搜索+路由

react基础3表单与后端

react基础4授权与部署

根目录

index.js

index.js加载了App,所以也要着重关注

关于react-redux中Provider、connect的解析

阮一峰的博客中也有对Provider的解释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore, applyMiddleware } from 'redux';
import { Provider } from 'react-redux';
import thunkMiddleware from 'redux-thunk';
import loggerMiddleware from './store/middleware/logger';
import rootReducer from './store';
import App from './page/App.jsx';
//Store 就是保存数据的地方,你可以把它看成一个容器。整个应用只能有一个 Store。
/*
Provider是react-redux 提供的一个 React 组件
作用就是把 store 提供给其子组件
*/
const store = createStore(rootReducer, applyMiddleware(thunkMiddleware, loggerMiddleware));

ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root'),
);

setupProxy.js

这个文件的目的是设置一个代理。前端启动端口是3000,后端是8080,这样就会跨域,前端做一个代理,把所有api的接口请求都转发到8080端口。这样就能把所有的请求都转发到你起的后端的服务上了

参考react配置多个代理,跨域

1
2
3
4
5
const proxy = require('http-proxy-middleware');

module.exports = function(app) {
app.use(proxy('/api', { target: 'http://localhost:8080/' }));
};

服务

Api.js

可以使用自定义配置新建一个 axios 实例,自定义实例默认值

比如后端接口的完整的路径是:localhost:8080/api/users/list ,我把所有的请求公共部分,localhost:8080/api 抽出来,放在axios的baseURL里面,这样在代码里面,就不用每次都写localhost:8080/api这样的开头了。降低了代码的耦合度

假如有一天,后端改了api的开头,改了端口号,都不用一个个在代码里面一个个改了

1
2
3
4
5
6
7
import axios from 'axios';
const Api = axios.create({
baseURL: 'http://localhost:8080/api/',
// timeout: ,
});

export default Api;

可以参照axios官方文档

Util.js

这相当于react基础4授权与部署中的authService中存取token的操作

1
2
3
4
5
6
7
8
9
10
11
12
const Util = {
// 登录后将token存到localStorage中
async setToken(token) {
await localStorage.setItem('token', token);
},
// 取出localStorage中的token
getToken() {
return localStorage.getItem('token');
},
};

export default Util;

store目录既Redux理解

学习了

阮一峰的技术博客1

阮一峰的技术博客2

阮一峰的技术博客3

React-Redux流程中,mapStateToProps的理解

关于react-redux中Provider、connect的解析
b站Mosh教程

写下了一篇子博客 浅谈redux1

对redux有了一些了解,于是我把他应用到处理登录这一部分

处理登录目录

actions.js

浅谈redux1 中,引用阮一峰的文字也好,自己写下的理解也罢,对action的作用已经明白了,可以用下图来解释

我这里有两个Actions,一个是关于用户登陆的,一个是关于获取用户信息的。

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
import { USER_LOGIN, GET_USER_INFO } from './action-type';
/*action-type:
export const GET_USER_INFO = 'GET_USER_INFO';
export const USER_LOGIN = 'USER_LOGIN';
*/
import Util from '../../js/Util';
import Api from '../../js/Api';

const actionUserLoginCreator = (type, data) => {
const { name } = data;
return {
type,
data: {
isLogin: true, // 是否登录
name,
},
};
};
/*
判断用户是否登陆成功的action,如果在登陆界面我点击登陆的化,表单中的信息(账号密码)会当作参数
传入,然后我们通过后端Api判断这个账号密码是否合法,合法的话我们就会得到一个response,
response中会有一个data对象
然后我们就把这个对象中的token存在本地storage当中,这样能让用户保持登陆状态。
最后我们发出一个action,让reducer按照我们的要求更新state,并返回给需要的组件。
*/
export const userLogin = (params) => {
return (dispatch) => {
return Api.post('/users/login', params).then((res) => {
if (res.data) {
Util.setToken(res.data.token);
dispatch(actionUserLoginCreator(USER_LOGIN, { name: res.data.name }));
}
});
};
};
/*
获取用户信息的action
这个action就是说首先我从当前的storage中拿到token
然后如果token存在,那么调用Api.get获取到用户的信息,然后调用actionUserLoginCreator函数
dispatch出去,(这里为了让代码的结构更加明了,我把对象抽离出来新建了个函数。),其中:
type:GET_USER_INFO
data:{
name:当前用户信息
isLogin:true 说明当前用户已经登陆。
}

那么如果token没有,说明当前没有用户登录,那么我们就dispatch一个action对象
type还是一样的,但是isLogin为false,而且因为没有登陆,所以没有名字。
type: GET_USER_INFO,
data: {
isLogin: false,
name: null,
},
*/
export const getUserInfo = () => {
const token = Util.getToken();
return (dispatch) => {
if (token) {
return Api.get('/users/info', {
headers: { Authorization: `Bearer ${token}` },
}).then((res) => {
if (res.data.data.name) {
dispatch(actionUserLoginCreator(GET_USER_INFO, { name: res.data.data.name }));
}
});
} else {
dispatch({
type: GET_USER_INFO,
data: {
isLogin: false,
name: null,
},
});
}
};
};

reducer

浅谈redux1 中说过了reducer是一个纯函数。他只接收state和action然后返回对应的更新完毕的state

因为redux的store中保管了所有组件的state,所以需要state的组件会通过某种方式dispatch一个action过来,

reducer接收了以后更新state对象并返回给那个组件。

这个app中有两个actions,他们的名字分别是USER_LOGIN和GET_USER_INFO

因为actions不多,所以我没有必要去拆分reducer。做一个switchcase就可以了。

如果action的type=USER_LOGIN,那么我们就把用户的名字赋给state,并把isLogin设置为true,

GET_USER_INFO亦然

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
import { USER_LOGIN, GET_USER_INFO } from './action-type';
const initState = {
data: {
isLogin: false, // 是否登录
username: null,
},
};
const reducer = (state = initState, action) => {
switch (action.type) {
case USER_LOGIN:
return {
...state,
data: action.data,
};
case GET_USER_INFO:
return {
...state,
data: action.data,
};
default:
return state;
}
};

export default reducer;

中间件

这里需要了解下函数式编程,我在 浅谈redux1:函数式编程 中已经有了初步的介绍

这个就是一个单纯的记录日志的中间件。对整个app用处不大

1
2
3
4
5
6
7
8
9
10
const logger = ({ getState, dispatch }) => (next) => (action) => {
console.group(action.type);
console.log('dispatching:', action);
const result = next(action);
console.log('next state:', getState());
console.groupEnd();
return result;
};

export default logger;

index.js

Redux 提供了一个combineReducers方法,用于 Reducer 的拆分。你只要定义各个子 Reducer 函数,然后用这个方法,将它们合成一个大的 Reducer。

1
2
3
4
5
6
import { combineReducers } from 'redux';
import userData from './login/reducer';

export default combineReducers({
userData,
});

mapStateToProps的理解

因为在代码中很多时候涉及到这两个函数,我就直接在这里解释一下。

1
2
3
4
5
const mapStateToProps = (state) => {
return {
userData: state.userData.data,
};
};

mapStateToProps方法就是容器组件向store声明需要的state的地方,因为我们的store是整个应用只有一份,根据redux的思想通过context可以保证每一个组件都可以从context中获取到store,不需要一级一级的从顶层传递下来。所以,一般容器组件上会有这个函数负责通过context获取到store中想要的state,即从store中获取到的state相当于容器组件从父组件拿到的props,因此,mapStateToProps函数一般只存在于容器组件(或顶层组件)中。

由于,store中的所有state都由reducer来更新,所有一般mapStateToProps方法中需要定义所有的reducer,把所有的reducer的结果都要拿到,保证我们的store中能包含所有的state,也就是我们这个大应用需要的所有数据都能从顶层组件(或容器组件)获取,然后传递下去;

这里,通过这个mapStateToProps,返回了一个本地的state,state中有userData.data

userData.data其实就是reducer中返回的对象中user的名字和登陆与否的状态

mapDispatchToProps的理解

mapDispatchToProps

  • dispatch是必须的参数
  • mapDispatchToProps在组件constructor()中被执行,因而只执行一次
  • mapDispatchToProps为组件提供了用于改变Store状态的方法,并将其定义为组件的prop

mapDispatchToProps用于建立组件跟store.dispatch的映射关系,可以是一个object,也可以传入函数
如果mapDispatchToProps是一个函数,它可以传入dispatch,ownProps, 定义UI组件如何发出action,实际上就是要调用dispatch这个方法
这两个例子

1.就是说如果我调用getUserInfo的话,我就会dispatch一个getUserInfo的action,这个action的作用是获取到用户的信息

2.如果我调用getUserLogin,那么我就会dispatch一个userLogin的action,里面包含了我传入的登录信息params

1
2
3
const mapDispatchToProps = (dispatch) => ({
getUserInfo: () => dispatch(getUserInfo()),
});
1
2
3
const mapDispatchToProps = (dispatch) => ({
getUserLogin: (params) => dispatch(userLogin(params)),
});

connect的理解

mapStateToProps,mapDispatchToProps函数是通过redux的connect函数与react联系在一起的,如connect(mapStateToPropsmapDispatchToProps)(xxxPages),connect返回的是一个函数,这个函数会调用子组件(傻瓜组件、木偶组件),完成容器组件与木偶组件的链接,而mapStateToProps,mapDispatchToProps的返回就是这个木偶组件的props,走这个木偶组件的更新过程

1
2
3
4
export default connect(
mapStateToProps,
mapDispatchToProps,
)(wrappedLoginForm);

provider的理解

在index中,我们有这样一个provider标签

1
2
3
4
5
6
7
//...
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root'),
);

组件部分

实现形式:

admin当中

在普通用户中

在未登录界面

实现逻辑+代码分析

为了方便,我们对源码进行分割

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React from 'react';
import { connect } from 'react-redux';
import { createHashHistory } from 'history';
import './style.scss';
import { Icon, Dropdown, Menu } from 'antd';
import Util from '../../../js/Util';
const history = createHashHistory();
const { Fragment } = React;
class CommonHeader extends React.Component {
constructor(props) {
//在构造器中首先super
super(props);
//state中保存NavBar组件的默认信息,里面有isLogin,默认未登录;name默认为空串
this.state = {
isLogin: false,
name: '',
};
}
//cdm为空
componentDidMount() {}
一些函数的实现

1.history.push()在上文react基础2分页排序搜索+路由中的Programmatic Navigation中的路由部分有详细提到。再次不必多说
2.NavBar中实现的路由跳转:
3.goSign: 跳转到/type其中这个语法是ES6的模板语法,${}中的元素会随着传入的不同而实时渲染,具体的实现功能就是路由跳转,我之所以写成这样是因为它既可以可以跳转到login,也可以跳register 跳到哪里完全看我传入什么type
4.goHome: 跳转到主页
5.logout: 删除当前的token(置空)跳转到主页.其中Util中封装了setToken和getToken两个函数,分别
是用来设置Token和获取Token的函数(也就是localStorage.setItem之类的,在react基础4授权中的Storing the JWT(JSON Token)章节提及)
6.goManage:跳转到/manage也就是管理页面
7.goTarget:跳转到的地方随target决定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

goSign(type) {
history.push(`/${type}`);
}

goHome() {
history.push('/');
}
logout() {
Util.setToken('');
window.location.reload('/');
}
goManage() {
history.push('/manage');
}
goTarget(target) {
history.push(`/${target}`);
}
render()渲染器部分

render渲染器部分,首先析构userData
然后从userData中析构名字和登陆状态
menu就是下拉表单,里面封装了一个按钮,也就是推出,点击退出会删除Token并回到首页

render中返回的组件部分:
1.一开始当然是个Navbar标签,点击它就会回到主页
2.随后是条件渲染,就是只有当用户是admin的时候,才会显示这个按钮,否则是不会显示的。点击这个按钮跳转到用户管理界面,具体界面的逻辑再次不细说。
3.然后是四个表单的条件渲染。就是只有isLogin= true的时候,才会显示这四个表单的按钮.点击这4个按钮会分别跳转到相应的展示页面
4.再然后是右边的部分,这个部分会根据是否登陆而变换。通过判断isLogin?如果未登录,那么就渲染登录和注册这两个按钮,点击后调用goSign传入我想取得地方(login或者register)这就体现了ES6模板化语言的好处,当然直接利用字符串拼接也可以; 如果已经注册,那么再有边显示一个下拉表单: 下拉表单中有上文const的menu对象,也就是退出。未显示下拉表单的时候右边是一个antd库的icon和用户的名字,
就如上文图片中展示的一样

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
 render() {
const { userData } = this.props;
const { name, isLogin } = userData;
const menu = (
<Menu>
<Menu.Item key="0" onClick={() => this.logout()}>
退出
</Menu.Item>
</Menu>
);
return (
<div className="header-nav">
<div className="home-logo">
<div className="logo-text" onClick={() => this.goHome()}>
NavBar
</div>
<div className="tab-bar">
{name === 'admin' ? (
<div className="bar-item" onClick={() => this.goManage()}>
用户管理
</div>
) : null}
{isLogin && (
<div style={{ display: 'flex' }}>
<div className="bar-item" onClick={() => this.goTarget('douban')}>
豆瓣Top250
</div>
<div className="bar-item" onClick={() => this.goTarget('maoyan')}>
猫眼Top100
</div>
<div className="bar-item" onClick={() => this.goTarget('operas')}>
剧集
</div>
<div className="bar-item" onClick={() => this.goTarget('taobao')}>
淘宝咖啡
</div>
</div>
)}
</div>
</div>
<div className="nav-right">
<div className="mine">
{!isLogin ? (
<Fragment>
<div className="sign" onClick={() => this.goSign('login')}>
登录
</div>
<div className="split-line">|</div>
<div className="sign" onClick={() => this.goSign('register')}>
注册
</div>
</Fragment>
) : (
<Dropdown overlay={menu}>
<div className="icon-header">
<Icon type="smile" className="icon-mine icon" theme="twoTone" />
<span className="user-name">{name}</span>
<Icon type="caret-down" className="down icon" />
</div>
</Dropdown>
)}
</div>
</div>
</div>
);
}
}

const mapStateToProps = (state) => {
return {
userData: state.userData.data,
};
};

export default connect(mapStateToProps, null)(CommonHeader);

Layout

Layout组件就是大家的共享主页,也就是说在里面渲染了三个主要部分

1.NavBar

2.所有页面中的元素(继承props)

3.Footer(下图中最后一条暗带)

(忽略HelloWorld)这就是Layout组件的样子

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
import React from 'react';
import './layout.scss';
import { Layout } from 'antd';
import HeaderNav from '../headerNav';
//Header,Footer,Content都是从 antd导入的Layout对象,就不用自己设计css了
const { Header, Footer, Content } = Layout;
render() {
return (
<div>
<Layout className="layout">
<Header className="header">
<div className="content-1200">
<HeaderNav />
</div>
</Header>
<Content className="main">
<div className="content-1200">
{this.props.children}
</div>
</Content>
<Footer className="footer">
<div className="content-1200">
<div className="footer-content" />
</div>
</Footer>
</Layout>
</div>
);
}
export default CommonLayout;

layout.scss表单在这里就不展示了。

Loading

这就是一个当页面加载的时候转圈圈的组件,从antd导入即可

1
2
3
4
5
6
7
8
9
10
11
12
13
import React, { Component } from 'react';
import { Spin } from 'antd';

class Loading extends Component {
render() {
return (
<div>
<Spin />
</div>
);
}
}
export default Loading;

PrivateRoute

这就是一个私有路由,目的是不让未授权的用户得到我的展示页面

实现逻辑在react基础4授权与部署的Extracting PrivateRoute 章节中有详细说明。在这里仅仅展示代码,并不细说逻辑。

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
import React, { Component } from 'react';
import { Route, Redirect } from 'react-router-dom';
import { connect } from 'react-redux';
import { getUserInfo } from '../../../store/login/actions';

class PrivateRoute extends Component {
componentDidMount() {
this.props.getUserInfo();
}

render() {
const { component: Component, userData, ...rest } = this.props;
const { isLogin } = userData;
return (
<Route
{...rest}
render={(props) => {
return isLogin ? (
<Component {...props} />
) : (
<Redirect
to={{
pathname: '/login',
state: { from: props.location },
}}
/>
);
}}
/>
);
}
}

const mapStateToProps = (state, props) => {
return {
userData: state.userData.data,
};
};
const mapDispatchToProps = (dispatch) => ({
getUserInfo: () => dispatch(getUserInfo()),
});

export default connect(mapStateToProps, mapDispatchToProps)(PrivateRoute);

页面部分

App.jsx

App组件是根组件所以我们单独讲。先上代码

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
import React, { Suspense, lazy } from 'react';
import './style.scss';
import { HashRouter as Router, Route, Switch } from 'react-router-dom';
import PrivateRoute from './components/PrivateRoute';
import Loading from './components/loading';

const Home = lazy(() => import('./home'));
const Sign = lazy(() => import('./sign'));
const Register = lazy(() => import('./register'));
const Manage = lazy(() => import('./manage'));
const Douban = lazy(() => import('./douban'));
const Taobao = lazy(() => import('./taobao'));
const Maoyan = lazy(() => import('./maoyan'));
const Operas = lazy(() => import('./operas'));
class App extends React.Component {
render() {
return (
<Router>
<Suspense
fallback={//这里如果等待的化,那么就加载上面所说的Loading组件
<div className="loading">
<Loading />
</div>
}
>
<Switch>
<PrivateRoute exact path="/" component={Home} />
<PrivateRoute exact path="/manage" component={Manage} />
<Route exact path="/login" component={Sign} />
<Route exact path="/register" component={Register} />
<PrivateRoute exact path="/douban" component={Douban} />
<PrivateRoute exact path="/taobao" component={Taobao} />
<PrivateRoute exact path="/maoyan" component={Maoyan} />
<PrivateRoute exact path="/operas" component={Operas} />
</Switch>
</Suspense>
</Router>
);
}
}
export default App;

这里需要讲几个问题:

lazy+Suspense 的作用

react官方文档中我截取了一段

React.lazy 函数能让你像渲染常规组件一样处理动态引入(的组件)。

1
const OtherComponent = React.lazy(() => import('./OtherComponent'));

此代码将会在组件首次渲染时,自动导入包含 OtherComponent 组件的包。

React.lazy 接受一个函数,这个函数需要动态调用 import()。它必须返回一个 Promise,该 Promise 需要 resolve 一个 default export 的 React 组件。

然后应在 Suspense 组件中渲染 lazy 组件,如此使得我们可以使用在等待加载 lazy 组件时做优雅降级(如 loading 指示器等)。

1
2
3
4
5
6
7
8
9
10
11
12
13
import React, { Suspense } from 'react';

const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}

fallback 属性接受任何在组件加载过程中你想展示的 React 元素。你可以将 Suspense 组件置于懒加载组件之上的任何位置。你甚至可以用一个 Suspense 组件包裹多个懒加载组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { Suspense } from 'react';

const OtherComponent = React.lazy(() => import('./OtherComponent'));
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));

function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<section>
<OtherComponent />
<AnotherComponent />
</section>
</Suspense>
</div>
);
}

Switch,exact

react基础2中的Switch有详细提及

PrivateRoute实现逻辑

react基础4授权与部署的Protecting Routes中有详细提及

登录页面与注册页面

登陆页面和注册页面的实现逻辑react基础4授权与部署 中的花了大篇幅说明和实现(从1.1.2. Registering a New User 开始)。

不过还要插一嘴: 在博客中我们没有借助外部的库来实现提交表单,但这里我使用了antd中表单。具体学习博客可以参照

antd中表单提交的基本使用

源码中用到的几个重要的api如下

Form.create

使用Form.create({options})(Forgot)包装组件,包装之后的组件会自动添加this.props.form 属性,该属性包含以下API:

  • getFieldDecorator :用于和表单数据进行双向绑定,设置该表单类型为email,在rules中设置校验规则和提示信息。
  • validateFields:获取输入域的数据和error,用于在提交前的判断。如果!err才可以提交。数据values就是{email: a@qq.com}这样的格式。传递给handleConfirm函数进行提交请求操作。

login.jsx

为了讲解方便,我们把源代码拆成几个部分

handleSubmit部分

首先e.preventDefault()是阻止页面在提交表单的时候重新渲染

然后利用antd中的api,form.validateFields来进行验证,如果没有错的话,就发送一个dispatch一个 action给store,store再通过调用reducer,reducer把我们传入的登录信息渲染到state当中 然后返回,这样再login中就有本地的state信息了。(敲黑板的重点!)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React from 'react';
import { connect } from 'react-redux';
import { Redirect } from 'react-router-dom';
import { userLogin } from '../../store/login/actions';
import { Form, Icon, Input, Button } from 'antd';
import Layout from '../components/layout';
import './style.scss';

class LoginForm extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {}

handleSubmit = (e) => {
e.preventDefault();
this.props.form.validateFields((err, values) => {
if (!err) {
this.props.getUserLogin(values);
}
});
};

render部分

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

render() {
const { getFieldDecorator } = this.props.form;
const { userData } = this.props;
const { isLogin } = userData;
if (isLogin) {
return <Redirect to="/" />;
}
const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 8 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 16 },
},
};
const tailFormItemLayout = {
wrapperCol: {
xs: {
span: 24,
offset: 0,
},
sm: {
span: 22,
offset: 2,
},
},
};
return (
<Layout>
<Form {...formItemLayout} onSubmit={this.handleSubmit} className="login-form">
<Form.Item label="用户名" className="item">
{getFieldDecorator('name', {
rules: [{ required: true }],
})(
<Input
size="large"
prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />}
placeholder="请输入用户名"
/>,
)}
</Form.Item>
<Form.Item label="密码" className="item">
{getFieldDecorator('password', {
rules: [{ required: true }],
})(
<Input
size="large"
prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />}
type="password"
placeholder="请输入密码"
/>,
)}
</Form.Item>
<Form.Item {...tailFormItemLayout} className="item">
<Button size="large" type="primary" htmlType="submit" className="login-form-button">
登录
</Button>
</Form.Item>
</Form>
</Layout>
);
}
}

const mapStateToProps = (state) => {
return {
userData: state.userData.data,
};
};
const mapDispatchToProps = (dispatch) => ({
getUserLogin: (params) => dispatch(userLogin(params)),
});

const wrappedLoginForm = Form.create({ name: 'login' })(LoginForm);

export default connect(
mapStateToProps,
mapDispatchToProps,
)(wrappedLoginForm);

register.jsx

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
import React from 'react';
import { Redirect } from 'react-router-dom';
import { connect } from 'react-redux';
import { createHashHistory } from 'history';
import { Form, Icon, Input, Button, message } from 'antd';
import Layout from '../components/layout';
import './style.scss';
import Api from '../../js/Api';
const history = createHashHistory({ forceRefresh: true });

class LoginForm extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {}

handleSubmit = (e) => {
e.preventDefault();
this.props.form.validateFields((err, values) => {
if (!err) {
Api.post('/users/register', values)
.then((res) => {
if (res.data) {
message.success('register success');
history.push('/login');
}
})
.catch((err) => {
console.log(err);
message.error(err.message);
});
}
});
};

render部分:

表单渲染在上面也说了,这里就不再细讲了

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
  render() {
const { getFieldDecorator } = this.props.form;
const { userData } = this.props;
const { isLogin } = userData;
if (isLogin) {
return <Redirect to="/" />;
}
const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 8 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 16 },
},
};
const tailFormItemLayout = {
wrapperCol: {
xs: {
span: 24,
offset: 0,
},
sm: {
span: 22,
offset: 2,
},
},
};
return (
<Layout>
<Form {...formItemLayout} onSubmit={this.handleSubmit} className="login-form">
<Form.Item label="用户名" className="item">
{getFieldDecorator('name', {
rules: [{ required: true }],
})(
<Input
size="large"
prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />}
placeholder="请输入用户名"
/>,
)}
</Form.Item>
<Form.Item label="密码" className="item">
{getFieldDecorator('password', {
rules: [{ required: true }],
})(
<Input
size="large"
prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />}
type="password"
placeholder="请输入密码"
/>,
)}
</Form.Item>
<Form.Item {...tailFormItemLayout} className="item">
<Button size="large" type="primary" htmlType="submit" className="login-form-button">
注册
</Button>
</Form.Item>
</Form>
</Layout>
);
}
}

const mapStateToProps = (state) => {
return {
userData: state.userData.data,
};
};

const wrappedLoginForm = Form.create({ name: 'login' })(LoginForm);
export default connect(mapStateToProps, null)(wrappedLoginForm);

管理员的用户管理界面

管理员界面和普通用户界面的区别就在于管理员多了一个用户管理页面可以删除别的用户的信息。这里我通过两个jsx文件实现,在主文件中存放UserManage组件,这样的写法让代码的层次更加分明简洁

index.jsx,关于文件中Tabs的使用方法,参考antd的官方文档

1
2
3
4
5
6
7
8
9
10
11
12
13
//...
render() {
return (
<Layout>
<Tabs defaultActiveKey="1">
<TabPane tab="user management" key="1">
<UsersManage key="user" />
</TabPane>
</Tabs>
</Layout>
);
}
//...

user.js

为了讲解方便我们把源码拆分成几个部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React from 'react';
import { Table, Icon, Tooltip, message, Modal } from 'antd';
import Util from '../../js/Util';
import Api from '../../js/Api';
import './style.scss';
const { confirm } = Modal;

class UsersManage extends React.Component {
constructor(props) {
super(props);
this.state = {
userListData: [],
};
}

componentDidMount() {
this.getUserList();
}

getUserList实现逻辑:

首先从获取token ,关于token是什么,在我的博客react基础4授权与部署的这个章节Storing the JWT(JSON Token)以及后来的章节已经说的很清楚了,在此不赘述。

然后通过Api.get方法可参照 axios官方文档 。只有 url 是必需的。如果没有指定 method,请求将默认使用 get 方法。 config 是为请求提供的配置信息

react基础4授权与部署中我们在头部传入一个token的方法是在后端进行修改,然后在头部中获得token然后存储到本地的。

这里我直接利用了axios.get,在请求头中加入token,也就是利用 headers: { Authorization: Bearer ${token} },只有当后端验证了这个token,他才肯把信息传过来嘛,是不是?

然后把response的内容,利用setState方法更新本地state,也就是现在,管理员就可以获得这个应用中用户的表单了

1
2
3
4
5
6
7
8
9
10
11
12
13

getUserList() {
const token = Util.getToken();
Api.get('/users/list', {
headers: { Authorization: `Bearer ${token}` },
}).then((res) => {
if (res.data) {
this.setState({
userListData: res.data.data,
});
}
});
}

删除操作

管理员利用删除操作可以删除用户信息。

这里我用了antd库中的Modal对话框中的确认对话框:Modal.confirm

这里我截取了一段原文档

何时使用?

需要用户处理事务,又不希望跳转页面以致打断工作流程时,可以使用 Modal 在当前页面正中打开一个浮层,承载相应的操作。

另外当需要一个简洁的确认框询问用户时,可以使用 Modal.confirm() 等语法糖方法。

使用 confirm() 可以快捷地弹出确认框。onCancel/onOk 返回 promise 可以延迟关闭。

通过观察confirm对话框,和对应的代码。我们可以清楚的看到titlecontentokText,okType,cancelText分别对应了什么。我就不细说了。

那么我们如果点击cancel,那么相安无事。如果点击onOk,也就是confirm的话,那么就会有这样的操作:

首先获取到我这里管理员的token

其次获得一个url,通过这个url可以重置这个人的信息,把他所有的信息抹去。

利用Api.post()传入token,更新data,完成了删除用户的功能

然后判断返回的结果。这里涉及到后端

1
2
3
4
async delete(req, res) {
const result = await User.findByIdAndRemove(req.params.id);
res.status(201).json({ data: result });
}

如果删除成功,那么就会返回201,附带一个json对象,我们前端通过判断有没有这个对象来判断是否删除成功

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
// 删除
deleteUser = (item) => {
const _this = this;
confirm({
title: 'Are you sure delete?',
content: 'Hope you know what you are doing!',
okText: 'confirm',
okType: 'danger',
cancelText: 'cancel',
onOk() {
const token = Util.getToken();
const postUrl = `/users/${item._id}/delete`;
Api.post(
postUrl,
{},
{
headers: { Authorization: `Bearer ${token}` },
},
)
.then((res) => {
console.log(res);
if (res.data) {
message.success('delete success');
_this.getUserList();
} else {
message.error('fail');
}
})
.catch((err) => {
console.log(err);
});
},
onCancel() {
console.log('Cancel');
},
});
};

render渲染器部分

搞懂了前面的函数逻辑,渲染器部分就很简单了。首先渲染初一张表格来,利用antd中的Table即可

bordered 是否展示外边框和列边框
dataSource 数据数组
rowKey 表格行 key 的取值,可以是字符串或一个函数
columns 列描述数据对象,是 columns 中的一项,Column 使用相同的 API

注意最后一列,是一个条件渲染。如果item.nameadmin的话,渲染-- (因为admin不会自己删除自己) 否则就渲染一个删除icon,也是antd中引用过来的。

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
  render() {
const { userListData } = this.state;
return (
<div>
<Table
bordered
dataSource={userListData}
rowKey={(row) => row._id}
columns={[
{
title: 'id',
dataIndex: '_id',
key: '_id',
align: 'center',
},
{
title: 'name',
dataIndex: 'name',
key: 'name',
align: 'center',
},
{
title: 'auth',
dataIndex: 'auth',
key: 'auth',
align: 'center',
},
{
title: 'handle',
key: 'handle',
align: 'center',
render: (row, item) => (
<div className="person-table-btn">
{item.name !== 'admin' ? (
<Tooltip placement="top" title={'delete'}>
<Icon
type="delete"
theme="twoTone"
twoToneColor="#eb2f96"
className="table-btn-item"
onClick={() => this.deleteUser(item)}
/>
</Tooltip>
) : (
'--'
)}
</div>
),
},
]}
/>
</div>
);
}
}
export default UsersManage;

以豆瓣为例的展示页面

因为4张表格只选择这一张作为我的例子,所以我打算详细讲一下内部逻辑.为了方便起见,我把源码拆分成几个部分分别讲解。

需要了解axios官方文档axios.get

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
import React from 'react';
//首先从antd中导入一些样式,表格,输入框,分页器,表单,按钮等等
import { Table, Input, Button, Select, Pagination, Form, message } from 'antd';
import Layout from '../components/layout';
import './style.scss';
//导入Api和Util Api是后端接出来的,我们前端只管调用就可以了
import Api from '../../js/Api';
import Util from '../../js/Util';
import { connect } from 'react-redux';

const { Option } = Select;

class DouBan extends React.Component {
constructor(props) {
super(props);
/*
state是这张表格本地的信息,current表示当前页面,total表示一张图标显示多少条信息
label就是表头的信息,type就是搜索框的类型
*/

this.state = {
list: [],
current: 1,
total: 10,
label: [
{
label: '电影名字',
value: 'title',
},
{
label: '评分',
value: 'score',
},
{
label: '国家',
value: 'country',
},
{
label: '标签',
value: 'sort',
},
{
label: '导演',
value: 'director',
},
{
label: '演员',
value: 'actor',
},
{
label: '上映时间',
value: 'time',
},
],
type: [
{
label: 'AND',
value: 'AND',
},
{
label: 'OR',
value: 'OR',
},
],
};
}
//render()之后调用cdmhook来初始化表格
componentDidMount() {
this.initTableData();
}

inimitable的逻辑

首先获取到当前的token,然后通过请求头传递到后端去申请数据,后端调用的是findAll函数 (详见后端实现博客: Express框架在后端的应用之getAll,把返回的数据,也就是当前页的信息,对state进行更新。

1
2
3
4
5
6
7
8
9
10
11
12
13
initTableData(current, sort) {
const token = Util.getToken();
Api.get(`/douban/list?page=${current}&sort=${sort}`, {
headers: { Authorization: `Bearer ${token}` },
}).then((res) => {
if (res.data) {
this.setState({
list: res.data.data,
total: res.data.total,
});
}
});
}

onChange的逻辑

这个用在分页上。首先获得token,透过请求头进行传递。认证后将返回的data更新本地state,注意,因为分页了,所以还要更新current 为当前页

1
2
3
4
5
6
7
8
9
10
11
12
13
14
onChange = (page) => {
const token = Util.getToken();
Api.get(`/douban/list?page=${page}`, { headers: { Authorization: `Bearer ${token}` } }).then(
(res) => {
if (res.data) {
this.setState({
list: res.data.data,
total: res.data.total,
current: page,
});
}
},
);
};

提交表单操作

首先获取到token, e.preventDefault();不让重新加载

validateFields是验证表单的函数,这是antd提供的api ,是用于出发表单验证的

首先从values中解构初我们传入的信息

也就是这五个框框。然后写我们的验证逻辑。

  1. label1 不能等于label2
  2. 如果type=AND,就说明两个条件都要满足,type=OR那么只满足一个条件即可.这里我不谈后端如何实现。因为有专门的博客Express框架在后端的应用来介绍,然后把返回的结果更新到本地的state当中
  3. 如果是AND请求,那么我需要请求/douban/getList ,在这里后端会调用 getListByKey函数 ,在Express框架在后端的应用douban.js中有所介绍,返回两者皆满足的信息
  4. 如果是OR请求,那么我需要请求/douban/getListOr,在这里后端会调用getListByOr函数,在Express框架在后端的应用douban.js中有所介绍,返回两者择一满足的信息
  5. 同时,我们要满足选择的时候要排序这个功能,那么我们需要在req中加上sort的类型
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
 
handleSubmit = (e) => {
const { sort } = this.state;
const token = Util.getToken();
e.preventDefault();
this.props.form.validateFields((err, values) => {
if (!err) {
const { label1, value1, type, label2, value2 } = values;
if (label1 === label2) {
message.error('两个查询字段不可重复!');
} else {
const keyWords = `${label1}=${value1}&${label2}=${value2}`;
if (type === 'AND') {
Api.get(`/douban/getList?${keyWords}&sort=${sort}`, {
headers: { Authorization: `Bearer ${token}` },
}).then((res) => {
if (res.data) {
this.setState({
list: res.data.data,
total: res.data.total,
current: 1,
});
}
});
} else {
Api.get(`/douban/getListOr?${keyWords}`, {
headers: { Authorization: `Bearer ${token}&sort=${sort}` },
}).then((res) => {
if (res.data) {
this.setState({
list: res.data.data,
total: res.data.total,
current: 1,
});
}
});
}
}
}
});
};

重置操作

resetFields 会重置整个 Field,因而其子组件也会重新 mount
从而消除自定义组件可能存在的副作用(例如异步数据、状态等等)
然后调用initTableData函数初始化

最后别忘了设置current=1,否则下面的分页器还停留在原来的位置,但是表格已经回到了首页

1
2
3
4
5
reset() {
this.props.form.resetFields();
this.initTableData();
this.state.current = 1;
}

排序操作

后端排序和前端排序不太一样,前端排序的逻辑在react基础2中的sort已经提到了

而后端排序则是前端发送一个request到后端,后端根据前端的sort种类来进行排序,再把数据返回

这里是一个更新State中sort种类的函数,默认sort为null,也就是不排序

后端排序的逻辑在这里实现: 后端排序findAll

1
2
3
4
5
6
7
8
9
10
sort(type) {
this.setState(
{
sort: type,
},
() => {
this.initTableData(null, type);
}
);
}

随后,我们在initTableData函数当中发送我们的sort请求

在排序过程中出现了一些小插曲,一开始我是根据字符串大小排序的。但是修改了变量类型之后,问题解决了

我们看到排序过后,价格是按照数字排序的,而不是字符串大小排序的。

render渲染器

首先解构state和form(antd)

然后渲染选择框:

  1. 第一个请选择查询字段的选择框:getFieldDecorator用户和表单进行双向绑定。 required: true是必须要填(如果想提交) ,然后就是一个选择框,默认占位符是请选择查询的字段。然后把label数组中信息的都渲染到下拉选择表单当中
  2. 第二个是输入框,也是必须要填的,也就是我们想搜索的内容
  3. 第三个框是选择查询类型,这里可以是AND,可以为OR,因为渲染的是type数组
  4. 第四第五个的逻辑和第一第二个一样
  5. 然后是查询和充值按钮,点击后分别触发handleSubmit()reset()
  6. 最后是一个分页器
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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
  render() {
const { getFieldDecorator } = this.props.form;
const { list, total, current, label, type } = this.state;

return (
<div>
<Layout>
{/*
导出部分分为两大组件,分别是搜索框和表格Tablebody部分
*/}
<div className="search">
<Form layout="inline" onSubmit={this.handleSubmit}>
<Form.Item className="item">
{getFieldDecorator('label1', {
rules: [
{
required: true,
},
],
})(
<Select placeholder="请选择查询的字段" style={{ width: '200px' }}>
{label.map((item) => (
<Option value={item.value} key={item.label} label={item.label}>
{item.label}
</Option>
))}
</Select>,
)}
</Form.Item>
<Form.Item className="item">
{getFieldDecorator('value1', {
rules: [{ required: true }],
})(<Input size="default" />)}
</Form.Item>
<Form.Item className="item">
{getFieldDecorator('type', {
rules: [
{
required: true,
},
],
})(
<Select placeholder="查询类型" style={{ width: '150px' }}>
{type.map((item) => (
<Option value={item.value} key={item.label} label={item.label}>
{item.label}
</Option>
))}
</Select>,
)}
</Form.Item>
<Form.Item className="item">
{getFieldDecorator('label2', {
rules: [
{
required: true,
},
],
})(
<Select placeholder="请选择查询的字段" style={{ width: '200px' }}>
{label.map((item) => (
<Option value={item.value} key={item.label} label={item.label}>
{item.label}
</Option>
))}
</Select>,
)}
</Form.Item>
<Form.Item className="item">
{getFieldDecorator('value2', {
rules: [{ required: true }],
})(<Input size="default" />)}
</Form.Item>
<Form.Item className="item">
<Button size="default" type="primary" htmlType="submit">
查询
</Button>
<Button size="default" type="primary" onClick={() => this.reset()} style={{marginLeft: '10px'}}>
重置
</Button>
</Form.Item>
</Form>
<Button
type={sort === "normal" ? "primary" : ""}
style={{ marginRight: "10px" }}
onClick={() => this.sort("normal")}
>
分数从高到低
</Button>
<Button
type={sort === "reverse" ? "primary" : ""}
onClick={() => this.sort("reverse")}
style={{ marginRight: "10px" }}
>
分数从低到高
</Button>
</div>

<div>
<div style={{ display: 'flex' }}></div>
<Table //渲染表格
bordered
dataSource={list}
rowKey={(row) => row._id}
pagination={false}
//列
columns={[
{
title: '电影名字',
dataIndex: 'title',
key: 'title',
align: 'center',
},
{
title: '导演',
dataIndex: 'director',
key: 'director',
align: 'center',
},
{
title: '演员',
dataIndex: 'actor',
key: 'actor',
align: 'center',
// width: 600,
},
{
title: '国家',
dataIndex: 'country',
key: 'country',
align: 'center',
width: 100,
},
{
title: '标签',
dataIndex: 'sort',
key: 'sort',
align: 'center',
width: 200,
},
{
title: '评分',
dataIndex: 'score',
key: 'score',
align: 'center',
},
{
title: '上映时间',
dataIndex: 'time',
key: 'time',
align: 'center',
},
]}
/>
<Pagination
//分页器,在react基础2分页部分有具体实现,这里直接引用了antd中的分页器样式
style={{ marginTop: '20px' }}
defaultPageSize={10}
current={current}
onChange={this.onChange}
total={total}
/>
</div>
</Layout>
</div>
);
}
}

const mapStateToProps = (state) => {
return {
userData: state.userData.data,
};
};

const wrappedDouBanForm = Form.create({ name: 'douban' })(DouBan);

export default connect(mapStateToProps, null)(wrappedDouBanForm);

Operas.jsx 新建表单

剩下的三张表中,猫眼和淘宝咖啡都可以进行一些排序(分数,价格)。 但是Opera表格却没有什么可以排序的

所以我就在operas.jsx中新加入了一个创建新剧的功能

点击新建按钮,我们会跳转一个表单

然后我们输入我们想要创建的新电视剧

比如:

因为单集片长可选填,所以这里就暂时不填

点击创建后,我们就可以在表中查询到这条电视剧信息啦!

实现逻辑

后端逻辑请移步

首先新建一个operaform的表格,用于存放我们的表单

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
import React from 'react';
import { Redirect } from 'react-router-dom';
import { connect } from 'react-redux';
import { createHashHistory } from 'history';
import { Form, Icon, Input, Button, message } from 'antd';
import Layout from '../components/layout';
import './style.scss';
import Api from '../../js/Api';
const history = createHashHistory({ forceRefresh: true });

class MovieForm extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
componentDidMount() {}
// 提交表单操作,我们利用post这个api新建一条信息,然后如果新建成功的话就跳转到operas界面
handleSubmit = (e) => {
e.preventDefault();
this.props.form.validateFields((err, values) => {
if (!err) {
Api.post('/operas/create', values)
.then((res) => {
if (res.data) {
message.success('create successfully');
history.push('/operas');
}
})
.catch((err) => {
console.log(err);
message.error(err.message);
});
}
});

};

render() {
const { getFieldDecorator } = this.props.form;
const { userData } = this.props;
const { isLogin } = userData;
const formItemLayout = {
labelCol: {
xs: { span: 24 },
sm: { span: 8 },
},
wrapperCol: {
xs: { span: 24 },
sm: { span: 16 },
},
};
const tailFormItemLayout = {
wrapperCol: {
xs: {
span: 24,
offset: 0,
},
sm: {
span: 22,
offset: 2,
},
},
};
return (
<Layout>
<h1 className="title">快来创建最新的电视剧吧!</h1>
<Form {...formItemLayout} onSubmit={this.handleSubmit} className="login-form">
<Form.Item label="剧集名字" className="item">
{getFieldDecorator('title', {
rules: [{ required: true }],
})(
<Input
size="large"
placeholder="剧集名字"
/>,
)}
</Form.Item>
<Form.Item label="演员" className="item">
{getFieldDecorator('actors', {
rules: [{ required: true }],
})(
<Input
size="large"
placeholder="演员"
/>,
)}
</Form.Item>
<Form.Item label="国家" className="item">
{getFieldDecorator('country', {
rules: [{ required: true }],
})(
<Input
size="large"
placeholder="国家"
/>,
)}
</Form.Item>
<Form.Item label="类型" className="item">
{getFieldDecorator('type', {
rules: [{ required: true }],
})(
<Input
size="large"
placeholder="类型:"
/>,
)}
</Form.Item>
<Form.Item label="单集片长" className="item">
{getFieldDecorator('single', {
rules: [{ required: false }],
})(
<Input
size="large"
placeholder="单集片长:"
/>,
)}
</Form.Item>
<Form.Item label="首播时间" className="item">
{getFieldDecorator('first_date', {
rules: [{ required: false }],
})(
<Input
size="large"
placeholder="首播时间:"
/>,
)}
</Form.Item>
<Form.Item {...tailFormItemLayout} className="item">
<Button size="large" type="primary" htmlType="submit" className="login-form-button">
创建
</Button>
</Form.Item>
</Form>
</Layout>
);
}
}

const mapStateToProps = (state) => {
return {
userData: state.userData.data,
};
};

const wrappedMovieForm = Form.create({ name: 'MovieForm' })(MovieForm);
export default connect(mapStateToProps, null)(wrappedMovieForm);

其次在operas.jsx中新建一个button,用于跳转

1
2
3
4
5
6
7
8
9
10
11
12
13
  //...
createForm() {
history.push("/operas/create");
}
//...
<Button
size="default"
type="primary"
onClick={() => this.createForm()}
style={{ marginLeft: "10px" }}
>
新建
</Button>

最后app.jsx中申请路由。

1
2
3
4
const OperaForm = lazy(() => import("./operaform"));
//...
<PrivateRoute exact path="/operas/create" component={OperaForm} />
//...

有新建,必定有删除鸭

实现逻辑和admin删除user一样。这里就不多讲了

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
//... 
deleteOperas = (item) => {
const _this = this;
confirm({
title: "Are you sure delete?",
content: "Hope you know what you are doing!",
okText: "confirm",
okType: "danger",
cancelText: "cancel",
onOk() {
const token = Util.getToken();
Api.post( `/operas/${item._id}/delete`,{
headers: { Authorization: `Bearer ${token}` },
})
.then((res) => {
console.log(res);
if (res.data) {
message.success("delete success");
_this.getUserList();
} else {
message.error("fail");
}
})
.catch((err) => {
console.log(err);
});
},
onCancel() {
console.log("Cancel");
},
});
};
//...
{
title: "delete",
key: "delete",
align: "center",
render: (row, item) => (
<div className="person-table-btn">
{
<Tooltip placement="top" title={"delete"}>
<Icon
type="delete"
theme="twoTone"
twoToneColor="#eb2f96"
className="table-btn-item"
onClick={() => this.deleteOperas(item)}
/>
</Tooltip>
}
</div>
),
},

实现收藏功能

我想,能不能加入一个收藏夹的功能,让用户能够把他中意的美剧放在收藏夹当中呢?

我的实现逻辑:首先新建一张schema,来存放我收藏夹中的信息,让然后在opera表格当中新添加一列 Like列,里面有一个点赞按钮,当点击后这个美剧就被添加到我们的收藏夹当中了。

在收藏夹中,我也以表格形式展示,可以删除收藏夹中的信息。还有返回按钮返回展示页面

因代码和展示页面类似,所以这里就不再展示了。

实现具体信息展示

这个功能在学习博客:Register Form 中,我把他运用到了这个项目当中

简单的来说,就是在展示界面的title列,渲染一个链接,让它指向一个网页,这个网页会在进行componentDidMount时自动把从后端请求过来的detailed message 渲染到表单当中。但是美中不足的时这个表单暂时不支持修改。

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
import React from "react";
import { connect } from "react-redux";
import { createHashHistory } from "history";
import { Form, Input, Button } from "antd";
import Layout from "../components/layout";
import "./style.scss";
import Api from "../../js/Api";
const history = createHashHistory({ forceRefresh: true });

class MovieForm extends React.Component {
constructor(props) {
super(props);
this.state = {};
this.state = {
_id: null,
actors: null,
country: null,
first_date: null,
now: null,
other_name: null,
single: null,
station: null,
title: null,
type: null,
url: null,
};
}

async componentDidMount() {
await this.populateOpera();
}

async populateOpera() {
const operaname = this.props.match.params.id; //获取url参数
const opera = Api.get(`/operas/${operaname}`);
const details = (await opera).data;
const infos = details.targetOpera[0];
const {
_id,
actors,
country,
first_date,
now,
other_name,
single,
station,
title,
type,
url,
} = infos;
this.setState({
_id,
actors,
country,
first_date,
now,
other_name,
single,
station,
title,
type,
url,
});
// console.log(actors)
}
back() {
history.push('/operas');
}
render() {
const {
_id,
actors,
country,
first_date,
now,
other_name,
single,
station,
title,
type,
url,
} = this.state;
console.log(actors);
const { getFieldDecorator } = this.props.form;
const { userData } = this.props;
const { name, isLogin } = userData;
const formItemLayout = {
labelCol: { span: 50 },
wrapperCol: { span: 60 },
};
const tailFormItemLayout = {
wrapperCol: {
xs: {
span: 24,
offset: 0,
},
sm: {
span: 22,
offset: 2,
},
},
};

return (
<Layout>
<strong className="title">DetailedInformation</strong>
<Form
{...formItemLayout}
onSubmit={this.handleSubmit}
className="login-form"
>
<Form.Item label="剧集名字" className="item">
<Input size="large" value={title} />
</Form.Item>
<Form.Item label="演员" className="item">
<Input.TextArea rows={4} size="large" value={actors} />
</Form.Item>
<Form.Item label="国家" className="item">
<Input size="large" value={country} />
</Form.Item>
<Form.Item label="类型" className="item">
<Input size="large" value={type} />
</Form.Item>
<Form.Item label="单集片长" className="item">
<Input size="large" value={single} />
</Form.Item>
<Form.Item label="首播时间" className="item">
<Input size="large" value={first_date} />
</Form.Item>
<Form.Item label="更新至" className="item">
<Input size="large" value={now} />
</Form.Item>
<Form.Item label="别名" className="item">
<Input size="large" value={other_name} />
</Form.Item>
<Form.Item label="电视台" className="item">
<Input size="large" value={station} />
</Form.Item>
<Form.Item label="url" className="item">
<Input size="large" value={url} />
</Form.Item>
<Form.Item {...tailFormItemLayout} className="item">
<Button
size="large"
type="primary"
onClick={() => this.back()}
className="login-form-button"
>
返回
</Button>
</Form.Item>
</Form>
</Layout>
);
}
}

const mapStateToProps = (state) => {
return {
userData: state.userData.data,
};
};

const wrappedMovieForm = Form.create({ name: "MovieForm" })(MovieForm);
export default connect(mapStateToProps, null)(wrappedMovieForm);

App.js

App组件是根组件所以我们单独讲。先上代码

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
import React, { Suspense, lazy } from 'react';
import './style.scss';
import { HashRouter as Router, Route, Switch } from 'react-router-dom';
import PrivateRoute from './components/PrivateRoute';
import Loading from './components/loading';

const Home = lazy(() => import('./home'));
const Sign = lazy(() => import('./sign'));
const Register = lazy(() => import('./register'));
const Manage = lazy(() => import('./manage'));
const Douban = lazy(() => import('./douban'));
const Taobao = lazy(() => import('./taobao'));
const Maoyan = lazy(() => import('./maoyan'));
const Operas = lazy(() => import('./operas'));
class App extends React.Component {
render() {
return (
<Router>
<Suspense
fallback={//这里如果等待的化,那么就加载上面所说的Loading组件
<div className="loading">
<Loading />
</div>
}
>
<Switch>
<PrivateRoute exact path="/" component={Home} />
<PrivateRoute exact path="/manage" component={Manage} />
<Route exact path="/login" component={Sign} />
<Route exact path="/register" component={Register} />
<PrivateRoute exact path="/douban" component={Douban} />
<PrivateRoute exact path="/taobao" component={Taobao} />
<PrivateRoute exact path="/maoyan" component={Maoyan} />
<PrivateRoute exact path="/operas" component={Operas} />
</Switch>
</Suspense>
</Router>
);
}
}
export default App;

这里需要讲几个问题:

lazy+Suspense 的作用

react官方文档中我截取了一段

React.lazy 函数能让你像渲染常规组件一样处理动态引入(的组件)。

1
const OtherComponent = React.lazy(() => import('./OtherComponent'));

此代码将会在组件首次渲染时,自动导入包含 OtherComponent 组件的包。

React.lazy 接受一个函数,这个函数需要动态调用 import()。它必须返回一个 Promise,该 Promise 需要 resolve 一个 default export 的 React 组件。

然后应在 Suspense 组件中渲染 lazy 组件,如此使得我们可以使用在等待加载 lazy 组件时做优雅降级(如 loading 指示器等)。

1
2
3
4
5
6
7
8
9
10
11
12
13
import React, { Suspense } from 'react';

const OtherComponent = React.lazy(() => import('./OtherComponent'));

function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<OtherComponent />
</Suspense>
</div>
);
}

fallback 属性接受任何在组件加载过程中你想展示的 React 元素。你可以将 Suspense 组件置于懒加载组件之上的任何位置。你甚至可以用一个 Suspense 组件包裹多个懒加载组件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { Suspense } from 'react';

const OtherComponent = React.lazy(() => import('./OtherComponent'));
const AnotherComponent = React.lazy(() => import('./AnotherComponent'));

function MyComponent() {
return (
<div>
<Suspense fallback={<div>Loading...</div>}>
<section>
<OtherComponent />
<AnotherComponent />
</section>
</Suspense>
</div>
);
}
-------------本文结束,感谢您的阅读-------------