捆绑一个 NestJS + TypeORM 应用程序使用 webpack

问题描述

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

  • NestJS 6 / Express
  • TypeORM 0.2
  • 使用了 TypeScript

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

我做了一些研究和很多people explicitly advices反对捆绑。主要论点是本机扩展将因 webpackrollup 之类的捆绑器而失败(剧透:这是真的,但有一个解决方案)。在我看来,这主要是因为人们不关心这一点:node-pre-gyp 的作者使用了 nearly the same words for this use case。所以通常情况下,我被告知使用 yarn installsync the node_modules/ folder

项目是新的,但 node_modules/ 文件夹已经超过 480 MB。使用最大压缩的 XZ 给了我 20 MB 的存档。这对我来说还是太大了,似乎是对资源的巨大浪费。

我还看了以下问答:

TypeORM 也有一些单独的问答,但似乎都需要安装 ts-nodetypescript

解决方法

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

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

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

我必须坚持:我的解决方案不适用于本机扩展。幸运的是,popular bcrypt 可以替换为 pure JS implementation,并且 most popular postgresql package 既可以使用编译的 JS,也可以使用纯 JS。

如果想捆绑原生扩展,可以尝试使用ncc。他们设法 implement a solution for node-pre-gyp dependent packages 在一些初步测试中对我有用。当然,编译的扩展应该与您的目标平台相匹配,就像编译的东西一样。

我个人选择 webpack 是因为 NestJS support this in it's build command。这只是对 webpack 编译器的传递,但似乎调整了一些路径,因此更容易一些。

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

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

而且由于每个捆绑都需要不同的选项……我使用了 3 个 webpack 文件。这是布局:

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

所有这些文件都基于相同的 template kindly provided by 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,}
};

TypeORM 的配置文件比较冗长,因为我们需要明确使用 TypeScript。幸运的是,他们有一些advices for this in their FAQ。但是,捆绑迁移工具需要另外两个技巧:

// 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',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,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',// 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'],loader: 'ts-loader' }
    ]
  },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,}
      })
    ],};

之后,为了简化构建过程,我在 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"
  },}

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

最后一步是编写 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/{}" \;

systemd 服务文件可以告诉你如何启动它。目标平台是CentOS7,所以我必须使用NodeJS 10 from software collections。您可以调整 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 用于迁移)

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...