Follow me on my blog shymean.com

Having written Vue for a long time, you should also think and rethink the proper way to encapsulate components in addition to regular business development. Take the popover component as an example, one implementation is to introduce the components that need to be displayed in the need template, and then control the components of the popover through a flag variable. The business code will be filled with redundant popover component logic, which is very inelegant.

This article has organized some tips for developing Vue components, including numerous code examples.

The development environment

Vue-cli3 provides very convenient functionality to quickly write some test demos and is a necessary environment for developing components. Here are the installation steps

Vue -cli3 NPM install -g@vue /cli vue -v // Check whether the version is 3.x // Install the extension, You can quickly start a single vue file NPM install -g@vue /cli-service-global // Quickly start the demo file vue serve demo.vueCopy the code

If SCSS is required, you also need to install sass-Loader in the directory.

The following are some of the problems you may encounter with vuE-Cli3, and more tutorials are available: a super detailed vue-Cli3.0 tutorial.

Custom entry file

If you need to (for example, if you need to develop components for mobile), you can customize the HTML entry file when using Vue Serve, write index.html in the root directory, and make sure the page contains the dom of #app.

Introduce public mixed files

Introduce common style blends in each file via style-resources-loader, etc., see automated imports

You need access to Vue global objects

There are times when global Vue objects need to be exposed, such as when developing global directives or plug-ins

import Vue from "vue" import somePlugin from ".. /src/somePlugin" Vue.use(somePlugin)Copy the code

This does not work because vue serves XXX. Vue is only used as a rapid prototyping solution, and the vUE used is not the same object as the vUE imported by import. One solution is to manually specify the entry file for vue Serve

// index.js
import Vue from ".. /node_modules/vue/dist/vue.min"
import placeholder from ".. /src/placeholder/placeholder"

Vue.use(placeholder)

new Vue({
    el: "#app".template: ` `,
    created(){},
})
Copy the code

Vue component system

The Vue component API consists of three parts: Prop, Event, and Slot

  • Props represents the parameters that the component receives. It is best written as an object. This allows you to set the type, default value, or custom value of the validation attribute for each attribute
  • Slot can dynamically insert some content or components into a component, which is an important way to implement higher-order components. When multiple slots are required, named slots can be used
  • Events are an important way for children to pass messages to their parents

Unidirectional data flow

Reference: One-way data Flow – Official documentation.

Updates to the parent prop flow down to the child components, but not the other way around

One-way data flow is an obvious feature of Vue components, and the value of props should not be modified directly in child components

  • If the prop passed is for presentation only and does not involve modification, use it directly in the template
  • If you need to convert the value of a prop and then display it, you should use computed properties
  • If the value of prop is used for initialization, you should define a child component’s data property and use prop as its initial value

From source/SRC/core/vdom/create – component. Js and/SRC/core/vdom/helpers/extract – props. In js, can be seen in the treatment of the props of values, from the first

function extractPropsFromVNodeData(){
  const res = {}
  const { attrs, props } = data
  // Execute shallow copy
  checkProp(res, props, key, altKey, true) || checkProp(res, attrs, key, altKey, false)

  return res
}

Copy the code

In child components modified props, but not modify the parent component, this is because the extractPropsFromVNodeData is through a shallow copy pass attrs props.

Shallow copying means that changes to the props of objects and arrays in the child component still affect the parent component, which violates the design of one-way data flow. So you need to avoid that.

Communication between components

Here can refer to: VUE component communication full disclosure, write more comprehensive

  • The parent-child component relationship can be summarized as prop passing down and events passing up
  • Data transfer between ancestor and descendant components (across multiple generations) can be implemented using provide and Inject

In addition, if you need to communicate across components or between sibling components, you can do so through eventBus or VUex.

“Bypass” one-way data flows

Consider a scenario where a parent component passes data to a child component in the form of a prop, and the child component performs operations and changes the data, requiring modification of the parent component’s prop value (a typical example would be the item counter component of a shopping cart).

According to the component one-way data flow and event communication mechanism, the child component needs to notify the parent component through the event, and modify the original PROP data in the parent component to complete the state update. It is also common in business to modify the parent’s data in a child component, so what is the way to “circumvent” the limitation of one-way data flow?

State of ascension

You can refer to the React state promotion to pass the data processing logic of the parent element to the child components using props. The child components can 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>
Copy the code

The event handler is then passed in when called

<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>
Copy the code

Obviously, since on-minus and on-Plus need to be implemented in each parent component, state improvement doesn’t solve the problem at all.

V – model syntactic sugar

Vue is built with the V-model directive, which is a syntax candy that can be decomposed into props: Value and events: input. This means that the component provides a prop named Value and a custom event named input. If these two conditions are met, the user can use the V-Model on the custom 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 the input event}}};</script>

Copy the code

Then the call just needs to pass in the V-model directive

<counter v-model="counerVal"/>
Copy the code

Using the V-Model, you can easily synchronize data from the parent component in the child component. In versions after 2.2, it is possible to customize prop and event names for V-Model directives, refer to model configuration items

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

Get a reference to the component instance

Getting component instances is a very useful way to develop components. Components can get VM instance references via $refs, $parents, $children, and so on

  • $refs adds the ref attribute to the component (or dom)

  • $parents gets the parent node to which the child component is mounted

  • $children gets all the child nodes of the component

These interfaces return vNodes. You can obtain the corresponding component instance through vnode.componentInstance, and then directly call the component’s methods or access data. Although this approach somewhat violates the design philosophy of components and increases the cost of coupling between components, the code implementation is much cleaner.

