戰爭,信念,意志和情感,這些散發着光芒和硝煙的詞彙,象一枚枚炮彈轟入咱們如今的生活。歷史的記憶不會被抹滅。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 或者掃二維碼先加微信勾搭:)