Secondary packaging of wechat applet Page and Component (in line with vue2's development habits)

preface

I believe many developers who have developed wechat applets should know that wechat applets are very similar to vue in syntax, but there are some differences. This article will describe how to re encapsulate the Page constructor and Component constructor of wechat applets, and smooth out the differences between some wechat applets and vue

objective

Many people may wonder why it is necessary to encapsulate the Page and Component constructors. The reasons are as follows:

  • For developers who are not familiar with wechat applet development but are familiar with vue development, encapsulating Page and Component into a more vue development habit can make them get started with small programs more quickly. It's very friendly for novices.

  • You can customize hook functions (lifecycle functions). In actual project developers, many wechat applets need to obtain user information, and the user information is obtained on app JS, but the rendering of the Page is the same as that of app JS is executed asynchronously, that is, the onLoad life cycle function of the Page is executed, app JS may not get the user information, so the Page cannot get the user information. This is also a problem encountered by many people. There are many solutions online, but it is troublesome. We can customize an onAppLoad life cycle function for the secondary encapsulated Page constructor, and the execution time of the function is in app JS is executed after obtaining the user information, so that the sub Page can obtain the user information.

  • Extend customization. We know that vue can use the watch listening attribute and the computed calculation attribute. Wechat applet can also be supported, but it needs a third-party npm package, which is cumbersome to use. We can also add the watch monitoring attribute and calculated calculation attribute in the secondary packaging to simplify some processes (package introduction, setting behaviors, etc.)

  • Unified management. On the project, some pages may require some permissions for users to enter the Page. Based on this requirement, we can uniformly check the user permissions in the secondary encapsulated Page constructor, instead of writing the check permissions once in each Page.

Page encapsulation

Page common fields

Let's first take a look at the fields commonly used by Page. I won't list those that are not commonly used. You can view the document by yourself. Click here to view the document

Page({
  // Mix in
  behaviors: [],
  // Page data
  data: {},
  // Page loading completed
  onLoad() {},
  // The page is displayed
  onShow() {},
  // Page rendering completed
  onReady() {},
  // Page unload
  onUnload() {},
  // Pull up to touch the bottom
  onReachBottom() {},
  // Custom function
  onClick() {},
});

vue common fields

Let's take a look at what common fields vue has

export default {
  mixins: [],
  data() {
    return {};
  },
  created() {},
  mounted() {},
  methods() {},
  computed: {},
  watch: {},
  destroyed() {},
};

Field correspondence

From the above comparison, it is not difficult to find the corresponding relationship between wechat applet and vue field

  • Page.data -> vue.data

  • Page.onLoad -> vue.created

  • Page.onReady -> vue.mounted

  • Page.onUnload -> vue.destroyed

  • Page.onClick -> vue.methods.onClick

  • Page.behaviors -> vue.mixins

  • Computed and watch wechat applets can only be used in combination with miniprogram computed, which we will package next

  • onShow, onReachBottom and other fields are unique to the Page constructor and need to be preserved during encapsulation

  • In vue, the query parameters on the page are stored in this$ route. Query, but the wechat applet is obtained from the callback parameters of onLoad. We mount the page query parameters of the wechat applet to this$ Query.

Packaging effect

The final form we encapsulated is as follows:

MyPage({
  mixins: [],
  data() {
    return {};
  },
//   It can also be
//   data:{},
  created() {},
  mounted() {},
  methods() {
    onClick(){}
  },
  computed: {},
  watch: {},
  destroyed() {},
  onShow(){},
  onReachBottom(){}
});

technological process

In fact, many fields are still mapped, but there are no significant changes. The process is as follows:

  • Check whether the data field is a function or an object. If it is a function, you need to execute this function to obtain the returned object

  • Map the mixins, data, created, mounted, computed, watch, destroyed, onShow, onReachBottom and other fields to the corresponding fields of wechat applet

  • methods needs special processing, because the custom function method of wechat applet is at the same level as data, onShow and other fields, which is a brotherhood

  • Check whether fields such as calculated or watch are used. If they are used, a computed behavior (introduced through miniprogram calculated) will be automatically added to the behaviors field

  • Rewrite the onLoad lifecycle function to mount the query parameters of the page address to this$ Query

Source code

The final code is as follows:

import { isFunction } from "./is";
import { behavior as computedBehavior } from "miniprogram-computed";

function mapKeys(fromTarget, toTarget, map) {
  Object.keys(map).forEach((key) => {
    if (fromTarget[key]) {
      toTarget[map[key]] = fromTarget[key];
    }
  });
}

function proxyMethods(fromTarget, toTarget) {
  if (fromTarget) {
    Object.keys(fromTarget).forEach((key) => {
      toTarget[key] = fromTarget[key];
    });
  }
}

function proxyOnLoad(MyOptions, options) {
  // Save the original onLoad function
  const oldLoad = options.onLoad;
  options.onLoad = function (query) {
    // Mount query parameters
    this.$query = query;
    if (isFunction(oldLoad)) {
      // Execute the original onLoad function
      oldLoad.call(this, query);
    }
  };
}

function proxyComputedAndWatch(MyOptions, options) {
  if (MyOptions.computed || MyOptions.watch) {
    options.behaviors = options.behaviors || [];
    // If 'computed' or 'watch' is used, you need to add the corresponding 'behaviors', otherwise it is invalid
    options.behaviors.push(computedBehavior);
  }
}

function MyPage(MyOptions) {
  const options = {};
  // Check whether the 'data' field is a function
  if (isFunction(MyOptions.data)) {
    MyOptions.data = MyOptions.data();
  }
  // Field mapping
  mapKeys(MyOptions, options, {
    data: "data",
    onReachBottom: "onReachBottom",
    onShow: "onShow",
    mounted: "onReady",
    created: "onLoad",
    mixins: "behaviors",
    computed: "computed",
    watch: "watch",
    destroyed: "onUnload",
  });
  // Data in the flat methods field
  proxyMethods(MyOptions.methods, options);
  // Check whether the 'computed' or 'watch' field is used
  proxyComputedAndWatch(MyOptions, options);
  // Rewrite the 'onLoad' lifecycle function and mount the query parameters of the page address to 'this$ Query ` on
  proxyOnLoad(MyOptions, options);

  Page(options);
}

export default MyPage;

Extend custom functions or lifecycle functions

Let's take an example of extending an onAppLoad lifecycle function, which is in app JS is called after the onLaunch function is executed

Transform app js

This lifecycle function needs to be applied to app JS can only take effect after transformation. The transformation points are as follows:

  • Add an isLoad attribute to identify app Is the onLaunch function of JS completed

  • Add a taskList asynchronous task queue to store the tasks to be executed after the onLaunch function is executed

  • If there is an asynchronous task in the onLaunch function, you need to use async and await to wait for the asynchronous task to complete.

  • Use the try catch wrapper function body, in finally, mark the isLoad attribute as true, and then execute the task in the taskList

The final modification code is as follows:

// app.js
App({
  // Asynchronous task queue
  taskList: [],
  // Loading complete
  isLoad: false,
  async onLaunch() {
    // Add a try catch to prevent the request from exploding and the taskList cannot be executed
    try {
      await this.getUserInfo();
      // ...
    } catch (error) {
      console.log("Error on home page", error);
    } finally {
      this.isLoad = true;
      // Execute task queue after loading
      this.runTask();
    }
  },
  // Get user information
  async getUserInfo(params) {
    // ...
  },
  runTask() {
    const taskList = this.taskList.slice();
    if (taskList.length > 0) {
      taskList.forEach((task) => {
        task();
      });
      this.taskList = [];
    }
  },
});

Implement the onAppLoad function

app. After the transformation of app.js, we need to prepare a function. The function is to obtain the globally unique app instance of the applet (actually this in app.js) through getApp(), and then use app. JS Isload judge app JS, and finally returns a Promise

The code is as follows:

// on-app-load.js
const app = getApp();

function onAppLoad() {
  return new Promise((resolve, reject) => {
    try {
      if (!app.isLoad) {
        app.taskList.push(() => {
          resolve();
        });
      } else {
        resolve();
      }
    } catch (error) {
      reject(error);
    }
  });
}

export default onAppLoad;

Remake onLoad lifecycle function

Finally, go back to our proxyOnLoad function and reconstruct the onLoad function as follows:

import onAppLoad from "./on-app-load";
function proxyOnLoad(MyOptions, options) {
  const oldLoad = options.onLoad;
  options.onLoad = function (query) {
    this.$query = query;
    // Check whether the onAppLoad lifecycle function is used
    if (isFunction(MyOptions.onAppLoad)) {
      // Call the encapsulated onAppLoad function and execute ` myoptions onAppLoad ` this lifecycle function
      onAppLoad().then(() => {
        MyOptions.onAppLoad.call(this, query);
      });
    }
    if (isFunction(oldLoad)) {
      oldLoad.call(this, query);
    }
  };
}

Final effect

The final use effect is as follows:

MyPage({
  onAppLoad() {
    // todo, where you can ensure that you get user information
  },
});

Component encapsulation

The encapsulation of Component is similar to that of Page, but there are some differences in fields. In addition, compared with Page constructor, Component has more functions

Component common fields

Let's take a look at the commonly used fields of Component. I won't list those that are not commonly used. You can view the document yourself

Component({
  // Some options
  options: {},
  // External style class accepted by component
  externalClasses: [],
  // Mix in
  behaviors: [],
  // External properties of components
  properties: {},
  // Internal data of components
  data: {},
  // Component customization method
  methods: {},
  // Execute when the component instance is just created
  created() {},
  // Execute when the component instance enters the page node tree
  attached() {},
  // Execute after the component layout is completed
  ready() {},
  // Relationship definition between components
  relations: {},
  // Executed when a component instance is removed from the page node tree
  detached() {},
  // Component data field listener
  observers: {},
});

vue common fields

Let's take a look at what common fields vue has

export default {
  mixins: [],
  data() {
    return {};
  },
  props: {},
  beforeCreate() {},
  created() {},
  mounted() {},
  methods() {},
  computed: {},
  watch: {},
  destroyed() {},
};

Field correspondence

From the above comparison, it is not difficult to find the corresponding relationship between wechat applet and vue field

  • Page.data -> vue.data

  • Page.properties -> vue.props

  • Page.behaviors -> vue.mixins

  • Page.methods -> vue.methods

  • Page.created -> vue.beforeCreate

  • Page.attached -> vue.created

  • Page.ready -> vue.mounted

  • Page.detached -> vue.destroyed

  • Page.observers -> vue.watch

  • Computed can only be used in combination with miniprogram computed

  • Because the Component constructor already has the corresponding field observers implemented, the watch field does not need to be combined with miniprogram computed

  • Relationships is used to define the relations hip between components. For details, see Click here to view . Component constructor specific fields, which need to be reserved

  • Options is used to define some options, such as whether to turn on the global style (addGlobalClass:true) and whether to allow multiple slots (the components of wechat applet can only have one slot by default. If a named slot is required, it is necessary to declare multipleSlots:true in this field). Component constructor specific fields, which need to be reserved

  • The external style class accepted by the externalClasses Component. The style of wechat applet components is isolated by default (that is, the page style does not affect the Component style, and the Component style does not affect the page style). During encapsulation, we will add options by default Addglobalclass = true, so that the sub Component can be affected by the page style, which is convenient to modify the style of the Component. However, if it is based on a Component, the Component style will not affect the style of other components during secondary packaging. You need to specify the style class through externalClasses. The Component constructor has a unique field, corresponding to the encapsulated classes field

  • Like the Page constructor encapsulation, we also mount the address query parameters of the Page to this$ Query, but the way component obtains the Page address is different from that of Page. Component obtains the Page's query parameters by obtaining the instance of the Page where the component is located in the ready life cycle function, and then obtains the Page's query parameters

Packaging effect

MyComponent({
  mixins: [],
  data() {
    return {};
  },
  //   It can also be
  //   data:{},
  props: {},
  methods() {},
  beforeCreate() {},
  created() {},
  mounted: {},
  relations: {},
  destroyed() {},
  classes: [],
  watch: {},
  computed: {},
});

technological process

  • Check whether the data field is a function or an object. If it is a function, you need to execute this function to obtain the returned object

  • Map the data, props, mixins, methods, beforeCreate, created, mounted, destroyed, watch, computed, relations hips, classes and other fields to the corresponding fields of wechat applet

  • Check whether the calculated and other fields are used. If they are used, a calculated behavior (introduced through miniprogram calculated) will be automatically added to the behaviors field

  • The options field turns on multipleSlots:true and addGlobalClass:true by default

  • Rewrite the ready lifecycle function and mount the query parameters of the page address to this$ Query

Source code

import { behavior as computedBehavior } from "miniprogram-computed";
import { isFunction } from "./is";

function mapKeys(source, target, map) {
  Object.keys(map).forEach((key) => {
    if (source[key]) {
      target[map[key]] = source[key];
    }
  });
}

function getCurrentPageParam() {
  // Get loaded page
  const pages = getCurrentPages();
  //Gets the object of the current page
  const currentPage = pages[pages.length - 1];
  //If you want to get the parameters carried in the url, you can view options
  const options = currentPage.options;
  return options;
}

function proxyReady(MyOptions, options) {
  // Save the original ready function
  const ready = options.ready;
  options.ready = function () {
    // Mount query parameters
    this.$query = getCurrentPageParam();
    if (isFunction(ready)) {
      // Execute the original onLoad function
      ready.call(this);
    }
  };
}

function proxyComputed(MyOptions, options) {
  // If 'computed' is used, you need to add the corresponding 'behaviors', otherwise it is invalid
  if (MyOptions.computed) {
    options.behaviors = options.behaviors || [];
    options.behaviors.push(computedBehavior);
  }
}

function proxyProps(MyOptions, options) {
  // vue's props writing method is slightly different from that of wechat applet, and some fields need to be specially handled
  if (options.properties) {
    Object.keys(options.properties).forEach((name) => {
      if (Array.isArray(options.properties[name])) {
        options.properties[name] = null;
      }
    });
  }
}

function MyComponent(MyOptions) {
  const options = {};

  // Check whether the 'data' field is a function
  if (isFunction(MyOptions.data)) {
    MyOptions.data = MyOptions.data();
  }

  // Field mapping
  mapKeys(MyOptions, options, {
    data: "data",
    props: "properties",
    mixins: "behaviors",
    methods: "methods",
    beforeCreate: "created",
    created: "attached",
    mounted: "ready",
    relations: "relations",
    destroyed: "detached",
    classes: "externalClasses",
    watch: "observers",
    computed: "computed",
  });

  // Check whether the 'computed' field is used
  proxyComputed(MyOptions, options);
  // Special handling of some fields of props
  proxyProps(MyOptions, options);

  // Some options are enabled by default
  options.options = {
    multipleSlots: true,
    addGlobalClass: true,
  };

  // Rewrite the 'ready' lifecycle function and mount the query parameters of the page address to 'this$ Query ` on
  proxyReady(MyOptions, options);

  Component(options);
}

export default MyComponent;

Add extended features

Add field

Wechat applet can add built-in behaviors (built-in form behavior, details) for custom components Click here to view )Then, you can turn the custom component into a form component with the corresponding functions of the form component.

