Meteor特殊目录机制:client/server/lib等六大目录原理与实践

📅 2026/6/22 6:44:49
Meteor特殊目录机制:client/server/lib等六大目录原理与实践
1. 项目概述Meteor 中那些“自带魔法”的特殊目录如果你刚接触 Meteor或者正被一个老项目里散落各处的client/、server/、public/目录搞得晕头转向——别急这不是你配置错了也不是代码写乱了而是 Meteor 从诞生第一天起就埋下的核心设计哲学约定优于配置目录即逻辑。它不像 Express 那样靠app.use()显式挂载中间件也不像 Next.js 那样靠文件系统路由自动映射页面Meteor 把“代码该在哪跑、资源该怎么用、测试该怎么写”这些决策直接编码进了项目根目录下的几个固定名字里。client/目录下的 JS 只在浏览器里执行server/下的代码永远只在 Node 进程里运行public/里的图片和字体会原样暴露给浏览器请求而private/里的 JSON 或模板则只能被服务端读取——这种“所见即所得”的组织方式让初学者上手极快也让团队协作时几乎不用争论“这个 API 路由该放哪”“这个工具函数要不要打包进前端”。我带过三个不同行业的 Meteor 团队从电商后台到工业数据看板最常听到的感叹不是“这框架太难”而是“原来改个按钮颜色真的只要改 client/stylesheets/ 里的一个 CSS 文件就行”。当然这种便利背后也有代价一旦你试图绕过这些目录规则去搞“动态加载服务端模块到客户端”或者想把lib/里定义的共享方法偷偷塞进tests/的单元测试里跑就会立刻撞上 Meteor 编译器那套不容商量的静态分析规则。所以这篇内容不讲怎么安装 Meteor也不讲 Blaze 或 React 组件怎么写就聚焦在这些看似普通、实则掌控着整个应用生命周期的“特殊目录”上——它们不是文件夹是 Meteor 的语法糖是编译器的指令集更是你理解这个框架底层逻辑的第一把钥匙。2. 核心目录设计与思路拆解为什么是这六个而不是五个或七个Meteor 的六个特殊目录client/、server/、public/、private/、tests/、lib/不是拍脑袋定的而是对 Web 应用开发中“环境隔离”“资源分发”“代码复用”“质量保障”四大刚需的精准映射。我们来逐个拆解它的设计逻辑看看每个目录背后藏着哪些被省略掉的 if-else 判断和 require 路径拼接。2.1 client/ 与 server/环境隔离的物理边界这是 Meteor 最标志性的设计。传统 Node.js 应用里你得靠if (process.env.NODE_ENV client)或者 Webpack 的target: web来区分前后端代码稍有不慎一个require(fs)就会让整个前端 bundle 报错。Meteor 直接用目录名做了硬性切割所有在client/下的.js、.ts、.html、.css文件编译器在构建阶段就只打包进浏览器端的 JS bundle而server/下的同名文件则被剥离出来只参与服务端 Node 进程的启动。这种设计的底层原理其实很朴素Meteor 的构建工具当时叫meteor build现在基于meteorjs/esbuild在扫描源码时会先按目录层级做一次预分类再对每个类别执行不同的编译策略。比如client/下的 ES6 模块会被转成 IIFE 并注入 Meteor 的全局上下文而server/下的代码则保留 CommonJS 语法直接交给 Node 执行。我曾经为了验证这点在一个client/main.js里写了console.log(process.env.NODE_ENV)结果浏览器控制台输出development而服务端日志里完全没这条记录——因为那行代码压根没被送到服务端进程里。这种“物理隔离”带来的好处是确定性你永远不用担心某个工具函数意外地把服务端敏感逻辑泄露到前端也不用为window对象在服务端不存在而加一堆 typeof 判断。但它的代价也很明显当你需要一个既能在客户端调用、又能在服务端复用的校验函数时你就不能把它放在client/或server/里而必须挪到lib/——这就是下一个目录存在的理由。2.2 lib/跨环境共享的“中央枢纽”lib/是 Meteor 项目里最安静、也最关键的目录。它不负责渲染不处理请求甚至不直接参与任何业务逻辑但它像一座桥把client/和server/两个孤岛连接起来。所有放在lib/下的代码会在编译阶段被同时注入到客户端和服务端的执行环境中。这意味着你可以在lib/utils.js里定义一个formatCurrency(amount)函数然后在client/templates/payment.js和server/methods/processPayment.js里毫无障碍地import { formatCurrency } from /lib/utils.js。Meteor 实现这一点的技术细节是在构建流程中lib/目录会被优先编译并生成两份中间产物——一份供客户端 bundle 引用一份供服务端 Node 进程 require。更妙的是这种共享不是简单的文件复制而是支持完整的模块依赖树。比如lib/validation.js依赖lib/schemas.js而schemas.js又依赖npm包joiMeteor 会自动解析并打包所有依赖确保两端拿到的都是同一套校验逻辑。我见过最典型的误用场景是有人把数据库 Schema 定义放在server/models/下结果在客户端调用Meteor.methods时传入的数据格式跟服务端校验不一致调试半天才发现问题出在“两边用的不是同一套校验规则”。后来我们统一把所有 Schema、常量、工具函数都收归lib/上线后这类跨端不一致的 bug 直接归零。不过要注意lib/不是万能的“安全区”如果你在lib/里写了require(fs)或window.localStorageMeteor 编译器不会报错但运行时一定会崩——因为它只保证“代码能被两端加载”不保证“代码在两端都能执行”。所以lib/的黄金法则是只放纯逻辑、无环境依赖的代码。2.3 public/ 与 private/静态资源的权限二分法Web 应用离不开静态资源logo 图片、字体文件、第三方库的未压缩版、甚至是一些初始化配置的 JSON。Meteor 把这类资源的管理也交给了目录约定。public/是公开的“资源集市”里面的所有文件都会被 Meteor 内置的 Web 服务器原样暴露路径就是文件在public/下的相对路径。比如public/images/logo.png浏览器直接访问/images/logo.png就能拿到。而private/则是私密的“保险柜”里面的文件永远不会被 Web 服务器直接提供只能通过服务端代码比如Assets.getText(config.json)读取。这种设计解决了两个经典痛点一是避免敏感配置如 API 密钥、数据库连接串被意外放到public/下导致泄露二是让服务端能动态读取一些不希望被缓存或需要权限校验的资源。举个实际例子我们有个工业监控项目需要在服务端读取一个private/devices.yaml文件来初始化设备列表这个 YAML 文件包含设备 IP 和认证 token绝对不能让浏览器直接下载到。如果放在public/一个简单的 curl 就能拿到全部信息而放在private/只有服务端代码能通过AssetsAPI 访问且我们可以在这个读取逻辑里加入权限检查比如“只有 admin 角色才能触发设备重载”。这种“目录即权限”的设计比在 Express 里写一堆app.get(/config, authMiddleware, (req, res) {...})要简洁得多也更不容易遗漏。2.4 tests/测试即一等公民的工程实践在很多框架里测试文件是“附属品”散落在各个业务模块旁边或者集中在一个test/目录下但运行时需要额外配置测试框架如 Jest 的--rootDir。Meteor 把tests/设为特殊目录意味着所有在tests/下的代码只在运行meteor test命令时被加载且默认以服务端环境执行。这背后的设计意图很清晰——测试不是开发的负担而是开发流程的自然延伸。当你执行meteor test --driver-package meteortesting:mochaMeteor 会启动一个精简的服务端实例只加载tests/下的文件和它们依赖的lib/代码而client/和server/的业务代码则被隔离在外避免测试污染生产环境。更关键的是tests/目录支持子目录约定tests/server/下的测试只在服务端运行tests/client/下的测试则会启动一个真实的浏览器环境通过 Selenium 或 Puppeteer让你能写it(should render login button, () { ... })这样的端到端测试。我带过的团队里新成员入职第一周的任务不是写功能而是给lib/里的工具函数补全tests/下的单元测试。这种强制的测试入口让我们的核心校验逻辑覆盖率常年保持在 95% 以上远超行业平均水平。当然这也带来一个隐性要求你的业务代码必须足够“可测试”比如数据库操作要封装成可 mock 的方法UI 渲染要分离出纯函数组件——否则tests/目录再规范也救不了糟糕的代码结构。3. 核心目录实操要点与避坑指南那些文档里不会写的细节光知道六个目录的名字和大概作用远远不够。在真实项目里你会遇到一堆“理论上应该可行但 Meteor 就是不认账”的诡异情况。这些坑往往源于对 Meteor 构建流程的细微偏差理解。下面这些实操要点是我踩过至少三次才总结出来的血泪经验每一条都配了可复现的代码片段和错误日志。3.1 client/ 目录的“隐形陷阱”HTML 模板的加载顺序很多人以为client/下的 HTML 文件只是用来写模板的但 Meteor 的 Blaze 模板引擎对client/下 HTML 的解析有严格顺序。它不是按文件名字母序而是按目录深度优先。比如client/ ├── main.html # head 和 body 标签在这里定义 ├── templates/ │ ├── header.html # 被 main.html 的 {{ header}} 调用 │ └── footer.html # 同上 └── stylesheets/ └── main.css # 这个 CSS 会被自动注入 head如果你把header.html放在client/templates/而main.html里写了{{ header}}一切正常。但如果你不小心把header.html放到了client/根目录下而main.html也在根目录Meteor 就会报错Template.header is not defined。原因在于Meteor 在解析client/下的 HTML 时会先加载所有根目录的.html文件再递归加载子目录。而main.html里引用header时header.html还没被解析到。解决方案很简单所有被其他模板引用的子模板必须放在子目录里且引用路径要匹配目录结构。比如client/templates/header.html就在main.html里写{{ templates.header}}。这个细节在官方文档里提都没提但却是新人最常见的报错来源之一。3.2 server/ 目录的“冷启动”问题数据库连接的时机server/下的代码在 Meteor 启动时就会执行但有一个关键时间点MongoDB 连接建立完成之前所有server/代码都已开始运行。这意味着如果你在server/main.js里写了// ❌ 错误示范假设这里要初始化管理员账号 Meteor.startup(() { if (Meteor.users.find().count() 0) { Accounts.createUser({ email: adminexample.com, password: 123456, profile: { name: Admin } }); } });这段代码看起来没问题但实际运行时Meteor.users.find().count()很可能返回0即使数据库里已经有用户。因为Meteor.startup的回调是在服务端代码加载完后立即注册的但此时 MongoDB 的连接可能还没真正建立好find()操作返回的是空结果。正确的做法是把数据库依赖的操作包裹在Mongo.Collection的ready()回调里或者使用Meteor.defer延迟执行。更稳妥的方案是// ✅ 正确示范等待数据库就绪 Meteor.startup(() { // 确保 MongoDB 已连接 Meteor.defer(() { if (Meteor.users.find().count() 0) { Accounts.createUser({ email: adminexample.com, password: 123456, profile: { name: Admin } }); } }); });Meteor.defer会把回调推到事件循环的下一帧此时 MongoDB 连接基本已经稳定。我在一个金融项目里就栽过这个跟头服务端启动脚本里有一段初始化交易流水号的逻辑因为没加defer导致每次重启后第一个订单的流水号都是000001而不是预期的000002。排查了两天才发现是数据库连接时序问题。3.3 public/ 目录的“缓存噩梦”如何强制浏览器更新静态资源public/下的文件虽然方便但带来一个经典问题浏览器缓存。比如你更新了public/images/logo.png但用户浏览器里还是旧版本因为 HTTP 响应头里Cache-Control: max-age31536000一年。Meteor 默认对public/资源启用了强缓存这是为了性能但开发和灰度发布时就成了障碍。官方文档建议用?vxxx参数强制刷新但这需要手动改所有 HTML 里的img src/images/logo.png?v123不现实。真正的解决方案是利用 Meteor 的构建哈希机制在构建时自动生成带哈希的文件名。你不需要改public/目录结构只需要在server/里加一段代码// server/public-hasher.js import { WebApp } from meteor/webapp; import fs from fs; import path from path; // 在构建时Meteor 会把 public/ 下的文件复制到 .meteor/local/build/programs/web.browser/app/ // 我们可以监听这个目录用 gulp 或 webpack 插件生成哈希文件名 // 但更简单的方法是在部署前用 shell 脚本重命名 public/ 下的文件 // 例如mv public/images/logo.png public/images/logo.a1b2c3d4.png // 然后在 HTML 里用 Meteor.settings.public.logoPath 动态引入不过最轻量级的实战技巧是在开发环境直接禁用缓存。在server/main.js里加WebApp.connectHandlers.use((req, res, next) { if (process.env.NODE_ENV development) { res.setHeader(Cache-Control, no-store, no-cache, must-revalidate, max-age0); } next(); });这样每次 F5浏览器都会重新拉取public/下的最新文件。上线时再切回默认缓存策略。这个技巧我教给过十几个团队几乎成了 Meteor 开发者的“肌肉记忆”。3.4 private/ 目录的“读取权限”Assets API 的正确用法private/目录的安全性完全依赖AssetsAPI 的正确使用。常见错误是试图用 Node.js 原生的fs.readFile去读private/下的文件。比如// ❌ 绝对禁止这会直接报错 import { readFileSync } from fs; const config readFileSync(private/config.json); // Error: ENOENT: no such file or directory // ✅ 唯一正确的方式必须用 Assets API import { Assets } from meteor/assets; const configText Assets.getText(config.json); // 返回字符串 const config JSON.parse(configText);为什么因为private/下的文件在构建时会被 Meteor 打包进服务端 bundle 的一个特殊资源区而不是直接复制到文件系统。Assets.getText()和Assets.getBinary()是 Meteor 提供的唯一“钥匙”用来从这个资源区里取出内容。而且AssetsAPI 还支持子目录Assets.getText(data/users.json)会去private/data/users.json里找。另一个容易忽略的点是Assets.getText()只能在服务端同步调用不能在客户端调用也不能在异步回调里调用。比如// ❌ 错误在 setTimeout 里调用会报错 setTimeout(() { const data Assets.getText(config.json); // Error: Cannot call Assets.getText on the client }, 1000); // ✅ 正确在服务端同步上下文里调用 Meteor.methods({ loadConfig: function() { return Assets.getText(config.json); } });这个限制是为了防止服务端资源被意外暴露。我在一个医疗项目里就吃过亏为了实现“配置热更新”我把Assets.getText放在了一个setInterval里结果每次轮询都触发一次服务端读取导致 CPU 占用飙升。后来改成只在应用启动时读取一次后续通过 Redis Pub/Sub 通知配置变更才解决问题。3.5 lib/ 目录的“循环依赖”雷区如何安全地组织共享代码lib/是共享的天堂但也可能是循环依赖的地狱。Meteor 的模块解析器对循环依赖非常敏感一旦出现构建就会失败报错信息往往是Error: Cannot find module xxx但实际原因是 A 依赖 BB 又依赖 A。典型场景是lib/collections.js定义了Posts集合而lib/methods.js里要调用Posts.insert()于是methods.jsimportcollections.js但collections.js里又需要methods.js里定义的某些校验函数于是又 import 回去。解决这个问题我总结了三条铁律分层隔离lib/下再建子目录按职责分层。比如lib/collections/只放集合定义、lib/schemas/只放校验规则、lib/utils/只放纯函数。每一层只允许向下依赖不允许向上或平级循环。延迟 require在函数内部而不是文件顶部用require()动态加载。比如lib/methods.js里Meteor.methods({ posts.insert: function(post) { // ✅ 在函数体内 require避免顶层循环 const { validatePost } require(/lib/schemas/posts.js); validatePost(post); Posts.insert(post); } });接口抽象为循环依赖的部分定义一个“接口文件”。比如lib/interfaces.js里只导出类型声明或空函数桩collections.js和methods.js都只依赖这个接口具体实现由第三方模块注入。这三条规则让我维护的一个拥有 200 个共享模块的 Meteor 项目五年来从未因循环依赖中断过构建。4. 实操过程与核心环节实现从零搭建一个符合规范的 Meteor 项目现在我们把前面所有的理论和避坑经验落地到一个完整的实操流程里。我会带你从初始化一个空项目开始一步步构建出一个结构清晰、符合 Meteor 最佳实践的目录骨架并填充上真实可用的代码。这个过程不是照着文档抄命令而是每一步都解释“为什么这么选”“如果不这么选会怎样”。4.1 初始化与目录骨架搭建拒绝“meteor create”一键生成很多教程第一步就是meteor create myapp这确实能快速生成一个 demo但生成的目录结构是扁平的所有文件都在根目录不符合我们讨论的“特殊目录”规范。真正的专业做法是手动创建骨架强制自己思考每个文件的归属。步骤如下创建空项目目录mkdir meteor-special-dirs-demo cd meteor-special-dirs-demo初始化 Git 和 npmMeteor 项目本质是 Node.js 项目git init npm init -y手动创建六个特殊目录mkdir client server public private lib tests初始化 Meteor注意不要用meteor create# 全局安装 meteor CLI如果还没装 npm install -g meteor # 在当前目录初始化 Meteor这会生成 .meteor/ 目录和 package.json meteor提示执行meteor命令后它会检测到当前是空目录自动创建最小化的 Meteor 项目结构并提示你“Welcome to Meteor!”。此时项目还不能运行因为我们还没放任何代码。为什么不用meteor create因为meteor create会生成一个包含imports/目录的现代结构Meteor 1.3 推荐而我们要专注的是client/、server/这套经典约定。手动创建能让你彻底掌控每一个目录的存在意义而不是被脚手架带着走。4.2 client/ 目录实现一个带状态管理的登录表单现在我们在client/下构建一个真实的登录界面。目标用户输入邮箱和密码点击登录调用服务端方法显示成功或失败消息。创建client/main.html定义页面骨架!-- client/main.html -- head titleMeteor Special Directories Demo/title /head body {{ loginForm}} /body template nameloginForm div classlogin-container h2Login/h2 form idlogin-form input typeemail idemail placeholderEmail required / input typepassword idpassword placeholderPassword required / button typesubmitLogin/button /form div idmessage/div /div /template创建client/main.js实现交互逻辑// client/main.js import { Template } from meteor/templating; import { Meteor } from meteor/meteor; Template.loginForm.events({ submit #login-form: function(event) { event.preventDefault(); const email event.target.email.value; const password event.target.password.value; // 调用服务端方法 Meteor.call(user.login, email, password, (error, result) { if (error) { document.getElementById(message).innerText Error: ${error.reason}; } else { document.getElementById(message).innerText Success! Welcome, ${result.name}; } }); } });创建client/main.css添加基础样式/* client/main.css */ .login-container { max-width: 400px; margin: 50px auto; padding: 20px; border: 1px solid #ddd; border-radius: 5px; } #message { margin-top: 10px; color: #d32f2f; }这个client/目录的实现展示了三个关键点HTML 模板、JS 交互、CSS 样式全部按约定分开放置所有代码只在浏览器执行通过Meteor.call与服务端通信而不是直接操作 DOM 或发 AJAX 请求。如果你现在运行meteor就能看到一个可工作的登录表单。4.3 server/ 目录实现安全的登录方法与数据库操作client/发起了请求现在轮到server/来响应。我们要实现一个安全的登录方法它会验证用户凭据并返回用户基本信息。创建server/main.js注册登录方法// server/main.js import { Meteor } from meteor/meteor; import { Accounts } from meteor/accounts-base; import { check } from meteor/check; Meteor.methods({ user.login(email, password) { // ✅ 类型校验防止恶意输入 check(email, String); check(password, String); // ✅ 使用 Meteor 内置的 Accounts API而不是自己查数据库 // 这样能自动处理密码哈希、失败次数限制等安全细节 try { // Meteor.loginWithPassword 会返回一个 token但我们只关心用户信息 const userId Meteor.userId(); if (!userId) { throw new Meteor.Error(not-logged-in, Please log in first); } const user Meteor.users.findOne(userId, { fields: { profile.name: 1, emails.0.address: 1 } }); return { _id: user._id, name: user.profile?.name || Anonymous, email: user.emails?.[0]?.address || }; } catch (error) { throw new Meteor.Error(login-failed, error.message); } } });创建server/startup.js初始化管理员用户利用前面讲的Meteor.defer// server/startup.js import { Meteor } from meteor/meteor; import { Accounts } from meteor/accounts-base; Meteor.startup(() { Meteor.defer(() { // 如果没有管理员用户创建一个 if (Meteor.users.find({profile.role: admin}).count() 0) { Accounts.createUser({ email: adminexample.com, password: admin123, profile: { name: System Admin, role: admin } }); } }); });这个server/目录的实现体现了 Meteor 的安全哲学不重复造轮子。我们没有自己写密码校验逻辑而是信任Accounts包的成熟实现我们没有手动查users集合而是用Meteor.userId()获取当前上下文。所有这些代码只在服务端运行client/下的 JS 永远看不到它们。4.4 lib/ 目录实现共享的用户角色校验工具现在我们想在多个地方比如服务端方法、客户端订阅检查用户是否有管理员权限。这个逻辑必须共享所以放进lib/。创建lib/roles.js// lib/roles.js export const hasRole (userId, role) { if (!userId) return false; const user Meteor.users.findOne(userId, { fields: { profile.role: 1 } }); return user?.profile?.role role; }; // ✅ 纯函数无副作用无环境依赖 export const ROLES { ADMIN: admin, USER: user };在server/main.js里使用它// server/main.js (追加) import { hasRole, ROLES } from /lib/roles.js; Meteor.methods({ admin.dashboard() { if (!this.userId || !hasRole(this.userId, ROLES.ADMIN)) { throw new Meteor.Error(not-authorized, Admin access required); } return { message: Welcome to admin dashboard! }; } });在client/main.js里使用它用于 UI 权限控制// client/main.js (追加) import { hasRole, ROLES } from /lib/roles.js; Template.loginForm.helpers({ isAdmin() { return Meteor.userId() hasRole(Meteor.userId(), ROLES.ADMIN); } });lib/目录的这个实现完美展示了“跨环境共享”的威力同一套角色校验逻辑既用于服务端鉴权也用于客户端 UI 显示控制且代码零重复。4.5 public/ 与 private/ 目录实现静态资源与敏感配置最后我们来演示public/和private/的协同工作。在public/下放一个 logomkdir -p public/images # 假设你有一个 logo.png 文件放到 public/images/logo.png在client/main.html里引用它!-- client/main.html (在 body 内追加) -- img src/images/logo.png altLogo width100 /在private/下创建一个敏感配置mkdir -p private/config # 创建 private/config/api-keys.json内容如下 # { # stripe: sk_test_XXXXXXXXXXXXXXXXXXXX, # sendgrid: SG.xxxxxx # }在server/main.js里安全读取它// server/main.js (追加) import { Assets } from meteor/assets; Meteor.startup(() { try { const keysText Assets.getText(config/api-keys.json); const keys JSON.parse(keysText); process.env.STRIPE_KEY keys.stripe; process.env.SENDGRID_KEY keys.sendgrid; } catch (error) { console.error(Failed to load private config:, error); } });这样public/的 logo 对所有人可见而private/的 API 密钥只在服务端内存里存在永远不会被浏览器下载到。整个流程没有一行配置全是目录约定驱动。5. 常见问题与排查技巧实录那些让你抓狂的 Meteor 目录报错在真实项目里Meteor 的特殊目录机制带来的报错往往让人摸不着头脑。下面整理了我遇到过的、最典型、最高频的 8 个问题每个都附带错误日志、根本原因、排查思路和终极解决方案。这些不是教科书式的问答而是从凌晨三点的 Slack 消息里抢救出来的实战记录。5.1 问题速查表Meteor 目录相关错误诊断指南错误现象错误日志片段根本原因排查思路解决方案客户端报错ReferenceError: Meteor is not definedUncaught ReferenceError: Meteor is not defined at client/main.js:1client/下的 JS 文件被当作了普通浏览器脚本加载而非 Meteor bundle 的一部分检查文件是否真的在client/目录下检查文件扩展名是否是.js不是.ts或.jsx且未配置编译器检查是否在 HTML 里用script src...手动引入了它确保文件在client/下删除手动script标签如果是 TypeScript确保已安装typescript和types/meteor包服务端报错Error: Cannot find module fsError: Cannot find module fs at client/lib/utils.js:1把 Node.js 原生模块如fs,path放到了client/或lib/下而这些模块在浏览器里不存在搜索整个项目找到所有require(fs)或import * as fs from fs确认这些代码的执行环境把fs相关代码移到server/下如果必须在lib/里用用if (typeof window undefined) { ... }包裹public/ 资源 404GET http://localhost:3000/images/logo.png 404 (Not Found)public/下的文件路径与浏览器请求路径不匹配或文件名大小写错误Linux 服务器区分大小写在终端执行ls -la public/images/确认文件存在且名字完全一致检查浏览器地址栏的 URL 是否多了一个斜杠或少了一个斜杠确保public/下的路径与img src/images/logo.png中的路径完全一致在 Linux 服务器上用ls命令确认大小写private/ 资源读取失败Error: Cannot find asset: config.jsonAssets.getText(config.json)的参数是相对于private/的路径但文件实际在private/config/下检查private/目录结构确认Assets.getText的参数是否包含了子目录Assets.getText(config/config.json)而不是Assets.getText(config.json)lib/ 代码未生效TypeError: mySharedFunction is not a functionlib/下的代码没有被正确 import或者 import 路径错误Meteor 的模块解析是基于项目根目录的在client/main.js里console.log(import.meta.url)确认当前文件路径检查 import 语句的路径是否以/开头所有 import 必须以/开头如import { x } from /lib/utils.js绝对不要用../lib/utils.jstests/ 不运行No tests foundmeteor test命令默认只运行tests/下的文件但如果项目里有package.json的test脚本可能会冲突运行meteor test --help确认