【 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.
- 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.
- 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
- intercept
C,
Access operations to objects - Of the object itself
S name
replaceC,
- with
S name
Continue 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:
- I didn’t define the adaptation property, so I’ll use the original one
- Defines the adaptation property, yes
string
Type (array can benumber
(Just use it - Defines the adaptation object, take the object
name
Property, 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:
- Differences in data structures
- Attribute naming differences
- 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:
- For simple enumeration
convert: {
'0': '♀.'1': 'came here'
}
Copy the code
- 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 😁.