Appearance
Meteor-RPC
Who maintains the package
- Grubba27,你可以通过 X 联系我们¥
Who maintains the package
– Grubba27, you can get in touch via X
这是个什么包?
¥What is this package?
受 zodern:relay 和 tRPC 的启发
¥Inspired on zodern:relay and on tRPC
此软件包提供了构建专注于 React 前端的端到端类型安全 RPC 的函数。
¥This package provides functions for building E2E type-safe RPCs focused on React front ends.
如何下载?
¥How to download it?
警告
此软件包仅适用于 Meteor 2.8 或更高版本。
¥This package works only with Meteor 2.8 or higher.
如果你不确定正在使用的 Meteor 版本,可以在项目的终端中运行以下命令进行检查:
¥If you are not sure about the version of Meteor you are using, you can check it by running the following command in your terminal within your project:
bash
meteor --version
bash
meteor npm i meteor-rpc @tanstack/react-query zod
警告
在继续安装之前,请确保你的项目中已设置好 react-query
;更多信息,请关注他们的 快速入门指南。
¥Before continuing the installation, make sure you have react-query
all set in your project; for more info, follow their quick start guide.
如何使用?
¥How to use it?
使用此包时,有几个重要的概念:
¥There are a few concepts that are important while using this package:
此包构建于
Meteor.methods
和Meteor.publish
之上,但包含类型和运行时验证,因此理解它们对于使用此包至关重要。¥This package is built on top of
Meteor.methods
andMeteor.publish
but with types and runtime validation, their understanding is important to use this package.每个方法和发布都使用
Zod
来验证参数,因此你可以确保收到的数据符合你的预期。¥Every method and publication uses
Zod
to validate the arguments, so you can be sure that the data you are receiving is what you expect.
提示
如果你接受任何类型的数据,则可以使用 z.any()
作为架构,或者在没有参数时使用 z.void
。
¥If you are accepting any type of data, you can use z.any()
as the schema or z.void
when there is no argument
createModule
此函数用于创建一个模块,该模块将用于调用我们的方法和发布。
¥This function is used to create a module that will be used to call our methods and publications
不带命名空间的 subModule
:createModule()
用于创建 main
服务器模块,该模块将被导出以供客户端使用。
¥subModule
without a namespace: createModule()
is used to create the main
server module, the one that will be exported to be used in the client.`
带有命名空间的 subModule
:createModule("namespace")
用于创建将添加到主模块的子模块。
¥subModule
with a namespace: createModule("namespace")
is used to create a submodule that will be added to the main module.
请记住在模块创建结束时使用
build
,以确保模块能够被创建。¥Remember to use
build
at the end of module creation to ensure that the module will be created.
示例:
¥Example:
typescript
import { createModule } from "meteor-rpc";
import { Chat } from "./chat";
const server = createModule() // server has no namespace
.addMethod("bar", z.string(), (arg) => "bar" as const)
.addSubmodule(Chat)
.build();
export type Server = typeof server;
typescript
import { createModule } from "meteor-rpc";
import { ChatCollection } from "/imports/api/chat";
import { z } from "zod";
export const Chat = createModule("chat")
.addMethod("createChat", z.void(), async () => {
return ChatCollection.insertAsync({ createdAt: new Date(), messages: [] });
})
.buildSubmodule();
typescript
import { createClient } from "meteor-rpc";
// you must import the type of the server
import type { Server } from "/imports/api/server";
const api = createClient<Server>();
const bar: "bar" = await api.bar("some string");
// ?^ 'bar'
const newChatId = await api.chat.createChat(); // with intellisense
module.addMethod
类型:
¥Type:
ts
addMethod(
name: string,
schema: ZodSchema,
handler: (args: ZodTypeInput<ZodSchema>) => T,
config?: Config<ZodTypeInput<ZodSchema>, T>
)
这相当于 Meteor.methods
,但具有类型和运行时验证。
¥This is the equivalent of Meteor.methods
but with types and runtime validation.
typescript
import { createModule } from "meteor-rpc";
import { z } from "zod";
const server = createModule()
.addMethod("foo", z.string(), (arg) => "foo" as const)
.build();
typescript
import { Meteor } from "meteor/meteor";
import { z } from "zod";
Meteor.methods({
foo(arg: string) {
z.string().parse(arg);
return "foo";
},
});
module.addPublication
类型:
¥Type:
typescript
addPublication(
name: string,
schema: ZodSchema,
handler: (args: ZodTypeInput<ZodSchema>) => Cursor<Result, Result>
)
这相当于 Meteor.publish
,但具有类型和运行时验证。
¥This is the equivalent of Meteor.publish
but with types and runtime validation.
typescript
import { createModule } from "meteor-rpc";
import { ChatCollection } from "/imports/api/chat";
import { z } from "zod";
const server = createModule()
.addPublication("chatRooms", z.void(), () => {
return ChatCollection.find();
})
.build();
typescript
import { Meteor } from "meteor/meteor";
import { ChatCollection } from "/imports/api/chat";
Meteor.publish("chatRooms", function () {
return ChatCollection.find();
});
module.addSubmodule
这用于向主模块添加子模块,为你的方法和发布添加命名空间,并使你的代码组织更容易。
¥This is used to add a submodule to the main module, adding namespaces for your methods and publications and making it easier to organize your code.
请记住在创建子模块时使用
submodule.buildSubmodule
¥Remember to use
submodule.buildSubmodule
when creating a submodule
typescript
import { ChatCollection } from "/imports/api/chat";
import { createModule } from "meteor-rpc";
export const chatModule = createModule("chat")
.addMethod("createChat", z.void(), async () => {
return ChatCollection.insertAsync({ createdAt: new Date(), messages: [] });
})
.buildSubmodule(); // <-- This is important so that this module can be added as a submodule
typescript
import { createModule } from "meteor-rpc";
import { chatModule } from "./server/chat";
const server = createModule()
.addMethod("bar", z.string(), (arg) => "bar" as const)
.addSubmodule(chatModule)
.build();
server.chat; // <-- this is the namespace for the chat module
server.chat.createChat(); // <-- this is the method from the chat module and it gets autocompleted
module.addMiddlewares
类型:
¥Type:
typescript
type Middleware = (raw: unknown, parsed: unknown) => void;
addMiddlewares(middlewares: Middleware[])
这用于向模块添加中间件;它应该用于为方法和发布添加副作用逻辑,这对于日志记录或速率限制非常理想。
¥This is used to add middleware to the module; it should be used to add side effects logic to the methods and publications, which is ideal for logging or rate limiting.
中间件的排序遵循后进先出的原则。查看以下示例:
¥The middleware ordering is last in, first out. Check the example below:
typescript
import { ChatCollection } from "/imports/api/chat";
import { createModule } from "meteor-rpc";
export const chatModule = createModule("chat")
.addMiddlewares([
(raw, parsed) => {
console.log("runs first");
},
])
.addMethod("createChat", z.void(), async () => {
return ChatCollection.insertAsync({ createdAt: new Date(), messages: [] });
})
.buildSubmodule();
typescript
import { createModule } from "meteor-rpc";
import { chatModule } from "./server/chat";
const server = createModule()
.addMiddlewares([
(raw, parsed) => {
console.log("runs second");
},
])
.addMethod("bar", z.string(), (arg) => "bar" as const)
.addSubmodule(chatModule)
.build();
typescript
import { createClient } from "meteor-rpc";
import type { Server } from "/imports/api/server"; // you must import the type
const api = createClient<Server>();
await api.chat.createChat(); // logs "runs first" then "runs second"
await api.bar("str"); // logs "runs second"
module.build
这用于构建模块,应在模块创建结束时使用它,以确保导出的类型正确。
¥This is used to build the module, it should be used at the end of the module creation to ensure that the exported type is correct.
typescript
// ✅ it has the build method
import { createModule } from "meteor-rpc";
import { z } from "zod";
const server = createModule()
.addMethod("bar", z.string(), (arg) => "bar" as const)
.build();
export type Server = typeof server;
typescript
// ❌ it is missing the build method
import { createModule } from "meteor-rpc";
import { z } from "zod";
const server = createModule().addMethod(
"bar",
z.string(),
(arg) => "bar" as const
);
export type Server = typeof server;
module.buildSubmodule
这用于构建子模块,应在子模块创建结束时使用它,并在主模块的 addSubmodule
方法中导入。
¥This is used to build the submodule, it should be used at the end of the submodule creation and imported in the main module in the addSubmodule
method.
typescript
import { createModule } from "meteor-rpc";
import { z } from "zod";
export const chatModule = createModule("chat")
.addMethod("createChat", z.void(), async () => {
return "chat" as const;
})
// ✅ it has the buildSubmodule method
.buildSubmodule();
typescript
import { createModule } from "meteor-rpc";
import { z } from "zod";
export const otherSubmodule = createModule("other")
.addMethod("otherMethod", z.void(), async () => {
return "other" as const;
})
// ❌ it is missing the buildSubmodule method
.build();
export const otherSubmodule = createModule("other").addMethod(
"otherMethod",
z.void(),
async () => {
return "other" as const;
}
); // ❌ it is missing the buildSubmodule method
typescript
import { createModule } from "meteor-rpc";
import { chatModule } from "./server/chat";
const server = createModule()
.addMethod("bar", z.string(), (arg) => "bar" as const)
.addSubmodule(chatModule)
.build();
在客户端中使用
¥Using in the client
在客户端使用时,你必须使用 createModule
和 build
方法创建将在客户端中使用的模块,并确保导出模块的类型。
¥When using in the client, you have to use the createModule
and build
methods to create a module that will be used in the client and be sure that you are exporting the type of the module
你的应用中只能创建一个客户端。
¥You should only create one client in your application
你可以使用类似 api.ts
的功能来导出客户端及其类型。
¥You can have something like api.ts
that will export the client and the type of the client
typescript
import { createModule } from "meteor-rpc";
const server = createModule()
.addMethod("bar", z.string(), (arg) => "bar" as const)
.build();
export type Server = typeof server;
typescript
// you must import the type
import type { Server } from "/imports/api/server";
const app = createClient<Server>();
await app.bar("str"); // it will return "bar"
React 核心 API
¥React focused API
我们的软件包有一个以 React 为中心的 API,使用 react-query
来处理数据获取和变更。
¥Our package has a React-focused API that uses react-query
to handle the data fetching and mutations.
method.useMutation
它使用来自 react-query 的 useMutation
来创建一个将调用该方法的变更
¥It uses the useMutation
from react-query to create a mutation that will call the method
typescript
import { createModule } from "meteor-rpc";
const server = createModule()
.addMethod("bar", z.string(), (arg) => {
console.log("Server received", arg);
return "bar" as const;
})
.build();
export type Server = typeof server;
tsx
// you must import the type
import type { Server } from "/imports/api/server";
const app = createClient<Server>();
export const Component = () => {
const { mutate, isLoading, isError, error, data } = app.bar.useMutation();
return (
<button
onClick={() => {
mutation.mutate("str");
}}
>
Click me
</button>
);
};
method.useQuery
它使用来自 react-query 的 useQuery
来创建一个将调用该方法的查询,并使用 suspense
来处理加载状态
¥It uses the useQuery
from react-query to create a query that will call the method, it uses suspense
to handle loading states
typescript
import { createModule } from "meteor-rpc";
const server = createModule()
.addMethod("bar", z.string(), (arg) => "bar" as const)
.build();
export type Server = typeof server;
tsx
// you must import the type of the server
import type { Server } from "/imports/api/server";
const app = createClient<Server>();
export const Component = () => {
const { data } = app.bar.useQuery("str"); // this will trigger suspense
return <div>{data}</div>;
};
publication.useSubscription
客户端上的订阅具有 useSubscription
方法,可用作订阅发布的钩子。它使用 suspense
来处理加载状态
¥Subscriptions on the client have useSubscription
method that can be used as a hook to subscribe to a publication. It uses suspense
to handle loading states
typescript
// server/main.ts
import { createModule } from "meteor-rpc";
import { ChatCollection } from "/imports/api/chat";
import { z } from "zod";
const server = createModule()
.addPublication("chatRooms", z.void(), () => {
return ChatCollection.find();
})
.build();
export type Server = typeof server;
tsx
import type { Server } from "/imports/api/server"; // you must import the type
const app = createClient<Server>();
export const Component = () => {
// it will trigger suspense and `rooms` is reactive in this context.
// When there is a change in the collection it will rerender
const { data: rooms, collection: chatCollection } =
api.chatRooms.usePublication();
return (
<div>
{rooms.map((room) => (
<div key={room._id}>{room.name}</div>
))}
</div>
);
};
示例
¥Examples
目前,我们有:
¥Currently, we have:
使用此包创建聊天应用的 chat-app
¥chat-app that uses this package to create a chat-app
¥askme that uses this package to create a Q&A app, you can check it live here
高级用法
¥Advanced usage
你可以利用钩子为你的方法添加自定义逻辑,检查原始数据、解析数据以及方法的结果。如果方法失败,你还可以检查错误。
¥You can take advantage of the hooks to add custom logic to your methods, checking the raw and parsed data and the result of the method, If the method fails, you can also check the error.
typescript
import { createModule } from "meteor-rpc";
import { z } from "zod";
const server = createModule()
.addMethod("bar", z.string(), (arg) => "bar" as const)
.build();
// you can add hooks after the method has been created
server.bar.addBeforeResolveHook((raw, parsed) => {
console.log("before resolve", raw, parsed);
});
server.bar.addAfterResolveHook((raw, parsed, result) => {
console.log("after resolve", raw, parsed, result);
});
server.bar.addErrorResolveHook((err, raw, parsed) => {
console.log("on error", err, raw, parsed);
});
export type Server = typeof server;
typescript
import { createModule } from "meteor-rpc";
import { z } from "zod";
const server = createModule()
// Or you can add hooks when creating the method
.addMethod("bar", z.any(), () => "str", {
hooks: {
onBeforeResolve: [
(raw, parsed) => {
console.log("before resolve", raw, parsed);
},
],
onAfterResolve: [
(raw, parsed, result) => {
console.log("after resolve", raw, parsed, result);
},
],
onErrorResolve: [
(err, raw, parsed) => {
console.log("on error", err, raw, parsed);
},
],
},
})
.build();
export type Server = typeof server;
已知问题
¥Known issues
如果你遇到类似以下错误:
¥if you are getting a similar error like this one:
text
=> Started MongoDB.
Typescript processing requested for web.browser using Typescript 5.7.2
Creating new Typescript watcher for /app
Starting compilation in watch mode...
Compiling server/chat/model.ts
Compiling server/chat/module.ts
Compiling server/main.ts
Writing .meteor/local/plugin-cache/refapp_meteor-typescript/0.5.6/v2cache/buildfile.tsbuildinfo
Compilation finished in 0.3 seconds. 3 files were (re)compiled.
did not find /app/.meteor/local/plugin-cache/refapp_meteor-typescript/0.5.6/v2cache/out/client/main.js
did not find /app/.meteor/local/plugin-cache/refapp_meteor-typescript/0.5.6/v2cache/out/client/main.js
Nothing emitted for client/main.tsx
node:internal/crypto/hash:115
throw new ERR_INVALID_ARG_TYPE(
^
TypeError [ERR_INVALID_ARG_TYPE]: The "data" argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received null
at Hash.update (node:internal/crypto/hash:115:11)
at /Users/user/.meteor/packages/meteor-tool/.3.0.4.1tddsze.as7rh++os.osx.arm64+web.browser+web.browser.legacy+web.cordova/mt-os.osx.arm64/tools/fs/tools/fs/watch.ts:329:28
at Array.forEach (<anonymous>)
at /Users/user/.meteor/packages/meteor-tool/.3.0.4.1tddsze.as7rh++os.osx.arm64+web.browser+web.browser.legacy+web.cordova/mt-os.osx.arm64/tools/fs/tools/fs/watch.ts:329:8
at JsOutputResource._get (/tools/isobuild/compiler-plugin.js:1002:19) {
code: 'ERR_INVALID_ARG_TYPE'
}
Node.js v20.18.0
请检查你是否正在使用 refapp:meteor-typescript
软件包,如果是,你可以将其移除并改用 typescript
软件包。refapp:meteor-typescript
软件包目前与 meteor-rpc
软件包不兼容。
¥Please check if you are using refapp:meteor-typescript
package, if so, you can remove it and use the typescript
package instead. The refapp:meteor-typescript
package is currently incompatible with the meteor-rpc
package.
如果仍然无法正常工作,请在 repo 中提交问题。
¥If it is still not working, please open an issue in the repo