阅读 2018

新手搭建简洁的Express-React-Redux脚手架

写在前面

我发现网上的很多新手教程都并不是完全针对新手的,新手在使用起来也是丈二的和尚摸不到头脑,最近想用node做后端,react做前端搭建一个小系统,想把过程记录下来,从头开始搭建脚手架,绝对适合新手(本人也就是个前端小白)。既然是针对新手,那么就除去那些复杂的内容,什么服务端渲染之类的全部不考虑,使用的也绝对都是主流:Node+Express做后端,数据库采用MongoDB,前端用React+React-Router,然后使用Redux做状态管理,再搭配一个UI框架antd。其他的想用直接在这基础之上添加就可以了,新手参照下面步骤完全可以自己搭建出来项目骨架以及通过文章掌握一些知识点。 下面是脚手架截图:

首页:


用户列表页:

我还丧心病狂的为你们配置了404页(情不自禁给自己点赞):

虽然只有三个页面,但是麻雀虽小五脏俱全哦:包括前后端路由的配置、数据库的链接,数据的获取、react和redux的使用等等,老铁们,说它是react全家桶不过分吧。

项目地址请点此处,喜欢的小伙伴可以star哦!

第一步 create-react-app

Facebook官方出的脚手架,基本配置完全够用,初始化创建项目就不多BB了,你可以自己去看官网。这里只讲一句,因为要配置antd按需加载,可以按照antd官网一步步安装,不过我在按照官网安装的时候遇到了一些问题,最后还是按照自己的安装来吧。
首先,安装依赖项:

yarn add react-app-rewired react-app-rewire-less antd babel-plugin-import 
// react-app-rewired 是用来修改create-react-app的默认配置的
// babel-plugin-import 按需加载antd组件必须的插件
// react-app-wire-less antd是依赖less的
复制代码

其次,进行配置:

  • 修改package.json文件,将启动方式变为rewired启动
     "start": "react-app-rewired start",
     "build": "react-app-rewired build",
    复制代码
  • 在根目录添加config-overrides.js文件,配置antd按需加载
    /* config-overrides.js */
      const { injectBabelPlugin } = require('react-app-rewired');
      const rewireLess = require('react-app-rewire-less');
      
      module.exports = function override(config, env) {
         config = injectBabelPlugin(['import', { libraryName: 'antd', style: true }], config);
         config = rewireLess.withLoaderOptions({
           modifyVars: { "@primary-color": "#ADFF2F" }, // 可以在这里修改antd的默认配置
         })(config, env);
          return config;
      };
    复制代码

至此,就可以在组件里按需引用antd了。

第二步 配置router

// 首先,16之后react-router和react-router-dom安装一个即可
yarn add react-router-dom 
// 其次,使用BrowserRouter作为路由,同时需要history配合
yarn add history
// 最后,router的配置
...
import { Router, Switch, Route, Redirect} from 'react-router-dom';
import createBrowserHistory from 'history/createBrowserHistory';
... 
const router = (
  <Router history={history}>
    <Switch>
      <Route exact path="/home" component={App}/> // 首页路由
      <Route path="/userList" component={UserList} /> //用户列表页
      <Redirect from='' to="/home" />
    </Switch>
  </Router>
);
ReactDOM.render(router, document.getElementById('root'));
registerServiceWorker();
复制代码

第三步 node + express

接下来,就是在项目里添加后端,express。

  • 在根目录下新建文件夹server,然后新建package.json,内容如下:
        {
            "name": "server",
            "version": "1.0.0",
            "description": "server config",
            "main": "server.js",
            "author": "luffy",
            "license": "MIT",
            "dependencies": {
              "babel-cli": "^6.26.0",
              "babel-preset-es2015": "^6.24.1",
              "body-parser": "^1.18.2",
              "express": "^4.16.3",
              "mongoose": "^5.0.16"
             },
            "scripts": {
              "start": "nodemon ./server.js",
              "build": "babel ./server.js --out-file server-compiled.js",
              "serve": "node server-compiled.js"
            }
      }
    
    复制代码

    这里注意,原本的start命令应该是node,但是为了让后端也达到修改代码自动更新的效果,需要全局安装nodemon,npm install nodemon -g

  • server文件夹下新建server.js文件,内容如下:
      const express = require('express');
      const bodyParser = require('body-parser');
    
      const app = express();
      // 给app配置bodyParser中间件
      // 通过如下配置再路由种处理request时,可以直接获得post请求的body部分
      app.use(bodyParser.urlencoded({ extended: true }));
      app.use(bodyParser.json());
      // 注册路由
      const router = express.Router();
      // 路由中间件
      router.use((req, res, next) => {
        // 任何路由信息都会执行这里面的语句
        console.log('this is a api request!');
        // 把它交给下一个中间件,注意中间件的注册顺序是按序执行
        next();
      })
      // 获取用户列表信息的路由
      router.get('/user/list', (req, res) => {
        const userList = [
          {
            name: 'luffy',
            age: 24,
            gender: '男'
          },{
            name: 'lfy',
            age: 23,
            gender: '女'
          }
        ];
        res.json(userList);
      });
      // 所有的路由会加上“/api”前缀
      app.use('/api', router); //添加router中间件
      
      // express 自动帮我们创建一个server,封装的node底层http
      app.listen(3003, () => {
        console.log('node server is listening 3003');
      });
    复制代码

