前言
github地址:https://github.com/yexiaochai/wxdemo
接上文繼續,我們前面學習了小程式的生命週期、小程式的標籤、小程式的樣式,後面我們寫了一個簡單的loading元件,顯然他是個半成品,我們在做loading元件的時候意識到一個問題:
1 2 3 4 |
小程式的元件事實上是標籤 我們沒有辦法獲得標籤的例項,至少我暫時沒有辦法 所以這些前提讓我們對標籤的認識有很大的不同,完成小程式特有的UI庫,那麼就需要從標籤出發 這裡面關注的點從js中的例項變成了wxml中的屬性 |
我們今天嘗試做幾個元件,然後先做未完成的loading,然後做訊息類彈出元件,然後做日曆元件,我希望在這個過程中,我們形成一套可用的體系,這裡涉及了元件體系,我們可能需要整理下流程:
① 首先我們這裡做的元件其實是“標籤”,這個時候就要考慮引入時候的怎麼處理了
② 因為寫業務頁面的同事(寫page的同事),需要在json配置中引入需要使用的標籤:
1 2 3 |
"usingComponents": { "ui-loading": "/components/ui-loading" } |
因為不能動態插入標籤,所以需要一開始就把標籤放入頁面wxml中:
1 |
<ui-loading is-show="{{isLoadingShow}}"></ui-loading> |
③ json中的配置暫時只能拷貝,但是我們可以提供一個ui-set.wxml來動態引入一些元件,如全域性使用的loading彈出類提示框
④ 像日曆類元件或者平時用的比較少的彈出層元件便需要自己在頁面中引入了,工作量貌似不大,後續看看情況,如何優化
⑤ 我們這裡給每個元件設定一個behaviors,behaviors原則只設定一層(這裡有點繼承的關係),層級多了變比較複雜了,彈出層類是一個、一般類一個(用於日曆類元件)
有了以上標準,我們這裡先來改造我們的loading元件
⑥ 預設所有的元件初期WXSS直接設定為隱藏
改造loading
這裡首先改造彈出層都要繼承的behaviors behavior-layer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
const util = require('../utils/util.js') module.exports = Behavior({ properties: { //重要屬性,每個元件必帶,定義元件是否顯示 isShow: { type: String } }, //這裡設定彈出層必須帶有一個遮蓋層,所以每個彈出層都一定具有有個z-index屬性 data: { maskzIndex: util.getBiggerzIndex(), uiIndex: util.getBiggerzIndex() }, attached: function() { console.log('layer') }, methods: { } }) |
其次我們改造下我們的mask元件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
let LayerView = require('behavior-layer') Component({ behaviors: [LayerView], properties: { //只有mask的z-index屬性需要被呼叫的彈出層動態設定 zIndex: { type: String } }, data: { }, attached: function () { console.log('mask') }, methods: { onTap: function() { this.triggerEvent('customevent', {}, {}) } } }) |
WXML不做變化,便完成了我們的程式碼,並且結構關係似乎更加清晰了,但是作為loading元件其實是有個問題的,比如點選遮蓋層要不要關閉整個元件,像類似這種點選遮蓋層要不要關閉整個元件,其實該是一個公共屬性,所以我們對我們的layer、mask繼續進行改造(這裡具體請看github程式碼):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
const util = require('../utils/util.js') module.exports = Behavior({ properties: { //重要屬性,每個元件必帶,定義元件是否顯示 isShow: { type: String } }, //這裡設定彈出層必須帶有一個遮蓋層,所以每個彈出層都一定具有有個z-index屬性 data: { maskzIndex: util.getBiggerzIndex(), uiIndex: util.getBiggerzIndex(), //預設點選遮蓋層不關閉元件 clickToHide: false }, attached: function() { console.log('layer') }, methods: { } }) |
1 2 3 4 5 6 7 8 9 10 |
methods: { onMaskEvent: function (e) { console.log(e); //如果設定了點選遮蓋層關閉元件則關閉 if (this.data.clickToHide) this.setData({ isShow: 'none' }); } } |
這個時候,點選要不要關閉,基本就在元件裡面設定一個屬性即可,但是我們這個作為了內部屬性,沒有釋放出去,這個時候我們也許發現了另外一個比較幽默的場景了:
我們因為沒法獲取一個標籤的例項,所以我們需要在頁面裡面動態呼叫:
1 2 3 4 5 6 7 8 9 10 11 12 |
onShow: function() { let scope= this; this.setData({ isLoadingShow: '' }); //3秒後關閉loading setTimeout(function () { scope.setData({ isLoadingShow: 'none' }); }, 3000); }, |
可以看到,標籤接入到頁面後,控制標籤事實上是動態操作他的屬性,也就是說操作頁面的狀態資料,頁面的UI變化全部是資料觸發,這樣的邏輯會讓介面變得更加清晰,但是作為全域性類的loading這種引數,我並不想放到各個頁面中,因為這樣會導致很多重複程式碼,於是我在utils目錄中新建了一個ui-util的工具類,作為一些全域性類的ui公共庫:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
//因為小程式頁面中每個頁面應該是獨立的作用域 class UIUtil { constructor(opts) { //用於儲存各種預設ui屬性 this.isLoadingShow = 'none'; } //產出頁面loading需要的引數 getPageData() { return { isLoadingShow: this.isLoadingShow } } //需要傳入page例項 showLoading(page) { this.isLoadingShow = ''; page.setData({ isLoadingShow: this.isLoadingShow }); } //關閉loading hideLoading(page) { this.isLoadingShow = 'none'; page.setData({ isLoadingShow: this.isLoadingShow }); } } //直接返回一個UI工具了類的例項 module.exports = new UIUtil |
index.js使用上產生一點變化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
//獲取公共ui操作類例項 const uiUtil = require('../../utils/ui-util.js'); //獲取應用例項 const app = getApp() Page({ data: uiUtil.getPageData(), onShow: function() { let scope= this; uiUtil.showLoading(this); //3秒後關閉loading setTimeout(function () { uiUtil.hideLoading(scope); }, 3000); }, onLoad: function () { } }) |
這樣,我們將頁面裡面要用於操作元件的資料全部放到了一個util類中,這樣程式碼會變得清晰一些,元件管理也放到了一個地方,只是命名規範一定要安規則來,似乎到這裡,我們的loading元件改造結束了,這裡卻有一個問題,我們在ui-util類中儲存的事實上是頁面級的資料,其中包含是元件的狀態,但是真實情況我們點選遮蓋層關閉元件,根本不會知會page層的資料,這個時候我們loading的顯示狀態搞不好是顯示,而真實的元件已經關閉了,如何保證狀態統一我們後面點再說,我暫時沒有想到好的辦法。
toast元件
我們現在先繼續作toast元件,toast元件一樣包含一個遮蓋層,但是點選的時候可以關閉遮蓋層,顯示3秒後關閉,顯示多久關閉的屬性應該是可以配置的(作為屬性傳遞),所以我們新增元件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
const util = require('../utils/util.js'); let LayerView = require('behavior-layer'); Component({ behaviors: [ LayerView ], properties: { message: { type: String } }, data: { }, attached: function () { console.log(this) }, methods: { onMaskEvent: function (e) { console.log(e); //如果設定了點選遮蓋層關閉元件則關閉 if (this.data.clickToHide) this.setData({ isShow: 'none' }); } } }) |
整體程式碼請各位在git上面去看,這裡也引起了一些問題:
① 我的元件如何居中?
② 一般來說toast消失的時候是可以定製化一個事件回撥的,我們這裡怎麼實現?
這裡我們先拋開居中問題,我們先來解決第二個問題,因為小程式中沒有addEventListener這個方法,所以能夠改變元件特性的方式只剩下資料操作,回顧我們這裡可以引起元件隱藏的點只有:
① toast中的點選彈出層時改變顯示屬性
1 2 3 4 5 6 7 8 |
onMaskEvent: function (e) { console.log(e); //如果設定了點選遮蓋層關閉元件則關閉 if (this.data.clickToHide) this.setData({ isShow: 'none' }); } |
② 然後就是頁面中動態改變資料屬性了:
1 2 3 4 5 6 7 8 |
onShow: function() { let scope= this; uiUtil.showToast(this, '我是美麗可愛的toast'); //3秒後關閉loading setTimeout(function () { uiUtil.hideToast(scope); }, 3000); }, |
這裡,我們不得不處理之前的資料同步問題了,我們應該給toast提供一個事件屬性可定義的點,點選遮蓋層的真正處理邏輯需要放到page層,其實認真思考下,標籤就應該很純粹,不應該與業務相關,只需要提供鉤子,與業務相關的是page中的業務,這個時候大家可以看到我們程式碼之間的關聯是多麼的複雜了:
① 頁面index.js依賴於index.wxml中元件的標籤,並且依賴於uiUtil這個工具類
② 單單一個toast元件(標籤)便依賴了mask標籤,一個工具欄,還有基礎的layer behavior
③ 因為不能獲取例項,所以元件直接通訊只能通過標籤的bindevent的做法,讓情況變得更加詭異
從這裡看起來,呼叫方式也著實太複雜了,而這還僅僅是一個簡單的元件,這個是不是我們寫法有問題呢?答案是!我的思路還是以之前做js的元件的思路,但是小程式暫時不支援動態插入標籤,所以我們不應該有過多的繼承關係,其中的mask是沒有必要的;另一方面,每個頁面要動態引入ui-utils這個莫名其妙的元件庫,似乎也很彆扭,所以我們這裡準備進行改造,降低沒有必要的複雜度
元件改造
經過思考,我們這裡準備做以下優化(PS:我小程式也是上星期開始學習的,需要逐步摸索):
① 保留mask元件,但是去除toast、loading類元件與其關聯,將WXML以及樣式直接內聯,使用空間複雜度降低程式碼複雜度
② 取消ui-uitil攻擊類,轉而實現一個page基類
我們這裡先重新實現toast元件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
//behavior-layer const util = require('../utils/util.js') module.exports = Behavior({ properties: { //重要屬性,每個元件必帶,定義元件是否顯示 isShow: { type: String } }, //這裡設定彈出層必須帶有一個遮蓋層,所以每個彈出層都一定具有有個z-index屬性 data: { maskzIndex: util.getBiggerzIndex(), uiIndex: util.getBiggerzIndex(), //預設點選遮蓋層不關閉元件 clickToHide: true }, attached: function() { console.log('layer') }, methods: { onMaskEvent: function (e) { this.triggerEvent('maskevent', e, {}) } } }) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
.cm-overlay { background: rgba(0, 0, 0, 0.5); position: fixed; top: 0; right: 0; bottom: 0; left: 0; } .cm-modal { background-color: #fff; overflow: hidden; width: 100%; border-radius: 8rpx; } .cm-modal--toast { width: auto; margin-top: -38rpx; background: rgba(0, 0, 0, 0.7); color: #fff; padding: 20rpx 30rpx; text-align: center; font-size: 24rpx; white-space: nowrap; position: fixed; top: 50%; left: 50%; } .cm-modal--toast .icon-right { display: inline-block; margin: 10rpx 0 24rpx 10rpx; } .cm-modal--toast .icon-right::before { content: ""; display: block; width: 36rpx; height: 16rpx; border-bottom: 4rpx solid #fff; border-left: 4rpx solid #fff; -webkit-transform: rotate(-45deg); transform: rotate(-45deg); -webkit-box-sizing: border-box; box-sizing: border-box; } |
1 2 3 4 5 |
<section class="cm-modal cm-modal--toast" style="z-index: {{uiIndex}}; display: {{isShow}}; "> {{message}} </section> <view class="cm-overlay" bindtap="onMaskEvent" style="z-index: {{maskzIndex}}; display: {{isShow}}" > </view> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
const util = require('../utils/util.js'); let LayerView = require('behavior-layer'); Component({ behaviors: [ LayerView ], properties: { message: { type: String } }, data: { }, attached: function () { console.log(this) }, methods: { } }) |
頁面層的使用不必變化就已經煥然一新了,這個時候我們開始做ui-util與page關係的改造,看看能不能讓我們的程式碼變得簡單,我這裡的思路是設計一個公共的abstract-view出來,做所有頁面的基類:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 |
class Page { constructor(opts) { //用於基礎page儲存各種預設ui屬性 this.isLoadingShow = 'none'; this.isToastShow = 'none'; this.toastMessage = 'toast提示'; //通用方法列表配置,暫時約定用於點選 this.methodSet = [ 'onToastHide', 'showToast', 'hideToast', 'showLoading', 'hideLoading' ]; //當前page物件 this.page = null; } initPage(pageData) { //debugger; let _pageData = {}; //為頁面動態新增操作元件的方法 Object.assign(_pageData, this.getPageFuncs(), pageData); //生成真實的頁面資料 _pageData.data = {}; Object.assign(_pageData.data, this.getPageData(), pageData.data || {}); console.log(_pageData); return _pageData; } //當關閉toast時觸發的事件 onToastHide(e) { this.hideToast(); } //設定頁面可能使用的方法 getPageFuncs() { let funcs = {}; for (let i = 0, len = this.methodSet.length; i < len; i++ ) { funcs[this.methodSet[i]] = this[this.methodSet[i]]; } return funcs; } //產出頁面元件需要的引數 getPageData() { return { isLoadingShow: this.isLoadingShow, isToastShow: this.isToastShow, toastMessage: this.toastMessage } } showToast(message) { this.setData({ isToastShow: '', toastMessage: message }); } hideToast() { this.setData({ isToastShow: 'none' }); } //需要傳入page例項 showLoading() { this.setData({ isLoadingShow: '' }); } //關閉loading hideLoading() { this.setData({ isLoadingShow: 'none' }); } } //直接返回一個UI工具了類的例項 module.exports = new Page abstract-view |
這裡還提供了一個公共模板用於被頁面include,abstract-view.wxml:
1 |
<ui-toast bindonToastHide="onToastHide" is-show="{{isToastShow}}" message="{{toastMessage}}"></ui-toast> |
頁面呼叫時候的程式碼發生了很大的變化:
1 2 3 4 5 |
<import src="./mod.searchbox.wxml" /> <view> <template is="searchbox" /> </view> <include src="../../utils/abstract-page.wxml"/> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
//獲取公共ui操作類例項 const _page = require('../../utils/abstract-page.js'); //獲取應用例項 const app = getApp() Page(_page.initPage({ data: { ttt: 'ttt' }, // methods: uiUtil.getPageMethods(), methods: { }, onShow: function () { let scope = this; this.showToast('我是美麗可愛的toast'); // 3秒後關閉loading // setTimeout(function () { // scope.hideToast(); // }, 3000); }, onLoad: function () { // this.setPageMethods(); } })) |
這樣我們相當於變相給page賦能了,詳情請各位看github上的程式碼:https://github.com/yexiaochai/wxdemo,這個時候,我們要為toast元件新增關閉時候的事件回撥,就變得相對簡單了,事實上我們可以看到這個行為已經跟元件本身沒有太多關係了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
showToast(message, callback) { this.toastHideCallback = null; if (callback) this.toastHideCallback = callback; let scope = this; this.setData({ isToastShow: '', toastMessage: message }); // 3秒後關閉loading setTimeout(function () { scope.hideToast(); }, 3000); } hideToast() { this.setData({ isToastShow: 'none' }); if (this.toastHideCallback) this.toastHideCallback.call(this); } |
1 |
this.showToast('我是美麗可愛的toast', function () { console.log('執行回撥')} ); |
當然這裡可以做得更加人性化,比如顯示時間是根據message長度動態設定的,我們這裡先這樣。
alert類元件
本篇篇幅已經比較長了,我們最後完成一個alert元件便結束今天的學習,明天主要實現日曆等元件,alert元件一般是一個帶確定框的提示彈出層,有可能有兩個按鈕,那個情況要稍微複雜點,我們這裡依舊為其新增元件結構wxml以及wxss:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
//獲取公共ui操作類例項 const _page = require('../../utils/abstract-page.js'); //獲取應用例項 const app = getApp() Page(_page.initPage({ data: { }, // methods: uiUtil.getPageMethods(), methods: { }, onShow: function () { global.sss = this; let scope = this; this.showMessage({ message: '我是一個確定框', ok: { name: '確定', callback: function () { scope.hideMessage(); scope.showMessage('我選擇了確定'); } }, cancel: { name: '取消', callback: function () { scope.hideMessage(); scope.showToast('我選擇了取消'); } } }); }, onLoad: function () { // this.setPageMethods(); } })) |
結語
github地址:https://github.com/yexiaochai/wxdemo
今天我們似乎找到了一個適合小程式的元件編寫方式,明天我們繼續完成一些元件,元件完成後我們便開始寫實際業務程式碼了