Vue advanced component (HOC) implementation principle

Vue advanced component (HOC) implementation principle

Vue Advanced Components (HOC) Implementation Principle

Go to: HOC

In actual business, if you want to simplify asynchronous state management, you can use the open source library vue-promised based on slot-scopes .

This article mainly emphasizes the idea of actual such high-level components. If you want to use this in a production environment, it is recommended to use the open source library vue-promised

for example

In normal development, the most common requirements are: abnormal request data, and make corresponding processing:

  • In the data request, a prompt is given, such as:'loading'
  • When there is an error in the data request, an error message will be given, such as:'failed to load data'

E.g:

< template > < div > < div v-if = "error" > failed to load data! </div > < div v-else-if = "loading" > loading... </div > < div v-else > result: {{ result.status }} </div > </div > </template > < script > /* eslint-disable prettier/prettier */export default { data () { return { result : { status : 200 }, loading : false , error : false } }, async created () { try { this .loading = true const data = await this .$service.get( '/api/list' ) this .result = data } catch (e) { this .error = true } finally { this .loading = false } } } </script > < style lang = "less" scoped > </style > Copy code

Under normal circumstances, we may write this. But there is a problem with this. Every time you use an asynchronous request, you need to manage the loading and error status, and you need to process and manage the data.

Is there a way to abstract it? Here, high-level components may be an option.

What is a high-order (HOC) component?

A high-level component is actually a function that accepts a component as a parameter and returns a packaged component.

In Vue, a component is an object, so a high-level component is a function that accepts an object and returns a packaged object, namely:

Higher order components are: fn (object) => newObject copy the code

Initial realization

Based on this idea, we can start to try

The high-order component is actually a function, so we need to implement the high-order function of request management, first define its two input parameters:

  • component, the component object that needs to be wrapped
  • promiseFunc, asynchronous request function, must be a promise

The loading, error and other states, the corresponding views, we will handle them in the higher-order function, and return a new packaged component.

export const wrapperPromise = ( component, promiseFn ) => { return { name : 'promise-component' , data () { return { loading : false , error : false , result : null } }, async mounted () { this .loading = true const result = await promiseFn().finally( () => { this .loading = false }) this .result = result }, render ( h ) { return h(component, { props : { result : this .result, loading : this .loading } }) } } } Copy code

At this point, we have almost implemented a preliminary version. Let's add a sample component:

View.vue

< template > < div > {{ result.status }} </div > </template > < script > export default { props : { result : { type : Object , default : () => {} }, loading : { type : Boolean , default : false } } } </script > Copy code

At this point, if we use

wrapperPromise
Wrap this
View.vue
Component

const request = () => { return new Promise ( ( resolve ) => { setTimeout ( () => { resolve({ status : 200 }) }, 500 ) }) } const hoc = wrapperPromise (View, Request) copy the code

And use it to render in the parent component (Parent.vue):

< template > < div > < hoc/> </div > </template > < script > import View from './ View ' import {wrapperPromise} from './utils' const request = () => { return new Promise ( resolve => { setTimeout ( () => { resolve({ status : 200 }) }, 500 ) }) } const hoc = wrapperPromise(View, request) export default { components : { hoc } } </script > copy code

At this point, the component is rendered after being blank for 500ms

200
, The code runs successfully, and the asynchronous data stream runs.

Further optimize high-level components, add "loading" and "error" views, which are more friendly in terms of interaction.

/* eslint-disable max-lines-per-function */ export const wrapperPromise = ( component, promiseFn ) => { return { name : 'promise-component' , data () { return { loading : false , error : false , result : null } }, async mounted () { this .loading = true const result = await promiseFn().finally( () => { this .loading = false }) this .result = result }, render ( h ) { const conf = { props : { result : this .result, loading : this .loading } } const wrapper = h( 'div' , [h(component, conf), this .loading? h( 'div' , [ 'loading...' ]): null , this .error? h( 'div' , [ 'error!!!' ]): null ]) return wrapper } } } Copy code

Perfect again

