1. 程式人生 > >微信小程式開發框架搭建

微信小程式開發框架搭建

使用開發工具的正確姿勢

微信提供的開發工具的編輯功能不是一般的水,寫程式碼肯定不能用它,否則就是浪費生命.不說別的,連自動儲存都沒有,第一次寫時寫了一個多小時,後面下班直接關掉,也不彈出提示說沒儲存.然後第二天過來,寫的程式碼全沒了!!! 頓時感到巨坑無比.這些工具開發人員吃乾飯的麼???
(後來的版本已經修復不能自動儲存的問題了,當然編輯功能還是不好用.)

它的正確用法是作為執行和除錯工具.

那麼適合作為編輯工具的是: webStorm.基於IntelJ核心,開啟Dracula主題,跟Android studio的使用習慣非常接近,so cool!各種方法提示,自動儲存,快速查詢…應有盡有.閉源的微信開發工具就不要用來寫程式碼了,珍惜生命.
webStorm要識別wxml和wxss,還需要配置一下檔案型別:(看下面別人截的圖)
記住html和css裡都要加上微信小程式對應的型別

1478137328137_4.png

綜上,開發時,用webstorm來寫程式碼,用微信開發工具來執行和除錯,速度颼颼的!

網路請求的封裝

微信提供了底層網路驅動以及成功和失敗的回撥.但對於一個專案中的實際使用而言,仍然還是顯得繁瑣,還有很多封裝和簡化的空間.

wx.request({
  url: 'test.php',//請求的url
  data: {//請求的引數
     x: '' ,
     y: ''
  },
  header: {//請求頭
      'Content-Type': 'application/json'
  },
  method:"POST",
  success: function(res) {//成功的回撥
    console.log(res.data)
  }
})

網路框架二次封裝的一般姿勢

對於一個網路訪問來說,請求一般是get和post,拼上各種引數以及請求頭,然後拿到回來的響應,解析並得到最終需要的資料.

對於具體專案來說,請求時會有每個(或大多數)請求都要帶的引數,都要帶的請求頭,返回的資料格式可能都是一致的,那麼基於此,對微信的網路請求api進行二次封裝:

在我目前的專案中,

請求:

大多數請求是post,基本上每個請求都需要攜帶sessionId來與伺服器驗證登入狀態,還有很多請求是基於分頁的,需要帶上pageSize和pageIndex.
再跟頁面邏輯關聯起來,請求可能是因為第一次進入頁面,或者重新整理,或者上拉載入更多.

響應:

大多數拿到的資料格式是標準json格式,如下

{
    "code":1,
    "data":xxx,//可能是String,也可能是JsonObject,或JsonArray,也可能是null,或undefined
    "msg":yyy//可能為空

}

通過請求的狀態碼code來判斷這個請求是否真正成功.我們的專案中還有常見的是code=5:登入過期或未登入,code=2: 沒有找到對應的內容 等等.

我們實際使用中需要的:

如果是大多數情況的請求時,只需要指定:

  1. url的尾部
  2. 該請求的非一般的引數
  3. 該請求由什麼動作引起的(第一次進入,重新整理,載入更多)

對於響應,我們只需要:

  1. 成功時:拿到data裡面的資料
  2. 失敗時,拿到失敗的資訊(細分一下,包括可以直接顯示給使用者的和不能讓使用者看到的),以及失敗狀態碼
  3. 資料為空的回撥:(常見於列表資料的載入)

我們期望的api是:

netUtil.buildRequest(page,urlTail,params,callback)//必須的引數和回撥
.setXxx(xxx)//額外的設定,鏈式呼叫
..
.send();//最終發出請求的動作

基於上面的分析,封裝如下:

定義好攜帶構建請求的物件:

//這兩個錯誤碼是專案介面文件統一定義好的
const code_unlogin = 5;
const code_unfound = 2;

function requestConfig(){
    this.page;  //頁面物件
    this.isNewApi = true;
    this.urlTail='';
    this.params={
        pageIndex:0,
        pageSize:getApp().globalData.defaultPageSize,
        session_id:getApp().globalData.session_id
    };
    this.netMethod='POST';
    this.callback={
        onPre: function(){},
        onEnd: function(){

        },
        onSuccess:function (data){},
        onEmpty : function(){},
        onError : function(msgCanShow,code,hiddenMsg){},
        onUnlogin: function(){
            this.onError("您還沒有登入或登入已過期,請登入",5,'')
        },
        onUnFound: function(){
            this.onError("您要的內容沒有找到",2,'')
        }
    };

    this.setMethodGet = function(){
        this.netMethod = 'GET';
        return this;
    }

    this.setApiOld = function(){
        this.isNewApi = false;
        return this;
    }

    this.send = function(){
        request(this);
    }
}

請求的封裝

供我們呼叫的頂層api:

//todo 拷貝這段程式碼去用--buildRequest裡的callback
/*
 onPre: function(){},
 onEnd: function(){
       hideLoadingDialog(page);
 },
 onSuccess:function (data){},
 onEmpty : function(){},
 onError : function(msgCanShow,code,hiddenMsg){},
 onUnlogin: function(){
 this.onError("您還沒有登入或登入已過期,請登入",5,'')
 },
 onUnFound: function(){
 this.onError("您要的內容沒有找到",2,'')
 }

* */



/**
 * 注意,此方法呼叫後還要呼叫.send()才是傳送出去.
 * @param page
 * @param urlTail
 * @param params
 * @param callback  拷貝上方註釋區的程式碼使用
 * @returns {requestConfig}
 */
function buildRequest(page,urlTail,params,callback){
    var config = new requestConfig();
    config.page = page;
    config.urlTail = urlTail;

    if (getApp().globalData.session_id == null  || getApp().globalData.session_id == ''){
        params.session_id=''
    }else {
        params.session_id = getApp().globalData.session_id;
    }
    if (params.pageIndex == undefined || params.pageIndex <=0 || params.pageSize == 0){
        params.pageSize=0
    }else {
        if (params.pageSize == undefined){
            params.pageSize = getApp().globalData.defaultPageSize;
        }
    }
    log(params)
    config.params = params;

    log(config.params)

    //config.callback = callback;

    if(isFunction(callback.onPre)){
        config.callback.onPre=callback.onPre;
    }

    if(isFunction(callback.onEnd)){
        config.callback.onEnd=callback.onEnd;
    }

    if(isFunction(callback.onEmpty)){
        config.callback.onEmpty=callback.onEmpty;
    }

    if(isFunction(callback.onSuccess)){
        config.callback.onSuccess=callback.onSuccess;
    }

    if(isFunction(callback.onError)){
        config.callback.onError=callback.onError;
    }

    if(isFunction(callback.onUnlogin)){
        config.callback.onUnlogin=callback.onUnlogin;
    }
    if(isFunction(callback.onUnFound)){
        config.callback.onUnFound=callback.onUnFound;
    }
    return config;
}

最終請求的傳送:

function request(requestConfig){

    //檢驗三個公有引數並處理.這裡與上面有所重複,是為了相容之前寫的幾個api,不想改了.
    requestConfig.params.sessionId= getApp().globalData.sessionId;
    if (requestConfig.params.sessionId ==null  || requestConfig.params.sessionId == ''){
      delete  requestConfig.params.sessionId;
    }
    if (requestConfig.params.pageIndex ==0 || requestConfig.params.pageSize == 0){
       delete requestConfig.params.pageIndex ;
        delete  requestConfig.params.pageSize;
    }


    //var body = getStr("&", requestConfig.params);//拼接請求引數
    requestConfig.onPre();//請求發出前
    wx.request({
       // url: getApp().globalData.apiHeadUrl+requestConfig.urlTail+"?"+body,貌似這樣寫,同時不給data賦值,post請求也是可以成功的
        url: getApp().globalData.apiHeadUrl+requestConfig.urlTail,
        method:requestConfig.netMethod,
        data:requestConfig.params,
        header: {'Content-Type':'application/json'},
        success: function(res) {
            console.log(res);
            if(res.statusCode = 200){
                var responseData = res.data
                var code = responseData.code;
                var msg = responseData.message;

                if(code == 0){
                    var data = responseData.data;
                    var isDataNull = isOptStrNull(data);
                    if(isDataNull){
                        requestConfig.onEmpty();
                    }else{
                        requestConfig.onSuccess(data);
                    }
                }else if(code == 2){
                    requestConfig.onUnFound();
                }else if(code == 5){
                    requestConfig.onUnlogin();
                }else{
                    var isMsgNull = isOptStrNull(msg);
                    if(isMsgNull){
                        var isCodeNull = isOptStrNull(code);
                        if (isCodeNull){
                            requestConfig.onError("資料異常!,請核查",code,'');
                        }else {
                            requestConfig.onError("資料異常!,錯誤碼為"+code,code,'');
                        }

                    }else{
                        requestConfig.onError(msg,code,'');
                    }
                }
            }else if(res.statusCode >= 500){
                requestConfig.onError("伺服器異常!",res.statusCode,'');
            }else if(res.statusCode >= 400 && res.statusCode < 500){
                requestConfig.onError("沒有找到內容",res.statusCode,'');
            }else{
                requestConfig.onError("網路請求異常!",res.statusCode,'');
            }
        },
        fail:function(res){
            console.log("fail",res)
            requestConfig.onError("網路請求異常!",res.statusCode,'');

        },
        complete:function(res){
            // that.setData({hidden:true,toast:true});
        }
    })
}

將方法暴露,並在需要時引用:

方法寫在netUtil.js下,在該js檔案最下方暴露方法:

module.exports = {
 buildRequest:buildRequest

}

實際引用:

var netUtil=require("../../utils/netUtil.js");

實際使用時:

小技巧: js無法像java一樣定義好了介面,然後IDE自動生成程式碼.可以這樣: 將callback的空方法寫到netUtil的buildRequest方法上方的註釋區,每次用時,點選方法名跳到那邊去拷貝即可.

 var params = {};
params.id = id;

netUtil.buildRequest(that,API.Album.DETAIL,params,{
  onPre: function(){
    netUtil.showLoadingDialog(that);
  },
  onEnd:function(){

  },
  onSuccess:function (data){
    netUtil.showContent(that);
        ....
  },
  onEmpty : function(){

  },
  onError : function(msgCanShow,code,hiddenMsg){
    netUtil.showErrorPage(that,msgCanShow);
  },
  onUnlogin: function(){
    this.onError("您還沒有登入或登入已過期,請登入",5,'')
  },
  onUnFound: function(){
    this.onError("您要的內容沒有找到",2,'')
  }
}).send();

},

頁面狀態管理

對於大多數網路請求後顯示的頁面,有這麼幾種頁面狀態:

  1. 第一次進入時,網路請求過程中:顯示”載入中”的狀態
  2. 載入的內容為空,顯示”空白頁面”
  3. 載入發生錯誤,顯示”錯誤頁面”,此頁面一般有一個點選重試的按鈕.該按鈕一般的邏輯是:如果沒有網路則點選後去開啟網路設定,如果有網路,則重新發送網路請求.
  4. 載入成功,就顯示內容頁.

對於已經載入成功了,顯示了內容頁的”下拉拉重新整理”:

  1. 頁面上方會有”重新整理中”的ui顯示,這個微信已經原生整合,無需處理.
  2. 重新整理成功,最好是彈出toast提示資料重新整理成功
  3. 重新整理失敗,可以不提示,也可以提示,看具體選擇.

對於一些分批載入的列表資料,一般還有上拉”載入更多”的功能:

參考微信文件中ui設計規範,上拉載入更多的ui提示應該放在頁面最下部佔一行,而不應該在頁面中間顯示一個大大的loading的效果.

  1. scrollview拉到最底部,觸發載入事件,顯示”載入中”的ui
  2. 載入成功,直接就將資料新增到原list上,這時也看不到最底部那行ui,所以不用處理
  3. 載入失敗,則在那一行顯示”載入失敗”的字樣,同時提示使用者”上拉重試”,或者在那一行放置一個按鈕,點選按鈕重試.

封裝

通過上面的分析,可以確定大部分頁面的通用狀態管理邏輯,那麼就可以設計通用的狀態管理模板了.

ui的顯示是通過Page裡的data中的資料來控制的,並通過page.setData({xxx})來重新整理的,原先每個頁面都拷貝同樣的js屬性和wxml程式碼去實現封裝,後來進行了封裝,js屬性用方法來封裝,通過微信提供的template封裝共同的wxml程式碼,通過import或include匯入到wxml中(但是不知什麼bug,template一直無法起作用).

控制ui顯示與否的屬性的封裝

function netStateBean(){
//toast的是老api,工具升級後無需設定了
    this.toastHidden=true,
    this.toastMsg='',

    this.loadingHidden=false,
    this.emptyHidden = true,
    this.emptyMsg='暫時沒有內容,去別處逛逛吧',

    this.errorMsg='',
    this.errorHidden=true,

    this.loadmoreMsg='載入中...',
    this.loadmoreHidden=true,
}

頁面js裡的使用:

Page(
    data: {
      title:'名師',//todo 設定標題欄
      emptyMsg:'暫時沒有內容,去別處逛逛吧',//todo 空白頁面的顯示內容
      netStateBean: new netUtil.netStateBean(),
      ...
      },
      ...
    )

wxml裡:

模板

    <template name="pagestate" >
        <view class ="empty_view" wx:if="{{!emptyHidden}}"   >
            <view class="center_wrapper" >
                <view class="center_child" >
                    <icon type="info" size="45"/>
                    <view class="msg"> {{emptyMsg}}</view>
                </view>
            </view>
        </view>

        <view class ="error_view" wx:if="{{!errorHidden}}"  >
            <view class="center_wrapper">
                <view class="center_child" >
                    <icon type="warn" size="45" />
                    <view class="msg"> {{errorMsg}}</view>
                    <button  class = "retrybtn"  type="warn"  loading="{{btnLoading}}"
                             disabled="{{btnDisabled}}" catchtap="onRetry" hover-class="other-button-hover"> 點選重試 </button>
                </view>
            </view>
        </view>

     </template>

使用

 <!--狀態管理模板-->
<import src="../../template/pagestate.wxml"/>
<view >
    <template is="pagestate" data="{{...netStateBean}}"/>
</view>

js中提供API來控制頁面狀態:

module.exports = {
    showContent:showContent,
    showErrorPage:showErrorPage,
    showEmptyPage:showEmptyPage,
    loadMoreNoData:loadMoreNoData,
    loadMoreStart:loadMoreStart,
    loadMoreError:loadMoreError,
    hideLoadingDialog:hideLoadingDialog,
    showLoadingDialog:showLoadingDialog,
    showSuccessToast:showSuccessToast,
    dismissToast:dismissToast,
    showFailToast:showFailToast,
    ....

    }

    //具體的實現就是,拿到page物件中的data.netStateBean,修改部分數值,然後重新整理ui.
    function showEmptyPage(that){
        hideLoadingDialog(that);
        var bean = that.data.netStateBean;
        bean.emptyHidden = false;
        bean.loadingHidden = true;
        var empty = that.data.emptyMsg;
        if (isOptStrNull(empty)){//如果那個頁面沒有自定義空白說明文字,就使用預設的.
            empty = "沒有內容,去別的頁面逛逛吧"
        }
        bean.emptyMsg= empty;
        bean.contentHidden=true;//隱藏content頁面
        bean.errorHidden = true;//隱藏error頁面
        //重新整理UI
        that.setData({
            netStateBean: bean
        });
    }

最終的效果:

black.jpg

1478250101201_4.png

常用頁面模板的封裝

整個頁面就是隻有一個簡單的listview或gridview,資料從網路拉取,帶上拉重新整理和下拉載入更多的功能

分析

對於這種簡單的頁面來說,分析各頁面不同的地方:
1.上一個頁面傳入的引數:
2.網路請求的url
3.網路請求的部分引數(其中分批載入的每批大小和第幾批這兩個引數的key在整個專案中都是一樣的,每批大小的value可能不一樣)
4.response資料回來後,從哪個欄位中取出列表對應的資料,可能會不一樣
5.對列表資料datas的每一條,有些欄位的資料需要處理,處理的方式會不一樣.
6.UI中:item內容和樣式每個頁面會不一樣

7.標題欄文字
8.列表資料為空時的說明文字
9.非第一進入時呼叫onShow(相當於安卓中的onResume)時,是否自動重新整理頁面資料

js-頁面載入邏輯全部封裝在方法中:

 netUtil.requestSimpleList(that,pageIndex,action);

js–page裡:需要設定的都加上了todo註釋,高亮顯示,便於直接填內容

var utils=require("../../utils/util.js");
var netUtil=require("../../utils/netUtil.js");
var viewBeans=require("../../beans/viewBeans.js");
var infoBeans=require("../../beans/infoBeans.js");
var API=require("../../utils/API.js");

const request_firstIn = 1;
const request_refresh = 2;
const request_loadmore = 3;
const request_none = 0;

var app = getApp();
var that;
var id =0;  //頁面id
var intentDatas;
var isFristIn = true;

var needRefreshOnResume = false;//todo 頁面需要自己決定

Page({
  data: {
    title:'我的收藏',//todo 設定標題欄
      emptyMsg:'暫時沒有內容,去別處逛逛吧',//todo 空白顯示內容
      requestMethod:"POST",//todo 如果不是post,則在這裡改
      urlTail:API.User.MYCOLLECTION,//todo 需補全的,頁面請求的url

      netStateBean: new netUtil.netStateBean(),
      currentPageIndex:1,
      currentAction : 0,
      infos:[],//列表資料
  },

//以下四個方法是生命週期方法,已封裝好,無需改動
 onLoad: function(options) {
   that = this;
   intentDatas = options;
     if (that.data.emptyMsg != null && that.data.emptyMsg != '' ){
         that.data.netStateBean.emptyMsg = that.data.emptyMsg;
     }
    that.parseIntent(options);
   this.requestList(1,request_firstIn)
  },
  onReady: function () {
    wx.setNavigationBarTitle({
      title: this.data.title
    });
  },
  onHide:function(){

    },
  onShow:function(){
    if (isFristIn){
        isFristIn = false;
    }else {
        if (needRefreshOnResume){
           if (that.data.currentAction ==request_none){
                this.requestList(1,request_refresh);//重新整理
           }
        }
    }
  },
    //上拉載入更多
    onLoadMore: function(e) {
    console.log(e);
        console.log(that.data.currentAction +"---"+request_none);
     if (that.data.currentAction ==request_none){
         this.requestList(that.data.currentPageIndex+1,request_loadmore)
     }
  },
    //下拉重新整理,通過onPullDownRefresh來實現,這裡不做動作
    onRefesh: function(e) {
    console.log(e);
  },



 onPullDownRefresh:function(e){
     this.requestList(1,request_refresh);
},

    //針對純listview,網路請求直接一行程式碼呼叫
    requestList:function(pageIndex, action){
       netUtil.requestSimpleList(that,pageIndex,action)
    },

    //todo 滑動監聽,各頁面自己回撥
    scroll: function(e) {
        console.log(e)
    },
    //todo 將intent傳遞過來的資料解析出來
    parseIntent:function(options){

    },

    //todo 設定網路引數
    /**
     * 設定網路引數
     * @param params config.params
     * @param id 就是請求時傳入的id,也是成員變數-id
     */
    setNetparams: function (params) {

    },

    //todo 如果list資料是netData裡一個欄位,則更改此處
    getListFromNetData:function(netData){
        return netData;
    },

    //todo 資料的一些處理並重新整理資料
    handldItemInfo:function(info){

    }
})

wxml中,寫好空白頁面,錯誤頁面,載入更多欄

