在开发微信小程序时,发现缺乏了组件化开发体验,在网上找了一波资源,发现都不是很好。其中,有用开发Vue的方式去开发小程序,好比,WePY,最后将源代码编译成小程序的官方文件模式。这种方式,开发感受爽,可是若是小程序版本升级变了以后,不在支持这种方式,那么就得从新开发一套小程序官方支持的代码了,成本代价很大。而且,此次项目时间很是紧,团队成员不熟悉vue的状况下,不敢用WePY。可是,小程序官方又对组件化支持不是很友好。因而,决定本身弄一套,既有组件化开发体验,又是最大限度的接近小程序官方的开发模式。javascript
目前项目已经成功上线,小程序:会过精选
源码地址以及实例地址vue
因为小程序的页面定义是经过Page
方法去定义的,那么,Page
必定在小程序内能够认为是一个全局变量,我只须要改写Page
这个方法,去能够引用组件,调用组件,触发组件的生命周期方法,维持组件内部的数据状态,那么,是否是就能够接近了组件化的编程体验了,而且能够抽离经常使用组件,达到复用的目的。java
//先保存原Page const nativePage = Page; /* 自定义Page */ Page = data => { //...改写Page逻辑,增长本身的功能 //最后必定得调用原Page方法,否则,小程序页面没法生成 nativePage(c); };
确保后面页面调用的Page
是咱们改写的,那么,必须在小程序启动时引入这个文件,达到改写Page
的目的。git
//在app.js 头部引入,假如咱们的文件名叫registerPage.js import "./registerPage"; App();
原page
的参数是一个对象,这个对象里定义了页面的data
,生命周期方法,等等。若是要引入组件,我得定一个字段,用来代表,须要引入的组件。我决定用componnets
这个字段去引入当前页面须要引入的组件。components
是一个数组,能够同时引入多个不一样的组件。es6
//在components中引入页面须要的组件,咱们这里引入了Toast和LifeCycle这2个组件 Page({ components: ["Toast", "LifeCycle"], data: { motto: "Hello World", userInfo: {} } });
经过components
,代表了须要引入的组件。那么,咱们须要注入组件的相关数据和方法到当前页面,以保证当前页面内能调用组件的方法,或更改组件的数据状态,以达到页面的更新。为了实现这个,咱们须要定义规范组件的结构,这样才能正确拿到组件的相关信息。咱们定义的组件格式为github
//定义了一个初始化组件的方法initComponent,这个方法就是返回一个对象,跟page里的参数相似,描述了组件了相关信息。 function initComponent() { return { timer: null, data: { content: "" }, show: function(msg, options) { } }; } export { initComponent };
有了组件的相关信息,咱们须要把这些信息自动注入到页面中,这样,在页面中才能与组件通讯,而且也须要把页面的信息引入到组件内,这样,在组件中也可与父级页面通讯。其中,组件内部最为重要的就是data
字段了,这个字段内的数据变化了,也要保证页面自动刷新,跟页面功能同样。为了隔离各个组件内部的数据,我对每一个组件默认定一个命名空间,这个命名空间就是组件的名字。把组件内部的数据挂在本身的命名空间下,再把这个命名空间挂到页面的data
下。同时,把组件的方法和其余熟悉以组件名.方法名或属性名
的方法挂到页面下。这样,组件的相关信息就都注入到页面中了。编程
//挂载组件的data,以组件名为命名空间挂载 if (v.data) { o.data = { ...o.data, [v.name]: v.data }; } //挂载组件的方法,以【组件名.方法名】挂载 let fns = Object.keys(v).filter( vv => v.hasOwnProperty(vv) && typeof v[vv] === "function" ); for (let fn of fns) { o[`${v.name}.${fn}`] = function() { let newThis = createComponentThis(v, this); let args = Array.from(arguments); args.length < 5 ? v[fn].call(newThis, ...args) : v[fn].apply(newThis, args); }; }
为了在组件内调用本身的方法,有本身的做用域,咱们必须为每一个组件建立一个独立的做用域,以隔离组件和父级页面做用域。保证了,在组件内部更改this
,不会对父级页面有影响。同时,组件内部也必须有和父级页面相似的setData
方法,达到一样的刷新页面的目的。咱们定义一组保护的属性名。小程序
/* 受保护的属性 */ const protectedProperty = ["name", "parent", "data", "setData"];
name
是组件的名称,parent
是对父级页面的引用,data
是组件内部数据状态,setData
是跟父级页面相似的方法,用来更改组件内部本身的数据。微信小程序
建立组件做用域数组
/* 建立一个新的Component做用域 */ const createComponentThis = (component, page) => { let name = component.name; if (page[`__${name}.this__`]) { return page[`__${name}.this__`]; } let keys = Object.keys(component); let newThis = Object.create(null); let protectedKeys = protectedProperty.concat(protectedEvent); let otherKeys = keys.filter(v => !~protectedKeys.indexOf(v)); for (let key of otherKeys) { if (typeof component[key] === "function") { Object.defineProperty(newThis, key, { get() { return page[`${name}.${key}`]; }, set(val) { page[`${name}.${key}`] = val; } }); } else { Object.defineProperty(newThis, key, { get() { return component[`${key}`]; }, set(val) { component[`${key}`] = val; } }); } } Object.defineProperty(newThis, "name", { configurable: false, enumerable: false, get() { return name; } }); Object.defineProperty(newThis, "data", { configurable: false, enumerable: false, get() { return page.data[name]; } }); Object.defineProperty(newThis, "parent", { configurable: false, enumerable: false, get() { return page; } }); Object.defineProperty(newThis, "setData", { value: function(data) { page.setData(parseData(name, this.data, data)); }, enumerable: false, configurable: false }); page[`__${name}.this__`] = newThis; return newThis; };
每一个组件必须均可以定义本身的生命周期方法,这些生命周期方法与父级页面的同样。由于,组件的生命周期方法必须是在父级页面的生命周期方法内触发的。必须是小程序官方支持的。
/* 受保护的页面事件 */ const protectedEvent = [ "onLoad", "onReady", "onShow", "onHide", "onUnload", "onPullDownRefreash", "onReachBottom", "onPageScroll" ];
咱们必须把组件的生命周期方法挂在父级页面的对应的生命周期方法内,这样,才能在触发父级页面的生命周期方法时,自动触发组件对应的生命周期方法。其中,先是触发完全部的组件的方法,再最后触发父级页面的方法
/* 绑定子组件生命周期钩子函数 */ const bindComponentLifeEvent = page => { let components = page.components; for (let key of protectedEvent) { let symbols = page[Symbol["for"](key)]; let pageLifeFn = page[key]; if (Array.isArray(symbols) && symbols.length > 0) { if (typeof pageLifeFn === "function") { symbols.push({ fn: pageLifeFn, type: "page", context: page }); } page[key] = function() { let pageThis = this; let args = Array.from(arguments); for (let ofn of symbols) { let currentThis; if (ofn.type === "component") { currentThis = createComponentThis(ofn.context, pageThis); } else { currentThis = pageThis; } args.length < 5 ? ofn.fn.call(currentThis, ...args) : ofn.fn.apply(currentThis, args); } }; } } };
经过上述这些步骤改写Page
以后,那么我就能够快速开始了个人小程序组件化编程体验了。
其实原理以下:
注意点: