分类: 微信小程序

  • 微信小程序开发 MCP

    https://github.com/yfmeii/weapp-dev-mcp

    https://www.npmjs.com/package/@yfme/weapp-dev-mcp

    起因是前几天微信开放社区说微信开发者工具 2.0 发布了,据说有增强 AI 协作能力的封装

    但是事实是并没有找到任何暴露 MCP 服务的迹象,社区也没有人做出回复

     

    那就只能自己动手了,小程序有一个自动化,是用来做自动化测试的,能通过编程的方式控制和读取运行状态下的小程序,也就是说设想中的完美开发方式,AI 完成编码后自己能走一遍交互能大大降低返工次数也能节省 AI 的使用成本

    当然有很多现成的别人做过的 MCP,大多都是以 cli 形式控制的,但是这样就存在一个问题,部分工具的 MCP 有沙箱机制,会导致 MCP 无法调用工作目录以外的文件,所以我这里主要使用 websocket 控制

    首先要打开微信开发者工具的自动化测试:

    1. 打开微信开发者工具
    2. 进入 设置 → 安全设置 → 服务端口
    3. 开启 “HTTP 调试” 和 “自动化测试”

     

    配置:

    {
      "mcpServers": {
        "weapp-dev": {
          "command": "npx",
          "args": [
            "-y",
            "-p", "@modelcontextprotocol/sdk@1.17.2",
            "-p", "fastmcp@3.23.0",
            "-p", "@yfme/weapp-dev-mcp",
            "weapp-dev-mcp"
          ],
          "env": {
            "WEAPP_WS_ENDPOINT": "ws://localhost:9420"
          }
        }
      }
    }

    启动微信开发者工具(macOS):

    /Applications/wechatwebdevtools.app/Contents/MacOS/cli auto --project /path/to/your/project --auto-port 9420

    其中:

    • --project 参数指定小程序项目目录路径(请替换为实际的项目路径)
    • --auto-port 参数指定 WebSocket 服务端口(默认 9420)

     

    提供工具:

    应用工具(Application Tools)

    • mp_ensureConnection – 确保自动化会话就绪;可选择强制重连或覆盖连接设置
    • mp_navigate – 在小程序内导航,支持 navigateToredirectToreLaunchswitchTab 或 navigateBack
    • mp_screenshot – 捕获屏幕截图并返回(或保存到磁盘)
    • mp_callWx – 调用微信小程序 API 方法(如 wx.showToast
    • mp_getLogs – 获取小程序控制台日志,可选择获取后清除

    页面工具(Page Tools)

    • page_getElement – 通过选择器获取页面元素
    • page_waitElement – 等待元素出现在页面上(⚠️ 不适用于自定义组件内部元素)
    • page_waitTimeout – 等待指定的毫秒数
    • page_getData – 获取当前页面的数据对象,可指定路径
    • page_setData – 使用 setData 更新当前页面的数据
    • page_callMethod – 调用当前页面实例上暴露的方法

    元素工具(Element Tools)

    • element_tap – 通过 CSS 选择器点击 WXML 元素
    • element_input – 向元素输入文本(适用于 input 和 textarea 组件)
    • element_callMethod – 调用自定义组件实例的方法(需要 ID 选择器)
    • element_getData – 获取自定义组件实例的渲染数据(需要 ID 选择器)
    • element_setData – 设置自定义组件实例的渲染数据(需要 ID 选择器)
    • element_getInnerElement – 获取元素内的元素(相当于 element.$(selector)
    • element_getInnerElements – 获取元素内的元素数组(相当于 element.$$(selector)
    • element_getSize – 获取元素大小(宽度和高度)
    • element_getWxml – 获取元素 WXML(内部或外部)
  • Donut 与原生的两次相遇

    Donut 与原生的两次相遇

    在大概半年前,工作原因采用了微信新出的 Donut 平台,从其域名可以看出对微信的重要性

    Donut 是一个多端的应用程序开发方案,主要是通过微信小程序的开发方式开发移动应用程序,兼容 iOS 和 Android,当然对于应用程序来说,微信小程序的 API 和功能是不够的,尽管其推出了渲染性能和原生差别不大的 Skyline 渲染引擎

    小程序的开发方式对于第三方 SDK 或插件来说是较为不友好的,特别是在移动应用程序中,而且在初期版本中,连用户对这个应用的授权情况都无法得知,于是有了第一次相遇

    第一次相遇

    刚开始 Donut 框架还是提供原生工程的,在初期 API 的支持确实过于贫瘠,所以我们的考虑是首个版本需要快速上线,就采用以 Donut 为主要工程,使用原生补充其功能,后期采用原声工程嵌入 Donut 模块

    但是马上问题就随之而来,因为我们是早期的用户,所以 Donut 提供了技术支持,在将原生工程嵌入 Donut 项目的过程中,我们处处碰壁,在官方文档上也就只有简单的描述如何让小程序模块与原生工程通信

    在尝试寻求 Donut 的帮助的时候,他们告知将来将不会提供原生工程,也让我们不要使用原生

    随后不久官网说明也变成了下面这样子

     

    我不会原生开发,当时也是硬着头皮做

    但是适配工作已经开始了,在上线的 ddl 之前,Donut 显然无法做到我们能用的程度,我们还是硬着头皮继续研究我们的原生部分,在他们的工程中显然做了很大的限制,例如在 Android 中,小程序包启动完成后就销毁了对应的 Activity 等等

    具体踩坑过程就不再赘述

    第二次相遇

    在前段时间,Donut 告知我们,使用原生工程构建的应用将在 12月底停用,届时所有用户将无法使用应用

    于是我们又要做第二次的适配,当时我们的项目已是如此

    但是好在前不久,Donut 推出了原生插件,并且这次提供了更加完善的接入原生的文档和支持,这次我们打算抛弃 React Native 部分,仅保留难以迁移的之前适配好的原生部分

     

    我主要做 iOS 部分的适配,这里仅说明 iOS

    原生插件的工程是 OC 的,“得益” 于 OC 的语法,我不得不使用 Swift

    在 iOS 中,插件是一个 <pluginId>.framework

    使用 Swift,首先就是要创建一个桥文件,新建文件后,在 #import,但是无论怎么编译,就是找不到这个桥文件,最后翻遍了全网才看到官网上的描述,文档地址

    #import <ProductName/ProductModuleName-Swift.h>

    在解决完这个问题后终于能编译通过了

    良好的编程体验

    在原生插件中,分为同步方法和异步方法,都是直接暴露插件的原生函数,而定义函数又是需要在 OC 中进行,所以这及其影响体验

    所以我在 OC 中只暴露了 sync 和 async 两个方法

    // 声明插件同步方法
    WEAPP_EXPORT_PLUGIN_METHOD_SYNC(sync, @selector(sync:))
    
    // 声明插件异步方法
    WEAPP_EXPORT_PLUGIN_METHOD_ASYNC(async, @selector(async:withCallback:))
    
    - (id)sync:(NSDictionary *)param {
        NSLog(@"sync %@", param);
        
        // 提取方法名和数据
        NSString *methodName = param[@"name"];
        id data = param[@"data"] ?: nil;
    
        // 调用 invoke 方法,并传递提取出的方法名和数据
        return [self.nativeSync invoke:methodName withData:data];
    }
    
    - (void)async:(NSDictionary *)param withCallback:(WeAppNativePluginCallback)callback {
        NSLog(@"async %@", param);
    
        // 提取方法名和数据
        NSString *methodName = param[@"name"];
        id data = param[@"data"] ?: nil;
    
        // 调用 Swift 的异步 invoke 方法
        [self.nativeAsync invoke:methodName withData:data withCallback:^(id result) {
            // 处理回调结果,这里使用了一个示例回调数据
            NSDictionary *callbackData = @{ @"data": result };
            callback(callbackData);
        }];
    }
    
    @objcMembers
    class NativeSync: NSObject {
    
        public func invoke(_ methodName: String, withData data: Any?) -> NSDictionary {
            let selectorName = methodName + (data != nil ? "WithData:" : "")
            let selector = Selector(selectorName)
    
            if self.responds(to: selector) {
                let result = self.perform(selector, with: data)
                return result?.takeUnretainedValue() as? NSDictionary ?? RespObject(msg: "Error")
            } else {
                return RespObject(msg: "No such method")
            }
        }
        
        /*
         自定义原生同步函数编写规则:
         1. 函数接受参数必须为 data,且类型只能为 Any,类型判断在函数内进行
         2. 函数返回值必须为 NSDictionary,可以使用 RespObject 函数构建返回值
         */
    
        func someFunction(data: Any) -> NSDictionary {
            RespObject(data: "Result from someFunction \(data)")
        }
    }
    
    @objcMembers
    class NativeAsync: NSObject {
    
        public func invoke(_ methodName: String, withData data: Any?, withCallback callback: @escaping (NSDictionary) -> Void) {
            let selectorName = methodName + (data != nil ? "WithData:callback:" : "WithCallback:")
            let selector = Selector(selectorName)
            
            let block: @convention(block) (NSDictionary) -> Void = callback
    
            if self.responds(to: selector) {
                if let data = data {
                    self.perform(selector, with: data, with: block)
                } else {
                    self.perform(selector, with: block)
                }
            } else {
                callback(RespObject(msg: "\(selectorName) is not a valid method."))
            }
        }
        
        /*
         自定义原生异步函数编写规则:
         1. 函数接受参数必须为 data,且类型只能为 Any,类型判断在函数内进行
         2. 函数接受闭包必须为 callback,且类型只能为 (NSDictionary) -> Void,可以使用 RespObject 函数构建
         */
    
    
        func someAsyncFunction(callback: @escaping (NSDictionary) -> Void) {
            DispatchQueue.global().async {
                DispatchQueue.main.async {
                    callback(RespObject(data: "Result from someAsyncFunction"))
                }
            }
        }
    }
    
    var sendMsgClosure: ((Any) -> Void)?
    
    @objcMembers
    class NativeMsg: NSObject {
        static let shared = NativeMsg()
        
        private override init() { }
        
        class func configure(_ block: @escaping (Any) -> Void) {
            sendMsgClosure = block
        }
        
        class func sendMsg(_ name: String, with data: NSDictionary?) {
            var msg: [String: AnyHashable] = ["name": name]
            if data != nil {
                msg["data"] = data
            }
            
            sendMsgClosure?(msg)
        }
        
        class func sendMsg(_ name: String) {
            let msg: NSDictionary = ["name": name]
            
            sendMsgClosure?(msg)
        }
    
    }
    
    

    这样就能在 Swift 中实现所有原生逻辑了,当然在开发过程中还做了原生部分单方面对小程序发送消息,这是封装好的调用插件的东西,时间关系没有更深入封装

    export class Plugin {
        eventListeners = {};
    
        plugin = null;
        constructor(pluginId) {
            let that = this;
            console.log('init plugin')
            wx.miniapp.loadNativePlugin({
                pluginId,
                success: (res) => {
                    that.plugin = res
    
                    // 监听事件
                    res.onMiniPluginEvent((event) => {
                        const name = event.name;
                        const data = event.data;
    
                        // 根据事件名,找到对应的事件id
                        const eventIds = Object.keys(that.eventListeners).filter(eventId => {
                            return that.eventListeners[eventId].name === name;
                        });
    
                        // 根据事件id,找到对应的回调函数
                        eventIds.forEach(eventId => {
                            const callback = that.eventListeners[eventId].callback;
                            callback(data);
                        });
                    })
                },
                fail: (e) => {
                    console.log('load plugin fail', e)
                }
            })
        }
    
        async getPlugin() {
            let plugin = null
            if (this.plugin) {
                plugin = this.plugin
            } else {
                plugin = await new Promise((resolve, reject) => {
                    setTimeout(() => {
                        reject('timeout');
                    }, 1500);
                    let interval = setInterval(() => {
                        if (this.plugin) {
                            clearInterval(interval)
                            resolve(this.plugin)
                        }
                    }, 10)
                })
            }
    
            return plugin
        }
    
        add(name, callback) {
            // 随机取一个唯一的事件id
            const eventId = Math.random().toString(36).substr(2);
    
            // 在这个事件id下,存储需要监听的事件名和回调函数
            this.eventListeners[eventId] = {
                name,
                callback,
            };
    
            // 返回这个事件id,用于后续的移除事件监听
            return eventId;
        }
    
        once(name, callback) {
            // 用add方法添加事件监听
            const eventId = this.add(name, (data) => {
                // 执行回调函数
                callback(data);
    
                // 执行完回调函数后,移除事件监听
                this.remove(eventId);
            });
        }
    
        remove(eventId) {
            // 从eventListeners中删除这个事件id
            delete this.eventListeners[eventId];
        }
    }
    
    let pluginInstance = new Plugin('')
    
    
    export default pluginInstance

    最后

    抱着一开始的设想,如果能使用原生工程中嵌入小程序模块,对开发者来说无疑是最好的,有着优秀的拓展性和足够简单的开发方式,例如 Widget 等,如果只能在小程序工程中嵌入原生模块,就只能等 Donut 的更新

  • Design Token初探——Figma Tokens插件与Taro React的结合

    Design Token初探——Figma Tokens插件与Taro React的结合

    老早之前就听说了Design Token,最近也在B站学习了草帽老师在新像素账号发表的Design Token的介绍与实战

    作为UI兼开发,经常画完设计稿就懒得写样式了,不同的方式做到同一效果确实十分枯燥

    使用Design Token也能减少一部分重复的工作

    我对Design Token的理解

    在我的理解Design Token就是将样式转换为一个Token,UI在设计时为元素或组件绑定Token后,在框架不变的情况下能够一定程度上不更改代码实现样式的更新与迭代

    Design Token的搭建

    首先是色板,色板的创建方式有很多种,为了方便我通过字节跳动的Figma的Semi插件生成了一个色板

    然后根据草帽老师的讲解,我这里也使用了Figma Tokens插件

    参考草帽老师,我建立了了三层Token:

    (由于是测试可行性,我这里命名也十分草率)

    目前一共是四个文件,后续根据我的工作流,可能还需要建立多个兼容文件,比如说React Native和小程序端支持的样式字段不一样

    为了开发方便,token文件的名称如果没有特殊需求,最好与这个文件的根结点名字相同

    上面的这个token在的根结点为com,它也存在于名为com的Token文件中

    设计稿的绘制

    我也随便绘制了一个设计图

    为了测试切换,我就像上面的三层Token文件一样,制作了sys-dark,用于切换夜间模式

    发现在设计稿中能够正常切换

    结合程序开发

    这样一个Token所生成的就是几个JSON文件,JSON对于开发来说再熟悉不过了,但是此时这个JSON文件转换成样式还需要另外处理

    已知所有的颜色都在这几个JSON文件中,其中也有互相引用的部分

    在某一个标签的颜色的Token中,在不断的寻找上一层的Token后,最终会找到这个颜色

    通过这个Token我就能知道这个Token存在于哪个文件中

    通过递归调用最终能找到这个颜色

    由于我使用的是Taro,所以这里的例子中的夜间模式也是通过Taro的API判断的

    例子代码(不是很会写TS):

    import Taro from "@tarojs/taro";
    
    import base from './token/base.json'
    import sys from './token/sys.json'
    import sys_dark from './token/sys-dark.json'
    import com from './token/com.json'
    
    type Token = {
        value: string | object;
        type: string;
    }
    
    export default class StyleToken {
        systemInfo: any;
    
        constructor() {
            this.systemInfo = Taro.getSystemInfoSync()
        }
    
        private getBatch(key: string): object {
            if (key[0] == '$') {
                key = key.substring(1)
            }
    
            switch (key.split('.')[0]) {
                case 'base':
                    return base;
                case 'sys':
                    if (this.systemInfo.theme === 'dark') {
                        return sys_dark;
                    }
                    return sys;
                case 'com':
                    return com;
                default:
                    return {}
            }
        }
    
        private getToken(token: string): null | string | Token {
            if (token[0] == '$') {
                token = token.substring(1)
            }
    
            let keys = token.split('.');
            let batch = this.getBatch(keys[0]);
    
            let result: any = batch;
    
            for (let i = 0; i < keys.length; i++) {
                result = result[keys[i]];
            }
    
            return result;
        }
    
        public get(token: string): string | object {
            const { value, type } = this.getToken(token) as Token;
    
            if (typeof value === 'string') {
                if (value[0] == '$') {
                    return this.get(value.substring(1))
                } else {
                    return value;
                }
            }
    
            if (typeof value === 'object') {
                Object.keys(value as object).map(e => {
                    (value as object)[e] = this.get(value[e]);
                })
    
                return value!;
            }
    
            return value
        }
    }
    

    在全局状态中实例化上面这个StyleToken类,然后通过这个类的公开方法get获取到某个token的样式

    我这里就直接在某个组件中使用了

    import StyleToken from '@/common/styles/getToken'
    const Style = new StyleToken();
    
    
    // ......省略若干代码......
    
    {
        backgroundColor: Style.get('sys.color.surface'),
    }
    
    // ......省略若干代码......

    最终效果

    我也只实践了顶上的导航栏,为了简便也没有放icon,页面的颜色切换是微信适配的

    最后

    这也是我对Design Token的初步探索,不知道后续会不会有所更新这一部分。

    最后十分感谢草帽老师的分享,让我对Design Token有了认识。

  • 小程序云开发数据库/Mongodb时间戳转时间

    我个人习惯在数据库中把所有时间按照时间戳保存,因此在某些使用场景如查询同一天过生日的人,需要忽略年份

    此时需要将时间戳转为时间,并且格式化查询

    AggregateCommand.add(value: Expression[]): Object
    聚合操作符。将数字相加或将数字加在日期上。如果数组中的其中一个值是日期,那么其他值将被视为毫秒数加在该日期上。

    故如下操作即可

    $dateToString: {
        format: "%m%d",
        date: {
            "$add": [new Date(0), "$birthday"]
        }
    }
  • 小程序长列表性能优化

    小程序长列表性能优化

    2023 年 1 月 26 日更新:

    在 skyline 渲染器中, scroll-view 设置 type=”list” 即可自动处理长列表的渲染


    在小程序官方的文档“性能“一部分中写道

    建议一个页面使用少于 1000 个 WXML 节点,节点树深度少于 30 层,子节点数不大于 60 个。一个太大的 WXML 节点树会增加内存的使用,样式重排时间也会更长,影响体验。

    但是有一些场景下我们需要”无限“下划,此时越往下划页面上的WXML节点就越多。于是不显示在屏幕上的节点我们可以将其卸载掉。

    在微信官方文档中,平台能力-扩展能力-扩展插件-“recycle-view”组件就可以用来解决这个问题,但是我们需要自己写一个方法来传入当前循环的列表组件的尺寸,但是很多时候,我们不能很好的预知即将渲染的组件的确切尺寸。

    解决方法

    我们在微信官方文档中看到了IntersectionObserver,可以用来监听组件是否和某个区域有交集,我们使用IntersectionObserver IntersectionObserver.relativeToViewport(Object margins)来监控可视区域的所有组件

    同时长列表中的每个元素大多为循环,给每一个组件分配一个唯一的id,然后监听这个组件即可:

    this.listener = this.createIntersectionObserver()
            this.listener.relativeToViewport({
                top,
                bottom
              }).observe(`#${id}`, (res) => {
                if (res.intersectionRatio == 0) {
                  this.setData({
                    show: false,
                    height: res.boundingClientRect.height
                  })
                } else {
                  this.setData({
                    show: true,
                    height: res.boundingClientRect.height
                  })
                }
              })

    参考recycle-view中在滚动过程中,为了避免频繁出现白屏,会多渲染当前屏幕的前后2个屏幕的内容,所以上面的top和bottom参数为两个屏幕高度。

    然后在渲染和卸载时,重新获得组件高度,在下次渲染时基本能够确定高度,设置css min-height为监听器中获得的height,在内容卸载后保持原有高度,防止滚动条发生奇怪的抖动。

    由于近期我在外面出差,所以只是大致在原理上讲了讲,在即将写完的时候,发现网上有一篇文章使用了类似的方法,并且还给出了相关示例代码,文章:https://juejin.cn/post/6844904014404927496,GitHub:https://github.com/godaangel/wxapp-long-list

    大家可以参考和使用他的解决方法,他的示例代码中没有卸载时的组件高度获取,时间关系,我就不测试是否会出问题了,我认为如果在中途组件高度有变化,在卸载时重新获取一次组件高度似乎更加可行。

  • 小程序图片展示尺寸优化

    小程序图片展示尺寸优化

    近期制作了一个类似论坛/朋友圈/树洞的社交类小程序(未公开使用)

    沙雕网友们的沙雕图是大伙的快乐源泉,所以图片的展示就极为重要

    这里只讨论类似微信朋友圈的图片尺寸方面的思路

    大致思路

    大致需要考虑一条动态不能占用过大的尺寸以至于不能让同屏展示的动态数过少

    然后要考虑图片展示的美观性,在上面那个前提下,尽可能平衡图片细节和图片的大小

    而且不能让任何一张图片被拉伸,加上mode="aspectFill"属性即可


    一条有图片的动态可分为单张图和多张图

    在多张图(每行三张)的情况下,我们将全部横向空间分为三张图片和两个间隔,以下内容将主要以该尺寸模式延伸

    在这里我们以两张图片和一个间隔为基准,我们在暂时假设一个尺寸:图片宽度64px,间隔8px

    多张图片

    多张图片所有图片使用正方形展示

    按上文所述 将全部横向空间分为三张图片和两个间隔 计算出每张图片的尺寸

    图片数量234
    排列模式1×21×32×2

    按照上表的模式(4张图片除外),更多图片使用多行处理即可

    展示效果如下图

    单张图片

    获得图片的尺寸信息

    微信小程序可以通过wx.getImageInfo()获得图片的尺寸信息

    图片的简单分类

    根据图片的长宽比,大致可以分为两类分别是宽图片(宽大于高)高图片(高大于宽)

    根据两种分类就有了两个不同的优化方式,但两者十分类似

    宽图片

    宽图片在处理上较为简单,最大宽度可以占用全部横向空间

    第一步 设置宽度

    我们将图片的宽度设置为一个基准(以两张图片和一个间隔为基准,后面不再赘述)

    然后,高度根据图片的原始比例计算(由于是宽度图片,高度不会超过基准)

    第二步 确定高度

    在第一步后,我们很容易发现问题,如果一张图片类似这样:

    这样十分不直观,所以我们需要确定一步高度,更准确一点就是确定最小高度

    为了方便,我们取黄金比例0.618

    就是在正常情况下图片高度太小,我们就引入黄金比例(非将图片以黄金比例展示,仅使用0.618)

    我们这里使用上面的多张图展示的图片边长64px × 1.618为最小高度

    取高度

    如果根据第一步设置宽度后,按照图片原始比例计算高度大于最小高度,则取图片原始比例计算高度

    反之:按照上面的多张图展示的图片边长64px × 1.618为高度

    也就是根据图片原始尺寸比例的计算高度与最小高度 两者取较大值

    第三步 重新确定宽度

    在上面两步后,图片的展示已经足够完善了,但是仍有一个问题,如果一张图片按照正常情况,高度接近最小高度(由于是正常情况,故高度比最小高度大)

    另一张图使用最小高度

    两张图实际的长宽比相差可能会很大,但展示效果却十分相似,所以我们需要对两种情况进行区分

    上方提到过,图片最大宽度可以占用全部横向空间

    所以在使用了最小高度时,我们将图片的宽度铺满全部空间

    结果

    在以上处理后,实测图片没有出现图片过大等问题

    高图片

    高图片与宽图片类似

    但我们无法将高图片的高度设置过高,太高就会单条动态占用过大空间

    第一步 设置高度

    我们首先将高度设置为一个基准

    然后,宽度根据图片的原始比例计算

    第二步 确定宽度

    类似宽图片,如果图片“过细”,效果也不太好

    同样我们设置一个最小宽度与最小高度一致64px × 1.618

    根据图片原始尺寸比例的计算宽度与最小宽度 两者取较大值

    结果

    高图片与宽图片不同,高图片在使用最小宽度后没有拉长高度,这是因为继续拉长会过高显得右边空白过大

    理后,实测图片没有出现图片过大等问题

  • 微信小程序云开发数据库实时收据推送

    最近接到一个项目,只有两三天的开发时间,就是学校音乐协会专场的现场互动抽奖小程序

    由于时间有限,只做了节目热度节目切换热度排行榜现场抽奖

    最核心的东西就是小程序云开发数据库的实时推送

    开放文档

    根据文档,嗲用Collection上的watch方法即可坚挺给定查询条件对应的数据,可以使用orderBylimit

    也就是说使用watch方法可以被动接收数据库的变化

    具体实现

    我这里放出文档中体现较完整的中奖列表的实时刷新

    prizeBoardWatcher = db.collection('prize')
          .orderBy('time', 'desc')
          .watch({
            onChange: function (snapshot) {
              console.log(snapshot.docs);
              that.setData({
                prize: snapshot.docs
              })
            },
            onError: function (err) {
              console.error('the watch closed because of error', err)
            }
          })

    代码很简单能实现想要的效果

    问题

    具体我遇到了一个比较严重的问题,就是有时候会监听失败

    the watch closed because of error Error: errCode: -402002 realtime listener init watch fail | errMsg: login fail Error: wsclient.send timedout
    history states: UNINIT-(loginStart)->INIT_LOGGING_IN-(loginFail)->INIT_LOGIN_FAIL-(loginStart)->INIT_LOGGING_IN-(loginFail)->INIT_LOGIN_FAIL-(loginStart)->INIT_LOGGING_IN-(loginFail)->INIT_LOGIN_FAIL-(close)->CLOSED

    这个问题我也搜了一下,发现很多人都会遇到,但是没有给出解决方案,但是根据官方案例似乎没有遇到这样的问题

    所以很可能是自己代码性能优化的问题

    这里有一篇文章如何用云开发打造万人同屏高并发实时互动小程序

    在专场结束后虽然需求方很满意,但是我还是第一时间打开了微信开发者工具看看到底是哪里出了问题

    最后也只能找到几个可以优化的地方

    优化

    首先是开始监听的位置,根据官方文档给出的例子,const watcher = db.coll......,我是直接将其放在了生命周期函数中的onLoad

    重新看了一遍小程序生命周期的文档

    发现显然在onShow中开始监听,在onHide中暂停监听更好

    希望在下次现场活动中能够验证本次优化,我也会在下次充分准备希望能解决或降低问题出现的概率

  • 使用小程序云开发写爬虫的一个坑

    如果目标文档是GBK编码的,那么请求头里面一定要去掉这行'Accept-Encoding': 'gzip, deflate',否则神仙都救不回来

  • 「微信小程序踩坑记1」自定义tabbar

    「微信小程序踩坑记1」自定义tabbar

    写在前面

    我在上学期就开始做一款学校用的小程序,就是课表那些,无奈如上一篇文章所说的代码丢失,那个小程序的源码就正正好好丢失了,半个月的成果,好不容易涨到每天超过1000人使用,因为代码丢失没法更新换学期,于是截至3月19日每天最高不到150人访问。

    当然,小程序是重写了的,这次就没有像之前那样,借鉴另外一款小程序的其中一个UI组件,然后自己设计UI,这次使用了Vant Weapp,这个用的是微信小程序自定义组件,所以与之相关的坑就接踵而至,今天先说一下这个tabber

    Vant Weapp tabber的使用

    根据文档,这个tabber图标的切换用的是一个acitve变量,所以我们可以单独放一个页面,最底部50px用作tabber的位置,然后上面的部分用来放其子页面。

    有了这么一个思路,我们就将子页面都按照自定义组件来用,把当前active的页面显示出来,其他的隐藏。

    具体操作

    我们假设页面的容器和tabber单独的页面放在根目录,并命名为index,所以我们在index.json通过如下代码引入

    "usingComponents": {
      "van-tabbar": "path/to/@vant/weapp/dist/tabbar/index",
      "van-tabbar-item": "path/to/@vant/weapp/dist/tabbar-item/index"
    }

    然后就是tabber的框架,我们预留了50px,并且置于最顶层(如果有遮罩层之类的可能需要另外考虑层级)

    <view style="height:50px">
      <van-tabbar active="{{ activeIndex }}" z-index="2" bind:change="onChange">
        <van-tabbar-item icon="home-o">首页</van-tabbar-item>
        <van-tabbar-item icon="tosend">课程表</van-tabbar-item>
        <van-tabbar-item icon="orders-o">公告</van-tabbar-item>
        <van-tabbar-item icon="setting-o">设置</van-tabbar-item>
      </van-tabbar>
    </view>

    然后就是子页面的容器了

    <view class='wrapper' style='margin-bottom:50px;'>
      <page0 wx:if='{{activeIndex == 0}}' bind:topage="topage"></page0>
      <page1 wx:if='{{activeIndex == 1}}'></page1>
      <page2 wx:if='{{activeIndex == 2}}'></page2>
      <page3 wx:if='{{activeIndex == 3}}'></page3>
    </view>

    上面的page0page1就是子页面的component-tag-name了,也需要在index.json里面声明,注意:这里的usingComponents需要和上面的合并

    "usingComponents": {
        "page0": "../pages/index/index",
        "page1": "../pages/classtable/classtable",
        "page2": "../pages/notice/notice",
        "page3": "../pages/setting/setting"
      }

    最后就是在tabber上的bind:change和类似于page0上的bind:topage="topage"

    bind:change是用于切换页面的,容易理解;bind:topage是为了在子页面来切换页面,也算一个坑,这里也说一下

    onChange(event) {
        // event.detail 的值为当前选中项的索引
        if (event.detail == this.data.activeIndex) return;
        this.setData({
          activeIndex: event.detail
        })
      }
    topage(event) {
        // console.log(e.detail)
        this.onChange(event)
      }

    那个在子页面切换页面的在子页面中如下实现

    this.triggerEvent("topage", 1)

    最后

    感觉微信小程序就是个无底洞,各种坑,自定义组件、只能异步请求等等。

    我这个解决方法只是其中一种,并不是最好的解决方法。