Electron 使用 SQLite + Prisma 打包最佳实践
背景
-
开始前先说一下这个Prisma,固然很好用,在传统的Node.js里面可以直接帮我们操作数据库,而无需去关注SQL的编写。
-
但是Electron的环境下,Prisma是运行在主进程Main中的,开发环境没啥问题但是呢,一到打包的时候就不行了。
-
原因是Prisma使用运行时的Client,意思是会在node_modules/.prisma/client(pnpm则会是node_modules/.pnpm/@prisma+client@6.8.2_prisma_xxxx/node_modules/.prisma/client)文件夹里面生成真实的查询器。
-
而我们通过webpack/vite进行静态分析的过程中,没办法找到这个动态生成的查询器,也就导致了其未被打包进入Electron里面去。
-
目前我的解决方案是通过修改其动态生成的client的位置,通过package软链接依赖,实际使用时使用这个软链接依赖,让静态分析器能够找到这个Client。
-
从而,让Client能够正确被打包进项目中。说白了就是让这个动态生成的Client作为代码的一部分。在打包的命令里面需要添加npx prisma generate &&,这样执行命令就会先生成最新的Client。
![image]()
-
找了一圈,AI也问了,Github也翻了,文档也看了,网上关于修改extraResoures/asarUnpack的配置,都不行,默认存放Client的位置都没法被打包进去。
-
都会提示:A JavaScript error occurred in the main process Error: Cannot find module '@prisma/client'。
-
吐槽一下我也有尝试更新至v7版本(我现在是v6.8.2),v7去掉了schema url的配置,又要手动导入adapter麻烦的要死,能不能开箱即用啊。
-
下面给出我的解决办法。
- 修改
schema.prisma中的output路径为../src/generated/client - 在
package.json中创建软链接:"db": "link:src\\generated\\client" - 通过
import { PrismaClient } from 'db'引用 - 在 prisma.ts(/src/main/api/config/prisma.ts) 中封装复杂的引擎路径查找逻辑
- 修改
下面是详细的说明:
Electron + Prisma + pnpm 打包解决方案实践总结
问题背景
在使用 Electron + Prisma + pnpm 的项目中,打包后经常遇到以下错误:
[Window Title]
Error
[Main Instruction]
A JavaScript error occurred in the main process
[Content]
Uncaught Exception:
Error: Cannot find module '@prisma/client'
Require stack:
- C:\Users\Administrator\AppData\Local\Programs\xxx\resources\app.asar\out\main\index.js
-
at Module._resolveFilename (node:internal/modules/cjs/loader:1219:15)
at s._resolveFilename (node:electron/js2c/browser_init:2:124514)
at Module._load (node:internal/modules/cjs/loader:1050:27)
at c._load (node:electron/js2c/node_init:2:16955)
at Module.require (node:internal/modules/cjs/loader:1305:19)
at require (node:internal/modules/helpers:182:18)
at Object.<anonymous> (C:\Users\Administrator\AppData\Local\Programs\xxx\resources\app.asar\out\main\index.js:10:16)
at Module._compile (node:internal/modules/cjs/loader:1544:14)
at Module._extensions..js (node:internal/modules/cjs/loader:1629:10)
at Module.load (node:internal/modules/cjs/loader:1282:32)
[确定]
核心问题分析
Prisma 在 Electron + ASAR 环境下存在两个关键问题:
- 模块解析问题:当
@prisma/client被打包到app.asar中时,其内部的require('.prisma/client/default')会在 ASAR 内查找,导致模块找不到 - 引擎加载问题:即使使用
asarUnpack解包文件,Prisma 也无法自动感知引擎的新位置,必须通过binaryPath显式指定 - pnpm 特殊性:pnpm 使用符号链接和
.pnpm目录管理依赖,导致标准的node_modules/.prisma路径实际上不存在
我们的解决方案
经过实践,我们采用了以下方案,已在生产环境稳定运行:
1. 修改 Prisma Client 输出路径
文件: prisma/schema.prisma
generator client {
provider = "prisma-client-js"
output = "../src/generated/client" // 自定义输出到 src 目录
binaryTargets = ["native", "windows"]
previewFeatures = ["driverAdapters"]
}
datasource db {
provider = "sqlite"
url = "file:./app.db"
}
关键点:
- 将 Prisma Client 生成到
src/generated/client目录 - 这样可以确保生成的代码被打包到
out目录中 - 避免了 pnpm 符号链接带来的路径问题
2. 创建 package.json 软链接
文件: package.json
{
"dependencies": {
"@prisma/client": "^6.8.2",
"db": "link:src\\generated\\client", // 创建软链接
"prisma": "^6.8.2"
}
}
关键点:
- 使用
link:协议创建本地包链接 - 允许通过
import { PrismaClient } from 'db'引用 - 保持代码的简洁性
3. 封装引擎路径查找逻辑
文件: src/main/api/config/prisma.ts
import { PrismaClient } from 'db'
import { app } from 'electron'
import path from 'path'
import { existsSync } from 'fs'
import { dbPath, isDev } from '../../config/database-path'
import { isDevelopment } from '../../config/env'
import logger from './logger'
/**
* 获取平台名称(包括 ARM64 检测)
*/
function getPlatformName(): string {
const isDarwin = process.platform === 'darwin'
if (isDarwin && process.arch === 'arm64') {
return process.platform + 'Arm64'
}
return process.platform
}
/**
* 平台到引擎文件的映射
*/
const platformToExecutables: Record<string, { queryEngine: string; migrationEngine: string }> = {
win32: {
queryEngine: 'query_engine-windows.dll.node',
migrationEngine: 'migration-engine-windows.exe'
},
linux: {
queryEngine: 'libquery_engine-debian-openssl-1.1.x.so.node',
migrationEngine: 'migration-engine-debian-openssl-1.1.x'
},
darwin: {
queryEngine: 'libquery_engine-darwin.dylib.node',
migrationEngine: 'migration-engine-darwin'
},
darwinArm64: {
queryEngine: 'libquery_engine-darwin-arm64.dylib.node',
migrationEngine: 'migration-engine-darwin-arm64'
}
}
/**
* 获取查询引擎库文件路径
* 在打包环境中,需要在多个可能的位置查找引擎文件
*/
function getQueryEnginePath(): string | null {
try {
// 开发环境让 Prisma 自动找到引擎
if (!app || isDevelopment) {
return null
}
const platformName = getPlatformName()
const engines = platformToExecutables[platformName]
if (!engines) {
logger.warn(`不支持的平台: ${platformName}`)
return null
}
// 获取应用路径
const appPath = app.getAppPath()
const isAsarPackaged = appPath.includes('app.asar')
let extraResourcesPath: string
if (isAsarPackaged) {
// 如果是打包的 asar 文件
extraResourcesPath = path.join(path.dirname(appPath), '..')
} else {
// 如果是解压后的目录
extraResourcesPath = path.dirname(appPath)
}
// 构建查询引擎路径 - 多个可能的位置
const possiblePaths = [
// extraResources 中的路径
path.join(extraResourcesPath, 'node_modules', '@prisma', 'engines', engines.queryEngine),
// app.asar.unpacked 中的路径
path.join(
extraResourcesPath,
'app.asar.unpacked',
'node_modules',
'@prisma',
'engines',
engines.queryEngine
),
// resources 目录中的路径
path.join(process.resourcesPath, 'node_modules', '@prisma', 'engines', engines.queryEngine),
// 应用目录中的路径
path.join(
appPath.replace('app.asar', 'app.asar.unpacked'),
'node_modules',
'@prisma',
'engines',
engines.queryEngine
),
// 备用路径
path.join(
path.dirname(extraResourcesPath),
'node_modules',
'@prisma',
'engines',
engines.queryEngine
)
]
// 查找第一个存在的引擎文件
for (const enginePath of possiblePaths) {
if (existsSync(enginePath)) {
logger.info(`找到 Prisma 查询引擎: ${enginePath}`)
return enginePath
}
}
logger.warn(`未找到 Prisma 查询引擎文件: ${engines.queryEngine}`)
logger.warn('检查的路径:')
possiblePaths.forEach((p) => logger.warn(` - ${p}`))
return null
} catch (error) {
logger.error('获取查询引擎路径时出错:', error)
return null
}
}
/**
* 创建配置好的 PrismaClient 实例
*/
export function createPrismaClient(options?: any): PrismaClient {
const enginePath = getQueryEnginePath()
// 动态设置数据库URL
const databaseUrl = isDev ? 'file:./app.db' : `file:${dbPath}`
logger.info(`使用的数据库 URL: ${databaseUrl}`)
logger.info(`数据库文件路径: ${dbPath}`)
logger.info(`查询引擎路径: ${enginePath}`)
const prismaOptions: any = {
...options,
datasources: {
db: {
url: databaseUrl
}
},
log: isDevelopment ? ['warn', 'error'] : ['error']
}
// 如果找到了引擎路径,则配置它
if (enginePath) {
prismaOptions.__internal = {
...options?.__internal,
engine: {
...options?.__internal?.engine,
binaryPath: enginePath
}
}
}
return new PrismaClient(prismaOptions)
}
// 导出默认配置的客户端实例
export const prisma = createPrismaClient()
// 为了向后兼容,也导出 PrismaClient 类型
export { PrismaClient } from 'db'
/** database-path.ts */
import path from 'path'
import { app } from 'electron'
import { isDevelopment } from './env'
export const isDev = isDevelopment
/**
* 获取数据库文件路径
* 生产环境:放在应用数据目录
* 开发环境:放在项目根目录
*/
export const getDatabasePath = (): string => {
if (isDev) {
// 开发环境:使用项目根目录
return path.join(process.cwd(), 'prisma', 'app.db')
} else {
// 生产环境:使用应用数据目录
const userDataPath = app.getPath('userData')
return path.join(userDataPath, 'app.db')
}
}
export const dbPath = getDatabasePath()
关键点:
- 封装了跨平台的引擎文件名映射
- 实现了多路径查找逻辑,提高容错性
- 通过
__internal.engine.binaryPath显式指定引擎位置 - 开发环境和生产环境自动切换
4. 配置 electron-builder
文件: electron-builder.yml
appId: com.electron.app
productName: XXX
directories:
buildResources: build
output: dist
files:
- out/**/*
- prisma/**/*
- '!prisma/app.db'
- '!**/.vscode/*'
- '!src/*'
- '!electron.vite.config.{js,ts,mjs,cjs}'
- '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}'
- '!{.env*,.npmrc,pnpm-lock.yaml}'
- '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
- '!mock/**/*'
- '!demo/**/*'
- '!docs/**/*'
- '!logs/**/*'
- '!scripts/**/*'
- '!build/**/*'
# 排除不需要的 Prisma 引擎文件
- '!**/node_modules/@prisma/engines/introspection-engine*'
- '!**/node_modules/@prisma/engines/migration-engine*'
- '!**/node_modules/@prisma/engines/prisma-fmt*'
- '!**/node_modules/prisma/**/*.mjs'
- '!**/.*'
asar: true
asarUnpack:
- 'prisma/**/*' # 解包 prisma schema 文件
win:
executableName: 'XXX'
nsis:
artifactName: ${name}-${version}-setup.${ext}
shortcutName: ${productName}
uninstallDisplayName: ${productName}
createDesktopShortcut: always
关键点:
- 使用
asar: true启用 ASAR 打包 - 通过
asarUnpack解包prisma目录(包含 schema 文件) - 排除不需要的 Prisma 引擎文件,减小打包体积
- 不需要配置
extraResources,因为生成的 client 已经在out目录中
方案优势
✅ 解决 pnpm 问题:通过自定义输出路径,完全避开 pnpm 的符号链接机制 ✅ 路径可控:生成的代码在 src 目录中,打包后在 out 目录中,路径清晰可预测✅ 跨平台支持:封装了平台检测和引擎文件映射,支持 Windows、macOS、Linux ✅ 容错性强:多路径查找逻辑,提高了在不同打包环境下的兼容性✅ 开发体验好:通过软链接保持代码简洁,import from 'db' 比 '@prisma/client' 更短✅ 生产稳定:已在实际项目中验证,打包后运行稳定
使用方法
开发环境
# 生成 Prisma Client
npx prisma generate
# 启动开发服务器
npm run dev
生产环境
# 构建并打包
npm run build
npm run buildw # Windows 打包
代码中使用
// 导入 Prisma Client
import { prisma } from '@/api/config/prisma'
// 使用数据库操作
const users = await prisma.user.findMany()
验证方法
打包后检查以下内容:
- ✅
dist/win-unpacked/resources/app.asar.unpacked/prisma/存在 schema 文件 - ✅
dist/win-unpacked/resources/app.asar.unpacked/node_modules/@prisma/engines/存在查询引擎 - ✅ 运行打包后的应用,数据库操作正常
- ✅ 查看日志,确认引擎路径被正确找到
注意事项
1. Prisma 版本升级
每次升级 Prisma 版本后,需要重新生成 client:
npx prisma generate
2. 跨平台打包
如果需要为多个平台打包,确保在 schema.prisma 中配置了对应的 binaryTargets:
generator client {
provider = "prisma-client-js"
output = "../src/generated/client"
binaryTargets = ["native", "windows", "darwin", "darwin-arm64", "linux-musl"]
}
3. 数据库文件位置
生产环境的数据库文件应该放在用户数据目录,而不是应用安装目录:
// ✅ 正确
const dbPath = path.join(app.getPath('userData'), 'app.db')
// ❌ 错误(安装目录通常是只读的)
const dbPath = path.join(app.getAppPath(), 'app.db')
常见问题
Q: 为什么不使用 Prisma 默认的 node_modules/.prisma/client?
A: 在 pnpm 环境下,node_modules/.prisma 实际上是一个符号链接,指向 node_modules/.pnpm/@prisma+client@版本号_hash/node_modules/.prisma。这个路径在打包后会失效,导致模块找不到。通过自定义输出路径到 src 目录,可以确保代码被正确打包。
Q: 为什么需要 getQueryEnginePath 函数?
A: Prisma Client 在运行时需要加载查询引擎二进制文件。在 Electron 打包后,文件位置会发生变化,Prisma 无法自动感知新位置。必须通过 __internal.engine.binaryPath 显式指定引擎路径。getQueryEnginePath 函数实现了多路径查找逻辑,提高了容错性。
Q: 可以使用 extraResources 代替这个方案吗?
A: 理论上可以,但在 pnpm 环境下会更复杂:
- 需要在
extraResources中配置 pnpm 的实际路径(包含版本号和 hash) - 每次升级 Prisma 版本都需要更新这个路径
- 需要额外配置
module.paths来让 Node.js 能找到 resources 目录中的模块
我们的方案通过自定义输出路径,避免了这些复杂性。
环境信息
- Prisma: 6.8.2
- Electron: 34.2.0
- electron-builder: 26.1.0
- pnpm: 10.4.1
- Node.js: 22.x
- 操作系统: Windows 11
总结
这个方案的核心思路是:
- 避开 pnpm 的符号链接:通过自定义 Prisma Client 输出路径
- 简化引用:通过 package.json 软链接保持代码简洁
- 显式指定引擎路径:通过封装的查找逻辑和
binaryPath配置 - 提高容错性:通过多路径查找机制
希望这个方案能帮助到遇到类似问题的开发者!
最后更新: 2026-01-08


浙公网安备 33010602011771号