# 《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。


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://gb.akanote.cn/react/project.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
