本地存储方式

Electron的本地数据存储方式有很多种,可以直接读写文件,也可以用相关的库,方便数据管理。

经过对比选择lowdb,基于json文件数据储存方式

image-20191116092458288

点击查看对比详情

时间数据的存储方案

数据结构

最初的错误思路是按年月日的嵌套索引来存储如下,一是害怕数据量太大影响效率(后面有效率的测试结果),二方便按年月日查询数据,三是方便数据的显示和阅读;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
"timeData": {
"2019": { //年
"11": { //月
"12": [ //日
{
"start": "21:14",
"end": "21:25",
"duration": "0:11"
},
{
"start": "23:34",
"end": "23:45",
"duration": "0:11"
}
],
"13": [
{
"start": "23:34",
"end": "23:45",
"duration": "0:11"
}
]
}
}
}

为了实现上面数据的读取和添加,想了很多方法,结果发现很复杂,详见: JavaScript条件嵌套代码的可读性优化

正确的方式

后来又发现日期以字符方式存储,计算很不方便,而且完全没考虑时区的转换问题。经Google查询,正确的方式应该是存储时间的绝对值,即相对1970-00-00 00:00:00 UTC的毫秒数的时间戳,这样有以下好处:

  1. 与所有系统的时间处理方式一致
  2. 不管在全球任何地区,时间都是正确的,不用考虑时区的转换
  3. 遵循数据存储与显示分离的原则
  4. 基于整数的数据查询和计算都很方便
  5. 很容易计算时间差

数据结构如下

1
2
3
4
5
6
7
8
9
10
11
12
{
"timeData": [
{
"start": 1573865223246,
"duration": 120390
},
{
"start": 1573866723246,
"duration": 1500000
},
]
}

数据处理方法

查找给定日期的的数据

参数可以是一个日期,如:2019/11/16 查该日, 2019/11 查该月,2019/11/11 12 查该时之内的数据。

也可以传入一个时间范围数组,如:[‘2019/11/16 14:30’, ‘2019/11/19’]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
function getData(start: string | string[]): any[] {
let [b, e] = [0, 0]
if (typeof start === 'string') {
//* 传入为日期字符
if (start.length === 4) start = start + '/' //末尾加 /,否则只Date.parse('2019')会按本地时间转换而多出8小时
if (start.length === 13) start = start + ':' //末尾加 :,否则Date.parse('2019/11/11 12')会报错

let startTime = new Date(start)
let add1: any = {
19: () => +startTime + 1000, //加1秒
16: () => +startTime + 60 * 1000, //加1分钟
14: () => +startTime + 60 * 60 * 1000, //加1小时
10: () => +startTime + 24 * 3600 * 1000, //加1天
7: () => +startTime.setMonth(startTime.getMonth() + 1), //加1月
5: () => +startTime.setFullYear(startTime.getFullYear() + 1), //加1年
}
;[b, e] = [+startTime, add1[start.length]() - 1] //得到给定的年/月/日的首尾时间,尾时间通过+1个单位时间, 再-1毫秒得到
} else {
//* 传入为时间段数组
if (start[0].length === 4) start[0] = start[0] + '/'
if (start[1].length === 4) start[1] = start[1] + '/'
;[b, e] = [Date.parse(start[0]), Date.parse(start[1])]
}
log('getData start', dayjs(b).format('YYYY-MM-DD HH:mm:ss'), b, 'end', dayjs(e).format('YYYY-MM-DD HH:mm:ss'), e)

return get('timeData').filter((item: any) => item.start >= b && item.start <= e)
}

添加数据

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 添加数据
*
* @param {number} start 格式必须为时间戳,如:1577808000000
* @param {number} end 同上
*/
function addTime(start: number, end: number) {
log('addTime start', start, 'end', end)
get('timeData').push({
start: start,
duration: end - start,
})
save()
}

时间显示插件

这里推荐一个查看unix时间戳的具体时间的插件:Time Converter

Hover_Sample

大量数据效率测试

不想看过程的可以直接跳到最后看结论

测试条件

系统:MacBook pro MacOS 14

内存:8G

json数据库文件大小:74MB

数据形式如下,数据量为100万个对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"timeData": [
{
"start": 1573864067907,
"duration": 0
},
{
"start": 1573865567907,
"duration": 1500000
},
.
.
.
{
"start": 1573867067907,
"duration": 3000000
},
{
"start": 1573868567907,
"duration": 4500000
}
]
}

数据生成和写入测试

生成100万条数据,代码如下:

1
2
3
4
5
6
7
8
9
10
11
let d = db.get('timeData')
log('开始')
for (let index = 0; index < 1000000; index++) {
let a = index * 25 * 60000
d.push({
start: Date.now() + a,
duration: a,
})
}
db.save()
log('结束')

结果如下,过程耗时1730毫秒

image-20191116084921194

数据读取和处理测试

遍历100万条数据计算某一天的时段之和,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
db.loadDb(Path.join(DB_PATH, '/db.json'))
let [b, e] = [Date.parse('2019/11/16'), Date.parse('2019/11/17') - 1],
wTime = 0
log('1')
db.get('timeData1').forEach((item: any) => {
if (item.start > b && item.start < e) {
log('start', new Date(item.start).toLocaleString('zh', { hour12: false }), 'duration', item.duration, 'wTime', wTime)

wTime += item.duration
}
})
log(new Date(wTime).toLocaleString('zh', { hour12: false }))

结果如下,遍历查询和处理的过程只花了83毫秒,还是相当快的

image-20191116083911495

结论:

lowdb的本地读写效率较高,可以轻松应对百万量级的数据,一百万的数据量,74MB的json文件数据,写入时间为1730毫秒,遍历读取时间仅需83毫秒。