├── .gitignore ├── README.md ├── example ├── managment-express │ ├── app.js │ ├── package.json │ └── public │ │ ├── html │ │ ├── favicon.ico │ │ └── index.html │ │ └── index.jpg └── mini-example │ ├── README.md │ ├── cloudfunctions │ └── service │ │ ├── .gitignore │ │ ├── controller.js │ │ ├── index.js │ │ ├── middleware │ │ └── errorHandle.js │ │ ├── model │ │ ├── index.js │ │ └── user.js │ │ ├── package.json │ │ └── router.js │ ├── miniprogram │ ├── app.js │ ├── app.json │ ├── app.wxss │ ├── pages │ │ ├── chooseLib │ │ │ ├── chooseLib.js │ │ │ ├── chooseLib.json │ │ │ ├── chooseLib.wxml │ │ │ └── chooseLib.wxss │ │ └── index │ │ │ ├── index.js │ │ │ ├── index.json │ │ │ ├── index.wxml │ │ │ ├── index.wxss │ │ │ └── user-unlogin.png │ ├── sitemap.json │ ├── style │ │ └── guide.wxss │ └── util │ │ └── request.js │ └── project.config.json ├── index.js ├── mockReqRes.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | package-lock.json 5 | npm-debug.log* 6 | yarn-debug.log* 7 | yarn-error.log* 8 | 9 | # Editor directories and files 10 | .idea 11 | *.suo 12 | *.ntvs* 13 | *.njsproj 14 | *.sln 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # wx-koa 2 | 基于koa框架的一个简单的库,使得微信小程序云开发跟后台koa开发保持一致,方便后期把微信小程序云开发的项目迁移到线下服务器 3 | 4 | ## 需求 5 | 目前小程序云开发提供了托管函数的云引擎,如果每个接口都各自写一个函数,那么对开发无疑是巨大的灾难的。不方便管理,代码共用也比较麻烦。所以能否有框架能像普通的后台开发一样处理前端的请求。 6 | 7 | ## 基于koa实现 8 | 1. koa是一个很有意思的web框架,实现很简单,核心代码大概有100多行。越简单的东西可玩性就越强,改造起来也方便。 9 | 2. 思路也很简单,就是mock一个request对象和response对象,其他都不改变,继承原有的application对象,重写了一些方法。支持了http协议的header, method,让云开发和普通的后台开发提供一致的体验和功能。 10 | 3. 理论上支持大部分koa插件,request对象mock不是很完整,只是简单赋值了一些属性数据。response对象的end方法重写了。所以对这2个对象有比较深入的依赖,那么可能会不支持。当然目前的已经可以满足大部分需求了。特别的需求可以尝试自己写插件。 11 | 4. 内置了koa-router,你可以直接使用它,仅仅继承了它,没有做任何改变,也许以后会用的到吧。 12 | 13 | ## 使用 14 | 1. 云函数直接安装wx-koa,npm i wx-koa -s 15 | 16 | 2. 云函数使用方式: 17 | ``` 18 | const Koa = require('wx-koa').WxKoa 19 | const app = new Koa() 20 | const Router = require('wx-koa').WxRoute 21 | const router = new Router() 22 | 23 | router.all('/*', (ctx, next) => { 24 | // 实现你的业务 25 | ctx.body = { headers: ctx.headers, header: ctx.header, requrl: ctx.request.url } 26 | }) 27 | app.use(router.routes()).use(router.allowedMethods()) 28 | // 云函数入口函数 29 | exports.main = async(event, context) => { 30 | // 把cloudId的对象放到req.body中,比如获取手机号 31 | event.data.weRunData = event.weRunData 32 | const wxContext = cloud.getWXContext() 33 | return app.service(event, wxContext) 34 | } 35 | ``` 36 | 3. 小程序端使用 37 | ``` 38 | // weRunData字段意思是 weRunData: wx.cloud.CloudID('xxx'),需要放在顶层字段 39 | wx.cloud.callFunction({ 40 | name: 'service', 41 | data: { url, data, method, headers, weRunData }, 42 | success: res => { 43 | const data = res.result 44 | console.info(data) 45 | } 46 | }) 47 | 48 | ``` 49 | 4. 具体使用方式可以参考example中的例子,新增数据库集合user,修改appid就可以运行使用。 50 | 51 | ## 其他 52 | 1. 迁移到线下服务器来,迁移成本主要是在数据库的访问层,貌似跟原生的mongodb操作方式有些不太一样。目前不知道它们是不是mongodb数据库,如果是的话,理论上可以使用自己的mongodb客户端模块(参考了腾讯云数据库),它们连接数据库也是通过secretid,secretkey,可以查看他们代码找到获取这2个钥匙的入口,然后放到自己mongodb客户端模块中,这里我没有去研究实践,小伙伴可以去搞搞哦,整好留言给我吧。 53 | 2. 既然是小程序,那么管理后台如何访问这些数据库数据?或者访问这些云函数?云开发提供了相关的api接口。example中managment-express项目是一个简单云函数代理访问的后台服务,可以满足你的需求哦。里面配置 ENV 和appid, appsecret,然后就可以访问到云函数接口,跟普通ajax保持请求一致。 54 | 55 | 附上最近做的小程序叫 即刻阅 ,支持订阅RSS,微博等信息,支持黑暗模式,很酷的,可以喵喵哦。 56 | 57 | ![即刻阅](https://duckfiles.oss-cn-qingdao.aliyuncs.com/eleduck/image/5f3695cf-ed32-43ba-a6da-d64f6f980272.png) 58 | 59 | -------------------------------------------------------------------------------- /example/managment-express/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express') 2 | const path = require('path') 3 | const app = express() 4 | const schedule = require('node-schedule'); 5 | const bodyParser = require('body-parser'); 6 | const request = require('request') 7 | 8 | const ENV = 'test-tp1cj' 9 | const appId = 'appId' 10 | const appSecret = 'appSecret' 11 | 12 | const url = `https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=${appId}&secret=${appSecret}` 13 | let accessToken = '32_vb123Lk37ra11rv-kicXEez1No75n3hG6KxEeH5zF6tyVGSXOBsilBCeFL94XtQJMBH7s5kyT2l9tJ8O4jlrDueYIz-ma7jbl_JMOhc73U6Ak8J7hXPxNJhRZIWQ_zvSuGhJh5c3MeRSLcAJAWXQ' 14 | // 定时任务 15 | async function getAccessToken() { 16 | try { 17 | request(url, (err, res) => { 18 | accessToken = JSON.parse(res.body).access_token 19 | console.log('scheduleJob accessToken ', new Date(), accessToken) 20 | }) 21 | } catch (err) { 22 | console.log(err) 23 | } 24 | } 25 | schedule.scheduleJob('0 */30 * * * *', getAccessToken) 26 | getAccessToken() 27 | // 设置静态资源路径 28 | app.use(express.static(path.join(__dirname, 'public/html'))) 29 | 30 | // 解析 application/json 31 | app.use(bodyParser.json()) 32 | // 解析 application/x-www-form-urlencoded 33 | app.use(bodyParser.urlencoded()) 34 | // 解决跨域 35 | app.all('*', function (req, res, next) { 36 | res.header('Access-Control-Allow-Origin', '*'); 37 | //Access-Control-Allow-Headers ,可根据浏览器的F12查看,把对应的粘贴在这里就行 38 | res.header('Access-Control-Allow-Headers', '*'); 39 | res.header('Access-Control-Allow-Methods', '*'); 40 | next(); 41 | }); 42 | app.all('*', function (req, res, next) { 43 | if(req.method === 'OPTIONS'){ 44 | res.send() 45 | }else{ 46 | next() 47 | } 48 | }); 49 | // Routes 50 | const wxminiUrl = `https://api.weixin.qq.com/tcb/invokecloudfunction?access_token=ACCESS_TOKEN&env=${ENV}&name=service` 51 | app.post(`/*`, (req, res) => { 52 | // 获取请求路径,请求方法,请求参数,然后请求 小程序云函数 53 | let miniUrl = wxminiUrl.replace('ACCESS_TOKEN', accessToken) 54 | // 路径要去掉跟路径 req.path, 不能在app.use 里面用,只能在get, post 55 | const data = { 56 | url: req.originalUrl, 57 | method: req.method, 58 | headers: req.headers, 59 | data: req.body 60 | } 61 | console.log('req', data.url, data.data) 62 | request.post({ 63 | url: miniUrl, 64 | json: data 65 | }, (err, result) => { 66 | console.log('res', result.body) 67 | try { 68 | const resultJson = JSON.parse(result.body.resp_data) 69 | res.send(resultJson) 70 | } catch (err) { 71 | console.log('res', result.body) 72 | } 73 | }) 74 | }) 75 | 76 | app.get('/*', function(req, res, err) { 77 | res.redirect('/') 78 | }) 79 | // Error handler 80 | app.use(function(req, res, err) { 81 | console.error(err) 82 | res.status(500).send('Internal Serverless Error') 83 | }) 84 | 85 | module.exports = app 86 | const port = 9110 87 | app.listen(port); 88 | console.log('port:', port) -------------------------------------------------------------------------------- /example/managment-express/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "express-demo", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "app.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "yugasun", 10 | "license": "MIT", 11 | "dependencies": { 12 | "body-parser": "^1.19.0", 13 | "express": "^4.17.1", 14 | "node-schedule": "^1.3.2", 15 | "request": "^2.88.2" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /example/managment-express/public/html/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/traceless/wx-koa/618fbc36f9b0fb6e51a08ca08bc8f4b3481a6369/example/managment-express/public/html/favicon.ico -------------------------------------------------------------------------------- /example/managment-express/public/html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | ZXNEST Admin 11 | 12 | 13 | 14 | 欢迎来到后台管理,请使用postman 测试你的云函数接口吧 15 | 16 | 17 | -------------------------------------------------------------------------------- /example/managment-express/public/index.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/traceless/wx-koa/618fbc36f9b0fb6e51a08ca08bc8f4b3481a6369/example/managment-express/public/index.jpg -------------------------------------------------------------------------------- /example/mini-example/README.md: -------------------------------------------------------------------------------- 1 | # 云开发 quickstart 2 | 3 | 这是云开发的快速启动指引,其中演示了如何上手使用云开发的三大基础能力: 4 | 5 | - 数据库:一个既可在小程序前端操作,也能在云函数中读写的 JSON 文档型数据库 6 | - 文件存储:在小程序前端直接上传/下载云端文件,在云开发控制台可视化管理 7 | - 云函数:在云端运行的代码,微信私有协议天然鉴权,开发者只需编写业务逻辑代码 8 | 9 | ## 参考文档 10 | 11 | - [云开发文档](https://developers.weixin.qq.com/miniprogram/dev/wxcloud/basis/getting-started.html) 12 | 13 | -------------------------------------------------------------------------------- /example/mini-example/cloudfunctions/service/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Editor directories and files 9 | .idea 10 | *.suo 11 | *.ntvs* 12 | *.njsproj 13 | *.sln 14 | -------------------------------------------------------------------------------- /example/mini-example/cloudfunctions/service/controller.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const exp = 1000 * 60 * 60 * 12 3 | const crypto = require('crypto') 4 | const { User } = require('./model') 5 | 6 | class UserContoller { 7 | // 登录 8 | async login(ctx, next) { 9 | const { OPENID } = ctx 10 | const expire = new Date().getTime() + exp 11 | const token = crypto.createHash('md5').update(ctx.OPENID + expire).digest('hex') 12 | // 获取用户信息 13 | let res = await ctx.db.collection('user').where({ openId: OPENID }).get() 14 | let user = null 15 | if (res.data.length > 0) { 16 | user = res.data[0] 17 | } 18 | // 添加用户 19 | if (user == null) { 20 | const { openId, nickName, avatarUrl, city } = ctx.req.body.weRunData.data 21 | user = new User(openId, nickName, avatarUrl, city) 22 | await ctx.db.collection('user').add({ data: user }) 23 | } 24 | ctx.body = { user, expire, token } 25 | } 26 | 27 | // 获取手机号 28 | async getUserMobile(ctx, next) { 29 | ctx.body = ctx.req.body.weRunData 30 | } 31 | } 32 | 33 | module.exports = { userContoller: new UserContoller() } 34 | -------------------------------------------------------------------------------- /example/mini-example/cloudfunctions/service/index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // 云函数入口文件 4 | const cloud = require('wx-server-sdk') 5 | cloud.init({ 6 | env: cloud.DYNAMIC_CURRENT_ENV 7 | }) 8 | const Koa = require('wx-koa').WxKoa 9 | const router = require('./router') 10 | const errorHandle = require('./middleware/errorHandle') 11 | 12 | const db = cloud.database() 13 | const app = new Koa() 14 | 15 | // 全局异常处理 16 | app.use(errorHandle) 17 | // mock openid 18 | app.use(async(ctx, next) => { 19 | const { OPENID } = ctx 20 | // 写入自己的openid测试 21 | if (!OPENID) { 22 | ctx.OPENID = 'ohdEC5V6TmxsTyP12333233' 23 | console.info(' test openid: ', ctx.OPENID) 24 | } 25 | // 初始化db 26 | ctx.db = db 27 | await next() 28 | // 封装结果参数,body 三种数据结构,1、字符串,2、直接返回结构,3、自定义放回信息 29 | if (typeof (ctx.body) === 'object' && ctx.body.success === undefined) { 30 | ctx.body = { 31 | success: true, 32 | message: '操作成功', 33 | code: 200, 34 | data: ctx.body 35 | } 36 | } 37 | if (typeof (ctx.body) === 'string') { 38 | ctx.body = { 39 | success: true, 40 | message: ctx.body, 41 | code: 200 42 | } 43 | } 44 | }) 45 | 46 | app.use(router.routes()).use(router.allowedMethods()) 47 | // 云函数入口函数 48 | exports.main = async(event, context) => { 49 | // 把cloudId的对象放到req.body中,比如获取手机号 50 | event.data.weRunData = event.weRunData 51 | const wxContext = cloud.getWXContext() 52 | return app.service(event, wxContext, (req, res) =>{ 53 | // 可以改变req,res 对象相关属性,这里是处理之前的。 54 | req.headers.field 55 | }) 56 | } 57 | 58 | let event = {} 59 | event = { 60 | 'url': '/user/createOrder2', 61 | 'data': { 62 | 'token': 'ec014bb1ba55e6435ae074f6b97b9f41', 63 | 'expire': 1586690832341, 64 | 'code': '34344', 65 | 'mobile': '123455553', 66 | 'elevatorName': 'dd' 67 | } 68 | } 69 | // 本地直接调用简单的测试。 70 | exports.main(event).then(data => { 71 | console.info('data:', data) 72 | }) 73 | -------------------------------------------------------------------------------- /example/mini-example/cloudfunctions/service/middleware/errorHandle.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const cloud = require('wx-server-sdk') 3 | module.exports = async function(ctx, next) { 4 | const isPro = ~cloud.DYNAMIC_CURRENT_ENV.toString().indexOf('pro') 5 | try { 6 | await next() 7 | // 参数转换, 转换成自己的数据格式 8 | if (typeof (ctx.body) === 'object') { 9 | const { success, message, code, data } = ctx.body 10 | const body = { 11 | success: success || false, 12 | message, 13 | code: code || 200, 14 | data 15 | } 16 | ctx.body = body 17 | } 18 | if (ctx.status === 404) { 19 | ctx.throw(404, '请求资源未找到!') 20 | } 21 | } catch (err) { 22 | // 所有的异常都在 app 上触发一个 error 事件,框架会记录一条错误日志 23 | // app.emit('error', err, this); 24 | const status = err.status || 500 25 | // 生产环境时 500 错误的详细错误内容不返回给客户端,因为可能包含敏感信息 26 | const error = status === 500 && isPro 27 | ? 'Internal Server Error' 28 | : err.message 29 | // 从 error 对象上读出各个属性,设置到响应中 30 | ctx.body = { 31 | success: false, 32 | message: error, 33 | code: status, // 服务端自身的处理逻辑错误(包含框架错误500 及 自定义业务逻辑错误533开始 ) 客户端请求参数导致的错误(4xx开始),设置不同的状态码 34 | data: null 35 | } 36 | // 406 是能让用户看到的错误,参数校验失败也不能让用户看到(一般不存在参数校验失败) 37 | if (status === '403' || status === '406') { 38 | ctx.body.message = error 39 | } 40 | ctx.status = 200 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /example/mini-example/cloudfunctions/service/model/index.js: -------------------------------------------------------------------------------- 1 | exports.User = require('./user') 2 | -------------------------------------------------------------------------------- /example/mini-example/cloudfunctions/service/model/user.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | class User { 4 | constructor(openId, nickName, avatarUrl, city) { 5 | this.openId = openId 6 | this.nickName = nickName 7 | this.avatarUrl = avatarUrl 8 | this.city = city 9 | this.createTime = new Date() 10 | } 11 | } 12 | module.exports = User -------------------------------------------------------------------------------- /example/mini-example/cloudfunctions/service/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "service", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "dependencies": { 12 | "extend": "^3.0.2", 13 | "koa": "^2.11.0", 14 | "koa-router": "^8.0.8", 15 | "tencentcloud-sdk-nodejs": "^3.0.165", 16 | "wx-koa": "^1.0.1", 17 | "wx-server-sdk": "latest" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /example/mini-example/cloudfunctions/service/router.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const Router = require('wx-koa').WxRoute 4 | const router = new Router() 5 | const crypto = require('crypto') 6 | const { userContoller } = require('./controller') 7 | 8 | // 拦截登录 9 | router.all('/user/*', async(ctx, next) => { 10 | const { token, expire } = ctx.request.headers 11 | if (token && expire > new Date().getTime()) { 12 | const openid = ctx.OPENID 13 | const md5 = crypto.createHash('md5').update(openid + expire).digest('hex') 14 | if (md5 === token) { 15 | // 把用户信息加到上下文中, 正常是通过token从redis,获取用户信息的。这里使用openid 直接查询数据库去得到 16 | const res = await ctx.db.collection('user').where({ openid }).get() 17 | let user = res.data[0] 18 | ctx.user = user 19 | await next() 20 | return 21 | } 22 | } 23 | ctx.body = { code: 1002, message: '当前用户未登录', success: false } 24 | }) 25 | 26 | // 用户登录, jwt鉴权 12小时 27 | router.post('/wxApi/login', userContoller.login) 28 | 29 | // 检查是否登录 30 | router.all('/user/checkLogin', async(ctx, next) => { 31 | ctx.body = '已经登录' 32 | }) 33 | 34 | // 获取手机号 35 | router.post('/user/getUserMobile', userContoller.getUserMobile) 36 | 37 | // 捕抓最后的路径,error handle 实际可以处理 38 | router.all('/*', (ctx, next) => { 39 | // ctx.router available 40 | ctx.body = { headers: ctx.headers, header: ctx.header, requrl: ctx.request.url } 41 | }) 42 | 43 | module.exports = router 44 | -------------------------------------------------------------------------------- /example/mini-example/miniprogram/app.js: -------------------------------------------------------------------------------- 1 | //app.js 2 | App({ 3 | onLaunch: function () { 4 | 5 | if (!wx.cloud) { 6 | console.error('请使用 2.2.3 或以上的基础库以使用云能力') 7 | } else { 8 | wx.cloud.init({ 9 | // env 参数说明: 10 | // env 参数决定接下来小程序发起的云开发调用(wx.cloud.xxx)会默认请求到哪个云环境的资源 11 | // 此处请填入环境 ID, 环境 ID 可打开云控制台查看 12 | // 如不填则使用默认环境(第一个创建的环境) 13 | // env: 'mytest-8hgs5', 14 | traceUser: true, 15 | }) 16 | } 17 | 18 | this.globalData = {} 19 | } 20 | }) 21 | -------------------------------------------------------------------------------- /example/mini-example/miniprogram/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "pages": [ 3 | "pages/index/index", 4 | "pages/chooseLib/chooseLib" 5 | ], 6 | "window": { 7 | "backgroundColor": "#F6F6F6", 8 | "backgroundTextStyle": "light", 9 | "navigationBarBackgroundColor": "#F6F6F6", 10 | "navigationBarTitleText": "云开发 QuickStart", 11 | "navigationBarTextStyle": "black" 12 | }, 13 | "sitemapLocation": "sitemap.json", 14 | "style": "v2" 15 | } -------------------------------------------------------------------------------- /example/mini-example/miniprogram/app.wxss: -------------------------------------------------------------------------------- 1 | /**app.wxss**/ 2 | .container { 3 | display: flex; 4 | flex-direction: column; 5 | align-items: center; 6 | box-sizing: border-box; 7 | } 8 | 9 | button { 10 | background: initial; 11 | } 12 | 13 | button:focus{ 14 | outline: 0; 15 | } 16 | 17 | button::after{ 18 | border: none; 19 | } 20 | 21 | 22 | page { 23 | background: #f6f6f6; 24 | display: flex; 25 | flex-direction: column; 26 | justify-content: flex-start; 27 | } 28 | 29 | .userinfo, .uploader, .tunnel { 30 | margin-top: 40rpx; 31 | height: 140rpx; 32 | width: 100%; 33 | background: #fff; 34 | border: 1px solid rgba(0, 0, 0, 0.1); 35 | border-left: none; 36 | border-right: none; 37 | display: flex; 38 | flex-direction: row; 39 | align-items: center; 40 | transition: all 300ms ease; 41 | } 42 | 43 | .userinfo-avatar { 44 | width: 100rpx; 45 | height: 100rpx; 46 | margin: 20rpx; 47 | border-radius: 50%; 48 | background-size: cover; 49 | background-color: white; 50 | } 51 | 52 | .userinfo-avatar:after { 53 | border: none; 54 | } 55 | 56 | .userinfo-nickname { 57 | font-size: 32rpx; 58 | color: #007aff; 59 | background-color: white; 60 | background-size: cover; 61 | } 62 | 63 | .userinfo-nickname::after { 64 | border: none; 65 | } 66 | 67 | .uploader, .tunnel { 68 | height: auto; 69 | padding: 0 0 0 40rpx; 70 | flex-direction: column; 71 | align-items: flex-start; 72 | box-sizing: border-box; 73 | } 74 | 75 | .uploader-text, .tunnel-text { 76 | width: 100%; 77 | line-height: 52px; 78 | font-size: 34rpx; 79 | color: #007aff; 80 | } 81 | 82 | .uploader-container { 83 | width: 100%; 84 | height: 400rpx; 85 | padding: 20rpx 20rpx 20rpx 0; 86 | display: flex; 87 | align-content: center; 88 | justify-content: center; 89 | box-sizing: border-box; 90 | border-top: 1px solid rgba(0, 0, 0, 0.1); 91 | } 92 | 93 | .uploader-image { 94 | width: 100%; 95 | height: 360rpx; 96 | } 97 | 98 | .tunnel { 99 | padding: 0 0 0 40rpx; 100 | } 101 | 102 | .tunnel-text { 103 | position: relative; 104 | color: #222; 105 | display: flex; 106 | flex-direction: row; 107 | align-content: center; 108 | justify-content: space-between; 109 | box-sizing: border-box; 110 | border-top: 1px solid rgba(0, 0, 0, 0.1); 111 | } 112 | 113 | .tunnel-text:first-child { 114 | border-top: none; 115 | } 116 | 117 | .tunnel-switch { 118 | position: absolute; 119 | right: 20rpx; 120 | top: -2rpx; 121 | } 122 | 123 | .disable { 124 | color: #888; 125 | } 126 | 127 | .service { 128 | position: fixed; 129 | right: 40rpx; 130 | bottom: 40rpx; 131 | width: 140rpx; 132 | height: 140rpx; 133 | border-radius: 50%; 134 | background: linear-gradient(#007aff, #0063ce); 135 | box-shadow: 0 5px 10px rgba(0, 0, 0, 0.3); 136 | display: flex; 137 | align-content: center; 138 | justify-content: center; 139 | transition: all 300ms ease; 140 | } 141 | 142 | .service-button { 143 | position: absolute; 144 | top: 40rpx; 145 | } 146 | 147 | .service:active { 148 | box-shadow: none; 149 | } 150 | 151 | .request-text { 152 | padding: 20rpx 0; 153 | font-size: 24rpx; 154 | line-height: 36rpx; 155 | word-break: break-all; 156 | } 157 | -------------------------------------------------------------------------------- /example/mini-example/miniprogram/pages/chooseLib/chooseLib.js: -------------------------------------------------------------------------------- 1 | 2 | // pages/chooseLib/chooseLib.js 3 | Page({ 4 | 5 | /** 6 | * 页面的初始数据 7 | */ 8 | data: { 9 | 10 | }, 11 | 12 | /** 13 | * 生命周期函数--监听页面加载 14 | */ 15 | onLoad: function (options) { 16 | 17 | }, 18 | 19 | /** 20 | * 生命周期函数--监听页面初次渲染完成 21 | */ 22 | onReady: function () { 23 | 24 | }, 25 | 26 | /** 27 | * 生命周期函数--监听页面显示 28 | */ 29 | onShow: function () { 30 | 31 | }, 32 | 33 | /** 34 | * 生命周期函数--监听页面隐藏 35 | */ 36 | onHide: function () { 37 | 38 | }, 39 | 40 | /** 41 | * 生命周期函数--监听页面卸载 42 | */ 43 | onUnload: function () { 44 | 45 | }, 46 | 47 | /** 48 | * 页面相关事件处理函数--监听用户下拉动作 49 | */ 50 | onPullDownRefresh: function () { 51 | 52 | }, 53 | 54 | /** 55 | * 页面上拉触底事件的处理函数 56 | */ 57 | onReachBottom: function () { 58 | 59 | }, 60 | 61 | /** 62 | * 用户点击右上角分享 63 | */ 64 | onShareAppMessage: function () { 65 | 66 | } 67 | }) -------------------------------------------------------------------------------- /example/mini-example/miniprogram/pages/chooseLib/chooseLib.json: -------------------------------------------------------------------------------- 1 | { 2 | "navigationBarTitleText": "选择基础库", 3 | "usingComponents": {} 4 | } -------------------------------------------------------------------------------- /example/mini-example/miniprogram/pages/chooseLib/chooseLib.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 初始化失败 7 | 8 | 9 | 请使用 2.2.3 或以上的基础库以使用云能力 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /example/mini-example/miniprogram/pages/chooseLib/chooseLib.wxss: -------------------------------------------------------------------------------- 1 | /* pages/chooseLib/chooseLib.wxss */ 2 | 3 | @import "../../style/guide.wxss"; 4 | 5 | .black { 6 | color: black; 7 | } -------------------------------------------------------------------------------- /example/mini-example/miniprogram/pages/index/index.js: -------------------------------------------------------------------------------- 1 | //index.js 2 | const app = getApp() 3 | const request = require('../../util/request.js') 4 | 5 | Page({ 6 | data: { 7 | avatarUrl: './user-unlogin.png', 8 | nickName: '', 9 | userInfo: {}, 10 | logged: false, 11 | takeSession: false, 12 | requestResult: '' 13 | }, 14 | 15 | onLoad: function() { 16 | if (!wx.cloud) { 17 | wx.redirectTo({ 18 | url: '../chooseLib/chooseLib', 19 | }) 20 | return 21 | } 22 | 23 | }, 24 | 25 | getMobile: function(ev) { 26 | const weRunData = wx.cloud.CloudID(ev.detail.cloudID) 27 | request.requestCloud({ 28 | url: '/user/getUserMobile', 29 | weRunData 30 | }).then(res =>{ 31 | console.log('res', res) 32 | wx.showToast({ 33 | title: '手机号:' + res.data.data.phoneNumber, 34 | icon: 'none', 35 | duration: 4000 36 | }) 37 | }).catch(err =>{ 38 | wx.showToast({ 39 | title: err.message, 40 | icon: 'none', 41 | duration: 2000 42 | }) 43 | }) 44 | }, 45 | 46 | onGetUserInfo: function(ev) { 47 | const weRunData = wx.cloud.CloudID(ev.detail.cloudID) 48 | request.requestCloud({ 49 | url: '/wxApi/login', 50 | weRunData 51 | }).then(res => { 52 | console.info('login res', res) 53 | wx.showToast({ 54 | title: '登录成功!', 55 | icon: 'none', 56 | duration: 2000 57 | }) 58 | this.setData({ 59 | logged: true, 60 | avatarUrl: res.data.user.avatarUrl, 61 | userInfo: res.data.user, 62 | nickName: res.data.user.nickName 63 | }) 64 | app.globalData.token = res.data.token 65 | app.globalData.expire = res.data.expire 66 | }) 67 | 68 | }, 69 | 70 | }) -------------------------------------------------------------------------------- /example/mini-example/miniprogram/pages/index/index.json: -------------------------------------------------------------------------------- 1 | { 2 | "usingComponents": {} 3 | } -------------------------------------------------------------------------------- /example/mini-example/miniprogram/pages/index/index.wxml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | {{ nickName }} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /example/mini-example/miniprogram/pages/index/index.wxss: -------------------------------------------------------------------------------- 1 | /**index.wxss**/ 2 | 3 | page { 4 | background: #fff; 5 | display: flex; 6 | flex-direction: column; 7 | justify-content: flex-start; 8 | } 9 | 10 | .userinfo, .uploader, .tunnel { 11 | margin-top: 40rpx; 12 | height: 140rpx; 13 | width: 100%; 14 | background: #fff; 15 | border: 1px solid rgba(0, 0, 0, 0.1); 16 | border-left: none; 17 | border-right: none; 18 | display: flex; 19 | flex-direction: row; 20 | align-items: center; 21 | transition: all 300ms ease; 22 | } 23 | 24 | .userinfo { 25 | padding-left: 120rpx; 26 | } 27 | 28 | .userinfo-avatar { 29 | width: 100rpx; 30 | height: 100rpx; 31 | margin: 20rpx; 32 | border-radius: 50%; 33 | background-size: cover; 34 | background-color: white; 35 | } 36 | 37 | .userinfo-avatar[size] { 38 | width: 100rpx; 39 | } 40 | 41 | 42 | .userinfo-avatar:after { 43 | border: none; 44 | } 45 | 46 | .userinfo-nickname { 47 | font-size: 32rpx; 48 | color: #007aff; 49 | background-color: white; 50 | background-size: cover; 51 | text-align: left; 52 | padding-left: 0; 53 | margin-left: 10px; 54 | } 55 | 56 | .userinfo-nickname::after { 57 | border: none; 58 | } 59 | 60 | .userinfo-nickname-wrapper { 61 | flex: 1; 62 | } 63 | 64 | .uploader, .tunnel { 65 | height: auto; 66 | padding: 0 0 0 40rpx; 67 | flex-direction: column; 68 | align-items: flex-start; 69 | box-sizing: border-box; 70 | } 71 | 72 | .uploader-text, .tunnel-text { 73 | width: 100%; 74 | line-height: 52px; 75 | font-size: 34rpx; 76 | color: #007aff; 77 | } 78 | 79 | .uploader-container { 80 | width: 100%; 81 | height: 400rpx; 82 | padding: 20rpx 20rpx 20rpx 0; 83 | display: flex; 84 | align-content: center; 85 | justify-content: center; 86 | box-sizing: border-box; 87 | border-top: 1px solid rgba(0, 0, 0, 0.1); 88 | } 89 | 90 | .uploader-image { 91 | width: 100%; 92 | height: 360rpx; 93 | } 94 | 95 | .tunnel { 96 | padding: 0 0 0 40rpx; 97 | } 98 | 99 | .tunnel-text { 100 | position: relative; 101 | color: #222; 102 | display: flex; 103 | flex-direction: row; 104 | align-content: center; 105 | justify-content: space-between; 106 | box-sizing: border-box; 107 | border-top: 1px solid rgba(0, 0, 0, 0.1); 108 | } 109 | 110 | .tunnel-text:first-child { 111 | border-top: none; 112 | } 113 | 114 | .tunnel-switch { 115 | position: absolute; 116 | right: 20rpx; 117 | top: -2rpx; 118 | } 119 | 120 | .disable { 121 | color: #888; 122 | } 123 | 124 | .service { 125 | position: fixed; 126 | right: 40rpx; 127 | bottom: 40rpx; 128 | width: 140rpx; 129 | height: 140rpx; 130 | border-radius: 50%; 131 | background: linear-gradient(#007aff, #0063ce); 132 | box-shadow: 0 5px 10px rgba(0, 0, 0, 0.3); 133 | display: flex; 134 | align-content: center; 135 | justify-content: center; 136 | transition: all 300ms ease; 137 | } 138 | 139 | .service-button { 140 | position: absolute; 141 | top: 40rpx; 142 | } 143 | 144 | .service:active { 145 | box-shadow: none; 146 | } 147 | 148 | .request-text { 149 | padding: 20rpx 0; 150 | font-size: 24rpx; 151 | line-height: 36rpx; 152 | word-break: break-all; 153 | } 154 | -------------------------------------------------------------------------------- /example/mini-example/miniprogram/pages/index/user-unlogin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/traceless/wx-koa/618fbc36f9b0fb6e51a08ca08bc8f4b3481a6369/example/mini-example/miniprogram/pages/index/user-unlogin.png -------------------------------------------------------------------------------- /example/mini-example/miniprogram/sitemap.json: -------------------------------------------------------------------------------- 1 | { 2 | "desc": "关于本文件的更多信息,请参考文档 https://developers.weixin.qq.com/miniprogram/dev/framework/sitemap.html", 3 | "rules": [{ 4 | "action": "allow", 5 | "page": "*" 6 | }] 7 | } -------------------------------------------------------------------------------- /example/mini-example/miniprogram/style/guide.wxss: -------------------------------------------------------------------------------- 1 | page { 2 | background: #f6f6f6; 3 | display: flex; 4 | flex-direction: column; 5 | justify-content: flex-start; 6 | } 7 | 8 | .list { 9 | margin-top: 40rpx; 10 | height: auto; 11 | width: 100%; 12 | background: #fff; 13 | padding: 0 40rpx; 14 | border: 1px solid rgba(0, 0, 0, 0.1); 15 | border-left: none; 16 | border-right: none; 17 | transition: all 300ms ease; 18 | display: flex; 19 | flex-direction: column; 20 | align-items: stretch; 21 | box-sizing: border-box; 22 | } 23 | 24 | .list-item { 25 | width: 100%; 26 | padding: 0; 27 | line-height: 104rpx; 28 | font-size: 34rpx; 29 | color: #007aff; 30 | border-top: 1px solid rgba(0, 0, 0, 0.1); 31 | display: flex; 32 | flex-direction: row; 33 | align-content: center; 34 | justify-content: space-between; 35 | box-sizing: border-box; 36 | } 37 | 38 | .list-item:first-child { 39 | border-top: none; 40 | } 41 | 42 | .list-item image { 43 | max-width: 100%; 44 | max-height: 20vh; 45 | margin: 20rpx 0; 46 | } 47 | 48 | .request-text { 49 | color: #222; 50 | padding: 20rpx 0; 51 | font-size: 24rpx; 52 | line-height: 36rpx; 53 | word-break: break-all; 54 | } 55 | 56 | .guide { 57 | width: 100%; 58 | padding: 40rpx; 59 | box-sizing: border-box; 60 | display: flex; 61 | flex-direction: column; 62 | } 63 | 64 | .guide .headline { 65 | font-size: 34rpx; 66 | font-weight: bold; 67 | color: #555; 68 | line-height: 40rpx; 69 | } 70 | 71 | .guide .p { 72 | margin-top: 20rpx; 73 | font-size: 28rpx; 74 | line-height: 36rpx; 75 | color: #666; 76 | } 77 | 78 | .guide .code { 79 | margin-top: 20rpx; 80 | font-size: 28rpx; 81 | line-height: 36rpx; 82 | color: #666; 83 | background: white; 84 | white-space: pre; 85 | } 86 | 87 | .guide .code-dark { 88 | margin-top: 20rpx; 89 | background: rgba(0, 0, 0, 0.8); 90 | padding: 20rpx; 91 | font-size: 28rpx; 92 | line-height: 36rpx; 93 | border-radius: 6rpx; 94 | color: #fff; 95 | white-space: pre 96 | } 97 | 98 | .guide image { 99 | max-width: 100%; 100 | } 101 | 102 | .guide .image1 { 103 | margin-top: 20rpx; 104 | max-width: 100%; 105 | width: 356px; 106 | height: 47px; 107 | } 108 | 109 | .guide .image2 { 110 | margin-top: 20rpx; 111 | width: 264px; 112 | height: 100px; 113 | } 114 | 115 | .guide .flat-image { 116 | height: 100px; 117 | } 118 | 119 | .guide .code-image { 120 | max-width: 100%; 121 | } 122 | 123 | .guide .copyBtn { 124 | width: 180rpx; 125 | font-size: 20rpx; 126 | margin-top: 16rpx; 127 | margin-left: 0; 128 | } 129 | 130 | .guide .nav { 131 | margin-top: 50rpx; 132 | display: flex; 133 | flex-direction: row; 134 | align-content: space-between; 135 | } 136 | 137 | .guide .nav .prev { 138 | margin-left: unset; 139 | } 140 | 141 | .guide .nav .next { 142 | margin-right: unset; 143 | } 144 | 145 | -------------------------------------------------------------------------------- /example/mini-example/miniprogram/util/request.js: -------------------------------------------------------------------------------- 1 | let serverPath = 'https://mini.youcompany.com' 2 | let serverPathTrial = 'https://mini.youcompany.com' 3 | let serverPathDev = 'https://mini.youcompany.com' 4 | const app = getApp() 5 | // eslint-disable-next-line no-undef 6 | const env = __wxConfig.envVersion 7 | // 请求自己服务器 8 | export function request({ url, data, method }) { 9 | // 默认正式环境,如果非正式环境,则切换 10 | if (env === 'develop') { 11 | serverPath = serverPathDev 12 | } 13 | if (env === 'trial') { 14 | serverPath = serverPathTrial 15 | } 16 | return new Promise((resolve, reject) => { 17 | wx.request({ 18 | url: serverPath + url, 19 | data, 20 | method: method || 'POST', 21 | header: { 22 | 'content-type': 'application/json', 23 | 'X-Token': app.globalData.token 24 | }, 25 | // config.headers['X-Requested-With'] = 'XMLHttpRequest' 26 | success(res) { 27 | if (res.statusCode !== 200) { 28 | reject(new Error('请求异常')) 29 | return 30 | } 31 | const data = res.data 32 | if (data.code !== 200) { 33 | // 未登录 34 | if (data.code === 1002) { 35 | // 重新登录 36 | reject(new Error('刚登录掉啦,请重试!')) 37 | return 38 | } 39 | // 其他异常码的处理 40 | reject(new Error(data.message)) 41 | return 42 | } 43 | resolve(data) 44 | }, 45 | fail(ret) { 46 | reject(ret) 47 | } 48 | }) 49 | }) 50 | } 51 | // 请求云服务, header 遵循wx.request 的规范,实际请求通常是headers 52 | export function requestCloud({ url, data = {}, method = 'post', header={}, weRunData }) { 53 | // data.token = app.globalData.token 54 | // data.expire = app.globalData.expire 55 | header.token = app.globalData.token 56 | header.expire = app.globalData.expire 57 | return new Promise((resolve, reject) => { 58 | wx.cloud.callFunction({ 59 | name: 'service', 60 | data: { url, data, method, headers: header, weRunData }, 61 | success: result => { 62 | const data = result.result 63 | if (data.code !== 200) { 64 | // 未登录 65 | if (data.code === 1002) { 66 | // 重新登录 67 | reject(new Error('你的登录掉啦,请先登录!')) 68 | return 69 | } 70 | // 其他异常码的处理 71 | reject(new Error(data.message)) 72 | return 73 | } 74 | resolve(data) 75 | }, 76 | fail: err => { 77 | // new Error(res.data.message) 78 | console.error(err) 79 | reject(new Error('调用异常')) 80 | } 81 | }) 82 | }) 83 | } 84 | -------------------------------------------------------------------------------- /example/mini-example/project.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "miniprogramRoot": "miniprogram/", 3 | "cloudfunctionRoot": "cloudfunctions/", 4 | "setting": { 5 | "urlCheck": true, 6 | "es6": true, 7 | "postcss": true, 8 | "preloadBackgroundData": false, 9 | "minified": true, 10 | "newFeature": true, 11 | "autoAudits": false, 12 | "coverView": true, 13 | "showShadowRootInWxmlPanel": true, 14 | "scopeDataCheck": false, 15 | "enhance": true, 16 | "useCompilerModule": false 17 | }, 18 | "appid": "wxb61fcfbb4012b53d", 19 | "projectname": "miniexample", 20 | "libVersion": "2.8.1", 21 | "simulatorType": "wechat", 22 | "simulatorPluginLibVersion": {}, 23 | "cloudfunctionTemplateRoot": "cloudfunctionTemplate", 24 | "condition": { 25 | "search": { 26 | "current": -1, 27 | "list": [] 28 | }, 29 | "conversation": { 30 | "current": -1, 31 | "list": [] 32 | }, 33 | "plugin": { 34 | "current": -1, 35 | "list": [] 36 | }, 37 | "game": { 38 | "list": [] 39 | }, 40 | "miniprogram": { 41 | "current": 0, 42 | "list": [ 43 | { 44 | "id": -1, 45 | "name": "db guide", 46 | "pathName": "pages/databaseGuide/databaseGuide" 47 | } 48 | ] 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | const Koa = require('koa') 3 | const mockReqRes = require('./mockReqRes') 4 | const Route = require('koa-router') 5 | const onFinished = require('on-finished') 6 | 7 | class WxKoa extends Koa { 8 | /** 9 | * 初始换实例 10 | * 11 | * @api public 12 | */ 13 | 14 | /** 15 | * 16 | * @param {object} [options] Application options 17 | * @param {string} [options.env='development'] Environment 18 | * @param {string[]} [options.keys] Signed cookie keys 19 | * @param {boolean} [options.proxy] Trust proxy headers 20 | * @param {number} [options.subdomainOffset] Subdomain offset 21 | * @param {boolean} [options.proxyIpHeader] proxy ip header, default to X-Forwarded-For 22 | * @param {boolean} [options.maxIpsCount] max ips read from proxy ip header, default to 0 (means infinity) 23 | * 24 | */ 25 | 26 | constructor(options) { 27 | super(options) 28 | // 添加默认中间件 29 | this.use(async (ctx, next) => { 30 | ctx.request.body = ctx.req.body 31 | // 一定要注意这个next 需要是同步,不然无法使用 32 | await next() 33 | }) 34 | } 35 | 36 | /** 37 | * 处理业务 38 | * @api public 39 | */ 40 | 41 | /** 42 | * 43 | * @param {object} [event] wx cloud event 44 | * @param {string} [event.url] url 45 | * @param {string} [event.method] request method 46 | * @param {object} [event.data] request data 47 | * 48 | */ 49 | async service(event, wxContext, customReqRes) { 50 | if (!wxContext) { 51 | throw new Error('微信上下文参数不能为空') 52 | } 53 | const { req, res } = mockReqRes(event) 54 | if (typeof (customReqRes) === 'function') { 55 | customReqRes(req, res) 56 | } 57 | const myHandleRequest = this.callback() 58 | req.wxContext = wxContext 59 | return myHandleRequest(req, res) 60 | } 61 | 62 | createContext(req, res) { 63 | const koaContext = super.createContext(req, res) 64 | const myContext = Object.assign(koaContext, req.wxContext) 65 | return myContext 66 | } 67 | /** 68 | * 重写 handleRequest 方法 69 | * @param {context} ctx 70 | * @param {*} fnMiddleware 71 | */ 72 | handleRequest(ctx, fnMiddleware) { 73 | const res = ctx.res 74 | res.statusCode = 404 75 | const onerror = err => ctx.onerror(err) 76 | const handleResponse = () => ctx.body 77 | onFinished(res, onerror) 78 | return fnMiddleware(ctx).then(handleResponse).catch(onerror) 79 | } 80 | } 81 | 82 | /** 83 | * 自由扩展 84 | */ 85 | class WxRoute extends Route { 86 | 87 | } 88 | exports.WxKoa = WxKoa 89 | exports.WxRoute = WxRoute 90 | -------------------------------------------------------------------------------- /mockReqRes.js: -------------------------------------------------------------------------------- 1 | const { IncomingMessage, ServerResponse } = require('http') 2 | const extend = require('extend') 3 | // menthod 要大写,不然无法定位路由 4 | module.exports = function(event) { 5 | const req = new IncomingMessage() 6 | const { url = '\\', method = 'POST', data, headers = {} } = event 7 | req.url = url 8 | req.method = method.toUpperCase() 9 | req.body = data 10 | req.headers = headers 11 | const response = { 12 | statusCode: 404, 13 | statusMessage: 'Not Found', 14 | end: (body) => body 15 | } 16 | const res = extend(new ServerResponse(req), response) 17 | res.headersSent = false 18 | res.setHeader('Content-Type', 'application/json') 19 | return { req, res } 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wx-koa", 3 | "version": "1.0.1", 4 | "description": " Develop wechat applets like KOA ", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "koa", 11 | "wx-koa" 12 | ], 13 | "files": [ 14 | "index.js", 15 | "mockReqRes.js" 16 | ], 17 | "author": "docter", 18 | "license": "ISC", 19 | "dependencies": { 20 | "extend": "^3.0.2", 21 | "koa": "^2.11.0", 22 | "koa-router": "^8.0.8" 23 | } 24 | } 25 | --------------------------------------------------------------------------------