What does vue-create-API do?

Described in readme. md, a plug-in that allows Vue components to be called via an API. (vue-create-API source code address)

Install and use

There are currently two installations available: install vue-create-api via NPM, or import JS static resource files.

An example usage is provided in readme.md as follows:

import CreateAPI from 'vue-create-api'

Vue.use(CreateAPI)

Vue.use(CreateAPI, {
  componentPrefix: 'cube-'
  apiPrefix: '$create-'
})

import Dialog from './components/dialog.vue'

Vue.createAPI(Dialog, true)

Dialog.$create({
  $props: {
    title: 'Hello'.content: 'I am from pure JS'
  }
}).show()

this.$createDialog({
  $props: {
    title: 'Hello'.content: 'I am from a vue component'
  }
}).show()
Copy the code

The vuE-create-API plugin is introduced. When installing the plugin, you can set componentPrefix and apiPrefix, and add a createAPI method under the vue constructor. Import the Dialog component, call createAPI to produce the corresponding API, and mount it to vue. prototype and Dialog objects. It can then be created and used in a Vue component by calling this, or in a JS file called $create.

directory

The file name instructions
creator Create components
debug Error message
index The main entrance
instantiate instantiation
parse Parameter Settings
util Tool library

Next, we will start from the entrance analysis, in-depth understanding of its principle and implementation process.

The entrance

If the Vue plug-in is an object, the install method must be provided. If the plug-in is a function, this function is used as the install method. When the install method is called, Vue is passed in as an argument. The install method for vue-create-api is defined in the SRC /index.js file:

import { camelize, escapeReg, isBoolean } from './util'
import { assert, warn } from './debug'
import apiCreator from './creator'
import instantiateComponent from './instantiate'

function install(Vue, options = {}) {
  const {componentPrefix = ' ', apiPrefix = '$create-'} = options

  Vue.createAPI = function (Component, events, single) {
    if (isBoolean(events)) {
      single = events
      events = []
    }
    const api = apiCreator.call(this, Component, events, single)
    const createName = processComponentName(Component, {
      componentPrefix,
      apiPrefix,
    })
    Vue.prototype[createName] = Component.$create = api.create
    return api
  }
}
Copy the code

The install method provides the options configuration parameter. ComponentPrefix is the component name prefix, which is ignored by the generated API. ApiPrefix is added to the generated API.

The vue. createAPI method is defined in the body of the method and provides a Component with three parameters, an array of events, and a singleton instantiation Component. Events can pass Boolean or Array values. In this example events is true. According to the code logic, when events is Boolean, single = events so single is true and events is assigned the value [].

The API object is obtained through the apiCreator method, which has two internal methods, before and create. The reason call is used here is to point this to the Vue class. The path to the code file is in SRC/Creator. Js, which is covered in detail after the implementation logic, but we’ll move on.

Get the crateName property name using the processComponentName method, assign api.create to Component.$CREATE and Vue.prototype[createName], and return the API. This is how this.$createDialog() and dialog.$create() are implemented in the example above.

The processComponentName method is very simple, with the following code:

function processComponentName(Component, options) {
  const {componentPrefix, apiPrefix} = options
  const name = Component.name
  assert(name, 'Component must have name while using create-api! ')
  const prefixReg = new RegExp(` ^${escapeReg(componentPrefix)}`.'i')
  const pureName = name.replace(prefixReg, ' ')
  let camelizeName = `${camelize(`${apiPrefix}${pureName}`)}`
  return camelizeName
}
Copy the code

The purpose of this code is to match the clipped concatenation string, and finally return the camelizeName value. Note that we use component. name, and check if name is defined. The component must define a name.

Create API

The file path is SRC /creator. Js, and the code is quite large. For ease of reading, I will explain it in sections according to main logic:

import instantiateComponent from './instantiate'
import parseRenderData from './parse'
import { isFunction, isUndef, isStr } from './util'

