微信小程序開發04-打造自己的UI庫


前言

github地址:https://github.com/yexiaochai/wxdemo

接上文繼續,我們前面學習了小程序的生命周期、小程序的標簽、小程序的樣式,后面我們寫了一個簡單的loading組件,顯然他是個半成品,我們在做loading組件的時候意識到一個問題:

小程序的組件事實上是標簽
我們沒有辦法獲得標簽的實例,至少我暫時沒有辦法
所以這些前提讓我們對標簽的認識有很大的不同,完成小程序特有的UI庫,那么就需要從標簽出發
這里面關注的點從js中的實例變成了wxml中的屬性

我們今天嘗試做幾個組件,然后先做未完成的loading,然后做消息類彈出組件,然后做日歷組件,我希望在這個過程中,我們形成一套可用的體系,這里涉及了組件體系,我們可能需要整理下流程:

① 首先我們這里做的組件其實是“標簽”,這個時候就要考慮引入時候的怎么處理了

② 因為寫業務頁面的同事(寫page的同事),需要在json配置中引入需要使用的標簽:

"usingComponents": {
  "ui-loading": "/components/ui-loading"
}

因為不能動態插入標簽,所以需要一開始就把標簽放入頁面wxml中:

<ui-loading is-show="{{isLoadingShow}}"></ui-loading>

③ json中的配置暫時只能拷貝,但是我們可以提供一個ui-set.wxml來動態引入一些組件,如全局使用的loading彈出類提示框

④ 像日歷類組件或者平時用的比較少的彈出層組件便需要自己在頁面中引入了,工作量貌似不大,后續看看情況,如何優化

⑤ 我們這里給每個組件設置一個behaviors,behaviors原則只設置一層(這里有點繼承的關系),層級多了變比較復雜了,彈出層類是一個、一般類一個(用於日歷類組件)

有了以上標准,我們這里先來改造我們的loading組件

⑥ 默認所有的組件初期WXSS直接設置為隱藏

改造loading

這里首先改造彈出層都要繼承的behaviors behavior-layer:

 1 const util = require('../utils/util.js')
 2 module.exports = Behavior({
 3   properties: {
 4     //重要屬性,每個組件必帶,定義組件是否顯示
 5     isShow: {
 6       type: String
 7     }
 8   },
 9   //這里設置彈出層必須帶有一個遮蓋層,所以每個彈出層都一定具有有個z-index屬性
10   data: {
11     maskzIndex: util.getBiggerzIndex(),
12     uiIndex: util.getBiggerzIndex()
13   },
14   attached: function() {
15     console.log('layer')
16   },
17   methods: {
18   }
19 })

其次我們改造下我們的mask組件:

 1 let LayerView = require('behavior-layer')
 2 Component({
 3   behaviors: [LayerView],
 4   properties: {
 5     //只有mask的z-index屬性需要被調用的彈出層動態設置
 6     zIndex: {
 7       type: String
 8     }
 9   },
10   data: {
11   },
12   attached: function () { 
13     console.log('mask')
14   },
15   methods: {
16     onTap: function() {
17       this.triggerEvent('customevent', {}, {})
18     }
19   }
20 })

WXML不做變化,便完成了我們的代碼,並且結構關系似乎更加清晰了,但是作為loading組件其實是有個問題的,比如點擊遮蓋層要不要關閉整個組件,像類似這種點擊遮蓋層要不要關閉整個組件,其實該是一個公共屬性,所以我們對我們的layer、mask繼續進行改造(這里具體請看github代碼):

 1 const util = require('../utils/util.js')
 2 module.exports = Behavior({
 3   properties: {
 4     //重要屬性,每個組件必帶,定義組件是否顯示
 5     isShow: {
 6       type: String
 7     }
 8   },
 9   //這里設置彈出層必須帶有一個遮蓋層,所以每個彈出層都一定具有有個z-index屬性
10   data: {
11     maskzIndex: util.getBiggerzIndex(),
12     uiIndex: util.getBiggerzIndex(),
13     //默認點擊遮蓋層不關閉組件
14     clickToHide: false
15   },
16   attached: function() {
17     console.log('layer')
18   },
19   methods: {
20   }
21 })
 1 methods: {
 2   onMaskEvent: function (e) {
 3     console.log(e);
 4     //如果設置了點擊遮蓋層關閉組件則關閉
 5     if (this.data.clickToHide)
 6       this.setData({
 7         isShow: 'none'
 8       });
 9   }
10 }