So far, although high-end components can be used, they are not good enough and still lack some functions, such as:

  1. Get the parameters from the sub-component, which is used to send the parameters of the asynchronous request
  2. Monitor the change of request parameters in the subcomponent and resend the request
  3. The parameters passed by the external component to the hoc component are not passed on. For example, when we use the hoc component in the outermost layer, we hope to provide some additional props or attrs (or slots, etc.) to the innermost packaged component. At this time , The hoc component is needed to pass this information down.

In order to achieve point 1, you need to

View.vue
Add a specific field as a request parameter, such as: requestParams

< template > < div > {{ result.status + '=>' + result.name }} </div > </template > < script > export default { props : { result : { type : Object , default : () => {} }, loading : { type : Boolean , default : false } }, data () { return { requestParams : { name : 'http' } } } } </script > < style lang = "scss" scoped > </style > Copy code

At the same time, write down the request function and let it accept the request parameters. Here we don't do anything to deal with it, and return it as it is.

const request = params => { return new Promise ( resolve => { setTimeout ( () => { resolve({...params, status : 200 }) }, 500 ) }) } Copy code

One question is how can we get

View.vue
What about the value in the component? You can consider adopting
ref
To get, for example:

export const wrapperPromise = ( component, promiseFn ) => { return { name : 'promise-component' , data () { return { loading : false , error : false , result : null } }, async mounted () { this .loading = true //Get the request parameters of the package component const {requestParams} = this .$refs.wrapper const result = await promiseFn(requestParams).finally( () => { this .loading = false }) this .result = result }, render ( h ) { const conf = { props : { result : this .result, loading : this .loading }, ref : 'wrapper' } const wrapper = h( 'div' , [h(component, conf), this .loading? h( 'div' , [ 'loading...' ]): null , this .error? h( 'div' , [ 'error!!!' ]): null ]) return wrapper } } } Copy code

Point 2: When the request parameters of the child component change, the parent component needs to update the request parameters synchronously, re-send the request, and then pass the new data to the child component.

/* eslint-disable max-lines-per-function */ export const wrapperPromise = ( component, promiseFn ) => { return { name : 'promise-component' , data () { return { loading : false , error : false , result : null } }, methods : { async request () { this .loading = true const {requestParams} = this .$refs.wrapper const result = await promiseFn(requestParams).finally( () => { this .loading =false }) this .result = result } }, async mounted () { this .$refs.wrapper.$watch( 'requestParams' , this .request.bind( this ), { immediate : true }) }, render ( h ) { const conf = { props : { result : this .result, loading : this .loading }, ref :'wrapper' } const wrapper = h( 'div' , [h(component, conf), this .loading? h( 'div' , [ 'loading...' ]): null , this .error? h( 'div' , [ 'error!!!' ]): null ]) return wrapper } } } Copy code

The second question, we only put

$attrs
,
$listeners
,
$scopedSlots
Just pass it on.

export const wrapperPromise = ( component, promiseFn ) => { return { name : 'promise-component' , data () { return { loading : false , error : false , result : {} } }, methods : { async request () { this .loading = true const {requestParams} = this .$refs.wrapper const result = await promiseFn(requestParams).finally( () => { this .loading = false }) this .result = result } }, async mounted () { this .$refs.wrapper.$watch( 'requestParams' , this .request.bind( this ), { immediate : true , deep : true }) }, render ( h ) { const conf = { props : { //mix into $attrs ...this.$attrs, result : this .result, loading : this .loading }, //Pass events on : this .$listeners, //Pass $scopedSlots scopedSlots : this .$scopedSlots, ref : 'wrapper' } const wrapper = h( 'div' , [h(component, conf), this .loading? h( 'div' , [ 'loading...' ]): null , this .error? h( 'div' , [ 'error!!!' ]): null ]) return wrapper } } } Copy code

So far, the complete code Parent.vue

< template > < div > < hoc/> </div > </template > < script > import View from './ View ' import {wrapperPromise} from './utils' const request = params => { return new Promise ( resolve => { setTimeout ( () => { resolve({...params, status : 200 }) }, 500 ) }) } const hoc = wrapperPromise(View, request) export default { components : { hoc } } </script > Copy code

utils.js

export const wrapperPromise = ( component, promiseFn ) => { return { name : 'promise-component' , data () { return { loading : false , error : false , result : {} } }, methods : { async request () { this .loading = true const {requestParams} = this .$refs.wrapper const result = await promiseFn(requestParams).finally( () => { this .loading = false }) this .result = result } }, async mounted () { this .$refs.wrapper.$watch( 'requestParams' , this .request.bind( this ), { immediate : true , deep : true }) }, render ( h ) { const conf = { props : { //mix into $attrs ...this.$attrs, result : this .result, loading : this .loading }, //Pass events on : this .$listeners, //Pass $scopedSlots scopedSlots : this .$scopedSlots, ref :'wrapper' } const wrapper = h( 'div' , [h(component, conf), this .loading? h( 'div' , [ 'loading...' ]): null , this .error? h( 'div' , [ 'error!!!' ]): null ]) return wrapper } } } Copy code

View.vue

< template > < div > {{ result.status + '=>' + result.name }} </div > </template > < script > export default { props : { result : { type : Object , default : () => {} }, loading : { type : Boolean , default : false } }, data () { return { requestParams : { name : 'http' } } } } </script > Copy code

Expand

Suppose, business needs to help print logs in mounted hook functions of certain components

const wrapperLog = ( component ) => { return { mounted () { window .console.log( "I am mounted!!!" ) }, render ( h ) { return h(component, { on : this .$listeners, attr : this .$attrs, scopedSlots : this .$scopedSlots, }) } } } Copy code

Thinking

At this point, if you combine the high-end components implemented in the previous article, what if the two are used together?

const hoc = wrapperLog (wrapperPromise (View, Request)) copying the code

This way of writing seems to be a little difficult. If you have learned React, you can consider using the compose function in Redux.

compose

Before understanding the redux compose function, understand the definition of pure function in functional programming. :::tip Pure function pure function refers to the same input, will always get the same output, and there are no observable side effects. :::

export default function compose ( ...funcs ) { if (funcs.length === 0 ) { return arg => arg } if (funcs.length === 1 ) { return funcs[ 0 ] } return funcs.reduce( ( a, b ) => ( ...args ) => a(b(...args))) } Copy code

In fact, the compose function is to

var a = fn1(fn2(fn3(fn4(x))))
This difficult-to-read nested call method is rewritten as:
var a = compose(fn1, fn2, fn3, fn4)(x)
Way to call.

Redux's compose implementation is very simple, using arrays

reduce
Method to achieve, the core code is only one sentence:

return funcs.reduce ( ( A, B ) => ( ..args ) => A (B (args ...))) copying the code

Although I have written front-end code for many years, I have used it

reduce
Function, but it's still rather silly to see this code.

for example

Therefore, here is an example to see if this function is executed.

Import {} Compose from 'Redux' let a = 10 function fn1 ( s ) { return s + 1 } function fn2 ( s ) { return s + 2 } function fn3 ( s ) { return s + 3 } function fn4 ( s ) { return s + 4 } let res = fn1(fn2(fn3(fn4(a)))) //i.e.: 10 + 4 + 3 + 2 + 1 // compose , : let composeFn = compose(fn1, fn2, fn3, fn4) let result = composeFn(a) //20

compose
,
composeFn
:

[fn1, fn2, fn3, fn4].reduce((a, b) => (...args) => a(b(...args)))
a b
1 fn1fn2(...args) => fn1(fn2(...args))
2 (...args) => fn1(fn2(...args))fn3(...args) => fn1(fn2(fn3(...args)))
3 (...args) => fn1(fn2(fn3(...args)))fn4(...args) => fn1(fn2(fn3(fn4(...args))))

:

(...args) => fn1(fn2(fn3(fn4(...args))))
. compose , .

,

wrapperPromise
, , .

const wrapperPromise = (promiseFn) => { return function(wrapper){ return { mounted() {}, render() {} } } }

, .

const composed = compose(wrapperPromise(request), wrapperLog) const hoc = composed(View)

compose
.
redux
compose
, .

Array.prototype.reduce
, ,
=>
( ), .

::: tip warning (

compose
) , , , . :::