当前位置: 首页 > 知识库问答 >
问题:

捆绑NestJS TypeORM应用程序(与webpack)

柯苗宣
2023-03-14

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

  • 雀巢6/Express

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

我做了一些研究,很多人明确建议不要捆绑销售。主要的论点是本机扩展将失败与捆绑器,如webpackrollup(剧透:这是真的,但有一个解决方案)。在我看来,这在很大程度上是由于人们不关心这个事实:node-pre-gyp的作者在这个用例中使用了几乎相同的词。所以通常,我被告知要么使用yarn安装,要么同步node_modules/文件夹。

该项目是新的,但节点\u模块/文件夹已超过480 MB。使用最大压缩的XZ给了我20 MB的存档。这对我来说还是太大了,而且似乎是对资源的巨大浪费。

我还看了下面的Q

  • 如何使用捆绑包中的节点模块依赖项为生产正确构建NestJS应用程序?说明NestJS支持没有现成的node\u模块的webpack/

还有一些单独的问题

  • TypeORM Webpack会导致同步错误:实体文件的意外令牌
  • 同构应用,TypeORM的问题

共有1个答案

养俊驰
2023-03-14

最初的响应是用NestJS 6完成的,它使用了Webpack 4。由于NestJS 8使用Webpack 5,块分割是可用的,并提供了一个更好的解决方案。

我还集成了webpack合并的使用,使其只有一个配置文件。这只会更改阅读时看到的网页包配置。

我设法找到了一个很好的解决方案,通过以下工具生成了2.7 MB的自包含RPM:

  • 具有特殊配置的网页包
  • RPM,使用webpack,以分发生成的文件

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

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

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

我个人选择webpack是因为NestJS在它的build命令中支持这个。这仅仅是一个通向webpack编译器的通道,但是它似乎调整了一些路径,所以有点容易。

那么,如何做到这一点呢webpack可以将所有内容捆绑在一个文件中,但在本用例中,我需要三个:

  • 主要程序
  • TypeORM迁移CLI工具
  • TypeORM迁移脚本,因为它们不能与工具捆绑在一起,因为它依赖于文件名

因为每个捆绑需要不同的选择...我用了3个webpack文件。这里是布局:

webpack.config.js
webpack
├── migrations.config.js
└── typeorm-cli.config.js

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

// webpack.config.js
const { NODE_ENV = 'production' } = process.env;

console.log(`-- Webpack <${NODE_ENV}> build --`);

module.exports = {
  target: 'node',
  mode: NODE_ENV,
  externals: [
    // Here are listed all optional dependencies of NestJS,
    // that are not installed and not required by my project
    {
      'fastify-swagger': 'commonjs2 fastify-swagger',
      'aws-sdk': 'commonjs2 aws-sdk',
      '@nestjs/websockets/socket-module': 'commonjs2 @nestjs/websockets/socket-module',
      '@nestjs/microservices/microservices-module': 'commonjs2 @nestjs/microservices/microservices-module',
      
      // I'll skip pg-native in the production deployement, and use the pure JS implementation
      'pg-native': 'commonjs2 pg-native'
    }
  ],
  optimization: {
    // Minimization doesn't work with @Module annotation
    minimize: false,
  }
};

TypeForm的配置文件更详细,因为我们需要明确使用TypeScript。幸运的是,他们在FAQ中对此有一些建议。然而,捆绑迁移工具还需要两个技巧:

  • 忽略文件开头的shebang。使用shebang loader轻松解决(5年后仍能正常工作!)
// webpack/typeorm-cli.config.js

const path = require('path');
// TypeScript compilation option
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
// Don't try to replace require calls to dynamic files
const IgnoreDynamicRequire = require('webpack-ignore-dynamic-require');

const { NODE_ENV = 'production' } = process.env;

console.log(`-- Webpack <${NODE_ENV}> build for TypeORM CLI --`);

module.exports = {
  target: 'node',
  mode: NODE_ENV,
  entry: './node_modules/typeorm/cli.js',
  output: {
    // Remember that this file is in a subdirectory, so the output should be in the dist/
    // directory of the project root
    path: path.resolve(__dirname, '../dist'),
    filename: 'migration.js',
  },
  resolve: {
    extensions: ['.ts', '.js'],
    // Use the same configuration as NestJS
    plugins: [new TsconfigPathsPlugin({ configFile: './tsconfig.build.json' })],
  },
  module: {
    rules: [
      { test: /\.ts$/, loader: 'ts-loader' },
      // Skip the shebang of typeorm/cli.js
      { test: /\.[tj]s$/i, loader: 'shebang-loader' }
    ],
  },
  externals: [
    {
      // I'll skip pg-native in the production deployement, and use the pure JS implementation
      'pg-native': 'commonjs2 pg-native'
    }
  ],
  plugins: [
    // Let NodeJS handle are requires that can't be resolved at build time
    new IgnoreDynamicRequire()
  ]
};
// webpack/migrations.config.js

