Principles and techniques of encapsulating Vue components

Principles and techniques of encapsulating Vue components

Vue's component system

The API of Vue component mainly includes three parts: prop, event and slot

  • props represents the parameters received by the component. It is best to use the object writing method. In this way, you can set the type, default value or user-defined verification attribute value for each attribute. In addition, you can verify the input through type, validator, etc
  • slot can dynamically insert some content or components into components, which is an important way to realize high-level components; When multiple slots are required, named slots can be used
  • event is an important way for child components to pass messages to parent components

Unidirectional data flow

Reference: one way data flow - official document.

Parent prop Updates flow down the child components, but not vice versa

Unidirectional data flow is a very obvious feature of Vue components. The value of props should not be directly modified in sub components

  • If the passed prop is only used for presentation and does not involve modification, it can be used directly in the template
  • If you need to convert the value of prop and display it, you should use computed to calculate the attribute
  • If the value of prop is used as initialization, you should define the data attribute of a child component and take prop as its initial value
    From the source code / SRC / core / vdom / create component JS and / SRC / core / vdom / helpers / extract props As can be seen in JS, when processing the value of props, first start from
function extractPropsFromVNodeData(){  
    const res = {}  const { attrs, props } = data  // Perform shallow copy  
    checkProp(res, props, key, altKey, true) || checkProp(res, attrs, key, altKey,  false)   
    return res
} 

If props is modified in the child component, the parent component will not be modified. This is because attrs is passed to props through shallow replication in extractPropsFromVNodeData.

Shallow copy means that modifying props of objects and arrays in child components will still affect parent components, which violates the design of one-way data flow. Therefore, this situation needs to be avoided.

Communication between components

Here you can refer to: Full disclosure of vue component communication , the writing is quite comprehensive

  • The relationship between parent and child components can be summarized as prop passing down and event passing up
  • The data transfer of ancestor components and descendant components (across multiple generations) can be realized by using provide and inject
    In addition, if communication between cross components or sibling components is required, it can be realized through eventBus or vuex.

Bypass one-way data flow

Consider the following scenario: the parent component transfers the data to the child component in the form of prop. The child component performs relevant operations and modifies the data. It is necessary to modify the prop value of the parent component (a typical example is the commodity quantity counter component of the shopping cart).

According to the component one-way data flow and event communication mechanism, the child component needs to notify the parent component through events, and modify the original prop data in the parent component to complete the status update. The scenario of modifying the data of the parent component in the child component is also common in business, so what can be done to "bypass" the restriction of one-way data flow?

State promotion

You can refer to the state promotion of React and directly transfer the data processing logic of the parent element to the child components through props. The child components only display data and mount events

<template>
    <div class="counter">
        <div class="counter_btn" @click="onMinus">-</div>
        <div class="counter_val">{{value}}</div>
        <div class="counter_btn" @click="onPlus">+</div>
    </div>
</template>
 
<script>
    export default {
        props: {
            value: {
                type: Number,
                default: 0
            },
            onMinus: Function,
            onPlus: Function
        },
    };
</script>

The event handler function is then passed in at call time

<template>
    <div>
        <counter :value="counter2Val" :on-minus="minusVal" :on-plus="plusVal"></counter>
    </div>
</template>
<script>
    export default {
        data() {
            return {
                counter2Val: 0,
            }
        },
        methods: {
            minusVal(){
                this.counter2Val--
            },
            plusVal(){
                this.counter2Val++
            }
        }
    }
</script>

Obviously, since each parent component needs to implement on minus and on plus, state promotion does not fundamentally solve the problem.

v-model syntax

Vue has built-in v-model instruction. v-model is a syntax sugar, which can be disassembled into props: value and events: input. That is, as long as the component provides a prop named value and a user-defined event named input, and these two conditions are met, the user can use v-model on the user-defined component

<template>
  <div>
    <button @click="changeValue(-1)">-1</button>
    <span>{{currentVal}}</span>
    <button @click="changeValue(1)">+1</button>
  </div>
</template>
 
