个人案例
- 获取平台证书接口报「系统繁忙」/「文件不存在」,原因分析及解决方法看这里
写在开头: 其实微信支付侧在很早~很早~~很早~~~之前就打算将平台证书抛弃改换公钥模式进行敏感信息加密和回调签名验证了,平台证书只有5年有效期,现在的开发者只会管当下“能run就行”,哪会考虑到5年之后平台证书过期的事情,导致在首次平台证书过期时间点(2023年末2024年初)很多线上平稳运营多年的系统就突然炸了,还一时不容易定位问题。 在解决平台证书问题上,微信支付推出了新的公钥模式,由于前期一直在各种线下会议上宣传,没有给出正式通知和明确的使用指引,高估了大部分开发者的情况,再加上很突然的线上灰度,盲猜内部没协调好引发新申请商户号调用“获取平台证书”接口出现已读乱回的情况,社区因此出现大量此类报错提问,可以看出很多开发着开始迷茫了:“我是谁?我在哪?为啥同样代码其他商户号都正常?为啥我’差不多‘时间申请的商户号其他的能有,就这不能用?”。 出于作为“用爱发电的热心网友”,就简单写个让大家能看得懂的说明吧。 PS:其实我们要感谢微信支付团队,给各位开发者多找点事情做,避免大家被“毕业”,要感谢他们的良苦用心~! 吐槽两句 真不知道负责这块接口设计的人是咋想的,在不知情需要获取公钥的情况下,你获取平台证书接口提示个“证书不存在”是写给谁看?写的报错提示谁能直接看得出来你想表达的意思?脑回路清奇,建议拉出去弹吉他十分钟!!! 问题说明 微信支付对于新申请商户号以及平台证书过期商户已不再签发平台证书,需要更换使用微信支付平台公钥进行敏感信息加密、通知回调签名验证,因此此类商户号调用获取平台证书接口时会出现报错“证书不存在”或者“系统繁忙”的情况。 解决方案 目前会存在两种情况,一种是新申请商户号商户后台没有“平台证书”管理入口,另一种为存量商户存在有在有效期的平台证书和平台证书过期没有签发,第一种可以直接在商户后台->账户中心->API安全->启用“微信支付公钥”,下面教程主要以存量商户切换公钥进行说明(懒得申请新商户号了,拿个白名单商户来写的)。 1.1获取商户对应的平台公钥 商户后台->账户中心->API安全->申请“微信支付公钥“,在点击申请的时候会提示你查看指引,要点查看才可以进行公钥申请!!! [图片] 下载公钥 [图片] 点击“下载公钥”后会自动下载文件名为’pub_key.pem’的公钥并在后台生成“PUB_KEY_ID”开头的公钥ID(丢了也没事,后台可以重复下载,公钥ID不变) [图片] 1.2 接口开发 下面所写示例说明均使用官方sdk,仅供参考 PHP 同时支持平台证书和平台公钥两种方法,在返回的wechatpay-serial值,在certs里有定义,就会自动匹配 [代码]// 从本地文件中加载「微信支付平台证书」或者「微信支付平台公钥」,用来验证微信支付应答的签名,这里直接使用前面从后台获取的微信支付平台公钥; $platformCertificateOrPublicKeyFilePath = 'file:///path/to/wechatpay/certificate_or_publickey.pem'; $platformPublicKeyInstance = Rsa::from($platformCertificateOrPublicKeyFilePath, Rsa::KEY_TYPE_PUBLIC); // 「微信支付平台证书」的「证书序列号」或者是「微信支付平台公钥ID」 // 「平台证书序列号」及/或「平台公钥ID」可以从 商户平台 -> 账户中心 -> API安全 直接查询到,这里直接写前面从后台获取的微信支付平台公钥ID,注意要带上'PUB_KEY_ID_' $platformCertificateSerialOrPublicKeyId = 'PUB_KEY_ID_0114232134912410000000000000'; [代码] java 将原“RSAAutoCertificateConfig.Builder”配置改为使用“RSAPublicKeyConfig.Builder”,publicKeyId填写前面从后台获取的微信支付平台公钥ID,注意要带上’PUB_KEY_ID_’ [代码]// 可以根据实际情况使用publicKeyFromPath或publicKey加载公钥 Config config = new RSAPublicKeyConfig.Builder() .merchantId(merchantId) .privateKeyFromPath(privateKeyPath) .publicKeyFromPath(publicKeyPath) .publicKeyId(publicKeyId) .merchantSerialNumber(merchantSerialNumber) .apiV3Key(apiV3Key) .build(); [代码] go 仅可使用微信支付的公钥验证应答和回调的签名,使用公钥ID初始化 [代码]var ( wechatpayPublicKeyID string = "PUB_KEY_ID_0114232134912410000000000000" // 微信支付公钥ID ) wechatpayPublicKey, err = utils.LoadPublicKeyWithPath("/path/to/wechatpay/pub_key.pem") if err != nil { panic(fmt.Errorf("load wechatpay public key err:%s", err.Error())) } // 初始化 Client opts := []core.ClientOption{ option.WithWechatPayPublicKeyAuthCipher( mchID, mchCertificateSerialNumber, mchPrivateKey, wechatpayPublicKeyID, wechatpayPublicKey), } client, err := core.NewClient(ctx, opts...) // 初始化 notify.Handler handler := notify.NewNotifyHandler( mchAPIv3Key, verifiers.NewSHA256WithRSAPubkeyVerifier(wechatpayPublicKeyID, *wechatPayPublicKey)) [代码] 1.3以上是使用sdk的参考,不使用sdk的情况下,可以参考下面链接内的示例(应该能跑,没测试):点我查看 1.4 开发对接完成后,在商户后台更换验签方式 此操作可在灰度完成之前操作终止,操作需要超管进行操作并进行安全验证,更换以后回调灰度进度由平台控制在7天内完成,应答进度由商户请求参数控制,更换完成后才可以进行平台证书作废操作。 [图片] 就这样吧,有什么问题跟帖回复
10-30 - 微信开发者工具使用的域名列表
登录相关 https://mp.weixin.qq.com https://open.weixin.qq.com https://long.open.weixin.qq.com https://lp.open.weixin.qq.com 主服务器 https://servicewechat.com CDN https://dldir1.qq.com https://res.wx.qq.com https://res.servicewechat.com (基础库下载地址) 云开发相关 https://tcb.cloud.tencent.com https://scf.tencentcloudapi.com https://flexdb.tencentcloudapi.com https://tcb.tencentcloudapi.com 真机调试 wss://wxagame.weixin.qq.com 帧同步服务器 https://long.wxagame.weixin.qq.com 上报相关 https://cube.weixinbridge.com https://repot.url.cn
2023-05-23 - 小程序、公众号、第三方平台等场景下,微信服务器推送的消息如何解密|NodeJS
本篇是对微信官方文档进行补充,一般需要消息解密的场景主要有如下几种: 第三方平台的消息推送:https://developers.weixin.qq.com/doc/oplatform/Third-party_Platforms/2.0/api/Before_Develop/message_push.html网站应用的消息推送:https://developers.weixin.qq.com/doc/oplatform/Website_App/WeChat_Login/message_push.html移动应用的消息推送:https://developers.weixin.qq.com/doc/oplatform/Mobile_App/WeChat_Login/message_push.html公众号接收用户消息:https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_standard_messages.html公众号接收事件推送:https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Receiving_event_pushes.html小程序接收用户消息和事件:https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/customer-message/receive.html你需要按照各自指引文档,开放你的服务器 http 服务,通过在管理后台配置你服务器的指定路径和加密参数,来和微信服务确定消息的沟通事宜。 [图片] 一般涉及到加密消息的只有 Token 和 EncodeingAESKey 两个,只需要保证你的服务器加解密的信息和微信服务的一致即可,所以这个你可以指定。 指定加密信息和服务器路径之后,微信服务器会按照你的加密信息对原始数据进行加密,然后向你指定的 http 服务路径中发送 post 请求。 当你的服务器收到微信服务器发来的请求时,你应该能得到直接拼在请求路径里的 query 参数 比如你的路径为:https://www.example.com/wxmp/ 那么实际收到的应该是: https://www.example.com/wxmp/?signature=f464b24fc39322e44b38aa78f5edd27bd1441696&echostr=4375120948345356249×tamp=1714036504&nonce=1514711492 你可以将 query 参数拿出来,以备后面解密使用。 另外在请求的 body 中,是一个 base64 编码数据(如果你选择的明文模式,收到的可能是明文) 你需要先解析一下这个 base64 数据,得到一个 xml 或者 json 类型的明文数据,里面有两个东西: Appid,这个消息应该所属的应用id(小程序、公众号、第三方平台、移动应用、网站应用、企业微信等)Encrypt,真正的消息体(加密过的,我们后面需要解密)接下来我们就对比 Appid 是否是我们的应用,如果是则继续处理消息体;如果不是就直接丢掉。 处理消息体前,我们需要先验证一下真实性,对 Encrypt 内容进行签名(需要用到配置的 Token,以及 query 里的 timestamp、nonce ),看下是否和 query 参数里提供的 signature (前面如果有加密的话,这里应该是 msg_signature)相同,不同则证明被篡改或者解密信息对应,也没必要继续解了。 如果签名一致,我们就可以继续解密 Encrypt 内容。 解密需要用到一开始配置的 EncodeingAESKey ,AES 使用 CBC 模式,具体的可以直接参考代码: 代码包下载:https://wximg.gtimg.com/shake_tv/mpwiki/cryptoDemo.zip 上面链接代码包里没有 NodeJS,考虑到相当部分的开发者都用 NodeJS 来开发服务端,所以这里提供最小代码,方便参照设计自己的解密模块。 代码可以直接复制下来运行,需要额外安装 xml2js 模块,最后部分为测试实例(里面的 token 和 key 信息都脱敏过,不需要担心) const crypto = require('crypto') const xml2js = require('xml2js') const xmlparser = new xml2js.Parser() const ALGORITHM = 'aes-256-cbc' // 使用的加密算法 const MSG_LENGTH_SIZE = 4 // 存放消息体尺寸的空间大小。单位:字节 const RANDOM_BYTES_SIZE = 16 // 随机数据的大小。单位:字节 /** * 解密数据 * @param {*} encryptdMsg 加密消息体 * @param {*} encodingAESKey AES 加密密钥 * @returns */ function decode (encryptdMsg, encodingAESKey) { const key = Buffer.from(encodingAESKey + '=', 'base64') // 解码密钥 const iv = key.subarray(0, 16) // 初始化向量为密钥的前16字节 const encryptedMsgBuf = Buffer.from(encryptdMsg, 'base64') // 将 base64 编码的数据转成 buffer const decipher = crypto.createDecipheriv(ALGORITHM, key, iv) // 创建解密器实例 decipher.setAutoPadding(false) // 禁用默认的数据填充方式 let decryptdBuf = Buffer.concat([decipher.update(encryptedMsgBuf), decipher.final()]) // 解密后的数据 const padSize = decryptdBuf[decryptdBuf.length - 1] decryptdBuf = decryptdBuf.subarray(0, decryptdBuf.length - padSize) // 去除填充的数据 const msgSize = decryptdBuf.readUInt32BE(RANDOM_BYTES_SIZE) // 根据指定偏移值,从 buffer 中读取消息体的大小,单位:字节 const msgBufStartPos = RANDOM_BYTES_SIZE + MSG_LENGTH_SIZE // 消息体的起始位置 const msgBufEndPos = msgBufStartPos + msgSize // 消息体的结束位置 const msgBuf = decryptdBuf.subarray(msgBufStartPos, msgBufEndPos) // 从 buffer 中提取消息体 return msgBuf.toString() // 将消息体转成字符串,并返回数据 } /** * 生成签名 * @param {*} encrypt 加密消息体 * @param {*} timestamp 时间戳 * @param {*} nonce 随机数 * @param {*} token 令牌 * @returns */ function genSign (encrypt, timestamp, nonce, token) { const rawStr = [token, timestamp, nonce, encrypt].sort().join('') // 原始字符串 const signature = crypto.createHash('sha1').update(rawStr).digest('hex') // 计算签名 return signature } // --------------------------- 以下为测试代码 --------------------------- // 与微信后台的 encodingAESKey 保持一致 const encodingAESKey = "KE66t5p56ig2rz66Ep2H6bTiitgP2ib26GbuT9tumBB" // 与微信后台的 token 保持一致 const token = "fjKCjCksHwYwHZWARPgtjKKYZyRAstkr" // 你的服务器会收到微信服务器发送的消息,这是 query 参数 const query = { signature: 'eab668051feb6330f0f84e9a9b0bc8d9d972c049', // 有些平台会对加密消息体再做一层 base64,这里就是此 base64 的签名,否则就是真的加密消息体的签名 timestamp: '1721800206', // 签名要用 nonce: '1727131331', // 签名要用 encrypt_type: 'aes', // 有些平台会有特殊加解密方式,请参考实际文档,默认 aes_cbc msg_signature: '31c02ec801f178b72e712427a64a59be01ebec5a' // 有些平台会对加密消息体再做一层 base64,所以这里有提供就是真的加密消息体的签名 } // 你的服务器会收到微信服务器发送的消息,这是 body 参数,绝大部分会是 xml 消息体,有时会做一层 base64 编码 // 需要先 base64 解码,然后得到 xml 解析,xml 中会包含 Appid 和 Encrypt 两个,你需要验证 Appid 是你自己的,并且只取 Encrypt 中的内容就好,下面是内容。 const body = 'Sk+nxj+6bfJYOu1zmckuQhxVPaT9IRHA3C4wmNAJvYa7oBmxBg6nETY8dv7nWw9O8N2VbJYcANjBfDeGXOySKweRlEUHbDEzsHprI3A1WY6H73v4lg97TPyTudYVNDvY6ve4hEwlkxgx8WOl7Jd9HbpuIZJ4711CQuJQNuQyONkBFBXxQXuyIurthDv9/pxKYwnmV/NZQF6LKyuezAfjyoz35lqK7QgnfCW6kAB5wAiYOaW0CK4/4b9jo9J40jPumvs4d9Wa+t4Xzh1a8R6ogCTlw6EQSKGqEbKblSI8p0Dc2QDzEuRO+1qlcnoHmaacf14aL41Y148n6lObt9YPsTWI4CZNA0C4P7mPVsWd1v63vHTk4shI6VanpTruIjhdAEiWbeg+rFU5CQj5+0V0QJpQOQasFW9T4VW2m8s85isB9f4A91Q3+6bAoWlHBqj6c8tAnQzxtjMAJZDX2SNCov+B3kICZke2FLE+OhUVNxnsaETJ++EFUTwE7kOzgSGRen9Shnsi3CHEHSDLtveUavzfS33RWQzRSTGsZc++0ZNKrI1pgVR4Oew4WQgHGDxlRiVPeNvPFMD36V72ZE/0BSLdW3It/9TK34rLTx6xv7952VJ/gYKbTq0LBvvrt6YaFMJdPsnZlix8TUfNXRZN/MNsNJPOj4B+PgkU5duxbYc0bHvcA7oUlWnm3vSpIHaGBBsrsRaMAXLqluky9zM0l4fcS93Cyjcgt/eS7QkZWVpLKrtBkj2OmN0plBJNv8MiRcM/GX0BcHSBlyXeXbO/4vkkTRgKGbjLAWRBff8iP9j0RziTyVZ/i08QO79k0dXPrY/SON4wtVagHdU2FZ8pQA==' // 你需要自己计算一下消息的签名,并和微信服务器计算出来的签名进行比较。 const signature = genSign(body, query.timestamp, query.nonce, token) console.log('本地计算的签名:', signature) console.log('微信服务器计算的签名:', query.msg_signature) // 如果相同,说明消息是微信服务器发来的,否则说明消息不是微信服务器发来的 if(signature === query.msg_signature){ const result = decode(body, encodingAESKey) console.log('解密后的数据', result) // 如果是 XML 格式,可能比较难处理,所以下一步转换为 JSON,如果指定微信服务器发送JSON格式,可以忽略 xmlparser.parseStringPromise(result).then(res=>console.log('最终数据', res.xml)) } else { console.log('签名不一致') }
07-25 - 长列表:按需渲染vs回收创建
在 Skyline 支持了长列表按需渲染之后,还是有很多开发者对于按需渲染表示疑惑: 开发者A:scroll-view 下拉不太流畅 开发者B:list-view 有什么作用呢? 开发者C:关于长列表的按需渲染功能,我们如何能检测到这个功能正确触发了呢? 关于以上几个问题,我们一一来解答: Q:scroll-view 下拉不太流畅? 当发现 scrll-view 下拉不够流畅时,可能是用法不对导致的不流畅。 根据 type 不同,按需渲染的用法也不同,建议按以下方式检查一下 type="list" : 根据直接子节点是否在屏来按需渲染type="custom" : 只渲染在屏节点,对于列表、网格、瀑布流等,子节点必须包裹在 list-view、grid-view 内部才会按需渲染。 Q:list-view 有什么作用呢? 对于 list-view、grid-view 等 *-view 组件,符合规定的写法则会按需渲染。 默认情况下,视口外节点不渲染。也可以根据业务需要,设置 scroll-view 的 cache-extent 指定视口外渲染区域的距离来优化滚动体验和加载速度。 [图片] 当然 cache-extent 越大也会提高内存占用且影响首屏速度,建议大家按需启用。 Q:关于长列表的按需渲染功能,我们如何能检测到这个功能正确触发了呢? 当使用按需渲染时,例如下面用的 type="list",其实直接子节点都是一开始就创建的,所以没有办法从开发者工具检查到这个功能正常触发。 [图片] 不过可以在真机上开启 “开发调试 - Debug Skyline - checkerboardRasterCacheImages” 调试 [图片] 当滚动 view 离开屏幕回来之后颜色变了,说明节点重新渲染了,以此来确认按需渲染功能正确触发 👇例如下图中第一个节点,一开始是紫色,当离开屏幕重新滚动回屏幕时,变成了黄色,证明按需渲染成功~ 注意:不是所有的组件都会形成 RasterCache,需要结构复杂一些才会; [图片] *-builder 组件 除了 *-view 组件,很多开发者应该也注意到了 *-builder 组件 list-view 对应 list-buildergrid-view 对应 grid-builder看文档描述的能力是一样的,但是为什么会分成两个组件呢? 因为目前 *-view 组件是按需渲染,节点还是会不断的创建,当长列表越来越长时,内存占用会越来越多。 于是我们新增了 *-builder 组件来支持 scroll-view 的可回收,可以更大程度降低创建节点的开销。 我们来看下效果,可以从开发者工具的 wxml 看到,当列表滚动时,list-builder 中渲染的 view 节点只有在屏的几个 [图片] 除了使用 wxml 板块查看之外,*-builder 组件还提供了监听事件,开发者可以监听列表创建和回收 binditembuild:列表项创建时触发,event.detail = {index},index 即被创建的列表项序号binditemdispose:列表项回收时触发,event.detail = {index},index 即被回收的列表项序号 使用场景既然 *-builder 组件拥有回收+创建能力,是不是可以不用 *-view 组件啦? 当然不是啦~~~ 回收+创建能力本身就是有开销的,所以也要根据业务场景按需使用哦 *-builder:对于长列表、无限滚动列表等,或者节点内存占用高的,每个时刻都确保不会有太多节点创建出来,使用 *-builder 可以节省内存*-view:对于短列表,或者内存占用不高的列表则比较适合使用 *-view 代码片段:https://developers.weixin.qq.com/s/rp07iKmW7UQS
05-16 - 手把手教你备案微信小程序(非个人主体备案)
备案材料准备 在提交备案前,请务必提前准备好备案所需材料,以免由于材料更新问题,导致备案需延期提交。下面将会带大家详细了解备案材料的要求,这样后续在提交时就能避免因为材料问题而导致失败。 材料示例及注意事项 [图片] 注:所有上传材料大小应不超过2M,分辨率不低于720* 1280 ,仅支持JPG、JPEG、PNG 格式 若想查看更多小程序备案材料示例,详情可查看文档 备案信息填写 备案材料准备好后,就可以前往【小程序管理后台-设置-小程序备案】提交备案申请了。下面将会详细教大家如何进行备案信息的填写,一共分为五个部分:主办单位信息填写、主体负责人信息填写、小程序信息填写、小程序管理员信息填写和上传其他信息材料。 1.主办单位信息填写 [图片] [图片] 填写说明 常见报错/问题 解决方案 ①选择地区:选择与证件地址相一致的省市区信息 该主体已在XX完成备案,请修改备案省份或注销备案主体重新备案 请核实该主体是否有在其他省份备案过,由于同主体在所有平台的备案省份必须保持一致,需修改备案省份或注销备案主体重新备案 ②主办者性质:默认与小程序主体认证信息相一致 / / ③证件类型:默认与小程序主体认证信息相一致 / / ④上传证件:按要求提供最新版证件 营业执照有效期不足 请联系工商部门更新证件有效期 ⑤企业名称:填写证件相对应名称信息 主办者与小程序主体不一致 请核实填写企业名称是否与小程序主体名称、上传营业执照名称相一致 ⑤企业名称:填写证件相对应名称信息 营业执照名称为空或者* 号 请联系工商部门更新企业名称信息 ⑥证件住所:填写证件相对应经营场所信息 【主体证件住所】工商数据对比不通过 请参考文档进行排查 ⑦证件号码:填写证件相对应统一社会信用代码信息 未查询到企业信息,请检查主体证件号是否有误 请核实填写的是否为统一社会信用代码,若无,请联系工商部门更新证件信息,不能填写其他如工商注册号等 ⑧通讯地址:填写当前主体所在的实际通讯地址(无需填写省、市、区) 通讯地址未能精确到门牌号 若无具体门牌号,需要在备注中说明情况 ⑨备注(选填):针对主体信息进行补充说明,如有可填写 / / 注:若为新建企业或近期有做信息变更,可能会存在企业工商数据更新延迟的情况,建议过段时间(5~15个工作日)再进行重试,否则无法正常发起验证流程。 2.主体负责人信息填写 [图片] 填写说明 常见报错/问题 解决方案 ①证件类型:选择主体负责人证件类型信息 / / ②上传证件:按要求提供最新版证件 / / ③负责人名称:通过上传证件自动识别,有误可自行修改 主体负责人与法定代表人不一致,且备案所在地不支持法定代表人授权 请核实填写的主体负责人是否为法人,需与营业执照信息一致,由于所属地区不支持授权,只能填写法人信息 ④负责人证件号:通过上传证件自动识别,有误可自行修改 【主体负责人证件号码】企业工商四要素核验失败 请核实填写的主体负责人名称、证件号信息是否正确 ⑤证件有效期:通过上传证件自动识别,有误可自行修改 / / ⑥手机号:主体负责人手机号码 【主体负责人手机号码】不允许被多人使用 请核实填写的手机号是否为其他人的信息,仅在同一主体下,同一个人允许为多个小程序备案,可提交一致的手机号、应急手机号及邮箱信息,否则不能出现个人信息混用的情况 ⑦验证码:主体负责人手机号码收到的对应验证码 验证码不正确 请核实验证码是否已失效,验证码有效期为10分钟 ⑧应急手机号:主体负责人的应急电话 【主体负责人应急联系方式】不允许被多人使用 请核实填写的应急手机号是否为其他人的信息,仅在同一主体下,同一个人允许为多个小程序备案,可提交一致的手机号、应急手机号及邮箱信息,否则不能出现个人信息混用的情况 ⑨邮箱地址:主体负责人的电子邮箱 / / 3.小程序信息填写 [图片] 填写说明 常见报错/问题 解决方案 ①服务内容标识:根据小程序实际运营内容选择合适的即可 小程序服务内容类型数目不能超过5个 服务内容标识是通信管局对各个行业的分类,平台部分行业类目与管局行业类目名称不完全不一致,建议根据备案小程序实际运营内容尽可能选择对应的服务内容标识,最多选择5个。 ②互联网信息服务前置审批项:根据小程序实际运营内容判断是否需要进行前置审批 如从事XXX业务,请上传前置审批文件 小程序实际运营内容涉及前置审批项,需上传对应的审批文件 ②互联网信息服务前置审批项:根据小程序实际运营内容判断是否需要进行前置审批 前置审批项必须选择“以上都不涉及” 小程序实际运营内容不涉及前置审批项,需要选择"以上都不涉及” ③备注(必填):具体描述小程序实际经营内容,主要服务内容 请在小程序备注按格式填写 请核实是否有根据备注格式进行填写,仅自行补充带星号内容即可。 4.小程序管理员信息填写 [图片] 填写说明 常见报错/问题 解决方案 ①证件类型:选择小程序负责人证件类型信息(目前仅支持身份证) / / ②上传证件:按要求提供最新版证件(目前仅支持身份证) / / ③负责人名称:通过上传证件自动识别,有误可自行修改 【小程序负责人姓名】负责人与小程序管理员不一致 请核实小程序是否未完善管理员实名信息,需参考指引文档进行补充 ④负责人证件号:通过上传证件自动识别,有误可自行修改 【小程序负责人证件号码】负责人与小程序管理员不一致 请核实小程序是否未完善管理员实名信息,需参考指引文档进行补充 ⑤证件有效期:通过上传证件自动识别,有误可自行修改 / / ⑥手机号:小程序负责人手机号码 【小程序负责人手机号码】不允许被多人使用 请核实填写的手机号是否为其他人的信息,仅在同一主体下,同一个人允许为多个小程序备案,可提交一致的手机号、应急手机号及邮箱信息,否则不能出现个人信息混用的情况 ⑦验证码:小程序负责人手机号码收到的对应验证码 验证码不正确 请核实验证码是否已失效,验证码有效期为10分钟 ⑧应急手机号:小程序负责人的应急电话 【小程序负责人应急联系方式】不允许被多人使用 请核实填写的应急手机号是否为其他人的信息,仅在同一主体下,同一个人允许为多个小程序备案,可提交一致的手机号、应急手机号及邮箱信息,否则不能出现个人信息混用的情况 ⑨邮箱地址:小程序负责人的电子邮箱 / / ⑩负责人人脸核身:小程序管理员(小程序负责人)需使用微信APP扫码,完成人脸核身 当前场景仅支持居民身份证 核实小程序管理员证件是否为大陆居民身份证,目前港澳台管理员无法进行人脸核身,建议先更换小程序管理员为中国大陆地区的人员,作为备案小程序负责人。 5.上传其他信息材料 [图片] 填写说明 常见报错/问题 解决方案 互联网信息服务承诺书:1. 广东地区:下载页面提供的模版文件,填写完整后上传提交;2. 非广东地区:点击阅读确认后提交 承诺书需加盖公章,但个体户没有公章 若个体工商户无公章,需要主体负责人手写日期+签名+盖手印+身份证号码,同时请在主体备注处备注“个体工商户无公章”。注:江苏、宁夏、福建地区,须刻章后提交备案,不接受负责人手印。 以上信息都填写完毕,就可点击提交,后续的备案审核流程可参考: [图片] 如有其他相关疑问,欢迎随时参与社区讨论。
05-28 - 小程序全局工具栏:提升用户体验的新利器
[图片] 在过去的小程序开发过程中,全局工具栏一直是开发者们急需解决的问题。然而,小程序并未直接支持此功能,开发者们只能采用自定义组件方式来实现。这种方法需要在每个页面的 wxml 中引入组件,导致大量重复代码,同时状态同步过程复杂且不够顺滑,给开发者带来了很大的困扰。 为了解决这个头疼的问题,Skyline 已经支持了小程序全局工具栏功能。这个全新的功能将大大简化开发流程,提升用户体验和操作便利性。全局工具栏可以实现跨页面渲染和状态同步,其渲染层级位于页面之上,使得用户在不同页面间切换时,工具栏的状态能得到保持,大大增强了操作的便利性。 接下来的文章中,我们将详细介绍小程序全局工具栏的使用流程,希望能帮助开发者们更好地理解和应用这一强大的功能~ 使用步骤通过以下三个步骤,你将能够轻松实现全局工具栏的功能: 1、配置信息:首先,在 app.json 文件中添加 appBar 选项,如下所示: { "appBar": {} } 2、添加 appBar 代码文件:在代码根目录下添加入口文件,包括以下四个文件: app-bar/index.js app-bar/index.json app-bar/index.wxml app-bar/index.wxss 3、编写 appBar 代码:使用自定义组件的方式编写 appBar 代码。在 app-bar/index.js 文件中,创建一个自定义组件实例,并在 app-bar/index.wxml 文件中编写组件的结构。同时,可以在 app-bar/index.wxss 文件中添加样式。最后,在需要获取 appBar 组件实例的页面中,使用 getAppBar 接口获取实例,进行操作。例如: Page({ getInstance() { if (typeof this.getAppBar === 'function') { const appBarComp = this.getAppBar(); appBarComp.setData({}); } } }); 通过以上三个步骤,你已经成功实现小程序全局工具栏,从而简化开发流程并提升用户体验和操作便利性~ 注意事项和限制在使用小程序全局工具栏时,我们需要关注以下几点注意事项和限制,以确保功能的正常运行: app-bar 组件仅支持 Skyline 渲染,Webview 渲染不支持,需确保项目采用 Skyline 渲染引擎以实现全局工具栏功能。在 Webview 渲染和 Skyline 渲染之间混跳时,全局工具栏的状态同步会受到影响,需要注意在跳转页面时处理状态变化。由于全局工具栏的渲染层级位于页面之上,需要避免与其他页面元素发生层叠覆盖,确保留出足够的空间容纳全局工具栏,避免影响用户体验。全局工具栏功能需要 v3.3.1 及以上的小程序基础库版本支持。遵循以上注意事项和限制,开发者将能够更好地利用小程序全局工具栏功能,提升用户体验和操作便利性。 案例展示我们通过实际案例来介绍小程序全局工具栏的应用场景~ 以微信学堂为例,我们可以使用全局工具栏来实现课程播放栏的功能。在课程播放的过程中,我们可以浏览其他课程、查看课程介绍等等。在这种情况下,实现一个全局的课程播放栏可以很好地提升用户体验。借助小程序全局工具栏,我们可以轻松实现这一功能~ [图片] 接下来,让我们来看看具体的代码实现~ 使用十分简单,我们参照使用步骤建立 app-bar 组件,然后我们只需要在 app-bar 组件中实现我们的业务代码即可。 我们的课程播放器还结合了手势和 worklet 动画,实现了最小化和最大化全局工具栏的效果,用户可以根据自己的需要来切换全局工具栏的大小,提高用户使用体验,实现更加丰富和高效的用户界面~ 在 app-bar/index.wxml 中,实现的代码与我们平时写自定义组件无异,可以根据业务诉求实现自定义的组件,我们这里展示了课程播放器的实现。 <!-- app-bar/index.wxml --> <vertical-drag-gesture-handler worklet:ongesture="handleVerticalDrag"> <view class="expand-container"> <!-- 最大化展示:nav-bar --> <view class="nav-bar column"></view> <!-- 跟着手势变化,改变组件的展示效果 --> <view class="cover-area" style="height: {{maxCoverSize}}px;"> <view class="row " bind:tap="expand"> <!-- 跟着手势改变宽高 --> <image class="cover" mode="aspectFill" src="{{musicCover}}" /> <!-- 最小化展示:标题、按钮 --> <view class="title-wrap row-between"> <view class="title column"> <text overflow="ellipsis" max-lines="1">Skyline 渲染框架入门与实践</text> <text class="name" overflow="ellipsis" max-lines="1">小程序技术专员 - binnie</text> </view> <view class="row"> <image class="icon" style="margin-right: 24px;" src="/assets/play.png" /> <image class="icon" src="/assets/next.png" /> </view> </view> </view> </view> <!-- 最大化展示:详情内容 --> <view class="music-title">...</view> <view class="footer">...</view> </view> </vertical-drag-gesture-handler> 因为我们在 wxml 中使用了手势系统,所以我们需要给手势加上手势事件,这样我们就实现了一个顺滑的课程播放器啦~ 如果你还没有学习过 worklet 动画和手势系统,建议可以学习一下 worklet 动画 和 手势系统,了解之后你将更好地理解我们在课程播放器中使用的手势和 worklet 动画的实现方式,同时,worklet 动画和手势系统也可以帮助你实现更加复杂和丰富的用户界面。 Component({ lifetimes: { attached() { const progress = shared(0) // 通过 worklet 改变各个组件的状态 this.applyAnimatedStyle('.cover', () => { 'worklet' ... }) this.applyAnimatedStyle('.expand-container', () => { 'worklet' ... }) this.applyAnimatedStyle('.title-wrap', () => { 'worklet' return { opacity: 1 - progress.value } }) this.applyAnimatedStyle('.nav-bar', () => { 'worklet' ... }) this.progress = progress } }, methods: { // vertical-drag-gesture-handler 绑定的手势事件 handleVerticalDrag(evt) { 'worklet' if (evt.state === GestureState.ACTIVE) { const delta = evt.deltaY / screenHeight this.handleDragUpdate(delta) } else if (evt.state === GestureState.END) { const velocity = evt.velocityY / screenHeight this.handleDragEnd(velocity) } else if (evt.state === GestureState.CANCELLED) { this.handleDragEnd(0.0) } }, // 手势变化时 handleDragUpdate(delta) { 'worklet' const curValue = this.progress.value const newVal = curValue - delta this.progress.value = clamp(newVal, 0.0, 1.0) }, // 手势结束时 handleDragEnd(velocity) { 'worklet' ... if (animateForward) { this.progress.value = timing( 1.0, { duration: 200, easing: animationCurve, }) } ... } } }) 以上是我们实现课程播放器的核心代码,如果您想要进一步了解和实践,可在 PC 端点击 代码片段 进行调试。 总结小程序全局工具栏为用户提供了更加便捷的操作方式和更丰富的功能,让小程序的使用体验得到了显著提升。通过全局工具栏,用户可以在不同页面间快速切换,轻松访问常用功能,同时还可以自定义工具栏,满足个性化需求。此外,全局工具栏还可以帮助开发者提高用户粘性,提升小程序的活跃度。 小程序全局工具栏适用于多种场景,如电商平台、在线教育、新闻资讯、工具类等。例如,电商平台可以将购物车、订单等入口放置在全局工具栏上,方便用户随时查看和管理;在线教育平台可以将课程列表、学习记录等功能放置在全局工具栏上,方便用户快速找到所需内容。 小程序全局工具栏是一个非常实用的功能,可以帮助用户更好地使用小程序,同时也为开发者提供了提升用户体验和增加用户粘性的机会。我们邀请大家尝试将小程序全局工具栏这个能力应用到自己的小程序上,为用户带来更好的使用体验~
02-21 - 【解决方案】交易类小程序规范受限,云开发微信支付无法使用问题
最近受平台规范监管影响,关于部分自营交易小程序受到无法绑定和正常使用云开发微信支付,本文详细介绍相关解决方案思路。 引用 Memory的社区回答: 根据2022年12月25号更新的「交易类小程序运营规范」,适用于符合如下一种或多种情形的小程序: 1.1、 小程序内提供珠宝玉石、3C数码等商品在线销售及配送服务; 1.2、 小程序的账号主体为近一年内新成立的企业或个体户主体; 1.3、 小程序的账号管理员、运营者等角色,与其它高风险小程序存在关联 ; 1.4、 小程序内经营预售商品。 如符合以上情况的,需在用户主动/系统自动确认收货后(在用户点击“确认收货”、或商家录入发货信息后达到系统自动确认收货的时间周期),进行资金结算。 目前银行、支付机构、渠道商、普通服务商及从业机构特约商户五类商户暂不支持接入及授权,所以商家自营的小程序暂不支持和上述类型的商户号关联。 云开发原微信支付走的是授权特殊服务商,命中上面规则就不能走服务商模式了,所以无法绑定。 一、影响判断和后台操作关于如何判断是否受影响,以及受影响后具体的操作步骤可以参考此指南: 一文教你处理“受平台方要求暂时冻结资金"问题 其中关于微信支付接口的对接需要单独开发,对于原云开发微信支付使用开发者来说,独立开发门槛如果太高,可以直接使用云开发最新的工作流解决这个问题,接下来具体的介绍相关的操作步骤 二、使用工作流完成微信支付对接目前云开发推出专门用来接入微信支付的云模板,可以直接使用,详情参考: 微信开放文档|微信云开发云模板-微信支付模板 微信学堂|微信支付模板课程 工作流是云开发推出的服务端业务逻辑的可视化编排工具,帮助开发者更清晰、灵活、高效地组织和管理业务逻辑。 具体的流程图如下: [图片] 详细操作步骤请参考此系列文档: 工作流实现微信支付付款、接收事件回调工作流实现微信支付退款场景 三、总结除了上述微信支付相关支持外,工作流还能做更多,比如: 公众号的普通消息监听 和 事件推送小程序的客服消息与事件监听 和 发货信息管理服务消息 工作流主要有以下特性: 可视化:开发者通过拖拉拽方式以及简单的节点完成配置与调试,快速开发业务流程,使其更直观和易于理解,同时便于团队成员之间沟通和协作;可扩展:工作流提供基于云函数的脚本节点,支持用自定义代码实现复杂逻辑,满足各种不同的业务场景和需求。 如果你在使用中有任何疑点或者寻求方案,可以联系微信云服务的架构师。 关于本文中提出的观点和内容,如果你有其他补充和意见,欢迎在文章下留言~
06-12 - 怎么建设小程序的后台比较好?|零基础探索指南
一、写在前面 我比较喜欢做微信生态的解决方案,给企业或个人提供一些技术方案和应用形态选型的咨询。最近跟一些朋友聊天,他们在各自的工作岗位中,随着工作熟悉就会自然而然有很多能提升工作效率的点子。 有些人喜欢用文档、问卷、表单等 SAAS 应用捏合一个自己的效率集,一般能满足很多场景;但还有一些场景可能没有合适的 SAAS 应用,于是就想搞个小程序,然后来问我怎么做比较合适。 “小程序一定程度上降低了开发者搭建应用的门槛,做一个小程序相比其他的产品形态效率更高,落地更快。”,这个观念在这几年的发展中越来越深入,有很多不是软件行业的朋友都能提出小程序这种想法。 通过简单的代码,做一个满足自身小场景的小程序,并不是很难。有一定的数理基础,配合社区的入门教程很容易上手。上手门槛已经降低到中小学生都能触及到的程度了,微信还针对的搞了个少年挑战赛。 完整的闭环一个应用,单纯用小程序做个前台还是不够的,很多情况下我们需要有后台做管理支撑。 比如一个登记小程序,除了小程序能够提供登记信息填写外,还需要一个后台能够承载管理人员做状态扭转或者其他的后备工作。 一般这种情况下有几种解决思路: 1. 在小程序中编写管理页面,通过用户 openid 来指定仅管理员可见。 2. 通过接口对接企业微信或者其他 SAAS 平台的 webhook 能力来组合管理。 3. 搭建一个可操作的管理后台,有细粒度的权限管控。 接下来我主要分析一下这几种方式的实现和利弊,你可以根据自身的需要有针对的选型。 二、后台方案对比 1. 在小程序中编写管理页面,通过用户 openid 来指定仅管理员可见。 实现方式:编写单独的管理页面,或者在业务页面中嵌入一些管理按钮,每次加载时,通过后端比对 openid 返回给小程序端是否可以展示管理页面或内容。方案优势:小程序开发难度,低门槛,实现简单。方案劣势:管理权限的授予以及可靠性维护太差,浪费资源。 ○ 管理者的身份需要设计机制去确认(手机号关联or访问记录) ○ 管理页和内容占用小程序包资源,影响普通用户的使用体验 ○ 正常用户访问也会判断管理员逻辑,浪费数据库读资源(硬编码则维护难度增大) ○ 管理接口与业务接口有混用的风险 [图片] 这种实现是大部分初入者首选的,因为微信用户体系比较完备,可以通过 openid 来确定一个微信用户。在用户打开小程序时,服务端判断用户身份是否是管理员,如果是的话返回一些信息给小程序,小程序对应的展示相关管理页面内容或入口。 劣势也是非常明显的,由于管理逻辑与业务逻辑都集中在小程序通信,会在身份判断,业务处理上有更多的无效的数据库读请求,浪费资源。 [图片] 2. 通过接口对接企业微信或者其他 SAAS 平台的 webhook 能力来组合管理。 实现方式:暴露一些接口,然后使用企业微信的机器人或其他工具对接暴露的接口,实现通过消息或者点击驱动接口完成一些配置。比如访问接口地址填写一些特定参数来实现状态扭转(https://www.example.com/api/status?id=10001&type=success&key=1212)方案优势:只需要提供接口即可,几乎没有前端的开发成本。方案劣势:可读性比较差,并且有被攻击的风险,接口安全需要下功夫。 ○ 可读性比较差,无法可视化,但可通过其他工具加以改善。 ○ 由接口的参数驱动,权限校验为明码,容易泄露。 ○ 应对管理场景有限,无法应用到复杂的管理操作(如上传图像或批量处理) [图片] 这种方案一般用于非常简单的管理场景中,比如就只针对一个事项做状态扭转,发起一个批处理的任务,发送通知等。完全由接口来驱动完成。但权限管控只能通过参数来明码输入,或者通过结合 saas 工具,来利用其调用时的 header 信息来做权限判断。 [图片] 3. 搭建一个可操作的管理后台,有细粒度的权限管控。 实现方式:搭建独立的 WEB 后台(或APP ),来统一设计用户管理权限,以及搭建各种业务的管理流程。尽量不在接口层面与业务端有交集,仅做数据层面的处理和互通。方案优势:灵活度大,可以实现任意后台管理场景,并且可以接入丰富的权限管控。方案劣势:搭建难度大,处理的开发流程复杂,可能需要重新设计一套用户体系。 ○ 搭建难度可能远远超过小程序前台,需要考虑投入是否值得。 ○ 根据所需可能要对接不同的用户登录体系,比如用户名密码+微信登录+邮件登录。 [图片] 这是一个标准的方案,业务后台页面的构成和功能设计需要根据自己的业务场景来单独设计,其用户群体为业务管理者。在功能实现上既可以有数据库的增删改查,也可以有外部平台的调用(比如微信支付分账,发起退款,发送邮件通知等)。作为开发者在搭建后台时,遇到的实现点和难度可能都是高门槛的,所以需要一定的技术积累,并充分理解用户群体(业务管理员)的工作流程。 [图片] 三、微信在后台方面做了什么? 为了让开发者在做个小程序这方面的门槛降低,从设备端到用户体系,从单机存储到联网同步。微信团队围绕小程序开发的各个环节提供了很多产品和服务形态,其核心目标就是让开发者做小程序的门槛变得更低。 比如微信云服务,Donut 开发平台,通过提供云端资源和平台能力,在一些方面多做一些,让开发者少做一些。 在业务后台这个方面,实现上脱离小程序本体,但是从业务经营者角度来看,又是小程序业务不可或缺的一环。所以微信针对业务后台这方面也提供了解决方案: 云后台是微信最近上线的一个能力,主要面向小程序或公众号 H5 场景的业务开发者,沉淀常见的后台管理场景;通过提供丰富应用模板、开放数据接口、灵活管理权限等功能,降低开发者搭建后台的成本,提升开发效率。 [图片] 通俗来解释的话主要就有 3 个亮点: 1. 丰富的模板应用和灵活的搭建驱动。 除了我们常需要的 CMS 内容管理,还提供常见的商品管理,订单管理,数据分析,支付管理等等。如果这些模板不能满足你的场景或者场景没覆盖全,你可以直接去编辑应用,用低代码编辑器来搭建自己心目中的理想应用。[图片] 2. 可以自由定义的数据接口和外部平台连接 云后台自动携带一个中心化数据源,并提供完备的数据操作接口。如果你自己有自建的数据源,可以在云后台自由配置连接,直接对接到各个后台应用提供针对微信开放平台、微信支付平台的现成连接接口,只需要配置令牌信息就可以直接用,不需要花时间再做调试。[图片] 3. 提供灵活的用户权限管理,细粒度控制 云后台内置用户权限体系,可以针对不同业务管理场景创建角色,并赋予特定的应用可见和可操作权限。提供丰富的认证源,比如微信扫码登录,或者自建的认证源。[图片] [图片] 其中我觉得对初学者友好的一点是,如果你只开发了一个单机小程序,还没有做云端接入。云后台由于有中心化数据源,也就意味着你不需要自己搭建后端服务了,直接使用云后台暴露出来的接口调用就好。 [图片] [图片] 有关于云后台的具体使用细节的动态体验,我会在后面单独开一篇文章,发布后会更新到这里,如果感兴趣可以关注下。 四、总结 如果你目前正在为业务后台的搭建烦恼,或者有计划升级你的后台形式,不妨可以花点时间体验一下,具体请参见云后台文档。 如果你有小程序前后端开发的疑点或者寻求方案,可以联系微信云服务的架构师。 关于本文中提出的观点和内容,如果你有其他补充和意见,欢迎在文章下留言一起探讨~
2023-12-11 - “获取手机号收费标准”组件公布了套餐,那么获如何选择套餐?
获取手机号组件如何选择套餐,怎么选择呢? [图片] 请广大开发者,根据自己的额度选择最佳效率的。 比如我的小程序每月2.4w,综合下来选择最优先考虑的不是单价是合适的月份消耗(仅供参考)。我会选择以下几个套餐来充值。 [图片] [图片] 以下表格针对个人开发者参考(如需要原表格建议请评论区联系) 价格优惠排序版本 [图片] 时间排序版本 [图片] 针对服务商部分建议关注帖子。作者会在收到套餐后更新/
2023-06-28 - Skyline|电商小程序 留住用户秘诀
你是否也收到这样的用户反馈? 商品列表滚动区域太小,很难找到想要的商品。头部的搜索广告占据了半个屏幕,挤占了实际空间。在我手机这样小的屏幕上,展示区域太小了,能否把它放大点?[图片] 在电商页面中,我们需要向用户展示众多的商品、广告等信息。 然而,如何在有限的屏幕空间中更好地展示它们,是一个需要我们深入思考的问题。 由于小程序 webview 渲染框架在技术存在一定的局限性,我们需要在不同的设计之间进行抉择。 当广告具有较高的优先级时,我们会考虑突出广告的展示,同时减小商品列表在界面中所占比例。当商品列表具有较高的优先级时,我们会考虑优先展示商品,而放弃广告的展示。[图片] 但是当广告和商品列表同样重要的情况下,要怎么办呢? 常见的一种设计方式是设计一个隐藏按钮,当用户不想看广告的时候把广告隐藏掉,隐藏之后商品列表就有更多展示空间 [图片] 但是这种情况也只是针对愿意手动点击隐藏按钮的情况下,还是有一定的局限性。 那么,有没有办法做到在无形中隐藏广告呢? [图片] 说到这里,当然是可以的啦✌️ 1、吸顶布局 + worklet 轻松实现 在常见的电商小程序首页,通常是顶部展示类目、接着展示商品详情,商品详情顶部也有热门等等的分类。 当页面滚动的时候,我们希望商品详情热门分类可以吸在顶部,便于切换。 [图片] 这里我们用到了 scroll-view 的 sticky-header、sticky-section 吸顶布局容器即可轻松实现。 ... ... ... 当 scroll-view 滚动的时候,根据滚动位置把搜索框放到标题的位置,可以再节省一点空间 attached() { // nav-bar 隐藏或展示 this.applyAnimatedStyle('.nav-bar', () => { 'worklet' return { opacity: this.navBarOpactiy.value } }) // 改变搜索框宽度 this.applyAnimatedStyle('.search', () => { 'worklet' return { width: `${this.searchBarWidth.value}%`, } }) }, // scroll-view 监听函数 handleScrollUpdate(evt) { 'worklet' const maxDistance = 60 const scrollTop = clamp(evt.detail.scrollTop, 0, maxDistance) const progress = scrollTop / maxDistance const EasingFn = Easing.cubicBezier(0.4, 0.0, 0.2, 1.0) this.searchBarWidth.value = lerp(100, 70, EasingFn(progress)) this.navBarOpactiy.value = lerp(1, 0, progress) }, 2、手势 + worklet 操作更灵活 小程序新渲染框架支持了手势系统,手势在这里可以发挥大作用~ 我们可以使用手势协商让小程序页面中的广告、商品等无缝切换和更好的展示 [图片] // .wxml ... // .js handlePan(e) { 'worklet' ... if (e.state === GestureState.ACTIVE) { if (this._interactionState.value === InteractionState.UNFOLD) { // 展开状态下,往上滑才折叠起来 if (e.absoluteY - this._startY.value < 0) { this._interactionState.value = InteractionState.ANIMATING this._translY.value = timing(0.0, { duration: 250 }, () => { 'worklet' this._interactionState.value = InteractionState.RESET }) } } else { // 其它情况,跟随手指滑动 this._translY.value = e.absoluteY - this._startY.value } } // 其他状态下的处理 ... }, 加入手势之后,除了可以隐藏广告,我们还可以将一些头部信息隐藏 在用户查看商品列表时,隐藏大部分无用信息,将商品列表展示区域最大化 // 最外层 .page 往上挪 this.applyAnimatedStyle('.page', () => { 'worklet' const translY = clamp(this._translY.value, -this._tabsTop.value, 0) return { transform: `translateY(${translY}px)` } }) // 改变 .navigation-bar 背景色 this.applyAnimatedStyle('.navigation-bar', () => { 'worklet' const translY = clamp(this._translY.value, -this._tabsTop.value, 0) const opacity = translY / -this._tabsTop.value return { backgroundColor: `rgba(255, 255, 255, ${opacity})` } }) // 输入框:改变宽度并且展示 this.applyAnimatedStyle('.search-input', () => { 'worklet' const translY = clamp(this._translY.value, -this._tabsTop.value, 0) const percentage = translY / -this._tabsTop.value return { width: `${percentage * 60 + 40}%`, opacity: percentage, } }) 除此之外,我们这里可以利用手势来展示商家的一些信息 当在商品列表往下拉到顶部时,触发整个列表下拉展示出商家信息 再往上滑动则商品列表重新展示~ [图片] // 商品详情往下拉 this.applyAnimatedStyle('.main', () => { 'worklet' const translY = clamp(this._translY.value, 0, Number.MAX_VALUE) console.log(222, translY) return { transform: `translateY(${translY}px)` } }) // 简单的 header 渐隐 // 商品详情展示时,仅显示简单的 header:学堂名称和几个标签 this.applyAnimatedStyle('.header-shop-info-simple', () => { 'worklet' const min = 50 const max = 100 const translY = clamp(this._translY.value, min, max) - min return { opacity: 1 - (translY / (max - min)) } }) // 复杂的 header 渐显 // 商品详情下拉,显示复杂 header:展示热门活动、公告等等信息 this.applyAnimatedStyle('.header-shop-info-detail', () => { 'worklet' const min = 100 const max = 150 const translY = clamp(this._translY.value, min, max) - min return { opacity: translY / (max - min) } }) 加入手势动画之后,我们的页面展示对比之前有了以下的优势: 更加自然:更符合用户操作习惯,用户自然滚动屏幕时不会感到突兀更节省空间:滚动隐藏更为灵活、省空间,使页面更清爽更高效的展示:可以将要展示的内容更好的展示,无需做取舍 借助手势动画,我们可以优化小程序界面展示、提升用户体验,从而获得更高的商业价值。 如果你也想更好的留住用户,mark 下这个 源码 [ 瀑布流页面 / 分类页面 ] 直接接到到你的小程序吧~
2023-08-03 - Skyline|原生级卡片转场,小程序轻松实现
在上一篇文章《在小程序中实现原生相册》中,我们学习了自定义路由搭配共享元素实现的原生相册效果,共享元素可以让用户在体验小程序时视觉关联性更强。 除了相册实现之外,常见的卡片转场也非常适合。 [图片] ⬆️ 演示效果:默认动画 vs 卡片转场动画 👇 下面我们来看看卡片转场中通过 共享元素 + 自定义路由 来实现无痕跳转。 [图片] 这里的转场稍微有点复杂,涉及到以下 3 个点 旧卡片:图片放大、内容渐隐新页面:按比例放大、页面渐显手势搭配1、旧卡片:图片放大、内容渐隐 在本示例中,列表页采用的是 scroll-view 瀑布流布局的实现。 [图片] 这里我们的共享元素是卡片,即 grid-view 中的内容 card,卡片包括 图片、内容描述。 [图片] 默认情况下,共享元素是整个节点进行飞跃的,由于前后页面的图片元素一致但文本内容不一致, 导致在第一帧或者最后一帧会有跳动的效果。 为了让转场动画更加自然,我们需要在飞跃的过程中渐隐旧卡片的内容描述。 [图片] 在这里,我们需要先用 this.applyAnimatedStyle 来给对应的节点绑定 worklet 驱动动画。 .card_wrap 节点:整个卡片按比例放大.card_desc 节点:内容描述渐隐[图片] 关于动画执行的时机,我们可以通过配置项修改。 immediate:设置是否立即执行驱动动画flush:shareValue 更新时,applyAnimatedStyle 的 updater 函数刷新时机在本例中,需要保证共享元素的图片与目标页面图片位置重叠,所以 flush 设置 sync 在当前时间片刷新。 [图片] 绑定完驱动动画之后,我们需要给共享元素绑定帧回调事件,根据当前动画进度改变共享变量的值来驱动共享动画 [图片] 2、新页面:按比例放大、页面渐显 新页面在路由中的动画,需要在自定义路由中进行配置。关于自定义路由的更多介绍,可参考《小程序页面转场动画》 在路由动画过程中,我们将上一步的共享元素帧回调拿到 begin、end 的值,然后结合动画进度 t 计算得出新页面的位置、缩放比例。 还有根据动画进度,设置页面渐显,与前面的卡片渐隐承接。 [图片] 3、手势搭配 学习过我们前面的文章的同学都知道,自定义路由经常需要结合页面手势,来实现手势返回,关于手势的基础知识可参考《小程序页面转场动画》 [图片] 这里我们希望手势缩小整个当前页面,所以这里手势返回时只在当前页面做手势动画即可。 在页面详情页的最外层,嵌套一个手势组件 pan-gesture-handler,当手势拖动时根据手势的位置改变整个页面(通过 #fake-host 控制)的位置和大小来达到拖动的效果。 [图片] 同样绑定页面驱动动画,通过 applyAnimatedStyle 给 #fake-host 绑定驱动动画,当共享变量 transX、transY 等变化时则自动改变 transform 来驱动 #fake-host 缩小。 [图片] 接着绑定手势事件,根据手势拖动时拿到位置信息改变共享变量 transX、transY 的值。 [图片] 最后我们需要设置背景颜色透明,来达到类似把卡片拖回列表的视觉效果,更好的减少页面切换感~ [图片] 一个自定义路由的页面会有 3 层可以设置到背景色,要做到透明的效果需要将 3 个背景色都设置为透明。更多自定义路由背景色的详情参考官方文档。 [图片] 想要试试卡片转场的无恒效果~扫描 ⬇️ 下方小程序码即可体验。 如果你也想在小程序中实现卡片转场动画,mark 下这个 源码 直接接到到你的小程序吧~ [图片]
2023-08-03 - Skyline|在小程序实现原生相册的效果
相册在日常生活中经常使用到,如手机自带相册、朋友圈、商品展示图、评论贴图等等,都经常用到相册的能力。 👇下面演示 iOS 原生相册、朋友圈等相册使用效果,我们可以看到图片切换非常顺滑,视觉焦点不变。 [图片] 😭 但是在小程序中,页面切换会有明显的切换感。用户焦点会丢失,缺少视觉关联性。 [图片] 共享元素🔥 为了丰富用户交互效果、提升用户体验、增强视觉关联性,小程序支持了页面间的共享元素 下图展示有无共享元素的页面切换效果,可以看出使用共享元素之后,转场动画更灵活 [图片] 共享元素 经常作用在图片上,例如上面示例中的相册效果,是那么共享元素动画要怎么实现呢? 在页面跳转时,两个页面 key 相同的 share-element 组件则会产生飞跃的过渡效果 [图片] 在上一篇文章中,我们学习了 页面转场动画,共享元素动画跟页面转场动画是类似的,同样是在页面切换间的动画。 动画进度、时间 与 路由进度、时间保持一致(非自定义路由也支持共享元素动画) 在共享元素飞跃的过程中,前后页面图片的裁剪方式(mode) 可能不一致 这种情况下容易导致图片突然跳变,所以我们需要在飞跃的过程中改变图片的大小来保证平滑飞跃 [图片] 在共享元素动画进行的过程中,share-element 可以收到 onFrame 表示动画帧回调 我们可以在帧回调中处理内部元素的显示 例如:我们这里通过在帧回调中改变图片宽高来达到平滑飞跃的效果 // .wxml // .js // 初始化 attached() { this.aspectRatio = shared(0) this.curRect = shared(undefined) // 绑定 worklet 动画 this.applyAnimatedStyle('.img', () => { 'worklet' const curRect = this.curRect.value return { left: `${curRect.left}px`, top: `${curRect.top}px`, width: `${curRect.width}px`, height: `${curRect.height}px` } }) }, // 获取图片初始宽高比 onImageLoad(e) { const { width, height } = e.detail this.aspectRatio.value = width / height }, // 动画帧回调,调整图片大小 onFrame(data) { 'worklet' // 当前帧容器的宽高、进度等信息 const { begin, end, progress, direction } = data ... // 根据图片初始宽高比、共享元素容器、动画进度等计算出变化过程中的值 this.curRect.value = { left = lerp(begin.left, end.left, t), top = lerp(begin.top, end.top, t), width = lerp(begin.width, end.width, t), height = lerp(begin.height, end.height, t), } } 更多共享元素动画原理请查看 官方文档 手势搭配打开图片之后,我们经常需要用到手势来操作图片,如缩放、移动、双击等等 [图片] 我们上次学过的 手势系统 又派上用场啦 通过监听手势事件配合 worklet 函数即可在小程序实现图片预览效果 👇 下面演示缩放手势的处理,除了缩放之外,相册在手势处理上还有很多复杂的逻辑,包括惯性、边界逻辑判断等 点击查看更多相册相关的手势操作 // .wxml // 绑定缩放手势 let sharedValues = this.sharedValues ?? [] // .js // 绑定缩放 this.applyAnimatedStyle('#image', () => { 'worklet' // worklet 函数,sharedValues 变化时,函数会立即执行 return { transform: `scale(${sharedValues[SCALE].value})` } }) // 监听缩放 onScale(evt) { 'worklet' // 连续的手势状态 && 双指放缩 if (evt.state === GestureState.ACTIVE && evt.pointerCount === 2) { // 计算出当前真正的缩放值 sharedValues[SCALE].value = evt.scale / sharedValues[TEMP_LAST_SCALE].value sharedValues[TEMP_LAST_SCALE].value = evt.scale } } 最后,我们来看下小程序实现出来的相册跟原生相册的使用对比,在小程序也可以顺滑的实现类原生的效果啦~ [图片] 目前,同程旅行 已经上线了共享元素结合手势的相册效果,mark这个 相册源码 直接接入到你的小程序吧~ [视频]
2023-08-03 - Skyline|小程序手势:让半屏弹窗更顺滑
在小程序页面开发中,我们经常用半屏弹窗来进来内容展示,例如:微信开放社区切换主页、加入购物车的选项页、文章留言区等等。 [图片] 常见的半屏弹窗展示逻辑是这样的: 打开弹窗:点击 “打开弹窗” 按钮展示弹窗关闭弹窗:点击“关闭按钮” or 遮罩层 关闭弹窗当我们想在半屏弹窗加一些交互动画时,可以监听节点的 touch 事件来做一些手势判断,进而处理拖拽事件。但是这种方式实现的滚动动画容易卡顿,出现延迟的情况,效果并不理想。 为了丰富小程序的交互体验,我们内置了一批手势组件,可以帮助开发者更好的实现交互动画的效果。 下图演示使用手势的半屏弹窗下拉效果与普通半屏下拉的对比。 当内部评论列表往下拉到顶部时,变为半屏的下拉,可直接下拉关闭弹窗。 [图片] 我们来看下这种操作是怎么实现的 在上面评论列表的半屏弹窗中会有一个 scroll-view 滚动组件,在 scroll-view 中会有滚动事件,当滚动到顶部时,我们希望有整个半屏的下拉事件。 所以我们需要在半屏的最外层放置一个拖动手势组件 pan-gesture-handler 由于拖动组件内部的 scroll-view 也是可以滚动的,所以这里需要进行一个手势协商的处理,就是什么条件下由哪个组件来响应手势。 当手势往下 ⬇️ 滚动时,此时判断内部 scroll-view 滚动条的位置 滚动条处于顶部:外层 pan-gesture-handler 响应滚动,此时半屏往下拖动至关闭半屏滚动条不处于顶部:内层 scroll-view 响应滚动,此时内部列表往上滚[图片] 当手势往上 ⬆️ 滚动时,此时判断半屏的位置 半屏不完全打开时:外层 pan-gesture-handler 响应滚动,此时半屏往上拖动至完全打开半屏半屏完全打开时:内层 scroll-view 响应滚动,此时内部列表往下滚[图片] 我们来看一下代码的实现,这里用到的手势组件 pan-gesture-handler(拖动时触发)和 vertical-drag-gesture-handler(纵向滑动时触发),手势组件有以下属性 on-gesture-event:手势回调事件should-response-on-move:是否响应当前手势的 move 阶段simultaneous-handlers:指定需要协商的手势是哪几个,下面演示表示 pan 和 scroll 协同触发。native-view:代理的原生节点,这里 scroll-view(scroll-y) 内有个 vertical-drag 手势,scroll-view 自身无法处理,需要被代理出来 ... 接着,我们看看在页面 js 中怎么处理手势。 在手势处理的回调中因为会改变半屏的状态值,所以这里的回调函数采用 worklet 函数,worklet 函数运行在 UI 线程,使得小程序可以做到类原生动画般的体验。 // page.js // shared 创建的变量为共享变量,可在 UI 线程和 JS 线程间同步 this.transY = wx.worklet.shared(1000) this.scrollTop = wx.worklet.shared(0) this.startPan = wx.worklet.shared(true) // shouldPanResponse 和 shouldScrollViewResponse 用于 pan 手势和 scroll-view 滚动手势的协商 shouldPanResponse() { 'worklet' return this.startPan.value }, shouldScrollViewResponse(pointerEvent) { 'worklet' // transY > 0 说明 pan 手势在移动半屏,此时 scroll-view 滚动不应生效 if (this.transY.value > 0) return false const scrollTop = this.scrollTop.value const { deltaY } = pointerEvent // deltaY > 0 是往上滚动,scrollTop <= 0 是滚动到顶部边界,此时 pan 开始生效,scroll-view 滚动不生效 const result = scrollTop <= 0 && deltaY > 0 this.startPan.value = result return !result }, // pan 手势处理 handlePan(gestureEvent) { 'worklet' if (gestureEvent.state === GestureState.ACTIVE) { const curPosition = this.transY.value const destination = Math.max(0, curPosition + gestureEvent.deltaY) // 改变半屏的位置 this.transY.value = destination } // 其他手势状态的处理,如滚动结束时计算半屏处于打开还是关闭的状态 } 目前,同程旅行 已经上线了手势结合半屏的效果 体验路径:酒店查询 - 选择酒店 - 选择入住人 - 新增入住人 [图片] 普通半屏结合手势代码片段:https://developers.weixin.qq.com/s/lx0RH1mD7rGj 手势除了在普通半屏的应用之外,也可以实现分段式半屏。下面演示的分段式半屏比普通半屏的判断条件更多一些。 判断条件同普通半屏类似,根据手势方向 和 分段式半屏当前的位置来判断是响应分段式半屏还是内部列表,响应分段式半屏是改变到哪一个位置。 [图片] 这里与普通半屏不同的是我们还改变了地图的缩放级别(scale) 因为 worklet 函数是在 UI 线程运行的,当要改变 data 值时,需要通过 wx.worklet.runOnJS 调回 JS 线程。 // page.js // 设置 map scale // 运行在 JS 线程 setMapScale(scale) { this.setData({ scale }) }, // worklet 函数,运行在 UI 线程 scrollTo(toValue) { 'worklet' let scale = 18 if (toValue > screenHeight / 2) { scale = 16 } // 从 UI 线程调回 JS 线程 wx.worklet.runOnJS(this.setMapScale.bind(this))(scale) this.transY.value = timing(toValue, { duration: 200 }) }, // 处理拖动半屏的手势 handlePan(gestureEvent) { 'worklet' // 滚动半屏的位置 if (gestureEvent.state === GestureState.ACTIVE) { // deltaY < 0,往上滑动 this.upward.value = gestureEvent.deltaY < 0 // 当前半屏位置 const curPosition = this.transY.value // 只能在 [statusBarHeight, screenHeight] 之间移动 const destination = clamp(curPosition + gestureEvent.deltaY, statusBarHeight, screenHeight) if (curPosition === destination) return // 改变 transY,来改变半屏的位置 this.transY.value = destination } if (gestureEvent.state === GestureState.END || gestureEvent.state === GestureState.CANCELLED) { if (this.transY.value <= screenHeight / 2) { // 在上面的位置 if (this.upward.value) { this.scrollTo(statusBarHeight) } else { this.scrollTo(screenHeight / 2) } } else if (this.transY.value > screenHeight / 2 && this.transY.value <= this.initTransY.value) { // 在中间位置的时候 if (this.upward.value) { this.scrollTo(screenHeight / 2) } else { this.scrollTo(this.initTransY.value) } } else { // 在最下面的位置 this.scrollTo(this.initTransY.value) } } }, 分段式页面代码片段:https://developers.weixin.qq.com/s/fw0U31mI7bGf 半屏的交互除了在页面内实现,也能跨页面实现,如常见的下沉式半屏交互。其中,半屏效果与上述实现类似,而前一页面的下沉实现需要结合自定义路由 后面的文章中我们会介绍自定义路由结合手势怎么去实现下沉式半屏效果,不仅如此,还有很多类原生的页面切换效果都能通过自定义路由实现 [图片]
2023-08-03 - Skyline|小程序吸顶、网格、瀑布流布局都拿下~
在之前的文章中,我们知道了新 scroll-view 可以让小程序的长列表做到丝滑滚动~ 也提到了新 scroll-view 提供了很多新能力 sticky、网格布局、瀑布流布局等,这一篇,我们就来看看这些新能力是怎么使用的~ 新 scroll-view 在原来列表模式(type="list")的基础上,新增了自定义模式(type="custom") 在自定义模式下,新增了以下新组件供开发者调用 list-view:列表布局容器sticky-section / sticky-header:吸顶布局容器grid-view:网格布局容器,可实现网格布局、瀑布流布局等sticky布局sticky 布局即在应用中常见的吸顶布局,与 CSS 中的 position: sticky 实现的效果一致,当组件在屏幕范围内时,会按照正常的布局排列,当组件滚出屏幕范围时,始终会固定在屏幕顶部。 常见的使用场景有:通讯录、账单列表、菜单列表等等。 与 position: sticky 不同的是,position: sticky 很难实现列表滚动需要的交错吸顶效果,而 sticky 组件则可以帮忙开发者轻松实现交错吸顶的效果。 sticky 的使用非常简单: 将 scroll-view 切换到 custom 模式采用 sticky-section 作为 scroll-view 的子元素sticky-header 放置吸顶内容list-view 放置列表内容 {{item.name}} ... 我们来看下采用 sticky 布局做出来的通讯录效果~ [视频] sticky 布局也可以通过给 sticky-section 配置 push-pinned-header 来声明吸顶元素重叠时是否继续上推 像下图输入框和标签列表这种类型,标签列表吸顶时还是希望保留输入框吸顶。 [视频] 网格布局网格布局即将列表切割成格子,每一行的高度固定,常见的视频列表、照片列表等通常都采用网格布局。 在此之前,实现网格布局需要开发者自行实现网格切割,再嵌入到 scroll-view 中。 新 scroll-view 直接提供了 grid-view 组件供开发者使用~ 将 scroll-view 切换到 custom 模式采用 grid-view 类型为 aligned 做为直接子节点grid-view 中直接编写列表 ... 下面是使用网格布局实现的图片列表效果~ [视频] 瀑布流布局瀑布流布局与网格布局类似,不同的是瀑布流布局中每个格子的高度都可以是不一致的,所以在小程序中实现瀑布流布局就比较复杂了。 开发者需要通过计算格子高度,然后再进行瀑布流拼接,当滚动内容过多时还需要处理节点过多导致内存不足等问题。 grid-view 组件直接支持了瀑布流模式供开发者直接使用,grid-view 组件会根据子元素高度自动布局: 将 scroll-view 切换到 custom 模式采用 grid-view 类型为 masonry 做为直接子节点grid-view 中直接编写列表 ... 下面是使用瀑布流布局实现的图片列表效果~ [视频] 想要立即体验?现在通过微信开发者工具导入 代码片段,即可体验新版 scroll-view 组件能力~
2023-08-03 - 「微信开发平台Donut」入门指南|安全网关篇
一、 背景叙述 从公众号开始,小程序、移动应用、网站应用,到现在的视频号小店,微信提供很多种应用形态,让企业和个人开发者可以用适合的形式接入微信生态,满足自己的业务需求。 另外微信还提供第三方平台,为各行业的解决方案服务商提供了一个窗口,能够服务更多商家或中小企业个体接入微信生态,提升客户体验。 但无论是小程序、公众号还是第三方平台,依然只是在C端提供服务。无论是服务商还是企业个人开发者,想要通过上述形态接入微信生态,仍然离不开云端服务。 微信团队提供很优秀的开发和调试工具,单独做一个不联网的小程序,实现一些简单的功能,简单入门上手开发并不是很难。但如果是联网提供完整的带服务的小程序,对很多开发者来说就比较难了;更难的是另一个问:如何做一个稳定的、安全的后端服务? 2018年,微信联合腾讯云推出云开发,主要解决的是小程序后端服务的高门槛问题。但提供的云函数对传统开发模式的改造成本太大,灵活度不是很高。于是在2020年推出微信云托管,提供通用的容器服务,将服务构建环节交给开发者。 无论是云开发和云托管,最关键的并不是提供一个后端服务,而是提供后端服务的安全访问。所以可以看到,无论是云开发还是云托管,在微信应用(小程序、公众号服务)中提供的callFunction、callContainer方法,相比request方法本质上是提供了一个安全的访问入口。 云开发、云托管在一定程度上承担了运维和部署环节,使得其非常容易入门,对新手开发者很友好。但并不是所有开发者都是新手,很多成熟的开发者都有自己的一套成熟后端服务。如果为了接入微信的安全访问,而直接迁移自己的服务到云开发或云托管,阻力是相当大的。 另外云开发、云托管提供的是可独立存在的后端云服务,有独立的数据存储,使得其在调用方上挣脱不开同主体的限制,这也是服务商开发者使用起来最为难受的点。 所以有没有一种方案,让开发者既可以使用自己熟悉的后端服务基座,又可以非常灵活方便的接入微信的安全访问? 这就是新推出的Donut安全网关! 二、 什么是安全网关,有什么特色? Donut安全网关提供了一个安全的接入链路,从调用端(小程序、公众号H5、APP、WEB)发起的请求直接进入微信环境接入层,微信接入层通过中转域名将请求转发至后端服务网络的「网关实例」中,在「网关实例」中通过路由配置将请求转发到开发者自己的上游业务服务,完成整个请求。 [图片] 以上是整个链路的全景图,其中「网关实例」指的是部署在服务器或者是容器服务中的一套envoy网关(经过微信信息处理,有专属的证书加密)。 相比于云开发或云托管的访问链路,安全网关的链路是完全独立的,相对来说也更加成熟和安全。并且由于只做请求转发,不受同主体可用的限制。也就是说任何一个小程序或者公众号H5或其他形态应用,都可以使用安全网关链路实现请求。 当然,微信在链路中也提供了来源客户端的appid等标识,开发者可以在控制台或自己的后端服务针对的限制,防止其他未授权的客户端调用。 但是需要注意,安全网关并不是替代云开发或者云托管,安全网关可以与现在的云托管做深度融合,使得最终形态一定是对各类开发者均友好的。在云端服务领域还有很长的路程需要探索和精进! 三、 开始配置安全网关 1. 登录注册Donut平台 访问Donut控制台,使用微信扫码登录。 [图片] 扫码完成后页面会自动跳转,进入控制台。首先先点击右上角头像,在列表中点击实名认证。 [图片] 填入自己的姓名和身份证号码后,会弹出如下二维码,使用微信客户端扫码验证。 [图片] 验证完成后会自动提示如下,此时就完成了实名认证流程。 [图片] 2. 开通安全网关 在控制台页面点击安全网关的「立即开通」按钮 [图片] 阅读服务协议并同意,开通完成! [图片] 页面会自动刷新,点击上方菜单栏「安全网关」,显示下述页面,开通流程完成。 [图片] 3. 新建网关 在安全网关页面点击「新建网关」按钮,在弹出的对话框中填写网关名称,完成新建。一般网关以使用角度的应用维度区分,一般只需要一个即可,可以再创建一个用于规则测试。 [图片] 在这里我填写的是app,大家可以按自己想法来填。 4. 安装网关实例 网关实例是一个经过处理过的envoy网关,主要作用就是接收微信侧发过来的请求,并按照自己设置做路由分发等流量管理。所以网关实例运行的服务器或者容器服务,应该具备以下几个条件: 有公网接入:可以是带公网IP的服务器,也可以是任何一个公网访问的容器服务[比如TKE、云托管等],如果服务器没有公网IP,可以在服务器的VPC网络中配置NAT网关,转发端口流量。尽量靠近业务后端服务:网关实例最重要的作用就是路由分发,分发的上游服务可以用内网IP访问,也可以用私有域名访问;不推荐使用公网再发出去,会增加请求耗时。所以尽量与业务后端服务处于同一个私有网络,如果跨地域了,可以考虑对等连接,或者在不同地域配置多个网关实例(用域名解析来实现就近接入)尽量避免与高峰值服务混在一起:虽然envoy本身对资源的消耗很小,但仍然需要注意不要与其他重资源消耗并可能引发宕机的服务部署在一起,否则将会导致网关实例不可用,从而影响请求顺利的转发。一般根据自身的情况,有很多种方案可以选择。选择好实例所在位置后,根据环境不同,参照控制台给出的详细步骤,安装实例。目前有如下几种: Docker:如果你的服务器安装有Docker,这是最方便的。将镜像直接下载下来运行即可。[图片] Linux:适用于大多数情况。有很多系统版本,注意自己的系统版本不要出错。安装过程基本就是开管理员权限进入终端,然后安装curl、wget、jq(基本都有,如果没有就搜索一下linux怎么安装XXX),剩下的就是直接复制一把运行,如果有出错,可以先搜索引擎一下解决,实在不知道怎么解决,可以评论下来看看[图片] K8s:如果你的服务有运行于K8s运管的容器平台,可以用这个方法快速新建网关实例。最省心的方法![图片] 在这里我使用一个服务器来执行安装过程,配置截图如下: [图片] 启动后进入root账号,执行命令如下:(最后一个是启动网关实例,可以使用nohup不挂断运行) [图片] 网关实例安装完毕后,控制台中点击「完成」按钮,进入下一步,可以在实例列表中看到已经安装在线的实例。 [图片] 注意:1. 在线情况下不可以从列表删除,需要先将网关下线,才可以从列表删除。2. 默认的对外端口为9903,但根据自己配置也会有所不同,注意打印的port内容。 5. 发布网关配置 网关配置主要分为两部分:上游服务、路由配置。我们来分别配置。 5.1 上游服务 上游服务就是请求最终到达的地方,一般可以称为业务服务地址。可以是域名,也可以是IP地址。 [图片] 在配置页点击「新建上游服务」按钮,弹出对话框,我们来填写一下。 [图片] 上游服务名称:路由配置时用它来区分,一般言简意赅的单词即可,比如login_server。备注:有时候英文不太容易记起具体是个什么,这里可以用中文长句描述一下给自己看。协议:有HTTP和HTTPS,一般内网就HTTP,节省耗时,外网或者有安全需要的HTTPS类型:分域名和IP,根据自己的情况来选择,一般选择IP。域名:内网可以用私有DNS解析来配置域名,公网可以正常填写。IP:类型为IP时可见,IP可以填写公网IP;同属一个私有网络可以填写内部IP;如果跟网关实例同属于一个服务器,则可以填127.0.0.1。在这里测试为了能让大家都能测试顺利并没有差异,直接填写一个mp官网的域名,后续可以根据自己的情况自己来变更。 注意:只在本测试中使用mp.weixin.qq.com,请不要在生产环境中尝试调用或恶意攻击操作。 [图片] 5.2 路由配置 配置完上游服务后,会提示配置路由,点击即可跳转至路由配置页面的对话框 [图片] 路由名称:清楚表示这个路由的作用即可,一般言简意赅的英文单词组合。上游服务:拉取当前配置的服务列表,以上游服务名称为索引。服务路径:就是访问时的路径,比如https://xx.com/api,想要匹配这个规则,路径就填写/api,如果没有路径就填 /路径改写-可选:目前只支持静态改写,就是将匹配的服务路径改写为指定的路径,再传递给上游服务。比如上面说的 /api 进来,可以改写成 /interface 传递给上游。请求限频-可选:配置这条路由的QPS值,流量防治的一种策略,默认是1000,范围是1-10000准入域名-可选:发过来的请求会匹配请求头中的host值;如果配置则只接受此host发来的请求,一般在接入层有多个时但需要区分流量时使用。在这里我们来配置一个测试用的路径,后面我们再配置更多来测试一下路由的能力。 [图片] 5.3 发布配置 在控制台中反复配置的路由和上游服务,并不会影响实际线上的网关实例。我们在配置完成后,需要点击控制台的「发布配置」按钮,将配置上传至网关实例。此时网关实例才能够应用我们的最新配置。 · [图片] 线上有了配置之后,我们就可以在右侧点击「线上网关配置」查看线上生效的配置情况 [图片] [图片] 6. 配置接入层 接入层是在微信侧的概念。网关接入层承接并转发业务流量,具备分布式安全防护能力,支持就近接入弱网加速,同时集成微信安全风控能力。 通俗来讲,就是客户端在访问网关时,需要有一个地址,而这个地址就是接入层的一个节点。微信对每一个接入节点创建了一个专门的地址,用来接收客户端流量。 接入节点一般以客户端应用区分(比如A应用APP、B小程序,C网页WEB),或者应用矩阵区分(A应用多端APP、小程序、WEB),这样可以随时阻断或者转移流量。另外可以通过路由配置的准入域名,来实现不同应用的差异化转发;服务商第三方平台也可以利用此特性,来实现普通客户的应用承载,以及VIP客户的应用承载分流。 只有一个接入节点的域名地址也没有用,还需要我们配置一下进入到这里的请求流量转发到哪里。 在控制台中,点击「接入层」的「新建接入节点」按钮。 [图片] 节点名称:一般以应用命名,言简意赅的英文单词组合即可。备注:可以详细描述一下服务的应用是哪些,给自己备注一下。访问方式:公网访问、小程序访问,可以根据自己的接入需要来选择,这会控制你的客户端请求的访问许可。APPID:当选择「小程序访问」时会出现,需要将允许访问的小程序appid填入,多个需要用,(英文逗号)隔开。服务接入节点类型:域名、IP或云托管,就是网关实例应该以什么形式发现。如果就一个实例,则直接IP,域名一般用于IP容易变化的服务器或者容器服务,另外在多地域就近配置时使用也很不错,需要搭配DNS高级解析能力;云托管需要在空间管理TAB中绑定云托管所属账号,这个另外开一篇指引。域名:填写域名,可以在公网指向网关实例所在的服务器,端口填写网关实例对外暴露的服务端口。IP:填写公网IP,能够指向网关实例所在的服务器,端口填写网关实例对外暴露的服务端口。云托管:选择云托管环境-服务作为服务接入节点。 注意:1. 多个接入层可以使用同一个网关实例,没有什么限制。一个接入层用多个实例,需要用域名来实现。2. 需要注意配置安全规则,保证网关实例对外暴露的端口能够被公网访问。不要使用七层负载均衡来转发端口,可以使用NAT网关转发。 在这里配置的如下,IP自己注意替换一下 [图片] 如果顺利完成不弹窗报错,则一切顺利,控制台可以看到节点。如果提示报错连接失败等,首先看下端口IP安全规则是否放开,注意是TCP协议。 [图片] 7. 简单访问 上面步骤的接入域名为:https://a04fa8d0a-wx79c3e1e62f1acac0.preview.wxcloudrun.com 格式大概如下:https://${接入节点ID}-${Donut平台Appid}.preview.wxcloudrun.com 下面所有访问都请替换成自己的接入域名 我们在浏览器中访问域名时,效果应该如下: [图片] 当我们访问下面地址(替换自己的前缀),效果为: https://xxx-wxxxx.preview.wxcloudrun.com/cgi-bin/scanloginqrcode?action=ask [图片] 也就是说,我们之前配置的路由,访问路径只是前缀匹配。 接下来我们新增一个配置,如下所示。记得「发布配置」线上生效。 [图片] 当我们访问下面地址(替换自己的前缀) https://xxx-wxxxx.preview.wxcloudrun.com/test?action=ask 会是404的提示。 接下来我们将一开始的index路由删掉,只保留api路由。 [图片] 再次访问时会发现,页面为正常的返回了。 [图片] 当我们再次将index路由配置回来,如下时: [图片] 一切都是可以的! 以上是一个路由优先级的实验,当流量转发过来时,会根据配置规则列表依次匹配,当匹配存在时立刻转发,匹配不存在时再向后匹配,直到匹配到或者匹配完为止。因此如果是/根路径,一般放置在路径最后,否则将直接从/转发走。 另外还有准入域名的实验,如果感兴趣我后面再补充。原理先在这里写一下: 准入域名一般填写的是接入域名或者是自定义域名,支持*通配符;当一个路由规则配置了准入域名A,则A的流量只会匹配这一个规则,其他未填写准入域名,或者准入域名不是一个优先级的路由规则根本不匹配。准入域名可以写如下格式: 准确域名,例:www.wx.com后缀通配符,例:*.wx.com,*-wx.com前缀通配符,例:wx.*,wx-**不匹配空,比如*wx.com 将匹配 mpwx.com 而不匹配 wx.com当一个域名打到了多个路由的准入域名,则优先级按最长的来。比如www.wx.qq.com 匹配 *.wx.qq.com ,而不是 *.wx.com 四、 在客户端中使用安全网关 目前可以在微信小程序中使用安全网关,使用步骤如下: 1. 准备环境 公测阶段,小程序调用安全网关需要开白名单,可以在申请页面填写问卷提交信息,通过后重新进入小程序,能够在基础库的列表最后,看到 develop 基础库,环境准备就完成了。 [图片] 2. 获取接入节点ID 在安全网关控制台,转到「接入层」页面,将接入节点的默认域名取出,截取ID部分,如下图所示: [图片] 上图中,接入节点的ID为:a04fa8d0a-wx79c3e1e62f1acac0 这个ID需要替换成自己,每个人都不一样。 3. 配置请求域名 当前,网关的请求需要在mp后台配置request域名,直接将节点域名的完整地址填入就可以,协议为https,如下: [图片] [图片] 团队正在优化这个点,后面就不需要这个合法域名的配置了! 4. 编写请求代码 在这里我已经写了一个代码片段,可以直接导入到IDE 小程序端调用代码片段:https://developers.weixin.qq.com/s/D5lCBnmD77Ey 导入时需要注意使用第一步申请公测时填写的APPID,不要填写其他或者接口测试号,将不会显示 develop 基础库。 [图片] 导入后,右上角点击「详情」按钮,在本地设置中,基础库选择develop [图片] 然后打开 app.js 文件,按下图所示,替换自己在第2步的接入节点ID [图片] 保存文件后,触发重新编译,在调试器console中可以看到网关请求打印的返回数据: [图片] 请求实际上有两个步骤,第一是网关的初始化,第二是call请求,在这里我对call请求做了简单的封装,并挂载在app下,大家也可以根据自己的需要封装。 [图片] 请求的触发在 pages/index/index.js 中,代码片段中初始的是前面步骤简单访问时的页面 https://xxx-wxxxx.preview.wxcloudrun.com/test?action=ask [图片] 当method为GET时,data的数据将拼接成url参数,除了path之外,其他的大部分参数都遵循wx.request。所以可以参考这里。 当将上面的data参数去掉后,由于接口的特点会直接返回页面html内容而不是JSON字符串,所以会出现下述报错。 [图片] 这是因为请求时传入datatype为json,会自动尝试对请求返回结果做json解析,如果不是json字符串,就会引起解析报错。可以直接将datatype改为text,如下图: [图片] 5. 上传体验版 公测期间,IDE内调试开发使用的是 develop 基础库,但是线上不会存在这种基础库,因此网关的调用代码需要跟随你的应用代码包一起上传。 在app.json中加入如下配置: { "cloud": true, "cloudVersion": "alpha" } [图片] 这样在你上传体验版时,网关的相关代码就随着你的代码一起打包上传了。 如果后面有一些新的特性,需要重新执行上传的步骤,其中的cloudVersion指的就是网关代码的版本,和你引入模块的版本管理形式相同,自己把握就行。 6. 降级重试 推荐大家有条件做一下降级策略,防止网关实例出现异常,或者接入层的规则出现问题,影响线上的用户调用。 先使用网关调用,网关调用失败时,使用wx.request再来一次。 异常情况下,返回的内容中有errCode,所以可以根据此来设置降级策略。 { "data": {}, "statusCode": 404, // http状态码 "errMsg": "gateway.call:ok", // 请求正常则始终为 gateway.call:ok "errCode": -651000, // 正常时无此属性。异常时会返回负值,代表云开发网关侧异常 "header": { "date": "Wed, 19 May 2021 02:37:59 GMT", "server": "envoy", "content-length": "0" }, "callID": "1665645158626-tsmg9Dpy" } 上面代码片段里,在封装的方法中有预留降级的插入为止,大家根据自己的业务做一下改造就可以。 五、 其他补充 1、 网关实例9903是正式给微信接入层做请求转发用的,端口有证书校验,不需要担心这里的攻击问题。由于默认证书可能在多处用,你可以在控制台中生成一个专属的证书,直接在对应路径替换文件,然后重启一下实例。 [图片] 2、 网关实例还有一个9902端口,是用来做路由测试的,你可以直接拼IP地址+9902端口,按照路由规则的路径测试,这个是不带证书校验的纯本地测试,也可以用于内网的流量转发,安全性不是很好,内网注意安全规则设置。 3、 网关实例还有一个19000端口,这个是管理控制台,所有人只要能访问到,就能把网关实例给停止了,还能读取你的所有配置,比较高危险了,不要裸露在外网,调试也尽量配置安全规则只允许你自己的IP访问。 [图片] 4、网关实例已经启动了,但是没在控制台的列表里显示?先看下secret有没有填写正确,重新init一下;其次再看一下网络安全规则是否放通,在实例所在服务端ping一下xds.preview.wxcloudrun.com,或者telnet一下其80端口,如果不通需要放通。 5、当配置多个网络实例时,个别实例配置不更新还是旧的?重新启动一下实例基本能解决问题,或者19000控制台进入,看一下clusters和config_dump的信息是否和配置的描述相符,不相符控制台quitquitquit,然后重新运行启动命令。 6、当确认操作步骤正确,但是效果不符合预期时,可以将启动后的log日志完整截取复制,反馈到社区相关板块。docker启动执行 docker logs envoy;linux启动,将stdout或者nohup.out的内容截取复制。 7、如果代码片段打开切换develop基础库时,长时间不启动进入页面,调试器也没有输出,请耐心等待develop基础库下载完成,会有loading的动画加载。如果等待3min仍然不变化,重新启动IDE可解决。
2022-12-21 - 还在为开发调试头疼?来来来,这里有一份微信支付APIv3脚本,真金白银开源了!
脚本名称: Name:微信支付 APIv3 脚本说明: 本脚本是基于 微信支付 APIv3 的 Postman请求前置脚本(Pre-Request Script)进行完善,补充了微信支付普通商户所有已知公开接口,每个接口请求预置了请求参数示例与请求成功返回的参数示例,帮助商户开发者、测试人员以及小白用户也可以快速上手。 仅修改原脚本变量为常用叫法,无其他修改部分: merchantId->mchid merchantSerialNo->merchant_serial_no merchantPrivateKey->apiclient_key.pem 使用前提条件 postman,建议注册一个账户,便于使用它各种功能,例如同步。 有一个微信支付商户号,支持微信支付直连普通商户、微信支付直连特约商户,不支持微信二级子商户。 商户 API 私钥与商户证书序列号:商户API私钥是在商家平台申请商户API证书时,会生成商户私钥,并保存在本地证书文件夹的文件 apiclient_key.pem 中,商户商户序列号可在商家平台->账户中心->API安全->管理API证书查询到。 快速开始 1、使用Fork 方式导入脚本 1.1、点击[图片]进入向导,如下图所示。 [图片] 1.2:点击 Fork Collection 进入下一步,填入标签 Fork Label 并选择目的工作台 Workspace。一般情况下,导入个人工作台 My Workspace 即可。 如未登录账户,会跳转到账户登录页面,如无账户建议先去注册一个 [图片] 1.3点击 Fork Collection 完成导入。在你指定的 workspace 中可以看到《微信支付 APIv3》了。 [图片] 2、配置Environment 环境(Environment) 是一组变量 (Varibles) 的集合。 脚本从环境中读取变量,用来计算请求的签名。 你可以从《微信支付 APIv3》提供的 商户参数模版 中 fork 一个空环境到自己的工作台。 [图片] 接下来,在你工作台的 Enviroments 中找到新建的环境,点击 Add a new varialbe 添加新的变量: 变量名 是否必填 描述 备注 server_url 必填 微信支付接口域名 固定值:https://api.mch.weixin.qq.com mchid 必填 微信支付商户号 纯数字 merchant_serial_no 必填 商户 API 证书序列号 apiclient_key.pem 必填 PEM 格式的商户 API 私钥 以 -----BEGIN PRIVATE KEY----- 开始的 PEM 格式的商户 API 私钥。 appid 必填 用于微信支付接口请求中的APPID APPID需要与填写的mchid有绑定关系 openid 选填 用于微信支付接口请求中的openid 如不配置全局变量,请求时需要将参数中的变量openid替换为实际openid值 一组常见配置如下图所示: [图片] 3、发送测试请求 此处建议,使用桌面版 Postman app 发送请求,速度更快,体验更好! 现在回到工作台的请求构造界面,填入请求方法、URL、请求参数、Body 等参数。 工作台预置了微信支付普通商户所有请求样例供开发者参考,开发者也可以参考请求样例,构造自己的请求。 最后,选择你之前配置的 Environment,再点击地址栏右侧的Send按钮,发送请求。 [图片] 常见问题 1、发送请求时遇到错误提示“Error: Too few bytes to parse DER.”或者“Too few bytes to read ASN.1 value.” A:通常是环境 Environments 里配置的变量 merchantPrivateKey 填写有误导致的。脚本接收的私钥,以 -----BEGIN PRIVATEKEY----- 开始,以 -----END PRIVATE KEY----- 结束的一串字符串。 2、为什么我发送请求很慢? A:如果你使用的网页版 Postman,请使用桌面版 Postman app。因为浏览器中跨域资源共享(CORS)的限制,网页版发送请求是由 Postman 后台中转的。 WeChatPay Developers QQ Group ID:684379275
2022-11-08 - 服务商助手审核专区使用手册
本月可提审次数 显示服务商本月剩余可提审次数和分配的总quota值,每月1号更新 本月可加急次数 显示服务商本月剩余可加急次数和分配的总加急次数,每月1号更新 以上审核资源受服务商线上表现(小程序内容安全、活跃度、平台配合度)影响,各服务商务必做好小程序商户提供内容素材的管理,减少提审驳回情况和线上违规情况 更新时间为1号早上6点,0-6点显示的是上一月quota,6点后更新为本月 审核数据 服务商可以查看近7天、本月、上月的每日审核量、合格率和期间平均合格率、驳回原因分布 平台合格率为所选择时间周期内的合格率,合格率为重要运营指标,对服务商的资源分配有较强影响,请注意提升 一期合格率存在一定的误差,仅供参考,后续会不断完善,提升这里的数据准确度 审核队列 可以对当前审核队列中的各个审核单进行管理 审核中:提审后等待审核的单,审核时效一般为7天内,表现良好的服务商有更快审核速度 审核冻结:大批量提审未通过预检,被延后审核,建议服务商基于预检不通过原因撤回整改后再次提审,审核时效>15-30个工作日,冻结不及时撤回将被平台追加处罚 审核驳回:近7天被驳回的审核单 注意,审核队列只保留最近一次审核状态的单,如近7天被驳回的单再次提审,将从审核驳回队列中消失,进入审核中队列或审核冻结队列 审核队列支持按小程序名称,小程序appid,模板id进行搜索 搜索时,将在全部审核队列中搜索。小程序名搜索的优先级高于模板id,如”幸运58”的小程序和模板id为5828的100个小程序同时存在时,搜索58将只显示“幸运58”,搜索582才会显示模板5828 appid只支持精确匹配,名称和模板id支持模糊匹配 搜索模板id时,可以按全部单和只看审核冻结单进行筛选,方便进行批量操作 目前一页可展示100个审核单,如需查看更多,请点击页面底部的“加载更多审核单”,加载第二页,以此类推 小程序加急 服务商对于审核中和审核冻结的小程序可以进行加急操作,加急后,该审核单将优先安排审核,原本被冻结的状态也会解除 加急额度属于平台稀有资源,请谨慎使用,严禁任何形式的转卖转赠,一经发现将对quota和加急次数进行一年的处罚 单个加急:可以在小程序的明细页点击加急按钮进行 多个加急:可以点击审核队列的加急按钮,选择多个小程序进行加急 不可加急的类型:需要被报备网信办等机构的审核,无法加急,提示加急失败,加急额度返还 小程序撤回 当发现bug,或者预检不通过导致大批小程序被冻结时,服务商可以操作撤回这批小程序,整改后再次提审。 对大批量问题提审单撤回有助于提升服务商合格率,降低对平台资源消耗。预检不通过的冻结单未及时撤回将会被处罚 单个撤回:可以在小程序的明细页点击撤回按钮进行 批量撤回:可以点击审核队列的撤销审核按钮,选择多个小程序进行撤回,批量操作时,可以基于模板去筛选,可以按照只看冻结去筛选 推荐操作流程:当服务商需要对某个模板id,如5234模板id中全部提审单或全部冻结审核单进行撤回时,建议点击撤销审核按钮,进入选择审核单页面,搜索模板5234,在进入的模板页,点击筛选选项(全部/只看审核冻结),然后点击全选,可以看到本次要撤回的小程序个数,最后点击确定 批量撤回在单数较多时,将生成一个后台异步任务执行,执行结束后将批量撤回结果反馈回来,大批量的撤回可能要等待较久时间 不可撤回的类型: 1,单个小程序每月撤回次数上限为10次,超过时不可撤回 2,已经被其他渠道(如API接口)撤回的单,不可重复撤回 3,其他 客服专区 本次审核专区将为部分服务商开放客服专区入口,客服专区包括机器人客服为服务商提供常用知识库,以及人工客服解决服务商具体问题 开放范围:基于服务商每月quota排名进行分配,一期开放100个,后续将不断放开,直至覆盖全部服务商 客服工作时间:工作日:9:00-12:00,14:00-18:00,特殊时期以平台公告为准 客服问题范围:小程序运营规则、服务商运营政策、提审/审核/线上处罚常见问题咨询、服务商助手体验bug问题等 使用过程中任何问题都可以通过客服入口留言,如无客服入口可以通过开放社区进行反馈,我们将以帮助服务商提高获取信息和办理业务的效率为核心目标,不断完善服务商助手小程序。 最后更新时间:2020年3月5号
2020-11-27 - 小程序公众号干货运营之注销篇
各位亲,面对帐号注销是不是束手无策呢?帐号如何注销,怎么注销,注销需要提供什么信息内容呢?请仔细往下看看 小程序 关于小程序注销的条件,若未冻结的个人帐号和组织类帐号就不 一 一 细讲,详情请参腾讯客服文档:https://kf.qq.com/product/wx_xcx.html#hid=2826 1:小程序注销之政府无对公账户: 详细流程请参考:https://kf.qq.com/faq/190104YnQbYN190104RzaYba.html 政府的有一致主体是提供一致的主体证件和公章,如果有变更请提供4项材料:因机构改革、单位合并、撤一建一等情况,导致机构主体名称有变更,提供以下材料申请注销: 1、更名相关的红头文件(有鲜章); 2、主体名称变更情况说明书(加盖新主体公章); 3、变更后新主体的主体证件;(原件拍照或加盖公章的复印件) 4、注销申请函(加盖新主体公章); 2:小程序注销之个体工商户 若个体工商户存在对公账户,请使用对公账户小额打款注销 若个体工商户类型无对公账户注销小程序工单指引流程如下 工单所需材料 1、小程序绑定邮箱/原始ID: 2、主体证件材料(营业执照/组织机构代码证等): 3、小程序绑定的法人身份证原件正反面的清晰扫描件或照片: 4、小程序的注销书面申请,申请书必须加盖公章。(若个体户没有公章可支持法人手写签名) 附注:注销申请书模板(https://kf.qq.com/faq/200306R7N3mI200306I3aEBz.html) 材料提交链接:https://kf.qq.com/touch/bill/200306selfqaafe6c551.html(手机端打开) 3:小程序注销之帐号主体已注销 主体已注销小程序工单指引流程如下, 1、小程序绑定邮箱/原始ID: 2、主体注销证明: 3、小程序绑定的法人身份证原件正反面的清晰扫描件或照片: 4、小程序的注销书面申请,企业账号的申请必须有加盖公章的函件(公章被收的请法人手写签字)附注:注销申请书模板(https://kf.qq.com/faq/200306R7N3mI200306I3aEBz.html) 材料提交链接:https://kf.qq.com/touch/bill/200306selfqaafe6c551.html(手机端打开) 4:小程序注销之门店小程序 门店小程序依附于公众号,不支持单独注销,公众号注销门店小程序才支持注销 5:公众号正常运营,门店小程序如何释放昵称 如果需要释放该小店小程序昵称,发送邮件到“miniprogram@tencent.com”,标题格式【关于XXX名称释放请求】,需提供以下材料: 1、小程序帐号(原始ID); 2、绑定的管理员微信号; 3、小程序主体营业执照等主体证件; 4、小程序所有者的书面申请,申请书需加盖小程序主体公章;(个体户无公章:申请书需要加上法人签名); 邮件内容:需包含背景、释放请求原因。 6:复用公众号资质快速注册的小程序如何注销 复用资质申请的小程序是独立存在的,请按照正常流程注销即可 7:注册小程序选择微信认证,若未完成微信认证如何注销呢? 小程序30天未认证或认证失败且7天内未发起认证不会释放邮箱,但该邮箱支持重新注册小程序,会释放主体信息、管理员信息、昵称。 公众号 关于公众号若未冻结的个人帐号和组织类帐号就不一一细讲,详情请参考腾讯客服文档:https://kf.qq.com/product/weixinmp.html#hid=2267 1:公众号注销之政府无对公账户: 详细流程请参考:https://kf.qq.com/faq/190531qyuuiY190531BjyyEv.html 政府的有一致主体是提供一致的主体证件和公章,如果有变更请提供4项材料:因机构改革、单位合并、撤一建一等情况,导致机构主体名称有变更,提供以下材料申请注销: 1、更名相关的红头文件(有鲜章); 2、主体名称变更情况说明书(加盖新主体公章); 3、变更后新主体的主体证件;(原件拍照或加盖公章的复印件) 4、注销申请函(加盖新主体公章); 2:公众号注销之个体工商户 若个体工商户存在对公账户,请使用对公账户小额打款注销 若个体工商户类型无对公账户,请使用法人扫脸注销公众号 详情请参考:https://kf.qq.com/faq/220309bUvmIB220309BbAjMz.html 3:公众号注销之帐号主体已注销 主体已注销公众号工单指引流程如下, 1、公众号绑定邮箱/原始id/微信号: 2、主体注销证明: 3、公众号绑定的法人身份证原件正反面的清晰扫描件或照片: 4、公众号的注销书面申请,企业账号的申请必须有加盖公章的函件(公章被收的请法人手写签字) 附注:注销申请书模板(http://kf.qq.com/faq/171018R3IVBF171018INjUvA.html ) 材料提交链接:https://kf.qq.com/touch/bill/180227selfqa9ab6ac55.html(手机端打开) 4:未注册成功的帐号如何注销 若帐号当时没有走完注册流程且长期没有登录该帐号,到期会被系统注销。没有走完注册流程的帐号不占用个人信息,也不支持找回,建议重新注册 5:注册公众号选择微信认证,若未完成微信认证如何注销呢? 若公众号注册时选择微信认证,自注册日起30天内未进行认证(第30天仍在认证中不算),点击“重新提交材料”,帐号角色变为注册失败,不会释放帐号邮箱,但该邮箱支持重新注册公众号,会释放主体信息、管理员信息、昵称, 6:小程序公众号注销确认期 注销确认期的7天内每天会发送一次确认注销的通知,若管理员一直未点击确认注销则默认取消注销,注销失败。因此管理员请关注公众平台安全助手!!!
2022-04-08 - 小程序用户信息相关接口调整公告
为进一步规范开发者调用用户信息相关接口或功能,提升用户体验,平台将对部分用户信息相关功能及接口进行调整,具体如下: 访问蓝牙、添加通讯录联系人、添加日历事件需要用户授权小程序处理用户的个人信息,需要获取用户明示同意,平台计划从2022年2月21日24时起对以下接口增加用户授权: 访问蓝牙:调用wx.openBluetoothAdapter、wx.createBLEPeripheralServer,需要授权scope.bluetooth添加通讯录联系人:调用wx.addPhoneContact,需要授权scope.addPhoneContact添加日历事件:调用wx.addPhoneRepeatCalendar、wx.addPhoneCalendar,需要授权scope.addPhoneCalendar开发者可在平台调整前提前增加使用 wx.getSetting 获取用户当前的授权状态的逻辑,若授权状态为false可以调用 wx.openSetting 打开设置界面,引导用户开启授权。 授权功能详细说明可参考:官方文档 <open-data>组件功能调整开发者在未获取用户明示同意的情况下通过 <open-data>组件 在小程序中展示用户个人信息,用户容易误以为自己的个人信息在未授权的情况下,被小程序获取。平台计划从2022年2月21日24时起回收通过<open-data>展示个人信息的能力,若小程序需收集用户昵称头像等信息,可以通过 头像昵称填写功能 功能进行收集。具体回收方式为: 头像展示 灰色头像用户昵称展示“微信用户”用户性别、地区、语言展示为为空(“”)小程序通过<open-data>展示群名称能力保留,平台会针对小程序生命周期内首次调用该组件展示群名称向用户提示:“群名称仅你可见,小程序无法获取。” 获取手机号能力安全升级此前小程序获取用户手机号是通过基础库接口直接获取encryptedData后进行解密。从基础库2.21.2版本起,回调参数中增加code参数,开发者获取code参数后,通过服务端auth.getPhoneNumber接口,使用code换取encryptedData,用于解密手机号。 为不影响开发者现有逻辑,原有基础库接口中的encryptedData参数依旧保留,建议开发者尽快使用新的方式获取用户手机号。 详细功能描述可参考 官方文档 微信团队 2021年12月27日
2023-09-26 - 小程序开通云闪付支付后常见问题Q&A
看完记得点赞收藏好评一键三连 Q1:小程序如何开通云闪付支付功能? A1:超级管理员扫码登录商户平台「点我访问」,点击「产品中心」->「开发配置」->「支付方式配置」->「开通“云闪付付款”功能」 小提示:支付方式配置在页面最底部 [图片] Q2:开通“云闪付付款”功能后小程序需要做开发对接吗? A2:不需要,开通“云闪付付款”功能后,商户号绑定的小程序默认就支持云闪付付款了,无需做任何开发对接,原有系统无需调整。 Q3:用户使用云闪付付款,商户收款手续费是多少? A3:与商户号原费率保持一致,举例:用户在商户A小程序使用云闪付支付100元,商户A当前费率为0.6%,则应收手续费应收为1000.6%=0.6元。 Q4:用户使用云闪付付款,商户收款资金在什么时间结算到商家银行卡? A4:用户使用云闪付付款实时到微信支付商户号,根据商户号原结算周期结算。举例:用户在商户A小程序使用云闪付支付100元,商户A当前结算周期为T+1并开通自动提现功能,则在用户付款时间后一个工作日自动提现到商户银行卡。 Q5:用户使用云闪付付款的订单应如何进行查询? A5:1.在商户后台通过交易账单中的「付款银行」字段来检索对应的订单 扫码登录商户平台-交易中心-交易账单-下载交易账单,「付款银行字段」取值为:UPQUICKPASS_CREDIT和UPQUICKPASS_DEBIT的,即为用户使用云闪付付款的交易 [图片] [图片] 2.微信端可以通过商家助手小程序查看,访问「微信支付商家助手小程序」,点击「收款记录」,付款人展示为“云闪付用户”的,即为用户使用云闪付进行付款的交易。[图片] Q6:小程序“云闪付付款”功能是否支持服务商模式? A5:支持 Q7:某个小程序可以单独关闭用户使用云闪付付款功能吗? A7:可以,商户可以在商户平台指定小程序关闭该功能,并查看已关闭的小程序列表。超级管理员扫码登录商户平台「点我访问」,点击「产品中心」->「开发配置」->「支付方式配置」->「新增关闭云闪付APPID」,添加成功后该小程序用户将不再支持云闪付付款 小提示:支付方式配置在页面最底部 [图片] Q8:用户在小程序使用云闪付付款是否支持云闪付优惠? A8:支持云闪付通用优惠或全场优惠 Q9:用户使用云闪付付款后支付结果通知没有「attach」字段返回是什么原因? A9:已知问题,已排期修复 Q10 :商户后台开通了“云闪付付款”功能,为什么小程序支付时没有云闪付付款功能入口呢? A:1、手机需要安装云闪付APP 2、云闪付付款选项需要调起微信支付后才会让用户选择,当小程序自身功能有多项支付选择时,需要选择「微信支付」后才可会有云闪付付款选择[图片] [图片] Q11:用户在小程序使用“云闪付”付款后,服务商还有技术服务费吗? A:满足基础技术服务费和行业政策要求的,保持不变 Q12:商户后台开通配置云闪付后,支付时没有云闪付付款选择如何排查? A: 1.商户在后台关闭了此功能; 2.用户手机有安装云闪付app; 3.如属于以下场景,也不会展示云闪付:境外交易、指定身份支付、未成年支付、支付中签约; 4.商户号为新开通商户或近期无交易,要有稳定的流水后才会开启入口; 5.开通了“自助清关”产品不会展示云闪付; 6.电商收付通托管模式的商户不会展示云闪付; 7.小微商户(注:商业版小微灰度内测阶段)不会展示云闪付。 如有更多疑问可以跟帖回复,也可以拨打95017进行咨询
2021-12-31 - 【微信支付新人必读】智慧的提问,快速的解答
写在开头 在技术社区里,你技术提问的解答情况,很大程度上取决于你提问的方式与解决此问题的难度。 智慧的提问就是好的提问习惯和好的提问规范的结合,它能让你事半功倍。至少在微信支付社区,它是真实成立的 如果您没有时间读完全文,请务必读完微信支付社区提问智慧 首先来看一下,微信支付社区的智慧提问法则: 微信支付社区提问智慧 好的提问习惯 如果你有以下几个提问习惯,能直接解决大部分你想问的问题~ [代码]1. 尝试在搜索框中搜索答案; 2. 尝试阅读相关官方文档以找到答案; 3. 尝试阅读FAQ以找到答案; 4. 尝试自己检查或试验以找到答案; [代码] 好的提问规范 好的提问规范,能在最快的解决让你得到最优效的解答~ [代码]请用陈述句准确描述问题 1. 标题定位到微信支付具体业务,比如:支付分、代金券、普通支付、合单支付等; 2. 50+字详细准确描述问题的症状: 3. 包含必要的错误信息、期待的结果; 4. 包含必要的截图或代码等细节; 5. 请描述已经尝试过的方法。 [代码] 如果期望得到微信支付技术支持,请参考以下模板:(有敏感信息可私信提供) [代码]1. 请求的具体API接口(提供文档地址和请求的URL): 2. 问题发生时间【必填】 3. 商户号【必填】: 4. 商户订单号【必填】: 5. 相关报错信息文案: 6. 完整的请求和返回参数以及单号: 7. 问题截图或视频: 8. 已经尝试过的方法: [代码] 如果你已做了上述事情,我们会非常乐意回答比较规范的优质提问,也会在最快的时间推送到支持组进行针对性的回答。 当然为了维持社区的内容质量,无效提问(空泛、偏离技术讨论、软文传播、推广引流等内容),我们也有有权进行删除处理。 下面跟着大家看一下通用的技术社区提问智慧,仅供参考~ 通用技术社区提问智慧 量不在多,精炼则灵 简单的将一大堆代码或数据罗列在求助信息中达不到目的。如果你有一个很大且复杂的测试样例让程序崩溃,尝试将其裁剪得越小越好。 描述问题而不是猜测 提问中描述是什么导致了问题是没用的(如果你的诊断理论是对的,或许你就不会来这儿咨询求助了?)。所以,确保只要描述问题的原始症状,而不是你的解释和理论,让社区名人或官方支持来解释和诊断。如果你认为陈述自己的猜测很重要,应清楚地说明这只是你的猜测并描述为什么它们不起作用。 错误的示范: 我在XX时遇到了YY错误,怀疑是ZZ原因,这个问题怎么解决? 智慧的提问: 我组装的电脑(电脑信息)最近在开机20分钟左右,做内核编译时频繁地报SLG11错,但在开头20分钟内从不出问题,重启动不会复位时钟,但整夜关机会。更换所有内存未解决问题,相关的典型编译会话日志附后。 按时间先后罗列问题症状 刚出问题之前发生的事情通常包含有解决问题最有效的线索。所以,问题描述中尽可能的描述在问题出现之前都做了什么。在命令行处理的情况下,有会话日志(如运行脚本工具生成的)并引用相关的若干(如 20)行记录会非常有帮助。 提问应明确 漫无边际的问题通常也被视为没有明确限制的时间无底洞。最有可能给你有用答案的人通常也是最忙的人(假如只是因为他们承担了太多工作的话),这些人对于没有止境的时间无底洞极其敏感,所以他们也倾向于不太喜欢那些漫无边际的问题。 如果你明确了想让回复者做的事(如指点方向、发送代码、检查补丁或其它),你更有可能得到有用的回复。(因为)这样可以让他们集中精力并间接地设定了他们为帮助你需要花费的时间和精力上限。 结语 提问的智慧就是一个敲门砖,它会让你了解到一个事实,为什么那些看起来很牛的人几乎从不提问,似乎他们一进入这个行业就是牛人了。不是的,他们也有问题,但是通常在提问之前就自己解决了;不是因为他们本来就懂得怎么解决,而是解决问题的经历让他们成为牛人;最终,你只会看到网络上多了一篇文章:关于解决 某某 问题的方案。 最后,祝你在微信支付开发的路上,早日晋升为大神~希望未来有一天,您也能将在社区得到的帮助回馈给更多需要帮助的“微信支付新人”。
2020-11-12 - 公众平台/小程序服务端API的access_token的内部设计
一、背景 对于使用过公众平台的API功能的开发者来说,access_token绝对不会陌生,它就像一个打开家门的钥匙,只要拿着它,就能使用公众平台绝大部分的API功能。因此,对于开发者而言,access_token的使用方式就变得尤其的重要。在日常API接口的运营中,经常遇到各种的疑问:为什么我的access_token突然非法了?为什么刚刚拿到的access_token,用了10min就过期了?对于这些疑问,我们提供出access_token的设计方案,便于开发者对access_token使用方式上的理解。 对于access_token的获取,可以参考公众平台的官方文档:auth.getAccessToken、获取Access token 二、access_token的内部设计 2.1 access_token的时效性 众所周知,access_token是通过appid和appsecret来生成的。内部设计的步骤如下: (1)开发者通过https请求方式: GET https://API.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=APPID&secret=APPSECRET,传入appid及apppsecret的参数 (2)公众平台后台会校验appid和哈希(appsecret)是否与存储匹配,若匹配,结合当前时间戳,生成新的access_token。 (3)生成新的access_token的同时,会对老的access_token的过期时间戳更新为当前时间戳。 (4)返回新的access_token给开发者。 这里以图示的方式说明一下,新旧token交替过程: [图片] 从上图需要注意的几点: (1)公众平台存储层只会存储新老两个access_token,意味着假设开发者重复调用3次接口,则会导致最早的access_token立刻失效。 (2)虽然请求新的access_token后,老的access_token过期时间会更新为当前时间,但也不会立刻失效,原理请参考 【2.2 access_token 的逐渐失效性】 (3)出于信息安全考虑,公众平台并不会明文存储appsecret,仅存储appid以及appsecret的哈希值。因此开发者要妥善保管appsecret。当appsecret疑似泄露时,需要及时登录mp.weixin.qq.com重置appsecret。 2.2 access_token 的逐渐失效性 从【access_token的时效性】了解到,当开发者请求获取新的access_token时,老的access_token过期时间会被更新为当前时间,但此时不会立刻失效,因为公众平台会提供【5分钟的新老access_token交替缓冲时间】,因此也称为access_token 的逐渐失效性。 实现的原理是: 1. 由于老的access_token过期时间戳已被刷新,所以在API接口请求期间,带上的access_token解开后,过期时间戳会加上5分钟,然后和当前设备时间进行比对,若超过当前设备时间,判断为失效。 2. 公众平台的设备会保持时钟同步,但设备之间仍然可能会存在1-2分钟的时间差异,所以【5分钟】并非绝对的时间值。当开发者获取到新的access_token后应该尽快切换到新的access_token。 [图片] 从上图需要注意的几点: (1)由于存在设备时间同步的差异,可能会导致开发者遇到拿着老的access_token请求API接口,部分请求成功,部分请求失败的情况,建议开发者获取到新的access_token后尽快使用。 (2)通过理解两个图示,对开发者来说,access_token是相当关键且不能乱调的接口,建议开发者统一管理access_token,以免造成多次请求导致access_token失效。
2021-05-11 - 【一】从零实现商城多规格sku
前言 在商城里产品的spu、sku展示是很重要的一部分,常见的商城一般没有sku的概念,会把一个多规格的sku拆分成多个spu从而让用户选择。这样是最简单的做法,但是需求是真的跟不上,一般boss都会要求做多规格的sku选择。下面分享一个多规格sku实现的思路以及过程。 效果图 [图片] [图片] 数据分析 要实现sku首先要知道是什么数据组合成的sku列表,下面大概说说我自己sku数据格式。 [代码]{ "code": "1@0-0#1-0#2-0", "specs": [ { "id": "0-0", "key": "颜色", "value": "白色" }, { "id": "1-0", "key": "图案", "value": "圆点" }, { "id": "2-0", "key": "尺码", "value": "XXL" } ] } [代码] [代码]code[代码] 表示一个sku,在当前sku_list数据中是唯一存在的,后续都得通过 [代码]code[代码] 来查找sku数据。 [代码]specs[代码] 表示sku的规格信息,[代码]id[代码] 也表示是当前specs里唯一的值,如果用关系型数据库,可能数据库设计的时候,这个id会是子表的id主键,通过id去关联查询对应的数据,我这里[代码]0-0[代码] 则用预先定义好的规格key的下标来表示,其实跟关系型数据库的主键id一样。只要是唯一不会冲突即可,语义化之后等同于 [代码]颜色: 白色[代码],仔细观察其实是有规矩可行的,id会跟code对应起来。 视图规格列表 上面是定义的接口数据,并不能直接在视图上渲染成多规格的样式,因为还需要将多个sku的数据进行转换才能得到视图所见的sku列表。 如何转换数据 接口数据遍历如下: 黑色 圆点 XXL 白色 条纹 S 红色 卡通 L 视图渲染所需数据如下: 黑色 白色 红色 圆点 条纹 卡通 XXL S L 对照两组数据,其实我们将数据一进行了旋转,从而得到了数据二,用数学名词表示即是 [代码]矩阵转置[代码],具体是怎样可以百度百科,点我查看。只要搜搜 [代码]js数组矩阵转置[代码] 等关键词则可以找到相关的代码。 转置计算 [代码]const rows = [ { name: '颜色', values: ['黑色', '白色', '红色', '粉色', '紫色'] }, { name: '图案', values: ['圆点', '条纹', '卡通'] }, { name: '尺码', values: ['XXL', 'XL', 'L', 'M', 'S'] }, ] const skus = [ '1@0-0#1-0#2-0', '1@0-1#1-0#2-0', '1@0-2#1-0#2-0', '1@0-3#1-0#2-0', '1@0-4#1-0#2-0', '1@0-0#1-1#2-0', '1@0-1#1-1#2-1', '1@0-2#1-1#2-2', '1@0-3#1-1#2-3', '1@0-4#1-1#2-0', '1@0-0#1-2#2-0', '1@0-1#1-2#2-0', '1@0-2#1-2#2-0', '1@0-3#1-2#2-0', '1@0-4#1-2#2-2', '1@0-0#1-0#2-0', '1@0-1#1-0#2-1', '1@0-2#1-0#2-1', '1@0-3#1-2#2-4', '1@0-2#1-1#2-4', '1@0-3#1-0#2-4', '1@0-4#1-0#2-3', '1@0-4#1-2#2-0', ] const sku_list = skus.map((v) => { const codes = v.split('@')[1].split('#') const specs = codes.map((c) => { const key = c.split('-')[0] const value = c.split('-')[1] return { id: c, key: rows[key].name, value: rows[key].values[value], } }) return { code: v, specs } }) const EStatus = { PENDING : 'pending', DISABLED : 'disabled', SELECTED : 'selected', } const specs = sku_list.map(v => v.specs) const _isRepeat = (list, c, cell) => { return list[c].cells.some((v) => v.id === cell.id) } const _transpose = (specs) => { const result = [] for (let c = 0; c < specs[0].length; c++) { result[c] = { key: '', cells: [] } for (let i = 0; i < specs.length; i++) { // 去重 const cell = specs[i][c] if (!_isRepeat(result, c, cell)) { result[c].key = cell.key result[c].cells.push({ id: cell.id, status: EStatus.PENDING, value: cell.value, }) } } } return result } const fences = _transpose(specs) console.log('数组转置') console.log(JSON.stringify(fences)) [代码] 复制代码运行查看结果 [代码][{"key":"颜色","cells":[{"id":"0-0","status":"pending","value":"黑色"},{"id":"0-1","status":"pending","value":"白色"},{"id":"0-2","status":"pending","value":"红色"},{"id":"0-3","status":"pending","value":"粉色"},{"id":"0-4","status":"pending","value":"紫色"}]},{"key":"图案","cells":[{"id":"1-0","status":"pending","value":"圆点"},{"id":"1-1","status":"pending","value":"条纹"},{"id":"1-2","status":"pending","value":"卡通"}]},{"key":"尺码","cells":[{"id":"2-0","status":"pending","value":"XXL"},{"id":"2-1","status":"pending","value":"XL"},{"id":"2-2","status":"pending","value":"L"},{"id":"2-3","status":"pending","value":"M"},{"id":"2-4","status":"pending","value":"S"}]}] [代码] 渲染视图 [代码]<view class="demo"> <view wx:for="{{ skus }}" wx:key="item" mark:y="{{ index }}" class="rows" > <view class="key">{{ item.key }}</view> <view class="columns"> <view wx:for="{{ item.cells }}" wx:key="item" mark:x="{{ index }}" mark:status="{{ item.status }}" class="cell {{ item.status }}" bind:tap="change" >{{ item.value }}</view> </view> </view> </view> [代码] 如何获取可视规格 当我们将所有的sku规格进行数据转换之后,还需要将sku的所有组合计算出来,通过拆分 [代码]code[代码] 可以得到sku组合的信息,通过 [代码]组合[代码] 算法得到所有的可视规格,即视图所有可以点的规格路径,具体百度百科了解,点我。 组合计算 [代码]const codes = sku_list.map(v => v.code) const _combination = (arr, symbol = '#') => { let result = [] let s = [] for (let i = 0; i < arr.length; i++) { s.push(arr[i]) for (let j = 0; j < result.length; j++) { s.push(result[j] + symbol + arr[i]) } result = [...s] } return result } const paths = [] codes.map(v => { paths.push(..._combination(v.split('@')[1].split('#'))) }) console.log('数组组合') console.log(JSON.stringify(paths)) [代码] 运算结果 [代码]["0-0","1-0","0-0#1-0","2-0","0-0#2-0","1-0#2-0","0-0#1-0#2-0","0-1","1-0","0-1#1-0","2-0","0-1#2-0","1-0#2-0","0-1#1-0#2-0","0-2","1-0","0-2#1-0","2-0","0-2#2-0","1-0#2-0","0-2#1-0#2-0","0-3","1-0","0-3#1-0","2-0","0-3#2-0","1-0#2-0","0-3#1-0#2-0","0-4","1-0","0-4#1-0","2-0","0-4#2-0","1-0#2-0","0-4#1-0#2-0","0-0","1-1","0-0#1-1","2-0","0-0#2-0","1-1#2-0","0-0#1-1#2-0","0-1","1-1","0-1#1-1","2-1","0-1#2-1","1-1#2-1","0-1#1-1#2-1","0-2","1-1","0-2#1-1","2-2","0-2#2-2","1-1#2-2","0-2#1-1#2-2","0-3","1-1","0-3#1-1","2-3","0-3#2-3","1-1#2-3","0-3#1-1#2-3","0-4","1-1","0-4#1-1","2-0","0-4#2-0","1-1#2-0","0-4#1-1#2-0","0-0","1-2","0-0#1-2","2-0","0-0#2-0","1-2#2-0","0-0#1-2#2-0","0-1","1-2","0-1#1-2","2-0","0-1#2-0","1-2#2-0","0-1#1-2#2-0","0-2","1-2","0-2#1-2","2-0","0-2#2-0","1-2#2-0","0-2#1-2#2-0","0-3","1-2","0-3#1-2","2-0","0-3#2-0","1-2#2-0","0-3#1-2#2-0","0-4","1-2","0-4#1-2","2-2","0-4#2-2","1-2#2-2","0-4#1-2#2-2","0-0","1-0","0-0#1-0","2-0","0-0#2-0","1-0#2-0","0-0#1-0#2-0","0-1","1-0","0-1#1-0","2-1","0-1#2-1","1-0#2-1","0-1#1-0#2-1","0-2","1-0","0-2#1-0","2-1","0-2#2-1","1-0#2-1","0-2#1-0#2-1","0-3","1-2","0-3#1-2","2-4","0-3#2-4","1-2#2-4","0-3#1-2#2-4","0-2","1-1","0-2#1-1","2-4","0-2#2-4","1-1#2-4","0-2#1-1#2-4","0-3","1-0","0-3#1-0","2-4","0-3#2-4","1-0#2-4","0-3#1-0#2-4","0-4","1-0","0-4#1-0","2-3","0-4#2-3","1-0#2-3","0-4#1-0#2-3","0-4","1-2","0-4#1-2","2-0","0-4#2-0","1-2#2-0","0-4#1-2#2-0"] [代码] 修改规格状态 当点击规格列表里任意一个时,点击的需要显示激活状态,无规格的需要显示禁用状态。改变自身的状态很容易,要改变其他规格的状态就有点复杂了,需要通过多次循环遍历计算当前点击的可视规格,将不存在可视规格里的规格全部修改成禁用状态,语言组织起来比较难以理解,过程即是通过行号、列号找到对应规格,然后通过组合计算可视规格,通过对比以后就知道该显示的状态是什么了。 修改事件 [代码]// index.js import { sku_list } from '../mocks/demo.mock' import Sku, { IFence } from './sku' Page({ data: { sku: {} as Sku, skus: [] as IFence[], }, onLoad() { const sku = new Sku(sku_list) this.data.sku = sku this.setData({ skus: sku.fences, }) }, change({ mark }) { const { sku } = this.data sku.change(mark) this.setData({ skus: sku.fences, }) }, }) [代码] [代码]const selected = [] const change = ({ x, y, status }) => { if (status === EStatus.DISABLED) return // 改变点击的cell _changeCurrentCellStatus(x, y, status) // 改变其他cell fences.forEach((v, y) => { v.cells.forEach((cell, x) => { _changeOtherCellStatus(cell, x, y) }) }) } [代码] 修改自身状态 [代码]const _setCellStatus = (x, y, status) => { fences[y].cells[x].status = status } const _changeCurrentCellStatus = (x, y, status) => { const cell = fences[y].cells[x] // 选择 if (status === EStatus.PENDING) { selected[y] = cell _setCellStatus(x, y, EStatus.SELECTED) } // 反选 else if (status === EStatus.SELECTED) { selected[y] = null _setCellStatus(x, y, EStatus.PENDING) } } [代码] 修改无规格状态 [代码]const _changeOtherCellStatus = (cell, x, y) => { const path = _generatePath(cell, y) if (!path) return // 判断是否存在 if (paths.includes(path)) { _setCellStatus(x, y, EStatus.PENDING) } else { _setCellStatus(x, y, EStatus.DISABLED) } } const _generatePath = (cell, y) => { const path = [] for (let index = 0; index < fences.length; index++) { if (index === y) { if (isSelected(y, cell)) { return } path.push(cell.id) } else { const cell = selected[index] if (cell) { path.push(selected.id) } } } return path.join('#') } const isSelected = (index, cell) => { const value = selected[index] if (!value) { return false } return value.id === cell.id } [代码] 模拟点击规格 [代码]change({x: 0, y: 0, status: EStatus.PENDING}) change({x: 0, y: 1, status: EStatus.PENDING}) change({x: 0, y: 2, status: EStatus.PENDING}) console.log('点击规格') console.log(selected) [代码] 已选择sku的信息 [代码][ { id: '0-0', status: 'selected', value: '黑色' }, { id: '1-0', status: 'selected', value: '圆点' }, { id: '2-0', status: 'selected', value: 'XXL' } ] [代码] 总结 这篇文章主要分享多规格数据的转换,以及通过 [代码]code[代码] 码来获取所有的可视规格,通过行列号获取当前点击的规格以及当前点击的可视规格,比较绕口。查看在线代码示例,直接运行查看结果,也可查看代码片段直接体验demo。后续将继续分享多规格sku的联动,价格、图片、库存等同步更新。由于代码片段包体积有限制,项目如果报ts错误,执行 [代码]npm i[代码] 或者 [代码]yarn add[代码],将小程序的声明依赖添加就行了。
2021-03-29 - 微信支付商户平台(服务商平台)扫码登录后提示“登录超时,请重新登录”时该怎么处理
问题说明 微信支付的服务商 or 商户在登录微信支付平台时,通过扫码方式登录,在手机微信端选择「允许登录」后,商户平台一直提示“登录超时,请重新登录”,很多人这时候会以为是自己有问题,就会重新扫码登录,然后发现还是无法登录,无限死循环。换个浏览器操作,发现又可以正常登录。心里默默的「文明用语」问候一下开发者。 问题复现 访问腾讯地图 map.qq.com后,再访问微信支付商户后台,扫码登录就会出现「登录超时,请重新登录」这个死循环。这时候就有人想说了,你干嘛要访问腾讯地图呢?可能有一些新商户或者新服务商不太了解以前微信支付推出的「智慧经营」活动,也就是早期的「微信支付交易达标免费投放朋友圈广告」功能,这个功能在操作时需要为广告指定一个「门店」,以门店周围3公里为准进行朋友圈广告曝光,这个「门店」添加时需要在腾讯地图上标注过才能在服务商平台里面进行选择,于是访问服务商平台的同时,还会访问腾讯地图查询商户是否可以搜到,如果搜不到就要给商户进行地图标注。 问题来了,这时候微信支付服务商平台还是可以正常使用的,如果你一旦主动退出或会话超时自动退出、切换商户号后,再去登录微信支付商户平台就会出现「登录超时,请重新登录」,此时心里又默默的「文明用语」。当然,如果你打开浏览器后,先访问腾讯地图网站后,再登录微信支付服务商平台,那么同样会出现这个问题。 bug原因 该问题与微信支付商户平台网页的「cookies」有关。如果只登录微信支付商户平台,这个时候平台页面对应的cookies中,只有一个「Name」为 「session_id」 的「cookies」,该「cookies」的「domain」为 「pay.weixin.qq.com」 。 [图片] 如果访问过腾讯地图网站后,那微信支付商户平台页面对应的「cookies」中,就会出现2个「Name」为 「session_id」 的「cookies」,多了一个「domain」为 「.qq.com 」。 [图片] 正是因为这个「cookies」的原因,才导致商户平台出现「登录超时,请重新登录」这个死循环。 如何解决 1、不要在同一个浏览器同时登录微信支付商户平台和腾讯地图网站 2、出现扫码登录确认后,商户平台出现「登录超时,请重新登录」的情况时,清空浏览器浏览记录中的cookies,然后重新扫码登录即可。 3、在浏览器的收藏夹中新增一个书签,名称自己随便取,哪怕你取个「文明用语」,你只要能知道是做什么用的就好了,把下面内容复制添加到网址里面,扫码登录后出现「登录超时,请重新登录」的情况时点击这个添加好的书签,你会神奇的发现,你可以正常访问微信支付商户后台了: [代码]javascript: void((function(){function delecookie(a){var b=new Date;b.setTime(b.getTime()-1e5),document.cookie=a+"=v;expires="+b.toGMTString()+";path=/;domain=.qq.com"}delecookie("session_id");window.location.href = $(".page-error p a").attr("href")})())[代码] 结束语 此问题发现接近「四年」,期间反馈给各种支付、地图各种渠道N次,无奈一直没能解决,希望可以早日修复,大家都不会用到这篇教程。祝大家新的一年里,代码没bug,升职加薪。
2021-03-17 - [开盖即食]基于canvas的“刮刮乐”刮奖组件
[图片] 工作中有时候会遇到一些关于“抽奖”的需求,这次以“刮刮乐项目”举例,分享一个实战抽奖功能。 本人对之前网上流传的一些H5刮刮乐JS插件版本进行了一些改造,使其能适用于实际项目,并且支持小程序canvas 2D的新API,这里顺便提下2D API和实际H5 canvas中JS写法非常类似,只有少数不同。 [图片] 1、方法介绍: 1.1 刮刮乐JS组件 [代码]class Scratch { constructor(page, opts) { opts = opts || {}; this.page = page; this.canvasId = opts.canvasId || 'canvas'; this.width = opts.width || 300; this.height = opts.height || 300; this.bgImg = opts.bgImg || ''; //覆盖的图片 this.maskColor = opts.maskColor || '#edce94'; this.size = opts.size || 15, //this.r = this.size * 2; this.r = this.size; this.area = this.r * this.r; this.showPercent = opts.showPercent || 0.2; //刮开多少比例显示全部 this.rpx = wx.getSystemInfoSync().windowWidth / 750; //设备缩放比例 this.scale = opts.scale || 0.5; this.totalArea = this.width * this.height; this.startCallBack = opts.startCallBack || false; //第一次刮时触发刮奖效果 this.overCallBack = opts.overCallBack || false; //刮奖完触发 this.init(); } init() { let self = this; this.show = false; this.clearPoints = []; const query = wx.createSelectorQuery(); //console.log(this.canvasId); query.select(this.canvasId) .fields({ node: true, size: true }) .exec((res) => { //console.log(res); this.canvas = res[0].node; this.ctx = this.canvas.getContext('2d') this.canvas.width = res[0].width; this.canvas.height = res[0].height; //const dpr = wx.getSystemInfoSync().pixelRatio; //this.canvas.width = res[0].width * dpr; //this.canvas.height = res[0].height * dpr; self.drawMask(); self.bindTouch(); }) } async drawMask() { let self = this; if (self.bgImg) { //判断是否是网络图片 let imgObj = self.canvas.createImage(); if (self.bgImg.indexOf("http") > -1) { await wx.getImageInfo({ src: self.bgImg, //服务器返回的图片地址 success: function (res) { imgObj.src = res.path; //res.path是网络图片的本地地址 }, fail: function (res) { //失败回调 console.log(res); } }); } else { imgObj.src = self.bgImg; //res.path是网络图片的本地地址 } imgObj.onload = function (res) { self.ctx.drawImage(imgObj, 0, 0, self.width * self.rpx, self.height * self.rpx); //方法不执行 } imgObj.onerror = function (res) { console.log('onload失败') //实际执行了此方法 } } else { this.ctx.fillStyle = this.maskColor; this.ctx.fillRect(0, 0, self.width * self.rpx, self.height * self.rpx); } //this.ctx.draw(); } bindTouch() { this.page.touchStart = (e) => { this.eraser(e, true); } this.page.touchMove = (e) => { this.eraser(e, false); } this.page.touchEnd = (e) => { if (this.show) { //this.page.clearCanvas(); if (this.overCallBack) this.overCallBack(); this.ctx.clearRect(0, 0, this.width * this.rpx, this.height * this.rpx); //this.ctx.draw(); } } } eraser(e, bool) { let len = this.clearPoints.length; let count = 0; let x = e.touches[0].x, y = e.touches[0].y; let x1 = x - this.size; let y1 = y - this.size; if (bool) { this.clearPoints.push({ x1: x1, y1: y1, x2: x1 + this.r, y2: y1 + this.r }) } for (let item of this.clearPoints) { if (item.x1 > x || item.y1 > y || item.x2 < x || item.y2 < y) { count++; } else { break; } } if (len === count) { this.clearPoints.push({ x1: x1, y1: y1, x2: x1 + this.r, y2: y1 + this.r }); } //添加计算已清除的面积,达到标准值后,设置刮卡区域刮干净 let clearNum = parseFloat(this.r * this.r * len) / parseFloat(this.scale * this.totalArea); if (!this.show) { this.page.setData({ clearNum: parseFloat(this.r * this.r * len) / parseFloat(this.scale * this.totalArea) }) }; if (this.startCallBack) this.startCallBack(); //console.log(clearNum) if (clearNum > this.showPercent) { //if (len && this.r * this.r * len > this.scale * this.totalArea) { this.show = true; } this.clearArcFun(x, y, this.r, this.ctx); } clearArcFun(x, y, r, ctx) { let stepClear = 1; clearArc(x, y, r); function clearArc(x, y, radius) { let calcWidth = radius - stepClear; let calcHeight = Math.sqrt(radius * radius - calcWidth * calcWidth); let posX = x - calcWidth; let posY = y - calcHeight; let widthX = 2 * calcWidth; let heightY = 2 * calcHeight; if (stepClear <= radius) { ctx.clearRect(posX, posY, widthX, heightY); stepClear += 1; clearArc(x, y, radius); } } } } export default Scratch [代码] 1.2 JS 调用方法 [代码]new Scratch(self, { canvasId: '#coverCanvas', //对应的canvasId width: 600, height: 300, //maskColor:"", //封面颜色 showPercent: 0.3, //刮开多少比例显示全部,比如0.3为 30%面积 bgImg: "./cover.jpg", //封面图片 overCallBack: () => { //刮奖刮完回调函数 }, startCallBack: () => { //当用户触摸canvas板的时候触发回调 } }) [代码] 实际中还支持其他很多的配置项,比如缩放比例,刮开比例,放置区域等等,大家可以根据实际需求设置。 1.3 实际页面中的JS调用方法: [代码]//引入刮刮乐部分 import Scratch from './scratch.js'; const app = getApp() Page({ data: { firstTouch: 0, isOver: 0, }, onLoad() { let self = this; new Scratch(self, { canvasId: '#coverCanvas', width: 600, height: 300, //maskColor:"", //封面颜色 bgImg: "./cover.jpg", //封面图片 overCallBack: () => { this.setData({ isOver: "结束啦" }) //this.clearCanvas(); }, startCallBack: () => { this.setData({ firstTouch: "开始刮啦" }) //this.postScratchSubmit(); } }) }, //刮卡已刮干净 clearCanvas() { let self = this; console.log("over"); }, }) [代码] 1.4 HTML/CSS [代码]<-- html --> <view class="wrap"> <canvas class="cover_canvas" type="2d" disable-scroll="false" id='coverCanvas' bindtouchstart="touchStart" bindtouchmove="touchMove" bindtouchend="touchEnd"></canvas> <image class="img" src="reward.jpg" mode="widthFix" /> </view> /* css */ .wrap { width: 600rpx; height: 300rpx; margin: 100rpx auto; border: 1px solid #000; position: relative; } .cover_canvas { width: 600rpx; height: 300rpx; z-index: 9; } .wrap .img { position: absolute; left: 0; top: 0; z-index: 1; width: 600rpx; height: 300rpx; } [代码] 这里注意 type=“2d” 写法,这里使用的是新的2D canvas。 2、注意事项 canvas一些效果不支持真机调试,直接预览就行了 如果刮奖结果是通过第一次触碰canvas触发的,这里的请求需要写一个同步方法 刮刮乐JS的配置会优先判断bgImg这个属性,再判断maskColor 需要反复刮奖,可以反复new 它。 3、代码片段 地址: https://developers.weixin.qq.com/s/RxiaHam574or 建议将IDE工具升级到 1.03.24以上,避免一些BUG [图片] 觉得有用,请点个赞,这是我继续分享的动力~
2021-02-18 - 如何从零实现上拉无限加载瀑布流组件
代码已优化请查看另外一篇文章 https://developers.weixin.qq.com/community/develop/article/doc/00026c521ece40c2d2db97f7156013 小程序瀑布流组件 前言:为了实现这个组件也花费了些时间,以前也做过瀑布流的功能,不过是利用 js 去 计算图片的高度,然后通过 css 的绝对定位去改变位置。不过这种要提前加载完一个列 表的图片,然后通过排列的算法生成排序的数组。总之就是太复杂了,后来在网上也看到 纯 css 实现,比如 flex 两列布局,columns 等,不做过多的阐述,下面分享下自己项 目中实现的瀑布流过程。 Css Grid 布局 Css3 变量属性 Js 动态修改 css 变量属性 Wxs 小程序脚本语言 Wxml 节点 Api Component 自定义组件 效果图 代码片段 [图片] Css Grid 网格布局实现多列多行布局 [代码]<view class="c-waterfall"> <view wx:for="{{ 10 }}" wx:key="item" class="view-container" > {{ item }} </view> </view> [代码] [代码].c-waterfall { display: grid; grid-template-columns: repeat(2, 1fr); grid-auto-flow: row dense; grid-auto-rows: 10px; grid-gap: 10px; } .view-container { width: 100%; grid-row: auto / span 20; } [代码] Css3 变量,可以通过[代码]js动态[代码]改变 [代码].c-waterfall { --grid-span: 10; --grid-column: 2; --grid-gap: 10px; --grid-rows: 10px; width: 100%; display: grid; grid-template-columns: repeat(var(--grid-column), 1fr); grid-auto-flow: row dense; grid-auto-rows: var(--grid-rows); grid-gap: var(--grid-gap); } .view-container { width: 100%; grid-row: auto / span var(--grid-span); } [代码] 动态修改 css 变量,实现遍历的节点都有独立的样式 [代码]<view class="c-waterfall" style="{{ style }}"> <view wx:for="{{ 10 }}" wx:key="item" class="view-container style="grid-row: auto / span var(--grid-row-{{ index }})" > {{ item }} </view> </view> [代码] [代码]Page({ data: { span: 20, style: '' }, onReady() { this.setData({ style: '--grid-row-0: 10;--grid-row-1: 10;' // 0-9... }) } }) [代码] 显然通过这种方式去修改emmm,有点不尽人意,当view渲染的时候,通过[代码]index[代码]下标给每个view都设置独立的[代码]grid-row[代码]样式,然后在修改view父级的style,将[代码]--grid-row-xxx[代码]变量写进去实现子类继承,虽然比直接去修改每个view的样式要优雅些,但是一旦views的节点多了,100个、1000个、没上限呢,那这个父级的style真的惨不忍睹。。比如100个view,那么style将会是下面这样,所以需要换个思路还是得单独去设置view的样式。 [代码]const views = [...99].map((v, k) => `--grid-row-${k}: 10;`) console.log(views) // ["--grid-row-0: 10;", "--grid-row-1: 10;", ... "--grid-row-2: 10;", "--grid-row-3: 10;", "--grid-row-98: 10;", "--grid-row-99: 10;"] [代码] 通过Wxs脚本语言来修改view的样式,相比较通过[代码]setData[代码]去修改view的样式,wxs的性能绝对比js强。 WXS 不依赖于运行时的基础库版本,可以在所有版本的小程序中运行。 WXS 与 JavaScript 是不同的语言,有自己的语法,并不和 JavaScript 一致。 WXS 的运行环境和其他 JavaScript 代码是隔离的,WXS 中不能调用其他 JavaScript 文件中定义的函数,也不能调用小程序提供的API。 WXS 函数不能作为组件的事件回调。 由于运行环境的差异,在 iOS 设备上小程序内的 WXS 会比 JavaScript 代码快 2 ~ 20 倍。在 android 设备上二者运行效率无差异。 一般在对wxs的使用场景上大多数用来做[代码]computed[代码]计算,因为在[代码]wxml[代码]模板语法里只能进行简单的三元运算,所以一些复杂的运算、逻辑判断等都会放到wxs里面去处理,然后返回给wxml。 [代码]// index.wxs var format = function(string) { return string + 'px' } module.exports = { format: format } [代码] [代码]<!-- index.wxml --> <wxs src="./index.wxs" module="wxs"></wxs> <view>{{ wxs.format('100') }}</view> <view>{{ wxs.format(span) }}</view> <button bind:tap="modifySpan">修改span的值</button> [代码] [代码]// index.js page({ data: { span }, modifySpan() { this.setData({ span: '200' }) } }) [代码] 通过WXS响应事件来修改视图层[代码]Webview[代码],跳过逻辑层[代码]App Service[代码],减少性能开销,比如一些频繁响应的事件监听,滚动条位置,手指滑动位置等,通过wxs来做视图层的修改,大大提升了流畅度。 通过wxs响应原生组件的事件,[代码]image[代码]组件的[代码]bind:load[代码]事件 [代码]<!-- index.html --> <wxs src="./index.wxs" module="wxs"></wxs> <image class="image" src="https://hbimg.huabanimg.com/ccf4a904deaebc25990a47471c61ea1c765694f82633b-71iPZs_/fw/480/format/webp" bind:load="{{ wxs.loadImg }}" /> [代码] [代码]// index.wxs var loadImg = function(event, ownerInstance) { // image组件load加载完返回图片的信息 var image = event.detail // 获取image的实例 var imageDom = ownerInstance.selectComponent('.image') // 设置image的样式 imageDom.setStyle({ height: image.height + 'px', background: 'red' // ... }) // 给image添加class imageDom.addClass('.loaded') // 更多的功能请参考文档 // https://developers.weixin.qq.com/miniprogram/dev/framework/view/interactive-animation.html } module.exports = { loadImg: loadImg } [代码] wxs监听data的值 [代码]<!-- index.html --> <wxs src="./index.wxs" module="wxs"></wxs> <view class="container"> <view change:text="{{ wxs.changeText }}" text="{{ text }}" class="text" data-options="{{ options }}" > {{ text }} </view> <view class="child-node"> this is childNode </view> <!-- 某个自定义组件 --> <test-component class="other-node" /> </view> [代码] [代码]// index.wxs var changeText = function(newValue, oldValue, ownerInstance, instance) { // 获取修改后的text var text = newValue // 获取data-options var options = instance.getDataset() // 获取当前页面的任意节点实例 var childNode = instance.selectComponent('.container .child-node') // 修改childNode样式 childNode.setStyle({ color: 'gree' }) // 获取页面的自定义组件 var otherNode = instance.selectComponent('.container .other-node') // 获取自定义组件内的节点实例 // 通过css选择器 > var otherChildNode = instance.selectComponent('.container .other-node >>> .other-child-node') // 获取自定义组件内部节点的样式 var style = otherChildNode.getComputedStyle(['width', 'height']) // 更多功能看文档 } module.exports = { changeText: changeText } [代码] 通过[代码]createSelectorQuery[代码]获取节点的信息,用来后续计算[代码]grid-row[代码]的参数 [代码]Page({ onReady() { wx.createSelectorQuery(this) .select('.view-container') .fields({size: true}) .exec((res) => { console.log(res) // [{width: 375, height: 390}] }) } }) [代码] 创建waterfall自定义组件 waterfall组件的职责,做成组件有什么好处,不做成组件又有什么好处,以及通过抽象节点来实现多组件复用。 prop的基本设置参数 [代码]Component({ properties: { views: Array, // 需要渲染的瀑布流视图列表 options: { // 瀑布流的参数定义 type: Object, default: { span: 20, // 节点高度比 column: 2, // 显示几列 gap: [10, 10], // xy轴边距,单位px rows: 2, // 网格的高度,单位px }, } } }) [代码] 组件内部默认的样式 [代码].c-waterfall { --grid-span: 10; --grid-column: 2; --grid-gap: 10px; --grid-rows: 10px; width: 100%; display: grid; grid-template-columns: repeat(var(--grid-column), 1fr); grid-auto-flow: row dense; grid-auto-rows: var(--grid-rows); grid-gap: var(--grid-gap); } .view-container { width: 100%; grid-row: auto / span var(--grid-span); } [代码] 组件的骨架 [代码]<wxs src="./index.wxs" module="wx" ></wxs> <!-- 样式承载节点 --> <view class="c-waterfall" change:loadStatus="{{ wx.load }}" loadStatus="{{ childNode }}" data-options="{{ options }}" style="{{ wx.setStyle(options) }}" > <!-- 抽象节点 --> <selectable class="view-container" id="view-{{ index }}" wx:for="{{ views }}" wx:key="item" value="{{ item }}" index="{{ index }}" bind:load="load" > </selectable> </view> [代码] 抽象节点 [代码]{ "component": true, "usingComponents": {}, "componentGenerics": { "selectable": true } } [代码] 抽象节点应该遵循什么 [代码]Component({ properties: { value: Object, // 组件自身需要的数据 index: Number, // 下标值 }, methods: { load(event) { // load节点响应事件 this.triggerEvent('load', { ...this.data, // value必填参数 {width,height} value: { ...event.detail }, }) }, }, }) [代码] 组件wxs响应事件 [代码].c-waterfall[代码]样式承载节点,主要是设置options传入的参数 [代码] var _getGap = function (gaps) { return gaps .map(function (v) { return v + 'px' }) .join(' ') } var setStyle = function (options) { if (!options) return var style = [ '--grid-span: ' + options.span || 10, '--grid-column: ' + options.column || 2, '--grid-gap: ' + _getGap(options.gap || [10, 10]), '--grid-rows: ' + (options.rows || 10) + 'px', ] return style.join(';') } [代码] 获取瀑布流样式承载节点实例 [代码] var _getWaterfall = function (dom) { var waterfallDom = dom.selectComponent('.c-waterfall') return { dom: waterfallDom, options: waterfallDom.getDataset().options, } } [代码] 获取事件触发的节点实例 [代码] var _getView = function (index, dom) { var viewDom = dom.selectComponent('.c-waterfall >>> #view-' + index) return { dom: viewDom, style: viewDom.getComputedStyle(['width', 'height']), } } [代码] 获取虚拟节点自定义组件load节点实例,初始化渲染时,节点是未知的,比如image组件,图片的宽高是未知的,需要等到image加载完成才会知道宽高,该节点用于存放异步视图展示,然后通过事件回调计算出节点高度。 [代码] var _getLoadView = function (index, dom) { return { dom: dom.selectComponent( '.c-waterfall >>> #view-' + index + '>>>.waterfall-load-node' ), } } [代码] 获取虚拟节点自定义组件other节点实例,初始化渲染就存在节点,比如一些文字就放在该节点,具体由组件的创造者去自定义。 [代码] var _getOtherView = function (index, dom) { var other = dom.selectComponent( '.c-waterfall >>> #view-' + index + '>>> .waterfall-load-other' ) return { dom: other, style: other.getComputedStyle(['height', 'width']), } } [代码] 已知瀑布流样式承载节点的宽度,等load节点异步视图回调时,获取到load节点的实际高度,比如一张400*800的图片,如果要显示在一个宽度180px的视图里,注意:[代码]image[代码]组件会有默认高度240px,或者用户自己设置了高度。如果要实现瀑布流,还是需要通过计算图片的宽高比例得到图片在视图中宽高,然后再通过计算grid布局的span值实现填充。 [代码] var fix = function (string) { if (typeof string === 'number') return string return Number(string.replace('px', '')) } var computedContainerHeight = function (node, view) { var vW = fix(view.width) var nW = fix(node.width) var nH = fix(node.height) var scale = nW / vW return { width: vW, height: nH / scale, } } [代码] 通过公式计算span的值,这个公式也是花了我不少时间去研究的,对grid布局使用也不多,很多潜在用法并不知道,所以通过大量的随机数据对比查找规律所在。[代码]gap为数组[x, y][代码],我们要取y计算,已知gap、rows求视图中节点高度[代码](gap[y] + rows) * span - gap[y] = height[代码],有了求height的公式,那么求span就简单了,[代码](height + gap[y]) / (gap[y] + rows) = span[代码],最终视图里的高度会跟计算出来的结果几个像素的误差,因为[代码]grid-row[代码]设置span不能为小数,只能为整数,而我们瀑布流的高度是未知的,通过计算有多位浮点数,所以只能向上取整了导致有几个像素的误差。 [代码] var computedSpan = function (height, options) { var rows = options.rows var gap = options.gap[1] var span = Math.ceil((height + gap) / (gap + rows)) return span } [代码] 最后我们能得到[代码]span[代码]的值了,只需要将[代码]load完成的视图修改样式即可[代码] [代码] var load = function (node, oldNode, dom) { if (!node.value) return false var index = node.index var waterfall = _getWaterfall(dom) // 获取虚拟组件,通过index下标确认是哪个,获取宽度高度 var view = _getView(index, dom) var otherView = _getOtherView(index, dom) var otherViewHeight = fix(otherView.style.height) // 计算虚拟组件的高度,其实就是计算图片在当前视图节点里的宽高比例 // image组件的mode="widthFix"也是这样计算的额 var virtualStyle = computedContainerHeight(node.value, view.style) // span取值,此处计算的高度应该是整个虚拟节点视图的高度 // load事件回调里,我们只传了load视图节点的宽高 // 后续通过selectComponent获取到了other视图节点的高度 var span = computedSpan( otherViewHeight + virtualStyle.height, waterfall.options ) // 设置虚拟组件的样式 view.dom.setStyle({ 'grid-row': 'auto / span ' + span, }) // 获取重新渲染后的虚拟组件高度 var viewHeight = view.dom.getComputedStyle(['width', 'height']) viewHeight = fix(viewHeight.height) // 上面说了因为浮点数的计算会导致有几个像素的误差 // 为了视图美观,我们将load视图节点的高度设置成虚拟视图节点的总高度减去静态节点的高度 var loadView = _getLoadView(index, dom) loadView.dom.setStyle({ width: virtualStyle.width + 'px', height: parseInt(viewHeight - otherViewHeight) + 'px', opacity: 1, visibility: 'visible', }) return false } module.exports = { load: load, setStyle: setStyle, } [代码] 抽离成虚拟节点自定义组件的利弊 利: 符合观察者模式的设计模式 降低代码耦合度 扩展性强 代码清晰 弊: 节点增加,如果视图节点过多会造成小程序性能警告 样式编写不便捷,需要写过多的判断代码去实现外部样式覆盖 wxs只能监听原生组件的事件,所以image的load事件触发时本可以直接去修改页面视图节点样式,不需要传回给父组件,然后父组件setData下标,wxs监听事件触发在去修改视图样式,多了一次setData的开销。 合: 时间有限没有扩展样式覆盖了,可以开启自定义组件的外部样式引入 节点过多的问题,在我自己电脑上,开发工具插入100个组件时,出现了卡顿,样式错乱,真机上目前还没发现上限。 后续想实现长列表功能,有回收机制,这样视图内的节点有限了,降低了性能开销,因为之前版本的长列表组件是通过[代码]createSelectorQuery[代码]获取节点信息,然后记录高度,通过创建[代码]createIntersectionObserver[代码]监听视图节点是否在视图来判断是否渲染。但是瀑布流有异步视图,初次渲染的高度跟异步加载完的高度是不一样,所以创建监听事件高度会不准确,若等到load完再创建监听事件,父级容器的高度又要经过计算,因为子节点会去填充空白区域实现瀑布流,目前项目中为了避免节点过大造成性能警告,加了item的个数限制,如果超过100或者1000个就清空数组,类似分页的功能。不过上面总结的思路可以去试试。 等把功能完善了,发布npm依赖包安装。 后续有时间会将项目里比较实用的组件抽离出来。。 自定义tabbar 自定义navbar 长列表 下拉刷新 上拉加载 购物车sku … Demo page调用页面 [代码]<view class="container"> <waterfall wx:if="{{ _type === 0 }}" generic:selectable="test-view" views="{{ views }}" options="{{ options }}" /> <waterfall wx:else generic:selectable="image-view" views="{{ images }}" options="{{ options }}" /> </view> <view class="btns"> <button bind:tap="loadView">模拟节点</button> <button bind:tap="loadImage">远程图片</button> </view> [代码] [代码]Page({ data: { views: [], loading: false, options: { span: 30, column: 2, gap: [10, 10], rows: 2, }, images: [], _page: 1, _type: 0, }, onLoad() { // 生成随机数据 // this.generateViews() // this.getHuaBanList() }, loadView() { this.data._page = 1 this.setData({ images: [], _type: 0 }) this.generateViews() }, loadImage() { this.data._type = 1 this.setData({ views: [], _type: 1 }) this.getHuaBanList() }, getHuaBanList() { let { images, _page } = this.data wx.request({ url: `https://huaban.com/search/?q=随机&page=${_page}&per_page=10&wfl=1`, header: { accept: 'application/json', 'accept-language': 'zh-CN,zh;q=0.9', 'x-request': 'JSON', 'x-requested-with': 'XMLHttpRequest', }, success: (res) => { res.data.pins.map((v) => { images.push({ url: `https://hbimg.huabanimg.com/${v.file.key}_/fw/480/format/webp`, title: v.raw_text, }) }) this.setData({ images, _page: ++_page }) wx.hideLoading() }, }) }, generateViews() { const { views } = this.data for (let i = 0; i < 10; i++) { views.push({ width: this._randomNum(150, 500) + 'px', height: this._randomNum(200, 600) + 'px', }) } this.setData({ views, }) }, _randomNum(minNum, maxNum) { switch (arguments.length) { case 1: return parseInt(String(Math.random() * minNum + 1), 10) break case 2: return parseInt(Math.random() * (maxNum - minNum + 1) + minNum, 10) break default: return 0 break } }, onReachBottom() { let { loading, _type } = this.data if (!loading) { wx.showLoading({ title: 'loading...', }) loading = true setTimeout(() => { _type === 0 ? this.generateViews() : this.getHuaBanList() wx.hideLoading() loading = false }, 1000) } }, }) [代码] [代码]{ "usingComponents": { "waterfall": "/components/waterfall/index", "test-view": "/components/test-view/index", "image-view": "/components/image-view/index" } } [代码] 模拟load异步的自定义组件 [代码]<view class="c-test-view"> <view class="waterfall-load-node"> {{value.width}}*{{value.height}} </view> <view class="waterfall-load-other">模拟加载图片</view> </view> [代码] [代码]Component({ properties: { value: Object, index: Number, }, lifetimes: { ready() { const { index } = this.data const timer = 1000 + 300 * String(index).charAt(index.length - 1) setTimeout(() => this.load(), timer) }, }, methods: { load() { this.triggerEvent('load', { ...this.data, }) }, }, }) [代码] [代码].c-test-view { width: 100%; height: 100%; display: flex; flex-flow: column; justify-content: center; align-items: center; background: white; } .c-test-view .waterfall-load-node { height: 50%; flex-grow: 1; transition: all 0.3s; display: inline-flex; flex-flow: column; justify-content: center; align-items: center; background: #eeeeee; width: 100%; opacity: 0; } .c-test-view .waterfall-load-other { width: 100%; height: 80rpx; display: inline-flex; justify-content: center; align-items: center; background: cornflowerblue; color: white; } [代码] 随机获取花瓣网图片的自定义组件 [代码]<view class="c-image-view"> <view class="waterfall-load-node"> <image class="load-image" src="{{ value.url }}" bind:load="load" /> </view> <view class="waterfall-load-other">{{ value.title }}</view> </view> [代码] [代码]Component({ properties: { value: Object, index: Number, }, lifetimes: { ready() {}, }, methods: { load(event) { this.triggerEvent('load', { ...this.data, value: { ...event.detail }, }) }, }, }) [代码] [代码].c-image-view { width: 100%; display: inline-flex; flex-flow: column; background: white; border-radius: 10px; overflow: hidden; height: 100%; } .c-image-view .waterfall-load-node { width: 100%; height: 50%; display: inline-flex; flex-grow: 1; background: gainsboro; transition: opacity 0.3s; opacity: 0; overflow: hidden; visibility: hidden; } .c-image-view .waterfall-load-node .load-image { width: 100%; height: 100%; overflow: hidden; } .c-image-view .waterfall-load-other { font-size: 30rpx; background: white; min-height: 60rpx; padding: 10px; display: flex; align-items: center; } [代码] 代码片段 https://developers.weixin.qq.com/s/Q02FETmW7ind
2021-03-19 - 服务商政策|流程|能力|接口变更通知(2021.1.8)
一、试用小程序产品能力正式上线第三方开发者可通过创建试用小程序接口提供快速注册试用小程序的能力,商家只需填写小程序名称、头像并进行实名授权即可创建试用小程序,总共耗时约在1分钟。体验后完成认证流程,即可转为正式小程序。 二、第三方平台票据长时间不接收则主动停止推送通知为了减少无效的票据推送以及无效的告警信息,从2021年2月1日起,平台将停止推送票据给超过90天没有正确接收component_verify_ticket的第三方平台,被停止推送的第三方平台若要重新启用服务可通过启动ticket推送服务接口重新启动推送。 三、第三方平台全网发布自动化测试新增测试号为了提高第三方平台全网发布自动化测试的效率,从2021年1月18日起,平台将分别新增4组公众号和小程序的专用测试号。详情可查看平台型第三方平台全网发布接入检测说明,请及时对新增的测试号配置测试 Case,以免影响提交第三方平台全网发布。 四、小程序开放接口wx.addCard将不再支持用户单次领取多张卡包会员卡为进一步维护用户端体验,2021年1月20日起,小程序开放接口wx.addCard将不再支持用户单次领取超过1张的“卡包会员卡”,其他功能不受影响。详情参考: https://developers.weixin.qq.com/community/develop/doc/00060aa283430885398b44a7c5d001?blockType=1 五、小程序类目资质更新[图片] 详情请参考小程序开放的服务类目 第三方快速创建的小程序可选择的类目参考“第三方平台-快速创建小程序接口-类目参考表”
2021-01-08 - 做个优秀的小程序 - 体验评分
随着小程序的开发迭代,慢慢的我们会更加关注小程序的质量,今天来讲讲小程序的隐藏功能 -- 体验评分。 为什么需要体验评分 我们多做一点,就可以给用户更好的体验。(窃喜) 当然,做为开发者的我们,动动鼠标点一点就能帮助我们发现问题,是不是很愉快~~ 接下来我们来看看怎么使用体验评分? 怎么使用体验评分 体验评分的能力目前开放在【微信开发者工具 - 调试器 - Audits】 操作步骤:运行体验评分 - 一顿操作 - 获取体验报告 - 一顿优化。 (优化其实是一个圈,新代码加上之后也要继续关注哦~) [图片] 体验评分实践 我们用《小程序示例》来操作一波看看效果~ 01. 运行体验评分 使用开发者工具打开小程序,调试器区域切换到 Audits 面板,就一个“运行”按钮,点它。 [图片] 02.一顿操作 然后在工具上对小程序进行操作,比如:我点开了 “接口 - 媒体 - 音频 - 播放 ”。 [图片] 03.获取体验报告 操作完之后,点击“停止”,我们就可以获取到体验报告(简单~)。 [图片] 拿到报告之后,我们就可以看到总分 98,最佳实践 80。往下拉会有扣分的实际原因。 看第一条是 “发现正在使用废弃接口”,报告已经很清楚的告诉我们使用了废弃组件 audio,我们根据报告进行优化即可。 [图片] 04.一顿优化 按照报告优化完之后,我们可以继续进行体验评分功能确认优化是否完善。这是一个有用的圈圈⚪⚪⚪ 我们来讲几个优化过程中遇到的问题,咳咳咳 存在图片没有按原图宽高比显示 [图片] 在测试预览图片的时候,发现图片被挤了,体验评分告诉我们宽高比有问题,发现是 <image> 使用了默认的 mode (scaleToFill:缩放模式,不保持纵横比缩放图片,使图片的宽高完全拉伸至填满 image 元素)。所以通过添加 mode="aspectFill" (缩放模式,保持纵横比缩放图片,只保证图片的短边能完全显示出来。)来解决宽高比的问题。 [图片] [图片] 发现固定底部的可点击组件可能不在iPhone X安全区域内 [图片] 这个问题我们用手机测试是正常的,但是体验评分给了提示,所以就来看看实现方式是不是有问题: 原有方式:通过接口监听systemInfo.model.indexOf('iPhone X') 给 view 添加专属 class 官方推荐:官方推荐的方式是用 wxss 来兼容,不一定只有 iPhone X 下面会有安全区域 [图片] 发现正在使用废弃接口 [图片] 这个问题对一些老旧代码来说很有用,比如示例很久之前写的 auto-focus,由于基本没有改动,所以代码就一直保持不变。使用体验评分的时候检测到了这个属性是废弃属性,所以我们更换了可用属性 focus 来解决问题。 [图片] 体验评分总结 使用体验评分进行小程序示例的优化,有以下优点: 可以发现代码中使用的废弃api,避免后续踩坑根据实际操作发现相关耗时久的情况,预先发现体验问题合理的视觉/交互检测,提前做好兼容资源使用检测,用合适的资源做好小程序当然,体验过程中也有不足: 开发者工具不支持预览的 组件 / API 暂不支持体验评分(听说官方已经在努力推进啦)一起体验评分 如果你也在做小程序优化,欢迎使用体验评分来优化哦~ 预祝大家都拿 100婚 !!! [图片] 体验评分文档传送门 如果你有疑问,请在下方评论区留言给binnie,㊗️大家都没有bug,✌️✌️✌️
2020-12-04 - 【问题排查】小程序闪退
在使用小程序的时候,偶然会发生闪退。这里来讲一下闪退的问题该如何排查。 版本排查 发生闪退的时候,首先,要确认下 版本 是不是最新的。如果不是,建议更新版本再重试。旧版本的问题会在新版本进行修复哦。 微信版本: 微信官网 基础库版本:基础库更新日志小程序自查 确认版本都是最新情况下,还是有闪退的问题的话,建议先进行小程序自查~ 一般情况下,闪退是因为内存使用过多导致的,小程序侧可以通过基础库提供 wx.onMemoryWarning 接口来监听内存不足的告警,当收到告警时,通过回收一些不必要资源避免进一步加剧内存紧张。 反馈官方 如果问题还是会出现的话建议反馈给官方处理,需要附带上以下信息点协助排查(划重点:完整的提供信息才可以加速问题处理进度哦!!!) 示例: 系统及微信版本号:安卓7.0.17、IOS 7.0.17(出现问题的时候,建议两端都测试,给出有问题的case)必现 or 偶现:必现可复现场景:代码片段 或者 线上小程序复现步骤:进入首页,点击添加按钮等等,推荐录制复现的 视频(重点)进行上传。上传日志:提供微信号,复现时间点(操作步骤:手机微信那里上传下日志: 我 -> 设置 -> 帮助与反馈:右上角扳手 -> 上报日志,选择出现问题的日期,上传日志)
2020-11-03 - #小程序云开发挑战赛#-垃圾问问-微旺网络
应用场景“垃圾问问”是为了方便居民日差查询垃圾分类、了解垃圾分类政策和知识的小程序。 垃圾分类正成为国民生活新时尚,各地都推出了系列垃圾分类新举措。垃圾问问不仅提供了垃圾分类的知识,还可以根据城市切换、垃圾库实时更新。比如宠物粪便、口红等冷门分类,都能在”垃圾问问“里得到答案。贴心的语音查询,更是为不方便输入的用户提供了便利。 目标用户对于某些垃圾不知道如何归类投放的用户。 实现思路本小程序采用基于云开发的原生开发,用到了云数据库存储数据,使用云函数和小程序端进行数据交互。 A. 整体架构图如下: [图片] b. 云函数端 使用tcb-router路由分发,代码结构更利于功能路由的规划,未来功能模块的横向展开。 c. 小程序端 引入mobx支持,划分为vm层(mobx store,page+wxml)以及service层,结构清晰,易于扩展,同时mobx store的存在,也让多page、多组件之间的通讯变的简单。 小程序端架构如下: [图片] 效果截图[图片] [图片] [图片] [图片] [图片] [图片] 作品体验二维码[图片] 功能演示腾讯视频:https://v.qq.com/x/page/f3152chjcwu.html 团队简介本团队的核心成员来自微旺网络科技有限公司,在微信开放社区也积极活跃。
2020-09-16 - 小程序页面(Page)扩展,为所有页面添加公共的生命周期、事件处理等函数
背景 在小程序的原生开发中,页面中经常会用到一些公共方法,例如在页面onLoad中验证权限、所有页面都需要onShareAppMessage设置分享等 假设我们在编码时每个页面都写一遍,显然不是一个高级程序员会干的事情,太Low了。如果我们定义一个公共文件,导出这些公共方法,每个页面都引入,然后再生命周期或者事件处理函数中调用,虽然看起来很方便,但不够优雅,达不到我们最终的目的(偷懒)。 下面给大家介绍一种相对比较优雅的实现方式,扩展Page来实现以上的操作。 Page(页面) 需要传入的是一个 [代码]object[代码] 类型的参数,那么我们重载一个 [代码]Page[代码] 函数,将这个 [代码]object[代码] 参数拦截改掉就可以了,下面直接上代码。 实现 1、在根目录新建一个 [代码]page-extend.js[代码] 文件,公共的逻辑都写在这里面 [代码]/** * * Page扩展函数 * * @param {*} Page 原生Page */ const pageExtend = Page => { return object => { // 导出原生Page传入的object参数中的生命周期函数 // 由于命名冲突,所以将onLoad生命周期函数命名成了onLoaded const { onLoaded } = object // 公共的onLoad生命周期函数 object.onLoad = function (options) { // 在onLoad中执行的代码 ... // 执行onLoaded生命周期函数 if (typeof onLoaded === 'function') { onLoaded.call(this, options) } } // 公共的onShareAppMessage事件处理函数 object.onShareAppMessage = () => { return { title: '分享标题', imageUrl: '分享封面' } } return Page(object) } } // 获取原生Page const originalPage = Page // 定义一个新的Page,将原生Page传入Page扩展函数 Page = pageExtend(originalPage) [代码] 2、在 [代码]app.js[代码] 中引入 [代码]page-extend.js[代码] 文件 [代码]require('./page-extend') App({ // 其他代码 ... }) [代码] 代码片段 https://developers.weixin.qq.com/s/Cyx8iGmV7Ldp 本文内容及评论未经允许,禁止任何形式的转载与复制(代码可在程序中使用)
2019-12-24 - Parser-基于wxParse的二次开发
在社区文章里看到老哥分享的。尝试使用了下。
2019-11-21 - Vue项目使用kbone打包成小程序的踩坑笔记
以下内容是初版kbone的笔记,现在kbone进行了大量的更新升级,以下配置方法大部分已经不适用新版kbone了,如需详细了解kbone的使用方法,请参考官方文档 kbone是大佬 @June 开发的一款Vue项目打包成小程序的工具(暂时只支持微信小程序) 已有的Vue项目只需通过简单的配置即可打包成小程序的代码,新项目也可以使用这个工具进行同步开发,项目中提供了很多示例供参考 Github连接:https://github.com/wechat-miniprogram/kbone 为什么选择kbone? 市面上有很多跨平台的框架,如: WePY、mpvue、uni-app、taro等 他们都有一些弊端和限制,现有的项目迁移不方便,对第三方组件库不兼容等 kbone作为一款工具,也可以说是一款webpack插件(Vue项目只需要引入 mp-webpack-plugin 插件,通过简单的webpack配置即可),无论是对新项目还是已有项目都是非常友好的,因为他完整的移植了Vue Runtime,通过webpack将Vue代码打包成小程序兼容的代码,通过适配器运行,所以大部分的第三方组件库是兼容的 如何使用? Github仓库中提供了很多示例,有兴趣的可以体验下,下面介绍下我的使用过程 创建项目 因为习惯了使用Vue Cli,所以我选择使用这个工具初始化一个项目,使用的版本是Vue Cli 3(这个工具提供了一套完全图形化的用户界面,这里就不介绍使用方法了,没有使用过的同学可以 点击这里 查看创建方法) 集成插件 kbone目前还没有提供Vue Cli的插件(大佬很忙,有能力的同学可以在Github上提PR),所以需要手动添加,具体的操作方式请参考QuickStart文档 安装 mp-webpack-plugin 插件 [代码]yarn add mp-webpack-plugin --dev # 或者 npm install mp-webpack-plugin --save-dev [代码] 在 src 目录中新增 main.mp.js 入口文件 [代码]import Vue from 'vue' import App from '@/App' import router from '@/router' import store from '@/store' // 需要将创建根组件实例的逻辑封装成方法 export default function createApp () { // 在小程序中如果要注入到 id 为 app 的 dom 节点上,需要主动创建 const container = document.createElement('div') container.id = 'app' document.body.appendChild(container) Vue.config.productionTip = false return new Vue({ router, store, render: h => h(App) }).$mount('#app') } [代码] 在根目录创建 miniprogram.config.js 文件,添加 mp-webpack-plugin 插件配置 [代码]module.exports = { // 页面 origin,默认是 https://miniprogram.default origin: '', // 填写项目中的图片资源地址,建议图片资源使用线上地址 // 入口页面路由,默认是 / entry: '/', // 页面路由,用于页面间跳转 router: { // 路由可以是多个值,支持动态路由 index: [] }, // 特殊路由跳转 redirect: { // 跳转遇到同一个 origin 但是不在 router 里的页面时处理方式,支持的值:webview - 使用 web-view 组件打开;error - 抛出异常;none - 默认值;什么都不做,router 配置项中的 key notFound: 'index', // 跳转到 origin 之外的页面时处理方式,值同 notFound accessDenied: 'index' }, // app 配置,同 https://developers.weixin.qq.com/miniprogram/dev/reference/configuration/app.html#window app: { navigationStyle: 'custom' // 自定义navigation }, // 全局配置 global: {}, // 页面配置,可以为单个页面做个性化处理,覆盖全局配置 pages: {}, // 优化 optimization: { domSubTreeLevel: 5, // 将多少层级的 dom 子树作为一个自定义组件渲染,支持 1 - 5,默认值为 5 // 对象复用,当页面被关闭时会回收对象,但是如果有地方保留有对象引用的话,注意要关闭此项,否则可能出问题 elementMultiplexing: true, // element 节点复用 textMultiplexing: true, // 文本节点复用 commentMultiplexing: true, // 注释节点复用 domExtendMultiplexing: true, // 节点相关对象复用,如 style、classList 对象等 styleValueReduce: 5000, // 如果设置 style 属性时存在某个属性的值超过一定值,则进行删减 attrValueReduce: 5000 // 如果设置 dom 属性时存在某个属性的值超过一定值,则进行删减 }, // 项目配置,会被合并到 project.config.json projectConfig: { appid: '', // 填写小程序的AppId projectname: '' // 填写小程序的项目名称 }, // 包配置,会被合并到 package.json packageConfig: { name: '', // 项目名称 description: '', // 描述 author: '' // 作者信息 } } [代码] 在根目录创建 .env.mp 文件,添加 mp 环境变量 [代码]NODE_ENV = mp [代码] 修改 vue.config.js 文件,添加打包小程序的 webpack 配置 [代码]const path = require('path') function resolve (dir) { return path.join(__dirname, dir) } const webpack = require('webpack') const MpWebpackPlugin = require('mp-webpack-plugin') const MiniCssExtractPlugin = require('mini-css-extract-plugin') module.exports = { css: { extract: true }, outputDir: process.env.NODE_ENV === 'mp' ? './dist/mp/common' : './dist/web', configureWebpack: { resolve: { extensions: ['*', '.js', '.vue', '.json'], alias: { 'vue$': 'vue/dist/vue.esm.js', '@': resolve('src') } } }, chainWebpack: config => { if (process.env.NODE_ENV === 'mp') { config .devtool('node') .entry('app').clear().add('./src/main.mp.js').end() .output.filename('[name].js') .library('createApp') .libraryExport('default') .libraryTarget('window').end() .target('web') .optimization.runtimeChunk(false) .splitChunks({ chunks: 'all', minSize: 1000, maxSize: 0, minChunks: 1, maxAsyncRequests: 100, maxInitialRequests: 100, automaticNameDelimiter: '~', name: true, cacheGroups: { vendors: { test: /[\\/]node_modules[\\/]/, priority: -10 }, default: { minChunks: 2, priority: -20, reuseExistingChunk: true } } }).end() .plugins.delete('copy').end() .plugin('define').use(new webpack.DefinePlugin({ 'process.env.isMiniprogram': process.env.isMiniprogram // 注入环境变量,用于业务代码判断 })).end() .plugin('extract-css').use(new MiniCssExtractPlugin({ filename: '[name].wxss', chunkFilename: '[name].wxss' })).end() .plugin('mp-webpack').use(new MpWebpackPlugin(require('./miniprogram.config.js'))).end() } } } [代码] 修改 package.json 中的 scripts 属性,添加用于开发和打包的任务 [代码]# 开发 "mp-serve": "vue-cli-service build --watch --mode mp" # 打包 "mp-build": "vue-cli-service build --mode mp" [代码] 配置完成,尽情玩耍吧! 目前已知的问题 小程序中无法支持 getComputedStyle 和 getBoundingClientRect 的同步接口,只能异步,所以依赖这两个方法的第三方库,需要进行改造 因为大佬很忙目前还处于开发阶段,很多组件还没有适配,有能力和有精力的同学欢迎去Github提PR 再次奉上Github连接:https://github.com/wechat-miniprogram/kbone
2019-12-10 - 关于unionid,以及手机浏览器唤起微信登录?知道的请解答一下
全屏编辑 我公众号有一个微信登录,然后pc网页端也有一个微信登录。他们两个appid获得的openid是不一样的。现在我登录的时候账号要互通,用unionid能确定要登录哪个会员号,但是,登录后如果我要给它推送消息呢?我得自己实现一套绑定,先利用unionid找到对应的会员号,然后利用会员号,找到公众号取得的openid吗?感觉这样处理很复杂啊,难道不应该是微信有统一接口,输入unionid就能推到消息给用户吗,也就是所有传入openid的地方,传unionid就能调用,不然的话我们开发者就得去实现一套帐号流程去各种找openid。谁知道是不是我想的这样,麻烦解答。还有一个问题,我发现 m.jd.com 在手机浏览器内访问,点击微信登录,能唤醒手机微信进行授权后跳回到浏览器,请问这是如何实现的?官方好像没有任何说明,求解
2019-07-23 - 「笔记」开发微信开放社区小程序版
前言 去年在微信公开课小程序里就看到有社区版块,但是就很好奇为啥社区自己不出一个小程序版本的。至少在3月份以前,微信开放社区在手机端虽然进行了简单适配,但是访问的时候用户体验并不是很好,到目前为止手机端访问社区时依然不支持搜索和回复功能。 微信开放社区已经做了手机的适配,为啥还要自己做一个小程序版本的呢? 2月13日在群里聊天的时候 @Stephen 突然发了一个社区列表接口地址,就聊起了要利用公开接口来自己做一个用于查看社区信息的想法。 隔了将近一周,有天实在是闲得没啥事干,于是就开始研究社区的接口来,大概用了一个上午的时间理清了社区栏目和文章页的接口信息和参数含义,接下来就可以做自己的小程序页面了。 体验地址 https://www.qzwu.com/WeChat 开发浏览功能 问题解答接口: https://developers.weixin.qq.com/community/ngi/timeline/{参数1}/{参数2}/{参数3}?page=1 [代码]参数1:版块分类 1:小程序 2:小游戏 8:微信支付 参数2:类型 1:默认 2:公告 参数3:标签ID 0:默认 [代码] 列表接口: https://developers.weixin.qq.com/community/ngi/{参数1}/list/{参数2}?page=1&blockType={参数3} [代码]参数1:文章分类 doc:普通帖子 article:文章 参数2:栏目ID 参数3:版块分类 1:小程序 2:小游戏 8:微信支付 [代码] 搜索接口: https://developers.weixin.qq.com/community/ngi/search?query=关键词&page=1&blogCategory={参数1} [代码]参数1:分类 511:相关帖子 1024:官方教程 512:小故事 [代码] 有了以上3个接口,简单的社区就能做出来了。 开发社区登陆功能 开发社区回复功能首先要解决登陆的问题,官方自然不会有API文档让小程序接入社区。以下登陆流程主要灵感来源于 @这都申请了 提供的思路。 1.获取state [代码]https://developers.weixin.qq.com/community/ngi/ [代码] 2.获取登录二维码页面 [代码]https://open.weixin.qq.com/connect/qrconnect?appid=wx1bb297ee890403a9&scope=snsapi_login&redirect_uri=https://developers.weixin.qq.com&state={state}&login_type=jssdk [代码] 3.解析获取登录二维码以及二维码的uuid 4.监听登录状态(超时时间设置为60秒) [代码]https://long.open.weixin.qq.com/connect/l/qrconnect?uuid={uuid}&_={随机数} [代码] 5.监听返回内容 wx_errcode为405代表登录成功,获取wx_code 6.登录身份验证地址获取用户cookie [代码]https://developers.weixin.qq.com/community/ngi/welogin?type=0&redirect_url=&code={wx_code}&state={state} [代码] 7.解析上一步页面中的set-cookie并保存到数据库 8.生成对应的小程序码 9.扫码进入小程序与小程序内登陆用户的openId或其它用户标识信息绑定 开发评论功能 发布评论接口: https://developers.weixin.qq.com/community/ngi/comment/create?random={随机数}&token={参数1} [代码]参数1:token 这个在登陆的时候能够获取到 [代码] 总结 开发目的 解决手机只能看不能回复的困扰; 可以方便的在床上、马桶上和社区网红 @卢霄霄 聊天; 不是为了采集数据或者以其它方式盈利,纯粹只是闲的; 还需要优化的地方 小程序内对富文本和一些H5特殊标签支持不是很好,会导致部分内容无法正常显示; 期待官方的社区小程序能早日推出; [图片] *最后鄙视下那些采集信息到自己网站上的人。
2020-03-12 - 开发第三方自定义组件遇到的那些坑
前言 本文仅针对初学者,大神可以忽略。 由于自己也是第一次开发自定义组件,所以是参考官方文档开发第三方自定义组件的步骤进行操作的,但是有些过于基础的问题文档中并没有给出说明,所以本文把自己遇到的问题进行总结。 遇到的坑 环境问题 开发前确认node.js版本是否<=8.9.4,高于此版本下载官方提供的模板执行 npm install 命令的时候会出现错误。 npm是否安装。 下载完模板后需要在目录下先执行 npm init 对项目进行初始化,生成 package.json 文件,并修改 package.json 中 repository 节点的信息,仓库地址必须为 github.com,否则会影响后续在微信开放社区发布插件时候的校验。 npm发布 检查镜像源是否为http://registry.npmjs.org,否则请使用以下命令切换。 [代码]npm config set registry http://registry.npmjs.org [代码] 是否有npm帐号,否则进入官网或则使用 npm adduser 命令注册帐号,如果已有帐号使用 npm login 命令进行登录。 使用 npm publish 命令发布代码,如果修改了代码,然后想要同步到 npm 上的话请修改 package.json 中的 version 然后再次 publish,更新的版本上传的版本要大于上次,不能使用同一个版本号多次发布。 PS:如需删除版本,切记至少保留一个有效版本号,全部删除的话则该 packages 在24小时内无法重新发布。
2019-01-12 - 小程序前后端交互使用JWT
前言 现在很多Web项目都是前后端分离的形式,现在浏览器的功能也是越来越强大,基本上大部分主流的浏览器都有调试模式,也有很多抓包工具,可以很轻松的看到前端请求的URL和发送的数据信息。如果不增加安全验证的话,这种形式的前后端交互时候是很不安全的。 相信很多开发小程序的开发者也不一定都是大神,能够精通前后端,作为小程序的初学者不少人也是根据官方的文档去学习开发的。我自己最开始接触小程序也是从wafer2开始的,那时候腾讯云提供的SDK包含PHP和Node.js,因为对于一直做前端的人来说,Node.js的学习成本比较低,只要会JS基本能看懂,也是从那时候才开始接触Node.js,所以本文主要是基于wafer2的服务端基于Koa2的后端来说(其实这个不重要,Node.js基本都差不多)。 什么是JWT? 根据维基百科的定义,JSON WEB Token,是一种基于JSON的、用于在网络上声明某种主张的令牌(token)。JWT通常由三部分组成: 头信息(header), 消息体(payload)和签名(signature)。 为什么使用JWT? 首先,这不是一个必选方案。有时候我们的API是其它服务端和小程序公用的,那么就涉及到安全验证的问题了。 微信官方不鼓励小程序一打开就要求必须登陆的方式去获取用户信息,因此我们也不能去校验这个用户是否有权限访问这个接口,但是有的接口又不能让任何人随便去看或者被随意采集。 基于token(令牌)的用户认证 用户输入其登录信息 服务器验证信息是否正确,并返回已签名的token token储在客户端,例如存在local storage或cookie中 之后的HTTP请求都将token添加到请求头里 服务器解码JWT,并且如果令牌有效,则接受请求 一旦用户注销,令牌将在客户端被销毁,不需要与服务器进行交互一个关键是,令牌是无状态的。后端服务器不需要保存令牌或当前session的记录。 关于JWT的详细介绍网上有很多,这里也就不说了,下面介绍在Koa2框架里的添加方法。 安装依赖 [代码]npm install jsonwebtoken npm install koa-jwt [代码] app.js 引用 [代码]const jwtKoa = require('koa-jwt'); [代码] 设置不需要JWT验证的目录或者文件 [代码]const secret = '设置密钥'; app.use(jwtKoa({secret}).unless({ path: ['/','\/favicon.ico',/^\demo/] })) [代码] 数组中的路径不需要通过jwt验证。 授权 小程序 wx.request 发送网络请求的 referer header 不可设置。 其格式固定为 https://servicewechat.com/{appid}/{version}/page-frame.html,其中 {appid} 为小程序的 appid,{version} 为小程序的版本号,版本号为 0 表示为开发版、体验版以及审核版本,版本号为 devtools 表示为开发者工具,其余为正式版本。 那么我们就可以根据 ctx.header 里的 referer 进行初步的限制,比如指定的 appid 才能生成令牌。 我们在生成令牌的时候可以把简单的信息加入进去,如: [代码]const userToken = { referer: refererArray[2], appid: refererArray[3], version: refererArray[4], data: '此处可传入用户的信息' } [代码] 生成令牌: [代码]const jwt = require('jsonwebtoken'); const secret = '设置密钥'; jwt.sign(userToken, secret, {expiresIn: '2h'}); [代码] expiresIn:为令牌的有效期 这样简单的JWT令牌就生成好了,再通过接口返回给小程序端。 小程序前端如何使用JWT? 很简单,在header里加入下面属性即可。 [代码]authorization: 'Bearer 获取到的令牌' [代码] JWT优点 可扩展性好 应用程序分布式部署的情况下,session需要做多机数据共享,通常可以存在数据库或者redis里面。而JWT不需要。 无状态 JWT不在服务端存储任何状态。RESTful API的原则之一是无状态,发出请求时,总会返回带有参数的响应,不会产生附加影响。用户的认证状态引入这种附加影响,这破坏了这一原则。另外JWT的载荷中可以存储一些常用信息,用于交换信息,有效地使用JWT,可以降低服务器查询数据库的次数。 JWT缺点 安全性 由于jwt的payload是使用base64编码的,并没有加密,因此jwt中不能存储敏感数据。而session的信息是存在服务端的,相对来说更安全。 性能 JWT太长。由于是无状态使用JWT,所有的数据都被放到JWT里,如果还要进行一些数据交换,那载荷会更大,经过编码之后导致jwt非常长,cookie的限制大小一般是4k,cookie很可能放不下,所以jwt一般放在local storage里面。并且用户在系统中的每一次http请求都会把jwt携带在Header里面,http请求的Header可能比Body还要大。而sessionId只是很短的一个字符串,因此使用JWT的http请求比使用session的开销大得多。 一次性 无状态是JWT的特点,但也导致了这个问题,JWT是一次性的。想修改里面的内容,就必须签发一个新的JWT。 (1)无法废弃 通过上面JWT的验证机制可以看出来,一旦签发一个 JWT,在到期之前就会始终有效,无法中途废弃。例如你在payload中存储了一些信息,当信息需要更新时,则重新签发一个JWT,但是由于旧的JWT还没过期,拿着这个旧的JWT依旧可以登录,那登录后服务端从JWT中拿到的信息就是过时的。为了解决这个问题,我们就需要在服务端部署额外的逻辑,例如设置一个黑名单,一旦签发了新的JWT,那么旧的就加入黑名单(比如存到redis里面),避免被再次使用。 (2)续签 如果你使用jwt做会话管理,传统的cookie续签方案一般都是框架自带的,session有效期30分钟,30分钟内如果有访问,有效期被刷新至30分钟。一样的道理,要改变JWT的有效时间,就要签发新的JWT。最简单的一种方式是每次请求刷新JWT,即每个http请求都返回一个新的JWT。这个方法不仅暴力不优雅,而且每次请求都要做JWT的加密解密,会带来性能问题。另一种方法是在redis中单独为每个JWT设置过期时间,每次访问时刷新JWT的过期时间。
2019-02-20 - 小程序架构设计(一)
在微信早期,我们内部就有这样的诉求,在微信打开的H5可以调用到微信原生一些能力,例如公众号文章里可以打开公众号的Profile页。所以早期微信提供了Webview到原生的通信机制,在Webview里注入JSBridge的接口,使得H5可以通过它调用到原生能力。 [图片] 我们可以通过JSBridge微信预览图片的功能: [代码]WeixinJSBridge.invoke('imagePreview', { current: https://img1.gtimg.com/1.jpg', urls: [ 'https://img1.gtimg.com/1.jpg', 'https://img1.gtimg.com/2.jpg', 'https://img1.gtimg.com/3.jpg' ] }) [代码] 早期微信官方是没有暴露这些接口的,都是腾讯内部业务在使用,很多外部开发者发现后,就依葫芦画瓢地使用了。 从另外一个角度看,JSBridge是微信和H5的通信协议,有一些能力可能要组合不同的能力才能完整调用。如果我们直接开放这套API,相当于所有开发者都要直接理解这样的接口协议,显然是很不合理的。 所以在2015年初的时候,微信就发布了JSSDK,其实就是隐藏了内部一些细节,包装了几十个API给到上层业务直接调用。 [图片] 前边的代码就变成了: [代码]wx.previewImage({ current: https://img1.gtimg.com/1.jpg', urls: [ 'https://img1.gtimg.com/1.jpg', 'https://img1.gtimg.com/2.jpg', 'https://img1.gtimg.com/3.jpg' ] }) [代码] 开发者可以用JSSDK来调用微信的能力,来完成一些以前H5做不到或者难以做到的事情。 能力上得到了更多的支持,但是微信里的H5体验却没有改善。 第一点是加载H5时的白屏。在微信里打开链接后会看到白屏,有一些H5的服务不稳定,这个白屏现象会更严重。 [图片] 第二点是在H5跳转到其他页面时,切换的效果也很不流畅,只能看到顶部绿色进度条在走。 [图片] 随着JSSDK的开放,还出现了更不好对付的问题。 微信上越来越多干坏事的人,有人做假红包,有人诱导分享,有伪造一些官方活动。他们会利用JSSDK的分享能力变相的去裂变分享到各个群或者朋友圈,由于JSSDK是根据域名来赋予api权限的,运营人员封了一个域名后,他们立马用别的域名又继续做坏,要知道注册一个新的域名的成本是很低的。 [图片] [图片] [图片] 龙哥在2016年微信公开课上提出了应用号的概念,我们要重新设计一个新的移动应用开发模式,同时我们要解决刚刚提到的一些问题。 至此,我们回顾一下目前移动应用开发的一些特点: Web开发的门槛比较低,而App开发门槛偏高而且需要考虑iOS和安卓多个平台; 刚刚说到H5会有白屏和页面切换不流畅的问题,原生App的体验就很好了; H5最大的优点是随时可以上线更新,但是App的更新就比较慢,需要审核上架,还需要用户主动安装更新。 我们更想要的一种开发模式应该是要满足一下几点: 像H5一样开发门槛低; 体验一定要好,要尽可能的接近原生App体验; 让开发者可以云端更新,而且我们平台要可以管控。 很多人可能会第一时间想到Facebook的React Native(下边简称RN),是不是RN就能解决这些问题呢? 是的,React Native貌似可以解决刚刚那些问题,我们也曾经想用RN来做。但是仔细分析了一下,我们发现了采用RN这个机制做开放平台还是存在一些问题。 RN只支持CSS的子集,作为一个开放的生态,我们还要告诉外边千千万万的开发者,哪些CSS属性能用,哪些不能用; RN本身存在一些问题,这些依赖RN的修复,同时这样就变成太过依赖客户端发版本去解决开发者那边的Bug,这样修复周期太长。 RN前阵子还搞出了一个Lisence问题,对我们来说也是存在隐患的。 [图片] 所以我们舍弃了这样的方案,我们改用了Hybrid的方式。简单点说,就是把H5所有代码打包,一次性Load到本地再打开。这样的好处是我们可以用一种近似Web的方式来开发,同时在体验上也可以做到不错的效果,并且也是可以做到云端更新的。 [图片] 现在留给我们的最后一个问题就是,平台的管控问题。 怎么理解呢?我们知道H5的界面结构是用HTML进行描述,浏览器进行一系列的解析最终绘制在界面上。 [图片] 同时浏览器提供了可以操作界面的DOM API,开发者可以用这些API进行一些界面上的变动,从而实现UI交互。 [图片] 既然我们要采用Web+离线包的方式,那我们要解决前边说过的安全问题,我们就要禁用掉很多危险的HTML标签,还要禁用掉一些API,我们要一直维护这样的白名单或者黑名单,实现成本太高了,而且未来浏览器内核一旦更新,对我们来说都是很大的安全隐患。 [图片] 这就是小程序一开始遇到的问题,在下篇文章《小程序架构设计(二)》,我们再详细展开一下小程序是如何解决以上这个问题的。
2019-02-26 - 出行e助手
提供天气、北京实时公交、全国地铁站、火车票余票查询、车辆限行、全国油价、地图导航、路线规划、电影资讯、台风运行轨迹等信息。
2018-07-20