Skip to content

定义一个规则

我们一起来学习如何定义规则来解决实际问题。比如需求:在下单页面,根据所选地址或物流方式来计算运费。

ts
const Order = typedef({
    address: typedef({
        id: string,
    }),
    express: typedef({
        code: string,
    }),
    freight: money,
})

const PlaceOrderPage = typedef({
    order: Order,
})

ruledef(
    PlaceOrderPage,
    'calcFreight', // 规则名
    { // 观察字段
        order: {
            '@or': true,
            address: {
                id: true,
            },
            express: {
                code: true,
            },
        },
    },
    async (self) => {
        self.order.freight = await Api.calcFreight({
            address_id: self.order.address.id,
            express_code: self.order.express.code,
        })
    }
)

// 初始化页面
const placeOrderPage = typeinit(PlaceOrderPage)
// 切换地址或物流
placeOrderPage.order.address.id = 'xxx'
placeOrderPage.order.express.code = 'xxx'

看上面高亮部分,我们深入并精确地观察到了全部目标字段。无论是切换地址还是切换物流,计算运费的规则都会执行,简单直接、安全可靠。 观察字段就是规则被触发的条件,一旦字段更新,就会自动触发规则。

描述符号

在观察字段中,我们使用了一个特殊的描述符号@or来描述多个字段之间是或者关系。任何一个字段更新,都会满足该层条件,并向上层冒泡,直到顶层也满足条件就会触发规则。若没有@or符号,则同层所有字段都更新才算满足条件。


仔细看,上面的代码有一个可以优化的地方如:当选择相同地址或相同物流时,也会触发计算运费的规则。我们来改进它,如下:

ts
            address: {
                id: true, 
                id: { '@diff': true }, 
            },
            express: {
                code: true, 
                code: { '@diff': true }, 
            },

我们用了一个新描述符号@diff,这样只有当字段更新为新值时,才会触发规则。选择相同地址或物流时,就不会重复计算运费了。


有时候字段可能为空(undefined,null),我们不希望触发规则。可以使用描述符号@notnil来解决。

在规则中使用递归

严格来讲,是不能使用递归写法的,但是我们可以变通一下。比如:若商品限购,如何控制购物车的商品数量。

ts
const Product = typedef({
    id: string,
    maxQuantity: int32,
})

const CartProduct = typedef({
    id: string,
    quantity: int32,
    product: Product,
})

ruledef(
    CartProduct,
    'quantity',
    {
        quantity: true,
    },
    (self) => {
        let { quantity } = self
        const { maxQuantity } = self.product
        if (maxQuantity && quantity > maxQuantity) {
            quantity = maxQuantity
        }
        if (quantity !== self.quantity) {
            setTimeout(() => {
                self.quantity = quantity
            })
        }
    }
)

由于结构体都是开放的、纯粹的,所以无法在赋值时进行拦截。不过我们可以通过定义一个规则来观察某个字段并修正自己。这就导致了递归,因为递归可能会导致堆栈溢出,所以我们巧妙地借助setTimeout来放空当前堆栈,在下一个调用帧来修正自己。

两个规则互相触发

有这么一种情况,规则A会触发规则B,规则B会触发规则A,陷入循环。怎么办呢?如果两个规则不互相触发就好了。

ts
enum OrderType {
    normal = 1,
    activity = 2,
    // ...
}

const Order = typedef({
    type: string as TypeDesc<keyof typeof OrderType>,
    typeCode: int32 as TypeDesc<OrderType>,
})

ruledef(
    Order,
    'type',
    {
        type: true,
    },
    (self) => {
        // if (self.typeCode !== OrderType[self.type]) {
        //     self.typeCode = OrderType[self.type]
        // }
        // 同上面
        self.typeCode = wrapval({ '@diff': true }, OrderType[self.type])
    }
)

ruledef(
    Order,
    'typeCode',
    {
        typeCode: true,
    },
    (self) => {
        self.type = wrapval({ '@diff': true }, OrderType[self.typeCode])
    }
)

没错,只有判断为新值时才更新字段,这样就能保证所有规则同时只触发一次,不会循环。为了方便,我们提供了一种新写法wrapval({ '@diff': true }, value)

TIP

wrapval是个新颖的特性,允许我们使用描述来包装一个值,这样就可以在赋值时提供灵活的控制力,能够实现奇妙的操作。

MIT Licensed