没有后端接口如何自建前端Mock体系?——基于 IndexedDB的数据存储方案

1,450 阅读7分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第5天,点击查看活动详情

一、前言

IndexedDB是一种底层API,用于在客户端存储大量的结构化数据(也包括文件/二进制大型对象(blobs))。该API使用索引实现对数据的高性能搜索。IndexedDB是一个事务型数据库系统,也是一个基于JS的面向对象数据库,它提供了类似数据库风格的数据存储和使用方式,我们只需要指定数据库模式,打开与数据库的连接,然后检索和更新一系列事务。

二、解决的问题

前端同学在开发中常常会因为迟迟没有等到后端提供的接口而感到苦恼,因此急需一套可用的Mock体系。

我们可以将自建Mock体系理解为,前端可以直接操作数据库里的数据,比如CRUD操作,经过基于IndexedDB数据库封装好的接口会返回一个promise对象,我们就像使用Axios请求后端接口一样去请求Mock接口。

创建本地Mock接口,这样即使在日常开发中,可以不完全依赖于后端接口,需要什么接口,直接使用IndexedDB自行封装就行。

我们可以提前根据后端开发者提供的接口文档,来自行开发一套Mock接口,供前端项目来调用,需要调用什么接口,直接使用IndexedDB自行封装,不需要建立http连接、处理跨域、联调等等。等到自测或者项目发布测试阶段,直接将Mock接口替换为真实的线上接口就可以了,这样不仅提高了前后端开发的效率,也使得前后端在某种程度上做到了解耦。

三、基本概念

基于以上,本文仅围绕如何“自建前端Mock体系”,学习IndexedDB一些基本的概念和API:

  • 数据库:存储数据的地方,每个域名可以创建多个数据库
  • 对象仓库:也就是objectStore,每个数据库包含若干个对象仓库
  • 索引: 可以为对象仓库中的属性创建对应的索引,并且根据索引来查询数据,一般索引和索引的属性一致
  • 事务: 数据库里增删查改操作都是通过事务(transaction)来完成
  • 数据记录: 每一条数据都是一条记录,有对应的key、value、主键、索引等属性

四、基本操作

  • 创建数据库连接
  • 创建objectStore
  • 创建一些索引
  • 通过事务来进行数据库操作

示例

如下图所示,localhost:3000本地域名下有一个名为elephant的数据库,当前版本号version为2,当前有一个存储对象,该存储对象包含两个索引nose和ear

image.png

如下图所示,存储对象elephant的主键(keypath)为id,elephant中存在两条数据,分别为id为1和3的数据,数据是以对象的形式存储。

image.png

五、TS封装实现

下面我们用TypeScript手动封装一个DB类的实现,用来操作indexedDB。包括增删查改的工具类,以及使用Promise async/await包装异步事务自建一套本地Mock体系。

数据库与对象仓库的创建

export default class DB{
    private dbName: string
    constructor(dbName:string) {
        this.dbName=dbName
    }
    //打开数据库
    public openStore(storeName:string,keyPath:string,indexs?:Array<string>) {
        const request = window.indexedDB.open(this.dbName, 1) //open方法第一个参数为数据库名称,第二个参数为版本
        //打开数据库成功的回调
        request.onsuccess = (event)=>{
            console.log('数据库打开成功')
        }
        //打开数据库失败的回调
        request.onerror = (event) => {
            console.log('数据库打开失败')
        }
        //数据库版本升级的回调
        request.onupgradeneeded = (event) => {
            console.log('数据库升级成功')
            const { result }: any = event.target

            //根据storeName创建对象仓库并设置索引自增,主键 
            const store = result.createObjectStore(storeName, { autoIncrement: true, keyPath }) 

            //遍历为对象仓库中的属性创建对应的索引,索引和索引的属性一致
            indexs?.map((v:string) => {
                store.createIndex(v,v,{unique:true})
            })

            //对象仓库创建成功的回调
            store.transaction.oncomplete = (event:any) => {
                console.log('创建对象仓库成功')
            }
        }
    }
}

执行openStore()方法在Chrome调试工具中可以看到如下结果:

image.png

image.png

完善增删查改功能

export default class DB{
    private dbName: string //数据库名称
    private db: any //数据库对象
    
    constructor(dbName:string) {
        this.dbName=dbName
    }
    //打开数据库
    public openStore(storeName:string,keyPath:string,indexs?:Array<string>) {
        const request = window.indexedDB.open(this.dbName, 1) //open方法第一个参数为数据库名称,第二个参数为版本
        //打开数据库成功的回调
        request.onsuccess = (event:any)=>{
            console.log('数据库打开成功')
            this.db = event.target.result
        }
        //打开数据库失败的回调
        request.onerror = (event) => {
            console.log('数据库打开失败')
        }
        //数据库版本升级的回调
        request.onupgradeneeded = (event) => {
            console.log('数据库升级成功')
            const { result }: any = event.target

            //根据storeName创建对象仓库并设置索引自增,主键 
            const store = result.createObjectStore(storeName, { autoIncrement: true, keyPath }) 

            //遍历为对象仓库中的属性创建对应的索引,索引和索引的属性一致
            indexs?.map((v:string) => {
                store.createIndex(v,v,{unique:false})
            })

            //对象仓库创建成功的回调
            store.transaction.oncomplete = (event:any) => {
                console.log('创建对象仓库成功')
            }
        }
    }

    // 新增/修改数据库数据
    public updateItem(storeName: string, data: any) {
        const store=this.db.transaction([storeName], 'readwrite').objectStore(storeName)
        const request = store.put({
            ...data,
            updateTime: new Date().getTime()
        })
        request.onsuccess = (event:any) => {
            console.log('数据写入成功')
        }
        request.onerror = (event: any) => {
            console.log('数据写入失败')
        }
    }

