捆绑 NestJS + TypeORM 应用程序(使用 webpack)

Adr*_*erc 14 bundling-and-minification typeorm nestjs

我最近必须考虑一个新软件的部署方法,该软件是用以下代码编写的:

  • NestJS 6 / Express
  • 类型ORM 0.2
  • 使用 TypeScript

该软件将部署在160多台服务器上,分布在欧洲各地,其中一些服务器的互联网连接非常糟糕。

我做了一些研究,很多人明确建议 反对捆绑。主要论点是,原生扩展将因像webpack或 这样的捆绑器而失败rollup(剧透:这是真的,但有一个解决方案)。在我看来,这很大程度上是因为人们不关心这一点:作者对这个用例node-pre-gyp使用了几乎相同的词语。因此,通常情况下,我被告知要使用yarn install同步node_modules/文件夹

该项目是新的,但node_modules/文件夹已经超过 480 MB。使用 XZ 进行最大压缩后,我得到了 20 MB 的存档。这对我来说仍然太大了,而且似乎是对资源的巨大浪费。

我还看了以下问答:

TypeORM 还有一些单独的问答,但所有这些似乎都需要安装ts-nodeor typescript

Adr*_*erc 17

更新 2022-09-05

\n

现在TypeORM 在 0.3.0 版本中ormconfig.json已被严重弃用,数据源定义是一个单独的 JS(甚至 TS)文件,必须由 Webpack 捆绑。

\n

这很棘手,因为这将是一个动态 require(),因此 Webpack 不知道如何在 TypeORM 代码中处理它。提示是library入口点定义中的关键,没有名称。您可以转到上面的代码,但这里是摘录:

