A component system is an important concept for Vue because it is an abstraction that allows us to build large applications from small, independent, and often reusable components. Almost any type of application interface can be abstracted into a tree of components. Componentalization can improve development efficiency, facilitate repeated use, simplify debugging steps, improve project maintainability, and facilitate multi-person collaborative development.

Componentized thinking

Imagine working on a project, opening a vUE file of 3000 lines, and opening another one of 4000 lines. Are you restless? A mouthful of old blood almost sprayed the screen.

How to design a good component?

This is a difficult question for me to answer, and everyone has their own opinion. The term “component” may not be limited to Vue components, but in a broader sense, front-end code modules, independent class libraries, and even functions should follow good rules when written. First, let’s look at what problems the presence of components solves and what benefits it has:

  • Better reuse
  • maintainability
  • scalability

Start with these three points and look at them from all angles.

High cohesion, low coupling

Six words of wisdom for programming. No matter what programming language, whether front-end or back-end, no matter how specific the design principle, is essentially the practice of this principle. In development, whether React or Vue, we used to divide components into business components and general components to achieve decoupling and improve reuse. When writing components or even functions, you should put together parts of the same functionality (e.g., the Vue3 composite API) and leave out as many irrelevant parts as possible. Imagine trying to change a function under one component only to find that it involves a bunch of other modules, or trying to reuse a component only to introduce a bunch of unrelated components. Did your blood pressure just go up to 200?

The principle of SOLID

SOLID is an acronym for five important Principles of Object-oriented design, and when we design classes and modules, following SOLID principles can make software more robust and stable. I think it also applies to component design.

  • Single Responsibility Principle (SRP)
  • Open and Closed Principle (OCP)
  • Richter’s Substitution Principle (LSP)
  • Interface Isolation Principle (ISP)
  • Dependency Inversion Principle (DIP)

Component design reference points

  • No side effects: Similar to pure functions, a component should be designed to have no side effects on the parent component so that references are transparent (multiple references do not affect the result).
  • Fault tolerance/default: in extreme cases, do not pass less than one parameter or fail to pass one parameter.
  • Granulation appropriate, moderate abstraction: This is a matter of experience, rational separation of components, single responsibility principle.
  • Configurable/extensible: Realize multi-scenario application and improve reusability. The internal implementation is too bad and the API is too bad.
  • Detailed documentation or comments/legibility: Code is not just for computers to execute, it is also for people to see. It’s convenient for you and me. You know!
  • Normalization: variable, method, file name naming specification, easy to understand, preferably code is a comment. You a big hump (UserInfo), a small hump (UserInfo), a skewer (user-info), you believe me or not I beat you to death.
  • Compatibility: There may be components written in different Vue versions on the same system. Why not switch to a smaller version?
  • Utilizing frame characteristicsThe Vue framework itself has some features that, when used well, can help us write more efficient code. Such asslotSlot,mixins; How sweet!

Component communication

props

The father passes value to the child

/ / the parent component
<HelloWorld msg="Welcome to Your Vue.js App"/>
/ / child component
props: { msg: String }
Copy the code

Custom events

The child passes value to the parent

/ / the parent component
<Cart @onAdd="cartAdd($event)"></Cart>
/ / child component
this.$emit('onAdd', data)
Copy the code

Event bus

Any two components between the value, Vue has implemented the corresponding interface, the following is the implementation principle, in fact, is a typical publish-subscribe model.

// Bus: event dispatch, listening, and callback management
class Bus {
  constructor() {
    this.callbacks = {}
  }

  $on (name, fn) {
    this.callbacks[name] = this.callbacks[name] || []
    this.callbacks[name].push(fn)
  }

  $emit (name, args) {
    if (this.callbacks[name]) {
      this.callbacks[name].forEach(cb= > cb(args))
    }
  }
}
// main.js
Vue.prototype.$bus = new Bus()
1 / / component
this.$bus.$on('add', handle)
2 / / component
this.$bus.$emit('add')
Copy the code

vuex

Passing values between any two components creates a unique global data manager store that manages data and notifies components of state changes. Vuex website

$parent/$roots

Sibling components can communicate with each other via a common ancestor bridge, $parent or $root.

// Sibling component 1
this.$parent.$on('foo', handle)
// Sibling component 2
this.$parent.$emit('foo')
Copy the code

$children

The parent component can communicate with the child component through $children.

/ / the parent component
this.$children[0].xx = 'xxx'
Copy the code

Note that $children does not guarantee order and is not responsive.

$attrs/$listenners

$attrs contains attribute bindings (except class and style) that are not recognized (and retrieved) as prop in the parent scope.

/ / the parent component<HelloWorld foo="foo"/>// Subcomponent: foo is not declared in props<p>{{ $attrs.foo }}</p>
Copy the code

The $listenners include V-on event listeners in the parent scope (without the.native modifier).

