meteor-fullstack
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMeteor Full-Stack Development (v3.x + React)
Meteor全栈开发(v3.x + React)
Modern Meteor 3.x (and 3.5+) full-stack development guide covering async-first patterns, React integration, MongoDB collections, methods, pub/sub, and project architecture.
Meteor 3 removed Fibers entirely — all server-side I/O is standard async/await. This is the single most important thing to internalize: every collection operation, every method body, every publication setup function that touches the database must be async. Node 24 is the standard runtime starting in Meteor 3.5.
现代Meteor 3.x(及3.5+)全栈开发指南,涵盖异步优先模式、React集成、MongoDB集合、方法、发布/订阅(pub/sub)以及项目架构。
Meteor 3彻底移除了Fibers——所有服务器端I/O均采用标准的async/await。这是需要掌握的最重要知识点:所有涉及数据库的集合操作、方法体、发布设置函数都必须是异步的。从Meteor 3.5开始,Node 24成为标准运行时。
Quick Reference: Async Collection APIs
快速参考:异步集合API
Always use the variants on the server. Sync versions exist only for client-side Minimongo.
*Async| Operation | Async API (server) | Sync API (client Minimongo only) |
|---|---|---|
| Insert | | |
| Find one | | |
| Update | | |
| Upsert | | |
| Remove | | |
| Count | | |
| Fetch | | |
| forEach | | |
| map | | |
| Observe | | |
| Create index | | — |
| Send email | | |
Publications still return sync cursors via for live-query reactivity — that hasn't changed.
collection.find(...)在服务器端请始终使用变体。同步版本仅适用于客户端Minimongo。
*Async| 操作 | 异步API(服务器端) | 同步API(仅客户端Minimongo) |
|---|---|---|
| 插入 | | |
| 查询单个文档 | | |
| 更新 | | |
| 插入或更新 | | |
| 删除 | | |
| 计数 | | |
| 获取结果 | | |
| 遍历 | | |
| 映射 | | |
| 监听 | | |
| 创建索引 | | — |
| 发送邮件 | | |
发布功能仍通过返回同步游标以实现实时查询响应性——这一点没有变化。
collection.find(...)Core Concepts at a Glance
核心概念概览
Methods (RPC)
方法(RPC)
Server functions callable from the client. In v3, always async:
js
Meteor.methods({
async 'todos.create'(text) {
if (!this.userId) throw new Meteor.Error('not-authorized');
return await Todos.insertAsync({ text, createdAt: new Date(), userId: this.userId });
},
});
// Client
const id = await Meteor.callAsync('todos.create', 'Buy milk');Read for stubs, optimistic UI, options, and the simulation timing trap.
references/methods-rpc.mdapplyAsync客户端可调用的服务器函数。在v3版本中,必须始终使用异步写法:
js
Meteor.methods({
async 'todos.create'(text) {
if (!this.userId) throw new Meteor.Error('not-authorized');
return await Todos.insertAsync({ text, createdAt: new Date(), userId: this.userId });
},
});
// 客户端
const id = await Meteor.callAsync('todos.create', 'Buy milk');如需了解存根、乐观UI、选项以及模拟时序陷阱,请阅读。
applyAsyncreferences/methods-rpc.mdPublications & Subscriptions
发布与订阅
Server pushes reactive data to the client over DDP:
js
// Server
Meteor.publish('todos.byUser', function () {
if (!this.userId) return this.ready();
return Todos.find({ userId: this.userId });
});
// Client (React)
const { todos, isLoading } = useTracker(() => {
const handle = Meteor.subscribe('todos.byUser');
return {
isLoading: !handle.ready(),
todos: Todos.find({}, { sort: { createdAt: -1 } }).fetch(),
};
}, []);Read for composite publications, SubsManager caching, counts, publication wrappers, and MongoDB Change Streams configuration.
references/pubsub.md服务器通过DDP向客户端推送响应式数据:
js
// 服务器
Meteor.publish('todos.byUser', function () {
if (!this.userId) return this.ready();
return Todos.find({ userId: this.userId });
});
// 客户端(React)
const { todos, isLoading } = useTracker(() => {
const handle = Meteor.subscribe('todos.byUser');
return {
isLoading: !handle.ready(),
todos: Todos.find({}, { sort: { createdAt: -1 } }).fetch(),
};
}, []);如需了解复合发布、SubsManager缓存、计数、发布包装器以及MongoDB变更流配置,请阅读。
references/pubsub.mdREST APIs (accounts-express)
REST API(accounts-express)
Build authenticated REST endpoints seamlessly using Express and :
accounts-expressjs
import express from 'express';
import { WebApp } from 'meteor/webapp';
import { createAuthMiddleware } from 'meteor/accounts-express';
const app = express();
app.use('/api', createAuthMiddleware({ required: true }));
app.get('/api/me', async (req, res) => {
const user = await Meteor.userAsync();
res.json({ userId: Meteor.userId(), email: user?.emails?.[0]?.address });
});
WebApp.handlers.use(app);Read for REST API module organization.
references/architecture.md使用Express和无缝构建带认证的REST端点:
accounts-expressjs
import express from 'express';
import { WebApp } from 'meteor/webapp';
import { createAuthMiddleware } from 'meteor/accounts-express';
const app = express();
app.use('/api', createAuthMiddleware({ required: true }));
app.get('/api/me', async (req, res) => {
const user = await Meteor.userAsync();
res.json({ userId: Meteor.userId(), email: user?.emails?.[0]?.address });
});
WebApp.handlers.use(app);如需了解REST API模块组织方式,请阅读。
references/architecture.mdReact Integration
React集成
Meteor's reactive data layer connects to React through (hooks) or (HOC):
useTrackerwithTrackerjsx
import { useTracker } from 'meteor/react-meteor-data';
function TodoList() {
const { todos, user } = useTracker(() => ({
todos: Todos.find().fetch(),
user: Meteor.user(),
}));
return todos.map(t => <TodoItem key={t._id} todo={t} user={user} />);
}Read for patterns, subscription lifecycle in components, and common pitfalls.
references/react-integration.mdwithTrackerMeteor的响应式数据层通过(钩子)或(高阶组件)与React连接:
useTrackerwithTrackerjsx
import { useTracker } from 'meteor/react-meteor-data';
function TodoList() {
const { todos, user } = useTracker(() => ({
todos: Todos.find().fetch(),
user: Meteor.user(),
}));
return todos.map(t => <TodoItem key={t._id} todo={t} user={user} />);
}如需了解模式、组件中的订阅生命周期以及常见陷阱,请阅读。
withTrackerreferences/react-integration.mdProject Structure
项目结构
A typical Meteor 3 + React project:
my-app/
├── client/ # Client entry, main.jsx, global styles
│ └── main.jsx # Meteor.startup(() => render(<App />))
├── server/ # Server entry, publications, startup
│ ├── main.js # Meteor.startup, indexes, seeds
│ └── publications/ # Pub definitions by domain
├── imports/ # Shared code (lazy-loaded by convention)
│ ├── api/ # Collections, methods, schemas
│ │ ├── todos/
│ │ │ ├── collection.js
│ │ │ ├── methods.js
│ │ │ └── publications.js
│ │ └── users/
│ ├── ui/ # React components
│ │ ├── components/ # Reusable UI
│ │ ├── pages/ # Route-level components
│ │ └── layouts/ # Layout wrappers
│ └── startup/ # Client/server bootstrap
├── public/ # Static assets (served as-is)
├── private/ # Server-only assets (Assets API)
├── .meteor/ # Meteor internals, packages, versions
└── package.jsonKey conventions:
- Everything under is lazy — only loaded when explicitly imported
imports/ - and
client/directories are eagerly loaded on their respective sidesserver/ - Files outside that aren't in
imports/orclient/load on both sidesserver/
Read for circular dependency prevention, import rules, and module organization patterns.
references/architecture.md典型的Meteor 3 + React项目结构:
my-app/
├── client/ # 客户端入口、main.jsx、全局样式
│ └── main.jsx # Meteor.startup(() => render(<App />))
├── server/ # 服务器入口、发布、启动逻辑
│ ├── main.js # Meteor.startup、索引、种子数据
│ └── publications/ # 按领域划分的发布定义
├── imports/ # 共享代码(按约定懒加载)
│ ├── api/ # 集合、方法、模式
│ │ ├── todos/
│ │ │ ├── collection.js
│ │ │ ├── methods.js
│ │ │ └── publications.js
│ │ └── users/
│ ├── ui/ # React组件
│ │ ├── components/ # 可复用UI组件
│ │ ├── pages/ # 路由级组件
│ │ └── layouts/ # 布局包装器
│ └── startup/ # 客户端/服务器启动引导
├── public/ # 静态资源(原样提供)
├── private/ # 服务器专属资源(通过Assets API访问)
├── .meteor/ # Meteor内部文件、包、版本信息
└── package.json关键约定:
- 下的所有代码均为懒加载——仅在显式导入时加载
imports/ - 和
client/目录下的代码会在对应端自动加载server/ - 不在、
imports/或client/中的文件会在客户端和服务器端同时加载server/
如需了解循环依赖预防、导入规则以及模块组织模式,请阅读。
references/architecture.mdCommon Patterns
常见模式
Error Handling in Methods
方法中的错误处理
Only reaches the client — other exceptions are sanitized to a generic 500:
Meteor.Errorjs
// Server method
async 'orders.cancel'(orderId) {
const order = await Orders.findOneAsync(orderId);
if (!order) throw new Meteor.Error('not-found', 'Order not found');
if (order.userId !== this.userId) throw new Meteor.Error('not-authorized', 'Not your order');
await Orders.updateAsync(orderId, { $set: { status: 'cancelled' } });
}
// Client
try {
await Meteor.callAsync('orders.cancel', orderId);
} catch (err) {
if (err.error === 'not-found') showToast(err.reason);
}只有会传递到客户端——其他异常会被处理为通用的500错误:
Meteor.Errorjs
// 服务器方法
async 'orders.cancel'(orderId) {
const order = await Orders.findOneAsync(orderId);
if (!order) throw new Meteor.Error('not-found', '订单不存在');
if (order.userId !== this.userId) throw new Meteor.Error('not-authorized', '这不是你的订单');
await Orders.updateAsync(orderId, { $set: { status: 'cancelled' } });
}
// 客户端
try {
await Meteor.callAsync('orders.cancel', orderId);
} catch (err) {
if (err.error === 'not-found') showToast(err.reason);
}Collection Helpers
集合助手
Attach computed properties and methods to documents using :
dburles:collection-helpersjs
Todos.helpers({
isOverdue() {
return this.dueDate && this.dueDate < new Date();
},
owner() {
return Meteor.users.findOne(this.userId);
},
});
// Usage — any document from Todos.find/findOne gets these methods
const todo = Todos.findOne(id);
if (todo.isOverdue()) { /* ... */ }使用为文档附加计算属性和方法:
dburles:collection-helpersjs
Todos.helpers({
isOverdue() {
return this.dueDate && this.dueDate < new Date();
},
owner() {
return Meteor.users.findOne(this.userId);
},
});
// 使用方式——从Todos.find/findOne获取的任何文档都拥有这些方法
const todo = Todos.findOne(id);
if (todo.isOverdue()) { /* ... */ }Authorization Pattern
授权模式
Guard methods and publications with :
this.userIdjs
Meteor.methods({
async 'projects.archive'(projectId) {
if (!this.userId) throw new Meteor.Error('not-authorized');
const project = await Projects.findOneAsync(projectId);
if (project.ownerId !== this.userId) {
throw new Meteor.Error('forbidden', 'Only the owner can archive');
}
return await Projects.updateAsync(projectId, { $set: { archived: true } });
},
});使用保护方法和发布:
this.userIdjs
Meteor.methods({
async 'projects.archive'(projectId) {
if (!this.userId) throw new Meteor.Error('not-authorized');
const project = await Projects.findOneAsync(projectId);
if (project.ownerId !== this.userId) {
throw new Meteor.Error('forbidden', '只有所有者可以归档项目');
}
return await Projects.updateAsync(projectId, { $set: { archived: true } });
},
});Accounts & Users
账户与用户
Meteor's built-in accounts system provides , , and their async equivalents:
Meteor.userId()Meteor.user()js
// Server — async required in v3
const user = await Meteor.userAsync();
// Client — sync is fine (reads from Minimongo)
const user = Meteor.user();
const userId = Meteor.userId();
// Reactive in useTracker
const user = useTracker(() => Meteor.user(), []);
// Async client logins
await Meteor.loginWithPasswordAsync(email, password);
await Meteor.loginWithTokenAsync(token);Meteor内置的账户系统提供、以及它们的异步版本:
Meteor.userId()Meteor.user()js
// 服务器端——v3版本必须使用异步
const user = await Meteor.userAsync();
// 客户端——同步调用即可(读取Minimongo数据)
const user = Meteor.user();
const userId = Meteor.userId();
// 在useTracker中响应式获取
const user = useTracker(() => Meteor.user(), []);
// 客户端异步登录
await Meteor.loginWithPasswordAsync(email, password);
await Meteor.loginWithTokenAsync(token);Sending Email
发送邮件
Always use on the server — it returns a Promise and fits the async-first model:
Email.sendAsyncjs
import { Email } from 'meteor/email';
Meteor.methods({
async 'notifications.send'(to, subject, html) {
if (!this.userId) throw new Meteor.Error('not-authorized');
await Email.sendAsync({ from: 'noreply@example.com', to, subject, html });
},
});在服务器端请始终使用——它返回Promise并符合异步优先模型:
Email.sendAsyncjs
import { Email } from 'meteor/email';
Meteor.methods({
async 'notifications.send'(to, subject, html) {
if (!this.userId) throw new Meteor.Error('not-authorized');
await Email.sendAsync({ from: 'noreply@example.com', to, subject, html });
},
});What to Watch Out For
注意事项
1. "Can't set timers inside simulations"
1. "Can't set timers inside simulations"
This browser error occurs when an async method stub is running (simulation context is active) and a / component re-renders, calling . Fix: add at the top of complex stubs to make them no-ops on the client. The server still runs the full logic; Minimongo updates via the subscription.
withTrackeruseTrackerMeteor.deferif (Meteor.isClient) return;当异步方法存根运行时(模拟上下文处于激活状态),/组件重新渲染并调用,会触发此浏览器错误。修复方法:在复杂存根顶部添加,使其在客户端成为空操作。服务器仍会运行完整逻辑;Minimongo通过订阅更新数据。
withTrackeruseTrackerMeteor.deferif (Meteor.isClient) return;2. Meteor.call
with async stubs
Meteor.call2. 异步存根搭配Meteor.call
Meteor.callMeteor.callMeteor.callAsyncMeteor.callMeteor.callAsync3. Circular dependencies in barrel imports
3. 桶式导入中的循环依赖
Mixed barrels that re-export models, components, actions, and schemas from a single index file are the #1 source of and errors in Meteor apps. Prefer direct file imports on hot paths.
Element type is invalidundefined在单个索引文件中重新导出模型、组件、操作和模式的混合桶式导入,是Meteor应用中和错误的首要原因。在热路径中优先使用直接文件导入。
Element type is invalidundefined4. Returning non-EJSON values from methods
4. 从方法返回非EJSON值
Method return values must be EJSON-serializable (plain objects, arrays, strings, numbers, dates, binary, ObjectID). Functions, class instances, and circular references will fail.
方法返回值必须是可EJSON序列化的(纯对象、数组、字符串、数字、日期、二进制数据、ObjectID)。函数、类实例和循环引用会导致失败。
5. Publication setup vs cursor return
5. 发布设置与游标返回
Publication functions can be for setup work, but must return a sync cursor or call :
asyncthis.ready()js
Meteor.publish('items.forTeam', async function (teamId) {
const team = await Teams.findOneAsync(teamId);
if (!team.members.includes(this.userId)) return this.ready();
return Items.find({ teamId }); // sync cursor for reactivity
});发布函数可以是用于设置工作,但必须返回同步游标或调用:
asyncthis.ready()js
Meteor.publish('items.forTeam', async function (teamId) {
const team = await Teams.findOneAsync(teamId);
if (!team.members.includes(this.userId)) return this.ready();
return Items.find({ teamId }); // 同步游标用于响应性
});6. Always use field projections in find()
/ findOneAsync()
find()findOneAsync()6. 在find()
/ findOneAsync()
中始终使用字段投影
find()findOneAsync()Fetching full documents when you only need a few fields wastes memory, bandwidth, and serialization time. Pass a (or ) option whenever you don't need the whole document:
fieldsprojectionjs
// Bad — fetches every field on every matching document
const names = await Users.find({ active: true }).fetchAsync();
// Good — only pull what you need
const names = await Users.find({ active: true }, { fields: { username: 1, email: 1 } }).fetchAsync();
// In publications — reduces data pushed over DDP
Meteor.publish('todos.titles', function () {
return Todos.find({ userId: this.userId }, { fields: { text: 1, done: 1, createdAt: 1 } });
});This matters especially in publications: every extra field is serialized and pushed to every subscribed client. See for projection strategies.
references/performance.md当只需要部分字段时获取完整文档会浪费内存、带宽和序列化时间。只要不需要整个文档,就传递(或)选项:
fieldsprojectionjs
// 不良写法——获取匹配文档的所有字段
const names = await Users.find({ active: true }).fetchAsync();
// 良好写法——仅获取所需字段
const names = await Users.find({ active: true }, { fields: { username: 1, email: 1 } }).fetchAsync();
// 在发布中使用——减少通过DDP推送的数据量
Meteor.publish('todos.titles', function () {
return Todos.find({ userId: this.userId }, { fields: { text: 1, done: 1, createdAt: 1 } });
});这在发布中尤为重要:每个额外字段都会被序列化并推送给每个订阅客户端。如需了解投影策略,请阅读。
references/performance.md7. rawCollection()
bypasses collection hooks
rawCollection()7. rawCollection()
会绕过集合钩子
rawCollection()rawCollection()updatedAtreferences/collections-models.mdrawCollection()updatedAtreferences/collections-models.md8. DDP queue blocking — async methods still block each other
8. DDP队列阻塞——异步方法仍会互相阻塞
Even in Meteor 3, where methods are natively , the DDP server preserves sequential per-client execution by default. A method awaiting a slow external API will block all subsequent method calls from that same client until it resolves. Fix: call at the top of methods that are safe to run in parallel (after auth guards). Never unblock write methods whose results are consumed immediately by a follow-up method from the same client (race condition). See for the full guide including the vs. decision matrix.
asyncthis.unblock()references/performance.mdthis.unblock()Meteor.defer()即使在原生支持的Meteor 3中,DDP服务器默认仍保留按客户端顺序执行的规则。等待慢速外部API的方法会阻塞同一客户端的所有后续方法调用,直到它解析完成。修复方法:在安全的并行方法顶部调用(在认证检查之后)。对于结果会被同一客户端后续方法立即使用的写入方法,切勿调用(会导致竞态条件)。如需完整指南,包括与的决策矩阵,请阅读。
asyncthis.unblock()this.unblock()this.unblock()Meteor.defer()references/performance.md9. Multiple publications, same collection, different projections — MergeBox wins unpredictably
9. 多个发布、同一集合、不同投影——MergeBox的合并结果不可预测
When two active subscriptions publish the same document into the same collection name but with different projections, Meteor's MergeBox merges them on the client. The rules:
_idfields- Top-level fields are unioned — if pub A publishes and pub B publishes
title, the client doc gets both. Good.body - Conflicting top-level fields are resolved arbitrarily — if both pubs publish but with different values, one wins. Which one? Unspecified.
status - No deep merge — if pub A sends and pub B sends
{ profile: { name: 'Alice' } }, the entire{ profile: { age: 30 } }object comes from whichever publication "wins" for that field. The other sub-fields silently disappear.profile - Unsub surprise — when one subscription stops, the MergeBox removes the fields it contributed. A component that assumed a field exists may suddenly see .
undefined
The fix: virtual collections. When you need the same document published with genuinely different shapes (e.g., list view vs. full detail), publish into separate client-side collection names:
js
// server — two publications, two DDP collection namespaces
Meteor.publish('messages.list', function (channelId) {
// Lightweight: just what the list UI needs
return Messages.find(
{ channelId },
{ fields: { authorId: 1, preview: 1, createdAt: 1 } }
);
// DDP collection name defaults to 'messages'
});
Meteor.publish('messages.full', function (messageId) {
// Use low-level API to push into a DIFFERENT client collection name
const self = this;
const doc = Messages.findOne(messageId); // or use cursor + observe
if (doc) self.added('messagesFull', doc._id, doc);
self.ready();
});js
// client — two separate Minimongo collections, no merge conflict
export const Messages = new Mongo.Collection('messages'); // list view
export const MessagesFull = new Mongo.Collection('messagesFull'); // detail viewNaming convention: , , — whatever communicates the intended field shape. The key is that each virtual collection has a single, stable field contract.
MessagesFullMessagesStrippedMessagesListUse this pattern whenever: (a) you need different field shapes of the same document in the same session, (b) you have a public list projection and a richer authenticated detail projection, or (c) you've seen fields mysteriously disappear when a second subscription activates.
当两个活跃订阅将同一文档发布到同一集合名称但使用不同投影时,Meteor的MergeBox会在客户端合并它们。规则如下:
_idfields- 顶级字段会被合并——如果发布A推送,发布B推送
title,客户端文档会同时拥有这两个字段。这是合理的。body - 冲突的顶级字段会被任意解析——如果两个发布都推送但值不同,其中一个会生效。具体哪一个?未指定。
status - 不支持深度合并——如果发布A发送,发布B发送
{ profile: { name: 'Alice' } },整个{ profile: { age: 30 } }对象会来自在该字段上"获胜"的发布。另一个发布的子字段会无声消失。profile - 取消订阅的意外情况——当一个订阅停止时,MergeBox会移除它贡献的字段。假设某个字段存在的组件可能会突然看到该字段变为。
undefined
修复方案:虚拟集合。当需要在同一会话中发布同一文档的不同结构(如列表视图 vs 完整详情)时,发布到不同的客户端集合名称:
js
// 服务器端——两个发布,两个DDP集合命名空间
Meteor.publish('messages.list', function (channelId) {
// 轻量版:仅列表UI所需字段
return Messages.find(
{ channelId },
{ fields: { authorId: 1, preview: 1, createdAt: 1 } }
);
// DDP集合名称默认为'messages'
});
Meteor.publish('messages.full', function (messageId) {
// 使用底层API推送到不同的客户端集合名称
const self = this;
const doc = Messages.findOne(messageId); // 或使用游标+observe
if (doc) self.added('messagesFull', doc._id, doc);
self.ready();
});js
// 客户端——两个独立的Minimongo集合,无合并冲突
export const Messages = new Mongo.Collection('messages'); // 列表视图
export const MessagesFull = new Mongo.Collection('messagesFull'); // 详情视图命名约定:、、——只要能传达预期的字段结构即可。关键在于每个虚拟集合都有单一、稳定的字段约定。
MessagesFullMessagesStrippedMessagesList当以下情况时使用此模式:(a) 需要在同一会话中使用同一文档的不同字段结构,(b) 有公开列表投影和更丰富的认证详情投影,(c) 遇到第二个订阅激活时字段神秘消失的情况。
Reference Files
参考文档
For deeper coverage, read these when working on specific areas:
| File | When to read |
|---|---|
| Stubs, optimistic UI, |
| Composite publications, SubsManager, reactive counts, data flow, MergeBox / virtual collection pattern |
| |
| Schemas, helpers, indexes, aggregation, |
| Fibers→async migration, async method/publication patterns |
| Project structure, import rules, circular dependency prevention, REST API ( |
| |
如需深入了解特定领域,请在对应场景下阅读以下文档:
| 文件 | 阅读场景 |
|---|---|
| 存根、乐观UI、 |
| 复合发布、SubsManager、响应式计数、数据流、MergeBox / 虚拟集合模式 |
| |
| 模式、助手、索引、聚合、 |
| Fibers→异步迁移、异步方法/发布模式 |
| 项目结构、导入规则、循环依赖预防、REST API( |
| |
Code Style Defaults
代码风格默认规则
Unless the project specifies otherwise:
- Use named exports (avoid default exports)
- Name files after the primary export
- Prefer direct file imports over barrel imports for React components, actions, and schemas
- Use /
asynceverywhere on the server — never rely on sync collection APIsawait - Guard all methods and publications with checks
this.userId - Throw for client-visible errors
Meteor.Error(errorCode, reason) - Use as the package manager
npm
除非项目另有规定:
- 使用命名导出(避免默认导出)
- 文件名与主要导出内容一致
- 对于React组件、操作和模式,优先使用直接文件导入而非桶式导入
- 在服务器端所有地方使用/
async——绝不依赖同步集合APIawait - 使用检查保护所有方法和发布
this.userId - 对于客户端可见的错误,抛出
Meteor.Error(errorCode, reason) - 使用作为包管理器
npm