详解Electron中如何使用SQLite存储笔记

目录
  • 前言
  • 数据库的选择
  • 安装
  • 创建表
  • Service
  • Controller
  • 业务
  • 总结

前言

上一篇,我们使用 remirror 实现了一个简单的 markdown 编辑器。接下来,我们要学习如何去存储这些笔记。

当然了,你也可以选择不使用数据库,不过若是你以后需要将该应用上架到 mac Apple Store ,就需要考虑这个了。因为上架 mac 应用需要启用 sandbox,当你第一次访问笔记中的媒体文件时,都要打开选择文件的弹窗,通过让用户主动选择来授权访问沙箱外的媒体文件。不过,如果你的媒体文件在第一次选择插入文档时复制到 sandbox 中,以后访问优先从沙箱容器中读取,那是不需要授权的。虽然我也可以这么做,但这里考虑到后面的功能,还是选择使用数据库,当需要导出笔记时再从数据库中导出。

数据库的选择

Electron 应用中常使用的数据库是 SQLiteIndexedDBIndexedDB 是在前端网页中去操作。有的文章里说 IndexedDB 的性能会比 SQLite 更好,大家看实际场景去选择使用。大多数桌面应用或者 App 需要使用数据库的时候一般都是用 SQLite

npm 上有两个最常用的 sqlite3 库,一是 better-sqlite3 ,一是 node-sqlite ,两种各有特点。前者是同步的 api ,执行速度快,后者是异步 api ,执行速度相对慢一点。值得注意的是,后者的编译支持 arm 机器,而且由于出的比较早,和其他库配合使用很方便。

安装

安装 node-sqlite

// 仓库名是 node-sqlite, package 名是 sqlite3
yarn add sqlite3

借助 Knex.js 简化数据库操作

Knex.js是为Postgres,MSSQL,MySQL,MariaDB,SQLite3,Oracle和Amazon Redshift设计的 SQL 查询构建器

安装 Knex.js

yarn add knex

创建表

现在,我们要开始设计数据库结构了。我们大概需要 3 张表,笔记本表,笔记表,还有一个媒体文件表。sqlite 支持 blob 数据类型,所以你也可以把媒体文件的二进制数据存到数据库中。这里我们就简单的记个 id ,把媒体文件存到沙箱内。

我们确定一下三张表的表名,notebooks, notes, media, 然后看一下该如何使用 Knex.js 创建表

import { app } from "electron";
import knex, { Knex } from "knex";
import { join } from "path";
import { injectable } from "inversify";
@injectable()
export class LocalDB {
  declare db: Knex;
  async init() {
    this.db = knex({
      client: "sqlite",
      useNullAsDefault: true,
      connection: {
        filename: join(app.getPath("userData"), "local.db"),
      },
    });
    // 新建表
    await this.sync();
  }
  async sync() {
    // notebooks
    await this.db.schema.hasTable("notebooks").then((exist) => {
      if (exist) return;
      return this.db.schema.createTable("notebooks", (table) => {
        table.bigIncrements("id", { primaryKey: true });
        table.string("name");
        table.timestamps(true, true);
      });
    });
    // notes
    await this.db.schema.hasTable("notes").then((exist) => {
      if (exist) return;
      return this.db.schema.createTable("notes", (table) => {
        table.bigIncrements("id", { primaryKey: true });
        table.string("name");
        table.text("content");
        table.bigInteger("notebook_id");
        table.timestamps(true, true);
      });
    });
    // media
    await this.db.schema.hasTable("media").then((exist) => {
      if (exist) return;
      return this.db.schema.createTable("media", (table) => {
        table.bigIncrements("id", { primaryKey: true });
        table.string("name");
        table.string("local_path"); // 本地实际路径
        table.string("sandbox_path"); // 沙盒中的地址
        table.bigInteger("note_id");
        table.timestamps(true, true);
      });
    });
  }
}

这里我用了一个 IOC 库 inversify, 后面遇到 injectableinjectioc.get等写法都是和这个有关,这里我就不多介绍了,具体用法可以看文档或其他文章。

注意:三张表中,notemedia 都一个外键,这里我简化了,并没有用 api 去创建。

Service

数据库表创建完了,接下来我们为表的操作写相关服务,这一块我是参考传统后端 api 的设计去写的,有 Service(数据库) 和 Controller(业务),以 Notebook 为例:

import { inject, injectable } from "inversify";
import { LocalDB } from "../db";
interface NotebookModel {
  id: number;
  name: string;
  create_at?: string | null;
  update_at?: string | null;
}
@injectable()
export class NotebooksService {
  name = "notebooks";
  constructor(@inject(LocalDB) public localDB: LocalDB) {}
  async create(data: { name: string }) {
    return await this.localDB.db.table(this.name).insert(data);
  }
  async get(id: number) {
    return await this.localDB.db
      .table<NotebookModel>(this.name)
      .select("*")
      .where("id", "=", id)
      .first();
  }
  async delete(id: number) {
    return await this.localDB.db.table(this.name).where("id", "=", id).delete();
  }
  async update(data: { id: number; name: string }) {
    return await this.localDB.db
      .table(this.name)
      .where("id", "=", data.id)
      .update({ name: data.name });
  }
  async getAll() {
    return await this.localDB.db.table<NotebookModel>(this.name).select("*");
  }
}

Service 只负责数据库的连接和表中数据的增删改查。

Controller

Controller 可以通过接入 Service 操作数据库,并做一些业务上的工作。

import { inject, injectable } from "inversify";
import { NotebooksService } from "../services/notebooks.service";
import { NotesService } from "../services/notes.service";
@injectable()
export class NotebooksController {
  constructor(
    @inject(NotebooksService) public service: NotebooksService,
    @inject(NotesService) public notesService: NotesService
  ) {}
  async create(name: string) {
    await this.service.create({
      name,
    });
  }
  async delete(id: number) {
    const row = await this.service.get(id);
    if (row) {
      const notes = await this.notesService.getByNotebookId(id);
      if (notes.length) throw Error("delete failed");
      await this.service.delete(id);
    }
  }
  async update(data: { id: number; name: string }) {
    return await this.service.update(data);
  }
  async getAll() {
    return await this.service.getAll();
  }
}

业务

如何创建笔记本?

我们先来实现创建笔记本,之后的删除笔记本,更新笔记本名称等等,依葫芦画瓢就行。我们在界面上添加一个创建按钮。

点击后就会出现这样一个弹窗,这里 UI 库我是用的 antd 做的。

看一下这个弹窗部分的逻辑

import { Modal, Form, Input } from "antd";
import React, { forwardRef, useImperativeHandle, useState } from "react";
interface CreateNotebookModalProps {
  onCreateNotebook: (name: string) => Promise<void>;
}
export interface CreateNotebookModalRef {
  setVisible: (visible: boolean) => void;
}
export const CreateNotebookModal = forwardRef<
  CreateNotebookModalRef,
  CreateNotebookModalProps
>((props, ref) => {
  const [modalVisible, setMoalVisible] = useState(false);
  const [form] = Form.useForm();
  const handleOk = () => {
    form.validateFields().then(async (values) => {
      await props.onCreateNotebook(values.name);
      setMoalVisible(false);
    });
  };
  useImperativeHandle(ref, (): CreateNotebookModalRef => {
    return {
      setVisible: setMoalVisible,
    };
  });
  return (
    <Modal
      visible={modalVisible}
      title="创建笔记本"
      onCancel={() => setMoalVisible(false)}
      onOk={handleOk}
      cancelText="取消"
      okText="确定"
      destroyOnClose
    >
      <Form form={form}>
        <Form.Item
          label="笔记本名称"
          name="name"
          rules={[
            {
              required: true,
              message: "请填写名称",
            },
            {
              whitespace: true,
              message: "禁止使用空格",
            },
            { min: 1, max: 100, message: "字符长度请保持在 1-100 之间" },
          ]}
        >
          <Input />
        </Form.Item>
      </Form>
    </Modal>
  );
});

外部提供的 onCreateNotebook 的实现:

const handleCreateNotebook = async (name: string) => {
    await window.Bridge?.createNotebook(name);
    const data = await window.Bridge?.getNotebooks();
    if (data) {
      setNotebooks(data);
    }
};

上面出现的 Bridge 是我在第一篇中讲的 preload.js 提供的对象,它可以帮我们和 electron 主进程通信。

接来写,我们具体看一下 preload 和 主进程部分的实现:

// preload.ts
import { contextBridge, ipcRenderer, MessageBoxOptions } from "electron";
contextBridge.exposeInMainWorld("Bridge", {
  showMessage: (options: MessageBoxOptions) => {
    ipcRenderer.invoke("showMessage", options);
  },
  createNotebook: (name: string) => {
    return ipcRenderer.invoke("createNotebook", name);
  },
  getNotebooks: () => {
    return ipcRenderer.invoke("getNotebooks");
  },
});

实际还是用 ipcRenderer 去通信,但是这种方式更好

// main.ts
import { ipcMain } from "electron"
ipcMain.handle("createNotebook", async (e, name: string) => {
  return await ioc.get(NotebooksController).create(name);
});
ipcMain.handle("getNotebooks", async () => {
  return await ioc.get(NotebooksController).getAll();
});

总结

最后,我们来看一下这部分的完整交互:

