yup 使用 2 - 获取默认值,循环依赖,超大数字验证,本地化

yup 使用 2 - 获取默认值,循环依赖,超大数字验证,本地化

上一篇的使用在这里:yup 基础使用以及 jest 测试,这篇讲的是比较基础的东西,

获取默认值

之前用的都是 cast({}),然后如果有些值是必须的,又没有提供默认值,yup 就会抛出异常。另一种可以直接获取默认值而不抛出异常的方式,可以使用内置的 getDefault()

const res = demoSchema.getDefault();

类型异常

如果使用 JavaScript 应该没什么问题,但是如果使用 TypeScript 的话,可能会抛出如下的异常:

在这里插入图片描述

这是因为导出的数据类型使用的是 Infer,schema 中没有定义默认值,因此就会出现 undefined 和字符串不匹配的情况,这种解决的方式可以通过重写 default 来实现:

export let demoSchema = object({
  // ...
  enumField: string()
    .required()
    .default(() => undefined as undefined | string)
    .oneOf(Object.keys(getTestEnum() || [])),
});

大多数情况下这应该不会有什么问题,只有在直接获取默认值并需要重新赋值的时候需要注意

循环依赖

这也是我们项目里存在的一个比较罕见的案例,就是需要同时检查 A 和 B

正常情况下,如果直接使用下面的实现,则会抛出异常:

export let demoSchema = object({
  // ...
  dependentField1: number()
    .required()
    .when("dependentField2", ([dependentField2], schema) => {
      return schema;
    }),
  dependentField2: number()
    .required()
    .when("dependentField1", ([dependentField1], schema) => {
      return schema;
    }),
});

在这里插入图片描述

造成这个错误的原因是,当 dependentField2 需要验证的时候,它需要去找 dependentField1 的值,而 dependentField1 又需要对 dependentField2 进行判断……

解决的方式可以使用 shape(),并且将依赖作为 dependency array 放到第二个参数中:

export let demoSchema = object().shape(
  {
    // ...
    dependentField1: number()
      .default(0)
      .required()
      .when("dependentField2", ([dependentField2], schema) => {
        return schema;
      }),
    dependentField2: number()
      .default(0)
      .required()
      .when("dependentField1", ([dependentField1], schema) => {
        return schema;
      }),
  },
  [["dependentField1", "dependentField2"]]
);

具体实现的验证为:

number()
  .default(0)
  .required()
  .when("dependentField2", ([dependentField2], schema) => {
    return schema.test({
      name: "none-zero",
      test: (dependentField1) =>
        !(dependentField1 === 0 && dependentField2 === 0),
      message: "DependentField1 and DependentField2 cannot be both 0.",
    });
  });

这就会检查 dependentField1dependentField2 是否同时为 0:

在这里插入图片描述

⚠️:在查文档的时候,我看到的目前 yup 支持是两两相对的,因此 dep array 中只能放 [[a, b], [b, c], [c, a]] 这样的实现,而不能使用 [[a, b, c]] 这样的实现

👀:我看到一些其他、实际的使用案例为地址,如一旦输入了地址,那么就需要验证城市、省/直辖区和邮政编码,如果没有输入地址,则不需要进行验证

数字最大值问题

这个实际上不是 yup 的问题,而是 JavaScript 的问题,如 JS 中有一个 MAX_SAFE_INTEGER 值,并且进行转换后,就会失去精确值:

在这里插入图片描述

而且对于 JS 本身来说,任何超过 MAX_SAFE_INTEGER 的操作,都会导致丢失精确值,而这里的挑战是:

  • 一旦使用任何的数字,并且保存到 JavaScript 中,就像使用 parseFloat 这个案例,精确值直接就丢了
  • 如果使用 number,在调用 yup 的时候,yup 内部会使用类似 parseFloat 的实现,因此也会导致精度丢失

因为后端暂时还是只接受 decimal/long,所以我们目前的解决方式是用 decimal.js,这个库的实现方式类似于 Java 的 big decimal,所以只要不转成浮点数,而是用字符串,就能够保持我们项目需要的精度

util 实现

主要是重写一些 Decimal 内部的比较方法,因为 Decimal 不接受 undefined,所以不写 util 会导致报错

import Decimal from "decimal.js";

export const compareTo = (
  a: Decimal.Value | undefined,
  b: Decimal.Value | undefined
): number => new Decimal(a ?? 0).comparedTo(b ?? 0);

