微信小程序中遇到的一些问题与总结

2,290 阅读12分钟

总结这次开发历程

遇山开路,遇水架桥。

微信小程序api给开发者造成了很大的局限性、甚至可以说是障碍,但我要根据自己需求使用它,就得想方设法绕过这些障碍。

微信云托管

问题

微信小程序的网络请求必须走域名且要求https且要求网站经过ICP备案...总之就是麻烦+麻烦

需要的东西可以总结下:

  1. 网站要有域名
  2. 网站要经过ICP备案
  3. 网站要有SSL证书
  4. 网站要能够使用https访问
  5. ...麻烦死了

解决方案

使用腾讯自家的东西呗: 微信云托管

部署确实很简单,把项目发布到github上,然后在微信云托管中选择对应仓库即可。每当仓库更新,微信云托管就会自动pull新的版本并发布。

而且微信云托管还有对应的mysql,支持内网访问,也就是说微信云托管的服务可以连接微信云托管的mysql

目前没有带Dockerfile的部署都失败了。

docker

什么是docker

一直感觉docker是个高大上的东西,用了以后,依然觉得高大上,对其实现原理并不懂。关于什么是docker,也不想在这篇文章赘述。

但使用一番后,可给出自己体会: "可以在Dockerfile中配置执行某个项目的总体环境,并且可以在Dockerfile中配置执行这个项目的流水线步骤。对于python(其他暂时不清楚),可以在requirements.txt中配置需要pip install的包以及版本。然后便可以开始执行docker指令,将该项目装在一个容器里面。这个容器中不仅有该项目的源码,还有该项目依赖运行的一切环境,且该容器不依赖也不冲突于windows中的任何环境, 像是一个抽象出来的独立空间。"

可见docker的好处就是:"方便移植项目本身以及项目当前的开发环境到其他主机中,在其他不同环境的主机中,抽象出一个独立空间(容器), 在该容器中安装一个相同的项目运行环境。"

windows10上使用docker

docker好像只能在linux上使用,所以在win10上想要用docker,要么在虚拟机中,要么双系统。

我用到了一个叫做wsl2的东西,配置的系统是kali(🤫i am used to be a hacker)

具体使用docker过程

  1. 项目中创建Dockerfile,放于根目录
FROM python

WORKDIR /app

ADD . /app

RUN pip install -r requirements.txt

EXPOSE 5000

CMD ["flask", "--app", "ChatGptApi", "run", "--host=0.0.0.0"]
  1. 项目中创建requirements.txt,放于根目录
PyMySQL~=1.0.2
requests~=2.28.1
openai~=0.25.0
Flask~=2.2.2
oss2~=2.16.0
  1. 将项目上传到github,然后在微信云托管中选择相应仓库部署即可。至于docker容器的创建、容器中环境的安装那就是微信云托管的事情了,它帮我们完成。

小程序前端基于base64编码传送图片到python Flask后端

需求

在微信小程序前端将用户头像上传到部署在微信云托管的flask后端,然后后端将图片上传到阿里云OSS

难点

微信小程序前端提供的api现在只能获取用户上传的头像的一个本地临时地址,如何将读取这个临时地址的图片并将其发送到后端,后端又如何接收

实现

微信小程序前端获取用户头像的模块(html):

<button class="avatar-wrapper" open-type="chooseAvatar" bind:chooseavatar="onChooseAvatar" hidden="{{isRegis}}">
        <image class="avatar" src="{{avatarUrl}}" mode="aspectFit"></image>
</button> 

微信小程序前端获取用户头像的模块(js):

onChooseAvatar(e) {
        const { avatarUrl } = e.detail 
        this.setData({
          avatarUrl,
        })
 },

