大家好,欢迎来到IT知识分享网。
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.", }); });
这就会检查 dependentField1
和 dependentField2
是否同时为 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 = "."; res.dependentField2 = "."; 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.0000004 > (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 是个比较方便的设置
我目前还没有找到特别好的能够解决 test
和 when
里的报错信息,可能说最终只会写一些其他的函数用来解决这个问题吧
免责声明:本站所有文章内容,图片,视频等均是来源于用户投稿和互联网及文摘转载整编而成,不代表本站观点,不承担相关法律责任。其著作权各归其原作者或其出版社所有。如发现本站有涉嫌抄袭侵权/违法违规的内容,侵犯到您的权益,请在线联系站长,一经查实,本站将立刻删除。 本文来自网络,若有侵权,请联系删除,如若转载,请注明出处:https://haidsoft.com/140339.html