利用indexDB带你手把手构建一个前端数据库体系

989 阅读7分钟

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第7篇文章,点击查看活动详情

很多时候我们需要把自己做的项目展示给别人看,但是一个完整的项目是需要后端提供数据存储支持的,作为一个前端程序员大多数不具备后端开发的能力,或者没必要花时间和精力来专门搞一个后台服务。因此,能不能利用浏览器提供的 indexDB 来搭建一套数据存储的能力呢?

答案是可以的。 下面我将会带大家从0到1搭建这样一套前端的 MOCK 体系,摆脱对后台的依赖,让前端直接起飞。

什么是 IndexDB ?

IndexDB 是一个运行在浏览器上的非关系型数据库。既然是数据库了,那就不是 5M、10M 这样小打小闹级别了。理论上来说,IndexDB 理论上是没有存储上限的一般来说不会小于 250M。它不仅可以存储字符串,还可以存储二进制数据。

为了后面能看懂代码,这里对 IndexDB 的 api 做一个简要的介绍。

打开数据库

// 打开我们的数据库 使用open方法
let request = window.indexedDB.open("MyTestDatabase");

// 成功
request.onsuccess = (event: any) => {
    console.log('数据库打开成功')
}

// 错误
request.onerror = function (event) {
    console.log("error", event);
}

除了成功和错误的事件之外,还有一个 onupgradeneeded 事件, 它表示当你创建一个新的数据库或者增加已存在的数据库的版本号时会被触发。

indexDB第二次参数就是版本号,默认是1,让你写2时就会触发onupgradeneeded事件。

window.indexedDB.open(this.dbName, 2)

创建对象仓库

createObjectStore

这个 api 是为数据库创建一个对象仓库,所谓对象仓库你可以把它理解为一张数据表。

createObjectStore(name, options)函数接收两个参数,第一个参数为仓库的名字,第二个参数是个对象,是可选参数,其中包括以下的属性:

  • keyPath:主键
  • autoIncrement:boolean类型,键生成器,是否自动生成,默认false

createIndex

我们知道 indexDB 是可以通过索引来高效查找数据的,为对象仓库创建索引使用createIndex, createIndex(name,keypath,options)接受三个参数

  • name:索引的名称,可为空
  • keypath:索引对应的keyPath
  • options:可选,比如:unique:boolean类型,如果为 true,则索引将不允许单个键的重复值
// 创建对象仓库
const store = result.createObjectStore(storeName, {
  autoIncrement: true,
  keyPath
})
// 为对象仓库创建索引
indexs.map((v: string) =>
    // createIndex可以新建索引,unique字段表示该字段的值是否可以重复
    store.createIndex(v, v, { unique: false })
)
// 创建成功的回调
store.transaction.oncomplete = (e: any) => {
  console.log('创建对象仓库成功')
}

增删改查

事务

indexDB 数据的读写和删改,都要通过事务完成。这意味着一系列操作步骤之中,只要有一步失败,整个事务就都取消,数据库回滚到事务发生之前的状态,不存在只改写一部分数据的情况。

transaction(storeNames, mode, options)表示一个事务。在数据库上创建一个事务,指定作用域(例如要访问的存储对象),并确定所需的访问类型(只读或读写)。接收三个参数:

  • storeNames:字符串数组,对象库名称,如果只有一个,也可以写字符串不写数组
  • mode:字符串,可选参数。readonly和readwrite,默认为readonly,表示只读,readwrite表示可以写入。
  • options:可选参数

查找数据

利用主键查询

可以使用get方法查找到某一条数据:

request.onsuccess = function (event) {
    db = event.target.result;
    // 创建事务
    var transaction = db.transaction(["userInfo"]);
    // 通过事务获取对象仓库,也就是数据表
    var objectStore = transaction.objectStore("userInfo");
    let men1 = objectStore.get(2);
    men1.onsuccess = function (e) {
        var data = e.target.result;
        console.log(data);
    };
};

利用索引查询

有时候我们并不能确定用户的id,只能通过其他方式去查找,比如我们想查找邮箱为"5@qq.com"的用户,这时候就需要用到索引。

使用索引之前必须确定已经设置了索引,在创建表的时候,我们通过 objectStore.createIndex("email", "email", {unique: true}); unique 为 true 表示email 字段的值不能重复。

request.onsuccess = function (event) {
    db = event.target.result;
    var transaction = db.transaction(["userInfo"]);
    var objectStore = transaction.objectStore("userInfo");
    let men1 = objectStore.index("email");
    // 查找邮箱为‘5@qq.com’的用户
    men1.get('5@qq.com').onsuccess = function (e) {
        var data = e.target.result;
        console.log(data);
    };
};

使用游标查询

如果我们要查询id 5-9的数据,不包含5,但包含9:

let men1 = objectStore.openCursor(IDBKeyRange.bound(5, 9, true, false));
let list=[]
men1.onsuccess = function (e) {
    var data = e.target.result;
    if (data) {
        list.push(data.value)
        data.continue(); // 使游标下移
    } else {
        console.log("No more entries!");
    }
    console.log(list); // 打印出id是6 7 8 9的数据
};

新增/修改数据

add 表示新增,put即可以表示新增,也能表示修改。

// 通过事务获取对象仓库
const store = this.db
  .transaction([storeName], 'readwrite')
  .objectStore(storeName)