const glob = require('glob');
const path = require('path');
// TypeScript compilation option
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
// Minimization option
const TerserPlugin = require('terser-webpack-plugin');

const { NODE_ENV = 'production' } = process.env;

console.log(`-- Webpack <${NODE_ENV}> build for migrations scripts --`);

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

自从nest-cli移到webpack 5后,现在有了一个有趣的特性:节点目标的块分割。

我也对用相同的逻辑管理多个文件感到不安,所以我决定使用webpack merge来只有一个配置文件。

您必须添加-D网页包合并,并拥有以下网页包。配置。js

// webpack.config.js
const { merge } = require("webpack-merge")
const path = require('path')
const glob = require('glob')
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin')
const TerserPlugin = require('terser-webpack-plugin')
const MomentLocalesPlugin = require('moment-locales-webpack-plugin')
const IgnoreDynamicRequire = require('webpack-ignore-dynamic-require')

const { NODE_ENV = 'production', ENTRY, npm_lifecycle_event: lifecycle } = process.env

// Build platform don't support ?? and ?. operators
const entry = ENTRY || (lifecycle && lifecycle.match(/bundle:(?<entry>\w+)/).groups["entry"])

if (entry === undefined) {
  throw new Error("ENTRY must be defined")
}

console.log(`-- Webpack <${NODE_ENV}> build  for <${entry}> --`);

const BASE_CONFIG = {
  target: 'node',
  mode: NODE_ENV,
  resolve: {
    extensions: ['.ts', '.js'],
    plugins: [new TsconfigPathsPlugin({ configFile: './tsconfig.build.json' })],
  },
  module: {
    rules: [
      { test: /\.ts$/, loader: 'ts-loader' }
    ]
  },
  output: {
    path: path.resolve(__dirname, 'dist/'),
    filename: '[name].js',
  },
}

const MIGRATION_CONFIG = {
  // Dynamically generate a `{ [name]: sourceFileName }` map for the `entry` option
  // change `src/db/migrations` to the relative path to your migration folder
  entry: glob.sync(path.resolve('src/migration/*.ts')).reduce((entries, filename) => {
    const migrationName = path.basename(filename, '.ts')
    return Object.assign({}, entries, {
      [migrationName]: filename,
    })
  }, {}),
  output: {
    path: path.resolve(__dirname, 'dist/migration'),
    // this is important - we want UMD (Universal Module Definition) for migration files.
    libraryTarget: 'umd',
    filename: '[name].js',
  },
  optimization: {
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          mangle: true, // Note `mangle.properties` is `false` by default.
          keep_classnames: true,
          keep_fnames: true,
        }
      })
    ],
  }
}

const TYPEORM_CONFIG = {
  entry: {
    typeorm: './node_modules/typeorm/cli.js'
  },
  externals: [
    {
      'pg-native': 'commonjs2 pg-native',
    }
  ],
  plugins: [
    new IgnoreDynamicRequire(),
  ],
  module: {
    rules: [
      { test: /\.[tj]s$/i, loader: 'shebang-loader' }
    ],
  },
}

const MAIN_AND_CONSOLE_CONFIG = {
  entry: {
    main: './src/main.ts',
    console: "./src/console.ts"
  },
  externals: [
    {
      'pg-native': 'commonjs2 pg-native',
      'fastify-swagger': 'commonjs2 fastify-swagger',
      '@nestjs/microservices/microservices-module': 'commonjs2 @nestjs/microservices/microservices-module',
      '@nestjs/websockets/socket-module': 'commonjs2 @nestjs/websockets/socket-module',
      // This one is a must have to generate the swagger document, but we remove it in production
      'swagger-ui-express': 'commonjs2 swagger-ui-express',
      'aws-sdk': 'commonjs2 aws-sdk',
    }
  ],
  plugins: [
    // We don't need moment locale
    new MomentLocalesPlugin()
  ],
  optimization: {
    // Full minization doesn't work with @Module annotation
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          mangle: true, // Note `mangle.properties` is `false` by default.
          keep_classnames: true,
          keep_fnames: true,
        }
      })
    ],
    splitChunks: {
      cacheGroups: {
        commons: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all'
        }
      }
    }
  }
}

const withPlugins = (config) => (runtimeConfig) => ({
  ...config,
  plugins: [
    ...runtimeConfig.plugins,
    ...(config.plugins || [])
  ]
})

const config = entry === "migrations" ? merge(BASE_CONFIG, MIGRATION_CONFIG)
  : entry === "typeorm" ? merge(BASE_CONFIG, TYPEORM_CONFIG)
  : entry === "main" ? merge(BASE_CONFIG, MAIN_AND_CONSOLE_CONFIG)
    : undefined


module.exports = withPlugins(config)

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

您还会注意到,在main中现在有多个入口点:main控制台。前者用于主应用程序,后者用于CLI助手。但是他们都共享相同的(并且巨大的)代码量,Webpack 5能够通过spitChunks部分做到这一点。这在Webpack 4中可用,但不适用于节点目标。

最后,当您保留类和函数名时,一些优化现在甚至可以与decorator(使用反射)一起完全工作。

捆绑更小,代码共享,包。json更清晰,每个人都很开心。

之后,为了简化构建过程,我在package.json中添加了一些目标:

{
  "scripts": {
    "bundle:application": "nest build --webpack",
    "bundle:migrations": "nest build --webpack --webpackPath webpack/typeorm-cli.config.js && nest build --webpack --webpackPath webpack/migrations.config.js",
    "bundle": "yarn bundle:application && yarn bundle:migrations"
  },
}

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

%build
mkdir yarncache
export YARN_CACHE_FOLDER=yarncache

# Setting to avoid node-gype trying to download headers
export npm_config_nodedir=/opt/rh/rh-nodejs10/root/usr/

%{_yarnbin} install --offline --non-interactive --frozen-lockfile
%{_yarnbin} bundle

rm -r yarncache/

%install
install -D -m644 dist/main.js $RPM_BUILD_ROOT%{app_path}/main.js

install -D -m644 dist/migration.js $RPM_BUILD_ROOT%{app_path}/migration.js
# Migration path have to be changed, let's hack it.
sed -ie 's/src\/migration\/\*\.ts/migration\/*.js/' ormconfig.json
install -D -m644 ormconfig.json $RPM_BUILD_ROOT%{app_path}/ormconfig.json
find dist/migration -name '*.js' -execdir install -D -m644 "{}" "$RPM_BUILD_ROOT%{app_path}/migration/{}" \;

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

[Unit]
Description=NestJS Server
After=network.target

[Service]
Type=simple
User=nestjs
Environment=SCLNAME=rh-nodejs10
ExecStartPre=/usr/bin/scl enable $SCLNAME -- /usr/bin/env node migration migration:run
ExecStart=/usr/bin/scl enable $SCLNAME -- /usr/bin/env node main
WorkingDirectory=/export/myapplication
Restart=on-failure

# Hardening
PrivateTmp=true
NoNewPrivileges=true
ProtectSystem=full
ProtectHome=read-only

[Install]
WantedBy=multi-user.target

最后统计数字:

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

 类似资料:
  • 我用的是edu。seis理学学士。启动4J插件,使用gradle构建脚本构建可分发的应用程序。我正试图用一个捆绑的JRE来制作这些。 这是格雷德尔的剧本 令人沮丧的是,这创建了一个运行的应用程序(由gradle任务createExe创建的exe),但显然没有捆绑在/旁边的JRE,大概是因为它运行是因为它返回到使用系统jre,这使得测试变得困难。如果我把一个故意损坏的jre放在 /jre/它似乎仍然

  • 我创建了一个依赖于OpenJDK 11和JavaFX的HelloWorldJava小应用程序。该应用程序打包在jar文件中,只有在我的系统上单独安装Java11和JavaFX时才能运行。 现在,我想将我的jar转换成一个自包含的Java应用程序,其中包括JavaFX和一个功能齐全的Java运行时环境。这将允许我在不安装OpenJDK 11的情况下运行我的应用程序(这会带来技术障碍,例如正确设置路径

  • 我刚刚启动了一个新的应用程序,但当我点击欢迎登船页面上的“关于您的应用程序的环境”链接时,会出现这个错误。 启动应用程序时出错当Pow试图运行时,您的Rack应用程序引发了异常。 Bundler::,但找不到任何源代码 我的应用正在运行: 轨道3.2。6 Ruby 1.9。3p194 Rubygems 1.8。24 RVM 1.14。5 战俘0.4。0 我发现了类似的问题,问题在于如何让乘客安静下

  • null 支持与平台无关的应用程序图标 支持对JAR的自动更新 运行my.jar时对JRE参数的支持 Linux支持(.deb或.rpm)

  • 问题内容: 我在使用browserify时遇到了一些麻烦。 目标 我正在尝试使用Backbone构建基本的TodoMVC单页应用程序,而不仅仅是在我的标签堆中,而是尝试将它们全部与browserify捆绑在一起。 到目前为止,这就是我要做的。 lib /模型/todo.js lib /收藏/todo.js lib / app.js 要构建我的捆绑包,我正在使用 最后,我很简单 问题 当我打开控制台

  • 我有以下结构: 我不知道如何构建一切,以便: 在本地运行服务器,以便调试 制作生产tar(例如使用应用程序插件) 生产应如下所示: ktor作为主服务器 ktor应用程序有API 所有反应JS/超文本标记语言文件也由ktor提供 换句话说,我想从单个Web应用程序提供所有服务。 我知道我可以制作两个独立的服务——一个用于反应,一个用于应用编程接口。但是我想把它捆绑到一个应用程序中。 地方发展要快。