This is the fourth day of my participation in the More text Challenge. For details, see more text Challenge

preface

Custom components can help simplify code complexity for better reuse and refactoring. Starting with version 1.6.3 of the Miniprogram Base library, miniprograms support concise componentized programming. All custom component-related features require base library version 1.6.3 or higher. Developers can abstract functional modules within a page into custom components that can be reused in different pages; You can also break complex pages into multiple modules that are less coupled to help with code maintenance. A custom component is very similar to the base component when used.

Custom components

Small program custom component development details of the core point:

  • Resource management of custom components;

– The life cycle of the component;

  • The communication flow between components.

In this process, componentization ideas are combined with Web Components specification to assist understanding. Web Components specification is a set of technical specifications introduced by W3C for packaging reusable and interactive front-end Components, aiming at providing a standard componentization pattern. Several popular front-end frameworks (Vue/React/Angular) all follow this specification to a certain extent, so does the custom component of wechat applet. Moreover, the custom component of wechat applet rendering uses Shadow DOM. This technology is part of the Web Components specification. As a front-end developer, you’ve probably been through the BootStrap era, or the older ExtJS era, and even if you don’t know either framework, you’ve inevitably used React, Vue, or Angular. React/Vue/Angular has one thing in common with its predecessor BootStrap/ExtJS: they are both proponents of front-end componentization. “Front-end componentization” can be understood as “the specific practice of object-oriented programming ideas in the front-end UI field, extracting part of THE UI content into independent reusable components”, which has the following three advantages:

Improve code reusability

This is the most direct advantage of componentization. If you do not use components, you need to rewrite the code every time you encounter the same business scenario, and the components that are pulled out can be reused within the applicable scenario, greatly reducing the development time.

Easier code maintenance

Imagine that all of the UI source code for a page is in the same HTML file. When the navigation bar of the page is bugged, you need to find the HTML tag for the navigation bar in thousands or even thousands of lines of HTML file. If the navigation bar is separated into a component, then you only need to look within that component. This is a common example of how componentization can make code easier to maintain.

Reduces the difficulty of system reconfiguration

Refactoring: Improving the Design of Existing Code shows that refactoring is not a one-time activity when the complexity of a system increases to a level that is difficult to maintain, but a frequent, small-scale, daily activity. To put it bluntly: you should always refactor to improve your system, no matter how small. However, this practice presents a significant challenge to code maintainability, which indirectly illustrates the positive impact of componentization on refactoring: it indirectly reduces the difficulty of refactoring the system by making the code more maintainable.

Custom component usage

1. Component concept

Component is composed of four files, WXML, WXSS, JS, and json. it needs to be in the same directory. Unlike pages, the constructor (or constructor) in Component is Component({}), while the constructor in Page is Page({}). To write a custom component, you first need to declare a custom component in a JSON file (set the Component field to true to make this set of files a custom component) :

{
  "component": true
}
Copy the code

The Component slot allows you to insert modules into your Component’s WXML free from external WXML. By default, a component’s WXML can have only one slot. If you need more than one, you can declare it enabled in component JS.

Component({
  options: {
    multipleSlots: true // Enable multi-slot support in the component definition options
  },
  properties: { / *... * / },
  methods: { / *... * /}})Copy the code

At this point, you can use multiple slots in the WXML of the component, identified by different names.

<! -- Component Templates --><view class="wrapper">
  <slot name="before"></slot>
  <view>Here are the internal details of the component</view>
  <slot name="after"></slot>
</view>
Copy the code

When used, the slot attribute is used to insert nodes into different slots.

<! -- Reference the component's page template --> <view> <component-tag-name> <! -- This section of content will be placed in the component <slot name="before"> --> <view slot="before"> Here is the content inserted into the component slot name="before" </view> <! -- This section of content will be placed in the component <slot name="after"> --> <view slot="after"> Here is the content inserted into the component's slot name="after" </view> </component-tag-name> </view>Copy the code

Component style writing considerations