微信小程序前端发送图片到后端的模块(js):

            // 将临时路径的图片进行base64编码:
            var image_binary = wx.getFileSystemManager().readFileSync(this.data.avatarUrl, "base64")
            // 提取文件名:
            var index1=this.data.avatarUrl.lastIndexOf("/");
            var index2=this.data.avatarUrl.length;
            var name=this.data.avatarUrl.substring(index1 + 1, index2).toUpperCase();
            console.log(name)
            // 将编码后的图片上传到后端
            wx.cloud.callContainer({
                config: {
                "env": app.globalData.WeChatCloudConfig
                },
                path: "/uploadImg",
                header: {
                "X-WX-SERVICE": app.globalData.WechatCloudHeader,
                "content-type": "application/x-www-form-urlencoded"
                },
                method: "POST",
                data: {
                    "file": image_binary,
                    "name": name
                },
                success: (resp) => {
                    this.setData({
                        remoteAvatarUrl: resp.data.avatarUrl
                    }),

后端接收图片的模块(python3 Flask2.x.x):

@app.route("/uploadImg", methods=['POST'])
def uploadImg():
    """
    接收前端的图片字节流
    :return:
    """
    image_binary = base64.b64decode(request.form['file'])  # 对base64进行解码还原
    alioss = AliOSS()
    avatar = alioss.uploadImage(image_binary, request.form['name'])
    avatarUrl = "https://slamd14bot.oss-cn-hangzhou.aliyuncs.com/" + avatar
    return {"avatarUrl": avatarUrl}

为什么是base64编码

我们知道在计算机中任何数据都是按ascii码存储的,而ascii码的128~255之间的值是不可见字符。而在网络上交换数据时,比如说从A地传到B地,往往要经过多个路由设备,由于不同的设备对字符的处理方式有一些不同,这样那些不可见字符就有可能被处理错误,这是不利于传输的。所以就先把数据先做一个Base64编码,统统变成可见字符,这样出错的可能性就大降低了。

先看看对二进制内容不进行base64编码,读取图片得到的结果:

// 将临时路径的图片不进行编码:
var image_binary = wx.getFileSystemManager().readFileSync(this.data.avatarUrl, "binary")

image_binary(下面显示的是二进制数据对应的ASCII码):

image.png

有很多的不可见字符

对二进制内容进行base64编码,读取图片得到的结果:

// 将临时路径的图片进行base64编码:
var image_binary = wx.getFileSystemManager().readFileSync(this.data.avatarUrl, "binary")

image_binary(下面显示的是二进制数据经过base64编码后的结果):

image.png

字符全部可见

我认为将图片内容经过base64编码后传输到后端,有利于后端接收图片。因为传输的都是可见字符串,后端完全可以用常规的方法来获取参数值,比如当前项目的flask,获取base64编码后的图片字符串与获取普通字符串数据都是用的一个方法: request.form['']

base64编码的另一个好处:

当不可见字节流在网络上传输时,比如说从 A 计算机传到 B 计算机,往往要经过多个路由设备,由于不同的设备(特指老的路由设备)对字节流的处理方式有一些不同,这样那些不可见字节就有可能被处理错误,这是不利于传输的。所以就先把数据先做一个 base64 编码,统统变成可见字节,也就是 ascii 码可表示的可见字符,确保数据可靠传输。base64 的内容是有 0 ~9,a ~z,A ~Z,+,/组成,正好 64 个字符,这些字符是在 ascii 可表示的范围内,属于 95 个可见字符的一部分。

我们往往使用 json 对要发送的数据进行序列化,然而,json 无法序列化字节类型的数据。那我们如果想发送字节类型的数据(图片,音频,视频,语音等二进制数据),应该怎么办呢?

可能会想,我们可以把字节类型数据转为字符串类型数据。然而不幸的是,对于字节类型的图片,音频,视频,语音等二进制数据里面含有很多不可见字节,而这些不可见字节无法转为字符串。但是,我们可以先用 base64 把这些不可见字节数据编码为可见字节数据,然后再转为字符串类型,然后在通过 json 进行序列化,然后再编码为字节流通过网络发送出去。而对方收到数据后,要先把字节流解码为字符串,然后再反序列化,然后再转为字节类型数据,然后再使用 base64 解码还原成原始数据。

一些概念

大多数编码都是由字符串转换为二进制的过程,而Base64的编码则是从二进制转为字符串。

base64利用流程:

先用 base64 把这些不可见字节数据(指的是ASCII不可见字符)编码为可见字节数据,然后再转为字符串类型,然后再通过 json 进行序列化,然后再编码为字节流通过网络发送出去。而对方收到数据后,要先把字节流解码为字符串,然后再反序列化,然后再转为字节类型数据,然后再使用 base64 解码还原成原始数据。

image.png

命令行启动python项目,明明文件相对路径没问题,还可以跳转,却报"file not found error"

报错位置

image.png

原因

原因真是百思不得其解

网上四处搜索, 大家都在科普什么是相对路径什么是绝对路径...😇...

不知道在哪个网页看到了一个api: os.getcwd() 输出当前目录

我倒要看看当前目录,难道不是myChatGpt/utils/AliOssUploadImg.py或者myChatGpt/utils/吗?

在AliOssUploadImg.py中调用了这个os.getcwd(), 输出匪夷所思: myChatGpt/

🤯 what happend?

又不知道在哪个网页看到了一个关键词: 命令行

命令行?

命令行怎么了

然后仔细看了看console中的一段报错信息:file "../config/config.json" not found

感觉很奇怪,为什么file是"../config/config.json", 我这写的不是相对路径吗

我当前通过命令行运行的ChatGptApi.py所处的路径是/myChatGpt, 那么file "../config/config.json"应该指的是相对于ChatGptApi.py的路径吧? 至少从这个报错的语义来看是这样,难怪file not found

所以正确相对路径写法应该这样写:

image.png

bug

聊天消息换边了

bug描述:

我发送的消息从小狗那边显示出来了,然后小狗回复的消息从我这边显示出来了

AA62B4FA3DD4936CBDE98D9CD5886C15.jpg

交互流程图:

5AFC1A6DE4660AD6EDEA472CE10E8E39.png

bug分析:

1、2、3、4、5、6步都是同步且阻塞的啊,真的想不通为什么会出现图中的那种bug

bug频率:

这个bug在正式版发布后,只出现了这一次。

待优化

长时间消息无响应

情况描述

后端长时间消息无响应会导致前端bot一直无法回复用户信息,并且用户输入框卡死,用户解决该问题的唯一方法就是后台关闭小程序重进,用户体验极差。

情况分析

  1. 前端:

(问题见注释)

wx.cloud.callContainer({
    "config": {
      "env": app.globalData.WeChatCloudConfig
    },
    "path": "/start",
    "header": {
      "X-WX-SERVICE": app.globalData.WechatCloudHeader,
      "content-type": "application/x-www-form-urlencoded"
    },
    "method": "POST",
    "data": {
      "mes": mes
    },
    // 不知道默认的timeout是多少,微信云托管规定最长可以设置为15s
    // 之前就是没写该参数用的默认的timeout,导致微信云托管部署的服务稍微响应慢一点(超过了默认timeout),微信云托管就回复给前端500,即使后面服务做出了响应,前端也再也接收不到了
    // 所以在这里显式加上最长timeout
    "timeout": 15000 
})
  1. 后端

不好写呀。

wx.cloud.callContainer()的最大15s超时把问题局限住了。很多创作性问题openAi的回复时间都是超过15s的。

两个办法:

  1. 为刚刚通过ICP备案的域名配阿里云ip,申请https,然后wx.request()去调接口,毕竟wx.request()没有超时限制。实在麻烦,刚通过ICP备案的域名是个.cn的顶级域,还要公安部备案...无语。暂时不考虑这个办法。

  2. 前端仍然使用wx.cloud.callContainer(),但迭代使用。

可以总结下后端遇到的问题:

在一些开发中,可能会遇到需要长时间处理的任务,此时就需要使用异步的方式来实现,让长时间任务在后台运行,先将本次请求的响应状态返回给前端,不让前端界面「卡顿」,当异步任务处理好后,如果需要返回状态,再将状态返回。

具体来说就是15s内,openai是有可能无法完成一次请求响应的,尤其是对于一些创作性问题。我需要延长前端等待的时间,但前端由于调用的是微信云托管接口,无法维持15s以上的连接,所以我想的是前端迭代向后端发送最多三次请求,每次请求在后端限制响应时间小于14s。

也就是说,我把45s作为openai卡死的标志。

  1. 为什么会卡死(一直不回消息

拜微信所赐,调用wx.cloud.callContainer(),若15s内后端未给出响应,则微信云托管就帮我们做出响应: 响应码500

也就是说智能小狗在思考的时候,微信云托管就给我擅作主张把连接断了,然后当时小程序前端又没加fail回调函数,所以对这种情况未做出处理,就导致了消息框一直无法输入,小狗一直没回复,也永远回复不了了。

解决

  1. 前端思路:

A64B0910EDE77EC80660BC3E338B3E46.png

  1. 后端思路:

B17296D60AA7E58179E517125A0CC764.png

后端多线程实现异步与redis代码:

主线程1

from concurrent.futures import ThreadPoolExecutor
import time

@app.route('/start2', methods=['POST'])
def start2():
    time_start = time.time()
    openid = request.form['openid']
    message = request.form['mes']
    cnt = int(request.form['cnt'])
    print(message)
    print(cnt)

    if cnt == 1:  # 由线程去执行与openAi交互的任务
        redisHandle.initForMes(openid)  # 为openid初始化redis
        executor.submit(withOpenAi, message, openid)

    while True:
        time_now = time.time()
        if time_now - time_start > 14:  # 严格控制后端响应时间,避免由微信云托管造成的500错误(15s超时)
            if cnt == 3:  # 前端第三次请求的14s快到了openai仍然没响应,认定openai宕机
                return {
                    "data": {
                        "respCode": "500",
                        "message": "智能小狗开小差了,该次提问机会不消耗,可尝试重新提问。"
                    }
                }
            if cnt ==1 or cnt ==2:  # 前端第一二次请求的14s快到了,则需要前端重新请求一次,为了避免15s超时
                return {
                    "data": {
                        "respCode": "302",
                        "message": "需要重新向后端请求一次"
                    }
                }
        if redisHandle.redisGetMsg(openid) != '':  # TODO redis中的openid对应的value是否有值
            return {
                "data": {
                    "respCode": "200",
                    "message": redisHandle.redisGetMsg(openid)
                }
            }

线程2(异步任务):

def withOpenAi(message, openid):  # 在新线程中跑, session_会失效. 因为flask的session是线程隔离的
   """
   处理与openai的长时间交互
   :param message: 用户的问题
   :param openid:
   :return:
   """

   data["prompt"] = message
   print("咨询中...")  # info "asking..."
   resp = requests.post('https://api.openai.com/v1/completions', headers=headers, data=json.dumps(data)).text
   dic = json.loads(resp)
   print(dic)
   dic['choices'][0]['text'] = strHandle(dic['choices'][0]['text'])
   redisHandle.redisSave(openid, dic['choices'][0]['text'])

redis部分:


import redis

pool = redis.ConnectionPool(host='阿里云ip', port=6379, password="嘎嘎嘎", decode_responses=True)
r = redis.Redis(connection_pool=pool)

def initForMes(openid):
    """
    初始化映射关系: openid<->''
    """
    r.set(openid, '')

def redisSave(openid, mes):
    """

    :param openid:
    :param mes: openAi的答复
    :return:
    """
    r.set(openid, mes)

def redisGetMsg(openid):
    """
    :param openid:
    :return: openAi的答复
    """
    return r.get(openid)

Flask session线程隔离

其实一度想用session来充当存储openAi的回复的中间件,但是发现始终无法成功。无论是在主线程开新线程执行异步任务时将session作为参数传递给异步任务,还是在异步任务中直接使用全局的session,都一律失败。

想到了原来Flask的session是线程隔离的。

想到web服务器最好是无状态,又想到需要跨线程读取异步写入的数据,并且对数据CRUD,便用到redis

感谢redis😘