preface
High-order components are a concept that has been popular in React, but not much discussed in the Vue community. This article will take you through some of the more advanced operations.
Just to be clear, the core of this article is to learn the idea of decoupling an intelligent component from a puppet component. It doesn’t matter if you haven’t heard of the concept, it will be explained below.
This can be done in many ways, such as slot-scopes, and in the future, composition-API. The code written in this article is not recommended for use in the production environment. There are more mature libraries to use in the production environment. This article focuses on the idea of adapting the React community gameplay.
Don’t spray me, don’t spray me, don’t spray me!! This article is just to demonstrate ideas for higher-order components, but if you want to simplify the asynchronous state management mentioned in this article in real business, use the slot-scopes based open source library vue-Promised
In addition, the mention of 20K in the title is actually a bit titleparty. What I want to express more is that we should have such a spirit. Knowing only this skill will certainly not make you reach 20K. But I believe that with the dedication to advanced usage, business code, and efficiency, we will get there, and that day is not far away.
example
This article uses the most common requirements in daily development, namely the request for asynchronous data as an example.
<template>
<div v-if="error">failed to load</div>
<div v-else-if="loading">loading...</div>
<div v-else>hello {{result.name}}!</div>
</template>
<script>
export default {
data() {
return {
result: {
name: ' ',},loading: false.error: false,}},async created() {
try {
Loading / / management
this.loading = true
/ / get data
const data = await this.$axios('/api/user')
this.data = data
} catch (e) {
/ / the error management
this.error = true
} finally {
Loading / / management
this.loading = false}}},</script>
Copy the code
We usually write in this way, and there is no problem with it. But in fact, every time we write asynchronous requests, we have to have loading and error states, and we need to have logic to fetch data, and we need to manage these states.
So how about abstracting it? The React community used HOC (high Order Component) to handle such abstractions before hooks became popular.
What are the higher-order components?
At this point, we need to think about what a higher-order component is. In the end, a higher-order component is:
A function takes a component as an argument and returns a wrapped component.
In the React
In React, components are classes, so higher-order components are sometimes implemented using decorator syntax, because the essence of decorators is to accept a Class and return a new Class.
In the React world, higher-order components are f(Class) -> the new Class.
In the Vue
In the world of Vue, a component is an object, so a higher-order component is a function that takes an object and returns a new wrapped object.
By analogy to the world of Vue, higher-order components are f(Object) -> new Object.
Smart components and puppet components
If you’re not familiar with the concept of puppet components and smart components, I’ll give you a quick rundown. It’s a well-established concept in the React community.
Puppet component: Like a marionette, render the view based on the props passed in from outside, regardless of where the data came from.
Intelligent component: generally package in the puppet component outside, through requests and other ways to obtain data, passed to the puppet component, control its rendering.
In general, their structural relationship looks like this:
<Intelligent components>
<Puppet components />
</Intelligent components>
Copy the code
They also have another nickname, container component and UI component.
implementation
In the example above (go back and read it if you forgot, haha), the idea is this:
- Higher-order component acceptance
Puppet components
和Requested method
As a parameter - in
mounted
Data is requested during the life cycle - Pass the requested data through
props
Passed to thePuppet components
.
First of all, as mentioned above, HOC is a function. This time, our requirement is HOC to realize request management. So we define it to accept two parameters first, and we call this HOC withPromise.
Loading and error states, as well as views of loading and loading errors, should be defined in the new wrapper component returned, that is, the new object returned in the function below.
const withPromise = (wrapped, promiseFn) = > {
return {
name: "with-promise",
data() {
return {
loading: false.error: false.result: null}; },async mounted() {
this.loading = true;
const result = await promiseFn().finally((a)= > {
this.loading = false;
});
this.result = result; }}; };Copy the code
In the parameters:
wrapped
That is, the component object that needs to be wrapped.promiseFunc
That is, the corresponding function to the request needs to return a Promise
This looks good, but we can’t write templates in the same way we write templates in the.vue single file.
But we know that the template will eventually be compiled into the render function on the component object, so we’ll just write the render function. (Note that this example uses the original syntax for demonstration purposes; scaffolding projects can be created using JSX syntax directly.)
In the render function, we wrap the wrapped puppet component passed in.
The intelligent component gets data -> the puppet component consumes data, and the data flows.
const withPromise = (wrapped, promiseFn) = > {
return {
data() { ... },
async mounted() { ... },
render(h) {
return h(wrapped, {
props: {
result: this.result,
loading: this.loading, }, }); }}; };Copy the code
At this point, with a barely usable prototype, let’s declare the puppet component.
This is a separation of logic and view.
const view = {
template: ` {{result? .name}} `.props: ["result"."loading"]};Copy the code
Note that the component here could be any.vue file, but I’m just using it for simplicity.
And then something magical happens, don’t blink, we wrap this view component withPromise.
// Pretend this is an AXIos request function
const request = (a)= > {
return new Promise((resolve) = > {
setTimeout((a)= > {
resolve({ name: "ssh" });
}, 1000);
});
};
const hoc = withPromise(view, request)
Copy the code
Then render it in the parent component:
<div id="app">
<hoc />
</div>
<script>
const hoc = withPromise(view, request)
new Vue({
el: 'app',
components: {
hoc
}
})
</script>
Copy the code
At this point, after a blank second, the component renders my name SSH, and the entire asynchronous data stream runs through.
Now add loading and loading failure views to make the interaction a little more user-friendly.
const withPromise = (wrapped, promiseFn) = > {
return {
data() { ... },
async mounted() { ... },
render(h) {
const args = {
props: {
result: this.result,
loading: this.loading,
},
};
const wrapper = h("div", [
h(wrapped, args),
this.loading ? h("span"["Loading...") :null.this.error ? h("span"["Loading error") :null,]);returnwrapper; }}; };Copy the code
The code up to this point can be viewed in the preview, and the source code can be viewed directly in the console source.
perfect
The higher-order component so far is demonstrable, but not complete, and lacks some features such as
- To get the parameters defined on the child component as parameters to initialize the send request.
- Listen for changes in the request parameters in the child component and resend the request.
- The external component passes to
hoc
Component parameters are not passed through now.
The first point is easy to understand; the parameters of the scenario we requested are flexible.
The second requirement is also a common requirement in real world scenarios.
For example, when we use hoc in the outermost layer, we might want to pass some extra props or attrs or even slots to the innermost puppet component. As a bridge, hoc components should assume the responsibility of transmitting it through.
To implement the first, we agreed that the View component would need to mount a field with a particular key as the request parameter, for example, we agreed that it would be called requestParams.
const view = {
template: ` {{result? .name}} `,
data() {
// Take it with you when sending requests
requestParams: {
name: 'ssh'}},props: ["result"."loading"]};Copy the code
So let’s rewrite our request function so that it’s ready to accept arguments,
And let its response data return the request parameters as is.
// Pretend this is an AXIos request function
const request = (params) = > {
return new Promise((resolve) = > {
setTimeout((a)= > {
resolve(params);
}, 1000);
});
};
Copy the code
So the question now is how do we get the value of the View component in hoc,
How do we normally take child component instances? That’s right, ref, it’s also used here:
const withPromise = (wrapped, promiseFn) = > {
return {
data() { ... },
async mounted() {
this.loading = true;
// Get the data from the child component instance
const { requestParams } = this.$refs.wrapped
// pass to the requesting function
const result = await promiseFn(requestParams).finally((a)= > {
this.loading = false;
});
this.result = result;
},
render(h) {
const args = {
props: {
result: this.result,
loading: this.loading,
},
// If you pass a ref here, you can get the child component instance, just like in the normal template.
ref: 'wrapped'
};
const wrapper = h("div"[this.loading ? h("span"["Loading...") :null.this.error ? h("span"["Loading error") :null,
h(wrapped, args),
]);
returnwrapper; }}; };Copy the code
To complete the second point, when the request parameters of the child component change, the parent component also responds to resend the request and brings the new data to the child component.
const withPromise = (wrapped, promiseFn) = > {
return {
data() { ... },
methods: {
// Requests are abstracted as methods
async request() {
this.loading = true;
// Get the data from the child component instance
const { requestParams } = this.$refs.wrapped;
// pass to the requesting function
const result = await promiseFn(requestParams).finally((a)= > {
this.loading = false;
});
this.result = result; }},async mounted() {
// Send the request immediately, and listen for parameter changes to request again
this.$refs.wrapped.$watch("requestParams".this.request.bind(this), {
immediate: true}); }, render(h) { ... }}; };Copy the code
$listeners, $listeners, and $scopedSlots are all we need to do to create a child component.
$listeners are the attributes declared on the external template, and $listeners are the functions declared on the external template.
Take this example:
<my-input value="ssh" @change="onChange" />
Copy the code
Inside the component you get something like this:
{
$attrs: {
value: 'ssh'
},
$listeners: {
change: onChange
}
}
Copy the code
Note that the functions of $listeners are the same as those of my-Input, and that the functions of $listeners are the same as those of my-Input. $listeners can upload $attrs and $listeners directly to the el-input process.
/ / my - input inside<template>
<el-input v-bind="$attrs" v-on="$listeners" />
</template>
Copy the code
So in the render function, we can pass it like this:
const withPromise = (wrapped, promiseFn) = > {
return {
...,
render(h) {
const args = {
props: {
/ / with $attrs. this.$attrs,result: this.result,
loading: this.loading,
},
// Pass events
on: this.$listeners,
/ / passed $scopedSlots
scopedSlots: this.$scopedSlots,
ref: "wrapped"};const wrapper = h("div"[this.loading ? h("span"["Loading...") :null.this.error ? h("span"["Loading error") :null,
h(wrapped, args),
]);
returnwrapper; }}; };Copy the code
At this point, the complete code is implemented:
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="Width = device - width, initial - scale = 1.0" />
<title>hoc-promise</title>
</head>
<body>
<div id="app">
<hoc msg="msg" @change="onChange">
<template>
<div>I am slot</div>
</template>
<template v-slot:named>
<div>I am named slot</div>
</template>
</hoc>
</div>
<script src="./vue.js"></script>
<script>
var view = {
props: ["result"],
data() {
return {
requestParams: {
name: "ssh",}}; },methods: {
reload() {
this.requestParams = {
name: "changed!!"}; }},template: ` {{result? .name}} < / span > < slot > < / slot > < slot name = "named" > < / slot > < button @ click = "reload" > reload data < / button > < / span > `};const withPromise = (wrapped, promiseFn) = > {
return {
data() {
return {
loading: false.error: false.result: null}; },methods: {
async request() {
this.loading = true;
// Get the data from the child component instance
const { requestParams } = this.$refs.wrapped;
// pass to the requesting function
const result = await promiseFn(requestParams).finally((a)= > {
this.loading = false;
});
this.result = result; }},async mounted() {
// Send the request immediately, and listen for parameter changes to request again
this.$refs.wrapped.$watch(
"requestParams".this.request.bind(this),
{
immediate: true}); }, render(h) {const args = {
props: {
/ / with $attrs. this.$attrs,result: this.result,
loading: this.loading,
},
// Pass events
on: this.$listeners,
/ / passed $scopedSlots
scopedSlots: this.$scopedSlots,
ref: "wrapped"};const wrapper = h("div"[this.loading ? h("span"["Loading...") :null.this.error ? h("span"["Loading error") :null,
h(wrapped, args),
]);
returnwrapper; }}; };const request = (data) = > {
return new Promise((r) = > {
setTimeout((a)= > {
r(data);
}, 1000);
});
};
var hoc = withPromise(view, request);
new Vue({
el: "#app".components: {
hoc,
},
methods: {
onChange() {},
},
});
</script>
</body>
</html>
Copy the code
You can preview the code here.
When we develop new components, we just take hoc and reuse it, and its business value is realized, and the code is reduced to unimaginable levels.
import { getListData } from 'api'
import { withPromise } from 'hoc'
const listView = {
props: ["result"].template: `
{{ item }}
`};export default withPromise(listView, getListData)
Copy the code
Everything becomes simple and elegant.
combination
Note that this chapter may be difficult for those of you who have not been exposed to React development, so read it or skip it.
One day, we were suddenly happy to write a higher-level component called withLog, which simply prints a log during mounted declaration cycles.
const withLog = (wrapped) = > {
return {
mounted() {
console.log("I am mounted!")
},
render(h) {
return h(wrapped)
},
}
}
Copy the code
Here we find that we need to extract on and scopedSlots and pass them transparently. We wrap a function that integrates the attributes we need to pass transparently from this:
function normalizeProps(vm) {
return {
on: vm.$listeners,
attr: vm.$attrs,
/ / passed $scopedSlots
scopedSlots: vm.$scopedSlots,
}
}
Copy the code
The second argument to h is then extracted and passed.
const withLog = (wrapped) = > {
return {
mounted() {
console.log("I am mounted!")
},
render(h) {
return h(wrapped, normalizeProps(this))}}}Copy the code
And then wrap it outside of the hoc:
var hoc = withLog(withPromise(view, request));
Copy the code
For example, the compose function is composed from the redux library. The compose function returns a new function, which is composed to the redux library.
Functional compose
function compose(. funcs) {
return funcs.reduce((a, b) = >(... args) => a(b(... args))) }Copy the code
Compose (a, B, c) returns a new function that nested several incoming functions
Function signature returned :(… args) => a(b(c(… args)))
It’s a function that might take a lot of time for those of you who are new to it, because it’s really complicated, but once you understand it, you take your functional thinking to the next level.
For example, compose uncompose is composed with multiple parameters. For example, compose uncompose is composed with multiple parameters
Circulating compose
For those of you who are not aware of the compose function, we can write a regular loop that returns a function, executes the array of incoming functions from right to left, and takes the return value of the last function as an argument to the next one.
The compose function should look something like this:
function compose(. args) {
return function(arg) {
let i = args.length - 1
let res = arg
while(i >= 0) {
let func = args[i]
res = func(res)
i--
}
return res
}
}
Copy the code
Transform withPromise
However, this means that we need to modify the higher-order withPromise function, because if you look closely at compose, it wraps the function, makes it accept an argument, and passes the return value of the first function to the next function as an argument.
For example, for compose(a, b), the value returned by b(arg) will be used as a parameter to the compose(b(args) call.
This requires making sure that each of the functions that compose accepts takes only one argument.
So along the same lines, we modify withPromise to further advance it so that it returns a function that takes only one argument:
const withPromise = (promiseFn) = > {
// Return a layer of function wrap, which takes only one argument
return function wrap(wrapped) {
// Go one layer further and return the component
return {
mounted() {},
render() {},
}
}
}
Copy the code
With it, you can compose higher-order components more elegantly:
const compsosed = compose(
withPromise(request),
withLog,
)
const hoc = compsosed(view)
Copy the code
The full code for the compose section above is here.
Note that this section is normal if you don’t understand these concepts for the first time. They are popular in the React community, but rarely discussed in the Vue community! About the compose function, in the React community come into contact with it for the first time I can’t understand completely, to know its usage, slowly understanding is not late.
Real business Scenario
Many people may find the above code to be of little practical value, but the vue-Router advanced usage documentation actually shows a scenario where higher-order components are used to solve the problem.
To briefly describe the scenario, we know that vue-Router can be configured with asynchronous routes. However, in the case of slow network speed, the chunk corresponding to this asynchronous route, namely component code, will be jumped after the download is completed.
During the Loading of asynchronous components, we want to show a Loading component to make the interaction more friendly.
In the section of Vue document — Asynchronous components, it can be clearly seen that Vue supports the rendering component corresponding to asynchronous component declaration loading:
const AsyncComponent = (a)= > ({
// The component to load (should be a 'Promise' object)
component: import('./MyComponent.vue'),
// The component used when the asynchronous component is loaded
loading: LoadingComponent,
// The component used when loading failed
error: ErrorComponent,
// Display the component delay time when loading. The default is 200 (ms)
delay: 200.// If a timeout is provided and the component loads time out,
// Use the component used when the load failed. The default value is' Infinity '
timeout: 3000
})
Copy the code
We tried writing this code to vue-router to override the original asynchronous route:
new VueRouter({
routes: [{
path: '/',
- component: () => import('./MyComponent.vue')
+ component: AsyncComponent}]})Copy the code
It is not supported at all. After debugging the source code of VUe-Router, it is found that vue-Router has two different logic for parsing asynchronous components and processing vue. In the implementation of Vue-Router, it will not help you to render Loading components.
This is certainly not difficult for the intelligent community leaders, we change a way of thinking, let vue-Router jump to a container component, this container component helps us to use vue internal rendering mechanism to render AsyncComponent, can not render the loading state? The specific code is as follows:
Since vue-Router’s Component field accepts a Promise, we wrap the component in promise.resolve.
function lazyLoadView (AsyncView) {
const AsyncHandler = (a)= > ({
component: AsyncView,
loading: require('./Loading.vue').default,
error: require('./Timeout.vue').default,
delay: 400.timeout: 10000
})
return Promise.resolve({
functional: true,
render (h, { data, children }) {
// Here we use vUE's internal rendering mechanism to render true asynchronous components
return h(AsyncHandler, data, children)
}
})
}
const router = new VueRouter({
routes: [{path: '/foo'.component: (a)= > lazyLoadView(import('./Foo.vue'))}]})Copy the code
In this way, a nice Loading component is rendered on the page in between Loading code on the jump.
conclusion
All of the code for this article is stored in the Github repository and previewed.
This literature to study in my source gave me great help on the road of the Vue technology insider author hcysun bosses, although I haven’t spoken to him, but when I was a few months working on the small white, a business needs to think: let I found this article explore the Vue high-order component | HcySunYang
At that time, I could not understand the source code problems and fixes mentioned in this article, and then switched to a different way to implement the business, but the article mentioned in my mind has always been, I am busy working in the spare time to learn the source code, in the hope that one day I can fully understand this article.
This bug was rewritten in Vue 2.6 due to the slot implementation, and it was also fixed. Now Vue uses the latest slot syntax in conjunction with higher-order functions. The bugs mentioned in this article are no longer encountered.
❤️ thank you
1. If this article is helpful to you, please support it with a like. Your like is the motivation for my writing.
2. Follow the public account “front-end from advanced to hospital” to add my friends, I pull you into the “front-end advanced communication group”, we communicate and progress together.