export const equalTo = (
  a: Decimal.Value | undefined,
  b: Decimal.Value | undefined
): boolean => new Decimal(a ?? 0).equals(b ?? 0);

更新 yup 验证

因为精确的关系,所以就需要把所有的数字转成字符串,并且手动重写验证,大体实现如下:

const MAX_DECIMAL = new Decimal(Number.MAX_SAFE_INTEGER).div(10 ** 6);

export let demoSchema = object().shape(
  {
    // ...
    dependentField1: string<string>()
      .default(() => "0" as string)
      .required()
      .test({
        name: "max-num",
        test: (dependentField1) => {
          return compareTo(dependentField1, MAX_DECIMAL) <= 0;
        },
        message: `DependentField1 must be smaller than or equal to ${MAX_DECIMAL}.`,
      }),
  },
  [["dependentField1", "dependentField2"]]
);

最后的验证如下:

在这里插入图片描述

这里对象的值为:

const res = demoSchema.getDefault();
res.dependentField1 = "9007199254.740991";
res.dependentField2 = "9007199254.740992";

demoSchema
  .validate(res, { abortEarly: false })
  .then((validatedRes) => console.log(validatedRes))
  .catch((e: ValidationError) => {
    e.inner.forEach((e) => {
      console.log(e.path, e.errors);
    });
  });

⚠️:max-num 的这个对象是可以改成一个函数,这样可以稍微减少一些代码:

export const maxNumTest = (
  fieldName: string,
  maxValue: number | Decimal.Value
) => ({
  name: "max-num",
  test: (value: any) => {
    return compareTo(value, maxValue) <= 0;
  },
  message: `${fieldName} must be smaller than or equal to ${maxValue}.`,
});

补充 - 精确值计算

这里主要就是 stack overflow 的解决方案:How can I deal with floating point number precision in JavaScript

大致运行是这样:

> var x = 0.1
> var y = 0.2
> var cf = 10
> x * y
0.020000000000000004
> (x * cf) * (y * cf) / (cf * cf)
0.02

里面提出的解决方式是:

var _cf = (function () {
  function _shift(x) {
    var parts = x.toString().split(".");
    return parts.length < 2 ? 1 : Math.pow(10, parts[1].length);
  }
  return function () {
    return Array.prototype.reduce.call(
      arguments,
      function (prev, next) {
        return prev === undefined || next === undefined
          ? undefined
          : Math.max(prev, _shift(next));
      },
      -Infinity
    );
  };
})();

Math.a = function () {
  var f = _cf.apply(null, arguments);
  if (f === undefined) return undefined;
  function cb(x, y, i, o) {
    return x + f * y;
  }
  return Array.prototype.reduce.call(arguments, cb, 0) / f;
};

Math.s = function (l, r) {
  var f = _cf(l, r);
  return (l * f - r * f) / f;
};

Math.m = function () {
  var f = _cf.apply(null, arguments);
  function cb(x, y, i, o) {
    return (x * f * (y * f)) / (f * f);
  }
  return Array.prototype.reduce.call(arguments, cb, 1);
};

Math.d = function (l, r) {
  var f = _cf(l, r);
  return (l * f) / (r * f);
};

我们内部使用的也是这个方式去计算还原,目前对于还原到 MAX_SAFE_INTEGER 来说问题不大……

setLocale

这是一个本地可以解决一些报错信息的方式,目前我找到的是内嵌的方法,如 required 这种,大致实现方式如下:

// 写在了另一个 const 文件里
export const FIELD_NAME: Record<string, string> = {
  description: "Description",
  enumField: "Dropdown Enum",
};

// 在 schema util 里的实现……或许放到 const 或者 i18 也行
setLocale({
  mixed: {
    required: ({ path }) => `${FIELD_NAME[path]} is a required field.`,
    oneOf: ({ path, values }) =>
      `${FIELD_NAME[path]} must have one of the following fields: ${values}.`,
  },
  string: {
    min: ({ path, min }) =>
      `${FIELD_NAME[path]} must be at least ${min} characters.`,
    max: ({ path, max }) =>
      `${FIELD_NAME[path]} must be at at most ${max} characters.`,
  },
});

这个 setLocale 只需要实现一次,所有的 schema 就会沿用这个设定,如:

在这里插入图片描述

做 i8 是个比较方便的设置

我目前还没有找到特别好的能够解决 testwhen 里的报错信息,可能说最终只会写一些其他的函数用来解决这个问题吧

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值