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