Electron+Typeorm+Sqlite3实践
背景
Electron关于本地数据的存储一般分为以下几类:
JSON
存储:推荐库electron-store,支持JS
中对象方式的访问,并且直接加密,但是有需求对于数据的频繁读写性能不是很好;localStorage
/IndexedDB
存储:- 本地数据库: 比如
sqlite
,配置简单无需服务器,数据库直接访问单一文件即可,数据保存在本地以及能进行,但是数据库一般需要写sql
,所以本文主要介绍在electron
中链接sqlite3
以及不需要sql
的主流解决方案orm
;
Core NPM Packages
默认安装electron
以及渲染进程需要的相关代码,还需要:
npm install @electron/rebuild better-sqlite3 typeorm
Electron rebuild
该package主要是根据 Electron 项目所使用的 Node.js 版本重建原生 Node.js 模块,由于better-sqlite3
内置的node
版本可能跟项目本身使用的node
版本不一样,所以需要rebuild
,可以在packages.json
中设置:
{
"scripts": {
"rebuild": "electron-rebuild -f -w better-sqlite3",
}
}
⚠️: 每次install
依赖后可能都需要rebuild
一次!否则会导致后续数据库启动失败!
Use sqlite3 & Typeorm
这里选择了better-sqlite3
:
- 新建文件夹
dataBase
以及文件index.ts
:
import {join} from "path";
import {DataSource} from "typeorm";
import {app} from "electron";
const dataBasePath = join(
app.getPath("appData"),
app.getName(),
`./Your Dir/index.db`
);
console.log("DataBase init path: ", dataBasePath)
const DataBase = new DataSource({
type: "better-sqlite3",
entities: [], // 后续新建表的实体
database: dataBasePath, // 数据库地址
synchronize: true, // 自动同步表
logging: ["error"],
/*
1. 这里是 better-sqlite3 的 二进制文件,在 rebuild 后生成,然后指向该文件;
2. 后续在打包也需要 copy 至打包后的文件夹中,并且路径访问需要跟以下一致
*/
nativeBinding: join(__dirname, "./better_sqlite3.node")
})
export default DataBase;
- 数据库初始化
import DataBase from "@src/dataBase";
app.on('ready', async () => {
if (!DataBase.isInitialized) {
const [err] = await to(DataBase.initialize());
if (err) {
console.log("数据库已初始化失败!")
} else {
console.log("数据库已初始化成功!")
}
} else {
console.log("数据库已初始化成功!")
}
// ... 初始化窗口等操作
});
- 新建表实体
在
dataBase
中新建文件夹entities
用来存储业务对应的表,一下是新建一个配置表config.ts
:
import {Entity, PrimaryGeneratedColumn, Column, CreateDateColumn} from "typeorm";
@Entity("config")
export class ConfigEntities {
// 主键设置,无需手动生成,自动为uuid
@PrimaryGeneratedColumn("uuid")
id: string = "";
// 注意sqlite3中文本的存储没有varchar等,这里可以选择text存储
@Column({type: "text", default: ""})
locale?: string | undefined = "";
// default 设置默认值
@Column({type: "text", default: "light"})
theme?: string | undefined = "";
@Column({type: "text", default: "magazine"})
listMode?: string | undefined = "";
// 创建数据条的时间,无需手动维护插入单条数据的时间
@CreateDateColumn()
createDate?: Date | undefined;
}
- 在第一步的
dataBase
文件中修改entities
字段注入实体:
import {ConfigEntities} from "./entities/config.ts";
const DataBase = new DataSource({
// 其他配置不变
entities: [ConfigEntities]
})
- 新建
service
文件夹处理不用表中的对应事务:
import Database from "../dataBase";
import {ConfigEntities} from "../entities/config";
const getOSQueryBuilder = async () => {
return Database.getRepository(ConfigEntities)
};
class OSService {
// 根据ID查询操作
static async getConfig(id: string) {
return new Promise(async (resolve) => {
const osQueryBuilder = await getOSQueryBuilder();
const data = await osQueryBuilder.findOne({
where: {
id
}
})
resolve(data);
})
}
// 插入数据操作
static async updateConfig(config: ConfigEntities) {
return new Promise(async (resolve) => {
const osQueryBuilder = await getOSQueryBuilder();
const existingOS = await osQueryBuilder.findOne({
where: {
id: config.id,
}
});
// 如果不存在导入,存在就直接返回
if (!existingOS) {
osQueryBuilder.save(config)
.then((saveRes) => {
console.log("导入OS成功: ", JSON.stringify(saveRes))
resolve(saveRes)
})
} else {
resolve(existingOS)
}
})
}
// 根据ID更新数据
static async updateOS(data: {
id: string;
locale?: string;
}) {
const osQueryBuilder = await getOSQueryBuilder();
const config = await osQueryBuilder.findOne({
where: {
id: data.id
}
});
if (config?.id) {
const item = await osQueryBuilder.save({
id: config.id,
title: data?.locale || "",
})
return item;
} else {
return null
}
}
// 根据ID删除数据
static async removeOS(id: string) {
const osQueryBuilder = await getOSQueryBuilder();
const deleteResult = await osQueryBuilder.delete(id);
return deleteResult;
}
}
export default OSService;
- IPC调用事务,通过前端的IPC请求区分不同的事务:
import {ipcMain} from "electron";
import OSService from "../dataBase/server/os";
ipcMain.handle("os:config", () => {
return OSService.getConfig()
})
前端调用,比如react
中:
useEffect(() => {
// 这里是需要在 electron 中 preload 中设置 contextBridge
window.IPC.invoke("os:config")
.then(console.log)
.catch(console.err)
}, [])