---
id: "20190626"
date: 2019/06/26 10:58:00
title: "从零开始react实战:云书签1- react环境搭建"
tags: ["react", "antd", "less", "create-react-web"]
categories:
  - "前端"
  - "react"
---

总览篇：[react 实战之云书签](https://blog.fleyx.com/blog/detail/20190625)

本篇是实战系列的第一篇，主要是搭建 react 开发环境,在`create-react-app`的基础上加上如下功能：

- antd 组件库按需引入 ，支持主题定制
- 支持 less 语法，并使用 css-module
- 配置路由
- 支持 http 请求
- 配置 redux

**注意**:需要 node 版本大于 8.0.

## 创建 create-react-app

1. 安装

```bash
npm install -g create-react-app
```

2. 创建 react 应用

```bash
create-react-app bookmark-world
```

<!-- more -->

生成的目录结构如下图所示：

![目录结构](https://raw.githubusercontent.com/FleyX/files/master/blogImg/20190625160702.png)

## 配置 antd,less

有两种方法能够对其配置进行修改：

- 通过`npm run eject`暴露出配置文件，然后 修改这些配置文件，相比于下面的方法不太优雅，因此不考虑.
- 通过`react-app-rewired`覆盖配置.

后续需要修改配置的都用第二种--覆盖配置。

### 首先安装依赖

在 2.1.x 版本的 react-app-rewired 需要配合`customize-cra`来进行配置覆盖。所以需要安装如下依赖：

- react-app-rewired ,配置覆盖
- customize-cra ,配置覆盖
- antd ,ui 库
- babel-plugin-import ,按需引入 antd
- less ,less 支持
- less-loader ,less 支持

代码如下：

```bash
npm install --save react-app-rewired customize-cra antd babel-plugin-import less less-loader
```

### 修改 package.json

用`react-app-rewired`替换掉原来的`react-scripts`

```json
/* package.json */
"scripts": {
-   "start": "react-scripts start",
+   "start": "react-app-rewired start",
-   "build": "react-scripts build",
+   "build": "react-app-rewired build",
-   "test": "react-scripts test",
+   "test": "react-app-rewired test",
}
```

### 创建 config-overrides.js

在项目根目录,也就是`package.json`的同级目录创建`config-overrides.js`文件.内容如下：

```javascript
const { override, fixBabelImports, addLessLoader } = require("customize-cra");

module.exports = override(
  fixBabelImports("import", {
    libraryName: "antd",
    libraryDirectory: "es",
    style: true
  }),
  addLessLoader({
    localIdentName: "[local]--[hash:base64:5]",
    javascriptEnabled: true,
    modifyVars: { "@primary-color": "#1DA57A" }
  })
);
```

### 使用 css-module

要使用 css-module 需要将 css 文件命名为`fileName.module.less`，然后就能在组件中引入并正常使用了，如下：

**注意默认情况下后缀必须是.module.less 才能用 css-module 的写法**

```javascript
import React, { Component } from "react";
import { Button } from "antd";
import styles1 from "./index.module.less";

class Hello extends Component {
  render() {
    return (
      <div className={styles1.main}>
        hello
        <div className={styles1.text}>world</div>
        <Button type="primary">你好</Button>
        <div className="text1">heihei</div>
      </div>
    );
  }
}

export default Hello;
```

## 配置路由

首先修改 src 目录结构。改成如下所示：

![目录结构](https://raw.githubusercontent.com/FleyX/files/master/blogImg/20190627115439.png)

目录解释：

- assets: 存放图标，小图片等资源文件
- components：存放公共组件
- layout: 存放样式组件，用于嵌套路由和子路由中复用代码
- pages: 存放页面组件
- redux：存放 redux 相关
  - action: 存放 action
  - reducer: 存放 reducer 操作
- util: 工具类

删除`serviceWorker.js`文件，并在`index.js`中删除和它相关的代码。这个是和离线使用相关的。

然后安装`react-router`依赖:

```bash
cnpm install --save react-router-dom
```

从路由开始就能体会到 react 一切都是 js 的精髓，react-router-dom 提供了一些路由组件来进行路由操作。本程序使用`history`路由。

首先修改`index.js`根组件放到`<BrowserRouter>`下，以开启 history 路由。代码如下：

```javascript
// index.js
import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import { BrowserRouter } from "react-router-dom";

const s = (
  <BrowserRouter>
    <App />
  </BrowserRouter>
);

ReactDOM.render(s, document.getElementById("root"));
```

然后路由的配置方式有很多种，这里采用代码的方式组织路由，并将将 App.jsx 作为路由配置中心。(也可以基于配置文件，然后写一个解析配置文件的代码)

先加入登录和主页的路由，主要代码如下：

```javascript

render() {
  const mainStyle = {
    fontSize: "0.16rem"
  };
  return (
    <Provider store={store}>
      <div className="fullScreen" style={mainStyle}>
        <Switch>
          <Route exact path="/" component={Main} />
          <Route exact path="/public/login" component={Login} />
          <Route exact path="/404" component={NotFound} />
          {/* 当前面的路由都匹配不到时就会重定向到/404 */}
          <Redirect path="/" to="/404" />
        </Switch>
      </div>
    </Provider>
  );
}
```

名词解释：

- Switch: 该组件表示只匹配一个，匹配到后不再继续往下匹配
- Route：路由组件
- exact：表示完全匹配，如果开启这个，`/`只匹配`/`,否则匹配所有的路径
- Redirect：重定向组件，当前面的都不匹配就会匹配这个（因为没有开启`exact`且 path 为`/`),然后重定向到`/404`

后续用到嵌套路由时会更加深入的讲解路由相关。

## 配置 http 请求工具

http 请求工具这里选择的是`axios`。

首先安装依赖：

```bash
cnpm install --save axios
```

然后编写工具类`util/httpUtil.js`,代码如下：

```javascript
// httpUtil.js

import { notification } from "antd";
import axios from "axios";

//定义http实例
const instance = axios.create({
  //   baseURL: "http://ali.tapme.top:8081/mock/16/chat/api/",
  headers: {
    token: window.token
  }
});

//实例添加拦截器
instance.interceptors.response.use(
  function(res) {
    return res.data;
  },
  function(error) {
    console.log(error);
    let message, description;
    if (error.response === undefined) {
      message = "出问题啦";
      description = "你的网络有问题";
    } else {
      message = "出问题啦:" + error.response.status;
      description = JSON.stringify(error.response.data);
      //401跳转到登录页面
    }
    notification.open({
      message,
      description,
      duration: 2
    });
    setTimeout(() => {
      if (error.response && error.response.status === 401) {
        let redirect = encodeURIComponent(window.location.pathname + window.location.search);
        window.location.replace("/public/login?redirect=" + redirect);
      }
    }, 1000);
    return Promise.reject(error);
  }
);

export default instance;
```

主要实现了如下功能：

- 自动添加 token，设计前后端通过 jwt 做认证，因此每个请求都要加上 token
- 响应预处理，如果有错误，自动弹窗提示。如果响应码为 401，重定向到登录页面。

## 配置 redux

redux 算是 react 的一大难点。这里我们可以把 redux 理解成一个内存数据库，用一个对象来存储所有的数据.

对这个数据的修改有着严格的限制，必须通过 reducer 来修改数据，通过 action 定义修改的动作。

这里以用户登录数据为例。

### 定义

1. 首先定义 action，创建文件`redux/action/loginInfoAction.js`,代码如下：

```javascript
// 定义登录信息在store中的名字
export const DATA_NAME = "loginInfo";

//定义修改loginInfo type
export const CHANGE_LOGIN_INFO = "changeLoginStatus";

export const changeLoginInfo = (token, userInfo) => {
  return {
    type: CHANGE_LOGIN_INFO,
    data: {
      token,
      userInfo
    }
  };
};
```

- CHANGE_LOGIN_INFO :定义操作类别
- changeLoginInfo: 定义一个 action，在组件中调用，传入要修改的数据，在这里加上 type 上传递到 reducer 中处理.

2. 定义 reducer，创建文件`redux/reducer/loginInfo.js`,代码如下：

```javascript
import * as loginAction from "../action/loginInfoAction";

function getInitData() {
  let token, userInfo;
  try {
    token = localStorage.getItem("token");
    userInfo = JSON.parse(localStorage.getItem("userInfo"));
  } catch (e) {
    console.error(e);
    token = null;
    userInfo = null;
  }
  window.token = token;
  window.userInfo = userInfo;
  return {
    token,
    userInfo
  };
}

const LoginStatusReducer = (state = getInitData(), action) => {
  switch (action.type) {
    case loginAction.CHANGE_LOGIN_INFO:
      return { ...action.data };
    default:
      return state;
  }
};

export default LoginStatusReducer;
```

- getInitData 方法用于初始化 userInfo 数据，这里写的比较复杂，会先从 localeStore 中取数据，然后挂载到 window 中，方便`httpUtil`中获取 token。
- LoginStatusReducer 方法用于处理 action 中的数据，输出处理后的 loginInfo 数据。

3. 编写 reducer 汇总类(redux/reducer/index.js)，所有 reducer 都要汇总到一个方法中，这样就能生成整个系统的 store 对象。代码如下：

```javascript
import { combineReducers } from "redux";
import { DATA_NAME } from "../action/loginInfoAction";
import loginInfo from "./loginInfo";

const data = {};
data[DATA_NAME] = loginInfo;

const reducer = combineReducers(data);

export default reducer;
```

4. 编写`redux/index.js`，这里生成真正的数据对象，代码如下：

```javascript
import { createStore } from "redux";
import reducer from "./reducer";

const store = createStore(reducer);

export default store;
```

5. 最后将 store 绑定到根节点(App.js)中即可,修改部分如下：

![](https://raw.githubusercontent.com/FleyX/files/master/blogImg/20190627165936.png)

![](https://raw.githubusercontent.com/FleyX/files/master/blogImg/20190627170032.png)

### 使用

这里以登录页为例，学习如何获取到 loginInfo 和修改 loginInfo.

1. 创建登录页组件，`pages/public/Login/index.js`
   登录页代码如下：

```javascript
import React, { Component } from "react";
import queryString from "query-string";
import { Button, Input, message } from "antd";
import IconFont from "../../../components/IconFont";
import styles from "./index.module.less";
import { connect } from "react-redux";
import { changeLoginInfo, DATA_NAME } from "../../../redux/action/loginInfoAction";
import axios from "../../../util/httpUtil";

function mapStateToProps(state) {
  return state[DATA_NAME];
}

function mapDispatchToProps(dispatch) {
  return {
    updateLoginInfo: (token, userInfo) => dispatch(changeLoginInfo(token, userInfo))
  };
}

class Login extends Component {
  constructor(props) {
    super(props);
    this.state = {
      username: "",
      password: ""
    };
    this.query = queryString.parse(window.location.search);
  }

  usernameInput = e => {
    this.setState({ username: e.target.value });
  };
  passwordInput = e => {
    this.setState({ password: e.target.value });
  };

  submit = () => {
    axios.post("/public/login", this.state).then(res => {
      localStorage.setItem("token", res.token);
      localStorage.setItem("userInfo", JSON.stringify(res.userInfo));
      window.token = res.token;
      window.userInfo = res.userInfo;
      message.success("登录成功");
      this.props.updateLoginInfo(res.token, res.userInfo);
      if (this.query.redirect) {
        this.props.history.replace(decodeURIComponent(this.query.redirect));
      } else {
        this.props.history.replace("/");
      }
    });
  };

  render() {
    return (
      <div className="fullScreen flex main-center across-center">
        // 省略其他部分
        <Button type="primary" onClick={this.submit}>
          登录
        </Button>
        ...
      </div>
    );
  }
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Login);
```

其中最关键的是下面三个部分：

- mapStateToProps:本方法从整个 store 中获取需要的数据，传递到 Login 组件的 props 中。
- mapDispatchToProps:本方法用于修改 store 数据，返回的函数对象也会绑定到 Login 组件的 props 中,其中的 dispath 参数，用于调用 reducer 中的处理函数，根据 changeLoginInfo 返回的 action。
- connect 方法用于将上面两个函数和 Login 组件绑定起来，这样就能在 props 中获取到了。**如果还有 withRouter**,应将 withRouter 放在最外层。

目前登录访问的接口为 yapi 的 mock 数据，真正的后台代码将会在后面编写。

## 结尾

作为一个刚开始学习 react 的菜鸟，欢迎各位大牛批评指正。

源码：[github](https://github.com/FleyX/ChatRoom),切换到 tag:`第一篇：环境搭建`，便可以看到截止到本篇的源码。

