Project function
Recently, I was doing a second-hand book trading website, which belongs to the course assignment of B/S architecture, but because of the adoption of a new framework, I am eager to try and record it.
The basic functions of a used book trading website are as follows:
- To achieve user registration and login functions, users need to fill in the necessary information and verify when they register, such as user name and password requirements of more than 6 bytes, email format verification, and ensure that the user name and email are unique in the system.
- Users can publish books to be traded after login. They need to edit relevant information, including title, original price, sale price, category and content introduction, appearance photos, etc. They can link to the detailed introduction page of external system (such as Amazon/Jingdong/Dangdang) through ISBN and title.
- According to the aggregation of books published by users, the home page can be classified and retrieved.
- Users can set the transaction mode for sending or offline transactions, and input different contents when generating orders.
- Integrating a message system, buyers and sellers can communicate.
- Provide purchase module, users can publish the books they want.
- Interface styles need to be adapted to PC and mobile browsers.
- Implement an Android or iphone client software, with the same function as the website, additional support for positioning function, record location when publishing, according to the location of the user to match the latest book for sale. Messages and orders support push.
Technical Selection
data base
The database is developed using MySQL because the environment has been configured before ()“
back-end
After the comparison between Express and Koa, Koa is chosen as the Web development framework based on Node.js. Koa is a new web framework, built by Express's original team behind the scenes, and uses ES6's new grammar (for example, discarding callback functions and using async to solve asynchronous invocation problems), which looks very elegant o()
Front end
With React+Semantic UI, since there has been enough practice on React before, the focus of this time is still on back-end development and front-end and back-end connections...
development process
Reference Course
Vue+Koa Full Stack Development
Koa Framework Course - Ruan Yifeng
Koa Framework Construction
Initialization
-
Command line input
npm init -y npm i koa koa-json npm i -D nodemon
- Change the package.json content to "start" in scripts: "nodemon app. js"
-
New app.js under the root directory
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;
- On the command line, enter node app.js, and the browser opens localhost:3000 to view the returned data.
sequelize Connects to Database
-
Installation package
npm install sequelize-auto -g npm install tedious -g npm install mysql -g
-
Enter the src directory and enter sequelize-auto-o". / schema"-d bookiezilla-h 127.0.1-u root-p 3306-x XXXXXXX-e mysql. (Following the-o parameter is the output folder directory, -d parameter is the database name, -h parameter is the database address, -u parameter is the database user name, -p parameter is the output folder directory.) After that is the port number, followed by the - x parameter is the database password - e parameter, followed by the database specified as mysql.
At this point, files for three tables are automatically generated under the schema folder, such as:
/* 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" } ); };
-
Create a new file database.js under server src config to initialize the connection between Sequelize and the database.
const Sequelize = require("sequelize"); // Use url connection to connect, pay attention to changing root: XXXX back to the password of your database const BookieZilla = new Sequelize( "mysql://root:XXXXX@localhost/bookiezilla", { define: { timestamps: false// Cancel Sequelzie to automatically add timestamps (createdAt and updatedAt) to the data table, otherwise you may get an error when you do add, delete and change checks. } } ); module.exports = { BookieZilla // Exposing BookieZilla's interface to facilitate Model calls };
- In order to query information according to user id conveniently, a data can be added to the database at will.
-
New file userModel.js under server src models. Connect database with table structure file.
const db = require("../config/database.js"); const userModel = "../schema/user.js";// Introducing user's table structure const BookieZilla = db.BookieZilla;// Introducing database const User = BookieZilla.import(userModel);// The import method of sequelize is used to introduce table structure and instantiate User. const getUserById = async function(id) { const userInfo = await User.findOne({ where: { UserID: id } }); return userInfo; }; module.exports = { getUserById, getUserByEmail };
-
Create a new file userController.js under server src controllers to execute this method and return the result.
Koa provides a Context object that represents the context of a conversation (including HTTP requests and HTTP replies). By processing this object, you can control the content returned to the user.
const user = require("../models/userModel.js"); const getUserInfo = async function(ctx) { const id = ctx.params.id;// Get the id in the parameter passed in the url const result = await user.getUserById(id); ctx.body = result;// Put the result of the request back in the body of response }; module.exports = { getUserInfo, vertifyUserLogin };
-
New file auth.js under server src routes is used to plan routing rules under auth.
const auth = require("../controllers/userController.js"); const router = require("koa-router")(); router.get("/user/:id", auth.getUserInfo); module.exports = router;
-
Go back to app.js in the root directory and "mount" the routing rules onto 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");// Introducing 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());// Mounted on koa-router, all auth request paths are preceded by'/ auth'request paths. app.use(router.routes()).use(router.allowedMethods());// Mount routing rules on Koa. app.listen(4113, () => console.log("----------Server Started----------")); module.exports = app;
- API Test
SUCCESS!!!
Front-end and back-end data transfer
Because this project uses a front-end and back-end separated architecture, it is necessary to transfer data through json, in order to achieve the login function as an example to illustrate the specific steps of implementation.
Backend Authentication Logon
-
Ser src models userModel. JS additions for finding users through mailboxes.
// ... const getUserByEmail = async function(email) { const userInfo = await User.findOne({ where: { UserEmail: email } }); return userInfo; }; module.exports = { getUserById, getUserByEmail };
-
Ser src controller userController. JS adds methods to validate login information and return the results to the front end as json.
Note that JSON-WEB-TOKEN is actually used here to implement stateless requests. Refer to the principles and implementation methods of jwt. This article and This article.
Simply put, the login system using JSON-WEB-TOKEN should be as follows:
- The user enters the account password on the login page and sends the request to the back end with the account password (password encrypted by md5).
- The back end verifies the user's account and password information, and if it does, sends a TOKEN back to the client. If not, do not send TOKEN back, return validation error message.
- If the login is successful, the client saves the TOKEN in some way (Session Storage, Local Storage), and then carries the TOKEN in the request Header when requesting other resources.
- The back end receives the request information, first verifies whether TOKEN is valid, then sends the requested resource if it is valid, and then returns the validation error if it is invalid.
The corresponding libraries need to be installed before use:
npm i koa-jwt jsonwebtoken util -s
In addition, in order to ensure security, the password of the back-end database can not be saved in plaintext, and bcrypt encryption is used here.
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; // The data from post exists in request.body const userInfo = await user.getUserByEmail(data.email); if (userInfo != null) { // If this user is not found, null will be returned. if (!bcrypt.compareSync(data.psw, userInfo.UserPsw) { ctx.body = { status: false, msg: "Wrong password" }; } else { // If the password is correct const userToken = { id: userInfo.UserID, email: userInfo.UserEmail }; const secret = "react-koa-bookiezilla"; // Specify the key, which is then used to determine token legitimacy const token = jwt.sign(userToken, secret); // Issuance of token ctx.body = { status: true, token: token // Return to token }; } } else { ctx.body = { status: false, msg: "User doesn't exist" }; } }; module.exports = { getUserInfo, vertifyUserLogin };
-
Update the routing rules in serversrcroutesauth.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;
Front-end verifies data and sends requests
The front end mainly uses react-router for routing jump, uses semantic-ui as UI component library, and uses axios to send requests. The Login.js code is as follows:
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);
React Configuration Agent
-
Install http-proxy-middleware.
npm install http-proxy-middleware -s
-
The project initialized by create-react-app requires an eject to expose the basic configuration.
npm run eject
-
New file setupProxy.js under client SRC to configure proxy to forward information.
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 is configured in const devServer = new Webpack Dev Server (compiler, server Config), followed by the statement require (". / SRC / setupProxy") (devServer);
-
The format of the request is as follows:
axios .post("/auth/login", { email: this.state.email, psw: this.state.psw }) .then(res => { console.log(res); }) .catch(err => { console.log(err); });
- Favorite test link!
design principle
data base
User
*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`));
Book
*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`));
Order
*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`));
Front end
directory structure
. │ .gitignore │ package-lock.json │ package.json │ README.md │ yarn.lock │ ├─config // Basic Profile │ │ 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 // File configuration generated after object │ build.js │ start.js │ test.js │ └─src // Main Pages and Components │ App.css │ App.js │ index.css │ index.js │ serviceWorker.js │ setupProxy.js // Setting up proxy forwarding to solve cross-domain problems │ ├─actions // react-redux needs to define actions │ UpdateActions.js │ ├─components // Component section of the page │ BookList.jsx │ BookMarket.jsx │ FeedBack.jsx │ OrderInfo.jsx │ PublishForm.jsx │ SearchBar.jsx │ SideMenu.jsx │ StatisticData.jsx │ StepFlow.jsx │ ├─images // Image resources used in projects │ logo.png │ matthew.png │ ├─pages // Page section │ Home.jsx │ Login.jsx │ Market.jsx │ Message.jsx │ Publish.jsx │ Signup.jsx │ └─reducers // reducers that react-redux needs to define rootReducer.js
Implementation details
React-router
Reaction-router is used to control routing in the project. The basic principles are as follows:
-
In App.js, page or component corresponding to routing is introduced, and BrowserRouter, Route and Switch components in react-router-dom are defined.
// 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;
-
When a page Jump is required in a project page, components can be wrapped in withRouter in react-router-dom, and then NavLink can be used to jump.
// 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
In this project, react-redux is used for state management. The main function of Redux is to allow the state to be transferred among components with different branches, thus avoiding the problems of data transfer between components with different branches caused by the original method (such as this.props), and the state of parent components can not be modified by sub-components. The specific methods of use are as follows:
-
The new file rootReducer.js under src reducers is used to update the information in the central state tree.
// 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;
-
New file UpdateActions.js in src actions is used to define behavior.
// UpdateActions.js export const updateId = id => { return { type: "UPDATE_ID", id: id }; }; export const updateToken = token => { return { type: "UPDATE_TOKEN", token: token }; };
-
In srcindex.js, the component in react-redux is used to wrap the project entry file, and the state tree is built globally.
// 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();
-
When the information in the state tree needs to be updated, the introduced action is used as a function to update it.
// 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));
-
When you need to use the information in the state tree, call the connect package component in react-redux first, and then use this.props to call directly.
// 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);
back-end
directory structure
. │ app.js │ package-lock.json │ package.json │ └─src ├─config // Database Configuration │ database.js │ ├─controllers // Controller, which fetches request data and calls methods in models to process and return results │ apiController.js │ msgController.js │ userController.js │ ├─models // Instance model, mainly using Sequelize definition method to add, delete and modify the database │ bookModel.js │ CommentModel.js │ orderModel.js │ userModel.js │ ├─routes // Routing, different files correspond to different types of api interfaces, which are related to authorization, function realization and information transmission respectively │ api.js │ auth.js │ msg.js │ └─schema // Database table structure, which can be automatically generated using Sequelize book.js comment.js order.js user.js
Implementation details
Route mounting
When Koa's back-end listening port receives the request, it will be processed according to the routing rules in app.js. We define different types of interfaces in different files, and then call them through router.use() to avoid interface confusion and complexity.
// 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;
Project results
Log in and register
Bookizilla can achieve user registration and user login functions, in which the data needed for user registration is formatted (such as verifying the Email format, ensuring that the two password input data are consistent and not less than 6 bytes). If the user makes an error in the registration process, a corresponding prompt will appear to guide the user to enter correctly.
Login.jsx
Signup.jsx
Personal Home Page
Bookiezilla's home page presents information and data related to the user (such as FAVES, VIEWS, etc.), but mocks and all books published by the user are temporarily used because the back end does not store relevant data.
Home.jsx
Book Market
Bookiezilla's book market presents all the books published by all users. Users can use the search box above to enter keywords (such as title, label, ISBN, etc.). Users can also click the button below the book to see the specific information, and then decide whether to conclude the transaction, or click the link in Amazon to see the book details.
Market.jsx
Book Publishing
Bookiezilla allows users to publish books and set key information about orders (such as book basic information, trading patterns, looking for buyers or sellers, etc.). It should be noted that since a large part of the content of book publishing and book purchasing overlaps, the two are merged here and the TradePart option is given to enable users to choose whether they want to publish or buy books.
Publish.jsx
Information Delivery
Bookiezilla has set up an information publishing panel for communication and information publishing among users. Users can directly post comments or reply to others'comments, so as to have continuous communication.
Message.jsx