# uni-koa **Repository Path**: cason/uni-koa ## Basic Information - **Project Name**: uni-koa - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2023-03-03 - **Last Updated**: 2023-03-03 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # uni-koa 基于 uniCloud 云函数项目兼容 koa2 插件或 uni-id 的 web 开发框架 uni-koa 借鉴 koa2 的思想,针对 uniCloud 云函数特点构建一个快速开发方便扩展的框架,充分使用 koa2 第三方插件和 uniClound 插件的优势最大程度来满足云函数项目开发的需求。 ## uni-koa 公共模块 把 uni-koa公共模块 导入或拷贝到在项目中 uniCloud/cloudfunctions/common 下 ### uni-koa 请求分类 + 习惯 koa 开发的用户常用 RESTful 方法+路由+参数的来请求,这里称为 **http 请求方式** + 在使用 uniCloud 云函数中 uni-id 我们常用 方法名+参数来请求,这里称为 **action 请求方式** ### uni-koa 上下文 ctx #### 设置数据 - **ctx.body = 数据** 设置请求成功信息,如 {code:0,data:数据} - **ctx.throw(状态码,失败信息)** 设置请求失败信息,如{code:状态码,msg:失败信息} - **ctx.state.变量=数据** 用来配置全局变量,如:登录成功后设置用户信息 ctx.state.user = 用户信息,这样全局就可以获取用户信息 - **ctx.token=新的** token 用于 token 自动刷新后设置的 newToken #### 获取数据 - **= ctx.method** 获取请求方法名。http请求方式post/delete/put/get(默认); action请求方式均为 post - **= ctx.url** 获取请求 url - **= ctx.path** 获取路径名 - **= ctx.query** 获取 get 请求的参数值 - **= ctx.request.body** 获取 post/delete/put 请求参数值 - **= ctx.params** 获取 http 请求方式中动态路参数 或 获取 action 请求中 params 值 - **= ctx.header.authorization** 获取 http 请求方式绑定的 "Bearer "+token - **= ctx.token** 获取新的token - **= ctx.body** 获取设置请求成功的信息 - **= ctx.status** 获取状态码 - **= ctx.event** 获取客户端调用云函数时传入的参数 - **= ctx.context** 获取客户端调用的调用信息和运行状态 - **= ctx.event.params** 获取 action 请求方式的参数 ## 使用笔记 以用户管理为例使用 uni-koa, 分为三个方面 + http 请求 开发方式 + action 请求 开发方式 + 前端使用 ### 常规目录结构 一般由入口文件、配置文件、路由文件、控制器文件、数据库业务文件和中间件文件构成 ``` app + config // 配置文件 + controller // 解析输入, 返回结果 + middleware // 存放中间件 + node_modules // 安装的包 + routes // 路由管理 - index.js // 自动挂载本目录下路由管理文件 + service // 业务逻辑, 读写数据库 + utils // 工具方法 - index.js // 云函数项目入口文件 - package.json ``` ## http 请求开发方式 ### 创建云函数项目 + 创建云函数 app-koa,一般一个项目就是一个云函数 + 初始化 app-koa 项目 ``` npm init -y ``` + 安装项目依赖 ``` npm install koa-router koa-parameter bcryptjs jsonwebtoken koa-jwt /* koa-router 路由管理 koa-parameter 参数校验 bcryptjs 加密 jsonwebtoken token生成与校验 koa-jwt 路由校验 */ ``` + 设置依赖 uni-koa ### 入口文件 index.js ```js // app-http/index.js const Koa = require("uni-koa") const app = new Koa() const parameter = require('koa-parameter') const koaJwt = require('koa-jwt') // 自动注入路由方法 const routes = require("./routes") // 自动刷新 token 中间件 const refresh = require("./middleware/refresh-token") // 耗时统计 app.use(async (ctx, next) => { const start = new Date() await next() const ms = new Date() - start console.log(`${ctx.method} ${ctx.url} - ${ms}ms`) // uniCloud.logger.info(`${ctx.method} ${ctx.url} - ${ms}ms`) }) // 自动刷新 token app.use(refresh) // 路由控制验证 token const {TOKEN}=require("./config") app.use(koaJwt({secret:TOKEN.key}).unless({ path:TOKEN.filter_path // 配置不验证 token 的路由 })) // 校验参数 app.use(parameter(app)) // 挂载路由 routes(app) // 启动监听请求并返回结果 exports.main = app.listen() ``` ### 配置文件 ```js // app-http/config/index.js module.exports = { TOKEN: { key: "设置你的 token 加密串", filter_path: [ // 不验证的路径 "/api/users/login", "/api/users/register" ], exp: 7 * 24 * 60 * 60, // 过期时间 7 天 auto_refresh:true // 自动刷新 token interval:7 * 24 * 60 * 60 / 2 } } ``` ### 中间件 自动刷新 token ```js // app-http/middleware/refresh-token.js const jsonwebtoken = require("jsonwebtoken") const { TOKEN } = require("../config") module.exports = async (ctx, next) => { if (TOKEN.auto_refresh) { let { exp, id, username } = ctx.state.user || {} if (exp) { // 这里约定 TOKEN.exp在前后超期范围一半范围内重新生成 token,否则重新登录 if (Math.abs(new Date(exp * 1000) - new Date()) / 1000 > parseInt(TOKEN.interval)) { const payload = { id, username } const token = jsonwebtoken.sign(payload, TOKEN.key, { expiresIn: TOKEN.exp }) ctx.token = token } } } await next() } ``` ### 路由 #### 路由自动装载到应用 这里未考虑子目录,需要自行扩展 ```js // app-http/routes/index.js const fs = require("fs") module.exports = (app) => { fs.readdirSync(__dirname).forEach(file => { if (file === "index.js") return const route = require(`./${file}`) app.use(route.routes()).use(route.allowedMethods()) }) } ``` #### 用户路由设置 ```js // app-http/routes/users.js const Router = require("koa-router") const router = new Router({ prefix: "/api/users" //前缀 }) const users = require("../controller/users") // 加载用户管理控制器 router.post("/register", users.register) // 用户注册 router.post("/login", users.login) // 用户登录 router.get("/", users.list) // 用户列表 router.put("/:id", users.update) // 修改用户 router.delete("/:id", users.remove) // 删除用户 module.exports = router ``` ### 用户管理控制器 ```js // app-http/controller/users.js const jsonwebtoken = require('jsonwebtoken') const users = require("../service/users") const { encrypt, compare, dateFormat } = require("../utils") const { TOKEN } = require("../config") class Users { // 用户注册 async register(ctx) { // 校验参数 ctx.verifyParams({ username: { type: "string", required: true }, password: { type: "string", required: true } }) // 获取参数 const { username, password } = ctx.request.body // 获取用户信息 const userinfo = await users.findOne({ username }) if (userinfo) ctx.throw(409, '用户名已经存在') // 加密密码 const pwd = encrypt({ username, password }) // 添加用户 let res = await users.add({ username, password: pwd }) || {} // 生成 token const payload = { id: res._id, username } const token = jsonwebtoken.sign(payload, TOKEN.key, { expiresIn: TOKEN.exp }) // 返回 token 给用户绑定 ctx.body = { token } } // 用户登录 async login(ctx) { // 校验参数 ctx.verifyParams({ username: { type: "string", required: true }, password: { type: "string", required: true } }) const { username, password } = ctx.request.body // 获取用户信息 const userinfo = await users.findOne({ username }) if (!userinfo) ctx.throw(401, '用户名或密码不正确') // 验证密码 if (!compare({ username, password }, userinfo.password)) ctx.throw(401, '用户名或密码不正确') // 生成token const payload = { id: userinfo._id, username } const token = jsonwebtoken.sign(payload, TOKEN.key, { expiresIn: TOKEN.exp }) // 返回 token 给用户绑定 ctx.body = { token } } // 用户列表 async list(ctx) { ctx.body = await users.find() } // 修改用户 async update(ctx) { ctx.body = await users.update(ctx.params.id, ctx.request.body) } // 删除用户 async remove(ctx) { ctx.body = await users.remove(ctx.params.id) } } module.exports = new Users() ``` ### 用户数据管理 ```js // app-http/service/users.js const { dateFormat } = require("../utils") const db = uniCloud.database() class Users { // 添加用户 {id: xxxx} async add(data) { const user = Object.assign({}, data, { create_time: dateFormat(new Date()), update_time: dateFormat(new Date()) }) return await db.collection("users").add(user) } // 删除用户 {affectedDoc:1,deleted:1} async remove(id) { return await db.collection("users").doc(id).remove() } // 修改用户 {affectedDoc:1,updated:1,upsertedId:xxxx} async update(id, data) { const user = Object.assign({}, data, { update_time: dateFormat(new Date()) }) return await db.collection("users").doc(id).update(user) } // 用户列表 {affectedDoc:20,data:[]} async find() { const res = await db.collection("users").field({ password: false }).get() return res.data } // 获取一个用户信息 async findOne(condition) { const res = await db.collection("users").where(condition).get() return res.data[0] } } module.exports = new Users() ``` ### 工具方法 ```js // app-http/utils/index.js const bcrypt = require('bcryptjs') // 加密密码 function encrypt(obj){ const salt = bcrypt.genSaltSync(10) return bcrypt.hashSync(JSON.stringify(obj), salt) } // 验证密码 function compare(obj, hash){ return bcrypt.compareSync(JSON.stringify(obj), hash) } /* 时间格式化 let date = new Date() dateFormat(date,"YYYY-MM-DD hh:mm") */ function dateFormat(date,fmt="YYYY-MM-DD hh:mm:ss") { let ret; const opt = { "Y+": date.getFullYear().toString(), // 年 "M+": (date.getMonth() + 1).toString(), // 月 "D+": date.getDate().toString(), // 日 "h+": date.getHours().toString(), // 时 "m+": date.getMinutes().toString(), // 分 "s+": date.getSeconds().toString() // 秒 // 有其他格式化字符需求可以继续添加,必须转化成字符串 }; for (let k in opt) { ret = new RegExp("(" + k + ")").exec(fmt); if (ret) { fmt = fmt.replace(ret[1], (ret[1].length == 1) ? (opt[k]) : (opt[k].padStart(ret[1].length, "0"))) }; }; return fmt; } module.exports = { dateFormat,encrypt,compare } ``` ## action请求 开发方式 ### 创建云函数项目 + 创建云函数项目 app-action + 初始化 app-action 项目, 安装依赖 ``` npm init -y npm install koa-router ``` + 设置公共函数 uni-koa 和 uni-id 依赖 ### 入口文件 ```js // app-action/index.js // 引入 uni-koa 创建 app 实例 const Koa = require("uni-koa") const app = new Koa() // 引入定义的路由集合 const routes = require("./routes") // 引入校验路由中间件, 校验 token const jwt=require("./middleware/jwt") app.use(jwt([ "login","register","logout" // 不需验证列表 ])) // 挂载路由 routes(app) // 启动监听请求并返回结果 exports.main = app.listen() ``` ### 中间件 校验路由 ```js // ./middleware/jwt.js const uniID = require("uni-id") // 传入不需要校验的路由数组 noNeedToken=[] module.exports = function(noNeedToken){ return async (ctx, next) => { if (!noNeedToken || noNeedToken.indexOf(ctx.event.action) === -1){ if (!ctx.event.uniIdToken) ctx.throw(403, "未携带token") const payload = await uniID.checkToken(ctx.event.uniIdToken) if (payload.code) ctx.throw(401, "校验未通过") ctx.state.user = payload if(payload.token) ctx.token = payload.token // 新 token 在config内配置了tokenExpiresThreshold的值 } await next() } } ``` ### 用户路由 + 自动路由挂载见前所述 ```js // app-action const Router = require("koa-router") const router = new Router() // 用户管理控制器 const users = require("../controller/users") // 这里约定均使用 post 方法标识请求 router.post("register",users.register) // 用户注册 router.post("login",users.login) // 用户登录 router.post("logout",users.logout) // 退出登录 router.post("getUserList", users.list) // 用户列表 router.post("updateUser", users.update) // 修改用户 router.post("removeUser", users.remove) // 删除用户 module.exports = router ``` ### 用户管理控制器 ```js // app-action/controller/user.js const uniID = require('uni-id') // 用户业务操作管理 const users = require("../service/users") class Users { // 用户注册 async register(ctx) { const { username, password } = ctx.params ctx.body = await uniID.register({username, password}) } // 用户登录 async login(ctx) { const { username, password } = ctx.params ctx.body = await uniID.login({username, password}) } // 退出登录 async logout(ctx) { const { username, password } = ctx.params ctx.body = await uniID.logout(ctx.uniIdToken) } // 用户列表 自定义方法 async list(ctx) { let res = await users.find() ctx.body = {code:0,list:res} } // 修改用户 一般只能自己修改自己 async update(ctx) { const userinfo = ctx.state.user ctx.body = await users.update(userinfo.id, ctx.params) } // 删除用户 async remove(ctx) { ctx.body = await users.remove(ctx.params.id) } } module.exports = new Users() ``` ### 用户数据操作 ```js // app-action/service/users.js const db = uniCloud.database() class Users { // 获取用户列表 async find() { // 移除密码 const res = await db.collection("uni-id-users").field({ password: false }).get() return res.data } // 删除用户 async remove(id){ return await await db.collection("uni-id-users").doc(id).remove() } // 修改用户 async update(id,data){ return await await db.collection("uni-id-users").doc(id).update(data) } } module.exports = new Users() ``` ## 前端数据请求 ### 封装请求方法 ```js // /api/request.js const BASE_URL = "app-action" // "你的云函数名称" function request(opts = {}) { if (!(opts.action ? opts.action : opts.url)) throw "请求方法必须有 " + opts.action ? "action." : "url." let data = opts.action ? { action: opts.action, params: opts.params || {} } : { method: opts.method || "get", url: opts.url, data: opts.data || {}, header: { // 自动绑定 token authorization: `Bearer ${uni.getStorageSync("uni_id_token") || ""}` } } return new Promise((reslove, reject) => { uniCloud.callFunction({ name: BASE_URL, data: data, success(res) { // 保存token,适用于自动更新 if (res.result && res.result.code === 0 && (res.result.token || (res.result.data && res .result.data.token))) { uni.setStorageSync('uni_id_token', res.result.token) } reslove(res.result) }, fail(err) { uni.showToast({ title: "请求失败" + err.message, icon: "none" }) reject() } }) }) } export default request ``` ### 使用 一般可以将封装好的请求挂载到 vue 上 ``` // main.js import request from "./api/request.js" Vue.prototype.$request = request ``` 使用时 this.$request 即可调用 #### http请求 开发使用 ```js let res = await this.$request({ method:"post", url:"/api/users/login", data:{ username:"admin", password:"123456" } }) ``` 返回信息 ```js // 正确返回 { code:0, data:{ token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." } } // 失败返回 { code:401, msg:"用户名或密码不正确" } ``` #### action请求 开发使用 ```js let res = await this.$request({ action:"login", params:{ username:"admin", password:"123456" } }) ``` 返回信息 ``` // 成功返回 { code: 0 message: "登录成功" msg: "登录成功" token: "eyJhbGciOiJ...." tokenExpired: 1624919764836 type: "login" uid: "60d7cafc3b7d35000175246c" userInfo: {_id: "60d7cafc3b7d35000175246c", username: "admin",…} username: "admin" } // 失败返回 { code: 10102, message: "密码错误", msg: "密码错误" } ``` ## 结语 通过使用,使用 uni-koa 框架,能更有效进行项目的组装,对于习惯 koa 开发者来说基本拿来就能上手,对于刚接触的 koa 开发的用户能更有效将 uniCloud 云开发项目更加健壮。