Use the following:

MyComponent({
  // Declare as a form component
  field: true,
  props: {
    name: {
      type: String,
    },
    value: {
      type: String,
    },
  },
});

Back to MyComponent, we need to make the following modifications:

function MyComponent(MyOptions) {
  // ...
  // If declared as a form component
  if (MyOptions.field) {
    options.behaviors = options.behaviors || [];
    // Add built-in behaviors
    options.behaviors.push("wx://form-field");
  }
  // ...
}

Add relation field

Sometimes, we will encounter some components with parent-child or grandson relationship. These components need to communicate (we need to obtain the parent component instance or child component instance, and then use the instance method or property). At this time, we need to use the relationships field, but it is still a little troublesome to use this field, So we can continue to encapsulate a relation field to simplify the process. Note that relationship is only applicable to components with only one identity. If components with multiple identities (either as a child component of a component or as a parent component of some other components) are not applicable, you still need to use the native relationships field

The function we need to implement is to hang this. In the sub component$ Parent (i.e. the parent component instance), hang it in this$ Children (i.e. sub component instances) are automatically destroyed when uninstalled.

The package code is as follows:

const relationFunctions = {
  // The associated target node should be an ancestor node
  ancestor: {
    // Triggered when a child component is inserted into a parent component
    linked(parent) {
      this.$parent = parent;
    },
    // Triggered when the child component is detached from the parent component
    unlinked() {
      this.$parent = null;
    },
  },
  // The associated target node should be a descendant node
  descendant: {
    // Triggered when a child component is inserted into a parent component
    linked(child) {
      this.$children = this.$children || [];
      this.$children.push(child);
    },
    // Triggered when the child component is detached from the parent component
    unlinked(child) {
      this.$children = (this.$children || []).filter((it) => it !== child);
    },
  },
};

