- 后端
【免费下载链接】mikro-orm
TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, MariaDB, MS SQL Server, PostgreSQL and SQLite/libSQL databases.
MikroORM 的实体发现(discovery)机制依赖 TypeScript 源码来推断属性类型,这直接影响了生产环境的部署方式。本文以 deployment.md 为骨架,系统讲解 MikroORM 7.2 在只部署编译产物、无 TS 源码、乃至无eval/new Function的受限运行时(如 Cloudflare Workers)下的六种部署方案,并结合 packages/cli/src/commands/GenerateCacheCommand.ts、packages/cli/src/commands/CompileCommand.ts、packages/core/src/cache/GeneratedCacheAdapter.ts 等源码,给出可复制的配置与打包实践。
为什么部署需要专门处理?
MikroORM 底层使用ts-morph读取所有实体的 TypeScript 源文件,以检测每个属性的类型。正因为"只写类型即可完成运行时校验",实体发现过程天然依赖.ts源文件的存在。
这给部署带来一个直接后果:当你只想部署编译后的 JS 产物、完全不携带 TS 源文件时,实体的发现过程很可能会失败。官方文档提供了多条出路,按"是否需要源码、是否运行在受限运行时"可以分成三类:
- 彻底移除 TS 源码依赖:生成预构建元数据缓存(
cache:generate --combined)、预编译运行函数(compile)、手动填充type/entity属性; - 简单粗暴保留源码:把 TS 源文件与编译产物一起部署;
- 打包为单文件:用 Webpack 或 esbuild 将实体与依赖打成一个 bundle。
下面逐一展开。
方案一:部署预构建元数据缓存(GeneratedCacheAdapter)
从 v6 开始,MikroORM 支持通过 CLI 将生产环境所需的元数据缓存打包成单个 JSON 文件:
npx mikro-orm cache:generate --combined该命令会在当前目录生成./temp/metadata.json,然后在生产配置中配合GeneratedCacheAdapter使用:
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 生成缓存包,生产构建只依赖这一份 JSON。
自定义缓存输出路径
--combined接受一个可选的路径参数,该路径相对当前目录下的temp文件夹解析。例如:
npx mikro-orm cache:generate --combined="../cache/mikro-orm-metadata.json"会把文件保存到./cache/mikro-orm-metadata.json。
提示:缓存 bundle 支持静态
import引入,在使用打包器(bundler)的场景下非常方便。
源码视角:cache:generate 命令做了什么
查看 GenerateCacheCommand.ts,可以看到该命令的两个关键选项:
--ts(布尔):为.ts文件生成开发用缓存;--combined/-c(字符串,别名-c):生成生产用单一 JSON 文件,与GeneratedCacheAdapter配套使用。
当--combined传入空字符串时会回退到默认路径./metadata.json(源码见 GenerateCacheCommand.ts)。命令内部会临时启用FileCacheAdapter并执行一次完整的MetadataDiscovery,再把结果写入缓存文件,最终日志会明确提示是Combined还是普通 JS/TS 缓存、输出到哪个路径。
GeneratedCacheAdapter的实现非常轻量(见 GeneratedCacheAdapter.ts):构造函数把预生成数据转成一个内存Map,get()时会把查询键上的.ts/.js后缀剥掉再命中,set()则直接写内存。也就是说,一旦缓存 bundle 生成,运行期完全不需要再读取任何 TS 文件,也不需要反射提供方参与。
metadataCache 配置项的完整参数
从 Configuration.ts 的类型定义可以看到metadataCache支持的完整选项:
| 参数 | 类型 | 说明 |
|---|---|---|
enabled | boolean | 是否启用元数据缓存,默认取决于元数据提供方的useCache()方法 |
combined | boolean \| string | 是否将所有元数据合并进单个缓存文件;true用默认路径,也可给自定义路径字符串 |
pretty | boolean | 是否美化(pretty print)JSON 缓存文件,默认false |
adapter | SyncCacheAdapter构造器 | 缓存适配器类;启用缓存但未显式指定时,使用异步MikroORM.init()会自动装配FileCacheAdapter |
options | Dictionary | 传给适配器构造器的选项,默认{ cacheDir: process.cwd() + '/temp' } |
生产环境使用GeneratedCacheAdapter时,只需把enabled: true、adapter指向适配器类、options.data指向生成的 JSON 即可。
方案二:预编译实体运行函数(compile)
有些运行时(如 Cloudflare Workers 和各种 edge 运行时)明确禁止new Function/eval。而 MikroORM 在运行期默认使用new Function来 JIT 编译针对每个实体优化的函数(用于 hydration 与实体比较)。解决办法是在构建期把这些函数预生成出来:
npx mikro-orm compile默认情况下,生成的文件位于你的 ORM 配置文件旁边。可以用--out自定义输出路径:
npx mikro-orm compile --out ./dist/compiled-functions.js然后在配置中传入生成的函数:
import compiledFunctions from './compiled-functions.js'; export default defineConfig({ compiledFunctions, });与 GeneratedCacheAdapter 组合:彻底告别 ts-morph 和 new Function
要获得"既无ts-morph、也无new Function"的完整生产部署,把两个方案叠加即可:
import { GeneratedCacheAdapter, defineConfig } from '@mikro-orm/core'; import compiledFunctions from './compiled-functions.js'; import metadata from './temp/metadata.json'; export default defineConfig({ compiledFunctions, metadataCache: { enabled: true, adapter: GeneratedCacheAdapter, options: { data: metadata }, }, });重要:只要实体定义或驱动配置发生变化,就必须重新生成 compiled functions 文件,否则运行期会使用过期函数。
源码视角:compile 命令如何"捕获"运行函数
CompileCommand.ts 的实现思路很巧妙:它先执行一次完整的MetadataDiscovery,然后通过替换Utils.createFunction全局钩子,把原本要在运行期 JIT 生成的代码字符串逐条捕获下来(见 CompileCommand.ts):
- 对每个实体元数据依次触发
ObjectHydrator的实体 hydrator(full/reference 两种模式)与EntityComparator的比较器、快照生成器、结果映射器、主键 getter/serializer 等; - 每条捕获结果以
'key': function(params) { ... }的形式写入输出文件; - 输出文件同时生成对应的
.d.ts类型声明,且按环境输出 ESM(export default)或 CJS(module.exports)两种格式(见 CompileCommand.ts); - 若未指定
--out,默认路径取 ORM 配置文件所在目录下的compiled-functions.js。
版本一致性校验
compiledFunctions生成文件里带有__version字段。在 Configuration.ts 中,ORM 初始化时会比对__version与当前Utils.getORMVersion(),如果不一致会打印警告,提示"编译函数是用 MikroORM vX 生成的,当前版本是 vY,请用npx mikro-orm compile重新生成"。这从源码层面印证了文档强调的"实体定义或驱动配置变化时必须重新生成"。
方案三:手动填充 type 或 entity 属性
实体发现过程的本质是"嗅探 TS 类型并把值保存为字符串,供后续运行时校验使用"。你可以完全跳过这一过程,手动提供这些值:
@Entity() export class Book { @PrimaryKey({ type: 'number' }) id!: number; @Property({ type: 'string' }) title!: string; @Enum(() => BookStatus) status?: BookStatus; @ManyToOne(() => Author) // or `@ManyToOne({ entity: () => Author })` author1!: Author; // or @ManyToOne({ entity: () => Author }) author2!: Author; } export enum BookStatus { SOLD_OUT = 'sold', ACTIVE = 'active', UPCOMING = 'upcoming', }需要注意:
- 标量属性用
type明确指定类型字符串(如'number'、'string'); - 枚举与关系用
() => Xxx的 entity 引用形式,@ManyToOne(() => Author)等价于@ManyToOne({ entity: () => Author }); - 数值枚举(numeric enum)无需手动标注,因为其值是运行时可直接得到的数字。
这种方式尤其适合配合下面的打包方案——因为打包器无法静态分析动态的目录扫描,需要所有实体信息都"硬编码"在代码里。
方案四:直接部署实体源文件(最简单)
大多数场景下,多部署几个文件无关紧要。因此最省事的做法是:把 TS 源文件原样放到编译产物旁边,和开发环境一样部署。这样实体发现照常工作,无需任何额外配置。缺点是产物里会多出源文件,且部署包中仍依赖ts-morph/@mikro-orm/reflection。
方案五:用 Webpack 打包实体与依赖
Webpack 可以把每个实体及其依赖打进单个文件,该文件包含所有必需的模块且没有外部依赖。
打包前的项目改造
Webpack 要求所有必需文件都被"硬编码"在代码里。下面这种动态导入不会生效(Webpack 不知道要把哪个文件打进 bundle,会直接报错):
let dependencyNameInVariable = 'dependency'; const dependency = import(dependencyNameInVariable);同时需要注意三点:
- 必须在
MikroORM.init()的entities选项中显式提供实体列表——基于文件夹/文件的发现不被支持(可用下方"动态加载依赖"作为替代方案); - 需要按方案三在所有地方填充
type或entity属性; - 禁用元数据缓存(会略微降低启动速度)。
提示:使用
ReflectMetadataProvider时缓存默认就是关闭的。
方式 A:手动列出实体
import { Author, Book, BookTag, Publisher, Test } from '../entities.js'; await MikroORM.init({ entities: [Author, Book, BookTag, Publisher, Test], // ... });方式 B:动态加载依赖
利用 Webpack 的**动态导入(dynamic imports)**特性,只要路径的一部分是已知的,就能把依赖打进来。下面的例子使用require.context——它只在 Webpack 构建期可用,因此同时提供了一个"当环境变量WEBPACK未设置时(例如开发期用tsx或swc运行)也能工作"的替代实现。
这里会从目录../entities导入所有扩展名为.ts的文件:
await MikroORM.init({ // ... entities: await getEntities(), // ... }); 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])); }process.env.WEBPACK由下面的EnvironmentPlugin({ WEBPACK: true })注入,从而保证打包期走require.context分支、开发期走原生动态import分支。
Webpack 配置
Webpack 可以不带配置文件运行,但要为 MikroORM 与 Node.js 目标构建 bundle,需要专门的配置。配置文件通常放在项目根目录,名为webpack.config.js。下面是一份针对 MikroORM 的完整推荐配置:
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('@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.org/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.org/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'), }, };这份配置的几个关键点:
- externals:把所有
devDependencies标记为外部依赖,避免它们被打进 bundle; - optionalModules:收集
@mikro-orm/core的peerDependencies与devDependencies(覆盖各种数据库驱动),用IgnorePlugin动态检查——模块磁盘上存在就保留,不存在就忽略,从而剔除未使用的数据库驱动; - Terser 压缩:
mangle: false并保留 class/function 名称,防止压缩器改动实体类名导致 ORM 按名称解析实体时失效; - loader 规则:
.ts用ts-loader,.node原生模块用node-loader,.mjs按javascript/auto处理; - EnvironmentPlugin:注入
WEBPACK=true,供上文动态加载实体的分支判断使用。
运行 Webpack
在项目根目录执行webpack(若未全局安装则用npx webpack)。构建过程大概率会抛出一些警告,其中与 MikroORM 相关的报错可以忽略:只要 bundle 正确生成,那些报错指向的代码片段在实际运行时根本不会执行。
方案六:用 esbuild 打包实体与依赖
esbuild同样可以把 MikroORM 的实体与依赖打成单个文件。但由于 esbuild 的打包机制,要让 MikroORM 正常工作,必须妥善处理下面这个关键问题。
排除不必要的依赖(external)
默认情况下,esbuild 会把 MikroORM 的所有包全部打进 bundle,包括全部数据库方言(及其数据库驱动依赖)。这通常不是我们想要的:bundle 会非常大,而绝大多数应用只与一种数据库平台交互。解决方法是把无关依赖通过 esbuild 的external配置排除掉。例如使用postgresql平台时:
external: [ '@mikro-orm/migrations', '@mikro-orm/entity-generator', '@mikro-orm/mariadb', '@mikro-orm/mongodb', '@mikro-orm/mysql', '@mikro-orm/mssql', '@mikro-orm/oracledb', '@mikro-orm/pglite', '@mikro-orm/seeder', '@mikro-orm/sqlite', '@mikro-orm/libsql', '@mikro-orm/sql-js', 'better-sqlite3', 'mysql', 'mysql2', 'oracledb', 'pg-native', 'pg-query-stream', 'sql.js', 'tedious', ]上面这份列表覆盖了 MikroORM 各可选方言包与底层驱动:@mikro-orm/sqlite/@mikro-orm/libsql/@mikro-orm/sql-js对应 SQLite 与 libSQL 生态,better-sqlite3/sql.js是它们的驱动;mysql/mysql2是 MySQL 与 MariaDB 驱动,oracledb、pg-native、pg-query-stream、tedious分别对应 Oracle、PostgreSQL 原生流式查询与 MSSQL。若你的应用恰好用到其中某一项,则应从 external 列表中移除,让它正常打进 bundle 或作为运行时依赖保留。
如何选择:六种方案适用场景速查
| 方案 | 是否需要 TS 源码 | 是否支持禁 eval 运行时 | 部署形态 | 适用场景 |
|---|---|---|---|---|
预构建缓存(cache:generate --combined+GeneratedCacheAdapter) | 不需要 | 支持 | JSON 缓存文件 + 编译产物 | 常规 Node.js 生产部署,想移除@mikro-orm/reflection |
预编译函数(compile+compiledFunctions) | 不需要 | 支持 | compiled-functions.js+ 缓存 | Cloudflare Workers、edge 运行时 |
手动填充type/entity | 不需要 | 视组合方案 | 纯编译产物 | 配合打包器使用,或实体数量少、想彻底去掉发现过程 |
| 部署实体源文件 | 需要 | 不支持(依赖 ts-morph) | 源文件 + 编译产物 | 快速上线、对部署体积不敏感 |
| Webpack 打包 | 不需要(需硬编码实体列表) | 不支持(默认仍用new Function) | 单文件server.js | 希望输出单一可执行文件、无外部依赖 |
| esbuild 打包 | 不需要 | 不支持(默认仍用new Function) | 单文件 bundle | 追求构建速度与最小 bundle,可配合 external 剔除方言 |
需要特别指出:方案五、六(Webpack/esbuild 打包)解决的是"单文件部署形态",若同时要跑在禁eval的运行时上,应把方案二(compile预编译函数)与方案一(GeneratedCacheAdapter)叠加进去,三者在配置上完全兼容——这正是 deployment.md 给出的终极组合:compiledFunctions+metadataCache: { adapter: GeneratedCacheAdapter }。
仓库中tests/features/compiled-functions/、tests/features/reflection/等测试目录以及 tests/Webpack.test.ts、tests/entities-webpack/ 下的示例实体,可以帮你进一步验证上述打包与缓存流程在真实项目中的行为。
- 后端
【免费下载链接】mikro-orm
TypeScript ORM for Node.js based on Data Mapper, Unit of Work and Identity Map patterns. Supports MongoDB, MySQL, MariaDB, MS SQL Server, PostgreSQL and SQLite/libSQL databases.
相关推荐
MikroORM 生产环境部署完全指南:从元数据缓存、预编译函数到 Webpack 与 esbuild 打包
MikroORM 生产环境部署完全指南:从元数据缓存、预编译函数到 Webpack 与 esbuild 打包 MikroORM 在运行时依赖 ts morph
后端MikroORM 生产部署实战指南:元数据缓存、预编译函数与 Webpack/esbuild 打包
MikroORM 生产部署实战指南:元数据缓存、预编译函数与 Webpack/esbuild 打包 MikroORM 的实体元数据发现机制依赖 ts morph
后端MikroORM 生产部署实战指南:元数据缓存打包、预编译函数与 Webpack/esbuild 打包策略
MikroORM 生产部署实战指南:元数据缓存打包、预编译函数与 Webpack/esbuild 打包策略 本篇指南基于 MikroORM 官方文档 deplo
后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考