从零到一的react.js+node.js+express.js+mysql产品开发全流程

序言

组长说要使自己对产品在技术层面有一个清晰且足够的了解,最好自己动手开发一个迷你产品,例如todolist,因为公司有提供员工自学使用的服务器,所以我就来试试了,而且一步一步的记录自己的学习过程,这个过程有请教问题、出现的问题、解决问题的方法和用到的技术栈等等。

以下开发步骤序号不代表产品开发绝对的顺序,是博主第一次学习走的顺序,仅供参考,建议先阅读黑色标题后再细读。

说明:博主使用的编程工具是vs code(https://code.visualstudio.com/),打开的命令窗口都是从vs code里打开,执行安装依赖的窗口也是从vs code里打开的。示例图如下:

GIF展示图如下:

一、预备工作

1. 服务器预备工作

生产环境(本部分初次建议不要阅读)

1.1 登录服务器(使用的是腾讯云,jumpserver;也可以是阿里云服务器,这里以腾讯云服务为例),进入到“我的资产”,可查看自己的服务器,然后点击“连接”进入到命令窗口(因为我司已配置服务器名称、ip,至于怎么配置可百度或者向他人寻求帮助)

 1.2 进入之后,切换到超级用户,不然会有很多的限制。操作命令:su - root ,然后它会提示你要输入密码,这个密码是你在配置服务器的时候设置的,或者自动生成的,总之需要你自己记下来。随后切换成功后,输入命令:ls ,可查看root用户下的所有文件和文件夹。

1.3  这个时候最好新建一个文件夹用于保存你的项目代码,以博主为例。输入命令:mkdir self-study-tc ,回车后再输入:ls ,再次回车后可查看到自己新建的文件夹。

1.4 进入创建的文件夹,输入命令:cd self-study-tc ,这个时候由于该文件夹为空,输入ls不会有任何文件显示,至此,服务器的预备工作第一阶段已完成。 

本地环境

1.5 根据自己的电脑配置去官网下载对应最新版本mysql(https://dev.mysql.com/downloads/mysql/),下载后解压到你想存放的路径(以我为例:D:/mysql-8.0.16-winx64),然后打开cmd,根据该链接(https://www.runoob.com/mysql/mysql-install.html)且依据计算机的配置和自己的sql安装路径选择对应的操作。但这个链接的坑有点多,个人建议最好的方式就是根据你电脑的配置去百度一下最好,比如win10的操作系统就搜索 “ win10 mysql-8.0.16 安装教程 ”,然后再查看文章。个人推荐这个链接:
https://jingyan.baidu.com/article/ab0b5630377e5ac15afa7d30.html

1.6 先不论大家在1.5节里的操作是否一切正常(我遇到了很多的坑,特别是设置root用户和密码的时候坑很多,密码最好设置简单点,比如:123456),我推荐的链接进行至第八个步骤即可,如果你能和图示显示的结果一致,那说明你的mysql已经安装并可以正常启动了。

上图表示你已经启动了数据库,并且能够登录数据库,即数据库部分的初始配置已经全部完毕。 

1.7 下载sqlyog(使用方便),然后创建一个新连接,输入你的账号root和密码,如果能连接成功那就万事大吉(mysql服务已启动为前提),如果没有,而且报2058的错误,那么可以参考链接:https://www.cnblogs.com/hualalalala/p/9344772.html。如果是其他的错误,不好意思,你需要自己上网搜索答案。

1.8 创建成功之后,自己新建一个表,然后填充一些测试数据,便于前端获取数据,使接口对的上。

2. 前端预备工作1(传统方法搭建React-Native App移动端项目,非移动端项目可跳过不看)

2.1 环境搭建,具体的环境搭建这里不再赘述,可参考官网链接:https://reactnative.cn/docs/getting-started.html ,请注意,该过程是一个比较繁琐且麻烦的过程,需要较好的耐心一步一步走,不出意外的话会很顺畅的安装完成,如果遇到问题则需要自己慢慢搜索答案去解决。
       上述过程一直进行到编译并运行应用,在这个步骤,android studio会报需要硬件加速器的错误,这个时候一般是你的电脑并未将电脑的virtualization technology打开,因此你需要重启你的电脑并进入到bios界面,重启的时候按F1健(这个是我的电脑:联想的快捷键,你们的要自己上网搜索),进入之后,其实在网上有很多种答案,说是在security/virtualization technology这里,但是很奇葩的是我的电脑并不是!还以为我的电脑并不支持VT呢!后台是我的师傅在CPU setup里找到的,真鸡儿坑!

2.2 前端项目搭建之后的文件夹目录如下,另外服务开启后,模拟器上显示的页面与官网不一样,但殊途同归。

2.3 安装成功并运行后的安卓模拟机显示图:

3. 前端预备工作2(新式工具expo搭建React-Native App移动端项目,非移动端项目可跳过不看)

3.1 介绍。在搭建react native app项目时推荐使用最新的Expo工具链。你可以完全不用去了解Xcode相关开发环境。Expo CLI会为你设置好开发环境,方便你快速开发App。 如果你熟悉原生的开发过程,推荐使用React Native CLI,即上述“2. 前端预备工作1”。附:windows系统必须先安装android studio,即先有安卓模拟机服务;ios系统必须先安装Xcode,即先有ios模拟机服务。

3.2 环境搭建。首先你需要Nodejs版本10+,然后使用npm安装Expo CLI command line utility。
安装expo-cli:npm install -g expo-cli

注:这里一般的计算机在安装expo-cli的时候,都是会成功的,但博主的一直报错!换了其他的机器也是一样的!不明白是npm包的问题还是计算机自身系统的问题,搞的我很崩溃,因为我组长是mac电脑,他根据安装步骤一步一步来完事了,所以这是我不用react-native app做项目的原因,因为连开发环境都搭不起来,但在4月份的时候,博主的电脑是可以的...也许真的是因为npm包的问题?有知道的筒子们可以留言交流。

报错如下图:无(2019.7.8即今日更新的时候,突然我再去执行npm install -g expo-cli命令的时候,法克!它居然安装成功了!现在倒是九成肯定是npm包的问题了!但我依稀还记得当初模糊报错代码:npm ERROR:... ... ... @expo/image-cli)

安装完成如下图:

3.3 创建项目。输入命令:

(1)expo init todolist-expo

(2)cd todolist-expo

(3)npm start 或 expo start

3.4 完毕之后,会在本地启动一个开发服务,并在你默认的浏览器打开一个页面,页面显示图如下(打马赛克的地方意思是在该步骤是未出现的):

然后要把项目在你的模拟机上运行,根据电脑的系统选择不同的模拟机,启动模拟机有两种方式(以安卓为例):
(1)直接在命令窗口输入:a 

(2)在页面左侧点击:Run on Android device/emulator

启动之后,模拟机显示图如下(第一次进入毕竟耗时,它要加载很多文件。另外模拟机启动的时候它会让你设置模拟机的一些数据,忽略或关闭就好。):

进程图:

结果图:

3.5 安装之后的项目文件目录结构图如下:

4. 前端预备工作3(react-rack-cli脚手架,PC端项目,推荐学习)

该脚手架是前端组的大佬自己写好上传至npm包的,公开使用的)搭建react PC端项目,博主选择该种方式,因为PC端的具有代表性~

4.1 介绍。React-rack-cli 是一个基于 React + ant design 进行快速开发的PC端完整系统。通过npm全局安装后就可以快速的的创建一个react项目

4.2 环境搭建。基础环境:Node.js (>=6.x, 8.x preferred), npm version 3+ and Git.
使用npm全局安装react-rack-cli:npm install -g react-rack-cli
然后可输入命令:react-rack --version 查看当前版本号

4.3 创建项目。输入命令:react-rack init todolist (todolist是项目名称)
然后输入命令:cd todolist
再输入命令:npm install

4.4 前端项目文件目录图(初始没有的得自己加):

4.5 后续工作。在根目录下创建一个logs文件夹:mkdir logs ,随后打包并运行服务。
打包:npm run build-dev
运行:node server.js

4.6 运行成功之后的效果图:

5. 后端预备工作(本次以express为例)

5.1 后端开发环境搭建。可使用基于node平台的expresskoa作为后台开发框架,本文选用的是express。express的安装和使用方法可参考官网介绍:http://www.expressjs.com.cn/
(1)安装express。输入命令:npm install -g express
(2)创建项目名。输入命令:express todolist-express
(3)进入并安装依赖。输入命令:cd todolist-express ,再出入命令:npm install
(4)启动服务。输入命令:npm start
执行(1)(2)(3)命令后的显示就不截图了,9.9成的概率都是会成功的。
运行之后的命令窗口显示为:

网页显示为:

二、对接工作

6. 前后端对接工作1(以expo搭建的React Native App移动端为例,非移动端项目可不看)

6.1 既然前后端的开发服务我们都能运行了,那么就该衔接两端,使数据能够跑通。即前端发送一个请求,后端接收请求并返回数据,前端接收数据后并作出修改和渲染。那就先按照前端-后端-前端的顺序来介绍吧。

6.2 前端请求封装。在前端项目根目录下新建一个文件夹api,用于保存请求接口,在该文件夹下新增index.js和api.js两个文件,api.js用于对接口的封装;index.js用于配置路径前缀、请求头部以及发出请求函数(请求方式、请求路径、请求参数和返回数据等)的封装。代码参考如下(api.js是我一位技术大牛写的):

 api.js

/*
 * @Description: Api 封装
 */

import superagent from 'superagent';

const methods = [
  'get',
  'head',
  'post',
  'put',
  'del',
  'options',
  'patch'
]

export default class Api {
  constructor(opts) {
    this.opts = opts || {};
    if (!this.opts.baseURI) {
      throw new Error('baseURI option is requiresd');
    }

    const _self = this;
    methods.forEach(method => {
      _self[method] = (path, { params, data } = {}) => new Promise((resolve, reject) => {
        const request = superagent[method](_self.opts.baseURI + path);
        if (params) {
          request.query(params);
        }

        if (_self.opts.headers) {
          request.set(_self.opts.headers);
        }

        if (data) {
          request.send(data);
        }

        request.end((err, { body, text } = {} ) => {
          return err ? reject(body || text || err) : resolve(body || text);
        });
      });
    });
  }
}

请注意:因为引入了superagent,所以要先安装它,执行命令:npm install superagent --save ,不然会报错的噢

index.js

import Api from './api';

const api = new Api({
  baseURI: 'localhost:3000',// 就是后端启动的路径,如果遇到请求错误问题,可尝试写成http:// + (your local ip address) + :3000。例如我的:http://10.108.9.56:3000
  headers: {
    'Accept': 'application/json',
    'Content-Type': 'application/json'
  }
});

export function AjaxServer(method, path, params = {}, data) { 
  return api[method](path, { params, data });
}

6.3 在同一个路径下再创建一个用于测试接口的js文件,就命名为todo.js,很显然这个文件就是用于放置关于todo的请求函数,代码示例如下:

import { AjaxServer } from './index'

export async function getTodoList(params) {
  return AjaxServer('get', '/app/todolist', params);
}

记得一定要引入AjaxServer,不然也会报错

6.4 后端接收请求函数并响应。既然前端(todolist-expo)发起了一个getTodoList请求,走的路径为'/app/todolist',那么我们就要把这个路径拿到,并把它想要的数据返回回去。在后端(todolist-express)的根目录下的routes下新建一个文件夹app,用于存放app端的接口文件,进去后再新建一个todo.js文件,写入以下代码:

var express = require('express');
var router = express.Router();

/* GET todo listing. */
router.get('/', function(req, res, next) {
  res.send({
    code: 1,
    msg: '成功',
    data: [
      {
		id: 0,
		name: 'Learning node.js on Monday'
	  },
	  {
		id: 1,
		name: 'Learning react.js on Tuesday'
	  },
	  {
		id: 2,
		name: 'Learning vue.js on Wednesday'
	  },
	  {
		id: 3,
		name: 'Learning angular.js on Thursday'
	  },
	  {
		id: 4,
		name: 'Learning express on Friday'
	  },
	  {
		id: 5,
		name: 'Learning koa on Saturday'
	  },
	  {
		id: 6,
		name: 'Learning react-native on Sunday'
	  }
    ]
  });  
});

module.exports = router;

然后在后端(todolist-express)的根目录下找到app.js文件,添加以下代码:

var todoRouterApp = require('./routes/app/todo');
app.use('/app/todolist', todoRouterApp);

意思不用我多说,懂一点js基础的同学都看的懂,我们接收到请求后并返回了数据,这下应该把前后端连接在一起了,然后在前端(todolist-expo)的App.js添加一个button并发出一个请求,这个就不多解释了,代码:

import React from 'react';
import { StyleSheet, Text, View, Button } from 'react-native';

import { getTodoList } from './api/todo';

export default class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = { fistShow: 'install node', todoList: '' };
  }

  onPressLearnMore = async () => {
    try{
      const response = await getTodoList();
      this.setState({ todoList: response.data })
    }
    catch(e){
      console.warn(e.message)
    }
  }
  render(){
    return (
      <View style={styles.container}>
        <Button title={this.state.fistShow} onPress={this.onPressLearnMore} />
          <View>
            {
              this.state.todoList == '' ? <Text style={styles.contentTxt}>暂无数据</Text> : this.state.todoList.map((item, index) => {
                return <Text style={styles.contentTxt} key={index}>{item.name}</Text>
              })  
            }
          </View>
      </View>
    );
  } 
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
  contentTxt: {
    marginTop: 20
  }
});

