最近在作一箇舊書交易網站,本屬於B/S體系結構的課程做業,但因爲採用了新的框架因此躍躍欲試想都記錄下來。css
實現一箇舊書交易網站,基本功能以下:html
- 實現用戶註冊、登陸功能,用戶註冊時須要填寫必要的信息並驗證,如用戶名、密碼要求在6字節以上,email的格式驗證,並保證用戶名和email在系統中惟一。
- 用戶登陸後能夠發佈要交易的書籍,須要編輯相關信息,包括書名、原價、出售價、類別和內容介紹等信息、外觀照片等,能夠經過ISBN和書名連接到外部系統(如Amazon/京東/噹噹等網站)的詳細介紹頁面。
- 根據用戶發佈的書籍聚合生成首頁,能夠分類檢索。
- 用戶能夠設置交易模式爲寄送仍是線下交易,生成訂單時錄入不一樣內容。
- 集成一個消息系統,買家和賣家之間能夠通訊。
- 提供求購模塊,用戶能夠發佈本身想要的書籍。
- 界面樣式須要適配PC和手機的瀏覽器。
- 實現一個Android或iphone客戶端軟件,功能同網站,額外支持定位功能,發佈時記錄位置,能夠根據用戶的位置匹配最近的待售書籍。消息和訂單支持推送。
數據庫使用MySQL進行開發,由於環境以前都已經配好了( ̄▽ ̄)"前端
通過Express和Koa比對,最終選擇Koa做爲基於Node.js的Web開發框架。Koa是一個新的web框架,由Express幕後原班人馬打造,語法上也使用了ES6新的語法(例如丟棄了回調函數而使用async解決異步調用問題),看起來十分優雅o( ̄▽ ̄)onode
採用React+Semantic UI,因爲以前對React有足夠多的實踐,所以本次重點仍是放在後端開發及先後端鏈接上……mysql
Vue+Koa全棧開發react
Koa框架教程 - 阮一峯webpack
命令行輸入ios
npm init -y npm i koa koa-json npm i -D nodemon
package.json
內容,將scripts
中的內容更改成"start":"nodemon app.js"
根目錄下新建app.js
git
const Koa = require("koa"); const json = require("koa-json"); const logger = require("koa-logger"); const KoaRouter = require("koa-router"); const parser = require("koa-bodyparser"); const app = new Koa(); const router = new KoaRouter(); // Json Prettier Middleware app.use(json()); app.use(parser()); app.use(logger()); // Simple Middleware Example // app.use(async ctx => (ctx.body = { msg: "Hello world" })); app.listen(4113, () => console.log("----------Server Started----------")); module.exports = app;
node app.js
,瀏覽器打開localhost:3000
查看返回數據安裝包github
npm install sequelize-auto -g npm install tedious -g npm install mysql -g
進入src
目錄,輸入sequelize-auto -o "./schema" -d bookiezilla -h 127.0.0.1 -u root -p 3306 -x XXXXX -e mysql
,(其中 -o 參數後面的是輸出的文件夾目錄, -d 參數後面的是數據庫名, -h 參數後面是數據庫地址, -u 參數後面是數據庫用戶名, -p 參數後面是端口號, -x 參數後面是數據庫密碼 -e 參數後面指定數據庫爲mysql)
此時schema
文件夾下會自動生成三個表的文件,例如:
/* jshint indent: 2 */ module.exports = function(sequelize, DataTypes) { return sequelize.define( "book", { BookID: { type: DataTypes.INTEGER(11), allowNull: false, primaryKey: true }, BookName: { type: DataTypes.STRING(45), allowNull: true }, BookCostPrice: { type: "DOUBLE", allowNull: true }, BookSalePrice: { type: "DOUBLE", allowNull: true }, BookCategory: { type: DataTypes.STRING(45), allowNull: true }, BookPhoto: { type: DataTypes.STRING(45), allowNull: true }, BookContent: { type: DataTypes.STRING(45), allowNull: true }, BookISBN: { type: DataTypes.STRING(45), allowNull: true } }, { tableName: "book" } ); };
在server\src\config
下新建文件database.js
,用於初始化Sequelize
和數據庫的鏈接。
const Sequelize = require("sequelize"); // 使用url鏈接的形式進行鏈接,注意將root: 後面的XXXX改爲本身數據庫的密碼 const BookieZilla = new Sequelize( "mysql://root:XXXXX@localhost/bookiezilla", { define: { timestamps: false// 取消Sequelzie自動給數據表加入時間戳(createdAt以及updatedAt),不然進行增刪改查操做時可能會報錯 } } ); module.exports = { BookieZilla // 將BookieZilla暴露出接口方便Model調用 };
在server\src\models
下新建文件userModel.js
,數據庫和表結構文件鏈接起來。
const db = require("../config/database.js"); const userModel = "../schema/user.js";// 引入user的表結構 const BookieZilla = db.BookieZilla;// 引入數據庫 const User = BookieZilla.import(userModel);// 用sequelize的import方法引入表結構,實例化了User。 const getUserById = async function(id) { const userInfo = await User.findOne({ where: { UserID: id } }); return userInfo; }; module.exports = { getUserById, getUserByEmail };
在server\src\controllers
下新建文件userController.js
,來執行這個方法,並返回結果。
Koa 提供一個 Context 對象,表示一次對話的上下文(包括 HTTP 請求和 HTTP 回覆)。經過加工這個對象,就能夠控制返回給用戶的內容。
const user = require("../models/userModel.js"); const getUserInfo = async function(ctx) { const id = ctx.params.id;// 獲取url裏傳過來的參數裏的id const result = await user.getUserById(id); ctx.body = result;// 將請求的結果放到response的body裏返回 }; module.exports = { getUserInfo, vertifyUserLogin };
在server\src\routes
下新建文件auth.js
,用於規劃auth
下的路由規則。
const auth = require("../controllers/userController.js"); const router = require("koa-router")(); router.get("/user/:id", auth.getUserInfo); module.exports = router;
回到根目錄下的app.js
,將這個路由規則「掛載」到Koa上去。
const Koa = require("koa"); const json = require("koa-json"); const logger = require("koa-logger"); const KoaRouter = require("koa-router"); const parser = require("koa-bodyparser"); const auth = require("./src/routes/auth.js");// 引入auth const app = new Koa(); const router = new KoaRouter(); // Json Prettier Middleware app.use(json()); app.use(parser()); app.use(logger()); // Simple Middleware Example // app.use(async ctx => (ctx.body = { msg: "Hello world" })); // Router Middleware router.use("/auth", auth.routes());// 掛載到koa-router上,同時會讓全部的auth的請求路徑前面加上'/auth'的請求路徑。 app.use(router.routes()).use(router.allowedMethods());// 將路由規則掛載到Koa上。 app.listen(4113, () => console.log("----------Server Started----------")); module.exports = app;
SUCCESS!!!
因爲本項目採用的是先後端分離的架構,所以須要經過json來傳遞數據,以實現登陸功能爲例來闡述實現的具體步驟。
server\src\models\userModel.js
增長方法,用於經過郵箱查找用戶。
// ... const getUserByEmail = async function(email) { const userInfo = await User.findOne({ where: { UserEmail: email } }); return userInfo; }; module.exports = { getUserById, getUserByEmail };
server\src\controller\userController.js
增長方法,用於驗證登陸信息並將結果以json
形式返回給前端。
注意此處實際上應用了JSON-WEB-TOKEN實現無狀態請求,關於jwt
的原理和實現方法請參考這篇文章和這篇文章。
簡單來講,運用了JSON-WEB-TOKEN的登陸系統應該是這樣的:
使用前須要安裝相應庫:
npm i koa-jwt jsonwebtoken util -s
此外,爲保證安全性,後端數據庫的密碼不能採用明文保存,此處使用bcrypt
的加密方式。
npm i bcryptjs -s
const user = require("../models/userModel.js"); const jwt = require("jsonwebtoken"); const bcrypt = require("bcryptjs"); const getUserInfo = async function(ctx) { const id = ctx.params.id; const result = await user.getUserById(id); ctx.body = result; }; const vertifyUserLogin = async function(ctx) { const data = ctx.request.body; // post過來的數據存在request.body裏 const userInfo = await user.getUserByEmail(data.email); if (userInfo != null) { // 若是查無此用戶會返回null if (!bcrypt.compareSync(data.psw, userInfo.UserPsw) { ctx.body = { status: false, msg: "Wrong password" }; } else { // 若是密碼正確 const userToken = { id: userInfo.UserID, email: userInfo.UserEmail }; const secret = "react-koa-bookiezilla"; // 指定密鑰,這是以後用來判斷token合法性的標誌 const token = jwt.sign(userToken, secret); // 簽發token ctx.body = { status: true, token: token // 返回token }; } } else { ctx.body = { status: false, msg: "User doesn't exist" }; } }; module.exports = { getUserInfo, vertifyUserLogin };
更新server\src\routes\auth.js
中的路由規則。
const auth = require("../controllers/userController.js"); const router = require("koa-router")(); router.get("/user/:id", auth.getUserInfo); router.post("/login", auth.vertifyUserLogin); module.exports = router;
前端主要使用了react-router
進行路由跳轉,使用semantic-ui
做爲UI組件庫,使用axios
發送請求,Login.js
代碼以下:
import React, { Component } from "react"; import { Button, Form, Grid, Header, Image, Message, Segment, Loader } from "semantic-ui-react"; import { NavLink, withRouter } from "react-router-dom"; import axios from "axios"; import Logo from "../images/logo.png"; class Login extends Component { state = { email: "", psw: "", alert: false, load: false }; vertifyFormat = () => { var pattern = /^([A-Za-z0-9_\-\.])+\@([A-Za-z0-9_\-\.])+\.([A-Za-z]{2,4})$/; return pattern.test(this.state.email) && this.state.psw.length >= 6; }; sendLoginRequest = () => { if (this.vertifyFormat()) { this.setState({ alert: false, load: true }); axios .post("/auth/login", { email: this.state.email, psw: this.state.psw }) .then(res => { console.log(res); }) .catch(err => { console.log(err); }); } else { this.setState({ alert: true }); } }; render() { var alert = this.state.alert === false ? ( <div /> ) : ( <Message error header="Could you check something!" list={[ "Email format must conform to the specification.", "Password must be at least six characters." ]} /> ); var load = this.state.load === false ? <div /> : <Loader />; return ( <Grid textAlign="center" style={{ height: "100vh", background: "#f6f6e9" }} verticalAlign="middle" > <Grid.Column style={{ maxWidth: 450 }}> <Header as="h2" color="teal" textAlign="center"> <Image src={Logo} /> Log-in to your B::kzilla </Header> <Form size="large" error active> <Segment> <Form.Input fluid icon="user" iconPosition="left" placeholder="E-mail address" onChange={event => { this.setState({ email: event.target.value }); }} /> <Form.Input fluid icon="lock" iconPosition="left" placeholder="Password" type="password" onChange={event => { this.setState({ psw: event.target.value }); }} /> {alert} {load} <Button color="teal" fluid size="large" onClick={this.sendLoginRequest} > Login </Button> </Segment> </Form> <Message> New to us? <NavLink to="/signup"> <a href="#"> Sign Up</a> </NavLink> </Message> </Grid.Column> </Grid> ); } } export default withRouter(Login);
安裝http-proxy-middleware
中間件。
npm install http-proxy-middleware -s
create-react-app
初始化的項目須要eject
,使基本配置暴露出來。
npm run eject
client\src
下新建文件setupProxy.js
,配置代理轉發信息。
const proxy = require("http-proxy-middleware"); module.exports = function(app) { app.use( proxy("/api", { target: "http://localhost:4113", changeOrigin: true }) ); app.use( proxy("/auth", { target: "http://localhost:4113", changeOrigin: true }) ); };
client\scripts\start.js
中進行配置,在const devServer = new WebpackDevServer(compiler, serverConfig);
後添加語句require("../src/setupProxy")(devServer);
發送請求格式以下:
axios .post("/auth/login", { email: this.state.email, psw: this.state.psw }) .then(res => { console.log(res); }) .catch(err => { console.log(err); });
*UserID | UserName | UserPsw | *UserEmail |
---|---|---|---|
INT | VARCHAR(45) | VARCHAR(45) | VARCHAR(45) |
CREATE TABLE `bookiezilla`.`user` ( `UserID` INT NOT NULL, `UserName` VARCHAR(45) NULL, `UserPsw` VARCHAR(45) NULL, `UserEmail` VARCHAR(45) NOT NULL, PRIMARY KEY (`UserID`, `UserEmail`));
*BookID | BookName | BookCostPrice | BookSalePrice | BookCategory | BookPhoto | BookContent | BookISBN | BookRefs |
---|---|---|---|---|---|---|---|---|
INT | VARCHAR(45) | DOUBLE | DOUBLE | VARCHAR(45) | VARCHAR(45) | VARCHAR(45) | VARCHAR(45) | VARCHAR(45) |
CREATE TABLE `bookiezilla`.`book` ( `BookID` INT NOT NULL, `BookName` VARCHAR(45) NULL, `BookCostPrice` DOUBLE NULL, `BookSalePrice` DOUBLE NULL, `BookCategory` VARCHAR(45) NULL, `BookPhoto` VARCHAR(45) NULL, `BookContent` VARCHAR(45) NULL, `BookISBN` VARCHAR(45) NULL, PRIMARY KEY (`BookID`));
*OrderID | *UserID | *BookID | TradeMethod | TradeStatus | TradeParty | TraderID |
---|---|---|---|---|---|---|
INT | INT | INT | VARCHAR(45) | VARCHAR(45) | VARCHAR(45) | INT |
CREATE TABLE `bookiezilla`.`order` ( `OrderID` INT NOT NULL, `UserID` INT NOT NULL, `BookID` INT NOT NULL, `TradeMethod` VARCHAR(45) NULL, `TradeStatus` VARCHAR(45) NULL, `TraderID` INT NULL, PRIMARY KEY (`OrderID`));
. │ .gitignore │ package-lock.json │ package.json │ README.md │ yarn.lock │ ├─config // 基本配置文件 │ │ env.js │ │ modules.js │ │ paths.js │ │ pnpTs.js │ │ webpack.config.js │ │ webpackDevServer.config.js │ │ │ └─jest │ cssTransform.js │ fileTransform.js │ ├─public │ favicon.ico │ index.html │ manifest.json │ ├─scripts // eject後生成的文件配置 │ build.js │ start.js │ test.js │ └─src // 主要頁面及組件部分 │ App.css │ App.js │ index.css │ index.js │ serviceWorker.js │ setupProxy.js // 設置代理轉發,解決跨域問題 │ ├─actions // react-redux須要定義的actions │ UpdateActions.js │ ├─components // 頁面的組件部分 │ BookList.jsx │ BookMarket.jsx │ FeedBack.jsx │ OrderInfo.jsx │ PublishForm.jsx │ SearchBar.jsx │ SideMenu.jsx │ StatisticData.jsx │ StepFlow.jsx │ ├─images // 項目中使用的圖片資源 │ logo.png │ matthew.png │ ├─pages // 頁面部分 │ Home.jsx │ Login.jsx │ Market.jsx │ Message.jsx │ Publish.jsx │ Signup.jsx │ └─reducers // react-redux須要定義的reducers rootReducer.js
項目中使用了react-router
來控制路由,基本原理以下:
在App.js
中引入路由對應的頁面或組件,並引入react-router-dom
中的BrowserRouter
、Route
、Switch
組件進行定義。
// App.jsx import React, { Component } from "react"; import { BrowserRouter, Route, Switch } from "react-router-dom"; import SideMenu from "./components/SideMenu"; import Login from "./pages/Login"; import Signup from "./pages/Signup"; import Home from "./pages/Home"; import Market from "./pages/Market"; import Publish from "./pages/Publish"; import Message from "./pages/Message"; import OrderInfo from "./components/OrderInfo"; class App extends Component { render() { return ( <BrowserRouter> <div className="App"> <Switch> <Route exact path="/" component={Login} /> <Route path="/signup" component={Signup} /> <div> <div> <SideMenu /> </div> <div style={{ margin: "10px 10px 10px 160px" }}> {/* Only match one */} <Route path="/home" component={Home} /> <Route path="/market" component={Market} /> <Route path="/publish" component={Publish} /> <Route path="/message" component={Message} /> <Route path="/books/:book_id" component={OrderInfo} /> </div> </div> </Switch> </div> </BrowserRouter> ); } } export default App;
當項目頁面中須要進行頁面跳轉時,可以使用react-router-dom
中的withRouter
將組件包裹起來,再使用NavLink
進行跳轉。
// Login.jsx import { NavLink, withRouter } from "react-router-dom"; class Login extends Component { ..... sendLoginRequest = () => { ...... this.props.history.push("/home"); render(){ ...... } }; export default withRouter(Login);
本項目中採用了react-redux
進行狀態管理,redux的主要做用是容許狀態在不一樣分支的組件中進行傳遞,從而避免了使用原始方法(如this.props
)致使的不一樣分支組件之間數據沒法傳遞、子組件沒法修改父組件狀態等問題。具體使用方法以下:
在src\reducers
下新建文件rootReducer.js
用於更新中心狀態樹中的信息。
// rootReducer.js const initState = { id: null, token: null }; const rootReducer = (state = initState, action) => { if (action.type === "UPDATE_ID") { return { ...state, id: action.id }; } if (action.type === "UPDATE_TOKEN") { return { ...state, token: action.token }; } return state; }; export default rootReducer;
在src\actions
中新建文件UpdateActions.js
用於定義行爲。
// UpdateActions.js export const updateId = id => { return { type: "UPDATE_ID", id: id }; }; export const updateToken = token => { return { type: "UPDATE_TOKEN", token: token }; };
在src\index.js
中使用react-redux
中的組件對項目入口文件進行包裹,並在全局範圍內創建狀態樹。
// index.js import React from "react"; import ReactDOM from "react-dom"; import "./index.css"; import App from "./App"; import * as serviceWorker from "./serviceWorker"; import "semantic-ui-css/semantic.min.css"; import { createStore } from "redux"; import { Provider } from "react-redux"; import rootReducer from "./reducers/rootReducer"; const store = createStore(rootReducer); ReactDOM.render( <Provider store={store}> <App />, </Provider>, document.getElementById("root") ); // If you want your app to work offline and load faster, you can change // unregister() to register() below. Note this comes with some pitfalls. // Learn more about service workers: https://bit.ly/CRA-PWA serviceWorker.unregister();
當須要更新狀態樹中的信息時,使用引入的action
做爲函數進行更新。
// Login.jsx import { connect } from "react-redux"; import { updateId, updateToken } from "../actions/UpdateActions"; class Login extends Component { ...... sendLoginRequest = () => { ...... this.props.updateId(res.data.id); this.props.updateToken(res.data.token); ...... }; } const mapStateToProps = state => { return {}; }; const mapDispatchToProps = dispatch => { return { updateToken: token => { dispatch(updateToken(token)); }, updateId: id => { dispatch(updateId(id)); } }; }; export default connect( mapStateToProps, mapDispatchToProps )(withRouter(Login));
當須要使用狀態樹中的信息時,先調用react-redux
中的connect
包裹組件,再使用this.props
直接調用便可。
// PublishForm.jsx import { connect } from "react-redux"; class PublishForm extends Component { ...... var UserID = this.props.id; var UserToken = this.props.token; ...... } const mapStateToProps = state => { return { id: state.id, token: state.token }; }; export default connect(mapStateToProps)(PublishForm);
. │ app.js │ package-lock.json │ package.json │ └─src ├─config // 數據庫配置 │ database.js │ ├─controllers // 控制器,獲取請求數據並調用models中的方法進行處理並返回結果 │ apiController.js │ msgController.js │ userController.js │ ├─models // 實例模型,主要使用Sequelize定義的方法對數據庫進行增刪改查 │ bookModel.js │ CommentModel.js │ orderModel.js │ userModel.js │ ├─routes // 路由,不一樣文件對應不一樣類型的api接口,分別與受權、功能實現、信息傳遞有關 │ api.js │ auth.js │ msg.js │ └─schema // 數據庫表結構,可以使用Sequelize自動生成 book.js comment.js order.js user.js
當Koa後端監聽的端口接收到請求時,會根據app.js
中的路由規則進行處理,咱們將不一樣類型的接口定義在不一樣文件中,再經過router.use()
進行調用,避免發生接口冗亂複雜的狀況。
// app.js const Koa = require("koa"); const json = require("koa-json"); const logger = require("koa-logger"); const KoaRouter = require("koa-router"); const parser = require("koa-bodyparser"); const auth = require("./src/routes/auth.js"); const api = require("./src/routes/api.js"); const msg = require("./src/routes/msg.js"); const app = new Koa(); const router = new KoaRouter(); // Json Prettier Middleware app.use(json()); app.use(parser()); app.use(logger()); // Simple Middleware Example // app.use(async ctx => (ctx.body = { msg: "Hello world" })); // Router Middleware router.use("/auth", auth.routes()); router.use("/msg", msg.routes()); router.use("/api", api.routes()); app.use(router.routes()).use(router.allowedMethods()); app.listen(4113, () => console.log("----------Server Started----------")); module.exports = app;
// auth.js const auth = require("../controllers/userController.js"); const router = require("koa-router")(); router.get("/user/:id", auth.getUserInfo); router.post("/login", auth.vertifyUserLogin); router.post("/signup", auth.signupNewUser); module.exports = router;
// api.js const api = require("../controllers/apiController.js"); const router = require("koa-router")(); router.get("/getbooks", api.getAllBooks); router.get("/getorder/:id", api.getOrderInfo); router.post("/searchbooks", api.searchBooks); router.post("/publish", api.publishNewBook); router.post("/confirmorder", api.updateOrderOfTrade); module.exports = router;
// msg.js const msg = require("../controllers/msgController.js"); const router = require("koa-router")(); router.get("/getcomments", msg.getAllComments); router.post("/newcomment", msg.publishNewComment); module.exports = router;
Bookizilla可以實現用戶註冊、用戶登陸功能,其中對用戶註冊時須要的數據作了格式處理(如驗證Email格式、保證兩次密碼輸入數據相符且不小於6字節等)。若是用戶在註冊過程當中出現錯誤,則會出現相應提示以指導用戶進行正確輸入。
Login.jsx
Signup.jsx
Bookiezilla的主頁呈現的是與該用戶有關的信息數據(如FAVES、VIEWS等,但因爲目先後端並未儲存相關數據因此暫用了mocks)及該用戶所發佈的全部書籍。
Home.jsx
Bookiezilla的書籍市場呈現了全部用戶發佈的全部書籍,用戶可使用上方的搜索框輸入關鍵詞(如書名、標籤 、ISBN等)。用戶還可點擊圖書下方按鈕以查看具體信息,進而決定是否達成交易,也可點擊連接在Amazon中查看書籍的詳細介紹。
Market.jsx
Bookiezilla容許用戶發佈書籍,並設置訂單的關鍵信息(如書籍基本信息、交易模式、尋求買家或賣家等)。須要注意的是,因爲書籍發佈和書籍求購很大一部份內容是重合的,因此此處將兩者合併而且給出TradeParty
選項來使用戶選擇是想要發佈書籍仍是求購書籍。
Publish.jsx
Bookiezilla設置了信息發佈面板,用於用戶之間的溝通交流、信息發佈等。用戶可直接發佈評論或回覆他人的評論,從而進行持續性的交流。
Message.jsx