移植JavaScript对象模式的一种方法

54 阅读5分钟

简介

最近,我发现自己处于这样一个位置:一个应用程序严重依赖一个状态对象。这对于单页面应用程序(SPA)来说是相当典型的,当你的状态对象的模式发生重大变化,而你的用户的数据保存在旧的模式下时,就会带来挑战。

在这篇文章中,我将探讨一个概念验证的解决方案,我把它放在一起探讨这个话题。虽然我确信已经有一些模式迁移工具存在,但我认为这将是一个有趣的、具有教育意义的主题探索

一个问题实例

假设我创建了一个应用程序,其中有一个用户,该用户可以输入他们的宠物类型和品种。启动MVP后,我的状态对象看起来像这样。

const state = {
  person: {
    name: 'Edgar',
    pets: {
      type: 'dog',
      name: 'Daffodil',
    },
  },
};

这对MVP来说是很好的,但很快我就意识到我不希望pets 属性在person 属性下,而是希望它在state 下成为它自己的属性。换句话说,我的理想状态可能是这样的。

const state = {
  person: {
    name: 'Edgar',
  },
  pets: {
    type: 'dog',
    name: 'Daffodil',
  },
};

虽然我希望能够在我的SPA中进行这种改变,但我担心现有的应用程序用户在某个地方保存了我的原始模式(例如,本地存储、nosql、JSON字符串等)。如果我加载旧的数据,但我的应用程序希望使用新的模式,我可能会尝试访问错误的地方的属性(例如,state.pets.typestate.person.pets.type ),从而导致问题。

模式迁移的拯救!

模式迁移并不是一个新概念;它被用来在不同版本的应用程序之间迁移数据库表已经有一段时间了。在这篇文章中,我将使用模式迁移背后相同的基本概念来迁移JavaScript对象。

定义我们的迁移数组

让我们来定义一个要运行的迁移数组。每个迁移都会有一个from,to,up, 和down 属性。fromto 属性将分别代表较低和较高的版本,而updown 属性将是将一个模式从from 版本迁移到to 版本的函数,反之亦然。这听起来可能有点混乱,但我认为在我们的person/pets例子的背景下会更有意义一些。

让我们来写第一个迁移。

const migrations = [
  {
    from: '1.0',
    to: '1.1',
    up: (schema) => {
      const newSchema = {
        version: '1.1',
        person: {
          name: schema.person.name,
        },
        pets: {
          ...schema.person.pets,
        },
      };
      return newSchema;
    },
    down: (schema) => {
      const newSchema = {
        version: '1.0',
        person: {
          ...schema.person,
          pets: { ...schema.pets },
        },
      };
      return newSchema;
    },
  },
];

如果我们有一个 "1.0 "版本的模式,这个对象的up 方法将把这个模式转换成 "1.1"。相反,如果我们有一个 "1.1 "版本的模式,down 方法将把该模式转换成 "1.0"。

让迁移发生

这在概念上是很酷的,但我们需要创建一个实际执行迁移的函数。为了做到这一点,我们将创建一个migrate 函数,该函数将一个模式和该模式应该被迁移的版本号作为参数。

const migrate = (schema, toVersion) => {
  const fromVersion = schema.version;
  const direction = upOrDown(fromVersion, toVersion);
  if (direction === 'same') {
    return schema;
  }
  const currentMigration = migrations.find(
    (migration) => migration[direction === 'up' ? 'from' : 'to'] === fromVersion
  );
  const newSchema = currentMigration[direction](schema);
  return migrate(newSchema, toVersion);
};

你可能会注意到这个函数的几个特点:它是递归的(直到我们迁移到目标版本才会停止),而且它引用了一个辅助函数upOrDown ,我在下面定义了这个函数。这个函数只是帮助确定迁移的方向(1.0到1.1是向上,1.1到1.0是向下)。