const eventBeforeDestroy = 'hook:beforeDestroy'

export default function apiCreator(Component, events = [], single = false) {
  let Vue = this
  let currentSingleComp
  let singleMap = {}
  const beforeHooks = []

  ...

  const api = {
    before(hook) {
      beforeHooks.push(hook)
    },
    create(config, renderFn, _single) {
      if(! isFunction(renderFn) && isUndef(_single)) { _single = renderFn renderFn =null
      }

      if (isUndef(_single)) {
        _single = single
      }

      const ownerInstance = this
      constisInVueInstance = !! ownerInstance.$onlet options = {}

      if (isInVueInstance) {
        // Set parent to store router i18n ...
        options.parent = ownerInstance
        if(! ownerInstance.__unwatchFns__) { ownerInstance.__unwatchFns__ = [] } }const renderData = parseRenderData(config, events)

      let component = null

      processProps(ownerInstance, renderData, isInVueInstance, (newProps) => {
        component && component.$updateProps(newProps)
      })
      processEvents(renderData, ownerInstance)
      process$(renderData)

      component = createComponent(renderData, renderFn, options, _single)

      if (isInVueInstance) {
        ownerInstance.$on(eventBeforeDestroy, beforeDestroy)
      }

      function beforeDestroy() {
        cancelWatchProps(ownerInstance)
        component.remove()
        component = null
      }

      return component
    }
  }

  return api
}
Copy the code

This js file is the core of vue-create-API, which contains operations such as parsing render data, event attribute listening, and creating components, which I will explain in detail.

The apiCreator function takes three arguments, Component, events, and single. This is consistent with the createAPI. First Vue = this, here’s this point to this class is a Vue, Vue source code in the SRC/core/instance/index in js, as shown below:

function Vue (options) {
  if(process.env.NODE_ENV ! = ='production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')}this._init(options)
}
Copy the code

The new Vue operation in our development is to instantiate this object method. Within the body of the method, the this._init method is executed for initialization, such as life cycles, events, rendering, and so on.

Again, defining the variables currentSingleComp, singleMap, and beforeHooks will be covered later. Let’s take a look at what the const API defines. It provides before and create methods.

Before provides a hook argument, which is a hook function that uses the beforeHooks array defined at the beginning of the method body to add hooks to that array. As we can guess from the name definition, these functions are defined when the component is initialized, and this method can be used for some kind of restriction.

Create provides three parameters: config configuration, renderFn for generating child VNodes, and _single singleton. If renderFn is not a function and _single is undefined, _single = renderFn, renderFn = null If _single is undefined, _single = single.

Const ownerInstance = this where the this context refers to the caller. This.$createDialog() this points to a vue instance. If dialog. $create() is used, this points to a Dialog object. The latter is false. Ownerinstance. __unwatchFns__ Is used to listen for Prop changes. So there is no way to make Prop responsive when creating and using instances of components like dialog.$create().

The render data is obtained through the parseRenderData method, which is implemented later.

The processProps, processEvents, and Process $methods listen for parameters, events, and parameter objects, respectively.

The createComponent method creates an instance of the component and returns the example. There is one piece of code to note, as follows

if (isInVueInstance) {
  ownerInstance.$on(eventBeforeDestroy, beforeDestroy)
}

function beforeDestroy() {
  cancelWatchProps(ownerInstance)
  component.remove()
  component = null
}
Copy the code

Determines whether the component is used in Vue and, if so, binds it with a beforeDestroy event hook that empties and destroys listening event properties and instances.

  • Note: This method will not work for server rendering (SSR).

Next, we’ll step through parsing render data, event attribute listening, and how the creation component is implemented.

Parsing render data

The file path is SRC /parse.js and the code is as follows:

import { camelize } from './util'

export default function parseRenderData(data = {}, events = {}) {
  events = parseEvents(events)
  constprops = {... data}const on = {}
  for (const name in events) {
    if (events.hasOwnProperty(name)) {
      const handlerName = events[name]
      if (props[handlerName]) {
        on[name] = props[handlerName]
        delete props[handlerName]
      }
    }
  }
  return {
    props,
    on
  }
}

function parseEvents(events) {
  const parsedEvents = {}
  events.forEach((name) = > {
    parsedEvents[name] = camelize(`on-${name}`)})return parsedEvents
}
Copy the code

This method provides two parameters, the first of which, data, is passed when the component is created. The second parameter is events defined when createAPI is called.

So the data argument has two forms.

{$props, $events} {$props, $events} {$props, $events} {$props, $events} {$props, $events}} $events is the component’s event callback. Here’s an example:

this.$createDialog({
  $props: {
    title: 'Hello'.content: 'I am from a vue component'
  },
  $event: {
    change: (a)= > {}
  }
}).show()
Copy the code

The second way to pass a value is to put the parameters of $props directly into an object, such as {title, content}. What if this structure wants to listen for events?

Camelize (on-${name}); camelize(on-${name}); camelize(on-${name}); The events loop checks if there is an argument in data that starts with on*. If the match is successful, it assigns the value to the ON object and returns it with props.

So if you want to listen for events in the second way, you define it as follows:

Vue.createAPI(Dialog, ['change'])

this.$createDialog({
  title: 'Hello'.content: 'I am from a vue component'.onChange: (a)= > {}
}).show()
Copy the code
  • Note: Much of this code is to support configurationon*Event listening. This can be optimized if the user does not have such a requirement.

Event attribute listening

The file path is still SRC /creator. Js, and the processProps method is as follows:

  function processProps(ownerInstance, renderData, isInVueInstance, onChange) {
    const $props = renderData.props.$props
    if ($props) {
      delete renderData.props.$props

      const watchKeys = []
      const watchPropKeys = []
      Object.keys($props).forEach((key) = > {
        const propKey = $props[key]
        if (isStr(propKey) && propKey in ownerInstance) {
          // get instance value
          renderData.props[key] = ownerInstance[propKey]
          watchKeys.push(key)
          watchPropKeys.push(propKey)
        } else {
          renderData.props[key] = propKey
        }
      })
      if (isInVueInstance) {
        const unwatchFn = ownerInstance.$watch(function () {
          const props = {}
          watchKeys.forEach((key, i) = > {
            props[key] = ownerInstance[watchPropKeys[i]]
          })
          return props
        }, onChange)
        ownerInstance.__unwatchFns__.push(unwatchFn)
      }
    }
  }
Copy the code

The main purpose of this method is to respond to and store data. It takes four parameters: ownerInstance creator instance object, renderData rendered data object, isInVueInstance to determine whether it was created within the vue component, and onChange a callback function.

If $props is provided in the render data, the method will proceed when the user sets the $props property.

The watchKeys and watchPropKeys store data and parameter keys that need to listen for updates. Loop over the key of $props and get the propKey value of the corresponding key. Next there is an important criterion isStr(propKey) && propKey in ownerInstance to determine whether the propKey is a string and whether the property is in the ownerInstance object or its prototype chain. If true, store the corresponding value of the instance in renderData and store the key in the Watch array.

$watch [watchPropKeys[I]] is called whenever ownerInstance[watchPropKeys[I]] changes. $updateProps This method is defined in SRC /instantiate.js:

  component.$updateProps = function (props) {
    Object.assign(renderData.props, props)
    instance.$forceUpdate()
  }
Copy the code

$forceUpdate causes the Vue instance to be re-rendered for the new data for props.

  • Note:

  • 1) When the developer uses the plug-in to update data, the value of the attribute to be updated should be a string, corresponding to the data object of the Vue instance.

  • 2) According to source code analysis, instances not created in Vue cannot be updated. This is also explained in README. After analyzing the source code, let’s understand the real reason.

Next we analyze the processEvents method, which looks like this:

  function processEvents(renderData, ownerInstance) {
    const $events = renderData.props.$events
    if ($events) {
      delete renderData.props.$events

      Object.keys($events).forEach((event) = > {
        let eventHandler = $events[event]
        if (typeof eventHandler === 'string') {
          eventHandler = ownerInstance[eventHandler]
        }
        renderData.on[event] = eventHandler
      })
    }
  }
Copy the code

This method primarily listens for user-bound callback events to trigger. It takes two arguments, renderData and ownerInstance.

It first checks if $events is provided in the render data, so when the user sets the $events property, the method continues.

$events key = eventHandler $events key = eventHandler $events key = eventHandler Get the function corresponding to this property in the instance and assign it to eventHandler, and finally to renderData.

Next we examine the process$method, which looks like this:

 function process$(renderData) {
    const props = renderData.props
    Object.keys(props).forEach((prop) = > {
      if (prop.charAt(0) = = ='$') {
        renderData[prop.slice(1)] = props[prop]
        delete props[prop]
      }
    })
  }
Copy the code

This method allows users to set the $XXX configuration, which makes it more flexible to use. For example, if you want to set the component to an additional className, you can set it to $class: ‘my-class’, the method body will iterate over whether the first argument is $, and then store the data in renderData, which will be rendered later.

Create components

The file path is still SRC /creator. Js, with the following code:

  function createComponent(renderData, renderFn, options, single) {
    beforeHooks.forEach((before) = > {
      before(renderData, renderFn, single)
    })
    const ownerInsUid = options.parent ? options.parent._uid : - 1
    const {comp, ins} = singleMap[ownerInsUid] ? singleMap[ownerInsUid] : {}
    if (single && comp && ins) {
      ins.updateRenderData(renderData, renderFn)
      ins.$forceUpdate()
      currentSingleComp = comp
      return comp
    }
    const component = instantiateComponent(Vue, Component, renderData, renderFn, options)
    const instance = component.$parent
    const originRemove = component.remove

    component.remove = function () {
      if (single) {
        if(! singleMap[ownerInsUid]) {return
        }
        singleMap[ownerInsUid] = null
      }
      originRemove && originRemove.apply(this.arguments)
      instance.destroy()
    }

    const originShow = component.show
    component.show = function () {
      originShow && originShow.apply(this.arguments)
      return this
    }

    const originHide = component.hide
    component.hide = function () {
      originHide && originHide.apply(this.arguments)
      return this
    }

    if (single) {
      singleMap[ownerInsUid] = {
        comp: component,
        ins: instance
      }
      currentSingleComp = comp
    }
    return component
  }

Copy the code

This method takes four parameters, renderData, which has been processed for rendering, renderFn, which is used to generate child VNodes, Options component instances, and whether single is a singleton.

  beforeHooks.forEach((before) = > {
    before(renderData, renderFn, single)
  })
Copy the code

First loop beforeHooks to get the method that is bound when vue.createAPI is called, and if before is set, this method is executed first on every call.

  const ownerInsUid = options.parent ? options.parent._uid : - 1
  const {comp, ins} = singleMap[ownerInsUid] ? singleMap[ownerInsUid] : {}
  if (single && comp && ins) {
    ins.updateRenderData(renderData, renderFn)
    ins.$forceUpdate()
    currentSingleComp = comp
    return comp
  }
  const component = instantiateComponent(Vue, Component, renderData, renderFn, options)
  const instance = component.$parent

  ...

  if (single) {
    singleMap[ownerInsUid] = {
      comp: component,
      ins: instance
    }
    currentSingleComp = comp
  }
Copy the code

Part of the effect is that the component uses the singleton pattern. Define the ownerInsUid as the unique identifier of the current instance. If options.parent exists, get the unique identifier of the Vue component, _uid, or -1.

Check whether singleMap[ownerInsUid] exists. If so, get comp and INS. Then check whether signle, COMp, ins exist or are true.

The updateRenderData method updates render data and callback methods. The $forceUpdate method rerenders the current instance.

InstantiateComponent Is a method for creating a component instance, described here later.

If single is true, it is stored in the singleMap object with ownerInsUid as the key. The value is an object. Comp corresponds to Component, which is the instance of the current component, and INS corresponds to the parent instance component.$parent.

  const originRemove = component.remove
  component.remove = function () {
    if (single) {
      if(! singleMap[ownerInsUid]) {return
      }
      singleMap[ownerInsUid] = null
    }
    originRemove && originRemove.apply(this.arguments)
    instance.destroy()
  }

  const originShow = component.show
  component.show = function () {
    originShow && originShow.apply(this.arguments)
    return this
  }

  const originHide = component.hide
  component.hide = function () {
    originHide && originHide.apply(this.arguments)
    return this
  }
Copy the code

There are three methods added to the component: remove, show, and hide.

Remove: Checks whether the current singleton is a singleton and deletes the corresponding value in singleMap. Determine if the component has the remove method set, execute with the apply method, and finally destroy the parent instance.

The show and hide methods are similar in that they return the current component instance.

InstantiateComponent method (SRC /instantiate.js)

export default function instantiateComponent(Vue, Component, data, renderFn, options) {
  let renderData
  let childrenRenderFn

  const instance = newVue({ ... options, render(createElement) {let children = childrenRenderFn && childrenRenderFn(createElement)
      if (children && !Array.isArray(children)) {
        children = [children]
      }

      returncreateElement(Component, {... renderData}, children || []) },methods: {
      init() {
        document.body.appendChild(this.$el)
      },
      destroy() {
        this.$destroy()
        document.body.removeChild(this.$el)
      }
    }
  })
  instance.updateRenderData = function (data, render) {
    renderData = data
    childrenRenderFn = render
  }
  instance.updateRenderData(data, renderFn)
  instance.$mount()
  instance.init()
  const component = instance.$children[0]
  component.$updateProps = function (props) {
    Object.assign(renderData.props, props)
    instance.$forceUpdate()
  }
  return component
}

Copy the code

This method contains five parameters: Vue class, Component, data Component parameters and callback events. RenderFn is used to generate child VNode nodes and Options Component instances.

Create a Vue instance new Vue. Add a parent component instance to it by deconstructing options.

The Render method is an alternative to a string template, and createElement creates a VNode. First determine the childrenRenderFn value, which is set to renderFn to generate the child VNode. Pass createElement if it exists. This returns the createElement method, which you can check out later in the vUE documentation if you’re not familiar with it. When it comes to the childrenRenderFn method, the plug-in can be configured as follows:

this.$createDialog({
  $props: {
    title: 'Hello'.content: 'I am from a vue component'
  }
}, createElement => {
  return [
    createElement('p'.'other content')
  ]
}).show()
Copy the code

Next, we define two methods init and deStory. The init method adds the root DOM element used by the Vue instance to the body, and the destory method removes it and destroys it.

UpdateRenderData renders data for updates.

$mount Manually mounts an unmounted instance. That is, without calling the method, there is no $EL in the Vue instance.

Instance.$children[0] gets the component instance, binds the $updateProps method, and returns the component instance.

conclusion

At this point, the core code of the VUE-create-API plug-in and the entire operation process are covered. There are two important reasons to share this plug-in source analysis.

First, the author is Huang Yi, I have read Teacher Huang’s “Vue.js technology reveal” to learn a lot of knowledge, this plug-in is also done by Teacher Huang himself, is attracted by the reputation.

Two, the code itself, read the source code can see the author thinking clearly, the implementation process is not messy, concise language is worth repeated reading.

Finally, the blog guide version of this article is attached for your collection and reading. Or scan the code to follow the wechat public account [Incredible front end], and share technical dry goods, best practices and interesting front end skills with you every month.