问题描述
我最近不得不考虑一个新软件的部署方法,它是用以下内容编写的:
- NestJS 6 / Express
- 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 没有
node_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 也有一些单独的问答,但似乎都需要安装 ts-node
或 typescript
:
- TypeORM + Webpack causes SyntaxError: Unexpected token for entity file
- Isomoprhic application,problem with TypeORM && TypeScript && Express && Webpack setup
- SyntaxError: Unexpected token import typeORM entity
解决方法
我设法找到了一个很好的解决方案,它使用以下工具生成了 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。但是,捆绑迁移工具需要另外两个技巧:
- 忽略文件开头的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',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
用于迁移)