function makeRelation(MyOptions, options) {
  const { type, name, linked, unlinked, linkChanged } = MyOptions.relation;
  const { created, detached } = options;
  if (type === "descendant") {
    // Parent component type
    options.created = function () {
      created && created.bind(this)();
      // Add the $children attribute by default
      this.$children = this.$children || [];
    };
    options.detached = function () {
      this.$children = [];
      detached && detached.bind(this)();
    };
  }
  // Merge the contents of the 'relationship' field into the 'relationships' field
  options.relations = Object.assign(options.relations || {}, {
    [name]: {
      type,
      linked(node) {
        relationFunctions[type].linked.bind(this)(node);
        linked && linked.bind(this)(node);
      },
      linkChanged(node) {
        linkChanged && linkChanged.bind(this)(node);
      },
      unlinked(node) {
        relationFunctions[type].unlinked.bind(this)(node);
        unlinked && unlinked.bind(this)(node);
      },
    },
  });
}

function MyComponent(MyOptions) {
  // ...
  const { relation } = MyOptions;
  if (relation) {
    // Handling relationships between components
    makeRelation(MyOptions, options);
  }
  // ...
}

Suppose we now have a Form component and a FormItem component. The Form component is the parent component and the FormItem component is the child component

The above usage of wxml:

<form>
  <form-item></form-item>
  <form-item></form-item>