這個時候,點擊要不要關閉,基本就在組件里面設置一個屬性即可,但是我們這個作為了內部屬性,沒有釋放出去,這個時候我們也許發現了另外一個比較幽默的場景了:

我們因為沒法獲取一個標簽的實例,所以我們需要在頁面里面動態調用:

 1 onShow: function() {
 2   let scope= this;
 3   this.setData({
 4     isLoadingShow: ''
 5   });
 6   //3秒后關閉loading
 7   setTimeout(function () {
 8     scope.setData({
 9       isLoadingShow: 'none'
10     });
11   }, 3000);
12 },

可以看到,標簽接入到頁面后,控制標簽事實上是動態操作他的屬性,也就是說操作頁面的狀態數據,頁面的UI變化全部是數據觸發,這樣的邏輯會讓界面變得更加清晰,但是作為全局類的loading這種參數,我並不想放到各個頁面中,因為這樣會導致很多重復代碼,於是我在utils目錄中新建了一個ui-util的工具類,作為一些全局類的ui公共庫:

 1 //因為小程序頁面中每個頁面應該是獨立的作用域
 2 class UIUtil {
 3   constructor(opts) {
 4     //用於存儲各種默認ui屬性
 5     this.isLoadingShow = 'none';
 6   }
 7   //產出頁面loading需要的參數
 8   getPageData() {
 9     return {
10       isLoadingShow: this.isLoadingShow
11     }
12   }
13   //需要傳入page實例
14   showLoading(page) {
15     this.isLoadingShow = '';
16     page.setData({
17       isLoadingShow: this.isLoadingShow
18     });
19   }
20   //關閉loading
21   hideLoading(page) {
22     this.isLoadingShow = 'none';
23     page.setData({
24       isLoadingShow: this.isLoadingShow
25     });
26   }
27 }
28 
29 //直接返回一個UI工具了類的實例
30 module.exports = new UIUtil

index.js使用上產生一點變化:

 1 //獲取公共ui操作類實例
 2 const uiUtil = require('../../utils/ui-util.js');
 3 //獲取應用實例
 4 const app = getApp()
 5 Page({
 6   data: uiUtil.getPageData(),
 7   onShow: function() {
 8     let scope= this;
 9     uiUtil.showLoading(this);
10     //3秒后關閉loading
11     setTimeout(function () {
12       uiUtil.hideLoading(scope);
13     }, 3000);
14   },
15   onLoad: function () {
16   }
17 })

這樣,我們將頁面里面要用於操作組件的數據全部放到了一個util類中,這樣代碼會變得清晰一些,組件管理也放到了一個地方,只是命名規范一定要安規則來,似乎到這里,我們的loading組件改造結束了,這里卻有一個問題,我們在ui-util類中存儲的事實上是頁面級的數據,其中包含是組件的狀態,但是真實情況我們點擊遮蓋層關閉組件,根本不會知會page層的數據,這個時候我們loading的顯示狀態搞不好是顯示,而真實的組件已經關閉了,如何保證狀態統一我們后面點再說,我暫時沒有想到好的辦法

toast組件

我們現在先繼續作toast組件,toast組件一樣包含一個遮蓋層,但是點擊的時候可以關閉遮蓋層,顯示3秒后關閉,顯示多久關閉的屬性應該是可以配置的(作為屬性傳遞),所以我們新增組件:

 1 const util = require('../utils/util.js');
 2 let LayerView = require('behavior-layer');
 3 
 4 Component({
 5   behaviors: [
 6     LayerView
 7   ],
 8   properties: {
 9     message: {
10       type: String
11     }
12   },
13   data: {
14   },
15   attached: function () { 
16     console.log(this)
17   },
18   methods: {
19     onMaskEvent: function (e) {
20       console.log(e);
21       //如果設置了點擊遮蓋層關閉組件則關閉
22       if (this.data.clickToHide)
23         this.setData({
24           isShow: 'none'
25         });
26     }
27   }
28 })

整體代碼請各位在git上面去看,這里也引起了一些問題:

① 我的組件如何居中?

② 一般來說toast消失的時候是可以定制化一個事件回調的,我們這里怎么實現?

這里我們先拋開居中問題,我們先來解決第二個問題,因為小程序中沒有addEventListener這個方法,所以能夠改變組件特性的方式只剩下數據操作,回顧我們這里可以引起組件隱藏的點只有:

① toast中的點擊彈出層時改變顯示屬性

1 onMaskEvent: function (e) {
2   console.log(e);
3   //如果設置了點擊遮蓋層關閉組件則關閉
4   if (this.data.clickToHide)
5     this.setData({
6       isShow: 'none'
7     });
8 }

