部署
在底层,MikroORM
使用 ts-morph
读取所有实体的 TypeScript 源文件,以便能够检测所有类型。多亏了这一点,定义类型就足以进行运行时验证。
¥Under the hood, MikroORM
uses ts-morph
to read TypeScript source files of all entities to be able to detect all types. Thanks to this, defining the type is enough for runtime validation.
这会对应用的部署产生一些影响。有时你只想部署编译后的输出,而根本不需要 TS 源文件。在这种情况下,发现过程可能会失败。你有多种选择:
¥This has some consequences for deployment of your application. Sometimes you will want to deploy only your compiled output, without TS source files at all. In that case, discovery process will probably fail. You have several options:
部署预建缓存
¥Deploy pre-built cache
自 v6 起,MikroORM 允许你通过 CLI 将生产缓存包生成为单个 JSON 文件:
¥Since v6, MikroORM lets you generate production cache bundle into a single JSON file via CLI:
npx mikro-orm cache:generate --combined
这将创建 ./temp/metadata.json
文件,该文件可与 GeneratedCacheAdapter
一起用于生产配置:
¥This will create ./temp/metadata.json
file which can be used together with GeneratedCacheAdapter
in your production configuration:
import { GeneratedCacheAdapter, MikroORM } from '@mikro-orm/core';
await MikroORM.init({
metadataCache: {
enabled: true,
adapter: GeneratedCacheAdapter,
options: { data: require('./temp/metadata.json') },
},
// ...
});
这样,你可以将 @mikro-orm/reflection
包仅保留为开发依赖,使用 CLI 创建缓存包,并在生产版本中仅依赖它。
¥This way you can keep the @mikro-orm/reflection
package as a development dependency only, use the CLI to create the cache bundle, and depend only on that in your production build.
缓存包可以静态导入,如果你使用某些打包器,这将非常方便。
¥The cache bundle can be statically imported, which is handy in case you are using some bundler.
在任何地方填充类型或实体属性
¥Fill type or entity attributes everywhere
发现过程的作用是嗅探 TS 类型并将其值保存为字符串,以便以后用于验证。你可以通过简单地手动提供这些值来跳过整个过程:
¥What discovery process does is to sniff TS types and save their value to string, so it can be used later for validation. You can skip the whole process by simply providing those values manually:
@Entity()
export class Book {
@PrimaryKey({ type: 'number' })
id!: number;
@Property({ type: 'string' })
title!: string;
@Enum(() => BookStatus)
status?: BookStatus;
@ManyToOne(() => Author) // or `@ManyToOne({ type: 'Author' })` or `@ManyToOne({ entity: () => Author })`
author1!: Author;
// or
@ManyToOne({ type: 'Author' })
author2!: Author;
// or
@ManyToOne({ entity: () => Author })
author3!: Author;
}
export enum BookStatus {
SOLD_OUT = 'sold',
ACTIVE = 'active',
UPCOMING = 'upcoming',
}
对于数字枚举,这不是必需的。
¥For numeric enums this is not be required.
部署你的实体源文件
¥Deploy your entity source files
通常,你部署的文件多于所需文件并不重要,因此最简单的方法是将 TS 源文件部署在编译输出旁边,就像在开发过程中一样。
¥Usually it does not matter much that you deploy more files than needed, so the easiest way is to just deploy your TS source files next to the compiled output, just like during development.
使用 Webpack 部署一组实体和依赖
¥Deploy a bundle of entities and dependencies with Webpack
Webpack 可用于打包每个实体和依赖:你将获得一个包含所有必需模块/文件且没有外部依赖的单个文件。
¥Webpack can be used to bundle every entity and dependency: you get a single file that contains every required module/file and has no external dependencies.
为 Webpack 准备项目
¥Prepare your project for Webpack
Webpack 要求将每个必需的文件硬编码到代码中。这样的代码将无法工作(它会抛出一个错误,因为 Webpack 不知道要将哪个文件包含在包中):
¥Webpack requires every required file to be hardcoded in your code. Code like this won't work (it will throw an error because Webpack doesn't know which file to include in the bundle):
let dependencyNameInVariable = 'dependency';
const dependency = import(dependencyNameInVariable);
当 Webpack 创建文件包时,不希望它扫描目录中的实体或元数据。因此,你需要在初始化函数中的 entities
选项中提供实体列表,不支持基于文件夹/文件的发现(请参阅动态包含实体作为替代解决方案)。此外,你需要在所有地方填写 type
或 entity
属性(见上文)并禁用缓存(这将稍微减少启动时间)。
¥As Webpack creates a file bundle, it isn't desired that it scans directories for entities or metadata. Therefore, you need to provide list of entities in the entities
option in the initialization function, folder/file based discovery is not supported (see dynamically including entities as an alternative solution). Also, you need to fill type
or entity
attributes everywhere (see above) and disable caching (it will decrease start-time slightly).
在 v4 中使用
ReflectMetadataProvider
时默认禁用缓存。¥In v4 caching is disabled by default when using
ReflectMetadataProvider
.
禁用动态文件访问
¥Disabling dynamic file access
你应该做的第一件事是通过 discovery.disableDynamicFileAccess
切换禁用发现过程中的动态文件访问。这将有效地完成:
¥First thing you should do is to disable dynamic file access in the discovery process via the discovery.disableDynamicFileAccess
toggle. This will effectively do:
-
将元数据提供程序设置为
ReflectMetadataProvider
¥set metadata provider to
ReflectMetadataProvider
-
禁用缓存
¥disable caching
-
禁止在
entities/entitiesTs
中使用路径¥disallow usage of paths in
entities/entitiesTs
手动定义实体
¥Manually defining entities
import { Author, Book, BookTag, Publisher, Test } from '../entities';
await MikroORM.init({
...
entities: [Author, Book, BookTag, Publisher, Test],
discovery: { disableDynamicFileAccess: true },
...
});
动态加载依赖
¥Dynamically loading dependencies
这将使用名为 动态导入 的 Webpack 功能。这样,只要知道部分路径,你就可以导入依赖。
¥This will make use of a Webpack feature called dynamic imports. This way you can import dependencies as long as part of the path is known.
在下面的例子中使用了 require.context
。此 'function' 仅在使用 Webpack 的构建过程中可用。因此,提供了一种替代解决方案,只要未设置环境变量 WEBPACK
(例如,在使用 ts-node
进行开发期间),该解决方案就会起作用。
¥In following example require.context
is used. This 'function' is only usable during the build process with Webpack. Therefore, an alternative solution is provided that will work as long as the environment variable WEBPACK
is not set (e.g., during development with ts-node
).
在这里,所有扩展名为 .ts
的文件都将从目录 ../entities
导入。
¥Here, all files with the extension .ts
will be imported from the directory ../entities
.
flatMap
是 ECMAScript 2019 中的一种方法,需要 Node.js 11 或更高版本。¥
flatMap
is a method from ECMAScript 2019 and requires Node.js 11 or higher.
await MikroORM.init({
// ...
entities: await getEntities(),
discovery: { disableDynamicFileAccess: true },
// ...
});
async function getEntities(): Promise<any[]> {
if (process.env.WEBPACK) {
const modules = require.context('../entities', true, /\.ts$/);
return modules
.keys()
.map(r => modules(r))
.flatMap(mod => Object.keys(mod).map(className => mod[className]));
}
const promises = fs.readdirSync('../entities').map(file => import(`../entities/${file}`));
const modules = await Promise.all(promises);
return modules.flatMap(mod => Object.keys(mod).map(className => mod[className]));
}
Webpack 配置
¥Webpack configuration
Webpack 可以在没有 配置文件 的情况下运行,但要构建 MikroORM 和 Node.js 包,它需要额外的配置。Webpack 的配置存储在项目的根目录中,名为 webpack.config.js
。有关所有选项,请参阅以下 page。
¥Webpack can be run without configuration file but for building MikroORM and Node.js bundles it requires additional configuration. Configuration for Webpack is stored in the root of the project as webpack.config.js
. For all the options please refer to the following page.
打包 MikroORM 需要以下配置:
¥For bundling MikroORM the following configuration is required:
const path = require('path');
const { EnvironmentPlugin, IgnorePlugin } = require('webpack');
const TerserPlugin = require('terser-webpack-plugin');
// Mark our dev dependencies as externals so they don't get included in the webpack bundle.
const { devDependencies } = require('./package.json');
const externals = {};
for (const devDependency of Object.keys(devDependencies)) {
externals[devDependency] = `commonjs ${devDependency}`;
}
// And anything MikroORM's packaging can be ignored if it's not on disk.
// Later we check these dynamically and tell webpack to ignore the ones we don't have.
const optionalModules = new Set([
...Object.keys(require('knex/package.json').browser),
...Object.keys(require('@mikro-orm/core/package.json').peerDependencies),
...Object.keys(require('@mikro-orm/core/package.json').devDependencies || {})
]);
module.exports = {
entry: path.resolve('app', 'server.ts'),
// You can toggle development mode on to better see what's going on in the webpack bundle,
// but for anything that is getting deployed, you should use 'production'.
// mode: 'development',
mode: 'production',
optimization: {
minimizer: [
new TerserPlugin({
terserOptions: {
// We want to minify the bundle, but don't want Terser to change the names of our entity
// classes. This can be controlled in a more granular way if needed, (see
// https://terser.nodejs.cn/docs/api-reference.html#mangle-options) but the safest default
// config is that we simply disable mangling altogether but allow minification to proceed.
mangle: false,
// Similarly, Terser's compression may at its own discretion change function and class names.
// While it only rarely does so, it's safest to also disable changing their names here.
// This can be controlled in a more granular way if needed (see
// https://terser.nodejs.cn/docs/api-reference.html#compress-options).
compress: {
keep_classnames: true,
keep_fnames: true,
},
}
})
]
},
target: 'node',
module: {
rules: [
// Bring in our typescript files.
{
test: /\.ts$/,
exclude: /node_modules/,
loader: 'ts-loader',
},
// Native modules can be bundled as well.
{
test: /\.node$/,
use: 'node-loader',
},
// Some of MikroORM's dependencies use mjs files, so let's set them up here.
{
test: /\.mjs$/,
include: /node_modules/,
type: 'javascript/auto',
},
],
},
// These are computed above.
externals,
resolve: {
extensions: ['.ts', '.js']
},
plugins: [
// Ignore any of our optional modules if they aren't installed. This ignores database drivers
// that we aren't using for example.
new EnvironmentPlugin({ WEBPACK: true }),
new IgnorePlugin({
checkResource: resource => {
const baseResource = resource.split('/', resource[0] === '@' ? 2 : 1).join('/');
if (optionalModules.has(baseResource)) {
try {
require.resolve(resource);
return false;
} catch {
return true;
}
}
return false;
},
}),
],
output: {
filename: 'server.js',
libraryTarget: 'commonjs',
path: path.resolve(__dirname, '..', 'output'),
},
};
运行 Webpack
¥Running Webpack
要运行 Webpack,请在项目的根目录中执行 webpack
(如果未全局安装,则执行 npx webpack
)。它可能会抛出一些警告,但你可以忽略有关 MikroORM 的错误:如果与 Webpack 正确打包,则不会执行上述代码片段。
¥To run Webpack execute webpack
(or npx webpack
if not installed globally) in the root of the project. It will probably throw a few warnings, but you can ignore the errors regarding MikroORM: the mentioned pieces of code won't be executed if properly bundled with Webpack.
使用 esbuild
部署一组实体和依赖
¥Deploy a bundle of entities and dependencies with esbuild
esbuild
可用于打包 MikroORM 实体和依赖:你将获得一个包含所有必需模块/文件的单个文件。由于打包的工作方式,需要正确解决一些问题才能使 esbuild
与 MikroORM 一起工作。
¥esbuild
can be used to bundle MikroORM entities and dependencies: you get a single file that contains every required module/file. Due to how the bundling works, there are few issues that needs to be properly addressed to make esbuild
work with MikroORM.
带有 esbuild 的 Knex 所需的垫片
¥Required shim for Knex with esbuild
Knex 与 esbuild 存在已知不兼容性 - Knex 尝试使用动态导入来处理各种可能的数据库方言(mySQL、MongoDB、Oracle 等),但 esbuild 不提供对动态导入功能的支持(参见 这个 GitHub 问题)。
¥Knex has a known incompatibility with esbuild - Knex attempts to use dynamic imports to handle the various possible database dialects (mySQL, MongoDB, Oracle, etc.) but esbuild does not provide support for dynamic import functionality (see this GitHub issue).
为了解决这个问题,你可以定义一个如下所示的 shim 模块,它在运行时拦截 Knex 的客户端解析并处理操作本身(从而避免动态导入代码)。这使 esbuild
打包能够正常工作。
¥In order to work around this issue, you can define a shim module as shown below which intercepts Knex's client resolution at runtime and handles the operation itself (thus avoiding the dynamic import code). This enables esbuild
bundling to work correctly.
定义文件 knex.d.ts
如下:
¥Define a file knex.d.ts
as follows:
declare module 'knex/lib/dialects/postgres' {
import { Knex } from 'esbuild-support/knex';
const client: Knex.Client;
export = client;
}
从 esbuild 中排除依赖
¥Excluding dependencies from esbuild
默认情况下,esbuild 将打包所有 MikroORM 的软件包,包括所有数据库方言(及其对数据库驱动程序的依赖)。这可能是不可取的,因为它会创建一个相当大的包,并且大多数应用只需要与一个数据库平台交互。要排除这些不必要的依赖,请通过 external 配置选项将排除列表传递给 esbuild。例如,如果使用 postgresql
平台,你可以排除其他不需要的依赖,如下所示:
¥By default, esbuild will bundle all of MikroORM's packages, including all database dialects (and their dependencies on database drivers). This is likely undesirable since it will create quite a large bundle, and most applications will only need to interact with one database platform. To exclude these unnecessary dependencies, pass a list of exclusions to esbuild via the external configuration option. For example, if using the postgresql
platform you can exclude other unneeded dependencies as follows:
external: [
'@mikro-orm/better-sqlite',
'@mikro-orm/migrations',
'@mikro-orm/entity-generator',
'@mikro-orm/mariadb',
'@mikro-orm/mongodb',
'@mikro-orm/mysql',
'@mikro-orm/mssql',
'@mikro-orm/seeder',
'@mikro-orm/sqlite',
'@mikro-orm/libsql',
'@vscode/sqlite3',
'sqlite3',
'better-sqlite3',
'mysql',
'mysql2',
'oracledb',
'pg-native',
'pg-query-stream',
'tedious',
]