最近由于项目需要,我选择用Nestjs
做一个后端项目。
以下是主要技术栈
- NestJS + Graphql : Documentation | NestJS - A progressive Node.js framework
- Typescript : TypeScript: JavaScript With Syntax For Types. (typescriptlang.org)
- Prisma : Prisma | Next-generation ORM for Node.js & TypeScript
我的开发环境:
- Windows 11 23H2 欢迎使用 Windows (microsoft.com)
- Node v16.18.1: Index of /dist/v16.18.1/ (nodejs.org)
- pnpm v8.10.3 : Fast, disk space efficient package manager | pnpm
- MYSQL 8.0 : MySQL
orm
框架没用typeorm
,是想尝尝新的工具库。
网络上有很多打包nest
的博文,但是和我用类似技术栈(prisma
)还用pkg
打包的貌似很少。因为这个框架的特殊性,给打包的过程中也带来了很多依赖加载的问题,在查询文档的过程中我还看到有使用electron
的同志也遇到了类似的依赖加载问题。
这篇问题记录在记录遇到的问题的同时分享一下三种打包方式,希望给使用类似技术栈的读者能带来帮助。
webpack
webpack打包时, 我参考了这篇文章的相关内容: Nest系列(八)一路坎坷,我实现了最简便的方式打包部署nestjs+prisma应用 - 掘金
他的基础配置如下:
不出意外的话要出意外了, 以下是部分报错内容
ERROR in ./node_modules/@nestjs/apollo/dist/drivers/apollo-base.driver.js 100:155-190
Module not found: Error: Can't resolve '@as-integrations/fastify' in 'E:\VSCode Proj\NavigationSite\Backend\node_modules\@nestjs\apollo\dist\drivers
@ ./node_modules/@nestjs/apollo/dist/drivers/apollo-gateway.driver.js 10:29-60
@ ./node_modules/@nestjs/apollo/dist/drivers/index.js 5:21-55
@ ./node_modules/@nestjs/apollo/dist/index.js 5:21-41
@ ./src/app.module.ts 20:17-42
@ ./src/main.ts 4:21-44
可以看出,都是类似于某某依赖无法正常导入,我将报错出现过的依赖全部添加进webpack.IgnorePlugin
插件里的lazyImports
后再生成,就能够正常生成了。
大概有这些依赖:
const lazyImports = [
'@nestjs/websockets/socket-module',
'@nestjs/microservices',
'@nestjs/microservices/microservices-module',
'ts-morph',
'@as-integrations/fastify',
'@apollo/gateway',
'@apollo/subgraph',
'@apollo/subgraph/package.json',
'@apollo/subgraph/dist/directives',
'cache-manager',
'class-validator',
'class-transformer',
'class-transformer/storage',
];
但是运行时仍然报错:
Error: Schema must contain uniquely named types but contains multiple types named "s".
at new Bt (E:\VSCode Proj\Backend\dist\main.js:16:2190263)
at r.create (E:\VSCode Proj\Backend\dist\main.js:16:36799)
at p.generateSchema (E:\VSCode Proj\Backend\dist\main.js:10:827)
at p.build (E:\VSCode Proj\Backend\dist\main.js:10:601)
at h.generateSchema (E:\VSCode Proj\Backend\dist\main.js:10:5265)
at u.generateSchema (E:\VSCode Proj\Backend\dist\main.js:2:312804)
at r.onModuleInit (E:\VSCode Proj\Backend\dist\main.js:16:2227)
at async t.callModuleInitHook (E:\VSCode Proj\Backend\dist\main.js:2:178229)
at async _.callInitHook (E:\VSCode Proj\Backend\dist\main.js:2:244363)
at async _.init (E:\VSCode Proj\Backend\dist\main.js:2:248580)
根据schema
大概推测是是graphql
报的错,由于webpack
会压缩js
,所以会改变graphql
的resolver
和相关类实现,而在我的项目中,使用了nest
的GraphQLModule
来动态生成graphql
的相关schema
,而动态生成的过程中,会使用类的原名,这就导致了错误,因为压缩过程中会有许多同名类,也就导致多个相同名字的schema
生成,上述报错就是说schema
需要类型名唯一
我是使用以下方式加载的
GraphQLModule
GraphQLModule.forRoot<ApolloDriverConfig>({ driver: ApolloDriver, autoSchemaFile: join(process.cwd(), 'src/schema.gql'), }),
分析出了错误,解决方法也就清晰了,修改webpack
的相关配置,让TerserPlugin
在压缩js
时,保持类名和函数名就行了
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
terserOptions: {
keep_classnames: true,
keep_fnames: true,
},
}),
],
},
打包完成后,复制到其他文件夹运行试试,不出意外的话又会有新的错误
[Nest] 31708 - 2023/11/15 17:11:31 ERROR [ExceptionHandler] Prisma Client could not find its `schema.prisma`. This is likely caused by a bundling step, which leads to `schema.prisma` not being copied near the resulting bundle. We would appreciate if you could take the time to share some information with us.
Please help us by answering a few questions: https://pris.ly/bundler-investigation-error
大概就是缺少文件,把运行时会用到的文件也拷贝到这个文件夹,主要有这两个文件
- .env : 环境配置
- schema.prisma : prisma库的配置文件
复制到目录后还是报错:
PrismaClientInitializationError: Prisma Client could not locate the Query Engine for runtime "windows".
查阅文档可知,这是由于prisma
会根据目标平台(开发环境)生成一个二进制文件,这个二进制文件是prisma
与数据库交互的必备组件,如果没有修改schema.prisma
配置文件里generator client
配置的output
属性的话,这个二进制文件的默认路径在node_modules/.prisma/client/
文件夹下,如果修改了,则在output
目录下,后缀为.node
, windows
环境下这个文件是query_engine-windows.dll.node
把这个文件也复制到目录中再运行试试
> node main.js
…………
[Nest] 57292 - 2023/11/15 17:51:05 LOG [InstanceLoader] GraphQLSchemaBuilderModule dependencies initialized +1ms
[Nest] 57292 - 2023/11/15 17:51:05 LOG [InstanceLoader] GraphQLModule dependencies initialized +0ms
[Nest] 57292 - 2023/11/15 17:51:05 LOG [RoutesResolver] AppController {/}: +6ms
[Nest] 57292 - 2023/11/15 17:51:05 LOG [RouterExplorer] Mapped {/, GET} route +4ms
[Nest] 57292 - 2023/11/15 17:51:05 LOG [GraphQLModule] Mapped {/graphql, POST} route +99ms
[Nest] 57292 - 2023/11/15 17:51:05 LOG [NestApplication] Nest application successfully started +11ms
Ok,启动成功,打包过后大概有这些文件(windows环境)
│ .env
│ main.js
│ query_engine-windows.dll.node
│ schema.prisma
只要移植这些文件到一个拥有node
环境的windows
电脑上就能运行。(node
版本最好一致)
如果要移植到linux
,根据上述prisma
的文档,修改schema.prisma
配置文件里generator client
配置的binaryTargets
属性为指定架构的linux
就行,具体填写的值,请参考文档。之后使用webpack
打包完,像上面一样把二进制文件也复制打包到目标计算机,就能够正常运行了,得益于nodejs
的跨平台特性啊。
webpack配置微调
上面的webpack
打包时没有任何输出,且得手动复制prisma
的相关依赖文件,下面是我添加了进度条插件和复制插件后的webpack
配置,需要自取
docker
docker打包没什么阻力, 主要是docker镜像的大小有点超出我的预料, 足足500M, 压缩完也还有100M+., 所以放弃了使用docker部署的念头。
Dockerfile如下
运行以下指令生成镜像,然后运行镜像
docker build -t backend .
docker run -p 3000:3000 <镜像ID>
由于我的服务器环境中,mysql
运行在宿主机上, 所以要将mysql
的连接地址从localhost
改为host.docker.internal
, 使得docker
容器能够正常访问到宿主机的mysql
数据库。
docker
打包成功,正常运行,但是镜像文件过于庞大,还是选择使用webpack
打包后直接在node
环境运行
pkg打包
使用nest编译, pkg打包 (失败)
pkg打包流程如下
pnpm build # 实际调用的是nest build
pkg .\dist\main.js -t node16-win-x64
.\main.exe
运行时, 会报以下错误
PrismaClientInitializationError: Unable to require(`C:\snapshot\Backend\node_modules\.prisma\client\query_engine-windows.dll.node`).
The Prisma engines do not seem to be compatible with your system. Please refer to the documentation about Prisma's system requirements: https://pris.ly/d/system-requirements
Details: UNEXPECTED-15
at fn.loadLibrary (C:\snapshot\Backend\node_modules\@prisma\client\runtime\library.js:112:10067)
at processTicksAndRejections (node:internal/process/task_queues:96:5)
at process.runNextTicks [as _tickCallback] (node:internal/process/task_queues:65:3)
at Function.runMain (pkg/prelude/bootstrap.js:1980:13)
at node:internal/main/run_main_module:17:47
at async mr.loadEngine (C:\snapshot\Backend\node_modules\@prisma\client\runtime\library.js:114:447)
at async mr.instantiateLibrary (C:\snapshot\Backend\node_modules\@prisma\client\runtime\library.js:113:1762) {
clientVersion: '5.5.2',
errorCode: undefined
}
prisma官方有类似解决方案: Solve ENOENT package error with vercel/pkg | Prisma Docs
根据解决方案,在package.json文件中添加了如下配置
"pkg": {
"assets": [
"node_modules/.prisma/client/*.node",
"prisma/schema.prisma",
".env"
],
"targets": [
"node16-win-x64"
],
"outputPath": "bin"
}
再次输入pkg .
指令生成后运行仍然报相同错误, 应该是该二进制文件没有被正确打包, 即使手动复制node_modules
的相关文件夹也一样报错, 具体原因我暂时没有找到相关解决方案,此打包方式尝试失败。
类似问题相关github issue讨论页面: Cannot access engine inside vercel/pkg · Issue #8449 · prisma/prisma (github.com)
使用webpack先打包, 在用pkg生成
分析了一下,觉得nestjs
编译时只是转译typescript
为javascript
,并没有包含依赖,所以使用nestjs
编译再用pkg
打包会失败,pkg
也没有正确的包含二进制文件,使得程序运行时无法正确加载依赖而崩溃
所以我又尝试了先使用上面的webpack
打包,然后再使用pkg
打包的方式,试验是否能够解决这个问题。
结果是我直接在开发目录运行能够运行成功,拷贝到其他目录时就会运行失败,说明在开发目录能够加载到相关依赖,而迁移后不行。
然后我打开打包后的main.js
查找相关加载代码,找到了以下这段(已格式化):
it = T(57147);
if (
((nt.dirname = __dirname),
!it.existsSync(rt.join(__dirname, "schema.prisma")))
) {
const c = ["node_modules/.prisma/client", ".prisma/client"],
p =
c.find((c) =>
it.existsSync(rt.join(process.cwd(), c, "schema.prisma"))
) ?? c[0];
(nt.dirname = rt.join(process.cwd(), p)), (nt.isBundled = !0);
}
因为有process.cwd()
动态获取程序运行目录,所以我初步判断这就是动态加载prisma
二进制文件引擎和schema
文件的代码。
尝试在程序目录下创建.prisma/client
文件,并拷入二进制文件和schema.prisma
文件
再运行,成功了!
> node main.js
…………
[Nest] 26416 - 2023-11-15 18:50:27 LOG [InstanceLoader] GraphQLSchemaBuilderModule dependencies initialized +0ms
[Nest] 26416 - 2023-11-15 18:50:27 LOG [InstanceLoader] GraphQLModule dependencies initialized +0ms
[Nest] 26416 - 2023-11-15 18:50:27 LOG [RoutesResolver] AppController {/}: +2ms
[Nest] 26416 - 2023-11-15 18:50:27 LOG [RouterExplorer] Mapped {/, GET} route +1ms
[Nest] 26416 - 2023-11-15 18:50:27 LOG [GraphQLModule] Mapped {/graphql, POST} route +56ms
[Nest] 26416 - 2023-11-15 18:50:27 LOG [NestApplication] Nest application successfully started +36ms
目录结构如下:
│ main.exe
│
├─ .prisma
│ └─client
│ query_engine-windows.dll.node
│ schema.prisma
不过这样的结构总感觉不太好看。
深入一点,生成单文件
仔细观察上一节我找到的js代码
if (
((nt.dirname = __dirname),
!it.existsSync(rt.join(__dirname, "schema.prisma")))
)
这个if
判断是先判断了path.join(__dirname, "schema.prisma")
文件不存在后,才去程序目录下"node_modules/.prisma/client", ".prisma/client"
的两个文件夹下找schema
文件。
一般情况下__dirname
与process.cwd()
代表的都是运行的js
所在的文件目录地址,但在pkg
打包后,情况有所改变,以下是pkg
打包前后的文件地址对应关系
value | with node | packaged | comments |
---|---|---|---|
__filename | /project/app.js | /snapshot/project/app.js | |
__dirname | /project | /snapshot/project | |
process.cwd() | /project | /deploy | suppose the app is called ... |
process.execPath | /usr/bin/nodejs | /deploy/app-x64 | app-x64 and run in /deploy |
process.argv[0] | /usr/bin/nodejs | /deploy/app-x64 | |
process.argv[1] | /project/app.js | /snapshot/project/app.js | |
process.pkg.entrypoint | undefined | /snapshot/project/app.js | |
process.pkg.defaultEntrypoint | undefined | /snapshot/project/app.js | |
require.main.filename | /project/app.js | /snapshot/project/app.js |
只需要关注第二行和第三行, 从中可知process.cwd()
运行时还是保持程序运行目录,但是__dirname
会变成打包时,打包的js
文件所在的目录(相对路径)。
而上面的if
使用的是path.join(__dirname, "schema.prisma")
来合成schema
所在地址,知道这些,解决方案也就大概知道啦,那就是把依赖和webpack
打包生成的dist放在同一目录下。
刚才配置webpack
的时候我就用CopyPlugin
自动复制了prisma
相关依赖到最终的dist
目录,这里我只需要把pkg
的assets
选项也进行一些变更,在编译生成运行,就能验证这个方案是否生效了。
修改前的配置:
"pkg": {
"assets": [
"node_modules/.prisma/client/query_engine-windows.dll.node",
"prisma/schema.prisma",
"dist/.env"
],
"targets": [
"node16-win-x64"
],
"outputPath": "bin"
},
修改后的配置:
"pkg": {
"assets": [
"dist/query_engine-windows.dll.node",
"dist/schema.prisma",
"dist/.env"
],
"targets": [
"node16-win-x64"
],
"outputPath": "bin"
},
输入pkg .
打包
然后测试运行
PrismaClientInitializationError: error: Environment variable not found: DATABASE_URL.
缺少env
文件,拷贝后再运行,成功运行。
到这里其实就可以结束了。pkg打包后,只有可执行文件和env变量文件。
不过还没有实现单可执行文件的目标。要是想要生成的文件,可以动态修改运行变量,可以显式的保留env
文件。
但如果想要继续将env也打包进可执行文件内,咱们接着往下走。
看样子加载env
的逻辑还得微调,加载env
我用的dotenv
库,而这个库默认会在process.cwd()
目录下找env
文件,以下是dotenv
加载的部分源码
let dotenvPath = path.resolve(process.cwd(), '.env')
if (options && options.path && options.path.length > 0) {
dotenvPath = options.path
}
所以我们只需要在dotenv
加载env
的时候判断一下文件是否存在,不存在就使用
path.join(__dirname, ".env")
目录就能够修复这个小问题。而且不会对开发测试有影响。(vscode
会甚至会自动将env文件里的变量添加进process.env
,而不用手动加载)
在加载env
的地方做出如下修改
const envPath = fs.existsSync(path.resolve(process.cwd(), '.env'))
? path.resolve(process.cwd(), '.env')
: path.resolve(__dirname, '.env');
env.config({ path: envPath });
打包编译运行,成功。至此,成功实现pkg
打包成单文件运行后端。就是这体积。。。
100M+,当然打包的时候加上--compress Brotli
参数能压缩到50M+,也还是可以接受的。
总结
文章写的比较匆忙,若有错误请加以指正。
查阅文档也许解决不了所有问题,但能解决大部分问题。查阅文档的同时配合源码的阅读那就更加快了问题的解决速度,事半功倍。所以遇事不决看文档和源码,祝各位在编码的路上不忘初心,越走越远。咱们顶峰见!
pkg打包解决了 直接打包 原因调试的时候可以看出 Error! Error: spawnSync patch ENOENT
patch 要安装这个
过程:
先调试输出
Error! Error: spawnSync patch ENOENT
安装patch
Install the patch utility -> https://sourceforge.net/projects/gnuwin32/files/patch/
if you have Chocolatey installed, then you can simply run choco install patch in an elevated command prompt.
安装好之后 还要在资源里面加上
"assets": [
"node_modules/.prisma/*/",
"node_modules/@prisma/*/",
".env"
],
我是把prisma文件夹资源放在同级目录的
如果要打包进去 在上面加进去
这样的话 在执行这些文件的时候就要指定path了
prisma 用 --schema 这个指定目录
希望大家一起努力 搞了好几天 才搞好
我上面给出了webpack打包后在用pkg打包的方法,这样的话如果不想用可执行文件也可以使用单main
.js文件运行,不失为一种好办法
嗯嗯 只是在网上找pkg直接打包的方法 一直没找到
解决了就分享出来 直接打包简单一点 如果系统组件不缺少的话 只需要把package的assets设置好就行