② 然后就是頁面中動態改變數據屬性了:

1 onShow: function() {
2   let scope= this;
3   uiUtil.showToast(this, '我是美麗可愛的toast');
4   //3秒后關閉loading
5   setTimeout(function () {
6     uiUtil.hideToast(scope);
7   }, 3000);
8 },

這里,我們不得不處理之前的數據同步問題了,我們應該給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 //behavior-layer
 2 const util = require('../utils/util.js')
 3 module.exports = Behavior({
 4   properties: {
 5     //重要屬性,每個組件必帶,定義組件是否顯示
 6     isShow: {
 7       type: String
 8     }
 9   },
10   //這里設置彈出層必須帶有一個遮蓋層,所以每個彈出層都一定具有有個z-index屬性
11   data: {
12     maskzIndex: util.getBiggerzIndex(),
13     uiIndex: util.getBiggerzIndex(),
14     //默認點擊遮蓋層不關閉組件
15     clickToHide: true
16   },
17   attached: function() {
18     console.log('layer')
19   },
20   methods: {
21     onMaskEvent: function (e) {
22       this.triggerEvent('maskevent', e, {})
23     }
24   }
25 })
View Code
 1 .cm-overlay {
 2     background: rgba(0, 0, 0, 0.5);
 3     position: fixed;
 4     top: 0;
 5     right: 0;
 6     bottom: 0;
 7     left: 0;
 8 }
 9 
10 .cm-modal {
11   background-color: #fff;
12   overflow: hidden;
13   width: 100%;
14   border-radius: 8rpx;
15 }
16 
17 .cm-modal--toast {
18   width: auto;
19   margin-top: -38rpx;
20   background: rgba(0, 0, 0, 0.7);
21   color: #fff;
22   padding: 20rpx 30rpx;
23   text-align: center;
24   font-size: 24rpx;
25   white-space: nowrap;
26   position: fixed;
27   top: 50%;
28   left: 50%;
29 
30 }
31 .cm-modal--toast .icon-right {
32   display: inline-block;
33   margin: 10rpx 0 24rpx 10rpx;
34 }
35 .cm-modal--toast .icon-right::before {
36   content: "";
37   display: block;
38   width: 36rpx;
39   height: 16rpx;
40   border-bottom: 4rpx solid #fff;
41   border-left: 4rpx solid #fff;
42   -webkit-transform: rotate(-45deg);
43           transform: rotate(-45deg);
44   -webkit-box-sizing: border-box;
45           box-sizing: border-box;
46 }
View Code
1 <section class="cm-modal cm-modal--toast" style="z-index: {{uiIndex}}; display: {{isShow}}; ">
2   {{message}}
3 </section>
4 <view class="cm-overlay" bindtap="onMaskEvent" style="z-index: {{maskzIndex}}; display: {{isShow}}" >
5 </view>
 1 const util = require('../utils/util.js');
 2 let LayerView = require('behavior-layer');
 3 Component({
 4   behaviors: [
 5     LayerView
 6   ],
 7   properties: {
 8     message: {
 9       type: String
10     }
11   },
12   data: {
13   },
14   attached: function () { 
15     console.log(this)
16   },
17   methods: {
18   }
19 })

頁面層的使用不必變化就已經煥然一新了,這個時候我們開始做ui-util與page關系的改造,看看能不能讓我們的代碼變得簡單,我這里的思路是設計一個公共的abstract-view出來,做所有頁面的基類:

 1 class Page {
 2     constructor(opts) {
 3         //用於基礎page存儲各種默認ui屬性
 4         this.isLoadingShow = 'none';
 5         this.isToastShow = 'none';
 6         this.toastMessage = 'toast提示';
 7 
 8         //通用方法列表配置,暫時約定用於點擊
 9         this.methodSet = [
10             'onToastHide', 'showToast', 'hideToast', 'showLoading', 'hideLoading'
11         ];
12 
13         //當前page對象
14         this.page = null;
15     }
16     initPage(pageData) {
17         //debugger;
18 
19         let _pageData = {};
20 
21         //為頁面動態添加操作組件的方法
22         Object.assign(_pageData, this.getPageFuncs(), pageData);
23 
24         //生成真實的頁面數據
25         _pageData.data = {};
26         Object.assign(_pageData.data, this.getPageData(), pageData.data || {});
27 
28         console.log(_pageData);
29         return _pageData;
30     }
31     //當關閉toast時觸發的事件
32     onToastHide(e) {
33         this.hideToast();
34     }
35     //設置頁面可能使用的方法
36     getPageFuncs() {
37         let funcs = {};
38         for (let i = 0, len = this.methodSet.length; i < len; i++ ) {
39             funcs[this.methodSet[i]] = this[this.methodSet[i]];
40         }
41         return funcs;
42     }
43     //產出頁面組件需要的參數
44     getPageData() {
45         return {
46             isLoadingShow: this.isLoadingShow,
47             isToastShow: this.isToastShow,
48             toastMessage: this.toastMessage
49         }
50     }
51     showToast(message) {
52         this.setData({
53             isToastShow: '',
54             toastMessage: message
55         });
56     }
57     hideToast() {
58         this.setData({
59             isToastShow: 'none'
60         });
61     }
62     //需要傳入page實例
63     showLoading() {
64         this.setData({
65             isLoadingShow: ''
66         });
67     }
68     //關閉loading
69     hideLoading() {
70         this.setData({
71             isLoadingShow: 'none'
72         });
73     }
74 }
75 //直接返回一個UI工具了類的實例
76 module.exports = new Page
abstract-view