最后重新打包,并启动模拟机看看运行效果:

然后点击当中的按钮,会发现提示以下warning:

这是未设置跨域问题提示的warning,所以我们还要设置跨域。

6.5 设置跨域。 添加如下代码:

// app端设置跨域访问
app.all('/app/*', function(req, res, next) {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Headers', 'X-Requested-With, Content-Type'); 
  res.header('Access-Control-Allow-Methods', '*');
  res.header('Content-Type', 'application/json;charset=utf-8');
  res.header('Access-Control-Allow-Credentials', 'true'); //一定要设置这一句
  next();
});

注意:这段代码一定要放在调用接口前

6.6 重启后台服务,然后再次运行并点击那个按钮,会发现下面的text发生了变化!就是后台返回给我们的数据!

GIF演示图如下:

7. 前后端对接工作2(以react-rack-cli搭建的PC端为例,推荐学习

由于PC端的项目比App稍复杂一些,所以我们先前端后后端的顺序来开发。

7.1 前端页面代码与请求。废话不多说,直接上代码(记得把原本一些不需要的代码进行注释):

import React from 'react';
import {bindActionCreators} from 'redux';
import PropTypes from 'prop-types';
import {connect} from 'react-redux';
import { Layout, Button } from 'antd';
// import Header from '../../components/Header';
import './index.less';

import { getTodoList } from '../../actions/todo';

class App extends React.Component {
  state = {
    firstShow: 'install node'
  }
  componentWillMount() {
  }
  componentWillReceiveProps() {
  }
  handleClick = () => {
    this.props.getTodoList();
  }
  render() {
    return (
      <div className='app'>
        {/* <Header /> */}
        <div className='content-main'>
          {/* {this.props.children} */}
          <Button className='content-button' onClick={this.handleClick}>{this.state.firstShow}</Button>
          <div className='content-wrapper'>
            {
              this.props.todoList == '' ? <p className='no-data'>暂无数据</p> : this.props.todoList.map((item, index) => {
                return <p className='content-list' key={index}>{item.name}</p>
              })  
            }
          </div>
        </div>
      </div>
    );
  }
}

App.propTypes = {
  children: PropTypes.node.isRequired,
  getTodoList: PropTypes.func.isRequired,
  todoList: PropTypes.array.isRequired,
};

App.contextTypes = {
};

const mapStateToProps = (state) => ({
  todoList: state.todo.todoList,
});

function mapDispatchToProps(dispatch) {
  return {
    getTodoList: bindActionCreators(getTodoList, dispatch),
  };
}

export default connect(mapStateToProps, mapDispatchToProps)(App);

7.2 从上述代码中可以知道,点击按钮后会发送一个请求,这个请求会获取todo列表。所以我们需要对this.props.getTodoList进行开发(其余的像App.propsTypes、mapStateToProps和function mapDispatchToProps就不多介绍了)。由于我们在该页面引进了getTodoList这个方法:import { getTodoList } from '../../actions/todo' ; 可根据这个路径进行新增我们需要的文件。

首先进入src目录下的actions,新建一个todo.js的文件,新增代码如下:

import types from '../store/types';

import {
  get_todo_list,
} from '../api/todo';

export function getTodoList(params) {
  return (dispatch) => {
	dispatch({
	  type: types.GET_TODO_LIST,
	  payload: {
		promise: get_todo_list(params)
	  }
	});
  };
}

这个文件引入两个变量,一个是types,一个是get_todo_list,所以我们先进入到store文件夹,找到types.js这个文件,新增代码如下(只需要加原本没有的):

import keyMirror from 'key-mirror';

/**
 * key-mirror:
 * keyMirror() 创建的对象,值会与名字一致,编码起来更方便
 */

export default keyMirror({
  GET_TABS_DATA:null, 
  GET_TODO_LIST:null
});

然后进入根目录src下的api文件夹下,该文件夹下已有一个api.js和一个index.js文件,我们只需要再新增一个todo.js文件即可,代码如下:

import { AjaxServer } from './index.js';

/**
 * 获取pc端todo列表
 */
export async function get_todo_list(params){
  return AjaxServer('get', '/pc/todolist', {}, params);
}

另外,AjaxServer走的前缀路径记得要配置一下,打开src/util下的index.js文件,并下拉至228行左右,找到getServerBase()方法,在case里面的dev更改为localhost:3000或者http://your local ip address:3000(以我为例:http://10.108.9.56:3000),代码如下所示:

export function getServerBase(){
  switch (process.env.NODE_ENV) {
    case 'dev': return 'http://10.108.9.56:3000';//开发接口请求地址
    // case 'dev': return '';
    // case 'test': return '';
    // case 'staging': return '';
    // case 'prod': return '';//生产环境请求地址
    // default: return 'http://localhost/api/';
    default: return 'http://10.108.9.56:3000';
  }
}

最后还有一个容易忽略的地方,就是每个请求后返回的数据都需要经过一个中间件处理(具体可查看src/store/middlewares下的两个文件),而本项目返回的数据要在哪里处理呢?要在src/reducers路径下新增一个todo.js的文件,代码如下:

import {
  createReducer,
  clone
} from '../util';

import types from '../store/types';

const InitState = {
  todoList: [],      
};

export default createReducer(InitState, {
  [`${types.GET_TODO_LIST}_SUCCESS`]: (state, data, params) => {
    const stateClone = clone(state);
    if(data.status === 1){
      stateClone.todoList = data.data;
    }
    return stateClone;
  }
})

7.3 前端的准备工作完毕之后,接下来就是准备后端工作了。后台的服务依旧是todolist-express,我们可以预测的到,肯定会出现跨域的情况,因此我们需要先处理跨域的情况。打开todolist-express的根目录下的app.js文件,新增代码如下(可与app端处理跨域的代码平行放置):

// PC端设置跨域访问
app.all('/pc/*', function(req, res, next) {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Headers', '*');
  res.header('Access-Control-Allow-Methods', '*');
  res.header('Content-Type', 'application/json;charset=utf-8');
  res.header('Access-Control-Allow-Credentials', 'true'); //一定要设置这一句
  next();
});

然后我们要接收前端发过来的请求路径并进行处理,接收代码(这部分代码要放在处理跨域代码的后面):

app.use('/pc/todolist', todoRouterPC);

很明显有一个变量todoRouterPC我们未定义,所以我们需要定义并赋值,这个todoRouterPC就是要指向要处理请求并返回数据的文件,代码如下(该部分代码应放在文件的上面):

var todoRouterPC = require('./routes/pc/todo');

接下来,就要在routes文件夹下新增一个pc文件夹,进入后再新增一个todo.js的文件,里面的代码如下:

var express = require('express');
var router = express.Router();

/* GET todo listing. */
router.get('/', function(req, res, next) {
  res.send({
    code: 1,
    msg: '成功',
    data: [
      {
		id: 0,
		name: 'Learning node.js on Monday'
	  },
	  {
		id: 1,
		name: 'Learning react.js on Tuesday'
	  },
	  {
		id: 2,
		name: 'Learning vue.js on Wednesday'
	  },
	  {
		id: 3,
		name: 'Learning angular.js on Thursday'
	  },
	  {
		id: 4,
		name: 'Learning express on Friday'
	  },
	  {
		id: 5,
		name: 'Learning koa on Saturday'
	  },
	  {
		id: 6,
		name: 'Learning react-native on Sunday'
	  }
    ]
  });  
});

module.exports = router;

至此,前后端连接通道差不多已结束,重启后端服务,重新打包前端代码,然后再进行测试。

后台服务更改后的路径图如下:

7.4 测试数据。最后一步就是测试数据是否能顺畅在前后端流通。后端服务启动后,前端代码打包并运行,在浏览器中输入:http://localhost:4002,即可查看页面效果,效果图如下(样式得自己写):

点击页面中的按钮,打开浏览器后台查看网络请求和返回的数据以及页面的渲染结果,如下图所示:

 结果完美~

GIF演示图如下:

8. 后台数据自动化(以PC端为例)

8.1 第6,7两节我们知道了数据怎么流通的,但是可惜的是后台的数据是我们人为写的,也就是说数据并不是来自服务器,所以需要对后台逻辑业务进一步修改,也就是我们常用的增、删、改和查几个操作来满足前端的业务。

---------------------------查--------------------------

8.2

在7.3小节的时候,明白router.get(url, function)方法接收两个参数,一个是请求的路径,一个是执行该请求并返回数据的函数,而在前端的api文件里发出的请求为:AjaxServer('get', '/pc/todolist', params);后端路由要接收该请求,在后端app.js文件中为了更好的将不同类型的接口区分开,可做如下处理:

// 接口路由路径
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var todoRouterApp = require('./routes/app/todo');
var todoRouterPC = require('./routes/pc/todo');

// 设置跨域访问
app.all('*', function(req, res, next) {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Headers', 'X-Requested-With, Content-Type'); 
  res.header('Access-Control-Allow-Methods', '*');
  res.header('Content-Type', 'application/json;charset=utf-8');
  res.header('Access-Control-Allow-Credentials', 'true'); //一定要设置这一句
  next();
});

// 调用接口
app.use('/', indexRouter);       // 用于全局接口

app.use('/users', usersRouter);  // 用于关于用户的接口

app.use('/app', todoRouterApp);  // 用于app端的接口

app.use('/pc', todoRouterPC);    // 用于web端的接口

很明显在接收AjaxServer('get', '/pc/todolist', params)请求时,应走上述代码片段的倒数第一行,然后进入todoRouterPC的文件内,写下如下代码:

var express = require('express');
var router = express.Router();

const Todo = require('../../controllers/pc/todo');

router.get('/todolist', Todo.getTodoList); // 这里的路径是承接app.js里的'/pc'

module.exports = router;

注意:上述代码中的倒数第二行里的路径是承接app.js里的app.use()里的路径,这样就形成了完整的前端请求路径:'/pc/todolist'。意味着后端完整接收了前端发过来的请求,接下来就是处理这个请求的逻辑。

8.3 controllers文件夹是专用于放置处理请求方法的文件,是后端最重要的一环,数据处理的是否符合逻辑或符合业务期望都在这一步,因为有可能在执行sql语句后获得数据,还需进一步处理。示例代码如下:

const Todo = require('../../models/pc/todo');

// 获取todo列表
async function getTodoList(req, res) {
  try {
    const data = await Todo.getTodoList();
    res.json({
      status: 1,
      msg: '获取成功',
      data: [...data]
    });
  } catch(err) {
    res.json({
      status: 0,
      msg: err.message
    });
  }
}

module.exports = {
  getTodoList
};

这里主要执行了一个Todo.getTodoList()的方法,执行该方法后就可以获得所需要的数据,而该方法是源自models,所以还需查看models里的js是怎么写的。

8.4 models文件夹专用于放置处理数据库的逻辑函数,能不能获取到数据就在这一环,示例代码如下:

const Model = require('../index');
const sequelize = Model.sequelize;
const TodoSQL = require('../../sql/todo');

/**
 * 请求所有留言列表(pc)
 */
const getTodoList = () => sequelize.query(
  TodoSQL.todoList,
  { type: sequelize.QueryTypes.SELECT}
);

module.exports = {
  getTodoList
};

该代码片段只是实现的方式之一,它主要是根据我们自己写的sql语句执行的,而自己写的这些sql文件存放于sql文件夹中。

8.5 sql文件夹专用于放置sql语句,示例代码如下:

const todoSql = {
  todoList: `
    SELECT
      todoId,
      title,
      userId,
      userName,
      update_time
    FROM
      todolist
    ORDER BY
      todolist.update_time DESC
  `
}

module.exports = todoSql;

这应该很好理解吧~若是不理解,我截图把sql里的数据表呈现出来,如下图所示:

部分数据如下图所示:

实在不好理解,那么看到数据之后,把上述的sql语句复制粘贴至sqlyog软件里执行一次就知道是否成功,如下图所示:

上面是执行语句,下面是执行的结果。

8.6 执行sql语句获取数据的第二个方法,代码示例如下:

const Model = require('../index');
const sequelize = Model.sequelize;
const Todo = Model.Todo;

/**
 * 请求所有留言列表(pc)
 */
const getTodoList = () => {
  const data = Todo.findAll();
  return data;
}

疑问就在于这个Todo = Model.Todo是怎么来的?这个方法需要额外再创建一个文件夹,用于存放数据表模板,如下图所示:

schema下的todo.js代码示例如下:

module.exports = function(sequelize, DataTypes) {
  return sequelize.define('Todo', {
    userId: {
      type: DataTypes.CHAR(4),
      allowNull: false,
      primaryKey: true,
      autoIncrement: true
    },
    userName: {
      type: DataTypes.STRING(50),
      allowNull: true
    },
    title: {
      type: DataTypes.STRING(256),
      allowNull: true
    },
    todoId: {
      type: DataTypes.CHAR(4),
      allowNull: true
    },
    create_time: {
      type: DataTypes.DATE,
      allowNull: true
    },
    update_time: {
      type: DataTypes.DATE,
      allowNull: true
    }
  }, {
    tableName: 'todolist'
  });
};

看到没,一目了然,就相当于把你创建的todolist这个数据表再用文件重新定义一次,它是用sequelize中间件进行了处理,使开发者更简便的开发,所以sequelize是一个好东西,值得学,网址:sequelize操作数据库的用法

8.7 经过上述一系列手把手教程之后,可以很清晰的理解后端运行的逻辑顺序是这样的:

1)routes--controllers--models--sql

