如何引入客户端数据库及相关工具?
在上一节中我们介绍如何使用 Pinia 库来管控应用的数据状态,现在我们是时候该考虑如何把应用内的业务数据持久化到用户本地磁盘上了。
对于简单的数据类型来说,开发者可以直接把它们存储在 localStorage 中,这些数据是持久化在用户磁盘上的,不会因为用户重启应用或者重装应用而丢失。
对于稍微复杂的数据类型来说,开发者有两个选择,其一是把这类数据存储在 IndexedDB 中,与 localStorage 类似,这也是谷歌浏览器核心提供的数据持久化工具,它以 JSON 对象的方式存储数据,数据较多时,复杂的条件查询效率不佳。
第二个选择就是把数据存储在 SQLite 中,这是一个关系型数据库,天生对复杂条件查询支持良好。接下来我们就介绍如何把 SQLite 引入到 Electron 应用中。
SQLite 是使用 C 语言编写的嵌入式数据库引擎,它不像常见的客户端-服务器数据库范例,SQLite 内嵌到开发者的应用中,直接为开发者提供数据存储与访问的 API,数据存储介质可以是终端的内存也可以是磁盘,其特点是自给自足、无服务器、零配置、支持事务。它是在世界上部署最广泛的 SQL 数据库引擎。
引入 SQLite
有很多 Node.js 的库可以把 SQLite 引入到 Node.js 的开发环境中,比如:better-sqlite3 和 sqlite3,由于better-sqlite3在性能方面表现的更出色,所以我们选用它作为我们的 SQLite 包装器。下表是两个库的性能对比:
select 1 row get() | select 100 rows all() | select 100 rows iterate() 1-by-1 | insert 1 row run() | insert 100 rows in a transaction | |
|---|---|---|---|---|---|
| better-sqlite3 | 1x | 1x | 1x | 1x | 1x |
| sqlite and sqlite3 | 11.7x slower | 2.9x slower | 24.4x slower | 2.8x slower | 15.6x slower |
你可以使用如下指令为我们的工程安装better-sqlite3:
npm install better-sqlite3 -D这个模块安装完成后,大概率你是无法使用这个模块的,你可能会碰到如下报错信息:
Error: The module '...node_modules\better-sqlite3\build\Release\better_sqlite3.node'
was compiled against a different Node.js version using
NODE_MODULE_VERSION $XYZ. This version of Node.js requires
NODE_MODULE_VERSION $ABC. Please try re-compiling or re-installing
the module (for instance, using `npm rebuild` or `npm install`).这是因为 Electron 内置的 Node.js 的版本可能与你编译原生模块使用的 Node.js 的版本不同导致的。
Electron 内置的 Node.js 中的一些模块也与 Node.js 发行版不同,比如 Electron 使用了 Chromium 的加密解密库 BoringSL,而 Node.js 发行版使用的是 OpenSSL 加密解密库。
建议开发者使用 Electron 团队提供的 electron-rebuild 工具来完成此工作,因为 electron-rebuild 会帮我们确定 Electron 的版本号、Electron 内置的 Node.js 的版本号、以及 Node.js 使用的 ABI 的版本号,并根据这些版本号下载不同的头文件和类库。
使用如下命令安装electron-rebuild:
npm install electron-rebuild -D然后,在你的工程的 package.json 中增加如下配置节(scripts配置节):
"rebuild": "electron-rebuild -f -w better-sqlite3"接着,在工程根目录下执行如下指令:
npm run rebuild当你的工程下出现了这个文件node_modules\better-sqlite3\build\Release\better_sqlite3.node,才证明better_sqlite3模块编译成功了,如果上述指令没有帮你完成这项工作,你可以把指令配置到node_modules\better-sqlite3模块内部再执行一次,一般就可以编译成功了(如下图所示)。
这样就为 Electron 重新编译了一遍better-sqlite3,我们就可以在 Electron 应用内使用better-sqlite3提供的 API 了。
你可以在应用中试试如下代码(渲染进程和主进程均可,甚至在渲染进程的开发者调试工具中也没问题),看是不是可以正确创建 SQLite 的数据库。
const Database = require("better-sqlite3");
const db = new Database("db.db", { verbose: console.log, nativeBinding: "./node_modules/better-sqlite3/build/Release/better_sqlite3.node" });不出意外的话,你的工程根目录下将会创建一个名为db.db的 SQLite 数据库文件,说明better-sqlite3库已经生效了。
压缩安装包体积
可能有的同学会有疑问:better-sqlite3不是一个原生模块吗,但这里我们仍然把它安装成了开发依赖,大家都知道原生模块是无法被 Vite 编译到 JavaScript的,那我们为什么还要把它安装程开发依赖呢?
把better-sqlite3安装成生产依赖,在功能上没有任何问题,electron-builder 在制作安装包时,会自动为安装包附加这个依赖(better-sqlite3这个库自己的依赖也会被正确附加到安装包内)。
但electron-builder会把很多无用的文件(很多编译原生模块时的中间产物)也附加到安装包内。无形中增加了安装包的体积(大概 10M),如下图所示:
上图中红框内的文件都是无用的文件,还有很多无用的文件没出现在这个截图中,如果你像我一样,无法容忍这一点,那么接下去我们就介绍一种办法来处理这个问题。
无用文件的多少实际上与开发环境和具体原生模块的配置有关,如果你的环境中没有过多的垃圾文件,你不做这些工作问题也不大。
还记得我们在《如何开发 Vite 3 插件打包 Electron 应用?》中讲的BuildObj类吗?我们为这个对象增加一个方法,代码如下:
//plugins\buildPlugin.ts
//import fs from "fs-extra";
async prepareSqlite() {
//拷贝better-sqlite3
let srcDir = path.join(process.cwd(), `node_modules/better-sqlite3`);
let destDir = path.join(process.cwd(), `dist/node_modules/better-sqlite3`);
fs.ensureDirSync(destDir);
fs.copySync(srcDir, destDir, {
filter: (src, dest) => {
if (src.endsWith("better-sqlite3") || src.endsWith("build") || src.endsWith("Release") || src.endsWith("better_sqlite3.node")) return true;
else if (src.includes("node_modules\\better-sqlite3\\lib")) return true;
else return false;
},
});
let pkgJson = `{"name": "better-sqlite3","main": "lib/index.js"}`;
let pkgJsonPath = path.join(process.cwd(), `dist/node_modules/better-sqlite3/package.json`);
fs.writeFileSync(pkgJsonPath, pkgJson);
//制作bindings模块
let bindingPath = path.join(process.cwd(), `dist/node_modules/bindings/index.js`);
fs.ensureFileSync(bindingPath);
let bindingsContent = `module.exports = () => {
let addonPath = require("path").join(__dirname, '../better-sqlite3/build/Release/better_sqlite3.node');
return require(addonPath);
};`;
fs.writeFileSync(bindingPath, bindingsContent);
pkgJson = `{"name": "bindings","main": "index.js"}`;
pkgJsonPath = path.join(process.cwd(), `dist/node_modules/bindings/package.json`);
fs.writeFileSync(pkgJsonPath, pkgJson);
}这段代码主要做了两个工作。
- 第一:把开发环境的
node_modules\better-sqlite3目录下有用的文件拷贝到dist\node_modules\better-sqlite3目录下,并为这个模块自制了一个简单的package.json。 - 第二:完全自己制作了一个
bindings模块,把这个模块放置在dist\node_modules\bindings目录下。
这里bindings模块是better-sqlite3模块依赖的一个模块,它的作用仅仅是确定原生模块文件better_sqlite3.node的路径。
接下来再修改一下BuildObj的preparePackageJson方法,在生成package.json文件之前,为其附加两个生产依赖,代码如下:
//版本号是否正确无关紧要
localPkgJson.dependencies["better-sqlite3"] = "*";
localPkgJson.dependencies["bindings"] = "*";有了这两个配置electron-builder就不会再为我们自动安装这些模块了。
完成这些工作后,在closeBundle钩子函数中调用这个方法:buildObj.prepareSqlite(),你再打包看看,安装包的体积是否变小了呢?
你可以在
release\win-unpacked\resources这个路径下执行asar list app.asar这个命令行指令,观察打包到生产环境的文件(需要全局安装 asar 工具)。asar 是一种特殊的存档格式,它可以把大批的文件以一种无损、无压缩的方式链接在一起,并提供随机访问支持。默认情况下 electron-builder 会把开发者编写的 HTML、CSS 和 JavaScript 代码以及相关的资源打包成 asar 文件嵌入到安装包中,再分发给用户。electron-builder 是通过 Electron 官方提供的 asar 工具制成和提取 asar 文档的。开发者自己全局安装这个工具,随时查阅生产环境下的资源文件。(这是非常有必要的。)
引入 Knex.js
成功引入better-sqlite3并且压缩了better-sqlite3模块在安装包的体积后,我们马上就面临着另一个问题需要解决。
使用better-sqlite3读写数据库中的数据时,要书写 SQL 语句,这种语句是专门为数据库准备的指令,是不太符合现代编程语言的习惯的,下面是为 sqlite 数据库建表和在对应表中完成增删改查的 SQL 语句:
create table admin(username text,age integer);
insert into admin values('allen',18);
select * from admin;
update admin set username='allen001',age=88 where username='allen' and age=18;
delete from admin where username='allen001';我们完全可以使用Knex.js库来完成对应的操作,Knex.js允许我们使用 JavaScript 代码来操作数据库里的数据和表结构,它会帮我们把 JavaScript 代码转义成具体的 SQL 语句,再把 SQL 语句交给数据库处理。我们可以把它理解为一种 SQL Builder。
安装Knex.js的指令如下:
npm install knex -D这个库大部分时候都被用在 Node.js 的服务端,所以设计过程中没有过多地考虑库的体积的问题。但如果你不在意安装包的体积,也可以以生产依赖的形式安装这个库,让 electron-builder 为你安装这个模块和它依赖的那些子模块。
如果你希望做得完美一些,那么我们下面就介绍打包之前编译这个库的方法,代码如下所示:
//plugins\buildPlugin.ts
//import fs from "fs-extra";
prepareKnexjs() {
let pkgJsonPath = path.join(process.cwd(), `dist/node_modules/knex`);
fs.ensureDirSync(pkgJsonPath);
require("esbuild").buildSync({
entryPoints: ["./node_modules/knex/knex.js"],
bundle: true,
platform: "node",
format: "cjs",
minify: true,
outfile: "./dist/node_modules/knex/index.js",
external: ["oracledb", "pg-query-stream", "pg", "sqlite3", "tedious", "mysql", "mysql2", "better-sqlite3"],
});
let pkgJson = `{"name": "bindings","main": "index.js"}`;
pkgJsonPath = path.join(process.cwd(), `dist/node_modules/knex/package.json`);
fs.writeFileSync(pkgJsonPath, pkgJson);
}这个方法也是放置在buildPlugin.ts文件中的,相对于压缩better-sqlite3的体积来说,压缩Knex.js包的体积就简单很多了,我们仅仅是通过esbuild工具编译了一下这个包的代码就完成了工作。
这段代码有以下几点需要注意。
- 配置项
external是为了避免编译过程中esbuild去寻找这些模块而导致编译失败,也就是说Knex.js中这样的代码会保持原样输出到编译产物中:require('better-sqlite3')。 - 同样,我们要再为 package.json 增加一个生产依赖:
localPkgJson.dependencies["knex"] = "*";,以避免 electron-builder 为我们安装Knex.js模块。 - 别忘记在
closeBundle钩子函数中调用这个方法:buildObj.prepareKnexjs()。
完成这些工作之后,我们就把 Knex.js 安装到我们的工程内了。下一节将介绍如何使用这个库。
总结
本节我们讲了三个知识点,第一个是如何为工程引入原生模块 better-sqlite3,在介绍这部分知识时,我们介绍了如何使用 lectron-rebuild 编译原生模块。第二个知识点是如何压缩 better-sqlite3 这个依赖库的体积,在这里丰富了编译插件的逻辑(这是《如何开发 Vite3 插件打包 Electron 应用》介绍的内容)。第三个知识点是如何引入 Knex.js 库并压缩这个依赖库的体积。希望你学完本节的知识后能更深入地掌控 electron-builder 的打包流程。
下一节将介绍一些基础的数据库操作知识,让你在学习数据库基础操作的同时掌握 better-sqlite3 和 Knex.js 的用法。
源码
本节示例代码请通过如下地址自行下载: