序言
組長說要使自己對產品在技術層面有一個清晰且足夠的瞭解,最好自己動手開發一個迷你產品,例如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校驗成功之後,纔會走接下來的這一步。