- webview嵌套h5,拍照自定刷新页面,代码如下,无法拍照上传图片?
[图片][图片] 就一个input的onchange事件 并且没有做任何处理,拍照不了,从相册上传是可以的,是怎么回事???? 机型:苹果12、、、小米手机,只是其中2款手机都是这样的拍照上传不了 [图片][图片]
2022-07-16 - setCenterOffset更改地图中心点偏移量后,获取的地图中心坐标没有应用偏移。
通过MapContext.setCenterOffset 将地图中心点偏移量设置为[0.5,0.15]随后通过bindregionchange或者getCenterLocation拿到地图的中心位置但是拿到的经纬度还是位于地图0.5,0.5位置的经纬度,并没有应用offset详细的可以看代码片段 开发者工具上看是ok的,符合预期,但是iOS上表现的异常。
2023-05-05 - 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|小程序手势:让半屏弹窗更顺滑
在小程序页面开发中,我们经常用半屏弹窗来进来内容展示,例如:微信开放社区切换主页、加入购物车的选项页、文章留言区等等。 [图片] 常见的半屏弹窗展示逻辑是这样的: 打开弹窗:点击 “打开弹窗” 按钮展示弹窗关闭弹窗:点击“关闭按钮” 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渲染引擎初尝试
小程序架构介绍 我们都知道小程序本质上是运行在安卓端,苹果端的混合APP,只是微信提供了一套JSBridge,方便用户对一些原生功能和微信相关的功能的进行调用。而微信为了安全和性能的需要,一改以往网络架构中的单线程架构,改为小程序的双线程架构。分别是AppServie 和 Webview 两个线程,我们在小程序中编写的JS代码就是运行在AppService线程的JSCore引擎(类似V8 引擎,一个Js解释器)中,而我们的Wxml和Wxss则会依赖WebView线程进行渲染。 [图片] 目前架构存在的问题 这样的架构虽然已经极大了提高了webview的渲染性能,但是依然会存在一些问题比如: 当页面节点数目过多,很容易发生卡顿 当我们新建一个页面,就要新建一个Webview进行渲染 页面之间共享资源,需要使用Native进行通信,就会消耗更多性能 当AppService(逻辑层)与Webview(视图层)通信也需要依赖Native 所以为了解决这些问题小程序推出Skyline渲染引擎 Skyline引擎介绍 在Skyline环境中,Skyline 会创建了一条渲染线程来负责 Layout, Composite 和 Paint 等渲染任务,并在 AppService 中划出一个独立的上下文,来运行之前 WebView 承担的 JS 逻辑、DOM 树创建等逻辑。说白了就是之前的样式计算是放到渲染线程来处理,现在把和样式相关的逻辑也放到AppService线程中处理,个人猜测这个渲染线程很有可能很有可能就是flutter,这样的架构就极大减少内存的消耗,和线程上通信时间的消耗。原本wxs中的逻辑,也可以移到Appservice线程中运行 [图片] 使用Skyline引擎的使用步骤 在app.json 文件添加 [代码]"lazyCodeLoading": "requiredComponents"[代码] 属性,这是因为Skyline 依赖按需注入的特性。 [代码] { "pages": [ "pages/index/index", "pages/logs/logs", "pages/test/test" ], "window": { "backgroundTextStyle": "light", "navigationBarBackgroundColor": "#fff", "navigationBarTitleText": "Weixin", "navigationBarTextStyle": "black" }, "sitemapLocation": "sitemap.json", // 在 app.json 文件添加 "lazyCodeLoading": "requiredComponents" } [代码] 在全局或页面配置中声明为 Skyline 渲染,即 app.json 或 page.json 配上[代码]"renderer": "skyline"[代码] Skyline 不支持页面全局滚动,需在页面配置项加上 [代码]"disableScroll": true[代码],在需要滚动的区域使用scroll-view 实现 Skyline 不支持原生导航栏,需在页面配置项加上 [代码]"navigationStyle": "custom"[代码],并自行实现自定义导航栏 [代码] { "usingComponents": {}, // 在 app.json 文件添加或者page页面的json中添加 "disableScroll": true, "navigationStyle": "custom" //也可以放在App.json文件中 "renderer": "skyline" } [代码] 组件适配,参考 Skyline 基础组件支持与差异 WXSS 适配,参考 Skyline WXSS 样式支持与差异 在本地设置中勾选Skyline渲染调试,如果看不到这个选项框,看一下是否在app.json中配置了[代码]"renderer": "skyline"[代码] [图片] Skyline的 worklet 动画介绍 小程序采用双线程架构,渲染线程(UI 线程)和逻辑线程(JS 线程)分离。[代码]JS[代码] 线程不会影响 [代码]UI[代码] 线程的动画表现,如滚动效果。但引入的问题是,[代码]UI[代码] 线程的事件发生后,需跨线程传递到 [代码]JS[代码] 线程,进而触发开发者回调,当做交互动画(如拖动元素)时,这种异步性会带来较大的延迟和不稳定,[代码]worklet[代码] 动画正是为解决这类问题而诞生的,使得小程序可以做到类原生动画般的体验 worklet函数定义 [代码]function helloWorklet() { 'worklet'; //'worklet'声明该函数为work函数,可以在js线程和UI线程中调用 console.log('hello worklet'); } Page({ onLoad(options) { helloWorklet('hello') // print: hello wx.worklet.runOnUI(helloWorklet)() }, }) [代码] 在小程序控制台可以看到如下输出 [图片] 如果看见SkylineGlobal is not defined错误看看是否开启了Skyline渲染调试 [图片] worklet函数间的相互调用 [代码]function slave() { 'worklet'; return "I am slave" } function master() { 'worklet'; const value = slave() console.log(value); } [代码] 从 UI 线程调回到 JS 线程 [代码]const {runOnUI ,runOnJS} = wx.worklet function jsFun(message) { // 普通函数不需要声明为worklet console.log(message) } function uiFun() { 'worklet'; runOnJS(jsFun)('I am from UI') } [代码] 使用shared共享数据 由worklet函数捕获的静态变量,会在编译期间序列化后生成在UI线程的拷贝环境之中,这就导致我们在JS线程中后续更新了变量,但是在UI线程中时得不到最新的数值的。 [代码]const obj = { name: 'skyline'} function someWorklet() { 'worklet' console.log(obj.name) // 输出的仍旧是 skyline } obj.name = 'change name' wx.worklet.runOnUI(someWorklet)() [代码] 因此shyline使用shared来实现线程之间数据的共享 [代码]const { shared, runOnUI } = wx.worklet const offset = shared(0) function someWorklet() { 'worklet' console.log(offset.value) // 输出的是新值 1 } offset.value = 1 runOnUI(someWorklet)() [代码] 简单案例–实现探探的卡片功能 注意:编辑器版本:1.06.2303162 基础库版本:2.30.2 先看效果 [图片] 代码如下 <br> wxml 代码 [代码]<navigation-bar title="探探" /> <view class="page"> <block wx:for="{{containers}}" wx:key="*this"> <pan-gesture-handler data-id="container-{{index}}" onGestureEvent="handlePan"> <view id="container-{{index}}" class="container" style="z-index: {{zIdnexes[index]}};background-image: url({{partContentList[index]}});"> </view> </pan-gesture-handler> </block> </view> [代码] scss代码 [代码].page{ display: flex; justify-content: center; align-items: center; height: 100vh; width: 100vw; position: relative; .container{ height: 80vh; width: 95vw; background-color: burlywood; position: absolute; border-radius: 16rpx; display: flex; justify-content: center; align-items: center; background-size: cover; .image{ display: block; height: 1067rpx; width: 712rpx; margin: 0 0; } } } [代码] 核心逻辑 [代码]import { useAnimation, setAni, Animation, GestureState } from "./method" Page<{ pos: Animation }, any>({ /** * 页面的初始数据 */ data: { containers: [ "burlywood", "blue", "cyan", "black" ], zIdnexes:[], current:0, partContentList:[] }, /** * 生命周期函数--监听页面加载 */ onLoad() { this.initNode() // 当前node的下标 this.active = wx.worklet.shared(0) // 当前contentList的下标 this.current = wx.worklet.shared(0) this.zIndex = 100000 }, initNode() { // 用与保存shared值 this.Nodes = {} // 图片文件 this.contentList = [ "https://i.hexuexiao.cn/up/ca/63/4a/a32912fc26b8445797c8095ab74a63ca.jpg", "https://th.bing.com/th/id/OIP.kSrrRGx6nqOgWzbaEvVD9AHaNK?pid=ImgDet&rs=1", "https://img.zmtc.com/2019/0806/20190806061552744.jpg", "https://img.zmtc.com/2019/0806/20190806061000600.jpg", "https://img.ratoo.net/uploads/allimg/190523/7-1Z5231J058.jpg", "https://th.bing.com/th/id/R.47de9dfcc25d579d84850d4575d24a6a?rik=%2fGkmrewzIEY4Iw&riu=http%3a%2f%2fimg3.redocn.com%2ftupian%2f20150930%2fqizhimeinvlisheyingtu_5034226.jpg&ehk=rG9Ks2QRzj81mZl38gVGmWVAgCHVLWppoDezpfwdxjo%3d&risl=&pid=ImgRaw&r=0", "https://th.bing.com/th/id/R.95f8e6f6bd5b660ae3ad4f3e0d712276?rik=ELKcha%2bE5ryuiw&riu=http%3a%2f%2f222.186.12.239%3a10010%2fwlp_180123%2f003.jpg&ehk=mVN7AzIRR%2fmVPJYWrWOFbEiher3QWtwSdH%2f%2fe4lE7n8%3d&risl=&pid=ImgRaw&r=0" ] this.data.containers.forEach((_: string, index: number) => { if (index == 0) { this.Nodes[`#container-${index}`] = useAnimation(`#container-${index}`, { x: 0, y: 0 }, this) this.setData({ [`zIdnexes[${index}]`]:100000-index, [`partContentList[${index}]`]:this.contentList[index] }) } else { console.log("10123") this.Nodes[`#container-${index}`] = useAnimation(`#container-${index}`, { x: 0, y: 20, scale: 0.95 }, this) this.setData({ [`zIdnexes[${index}]`]:100000-index, [`partContentList[${index}]`]:this.contentList[index] }) } }); }, handlePan(evt: any) { "worklet"; console.log(evt) const now = this.Nodes[`#container-${this.active.value}`] as Animation const next = this.Nodes[`#container-${(this.active.value+1)%4}`] as Animation if (evt.state == GestureState.ACTIVE) { // 滑动激活状态 // 设置当前的滑动块 now.x.value += evt.deltaX now.y.value += evt.deltaY now.rotate.value = now.x.value * 10 / 360 // 设置下一个滑动块 let rate = Math.abs(now.x.value) / 150 rate = rate > 1 ? 1 : rate next.y.value = (20 - rate * 20) < 0 ? 0 : (20 - rate * 20) next.scale.value = 0.95 + rate * 0.05 } if (evt.state == GestureState.END) { // 滑动结束 if (Math.abs(now.x.value) < 150) { // 判断是否超过界限值 setAni(now.x, 0) setAni(now.y, 0) setAni(now.rotate, 0) } else if (now.x.value < 0) { // 判断判断左划还是右划 setAni(now.x, -2000) setAni(now.y, -2000) setAni(now.rotate, 0) // 通知js线程进行数据的更新 wx.worklet.runOnJS(this.toNext.bind(this))() } else if (now.x.value > 0) { setAni(now.x, 2000) setAni(now.y, -2000) setAni(now.rotate, 0) wx.worklet.runOnJS(this.toNext.bind(this))() } } }, // 将当前序号的跳转到下一个 toNext(){ const current = this.current.value+1 this.active.value = current%4 this.current.value = current this.setData({ current }) if(current-2>=0){ wx.worklet.runOnUI(this.toReset)((current-2)%4) this.setData({ [`zIdnexes[${(current-2)%4}]`]:99998-current, [`partContentList[${(current-2)%4}]`]:this.contentList[current+2] }) } }, // 将动画归位 toReset(index:number){ "worklet"; const reset = this.Nodes[`#container-${index}`] as Animation setAni(reset.x, 0,0) setAni(reset.y, 20,0) setAni(reset.rotate, 0,0) setAni(reset.scale, 0.95,0) } }) [代码] 参考 skyline worklet 动画
2023-03-20 - 小程序内嵌的H5页面 怎么进行小程序订阅消息的授权和调用?
现在在小程序里面是通过内嵌H5页面来实现的功能,用户都是在这些内嵌的H5页面上进行浏览操作,请问各位大佬,怎么在小程序内嵌的H5页面,进行小程序订阅消息的授权和调用
2021-03-31 - 小程序内嵌h5有办法调订阅消息通知吗?
小程序内嵌h5有办法调订阅消息通知吗?
2021-11-23 - 如何自己在小程序内做埋点数据统计
如何在自己小程序内做数据埋点 小程序后台已经有了较为完善的数据统计和基础的分析,但是功能还是比较基础的,通常我们对数据分析有较高的要求时,就需要自己做数据收集了。那小程序内如何做自动的数据埋点和手动埋点呢。 自动埋点 启动时间 (onLaunch) 系统信息 (systemInfo) 停留时长 (Page onHide - Page onShow) 来源 (query 参数) PV (Page onShow) UV (结合用户筛选PV) 预设点击数据收集 (onTap 等) 手动埋点 自定义点击收集数据 改造小程序生命周期 自动埋点是需要集成到底层内,不能对业务进行侵入,所以,我们需要改造小程序生命周期,在不同的生命周期内进行预设收集数据的功能。 对 [代码]App[代码] 进行重写 [代码]const oldApp = App; // 我们需要重写的方法 const appFn = ['onLaunch', 'onShow', 'onHide']; App = function (options) { let oldFuncs = {}; appFn.forEach((item) => { oldFuncs[item] = options[item] }) appFn.forEach((item) => { options[item] = function (options) { // todo 做各类数据收集 oldFuncs[item].apply(this, arguments) } }) oldApp.apply(this, arguments); }; [代码] 对 [代码]Page[代码] 重写 [代码]const oldPage = Page; const pageFn = ['onLoad', 'onShow', 'onHide', 'onUnload', 'onShareAppMessage', 'onAddToFavorites'] Page = function (options) { let oldFuncs = {}; pageFn.forEach((item) => { if (options[item]) { oldFuncs[item] = options[item] } }) pageFn.forEach((item) => { if (options[item]) { options[item] = function () { console.log('Page', item, ); // 收集各类数据 oldFuncs[item].apply(this, arguments) } } }) // 以下代码则是对除生命周期类的方法进行重写,做预设点击事件收集数据 const methods = getMethods(options); if (!!methods) { for (var i = 0, len = methods.length; i < len; i++) { clickProxy(options, methods[i]); } } oldPage.apply(this, arguments); } [代码] 对 [代码]Component[代码] 重写 [代码]const oldComponent = Component; Component = function (options) { // 对组建内 methods 进行重写预设点击事件埋点收集 Object.keys(options.methods).forEach((method) => { clickProxy(options.methods, method) }) oldComponent.apply(this, arguments); } [代码] 以上对 [代码]App[代码] [代码]Page[代码] [代码]Component[代码] 进行重写之后,在必要的地方,加入自己的上报代码. 以下完整代码 [代码] const mpHook = { data: 1, onLoad: 1, onShow: 1, onReady: 1, onPullDownRefresh: 1, onReachBottom: 1, onShareAppMessage: 1, onPageScroll: 1, onResize: 1, onTabItemTap: 1, onHide: 1, onUnload: 1, }; const oldApp = App; const oldPage = Page; const oldComponent = Component; const appFn = ['onLaunch', 'onShow', 'onHide'] App = function (options) { let oldFuncs = {}; appFn.forEach((item) => { oldFuncs[item] = options[item] }) appFn.forEach((item) => { options[item] = function (options) { console.log('App', item); // 收集各类数据 oldFuncs[item].apply(this, arguments) } }) oldApp.apply(this, arguments); }; const pageFn = ['onLoad', 'onShow', 'onHide', 'onUnload', 'onShareAppMessage', 'onAddToFavorites'] Page = function (options) { let oldFuncs = {}; pageFn.forEach((item) => { if (options[item]) { oldFuncs[item] = options[item] } }) pageFn.forEach((item) => { if (options[item]) { options[item] = function () { console.log('Page', item, ); // 收集各类数据 oldFuncs[item].apply(this, arguments) } } }) const methods = getMethods(options); if (!!methods) { for (var i = 0, len = methods.length; i < len; i++) { clickProxy(options, methods[i]); } } oldPage.apply(this, arguments); } Component = function (options) { Object.keys(options.methods).forEach((method) => { clickProxy(options.methods, method) }) oldComponent.apply(this, arguments); } function clickProxy(options, method) { const oldFunc = options[method]; options[method] = function () { const pages = getCurrentPages(); const currentPage = pages[pages.length - 1]; const pageQuery = currentPage.options || {}; const pagePath = currentPage.route; const res = oldFunc.apply(this, arguments); let prop = {}, type = ""; if (isObject(arguments[0])) { const current_target = arguments[0].currentTarget || {}; const dataset = current_target.dataset || {}; type = arguments[0]["type"]; prop["$event_type"] = type; prop["$event_timestamp"] = Date.now(); prop["$element_id"] = current_target.id; prop["$element_type"] = dataset["type"]; prop["$element_content"] = dataset["content"]; prop["$element_name"] = dataset["name"]; prop["$page_path"] = pagePath; prop["$page_quey"] = pageQuery; if (isObject(arguments[0].event_prop)) { prop = Object.assign(prop, arguments[0].event_prop); } } console.log('type', type) if (type) { // 可以对不同事件类型进行筛选是否需要收集 post(prop) } console.log(res); return res; }; } const getMethods = function (options) { let methods = []; for (let m in options) { if (typeof options[m] === "function" && !mpHook[m]) { methods.push(m); } } return methods; }; const isObject = function (obj) { if (obj === undefined || obj === null) { return false; } else { return toString.call(obj) == "[object Object]"; } }; const post = function (data) { console.log('data', data) // 提交数据时,可以在组合下 systeminfo 用户信息等相关信息 wx.request({ url: 'https://www.example.php', method: 'post', data }) } // 手动埋点的部分,自己在需要收集的地方调用相关方法收集数据 [代码]
2021-06-24 - 小程序一次性订阅消息详解
简介 相对来说小程序发送通知用得更多的还是一次性订阅模版,发现社区挺多同学对一次性订阅的一些细节设定存在不少疑问。所以这里整理了一些常见的问题。 订阅框长这样,本文也围绕这个弹窗展开: [图片] 常见问题Q&A 1.获取模版ID 1.1 如何获取到合适的模版ID? 答:进入MP后台,https://mp.weixin.qq.com, 可以在【功能】->【订阅消息】->【公共模板库】选择合适的模版。选择后在【我的模版】 查看到模版ID。 注意: 不同小程序,选用同一个模版,生成的模版ID是不一样的。 [图片] 1.2 如何申请新的模版? 答:在MP后台申请,入口比较隐蔽。如要在【公共模版库】搜索任意模版,然后翻到最后一页,会出现【帮助我们完善模板库】的选项,点击进入申请新模版。比如此处我搜索了签到 [图片] 2.订阅次数 2.1 勾选了【总是保持以上选择,不再询问】,是不是就可以多次向发送通知了? 答:不能,用户没有再次点击触发订阅的按钮就不会增加订阅次数。勾选之后只是触发订阅时不再弹窗,保持上一次用户的选项进行订阅接口的调用。 2.2 用户一天内多次点击订阅后,是不是就能发送多条通知? 答: 是的,用户一天内点了N次订阅,就可以向用户发送多条通知。 2.3 用户点击订阅后,此次发通知机会的有效期是多久? 答: 永久有效。 2.4 勾选部分模版,再选择勾选 【总是保持以上选择,不再询问】,点击【确定】后,下次订阅哪几个模版的订阅次数会增加? [图片] 答: 如上图,只勾选了AB,然后保持选项点击【确定】,下次订阅时只有A,B模版的订阅次数会增加。 2.5 勾选部分模版,再选择勾选 【总是保持以上选择,不再询问】,点击【取消】后,下次订阅哪几个模版的订阅次数会增加? 答: 如果选择了【取消】+ 【保持选项】,所有订阅消息次数都不会再增加。如上图,不管是否勾选,A、B、C三个模版次数都不会增加。 2.6 勾选【总是保持以上选择,不再询问】选项后,如何修改选择? 答: 可以在小程序设置页面,配置是否接收模版消息。(小程序右上角三点按钮,可以唤起进入设置页面的面板) [图片] 2.7 为什么我设置的通知页面没有显示MP后台配置的通知模版? 答:只有勾选过【总是保持以上选择,不再询问】的模版消息才会进入这个配置页面。 2.8 关闭接口通知按钮后(上图红框内按钮),再开启,订阅次数怎么计算? 答: 关闭按钮后,所有通知都无法接收,订阅次数全部清零。所以重新开启后,也需要用户重新订阅才能再次发送通知。 3.弹窗相关 3.1 模版ABC已经勾选【保持选项】,在另一处同时订阅ADF,此时还会弹窗吗? 答: 会,但是弹窗中只会有两个模版DF,模版A不显示。 3.2 开发者勾选【总是保持以上选择,不再询问】后,怎么重新唤起弹窗? 答:微信开发者工具,清除全部缓存。重新生成二维码,测试机扫码后即可重新唤起弹窗。 Tips:登陆开发者工具的微信账号和需要重置弹窗的微信账号需要保持一致。
09-04 - 微信小程序如何知道onLaunch和onShow得到的场景值,是冷启动场景值还是热启动场景值?
公众号的显示和冷热场景值相关,如何知道onLaunch和onShow得到的场景值,是冷启动场景值还是热启动场景值?
2023-04-24 - 关于小程序隐私保护指引设置具体生效时间的问题?
前期说0915之后,官方强制隐私协议弹窗内容就生效了,现在又说生效时间可能延期到10月。那如果在官方生效之前,小程序发布的时候app.json文件中设置了"__usePrivacyCheck__": true ,那最终发布的生产环境小程序有没有启用隐私相关功能呢?小程序能否正常调用隐私接口? 官方说法: [图片] 官方说法: [图片]
2023-09-14 - 开发隐私协议后,体验版正常弹窗,发布正式版后不弹窗?
在体验版中,使用被动触发 隐私协议,正常弹窗。但是提交审核后,发布了正式版本。隐私协议不弹窗。有遇到类似情况的人请问怎么解决?或者有什么知情原因的大佬请指点一下
2023-09-14 - 小程序绑定了第三方平台 chooseAvatar 应该怎么设置隐私协议?
目前<button open-type="chooseAvatar"> 报错 <button>: chooseAvatar:fail api scope is not declared in the privacy agreement; 通过commit接口上传的代码包,然后再getprivacysetting接口,返回小程序并不需要设置任何隐私协议内容。 如果我们setprivacysetting接口把setting_list设置为空数组,审核可以通过,如果加上UserInfo,那审核的时候就会提示要完善隐私协议。 所以这个到底要怎么调用接口去设置?
2023-09-14 - 隐私协议提示现网隐私协议不存在?
APPID:wxfeb2202a4ffcea9f { "errcode": 86074, "errmsg": "现网隐私协议不存在 rid: 621f1a81-660cbe14-292de404" }
2022-03-02 - 隐私协议设置成功,api 还是无法调用 ?errno:112
通过三方授权设置协议后,提审发布小程序,通过 openPrivacyContract 可以看最新隐私协议,但是协议中部分 api 仍然无法调用,errno:112 错误。请问是什么原因。
2023-08-31 - 您好,隐私协议已经更新并通过审核,getUserProfile报错,errno: 112?
您好,我的隐私协议已经更新,并通过了审核,但是我在调用了隐私协议弹窗组件后,wx.getUserProfile报错 {errMsg: "getUserProfile:fail api scope is not declared in the privacy agreement", errno: 112},需要我怎么操作 [图片] [图片] [图片]
2023-09-06 - 《用户隐私保护指引》mp.weixin.qq.com-设置-服务内容声明模块缺失?
你好,检测到你的小程序有采集用户隐私信息,根据《中华人民共和国个人信息保护法》等法规政策及平台运营规范,需在“mp.weixin.qq.com-设置-服务内容声明模块”完善《用户隐私保护指引》。平台将于2023年6月15日确认你的小程序京心团购店长版(wx0de1a40953182cb1)《用户隐私保护指引》完善情况,若到期未完善将回收小程序隐私接口调用权限。 [图片]
2023-06-09