Appearance
Jam 方法
¥Jam Method
Who maintains the package
- Jam
这是个什么包?
¥What is this package?
Method 是一种使用 Optimistic UI 创建 Meteor 方法的简便方法。它是为 Meteor 3 构建的,并且与 Meteor 2 兼容,以便于迁移。它是 Validated Method 的替代品,并附带其他功能:
¥Method is an easy way to create Meteor methods with Optimistic UI. It's built with Meteor 3 in mind and is compatible with Meteor 2 to make migration easy. It's meant to be a drop in replacement for Validated Method and comes with additional features:
前后钩子
¥Before and after hooks
适用于所有方法的全局 before 和 after 钩子
¥Global before and after hooks for all methods
管道一系列函数
¥Pipe a series of functions
默认已验证(可覆盖)
¥Authed by default (can be overriden)
轻松配置速率限制
¥Easily configure a rate limit
可选择仅在服务器上运行方法
¥Optionally run a method on the server only
将方法附加到集合(可选)
¥Attach the methods to Collections (optional)
使用受支持的架构包之一或自定义验证函数进行验证
¥Validate with one of the supported schema packages or a custom validate function
无需像 Validated Method 那样使用 .call 调用该方法。
¥No need to use .call to invoke the method as with Validated Method
如何下载?
¥How to download it?
在你的 Meteor 项目中运行以下命令将包添加到你的应用中:
¥Add the package to your app by running the following in your Meteor project:
bash
meteor add jam:method
来源
¥Sources
如何使用?
¥How to use it?
创建方法
¥Create a method
name
是必需的,Meteor 内部会以此来识别它。
¥name
is required and will be how Meteor's internals identifies it.
schema
将使用 支持的 schema 自动验证。
¥schema
will automatically validate using a supported schema.
调用该方法时将执行 run
。
¥run
will be executed when the method is called.
js
import { createMethod } from 'meteor/jam:method'; // can import { Methods } from 'meteor/jam:method' instead and use Methods.create if you prefer
export const create = createMethod({
name: 'todos.create',
schema: Todos.schema, // using jam:easy-schema in this example
async run({ text }) {
const todo = {
text,
done: false,
createdAt: new Date(),
authorId: Meteor.userId(), // can also use this.userId instead of Meteor.userId()
}
const todoId = await Todos.insertAsync(todo);
return todoId;
}
});
支持的模式
¥Supported schemas
目前支持以下模式:
¥Currently, these schemas are supported:
如果你使用 jam:easy-schema
,请务必查看下面的 与 jam:easy-schema 结合使用,了解如何使用更少的样板代码编写方法。
¥If you're using jam:easy-schema
, be sure to check out Using with jam:easy-schema below for details on a way to write methods with less boilerplate.
以下是每个函数语法的简单示例。它们的功能各不相同,因此请选择最符合你需求的。
¥Here's a quick example of each one's syntax. They vary in features so pick the one that best fits your needs.
js
// jam:easy-schema. you'll attach to a Collection so you can reference one {Collection}.schema in your methods
const schema = {text: String, isPrivate: Optional(Boolean)}
// check
const schema = {text: String, isPrivate: Match.Maybe(Boolean)}
// zod
const schema = z.object({text: z.string(), isPrivate: z.boolean().optional()})
// simpl-schema
const schema = new SimpleSchema({text: String, isPrivate: {type: Boolean, optional: true}})
自定义验证函数
¥Custom validate function
如果你未使用受支持的架构,则可以使用 validate
传入自定义验证函数。Note
:如果需要,validate
可以是异步函数。
¥If you're not using one of the supported schemas, you can use validate
to pass in a custom validation function. Note
: validate
can be an async function if needed.
js
// import your schema from somewhere
// import your validator function from somewhere
export const create = createMethod({
name: 'todos.create',
validate(args) {
validator(args, schema)
},
async run({ text }) {
const todo = {
text,
done: false,
createdAt: new Date(),
authorId: Meteor.userId() // can also use this.userId instead of Meteor.userId()
}
const todoId = await Todos.insertAsync(todo);
return todoId;
}
});
在客户端导入并使用
¥Import on the client and use
js
import { create } from '/imports/api/todos/methods';
async function submit() {
try {
await create({text: 'book flight to Hawaii'})
} catch(error) {
alert(error)
}
}
前后钩子
¥Before and after hooks
你可以执行方法的 run
函数中的 before
和/或 after
函数。before
和 after
接受单个函数或函数数组。
¥You can execute functions before
and / or after
the method's run
function. before
and after
accept a single function or an array of functions.
使用 before
时,传递给方法的原始输入将可用。原始输入将自动从 before
函数返回,以便 run
函数接收最初传递给该方法的内容。
¥When using before
, the original input passed into the method will be available. The original input will be returned automatically from a before
function so that run
receives what was originally passed into the method.
使用 before
的一个很好的用例是验证用户是否拥有权限。例如:
¥A great use case for using before
is to verify the user has permission. For example:
js
async function checkOwnership({ _id }) { // the original input passed into the method is available here. destructuring for _id since that's all we need for this function
const todo = await Todos.findOneAsync(_id);
if (todo.authorId !== Meteor.userId()) {
throw new Meteor.Error('not-authorized')
}
return true; // any code executed as a before function will automatically return the original input passed into the method so that they are available in the run function
}
export const markDone = createMethod({
name: 'todos.markDone',
schema: Todos.schema,
before: checkOwnership,
async run({ _id, done }) {
return await Todos.updateAsync(_id, {$set: {done}});
}
});
使用 after
时,run
函数的结果将作为第一个参数可用,第二个参数将包含传递给方法的原始输入。run
函数的结果将自动从 after
函数返回。
¥When using after
, the result of the run
function will be available as the first argument and the second argument will contain the original input that was passed into the method. The result of the run
function will be automatically returned from an after
function.
js
function exampleAfter(result, context) {
const { originalInput } = context; // the name of the method is also available here
// do stuff
return 'success'; // any code executed as an after function will automatically return the result of the run function
}
export const markDone = createMethod({
name: 'todos.markDone',
schema: Todos.schema,
before: checkOwnership,
async run({ _id, done }) {
return await Todos.updateAsync(_id, {$set: {done}});
},
after: exampleAfter
});
Note
:如果你对 before
、run
或 after
使用箭头函数,你将无法访问 this
(即方法调用)。你可能愿意牺牲这一点,因为 this.userId
可以用 Meteor.userId()
替换,this.isSimulation
可以用 Meteor.isClient
替换,但值得注意的是。
¥Note
: If you use arrow functions for before
, run
, or after
, you'll lose access to this
– the methodInvocation. You may be willing to sacrifice that because this.userId
can be replaced by Meteor.userId()
and this.isSimulation
can be replaced by Meteor.isClient
but it's worth noting.
管道一系列函数
¥Pipe a series of functions
你可以使用 .pipe
编写函数,而不是使用 run
。每个函数的输出将可用作下一个函数的输入。
¥Instead of using run
, you can compose functions using .pipe
. Each function's output will be available as an input for the next function.
js
// you'd define the functions in the pipe and then place them in the order you'd like them to execute within .pipe
// be sure that each function in the pipe returns what the next one expects, otherwise you can add an arrow function to the pipe to massage the data, e.g. (input) => manipulate(input)
export const create = createMethod({
name: 'todos.create',
schema: Todos.schema
}).pipe(
checkOwnership,
createTodo,
sendNotification
)
将方法附加到其集合(可选)
¥Attach methods to its Collection (optional)
你可以将每个方法附加到集合中,而不是导入它们。如果你使用 jam:easy-schema,请确保在附加方法之前附加架构。
¥Instead of importing each method, you can attach them to the Collection. If you're using jam:easy-schema be sure to attach the schema before attaching the methods.
js
// /imports/api/todos/collection
import { Mongo } from 'meteor/mongo';
import { schema } from './schema';
export const Todos = new Mongo.Collection('todos');
Todos.attachSchema(schema); // if you're using jam:easy-schema
const attach = async () => {
const methods = await import('./methods.js') // dynamic import is recommended
return Todos.attachMethods(methods); // if you prefer not to use dynamic import, you can simply call attachMethods synchronously
};
attach().catch(error => console.error('Error attaching methods', error))
attachMethods
接受方法 options
作为可选的第二个参数。请参阅 配置 获取 options
的列表。
¥attachMethods
accepts the method options
as an optional second parameter. See Configuring for a list of the options
.
使用附加的方法,你将在客户端上像这样使用它们:
¥With the methods attached you'll use them like this on the client:
js
import { Todos } from '/imports/api/todos/collection';
// no need to import each method individually
async function submit() {
try {
await Todos.create({text: 'book flight to Hawaii'})
} catch(error) {
alert(error)
}
}
仅在服务器上执行代码
¥Executing code on the server only
默认情况下,方法是乐观的,这意味着它们将在客户端执行,然后在服务器上执行。如果方法中只有一部分应该在服务器上执行而不是在客户端执行,那么只需将该部分代码封装在 if (Meteor.isServer)
块中即可。这样,你仍然可以保留 Optimistic UI 的优势。例如:
¥By default, methods are optimistic meaning they will execute on the client and then on the server. If there's only part of the method that should execute on the server and not on the client, then simply wrap that piece of code in a if (Meteor.isServer)
block. This way you can still maintain the benefits of Optimistic UI. For example:
js
export const create = createMethod({
name: 'todos.create',
schema: Todos.schema,
async run(args) {
// rest of your function
// code running on both client and server
if (Meteor.isServer) {
// code running on the server only
import { secretCode } from '/server/secretCode'; // since it's in a /server folder the code will not be shipped to the client
// do something with secretCode
}
// code running on both client and server
return Todos.insertAsync(todo)
}
});
如果你愿意,可以通过设置 serverOnly: true
强制整个方法仅在服务器上执行。它可以与 run
或 .pipe
一起使用。以下是 run
的示例:
¥If you prefer, you can force the entire method to execute on the server only by setting serverOnly: true
. It can be used with run
or .pipe
. Here's an example with run
:
js
export const create = createMethod({
name: 'todos.create',
schema: Todos.schema,
serverOnly: true,
async run(args) {
// all code here will execute only on the server
}
});
你还可以将所有方法设置为 serverOnly
。请参阅下方 配置。
¥You can also set all methods to be serverOnly
. See Configuring below.
安全说明
¥Security note
Important
:由于 Meteor 目前不支持 tree shake,即使你使用 if (Meteor.isServer)
或 serverOnly: true
,run
函数或 .pipe
函数内的代码内容仍然可能对客户端可见。为了防止这种情况,你可以使用以下选项:
¥Important
: Since Meteor does not currently support tree shaking, the contents of the code inside run
function or .pipe
could still be visible to the client even if you use if (Meteor.isServer)
or serverOnly: true
. To prevent this, you have these options:
使用动态导入将方法附加到其集合,如上所示 将方法附加到其集合(可选)
¥Attach methods to its Collection with a dynamic import as shown above Attach methods to its Collection (optional)
从
/server
文件夹中的文件导入函数。从/server
文件夹导入的任何代码都不会发送到客户端。/server
文件夹可以位于项目文件结构中的任何位置,并且可以有多个/server
文件夹。例如,你可以将其与你的集合文件夹(例如/imports/api/todos/server/
)放在同一位置,也可以将其放在项目的根目录下。有关更多信息,请参阅 Secret 服务器代码。¥Import function(s) from a file within a
/server
folder. Any code imported from a/server
folder will not be shipped to the client. The/server
folder can be located anywhere within your project's file structure and you can have multiple/server
folders. For example, you can co-locate with your collection folder, e.g./imports/api/todos/server/
, or it can be at the root of your project. See Secret server code for more info.
js
export const create = createMethod({
name: 'todos.create',
schema: Todos.schema,
serverOnly: true,
async run(args) {
import { serverFunction } from '/server/serverFunction';
serverFunction(args);
}
});
动态导入函数。这些不必位于
/server
文件夹中。这将阻止通过浏览器控制台检查代码。¥Dynamically import function(s). These do not have to be inside a
/server
folder. This will prevent the code being inspectable via the browser console.
js
export const create = createMethod({
name: 'todos.create',
schema: Todos.schema,
serverOnly: true,
async run(args) {
const { serviceFunction } = await import('./services');
serviceFunction(args);
}
});
更改身份验证规则
¥Changing authentication rules
默认情况下,所有方法都将受到身份验证保护,这意味着如果没有登录用户,它们将抛出错误。你可以通过设置 open: true
来更改单个方法的设置。请参阅下面的 配置 以更改所有方法的设置。
¥By default, all methods will be protected by authentication, meaning they will throw an error if there is not a logged-in user. You can change this for an individual method by setting open: true
. See Configuring below to change it for all methods.
js
export const create = createMethod({
name: 'todos.create',
schema: Todos.schema,
open: true,
async run({ text }) {
// ... //
}
});
速率限制
¥Rate limiting
通过设置方法在给定时间段(毫秒)内的最大请求数(limit
) - interval
,轻松实现速率限制。
¥Easily rate limit a method by setting its max number of requests – the limit
– within a given time period (milliseconds) – the interval
.
js
export const create = createMethod({
name: 'todos.create',
schema: Todos.schema,
rateLimit: { // rate limit to a max of 5 requests every second
limit: 5,
interval: 1000
},
async run({ text }) {
// ... //
}
});
不执行方法的验证
¥Validate without executing the method
有时你可能希望在不执行该方法的情况下进行验证。在这些情况下,你可以使用 .validate
。如果你只想验证部分架构,请使用 .validate.only
。
¥There may be occassions where you want to validate without executing the method. In these cases, you can use .validate
. If you want to validate against only part of the schema, use .validate.only
.
js
export const create = createMethod({
name: 'todos.create',
schema: Todos.schema,
async run({ text }) {
// ... //
}
});
// validate against the schema without executing the method
create.validate({...})
// validate against only the relevant part of the schema based on the data passed in without executing the method
create.validate.only({...})
如果你使用的是自定义验证函数而不是受支持的模式之一,并且想要使用 .validate.only
,你只需将 only
函数附加到你提供的 validate
函数上即可。
¥If you're using a custom validate function instead of one of the supported schemas and you'd like to use .validate.only
, you can simply append an only
function onto the validate
function that you supply.
Meteor.applyAsync 选项
¥Options for Meteor.applyAsync
调用时,该方法会在后台使用 Meteor.applyAsync 执行你的 run
函数或 .pipe
函数。Meteor.applyAsync
接受一些选项,这些选项可用于更改 Meteor 处理该方法的方式。如果你要更改默认值或包含其他受支持的选项,请在创建方法时传入 options
。
¥When called, the method uses Meteor.applyAsync under the hood to execute your run
function or .pipe
function(s). Meteor.applyAsync
takes a few options which can be used to alter the way Meteor handles the method. If you want to change the defaults or include other supported options, pass in options
when creating the method.
js
export const create = createMethod({
name: 'todos.create',
schema: Todos.schema,
options: {
// ... //
},
async run({ text }) {
// ... //
}
});
默认情况下,此包使用以下 options
:
¥By default, this package uses the following options
:
js
{
// Make it possible to get the ID of an inserted item
returnStubValue: true,
// Don't call the server method if the client stub throws an error, so that we don't end
// up doing validations twice
throwStubExceptions: true,
};
请参阅下面的 配置 以设置所有方法的 options
。
¥See Configuring below to set options
for all methods.
使用存根结果 (Meteor 3.0+)
¥Working with the stub result (Meteor 3.0+)
在 Meteor 3.0+ 版本中,你可以选择对存根结果(即在客户端运行方法模拟时,在服务器返回最终结果或错误之前的结果)采取行动。当你希望让用户感觉应用即时执行,并且你相对确定操作会成功时(例如,将新文档插入数据库时),这会非常方便。
¥In Meteor 3.0+, you can optionally take action with the stub result, i.e. the result when the method simulation is run on the client, before the server has returned with the final result or error. This can come in handy when you want to make your app feel instant for the user and you're relatively sure the action will succeed, e.g. when inserting new documents into the database.
js
const { stubPromise, serverPromise } = create();
const _id = await stubPromise.catch(error => {
// optionally handle a stub error
});
// take action with the _id stub result, for example, route to a new page
router.go(`/detail/${_id}`)
await serverPromise.catch(error => {
// handle server error, rollback changes as needed, for example route to home
router.go('/')
alert('sorry, could not create')
});
模拟方法上下文
¥Mocking the method context
你可以通过使用 .call(context, args)
调用方法来模拟方法调用上下文,也就是方法内部的 this
值。这对于单元测试模拟 this.userId
特别有用:
¥You can mock the method invocation context, aka the this
value inside the method, by invoking the method with .call(context, args)
. This is particularly useful for unit testing to mock the this.userId
:
js
const context = {
userId: 'fakeUserId',
// ... //
}
await create.call(context, {...})
配置(可选)
¥Configuring (optional)
如果你喜欢默认设置,则无需配置任何内容。但使用此包的方式有一定的灵活性。
¥If you like the defaults, then you won't need to configure anything. But there is some flexibility in how you use this package.
以下是全局默认值:
¥Here are the global defaults:
js
const config = {
before: [], // global before function(s) that will run before all methods
after: [], // global after function(s) that will run after all methods
serverOnly: false, // globally make all methods serverOnly, aka disable Optimistic UI, by setting to true
open: false, // by default all methods will be protected by authentication, override it for all methods by setting this to true
loggedOutError: new Meteor.Error('logged-out', 'You must be logged in'), // customize the logged out error
options: {
returnStubValue: true, // make it possible to get the ID of an inserted item on the client before the server finishes
throwStubExceptions: true, // don't call the server method if the client stub throws an error, so that we don't end up doing validations twice
}
};
要更改全局默认值,请使用:
¥To change the global defaults, use:
js
// put this in a file that's imported on both the client and server
import { Methods } from 'meteor/jam:method';
Methods.configure({
// ... change the defaults here ... //
});
全局 before 和 after 钩子
¥Global before and after hooks
你可以创建 before 和/或 after 函数,在所有方法之前/之后运行。before
和 after
都接受单个函数或函数数组。
¥You can create before and/or after functions to run before / after all methods. Both before
and after
accept a single function or an array of functions.
js
import { Methods } from 'meteor/jam:method';
const hello = () => { console.log('hello') }
const there = () => { console.log('there') }
const world = () => { console.log('world') }
Methods.configure({
before: [hello, there],
after: world
});
用于记录方法的实用函数
¥Helpful utility function to log your methods
以下是一个实用函数 - log
- 你可以考虑添加。它不包含在这个包中,但你可以将其复制粘贴到你认为合适的代码库中。
¥Here's a helpful utility function - log
- that you might consider adding. It isn't included in this package but you can copy and paste it into your codebase where you see fit.
js
// log will simply console.log or console.error when the Method finishes
function log(input, pipeline) {
pipeline.onResult((result) => {
console.log(`Method ${pipeline.name} finished`, input);
console.log('Result', result);
});
pipeline.onError((err) => {
console.error(`Method ${pipeline.name} failed`);
console.error('Error', err);
});
};
然后,你可以像这样使用它:
¥Then you could use it like this:
js
import { Methods, server } from 'meteor/jam:method';
Methods.configure({
after: server(log)
});
替代函数式语法
¥Alternative functional-style syntax
如果你愿意,可以使用函数式语法来编写你的方法。以下是一个例子。
¥You can use a functional-style syntax to compose your methods if you prefer. Here's an example.
js
const fetchGifs = async({ searchTerm, limit }) => {...}
export const getGifs = createMethod(server(schema({ searchTerm: String, limit: Number })(fetchGifs)))
getGifs
可从客户端调用,但只能在服务器上运行。在内部,它将被标识为 fetchGifs
¥getGifs
is callable from the client but will only run on the server. Internally it will be identified as fetchGifs
Note
:如果你将一个命名函数传入 createMethod
,那么该函数将在内部用于标识该方法。否则,如果你传入匿名函数,jam:method
会根据其架构生成一个唯一名称,以便在内部进行标识。
¥Note
: if you pass in a named function into createMethod
, then that will be used to identify the method internally. Otherwise if you pass in an anonymous function, jam:method
generates a unique name based on its schema to identify it internally.
使用函数式语法自定义方法
¥Customizing methods when using functional-style syntax
当你需要自定义方法时,可以使用一些函数:schema
, server
, open
, close
.这些可以在需要时组合。
¥There are a few functions available when you need to customize the method: schema
, server
, open
, close
. These can be composed when needed.
schema
指定要验证的架构。
¥Specify the schema to validate against.
js
import { schema } from 'meteor/jam:method';
export const doSomething = schema({thing: String, num: Number})(async ({ thing, num }) => {
// ... //
});
server
使该方法仅在服务器上运行。
¥Make the method run on the server only.
js
import { server } from 'meteor/jam:method';
export const aServerOnlyMethod = server(async data => {
// ... //
});
open
使该方法公开可用,以便不需要用户登录。
¥Make the method publically available so that a logged-in user isn't required.
js
import { open } from 'meteor/jam:method';
export const aPublicMethod = open(async data => {
// ... //
});
close
使该方法检查用户是否已登录。
¥Make the method check for a logged-in user.
Note
:默认情况下,所有方法都需要登录用户,因此如果你坚持使用默认设置,则无需使用此功能。参见 配置。
¥Note
: by default, all methods require a logged-in user so if you stick with that default, then you won't need to use this function. See Configuring.
js
import { close } from 'meteor/jam:method';
export const closedMethod = close(async data => {
// ... //
});
与 jam:easy-schema
一起使用
¥Using with jam:easy-schema
jam:method
与 jam:easy-schema
集成,并提供了一种减少样板代码并使你的方法更易于编写的方法(尽管你仍然可以根据需要使用 createMethod
)。
¥jam:method
integrates with jam:easy-schema
and offers a way to reduce boilerplate and make your methods even easier to write (though you can still use createMethod
if you prefer).
例如,与其这样写:
¥For example, instead of writing this:
js
export const setDone = createMethod({
name: 'todos.setDone',
schema: Todos.schema,
before: checkOwnership,
async run({ _id, done }) {
return Todos.updateAsync({ _id }, { $set: { done } });
}
});
你可以这样写:
¥You can write:
js
export const setDone = async ({ _id, done }) => {
await checkOwnership({ _id });
return Todos.updateAsync({ _id }, { $set: { done } });
};
Note
:这假设你将方法附加到其集合中。参见 将方法附加到其集合。
¥Note
: This assumes that you're attaching your methods to its collection. See Attach methods to its Collection.
当你从客户端调用 Todos.setDone
时,参数将自动根据 Todos.schema
进行检查。该方法将在内部自动命名为 todos.setDone
,以便在应用性能监控 (APM) 中识别它。
¥When you call Todos.setDone
from the client, the arguments will be automatically checked against the Todos.schema
. The method will automatically be named todos.setDone
internally to identify it for app performance monitoring (APM) purposes.
你还可以使用 函数式语法 中提供的功能进行组合。例如:
¥You can also compose with the functions available in the function-style syntax. For example:
js
export const setDone = server(async ({ _id, done }) => {
await checkOwnership({ _id });
return Todos.updateAsync({ _id }, { $set: { done } });
});
现在,当你从客户端调用 Todos.setDone
时,它只会在服务器上运行。
¥Now when you call Todos.setDone
from the client it will only run on the server.
与 jam:offline
一起使用
¥Using with jam:offline
jam:method
与 jam:offline
集成,可在用户离线时自动将方法排队。你无需为此在 jam:method
中进行任何配置。🎉 然后,当用户重新连接时,jam:offline
将重放这些操作。有关更多详细信息,请参阅 jam:offline。
¥jam:method
integrates with jam:offline
to automatically queue methods when a user is offline. You don't need to configure anything in jam:method
for this. 🎉 jam:offline
will then replay them when the user reconnects. See jam:offline for more details.
来自 Validated Method
?
¥Coming from Validated Method
?
你可能熟悉 mixins
,并且想知道它们在哪里。使用此软件包的功能 - 默认已验证,before
/after
钩子,.pipe
- 你的 mixin 代码可能不再需要,或者可以简化。如果你有其他 Mixin 无法翻译的用例,我很乐意听取你的意见。打开新的讨论,我们来聊聊。
¥You may be familiar with mixins
and wondering where they are. With the features of this package - authenticated by default, before
/ after
hooks, .pipe
- your mixin code may no longer be needed or can be simplified. If you have another use case where your mixin doesn't translate, I'd love to hear it. Open a new discussion and let's chat.