Skip to content

Meteor-RPC

  • Who maintains the package - Grubba27,你可以通过 X 联系我们

    ¥Who maintains the packageGrubba27, you can get in touch via X

这是个什么包?

¥What is this package?

zodern:relaytRPC 的启发

¥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.methodsMeteor.publish 之上,但包含类型和运行时验证,因此理解它们对于使用此包至关重要。

    ¥This package is built on top of Meteor.methods and Meteor.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

不带命名空间的 subModulecreateModule() 用于创建 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.`

带有命名空间的 subModulecreateModule("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

在客户端使用时,你必须使用 createModulebuild 方法创建将在客户端中使用的模块,并确保导出模块的类型。

¥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,你可以实时查看 此处

    ¥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