这里暂时没有抽离路由部分,只是测试。

第四步,前后端测试

  • 分别运行后端和前端代码
    // 后端运行
    cd server && yarn start
    // 前端运行
    yarn start
    复制代码
  • 在浏览器访问http://localhost:3003/api/user/list

看到上图说明后端运行正常。

  • 前端增加页面UserList,从后端获取数据渲染在组件里
      import React, { Component } from 'react';
      import axios from 'axios';
      import { Table } from 'antd';
      
      class UserList extends Component {
        constructor(props) {
          super(props);
          this.state = { userList:[] };
        }
      
        componentDidMount() {
          // 获取用户列表
          axios.get('/api/user/list')
          .then((res) => {
            console.log(res);
            this.setState({ userList: res.data })
          })
          .catch(function (error) {
            console.log(error);
          });
        }
      
        render() {
          const columns = [{
            title: '姓名',
            dataIndex: 'name',
            key: 'name',
          }, {
            title: '年龄',
            dataIndex: 'age',
            key: 'age',
          }, {
            title: '性别',
            dataIndex: 'gender',
            key: 'gender',
          }];
          return (
            <div>
              <h1 style={{ textAlign: 'center' }}>用户列表页</h1>
              <div style={{ width: '50%', margin: '10px auto' }}>
                <Table dataSource={this.state.userList} columns={columns} />
              </div>
            </div>
          )
        }
      }
      export default UserList;
    复制代码

httpRequest使用的是axios,使用fetch就可以,但是与时俱进,毕竟vue2.0推荐的是axios,而且文档良好,yarn add axios.

  • 同时启动,前后端。 上面启动项目需要先启动后端,再启动前端,至少需要开启两个命令行工具,一个工程两个命令行感觉很不友好,虽然以前一直这么做,O(∩_∩)O哈哈~。

    这里使用 concurrently来帮我们同时执行两条命令。

    yarn add concurrently 修改package.json下scripts代码如下:

    "scripts": {
      "react-start": "react-app-rewired start",
      "react-build": "react-app-rewired build",
      "start": "concurrently \"react-app-rewired start\" \"cd server && yarn start\"",
      "build": "concurrently \"react-app-rewired build\" \"cd server && yarn build\"",
      "test": "react-scripts test --env=jsdom",
      "eject": "react-scripts eject"
    },
    复制代码

    接下来,秩序执行yarn start就可以同时启动前端和后端了。

  • 解决跨域

    第一种 create-react-app proxy属性(推荐)

    只需在package.json增加下面这一条代码,即可实现跨域获取数据,本项目前端是3000端口,后端是3003端口。配置如下:

    "proxy": "http://127.0.0.1:3003"

    第二种 node端解决跨域
    //allow custom header and CORS
      app.all('*',function (req, res, next) {
        res.header('Access-Control-Allow-Origin', '*');
        res.header('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild');
        res.header('Access-Control-Allow-Methods', 'PUT, POST, GET, DELETE, OPTIONS');
      
        if (req.method == 'OPTIONS') {
          res.send(200); /让options请求快速返回/
        }
        else {
          next();
        }
      });
    复制代码

最后,需要特别注意的是,项目使用node+express作为后端提供API服务,因此后端并无任何渲染页面,这跟使用node+express搭建博客系统等有本质区别,所以我们并没有后端的渲染页面,也就是view,因此,所有的路由都需要使用res.json()作为返回而不能使用res.render()作为返回,否则会报错Express Error: No default engine was specified and no extension was provided

第四步 连接数据库

数据库采用MongoDB,因此,node端需要安装mongoose。yarn add mongoose

// 下面是关于mongoose的几个概念:
Schema: 一种以文件形式存储的数据库模型骨架,不具备数据库的操作能力
Model: 由Schema发布生成的模型,具有抽象属性和行为的数据库操作对象
Entity: 由Model创建的实体,它的操作也会影响数据库
Schema、Model、Entity的关系请牢记,Schema生成Model,Model创造Entity,Model和Entity都可对数据库操作造成影响,但Model比Entity更具操作性。 
复制代码

