战争,信念,意志和情感,这些散发着光芒和硝烟的词汇,象一枚枚炮弹轰入咱们如今的生活。历史的记忆不会被抹灭。
当咱们在各自项目里幸福的拷贝着官方代码 demo,在 componnets
文件夹里使用 Component
方法书写一个个组件时,不要忘记,在 2018 年上半年之前,小程序是没有提供组件化方案的。html
当时,主要有两种解决方法,一种是 WePY 拷贝法,另外一种则是摩拜 template 法。小程序
好比有个最简单的按钮组件:微信小程序
<!-- components/button.wpy --> <template> <view class="button"> <button @tap="onTap">点这里</button> </view> </template> <!-- pages/index.wpy --> <template> <view class="container"> <wpy-button /> // button 组件1 <wpy-button2 /> // button 组件2 </view> </template>
通过编译后结果以下:api
<view class="container"> <view class="button"> <button bindtap="$wpyButton$onTap">点这里</button> </view> <view class="button"> <button bindtap="$wpyButton2$onTap">点这里</button> </view> </view>
为了方便变量隔离,因此引入到页面中的组件得单独命名:缓存
import wepy from 'wepy' import Button from '@/components/button' export default class Index extends wepy.page { components = { 'wpy-button': Button, 'wpy-button2': Button } ... }
有一些不便的地方,但也很好的解决了组件化缺失的问题。微信
有心的同窗可能记得当初咱们发了这篇文章:微信小程序组件化解决方案wx-component,当时主要讲了如何使用,此次讲讲技术的细节。app
主要利用小程序当时提供的 template
模板方法,使用方式以下:函数
<!-- pages/template/login.wxml --> <template name="login"> <view class="login">这是登陆组件</view> </template>
<!-- pages/login/index.wxml --> <import src='../../components/login/index.wxml'/> <view class="login-box"> <template is="login" data="{{...}}"></template> </view>
因为知道这只是临时的解决方法,最终还会迁移到微信官方组件化方案。了解到微信团队正在开发,就死皮赖脸找了微信研发同窗要下技术方案,以便后期迁移成本作到最低。最后微信同窗不耐烦的扔给咱们以下代码,并特别嘱咐不要泄露出去:oop
Component({ // 组件名 name: '', // 为其余组件指定别名 using: {}, // 相似mixins,组件间代码复用 behaviors: [], // 组件私有数据 data: { }, // 外部传入的组件属性 propties: { }, // 当组件被加载 attached () { }, // 当组件被卸载 detached () { }, // 组件私有方法 methods: { } })
一目了然,依照此文档实现一个简单的组件化方案也有了思路。组件化
因为没有办法在小程序全局注入 Component
方法,能够将组件代码以模块方式导出,在页面的 Page
方法里引入:
// components/login/index.wxml <template name="login"> <form bindsubmit="onLoginSubmit"> ... <button type="primary" formType="submit">{{btnText}}</button> </form> </template>
// components/login/index.js module.exports = { name: 'login', data: { btnText: '' } .... }
// pages/index/index.js Page({ data: { ... }, components: { login: { btnText: '开始', onLoginCallback() { ... } } } })
<!-- pages/index/index.wxml --> <import src='../../components/login/index.wxml'/> <view class="login-box"> <template is="login" data="{{...login}}"></template> </view>
在 Page
的传参里多了 components
属性,传入了组件名login
,以及组件对应的属性值和方法。为了使这些新增传参生效,那势必须要对 Page
进行改造。
如何用一行代码毁掉你的小程序,在小程序根目录的 app.js
里加入这段代码便可:
Page = funtion() {}
这样核心的 Page
的方法就被覆盖掉了,因此利用这个“特性”,能够改造 Page
方法:
// utils/wx.js var page = function() { // 改造代码 ... } module.exports = { page }
// app.js Page = require('./utils/wx').page
这就完成了独一无二的自定义的小程序 Page
的方法。
精简了核心的代码以下:
function noop() {} class Component { constructor (config) { // 兼容 onLoad onUnload 的写法 config.onLoad = config.onLoad || config.attached || noop config.onUnload = config.onUnload || config.detached || noop this.data = config.data || {} this.config = config this.methods = config.methods || {} for (let name in this.methods) { // 为了使组件事件绑定生效,直接挂在到 this 下 this[name] = methods[name] } } setData (data, deepExtend) { let name = this.name let parent = this.parent let mergeData = extend(deepExtend !== false, parent.data[name], data) let newData = {} newData[name] = mergeData this.data = mergeData // 更新页面的 data parent.setData(newData) } setName (name) { this.name = name } setParent (parent) { this.parent = parent } }
主要完成了三件事:
attached
和 detached
template
的 bindtap
等代码生效setData
功能有个细节,为了让你们容易理解(不泄露微信的方法名),分享到外部的文章用 onLoad
、onUnload
代替了 attached
、detached
,但内部早就开始用微信命名的这两个属性名,才有了代码中的兼容写法。
整理了大体的核心代码以下:
// 缓存下微信的 Page const originalPage = Page // 组件生命周期 const LIFETIME_EVENT = [ 'onLoad', 'onUnload' ] class MyPage { constructor (origin) { this.origin = origin this.config = {} this.children = {} this.childrenEvents = {} // 是否须要`components` let components = this.components = origin.components if (components) { this.config.data = {} for (let item in components) { let props = components[item] || {} let component = new Component(require(`../components/${item}/index`)) this.children[name] = component // 合并组件的 data extend(component.data, component.props) // ... // 合并组件的 method for (let fnName in component.methods) { this.config[fnName] = component.methods[fnName].bind(component) } // ... let childrenEvents = this.childrenEvents[item] = {} LIFETIME_EVENT.forEach((prop) => { childrenEvents[item][prop] = component.config[prop] }) } // 合并全部依赖组件的生命周期函数 LIFETIME_EVENT.forEach((prop) => { this.config[prop] = () => { for (let item in this.components) { this.childrenEvents[item][prop].apply(this.component, arguments) } this.origin[prop] && this.origin[prop].apply(this, arguments) } }) // 把新生成的 config 传给原始的微信的 Page 方法 originalPage(this.config) } else { // 没有依赖组件,直接透传给微信的 Page 方法 originalPage(origin) } } }
可能有点乱,其实就是不断 merge data
和 method
的过程。最终全部组件自定的数据和方法都被挂在到了 Page
的传参里。
最后,导出自定义的 page
:
// utils/wx.js const page = function (config) { return new MyPage(config) } module.exports = { page }
在 app.js
中覆盖掉原有的 Page
方法:
// app.js Page = require('./utils/wx').page
虽然知足业务了,但也是有些问题的,例如上面 MyPage
方法里的这段:
for (let fnName in component.methods) { this.config[fnName] = component.methods[fnName].bind(component) }
能够看出,直接把组件内部定义的方法,挂在到 config
中去了,这就要求页面的方法和组件的方法不能重名,这是为了方便 template
能够直接绑定组件定义的事件,只能经过把组件事件转移到页面的事件方法里。
也有不少不完善的地方,但经过内部约束代码规范也基本能够解决。
这种近乎 Hack 的方式支撑了摩拜单车小程序业务大半年的时间,期间产出了大大小小十多个组件。而因为组件内部基本是按照微信官方组件化 api 书写,等待官方推出组件化方案后,所有迁移过去的成本也大大减少。