1 Star 7 Fork 1

丁学杰 / uni-koa

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
贡献代码
同步代码
取消
提示: 由于 Git 不支持空文件夾,创建文件夹后会生成空的 .keep 文件
Loading...
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

// 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()

配置文件

// 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

// 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()
}

路由

路由自动装载到应用

这里未考虑子目录,需要自行扩展

// 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())
    })
}

用户路由设置

// 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

用户管理控制器

// 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()

用户数据管理

// 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()

工具方法

// 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 依赖

入口文件

// 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()

中间件 校验路由

// ./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()
	}
}

用户路由

  • 自动路由挂载见前所述
// 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

用户管理控制器

// 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()

用户数据操作

// 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()

前端数据请求

封装请求方法

// /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请求 开发使用

let res = await this.$request({
	method:"post",
	url:"/api/users/login",
	data:{
		username:"admin",
		password:"123456"
	}
})

返回信息

// 正确返回
{
	code:0,
	data:{
		token: "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
	}
}

// 失败返回
{
	code:401,
	msg:"用户名或密码不正确"
}

action请求 开发使用

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 云开发项目更加健壮。

空文件

简介

暂无描述 展开 收起
JavaScript 等 3 种语言
取消

发行版

暂无发行版

贡献者

全部

近期动态

加载更多
不能加载更多了
1
https://gitee.com/dxj090/uni-koa.git
git@gitee.com:dxj090/uni-koa.git
dxj090
uni-koa
uni-koa
master

搜索帮助