GitHub帐号
基本介绍onShow、onLoad与onReady都是小程序页面生命周期函数。 onLoad 在页面加载时调用,仅一次; onShow页面显示/切入前台时触发,两个生命周期非阻塞式调用。 onReady 是页面初始化数据已经完成后调用的,并不意味着onLoad和onShow执行完毕。 调用顺序是onLoad > onShow > onReady 根据对应的执行机制,我们预期有三种执行的逻辑 A. 页面每次出现都会执行 从其他页面返回手机锁屏唤醒,重新看到小程序页面把当前小程序页面重写切换到前台(多任务)B. 页面加载后只需执行一次(页面第一次载入) C. 只在页面非第一次执行时才执行(A情况的子集,页面非第一次展示时) 需求与问题逻辑1: 因为onLoad和onShow是非阻塞执行的,当我们有一个这样的需求:页面载入执行A方法,页面展示执行B、C、D方法时,A需要在BCD之前执行,此时把A放在onLoad中,BCD放在onShow中就无法实现需求 逻辑2: 还有一种需求是:页面第一次执行A,非第一次执行R-A,这里onLoad和onShow并没有非第一次的逻辑,需要手动判断。 一种实践方法下面是纯粹使用onShow代替onLoad,完成所有逻辑的示例,保证了业务逻辑的执行顺序可控。 options获取使用其他方式代替。 为了保持onShow中逻辑的清晰性,尽量使用EventChannel去替代原本onShow+globalData的逻辑。 data:{ first: true async onShow(){ //代替onLoad中的options的获取 const pages = getCurrentPages(); const currentPage = pages[pages.length - 1]; const options = currentPage.options; this.funD() // C2 页面每次都调用的逻辑 if(this.data.first){ this.data.first = false; await this.funA(); //A 仅在页面初次调用的逻辑(按需是否阻塞调用) }else{ await this.funB(); //B 仅在页面非初次时调用的逻辑 await this.funC(); //C1 页面每次都调用的逻辑 另外一种使用实践data:{ first: true onShow(){ this.funD() //页面每次都调用的逻辑(仅非阻塞) if(!this.data.first){ this.funC() //仅在页面非初次时调用的逻辑 await this.funE() //页面每次都调用的逻辑(可阻塞,可非阻塞) onLoad(){ //仅在页面初次调用的逻辑 this.funA(); await this.funB(); onReady(){ this.data.first = false; 如有错误,恳请指出。
小程序开发常用的几个NPM包 ~ 1)js-base64 场景 平时在做支付的时候会做一下加解密的工作,用于数据的验证用 [图片] ~ 示例 [图片] ~ 2)jsencrypt 场景 目前的应用场景是在用户注册或登录的时候,用公钥对密码进行加密,再去传给后台,后台用私钥对加密的内容进行解密,然后进行密码校验或者保存到数据库。 [图片] ~ 示例 [图片] ~ 3)urijs 场景 日常操作组装链接使用 [图片] ~ ~ 示例 buildQuery [图片] ~
需求:将文字转为语音并播放,文字内容有电话格式1234-12234343和时间格式9:00-18:30,类似如下字符串:"我们上班时间:9:00-18:30,电话是1234-12234343" 实现方法:用微信的同声传译插件。 遇到问题:同声传译插件会将电话号码的1234读出一千两百三十四,听起来很奇怪有木有?=<=。 解决方法:所以我想着将电话号码之间加上空格,写个正则(/\d/g," $&"),数字是一个一个的读了。 又遇到问题:时间9:00-18:30被分割成了 9: 0 0- 1 8: 3 0,转成语音后就是九零零一八三零,也很奇怪有木有?=<=。 再次解决方法:将时间格式写个正则(/(\d+):(\d+)-(\d+):(\d+)/g,"$1点$2分到$3点$4分"),简直完美,时间格式完美读了出来^-^ 又又遇到问题:处理时间的正则和电话的冲突了呀,那我电话的空格怎么加呢? 再再次解决方法:这个解决方法我研究了1天!终于写出来了,还用上了断言呢~ .replace(/(\d+):(\d+)-(\d+):(\d+)/g,"$1点$2分到$3点$4分").replace(/(?<!到)(?<!点)\d(?!点)(?!到)(?!分)/g, ' $&'),一共花了2天时间搞得正则,终于啊,成功了!迫不及待用手机扫开发版试试,听听那动人的语音结果。 又又又遇到问题:结果……手机打开一片空白,查了下微信社区,555,在ios上不支持,简直晴天霹雳,看下图官方的回复…… [图片] 再再再次解决方法:再次查社区,看到下方正义之光的回答,十分感谢这位卢霄霄同志!! [图片] 我的终极解决方案: /** * @param {传入的原始字符串} str dealTextToVoice(str) { // 将时间格式更换为中文 str = str.replace(/(\d+):(\d+)-(\d+):(\d+)/g, "$1点" + "$2分" + "到$3点" + "$4分") let patt = new RegExp("\\d{4}\.", "g") let result // 将3位以上的数字加空格,避免语音读出计数 while ((result = patt.exec(str)) != null) { let newNum = result[0].replace(/\d/g, '$& ') str = str.replace(result[0], newNum) return str 至此,完美解决~
UI组件库合集,大家有遇到好的组件库,欢迎留言评论然后加入到文档里。 第一款: 官方WeUI组件库,地址 https://developers.weixin.qq.com/miniprogram/dev/extended/weui/ 预览码: [图片] 第二款: ColorUI:地址 https://github.com/weilanwl/ColorUI 预览码: [图片] 第三款: vantUI(又名:ZanUI):地址 https://youzan.github.io/vant-weapp/#/intro 预览码: [图片] 第四款: MinUI: 地址 https://meili.github.io/min/docs/minui/index.html 预览码: [图片] 第五款: iview-weapp:地址 https://weapp.iviewui.com/docs/guide/start 预览码: [图片] 第六款: WXRUI:暂无地址 预览码: [图片] 第七款: WuxUI:地址https://www.wuxui.com/#/introduce 预览码: [图片] 第八款: WussUI:地址 https://phonycode.github.io/wuss-weapp/quickstart.html 预览码: [图片] 第九款: TouchUI:地址 https://github.com/uileader/touchwx 预览码: [图片] 第十款: Hello UniApp: 地址 https://m3w.cn/uniapp 预览码: [图片] 第十一款: TaroUI:地址 https://taro-ui.jd.com/#/docs/introduction 预览码: [图片] 第十二款: Thor UI: 地址 https://thorui.cn/doc/ 预览码: [图片] 第十三款: GUI:https://github.com/Gensp/GUI 预览码: [图片] 第十四款: QyUI:暂无地址 预览码: [图片] 第十五款: WxaUI:暂无地址 预览码: [图片] 第十六款: kaiUI: github地址 https://github.com/Chaunjie/kai-ui 组件库文档:https://chaunjie.github.io/kui/dist/#/start 预览码: [图片] 第十七款: YsUI:暂无地址 预览码: [图片] 第十八款: BeeUI:git地址 http://ued.local.17173.com/gitlab/wxc/beeui.git 预览码: [图片] 第十九款: AntUI: 暂无地址 预览码: [图片] 第二十款: BleuUI:暂无地址 预览码: [图片] 第二十一款: uniydUI:暂无地址 预览码: [图片] 第二十二款: RovingUI:暂无地址 预览码: [图片] 第二十三款: DojayUI:暂无地址 预览码: [图片] 第二十四款: SkyUI:暂无地址 预览码: [图片] 第二十五款: YuUI:暂无地址 预览码: [图片] 第二十六款: wePyUI:暂无地址 预览码: [图片] 第二十七款: WXDUI:暂无地址 预览码: [图片] 第二十八款: XviewUI:暂无地址 预览码: [图片] 第二十九款: MinaUI:暂无地址 预览码: [图片] 第三十款: InyUI:暂无地址 预览码: [图片] 第三十一款: easyUI:地址 https://github.com/qq865738120/easyUI 预览码: [图片] 第三十二款 Kbone-UI: 地址 https://wechat-miniprogram.github.io/kboneui/ui/#/ 暂无预览码 第三十三款 VtuUi: 地址 https://github.com/jisida/VtuWeapp 预览码: [图片] 第三十四款 Lin-UI 地址:http://doc.mini.talelin.com/ 预览码: [图片] 第三十五款 GraceUI 地址: http://grace.hcoder.net/ 这个是收费的哦~ 预览码: [图片] 第三十六款 anna-remax-ui npm:https://www.npmjs.com/package/anna-remax-ui/v/1.0.12 anna-remax-ui 地址: https://annasearl.github.io/anna-remax-ui/components/general/button 预览码 [图片] 第三十七款 Olympus UI 地址:暂无 网易严选出品。 预览码 [图片] 第三十八款 AiYunXiaoUI 地址暂无 预览码 [图片] 第三十九款 visionUI npm:https://www.npmjs.com/package/vision-ui 预览码: [图片] 第四十款 AnimaUI(灵动UI) 地址:https://github.com/AnimaUI/wechat-miniprogram 预览码: [图片] 第四十一款 uView 地址:http://uviewui.com/components/quickstart.html 预览码: [图片] 第四十二款 firstUI 地址:https://www.firstui.cn/ 预览码: [图片]
1、昨天开始微信开发者工具无法登录 2、卸载重新安装后,无法自动编译了,手动编译也不好使 3、设置中已勾选保存文件自动编译 但就是不好使 4、先后下载了稳定版、开发版,都不好使 请问是什么问题?能尽快修复吗 没法工作了
背景和需求 众所周知,在微信小程序内,TabBar 页面必须放主包内,这固然是为了用户体验做出的限制,但是也限制了开发者,如果想要实现不同的客户可以定制不同的TabBar页面,而很多页面又是分散到不同分包内的,那我们能选择的方案也就是在所有可作为TabBar页面上放置自定义TabBar组件,而后根据客户的不同配置,展示不同的TabBar 选项,当客户点击Tab时,使用[代码]navigateTo[代码]或[代码]redirectTo[代码]进行切换页面。 但这个方案存在明显的问题,首先如果使用[代码]navigateTo[代码]进行切换,会有很明显的页面切换动画,很容易到达10层页面栈限制(当然这个可以使用无限路由方案进行缓解,但是无限路由是一种万不得已且体验很差的路由方案),且由于页面未进行销毁,内存占用会比较大,容易造成卡顿;如果使用[代码]redirectTo[代码]进行切换,页面节点状态无法保存(如滚动位置),页面数据倒是可以使用全局状态管理库进行保存,但是每次在切换 Tab 都会有明显的数据重新加载的动画效果。 在微信小程序支持分包异步化之前,对于上面的问题一直没有好的解决方案,支持分包异步化之后,我们可以将一些组件放入分包内异步加载,这一定程度上解决了主包过大的问题。同时也让我们看到了希望,我们可以将很多组件放入分包内进行异步加载,主包空间空了出来,可以放更多的页面,但不是所有页面都能放入主包,那还有其他方案吗? 我们想,既然组件能从分包异步加载,那页面可以吗? 我们知道,在微信小程序内,通常都会使用Page进行声明页面,但也可以用Component声明页面,也就是说 Component 声明的组件可以当成页面用,那反过来,Page 声明的页面可以当成组件用吗? 答案是可以,但是当这样使用的时候,页面的生命周期方法不会被执行,且实例对象上不存在options(页面路由参数),route(当前页面路由地址)等数据,那我们就不能愉快地玩耍了吗? 没有页面该有的属性?那我们就拿到实例对象给他补上去! 生命周期方法不执行?那我们就拿到实例对象后自己去调用! 要将现有页面作为组件加载,那我们必须要有一个容器页面,去承载真实页面,在容器页面中去补上已经作为组件的真实页面缺失的属性,在对应的生命周期方法中调用真实页面的生命周期钩子。 我们第一步就需要创建一个容器页面出来,我们可以选择手动创建,也可以自动化创建, 但是已有项目来说,手动创建太费时,且每增加新页面都要修改容器页面代码,故此不考虑。 自动化构建容器页面包含如下步骤: 读取 app.json,获取所有分包页面路径 读取分包页面对应的json文件,将其中内容记录到 [代码]tab-bar-page-config.js[代码] 中,因为我们需要在运行时读取真实页面的标题,背景色等信息,而微信小程序不支持从js中读取json文件,故需要将json内容提前读取出来,为了减少数据量,记录时可以将[代码]usingComponents[代码]等无需运行时使用的数据去掉。效果如下: [代码]// tab-bar-page-config.js module.exports = { "/pack_a/page_1": { "navigationBarTitleText": "页面标题", "navigationBarBackgroundColor": "#ffffff" "/pack_b/page_2": { "navigationBarTitleText": "页面标题", "navigationBarBackgroundColor": "#ffffff" /* 其他页面信息 */ 生成 wxml 文件,效果如下: [代码]<pack_a_page_1 id="pack_a_page_1" wx:if="{{ pagePath === '/pack_a/page_1' }}" /> <pack_b_page_2 id="pack_b_page_2" wx:elif="{{ pagePath === '/pack_b/page_2' }}" /> <!-- 其他页面节点 --> 生成容器页面 json 文件,效果如下: [代码]{ "usingComponents": { "pack_a_page_1": "/pack_a/page_1", "pack_b_page_2": "/pack_b/page_2", /* 其他页面 */ "componentPlaceholder": { "pack_a_page_1": "view", "pack_b_page_2": "view", /* 其他页面 */ 编写容器页面 js 逻辑,大体如下: [代码]Page({ data: { // 当前真实页面的路径 pagePath: '', // 真实页面的实例 pageInstance: null, onLoad() { // 根据网络接口返回数据,得到当前容器页面应当显示的真实页面路径 this.setData({ pagePath: someDataFromNet.pagePath, onShow() { this.pageInstance?.onShow?.(); onReady() { this.pageInstance?.onReady?.(); /* 其他生命周期 */ 我们现在面临一个问题,那就是我们是使用分包异步化组件进行加载真实页面,那真实页面是什么时候加载成功的呢?我们知道当组件加载成功后,会执行组件的 [代码]lifetimes.attached[代码] 生命周期, 那既然页面可以当成组件用,那页面是否也有这个生命周期呢?通过查阅文档,我们知道了可以在页面中使用[代码]Behavior[代码], 我们可以通过[代码]Behavior[代码]中定义 [代码]lifetimes.attached[代码],在其中通过 [代码]this.triggerEvent('pageattached')[代码] 去通知容器页面,现在我们的 wxml 需要做一些修改,如下: [代码]<pack_a_page_1 wx:if="{{ pagePath === '/pack_a/page_1' }}" bind:pageattached="onPageAttached" /> <pack_b_page_2 wx:elif="{{ pagePath === '/pack_b/page_2' }}" bind:pageattached="onPageAttached" /> <!-- 其他页面节点 --> js 中也要增加 [代码]onPageAttached[代码] 方法,如下: [代码]Page({ /* 其他生命周期方法 */ onPageAttached() { const route = this.data.pagePath.slice(1); const id = route.replace(/\//g, '_'); const page = this.selectComponent(`#${route}`); page.route = route; page.options = {}; // 补全其他信息, // 调用对应生命周期方法, page.onLoad?.(page.options); // 由于组件可能加载得比较晚,容器页面的 onShow 和 onReady 已经执行过了,这里需要手动执行一遍真实页面的 onShow 和 onReady // 还需要额外做一些判断,避免 onShow 连续执行多遍 page.onShow?.(page.options); page.onReady?.(page.options); 好了,准备工作基本上做完,现在就差给所有页面加上我们之前写的[代码]Behavior[代码]了,如果项目一开始就封装了[代码]BasePage[代码]之类的方法,我们只需要在 BasePage 将这个[代码]Behavior[代码]加到[代码]BasePage[代码]中就行,如果没有的话,可以通过改写[代码]Page[代码]去实现,这里就不举例了。 现在我们按照上面的步骤生成5个容器页面并且加入到 [代码]app.json[代码] 中了,然后开始下一步了, 等等。。。5个容器页面?生成的代码有5份!不行,这样会平白占用很多主包空间的,我们需要做一些优化:将生成5个容器页面优化成生成一个容器组件,然后在5个容器页面内去引用该组件,并修改上面的一些逻辑,这样生成的代码就基本上少了1/5,还是很可观的。 现在还差封装[代码]switchTab[代码]方法了,在其中将[代码]url[代码]替换成容器页面的地址,然后记录该容器页面需要展示的真实页面地址,在容器页面中加载对应的真实页面即可。亦可改写 [代码]wx.switchTab[代码] 去调用我们封装的 [代码]switchTab[代码] 方法,在此就不举例了。 好了,现在基础步骤已完成,就差看效果了。 咦,好像还差某些东西,页面标题呢?怎么不能下拉刷新了?这个页面好像是没有顶部导航栏的呀。 我们一个一个来。 标题及背景颜色等 还记得之前生成的 [代码]tab-bar-page-config.js[代码] 吗?我们在其中记录了页面的一些信息,现在,我们需要在运行时去调用微信API设置标题,颜色等信息。解决。 微信没有提供是否启用下拉刷新的API,所以我们只能给所有容器页面都加上下拉刷新,然后 [代码]onPullDownRefresh[代码] 中判断如果当前真实页面没有启用下拉刷新,就调用[代码]wx.stopPullDownRefresh[代码]停止下拉刷新,否则就调用真实页面的[代码]onPullDownRefresh[代码]钩子。额。。。勉强算解决吧。 顶部导航栏 微信同样没提供是否启用顶部导航栏的API,故只能将5个容器页面分成2类,2个是不带顶部导航的,剩下3个是带顶部导航的,在我们封装的 [代码]switchTab[代码] 中增加判断要跳转的页面是否是包含顶部导航的,分别落到不同的容器页面上即可。解决。 至此,动态Tab页面基本上实现了,还有些样式上的兼容问题,如:某个页面的wxss声明了 [代码]page { backgroud: 'red'; 那容器页面内的所有页面都会被影响,对此我们只能在页面的[代码]wxss[代码]中不使用[代码]标签选择器[代码],实际上在微信开发者工具中,使用[代码]标签选择器[代码]是会报警告的,但是口头约束是没有用的,还是会有人会写,故我们引入了[代码]postcss[代码],编写插件使在构建时将标签选择器去掉,并且报出警告。 至此,功能基本完成,需要做的就是验证哪些功能出现了问题,做出相应的修改。 分包异步化作为一个新出现的特性,还存在一些不稳定,如在开发者工具中,经常出现加载失败的问题,ios 真机调试报错等问题,且要求的最低SDK版本为[代码]2.17.3[代码],要在生产环境中使用还需要做很多的验证工作,也希望微信官方能尽早修改开发者工具中的问题。
快递100“快递跟踪”微信小程序插件上线啦! 帮助所有小程序解决快递物流查询问题,现面向所有第三方小程序、小程序开发服务商(包括个体小程序)免费开放。 目前已经有1000家小程序申请接入了快递100【快递跟踪】插件, 点链接→https://fuwu.weixin.qq.com/service/detail/00008caeab84c07c17dcdabf55b815,立即添加插件(添加时请从电脑端打开链接) 公开数据显示,今年上半年微信小程序数量已超过430万。 随着小程序生态不断发展,越来越多商家和开发者在小程序上建立自有商城。大到京东这样的巨型平台,小到一个公众号、博主自己开的店铺,用户都可以在小程序上下单。业务逐渐壮大后,物流却成为困扰不少商家和开发者的一大难题。 大平台有大量的资金和人力来调配资源,自主开发接入物流公司系统,给顾客及时物流反馈;而对于那些中小店铺的小程序商家们来说,没有足够人力、财力支撑,无法自主开发接入。 近日,中国领先的快递物流信息服务商快递100宣布,正式上线“快递跟踪”小程序功能插件,开放快递物流信息查询模块,允许第三方小程序 免费 接入。 “快递跟踪”小程序插件整合了快递100快递查询能力,支持全球1000+快递物流公司信息查询,对全行业的小程序免费开放接入,包括电商平台、商家、医药寄送、信息查询或本地生活服务平台等 任何有物流查询需求的小程序开发者,为企业、商家、个体小程序赋能。 点链接→https://fuwu.weixin.qq.com/service/detail/00008caeab84c07c17dcdabf55b815,立即添加插件 01 无门槛免费接入 无论是电商商城,还是社群团购、回收类等任何有涉及快递物流环节的小程序,物流信息查询是必须重视的一项服务。卖家是否能提供及时的物流信息更新服务,会影响到用户的二次购买决策。 “快递跟踪”小程序插件,是免费接入。接入插件后,用户只要在小程序内点击快递单号,就可以查看最新物流信息,有效提升用户的购物体验,提高小程序的回访率和复购转化率。 02 原生体验,无第三方跳转 快递100“快递跟踪”插件依托微信小程序生态,第三方小程序接入后无需任何跳转,在自己的小程序内即可直接查看物流信息,简化用户操作流程。 [图片] 接入方式也非常简单快捷,模板化快速接入,无需再次开发,几个小时即可完成接入,大大降低开发和运营成本。 快递100“快递跟踪”插件开放接入,不仅能够帮助小程序开发者降低物流服务的开发门槛和成本,同时也为小程序商家提供了更好服务用户的方式。 03 支持国内外1000+家快递公司物流查询 通过快递100“快递跟踪”小程序插件,支持全网快递物流查询,可查看国内、国际1000+快递物流公司的信息,同时还提供官方客服热线。 “快递跟踪”插件服务稳定,让商家、开发者管理更加方便。 除了常见的电商场景,“快递跟踪”插件同样非常适合有特定物品物流信息查询需求的机构和企业接入 —— 例如医院类公众号,病历档案预约寄出后的进度查询;驾校机构,寄出驾照后的进度查询;校园机构的报到证、档案等资料的快递查询等。 点链接→https://fuwu.weixin.qq.com/service/detail/00008caeab84c07c17dcdabf55b815,立即添加插件 另外,快递100也可提供快递信息推送、实时快递、地图轨迹API等服务,点这里→https://api.kuaidi100.com/ 了解详情 快递100是中国领先的快递物流信息服务商,国家高新技术企业、新基建代表企业。 快递100目前拥有个人注册用户1.6亿,企业客户60万+,日均查询量3亿次,是国内查询量最大的快递物流信息查询平台;年寄件量超8亿单,寄件功能官方合作京东、邮政、德邦、圆通、韵达、DHL、TNT、UPS等多家国内外快递公司。 快递100致力构建中国最大的物流信息服务枢纽,始终秉承开放态度与快递行业共创共赢,为用户、商家、企业提供专业、可靠的服务,实现互联互通互动。 点链接→https://fuwu.weixin.qq.com/service/detail/00008caeab84c07c17dcdabf55b815,立即添加插件
微信小程序动画实现方案 背景 基于商城业务需求背景下,实现加购动画;具体要求如下: 动画落点不准 体验优化,动画先行,每点击一次需要触发一次动画 减少动画掉帧,卡;避免动画延迟,解决加购卡顿问题 基于以上诉求,对微信小程序各种动画实现方案做了简单的对比分析 动画落点不准确 最开始实现方案,就是在dom ready 的时候去获取元素落点,缓存下来,后续复用改落点即可; [代码] onReady() { // 获取落点坐标 this.getBubblePos('#end-point'); 然而在商城当前业务中,这种方案,获取到的坐标频繁出现较大偏差。基于现状,延迟获取落点坐标,出现以下方案(timeout 和 nextTick 都尝试了,依然没能解决问题,于是综合了一下) [代码] onReady() { // 获取落点坐标 this.$nextTick(() => { setTimeout(() => { this.getBubblePos('#end-point'); }, xxxx); [代码]timeout[代码]方案尝试了不同的延迟时间,发现在1s内,依旧会出现 获取到的位置不准确,初步判断跟加购动画落点所在的结算条的条件渲染有关。timeout 解决不了问题。 最终在x大佬的指导下,参考了其他平台的实现方案,采取了点击时获取落点坐标,解决了该问题; [代码] async startAnimation(e) { //点击时 获取落点 并 缓存 await this.setStartPos(`#end-point`); // 开始动画 this.useAnimateApi(0); 快速点击下能连续触发,多个动画并存 需要有多个动画元素,保证快速点击的时候,动画不延迟,并且每次点击都能触发一个动画元素执行动画;动画元素要求能购复用,减少冗余dom 元素; 业务现状: 现有加购按钮跟落点所在的结算条位于不同的组件内;点击时获取到点击位置的坐标,然后需要将动画元素从点击位置 移动到落点; 解决方案: 每个页面初始化 [代码]n[代码] (n=10)个元素隐藏在 页面内部 css 隐藏到页面某个区域;为了回收复用动画元素 用一个队列来维护当前可使用动画元素; [代码] <!-- css 元素2个元素实现 --> <block wx:for="{{transition}}" wx:key="id"> <view class="bubblebox bubblebox_{{index}}" style="{{item.parent}}"> <view class="bubble bubble_{{index}}" bind:transitionend="transitionEnd" data-index="{{index}}" style="{{item.child}}"></view> </view> </block> [代码]// 维护动画元素队列,动画执行完成之后 回收当前动画元素 transitionEnd(e) { console.log(e); // 监听动画结束时间 清除动画 const { index = 0 } = e.currentTarget.dataset || {}; this.data.queue.push(Number(index)); this.data.transition.splice(index, 1, { id: index, parent: '', child: '' }); this.setData({ transition: this.data.transition, 掉帧 卡顿问题 针对该问题,对各个实现方案做了个对比 Note: 商城加购动画 最终实现方案采用 css transition 实现 [代码]wx.createAnimation[代码] [代码] useCreateAnimation(index) { const { x: x1, y: y1 } = this.data.startPos; const { x: x2, y: y2 } = this.data.dropPos; // 1. 移动到 起点 // duration 不能为0 最小为1 this.animation.translate(x1, y1).opacity(1).step({ duration: 1, delay: 0 }); // const parent = this.animation.export(); // // 2. 起点移动到 落点 this.animation.translate(x2, y2).step({ duration: 1000, delay: 1 }); // // // 3. 隐藏气泡 回到起点 this.animation.opacity(0).translate(0, 0).step({ duration: 1, delay: 1000 }); const child = this.animation.export(); this.data.transition.splice(index, 1, { id: index, parent: '', child }); this.setData({ transition: this.data.transition, [代码]this.animate[代码] 从小程序基础库 2.9.0 开始 [代码] useAnimateApi(index) { const { x: x1, y: y1 } = this.data.startPos; const { x: x2, y: y2 } = this.data.dropPos; console.log(`useAnimateApi`, index, x1, y1, x2, y2); // 2. 上 下 移动动画 this.animate(`#bubble_${index}`, [{ left: `${x1}px`, top: `${y1}px`, opacity: 1 }, { translate: [`${x2 - x1}px`, `${y2 - y1}px`] }], 500, () => { console.log('animationEnd'); this.clearAnimation(`#bubble_${index}`, () => {}); this.animationEnd(index); [代码]css3 transition[代码] [代码] useCssAnimate(index) { const { x: x1, y: y1 } = this.data.startPos; const { x: x2, y: y2 } = this.data.dropPos; const parent = `transition: transform 0ms 0ms linear;transform: translate(${x1}px,${y1}px)`; const child = `transition:opacity 0s 0s,transform 2s 0ms linear;transform: translate(${x2 - x1}px,${y2 - y1}px);opacity:1;`; this.data.transition.splice(index, 1, { id: index, parent, child }); this.setData({ transition: this.data.transition, [代码]wxs[代码] 相应事件 小程序双引擎设计,setData 是影响性能的关键因素,wxs 相应事件能减少 setData 次数,有助于提高动画性能; [代码]<wxs module="utils"> var transition = function(e, ownerInstance) { var index = 0 var parent = ownerInstance.selectComponent('#bubblebox_1_'+index) // 返回组件的实例 var child = ownerInstance.selectComponent('#bubble_1_'+index) // 返回组件的实例 var x1 = 300 var y1 =50 var x2 = 50 var y2 = 278 console.log(parent,child) parent.setStyle({ transition: 'transform 0ms 0ms linear', transform: 'translate(300px,50px)' child.setStyle({ transition:'opacity 0s 0s,transform 2s 0ms linear', transform: 'translate(-250px,238px);opacity:1' return false // 不往上冒泡,相当于调用了同时调用了stopPropagation和preventDefault module.exports = { transition:transition </details> [代码]css3 animation[代码] 待尝试,动画生成keyframes 如何绑定到 dom 上? [代码]css transition[代码] 细节实现 分析当前动画元素过程 css 元素从隐藏的位置(fixed到左上角,(0,0))移动到起点,并可见; [代码]transition:opacity 0s 0s,top 0s 0s,left 0s 0s,transform 2s 0ms linear; transform: translate(1px,1px);opacity:1; top:100px; left:100px; 通过top,left 移动元素到起点,并且 通过opacity 控制元素展示出来;执行 translate 动画 利用[代码]transition[代码] 可以同时为不同的 [代码]动画属性[代码] 指定不同的 [代码]duration[代码] [代码]timeFunction[代码] [代码]delay[代码] 动画结束 移除动画元素绑定的 style,动画元素回到起点;动画结束,维护可使用的动画元素队列;监听动画结束事件,回收当前元素入队。 [代码] <view bind:transitionend="transitionEnd"></view> [代码] transitionEnd(e) { console.log(e); const { index = 0 } = e.currentTarget.dataset || {}; // 动画元素 入队 this.data.queue.push(Number(index)); // 监听动画结束时间 清除动画 this.data.transition.splice(index, 1, { id: index, parent: '', child: '' }); this.setData({ transition: this.data.transition, 可以用1层dom,为何用2层dom ? 虽然元素已经脱离了文档流,top left 并不会触发 重绘 引起性能问题;但是不能充分利用css3 硬件加速的能力。 完美解决??? 该方案对 dom性能有较大提高,但是消耗的内存占用也不容小觑。目前商城业务高度复杂,内存占本来就高的情况下,改方案依旧会在内存占用过高的情况下[代码]闪现[代码],过渡态消失的情况。 抛物线方案 该方案 必须使用2层view元素实现 抛物线方案,改方案初步实现后在小程序里面不同机型上 效果差异较大,动画方案回退为直线; 具体步骤: 移动到起点 父级元素 向左边移动 同时子级元素先向上移动指定距离,再向下(时间函数控制曲线斜率) [代码]// 1. 抛物线 const parent = `transition: top 0s 0s, left 0s 0s, opacity 0s 0s, visibility 0s 0s, transform 500ms 0ms ease-in;transform: translateX(${ x2 - x1 }px);left:${x1}px;top:${y1}px;opacity:1;visibility:visible;` const child = `transition: transform 300ms 200ms ease-in, margin-top 200ms 0ms ease-in-out,opacity 0ms 501ms linear,visibility 0ms 501ms linear;margin-top: -66px;transform: translateY(${ y2 - y1 + 66 }px);opacity:0;;` Refer weixin miniprogram wxs 响应事件 浏览器的重绘和回流(Repaint & Reflow)
rxjs-mp 在小程序中使用RxJs。(RxJs for miniprogram) 1. 安装依赖: [代码]npm install --save rxjs-mp[代码] [代码]npm install --save-dev rxjs[代码] 安装完成后使用小程序开发者工具构建npm,然后就可以在项目中使用rxjs了。此外,在vscode里还能享受rxjs的语法提示。 2. 如何使用: 为了演示在小程序中如何使用rxjs,现在举一个用rjxs封装http请求的例子。相对于Promise的封装,rxjs可随时取消已经发出的请求,这一点是Promise很难实现的。我们在utils目录下建一个http.js的文件。 2.1 封装utils/http.js [代码]const Rx = require('rxjs-mp'); * Request 方法 * @param {string} url * @param {any} data * @param {'GET'|'POST'|'PUT'|'DELETE'} method * @param {{header?: object}} options function request(url, data = {}, method = "GET", { header }={}) { return new Rx.Observable(ob => { const requestTask = wx.request({ url, data, method, header: { ...(header || {}) Token: wx.getStorageSync('token') success: res => { if (res.statusCode == 200) { ob.next(res); ob.complete(); } else { ob.error(res); fail: err => { console.error(err); ob.error(err); return () => requestTask.abort(); * GET 方法 * @param {string} url * @param {{header?: object}} options function get(url, options) { return request(url, null, 'GET', options); * POST 方法 * @param {string} url * @param {any} data * @param {{header?: object}} options function post(url, data, options) { return request(url, data, 'POST', options); * PUT 方法 * @param {string} url * @param {any} data * @param {{header?: object}} options function put(url, data, options) { return request(url, data, 'PUT', options); * GET 方法 * @param {string} url * @param {{header?: object}} options function delete_(url, options) { return request(url, null, 'DELETE', options); module.exports = { http: { get, post, put, delete: delete_, request } 2.2 调用封装,pages/your/page.js 管理rxjs订阅可以使用subsink2订阅管理工具(安装:[代码]npm install subsink2[代码]). [代码]const Rx = require('rxjs-mp'); const { SubSink } = require('subsink2'); const http = require('../uitls/http.js'); const subs = new SubSink(); Page({ data: { todoList: [], onLoad() { // 发起请求 this.httpRequest01(); this.httpRequest02(); this.httpRequest03(); httpRequest01() { // 请求1,用subsink的id方法标识请求,可以防重得请求,还可以随时取消 subs.id('sub01').sink = http.get('htts://some-api-url').pipe( Rx.operators.map(res => res && res.data || {}) ).subscribe(res => { console.log(res); }, err => { console.error(err); httpRequest02() { // 请求2,直接把请求加入订阅池 subs.sink = http.post('htts://some-api-url', {}).pipe( Rx.operators.map(res => res && res.data || {}) ).subscribe(res => { console.log(res); }, err => { console.error(err); httpRequest03() { // 请求2,把请求加入订阅池的另一种方法 subs.add(http.get('htts://some-api-url').pipe( Rx.operators.map(res => res && res.data || {}) ).subscribe(res => { console.log(res); }, err => { console.error(err); bindUnSubRequest01() { // 取消request01的请求 subs.id('sub01').unsubscribe(); onUnload() { // 销毁时,取消所有请求 subs.unsubscribe(); 2.3 你还可使用rxjs其它强大的功能 比如消息发布和订阅,rxjs基于流的响应式编程帮你逃出Promise的回调地狱。 [代码]// ================================================ // utils/message.js // ================================================ const Rx = require('rxjs-mp'); module.exports = { onSomeThingChange$: new Rx.Subject(), onOtherThingChange$: new Rx.BehaviorSubject(false), // ================================================ // pages/your/page.js // ================================================ const Rx = require('rxjs-mp'); const { onSomeThingChange$, onOtherThingChange$ } = require('../../utils/message.js'); onSomeThingChange$.pipe( Rx.operators.first() ).subscribe(status=> { console.log(status) // doSomeThing onOtherThingChange$.subscribe(status => { console.log(status) // doSomeThing onSomeThingChange$.next(true); onOtherThingChange$.next(true);
我理想中的结果是一张一张图片鉴黄,把鉴定违规的图片删除,但是现在的结果如图: [图片] //上传图片 onChangeFlockData: async function (e) { let imgUrls = e.detail.all; //上传的图片数组 for (let i = 0; i < imgUrls.length; i++) { await this.imgInfoCheck(imgUrls[i].url) //获取图片信息 imgInfoCheck: async function (url) { wx.getImageInfo({ src: url, }).then(async (res) => { console.log("图片信息:", res) const imgInfo = res.path; const imgWidth = res.width; const imgHeight = res.height; await this._compressImage(imgInfo, imgWidth, imgHeight); //图片压缩 _compressImage: async function (imgInfo, imgWidth, imgHeight) { const query = wx.createSelectorQuery() query.select('#canvas') .fields({ node: true, size: true .exec(res => { const canvas = res[0].node; const ctx = canvas.getContext('2d'); const dpr = wx.getSystemInfoSync().pixelRatio; const imgW = Math.trunc(imgWidth / dpr); const imgH = Math.trunc(imgW / imgWidth * imgHeight); canvas.width = imgW; canvas.height = imgH; ctx.clearRect(0, 0, imgW, imgH); this.setData({ canvasWidth: imgW, canvasHeight: imgH let imageObj = canvas.createImage(); imageObj.src = imgInfo; imageObj.onload = (res) => { ctx.drawImage(imageObj, 0, 0, imgW, imgH) const cfgSave = { fileType: "jpg", quality: 0.5, width: imgW, height: imgH, destWidth: imgW, destHeight: imgH, canvas: canvas, wx.canvasToTempFilePath({ ...cfgSave, }).then(async res => { let tempUrl = res.tempFilePath console.log("压缩后图片:", res) await this.imgSecCheck(tempUrl) //图片送审 imgSecCheck: async function (tempUrl) { wx.showLoading({ mask: true wx.getFileSystemManager().readFile({ filePath: tempUrl, encoding: "base64", success: function (res) { let imgBuffer = res.data wx.cloud.callFunction({ name: "msgSecCheck", data: { type: 'imgSecCheckBuffer', //以buffer方式上传送审 value: imgBuffer, }).then(res => { console.log("图片检测结果:", res.result.errCode) wx.hideLoading() if (res.result.errCode === 87014) { wx.hideLoading() wx.showToast({ title: '图片含有违法违规内容', icon: 'none' return // this._uploadImg(tempUrl) }).catch(err => { console.log(err) wx.hideLoading() wx.showModal({ title: '提示', content: '图片尺寸过大,请调整图片尺寸', success(res) { if (res.confirm) { console.log('用户点击确定') } else if (res.cancel) { console.log('用户点击取消') fail: err => { console.error(err);
华为mate30 canvas 2d drawImage行为和其他手机不一致 使用canvas 2d, const { pixelRatio } = wx.getSystemInfoSync; canvas.width = res[0].width * pixelRatio; canvas.height = res[0].height * pixelRatio; // 华为mate30机型 dx,dy,dWidth, dHeight需要除掉pixelRatio,才会显示正常 ctx.drawImage(img, 100, 100, cw * dpr, ch * dpr, 0, 0, w / pixelRatio, h / pixelRatio); // 非华为mate30 dx,dy,dWidth, dHeight不需要除掉pixelRatio,就可以显示正常 ctx.drawImage(img, 100, 100, cw * dpr, ch * dpr, -w, 0, w, h);
icebreaker手把手教你定制小程序码 小程序菊花码,相比与普通的二维码,辨识度高,一看就知道拿微信扫。 默认情况下,我们可以自定义生成码的 [代码]参数[代码], [代码]路径[代码], [代码]大小[代码], [代码]自动或手动配置线条颜色[代码],[代码]底色是否为透明[代码] 这些配置项。 然而,这些配置项往往是无法满足我们的定制化需求的。 举个例子,我们需要在不破坏 [代码]小程序码[代码] 可识别性的情况下,把中间的 [代码]Logo[代码] 替换掉,怎么做呢? 接下来就由笔者手把手来教你。 我们先要理清楚这个问题的本质。这个其实就是个 图像处理 问题, 而这个工作服务端和客户端都能做。 于是就有 [代码]2[代码] 个方案: 服务端生码并且缝合出结果的 服务端处理方案。具体怎么做,有兴趣的同学,可以查看笔者的这篇文章 Web 函数自定义镜像实战:构建图象处理函数 服务端生码,客户端缝合方案。这就是本篇文章具体提及的。 注:小程序码一般由服务端调用微信api接口生成 云调用——生码最简便方案 总所周知,微信小程序环境的 [代码]wxacode.getUnlimited[代码] 有 [代码]2[代码] 种生成模式,一种为 [代码]HTTPS 调用[代码] , 一种为 [代码]云调用[代码]。 其中 [代码]云调用[代码] 作为一种场景定制化的 [代码]serverless[代码]解决方案,往往能为我们的开发,带来效率上的提升。我们接下来快速部署一个[代码]getWxacodeUnlimit[代码]函数,来为我们提供测试素材。 [代码]getWxacodeUnlimit/index.js[代码]: [代码]import cloud from 'wx-server-sdk' cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV export async function main (event, context) { const { scene = '', page, width = 430, autoColor, lineColor, isHyaline } = event const result = await cloud.openapi.wxacode.getUnlimited({ scene, page, width, autoColor, lineColor, isHyaline return result // 笔者使用了打包工具, 大家想直接跑,把 esm 转化为 cjs 即可 [代码]getWxacodeUnlimit/config.json[代码]: [代码]{ "permissions": { "openapi": ["wxacode.getUnlimited"] 通过上述 [代码]2[代码] 段代码块,我们的测试函数就部署完成了。 把返回的 [代码]buffer[代码] 转成本地临时图片 [代码]const suffixMap = { 'image/jpeg': 'jpeg', export function getPath(filename = 'tmp', contentType = 'image/jpeg') { return `${wx.env.USER_DATA_PATH}/${filename}.${suffixMap[contentType] || 'jpeg'}` export function writeFile(buff, contentType = 'image/jpeg', filename = 'tmp') { return new Promise((resolve, reject) => { const fsm = wx.getFileSystemManager() const filePath = getPath(filename, contentType) fsm.writeFile({ filePath, data: buff, encoding: 'binary', success() { resolve(filePath) fail(error) { reject(error) // 在需要用到的地方直接 try { loading('生成中') const result = await getQrcode(scene, option) // 云调用封装function return await writeFile(result.buffer, result.contentType, scene) // : string } catch (e) { console.error(e) } finally { loaded() 客户端的图像处理 提到客户端图像处理就不得不提到 [代码]canvas[代码] 这个原生组件了,所以我们只需要通过它,把小程序码中间的 [代码]Logo[代码] 部分,进行 测量和裁剪替换 为我们想自定义的图像就可以了。 这里以默认小程序码大小 [代码]430px * 430px[代码] 为例。(本[代码]case[代码]为了简单易懂,都使用的此分辨率的小程序码,如需求分辨率不同,可按比例进行计算裁剪。) 从图上的标注可知,在 [代码]430px * 430px[代码] 分辨率下,上下左右的边距为 [代码]120px[代码] ,可以算出中间 [代码]Logo圆形[代码] 的直径为 [代码]190px[代码], 半径为 [代码]95px[代码]。 所以接下来就可以轻松愉快的写代码了。 利用canvas 2d实现 1代小程序 api 版本被淘汰了,现在直接使用 type=“2d” 版本,Api文档在 [代码]MDN[代码] 上 前置标签和样式 [代码]<!-- uni-app vue 格式,可自行转化为 wxml 简单的语法转化 `: => {{}}` --> <canvas :class="visible ? '': 'canvas offscreen'" type="2d" id="canvas" :style="{ width:width+'rpx', height:height+'rpx' ></canvas> [代码]// scss .canvas.offscreen{ // 2个 class 选择器,增加优先级 position: absolute; bottom: 0; left: -9999rpx; // 这叫物理离屏渲染,笑~ 注: 不能在 [代码]canvas[代码]上 使用 [代码]hidden[代码] or [代码][代码],它们会导致渲染空白。 核心 js 实现 初始化canvas实例和上下文 初始化 [代码]canvas[代码] 实例和 [代码]ctx[代码] 上下文: [代码]let canvas let ctx {...codes...} onReady(){ .createSelectorQuery() .in(this) // 如果canvas在组件中,则需要加这一行 .select('#canvas') .fields({ node: true, size: true .exec((res) => { if (res[0]) { this.canvas = canvas = res[0].node this.ctx = ctx = canvas.getContext('2d') // 下面可根据设备的 pixelRatio 自行按比例调整,此处为了演示方便,就直接赋值了。 canvas.width = 430 canvas.height = 430 第一次渲染-画布背景 第一次渲染,把小程序码,作为图像传入画布。 [代码]drawBackgroud[代码]: [代码]async drawBackgroud (orginQrcodeUrl) { const [err, res] = await uni.getImageInfo({ // 这里可以是远程地址(需要配置downloadUrl) // 也可以是本地地址(直接返回参数自己) // 甚至 cloud:// 前缀的云存储url也可以哟 src: orginQrcodeUrl if (err) { throw err const { path } = res const img = canvas.createImage() img.src = path await new Promise((resolve, reject) => { img.onload = () => { // 下面这一行,把小程序码,整个铺进画布中! ctx.drawImage(img, 0, 0, canvas.width, canvas.height) resolve() img.onerror = (event) => { reject(event) 第二次渲染-裁剪加填充 第二次渲染,把背景裁剪一个圆,并把图片填充进去。 [代码]drawAvatar[代码]: [代码]async drawAvatar (remoteAvatarUrl) { const [err, res] = await uni.getImageInfo({ // 比如这里我用了云储存里的图像地址 prefix: cloud:// src: remoteAvatarUrl if (err) { throw err const { path } = res const img = canvas.createImage() img.src = path // 测量数据在这里用上了 const offsetX = 120 // x 轴偏移 120px const offsetY = 120 // y 轴偏移 120px const diam = 190 // 圆的直径 (430 - 120* 2) / 2 const radius = diam / 2 // 圆的半径 const borderWidth = 2 // 多加2px来把原先logo的纯色边抹除 const circle = { // 裁剪部分圆的大小属性 x: offsetX + radius, y: offsetY + radius, radius: radius + borderWidth await new Promise((resolve, reject) => { img.onload = () => { ctx.save() // 开始! 把原先中间的Logo干掉! ctx.arc(circle.x, circle.y, circle.radius, 0, Math.PI 2, false) ctx.clip() // 结束 // 开始! 把我们需要的自定义图像,平铺的插入进去! ctx.drawImage( offsetX - borderWidth, offsetY - borderWidth, circle.radius * 2, circle.radius * 2 // 结束 ctx.restore() resolve() img.onerror = (event) => { reject(event) 通过以上几步 ,我们就可以轻松的完成图像处理部分,把中间的默认[代码]Logo[代码]给替换成自定义的图像。 预览及下载到本地 [代码]// 获取 tempFilePath async getImage () { const [err, res] = await uni.canvasToTempFilePath({ canvas if (err) { throw err return res.tempFilePath // 预览 async preview (src) { if (src) { uni.previewImage({ urls: [src] // 保存到相册里 async save (src) { try { // 先授权,后保存 await authorize('scope.writePhotosAlbum') const [err, res] = await uni.saveImageToPhotosAlbum({ filePath: src if (err) { throw err this.$success('保存成功!') } catch (e) { console.error(e) 就这样,客户端生成自定义小程序码的整套解决方案就完成了 或者微信搜索 [代码]程序员名片[代码] 后,维护名片,上传头像,再点击下方 [代码]分享二维码[代码] 按钮,即可预览。 自定义生成转发图片 这篇文章这个案例还算是非常简单的,笔者之前还写过一个 小程序Canvas 2D自定义生成转发图片, 还有自定义分享海报,这些原理上都是大同小异的,一通百通。 wxacode.getUnlimited接口文档
宿主机是ubuntu19.04,虚拟机是virtualbox6,虚拟机系统是win10 64位,开发者工具版本v1.02.1907300。 virtualbox共享了一个目录,虚拟机里面的win10映射成一个网络驱动器z盘。 从z盘导入或者新建项目保存到z盘都报错,项目是小程序项目。 [代码]VM26:1 EISDIR: illegal operation on a directory, [代码][代码]watch[代码] [代码]'Z:/'[代码][代码]Error: EISDIR: illegal operation on a directory, [代码][代码]watch[代码] [代码]'Z:/'[代码][代码] [代码][代码]at FSWatcher.start (internal[代码][代码]/fs/watchers[代码][代码].js:165:26)[代码][代码] [代码][代码]at Object.[代码][代码]watch[代码] [代码](fs.js:1274:11)[代码][代码] [代码][代码]at new FileUtils (C:\Users\dev\AppData\Roaming\Tencent\微信开发者工具\package.nw\core.wxvpkg\8e2026561e71ea67df211489b756510c.js:42:30)[代码][代码] [代码][代码]at C:\Users\dev\AppData\Roaming\Tencent\微信开发者工具\package.nw\core.wxvpkg\d62fc37d7aa6416d5dcc240ba94175cd.js:23:20[代码][代码] [代码][代码]at new Promise (<anonymous>)[代码][代码] [代码][代码]at module.exports (C:\Users\dev\AppData\Roaming\Tencent\微信开发者工具\package.nw\core.wxvpkg\d62fc37d7aa6416d5dcc240ba94175cd.js:20:10)[代码][代码] [代码][代码]at Object.apply (C:\Users\dev\AppData\Roaming\Tencent\微信开发者工具\package.nw\node_modules.wxvpkg\lazyload\lazy-require.js:44:20)[代码][代码] [代码][代码]at module.exports (C:\Users\dev\AppData\Roaming\Tencent\微信开发者工具\package.nw\core.wxvpkg\60e94018e5c42875e658435ea04a006d.js:1:2606)[代码][代码] [代码][代码]at Object.apply (C:\Users\dev\AppData\Roaming\Tencent\微信开发者工具\package.nw\node_modules.wxvpkg\lazyload\lazy-require.js:44:20)[代码][代码] [代码][代码]at module.exports (C:\Users\dev\AppData\Roaming\Tencent\微信开发者工具\package.nw\core.wxvpkg\162bf2ee28b76d3b3d95b685cede4146.js:1:430)[代码] [图片]
需求是需要点击小程序里的某个图片,打开国务院的某个政策页面(比如:http://www.gov.cn/guoqing/2018-03/22/content_5276318.htm) 而我这样写,微信小程序会提示 不支持打开非业务域名https://www.gov.cn,请重新配置。 <web-view id="weburl" src="http://www.gov.cn/guoqing/2018-03/22/content_5276318.htm"></web-view> 如果要实现我描述的需求,请问我该怎么写? (https://www.gov.cn 我没法配置成业务域名,我无法把校验文件放过去)
开发者在开发小程序的时候可能会碰到一些这样的问题: 问题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 接口的更详细使用请参考文档。 希望通过这些小技巧,可以帮助大家顺畅地开发小程序。
作为前端的设计师和工程师,我们用 CSS 去做样式、定位并创建出好看的网站。我们经常用 CSS 去添加页面的运动过渡效果甚至动画,但我们经常做的不过如此。 [代码] 动效是一个有助于访客和用户理解我们设计的强有力工具。这里有些原则能最大限度地应用在我们的工作中。 迪士尼经过基础工作练习的长时间累积,在 1981 年出版的 The Illusion of Life: Disney Animation 一书中发表了动画的十二个原则 ([] (https://en.wikipedia.org/wiki/12_basic_principles_of_animation)) 。这些原则描述了动画能怎样用于让观众相信自己沉浸在现实世界中。 在本文中,我会逐个介绍这十二个原则,并讨论它们怎样运用在网页中。你能在 Codepen 找到它们[] (https://codepen.io/collection/AxKOdY/)。 挤压和拉伸 (Squash and stretch) 这是物体存在质量且运动时质量保持不变的概念。当一个球在弹跳时,碰击到地面会变扁,恢复的时间会越来越短。 [代码] 创建对象的时候最有用的方法是参照实物,比如人、时钟和弹性球。 当它和网页元件一起工作时可能会忽略这个原则。DOM 对象不一定和实物相关,它会按需要在屏幕上缩放。例如,一个按钮会变大并变成一个信息框,或者错误信息会出现和消失。 尽管如此,挤压和伸缩效果可以为一个对象增加实物的感觉。甚至一些形状上的小变化就可以创造出细微但抢眼的效果。 [代码] <h1>Principle 1: Squash and stretch</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle one"> <div class="shape"></div> <div class="surface"></div> </article> [代码].one .shape { animation: one 4s infinite ease-out; .one .surface { background: #000; height: 10em; width: 1em; position: absolute; top: calc(50% - 4em); left: calc(50% + 10em); @keyframes one { 0%, 15% { opacity: 0; 15%, 25% { transform: none; animation-timing-function: cubic-bezier(1,-1.92,.95,.89); width: 4em; height: 4em; top: calc(50% - 2em); left: calc(50% - 2em); opacity: 1; 35%, 45% { transform: translateX(8em); height: 6em; width: 2em; top: calc(50% - 3em); animation-timing-function: linear; opacity: 1; 70%, 100% { transform: translateX(8em) translateY(5em); height: 6em; width: 2em; top: calc(50% - 3em); opacity: 0; body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; text-decoration: none; color: #333; .principle { width: 100%; height: 100vh; position: relative; .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); 预备动作 (Anticipation) 运动不倾向于突然发生。在现实生活中,无论是一个球在掉到桌子前就开始滚动,或是一个人屈膝准备起跳,运动通常有着某种事先的累积。 [代码] 我们能用它去让我们的过渡动画显得更逼真。预备动作可以是一个细微的反弹,帮人们理解什么对象将在屏幕中发生变化并留下痕迹。 例如,悬停在一个元件上时可以在它变大前稍微缩小,在初始列表中添加额外的条目来介绍其它条目的移除方法。 [代码]<h1>Principle 2: Anticipation</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle two"> <div class="shape"></div> <div class="surface"></div> </article> [代码].two .shape { animation: two 5s infinite ease-out; transform-origin: 50% 7em; .two .surface { background: #000; width: 8em; height: 1em; position: absolute; top: calc(50% + 4em); left: calc(50% - 3em); @keyframes two { 0%, 15% { opacity: 0; transform: none; 15%, 25% { opacity: 1; transform: none; animation-timing-function: cubic-bezier(.5,.05,.91,.47); 28%, 38% { transform: translateX(-2em); 40%, 45% { transform: translateX(-4em); 50%, 52% { transform: translateX(-4em) rotateZ(-20deg); 70%, 75% { transform: translateX(-4em) rotateZ(-10deg); 78% { transform: translateX(-4em) rotateZ(-24deg); opacity: 1; 86%, 100% { transform: translateX(-6em) translateY(4em) rotateZ(-90deg); opacity: 0; /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; text-decoration: none; color: #333; .principle { width: 100%; height: 100vh; position: relative; .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); 演出布局 (Staging) 演出布局是确保对象在场景中得以聚焦,让场景中的其它对象和视觉在主动画发生的地方让位。这意味着要么把主动画放到突出的位置,要么模糊其它元件来让用户专注于看他们需要看的东西。 [代码] 在网页方面,一种方法是用 model 覆盖在某些内容上。在现有页面添加一个遮罩并把那些主要关注的内容前置展示。 另一种方法是用动作。当很多对象在运动,你很难知道哪些值得关注。如果其它所有的动作停止,只留一个在运动,即使动得很微弱,这都可以让对象更容易被察觉。 还有一种方法是做一个晃动和闪烁的按钮来简单地建议用户比如他们可能要保存文档。屏幕保持静态,所以再细微的动作也会突显出来。 [代码]<h1>Principle 3: Staging</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle three"> <div class="shape a"></div> <div class="shape b"></div> <div class="shape c"></div> </article> [代码].three .shape.a { transform: translateX(-12em); .three .shape.c { transform: translateX(12em); .three .shape.b { animation: three 5s infinite ease-out; transform-origin: 0 6em; .three .shape.a, .three .shape.c { animation: threeb 5s infinite linear; @keyframes three { 0%, 10% { transform: none; animation-timing-function: cubic-bezier(.57,-0.5,.43,1.53); 26%, 30% { transform: rotateZ(-40deg); 32.5% { transform: rotateZ(-38deg); 35% { transform: rotateZ(-42deg); 37.5% { transform: rotateZ(-38deg); 40% { transform: rotateZ(-40deg); 42.5% { transform: rotateZ(-38deg); 45% { transform: rotateZ(-42deg); 47.5% { transform: rotateZ(-38deg); animation-timing-function: cubic-bezier(.57,-0.5,.43,1.53); 58%, 100% { transform: none; @keyframes threeb { 0%, 20% { filter: none; 40%, 50% { filter: blur(5px); 65%, 100% { filter: none; /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; text-decoration: none; color: #333; .principle { width: 100%; height: 100vh; position: relative; .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); 连续运动和姿态对应 (Straight-Ahead Action and Pose-to-Pose) 连续运动是绘制动画的每一帧,姿态对应是通常由一个 assistant 在定义一系列关键帧后填充间隔。 [代码] 大多数网页动画用的是姿态对应:关键帧之间的过渡可以通过浏览器在每个关键帧之间的插入尽可能多的帧使动画流畅。 有一个例外是定时功能step。通过这个功能,浏览器 “steps” 可以把尽可能多的无序帧串清晰。你可以用这种方式绘制一系列图片并让浏览器按顺序显示出来,这开创了一种逐帧动画的风格。 [代码]<h1>Principle 4: Straight Ahead Action and Pose to Pose</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle four"> <div class="shape a"></div> <div class="shape b"></div> </article> [代码].four .shape.a { left: calc(50% - 8em); animation: four 6s infinite cubic-bezier(.57,-0.5,.43,1.53); .four .shape.b { left: calc(50% + 8em); animation: four 6s infinite steps(1); @keyframes four { 0%, 10% { transform: none; 26%, 30% { transform: rotateZ(-45deg) scale(1.25); 40% { transform: rotateZ(-45deg) translate(2em, -2em) scale(1.8); 50%, 75% { transform: rotateZ(-45deg) scale(1.1); 90%, 100% { transform: none; /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; text-decoration: none; color: #333; .principle { width: 100%; height: 100vh; position: relative; .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); 跟随和重叠动作 (Follow Through and Overlapping Action) 事情并不总在同一时间发生。当一辆车从急刹到停下,车子会向前倾、有烟从轮胎冒出来、车里的司机继续向前冲。 [代码] 这些细节是跟随和重叠动作的例子。它们在网页中能被用作帮助强调什么东西被停止,并不会被遗忘。例如一个条目可能在滑动时稍滑微远了些,但它自己会纠正到正确位置。 要创造一个重叠动作的感觉,我们可以让元件以稍微不同的速度移动到每处。这是一种在 iOS 系统的视窗 (View) 过渡中被运用得很好的方法。一些按钮和元件以不同速率运动,整体效果会比全部东西以相同速率运动要更逼真,并留出时间让访客去适当理解变化。 在网页方面,这可能意味着让过渡或动画的效果以不同速度来运行。 [代码]<h1>Principle 5: Follow Through and Overlapping Action</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle five"> <div class="shape-container"> <div class="shape"></div> </article> [代码].five .shape { animation: five 4s infinite cubic-bezier(.64,-0.36,.1,1); position: relative; left: auto; top: auto; .five .shape-container { animation: five-container 4s infinite cubic-bezier(.64,-0.36,.1,2); position: absolute; left: calc(50% - 4em); top: calc(50% - 4em); @keyframes five { 0%, 15% { opacity: 0; transform: translateX(-12em); 15%, 25% { transform: translateX(-12em); opacity: 1; 85%, 90% { transform: translateX(12em); opacity: 1; 100% { transform: translateX(12em); opacity: 0; @keyframes five-container { 0%, 35% { transform: none; 50%, 60% { transform: skewX(20deg); 90%, 100% { transform: none; /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; text-decoration: none; color: #333; .principle { width: 100%; height: 100vh; position: relative; .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); 缓入缓出 (Slow In and Slow Out) 对象很少从静止状态一下子加速到最大速度,它们往往是逐步加速并在停止前变慢。没有加速和减速,动画感觉就像机器人。 [代码] 在 CSS 方面,缓入缓出很容易被理解,在一个动画过程中计时功能是一种描述变化速率的方式。 使用计时功能,动画可以由慢加速 (ease-in)、由快减速 (ease-out),或者用贝塞尔曲线做出更复杂的效果。 [代码]<h1>Principle 6: Slow in and Slow out</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle six"> <div class="shape a"></div> </article> [代码].six .shape { animation: six 3s infinite cubic-bezier(0.5,0,0.5,1); @keyframes six { 0%, 5% { transform: translate(-12em); 45%, 55% { transform: translate(12em); 95%, 100% { transform: translate(-12em); /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; text-decoration: none; color: #333; .principle { width: 100%; height: 100vh; position: relative; .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); 弧线运动 (Arc) 虽然对象是更逼真了,当它们遵循「缓入缓出」的时候它们很少沿直线运动——它们倾向于沿弧线运动。 我们有几种 CSS 的方式来实现弧线运动。一种是结合多个动画,比如在弹力球动画里,可以让球上下移动的同时让它右移,这时候球的显示效果就是沿弧线运动。 [代码]<h1>Principle 7: Arc (1)</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle sevena"> <div class="shape-container"> <div class="shape a"></div> </article> [代码].sevena .shape-container { animation: move-right 6s infinite cubic-bezier(.37,.55,.49,.67); position: absolute; left: calc(50% - 4em); top: calc(50% - 4em); .sevena .shape { animation: bounce 6s infinite linear; border-radius: 50%; position: relative; left: auto; top: auto; @keyframes move-right { transform: translateX(-20em); opacity: 1; 80% { opacity: 1; 90%, 100% { transform: translateX(20em); opacity: 0; @keyframes bounce { transform: translateY(-8em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); 15% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); 25% { transform: translateY(-4em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); 32.5% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); 40% { transform: translateY(0em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); 45% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); 50% { transform: translateY(3em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); 56% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); 60% { transform: translateY(6em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); 64% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); 66% { transform: translateY(7.5em); animation-timing-function: cubic-bezier(.51,.01,.79,.02); 70%, 100% { transform: translateY(8em); animation-timing-function: cubic-bezier(.19,1,.7,1); /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; text-decoration: none; color: #333; .principle { width: 100%; height: 100vh; position: relative; .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); 另外一种是旋转元件,我们可以设置一个在对象之外的原点来作为它的旋转中心。当我们旋转这个对象,它看上去就是沿着弧线运动。 [代码]<h1>Principle 7: Arc (2)</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle sevenb"> <div class="shape a"></div> <div class="shape b"></div> </article> [代码].sevenb .shape.a { animation: sevenb 3s infinite linear; top: calc(50% - 2em); left: calc(50% - 9em); transform-origin: 10em 50%; .sevenb .shape.b { animation: sevenb 6s infinite linear reverse; background-color: yellow; width: 2em; height: 2em; left: calc(50% - 1em); top: calc(50% - 1em); @keyframes sevenb { 100% { transform: rotateZ(360deg); /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; text-decoration: none; color: #333; .principle { width: 100%; height: 100vh; position: relative; .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); 次要动作 (Secondary Action) 虽然主动画正在发生,次要动作可以增强它的效果。这就好比某人在走路的时候摆动手臂和倾斜脑袋,或者弹性球弹起的时候扬起一些灰尘。 在网页方面,当主要焦点出现的时候就可以开始执行次要动作,比如拖拽一个条目到列表中间。 [代码]<h1>Principle 8: Secondary Action</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle eight"> <div class="shape a"></div> <div class="shape b"></div> <div class="shape c"></div> </article> [代码].eight .shape.a { transform: translateX(-6em); animation: eight-shape-a 4s cubic-bezier(.57,-0.5,.43,1.53) infinite; .eight .shape.b { top: calc(50% + 6em); opacity: 0; animation: eight-shape-b 4s linear infinite; .eight .shape.c { transform: translateX(6em); animation: eight-shape-c 4s cubic-bezier(.57,-0.5,.43,1.53) infinite; @keyframes eight-shape-a { 0%, 50% { transform: translateX(-5.5em); 70%, 100% { transform: translateX(-10em); @keyframes eight-shape-b { transform: none; 20%, 30% { transform: translateY(-1.5em); opacity: 1; animation-timing-function: cubic-bezier(.57,-0.5,.43,1.53); 32% { transform: translateY(-1.25em); opacity: 1; 34% { transform: translateY(-1.75em); opacity: 1; 36%, 38% { transform: translateY(-1.25em); opacity: 1; 42%, 60% { transform: translateY(-1.5em); opacity: 1; 75%, 100% { transform: translateY(-8em); opacity: 1; @keyframes eight-shape-c { 0%, 50% { transform: translateX(5.5em); 70%, 100% { transform: translateX(10em); /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; text-decoration: none; color: #333; .principle { width: 100%; height: 100vh; position: relative; .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); 时间节奏 (Timing) 动画的时间节奏是需要多久去完成,它可以被用来让看起来很重的对象做很重的动画,或者用在添加字符的动画中。 [代码] 这在网页上可能只要简单调整 animation-duration 或 transition-duration 值。 这很容易让动画消耗更多时间,但调整时间节奏可以帮动画的内容和交互方式变得更出众。 [代码]<h1>Principle 9: Timing</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle nine"> <div class="shape a"></div> <div class="shape b"></div> </article> [代码].nine .shape.a { animation: nine 4s infinite cubic-bezier(.93,0,.67,1.21); left: calc(50% - 12em); transform-origin: 100% 6em; .nine .shape.b { animation: nine 2s infinite cubic-bezier(1,-0.97,.23,1.84); left: calc(50% + 2em); transform-origin: 100% 100%; @keyframes nine { 0%, 10% { transform: translateX(0); 40%, 60% { transform: rotateZ(90deg); 90%, 100% { transform: translateX(0); /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; text-decoration: none; color: #333; .principle { width: 100%; height: 100vh; position: relative; .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); 夸张手法 (Exaggeration) 夸张手法在漫画中是最常用来为某些动作刻画吸引力和增加戏剧性的,比如一只狼试图把自己的喉咙张得更开地去咬东西可能会表现出更恐怖或者幽默的效果。 在网页中,对象可以通过上下滑动去强调和刻画吸引力,比如在填充表单的时候生动部分会比收缩和变淡的部分更突出。 [代码]<h1>Principle 10: Exaggeration</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle ten"> <div class="shape"></div> </article> [代码].ten .shape { animation: ten 4s infinite linear; transform-origin: 50% 8em; top: calc(50% - 6em); @keyframes ten { 0%, 10% { transform: none; animation-timing-function: cubic-bezier(.87,-1.05,.66,1.31); 40% { transform: rotateZ(-45deg) scale(2); animation-timing-function: cubic-bezier(.16,.54,0,1.38); 70%, 100% { transform: rotateZ(360deg) scale(1); /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; text-decoration: none; color: #333; .principle { width: 100%; height: 100vh; position: relative; .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); 扎实的描绘 (Solid drawing) 当动画对象在三维中应该加倍注意确保它们遵循透视原则。因为人们习惯了生活在三维世界里,如果对象表现得与实际不符,会让它看起来很糟糕。 如今浏览器对三维变换的支持已经不错,这意味着我们可以在场景里旋转和放置三维对象,浏览器能自动控制它们的转换。 [代码]<h1>Principle 11: Solid drawing</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle eleven"> <div class="shape"> <div class="container"> <span class="front"></span> <span class="back"></span> <span class="left"></span> <span class="right"></span> <span class="top"></span> <span class="bottom"></span> </article> [代码].eleven .shape { background: none; border: none; perspective: 400px; perspective-origin: center; .eleven .shape .container { animation: eleven 4s infinite cubic-bezier(.6,-0.44,.37,1.44); transform-style: preserve-3d; .eleven .shape span { display: block; position: absolute; opacity: 1; width: 4em; height: 4em; border: 1em solid #fff; background: #2d97db; .eleven .shape span.front { transform: translateZ(3em); .eleven .shape span.back { transform: translateZ(-3em); .eleven .shape span.left { transform: rotateY(-90deg) translateZ(-3em); .eleven .shape span.right { transform: rotateY(-90deg) translateZ(3em); .eleven .shape span.top { transform: rotateX(-90deg) translateZ(-3em); .eleven .shape span.bottom { transform: rotateX(-90deg) translateZ(3em); @keyframes eleven { opacity: 0; 10%, 40% { transform: none; opacity: 1; 60%, 75% { transform: rotateX(-20deg) rotateY(-45deg) translateY(4em); animation-timing-function: cubic-bezier(1,-0.05,.43,-0.16); opacity: 1; 100% { transform: translateZ(-180em) translateX(20em); opacity: 0; /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; text-decoration: none; color: #333; .principle { width: 100%; height: 100vh; position: relative; .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em); 吸引力 (Appeal) 吸引力是艺术作品的特质,让我们与艺术家的想法连接起来。就像一个演员身上的魅力,是注重细节和动作相结合而打造吸引性的结果。 [代码] 精心制作网页上的动画可以打造出吸引力,例如 Stripe 这样的公司用了大量的动画去增加它们结账流程的可靠性。 [代码]<h1>Principle 12: Appeal</h1> <h2><a href="https://cssanimation.rocks/principles/" target="_parent">Animation Principles for the Web</h2> <article class="principle twelve"> <div class="shape"> <div class="container"> <span class="item one"></span> <span class="item two"></span> <span class="item three"></span> <span class="item four"></span> </article> [代码].twelve .shape { background: none; border: none; perspective: 400px; perspective-origin: center; .twelve .shape .container { animation: show-container 8s infinite cubic-bezier(.6,-0.44,.37,1.44); transform-style: preserve-3d; width: 4em; height: 4em; border: 1em solid #fff; background: #2d97db; position: relative; .twelve .item { background-color: #1f7bb6; position: absolute; .twelve .item.one { animation: show-text 8s 0.1s infinite ease-out; height: 6%; width: 30%; top: 15%; left: 25%; .twelve .item.two { animation: show-text 8s 0.2s infinite ease-out; height: 6%; width: 20%; top: 30%; left: 25%; .twelve .item.three { animation: show-text 8s 0.3s infinite ease-out; height: 6%; width: 50%; top: 45%; left: 25%; .twelve .item.four { animation: show-button 8s infinite cubic-bezier(.64,-0.36,.1,1.43); height: 20%; width: 40%; top: 65%; left: 30%; @keyframes show-container { opacity: 0; transform: rotateX(-90deg); 10% { opacity: 1; transform: none; width: 4em; height: 4em; 15%, 90% { width: 12em; height: 12em; transform: translate(-4em, -4em); opacity: 1; 100% { opacity: 0; transform: rotateX(-90deg); width: 4em; height: 4em; @keyframes show-text { 0%, 15% { transform: translateY(1em); opacity: 0; 20%, 85% { opacity: 1; transform: none; 88%, 100% { opacity: 0; transform: translateY(-1em); animation-timing-function: cubic-bezier(.64,-0.36,.1,1.43); @keyframes show-button { 0%, 25% { transform: scale(0); opacity: 0; 35%, 80% { transform: none; opacity: 1; 90%, 100% { opacity: 0; transform: scale(0); /* General styling */ body { margin: 0; background: #e9b59f; font-family: HelveticaNeue, Arial, Sans-serif; color: #fff; position: absolute; top: 0; left: 0; right: 0; text-align: center; font-weight: 300; font-size: 0.75em; position: absolute; bottom: 0; left: 0; right: 0; text-align: center; text-decoration: none; color: #333; .principle { width: 100%; height: 100vh; position: relative; .shape { background: #2d97db; border: 1em solid #fff; width: 4em; height: 4em; position: absolute; top: calc(50% - 2em); left: calc(50% - 2em);
什么是[代码]Service Worker[代码] [代码]Service Worker[代码]本质上充当Web应用程序与浏览器之间的代理服务器,也可以在网络可用时作为浏览器和网络间的代理。它们旨在(除其他之外)使得能够创建有效的离线体验,拦截网络请求并基于网络是否可用以及更新的资源是否驻留在服务器上来采取适当的动作。他们还允许访问推送通知和后台同步[代码]API[代码]。 [代码]Service Worker[代码]的本质是一个[代码]Web Worker[代码],它独立于[代码]JavaScript[代码]主线程,因此它不能直接访问[代码]DOM[代码],也不能直接访问[代码]window[代码]对象,但是,[代码]Service Worker[代码]可以访问[代码]navigator[代码]对象,也可以通过消息传递的方式(postMessage)与[代码]JavaScript[代码]主线程进行通信。 [代码]Service Worker[代码]是一个网络代理,它可以控制[代码]Web[代码]页面的所有网络请求。 [代码]Service Worker[代码]具有自身的生命周期,使用好[代码]Service Worker[代码]的关键是灵活控制其生命周期。 [代码]Service Worker[代码]的作用 用于浏览器缓存 实现离线[代码]Web APP[代码] [代码]Service Worker[代码]兼容性 [代码]Service Worker[代码]是现代浏览器的一个高级特性,它依赖于[代码]fetch API[代码]、[代码]Cache Storage[代码]、[代码]Promise[代码]等,其中,[代码]Cache[代码]提供了[代码]Request / Response[代码]对象对的存储机制,[代码]Cache Storage[代码]存储多个[代码]Cache[代码]。 在了解[代码]Service Worker[代码]的原理之前,先来看一段[代码]Service Worker[代码]的示例: [代码]self.importScripts('./serviceworker-cache-polyfill.js'); var urlsToCache = [ '/index.js', '/style.css', '/favicon.ico', var CACHE_NAME = 'counterxing'; self.addEventListener('install', function(event) { self.skipWaiting(); event.waitUntil( caches.open(CACHE_NAME) .then(function(cache) { return cache.addAll(urlsToCache); self.addEventListener('fetch', function(event) { event.respondWith( caches.match(event.request) .then(function(response) { if (response) { return response; return fetch(event.request); self.addEventListener('activate', function(event) { var cacheWhitelist = ['counterxing']; event.waitUntil( caches.keys().then(function(cacheNames) { return Promise.all( cacheNames.map(function(cacheName) { if (cacheWhitelist.indexOf(cacheName) === -1) { return caches.delete(cacheName); 下面开始逐段逐段地分析,揭开[代码]Service Worker[代码]的神秘面纱: [代码]polyfill[代码] 首先看第一行:[代码]self.importScripts('./serviceworker-cache-polyfill.js');[代码],这里引入了Cache API的一个polyfill,这个[代码]polyfill[代码]支持使得在较低版本的浏览器下也可以使用[代码]Cache Storage API[代码]。想要实现[代码]Service Worker[代码]的功能,一般都需要搭配[代码]Cache API[代码]代理网络请求到缓存中。 在[代码]Service Worker[代码]线程中,使用[代码]importScripts[代码]引入[代码]polyfill[代码]脚本,目的是对低版本浏览器的兼容。 [代码]Cache Resources List[代码] And [代码]Cache Name[代码] 之后,使用一个[代码]urlsToCache[代码]列表来声明需要缓存的静态资源,再使用一个变量[代码]CACHE_NAME[代码]来确定当前缓存的[代码]Cache Storage Name[代码],这里可以理解成[代码]Cache Storage[代码]是一个[代码]DB[代码],而[代码]CACHE_NAME[代码]则是[代码]DB[代码]名: [代码]var urlsToCache = [ '/index.js', '/style.css', '/favicon.ico', var CACHE_NAME = 'counterxing'; [代码]Lifecycle[代码] [代码]Service Worker[代码]独立于浏览器[代码]JavaScript[代码]主线程,有它自己独立的生命周期。 如果需要在网站上安装[代码]Service Worker[代码],则需要在[代码]JavaScript[代码]主线程中使用以下代码引入[代码]Service Worker[代码]。 [代码]if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js').then(function(registration) { console.log('成功安装', registration.scope); }).catch(function(err) { console.log(err); 此处,一定要注意[代码]sw.js[代码]文件的路径,在我的示例中,处于当前域根目录下,这意味着,[代码]Service Worker[代码]和网站是同源的,可以为当前网站的所有请求做代理,如果[代码]Service Worker[代码]被注册到[代码]/imaging/sw.js[代码]下,那只能代理[代码]/imaging[代码]下的网络请求。 可以使用[代码]Chrome[代码]控制台,查看当前页面的[代码]Service Worker[代码]情况: 安装完成后,[代码]Service Worker[代码]会经历以下生命周期: 下载([代码]download[代码]) 安装([代码]install[代码]) 激活([代码]activate[代码]) 用户首次访问[代码]Service Worker[代码]控制的网站或页面时,[代码]Service Worker[代码]会立刻被下载。之后至少每[代码]24[代码]小时它会被下载一次。它可能被更频繁地下载,不过每[代码]24[代码]小时一定会被下载一次,以避免不良脚本长时间生效。 在下载完成后,开始安装[代码]Service Worker[代码],在安装阶段,通常需要缓存一些我们预先声明的静态资源,在我们的示例中,通过[代码]urlsToCache[代码]预先声明。 在安装完成后,会开始进行激活,浏览器会尝试下载[代码]Service Worker[代码]脚本文件,下载成功后,会与前一次已缓存的[代码]Service Worker[代码]脚本文件做对比,如果与前一次的[代码]Service Worker[代码]脚本文件不同,证明[代码]Service Worker[代码]已经更新,会触发[代码]activate[代码]事件。完成激活。 如图所示,为[代码]Service Worker[代码]大致的生命周期: [代码]install[代码] 在安装完成后,尝试缓存一些静态资源: [代码]self.addEventListener('install', function(event) { self.skipWaiting(); event.waitUntil( caches.open(CACHE_NAME) .then(function(cache) { return cache.addAll(urlsToCache); 首先,[代码]self.skipWaiting()[代码]执行,告知浏览器直接跳过等待阶段,淘汰过期的[代码]sw.js[代码]的[代码]Service Worker[代码]脚本,直接开始尝试激活新的[代码]Service Worker[代码]。 然后使用[代码]caches.open[代码]打开一个[代码]Cache[代码],打开后,通过[代码]cache.addAll[代码]尝试缓存我们预先声明的静态文件。 监听[代码]fetch[代码],代理网络请求 页面的所有网络请求,都会通过[代码]Service Worker[代码]的[代码]fetch[代码]事件触发,[代码]Service Worker[代码]通过[代码]caches.match[代码]尝试从[代码]Cache[代码]中查找缓存,缓存如果命中,则直接返回缓存中的[代码]response[代码],否则,创建一个真实的网络请求。 [代码]self.addEventListener('fetch', function(event) { event.respondWith( caches.match(event.request) .then(function(response) { if (response) { return response; return fetch(event.request); 如果我们需要在请求过程中,再向[代码]Cache Storage[代码]中添加新的缓存,可以通过[代码]cache.put[代码]方法添加,看以下例子: [代码]self.addEventListener('fetch', function(event) { event.respondWith( caches.match(event.request) .then(function(response) { // 缓存命中 if (response) { return response; // 注意,这里必须使用clone方法克隆这个请求 // 原因是response是一个Stream,为了让浏览器跟缓存都使用这个response // 必须克隆这个response,一份到浏览器,一份到缓存中缓存。 // 只能被消费一次,想要再次消费,必须clone一次 var fetchRequest = event.request.clone(); return fetch(fetchRequest).then( function(response) { // 必须是有效请求,必须是同源响应,第三方的请求,因为不可控,最好不要缓存 if (!response || response.status !== 200 || response.type !== 'basic') { return response; // 消费过一次,又需要再克隆一次 var responseToCache = response.clone(); caches.open(CACHE_NAME) .then(function(cache) { cache.put(event.request, responseToCache); return response; 在项目中,一定要注意控制缓存,接口请求一般是不推荐缓存的。所以在我自己的项目中,并没有在这里做动态的缓存方案。 [代码]activate[代码] [代码]Service Worker[代码]总有需要更新的一天,随着版本迭代,某一天,我们需要把新版本的功能发布上线,此时需要淘汰掉旧的缓存,旧的[代码]Service Worker[代码]和[代码]Cache Storage[代码]如何淘汰呢? [代码]self.addEventListener('activate', function(event) { var cacheWhitelist = ['counterxing']; event.waitUntil( caches.keys().then(function(cacheNames) { return Promise.all( cacheNames.map(function(cacheName) { if (cacheWhitelist.indexOf(cacheName) === -1) { return caches.delete(cacheName); 首先有一个白名单,白名单中的[代码]Cache[代码]是不被淘汰的。 之后通过[代码]caches.keys()[代码]拿到所有的[代码]Cache Storage[代码],把不在白名单中的[代码]Cache[代码]淘汰。 淘汰使用[代码]caches.delete()[代码]方法。它接收[代码]cacheName[代码]作为参数,删除该[代码]cacheName[代码]所有缓存。 sw-precache-webpack-plugin sw-precache-webpack-plugin是一个[代码]webpack plugin[代码],可以通过配置的方式在[代码]webpack[代码]打包时生成我们想要的[代码]sw.js[代码]的[代码]Service Worker[代码]脚本。 一个最简单的配置如下: [代码]var path = require('path'); var SWPrecacheWebpackPlugin = require('sw-precache-webpack-plugin'); const PUBLIC_PATH = 'https://www.my-project-name.com/'; // webpack needs the trailing slash for output.publicPath module.exports = { entry: { main: path.resolve(__dirname, 'src/index'), output: { path: path.resolve(__dirname, 'src/bundles/'), filename: '[name]-[hash].js', publicPath: PUBLIC_PATH, plugins: [ new SWPrecacheWebpackPlugin( cacheId: 'my-project-name', dontCacheBustUrlsMatching: /\.\w{8}\./, filename: 'service-worker.js', minify: true, navigateFallback: PUBLIC_PATH + 'index.html', staticFileGlobsIgnorePatterns: [/\.map$/, /asset-manifest\.json$/], 在执行[代码]webpack[代码]打包后,会生成一个名为[代码]service-worker.js[代码]文件,用于缓存[代码]webpack[代码]打包后的静态文件。 一个最简单的示例。 [代码]Service Worker Cache[代码] VS [代码]Http Cache[代码] 对比起[代码]Http Header[代码]缓存,[代码]Service Worker[代码]配合[代码]Cache Storage[代码]也有自己的优势: 缓存与更新并存:每次更新版本,借助[代码]Service Worker[代码]可以立马使用缓存返回,但与此同时可以发起请求,校验是否有新版本更新。 无侵入式:[代码]hash[代码]值实在是太难看了。 不易被冲掉:[代码]Http[代码]缓存容易被冲掉,也容易过期,而[代码]Cache Storage[代码]则不容易被冲掉。也没有过期时间的说法。 离线:借助[代码]Service Worker[代码]可以实现离线访问应用。 但是缺点是,由于[代码]Service Worker[代码]依赖于[代码]fetch API[代码]、依赖于[代码]Promise[代码]、[代码]Cache Storage[代码]等,兼容性不太好。 本文只是简单总结了[代码]Service Worker[代码]的基本使用和使用[代码]Service Worker[代码]做客户端缓存的简单方式,然而,[代码]Service Worker[代码]的作用远不止于此,例如:借助[代码]Service Worker[代码]做离线应用、用于做网络应用的推送(可参考push-notifications)等。 甚至可以借助[代码]Service Worker[代码],对接口进行缓存,在我所在的项目中,其实并不会做的这么复杂。不过做接口缓存的好处是支持离线访问,对离线状态下也能正常访问我们的[代码]Web[代码]应用。 [代码]Cache Storage[代码]和[代码]Service Worker[代码]总是分不开的。[代码]Service Worker[代码]的最佳用法其实就是配合[代码]Cache Storage[代码]做离线缓存。借助于[代码]Service Worker[代码],可以轻松实现对网络请求的控制,对于不同的网络请求,采取不同的策略。例如对于[代码]Cache[代码]的策略,其实也是存在多种情况。例如可以优先使用网络请求,在网络请求失败时再使用缓存、亦可以同时使用缓存和网络请求,一方面检查请求,一方面有检查缓存,然后看两个谁快,就用谁。 优化方向:目前我所负责的DICOM项目,虽然还没有用上[代码]Service Worker[代码],但前面经过不断地优化迭代,通过从增加http层的缓存、无损压缩图像的替换、有损压缩图像的渐进加载、更换DICOM解压缩策略、使用indexed DB缓存CT图像、首屏可见速度已经从20多秒降低到5秒左右,内存占用从700M以上降低到250M左右。后期还会一直深挖这一块。主要方向之一就是service worker的替换,全站缓存静态资源。此外,高优先级的则是DICOM无损图像解压算法的最优选择与优化、cornerstone的jpg图像展示。 项目优化还在继续,力求极致性能和用户体验~
最近朋友圈里经常有看到这样的头像 既然这么火,大家要图又这么难,作为程序员的自己当然要自己动手实现一个。 老规矩,先看效果图 仔细研究了下,发现实现起来并不难,核心代码只有下面10行。 [代码] wx.canvasToTempFilePath({ x: 0, y: 0, width: num, height: num, destWidth: num, destHeight: num, canvasId: 'shareImg', success: function(res) { that.setData({ prurl: res.tempFilePath wx.hideLoading() fail: function(res) { wx.hideLoading() 一,首先要创建一个小程序 至于如何创建小程序,我这里就不在细讲了,我也有写过创建小程序的文章,也有路过相关的学习视频,去翻下我历史文章找找就行。 二,创建好小程序后,我们就开始来布局 布局很简单,只有下面几行代码。 [代码]<!-- 画布大小按需定制 这里我按照背景图的尺寸定的 --> <canvas canvas-id="shareImg"></canvas> <!-- 预览区域 --> <view class='preview'> <image src='{{prurl}}' mode='aspectFit'></image> <button size='mini' open-type="getUserInfo" bindgetuserinfo="shengcheng" data-k="1">生成头像1</button> <button size='mini' open-type="getUserInfo" bindgetuserinfo="shengcheng" data-k="2">生成头像2</button> <button size='mini' open-type="getUserInfo" bindgetuserinfo="shengcheng" data-k="3">生成头像3</button> <button size='mini' open-type="getUserInfo" bindgetuserinfo="shengcheng" data-k="4">生成头像4</button> <button type='primary' bindtap='save'>保存分享图</button> </view> 实现效果图如下 三,使用canvas来画图 其实我们实现微信头像挂红旗,原理很简单,就是把头像放在下面,然后把有红旗的相框盖在头像上面 下面就直接把核心代码贴给大家 [代码]let promise1 = new Promise(function(resolve, reject) { wx.getImageInfo({ src: "../../images/xiaoshitou.jpg", success: function(res) { console.log("promise1", res) resolve(res); let promise2 = new Promise(function(resolve, reject) { wx.getImageInfo({ src: `../../images/head${index}.png`, success: function(res) { console.log(res) resolve(res); Promise.all([ promise1, promise2 ]).then(res => { console.log("Promise.all", res) //主要就是计算好各个图文的位置 let num = 1125; ctx.drawImage('../../'+res[0].path, 0, 0, num, num) ctx.drawImage('../../' + res[1].path, 0, 0, num, num) ctx.stroke() ctx.draw(false, () => { wx.canvasToTempFilePath({ x: 0, y: 0, width: num, height: num, destWidth: num, destHeight: num, canvasId: 'shareImg', success: function(res) { that.setData({ prurl: res.tempFilePath wx.hideLoading() fail: function(res) { wx.hideLoading() 来看下画出来的效果图 四,头像加红旗画好以后,我们就要想办法把图片保存到本地了 保存图片的代码也很简单。 [代码]save: function() { var that = this wx.saveImageToPhotosAlbum({ filePath: that.data.prurl, success(res) { wx.showModal({ content: '图片已保存到相册,赶紧晒一下吧~', showCancel: false, confirmText: '好哒', confirmColor: '#72B9C3', success: function(res) { if (res.confirm) { console.log('用户点击确定'); 来看下保存后的效果图 到这里,我的微信头像就成功的加上了小红旗了。 源码我也已经给大家准备好了,有需要的同学在文末留言即可。 后面我准备录制一门视频课程出来,来详细教大家实现这个功能,敬请关注。
近期绝大部分小程序收到内容安全能力警告通知如下 官方回复一般为: 开发者你好,近期平台收到较多投诉及通报,反馈部分小程序未做好内容审核,存在安全隐患。为完善小程序内容安全生态,近日平台给部分类目陆续下发了相关的隐患提醒,请开发者结合自己实际情况,如未完善内容审核措施的,请尽快完善,可接入平台提供的内容安全接口或其他内容安全能力;如已有相关内容审核措施,请继续加强内容审核力度,做好内容安全掌控。这里并不强制必须要接入微信官方提供的安全接口。 不少开发者认为仅仅接入内容安全API就足够了吗? 其实微信官方提供的内容安全接口并不是万能的,也可能存在错报或不能正确识别的情况,特别是图片安全接口,违法违规信息可以任意展示,同时微信官方也没有强制必须要求接入微信官方提供的安全接口, > 官方内容传播控制策略建议(摘自:珊瑚内容安全用手[小程序]) 1、用户发布内容在未经审核前,建议限制内容传播范围,包括限制转发、访问,或不得在搜索结果中曝光。 2、运营者可根据文字、图片等鉴别能力的检测返回结果,自行对存在风险的内容及时删除、拦截处理。 3、对达到一定访问量或短时间转发次数异常的内容,建议运营者安排人工进行二次审核,及时发现有害内容。 4、对于恶意发布有害内容的用户账号,运营者可作限制使用、封停等处置,从账号维度强化内容管理。 5、对于封停账号,运营者可采取策略限制恶意用户再次使用产品。 所以仅仅接入内容安全API仍然是有相当一部分小程序被下架,但又不知道如何整改 根据我们自己的审核经验,总结了如下解决方法, 接入内容安全API 用户自行发布内容,需先经过内容安全API检测,此时无论通过与否,均不可展示给发布内容的用户及其他用户 再次进行人工审核 通过内容安全API检测后,因为内容安全API并不是能过滤所有有害信息,所以需要再次进行人工审核,审核无误后进行信息展示 说起来很简单,就是加上人工审核这一个环节,并在小程序代码上传时的项目备注里加上:已接入内容安全API,并有人工审核机制 有人可能会说人工审核这样的话效率不是大打折扣了吗?但我认为在大环境下这种牺牲是值得的,希望能帮助到大家
在小程序中,常常有些数据需要在几个页面或组件中共享。对于这样的数据,在 web 开发中,有些朋友使用过 redux 、 vuex 之类的 状态管理 框架。在小程序开发中,也有不少朋友喜欢用 MobX ,说明这类框架在实际开发中非常实用。 小程序团队近期也开源了 MobX 的辅助模块,使用 MobX 也更加方便。那么,在这篇文章中就来介绍一下 MobX 在小程序中的一个简单用例! 在小程序中引入 MobX 在小程序项目中,可以通过 npm 的方式引入 MobX 。如果你还没有在小程序中使用过 npm ,那先在小程序目录中执行命令: [代码]npm init -y 引入 MobX : [代码]npm install --save mobx-miniprogram mobx-miniprogram-bindings (这里用到了 mobx-miniprogram-bindings 模块,模块说明在这里: https://developers.weixin.qq.com/miniprogram/dev/extended/functional/mobx.html 。) npm 命令执行完后,记得在开发者工具的项目中点一下菜单栏中的 [代码]工具[代码] - [代码]构建 npm[代码] 。 MobX 有什么用呢? 试想这样一个场景:制作一个天气预报资讯小程序,首页是列表,点击列表中的项目可以进入到详情页。 首页如下: 详情页如下: 每次进入首页时,需要使用 [代码]wx.request[代码] 获取天气列表数据,之后将数据使用 setData 应用到界面上。进入详情页之后,再次获取指定日期的天气详情数据,展示在详情页中。 这样做的坏处是,进入了详情页之后需要再次通过网络获取一次数据,等待网络返回后才能将数据展示出来。 事实上,可以在首页获取天气列表数据时,就一并将所有的天气详情数据一同获取回来,存放在一个 数据仓库 中,需要的时候从仓库中取出来就可以了。这样,只需要进入首页时获取一次网络数据就可以了。 MobX 可以帮助我们很方便地建立数据仓库。接下来就讲解一下具体怎么建立和使用 MobX 数据仓库。 建立数据仓库 数据仓库通常专门写在一个独立的 js 文件中。 [代码]import { observable, action } from 'mobx-miniprogram' // 数据仓库 export const store = observable({ list: [], // 天气数据(包含列表和详情) // 设置天气列表,从网络上获取到数据之后调用 setList: action(function (list) { this.list = list 在上面数据仓库中,包含有数据 [代码]list[代码] (即天气数据),还包括了一个名为 [代码]setList[代码] 的 action ,用于更改数据仓库中的数据。 在首页中使用数据仓库 如果需要在页面中使用数据仓库里的数据,需要调用 [代码]createStoreBindings[代码] 来将仓库中的数据绑定到页面数据中,然后就可以在页面中直接使用仓库数据了。 [代码]import { createStoreBindings } from 'mobx-miniprogram-bindings' import { store } from './store' Page({ onLoad() { // 绑定 MobX store this.storeBindings = createStoreBindings(this, { store, // 需要绑定的数据仓库 fields: ['list'], // 将 this.data.list 绑定为仓库中的 list ,即天气数据 actions: ['setList'], // 将 this.setList 绑定为仓库中的 setList action // 从服务器端读取数据 wx.showLoading() wx.request({ // 请求网络数据 // ... success: (data) => { wx.hideLoading() // 调用 setList action ,将数据写入 store this.setList(data) onUnload() { // 解绑 this.storeBindings.destroyStoreBindings() 这样,可以在 wxml 中直接使用 list : [代码]<view class="item" wx:for="{{list}}" wx:key="date" data-index="{{index}}"> <!-- 这里可以使用 list 中的数据了! --> <view class="title">{{item.date}} {{item.summary}}</view> <view class="abstract">{{item.temperature}}</view> </view> 在详情页中使用数据仓库 在详情页中,同样可以使用 [代码]createStoreBindings[代码] 来将仓库中的数据绑定到页面数据中: [代码]import { createStoreBindings } from 'mobx-miniprogram-bindings' import { store } from './store' Page({ onLoad(args) { // 绑定 MobX store this.storeBindings = createStoreBindings(this, { store, // 需要绑定的数据仓库 fields: ['list'], // 将 this.data.list 绑定为仓库中的 list ,即天气数据 // 页面参数 `index` 表示要展示哪一条天气详情数据,将它用 setData 设置到界面上 this.setData({ index: args.index onUnload() { // 解绑 this.storeBindings.destroyStoreBindings() 这样,这个页面 wxml 中也可以直接使用 list : [代码]<view class="title">{{list[index].date}}</view> <view class="content">温度 {{list[index].temperature}}</view> <view class="content">天气 {{list[index].weather}}</view> <view class="content">空气质量 {{list[index].airQuality}}</view> <view class="content">{{list[index].details}}</view> 完整例子可以在这个代码片段中体验: https://developers.weixin.qq.com/s/YhfvpxmN7HcV 这个就是 MobX 在小程序中最基础的玩法了。相关的 npm 模块文档可参考 mobx-miniprogram-bindings 和 mobx-miniprogram 。 MobX 在实际使用时还有很多好的实践经验,感兴趣的话,可以阅读一些其他相关的文章。
什么是 Sourcemaps uglifyjs、bable 等工具会对 源代码 进行编译处理生成编译后的代码(下称目标代码),而 sourcemaps 就是保留了目标代码在源代码中的 位置信息 --------- 大神分割线 --------- 如何解读 Sourcemaps Sourcemaps 是一个 json [代码]{ "version": 3, "sources": ["a.js", "b.js"], // 源文件列表,这个表示是由 a.js 和 b.js 合并生成 "names": ["myFn", "test"], // 如果开启了变量名混淆,这里会保留变量名在源文件中名字信息 "sourcesContent: [], // 可选项,保存源码信息,顺序与 sources 字段对应,chrome 的 sources 面板中源码使用了这个字段的内容进行展示 "sourceRoot": "", // 源文件所在的目录信息 "file": "dist.js", // 可选,编译后的文件名 "mappings": "" // 这个是重点,是目标代码和源文件的位置的映射关系 mappings 目标文件"行"的信息 mappings 是使用 ; 分隔的,每个部分对应目标代码的行 如: “;AAAA;AAAA,BBBB;;” 本例子目标文件有 4 行 第 0 行和第 3 行没有源文件对应信息,所以这两行是编译过程中加入的代码 目标文件的"列"信息 如: “AAAA,CAEA,CAEA;” ‘,’ 表示行内的位置信息分隔符 本例表示目标文件的这一行有三个有效的位置信息。 位置信息的第一位表示目标文件的列的 偏移 信息 本例中,表示列的信息是 ‘A’、‘C’、‘C’,对应的数字为 0、+1、+1,(vlq 编码,在线编解码工具) 注意,这个是偏移信息; 列数从 0 开始,依次累加偏移值可以算出当前的位置信息对应的真正的列 所以本例中表示的是目标文件的第 n 行中的第 0 列,第 1 列,第 2 列(没错是第 2 列) 源文件的信息 如:‘AAAA;ACAA;ADAA;’ 位置信息的第二位表示源文件的信息,本例子中是 ‘A’、‘C’、‘D’,对应数字是 0、+1、-1 如果 sourcemaps 中的 sources 字段只有一个文件的话,那么位置信息中第二位一直是 A(不需要偏移) 假设 sourcemaps 中 sources: [‘a.js’, ‘b.js’] 本例的意思是 AAAA: 目标文件第 0 行第 0 列 对应 第 0 个文件 a.js ACAA; 目标文件第 1 行第 0 列 对应 第 1 个文件 b.js ADAA; 目标文件第 2 行第 0 列 对了 第 0 个文件 a.js (偏移是 -1 又回到了 a.js) 源文件的行信息 位置信息的第三位表示源文件中的行的信息, 理解了位置偏移的概念,我们很容易理解 如:‘AACA,CACA;AACA;‘ AACA: 目标文件的第 0 行第 0 列 对应 第 0 个文件的第 1 行 CACA: 目标文件的第 0 行第 0+1 列 对应 第 0 个文件的第 1+1 行 AACA:目标文件的第 1 行第 0 列 对应 第 0 个文件的第 1 行 (注意:’;’ 后的行列偏移信息归 0) 源文件中的列信息 位置信息的第四位表示源文件中的列的信息 如:'AAAA,CAAC;' AAAA: 目标文件的第 0 行第 0 列 对应 第 0 个文件的第 0 行第 0 列 CAAC: 目标文件的第 0 行第 0+1 列 对应 第 0 个文件的第 0 行第 0+1 列 位置信息的第五位 第五位表示变量的偏移,对应 sourcemaps 中的 names 字段,表示目标文件中的变量名对应域源文件中的变量 如:’AAAA,CAACC;AAAAD;' sourcemaps 中 names 字段是 [‘a’, ‘b’] AAAA: 目标文件的第 0 行第 0 列 对应 第 0 个文件的第 0 行第 0 列,没有变量的信息 CAACC: 目标文件的第 0 行第 0+1 列 对应 第 0 个文件的第 0 行第 0+1 列,有变量信息,变量在源文件中是 ‘b’ (0+1=1) AAAAD: 目标文件的第 1 行第 0 列 对应 第 0 个文件的第 0 行第 0 列,有变量信息,变量在源文件中是 ‘a’ (1-1=0) --------- 大神分割线 --------- 怎么使用 Sourcemaps Q: 线上小程序报错,我怎么通过 sourcemaps 还原到源代码中? A: 如报错 appservice.js 1:15000, 表示目标文件第一行 第 15000 列位置报错。根据上文介绍的,通过 mappings 字段算。 Q: 不会。 A: 如果你会写代码的话,参考下边 [代码]import fs = require('fs') import {SourceMapConsumer} from 'source-map' async function originalPositionFor(line, column) { const sourceMapFilePath = '如果你不真的替换的成 sourcemaps 在硬盘中的位置,那你还是放弃自己写代码吧。 ' const sourceMapConsumer = await new SourceMapConsumer(JSON.parse(fs.readFileSync(sourceMapFilePath, 'utf8'))) return sourceMapConsumer.originalPositionFor({ line, column, originalPositionFor(出错的行,出错的列) Q: 不会写代码 A: 下载最新版的开发者工具,菜单-设置-拓展设置-调试器插件 Q: 为啥都是 null? A: 每个小程序版本都应该对应一个sourcemap文件。 运营中心那里下载的 sourcemap 是对应线上最新的小程序版本。但运营中心的报错集合了多个小程序版本。拿旧小程序版本的报错信息,和最新版本的 sourcemap,是匹配不出的。开发者工具和ci 上传的时候,会提示下载对应版本的 sourcemap 信息,可以自助保存。 Q: 怎么确定有没有版本对应上 A: 下载的 sourcemap 中有个 wx 字段,标明了该 sourcemap 文件对应小程序版本号。 1.确保发生错误的小程序版本和下载回来的 sourcemap 版本是一致的。 a. 下载 sourceMap 文件,可在 mp 后台或开发者工具上传成功弹窗下载 2.确保 map 文件和发生错误的 js 文件是对应的。sourcemap 的目录和文件说明 a. APP 是主包,FULL 是整包(仅在不支持分包的低版本微信中使用),其他目录是分包 b. 每个分包下都有对应的 app-service.js.map 文件。 c. 如果是使用了按需注入特性(app.json中配置了lazyCodeLoading),那么每个分包下还会有 appservice.app.js.map(对应分包下非页面的js),和所有页面的 xxx.js.map 以上事情都确保正确之后,还是出现行列号匹配不出来的情况。那就需要进一步排查。 线上运行的小程序 sourcemap 文件是怎么生成的? 处理流程:源码 [ a.js a.js.map b.js b.js.map ] -> 开发者工具(JS转 ES5,压缩)-> 微信后台(合并 js 文件)[ appservice.app.js appservice.app.js.map]。 注意:如果源码在交给工具之前是经过了 webpack 等打包工具的处理,那源码这里需要有 map 文件。否则不需要存在 map 文件。 可以看出,map 文件经过三个步骤的处理,每个步骤都有可能导致出错,因此开发者需要先排查,是否是前两个步骤出错导致的 map 文件失效的。 如何排查前两个步骤产生的 map 文件是否有问题。 1.排查 a.js.map 文件是否有问题。 a. 可以在 a.js 的代码中写一下 throw new Error(‘test sourcemap’)。 b. 使用了 webpack 的情况下,要构建为生产环境的版本。 c. 在开发者工具模拟器中运行对应的页面,看看控制台中的报错,错误行列号是否能正常映射到源文件。 2.排查 开发者工具(JS转 ES5,压缩)步骤是否有问题。 在排查完第一步的基础上,点击预览,用微信上扫码预览,并打开调试 vConsole 功能,检查 vConsole 中是否有报错信息,检查报错信息中的行列号是否能正常映射到源文件。 如何排查 微信后台(合并 js 文件)是否有问题。 a. 一定要先排查完前两个步骤再来排查这一步,一般情况下,这一步是不会出错的。 b. 如果有问题,也只会导致 map 文件中的行号信息出现偏移。比如 Error 信息中显示报错地址是 100: 200,行号是 100。那么你可能直接用 100: 200 在 map 文件中搜索不出信息,但是如果 用 150: 200 就可以搜索出来,说明行号偏移了 50。那其他报错也可以偏移 50 后再进行搜索就找到结果。 c. 怎么排查偏移了多少?可以结合 error.message 的内容,初步判断大概错误的内容是什么。把对应的 map 文件放到这个网站上 source-map-visualization 进行搜索,找出哪些相同列号的地方。再结合 error.message 的内容进行判断。 d. 如果排查到是这一步导致的问题,请在社区上联系我们,我们会在后续版本进行修复。 依旧排查不出原因? 先整理一下按照上述步骤排查的结论,再在社区上联系我们协助
随着小程序的开发迭代,慢慢的我们会更加关注小程序的质量,今天来讲讲小程序的隐藏功能 -- 体验评分。 为什么需要体验评分 我们多做一点,就可以给用户更好的体验。(窃喜) 当然,做为开发者的我们,动动鼠标点一点就能帮助我们发现问题,是不是很愉快~~ 接下来我们来看看怎么使用体验评分? 怎么使用体验评分 体验评分的能力目前开放在【微信开发者工具 - 调试器 - Audits】 操作步骤:运行体验评分 - 一顿操作 - 获取体验报告 - 一顿优化。 (优化其实是一个圈,新代码加上之后也要继续关注哦~) [图片] 体验评分实践 我们用《小程序示例》来操作一波看看效果~ 01. 运行体验评分 使用开发者工具打开小程序,调试器区域切换到 Audits 面板,就一个“运行”按钮,点它。 [图片] 02.一顿操作 然后在工具上对小程序进行操作,比如:我点开了 “接口 - 媒体 - 音频 - 播放 ”。 [图片] 03.获取体验报告 操作完之后,点击“停止”,我们就可以获取到体验报告(简单~)。 [图片] 拿到报告之后,我们就可以看到总分 98,最佳实践 80。往下拉会有扣分的实际原因。 看第一条是 “发现正在使用废弃接口”,报告已经很清楚的告诉我们使用了废弃组件 audio,我们根据报告进行优化即可。 [图片] 04.一顿优化 按照报告优化完之后,我们可以继续进行体验评分功能确认优化是否完善。这是一个有用的圈圈⚪⚪⚪ 我们来讲几个优化过程中遇到的问题,咳咳咳 存在图片没有按原图宽高比显示 [图片] 在测试预览图片的时候,发现图片被挤了,体验评分告诉我们宽高比有问题,发现是 <image> 使用了默认的 mode (scaleToFill:缩放模式,不保持纵横比缩放图片,使图片的宽高完全拉伸至填满 image 元素)。所以通过添加 mode="aspectFill" (缩放模式,保持纵横比缩放图片,只保证图片的短边能完全显示出来。)来解决宽高比的问题。 [图片] [图片] 发现固定底部的可点击组件可能不在iPhone X安全区域内 [图片] 这个问题我们用手机测试是正常的,但是体验评分给了提示,所以就来看看实现方式是不是有问题: 原有方式:通过接口监听systemInfo.model.indexOf('iPhone X') 给 view 添加专属 class 官方推荐:官方推荐的方式是用 wxss 来兼容,不一定只有 iPhone X 下面会有安全区域 [图片] 发现正在使用废弃接口 [图片] 这个问题对一些老旧代码来说很有用,比如示例很久之前写的 auto-focus,由于基本没有改动,所以代码就一直保持不变。使用体验评分的时候检测到了这个属性是废弃属性,所以我们更换了可用属性 focus 来解决问题。 [图片] 体验评分总结 使用体验评分进行小程序示例的优化,有以下优点: 可以发现代码中使用的废弃api,避免后续踩坑根据实际操作发现相关耗时久的情况,预先发现体验问题合理的视觉/交互检测,提前做好兼容资源使用检测,用合适的资源做好小程序当然,体验过程中也有不足: 开发者工具不支持预览的 组件 / API 暂不支持体验评分(听说官方已经在努力推进啦)一起体验评分 如果你也在做小程序优化,欢迎使用体验评分来优化哦~ 预祝大家都拿 100婚 !!! [图片] 体验评分文档传送门 如果你有疑问,请在下方评论区留言给binnie,㊗️大家都没有bug,✌️✌️✌️
小程序图片压缩那些事 ~ 小程序图片压缩技术方案 1 [图片] 1 [图片] 1 非原图模式上传 [图片] 1 [图片] 1 [图片] 参考资料 本文主要整理小程序在图片压缩方面的一些资料 https://developers.weixin.qq.com/miniprogram/dev/api/media/image/wx.chooseImage.html https://developers.weixin.qq.com/miniprogram/dev/api/media/image/wx.compressImage.html
最近一直在对前期项目进行重构,遇到了之前一个悬而未解的问题,梳理下,寻找可能存在的解决方案 大家都知道微信上传图片被压缩了,但是这种情况是否能解决呢? 1 [图片] 2微信上传图片主要用到以下几个api [图片] 3 目前项目上传方案用到上述的①②两个接口,通过①选择图片,然后通过②获取图片的base64,其中在选择图片时,采用的尺寸模式为压缩。 由于该方案在减少传输和存储压力的同时,极大降低了图片的质量,导致在后续识别过程中,造成非常大的困扰。 同时,由于该方案在压缩模式选择这块,即使选择了原图,部分机型也会存在压缩的情况,并且没有一个明确的清晰的压缩策略,有时候这种压缩比非常大,同样会导致上传的身份证照片带的细节信息丢失,最后的图片甚至人眼不可识别 针对这个问题目前可供参考的解决方案是: 利用上述①③两个接口,在选择图片的时候,将图片上传到微信服务器,即通过微信的uploadImage上传到微信服务器,拿到服务器返回的文件serverId,然后通过素材管理,临时素材管理接口,根据serverId将图片下载到自己服务器,这种方案的优势在于,图片的压缩策略完全是由我们来掌控的,不管具体采用哪个压缩比,都是可以通过代码来控制。 下面简单分析下图片上传用到的几个api,传递参数以及输出相应 ①chooseImage拍照或从手机相册中选图接口 [图片] {"localIds":["wxLocalResource://6110441863775331"],"sourceType":"album","errMsg":"chooseImage:ok"} [图片] ②uploadImage上传图片接口 [图片] {"localId":"wxLocalResource://6110448596555452","serverId":"uNMAdM7ElbVX2m6bqfh77pMGD8t4u8TebDdcjOJpKidsWMKY3F0RHbQPFQp76ACB","errMsg":"uploadImage:ok"} [图片] 备注:上传图片有效期3天,可用微信多媒体接口下载图片到自己的服务器,此处获得的 serverId 即 media_id。 后端从微信服务器下拉图片 https://developers.weixin.qq.com/doc/offiaccount/Asset_Management/Get_temporary_materials.html 属于素材管理里面的获取临时素材接口 [图片] 关于access_token如何生成,具体可以参考下面链接的文档 https://developers.weixin.qq.com/doc/offiaccount/Basic_Information/Get_access_token.html 可在下面网址进行测试 https://mp.weixin.qq.com/debug/cgi-bin/apiinfo?t=index&type=%E5%9F%BA%E7%A1%80%E6%94%AF%E6%8C%81&form=%E5%A4%9A%E5%AA%92%E4%BD%93%E6%96%87%E4%BB%B6%E4%B8%8A%E4%BC%A0%E6%8E%A5%E5%8F%A3%20/media/upload [图片] 2 [图片] 具体参考文档 https://developers.weixin.qq.com/community/develop/doc/00088493fb47182c6e27b681b54c00 目前该方案已上生产,经得起实践的检验。
1、功能说明 微信内置浏览器支持的<wx-open-launch-app>开放标签可以让你的H5网页拉起APP。这个是不是很神奇也是很有必要的一个功能?微信为你想好啦~实现这个功能并不复杂,代码量可以忽略为0.但是一些相关的注意事项,准入规则还是必须要明确的,否则在开发过程中容易踩到各种坑。 2、接入逻辑 2.1 设置服务号的JS安全域名,开放标签必须在这个域名或者子域名下生效详见《微信开放标签说明文档》 2.2 注册登陆微信开放平台,新建APP审核并上架成功。然后登记域名和你的APP应用绑定关系,让他们能关联起来 3、准入门槛 看起来第二大步很简单,其实操作起来还是有点繁琐的,除去繁琐的设置外,这里有个准入门槛: 3.1 服务号门槛 服务号已认证 开放平台账号已认证 服务号与开放平台账号同主体 绑定域名和移动应用 绑定域名的要求: 域名须为当前服务号的 JS 安全域名或其子域名 域名只能同时绑定一个移动应用,因此须确保域名未被其他移动应用绑定 3.2 绑定移动应用的要求 只能绑定同一微信开放平台账号下审核通过的移动应用 3.3 绑定次数 每月可修改绑定3次 4、参考文档: 微信内网页跳转APP功能-功能介绍 | 微信开放文档 开放标签说明文档 | 微信开放文档
我们一步步来吧,包括我踩过的坑我也会还原一遍,让大家一起长长见识 遮罩层 遮罩层是最没技术难度的,写个css就可以了。 .mask { z-index: 1110; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.6); 按然后再加个bindtap,控制点击之后消失,感觉直接就可以用了呢。这里有铅笔画不出蜡笔的味道提供的一个代码片段: https://developers.weixin.qq.com/s/cvqEYzmM7Ffn 突出元素 这个才是难点所在。 1、确定元素位置。 我们首先要找出这个元素的轮廓,这个我是通过boundingClientRect实现的。 const query = wx.createSelectorQuery(); query.select("#gameInfo").boundingClientRect(); //gameInfo就是我们所需要突出展示的元素ID,后续可以用config传入 query.exec(function(res) { console.log(res); 输出如下: [图片] 可以看到这个办法很好地取到了所要突出的元素的位置。 2、画出镂空遮罩。 这里我参考了这篇文章:https://www.cnblogs.com/mxdmg/p/10427605.html 文章中的方法一就不说了,又麻烦又浪费空间。 方法二我试了下,展示效果确实不错,但是也如文中所说的,点遮罩的时候会点到底下的元素,肯定会影响效果。 方法三没试,因为感觉有跟二一样的缺点。 方法四也没试,因为一看就知道很麻烦。 方法五把我导向了mask-image,想说能否用这个css属性在遮罩层上挖个洞出来。关于这方面的应用我觉得这篇文章写得挺好的:https://www.zhangxinxu.com/wordpress/2017/11/css-css3-mask-masks/。然而一一试过后,发现这个属性在小程序中好像用不了(也有可能是我哪里出错,反正就是没法生效)。 呵呵。。。以上几种方式折腾了一个晚上。 后来想想,干脆抛开幻想抛开依赖,自力更生。 不搜了,自己土办法画一个。 [图片] 我用四个view作为遮罩,框出了需要突出的元素。 <view wx:if="{{showGuide}}" class="mask" bindtap="getHidden"> <view class="mask_block" style="width:100%;top:0px;left:0px;height:{{showArea.top}}px"></view> //上 <view class="mask_block" style="width:100%;top:{{showArea.bottom}}px;left:0px;height:100%"></view> //下 <view class="mask_block" style="height:{{showArea.height}}px;top:{{showArea.top}}px;left:0px;width:{{showArea.left}}px"></view> //左 <view class="mask_block" style="height:{{showArea.height}}px;top:{{showArea.top}}px;left:{{showArea.right}}px;width:100%"></view> //右 </view> 上面的showArea就是第一步中取到的元素位置。 这样一来,遮罩中的元素突出就解决了。 3、还有一个坑 本来以为这一步搞定的时候,发现这个镂空的位置竟然还会漂移!在开发工具和真机中的位置不一样,就算同在真机中,这次展示和下次展示的位置也有可能不一样! 这TM是薛定谔的镂空框! [图片] 这就是当时的神奇漂移。 经验告诉我,会出现这种不确定性的—— 十之八九都和元素的加载是否完成有关系 请好好记住这句话,可以节省你至少一天的调试时间。 是的,坑就在于步骤一的 query.select("#gameInfo").boundingClientRect(); 什么时候取的很关键。在小程序布局完成前,你可能会取到一个偏差值,导致了后面的踩坑。 我之前是在组件生命周期(还不清楚组件生命周期的看这里)中attached环节执行,不行。发现手册中还有一个ready,感觉和page中的onReady是一个东西,然而还是不行。。。 看来还是必须等页面的onReady触发之后才能取到完全正确的值。 所以我必须在主页面的onReady事件发生后再来做这个事情。 那么如何确定onReady呢?相当于我必须能够在onReady事件中去调用用户引导组件的初始化函数。。。想来想去,他们两者之间能够互动的也只有setData了。 也就是数据监听器,不懂的点这里。 幸好按计划本来也是要传入配置数组的。于是在pages中的onReady传入配置数组,然后在组件中对配置数组进行监听。 observers: { 'config': function(config) { if (config) { this.initMask(config); 如此一来,遮罩层跟元素的突出展示就算完成了。 当然,这样的遮罩层还是有些不足的,例如只支持方形镂空,如果是圆形元素看着就不那么美观。。。好在我目前没有这方面需求,以后有遇到再说吧。
近年,不论是正在快速增长的直播,远程教育以及IM聊天场景,还是在常规企业级系统中用到的系统提醒,对websocket的需求越来越大,对websocket的要求也越来越高。从早期对websocket的应用仅限于少部分功能和IM等特殊场景,逐步发展为追求支持高并发,百万、千万级每秒通讯的高可用websocket服务。 面对各种新场景对websocket功能和性能越来越高的需求,不同的团队有不同的选择,有的直接使用由专业团队开发的成熟稳定的第三方websocket服务,有些则选择自建websocket服务。 作为一个具有多年websocket开发经验的老程序猿,经历了GoEasy企业级websocket服务从无到有,从小到大的过程,此文是根据过去几年在GoEasy开发过程中踩过的坑,以及为众多开发团队提供websocket服务、与众多开发者交流中的总结的一些经验和体会。 这次主要从搭建websocket服务的基本功能和特性方面做一些分享,下次有机会再从构建一个高可用websocket时要面对的高并发,海量消息,集群容灾,横向扩展,以及自动化运维等方面进更多的分享。 以下几点是个人认为在构建websocket服务时必须要考虑的一些技术特性以及能显著提高用户体验的功能,供各位同学参考: 1.建立心跳机制 心跳机制几乎是所有网络编程的第一步,经常容易被新手忽略。因为在websocket长连接中,客户端和服务端并不会一直通信,如果双方长期没有沟通则都不清楚彼此当前状态,所以需要发送一段很小的报文告诉对方“我还活着”。另外还有两个目的: 服务端检测到某个客户端迟迟没有心跳过来可以主动关闭通道,让它下线; 客户端检测到某个服务端迟迟没有响应心跳也能重连获取一个新的连接。 2.建立具有良好兼容性的客户端SDK 虽说现在主流浏览器都支持websocket,但在编码中还是会遇到浏览器兼容性问题,而且通过websocket通信的客户端早已不仅限于各种web浏览器,还包括越来越多的APP,小程序。因此就要求构建的websocket服务必须能够很友好的支持各种客户端。最好的方式就是构建一个能够兼容所有主流浏览器、小程序和APP,以及uni-app、vue、react-native等目前常见的各种前端框架的客户端SDK,这样不论公司的各个项目使用什么样的前端技术,都能够快速的集成websocket服务。 3.断网自动重连和消息补发机制 移动互联网时代,终端用户所处的网络环境多样且复杂,如用户进出电梯,出入地下室或地铁等网络不稳定的场所,或其他原因导致的网络不稳定都是很常见的场景。因此,一个可靠的websocket服务必须具备完善的断网自动重连机制。确保断网后,网络一旦恢复,能第一时间自动重新建立长连接,并且能够立即补发在网络不稳定期间发送的消息。 4.离线消息 基础的Websocket通讯从技术上来说,消息送达的前提条件就是建立起一个长连接,没有建立网络连接就来讨论通讯那是耍流氓。但是从使用者的角度上来说,随手关闭浏览器,或者将小程序、APP进程直接杀掉而导致网络连接断开的情况是随时都在发生的。然后我们下意识的期待,就是我下次打开浏览器访问网页,或者打开APP时,能够收到用户离开系统期间的所有信息。从技术上这是一个跟websocket没有多大关系的需求,但实际上却是websocket服务不可或缺的基本特性,也是一个能够极大提升用户体验的功能。 5.上下线提醒,客户端在线列表 掌握当前系统有哪些用户在线,捕捉用户上下线事件,是搭建一个企业级websocket服务,必不可少的特性,尤其是开发IM和游戏类产品。 6.支持历史消息查询 websocket服务,某种意义也是属于一个消息系统,对于历史消息的查询需求,是无法绕开的话题。比如IM系统中常见的历史消息,因此在websocket服务内部实现一个高速,可靠的消息队列机制来支持websocket服务实现历史消息的查询就是一个必须的工作。 7.消息的压缩机制 不论是为了保证消息通讯的速度和实时性,还是为了节约流量和带宽费用,或者是出于提高网卡的使用效率和增加系统的吞吐量,在通讯过程中对消息进行必要的压缩都是必不可少的。 除了需要考虑以上七点以外,笔者认为,还有几个问题也是很值得初学者积极关注的: 1.缓存和持久化 选择合适的消息缓存机制,是企业级websocket服务保证性能必须要考虑的问题。 2.异步调用 要支持大量消息通讯的高性能系统,必然推荐异步调用。若设计为同步调用,调用方就需要一直等待被调用方完成。如果一层一层的同步调用下去,所有的调用方需要相同的等待时间,调用方的资源会被大量的浪费。更糟糕的是一旦被调用方出问题,其他调用就会出现多米诺骨牌效应跟着出问题,导致故障蔓延。收到请求立即返回结果,然后再异步执行,不仅可以增加系统的吞吐量,最大的好处是让服务之间的解耦更为彻底。 3.独立于业务和标准化 尽管在一个web项目中可以同时存在常规http服务和websocket服务,尤其对性能要求不高的单应用web系统,这种方式更简单,更便于维护。但对于性能和可用性高的企业级系统或者互联网平台,更好的方式,是将websocket服务作为一个单独的微服务来进行设计,避免和常规的http服务抢占资源,导致系统性能不可控,同时也更便于横向扩展。 一个设计良好的企业级websocket服务应该是一个独立于业务系统、标准化的单独存在的技术性微服务,能够作为公司基础架构的一部分为公司的所有项目提供通讯服务。 4.幂等性和重复消息的过滤 所谓幂等性,就是一次和多次请求一个接口都应该具有同样的后果。为什么需要?对每个接口的调用都会有三种可能的结果:成功,失败和超时。对最后一种的原因很多可能是网络丢包,可能请求没有到达,也有可能返回没有收到。于是在对接口的调用时往往都会有重试机制,但重试机制很容易导致消息的重复发送,从用户层面这往往是不可接受的,因此在接口的设计时,我们就需要考虑接口的幂等性,确保同一条消息发送一次和十次都不回导致消息的重复到达。 5.支持QoS 服务质量分级 其实对于上一点消息重复的问题,行业已经有了解决方案和标准规范,对于消息到达率和重复,常用的手段就是通过消息确认的方式来确保消息到达,要求越高,意味着确认机制越复杂,成本越高。为了在成本和到达率之间有很好的平衡,通常对消息系统的服务质量(QoS)分为以下三个级别 : QoS 0(At most once):“最多发一次”,意味着发送就可以了,不需要确认机制,发送了即可,适用于要求不高的场景,可以接受一定的不到达率,成本最低。 QoS 1(At least once):“至少发一次”,意味着发送方必须明确收到接收方的确认信号,否则就会反复发,每条消息至少需要两次通信来确认到达,可以接受一些消息被重发,但成本不高 。 QoS 2(Exactly once):“确保只发一次”,意味着每条消息只能到达一次,且不允许重复到达,为了达到这个目标就需要双方至少通讯三次,成本最高。 一个完善的websocket服务面对不同的应用场景,应该能够支持选择不同等级的QoS,在成本和服务质量之间取得平衡。 虽然websocket已经广泛的应用于各种系统和平台,但如果要搭建一个满足企业级或者大型互联网平台的可靠、安全稳定的websocket服务,对于没有经验的同学,在具体的技术实践过程依然是有不少的坑要踩。 对websocket服务有较高要求,选择成熟可靠的第三方websocket服务其实也是一个成本更低和高效的选择。GoEasy作为国内领先的第三方websocket消息平台,已经稳定运行了5年时间,支持千万级消息并发,除了兼容所有常见的浏览器以外,同时也兼容uni-app,各种小程序,以及vue、react-native等常见的前端框架。 希望本文能为初次搭建websocket服务的同学在思路上有所帮助和参考,也欢迎各位前辈多多批评指正,同时也希望未来有机会就更多的技术与大家进行交流。 GoEasy官网:https://www.goeasy.io/ GoEasy系列教程: 搭建websocket消息推送服务,必须要考虑的几个问题 websocket IM聊天教程-教你用GoEasy快速实现IM聊天 Websocket直播间聊天室教程-GoEasy快速实现聊天室 微信小程序使用GoEasy实现websocket实时通讯 Uniapp使用GoEasy实现websocket实时通讯 IM聊天教程:发送图片/视频/语音/表情
1.关于onshow优化方案,onshow每次页面切换都会调用接口,但是有种场景下的确要用到onshow,比如说登陆成功之后刷新首页接口,但是吧,刷新完成之后你又不想在onshow调用接口,你可以在onload调用一波接口,这个时候你可以在全局app.js设置一个flag开关默认为false,登陆成功之后将全局的flag开关重置为true,当前页面onshow判断if(app.globalData.flag){/*调用接口相关操作*/,}调用完接口之后在将flag重置为false,这样就减少了onshow的每次切换接口都会请求相关没必要操作。 2.关于setData,一些没必要的参数重置可以不用setData,比如说商品列表页面跳转商品详情页面需要传入一个商品唯一标示,这个时候很多人习惯在onload里面判断if(options&&options.itemUuid){ /*itemUuid你可以理解为商品唯一标示*/ this.setData({ itemUuid }) this.getItemList() },这个时候itemUuid不需要动态更新,你可以这样做减少setData调用(不懂的setData小伙伴可以了解下下diff算法), if(options&&options.itemUuid){ /*itemUuid你可以理解为商品唯一标示*/ this.data. itemUuid = options.itemUuid this.getItemList() } 3.关于接口请求等待优化,可以做一些骨架屏、gif动画类似,微信开发者工具最新版右下角又三个点点,打开生成骨架屏会在当前文件夹生成一套wxss,wxml的相关骨架屏文件,开发者只需要在wxml和wxss引入骨架屏的样式就可以,还是蛮方便的哟 今天先到这里,改天再来更新!!!!
流行是一种轮回,今年新拟态的风格在设计圈非常火,虽然这个设计风格存在一些问题,但个人也是蛮喜欢的,于是就把这个应用到自己的小程序里了。 一、新拟态定义和特点 [图片] 通过观察,我们发现新拟态有如下特点: 只有一个光源,左上角亮色投影,右下角深色投影常用与按钮组件和卡片上与背景对比度较弱分为两种状态,凹下去和凸出来二、代码实现 .neumorphism{ box-shadow: -7px -7px 20px 0px #fff9, -4px -4px 5px 0px #fff9, 7px 7px 20px 0px #0002, 4px 4px 5px 0px #0001; .neumorphismin,.neumorphism:active{ box-shadow: 0px 0px 0px 0px #fff9, 0px 0px 0px 0px #fff9, 0px 0px 0px 0px #0001, 0px 0px 0px 0px #0001, inset -7px -7px 20px 0px #fff9, inset -4px -4px 5px 0px #fff9, inset 7px 7px 20px 0px #0003, inset 4px 4px 5px 0px #0001; 在使用的过程中,只需要在原来的按钮加上class即可,点击态自动加上凹下去效果。 凸起:class ="neumorphism" 凹下:class ="neumorphismin" 实现前后对比图: [图片]
小程序中要把图片缩小再上传,可使用画布组件(canvas)缩小图片。在 wxml 代码中定义画布,位置应在屏幕外,这样就像是在后台处理图片而不显示在屏幕上。wxml 文件中的 canvas 代码: <view style="width:0px;height:0px;overflow:hidden;"> <canvas canvas-id="canvasid1" style="width:600px;height:600px;top:-800px;left:-800px;background-color:#cdcdcd;border:1px solid blue;"> </canvas> </view> 这段代码可处于 wxml 文件的末尾处。 要处理的图片不止一张,在缩小图片的代码中,用递归调用方式: function resize_recursion() { // canvas : resize ctx1.drawImage(arr1[i].file, 0, 0, arr1[i].widthx, arr1[i].heightx) ctx1.draw(false, res => { var lca = wx.canvasToTempFilePath({ x: 0, y: 0, width: arr1[i].widthx, height: arr1[i].heightx, canvasId: 'canvasid1', quality: 1, success: res => { arr1[i].file1 = res.tempFilePath i = i + 1 if(i==arr1.length){ lca = uploadproc() return }else{ return resize_recursion() } }, fail: function (err) { console.log(err) } }) }) // end of draw } 上传图片用到 js 的 Promise对象,提高传输效率: var arrPromise1 = new Array() for (i = 0; i < arr1.length; i++) { arrPromise1.push(new Promise(function (resolve, reject) { wx.cloud.uploadFile({ cloudPath: arr1[i].file1, filePath: arr1[i].file2, success: res => { resolve(res) }, fail: error => { reject(error) } }) })) } Promise.all(arrPromise1).then(res => { for (var i = 0; i < res.length; i++) { arr1[i].upfileId = res[i].fileID } } 图片文件最初来自交互操作:wx.chooseimage(),选定的图片存放在数组arr1中。然后读取图片的尺寸,根据大小来决定是否需要执行缩小代码。这是读取图片大小的代码,也用到 js 的 Promise对象: var arrPromise1 = new Array() for (i = 0; i < arr1.length; i++) { arr1[i] = { "file": arr1[i].path, "file1": '', "upfileId": '', "size": arr1[i].size, "width1": 0, "height1": 0, "widthx": 0, "heightx": 0, "flag": 0, "idx1": 0 } arrPromise1.push(new Promise(function (resolve, reject) { wx.getImageInfo({ src: arr1[i].file, success: res => { resolve(res) }, fail: error => { reject(error) } }) })) } Promise.all(arrPromise1).then(res => { for (i = 0; i < res.length; i++) { arr1[i].width1 = res[i].width arr1[i].height1 = res[i].height arr1[i].widthx = 200 arr1[i].heightx = 350 arr1[i].flag = lca.flagx arr1[i].idx1 = i } }, function (res) { console.log('promiseerr') }) 整个过程中,读取图片大小和上传可以用 Promise对象,缩小图片因为要用画布组件而无法使用Promise。[END]
应需求开发一个微信小程序功能,一个列表中每一项都放一个分享按钮,分享当前类目中的不同标题、封面、链接。 思路一 touchstart事件会比tap事件靠前,先来验证一下 js代码 Page({ data: { list : [ id : 1, cover : '图片1链接', title: '标题1' id : 2, cover : '图片2链接', title: '标题2' onShareAppMessage(e) { console.log('onShareAppMessage'); touchstartHandle(e) { console.log('touchstartHandle'); xml代码 <block wx:for="{{ list }}" wx:key="i"> <button type="primary" data-info="{{ item }}" bindtouchstart="touchstartHandle" open-type="share">{{ item.title }}</button> </block> 结果,第一次的执行顺序是onShareAppMessage => touchstartHandle,第二次正常touchstartHandle => onShareAppMessage。坑看来比较大 思路二 https://mp.weixin.qq.com/s/VlD1XMhyPrRaS1Uh7-mtsQ
//把当前画布指定区域的内容导出生成指定大小的图片 canvasToTempFilePath({ canvasId: 'shareCanvas' }).then((res) => { //获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限。 getSetting().then((getSettingRes) => { //判断是否有过“scope.writePhotosAlbum”这个授权 if(!getSettingRes.authSetting["scope.writePhotosAlbum"]){ //没有授权过则提前向用户发起授权请求 authorize({ scope: "scope.writePhotosAlbum", }).then((res) => { console.log("authorize",res) },(err) => { //授权失败则提示 因为官方调整有按钮才能调起openSetting这个方法 wx.showModal({ title: '提示', content: '若点击不授权,将无法使用保存图片功能', cancelText:'不授权', cancelColor:'#999', confirmText:'授权', confirmColor:'#f94218', success(res) { if (res.confirm) { //调起客户端小程序设置界面,返回用户设置的操作结果。 //设置界面只会出现小程序已经向用户请求过的权限。 openSetting() } else if (res.cancel) { console.log('用户点击取消') }else{ //授权过则调用保存图片到相册的方法 saveImageToPhotosAlbum({ filePath: res.tempFilePath }).then((res) => { console.log("saveImageToPhotosAlbum",res) wx.showToast({ title: "图片保存成功", icon: 'none', duration: 2000 this.setData({ sharehidden: true, 提示:以上方法皆是微信原生api的异步封装
1.这里使用了自定义导航,然后通过监听动态来改变background的rgba透明值达到效果 2.滚动监听setData是一件很消耗性能的问题,在这里使用了事件节流,及判断了scrollTop 最终特效[图片][图片][图片][图片] 代码片段:https://developers.weixin.qq.com/s/KyKkpmmb7hhi 时间紧迫前期只做了动态实现顶部导航渐变,后面会完善动态锚点及锚点监听 原理也很简单 依据 <scroll-view scroll-into-view="{{toView}}" scroll-with-animation="true" scroll-y="true"> 既可以实现
思路是先把整个编辑器的字体也就是父级元素设置为0大小,然后在插入子元素时把子元素的字体设置回去,为了在插入图片后方便光标定位编辑,所以在插入图片完成后再自动换行和设置字体大小 首先把编辑器字体设置为0,把 padding也设置为0[图片]然后在编辑器渲染时把字体设置进去[图片] 其后在添加完图片后自动换行[图片]
cardSwipe - 小程序卡片滑动组件 此组件是使用原生微信小程序代码开发的一款高性能的卡片滑动组件,无外部依赖,导入即可使用。其主要功能效果类似探探的卡片滑动,支持循环,新增,删除,以及替换卡片。 [代码]git clone https://github.com/1esse/cardSwipe.git 相关文件介绍: /components /card /cardSwipe /pages /index 其中,components文件夹下的card组件是cardSwipe组件的抽象节点,放置卡片内容,需要调用者自己实现。而cardSwipe组件为卡片功能的具体实现。pages下的index为调用组件的页面,可供参考。 支持热循环(循环与不循环动态切换),动态新增,动态删除以及动态替换卡片 卡片的wxml节点数不受卡片列表的大小影响,只等于卡片展示数,如果每次只展示三张卡片,那么卡片所代表的节点数只有三个 支持调节各种属性(滑动速度,旋转角度,卡片展示数…等等) 节点数少,可配置属性多,自由化程度高,容易嵌入各种页面且性能高 实现方式: 循环/不循环: 属性circling(true/false)控制 新增: 向卡片的循环数组中添加(不推荐新增,具体原因后面分析。硬要新增的话,如果卡片列表不大,并且需要新增多张卡片,可以直接将数据push到卡片列表中然后setData整个数组。如果是每次只增加一张卡片,推荐使用下面这种方式,以数据路径的形式赋值) [代码]this.setData({ [`cards[${cards.length}]`]: { title: `新增卡片${cards.length + 1}`, // ... [代码]// removeIndex: Number,需要删除的卡片的索引,将删除的卡片的值设置为null // removed_cards: Array,收集已删除的卡片的索引,每次删除卡片都需要更新 this.setData({ [`cards[${removeIndex}]`]: null, removed_cards [代码]// replaceIndex:Number,需要替换的卡片的索引 // removed_cards: Array,收集已删除的卡片的索引,如果replaceIndex的卡片是已删除的卡片的话,需要将该卡片索引移出removed_cards this.setData({ [`cards[${replaceIndex}]`]: { title: `替换卡片${replaceIndex}`, // ... // removed_cards 注意:删除和替换操作都需要同步removed_cards why?为什么使用removed_cards而不直接删除数组中的元素 由于小程序的机制,逻辑层和视图层的通信需要使用setData来完成。这对大数组以及对象特别不友好。如果setData的数据太多,会导致通信时间过长。又碰巧数组删除元素的操作无法通过数据路径的形式给出,这会导致每次删除都需要重新setData整张卡片列表,如果列表数据过大,将会引发性能问题。 删除的时候,如果删除的卡片索引在当前索引之前,那么当前索引所代表的卡片将会是原来的下一张,导致混乱。 保留删除元素,为卡片列表的替换以及删除提供更方便,快捷,稳定的操作。 由于组件支持动态的删除以及替换,这使得我们可以以很小的卡片列表来展示超多(or 无限)的卡片 场景1:实现一个超多的卡片展示(比如1000张) 实现思路:以分页的形式每次从后台获取数据,先获取第一页和第二页的数据。在逻辑层(js)创建一个卡片列表,把第一页数据赋值给它,用于跟视图层(wxml)通信。开启循环,用户每滑动一次,将划过的卡片替换成第二页相同索引的卡片,第一页卡片全部划过,第二页的内容也已经同步替换完毕,再次请求后台,获取第三页的内容,以此类推。到最后一页的时候,当前索引为0时,关闭循环,删除最后一页替换不到的上一页剩余的卡片 场景2:实现一个无限循环的卡片 实现思路:类似场景1。不关闭循环。 为什么不建议新增卡片 新增卡片会增加卡片列表的长度,由于每次滑动都要重新计算卡片列表中所有卡片的状态,卡片列表越大,预示着每次滑动卡片需要计算状态的卡片越多,越消耗性能。在完全可以开启循环然后用替换卡片操作代替的情况下,不推荐新增卡片。建议卡片列表大小保持在10以内以保证最佳性能。 以下为卡片列表大小在每次滑动时对性能的影响(指再次渲染耗时) 注:不同手机可能结果不同,但是耗时差距的原因是一样的 耗时(ms/毫秒) 10张卡片 100张卡片 1000张卡片
模拟器的小眼睛旁边的有个骨架屏功能,如下图: [图片] 这个一点进去,挖,不得了啊,马上生成了当前页面的骨架屏幕代码XXX.skeleton.wxml和XXX.skeleton.wxss在同级目录下 [图片] (上图为开发者工具生成的骨架屏代码,里面的参考文档链接目前是404状态) 然后在你的主wxml里引用后写相关代码就可以啦: [图片] 1、import上一步生成的wxml。(wxss里也导入对应的wxss) 2、插入template,这个template的展示逻辑由wx:if的isLoading控制,原来的正常显示代码用wx:else显示。isLoading初始化为true,表示先显示骨架屏,等你数据请求处理完后setData isLoading为false渲染你原来的正常页面。 你就能看到没加载完成数据前的骨架屏效果啦啦啦~~
eval5是基于TypeScript编写的JavaScript解释器,100%支持ES5语法。 支持浏览器、node.js、小程序等 JavaScript 运行环境 。 项目地址: https://github.com/bplok20010/eval5 浏览器环境中需要沙盒环境来执行JavaScript代码 浏览器环境控制代码执行时长 不支持eval/Function的JavaScript运行环境,如:微信小程序 示例 1.4.5 修复with语句中函数调用时丢失this信息 1.4.4 修复在未使用try-catch情况下出现异常时导致下次调用evaluate时的变量声明错乱问题。 1.4.3 修复 WithStatement 中赋值不生效问题。 rootContext创建调整为:Object.create(options.rootContext),防污染。 1.4.2 新增内置对象:URIError RangeError SyntaxError ReferenceError 修复 assignment 表达式触发对象的getter方法调用 1.4.1 修复再次执行事超时机制失效问题 修复函数表达式赋值时引起的返回值错乱问题 1.4.0 解释器内部eval/Function重写 新增参数 options.rootContext 新增参数 options.globalContextInFunction 移除Interpreter.rootContext eval5先将源码编译得到树状结构的抽象语法树(AST)。 抽象语法树由不同的节点组成,每个节点的type标识着不同的语句或表达式,例如: 1+1的抽象语法树 [代码]{ "type": "Program", "body": [ "type": "ExpressionStatement", "expression": { "type": "BinaryExpression", "operator": "+", "left": { "type": "Literal", "value": 1, "raw": "1" "right": { "type": "Literal", "value": 1, "raw": "1" "sourceType": "script" 根据节点type编写不同的处理模块并得到最终结果。例如:根据1+1的语法树我们可以写出一下解释器代码: [代码]function handleBinaryExpression(node) { switch( node.operator ) { case '+': return node.left.value + node.right.value; case '-': return node.left.value - node.right.value; 以下是解析echarts4效果示例:
腾讯云 IM 的直播大群,又叫音视频聊天室(AVChatRoom)有以下特点: - **适用于互动直播场景,群成员人数无上限**。 - **支持针对涉黄、涉政以及不雅词的安全打击,满足安全监管需求**。 - 支持向全体在线用户推送消息(群系统通知)。 - Web 和微信小程序端支持以游客身份(即不登录)接收消息。 - 申请加群后,无需管理员审批,直接加入。 >?本文以 Web 和微信小程序端 SDK 为例,其他端 SDK 实现流程相同,操作略有差异。 ## 适用场景 #### 直播弹幕 AVChatRoom 支持弹幕、 送礼和点赞等多消息类型,轻松打造良好的直播聊天互动体验;提供弹幕内容审核能力,保证您的直播免受不雅信息干扰。 [图片] #### 网红带货 AVChatRoom 与商业直播相结合,通过提供点赞、询价、购物券等特定消息类型,帮助直播客户实现流量变现。 [图片] #### 教学白板 AVChatRoom 可提供在线课堂、文本消息、画笔轨迹等能力,轻松实现教师学生沟通、画笔轨迹保存、大班课与小班课教学等教学场景。 [图片] ## 使用限制 - 不支持撤回消息。 - 群主不可以退群,群主退群只能通过解散群组的方式。 - 不支持移除群成员。 ## 相关文档 - 群组管理 - 群组系统 - SDK 下载 - 更新日志(Web & 小程序) - SDK 手册 - 集成 SDK(Web & 小程序) - 初始化(Web & 小程序) - 登录(Web & 小程序) - 消息收发(Web & 小程序) - 群组管理(Web & 小程序) ## 使用指南 ### 步骤1:创建应用 1. 登录 即时通信 IM 控制台。 >?如果您已有应用,请记录其 SDKAppID,并执行 [步骤2](#Step2)。 >同一个腾讯云账号,最多可创建100个即时通信 IM 应用。若已有100个应用,您可以先 停用 并删除无需使用的应用后再创建新的应用。**应用删除后,该 SDKAppID 对应的所有数据和服务不可恢复,请谨慎操作。** 2. 单击【+添加新应用】。 3. 在【创建应用】对话框中输入您的应用名称,单击【确定】。创建完成后,可在控制台总览页查看新建应用的状态、业务版本、SDKAppID、创建时间以及到期时间。 4. 记录该应用的 SDKAppID 信息。 ### 步骤2:创建 AVChatRoom 您可以通过控制台创建群组,也可以通过调用 创建群组 API 创建群组。本文以通过控制台创建为例。 1. 登录 即时通信 IM 控制台,单击目标应用卡片。 2. 在左侧导航栏选择【群组管理】,单击【添加群组】。 3. 输入群名称,选填群主 ID,选择【群类型】为【互动直播聊天室】。 4. 单击【确定】,待群组创建成功后,记录其【群ID】(本文以`@TGS#aC72FIKG3`为例)。 ### 步骤3:集成 SDK 您可以通过 NPM 或 Script 集成 SDK,推荐使用 NPM 集成。本文以使用 NPM 集成为例。 - Web 项目 // Web 项目 npm install tim-js-sdk --save-dev - 小程序项目 // 微信小程序项目 npm install tim-wx-sdk --save-dev >?若同步依赖过程中出现问题,请切换 npm 源后再次重试。 // 切换 cnpm 源 npm config set registry http://r.cnpmjs.org/ ### 步骤4:创建 SDK 实例 // 创建 SDK 实例,TIM.create() 方法对于同一个 SDKAppID 只会返回同一份实例 let options = { SDKAppID: 0 // 接入时需要将0替换为您的即时通信应用的 SDKAppID let tim = TIM.create(options) // SDK 实例通常用 tim 表示 // 设置 SDK 日志输出级别,详细分级请参考 setLogLevel 接口的说明 tim.setLogLevel(0) // 普通级别,日志量较多,接入时建议使用 tim.on(TIM.EVENT.SDK_READY, function (event) { // SDK ready 后接入侧才可以调用 sendMessage 等需要鉴权的接口,否则会提示失败! // event.name - TIM.EVENT.SDK_READY tim.on(TIM.EVENT.MESSAGE_RECEIVED, function(event) { // 收到推送的单聊、群聊、群提示、群系统通知的新消息,可通过遍历 event.data 获取消息列表数据并渲染到页面 // event.name - TIM.EVENT.MESSAGE_RECEIVED // event.data - 存储 Message 对象的数组 - [Message] const length = event.data.length let message for (let i = 0; i < length; i++) { // Message 实例的详细数据结构请参考 Message // 其中 type 和 payload 属性需要重点关注 // 从v2.6.0起,AVChatRoom 内的群聊消息,进群退群等群提示消息,增加了 nick(昵称) 和 avatar(头像URL) 属性,便于接入侧做体验更好的展示 // 前提您需要先调用 updateMyProfile 设置自己的 nick(昵称) 和 avatar(头像URL),请参考 updateMyProfile 接口的说明 message = event.data[i] switch (message.type) { case TIM.TYPES.MSG_TEXT: // 收到了文本消息 this._handleTextMsg(message) break case TIM.TYPES.MSG_CUSTOM // 收到了自定义消息 this._handleCustomMsg(message) break case TIM.TYPES.MSG_GRP_TIP: // 收到了群提示消息,如成员进群、群成员退群 this._handleGroupTip(message) break case TIM.TYPES.MSG_GRP_SYS_NOTICE: // 收到了群系统通知,通过 REST API 在群组中发送的系统通知请参考 在群组中发送系统通知 API this._handleGroupSystemNotice(message) break default: break _handleTextMsg(message) { // 详细数据结构请参考 TextPayload 接口的说明 console.log(message.payload.text) // 文本消息内容 _handleCustomMsg(message) { // 详细数据结构请参考 CustomPayload 接口的说明 console.log(message.payload) _handleGroupTip(message) { // 详细数据结构请参考 GroupTipPayload 接口的说明 switch (message.payload.operationType) { case TIM.TYPES.GRP_TIP_MBR_JOIN: // 有成员加群 break case TIM.TYPES.GRP_TIP_MBR_QUIT: // 有群成员退群 break case TIM.TYPES.GRP_TIP_MBR_KICKED_OUT: // 有群成员被踢出群 break case TIM.TYPES.GRP_TIP_MBR_SET_ADMIN: // 有群成员被设为管理员 break case TIM.TYPES.GRP_TIP_MBR_CANCELED_ADMIN: // 有群成员被撤销管理员 break case TIM.TYPES.GRP_TIP_GRP_PROFILE_UPDATED: // 群组资料变更 //从v2.6.0起支持群组自定义字段变更内容 // message.payload.newGroupProfile.groupCustomField break case TIM.TYPES.GRP_TIP_MBR_PROFILE_UPDATED: // 群成员资料变更,例如群成员被禁言 break default: break _handleGroupSystemNotice(message) { // 详细数据结构请参考 GroupSystemNoticePayload 接口的说明 console.log(message.payload.userDefinedField) // 用户自定义字段。使用 RESTAPI 发送群系统通知时,可在该属性值中拿到自定义通知的内容。 // 用 REST API 发送群系统通知请参考 在群组中发送系统通知 API ### 步骤5:登录 SDK let promise = tim.login({userID: 'your userID', userSig: 'your userSig'}); promise.then(function(imResponse) { console.log(imResponse.data); // 登录成功 }).catch(function(imError) { console.warn('login error:', imError); // 登录失败的相关信息 ### 步骤6:设置自己的昵称和头像 2.6.2及以上版本 SDK,AVChatRoom 内的群聊消息和群提示消息(例如进群退群等),都增加了 nick(昵称) 和 avatar(头像URL) 属性,您可以调用接口 updateMyProfile 设置自己的 nick(昵称) 和 avatar(头像URL)。 // 从v2.6.0起,AVChatRoom 内的群聊消息,进群退群等群提示消息,增加了 nick(昵称) 和 avatar(头像URL) 属性,便于接入侧做体验更好的展示,前提您需要先调用 updateMyProfile 设置个人资料。 // 修改个人标配资料 let promise = tim.updateMyProfile({ nick: '我的昵称', avatar: 'http(s)://url/to/image.jpg' promise.then(function(imResponse) { console.log(imResponse.data); // 更新资料成功 }).catch(function(imError) { console.warn('updateMyProfile error:', imError); // 更新资料失败的相关信息 ### 步骤7:加入群组 // 匿名用户加入(无需登录,入群后仅能接收消息) let promise = tim.joinGroup({ groupID: 'avchatroom_groupID'}); promise.then(function(imResponse) { switch (imResponse.data.status) { case TIM.TYPES.JOIN_STATUS_WAIT_APPROVAL: // 等待管理员同意 break case TIM.TYPES.JOIN_STATUS_SUCCESS: // 加群成功 console.log(imResponse.data.group) // 加入的群组资料 break case TIM.TYPES.JOIN_STATUS_ALREADY_IN_GROUP: // 已经在群中 break default: break }).catch(function(imError){ console.warn('joinGroup error:', imError) // 申请加群失败的相关信息 ### 步骤8:创建消息实例并发送 本文以发送文本消息为例。 // 发送文本消息,Web 端与小程序端相同 // 1. 创建消息实例,接口返回的实例可以上屏 let message = tim.createTextMessage({ to: 'avchatroom_groupID', conversationType: TIM.TYPES.CONV_GROUP, // 消息优先级,用于群聊(v2.4.2起支持)。如果某个群的消息超过了频率限制,后台会优先下发高优先级的消息,详细请参考 消息优先级与频率控制// 支持的枚举值:TIM.TYPES.MSG_PRIORITY_HIGH, TIM.TYPES.MSG_PRIORITY_NORMAL(默认), TIM.TYPES.MSG_PRIORITY_LOW, TIM.TYPES.MSG_PRIORITY_LOWESTpriority: TIM.TYPES.MSG_PRIORITY_NORMAL, payload: { text: 'Hello world!' // 2. 发送消息 let promise = tim.sendMessage(message) promise.then(function(imResponse) { // 发送成功console.log(imResponse) }).catch(function(imError) { // 发送失败console.warn('sendMessage error:', imError)
首先先指出问题:我认为微信开发者工具对于calc()这个css语法存在检测性不好的情况 建议开发团队解决这个bug 其次我在说怎么解决这个问题 left: calc(0px - (100% - 80rpx) / 2); 找到图上的代码 它在colorui/main.wxss文件下的2760行左右(仔细看是左右不一定是2760行)把它改成left: calc(0rpx - (100% - 80rpx) / 2); 就好了 它的问题是微信开发者工具检测不好这个属性的px和rpx, 导致测试不准, 同学们可以多试试这个属性看看到底是哪里有问题 ,但这个问也有colorui开发团队的失误 , 问题已经反应 ,后续团队会更新这个小问题 ,最后上传一张真机调试线错乱的图片 [图片] 修改完成后线就会恢复正常 [图片] 请看仔细啊 一定要真机调试才能看出来!!!!!!!
https://developers.weixin.qq.com/s/XPgINQmv7ScN
小程序直播文档只说明用flv,rtmp格式,小程序直播视频源用websocket flv通讯可以吗?
[代码]var[代码] [代码]socketOpen = [代码][代码]false[代码][代码];[代码][代码]var[代码] [代码]frameBuffer_Data, session, SocketTask;[代码][代码]var[代码] [代码]url = [代码][代码]'wss://...'[代码][代码];[代码] [代码]Page({[代码][代码] [代码][代码]data: {[代码][代码] [代码][代码]toView: [代码][代码]'green'[代码][代码],[代码][代码] [代码][代码]windowH: [代码][代码]"1000"[代码][代码],[代码][代码] [代码][代码]user_input_text: [代码][代码]''[代码][代码],[代码][代码]//用户输入文字[代码][代码] [代码][代码]inputValue: [代码][代码]''[代码][代码],[代码][代码] [代码][代码]returnValue: [代码][代码]''[代码][代码],[代码][代码] [代码][代码]addImg: [代码][代码]false[代码][代码],[代码][代码] [代码][代码]//格式示例数据,可为空[代码][代码] [代码][代码]allContentList: [],[代码][代码] [代码][代码]num: 0,[代码][代码] [代码][代码]wo: [代码][代码]""[代码][代码],[代码][代码] [代码][代码]ta: [代码][代码]""[代码][代码],[代码][代码] [代码][代码]youImg: [代码][代码]""[代码][代码] [代码][代码]},[代码][代码] [代码][代码]//通过 WebSocket 连接发送数据,需要先 wx.connectSocket,并在 wx.onSocketOpen 回调之后才能发送。[代码][代码] [代码][代码]sendSocketMessage: [代码][代码]function[代码] [代码](msg) {[代码][代码] [代码][代码]var[代码] [代码]that = [代码][代码]this[代码][代码];[代码][代码] [代码][代码]console.log([代码][代码]'通过 WebSocket 连接发送数据'[代码][代码], JSON.stringify(msg))[代码][代码] [代码][代码]// debugger[代码][代码] [代码][代码]SocketTask.send([代码][代码] [代码][代码]{[代码][代码] [代码][代码]data: JSON.stringify(msg)[代码][代码] [代码][代码]},[代码][代码] [代码][代码]function[代码] [代码](res) {[代码][代码] [代码][代码]console.log([代码][代码]'已发送'[代码][代码], res)[代码][代码] [代码][代码]}[代码][代码] [代码][代码])[代码][代码] [代码][代码]},[代码][代码] [代码][代码]onLoad: [代码][代码]function[代码] [代码](options) {[代码][代码] [代码][代码]const that = [代码][代码]this[代码][代码];[代码][代码] [代码][代码]},[代码][代码] [代码][代码]onReady: [代码][代码]function[代码] [代码]() {[代码][代码] [代码][代码]var[代码] [代码]that = [代码][代码]this[代码][代码];[代码][代码] [代码][代码]SocketTask.onOpen(res => {[代码][代码] [代码][代码]socketOpen = [代码][代码]true[代码][代码];[代码][代码] [代码][代码]console.log([代码][代码]'监听 WebSocket 连接打开事件。'[代码][代码], res)[代码][代码] [代码][代码]//发送登陆信息[代码][代码] [代码][代码]var[代码] [代码]data = {[代码][代码] [代码][代码]// body: that.data.inputValue,[代码][代码] [代码][代码]"Name"[代码][代码]: that.data.wo,[代码][代码] [代码][代码]"content"[代码][代码]: [代码][代码]"login"[代码][代码],[代码][代码] [代码][代码]"type"[代码][代码]: 4[代码][代码] [代码][代码]}[代码][代码] [代码][代码]that.sendSocketMessage(data);[代码][代码] [代码][代码]//循环发送心跳[代码][代码] [代码][代码]setInterval([代码][代码] [代码][代码]function[代码] [代码]() {[代码][代码] [代码][代码]var[代码] [代码]ping = { [代码][代码]"type"[代码][代码]: [代码][代码]"ping"[代码] [代码]};[代码][代码] [代码][代码]that.sendSocketMessage(ping);[代码][代码] [代码][代码]}, 20000[代码][代码] [代码][代码]);[代码][代码] [代码][代码]})[代码][代码] [代码][代码]SocketTask.onClose(onClose => {[代码][代码] [代码][代码]console.log([代码][代码]'监听 WebSocket 连接关闭事件。'[代码][代码], onClose)[代码][代码] [代码][代码]socketOpen = [代码][代码]false[代码][代码];[代码][代码] [代码][代码]this[代码][代码].webSocket()[代码][代码] [代码][代码]})[代码][代码] [代码][代码]SocketTask.onError(onError => {[代码][代码] [代码][代码]console.log([代码][代码]'监听 WebSocket 错误。错误信息'[代码][代码], onError)[代码][代码] [代码][代码]socketOpen = [代码][代码]false[代码][代码] [代码][代码]})[代码][代码] [代码][代码]SocketTask.onMessage(onMessage => {[代码][代码] [代码][代码]console.log([代码][代码]"onMessage:::::"[代码] [代码]+ onMessage.data);[代码][代码] [代码][代码]if[代码] [代码](onMessage.data.indexOf([代码][代码]"上线"[代码][代码]) != -1 || onMessage.data.indexOf([代码][代码]"下线"[代码][代码]) != -1) {[代码][代码] [代码][代码]return[代码][代码];[代码][代码] [代码][代码]}[代码][代码] [代码][代码]console.log([代码][代码]'监听WebSocket接受到服务器的消息事件。服务器返回的消息'[代码][代码], JSON.parse(onMessage.data))[代码][代码] [代码][代码]var[代码] [代码]onMessage_data = JSON.parse(onMessage.data)[代码][代码] [代码][代码]if[代码] [代码](onMessage_data.toName == that.data.wo && onMessage_data.name == that.data.ta) {[代码][代码] [代码][代码]// addmsglist1(msg1.name, msg1.content)[代码][代码] [代码][代码]that.data.allContentList.push({[代码][代码] [代码][代码]"id"[代码][代码]: [代码][代码]""[代码][代码],[代码][代码] [代码][代码]"hx_id"[代码][代码]: [代码][代码]""[代码][代码],[代码][代码] [代码][代码]"wo"[代码][代码]: that.data.ta,[代码][代码] [代码][代码]"ta"[代码][代码]: that.data.wo,[代码][代码] [代码][代码]"content"[代码][代码]: onMessage_data.content,[代码][代码] [代码][代码]"voice_url"[代码][代码]: [代码][代码]""[代码][代码],[代码][代码] [代码][代码]"fileurl"[代码][代码]: [代码][代码]null[代码][代码],[代码][代码] [代码][代码]"create_date"[代码][代码]: [代码][代码]"2019-12-03"[代码][代码] [代码][代码]});[代码][代码] [代码][代码]that.setData({[代码][代码] [代码][代码]allContentList: that.data.allContentList[代码][代码] [代码][代码]})[代码][代码] [代码][代码]}[代码][代码] [代码][代码]})[代码][代码] [代码][代码]},[代码][代码] [代码][代码]onShow: [代码][代码]function[代码] [代码]() {[代码][代码] [代码][代码]if[代码] [代码](!socketOpen) {[代码][代码] [代码][代码]this[代码][代码].webSocket()[代码][代码] [代码][代码]}[代码][代码] [代码][代码]},[代码][代码] [代码][代码]webSocket: [代码][代码]function[代码] [代码]() {[代码][代码] [代码][代码]const that = [代码][代码]this[代码][代码];[代码][代码] [代码][代码]// 创建Socket[代码][代码] [代码][代码]SocketTask = wx.connectSocket({[代码][代码] [代码][代码]url: url,[代码][代码] [代码][代码]data: [代码][代码]'data'[代码][代码],[代码][代码] [代码][代码]header: {[代码][代码] [代码][代码]'content-type'[代码][代码]: [代码][代码]'application/json'[代码][代码] [代码][代码]},[代码][代码] [代码][代码]method: [代码][代码]'post'[代码][代码],[代码][代码] [代码][代码]success: [代码][代码]function[代码] [代码](res) {[代码][代码] [代码][代码]console.log([代码][代码]'WebSocket连接创建'[代码][代码], res)[代码][代码] [代码][代码]},[代码][代码] [代码][代码]fail: [代码][代码]function[代码] [代码](err) {[代码][代码] [代码][代码]wx.showToast({[代码][代码] [代码][代码]title: [代码][代码]'网络异常!'[代码][代码],[代码][代码] [代码][代码]})[代码][代码] [代码][代码]console.log(err)[代码][代码] [代码][代码]},[代码][代码] [代码][代码]})[代码][代码] [代码][代码]},[代码][代码] [代码][代码]onHide: [代码][代码]function[代码] [代码]() {[代码][代码] [代码][代码]SocketTask.close([代码][代码]function[代码] [代码](res) {[代码][代码] [代码][代码]console.log(res);[代码][代码] [代码][代码]})[代码][代码] [代码][代码]},[代码][代码] [代码][代码]onUnload: [代码][代码]function[代码] [代码]() {[代码][代码] [代码][代码]SocketTask.close([代码][代码]function[代码] [代码](res) {[代码][代码] [代码][代码]console.log(res);[代码][代码] [代码][代码]})[代码][代码] [代码][代码]}[代码][代码]})[代码][图片] 能够建立起连接,但是后续方法都没用,监听SocketTask.onOpen方法,监听不到,SocketTask.onMessage方法也监听不到
我在封装的公用请求方法中放入了这个两个api,请求调用之前调用wx.showLoading,在complete回调中调用 wx.hideLoading,但是在页面中请求成功没有正常的关闭wx.showLoading [图片] [图片][图片]
这个BUG尽快解决啊 一直卡在加载中, 都炸了, onload里面调用接口, 接口中封装了有wx.hideLoading, 全部失效, 查了官方说是onload的问题, 我换成onShow,还是会有这个BUG, 这不解决小程序全炸, 除非不用 loading .下星期还要上线啊, 无奈了 截止2020年1月3号下午16:42. 貌似还没完全修复, 暂时解决办法只能注释loading 或者写个500-1000ms的延迟关闭, onload的可以改到onReady里面 ,onReady没有这个BUG ,
真机不行,真机不行,真机不行,重要的事说三遍 原先是以为布局搞鬼,现在使用了这个简单布局都不行 以为自己的手机不行,其它手机也不行 page,.all{width: 100%;height: 100%;padding: 0;margin: 0;} view{width: 100%;} .all{ opacity: 0.4;} .a{height: 20%;background-color: #f0f;} .b{background-color: #0ff;height:80%;overflow-y: scroll;} .b>view{height: 5rem;} .b>view:nth-child(odd){background-color: #ff0;} .b>view:nth-child(even){background-color: #0f0;} .b>view>textarea{height: 100%;width: 100%;} <view class="all"> <view class="a"> </view> <view class="b"> <view wx:for="{{[1, 2, 3, 4, 5, 6, 7, 8, 9, 2, 3, 4, 5, 6, 7, 8, 9]}}"> <textarea value="{{'textarea外使用滚动条,输入内容不随滚动条移动,模拟器可行,真机不可行'}}"></textarea> </view> </view> </view>
- 当前 Bug 的表现(可附上截图) websocket长时间无法连接服务器 很简单的代码 socket = wx.connectSocket socket.onOpen sockst.send ... 偶尔会出现长时间无法连接的情况,然后不断尝试重连,大概需要1分钟左右才能成功连接。 如这时候重启微信再进入就能连接成功。 安卓、ios机型都有遇到这个问题 另外发现,这种情况在wifi下多人同时连接同一个wss服务器时会碰到比较多。而切换到4G网络下很少碰到。 错误信息:WebSocket connection to 'wss://................' failed: WebSocket is closed before the connection is established. (因为有个超时检查,几秒钟后连接不上就socket.close()) 发生问题时,通过抓包未发现有网络异常。 - 预期表现 能正常快速建立连接 - 复现路径 首次打开小程序很少碰到这种情况,但是回到微信聊天从分享卡片再次进入小程序后碰到概率很大。 - 提供一个最简复现 Demo 在提问前搜索了一下相关帖子,发现"7.0下websocket连接问题"反馈比较多,不知道我这种情况是否也是微信7.0的问题导致?
最近在获取到微信的昵称后,需要使用MD5加密传给后台,但是发现小程序直接使用可以加密中英文的md5.js来加密还有emjoy字符串的昵称,与服务器的加密不一致。有想法把emjoy字符串转成base64,但是发现没有emjoy字符串base64转换的js,所以想咨询一下,emjoy字符串在小程序中,改如果进行MD5加密或者改怎么转换成base64
为什么不能频繁 setData 先科普下 setData 做的事情: 在数据传输时,逻辑层会执行一次 JSON.stringify 来去除掉 setData 数据中不可传输的部分,之后将数据发送给视图层。同时,逻辑层还会将 setData 所设置的数据字段与 data 合并,使开发者可以用 this.data 读取到变更后的数据。 因此频繁调用,视图会一直更新,阻塞用户交互,引发性能问题。 但频繁调用是常见开发场景,能不能频繁调用的同时,视图延迟更新呢? 参考 Vue,我们能知道,Vue 每次赋值操作并不会直接更新视图,而是缓存到一个数据更新队列中,异步更新,再触发渲染,此时多次赋值,也只会渲染一次。 [代码]let newState = null; let timeout = null; const asyncSetData = ({ newData, }) => { newState = { ...newState, ...newData, clearTimeout(timeout); timeout = setTimeout(() => { vm.setData({ ...newState, newState = null }, 0); 由于异步代码会在同步代码之后执行,因此,当你多次使用 asyncSetData 设置 newState 时,newState 都会被缓存起来,并异步 setData 一次 但同时,这个方案也会带来一个新的问题,同步代码会阻塞页面的渲染。 同步代码会阻塞页面的渲染的问题其实在浏览器中也存在,但在小程序中,由于是逻辑、视图双线程架构,因此逻辑并不会阻塞视图渲染,这是小程序的优点,但在这套方案将会丢失这个优点。 鱼与熊掌不可兼得也! 对于信息流页面,数据过多怎么办 单次设置的数据不能超过 1024kB,请尽量避免一次设置过多的数据 通常,我们拉取到分页的数据 newList,添加到数组里,一般是这么写: [代码]this.setData({ list: this.data.list.concat(newList) 随着分页次数的增加,list 会逐渐增大,当超过 1024 kb 时,程序会报 [代码]exceed max data size[代码] 错误。 为了避免这个问题,我们可以直接修改 list 的某项数据,而不是对整个 list 重新赋值: [代码]let length = this.data.list.length; let newData = newList.reduce((acc, v, i)=>{ acc[`list[${length+i}]`] = v; return acc; }, {}); this.setData(newData); 这看着似乎还有点繁琐,为了简化操作,我们可以把 list 的数据结构从一维数组改为二维数组:[代码]list = [newList, newList][代码], 每次分页,可以直接将整个 newList 赋值到 list 作为一个子数组,此时赋值方式为: [代码]let length = this.data.list.length; this.setData({ [`list[${length}]`]: newList 同时,模板也需要相应改成二重循环: [代码]<block wx:for="{{list}}" wx:for-item="listItem" wx:key="{{listItem}}"> <child wx:for="{{listItem}}" wx:key="{{item}}"></child> </block> 下拉加载,让我们一夜回到解放前 信息流产品,总避免不了要做下拉加载。 下拉加载的数据,需要插到 list 的最前面,所以我们应该这样做: [代码]this.setData({ `list[-1]`: newList 哦不,对不起,上面是错的,应该是下面这样: [代码]this.setData({ list: this.data.list.unshift(newList) 这下好,又是一次性修改整个数组,一夜回到解放前… 为了解决这个问题,这里需要一点奇淫巧技: 为下拉加载维护一个单独的二维数组 pullDownList 在渲染时,用 wxs 将 pullDownList reverse 一下 此时,当下拉加载时,便可以只修改数组的某个子项: [代码]let length = this.data.pullDownList.length; this.setData({ [`pullDownList[${length}]`]: newList 关键在于渲染时候的反向渲染: [代码]<wxs module="utils"> function reverseArr(arr) { return arr.reverse() module.exports = { reverseArr: reverseArr <block wx:for="{{utils.reverseArr(pullDownList)}}" wx:for-item="listItem" wx:key="{{listItem}}"> <child wx:for="{{listItem}}" wx:key="{{item}}"></child> </block> 问题解决! 终极蛇皮上帝视角之微信小程序之告别 setData, 佯真愚, 2018年08月12日
[图片] 如题如图,麻烦有官方来给个解答么
问题描述: 使用官方websocketAPI后,在ios手机上运行没有问题,但是在微信7.0.4和7.0.5版本部分安卓真机上建立websocket连接后可以进入success回调,但是会自动断开连接,报socket error: {errMsg: "exception onOpen fail code:20, msg:Invalid HTTP status."}错误或等待一段时间报socket error: {errMsg: "connect response time out"}错误。 部分机型: 红米5 plus(android8.1.0) 华为荣耀note10(android9) 华为mate 10(android8.1.0) vivo Z5x(andorid9) 现象描述: 红米5 plus、华为荣耀note10,调用wx.connectSocket建立连接,可进入success回调,然后监听到onSocketError,报socket error: {errMsg: "exception onOpen fail code:20, msg:Invalid HTTP status."}错误。 vivo Z5x、华为mate 10,调用wx.connectSocket建立连接,可进入success回调,过大约60秒后报socket error: {errMsg: "connect response time out"}错误。
1,问:创建canvas 绘制图像,保存的图片不清晰? 答:这个问题大概就是canvas宽高太小。 2,问:绘制图像,图片绘制不上? 答:绘制图片的时候图片路径必须为本地路径,如果使用网络路径是不行的,我们只需要将网络路径下载到本地就行wx.downloadFile()。 3,问:绘制好的图像,图片保存不了? 答:我们绘制好的图像,要保存成图片,其实需要2步。 第1步:利用wx.canvasToTempFilePath()将canvas画布图像,生成图片。我们需要将wx.canvasToTempFilePath(),放到 ctx.draw(false, function() { wx.canvasToTempFilePath()生成的图片路径进行 里面确保canvas已经将图像全部绘制完成。 第2步:将wx.canvasToTempFilePath()生成的图片路径进行 wx.saveImageToPhotosAlbum()保存到相册就ok。 以上只是个人遇到的问题,如有步骤不明确请在评论区提问。 谢谢大家的支持。
由于我们无法将小程序直接分享到朋友圈,但分享到朋友圈的需求又很多,业界目前的做法是利用小程序的 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即可。
在有scroll-view滚动条页面的wxss里,例如在首页index.wxss,添加 [代码]::-webkit-scrollbar { display: none; width: 0; height: 0; color: transparent; 不用选择器,以及不能在app.wxss直接添加。
1:安装微信sdk,cnpm install weixin-js-sdk -S; 2:安装完成之后再main.js文件引入,注入到vue原型 import wx-sdk from “weixin-js-sdk”; 3:在第二个或者第三个页面去调用后台提供的接口初始化微信的sdk(最好在第二个页面去初始化,不然会遇到一个生命周期的问题) 参数说明:debug:true||false,查看初始化结果,成功与否,appID:公众号的APPID。timestamp:生成签名的时间戳。 nonceStr:生成签名的随机字符串。signature:微信生成的签名。jsApiList:需要在项目当中使用的那些方法,比如说支付chooseWXPay,直接把方法写进jsApiList里面既可。 4:我这里用微信支付演示一下 然后调起支付的参数就和小程序是一样的道理,由后台给你传过来即可。 微信小程序开发交流群纯交流群,没有任何广告,学习微信小程序的欢迎加入,里面有性感大牛在线解决问题
今天发现,在app.js 中执行 wx.getSystemInfoSync() 获取系统信息的 windowHeight是减去 tabbar 的。 在没有tabbar的 页面执行 后获取的高度是算上tabbar高度的。 这样原来想着在 globalData里放上 一次系统信息就可以调用的想法在这个 windowHeight 上是不行的。
段落的高度和行数,在canvas绘制海报图的场景下需要用到,其他使用场景暂时还没遇到。 使用微信接口获取段落高度: wx.createSelectorQuery().selectAll('.paragraph').fields({ size: true, }, function (res) { //res里面有高度值 }).exec() 获取行数: 自己模拟写一个只有一行的段落,比如<view class="demo">模拟一行</view>,样式跟原段落设置成一样。 使用上面的高度接口,获取段落只有一行时的高度,然后 原段落高度/一行段落高度 = 原段落行数
去找小程序的菜单按钮,没有找到,于是自己摆弄了一个出来,虽然是个很简单的东西,考虑到可能还有其他人觉得写一个麻烦,现在把代码发一下,大神勿喷。 先看一下效果: cc-mainbutton.js [代码] Component({ lifetimes: { attached: function attached() { // 在组件实例进入页面节点树时执行 this.animation = wx.createAnimation(); detached: function detached() { // 在组件实例被从页面节点树移除时执行 data: { dial_btn_options_show: false methods: { // 菜单按钮的动画 rotate: function rotate() { if (this.data.dial_btn_options_show == false) { this.animation.rotate(-135).step(); this.setData({ dial_btn_options_show: true animation: this.animation.export() } else { this.animation.rotate(0).step(); this.setData({ dial_btn_options_show: false animation: this.animation.export() //点击子按钮 click_option: function click_option(e) { switch (e.currentTarget.dataset.option) { case '1': break; case '2': break; case '3': break; default: break; cc-mainbutton.wxml [代码]<view class="main_btn_ctn" style="width: 60px;height: 60px;"> <image animation="{{animation}}" bindtap="rotate" class="dial-btn {{dial_btn_options_show?'dial-btn-active':''}}" src="../static/images/main-btn.png" /> <view bindtap="click_option" data-option="1" class="dial-btn--option flex-def flex-zCenter flex-cCenter flex-zTopBottom"> <image style="height: 25px;width: 25px" class="" src="../static/images/add_shuoshuo.png" mode="widthFix" /> </view> <view bindtap="click_option" data-option="2" class="dial-btn--option flex-def flex-zCenter flex-cCenter flex-zTopBottom"> <image style="height: 25px;width: 25px" class="" src="../static/images/reflesh.png" mode="widthFix" /> </view> <view bindtap="click_option" data-option="3" class="dial-btn--option flex-def flex-zCenter flex-cCenter flex-zTopBottom"> <image style="height: 25px;width: 25px" class="" src="../static/images/go-top.png" mode="widthFix" /> </view> </view> cc-mainbutton.wxss。 [代码]/* index/main-button/cc-mainbutton.wxss */ .flex-def { display: flex; /* 主轴居中 */ .flex-zCenter { justify-content: center; /* 侧轴居中 */ .flex-cCenter { align-items: center; /* 主轴从上到下 */ .flex-zTopBottom { flex-direction: column; .dial-btn { border: none; z-index: 7; position: absolute; height: 60px; width: 60px; left: 50%; top: 50%; margin: -30px 0 0 -30px; /*子按钮初始位置隐藏在主按钮后面,透明度0*/ .dial-btn--option { background: yellowgreen; position: absolute; height: 46px; width: 46px; border-radius: 100%; left: 50%; top: 50%; margin: -23px 0 0 -23px; transform: translate(0, 0); /* 过渡效果 */ transition: opacity 0.25s ease-in-out, transform 0.25s ease 0s; .dial-btn--option:nth-of-type(1) { z-index: 2; opacity: 0; transition-delay: 0.2s; .dial-btn--option:nth-of-type(2) { z-index: 3; opacity: 0; transition-delay: 0.3s; .dial-btn--option:nth-of-type(3) { z-index: 4; opacity: 0; transition-delay: 0.4s; /* 通过nth-of-type定义每个子按钮的不同定位,设置透明度1 */ .dial-btn-active ~ .dial-btn--option:nth-of-type(1) { opacity: 1; transform: translate(-65px, 5px); .dial-btn-active ~ .dial-btn--option:nth-of-type(2) { opacity: 1; transform: translate(-40px, -40px); .dial-btn-active ~ .dial-btn--option:nth-of-type(3) { opacity: 1; transform: translate(5px, -65px); 预览网址:https://developers.weixin.qq.com/s/if7B8SmT7E8q
export default (options, type = 1) => { return new Promise((reslove, reject) => { routes[type](Object.assign(getPath(options), { success: reslove, fail: reject, function getPath(options) { switch (Reflect.toString.call(options)) { case “[object Object]”: return { url: [代码]${options.url}?data=${encodeURIComponent(JSON.stringify(options.data))}[代码], case “[object Number]”: return { delta: options, case “[object String]”: return { url: options, const routes = { 1: wx.navigateTo, 2: wx.switchTab, 3: wx.navigateBack, 4: wx.reLaunch, 5: wx.redirectTo,
[代码]// SHA1 加密[代码] [代码]var[代码] [代码]value = [代码][代码]"123456"[代码][代码];[代码] [代码]var[代码] [代码]wordArray = CryptoJS.SHA1(value);[代码] [代码]var[代码] [代码]str = wordArray.toString(CryptoJS.enc.Hex);[代码] [代码]// HmacSHA1加密[代码] [代码]var[代码] [代码]message = [代码][代码]"message"[代码][代码];[代码] [代码]var[代码] [代码]key = [代码][代码]"key"[代码][代码];[代码] [代码]var[代码] [代码]wordArray = CryptoJS.HmacSHA1(message, key);[代码] [代码]var[代码] [代码]str = wordArray.toString(CryptoJS.enc.Hex);[代码] [代码]// md5 加密[代码] [代码]var[代码] [代码]md5 = CryptoJS.MD5([代码][代码]"md5"[代码][代码]).toString();[代码] [代码]// AES 加解密 开始[代码] [代码]/**[代码] [代码] [代码][代码]* //AES 解密方法[代码] [代码] [代码][代码]* word 字符串[代码] [代码] [代码][代码]*/[代码] [代码]const AES_JIA = [代码][代码]function[代码] [代码](word, key, iv) {[代码] [代码] [代码][代码]let encryptedHexStr = CryptoJS.enc.Hex.parse(word);[代码] [代码] [代码][代码]let srcs = CryptoJS.enc.Base64.stringify(encryptedHexStr);[代码] [代码] [代码][代码]let decrypt = CryptoJS.AES.decrypt(srcs, key, { iv: iv, mode: CryptoJS.mode.ECB, padding: CryptoJS.pad.Pkcs7 });[代码] [代码] [代码][代码]let decryptedStr = decrypt.toString(CryptoJS.enc.Utf8);[代码] [代码] [代码][代码]return[代码] [代码]decryptedStr.toString();[代码] [代码]}[代码] [代码]/**[代码] [代码] [代码][代码]* //AES 加密方法[代码] [代码] [代码][代码]* word 字符串[代码] [代码] [代码][代码]*/[代码] [代码]const AES_JIE = [代码][代码]function[代码] [代码](word, key, iv) {[代码] [代码] [代码][代码]let srcs = CryptoJS.enc.Utf8.parse(word);[代码] [代码] [代码][代码]let encrypted = CryptoJS.AES.encrypt(srcs, key, {[代码] [代码] [代码][代码]iv: iv,[代码] [代码] [代码][代码]mode: CryptoJS.mode.ECB,[代码] [代码] [代码][代码]padding: CryptoJS.pad.Pkcs7[代码] [代码] [代码][代码]});[代码] [代码] [代码][代码]return[代码] [代码]encrypted.ciphertext.toString().toUpperCase();[代码] [代码]}[代码] [代码]const word = [代码][代码]"字符串格式"[代码][代码]; [代码][代码]// 字符串格式[代码] [代码]const key = CryptoJS.enc.Utf8.parse([代码][代码]"1234567890123456"[代码][代码]); [代码][代码]//十六位十六进制数作为密钥 ,十六位,十六位,不要 误以为 1234567890123456 == 123 是行得通的 字符长度16不等于 3,除非 key = 123[代码] [代码]const iv = CryptoJS.enc.Utf8.parse([代码][代码]''[代码][代码]); [代码][代码]//十六位十六进制数作为密钥偏移量[代码] [代码]var[代码] [代码]ctext = AES_JIA(word, key, iv);[代码] [代码]console.log([代码][代码]"ctext=>"[代码][代码], ctext); [代码][代码]// AES 加密[代码] [代码]var[代码] [代码]ptext = AES_JIE(ctext, key, iv);[代码] [代码]console.log([代码][代码]"ptext=>"[代码][代码], ptext); [代码][代码]// AES 解密[代码] [代码]// AES 加解密 结束[代码] [代码]//DES 加密[代码][代码]function[代码] DES_JIA[代码](message, key, iv) {[代码][代码] [代码][代码]var[代码] [代码]keyHex = CryptoJS.enc.Utf8.parse(key);[代码][代码] [代码][代码]var[代码] [代码]encrypted = CryptoJS.DES.encrypt(message, keyHex, {[代码][代码] [代码][代码]iv: iv,[代码][代码] [代码][代码]mode: CryptoJS.mode.ECB,[代码][代码] [代码][代码]padding: CryptoJS.pad.Pkcs7[代码][代码] [代码][代码]});[代码][代码] [代码][代码]return[代码] [代码]encrypted.toString();[代码][代码]}[代码] [代码]//DES 解密[代码][代码]function[代码] [代码]DES_JIE(ciphertext, key, iv) {[代码][代码] [代码][代码]var[代码] [代码]keyHex = CryptoJS.enc.Utf8.parse(key);[代码][代码] [代码][代码]// direct decrypt ciphertext[代码][代码] [代码][代码]var[代码] [代码]decrypted = CryptoJS.DES.decrypt({[代码][代码] [代码][代码]ciphertext: CryptoJS.enc.Base64.parse(ciphertext)[代码][代码] [代码][代码]}, keyHex, {[代码][代码] [代码][代码]iv: iv,[代码][代码] [代码][代码]mode: CryptoJS.mode.ECB,[代码][代码] [代码][代码]padding: CryptoJS.pad.Pkcs7[代码][代码] [代码][代码]});[代码][代码] [代码][代码]return[代码] [代码]decrypted.toString(CryptoJS.enc.Utf8);[代码][代码]}[代码] [代码]var[代码] [代码]des_text = DES_JIA(word, key, iv);[代码][代码]console.log([代码][代码]"des_text=>"[代码][代码], des_text); [代码][代码]// des 加密[代码] [代码]var[代码] [代码]ntext = DES_JIE(des_text, key, iv);[代码][代码]console.log([代码][代码]"ntext=>"[代码][代码], ntext); [代码][代码]// des 解密[代码] 调试(SHA1 加密)图片示例: [图片] 参考资料: https://cryptojs.gitbook.io/docs/ https://www.bootcdn.cn/crypto-js/
扩展Page / App? 自定义navigationBar? 冗余授权代码? local/session/expire缓存管理混乱? 总结了一些开发微信小程序过程中遇到问题的解决方式/经验分享,另外共享几个通用组件. star+✨ https://github.com/JoweiBlog/wechat-miniprogram-dev
哈喽 我又来了 这是我第二次分享文章了 希望能够帮助大家 也希望大家喜欢~ image组件中的 mode=“aspectFill” 属性 这个属性是等比例缩放 如果你的图片是这个属性的需要注意注意注意 图片渲染完成后 再等比例缩放 及 先渲染 再等比缩放 例子: 当你要获取这个图片距离顶部的距离是 需要使用 wx.createSelectorQuery来来找到这个标签并获取到这个标签的参数 一般会写在 onReady() 生命周期钩子函数里 但是 问题就在这个时候出 现了 我获取的标签数据 不是 实际的数据 而是 图片没有缩放的数据 解决这个问题的时候 我使用了 setTimeout 函数 把时间设置为500 即 半秒后 再获取图片的标签的 参数 这时候 获取到的数据就是正确的数据了 暂时没有测试不写等待时间 有兴趣大家可以试一下 前端绘制海报性能优化 绘制海报我们用到了canvas 绘制海报的前 提是 绘制的素材要下载到本地 如果我们在绘制的时候下载素材 这个时 候 绘制的进度就会变慢 优化的思想如下 B页面是绘制海报的 A页面 点击某个按钮 进入到 B页面 那么我们就在 渲染A页面的时候 就下载素材呢 等到了B页面 素材都已经有了 直接使用,绘制效果会非常好 甚至是 秒绘制完成 在B页面onUnload函数内 清除下载文件的缓存 避免缓存太多 字符串10 减去 数字0 最后 变成了 数字 10 let string = “10” string - 0 此时 string 就是 数字 10 类型是number // JS的隐式转换 很常用的一种改变数据类型的方式 0 的 布尔值 是 false 防止数据抖动的方法 数据抖动 说白了 就是 一个按钮有一个事件 然后用户在很短的事件内重复点击 类似的有 购买物品 提交完成按钮 这些 解决方法 先声明一个变量 值为true 当做锁 当执行函数的时候 把这个锁变成 false 那么这个函数就被锁死了 只有这个函数完成所有操作的时候 再把锁变成true 此刻用户才可以再次真正的点击 代码如下: 今天的分享就到这里了 如果喜欢请大家动动小手指 点个赞吧 欢迎各位大佬亲临指导 如果有问题请及时指出 我会第一时间修改的 嘻嘻
打开社区,突然发现社区变白了,应该是擦粉了。🤣🤣🤣 分享一个表单验证组件,可提供数据验证和界面错误显示,效果截图如下: [图片][图片] 传送门在这里 https://github.com/ikrong/miniprogram-form-validator 下面格式不知道为什么都乱了,这什么鬼编辑器😒😒😒 [代码]npm install miniprogram-form-validator // or yarn add miniprogram-form-validator 在app.json中引入可全局使用 引入之后需要点击小程序开发工具的 【工具>构建npm】, 否则会报错的 [代码]{ "usingComponents": { "form-validator": "miniprogram-form-validator/form-validator", "form-tip": "miniprogram-form-validator/form-tip" [代码]<form-validator id="form" formGroup="{{formGroup}}" formData="{{formData}}"> <form-tip name="id"></form-tip> </form-validator> 验证器的字段含义 [代码]{ required: true, // 是否必填 message: "", // 出错后的提示信息 type: "", // 内置验证方法 regexp: RegExp, // 正则表达式 validator: Function // 自定义验证方法,返回 boolean 或者 Promise<boolean> [代码]Page({ data:{ formGroup:{ { required:true }, validator:(value,name)=>Promise<Boolean> formData:{ id:"", async validate(){ let result = await this.selectComponents("#form").validate(); // 验证通过了
小程序不像web浏览器有cookie机制,在默认使用cookie存sessionid的机制下,后台将无法正常使用session功能,如果正确使用session呢,提供两个方案。 [代码]1、将sessionid通过url进行传递 用户每次登录成功后将生成的sessionid值使用参数回传到客户端, 客户端接到sessionid后保存到本地, 在发起网络请求的底层接口中默认自动带上sessionid=本地存储的sessionid值。 需要配合服务器一起更改,服务器后端默认使用cookie机制 2、无缝对接cookie, 将服务器的set-cookie值保存到本地,再请求的时候模拟浏览器头部信息并带上保存的cookie信息 1)保存cookie值: _XHR('login',{'code':res.code}).then(function( ret ){ ret.header["Set-Cookie"] != undefined && wx.setStorageSync("cookie", ret.header["Set-Cookie"]); 2)请求的时候自动带上cookie信息 var header={}; header = { 'content-type': 'application/x-www-form-urlencoded' var cookie = wx.getStorageSync("cookie"); if( url != 'login' && !isNull( cookie ) ){ header['cookie'] = cookie; 将header 赋值到 request的header内 wx.request({ url: qryDomian + url + '.html', data: _data, method: 'POST', header: header, dataType:'json' ...... 第二种方案服务器无需做任务操作。[代码]
DatePicker 微信上的时间选择,有的时候你会发现,你不能同时选择日期和时间,而且时间不能选到秒。DatePicker让你想选什么选什么… DatePicker分为四个mode:YMDhms(年月日时分秒)、YMD(年月日)、MD(月日)、hm(时分)。 我自己觉得用起来很爽快。 mode:YMDhms (年月日时分秒) mode:YMD(年月日) mode:MD (月日) mode:hm (时分) gitHub地址
用cover-view 来显示,input来输入,通过bindinput来赋值,用cover-view来覆盖input的位置,同时把cover-view的背景颜色设置透明,这样既不会被其它组件盖住,也能显示出input的光标出来。
示例代码地址 https://github.com/s568774056/swipe.git 对于整页都是swiper的情况下。例如下面这张图: [图片] 则可以使用如下css [代码] [代码] [代码]swiper,swiper-item{[代码] [代码] [代码][代码]height[代码][代码]: [代码][代码]100[代码][代码]vh [代码][代码]!important[代码][代码];[代码][代码]}[代码] [代码] [代码] [代码]或者 [代码] [代码] [代码] [代码][代码] [代码][代码] swiper,swiper-item{ height: calc(100vh - 75rpx) !important; } [代码][代码] [代码] [代码] 对于swiper占据部分高度的情况下。 [图片] 使用如下代码 原理为在[代码]swiper-item[代码][代码][代码]的最上面和最下面插入空view,并利用wx api获取两个之间的高度差,然后设置给[代码]swiper[代码]。 细节方面需要自己调整下。为什么小程序不把这个组件做好呢?还得自己计算- -! <swiper class='hide' bindanimationfinish="swiperChange" style="height:{{swiper_height}};" current="{{isIndex}}"> <swiper-item wx:for="{{roomList}}" wx:for-item='room' wx:for-index="index"> <view id="start{{index}}" class='start-view'></view> <block wx:for="{{imgUrls}}" wx:for-item='path' wx:for-index="img-index"> <image mode="aspectFill" src="{{path}}" /> </block> <view id="end{{index}}" class='start-view'></view> </swiper-item> </swiper> [代码][代码][代码][代码] swiper { margin-top: 45rpx; } Page({ data: { roomList: ['Room1', 'Room2', 'Room3'], imgUrls: [ 'https://images.unsplash.com/photo-1551334787-21e6bd3ab135?w=640', 'https://images.unsplash.com/photo-1551214012-84f95e060dee?w=640', 'https://images.unsplash.com/photo-1551446591-142875a901a1?w=640' ], swiper_height: 0, isIndex:0 }, onLoad: function () { this.autoHeight(); }, changeNavBar: function (e) { this.setData({ isIndex: e.detail }); }, swiperChange: function (e) { this.setData({ isIndex: e.detail.current }); this.autoHeight(); }, autoHeight() { let { isIndex } = this.data; wx.createSelectorQuery() .select('#end' + isIndex).boundingClientRect() .select('#start' + isIndex).boundingClientRect().exec(rect => { let _space = rect[0].top - rect[1].top; _space = _space + 'px'; this.setData({ swiper_height: _space }); }) } }) 参考文章https://developers.weixin.qq.com/community/develop/doc/00008aaf4a473056d1c69a8b253c04
那些被忽略的盒子模型小知识 本文是笔者在学习CSS时的一些小白总结 我们知道的盒子模型主要由4个区域组成,分别是内容区域(content),内边距区域(padding),边框区域(border)和外边距区域(margin)。 对于不了解盒子模型的朋友可以移步到这里了解一下。 Content(内容) 1. 替换元素 替换元素(replaced element),顾名思义就是内容可以被替换的元素。 我们通常会把一些特殊意义的文本替换成图片,比如一个网站的logo。 我们会在页面上看到的不是h1标签显示的”Google“文字,而是谷歌logo的图片。使用了content的元素的内容在html标签中是不存在的。这样做就有个好处,当爬虫来访问我们的网站,爬虫可以知道我们这个主站的h1标题是”Google“而不是一个img标签,且在视觉上给用户更好的体验。 2. 伪元素::before和::after 为了实现上面显示价格,之前写react代码时候会经常这么写,感觉在逻辑上写了好多关联性不大的文本。其实可以利用[代码]::before[代码]和[代码]::after[代码]两个伪元素,把这些与逻辑不相关的写在css里,react dom则专注于数据的表现。 [代码]<div className="price-panel"> <span className="price-panel__price"> {(totalPrice / 100).toFixed(1)} </span> <span className="price-panel__discount-price"> {(totalDiscountPrice / 100).toFixed(1)} </span> <span className="price-panel__discount"> {(discount / 10).toFixed(1)} </span> 使用了伪元素后的react代码显然更加清晰表示数据。 HTML: [代码]<div className="price-panel"> <span className="price-panel__price"> {(totalPrice / 100).toFixed(1)} </span> <span className="price-panel__discount-price"> {(totalDiscountPrice / 100).toFixed(1)} </span> <span className="price-panel__discount"> {(discount / 10).toFixed(1)} </span> SCSS: [代码].price-panel { &__price { &::before { content: '¥'; &__discount-price { &::before { content: '已省¥'; &__discount { &::before { content: '('; &::after { content: '折)'; 我们还能使用伪元素帮助实现一些本来需要多个div实现的样式,比如下面这个对话框。 HTML: [代码]<div class="dialog">Hi,I’m a bubble dialog. Can you see me?</div> [代码].dialog { background: #f0f; padding: 10px; border-radius: 10px; color: white; max-width: 250px; position: relative; overflow: visible; .dialog::after { position: absolute; content: ''; display: inline-block; border-width: 5px 10px; border-style: solid; border-color: transparent transparent #f0f #f0f; width: 0; height: 0; right: -20px; Padding(内边距) padding的百分比值是非常有用的。需要注意的padding的百分比值,无论是水平方向还是垂直方向都是相对于父级元素的宽度进行计算的。 如果需要弄一张16:9的等比缩放图片,可以利用padding的这个特性,设置一个[代码]padding-top[代码]或者[代码]padding-bottom[代码]为56.25%即可(100\16*9) Margin(外边距) 1. margin合并 块级元素的[代码]margin-top[代码]和[代码]margin-bottom[代码]有时候会合并为单个margin,这种现象叫margin合并。 margin合并发生两个重要元素 必须是块级元素 只发生在垂直方向。 margin合并的场景 1.1 相邻兄弟元素 1.2 父级和第一个/最后一个子元素 在实际开发中,父子margin合并很有可能会带给我们麻烦。 如下图所示,div表现出和我们预想不一致的结果。 那么怎么才能防止这种父子margin合并导致的和预想不一致问题呢? 解决方法如下(这里直接复制了张鑫旭老师书籍《CSS世界》的原话。): (1)对于margin-top合并(满足一个即可): 父元素设置为BFC 设置[代码]border-top[代码]的值(亲测transparent也可以的) 设置[代码]padding-top[代码]的值 父元素和第一个子元素之间添加内联元素 (2)对于margin-bottom合并(满足一个即可): 父元素设置为BFC 设置[代码]border-bottom[代码](transparent也可以的) 设置[代码]padding-bottom[代码] 父元素和最后一个子元素之间添加一个内联元素 父元素设置[代码]height[代码]、[代码]min-height[代码]或者[代码]max-height[代码] 1.3 空块级元素的margin合并 2. margin auto 每当说到[代码]margin:auto[代码],我的第一反应是居中。但这个只是一个浅层应用的表象。 接下来我们去一起看看这个[代码]margin:auto[代码]究竟是‘何方神圣’。 [代码]margin:auto[代码]的填充规则如下: 如果一侧定值,一侧auto,则auto为剩余空间大小。注意auto并不是0的意思。 如果两侧都是auto,则平分剩余的空间 我会疑惑为什么我设置了[代码]margin: auto[代码],却在垂直方向上没有居中。 这里《css世界》中给出的答案让人非常容易理解。假如把.son元素的height去掉,.son的高度会自动变成父元素的200px,显然不会,所以无法触发margin: auto。同理,如果把width为200px去掉,确实是会和父元素一样宽。 那么如何让垂直居中呢? 子元素使用绝对定位后设置[代码]margin: auto[代码]即可 Border(边框) 用border绘制三角形 我们可以利用border color为透明来绘制一些图形,比如三角形 注意[代码]border-color[代码]这个属性。 [代码]/* border-color: color; 单值语法 */ border-color: red; /* border-color: vertical horizontal; 双值语法*/ border-color: red #f015ca; /* border-color: top horizontal bottom; 三值语法 */ border-color: red yellow green; /* border-color: top right bottom left; 四值语法 */ border-color: red yellow green blue; 当然,我们绘制三角形不限于这种等腰三角。 这里绘制了一个底边分别是60px和160px的直角三角形。 文章主要参考了张鑫旭老师的《css世界》并根据自己的业务做出的一些实践总结。
第三方登录模块使开发者能快捷灵活的拥有自己的用户系统,是 LeanCloud 最受欢迎的功能之一。随着第三方平台的演化,特别是微信小程序的流行,LeanCloud 第三方登录模块也一直在改进: v2.0*:增加微信小程序一键登录功能。支持开发者不写任何后端代码实现微信小程序用户系统与 LeanCloud 用户系统的关联。 v3.6:增加 unionid 登录接口。支持开发者使用 unionid 关联一个微信开发者帐号下的多个应用从而共享一套 LeanCloud 用户系统。 这两个功能各自都非常简单可靠,但是其中重叠的部分需求却是一个难题:「如何在小程序中支持 unionid 登录,既能得到 unionid 登录机制的灵活性,又保留一键登录功能的便利性」。 在最近发布的 JavaScript SDK v3.13 中包含了微信小程序 unionid 登录支持。我们根据不同的需求设计了不同的解决方案。 * 这里的版本指开始支持该功能的 JavaScript SDK 版本。 LeanCloud 的用户系统支持一键使用微信用户身份登录。要使用一键登录功能,需要先设置小程序的 AppID 与 AppSecret: 1.登录 微信公众平台,在 设置 > 开发设置 中获得 AppID 与 AppSecret。 前往 LeanCloud 控制台 > 组件 > 社交,保存「微信小程序」的 AppID 与 AppSecret。 这样你就可以在应用中使用[代码]AV.User.loginWithWeapp()[代码]方法来使用当前用户身份登录了。 [代码]AV.User.loginWithWeapp().then(user => { this.globalData.user = user; }).catch(console.error); 使用一键登录方式登录时,LeanCloud 会将该用户的小程序 [代码]openid[代码] 与 [代码]session_key[代码] 等信息保存在对应的 [代码]user.authData.lc_weapp[代码] 属性中,你可以在控制台的 [代码]_User[代码] 表中看到: [代码]{ "authData": { "lc_weapp": { "session_key": "2zIDoEEUhkb0B5pUTzsLVg==", "expires_in": 7200, "openid": "obznq0GuHPxdRYaaDkPOHk785DuA" 如果用户是第一次使用此应用,调用登录 API 会创建一个新的用户,你可以在 控制台 > 存储 中的 [代码]_User[代码] 表中看到该用户的信息,如果用户曾经使用该方式登录过此应用(存在对应 openid 的用户),再次调用登录 API 会返回同一个用户。 用户的登录状态会保存在客户端中,可以使用 [代码]AV.User.current()[代码] 方法来获取当前登录的用户,下面的例子展示了如何为登录用户保存额外的信息: [代码]// 假设已经通过 AV.User.loginWithWeapp() 登录 // 获得当前登录用户 const user = AV.User.current(); // 调用小程序 API,得到用户信息 wx.getUserInfo({ success: ({userInfo}) => { // 更新当前用户的信息 user.set(userInfo).save().then(user => { // 成功,此时可在控制台中看到更新后的用户信息 this.globalData.user = user; }).catch(console.error); [代码]authData[代码] 默认只有对应用户可见,开发者可以使用 masterKey 在云引擎中获取该用户的 [代码]openid[代码] 与 [代码]session_key[代码] 进行支付、推送等操作。详情的示例请参考 支付。 小程序的登录态([代码]session_key[代码])存在有效期,可以通过 wx.checkSession() 方法检测当前用户登录态是否有效,失效后可以通过调用 [代码]AV.User.loginWithWeapp()[代码] 重新登录。 使用 unionid 微信开放平台使用 unionid 来区分用户的唯一性,也就是说同一个微信开放平台帐号下的移动应用、网站应用和公众帐号(包括小程序),用户的 unionid 都是同一个,而 openid 会是多个。如果你想要实现多个小程序之间,或者小程序与使用微信开放平台登录的应用之间共享用户系统的话,则需要使用 unionid 登录。 要在小程序中使用 unionid 登录,请先确认已经在 微信开放平台 绑定了该小程序 在小程序中有很多途径可以 获取到 unionid。不同的 unionid 获取方式,接入 LeanCloud 用户系统的方式也有所不同。 一键登录时静默获取 unionid 当满足以下条件时,一键登录 API [代码]AV.User.loginWithWeapp()[代码] 能静默地获取到用户的 unionid 并用 unionid + openid 进行匹配登录。 微信开放平台帐号下存在同主体的公众号,并且该用户已经关注了该公众号。 微信开放平台帐号下存在同主体的公众号或移动应用,并且该用户已经授权登录过该公众号或移动应用。 要启用这种方式,需要在一键登录时指定参数 [代码]preferUnionId[代码] 为 true: [代码]AV.User.loginWithWeapp({ preferUnionId: true, 使用 unionid 登录后,用户的 authData 中会增加 _[代码]weixin_unionid[代码] 一项(与 [代码]lc_weapp[代码] 平级): [代码]{ "authData": { "lc_weapp": { "session_key": "2zIDoEEUhkb0B5pUTzsLVg==", "expires_in": 7200, "openid": "obznq0GuHPxdRYaaDkPOHk785DuA", "unionid": "ox7NLs5BlEqPS4glxqhn5kkO0UUo" "_weixin_unionid": { "uid": "ox7NLs5BlEqPS4glxqhn5kkO0UUo" 用 unionid + openid 登录时,会按照下面的步骤进行用户匹配: 如果已经存在对应 [代码]unionid(authData._weixin_unionid.uid[代码])的用户,则会直接作为这个用户登录,并将所有信息([代码]openid[代码]、[代码]session_key[代码]、[代码]unionid[代码] 等)更新到该用户的 [代码]authData.lc_ewapp[代码] 中。 如果不存在匹配 unionid 的用户,但存在匹配 openid([代码]authData.lc_weapp.openid[代码])的用户,则会直接作为这个用户登录,并将所有信息([代码]session_key[代码]、[代码]unionid[代码] 等)更新到该用户的 [代码]authData.lc_ewapp[代码] 中,同时将 [代码]unionid[代码] 保存到 [代码]authData._weixin_unionid.uid[代码] 中。 如果不存在匹配 unionid 的用户,也不存在匹配 openid 的用户,则创建一个新用户,将所有信息([代码]session_key[代码]、[代码]unionid[代码] 等)更新到该用户的 [代码]authData.lc_ewapp[代码] 中,同时将 [代码]unionid[代码] 保存到 [代码]authData._weixin_unionid.uid[代码] 中。 不管匹配的过程是如何的,最终登录用户的 [代码]authData[代码] 都会是上面这种结构。 LeanTodo Demo 便是使用这种方式登录的,如果你已经关注了其关联的公众号(搜索 AVOSCloud,或通过小程序关于页面的相关公众号链接访问),那么你在登录后会在 LeanTodo Demo 的 设置 - 用户 页面看到当前用户的 [代码]authData[代码] 中已经绑定了 unionid。 微信扫描二维码进入 Demo 需要注意的是: 如果用户不符合上述静默获取 unionid 的条件,那么就算指定了 [代码]preferUnionId[代码] 也不会使用 unionid 登录。 如果用户符合上述静默获取 unionid 的条件,但没有指定 [代码]preferUnionId[代码],那么该次登录不会使用 unionid 登录,但仍然会将获取到的 unionid 作为一般字段写入该用户的 [代码]authData.lc_weapp[代码] 中。此时用户的 [代码]authData[代码] 会是这样的: [代码]{ "authData": { "lc_weapp": { "session_key": "2zIDoEEUhkb0B5pUTzsLVg==", "expires_in": 7200, "openid": "obznq0GuHPxdRYaaDkPOHk785DuA", "unionid": "ox7NLs5BlEqPS4glxqhn5kkO0UUo" 通过其他方式获取 unionid 后登录 如果开发者自行获得了用户的 unionid(例如通过解密 wx.getUserInfo 获取到的用户信息),可以在小程序中调用 [代码]AV.User.loginWithWeappWithUnionId()[代码] 投入 unionid 完成登录授权: [代码]AV.User.loginWithWeappWithUnionId(unionid, { asMainAccount: true }).then(console.log, console.error); 通过其他方式获取 unionid 与 openid 后登录 如果开发者希望更灵活的控制小程序的登录流程,也可以自行在服务端实现 unionid 与 openid 的获取,然后调用通用的第三方 unionid 登录接口指定平台为 [代码]lc_weapp[代码] 来登录: [代码]const unionid = ''; const authData = { openid: '', session_key: '' const platform = 'lc_weapp'; AV.User.loginWithAuthDataAndUnionId(authData, platform, unionid, { asMainAccount: true }).then(console.log, console.error); 相对上面提到的一些 Weapp 相关的登录 API,loginWithAuthDataAndUnionId 是更加底层的第三方登录接口,不依赖小程序运行环境,因此这种方式也提供了更高的灵活度: 可以在服务端获取到 unionid 与 openid 等信息后返回给小程序客户端,在客户端调用 [代码]AV.User.loginWithAuthDataAndUnionId[代码] 来登录。 也可以在服务端获取到 unionid 与 openid 等信息后直接调用 [代码]AV.User.loginWithAuthDataAndUnionId[代码] 登录,成功后得到登录用户的 [代码]sessionToken[代码] 后返回给客户端,客户端再使用该 [代码]sessionToken[代码] 直接登录。 关联第二个小程序 这种用法的另一种常见场景是关联同一个开发者帐号下的第二个小程序。 因为一个 LeanCloud 应用默认关联一个微信小程序(对应的平台名称是 [代码]lc_weapp[代码]),使用小程序系列 API 的时候也都是默认关联到 [代码]authData.lc_weapp[代码] 字段上。如果想要接入第二个小程序,则需要自行获取到 unionid 与 openid,然后将其作为一个新的第三方平台登录。这里同样需要用到 [代码]AV.User.loginWithAuthDataAndUnionId[代码] 方法,但与关联内置的小程序平台([代码]lc_weapp[代码])有一些不同: 需要指定一个新的 [代码]platform[代码] 需要将 [代码]openid[代码] 保存为 [代码]uid[代码](内置的微信平台做了特殊处理可以直接用 [代码]openid[代码] 而这里是作为通用第三方 OAuth 平台保存因此需要使用标准的 [代码]uid[代码] 字段)。 这里我们以新的平台 [代码]weapp2[代码] 为例: [代码]const unionid = ''; const openid = ''; const authData = { uid: openid, session_key: '' const platform = 'weapp2'; AV.User.loginWithAuthDataAndUnionId(authData, platform, unionid, { asMainAccount: true }).then(console.log, console.error); 获取 unionid 后与现有用户关联 如果一个用户已经登录,现在通过某种方式获取到了其 unionid(一个常见的使用场景是用户完成了支付操作后在服务端通过 getPaidUnionId 得到了 unionid)希望与之关联,可以在小程序中使用 [代码]AV.User#associateWithWeappWithUnionId()[代码]: [代码]const user = AV.User.current(); // 获取当前登录用户 user.associateWithWeappWithUnionId(unionid, { asMainAccount: true }).then(console.log, console.error); 启用其他登录方式 上述的登录 API 对接的是小程序的用户系统,所以使用这些 API 创建的用户无法直接在小程序之外的平台上登录。如果需要使用 LeanCloud 用户系统提供的其他登录方式,如用手机号验证码登录、邮箱密码登录等,在小程序登录后设置对应的用户属性即可: [代码]// 小程序登录 AV.User.loginWithWeapp().then(user => { // 设置并保存手机号 user.setMobilePhoneNumber('13000000000'); return user.save(); }).then(user => { // 发送验证短信 return AV.User.requestMobilePhoneVerify(user.getMobilePhoneNumber()); }).then({ // 用户填写收到短信验证码后再调用 AV.User.verifyMobilePhone(code) 完成手机号的绑定 // 成功后用户的 mobilePhoneVerified 字段会被置为 true // 此后用户便可以使用手机号加动态验证码登录了 }).catch(console.error); 验证手机号码功能要求在 控制台 > 存储 > 设置 > 用户账号 启用「用户注册时,向注册手机号码发送验证短信」。 绑定现有用户 如果你的应用已经在使用 LeanCloud 的用户系统,或者用户已经通过其他方式注册了你的应用(比如在 Web 端通过用户名密码注册),可以通过在小程序中调用 [代码]AV.User#associateWithWeapp()[代码] 来关联已有的账户: [代码]// 首先,使用用户名与密码登录一个已经存在的用户 AV.User.logIn('username', 'password').then(user => { // 将当前的微信用户与当前登录用户关联 return user.associateWithWeapp(); }).catch(console.error); 更多内容欢迎查看《在微信小程序与小游戏中使用 LeanCloud》。
写在前面的话 大家看到这个文章时一定会感觉这是在炒剩饭,社区中已经有那么多分享自定义导航适配的文章了,为什么我还要再写一个呢? 主要原因就是,社区中大部分的适配方案中给出的大小是不精确的,并不能完美适配各种场景。 社区中大部分文章给到的值是 iOS -> 44px , Android -> 48px 正常来讲,iOS和Android下的胶囊按钮的位置以及大小都是相同且不变的,我们可以通过胶囊按钮的位置和大小再配合 wx.getSystemInfo 或者 wx.getSystemInfoSync 中得到的 [代码]statusBarHeight[代码] 来计算出导航栏的位置和大小。 小程序提供了一个获取菜单按钮(右上角胶囊按钮)的布局位置信息的API,可以通过这个API获取到胶囊按钮的位置信息,但是经过实际测试,这个接口目前存在BUG,得到的值经常是错误的(通过特殊手段可以偶尔拿到正确的值),这个接口目前是无法使用的,等待官方修复吧。 下面是我经过实际测试得到的准确数据: 真机和开发者工具模拟器上的胶囊按钮不一样 [代码]# iOS top 4px right 7px width 87px height 32px # Android top 8px right 10px width 95px height 32px # 开发者工具模拟器(iOS) top 6px right 10px width 87px height 32px # 开发者工具模拟器(Android) top 8px right 10px width 87px height 32px [代码]top[代码] 的值是从 [代码]statusBarHeight[代码] 作为原点开始计算的。 使用上面数据中胶囊按钮的高度加 [代码]top[代码] * 2 上再加上 [代码]statusBarHeight[代码] 的高度就可以得到整个导航栏的高度了。 为什么 [代码]top[代码] * 2 ?因为胶囊按钮是垂直居中在 title 那一栏中的,上下都要有边距。 通过胶囊按钮的 [代码]right[代码] 可以准确的算出自定义导航的 [代码]左边距[代码]。 通过胶囊按钮的 [代码]right[代码] + [代码]width[代码] 可以准确的算出自定义导航的 [代码]右边距[代码] 。 通过 wx.getSystemInfo 或者 wx.getSystemInfoSync 中得到的 [代码]windowWidth[代码] - 胶囊按钮的 [代码]right[代码] + [代码]width[代码] 可以准确的算出自定义导航的 [代码]width[代码] 。 wx.getSystemInfo 或者 wx.getSystemInfoSync 中得到的 [代码]statusBarHeight[代码] 每个机型都不一样,刘海屏得到的数据也是准确的。 如果是自定义整个页面,iPhone X系列的刘海屏,底部要留 [代码]68px[代码] ,不要问我为什么! https://developers.weixin.qq.com/s/Q79g6kmo7w5J
前段时间一直在做微信小程序的,遇到了许多的坑,其中遇到了需要前端合成图片保存到相册用于分享到朋友圈。借简书记录一下最终解决方案,先看一下最终效果 该文章的所有演示代码托管与github,代码地址,微信调试工具中访问请[代码]关闭合法域名检查[代码],[代码]开启es6转换[代码],真机调试请打开调试[代码]vconsole[代码] 该文章解决的问题如下: 微信小程序生成图片,并保存到相册 微信小程序生成图片实现响应式 微信小程序canvas原生组件如何给画布添加css动画 保存高清分享图方案 微信小程序生成图片实现单屏适应 微信小程序生成图片,并保存到相册 首先,我们希望能实现如下功能,点击用户头像,从底部弹出一个分享弹窗,可以保存合成图片到相册,可以关闭弹层 我们将该功能封装成一个Component自定义组件 定义wxml基本结构 [代码]<view class="share {{visible ? 'show' : ''}}"> <view class="content"> <canvas class="canvas" canvas-id="share" /> <view class="footer"> <view class="save">保存到相册</view> <view class="close">关闭</view> </view> </view> </view> 定义wxss样式 [代码].share { position: fixed; top: 0; left: 0; min-height: 100vh; width: 100%; background: rgba(61, 61, 61, 0.5); visibility: hidden; opacity: 0; transition: opacity 0.2s ease-in-out; z-index: 99999; .share.show { visibility: visible; opacity: 1; .share .content { display: flex; flex-direction: column; justify-content: center; align-items: center; .share .content .footer { width: 562rpx; height: 100rpx; background: #fff; border-top: 2rpx solid #e9e9e9; display: flex; flex-direction: row; justify-content: center; align-items: center; font-size: 28rpx; .share .content .footer .close { width: 100rpx; height: 100rpx; line-height: 100rpx; flex-grow: 0; flex-shrink: 0; text-align: center; border-left: 2rpx solid #e9e9e9; .share .content .footer .save { height: 100rpx; line-height: 100rpx; flex-grow: 1; flex-shrink: 1; text-align: center; .share.show .content .canvas { display: inline-block; .share .content .canvas { display: inline-block; background: #fff; margin: 60rpx 0 0 0; width: 562rpx; height: 1000rpx; 定义json [代码]{ "component": true 定义组件构造器 [代码]Component({ properties: { visible: { type: Boolean, value: false // 由于需要绘制用户信息,由页面传入 userInfo: { type: Object, value: false methods: { draw() { // 实际绘制函数,后续绘制代码放于此处 基本结构和样式定义完成,接下来开始可一开始我们绘制之旅了,合成图片需要用到微信小程序wx.getImageInfo函数,我们先对它进行Promise化方便后期调用 [代码]function getImageInfo(url) { return new Promise((resolve, reject) => { wx.getImageInfo({ src: url, success: resolve, fail: reject, 前期的准备工作建立完成,我们开始定义绘制方法draw [代码]const { userInfo } = this.data const { avatarUrl, nickName } = userInfo // 获取头像图像信息 const avatarPromise = getImageInfo(avatarUrl) // 获取背景图像信息 const backgroundPromise = getImageInfo('https://img.xiaomeipingou.com/_assets_home-share-bg.jpg') Promise.all([avatarPromise, backgroundPromise]) .then(([avatar, background]) => { // 创建绘图上下文 const ctx = wx.createCanvasContext('share', this) const canvasWidth = 281 const canvasHeight = 500 // 绘制背景,填充满整个canvas画布 ctx.drawImage(background.path, 0, 0, canvasWidth, canvasHeight) const avatarWidth = 60 const avatarHeight = 60 const avatarTop = 40 // 绘制头像 ctx.drawImage( avatar.path, canvasWidth / 2 - avatarWidth / 2, avatarTop - avatarHeight / 2, avatarWidth, avatarHeight // 绘制用户名 ctx.setFontSize(20) ctx.setTextAlign('center') ctx.setFillStyle('#ffffff') ctx.fillText( nickName, canvasWidth / 2, avatarTop + 50, ctx.stroke() // 完成作画 ctx.draw() 接下来,我们需要监测visible属性的变化,决定是否开始绘制 [代码]Component({ properties: { visible: { type: Boolean, value: false, observer(visible) { // 当开始显示分享弹窗时开始绘制 if (visible) { this.draw() ....省略其他代码 此时,前端的绘制已基本成型,运行小程序变可看见合成图,由于我们的绘制尺寸是基于iphone6s进行绘制的,在iphone6s及部分相同分辨率查看,尺寸完全吻合,没有任何问题,然而当我们用iphone6s plus或者其他不同分辨率的手机打开时却变成了下面这个样子 绘制的图像没有完全占满画布了,为什么呢?这个是遇到的第二个问题 微信小程序生成图片实现响应式 其实我们的画布宽高单位都是基于rpx单位,因此在不同分辨率的手机上,实际的尺寸也就不同,然而我们绘制图片的尺寸都是以px为单位,自然无法实现响应式,因此我们需要一个js方法用于转换rpx值为px值 解读微信官方文档我们定义如下一个简单的转换方法 [代码]function createRpx2px() { const { windowWidth } = wx.getSystemInfoSync() return function(rpx) { return windowWidth / 750 * rpx const rpx2px = createRpx2px() 定义好了单位转换函数,我们只需转换相关值即可 [代码]const { userInfo } = this.data const { avatarUrl, nickName } = userInfo // 获取头像图像信息 const avatarPromise = getImageInfo(avatarUrl) // 获取背景图像信息 const backgroundPromise = getImageInfo('https://img.xiaomeipingou.com/_assets_home-share-bg.jpg') Promise.all([avatarPromise, backgroundPromise]) .then(([avatar, background]) => { // 创建绘图上下文 const ctx = wx.createCanvasContext('share', this) const canvasWidth = rpx2px(281 * 2) const canvasHeight = rpx2px(500 * 2) // 绘制背景,填充满整个canvas画布 ctx.drawImage(background.path, 0, 0, canvasWidth, canvasHeight) const avatarWidth = rpx2px(60 * 2) const avatarHeight = rpx2px(60 * 2) const avatarTop = rpx2px(40 * 2) // 绘制头像 ctx.drawImage( avatar.path, canvasWidth / 2 - avatarWidth / 2, avatarTop - avatarHeight / 2, avatarWidth, avatarHeight // 绘制用户名 ctx.setFontSize(rpx2px(20 * 2)) ctx.setTextAlign('center') ctx.setFillStyle('#ffffff') ctx.fillText( nickName, canvasWidth / 2, avatarTop + rpx2px(50 * 2), ctx.stroke() // 完成作画 ctx.draw() 此时不管在什么分辨率下的手机都能正常显示了 微信小程序canvas原生组件如何给画布添加css动画 我们都知道微信小程序的canvas是原生组件,对于原生组件有许多的限制,比如不可以使用css动画,官方文档如下: 首先我们试着给canvas父层标签View.content标签添加弹出动画,修改样式如下: [代码].share .content { display: flex; flex-direction: column; justify-content: center; align-items: center; // 新增动画控制 transform: translate3d(0, 100%, 0); transition: transform 0.2s ease-in-out; // 新增动画控制 .share.show .content { transform: translate3d(0, 0, 0); 在调试器中使用,一切都很美好,完全按着预期由底部弹出,然后淡隐,不过当你用真机调试,canvas部分的效果变得不那么顺畅,流畅,没有弹出动画,没有淡隐效果,一切都变得那么的僵硬,那我们该怎么办呢? 解决办法的思路如下: 提供一个canvas标签,不可以做隐藏(隐藏会导致绘制失效),通过css tansform属性移除屏幕让其不可见 用image标签代替canvas标签显示给用户查看 当画布绘制完成后,我们保存绘制的图像到临时目录中,并获取图片地址 将地址提供给image标签用于展示 基于以上思路,首先改造我们的文档结构 [代码]<view class="share {{ visible ? 'show' : '' }}"> <canvas class="canvas-hide" canvas-id="share" /> <view class="content"> <image class="canvas" src="{{imageFile}}" /> <view class="footer"> <view class="save">保存到相册</view> <view class="close" bindtap="handleClose">关闭</view> </view> </view> </view> [代码].share .canvas-hide { position: fixed; top: 0; left: 0; transform: translateX(-100%); width: 562rpx; height: 1000rpx; 想要保存canvas绘制的图像到临时目录,我们需要利用微信小程序的一个api接口wx.canvasToTempFilePath,因此首先我们还是对其进行Promise化 [代码]function canvasToTempFilePath(option, context) { return new Promise((resolve, reject) => { wx.canvasToTempFilePath({ ...option, success: resolve, fail: reject, }, context) 在组件的data属性中新增imageFile [代码]// 仅列出新增部分,省略之前的代码 Component({ data: { imageFile: '' 修改我们的绘制方法 [代码]// 仅列出新增部分,省略之前的代码 // 修改画布的draw函数如下 ctx.draw(false, () => { canvasToTempFilePath({ canvasId: 'share', }, this).then(({ tempFilePath }) => this.setData({ imageFile: tempFilePath })) 此时在真机上运行调试,可以看到完美的满足我们的需求(沾沾自喜) 保存高清分享图方案 接下来我们需要实现保存到相册中,用于分享给朋友圈或者其他微博 保存图片到相册需要调用微信小程序api,wx.saveImageToPhotosAlbum,依照惯例进行Promise化 [代码]function saveImageToPhotosAlbum(option) { return new Promise((resolve, reject) => { wx.saveImageToPhotosAlbum({ ...option, success: resolve, fail: reject, 我们为保存相册新增点击事件 [代码]<view class="save" bindtap="handleSave">保存到相册</view> 最后定义我们的保存方法 [代码]// 仅列出新增部分,省略之前的代码 Component({ methods: { handleSave() { const { imageFile } = this.data if (imageFile) { saveImageToPhotosAlbum({ filePath: imageFile, }).then(() => { wx.showToast({ icon: 'none', title: '分享图片已保存至相册', duration: 2000, 至此保存到相册功能完成了,但是有点瑕疵,原本我们用于绘制的图片非常的高清,可以绘制后保存的图片变得模糊了,没那么高清,这是过不了UED小姐姐那关的 那如何保证保存的图片不会失真呢,我们可以考虑把canvas大小放大到3倍,绘制3倍的图 [代码].share .content .canvas { display: inline-block; background: #fff; margin: 60rpx 0 0 0; width: 1686rpx; // 修改为之前的3倍 height: 3000rpx; // 修改为之前的3倍 修改绘制函数,增长绘制大小为3倍 [代码]const { userInfo } = this.data const { avatarUrl, nickName } = userInfo // 获取头像图像信息 const avatarPromise = getImageInfo(avatarUrl) // 获取背景图像信息 const backgroundPromise = getImageInfo('https://img.xiaomeipingou.com/_assets_home-share-bg.jpg') Promise.all([avatarPromise, backgroundPromise]) .then(([avatar, background]) => { // 创建绘图上下文 const ctx = wx.createCanvasContext('share', this) const canvasWidth = rpx2px(281 * 2 * 3) // 扩大3倍 const canvasHeight = rpx2px(500 * 2 * 3) // 扩大3倍 // 绘制背景,填充满整个canvas画布 ctx.drawImage(background.path, 0, 0, canvasWidth, canvasHeight) const avatarWidth = rpx2px(60 * 2 * 3) // 扩大3倍 const avatarHeight = rpx2px(60 * 2 * 3) // 扩大3倍 const avatarTop = rpx2px(40 * 2 * 3) // 扩大3倍 // 绘制头像 ctx.drawImage( avatar.path, canvasWidth / 2 - avatarWidth / 2, avatarTop - avatarHeight / 2, avatarWidth, avatarHeight // 绘制用户名 ctx.setFontSize(rpx2px(20 * 2 * 3)) // 扩大3倍 ctx.setTextAlign('center') ctx.setFillStyle('#ffffff') ctx.fillText( nickName, canvasWidth / 2, avatarTop + rpx2px(50 * 2 * 3), // 扩大3倍 ctx.stroke() // 完成作画 ctx.draw(false, () => { canvasToTempFilePath({ canvasId: 'share', }, this).then(({ tempFilePath }) => this.setData({ imageFile: tempFilePath })) 我们重新保存图片,发现图片变得高清了,hu~~~ 最后我们可以兴高采烈的把成果交给小测试了,一切看起来都很顺利,可惜终究过不了各种机型分辨率的测试,由于我们的设计基于iphone6s尺寸设计,在部分宽高比不同的机型,高度会超出屏幕高度,变成下面这个样子 按钮被挡住了,这下无奈了 微信小程序生成图片实现单屏适应 我们希望分享弹窗内容能在一个屏幕下显示完全,那可以根据当前手机宽高比与设计稿尺寸宽高比求出一个缩放比例对整体内容进行缩放即可 定义缩放比例计算 [代码]// 仅列出新增部分,省略之前的代码 Component({ data: { responsiveScale: 1, // 缩放比例默认为1 lifetimes: { ready() { const designWidth = 375 const designHeight = 603 // 这是在顶部位置定义,底部无tabbar情况下的设计稿高度 // 以iphone6为设计稿,计算相应的缩放比例 const { windowWidth, windowHeight } = wx.getSystemInfoSync() const responsiveScale = windowHeight / ((windowWidth / designWidth) * designHeight) if (responsiveScale < 1) { this.setData({ responsiveScale, 修改wxml文档 [代码]<view class="share {{ visible ? 'show' : '' }}"> <canvas class="canvas-hide" canvas-id="share" /> <view class="content" style="transform:scale({{responsiveScale}});-webkit-transform:scale({{responsiveScale}});"> <image class="canvas" src="{{imageFile}}" /> <view class="footer"> <view class="save" bindtap="handleSave">保存到相册</view> <view class="close" bindtap="handleClose">关闭</view> </view> </view> </view> 修改wxss样式表 [代码].share .content { // 省略其他定义 // 新增缩放中心控制为顶部中心 transform-origin: 50% 0; 整体分享遇到的坑都得到了解决,代码较多,所有的代码都托管到了github,欢迎访问运行代码地址,只有亲力亲为才能真正的掌握知识
日常的微信H5营销活动/微信小程序中,经常会碰到需要定时通知/提醒用户的场景,本文主要帮你了解各种通知用户的方法和场景. 各类消息功能对比 先上各类消息功能和限制上的对比结果,如下表 服务号也有客服消息,但触发条件比较苛刻,需要 1、用户发送信息 2、点击自定义菜单(仅有点击推事件、扫码推事件、扫码推事件且弹出“消息接收中”提示框这3种菜单类型是会触发客服接口的) 3、关注公众号 4、扫描二维码 5、支付成功 6、用户维权 跟平时H5跟小程序的场景不符合,本文暂不讨论服务号的客服消息 服务号发送模板消息流程 1.第一次使用模板消息需要申请模板: 登陆服务号后台,访问功能->模板消息,设置对应的行业,并申请模板,并获得模板id和模板格式 2.调用发送接口 用户必须关注才能收到服务号模板消息! 用户必须关注才能收到服务号模板消息! 用户必须关注才能收到服务号模板消息! 发送给未关注的用户会得到类似的返回结果: {“errcode”:43004,“errmsg”:“require subscribe hint: [l8Xf.a0132dsz1]”} 详细参数接口请看: https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1433751277 3.用户接收消息效果图 会话列表:[图片] 会话内容页: 发送一次性订阅消息流程 1.获取用户授权 控制页面跳转到https://mp.weixin.qq.com/mp/subscribemsg?action=get_confirm&appid=你的AppId&scene=1000&template_id=1uDxHNXwYQfBmXOfPJcjAS3FynHArD8aWMEFNRGSbCc&redirect_url=跳转回来的链接&reserved=test#wechat_redirect 参数说明: appid:服务号appid scene:场景值id,一般一个活动页面用一个唯一的即可,1-10000的int类型 reserved:防CSRF的参数,随机值就可以,回调地址再校验一次,可选参数 redirect_url:接收回调参数的地址,urlencode,第一次接入必须在设置-公众号设置-功能设置 添加业务域名,如下图 template_id:在公众号后台获取即可,接口权限->一次性订阅消息->查看模板id,如下图 2.用户跳转到页面后会微信显示如下内容 用户确认后,会跳转到上面传过去的redirect_url,会带上以下参数 openid:用户id,用户拒绝没有此参数 template_id:模板id action:用户确认则是confirm,反之为cancel scene:场景值 reserved:防csrf攻击的参数 后端保存openid,template和scene,用于发送数据 3.后端调用接口发送消息 到了需要发送的时候(活动到点提醒/抽奖结果通知等等),后端再调用接口发送: 可发送小程序或网页url,网页的url无域名限制,用户无需关注也可以发送消息,订阅号也可以发送此类消息. 更多参数以官方文档为准: https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1500374289_66bvB 3.发送效果图 已关注服务号的用户会出现在服务号会话里,如下: 未关注服务号的用户会出现在服务通知: 小程序发送客服消息流程 由于前期开发者对客服消息的滥用,微信封禁了用户进入会话的事件,现在需要用户从客服会话窗口主动发送消息,服务端收到事件才可以对用户推送消息. 自4月9日起,用户通过客服消息按钮进入会话,该动作不再支持开发者给用户下发客服消息,改由平台统一给用户展示接入提示。开发者仅可在用户主动咨询后进行回复,否则将收到45047的错误码返回:客服接口下行条数超过上限。请尽快进行适配。 1.设置消息推送URL 如果是第一次接入客服消息,需要设置消息推送url 设置->开发设置->消息推送 接入的详细文档请看:https://developers.weixin.qq.com/miniprogram/dev/api/custommsg/callback_help.html 2.引导用户发送消息到客服消息 引导用户点击’open-type=“contact”'的button进入客服会话聊天窗口,并让用户发送对应文字 <button class="download" open-type="contact" session-from="wesixpuzzleapp"></button> 如下图,点击后即进入客服会话 服务端接收微信的消息推送后,可以下发消息到聊天窗,该消息可以打开任意url和小程序,可拉起应用下载 小程序发送模板消息流程 1.一如既往地先申请/添加模板 2.引导用户提交form 一般是让用户点击form里面的button,如签到/留言等功能按钮,form标签的report-submit属性必须设置为true,如: <form bindsubmit="formSubmit" report-submit="{{true}}"> <button class="download" formType="submit"></button> </form> (样式也是继续使用上面客服消息的例子) 3.向后台传递formId 用户点击后,触发bindsubmit的事件,callback的参数会带上formId,传递这个formId到后台 formSubmit: function(e) { wx.request({ url: ‘https://xxxx.xxx/miniapp/push-message’, method: “post”, data: { session_id: app.globalData.sessionId, form_id: e.detail.formId, //formId,重要 success(e) { console.log(e) [代码]4.后台发送模板消息 注意,formId七天内有效,超过其他就不能再用该formId向用户发送模板消息,该消息只能打开原有的小程序 后台的api文档请看这里https://developers.weixin.qq.com/miniprogram/dev/api/notice.html#%E5%8F%91%E9%80%81%E6%A8%A1%E6%9D%BF%E6%B6%88%E6%81%AF 5.用户将在服务通知收到推送的模板消息 效果如下:
getCurrentPages()的用法 getCurrentPages()是个好东西,今天来说说他的用法。 先看看官方文档: [路由 · 小程序]:https://developers.weixin.qq.com/miniprogram/dev/framework/app-service/route.html getCurrentPages() 函数用于获取当前页面栈的实例,以数组形式按栈的顺序给出,第一个元素为首页,最后一个元素为当前页面。 简单说,就是可以获取到当前小程序的页面栈 那么,获取到页面栈,有什么用处呢? 1、判断页面栈是否超过10级,超过10级,将不能打开新页面(主要是不用用navigateTo方式打开)。 2、可以修改某个页面栈的data数据,或者方法。 这里给大家分享一个实际应用场景:仅发起者可分享。 有些投票、通知、抽奖、签到等,发起者会在私密的圈子内进行,比如,仅会员群才能参与的抽奖、公司内部的通知公告、班级内部的投票等。发起者是不希望别人分享出去,那么小程序里面要怎么做? 说到分享,在小程序内,应该是想到onShareAppMessage这个方法。只要page.js中有这个方法,不管你是否在内部写了代码逻辑,小程序默认就是可以分享的,如果没有这个方法,小程序右上角的“…”就不会出现“转发”的选项。 问题是,是否允许分享,一般都是小程序内的一个开关设置项,可以看下图: 用户在加载内容时,需要先从服务端获取到这个开关状态,再决定是否出现“转发”的选项。 此时,我们默认不给page添加onShareAppMessage方法,这样,你转发出去的小程序卡片,别人将无法通过长按进行分享(群聊无法长按分享,私聊还是个坑,看下图)。 然后再动态设置当前page的onShareAppMessage方法,用this,或者getCurrentPages()都能解决,看下图: 目前私聊的卡片,长按依然可以转发,似乎不是很完美,但是,功能基本实现了。 如果想让私聊卡片的转发无效,你也可以变通一下,比如做个限群成员可见功能,即使私聊卡片被转发,可以判断小程序场景值,不展示内容即可~ 1、用wx.getLaunchOptionsSync() 获取小程序启动时的参数: 2、判断群聊和私聊的场景值: 3、如果是微信私聊中打开,给用户提示即可~ 欢迎各位一起讨论技术问题:mianhuabingbei
以下以微信小程序“虎牙直播”为例,演示如何复制微信小程序页面的路径。 1.进入小程序的“关于虎牙直播”页面 2.点击右上角的“…”进入“更多资料”页面 3.复制AppID:wx74767bf0b684f7d3 4.进入小程序后台输入appid并搜索,然后点下一步 5.鼠标移动到“获取更多页面路径”,在弹出窗口输入当前登陆的小程序的任意开发者微信号,然后点击开启,出现顶部的“开启入口成功”就可以使用手机访问“虎牙直播”任意页面进行复制了 6.某个直播间的页面路径:pages/main/liveRoom/index.html?anchorUid=1678113423&source=search[图片] PS:复制出来的页面路径在小程序里使用的时候记得删除 .html 才能正常访问。
相信各位在开发的时候应该有遇到这样一个场景,比如商品的图片浏览,有时图片的浏览会很大,多的时候达几百张或上千张,这样就需要swiper里需要很多swiper-item,如此一来渲染的时候就会很消耗性能,渲染时会有一大段的空白时间,有时还会造成卡顿,体验非常差,下面给大家介绍一下我的解决方案。 首先是wxml结构: 主要是利用current属性,swiper里面只放3个swiper-item,要显示的图片放在第二,第一和第三放的是加载的动画背景,步骤如下: 将请求到的数据存入一个数组picListAll内,这里不需要setData,只需要在data外面定义一个变量就行了,以减少渲染性能; 把要显示的图片路径赋值给picUrl; 切换的时候根据bindchange获取current属性,当current改变时判断当前图片在picListAll的index,根据index拿到图片再赋值给picUrl; 主要实现步骤就是以上3 步,比较简单,要注意的是当切换到第一张和最后一张的时候要判断一下,把loding动画去掉,请求的时候还可以传入index参数以显示不同的图片,方便从前一页点击图片进入到此页面时能定位到该图片,例子里我是自己mock数据的,只是为了展示,如果你有服务器的话可以弄几百张看看效果,对比直接渲染和用以上方式渲染的差异。当然,这只是我的解决方案,如果各位有更好的方案欢迎一起讨论,一起进步。 完整代码:https://github.com/HaveYuan/swiper
实现代码是 /* ::-webkit-scrollbar { width: 0; height: 0; color: transparent; display: none; 但是今天使用的时候突然不能生效了,之前是可以的。 研究了一下,发现没有给父级元素设置属性。 .store-page { //父元素 width: 100vw; height: 100vh; overflow-x: hidden; overflow-y: auto;
开发微信小程序时,不能直接在wxss文件里引用本地图片,运行时会报错:“本地资源图片无法通过WXSS获取,可以使用网络图片,或者 base64,或者使用<image/>标签。” 这里主要介绍使用<image>标签的方法 网上有很多方法,笔者也尝试了不少,期间也遇到一些问题。最后总结一下,只需2步: 1、在wxml文件中添加一个<image>标签: [代码]<!--页面根标签--> <view class="content"> <!--pics文件夹下的background.jpg文件--> <image class='background' src="../../pics/background.jpg" mode="aspectFill"></image> <!--页面其它部分--> </view> 2、在wxss文件中添加: [代码]page{ height:100%; .background { width: 100%; height: 100%; position:fixed; background-size:100% 100%; z-index: -1; 要说明的是z-index: -1,可以让图片置于最底层,不会影响其它部分。
小程序海报组件 https://github.com/jasondu/wxa-plugin-canvas 小程序分享到朋友圈只能使用小程序码海报来实现,生成小程序码的方式有两种,一种是使用后端方式,一种是使用小程序自带的canvas生成;后端的方式开发难度大,由于生成图片耗用内存比较大对服务端也是不小的压力;所以使用小程序的canvas是一个不错的选择,但由于canvas水比较深,坑比较多,还有不同海报需要重现写渲染流程,导致代码冗余难以维护,加上不同设备版本的情况不一样,因此小程序海报生成组件的需求十分迫切。 在实际开发中,我发现海报中的元素无非一下几种,只要实现这几种,就可以通过一份配置文件生成各种各样的海报了。 海报中的元素分类 要解决的问题 canvas隐藏问题 圆角矩形、圆角图片 超长文字和多行文字缩略问题 矩形包含文字 多个元素间的层级问题 图片尺寸和渲染尺寸不一致问题 canvas转图片 IOS 6.6.7 clip问题 关于获取canvas实例 canvas绘制使用的是px单位,但不同设备的px是需要换算的,所以在组件中统一使用rpx单位,这里就涉及到单位怎么换算问题。 通过wx.getSystemInfoSync获取设备屏幕尺寸,从而得到比例,进而做转换,代码如下: [代码]const sysInfo = wx.getSystemInfoSync(); const screenWidth = sysInfo.screenWidth; this.factor = screenWidth / 750; // 获取比例 function toPx(rpx) { // rpx转px return rpx * this.factor; function toRpx(px) { // px转rpx return px / this.factor; canvas隐藏问题 在绘制海报过程时,我们不想让用户看到canvas,所以我们必须把canvas隐藏起来,一开始想到的是使用; 但这样在转化成图片时会空白,所以这个是行不通的,所以只能控制canvas的绝对定位,将其移出可视界面,代码如下: [代码].canvas.pro { position: absolute; bottom: 0; left: -9999rpx; 圆角矩形、圆角图片 由于canvas没有提供现成的圆角api,所以我们只能手工画啦,实际上圆角矩形就是由4条线(黄色)和4个圆弧(红色)组成的,如下: 圆弧可以使用canvasContext.arcTo这个api实现,这个api的入参由两个控制点一个半径组成,对应上图的示例 [代码]canvasContext.arcTo(x1, y1, x2, y2, r) 接下来我们就可以非常轻松的写出生成圆角矩形的函数啦 [代码]/** * 画圆角矩形 _drawRadiusRect(x, y, w, h, r) { const br = r / 2; this.ctx.beginPath(); this.ctx.moveTo(this.toPx(x + br), this.toPx(y)); // 移动到左上角的点 this.ctx.lineTo(this.toPx(x + w - br), this.toPx(y)); // 画上边的线 this.ctx.arcTo(this.toPx(x + w), this.toPx(y), this.toPx(x + w), this.toPx(y + br), this.toPx(br)); // 画右上角的弧 this.ctx.lineTo(this.toPx(x + w), this.toPx(y + h - br)); // 画右边的线 this.ctx.arcTo(this.toPx(x + w), this.toPx(y + h), this.toPx(x + w - br), this.toPx(y + h), this.toPx(br)); // 画右下角的弧 this.ctx.lineTo(this.toPx(x + br), this.toPx(y + h)); // 画下边的线 this.ctx.arcTo(this.toPx(x), this.toPx(y + h), this.toPx(x), this.toPx(y + h - br), this.toPx(br)); // 画左下角的弧 this.ctx.lineTo(this.toPx(x), this.toPx(y + br)); // 画左边的线 this.ctx.arcTo(this.toPx(x), this.toPx(y), this.toPx(x + br), this.toPx(y), this.toPx(br)); // 画左上角的弧 如果是画线框就使用[代码]this.ctx.stroke();[代码] 如果是画色块就使用[代码]this.ctx.fill();[代码] 如果是圆角图片就使用 [代码]this.ctx.clip(); this.ctx.drawImage(***); clip() 方法从原始画布中剪切任意形状和尺寸。一旦剪切了某个区域,则所有之后的绘图都会被限制在被剪切的区域内(不能访问画布上的其他区域)。可以在使用 clip() 方法前通过使用 save() 方法对当前画布区域进行保存,并在以后的任意时间对其进行恢复(通过 restore() 方法)。 如果是连续多段不同格式的文字,如果让用户每段文字都指定坐标是不现实的,因为上一段文字的长度是不固定的,这里的解决方案是使用[代码]ctx.measureText[代码] (基础库 1.9.90 开始支持)Api来计算一段文字的宽度,记住这里返回宽度的单位是px(坑),从而知道下一段文字的坐标。 超长文字和多行文字缩略问题 设置文字的宽度,通过[代码]ctx.measureText[代码]知道文字的宽度,如果超出设定的宽度,超出部分使用“…”代替;对于多行文字,经测试发现字体的高度大约等于字体大小,并提供lineHeight参数让用户可以自定义行高,这样我们就可以知道下一行的y轴坐标了。 矩形包含文字 这个同样使用[代码]ctx.measureText[代码]接口,从而控制矩形的宽度,当然这里用户还可以设置paddingLeft和paddingRight字段; 文字的垂直居中问题可以设置文字的基线对齐方式为middle([代码]this.ctx.setTextBaseline('middle');[代码]),设置文字的坐标为矩形的中线就可以了;水平居中[代码]this.ctx.setTextAlign('center');[代码]; 多个元素间的层级问题 由于canvas没有Api可以设置绘制元素的层级,只能是根据后绘制层级高于前面绘制的方式,所以需要用户传入zIndex字段,利用数组排序(Array.prototype.sort)后再根据顺序绘制。 图片尺寸和渲染尺寸不一致问题 绘制图片我们使用[代码]ctx.drawImage()[代码]API; 如果使用[代码]drawImage(dx, dy, dWidth, dHeight)[代码],图片会压缩尺寸以适应绘制的尺寸,图片会变形,如下图: 在基础库1.9.0起支持[代码]drawImage(sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)[代码],sx和sy是源图像的矩形选择框左上角的坐标,sWidth和sHeight是源图像的矩形选择框的宽度和高度,如下图: 如果绘制尺寸比源图尺寸宽,那么绘制尺寸的宽度就等于源图宽度;反之,绘制尺寸比源图尺寸高,那么绘制尺寸的高度等于源图高度; 我们可以通过[代码]wx.getImageInfo[代码]Api获取源图的尺寸; canvas转图片 在canvas绘制完成后调用[代码]wx.canvasToTempFilePath[代码]Api将canvas转为图片输出,这样需要注意,[代码]wx.canvasToTempFilePath[代码]需要写在[代码]this.ctx.draw[代码]的回调中,并且在组件中使用这个接口需要在第二个入参传入this(坑),如下 [代码]this.ctx.draw(false, () => { wx.canvasToTempFilePath({ canvasId: 'canvasid', success: (res) => { wx.hideLoading(); this.triggerEvent('success', res.tempFilePath); fail: (err) => { wx.hideLoading(); this.triggerEvent('fail', err); }, this); IOS 6.6.7 clip问题 在IOS 6.6.7版本中clip方法连续裁剪图片时,只有第一张有效,这是微信的bug,官方也证实了(https://developers.weixin.qq.com/community/develop/buglist) 关于获取canvas实例 我们可以使用[代码]wx.createCanvasContext[代码]获取小程序实例,但在组件中使用切记第二个参数需要带上this,如下 [代码]this.ctx = wx.createCanvasContext('canvasid', this); 如何使用组件 https://github.com/jasondu/wxa-plugin-canvas
大家好,上次给大家分享了swiper仿tab的小技巧: https://developers.weixin.qq.com/community/develop/article/doc/000040a5dc4518005d2842fdf51c13 [代码]今天给大家分享两个有用的函数,《函数防抖和函数节流》 函数防抖和函数节流是都优化高频率执行js代码的一种手段,因为是js实现的,所以在小程序里也是适用的。 首先先来理解一下两者的概念和区别: [代码] 函数防抖(debounce)是指事件在一定时间内事件只执行一次,如果在这段时间又触发了事件,则重新开始计时,打个很简单的比喻,比如在打王者荣耀时,一定要连续干掉五个人才能触发hetai kill '五连绝世'效果,如果中途被打断就得重新开始连续干五个人了。 函数节流(throttle)是指限制某段时间内事件只能执行一次,比如说我要求自己一天只能打一局王者荣耀。 这里也有个可视化工具可以让大家看一下三者的区别,分别是正常情况下,用了函数防抖和函数节流的情况下:http://demo.nimius.net/debounce_throttle/ 搜索框搜索联想。只需用户最后一次输入完,再发送请求 手机号、邮箱验证输入检测 窗口resize。只需窗口调整完成后,计算窗口大小。防止重复渲染 高频点击提交,表单重复提交 滚动加载,加载更多或滚到底部监听 搜索联想功能 [代码] 函数防抖 [代码]const _.debounce = (func, wait) => { let timer; return () => { clearTimeout(timer); timer = setTimeout(func, wait); [代码] 函数节流 [代码]const throttle = (func, wait) => { let last = 0; return () => { const current_time = +new Date(); if (current_time - last > wait) { func.apply(this, arguments); last = +new Date(); [代码] 上面两个方法都是比较常见的,算是简化版的函数 lodash中的 Debounce 、Throttle [代码] lodash中已经帮我们封装好了这两个函数了,我们可以把它引入到小程序项目了,不用全部引入,只需要引入debounce.js和throttle.js就行了,链接:https://github.com/lodash/lodash 使用方法可以看这个代码片段,具体的用法可以看上面github的文档,有很详细的介绍:https://developers.weixin.qq.com/s/vjutZpmL7A51[代码]
项目有一个使用二维码图片(远程url)生成海报的功能,基于微信的画布(canvas)接口实现,开发者工具生成无问题,到真机后发现图片未绘制出来( 准确的说的绘制后保存tempFile没有图片,但文字内容都是ok的 )。经过一番摸索猜测原因为真机canvas无法实时绘制远程图片资源(可能是异步、执行顺序的问题?)。 通过以下方式解决: 先使用getImageInfo接口预加载远程图片,让后用预加载的缓存图片进行绘制。 示例代码: [代码]let ctx = wx.createCanvasContext('your-canvas-id') let remote_url = 'https://xxx.com/your-image.png'; wx.getImageInfo({ src : remote_url, success : res => { //res.path 为getImageInfo预加载的缓存图片地址 ctx.drawImage( res.path , x, y, w, h);
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 代码 //会创建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哦!🌟🌟🌟
基本实现功能 1,小程序仿天猫超市大转盘 2,九宫格转盘抽奖 3,积分抽奖 4,抽到的积分随机生成 5,抽奖结果可以同步到服务器(小程序云开发后台) 老规矩先看效果图 简单说一下实现原理. 我们借助js的定时器,来执行一个加法。比如我们设置一个上限300,每过一定时间执行一次,然后我们再做一个随机数,这个随机数不停的++,直到总数大于300.就代表抽奖结束。核心代码如下。 [代码] //开始抽奖 startGame: function() { if (this.data.isRunning) return this.setData({ isRunning: true var _this = this; var indexSelect = 0 var i = 0; var timer = setInterval(function() { indexSelect++; let randomNum = Math.floor(Math.random() * 10) * 10; //可均衡获取0到90的随机整数 i += randomNum; if (i > 300) { //去除循环 clearInterval(timer) //获奖提示 let jifen = 1; let selectNum = _this.data.indexSelect console.log("选号:" + selectNum ); if (selectNum===0) { jifen = 2; } else if (selectNum === 1) { jifen = 3; } else if (selectNum === 2) { jifen = 4; } else if (selectNum === 3) { jifen = 5; } else if(selectNum === 4) { jifen = 6; } else if(selectNum === 5) { jifen = 8; } else if (selectNum === 6) { jifen = 10; wx.showModal({ title: '恭喜您', content: '获得了' + jifen + "积分", showCancel: false, //去掉取消按钮 success: function(res) { if (res.confirm) { _this.setData({ isRunning: false indexSelect = indexSelect % 8; _this.setData({ indexSelect: indexSelect }, (200 + i)) 完整源码可以加我微信,如果有关于小程序的问题,可以加我微信2501902696(备注小程序)
在目前没有同层渲染的原生组件上加入文字、聊天、弹幕等就要用cover-view来解决了。 聊天每增加一条就自动滚动,上代码 此代码准确获取需要滚动的高度,解决苹果手机上的scroll-top过大导致溢出问题 <cover-view class=‘chat’ scroll-top="{{scrollTop}}" style=“bottom:105rpx;z-index:999999;{{!showbtn ? ‘;’:‘display:block;’}}” bindtap=“hideshow”> <cover-view id=“bianjie”> <cover-view class=‘item’ wx:for="{{chatlist}}" wx:for-item=“record” wx:for-index=“recordid” wx:key=“index” > <cover-view class=‘name’>{{record.nickname}}</cover-view> <cover-view class=‘text’>{{record.content}}</cover-view> </cover-view> </cover-view> </cover-view> <button formType=“submit” class="btn-send ">发送</button> js中处理 //发送聊天 formSubmit: function (e) { //处理聊天内容 [代码]//设置scrollTop this.queryMultipleNodes(); // 获取节点信息并设置scrollTop queryMultipleNodes: function () { var query = wx.createSelectorQuery(), e = this; wx.createSelectorQuery().in(e).select(’.chat’).boundingClientRect(function (res) { e.setData({ chatbottom: res.bottom, }).exec() query.in(e).select(’#bianjie’).boundingClientRect(function (res) { if (res.bottom != undefined && res.bottom != ‘’ && res.bottom != null && typeof (res.bottom) != ‘undefined’) { //var setsbottom = Math.ceil(res.bottom) ; var setsbottom = setsbottom = Math.ceil(parseInt(res.bottom) - parseInt(e.data.chatbottom)); e.setData({ scrollTop: setsbottom }); }).exec() 初次分享,献丑了,如有不对的地方请各位大神多多指正,谢谢(-)。
Protocol Buffers,是Google公司开发的一种数据描述语言,类似于XML能够将结构化数据序列化,可用于数据存储、通信协议等方面。 本文和大家聊聊怎样在前端使用 protobuf,在开始前先聊聊 JSON。 关于 JSON 我们前端通常是使用 JSON 格式作为数据格式,JSON也有优点: 1、原生支持 符合 JavaScript 原生语法,书写简单,直观一目了然 我们可以使用 JSON.parse 和 JSON.stringify 来做反序列化和序列化,性能好,这一切不需要使用第三方库。 3、不需要描述文件 这点还是优于 PB 的,PB文件需要描述文件来做对应的解析,而 JSON 不需要描述文件。 4、抓包方便 我们可以很方便地通过 Chrome DevTools , Fiddler,Whistle 等抓包工具可以查看返回的 JSON 数据。 关于 protobuf 要使用 protobuf 首先要一个数据结构的描述文件,然后使用特殊生成的源代码轻松的在各种数据流中使用各种语言进行编写和读取结构数据。 1、体积小 protobuf 对数据序列化后,数据大小可约小3倍(当然,压缩比还要看具体内容)。 2、语言无关、平台无关 ProtoBuf 支持 Java、C++、Python 等多种语言,支持多个平台。 即比 XML 更小(3 ~ 10倍)、更快(20 ~ 100倍)、更为简单。 4、扩展性、兼容性好 你可以更新数据结构,而不影响和破坏原有的旧程序。 为什么在前端使用 protobuf protobuf 已经被后台广为使用,而且优点那么多,为什么前端不愿意使用呢?我认为很重要的两点: 1、不便于抓包和调试,因为 protobuf 需要依赖对应的描述文件才可以正确地把数据解析出来,而JSON不需要,这就导致在开发和测试场景下不便于抓包。 2、需要先把 protobuf 描述文件异步加载,这就多了一次网络请求,或者打包到业务代码中,导致业务代码变大。 3、解析后有很多冗余的字段,需要前端过滤和整理成可用的 JSON 结构体。 有了这些问题为什么还折腾 protobuf 呢? 其实在某些场景下比如单个请求数据量较大,上几M 的回包,当然也可以选择分包拉取,但这样无论并发加载还是阻塞加载,如果需要读取到某片数据,那么你的业务代码写起来将是将是非常复杂。 ##数据对比 后端通过 protobuf 指令生成的 protobuf 描述文件,需要依赖于 google-protobuf.js。 拿我们的回放业务来做对比,课堂进入回放模式时需要加载互动字幕文件,如果这些数据使用 protobuf 会有什么变化? 先看看企鹅辅导的回放字幕文件结构: 注:因为数据有嵌套 protobuf ,所以要分为整体字幕文件 和 具体的交互 pb 描述文件,待解析到具体交互时,才会使用 交互 pb 描述文件去解析 。 注:以上 pb 描述文件整体 zip 之后 52 KB。 google-protobuf.js 大小:157 KB,zip 后 33KB。 一共 zip 后 85 KB (机子压缩工具没 gzip 格式,这里暂用 zip 。gzip 比 zip 压缩比更高) (1)两种格式字幕文件体积对比 这里抽样对比pb格式的二进制字幕文件体积与对应的json格式的字幕文件的体积 pb字幕文件体积 json字幕文件体积 json体积/pb体积 通过抽样对比可得,json格式的字幕文件的体积基本是pb格式的2倍以上,最多甚至是4倍以上,体积差异比较大。 (2)浏览器解析pb字幕文件速度对比 因为字幕文件中的信息节点较多,这里调研解析pb字幕文件的速度。使用的字幕文件是目前体积最大的文件,体积是4.3M。 解析字幕文件的过程分为两步: 解析外层数据,得到json格式概要信息 + 二进制具体数据 解析内层数据,得到json格式具体数据 字幕pb 文件(4.3M)做测试得出耗时数据: 总结:如果一次解析外层数据和内层数据,会耗时较长,而如果只解析内层数据只需要279ms,比较短。在实现的时候只需要解析外层数据得到概要信息(时间信息、消息类型等),在具体展示的时候才解析内层数据,这样是可以大幅降低初次解析数据导致的耗时。 dcodeIO 给出的浏览器兼容 IE9+ Show you the code 后端提供给前端 pb 描述文件需要执行以下命令来生成 js 可用的 pb 描述文件 [代码]protoc --js_out=library=myproto_libs,binary:. messages.proto base.proto 假设前端已经拿到了 pb 描述文件 pbplaybackinfo_pb.js ,但描述文件依赖于 google-protobuf.js ,要整体打包后使用。 那么首先 npm i google-protobuf 在业务代码中开始引入: [代码]const messages = require('./pbplaybackinfo_pb'); const playbackRspBody = new messages.PlaybackRspBody; fetch('https://fudao.qq.com/pb/test.pb').then((res) => { res.arrayBuffer().then((res) => { console.time('pb'); const arrayBuffer = new Uint8Array(res); console.log(messages.PlaybackRspBody.deserializeBinary(arrayBuffer, playbackRspBody).toObject()); console.timeEnd('pb'); 接着我们这里的例子是异步拉取测试用的 pb 文件,假设是 test.pb。 因为 pb 数据文件是二进制的,所以这里需要使用 arrayBuffer() 的方式,待拉取完毕后再转 Uint8Array 类型。什么是 arrayBuffer ,什么是 uint8Array? google-protobuf 提供的反序列化接口 deserializeBinary 必须是由对应的 pb 描述中的静态方提供。所以上面使用了 [代码]messages.PlaybackRspBody.deserializeBinary( )[代码] 到这里你拿到是未格式化的二进制 arrayBuffer,所以一定要 .toObject( ) ,官方文档没有提及到这个方法,在这我也折腾了比较长的时间。 前端使用 json 还是 protobuf,还是看具体场景,普通的数据量小的接口当然是 JSON 最好,因为无论从可读性和调试成本上来看,这是最做优的。如果是数据量较大的情况下,需要综合业务代码的改造成本和用户体验等多方面做权衡。
这几天看了很多关于防抖函数的博客,我是在微信小程序中使用,在此总结一下关于防抖函数的知识。 为什么需要防抖函数? 防抖函数适用的是【有大量重复操作】的场景,比如列表渲染之后对每一项进行操作。 函数代码: [代码]var timer; debounce: function (func, wait) { return () => { clearTimeout(timer); timer = setTimeout(func, wait); func:需要防抖的函数; wait:number类型,setTimeout的时间参数; 代码分析: 命名一个叫做debounce的函数,参数有两个(func,wait),return一个函数,内容为清除计时器,然后设置计时器,计时器的意思是:在wait时间后执行func。 清除计时器是整个函数的核心,因为防抖需要不停地清除计时器,最后在计时器结束后触发func来达到效果。 防抖函数的调用方法 example: [代码]this.debounce(this.函数名,3000)() 在使用这个函数的时候我遇到了一些问题: 因为微信小程序中很多地方都需要使用this.setData,如果对于this指向的理解不深入的话,很容易出现以下情况: 1:this==undefined; 2:Error:data is not defined; 等等一些列关于this的问题。 解决方法: [代码]this.debounce(this.函数名.bind(this),3000)() 使用bind(this)把this指向传到函数内部就解决了。
近期,在波洞星球的PC官网项目中,我们采用了新版的 babel7 作为 ES 语法转换器。而 babel7 中的一大变更就是对配置文件的加载逻辑进行了改进,然而实际上对于不熟悉 babel 配置逻辑的朋友往往会带来更多问题。本文就是 babel7 配置文件的中文指南,它是英语渣渣的救星,是给懒人送到口边的一道美味。如有错误 概不负责 欢迎指正。 babel7 从 2018年3月开始进入 alpha 阶段,时隔5个月直到 2018年8月份 release 第一个版本,目前的最新版是2019年2月26号发布的 7.3.4. 时光如梭,在这美好的 9012 年,ES2019 都快要发布了的时刻,我想: 是时候用一用 babel7 了。 本文不是 babel7 的升级教程,而是对 babel7 的新变化和配置逻辑的一点心得。babel7 对monorepo 结构项目的优化恰好符合我们目前项目架构的预期,这简化了我们配置的复杂度,但其难以理解的配置加载逻辑,却让我踩了不少坑,这也正是本文的来源。 在开始讲 babel7 的配置逻辑之前,我们先从以下几个方面来啰嗦几句 babel7 所做的变更及其逻辑意义。 proposal 语法特性 在历史上(babel6)的时代,人们通常使用 babel 提供的 preset-stage 预设来体验 ES6 之后的处于建议阶段的语法特性。例如做如下的 babel 配置: [代码]"presets": ["es2015", "react", "stage-0"] 其中,es2015 预设会包含 ES6 标准中所有语法特性;stage-0预设会包含当前(安装该预设npm包的时刻) 的 ES 语法进展中的 stage 0到3的特性(数字小的包含数字大的)。但事实上 babel 官方这样提供 stage 预设,会有不少问题 随着 es 标准的不断发展,大量的新特性几乎已经成为标准。与此同时,stage0-3阶段的特性必然也发生变化。可以说,stage0-3的阶段特性他们是不稳定的,极有可能在某个时机被TC39委员会除名、变更阶段、改变语法。尽管 babel-preset-* 预设会跟随TC39 保持一致的更新, 但这样的用法需要使用者也不断保持更新 才能跟标准一致 历史上的 preset-es2015 配合 preset-stage-0 的做法极易产生疑惑,例如没有人知道他所需要的特性在stage几 一个语言特性如果从 stage3 变更为 stage4,往往会导致以前的 stage0(包含了1、2、3) 的配置出问题。因为特性推进后,新的stage0中就不再包含该特性内容,但使用者可能不知道要把该特性所在的 ES标准 加入到配置中 大量的社区工具 eslint 等等都依赖 babel;babel 的 preset-stage 预设更新就会导致这些社区工具频频出现问题。 如今,babel 官方认为,把不稳定的 stage0-3 作为一种预设是不太合理的,因此废弃了 stage 预设,转而让用户自己选择使用哪个 proposal 特性的插件,这将带来更多的明确性(用户无须理解 stage,自己选的插件,自己便能明确的知道代码中可以使用哪个特性)。所有建议特性的插件,都改变了命名规范,即类似 [代码]@babel/plugin-proposal-function-bind[代码] 这样的命名方式来表明这是个 proposal 阶段特性。 ES 标准特性 对于正经的 ES 标准特性,babel从6开始就建议使用 babel-preset-env 这个能根据环境进行自动配置的预设。到了 babel7,我们就可以完全告别这几个历史预设了: preset-es2015/es2016/es2017/latest 为什么 preset-env 要更好呢? 我认为,对于开发者而言,关注目标用户平台(兼容哪些浏览器)要比关注 “编译为哪份ES标准” 要更易理解。把选择编译插件的事情交给 preset-env 就好了。它会根据 compat table 和你设置的目标用户平台选择正确的插件。 polyfill 跟 stage 预设的结局一样,对于处于建议阶段的特性,polyfill里面也移除了对他们的支持。 以前的 babel-polyfill 是这么实现的: [代码]import "core-js/shim"; // included < Stage 4 proposals import "regenerator-runtime/runtime" 现在的 @babel/polyfill 就直接引入 core-js v2 的属于ES正式标准的模块。这意味着,如果你需要使用处于 proposal 阶段的语法特性,你需要手工 import core-js 中的对应模块。 从 babel7 开始,所有的官方插件和主要模块,都放在了 @babel 的命名空间下。从而可以避免在 npm 仓库中 babel 相关名称被抢注的问题。有必要说一下的,比如 @babel/node @babel/core @babel/clil @babel/preset-env transform-runtime 以前的 [代码]babel-transform-runtime[代码] 是包含了 helpers 和 polyfill。而现在的 [代码]@babel/runtime[代码] 只包含 helper,如果需要 polyfill,则需主动安装 core-js 的 runtime版本 [代码]@babel/runtime-corejs2[代码] 。并在 [代码]@babel/plugin-transform-runtime[代码] 的插件中做配置。 说重点: 配置 这是本文的重点,先来看一段 babel7 对配置的变更说明 Babel has had issues previously with handling node_modules, symlinks, and monorepos. We’ve made some changes to account for this: Babel will stop lookup at the package.json boundary instead of looking up the chain. For monorepo’s we have added a new babel.config.js file that centralizes our config across all the packages (alternatively you could make a config per package). In 7.1, we’ve introduced a rootMode option for further lookup if necessary. 段落的意思大概有这么几点: Babel将停止在package.json边界查找而不是查找链。译者注:这说明以前babel会递归向上查找babelrc 而现在检索行为会停在package.json所在层级。这可以解决部分符号链接的js向上查找babelrc错乱的问题。 添加了一个新的项目全局babel.config.js文件,可以将整个项目的配置集中在所有包中。译者注:除了新增的这个全局配置,也可以同时支持以前的基于文件的.babelrc的配置 引入了一个rootMode选项,以便在必要时按一定策略查找 babel.config.js 除此之外,babel7 还有一个特性是: 默认情况下,不会加载monorepo项目的任何独立子项目中的 .babelrc 文件 然而,对上面的解释,你可能: 每个字都认识,连在一起却不知道在说什么。下面我们来剖析一下 为了理解 babel7 的配置逻辑,我们就以 babel7 真正所解决的痛点 [monorepo 类型的项目] 为例来剖析。在此之前,我们需要预先确定几个概念。 monorepo。这是个自造词。按我的理解,它的含义是说 [代码]单个大项目但是包含多个子项目[代码] 的含义。如果还是不能理解的话,就把 项目 二字 换成 [代码]npm模块包[代码] (以package.json文件作为分界线)。即 [代码]单个npm包中又包含多个子npm包[代码] 的项目。 例如,波洞的 PC 版采用的是 Node.js 作为前端接入层的方式,在我们的项目结构组织上,是这样的: [代码]|- backend |-package.json |- frontend |-package.json |- node_modules |- config.js |- babel.config.js |- package.json [代码] 这就是典型的 monorepo 结构。 全局配置。在 babel 文档中又叫 项目级别的配置,特指 babel.config.js。如上图的monorepo结构,其 babel.config.js 就是全局配置/项目配置,该 babel 配置对 backend、frontend、甚至 node_modules 中的模块全部生效。 局部配置。在 babel 文档中可能叫 相对于文件的配置。这种配置就是特指的 .babelrc 或 .babelrc.js 了。他们的生效范围是与待编译文件的位置有关的。 懂了几种配置文件的概念和作用范围之后,我们就可以来根据文档和代码测试结果来精确描述 babel7 的配置规则。这里我们直接以 monorepo 类型项目为例来说,因为普通项目会更简单。 下文中可能用到的名词解释: 我们用 package 来代指一个具有独立 package.json 的项目,如上面案例中的 frontend 可以称作一个 package,backend也可以称作一个package; 我们用 相对配置 这个名词来表达所谓的 .babelrc 和 .babelrc.js,用全局配置来代指 babel.config.js这份配置 对monorepo类型项目,babel7 的处理逻辑是: 【全局配置】全局配置 babel.config.js 里的配置默认对整个项目生效,包括node_modules。除非通过 exclude 配置进行剔除。 【全局配置】全局配置中如果没有配置 babelrcRoots 字段,那么babel 默认情况下不会加载任何子package中的相对配置(如.babelrc文件)。除非在全局配置中通过 babelrcRoots 字段进行配置。 【全局配置】babel 全局配置文件所在的位置就决定了你的项目根目录在哪里,默认就是执行babel的当前工作目录,例如上面的例子,你在根目录执行babel,babel才能找到babel.config.js,从而确定该monorepo的根目录,进而将配置对整个项目生效 【相对配置】相对配置可被加载的前提是在 babel.config.js 中配置了 babelrcRoots. 如 babelrcRoots: [’.’, ‘./frontend’],这表示要对当前根目录和frontend这个子package开启 .babelrc 的加载。(注意: 项目根目录除了可以拥有一个 babel.config.js,同时也可以拥有一个 .babelrc 相对配置) 【相对配置】相对配置加载的边界是当前package的最顶层。假设上文案例中要编译 frontend/src/index.js 那么,该文件编译时可以加载 frontend 下的 .babelrc 配置,但无法向上检索总项目根目录下的 .babelrc 还是以上面的代码结构为例。 [代码]|- backend |-package.json |- frontend |-package.json |- node_modules |- config.js |- babel.config.js |- package.json 该案例中,我们思考发现,[代码]我们需要利用 babel7 的全局配置能力[代码]。原因在于,monrepo 中存在多个 子 package。由于 babel7 默认检索 babelrc 的边界是 当前package。因此每个package中撰写的babelrc只会对当前package生效,这会导致我们的frontend中依赖根目录的config.js时无法得到正确的编译;另一个问题是: frontend和backend中的相同的babel配置部分无法共享 存在一定冗余。为此,我们需要在项目根目录设置一个 babel.config.js的配置,用它再配合babelrc来做babel配置的共享和融合。 但是,问题很快来了:[代码]当工作目录不在根目录时,无法加载到全局配置[代码]。我们的前端编译脚本通常放置在 frontend目录下,(我们执行编译的工作目录是在 frontend 中),此时 babel build 行为的 工作目录 便是 frontend. 由于 babel 默认只在当前目录寻找 babel.config.js 这个全局配置,因此会导致无法找到根目录的 babel.config.js,这样我们所设想的整个项目的全局配置就无法生效。 幸好,babel7 提供了 rootMode 选项,可以将它指定为 “upward”, 这样babel 会自动向上寻找全局配置,并确定项目的根目录位置。 设置方法: CLI: babel --rootMode=upward webpack: 在 babel-loader 的配置上设置 rootMode: ‘upward’ 现在,全局配置有了,我们可以在里面配置 babel 转译规则,它可以对全项目生效,frontend下的 vue.js 编译自然没有问题了。 不过,假设我们 backend 项目中也要使用 babel 转译(目前我们实际在 backend 中并没有使用,因为我们认为只图esmodule而多加一层编译得不偿失),那么[代码]必然 backend 与 frontend 中的编译配置是不同的[代码],frontend 需要加载 vue 的 jsx 插件和polyfill (useBuiltIns: usage,modules: false),而backend只需要转译基本模块语法(modules: true, useBuiltIns: false)。该场景的解决方案便是,为每个子 package 提供独立的 .babelrc 相对配置,在全局 babel.config.js 中设置共用的配置。此时项目组织结构如下: [代码]|- backend |- .babelrc.js |-package.json |- frontend |- .babelrc.js |-package.json |- node_modules |- config.js |- .babelrc.js // 这份配置在本场景下不需要(如果根目录下的代码有区别于子package的babel配置,则需要使用) |- babel.config.js |- package.json 根目录的 babel.conig.js 配置应该如下: [代码]const presets = [ // 根、frontend、backend 的公共预设 const plugins = [ // 根、frontend、backend 的公共插件 module.exports = { presets, plugins, babelrcRoots: ['.', './frontend', './backend'] // 允许这两个子 package 加载 babelrc 相对配置 以为此时已经高枕无忧了?navie,由于我们前端 Vue.js 采用 webpack 打包。实际开发过程中发现,这种配置会造成 webpack 打包模块时出现故障,故障原因在于:[代码]同一个模块中错误混用 esmodule 和 commonjs 语法会造成 webpack故障[代码]。 前文讲到 全局配置 global.config.js 会作用到 [代码]整个项目[代码],甚至包括 node_modules。因此babel编译时会同时编译 node_modules 下的模块,虽然模块作者不可能在一个js文件中混用不同模块语法,但他们作为释出包 通常是commonjs的模块语法。 而preset-env预设在编译时会通过 [代码]usage[代码] 方式 默认注入import语法的 polyfill Since Babel defaults to treating files are ES modules, generally these plugins/presets will insert import statements 这便是蛋疼的来源:babel加载过的node_modules模块会变成 同一个js文件里既有commonjs语法又有esmodule语法。 解决方案:不要对 node_modules 下的模块采用babel编译。我们需要在 babel.config.js 配置中增加选项: [代码]exclude: /node_modules/ 至此,我们的 monorepo 项目就可以使用一份 全局配置+两份相对配置,实现分别对 前端和后端 进行合理的ES6+语法的编译了。这是我们配置工程师的一小步,但是前端走向未来语法的一大步。 总结 babel7 的配置加载逻辑如下: babel.config.js 是对整个项目(父子package) 都生效的配置,但要注意babel的执行工作目录。 .babelrc 是对 [代码]待编译文件[代码] 生效的配置,子package若想加载.babelrc是需要babel配置babelrcRoots才可以(父package自身的babelrc是默认可用的)。 任何package中的babelrc寻找策略是: 只会向上寻找到本包的 package.json 那一级。 node_modules下面的模块一般都是编译好的,请剔除掉对他们的编译。如有需要,可以把个例加到 babelrcRoots 中。 虽然写的很乱,但您有收获吗,有的话点个赞吧. 或许你还没有看明白。没关系,知道最终的配置代码怎么粘贴就好了~
0. 前言 平时写CSS,感觉有很多多余的代码或者不好实现的方法,于是有了预处理器的解决方案,主旨是write less &do more。其实原生css中,用上css变量也不差,加上bem命名规则只要嵌套不深也能和less、sass的嵌套媲美。在一些动画或者炫酷的特效中,不用js的话可能是用了css动画、svg的animation、过渡,复杂动画实现用了js的话可能用了canvas、直接修改style属性。用js的,然后有没有想过一个问题:“要是canvas那套放在dom上就爽了”。因为复杂的动画频繁操作了dom,违背了倒背如流的“性能优化之一:尽量少操作dom”的规矩,嘴上说着不要,手倒是很诚实地[代码]ele.style.prop = <newProp>[代码],可是要实现效果这又是无可奈何或者大大减小工作量的方法。 我们都知道,浏览器渲染的流程:解析html和css(parse),样式计算(style calculate),布局(layout),绘制(paint),合并(composite),修改了样式,改的环节越深代价越大。js改变样式,首先是操作dom,整个渲染流程马上重新走,可能走到样式计算到合并环节之间,代价大,性能差。然后痛点就来了,浏览器有没有能直接操作前面这些环节的方法呢而不是依靠js?有没有方法不用js操作dom改变style或者切换class来改变样式呢? 于是就有CSS Houdini了,它是W3C和那几个顶级公司的工程师组成的小组,让开发者可以通过新api操作CSS引擎,带来更多的自由度,让整个渲染流程都可以被开发者控制。上面的问题,不用js就可以实现曾经需要js的效果,而且只在渲染过程中,就已经按照开发者的代码渲染出结果,而不是渲染完成了再重新用js强行走一遍流程。 关于houdini最近动态可点击这里 上次CSS大会知道了有Houdini的存在,那时候只有cssom,layout和paint api。前几天突然发现,Animation api也有了,不得不说,以后很可能是Houdini遍地开花的时代,现在得进一步了解一下了。一句话:这是css in js到js in css的转变 1. CSS变量 如果你用less、sass只为了人家有变量和嵌套,那用原生css也是差不多的,因为原生css也有变量: 比如定义一个全局变量–color(css变量双横线开头) [代码]:root { --color: #f00; 使用的时候只要var一下 [代码].f{ color: var(--color); 我们的html: [代码]<div class="f">123</div> 于是,红色的123就出来了。 css变量还和js变量一样,有作用域的: [代码]:root { --color: #f00; --color: #aaa color: var(--color); .ft { color: var(--color); html: [代码] <div className="f"> <div className="ft">123</div> <div className=""> <div className="g">123</div> 于是,是什么效果你应该也很容易就猜出来了: css能搞变量的话,我们就可以做到修改一处牵动多处的变动。比如我们做一个像准星一样的四个方向用准线锁定鼠标位置的效果: 用css变量的话,比传统一个个元素设置style优雅多了: [代码]<div id="shadow"> <div class="x"></div> <div class="y"></div> <div class="x_"></div> <div class="y_"></div> [代码] :root{ --x: 0px; --y: 0px; body{ margin: 0 #shadow{ width: 50%; height: 600px; border: #000 1px solid; position: relative; margin: 0; .x, .y, .x_, .y_ { position: absolute; border: #f00 2px solid; top: 0; left: var(--x); height: 20px; width: 0; top: var(--y); left: 0; height: 0; width: 20px; .x_ { top: 600px; left: var(--x); height: 20px; width: 0; .y_ { top: var(--y); left: 100%; height: 0; width: 20px; [代码]const style = document.documentElement.style shadow.addEventListener('mousemove', e => { style.setProperty(`--x`, e.clientX + 'px') style.setProperty(`--y`, e.clientY + 'px') 那么,对于github的404页面这种内容和鼠标位置有关的页面,思路是不是一下子就出来了 2. CSS type OM 都有DOM了,那CSSOM也理所当然存在。我们平时改变css的时候,通常是直接修改style或者切换类,实际上就是操作DOM来间接操作CSSOM,而type om是一种把css的属性和值存在attributeStyleMap对象中,我们只要直接操作这个对象就可以做到之前的js改变css的操作。另外一个很重要的点,attributeStyleMap存的是css的数值而不是字符串,而且支持各种算数以及单位换算,比起操作字符串,性能明显更优。 接下来,基本脱离不了window下的CSS这个属性。在使用的时候,首先,我们可以采取渐进式的做法: [代码]if('CSS' in window){...}[代码] 2.1 单位 [代码]CSS.px(1); // 1px 返回的结果是:CSSUnitValue {value: 1, unit: "px"} CSS.number(0); // 0 比如top:0,也经常用到 CSS.rem(2); //2rem new CSSUnitValue(2, 'percent'); // 还可以用构造函数,这里的结果就是2% // 其他单位同理 2.2 数学运算 自己在控制台输入CSSMath,可以看见的提示,就是数学运算 [代码]new CSSMathSum(CSS.rem(10), CSS.px(-1)) // calc(10rem - 1px),要用new不然报错 new CSSMathMax(CSS.px(1),CSS.px(2)) // 顾名思义,就是较大值,单位不同也可以进行比较 2.3 怎么用 既然是新的东西,那就有它的使用规则。 获取值[代码]element.attributeStyleMap.get(attributeName)[代码],返回一个CSSUnitValue对象 设置值[代码]element.attributeStyleMap.set(attributeName, newValue)[代码],设置值,传入的值可以是css值字符串或者是CSSUnitValue对象 当然,第一次get是返回null的,因为你都没有set过。“那我还是要用一下getComputedStyle再set咯,这还不是和之前的差不多吗?” 实际上,有一个类似的方法:[代码]element.computedStyleMap[代码],返回的是CSSUnitValue对象,这就ok了。我们拿前面的第一部分CSS变量的代码测试一波 [代码]document.querySelector('.x').computedStyleMap().get('height') // CSSUnitValue {value: 20, unit: "px"} document.querySelector('.x').computedStyleMap().set('height', CSS.px(0)) // 不见了 3. paint API paint、animation、layout API都是以worker的形式工作,具体有几个步骤: 建立一个worker.js,比如我们想用paint API,先在这个js文件注册这个模块registerPaint(‘mypaint’, class),这个class是一个类下面具体讲到 在html引入CSS.paintWorklet.addModule(‘worker.js’) 在css中使用,background: paint(mypaint) 主要的逻辑,全都写在传入registerPaint的class里面。paint API很像canvas那套,实际上可以当作自己画一个img。既然是img,那么在所有的能用到图片url的地方都适合用paint API,比如我们来自己画一个有点炫酷的背景(满屏随机颜色方块)。有空的话可以想一下js怎么做,再对比一下paint API的方案。 [代码]// worker.js class RandomColorPainter { // 可以获取的css属性,先写在这里 // 我这里定义宽高和间隔,从css获取 static get inputProperties() { return ['--w', '--h', '--spacing']; * 绘制函数paint,最主要部分 * @param {PaintRenderingContext2D} ctx 类似canvas的ctx * @param {PaintSize} PaintSize 绘制范围大小(px) { width, height } * @param {StylePropertyMapReadOnly} props 前面inputProperties列举的属性,用get获取 paint(ctx, PaintSize, props) { const w = (props.get('--w') && +props.get('--w')[0].trim()) || 30; const h = (props.get('--h') && +props.get('--h')[0].trim()) || 30; const spacing = +props.get('--spacing')[0].trim() || 10; for (let x = 0; x < PaintSize.width / w; x++) { for (let y = 0; y < PaintSize.height / h; y++) { ctx.fillStyle = `#${Math.random().toString(16).slice(2, 8)}` ctx.beginPath(); ctx.rect(x * (w + spacing), y * (h + spacing), w, h); ctx.fill(); registerPaint('randomcolor', RandomColorPainter); 接着我们需要引入该worker: [代码]CSS && CSS.paintWorklet.addModule('worker.js');[代码] 最后我们在一个class为paint的div应用样式: [代码].paint{ background-image: paint(randomcolor); width: 100%; height: 600px; color: #000; --w: 50; --h: 50; --spacing: 10; 再想想用js+div,是不是要先动态生成n个,然后各种计算各种操作dom,想想就可怕。如果是canvas,这可是canvas背景,你又要在上面放一个div,而且还要定位一波。 注意: worker是没有window的,所以想搞动画的就不能内部消化了。不过可以靠外面的css变量,我们用js操作css变量可以解决,也比传统的方法优雅 4. 自定义属性 支持情况 点击这里查看 首先,看一下支持度,目前浏览器并没有完全稳定使用,所以需要跟着它的提示:[代码]Experimental Web Platform features” on chrome://flags[代码],在chrome地址栏输入[代码]chrome://flags[代码]再找到[代码]Experimental Web Platform features[代码]并开启。 [代码]CSS.registerProperty({ name: '--myprop', //属性名字 syntax: '<length>', // 什么类型的单位,这里是长度 initialValue: '1px', // 默认值 inherits: true // 会不会继承,true为继承父元素 说到继承,我们回到前面的css变量,已经说了变量是区分作用域的,其实父作用域定义变量,子元素使用该变量实际上是继承的作用。如果[代码]inherits: true[代码]那就是我们看见的作用域的效果,如果不是true则不会被父作用域影响,而且取默认值。 这个自定义属性,精辟在于,可以用永久循环的animation驱动一次性的transform。换句话说,我们如果用了css变量+transform,可以靠js改变这个变量达到花俏的效果。但是,现在不需要js,只要css内部消化,transform成为永动机。 [代码]// 我们先注册几种属性 ['x1','y1','z1','x2','y2','z2'].forEach(p => { CSS.registerProperty({ name: `--${p}`, syntax: '<angle>', inherits: false, initialValue: '0deg' 然后写个样式 [代码]#myprop, #myprop1 { width: 200px; border: 2px dashed #000; border-bottom: 10px solid #000; animation:myprop 3000ms alternate infinite ease-in-out; transform: rotateX(var(--x2)) rotateY(var(--y2)) rotateZ(var(--z2)) 再来看看我们的动画,为了眼花缭乱,加了第二个改了一点数据的动画 [代码]@keyframes myprop { 25% { --x1: 20deg; --y1: 30deg; --z1: 40deg; 50% { --x1: -20deg; --z1: -40deg; --y1: -30deg; 75% { --x2: -200deg; --y2: 130deg; --z2: -350deg; 100% { --x1: -200deg; --y1: 130deg; --z1: -350deg; @keyframes myprop1 { 25% { --x1: 20deg; --y1: 30deg; --z1: 40deg; 50% { --x2: -200deg; --y2: 130deg; --z2: -350deg; 75% { --x1: -20deg; --z1: -40deg; --y1: -30deg; 100% { --x1: -200deg; --y1: 130deg; --z1: -350deg; html就两个div: [代码] <div id="myprop"></div> <div id="myprop1"></div> 效果是什么呢,自己可以跑一遍看看,反正功能很强大,但是想象力限制了发挥。 自己动手改的时候注意一下,动画关键帧里面,不能只存在1,那样子就不能驱动transform了,做不到永动机的效果,因为我的rotate写的是 rotateX(var(–x2))。接下来随意发挥吧 再啰嗦一次 关于houdini最近动态可点击这里 关于houdini在浏览器的支持情况 ENJOY YOURSELF!!!
FlashEvent FlashEvent 小程序页面间的通信工具 - 类似于EventBus FlashEvent 在小程序中 能够简化各页面间的通信,让代码书写变得简单,能有效的解耦事件发送方和接收方,能避免复杂和容易出错的依赖性和生命周期问题。 github add: https://github.com/wuyajun7/FlashEvent 使用方式: 前置:将FlashEvent.js导入到项目的utils文件中 1、接收方js代码中 1.1 引入该类,如:let flashEvent = require(‘你的路径/utils/FlashEvent.js’); 1.2 注册FlashEvent,如:在onLoad中 [代码] flashEvent.register(flashEvent.EVENT_KEYS.FIRST_EVENT, this, function (data) { this.setData({ eventCallBack: data }) 1.3 注销FlashEvent,如:在onUnload中调用 flashEvent.unregister(flashEvent.EVENT_KEYS.FIRST_EVENT, this); 2、发送方js代码中 2.1 引入该类,如:let flashEvent = require(‘你的路径/utils/FlashEvent.js’); 2.2 发送事件,如:flashEvent.post(flashEvent.EVENT_KEYS.FIRST_EVENT, ‘发送的数据’); flashEvent 简单接入、方便使用
先给个图片,以前我在网上查过,问小程序如何去掉导航栏,网上一致回答说不能去掉,因为这是小程序的统一性,谁知过了不久开发文档里页面配置里就开放了这一项,手机本身版面就不大,这导航栏确实有点浪费,如今我用它做菜单,觉得很方便,如同地球有了一个空间站,我做的象棋棋谱就利用上了.把图发上来让大家看一下,不怕费时的就上小程序里搜象棋棋谱看一下,整个程序是用画布作的,走棋可以生成棋谱术语,很好玩.
分享微信小程序代码 如果有运营意向的欢迎联系我 资讯文章类小程序 玩法:阅读、点赞文章获得积分(组队满员后当天任务积分翻倍),积分用于在积分商城兑换现金会员等礼品。 获得礼品需要添加微信客服为好友,发送兑换码完成。 通过完成任务获得积分,积分在商城兑换成卷,添加客服微信好友后凭卷密令提现。 日常活动: 签到 10积分 组队 满员后日常奖励翻倍 推荐新用户 10积分/1人(每日最多可推荐100个) 推荐的新用户每日签到 3积分/1人(每日最多获得300次) 日常任务: 阅读文章(要拉到底部) 10篇 每篇1积分,共可获得10积分 点赞文章 5篇 每篇2积分,共可获得10积分 预览地址: 使用了以下开源项目进行开发 colorUI wemark 本项目供个人学习、参考,商用需获得本人授权(限免)。 开源仓库地址: https://github.com/yizenghui/wecont
运营:咱们最近需要拉新用户,做个简单点的活动,就老虎机形式吧。 产品:老虎机的话比较简单,网上demo那么多随便拷贝拷贝就能用啦。 开发:你来做。 进入正题: 网上老虎机的插件挺多的,实现原理也各不一样, 然后这里主要提下自己当初做老虎机抽奖活动时想到的一个原理: 划重点啦: css的 background-position属性是设置背景图像的起始位置,那么我们控制背景图在0-3秒的时间内显示不同的位置,再加上过渡动画就可以达到老虎机旋转的效果 第一个版本的在 这里 vue的版本 看下效果视频(第二个版本) markdown只能上传图片 - -然后我视频转的gif有十多兆,放这里有点卡。就上传视频了。 该版本定时4秒停再弹窗,比较突兀,未做到老虎机底部滚动停止后再显示弹窗。 今天要说的是第三种方案(实现底部滚动停止后显示弹窗且跟后端返回的中奖码一致) 直接上代码 [代码]<view class="box-container"> <view class="box-tips">{{boxTips}}</view> <view class="wheel-boxs"> <view class="box-list" wx:for="{{boxStatus}}" wx:key="index"> <view class="box-text" wx:if="{{!isStart}}">{{item}}</view> <view class="box-image" style="background: url('https://qiniu-image.qtshe.com/20181113wheels.png'); background-position-y: {{isStart ? ((16 - item) * 100) + 1500 + 'rpx' : 0}}; background-size: 100% 100%; transition-property: {{isStart ? 'all' : 'none'}}; transition-delay: {{(index + 1) * 100 + 'ms'}}; transition-duration: 3.5s;"> </view> {{item}} </view> </view> <view class="start-box"> <form bindsubmit="startDraw" report-submit="true" wx:if="{{pageVo.remainCount !== 0}}"> <button class="start-draw" formType="submit" /> </form> </view> <view class="last-tips">当前剩余 <text>{{pageVo.remainCount || 0}}</text> 次攒码机会</view> </view> [代码].box-container { width: 680rpx; height: 380rpx; background: url(https://qiniu-image.qtshe.com/20190227goddess_02.png) no-repeat center center; background-size: 100% 100%; position: relative; z-index: 10; margin: auto; overflow: hidden; .wheel-boxs { width: 680rpx; padding: 0 80rpx; margin-top: 16rpx; .box-list { width: 90rpx; height: 100rpx; background: url(https://qiniu-image.qtshe.com/20190227goddess_11.png) no-repeat center center; background-size: 100% 100%; display: inline-block; margin-right: 16rpx; overflow: hidden; .box-list:last-child { margin-right: 0; .box-tips { width: 500rpx; height: 54rpx; background: url(https://qiniu-image.qtshe.com/20190227goddess_10.png) no-repeat center center; overflow: hidden; background-size: 100% 100%; margin: 20rpx auto; color: #000; font-size: 24rpx; text-align: center; line-height: 54rpx; margin-top: 36rpx; .box-text { width: 100%; height: 100rpx; line-height: 100rpx; text-align: center; font-size: 44rpx; color: #f8294a; font-weight: 600; .box-image { height: 1500%; .start-box { width: 100%; text-align: center; margin: 16rpx 0 8rpx; .start-box button { width: 290rpx; height: 76rpx; background: url(https://qiniu-image.qtshe.com/20190227startDraw.png) no-repeat center center; background-size: 290rpx 76rpx; margin: 0 auto; .start-box .start-draw { width: 290rpx; height: 76rpx; background: url(https://qiniu-image.qtshe.com/20190227startDraw.png) no-repeat center center; background-size: 290rpx 76rpx; margin: 0 auto; [代码]const app = getApp() Page({ data: { isStart: false, //是否开始抽奖 isDialog: false, //是否显示中奖弹窗 dialogId: 1, //显示第几个中奖弹窗 boxTips: '本场女神码将在3月8日 19:00截止领取', //页面中部文案显示 typeTips: '3月8日20点开奖哦!', boxStatus: ['码', '上', '有', '红', '包'], //五个抽奖默认文案 results: [], //抽中的码 onLoad() { this.initData() //显隐藏中奖弹窗或规则弹窗等 handleModel() { this.setData({ isDialog: !this.data.isDialog onShow() {}, //初始化页面数据 initData() { let postData = { url: 'xxx' app.ajax(postData).then((res) => { if (res.success) { this.setData({ pageVo: res.data //页面所有数据 } else { util.toast( res.msg || '团团开小差啦,请稍后再试') }, () => { wx.hideLoading() util.toast( '团团开小差啦,请稍后再试') //收集FormId 发模版消息用 addFormId(e) { if (e.detail.formId !== 'the formId is a mock one') { //开发者工具上显示这段文案,过滤掉 let formData = { url: 'xxx', data: { formId: e.detail.formId, openId: wx.getStorageSync('openId') || '' app.ajax(formData) //开始抽奖 startDraw(e) { //这里可以做下节流 this.addFormId(e) //收集formId let postData = { url: 'xxx' app.ajax(postData).then((res) => { if (res.success) { this.setData({ isStart: true, results: res.data.result.split(','), //假如后端返回[1,2,3,4,5] dialogId: res.data.special ? 3 : 2 //3为彩蛋状态,2为普通状态 } else { util.toast(res.msg || '团团开小差啦,请稍后再试') }, () => { wx.hideLoading() util.toast( '团团开小差啦,请稍后再试') onShareAppMessage() { return { title: '码上有红包!点我瓜分10万女神节礼金!', path: '/activity/xxx/xxx', imageUrl: 'https://qiniu-image.qtshe.com/20190227goddess-share.png' 最后完整的实现效果在这里: 点我查看完整的视频效果 注意两个点: 旋转的背景图是雪碧图。我这里用到的是这张图可供参考 控制好图的位移单位,需要计算下,这样才可以让后端返回的值与你的图相匹配。我这里是15个icon所以计算方式如下 [代码]<view class="box-image" style=" background: url('https://qiniu-image.qtshe.com/20181113wheels.png'); background-position-y: {{isStart ? ((16 - item) * 100) + 1500 + 'rpx' : 0}}; background-size: 100% 100%; transition-property: {{isStart ? 'all' : 'none'}}; transition-delay: {{(index + 1) * 100 + 'ms'}}; transition-duration: 3.5s;"> </view> 这里可以封装成自定义组件,传入图片以及数量即可。后面如果有再用到 我会封装下再发出来。 最后说下弹窗显示的图的匹配方法,根据图片大小计算,比较麻烦: [代码] <view class="ci-wrapper"> <view wx:if="{{icontype ==='nomal'}}" class="icon-default icon-nomal" style=" background-position-y: {{(-24 - 117.86 * (typeId - 1)) + 'rpx'}};"> </view> <view wx:else class="icon-default icon-fade" style=" background-position-y: {{(-20 - 110.73 * (typeId - 1)) + 'rpx'}}; "> </view> </view> [代码].icon-default { width: 70rpx; height: 70rpx; background-repeat: no-repeat; .icon-nomal { background-image: url(https://qiniu-image.qtshe.com/20181113wheels.png); background-position-x: -17rpx; background-size: 100rpx 1768rpx .icon-fade { background-image: url(https://qiniu-image.qtshe.com/20181113wheels_fade.png); background-size: 110rpx 1661rpx; background-position-x: -18rpx; [代码]Component({ properties: { icontype: { type: String, value: "nomal" iconid: { type: Number, value: 1, observer(newVal, oldVal) { this.setData({ typeId: newVal }); data: { nomalOrigin: { x: -17, y: -24 fadeOrigin: { x: -17, y: -24 typeId: 1 至于引用的的地方嘛,就这样操作(resultList为中奖数字的数组): [代码]<code-icon wx:for="{{resultList}}" icontype="nomal" iconid="{{item}}" wx:key="{{index}}"></code-icon> 以上就是一个完整小程序的老虎机实现方案,有什么优化点大家可以指出来。 最后写了个代码片段:https://developers.weixin.qq.com/s/1k5eSnmh7z72
https://developers.weixin.qq.com/s/47VZSGmR7Q7w 这是代码片段链接 项目中有好多地方都需要用到 navbar ,一个项目中重复的使用同一段代码感觉很烦人,所以就自己写了一个,适合 2-4 个 tab,【支持多个】多个的话稍微修改一下布局就可以了 ━((′д`)爻(′д`))━!!!-图片传不上去 大家可以 打开代码链接看一下 如果感觉写的还凑合的 帮忙点个赞! 有什么可以改进的也可以 在下方评论
众所周知,图片等一些盒子都可以利用opacity属性来设置不透明度,但是前两天我朋友忽然给我一个截图,截图效果如下 图中红框圈住的位置图片或者说摄像头采集的画面出现了渐变到透明,可以清楚的看到可以看到后面小哥的胳膊,然后问我如何实现这种效果,这下把我难住了(呵 天天给我出难题),我开始在个大论坛开始寻找解决方案; 忽然在前天,日常逛论坛时看到一个文字投影的效果,而后忽然灵机一动就想,能不能变相的实现前两天我想要的那种效果,于是乎赶紧打开编辑器试了下,发现确实可以把我想要的图片或者盒子进行投影并给投影设置上渐变颜色及透明,结果出来了,只不过出来的效果他反了 随后利用transform: rotate(180deg);控制他使出倒挂金钩此等功夫,果然不负所望,成功翻转过来 但是我想要的只有投影,因为我想要效果目前只能用投影去实现去控制,但是他却本体与投影共同出现了,我不想看到本体,太丑了,怎么办呢,那就给他装个position: absolute; top给他爸爸装个position: relative; overflow: hidden;让他滚出~,结果显而易见,我胜利了; 我得到了我想要的结果,为了验证结果,我用文字放在他的下方 看看是否透明; 我真的成功了,哈哈(小开心一会儿),为了再次确认他真是的图片实现了渐变透明,我把渐变的透明度改成了1(也就是不透明) 事实证明,我真的成功了!!! 吹完牛皮,赶紧附上完成代码: html: 最终效果图: 呃…其实核心就是利用投影来完成的-webkit-box-reflect: below 0 linear-gradient(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 1) 100%); https://www.w3cschool.cn/css3/box-reflect.html 当然 肯定有大佬在我之前发现这种实现方式,不过当时我找了很久都没找到实现方式的写法,想了想 就发出来吧,如果有什么不对的地方,或者有其他方式也可以实现同等效果的话 还劳请告知,在下多谢各位大佬了!!!