# 实战(二)react-hooks、路由
上一章把webpack进行了基本的配置,下面将react的一些基本逻辑写一写,使用react-hooks mobx antd
# 简单的页面
src下创建client文件夹,如下图所示
写几个公共组件
nav
import * as React from "react";
import { Link } from "react-router-dom";
const Nav = () => (
<ul>
<li><Link to="/login">please login</Link></li>
<li><Link to="/report">go to report</Link></li>
<li><Link to="/home">back to home</Link></li>
</ul>
)
export default Nav;
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
先在pages下创建几个简单的页面
home
import * as React from "react";
import Nav from "@components/nav";
import { Button } from 'antd';
const Home: React.FC = (props) => {
return (
<div>
<Nav />
<p>this is home</p>
</div>
)
};
export default Home;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
report
import * as React from "react";
import Nav from "@components/nav";
import { Button } from 'antd';
const Home: React.FC = (props) => {
return (
<div>
<Nav />
<p>this is home</p>
</div>
)
};
export default Home;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
# 路由router
现在已经有了几个基本的页面,接下来就要配置路由了。
routers/index.tsx
import * as React from "react";
import { Switch, Route, RouteProps, Redirect } from "react-router-dom";
import Loading from "@components/Loading";
import Login from "@pages/login";
import NoMatch from "@components/NoMatch";
const { Suspense, lazy } = React;
// 使用 react 懒加载加载页面
// /* webpackChunkName: "report" */ 是魔法注释,打包的时候会用这个名字命名,否则就是md5串儿
const Report = lazy(() =>
import(/* webpackChunkName: "report" */ "@pages/report")
);
const Home = lazy(() =>
import(/* webpackChunkName: "home" */ "@pages/home")
);
interface YDProps extends RouteProps {
key: string,
auth?: boolean,
}
const routeConfig: YDProps[] = [
{
key: 'login',
path: "/login",
exact: true,
component: Login
},
{
key: 'report',
path: "/report",
exact: true,
component: Report
},
{
key: 'home',
path: "/home",
exact: true,
component: Home,
},
];
const generateRoutes = (routeConfig: YDProps[]) => store => (
<Suspense fallback={Loading}>
<Switch>
<Route path="/" exact render={() => <Redirect to="/login" />} key="/home" />,
{
routeConfig.map((r, i: number) => {
const { path, component, exact, key } = r;
const LazyCom = component;
return (
<Route
key={key}
exact={exact}
path={path}
render={props => <LazyCom {...props} />}
/>
);
})
}
<Route component={NoMatch} />
</Switch>
</Suspense>
);
const Routes = generateRoutes(routeConfig);
export default Routes;
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
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
- 将不需要一上来就加载的页面组件通过react的懒加载导进来
- 懒加载采用异步加载,其中的
/* webpackChunkName: "report" */
为魔法注释,在打包的时候会打成里面的名字 - 配置路由数组,将所有页面的路由统一
- 因要使用懒加载,所以用
Suspense
包裹Switch
,将配置的路由数组循环渲染进去
接下来写一下入口文件
index.tsx
import * as React from "react";
import * as ReactDOM from "react-dom";
import Routes from "./routers";
import "@assets/styles/common.css";
import { BrowserRouter } from "react-router-dom";
import { message } from 'antd';
message.config({ duration: 2 })
window.message = message;
const App = observer(() => {
return (
<ErrorBoundary>
<BrowserRouter basename="/">
{Routes()}
</BrowserRouter>
</ErrorBoundary>
)
})
ReactDOM.render(<App />, document.getElementById("app"));
if (module.hot) {
module.hot.dispose(function () {
// 模块即将被替换时
console.log("module will be replaced");
});
module.hot.accept(function () {
// 模块或其依赖项之一刚刚更新时
console.log("module update");
});
}
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
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
- 用
ErrorBoundary
包裹一下,作为错误处理,react 16.8之后有了容错机制,可以通过componentDidCatch
捕捉到react中的报错,进行操作,防止白屏 - 引入
BrowserRouter
作为路由入口,包裹router中导出的路由组件 - 这里直接将 antd 的
message
组件挂在了全局window上,这样再用的时候直接使用 module.hot
用在开发阶段热更新,不添加时修改页面后自动更新时会刷新整个页面,加上之后会进行局部的更新,不用再刷整个页面
ErrorBoundary.tsx
import * as React from 'react'
import './index.less'
export default class ErrorBoundary extends React.Component {
constructor(props) {
super(props)
this.state = {
hasError: false,
error: null,
errorInfo: null
}
}
static getDerivedStateFromError(error) {
// Update state so the next render will show the fallback UI.
return { hasError: true }
}
componentDidCatch(error, errorInfo) {
// You can also log the error to an error reporting service
this.setState({ error, errorInfo })
}
render() {
if (this.state.hasError) {
return (
<div className="cmpt-error">
<h2>页面出现错误</h2>
</div>
)
}
return this.props.children;
}
}
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
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
# store
接下来要对数据进行管理。
react16.8之后推出了hooks,将redux融入到了hooks中,可以尝试使用。
本文使用mobx 和 mobx-react-lite 来进行数据管理(没想到吧╮(╯▽╰)╭)
先来搞两个小demo看一下mobx怎么用
在models下创建 reportStore.tsx
reportStore.tsx
import * as React from "react";
import { decorate, observable } from "mobx";
const { createContext } = React;
export class ReportStore {
public num = 0;
public name: string
public add(name: string): void {
this.num += 1;
this.name = name;
}
}
decorate(ReportStore, {
num: observable,
name: observable
})
export default createContext(new ReportStore());
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- 通过
decorate
来监控ReportStore
类及name
和num
属性,当这两个属性发生变化的时候就会被监听到。 - 用
createContext
将类绑到store里
先改造一下home 和report 页面
home.tsx
import * as React from "react";
import Nav from "@components/nav";
import ReportStore from '@models/reportStore';
import { observer } from 'mobx-react-lite'
import { Button } from 'antd';
const {useContext} = React;
const Home: React.FC = observer((props) => {
console.log('props',props)
const reportStore = useContext(ReportStore);
return (
<div>
<Nav />
<p>this is home</p>
<Button onClick={() => reportStore.add('zxy')}>11</Button>
<p>num:{reportStore.num}</p>
</div>
)
});
export default Home;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
report.tsx
import * as React from "react";
import Nav from "@components/nav";
import ReportStore from '@models/reportStore';
import { observer } from 'mobx-react-lite';
import { Button } from 'antd';
const { useContext } = React;
const Report: React.FC = observer(() => {
const reportStore = useContext(ReportStore);
console.log('reportStore', reportStore);
return (
<div>
<Nav />
<p>this is report</p>
<Button onClick={() => reportStore.add('zxy')}>11</Button>
<p>num:{reportStore.num}</p>
</div>
)
});
export default Report;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- 将要使用store的页面用
observer
包裹 - 使用
useContext
拿到store - 在home中点击,可以看到数字在增加,切换到report页面后,数字仍在,如图
# 改造几个正经页面
接下来开始改造
store.tsx
import * as React from "react";
import { decorate, observable } from "mobx";
const { createContext } = React;
interface User {
username: string;
password: string;
}
export class Ydstore {
public token: string = window.localStorage["token"];
public userInfo: object;
public async login(user: User): string {
const { username, password } = user;
if (username !== "admin" || password !== "admin") {
throw new Error("用户名密码错误!");
}
this.token = Math.random().toString();
this.userInfo = { name: "zxy" };
return this.token;
}
public logout(): void {
window.localStorage["token"] = "";
}
}
decorate(Ydstore, {
token: observable,
userInfo: observable
});
export default createContext(new Ydstore());
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
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
- 添加一个登陆用的数据管理,因为登陆的信息所有的页面都可能会用到
修改入口文件,将登陆信息这种都需要用的信息包裹进去传给所有页面
index.tsx
import * as React from "react";
import * as ReactDOM from "react-dom";
import Routes from "./routers";
import "@assets/styles/common.css";
import { BrowserRouter } from "react-router-dom";
import YdStore from '@models/store';
import { message } from 'antd';
import ErrorBoundary from '@components/Error'
import { observer } from 'mobx-react-lite';
message.config({ duration: 2 })
window.message = message;
const { useContext } = React
const App = observer(() => {
const ydStore = useContext(YdStore)
return (
<ErrorBoundary>
<BrowserRouter basename="/">
{Routes(ydStore)}
</BrowserRouter>
</ErrorBoundary>
)
})
ReactDOM.render(<App />, document.getElementById("app"));
if (module.hot) {
module.hot.dispose(function () {
// 模块即将被替换时
console.log("module will be replaced");
});
module.hot.accept(function () {
// 模块或其依赖项之一刚刚更新时
console.log("module update");
});
}
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
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
对应着修改一下router,添加判断是否登陆,顺便有些页面会有子路由,都加一下
router.tsx
import * as React from "react";
import { Switch, Route, RouteProps, Redirect } from "react-router-dom";
import Loading from "@components/Loading";
import Login from "@pages/login";
import NoMatch from "@components/NoMatch";
const { Suspense, lazy } = React;
const Report = lazy(() =>
import(/* webpackChunkName: "report" */ "@pages/report")
);
const Home = lazy(() =>
import(/* webpackChunkName: "home" */ "@pages/home")
);
const Demo1 = lazy(() =>
import(/* webpackChunkName:"demo1" */ "@components/Demo1")
);
const Demo2 = lazy(() =>
import(/* webpackChunkName:"demo2" */ "@components/Demo2")
);
interface YDProps extends RouteProps {
key: string,
auth?: boolean,
children?: any
}
const routeConfig: YDProps[] = [
{
key: 'login',
path: "/login",
exact: true,
component: Login
},
{
key: 'report',
path: "/report",
exact: true,
component: Report
},
{
key: 'home',
path: "/home",
exact: true,
component: Home,
children: [
{
key: 'demo1',
path: '/home/demo1',
component: Demo1,
exact: true
},
{
key: 'demo2',
path: '/home/demo2/:id',
component: Demo2,
// exact:true
},
]
},
];
const generateRoutes = (routeConfig: YDProps[]) => store => (
<Suspense fallback={Loading}>
<Switch>
<Route path="/" exact render={() => <Redirect to="/login" />} key="/home" />,
{
routeConfig.map((r, i: number) => {
const { path, component, exact, key } = r;
const LazyCom = component;
return (
<Route
key={key}
exact={exact}
path={path}
render={props => {
if (!r.auth) return <LazyCom {...props} />
if (!store.token) {
return (
<Redirect
to={{
pathname: "/login",
state: { from: props.location }
}}
/>
)
}
if (!r.children || !r.children.length) {
return <LazyCom {...props} store={store} />
}
return (
<LazyCom props={props} store={store}>
<Switch>
{
r.children.map(child => {
const { key, path, exact, component } = child
const ChildCMP = component
return <Route
key={key}
path={path}
exact={exact}
render={props => <ChildCMP {...props} store={store} />}
/>
})
}
<Redirect to={r.children[0].path} />
</Switch>
</LazyCom>
)
}
}
/>
);
})
}
<Route component={NoMatch} />
</Switch>
</Suspense>
);
const Routes = generateRoutes(routeConfig);
export default Routes;
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
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
- 其他页面通过react懒加载进行异步加载,/* webpackChunkName: "report" */ 是魔法注释,在拆包的时候能按照设置的名命名
- 在配置路由时,有些页面只是里面的内容动,像导航、header之类的东西不想让它每次都刷,这时就传children,children里又是子组件
- 在循环添加路由的时候,通过
auth
参数判断页面是否需要登录才能显示,再判断是否已经登录了 - 子路由像正组件一样直接插入到里面就可以
修改一下登录页,将数据管理在仓库里 login.tsx
import * as React from 'react'
import { NavLink, RouteComponentProps } from 'react-router-dom'
import { observer } from 'mobx-react-lite'
import { Form, Icon, Input, Button } from 'antd'
import YdStore from '@models/YdStore'
import { setStorage } from '@utils/storage'
import './index.less'
const { useState, useEffect, useContext } = React
const Login = observer((routerProps: RouteComponentProps) => {
const { location, history } = routerProps
const redirectUrl = location.state ? location.state.from.pathname : '/home';
const ydstore = useContext(YdStore)
// const RedirectUrl = location.state ? location.state.from.pathname : "/index/index";
const { 0: user, 1: setUser } = useState({
username: '',
password: ''
})
useEffect(() => {
document.title = '系统登录'
}, [])
const onInputChange = ({ target: { name, value } }) => setUser({ ...user, [name]: value })
const onInputKeyUp = ({ keyCode }) => keyCode === 13 && onSubmit()
const checkLogin = ({ username, password }) => {
if (!username) {
return {
status: false,
msg: '用户名不能为空!'
}
}
if (!password) {
return {
status: false,
msg: '密码不能为空!'
}
}
return {
status: true,
msg: '验证通过'
}
}
const onSubmit = async () => {
const { status, msg } = checkLogin(user)
if (status) {
try {
const res = await ydstore.login(user)
setStorage('token', res)
history.push(redirectUrl)
} catch (err) {
console.log(err)
window.message.error('用户名或密码错误')
}
} else {
window.message.error(msg)
}
}
return (
<section className="page-login">
<section className="login-panel">
<h1 className="login-panel-title">系统登录</h1>
<div className="form-group">
<Input prefix={<Icon type="user" style={{ color: 'rgba(0,0,0,.25)' }} />}
name="username"
placeholder="用户名"
onKeyUp={onInputKeyUp}
onChange={onInputChange}
/>
</div>
<div className="form-group">
<Input
prefix={<Icon type="lock" style={{ color: 'rgba(0,0,0,.25)' }} />}
name="password"
type="password"
placeholder="密码"
onKeyUp={onInputKeyUp}
onChange={onInputChange}
/>
</div>
<div className="login-btn-group">
<Button
type="primary"
className="login-form-button"
onClick={onSubmit}
>登录</Button>
</div>
</section>
<nav className="nav-bar">
<NavLink to="/about" className="nav-bar-item">关于我们</NavLink>
</nav>
</section>
)
})
export default Login
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
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
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
login.less
.page-login {
position: relative;
width: 100%;
height: 100%;
background-size: cover;
background-image: url('../../assets/images/bg.jpeg');
.login-panel {
box-sizing: border-box;
position: absolute;
width: 400px;
height: 280px;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
padding: 25px 50px;
background: #fff;
border-radius: 6px;
.login-panel-title {
text-align: center;
padding-bottom: 10px;
font-size: 18px;
}
.form-group {
margin: 10px 0 15px;
}
.login-btn-group {
margin-top: 35px;
text-align: center;
.login-form-button {
width: 100%;
}
}
}
.nav-bar {
box-sizing: border-box;
display: flex;
justify-content: flex-end;
padding-right: 100px;
padding-top: 20px;
}
}
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
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
修改一下home页面
home.tsx
import * as React from 'react'
import { Layout, Menu, Icon } from 'antd'
import { NavLink, withRouter } from 'react-router-dom'
import Breadcrumb from '@components/Breadcrumb'
import './index.less'
const { Header, Sider, Content } = Layout
const { useState, useEffect, useContext } = React
const Home = props => {
const { store, history } = props
const { 0: state, 1: setState } = useState({
collapsed: false,
})
const toggle = () => setState({ ...state, collapsed: !state.collapsed })
const logout = () => {
store.logout()
history.push('/login')
}
useEffect(() => {
document.title = '京程一灯CRM'
}, [])
return (
<section className="page-home">
<Layout>
<Sider trigger={null} collapsible collapsed={state.collapsed}>
<div className="logo">京程一灯CRM</div>
<Menu theme="dark" mode="inline" defaultSelectedKeys={['1']}>
<Menu.Item key="1">
<Icon type="user" />
<span>功能一</span>
<NavLink to="/index/demo1" />
</Menu.Item>
<Menu.Item key="2">
<Icon type="video-camera" />
<span>功能二</span>
<NavLink to="/index/demo2/123" />
</Menu.Item>
</Menu>
</Sider>
<Layout>
<Header className="header-layout" style={{ background: '#fff', padding: 0 }}>
<Icon
className="trigger"
type={state.collapsed ? 'menu-unfold' : 'menu-fold'}
onClick={toggle}
/>
<div className="header-right">
<span onClick={logout}>[退出]</span>
</div>
</Header>
<Breadcrumb />
<Content className='layout-content'>{props.children}</Content>
</Layout>
</Layout>
</section>
)
}
export default withRouter(Home)
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
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
home.less
.page-home {
height: 100%;
width: 100%;
overflow: hidden;
.logo {
// display: flex;
height: 50px;
background-color: #4A4C5B;
color: #FFFFFF;
font-size: 18px;
line-height: 50px;
text-align: center;
}
.ant-spin-nested-loading {
height: 100%;
.ant-spin-container {
height: 100%;
}
}
.ant-layout.ant-layout-has-sider {
height: 100%;
}
.header-right {
position: absolute;
right: 50px;
top: 0;
font-size: 14px;
cursor: pointer;
}
.layout-content {
height: 100%;
max-height: 100%;
overflow: auto;
margin: 24px 16px;
padding: 24px;
background: #fff;
}
}
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
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
至此页面部分的一些简单配置完成