- 针对同层渲染偶发失败,导致原生组件把web组件给遮住的问题,在官方彻底解决之前,老夫有一曲线救国之妙计
官方在19年就发布支持原生组件的同层渲染,但众所周知在这之后官方一直无法确保此功能在所有机型有效,我也理解官方的难处,毕竟同层渲染的实现原理确实有点取巧,同层渲染让我们可以将web组件渲染在原生组件之上,支持完整的样式、动画,这是令人心动的。 但是。同层渲染存在失败几率,需要通过bindrendererror来进行效果降级,如果就此为止那也就没啥了,大不了渲染失败时我用cover系列组件顶上,丑就丑点。 问题是!同层渲染已完成后我们发现web组件偶发性被渲染到原生组件底下去了并且不会触发bindrendererror!特别是涉及滑动这类操作时更为明显。 因此,我在此先给出一个曲线救国的办法,我发现发生此类情况时,只需要切换页面并回来或者点击vConsole或者后台切回都会恢复正常,那就是意味着,只要原生组件上有其它组件需要渲染时会重新进行同层渲染!那么我只要确保有一个看不见的元素不断的显示和隐藏就可以一直触发同层渲染!只要速度够快用户就看不见bug! 以下是实现前(上拉并拉回时一个按钮和一条白边被渲染到原生组件底下去了): [图片] 实现后(正常渲染)的效果: [图片] 以下是实现方法,虽然不太好,但我目前只能想到这个,后续看看有没有更好的优化方法: 方案一: 建议使用方案二,不会直接对DOM树进行增加和删除元素,对性能会友好一点,当然,如果方案二你们测试有问题可以使用方案一。 1.给原生组件上面放一个0宽高的元素,由一个变量控制显示或隐藏 [图片] 2.写一个定时器控制该元素一直显示和隐藏,我这里只设100ms,低了怕影响性能,这个定时器只放在需要用同层渲染的时候,并且卸载页面时要记得停掉 [图片] 3.然后由于这个元素一直被显示和隐藏,所以一直触发了同层渲染,就可以让错误渲染的web组件重新回到最前面 方案二: 1.给原生组件上面放一个0宽高的元素,由一个变量控制display样式来实现显示或隐藏 [图片] 2.写一个定时器控制该元素一直显示和隐藏,我这里只设100ms,低了怕影响性能,这个定时器只放在需要用同层渲染的时候,并且卸载页面时要记得停掉 [图片] 3.然后由于这个元素一直被显示和隐藏,所以一直触发了同层渲染,就可以让错误渲染的web组件重新回到最前面
2021-02-05 - @keyframes 动画在ios上没效果,安卓和工具ok
用css3动画做了一个倒计时,本来以为找到了比较简单的方法完成了,但是发现在ios上没有效果,在工具和安卓机都是可以的,求解! [图片]
2019-09-19 - 小程序取消橡皮筋回弹效果解决方案及坑总结
提到ios系统的橡皮筋效果,作为开发者是又爱又恨,有想要这个效果又有不想要的,无奈的是却没有一个简单的开关来设置这个效果是否开启。 最近在开发小程序时也遇到有关于ios橡皮筋回弹的问题,这里分两部分(取消橡皮筋回弹效果和因为这个效果遇到的坑)和大家分享一下。 取消IOS橡皮筋回弹效果的解决方案 1) 页面无滚动区域时,可通过页面json配置文件设置disableScroll:true禁止整个页面滚动,从而取消橡皮筋效果。 [代码]{ "disableScroll":true } [代码] 测试代码:https://github.com/YuniorZen/minicode-debug/tree/master/minicode01/pages/demo1 2) 页面有滚动区域,滚动区域通过view模拟实现,然后在页面json配置文件设置disableScroll:true禁止整个页面滚动,从而取消橡皮筋效果。 [代码]json文件配置 { "disableScroll":true } view元素模拟实现滚动样式 { height: calc(100vh - 120rpx); //高度必须是固定的值 overflow-y: auto; } [代码] 不足之处在于view元素模拟的滚动区域滚动时不够连贯,没有scroll-view那种原生丝滑般的感觉。 测试代码:https://github.com/YuniorZen/minicode-debug/tree/master/minicode01/pages/demo2 3) 页面有滚动区域,滚动区域使用scroll-view,这时通过disableScroll则无法实现,尝试设置一下scroll-view的scroll-y="{{false}}",上拉或下拉时居然不再触发橡皮筋的回弹啦,当然滚动区域也不能滚动。 小脑袋动一动,解决方法有啦! 通过设置一个变量scrollY动态控制滚动区域的滚动从而阻止回弹。 监听bindscrolltoupper\bindscrolltolower当scroll-view区域滚动到顶部或底部时候设置scrollY:false来关闭页面滚动,从而阻止回弹。 监听bindtouchstart\bindtouchmove 当用户反方向滑动的时候设置scrollY:true,再次开启页面滚动。 [代码]wxml滚动区域属性和事件处理,具体实现请点击测试代码链接 <scroll-view scroll-y="{{scrollY}}" class="list" upper-threshold="5" lower-threshold="5" bindscrolltoupper="bindscrolltoupper" bindscrolltolower="bindscrolltolower" bindtouchstart="touchstart" bindtouchmove="touchmove"> <view class="list-item" wx:for="{{list}}" wx:key="{{index}}">{{item}}</view> </scroll-view> [代码] 相对view模拟实现滚动区域,scroll-view滚动更丝滑,不过每次滚动到底部或顶部的时候,再反向滑动时由于再次开启scroll-view滚动会有操作卡顿的感觉,暂时没想到好的解决方法,有解决的大佬希望提供一下想法,一起学习下。 测试代码:https://github.com/YuniorZen/minicode-debug/tree/master/minicode01/pages/demo3 IOS橡皮筋效果遇到的坑 1) 操作左滑删除组件时上下移动,会触发橡皮筋效果导致页面抖动的问题 这个坑的严重程度看设计师的意愿了,反正我们团队目前是需要解决的,方案类似取消橡皮筋解决方案的第三种 在左滑的时候关闭scroll-view的滚动,取消时再次开启滚动 2) 如果页面顶部有置顶的横向滚动区域scroll-view,当页面滚动到底部时继续上拉会导致置顶头部消失,松开回弹后头部又会出现。 坑是社区里的朋友提出来的,我借了个iphone x 一预览,我嚓,还是真是个奇坑! 微信官方回复已复现正在解决中… 不想继续等下去的,暂时解决方法是 监听页面的滚动区域,当滚动到底部时设置顶部横向滚动scroll-view的scroll-x=false来解决。 写在最后 以上便是我在小程序开发中有关于ios橡皮筋回弹效果的分享,示例代码已上传github,可自行下载体验。 https://github.com/YuniorZen/minicode-debug/tree/master/minicode01 目前微信官方虽说已经着手解决(已两年)此类bug,但哪吒说 我命由我不由天,所以还是我们开发者多分享些解决方案自救来的快。 分享方案如有问题还望不吝指出,没有涉及到的坑也欢迎评论提出,一起学习和解决,后续也会基于此篇不断更新总结。
2021-01-14 - 教你怎么监听小程序的返回键
更新:2020年7月28日08:51:11 基础库2.12.0起,可以调用wx.enableAlertBeforeUnload监听原生右上角返回、物理返回以及wx.navigateBack时弹框提示 AIP详情请看: https://developers.weixin.qq.com/miniprogram/dev/api/ui/interaction/wx.enableAlertBeforeUnload.html //======================================== 怎么监听小程序的返回键? 应该有很多人想要监听用户的这个动作吧,但是很遗憾,小程序不会给你这个API的,那是不是就没辙了? 幸好我们还可以自定义导航栏,这样一来我们就可以监听用户的这一动作了。 什么?这你已经知道啦? 那好咱们就不说自定义导航栏的返回监听了,说一下物理返回和左滑?右滑?(不管了,反正是滑)返回上一页怎么监听。 监听物理返回 首先说一下这个监听方法的缺点,虽说是监听,但是还是无法真正意义上的监听并拦截来阻止页面跳转,页面还是会返回上一页,而后重新载入刚刚的页面,如果这不是你想要的,那可以不用往下看了 其次说一下用到什么东西: wx.onAppRoute、wx.showModal 最后是一些主要代码: 重写wx.showModal,主要是加个confirmStay参数和使wx.showModal Promise化 [代码]const { showModal } = wx; Object.defineProperty(wx, 'showModal', { configurable: false, // 是否可以配置 enumerable: false, // 是否可迭代 writable: false, // 是否可重写 value(...param) { return new Promise(function (rs, rj) { let { success, fail, complete, confirmStay } = param[0] param[0].success = (res) => { res.navBack = (res.confirm && !confirmStay) || (res.cancel && confirmStay) wx.setStorageSync('showBackModal', !res.navBack) success && success(res) rs(res) } param[0].fail = (res) => { fail && fail(res) rj(res) } param[0].complete = (res) => { complete && complete(res) (res.confirm || res.cancel) ? rs(res) : rj(res) } return showModal.apply(this, param); // 原样移交函数参数和this }.bind(this)) } }); [代码] 使用wx.onAppRoute实现返回原来的页面 [代码]wx.onAppRoute(function (res) { var a = getApp(), ps = getCurrentPages(), t = ps[ps.length - 1], b = a && a.globalData && a.globalData.pageBeforeBacks || {}, c = a && a.globalData && a.globalData.lastPage || {} if (res.openType == 'navigateBack') { var showBackModal = wx.getStorageSync('showBackModal') if (c.route && showBackModal && typeof b[c.route] == 'function') { wx.navigateTo({ url: '/' + c.route + '?useCache=1', }) b[c.route]().then(res => { if (res.navBack){ a.globalData.pageBeforeBacks = {} wx.navigateBack({ delta: 1 }) } }) } } else if (res.openType == 'navigateTo' || res.openType == 'redirectTo') { if (!a.hasOwnProperty('globalData')) a.globalData = {} if (!a.globalData.hasOwnProperty('lastPage')) a.globalData.lastPage = {} if (!a.globalData.hasOwnProperty('pageBeforeBacks')) a.globalData.pageBeforeBacks = {} if (ps.length >= 2 && t.onBeforeBack && typeof t.onBeforeBack == 'function') { let { onUnload } = t wx.setStorageSync('showBackModal', !0) t.onUnload = function () { a.globalData.lastPage = { route: t.route, data: t.data } onUnload() } } t.onBeforeBack && typeof t.onBeforeBack == 'function' && (a.globalData.pageBeforeBacks[t.route] = t.onBeforeBack) } }) [代码] 改造Page [代码]const myPage = Page Page = function(e){ let { onLoad, onShow, onUnload } = e e.onLoad = (() => { return function (res) { this.app = getApp() this.app.globalData = this.app.globalData || {} let reinit = () => { if (this.app.globalData.lastPage && this.app.globalData.lastPage.route == this.route) { this.app.globalData.lastPage.data && this.setData(this.app.globalData.lastPage.data) Object.assign(this, this.app.globalData.lastPage.syncProps || {}) } } this.useCache = res.useCache res.useCache ? reinit() : (onLoad && onLoad.call(this, res)) } })() e.onShow = (() => { return function (res) { !this.useCache && onShow && onShow.call(this, res) } })() e.onUnload = (() => { return function (res) { this.app.globalData = Object.assign(this.app.globalData || {}, { lastPage: this }) onUnload && onUnload.call(this, res) } })() return myPage.call(this, e) } [代码] 在需要监听的页面加个onBeforeBack方法,方法返回Promise化的wx.showModal [代码]onBeforeBack: function () { return wx.showModal({ title: '提示', content: '信息尚未保存,确定要返回吗?', confirmStay: !1 //结合content意思,点击确定按钮,是否留在原来页面,confirmStay默认false }) } [代码] 运行测试,Oj8K 是不是很简单,马上去试试水吧,效果图就不放了,静态图也看不出效果,动态图懒得弄,想看效果的自己运行代码片段吧 代码片段 https://developers.weixin.qq.com/s/hc2tyrmw79hg
2020-07-28 - 一个奇葩思路实现的瀑布流双列布局
传统的瀑布流布局实现一般关键是去计算每一列的高度,从而判断下一个元素应该插入到哪一列(当然是最短的那列)。 这个奇葩思路没有任何计算,主要思路如下: 在瀑布流容器底部加入一根细线 利用微信小程序的IntersectionObserver,为每一列和细线添加监听 逐个加入要插入的item元素 根据监听相交变化结果判断下一个item应该插入哪一列(简单来说就是插入到当前不与细线相交的那一列,因为比较短) 这个思路实际上就是把计算高度换成了监听判断哪列更高,因此也不必知道每个元素的高度。 目前只能支持两列布局的情况,如果列数更多我没办法不通过计算来知道哪列最短,如果有思路或想法的童鞋欢迎交流~ 实现过程也比较简单,就分享个思路,不贴代码了(问就是懒!) 感兴趣的童鞋可以看代码片段,里面有完整的实现代码: https://developers.weixin.qq.com/s/nH5pg4mE78dG
2019-11-23 - 笔记
1.textarea placeholder 换行 在需要换行的地方加 \n ,在开发者工具没有效果,手机预览可以正确换行 2.给上一个页面传值 2.1.将值放入app.globalData 中,在返回页面onShow中取值 2.2. [代码] let pages= getCurrentPages(); let prePage = pages[pages.length - 2]; prePage.setData({ }) [代码] 3.页面传值带有特殊符号 3.1 放入app.globalData 中,在需要的页面取值 3.2 对参数的值进行编码,在需要的页面解码 4.将页面不需要的值定义一个otherData,将值放入 5.IOS中处理时间,new Date()不支持 ‘-’,可将‘-’替换为‘/‘,再进行时间计算 6.双击事件处理 [代码] <view bindtouchstart="touchStart" lang='zh-CN' bindlongpress='longFalse' bindtouchend="touchEnd" bindtap='click'>点击</view> /// 按钮触摸开始触发的事件 touchStart(e) { this.touchStartTime = e.timeStamp; }, /// 按钮触摸结束触发的事件 touchEnd(e) { this.touchEndTime = e.timeStamp }, // 长按时 longFalse: function() { return; }, click(){ var that = this; if (that.touchEndTime - that.touchStartTime < 350) { // 当前点击的时间 var currentTime = e.timeStamp; var lastTapTime = that.lastTapTime; // 更新最后一次点击时间 that.lastTapTime = currentTime; // 如果两次点击时间在300毫秒内,则认为是双击事件 if (currentTime - lastTapTime < 350) { // 成功触发双击事件时,取消单击事件的执行 } else { //单击事件处理 } } } [代码]
2019-06-30 - 养成良好的代码风格--03 IDEA
IDEA 良好的统一的代码风格的好处真的是太多了。。。。 算是接上篇文,因为最近接手维护一个管理后台。撸小程序的时候管理后台也要修修改改,过个工具来回切有点耗内存,加上开发者工具也不少吃,小机子扛不住了~。为了解决这个问题,我选择了这个超集工具IDEA,平时的话在用vscode,做项目用IDEA。 👉 IDEA 撸码 开发者工具只做预览,嗯。舒服,觉得开发者工具卡的话也可以用 其他工具+开发者工具配合使用。 xxxxxxxx 代码格式化是一部分,别忘了加上合适的注释,搭配使用才完美 因为之前vscode用了eslint + prettier 根本好用到停不下来,昨天打算用它来开发小程序,那必须也要用上。很可惜 prettier 不支持 [代码]*.wxml *.wxss[代码] 所以对着文档配置(加班搞呀搞呀搞呀搞),今天又接着配置,可惜在IDEA就是不生效,,,,,放弃 😊。。。 最后解决办法是 [代码]js[代码] 和[代码]json[代码] 用prettier 。[代码]wxml wxss[代码] 用IDEA自带的格式化处理。 维护自己的项目都头晕烟花,更别说维护其他人的,先美化一下是不是舒服多了 使用前 [图片] 使用后 [图片] 然后就是一个很好用的功能 [代码]file-watchers[代码] 。webstrom 自带支持,IDEA需要去插件市场安装插件,使用方法还是一样的。超集真的是爸爸级别的。 个人习惯设置为主动保存或者10s无动作自动格式化。👇 [图片] 有一个趁手的工具还是很有必要的。 prettier大神教教我怎么添加其他类型的文件,按照文档怎么都不生效。。。👇下面是配置经过, 养成良好的代码习惯少不了 eslint。今天主要使用了两个插件prettier和file-watchers。file watch、prettier 在webstrom自带有,IDEA的话需要安装插件。 eslint eslint是用来识别 ECMAScript 并且按照规则给出报告的代码检测工具。使用它可以避免第几错误和统一代码的风格。 eslint 配置 ESlint被设计为完全可配置的,主要有两种方式来配置 在注释中配置:使用JavaScript注释直接把配置潜逃到JS文件中 配置文件:使用配置文件为所有目录或子目录指定配置信息 javascript:使用[代码].eslintrc.js[代码]文件配置并导出一个包含配置的对象。 YAML:[代码].eslintrc.yaml[代码]or[代码].eslintrc.yml[代码]。 JSON: [代码].eslintrc.json[代码],json文件允许使用JS形式的注释。 package.json: 在[代码]package.json[代码] 文件中创建[代码]eslintConfig[代码]属性,所有的配置包含在这个属性中。 废弃的用法: [代码].eslintrc[代码] 可以是JSON或者YAML。 这些配置文件的优先级的顺序([代码].eslintrc.js[代码] > [代码].eslintrc.yaml[代码] > [代码].eslintrc.yml[代码] > [代码].eslintrc.json[代码] > [代码].eslintrc[代码] > [代码]package.json[代码])。 配置主要的信息分为三类 Environments:你的javascript脚本将要运行在什么环境中(例如:node.js、browser、commonjs等)。 Globals: 执行代码脚本时需要访问的额外全局变量。 Rules:规则 [代码]// 参考AlloyTeam配置 module.exports = { extends: [ 'alloy', ], env: { // 这里填入你的项目用到的环境 // 它们预定义了不同环境的全局变量,比如: // // browser: true, // node: true, // mocha: true, // jest: true, // jquery: true }, globals: { // 这里填入你的项目需要的全局变量 // false 表示这个全局变量不允许被重新赋值,比如: // // myGlobal: false }, rules: { // 这里填入你的项目需要的个性化配置 } }; [代码] eslint 安装 IDEA eslint网址:https://www.jetbrains.com/help/idea/eslint.html 1.In the embedded Terminal (⌥F12), type one of the following commands: [代码]npm install eslint -g for global installation. npm install eslint --save-dev to install ESLint as a development dependency. [代码] 2.Optionally, install additional plugins, for example, eslint-plugin-react to lint React applications. [代码]安装完毕后,接下来新建一个配置文件.eslintrc.js,或者使用如下的命令行来自动生成。`eslint --init` [代码] AlloyTeam ESLint 规则 AlloyTeam ESLint 规则不仅是一套科学的 ESLint 配置规范,而且也是你配置个性化 ESLint 规则的最佳参考。https://github.com/AlloyTeam/eslint-config-alloy 个人觉得用这个足够了,如果团队有其他配置,在此基础上添加相关配置。 Prettier AlloyTeam 文档有写,v3 版本去掉了所有样式相关的规则(比如缩进、分号等),这些规则应该交给更专业的 Prettier 来处理。然后了解了下 prettier(webstrom以及内置,IDEA单独安装插件)。官网:https://prettier.io/ prettier安装 [代码]npm install --save-dev --save-exact prettier # or globally npm install --global prettier [代码] AlloyTeam 使用的 prettier.config.js 配置,仅供参考 [代码]// prettier.config.js or .prettierrc.js module.exports = { // 一行最多 100 字符 printWidth: 100, // 使用 4 个空格缩进 tabWidth: 4, // 不使用缩进符,而使用空格 useTabs: false, // 行尾需要有分号 semi: true, // 使用单引号 singleQuote: true, // 对象的 key 仅在必要时用引号 quoteProps: 'as-needed', // jsx 不使用单引号,而使用双引号 jsxSingleQuote: false, // 末尾不需要逗号 trailingComma: 'none', // 大括号内的首尾需要空格 bracketSpacing: true, // jsx 标签的反尖括号需要换行 jsxBracketSameLine: false, // 箭头函数,只有一个参数的时候,也需要括号 arrowParens: 'always', // 每个文件格式化的范围是文件的全部内容 rangeStart: 0, rangeEnd: Infinity, // 不需要写文件开头的 @prettier requirePragma: false, // 不需要自动在文件开头插入 @prettier insertPragma: false, // 使用默认的折行标准 proseWrap: 'preserve', // 根据显示样式决定 html 要不要折行 htmlWhitespaceSensitivity: 'css', // 换行符使用 lf endOfLine: 'lf' }; [代码] IDEA 使用 在设置中简单设置,路径 Languages and Frameworks | JavaScript | Prettier. 可以右键选择 Prettier with Prettier 可以[代码]⇧⌘A[代码] 从查找操作(Actions)中选择 Prettier with Prettier file Watching 每次都要快捷键有点儿麻烦,所以配置下Watching 。 IDEA是没有这个选项装上插件后就可以参考webstrom的配置了。路径在 Preferences | Tools | File Watchers。 插件使用网址:https://www.jetbrains.com/help/idea/tutorial-file-watchers-in-product.html。 prettier配置网址:https://prettier.io/docs/en/webstorm.html 常见错误:prettier error=13, Permission denied,解决办法全局安装Prettier,填入[代码]/usr/local/bin/prettier[代码](mac).可正常使用 实现效果编辑完,编辑器自动保存,触发自动保存。 错误类型 代码风格方面的 quotes: 必须使用单引号,而不是双引号表示字符串. prefer-template: 使用模板字符串操作字符串拼接. key-spacing: 关键字之间的空格样式问题. brace-style: 大括号的样式 indent:缩进 2 两个空格. 代码质量方面的 prefer-const: 使用 const 关键字表示所有不会被修改的变量. no-var: 不使用 var 表示变量,而是使用 let. no-unused-vars:不允许定义了但是未使用的变量. eqeqeq: 使用全等 === 比较 no-undef: 变量未定义 小建议:全局配置安装,之后的项目里只需要建一个配置文件就可以了,不用到处npm install。代码格式化是一部分,别忘了加上合适的注释,搭配使用才完美 [图片] 相关链接: 梳理前端开发使用eslint和prettier来检查和格式化代码问题:https://www.jianshu.com/p/d38e7e3b80cb eslint 产生的各种错误:https://blog.tomyail.com/how-to-lint-legacy-jscode-in-30min/
2019-12-13 - 如果不使用wepy,小程序开发应该怎么引入esLint
因为项目有多个人在开发,代码比较乱.现在想用eslint来规范下,但是似乎直接开发不支持npm,我看了下eslint的包大该有2M多,我们项目代码也已经有2M多了,还会继续增加.感觉好像不能直接把包放到代码里.我想问一下有什么办法可以来用esLint来规范小程序的开发代码呢
2018-01-11 - 成语答题小程序
接前文 1)成语答题小程序云开发记录系列一? https://developers.weixin.qq.com/community/develop/article/doc/000a64f2dbcff012591b0361356813 输入图片说明 最近从答题小程序切入成语答题这个细分领域,发现一些有意思的事情,今天跟大家谈谈 本文背景 说起答题小程序,可能更多的是面向行业,面向某部分人群,但是作为成语答题,这种天然带娱乐性质的,老少皆宜,不区分人群,任何人都可能会感兴趣来两把, 所以当你你把一个成语答题小程序分享给其他微信好友的时候,不会给人打来烦恼,反而有可能因为你的分享,而喜欢上这个答题活动 今天主要谈谈在开发和运营过程中的一些实际运营操作 本文内容 我大概从以下几个方面来讲 1)关于兑换 2)关于盈利 关于兑换,就要提到金币、红包、商品 这块我参考了很多小程序,也跟很多成语答题的运营同学有过沟通,主要的兑换粗略是这样 金币主要是用来答题的,用户进来默认会给一些金币,比如默认是5000金币; 金币获取的场景 1)用户授权,会默认5000金币; 2)用户邀请好友,邀请一个好友,会增加一定金币;(此处是否合规有待考虑) 3)观看激励式视频广告可获取一定金币(此处是否合规有待考虑) 金币消费的场景 1)答题消费,用户每作答一次,减少100金币,不管有没有正确 金币不能兑换红包,红包是另外单独的维度,在答题过程中,每次答题挑战失败,都会有一定金额的红包,这个红包金额的发放策略非常讲究任性,可以参考拼多多的砍价模式 我刚开始玩的时候,以为我做对的越多,红包给的也越多,后面跟几个运营同学聊天才发现,完全不是这么一回事,我贴个图 通过上图我们不难看出 这个红包奖励金额跟用户当前余额有关系,余额越少奖励红包越大,当用户接近兑换商品的临界值时,基本是一分,二分的递增 具体的红包获取界面截图如下所示 关于兑换 当红包累计到一定金额,可以通过兑换商品来消费掉这些红包 关于广告 对于成语答题赚这种小程序,开通流量主,通过广告变现是唯一一种盈利方式,具体几个广告位我来介绍下 首页可以安排底部banner和一个插屏广告,具体截图如下所示 [图片] 广告位置2-答题页广告位,此处建议安排视频广告,因为在这个答题的页面,往往用户持续的时间比较长,这个地方非常适合视频广告这种长时间展示的。 [图片] 3)广告位置3,商品兑换页面,此处安排一般的banner广告和插屏广告 [图片] 4)广告位置4,免费得金币模块,此处安排普通视频广告和激励式视频广告 [图片] 广告安排 最近总结了下几点广告设置的小经验 1、用户停留时间长的页面,安排视频广告 2、用户刚需页面,可安排插屏广告,由于插屏广告是可以带来操作障碍的,非刚需页面尽量不要安排这种广告类型。 [图片] 广告收益 近期运营广告收益截图如下 1 本文总结 本文通过自身最近几天的体验,总结了成语答题赚小程序的一些运营策略,希望通过这种分享,认识更多的朋友 经过一番针对广告类型的踩坑操作(此处省略一万字…😢),最终得到正解(此处感谢小伙伴的雪中送炭🙏) 本文备注 该成语答题小程序由其他几个小程序输血,包括抽奖小程序、激活码小程序以及矩阵内的其他几个小程序,都有对应的入口引导到成语答题小程
2020-11-02 - 答题小程序搭建系列一
答题小程序搭建系列一 后台界面 截图一 [图片] 截图二 [图片] 截图三 [图片] 截图四 [图片] 截图五 [图片] 截图六 [图片] 截图七 [图片] 截图八 [图片] 截图九 [图片] 截图十 [图片] 截图十一 [图片] 截图十二 [图片] 数据设计 [图片] 截图一 [图片] 截图二 [图片] 截图三 [图片] 截图四 [图片] 更新记录 答题小程序 2020-10-12 新增对主观题的支持,具体包括以下几种类型 [代码] "单选"=>"01", "多选"=>"02", "判断"=>"03", "填空"=>"04", "简答"=>"05", "论述"=>"06", "名词解释"=>"07" [代码] 其他
2020-10-12 - 小程序真机长按超N秒会触发touchcancel事件
具体现象:开发工具中一直正常。真机调试模式下也一直正常。只有在真机环境下才有此问题。 真机环境(非扫码调试):第一次长按时间如果时间较短(比如1秒内),后续长按会则正常。 但只要有一次长按时间较长(比如超过3秒),此次长按能正常触发touchend及tap事件,而后续的所有长按(不管是否超过3秒)都会在longtap后自动触发touchcancel事件(确保手没有移动)。 机型:小米8
2019-12-09 - wxParse 富文本解析 style为undefined
请求后台接口,返回一段html代码,使用wxParse插件解析,出现style为undefined,查看插件源码,没有解决,有知道怎么解决或建议和想法的吗? [图片]
2019-02-22 - 小程序富文本能力的深入研究与应用
前言 在开发小程序的过程中,很多时候会需要使用富文本内容,然而现有的方案都有着或多或少的缺陷,如何更好的显示富文本将是一个值得继续探索的问题。 [图片] 现有方案 WxParse [代码]WxParse[代码] 作为一个应用最为应用最广泛的富文本插件,在很多时候是大家的首选,但其也明显的存在许多问题。 格式不正确时标签会被原样显示 很多人可能都见到过这种情况,当标签里的内容出现格式上的错误(如冒号不匹配等),在[代码]WxParse[代码]中都会被认为是文本内容而原样输出,例如:[代码]<span style="font-family:"宋体"">Hello World!</span> [代码] 这是由于[代码]WxParse[代码]的解析脚本中,是通过正则匹配的方式进行解析,一旦格式不正确,就将导致无法匹配而被直接认为是文本[代码]//WxParse的匹配模式 var startTag = /^<([-A-Za-z0-9_]+)((?:\s+[a-zA-Z_:][-a-zA-Z0-9_:.]*(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)>/, endTag = /^<\/([-A-Za-z0-9_]+)[^>]*>/, attr = /([a-zA-Z_:][-a-zA-Z0-9_:.]*)(?:\s*=\s*(?:(?:"((?:\\.|[^"])*)")|(?:'((?:\\.|[^'])*)')|([^>\s]+)))?/g; [代码] 然而,[代码]html[代码] 对于格式的要求并不严格,一些诸如冒号不匹配之类的问题是可以被浏览器接受的,因此需要在解析脚本的层面上提高容错性。 超过限定层数时无法显示 这也是一个让许多人十分苦恼的问题,[代码]WxParse[代码] 通过 [代码]template[代码] 迭代的方式进行显示,当节点的层数大于设定的 [代码]template[代码] 数时就会无法显示,自行增加过多的层数又会大大增加空间大小,因此对于 [代码]wxml[代码] 的渲染方式也需要改进。 对于表格、列表等复杂内容支持性差 [代码]WxParse[代码] 对于 [代码]table[代码]、[代码]ol[代码]、[代码]ul[代码] 等支持性较差,类似于表格单元格合并,有序列表,多层列表等都无法渲染 rich-text [代码]rich-text[代码] 组件作为官方的富文本组件,也是很多人选择的方案,但也存在着一些不足之处 一些常用标签不支持 [代码]rich-text[代码] 支持的标签较少,一些常用的标签(比如 [代码]section[代码])等都不支持,导致其很难直接用于显示富文本内容 ps:最新的 2.7.1 基础库已经增加支持了许多语义化标签,但还是要考虑低版本兼容问题 不能实现图片和链接的点击 [代码]rich-text[代码] 组件中会屏蔽所有结点事件,这导致无法实现图片点击预览,链接点击效果等操作,较影响体验 不支持音视频 音频和视频作为富文本的重要内容,在 [代码]rich-text[代码] 中却不被支持,这也严重影响了使用体验 共同问题 不支持解析 [代码]style[代码] 标签 现有的方案中都不支持对 [代码]style[代码] 标签中的内容进行解析和匹配,这将导致一些标签样式的不正确 [图片] 方案构建 因此要解决上述问题,就得构建一个新的方案来实现 渲染方式 对于该节点下没有图片、视频、链接等的,直接使用 [代码]rich-text[代码] 显示(可以减少标签数,提高渲染效果),否则则继续进行深入迭代,例如: [图片] 对于迭代的方式,有以下两种方案: 方案一 像 [代码]WxParse[代码] 那样通过 [代码]template[代码] 进行迭代,对于小于 20 层的内容,通过 [代码]template[代码] 迭代的方式进行显示,超过 20 层时,用 [代码]rich-text[代码] 组件兜底,避免无法显示,这也是一开始采用的方案[代码]<!--超过20层直接使用rich-text--> <template name='rich-text-floor20'> <block wx:for='{{nodes}}' wx:key> <rich-text nodes="{{item}}" /> </block> </template> [代码] 方案二 添加一个辅助组件 [代码]trees[代码],通过组件递归的方式显示,该方式实现了没有层数的限制,且避免了多个重复性的 [代码]template[代码] 占用空间,也是最终采取的方案[代码]<!--继续递归--> <trees wx:else id="node" class="{{item.name}}" style="{{item.attrs.style}}" nodes="{{item.children}}" controls="{{controls}}" /> [代码] 解析脚本 从 [代码]htmlparser2[代码] 包进行改写,其通过状态机的方式取代了正则匹配,有效的解决了容错性问题,且大大提升了解析效率 [代码]//不同状态各通过一个函数进行判断和状态跳转 for (; this._index < this._buffer.length; this._index++) this[this._state](this._buffer[this._index]); [代码] 兼容 [代码]rich-text[代码] 为了解析结果能同时在 [代码]rich-text[代码] 组件上显示,需要对一些 [代码]rich-text[代码]不支持的组件进行转换[代码]//以u标签为例 case 'u': name = 'span'; attrs.style = 'text-decoration:underline;' + attrs.style; break; [代码] 适配渲染需要 在渲染过程中,需要对节点下含有图片、视频、链接等不能由 [代码]rich-text[代码]直接显示的节点继续迭代,否则直接使用 [代码]rich-text[代码] 组件显示;因此需要在解析过程中进行标记,遇到 [代码]img[代码]、[代码]video[代码]、[代码]a[代码] 等标签时,对其所有上级节点设置一个 [代码]continue[代码] 属性用于区分[代码]case 'a': attrs.style = 'color:#366092;display:inline;word-break:break-all;overflow:auto;' + attrs.style; element.continue = true; //冒泡:对上级节点设置continue属性 this._bubbling(); break; [代码] 处理style标签 解析方式 方案一 正则匹配[代码]var classes = style.match(/[^\{\}]+?\{([^\{\}]*?({[\s\S]*?})*)*?\}/g); [代码] 缺陷: 当 [代码]style[代码] 字符串较长时,可能出现栈溢出的问题 对于一些复杂的情况,可能出现匹配失败的问题 方案二 状态机的方式,类似于 [代码]html[代码] 字符串的处理方式,对于 [代码]css[代码] 的规则进行了调整和适配,也是目前采取的方案 匹配方式 方案一 将 [代码]style[代码] 标签解析为一个形如 [代码]{key:content}[代码] 的结构体,对于每个标签,通过访问结构体的相应属性即可知晓是否匹配成功[代码]if (this._style[name]) attrs.style += (';' + this._style[name]); if (this._style['.' + attrs.class]) attrs.style += (';' + this._style['.' + attrs.class]); if (this._style['#' + attrs.id]) attrs.style += (';' + this._style['#' + attrs.id]); [代码] 优点:匹配效率高,适合前端对于时间和空间的要求 缺点:对于多层选择器等复杂情况无法处理 因此在前端组件包中采取的是这种方式进行匹配 方案二 将 [代码]style[代码] 标签解析为一个数组,每个元素是形如 [代码]{key,list,content,index}[代码] 的结构体,主要用于多层选择器的匹配,内置了一个数组 [代码]list[代码] 存储各个层级的选择器,[代码]index[代码] 用于记录当前的层数,匹配成功时,[代码]index++[代码],匹配成功的标签出栈时,[代码]index--[代码];通过这样的方式可以匹配多层选择器等更加复杂的情况,但匹配过程比起方案一要复杂的多。 [图片] 遇到的问题 [代码]rich-text[代码] 组件整体的显示问题 在显示过程中,需要把 [代码]rich-text[代码] 作为整体的一部分,在一些情况下会出现问题,例如: [代码]Hello <rich-text nodes="<div style='display:inline-block'>World!</div>"/> [代码] 在这种情况下,虽然对 [代码]rich-text[代码] 中的顶层 [代码]div[代码] 设置了 [代码]display:inline-block[代码],但没有对 [代码]rich-text[代码] 本身进行设置的情况下,无法实现行内元素的效果,类似的还有 [代码]float[代码]、[代码]width[代码](设置为百分比时)等情况 解决方案 方案一 用一个 [代码]view[代码] 包裹在 [代码]rich-text[代码] 外面,替代最外层的标签[代码]<view style="{{item.attrs.style}}"> <rich-text nodes="{{item.children}}" /> </view> [代码] 缺陷:当该标签为 [代码]table[代码]、[代码]ol[代码] 等功能性标签时,会导致错误 方案二 对 [代码]rich-text[代码] 组件使用最外层标签的样式[代码]<rich-text nodes="{{item}}" style="{{item.attrs.style}}" /> [代码] 缺陷:当该标签的 [代码]style[代码] 中含有 [代码]margin[代码]、[代码]padding[代码] 等内容时会被缩进两次 方案三 通过 [代码]wxs[代码] 脚本将顶层标签的 [代码]display[代码]、[代码]float[代码]、[代码]width[代码] 等样式提取出来放在 [代码]rich-text[代码] 组件的 [代码]style[代码] 中,最终解决了这个问题[代码]var res = ""; var reg = getRegExp("float\s*:\s*[^;]*", "i"); if (reg.test(style)) res += reg.exec(style)[0]; reg = getRegExp("display\s*:\s*([^;]*)", "i"); if (reg.test(style)) { var info = reg.exec(style); res += (';' + info[0]); display = info[1]; } else res += (';display:' + display); reg = getRegExp("[^;\s]*width\s*:\s*[^;]*", "ig"); var width = reg.exec(style); while (width) { res += (';' + width[0]); width = reg.exec(style); } return res; [代码] 图片显示的问题 在 [代码]html[代码] 中,若 [代码]img[代码] 标签没有设置宽高,则会按照原大小显示;设置了宽或高,则按比例进行缩放;同时设置了宽高,则按设置的宽高进行显示;在小程序中,若通过 [代码]image[代码] 组件模拟,需要通过 [代码]bindload[代码] 来获取图片宽高,再进行 [代码]setData[代码],当图片数量较大时,会大大降低性能;另外,许多图片的宽度会超出屏幕宽度,需要加以限制 解决方案 用 [代码]rich-text[代码] 中的 [代码]img[代码] 替代 [代码]image[代码] 组件,实现更加贴近 [代码]html[代码] 的方式 ;对 [代码]img[代码] 组件设置默认的效果 [代码]max-width:100%;[代码] 视频显示的问题 当一个页面出现过多的视频时,同时进行加载可能导致页面卡死 解决方案 在解析过程中进行计数,若视频数量超过3个,则用一个 [代码]wxss[代码] 绘制的图片替代 [代码]video[代码] 组件,当受到点击时,再切换到 [代码]video[代码] 组件并设置 [代码]autoplay[代码] 以模拟正常效果,实现了一个类似懒加载的功能 [代码]<!--视频--> <view wx:if="{{item.attrs.id>'media3'&&!controls[item.attrs.id].play}}" class="video" data-id="{{item.attrs.id}}" bindtap="_loadVideo"> <view class="triangle_border_right"></view> </view> <video wx:else src='{{controls[item.attrs.id]?item.attrs.source[controls[item.attrs.id].index]:item.attrs.src}}' id="{{item.attrs.id}}" autoplay="{{item.attrs.autoplay||controls[item.attrs.id].play}}" /> [代码] 文本复制的问题 小程序中只有 [代码]text[代码] 组件可以通过设置 [代码]selectable[代码] 属性来实现长按复制,在富文本组件中实现这一功能就存在困难 解决方案 在顶层标签上加上 [代码]user-select:text;-webkit-user-select[代码] [图片] 实现更加丰富的功能 在此基础上,还可以实现更多有用的功能 自动设置页面标题 在浏览器中,会将 [代码]title[代码] 标签中的内容设置到页面标题上,在小程序中也同样可以实现这样的功能[代码]if (res.title) { wx.setNavigationBarTitle({ title: res.title }) } [代码] 多资源加载 由于平台差异,一些媒体文件在不同平台可能兼容性有差异,在浏览器中,可以通过 [代码]source[代码] 标签设置多个源,当一个源加载失败时,用下一个源进行加载和播放,在本插件中同样可以实现这样的功能[代码]errorEvent(e) { //尝试加载其他源 if (!this.data.controls[e.currentTarget.dataset.id] && e.currentTarget.dataset.source.length > 1) { this.data.controls[e.currentTarget.dataset.id] = { play: false, index: 1 } } else if (this.data.controls[e.currentTarget.dataset.id] && e.currentTarget.dataset.source.length > (this.data.controls[e.currentTarget.dataset.id].index + 1)) { this.data.controls[e.currentTarget.dataset.id].index++; } this.setData({ controls: this.data.controls }) this.triggerEvent('error', { target: e.currentTarget, message: e.detail.errMsg }, { bubbles: true, composed: true }); }, [代码] 添加加载提示 可以在组件的插槽中放入加载提示信息或动画,在加载完成后会将 [代码]slot[代码] 的内容渐隐,将富文本内容渐显,提高用户体验,避免在加载过程中一片空白。 最终效果 经过一个多月的改进,目前实现了这个功能丰富,无层数限制,渲染效果好,轻量化(30.0KB),效率高,前后端通用的富文本插件,体验小程序的用户数已经突破1k啦,欢迎使用和体验 [图片] github 地址 npm 地址 总结 以上就是我在开发这样一个富文本插件的过程大致介绍,希望对大家有所帮助;本人在校学生,水平所限,不足之处欢迎提意见啦! [图片]
2020-12-27 - 小程序跳转公众号文章
现在小程序跳转公众号文章,只能跳转已经绑定的公众号的文章嘛?外部的公众号文章可以跳转嘛?想确认下这个事,求教各位大佬。
2019-03-12 - 小程序内嵌H5页面跳转至小程序页面,再返回到H5页面不刷新问题解决方案
需求:类似常见的电商页面,从首页跳转至H5页面,在H5页面展示商品列表,点击某个商品进入小程序商品详情页 1.小程序->2.h5->3.小程序 问题是从3返回到2时,H5页面自带的监听页面可见的函数无效,如图所示: [代码]//监听页面显示隐藏 document.addEventListener(visibilitychange, this.isShow); [代码] 无法触发此类函数,类似的还有pageshow等等。 想在页面解决该问题,不通过即时通信的话,就只有两种方案 刷新webview所在的小程序页面,在onshow时更新webview 触发H5显示隐藏的监听 由于尝试了各种监听事件均无效,因此放弃了方案2,如果有什么好办法欢迎留言。 方案一具体实现: 回退可以触发onshow函数,定义一个变量,如时间戳,加在webview的url后边,url发生改变,H5页面重新加载,可以达到更新页面的目的。 [代码]onShow() { this.version = (new Date()).valueOf(); this.url = www.baidu.com+this.version; } [代码] 但是这种做法有个很严重的问题,改变url 会增加webview的history,在webview所在页面,点击后退,会返回之前的H5页面,用户可以多次点击才能回到首页。 [图片] [图片] 因此想到了在这个页面onhide的时候,移除webview组件,页面重新显示后,再增加改组件,这样就解决了这个问题。
2019-07-17 - (15)真机定位问题技巧
开发者在开发小程序的时候可能会碰到一些这样的问题: 问题1 开发者工具上看效果没问题,但是在真机上测试不行? 问题2 有用户遇到小程序功能无法使用的问题,但无法快速定位解决? 今天我们的小故事与大家分享一些真机定位的技巧,可以解决上面两个问题。 1 vConsole开发利器和远程调试功能 针对问题1,我们提供了 vConsole 开发利器和远程调试功能,可以协助开发者在定位真机上的问题。 vConsole 的有四个Tab面板,可以先看下 Log 面板,看是否有异常信息,异常类型 thirdScriptError 是框架捕捉到的开发者的代码执行的异常,可以优先处理异常信息看是否可以解决问题。Log 面板可以看到异常出现的文件和行数。 [图片] 除了异常日志,开发者还可以通过 console.log 接口在一些关键执行路径上打日志来定位问题,这些日志会呈现在 Log 面板上。 vConsole 默认是不开启的,可以通过下面2个方法来开启: 1 开发版和体验版可以点击小程序页面右上角的...按钮打开的菜单项“打开调试”来开启 vConsole。 2 正式版没有“打开调试”的菜单项,可以先通过开发版和体验版来开启 vConsole,然后再打开正式版。或者可以预埋一个隐藏操作,比如连续点击某个 Button 多次,然后调用 API 接口 wx.setEnableDebug 来打开。 vConsole 虽然强大,但在手机上查看大量的日志信息不方便,此外,vConsole 没有断点调试、无法修改样式,定位复杂问题需要花费比较多的时间。 小程序的业务逻辑运行在 AppService 层,页面渲染在 WebView 运行,并通过微信客户端通信,因此,我们想到了可以让 AppService 运行在开发者工具,页面渲染还是在手机 WebView,两者通过网络来通信,这样借助开发者工具的调试能力,就可以实现远程调试功能。 远程调试窗口通过手机客户端扫描开发者工具上生成的二维码来打开,无需像普通手机 H5 页面调试一样,需要在手机端进行一些设置。 [图片] 打开的远程调试界面和开发者工具的模拟器的调试界面很像,需要注意的是,要在 Console 里对小程序进行调试,需要将调试的上下文切换到 VM Context 1 。 [图片] 更多的远程调试的使用方法请参考使用文档。 2 意见反馈能力 对于问题2,小程序的使用反馈来自用户投诉,这种情况用户无法联系到开发者。我们遇见过有小程序功能出现问题,用户无法使用,但投诉无门的情况,而这些问题,开发者也没有途径去收集以及处理,这就导致了小程序服务质量下降,用户流失。 为此,我们开发了“意见反馈”功能,当出现问题时,开发者可以引导用户使用“意见反馈”进行反馈,并上传日志来辅助开发者定位问题。操作过程如下: 引导用户进入小程序帐号详情页面,具体可以在小程序界面点击右上角...按钮,选择关于菜单。接着在帐号详情页面点击右上角...按钮,选择意见反馈菜单进入页面。页面可以上传图片和日志,建议用户上传异常情况的截图,以及勾选允许开发者使用小程序日志选项上传日志,反馈信息越详细,越有助于定位问题。 [图片] [图片] 如果觉得上面的操作步骤太麻烦,开发者可以通过在页面 WXML 添加下面的按钮,用户点击按钮可以直接打开“意见反馈”页面。 [图片] 开发者需要定时处理用户的反馈,这样才能保证小程序的质量。开发者可以登录小程序管理后台,进入左侧菜单客服反馈,就可以看到用户的反馈内容以及下载日志来辅助定位问题。 [图片] 为了保证日志信息足够详细,开发者需要用下面的接口在代码的关键执行路径上写日志。 [图片] wx.getLogManager 接口的更详细使用请参考文档。 希望通过这些小技巧,可以帮助大家顺畅地开发小程序。
2019-04-29 - wx.requestSubscribeMessage点击的次数和收到订阅信息的推送成正比?
最近在做一个项目因为需求所以要用到推送功能,然后社区大佬和我说微信订阅开通了,我开心的去用了,试下来的确香,省去了获取formid还有7天限制等繁琐限制与步骤,但是今天测试的时候后台平凡给我报错,最后发现是wx.requestSubscribeMessage点击一次才会收到一条推送,我是大写的黑人脸问号,那我想问既然这样的话这东西点击 记住选择的操作有什么用?要是客户以为需求只是绑定了一下小程序就一直在pc端操作是不是就只能收到一条信息?或者说我应该在点击订阅的时候给写个几百次循环然后让客户定期去点一次呢?感觉很不合理啊,有没有官方的大神给我说说?
2019-11-08 - 订阅消息如果选择选择‘总是保持以上选择,"不再询问"后的设置问题
目前是选择‘总是保持以上选择,"不再询问"后,可以在设置中开启或拒绝接收,但不会再次拉起授权弹窗
2019-10-18 - 小程序订阅消息开发指南
2019年10月12日微信开放了小程序订阅消息的功能。按官方的说法,目前的模板消息在实现小程序服务闭环上存在缺陷: 1. 部分开发者在用户无预期或未进行服务的情况下发送与用户无关的消息,对用户产生了骚扰;2. 模板消息需在用户访问小程序后的 7 天内下发,不能满足部分业务的时间要求模板消息确实存在上述的硬伤,不利于小程序的用户留存和用户体验。为了解决这些问题,微信官方推出了用户订阅消息功能。我在微慕专业版上加了订阅消息的功能,并验证了这个功能。这个功能是否能都达到官方的预期,这个我感觉不那么乐观。这里我先说我的感受:目前的订阅消息还不完善,后续还有很大的优化空间。 目前,官方只开放了“一次性订阅消息”,尚未开放“长期性订阅消息”,因此我只尝试了“一次性订阅消息”。 一次性订阅消息:用于解决用户使用小程序后,后续服务环节的通知问题。用户自主订阅后,开发者可不限时间地下发一条对应的服务消息;每条消息可单独订阅或退订。 订阅消息推送位置:服务通知 订阅消息下发条件:用户自主订阅 订阅消息卡片跳转能力:点击查看详情可跳转至该小程序的页面 以下我简单说明订阅消息的开发过程和使用体验。 一.订阅消息的开发1.获取订阅消息的模板ID 在微信小程序的管理后台,在左侧“功能”菜单,选择“订阅消息”,然后点击“添加” [图片] 然后选择你需要的消息模板,并配置关键词。 [图片] 配置完成后,如下图所示。 [图片] 值得关注的是,在配置好的模板详情页面里的“详细内容”很重要,这个就是开发订阅消息时需要遵循的消息格式,这个格式和模板消息有细微的差别 根据微慕小程序的需要,我选用了“新的评论提醒”和“内容更新提醒”这两个消息模版。前者用于提醒发表话题或文章的作者,有新的话题或文章评论,增强作者与读者之间的交流互动;后者是提醒订阅用户,小程序有新的文章发布,引导用户回归小程序。 订阅消息申请模板的时候,需要选择所属类目,只能选择当前小程序相关的类目模板,对于模板消息不需要选择对应类目。如果删除小程序类目,就会把订阅消息模板一起删除。因此删除类目要小心谨慎。 [图片] 2.触发用户订阅,获取下发的权限 触发用户订阅,微信小程序提供的api是: [代码]wx.requestSubscribeMessage[代码],用户发生点击行为或者发起支付回调后,才可以调起订阅消息界面。 注意:微信小程序开发工具尚不支持此功能,在开发工具触发订阅的api,会提示: requestSubscribeMessage:fail 开发者工具暂时不支持此 API 调试,请使用真机进行开发 调用api的代码示例如下: [代码]wx.requestSubscribeMessage({[代码] [代码]tmplIds: ["模板A","模板B"],[代码] [代码]success: function (res) {[代码] [代码]//成功[代码] [代码]},[代码] [代码]fail(err) {[代码] [代码]//失败[代码] [代码]console.error(err);[代码] [代码]}[代码] [代码]})[代码] wx.requestSubscribeMessage(Object object) 的回调函数[代码]object.success [代码]参数有两个:errMsg和TEMPLATE_ID; 接口调用成功时errMsg值为’requestSubscribeMessage:ok’。TEMPLATE_ID是动态的键,即模板id,值包括’accept’、’reject’、’ban’。’accept’表示用户同意订阅该条id对应的模板消息,’reject’表示用户拒绝订阅该条id对应的模板消息,’ban’表示已被后台封禁。例如 { errMsg: “requestSubscribeMessage:ok”, zun-LzcQyW-edafCVvzPkK4de2Rllr1fFpw2A_x0oXE: “accept”} 表示用户同意订阅zun-LzcQyW-edafCVvzPkK4de2Rllr1fFpw2A_x0oXE这条消息。 个人觉得这个动态键不是特别合理,代码处理起来有些麻烦,如果改成静态键的json格式比较方便处理,例如: [代码]{[代码] [代码] errMsg:"requestSubscribeMessage:ok",[代码] [代码] result: [[代码] [代码] { templateId:"zun-LzcQyW-edafCVvzPkK4de2Rllr1fFpw2A_x0oXE",[代码] [代码]status:"accept"[代码] [代码]}[代码] [代码] ][代码] [代码]}[代码] 在手机上调用此api方法会调出订阅消息的界面,如下图所示: [图片] 关于这个订阅消息的授权有几点要注意: 1) 在确认提示框里,如果用户选择“取消”表示拒绝(取消)订阅消息,选择“允许”表示用户订阅一次消息。 2) 如果用户不勾选“总是保持以上选择,不再询问”,那么每次用户触发都会弹出提示框。 3) 如果用户勾选“总是保持以上选择,不再询问”,那么将再也不会唤起这个对话框。同时,如果选择“取消”,那么以后每次调用这个api的时候,都会自动拒绝;如果选择“允许”,那么以后每次调用此api,都会自动允许授权。 目前小程序没有提供获取用户是否授权订阅消息的方法。通过wx.openSetting 方法无法获取用户是否授权消息订阅的信息,scope 列表没有订阅消息的内容。 如果想从自动拒绝转换到自动自动运行,需要打开小程序的设置去配置。设置方法:点击小程序右上角的三个点,打开如下对话框 [图片] 然后选择“设置”,在设置项里选择“订阅消息” [图片] [图片] 4)对于同一种消息,用户可以订阅多次,订阅多少次,就会收到多少次订阅消息,这个订阅次数是否有上限,官方没有说明,初步判断是不限的。但是,微信不会提供订阅的次数,因此需要在小程序的后端服务里存储用户订阅的次数。因此,我在微慕小程序专业版里,提供了一个给用户多次订阅的设置,并记录用户订阅的次数。 [图片] 如果用户需要某个消息服务,可以订阅多次,当然也可以在点击“订阅”的对话框里选择“取消”,“取消”一次也就减少一次订阅。 5)对于支付的场景,也需要用户确认是否订阅,这个我觉得不合理,支付后给用户一个订单推送消息应该是刚性需求,不需要再询问一遍用户是否订阅。 2.调用接口下发订阅消息 订阅消息下发的接口是小程序后台服务端调用:subscribeMessage.send,此方法类似下发模板消息的方法,详细调用说明见参考官方的链接: https://developers.weixin.qq.com/miniprogram/dev/api-backend/open-api/subscribe-message/subscribeMessage.send.html 订阅消息的下发接口方法和模板消息稍有不同, 模板消息的json格式如下 [代码]"data": {[代码] [代码]"keyword1": {[代码] [代码]"value": "内容1",[代码] [代码]"color": "#000"[代码] [代码]},[代码] [代码]"keyword2": {[代码] [代码]"value": "内容2",[代码] [代码]"color": "#000"[代码] [代码]}[代码] [代码]}[代码] 而订阅消息的json格式如下: [代码]"data": {[代码] [代码]"thing1": {[代码] [代码]"value": "内容"[代码] [代码]},[代码] [代码]"number2": {[代码] [代码]"value": 20[代码] [代码]}[代码] 订阅消息的字段key是和数据类型有关,value的参数需要严格按照设置的类型提交,如果不按类型提交,会导致发送失败。同时如果是文本型的内容,字数也有限制,超过限制也会发送失败,但具体字数是多少,官方没有给出,同时中英文混合计算的长度也有差异,据我目前测试25个中文字符是可以的。希望官方能给出具体的字符长度限制的明确数字。 如果调用下发的次数大于用户的订阅次数,调用接口下发订阅消息会返回失败。报如下错误 [图片] 二.订阅消息使用心得1.订阅消息虽然把订阅的授权的交给了用户,但是也增加了用户使用难度,同时,一次性订阅只能收到一次,操作起来比较繁琐,如果不是刚需用户可能会首次就拒绝了这个服务,要想重新获取授权,需要用户自己打开小程序设置里去配置,颇为麻烦,小程序没有提供更简便的方法去唤起。 2.小程序的服务商为了获得更多给用户发送订阅消息的次数,肯定会想方设法去埋点引诱用户去点击订阅,这种诱导估计也是违规。 3.用户使用门槛和学习比较高,比如某个预约的服务,原来的场景是用户只要有提交表单,小程序就可以推送消息给用户,但是现在需要用户主动去订阅,无形中多了一步,如果用户不熟悉订阅消息或者直接点了“取消”,小程序就没法通知到用户了,用户可能因此错失服务,对商家和用户都是损失。 4.微信小程序将采用订阅消息,并逐步取消模板消息,虽然微信官方试图在方便用户和不打扰用户这两种选择里去寻求平衡,但订阅消息目前的模式恐怕无法达到这个期望,至少在我看来,无论对小程序的服务商,还是小程序的用户,都感到不方便。 update:2020年5月18日,日前订阅消息已经支持微信小程序开发工具。
2020-05-18 - 小程序订阅消息,现在点击总是授权之后,还能再次唤起订阅的弹窗吗?
wx.requestSubscribeMessage 在弹起授权弹窗之后,用户点击了总是选择该选项,不再询问后。 之后不再会唤起授权弹窗,能否设置重新唤起授权弹窗呢?
2019-10-17 - 小程序实现复制粘贴功能
小程序长按事件调用剪贴板实现了复制 [代码]longTap: [代码][代码]function[代码] [代码](e) {[代码][代码] [代码][代码]wx.setClipboardData({[代码][代码] [代码][代码]data: [代码][代码]this[代码][代码].data.ClipBoardText,[代码][代码] [代码][代码]success: [代码][代码]function[代码] [代码](res) {[代码][代码] [代码][代码]wx.getClipboardData({[代码][代码] [代码][代码]success: [代码][代码]function[代码] [代码](res) {[代码][代码] [代码][代码]console.log(res.data) [代码][代码]// data[代码][代码] [代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]},[代码]那我成功复制了内容,该怎样实现粘贴功能呢?求教谢谢
2019-04-03 - 「笔记」订阅消息体验踩坑
前言 10月12日夜晚社区发了公告小程序模板消息能力调整通知,正式发布了 一次性订阅消息 这一能力,所以第一时间进行了体验。 本文主要是补充一下官方未提供的使用方法,和使用中与模板消息用法的不同。 文档地址 https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/subscribe-message.html 使用方法 [代码]wx.requestSubscribeMessage({ tmplIds: ["模板id1","模板id2"], success: function (res) { //成功 }, fail(err) { //失败 console.error(err); } }) [代码] 第一个坑 如果不勾选红色方框内的内容,用户每次触发订阅消息功能都会弹出授权窗口,如果用户勾选了则不会出现弹窗。 [图片] 第二个坑 目前开发者工具(v1.02.191012)不支持调试,只能通过真机调试。 [图片] 第三个坑 微信不会为开发者保存订阅次数,需要自己在后台记录用户触发的次数。 超过次数调用接口下发订阅消息会返回失败。 [图片] 第四个坑 发送模板格式和原来的模板消息格式不一致,特别是data内的内容,订阅消息的字段key是和数据类型有关,value的参数需要严格按照设置的类型提交,具体使用参考后台的模板详情。 模板消息的格式: [代码]"data": { "keyword1": { "value": "内容", "color": "#000" }, "keyword2": { "value": "内容", "color": "#000" } } [代码] 订阅消息的格式: [代码]"data": { "thing1": { "value": "内容" }, "number2": { "value": 20 } [代码] 第五个坑 订阅消息申请模板的时候,需要选择所属类目,而且只能是自己小程序相关类目,模板消息是不需要选择对应类目的。 如果删除小程序类目,则会把订阅消息模板一起删除,需谨慎操作。 [图片] 第六个坑 长期订阅消息只针对特定行业开放,所以普通开发者并无法使用。 结束 暂时就先总结这些,有其它坑再补充。
2019-10-13 - es6支持错误regeneratorRuntime is not defined
Promise现在可以使用了,但是yield 关键字被转码后,是这样的: var t1 = regeneratorRuntime.mark(function t1() { var l; return regeneratorRuntime.wrap(function t1$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: _context.next = 2; return t; case 2: l = _context.sent; console.log(l); case 4: case 'end': return _context.stop(); } } }, t1, this); }); 代码执行报错: WAService.js:3 thirdScriptError regeneratorRuntime is not defined;at "pages/index/index" page lifeCycleMethod onLoad function ReferenceError: regeneratorRuntime is not defined 开发工具和手机上都这样
2017-04-05 - 小程序 web-view 调用 jssdk 63002,invalid signature?
经过多次验证,两种情况操作,其中一种则会报错: 第一种情况,从小程序直接跳转到h5页面后,wx.config 就会报错:config:fail,Error: 系统错误,错误码:63002,invalid signature [20191018 11:27:34][wxb6f67e04f6d130a2] 第二种情况,在小程序里我先进入到别的h5页面,然后通过点击url超链接方式进入到同样的页面,则成功 这个是不是官方小程序BUG , 求关注,求帮忙。。。。
2019-10-18 - web-view首次进入h5详情页,调用jssdk注册始终签名错误?@官方能否给个解答?
主要问题描述: web-view, h5页面调用wx.config,始终报错:config:fail,Error: 系统错误,错误码:63002,invalid signature 这个问题困扰好几天了, 我的项目是使用原生小程序作为首页展示, 点击后进入到商品详情用的是我们自己的h5页面,在商品图片预览时采用jssdk的wx.previewimage注入配置信息报错 经过我多次调试,可以明确,并非是我的配置注册问题,同样的h5页面,从小程序跳转过去就注册失败, 而在项目里通过h5的超链接进入其他商品详情,都是注册成功的, 签名算法,url环节 我能验证的都验证了,均是正确 所以真的不知道问题是在哪了,求助官方及大师们 以下代码片段,进到商品详情后,在下面的商品列表随便进哪个商品都是注册成功的,就首次从小程序到h5页注册失败. https://developers.weixin.qq.com/s/GnisfgmU72cW appid: wxb6f67e04f6d130a2 PS:搜到个类似的问题2018年的,尚未有结果,不知道是不是一个问题(小程序web-view嵌套h5调用jssdk签名错误? - 微信开放社区 https://developers.weixin.qq.com/community/develop/doc/0004c2008bc0b059cac78cdda56800)
2019-10-20 - 微信公众号开发调用jssdk,配置时总是 invalid signature ,提示非法签名?
首先贴上必要信息: 开发者ID(AppID):wx3806a4c78e7bf173 原始ID:gh_2314fcde77a4 [图片] [图片] 以上是公众号本身的配置 接下来放一下,我这边的config代码 [代码]const modeList = [[代码][代码] [代码][代码]'updateAppMessageShareData'[代码][代码],[代码][代码] [代码][代码]'updateTimelineShareData'[代码][代码],[代码][代码] [代码][代码]'onMenuShareAppMessage'[代码][代码],[代码][代码] [代码][代码]'onMenuShareTimeline'[代码][代码],[代码][代码]];[代码] [代码]const initConfig = async (res) => {[代码][代码] [代码][代码]const options = {[代码][代码] [代码][代码]debug: !isProd,[代码][代码] [代码][代码]appId: res.appId, [代码][代码]// 必填,公众号的唯一标识[代码][代码] [代码][代码]timestamp: res.timestamp, [代码][代码]// 必填,生成签名的时间戳[代码][代码] [代码][代码]nonceStr: res.nonceStr, [代码][代码]// 必填,生成签名的随机串[代码][代码] [代码][代码]signature: res.signature, [代码][代码]// 必填,签名,见附录1[代码][代码] [代码][代码]jsApiList: modeList,[代码][代码] [代码][代码]}[代码][代码] [代码][代码]console.log([代码][代码]'config options::----------------------------'[代码][代码], options);[代码][代码] [代码][代码]wx.config(options);[代码][代码] [代码][代码]// ready[代码][代码] [代码][代码]wx.ready(() => {[代码][代码] [代码][代码]console.log([代码][代码]'config success::----------------------------'[代码][代码]);[代码][代码] [代码][代码]// 分享到朋友圈[代码][代码] [代码][代码]wx.updateTimelineShareData({[代码][代码] [代码][代码]title: [代码][代码]''[代码][代码], [代码][代码]// 分享标题[代码][代码] [代码][代码]link: [代码][代码]''[代码][代码], // 分享链接[代码][代码] [代码][代码]imgUrl: [代码][代码]''[代码][代码], [代码][代码]// 分享图标[代码][代码] [代码][代码]success: [代码][代码]function[代码] [代码]() {[代码][代码] [代码][代码]// 用户确认分享后执行的回调函数[代码][代码] [代码][代码]},[代码][代码] [代码][代码]cancel: [代码][代码]function[代码] [代码]() {[代码][代码] [代码][代码]// 用户取消分享后执行的回调函数[代码][代码] [代码][代码]},[代码][代码] [代码][代码]});[代码][代码] [代码][代码]// 分享给朋友[代码][代码] [代码][代码]wx.updateAppMessageShareData({[代码][代码] [代码][代码]title: [代码][代码]''[代码][代码], [代码][代码]// 分享标题[代码][代码] [代码][代码]link: [代码][代码]''[代码][代码], // 分享链接[代码][代码] [代码][代码]imgUrl: [代码][代码]''[代码][代码], [代码][代码]// 分享图标[代码][代码] [代码][代码]desc: [代码][代码]''[代码][代码], [代码][代码]// 分享描述[代码][代码] [代码][代码]success: [代码][代码]function[代码] [代码]() {[代码][代码] [代码][代码]// 用户确认分享后执行的回调函数[代码][代码] [代码][代码]},[代码][代码] [代码][代码]cancel: [代码][代码]function[代码] [代码]() {[代码][代码] [代码][代码]// 用户取消分享后执行的回调函数[代码][代码] [代码][代码]},[代码][代码] [代码][代码]});[代码][代码] [代码][代码]})[代码][代码] [代码][代码]// error[代码][代码] [代码][代码]wx.error((res) => {[代码][代码] [代码][代码]console.log([代码][代码]'catch config error :: =============== ::'[代码][代码], res);[代码][代码] [代码][代码]})[代码][代码]}[代码]以上代码执行之后出现如下问题 [代码]{[代码][代码] errMsg: [代码][代码]"config:fail,Error: 系统错误,错误码:63002,invalid signature [20190919 17:06:41][]"[代码][代码] jsApiList: [][代码][代码]}[代码]差了错误代码之后,发现是非法签名,好了,我先去了https://mp.weixin.qq.com/debug/cgi-bin/sandbox?t=jsapisign这个校验网站校验了,signature 没有问题两边一致,但是config一直提示非法。 然后我去了https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/JS-SDK.html这边看常见解决办法。试了个遍。然而并没有效果,另外,恕我直言这个文档真实辣鸡。可能你们的核心已经是小程序了,但是小程序的BUG也一堆没修。 [图片] 然后吐槽一下,这个错误码的返回,之前有返回过config:fail,Error: 系统错误,错误码:63002,invalid signature [20190919 17:06:41][undefind] 这样的格式,还不容易变成 config:fail,Error: 系统错误,错误码:63002,invalid signature [20190919 17:06:41][] 这样我也不知道啥问题,就这几个参数,咱写写明白行不,URL错就提示URL错,就一个签名错误???wtf,这里太笼统了! 好了说到URL错,好网上说99%的错误都是URL导致的,好的尝试了各种办法,并没有什么用,已经调试了不知道几天了,最后只能哀求官方帮我解决一下,帮我看看问题到底在哪里,头发都要秃了。 我用的是SPA (vue) HASH模式(https://xxx.com/#/)取得地址为 https://xxx.com/ 单凡我有一点办法都不会来这请教 综上,请赐教!求快速反馈! 顺便吐槽,标题为什么一定要问号,我就是个陈述句,阐述问题而已!
2019-09-19 - 如何实现小程序的强制更新
大家都知道小程序提交审核发布以后是不会马上更新版本的,用户需要下次使用才会更新到新的版本,这就是冷更新。 那么如果要做到及时生效怎么办呢?这时候就要做处理了,将下面的代码添加到app.js,提交审核,发布就会生效了 [代码]onLaunch: [代码][代码]function[代码] [代码](options) {[代码] [代码] [代码][代码]this[代码][代码].autoUpdate()[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]autoUpdate: [代码][代码]function[代码] [代码]() {[代码] [代码] [代码][代码]var[代码] [代码]self = [代码][代码]this[代码] [代码] [代码][代码]// 获取小程序更新机制兼容[代码] [代码] [代码][代码]if[代码] [代码](wx.canIUse([代码][代码]'getUpdateManager'[代码][代码])) {[代码] [代码] [代码][代码]const updateManager = wx.getUpdateManager()[代码] [代码] [代码][代码]//1. 检查小程序是否有新版本发布[代码] [代码] [代码][代码]updateManager.onCheckForUpdate([代码][代码]function[代码] [代码](res) {[代码] [代码] [代码][代码]// 请求完新版本信息的回调[代码] [代码] [代码][代码]if[代码] [代码](res.hasUpdate) {[代码] [代码] [代码][代码]//检测到新版本,需要更新,给出提示[代码] [代码] [代码][代码]wx.showModal({[代码] [代码] [代码][代码]title: [代码][代码]'更新提示'[代码][代码],[代码] [代码] [代码][代码]content: [代码][代码]'检测到新版本,是否下载新版本并重启小程序?'[代码][代码],[代码] [代码] [代码][代码]success: [代码][代码]function[代码] [代码](res) {[代码] [代码] [代码][代码]if[代码] [代码](res.confirm) {[代码] [代码] [代码][代码]//2. 用户确定下载更新小程序,小程序下载及更新静默进行[代码] [代码] [代码][代码]self.downLoadAndUpdate(updateManager)[代码] [代码] [代码][代码]} [代码][代码]else[代码] [代码]if[代码] [代码](res.cancel) {[代码] [代码] [代码][代码]//用户点击取消按钮的处理,如果需要强制更新,则给出二次弹窗,如果不需要,则这里的代码都可以删掉了[代码] [代码] [代码][代码]wx.showModal({[代码] [代码] [代码][代码]title: [代码][代码]'温馨提示'[代码][代码],[代码] [代码] [代码][代码]content: [代码][代码]'本次版本更新涉及到新的功能添加,旧版本可能无法正常访问哦'[代码][代码],[代码] [代码] [代码][代码]showCancel: [代码][代码]false[代码][代码],[代码][代码]//隐藏取消按钮[代码] [代码] [代码][代码]confirmText: [代码][代码]"确定更新"[代码][代码],[代码][代码]//只保留确定更新按钮[代码] [代码] [代码][代码]success: [代码][代码]function[代码] [代码](res) {[代码] [代码] [代码][代码]if[代码] [代码](res.confirm) {[代码] [代码] [代码][代码]//下载新版本,并重新应用[代码] [代码] [代码][代码]self.downLoadAndUpdate(updateManager)[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]} [代码][代码]else[代码] [代码]{[代码] [代码] [代码][代码]// 如果希望用户在最新版本的客户端上体验您的小程序,可以这样子提示[代码] [代码] [代码][代码]wx.showModal({[代码] [代码] [代码][代码]title: [代码][代码]'提示'[代码][代码],[代码] [代码] [代码][代码]content: [代码][代码]'当前微信版本过低,无法使用该功能,请升级到最新微信版本后重试。'[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]}[代码] [代码] [代码][代码]},[代码] [代码] [代码][代码]/**[代码] [代码] [代码][代码]* 下载小程序新版本并重启应用[代码] [代码] [代码][代码]*/[代码] [代码] [代码][代码]downLoadAndUpdate: [代码][代码]function[代码] [代码](updateManager) {[代码] [代码] [代码][代码]var[代码] [代码]self = [代码][代码]this[代码] [代码] [代码][代码]wx.showLoading();[代码] [代码] [代码][代码]//静默下载更新小程序新版本[代码] [代码] [代码][代码]updateManager.onUpdateReady([代码][代码]function[代码] [代码]() {[代码] [代码] [代码][代码]wx.hideLoading()[代码] [代码] [代码][代码]//新的版本已经下载好,调用 applyUpdate 应用新版本并重启[代码] [代码] [代码][代码]updateManager.applyUpdate()[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]updateManager.onUpdateFailed([代码][代码]function[代码] [代码]() {[代码] [代码] [代码][代码]// 新的版本下载失败[代码] [代码] [代码][代码]wx.showModal({[代码] [代码] [代码][代码]title: [代码][代码]'已经有新版本了哟'[代码][代码],[代码] [代码] [代码][代码]content: [代码][代码]'新版本已经上线啦,请您删除当前小程序,重新搜索打开哟'[代码][代码],[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]})[代码] [代码] [代码][代码]},[代码]
2019-06-07 - swiper设置圆角在真机上不显示
在模拟器中的效果: [图片] 在真机上面的效果: [图片]
2018-07-09 - web-view怎么查看console.log?
使用了web-view内嵌了一个vue项目,怎么可以在小程序中console.log?
2019-09-26 - 小程序内怎么调试web-view?
开发工具上在web-view页面内点击鼠标右键有个调试的选项 需要在真机上调试需要自行引入vconsole:https://github.com/Tencent/vConsole/blob/dev/README_CN.md
2019-10-09 - 复杂瀑布流长列表页踩坑记录,内存不足问题【1】
这篇文章主要是解决小程序无限滚动瀑布流页面引起的ios内存不足,自动退出问题 问题回顾:我们有一个列表展示页,是无限瀑布流式的,展示的元素我们封装成了单个组件,暂且叫它[代码]Item组件[代码]。这个瀑布流包含若干个Item组件,并且这个Item组件也比较复杂,包含各种展示样式(根据不同类型,大概有9种吧,反正渲染节点很多),在进行滑动的过程中,item大概加载30-40个以后,就会造成小程序内存不足而退出,蓝瘦香菇… 点击此处查看二期 解决思路: 将超出屏幕一定部分的列表内的组件进行不渲染的处理(也就是用wx:if卸载掉组件),当到达渲染临界点时再开始渲染;保证每次少量的数据展示。 我们的项目中是保持15条Item,我们是每次分页请求5条,按照前5条,中间5条和后5条来划分,如果不在这个范围,则用一个等高度的骨架代替,并且卸载这些组件 实现方式 使用曝光监听,当一个Item曝光时,记录Item高度,并放到数组里面,作为骨架的填充高度,如果已经记录了高度,则不再重复记录;曝光时向外传递一个当前渲染范围的中心值(比如当前Item所属页码,或者当前Item索引),以此进行处理; 这里有一点要注意,如果你的列表item组件比较复杂,需要在ready的时候将记录的高度设置为item最小高度,不然组件重新装载时会有一定的渲染时间,在临界点会造成跳屏【此处已经通过骨架组件解决,可以忽略,只是作为踩坑记录】 此时优化点 为避免频繁setData和渲染,做了防抖函数,时间是600ms 此时缺点 滑动特别快时,会出现白屏,是因为曝光监听是在组件里面,而超快速滚动时,组件没有装载进来,也无法进行曝光监听,所以无法触发,这里考虑用骨架组件进行二次监听曝光 优化迭代 将骨架组件作为外壳套在Item外面(用[代码]slot[代码]),并对骨架进行监听曝光,可以解决上面缺点 给骨架组件做一个常规骨架屏样式,而不是纯白色,看起来更优雅 最后,还是尽量减少节点数,优化代码
2019-12-05 - swiper的卡顿问题
swiper 来回左右滑动,滑动7,8次,就会卡死,官方的demo也一样,也会卡死
2019-02-26 - 自定义组件动态渲染问题请教!动态渲染自定义组件,发生组件之间数据相窜情况,求解
- 当前 Bug 的表现(可附上截图) 示意结构: 顶部tabsbar 每个tab对应一个内容容器 内容容器中对内容进行渲染 [图片] 为了减少页面dom,不进行全量tab对应容器组件的渲染, 每次只渲染当前tab和相邻的2个tab的数据, [左边tab容器] [当前tab容器] [下一个tab容器] 举个例子: 当前tab为3,那需要渲染的tab就是2、3、4,这3个tab, 当点击到tab4时,需要渲染的tab就是3、4、5,这3个tab 每次切换tab后,会对当前tab的容器进行渲染, 加载对应容器的内容展示。 tab容器为同一个组件,通过组件设置id进行区分, [图片] 每次点击tab,会更新需要渲染的tabs, 通过组件id进行组件的选择 然后对tab对应的容器渲染 为了测试方便展示数据动态增量(只在对应id组件内), 数据测试时进行了concat增量, [图片] 现在发现容器组件会发生数据相窜, [图片] 提供了代码片段, 在容器内进行了测试数据输出 - 预期表现 点击tab,只渲染对应容器的内容 - 复现路径 点击tab-> 容器测试数据输出-> 切换tab-> 数据相窜结果出现 [图片] - 提供一个最简复现 Demo https://developers.weixin.qq.com/s/FTBOx6m87ia8 - 补充 如果不进行tab的动态更新,直接渲染全量tab,却不会发生数据相窜的情况 [图片]
2019-07-25 - wx.saveImageToPhotosAlbum ios下保存gif是静态的
这个问题我看到论坛里已经有好几个人反馈了,但是官方一直没有给回复,我想了解下是bug还是解决不了。
2018-05-04 - (4)获取用户信息
背景 我们发现大部分小程序都会使用 [代码]wx.getUserInfo[代码] 接口,来获取用户信息。原本设计这个接口时,我们希望开发者在真正需要用户信息的情况下才去调取这个接口,但很多开发者会直接调用这个接口,导致用户在使用小程序的时候产生困扰,归结起来有几点: 开发者在小程序首页直接调用 [代码]wx.getUserInfo[代码] 进行授权,弹框获取用户信息,会使得一部分用户点击“拒绝”按钮。 在开发者没有处理用户拒绝弹框的情况下,用户必须授权头像昵称等信息才能继续使用小程序,会导致某些用户放弃使用该小程序。 用户没有很好的方式重新授权,尽管我们增加了[代码]设置[代码]页面,可以让用户选择重新授权,但很多用户并不知道可以这么操作。 此外,我们发现开发者默认将 [代码]wx.login[代码] 和 [代码]wx.getUserInfo[代码] 绑定使用,这个是由于我们一开始的设计缺陷和实例代码导致的([代码]wx.getUserInfo[代码] 必须通过 [代码]wx.login[代码] 在后台生成 [代码]session_key[代码]后才能调用)。同时,我们收到开发者的反馈,希望用户进入小程序首页便能获取到用户的 [代码]unionId[代码],以便识别到用户是否以前关注了同主体公众号或使用过同主体的App 。 为了解决以上问题,针对获取用户信息我们更新了三个能力: 1.使用组件来获取用户信息 2.若用户满足一定条件,则可以用[代码]wx.login[代码] 获取到的[代码]code[代码]直接换到[代码]unionId[代码] 3.[代码]wx.getUserInfo[代码] 不需要依赖 [代码]wx.login[代码] 就能调用得到数据 获取用户信息组件介绍 [代码][代码] 组件变化: [代码]open-type [代码]属性增加 [代码]getUserInfo[代码] :用户点击时候会触发 [代码]bindgetuserinfo[代码] 事件。 新增事件 [代码]bindgetuserinfo[代码] :当 [代码]open-type[代码]为 [代码]getUserInfo[代码] 时,用户点击会触发。可以从事件返回参数的 [代码]detail[代码] 字段中获取到和 [代码]wx.getUserInfo[代码] 返回参数相同的数据。 示例: [代码]<button open-type="getUserInfo" bindgetuserinfo="userInfoHandler"> Click me button>[代码]和 [代码]wx.getUserInfo[代码] 不同之处在于: 1.API [代码]wx.getUserInfo[代码] 只会弹一次框,用户拒绝授权之后,再次调用将不会弹框; 2.组件 [代码][代码][代码][代码] 由于是用户主动触发,不受弹框次数限制,只要用户没有授权,都会再次弹框。 通过获取用户信息的组件,就可以解决用户再次授权的问题。 直接获取unionId开发者申请 [代码]userinfo[代码] 授权主要为了获取 [代码]unionid[代码],我们鼓励开发者在不骚扰用户的情况下合理获得[代码]unionid[代码],而仅在必要时才向用户弹窗申请使用昵称头像。为此,凡使用“获取用户信息组件”获取用户昵称头像的小程序,在满足以下全部条件时,将可以静默获得 [代码]unionid[代码]: 1.在微信开放平台下存在同主体的App、公众号、小程序。 2.用户关注了某个相同主体公众号,或曾经在某个相同主体App、公众号上进行过微信登录授权。 这样可让其他同主体的App、公众号、小程序的开发者快速获得已有用户的数据。 不依赖登录的用户信息获取某些工具类的轻量小程序不需要登录行为,但是也想获取用户信息,那么就可以在 [代码]wx.getUserInfo[代码] 的时候加一个参数 [代码]withCredentials: false[代码] 直接获取到用户信息,可以少一次网络请求。 这样可以在不给用户弹窗授权的情况下直接展示用户的信息。 最佳实践 1.调用 [代码]wx.login[代码] 获取 [代码]code[代码],然后从微信后端换取到 [代码]session_key[代码],用于解密 [代码]getUserInfo[代码]返回的敏感数据。 2.使用 [代码]wx.getSetting[代码] 获取用户的授权情况 1) 如果用户已经授权,直接调用 API [代码]wx.getUserInfo[代码] 获取用户最新的信息; 2) 用户未授权,在界面中显示一个按钮提示用户登入,当用户点击并授权后就获取到用户的最新信息。 3.获取到用户数据后可以进行展示或者发送给自己的后端。 One More Thing 除了获取用户方案介绍之外,再聊一聊很多初次接触微信小程序的开发者所不容易理解的一些概念: 1.关于OpenId和UnionId [代码]OpenId[代码] 是一个用户对于一个小程序/公众号的标识,开发者可以通过这个标识识别出用户。 [代码]UnionId[代码] 是一个用户对于同主体微信小程序/公众号/APP的标识,开发者需要在微信开放平台下绑定相同账号的主体。开发者可通过[代码]UnionId[代码],实现多个小程序、公众号、甚至APP 之间的数据互通了。 同一个用户的这两个 ID 对于同一个小程序来说是永久不变的,就算用户删了小程序,下次用户进入小程序,开发者依旧可以通过后台的记录标识出来。 2.关于 getUserInfo 和 login 很多开发者会把 [代码]login[代码] 和 [代码]getUserInfo[代码] 捆绑调用当成登录使用,其实 [代码]login[代码] 已经可以完成登录,[代码]getUserInfo[代码] 只是获取额外的用户信息。 在 [代码]login[代码] 获取到 [代码]code[代码] 后,会发送到开发者后端,开发者后端通过接口去微信后端换取到 [代码]openid[代码] 和[代码]sessionKey[代码](现在会将 [代码]unionid[代码] 也一并返回)后,把自定义登录态 [代码]3rd_session[代码]返回给前端,就已经完成登录行为了。而 [代码]login[代码] 行为是静默,不必授权的,用户不会察觉。 [代码]getUserInfo[代码] 只是为了提供更优质的服务而存在,比如展示头像昵称,判断性别,开发者可通过 [代码]unionId[代码] 和其他公众号上已有的用户画像结合来提供历史数据。因此开发者不必在用户刚刚进入小程序的时候就强制要求授权。 可以在官方的文档中看到 [代码]login[代码] 的最佳实践: [图片] Q & A Q1: 为什么 login 的时候不直接返回 openid,而是要用这么复杂的方式来经过后台好几层处理之后才能拿到? A: 为了防止坏人在网络链路上做手脚,所以小程序端请求开发者服务器的的请求都需要二次验证才是可信的。因为我们采取了小程序端只给 [代码]code[代码] ,由服务器端拿着 [代码]code[代码] 和 [代码]AppSecrect[代码] 去微信服务器请求的方式,才会给到开发者对应的[代码]openId[代码] 和用于加解密的 [代码]session_key。[代码] Q2: 既然用户的[代码]openId[代码] 是永远不变的,那么开发者可以使用[代码]openId[代码] 作为用户的登录态么? A: 不行,这是非常危险的行为。因为 [代码]openId[代码] 是不变的,如果有坏人拿着别人的 [代码]openId[代码] 来进行请求,那么就会出现冒充的情况。所以我们建议开发者可以自己在后台生成一个拥有有效期的 [代码]第三方session[代码] 来做登录态,用户每隔一段时间都需要进行更新以保障数据的安全性。 Q3: 是不是用户每次打开小程序都需要重新[代码]login[代码]? A: 不必,可以将登录态存入[代码]storage[代码]中,用户再次登录就可以拿[代码]storage[代码] 里的登录态做正常的业务请求,只有当登录态过期了之后才需要重新[代码]login[代码] 。这样子做一则可以减少用户等待时间,二则可以减少网络带宽。 目前微信的[代码]session_key[代码] 有效期是三天,所以建议开发者设置的登录态有效期要小于这个值。
2018-08-17 - 发送模版消息formid只能发给生成formid的人吗
- 当前 Bug 的表现(可附上截图) 自己生成的formid 发送模板,使用另一个人的openid的时候出现 41028 错误
2019-06-23 - Painter 一款轻量级的小程序海报生成组件
生成海报相信大家有的人都做过,但是canvas绘图的坑太多。大家可以试试这个组件。然后附上楼下大哥做的可视化拖拽生成painter代码的工具:链接地址https://developers.weixin.qq.com/community/develop/article/doc/000e222d9bcc305c5739c718d56813
2019-09-27 - 用户保存图片到相册授权问题。
问题描述:大家可能会遇到这样的问题,有一个保存图片到相册的功能,我们利用wx.saveImageToPhotosAlbum() api,正常情况下很简单用户点击按钮,系统询问是否授权,用户点击授权图片保存成功。 但是事情往往不是我们想到那样简单,用户的操作可谓是什么操作都有。当系统询问是否授权时,非常敏感的用户一看到授权2话不说,直接拒绝。心想:“握草,想获取我的隐私?”。不一会自己又感觉不对劲,我要保存图片啊,我怎么点了取消。然后又去点保存,发现什么反应都没,心想:“握草怎么没反应,是不是手机坏了,什么鬼”,强迫症患者这个情况怕不是要疯了。 为什么会出现这种情况,根据微信小程序api发现 wx.saveImageToPhotosAlbum() api 只会在用户第一次调用的时候发起授权弹窗,如果用户拒绝授权,wx.saveImageToPhotosAlbum()调用失败,这个时候wx.saveImageToPhotosAlbum()走了失败回调,我们在控制台打印 console.log(“用户授权api调用失败,几率0.01%”) console.log(“用户拒绝授权,几率99.99%”) 用户拒绝授权怎么办,么慌。根据微信小程序文档我们会知道,这种情况下需要用户主动到授权管理页面打开相应的授权开关,高级微信用户都是到入口在那,一般用户授权:“授权管理页面是什么鬼?“,我们不能把希望寄托在他们身上了,他们不是会再一次去点击保存按钮吗? 来一个偷梁换柱,wx.saveImageToPhotosAlbum()调用失败检测是否未授权 this.get_setting() 发现用户未授权,将页面按钮换成 <button open-type=‘openSetting’ bindopensetting=‘bind_load’ class=‘fenxiang_bot_box nobutton’></button> 打开授权管理页面, 用户发现自己“看到了不该看的,握草,用户信息,保存相册,敢获取我的信息,都关掉”,哈哈,用户已经知道是怎么回事了,“不行我得把保存相册打开,这个图片太好了我要保存”; get_setting(t) { wx.getSetting({ success: (res) => { if (res.authSetting[‘scope.writePhotosAlbum’] == undefined) { this.setData({ img_status: true }); } else { this.setData({ img_status: res.authSetting[‘scope.writePhotosAlbum’] }); } if (t) { t() } } }) }, wx.saveImageToPhotosAlbum({ filePath: this.data.img, success(res) { wx.showToast({ title: ‘保存成功’ }) }, fail: (res) => { [代码] this.get_setting(() => { if (this.data.img_status) { wx.showToast({ title: '保存失败', icon: 'none' }) } else { wx.showToast({ title: '权限不足', icon: 'none' }) } }); } }) [代码] 用户同意授权,打开开关返回bindopensetting=‘bind_load’ bind_load 事件触发保存操作, 保存成功^ _^
2019-07-03 - 如何实现快速生成朋友圈海报分享图
由于我们无法将小程序直接分享到朋友圈,但分享到朋友圈的需求又很多,业界目前的做法是利用小程序的 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 - 小程序处于活跃状态,通过分享卡片打开小程序,app.js的options不更新
- 当前 Bug 的表现(可附上截图) 聊天界面有两个分享卡片,带不同参数,先打开第一个卡片,启动小程序,然后切回聊天界面,再打开聊天界面第二个分享卡片,小程序只是从后台切回前台,app.js中onShow的options参数不会刷新,在页面的onshow中调用wx.getLaunchOptionsSync()获取到的options也没有更新,这个是BUG还是机制如此,或者有什么解决办法? - 预期表现 第二次打开小程序时,onshow里面的参数刷新 - 复现路径 - 提供一个最简复现 Demo
2019-07-17 - 【转】微信小程序中实现瀑布流布局和无限加载
瀑布流布局是一种比较流行的页面布局方式,最典型的就是Pinterest.com,每个卡片的高度不都一样,形成一种参差不齐的美感。 在HTML5中,我们可以找到很多基于jQuery之类实现的瀑布流布局插件,轻松做出这样的布局形式。在微信小程序中,我们也可以做出这样的效果,不过由于小程序框架的一些特性,在实现思路上还是有一些差别的。 今天我们就来看一下如何在小程序中去实现这种瀑布流布局: [图片] 小程序瀑布流布局 我们要实现的是一个固定2列的布局,然后将图片数据动态加载进这两列中(而加载进来的图片,会根据图片实际的尺寸,来决定到底是放在左列还是右列中)。 [代码]/* 单个图片容器的样式 */.img_item { width: 48%; margin: 1%; display: inline-block; vertical-align: top; }[代码]我们知道,在HTML中,我们要动态加载图片的话,通常会使用new Image()创建一个图片对象,然后通过它来动态加载一个url指向的图片,并获取图片的实际尺寸等信息。而在小程序框架中,并没有提供相应的JS对象来处理图片加载。其实我们可以借助wxml中的[图片]组件来完成这样的功能,虽然有点绕,但还是能满足我们的功能要求的。 [代码]<view style="display:none"> <image wx:for="{{images}}" wx:key="id" id="{{item.id}}" src="{{item.pic}}" bindload="onImageLoad">image>view>[代码]我们可以在Page中通过数据绑定,来传递要加载的图片信息到wxml中,让[图片]组件去加载图片资源,然后当图片加载完成的时候,通过bindload指定的事件处理函数来做进一步处理。 我们来看一下Page文件中定义的onImageLoad函数。在其中,我们可以从传入的事件对象e上,获取到[图片]组件的丰富信息,包括通过它加载进来的图片的实际大小。然后我们将图片按照页面上实际需要显示的尺寸,计算出同比例缩放后的尺寸。接着,我们可以根据左右两列目前累积的内容高度,来决定把当前加载进来的图片放到哪一边。 [代码]let col1H = 0;let col2H = 0; Page({ data: { scrollH: 0, imgWidth: 0, loadingCount: 0, images: [], col1: [], col2: [] }, onLoad: function () { wx.getSystemInfo({ success: (res) => { let ww = res.windowWidth; let wh = res.windowHeight; let imgWidth = ww * 0.48; let scrollH = wh; this.setData({ scrollH: scrollH, imgWidth: imgWidth }); //加载首组图片 this.loadImages(); } }) }, onImageLoad: function (e) { let imageId = e.currentTarget.id; let oImgW = e.detail.width; //图片原始宽度 let oImgH = e.detail.height; //图片原始高度 let imgWidth = this.data.imgWidth; //图片设置的宽度 let scale = imgWidth / oImgW; //比例计算 let imgHeight = oImgH * scale; //自适应高度 let images = this.data.images; let imageObj = null; for (let i = 0; i < images.length; i++) { let img = images[i]; if (img.id === imageId) { imageObj = img; break; } } imageObj.height = imgHeight; let loadingCount = this.data.loadingCount - 1; let col1 = this.data.col1; let col2 = this.data.col2; //判断当前图片添加到左列还是右列 if (col1H <= col2H) { col1H += imgHeight; col1.push(imageObj); } else { col2H += imgHeight; col2.push(imageObj); } let data = { loadingCount: loadingCount, col1: col1, col2: col2 }; //当前这组图片已加载完毕,则清空图片临时加载区域的内容 if (!loadingCount) { data.images = []; } this.setData(data); }, loadImages: function () { let images = [ { pic: "../../images/1.png", height: 0 }, { pic: "../../images/2.png", height: 0 }, { pic: "../../images/3.png", height: 0 }, { pic: "../../images/4.png", height: 0 }, { pic: "../../images/5.png", height: 0 }, { pic: "../../images/6.png", height: 0 }, { pic: "../../images/7.png", height: 0 }, { pic: "../../images/8.png", height: 0 }, { pic: "../../images/9.png", height: 0 }, { pic: "../../images/10.png", height: 0 }, { pic: "../../images/11.png", height: 0 }, { pic: "../../images/12.png", height: 0 }, { pic: "../../images/13.png", height: 0 }, { pic: "../../images/14.png", height: 0 } ]; let baseId = "img-" + (+new Date()); for (let i = 0; i < images.length; i++) { images[i].id = baseId + "-" + i; } this.setData({ loadingCount: images.length, images: images }); } })[代码]这里是显示在两列图片的wxml代码,我们可以看到在scroll-view>组件上,我们通过使用bindscrolltolower设置了事件监听函数,当滚动到底部的时候,会触发loadImages去再加载下一组的图片数据,这样就形成了无限的加载:/scroll-view> [代码]<scroll-view scroll-y="true" style="height:{{scrollH}}px" bindscrolltolower="loadImages"> <view style="width:100%"> <view class="img_item"> <view wx:for="{{col1}}" wx:key="id"> <image src="{{item.pic}}" style="width:100%;height:{{item.height}}px">image> view> view> <view class="img_item"> <view wx:for="{{col2}}" wx:key="id"> <image src="{{item.pic}}" style="width:100%;height:{{item.height}}px">image> view> view> view>scroll-view>[代码]好了,挺简单的一个例子,如果你有更好的方法,不吝分享一下哦。 完整代码可以在我的Github下载:https://github.com/zarknight/wx-falls-layout 原作者:一斤代码(简书作者) 原文链接:http://www.jianshu.com/p/260f2623562d
2016-11-29 - 小打卡|如何组件化拆分一个200+页面的小程序
大家好,我是小打卡的前端唐驰。刚才金轩正同学分享了基于原生小程序底层架构,在此基础上我为大家分享下如何拆分一个200+页面的小程序,主要通过以下几点来聊一聊小打卡在组件化路上的一些实践 1.背景 2.组件与方案 3.组件间通讯 4.基于组件我们做了哪些事 [图片] [图片] 1. 其实一开始小打卡是没有引入组件化的,因为微信最开始是不支持组件化的。当时js代码已经4k+行了,各种功能代码,有用的没有用的,不知道干什么的代码就躺在那里,一动不动。举个例子,一个头像点击跳转的逻辑搜索了下,遍布在各个页面。修改起来可想而知的胆战心惊。另一个原因就是当时由于业务功能直线上升,很快我们就遇到了代码包超包了。在微信还没有实现分包之前,我们就只能一个一个页面的去review剔除代码,效率极低。这也是促成我们决定寻求出路的原因之一。可是删代码删功能是不能解决问题,期间我们也考虑过h5的方式,跑了demo之后却发现h5方式的多次渲染, 与加载首页白屏,尽管有各种服务端渲染方案,但是我们一致觉得为了用户体验,放弃了。 [图片] 2. 对于小打卡来说,我们不能再任由项目裸奔了,需要一种开发方式来进行约束,主要是有几个诉求: 在之前的项目上,为了方便。功能与功能之间的耦合程度极其的高,各种为了使用方便而随意修改某一个方法。 1.降低页面上各个功能点的耦合程度 我们不希望同一个功能点同样的代码在页面肆意copy,这样带来了极高的维护成本。以至后面无法维护。并且功能的复用不希望是copy,前端与后端不同的是不仅是单单的逻辑复用、更有布局、样式等。 2.提供代码的可复用性、可维护性 对于一个程序员来说,如果你打开一个代码文件。映入眼帘的是密密麻麻的代码,行数达到好几千K行,我相信大家的第一反应是抗拒的,更别说去修改代码,天知道会改出什么问题。 3.降低单一文件的复杂度 4.如果是公共功能的化我们还希望它能够有自己的作用域,保持自己的独立性。 [图片] 3. 根据以上几点,我们用一个页面举例,如何去拆分一个页面,首先我们需要有以下几点认识: 决定一个页面如何组件化的前提是该页面的功能是否是有全局都需要的功能模块 功能模块是否需要与页面其他模块强耦合 单个功能模块逻辑是否过于复杂(占用代码空间过大)——>单纯是为了页面代码的可读性。 不是全拆成组件就是最好的,不能为了组件而组件化 [图片] 4. 说了这么多,其实我们应该首先应该了解下,组件的特性? 专一性(一个组件只干一件事情,或者某一类事情。)功能的高度内聚 比如说右侧的feed集上的头像、它是一个组件、就负责显示头像跟跳转,其他的事它都不参与 可配置(能够适应通过设置属性值的方式来输出不同的东西)输入影响部分输出 然后我们同时可以设置头像组件上的size属性来设置头像在不同页面下的大小样式 生命周期(组件可以在自身或者说所在页面的生命周期内可以做不同的事情)比如可以在组件生成的时候进行数的初始化、属性值的类型校验、组件销毁时并同时销毁定时器等其他任务 事件传递 (既然要让组件与页面保持独立性,那么组件与页面的通讯交互就得需要一个标准) 右侧的feed组件其实是一个组件集合、我们通过组合不同的组件然后就形成了feed组件。就跟搭积木一样、只需要引入组件就行了。特别方便。 [图片] 5. 说到组件,那么小程序早期的不支持自定义组件开发这就很让人头疼、同样的feed组件我们经历了几乎三个版本的大改动、从最开始的直接写在页面里,后台使用template方式、再到后来的自定义组件方式。所以我们的演进步骤就成了page->template->component, 这儿列了一个表格对比了下几种组件化方式的对比。 可以看到,include的方式其实是最鸡肋的,include的方式其实实际意义上我理解成更多的是代码的切割,并且还不能将(template、wxs)分出去、所以这种方式我们直接pass掉了, 而template的方式其实是我们曾经主力使用的方式、到现在我们也还在使用、相对于include来说,template有了独立的作用域、虽然css、跟js还是与页面共享的。但是已经可以做一些比较简单的事情了。 对于component来说,完完全全的组件,满足了组件的所有要求。 [图片] 6. 先说说template的方式吧,举个列子,这个是我们的使用template构建的头像组件。跟写页面的方式很像、同样是js、wxss、wxml组成。用名称来命名。但是由于微信当时没有很方的方式去引用这些文件,或者说没有一种方法可以打包供我们很方便的使用。但是比起之前直接copy代码的方式、这样通过引用的方式使用其实感觉已经好了很多了。 [图片] [图片] 7. 具体的使用方式我画了张图,对应组件内文件与页面文件的对应方式、这里对于js的引用其实我们是做了一些小动作, 我们在调用Page方法前做了一次page方法与组件方法的check,因为在page代码里我们不能保证所有的方法名不会跟组件内的方法名不会冲突,所以我们做了这个一个检查、 然后mix函数还做了另一个事情就是将page方法与组件方法合并。然后对于mix函数其实我们还可以做很多事情、、比如规范生命周期回调函数放在一个对象内,然后我们自己定义的方法放在另一个对象里,就跟vue一样。 But,在经历了一段template组件化的时间后,我们又觉得这个方式还是有点烂,为什么呢?在使用时仍然不能避免引入众多的文件、虽然我们对js文件做了处理,但是wxss的样式仍然会被污染、js与page仍然共享作用域。并不能成为一个真正的标准组件。好在后来,微信上了自定义组件的功能,接下来聊聊这个标准的微信自定义组件吧。 8. 微信提供了自定义组件的功能后我们也第一时间跟进了,相对于template这种方式来说,现在是真正的独立于页面存在。使用也比之前更为方便与简洁,右图是我们对component的一个项目目录划分。我们将component划分为了公共组件与页面组件、为什么会有页面组件, 1.是为了降低页面代码的复杂度 2.为了好看。 公共组件就不说了,一定是最基础、最通用的组件。 [图片] 9. 转向component方式后有一个问题逐渐便凸显出来了,由于组件的独立作用域,组件间的通讯就成了一个问题,接下来聊一聊组件的事件传递。微信最开始的时候提供了一种triggerEvent的方式,可是这样的方式似乎并不能满足我们某些场景下的需求。后来又提供了page下selectComponent方法来直接操作组件内部的属性与方法。然后还有就是基于我们自己的事件广播机制。这几种方式构成了小打卡现目前最主要的组件与page、组件与组件间的数据交互方式 [图片] 10. 先来说说triggerEvent模式,微信在自定义组件上可以自定义监听函数。我们在组件内将需要向外抛出的事件统一通过this.triggerEvent(‘invoke’,{handler:’fun’,data:{}})这个方法来执行。其中invoke对应了我们绑定在组件标签上的监听函数。而将需要外部执行的方法与数据通过数据的方式传给监听函数。而在page上面我通过统一的监听回调函数去自动执行需要执行的方法、这里的trigger与event都不要我们去手写在组件与page创建的时候底层就已经帮我们预置了,我们只需要关注业务开发就行。这是对于一部分需要page与组件交互的模式。而对于我们想直接操作组件方法而不需要反馈的模式就得使用selectComponent的模式 [图片] 11. 一个简单的列子:全局的toast组件。在需要弹出toast的时候我们想直接调用就行、不用在通过传值给组件、然后由组件来执行显示或隐藏。这类组件我们在组件目录里新增了一个lib的文件。在page里只需要引入这个lib文件然后就可以直接调用toast组件。lib主要是对this.selectCompent与执行逻辑的一个封装。 [图片] 12. 事件发布订阅模式:基于底层的eventBus。简化后我们用在了组件与组件之间的通讯上、特点是简单。 [图片] 13. 解决了组件间的通讯问题,可是对于公共组件的引用仍然让我们觉得麻烦与不畅快、所以我们构建了全局通用模版、它是干什么的呢。它提供给了一些基础的全局组件、比如自定义导航头、toast、loading等等。小打卡所有的页面都通过slot的方式插入到这个模版组件x-page下面。这样就解决了我们需要在每个页面引入公共组件的问题。另一个问题使用自定义导航栏的时定位起点会有状态栏下移动到屏幕左上方。会造成布局的错误。通过x-page可以很好解决这个问题而不用重新布局。并且通信问题也不用担心,都是由x-page组件作为中台来对内对外进行分发与执行。 [图片] [图片] [图片] 14. 通过以上小打卡的开发模式就基本形成。要做的事情还有很多,更多组件的玩儿法,对于现在或者将来我们正在做的。 是构建小打卡的组件与基础sdk的仓库。 拆分组件开发与业务开发。 通过npm包管理的方式来应对越来越多的小程序平台的开发。 或者通过形成小程序插件的方式供其他小伙伴使用。 [图片] [图片] 以上就是我今天分享的内容。谢谢。
2019-04-26 - 小打卡 | 如何基于微信原生构建应用级小程序底层架构(下)
点击查看:小打卡 | 如何基于微信原生构建应用级小程序底层架构(上)装饰模式[图片] 可能有朋友会好奇装饰器和我们接下来要做的原生函数扩展有什么关系? 先看下装饰模式的定义:在不改变原函数的基础上,附加一些额外的功能 方便更好的理解 创建一个函数,然后将存在originF这个变量里,并对它重新赋值,最后执行,最后的执行直接结果是什么? hello world!! 这样做的好处是什么,我们对函数f添加了一个新的功能,但是我们没有对原函数进行任何的修改 那么对于小程序的应用 照猫画虎一下,同样的将小程序原生的Page函数存起来,再重新赋值,看下结果,发现Page执行的时候每次都会console这句话,我这里有两个未分包的页面,就执行了2次 [图片] 回归到刚开始需要解决的问题,“多个页面pv,uv如何监控,不可能每个页面都要手动收集”, pv怎么算,pv的意思是页面浏览量,也就是需要页面生成时,对应到小程序生命周期就是onLoad 刚才我们做到了每个Page执行时添加功能,但是怎么在onLoad时进行数据统计呢? 同样的可以用装饰函数修饰page里面的函数方法 [图1] 这个data就是我们实际在pages下写的业务逻辑对象,我们先拿到该页面的key名来进行遍历,首先排除掉非函数,拿到onLoad函数,这时候对它进行扩展,这时候每一个页面执行onLoad的时候都会console一次字符串,当然也可以替换为你想要的任何功能 [图2] 其实我们可以再优化一下,比如抽出一个对象,将你想要装饰的函数写入其中,如果原函数存在则进行装饰,如果不存在则直接赋值,这个抽出来的对象其实可以算是另一种继承方式的实现 它的意义在于[图片] 帮助我们开辟了一块公有空间,帮助我们扩展Page对象,并且可以劫持任意方法,在不修改原业务函数代码的情况下增加新的逻辑 其次也是最重要的是,我们完全不需要处理历史包袱 一个应用场景[图片] 左边是微信原生的分享方法 遇到的一些问题: 不便于扩展,多个分享策略时,代码块过大,并且分享的通用数据需要每个页面单独处理,例如统计回流信息需要的分享页面信息,事件信息 右边是依赖于我们刚刚开辟的公有空间里填写的公共方法,函数很简单,获得参数,获得页面分享策略,执行,顺便还做了obj转url的处理,避免了纯url书写长路径时的尴尬 思考[图片] 既然能扩展Page对象,那么App,Component甚至wx全局对象下的方法呢? 有兴趣的朋友可以下来想一下 跨页面 / 多页面事件通知[图片] 我这边简单提一下跨页面通知的问题,这个应该算是很多小程序开发者遇到的通用问题,我问过的一些人,大部分是使用下面这两种方法,一种是getCurrentPages 页面栈队列,二种onShow upData global里的存储的数据,不管哪一种在业务复杂的情况下都会引起一定的问题,比如第一种的多级入口,第二种的话属于无效判断和及时性 小打卡这边用的简版的event Bus,一个只有80行代码的订阅方法, 左图是一个简单的示例,大致上就是分两种角色,订阅者和发布者,中间依靠任务队列联系,每次发布者推送消息,订阅者都会收到通知和相应的数据 [图片] 其实单纯的event bus也有很多的问题,比如:业务复杂情况下过于频繁,对业务代码侵入频繁,可以想像一下到处都是on,off, emit场景 解绑需要树立规范,但是人总是会犯错,容易绑定后忘记解绑或重复绑定的问题,比较浪费内存,对性能也有消耗 结合公共空间我们已知可以对生命周期进行扩展的时候怎么去解决这个问题 其实订阅必然是和页面结合,因为在页面不存在的情况下,发送通知也不会有反馈, 如何证明页面存在自然是onLoad 和 onUnload 按照这个逻辑我们只需要在onLoad和onShow做些处理即可 [图1] 先看右侧业务代码,按照策略注册了一个函数集合,在执行onLoad时,自动将业务内的onRss函数遍历,自定绑定订阅,并推到一个缓存数组内 onUnload时遍历缓存,自动解绑 这时订阅与页面的生命周期强绑定,我们不再需要处理解绑事件,也不需要在业务内插入订阅代码,只需要管理好当前页面的订阅策略即可 技术选型[图片] 没有哪一种一定正确的方案,在选型的时候可能需要考虑到上面大致6种元素, 总之选择最适合自己团队方案非常重要 一些小贴士:开发要尽量避免过度设计, 应根据实际需求, 比如之前在写现在这个简版的event bus库的时候设计了很多奇奇怪怪的 功能,现在2年过去还没用到 小程序和浏览器,node本质上是相通的,可以多借鉴和参考 The End今天我的分享就到此结束,算是这段时间对小程序开发上的一点心得体会,谢谢 [图片]
2019-04-22 - 小打卡 | 如何基于微信原生构建应用级小程序底层架构(上)
[图片] 大家好,我是小打卡的前端负责人金轩正,今天分享的主题是如何基于微信原生构建应用级小程序底层架构,这个命题看上去好像有些大,不过不要紧,这次分享我把它拆一下,大致从 小程序原生开发面临的问题 小打卡整体架构演进 开发中摸索与实践 这三个方面来看这个讲一下 [图片] 小程序原生开发面临的问题[图片] ok,首先第一个方面原生开发遇到的问题 小程序从17年诞生2年来一直处于互联网风口,不过对于开发者而言的整个开发体验不是特别友好,在17-18年之间我和很多开发小程序的小伙伴们聊过,大多数的反馈可能分为下面大致几类,当然还有更多: 没有父类,无法使用继承挂载全局方法,扩展生命周期没有父类,无法使用继承挂载全局方法,扩展生命周期 不支持跨页面/多页面通讯 setData的性能瓶颈 代码包大小限制 1/2/4/8 M,没有npm包 代码发布流程繁琐 其根本原因是将刚刚诞生的小程序与已经非常成熟的React,vue,angular作对比,而没有将小程序作为一个新的生态来看待,当然这个是一种看待事物的进步,并不是倒退,我在这里说这句话的意思是有更多的问题需要我们开发者主动去解决问题,推动整个生态的前进与发展 [图片] 其实这里可能有些朋友会问,已经有很多优秀的框架已经解决了这些问题,那么为什么还要使用原生开发? 确实在这段时间内出现了很多优秀的解决方案,我们不用并不是因为情怀哈(当然还是有那么一丢丢) 更多的是下面几点: 历史包袱,改造成本过高 小打卡在小程序刚出现的时候就进入开发了,当时框架还不成熟,而且对创业公司来说时间和迭代效率高于一切,在人手不足,业务模式尚未形成,还处于探索阶段的情况下花费大量时间去做对产品影响较小, 甚至delay迭代速度事情不是很赚 减少与第三方沟通成本 高速迭代的情况下,将时间尽可能的覆盖于业务上,避免在整个开发-上线闭环上增加节点 避免开发黑盒,控制风险 虽然整个社区是非常活跃的,fixed一个问题同样是需要花费一定时间,但是很多时候需求是不会等你bug fixed 如非必要,勿增实体 即“简单有效原理”,这句话还是我去年刚来公司的时候和阿赖聊他所说过的 放在项目开发上我的理解是在架构层面要做的尽可能的薄,避免过度设计 这样才有足够的扩展性,灵活性,容错性 这些框架虽好,但是对我们当前业务来说可能过于复杂,比如跨端在之前的阶段还没有这方面需求,而像组件化小程序已经支持,自动化构建我们自己也是可以搭建的并不复杂 相信微信小程序团队 是真正的想把这件事情做好,而且做的是一个生态,不论是小程序对于反馈响应速度,和迭代速度非常给力,还是对开发者社区运营,比如是社区活跃与审核速度挂钩,社区周刊,优质个人和优质企业 对齐web标准,并且更加开放 [图片] 小打卡整体架构演进其实小打卡整个架构并非一蹴而就的,就像前面所说的如非必要,勿增实体,而是大量的实际开发中遇到的共同问题解决方案的集合题 [图片] 常规架构这个是微信小程序给出的快速开发模版的一个开发模式: server模块提供数据,App作为全局对象直连所有的业务模块,工具函数提供api处理业务模块的需求 优点: 整个模型非常简单,上手快,学习成本 低结构清晰,在业务不复杂的情况下可以快速开发 不瞒大家其实小打卡在最初的半年内基本都是这套模式。 当然是在业务不复杂的情况下,复杂情况下会出现哪些问题呢? App作为全局对象在有大量业务模块连接的情况下,代码很容易膨胀,在多人开发的时候问题非常明显,无论是fixed bug还是正常的业务开发都会造成麻烦 页面之间独立,缺少公共模块,唯一的工具函数又要尽可能保持单一职责来提供服务(小打卡当时就是因为这个问题导致很多工具函数内部存储直接修改外部状态,导致大量强耦函数合无法拆分) 业务层直连server层,未拆分数据层的情况下,基本不存在复用性 上面所述的问题,从我接手这个项目到真正的调整持续了挺长一段时间,主要是缺乏一个契机来进行优化 优化的转折点 [图片] 然后突然有一天产品同学跑过来说: 我们要有自己的核心数据仓库,我们要看实时数据 ok,涉及到数据采集的问题了,我这边从浅到深大概列了几项: 最基础的多个页面pv,uv如何监控,不可能每个页面都要手动收集 为了统计页面和事件的分享和回流的数据,需要在分享事件携带大量的参数 微信的wx.previewImage, wx.chooseImage 等api对于用户session的收集造成很大麻烦 我们先解决第一个问题,如何收集页面pv,uv 容易陷入的误区 [图片] 在解决问题之前,我们先说一下开发小程序容易进入的误区 App 和 Page 等函数工厂是微信原生提供,不可修改 小程序项目结构是基于App, Page, 工具函数三个模块构建的 小程序的全局存储只有globalData和本地缓存 其实产生这些误区最根本的原因是小程序没有提供在复杂业务逻辑下的开发范式,比如vue,react有自己的通用开发模版 如果保持这些观念来进行开发的话,很容易将路子走窄,并且难以解决一些实际上的问题, 其实不论小程序和传统web有多少不同, 本质上还是在js环境下开发 小打卡架构图解 [图片] 为了更好的方便理解后面的具体实现,我提前放了一张目前小打卡的架构图 首先很熟悉的server这一边垫了一个数据层,主要将数据层和业务层解耦,提高复用性,并且提供一些通用功能,比如返回格式化数据问题,参数校验,日志监控... 在App对象和业务层同样增加了一个全局模块,提供独立于业务和工具类,只提供api之间双向通讯的渠道 工具模块的话其实就是对业务层的增强,比如常见的请求模块,上传模块,路由拦截等等 业务模块的话基本除了增加Component和中间层外没有太大变化 这个图上可能有两块可能大家觉得比较怪异,一个是global里面的函数重载,还有一个是业务模块的中间层是什么? 函数重载其实就是修改微信提供的App, Page, Component函数,使其更符合我们的业务场景, 业务模块的中间层就是依赖于函数重载的扩展 其实小打卡的整套架构都是基于这两个模块,这两个模块赋予了更多的可能性,然而实现却十分的简单 点击查看:小打卡 | 如何基于微信原生构建应用级小程序底层架构(下)
2019-04-22 - 小打卡前端周刊(2019-08-19)
小打卡前端周刊(2019-08-19) 上周看起来是波澜不惊的一周,并没有搞出个大新闻 前端资讯 主打无需配置特点的前端打包工具 Parcel 发布了 2.0 的 alpha 版本,新版本提供更好的拓展性、延展性、可靠性,现在可使用 [代码]yarn add parcel@v2[代码] 立即尝试。详细信息 V8 引擎发布了 v7.7 版本,新版本减少了 2-8% 的内存占用量并且增强了 WebAssembly 的后台编译能力。详细信息 工具推荐 两个通过游戏的方式练习 FLEX 布局的网站:Flexbox Froggy、Flexbox Defense NPKILL:帮你轻松找到并且删除旧版本并且体积巨大的 [代码]node_modules[代码] 文件夹 buffer:可以让你在浏览器中使用 node.js 的 buffer 模块 每周文章:【翻译】globalThis 全知道 本文译自 ES proposal: globalThis 译者 郭梓梁,首次发布于 MeloGuo Blog,转载请保留以上链接 由 Jordan Harband 提出的 ES 的新特性 [代码]globalThis[代码] 提供了一种标准方式来获取全局对象。 JavaScript 的全局作用域 JavaScript 的作用域从全局作用域开始为节点,形成了一个嵌套的树形结构。 仅仅在浏览器里直接使用 [代码]<script>[代码] 标签时的代码直接运行在全局作用域中。在这当中有两种全局变量: 普通的_全局声明变量_ 它们通过 [代码]const[代码], [代码]let[代码] 和 [代码]class[代码] 声明被创建在脚本的最顶层作用域 存放在_全局对象_中的_全局对象变量_ 它们通过 [代码]var[代码] 和函数声明被创建在脚本的最顶层作用域 它们能够通过全局对象被创建、被删除以及被读取 除了以上两点,它们就和普通变量没什么区别 全局对象能够通过 [代码]globalThis[代码] 访问到。下面的 HTML 代码示范了 [代码]globalThis[代码] 和几种不同的全局变量。 [代码]<script> const one = 1; var two = 2; </script> <script> // 所有的 script 标签中都共享同一个顶层作用域 console.log(one); // 1 console.log(two); // 2 // 不是所有的声明都在全局对象上创建了属性 console.log(globalThis.one); // undefined console.log(globalThis.two); // 2 </script> [代码] 应注意的是,每一个模块都有自己的作用域。因此,在模块中的顶层作用域的变量不是全局的。下图展示了不同作用域之间的关系。 [图片] this 的值 无论何时调用方法的对象中, [代码]this[代码] 的值都取决于当前的作用域: 顶级作用域中:全局对象 ES 模块的顶层中:undefined 在函数调用中: 严格模式(包括模块):undefined 松散模式:同全局 [代码]this[代码] 如果你间接的调用 [代码]eval()[代码],它将会随意的在全局作用域中执行。因此,你能用下面的代码来获取全局的 [代码]this[代码]: [代码]const theGlobalThis = eval.call(undefined, this); [代码] 需要警告的是,如果你使用了 CSP (Content Security Policy), [代码]eval[代码], [代码]new Function()[代码] 等方法是无法生效的。这就使得这种获取全局对象的方法在一些情况下是不合适的。 globalThis 和其他(window 等等) [代码]globalThis[代码] 是一个全新的标准方法用来获取全局 [代码]this[代码] 。之前开发者会通过如下的一些方法获取: 全局变量 [代码]window[代码]:是一个经典的获取全局对象的方法。但是它在 Node.js 和 Web Workers 中并不能使用 全局变量 [代码]self[代码]:通常只在 Web Workers 和浏览器中生效。但是它不支持 Node.js。一些人会通过判断 [代码]self[代码] 是否存在识别代码是否运行在 Web Workers 和浏览器中 全局变量 [代码]global[代码]:只在 Node.js 中生效 新提案也规定了,[代码]Object.prototype[代码] 必须在全局对象的原型链中。下面的代码在现在的最新浏览器中已经会返回 [代码]true[代码] 了: [代码]Object.prototype.isPrototypeOf(globalThis); // true [代码] globalThis 使用实例 由于需要向下兼容,全局对象现在已经被认为是一个 JavaScript 无法摆脱的错误了。它会造成负面的性能影响并且令人产生困惑。 ECMAScript 介绍了一些方法来让你更简单的避免触碰全局对象 - 例如: [代码]const[代码] 、[代码]let[代码] 和 [代码]class[代码] 声明被使用在全局作用域时也不会创建全局对象属性 每个 ECMAScript 模块他都它自身的作用域 将全局变量只作为一个变量来处理要比作为 [代码]globalThis[代码] 的属性来处理要合适的多。这个特性已经在所有的 JavaScript 平台上都能工作了。 因此,这有几个和 [代码]globalThis[代码] 相关的例子 - 例如: 在 polyfills 和 shims 中提供 JavaScript 引擎新特性 通过特性检测,去找出一个 JavaScript 引擎支持哪种特性种特性 polyfill 此提案的作者 Jordan Harband 写过一个 [代码]globalThis[代码] 的 polyfill。 用 CommonJS 语法引入: [代码]// 生成 `global` 变量的值 var global = require('globalthis')(); // 垫片 `global` (全局安装) require('globalthis/shim')(); [代码] 用 ES6 模块语法引入: [代码]// 生成 `global` 变量的值 import getGlobal from 'globalthis'; const global = getGlobal(); // 垫片 `global` (全局安装) import shim from 'globalthis/shim'; shim(); [代码] 这个包总是会使用“最原生”的方法来访问全局对象(在 Node.js 中是 [代码]global[代码],在浏览器中是 [代码]window[代码] 等等) 得到一个全局对象的引用 在内部实现中 polyfill 使用 [代码]getPolyfill()[代码] 来得到全局对象。下面是实现方法: [代码]// polyfill.js var implementation = require('./implementation'); module.exports = function getPolyfill() { if (typeof global !== 'object' || !global || global.Math !== Math || global.Array !== Array) { return implementation; } return global; }; [代码] [代码]// implementation.js if (typeof self !== 'undefined') { module.exports = self; } else if (typeof window !== 'undefined') { module.exports = window; } else if (typeof global !== 'undefined') { module.exports = global; } else { module.exports = Function('return this')(); } [代码] FAQ: globalThis 为什么不在所有地方都使用 global、self 或 window? 唉,这是不可能的,因为许多 JavaScript 库使用这些变量来检测代码运行在哪一个平台上。 globalThis 有考虑使用过其他名字吗? 这个 issue列出了我们考虑过的所有命名以及没有采用的原因。 资源与背景 General background: A general overview of global variables: section “Global variables” in “JavaScript for impatient programmers” In-depth information on how JavaScript manages its global variables: “How do JavaScript’s global variables really work?” on 2ality Useful background for what happens in browsers: “Defining the WindowProxy, Window, and Location objects” by Anne van Kesteren Various ways of accessing the globalthisvalue: “A horrifyingglobalThispolyfill in universal JavaScript” by Mathias Bynens Specifications: Very technical: “Realms, settings objects, and global objects” in the WHATWG HTML standard In the ECMAScript specification, you can see how web browsers customize globalthis: “InitializeHostDefinedRealm()”
2019-08-19 - json2canvas:使用JSON生成小程序海报
作者:诗人的咸鱼 原文:小程序生成分享海报,一个json就够了。同时支持web Fundebug经授权转载,版权归原作者所有。 需求 在项目里写过几个canvas生成分享海报页面后,觉得这是个重复且冗余的工作.于是就想有没有能通过类似json直接生成海报的库. 然后就在github找到到两个项目: wxa-plugin-canvas,不太喜欢配置文件的写法.就没多去了解 mp_canvas_drawer,使用方式就比较符合直觉,不过可惜功能有点少. 然后就想着能不能自己再造个轮子.于是就有了这个项目 json2canvas,你可以简单的理解为是mp_canvas_drawer的增强版吧. json2canvas canvas绘制海报,写个json就够了. 项目的canvas绘制是基于cax实现的.所以天然的带来一个好处,json2canvas同时支持小程序和web 功能 支持缩放. 如果设计稿是750,而画布只有375时.你不需要任何换算,只需要将scale设置为0.5即可. 支持文本(长文本自动换行,感谢 coolzjy@v2ex 提供的正则 https://regexr.com/4f12l ,优化了换行的计算方式(不会粗暴的折断单词)) 支持图片(圆角) 支持圆型,矩形,矩形圆角 支持分组(cax里很好用的一个功能) 同时支持小程序和web 示例 web-demo 界面左边的json,可以进行编辑,直接看效果哟~ 小程序-demo [代码]git clone https://github.com/willnewii/json2canvas.git 微信开发者工具导入项目 example/weapp/ [代码] 小程序安装 [代码]npm i json2canvas 微信开发者工具->工具->构建npm [代码] 在需要使用的界面引入Component [代码]{ "usingComponents": { "json2canvas":"/miniprogram_npm/json2canvas/index" } } [代码] 效果图 想要生成一个这样的海报,需要怎么做?(红框是图片元素,蓝框是文字元素,其余的是一张背景图。) [图片] 一个json就搞定.具体支持的元素和参数,请查看项目readme [代码]{ "width": 750, "height": 1334, "scale": 0.5, "children": [ { "type": "image", "url": "http://res.mayday5.me/wxapp/wxavatar/tmp/bg_concerts_1.jpg", "width": 750, "height": 1334 }, { "type": "image", "url": "http://res.mayday5.me/wxapp/wxavatar/tmp/wxapp_code.jpg", "width": 100, "x": 48, "y": 44, "isCircular": true, }, { "type": "circle", "r": 50, "lineWidth": 5, "strokeStyle": "#CCCCCC", "x": 48, "y": 44, }, { "type": "text", "text": "歌词本", "font": "30px Arial", "color": "#FFFFFF", "x": 168, "y": 75, "shadow": { "color": "#000", "offsetX": 2, "offsetY": 2, "blur": 2 } }, { "type": "image", "url": "http://res.mayday5.me/wxapp/wxavatar/tmp/medal_concerts_1.png", "width": 300, "x": "center", "y": 361 }, { "type": "text", "text": "一生活一场 五月天", "font": "38px Arial", "color": "#FFFFFF", "x": "center", "y": 838, "shadow": { "color": "#000", "offsetX": 2, "offsetY": 2, "blur": 2 } }, { "type": "text", "text": "北京6场,郑州2场,登船,上班,听到你想听的歌了吗?", "font": "24px Arial", "color": "#FFFFFF", "x": "center", "y": 888, "shadow": { "color": "#000", "offsetX": 2, "offsetY": 2, "blur": 2 } }, { "type": "rect", "width": 750, "height": 193, "fillStyle": "#FFFFFF", "x": 0, "y": "bottom" }, { "type": "image", "url": "http://res.mayday5.me/wxapp/wxavatar/tmp/wxapp_code.jpg", "width": 117, "height": 117, "x": 47, "y": 1180 }, { "type": "text", "text": "长按识别小程序二维码", "font": "26px Arial", "color": "#858687", "x": 192, "y": 1202 }, { "type": "text", "text": "加入五月天 永远不会太迟", "font": "18px Arial", "color": "#A4A5A6", "x": 192, "y": 1249 }] } [代码] 问题反馈 有什么问题可以直接提issue
2019-06-29 - 小程序开发 · 一个大二学生的成长之旅
一、初窥门径 大一那年,懵懵懂懂的入学,每天上课学习,下课游戏,奔波于各式活动之内,游走于各大社团之中。直到那天,看到了首届高校微信小程序应用开发赛,心头一动,点滴想法悄然酝酿而生。 二、出师未捷 石楠轩606宿舍内,几个大小伙子讨论得热火朝天: “我觉得可以,现在学校内还没有这样的小程序,如果做好了,肯定能迅速占领市场。” “没错,而且我们还能借着比赛的机会学一学小程序开发,有利无害呀” … … 当晚我们便着手报名,名曰“南柘”,将楠字左边的木嫁接到石字上,隐喻我们住在石楠。 然后… 就没有然后了… 稚嫩的我们终究没有坚持完比赛,甚至连个像样的模块都没搭建出来。 当时我们都是小白,不懂html、不懂js,更别提后台了。比赛开始后,我找了微信小程序的教学视频来看,发现讲得太泛,并不适合我这种初学者。我迫切需要的是手把手的教学视频或书籍,然而,当时自身底子太薄,学起来很慢,而队友们则忙于他事,连教学视频都没看过几眼。 自然而然的,我们默认了失败,这场尚未开战便宣告失败的战役,仿若一根耻辱柱,时时刻刻提醒我,梦开始的地方,不是做梦开始的地方。 三、沉淀 大一下学期,我成功转专业到计算机学院,开始恶补相关知识; 大二上学期,学了数据结构、算法、数据库,还写了个APP; 虽然囫囵吞枣,但也力求搞懂重点,尤其经典的算法。 随后机缘巧合下接触了Django后端开发,感谢这次开发经历,我对于前后端交互有了一个直观的感觉。第二届小程序开赛前,小程序开发于我而言不再是一个遥不可及的梦。 四、再战 去年一个队伍三个人,今年有些变化,一个队伍可以四个人,得益于此,我再次面对小程序比赛。好友薯饼邀请我加入他的队伍,已经有一个后端,一个UI;后来经过讨论,我和薯饼一起担任前端开发。 任何成果的取得,都不是轻而易举的。 1. 前期准备 我们开始讨论要做什么小程序,脑暴,调研,跟已有的校内小程序对比分析,最终我们决定了我们的作品 —— WritByU(writ 取自古英语,并非 write 的错别字) WritByU 是针对校园用户进行信息分享传播和问答的一个小社区,用户既可以以文 字、图片的形式实现信息的即时分享和传播互动,也可以围绕某一话题进行提问、答疑 以及讨论。 甚至于,我还用PPT做了简单的界面,现在想想确实好笑。 [图片] 随后便是确定技术路线,几经周转,也找到了一个高颜值的开源UI库:ColorUi,虽然没有文档用起来稍有不适,只能翻看源码使用。 [图片] 2. 爆肝开发 前期开发并不顺畅,毕竟万事开头难,一开始使用 ColorUi 就遇到了不少问题,因为没有文档,之前也没使用过 Component,为了解决问题,我找到了 ColorUi 的开发者,进行了交流群,虽然没有直接解决我的问题,但看群里大佬的交流却给我提供了不少思路,受益匪浅。 [图片] 此外,前期开发也比较容易拖沓,心里总想着不急,还有时间,(这为我们后面的通宵爆肝埋下了伏笔)有时间也是慢悠悠的写,或者把时间分配给了其他事情。 一些技术难点:(对于初学者来说想实现这样的小功能并不容易,但是耐下性子多看看别人的源码就可以解决。) 【富文本编辑器】 一开始写这个小程序的时候还没有出 editor,当时想着实在找不到可用的组件就手撸吧,结果还真的没找到(捂脸)。 最后勉强实现了图文混排,当然远不如现在的editor强大,结果富文本编辑器做得差不多的时候 editor发布了(求心理阴影面积) 当然愉快的尝鲜,结果发现 editor 的文档,也太简陋了吧,无奈之下翻看源码,折腾了半天总算搞定初始化、获取内容、更新内容等方法。 编辑框如下图,上方是 editor 强大的控件板,我们保留了一部分常用的功能, 下方加入了插入封面和标题(input),分享内容则使用了 editor 。 [图片] 【瀑布流】 广场页面为了实现瀑布流,我上网找了不少例程和方法,无奈没有可以直接用的轮子,理清了demo的思路就自己操刀开始干。 [图片] 渲染层:使用 leftData 和 rightData,分左右两边渲染。 [图片] 逻辑层: 本markdown编辑框的代码解析似乎出了点问题,输入左箭头会变成 <,如下所示,因此这里还是截图。 [代码]for (let index = 0; index < array.length; index++) [代码] [图片] 中期开发过程则比较艰辛,有段时间课内作业很多,大家都在今天赶这个实验报告,明天赶那个实验报告,大家很难凑在一起开发,童心,我们的后台小伙伴只好自行吭哧吭哧写接口,后续也因为一些逻辑没有提前沟通好修改了不少接口。 我们的API文档: [图片] 3. 提交作品 5.30晚,BUG基本都改完了,队友们也在进一步完善文档,凌晨四点多,我给小程序的使用方法录屏后,开始配音,剪辑视频,果然也是个技术活,几个小时后,终于捣鼓完了,不禁松了口气。打包代码、上传体验版,提交作品,这一阶段结束。几个人吃了个早餐上课的准备上课,我则回宿舍安心睡会觉。 [图片] 五、给初学者的技术建议 掌握基础知识:html、css、JavaScript,写几个小页面快速上手 看微信小程序的API文档,setData、bindtap,了解数据渲染、方法绑定; 了解 ajax 请求,找懂后端的师兄师姐或者同学开个简单的get请求接口给你练习(当然自己开个后端服务就更好了,比如Django什么的,上手方便,也可以用小程序云开发功能),然后自己阅读微信小程序API文档,试着使用wx.request。 此时已经基本掌握写一个单页面小程序的能力了,紧接着可以找一些开源的UI库,美化自己的界面,了解小程序页面、组件的生命周期找一些demo进心学习。 书法学习有临与摹,临,是照着原作写;摹,是用薄纸蒙在原作上面写。写代码也一样给,建议掌握必要的基础知识后,通读 demo 源码,了解大概思路,然后自行临写,而非对着别人的 demo 一行一行的摹,如此,一开始虽更费劲,但效果显著。 以上,是个人的一点小建议,如有更好的想法,可以在下方留言哦,欢迎交流~
2019-06-28 - 【技巧】swiper仿tab切换
大家好,上次给大家分享了swiper多图片的解决方案:https://developers.weixin.qq.com/community/develop/article/doc/0008aa4fdbce08405c288f37951813 今天再给大家分享一个关于swiper的小技巧,利用swiper仿tab切换。 相信大家在app或浏览器上阅读新闻时,比如今日头条,会有这样一个场景,左右滑动的时候可以切换不同栏目,体验非常好,但是小程序好像没有提供相关组件,如果想实现这种效果该怎么做呢今天就给大家介绍一下在小程序里是怎么实现的。 首先先看下效果 [图片] 实现原理很简单,利用小程序swiper再配合scroll-view就能实现,不过这里面有几点需要注意一下: 1.scroll-view一定要给一个高度,不然会有问题; 2.切换的时候只显示当前的swiper-item里的内容,其它swiper-item里的内容可以先隐藏掉,这是因为如果你的swiper-item里的图片太多的话可能会造成页面回收,因为新闻列表大多是图文列表,而tab经常是不止两个的,可能是7、8个或更多,如果每个tab都显示的话到时上拉加载页面会非常庞大,所以这里我建议不用显示的内容先隐藏,记住是swiper-item里的内容不是swiper-item,到时切换回来时再重新渲染,如果你要保存滚动的位置还要做其它的一些处理,这里就不仔细讲解了; 3.这里适用的是整个页面都是tab切换的,如果只是在页面的某处实现tab切换,还要考虑高度的问题,加载数据的时候根据数据个数长度来计算高度,每次加载数据都要计算高度,切换到不同的tab也是,这部分比较麻烦,因为要计算,不过并不难,只要 计算正确的话是没有问题的; 大概就是这样,基本实现思路,大家可以根据这个思路去拓展,在上面加上自己的功能,over! 代码片段:https://developers.weixin.qq.com/s/89OO1smX736d
2019-02-26 - 如何监听小程序中的手势事件(缩放、双击、长按、滑动、拖拽)
mina-touch [图片] [代码]mina-touch[代码],一个方便、轻量的 小程序 手势事件监听库 事件库部分逻辑参考[代码]alloyFinger[代码],在此做出声明和感谢 change log: 2019.03.10 优化监听和绘制逻辑,动画不卡顿 2019.03.12 修复第二次之后缩放闪烁的 bug,pinch 添加 singleZoom 参数 2020.12.13 更名 mina-touch 2020.12.27 上传 npm 库;优化使用方式;优化 README 支持的事件 支持 pinch 缩放 支持 rotate 旋转 支持 pressMove 拖拽 支持 doubleTap 双击 支持 swipe 滑动 支持 longTap 长按 支持 tap 按 支持 singleTap 单击 扫码体验 [图片] demo 展示 demo1:监听 pressMove 拖拽 手势 查看 demo 代码 [图片] [图片] demo2: 监听 pinch 缩放 和 rotate 旋转 手势 (已优化动画卡顿 bug) 查看 demo 代码 [图片] [图片] demo3: 测试监听双击事件 查看 demo 代码 [图片] [图片] demo4: 测试监听长按事件 查看 demo 代码 [图片] [图片] demo 代码 demo 代码地址 mina-tools-client/mina-touch 使用方法 大致可以分为 4 步: npm 安装 mina-touch,开发工具构建 npm 引入 mina-touch onload 实例化 mina-touch wxml 绑定实例 命令行 [代码]npm install mina-touch[代码] 安装完成后,开发工具构建 npm *.js [代码]import MinaTouch from 'mina-touch'; // 1. 引入mina-touch Page({ onLoad: function (options) { // 2. onload实例化mina-touch //会创建this.touch1指向实例对象 new MinaTouch(this, 'touch1', { // 监听事件的回调:multipointStart,doubleTap,longTap,pinch,pressMove,swipe等等 // 具体使用和参数请查看github-README(底部有github地址 }); }, }); [代码] NOTE: 多类型事件监听触发 setData 时,建议把数据合并,在 touchMove 中一起进行 setData ,以减少短时内多次 setData 引起的动画延迟和卡顿(参考 demo2) *.wxml 在 view 上绑定事件并对应: [代码]<view catchtouchstart="touch1.start" catchtouchmove="touch1.move" catchtouchend="touch1.end" catchtouchcancel="touch1.cancel" > </view> <!-- touchstart -> 实例对象名.start touchmove -> 实例对象名.move touchend -> 实例对象名.end touchcancel -> 实例对象名.cancel --> [代码] NOTE: 如果不影响业务,建议使用 catch 捕获事件,否则易造成监听动画卡顿(参考 demo2) 以上简单几步即可使用 mina-touch 手势库 😊😊😊 具体使用和参数请查看Github https://github.com/Yrobot/mina-touch 如果喜欢mina-touch的话,记得在github点个start哦!🌟🌟🌟
2021-06-24 - 小程序实现看一看视频滑动切换
最终效果 由于很多人不知道看一看还有个视频功能,所以这里先让大家看下我们最终要完成的效果。 它的入口在 发现 -> 看一看 -> 精选 -> 随便找个视频点进去即可。 [图片] 初步想法 由效果可以看出,其实就是需要监听视频的滚动,当超出可视区范围多少px,就切换到下一个视频。 要实现这个功能,大多数人的想法都是:监听scroll 事件后,在调用目标元素的getBoundingClientRect()方法,得到它对应于视图的坐标,再判断是否在可视区域之内,然后切换视频。 缺点 但是这样做的缺点是:调用目标元素的getBoundingClientRect 是会触发重排的,尤其是元素一多起来,调用所有元素的getBoundingClientRect得到信息在进行判断所以很容易造成性能问题。而且这种切换计算的逻辑会写的非常复杂,可以自行脑补一下。 所以我们要换个思维,不要通过监听scoll事件去计算目标元素距离顶部或者底部距离。而应该是直接监听当前目标元素是否还在可视区域内。当离开可视区域的时候,切换到下一个。 整理完大致思路之后,终于要开搞了。 [图片] 实现 前面已经分析了要通过监听当前目标元素是否还在可视区域内来做切换的动作,那么有什么API是可以用来做这件事的呢? 答案是: IntersectionObserver API 这个API是用来观察目标元素与指定元素交集的变化。当交集 < 0 的时候,说明不在指定元素区域内。当目标元素进入或者退出指定元素的时候,会执行相应的回调函数。所以我们可以通过这个API注册一个回调函数用于切换视频。 在小程序里,同样提供了这个API,是IntersectionObserver。 有了这个API,就可以开始干活了。由于我们这个区域是一个滚动区域,所以我用了scoll-view。 index.wxml 文件 [代码]<scroll-view> <view wx:for="{{ videos }}" wx:for-index="idx" wx:for-item="videoItem"> <!-- <view class="{{ currentPlayVideoIndex === idx ? 'active test' : 'test'}}" data-index="{{ idx }}" id="{{ videoItem.video_id }}"> {{ idx }}dddd</view> --> <span class="{{ currentPlayVideoIndex === idx ? 'active' : ''}}">{{ idx }}ddddddd </span> <video id="{{ videoItem.video_id }}" data-index="{{ idx }}" preload src="http://wxsnsdy.tc.qq.com/105/20210/snsdyvideodownload?filekey=30280201010421301f0201690402534804102ca905ce620b1241b726bc41dcff44e00204012882540400&bizid=1023&hy=SH&fileparam=302c020101042530230204136ffd93020457e3c4ff02024ef202031e8d7f02030f42400204045a320a0201000400" class='video-item' muted controls> </video> </view> </scroll-view> [代码] index.wxss [代码].video-item { height: 450px; } .test { width: 100%; height: 450px; border: 1px solid red; padding: 30px; } .active { color: pink; } [代码] [代码]Page({ /** * 页面的初始数据 */ data: { videos: [{ video_id: 'mpVideo0', url: 'http://wxsnsdy.tc.qq.com/105/20210/snsdyvideodownload?filekey=30280201010421301f0201690402534804102ca905ce620b1241b726bc41dcff44e00204012882540400&bizid=1023&hy=SH&fileparam=302c020101042530230204136ffd93020457e3c4ff02024ef202031e8d7f02030f42400204045a320a0201000400', }, { video_id: 'mpVideo1', url: 'http://mpvideo.qpic.cn/tjg_2394158861_50000_01730a9db3924ffa98201662d51615ed.f10002.mp4?dis_k=23639703f249e3c59cf674369cfcac86&dis_t=1562297977', }, { video_id: 'mpVideo2', url: 'http://mpvideo.qpic.cn/tjg_2394158861_50000_01730a9db3924ffa98201662d51615ed.f10002.mp4?dis_k=23639703f249e3c59cf674369cfcac86&dis_t=1562297977', }, { video_id: 'mpVideo3', url: 'http://mpvideo.qpic.cn/tjg_2394158861_50000_01730a9db3924ffa98201662d51615ed.f10002.mp4?dis_k=23639703f249e3c59cf674369cfcac86&dis_t=1562297977', }, { video_id: 'mpVideo4', url: 'http://mpvideo.qpic.cn/tjg_2394158861_50000_01730a9db3924ffa98201662d51615ed.f10002.mp4?dis_k=23639703f249e3c59cf674369cfcac86&dis_t=1562297977', }, { video_id: 'mpVideo5', url: 'http://mpvideo.qpic.cn/tjg_2394158861_50000_01730a9db3924ffa98201662d51615ed.f10002.mp4?dis_k=23639703f249e3c59cf674369cfcac86&dis_t=1562297977', }, { video_id: 'mpVideo6', url: 'http://mpvideo.qpic.cn/tjg_2394158861_50000_01730a9db3924ffa98201662d51615ed.f10002.mp4?dis_k=23639703f249e3c59cf674369cfcac86&dis_t=1562297977', }, { video_id: 'mpVideo7', url: 'http://mpvideo.qpic.cn/tjg_2394158861_50000_01730a9db3924ffa98201662d51615ed.f10002.mp4?dis_k=23639703f249e3c59cf674369cfcac86&dis_t=1562297977', }], currentPlayVideoIndex: 0, isActive: true }, /** * 生命周期函数--监听页面加载 */ onLoad: function (options) { // onLoad 的时候立刻调用handleVideoScroll // 对视频进行监听 this.handleVideoScroll(); // cgi请求,用于获取videos 的数据,由于是demo演示,我直接写死了videos... }, /** * 页面上拉触底事件的处理函数 */ onReachBottom: function () { }, controlVideos: function (res) { console.log('调用controvideos', res); }, handleVideoScroll: function () { const currentId = this.data.videos[this.data.currentPlayVideoIndex].video_id; // 关键代码 // relativeToViewport 这里指定对比的就是viewport,viewport的意思就是document中的可视区域 this.observerObj = wx.createIntersectionObserver().relativeToViewport(); console.log('listen ' + currentId); // 监听目标视频跟viewport相交区域的变化 this.observerObj.observe(`#${currentId}`, this.controlVideos); } }) [代码] copy上面的代码进入小程序,你就会看到这样一个界面。 [图片] 其中外面的一圈表示的是viewPort,里面一层就是我们现在正在监听的视频,我用右上角的粉色字体来标记了,它的回调函数是controlVideos。当目标视频进入或者退出viewport的时候,controlVideos就会执行。 onLoad的时候执行了handleVideoScroll,这时候开始对目标视频进行监听,此时目标元素在viewport内,所以会调用controVideos,打印出相关信息。 [图片] 其他的字段先不说,其中的intersectionRatio表示了他们相交的比例。其中1表示完全在viewport内,0表示不在viewport内。 如果我持续去滚动第一个视频,直到它看不到了,就会看到控制台打印出 [图片] 这时候的intersectionRatio = 0,代表已经不在viewport内了,所以我们就可以将currentPlayIndex 切换到下一个了。 切换代码如下: [代码] controlVideos: function (res) { const { currentPlayVideoIndex } = this.data; console.log('当前currentIndex', currentPlayVideoIndex) const currentId = this.data.videos[currentPlayVideoIndex].video_id; if (res && res.intersectionRatio > 0) { // 视频在可视区域内,播放视频 wx.createVideoContext(currentId).play(); console.log("play" + currentPlayVideoIndex) } else { // 需要切换视频的时候,将当前视频暂停播放 // 并且通过handleVideoScroll 来播放下一个视频 wx.createVideoContext(currentId).pause(); // 切换到下一个视频 this.setData({ 'currentPlayVideoIndex': currentPlayVideoIndex + 1 }, () => { // 注意切换完成之后,还需要在调用handleVideoScroll 来对下一个视频进行绑定 this.handleVideoScroll(); }); } }, [代码] 到这一步,应该就可以看到这样的向下切换的效果了。 [图片] 但是,我们现在只是做下向下滚动的切换。那么向上的呢?要做向上滑动的切换,首先要知道视频是在向下还是向上滑动。这里有个字段可以帮助我们识别:boundingClientRect 。 它表示的是目标元素相对与viewport的节点信息。当视频向上滚动的时候,它距离viewport的top值为负,向下滚动的时候,为正值。 [图片] 有了这个字段,我们就可以通过判断向上还是向下的滚动,来切换视频了。 [代码] controlVideos: function (res) { const { currentPlayVideoIndex } = this.data; console.log('当前currentIndex', currentPlayVideoIndex) const currentId = this.data.videos[currentPlayVideoIndex].video_id; if (res && res.intersectionRatio > 0) { // 视频在可视区域内,播放视频 wx.createVideoContext(currentId).play(); console.log("play" + currentPlayVideoIndex) } else { // 需要切换视频的时候,将当前视频暂停播放,并且通过handleVideoScroll 来播放下一个视频 wx.createVideoContext(currentId).pause(); // 当top < 0的时候,说明是在向上滑动,这时候currentPlayVideoIndex 需要加1 if (res.boundingClientRect.top < 0) { if (currentPlayVideoIndex < this.data.videos.length - 1) { this.setData({ 'currentPlayVideoIndex': currentPlayVideoIndex + 1 }, () => { // 同时解绑第一个视频,保证同一个时间只监听一个视频 this.observerObj.disconnect(); this.handleVideoScroll(); }); } } else { // 当top > 0的时候,说明是在向下滑动,这时候currentPlayVideoIndex 需要减1 if (currentPlayVideoIndex - 1 < 0) { return; } this.setData({ 'currentPlayVideoIndex': this.data.currentPlayVideoIndex - 1 }, () => { this.observerObj.disconnect(); this.handleVideoScroll(); }) } }, [代码] 但是我们的产品在体验的过程中,会提出并不是完全看不见了才去切换,可能想要还剩个150px就切换了,所以我这里要对viewport调整一下 [代码] this.observerObj = wx.createIntersectionObserver().relativeToViewport({ top: -300, bottom: -300 }); [代码] 完成之后,你就可以缓缓的滑动你的视频,实现视频切换的效果了。可以看到当视频差不多被遮住不到一半,就开始切换了。 [图片] 总结 整个过程其实就是好好利用了IntersectionObserver这个API而已。当然现在只是一个非常简单的实现,性能问题,以及快读滑动的情况都无法应对,我们下一篇在接着~。
2019-08-15 - 功能- 选项卡, 要求可左右滑动可点击切换,现象,滑动会卡掉帧 实现:按照
功能- 选项卡, 要求可左右滑动可点击切换,现象,滑动会卡掉帧 实现:按照bindchange来对currentTab进行setData操作,没错,官网的确有说过尽量别这样,但是现在当需要这样的功能时,如何让体验更好
2018-11-07 - 如何使用scroll-view制作左右滚动导航条效果
最新:2020/06/13。修改为scroll-view与swiper联动效果,新增下拉刷新以及上拉加载效果。。具体效果查看代码片段,以下文章内容和就不改了 刚刚在社区里看到 有老哥在问如何做滚动的导航栏。这里简单给他写了个代码片段,需要的大哥拿去随便改改,先看效果图: [图片] 代码如下: wxml [代码]<scroll-view class="scroll-wrapper" scroll-x scroll-with-animation="true" scroll-into-view="item{{currentTab < 4 ? 0 : currentTab - 3}}" > <view class="navigate-item" id="item{{index}}" wx:for="{{taskList}}" wx:key="{{index}}" data-index="{{index}}" bindtap="handleClick"> <view class="names {{currentTab === index ? 'active' : ''}}">{{item.name}}</view> <view class="currtline {{currentTab === index ? 'active' : ''}}"></view> </view> </scroll-view> [代码] wxss [代码].scroll-wrapper { white-space: nowrap; -webkit-overflow-scrolling: touch; background: #FFF; height: 90rpx; padding: 0 32rpx; box-sizing: border-box; } ::-webkit-scrollbar { width: 0; height: 0; color: transparent; } .navigate-item { display: inline-block; text-align: center; height: 90rpx; line-height: 90rpx; margin: 0 16rpx; } .names { font-size: 28rpx; color: #3c3c3c; } .names.active { color: #00cc88; font-weight: bold; font-size: 34rpx; } .currtline { margin: -8rpx auto 0 auto; width: 100rpx; height: 8rpx; border-radius: 4rpx; } .currtline.active { background: #47CD88; transition: all .3s; } [代码] JS [代码]const app = getApp() Page({ data: { currentTab: 0, taskList: [{ name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, { name: '有趣好玩' }, ] }, onLoad() { }, handleClick(e) { let currentTab = e.currentTarget.dataset.index this.setData({ currentTab }) }, }) [代码] 最后奉上代码片段: https://developers.weixin.qq.com/s/nkyp64mN7fim
2020-06-13 - 小程序模拟器在笔记本上input无法获取焦点,无法输入
用最新版的微信开发者工具开发小程序。在PC上模拟器调试正常,但是在surface pro 4 和其他一般笔记本上 鼠标点击input输入框时 无法获取焦点。点击后焦点会自动消失导致无法输入内容。如下页面 [图片]
2017-10-11