這里還提供了一個公共模板用於被頁面include,abstract-view.wxml:

<ui-toast bindonToastHide="onToastHide" is-show="{{isToastShow}}" message="{{toastMessage}}"></ui-toast>

頁面調用時候的代碼發生了很大的變化:

<import src="./mod.searchbox.wxml" />
<view>
  <template is="searchbox" />
</view>
<include src="../../utils/abstract-page.wxml"/>
 1 //獲取公共ui操作類實例
 2 const _page = require('../../utils/abstract-page.js');
 3 //獲取應用實例
 4 const app = getApp()
 5 
 6 Page(_page.initPage({
 7   data: {
 8     ttt: 'ttt'
 9 
10   },
11   // methods: uiUtil.getPageMethods(),
12   methods: {
13   },
14   onShow: function () {
15      let scope = this;
16      this.showToast('我是美麗可愛的toast');
17      // 3秒后關閉loading
18     //  setTimeout(function () {
19     //    scope.hideToast();
20     //  }, 3000);
21   },
22   onLoad: function () {
23     // this.setPageMethods();
24   }
25 }))

這樣我們相當於變相給page賦能了,詳情請各位看github上的代碼:https://github.com/yexiaochai/wxdemo,這個時候,我們要為toast組件添加關閉時候的事件回調,就變得相對簡單了,事實上我們可以看到這個行為已經跟組件本身沒有太多關系了:

 1 showToast(message, callback) {
 2   this.toastHideCallback = null;
 3   if (callback) this.toastHideCallback = callback;
 4   let scope = this;
 5   this.setData({
 6     isToastShow: '',
 7     toastMessage: message
 8   });
 9 
10   // 3秒后關閉loading
11   setTimeout(function () {
12     scope.hideToast();
13   }, 3000);
14 }
15 hideToast() {
16   this.setData({
17     isToastShow: 'none'
18   });
19   if (this.toastHideCallback) this.toastHideCallback.call(this);
20 }
this.showToast('我是美麗可愛的toast', function () { console.log('執行回調')} );

當然這里可以做得更加人性化,比如顯示時間是根據message長度動態設置的,我們這里先這樣。

alert類組件

本篇篇幅已經比較長了,我們最后完成一個alert組件便結束今天的學習,明天主要實現日歷等組件,alert組件一般是一個帶確定框的提示彈出層,有可能有兩個按鈕,那個情況要稍微復雜點,我們這里依舊為其新增組件結構wxml以及wxss:

 1 //獲取公共ui操作類實例
 2 const _page = require('../../utils/abstract-page.js');
 3 //獲取應用實例
 4 const app = getApp()
 5 
 6 Page(_page.initPage({
 7   data: {
 8   },
 9   // methods: uiUtil.getPageMethods(),
10   methods: {
11   },
12   onShow: function () {
13     global.sss = this;
14     let scope = this;
15     this.showMessage({
16       message: '我是一個確定框',
17       ok: {
18         name: '確定',
19         callback: function () {
20           scope.hideMessage();
21           scope.showMessage('我選擇了確定');
22         }
23       },
24       cancel: {
25         name: '取消',
26         callback: function () {
27           scope.hideMessage();
28           scope.showToast('我選擇了取消');
29         }
30       }
31     });
32 
33   },
34   onLoad: function () {
35     // this.setPageMethods();
36   }
37 }))

結語

github地址:https://github.com/yexiaochai/wxdemo

今天我們似乎找到了一個適合小程序的組件編寫方式,明天我們繼續完成一些組件,組件完成后我們便開始寫實際業務代碼了


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM