--- 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 ``` 生成的目录结构如下图所示: ![目录结构](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 (
hello
world
heihei
); } } 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`根组件放到``下,以开启 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 = ( ); ReactDOM.render(s, document.getElementById("root")); ``` 然后路由的配置方式有很多种,这里采用代码的方式组织路由,并将将 App.jsx 作为路由配置中心。(也可以基于配置文件,然后写一个解析配置文件的代码) 先加入登录和主页的路由,主要代码如下: ```javascript render() { const mainStyle = { fontSize: "0.16rem" }; return (
{/* 当前面的路由都匹配不到时就会重定向到/404 */}
); } ``` 名词解释: - 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 (
// 省略其他部分 ...
); } } 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:`第一篇:环境搭建`,便可以看到截止到本篇的源码。