【 Write in front 】

What is an adapter

The adapter in this paper refers to the interface between the front and back end interaction, so that the interface data adapt to the front-end program, reduce the impact of interface data on the program, easier to cope with interface changes.

Why is it needed

After the separation of front and rear ends, the interface as the main road of front and rear communication, is the high incidence of car 🚗 disaster lot.

  1. Lack of documentation due to the rush of time, many times require the synchronous development of the front and back ends, but only the prototype, the back end can not give a definitive interface document, then the front end can only be developed according to the prototype, and after development to get the specific interface document and then modify according to the document.
  2. Interface changes When the back end changes the property name of an object in the interface, the front end also needs to make a lot of code changes. Because in the front-end code:
  • It is possible that multiple projects use this interface

    • H5, APP, small programs and other platforms
    • Or buyers and sellers, drivers and passengers
  • Multiple pages may invoke this interface

  • It is possible that this property is used in more than one place on a page

  • It may be necessary to pass this object to another page for use

    These areas must be identified and corrected. Thus, how to minimize the cost of interface changes is a problem worth thinking about. Next, let’s solve this problem.


[Basic Functions]

🎯 Requirements and goals

For example, suppose that the backend interface returns the following user data

// Data from the back end
const response = {
  useId: '1'.userName: 'White mouse',}Copy the code

The front-end data format is as follows

// This is the data name that the front-end is using
const user = {
  id: '1'.name: 'White mouse',}Copy the code

Now I want to get the value of Response. userName via user.name, and I also want to change the value of Response. userName when user.name = ‘rat’.

🔍 Requirements Analysis

From the macroscopic point of view, the naming of attributes is determined by both ends, which is subjective and has no rules to follow. Therefore, the description of the corresponding relationship between the two ends of the name is an essential element to achieve the requirements.

The description of the corresponding relationship is a bridge connecting the two sides of the Taiwan Strait. I call this bridge an Adapter.

Corresponding to the actual requirements, it looks like this:

user The Adapter Adapter response
id < = conversion = > userId
name < = conversion = > userName

Introduction to adapter patterns

To understand adapters, consider a real-life example:


Here paste a section of Baidu Encyclopedia adapter mode entry introduction

In computer programming, the adapter pattern (sometimes called wrapper style or wrapper) ADAPTS the interface of a class to what the user expects. An adaptation allows classes that would normally not work together because of interface incompatibilities to work together by wrapping the class’s own interface in an existing class.

Although js does not have such a strong class concept, but the core idea is consistent.

🛠 Functions

structure

According to the design philosophy of the adapter pattern, can be obtained

Adaptee + Adapter –> Target
The source role + The adapter –> The target character
The interface data + The adapter –> Wrapped data

To code, you write a method that passes in interface data and adapter and returns wrapped data.

Core API

It’s getting cold, so everyone starts to eat hot pot. Hotpot is a more popular mode of eating while you cook, rather than preparing a whole table of dishes before you start eating. First, there are many people to eat, and it takes time and effort to prepare a whole table. Hot pot only needs to deal with the ingredients in advance. Second, when eating slowly, dishes tend to get cold, while hot pot does not.

Thanks to the hot pot design idea, I gave up the stupid idea of processing the data in advance and then going back later, and gave up the idea of putting all the data object.defineProperty () first, and chose the dynamic and high-performance ES6 Proxy.

First, it requires traversal and recursion to process the data in advance, which is inefficient in the case of a multi-layer long list. The Proxy only processes the data when it is needed. Second, even if all the data has been processed in advance, it is very inconvenient to deal with the addition or modification of data, while Proxy is more intelligent.

This is one of the reasons why Vue 3.0 ditched Object.defineProperty() in favor of Proxy.

This article focuses on the idea and design, but the basic usage is not verbose. For those who do not know how to use Proxy, check out Ruan yifeng’s Introduction to ECMAScript 6 and check out the corresponding Reflect. In this article, only the simplest set and get are used.

Packaging method structure

To sum up, the packaging method structure in this example is as follows

/** * wrap * @param {object} adaptee Source role, adaptee * @param {object} adapter adapter * @return {proxy} A wrapped object */
function wrap(adaptee, adapter) {
  // Proxy wraps data
}
Copy the code

Adapter format

Define the format of the adapter with the front end property names on the left and the back end interface property names on the right

// Adapter Adapter
const adapter = {
  // Front-end use: back-end use
  id: 'useId'.name: 'userName',}Copy the code

