寫(xiě)在前面
本文適用于對(duì)后端開(kāi)發(fā)領(lǐng)域較為陌生的初級(jí)前端開(kāi)發(fā)小伙伴屯断,如果你想一個(gè)人搞定一整個(gè)項(xiàng)目(服務(wù)端接口定義開(kāi)發(fā)+前端頁(yè)面渲染+數(shù)據(jù)庫(kù)搭建+服務(wù)器搭建+部署上線)殖演,希望這篇文章能給你帶來(lái)一點(diǎn)點(diǎn)參考價(jià)值趴久。
項(xiàng)目簡(jiǎn)介
實(shí)現(xiàn)簡(jiǎn)單的登錄彼棍、表單的增刪改查、文件上傳等功能。以下是項(xiàng)目地址:
前端源碼
服務(wù)端源碼(ps:數(shù)據(jù)庫(kù)rnpxDemo.sql在主目錄下)
技術(shù)棧
前端
主要基于React全家桶畦幢,UI框架選用ant-design-pro宇葱。
前端部分不做過(guò)多贅述,有興趣的可以直接查看源碼诸尽。
服務(wù)端
服務(wù)端采用Nodejs您机,框架采用的是Koa2.0
目錄結(jié)構(gòu)說(shuō)明
├── bin
│ ├── index.js # 項(xiàng)目入口文件
│ ├── www # 配置引入
├── config
│ ├── config.js # 配置項(xiàng)目
│ ├── db.js # 數(shù)據(jù)庫(kù)配置
│ ├── redis # redis配置
│ ├── render.js # 數(shù)據(jù)接口渲染
├── app
│ ├── controllers # 操作層 執(zhí)行服務(wù)端模板渲染际看,json接口返回?cái)?shù)據(jù)仲闽,頁(yè)面跳轉(zhuǎn)
│ │ ├── admin.js
│ │ ├── index.js
│ │ ├── user.js
│ │ └── work.js
│ ├── middleware # 中間件集合
│ │ └── index.js
│ ├── models # 數(shù)據(jù)模型層 執(zhí)行數(shù)據(jù)操作
│ │ └── user.js
│ ├── routers # 路由層 控制路由
│ │ ├── user.js # /user/* 子路由
│ │ ├── home.js # 主頁(yè)子路由
│ │ ├── index.js # 子路由匯總文件
├── static #靜態(tài)資源
數(shù)據(jù)庫(kù)
數(shù)據(jù)庫(kù)采用Mysql赖欣,框架采用的是Sequelize,是一個(gè)基于promise的關(guān)系型數(shù)據(jù)庫(kù)Node.js ORM框架,用起來(lái)還蠻方便验庙,推薦使用壶谒。
安裝mySql數(shù)據(jù)庫(kù)
可以參考官方文檔
這里順便推薦一款比較實(shí)用的數(shù)據(jù)庫(kù)管理工具:Navicat Premium
集群管理
集群管理采用PM2汗菜,是一個(gè)帶有負(fù)載均衡功能的 Node 應(yīng)用的進(jìn)程管理器。pm2的安裝和使用非常容易巡揍,具體可參考pm2的安裝和使用
API文檔工具
這里再分享一個(gè)能自動(dòng)生成API文檔的庫(kù)koa2-swagger-ui,可以把 json 文件的 api 數(shù)據(jù)渲染成 swagger ui阱当,具體使用方法可以參考項(xiàng)目代碼糜工。
npm i koa2-swagger-ui -D
服務(wù)端配置說(shuō)明
www入口文件配置
const koa = require("koa");
const koaSwagger = require('koa2-swagger-ui');
const router = require('../app/routers/index');
const path = require('path');
const koaBody = require('koa-body');
const { cross, onerror, onheader } = require("../app/middleware");
const pkg = require("../package.json");
const app = new koa();
app.use(onerror()); // 系統(tǒng)錯(cuò)誤,統(tǒng)一錯(cuò)誤捕獲
/*更多的配置可以拉取倉(cāng)庫(kù)代碼進(jìn)行查看*/
數(shù)據(jù)庫(kù)配置
配置文件路徑:/config/db.js
針對(duì)不同環(huán)境油坝,配置不同的數(shù)據(jù)庫(kù)地址:
const Sequelize = require('sequelize');
const { env } = require("./config");
let config = {};
switch (env) {
case "dev":
config = {
database: 'testDB', // 使用哪個(gè)數(shù)據(jù)庫(kù)
username: 'root', // 用戶名
password: 'dev@123', // 口令
host: '127.0.01', // 主機(jī)名
port: 3306 // 端口號(hào)澈圈,MySQL默認(rèn)3306
};
break;
case "test":
config = {
database: 'testDB', // 使用哪個(gè)數(shù)據(jù)庫(kù)
username: 'root', // 用戶名
password: 'test@123', // 口令
host: 'test.mysql.rds.aliyuncs.com', // 主機(jī)名
port: 3306 // 端口號(hào)瞬女,MySQL默認(rèn)3306
};
break;
case "prd":
config = {
database: 'testDB', // 使用哪個(gè)數(shù)據(jù)庫(kù)
username: 'root', // 用戶名
password: 'prd@123', // 口令
host: 'prd.mysql.rds.aliyuncs.com', // 主機(jī)名
port: 3306 // 端口號(hào)努潘,MySQL默認(rèn)3306
};
break;
}
export const sequelize = new Sequelize(config.database, config.username, config.password, {
host: config.host,
dialect: 'mysql',
pool: {
max: 5,
min: 0,
idle: 30000
}
});
路由配置
配置文件路徑:/app/routers/index.js
const router = new require('koa-router')()
const home = require('./home')
const user = require("./backend/user")
const swaggerJSDoc = require('swagger-jsdoc')
const pkg = require("../../package.json")
const swaggerDefinition = {
info: {
title: 'API',
version: '1.0.0',
description: 'API',
},
host: `localhost:${pkg.port}`,
basePath: '/' // Base path (optional)
};
const options = {
swaggerDefinition,
apis: ['./app/routers/backend/*.js', './app/routers/front/*.js'], // 寫(xiě)有注解的router的存放地址
};
const swaggerSpec = swaggerJSDoc(options)
// 通過(guò)路由獲取生成的注解文件
router.get('/swagger.json', async function (ctx) {
ctx.set('Content-Type', 'application/json');
ctx.body = swaggerSpec;
})
router.use('', home.routes())
router.use('/backend/user', user.routes())
module.exports = router
中間件集合
通俗的講:中間件就是匹配路由之前或者匹配路由完成做的一系列的操作渤刃。
const { renderData } = require("../../config/render")
const { log } = require("../../util/utils")
const koajwt = require('koa-jwt')
/**
* 跨域處理
* @example app.use(cross())
*/
export function cross() {
return async function (ctx, next) {
ctx.set('Access-Control-Allow-Origin', ctx.get("Origin") == null ? "*" : ctx.get("Origin"));
ctx.set('Access-Control-Allow-Methods', ctx.get("Access-Control-Request-Method") || "PUT, GET, POST, DELETE, OPTIONS");
ctx.set('Access-Control-Allow-Headers', ctx.get("Access-Control-Request-Headers") || "Content-Type");
ctx.set('Access-Control-Allow-Credentials', "true");
if (ctx.method == "OPTIONS") {
ctx.status = 204
}
await next();
}
}
/**
* 兼容路由后綴卖子,如 xxx/xxx.json == xxx/xxx 地址
* @param suffixes string[] 后綴列表
* @example app.use(adaptRouteSuffix([".json"]))
*/
export function adaptRouteSuffix(suffixes = []) {
return async function (ctx, next) {
suffixes.forEach(e => {
var re = new RegExp(e);
ctx.path = ctx.path.replace(re, "")
})
await next();
}
}
/**
* 錯(cuò)誤日志捕獲
* @example app.use(onerror())
*/
export function onerror() {
return async function (ctx, next) {
return next().catch((err) => {
if (err.status === 401) {
ctx.status = 200;
renderData(ctx, null, 401, "用戶沒(méi)有權(quán)限(令牌洋闽、用戶名诫舅、密碼錯(cuò)誤)", err.message)
} else if (ctx.status === 404) {
var err = new Error('Not Found');
err.status = 404;
renderData(ctx, null, 404, "系統(tǒng)繁忙刊懈,請(qǐng)聯(lián)系管理員", err.message);
} else {
renderData(ctx, null, -1, "服務(wù)端錯(cuò)誤", err.message);
log(err.message);
}
})
}
}
/**
* 延遲
* @example await delay(5000) // 延遲5s
*/
export function delay(time) {
return new Promise(function (resolve) {
setTimeout(function () {
resolve();
}, time);
});
};
/**
* 請(qǐng)求頭攔截
*/
export function onheader() {
return koajwt({
secret: 'polo_token'
}).unless({
path: [/\/user\/login|swagger|front/]
})
}
基本功能展示
登錄
登錄模塊虚汛,這里就介紹下服務(wù)端的實(shí)現(xiàn)皇帮,主要依賴兩個(gè)插件属拾,jsonwebtoken(jwt)和koa-jwt冷溶,前者主要負(fù)責(zé)加密逞频,生成token返回給前端虏劲,后者實(shí)現(xiàn)對(duì)token的校驗(yàn)褒颈,在中間件中實(shí)現(xiàn)請(qǐng)求頭攔截谷丸,驗(yàn)證失敗返回401刨疼。具體可以參考代碼./app/controllers/user.js鹅龄。
/**
* 用戶登錄
* @return 用戶登錄成功對(duì)象
*/
export async function login(ctx) {
const data = ctx.request.body;
if (!data.name || !data.password) {
renderData(ctx, null, 102, '參數(shù)不合法');
}
const result = await GetUser({
name: data.userName,
password: data.password
})
if (result !== null) {
const token = jwt.sign({
name: result.name,
password: result.password
}, 'polo_token', { expiresIn: '2h' });
const userData = {
userName: result.name,
token: 'Bearer ' + token
}
renderData(ctx, userData, 0, '登錄成功');
} else {
renderData(ctx, null, 101, '用戶名或密碼錯(cuò)誤');
}
}
/**
* 請(qǐng)求頭攔截
*/
export function onheader() {
return koajwt({
secret: 'polo_token'
}).unless({
path: [/\/user\/login|swagger|front/]
})
}
后臺(tái)部分內(nèi)容展示
寫(xiě)在最后
以上文章是我前段時(shí)間全棧開(kāi)發(fā)已上線項(xiàng)目的簡(jiǎn)略版分享玷坠,文章開(kāi)頭已附上了該項(xiàng)目的前后端源碼,順便提供了測(cè)試數(shù)據(jù)(rnpxDemo.sql),只需把代碼克隆下來(lái)并保證電腦已安裝所需軟件樟凄,另有不足之處也請(qǐng)多多指正缝龄,非常感謝挂谍。