战争,信念,意志和情感,这些散发着光芒和硝烟的词汇,象一枚枚炮弹轰入咱们如今的生活。历史的记忆不会被抹灭。html
当咱们在各自项目里幸福的拷贝着官方代码 demo,在 componnets
文件夹里使用 Component
方法书写一个个组件时,不要忘记,在 2018 年上半年之前,小程序是没有提供组件化方案的。前端
当时,主要有两种解决方法,一种是 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>
复制代码
因为知道这只是临时的解决方法,最终还会迁移到微信官方组件化方案。了解到微信团队正在开发,就死皮赖脸找了微信研发同窗要下技术方案,以便后期迁移成本作到最低。最后微信同窗不耐烦的扔给咱们以下代码,并特别嘱咐不要泄露出去-_-:函数
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 书写,待官方推出组件化方案后,所有迁移过去的成本也大大减少。
美团单车事业部(摩拜单车)诚招前端 / 小程序研发工程师,位置北京,有兴趣能够发简历到 zhangshibing@mobike.com 或者扫二维码先加微信勾搭:)