# 实战(二)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

先在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

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

# 路由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
  • 将不需要一上来就加载的页面组件通过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
  • 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

# 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
  • 通过 decorate 来监控 ReportStore 类及 namenum属性,当这两个属性发生变化的时候就会被监听到。
  • 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

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
  • 将要使用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
  • 添加一个登陆用的数据管理,因为登陆的信息所有的页面都可能会用到

修改入口文件,将登陆信息这种都需要用的信息包裹进去传给所有页面

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

对应着修改一下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
  • 其他页面通过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

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

修改一下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

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

至此页面部分的一些简单配置完成