Skip to content

数据生成钩子

介绍

钩子函数就是在数据生成前后或数据生成中执行的函数,它能让你在数据生成前后或数据生成中操作数据项和模板,改变数据生成方式,DataFaker提供了四类钩子函数:

  • 数据生成前操作模板-beforeAllCbs
  • 数据生成后操作数据-afterAllCbs
  • 数据项生成前设置模板-beforeEachCbs
  • 数据项生成后操作数据-afterEachCbs

数据生成前操作模板-beforeAllCbs

基本概念

这个钩子函数在数据生成前执行,它会接受一个 schema 参数,这个schema就是数据生成的模板,你可以在这个钩子函数中对 schema 进行修改,比如添加或删除数据项,修改数据项的生成方式等。

全局定义

使用setHooks函数可以设置全局的钩子函数,全局的钩子函数会作用于所有生成的数据,如下所示:

ts
// 全局定义beforeAllCbs回调函数
DataFaker.setHooks({
  beforeAllCbs: (schema) => {
    console.log(schema);
    return schema;
  },
});
// 用户模型
const userModel = defineModel('user', {
  firstName: 'person.firstName',
  secondName: 'person.lastName',
  age: ['number.int', { min: 18, max: 65 }],
  email: (ctx) => {
    return faker.internet.email({ firstName: ctx.firstName, lastName: ctx.secondeName });
  },
  address: { refModel: 'address', count: 1 },
});
const userDatas = fakeData(userModel);
console.dir(userDatas, { depth: Infinity });

上面的案例中,我们定义了一个全局的beforeAllCbs回调函数,并且使用console.log(schema);打印出了schema,打印出的schema就是userModel的模板,如下所示:

ts
{
  firstName: 'person.firstName',
  secondName: 'person.lastName',
  age: ['number.int', { min: 18, max: 65 }],
  email: (ctx) => {
    return faker.internet.email({ firstName: ctx.firstName, lastName: ctx.secondeName });
  },
  address: { refModel: 'address', count: 1 },
}

所以你可以在数据生成前修改模板,比如我希望age属性固定为 18 岁,可以这样修改:

ts
DataFaker.setHooks({
  beforeAllCbs: (schema) => {
    schema.age = () => {
      return 18;
    };
    return schema;
  },
});

那么数据生成时就会依照新的模板项来生成数据

json
{
  "firstName": "Brooke",
  "secondName": "Brekke",
  "address": null,
  "age": 18,
  "email": "Brooke0@hotmail.com"
}

回调队列

beforeAllCbs这个项所接受的不仅仅是一个函数,还可以是一个数组,数组中的函数会按顺序执行,并且返回值会作为下一个函数的参数。

ts
DataFaker.setHooks({
  beforeAllCbs: [
    (schema) => {
      // age字段值固定为18而不是{ min: 18, max: 65 }
      schema.age = () => {
        return 18;
      };
      return schema;
    },
    (schema) => {
      // 添加一个hobby字段
      schema.hobby = ['helpers.arrayElements', ['篮球', '乒乓球']];
      return schema;
    },
  ],
});

如此,以后生成的数据age都会默认为 18 岁,并且会随机生成一个hobby字段,值为篮球乒乓球

注意

beforeAllCbs中的函数都需要返回schema,否则将会安装空schema来生成数据,导致生成数据为null

运行时定义

使用defineModel函数可以定义模型,defineModel函数的第二个参数可以接受一个hooks参数,这个参数可以定义模型的钩子函数,其定义方式与全局定义一致,如下所示:

ts
const userDatas = fakeData(userModel, {
  hooks: {
    beforeAllCbs: (schema) => {
      schema.age = () => {
        return 18;
      };
      return schema;
    },
    // 或数组
  },
});

钩子函数的合并

全局钩子会和运行时钩子函数进行合并,且运行时钩子会在全局钩子之前执行。

ts
DataFaker.setHooks({
  beforeAllCbs: (schema) => {
    schema.hobby = ['helpers.arrayElements', ['篮球', '乒乓球']];
    return schema;
  },
});
const addressModel = defineModel('address', {
  country: 'location.country',
  city: 'location.city',
  children: {
    refModel: 'address',
  },
});
// 用户模型
const userModel = defineModel('user', {
  firstName: 'person.firstName',
  secondName: 'person.lastName',
  age: ['number.int', { min: 18, max: 65 }],
  email: (ctx) => {
    return faker.internet.email({ firstName: ctx.firstName, lastName: ctx.secondeName });
  },
  address: { refModel: 'address', count: 1 },
});
const userDatas = fakeData(userModel, {
  hooks: {
    beforeAllCbs: (schema) => {
      schema.age = (ctx) => {
        return 18;
      };
      return schema;
    },
  },
});
console.dir(userDatas, { depth: Infinity });
ts
{
  // 全局钩子和运行时钩子合并
  beforeAllCbs: [ [Function: beforeAllCbs], [Function: beforeAllCbs] ],
  afterAllCbs: [],
  beforeEachCbs: [],
  afterEachCbs: []
}
json
{
  "firstName": "Oliver",
  "secondName": "Ankunding-Paucek",
  "address": {
    "country": "Sudan",
    "city": "Kiarahaven",
    "children": { "country": "Jamaica", "city": "Fort Suzannemouth", "children": null }
  },
  "hobby": ["乒乓球", "篮球"],
  "age": 18,
  "email": "Oliver_Daniel97@gmail.com"
}

上下文对象

在前面模板语法中讲到过,若定义为函数形式,可以接受一个上下文对象,该对象会包含前面已经生成的数据,同样当定义新的模板项时,如果用函数形式也可以接受一个上下文对象ctx

