├── .eslintrc.js ├── LICENSE ├── README.md ├── localDB.js └── localDB.min.js /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // eslint 检查配置 2 | module.exports = { 3 | "env": { 4 | "browser": true, 5 | "commonjs": true, 6 | "es6": true 7 | }, 8 | "extends": ["eslint:recommended"], 9 | "globals": { 10 | "wx": "readonly" 11 | }, 12 | "parser": "babel-eslint", 13 | "rules": { 14 | "no-console": ["error", { "allow": ["warn", "error"] }] 15 | } 16 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 jin-yufeng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MpLocalDB 2 | 3 | > 微信小程序本地数据库 4 | 5 | ## 使用方法 ## 6 | 1. 复制 `localDB.js`(`7.97KB`,`min` 版本 `4.26KB`)到 `utils` 目录下 7 | 2. 在需要使用的页面的 `js` 文件中添加 8 | 9 | ```javascript 10 | const localDB = require('utils/localDB.js') 11 | const _ = localDB.command 12 | ``` 13 | 14 | ## 示例程序 ## 15 | ```javascript 16 | const localDB = require('utils/localDB.js') 17 | const _ = localDB.command 18 | localDB.init() // 初始化 19 | var articles = localDB.collection('articles') 20 | if(!articles) 21 | articles = localDB.createCollection('articles') // 不存在则先创建 22 | // 按文章 id 查找 23 | var doc = articles.doc('xxx') 24 | if(doc) { 25 | var data = doc.get() // 取得数据 26 | } else { 27 | // 网络请求获取 data 28 | data._timeout = Date.now() + 15 * 24 * 3600000 // 设置过期时间为 15 天 29 | articles.add(data) // 添加到本地数据库 30 | } 31 | // 按类型查找 32 | var data = articles.where({ 33 | type: 'xxx' 34 | }).get() 35 | // 正则查找 36 | var data = articles.where({ 37 | title: /xxx/ // 标题中含有 xxx 的 38 | }).get() 39 | // 分页查找 40 | var page2 = articles.skip(10).limit(10).get() 41 | // 按时间查找 42 | var data = articles.where({ 43 | date: _.gte('20200501').and(_.lte('20200510')) // 大于等于 20200501 小于等于 20200510 44 | }).get() 45 | // 结果排序 46 | var data = articles.orderBy('date', 'desc').get() // 按日期降序排序 47 | // 清理过期数据 48 | articles.where({ 49 | _timeout: _.lt(Date.now()) // 过期时间早于当前的 50 | }).remove() 51 | ``` 52 | 53 | ## api ## 54 | ### db ### 55 | 56 | | 名称 | 输入值 | 返回值 | 功能 | 57 | |:---:|:---:|:---:|:---:| 58 | | init | / | / | 初始化数据库 | 59 | | collection | name | Collection | 获取名称为 name 的集合 | 60 | | createCollection | name | Collection | 创建一个名称为 name 的集合 | 61 | | removeCollection | name | / | 移除名称为 name 的集合 | 62 | 63 | ### collection ### 64 | 65 | | 名称 | 输入值 | 返回值 | 功能 | 66 | |:---:|:---:|:---:|:---:| 67 | | add | data | id | 向集合中添加一条数据 | 68 | | count | / | number | 统计匹配查询条件的记录的条数 | 69 | | doc | id | document | 获取一条记录 | 70 | | get | / | array | 获取集合数据 | 71 | | limit | number | collection | 指定查询结果集数量上限 | 72 | | orderBy | field, order | collection | 指定查询排序条件 | 73 | | remove | / | / | 删除多条数据 | 74 | | skip | number | collection | 指定查询返回结果时从指定序列后的结果开始返回 | 75 | | update | newVal | / | 更新多条数据 | 76 | | where | query | collection | 进行条件查询 | 77 | 78 | 附:`limit` 和 `skip` 仅对 `get` 有效 79 | 80 | ### document ### 81 | 82 | | 名称 | 输入值 | 返回值 | 功能 | 83 | |:---:|:---:|:---:|:---:| 84 | | get | / | data | 获取记录数据 | 85 | | remove | / | / | 删除该记录 | 86 | | update | newVal | / | 更新记录数据 | 87 | 88 | ### command ### 89 | 90 | 查询指令: 91 | 92 | | 名称 | 功能 | 93 | |:---:|:---:| 94 | | eq | 等于 | 95 | | neq | 不等于 | 96 | | lt | 小于 | 97 | | lte | 小于或等于 | 98 | | gt | 大于 | 99 | | gte | 大于或等于 | 100 | | in | 字段值在给定数组中 | 101 | | nin | 字段值不在给定数组中 | 102 | | exists | 判断字段是否存在 | 103 | | or | 多字段或查询 | 104 | 105 | 单个字段的条件之间还可以通过 `or` 和 `and` 进行组合,如 106 | ```javascript 107 | _.gt(30).and(_.lt(70)) // 大于 30 且小于 70 108 | _.eq(0).or(_.eq(100)) // 等于 0 或等于 100 109 | ``` 110 | 111 | `or` 指令用于多字段或查询(默认是与查询) 112 | ```javascript 113 | // 查询 collection 表中 a 字段为 1 或 b 字段为 2 的记录 114 | collection.where(_.or([{ 115 | a: 1 116 | }, { 117 | b: 2 118 | }])) 119 | ``` 120 | 121 | 更新指令: 122 | 123 | | 名称 | 功能 | 124 | |:---:|:---:| 125 | | set | 设置字段为指定值 | 126 | | remove | 删除字段 | 127 | | inc | 原子自增字段值 | 128 | | mul | 原子自乘字段值 | 129 | | push | 如字段值为数组,往数组尾部增加指定值 | 130 | | pop | 如字段值为数组,从数组尾部删除一个元素 | 131 | | shift | 如字段值为数组,从数组头部删除一个元素 | 132 | | unshift | 如字段值为数组,往数组头部增加指定值 | 133 | 134 | ## 注意事项 ## 135 | 1. 数据库存储在本地 `storage` 中,账号、设备之间 **存在隔离**;最大大小为 `10MB`;**请勿覆盖或删除** `key` 为 `localDB` 的 `storage`,否则可能造成数据丢失 136 | 2. 使用前 **必须调用** `db.init` 方法(从 `storage` 中读取保存的数据,数据量较大的时候,需要选择一个合适的时机进行载入) 137 | 3. 所有数据都在内存中,存取都较快,因此所有方法 **均为同步方法**,不返回 `Promise` 138 | 4. 所有操作 **不可撤销和恢复**,尤其是 `remove` 方法需谨慎调用 139 | 5. 集合名和一个集合内的 `_id`(可自动生成)**不可重复**,否则将无法创建 140 | 6. 方法设置参考了云数据库的操作,关于各方法的详细信息可以直接参考 [云数据库的文档](https://developers.weixin.qq.com/miniprogram/dev/wxcloud/reference-sdk-api/Cloud.database.html) 141 | 142 | ## 更新日志 ## 143 | - 2020.7.9 144 | 1. `F` 修复了 `remove` 后执行 `where` 可能出错的问题 145 | 146 | - 2020.6.30 147 | 1. `A` 增加 `or` 指令,可以实现多字段或查询 148 | 149 | - 2020.5.13 150 | 1. `U` 支持多字段排序(设置多个 `orderBy`) 151 | 2. `U` 同时设置 `orderBy` 和 `skip`、`limit` 时将先进行排序再执行 `skip` 和 `limit` 152 | 153 | - 2020.5.9 154 | 1. `A` 添加了 `count` 方法 -------------------------------------------------------------------------------- /localDB.js: -------------------------------------------------------------------------------- 1 | /** 2 | * 小程序本地数据库 3 | * @tutorial https://github.com/jin-yufeng/MpLocalDB 4 | * @author JinYufeng 5 | * @listens MIT 6 | */ 7 | var localDB, _dirty = false 8 | // 异步写回本地缓存 9 | function _write() { 10 | _dirty = true 11 | setTimeout(() => { 12 | if (_dirty) { 13 | _dirty = false 14 | wx.setStorage({ 15 | data: localDB, 16 | key: 'localDB' 17 | }) 18 | } 19 | }, 200) 20 | } 21 | // 更新记录 22 | function _update(item, update) { 23 | for (var key in update) 24 | item[key] = typeof update[key] == "function" ? update[key](item[key]) : update[key] 25 | } 26 | 27 | // 集合 28 | function Collection(data, root, options) { 29 | this.data = data 30 | this.options = options || { 31 | orderBy: [] 32 | } 33 | this.root = root || data 34 | } 35 | /** 36 | * 添加一条记录 37 | * @param {Object} data 要添加的数据 38 | * @returns {String} 创建成功返回记录的 id,否则返回 null 39 | */ 40 | Collection.prototype.add = function (data) { 41 | var id = data._id || '' 42 | if (typeof data != 'object' || this.data[id]) return null 43 | data = JSON.parse(JSON.stringify(data)) 44 | data._id = void 0 45 | while (!id || this.data[id]) { 46 | var map = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789' 47 | for (var i = 0; i < 4; i++) 48 | id += map[parseInt(Math.random() * 62)] 49 | } 50 | this.data[id] = data 51 | _write() 52 | return id 53 | } 54 | /** 55 | * 获取一条记录 56 | * @param {String} id 要获取记录的 id 57 | * @returns {Object} 返回一个 document 对象 58 | */ 59 | Collection.prototype.doc = function (id) { 60 | var data = this.data[id] 61 | return { 62 | get: () => { 63 | var res = JSON.parse(JSON.stringify(data)) 64 | res._id = id 65 | return res 66 | }, 67 | update: (newVal) => { 68 | if (typeof newVal != 'object') return false 69 | _update(data, newVal) 70 | _write() 71 | return true 72 | }, 73 | remove: () => (this.data[id] = void 0, true) 74 | } 75 | } 76 | /** 77 | * 查询记录 78 | * @param {Object} obj 查询条件 79 | * @returns 返回找到的集合 80 | */ 81 | Collection.prototype.where = function (obj) { 82 | const query = (obj, key) => { 83 | for (var item in obj) { 84 | var val = item == '_id' ? key : this.data[key][item] 85 | if (obj[item] instanceof Command || obj[item] instanceof RegExp) { 86 | if (!obj[item].exec(val)) 87 | return false 88 | } else if (obj[item] != val) 89 | return false 90 | } 91 | return true 92 | } 93 | var res = {} 94 | for (var key in this.data) { 95 | if (!this.data[key]) continue 96 | if (obj.type == 'or') { 97 | for (var i = 0; i < obj.arr.length; i++) { 98 | if (query(obj.arr[i], key)) { 99 | res[key] = this.data[key] 100 | break 101 | } 102 | } 103 | } else if (query(obj, key)) 104 | res[key] = this.data[key] 105 | } 106 | return new Collection(res, this.root, this.options) 107 | } 108 | /** 109 | * 指定查询结果集数量上限 110 | * @param {Number} limit 数量上限 111 | * @returns {Collection} 返回设置后的集合 112 | */ 113 | Collection.prototype.limit = function (limit) { 114 | var options = JSON.parse(JSON.stringify(this.options)) 115 | options.limit = limit 116 | return new Collection(this.data, this.root, options) 117 | } 118 | /** 119 | * 指定查询返回结果时从指定序列后的结果开始返回 120 | * @param {Number} skip 开始返回的位置 121 | * @returns {Collection} 返回设置后的集合 122 | */ 123 | Collection.prototype.skip = function (skip) { 124 | var options = JSON.parse(JSON.stringify(this.options)) 125 | options.skip = skip 126 | return new Collection(this.data, this.root, options) 127 | } 128 | /** 129 | * 指定查询排序条件 130 | * @param {String} field 排序字段 131 | * @param {('asc'|'desc')} order 升序或降序 132 | * @returns {Collection} 返回设置后的集合 133 | */ 134 | Collection.prototype.orderBy = function (field, order = 'asc') { 135 | var options = JSON.parse(JSON.stringify(this.options)) 136 | options.orderBy.push({ 137 | field, 138 | order 139 | }) 140 | return new Collection(this.data, this.root, options) 141 | } 142 | /** 143 | * 统计匹配查询条件的记录的条数 144 | * @returns {Number} 记录的条数 145 | */ 146 | Collection.prototype.count = function () { 147 | var count = 0 148 | for (var key in this.data) 149 | if (this.data[key]) count++ 150 | return count 151 | } 152 | /** 153 | * 获取集合数据 154 | * @returns {Array} 集合数据 155 | */ 156 | Collection.prototype.get = function () { 157 | var res = [], 158 | add = key => { 159 | var item = this.data[key] 160 | if (item) { 161 | item = JSON.parse(JSON.stringify(item)) 162 | item._id = key 163 | res.push(item) 164 | } 165 | } 166 | if (this.options.orderBy.length) { 167 | for (let key in this.data) 168 | add(key) 169 | res.sort((a, b) => { 170 | for (var i = 0, item; 171 | (item = this.options.orderBy[i]); i++) 172 | if (a[item.field] != b[item.field]) 173 | return (item.order == 'desc' ? -1 : 1) * (a[item.field] - b[item.field]) 174 | return 0 175 | }) 176 | if (this.options.skip) res = res.slice(this.options.skip) 177 | if (this.options.limit) res = res.slice(0, this.options.limit) 178 | } else { 179 | var i 180 | for (let key in this.data) { 181 | if (!this.options.skip || i >= this.options.skip) { 182 | add(key) 183 | if (i + 1 - (this.options.skip || 0) == this.options.limit) break 184 | } 185 | i++ 186 | } 187 | } 188 | return res 189 | } 190 | /** 191 | * 更新多条记录 192 | * @param {Object} newVal 更新内容 193 | */ 194 | Collection.prototype.update = function (newVal) { 195 | if (typeof newVal != 'object') return false 196 | for (var key in this.data) 197 | _update(this.data[key], newVal) 198 | _write() 199 | return true 200 | } 201 | /** 202 | * 删除多条记录 203 | */ 204 | Collection.prototype.remove = function () { 205 | for (var key in this.data) 206 | this.root[key] = void 0 207 | _write() 208 | } 209 | 210 | // 查询指令 211 | function Command(func) { 212 | this.exec = func 213 | } 214 | /** 215 | * 与查询 216 | * @param {Command} 下一条查询指令 217 | * @returns {Command} 与查询指令 218 | */ 219 | Command.prototype.and = function (cmd) { 220 | return new Command(val => this.exec(val) && cmd.exec(val)) 221 | } 222 | /** 223 | * 或查询 224 | * @param {Command} 下一条查询指令 225 | * @returns {Command} 或查询指令 226 | */ 227 | Command.prototype.or = function (cmd) { 228 | return new Command(val => this.exec(val) || cmd.exec(val)) 229 | } 230 | 231 | module.exports = { 232 | /** 233 | * 初始化数据库 234 | */ 235 | init() { 236 | if (!localDB) 237 | localDB = wx.getStorageSync('localDB') || {} 238 | }, 239 | /** 240 | * 创建一个集合 241 | * @param {String} name 要创建的集合名称 242 | * @returns {Collection} 创建成功返回集合对象,否则返回 false 243 | */ 244 | createCollection(name) { 245 | if (!localDB) return console.warn('请先初始化'), false 246 | if (localDB[name]) return false 247 | localDB[name] = {} 248 | _write() 249 | return new Collection(localDB[name]) 250 | }, 251 | /** 252 | * 移除一个集合 253 | * @param {String} name 要移除的集合名称 254 | */ 255 | removeCollection(name) { 256 | if (!localDB) return console.warn('请先初始化') 257 | localDB[name] = void 0 258 | _write() 259 | }, 260 | /** 261 | * 获取一个集合对象 262 | * @param {String} name 集合名称 263 | * @returns {Collection} 存在则返回集合对象,否则返回 null 264 | */ 265 | collection(name) { 266 | if (!localDB) return console.warn('请先初始化'), false 267 | return localDB[name] ? new Collection(localDB[name]) : null 268 | }, 269 | command: { 270 | // 查询指令 271 | eq: query => new Command(val => val == query), 272 | neq: query => new Command(val => val != query), 273 | lt: query => new Command(val => val < query), 274 | lte: query => new Command(val => val <= query), 275 | gt: query => new Command(val => val > query), 276 | gte: query => new Command(val => val >= query), 277 | in: query => new Command(val => query.includes(val)), 278 | nin: query => new Command(val => !query.includes(val)), 279 | exists: query => new Command(val => val == void 0 ? !query : query), 280 | or: (...arr) => ({ 281 | type: 'or', 282 | arr: arr[0] instanceof Array ? arr[0] : arr 283 | }), 284 | // 更新指令 285 | set: val => JSON.parse(JSON.stringify(val)), 286 | remove: () => void 0, 287 | inc: diff => (val => val += diff), 288 | mul: diff => (val => val *= diff), 289 | push: diff => (val => (val.push(diff), val)), 290 | pop: () => (val => (val.pop(), val)), 291 | shift: () => (val => (val.shift(), val)), 292 | unshift: diff => (val => (val.unshift(diff), val)) 293 | } 294 | } -------------------------------------------------------------------------------- /localDB.min.js: -------------------------------------------------------------------------------- 1 | // 小程序本地数据库 https://github.com/jin-yufeng/MpLocalDB 2 | function t(){e=!0,setTimeout(function(){e&&(e=!1,wx.setStorage({data:i,key:"localDB"}))},200)}function n(t,n){for(var r in n)t[r]="function"==typeof n[r]?n[r](t[r]):n[r]}function r(t,n,r){this.data=t,this.options=r||{orderBy:[]},this.root=n||t}function o(t){this.exec=t}var i,e=!1;r.prototype.add=function(n){var r=n._id||"";if("object"!=(void 0===n?"undefined":typeof(n))||this.data[r])return null;for(n=JSON.parse(JSON.stringify(n)),n._id=void 0;!r||this.data[r];)for(var o=0;o<4;o++)r+="abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"[parseInt(62*Math.random())];return this.data[r]=n,t(),r},r.prototype.doc=function(r){var o=this,i=this.data[r];return{get:function(){var t=JSON.parse(JSON.stringify(i));return t._id=r,t},update:function(r){return"object"==(void 0===r?"undefined":typeof(r))&&(n(i,r),t(),!0)},remove:function(){return o.data[r]=void 0,!0}}},r.prototype.where=function(t){var n=this,i=function(t,r){for(var i in t){var e="_id"==i?r:n.data[r][i];if(t[i]instanceof o||t[i]instanceof RegExp){if(!t[i].exec(e))return!1}else if(t[i]!=e)return!1}return!0},e={};for(var u in this.data)if(this.data[u])if("or"==t.type){for(var f=0;f1&&void 0!==arguments[1]?arguments[1]:"asc",o=JSON.parse(JSON.stringify(this.options));return o.orderBy.push({field:t,order:n}),new r(this.data,this.root,o)},r.prototype.count=function(){var t=0;for(var n in this.data)this.data[n]&&t++;return t},r.prototype.get=function(){var t=this,n=[],r=function(r){var o=t.data[r];o&&(o=JSON.parse(JSON.stringify(o)),o._id=r,n.push(o))};if(this.options.orderBy.length){for(var o in this.data)r(o);n.sort(function(n,r){for(var o,i=0;(o=t.options.orderBy[i]);i++)if(n[o.field]!=r[o.field])return("desc"==o.order?-1:1)*(n[o.field]-r[o.field]);return 0}),this.options.skip&&(n=n.slice(this.options.skip)),this.options.limit&&(n=n.slice(0,this.options.limit))}else{var i;for(var e in this.data){if((!this.options.skip||i>=this.options.skip)&&(r(e),i+1-(this.options.skip||0)==this.options.limit))break;i++}}return n},r.prototype.update=function(r){if("object"!=(void 0===r?"undefined":typeof(r)))return!1;for(var o in this.data)n(this.data[o],r);return t(),!0},r.prototype.remove=function(){for(var n in this.data)this.root[n]=void 0;t()},o.prototype.and=function(t){var n=this;return new o(function(r){return n.exec(r)&&t.exec(r)})},o.prototype.or=function(t){var n=this;return new o(function(r){return n.exec(r)||t.exec(r)})},module.exports={init:function(){i||(i=wx.getStorageSync("localDB")||{})},createCollection:function(n){return i?!i[n]&&(i[n]={},t(),new r(i[n])):(console.warn("请先初始化"),!1)},removeCollection:function(n){if(!i)return console.warn("请先初始化");i[n]=void 0,t()},collection:function(t){return i?i[t]?new r(i[t]):null:(console.warn("请先初始化"),!1)},command:{eq:function(t){return new o(function(n){return n==t})},neq:function(t){return new o(function(n){return n!=t})},lt:function(t){return new o(function(n){return nt})},gte:function(t){return new o(function(n){return n>=t})},in:function(t){return new o(function(n){return t.includes(n)})},nin:function(t){return new o(function(n){return!t.includes(n)})},exists:function(t){return new o(function(n){return void 0==n?!t:t})},or:function(){for(var t=arguments.length,n=Array(t),r=0;r