Components and pages that reference components cannot use id selectors (#a), property selectors ([a]), and tag name selectors; use class selectors instead. Use of descendant selectors (.A.b) on components and pages that reference components can be unexpected in extreme cases, so avoid using them. Child element selectors (.a>.b) can only be used between the View component and its children; using them with other components may cause unexpected conditions.

Inherited styles, such as font, color, are inherited from outside the component into the component. Except for inherited styles, the styles in app. WXSS and the styles on the page where the component is located do not apply to custom components (small programs frequently report a large number of warnings).

#a { } /* You cannot use */ in components
[a] { } /* You cannot use */ in components
button { } /* You cannot use */ in components
.a > .b { } /* This does not necessarily take effect unless.a is a View component node */
Copy the code

External style class

Use the external style class to make a component use the specified external style class. If you want the external style class to fully affect the component inside, set the options.addGlobalClass field in the component constructor to true.

/* Component custom- Component.js */
Component({
  externalClasses: ['my-class']}) <! Custom.component.wxml --><custom-component class="my-class">The color of this text is determined by the class outside the component</custom-component>
/* Style definitions outside the component */
.red-text {
  color: red;
}
Copy the code

Create a component

<! --components/component/component.wxml--><view class="inner">
    {{innerText}}
</view>
<slot></slot>
Copy the code

Write a JS file where the component’s property values and internal data are used to render the component’s WXML, where the property values can be passed in from outside the component

// components/component/component.js
Component({
    /** * Component property list */
    properties: {
        innerText: {
            type: String.value: 'hello world'
        },
        myProperties:String
    },

    /** * initial data for the component */
    data: {},/** * Component's method list */
    methods: {}})Copy the code

Sets the color of the font

/* components/component/component.wxss */
.inner{color: red; }Copy the code

Complete the initialization of the component, including setting the property list, initializing data, and setting the related methods.

Use custom components

Before you can use a registered custom component, you must first declare the reference in the PAGE’s JSON file. In this case, you need to provide the label name of each custom component and the corresponding custom component file path:

{
  "usingComponents": {
    "component": "/components/component/component"}}Copy the code

Add a declared custom component under page:

// <component></component>

<view>
  <component>
    <! -- This part of the content will be placed in the component <slot> location -->
    <view>Here is what was inserted into the component slot</view>
  </component>
</view>
Copy the code

At the top is one of the simplest custom components. In the development of wechat small program custom components of the three core links we need to pay attention to the following details:

User-defined component resource management

We need to use the Component constructor to create the custom Component of wechat applet, which is the smallest constructor in the structure system of wechat applet. The outer layer is the Page constructor, and the outermost layer is the App constructor. The relationship among the three is as follows:

App > Page > Component (1:N)

  • An App (i.e., a small program) can contain N (N >= 1) pages;

  • 1 A Page can contain N (N>=1) Components.

Resources for each custom component must include four basic files:

  • A WXML file that describes the structure of the component;

  • WXSS file to describe the component style;

  • A JS file that describes the behavior of a component.

  • The JSON file used to declare the component configuration.

Compared with traditional front-end development, the writing method of WXML and WXSS files of small program custom components is basically similar to that of HTML and CSS. Do not pay special attention to them. The differences are mainly reflected in JS and JSON files. You must declare this component as a custom component in the JSON file using the Component field, as follows:

{
  "component": true
}
Copy the code

The Component constructor is used to create the logical entity of the Component.

Component({
  behaviors: [].properties: {},data: {},
  lifetimes: {},
  pageLifetimes: {},
  methods: {}});Copy the code

A few properties of the Component constructor are easier to understand compared to Vue and React: Behaviors, similar to the Mixins in Vue and React, are used to define shared logic between multiple components and can include a set of definitions for properties, data, lifetimes, and methods.

Properties are similar to props in Vue and React and are used to receive incoming data from the outer (parent) component.

Data is similar to data in Vue and state in React. It describes private data (state) of a component.

Lifetimes is used to define the lifecycle functions of the component itself. This method was introduced in version 2.2.3 of the miniprogram base library. The original method is similar to Vue and React, which are directly mounted to the first-class properties of the component (we will discuss lifecycle functions in more detail in the next section).

PageLifetimes is a set of original logic of micro channel small program custom component, used to listen to the life cycle of the component page. This is generally used to change the state of A component during A particular lifecycle of the page. For example, the state of the component is set to A when the page is shown (show) and B when the page is hidden (hide).

Methods are similar to Vue’s methods and are used to define functions inside a component.

In addition to the four basic files, custom components can also contain some other necessary resources, such as images. The following figure shows the list of resources for the custom component Chatroom: As you can see, in addition to the WXML/WXSS /js/ JSON file, there are two image files that can be directly referenced in WXML using relative directories.

<image src="./photo.png"></image>
Copy the code

Custom component lifecycle

In the case of a component, the lifecycle refers to the process from the creation of the component to the destruction of the component. Milestone phases in the process expose hook functions that allow developers to write logic for the different phases. These functions are called “lifecycle functions.” The life cycle functions of micro channel small program custom components are as follows:

The mini program custom component lifecycle is closer to the Web Components specification than Vue and React. So let’s understand the life cycle of small program custom Components in conjunction with the Web Components specification. The Web Components specification introduces the concept of custom HTML elements. The goal is to create a custom UI component, similar to a small program. In the browser environment, each HTML tag has a corresponding Class. For example, paragraph nodes correspond to the HTMLParagraphElement Class. Elements that inherit this Class are custom HTML elements, such as the following code:

// Create a custom element
class MyCustomParagraphElement extends HTMLParagraphElement {
   / /...
}
// Register custom elements
customElements.define('custom-p', MyCustomParagraphElement);
Copy the code

Custom elements must be registered (or defined) before they can be used. The last line of this code is the registration logic, and the first parameter is the HTML tag name after the element was registered. Once registered, you can use the element directly in HTML, as follows:

<custom-p></custom-p>
Copy the code

This process is very similar to the custom components of wechat small programs, but the behavior of registering components is handled by the low level of small programs, and developers only need to write the code of the components themselves. The Web Components specification describes the lifecycle of custom HTML elements as a process shown in the following figure:

Comparing the lifecycle of the Web Components specification with that of small program custom Components, there are some similarities but not exactly the same, and the following points are summarized:

  • The attached and detached functions of small program custom Components correspond to the connectedCallback and disconnectedCallback functions of the Web Components specification, respectively.

  • The Moved function for the applette custom component is similar to but not identical to the Web Components specification’s adoptedCallback. Because applets do not support iframes, there is no migration of components across the document, only between different parent nodes of the same document. Therefore, there is no adopted state. The moved function can be understood as a variant of the adopted state;

  • Small program custom component unique lifecycle functions, created, ready and error;

  • The Web Components specification’s unique lifecycle function, attributeChangedCallback.

The main differences between the applet custom component and the Web Components specification can be seen in points 3 and 4. Why the difference?

Difference 1: Why doesn't the applet's custom component have an attributeChangedCallback function?

The first thing we need to know is when attributeChangedCallback is triggered, which is described by the Web Components specification as “when any attribute of a custom element changes (new, deleted, or updated).” Updating element attributes is a common behavior in traditional DOM programming. In the context of data-driven UI, most frameworks operate DOM indirectly through VDOM, so updating attributes is very rare in the current era.

Wechat applet, like Vue/React, does not allow direct manipulation of DOM, so it is fundamentally impossible to change DOM attributes. This explains why there is no attributeChangedCallback function in the life cycle of the applet custom component.

Difference 2: Why does the Web Components specification not have created/ Ready/Error functions?

The technical specification is a kind of guideline, and the specific implementation way often needs to be decided according to the reality; The same is true of the Web Components specification, which is separated from business and provides the most basic standards and references purely from the technical perspective. When it comes to the implementation level, frameworks like Vue/React have their own understandings, and wechat small programs also have their own uniqueness.

The differences are due both to the understanding and extension of the specification by the framework developers and to the actual business needs, so there are often “innovations” that are not covered by the specification, the most typical being the Document.Ready event. JQuery’s $(document).Ready event was popular in the front end of the tech world long before DOMContentLoad was introduced. This event occurs before window.onload, when the document state is rendered unfinished but interactive. So this event is frequently used in the FIT (First Load Time) to optimize site performance.

Back to the problem itself, the three functions created, ready and Error of small program custom components are similar to document.ready, which are “innovations” developed by combining the characteristics of the framework itself and business requirements beyond the standard specification.

To sum up, the core reason for the above two differences can be summarized in one sentence: theoretical norms need to be combined with realistic objective conditions when they are implemented. The specification is the reference standard of the upper level implementation, but it does not limit and frame the specific pattern of the upper level implementation. The difference is that the small program does not operate the DOM, and the difference is that the three functions created, ready and Error are beyond the specification, and the small program is an “innovation” according to its own technical characteristics. Once you understand the resource management and lifecycle of custom components, you can develop a good custom component. However, as mentioned above, there can be multiple custom components in a Page, all of which are serving the same Page, and there will inevitably be some flow of data. This is where you come across a very typical problem in the componentization domain: How do the components communicate with each other?

Develop a toast custom component

// toast.wxml
<view class="container {{mask? 'containerShowMask':'containerNoMask'}}" hidden="{{! status}}" style="z-index:{{zIndex}}">
  <view class="loreal-bg-class toast-bg" wx:if="{{mask}}"></view>
  <view class="loreal-class toast toast-{{placement || 'bottom'}}" style="padding-top:{{(placement || 'bottom')=== 'bottom' ? image || icon ? '25rpx': '': ''}}; position:relative; left:{{offsetX}}rpx; top:{{offsetY}}rpx; margin-bottom:{{distance}}px">
    <image class="loreal-image-class toast-icon" wx:if="{{image}}" src="{{image}}"/>
    <l-icon class="loreal-icon-class toast-icon toast-icon-{{icon === 'loading'? 'loading':''}}" wx:elif="{{icon && ! image}}" size="{{iconSize? iconSize : 60}}" color="{{iconColor? iconColor: icon === 'success'? '#00C292' : icon === 'error' ? '#F4516C' : '#ffffff'}}" name="{{icon}}"/>
    <slot wx:else/>
    <text class="toast-text loreal-title-class toast-text-{{placement}}" style="{{placement || 'bottom' === 'bottom' ? icon || image? 'margin-top:10rpx' : '': '' }}">{{ title }}</text>
  </view>
</view>

// toast.js
import validator from ".. /behaviors/validator";
  Component({
      externalClasses: ["loreal-class"."loreal-label-class"."loreal-hover-class"."loreal-img-class"."loreal-icon-class"].behaviors: [validator],
      properties: {
          name: {
              type: String.value: "lin"
          },
          type: {
              type: String.value: "default".options: ["warning"."success"."error"."default"]},plain: Boolean.size: {
              type: String.value: "medium".options: ["medium"."large"."mini"."long"]},shape: {
              type: String.value: "circle".options: ["square"."circle"."semicircle"]},disabled: {
              type: Boolean.value:!1
          },
          special: {
              type: Boolean.value:!1
          },
          loading: {
              type: Boolean.value:!1
          },
          width: Number.height: Number.icon: String.image: String.bgColor: String.iconColor: String.iconSize: String.openType: String.appParameter: String.lang: String.hoverStopPropagation: Boolean.hoverStartTime: {
              type: Number.value: 20
          },
          hoverStayTime: {
              type: Number.value: 70
          },
          sessionFrom: {
              type: String.value: ""
          },
          sendMessageTitle: String.sendMessagePath: String.sendMessageImg: String.showMessageCard: Boolean.formType: String
      },
      methods: {
          handleTap() {
              if (this.data.disabled || this.data.loading) return !1;
              this.triggerEvent("lintap", {}, {
                  bubbles:!0.composed:!0})},openTypeEvent(e) {
              this.triggerEvent(e.type, e.detail, {})
          }
      }
 });

// toast.wxss
.container{position:fixed}.containerNoMask{left:50%; top:50%; transform:translate(-50%, -50%)}.containerShowMask{height:100%; width:100%; top:0; left:0; display:flex; flex-direction:column; align-items:center; justify-content:center; z-index:999}.container .toast-bg{height:100%; width:100%; background:rgba(255.255.255.. 5); position:absolute; top:0; left:0}.container .toast-top{flex-direction:column-reverse}.container .toast-right{flex-direction:row}.container .toast-bottom{flex-direction:column}.container .toast-left{flex-direction:row-reverse}.container .toast{display:flex; align-items:center; justify-content:center; max-width:400rpx; min-width:280rpx; min-height:88rpx; background:rgba(0.0.0.7.); border-radius:12rpx; color:#fff; font-size:28rpx; line-height:40rpx; box-sizing:border-box; padding:30rpx 50rpx; z-index:999}.container .toast .toast-icon{margin-top:20rpx; margin-bottom:20rpx}.container .toast .toast-icon-loading{animation:loading-fadein 1.5s linear 0s infinite}.container .toast .toast-text{display:inline-block; text-align:center}.container .toast .toast-text-right{display:inline-block; text-align:center; margin-left:20rpx}.container .toast .toast-text-left{display:inline-block; text-align:center; margin-right:20rpx}.container .toast .toast-text-top{margin-bottom:10rpx}@keyframes loading-fadein{0% {transform:rotate(0)}100% {transform:rotate(360deg)}}
Copy the code

In addition, custom components have special lifecycles that are not strongly associated with the component, but sometimes the component needs to know about them for internal processing. This life cycle is called the “life cycle of the page on which the component resides” and is defined in the pageLifetimes definition section. The available life cycles include:

The generated component instance can be accessed through this in the component’s method, lifecycle function, and property observer. Components contain some common properties and methods.

Component sends data to the home page

The main form of interaction between components is custom events. The component triggers custom events via this.triggerEvent(), and the home page receives custom events by bind:myevent=”onMyEvent” on the component. The this.triggerEvent() method receives two objects, eventDetail and eventOptions, in addition to the custom event name.

<! -- In a custom component --><button bindtap="onTap">Clicking this button will trigger the "myevent" event</button>
Component({
  properties: {}
  methods: {
    // The child component fires the custom event
    ontap () {
    // All data to be carried to the main page is contained in eventDetail
    var eventDetail = {
            name:'sssssssss'.test: [1.2.3]}// whether bubbles bubbles, composed can cross component boundaries, and capturePhase has a capturePhase
    var eventOption = {
            composed: true
    }
    this.triggerEvent('myevent', eventDetail, eventOption)
    }
  }
})
Copy the code

The triggered events include:

Listen for an event

A custom component can fire arbitrary events that can be listened for by pages referencing the component. The way you listen for custom component events is exactly the same as the way you listen for base component events: you listen for values passed from the component in the Page event.

Page({
  onMyEvent: function(e){
    e.detail // Customizes the detail object that the component provides when it fires an event}})Copy the code

behaviors

Behaviors are features used to share code between components, similar to “mixins” or “traits” in some programming languages. Each behavior can contain a set of properties, data, lifecycle functions, and methods. When a component references it, its properties, data, and methods are incorporated into the component, and lifecycle functions are called at the appropriate time. Each component can reference more than one behavior. Behaviors can also refer to other behaviors.

// validator.js
module.exports = Behavior({
  behaviors: [].properties: {
    myBehaviorProperty: {
      type: String}},data: {
    myBehaviorData: {}},attached: function(){},
  methods: {
    myBehaviorMethod: function(){}}})Copy the code

When referencing components, list them individually in the Behaviors definition section.

// my-component.js
var myBehavior = require('my-behavior')
Component({
  behaviors: [myBehavior],
  properties: {
    myProperty: {
      type: String}},data: {
    myData: {}},attached: function(){},
  methods: {
    myMethod: function(){}}})Copy the code

Overwriting and composition rules for fields

The component and the behavior it references can contain fields of the same name. These fields are treated as follows: If there is a property or method of the same name, the property or method of the component itself will override the property or method of the behavior. If multiple behaviors are referenced, the property or method of the behavior later in the definition will override the property or method of the behavior earlier. If there are data fields with the same name, if the data is of object type, the objects will be merged, if the data is not of object type, they will be overwritten. The lifecycle functions do not override each other, but are called one by one at the appropriate firing time. If the same behavior is referenced multiple times by a component, the lifecycle functions defined by it are executed only once. Relationship between built-in Behavior components

<custom-ul>
  <custom-li> item 1 </custom-li>
  <custom-li> item 2 </custom-li>
</custom-ul>
Copy the code

In this example, both custom-ul and custom-Li are custom components that have relationships with each other, and the communication between them is often complex. This problem can be solved by adding the relations definition section to the component definition. Example:

// path/to/custom-ul.js
Component({
  relations: {
    './custom-li': {
      type: 'child'.// The associated target node should be a child node
      linked: function(target) {
        // This is executed every time a custom-Li is inserted. The target is the node instance object and is triggered after the node attached lifecycle
      },
      linkChanged: function(target) {
        // Execute each time a custom- Li is moved. The target is the instance object of the node and is triggered after the node moved lifecycle
      },
      unlinked: function(target) {
        // Execute every time a custom-Li is removed. Target is the instance object of the node and is triggered after the node detached lifecycle}}},methods: {
    _getAllLi: function(){
      // Use the getRelationNodes command to get an array of Nodes, which contains all the custom- Li associated nodes in order
      var nodes = this.getRelationNodes('path/to/custom-li')}},ready: function(){
    this._getAllLi()
  }
})
// path/to/custom-li.js
Component({
  relations: {
    './custom-ul': {
      type: 'parent'.// The associated target node should be the parent node
      linked: function(target) {
        Target is the instance object of the Custom -ul node, triggered after the Attached life cycle
      },
      linkChanged: function(target) {
        // Execute after each move. Target is the custom-ul node instance object, triggered after the Moved lifecycle
      },
      unlinked: function(target) {
        // Execute each time it is removed. Target is the custom-ul node instance object, triggered after the detached lifecycle}}}})Copy the code

The communication flow between components

Unlike Vue/React, applets do not have similar Vuex or Redux data flow management modules, so the communication process between custom components of applets is relatively primitive event-driven mode, that is, child components pass data to parent components by throwing events. The parent component passes data to the child component through properties.

Suppose there are two components in a Page of the applet. Both components depend on some properties of the parent component (Page), which are passed to the child component by properties, as shown in the figure below:

When component A needs to communicate with component B, it throws an event to notify the parent component Page, which receives the event and extracts the information carried by the event and passes it to component B via properties. This completes message passing between the child components.

In addition to event-driven communication, applets provide a more straightforward approach: the parent component directly gets the instance object of a child component through the selectComponent method, and then has access to any properties and methods of that child component. One of the properties of this child component is then passed to another child component via properties. The event-driven approach is more elegant and process-controlled, so event-driven communication is generally recommended.

conclusion

  • 1. The WXML node tag name can contain only lowercase letters, hyphens (-), and underscores (_). Therefore, the tag name of a custom component can contain only these characters.
  • 2. A custom component can also reference a custom component in a similar way to how a page references a custom component (using the usingComponents field).
  • 3. The name of the root directory of the project where the user-defined component and the page that uses the user-defined component reside cannot be prefixed with WX -; otherwise, an error message will be displayed.
  • 4. The previous version of the base library does not support custom components. In this case, the node that references the custom component will become the default empty node.