在主进程里打开一个数据库子进程, 所有的渲染进程网页都可以通过 Socket 访问此数据库
示例中采用的 nedb 数据库, 可用相同方法封装其他数据库
解决问题::如果多个渲染进程再同一时刻打开nedb数据库, 会报错"rename错误"
🍊🍊 原 nedb 停止维护, 但 @seald-io/nedb 克隆了它
@seald-io/nedb 数据库速度:
Insert: 10,680 ops/s
Find: 43,290 ops/s
Update: 8,000 ops/s
Remove: 11,750 ops/s
使用( 用 socket有点慢,建议用第二种(electron MessageChannelMain) )
1.主进程中
import { NeDB数据库_port客户端, NeDB数据库_Socket客户端 } from "path/to/服务端_nedb数据库.mjs"
let 脚本路径 = "path/to/服务端_nedb数据库.mjs"
let 数据库进程 = await NeDB数据库_Socket客户端.创建服务端进程( 脚本路径 )
//-----------------------------------------------
2.渲染进程的预加载脚本中
import { NeDB数据库_port客户端, NeDB数据库_Socket客户端 } from "path/to/服务端_nedb数据库.mjs"
var 软件配置db, 缩略图db, 内存缩略图db
try{
[ 软件配置db, 缩略图db , 内存缩略图db] = await Promise.all([
NeDB数据库_Socket客户端.打开( "path/to/数据库_软件配置.db" ),
NeDB数据库_Socket客户端.打开( "path/to/数据库_缩略图.db" ),
NeDB数据库_Socket客户端.nedb新建内存数据库()
])
}catch(err){ throw(`🍎 !!! >>> 读取数据库错误 >>> !!! ${err}`) }
//-----------------------------------------------
3.原有 nedb 接口和自定义的接口都可以使用
let 查找 = await 软件配置db.findAsync({name:"test"})
let 查找 = await 软件配置db.查找({name:"test"})
核心文件 服务端_nedb数据库.mjs
import events from 'events'
import net from "net"
import fs from "fs"
import child_process from "child_process"
import NeDB from '@seald-io/nedb' //不能被打包,否则无法写入文件 ???
export { NeDB数据库_port客户端, NeDB数据库_Socket客户端 }
//真实数据库所在, 封装nedb增加自定义接口
class NeDB数据库 {
//私有属性 在原标准上增加一种规则, <匹配条件> 是字符串, 不处理数据,查找直接返回数据:: 用于保存唯一数据
#自定义_匹配条件(传入匹配条件){ return typeof 传入匹配条件=="string" ? {"_🌷":传入匹配条件} : 传入匹配条件 }
#自定义_保存数据(传入匹配条件, 传入数据){return typeof 传入匹配条件=="string" ? {"_🌷":传入匹配条件,"_🍊":传入数据 } : 传入数据 }
#自定义_返回数据(传入匹配条件, 返回数据){return typeof 传入匹配条件=="string" ? 返回数据?.["_🍊"] : 返回数据 }
constructor(配置对象){
this.数据库 = new NeDB(配置对象)
this.__proto__.__proto__ = this.数据库 //继承了NeDB所有方法后, 原有的nedb接口也可以用
}
static nedb新建内存数据库(){ return new NeDB数据库({ inMemoryOnly:true }) }
static async nedb打开本地数据库(路径){
let 数据库 = new NeDB数据库({ filename: 路径 ,corruptAlertThreshold:1 }) // autoload:true ,
await 数据库.loadDatabaseAsync()
return 数据库
}
static 获取数据库操作keys(){
let 自定义keys = Object.getOwnPropertyNames(NeDB数据库.prototype)
let NeDBkeys = Object.getOwnPropertyNames(NeDB.prototype)
let keys=[]
自定义keys.forEach(key=> /constructor/g.test(key) ? "" : keys.push(key) )
NeDBkeys.forEach( key=> /(constructor)|(^_)/g.test(key) ? "" : keys.push(key) )
// console.log("数据库可用操作",keys)
return keys
}
//新增自定义接口
async 保存(传入匹配条件, 传入数据){ //这个用于覆盖保存 ( 已经有就更新 updateAsync,没有就写入 insertAsync)
try{
let 结果 = await this.更新(传入匹配条件, 传入数据)
if(结果.numAffected) return 结果
else return await this.数据库.insertAsync( this.#自定义_保存数据(传入匹配条件, 传入数据) )
}catch(err){ return await this.数据库.insertAsync( this.#自定义_保存数据(传入匹配条件, 传入数据) ) }
}
async 查找单个(传入匹配条件){ //不再中断报错, 找不到返回 undefined/null
try{
let 结果 = await this.数据库.findOneAsync( this.#自定义_匹配条件(传入匹配条件) ,{_id:0} )
return this.#自定义_返回数据(传入匹配条件,结果) //找不到可能返回 null
}catch(err){ console.error(err); return undefined }
}
async 查找(传入匹配条件){ //不再中断报错, 找不到返回 undefined/[]
try{
let 结果数组 = await this.数据库.findAsync( this.#自定义_匹配条件(传入匹配条件) ,{_id:0})
return (typeof 传入匹配条件 != "string") ? 结果数组 : 结果数组.map(结果=> { return this.#自定义_返回数据(传入匹配条件, 结果) } )
}catch(err){ console.error(err); return [] }
}
async 插入(传入数据){ return await this.数据库.insertAsync(传入数据) }
async 更新(传入匹配条件, 传入数据){return await this.数据库.updateAsync( this.#自定义_匹配条件(传入匹配条件) ,this.#自定义_保存数据(传入匹配条件, 传入数据) ) }
async 删除单个(传入匹配条件){ return await this.数据库.removeAsync( this.#自定义_匹配条件(传入匹配条件) )}
async 删除(传入匹配条件){ return await this.数据库.removeAsync( this.#自定义_匹配条件(传入匹配条件) ,{ multi: true }) }
async 总数(传入匹配条件){ return await this.数据库.countAsync( this.#自定义_匹配条件(传入匹配条件) )}
async 移除索引(fieldName){ return await this.数据库.removeIndexAsync( fieldName )}
async 建立索引(Options){ return await this.数据库.ensureIndexAsync( Options )}
//db.建立索引({ fieldName: 'smb路径', unique: true , sparse:true}) // 索引 唯一 sparse:true 在字段不存在时也可以建立索引,否则报错
//索引只能用来加速基本查询以及使用$in, $lt, $lte, $gt 和 $gte运算符的查询
压缩数据(){ this.数据库.compactDatafile() }
定时压缩(毫秒){ this.数据库.setAutocompactionInterval(毫秒) }
定时压缩_停止(){ this.数据库.stopAutocompaction() }
}
//------------------ 客户端 ---------------------
class Socket信令码{
constructor(){
this.缓存
}
编码数据(发送对象){ return "🌷"+ encodeURI(JSON.stringify(发送对象)) +"🍊" } //发送对象 --> 先转为字符串 --> 再转 Buffer
解码数据(接收字符串){ return JSON.parse( decodeURI(接收字符串) ) } // Buffer数据 --> 先转为字符串 --> 再转对象
获取信令数组(传入Buffer数据){
//-----------拼接Buffer
let 开头标志 = /^🌷/g.test(传入Buffer数据) , 结尾标志 = /🍊$/g.test(传入Buffer数据)
if (开头标志 && !结尾标志){ this.缓存 = 传入Buffer数据 ;return [] } //有开头 没有结尾
else if(!开头标志 && !结尾标志){ this.缓存 = Buffer.concat([this.缓存, 传入Buffer数据]) ;return [] } //没有开头也没有结尾
else if(!开头标志 && 结尾标志) { this.缓存 = Buffer.concat([this.缓存, 传入Buffer数据]) } //没有开头 ,拼接
else this.缓存 = 传入Buffer数据 //有开头 有结尾 完整
//============拼接 验证完整性
let 完整字符串 = this.缓存.toString()
let 未解码信令数组 = 完整字符串.match(/(?<=🌷)[\s\S]*?(?=🍊)/g) //有可能同时受到几条信令
return 未解码信令数组.map(信令项=>{return this.解码数据(信令项) } )
}
}
const 数据库_socket文件 = '/dev/shm/文件浏览器nedb数据库.socket' //linux 这个目录是在内存里
class NeDB数据库_Socket客户端 extends Socket信令码{
constructor(数据库路径){
super()
this.注册_数据库操作() //所有自定义和原nedb的接口都可以使用
this.数据库路径 = 数据库路径
this.事件 = new events.EventEmitter();
this.计数 = 1
this.客户端 = net.connect(数据库_socket文件)
this.监听_监听信令事件戳()
}
static async 打开(数据库路径){
let 客户端 = new NeDB数据库_Socket客户端(数据库路径)
let 结果 = await 客户端.数据库操作("打开数据库")
console.log(结果)
return 客户端
}
监听_监听信令事件戳(){
this.客户端.on('data', Buffer数据 =>{
let 信令数组 = this.获取信令数组(Buffer数据)
信令数组.forEach(信令=> this.事件.emit(信令.事件戳 ,信令.数据包))
})
this.客户端.on('end', () =>{ console.log({类型:'net Socket客户端 断开连接'}) })
this.客户端.on('error', err =>{ console.log({类型:'net Socket客户端 错误',err:err}) })
}
async 发送信令(数据对象){
return new Promise((成功后回调,失败后回调)=>{
let 信令 = { 事件戳: ++this.计数 , 数据包:数据对象 }
this.事件.once(信令.事件戳, 数据包=>{ (数据包.状态) ? 成功后回调(数据包.结果) : 失败后回调(数据包.结果) })
this.客户端.write( this.编码数据(信令) ) // 开头和结尾 标记一条完整的信令
})
}
async 数据库操作(函数名, 参数数组){ return await this.发送信令({数据库路径:this.数据库路径, 函数名:函数名, 参数数组:参数数组}) }
注册_数据库操作(){
let keys= NeDB数据库.获取数据库操作keys()
keys?.forEach?.(key => this[key] = async (...参数数组)=>{return await this.数据库操作(key, 参数数组) } )
}
//服务端 备用(这两个不用socket直接打开数据库)
static nedb新建内存数据库(){ return NeDB数据库.nedb新建内存数据库() }
static async nedb打开本地数据库(路径){ return await NeDB数据库.nedb打开本地数据库(路径) } // autoload:true , 忽略数据损坏问题
static async 创建服务端进程(服务端_进程脚本路径){
//效率进程 轻量级的进程
const { utilityProcess } = await import("electron")
return new Promise((成功后回调,失败后回调)=>{
let 数据库服务进程 = utilityProcess.fork(服务端_进程脚本路径, {stdio:"inherit"} )
数据库服务进程.结束 = function(){this.postMessage({类型:"kill"}) ; this.kill() }
数据库服务进程.once('exit', (err) => { console.log("❄❄数据库服务进程退出❄❄",err) ; 失败后回调(err) })
数据库服务进程.once('message', (message) => {
if(message==="💓💓_启动完成"){
数据库服务进程.postMessage({ 类型:"🍍🍍_打开数据库Socket端口"})
成功后回调(数据库服务进程)
}})
})
return new Promise((成功后回调,失败后回调)=>{
let 数据库服务进程 = child_process.fork(服务端_进程脚本路径,{stdio:"inherit"} ) //
数据库服务进程.once('message', (message) => { if(message==="💓💓_启动完成") 成功后回调(数据库服务进程) })
数据库服务进程.once('disconnect', (err) => { console.log("❄❄数据库服务进程退出❄❄",err) ; 失败后回调(err) })
数据库服务进程.once('exit', (err) => { console.log("❄❄数据库服务进程退出❄❄",err) ; 失败后回调(err) })
数据库服务进程.once('error', (err) => { console.log("❄❄数据库服务进程退出❄❄",err) ; 失败后回调(err) })
数据库服务进程.once('close', (err) => { console.log("❄❄数据库服务进程退出❄❄",err) ; 失败后回调(err) })
数据库服务进程.结束 = function(){ this.send({类型:"kill"}) ; this.kill() }
})
}
}
/*
1. 服务进程启动完成 "💓💓_启动完成"
2. 将 MessageChannel 端口注入到 子进程
2.1 客户端发信令 "🍍🍍_打开数据库port端口" ---> 主进程发送 "🍍🍍_打开数据库port端口"
2.2 子进程收到 "🍍🍍_打开数据库port端口" ----- 端口port发送标志 "🌈🌈_远程port已经启动"
*/
class NeDB数据库_port客户端 extends Socket信令码{
constructor(数据库路径,本地port,远程port){
super()
this.注册_数据库操作() //所有自定义和原nedb的接口都可以使用
this.数据库路径 = 数据库路径
this.事件 = new events.EventEmitter();
this.计数 = 1
this.本地port = 本地port
this.远程port = 远程port
this.监听_监听信令事件戳() //正式开始
}
//配合主进程
static async 打开(数据库路径){
const 生成端口后_让主进程发送子进程 = async ()=>{
const { ipcRenderer,ipcMain,MessageChannelMain } = await import("electron")
return new Promise((成功后回调,失败后回调)=>{
if(process.type =="renderer"){
const { port1:本地port, port2:远程port } = new MessageChannel()
本地port.onmessage = messageEvent =>{ if(messageEvent.data==="🌈🌈_远程port已经启动") 成功后回调({本地port:本地port,远程port:远程port}) }
本地port.start()
ipcRenderer.postMessage("🍍🍍_打开数据库port端口", null, [远程port])
}
else if(process.type=="browser"){
const { port1:本地port, port2:远程port } = new MessageChannelMain()
本地port.on("message" , messageEvent =>{ if(messageEvent.data==="🌈🌈_远程port已经启动") 成功后回调({本地port:本地port,远程port:远程port}) } )
本地port.start()
ipcMain.emit("🍍🍍_打开数据库port端口", { ports:[远程port] } )
}
})
}
const { 本地port , 远程port } = await 生成端口后_让主进程发送子进程() //端口写入 数据库子进程
const 客户端 = new NeDB数据库_port客户端(数据库路径, 本地port, 远程port)
const 结果 = await 客户端.数据库操作("打开数据库")
console.log(结果)
return 客户端
}
监听_监听信令事件戳(){
if(process.type =="renderer"){
this.本地port.onmessage = messageEvent =>{ let 信令 = messageEvent.data ; this.事件.emit(信令.事件戳 ,信令.数据包) }
this.本地port.start()
this.本地port.onclose = err=> console.log(err)
this.本地port.onmessageerror = err=> console.log(err)
}else if(process.type=="browser"){
this.本地port.on("message" ,messageEvent =>{ let 信令 = messageEvent.data ; this.事件.emit(信令.事件戳 ,信令.数据包) } )
this.本地port.start()
this.本地port.on("close", err=> console.log(err) )
}
}
async 发送信令(数据对象){
return new Promise((成功后回调,失败后回调)=>{
let 信令 = { 事件戳: ++this.计数 , 数据包:数据对象 }
this.事件.once(信令.事件戳, 数据包=>{ (数据包.状态) ? 成功后回调(数据包.结果) : 失败后回调(数据包.结果) })
this.本地port.postMessage( 信令 ) // 开头和结尾 标记一条完整的信令
})
}
async 数据库操作(函数名, 参数数组){ return await this.发送信令({数据库路径:this.数据库路径, 函数名:函数名, 参数数组:参数数组}) }
注册_数据库操作(){
let keys= NeDB数据库.获取数据库操作keys()
keys?.forEach?.(key => this[key] = async (...参数数组)=>{return await this.数据库操作(key, 参数数组) } )
}
//服务端 备用(这两个不用socket直接打开数据库)
static nedb新建内存数据库(){ return NeDB数据库.nedb新建内存数据库() }
static async nedb打开本地数据库(路径){ return await NeDB数据库.nedb打开本地数据库(路径) } // autoload:true , 忽略数据损坏问题
static async 创建服务端进程(服务端_进程脚本路径){
//效率进程 轻量级的进程
const { utilityProcess, ipcMain } = await import("electron")
return new Promise((成功后回调,失败后回调)=>{ //此脚本vite打包后,无法被启动
let 数据库服务进程 = utilityProcess.fork(服务端_进程脚本路径 , {stdio:"inherit" } )
数据库服务进程.once('message', (message) => { if(message==="💓💓_启动完成"){ 成功后回调(数据库服务进程) }})
数据库服务进程.once('exit', (err) => { console.log("❄❄数据库服务进程退出❄❄",err) ; 失败后回调(err) })
数据库服务进程.结束 = function(){this.postMessage({类型:"kill"}) ; this.kill() }
//主进程监听
ipcMain.on("🍍🍍_打开数据库port端口", (event)=>{ 数据库服务进程.postMessage({类型:"🍍🍍_打开数据库port端口"}, event.ports ) })
})
}
}
//------------------ 服务端 ----------------------
var 数据库列表 = {}
class 服务进程Socket_nedb数据库类 { //由主进程启动一次, 渲染进程无限次连接
constructor(){
this.服务器 = null
this.创建一个net服务器()
this.监听主进程消息()
}
async 监听socket连接_接收处理信令(socket连接,信令){
let 数据包 = 信令.数据包
if(数据包.函数名 == "打开数据库"){
let 数据库名称 = /[^\\\/]+$/g.exec(数据包.数据库路径)?.[0]
if(! 数据库列表?.[数据包.数据库路径] ){
try{
数据库列表[数据包.数据库路径] = await NeDB数据库.nedb打开本地数据库(数据包.数据库路径)
socket连接.发送信令({事件戳:信令.事件戳 ,数据包:{状态:true ,结果:{状态:`🍍子进程消息: 新打开 ${数据库名称}`, 数据库表:Object.keys(数据库列表) } }})
}catch(err){ socket连接.发送信令({事件戳:信令.事件戳 ,数据包:{状态:false ,结果:{状态:`🍎子进程消息: 新打开失败 ${数据库名称}`, 数据库表:Object.keys(数据库列表), err:err}} }) }
}
else socket连接.发送信令({事件戳:信令.事件戳 ,数据包:{状态:true ,结果:{状态:`🍍子进程消息: 已存在 ${数据库名称}`, 数据库表:Object.keys(数据库列表) } }})
}
else{
try{
let 返回数据 = await 数据库列表[数据包.数据库路径][数据包.函数名](...数据包.参数数组)
socket连接.发送信令({事件戳:信令.事件戳 ,数据包: {状态:true ,结果: 返回数据} })
}catch(err){
socket连接.发送信令({事件戳:信令.事件戳 ,数据包: {状态:false ,结果: err } })
}
}
}
创建一个net服务器(){
const 信令码 = new Socket信令码()
this.服务器 = net.createServer({keepAlive:true,noDelay:true}) // 创建一个 服务器
this.服务器.maxConnections = 1000
try{ fs.unlinkSync(数据库_socket文件) }catch(err){ }
this.服务器.listen(数据库_socket文件)
//this.服务器.unref() //调用 unref() 后,则当所有客户端连接关闭后,将关闭服务器。
this.服务器.on("connection",socket连接=>{
socket连接.setKeepAlive(true)
socket连接.发送信令=(信令) => { socket连接.write( 信令码.编码数据(信令) )}
socket连接.on('close', () => { socket连接.发送信令('🍎子进程消息:close断开连接') })
socket连接.on('error', err => { socket连接.发送信令(`🍎子进程消息:error ${err.message}`) })
socket连接.on('data', Buffer数据 => {
let 信令数组 = 信令码.获取信令数组(Buffer数据)
for(let 信令 of 信令数组){ this.监听socket连接_接收处理信令(socket连接, 信令) }
})
})
}
监听主进程消息(){
const 同步信令处理表={
"kill":信令=>{
this.服务器.close()
// parentPort?.close?.()
process.exit()
}
}
process?.on?.("message", 信令=>{ 同步信令处理表[信令.类型]?.(信令) }) // child_process
// parentPort.on?.("message", 信令=>{ 同步信令处理表[信令.类型]?.(信令?.数据包) }) // worker_threads 线程
process?.parentPort?.on?.("message", 信令=>{ 同步信令处理表[信令.类型]?.(信令) }) // electron UtilityProcess
}
}
class 服务进程port_nedb数据库类 { //由主进程启动一次, 渲染进程无限次连接
constructor(MessagePort){
this.MessagePort = MessagePort
this.发送信令 = (...参数)=> this.MessagePort.postMessage(...参数)
this.监听MessagePort消息()
}
async 监听port连接_接收处理信令(信令){
let 数据包 = 信令.数据包
if(数据包.函数名 == "打开数据库"){
let 数据库名称 = /[^\\\/]+$/g.exec(数据包.数据库路径)?.[0]
if(! 数据库列表?.[数据包.数据库路径] ){
try{
数据库列表[数据包.数据库路径] = await NeDB数据库.nedb打开本地数据库(数据包.数据库路径)
this.发送信令({事件戳:信令.事件戳 ,数据包:{状态:true ,结果:{状态:`🍍子进程消息: 新打开 ${数据库名称}`, 数据库表:Object.keys(数据库列表) } }})
}catch(err){ this.发送信令({事件戳:信令.事件戳 ,数据包:{状态:false ,结果:{状态:`🍎子进程消息: 新打开失败 ${数据库名称}`, 数据库表:Object.keys(数据库列表), err:err}} }) }
}
else this.发送信令({事件戳:信令.事件戳 ,数据包:{状态:true ,结果:{状态:`🍍子进程消息: 已存在 ${数据库名称}`, 数据库表:Object.keys(数据库列表) } }})
}
else{
try{
let 返回数据 = await 数据库列表[数据包.数据库路径][数据包.函数名](...数据包.参数数组)
this.发送信令({事件戳:信令.事件戳 ,数据包: {状态:true ,结果: 返回数据} })
}catch(err){
this.发送信令({事件戳:信令.事件戳 ,数据包: {状态:false ,结果: err } })
}
}
}
监听MessagePort消息(){
this.MessagePort.on("message",(messageEvent)=>{
let 信令 = messageEvent.data
this.监听port连接_接收处理信令(信令)
})
this.MessagePort.start()
this.MessagePort.postMessage("🌈🌈_远程port已经启动") //传递启动消息
this.MessagePort.on("close" , ()=>{ console.log("数据库 MessagePort 端口关闭") }) //窗口销毁后会触发
}
static 监听Process消息(){
const 同步信令处理表={
"kill":信令=>{
this.服务器?.close?.()
// parentPort?.close?.()
process.exit()
}
}
process?.on?.("message", 信令=>{ 同步信令处理表[信令.类型]?.(信令) }) // child_process
// parentPort.on?.("message", 信令=>{ 同步信令处理表[信令.类型]?.(信令?.数据包) }) // worker_threads 线程
process?.parentPort?.on?.("message", 信令=>{ 同步信令处理表[信令.data.类型]?.(信令.data) }) // electron UtilityProcess
}
}
//================ 如果是子进程 运行服务端 ==================
if(process?.send || process?.parentPort ){ //child_process - electron UtilityProcess
let 已启动标志 = false
process?.parentPort?.on?.("message", message=>{ //electron UtilityProcess
let 信令 = message?.data
if(信令.类型==="🍍🍍_打开数据库port端口"){
let MessagePort = message.ports[0]
new 服务进程port_nedb数据库类(MessagePort)
console.log("-------nedb数据库服务 新打开 MessagePort-------")
if(!已启动标志) 服务进程port_nedb数据库类.监听Process消息()
已启动标志 = true
}
else if(信令.类型==="🍍🍍_打开数据库Socket端口"){
new 服务进程Socket_nedb数据库类()
}
})
process?.send?.("💓💓_启动完成") // child_process
// parentPort?.postMessage?.("启动完成") // worker_threads 线程
process?.parentPort?.postMessage?.("💓💓_启动完成") // electron UtilityProcess
// console.log(process.argv)
console.log("-------唯一nedb数据库服务-------")
}