<view class="section" style="width: 100% ;height: 100%">

  <scroll-view wx:if="{{!netStateBean.contentHidden}}" scroll-y="true" style="height:1300rpx;position:relative; z-index:0;" lower-threshold="50"
               bindscrolltolower="onLoadMore" bindscrolltoupper="onRefesh" >

<!--todo listview-->

          <view class="list_data">
              <block wx:for-items="{{infos}}" wx:for-item="info" wx:for-index="index">
                  <navigator url="/pages/lession/{{info.classname}}?id={{info.id}}&title={{info.title}}" hover-class="">
                      <!--todo---listview: 這裡寫item的具體內容 -->

                  </navigator>
              </block>
          </view>


<!--todo gridview  同時js中getListFromNetData()方法返回utils.to2DimensionArr(netData,columnNum);-->

              <block wx:for-items="{{infos}}" wx:for-item="info" >
                  <view class="row-container">
                      <block wx:for-items="{{info}}" wx:for-item="item">
                          <navigator url="/pages/lession/album?id={{item.id}}" hover-class="">
                                <!--todo gridview 這裡寫item具體的內容-->

                          </navigator>
                      </block>
                  </view>
              </block>

      <!--載入更多的條欄-->
          <view class="loadmore_view" wx:if="{{!netStateBean.loadmoreHidden}}" >
              {{netStateBean.loadmoreMsg}}
          </view>
    </scroll-view>

    <!--空白頁面-->
    <view class ="empty_view" wx:if="{{!netStateBean.emptyHidden}}"   >
        <view class="center_wrapper" >
            <view class="center_child" >
                <icon type="info" size="45"/>
                <view class="msg"> {{netStateBean.emptyMsg}}</view>
            </view>
        </view>
    </view>

    <!--錯誤頁面-->
    <view class ="error_view" wx:if="{{!netStateBean.errorHidden}}"  >
        <view class="center_wrapper">
            <view class="center_child" >
                <icon type="warn" size="45" />
                <view class="msg"> {{netStateBean.errorMsg}}</view>
                <button  class = "retrybtn"  type="warn"  loading="{{loading}}"
                         disabled="{{disabled}}" bindtap="onRetry" hover-class="other-button-hover"> 點選重試 </button>
            </view>
        </view>
    </view>

</view>

日誌列印控制

function log(msg){
var isDebug = getApp().globalData.isDebug;
if (isDebug){
    console.log(msg);
}

}

android中的gridview在小程式裡的實現

小程式文件中只提供了一維的列表渲染示例,相對應的就是安卓中的列表listview.一維的陣列,一維的UI.
如果是實現gridview這種XY軸兩個方向的延伸,資料方面需要一個二維陣列,UI方法,需要兩層的巢狀渲染.

資料轉換:一維陣列轉換成二維陣列

/**
 *
 * @param arr 原始資料,是一個一維陣列
 * @param num 變成二維陣列後,每個小一維陣列的元素個數,也就是gridview中每行的個數
 */
function to2DimensionArr(arr,num){
    var newArr = new Array();//二維陣列
    if (arr == undefined){
        return newArr;
    }
    var subArr=null;
    for(var i =0;i<arr.length;i++){
        var item = arr[i];
        if((i%num==0) ||subArr==null){
            subArr = new Array();//內部的一維陣列
            newArr.push(subArr);
        }
        subArr.push(item);
    }
    return newArr;
}

UI上巢狀渲染:

           <block wx:for-items="{{infos}}" wx:for-item="info" >
                  <view class="row-container">
                      <block wx:for-items="{{info}}" wx:for-item="albumItem">
                          <view class="row-album-item">
                              <navigator url="/pages/lession/album?id={{albumItem.id}}" hover-class="">
                                  <image mode="aspectFill" src="{{albumItem.coverUrl}}" class="album-cover-img"/>
                                  <text class="album-name">{{albumItem.name}}</text>
                              </navigator>
                          </view>
                      </block>
                  </view>
             </block>

效果

1478249563128_2.png

程式碼