You can define the mapping between the attribute names at both ends as required. If the attribute names at both ends are the same, omit the mapping.

For convenience, we use the first letters of Client and Server to denote the front-end and back-end definitions C name = the name of the property used by the front-end in the adapter to access the object S name = the name of the property returned by the back-end interface in the adapter

Now let’s implement the wrap method, the core idea

  1. interceptC,Access operations to objects
  2. Of the object itselfS namereplaceC,
  3. withS nameContinue with the original access operation

Code implementation

function wrap(adaptee, adapter) {
  // Verify the validity
  if(! adaptee ||typeofadaptee ! = ='object'| |! adapter) {return adaptee
  }

  return new Proxy(adaptee, {
    // Intercept the value operation target.prop
    get(target, prop, receiver) {
      // Only properties defined by the user in the adapter are processed
      if (adapter.hasOwnProperty(prop)) {
        // Get the source target property instead of the value property
        prop = adapter[prop] 
      }
      return Reflect.get(target, prop, receiver)
    },

    // Intercept the assignment operation target.prop = value
    set(target, prop, value, receiver) {
      if (adapter.hasOwnProperty(prop)) {
        prop = adapter[prop]
      }
      return Reflect.set(target, prop, value, receiver)
    }
  })

  /* Reflect: Since the Proxy intercepts and modifies the behavior of the object, we use the Reflect method to execute the default behavior of the JS language */ to be on the safe side
}
Copy the code

Let’s do a quick test

const target = wrap(response, adapter)
console.log(target.name, target.userName)
// 'mouse' 'mouse'
target.name = 'the white'
console.log(target.name, target.userName)
// 'white' 'white'
Copy the code

👌, both the value and the assignment are as expected, and the goal of the first step has been achieved. Then comes the second goal.


[Multi-layer adaptation]

👁🗨 Found the problem

The above implementation only works with single-layer objects, which in many cases have objects inside them, like matryoshka dolls. If the user wants the object inside the object to have the same functionality, it would have to manually call wrap() again, which is obviously unreasonable, so it should be automatic.

🎯 Requirements and goals

Implementation fits objects at any level. For example, the phoneNum and userPass properties of the selfInfo object need to be adapted.

const response = {
  useId: '1'.userName: 'White mouse'.selfInfo: {
    phoneNum: 18888888888.userPass: 'awsl120120'}},Copy the code

🔍 Requirements Analysis

If you choose different directions before departure, you are destined to take different roads. The traditional approach is traversal and recursion, but choose Proxy, you can go step by step. As mentioned earlier, to make manual automatic, simply call wrap() instead of the user. The problem then is how to describe the adaptation of its own internal objects in the adapter.

Adapter format

To preserve the S name of the attribute map while internally describing a child adapter, a string clearly does not meet the requirement of two people co-existing, so it is changed to an object. Use the name attribute to store the S name, and use the adapter attribute to store the internal adapter as follows:

const adapter = {
  id: 'useId'.name: 'userName'.info: {
    name: 'selfInfo'.adapter: {
      phone: 'phoneNum'.// String notation
      password: {
        name: 'userPass' // Object notation}}}},Copy the code

For ease of use, it must be compatible with the original string writing.

🛠 Functions

Recursive adaptation

For readability and reuse, the code to replace the properties is separated into a separate function getProp(), which returns the properties of the final operation object, but implements the overall method before implementing this function. The idea is that if there are child adapters inside the adapter, the fetched value + the child adapter generates a wrapped proxy object and returns it.

function wrap(adaptee, adapter) {
  if(! adaptee ||typeofadaptee ! = ='object'| |! adapter) {return adaptee
  }

  return new Proxy(adaptee, {
    get(target, prop, receiver) {
      const finalProp = getProp(prop, adapter)
      const value = Reflect.get(target, finalProp, receiver)

      // Try to fetch adapter, undefined
      const { adapter: childAdapter } = adapter[prop] || {}

      /* Whether or not the childAdapter has a value, the wrap() method is called again to determine the condition */ written at the beginning of the wrap()
      return wrap(value, childAdapter)
    },

    set(target, prop, value, receiver) {
      const finalProp = getProp(prop, adapter)
      return Reflect.set(target, finalProp, value, receiver)
    }
  })
}
Copy the code

Seeing this, some readers must be thinking, cheated! Fall for it! Wrap () calls wrap() inside wrap(). It’s still recursion. You just mocked object.defineProperty () for being inefficient. Don’t worry, this is also recursive, but the point is that the inner wrap() is only run once at most every time it is called, because the inner wrap() is written inside the GET method, so it only fires when the property is called, and it only evaluates when it does, and that’s the key to performance improvement.

Everyone heard of schrodinger’s cat, the story behind the research of quantum mechanics is a kind of idea: when a quantum has not been observed, its status is not sure, as if a variable has not been accessed, its value has not been determined, both may be trueCat, also may be falseCat, at this point it is in two states of superposition. When the quantum or variable is observed, it takes on a concrete state. Such a design can greatly improve performance in such a large number of quantum situations. Maybe that’s why the world we live in doesn’t get stuck.

Method of getting propertiesgetProp()

There are three situations after the object type is added:

  1. I didn’t define the adaptation property, so I’ll use the original one
  2. Defines the adaptation property, yesstringType (array can benumber(Just use it
  3. Defines the adaptation object, take the objectnameProperty, if not, use the original
@param {String} Property property of the value @param {Object} adapter Adapter */
function getProp(property, adapter) {
  let prop = property
  if (adapter.hasOwnProperty(property)) {
    prop = adapter[property]
  }

  const map = {
    'number': prop, // Array subscript
    'string': prop,
    'object': prop.hasOwnProperty('name')? prop.name : property// If name is not specified, use the default
  }
  return map[typeof prop]
}
Copy the code

Let’s do a quick test

const target = wrap(response, adapter)
const { info } = target
console.log(info.phone, info.password)
// '18888888888' 'awsl120120'
info.password = 'awsl886'
console.log(target.selfInfo.userPass)
// 'awsl886'
Copy the code

Nothing wrong, another step 👏👏👏


[Array special processing]

👁🗨 Found the problem

We all know that arrays are objects, so what array do I have to deal with? Imagine a scenario where you have an array of objects, and you need to adapt all the objects inside. The objects all look the same. How do you format the adapter?

adapter: {
  0: {
    adapter: {}},1: {
    adapter: {}},// ...
}
Copy the code

It’s going to look silly, it’s not going to work, what about arrays?

adapter: [
  { adapter: {}},
  { adapter: {}},
  // ...
]
Copy the code

That’s kind of interesting, because it looks like you can generate an array with.fill(). The other problem with arrays, however, is that their lengths tend to be dynamic. This only defines how an array fits into its initial state, which is obsolete once new elements are added to the array and it becomes longer. So arrays become uncontrollable compared to objects, requiring special treatment.

🎯 Requirements and goals

We added a property to Response in the above example, which is an array of objects

// Data from the back end
const response = {
  / /... omit
  friendList: [
    {
      userId: '002'.userName: Little Black Mouse.friendTag: 'Surface Brothers'.moreInfo: { nickName: 'dark'}}, {userId: '003'.userName: 'Little Green Mouse'.friendTag: 'Plastic Sisters'.moreInfo: { nickName: 'green']}}},Copy the code

The goal now is to design a reasonable and user-friendly way for users to write an adapter to objects in an array once and then apply it to the entire array, including dynamically added objects to the array.

🔍 Requirements Analysis

First, there is no doubt that the object’s adapter is the same adapter. The problem is how to use this adapter when an array manipulates any object inside it.

Scheme 1. Wildcard characters

The convention uses the wildcard * to match all object property names, including array subscripts

const adapter = {
  / /... omit
  friendList: {
    adapter: {
      The '*': {
        adapter: {
          tag: 'friendTag'.moreInfo: {
            nick: 'nickName'
          }
        }
      }
    }
  }
}
Copy the code

Option 2. Automatic depth

Array that layer directly write object adapter, in the program to achieve automatic depth

const adapter = {
  / /... omit
  friendList: {
    adapter: {
      tag: 'friendTag'.moreInfo: {
        nick: 'nickName'}}}}Copy the code

🛠 Functions

Wildcard schemes – Analysis

The solution is as simple as using * as a spare when fetching child adapters

Wildcard scheme – code implementation

Original code

const { adapter: childAdapter } = adapter[prop] || {}
Copy the code

modified

let { adapter: childAdapter } = adapter[prop] || adapter[The '*') | | {}Copy the code

I! Get things done.

Automatic in-depth solution – analysis

There is only one target, and the adapters of all child objects in the array point to their own adapters. There are many methods, here on the basis of the wildcard scheme, to achieve a.

Automatic in-depth solution – code implementation

let { adapter: childAdapter } = adapter[prop] || adapter[The '*') | | {}if (
  Array.isArray(value) && ! childAdapter.hasOwnProperty(The '*') // The user may have already written
) {
  // Wrap it for yourself
  childAdapter = {
    The '*': { adapter: childAdapter }
  }
}
Copy the code

The childAdapter wraps itself with a layer and passes it in according to the logic it wrote earlier

return wrap(value, childAdapter)
Copy the code

The next time the adapter[‘*’] is executed, the layer will be automatically peeled off and the wrapped adapter will be removed. Between this pack a peel, although the data went to the inside of a layer, but the adapter is still the original adapter.


[Value conversion]

👁🗨 Found the problem

The above adapter solved the problem of naming properties differently at both ends. Now let’s zoom in and look at the interface again. The problem at both ends can be broken down into three areas:

  1. Differences in data structures
  2. Attribute naming differences
  3. The value represents the difference

Differences in data structures

The data structure here refers to the overall structure of data, for example: the structure of one end of the data 👇

const data = {
  dogs: [{ id: 1 }, { id: 2}].cats: [{ id: 3 }, { id: 4}].mouses: [{ id: 5}}]Copy the code

The structure of the data on the other side 👇

const data = [
  { id: 1.type: 'dog' },
  { id: 2.type: 'dog' },
  { id: 3.type: 'cat' },
  { id: 4.type: 'cat' },
  { id: 5.type: 'mouse'},]Copy the code

For such big structural differences, forced adaptation is obviously not appropriate, according to the actual situation to write a separate adaptation method will be a better choice.

Attribute naming differences

The difference in attribute naming is a problem that we have been trying to solve before, and the data structure must be the same before the solution can be used.

The value represents the difference

What are the differences in value representation? The meaning here refers to the same meaning but different expressions. Just like mouse and mouse and mouse both refer to this product 👉🐀. Let’s take a practical example: the backend is database-oriented and uses integer 0 and integer 1 to represent female and male, respectively, in order to reduce storage space. Because the front end is user oriented, in order to let the user understand, use strings female and male. In addition to gender, there are also common issues such as type and state, which we will address next.

🎯 Requirements and goals

Automatically converts one representation of a value to the representation of the value at the other end when evaluating and assigning values. In addition to the fixed correspondence between the sexes mentioned above, another scenario requirement was also made, that is, the adaptation of the representation of time is different.

🔍 Requirements Analysis

Simple enumeration

Such one-to-one mapping of gender is called enumeration. For such simple enumerations, a mapping relationship can be represented by a single object

const enu = {
  '0': '♀.'1': 'came here',}Copy the code

Values and assignments are converted by this mapping

Complex calculations

In contrast to simple enumerations, complex calculations cannot represent mappings between mappings with static data. Time, for example, has a timestamp on one end and a time string text on the other. For example, an array is stored as 2,3,3,3, and the other end is converted to an array [2, 3,3,3]. In short, such complex calculations are too fickle to be solved by simple enumeration and must be left entirely to the users themselves. Therefore, two hook functions are provided to handle the value and assignment operations.

🛠 Functions

Adapter format

First, you need to extend the convert attribute in the adapter to describe the conversion between values

const adapter: {
  name: ' './ / conversion
  adapter: {}, // Child adapter
  convert: {} // The conversion relation is an object
}
Copy the code

Convert allows two formats:

  1. For simple enumeration
convert: {
  '0': '♀.'1': 'came here'
}
Copy the code
  1. For complex calculations
convert: {
  // The value is triggered. Val is the value before processing
  get(val) {
    return xxx // XXX is the value to be handled
  },

  // Assignment is triggered. Val is the value before processing
  set(val) {
    return xxx // XXX is the value assigned by the process}}Copy the code

Add handler function

Add the value-handling functions getValue() and setValue() before the value and assignment, respectively, wrapped in // +++ to see where they were in the old code.

function wrap(adaptee, adapter) {
  if(! adaptee ||typeofadaptee ! = ='object'| |! adapter) {return adaptee
  }

  return new Proxy(adaptee, {
    get(target, prop, receiver) {
      const finalProp = getProp(prop, adapter)
      let value = Reflect.get(target, finalProp, receiver)
      let { adapter: childAdapter } = adapter[prop] || adapter[The '*') | | {}if (
        Array.isArray(value) && ! childAdapter.hasOwnProperty(The '*')
      ) {
        childAdapter = {
          The '*': { adapter: childAdapter }
        }
      }
      // +++++++++++++++++++++++++++++++++++
      value = getValue(value, adapter[prop])
      // +++++++++++++++++++++++++++++++++++
      return wrap(value, childAdapter)
    },

    set(target, prop, value, receiver) {
      const finalProp = getProp(prop, adapter)
      // +++++++++++++++++++++++++++++++++++
      value = setValue(value, adapter[prop])
      // +++++++++++++++++++++++++++++++++++
      return Reflect.set(target, finalProp, value, receiver)
    }
  })
}
Copy the code

getValue()andsetValue()

The logic in these two functions is almost exactly the same, so just to be lazy and not write it twice, I’m going to generate it in one way

@param {String} getOrSet 'get' or 'set' @return {Function} handler */
function genValueFn(getOrSet) {
  @param {any} value specifies the value before processing. @param {Object} options specifies the Object {name, adapter, Convert} * @return {any} The processed value */
  return function(value, options) {
    // todo
    return value
  }
}
const getValue = genValueFn('get')
const setValue = genValueFn('set')
Copy the code

Handle the logic of the function

Both methods need to be supported and share the same object. To prevent collisions, a rule must be given: If the convert object has at least one set or get method, it is considered to handle complex computations and use get or set, otherwise it is considered to handle simple enumerations. The Convert object is an enumeration object.

function genValueFn(xet) {
  return function(value, options) {
    if (options && options.convert) {
      const { get, set } = options.convert
      const isFn = {
        get: typeof get === 'function'.set: typeof set === 'function'     
      }
      if (isFn.get || isFn.set) { // There are get or set methods
        if (isFn[xet]) { // There may be only one of them: get or set
          /* Allows the user to store the data on which the 'get/set' methods depend in the 'convert' object, so it is necessary to ensure that 'this' points to' convert '*/ when executed by' xet '
          value = options.convert[xet](value)
        }
      } else { // Convert is an enumeration object
        // Use bidirectional mapping before use. GetEnum is implemented below
        const enu = getEnum(options.convert)
        value = enu[value]
      }
    }
    return value
  }
}

Copy the code

Inverse mapping andgetEnum()

Since it is JavaScript and not TypeScript, there is no emnu to use 😂, so reverse mapping is implemented. The general forward mapping applies when you’re evaluating

const enu = {
  '0': '♀.'1': 'came here'
}
Copy the code

You also need a reverse mapping that you can use when assigning values

const enu = {
  '♀: '0'.'came here': '1'
}
Copy the code

Regardless of which direction the user writes a mapping, we convert it to a bidirectional mapping for use.

@param {Ojbect} map one-way mapping * @return {Object} bidirectional mapping */
function bidirectional(map) {
  return Object.keys(map).reduce((enu, key) = > {
    enu[enu[map[key]] = key] = map[key]
    return enu
  }, Object.create(null))}Copy the code

To optimize the

Normally, bidirectional() is performed before each assignment to perform a conversion to a bidirectional mapping. Considering that it only takes once to convert a bidirectional map and the cost of iterating through the object is quite high, some optimizations are necessary to cache the transformed object.

Wrap another layer around bidirectional(), return it if it’s in the cache, perform bidirectional() if it’s not, and store it in the cache

const cache = new WeakMap(a)function getEnum(map) {
  let enu = cache.get(map)
  if(! enu) { enu = bidirectional(map) cache.set(map, enu) }return enu
}
Copy the code

Let’s do a quick test

Data from the back end

const response = {
  userSex: '0'.time: '1577531507563',}Copy the code

The adapter

const adapter = {
  sex: {
    name: 'userSex'.convert: {
      '0': '♀.'1': 'came here',}},time: {
    convert: {
      get: stamp2Str,
      set: str2Stamp
    }
  }
}
// Time stamp to time text
function stamp2Str(stamp) {
  stamp = Number.parseInt(stamp)
  return new Date(stamp).toLocaleDateString() + ' ' 
    + new Date(stamp).toTimeString().slice(0.8)}// Time text to timestamp
function str2Stamp(str) {
  return + new Date(str)
}
Copy the code

test

const target = wrap(response, adapter)
console.log(target.sex, target.time)
/ / ♀ 19:11:47 2019-12-28
target.sex = 'came here'
target.time = 'the 2019-12-28 20:11:47'
console.log(target)
// { userSex: '1', time: 1577535107000 }
Copy the code

Fix 😁

【 Write at the end 】

If you have any thoughts or suggestions, please share them in the comments section 😁.