这里关于MongoDB的安装就不多说了,安装完之后的各种配置直接问度娘就可以了。安装完之后可以使用各种可视化工具来查看数据库。这里我安装的是robo,并且创建了一个数据库luffy_blog,没错,后续可能会用这个脚手架搭建一个博客,因为我发现作为一个新手,有些教程确实不是很友好

如上图,我已经安装好了MongoDB,并且新建了数据库luffy_blog,并且新增了一条用户数据,接下来我们就使用express配合MongoDB获取数据传递给前端:

  • server目录下新建db文件夹,用于处理数据库相关

    目录结构如下:
    - db
      - config // MongoDB的配置文件
      - models // 数据模型model
      - schemas // 模型骨架schema
    复制代码
  • 对MongoDB进行配置

    config文件夹

    • config.js
      // 数据库地址: 'mongodb://用户名:密码@ip地址:端口号/数据库';
      // 一般如果没设置用户名和密码直接写IP地址就可以,数据库你可以新建一个
      module.exports = {
        mongodb : 'mongodb://127.0.0.1:27017/luffy_blog'
      };
      复制代码
    • mongoose.js
      // 用于连接数据库并且定义Schema和Model
      const mongoose = require('mongoose');
      const config = require('./config');
      module.exports = ()=>{
          mongoose.connect(config.mongodb);//连接mongodb数据库
          // 实例化连接对象
          var db = mongoose.connection;
          db.on('error', console.error.bind(console, '连接错误:'));
          db.once('open', (callback) => {
              console.log('MongoDB连接成功!!');
          });
          return db;
      };
      复制代码
      上面就完成了连接数据库的操作,接下来在server.js添加如下代码即可:
      // 连接mongodb数据库
      const mongoose = require('./db/config/mongoose');
      const db = mongoose();
      复制代码

    接下来就是创建数据骨架和模型,完全按照mongoose的模板来就可以,接下来就以用户模型user为例。

    schemas文件夹

    // UserSchema.js
      const mongoose = require('mongoose');
      const Schema = mongoose.Schema;
      //创建Schema
      const UserSchema = new Schema({
          name: String,
          password: String,
          email: String
      });
      module.exports = UserSchema;
    复制代码

    models文件夹

      // UserModel.js
      const mongoose = require('mongoose');
      const UserSchema = require('../schemas/UserSchema');
      //创建model,这个地方的user对应mongodb数据库中users的conllection。
      const User = mongoose.model('user',UserSchema);
      module.exports = User;
    复制代码

    万事俱备,只欠东风。数据库我们有了,数据我们也有了,express对数据库的连接也已经完成了,接下来只剩下将数据从数据库取出以API形式返给前端。例如:我们将接口定义为如下形式:

    接口名称: /api/user/list
    后端路由:
      const express = require('express');
      const User = require('../db/models/UserModel');// 引入模型
      
      const router = express.Router();
      
      router.get('/list', (req, res) => {
        User.find({}, (err, data) => {
          if (err) 
            next(err);
          res.json(data);
        });
      });
    浏览器访问:
        浏览器可以登录http://localhost:3003/api/user/list访问数据
    前端页面获取:
      axios.get('/api/user/list')
            .then(res => {
              return res.data;
            })
            .catch((error) => {
              console.log(error);
            }); 
    复制代码

    最后我们将数据渲染到页面上,效果如下:

以上,数据库部分引入完成,详细代码可以clone项目查看。

第五步 增加redux进行状态管理

重点来了,重点来了,重点来了(重要的事情说三遍)。提到react的项目,怎么可能不使用redux来进行状态管理呢。当然,现在的生态圈可能很多人对你说,用mobx吧,更简单一般项目来说足够了,但是我觉得,既然更简单,那么把redux学会了,再去看mobx吧,你们认为呢?

声明一点,这里不是讲解什么是redux,这里是讲解怎么在项目里使用redux,详细的讲解文章可以去看网上教程,入门的话推荐阮大神的,redux入门系列点此前往

