本文发布于 2023-11-15, 最后更新于 1015 天前,其中的信息可能已经有所发展或是发生改变。

文章摘要

最近由于项目需要,我选择用 Nestjs 做一个后端项目。 以下是主要技术栈 NestJS + Graphql : Documentation | NestJS - A progressive Node.js framework Typescript : TypeScript: JavaScript With Syntax For Types. (typescriptlang.org) Prism…

最近由于项目需要,我选择用Nestjs做一个后端项目。

以下是主要技术栈

我的开发环境:

orm框架没用typeorm,是想尝尝新的工具库。

网络上有很多打包nest的博文,但是和我用类似技术栈(prisma)还用pkg打包的貌似很少。因为这个框架的特殊性,给打包的过程中也带来了很多依赖加载的问题,在查询文档的过程中我还看到有使用electron的同志也遇到了类似的依赖加载问题。

这篇问题记录在记录遇到的问题的同时分享一下三种打包方式,希望给使用类似技术栈的读者能带来帮助。

webpack

他的基础配置如下:

不出意外的话要出意外了, 以下是部分报错内容

subunit (Auto identified)
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 ProjNavigationSiteBackendnode_modules@nestjsapollodistdrivers @ ./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后再生成,就能够正常生成了。

大概有这些依赖:

nginx (Auto identified)
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', ];

但是运行时仍然报错:

dart (Auto identified)
Error: Schema must contain uniquely named types but contains multiple types named "s". at new Bt (E:VSCode ProjBackenddistmain.js:16:2190263) at r.create (E:VSCode ProjBackenddistmain.js:16:36799) at p.generateSchema (E:VSCode ProjBackenddistmain.js:10:827) at p.build (E:VSCode ProjBackenddistmain.js:10:601) at h.generateSchema (E:VSCode ProjBackenddistmain.js:10:5265) at u.generateSchema (E:VSCode ProjBackenddistmain.js:2:312804) at r.onModuleInit (E:VSCode ProjBackenddistmain.js:16:2227) at async t.callModuleInitHook (E:VSCode ProjBackenddistmain.js:2:178229) at async _.callInitHook (E:VSCode ProjBackenddistmain.js:2:244363) at async _.init (E:VSCode ProjBackenddistmain.js:2:248580)

根据schema大概推测是是graphql报的错,由于webpack会压缩js,所以会改变graphqlresolver和相关类实现,而在我的项目中,使用了nestGraphQLModule来动态生成graphql的相关schema,而动态生成的过程中,会使用类的原名,这就导致了错误,因为压缩过程中会有许多同名类,也就导致多个相同名字的schema生成,上述报错就是说schema需要类型名唯一

分析出了错误,解决方法也就清晰了,修改webpack的相关配置,让TerserPlugin在压缩js时,保持类名和函数名就行了

text
optimization: { minimize: true, minimizer: [ new TerserPlugin({ terserOptions: { keep_classnames: true, keep_fnames: true, }, }), ], },

打包完成后,复制到其他文件夹运行试试,不出意外的话又会有新的错误

pgsql (Auto identified)
[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库的配置文件

复制到目录后还是报错:

text
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

把这个文件也复制到目录中再运行试试

dart (Auto identified)
> 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环境)

text
│ .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如下

运行以下指令生成镜像,然后运行镜像

text
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打包流程如下

text
pnpm build # 实际调用的是nest build pkg .distmain.js -t node16-win-x64 .main.exe

运行时, 会报以下错误

swift (Auto identified)
PrismaClientInitializationError: Unable to require(`C:snapshotBackendnode_modules.prismaclientquery_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:snapshotBackendnode_modules@prismaclientruntimelibrary.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:snapshotBackendnode_modules@prismaclientruntimelibrary.js:114:447) at async mr.instantiateLibrary (C:snapshotBackendnode_modules@prismaclientruntimelibrary.js:113:1762) { clientVersion: '5.5.2', errorCode: undefined }

根据解决方案,在package.json文件中添加了如下配置

prolog (Auto identified)
"pkg": { "assets": [ "node_modules/.prisma/client/*.node", "prisma/schema.prisma", ".env" ], "targets": [ "node16-win-x64" ], "outputPath": "bin" }

再次输入pkg .指令生成后运行仍然报相同错误, 应该是该二进制文件没有被正确打包, 即使手动复制node_modules的相关文件夹也一样报错, 具体原因我暂时没有找到相关解决方案,此打包方式尝试失败。

使用webpack先打包, 在用pkg生成

分析了一下,觉得nestjs编译时只是转译typescriptjavascript,并没有包含依赖,所以使用nestjs编译再用pkg打包会失败,pkg也没有正确的包含二进制文件,使得程序运行时无法正确加载依赖而崩溃

所以我又尝试了先使用上面的webpack打包,然后再使用pkg打包的方式,试验是否能够解决这个问题。

结果是我直接在开发目录运行能够运行成功,拷贝到其他目录时就会运行失败,说明在开发目录能够加载到相关依赖,而迁移后不行。

然后我打开打包后的main.js查找相关加载代码,找到了以下这段(已格式化):

dns (Auto identified)
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文件

再运行,成功了!

dart (Auto identified)
> 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

目录结构如下:

text
│ main.exe │ ├─ .prisma │ └─client │ query_engine-windows.dll.node │ schema.prisma

不过这样的结构总感觉不太好看。

深入一点,生成单文件

仔细观察上一节我找到的js代码

dns (Auto identified)
if ( ((nt.dirname = __dirname), !it.existsSync(rt.join(__dirname, "schema.prisma"))) )

这个if判断是先判断了path.join(__dirname, "schema.prisma")文件不存在后,才去程序目录下"node_modules/.prisma/client", ".prisma/client"的两个文件夹下找schema文件。

一般情况下__dirnameprocess.cwd()代表的都是运行的js所在的文件目录地址,但在pkg打包后,情况有所改变,以下是pkg打包前后的文件地址对应关系

valuewith nodepackagedcomments
__filename/project/app.js/snapshot/project/app.js
__dirname/project/snapshot/project
process.cwd()/project/deploysuppose the app is called ...
process.execPath/usr/bin/nodejs/deploy/app-x64app-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.entrypointundefined/snapshot/project/app.js
process.pkg.defaultEntrypointundefined/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目录,这里我只需要把pkgassets选项也进行一些变更,在编译生成运行,就能验证这个方案是否生效了。

修改前的配置:

prolog (Auto identified)
"pkg": { "assets": [ "node_modules/.prisma/client/query_engine-windows.dll.node", "prisma/schema.prisma", "dist/.env" ], "targets": [ "node16-win-x64" ], "outputPath": "bin" },

修改后的配置:

prolog (Auto identified)
"pkg": { "assets": [ "dist/query_engine-windows.dll.node", "dist/schema.prisma", "dist/.env" ], "targets": [ "node16-win-x64" ], "outputPath": "bin" },

输入pkg .打包

然后测试运行

text
PrismaClientInitializationError: error: Environment variable not found: DATABASE_URL.

缺少env文件,拷贝后再运行,成功运行。

到这里其实就可以结束了。pkg打包后,只有可执行文件和env变量文件。

不过还没有实现单可执行文件的目标。要是想要生成的文件,可以动态修改运行变量,可以显式的保留env文件。

但如果想要继续将env也打包进可执行文件内,咱们接着往下走。

看样子加载env的逻辑还得微调,加载env我用的dotenv库,而这个库默认会在process.cwd()目录下找env文件,以下是dotenv加载的部分源码

text
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的地方做出如下修改

dns (Auto identified)
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+,也还是可以接受的。

总结

文章写的比较匆忙,若有错误请加以指正。

查阅文档也许解决不了所有问题,但能解决大部分问题。查阅文档的同时配合源码的阅读那就更加快了问题的解决速度,事半功倍。所以遇事不决看文档和源码,祝各位在编码的路上不忘初心,越走越远。咱们顶峰见!

九仞之行

严于律己,宽以待人,深自警省,讷言敏行

订阅
💬 评论 (3)
单薄Chrome 128 · Windows 10日本

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 这个指定目录

希望大家一起努力 搞了好几天 才搞好

S
Styunlen博主Chrome 126 · Android中国

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文件运行,不失为一种好办法

单薄Chrome 114 · Android中国

我上面给出了webpack打包后在用pkg打包的方法,这样的话如果不想用可执行文件也可以使用单main

.js文件运行,不失为一种好办法

@Styunlen

嗯嗯 只是在网上找pkg直接打包的方法 一直没找到

解决了就分享出来 直接打包简单一点 如果系统组件不缺少的话 只需要把package的assets设置好就行

编辑器加载中…