[Nest + Prisma + Graphql] 三种方式打包部署nestjs应用时的问题记录
本文发布于163天前,最后更新于 163 天前,其中的信息可能已经有所发展或是发生改变。

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

以下是主要技术栈

我的开发环境:

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,所以会改变graphqlresolver和相关类实现,而在我的项目中,使用了nestGraphQLModule来动态生成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".

Generators (Reference) | Prisma Docs

查阅文档可知,这是由于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编译时只是转译typescriptjavascript,并没有包含依赖,所以使用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文件。

一般情况下__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选项也进行一些变更,在编译生成运行,就能验证这个方案是否生效了。

修改前的配置:

"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+,也还是可以接受的。

总结

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

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

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
Source: https://github.com/zhaoolee/ChineseBQB
Source: https://github.com/zhaoolee/ChineseBQB
Source: https://github.com/zhaoolee/ChineseBQB
颜文字
Emoji
小恐龙
花!
滑稽大佬
演奏
程序员专属
上一篇
下一篇