/ / the parent component<HelloWorld v-on:event-one="methodOne" />$listeners created() {console.log(this.$listeners) // {'event-one': f()}}Copy the code

$refs

An object that holds all DOM elements and component instances registered with ref Attributes.

<HelloWorld ref="hw" />
mounted() {
  this.$refs.hw.xx = 'xxx'
}
Copy the code

provide/inject

To allow an ancestor component to inject a dependency into all of its descendants.

// The ancestor component is provided
provide () {
  return { foo: 'bar'}}// Descendant component injection
inject: ['foo']
created () {
  console.log(this.foo) // => "bar"
}
Copy the code

slot

Slot syntax is a content distribution API implemented by Vue for composite component development. This technique is widely used in the development of common component library.

Anonymous slot

// comp
<div>
  <slot></slot>
</div> 

// parent 
<comp>hello</comp>
Copy the code

A named slot

Distributes content to the location specified by the child component

// comp2
<div>
  <slot></slot>
  <slot name="content"></slot>
</div>

// parent
<Comp2>
  <! -- Default slot with default parameter -->
  <template v-slot:default>A named slot</template>
  <! -- Named slot with slot name as parameter -->
  <template v-slot:content>Content...</template>
</Comp2>
Copy the code

Scope slot

Distributing content uses data from the child components

// comp3
<div>
  <slot :foo="foo"></slot>
</div>

// parent
<Comp3>
  <! Set the value of v-slot as a scoped context object -->
  <template v-slot:default="slotProps">Data from child components: {{slotProps. Foo}}</template>
</Comp3>
Copy the code

Implement the Alert plugin

In Vue, we can use Vue.component(tagName, options) for global registration, or we can use the Components option within the component for local registration.

Global components are mounted inVue.options.componentsUnder, while local components are mounted onvm.$options.componentsThis is why globally registered components can be used arbitrarily.

There are global components like Message, Toast, Loading, Notification, and Alert that are prototyped on top of the global Vue.

The following is to achieve a simple Alert component, mainly an understanding of the idea, the first effect diagram

@/components/alert/src/Alert.vue

<template>
  <transition name="fade">
    <div class="alert-box-wrapper" v-show="show">
      <div class="alert-box">
        <div class="alert-box-header">
          <div class="alert-box-title">{{ title }}</div>
          <div class="alert-box-headerbtn" @click="handleAction('close')">X</div>
        </div>
        <div class="alert-box-content">
          <div class="alert-box-container">{{ message }}</div>
        </div>
        <div class="alert-box-btns">
          <button class="cancel-btn"  @click="handleAction('cancel')">{{ cancelText }}</button>
          <button class="confirm-btn"  @click="handleAction('confirm')">{{ confirmText }}</button>
        </div>
      </div>
    </div>
  </transition>
</template>

<script>
export default {
  name: 'Alert',
  data () {
    return {
      title: 'title'.message: 'Here's a hint.'.show: false.callback: null.cancelText: 'cancel'.confirmText: 'sure'}},methods: {
    handleAction (action) {
      this.callback(action)
      this.destroyVm()
    },
    destroyVm () { / / destroy
      this.show = false
      setTimeout(() = > {
        this.$destroy(true)
        this.$el && this.$el.parentNode.removeChild(this.$el)
      }, 500)}}}</script>

<style lang="less" scoped>
.fade-enter-active..fade-leave-active {
  transition: opacity .3s;
}
.fade-enter..fade-leave-to  {
  opacity: 0;
}

.alert-box-wrapper {
  position: fixed;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  display: flex;
  justify-content: center;
  align-items: center;
  background: rgba(0.0.0.0.5);
  .alert-box {
    display: inline-block;
    width: 420px;
    padding-bottom: 10px;
    background-color: #fff;
    border-radius: 4px;
    border: 1px solid # 303133;
    font-size: 16px;
    text-align: left;
    overflow: hidden;
    .alert-box-header {
      position: relative;
      padding: 15px;
      padding-bottom: 10px;
      .alert-box-title {
        color: # 303133;
      }
      .alert-box-headerbtn {
        position: absolute;
        top: 15px;
        right: 15px;
        cursor: pointer;
        color: # 909399; }}.alert-box-content {
      padding: 10px 15px;
      color: # 606266;
      font-size: 14px;
    }
    .alert-box-btns {
      padding: 5px 15px 0;
      text-align: right;
      .cancel-btn {
        padding: 5px 15px;
        background: #fff;
        border: 1px solid #dcdfe6;
        border-radius: 4px;
        outline: none;
        cursor: pointer;
      }
      .confirm-btn {
        margin-left: 6px;
        padding: 5px 15px;
        color: #fff;
        background-color: #409eff;
        border: 1px solid #409eff;
        border-radius: 4px;
        outline: none;
        cursor: pointer; }}}}</style>
Copy the code

@/components/alert/index.js

import Alert from './src/Alert'

export default {
  install (Vue) {
    // Create constructor class
    const AlertConstructor = Vue.extend(Alert)

    const showNextAlert = function (args) {
      // Instantiate the component
      const instance = new AlertConstructor({
        el: document.createElement('div')})// Set the callback function
      instance.callback = function (action) {
        if (action === 'confirm') {
          args.resolve(action)
        } else if (action === 'cancel' || action === 'close') {
          args.reject(action)
        }
      }
      // Process parameters
      for (const prop in args.options) {
        instance[prop] = args.options[prop]
      }
      / / into the Body
      document.body.appendChild(instance.$el)
      Vue.nextTick(() = > {
        instance.show = true})}const alertFun = function (options) {
      if (typeof options === 'string' || options === 'number') {
        options = {
          message: options
        }
        if (typeof arguments[1= = ='string') {
          options.title = arguments[1]}}return new Promise((resolve, reject) = > {
        showNextAlert({
          options,
          resolve: resolve,
          reject: reject
        })
      })
    }

    Vue.prototype.$alert = alertFun
  }
}
Copy the code

@/main.js

import Alert from '@/components/alert'
Vue.use(Alert)
Copy the code

use

this.$alert({
  message: 'Description description Description'.title: 'tip'.cancelText: 'no'.confirmText: 'good'
}).then(action= > {
  console.log(` clicked${action}`)
}).catch(action= > {
  console.log(` clicked${action}`)})/ / or
this.$alert('Description description Description'.'tip').then(action= > {
  console.log(` clicked${action}`)
}).catch(action= > {
  console.log(` clicked${action}`)})Copy the code