// put可以新增和修改  add 只是新增
const request = store.put({ ...data, updateTime: +new Date() })
request.onsuccess = (event: any) => {
    console.log('数据写入成功')
}
request.onerror = (event: any) => {
    console.log('数据写入失败')
}

删除数据

const store = this.db
  .transaction([storeName], 'readwrite')
  .objectStore(storeName)
const request = store.delete(key)

indexDB的封装

export default class DB {
  private dbName: string
  private db: any // 数据库对象

  constructor(dbName: string) {
    this.dbName = dbName
  }

  // 第二个参数表示数据库的版本号,默认是1,这里设置为2,所以会进行升级,执行onupgradeneeded回调
  public openStore(stores: any) {
    const request = window.indexedDB.open(this.dbName, 2)
    return new Promise((resolve, reject) => {
      request.onsuccess = (event: any) => {
        console.log('数据库打开成功')
        // 把数据库对象绑定到db属性上
        this.db = event.target.result
        resolve(true)
      }
      request.onerror = event => {
        console.log('数据库打开失败')
        reject(event)
      }
      request.onupgradeneeded = event => {
        console.log('数据库升级成功')
        // 创建存储对象,也就是数据表
        const { result }: any = event.target
        for (const storeName in stores) {
          // 初始化多个ojectStore对象仓库,获取数据表的主键和索引
          const { keyPath, indexs } = stores[storeName]
          // 没有表则新建表
          if (!result.objectStoreNames.contains(storeName)) {
            // keyPath:主键,主键(key)是默认建立索引的属性;
            // autoIncrement:是否自增;
            // createObjectStore会返回一个对象仓库objectStore(即新建一个表)
            const store = result.createObjectStore(storeName, {
              autoIncrement: true,
              keyPath
            })
            // 创建数据表的索引
            if (indexs && indexs.length) {
              indexs.map((v: string) =>
                // createIndex可以新建索引,unique字段是否唯一
                store.createIndex(v, v, { unique: false })
              )
            }
            store.transaction.oncomplete = (e: any) => {
              console.log('创建对象仓库成功')
            }
          }
        }
      }
    })
  }

  // 往数据库添加数据: 新增/修改
  async updateItem(storeName: string, data: any) {
    // 添加数据通过事务来添加,事务是在数据库对象上
    // 第二个参数表示读写模式
    const store = this.db
      .transaction([storeName], 'readwrite')
      .objectStore(storeName)
    // put可以新增和修改  add 只是新增
    const request = store.put({ ...data, updateTime: +new Date() })
    return new Promise((resolve, reject) => {
      request.onsuccess = (event: any) => {
        console.log('数据写入成功')
        resolve(event)
      }
      request.onerror = (event: any) => {
        console.log('数据写入失败')
        reject(event)
      }
    })
  }

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

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

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

项目中使用indexDB

  1. 首先创建数据库,利用上面封装的类进行创建:
// 数据库 传入数据库的名字
export const airbnbDB = new DB('storeName')
  1. 创建数据表
airbnbDB.openStore({
    // 数据表的名字
    web_user: {
        // 主键
        keyPath: 'userId',
        // 索引列表
        indexs: ['mobile', 'password', 'status']
    }
})
  1. 创建接口

用户注册

export async function userSignApi(params: any) {
    let result
    const obj = { status: 0 }
    Object.assign(params, obj)
    result = await new Promise((resolve, reject) => {
      airbnbDB.updateItem('web_user', params).then(res => {
        resolve({
          code: '000000',
          success: true,
          message: '注册成功',
          result: null
        })
      })
    })
    return result
}

用户登录

export async function userLoginApi(params: any) {
  // 校验手机号和密码是否正确
  const correct: any = await new Promise((resolve, reject) => {
    airbnbDB.getList('web_user').then((res: any) => {
      res &&
        res.forEach((item: any) => {
          // 校验手机号
          if (item.mobile === params.mobile) {
            // 校验密码
            if (item.password === params.password) {
              resolve({ code: '000000', userId: item.userId })
            } else {
              resolve({ code: '000002' })
            }
          }
        })
      // 其他
      resolve({ code: '000004' })
    })
  })
  let result: IResultOr
  if (correct.code !== '000000') {
    result = await new Promise((resolve, reject) => {
      resolve({
        code: correct.code,
        success: false,
        message:
          correct.code === '000002'
            ? '密码不正确'
            : correct.code === '000003'
            ? '手机号不正确'
            : '不存在该用户,请先注册',
        result: null
      })
    })
  } else {
    // 手机号和密码正确后更新数据库的登录状态
    const token = new Date().getTime() + ''
    localStorage.setItem('token', token)
    const obj = { status: 1, userId: correct.userId, token }
    Object.assign(params, obj)
    result = await new Promise((resolve, reject) => {
      airbnbDB.updateItem('web_user', params).then(res => {
        resolve({
          code: '000000',
          success: true,
          message: '登录成功',
          result: obj
        })
      })
    })
  }
  return result
}
  1. 调用接口
import { userSignApi } from '@/api/login'

const userSign = (params) => {
  userSignApi(params).then(res => {
    const { success, message } = res
    ....
  })
}

这样我们就能像调用后台接口一样去增删改查,只不过我们这里的数据是存储在用户的浏览器中。

注意:IndexedDB 受到同源限制,每一个数据库对应创建它的域名。网页只能访问自身域名下的数据库,而不能访问跨域的数据库。

其实indexDB使用起来是非常简单的,但是我们可以利用它来完成一个真正完整的项目,不必依赖后台。