2)routes--controllers--models--schema

如果初始没有数据,可自己增添几条数据,或者你先学会下面的“”,再学习“”吧。

-----------------------------增-----------------------------

8.8 前端发出请求:AjaxServer('post', '/pc/addTodo', {}, params),很明显这是一个post请求,会发现与get请求传参的形式不一样,主要区别就是因为GET和POST请求方式不一样,那么接下来就是后台解析这个请求的步骤了,但总体是与GET方式是一致的,可查看8.2节,只不过在router.get('/todolist', Todo.getTodoList);的下面新增了代码:router.post('/addTodo', Todo.addTodo);其他都是一样的。

8.9 接下来便是controllers里的逻辑处理了,同样在controllers/pc/todo.js文件内新增如下代码:

// 新增todo
async function addTodo(req, res) {
  try {
    const { todoInfo, userId, userName } = req.body;//POST请求的参数是放在body内的
    await Todo.addTodo(todoInfo, userId, userName);
    res.json({
      status: 1,
      msg: '新增成功',
      data: []
    });
  } catch(err) {
    res.json({
      status: 0,
      msg: err.message
    });
  }
}

记得要将addTodo这个方法导出噢~

随后跳转到models/pc/todo.js文件,因为这里的处理逻辑都比较简单,默认选用8.6节方法,直接在这个文件内新增代码:

/**
 * 根据用户 Id 新增用户留言(pc)
 * @param {string} userId 用户 Id
 * @param {string} title 留言内容
 */
const addTodo = (todoInfo, userId, userName) =>
  Todo.create({
    title: todoInfo,
    userId,
    userName,
    create_time: new Date(),
    update_time: new Date()
  });

同样的,记得要将addTodo这个方法导出噢~

最后重启后端服务,然后在前端新增todo的请求按照请求todolist的方式照葫芦画瓢即可(记得会有些差别,get和post请求的差别,传参的形式等),然后运行一下是否可行。

-------------------------------------------------------------

8.10 基础的教程就不再赘述,复制粘贴谁都会,只要注意一些差别并修正就好

controllers/pc/todo.js,在该文件内新增代码:

// 修改todo
async function modifyTodo(req, res) {
  const { userId, todoInfo, todoId } = req.body;
  try {
    const data = await Todo.modifyTodo(userId, todoInfo, todoId);
    res.json({
      status: 1,
      msg: '修改成功',
      data: data
    });
  } catch(err) {
    res.json({
      status: 0,
      msg: err.message
    });
  }
}

models/pc/todo.js,在该文件内新增代码:

/**
 * 根据todoId 修改留言(pc)
 * @param {string} todoId
 */
const modifyTodo = (userId, title, todoId) => {
  const data = Todo.update({title, update_time: new Date()},{
    where: {
      todoId
    }
  });
  return data;
}

---------------------------------------------------------------

8.11 不多说,直接上代码

controllers/pc/todo.js,在该文件内新增代码:

// 删除todo
async function delTodo(req, res) {
  try {
    const { userId, todoId } = req.body;
    await Todo.delTodo(userId, todoId);
    res.json({
      status: 1,
      msg: '删除成功',
      data: []
    });
  } catch(err) {
    res.json({
      status: 0,
      msg: err.message
    });
  }
}

models/pc/todo.js,在该文件内新增代码:

/**
 * 根据todoId 删除用户留言(pc)
 * @param {string} todoId 留言 Id
 */
const delTodo = (userId, todoId) =>
  Todo.destroy({
    where: {
      todoId
    }
  });

8.12 这只是对于todo数据的一系列操作,但实际上,为了区别todo,或这个todo的归属,还需要建立一个专用于用户user的表、关于user的登录登出处理、user信息加密处理、数据处理、请求api等等,这是一个学习的难点,也是必须要会的一个知识点。

8.13 上述都是一些比较基础的sql操作,后续需要自己查看sequelize的官方文档进行学习。经过增删改查之后,相信数据库里的数据也会有一些变动,页面的数据流通也会呈现不同的形式,如果这一切都调通了,那么前后端的基础知识体系差不多已建立完全了。但是一般网站的发布不仅仅是前后端数据畅通,还需要将代码和数据库部署在远程服务器上,这样你的网站才能被全国人民看见或者搜索到,但这又需要大量的工作。如果只是个人学习使用,那么只需要买一个便宜的阿里云服务器就好,这是下面的学习内容了;但如果要所有人都可访问,那么不仅需要域名解析,还需要上传个人信息至一个管理网站的机构,这个过程不仅耗费金钱,还耗费时间,看个人需要吧。

9. 后台与服务器对接工作

9.1 生产环境的项目(即线上项目)运行的数据都是来自服务器,所以我们还需要搭建后台与服务器之间的桥梁,便于后台从服务器拿数据。

9.2 在第八节的时候学习了对数据库的增删改查等基本操作,高级的还有联合查询等骚操作,这需要读者自己线下学习。而要想将项目与线上的数据库对应上,其实也比较简单,一般来说开发人员是没有权限去操作线上的sql库,这都是服务器运维工作人员做的事情,但我们还是有必要了解一下,要做的工作如下:

1)将你本地创建的数据库,以sql语句的形式导出,操作如下:

即:右键你的表名,选择“备份/导出”选项,再点击“备份表作为SQL转储”并保存在相应的路径内即可

2)打开你保存的sql文件,将头部和尾部的代码删除,需要的代码部分示例:

CREATE TABLE `project` (
  `id` int(16) NOT NULL AUTO_INCREMENT,
  `name` varchar(32) COLLATE utf8_bin DEFAULT NULL,
  `location` varchar(32) COLLATE utf8_bin DEFAULT NULL,
  `date` date DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=24 DEFAULT CHARSET=utf8 COLLATE=utf8_bin;

3)线上数据库里所需要的表,你都可以进行1)2)两个步骤,然后将上述的sql语句在线上的数据库里的命令栏里一一执行,执行完毕之后,再查看线上的数据库内的表是否按照你的要求建立了。

请注意:执行数据库命令这一项的工作是服务器运维人员要做的事情,也就是创建数据库里的表工作,开发人员可能要做的就是告知服务器运维人员他们要创建哪些表,表内有哪些字段,字段的类型和长度等信息。

9.3 这一环节不需要你来做,就是获取到线上数据库的相关信息,例如主机、端口、用户名、密码等,就相当于你连接你本地数据库所需要的信息一样,以下是线上数据库的配置信息示例(隐去了私密信息):

config = {
    db_host: 'rm-xxxxxxxxxxxxxxxxx.mysql.rds.xxx.com',
    username: 'xxxxxxxxxxx',
    password: 'xxxxxxxxxxxxxxxxxx',
    db_port: '3306',
    db_name: 'xxxxxxxxxx',
    dialect: 'mysql', // 数据库类型
    pool: {
      max: 5,
      min: 0,
      idle: 10000
    }, // 连接池配置
    salt: 'handsome',
    frontSalt: 'beauty'
};

需要注意的是:线上数据库的配置信息开发人员是不知道的,而是服务器运维人员给开发人员的。最后将数据库配置信息然后用node语句导出:module.exports = config;

PS:可能有人对这个salt和frontSalt不理解,没关系,在下文token会讲解到。

9.4 重新打包你的项目,然后运行,但请注意:如果是你本地发送的相关sql请求,那么是访问不到线上数据库,会报500的服务器错误,应该要用你线上的网址进行访问。因为起初服务器运维人员在为你开通一个线上服务的时候,他/她已经把你项目的域名、线上数据库等相关信息给绑定了。

三、加密工作

10. 用户token校验

10.1 增加token

增加token的代码示例:

const crypto = require('crypto'); //加载加密文件
const jwt = require('jwt-simple'); //创建token
const config = require('../config/config.js');
const { salt, frontSalt } = require('.././config/config.js');

/**
 * 初始密码 123
 */
exports.resetPsw = () => {
  const pswf = md5('123', frontSalt);
  return md5(pswf, salt);
};
/**
 * 生成token
 */
exports.getToken = (uuid) => jwt.encode({
  uuid: uuid,
  iat: new Date().getTime(),
  exp: new Date().getTime() + 86400000
}, config.salt)  //token签名 有效期为1小时
;
/**
 * md5加密
 */
const md5 = (str, salt) => crypto.createHash('md5').update(str, salt).digest('hex').toUpperCase();//加密

exports.md5 = md5;

 上述代码引入的config就是数据库的配置信息,这个salt就是专服务于你的项目,由于md5加密算法不会改变,导致加密出来的密文不是随机的,例如,它加密“123”之后都会是“AANNKFIOSUF”这种,不会改变,但加上你的自定义salt之后,就与别人用的md5算法加密出来的“123”是不一样的。

10.2 校验token

校验token是在后端请求路由上添加的,也就是多了一个验证过程,routes文件内的代码示例:

const router = require('koa-router')()
const ProjectController = require('../controllers/project');
const Common = require('../controllers/common.js');

router.prefix('/project')

router.get('/getQuestion', Common.valiatorToken, ProjectController.getQuestion);

module.exports = router;

上述代码与原先无token校验,多了一个方法:Common.valiatorToken,这个方法就是校验token,而这个方法是从common.js引入的。

10.3 common.js

const jwt = require('jwt-simple');
const config = require('../config/config.js');
const admin = require('../models/admin.js');

/**
 * 校验token
 */
exports.valiatorToken = async (ctx, next) => {
  const token = ctx.request.body.token || ctx.query.token;//这个是koa.js的方法
  // token是用用户的uuid(这个是传递进来的)和过期时间加密而来的
  if (token) {
    try {
      const payload = jwt.decode(token, config.secret, 'HS256');
      if (payload.exp < new Date().getTime()) {
        ctx.body = {
          code: '-2',
          message: 'token已过期',
          data: {}
        }
      };
      if (payload.uuid) {
        const userData = await admin.adminLogin({ id: payload.uuid });
        const val = Object.keys(userData).length;
        if (val <= 0) {
          ctx.body = {
            code: '-1',
            message: 'token错误',
            data: {}
          }
        } else {
          //校验成功
          await next();
        }
      } else {
        ctx.body = {
          code: '-1',
          message: 'token错误',
          data: {}
        }
      }
    } catch(err) {
      ctx.body = {
        code: '-1',
        message: 'token错误'
      }
    }
  } else {
    ctx.body = {
      code: '0',
      message: 'token不存在'
    }
  }
};

exports.valiatorToken

上面有一个 next()方法,这个方法指的就是router.get('/getQuestion', Common.valiatorToken, ProjectController.getQuestion)内的ProjectController.getQuestion这个方法,也就是说,token校验成功之后,才会走接下来的这一步。

©️2020 CSDN 皮肤主题: 精致技术 设计师:CSDN官方博客 返回首页