0
点赞
收藏
分享

微信扫一扫

微信小程序 Page,Component 二次封装(符合 vue2 的开发习惯)

微信小程序 Page,Component 二次封装

前言

相信很多开发过微信小程序的开发者都应该知道,微信小程序在语法上面跟vue很像,但是又有点区别。本文将讲述如何对微信小程序的Page页面构造器和Component构造器进行二次封装,抹平一些微信小程序与vue之间差别

目的

很多人可能会有疑问,为什么需要对PageComponent构造器进行二次封装。原因如下:

  • 对于不是很熟悉微信小程序开发,但是熟悉vue开发的开发者来说,将PageComponent封装成更加符合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

  • computedwatch微信小程序需要结合miniprogram-computed才能使用,我们在接下来会封装进去

  • onShowonReachBottom等字段是Page构造器特有的,需要在封装的时候保留下来

  • vue中,页面上的查询参数是存放在this.$route.query中的,但是微信小程序是在onLoad的回调参数中获取的,我们把微信小程序的页面查询参数挂载到this.$query中。

封装效果

最终我们封装出来的形式如下:

MyPage({
  mixins: [],
  data() {
    return {};
  },
//   也可以是
//   data:{},
  created() {},
  mounted() {},
  methods() {
    onClick(){}
  },
  computed: {},
  watch: {},
  destroyed() {},
  onShow(){},
  onReachBottom(){}
});

流程

实际上,很多字段都还是做了个映射而已,实际上并没有很大的改动。流程如下:

  • 检查data字段是函数还是对象,如果是函数,就需要执行这个函数,获取返回的对象

  • mixinsdatacreatedmountedcomputedwatchdestroyedonShowonReachBottom等字段映射到微信小程序对应的字段上面

  • methods需要扁平特殊处理,因为微信小程序的自定义函数方法是跟dataonShow等字段同级的,是兄弟关系

  • 检查是否使用了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.jsonLaunch函数执行完毕之后被调用

改造 app.js

这个生命周期函数需要对app.js进行改造才能生效,改造点如下:

  • 添加一个isLoad属性,用来标识app.jsonLaunch的函数是否执行完毕

  • 添加一个taskList异步任务队列,存放需要在onLaunch函数执行完毕之后所以需要执行的任务

  • onLaunch函数中如果有异步任务,需要使用asyncawait等待异步任务执行完毕。

  • 使用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字段是函数还是对象,如果是函数,就需要执行这个函数,获取返回的对象

  • datapropsmixinsmethodsbeforeCreatecreatedmounteddestroyedwatchcomputedrelationsclasses等字段映射到微信小程序对应的字段上面

  • 检查是否使用了computed等字段,如果使用了,就自动给behaviors字段添加一个computedBehavior(通过miniprogram-computed引进来的)

  • options字段默认开启multipleSlots:trueaddGlobalClass: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.$parentthis.$children 又向 vue开发习惯更加靠近了

总结

通过对PageComponent的二次封装,我们让微信小程序开发变得像vue开发一样轻松简单了。我们在封装的基础上,不仅添加某些方便又好用的功能,比如computedwatch属性,还简化了一些操作,比如父子组件的this.$parentthis.$children 的获取和定义,已经在内部封装,定义好了,开箱即可使用,不需要再去定义和获取。

当然,我们还可以定义更多的功能,比如,微信小程序修改响应式数据是使用this.setData({xxx:xxx}),而vue修改响应式数据是使用this.xxx=xxx,我们可以自己写一个数据代理的方法,把微信小程序修改响应式数据的方式变成this.xxx=xxx

举报

相关推荐

0 条评论