preface

In the development process of wechat applets, some page modules that may be used in multiple pages can be encapsulated as a component to improve the development efficiency. Although we can introduce the whole component library such as Weui, Vant, etc., sometimes considering the package size limit of wechat applets, it is more controllable to package customized components.

And for some business modules, we can encapsulate for component reuse. This paper mainly covers the following two aspects:

  • Component declaration and use
  • Component communication

Component declaration and use

The underlying component system of wechat applets is realized by Exparser component framework, which is built in the basic library of the applets. All components in the applets, including built-in components and custom components, are organized and managed by Exparser.

A custom component, like a write page, contains the following files:

  • index.json
  • index.wxml
  • index.wxss
  • index.js
  • index.wxs

Take writing a TAB component as an example: when writing a custom component, set the Component field to true in the JSON file:

{
    "component": true
}
Copy the code

In the js file, the base library provides two constructors: Page and Component. The corresponding Page is the root Component of the Page, and Component corresponds to:

Component({
    options: { // Component configuration
        addGlobalClass: true.// Specify that all data fields starting with _ are pure data fields
        // Pure data fields are data fields that are not used for interface rendering and can be used to improve page update performance
        pureDataPattern: / / ^ _, 
        multipleSlots: true // Enable multiple slot support in the options at component definition
    },
    properties: {
        vtabs: {type: Array.value: []}},data: {
        currentView: 0,},observers: { / / monitoring
        activeTab: function(activeTab) {
            this.scrollTabBar(activeTab); }},relations: {  // Associated child/parent component
        '.. /vtabs-content/index': {
            type: 'child'.// The associated target node should be a child node
            linked: function(target) {
                this.calcVtabsCotentHeight(target);
            },
            unlinked: function(target) {
                delete this.data._contentHeight[target.data.tabIndex]; }}},lifetimes: { // Component declaration cycle
        created: function() {
            // The component instance has just been created
        },
        attached: function() {
            // Executed when the component instance enters the page node tree
        },
        detached: function() {
            // Executed when the component instance is removed from the page node tree}},methods: { // Component methods
        calcVtabsCotentHeight(target){}}});Copy the code

Those of you who have read about Vue2 will find this statement familiar.

When the applet starts, the constructor defines the properties, data, methods, etc., set by the developer,

Write to the component registry of Exparser. When this component is referenced by other components, instances of custom components can be created based on this registration information.

Template file WXML:

<view class='vtabs'>
    <slot />
</view>
Copy the code

Style file:

.vtabs {}
Copy the code

External page components, only need to be imported in the page’s JSON file

{
  "navigationBarTitleText": "Commodity Classification"."usingComponents": {
    "vtabs": ".. /.. /.. /components/vtabs",}}Copy the code

When the page is initialized, Exparser creates an instance of the page root component, and other components respond by creating component instances (this is a recursive process) :

The component creation process has the following main points:

  1. Create component nodes from component prototypes based on component registration informationJSObject, that is, of a componentthis;
  2. Add the component registration information todataMake a copy as component data, i.ethis.data;
  3. Combine this data with componentsWXMLFrom which to createShadow Tree(component node tree), becauseShadow TreeThere may be references to other components in, so this triggers the creation of other components recursively;
  4. willShadowTreeJoining together toComposed Tree(finally spliced into page node tree), and generate some cache data for optimizing component update performance;
  5. Triggering componentcreatedLife cycle function;
  6. If it is not the root component, you need to set the attribute value of the component according to the attribute definition on the component node.
  7. Trigger when a component instance is displayed on a pageattachedLife cycle function, ifShadow TreeThere are other components that fire their lifecycle functions one by one.

Component communication

Due to the responsibility of the business, we often need to split a large page into multiple components that need to communicate with each other.

Global state management can be considered for cross-generation component communication, and only common parent-child component communication is discussed here:

Method one: WXML data binding

Sets the specified property data for the parent component to the child component.

Subdeclares the properties property


Component({
    properties: {
        vtabs: {type: Array.value: []}, // Data item format is' {title} '}})Copy the code

Parent component call:

    <vtabs vtabs="{{ vtabs }}"</vtabs>
Copy the code

Method two event

Used to pass data from a child to a parent, and can pass any data.

The sub-component sends the event, first binding the sub-component’s click event in the WXML structure:

   <view bindtap="handleTabClick">
Copy the code

The event name can be customized. The second parameter can pass the data object. The third parameter is the event option.

 handleClick(e) {
     this.triggerEvent(
         'tabclick', 
         { index }, 
         { 
             bubbles: false.// Whether the event bubbles
             // Whether events can cross component boundaries. If false, events can only be triggered in the node tree that refers to the component.
             // Do not go inside any other components
             composed: false.capturePhase: false // Whether the event has a capture phase}); },handleChange(e) {
     this.triggerEvent('tabchange', { index });
 },
Copy the code

Finally, listen on the parent component using:

<vtabs 
    vtabs="{{ vtabs }}"
    bindtabclick="handleTabClick" 
    bindtabchange="handleTabChange" 
>
Copy the code

Method three selectComponent gets the component instance object

The selectComponent method gets an instance of a child component to call its methods.

WXML for the parent component

<view>
    <vtabs-content="goods-content{{ index }}"></vtabs-content>
</view>
Copy the code

Js of the parent component

Page({
    reCalcContentHeight(index) {
        const goodsContent = this.selectComponent(`#goods-content${index}`); }})Copy the code

Selector is similar to a CSS selector, but only supports the following syntax.

  • ID selector:#the-id(I only tested this, other readers can test for themselves)
  • Class selector (more than one can be specified consecutively) :.a-class.another-class
  • Child element selector:.the-parent > .the-child
  • Descendant selector:.the-ancestor .the-descendant
  • Descendant selectors across custom components:.the-ancestor >>> .the-descendant
  • Union of multiple selectors:#a-node..some-other-nodes

Method 4 url parameter communication

There will be such user stories in wechat mini programs such as e-commerce/logistics, with an “order page A” and “Goods information page B”.

  • Fill in basic information on “Order Page A”, and drill down to “Details page B” to fill in details. For example, a delivery order page, you need to drill down to the goods information page to fill in more detailed information, and then return to the previous page.
  • After you drill down from Order page A to Goods page B, the data on Goods page B must be displayed.

Wechat applets consist of an App() instance and multiple pages (). The applets framework maintains pages as stacks (up to 10) and provides the following API for page hopping. The page routing is as follows

  1. Wx. navigateTo (Can only jump to pages in the stack)
  2. Wx. redirectTo (jumps to a new page that is off the stack and replaces the current page)
  3. Wx. navigateBack (returns to previous page without parameters)
  4. Wx. switchTab (switchTab page, url parameter not supported)
  5. Wx. reLaunch

We can simply encapsulate a jumpTo jump function and pass in arguments:

export function jumpTo(url, options) {
    const baseUrl = url.split('? ') [0];
    // If the URL has parameters, you need to mount the parameters to options as well
    if (url.indexof('? ')! = = -1) {
        const { queries } = resolveUrl(url);
        Object.assign(options, queries, options); // Options have the highest priority
    } 
    cosnt queryString = objectEntries(options)
        .filter(item= > item[1] || item[0= = =0) // All non-values are filtered except the number 0
        .map(
            ([key, value]) = > {
                if (typeof value === 'object') {
                    // Object to string
                    value = JSON.stringify(value);
                }
                if (typeof value === 'string') {
                    // encode string
                    value = encodeURIComponent(value);
                }
                return `${key}=${value}`;
            }
        ).join('&');
    if (queryString) { // The assembly parameters are required
        url = `${baseUrl}?${queryString}`;
    }
    
    const pageCount = wx.getCurrentPages().length;
    if (jumpType === 'navigateTo' && pageCount < 5) {
        wx.navigateTo({ 
            url,
            fail: () = > { 
                wx.switch({ url: baseUrl }); }}); }else {
        wx.navigateTo({ 
            url,
            fail: () = > { 
                wx.switch({ url: baseUrl }); }}); }}Copy the code

JumpTo helper function:

export const resolveSearch = search= > {
    const queries = {};
    cosnt paramList = search.split('&');
    paramList.forEach(param= > {
        const [key, value = ' '] = param.split('=');
        queries[key] = value;
    });
    return queries;
};

export const resolveUrl = (url) = > {
    if (url.indexOf('? ') = = = -1) {
        // The url with no arguments
        return {
            queries: {},
            page: url
        }
    }
    const [page, search] = url.split('? ');
    const queries = resolveSearch(search);
    return {
        page,
        queries
    };
};
Copy the code

Transfer data on “Order Page A” :

jumpTo({ 
    url: 'pages/consignment/index', 
    { 
        sender: { name: 'naluduo233'}}});Copy the code

Get URL parameters in “Cargo Information page B” :

const sender = JSON.parse(getParam('sender') | |'{}');
Copy the code

Url parameter get helper function

// Return to the current page
export function getCurrentPage() {
    const pageStack = wx.getCurrentPages();
    const lastIndex = pageStack.length - 1;
    const currentPage = pageStack[lastIndex];
    return currentPage;
}

// Get the page URL parameter
export function getParams() {
    const currentPage = getCurrentPage() || {};
    const allParams = {};
    const { route, options } = currentPage;
    if (options) {
        const entries = objectEntries(options);
        entries.forEach(
            ([key, value]) = > {
                allParams[key] = decodeURIComponent(value); }); }return allParams;
}

// Return the value by field
export function getParam(name) {
    const params = getParams() || {};
    return params[name];
}
Copy the code

Auxiliary function

// Check whether the current path is tabBar
const { tabBar} = appConfig;
export isTabBar = (route) = > tabBar.list.some(({ pagePath })) => pagePath === route); 
Copy the code

What if the parameter is too long? Does the routing API not support carrying parameters?

Although the official document of wechat mini program does not specify how long the parameters can be carried on the page, there may be a risk that the parameters will be truncated if they are too long.

We can use global data to record parameter values and solve the problem of url parameters being too long and routing apis not supporting carrying parameters.

// global-data.js
// Since switchTab does not support parameters, consider using a global data store
// Mount data to switchTab
const queryMap = {
    page: ' '.queries: {}};Copy the code

Update jump function

export function jumpTo(url, options) {
    // ...
    Object.assign(queryMap, {
        page: baseUrl,
        queries: options
    });
    // ...
    if (jumpType === 'switchTab') {
        wx.switchTab({ url: baseUrl });
    } else if (jumpType === 'navigateTo' && pageCount < 5) {
        wx.navigateTo({ 
            url,
            fail: () = > { 
                wx.switch({ url: baseUrl }); }}); }else {
        wx.navigateTo({ 
            url,
            fail: () = > { 
                wx.switch({ url: baseUrl }); }}); }}Copy the code

Url parameter get helper function

/ / get the page url parameter export function getParams () {const currentPage = getCurrentPage () | | {}; const allParams = {}; const { route, options } = currentPage; if (options) { const entries = objectEntries(options); entries.forEach( ([key, value]) => { allParams[key] = decodeURIComponent(value); });+ if (isTabBar(route)) {
+ // is the tab-bar page, using the globally mounted parameters
+ const { page, queries } = queryMap;
+ if (page === `${route}`) {
+ Object.assign(allParams, queries);
+}
+}
    }
    return allParams;
}
Copy the code

By this logic, is there no need to differentiate between isTabBar pages and all pages are retrieved from queryMap? This question will be explored later, because I haven’t tried to get the missing value from the options in the page example. So you can keep the value of reading getCurrentPages.

Method 5 EventChannel event dispatch communication

Earlier I talked about passing data from “current page A” to the opened “page B” using the URL parameter. So what do you do to get the data that the opened page sends to the current page? Is it possible to use url parameters as well?

The answer is yes, provided you don’t need to save the state of “page A”. To preserve the state of “page A,” you need to use navigateBack to return to the previous page, and the API does not support url parameters.

In this case, you can use EventChannel, an inter-page event communication channel.

PageA page

// 
wx.navigateTo({
    url: 'pageB? id=1'.events: {
        // Adds a listener to the specified event to get data from the opened page to the current page
        acceptDataFromOpenedPage: function(data) {
          console.log(data) 
        },
    },
    success: function(res) {
        // Send data to the opened page via eventChannel
        res.eventChannel.emit('acceptDataFromOpenerPage', { data: 'test'}}}));Copy the code

PageB page

Page({
    onLoad: function(option){
        const eventChannel = this.getOpenerEventChannel()
        eventChannel.emit('acceptDataFromOpenedPage', {data: 'test'});
   
        // Listen for acceptDataFromOpenerPage events to get data sent from the previous page to the current page via eventChannel
        eventChannel.on('acceptDataFromOpenerPage'.function(data) {
          console.log(data)
        })
      }
})
Copy the code

Will the data not be listened to?

NavigateTo navigateTo log in to the top of the stack of 10 layers. If the current “page A” is not the 10th layer, then you can use navigateTo jump to keep the current page and jump to “page B”. When “page B” is filled in, “page A” can listen the data.

If page A is the tenth page, you can only use redirectTo to jump to the Page. The result is that the current “page A” is off the stack and the new “page B” is on the stack. NavigateBack is not able to return to the target “page A”, so the data cannot be monitored normally.

But in the small programs I’ve analyzed, there are very few 10 layers on the stack, and very few 5 layers. NavigateBack, wx.redirectTo closes the current page, and wx.switchTab closes all other non-Tabbar pages.

So it’s very rare that you can’t go back to the previous page to listen to the data, and if that happens, the first thing to worry about is not listening to the data, but how to make sure you can go back to the previous page.

For example, in the “PageA” page, call getCurrentPages to get the number of pages, then delete the other pages, and then jump to the “PageB” page, so as to avoid “PageA” call wx.redirectTo caused by closing “PageA”. However, it is not recommended that developers manually change the page stack, so be careful.

If any readers have encountered this situation and know how to solve it, please let me know. Thank you.

Use a custom event center EventBus

In addition to using the officially provided EventChannel, you can also customize a global EventBus event hub. Because it’s more flexible, you don’t need to pass in parameters to call apis like Wx. navigateTo, and it’s more portable across multiple platforms.

export default class EventBus {
 private defineEvent = {};
 // Register events
 public register(event: string, cb): void { 
  if(!this.defineEvent[event]) {
   (this.defineEvent[event] = [cb]); 
  }
  else {
   this.defineEvent[event].push(cb); }}// Dispatch event
 public dispatch(event: string, arg? :any) :void {
  if(this.defineEvent[event]) {{
            for(let i=0, len = this.defineEvent[event].length; i<len; ++i) { 
                this.defineEvent[event][i] && this.defineEvent[event][i](arg); }}}}/ / on listening
 public on(event: string, cb): void {
  return this.register(event, cb); 
 }
 / / off method
    public off(event: string, cb?) :void {
        if(this.defineEvent[event]) {
            if(typeof(cb) == "undefined") { 
                delete this.defineEvent[event]; // Deletes all files
            } else {
                // go through the search
                for(let i=0, len=this.defineEvent[event].length; i<len; ++i) { 
                    if(cb == this.defineEvent[event][i]) {
                        this.defineEvent[event][i] = null; // Mark empty - to prevent dispath length changes
                        // Delete corresponding events in a delayed manner
                        setTimeout(() = > this.defineEvent[event].splice(i, 1), 0); 
                        break; 
                    }
                }
            }
        } 
    }

    // use the once method to listen once
    public once(event: string, cb): void { 
        let onceCb = arg= > {
         cb && cb(arg); 
         this.off(event, onceCb); 
        }
        this.register(event, onceCb); 
    }
    // Clear all events
    public clean(): void {
        this.defineEvent = {}; }}export connst eventBus = new EventBus();
Copy the code

Listen on the PageA page:

eventBus.on('update'.(data) = > console.log(data));
Copy the code

Distributed on PageB page

eventBus.dispatch('someEvent', { name: 'naluduo233'});
Copy the code

summary

This paper mainly discusses how wechat applets customize components, involving two aspects:

  • Component declaration and use
  • Component communication

If you’re using Taro, use the React syntax to customize components. As for the component communication, taro will eventually be compiled into wechat applet, so the communication mode of URL and EventBus page component is applicable. I will analyze the source code of some components of VT-UI syndrome to see how Appellate syndrome works.

Thanks for reading, please point out any mistakes at 🙏

The resources

  • Applets Development Guide Custom components
  • Applets development Guide underlying framework
  • Micro channel applet page stack _ micro channel applet page stack details