    //删除数据(根据主键删除)
    public deleteItem(storeName:string,key:string | number) {
        const store=this.db.transaction([storeName], 'readwrite').objectStore(storeName)
        const request = store.delete(key)
        request.onsuccess = (event:any) => {
            console.log('数据删除成功')
        }
        request.onerror = (event: any) => {
            console.log('数据删除失败')
        }
    }

    //查询所有数据
    public getList(storeName:string | number) {
        const store=this.db.transaction([storeName], 'readwrite').objectStore(storeName)
        const request = store.getAll()
        request.onsuccess = (event:any) => {
            console.log('查询所有数据成功')
        }
        request.onerror = (event: any) => {
            console.log('查询所有数据失败')
        }
    }

    //查询某一条数据
    public getItem(storeName:string,key:string | number) {
        const store=this.db.transaction([storeName], 'readwrite').objectStore(storeName)
        const request = store.get(key)
        request.onsuccess = (event:any) => {
            console.log('查询一条数据成功')
        }
        request.onerror = (event: any) => {
            console.log('查询一条数据失败')
        }
    }
}

封装异步事务

IndexedDB中的事务是异步的,因此我们可以使用Promise async/await来封装异步事务,将异步变为"同步"

indexDB.ts

export default class DB{
    private dbName: string //数据库名称
    private db: any //数据库对象
    
    constructor(dbName:string) {
        this.dbName=dbName
    }
    //打开数据库
    public openStore(storeName: string, keyPath: string, indexs?: Array<string>) {
        return new Promise((resolve, reject) => {
            const request = window.indexedDB.open(this.dbName, 1) //open方法第一个参数为数据库名称,第二个参数为版本
            //打开数据库成功的回调
            request.onsuccess = (event:any)=>{
                console.log('数据库打开成功')
                this.db = event.target.result
                resolve(true)
            }
            //打开数据库失败的回调
            request.onerror = (event) => {
                console.log('数据库打开失败')
                reject(event)
            }
            //数据库版本升级的回调
            request.onupgradeneeded = (event) => {
                console.log('数据库升级成功')
                const { result }: any = event.target
    
                //根据storeName创建对象仓库并设置索引自增,主键 
                const store = result.createObjectStore(storeName, { autoIncrement: true, keyPath }) 
    
                //遍历为对象仓库中的属性创建对应的索引,索引和索引的属性一致
                indexs?.map((v:string) => {
                    store.createIndex(v,v,{unique:false})
                })
    
                //对象仓库创建成功的回调
                store.transaction.oncomplete = (event:any) => {
                    console.log('创建对象仓库成功')
                }
            }
        })
    }

    // 新增/修改数据库数据
    public updateItem(storeName: string, data: any) {
        return new Promise((resolve, reject) => {
            const store=this.db.transaction([storeName], 'readwrite').objectStore(storeName)
            const request = store.put({
                ...data,
                updateTime: new Date().getTime()
            })
            request.onsuccess = (event:any) => {
                console.log('数据写入成功')
                resolve(true)
            }
            request.onerror = (event: any) => {
                console.log('数据写入失败')
                reject(event)
            }
        })
    }

    //删除数据(根据主键删除)
    public deleteItem(storeName: string, key: string | number) {
        return new Promise((resolve, reject) => {
            const store=this.db.transaction([storeName], 'readwrite').objectStore(storeName)
            const request = store.delete(key)
            request.onsuccess = (event:any) => {
                console.log('数据删除成功')
                resolve(true)
            }
            request.onerror = (event: any) => {
                console.log('数据删除失败')
                reject('数据删除失败')
            }
        })
    }

    //查询所有数据
    public getList(storeName: string | number) {
        return new Promise((resolve, reject) => {
            const store=this.db.transaction([storeName], 'readwrite').objectStore(storeName)
            const request = store.getAll()
            request.onsuccess = (event:any) => {
                console.log('查询所有数据成功')
                resolve(event.target.result)
            }
            request.onerror = (event: any) => {
                console.log('查询所有数据失败')
                reject('查询所有数据失败')
            }
        })     
    }

    //查询某一条数据
    public getItem(storeName: string, key: string | number) {
        return new Promise((resolve, reject) => {
            const store=this.db.transaction([storeName], 'readwrite').objectStore(storeName)
            const request = store.get(key)
            request.onsuccess = (event:any) => {
                console.log('查询一条数据成功')
            }
            request.onerror = (event: any) => {
                console.log('查询一条数据失败')
                reject('查询一条数据失败')
            }
        })     
    }
}

api.ts

import IndexedDB from '../utils/indexedDB'
const airbnbDB=new IndexedDB('airbnb')

//Mock接口
export async function fetchElephant() {
    await airbnbDB.openStore('elephant', 'id', ['nose', 'ear'])
    const result = await airbnbDB.getList('elephant').then(res => {
        return  {
            code: '200',
            message: '操作成功',
            result: res,
            success:true
        }
    })
    console.log('Mock接口fetchElephant:result--->',result)
}

image.png

总结

Mock体系构建步骤

  • 将本地浏览器的indexedDB作为前端获取数据的源头
  • 将indexedDB仓库中的数据直接封装成各种接口,以供前端掉用
  • 各个mock接口行成闭环,相当于一个小型的admin后台管理系统,每条数据都有迹可循
  • 自给自足,不借助后端接口也能开发调试,后期整体替换成真实接口,与后端完全解耦