Talking about Applet Maintenance from VantComponent

Keywords: Javascript Vue React

When developing applets, we always expect to use the technical specifications and grammatical features of the past to write the current applet, so there will be a variety of applet frameworks, such as mpvue, taro and other compiled frameworks.Of course, these frameworks themselves are helpful for newly developed projects.And what about old projects that we want to maintain using the grammatical features of vue?
Here I'm going to look at youzan's vant-weapp.It was found that the components in the project were written this way.

import { VantComponent } from '../common/component';

VantComponent({
  mixins: [],
  props: {
    name: String,
    size: String
  },
  // You can use watch es to monitor props changes
  // It's actually extracting observer s from properties
  watch: {
    name(newVal) {
       ...
    },
    // Strings can be used directly instead of function calls
    size: 'changeSize'
  },
  // Get data using computed attributes, which can be used directly in wxml
  computed: {
    bigSize() {
      return this.data.size + 100
    }
  },
  data: {
    size: 0
  },
  methods: {
    onClick() {
      this.$emit('click');
    },
    changeSize(size) {
       // Use set
       this.set(size)
    }
  },

  // Corresponding applet component created cycle
  beforeCreate() {},

  // Corresponding applet component attached cycle
  created() {},

  // Corresponding applet component read cycle
  mounted() {},

  // Corresponding applet component detached cycle
  destroyed: {}
});

I found that the component is written in a way that resembles the Vue syntax as a whole.It does not compile itself.It seems the problem lies in the method of importing VantComponet.Let's start by detailing how to use VantComponet to maintain older projects.

TLDR (don't talk a lot, just conclude first)

The widget components are not described here.Here we give the code style for writing Page s using VantComponent.

import { VantComponent } from '../common/component'; 

VantComponent({
  mixins: [],
  props: {
    a: String,
    b: Number
  },
  // Watch is basically useless here on the page, because only watch changes props, page does not change props
  // The reason is detailed later
  watch: {},
  // Calculated properties are still available
  computed: {
    d() {
      return c++
    }
  },
  methods: {
    onLoad() {}
  },
  created() {},
  // Other component life cycle
})

Here you may be wondering, is VantComponet not effective for Component?How does this work for Page pages?In fact, we can use components to construct applet pages.
In the official documents, we can see that Using Component Constructor to Construct Pages
In fact, the page of the applet can also be viewed as a custom component.As a result, pages can also be constructed using Component constructors with the same definition segments and instance methods as common components.The code is as follows:

Component({
    // You can use the component's behaviors mechanism, although React does not think mixins are a good solution
    // However, to some extent, the solution does reuse the same logic code
    behaviors: [myBehavior],
   
    // Options corresponding to the page, which is typed by itself, and data obtained from options is of type string
    // Access page/pages/index/index?ParamA=123&paramB=xyz 
    // If properties paramA or paramB are declared, they are assigned 123 or xyz instead of string type
    properties: {
        paramA: Number,
        paramB: String,
    },
    methods: {
        // onLoad does not require option
        // However, page-level life cycles can only be written in methods
        onLoad() {
            this.data.paramA // Value 123 for page parameter paramA
            this.data.paramB // Value'xyz'of page parameter paramB
        }
    }

})

So how does the life cycle of a component correspond to the life cycle of a page?After some tests, the result is: (For ease of use).Only important life cycles will be listed)

// Component Instance is created into Component Instance Entry Page Node Tree
component created -> component attched -> 
// Page Page Loading to Components Completed in View Layer Layout
page onLoad -> component ready -> 
// Page unload to component instance removed from page node tree
page OnUnload -> component detached

Of course, we don't focus on the intermediate state between onload and onunload, because when the intermediate state is in place, we can use the page life cycle to do a better job on the page.
Sometimes some of our initialization code shouldn't be in onload, so we can consider putting it in component creation and even reusing it with behaviors.
In a way, if you don't need the Vue style, it's also a good maintenance solution for us to directly use Component instead of Page in our old projects.After all, there is no need to worry about a series of other follow-up issues by official standards.

VantComponent Source Parsing

VantComponent

At this point, we start parsing the VantComponent

// Assignment, operated on by map's key and value
function mapKeys(source: object, target: object, map: object) {
  Object.keys(map).forEach(key => {
    if (source[key]) {
      // The map[key] of the target object corresponds to the key of the source data object
      target[map[key]] = source[key];
    }
  });
}

// ts code, also known as generics
function VantComponent<Data, Props, Watch, Methods, Computed>(
  vantOptions: VantComponentOptions<
    Data,
    Props,
    Watch,
    Methods,
    Computed,
    CombinedComponentInstance<Data, Props, Watch, Methods, Computed>
  > = {}
): void {
  const options: any = {};
  // Use function to copy new data, which is the Vue style we can use
  mapKeys(vantOptions, options, {
    data: 'data',
    props: 'properties',
    mixins: 'behaviors',
    methods: 'methods',
    beforeCreate: 'created',
    created: 'attached',
    mounted: 'ready',
    relations: 'relations',
    destroyed: 'detached',
    classes: 'externalClasses'
  });

  // Edit relationships between components, but page s do not need to, and can be deleted
  const { relation } = vantOptions;
  if (relation) {
    options.relations = Object.assign(options.relations || {}, {
      [`../${relation.name}/index`]: relation
    });
  }

  // externalClasses are added by default to components, but page s are not needed and can be deleted
  // add default externalClasses
  options.externalClasses = options.externalClasses || [];
  options.externalClasses.push('custom-class');

  // Add basic by default to components, encapsulate $emit and applet node query methods, and can be deleted
  // add default behaviors
  options.behaviors = options.behaviors || [];
  options.behaviors.push(basic);

  // map field to form-field behavior
  // Add built-in behavior wx://form-field by default
  // It makes this custom component behave like a form control.
  // You can explore the built-in behaviors given below
  if (vantOptions.field) {
    options.behaviors.push('wx://form-field');
  }

  // add default options
  // Add component default configuration, multiple slot s
  options.options = {
    multipleSlots: true,// Enable multiple slot support in options when components are defined
    // If this Component constructor is used to construct pages, the default value is shared
    // apply-shared of a component to study component style isolation given below
    addGlobalClass: true 
  };

  // Monitor vantOptions
  observe(vantOptions, options);

  // Put the currently reconfigured options into Component
  Component(options);
}

Built-in behaviors
Component style isolation

basic behaviors

We just talked about basic behaviors, and the code looks like this

export const basic = Behavior({
  methods: {
    // Calling the $emit component actually uses triggerEvent
    $emit() {
      this.triggerEvent.apply(this, arguments);
    },

    // Encapsulator Node Query
    getRect(selector: string, all: boolean) {
      return new Promise(resolve => {
        wx.createSelectorQuery()
          .in(this)[all ? 'selectAll' : 'select'](selector)
          .boundingClientRect(rect => {
            if (all && Array.isArray(rect) && rect.length) {
              resolve(rect);
            }

            if (!all && rect) {
              resolve(rect);
            }
          })
          .exec();
      });
    }
  }
});

observe

Code parsing for applets watch and computed

export function observe(vantOptions, options) {
  // Get watch computed from the incoming option  
  const { watch, computed } = vantOptions;

  // Add behavior
  options.behaviors.push(behavior);

  ///If there is a watch object
  if (watch) {
    const props = options.properties || {};
    // For example: 
    // props: {
    //   a: String
    // },
    // watch: {
    //   a(val) {
    //     //Print every time the val ue changes
    //     consol.log(val)
    //   }
    } 
    Object.keys(watch).forEach(key => {
      
      // watch only monitors data in prop
      if (key in props) {
        let prop = props[key];
        if (prop === null || !('type' in prop)) {
          prop = { type: prop };
        }
        // The observer for prop is assigned by the watch, which is the function of the applet component itself.
        prop.observer = watch[key];
        // Put the current key in prop
        props[key] = prop;
      }
    });
    // By this method
    // props: {
    //  a: {
    //    type: String,
    //    observer: (val) {
    //      console.log(val)
    //    }
    //  }
    // }
    options.properties = props;
  }

  // Encapsulate calculated properties
  if (computed) {
    options.methods = options.methods || {};
    options.methods.$options = () => vantOptions;

    if (options.properties) {
      
      // Monitor props, and if props change, the calculated property itself changes
      observeProps(options.properties);
    }
  }
}

observeProps

Now there are two files left, observeProps and behavior, which were generated to compute attributes. Let's first explain the observeProps code

export function observeProps(props) {
  if (!props) {
    return;
  }

  Object.keys(props).forEach(key => {
    let prop = props[key];
    if (prop === null || !('type' in prop)) {
      prop = { type: prop };
    }

    // Save the previous observer, the prop generated by the previous code
    let { observer } = prop;
    prop.observer = function() {
      if (observer) {
        if (typeof observer === 'string') {
          observer = this[observer];
        }

        // Saved observer before calling
        observer.apply(this, arguments);
      }

      // Call set once to reset calculation properties when a change occurs
      this.set();
    };
    // Assign modified props back
    props[key] = prop;
  });
}

behavior

The final behavior is also the computed implementation mechanism


// Asynchronous call to setData
function setAsync(context: Weapp.Component, data: object) {
  return new Promise(resolve => {
    context.setData(data, resolve);
  });
};

export const behavior = Behavior({
  created() {
    if (!this.$options) {
      return;
    }

    // cache
    const cache = {};
    const { computed } = this.$options();
    const keys = Object.keys(computed);

    this.calcComputed = () => {
      // Data that needs to be updated
      const needUpdate = {};
      keys.forEach(key => {
        const value = computed[key].call(this);
        // Cached data does not equal the current calculated value
        if (cache[key] !== value) {
          cache[key] = needUpdate[key] = value;
        }
      });
      // Return computed for required updates
      return needUpdate;
    };
  },

  attached() {
    // Call once in the attached cycle to calculate the current computed value
    this.set();
  },

  methods: {
    // set data and set computed data
    // set can use callback and then
    set(data: object, callback: Function) {
      const stack = [];
      // Place data when set
      if (data) {
        stack.push(setAsync(this, data));
      }

      if (this.calcComputed) {
        // There are calculation properties and they are also placed in the stack, but each set is called once, and props changes are also called
        stack.push(setAsync(this, this.calcComputed()));
      }

      return Promise.all(stack).then(res => {
        // callback is called when all data and calculated properties are complete
        if (callback && typeof callback === 'function') {
          callback.call(this);
        }
        return res;
      });
    }
  }
});

Write after

  • js is a flexible language (manual comic)
  • After the applet Page, the Component itself is more mature and useful than the Page itself. Sometimes new solutions are often hidden in the document. It is never meaningless to read the document several times at a time.
  • Applet version 2.6.1 Component now implements observers to listen on props data Data Monitor VantComponent is not currently implemented. Of course, page does not need to listen on prop per se, because entering pages does not change at all, while data changes do not need to listen on themselves and functions can be called directly, so observers may or may not be available to pages.
  • The scheme also has a vue style on the js code, and no other articles have been written on template s or styles.
  • The performance of this scheme must be missing because computed is calculated every time a set is set, not based on the set's data, and after deletion I think it is acceptable.If you don't need the grammatical features of vue, you can write a Page directly using Component. Choosing a different solution is essentially a trade-off between the pros and cons.If there are other requirements or new projects in itself, it is still recommended to use the new technology, if it is an existing project and needs to be maintained, and also want to have the Vue feature.You can use this scenario because the code itself is small and can be modified to suit your needs.
  • At the same time, vant-weapp is a very good project, I recommend you to check it out and star t it.

Posted by mrobertson on Fri, 26 Apr 2019 12:12:35 -0700