拓冰建站拓冰建站
首页 / 资讯中心 / 正文

MikroORM 7 生产环境部署全指南:元数据缓存、预编译函数与 Webpack/esbuild 打包

后端【免费下载链接】mikro-ormTypeScript 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.项目地址https://gitcode.com/gh_mirrors/mi/mikro-orm点击查看免费下载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构造函数把预生成数据转成一个内存Mapget()时会把查询键上的.ts/.js后缀剥掉再命中set()则直接写内存。也就是说一旦缓存 bundle 生成运行期完全不需要再读取任何 TS 文件也不需要反射提供方参与。metadataCache 配置项的完整参数从 Configuration.ts 的类型定义可以看到metadataCache支持的完整选项参数类型说明enabledboolean是否启用元数据缓存默认取决于元数据提供方的useCache()方法combinedboolean \| string是否将所有元数据合并进单个缓存文件true用默认路径也可给自定义路径字符串prettyboolean是否美化pretty printJSON 缓存文件默认falseadapterSyncCacheAdapter构造器缓存适配器类启用缓存但未显式指定时使用异步MikroORM.init()会自动装配FileCacheAdapteroptionsDictionary传给适配器构造器的选项默认{ 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的实体 hydratorfull/reference 两种模式与EntityComparator的比较器、快照生成器、结果映射器、主键 getter/serializer 等每条捕获结果以key: function(params) { ... }的形式写入输出文件输出文件同时生成对应的.d.ts类型声明且按环境输出 ESMexport default或 CJSmodule.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(): Promiseany[] { 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 dont 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 MikroORMs packaging can be ignored if its not on disk. // Later we check these dynamically and tell webpack to ignore the ones we dont 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 whats 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 dont 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, Tersers compression may at its own discretion change function and class names. // While it only rarely does so, its 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 MikroORMs dependencies use mjs files, so lets 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 arent installed. This ignores database drivers // that we arent 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标记为外部依赖避免它们被打进 bundleoptionalModules收集mikro-orm/core的peerDependencies与devDependencies覆盖各种数据库驱动用IgnorePlugin动态检查——模块磁盘上存在就保留不存在就忽略从而剔除未使用的数据库驱动Terser 压缩mangle: false并保留 class/function 名称防止压缩器改动实体类名导致 ORM 按名称解析实体时失效loader 规则.ts用ts-loader.node原生模块用node-loader.mjs按javascript/auto处理EnvironmentPlugin注入WEBPACKtrue供上文动态加载实体的分支判断使用。运行 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 --combinedGeneratedCacheAdapter不需要支持JSON 缓存文件 编译产物常规 Node.js 生产部署想移除mikro-orm/reflection预编译函数compilecompiledFunctions不需要支持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 给出的终极组合compiledFunctionsmetadataCache: { adapter: GeneratedCacheAdapter }。仓库中tests/features/compiled-functions/、tests/features/reflection/等测试目录以及 tests/Webpack.test.ts、tests/entities-webpack/ 下的示例实体可以帮你进一步验证上述打包与缓存流程在真实项目中的行为。赞分享后端【免费下载链接】mikro-ormTypeScript 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.项目地址https://gitcode.com/gh_mirrors/mi/mikro-orm点击查看免费下载相关推荐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后端上一篇开源突破WebRL-GLM-4-9B让AI网页代理成功率提升7倍首次超越GPT-4下一篇3分钟掌握光学仿真OpticsPy让Python变身光学实验室创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门