# 管理结构

¥Managing schemas

# 复用校验函数

¥Re-using validation functions

当模式编译仅发生一次而校验发生多次时,Ajv 校验模型针对服务器端执行进行了优化 - 与在校验过程中解释模式的校验器相比,这具有显着的性能优势。

¥Ajv validation model is optimized for server side execution, when schema compilation happens only once and validation happens multiple times - this has a substantial performance benefit comparing with validators that interpret the schema in the process of validation.

从 Ajv v6 中基于模板的代码生成过渡到 v7 中基于树的代码生成带来了:

¥Transition from template-based code generation in Ajv v6 to the tree-based in v7 brought:

  • 类型级安全性,防止通过不受信任的结构注入代码

    ¥type-level safety against code injection via untrusted schemas

  • 更高效的校验代码(通过 树优化

    ¥more efficient validation code (via tree optimizations)

  • 编译函数的内存占用更小(结构不再序列化)

    ¥smaller memory footprint of compiled functions (schemas are no longer serialized)

  • 更小的打包尺寸

    ¥smaller bundle size

  • 更易于维护的代码

    ¥more maintainable code

这些改进会降低结构编译的速度,并增加在传递不同结构对象时重新编译的机会(请参阅 #1413 (opens new window)),因此正确管理结构非常重要,因此它们仅编译一次。

¥These improvements cost slower schema compilation, and increased chance of re-compilation in case you pass a different schema object (see #1413 (opens new window)), so it is very important to manage schemas correctly, so they are only compiled once.

有多种方法可以管理已编译的结构。

¥There are several approaches to manage compiled schemas.

# 独立校验代码

¥Standalone validation code

预编译结构的动机:

¥The motivation to pre-compile schemas:

  • 更快的启动时间

    ¥faster startup times

  • 更低的内存占用/包大小

    ¥lower memory footprint/bundle size

  • 与严格的内容安全策略兼容

    ¥compatible with strict content security policies

  • 多次编译结构几乎没有风险

    ¥almost no risk to compile schema more than once

  • 更适合短期环境

    ¥better for short-lived environments

详情请参见 独立校验代码

¥See Standalone validation code for the details.

在某些情况下这是不可能或困难的:

¥There are scenarios when it can be not possible or difficult:

  • dynamic or user-provided schemas - 虽然你可以进行缓存,但它可能难以实现或效率低下。

    ¥dynamic or user-provided schemas - while you can do caching, it can be either difficult to implement or inefficient.

  • 使用难以序列化为代码的闭包的用户定义关键字。

    ¥user-defined keywords that use closures that are difficult to serialize as code.

# 初始化期间编译

¥Compiling during initialization

最简单的方法是在应用启动时在处理请求的代码之外编译所有结构。可以简单地在模块作用域内完成:

¥The simplest approach is to compile all your schemas when the application starts, outside of the code that handles requests. It can be done simply in the module scope:

使用单个 Ajv 实例

它建议对整个应用使用单个 Ajv 实例,因此如果你在多个模块中使用校验,你应该:

¥It recommended to use a single Ajv instance for the whole application, so if you use validation in more than one module, you should:

  • 需要 Ajv 在一个单独的模块中负责校验

    ¥require Ajv in a separate module responsible for validation

  • 在那里编译所有校验器

    ¥compile all validators there

  • 导出它们以在应用的多个模块中使用

    ¥export them to be used in multiple modules of your application

# 使用 Ajv 实例缓存

¥Using Ajv instance cache

另一种更有效的方法是使用 Ajv 实例缓存,通过一次导入使所有已编译的校验器在应用中的任何位置可用。

¥Another, more effective approach, is to use Ajv instance cache to have all compiled validators available anywhere in your application from a single import.

在这种情况下,你将有一个单独的模块,你可以在其中实例化 Ajv 并在应用中使用此实例。

¥In this case you would have a separate module where you instantiate Ajv and use this instance in your application.

你可以加载所有结构并将它们添加到单个 validation 模块中的 Ajv 实例:

¥You can load all schemas and add them to Ajv instance in a single validation module:

然后你可以导入 Ajv 实例并访问任何应用模块中的任何结构,例如 user 模块:

¥And then you can import Ajv instance and access any schema in any application module, for example user module:

按需编译与初步编译

在上面的示例中,结构编译仅在第一次 API 调用时发生一次,而不是在应用启动时发生。这意味着应用的启动速度会更快一些,但第一个 API 调用会慢一些。如果这是不可取的,例如,你可以在添加所有添加的结构后调用 getSchema,然后当在路由处理程序内部调用 getSchema 时,它只会从实例缓存中获取已编译的校验函数。

¥In the example above, schema compilation happens only once, on the first API call, not at the application start-up time. It means that the application would start a bit faster, but the first API call would be a bit slower. If this is undesirable, you could, for example, call getSchema for all added schemas after they are added, then when getSchema is called inside route handler it would simply get compiled validation function from the instance cache.

# 缓存键:schema vs key vs $id

¥Cache key: schema vs key vs $id

在上面的示例中,传递给 addSchema 方法的键用于从缓存中检索结构。其他选项有:

¥In the example above, the key passed to the addSchema method was used to retrieve schemas from the cache. Other options are:

  • 使用结构根 $id 属性。虽然它通常看起来像 URI,但这并不意味着 Ajv 从此 URI 下载它 - 这只是用于识别和访问模式的 $id。不过,你可以配置 Ajv 以按需下载模式 - 参见 异步结构加载

    ¥use schema root $id attribute. While it usually looks like URI, it does not mean Ajv downloads it from this URI - this is simply $id used to identify and access the schema. You can though configure Ajv to download schemas on demand - see Asynchronous schema loading

  • 使用结构对象本身作为缓存的键(这是可能的,因为 Ajv 使用 Map)。不建议使用此方法,因为仅当你传递与传递给 addSchema 方法的模式对象相同的实例时,它才有效 - 很容易犯错误,导致每次使用模式时都会对其进行编译。

    ¥use schema object itself as a key to the cache (it is possible, because Ajv uses Map). This approach is not recommended, because it would only work if you pass the same instance of the schema object that was passed to addSchema method - it is easy to make a mistake that would result in schema being compiled every time it is used.

# 预添加所有结构与按需添加

¥Pre-adding all schemas vs adding on demand

在上面的示例中,所有结构都是提前添加的。也可以在使用时添加模式 - 如果有许多模式,它会很有帮助。在这种情况下,你需要首先通过调用 getSchema 方法检查模式是否已添加 - 如果不是,它将返回 undefined

¥In the example above all schemas were added in advance. It is also possible, to add schemas as they are used - it can be helpful if there are many schemas. In this case, you need to check first whether the schema is already added by calling getSchema method - it would return undefined if not:

const schema_user = require("./schema_user.json")
let validate = ajv.getSchema("user")
if (!validate) {
  ajv.addSchema(schema_user, "user")
  validate = ajv.getSchema("user")
}

如果你的结构具有 $id 属性,例如:

¥If your schema has $id attribute, for example:

那么上面的逻辑就可以更简单:

¥then the above logic can be simpler:

const schema_user = require("./schema_user.json")
const validate = ajv.getSchema("https://example.com/user.json")
              || ajv.compile(schema_user)

上述情况是可能的,因为当结构具有 $id 属性时,compile 方法会编译结构(返回校验函数)并同时将其添加到 Ajv 实例缓存中。

¥The above is possible because when the schema has $id attribute compile method both compiles the schema (returning the validation function) and adds it to the Ajv instance cache at the same time.

# 异步结构加载

¥Asynchronous schema loading

在某些情况下,你需要将大量结构存储在某个数据库或远程服务器上。在这种情况下,你可能会使用模式 $id 作为某些资源标识符来检索它 - 网络 URI 或数据库 ID。

¥There are cases when you need to have a large collection of schemas stored in some database or on the remote server. In this case you are likely to use schema $id as some resource identifier to retrieve it - either network URI or database ID.

你可以使用 compileAsync method 在编译结构时异步加载结构,按需加载从已编译结构引用的结构。Ajv 本身不执行任何 IO 操作,它使用你通过 loadSchema option 提供的函数从传递的 ID 加载结构。此函数应返回解析为结构的 Promise(你可以使用异步函数,如示例中所示)。

¥You can use compileAsync method to asynchronously load the schemas as they are compiled, loading the schemas that are referenced from compiled schemas on demand. Ajv itself does not do any IO operations, it uses the function you supply via loadSchema option to load schema from the passed ID. This function should return Promise that resolves to the schema (you can use async function, as in the example).

示例:

¥Example:

const ajv = new Ajv({loadSchema: loadSchema})

ajv.compileAsync(schema).then(function (validate) {
  const valid = validate(data)
  // ...
})

async function loadSchema(uri) {
  const res = await request.json(uri)
  if (res.statusCode >= 400) throw new Error("Loading error: " + res.statusCode)
  return res.body
}

# 在代码中缓存结构

¥Caching schemas in your code

你可以独立于 Ajv 维护应用中已编译结构的缓存。当你有多个 Ajv 实例时,它会很有帮助,例如:

¥You can maintain cache of compiled schemas in your application independently from Ajv. It can be helpful in cases when you have multiple Ajv instances because, for example:

  • 你需要使用不同的选项编译不同的结构

    ¥you need to compile different schemas with different options

  • 你在一个应用中同时使用 JSON 结构和 JSON 类型定义结构

    ¥you use both JSON Schema and JSON Type Definition schemas in one application

  • 你无法控制的不同第三方结构之间存在 $id 冲突

    ¥you have $id conflicts between different third party schemas you do not control

无论你使用什么方法,你都需要确保每个结构仅编译一次。

¥Whatever approach you use, you need to ensure that each schema is compiled only once.