接下来,我就当你已经了解redux是干什么的了,只不过怎么引入到项目里不是很清楚,因为网上的文章要不就是太深,深到还没读完第一段你就放弃了;要不就是太浅,永远都是计数器实例,看完我也不清楚怎么在项目里进行状态管理。如果是上面那样,恭喜你,总算遇到我了,下面绝对会让你学会如何使用redux。

  • 安装redux相关依赖

    yarn add redux react-redux redux-logger redux-thunk 总所周知(我就当你知道),redux依赖各种中间件,我们这里为了简易起见只使用redux-logger和redux-thunk,一个是输出redux日志,另一个是让我们方便进行异步操作。更具体的,去看官方文档和各种教程。

  • 前端增加redux目录

    // 目录结构,ok就是redux三剑客
    - redux
      - actions
      - reducers
      - store
      - middleware
    复制代码
  • 完成各种基础配置

    接下来,你只需按照下面几步,就可以在项目里引入redux。

    • 配置store
    // configureStore.js
      import { createStore, applyMiddleware } from 'redux';
      import thunkMiddleware from 'redux-thunk';
      import { logger } from '../middleware';
      import rootReducer from '../reducers';
      
      const nextReducer = require('../reducers');
      
      function configure(initialState) {
        const create = window.devToolsExtension
          ? window.devToolsExtension()(createStore)
          : createStore;
      
        const createStoreWithMiddleware = applyMiddleware(
          thunkMiddleware,
          logger,
        )(create);
      
        const store = createStoreWithMiddleware(rootReducer, initialState);
      
        if (module.hot) {
          module.hot.accept('../reducers', () => {
            store.replaceReducer(nextReducer);
          });
        }
        return store;
      }
      export default configure;
    复制代码

    配置好上面文件之后,肯定是一堆报错,没关系,我们一点点来。上面用到了logger中间件以及reducer,接下来就配置这两个。

    • 配置reducer和middleware
      /middleware/index.js
      // 你没有看错,中间件就是人家已经造好的轮子,我们直接拿来用就行。
      import logger from 'redux-logger';
      export {
        logger,
      };
      
      /reducers/index.js
      import { combineReducers } from 'redux';
      import user from './user/index'; // 一般会配置多个reducer然后使用combineReducers将他们合并起来
      const rootReducer = combineReducers({
        user
      });
      
      export default rootReducer;
      复制代码

配置好logger的效果如下:

  • 配置action

    说是配置action,其实action并不是配置得来的,而是我们将整个应用的状态都交给了redux来进行管理,所以我们如果想进行数据的更新,就必须通过redux来进行,redux为我们提供的更新数据的方式就是dispatch action。下面就以获取用户列表数据为例,真真切切的使用redux。

    /actions/User.js
    import {
      FETCH_ALL_USER_LIST,
      FETCH_ALL_USER_LIST_SUCCESS,
      FETCH_ALL_USER_LIST_FAIL
    } from '../../constants/ActionTypes';
    import axios from 'axios';
    
    // 获取用户列表
    const getAllUserList = () => ({
      type: FETCH_ALL_USER_LIST,
    });
    const getAllUserListSuccess = (payload) => ({
      type: FETCH_ALL_USER_LIST_SUCCESS,
      payload
    });
    const getAllUserListFail = () => ({
      type: FETCH_ALL_USER_LIST_FAIL
    });
    export const fetchAllUserList = () => (dispatch) => {
      dispatch(getAllUserList());
      // 获取用户列表
      // 因为设置了proxy的缘故,所以不需要写http://localhost:3003
      // 会自动定向到后端服务器
      return axios.get('/api/user/list')
              .then(res => {
                return dispatch(getAllUserListSuccess(res.data));
              })
              .catch((error) => {
                console.log(error);
                dispatch(getAllUserListFail());
              }); 
    };
    
    复制代码

    上面就是一个完整的触发action获取数据的过程,一般包括请求数据,请求数据成功和请求数据失败三个阶段。

  • 将组件分为容器组件containers和展示组件components

    同理,这两者区别还是去看大牛们的讲解,他们讲的很细致,我这里只讲一点,既然引入了redux,那么数据肯定不是在页面里componentDidMount()通过ajax获取到的,上面提到了,是通过action触发的,因此需要状态组件将页面所需的state和数据操作API传给展示组件。

    // /containers/UserList.js容器组件
      import { connect } from 'react-redux';
      import UserList from '../components/UserList';
      import {
        fetchAllUserList
      } from '../redux/actions/User';
      
      const mapStateToProps = state => ({
        list: state.user.userList.list,
      });
      
      const mapDispatchToProps = dispatch => ({
        fetchAllUserList() {
          dispatch(fetchAllUserList());
        }
      });
      
      export default connect(
        mapStateToProps,
        mapDispatchToProps
      )(UserList);
      
    // /components/UserList.js 展示组件
      import React, { Component } from 'react';
      import { Table } from 'antd';
      
      class UserList extends Component {
        constructor(props) {
          super(props);
          this.state = { userList: this.props.list };
        }
      
        componentDidMount() {
          this.props.fetchAllUserList(); //获取数据渲染页面
        }
      
       ...
      }
      export default UserList;
    复制代码

写在最后

总算是写完个人的第一篇纯技术性文章了,虽然没什么技术性,但是我觉得还是很有意义的,至少我觉得我写的东西应该会让新手或者在校生理解吧,希望大家多多指正!最后的最后,放上代码,小伙伴如果喜欢不要吝惜你们的star! 接下来可能会用这个脚手架做一些系统之类的练练手,主要目的也是增强能力。

GitHub:https://github.com/luffyZh/express-react-scaffold.git 快速通道

关注下面的标签,发现更多相似文章
评论