戰爭,信念,意志和情感,這些散發着光芒和硝煙的詞彙,象一枚枚炮彈轟入咱們如今的生活。歷史的記憶不會被抹滅。
當咱們在各自項目裏幸福的拷貝着官方代碼 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 書寫,等待官方推出組件化方案後,所有遷移過去的成本也大大減少。