- 我们和纺织公司帮助微信出海占领市场,但是现在微信反而给我们拖后腿,想和大家探讨一下?
我们的小程序是做SAAS服务的,为中国广大纺织厂家服务。 国内的纺织业企业至少有几十万家公司。 这些纺织厂家有做内贸,也有做外贸的,他们的出口业务范围遍布全球。 我们就是为这些厂家提供软件服务,帮忙注册他们自己的小程序抬头,再帮他们部署小程序。 使用他们的小程序用户,可以在我们的小程序集中查看当前用户看过的厂家和看过的产品,以及哪些产品下过单和下单信息(去除价格)。 如果是纺织外贸企业,我们的软件还会帮助他们把产品内容翻译为客户手机微信的语言。 比如客户的微信用的是法语,那么我们就帮助翻译成法文。 我们和纺织业出口企业相当于帮助腾讯把微信推广到全球海外纺织以及服装行业从业人员,这个人数是巨大的。 这些用户还可以在线下展会扫纺织厂家的二维码,浏览纺织厂家的产品。 这个举动也可能会顺便带动展会其他国家的纺织厂家也注册微信小程序。 但是现在微信不允许用户使用手机号码登陆我们的小程序,即便我们的用户协议中已经说清楚,手机号码是当用户在纺织厂下单后,厂家需通过手机号码联系用户。 了解下来纺织行业的厂家和用户都觉得不通过手机号联系不方便。 因为从事劳动密集型行业的他们不习惯使用邮箱和其他账号,而且纺织业货值金额巨大,厂家仅仅凭借无法查找具体人员,核实对方企业信息的账号密码,因此不敢备货。 这个问题该怎么解决? 也希望腾讯官方了解一下我们这个情况,毕竟老外用的人数越多,微信的海外市场占有率也会上来 中国的纺织业吸纳了众多初中文化的工人,很多人学历水平不高,老外可能使用邮箱,但是国内的纺织业从业人员并不会邮箱或者账号密码之类的,他们最多能报个手机号码已经是很好了 ----------- 大家可能会有疑问,为什么要在我们自己的小程序提供集中式的查看。 因为纺织行业企业非常多,不管是生产企业还是外贸企业。 有的用户作为采购面料的服装企业,在行业展会,往往会面对上万家纺织企业,对,是上万家。 让用户去注册上万家企业的小程序,用上万个账号密码登陆,非常不现实的。 而且当他扫过二维码,注册了,下单了,只凭借一个abc123账号要准备几万元甚至几十万元的货物,哪个老板敢冒这个风险。 纺织行业有的面料很贵,一定要联系到用户,确认他所在的公司信息,否则厂家不会搭理的。 目前这样就无法帮助内贸纺织厂家销售,也无法帮助外贸企业出口。 国内纺织业帮助微信出海也无从谈起。 我们,纺织行业,腾讯,是个三输的局面。 我们现在就很困惑,该怎么解决这个问题。
2022-08-19 - 这些 Canvas 小技巧,保证你新年用得上
来自「微信开发者」公众号,作者为微信小程序技术研发工程师binnie。 本文主要介绍了3个隐藏的 Canvas 小技巧: - 绘制并生成图片 - Video 绘制 Canvas / webgl - 视频解码并绘制到 webgl - 录制并导出 webgl 视频 一键加滤镜 快速合成音视频 轻松挑选视频封面 …… Canvas 能够做这些? 作为资深的开发者,相信大家对 Canvas 都不陌生。这项能力在绘制图形方面发挥着极大的作用,高效支持图片编辑、数据可视化等应用场景。但是只局限于一般能力应用,那格局就小了。 Canvas 的应用场景非常丰富!赶紧往下看看这些隐藏的 Canvas 小技巧,保证你新年用得上!还有手把手教程以及文末彩蛋哟。 -- • 绘制并生成图片 • -- [图片] 示例:新年模板长按保存祝福 适用场景:图片分享海报 相关 API:RenderingContext/Canvas/wx.canvasToTempFilePath Step 1: 创建实例获取对象 创建 Canvas 实例,获取 CanvasRenderingContext2D 对象(Canvas 绘图上下文)来绘制形状、文本、图像等。 const query = wx.createSelectorQuery() let canvas = null query.select('#myCanvas') .fields({ node: true, size: true }) .exec((res) => { // 通过 wx.createSelectorQuery 获取到 canvas 实例 canvas = res[0].node // 通过 canvas.getContext('2d') 获取 CanvasRenderingContext2D 对象 const ctx = canvas.getContext('2d') }) Step 2: 设置宽高调整图片 获取 Canvas 绘图上下文后,将 Canvas 的宽高设置为节点宽高 * 设备像素比,绘制出来的图片更清晰 // 获取设备像素比 const dpr = wx.getSystemInfoSync().pixelRatio // 将 canvas 宽高设置为 canvas.width = res[0].width * dpr canvas.height = res[0].height * dpr Step 3: 绘制内容 使用 CanvasRenderingContext2D 绘制,根据业务需要在画布中绘制头像、文字、背景等 // 矩形 ctx.fillStyle = '#FFFFFF' ctx.fillRect(0, 0, canvas.width , canvas.height ) // 图片 var image = canvas.createImage() himage.src = 'https://example.com/example.jpg' headImage.onload = (res) => { ctx.drawImage(himage 0, 0, 32, 32; } // 文本 ctx.font = "18px SimHei"; ctx.textAlgin = "left" ctx.fillStyle = "#07c160"; ctx.fillText("这是我的名字", 0, 0); Step 4: 生成并保存本地 使用 wx.canvasToTempFilePath 将画布生成图片,wx.saveImageToPhotosAlbum 将图片保存到本地。 wx.canvasToTempFilePath({ canvas: canvas, // canvas 实例 success(res) { // canvas 生成图片成功 wx.saveImageToPhotosAlbum({ filePath: res.tempFilePath, success(res) { // 保存成功 } }) } }) -- • Video 绘制 Canvas / webgl • -- [图片] 示例:视频文件绘制 Canvas 适用场景:制作 Video 滤镜、挑选 Video 封面等 相关 API:RenderingContext/Canvas Step 1: 获取实例 通过 wx.createSelectorQuery 获取 VideoContext 实例 let video = null wx.createSelectorQuery().select('#video').context(res => { // 通过 wx.createSelectorQuery 获取 VideoContext 实例 video = res.context; }) Step 2: 绘制内容 获取 VideoContext 实例后,将 VideoContext 传递给 Canvas 进行绘制。开发者根据业务需求选择绘制类型: Canvas 2d 写法:canvas.drawImage(video, ...)webgl 写法:gl.texImage2D(..., video) wx.createSelectorQuery().selectAll('#myCanvas,#webglCanvas').node(res => { const ctx = res[0].node.getContext('2d') const gl = res[1].node.getContext('webgl') setInterval(() => { // canvas 2d // 将 video 纹理对象传入 drawImage 进行绘制 ctx1.drawImage(video, 0, 0, w * dpr, h * dpr); // 添加一个蒙层 ctx1.fillStyle = 'rgba(0, 0, 0, 0.3)' ctx1.fillRect(0, 0, w * dpr, h * dpr); // webgl const render = createRenderer(res[1].node, w, h) render(new Uint8Array(ctx1.getImageData(0, 0, w * dpr, h * dpr).data), w * dpr, h * dpr) }, 1000 / 24) }).exec() function createRenderer(canvas, width, height) { const gl = canvas.getContext("webgl") ... return (arrayBuffer, width, height) => { ... // 指定二维纹理图像 gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, width, height, 0, gl.RGBA, gl.UNSIGNED_BYTE, arrayBuffer) gl.drawElements(gl.TRIANGLES, 6, gl.UNSIGNED_SHORT, 0) } } -- • 视频解码并绘制到 webgl • -- [图片] 示例:视频一键解码并绘制到 webgl 适用场景:添加特效、贴图等视频编辑场景 相关 API:wx.createVideoDecoder/VideoDecoder/RenderingContext/Canvas.requestAnimationFrame/wx.createMediaAudioPlayer/MediaAudioPlayer Step 1: 创建视频解码器进行解码 1. 调用 createVideoDecoder 对视频进行解码 2. 使用 videodecoder.start 启动解码,视频源文件不限制本地或远程路径 3. 通过 videodecoder.on('start', res => {}) 监听解码,通过 videodecoder.getFrameData() 获取到解码数据 // 获取视频解码器 getVideoDecoder(source, abortAudio) { return new Promise((resolve, reject) => { // 创建视频解码器 videodecoder = wx.createVideoDecoder() // 开始解码 videodecoder.start({ abortAudio: abortAudio, source: source, // 视频源文件,支持本地路径&远程路径 mode: 0 // 按pts解码,保证音画同步 }) // 监听解码 开始 videodecoder.on('start', res => { console.log('videodecoder start', res) // 状态初始化 isStop = false resolve(videodecoder) }) // 监听解码 结束 videodecoder.on('ended', res => { // 状态设置为结束,停止画面录制器 isStop = true }) }) }, Step 2: 解码数据绘制到 webgl 1. 通过 gl.texImage2D(..., image) 将解码数据绘制到 webgl 2. 使用 webgl.requestAnimationFrame 继续绘制,效果更加流畅 // 将解码数据绘制到 webgl 中 const query = wx.createSelectorQuery() query.select('#webglCanvas').node().exec((res) => { const webgl = res[0].node const requestAnimationFrame = webgl.requestAnimationFrame; // 初始化webgl let render = null if (!render) { render = createRenderer(webgl, 600, 400) } /** * 绘制视频帧到 canvas */ let i = 1 let loop = () => { // 解码结束,停止循环 if (isStop) { return } // 获取解码数据,绘制到 webgl 中 const imageData = videodecoder.getFrameData() if (imageData) { // render 的高宽需要设置为图片的宽高才可以绘制出来 render(new Uint8Array(imageData.data), imageData.width, imageData.height) } // 继续绘制 console.log('绘制帧数:', i++) requestAnimationFrame(loop) } // 启动录制循环 requestAnimationFrame(loop) }) Step 3: 添加音频播放器同步播放音频 完成 Step2 后,webgl 只有视频播放,缺少音频。因此使用 wx.createMediaAudioPlayer(),支持 addAudioSource 传入 videodecoder,保证视频帧渲染音画同步 /** * 创建媒体音频播放器 */ let mediaAudioPlayer = null let addAudio = () => { if (mediaAudioPlayer) return mediaAudioPlayer = wx.createMediaAudioPlayer() mediaAudioPlayer.start().then(() => { // 添加播放器音频来源 mediaAudioPlayer.addAudioSource(videodecoder).then(res => { console.log('add mediaAudioPlayer: ',) }) }) } // render 绘制视频同时添加音频 render(new Uint8Array(imageData.data), imageData.width, imageData.height) addAudio() -- • 录制并导出 webgl 视频 • -- [图片] 示例:录制并一键导出 webgl 视频 适用场景:将动画、编辑过的视频导出视频文件保存 相关 API:wx.createMediaRecorder/MediaRecorder/wx.createMediaContainer/MediaContainer/MediaTrack Step 1: 创建 webgl 画面录制器进行录制 通过 createMediaRecorder 创建页面录制器,并且绑定 webgl(建议离屏状态,效果更好)进行录制 /** * 获取画面录制器 */ getRecorder() { let canvas = this.getMainCanvasNode() let recorder = wx.createMediaRecorder(canvas, { fps: choosedVideoInfo.fps, // 实际视频的 fps videoBitsPerSecond: choosedVideoInfo.bitrate, // 实际视频的 bitrate gop: 12 }) // 监听录制事件 recorder.on("timeupdate", (res) => { console.log('recorder 录制中,当前时间:', res.currentTime) }) recorder.on("stop", (res) => { console.log('recorder停止') this.saveMedia(res.tempFilePath) }) // 开始录制 recorder.start() this.recorder = recorder return recorder }, // 初始化 画面录制器 并进行录制 await this.initRenderer() this.getDecoder().then((decoder) => { let recorder = this.getRecorder() var self = this function loop() { if (self.stopped) { return } let frameData = decoder.getFrameData() if (!frameData) { console.log('没取到帧') setTimeout(() => { loop() }, 1000/60) } else { self.renderFrame(frameData) recorder.requestFrame(() => { console.log('录制帧数:', i++) loop() }) } } loop() }) Step 2: 添加音频合成音视频 1. 通过 createMediaContainer 创建音视频处理容器来合成音视频 2. 通过 MediaContainer.extractDataSource 将视频源分离出视频轨道和音频轨道,将需要的轨道通过 MediaContainer.addTrack 添加到容器中 3. 通过 MediaContainer.export 导出即可获得合成后的视频文件 /** * 将视频和音频合到一起并保存到本地 * @param {*} videoTempFilePath */ saveMedia(videoTempFilePath) { const self = this let choosedFile = this.choosedFile const MediaContainer = wx.createMediaContainer() // webgl的取视频 MediaContainer.extractDataSource({ source: videoTempFilePath, success(res) { MediaContainer.addTrack(res.tracks[0]) // 源视频取音频 MediaContainer.extractDataSource({ source: choosedFile, success(res) { // 拿到音频轨道并加入到容器 res.tracks[0].kind == 'audio' && MediaContainer.addTrack(res.tracks[0]) res.tracks[1].kind == 'audio' && MediaContainer.addTrack(res.tracks[1]) // 合成视频并导出视频文件 MediaContainer.export({ success(res) { // 保存视频到本地 wx.saveVideoToPhotosAlbum({ filePath: res.tempFilePath, success() { wx.showToast({ title: '导出成功', icon: 'success', duration: 2000 }) self.destroy() } }) } }) } }) } }) }, -- •高效图像处理彩蛋 • -- 学会以上这些 Canvas 小技巧,还担心新年的美图美照美视频处理不过来?赶紧码下这个 Canvas 代码包,保证你就是家里最闪耀的靓女靓仔。 预祝大家新的一年 Canvas 在手,红包一直有!
2022-03-24 - 小程序调用云存储图片:[渲染层网络层错误] Failed to load image 请问怎么处理?
[图片] [图片]
2021-05-05 - 小程序调用云存储图片:[渲染层网络层错误] Failed to load image 请问怎么处理?
[图片] 在云开发控制台也不能预览图 [图片] 之前一直没有问题,今天出现的,请问是什么原因,怎么解决呢?
2020-08-26 - sub mch id与sub_appid不匹配?
[图片]sub mch id与sub_appid不匹配问题 这边三方技术说没问题 给出的结果是腾讯最近不允许第三方支付平台接口 想核实一下到底什么情况 还是有最新出的什么规定导致的
2023-07-15 - 调用新的wx.chooseMedia选择了Heic格式的图片,无语预览,也无法获取图片信息?
wx.chooseMedia({}).then(res => { console.log(res) this.setData({ imgUrl: res.tempFiles[0].tempFilePath }) wx.getImageInfo({ src: res.tempFiles[0].tempFilePath, }).then(res => { console.log(res) }).catch(err => { console.log(err) }) }) 选择了heic格式的图片,图片显示不出来,使用getImageInfo也无法获取图片信息.报错为: {errMsg: "getImageInfo:fail invalid"} 这个情况应该怎么处理? 用户上传了两张heic格式的图片无法显示.
2023-12-26 - 关于wx.chooseMedia,隐私条款已经授权,本地和真机调试没问题,发布到体验版本有问题?
[图片] 代码亲测,已经授权走的 下边这个方法,然后调用直接报错{errmsg:'chooseMeida':'ok','tem...':},此代码在真机模拟。本地运行完全OK 但就是上传到体验版本不行,求大神们讲解!! {"errMsg": "chooseMedia:fail api scope is not declared in the privacy agreement", "errno": 112}
02-29 - skyline之下,wxs还能响应事件吗?
在skyline渲染的时候,wxs还能响应事件吗??就是这个链接里面的内容https://developers.weixin.qq.com/miniprogram/dev/framework/view/interactive-animation.html 在skyline下,若不能,js里面的方法还能触发wxs里面的方法,并在wxs中更新页面元素的style。有没有平替方案
2023-11-15 - "renderer" 为 "skyline",引用weui库报错,怎么可以全局配置?目前一个个加
[ miniprogram_npm/weui-miniprogram/form-page/form-page.json 文件内容错误] miniprogram_npm/weui-miniprogram/form-page/form-page.json: 根据页面或 app.json 的配置,miniprogram_npm/weui-miniprogram/form-page/form-page.json 页面 "renderer" 为 "skyline",需在页面配置中添加 "disableScroll": true(env: Windows,mp,1.05.2204264; lib: 3.3.4)
03-05 - Webview、Skyline 混用切换耗时吗?
在学习 Skyline 的过程中,许多开发者会有一个疑问:是否可以将小程序的部分页面迁移到 Skyline? 对 Skyline 感兴趣但还没有完全决定是否要使用的开发者来说,可能只想先尝试一下 Skyline 的功能。 实际上,Skyline 支持最小粒度的页面配置,意味着我们可以为某个页面单独开启 Skyline,而不必将整个小程序迁移到 Skyline 上。 开发者可以更加灵活地使用 Skyline,并逐步将小程序迁移到 Skyline 上,从而获得更好的性能和用户体验。 我们知道 Webview 和 Skyline 是两个渲染引擎,对于 Webview 和 Skyline 混用,大家又有新的疑问:当进行页面切换的时,混用是否会增加耗时? 这里需要分三种情况: 1、Skyline -> Webview:这种情况取决于 app.json 里配置的全局 renderer,即小程序设置的默认渲染引擎 如果全局 renderer 是 Skyline,那么 Webview 不会被预加载,此时 Skyline 跳转 Webview 耗时会增加,开发者需要手动调用 wx.preloadWebview 做预加载。如果全局 renderer 是 Webview,由于 Webview 默认会预加载,所以 Skyline -> Webview 和 Webview -> Webview 耗时一样,不会增加耗时。2、Webview -> Skyline:Skyline 默认都不会被预加载,开发者需要手动调用 wx.preloadSkylineView 做预加载。 3、Skyline -> Skyline:速度变快,因为多个页面复用同一个 Skyline 实例。 根据上述三种情况的分析,为了保证混用渲染引擎的页面切换耗时最短,我们需要在以下时机进行预加载。 wx.preloadWebview 当 Skyline 页面跳转到 Webview 页面时并且全局 renderer 是 Skyline 由于 Skyline 不影响渲染线程,所以预加载 Webview 的时机只需要在主要逻辑完成后即可 // Skyline page.js Page({ onShow() { // 等待执行完主要逻辑后进行预加载 wx.preloadWebview() } }) wx.preloadSkylineView 当 Webview 页面跳转 Skyline 页面时,因为 Skyline 默认不预加载,所以我们需要手动预加载。 建议大家在 Skyline 页面的 onShow 生命周期里延迟一段时间后调用,这样可以保证在 Skyline 页面被返回时也能够重新预加载。 注意:预加载会影响当前页面的渲染,建议异步延迟去执行预加载操作 // Webview page.js Page({ onShow() { // 延迟 200ms 预加载 Skyline // 建议这个延迟时机在页面渲染完成之后 setTimeout(() => { wx.preloadSkylineView() }, 200) } }) 做好预加载是提高 Webview 和 Skyline 混用体验的有效方式,需要根据实际情况进行调整和优化,以达到最佳的预加载效果。
03-07 - masonry状态下瀑布流里面使用自定义组件失效?
[图片][图片]
2023-10-12 - 请问有大神知道小程序报错skylineWindow 29 is not exist是怎么回事吗?
开发者工具版本:RC 1.06.2402021 报错信息及调试库版本信息如下,期待被大神看到,非常感谢。谢谢。 [图片][图片]
04-14 - 如何实现快速生成朋友圈海报分享图
由于我们无法将小程序直接分享到朋友圈,但分享到朋友圈的需求又很多,业界目前的做法是利用小程序的 Canvas 功能生成一张带有小程序码的图片,然后引导用户下载图片到本地后再分享到朋友圈。相信大家在绘制分享图中应该踩到 Canvas 的各种(坑)彩dan了吧~ 这里首先推荐一个开源的组件:painter(通过该组件目前我们已经成功在支付宝小程序上也应用上了分享图功能) 咱们不多说,直接上手就是干。 [图片] 首先我们新增一个自定义组件,在该组件的json中引入painter [代码]{ "component": true, "usingComponents": { "painter": "/painter/painter" } } [代码] 然后组件的WXML (代码片段在最后) [代码]// 将该组件定位在屏幕之外,用户查看不到。 <painter style="position: absolute; top: -9999rpx;" palette="{{imgDraw}}" bind:imgOK="onImgOK" /> [代码] 重点来了 JS (代码片段在最后) [代码]Component({ properties: { // 是否开始绘图 isCanDraw: { type: Boolean, value: false, observer(newVal) { newVal && this.handleStartDrawImg() } }, // 用户头像昵称信息 userInfo: { type: Object, value: { avatarUrl: '', nickName: '' } } }, data: { imgDraw: {}, // 绘制图片的大对象 sharePath: '' // 生成的分享图 }, methods: { handleStartDrawImg() { wx.showLoading({ title: '生成中' }) this.setData({ imgDraw: { width: '750rpx', height: '1334rpx', background: 'https://qiniu-image.qtshe.com/20190506share-bg.png', views: [ { type: 'image', url: 'https://qiniu-image.qtshe.com/1560248372315_467.jpg', css: { top: '32rpx', left: '30rpx', right: '32rpx', width: '688rpx', height: '420rpx', borderRadius: '16rpx' }, }, { type: 'image', url: this.data.userInfo.avatarUrl || 'https://qiniu-image.qtshe.com/default-avatar20170707.png', css: { top: '404rpx', left: '328rpx', width: '96rpx', height: '96rpx', borderWidth: '6rpx', borderColor: '#FFF', borderRadius: '96rpx' } }, { type: 'text', text: this.data.userInfo.nickName || '青团子', css: { top: '532rpx', fontSize: '28rpx', left: '375rpx', align: 'center', color: '#3c3c3c' } }, { type: 'text', text: `邀请您参与助力活动`, css: { top: '576rpx', left: '375rpx', align: 'center', fontSize: '28rpx', color: '#3c3c3c' } }, { type: 'text', text: `宇宙最萌蓝牙耳机测评员`, css: { top: '644rpx', left: '375rpx', maxLines: 1, align: 'center', fontWeight: 'bold', fontSize: '44rpx', color: '#3c3c3c' } }, { type: 'image', url: 'https://qiniu-image.qtshe.com/20190605index.jpg', css: { top: '834rpx', left: '470rpx', width: '200rpx', height: '200rpx' } } ] } }) }, onImgErr(e) { wx.hideLoading() wx.showToast({ title: '生成分享图失败,请刷新页面重试' }) //通知外部绘制完成,重置isCanDraw为false this.triggerEvent('initData') }, onImgOK(e) { wx.hideLoading() // 展示分享图 wx.showShareImageMenu({ path: e.detail.path, fail: err => { console.log(err) } }) //通知外部绘制完成,重置isCanDraw为false this.triggerEvent('initData') } } }) [代码] 那么我们该如何引用呢? 首先json里引用我们封装好的组件share-box [代码]{ "usingComponents": { "share-box": "/components/shareBox/index" } } [代码] 以下示例为获取用户头像昵称后再生成图。 [代码]<button class="intro" bindtap="getUserInfo">点我生成分享图</button> <share-box isCanDraw="{{isCanDraw}}" userInfo="{{userInfo}}" bind:initData="handleClose" /> [代码] 调用的地方: [代码]const app = getApp() Page({ data: { isCanDraw: false }, // 组件内部关掉或者绘制完成需重置状态 handleClose() { this.setData({ isCanDraw: !this.data.isCanDraw }) }, getUserInfo(e) { wx.getUserProfile({ desc: "获取您的头像昵称信息", success: res => { const { userInfo = {} } = res this.setData({ userInfo, isCanDraw: true // 开始绘制海报图 }) }, fail: err => { console.log(err) } }) } }) [代码] 最后绘制分享图的自定义组件就完成啦~效果图如下: [图片] tips: 文字居中实现可以看下代码片段 文字换行实现(maxLines)只需要设置宽度,maxLines如果设置为1,那么超出一行将会展示为省略号 代码片段:https://developers.weixin.qq.com/s/J38pKsmK7Qw5 附上painter可视化编辑代码工具:点我直达,因为涉及网络图片,代码片段设置不了downloadFile合法域名,建议真机开启调试模式,开发者工具 详情里开启不校验合法域名进行代码片段的运行查看。 最后看下面大家评论问的较多的问题:downLoadFile合法域名在小程序后台 开发>开发设置里配置,域名为你图片的域名前缀 比如我文章里的图https://qiniu-image.qtshe.com/20190605index.jpg。配置域名时填写https://qiniu-image.qtshe.com即可。如果你图片cdn地址为https://aaa.com/xxx.png, 那你就配置https://aaa.com即可。
2022-01-20 - 从page-container跳转下一页面,再返回,page-container页返回按钮无效?
page-container组件、开发工具最新版、基础库2.22; 问题描述:从首页跳转详情页,在详情页打开page-container并跳转编辑页(编辑页在分包中),再从编辑页返回详情页后,详情页的返回按钮失效,无法返回首页。只有再次打开page-container并关闭,返回按钮才有效。 代码片段: https://developers.weixin.qq.com/s/eRkZy2mh7Dxc
2022-02-13 - 微信小程序页面之间正向传值和逆向传值的方法
微信小程序页面之间正向传值和逆向传值 正向传值 一 直接使用URL传值 [代码] wx.navigateTo({ url: `/pages/contacts-edit/contacts-edit?name=zhangsan&idx=1`, }) [代码] 但是如果一个对象结构比较复杂, 数据量比较大, 即使转换成JSON也有可能会被莫名其妙的截取. 所以使用URL传值的时候, 需要先编码 我是这样做的 [代码]// A页面触发事件, 跳转到B页面 _onClickCell: function (e) { let contacts = { name: '张三', phone: '13800001111', safePhone: '138****1111', idCard: '230524202113324455', safeIdCard: '230524********4455', typeStr: '成人', gender: '0', genderStr: '保密' } // 先对数据进行JSON let jsonStr = JSON.stringify(contacts) // 对数据进行URI编码, 如果不进行这一步操作, 数据有可能会被截断, 少量数据没有问题, 如果是一个大的对象, 就容易被截断获取不到完整的数据 let data = encodeURIComponent(jsonStr) wx.navigateTo({ url: `/pages/contacts-edit/contacts-edit?contacts=${data}&idx=${idx}`, }) }, // B页面再onLoad方法中接收参数 onLoad: function (options) { let idx = (!!options.idx) ? Number(options.idx) : -1 let contacts = {} if (!!options.contacts) { let jsonStr = decodeURIComponent(options.contacts) contacts = JSON.parse(jsonStr) } this.setData({ contacts, idx }) }, [代码] 二 使用eventChannel来传递 [代码]//A页面准备跳转到B页面 _onClickCell: function (e) { let address = { id: 457, name: '小艾-3', countryCode: '86', phone: '13892292222', reginoCode: '871', city: '市辖区', area: '海淀区', street: '东北旺路8号院中关村软件园8号楼华夏科技大厦', address: '中国北京市市辖区海淀区东北旺路8号院中关村软件园8号楼华夏科技大厦' }, wx.navigateTo({ url: '/pages/address-edit/address-edit', success: res => { // 这里给要打开的页面传递数据. 第一个参数:方法key, 第二个参数:需要传递的数据 res.eventChannel.emit('setAddressEditData', address) } }) } //B页面在onLoad方法中接收参数 onLoad: function (options) { // 接收上个页面传递来的数据 let eventChannel = this.getOpenerEventChannel() // setAddressEditData和上个页面设置的相同即可 eventChannel.on('setAddressEditData', (address) => { this.setData({ address: address || {}, }) }) }, [代码] 逆向传值 一 使用全局对象, 获取全部页面来逆向传值 [代码] _onClickComplete: function () { // 获取当前全部的页面栈 let arr = getCurrentPages() // 获取到要逆向传值的上一个页面 let lastPage = (arr.length >= 2) ? arr[arr.length - 2] : undefined // 判断拿到的上一个页面是不是我们要的页面 if (!!lastPage && lastPage.route == 'pages/contacts-list/contacts-list') { /* 这里我们就拿到了上一个页面的页面对象, 这里其实我们就可以使用lastPage做很多事情了, 例如直接操作lastPage.data, 修改上一个页面的数据 或者调用这个页面内的方法, 我上一个页面预留了一个更新方法, 所以这里就直接用上一个页面调用数据刷新的方法, 我这里给赋值, 就可以携带数据回上一个页面了 */ lastPage.updateContactList(this.data.contacts, this.data.idx) // 返回上一个页面 wx.navigateBack() } }, [代码] 二 使用eventChannel来逆向传值 B->A [代码]// B页面 _onClickComplete: function (e) { let eventChannel = this.getOpenerEventChannel() // updateAddressListData 这个方法需要上一个页面的支持, 上一个页面在navigateTo方法中的events数据中定义这个方法来接收数据 eventChannel.emit('updateAddressListData', this.data.address, this.data.idx) wx.navigateBack() }, // A页面需要的支持 _onClickCell: function (e) { wx.navigateTo({ url: '/pages/address-edit/address-edit', events: { // 这里用来接收后面页面传递回来的数据 updateAddressListData: (address, index) => { // 这里处理数据即可 } } }) } [代码] 代码片段
2020-07-21 - 小程序组件的函数属性及事件触发
开发小程序组件库 TDesign 有感 微信小程序,从基础库 [代码]2.0.9[代码] 开始,自定义组件的 [代码]type: Object[代码] 属性(properties)支持函数类型的值了,但仍不支持函数类型的属性,即: [代码]// dialog.js Component({ properties: { confirmBtn: { type: Object, // ok }, cancelBtn: { type: Function // wrong } }, observer: { confirmBtn(obj) { console.log(obj.bindgetuserinfo) // function } } }) [代码] 这种能力,在实现 Dialog 组件的时候,非常有用。这样在 Dialog 组件的 [代码]cancel[代码] 和 [代码]confirm[代码] 按钮可以方便地支持 Button 的各种开放能力。 于是,就会想当然地这样实现: [代码]<view class="t-dialog"> <!-- ... --> <button class="cancel-btn" size="{{cancelBtn.size}}" type="{{cancelBtn.type}}" plain="{{cancelBtn.plain}}" disabled="{{cancelBtn.disabled}}" open-type="{{cancelBtn.openType}}" bindgetuserinfo="{{cancelBtn.bindgetuserinfo}}" > 取消 </button> <button class="confirm-btn" size="{{confirmBtn.size}}" type="{{confirmBtn.type}}" plain="{{confirmBtn.plain}}" disabled="{{confirmBtn.disabled}}" open-type="{{confirmBtn.openType}}" bindgetuserinfo="{{confirmBtn.bindgetuserinfo}}" > 确认 </button> </view> [代码] 这样就会出现几个问题: 属性透传写法太冗余 事件不会触发 按钮内容没法传入 属性透传 Dialog 组件存在两个按钮,所以两个按钮都需要透传 button 属性,直观的想法就是采用 template 来处理: [代码]<!-- button.wxml --> <template name="button"> <button class="{{class}}" size="{{size}}" type="{{type}}" plain="{{plain}}" disabled="{{disabled}}" open-type="{{openType}}" bindgetuserinfo="{{bindgetuserinfo}}" > 确认 </button> </template> [代码] 于是 Dialog 的代码就可以省略成这样: [代码]<import src="./button.wxml" /> <view class="t-dialog"> <!-- ... --> <template is="button" data={{...cancelBtn, class: 'cancel-btn'}}> <template is="button" data={{...confirmBtn, class: 'confirm-btn'}}> </view> [代码] 这里确实挺奇怪的,可以直接传入了一个解构后的值。 这里可以直接合并对象 事件不会触发 一开始以为是 template 的值传递过程,不支持 function 类型的值,因此丢失了。 比如在 template 里面使用 wxs 打印类型,居然是空的。 后来经过各种测试,最后在官网文档找到答案:小程序框架/事件系统 在小程序的事件绑定,只需要传入的是字符串: [代码]<view bindtap="handletap">Tap me!</view> [代码] 也可以是一个数据绑定: [代码]<view bindtap="{{ handlerName }}">Tap me!</view> [代码] 但,这个数据的返回值类型应该是 string 而不是 function。 通过这点,恍然大悟,想起了小程序的双线程模型: [图片] 为了减轻线程之间的传输负担,是不需要将 function 传到渲染层的,只需要给一个函数名,然后在逻辑层执行对应的函数即可。 因此没有办法在 wxml 里面执行对象属性的函数,需要找一个代理函数(Proxy function)处理。 为了区分对应的按钮,因此 template 做了小改动,增加了一个 [代码]data-token[代码] 的属性: [代码]<template name="button"> <button data-token="{{token}}" bindtap="onTplButtonTap"> </template> [代码] 对应的 Dialog 的 wxml 的改动是这样的: [代码]<import src="button.wxml" /> <view class="t-dialog"> <!-- ... --> <template is="button" data={{...cancelBtn, token: 'cancel', class: 'cancel-btn'}}> <template is="button" data={{...confirmBtn, token: 'confirm', class: 'confirm-btn'}}> </view> [代码] 对应的 JS 是这样的: [代码]Component({ methods: { onTplButtonTap(e) { const { token } = e.target.dataset // cancel or confirm const evtType = e.type // 对应的事件名,如 getuserinfo/getphonenumber 等 const evtName = `bind${evtType}` const targetBtn = this.data[`${type}Btn`] if (typeof targetBtn[evtName] == 'function') { targetBtn[evtName](e.detail) } } } }) [代码] 这样就能完美透传并触发各种 button 事件了。 按钮内容传入 其事这个倒是个小问题,因为 TDesign 组件在规划的时候,就已经充分地考虑了多框架之间的差异。为了弥补框架之间的差异,都可以通过 content 的属性来传入插槽的内容,起初我还不理解,直到遇到了这个问题。 以前总觉得,可以通过 slot 的方式传入,又支持一个 content 有点多此一举。直到我遇到了需要透传 button 属性的 dialog 组件。 总结 小程序的黑盒子运行时,在遇到问题的时候真的很容易陷入盲调的困境,此时应该去看看官方文档的资料,或者网上搜一下是否其他人也遇到类似的问题,这样才可能破局。 毕竟只有他们才知道代码是怎么跑的。
2022-12-11 - 其它页面怎么读取组件的属性值?
其它页面怎么读取组件的属性值?
2021-12-27 - 如何实现一个自定义数据版省市区二级、三级联动
社区可能有其他的方案了,但是再分享下吧,给有需要的童鞋。 效果图: [图片] 额,这个视频转GIF因为社区上传不了大图,所以剪了一部分,具体的效果还是直接工具打开代码片段预览吧~ 第一步:你的页面JSON引入该组件: [代码]{ "usingComponents": { "city-picker": "/components/cityPicker/index" } } [代码] 第二步:你的页面WXML引入该组件 [代码]<city-picker visible="{{visible}}" column="2" bind:close="handleClick" bind:confirm="handleConfirm" /> [代码] 第三步:你的页面JS调用 [代码]// 显示/隐藏picker选择器 handleClick() { this.setData( visible: !this.data.visible }) }, // 用户选择城市后 点击确定的返回值 handleConfirm(e) { const { detail: { provinceName = '', provinceId = '', cityName, cityId='', areaName = '', areaId = '' } = {} } = e this.setData({ cityId, cityName, areaId, areaName, provinceId, provinceName }) } [代码] 组件属性 属性 默认值 描述 visible false 是否显示picker选择器 column 3 显示几列,可选值:1,2,3 values [0, 0, 0] 必填,默认回填的省市区下标,可选择具体省市区后查看AppData的regionValue字段 close function 点击关闭picker弹窗 confirm function 点击选择器的确定返回值 confirm: 属性 默认值 描述 provinceName 北京市 省份名称 provinceId 110000 省份ID cityName 市辖区 城市名称 cityId 110100 城市ID areaName 东城区 区域名称 areaId 110000 区域Id 至于怎么获取你想默认城市的下标,可以滑动操作下选中省市区后,点击确定后查看appData里的regionValue的值。 以上就是一个自定义数据版本的省市区二级、三级联动啦,老规矩,结尾放代码片段。 https://developers.weixin.qq.com/s/F9k9cTmT7LAz
2022-07-20 - 关于 企微第三放应用的授权流程说明
最近在做 企微 第三方应用的 H5 登录,下面把整个流程梳理下,分享给大家 首先 我们作为服务商的模式,需要构造第三方应用的授权链接https://work.weixin.qq.com/api/doc/90001/90143/91120 参考构造 第三方应用 链接构造,需要传 appid:第三方应用id授权之后,拿到code,走我们的系统登录,这个时候,接口会根据code 告知我们 哪个企业授权的应用,拿到解析后的 企业id ,以及我们自身应用的id 去初始化 js sdk 这里备注下:由于 我们开发中有更换过域名,当时初始化 js sdk的时候,一直报80001 错误,解决办法: 第三方应用修改应用内配置,这里修改可信域名 未上线的需要删除重新安装第三方应用,已上线的需要重新提交审核上线修改的配置才会生效 流程梳理如下: [图片] 代码如下: //实现一个 拦截 url search 的参数方法 const getUrlSearchParams = key => { const searUrlStr = window.location.search const paramStr = searUrlStr.split('?')[1] const searchParams = new URLSearchParams(paramStr) const val = searchParams.get(key) return val } //在页面初始化的时候,执行 getUrlSearchParams mounted(){ const code = getUrlSearchParams('code') if(code){ //执行登录逻辑 ...... //执行初始化 js-sdk ......... }else{ window.location.replace(AuthUrl) } }
2021-07-06 - 解锁小程序中使用SVG新姿势
SVG 的优势 清晰度: 可以进行放大,而不失真 更小的文件体积 可扩展性,可以动态颜色 动效 可以添加动效 在小程序中使用 目前小程序 的image标签已经支持了 svg 的显示 [代码] <image src="./xx.svg"/> [代码] 如何动态的改变 svg 属性呢? 大体思路:把svg转成 base64 然后通过 image标签 src设置图片,再动态赋值svg颜色 把svg转成base64 如下一个svg 代码文件 [代码]<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="24 24 48 48"><animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" from="0" to="360" dur="1400ms"></animateTransform><circle cx="48" cy="48" r="20" fill="none" stroke="#eeeeee" stroke-width="2" transform="translate\(0,0\)"><animate attributeName="stroke-dasharray" values="1px, 200px;100px, 200px;100px, 200px" dur="1400ms" repeatCount="indefinite"></animate><animate attributeName="stroke-dashoffset" values="0px;-15px;-125px" dur="1400ms" repeatCount="indefinite"></animate></circle></svg> [代码] 转成base64,其实就是 对这个svg进行 encodeURIComponent 得到 如下代码 [代码]%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22100%25%22%20height%3D%22100%25%22%20viewBox%3D%2224%2024%2048%2048%22%3E%3CanimateTransform%20attributeName%3D%22transform%22%20type%3D%22rotate%22%20repeatCount%3D%22indefinite%22%20from%3D%220%22%20to%3D%22360%22%20dur%3D%221400ms%22%3E%3C%2FanimateTransform%3E%3Ccircle%20cx%3D%2248%22%20cy%3D%2248%22%20r%3D%2220%22%20fill%3D%22none%22%20stroke%3D%22%23eeeeee%22%20stroke-width%3D%222%22%20transform%3D%22translate%5C(0%2C0%5C)%22%3E%3Canimate%20attributeName%3D%22stroke-dasharray%22%20values%3D%221px%2C%20200px%3B100px%2C%20200px%3B100px%2C%20200px%22%20dur%3D%221400ms%22%20repeatCount%3D%22indefinite%22%3E%3C%2Fanimate%3E%3Canimate%20attributeName%3D%22stroke-dashoffset%22%20values%3D%220px%3B-15px%3B-125px%22%20dur%3D%221400ms%22%20repeatCount%3D%22indefinite%22%3E%3C%2Fanimate%3E%3C%2Fcircle%3E%3C%2Fsvg%3E [代码] 拼接base64 [代码] data:image/svg+xml;charset=utf-8,encodeURIComponent后的代码 [代码] 在对应svg属性上动态设置颜色,比如这里用到的是填充颜色 在js文件 data中定义 color 状态 在wxml中动态渲染 [代码] <image src="data:image/svg+xml;charset=utf-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22100%25%22%20height%3D%22100%25%22%20viewBox%3D%2224%2024%2048%2048%22%3E%3CanimateTransform%20attributeName%3D%22transform%22%20type%3D%22rotate%22%20repeatCount%3D%22indefinite%22%20from%3D%220%22%20to%3D%22360%22%20dur%3D%221400ms%22%3E%3C%2FanimateTransform%3E%3Ccircle%20cx%3D%2248%22%20cy%3D%2248%22%20r%3D%2220%22%20fill%3D%22none%22%20stroke%3D%22%23{{color}}%22%20stroke-width%3D%222%22%20transform%3D%22translate%5C(0%2C0%5C)%22%3E%3Canimate%20attributeName%3D%22stroke-dasharray%22%20values%3D%221px%2C%20200px%3B100px%2C%20200px%3B100px%2C%20200px%22%20dur%3D%221400ms%22%20repeatCount%3D%22indefinite%22%3E%3C%2Fanimate%3E%3Canimate%20attributeName%3D%22stroke-dashoffset%22%20values%3D%220px%3B-15px%3B-125px%22%20dur%3D%221400ms%22%20repeatCount%3D%22indefinite%22%3E%3C%2Fanimate%3E%3C%2Fcircle%3E%3C%2Fsvg%3E" /> [代码] [代码]注意:这里的颜色 由于是已经被编码了,所以# 已经被转义了 %23, 直接写颜色数字即可[代码] 当然你也可以 去掉%23 自己实现一个内部方法 [代码] if (color && color.startsWith('#')) { return `%23${color.slice(1)}`; } [代码] 这样其实就实现了 svg的动态渲染,可是这种写法,写在wxml中 不是特别的优雅,那么如何重构下让我们的代码看起来更优雅呢? 把 svg 单独存放 支持动态返回 动态赋值 image src 属性 svg 动态函数 loading.svg.js 文件 [代码]export const loadingSvg = (color='#ddd') =>{ const svgXml = `<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%" viewBox="24 24 48 48"><animateTransform attributeName="transform" type="rotate" repeatCount="indefinite" from="0" to="360" dur="1400ms"></animateTransform><circle cx="48" cy="48" r="20" fill="none" stroke="${color}" stroke-width="2" transform="translate\(0,0\)"><animate attributeName="stroke-dasharray" values="1px, 200px;100px, 200px;100px, 200px" dur="1400ms" repeatCount="indefinite"></animate><animate attributeName="stroke-dashoffset" values="0px;-15px;-125px" dur="1400ms" repeatCount="indefinite"></animate></circle></svg>` return `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgXml)}` } [代码] 逻辑层引入,setData [代码] onLoad(){ const { loadingSvg } = require('./loading.svg.js') const svgImg = loadingSvg('#eee') this.setData({svgImg}) }, [代码] 渲染层使用 [代码] <image src="{{svgImg}}"/> [代码] github 使用案例 demoFormpSvg
2022-04-30 - 小程序什么时候支持svg交互?
现在用image引入的svg只支持显示,交互事件全部被屏蔽了,导致需要交互的动画都只能用canvas实现,用canvas的代价太大了,使用起来又非常不方便,如果支持事件能完美解决小程序大部分动画,用户体验直线上升啊@官方
2022-03-03 - 为什么小程序不继续沿用div标签而是重新定义个view标签?
为什么小程序不继续沿用div标签而是重新定义个view标签?这样做的好处是啥?
2019-06-25 - 小程序顶部自定义导航组件实现原理及坑分享
为什么使用自定义导航 对比默认导航栏,我们会更需要: 统一Android、IOS手机对于页面title的展示样式及位置 更丰富的导航栏定制效果,如添加home图标等 左上角返回事件的监听处理 统一实现双击返回顶部功能 自定义导航组件实现思路 自定义导航组件实现的核心是需要计算导航栏的真实高度 这里以官方文档->扩展能力中的Navigation组件为例分析实现思路。当使用"navigationStyle": "custom"时,默认导航被移除,页面的开始位置变成了屏幕顶部,这时我们需要实现的导航栏是在状态栏下面。 导航栏的真实高度=状态栏高度+导航栏内容。 [图片] 使用wx.getSystemInfo获取到statusBarHeight便是导航栏的高度,但是导航栏内容高度呢? 有人可能觉得导航栏内容高度顾名思义就是导航栏内容高度啊,内容撑起还用管嘛!要,必须要! 因为右上角胶囊按钮是原生加载的,我们的导航栏内容需要正好贴在胶囊的下方且垂直居中。 导航栏内容高度=(胶囊按钮的顶部距离 - 状态高度)*2 + 胶囊高度 [图片] 如何计算胶囊的数据呢?幸运的是我们有 wx.getMenuButtonBoundingClientRect() 获取胶囊按钮的布局位置信息,那么动态计算导航栏的内容高度就很方便啦。 好了,以上就是动态计算的核心思路,我们再来看官方Navigation组件高度是怎么实现的 [代码]page{--height:44px;--right:190rpx;} .weui-navigation-bar .android{--height:48px;--right:222rpx} .weui-navigation-bar__inner{ position:fixed;top:0;left:0;z-index:5001;display:flex;align-items:center; height:var(--height);padding-right:var(--right);width:calc(100% - var(--right)) } [代码] 导航栏内容的高度是通过- -height这个css变量提前声明好的,安卓机型会重新覆盖为新的css变量值,目前没发现有适配问题。 官方就是官方啊,具体尺寸都知道,那就不用一番计算周折啦,直接拿来主义即可。 导航的布局位置已经搞定啦,剩下就是写具体的内容,不同业务实现需求不同这里就不一一赘述了。 完善官方顶部导航组件 本着拿来主义,直接使用官方Navigation组件,但在实际业务开发中还是遇到不少需要自定义的需求,就比如: loadding样式没实现 标题内容超出没有出现省略号 和原生顶部的样式不兼容,导致单个页面引入时跳转有明显差异出现 没有双击返回顶部功能开关功能 引入页面需要获取导航栏的高度,来控制其他元素距离顶部的位置, 不能根据页面栈数据动态显示隐藏back按钮, 针对以上需求,我们对官方的组件进行二次完善开发,满足常规的自定义需求绰绰有余,直接引入开箱即用。 源码使用示例 https://github.com/YuniorZen/minicode-debug/tree/master/minicode02 [图片] 使用说明 [代码]/*自定义头部导航组件,基于官方组件Navigation开发。*/ <navigation-bar title="会员中心" bindgetBarInfo="getBarInfo"></navigation-bar> [代码] 组件属性列表 属性 类型 描述 bindgetBarInfo function 组件实例载入页面时触发此事件,首参为event对象,event.detail携带当前导航栏信息,如导航栏高度 event.detail.topBarHeight bindback function 点击back按钮触发此事件响应函数 backImage string back按钮的图标地址 homeImage string home按钮的图标地址 ext-class string 添加在组件内部结构的class,可用于修改组件内部的样式 title string 导航标题,如果不提供为空 background string 导航背景色,默认#ffffff color string 导航字体颜色 dbclickBackTop boolean 是否开启双击返回顶部功能,默认true border boolean 是否显示顶部导航下边框分割线,默认false loading boolean 是否显示标题左侧的loading,默认false show boolean 显示隐藏导航,隐藏的时候navigation的高度占位还在,默认true left boolean 左侧区域是否使用slot内容,默认false center boolean 中间区域是否使用slot内容,默认false Slot name 描述 left 左侧slot,在back按钮位置显示,当left属性为true的时候有效 center 标题slot,在标题位置显示,当center属性为true的时候有效 自定义顶部导航目前存在的坑 弹窗的背景蒙层无法覆盖原生胶囊按钮 页面下拉刷新的圆点会被自定义导航遮盖 如果要自定义顶部导航,以上问题避免不了,只能忍着接受。 目前还没遇到完美的解决方案,针对下拉刷新圆点被遮挡的问题微信官方还在需求开发中,如果你有好的想法欢迎留言反馈,一起学习交流。
2019-10-31 - 如何实现一个自定义导航栏
自定义导航栏在刚出的时候已经有很多实现方案了,但是还有大哥在问,那这里再贴下代码及原理: 首先在App.js的 onLaunch中获取当前手机机型头部状态栏的高度,单位为px,存在内存中,操作如下: [代码]onLaunch() { wx.getSystemInfo({ success: (res) => { this.globalData.statusBarHeight = res.statusBarHeight this.globalData.titleBarHeight = wx.getMenuButtonBoundingClientRect().bottom + wx.getMenuButtonBoundingClientRect().top - (res.statusBarHeight * 2) }, failure() { this.globalData.statusBarHeight = 0 this.globalData.titleBarHeight = 0 } }) } [代码] 然后需要在目录下新建个components文件夹,里面存放此次需要演示的文件 navigateTitle WXML 文件如下: [代码]<view class="navigate-container"> <view style="height:{{statusBarHeight}}px"></view> <view class="navigate-bar" style="height:{{titleBarHeight}}px"> <view class="navigate-icon"> <navigator class="navigator-back" open-type="navigateBack" wx:if="{{!isShowHome}}" /> <navigator class="navigator-home" open-type="switchTab" url="/pages/index/index" wx:else /> </view> <view class="navigate-title">{{title}}</view> <view class="navigate-icon"></view> </view> </view> <view class="navigate-line" style="height: {{statusBarHeight + titleBarHeight}}px; width: 100%;"></view> [代码] WXSS文件如下: [代码].navigate-container { position: fixed; top: 0; width: 100%; z-index: 9999; background: #FFF; } .navigate-bar { width: 100%; display: flex; justify-content: space-around; } .navigate-icon { width: 100rpx; height: 100rpx; display: flex; justify-content: space-around; } .navigate-title { width: 550rpx; text-align: center; line-height: 100rpx; font-size: 34rpx; color: #3c3c3c; font-weight: bold; text-overflow: ellipsis; overflow: hidden; white-space: nowrap; } /*箭头部分*/ .navigator-back { width: 36rpx; height: 36rpx; align-self: center; } .navigator-back:after { content: ''; display: block; width: 22rpx; height: 22rpx; border-right: 4rpx solid #000; border-top: 4rpx solid #000; transform: rotate(225deg); } .navigator-home { width: 56rpx; height: 56rpx; background: url(https://qiniu-image.qtshe.com/20190301home.png) no-repeat center center; background-size: 100% 100%; align-self: center; } [代码] JS如下: [代码]var app = getApp() Component({ data: { statusBarHeight: '', titleBarHeight: '', isShowHome: false }, properties: { //属性值可以在组件使用时指定 title: { type: String, value: '青团公益' } }, pageLifetimes: { // 组件所在页面的生命周期函数 show() { let pageContext = getCurrentPages() if (pageContext.length > 1) { this.setData({ isShowHome: false }) } else { this.setData({ isShowHome: true }) } } }, attached() { this.setData({ statusBarHeight: app.globalData.statusBarHeight, titleBarHeight: app.globalData.titleBarHeight }) }, methods: {} }) [代码] JSON如下: [代码]{ "component": true } [代码] 如何引用? 需要引用的页面JSON里配置: [代码]"navigationStyle": "custom", "usingComponents": { "navigate-title": "/pages/components/navigateTitle/index" } [代码] WXML [代码]<navigate-title title="青团社" /> [代码] 按上面步骤操作即可实现一个自定义的导航栏。 如何实现通栏的效果默认透明以及滚动更换title为白色背景,如下图所示: [图片] [图片] [图片] [图片] 最后代码片段如下: https://developers.weixin.qq.com/s/wi6Pglmv7s8P。 以下为收集到的社区老哥们的分享: @Yunior: 小程序顶部自定义导航组件实现原理及坑分享 @志军: 微信小程序自定义导航栏组件(完美适配所有手机),可自定义实现任何你想要的功能 @✨o0o有脾气的酸奶💤 [有点炫]自定义navigate+分包+自定义tabbar @安晓苏 分享一个自适应的自定义导航栏组件
2020-03-10 - 关于工作协同能力上的小程序支持的建议 ?
问题是围绕:个人的小程序卡片没有 shareTicket,和authPrivateMessage不支持云调用解密敏感信息。 需要实现的场景是工作协同,比如一项工作,A做了一部分,现在提交给负责人确认,需要用到卡片转发(*转到个人不是群),转发现支持两种方式,一是动态消息,一是私密消息。 一:动态消息, 整个场景必面用到activityID 来关联业务, 动态消息,转给个人不支持shareTicket ,没法拿到activityID 。所以走不通。但转到群里是可以的,不知道为什么个人不支持shareTicket ? 二:私密消息 查了下文档也测试了,转给个人是可以得到shareTicket ,但是这里也有个大问题,创作activityID 是支持云调用的,解密时不支持, 不像getShareInfo 可以用CloudID 来换取敏感数据,这样的话,不得不去维护每个用户的session_key ,从而来解密,我们没有必要去维护每个登录用户的Session_key。 总结:所以协同的场景,方式一如果个人支持ShareTicket 就好了, 方式二虽然是支持,但是这里又有新的问题,就是解密不支持云调用,类getShareInfo可以传入cloudID...... , 需要wx.authPrivateMessage返回cloudID , 这个就流畅了,感觉这块官方欠考虑,云调用这块没有支持到位,看现在文档给人以假象还以为是支持的,不知道是不是遗漏这块。 所以强烈建议wx.authPrivateMessage能返回cloudID。
2022-05-23 - 【严重安全风险】“云调用直接获取开放数据”如果用户使用自己构造的参数调用,云函数无法验证用户数据?
文档位置: https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/signature.html#method-cloud 问题描述: 根据文档描述,小程序端获取到cloudID后,将cloudID传给云函数,云函数会自动将cloudID替换为解密后的值。 如果用户按照解密后的数据结构构造参数调用云函数,云函数无法判断收到的数据是否是由cloudID解密而来。 举个例子: 小张参照着文档(https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/signature.html#method-cloud) 写了如下代码,来获取用户手机号: ////// 前端 //////// wx.cloud.callFunction({ name: modifyPhone, data: { phoneCloudID: wx.cloud.CloudID(xxx) } }) //////// 云函数 /////// exports.main = async (e) => { // 获取解密后的手机号,但是无法保证这个数据不是伪造的! const phone = e.phoneCloudID.data.phoneNumer } 小黑可以这样手动构造参数,来欺骗小张写的modifyPhone云函数: wx.cloud.callFunction({ name: modifyPhone, data: { phoneCloudID:{ cloudID:"11111111", data:{ phoneNumber: "17777777777" } } } }) 到此小程序就认为小黑的手机号为17777777777。 [图片] [图片] [图片]
2020-12-21 - 微信物流服务接入指引
微信物流服务是针对小程序内物流场景的系列解决方案,涵盖快递和同城配送,涉及到发货、查件、退货等环节。 商家只需引入组件,即可获得优质、稳定的物流服务,提高小程序经营效率和用户体验。
2022-03-29 - 微信服务平台简介
[视频] 电商直播、社区拼团、外卖点餐等越来越多的小程序玩法,引爆小程序的商业潜力,小程序经济时代真正来临。 商家线上数字化需求急增,各行业服务商业务量喷发,承接数字化市场巨量需求,微信小程序生态加速推进,数以万计的服务商,覆盖百万小程序商家,成为繁荣生态不可或缺的力量和贡献者。 迎来机遇的同时,也面临挑战: 许多商家想涉足小程序,但不会开发;有了小程序,又不会运营;面对微信海量用户,却对推广毫无头绪。 开发者也有痛点希望得到解决:人脸核身、视频/音乐、内容安全等相对高门槛的能力,中小开发者不易独立完成,急需靠谱、专业的成熟方案,以便低门槛快速使用。 为此,微信服务平台上线权威、全面、专业的认证服务,提供覆盖商家、开发者全流程各种需求的多品类优质服务。 通过平台,服务商能为商家解决小程序开发、运营、推广“三大难”;还能帮助开发者减轻开发成本,提高小程序的性能和品质;更有众多扶持和激励政策。
2021-11-26 - 首页开发实战
带你实战腾讯课堂首页开发,详解布局王者flex。 本节结束代码:https://share.weiyun.com/5Eqz7TV [视频]
2021-11-26 - 初识选择器、颜色和字体
初步认识“作为WXML和WXSS桥梁”的选择器,以及颜色与字体。 名片初始代码,点此领取:https://share.weiyun.com/5ASBOw3 [视频]
2021-11-26 - 传统前端和小程序前端对比
一、教学课件 教学PPT下载:https://share.weiyun.com/kYNlcejh 欢迎大家前往官网注册并下载工具体验:https://edu.weixin.qq.com/ 二、在线视频 [视频] 本课程将为大家讲解传统前端和小程序对比。 通过本课程的学习,将可以掌握两种前端技术的区别以及彼此的特点,让有传统前端开发经验的人员可以有一个开发思维上的过渡。
09-03 - 如何提升你的云函数性能
在使用云开发一段时间后,你一定会遇见一个问题:虽然云函数非常的方便,但我的云函数似乎性能不够好,为什么我的云函数每次加载都需 2 ~ 3 秒种,时间太长了!。 这篇文章,就来告诉你,应该如何提升你的云函数性能。 如何了解云函数运行情况? 在了解如何优化云函数的运行情况之前, 我们需要先了解,如何查看当前的云函数运行情况,这样才能有个对比。 [图片] 打开小程序开发者工具,并打开你的项目 进入到你要调试的页面,打开调试器 调用云函数,并在调试器中切换到 Network 页面,找到你的请求。 点击你的请求,然后切换到 Timing 页面,查看具体的情况。 在这个页面中,你可以理解其中的 Waiting(TTFB) 是你发起请求到你接收到返回结果的第一个字节的时间,简单的来说,就是服务器计算结果需要花费的时间。而下方的 Content Download 则是下载内容所需的时间,你可以理解是表现出网络速度快慢的数据。 总结来说,就是如果 Waiting TTFB 的值比较大,你就去优化云函数性能。如果 Content Download 的数值毕竟大,你就需要优化网络情况 优化 Waiting TTFB 云函数的运行机制 Waiting TTFB 的优化是云函数性能端的优化,那么在优化之前,我们就需要先来了解一下云函数的运行机制,以便帮助我们了解应该如何去进行性能优化。 [图片] 在蕴含运行时,具体的顺序是这样的 用户发起请求,请求发送到云开发的后台 云开发后台的调度器将请求分发给下方的执行的 worker 、容器 容器创建环境、下载代码 执行代码 在这个过程中,发起请求到云开发、调度器调度速度、调度器传递信息到容器、函数调用等,都是可以优化的,但是我们在具体的使用过程中。这些大都需要由云开发的工作人员来完成,对于我们自己来说,只能去尽可能的优化容器内部到代码层面的东西。 接下来,我们可以看看更细致的调用逻辑。 [图片] 在云开发中,我们可以将调用分为三种类型: 冷启动:图中的红色阶段,需要重新创建容器、下载代码,耗时最长 温启动:图中的黄色阶段,需要下载代码,耗时较长 热启动:图中的蓝色阶段,不需要下载代码,耗时最短 我们可以看到,最快的,是热启动,函数不需要创建容器,不需要启动函数就可以完成执行,显然比要创建容器或要下载代码的温启动和冷启动速度更快。这样,我们就得到了优化云函数性能的第一个方法 1. 让你的云函数每次调用都走热启动 当我们可以让我们的云函数的每一次调用都走热启动,少了容器的创建和函数的部署,请求的速度理所当然的要比冷启动和温启动更快。 我们可以测试一下,我设置每秒调用一次云函数,看看 TTFB 的变化。 [代码]setInterval(()=>{wx.cloud.callFunction({name:'profile'})},1000) [代码] 函数内代码是默认创建的云函数代码。 则对应的执行效果如下 [图片] 可以看到,函数的执行时间从第一次的 1.2s 降低到了 200ms左右,性能提升了 80%,我们仅仅是简单的提升了函数的调用频次,就可以实现提升函数的调用性能,这就是热启动带给我们的价值。 实施方案 如果你需要足够高的性能,不妨借助云开发的定时器,定期唤起你的容器,从而为你的容器保活,确保你的函数时刻被热启动。 2. 缩小你的函数大小 在前面我们曾介绍过,云函数在启动过程中,会创建容器和下载代码。创建容器的过程对于开发者来说不可控,不过我们可以使用一些方法,缩小我们的代码,提升代码的下载速度,比如说,缩小我们的函数代码。 这里我们可以做个测试,这里我创建了两个函数,两个函数的代码完全一致,不同的是,在实验组的函数中,我加入了一个 temp 变量的声明,这个变量的值是一个非常长的字符串,从而使得两个函数的大小分别是 68K 和 4K。 接下来,我们看看二者的执行时间。 [图片] 我们会发现,几乎没有差距的代码,因为加入了变量声明的因素,在性能上会略慢几毫秒,后续随着容器的不断复用,函数的之间的差距也越来越小,几乎可以忽视。 实施方案 对于你的代码,要尽可能的精炼,减少无用的代码,减少代码下载所需时间。 3. 削减不需要的 Package 除了下载代码以外,还需要下载 Node 环境运行所需的依赖包,虽然云开发可能针对 Node Modules 已经做了缓存,但依然存在下载的时间差区别,这里我也做了一个实验。 空包:什么都没装,把 wx-server-sdk 都卸载掉了。 复杂包:装了 Mongoose、sequelize、sails 等依赖的包。 函数逻辑上也相差无几,都是返回 Event ,则结果如下 [图片] 我们发现,前三次可能是因为涉及到依赖包的下载问题,所以前三次的时长大小对比特别的明显,而从第四次开始,二者的区别就不大了,可能是因为依赖已经完成了缓存,所以可以直接使用缓存来完成函数的执行。 实施方案 你可以选择看看你的 package.json ,看看其中是否有你不需要的依赖,将其删除,仅保留有需要的依赖,可以有效提升你的代码执行速度。 优化 Content Download 如果你想要优化 Content Download ,核心需要优化的是两个点: 手机到服务端的节点的距离和速度 内容的大小 前者一般来说,你可以通过切换不同的网络环境来实现优化,比如从 3G 切换到 4G ,从 4G 升级到 5G,这些都可以提升你的手机到服务端节点之间的速度。 此外,还可以借助内容分发网络 CDN 能力来完成缩小你到服务端节点之间的距离,不过对于云函数来说,因为你不可控,无法控制,所以这一点不再谈。 这里补充一句,云开发的文件存储都是有 CDN 的,因此,你通过云存储下载的文件才会比别人更快。 后者则一般通过调整代码来完成,比如只返回必须的资源,对于不需要的内容,不再返回,或压缩返回。 总结 最后,我们回顾一下这篇文章中介绍的优化云函数的方法: 函数下载性能优化 保持函数容器的热启动,提升函数启动性能 缩小函数大小,提升代码下载速度 削减不必要的包,减少依赖大小 网络优化 使用更好的网络,比如 Wi-Fi 云函数中仅返回所需要的内容,减少下载时间。 以上这些方法,你都在你的函数中试过么?有没有其他的优化方法?欢迎你与我分享。
2019-12-08 - 企业微信客户端字体问题?
[图片] 图1-浏览器 [图片] 图2-企业微信客户端 浏览器中的字体和客户端的字体一样,但是显示的效果不一样怎么解决 使用的字体是font-family: sans-serif;
2021-04-28 - 收不到微信支付回调通知解决方案
当收不到微信支付回调通知情况下,我们需要怎么做 1、核实上送回调地址是否可被外网访问,是否有DNS解析 2、核实是否有安全策略拦截微信支付回调通知 3、确认回调地址代码的可用性 4、如果使用V3接口,麻烦确认下是否设置加密的密钥,登录商户平台操作~请参考http://kf.qq.com/faq/180830E36vyQ180830AZFZvu.html 附: 回调通知注意事项:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=23_8&index=6 支付回调和查单实现指引:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=23_9&index=1 具体分析 一、返回XML格式错误(长期异常)已收到回调通知,并且做了业务处理。但是未按格式返回success(短期异常)回调页面已收到回调通知,但不能正确处理业务逻辑。一般出现内部代码异常或数据库异常 二、处理耗时过长或者返回非200(长期异常)已收到回调通知,并且做了业务处理。但是未按格式返回success(短期异常)一般是5秒内返回,常见PHP语言把return当做echo。或者其它写入数据流失败情况 三、网络连接失败(长期异常)商户填写无效回调地址,利用查单来获取订单状态,此失败可能会影响商户业务。但由于微信回调重试机制和商户自身查单策略,能保证业务正常(短期异常)商户服务器网络不稳定无效收到或者安全策略拦截回调通知 四、返回过多return_code非success(长期异常)商户已收到回调通知,并已完成业务逻辑处理,但返回内容错误。或者商户填写的无效地址。委托代扣因为会回调失败fail给商户,商户收到回会原样返回fail,出现大量失败数据(短期异常)同“返回XML格式错误”或商户返回错误包装成HTML网页格式展示且返回 五、回调域名无法解析(长期异常)商户填写的无效回调地址依赖主动查单获取订单状态(短期异常)域名过期,或上送回调地址错误
2021-06-10 - 云开发http请求的两种写法
对于简单的GET表单请求 可以直接将参数封装在url中 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') var request = require('request') // 云函数入口函数 exports.main = async (event, context) => { //qz return new Promise((resolve, reject) => { request({ url: event.URL, method: "POST",//GET json: true, headers: { "content-type": "application/json", "token": event.token }, }, function (error, response, body) { if (!error && response.statusCode == 200) { try { resolve(body) } catch (e) { reject() } } }) }) } [代码] 对于POST请求 参数不好封装的 [代码]// 云函数入口文件 const cloud = require('wx-server-sdk') var request = require('request') cloud.init() // 云函数入口函数 exports.main = async (event, context) => { //这里写普通话成绩查询方式 return new Promise((resolve, reject) => { request({ url: event.url, method: "POST", json: true, headers: { "content-type": "application/json", "token":event.token }, body: event.body }, function (error, response, body) { if (!error && response.statusCode == 200) { try { resolve(body) } catch (e) { reject() } } }) }) } [代码] body中填写需要的参数 body是json形式 [代码]{ xxx:xxx } [代码] 请求头可以根据自己的需要进行修改。
2019-05-28 - 小程序短信验证码登录,1分钟实现小程序发短信功能,借助云开发10行代码实现短信验证码登录小程序
老规矩先看效果图 普通短信 [图片] 验证码短信 [图片] 今天被云开发官方告知,云开发支持发短信功能了,然后就迫不及待的来尝下鲜。 进入官方文档一看,云开发给咱们开发者的福利还真不小。 不仅仅可以很方便的使用短信功能,还送了咱们1000条免费短信。不用白不用嘛。这1000条短信足够咱们把小程序短信功能,和小程序短信验证码功能都学会了。 废话不多说了,咱们直接来撸代码 一,使用云开发短信的条件 这个前置条件很重要,条件不满足,你就没法使用云开发短信功能。 使用条件 1,必须是企业小程序,目前个人小程序无法使用短信发送 2,必须开通静态网站功能(后面应该会逐步放开) 3,必须开通云开发(这个没得说,不开通云开发你还用啥云开发功能啊) 上面条件都满足以后,我们就可以来愉快的撸代码了。 二,开通静态网站功能 如果你不开通静态网站,直接调用短信发送,会报如下错误。 [图片] 其实官方文档里也有给出这个错误。 [图片] 那么我们就来开通静态网站功能。开通静态网站功能之前,必须开通云开发,配置好云开发的环境。这些我在云开发入门里讲过很多遍。还不知道的同学可以翻看下我前面的文章或者视频:https://edu.csdn.net/course/detail/26572 这里开通云开发我们借助小程序开发者工具来实现快速开通。 2-1,注册小程序 这里我就不再多说了,只有注册过小程序的appid才可以开通云开发 [图片] 我们注册好小程序后,就可以拿到appid了,如上图 2-2,创建一个小程序项目 小程序项目的创建,我这里不再多说,我前面小程序基础课里有讲过很多遍。《小程序基础学习》 [图片] 这里强调一点,就是创建小程序项目时一定要用我们自己的appid不要用测试号。 [图片] 如果你一开始是用测试appid创建的,也可以通过上图的方式更换成自己的小程序的appid。 2-3,开通云开发 这里云开发的开通,我就不做过多讲解了,我云开发课程里也讲过很多遍。大家可以去翻看下 [图片] 只需要点击开发者工具里的云开发按钮,跟着提示一步步操作就可以快速开通云开发。 2-4,开通静态网站功能 [图片] 我们上面云开发开通好以后,就可以在这里快速开通静态网站了。 [图片] 点击以后,直接点击开通即可 [图片] 这时候开通有个条件 [图片] 我们必须按照要求改变自己小程序的付费方式,把我们的付费方式改成按量付费即可。 [图片] 这里不用担心,这里的按量付费,每月都有免费额度。这些额度我们开发学习基本上够用了 [图片] [图片] 这个时候我们的静态网站功能就开通成功了。 [图片] 开通成功以后如下图。 [图片] [图片] 三,编写发送短信的云函数 其实上面静态网站功能开通以后,我们不用上传网站资源,就可以直接来使用短信功能了。 下面我们就来使用云开发的云函数功能来做短信发送功能。 老规矩先看效果图 [图片] 代码编写也很简单 [图片] 其实发送短信的代码很简单,就上面这几行。下面就来教大家如何编写这个云函数。 3-1,初始化云开发环境id 新建一个和pages平级的目录cloud,用于存放云函数 [图片] 然后在project.config.json里添加cloudfunctionRoot选项。 [图片] 然后对cloud选择当前环境 [图片] 在app.js里配置环境变量 [图片] 这个env环境id需要你去云开发控制台获取 [图片] 3-2,创建云函数 右键cloud目录,新建Node.js云函数 [图片] 然后新建一个云函数,名字你可以自定随便定。我这里用sendSms [图片] 3-3,编写云函数 [图片] 我这里把代码贴给大家,记得把env和接收短信的手机号换成你自己的。 [代码]const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) exports.main = async (event, context) => { try { const result = await cloud.openapi.cloudbase.sendSms({ env: 'xiaoshitou-zfl2q', content: '编程小石头发布了新的能力', phoneNumberList: [ "+8615611823564" ] }) return result } catch (err) { return err } } [代码] 3-4,部署云函数 上面云函数编写好了,一定要记得部署下云函数。右键sendSms然后点击下面箭头所示的即可。 [图片] 上传部署成功后,会有下面这样的提示 [图片] 四,调用云函数发送短信 我们上面云函数编写并部署成功以后,就可以来调用这个云函数,发送短信了。 4-1,编写wxml文件 在wxml文件里写一个button按钮,编写一个bindtap点击事件 [图片] 4-2,编写js文件 在js文件里实现上面button的点击事件,然后调用云函数 [图片] 调用云函数时,一定要记得这里的name必须和你的云函数名一模一样。 4-3,点击发送短信 点击发送短信 [图片] 点击发送 短信以后,可以看到日志里打印openapi.cloudbase.sendSms:ok 这就代表发送成功了。 然后再看下手机,收到下面的短信。 [图片] 到这里我们的短信发送功能就完整的实现了。 其实到这里该实现的功能,就已经实现了。但是我们使用短信场景更多的是用短信发送验证码。所以接下来给大家做一个发送短信验证码的例子出来 实战案例~发送验证码短信 老规矩,先看效果图 [图片] 我们只需要获取用户输入的手机号,然后点击获取验证码,最后输入短信里接收到的验证码,进行验证即可。 1,编写wxml 页面比较简单,就两个输入框和两个按钮 [图片] 2,编写js js里主要是获取用户输入的手机号,然后发送验证码,发送验证码调用云函数实现短信验证码发送功能。用户输入验证码以后进行校验即可。 [图片] 3,发送短信验证码 用户输入手机号以后,点击发送,可以看到我们手机上收到了如下短信。 [图片] 然后用户输入获取到的验证码,点击验证。 [图片] 可以看到验证成功,验证成功以后后面的操作就可以自己定了,比如验证成功以后跳转到登录成功页。 到这里我们就实现了验证码发送功能了。 生成随机验证码的方法 我这里把生成随机验证码的方法贴给大家。 [代码] //获取随机验证码,n代表几位 generateMixed(n) { let chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']; let res = ""; for (var i = 0; i < n; i++) { var id = Math.ceil(Math.random() * 35); res += chars[id]; } return res; } [代码] 我后面会专门录制讲解视频 官方文档: https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/cloudbase/cloudbase.sendSms.html 付费视频(含源码和笔记): https://edu.csdn.net/course/detail/26572 完整免费视频(记得三连):https://www.bilibili.com/video/BV1Ca4y1n73j/
2021-01-09 - 使用微信网页云开发 redirect_uri 域名与后台配置不一致 10003?
[图片] [图片]
2021-01-21 - 微信公众号模板信息发送有什么限制吗?那个formid有用不,用公众号发送模板消息还需要formid吗
微信公众号模板信息发送有什么限制,有什么要求吗?每天的频次多少 ,那个formid是干嘛的在这能用到吗?是不是小程序模板消息才需要fromid?,官方给的资料不很全怕遇到坑,大神帮帮忙
2020-02-05