const upOrDown = (fromVersion, toVersion) => {
  const fromNumbers = fromVersion.split('.').map((el) => Number(el));
  const toNumbers = toVersion.split('.').map((el) => Number(el));
  for (let i = 0; i < fromNumbers.length; i++) {
    if (fromNumbers[i] < toNumbers[i]) {
      return 'up';
    }
    if (fromNumbers[i] > toNumbers[i]) {
      return 'down';
    }
  }
  return 'same';
};

进行测试运行

让我们创建两个对象,一个是版本 "1.0 "模式,另一个是版本 "1.1 "模式。我们的目标是将 "1.0 "模式迁移到 "1.1",将 "1.1 "模式迁移到 "1.0"。

const schemaA = {
  version: '1.0',
  person: {
    name: 'Edgar',
    pets: {
      type: 'dog',
      name: 'Daffodil',
    },
  },
};

const schemaB = {
  version: '1.1',
  person: {
    name: 'Edgar',
  },
  pets: {
    type: 'dog',
    name: 'Daffodil',
  },
};

现在,让我们来运行我们的迁移。

// From 1.0 to 1.1
console.log(migrate(schemaA, '1.1'));
/*
{ version: '1.1',
  person: { name: 'Edgar' },
  pets: { type: 'dog', name: 'Daffodil' } }
*/

// From 1.1 to 1.0
console.log(migrate(schemaB, '1.0'));
/*
{ version: '1.0',
  person: { name: 'Edgar', pets: { type: 'dog', name: 'Daffodil' } } }
*/

完美!我们现在可以 "向上 "迁移了。我们现在可以从一个模式版本 "向上 "迁移到下一个版本,或者 "向下 "迁移。

另一个模式的改变!

我现在意识到,一个人可以有多个宠物,为什么不呢?因此,我们的pets 关键实际上应该是一个数组,而不是一个对象。此外,我意识到我们的person 键可能只是这个人的名字,而不是有一个name 键(我已经决定我们不会再有任何与这个人有关的道具)。这意味着一个新的模式,1.2版,看起来会是这样的。

const state = {
  person: 'Edgar',
  pets: [
    {
      type: 'dog',
      name: 'Daffodil',
    },
  ],
};

所以,我们来写一个从1.1版到1.2版的迁移。

const migrations = [
  {
    from: '1.0',
    to: '1.1',
    up: (schema) => {
      const newSchema = {
        version: '1.1',
        person: {
          name: schema.person.name,
        },
        pets: {
          ...schema.person.pets,
        },
      };
      return newSchema;
    },
    down: (schema) => {
      const newSchema = {
        version: '1.0',
        person: {
          ...schema.person,
          pets: { ...schema.pets },
        },
      };
      return newSchema;
    },
  },
  {
    from: '1.1',
    to: '1.2',
    up: (schema) => {
      const newSchema = {
        version: '1.2',
        person: schema.person.name,
        pets: [schema.pets],
      };
      return newSchema;
    },
    down: (schema) => {
      const newSchema = {
        version: '1.1',
        person: {
          name: schema.person,
        },
        pets: schema.pets[0],
      };
      return newSchema;
    },
  },
];

多版本迁移

还记得我们的migrate 函数是如何递归的吗?当我们需要迁移多个版本时,这就变得非常有用了。比方说,我们想从1.0模式迁移到1.2模式,反之亦然。我们可以做到这一点!

// 1.0 to 1.2
console.log(migrate(schemaA, '1.2'));
/*
{ version: '1.2',
  person: 'Edgar',
  pets: [ { type: 'dog', name: 'Daffodil' } ] }
*/

const schemaC = {
  version: '1.2',
  person: 'Edgar',
  pets: [
    {
      type: 'dog',
      name: 'Daffodil',
    },
  ],
};
// 1.2 to 1.0
console.log(migrate(schemaC, '1.1'));
/*
{ version: '1.0',
  person: { name: 'Edgar', pets: { type: 'dog', name: 'Daffodil' } } }
*/

嘿,这很有效

总结

这是对模式迁移世界的一次有趣的探索!在黑掉了一些模式迁移功能之后,我现在对能够使用 "自行开发 "的方法或现有的包来实现这一功能相当有信心。