從零到一的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校驗成功之後,纔會走接下來的這一步。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章