> For the complete documentation index, see [llms.txt](https://gb.akanote.cn/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://gb.akanote.cn/react/project.md).

# 《IT猿题库》项目实战

## 预习资料

项目地址：<http://codesohigh.com/yuantiku>

参考该项目及这份笔记，并尝试搭建项目。

## 实战课学习目标

结合React、React Redux、React Router、Hooks、Axios与material-ui，完成《IT猿题库》项目。

## 一、项目参考

项目地址：<http://codesohigh.com/yuantiku>

界面效果：

[![image-20210514194820474](https://tva1.sinaimg.cn/large/008i3skNgy1gqi74xkwc8j30kk18mwu3.jpg)](https://tva1.sinaimg.cn/large/008i3skNgy1gqi74xkwc8j30kk18mwu3.jpg)

### 项目图片资源

链接: <https://pan.baidu.com/s/1kTOSazkZ0i9MKnjLhQmiKQ> 提取码: 9p5h

## 二、技术栈

### 1、React

简介：使用React开发移动端项目

官网地址：<https://react.docschina.org/>

### 2、Material-UI

简介：一个基于 Preact / React / React Native 的 UI 组件库

官网地址：<https://v4.mui.com/zh/>

## 三、项目创建与安装

### 1、依赖安装

使用 `npx create-react-app yuantiku` 创建完项目后，安装material-ui：

```shell
# 用npm安装
$ npm install @material-ui/core

# 用yarn安装
$ yarn add @material-ui/core

# 顺便安装SVG 图标
# 通过 npm
$ npm install @material-ui/icons

# 通过 yarn
$ yarn add @material-ui/icons
```

### 2、FastClick解决

在 `public/index.html` 的 `head` 标签中插入：

```html
<title>IT猿题库</title>
<script src="https://as.alipayobjects.com/g/component/fastclick/1.0.6/fastclick.js"></script>
<script>
  if ('addEventListener' in document) {
    document.addEventListener('DOMContentLoaded', function() {
      FastClick.attach(document.body);
    }, false);
  }
  if(!window.Promise) {
    document.writeln('<script src="https://as.alipayobjects.com/g/component/es6-promise/3.2.2/es6-promise.min.js"'+'>'+'<'+'/'+'script>');
  }
</script>
```

> 这里建议把 <https://as.alipayobjects.com/g/component/fastclick/1.0.6/fastclick.js> 和 <https://as.alipayobjects.com/g/component/es6-promise/3.2.2/es6-promise.min.js> 保存到本地

### 3、调用

在 `App.js` 中：

```shell
import React, { Component } from 'react'
import Button from '@material-ui/core/Button';

export default class App extends Component {
  render() {
    return (
      <Button variant="contained" color="primary">你好，世界</Button>
    )
  }
}
```

然后就可以看到按钮被正式引入：

[![image-20210428111957851](https://tva1.sinaimg.cn/large/008i3skNgy1gpzaj1o26sj32ag0d6djx.jpg)](https://tva1.sinaimg.cn/large/008i3skNgy1gpzaj1o26sj32ag0d6djx.jpg)

### 4、清除默认边距与样式

npm包路径：<https://www.npmjs.com/package/reset-css>

安装：

```shell
$ yarn add reset-css
```

使用：

```js
// index.js中
import 'reset-css';
```

## 四、蓝湖

将设计图用photoshop打开，并上传至蓝湖。此时的设计图是3x尺寸，因此要勾选对应的1125px尺寸。

## 五、配置rem

### 1、安装依赖包

```shell
$ yarn add lib-flexible postcss-pxtorem
```

### 2、解包

解包需要先做git提交，否则无法解包，因此先执行：

```shell
$ git add .
$ git commit -m 'eject之前的提交'
```

接下来直接解包：

```shell
$ yarn eject
```

[![image-20210425145931070](https://tva1.sinaimg.cn/large/008i3skNgy1gpw00im87kj31o606eq76.jpg)](https://tva1.sinaimg.cn/large/008i3skNgy1gpw00im87kj31o606eq76.jpg)

### 3、配置loader

解包后，可以看到项目目录下多了一个 `config` 文件夹。打开 `config/webpack.config.js` ：

```js
// 引入 postcss-px2rem 
const px2rem = require('postcss-px2rem')
```

搜索 `postcss-loader` ，添加：

```js
const loaders = [
 	 ...,
  {
        // Options for PostCSS as we reference these options twice
        // Adds vendor prefixing based on your specified browser support in
        // package.json
        loader: require.resolve('postcss-loader'),
        options: {
          postcssOptions: {
            // Necessary for external CSS imports to work
            // https://github.com/facebook/create-react-app/issues/2677
            ident: 'postcss',
            config: false,
            plugins: !useTailwind
              ? [
                  'postcss-flexbugs-fixes',
                  [
                    'postcss-preset-env',
                    {
                      autoprefixer: {
                        flexbox: 'no-2009',
                      },
                      stage: 3,
                    },
                  ]],
	              /* -------添加下面这一段------- */
                  [
                    'postcss-pxtorem',
                    {
                      rootValue: 112.5,
                      selectorBlackList: [],
                      propList: ['*'],
                      exclude: /node_modules/i
                    }
                  ]，
               	  /* -------添加上面这一段------- */
                  // Adds PostCSS Normalize as the reset css with default options,
                  // so that it honors browserslist config in package.json
                  // which in turn let's users customize the target behavior as per their needs.
                  'postcss-normalize',
                ]
              : [
                  'tailwindcss',
                  'postcss-flexbugs-fixes',
                  [
                    'postcss-preset-env',
                    {
                      autoprefixer: {
                        flexbox: 'no-2009',
                      },
                      stage: 3,
                    },
                  ],
	              /* -------添加下面这一段------- */
                  [
                    'postcss-pxtorem',
                    {
                      rootValue: 112.5,
                      selectorBlackList: [],
                      propList: ['*'],
                      exclude: /node_modules/i
                    }
                  ]
               	  /* -------添加上面这一段------- */
                ],
          },
          sourceMap: isEnvProduction ? shouldUseSourceMap : isEnvDevelopment,
        },
  ...
]
```

这里的 `rootValue: 112.5` 的意思就是1rem = 112.5px 这个是根据1125px设计稿来的。

### 4、flexible引入

在 `入口文件 index.js` 里引入 `lib-flexible`：

```js
import 'lib-flexible'
```

### 5、rem测试

在 `App.js` 中写个类名，创建 `App.css` ，并写入：

```jsx
// App.js
import React, { Component } from 'react'
import './App.css'

export default class App extends Component {
  render() {
    return (
      <div className="box">
        盒子
      </div>
    )
  }
}

// App.css
.box{
    width: 1125px;
    height: 186px;
    background: pink;
}
```

接下来打开浏览器：

[![](https://tva1.sinaimg.cn/large/008i3skNgy1gpw15bihnuj32a10u0dv1.jpg)](https://tva1.sinaimg.cn/large/008i3skNgy1gpw15bihnuj32a10u0dv1.jpg)

可以看到，iphoneX的尺寸下，html的字体大小为37.5px，此时box的宽度为10rem，再来看看其他尺寸：

[![image-20210425154110249](https://tva1.sinaimg.cn/large/008i3skNgy1gpw17uy4wij32la0u07h9.jpg)](https://tva1.sinaimg.cn/large/008i3skNgy1gpw17uy4wij32la0u07h9.jpg)

当其他尺寸下时，可以发现html字体大小为41.1px，而此时box的宽度仍为10rem，这就代表我们rem配置成功了。

### 6、兼容ipad

但是，当你点开ipad时，会发现盒子兼容出了问题，这是因为淘宝弹性布局方案lib-flexible不兼容ipad和ipad pro。我们这里给出解决方案：

> 在public>index.html的head标签中添加：

```html
<script>
        /(iPhone|iPad|iPhone OS|Phone|iPod|iOS)/i.test(navigator.userAgent)&&(head=document.getElementsByTagName('head'),viewport=document.createElement('meta'),viewport.name='viewport',viewport.content='target-densitydpi=device-dpi, width=480px, user-scalable=no',head.length>0&&head[head.length-1].appendChild(viewport));
</script>
```

这样，我们就解决ipad的兼容问题了。

### 7、修改meta标签

```html
<meta name="viewport" content="minimum-scale=1, initial-scale=1, width=device-width" />
```

## 六、配置less

### 1、安装

```shell
$ yarn add less less-loader@5.0.0
# 或者
$ npm install less less-loader@5.0.0
```

### 2、解包

接下来要解包，如果上一步你已经解包过，就直接跳过。

如果未解包，请以上参考第五步。

### 3、配置loader

找到 `webpack.config.js` ，搜索 `sassRegex`：

```js
const lessModuleRegex = /\.less$/;
```

搜索 `sass-loader` 后，在其下方添加：

```js
module: {
  ...,
  // less加载器 
	{
    test: lessModuleRegex,
    use: getStyleLoaders(
        {
            //暂不配置
        },
        'less-loader'
    ),
	},
}
```

修改了配置文件，记得重新 `yarn start` 哦！

### 4、测试less

将 `App.css` 改为 `App.less` 进行测试，依然没问题。

### 5、文字三属性

在 `App.less` 中添加：

```less
@import url("http://at.alicdn.com/t/font_2390471_h1demfeh4rc.css");

#root {
    font-size: 38px;
    font-family: NotoSansHans;
    color: #333333;
}
```

这里注意，由于html和body标签已被强行设定了font-size，因此我们设定#root的font-size即可。

## 七、登录判断

如果我们每个页面都需要在 `componentWillMount` 判断是否已经登录，那么太麻烦。React中有mixins，但已经被淘汰了。取而代之，我们使用高阶函数来操作。

在src下创建 `hoc>index.js` ：

```jsx
import {Component} from 'react'

export const ifLoginFn = (Comp) => {
    // 定义一个判断登录的高阶组件，在需要判断登录的页面套上这个组件
    return class extends Component {
        UNSAFE_componentWillMount(){
            let token = localStorage.getItem('token');
            if(!token){
                this.props.history.push("/login")
            }
        }
        render(){
            return <Comp />
        }
    }
}
```

在任何一个页面都可以如下调用（这里以Home页面举例）：

```jsx
import React, { Component } from 'react'
import "./Home.less"
import {ifLoginFn} from '../../components/hoc'

class Home extends Component {
    render() {
        return (
            <div>
                首页
            </div>
        )
    }
    
}

// 使用高阶函数
export default ifLoginFn(Home)
```

## 八、request封装

### 1、接口文档

IT猿题库【IT猿题库】 <http://www.docway.net/project/1eRv5Lh2UW9/share/1evckeXPiQy> 阅读密码:zhaowenxian

### 2、封装request

react的数据请求我们依然使用axios，我们先封装request：

```js
import axios from 'axios'

const instance = axios.create({
  baseURL: '/api',	// 通过使用配置的proxy来解决跨域
  timeout: 5000
});

// 添加请求拦截器
instance.interceptors.request.use(function (config) {
  let token = localStorage.getItem("x-auth-token");
  if (token) {
    config.headers = {
      "x-auth-token": token
    }
  }
  return config;
}, function (error) {
  // 对请求错误做些什么
  return Promise.reject(error);
});

// 添加响应拦截器
instance.interceptors.response.use(function (response) {
  // 对响应数据做点什么
  return response.data;
}, function (error) {
  // 对响应错误做点什么
  return Promise.reject(error);
});


export default instance;
```

**Token过期：**

如果后端判断到token过期，可以在响应拦截器到reject之前：

```js
import {HashRouter} from 'react-router-dom' // 借用HashRouter协助路由操作

if (error.response.status === 500 && error.response.data.errCode === 1002) {
  // 使用HashRouter
  const router = new HashRouter();
  // 如果是token过期，直接跳到登录页
  router.history.push('/login');
}
```

### 3、解决跨域

#### 方案一：

react简单解决跨域可以直接在 `package.json` 中添加 `proxy` 属性

#### 方案二：

如果你已经进行了 `npm run eject` ，建议你直接修改 `config>webpackDevServer.config.js` ：

```js
proxy: {
  '/api': {
    target: 'https://www.ahsj.link/rambo', // 后台服务地址以及端口号
    changeOrigin: true, //是否跨域
    pathRewrite: { '^/api': '/' }
  }
}
```

#### 方案三（推荐）：

安装 `http-proxy-middleware` ：

```shell
yarn add http-proxy-middleware
```

这里注意，http-proxy-middleware 模块是有版本区别的，默认安装最新版本，然后在 src 目录下新建 `setupProxy.js` ：

```js
const { createProxyMiddleware } = require("http-proxy-middleware");
module.exports = function (app) {
    app.use(
        "/api",
        createProxyMiddleware({
            target: "https://www.ahsj.link/rambo",
            changeOrigin: true,
            pathRewrite: {
                "/api": "",
            },
        })
    );
};
```

重新 `npm run start` 即可解决跨域。

### 4、api导出

我们做请求时的api需要导出：

```js
import request from './request'

// 首页默认数据
export const HomeDefaultApi = () => request.get('/6666');

// 登录接口
export const LoginApi = (params) => request.post('/1024/login', params)

// 注册接口
export const RegisterApi = (params) => request.post('/1024/register', params)
```

### 5、请求

```js
componentDidMount(){
  // 获取token，判断是否有token，有则做请求
  let token = localStorage.getItem("x-auth-token");
  if(token){
    // 首页默认数据
    HomeDefaultApi().then(res=>{
      console.log(res)
    })
  }
}
```

## 九、路由配置

### 1、安装

安装 `react-router-dom`：

```shell
$ yarn add react-router-dom
```

### 2、路由配置

`src` 下创建 `router/index.js`：

```js
import {BrowserRouter as Router, Route, Switch} from 'react-router-dom'
import App from '../App'

export const BaseRoute = () => {
    return (
        <Router>
            <Switch>
                <Route path="/" component={App}></Route>
            </Switch>
        </Router>
    )
}
```

`App.js` 中：

```js
import React, { Component } from 'react';
import './App.less'

class App extends Component {
  render() {
    return (
      <div id="app">
        App
      </div>
    );
  }
}

export default App;
```

而 `src/index.js` 下：

```js
import ReactDOM from 'react-dom';
import 'reset-css';
import './base.less'
import 'lib-flexible'
import {BaseRoute} from './router'

ReactDOM.render(
  BaseRoute(),
  document.getElementById('root')
);
```

可以看到：

[![image-20210428115443204](https://tva1.sinaimg.cn/large/008i3skNgy1gpzbj65hv8j32is0cyqfo.jpg)](https://tva1.sinaimg.cn/large/008i3skNgy1gpzbj65hv8j32is0cyqfo.jpg)

### 3、创建五个页面

src下创建 `views/Home.jsx + Fast.jsx + User.jsx + Login.jsx + Register.jsx + Error.jsx`，分别代表：首页+快速刷题+我的+登录页+404页面，并在router下做配置：

```js
import { BrowserRouter as Router, Route, Switch, Redirect } from 'react-router-dom'
import App from '../App'
import Home from '../views/Home'
import Fast from '../views/Fast'
import User from '../views/User'
import Login from '../views/Login'
import Register from '../views/Register'
import Error from '../views/Error'

export const BaseRoute = () => {
    return (
        <Router>
            <Route path="/" component={() =>
                <App>
                    <Switch>
                        <Route exact path="/home" component={Home}></Route>
                        <Route exact path="/fast" component={Fast}></Route>
                        <Route exact path="/user" component={User}></Route>
                        <Route exact path="/login" component={Login}></Route>
                        <Route exact path="/register" component={Register}></Route>
                        <Route exact component={Error}></Route>
                    </Switch>
                </App>
            }></Route>
        </Router>
    )
}
```

修改 `App.js` ：

```jsx
import React, { Component } from 'react';
import './App.less'

class App extends Component {
  render() {
    return (
      <div id="app">
        {this.props.children}
      </div>
    );
  }
}

export default App;
```

如此，当我们在浏览器输入 `http://localhost:3000` 时，就会帮我们自动跳到 `http://localhost:3000/home` ，如果随意输入 `http://localhost:3000/aaa` ，就会自动重定向到错误页面。

## 十、Tabbar配置

这里参照 `material-ui` 的 `Bottom Navigation 底部导航栏` ：<https://material-ui.com/zh/components/bottom-navigation/> 。

### 1、Tabbar引用

src下创建 `components/Tabbar.js` :

```jsx
import React from 'react';
import './less/Tabbar.less'
import BottomNavigation from '@material-ui/core/BottomNavigation';
import BottomNavigationAction from '@material-ui/core/BottomNavigationAction';
import HomeTab from './HomeTab'
import FastTab from './FastTab'
import UserTab from './UserTab'

export default function SimpleBottomNavigation() {
    const [value, setValue] = React.useState(0);

    return (
        <BottomNavigation
            value={value}
            onChange={(event, newValue) => {
                setValue(newValue);
            }}
            showLabels
        >
            <BottomNavigationAction label="首页" icon={<HomeTab num={value} />} />
            <BottomNavigationAction label="快速刷题" icon={<FastTab />} />
            <BottomNavigationAction label="我的" icon={<UserTab num={value} />} />
        </BottomNavigation>
    );
}
```

`src/components/less/Tabbar.less`:

```less
.MuiBottomNavigation-root{
    position: fixed;
    width: 100%;
    left: 0;
    bottom: 0;
    .MuiBottomNavigationAction-wrapper {
        .MuiBottomNavigationAction-label {
            font-size: 30px;
        }
        img{
            width: 64px;
        }
    }
    
    .MuiBottomNavigationAction-root.Mui-selected{
        .MuiBottomNavigationAction-label {
            font-size: 30px;
        }
    }
}
```

`src/components/HomeTab.js`、`src/components/FastTab.js` 及 `src/components/UserTab.js` :

```jsx
// HomeTab.js
import React, { Component } from 'react';
import home1 from '../images/tabbar/home_1.png'
import home2 from '../images/tabbar/home_2.png'

class HomeTab extends Component {
    render() {
        return (
            <img src={this.props.num===0 ? home1 : home2} width="61" alt="" />
        );
    }
}

export default HomeTab;

// FastTab.js
import React, { Component } from 'react';
import fast from '../images/tabbar/fast.png'

class FastTab extends Component {
    render() {
        return (
            <img src={fast} width="60" alt="" />
        );
    }
}

export default FastTab;

// UserTab.js
import React, { Component } from 'react';
import user1 from '../images/tabbar/my_1.png'
import user2 from '../images/tabbar/my_2.png'

class UserTab extends Component {
    render() {
        return (
            <img src={this.props.num===2 ? user1 : user2} width="64" alt="" />
        );
    }
}

export default UserTab;
```

### 2、函数式组件路由

在 `Tabbar.js` 中，使用hooks：

```jsx
import {useHistory} from 'react-router-dom'		// 路由中含有 useHistory

export default function SimpleBottomNavigation() {
  let history = useHistory();		//  使用history
  return (
        <BottomNavigation
            value={value}
            onChange={(event, newValue) => {
                setValue(newValue);
                switch (newValue) {
                    case 0:
                        history.push('/home');
                        break;
                    case 1:
                        history.push('/fast');
                        break;
                    case 2:
                        history.push('/user');
                        break;
                    default:
                        break;
                }
            }}
            showLabels
        >
            <BottomNavigationAction label="首页" icon={<HomeTab num={value} />} />
            <BottomNavigationAction label="快速刷题" icon={<FastTab />} />
            <BottomNavigationAction label="我的" icon={<UserTab num={value} />} />
        </BottomNavigation>
    );
}
```

如此，就实现了路由切换。

### 3、Tabbar动态化

```jsx
import React, {useEffect} from 'react';

export default function SimpleBottomNavigation() {
  const [showNav, setShowNav] = React.useState(false);
  
  useEffect(()=>{
    // if可改写为switch，建议写为switch
    if(window.location.pathname==='/home' || window.location.pathname==='/fast' || window.location.pathname==='/user'){
      setShowNav(true)
    }else{
      setShowNav(false)
    }
  }, [window.location.pathname])
  
  return (
  	<BottomNavigation style={{display: showNav ? 'flex' : 'none'}}></BottomNavigation>
  )
}
```

### 4、最终Tabbar代码

```jsx
import React, { useEffect } from 'react';
import './less/Tabbar.less'
import BottomNavigation from '@material-ui/core/BottomNavigation';
import BottomNavigationAction from '@material-ui/core/BottomNavigationAction';
import HomeTab from './HomeTab'
import FastTab from './FastTab'
import UserTab from './UserTab'
import { useHistory } from 'react-router-dom'

export default function SimpleBottomNavigation() {
    const [value, setValue] = React.useState(0);
    const [showNav, setShowNav] = React.useState(false);
    let history = useHistory();

    useEffect(() => {
        switch (history.location.pathname) {
            case '/home':
                setShowNav(true);   // 根据url来决定是否显示tabbar
                setValue(0);    // 根据url来决定现在是哪个tabbar为当前项
                break;
            case '/fast':
                setShowNav(true);
                setValue(1);
                break;
            case '/user':
                setShowNav(true);
                setValue(2);
                break;
            default:
                setShowNav(false);
                break;
        }
        // eslint-disable-next-line
    }, [history.location.pathname])

    return (
        <BottomNavigation
            style={{ display: showNav ? 'flex' : 'none' }}
            value={value}
            onChange={(event, newValue) => {
                setValue(newValue);
                switch (newValue) {
                    case 0:
                        history.push('/home');
                        break;
                    case 1:
                        history.push('/fast');
                        break;
                    case 2:
                        history.push('/user');
                        break;
                    default:
                        break;
                }
            }}
            showLabels
        >
            <BottomNavigationAction label="首页" icon={<HomeTab num={value} />} />
            <BottomNavigationAction label="快速刷题" icon={<FastTab />} />
            <BottomNavigationAction label="我的" icon={<UserTab num={value} />} />
        </BottomNavigation>
    );
}
```

## 十一、Toast组件

### 1、Toast组件封装

这里需要结合react-redux来实现。首先新建 `Toast.jsx`：

```jsx
import React from 'react'
import "./less/Toast.less"
import { connect } from 'react-redux'

class Toast extends React.Component {
    render() {
        return (
            <>
                <div className="toast" style={{ display: this.props.showToast ? "block" : "none" }}>
                    <main style={{ display: this.props.icon === "none" ? 'none' : 'block' }}>
                        <i className={
                            this.props.icon === "success" ?
                                "iconfont icon-ceshijieguo-dui" :
                                this.props.icon === "error" ?
                                    "iconfont icon-cuowu" :
                                    "iconfont icon-loading"
                        }></i>
                    </main>
                    <section>{this.props.title}</section>
                </div>
            </>
        )
    }
}

const mapStateToProps = (state) => {
    return {
        showToast: state.showToast,
        title: state.title,
        icon: state.icon
    }
}

const mapDispatchToProps = (dispath) => {
    return {
        showToastFn(icon, title) {
            dispath({ type: "showToastFn", value: { icon, title } })
        }
    }
}

export default connect(mapStateToProps, mapDispatchToProps)(Toast)
```

`Toast.less` 中：

```less
.toast{
    position: fixed;
    z-index: 5;
    left: 50%;
    top: 50%;
    transform: translate(-50%, -50%);
    background: rgba(0,0,0,0.5);
    padding: 50px 40px;
    border-radius: 10px;
    min-width: 3rem;
    max-width: 3.3rem;
    text-align: center;
    color: #fff;
    main{
        text-align: center;
        padding: 20px 0;
        .iconfont{
            display: inline-block;
            margin-bottom: .5rem;
            font-size: 1rem;
        }
        .icon-loading{
            animation: round 1s linear infinite;
        }
    }
    section{
        font-size: 50px;
        width: 100%;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
        word-break: break-all;
    }
}

@keyframes round {
    from{transform: rotate(0);transform-origin: center center;}
    to{transform: rotate(360deg);transform-origin: center center;}
}
```

在 `store/reducer.js` 中：

```js
const defaultState = {
  /* 
  		提示图标：
      success: (icon-ceshijieguo-dui)
      error: (icon-cuowu1)
      loading: (icon-loading)
      none: (没有图标) 
  */
  icon: "success",
  // toast标题
  title: "暂无内容暂无内容暂无内容暂无内容",
  // 显示Toast
  showToast: false
};

// eslint-disable-next-line
export default (state = defaultState, action) => {
  let newState = JSON.parse(JSON.stringify(state));
  switch (action.type) {
    // 弹出提示
    case "showToastFn":
      newState.showToast = true;
      newState.icon = action.value.icon;
      newState.title = action.value.title;
      break;
    case "hideToastFn":
      newState.showToast = false;
      break;
    default:
      break;
  }
  return newState;
};
```

使用时机：

> 比如：每个接口的请求若报500并返回1002，就代表token错误或过期，此时就需要判断token，如果没有token，就直接跳到登录页，跳过去之前，就要先显示loading效果

在 `App.jsx` 中引入Toast组件：

```jsx
import React, { Component } from 'react';
import './App.less'
import Tabbar from './components/Tabbar'
import Toast from './components/Toast'

class App extends Component {
  render() {
    return (
      <div id="app">
        <Toast />
        {this.props.children}
        <Tabbar history={this.props.history} />
      </div>
    );
  }
}


// 这里不再是导出App，而是导出连接器
export default App;
```

### 2、调用方式

```jsx
import { connect } from 'react-redux'  //引入连接器

class 组件名称 extends Component {
    handleClick(){
      	// 打开提示
        this.props.showToastFn({
            icon: "none",
            title: "该功能暂未开放"
        })
    }
}

const mapDispatchToProps = (dispatch) => {
    return {
        showToastFn(value) {
            dispatch({ type: "showToastFn", value })
            setTimeout(() => {
                dispatch({ type: "hideToastFn" })
            }, 2000)
        }
    }
}

export default connect(null, dispatchToProps)(组件名称);
```

## 十三、登录注册页套用

> ⚠️ 这里插播一句：登录注册页请直接套用代码就好！

效果如下：

[![image-20210924091435946](https://tva1.sinaimg.cn/large/008i3skNgy1gurg8lxjyzj60ks0kiab302.jpg)](https://tva1.sinaimg.cn/large/008i3skNgy1gurg8lxjyzj60ks0kiab302.jpg)

### 1、登录页代码

将示例代码直接复制粘贴，然后稍做修改：

```jsx
import React, { useState } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import TextField from '@material-ui/core/TextField';
import { LoginApi } from '../request/api'
import { useHistory, Link } from 'react-router-dom'
import { connect } from 'react-redux'
import logo from '../images/logo.png'

const useStyles = makeStyles((theme) => ({
  loginPage: {
    background: '#fff',
    height: '100vh',
    overflow: 'hidden'
  },
  logo: {
    display: 'block',
    margin: '0 auto',
    marginTop: '20vh'
  },
  title: {
    fontSize: '.5rem',
    textAlign: 'right',
    width: '90%',
    margin: '0 auto 20px',
    color: '#02369d'
  },
  btn: {
    color: "#fff",
    fontWeight: 'normal',
    background: "#02369d"
  },
  copyright: {
    width: '90%',
    margin: '20px auto',
    paddingLeft: '8px',
    boxSizing: 'border-box'
  },
  root: {
    width: '90%',
    margin: 'auto',
    '& > *': {
      width: '100%',
      display: 'block',
      fontSize: '.5rem',
    },
    '& .MuiTextField-root': {
      fontSize: '.5rem',
      '& .MuiInputBase-input': {
        fontSize: '.5rem'
      }
    }
  },
}));

function SignIn(props) {
  const classes = useStyles();
  // 用户名
  const [username, setUsername] = useState(localStorage.getItem("phone") || "");
  // 密码
  const [password, setPassword] = useState("wolfcode123");
  // 获取路由
  const history = useHistory()

  // 点击了登录
  function submitFn() {
    LoginApi({
      username,
      password
    }).then(res => {
      if (res.errCode === 0) {
        // 成功
        props.showToastFn({
          icon: "success",
          title: "登录成功"
        });
        let token = res.data;
        // 存入token
        localStorage.setItem('x-auth-token', token);
        setTimeout(() => {
          props.hideToastFn();
          props.showToastFn({
            icon: "loading",
            title: "即将返回首页"
          });
          // 2秒后跳转到首页
          setTimeout(() => {
            // 返回首页
            history.push("/home");
          }, 2000)
        }, 1500)
      } else {
        props.showToastFn({
          icon: "error",
          title: res.message
        });
      }
    }).catch(err => {
      props.showToastFn({
        icon: "error",
        title: err.response.data.message
      });
    })
  }

  return (
    <div className={classes.loginPage}>
      <img src={logo} className={classes.logo} alt="" />
      <h2 className={classes.title}>Login Page</h2>
      <form className={classes.root}>
        <TextField
          variant="outlined"
          margin="normal"
          required
          fullWidth
          id="username"
          placeholder="请输入手机号码"
          name="username"
          autoComplete="username"
          autoFocus
          value={username}
          onChange={(e) => setUsername(e.target.value)}
        />
        <TextField
          variant="outlined"
          margin="normal"
          required
          fullWidth
          name="password"
          placeholder="请输入密码"
          type="password"
          id="password"
          autoComplete="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
        />
        <Button
          className={classes.btn}
          type="button"
          fullWidth
          variant="contained"
          onClick={submitFn}
        >直接登录</Button>
        <div style={{ marginTop: '20px', display: 'flex', justifyContent: 'space-between' }}>
          <Link to="/register" variant="body2" style={{ color: '#02369d' }}>前往注册</Link>
          <Link to="/home" variant="body2" style={{ color: '#999999' }}>返回首页</Link>
        </div>
      </form>
      <section className={classes.copyright}>
        {'Copyright © '}
        <Link color="inherit" to="http://codesohigh.com">
          你单排吧
        </Link>{' '}
        {new Date().getFullYear()}
        {'.'}
      </section>
    </div>
  );
}

const mapDispatchToProps = (dispatch) => {
  return {
    showToastFn(value) {
      dispatch({ type: "showToastFn", value })
      setTimeout(() => {
        dispatch({ type: "hideToastFn" })
      }, 2000)
    },
    hideToastFn() {
      dispatch({ type: "hideToastFn" })
    }
  }
}

export default connect(null, mapDispatchToProps)(SignIn)
```

然后在 `request/api.js` 中：

```js
// 登录接口
export const LoginApi = (params) => request.post('/1024/login', params)
```

> 如果缺少什么依赖，就按照提示安装即可。这里主要还缺少：
>
> ```shell
> yarn add @material-ui/lab
> ```

### 2、注册页代码

```jsx
import React, { useState } from 'react';
import { makeStyles } from '@material-ui/core/styles';
import Button from '@material-ui/core/Button';
import TextField from '@material-ui/core/TextField';
import { RegisterApi } from '../request/api'
import { useHistory, Link } from 'react-router-dom'
import { connect } from 'react-redux'
import logo from '../images/logo.png'

const useStyles = makeStyles((theme) => ({
    loginPage: {
        background: '#fff',
        height: '100vh',
        overflow: 'hidden'
    },
    logo: {
        display: 'block',
        margin: '0 auto',
        marginTop: '20vh'
    },
    title: {
        fontSize: '.5rem',
        textAlign: 'right',
        width: '90%',
        margin: '0 auto 20px',
        color: '#02369d'
    },
    btn: {
        color: "#fff",
        fontWeight: 'normal',
        background: "#02369d"
    },
    copyright: {
        width: '90%',
        margin: '20px auto',
        paddingLeft: '8px',
        boxSizing: 'border-box'
    },
    root: {
        width: '90%',
        margin: 'auto',
        '& > *': {
            width: '100%',
            display: 'block',
            fontSize: '.5rem',
        },
        '& .MuiTextField-root': {
            fontSize: '.5rem',
            '& .MuiInputBase-input': {
                fontSize: '.5rem'
            }
        }
    },
}));

function SignIn(props) {
    const classes = useStyles();
    // 用户名
    const [phone, setPhone] = useState("");
    // 密码
    const [password, setPassword] = useState("");
    // 获取路由
    const history = useHistory()

    // 点击了登录
    function submitFn() {
        RegisterApi({
            phone: Number(phone),
            password
        }).then(res => {
            if (res.errCode === 0) {
                // 成功
                props.showToastFn({
                    icon: "success",
                    title: "注册成功"
                });
                // 存储手机号
                localStorage.setItem("phone", phone);
                setTimeout(() => {
                    props.hideToastFn();
                    props.showToastFn({
                        icon: "loading",
                        title: "即将返回登录页"
                    });

                    // 2秒后跳转到首页
                    setTimeout(() => {
                        // 返回登录页
                        history.push('/login');
                    }, 2000)
                }, 1000)
            } else {
                props.showToastFn({
                    icon: "error",
                    title: res.message
                });
            }
        }).catch(err => {
            props.showToastFn({
                icon: "error",
                title: err.response.data.message
            });
        })
    }

    return (
        <div className={classes.loginPage}>
            <img src={logo} className={classes.logo} alt="" />
            <h2 className={classes.title}>Register Page</h2>
            <form className={classes.root}>
                <TextField
                    variant="outlined"
                    margin="normal"
                    required
                    fullWidth
                    id="phone"
                    placeholder="请输入手机号码"
                    name="phone"
                    autoComplete="phone"
                    autoFocus
                    value={phone}
                    onChange={(e) => setPhone(e.target.value)}
                />
                <TextField
                    variant="outlined"
                    margin="normal"
                    required
                    fullWidth
                    name="password"
                    placeholder="请输入密码"
                    type="password"
                    id="password"
                    autoComplete="password"
                    value={password}
                    onChange={(e) => setPassword(e.target.value)}
                />
                <Button
                    className={classes.btn}
                    type="button"
                    fullWidth
                    variant="contained"
                    onClick={submitFn}
                >立即注册</Button>
                <Link to="/login" variant="body2" style={{ color: '#02369d', marginTop: '20px' }}>返回登录</Link>
            </form>
            <section className={classes.copyright}>
                {'Copyright © '}
                <Link color="inherit" to="http://codesohigh.com">
                    你单排吧
                </Link>{' '}
                {new Date().getFullYear()}
                {'.'}
            </section>
        </div>
    );
}

const mapDispatchToProps = (dispatch) => {
    return {
        showToastFn(value) {
            dispatch({ type: "showToastFn", value })
            setTimeout(() => {
                dispatch({ type: "hideToastFn" })
            }, 2000)
        },
        hideToastFn() {
            dispatch({ type: "hideToastFn" })
        }
    }
}

export default connect(null, mapDispatchToProps)(SignIn);
```

然后在 `request/api.js` 中：

```js
// 注册接口
export const RegisterApi = (params) => request.post("/1024/register", params)
```

## 十三、开发小提示

### 1、JSX条件判断

在react的class组件中如何使用 `switch...case` 语法？

```jsx
<div style={{ backgroundColor: '#F8F8F8', height: '100%' }}>
  {
    (()=>{
      switch(page){
        case 'home':
          return <Home />;
        case 'fast':
          return <Fast />;
        case 'user':
          return <User />;
        default:
          break;
      }
    })()
  }
</div>
```

使用一个自执行函数，在函数体内使用 `switch...case` 。

### 2、路由跳转问题

当我们使用

```jsx
this.props.history.push()
```

得到 `push of undefined` 的报错时，可以这么解决：

```jsx
import {withRouter} from 'react-router-dom'

class Home extends Component {
  ...
}
  
// 关键一步
export default withRouter(Home)
```

如果使用的是函数式组件，还可以借助hook：

```jsx
import {useHistory} from 'react-router-dom'

export default function Home(){
  return (...)
  fn(){
    let history = useHistory();
    history.push("/...")
  }
}
```

### 3、自定义进度条

```jsx
import { withStyles } from '@material-ui/core/styles';
import LinearProgress from '@material-ui/core/LinearProgress';

const BorderLinearProgress = withStyles((theme) => ({
  root: {
    height: 10,
    borderRadius: 5,
  },
  colorPrimary: {
    backgroundColor: theme.palette.grey[theme.palette.type === 'light' ? 200 : 700],
  },
  bar: {
    borderRadius: 5,
    backgroundColor: '#2E57FF',
  },
}))(LinearProgress);

// 使用：
<BorderLinearProgress variant="determinate" value={this.state.progress} />
```

### 4、配置项目@指向src

在保证解包 `yarn eject` 之后，找到 `webpack.config.js` ，搜索 `alias` 对象，可以得到3个结果，索引第2个结果，并配置：

```js
alias: {
  '@': path.join(__dirname, '../src'),
   ...
},
```

重跑项目即可使用@符号指向src。

### 5、组件中无法使用history？

页面在Router中配置了，但它下面的组件无法使用 `this.props.history.push()` 进行跳转，并且报了如下错误：

[![image.png](https://tva1.sinaimg.cn/large/008i3skNgy1gq8ql1l0fdj30kc0143yp.jpg)](https://tva1.sinaimg.cn/large/008i3skNgy1gq8ql1l0fdj30kc0143yp.jpg)

这是因为组件并没有使用 `history` 的能力，可以使用以上的提示2，也可以将配置过路由的父级的history传递给该组件：

```jsx
// 父组件中：
<Subject history={this.props.history} />

// 子组件中：
this.props.history.push('/xxx', {title: "xxx"})

// 目标页面接收参数：
this.props.location.state.title;
```

### 6、setState是异步的

以下这段代码：

```jsx
import React, { Component } from 'react';

class Pratice extends Component {
    constructor(props) {
        super(props)
        this.state = {
            num: 1
        }
    }
    render() {
        return (
            <div>
            	<tag onClick={this.handleClick.bind(this)}></tag>
            </div>
        );
    }
    // 每个选项点击后执行的代码
    handleClick(title, index) {
        this.setState({
          num: 2
        });
      	console.log(this.state.num);	// 这里打印出来是1
    }
}

export default Pratice;
```

为什么以上打印出来是1，而不是2呢？

因为setState是异步的，如果想要正常打印出2，需要：

```js
handleClick(title, index) {
  this.setState({
    num: 2
  }, ()=>{
    console.log(this.state.num);	// 这里打印出来是2
  });
}
```

也就是，setState有第二个参数，属于回调函数。

### 7、统一使用消息提示

在使用消息提示的时候，我们每次都要单独写样式比较麻烦，这里提供代码，当你要使用 `material-ui` 的alert组件时，可以这样引入：

```jsx
import MuiAlert from '@material-ui/lab/Alert';

<MuiAlert elevation={6} variant="filled" severity="error" className="my-mui-alert">弹框</MuiAlert>
```

记得先在公用的 `base.less` 中写入：

```less
// 定义蓝色变量
@blue: #2E57FF;

// 定义灰色变量
@gray: #efefef;

// 消息提示的样式
.my-mui-alert{
    align-items: center;
    position: absolute;
    width: 100%;
    box-sizing: border-box;
    left: 0;
    top: 0;
    .MuiAlert-message{
        font-size: .4rem;
        padding: 0;
    }
}
```

这里注意：

severity属性规定了消息提示框的类型，分别有：

* error - 错误提示（红色）
* warning - 警告提示（橙色）
* info - 信息提示（蓝色）
* success - 成功提示（绿色 - 默认值）

至于消息提示框的显示隐藏，可以通过：

```jsx
<MuiAlert style={{display: xxx ? 'flex' : 'none'}}>弹框</MuiAlert>
```

来控制。记住，是 `display: flex` 不是 `display: block` 。

**Alert引用代码演示：**

```jsx
import React, { Component } from 'react';
import Select from '../components/Select'
import { PraticeApi, GetPraticeTestInfoApi } from '../request/api'
import "./less/Pratice.less"
import MuiAlert from '@material-ui/lab/Alert';

class Pratice extends Component {
    constructor(props) {
        super(props)
        this.state = {
            // 弹框提示
            showAlert: false,
            // 弹框类型
            alertType: "success",	// success | info | warning | error
            // 弹框内容
            alertContent: "弹框内容"
        }
    }
    render() {
        return (
            <div>
            		...
                <MuiAlert style={{display: this.state.showAlert ? 'flex' : 'none'}} elevation={6} variant="filled" severity={this.state.alertType} className="my-mui-alert">{this.state.alertContent}</MuiAlert>
            </div>
        );
    }
    // 考试模式
    examModelFn(){
        let _this = this;
        _this.setState({
            showAlert: true,
            alertType: "warning",
            alertContent: "该功能暂未开放"
        }, ()=>{
            setTimeout(()=>{
                _this.setState({
                    showAlert: false
                })
            }, 2000)
        })
    }
}

export default Pratice;
```

### 8、ReactRedux包装Alert

安装 redux 与 react-redux：

```shell
yarn add redux react-redux
```

将统一消息提示封装到组件 `Alert.js` 中：

```jsx
import React, { Component } from 'react';
import MuiAlert from '@material-ui/lab/Alert';
import {connect} from 'react-redux'  //引入连接器

class Alert extends Component {
    render() {
        return (
            <MuiAlert style={{display: this.props.showAlert ? 'flex' : 'none'}} elevation={6} variant="filled" severity={this.props.alertType} className="my-mui-alert">{this.props.alertContent}</MuiAlert>
        );
    }
}

// stateToProps是一种映射关系，把原来的state映射成组件中的props属性
const stateToProps = (state)=>{
    return {
        showAlert : state.showAlert,
        alertType : state.alertType,
        alertContent : state.alertContent
    }
}

export default connect(stateToProps,null)(Alert);
```

src目录下创建 `store/reducer.js` ：

```js
const defaultState = {
    // 弹框提示
    showAlert: false,
    // 弹框类型
    alertType: "success",
    // 弹框内容
    alertContent: "弹框内容"
}

// eslint-disable-next-line
export default (state = defaultState, action) => {
    let newState = JSON.parse(JSON.stringify(state));
    switch(action.type){
        case "showAlert":
            newState.showAlert = action.value.showAlert;
            newState.alertType = action.value.alertType;
            newState.alertContent = action.value.alertContent;
            break;
        default:
            break;
    }
    return newState;
}
```

创建 `store/index.js` :

```js
import { createStore } from 'redux'
import reducer from './reducer'

const store = createStore(reducer)
export default store
```

`src/App.js` 中引入 `Alert` 组件：

```jsx
import React, { Component } from 'react';
import './App.less'
import Tabbar from './components/Tabbar'
import Alert from './components/Alert'

class App extends Component {
  render() {
    return (
      <div id="app">
        <Alert />
        {this.props.children}
        <Tabbar />
      </div>
    );
  }
}

// 这里不再是导出App，而是导出连接器
export default App;
```

最后在 `<App />` 外套用提供器Provider：

```jsx
...
import { Provider } from 'react-redux'	// 提供器
import store from '../store'						// store

export const BaseRoute = () => {
    return (
        <Router>
            <Route path="/" component={() =>
                <Provider store={store}>
                    <App>
                        <Switch>
                            ...
                        </Switch>
                    </App>
                </Provider>
            }></Route>
        </Router>
    )
}
```

后面某个页面想要触发 `Alert` ，就可以在该页面使用连接器如下：

```jsx
...
import { connect } from 'react-redux'  //引入连接器

class Biancheng extends Component {
    render() {
        return (
            <div className="wenda" style={{display: this.props.questionType==="code" ? 'block' : 'none'}}>
                ...
                <div className="btn" onClick={this.sureBtnFn.bind(this)}>确定</div>
            </div>
        );
    }
    // 点击了确定按钮
    sureBtnFn() {
        let val = this.state.value;
        if (val.trim() === "") {
            // 提示不能为空
            this.props.showAlert({
                // 弹框提示
                showAlert: true,
                // 弹框类型
                alertType: "warning",	// success | info | warning | error
                // 弹框内容
                alertContent: "内容不能为空"
            });
            setTimeout(() => {
                this.props.showAlert({
                    // 弹框提示
                    showAlert: false
                });
            }, 2000)
        } else {
            // ...
        }
    }
}

// dispatchToProps也是一种映射，用于传递并修改数据，这里要返回一个对象并包含一个事件
const dispatchToProps = (dispatch) => {
    return {
        showAlert(value) {
            const action = {
                type: "showAlert",
                value
            }
            dispatch(action)
        }
    }
}

export default connect(null, dispatchToProps)(Biancheng);
```

### 9、React插入html

使用 `dangerouslySetInnerHTML` 属性。

```jsx
class xxxx extends Component {
    state = {
        htmlCode: `
            <ul>
                <li>列表1</li>
                <li>列表2</li>
                <li>列表3</li>
            </ul>
        `
    }
    render() {
        return (
            <div dangerouslySetInnerHTML={{__html: this.state.htmlCode}}></div>
        );
    }
}
```

这里注意：html前面是两个下划线。

### 10、反序列化问题

在子组件中获取到的props，想要将props中的字符串转对象或数组，需要使用 `JSON.parse(this.props.content || "[]")` 。但接收props存在请求异步的延迟，因此会报错：

```js
Unexpected end of JSON input
```

因此，我们可以改写为：

```js
JSON.parse(this.props.content || "[]");
```

### 11、React实现移动端滑动

先给元素加上css定位：

```less
ul{
    padding: 65px 0 0;
    position: relative;
    left: 0;
    top: 0;
    transition: left .5s linear;
    ...
}
```

js实现：

```jsx
import React, { Component } from 'react';
...

// 手指触摸点
let startX = -1;
// 手指松开点
let endX = -1;
// 当前li的索引值
let liIndex = 0;

class Test extends Component {
    state = {
      ulLeft: 0
    }
    render() {
        return (
            <div style={{ width: '100%', overflow: 'hidden' }}>
                <Tabs liIndex={Math.abs(liIndex)+1} />
                <ul className="timu"
                    onTouchStart={this.handleTouchStart.bind(this)}
                    onTouchMove={this.handleTouchMove.bind(this)}
                    onTouchEnd={this.handleTouchEnd.bind(this)}
                    style={{ left: this.state.ulLeft }}>
                </ul>
            </div>
        );
    }
    // 手指触摸到屏幕
    handleTouchStart(e){
        startX = e.touches[0].clientX
    }
    // 手指滑动
    handleTouchMove(e){
        endX = e.touches[0].clientX
    }
    // 手指离开屏幕
    handleTouchEnd(e){
      	let _this = this;
        // 获取滑动范围
        if(startX>-1 && endX>-1){
            let distance = Math.abs(startX - endX);
            if (distance > 50) {
                // 两个手指位置距离相差50px，即视为要滑动
                if (startX > endX) {
                    liIndex--;
                  	// index是不能超过数组长度的
                    if (Math.abs(liIndex) >= _this.state.timuArr.length-1) {
                        liIndex = -_this.state.timuArr.length+1;
                    }
                } else {
                    liIndex++;
                    if(liIndex>=0){
                        liIndex=0;
                    }
                }
                this.setState({ulLeft: 100*liIndex+'%'}, ()=>{
                    startX = -1;
                    endX = -1;
                });
            }else{
                return;
            }
        }
    }
}

export default Test;
```

### 12、答题之多选组件UI部分代码

多选组件比单选组件复杂，需要给数组项中手动添加ifSelect=false。

```jsx
/* eslint-disable */
import React, { Component } from 'react';
import "./less/Option.less"

// 多选
class Options extends Component {
    constructor(props){
        super(props);
        // 拿到传过来的content数组
        let arr = JSON.parse(props.content || "[]");
        // 每个数组项都添加一个ifSelect字段
        arr.map((item)=>{
            item.ifSelect = false;
        })
        this.state = {
            arr
        }
    }
    render() {
        return (
            <ul className="option">
                {
                    this.state.arr.map((item, index) => 
                        <li key={item.sort} onClick={this.liClick.bind(this, index)}>
                            <i className={item.ifSelect ? "iconfont icon-danxuanxiangxuanzhong" : "iconfont icon-normal"}></i>
                            <div className="answer">{item.key}. {item.value}</div>
                        </li>
                    )
                }
            </ul>
        );
    }
    // 每一项的点击事件
    liClick(index){
        // 做一次深拷贝
        let newArr = JSON.parse(JSON.stringify(this.state.arr));
        // 修改数组项中都ifSelect
        newArr[index].ifSelect = !newArr[index].ifSelect;
        this.setState({
            arr: newArr
        });
    }
}

export default Options;
```

### 13、事件参数与事件对象

```jsx
<tag onClick={this.handleClick.bind(this, index)}></tag>
```

当我们这么写事件时，存在一个问题，事件对象的位置不知道是在index之前还是index之后，如：

```jsx
class xx extends Component {
  // 正确写法
  handleClick(index, e){
    e.stopPropagation();	// 阻止冒泡
  }
  // 错误写法
  handleClick(e, index){
    e.stopPropagation();	// 此时e为索引值index
  }
}
```

### 14、禁用Link

想要禁用Link，开发阶段使用 `BrowserRouter` 可以使用：

```jsx
<Link to="!#">xxx</Link>
```

但如果上线使用 `HashRouter` ，就会跳到404页面，因此需要补充：

```jsx
<Link to="!#" disabled>xxx</Link>

// App.less中补上：
a[disabled] {
  pointer-events: none;
}
```

## 十四、项目打包

### 1、Hash模式打包

打包需要首先向 `package.json` 中添加：

```json
{
  ...,
  "homepage": "./"
}
```

然后找到 `router/index.js` ：

```js
// 将BrowserRouter改成HashRouter
import { HashRouter as Router, Route, Switch, Redirect } from 'react-router-dom'
```

> 打包完成后，凡是使用了 this.props.location.state.xxx 传值的路由，都会碰上 Cannot read property 'xxx' of undefined 的报错。

> 原因是HashRouter模式不支持 this.props.location.state 获取数据，需要使用： this.props.location.params 。因此在路由传值的写法上，也要改为这种形式：

```jsx
var query = {
  pathname: "/mmm",
  params: {key: value}
}
// 跳转到Toggle页面
this.props.history.push(query);
```

打包后可能会发现 `history.goBack()` 会造成上一级路由参数丢失，此时可以尝试 `history.go(-1)`。

### 2、History模式打包

在 `package.json` 中：

```json
{
  "homepage": "http://www.abcd.com/yuantiku/"
}
```

找到 `router/index.js`：

```jsx
import { BrowserRouter as Router, Route, Switch, Redirect } from 'react-router-dom'

<Router basename="/yuantiku/">
	...  
</Router>
```

但这里必须要有nginx的配置。

首先 `ssh root@IP地址` 连接上服务器，然后执行 `whereis nginx`，然后使用vim修改nginx.conf：

```nginx
location /yuantiku {
  alias /usr/local/nginx/html/yuantiku;
  index index.html; # 默认访问的文件
  try_files $uri $uri/ /yuantiku/index.html;
}
```

修改完配置后，浏览器打开 `http://www.abcd.com/yuantiku` 即可。

### 3、跨域

生产环境下跨域需要nginx配合解决，如果你已在开发环境下使用 `/api` 代理 `https://www.ahsj.link/ramb`，那么nginx的配置为：

```nginx
location /api {
  include  uwsgi_params;
  proxy_pass  https://www.ahsj.link/rambo;
}
```

## 十五、作业

### 作业1（初级）

使用Class Component完成微博发布功能。

### 作业2（中级）

使用Function Component完成微博发布功能。

### 作业3（高级）

使用Class Component完成Todolist。

### 作业4（顶级）

使用Function Component完成Todolist。
