Magicodes.WeiChat——自定義knockoutjs template、component實現微信自定義菜單


本人一向比較喜歡折騰,玩了這么久的knockoutjs,總覺得不夠勁,於是又開始准備折騰自己了。

最近在完善Magicodes.WeiChat微信開發框架時,發現之前做的自定義菜單這塊太不給力了,而各種第三方平台在這一塊做得也比較渣,功能不全不說,界面還很不友好,於是決心重整一版,以滿足需求。

下面先上圖,新的UI界面如下所示:

QQ截圖20150924175358

QQ截圖20150924175501

QQ截圖20150924175555

QQ截圖20150924175538

 

如何實現這個功能呢?下面請等我一一道來吧。

左側樹形結構綁定

HTML模板如下所示:

<div class="dd" id="nestable2">
    <ol class="dd-list" data-bind="foreach:Menus()">
                                    <li class="dd-item lv1">
                                        <div class="dd-handle">
                                            <span class="pull-right">
                                                <i class="fa fa-plus" data-bind="click:$root.AddClick"></i> &nbsp;&nbsp;
                                                <i class="fa fa-times" data-bind="click:$root.RemoveItem"></i>&nbsp;&nbsp;
                                                <i class="fa fa-pencil" data-bind="click:$root.ItemClick"></i>
                                            </span>
                                            <span>
                                                <span class="label label-info"><i class="fa" data-bind="css:$root.getIconCssByType(type)"></i></span><span data-bind="text:name,click:$root.ItemClick"></span>
                                            </span>
                                        </div>
                                        <!-- ko if:$data.sub_button !== undefined  -->
                                        <ol class="dd-list" data-bind="foreach:$data.sub_button">
                                            <li class="dd-item lv2" data-id="2">
                                                <div class="dd-handle">
                                                    <span class="pull-right">
                                                        <i class="fa fa-times" data-bind="click:$root.RemoveItem"></i>&nbsp;&nbsp;
                                                        <i class="fa fa-pencil" data-bind="click:$root.ItemClick"></i>
                                                    </span>
                                                    <span class="label label-success"><i class="fa" data-bind="css:$root.getIconCssByType(type)"></i></span> <span data-bind="text:name"></span>
                                                </div>
                                            </li>
                                        </ol>
                                        <!-- /ko -->
                                    </li>
                                </ol>
</div>

這里我解釋一下,上述模板用到了兩個foreach循環,以便綁定這個兩級列表。實際上如果數據結構支持的話,ko是可以遞歸的綁定的。ko的強大性是毋庸置疑的。然后注意這個注釋:“<!-- ko if:$data.sub_button !== undefined  -->”,這個真的不是注釋,這個是有用的。為了不產生臟元素,ko支持這種綁定寫法。這里先用if做了判斷,然后再綁定子集。其余的,就是簡單的data-bind語法了。

通過上述模板,我們注意到數據結構中兩個關鍵點:Menus和sub_button,那我們就來看看viewModel。viewModel中定義了Menus = ko.observableArray([]),然后使用Ajax獲取數據來填充:

//初始化,加載數據
            this.Init = function () {
                mwc.ui.setBusy();
                self.Api.request('GET', {
                    url: '/api/Menus',
                    func: function (data) {
                        mwc.ui.clearBusy();
                        $.each(data, function (i, v) {
                            if (v.sub_button) {
                                $.each(v.sub_button, function (i1, v1) {
                                    v.sub_button[i1] = $.extend(self.getModelTpl(), v1);
                                })
                            }
                            data[i] = $.extend(self.getModelTpl(), v);
                        });
                        self.Menus(ko.mapping.fromJS(data));
                    }
                });
            };

注意,因為方便,這里使用了knockout.mapping js,請注意ko.mapping.fromJS方法。

右側編輯模板綁定

這塊無疑是比較復雜的一塊,我們先進行肢解:

  1. 通用模塊:頂部按鈕組、名稱輸入框、保存按鈕
  2. 模板(按微信類型加載不同模板)

我們先來看看整體的編輯模板:

<div class="ibox-title">
                    <h5>按鈕其他參數 </h5>
                </div>
                <div class="ibox-content" data-bind="with:EditModel" style="min-height: 600px;">
                    <form class="form-horizontal">
                        <!-- ko if:type() != 'empty' -->
                        <buttonschoices params="SelectsModel: $root.SelectTypes,SelectValue:type"></buttonschoices>
                        <div class="hr-line-dashed"></div>
                        <div class="form-group">
                            <label class="col-sm-2 control-label">名稱</label>
                            <div class="col-sm-10">
                                <input type="text" class="form-control" data-bind="value:name" required>
                            </div>
                        </div>
                        <div class="hr-line-dashed"></div>
                        <!-- /ko -->
                        <div data-bind="template:{name:$root.GetEditTemplateName,data:$root.EditModel,afterRender:$root.afterEditTemplateRender}">
                        </div>
                        <!-- ko if:type() != 'empty'  -->
                        <div>
                            <button class="btn btn-primary pull-right" type="button" data-bind="click:$root.Save">
                                <i class="fa fa-save"></i>
                                <strong>保存</strong>
                            </button>
                        </div>
                        <!-- /ko -->
                    </form>
                </div>

由模板可知,整個編輯模塊由類型按鈕組、名稱框、動態模板、保存按鈕組成。接下來我就先介紹下類型按鈕組的定義與綁定:

類型按鈕組——knockout component

如上述代碼中,使用了html標簽buttonschoices。而這個標簽就是我定義的knockout compoent。使用knockout compoent能做什么呢?就如上述代碼中,我們可以知道以下幾點:

  • 返回HTML模板
  • 傳遞參數,綁定compoent ViewModel

那么封裝knockout compoent,有助於我們封裝一些通用UI組件,就比如按鈕組類型選擇。我們先來一覽代碼:

//按鈕組選擇組件
ko.components.register('buttonschoices', {
    viewModel: function (params) {
        var self = this;
        //所選值
        this.SelectValue = ko.observable();
        //text:文本
        //value:值
        //icon:圖標
        //des:描述
        this.SelectItem = ko.observable({ text: "", value: "", icon: "", des: "" });
        //選擇模型
        this.SelectsModel = ko.observableArray([]);
        if (params && typeof (params.SelectsModel()) != "undefined") {
            self.SelectsModel(params.SelectsModel());
            if (typeof (params.SelectValue()) != "undefined") {
                self.SelectValue(params.SelectValue());
                self.SelectItem($.grep(self.SelectsModel(), function (v, i) { return v.value == self.SelectValue() })[0]);
            }
        }
        this.GetActiveCss = function (item) {
            return item.value == self.SelectValue() ? "active btn-primary" : "";
        }
        this.buttonClick = function (item) {
            self.SelectValue(item.value);
            self.SelectItem(item);
            params.SelectValue(item.value);
        }
    },
    template: '<div class="btn-group" data-bind="foreach: SelectsModel">' +
                    '<button class="btn btn-white" data-bind="css:$parent.GetActiveCss($data),click:$parent.buttonClick"><i class="fa" data-bind="css:icon"></i>&nbsp;<span data-bind="text:text"></span></button>' +
                '</div>' +
                '<div class="well" data-bind="with:SelectItem">' +
                    '<span data-bind="text:des"></span>' +
                '</div>'
});

整個組件代碼很簡潔明了,通過ko.components.register注冊組件,buttonschoices為組件名稱,整個組件由兩部分組成:

  • viewModel:視圖模型
  • template:模板

其中,viewModel接收了傳入參數,並且進行了處理。我們來依次解析這個viewModel:

  • SelectValue:所選指。這個所選指會根據傳入參數(還記得前面的“<buttonschoices params="SelectsModel: $root.SelectTypes,SelectValue:type"></buttonschoices>”嗎,其中SelectValue:type就是傳入了參數SelectValue)進行賦值,如右側代碼:self.SelectValue(params.SelectValue())。
  • SelectItem:所選項。項結構為{ text: "", value: "", icon: "", des: "" },分別代表文本、值、圖標和描述。
  • SelectsModel:選擇模型,就是列表模型。有多少個按鈕,就看其有多少個項了。傳入參數見“SelectsModel: $root.SelectTypes”。我們來看看這個$root.SelectTypes是怎么定義的:
//類型選擇
            this.SelectTypes = ko.observableArray([
                { text: "點擊推事件", value: "click", icon: "fa-font", des: "用戶點擊此類型按鈕后,微信服務器會通過消息接口推送消息類型為event    的結構給開發者(參考消息接口指南),並且帶上按鈕中開發者填寫的key值,開發者可以通過自定義的key值與用戶進行交互" },
                { text: "跳轉URL", value: "view", icon: "fa-link", des: "用戶點擊此類型按鈕后,微信客戶端將會打開開發者在按鈕中填寫的網頁URL,可與網頁授權獲取用戶基本信息接口結合,獲得用戶基本信息。" },
                { text: "掃碼推事件", value: "scancode_push", icon: "fa-qrcode", des: "用戶點擊按鈕后,微信客戶端將調起掃一掃工具,完成掃碼操作后顯示掃描結果(如果是URL,將進入URL),且會將掃碼的結果傳給開發者,開發者可以下發消息。" },
                { text: "掃碼推事件且彈出“消息接收中”提示框", value: "scancode_waitmsg", icon: "fa-qrcode", des: "用戶點擊按鈕后,微信客戶端將調起掃一掃工具,完成掃碼操作后,將掃碼的結果傳給開發者,同時收起掃一掃工具,然后彈出“消息接收中”提示框,隨后可能會收到開發者下發的消息。" },
                { text: "彈出系統拍照發圖", value: "pic_sysphoto", icon: "fa-camera", des: "用戶點擊按鈕后,微信客戶端將調起系統相機,完成拍照操作后,會將拍攝的相片發送給開發者,並推送事件給開發者,同時收起系統相機,隨后可能會收到開發者下發的消息。" },
                { text: "彈出拍照或者相冊發圖", value: "pic_photo_or_album", icon: "fa-camera", des: "用戶點擊按鈕后,微信客戶端將彈出選擇器供用戶選擇“拍照”或者“從手機相冊選擇”。用戶選擇后即走其他兩種流程。" },
                { text: "彈出微信相冊發圖器", value: "pic_weixin", icon: "fa-picture-o", des: "用戶點擊按鈕后,微信客戶端將調起微信相冊,完成選擇操作后,將選擇的相片發送給開發者的服務器,並推送事件給開發者,同時收起相冊,隨后可能會收到開發者下發的消息。" },
                { text: "彈出地理位置選擇器", value: "location_select", icon: "fa-map-marker", des: "用戶點擊按鈕后,微信客戶端將調起地理位置選擇工具,完成選擇操作后,將選擇的地理位置發送給開發者的服務器,同時收起位置選擇工具,隨后可能會收到開發者下發的消息。" },
                { text: "下發消息(除文本消息)", value: "media_id", icon: "fa-newspaper-o", des: "用戶點擊按鈕后,微信服務器會將開發者填寫的永久素材id對應的素材下發給用戶,永久素材類型可以是圖片、音頻、視頻、圖文消息。請注意:永久素材id必須是在“素材管理/新增永久素材”接口上傳后獲得的合法id。" },
                { text: "跳轉圖文消息URL", value: "view_limited", icon: "fa-envelope", des: "用戶點擊按鈕后,微信客戶端將打開開發者在按鈕中填寫的永久素材id對應的圖文消息URL,永久素材類型只支持圖文消息。請注意:永久素材id必須是在“素材管理/新增永久素材”接口上傳后獲得的合法id。" }
            ]);

眾所周知,微信自定義菜單支持10中類型的按鈕,那么這里是其類型的定義。這也說明,這個按鈕組是完全通用的,你只要給予與上述結構一致的數據,其就能顯示成當前效果。

  • GetActiveCss:獲取當前所選樣式。選中返回選中樣式,否則返回空。
  • buttonClick:按鈕點擊事件,這里拿到的是數據項,ko就是這么方便。然后值得注意的是,參數是雙向的,我們可以利用“params.SelectValue(item.value);”來回寫值,這樣編輯模型的類型值才會產生改變。

viewModel很簡單,template也很簡單,就是將剛才所說的viewModel綁定,用到了BootStrap按鈕組樣式“btn-group”,用foreach綁定SelectsModel,然后逐個綁定。

注意:

$parent表示父級對象,即乃父,因為foreach之后,其實對象已經指定到了乃父的兒子(SelectsModel)的某個兒子($data)上,而GetActiveCss是viewModel的女兒,自然要通過乃父來獲取了,畢竟其乃父的兒子的子孫並不是她。

$data表示當前項,即乃父的兒子的某個兒子,用於循環中獲取當前項數據。

with類似於using命名空間一樣,用了它,下面的元素都可以省卻改命名空間了。

是不是很簡單的樣子。我們再來說說模板:

動態加載模板

首先,我們先聚焦到以下代碼:

<div data-bind="template:{name:$root.GetEditTemplateName,data:$root.EditModel,afterRender:$root.afterEditTemplateRender}">
                        </div>

首先我們得明確以下內容:

template語法用於綁定模板,其中name用於指定模板名稱,這里綁定了$root.GetEditTemplateName方法,data用於指定模板的viewModel。

然后我們再來看看GetEditTemplateName怎么回事?如下所示:

//根據類型獲取編輯模板
            this.GetEditTemplateName = function (data) {
                switch (data.type()) {
                    case "empty":
                        return "emptyTemplate";
                    case "media_id":
                    case "view_limited":
                        return "media_idTemplate";
                    case "view":
                        return "urlTemplate"
                    default:
                        return "keyTemplate";
                }
            };

看起來也蠻簡單的樣子,就返回了一個模板名稱,那我們再繼續來看看這些模板。

<script id="emptyTemplate" type="text/html">
    <div class="well">
        <h3>注意事項:</h3>
        創建自定義菜單后,由於微信客戶端緩存,需要24小時微信客戶端才會展現出來。測試時可以嘗試取消關注公眾賬號后再次關注,則可以看到創建后的效果。
    </div>
</script>
<script id="keyTemplate" type="text/html">
    <div class="form-group" id="buttonDetails_url_area">
        <label class="col-sm-2 control-label">關鍵字</label>
        <div class="col-sm-10">
            <input type="text" class="form-control" data-bind="value:key" />
        </div>
    </div>
</script>

<script id="urlTemplate" type="text/html">
    <div class="form-group" id="buttonDetails_url_area">
        <label class="col-sm-2 control-label">鏈接</label>
        <div class="col-sm-10">
            <input type="url" class="form-control" data-bind="value:url" />
        </div>
    </div>
</script>
<script id="media_idTemplate" type="text/html">
    <news-choice-button params="value: media_id"></news-choice-button>
</script>
<div data-bind="with:EditModel">
    <news-choice-modal params="value: media_id"></news-choice-modal>
</div>

模板的定義也蠻簡單的,id和上面的字符串是一致的,類型必須為text/html。上面模板分別為空模板,關鍵字模板,鏈接模板和素材模板。

其中素材模板里面使用了自定義的component,和之前的buttonschoices一樣,封裝了多圖文選擇代碼。

由於組件news-choice-button和news-choice-modal需要講解的篇幅比較長,這里就暫不介紹了。

至於增刪改查,對於ko來說,都是操作數據模型。比如左側樹形結構的增刪,則是對Menus數組的增減操作,而編輯,則需要更新數組中的數據項。viewModel的修改,ko會自動重繪UI。這里就不多介紹了。

總結

通過使用knockoutjs 的動態模板,我們可以很方便的根據需要加載不同的模板進行綁定顯示。而通過knockoutjs component的封裝,我們可以很方便的實現對業務或者通用UI組件的封裝,以達到重復使用的目的。


免責聲明!

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



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