微信小程式開發框架搭建
使用開發工具的正確姿勢
微信提供的開發工具的編輯功能不是一般的水,寫程式碼肯定不能用它,否則就是浪費生命.不說別的,連自動儲存都沒有,第一次寫時寫了一個多小時,後面下班直接關掉,也不彈出提示說沒儲存.然後第二天過來,寫的程式碼全沒了!!! 頓時感到巨坑無比.這些工具開發人員吃乾飯的麼???
(後來的版本已經修復不能自動儲存的問題了,當然編輯功能還是不好用.)
它的正確用法是作為執行和除錯工具.
那麼適合作為編輯工具的是: webStorm.基於IntelJ核心,開啟Dracula主題,跟Android studio的使用習慣非常接近,so cool!各種方法提示,自動儲存,快速查詢…應有盡有.閉源的微信開發工具就不要用來寫程式碼了,珍惜生命.
webStorm要識別wxml和wxss,還需要配置一下檔案型別:(看下面別人截的圖)
記住html和css裡都要加上微信小程式對應的型別
綜上,開發時,用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: 沒有找到對應的內容 等等.
我們實際使用中需要的:
如果是大多數情況的請求時,只需要指定:
- url的尾部
- 該請求的非一般的引數
- 該請求由什麼動作引起的(第一次進入,重新整理,載入更多)
對於響應,我們只需要:
- 成功時:拿到data裡面的資料
- 失敗時,拿到失敗的資訊(細分一下,包括可以直接顯示給使用者的和不能讓使用者看到的),以及失敗狀態碼
- 資料為空的回撥:(常見於列表資料的載入)
我們期望的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();
},
頁面狀態管理
對於大多數網路請求後顯示的頁面,有這麼幾種頁面狀態:
- 第一次進入時,網路請求過程中:顯示”載入中”的狀態
- 載入的內容為空,顯示”空白頁面”
- 載入發生錯誤,顯示”錯誤頁面”,此頁面一般有一個點選重試的按鈕.該按鈕一般的邏輯是:如果沒有網路則點選後去開啟網路設定,如果有網路,則重新發送網路請求.
- 載入成功,就顯示內容頁.
對於已經載入成功了,顯示了內容頁的”下拉拉重新整理”:
- 頁面上方會有”重新整理中”的ui顯示,這個微信已經原生整合,無需處理.
- 重新整理成功,最好是彈出toast提示資料重新整理成功
- 重新整理失敗,可以不提示,也可以提示,看具體選擇.
對於一些分批載入的列表資料,一般還有上拉”載入更多”的功能:
參考微信文件中ui設計規範,上拉載入更多的ui提示應該放在頁面最下部佔一行,而不應該在頁面中間顯示一個大大的loading的效果.
- scrollview拉到最底部,觸發載入事件,顯示”載入中”的ui
- 載入成功,直接就將資料新增到原list上,這時也看不到最底部那行ui,所以不用處理
- 載入失敗,則在那一行顯示”載入失敗”的字樣,同時提示使用者”上拉重試”,或者在那一行放置一個按鈕,點選按鈕重試.
封裝
通過上面的分析,可以確定大部分頁面的通用狀態管理邏輯,那麼就可以設計通用的狀態管理模板了.
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
});
}
最終的效果:
常用頁面模板的封裝
整個頁面就是隻有一個簡單的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>