这一篇,我们主要学习了如何在 Elctron 使用 SQLite 数据库,并且简单完成了 CRUD 中的 C。相关代码在 Github 上,感兴趣的同学可以自行查看。

以上就是详解Electron中如何使用SQLite存储笔记的详细内容,更多关于Electron SQLite存储笔记的资料请关注我们其它相关文章!

(0)

相关推荐

  • AntDesignPro使用electron构建桌面应用示例详解

    目录 注意事项声明 主要分为两个部分 开发环境使用 打包应用配置 package.json配置打包后的路径方式 使用 electron-builder 打包 exe 文件或者安装包,压缩包 安装 package.json添加命令 (打包windows) 添加打包配置 执行打包命令 使用 electron-packager 打包成 exe 文件 执行命令 提示 注意事项声明 所有 node 包必须使用 npm 安装不可使用 cnpm, 使用 cnpm 安装的 node 包会导致打包时间无限可能 具

  • vue electron实现无边框窗口示例详解

    目录 一.前言 二.实现方案 1.创建无边框窗口 2.创建windows窗口控件组件 三.后记 一.前言 无边框窗口是不带外壳(包括窗口边框.工具栏等),只含有网页内容的窗口.对于一个产品来讲,桌面应用带边框的很少,因为丑(我们的UI觉得--与我无关-.-).因此我们就来展开说下,在做无边框窗口时候需要注意的事项以及我踩过的坑. 二.实现方案 1.创建无边框窗口 要创建无边框窗口,只需在 BrowserWindow的 options 中将 frame 设置为 false: const { Bro

  • Electron学习应用程序打包实例详解

    目录 引言 如何将应用程序打包(Win) 1.关于package.js文件详解 2.使用electron-packager打包 3.使用electron-builder打包 整体感受 效果 引言 人真的是会变得越来越懒的,也正是人的惰性吧,真的是很讽刺. 关于这个应用程序的开发,断更了很久,但是代码部分还算没落下吧,终于在周一.周二终把这个应用程序写完了. 开发完不是终点.而是打包后可以使用才真的算是结束吧. 如何将应用程序打包(Win) 1.关于package.js文件详解 完整实例如下: j

  • vite + electron-builder 打包配置详解

    目录 创一个vite项目 安装打包工具 配置桌面环境 创建 主进程 main.js 添加electron 运行命令 打包项目,生成dist 解决资源无法加载 开发环境:热更新 两个工具 concurrently wait-on 打包exe 解决index.html找不到的问题 创一个vite项目 npm init vite 安装打包工具 npm i -D electron // 20.1.0 npm i -D electron-builder // 23.3.3 electron是开发时运行环境

  • Web Worker线程解决方案electron踩坑记录

    目录 初始化项目 编写入口文件和 electron 插件 websocket websocket 服务 连接 websocket 服务 发送心跳 取消心跳 重新连接 其它优化 Worker 初始化项目 electron 开发时会遇到一对多的情况,在进行 websocket 通信时,如果接收到服务端多个指令时,而这个指令刚好需要占用线程,这个时候整个界面就会失去响应,那么我们就可以使用线程来解决这个问题. npm create vite@latest electron-worker 执行完后修改

  • 详解Electron中如何使用SQLite存储笔记

    目录 前言 数据库的选择 安装 创建表 Service Controller 业务 总结 前言 上一篇,我们使用 remirror 实现了一个简单的 markdown 编辑器.接下来,我们要学习如何去存储这些笔记. 当然了,你也可以选择不使用数据库,不过若是你以后需要将该应用上架到 mac Apple Store ,就需要考虑这个了.因为上架 mac 应用需要启用 sandbox,当你第一次访问笔记中的媒体文件时,都要打开选择文件的弹窗,通过让用户主动选择来授权访问沙箱外的媒体文件.不过,如果你

  • 详解Python中如何将数据存储为json格式的文件

    一.基于json模块的存储.读取数据 names_writer.py import json names = ['joker','joe','nacy','timi'] filename='names.json' with open(filename,'w') as file_obj: json.dump(names,file_obj) 解释:我们先导入json模块,再创建一个名字列表,第5行我们指定了要将该列表存储到其中的文件的名称.通常使用扩展名.json来指出文件存储的数据为json格式.

  • 详解C++中的自动存储

    C++有3种管理数据内存的方式即自动存储(栈存储).静态存储和动态存储(堆存储).在不同的方式下,内存的分配形式和存在时间的长短都不同. 下面对自动存储进行说明. 自动存储(栈存储) 对于函数的形参.内部声明的变量及结构变量等,编译器将在函数执行时为形参自动分配存储空间,在执行到变量和结构变量等的声明语句时为其自动分配存储空间,因此称其为自动变量(Automatic Variable),有的教科书也称其为局部变量,在函数执行完毕返回时,这些变量将被撤销,对应的内存空间将被释放. 事实上,自动变量

  • 详解mysql中的存储引擎

    mysql存储引擎概述 什么是存储引擎? MySQL中的数据用各种不同的技术存储在文件(或者内存)中.这些技术中的每一种技术都使用不同的存储机制.索引技巧.锁定水平并且最终提供广泛的不同的功能和能力.通过选择不同的技术,你能够获得额外的速度或者功能,从而改善你的应用的整体功能. 例如,如果你在研究大量的临时数据,你也许需要使用内存存储引擎.内存存储引擎能够在内存中存储所有的表格数据.又或者,你也许需要一个支持事务处理的数据库(以确保事务处理不成功时数据的回退能力). 这些不同的技术以及配套的相关

  • 详解Android 中的文件存储

    目录 概要 当我们查看手机的文件管理器的时候,会发现里面的文件五花八门,想要找到自己项目所对应的文件非常困难,甚至有可能压根就找不到自己的文件,本文就来介绍一下APP开发过程当中文件存储的注意事项. 通常我们会将存放的文件分为两种:独立文件和专属文件.顾名思义,独立文件就是独立于APP之外的文件,不会随着APP的删除而删除,而专属文件则是专属于某个APP的文件,当APP删除后,会自动清空相对应的专属文件. 独立文件 独立文件指的是存放在shared/external storage direct

  • 详解MySql中InnoDB存储引擎中的各种锁

    目录 什么是锁 InnoDB存储引擎中的锁 锁的算法 行锁的3种算法 幻像问题 锁的问题 脏读 不可重复读 丢失更新 死锁 什么是锁 现实生活中的锁是为了保护你的私有物品,在数据库中锁是为了解决资源争抢的问题,锁是数据库系统区别于文件系统的一个关键特性.锁机制用于管理对共享资源的并发访. 数据库系统使用锁是为了支持对共享资源进行并发访问,提供数据的完整性和一致性 InnoDB存储引擎区别于MyISAM的两个重要特征就是:InnoDB存储引擎支持事务和行级别的锁,MyISAM只支持表级别的锁 In

  • 详解MySQL中存储函数创建与触发器设置

    目录 1.创建存储函数 2.调用存储函数 3.创建触发器 4.在触发器中调用存储过程 5.删除触发器 存储函数也是过程式对象之一,与存储过程相似.他们都是由SQL和过程式语句组成的代码片段,并且可以从应用程序和SQL中调用.然而,他们也有一些区别: 1.存储函数没有输出参数,因为存储函数本身就是输出参数. 2.不能用CALL语句来调用存储函数. 3.存储函数必须包含一条RETURN语句,而这条特殊的SQL语句不允许包含于存储过程中 1.创建存储函数 使用CREATE FUNCTION语句创建存储

  • 详解C++中存储类的使用

    目录 auto 存储类 register 存储类 static 存储类 extern 存储类 mutable 存储类 存储类定义 C++ 程序中变量/函数的范围(可见性)和生命周期.这些说明符放置在它们所修饰的类型之前.下面列出 C++ 程序中可用的存储类: auto register static extern mutable auto 存储类 在C++11 中, auto 关键字不再是C++存储类说明符.从C++11开始,auto 关键字声明一个变量,该变量的类型是从其声明中的初始化表达式推

  • 详解C++中的ANSI与Unicode和UTF8三种字符编码基本原理与相互转换

    目录 1.概述 2.Visual Studio中的字符编码 3.ANSI窄字节编码 4.Unicode宽字节编码 5.UTF8编码 6.如何使用字符编码 7.三种字符编码之间的相互转换(附源码) 7.1.ANSI编码与Unicode编码之间的转换 7.2.UTF8编码与Unicode编码之间的转换 7.3.ANSI编码与UTF8编码之间的转换 8.Windows系统对使用ANSI窄字节字符编码的程序的兼容 9.字符编码导致程序启动失败的案例 1.概述 在日常的软件开发过程中,会时不时地去处理不同

  • 详解mysql中的冗余和重复索引

    mysql允许在相同列上创建多个索引,无论是有意还是无意,mysql需要单独维护重复的索引,并且优化器在优化查询的时候也需要逐个地进行考虑,这会影响性能. 重复索引是指的在相同的列上按照相同的顺序创建的相同类型的索引,应该避免这样创建重复索引,发现以后也应该立即删除.但,在相同的列上创建不同类型的索引来满足不同的查询需求是可以的. CREATE TABLE test( ID INT NOT NULL PRIMARY KEY, A INT NOT NULL, B INT NOT NULL, UNI

随机推荐