buhichan / modexie

A simple and practical IndexedDB Model library, the Database and DOM can be synchronized (If you have an MVVM framework😃), and use your favorite Dexie.js to CURD🎉

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Modexie

简体中文 | English

一个简单实用的 IndexedDB Model 库,Database 的数据可以和 DOM 同步 (如果你有 MVVM 框架的话 😃),和用你最爱的 Dexie.js 来写增删改查 🎉

许可证

GLWTL

入门

# 使用 modexie 需要 dexie,所以两个都需要安装哦
npm i modexie dexie

接着我们在项目中新建一个 models 文件夹,用于存放我们的模型

假设我们要写一个图书馆应用,所以需要 BookAuthor 两个 Model

这是一个 Book 模型的示例文件:

// models/Book.js

export default {
  /**
   * 名称 (*必需属性*)
   * 会以此为依据在 indexedDB 中创建同名的表,每个模型都对应一张表且只对应一张表
   */
  name: "book",

  /**
   * 填充
   * 只在用户首次运行程序时才会执行
   * 第一个参数返回了一个 [Dexie Table](https://dexie.org/docs/Table/Table) 对象
   * 你可以用来对模型填充一些初始化数据,或者做一些别的事情
   * 例如下方向表中添加了一本书
   */
  seeding(table) {
    table.add({
      title: "1984",
      author_id: 1,
    });
  },

  /**
   * 默认属性
   * 当插入一条新数据时,若这些属性未指定,则赋予他们这些默认值
   * 如果一个默认属性是函数,则每次插入或修改模型时,此属性都会被执行函数的返回值覆盖
   * 用来实现如自动维护更新时时间戳的功能会很方便
   */
  attributes: {
    cover: "/images/book/default_cover.jpg",
    created_at: new Date().getTime(),
    updated_at() {
      return new Date().getTime();
    },
  },

  /**
   * 方法
   * 一般来讲,为了避免耦合,对模型的增删改查,最好都通过一个封装了所有查询语句的方法来完成。外部只需要调用此方法,就能得到它想要得到的
   * 第一个参数是 [Dexie Table](https://dexie.org/docs/Table/Table) 对象
   * 方法可以是异步的,也可以返回 Promise
   */
  methods: {
    addBook(table) {
      return table.add({
        title: "2666",
        author_id: null,
      });
    },
  },

  /**
   * 查询方法
   * "方法(methods)"和"查询方法(queries)"的区别是,"查询方法(queries)"可以使用模型关联、数据视图绑定等功能,而"方法(methods)"则只是一个简单的函数
   * "查询方法(queries)"必须返回一个数组,当然结果可以异步或是 Promise
   * 一般来说,增、删、改、或 count 等使用"方法(methods)",而查,则使用"查询方法(queries)"
   * 第一个参数是 [Dexie Table](https://dexie.org/docs/Table/Table) 对象
   */
  queries: {
    async all(table) {
      return await table.toArray();
    },
    async first(table) {
      return [await table.where({ id: 1 }).first()];
    },
  },
};

创建好模型后,在你应用的入口文件中初始化 Modexie

// main.js

import Dexie from "dexie";
import Modexie from "modexie";
// 引入你写的模型
import Book from "./models/Book";
import Author from "./models/Author";

const connection = new Modexie(
  // 第一个参数需传入一个 [Dexie 实例对象](https://dexie.org/docs/Dexie/Dexie),Modexie 的所有操作都将通过调用此对象来完成
  new Dexie("mydb"), // 小贴士:Dexie 的第一个参数是数据库名称
  // 第二个参数是要进行的迁移,见 [Dexie Version](https://dexie.org/docs/Dexie/Dexie.version())
  (db) => {
    db.version(2).stores({
      book: "++id, title, author_id",
      author: "++id, name, book_id",
    });

    db.version(1).stores({
      book: "++id, title",
      author: "++id, name, book_id",
    });
  },
  // 第三个参数是一个数组,里面传入所有你需要应用到此数据库的模型
  [Book, Author]
);