\n
  entry: {\n    main: \'./src/main.ts\',\n    console: "./src/console.ts",\n    \'data-source\': {\n      import: \'./src/data-source.ts\',\n      library: {\n        type: \'commonjs2\'\n      }\n    },\n    typeorm:  \'./node_modules/typeorm/cli.js\',\n  },\n
Run Code Online (Sandbox Code Playgroud)\n

更新 2021-08-25

\n

原始响应是使用 NestJS 6 完成的,该版本使用 Webpack 4。\n由于 NestJS 8 使用 Webpack 5,因此可以使用块分割,并提供更好的解决方案。

\n

我还集成了webpack-merge的使用,使其只有一个配置文件。\n这只会更改您在阅读时看到的 Webpack 配置。

\n

原答案

\n

我设法找到了一个很好的解决方案,使用以下工具生成 2.7 MB 的独立 RPM:

\n
    \n
  • webpack具有特殊配置
  • \n
  • RPM,使用webpack, 来分发生成的文件。
  • \n
\n

该软件是一个 API 服务器,使用 PostgreSQL 进行持久化。用户通常使用外部服务器进行身份验证,但我们可以拥有本地(紧急)用户,因此我们用于bcrypt存储和检查密码。

\n

我必须坚持:我的解决方案不适用于本机扩展。幸运的是,流行的bcrypt可以替换为纯 JS 实现,并且最流行的 postgresql 包既可以使用编译的也可以使用纯 JS。

\n

如果想与本机扩展捆绑,您可以尝试使用ncc。他们设法为依赖包实现了一个解决方案node-pre-gyp,该解决方案在一些初步测试中对我有用。当然,编译的扩展应该与您的目标平台相匹配,就像编译的东西一样。

\n

我个人选择是webpack因为 NestJS在它的build命令中支持这个。这只是webpack编译器的一个传递,但它似乎调整了一些路径,所以它更容易一些。

\n

那么,如何实现这一目标呢?webpack可以将所有内容捆绑在一个文件中,但在这个用例中,我需要其中三个:

\n
    \n
  • 主程序
  • \n
  • TypeORM 迁移 CLI工具
  • \n
  • TypeORM 迁移脚本,因为它们无法与该工具捆绑在一起,因为它依赖于文件名
  • \n
\n

由于每个捆绑都需要不同的 options\xe2\x80\xa6 我使用了 3 个webpack文件。这是布局:

\n
webpack.config.js\nwebpack\n\xe2\x94\x9c\xe2\x94\x80\xe2\x94\x80 migrations.config.js\n\xe2\x94\x94\xe2\x94\x80\xe2\x94\x80 typeorm-cli.config.js\n
Run Code Online (Sandbox Code Playgroud)\n

所有这些文件都基于ZenSoftware 善意提供的相同模板。主要区别是我从 切换IgnorePlugin到 ,externals因为它更易于阅读,并且完全适合用例。

\n
webpack.config.js\nwebpack\n\xe2\x94\x9c\xe2\x94\x80\xe2\x94\x80 migrations.config.js\n\xe2\x94\x94\xe2\x94\x80\xe2\x94\x80 typeorm-cli.config.js\n
Run Code Online (Sandbox Code Playgroud)\n

TypeORM 的配置文件更加冗长,因为我们需要显式使用 TypeScript。幸运的是,他们在常见问题解答中对此提供了一些建议。然而,捆绑迁移工具还需要两个技巧:

\n\n
// webpack.config.js\nconst { NODE_ENV = \'production\' } = process.env;\n\nconsole.log(`-- Webpack <${NODE_ENV}> build --`);\n\nmodule.exports = {\n  target: \'node\',\n  mode: NODE_ENV,\n  externals: [\n    // Here are listed all optional dependencies of NestJS,\n    // that are not installed and not required by my project\n    {\n      \'fastify-swagger\': \'commonjs2 fastify-swagger\',\n      \'aws-sdk\': \'commonjs2 aws-sdk\',\n      \'@nestjs/websockets/socket-module\': \'commonjs2 @nestjs/websockets/socket-module\',\n      \'@nestjs/microservices/microservices-module\': \'commonjs2 @nestjs/microservices/microservices-module\',\n      \n      // I\'ll skip pg-native in the production deployement, and use the pure JS implementation\n      \'pg-native\': \'commonjs2 pg-native\'\n    }\n  ],\n  optimization: {\n    // Minimization doesn\'t work with @Module annotation\n    minimize: false,\n  }\n};\n
Run Code Online (Sandbox Code Playgroud)\n
// webpack/typeorm-cli.config.js\n\nconst path = require(\'path\');\n// TypeScript compilation option\nconst TsconfigPathsPlugin = require(\'tsconfig-paths-webpack-plugin\');\n// Don\'t try to replace require calls to dynamic files\nconst IgnoreDynamicRequire = require(\'webpack-ignore-dynamic-require\');\n\nconst { NODE_ENV = \'production\' } = process.env;\n\nconsole.log(`-- Webpack <${NODE_ENV}> build for TypeORM CLI --`);\n\nmodule.exports = {\n  target: \'node\',\n  mode: NODE_ENV,\n  entry: \'./node_modules/typeorm/cli.js\',\n  output: {\n    // Remember that this file is in a subdirectory, so the output should be in the dist/\n    // directory of the project root\n    path: path.resolve(__dirname, \'../dist\'),\n    filename: \'migration.js\',\n  },\n  resolve: {\n    extensions: [\'.ts\', \'.js\'],\n    // Use the same configuration as NestJS\n    plugins: [new TsconfigPathsPlugin({ configFile: \'./tsconfig.build.json\' })],\n  },\n  module: {\n    rules: [\n      { test: /\\.ts$/, loader: \'ts-loader\' },\n      // Skip the shebang of typeorm/cli.js\n      { test: /\\.[tj]s$/i, loader: \'shebang-loader\' }\n    ],\n  },\n  externals: [\n    {\n      // I\'ll skip pg-native in the production deployement, and use the pure JS implementation\n      \'pg-native\': \'commonjs2 pg-native\'\n    }\n  ],\n  plugins: [\n    // Let NodeJS handle are requires that can\'t be resolved at build time\n    new IgnoreDynamicRequire()\n  ]\n};\n
Run Code Online (Sandbox Code Playgroud)\n

更新 2021-08-25 Nest 8/Webpack 5

\n

自从 Nest-cli 迁移到 webpack 5 以来,现在提供了一个有趣的功能:节点目标的块分割。

\n

我也很不高兴用相同的逻辑管理多个文件,所以我决定使用webpack-merge只有一个配置文件。

\n

您必须yarn add -D webpack-merge并具备以下条件webpack.config.js

\n
// webpack/migrations.config.js\n\nconst glob = require(\'glob\');\nconst path = require(\'path\');\n// TypeScript compilation option\nconst TsconfigPathsPlugin = require(\'tsconfig-paths-webpack-plugin\');\n// Minimization option\nconst TerserPlugin = require(\'terser-webpack-plugin\');\n\nconst { NODE_ENV = \'production\' } = process.env;\n\nconsole.log(`-- Webpack <${NODE_ENV}> build for migrations scripts --`);\n\nmodule.exports = {\n  target: \'node\',\n  mode: NODE_ENV,\n  // Dynamically generate a `{ [name]: sourceFileName }` map for the `entry` option\n  // change `src/db/migrations` to the relative path to your migration folder\n  entry: glob.sync(path.resolve(\'src/migration/*.ts\')).reduce((entries, filename) => {\n    const migrationName = path.basename(filename, \'.ts\');\n    return Object.assign({}, entries, {\n      [migrationName]: filename,\n    });\n  }, {}),\n  resolve: {\n    // assuming all your migration files are written in TypeScript\n    extensions: [\'.ts\'],\n    // Use the same configuration as NestJS\n    plugins: [new TsconfigPathsPlugin({ configFile: \'./tsconfig.build.json\' })],\n  },\n  module: {\n    rules: [\n      { test: /\\.ts$/, loader: \'ts-loader\' }\n    ]\n  },\n  output: {\n    // Remember that this file is in a subdirectory, so the output should be in the dist/\n    // directory of the project root\n    path: __dirname + \'/../dist/migration\',\n    // this is important - we want UMD (Universal Module Definition) for migration files.\n    libraryTarget: \'umd\',\n    filename: \'[name].js\',\n  },\n  optimization: {\n    minimizer: [\n      // Migrations rely on class and function names, so keep them.\n      new TerserPlugin({\n        terserOptions: {\n          mangle: true, // Note `mangle.properties` is `false` by default.\n          keep_classnames: true,\n          keep_fnames: true,\n        }\n      })\n    ],\n  },\n};\n
Run Code Online (Sandbox Code Playgroud)\n

使用此文件,可以从当前命令中选择 webpack 配置:bundle:main将选择主入口点的配置。

\n

您还会注意到 main:main和中现在有多个入口点console。前者用于主应用程序,后者用于 CLI 助手。但它们都共享相同(且大量)的代码,并且 Webpack 5 能够通过splitChunkssection 来做到这一点。这在 Webpack 4 中可用,但不适用于node目标。

\n

最后,当您保留类和函数名称时,即使使用装饰器(使用反射),一些优化现在也可以完全发挥作用。

\n

捆绑包更小,代码共享,package.json更清晰,每个人都高兴。

\n

更新结束

\n

TypeORM >= 0.3 的更新

\n

由于我们现在必须使用 JS 数据源,因此必须可以从 TypeORM CLI 中发现它。

\n
\nconst TYPEORM_CONFIG = {\n  entry: {\n    \'data-source\': {\n      // Are you ready? You must provide a data source as TypeORM cli >= 0.3\n      // But this will be a dynamic require(), so Webpack can\'t know how to handle it\n      // in TypeORM code. Instead, export this data source as a library.\n      // BUT, TypeORM expect it to be at the top level instead of a module variable, so\n      // we MUST remove the library name to make webpack export each variable from data source into the module\n      import: \'./src/data-source.ts\',\n      library: {\n        type: \'commonjs2\'\n      }\n    },\n    typeorm: \'./node_modules/typeorm/cli.js\'\n  },\n  externals: [\n    {\n      \'pg-native\': \'commonjs2 pg-native\',\n    }\n  ],\n  plugins: [\n    new IgnoreDynamicRequire(),\n  ],\n  module: {\n    rules: [\n      { test: /\\.[tj]s$/i, loader: \'shebang-loader\' }\n    ],\n  },\n}\n
Run Code Online (Sandbox Code Playgroud)\n

TypeORM >= 0.3 更新结束

\n

之后,为了简化构建过程,我在以下位置添加了一些目标package.json

\n
// webpack.config.js\nconst { merge } = require("webpack-merge")\nconst path = require(\'path\')\nconst glob = require(\'glob\')\nconst TsconfigPathsPlugin = require(\'tsconfig-paths-webpack-plugin\')\nconst TerserPlugin = require(\'terser-webpack-plugin\')\nconst MomentLocalesPlugin = require(\'moment-locales-webpack-plugin\')\nconst IgnoreDynamicRequire = require(\'webpack-ignore-dynamic-require\')\n\nconst { NODE_ENV = \'production\', ENTRY, npm_lifecycle_event: lifecycle } = process.env\n\n// Build platform don\'t support ?? and ?. operators\nconst entry = ENTRY || (lifecycle && lifecycle.match(/bundle:(?<entry>\\w+)/).groups["entry"])\n\nif (entry === undefined) {\n  throw new Error("ENTRY must be defined")\n}\n\nconsole.log(`-- Webpack <${NODE_ENV}> build  for <${entry}> --`);\n\nconst BASE_CONFIG = {\n  target: \'node\',\n  mode: NODE_ENV,\n  resolve: {\n    extensions: [\'.ts\', \'.js\'],\n    plugins: [new TsconfigPathsPlugin({ configFile: \'./tsconfig.build.json\' })],\n  },\n  module: {\n    rules: [\n      { test: /\\.ts$/, loader: \'ts-loader\' }\n    ]\n  },\n  output: {\n    path: path.resolve(__dirname, \'dist/\'),\n    filename: \'[name].js\',\n  },\n}\n\nconst MIGRATION_CONFIG = {\n  // Dynamically generate a `{ [name]: sourceFileName }` map for the `entry` option\n  // change `src/db/migrations` to the relative path to your migration folder\n  entry: glob.sync(path.resolve(\'src/migration/*.ts\')).reduce((entries, filename) => {\n    const migrationName = path.basename(filename, \'.ts\')\n    return Object.assign({}, entries, {\n      [migrationName]: filename,\n    })\n  }, {}),\n  output: {\n    path: path.resolve(__dirname, \'dist/migration\'),\n    // this is important - we want UMD (Universal Module Definition) for migration files.\n    libraryTarget: \'umd\',\n    filename: \'[name].js\',\n  },\n  optimization: {\n    minimizer: [\n      new TerserPlugin({\n        terserOptions: {\n          mangle: true, // Note `mangle.properties` is `false` by default.\n          keep_classnames: true,\n          keep_fnames: true,\n        }\n      })\n    ],\n  }\n}\n\nconst TYPEORM_CONFIG = {\n  entry: {\n    typeorm: \'./node_modules/typeorm/cli.js\'\n  },\n  externals: [\n    {\n      \'pg-native\': \'commonjs2 pg-native\',\n    }\n  ],\n  plugins: [\n    new IgnoreDynamicRequire(),\n  ],\n  module: {\n    rules: [\n      { test: /\\.[tj]s$/i, loader: \'shebang-loader\' }\n    ],\n  },\n}\n\nconst MAIN_AND_CONSOLE_CONFIG = {\n  entry: {\n    main: \'./src/main.ts\',\n    console: "./src/console.ts"\n  },\n  externals: [\n    {\n      \'pg-native\': \'commonjs2 pg-native\',\n      \'fastify-swagger\': \'commonjs2 fastify-swagger\',\n      \'@nestjs/microservices/microservices-module\': \'commonjs2 @nestjs/microservices/microservices-module\',\n      \'@nestjs/websockets/socket-module\': \'commonjs2 @nestjs/websockets/socket-module\',\n      // This one is a must have to generate the swagger document, but we remove it in production\n      \'swagger-ui-express\': \'commonjs2 swagger-ui-express\',\n      \'aws-sdk\': \'commonjs2 aws-sdk\',\n    }\n  ],\n  plugins: [\n    // We don\'t need moment locale\n    new MomentLocalesPlugin()\n  ],\n  optimization: {\n    // Full minization doesn\'t work with @Module annotation\n    minimizer: [\n      new TerserPlugin({\n        terserOptions: {\n          mangle: true, // Note `mangle.properties` is `false` by default.\n          keep_classnames: true,\n          keep_fnames: true,\n        }\n      })\n    ],\n    splitChunks: {\n      cacheGroups: {\n        commons: {\n          test: /[\\\\/]node_modules[\\\\/]/,\n          name: \'vendors\',\n          chunks: \'all\'\n        }\n      }\n    }\n  }\n}\n\nconst withPlugins = (config) => (runtimeConfig) => ({\n  ...config,\n  plugins: [\n    ...runtimeConfig.plugins,\n    ...(config.plugins || [])\n  ]\n})\n\nconst config = entry === "migrations" ? merge(BASE_CONFIG, MIGRATION_CONFIG)\n  : entry === "typeorm" ? merge(BASE_CONFIG, TYPEORM_CONFIG)\n  : entry === "main" ? merge(BASE_CONFIG, MAIN_AND_CONSOLE_CONFIG)\n    : undefined\n\n\nmodule.exports = withPlugins(config)\n\n
Run Code Online (Sandbox Code Playgroud)\n

\xe2\x80\xa6 你就快完成了。您可以调用yarn bundle,输出将在dist/目录中构建。我没有设法删除生成的一些 TypeScript 定义文件,但这不是真正的问题。

\n

最后一步是编写 RPM 规范文件:

\n
%build\nmkdir yarncache\nexport YARN_CACHE_FOLDER=yarncache\n\n# Setting to avoid node-gype trying to download headers\nexport npm_config_nodedir=/opt/rh/rh-nodejs10/root/usr/\n\n%{_yarnbin} install --offline --non-interactive --frozen-lockfile\n%{_yarnbin} bundle\n\nrm -r yarncache/\n\n%install\ninstall -D -m644 dist/main.js $RPM_BUILD_ROOT%{app_path}/main.js\n\ninstall -D -m644 dist/migration.js $RPM_BUILD_ROOT%{app_path}/migration.js\n# Migration path have to be changed, let\'s hack it.\nsed -ie \'s/src\\/migration\\/\\*\\.ts/migration\\/*.js/\' ormconfig.json\ninstall -D -m644 ormconfig.json $RPM_BUILD_ROOT%{app_path}/ormconfig.json\nfind dist/migration -name \'*.js\' -execdir install -D -m644 "{}" "$RPM_BUILD_ROOT%{app_path}/migration/{}" \\;\n
Run Code Online (Sandbox Code Playgroud)\n

如果您将 TypeORM >= 0.3 与 JS 数据源一起使用,则不必执行 sed 行,因为 DataSource 可以接受生产和开发源。但是,ormconfig.json 必须替换data-source.js

\n

systemd 服务文件可以告诉你如何启动它。目标平台是CentOS7,所以我必须使用软件集合中的NodeJS 10。您可以调整 NodeJS 二进制文件的路径。

\n
\nconst TYPEORM_CONFIG = {\n  entry: {\n    \'data-source\': {\n      // Are you ready? You must provide a data source as TypeORM cli >= 0.3\n      // But this will be a dynamic require(), so Webpack can\'t know how to handle it\n      // in TypeORM code. Instead, export this data source as a library.\n      // BUT, TypeORM expect it to be at the top level instead of a module variable, so\n      // we MUST remove the library name to make webpack export each variable from data source into the module\n      import: \'./src/data-source.ts\',\n      library: {\n        type: \'commonjs2\'\n      }\n    },\n    typeorm: \'./node_modules/typeorm/cli.js\'\n  },\n  externals: [\n    {\n      \'pg-native\': \'commonjs2 pg-native\',\n    }\n  ],\n  plugins: [\n    new IgnoreDynamicRequire(),\n  ],\n  module: {\n    rules: [\n      { test: /\\.[tj]s$/i, loader: \'shebang-loader\' }\n    ],\n  },\n}\n
Run Code Online (Sandbox Code Playgroud)\n

如果您使用 TypeORM JS 数据源,则必须添加-f data-source.jsExecStartPre.

\n

最终统计:

\n
    \n
  • 在双核虚拟机上构建时间为 3 分 30 秒。
  • \n
  • RPM 大小为 2.70 MB,独立,包含 3 个 JavaScript 文件和 2 个配置文件(.production.env用于主应用程序和ormconfig.json迁移)
  • \n
\n


归档时间:

查看次数:

8704 次

最近记录:

3 年,3 月 前