Westore 1.0 正式发布 - 小程序框架一个就够
世界上最小却强大的小程序框架 - 搞定全局状态管理和跨页通讯
github:
众所周知,小程序通过页面或组件各自的 setdata 再加上各种父子、祖孙、姐弟、嫂子与堂兄等等组件间的通讯会把程序搞成一团浆糊,如果再加上跨页面之间的组件通讯,会让程序非常难维护和调试。虽然市面上出现了许多技术栈编译转小程序的技术,但是我觉没有戳中小程序的痛点。小程序不管从组件化、开发、调试、发布、灰度、回滚、上报、统计、监控和最近的云能力都非常完善,小程序的工程化简直就是前端的典范。而开发者工具也在持续更新,可以想象的未来,组件布局的话未必需要写代码了。所以最大的痛点只剩下状态管理和跨页通讯。
受 omi 框架 的启发,且专门为小程序开发的 json diff 库,所以有了 westore 全局状态管理和跨页通讯框架让一切尽在掌握中,且受高性能 json diff 库的利好,长列表滚动加载显示变得轻松可驾驭。总结下来有如下特性和优势:
- 和 omi 同样简洁的 store api
- 超小的代码尺寸(包括 json diff 共100多行)
- 尊重且顺从小程序的设计(其他转译库相当于反其道行)
- this.update 比原生 setdata 的性能更优,更加智能
api
westore api 只有三个, 大道至简:
- create(store, option) 创建页面
- create(option) 创建组件
- this.update() 更新页面或组件
使用指南
定义全局 store
export default { data: { motto: 'hello world', userinfo: {}, hasuserinfo: false, caniuse: wx.caniuse('button.open-type.getuserinfo'), logs: [] }, logmotto: function () { console.log(this.data.motto) } }
你不需要在页面和组件上再声明 data 属性。如果申明了也没关系,会被 object.assign 覆盖到 store.data 上。后续只需修改 this.store.data 便可。
创建页面
import store from '../../store' import create from '../../utils/create' const app = getapp() create(store, { onload: function () { if (app.globaldata.userinfo) { this.store.data.userinfo = app.globaldata.userinfo this.store.data.hasuserinfo = true this.update() } else if (this.data.caniuse) { app.userinforeadycallback = res => { this.store.data.userinfo = res.userinfo this.store.data.hasuserinfo = true this.update() } } else { wx.getuserinfo({ success: res => { app.globaldata.userinfo = res.userinfo this.store.data.userinfo = res.userinfo this.store.data.hasuserinfo = true this.update() } }) } } })
创建 page 只需传入两个参数,store 从根节点注入,所有子组件都能通过 this.store 访问。
绑定数据
<view class="container"> <view class="userinfo"> <button wx:if="{{!hasuserinfo && caniuse}}" open-type="getuserinfo" bindgetuserinfo="getuserinfo"> 获取头像昵称 </button> <block wx:else> <image bindtap="bindviewtap" class="userinfo-avatar" src="{{userinfo.avatarurl}}" mode="cover"></image> <text class="userinfo-nickname">{{userinfo.nickname}}</text> </block> </view> <view class="usermotto"> <text class="user-motto">{{motto}}</text> </view> <hello></hello> </view>
和以前的写法没有差别,直接把 store.data
作为绑定数据源。
更新页面
this.store.data.any_prop_you_want_to_change = 'any_thing_you_want_change_to' this.update()
创建组件
import create from '../../utils/create' create({ ready: function () { //you can use this.store here }, methods: { //you can use this.store here } })
和创建 page 不一样的是,创建组件只需传入一个参数,不需要传入 store,因为已经从根节点注入了。
更新组件
this.store.data.any_prop_you_want_to_change = 'any_thing_you_want_change_to' this.update()
setdata 和 update 对比
拿官方模板示例的 log 页面作为例子:
this.setdata({ logs: (wx.getstoragesync('logs') || []).map(log => { return util.formattime(new date(log)) }) })
使用 westore 后:
this.store.data.logs = (wx.getstoragesync('logs') || []).map(log => { return util.formattime(new date(log)) }) this.update()
看似一条语句变成了两条语句,但是 this.update 调用的 setdata 是 diff 后的,所以传递的数据更少。
跨页面同步数据
使用 westore 你不用关系跨页数据同步,你只需要专注 this.store.data 便可,修改完在任意地方调用 update 便可:
this.update()
调试
console.log(getapp().globaldata.store.data)
超大型小程序最佳实践(两种方案)
不排除小程序被做大得可能,接触的最大的小程序有 60+ 的页面,所以怎么管理?这里给出了两个最佳实践方案。
- 第一种方案,拆分 store 的 data 为不同模块,如:
export default { data: { commona: 'a', commonb: 'b', pagea: { a: 1 xx: 'xxx' }, pageb: { b: 2, c: 3 } }, xxx: function () { console.log(this.data) } }
- 第二种方案,拆分 store 的 data 到不同文件且合并到一个 store 暴露给 create 方法,如:
a.js
export default { data: { a: 1 xx: 'xxx' }, amethod: function (num) { this.data.a += num } }
b.js
export default { data: { b: 2, c: 3 }, bmethod: function () { } }
store.js
import a from 'a.js' import b from 'b.js' export default { data: { commonnum: 1, commonb: 'b', pagea: a.data pageb: b.data }, xxx: function () { //you can call the methods of a or b and can pass args to them console.log(a.amethod(commonnum)) }, xx: function(){ } }
当然,也可以不用按照页面拆分文件或模块,也可以按照领域来拆分,这个很*,视情况而定。
原理
--------------- ------------------- ----------------------- | this.update | → | json diff | → | setdata()-setdata()...| → 之后就是黑盒(小程序官方实现,但是 dom/apply diff 肯定是少不了) --------------- ------------------- -----------------------
虽然和 omi 一样同为 store.updata 但是却有着本质的区别。omi 的如下:
--------------- ------------------- ---------------- ------------------------------ | this.update | → | setstate | → | jsx rerender | → | vdom diff → apply diff... | --------------- ------------------- ---------------- ------------------------------
都是数据驱动视图,但本质不同,原因:
- 小程序 store 和 dom 不在同一个环境,先在 js 环境进行 json diff,然后使用 diff 结果通过 setdata 通讯
- web 里使用 omi 的话 store 和 dom 在同一环境,setstate 直接驱动的 vdom diff 然后把 diff 结果作用在真是 dom 上
json diff
先看一下我为 westore 专门定制开发的 json diff 库 的能力:
diff({ a: 1, b: 2, c: "str", d: { e: [2, { a: 4 }, 5] }, f: true, h: [1], g: { a: [1, 2], j: 111 } }, { a: [], b: "aa", c: 3, d: { e: [3, { a: 3 }] }, f: false, h: [1, 2], g: { a: [1, 1, 1], i: "delete" }, k: 'del' })
diff 的结果是:
{ "a": 1, "b": 2, "c": "str", "d.e[0]": 2, "d.e[1].a": 4, "d.e[2]": 5, "f": true, "h": [1], "g.a": [1, 2], "g.j": 111, "g.i": null, "k": null }
diff 原理:
- 同步所有 key 到当前 store.data
- 携带 path 和 result 递归遍历对比所有 key value
export default function diff(current, pre) { const result = {} synckeys(current, pre) _diff(current, pre, '', result) return result }
同步上一轮 state.data 的 key 主要是为了检测 array 中删除的元素或者 obj 中删除的 key。
小程序 setdata
setdata 是小程序开发中使用最频繁的接口,也是最容易引发性能问题的接口。在介绍常见的错误用法前,先简单介绍一下 setdata 背后的工作原理。setdata 函数用于将数据从逻辑层发送到视图层(异步),同时改变对应的 this.data 的值(同步)。
其中 key 可以以数据路径的形式给出,支持改变数组中的某一项或对象的某个属性,如 array[2].message,a.b.c.d,并且不需要在 this.data 中预先定义。比如:
this.setdata({ 'array[0].text':'changed data' })
所以 diff 的结果可以直接传递给 setdata
,也就是 this.update
。
setdata 工作原理
小程序的视图层目前使用 webview 作为渲染载体,而逻辑层是由独立的 javascriptcore 作为运行环境。在架构上,webview 和 javascriptcore 都是独立的模块,并不具备数据直接共享的通道。当前,视图层和逻辑层的数据传输,实际上通过两边提供的 evaluatejavascript 所实现。即用户传输的数据,需要将其转换为字符串形式传递,同时把转换后的数据内容拼接成一份 js 脚本,再通过执行 js 脚本的形式传递到两边独立环境。
而 evaluatejavascript 的执行会受很多方面的影响,数据到达视图层并不是实时的。
常见的 setdata 操作错误:
- 频繁的去 setdata
- 每次 setdata 都传递大量新数据
- 后台态页面进行 setdata
上面是官方截取的内容。使用 webstore 的 this.update 本质是先 diff,再执行一连串的 setdata,所以可以保证传递的数据每次维持在最小。既然可以使得传递数据最小,所以第一点和第三点虽有违反但可以商榷。
update
这里区分在页面中的 update 和 组件中的 update。页面中的 update 在 onload 事件中进行实例收集。
const onload = option.onload option.onload = function () { this.store = store rewriteupdate(this) store.instances[this.route] = [] store.instances[this.route].push(this) onload && onload.call(this) } page(option)
组件中的 update 在 ready 事件中进行行实例收集:
const ready = store.ready store.ready = function () { this.page = getcurrentpages()[getcurrentpages().length - 1] this.store = this.page.store; this.setdata.call(this, this.store.data) rewriteupdate(this) this.store.instances[this.page.route].push(this) ready && ready.call(this) } component(store)
rewriteupdate 的实现如下:
function rewriteupdate(ctx){ ctx.update = () => { const diffresult = diff(ctx.store.data, origindata) for(let key in ctx.store.instances){ ctx.store.instances[key].foreach(ins => { ins.setdata.call(ins, diffresult) }) } for (let key in diffresult) { updateorigindata(origindata, key, diffresult[key]) } } }
license
mit