- uniapp 公众号H5跳微信小程序,不生效?
import wx from 'weixin-js-sdk' wx.miniProgram.navigateTo({ url: `/pages/contactList/contactList?phone=${userInfo.mobile}&stuId=${userInfo.studentId}&tenantId=${studentInfo.tenantId}`, }); 以上写法打不开小程序
08-08 - 自定义navigationBar顶部导航栏,兼容适配所有机型(附完整案例)
前言 navigationBar相信大家都不陌生把?今天我们就来说说自定义navigationBar,把它改变成我们想要的样子(搜索框+胶囊、搜索框+返回按钮+胶囊等)。 思路 隐藏原生样式 获取胶囊按钮、状态栏相关数据以供后续计算 根据不同机型计算出该机型的导航栏高度,进行适配 编写新的导航栏 引用到页面 正文 一、隐藏原生的navigationBar window全局配置里有个参数:navigationStyle(导航栏样式),default=默认样式,custom=自定义样式。 [代码]"window": { "navigationStyle": "custom" } [代码] 让我们看看隐藏后的效果: [图片] 可以看到原生的navigationBar已经消失了,剩下孤零零的胶囊按钮,胶囊按钮是无法隐藏的。 二、准备工作 1.获取胶囊按钮的布局位置信息 我们用wx.getMenuButtonBoundingClientRect()【官方文档】获取胶囊按钮的布局位置信息,坐标信息以屏幕左上角为原点: [代码]const menuButtonInfo = wx.getMenuButtonBoundingClientRect(); [代码] width height top right bottom left 宽度 高度 上边界坐标 右边界坐标 下边界坐标 左边界坐标 下面是官方给的示意图,方便大家理解几个坐标。 [图片] 2.获取系统信息 用wx.getSystemInfoSync()【官方文档】获取系统信息,里面有个参数:statusBarHeight(状态栏高度),是我们后面计算整个导航栏的高度需要用到的。 [代码]const systemInfo = wx.getSystemInfoSync(); [代码] 三、计算公式 我们先要知道导航栏高度是怎么组成的, 计算公式:导航栏高度 = 状态栏高度 + 44。 实例 【源码下载】 自定义导航栏会应用到多个、甚至全部页面,所以封装成组件,方便调用;下面是我写的一个简单例子: app.js [代码]App({ onLaunch: function(options) { const that = this; // 获取系统信息 const systemInfo = wx.getSystemInfoSync(); // 胶囊按钮位置信息 const menuButtonInfo = wx.getMenuButtonBoundingClientRect(); // 导航栏高度 = 状态栏高度 + 44 that.globalData.navBarHeight = systemInfo.statusBarHeight + 44; that.globalData.menuRight = systemInfo.screenWidth - menuButtonInfo.right; that.globalData.menuBotton = menuButtonInfo.top - systemInfo.statusBarHeight; that.globalData.menuHeight = menuButtonInfo.height; }, // 数据都是根据当前机型进行计算,这样的方式兼容大部分机器 globalData: { navBarHeight: 0, // 导航栏高度 menuRight: 0, // 胶囊距右方间距(方保持左、右间距一致) menuBotton: 0, // 胶囊距底部间距(保持底部间距一致) menuHeight: 0, // 胶囊高度(自定义内容可与胶囊高度保证一致) } }) [代码] app.json [代码]{ "pages": [ "pages/index/index" ], "window": { "navigationStyle": "custom" }, "sitemapLocation": "sitemap.json" } [代码] 下面为组件代码: /components/navigation-bar/navigation-bar.wxml [代码]<!-- 自定义顶部栏 --> <view class="nav-bar" style="height:{{navBarHeight}}px;"> <input class="search" placeholder="输入关键词!" style="height:{{menuHeight}}px; min-height:{{menuHeight}}px; line-height:{menuHeight}}px; left:{{menuRight}}px; bottom:{{menuBotton}}px;"></input> </view> <!-- 内容区域: 自定义顶部栏用的fixed定位,会遮盖到下面内容,注意设置好间距 --> <view class="content" style="margin-top:{{navBarHeight}}px;"></view> [代码] /components/navigation-bar/navigation-bar.json [代码]{ "component": true } [代码] /components/navigation-bar/navigation-bar.js [代码]const app = getApp() Component({ properties: { // defaultData(父页面传递的数据-就是引用组件的页面) defaultData: { type: Object, value: { title: "我是默认标题" }, observer: function(newVal, oldVal) {} } }, data: { navBarHeight: app.globalData.navBarHeight, menuRight: app.globalData.menuRight, menuBotton: app.globalData.menuBotton, menuHeight: app.globalData.menuHeight, }, attached: function() {}, methods: {} }) [代码] /components/navigation-bar/navigation-bar.wxss [代码].nav-bar{ position: fixed; width: 100%; top: 0; color: #fff; background: #000;} .nav-bar .search{ width: 60%; color: #333; font-size: 14px; background: #fff; position: absolute; border-radius: 50px; background: #ddd; padding-left: 14px;} [代码] 以下是调用页面的代码,也就是引用组件的页面: /pages/index/index.wxml [代码]<navigation-bar default-data="{{defaultData}}"></navigation-bar> [代码] /pages/index/index.json [代码]{ "usingComponents": { "navigation-bar": "/components/navigation-bar/navigation-bar" } } [代码] /pages/index/index.js [代码]const app = getApp(); Page({ data: { // 组件参数设置,传递到组件 defaultData: { title: "我的主页", // 导航栏标题 } }, onLoad() { console.log(this.data.height) } }) [代码] 效果图: [图片] 好了,以上就是全部代码了,大家可以文中复制代码,也可以【下载源码】,直接到开发者工具里运行,记得appid用自己的或者测试哦! 下面附几张其它小程序的效果图,大家也可以尝试照着做: [图片][图片] 总结 本文写了自定义navigationBar的一些基础性东西,里面涉及组件用法、参数传递、导航栏相关。 由于测试环境有限,大家在使用时如果发现有什么问题,希望及时反馈,以供及时更新帮助更多的人! 大家有什么疑问,欢迎评论区留言!
2022-06-23 - Skyline|小程序吸顶、网格、瀑布流布局都拿下~
在之前的文章中,我们知道了新 scroll-view 可以让小程序的长列表做到丝滑滚动~ 也提到了新 scroll-view 提供了很多新能力 sticky、网格布局、瀑布流布局等,这一篇,我们就来看看这些新能力是怎么使用的~ 新 scroll-view 在原来列表模式(type="list")的基础上,新增了自定义模式(type="custom") 在自定义模式下,新增了以下新组件供开发者调用 list-view:列表布局容器sticky-section / sticky-header:吸顶布局容器grid-view:网格布局容器,可实现网格布局、瀑布流布局等sticky布局sticky 布局即在应用中常见的吸顶布局,与 CSS 中的 position: sticky 实现的效果一致,当组件在屏幕范围内时,会按照正常的布局排列,当组件滚出屏幕范围时,始终会固定在屏幕顶部。 常见的使用场景有:通讯录、账单列表、菜单列表等等。 与 position: sticky 不同的是,position: sticky 很难实现列表滚动需要的交错吸顶效果,而 sticky 组件则可以帮忙开发者轻松实现交错吸顶的效果。 sticky 的使用非常简单: 将 scroll-view 切换到 custom 模式采用 sticky-section 作为 scroll-view 的子元素sticky-header 放置吸顶内容list-view 放置列表内容 {{item.name}} ... 我们来看下采用 sticky 布局做出来的通讯录效果~ [视频] sticky 布局也可以通过给 sticky-section 配置 push-pinned-header 来声明吸顶元素重叠时是否继续上推 像下图输入框和标签列表这种类型,标签列表吸顶时还是希望保留输入框吸顶。 [视频] 网格布局网格布局即将列表切割成格子,每一行的高度固定,常见的视频列表、照片列表等通常都采用网格布局。 在此之前,实现网格布局需要开发者自行实现网格切割,再嵌入到 scroll-view 中。 新 scroll-view 直接提供了 grid-view 组件供开发者使用~ 将 scroll-view 切换到 custom 模式采用 grid-view 类型为 aligned 做为直接子节点grid-view 中直接编写列表 ... 下面是使用网格布局实现的图片列表效果~ [视频] 瀑布流布局瀑布流布局与网格布局类似,不同的是瀑布流布局中每个格子的高度都可以是不一致的,所以在小程序中实现瀑布流布局就比较复杂了。 开发者需要通过计算格子高度,然后再进行瀑布流拼接,当滚动内容过多时还需要处理节点过多导致内存不足等问题。 grid-view 组件直接支持了瀑布流模式供开发者直接使用,grid-view 组件会根据子元素高度自动布局: 将 scroll-view 切换到 custom 模式采用 grid-view 类型为 masonry 做为直接子节点grid-view 中直接编写列表 ... 下面是使用瀑布流布局实现的图片列表效果~ [视频] 想要立即体验?现在通过微信开发者工具导入 代码片段,即可体验新版 scroll-view 组件能力~
2023-08-03 - 针对新手很容易出现理解误区的微信小程序订阅消息模块
1. 写在前面 微信小程序下架了模板消息功能,取而代之的是订阅消息功能。这个订阅消息目前又分为「一次性订阅」和「永久订阅」。使用订阅消息也有一段时间了,感觉对新手订阅消息很容易让新开发者进入一个理解的误区,这里觉得有必要说出来 2. 理解误区 很多新手认为,只要用户勾选了小程序端订阅消息弹出时底部的「总是保持以上选择…」后,就可以「为所欲为」的不限次数的推送订阅消息给用户了。如下图: [图片] 3. 正确理解 如果你使用的「一次性订阅」模板(目前发现绝大多数开发者都是只能用一次性的,因为永久性的订阅消息申请门槛太高),那么勾选底部的「总是…」这个并不代表以后可以直接推送了。官方原话wx.requestSubscribeMessage的介绍里是这样写的: 3.1 官方说明 wx.requestSubscribeMessage(Object object) 基础库 2.8.2 开始支持,低版本需做兼容处理。 调起客户端小程序订阅消息界面,返回用户订阅消息的操作结果。当用户勾选了订阅面板中的“总是保持以上选择,不再询问”时,模板消息会被添加到用户的小程序设置页,通过 wx.getSetting 接口可获取用户对相关模板消息的订阅状态。 注意事项 一次性模板 id 和永久模板 id 不可同时使用。 低版本基础库2.4.4~2.8.3 已支持订阅消息接口调用,仅支持传入一个一次性 tmplId / 永久 tmplId。 2.8.2 版本开始,用户发生点击行为或者发起支付回调后,才可以调起订阅消息界面。 2.10.0 版本开始,开发版和体验版小程序将禁止使用模板消息 fomrId。 3.2 重点关注 这里重点关注第7条:「用户发生点击行为或者发起支付回调后,才可以调起订阅消息界面。」这就意味着你需要在用户主动点击某个组件是触发调用wx.requestSubscribeMessage方法再次订阅,订阅后,你才可以「为所欲为」推送一次模板消息,注意只能一次。下次再想推送时,需要用户再次点击触发wx.requestSubscribeMessage。 4. 破局方案 目前订阅消息功能,就是这么个情况,所以针对这个情况的替代方案有以下 4.1 永久性订阅消息 如果能达到申请「永久性订阅」消息的模板的门槛,那自然是极好的,直接用永久性模板「为所欲为」。 4.2 使用服务号的模板消息替代 比较常用的是使用公众号服务号的模板消息代替小程序的订阅消息功能,公众号的模板消息功能限制就比订阅号好多了,基本上可以「为所欲为」的推送。但是这个方案有个致命的运营成本:必须要用户关注公众号,还有小程序要跟公众号同一主体并绑定在开放平台下。同时开发成本有所增加,要采用unionId机制来打通小程序跟公众号的openId。这个具体的实现方案,大家有兴趣的话可以讨论下。笔者目前就是用这种方案的。 5. 几个注意点 5.1 官方提示 订阅消息如果选择选择‘总是保持以上选择,"不再询问"后的设置问题: 目前是选择‘总是保持以上选择,"不再询问"后,可以在设置中开启或拒绝接收,但不会再次拉起授权弹窗 6. 长期性订阅消息 请参考官方最新文档: 小程序模板消息能力调整通知 | 微信开放社区 https://developers.weixin.qq.com/community/develop/doc/00008a8a7d8310b6bf4975b635a401 长期性订阅消息 一次性订阅消息可满足小程序的大部分服务场景需求,但线下公共服务领域存在一次性订阅无法满足的场景,如航班延误,需根据航班实时动态来多次发送消息提醒。为便于服务,我们提供了长期性订阅消息,用户订阅一次后,开发者可长期下发多条消息。 目前长期性订阅消息仅向政务民生、医疗、交通、金融、教育等线下公共服务开放,后期将逐步支持到其他线下公共服务业务。 7.题外话 鉴于被戴上各种「刷赞,冲级,让社区点赞“通货膨胀”」等等一些恶毒字眼(最近多了个职业回复的「雅称」),各种帽子戴得,做一个开发爱好者积极分享和解决各种问题太难了,姑且不论咱写一篇文章需要截图多少,单单排版就得废掉俺多少时间哈,很受伤,所以本人决定在微信开放者社区封笔。你看到是俺最后一篇发表在微信开放社区的文章。如果你想继续查看俺的一些文章可以私聊我。我会在其他平台保持继续创作。bye-bye~ 8. 最最重要的来了 看完后觉得有用记得点赞~~ ↓点赞处↓
2020-09-04 - (二) js中的继承
继承 [代码]OO语言分为接口继承和实现继承,ECMAScript支持实现继承,主要是依靠原型链来实现的 [代码] 1 原型链 [代码]利用原型,让一个引用类型继承令一个引用类型的属性和方法 实现原型链的一种模式: [代码] [代码] function SuperType(){ this.property = true; } SuperType.prototype.getSuperValue = function(){ return this.property; } function SubType(){ this.subProperty = false; } SubType.prototype = new SuperType(); let instance = new SubType(); console.log(instance.getSuperValue()) // true [代码] 1.1 别忘记默认的原型 Object 1.2 确定原型和实例的关系 [代码] 只要是原型链中出现的原型 都可以说是该原型链派生的实例的原型 [代码] [代码] instanceOf console.log(instance instanceof Object) //true console.log(instance instanceof SuperType) // true console.log(instance instanceof SubType) // true isPropertytypeOf console.log(Object.prototype.isPrototypeOf(instance)) //true console.log(SubType.prototype.isPrototypeOf(instance)) //true console.log(SuperType.prototype.isPrototypeOf(instance)) //true [代码] 1.3 谨慎的定义方法 [代码] 给原型添加的方法一定要放在替换原型的语句之后 注意尽量不要重写原型 如: Person.prototype = { } // 这样会重写原型 谨慎操作 [代码] 1.4 原型链的问题 [代码]1 由于 包含引用类型值的原型属性会被所有实例共享 用代码说明问题 [代码] [代码] function SuperType(){ this.colors = ['red' , 'yellow'] } function SubType(){ } SubType.prototype = new SuperType(); let instance1 = new SubType(); instance1.colors.push('black'); console.log(instance1.colors) // ["red", "yellow", "black"] let instance2 = new SubType(); console.log(instance2.colors) // ["red", "yellow", "black"] // 两个实例公用一个colors了 [代码] [代码]2 在创建子类型时的实例时,没法在不影响所有对象实例的情况下 向超类型的构造函数中传递参数 所以,实践中很少会单独使用原型链 [代码] 2 借用构造函数 [代码]即在子类型构造函数内部调用超类型构造函数 call apply 为了解决原型中包含引用类型值的问题 [代码] [代码] function SuperType(colors){ this.colors = colors; } function SubType(colors){ SuperType.call(this, colors); this.age = 10; } let instance1 = new SubType(['1' , '2']); console.log(instance1.colors) [代码] [代码]问题:方法都在构造函数中定义,所以函数复用也无从谈起 且超类型中定义的方法,对子类型而言也是不可见的,结果所有类型只能使用构造函数模式 [代码] 3 组合继承 [代码]将原型链与组合继承结合的一种集成方法 使用原型链实现对原型属性和方法的继承,通过构造函数实现对实例属性的继承 [代码] [代码] // 组合继承 // 使用原型链实现原型属性和方法的继承 使用构造函数实现实例属性的继承 function SuperType(name){ this.name = name; this.colors = [1,2,3] } SuperType.prototype.sayName = function(){ console.log(this.name) } function SubType(name , age){ this.age = age; SuperType.call(this , name) } // 继承方法 SubType.prototype = new SuperType(); SubType.prototype.sayAge = function(){ console.log(this.age) } let a = new SubType('a' , 10) a.colors.push(4) console.log(a.colors) a.sayName() a.sayAge(); let b = new SubType('b' , 10) console.log(b.colors) b.sayName() b.sayAge(); [代码] 4 原型式继承 [代码]借助原型可以基于已有的对象创建新对象,同时还不必创建自定义类型 这种原型式继承,必须有一个对象作为另一个对象的基础,把它传进object()函数,然后根据需求对得到的对象加以修改 [代码] [代码] function object(o){ function F(){} F.prototype = o; return new F(); } // 例子 let person = { name: 'nicolas', friends: ['jobs'] } let person1 = object(person); person1.name = 'p1'; // person1.friends.push('p1fw') person1.friends = [1,2,3] let person2 = object(person); person2.name = 'p2'; person2.friends.push('p2fw') console.log(person) console.log(person1) console.log(person2) [代码] [代码]不过,ES5通过新增Object.create()方法规范了原型式继承,这个方法接受两个参数,一个用作新对象原型的对象和(可选择的)一个为新对象定义额外属性的对象 在传入一个参数的情况下,Object.create()作用与上面object()作用相同 Object.create()第二个参数与Object.defineProperties()方法第二个参数格式相同 [代码] [代码] let person = { name: 'jobs', friends: ['f'] } let person1 = Object.create(person , { name:{ value: 'jack' }, friends:{ value: ['d'] } }) console.log(person1) [代码] [代码]在没有必要创建构造函数,而只想让一个对象与另一个对象类似的情况下,原型式继承是完全可以胜任的,但要注意包含引用类型值的属性始终都会共享相应的值,就像使用原型模式一样。 [代码] 5. 寄生式继承 [代码]创建一个仅用于封装继承过程的函数,该函数在内部以某种方式来增强对象,然后返回一个对象 [代码] [代码] function object(o){ function F(){} F.prototype = o; return new F(); } function createAnother(origin){ let clone = object(origin); // 通过调用函数创建一个新对象 clone.sayHi = function(){ // 增强这个新对象 console.log('hi') } return clone; // 返回创建的对象 } let person = { name: 'a' } let anotherPerson = createAnother(person); anotherPerson.sayHi() [代码] [代码]使用寄生式继承来为对象添加函数,会由于不能做到函数复用而降低效率,这一点与构造函数模式类似 [代码] 6 寄生组合式继承 [代码]前面说过,组合式继承是最常用的继承方式,不过组合继承最大的问题是无论什么情况下,都会调用两次超类型的构造函数 [代码] [代码] // 这是组合继承 function SuperType(name){ this.name = name } SuperType.prototype.sayName = function(){ console.log(this.name) } function SubType(name , age ){ SuperType.call(this , name); // 第二次调用SuperType() this.age = age } SubType.prototype = new SuperType() // 第一次调用SuperType() // 两次调用SuperType()的后果是 SubType()里会有两个name属性,一个在实例上,一个在SubType()原型上 SubType.prototype.constructor = SubType; SubType.prototype.sayAge = function(){ console.log(this.age) } let sub = new SubType('wu' , 20); console.log(sub) sub.sayAge() sub.sayName() // 所以用寄生组合式继承解决这个问题 // 即 借用构造函数来继承属性,用原型链来继承方法 function SuperType(name){ this.name = name; } SuperType.prototype.sayName = function(){ console.log(this.name) } function SubType(name , age){ this.age = age; SuperType.call(this, name) } // inheritPrototype()函数实现了寄生组合式继承的最简单形式 function inheritPrototype(SubType , SuperType){ // 第一步 创建对象 创建超类型原型的一个副本 let prototype = Object.create(SuperType.prototype); // 第二步 为创建的副本添加constructor 属性,弥补因重写原型原型而失去默认的constructor属性 prototype.constructor = SubType; // 第三步 将创建的对象赋值给子类型的原型 SubType.prototype = prototype; } inheritPrototype(SubType , SuperType); SubType.prototype.sayAge = function(){ console.log(this.age) } let sub = new SubType('wang' , 33); console.log(sub) sub.sayName() sub.sayAge() ``[代码]
2020-05-25 - 自定义tabbar 【恋爱小清单开发总结】
看官方demo的小伙伴知道,自定义tabbar需要在小程序根目录底下建一个名叫custom-tab-bar的组件(我有试过,如果放在components目录里面小程序会识别不了),目前我自己实现的效果是:通过在配置可以切换tab,也可以点击tab后重定向到新页面,支持隐藏tabbar,同时也可以显示右上角文本和小红点。 官方demo里面用的是cover-view,我改成view,因为如果页面有弹窗的话我希望可以盖住tabbar 总结一下有以下注意点: 1、tabbar组件的目录命名需要是custom-tab-bar 2、app.json增加自定义tabbar配置 3、wx.navigateTo不允许跳转到tabb页面 4、进入tab页面时,需要调用tabbar.js手动切换tab 效果图: [图片] 可以扫码体验 [图片] 代码目录如下: [图片] 代码如下: app.json增加自定义tabbar配置 "tabBar": { "custom": true, "color": "#7A7E83", "selectedColor": "#3cc51f", "borderStyle": "black", "backgroundColor": "#ffffff", "list": [ { "pagePath": "pages/love/love", "text": "首页" }, { "pagePath": "pages/tabbar/empty", "text": "礼物说" }, { "pagePath": "pages/tabbar/empty", "text": "恋人圈" }, { "pagePath": "pages/me/me", "text": "我" } ] }, 自定义tabbar组件代码如下 index.js //api.js是我自己对微信接口的一些封装 const api = require('../utils/api.js'); //获取应用实例 const app = getApp(); Component({ data: { isPhoneX: false, selected: 0, hide: false, list: [{ showRedDot: false, showBadge: false, badgeText: "", pagePath: "/pages/love/love", iconPath: "/images/tabbar/home.png", selectedIconPath: "/images/tabbar/home-select.png", text: "首页" }, { showRedDot: false, showBadge: false, badgeText: "", pagePath: "/pages/tabbar/empty", navigatePath: "/pages/gifts/giftList", iconPath: "/images/tabbar/gift.png", selectedIconPath: "/images/tabbar/gift-select.png", text: "礼物说", hideTabBar: true }, { showRedDot: false, showBadge: false, badgeText: "", pagePath: "/pages/tabbar/empty", navigatePath: "/pages/moments/moments", iconPath: "/images/tabbar/lover-circle.png", selectedIconPath: "/images/tabbar/lover-circle-select.png", text: "恋人圈", hideTabBar: true }, { showRedDot: false, showBadge: false, badgeText: "", pagePath: "/pages/me/me", iconPath: "/images/tabbar/me.png", selectedIconPath: "/images/tabbar/me-select.png", text: "我" }] }, ready() { // console.error("custom-tab-bar ready"); this.setData({ isPhoneX: app.globalData.device.isPhoneX }) }, methods: { switchTab(e) { const data = e.currentTarget.dataset; console.log("tabBar参数:", data); api.vibrateShort(); if (data.hideTabBar) { api.navigateTo(data.navigatePath); } else { /*this.setData({ selected: data.index }, function () { wx.switchTab({url: data.path}); });*/ /** * 改为直接跳转页面, * 因为发现如果先设置selected的话, * 对应tab图标会先选中,然后页面再跳转, * 会出现图标变成未选中然后马上选中的过程 */ wx.switchTab({url: data.path}); } }, /** * 显示tabbar * @param e */ showTab(e){ this.setData({ hide: false }, function () { console.log("showTab执行完毕"); }); }, /** * 隐藏tabbar * @param e */ hideTab(e){ this.setData({ hide: true }, function () { console.log("hideTab执行完毕"); }); }, /** * 显示小红点 * @param index */ showRedDot(index, success, fail) { try { const list = this.data.list; list[index].showRedDot = true; this.setData({ list }, function () { typeof success == 'function' && success(); }) } catch (e) { typeof fail == 'function' && fail(); } }, /** * 隐藏小红点 * @param index */ hideRedDot(index, success, fail) { try { const list = this.data.list; list[index].showRedDot = false; this.setData({ list }, function () { typeof success == 'function' && success(); }) } catch (e) { typeof fail == 'function' && fail(); } }, /** * 显示tab右上角文本 * @param index * @param text */ showBadge(index, text, success, fail) { try { const list = this.data.list; Object.assign(list[index], {showBadge: true, badgeText: text}); this.setData({ list }, function () { typeof success == 'function' && success(); }) } catch (e) { typeof fail == 'function' && fail(); } }, /** * 隐藏tab右上角文本 * @param index */ hideBadge(index, success, fail) { try { const list = this.data.list; Object.assign(list[index], {showBadge: false, badgeText: ""}); this.setData({ list }, function () { typeof success == 'function' && success(); }) } catch (e) { typeof fail == 'function' && fail(); } } } }); index.html <view class="footer-tool-bar flex-center {{isPhoneX? 'phx_68':''}}" hidden="{{hide}}"> <view class="tab flex-full {{selected === index ? 'focus':''}}" wx:for="{{list}}" wx:key="index" data-path="{{item.pagePath}}" data-index="{{index}}" data-navigate-path="{{item.navigatePath}}" data-hide-tab-bar="{{item.hideTabBar}}" data-open-ext-mini-program="{{item.openExtMiniProgram}}" data-ext-mini-program-app-id="{{item.extMiniProgramAppId}}" bindtap="switchTab"> <view class="text"> <view class="dot" wx:if="{{item.showRedDot}}"></view> <view class="badge" wx:if="{{item.showBadge}}">{{item.badgeText}}</view> <image class="icon" src="{{item.selectedIconPath}}" hidden="{{selected !== index}}"></image> <image class="icon" src="{{item.iconPath}}" hidden="{{selected === index}}"></image> </view> </view> </view> index.json { "component": true, "usingComponents": {} } index.wxss @import "/app.wxss"; .footer-tool-bar{ background-color: #fff; height: 100rpx; width: 100%; position: fixed; bottom: 0; z-index: 100; text-align: center; font-size: 24rpx; transition: transform .3s; border-radius: 30rpx 30rpx 0 0; /*padding-bottom: env(safe-area-inset-bottom);*/ box-shadow:0rpx 0rpx 18rpx 8rpx rgba(212, 210, 211, 0.35); } .footer-tool-bar .tab{ color: #242424; height: 100%; line-height: 100rpx; } .footer-tool-bar .focus{ color: #f96e49; font-weight: 500; } .footer-tool-bar .icon{ width: 44rpx; height: 44rpx; margin: 18rpx auto; } .footer-tool-bar .text{ line-height: 80rpx; height: 80rpx; position: relative; display: inline-block; padding: 0rpx 40rpx; box-sizing: border-box; margin: 10rpx auto; } .footer-tool-bar .dot{ position: absolute; top: 16rpx; right: 16rpx; height: 16rpx; width: 16rpx; border-radius: 50%; background-color: #f45551; } .footer-tool-bar .badge{ position: absolute; top: 8rpx; right: 8rpx; height: 30rpx; width: 30rpx; line-height: 30rpx; border-radius: 50%; background-color: #f45551; color: #fff; text-align: center; font-size: 20rpx; font-weight: 450; } .hide{ transform: translateY(100%); } app.wxss(这里的样式文件是我用来存放一些公共样式) /**app.wxss**/ page { background-color: #f5f5f5; height: 100%; -webkit-overflow-scrolling: touch; } .container { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: space-between; box-sizing: border-box; } .blur { filter: blur(80rpx); opacity: 0.65; } .flex-center { display: flex; align-items: center; justify-content: center; } .flex-column { display: flex; /*垂直居中*/ align-items: center; /*水平居中*/ justify-content: center; flex-direction: column; } .flex-start-horizontal{ display: flex; justify-content: flex-start; } .flex-end-horizontal{ display: flex; justify-content: flex-end; } .flex-start-vertical{ display: flex; align-items: flex-start; } .flex-end-vertical{ display: flex; align-items: flex-end; } .flex-wrap { display: flex; flex-wrap: wrap; } .flex-full { flex: 1; } .reset-btn:after { border: none; } .reset-btn { background-color: #ffffff; border-radius: 0; margin: 0; padding: 0; overflow: auto; } .loading{ opacity: 0; transition: opacity 1s; } .load-over{ opacity: 1; } .phx_68{ padding-bottom: 68rpx; } .phx_34{ padding-bottom: 34rpx; } 另外我还对tabbar的操作做了简单的封装: tabbar.js const api = require('/api.js'); /** * 切换tab * @param me * @param index */ const switchTab = function (me, index) { if (typeof me.getTabBar === 'function' && me.getTabBar()) { console.log("切换tab:", index); me.getTabBar().setData({ selected: index }) } }; /** * 显示 tabBar 某一项的右上角的红点 * @param me * @param index */ const showRedDot = function (me, index, success, fail) { if (typeof me.getTabBar === 'function' && me.getTabBar()) { me.getTabBar().showRedDot(index, success, fail); } }; /** * 隐藏 tabBar 某一项的右上角的红点 * @param me * @param index */ const hideRedDot = function (me, index, success, fail) { if (typeof me.getTabBar === 'function' && me.getTabBar()) { me.getTabBar().hideRedDot(index, success, fail); } }; /** * 显示tab右上角文本 * @param me * @param index * @param text */ const showBadge = function (me, index, text, success, fail) { if (typeof me.getTabBar === 'function' && me.getTabBar()) { me.getTabBar().showBadge(index, text, success, fail); } }; /** * 隐藏tab右上角文本 * @param me * @param index */ const hideBadge = function (me, index, success, fail) { if (typeof me.getTabBar === 'function' && me.getTabBar()) { me.getTabBar().hideBadge(index, success, fail); } }; /** * 显示tabbar * @param me * @param success */ const showTab = function(me, success){ if (typeof me.getTabBar === 'function' && me.getTabBar()) { me.getTabBar().showTab(success); } }; /** * 隐藏tabbar * @param me * @param success */ const hideTab = function(me, success){ if (typeof me.getTabBar === 'function' && me.getTabBar()) { me.getTabBar().hideTab(success); } }; module.exports = { switchTab, showRedDot, hideRedDot, showBadge, hideBadge, showTab, hideTab }; 最后,进入到tab对应页面的时候要手动调用一下swichTab接口,然tabbar聚焦到当前tab /** * 生命周期函数--监听页面显示 */ onShow: function () { tabbar.switchTab(this, this.data.tabIndex);//tabIndex是当前tab的索引 }
2021-11-09 - webview嵌套的H5页面如何跳转到另外一个H5页面?
webview嵌套的H5页面如何跳转到另外一个H5页面?请问谁能告诉一下
2020-03-31 - 浅谈前端响应式设计(二)
上一篇文章中提到了几种响应式的方案,以及它们的缺点。本文将介绍Observable以及它的一个实现,以及它在处理响应式时相对于上篇博客中的方案的巨大优势(推荐两篇文章对比阅读)。 Observable是一个集合了观察者模式、迭代器模式和函数式的库,提供了基于事件流的强大的异步处理能力,并且已在Stage 1草案中。本文介绍的Rxjs是Observable的一个实现,它是ReactiveX众多语言中的JavaScript版本。 在JavaScript中,我们可以使用T | null去处理一个单值,使用Iterator去处理多个值得情况,使用Promise处理异步的单个值,而Observable则填补了缺失的“异步多个值”。 [图片] 使用Rxjs 上文提到使用Event Emitter做响应式处理,在Rxjs中稍有些不同: [代码]/* const change$ = new Subject(); <Input change$={change$} /> <Search change$={change$} /> */ class Input extends Component { state = { value: '' }; onChange = e => { this.props.change$.next(e.target.value); }; componentDidMount() { this.subscription = this.props.change$.subscribe(value => { this.setState({ value }); }); } componentWillUnmount() { this.subscription.ubsubscribe(); } render() { const { value } = this.state; return <input value={value} onChange={this.onChange} />; } } class Search extends Component { // ... componentDidMount() { this.subscription = this.props.change$.subscribe(value => { ajax(/* ... */).then(list => this.setState({ list }) ); }); } componentWillUnmount() { this.subscription.ubsubscribe(); } render() { const { list } = this.state; return <ul>{list.map(item => <li key={item.id}>{item.value}</li>)}</ul>; } } [代码] 在这里,我们虽然也需要手动释放对事件的订阅,但是得益于Rxjs的设计,我们不需要像Event Emitter那样去存下回调函数的实例,用于释放订阅,因此我们很容易就可以通过高阶组件解决这个问题。例如: [代码]const withObservables = observables => ChildComponent => { return class extends Component { constructor(props) { super(props); this.subscriptions = {}; this.state = {}; Object.keys(observables).forEach(key => { this.subscriptions[key] = observables[key].subscribe(value => { this.setState({ [key]: value }); }); }); } onNext = (key, value) => { observables[key].next(value); }; componentWillUnmount() { Object.keys(this.subscriptions).forEach(key => { this.subscriptions[key].unsubscribe(); }); } render() { return ( <ChildComponent {...this.props} {...this.state} onNext={this.onNext} /> ); } }; }; [代码] 这样在需要聚合多个数据源时,也不会像Event Emitter那样手动释放资源造成麻烦。同时,在Rxjs中我们还有专用于聚合数据源的方法: [代码]Observable.combineLatest(foo$, bar$) .pipe( // ... ); [代码] 显然相对于Event Emitter的方式十分高效,同时它相对于Mobx也有巨大的优势。在Mobx中,我们提到需要聚合多个数据源的时候,采用autoRun的方式容易收集到不必要的依赖,使用observe则不够高效。在Rxjs中,显然不会有这些问题,combineLatest可以以很简练的方式声明需要聚合的数据源,同时,得益于Rxjs设计,我们不需要像Mobx一个一个去调用observe返回的析构,只需要处理每一个subscribe返回的subscription: [代码]class Foo extends Component { constructor(props) { super(props); this.subscription = Observable.combineLatest(foo$, bar$) .pipe( // ... ) .subscribe(() => { // ... }); } componentWillUnmount() { this.subscription.unsubscribe(); } } [代码] 异步处理 Rxjs使用操作符去描述各种行为,每一个操作符会返回一个新的Observable,我们可以对它进行后续的操作。例如,使用map操作符就可以实现对数据的转换: [代码]foo$.map(event => event.target.value); [代码] Rxjs 5.5之后所有的Observable上都引入了一个pipe方法,接收若干个操作符,pipe方法会返回一个Observable。因此,我们可以很容易配合tree shaking实现对操作符的按需引入,而不是把整个Rxjs引入进来: [代码]import { map } from 'rxjs/operators'; foo$.pipe(map(event => event.target.value)); [代码] 推荐使用这种写法。 在讨论面向对象的响应式的响应式中,我们提到对于异步的问题,面向对象的方式不好处理。在Observable中我们可以通过switchMap操作符处理异步问题,一个异步搜索看起来会是这样: [代码]input$.pipe(switchMap(keyword => Observable.ajax(/* ... */))); [代码] 在处理异步单值时,我们可以使用Promise,而Observable用于处理异步多个值,我们可以很容易把一个Promise转成一个Observable,从而复用已有的异步代码: [代码]input$.pipe(switchMap(keyword => fromPromise(search(/* ... */)))); [代码] switchMap接受一个返回Observable的函数作为参数,下游的流就会切到这个返回的Observable。 而要聚合多个数据源并做异步处理时: [代码]combineLatest(foo$, bar$).pipe( switchMap(keyword => fromPromise(someAsyncOperation(/* ... */))) ); [代码] 同时,由于标准制定的Promise是没有cancel方法的,有时候我们要取消异步方法的时候就有些麻烦(主要是为了解决一些并发安全问题)。switchMap当上游有新值到来时,会忽略结束已有未完成的Observable然后调用函数返回一个新的Observable,我们只使用一个函数就解决了并发安全问题。当然,我们可以根据实际需要选用switchMap、mergeMap、concatMap、exhaustMap等。 而对于时间轴的操作,Rxjs也有巨大优势。上篇博客中提到当我们需要延时 5 秒做操作时,无论是Event Emitter还是面向对象的方式都力不从心,而在Rxjs中我们只需要一个delay操作符即可解决问题: [代码]input$.pipe( delay(5000) // 下游会在input$值到来后5秒才接到数据 ); [代码] 用 Rxjs 处理数据 在实际开发过程中,事件不能解决所有问题,我们往往会需要存储数据,而Observable被设计成用于处理事件,因此它有很多符合事件直觉的设计。 Observable被设计为懒(lazy)的,当当没有订阅者时,一个流不会执行。对于事件而言,没有事件的消费者那么不执行也不会有问题。而在 GUI 中,订阅者可能是View: [代码]class View extends Component { state = { input: '' }; componentDidMount() { this.subscription = input$.subscribe(input => { this.setState({ input }); }); } componentWillUnmount() { this.subscription.unsubscribe(); } render() { // ... } } [代码] 由于这个View可能不存在,例如路由被切走了,那么我们的事件源就没有了订阅者,他就不会运行。但是我们希望在路由被且走后,后台的数据依然会继续。 对于事件而言,在事件发生之后的订阅者不会受到订阅之前的逻辑。例如在EventEmitter中: [代码]eventEmitter.emit('hello', 1); // ... eventEmitter.on('hello', function listener() {}); [代码] 由于listener是在hello事件发生后在监听的,不会收到值为1的事件。但是这在处理数据的时候会造成麻烦,我们的数据在View被卸载(例如路由切走)后丢失。 同时,由于Observable没有提供直接取到内部状态的方法,当我们使用Observable处理数据时,我们不方便随时拿到数据。那有办法解决这个问题,从而使Observable强大抽象能力去赋能数据层呢? 回到Redux。Redux的事件(Action)其实是一个事件流,那么我们就可以很自然地把Redux的事件流融入到Rxjs流中: [代码]() => next => { const action$ = new Subject(); return action => { action$.next(action); // ... }; }; [代码] 通过这样的封装,redux-observable就能让我们把Observable强大的事件描述和处理能力和Redux结合。我们可以非常方便地根据Action去处理副作用: [代码]action$.pipe( ofType('ACTION_1'), switchMap(() => { // ... }), map(res => ({ type: 'ACTION_2', payload: res })) ); action$.pipe( ofType('ACTION_3'), mergeMap(() => { // ... }), map(res => ({ type: 'ACTION_4', payload: res })) ); [代码] Redux Observable使我们可以结合Redux和Observable。在这里,Action被视作一个流,ofType相当于filter(action => action.type === ‘SOME_ACTION’),从而得到需要监听的Action,得益于Redux的设计,我们可以通过监听Action去完成副作用的处理或者监听数据变化。最后这个流返回一个新的Action流,Redux Observable会把这个新的Action流中的Action dispatch出去。由此,我们在使用Redux存储数据的基础上获得了Rxjs对异步事件的强大处理能力。 查看更多>> 浅谈前端响应式设计(一)
2019-03-20 - 小程序多端框架全面测评
最近前端届多端框架频出,相信很多有代码多端运行需求的开发者都会产生一些疑惑:这些框架都有什么优缺点?到底应该用哪个? 作为 Taro 开发团队一员,笔者想在本文尽量站在一个客观公正的角度去评价各个框架的选型和优劣。但宥于利益相关,本文的观点很可能是带有偏向性的,大家可以带着批判的眼光去看待,权当抛砖引玉。 那么,当我们在讨论多端框架时,我们在谈论什么: 多端 笔者以为,现在流行的多端框架可以大致分为三类: 1. 全包型 这类框架最大的特点就是从底层的渲染引擎、布局引擎,到中层的 DSL,再到上层的框架全部由自己开发,代表框架是 Qt 和 Flutter。这类框架优点非常明显:性能(的上限)高;各平台渲染结果一致。缺点也非常明显:需要完全重新学习 DSL(QML/Dart),以及难以适配中国特色的端:小程序。 这类框架是最原始也是最纯正的的多端开发框架,由于底层到上层每个环节都掌握在自己手里,也能最大可能地去保证开发和跨端体验一致。但它们的框架研发成本巨大,渲染引擎、布局引擎、DSL、上层框架每个部分都需要大量人力开发维护。 2. Web 技术型 这类框架把 Web 技术(JavaScript,CSS)带到移动开发中,自研布局引擎处理 CSS,使用 JavaScript 写业务逻辑,使用流行的前端框架作为 DSL,各端分别使用各自的原生组件渲染。代表框架是 React Native 和 Weex,这样做的优点有: 开发迅速 复用前端生态 易于学习上手,不管前端后端移动端,多多少少都会一点 JS、CSS 缺点有: 交互复杂时难以写出高性能的代码,这类框架的设计就必然导致 [代码]JS[代码] 和 [代码]Native[代码] 之间需要通信,类似于手势操作这样频繁地触发通信就很可能使得 UI 无法在 16ms 内及时绘制。React Native 有一些声明式的组件可以避免这个问题,但声明式的写法很难满足复杂交互的需求。 由于没有渲染引擎,使用各端的原生组件渲染,相同代码渲染的一致性没有第一种高。 3. JavaScript 编译型 这类框架就是我们这篇文章的主角们:[代码]Taro[代码]、[代码]WePY[代码] 、[代码]uni-app[代码] 、 [代码]mpvue[代码] 、 [代码]chameleon[代码],它们的原理也都大同小异:先以 JavaScript 作为基础选定一个 DSL 框架,以这个 DSL 框架为标准在各端分别编译为不同的代码,各端分别有一个运行时框架或兼容组件库保证代码正确运行。 这类框架最大优点和创造的最大原因就是小程序,因为第一第二种框架其实除了可以跨系统平台之外,也都能编译运行在浏览器中。(Qt 有 Qt for WebAssembly, Flutter 有 Hummingbird,React Native 有 [代码]react-native-web[代码], Weex 原生支持) 另外一个优点是在移动端一般会编译到 React Native/Weex,所以它们也都拥有 Web 技术型框架的优点。这看起来很美好,但实际上 React Native/Weex 的缺点编译型框架也无法避免。除此之外,编译型框架的抽象也不是免费的:当 bug 出现时,问题的根源可能出在运行时、编译时、组件库以及三者依赖的库等等各个方面。在 Taro 开源的过程中,我们就遇到过 Babel 的 bug,React Native 的 bug,JavaScript 引擎的 bug,当然也少不了 Taro 本身的 bug。相信其它原理相同的框架也无法避免这一问题。 但这并不意味着这类为了小程序而设计的多端框架就都不堪大用。首先现在各巨头超级 App 的小程序百花齐放,框架会为了抹平小程序做了许多工作,这些工作在大部分情况下是不需要开发者关心的。其次是许多业务类型并不需要复杂的逻辑和交互,没那么容易触发到框架底层依赖的 bug。 那么当你的业务适合选择编译型框架时,在笔者看来首先要考虑的就是选择 DSL 的起点。因为有多端需求业务通常都希望能快速开发,一个能够快速适应团队开发节奏的 DSL 就至关重要。不管是 React 还是 Vue(或者类 Vue)都有它们的优缺点,大家可以根据团队技术栈和偏好自行选择。 如果不管什么 DSL 都能接受,那就可以进入下一个环节: 生态 以下内容均以各框架现在(2019 年 3 月 11日)已发布稳定版为标准进行讨论。 开发工具 就开发工具而言 [代码]uni-app[代码] 应该是一骑绝尘,它的文档内容最为翔实丰富,还自带了 IDE 图形化开发工具,鼠标点点点就能编译测试发布。 其它的框架都是使用 CLI 命令行工具,但值得注意的是 [代码]chameleon[代码] 有独立的语法检查工具,[代码]Taro[代码] 则单独写了 ESLint 规则和规则集。 在语法支持方面,[代码]mpvue[代码]、[代码]uni-app[代码]、[代码]Taro[代码] 、[代码]WePY[代码] 均支持 TypeScript,四者也都能通过 [代码]typing[代码] 实现编辑器自动补全。除了 API 补全之外,得益于 TypeScript 对于 JSX 的良好支持,Taro 也能对组件进行自动补全。 CSS 方面,所有框架均支持 [代码]SASS[代码]、[代码]LESS[代码]、[代码]Stylus[代码],Taro 则多一个 [代码]CSS Modules[代码] 的支持。 所以这一轮比拼的结果应该是: [代码]uni-app[代码] > [代码]Taro[代码] > [代码]chameleon[代码] > [代码]WePY[代码]、[代码]mpvue[代码] [图片] 多端支持度 只从支持端的数量来看,[代码]Taro[代码] 和 [代码]uni-app[代码] 以六端略微领先(移动端、H5、微信小程序、百度小程序、支付宝小程序、头条小程序),[代码]chameleon[代码] 少了头条小程序紧随其后。 但值得一提的是 [代码]chameleon[代码] 有一套自研多态协议,编写多端代码的体验会好许多,可以说是一个能戳到多端开发痛点的功能。[代码]uni-app[代码] 则有一套独立的条件编译语法,这套语法能同时作用于 [代码]js[代码]、样式和模板文件。[代码]Taro[代码] 可以在业务逻辑中根据环境变量使用条件编译,也可以直接使用条件编译文件(类似 React Native 的方式)。 在移动端方面,[代码]uni-app[代码] 基于 [代码]weex[代码] 定制了一套 [代码]nvue[代码] 方案 弥补 [代码]weex[代码] API 的不足;[代码]Taro[代码] 则是暂时基于 [代码]expo[代码] 达到同样的效果;[代码]chameleon[代码] 在移动端则有一套 SDK 配合多端协议与原生语言通信。 H5 方面,[代码]chameleon[代码] 同样是由多态协议实现支持,[代码]uni-app[代码] 和 [代码]Taro[代码] 则是都在 H5 实现了一套兼容的组件库和 API。 [代码]mpvue[代码] 和 [代码]WePY[代码] 都提供了转换各端小程序的功能,但都没有 h5 和移动端的支持。 所以最后一轮对比的结果是: [代码]chameleon[代码] > [代码]Taro[代码]、[代码]uni-app[代码] > [代码]mpvue[代码]、[代码]WePY[代码] [图片] 组件库/工具库/demo 作为开源时间最长的框架,[代码]WePY[代码] 不管从 Demo,组件库数量 ,工具库来看都占有一定优势。 [代码]uni-app[代码] 则有自己的插件市场和 UI 库,如果算上收费的框架和插件比起 [代码]WePy[代码] 也是完全不遑多让的。 [代码]Taro[代码] 也有官方维护的跨端 UI 库 [代码]taro-ui[代码] ,另外在状态管理工具上也有非常丰富的选择(Redux、MobX、dva),但 demo 的数量不如前两个。但 [代码]Taro[代码] 有一个转换微信小程序代码为 Taro 代码的工具,可以弥补这一问题。 而 [代码]mpvue[代码] 没有官方维护的 UI 库,[代码]chameleon[代码] 第三方的 demo 和工具库也还基本没有。 所以这轮的排序是: [代码]WePY[代码] > [代码]uni-app[代码] 、[代码]taro[代码] > [代码]mpvue[代码] > [代码]chameleon[代码] [图片] 接入成本 接入成本有两个方面: 第一是框架接入原有微信小程序生态。由于目前微信小程序已呈一家独大之势,开源的组件和库(例如 [代码]wxparse[代码]、[代码]echart[代码]、[代码]zan-ui[代码] 等)多是基于原生微信小程序框架语法写成的。目前看来 [代码]uni-app[代码] 、[代码]Taro[代码]、[代码]mpvue[代码] 均有文档或 demo 在框架中直接使用原生小程序组件/库,[代码]WePY[代码] 由于运行机制的问题,很多情况需要小改一下目标库的源码,[代码]chameleon[代码] 则是提供了一个按步骤大改目标库源码的迁移方式。 第二是原有微信小程序项目部分接入框架重构。在这个方面 Taro 在京东购物小程序上进行了大胆的实践,具体可以查看文章《Taro 在京东购物小程序上的实践》。其它框架则没有提到相关内容。 而对于两种接入方式 Taro 都提供了 [代码]taro convert[代码] 功能,既可以将原有微信小程序项目转换为 Taro 多端代码,也可以将微信小程序生态的组件转换为 Taro 组件。 所以这轮的排序是: [代码]Taro[代码] > [代码]mpvue[代码] 、 [代码]uni-app[代码] > [代码]WePY[代码] > [代码]chameleon[代码] 流行度 从 GitHub 的 star 来看,[代码]mpvue[代码] 、[代码]Taro[代码]、[代码]WePY[代码] 的差距非常小。从 NPM 和 CNPM 的 CLI 工具下载量来看,是 Taro(3k/week)> mpvue (2k/w) > WePY (1k/w)。但发布时间也刚好反过来。笔者估计三家的流行程度和案例都差不太多。 [代码]uni-app[代码] 则号称有上万案例,但不像其它框架一样有一些大厂应用案例。另外从开发者的数量来看也是 [代码]uni-app[代码] 领先,它拥有 20+ 个 QQ 交流群(最大人数 2000)。 所以从流行程度来看应该是: [代码]uni-app[代码] > [代码]Taro[代码]、[代码]WePY[代码]、[代码]mpvue[代码] > [代码]chameleon[代码] [图片] 开源建设 一个开源作品能走多远是由框架维护团队和第三方开发者共同决定的。虽然开源建设不能具体地量化,但依然是衡量一个框架/库生命力的非常重要的标准。 从第三方贡献者数量来看,[代码]Taro[代码] 在这一方面领先,并且 [代码]Taro[代码] 的一些核心包/功能(MobX、CSS Modules、alias)也是由第三方开发者贡献的。除此之外,腾讯开源的 [代码]omi[代码] 框架小程序部分也是基于 Taro 完成的。 [代码]WePY[代码] 在腾讯开源计划的加持下在这一方面也有不错的表现;[代码]mpvue[代码] 由于停滞开发了很久就比较落后了;可能是产品策略的原因,[代码]uni-app[代码] 在开源建设上并不热心,甚至有些部分代码都没有开源;[代码]chameleon[代码] 刚刚开源不久,但它的代码和测试用例都非常规范,以后或许会有不错的表现。 那么这一轮的对比结果是: [代码]Taro[代码] > [代码]WePY[代码] > [代码]mpvue[代码] > [代码]chameleon[代码] > [代码]uni-app[代码] 最后补一个总的生态对比图表: [图片] 未来 从各框架已经公布的规划来看: [代码]WePY[代码] 已经发布了 [代码]v2.0.alpha[代码] 版本,虽然没有公开的文档可以查阅到 [代码]2.0[代码] 版本有什么新功能/特性,但据其作者介绍,[代码]WePY 2.0[代码] 会放大招,是一个「对得起开发者」的版本。笔者也非常期待 2.0 正式发布后 [代码]WePY[代码] 的表现。 [代码]mpvue[代码] 已经发布了 [代码]2.0[代码] 的版本,主要是更新了其它端小程序的支持。但从代码提交, issue 的回复/解决率来看,[代码]mpvue[代码] 要想在未来有作为首先要打消社区对于 [代码]mpvue[代码] 不管不顾不更新的质疑。 [代码]uni-app[代码] 已经在生态上建设得很好了,应该会在此基础之上继续稳步发展。如果 [代码]uni-app[代码] 能加强开源开放,再加强与大厂的合作,相信未来还能更上一层楼。 [代码]chameleon[代码] 的规划比较宏大,虽然是最后发的框架,但已经在规划或正在实现的功能有: 快应用和端拓展协议 通用组件库和垂直类组件库 面向研发的图形化开发工具 面向非研发的图形化页面搭建工具 如果 [代码]chameleon[代码] 把这些功能都做出来的话,再继续完善生态,争取更多第三方开发者,那么在未来 [代码]chameleon[代码] 将大有可为。 [代码]Taro[代码] 的未来也一样值得憧憬。Taro 即将要发布的 [代码]1.3[代码] 版本就会支持以下功能: 快应用支持 Taro Doctor,自动化检查项目配置和代码合法性 更多的 JSX 语法支持,1.3 之后限制生产力的语法只有 [代码]只能用 map 创造循环组件[代码] 一条 H5 打包体积大幅精简 同时 [代码]Taro[代码] 也正在对移动端进行大规模重构;开发图形化开发工具;开发组件/物料平台以及图形化页面搭建工具。 结语 那说了那么多,到底用哪个呢? 如果不介意尝鲜和学习 DSL 的话,完全可以尝试 [代码]WePY[代码] 2.0 和 [代码]chameleon[代码] ,一个是酝酿了很久的 2.0 全新升级,一个有专门针对多端开发的多态协议。 [代码]uni-app[代码] 和 [代码]Taro[代码] 相比起来就更像是「水桶型」框架,从工具、UI 库,开发体验、多端支持等各方面来看都没有明显的短板。而 [代码]mpvue[代码] 由于开发一度停滞,现在看来各个方面都不如在小程序端基于它的 [代码]uni-app[代码] 。 当然,Talk is cheap。如果对这个话题有更多兴趣的同学可以去 GitHub 另行研究,有空看代码,没空看提交: chameleon: https://github.com/didi/chameleon mpvue: https://github.com/Meituan-Dianping/mpvue Taro: https://github.com/NervJS/taro uni-app: https://github.com/dcloudio/uni-app WePY: https://github.com/Tencent/wepy (按字母顺序排序)
2019-03-19 - 你不知道的Virtual DOM(一):Virtual Dom介绍
一、前言 目前最流行的两大前端框架,React 和 Vue,都不约而同的借助 Virtual DOM 技术提高页面的渲染效率。那么,什么是 Virtual DOM?它是通过什么方式去提升页面渲染效率的呢?本系列文章会详细讲解 Virtual DOM 的创建过程,并实现一个简单的 Diff 算法来更新页面。本文的内容脱离于任何的前端框架,只讲最纯粹的 Virtual DOM。敲单词太累了,下文 Virtual DOM 一律用 VD 表示。 这是 VD 系列文章的开篇,后续还会有更多的文章带你深入了解 VD 的奥秘。 二、VD 是什么 本质上来说,VD 只是一个简单的 JS 对象,并且最少包含 tag、 props和 children三个属性。不同的框架对这三个属性的命名会有点差别,但表达的意思是一致的。它们分别是标签名(tag)、属性(props)和子元素对象(children)。下面是一个典型的 VD 对象例子: [代码]{ tag: "div", props: {}, children: [ "Hello World", { tag: "ul", props: {}, children: [{ tag: "li", props: { id: 1, class: "li-1" }, children: ["第", 1] }] } ] } [代码] VD 跟 dom 对象有一一对应的关系,上面的 VD 是由以下的 HTML 生成的: [代码]<div> Hello World <ul> <li id="1" class="li-1"> 第1 </li> </ul> </div> [代码] 一个 dom 对象,比如 li,由 tag(li), props({id:1,class:“li-1”})和 children([“第”,1])三个属性来描述。 三、为什么需要 VD 借助 VD,可以达到有效减少页面渲染次数的目的,从而提高渲染效率。我们先来看下页面的更新一般会经过几个阶段: [图片] 从上面的例子中,可以看出页面的呈现会分以下 3 个阶段: JS 计算 生成渲染树 绘制页面 这个例子里面,JS 计算用了 691毫秒,生成渲染树 578毫秒,绘制 73毫秒。如果能有效的减少生成渲染树和绘制所花的时间,更新页面的效率也会随之提高。 通过 VD 的比较,我们可以将多个操作合并成一个批量的操作,从而减少 dom 重排的次数,进而缩短了生成渲染树和绘制所花的时间。至于如何基于 VD 更有效率的更新 dom,是一个很有趣的话题,日后有机会将另写一篇文章介绍。 四、如何实现 VD 与真实 DOM 的映射 我们先从如何生成 VD 说起。借助 JSX 编译器,可以将文件中的 HTML 转化成函数的形式,然后再利用这个函数生成 VD。看下面这个例子: [代码]function render() { return ( <div> Hello World <ul> <li id="1" class="li-1"> 第1 </li> </ul> </div> ); } [代码] 这个函数经过 JSX 编译后,会输出下面的内容: [代码]function render() { return h( 'div', null, 'Hello World', h( 'ul', null, h( 'li', { id: '1', 'class': 'li-1' }, '\u7B2C1' ) ) ); } [代码] 这里的 h 是一个函数,可以起任意的名字。这个名字通过 babel 进行配置: [代码]// .babelrc 文件 { "plugins": [ ["transform-react-jsx", { "pragma": "h" // 这里可配置任意的名称 }] ] } [代码] 接下来,我们只需要定义 h 函数,就能构造出 VD: [代码]function flatten(arr) { return [].concat.apply([], arr); } function h(tag, props, ...children) { return { tag, props: props || {}, children: flatten(children) || [] }; } [代码] h 函数会传入三个或以上的参数,前两个参数一个是标签名,一个是属性对象,从第三个参数开始的其它参数都是 children。children 元素有可能是数组的形式,需要将数组解构一层。比如: [代码]function render() { return ( <ul> <li>0</li> { [1, 2, 3].map( i => ( <li>{i}</li> )) } </ul> ); } // JSX 编译后 function render() { return h( 'ul', null, h( 'li', null, '0' ), /* * 需要将下面这个数组解构出来再放到 children 数组中 */ [1, 2, 3].map(i => h( 'li', null, i )) ); } [代码] 继续之前的例子。执行 h 函数后,最终会得到如下的 VD 对象: [代码]{ tag: "div", props: {}, children: [ "Hello World", { tag: "ul", props: {}, children: [{ tag: "li", props: { id: 1, class: "li-1" }, children: ["第", 1] }] } ] } [代码] 下一步,通过遍历 VD 对象,生成真实的 dom [代码]// 创建 dom 元素 function createElement(vdom) { // 如果 vdom 是字符串或者数字类型,则创建文本节点,比如“Hello World” if (typeof vdom === 'string' || typeof vdom === 'number') { return doc.createTextNode(vdom); } const {tag, props, children} = vdom; // 1. 创建元素 const element = doc.createElement(tag); // 2. 属性赋值 setProps(element, props); // 3. 创建子元素 // appendChild 在执行的时候,会检查当前的 this 是不是 dom 对象,因此要 bind 一下 children.map(createElement) .forEach(element.appendChild.bind(element)); return element; } // 属性赋值 function setProps(element, props) { for (let key in props) { element.setAttribute(key, props[key]); } } [代码] createElement函数执行完后,dom元素就创建完并展示到页面上了(页面比较丑,不要介意…)。 [图片] 五、总结 本文介绍了 VD 的基本概念,并讲解了如何利用 JSX 编译 HTML 标签,然后生成 VD,进而创建真实 dom 的过程。下一篇文章将会实现一个简单的 VD Diff 算法,找出 2 个 VD 的差异并将更新的元素映射到 dom 中去。 PS: 想看完整代码见这里: 代码(https://gist.github.com/dickenslian/86c4e266ae5f2134373376133bec9e3d) 参考链接: The Inner Workings Of Virtual DOM (https://medium.com/@rajaraodv/the-inner-workings-of-virtual-dom-666ee7ad47cf) preact源码学习系列之一:JSX解析与DOM渲染 (https://github.com/youngwind/blog/issues/103)
2019-03-04 - 你不知道的Virtual DOM(二):Virtual DOM 的更新
一、前言 目前最流行的两大前端框架,React 和 Vue,都不约而同的借助 Virtual DOM 技术提高页面的渲染效率。那么,什么是 Virtual DOM ?它是通过什么方式去提升页面渲染效率的呢?本系列文章会详细讲解 Virtual DOM 的创建过程,并实现一个简单的 Diff 算法来更新页面。本文的内容脱离于任何的前端框架,只讲最纯粹的 Virtual DOM 。敲单词太累了,下文 Virtual DOM 一律用 VD 表示。 这是 VD 系列文章的第二篇,以下是本系列其它文章的传送门: 你不知道的Virtual DOM(一):Virtual Dom介绍 本文将会实现一个简单的 VD Diff 算法,计算出差异并反映到真实的 DOM 上去。 二、思路 使用 VD 的框架,一般的设计思路都是页面等于页面状态的映射,即UI = render(state)。当需要更新页面的时候,无需关心 DOM 具体的变换方式,只需要改变state即可,剩下的事情(render)将由框架代劳。我们考虑最简单的情况,当 state 发生变化时,我们重新生成整个 VD ,触发比较的操作。上述过程分为以下四步: - state 变化,生成新的 VD - 比较 VD 与之前 VD 的异同 - 生成差异对象(patch) - 遍历差异对象并更新 DOM 差异对象的数据结构是下面这个样子,与每一个 VDOM 元素一一对应: [代码]{ type, vdom, props: [{ type, key, value }] children } [代码] 最外层的 type 对应的是 DOM 元素的变化类型,有 4 种:新建、删除、替换和更新。props 变化的 type 只有2种:更新和删除。枚举值如下: [代码]const nodePatchTypes = { CREATE: 'create node', REMOVE: 'remove node', REPLACE: 'replace node', UPDATE: 'update node' } const propPatchTypes = { REMOVE: 'remove prop', UPDATE: 'update prop' } [代码] 三、代码实现 我们做一个定时器,500 毫秒运行一次,每次对 state 加 1。页面的li元素的数量随着 state 而变。 [代码]let state = { num: 5 }; let timer; let preVDom; function render(element) { // 初始化的 VD const vdom = view(); preVDom = vdom; const dom = createElement(vdom); element.appendChild(dom); timer = setInterval(() => { state.num += 1; tick(element); }, 500); } function tick(element) { if (state.num > 20) { clearTimeout(timer); return; } const newVDom = view(); } function view() { return ( <div> Hello World <ul> { // 生成元素为0到n-1的数组 [...Array(state.num).keys()] .map( i => ( <li id={i} class={`li-${i}`}> 第{i * state.num} </li> )) } </ul> </div> ); } [代码] 接下来,通过对比 2 个 VD,生成差异对象。 [代码]function tick(element) { if (state.num > 20) { clearTimeout(timer); return; } const newVDom = view(); // 生成差异对象 const patchObj = diff(preVDom, newVDom); } function diff(oldVDom, newVDom) { // 新建 node if (oldVDom == undefined) { return { type: nodePatchTypes.CREATE, vdom: newVDom } } // 删除 node if (newVDom == undefined) { return { type: nodePatchTypes.REMOVE } } // 替换 node if ( typeof oldVDom !== typeof newVDom || ((typeof oldVDom === 'string' || typeof oldVDom === 'number') && oldVDom !== newVDom) || oldVDom.tag !== newVDom.tag ) { return { type: nodePatchTypes.REPLACE, vdom: newVDom } } // 更新 node if (oldVDom.tag) { // 比较 props 的变化 const propsDiff = diffProps(oldVDom, newVDom); // 比较 children 的变化 const childrenDiff = diffChildren(oldVDom, newVDom); // 如果 props 或者 children 有变化,才需要更新 if (propsDiff.length > 0 || childrenDiff.some( patchObj => (patchObj !== undefined) )) { return { type: nodePatchTypes.UPDATE, props: propsDiff, children: childrenDiff } } } } // 比较 props 的变化 function diffProps(oldVDom, newVDom) { const patches = []; const allProps = {...oldVDom.props, ...newVDom.props}; // 获取新旧所有属性名后,再逐一判断新旧属性值 Object.keys(allProps).forEach((key) => { const oldValue = oldVDom.props[key]; const newValue = newVDom.props[key]; // 删除属性 if (newValue == undefined) { patches.push({ type: propPatchTypes.REMOVE, key }); } // 更新属性 else if (oldValue == undefined || oldValue !== newValue) { patches.push({ type: propPatchTypes.UPDATE, key, value: newValue }); } } ) return patches; } // 比较 children 的变化 function diffChildren(oldVDom, newVDom) { const patches = []; // 获取子元素最大长度 const childLength = Math.max(oldVDom.children.length, newVDom.children.length); // 遍历并diff子元素 for (let i = 0; i < childLength; i++) { patches.push(diff(oldVDom.children[i], newVDom.children[i])); } return patches; } [代码] 计算得出的差异对象是这个样子的: [代码]{ type: "update node", props: [], children: [ null, { type: "update node", props: [], children: [ null, { type: "update node", props: [], children: [ null, { type: "replace node", vdom: 6 } ] } ] }, { type: "create node", vdom: { tag: "li", props: { id: 5, class: "li-5" }, children: ["第", 30] } } ] } [代码] 下一步就是遍历差异对象并更新 DOM 了: [代码]function tick(element) { if (state.num > 20) { clearTimeout(timer); return; } const newVDom = view(); // 生成差异对象 const patchObj = diff(preVDom, newVDom); preVDom = newVDom; // 给 DOM 打个补丁 patch(element, patchObj); } // 给 DOM 打个补丁 function patch(parent, patchObj, index=0) { if (!patchObj) { return; } // 新建元素 if (patchObj.type === nodePatchTypes.CREATE) { return parent.appendChild(createElement(patchObj.vdom)); } const element = parent.childNodes[index]; // 删除元素 if (patchObj.type === nodePatchTypes.REMOVE) { return parent.removeChild(element); } // 替换元素 if (patchObj.type === nodePatchTypes.REPLACE) { return parent.replaceChild(createElement(patchObj.vdom), element); } // 更新元素 if (patchObj.type === nodePatchTypes.UPDATE) { const {props, children} = patchObj; // 更新属性 patchProps(element, props); // 更新子元素 children.forEach( (patchObj, i) => { // 更新子元素时,需要将子元素的序号传入 patch(element, patchObj, i) }); } } // 更新属性 function patchProps(element, props) { if (!props) { return; } props.forEach( patchObj => { // 删除属性 if (patchObj.type === propPatchTypes.REMOVE) { element.removeAttribute(patchObj.key); } // 更新或新建属性 else if (patchObj.type === propPatchTypes.UPDATE) { element.setAttribute(patchObj.key, patchObj.value); } }) } [代码] 到此为止,整个更新的流程就执行完了。可以看到页面跟我们预期的一样,每 500 毫秒刷新一次,构造渲染树和绘制页面花的时间也非常少。 [图片] 作为对比,如果我们在生成新的 VD 后,不经过比较,而是直接重新渲染整个 DOM 的时候,会怎样呢?我们修改一下代码: [代码]function tick(element) { if (state.num > 20) { clearTimeout(timer); return; } const newVDom = view(); newDom = createElement(newVDom); element.replaceChild(newDom, dom); dom = newDom; /* // 生成差异对象 const patchObj = diff(preVDom, newVDom); preVDom = newVDom; // 给 DOM 打个补丁 patch(element, patchObj); */ } [代码] 效果如下: [图片] 可以看到,构造渲染树(Rendering)和绘制页面(Painting)的时间要多一些。但另一方面花在 JS 计算(Scripting)的时间要少一些,因为不需要比较节点的变化。如果算总时间的话,重新渲染整个 DOM 花费的时间反而更少,这是为什么呢? 其实原因很简单,因为我们的 DOM 树太简单了!节点很少,使用到的 css 也很少,所以构造渲染树和绘制页面就花不了多少时间。VD 真正的效果还是要在真实的项目中才体现得出来。 四、总结 本文详细介绍如何实现一个简单的 VD Diff 算法,再根据计算出的差异去更新真实的 DOM 。然后对性能做了一个简单的分析,得出使用 VD 在减少渲染时间的同时增加了 JS 计算时间的结论。基于当前这个版本的代码还能做怎样的优化呢,请期待下一篇的内容:你不知道的Virtual DOM(三):Virtual DOM 更新优化
2019-03-05