数据生成后操作数据-afterAllCbs

afterAllCbsbeforeAllCbs定义方式完全一致,只不过所接受的参数不同,afterAllCbs接受到的是生成后的数据,可以这么说:

beforeAllCbs是通过改变模板进而改变最终的数据形式,而afterAllCbs则是直接改变生成的数据进而改变最终的数据形式”

如下所示,我们为生成的数据添加email

ts
// 用户模型
const userModel = defineModel('user', {
  firstName: 'person.firstName',
  secondName: 'person.lastName',
  age: ['number.int', { min: 18, max: 65 }],
  address: { refModel: 'address', count: 1 },
});
const userDatas = fakeData(userModel, {
  hooks: {
    afterAllCbs(data) {
      console.log(data);
      return {
        email: faker.internet.email(),
        ...data,
      };
    },
  },
});
console.dir(userDatas, { depth: Infinity });
ts
{
  firstName: 'Darlene',
  secondName: 'Quigley',
  age: 35,
  address: {
    country: 'Macao',
    city: 'Corkeryville',
    children: { country: 'Papua New Guinea', city: 'Eudorafort', children: null }
  }
}
ts
{
  email: 'Reta71@yahoo.com',
  firstName: 'Darlene',
  secondName: 'Quigley',
  age: 35,
  address: {
    country: 'Macao',
    city: 'Corkeryville',
    children: { country: 'Papua New Guinea', city: 'Eudorafort', children: null }
  }
}

数据项生成前设置模板-beforeEachCbs

基本概念

beforeEachCbs这个钩子函数与beforeAllCbs最大的不同在于,它会在每个单个数据项生成前执行(即对每个数据项执行一次),它会接受一个 ctx 参数,这个ctx是一个上下文对象,包含如下属性:

ts
/**
 * schema类型
 */
type SchemaType = 'function' | 'object' | 'array' | 'string';
/**
 * beforeEachCbs的上下文对象
 */
type BeforeEachContext = {
  /**
   * 每次循环的key
   */
  key: string | symbol;
  /**
   * 每个项的schema
   */
  schema: DataFieldType;
  /**
   * 模板schema的类型
   */
  type: SchemaType;
  /**
   * 所属对象(用于在递归中进行使用)
   */
  belongTo: string | symbol;
};

基本使用

移除地址数据

ts
const addressModel = defineModel('address', {
  country: 'location.country',
  city: 'location.city',
  children: {
    refModel: 'address',
  },
});
// 用户模型
const userModel = defineModel('user', {
  firstName: 'person.firstName',
  secondName: 'person.lastName',
  age: ['number.int', { min: 18, max: 65 }],
  address: { refModel: 'address', count: 1 },
});
const userDatas = fakeData(userModel, {
  hooks: {
    beforeEachCbs: (ctx) => {
      if (ctx.type === 'object' && ctx.key === 'address') {
        ctx.schema = () => null;
      }
      return ctx;
    },
  },
});
console.dir(userDatas, { depth: Infinity });
ts
{
  firstName: 'Gunnar',
  secondName: 'McCullough',
  age: 59,
  address: null
}

提示

你可以尽可能的依据typebelongTo属性对单个数据项进行甄别

数据项生成后操作数据-afterEachCbs

基本概念

afterEachCbs这个钩子函数与beforeEachCbs类似,只不过是在数据项生成之后执行,它会接受一个 ctx 参数,这个ctx也是一个上下文对象,与beforeEachCbsctx参数有些许不同,包含如下属性:

ts
/**
 * afterEachCbs的上下文对象
 */
type AfterEachContext = {
  /**
   * 每次循环的key
   */
  key: string | symbol;
  /**
   * 每次循环后的value
   */
  value: any;
  /**
   * 已经生成的数据结果
   */
  result: any;
  /**
   * 模板schema的类型
   */
  type: SchemaType;
  /**
   * 所属对象
   */
  belongTo: string | symbol;
};

对递归项的良好支持

上面的beforeAllCbsafterAllCbs钩子函数中是无法实现在引用数据或递归数据中添加或修改数据项的,因为他们是一次性操作,无法进行递归修改,beforeEachCbs也无法做到递归的添加数据,因为它只能修改模板,然而afterEachCbs钩子函数可以完美支持在递归数据中添加或修改数据项,因为他们会在每一个数据项生成后执行。

比如我希望为所有引用数据添加 id`属性

ts
const addressModel = defineModel('address', {
  country: 'location.country',
  city: 'location.city',
  children: {
    refModel: 'address',
  },
});
// 用户模型
const userModel = defineModel('user', {
  firstName: 'person.firstName',
  secondName: 'person.lastName',
  age: ['number.int', { min: 18, max: 65 }],
  address: { refModel: 'address', count: 1 },
});
const userDatas = fakeData(userModel, {
  hooks: {
    afterEachCbs: (ctx) => {
      if (ctx.type === 'object' && ctx.value) {
        // 对所有引用类型添加id
        ctx.value['id'] = faker.string.uuid();
      }
      return ctx;
    },
  },
});
console.dir(userDatas, { depth: Infinity });
ts
{
  firstName: 'Ernest',
  secondName: 'Ritchie',
  age: 42,
  address: {
    country: 'Sint Maarten',
    city: 'Joeborough',
    children: {
      country: 'Sudan',
      city: 'Watsicashire',
      children: null,
      id: '6b9dd2aa-26a2-4072-95af-6c63eddd6dc0'
    },
    id: '945e2165-2119-45ee-bd52-b0c0df8a73b1'
  }
}