微信小程序 Page,Component 二次封装
前言
相信很多开发过微信小程序的开发者都应该知道,微信小程序在语法上面跟vue很像,但是又有点区别。本文将讲述如何对微信小程序的Page页面构造器和Component构造器进行二次封装,抹平一些微信小程序与vue之间差别
目的
很多人可能会有疑问,为什么需要对Page和Component构造器进行二次封装。原因如下:
-
对于不是很熟悉微信小程序开发,但是熟悉
vue开发的开发者来说,将Page和Component封装成更加符合vue开发习惯,能够让他们更加快速上手小程序。对于新手来说是很友好的。 -
可自定义钩子函数(生命周期函数)。在实际的项目开发者,很多微信小程序都是需要获取用户信息的,而且获取用户信息是在
app.js中的,但是页面的渲染跟app.js的执行是异步的,也就是说,页面的onLoad生命周期函数执行完毕了,app.js可能还没获取到用户信息,导致页面获取不到用户信息。这也是很多人遇见的问题,网上也有很多解决的方案,但是比较麻烦。我们可以给二次封装的Page构造器自定义一个onAppLoad生命周期函数,该函数执行时间是在app.js获取完用户信息之后执行,这样子页面就可以获取得到用户的信息了。 -
扩展自定义功能。我们知道
vue是可以使用watch监听属性和computed计算属性。微信小程序也可以支持的,但是需要第三方npm包,使用起来比较麻烦,我们也可以在二次封装中,把watch监听属性和computed计算属性添加进去,简化一些流程(引包,设置behaviors等) -
统一管理。对于项目上来说,有些页面可能用户需要某种权限才能进来页面。基于这个需求,我们可以在二次封装好的
Page构造器中,统一检查用户权限,而不是在每个页面中写一次检查权限。
Page 封装
Page 常用字段
我们先来看看 Page常用的字段有哪些,不常用的我就不列举出来了,可自行查看文档。点击这里查看文档
Page({
// 混入
behaviors: [],
// 页面数据
data: {},
// 页面加载完毕
onLoad() {},
// 页面显示出来
onShow() {},
// 页面渲染完毕
onReady() {},
// 页面卸载
onUnload() {},
// 上拉触底
onReachBottom() {},
// 自定义函数
onClick() {},
});
vue 常用字段
我们再看看vue有哪些常用的字段
export default {
mixins: [],
data() {
return {};
},
created() {},
mounted() {},
methods() {},
computed: {},
watch: {},
destroyed() {},
};
字段对应关系
从上面对比,我们不难发现微信小程序和vue字段的对应关系
-
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和watch微信小程序需要结合miniprogram-computed才能使用,我们在接下来会封装进去 -
onShow,onReachBottom等字段是Page构造器特有的,需要在封装的时候保留下来 -
在
vue中,页面上的查询参数是存放在this.$route.query中的,但是微信小程序是在onLoad的回调参数中获取的,我们把微信小程序的页面查询参数挂载到this.$query中。
封装效果
最终我们封装出来的形式如下:
MyPage({
mixins: [],
data() {
return {};
},
// 也可以是
// data:{},
created() {},
mounted() {},
methods() {
onClick(){}
},
computed: {},
watch: {},
destroyed() {},
onShow(){},
onReachBottom(){}
});
流程
实际上,很多字段都还是做了个映射而已,实际上并没有很大的改动。流程如下:
-
检查
data字段是函数还是对象,如果是函数,就需要执行这个函数,获取返回的对象 -
将
mixins,data,created,mounted,computed,watch,destroyed,onShow,onReachBottom等字段映射到微信小程序对应的字段上面 -
methods需要扁平特殊处理,因为微信小程序的自定义函数方法是跟data,onShow等字段同级的,是兄弟关系 -
检查是否使用了
computed或者watch等字段,如果使用了,就自动给behaviors字段添加一个computedBehavior(通过miniprogram-computed引进来的) -
重写
onLoad生命周期函数,将页面地址的查询参数挂载到this.$query上
源码
最终代码如下:
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) {
// 保存原有的onLoad函数
const oldLoad = options.onLoad;
options.onLoad = function (query) {
// 挂载查询参数
this.$query = query;
if (isFunction(oldLoad)) {
// 执行原有的onLoad函数
oldLoad.call(this, query);
}
};
}
function proxyComputedAndWatch(MyOptions, options) {
if (MyOptions.computed || MyOptions.watch) {
options.behaviors = options.behaviors || [];
// 如果使用到了`computed`或者`watch`,就需要添加对应的`behaviors`,否则无效
options.behaviors.push(computedBehavior);
}
}
function MyPage(MyOptions) {
const options = {};
// 检查`data`字段是否为函数
if (isFunction(MyOptions.data)) {
MyOptions.data = MyOptions.data();
}
// 字段映射
mapKeys(MyOptions, options, {
data: "data",
onReachBottom: "onReachBottom",
onShow: "onShow",
mounted: "onReady",
created: "onLoad",
mixins: "behaviors",
computed: "computed",
watch: "watch",
destroyed: "onUnload",
});
// 扁平methods字段中的数据
proxyMethods(MyOptions.methods, options);
// 检查是否使用了`computed`或者`watch`字段
proxyComputedAndWatch(MyOptions, options);
// 重写`onLoad`生命周期函数,将页面地址的查询参数挂载到`this.$query`上
proxyOnLoad(MyOptions, options);
Page(options);
}
export default MyPage;
扩展自定义功能或者生命周期函数
我们以扩展一个onAppLoad生命周期函数为例,这个生命周期函数是在app.js的onLaunch函数执行完毕之后被调用
改造 app.js
这个生命周期函数需要对app.js进行改造才能生效,改造点如下:
-
添加一个
isLoad属性,用来标识app.js的onLaunch的函数是否执行完毕 -
添加一个
taskList异步任务队列,存放需要在onLaunch函数执行完毕之后所以需要执行的任务 -
onLaunch函数中如果有异步任务,需要使用async和await等待异步任务执行完毕。 -
使用
try-catch包裹函数体,在finally中,将isLoad属性标记为true,然后执行taskList中的任务
最终改造代码如下:
// app.js
App({
// 异步任务队列
taskList: [],
// 加载完毕
isLoad: false,
async onLaunch() {
// 加个try-catch,防止请求爆炸,taskList无法执行
try {
await this.getUserInfo();
// ...
} catch (error) {
console.log("首页出现的错误", error);
} finally {
this.isLoad = true;
// 加载完毕就执行任务队列
this.runTask();
}
},
// 获取用户信息
async getUserInfo(params) {
// ...
},
runTask() {
const taskList = this.taskList.slice();
if (taskList.length > 0) {
taskList.forEach((task) => {
task();
});
this.taskList = [];
}
},
});
实现 onAppLoad 函数
app.js改造完成之后,我们需要准备一个函数,这个函数的作用就是通过getApp()获取小程序全局唯一的app实例(实际上就是app.js中的this),然后通过app.isLoad判断app.js中的onLaunch函数是否执行完毕了,最终返回一个Promise
代码如下:
// 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;
重新改造 onLoad 生命周期函数
最后回来到我们的proxyOnLoad函数中,对onLoad函数重新进行改造,改造如下:
import onAppLoad from "./on-app-load";
function proxyOnLoad(MyOptions, options) {
const oldLoad = options.onLoad;
options.onLoad = function (query) {
this.$query = query;
// 检查是否使用了onAppLoad这个生命周期函数
if (isFunction(MyOptions.onAppLoad)) {
// 调用封装好的onAppLoad函数,执行`MyOptions.onAppLoad`这个生命周期函数
onAppLoad().then(() => {
MyOptions.onAppLoad.call(this, query);
});
}
if (isFunction(oldLoad)) {
oldLoad.call(this, query);
}
};
}
最终效果
最终使用效果如下:
MyPage({
onAppLoad() {
// todo,在这里就可以确保获取到用户信息
},
});
Component 封装
Component的封装跟Page的封装差不多,只是字段上面会有点差别,还有相比于Page构造器来说,Component会多了一些功能
Component 常用字段
我们先来看看 Component常用的字段有哪些,不常用的我就不列举出来了,可自行查看文档
Component({
// 一些选项
options: {},
// 组件接受的外部样式类
externalClasses: [],
// 混入
behaviors: [],
// 组件的对外属性
properties: {},
// 组件的内部数据
data: {},
// 组件自定义方法
methods: {},
// 组件实例刚刚被创建时执行
created() {},
// 组件实例进入页面节点树时执行
attached() {},
// 在组件布局完成后执行
ready() {},
// 组件间关系定义
relations: {},
// 组件实例被从页面节点树移除时执行
detached() {},
// 组件数据字段监听器
observers: {},
});
vue 常用字段
我们再看看vue有哪些常用的字段
export default {
mixins: [],
data() {
return {};
},
props: {},
beforeCreate() {},
created() {},
mounted() {},
methods() {},
computed: {},
watch: {},
destroyed() {},
};
字段对应关系
从上面对比,我们不难发现微信小程序和vue字段的对应关系
-
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需要结合miniprogram-computed才能使用 -
watch字段因为Component构造器已经有对应的字段observers实现了,所以watch字段不需要结合miniprogram-computed -
relations用来定义组件之间的关系,详情可点击这里查看。Component组件构造器特有字段,需要保留 -
options用来定义一些选项,比如是否开启全局样式(addGlobalClass:true),是否允许多插槽(微信小程序的组件默认只能允许有一个插槽,如果需要具名插槽,需要在该字段中声明multipleSlots:true)。Component组件构造器特有字段,需要保留 -
externalClasses组件接受的外部样式类。微信小程序组件的样式默认是隔离的(即页面样式不影响组件样式,组件样式也不影响页面样式)。我们在封装的过程中,会默认添加options.addGlobalClass=true,这样子组件就可以受页面样式的影响,方便修改组件的样式。但是如果是基于某个组件在进行二次封装组件的时候,组件样式是影响不了其他组件的样式的,需要通过externalClasses来指定样式类。Component组件构造器特有字段,对应封装后的是classes字段 -
跟
Page构造器封装一样,我们同样把页面的地址查询参数也挂载到this.$query上,但是Component获取页面地址的查询参数会跟Page获取的方式不一样,Component是在ready生命周期函数中,通过获取组件所在页面的实例,然后在获取得到页面的查询参数
封装效果
MyComponent({
mixins: [],
data() {
return {};
},
// 也可以是
// data:{},
props: {},
methods() {},
beforeCreate() {},
created() {},
mounted: {},
relations: {},
destroyed() {},
classes: [],
watch: {},
computed: {},
});
流程
-
检查
data字段是函数还是对象,如果是函数,就需要执行这个函数,获取返回的对象 -
将
data,props,mixins,methods,beforeCreate,created,mounted,destroyed,watch,computed,relations,classes等字段映射到微信小程序对应的字段上面 -
检查是否使用了
computed等字段,如果使用了,就自动给behaviors字段添加一个computedBehavior(通过miniprogram-computed引进来的) -
options字段默认开启multipleSlots:true和addGlobalClass:true -
重写
ready生命周期函数,将页面地址的查询参数挂载到this.$query上
源码
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() {
// 获取加载的页面
const pages = getCurrentPages();
//获取当前页面的对象
const currentPage = pages[pages.length - 1];
//如果要获取url中所带的参数可以查看options
const options = currentPage.options;
return options;
}
function proxyReady(MyOptions, options) {
// 保存原有的ready函数
const ready = options.ready;
options.ready = function () {
// 挂载查询参数
this.$query = getCurrentPageParam();
if (isFunction(ready)) {
// 执行原有的onLoad函数
ready.call(this);
}
};
}
function proxyComputed(MyOptions, options) {
// 如果使用到了`computed`,就需要添加对应的`behaviors`,否则无效
if (MyOptions.computed) {
options.behaviors = options.behaviors || [];
options.behaviors.push(computedBehavior);
}
}
function proxyProps(MyOptions, options) {
// vue的props写法和微信小程序的写法略有不同,需要特殊处理一下某些字段
if (options.properties) {
Object.keys(options.properties).forEach((name) => {
if (Array.isArray(options.properties[name])) {
options.properties[name] = null;
}
});
}
}
function MyComponent(MyOptions) {
const options = {};
// 检查`data`字段是否为函数
if (isFunction(MyOptions.data)) {
MyOptions.data = MyOptions.data();
}
// 字段映射
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",
});
// 检查是否使用了`computed`字段
proxyComputed(MyOptions, options);
// 特殊处理props的某些字段
proxyProps(MyOptions, options);
// 默认开启一些选项
options.options = {
multipleSlots: true,
addGlobalClass: true,
};
// 重写`ready`生命周期函数,将页面地址的查询参数挂载到`this.$query`上
proxyReady(MyOptions, options);
Component(options);
}
export default MyComponent;
添加扩展功能
添加 field 字段
微信小程序可以为自定义组件添加内置的behaviors(内置表单行为,详情点击这里查看),然后就可以使该自定义组件变成一个表单组件,拥有表单组件所对应的功能。
使用如下:
MyComponent({
// 声明为表单组件
field: true,
props: {
name: {
type: String,
},
value: {
type: String,
},
},
});
回到我们的MyComponent中,我们要进行如下改造:
function MyComponent(MyOptions) {
// ...
// 如果声明为表单组件
if (MyOptions.field) {
options.behaviors = options.behaviors || [];
// 添加内置的behaviors
options.behaviors.push("wx://form-field");
}
// ...
}
添加 relation 字段
有时候,我们会遇见一些具有父子或者祖孙关系的组件,这些组件需要进行通信(需要获取父组件实例或者子组件实例,然后使用实例的方法或者属性),这个时候就需要使用relations字段了,但是这个字段使用起来还是有点麻烦,所以我们可以继续在封装一个relation字段来简化流程。注意,relation只适用于只有一种身份的组件,如果具有多个身份的组件(既可以是作为某个组件子组件,也可以作为另外某些组件的父组件)并不适用,还是需要使用原生的relations字段
我们需要实现的功能是,往子组件中挂在this.$parent(即父组件实例),往父组件中挂在this.$children(即子组件实例),卸载的时候自动销毁。
封装代码如下:
const relationFunctions = {
// 关联的目标节点应为祖先节点
ancestor: {
// 子组件插入到父组件中时触发
linked(parent) {
this.$parent = parent;
},
// 子组件脱离父组件时触发
unlinked() {
this.$parent = null;
},
},
// 关联的目标节点应为子孙节点
descendant: {
// 子组件插入到父组件中时触发
linked(child) {
this.$children = this.$children || [];
this.$children.push(child);
},
// 子组件脱离父组件时触发
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") {
// 父组件类型
options.created = function () {
created && created.bind(this)();
// 默认添加$children属性
this.$children = this.$children || [];
};
options.detached = function () {
this.$children = [];
detached && detached.bind(this)();
};
}
// 将`relation`字段的东西合并到`relations`字段中
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) {
// 处理组件之间的关系
makeRelation(MyOptions, options);
}
// ...
}
假设我们现在有Form组件和FormItem组件,Form组件为父组件,FormItem组件为子组件
wxml 上面的用法:
<form>
<form-item></form-item>
<form-item></form-item>
</form>
Form组件
MyComponent({
relation: {
type: "descendant",
name: "../form-item/index", // 填写的是 form-item 组件所在路径
linked() {
this.updateChildren();
},
},
methods: {
updateChildren() {},
update() {
// this.$children
},
},
});
FormItem组件
MyComponent({
relation: {
type: "ancestor",
name: "../form/index", // 填写的是 form 组件所在路径
},
methods: {
update() {
// this.$parent
},
},
});
经过上述的操作,this.$parent 和 this.$children 又向 vue开发习惯更加靠近了
总结
通过对Page和Component的二次封装,我们让微信小程序开发变得像vue开发一样轻松简单了。我们在封装的基础上,不仅添加某些方便又好用的功能,比如computed和watch属性,还简化了一些操作,比如父子组件的this.$parent 和 this.$children 的获取和定义,已经在内部封装,定义好了,开箱即可使用,不需要再去定义和获取。
当然,我们还可以定义更多的功能,比如,微信小程序修改响应式数据是使用this.setData({xxx:xxx}),而vue修改响应式数据是使用this.xxx=xxx,我们可以自己写一个数据代理的方法,把微信小程序修改响应式数据的方式变成this.xxx=xxx










