一、项目初始化
1、创建项目依赖文件
npm init -y ,生成
pageage.json
文件,用于记录项目的依赖
2、创建git本地仓库
git init,生成
.git
隐藏文件,git的本地仓库
二、搭建项目
1、安装koa框架
npm i koa
2、编写最基本的app
创建src/main.js
const Koa = require('koa') const app = new Koa() // 中间件 app.use((ctx,next)=>{ ctx.body = 'hello Koa' }) // 监听端口 app.listen(3000,()=>{ console.log('server is running at http://localhost:3000') })
三、项目基本优化
1、自动重启服务
安装nodemon工具,npm i nodemon -D
编写package.json脚本,执行npm run dev 启动服务
{ "name": "api", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "dev": "nodemon ./src/main.js", "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "koa": "^2.15.3", }, "devDependencies": { "nodemon": "^3.1.4" } }
2、读取配置文件
安装dotenv,在应用程序中,可以使用dotenv库来读取.env文件中的配置信息,dotenv通过读取.env文件内容,并在解析后将键值对添加到process.env对象中
npm i dotenv
创建配置文件
.env
// .env文件 APP_PORT = 8000
创建配置文件
src/config/config.default.js
const dotenv = require('dotenv'); dotenv.config(); module.exports = process.env
改写
main.js
文件
const { APP_PORT } = require('./config/config.default') const Koa = require('koa') const app = new Koa() // 中间件 app.use((ctx,next)=>{ ctx.body = 'hello Koa' }) // 监听端口 app.listen(APP_PORT,()=>{ console.log('server is running at http://127.0.0.1:' + APP_PORT) })
四、添加路由
路由---根据不同的URL,调用对应的处理函数
1、安装路由
npm i koa-router
使用步骤:1、导入包;2、实例化对象;3、编写路由;4、注册中间件
改写
main.js
const Koa = require('koa') const Router = require('koa-router') const { APP_PORT } = require('./config/config.default') const app = new Koa() const router = new Router() // 路由 router.get('/', async(ctx,next)=>{ ctx.body = 'hello Koa' }) // 中间件 app.use(router.routes()) // 监听端口 app.listen(APP_PORT,()=>{ console.log('server is running at http://127.0.0.1:' + APP_PORT) })
2、编写路由
创建
src/router/user.route.js
const Router = require('koa-router') const userRouter = new Router({prefix:'/users'}) userRouter.get('/', async(ctx,next)=>{ ctx.body = 'hello users' }) module.exports = userRouter
改写
main.js
const Koa = require('koa') const { APP_PORT } = require('./config/config.default') const userRouter = require('./router/user.route') const app = new Koa() // 中间件 app.use(userRouter.routes()) // 监听端口 app.listen(APP_PORT,()=>{ console.log('server is running at http://127.0.0.1:' + APP_PORT) })
五、目录结构优化
1、将http服务和app业务拆分
创建
src/app/index.js
const Koa = require('koa') const userRouter = require('./router/user.route') const app = new Koa() // 中间件 app.use(userRouter.routes()) modelu.exports = app
2、将路由和控制器拆分
路由---解析URL,分发给控制器对应的方法
控制器---处理不同的业务
创建
src/controller/user.controller.js
class UserController { async register(ctx,next) { ctx.body = "用户注册成功" } async login(ctx,next) { ctx.body = "用户登录成功" } }
改写
src/router/user.route.js
const Router = require('koa-router') const router = new Router({prefix:'/users'}) const {register, login} = require('../controller/user.controller') // 注册接口 router.post('/register', register) // 登录接口 router.post('/login', login) // 导出路由 module.exports = router
六、解析body
1、安装koa-body
npm i koa-body
2、注册中间件
改写
app/index.js
const Koa = require('koa') const { koaBody } = require('koa-body') const userRouter = require('../router/user.route') const app = new Koa() app.use(koaBody()) // 路由中间件 app.use(userRouter.routes()) module.exports = app;
3、解析请求数据
改写
user.controller.js
/** * 用户控制模块 */ const { createUser } = require('../service/user.service') class UserController { async register(ctx, next) { // 1 获取数据 const { username, password } = ctx.request.body; // 2 操作数据库 const res = await createUser(username, password); // 3 返回数据 ctx.body = res } async login(ctx, next) { ctx.body = "用户登录成功" } } module.exports = new UserController()
4、拆分service层
service层主要用于数据库处理
创建
src/service/user.service.js
class UserService { async createUser(username, password) { return "写入数据库成功" } } module.exports = new UserService();
七、数据库操作
1、sequlize ORM 数据库工具
ORM:对象关系映射
1、数据表映射对应一个类
2、数据表中的数据行(记录)对应一个对象
3、数据表中的字段对应对象的属性
4、数据表的操作对应对象的方法
2、安装sequlize和mysql2
npm i --save sequlize mysql2
3、连接数据库
修改
.env
文件
# .env文件 APP_PORT = 8000 # 配置数据库 DB_HOST = localhost DB_PORT = 3306 DB_USER = root DB_PASSWORD = wlg1213 DB_NAME = data_analysis
创建
src/db/seq.js
文件
const { Sequelize } = require('sequelize') const { DB_HOST, DB_USER, DB_PORT, DB_PASSWORD, DB_NAME } = require('../config/config.default') const seq = new Sequelize(DB_NAME, DB_USER, DB_PASSWORD, { host: DB_HOST, dialect: 'mysql', logging: false, timezone: '+08:00' // 保存为本地时区 }) // seq.authenticate().then(() => { // console.log('数据库连接成功') // }).catch(err => { // console.log('数据库连接失败', err) // }) module.exports = seq
八、创建User模型
sequelize主要通过model操作对应数据表
创建
src/model/user.model.js
const { DataTypes } = require('sequelize'); const seq = require('../db/seq') // 定义模型 const User = seq.define('User', { // 在这里定义模型属性 username: { type: DataTypes.STRING, allowNull: false, unique: true, comment: '用户名, 唯一' }, password: { type: DataTypes.CHAR(64), allowNull: false, comment: '密码' }, is_admin: { type: DataTypes.BOOLEAN, allowNull: false, defaultValue: 0, comment: '是否管理员 0:否 1:是' } }, { // 这是其他模型参数 // timestamps: false }); // User.sync({ force: true }); // 自动创建表 module.exports = User;
九、添加用户入库
改写
src/service/user.service.js
const User = require('../model/user.model') class UserService { async createUser(username, password) { // todo: 写入数据库 // 插入数据 // await表达式:Promise对象的值 const res = await User.create({username, password}) // console.log(res) return res.dataValues } } module.exports = new UserService()
改写
src/constroller/user.controller.js
const { createUser } = require('../service/user.service') class UserController { async register(ctx, next) { // 1.获取数据 console.log(ctx.request.body) const { username, password} = ctx.request.body // 2.操作数据库 const res = await createUser(username, password) // 3.返回结果 ctx.body = { code: 0, message: '用户注册成功', result: { id: res.id, username: res.username } } } async login(ctx, next) { ctx.body = '登录成功' } } module.exports = new UserController()
十、错误处理
1、合法性处理
改写
src/controller/user.controller.js
/** * 用户控制模块 */ const { createUser, getUserInfo } = require('../service/user.service') class UserController { async register(ctx, next) { // 1 获取数据 const { username, password } = ctx.request.body; // 合法性验证 if (!username || !password) { console.error('用户名或密码为空',ctx.request.body) ctx.status = 400; ctx.body = { code: '10001', message: '用户名或密码为空', result: { } } return; } // 合理性验证 if(getUserInfo({username})){ ctx.status = 409; ctx.body = { code: '10002', message: '用户名已存在', result: { } } return } // 2 操作数据库 const res = await createUser(username, password); // 3 返回数据 ctx.body = { code: 0, message: '用户注册成功', result: { id: res.id, username: res.username } } } async login(ctx, next) { ctx.body = "用户登录成功" } } module.exports = new UserController()
2、合理性处理
改写改写
src/service/user.service.js
const User = require("../model/user.model.js"); class UserService { // 创建用户 async createUser(username, password) { // 利用用户模型插入数据 // await 表达式: promise 对象的值 const res = await User.create({ username, password }) // console.log(res); return res.dataValues; } // 查询用户是否存在 async getUserInfo({id, username, password,is_admin}) { const whereOpt = {} id && Object.assign(whereOpt, {id}) username && Object.assign(whereOpt, {username}) password && Object.assign(whereOpt, {password}) is_admin && Object.assign(whereOpt, {is_admin}) // 查询数据 const res = await User.findOne({ // 查询的字段 attributes: ['id', 'username', 'password','is_admin'], where: whereOpt, }) return res?res.dataValues:null; } } module.exports = new UserService();
十一、拆分中间件
利用中间件处理一些校验的问题
创建
src/middleware/user.middleware.js
const { getUserInfo } = require('../service/user.service') // 验证用户合法性 const userValidator = async (ctx, next) => { const { username, password } = ctx.request.body; if (!username || !password) { console.error('用户名或密码为空',ctx.request.body) ctx.status = 400; ctx.body = { code: '10001', message: '用户名或密码为空', result: { } } return; } await next(); } // 验证用户名的合理性 const verifyUser = async (ctx, next) => { const { username } = ctx.request.body; if(getUserInfo({username})){ ctx.status = 409; ctx.body = { code: '10002', message: '用户名已存在', result: { } } return } await next(); } module.exports = { userValidator, verifyUser }
在router中的应用
改写
src/router/user.router.js
const Router = require('koa-router') const router = new Router({prefix:'/users'}) const { userValidator,verifyUser } = require('../middleware/user.middleware') const {register, login} = require('../controller/user.controller') // 注册接口 router.post('/register',userValidator,verifyUser, register) // 登录接口 router.post('/login', login) // 导出路由 module.exports = router
十二、统一的错误处理
1、建立错误码
创建src/constant/err.type.js
module.exports = { userFormateError:{ code:'10001', message:'用户名或密码为空', result:'' }, userAlreadyExited:{ code:'10002', message:'用户已经存在', result:'' }, userRegisterError:{ code:'10003', message:'用户注册失败', result:'' } }
在处理校验的中间件中引用,并提交错误
改写
src/middleware/user.middleware.js
const { getUserInfo } = require('../service/user.service') const {userFormateError,userAlreadyExited} = require('../constant/err.type') // 验证用户合法性 const userValidator = async (ctx, next) => { const { username, password } = ctx.request.body; if (!username || !password) { console.error('用户名或密码为空',ctx.request.body) // ctx.status = 400; ctx.app.emit('error', userFormateError, ctx); return; } await next(); } // 验证用户名的合理性 const verifyUser = async (ctx, next) => { const { username } = ctx.request.body; if(getUserInfo({username})){ console.error('用户名已存在',ctx.request.body) ctx.app.emit('error', userAlreadyExited, ctx); return } await next(); } module.exports = { userValidator, verifyUser }
2、处理错误状态
对于提交的错误信息,在
src/app/index.js
中进行统一处理
const Koa = require('koa') const { koaBody } = require('koa-body') const errHandler = require('./errHandler') const userRouter = require('../router/user.route') const app = new Koa() app.use(koaBody()) app.use(userRouter.routes()) // 统一的错误处理 app.on('error', errHandler) module.exports = app;
创建
src/app/errHandler.js
module.exports = (error, ctx) => { let status = 500 switch (error.code) { case '10001': status = 400 break case '10002': status = 409 break default: status = 500 } ctx.status = status ctx.body = error }
十三、错误处理的补充与完善
对于
src/middleware/user.middleware.js
中验证用户合理性进行完善对于调用service层,应该用try...catch进行异常捕获
const { getUserInfo } = require('../service/user.service') const {userFormateError,userAlreadyExited,userRegisterError} = require('../constant/err.type') // 验证用户合法性 const userValidator = async (ctx, next) => { const { username, password } = ctx.request.body; if (!username || !password) { console.error('用户名或密码为空',ctx.request.body) // ctx.status = 400; ctx.app.emit('error', userFormateError, ctx); return; } await next(); } // 验证用户名的合理性 const verifyUser = async (ctx, next) => { const { username } = ctx.request.body; try { const res = await getUserInfo({ username }) if(res){ console.error('用户名已存在',ctx.request.body) ctx.app.emit('error', userAlreadyExited, ctx); return } } catch (error) { console.error('获取用户信息错误',error) ctx.app.emit('error', userRegisterError, ctx) return } await next(); } module.exports = { userValidator, verifyUser }
对于
src/controller/user.controller.js
,调用service层应该也用try...catch异常捕获
/** * 用户控制模块 */ const { createUser } = require('../service/user.service') const { userRegisterError} = require('../constant/err.type') class UserController { async register(ctx, next) { // 1 获取数据 const { username, password } = ctx.request.body; // 2 操作数据库 try { const res = await createUser(username, password); // 3 返回数据 ctx.body = { code: 0, message: '用户注册成功', result: { id: res.id, username: res.username } } } catch (error) { console.error('用户注册失败',error) ctx.app.emit('error', userRegisterError, ctx) } } async login(ctx, next) { ctx.body = "用户登录成功" } } module.exports = new UserController()
十四、加密
在将密码保存到数据库之前,要对密码进行加密处理
bcrypt.js 零依赖,加盐加密
1、安装bcryptjs
npm i bcryptjs
2、加密处理
加密做成一个中间件
改写
src/middleware/user.middleware.js
const bcrypt = require('bcryptjs') const { getUserInfo } = require('../service/user.service') const {userFormateError,userAlreadyExited,userRegisterError} = require('../constant/err.type') // 密码处理 const crpytPassword = async (ctx, next) => { const { password } = ctx.request.body; const salt = bcrypt.genSaltSync(10); // hash保存的是密文 const hash = bcrypt.hashSync(password, salt); ctx.request.body.password = hash; await next(); } module.exports = { crpytPassword }
十五、用户登录
1、用户验证
改写登录接口
// 登录接口 router.post('/login',userValidator,verifyLogin, login)
验证用户
const verifyLogin = async (ctx, next) => { const { username, password } = ctx.request.body; try { const res = await getUserInfo({ username }) // 验证用户是否存在 if (!res) { console.error('用户不存在',{username}) ctx.app.emit('error', userDoesNotExist, ctx); return; } // 验证密码 const isMatch = bcrypt.compareSync(password, res.password); if (!isMatch) { console.error('密码无效',ctx.request.body) ctx.app.emit('error', invalidePassword, ctx); return; } } catch (error) { console.error('用户登录失败',error) ctx.app.emit('error', userLoginError, ctx) return } await next() }
更新错误码类型
module.exports = { userDoesNotExist: { code:'10004', message:'用户不存在', result:'' }, userLoginError: { code:'10005', message:'用户登录失败', result:'' }, invalidePassword: { code:'10006', message:'密码不匹配', result:'' } }
2、用户认证
登录成功后,给用户颁发一个令牌token,用户在以后得每一次请求中携带这个令牌
JWT:JSON WEB TOKEN
JWT分为三部分:header头部、playload载荷、signature签名
安装jsonwebtoken
npm i jsonwebtoken
async login(ctx, next) { const { username } = ctx.request.body; // 1 获取用户信息 (在token中要包含用户的信息,id,username,is_admin) try { const res = await getUserInfo({ username }) // 从返回结果对象中除去password属性,将剩下的属性放到res对象中 const { password, ...resUser } = res ctx.body = { code: 0, message: '用户登录成功', result: { id: resUser.id, username: resUser.username, is_admin: resUser.is_admin, token: jwt.sign(resUser,JWT_SECRET, { expiresIn: '1d' }) } } } catch (error) { console.error('用户登录失败', error) ctx.app.emit('error', userLoginError, ctx) } }
十六、修改密码
1、鉴权中间件
创建src/middleware/auth.middleware.js
const jwt = require('jsonwebtoken') const { JWT_SECRET } = require('../config/config.default') const { tokenExpiredError,invalideToken } = require('../constant/err.type') const auth = async (ctx, next) =>{ const { authorization } = ctx.request.headers const token = authorization.replace('Bearer ','') try { // user中包含了payload的信息(id,username,is_admin) const user = jwt.verify(token, JWT_SECRET) ctx.state.user = user } catch (error) { switch (error.name) { case 'TokenExpiredError': console.error('token已过期',error) return ctx.app.emit('error', tokenExpiredError, ctx) case 'JsonWebTokenError': console.error('无效的token',error) return ctx.app.emit('error', invalideToken, ctx) default: break } } await next() } module.exports = { auth }
2、修改密码路由
// 修改密码 router.patch('/',auth,crpytPassword,changePassword )
3、修改密码控制器
// 修改密码 async changePassword(ctx, next) { // 1 获取数据 const { id } = ctx.state.user; const { password } = ctx.request.body; console.log(id, password); // 2 操作数据库 try { const res = await updateById({ id, password }); console.log(res); if (res) { ctx.body = { code: 0, message: '修改密码成功', result: '' } } else { ctx.body = { code: 1, message: '修改密码失败', result: '' } } } catch (error) { console.error('修改密码异常', error) ctx.app.emit('error', updatePasswordErr, ctx) } }
4、操作service层updateById
// 修改用户信息 async updateById({id, username, password, is_admin}) { const whereOpt = {id}; const newUser = {}; username && Object.assign(newUser, {username}); password && Object.assign(newUser, {password}); is_admin && Object.assign(newUser, {is_admin}); console.log(newUser); const res = await User.update(newUser, { where: whereOpt, }); return res[0] > 0 ? true : false; }
十七、路由自动加载
由于每一个模块都要写一个路由,在
app/index.js
里面注册,这样代码就比较臃肿,利用fs模块,让路由自动加载创建
src/router/index.js
;获取该目录下除了index.js
const fs = require('fs') const Router = require('koa-router') const router = new Router() fs.readdirSync(__dirname).forEach(file=>{ // console.log(file); if(file !== 'index.js'){ const r = require(`./${file}`) router.use(r.routes()) } }) module.exports = router;
在
app/index.js
中注册路由
const Koa = require('koa') const { koaBody } = require('koa-body') const errHandler = require('./errHandler') const router = require('../router') // console.log(router); const app = new Koa() app.use(koaBody()) app.use(router.routes()) // 统一的错误处理 app.on('error', errHandler) module.exports = app;