window.mydb = connection; // 你可以把它挂载到 window 方便使用
// Vue.prototype.$mydb = connection; // 如果你用 vue,也可以把它挂到你组件的 this 里

你可以这样访问到你定义的模型

window.mydb.models["你的模型名称"];

使用起来就非常简单了,例如我们想读取所有的书,我们可以调用我们先前定义的查询方法

const book = await this.$mydb.models.book.query("first");
const books = await this.$mydb.models.book.query("all");

console.log(book, books);

数据视图绑定

使用此功能前,你需要有在使用一款 MVVM 框架,此功能会监听模型的更改,当有更改时,立刻将更改同步到你查询方法查询出的对象中 (因为 Javascript 的 Array 和 Object 都是地址引用而不是值引用),接着由于数组或对象被更改,你 MVVM 框架就会将数据和你的视图同步

为了提供更大的自由度,最新版本已改成了监听的形式,下面是使用 Vue 的一个例子

<template>
  <div>
    <button @click="put">Put Book</button>
    <div v-for="book in books" :key="book.id">
      {{ book.title }}
    </div>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        books: [],
      };
    },
    async mounted() {
      // 调用 watch 函数来监听书籍变更,点下 `Put Book` 按钮吧!
      this.books = await this.$mydb.models.book
        .watch({
          creating: async (newObj) => {
            this.books.unshift(newObj);
          },
          updating: async (query) => {
            this.books = await query();
          },
          deleting: async (index, delObj) => {
            this.books.splice(index, 1);
          },
        })
        .with(["author"])
        .query("all");
    },
    components: {},
    methods: {
      put() {
        this.$mydb.models.book.method("put", {
          id: 1,
          title: Math.random().toString(36).slice(-8),
        });
      },
    },
  };
</script>

模型关联

IndexedDB 是非关系型数据库,存储的数据是树状而不是表状的。往往插入一个数组或对象要比一对一/一对多关联方便得多。

如果需要关联的话,目前比较推崇的数据库表设计范式是 如 MongoDB 所说,推荐先行阅读

一对一关联

假设一个 User 模型关联一个 Phone 模型

// models/User.js

{
  id: 42,
  name: 'david',
}
// models/Phone.js

{
  id: 36,
  user_id: 42,
  code: '086',
  number: '12345678901',
}

加载关联后查询出的结构为

{
  id: 42,
  name: 'david',
  phone: {
    id: 36,
    user_id: 42,
    code: '086',
    number: '12345678901',
  }
}

使用前需要先在模型文件中定义关联

// models/User.js

export default {
  name: "user",

  // ...

  /**
   * 模型关联
   */
  relationships: {
    async phone({ hasOne, belongsTo, hasMany, belongsToMany }) {
      return await hasOne({
        model: "phone", // 关联模型的名称,需与当前模型处于同一数据库
        // foreignKey: "user_id", // 外键,不填则默认为 `${父模型名}_id`
        // localKey: "id", // 主键,不填则默认为 `id`
        // defaultValue: {}, // 当关联结果为空的默认值,不填则默认为空对象,原因:https://en.wikipedia.org/wiki/Null_object_pattern
      });
    },
  },
};

然后在 query 前调用 with 即可使用,with 函数接受一个数组,你可以同时加载多个关联的子模型

this.list = await this.$mydb.models.user.with(["phone"]).query("yourQueryName");

一对一关联(反向)

我们已经可以从 User 拿到 Phone 了,那么我们想从 Phone 拿到 User 就在模型文件中定义反向关联

// models/Phone.js

export default {
  name: "user",

  // ...

  /**
   * 模型关联
   */
  relationships: {
    async user({ hasOne, belongsTo, hasMany, belongsToMany }) {
      return await belongsTo({
        model: "user", // 关联模型的名称,需与当前模型处于同一数据库
        // foreignKey: "user_id", // 外键,不填则默认为 `${子模型名}_id`
        // localKey: "id", // 主键,不填则默认为 `id`
        // defaultValue: {}, // 当关联结果为空的默认值,不填则默认为空对象,原因:https://en.wikipedia.org/wiki/Null_object_pattern
      });
    },
  },
};

然后在 query 前调用 with 即可使用

this.list = await this.$mydb.models.phone.with(["user"]).query("yourQueryName");

结果如下

{
  id: 36,
  user_id: 42,
  code: '086',
  number: '12345678901',
  user: {
    id: 42,
    name: 'david',
  }
}

一对多关联

假设一个 Author 模型关联多个 Book 模型

// models/Author.js

{
  id: 42,
  name: 'david',
}
// models/Book.js

{
  id: 36,
  author_id: 42,
  title: '2666',
}

使用前需要先在模型文件中定义关联

// models/Author.js

export default {
  name: "user",

  // ...

  /**
   * 模型关联
   */
  relationships: {
    async books({ hasOne, belongsTo, hasMany, belongsToMany }) {
      return await hasMany({
        model: "book", // 关联模型的名称,需与当前模型处于同一数据库
        // foreignKey: "user_id", // 外键,不填则默认为 `${父模型名}_id`
        // localKey: "id", // 主键,不填则默认为 `id`
        // defaultValue: [], // 当关联结果为空的默认值,不填则默认为空数组,原因:https://en.wikipedia.org/wiki/Null_object_pattern
      });
    },
  },
};

然后在 query 前调用 with 即可使用

this.list = await this.$mydb.models.author
  .with(["books"])
  .query("yourQueryName");

一对多关联(反向)

同一对一关联(反向)

多对多关联

因为 IndexedDB 是非关系数据库,所以没必要用一张中间表来存储关联关系,使用数组即可

假设 Book 模型和 Tag 模型互相多对多关联

// models/Book.js

{
  id: 1,
  title: '2666',
  tag_id: [1, 2]
}
// models/Tag.js

{
  id: 2,
  name: 'literature',
  book_id: [2, 4, 6]
}

模型中定义关联

// models/Book.js

export default {
  name: "book",

  // ...

  /**
   * 模型关联
   */
  relationships: {
    async tags({ hasOne, belongsTo, hasMany, belongsToMany }) {
      return await belongsToMany({
        model: "tag", // 关联模型的名称,需与当前模型处于同一数据库
        // foreignKey: "user_id", // 外键,不填则默认为 `${父模型名}_id`
        // localKey: "id", // 主键,不填则默认为 `id`
        // defaultValue: [], // 当关联结果为空的默认值,不填则默认为空数组,原因:https://en.wikipedia.org/wiki/Null_object_pattern
      });
    },
  },
};

嵌套关联

To Be Continued

API

const mydb = new Modexie(new Dexie("mydb"), [Book, Author]);

// 当前数据库所使用的原始 Dexie 对象
mydb.con;

// 当前数据库所有的模型
mydb.models;

// 当前模型的 [Dexie Table](https://dexie.org/docs/Table/Table) 对象
mydb.models.book.table();

// 调用模型的方法
mydb.models.book.method("methods name", ...args);

// 调用模型的查询方法
mydb.models.book.query("queries name", ...args);

// 加载模型关联
mydb.models.book.with(["relationships name"]).query(...);

// 模型视图绑定监听
mydb.models.book.watch(..., {
  primary: ...,
  creating: ...
});

// 模型自带可直接使用的 method 方法
mydb.models.book.method("add", objectOrArray);
mydb.models.book.method("put", objectOrArray);
mydb.models.book.method("delete", objectOrArray);

About

A simple and practical IndexedDB Model library, the Database and DOM can be synchronized (If you have an MVVM framework😃), and use your favorite Dexie.js to CURD🎉

License:Other


Languages

Language:JavaScript 98.9%Language:Shell 1.1%