- 基于Proxy的小程序状态管理
作者:wwayne 原文:基于Proxy的小程序状态管理 Fundebug经授权转载,版权归原作者所有。 微信小程序的市场在进一步的扩大,而背后的技术社区仍在摸索着最好的实践方案。我在帮助Nike,沃尔玛以及一些创业公司开发小程序后,依旧认为使用小程序原生框架是一个更高效,稳定的选择,而使用原生框架唯独缺少一个好的状态管理库,如果不引入状态管理则会让我们在模块化,项目结构以及单元测试上都有些捉襟见肘。 目前相对比较稳健的做法是针对redux或者mobx做一个adaptor应用到小程序中,但这样需要自己想办法打包引入外部库,还要想怎么去写这个adaptor,总显得有些麻烦。于是我迸发出一个想法去写一个专用于小程序的状态管理库,它使用起来足够简单并且可以通过小程序自己的npm机制安装。 目前我已经用这个开源库开发了两个电商小程序,在提高我开发效率的同时亦保证了程序的性能,所以接下来我想谈谈这背后的理念以启发更多开发者尝试新的解决方案。 基于Proxy的状态管理实现 Proxy在小程序中已经得到了足够好的支持,目前并没有发现在任何iPhone或者Android上不能使用Proxy的情况。而基于Proxy的状态管理其实也就是订阅监听的模式,一方面监听数据的变化,另一方面将这些变化传达给订阅的小程序页面。 举一个比较常见的例子,当一个用户从自己的主页进入用户编辑页面,然后更改了自己的用户名点击保存后,用户主页和用户编辑页上的用户名这时候都应该被更新。这背后的程序逻辑则是:更新这个行为将触发Proxy去通知状态管理库,然后状态管理库负责检查此时还在页面栈中的所有页面,更新订阅了用户名这个数据的页面,如下图: [图片] Part1: 监听数据变化 监听数据变化其实就是监听各个Store的属性变化,实现上就是在各个Store前面加了一层Proxy,用更直观的图片来表示就是这样: [图片] 当一个Store被观察以后,它的属性就都变成了Proxy实例,当这个属性值是Object或者Array的时候,它内部的值也会被包装成Proxy实例,这样无论多深层的数据变动都能被监听到。 而在Proxy的后面,Store的属性其实是被另一套数据(紫色部分)所维护,这套数据不负责监听,它就是纯数据,针对属性的任何变动最后都会应用到这套数据上来,它的作用是维护和返回最新的数据。 实现细节: https://github.com/wwayne/min… Part2: 页面数据绑定 因为小程序每个页面的js都是向Page中传递一个对象,这就让我们有机会包装这个对象,从而实现: 进入页面后,将页面保存在页面栈中 将来自状态管理库的数据映射到这个页面的data上来 页面退出时,将页面从页面栈中移除 实现细节: https://github.com/wwayne/min… Part3: 页面订阅更新 当数据被监听到变化后,我们需要依次做两件事,先是找到所有存储在页面栈里的页面,然后根据各个页面订阅的数据来检查变化,如果有变化就通知这些页面,从而让它们去触发setData更新页面。 实现细节:https://github.com/wwayne/min… 使用状态管理的例子 有了状态管理库,现在我们就来实现一开始举例的更新用户信息的操作,我们的文件路径如下: [代码]stores/ user.js pages/ userEdit/ index.js index.wxml [代码] 1. 首先我们创建一个Store保存用户的信息,并且监听它的变化: [代码]// stores/user.js import { observe } from 'minii' Class UserStore { constructor () { this.name = 'bob' } changeName (name) { this.name = name } } export default observe(new UserStore(), 'user') [代码] 2. 接着在我们的小程序页面订阅Store的信息 [代码]// pages/userEdit/index.js import { mapToData } from 'minii' import userStore from '../../stores/user' const connect = mapToData(state => (({ myName: state.user.name })) Page(connect({ updateNameToJames () { userStore. changeName('james') } })) [代码] 3. 完成,现在可以在页面中使用和更新数据了 [代码]// pages/userEdit/index.wxml <text>{{ myName }}</text> <button bindtap="updateNameToJames">update name to James</button> [代码] 最后 小程序因为有体积的限制,所以我希望在代码量上也尽量做到轻量和便捷,所以目前这个状态管理库并没有太多很复杂的功能,在小程序打包后所占用的体积也不到1kb,颇有点够用就好的意思。 我也已经用它开发了两款小程序,在经历了一段时间的用户使用后,我也更有信心说这个方案在小程序中是可行的。如果你有任何想法和建议,都欢迎告诉我。 项目Github: https://github.com/wwayne/minii 关于作者 Hi, 我是wwayne,是一名居住在上海的独立软件工程师,我正在开发我的新产品 talk-to-kim, 你可以在Github 或者专栏 一个人写代码找到我
2019-06-20 - Licia 支持小程序的 JS 工具库
导语 Licia 是一套在开发中实践积累起来的实用 JavaScript 工具库。该库目前拥有超过 300 个模块,同时支持浏览器、node 及小程序运行环境,提供了包括日期格式化、md5、颜色转换等实用模块,可以极大地提高开发效率。 前言 因为小程序运行的是 JavaScript 代码,传统前端所使用的 JS 库理应也能够被用在小程序中才对。然而,经过实际测试,你会发现有相当一部分 npm 包是无法直接在小程序中跑起来的。比如前端工程师十分常用的 lodash,在小程序中引入会报错。 为什么会这样? 主要原因就是绝大部分库的开发者在设计时只会考虑两种运行环境,浏览器和 node,而小程序并不会在其考虑范围内。因此,只要开发者的 JS 代码使用了只有浏览器与 node 中才有的接口,如 DOM 操作、文件读写等,该库就不能正常地运行在小程序环境中。除此之外,假如他们使用了小程序禁用的功能,例如全局变量与动态代码执行,这时候代码跑在小程序环境也会出错。 使用 使用 npm 安装 1、 安装 npm 包 [代码]npm i miniprogram-licia --save [代码] 2、点击开发者工具中的菜单栏:工具 --> 构建 npm 3、直接在代码中引入使用 [代码]const licia = require('miniprogram-licia'); licia.md5('licia'); // -> 'e59f337d85e9a467f1783fab282a41d0' licia.safeGet({a: {b: 1}}, 'a.b'); // -> 1 [代码] 生成定制化 util.js 使用 npm 包的方式会将所有功能引入到代码包中,大概会增加 100 kb 的大小。如果你只想引入所需脚本,可以使用在线工具生成定制化 util 库。 1、访问 https://licia.liriliri.io/builder.html 2、输入需要的模块名,点击生成下载 util.js。 3、将生成的工具库拷贝到小程序项目任意目录下然后直接引入使用。 [代码]const util = require('../lib/util'); util.wx.getStorage({ key: 'test' }).then(res => console.log(res.data)); [代码] 优点 1、目前拥有 270 多个模块可在小程序中正常运行,而 underscore 只有 120 个函数左右。 2、与 lodash 相比增加了不少更加实用的函数,比如 md5、atob、btoa、Emitter、dateFormat 等。 3、可以直接在小程序中引入运行,不像 lodash 需要进行一定的修改才能正常跑在小程序中。 4、定制化生成可以使用更小体积的工具库,这在限制了代码包大小的小程序中十分有用。 附录 这里只简单列出函数及其功能介绍,详细的用法请访问官网查看。 注:模块名右边有小程序图标即表明可以在小程序中使用。 Class: 创建 JavaScript 类。 Color: 颜色转换。 Dispatcher: Flux 调度器。 Emitter: 提供观察者模式的 Event emitter 类。 Enum: Enum 类实现。 JsonTransformer: JSON 转换器。 LinkedList: 双向链表实现。 Logger: 带日志级别的简单日志库。 Lru: 简单 LRU 缓存。 Promise: 轻量 Promise 实现。 PseudoMap: 类似 es6 的 Map,不支持遍历器。 Queue: 队列数据结构。 QuickLru: 不使用链表的 LRU 实现。 ReduceStore: 简单类 redux 状态管理。 Stack: 栈数据结构。 State: 简单状态机。 Store: 内存存储。 Tween: JavaScript 补间动画库。 Url: 简单 url 操作库。 Validator: 对象属性值校验。 abbrev: 计算字符串集的缩写集合。 after: 创建一个函数,只有在调用 n 次后才会调用一次。 allKeys: 获取对象的所有键名,包括自身的及继承的。 arrToMap: 将字符串列表转换为映射。 atob: window.atob,运行在 node 环境时使用 Buffer 进行模拟。 average: 获取数字的平均值。 base64: base64 编解码。 before: 创建一个函数,只能调用少于 n 次。 binarySearch: 二分查找实现。 bind: 创建一个绑定到指定对象的函数。 btoa: window.btoa,运行在 node 环境时使用 Buffer 进行模拟。 bubbleSort: 冒泡排序实现。 bytesToStr: 将字节数组转换为字符串。 callbackify: 将返回 Promise 的函数转换为使用回调的函数。 camelCase: 将字符串转换为驼峰式。 capitalize: 将字符串的第一个字符转换为大写,其余字符转换为小写。 castPath: 将值转换为属性路径数组。 centerAlign: 字符串居中。 char: 根据指定的整数返回 unicode 编码为该整数的字符。 chunk: 将数组拆分为指定长度的子数组。 clamp: 将数字限定于指定区间。 className: 合并 class。 clone: 对指定对象进行浅复制。 cloneDeep: 深复制。 cmpVersion: 比较版本号。 combine: 创建一个数组,用一个数组的值作为其键名,另一个数组的值作为其值。 compact: 返回数组的拷贝并移除其中的虚值。 compose: 将多个函数组合成一个函数。 concat: 将多个数组合并成一个数组。 contain: 检查数组中是否有指定值。 convertBase: 对数字进行进制转换。 createAssigner: 用于创建 extend,extendOwn 和 defaults 等模块。 curry: 函数柯里化。 dateFormat: 简单日期格式化。 debounce: 返回函数的防反跳版本。 decodeUriComponent: 类似 decodeURIComponent 函数,只是输入不合法时不抛出错误并尽可能地对其进行解码。 defaults: 填充对象的默认值。 define: 定义一个模块,需要跟 use 模块配合使用。 defineProp: Object.defineProperty(defineProperties) 的快捷方式。 delay: 在指定时长后执行函数。 detectBrowser: 使用 ua 检测浏览器信息。 detectMocha: 检测是否有 mocha 测试框架在运行。 detectOs: 使用 ua 检测操作系统。 difference: 创建一个数组,该数组的元素不存在于给定的其它数组中。 dotCase: 将字符串转换为点式。 each: 遍历集合中的所有元素,用每个元素当做参数调用迭代器。 easing: 缓动函数,参考 http://jqueryui.com/ 。 endWith: 检查字符串是否以指定字符串结尾。 escape: 转义 HTML 字符串,替换 &,<,>,",`,和 ’ 字符。 escapeJsStr: 转义字符串为合法的 JavaScript 字符串字面量。 escapeRegExp: 转义特殊字符用于 RegExp 构造函数。 every: 检查是否集合中的所有元素都能通过真值检测。 extend: 复制多个对象中的所有属性到目标对象上。 extendDeep: 类似 extend,但会递归进行扩展。 extendOwn: 类似 extend,但只复制自己的属性,不包括原型链上的属性。 extractBlockCmts: 从源码中提取块注释。 extractUrls: 从文本中提取 url。 fibonacci: 计算斐波那契数列中某位数字。 fileSize: 将字节数转换为易于阅读的形式。 fill: 在数组指定位置填充指定值。 filter: 遍历集合中的每个元素,返回所有通过真值检测的元素组成的数组。 find: 找到集合中第一个通过真值检测的元素。 findIdx: 返回第一个通过真值检测元素在数组中的位置。 findKey: 返回对象中第一个通过真值检测的属性键名。 findLastIdx: 同 findIdx,只是查找顺序改为从后往前。 flatten: 递归拍平数组。 fnParams: 获取函数的参数名列表。 format: 使用类似于 printf 的方式来格式化字符串。 fraction: 转换数字为分数形式。 freeze: Object.freeze 的快捷方式。 freezeDeep: 递归进行 Object.freeze。 gcd: 使用欧几里德算法求最大公约数。 getUrlParam: 获取 url 参数值。 has: 检查属性是否是对象自身的属性(原型链上的不算)。 hslToRgb: 将 hsl 格式的颜色值转换为 rgb 格式。 identity: 返回传入的第一个参数。 idxOf: 返回指定值第一次在数组中出现的位置。 indent: 对文本的每一行进行缩进处理。 inherits: 使构造函数继承另一个构造函数原型链上的方法。 insertionSort: 插入排序实现。 intersect: 计算所有数组的交集。 intersectRange: 计算两个区间的交集。 invert: 生成一个新对象,该对象的键名和键值进行调换。 isAbsoluteUrl: 检查 url 是否是绝对地址。 isArgs: 检查值是否是参数类型。 isArr: 检查值是否是数组类型。 isArrBuffer: 检查值是否是 ArrayBuffer 类型。 isArrLike: 检查值是否是类数组对象。 isBool: 检查值是否是布尔类型。 isBrowser: 检测是否运行于浏览器环境。 isClose: 检查两个数字是否近似相等。 isDataUrl: 检查字符串是否是有效的 Data Url。 isDate: 检查值是否是 Date 类型。 isEmail: 简单检查值是否是合法的邮件地址。 isEmpty: 检查值是否是空对象或空数组。 isEqual: 对两个对象进行深度比较,如果相等,返回真。 isErr: 检查值是否是 Error 类型。 isEven: 检查数字是否是偶数。 isFinite: 检查值是否是有限数字。 isFn: 检查值是否是函数。 isGeneratorFn: 检查值是否是 Generator 函数。 isInt: 检查值是否是整数。 isJson: 检查值是否是有效的 JSON。 isLeapYear: 检查年份是否是闰年。 isMap: 检查值是否是 Map 对象。 isMatch: 检查对象所有键名和键值是否在指定的对象中。 isMiniProgram: 检测是否运行于微信小程序环境中。 isMobile: 使用 ua 检测是否运行于移动端浏览器。 isNaN: 检测值是否是 NaN。 isNative: 检查值是否是原生函数。 isNil: 检查值是否是 null 或 undefined,等价于 value == null。 isNode: 检测是否运行于 node 环境中。 isNull: 检查值是否是 Null 类型。 isNum: 检测值是否是数字类型。 isNumeric: 检查值是否是数字,包括数字字符串。 isObj: 检查值是否是对象。 isOdd: 检查数字是否是奇数。 isPlainObj: 检查值是否是用 Object 构造函数创建的对象。 isPrime: 检查整数是否是质数。 isPrimitive: 检测值是否是字符串,数字,布尔值或 null。 isPromise: 检查值是否是类 promise 对象。 isRegExp: 检查值是否是正则类型。 isRelative: 检查路径是否是相对路径。 isSet: 检查值是否是 Set 类型。 isSorted: 检查数组是否有序。 isStr: 检查值是否是字符串。 isTypedArr: 检查值是否 TypedArray 类型。 isUndef: 检查值是否是 undefined。 isUrl: 简单检查值是否是有效的 url 地址。 isWeakMap: 检查值是否是 WeakMap 类型。 isWeakSet: 检查值是否是 WeakSet 类型。 kebabCase: 将字符串转换为短横线式。 keyCode: 键码键名转换。 keys: 返回包含对象自身可遍历所有键名的数组。 last: 获取数组的最后一个元素。 linkify: 将文本中的 url 地址转换为超链接。 longest: 获取数组中最长的一项。 lowerCase: 转换字符串为小写。 lpad: 对字符串进行左填充。 ltrim: 删除字符串头部指定字符或空格。 map: 对集合的每个元素调用转换函数生成与之对应的数组。 mapObj: 类似 map,但针对对象,生成一个新对象。 matcher: 传入对象返回函数,如果传入参数中包含该对象则返回真。 max: 获取数字中的最大值。 md5: MD5 算法实现。 memStorage: Web Storage 接口的纯内存实现。 memoize: 缓存函数计算结果。 mergeSort: 归并排序实现。 methods: 获取对象中所有方法名。 min: 获取数字中的最小值。 moment: 简单的类 moment.js 实现。 ms: 时长字符串与毫秒转换库。 negate: 创建一个将原函数结果取反的函数。 nextTick: 能够同时运行在 node 和浏览器端的 next tick 实现。 noop: 一个什么也不做的空函数。 normalizeHeader: 标准化 HTTP 头部名。 normalizePath: 标准化文件路径中的斜杠。 now: 获取当前时间戳。 objToStr: Object.prototype.toString 的别名。 omit: 类似 pick,但结果相反。 once: 创建只能调用一次的函数。 optimizeCb: 用于高效的函数上下文绑定。 pad: 对字符串进行左右填充。 pairs: 将对象转换为包含【键名,键值】对的数组。 parallel: 同时执行多个函数。 parseArgs: 命令行参数简单解析。 partial: 返回局部填充参数的函数,与 bind 模块相似。 pascalCase: 将字符串转换为帕斯卡式。 perfNow: 高精度时间戳。 pick: 过滤对象。 pluck: 提取数组对象中指定属性值,返回一个数组。 precision: 获取数字的精度。 promisify: 转换使用回调的异步函数,使其返回 Promise。 property: 返回一个函数,该函数返回任何传入对象的指定属性。 query: 解析序列化 url 的 query 部分。 quickSort: 快排实现。 raf: requestAnimationFrame 快捷方式。 random: 在给定区间内生成随机数。 randomItem: 随机获取数组中的某项。 range: 创建整数数组。 rc4: RC4 对称加密算法实现。 reduce: 合并多个值成一个值。 reduceRight: 类似于 reduce,只是从后往前合并。 reject: 类似 filter,但结果相反。 remove: 移除集合中所有通过真值检测的元素,返回包含所有删除元素的数组。 repeat: 重复字符串指定次数。 restArgs: 将给定序号后的参数合并成一个数组。 rgbToHsl: 将 rgb 格式的颜色值转换为 hsl 格式。 root: 根对象引用,对于 nodeJs,取 [代码]global[代码] 对象,对于浏览器,取 [代码]window[代码] 对象。 rpad: 对字符串进行右填充。 rtrim: 删除字符串尾部指定字符或空格。 safeCb: 创建回调函数,内部模块使用。 safeDel: 删除对象属性。 safeGet: 获取对象属性值,路径不存在时不报错。 safeSet: 设置对象属性值。 sample: 从集合中随机抽取部分样本。 selectionSort: 选择排序实现。 shuffle: 将数组中元素的顺序打乱。 size: 获取对象的大小或类数组元素的长度。 sleep: 使用 Promise 模拟暂停方法。 slice: 截取数组的一部分生成新数组。 snakeCase: 转换字符串为下划线式。 some: 检查集合中是否有元素通过真值检测。 sortBy: 遍历集合中的元素,将其作为参数调用函数,并以得到的结果为依据对数组进行排序。 spaceCase: 将字符串转换为空格式。 splitCase: 将不同命名式的字符串拆分成数组。 splitPath: 将路径拆分为文件夹路径,文件名和扩展名。 startWith: 检查字符串是否以指定字符串开头。 strHash: 使用 djb2 算法进行字符串哈希。 strToBytes: 将字符串转换为字节数组。 stringify: JSON 序列化,支持循环引用和函数。 stripAnsi: 清除字符串中的 ansi 控制码。 stripCmt: 清除源码中的注释。 stripColor: 清除字符串中的 ansi 颜色控制码。 stripHtmlTag: 清除字符串中的 html 标签。 sum: 计算数字和。 swap: 交换数组中的两项。 template: 将模板字符串编译成函数用于渲染。 throttle: 返回函数的节流阀版本。 timeAgo: 将时间格式化成多久之前的形式。 timeTaken: 获取函数的执行时间。 times: 调用目标函数 n 次。 toArr: 将任意值转换为数组。 toBool: 将任意值转换为布尔值。 toDate: 将任意值转换为日期类型。 toInt: 将任意值转换为整数。 toNum: 将任意值转换为数字。 toSrc: 将函数转换为源码。 toStr: 将任意值转换为字符串。 topoSort: 拓扑排序实现。 trim: 删除字符串两边指定字符或空格。 tryIt: 在 try catch 块中运行函数。 type: 获取 JavaScript 对象的内部类型。 types: 仅用于生成 ts 定义文件。 ucs2: UCS-2 编解码。 unescape: 和 escape 相反,转义 HTML 实体回去。 union: 返回传入所有数组的并集。 uniqId: 生成全局唯一 id。 unique: 返回数组去重后的副本。 unzip: 与 zip 相反。 upperCase: 转换字符串为大写。 upperFirst: 将字符串的第一个字符转换为大写。 use: 使用 define 创建的模块。 utf8: UTF-8 编解码。 values: 返回对象所有的属性值。 vlq: vlq 编解码。 waitUntil: 等待直到条件函数返回真值。 waterfall: 按顺序执行函数序列。 wrap: 将函数封装到包裹函数里面, 并把它作为第一个参数传给包裹函数。 wx: 小程序 wx 对象的 promise 版本。 zip: 将每个数组中相应位置的值合并在一起。
2019-05-07