<script>
export default {
  props: {
    value: {
      type: Number // Define the value attribute
    }
  },
  data() {
    return {
      currentVal: this.value
    };
  },
  methods: {
    changeVal(val) {
      this.currentVal += parseInt(val);
      this.$emit("input", this.currentVal); // Define input events
    }
  }
};
</script>

Then you only need to import v-model instructions when calling.

<counter v-model="counerVal"/>

Using v-model, you can easily synchronize the data of the parent component in the child component. In versions after 2.2, you can customize the prop and event names of the v-model instruction. Refer to the model configuration item

export default {    model: {        prop: 'value',        event: 'input'    },    // ... }

Get a reference to a component instance
In developing components, obtaining component instances is a very useful method. Components can obtain vm instance references through $refs, $parents, $children, etc

  • $refs add the ref attribute to the component (or dom)
  • $parents gets the parent component node of the child component mount
  • $children, get all child nodes of the component
    These interfaces return vnodes, which can be accessed through vnode Componentinstance obtains the corresponding component instance, and then directly calls the component's method or accesses data. Although this method more or less violates the design concept of components and increases the coupling cost between components, the code implementation will be more concise.

Form validation component

Generally, form validation is a very common application scenario before form submission. So, how to encapsulate the function of form verification in the component?

The following is an example of a form component, which shows that the form verification function is realized by obtaining the reference of the component.

First, define the usage of components,

  • XM form receives two prop s, model and rule
    • model represents the data object bound by the form, which is the object submitted by the form
    • Rule represents the validation rule policy, and the async validator plug-in can be used for form validation
  • The prop attribute received by XM form item corresponds to a key value of the model and rule of the form component. According to the key, get the form data from the model and the validation rule from the rule
    Here is the sample code to use
<template>
    <div class="page">
        <xm-form :model="form" :rule="rule" ref="baseForm">
            <xm-form-item label="full name" prop="name">
                <input v-model="form.name"/>
            </xm-form-item>
            <xm-form-item label="mailbox" prop="email">
                <input v-model="form.email"/>
            </xm-form-item>
            <xm-form-item>
                <button @click="submit">Submit</button>
            </xm-form-item>
        </xm-form>
    </div>
</template>
 
<script>
    import xmForm from "../src/form/form"
    import xmFormItem from "../src/form/form-item"
 
    export default {
        components: {
            xmForm,
            xmFormItem,
        },
        data() {
            return {
                form: {
                    name: "",
                    email: ""
                },
                rule: {
                    name: [
                        {required: true, message: 'User name cannot be empty', trigger: 'blur'}
                    ],
                    email: [
                        {required: true, message: 'Mailbox cannot be empty', trigger: 'blur'},
                        {type: 'email', message: 'The mailbox format is incorrect', trigger: 'blur'}
                    ],
                }
            }
        },
        methods: {
            submit() {
                // Call the validate method of the form component
                this.$refs.baseForm.validate().then(res => {
                    console.log(res)
                }).catch(e => {
                    console.log(e)
                })
            }
        }
    }
</script>

Next, let's implement the form item component, which is mainly used to place form elements and display error messages

<template>
    <label class="form-item">
        <div class="form-item_label">{{label}}</div>
        <div class="form-item_mn">
            <slot></slot>
        </div>
        <div class="form-item_error" v-if="errorMsg">{{errorMsg}}</div>
    </label>
</template>
<script>
    export default {
        name: "form-item",
        props: {
            label: String,
            prop: String
        },
        data() {
            return {
                errorMsg: ""
            }
        },
        methods: {
            showError(msg) {
                this.errorMsg = msg
            }
        }
    }
</script>

Then let's implement the form component

Get the reference of each XM form item through calcFormItems and save it in formItems
Expose the validate interface, internally call AsyncValidator, traverse the prop attribute of each form element in formItems according to the result, and process the corresponding error information

<template>
    <div class="form">
        <slot></slot>
    </div>
</template>
 
<script>
    import AsyncValidator from 'async-validator';
 
    export default {
        name: "xm-form",
        props: {
            model: {
                type: Object
            },
            rule: {
                type: Object,
                default: {}
            }
        },
        data() {
            return {
                formItems: []
            }
        },
        mounted() {
            this.calcFormItems()
        },
        updated() {
            this.calcFormItems()
        },
        methods: {
            calcFormItems() {
                // Get the reference of form item
                if (this.$slots.default) {
                    let children = this.$slots.default.filter(vnode => {
                        return vnode.tag &&
                            vnode.componentOptions && vnode.componentOptions.Ctor.options.name === 'form-item'
                    }).map(({componentInstance}) => componentInstance)
 
                    if (!(children.length === this.formItems.length && children.every((pane, index) => pane === this.formItems[index]))) {
                        this.formItems = children
                    }
                }
            },
            validate() {
                let validator = new AsyncValidator(this.rule);
 
                let isSuccess = true
 
                let findErrorByProp = (errors, prop) => {
                    return errors.find((error) => {
                        return error.field === prop
                    }) || ""
                }
 
                validator.validate(this.model, (errors, fields) => {
                    this.formItems.forEach(formItem => {
                        let prop = formItem.prop
                        let error = findErrorByProp(errors || [], prop)
                        if (error) {
                            isSuccess = false
                        }
 
                        formItem.showError(error && error.message || "")
                    })
                });
 
                return Promise.resolve(isSuccess)
            }
        }
    }
</script>

In this way, we have completed a general form verification component. From this example, we can see that obtaining component references is a very useful method in component development.

Encapsulating API components

Some components, such as prompt box and pop-up box, are more suitable for separate API calls, such as

import MessageBox from '@/components/MessageBox.vue'MessageBox.toast('hello)

How to implement this component that does not need to be manually embedded in the template? Originally, in addition to embedding components into the template to mount components to children, Vue also provides a method of manual mounting $mount for components

let component = new MessageBox().$mount()document.getElementById('app').appendChild(component.$el)

In this way, we can call components in the form of encapsulated API. The following is an interface encapsulation of alert message prompt

Message popup assembly

A message component is a component that specifies to draw and display prompt messages on the page. The following is a simple implementation

<template>
    <div class="alert">
        <div class="alert-main" v-for="item in notices" :key="item.name">
            <div class="alert-content">{{ item.content }}</div>
        </div>
    </div>
</template>
 
<script>
    let seed = 0;
 
    function getUuid() {
        return 'alert_' + (seed++);
    }
 
    export default {
        data() {
            return {
                notices: []
            }
        },
        methods: {
            add(notice) {
                const name = getUuid();
 
                let _notice = Object.assign({
                    name: name
                }, notice);
 
                this.notices.push(_notice);
 
                // Scheduled removal in seconds
                const duration = notice.duration;
                setTimeout(() => {
                    this.remove(name);
                }, duration * 1000);
            },
            remove(name) {
                const notices = this.notices;
 
                for (let i = 0; i < notices.length; i++) {
                    if (notices[i].name === name) {
                        this.notices.splice(i, 1);
                        break;
                    }
                }
            }
        }
    }
</script>

Let's implement the logic of attaching the message component to the page and expose the interface for displaying the message

import Vue from 'vue';
 
// Specific components
import Alert from './alert.vue';
Alert.newInstance = properties => {
    const props = properties || {};
    // Instantiate a component and mount it on the body
    const Instance = new Vue({
        data: props,
        render (h) {
            return h(Alert, {
                props: props
            });
        }
    });
    const component = Instance.$mount();
    document.body.appendChild(component.$el);
    // Maintain references to alert components through closures
    const alert = Instance.$children[0];
    return {
        // Two methods for external exposure of Alert components
        add (noticeProps) {
            alert.add(noticeProps);
        },
        remove (name) {
            alert.remove(name);
        }
    }
};
 
// Prompt single case
let messageInstance;
function getMessageInstance () {
    messageInstance = messageInstance || Alert.newInstance();
    return messageInstance;
}
function notice({ duration = 1.5, content = '' }) {
    // Instantiate the component when waiting for the interface call to avoid directly attaching it to the body when entering the page
    let instance = getMessageInstance();
    instance.add({
        content: content,
        duration: duration
    });
}
 
// Methods of external exposure
export default {
    info (options) {
        return notice(options);
    }
}

Then you can use the API to call the pop-up component

import alert from './alert.js'
// Direct use
alert.info({content: 'Message prompt', duration: 2})
// Or mount to the Vue prototype
Vue.prototype.$Alert = alert
// Then use it in the component
this.$Alert.info({content: 'Message prompt', duration: 2})

High order component

Higher order components can be seen as combinations in functional programming. A high-order component can be regarded as a function. It receives a component as a parameter and returns a function enhanced component.

High level component is a method to replace Mixin to realize the common functions of abstract components. It will not pollute the DOM (add unwanted div tags, etc.) due to the use of components, and can wrap any single child element, etc

In React, high-order components are commonly used as component encapsulation. How to implement high-order components in Vue?

In the render function of the component, you only need to return a vnode data type. If you do some processing in advance in the render function and return this$ slots. The vnode corresponding to default [0] can implement high-order components.

Built in keep alive

Vue has a built-in high-level component keep alive. You can find its implementation principle by viewing the source code. It is to realize the persistence of components by maintaining a cache and returning the cached vnode according to the key in the render function.

throttle

Throttling is a common requirement for dealing with events in web development. Common scenarios include timely search box to avoid frequent triggering of search interface, form button to prevent repeated submission in a short time, etc

First, let's take a look at the usage of the Throttle component, which receives two props

  • Time indicates the time interval of throttling
  • Events indicates the name of the event to be processed, and multiple events are separated by commas
    In the following example, the click event of its internal button is controlled through the Throttle component. At this time, click several times continuously, and the number of times to trigger clickBtn is smaller than the number of clicks (the throttling function is processed through a timer).
 <template>
    <div>
        <Throttle :time="1000" events="click">
            <button @click="clickBtn">click {{count}}</button>
        </Throttle>
    </div>
</template>

The following is the specific implementation. The main function of realizing high-order components is to process the vnode in the current slot in the render function

const throttle = function (fn, wait = 50, ctx) {
    let timer
    let lastCall = 0
    return function (...params) {
        const now = new Date().getTime()
        if (now - lastCall < wait) return
        lastCall = now
        fn.apply(ctx, params)
    }
}


 
export default {
    name: 'throttle',
    abstract: true,
    props: {
        time: Number,
        events: String,
    },
    created() {
        this.eventKeys = this.events.split(',')
        this.originMap = {}
        this.throttledMap = {}
    },
    // The render function directly returns the vnode of the slot to avoid adding wrapping elements to the outer layer
    render(h) {
        const vnode = this.$slots.default[0]
        this.eventKeys.forEach((key) => {
            const target = vnode.data.on[key]
            if (target === this.originMap[key] && this.throttledMap[key]) {
                vnode.data.on[key] = this.throttledMap[key]
            } else if (target) {
                // Replace the original event handler with the handler after throttle throttling
                this.originMap[key] = target
                this.throttledMap[key] = throttle(target, this.time, vnode)
                vnode.data.on[key] = this.throttledMap[key]
            }
        })
        return vnode
    },
}

We can further encapsulate and implement debounce components through debounce function. It can be seen that the role of high-order components is to enhance a component. For other applications of high-order components, you can refer to the application of hoc (high-order components) in vue.

Summary
This paper sorts out several techniques for implementing Vue components

  • Taking the counter component as an example, this paper shows the way to synchronize parent and child components through v-model syntax
  • Taking the form verification component as an example, this paper shows the method of encapsulating the component by obtaining the instance of the sub component
  • Taking the global pop-up component as an example, it shows the way to manually mount the component and encapsulate the API component
  • Taking the throttle throttling component as an example, this paper shows a way to implement high-order components in vue
    After understanding Vue's API, it's easy to understand the above concepts. Encapsulating components is more about JavaScript foundation than API proficiency. Vue entry is very easy, but it is also a lot of knowledge to write elegant Vue code.

Keywords: Vue

Added by beanfair on Mon, 03 Jan 2022 13:07:40 +0200