</form>

Form component

MyComponent({
  relation: {
    type: "descendant",
    name: "../form-item/index", // Fill in the path where the form item component is located
    linked() {
      this.updateChildren();
    },
  },
  methods: {
    updateChildren() {},
    update() {
      // this.$children
    },
  },
});

FormItem component

MyComponent({
  relation: {
    type: "ancestor",
    name: "../form/index", // Fill in the path where the form component is located
  },
  methods: {
    update() {
      // this.$parent
    },
  },
});

After the above operation, this$ Parent and this$ Children are getting closer to vue development habits

summary

Through the secondary encapsulation of Page and Component, we make wechat applet development as easy and simple as vue development. On the basis of encapsulation, we not only add some convenient and easy-to-use functions, such as the calculated and watch attributes, but also simplify some operations, such as this$ Parent and this$ The acquisition and definition of children have been encapsulated and defined internally, and can be used out of the box. There is no need to define and obtain.

Of course, we can also define more functions. For example, wechat applet uses this to modify responsive data SetData ({XXX: XXX}), and vue modifies the responsive data by using this XXX = XXX, we can write a data proxy method to change the way wechat applet modifies responsive data into this xxx=xxx

Keywords: Mini Program wechat Component

Added by BinaryStar on Fri, 21 Jan 2022 00:48:25 +0200