序言
组长说要使自己对产品在技术层面有一个清晰且足够的了解,最好自己动手开发一个迷你产品,例如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平台的express或koa作为后台开发框架,本文选用的是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校验成功之后,才会走接下来的这一步。