├── .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 | 
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 |
--------------------------------------------------------------------------------