捆绑一个 NestJS + TypeORM 应用程序(使用 webpack)
Posted
技术标签:
【中文标题】捆绑一个 NestJS + TypeORM 应用程序(使用 webpack)【英文标题】:Bundle a NestJS + TypeORM application (with webpack) 【发布时间】:2021-05-16 01:51:43 【问题描述】:我最近不得不考虑一种新软件的部署方法,它是用以下代码编写的:
NestJS 6 / 快递 TypeORM 0.2 使用了 TypeScript该软件将部署在 160 多台服务器上,分布在整个欧洲,其中一些服务器的互联网连接非常糟糕。
我做了一些研究和很多people explicitly advices 反对捆绑。主要论点是原生扩展将与 webpack
或 rollup
之类的捆绑程序一起失败(剧透:这是真的,但有一个解决方案)。在我看来,这很大程度上是由于人们不关心这一点:node-pre-gyp
的作者使用了nearly the same words for this use case。所以通常,我被告知要么使用yarn install
要么使用sync the node_modules/
folder。
该项目是新项目,但 node_modules/
文件夹已经超过 480 MB。使用最大压缩的 XZ 给了我 20 MB 的存档。这对我来说仍然太大了,似乎是一种巨大的资源浪费。
我还看了以下问答:
How to correctly build NestJS app for production with node_modules dependencies in bundle? 说明 NestJS 支持 webpack withoutnode_modules/
开箱即用。
Single file bundle with NestJS + Typescript + Webpack + node_modules 工作答案,无需进一步解释。 IgnorePlugins
似乎也有点过头了。
How to bundle nestjs application with webpack 似乎从 ZenSoftware 复制/粘贴了一个工作解决方案而没有引用。指向另一个答案的链接。
NestJS optimization minimize not work with webpack 建议使用 NestJS 应用程序禁用最小化
TypeORM也有一些单独的Q&A,不过好像都需要安装ts-node
或者typescript
:
【问题讨论】:
【参考方案1】:2021 年 8 月 25 日更新
最初的响应是用 NestJS 6 完成的,它使用了 Webpack 4。 由于 NestJS 8 使用 Webpack 5,因此可以使用分块拆分,并提供更好的解决方案。
我还集成了webpack-merge 的使用,只有一个配置文件。 这只会更改您在阅读时看到的 Webpack 配置。
原答案
我设法找到了一个很好的解决方案,它使用以下工具生成了一个 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
可以将所有内容捆绑在一个文件中,但在这个用例中,我需要其中三个:
而且由于每个捆绑都需要不同的选项……我使用了 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。但是,捆绑迁移工具需要另外两个技巧:
忽略文件开头的shebang。使用shebang-loader
轻松解决(5 年后仍然可以正常工作!)
强制webpack
不替换require
调用dynamic configuration file,用于从JSON 或env
文件加载配置。我在this QA 的指导下最终构建了my own package。
// 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,
)
],
,
;
2021 年 8 月 25 日更新 Nest 8/Webpack 5
自从 nest-cli 迁移到 webpack 5 后,一个有趣的功能现在可用:节点目标的块拆分。
我也对管理多个具有相同逻辑的文件感到不安,所以我决定使用webpack-merge 只有一个配置文件。
您必须yarn add -D webpack-merge
并拥有以下webpack.config.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)
使用此文件,从当前命令中选择 webpack 配置:bundle:main
将选择主入口点的配置。
您还会注意到,现在在 main 中有多个入口点:main
和 console
。前者用于主应用程序,后者用于 CLI 助手。但是它们都共享相同(且数量巨大)的代码,而 Webpack 5 可以通过 splitChunks
部分做到这一点。这在 Webpack 4 中可用,但对于node
目标不起作用。
最后,当您保留类和函数名称时,一些优化现在甚至可以使用装饰器(使用反射)。
Bundle更小,代码共享,package.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"
,
而且……你快完成了。您可以调用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
用于迁移)
【讨论】:
你能提供一个包含所有这些配置的示例项目吗?以上是关于捆绑一个 NestJS + TypeORM 应用程序(使用 webpack)的主要内容,如果未能解决你的问题,请参考以下文章
使用 NestJS 和 TypeOrm,在我运行 NestJS 应用程序后不会自动创建表
运行nestjs应用程序时typeorm迁移中的“不能在模块外使用import语句”
未找到连接“默认”-TypeORM、NestJS 和外部 NPM 包