世界上最小却强大的小程序框架 - 100多行代码搞定全局状态管理和跨页通讯

Github: https://github.com/dntzhang/westore

众所周知,小程序通过页面或组件各自的 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 @dntzhang

Westore 1.0 正式发布 - 小程序框架一个就够的更多相关文章

  1. 【Web技术】281- 滴滴开源小程序框架 Mpx2.0

    滴滴Mpx框架负责人@hiyuki,滴滴出行网约车webapp乘客团队的负责人,也是滴滴开源的小程序框架Mpx的负责人和核心作者 Mpx是一款致力于提高小程序开发体验和效率的增强型小程序框架,目前在滴 ...

  2. 零基础入门 实战mpvue2.0多端小程序框架

    第1章 课程快速预览(必看!!!)在这一章节中,老师讲带领你快速预览课程整体.其中,涉及到为什么要做这么一门实战课程.制作一个小程序的完整流程是怎么样的,以及如何做项目的技术选型. 第2章 30 分钟 ...

  3. mpvue最佳实践 , 美团出的一个小程序框架

    看手机微信,看到说美团出了1个小程序框架,  mpvue 搜下来试试,看了网上的一个对比 ----------------- 以下为引用 我们对微信小程序.mpvue.WePY 这三个开发框架的主要能 ...

  4. [转] 扩展微信小程序框架功能

    通过第三方 JavaScript 库,扩展微信小程序框架功能. 扩展微信小程序框架功能(1)——Promise ES6 对 Promise 有了原生的支持,但微信开发者工具更新版本(0.11.1122 ...

  5. 小程序框架之逻辑层App Service

    小程序开发框架的逻辑层使用 JavaScript 引擎为小程序提供开发者 JavaScript 代码的运行环境以及微信小程序的特有功能. 逻辑层将数据进行处理后发送给视图层,同时接受视图层的事件反馈. ...

  6. 小程序框架WePY 从入门到放弃踩坑合集

    小程序框架WePY 从入门到放弃踩坑合集 一点点介绍WePY 因为小程序的语法设计略迷, 所以x1 模块化起来并不方便, 所以x2 各厂就出了不少的框架用以方便小程序的开发, 腾讯看到别人家都出了框架 ...

  7. 微信小程序框架探究和解析

    何为框架 你对微信小程序的技术框架了解多少? 对wepy 框架进行一系列的深入了解 微信小程序框架解析和探究 小程序组件化框架WePY 在性能调优上做出的探究 开发者培训班上海专场PPT分享:小程序框 ...

  8. 美团小程序框架mpvue入门

    mpvue 主要特性 使用 mpvue 开发小程序,你将在小程序技术体系的基础上获取到这样一些能力: 1. 彻底的组件化开发能力:提高代码复用性 2. 完整的 Vue.js 开发体验 3. 方便的 V ...

  9. 美团小程序框架mpvue蹲坑指南

    美团小程序框架mpvue(花名:没朋友)蹲坑指南 第一次接触小程序大概是17年初,当时小程序刚刚内侧,当时就被各种限制折腾的死去活来的,单向绑定, 没有promise,请求数限制,包大小限制,各种反人 ...

随机推荐

  1. 遇到一个很古怪的问题,C++类static const成员的初始化

    在我的文件里有这class NFDuration, NFDuration.h里是这样的: // A Duration represents the elapsed time between two i ...

  2. ajax请求经典格式

    $.ajax({ url: url, type: "POST", dataType: "json", data: data, success: funtion1 ...

  3. c# 建立到数据源的连接 以及获取项目配置文件的属性

    两种连接数据库的写法: <connectionStrings> <add name="HRModelsContainer" connectionString=&q ...

  4. Amazon onsite behavior question

    https://www.1point3acres.com/bbs/thread-307462-1-1.html http://kraftshala.com/how-to-raise-the-bar-i ...

  5. ELK收集tomcat访问日志并存取mysql数据库案例

    这个案例中,tomcat产生的日志由filebeat收集,然后存取到redis中,再由logstash进行过滤清洗等操作,最后由elasticsearch存储索引并由kibana进行展示. 1.配置t ...

  6. 新安装 Ubuntu 系统设置root用户密码!谨此纪念自己踩过的坑!

    Ubuntu 在安装过程中创建的用户为普通用户,而root 用户密码该如何设置呢? 执行以下命令即可: sudo passwd root 提示你输入普通用户密码,然后设置root用户的密码!这样,就更 ...

  7. 《Java大学教程》--第3章 迭代

    迭代(iteration).重复(repetition):三种循环* for: 重复执行固定次数* while: 重复执行不固定次数* do...while: 比while至少多一次 1.答:P47迭 ...

  8. Cassandra联手Spark 大数据分析将迎来哪些改变?

    2014Spark峰会在美国旧金山举行,与会数据库平台供应商DataStax宣布,与Spark供应商Databricks合作,在它的旗舰产 品 DataStax Enterprise 4.5 (DSE ...

  9. Matplotlib 绘图 用法

    Matplotlib基础知识 一.Matplotlib基础知识 Matplotlib中的基本图表包括的元素 x轴和y轴 axis 水平和垂直的轴线 x轴和y轴刻度 tick 刻度标示坐标轴的分隔,包括 ...

  10. B树索引分裂

    一.索引分裂 1.  什么是分裂 在开始介绍之前,我们先来搞清楚什么是索引分裂吧.“索引分裂”就是索引块的分裂,当一次DML事务操作修改了索引块上的数据,但是旧有的索引块没有足够的空间来容纳新修改的数 ...