Form validation component

In general, form validation is a very common application scenario before a form is submitted. So how do you encapsulate form validation functionality inside a component?

Here is an example of a form component that shows form validation by obtaining a reference to the component.

First define how the component will be used,

  • xm-formreceivemodelandruleTwo prop
    • modelRepresents the data object bound to the form, which is the last object submitted to the form
    • ruleRepresents a validation rule policy that forms validation can useasync-validatorThe plug-in
  • xm-form-itemThe receivedpropProperty corresponding to a key of the Model and rule of the form component, based on which the form data is fetched from the Model and the validation rule from the rule

Here is the sample code to use it

<template>
    <div class="page">
        <xm-form :model="form" :rule="rule" ref="baseForm">
            <xm-form-item label="Name" prop="name">
                <input v-model="form.name"/>
            </xm-form-item>
            <xm-form-item label="Email" 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: 'Email format is not correct'.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>
Copy the code

Next, let’s implement the form-item component, whose main purpose is 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>
Copy the code

Then let’s implement the Form component

  • throughcalcFormItemsFor eachxm-form-itemIs stored in formItems
  • Exposes the validate interface, internally calls AsyncValidator, and iterates through the result of each form element in the formItemspropProperty to handle the corresponding error message
<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 a reference to 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>
Copy the code

This completes a generic form validation component. As you can see from this example, getting a component reference is a very useful method in component development.

Encapsulating API components

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

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

How do you implement a component that doesn’t need to be embedded in a template manually? It turns out that in addition to mounting children components by embedding components in templates, Vue also provides a manual mount method for components: $mount

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

In this way, we can encapsulate the API form of the calling component. Here is an interface encapsulation of the alert message

Message popover assembly

A message component is a component that specifies how to draw display prompt messages on a page. Here 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((a)= > {
                    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>
Copy the code

Let’s implement the logic of mounting the message component to the page and exposing the interface to display the message

// alert.js
import Vue from 'vue';

// Specific components
import Alert from './alert.vue';
Alert.newInstance = properties= > {
    const props = properties || {};
	// Instantiate a component and mount it to 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 a reference to the Alert component through a closure
    const alert = Instance.$children[0];
    return {
        // The Alert component exposes two methodsadd (noticeProps) { alert.add(noticeProps); }, remove (name) { alert.remove(name); }}};// Indicate a singleton
let messageInstance;
function getMessageInstance () {
    messageInstance = messageInstance || Alert.newInstance();
    return messageInstance;
}
function notice({ duration = 1.5, content = ' ' }) {
    // Wait for the interface to call before instantiating the component to avoid mounting the body directly on the page
    let instance = getMessageInstance();
    instance.add({
        content: content,
        duration: duration
    });
}

// External exposure method
export default {
    info (options) {
        returnnotice(options); }}Copy the code

You can then call the popover component using the API

import alert from './alert.js'
// Use it directly
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})
Copy the code

High order component

Higher-order components can be thought of as combinations in functional programming. A higher-order component can be thought of as a function that takes a component as an argument and returns an enhanced component.

Higher-order components are a way of taking over from mixins to implement the common functionality of abstract components without contaminating the DOM (adding unwanted div tags, etc.), wrapping arbitrary single child elements, and so on

High-order components are commonly used in React. How to implement high-order components in Vue?

In the render function of the component, we only need to return a vNode data type. If we do some pre-processing in the render function and return the vNode corresponding to this.$slot.default [0], we can implement the higher-order component.

The built-in keep alive

Vue built-in a high-order component keep-alive, check the source code can find its implementation principle, is by maintaining a cache, and in the render function according to the key to return the cache vnode, to achieve component persistence.

throttle

Throttling is a common requirement for handling events in Web development. Common scenarios include timely search box to avoid frequent triggering of search interface and form button to prevent repeated submission by mistake in a short time

First, let’s look at how the Throttle component is used, receiving two props

  • timeRepresents the time interval for throttling
  • eventsRepresents the name of the event to be processed. Multiple events are separated by commas

In the example below, the Throttle component controls the click events of its internal button, where multiple clicks in a row trigger clickBtn fewer times than clicks (the throttling function is handled by a timer).

 <template>
    <div>
        <Throttle :time="1000" events="click">
            <button @click="clickBtn">click {{count}}</button>
        </Throttle>
    </div>
</template>
Copy the code

The main function of the higher-order component 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 returns the slot's vnode directly, avoiding wrapping elements
    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 a throttle-throttled handler
                this.originMap[key] = target
                this.throttledMap[key] = throttle(target, this.time, vnode)
                vnode.data.on[key] = this.throttledMap[key]
            }
        })
        return vnode
    },
}
Copy the code

We can further encapsulate the debounce component by implementing it through the debounce function, so that the higher-order component is there to enhance a component. For other applications of higher-order components, see the use of HOC(higher-order Components) in VUE.

summary

This article has organized several techniques for implementing Vue components

  • Take the Counter counter component as an example to show how to sugar-synchronize parent and child components with the V-Model syntax
  • Using a form validation component as an example, we show how to encapsulate a component by getting an instance of a child component
  • Taking the global popover component as an example, it shows how to manually mount the component to encapsulate the API component
  • Taking throttle-throttling component as an example, this paper shows a way to implement higher-order components in VUE

After understanding the Vue API, it is easy to understand the above concepts, packaging components, in addition to API proficiency, more to look at JavaScript fundamentals. Getting started with Vue is easy, but writing elegant Vue code is no small feat.