詳解Electron中如何使用SQLite存儲筆記
前言
上一篇,我們使用 remirror 實(shí)現(xiàn)了一個簡單的 markdown 編輯器。接下來,我們要學(xué)習(xí)如何去存儲這些筆記。
當(dāng)然了,你也可以選擇不使用數(shù)據(jù)庫,不過若是你以后需要將該應(yīng)用上架到 mac Apple Store ,就需要考慮這個了。因?yàn)樯霞?mac 應(yīng)用需要啟用 sandbox,當(dāng)你第一次訪問筆記中的媒體文件時,都要打開選擇文件的彈窗,通過讓用戶主動選擇來授權(quán)訪問沙箱外的媒體文件。不過,如果你的媒體文件在第一次選擇插入文檔時復(fù)制到 sandbox 中,以后訪問優(yōu)先從沙箱容器中讀取,那是不需要授權(quán)的。雖然我也可以這么做,但這里考慮到后面的功能,還是選擇使用數(shù)據(jù)庫,當(dāng)需要導(dǎo)出筆記時再從數(shù)據(jù)庫中導(dǎo)出。
數(shù)據(jù)庫的選擇
Electron 應(yīng)用中常使用的數(shù)據(jù)庫是 SQLite、IndexedDB,IndexedDB 是在前端網(wǎng)頁中去操作。有的文章里說 IndexedDB 的性能會比 SQLite 更好,大家看實(shí)際場景去選擇使用。大多數(shù)桌面應(yīng)用或者 App 需要使用數(shù)據(jù)庫的時候一般都是用 SQLite。
npm 上有兩個最常用的 sqlite3 庫,一是 better-sqlite3 ,一是 node-sqlite ,兩種各有特點(diǎn)。前者是同步的 api ,執(zhí)行速度快,后者是異步 api ,執(zhí)行速度相對慢一點(diǎn)。值得注意的是,后者的編譯支持 arm 機(jī)器,而且由于出的比較早,和其他庫配合使用很方便。
安裝
安裝 node-sqlite
// 倉庫名是 node-sqlite, package 名是 sqlite3 yarn add sqlite3
借助 Knex.js 簡化數(shù)據(jù)庫操作
Knex.js是為Postgres,MSSQL,MySQL,MariaDB,SQLite3,Oracle和Amazon Redshift設(shè)計的 SQL 查詢構(gòu)建器
安裝 Knex.js
yarn add knex
創(chuàng)建表
現(xiàn)在,我們要開始設(shè)計數(shù)據(jù)庫結(jié)構(gòu)了。我們大概需要 3 張表,筆記本表,筆記表,還有一個媒體文件表。sqlite 支持 blob 數(shù)據(jù)類型,所以你也可以把媒體文件的二進(jìn)制數(shù)據(jù)存到數(shù)據(jù)庫中。這里我們就簡單的記個 id ,把媒體文件存到沙箱內(nèi)。
我們確定一下三張表的表名,notebooks, notes, media, 然后看一下該如何使用 Knex.js 創(chuàng)建表
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"); // 本地實(shí)際路徑
table.string("sandbox_path"); // 沙盒中的地址
table.bigInteger("note_id");
table.timestamps(true, true);
});
});
}
}
這里我用了一個 IOC 庫 inversify, 后面遇到 injectable、inject、ioc.get等寫法都是和這個有關(guān),這里我就不多介紹了,具體用法可以看文檔或其他文章。
注意:三張表中,note 和 media 都一個外鍵,這里我簡化了,并沒有用 api 去創(chuàng)建。
Service
數(shù)據(jù)庫表創(chuàng)建完了,接下來我們?yōu)楸淼牟僮鲗懴嚓P(guān)服務(wù),這一塊我是參考傳統(tǒng)后端 api 的設(shè)計去寫的,有 Service(數(shù)據(jù)庫) 和 Controller(業(yè)務(wù)),以 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 只負(fù)責(zé)數(shù)據(jù)庫的連接和表中數(shù)據(jù)的增刪改查。
Controller
Controller 可以通過接入 Service 操作數(shù)據(jù)庫,并做一些業(yè)務(wù)上的工作。
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();
}
}
業(yè)務(wù)
如何創(chuàng)建筆記本?
我們先來實(shí)現(xiàn)創(chuàng)建筆記本,之后的刪除筆記本,更新筆記本名稱等等,依葫蘆畫瓢就行。我們在界面上添加一個創(chuàng)建按鈕。

點(diǎn)擊后就會出現(xiàn)這樣一個彈窗,這里 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="創(chuàng)建筆記本"
onCancel={() => setMoalVisible(false)}
onOk={handleOk}
cancelText="取消"
okText="確定"
destroyOnClose
>
<Form form={form}>
<Form.Item
label="筆記本名稱"
name="name"
rules={[
{
required: true,
message: "請?zhí)顚懨Q",
},
{
whitespace: true,
message: "禁止使用空格",
},
{ min: 1, max: 100, message: "字符長度請保持在 1-100 之間" },
]}
>
<Input />
</Form.Item>
</Form>
</Modal>
);
});
外部提供的 onCreateNotebook 的實(shí)現(xiàn):
const handleCreateNotebook = async (name: string) => {
await window.Bridge?.createNotebook(name);
const data = await window.Bridge?.getNotebooks();
if (data) {
setNotebooks(data);
}
};
上面出現(xiàn)的 Bridge 是我在第一篇中講的 preload.js 提供的對象,它可以幫我們和 electron 主進(jìn)程通信。
接來寫,我們具體看一下 preload 和 主進(jìn)程部分的實(shí)現(xiàn):
// 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");
},
});
實(shí)際還是用 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();
});
總結(jié)
最后,我們來看一下這部分的完整交互:

這一篇,我們主要學(xué)習(xí)了如何在 Elctron 使用 SQLite 數(shù)據(jù)庫,并且簡單完成了 CRUD 中的 C。相關(guān)代碼在 Github 上,感興趣的同學(xué)可以自行查看。
以上就是詳解Electron中如何使用SQLite存儲筆記的詳細(xì)內(nèi)容,更多關(guān)于Electron SQLite存儲筆記的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
關(guān)于el-table表格組件中插槽scope.row的使用方式
這篇文章主要介紹了關(guān)于el-table表格組件中插槽scope.row的使用方式,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-08-08
vue實(shí)現(xiàn)動態(tài)顯示與隱藏底部導(dǎo)航的方法分析
這篇文章主要介紹了vue實(shí)現(xiàn)動態(tài)顯示與隱藏底部導(dǎo)航的方法,結(jié)合實(shí)例形式分析了vue.js針對導(dǎo)航隱藏與顯示的路由配置、事件監(jiān)聽等相關(guān)操作技巧,需要的朋友可以參考下2019-02-02
在Vue中實(shí)現(xiàn)地圖熱點(diǎn)展示與交互的方法詳解(如熱力圖)
隨著大數(shù)據(jù)和可視化技術(shù)的發(fā)展,地圖熱點(diǎn)展示越來越受到人們的關(guān)注,在Vue應(yīng)用中,我們通常需要實(shí)現(xiàn)地圖熱點(diǎn)的展示和交互,以便更好地呈現(xiàn)數(shù)據(jù)和分析結(jié)果,本文將介紹在Vue中如何進(jìn)行地圖熱點(diǎn)展示與交互,包括熱力圖、點(diǎn)聚合等2023-07-07
Vue2.0設(shè)置全局樣式(less/sass和css)
這篇文章主要為大家詳細(xì)介紹了Vue2.0設(shè)置全局樣式(less/sass和css),具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-11-11

