avm.js 跨端開發 | 拼團商城項目源碼解析

此項目爲拼團商城類型,主要功能包括商品分類、商品詳情、商品搜索、拼團、訂單管理等。 項目源碼在 https://github.com/apicloudcom/group-ec 倉庫的 widget 目錄下。html

項目中前端採用 avm 多端開發技術進行開發,要點包括 TabLayout 佈局、swiper 輪播圖、rich-text 富文本、scroll-view 滾動視圖、下拉刷新、組件封裝等。使用 APICloud 多端技術進行開發,實現一套代碼多端運行,支持編譯成 Android & iOS App 以及微信小程序。前端

項目後端則是使用的 APICloud 數據雲 3.0 自定義雲函數來構建的。node

使用步驟

1. 使用 APICloud Studio 3 做爲開發工具。git

2. 下載本項目源碼。github

3. 在開發工具中新建項目,並將本源碼導入新建的項目中,注意更新 config.xml 中的 appid 爲你項目的 appid。ajax

4. 使用 AppLoader 進行真機同步調試預覽。json

5. 或者提交項目源碼,併爲當前項目雲編譯自定義 Loader 進行真機同步調試預覽。小程序

6. 雲編譯 生成 Android & iOS App 以及微信小程序源碼包。後端

若是以前未接觸過 APICloud 開發,建議先了解一個簡單項目的初始化、預覽、調試和打包等操做,請參考 APICloud 多端開發快速上手教程。微信小程序

網絡請求接口封裝

在 script/util.js 中,封裝了統一的網絡請求接口 ajax 方法,對整個項目的請求進行統一管理,包括處理傳入參數、拼裝完整請求 url、設置請求頭等,最後調用 api.ajax 方法發起請求,在請求的回調方法裏面還對 cookie 是否過時作了全局判斷,過時後會清除本地用戶登陸信息,並提示從新登陸。

// util.js
ajax(p, callback) {
   var param = p;
   if (!param.headers) {
       param.headers = {};
   }
   param.headers['X-AppToken'] = UserCenter.getAccessToken();
   if (param.data && param.data.body) {
       param.headers['Content-Type'] = 'application/json; charset=utf-8';
   }
   if (param.url) {
       var baseUrl = 'https://a6047344573226-dev.apicloud-saas.com/api/';
       param.url = baseUrl + param.url;
   }
   api.ajax(param, (res, err)=> {
       let ret = res;
       if (err && err.body && err.body.errCode) {
           ret = err.body;
           callback(ret);
       } else {
           callback(ret, err);
       }
       let sessionExpiration = false;
       if (ret && ret.errCode && ret.errCode>100) {
           sessionExpiration = true;
       }
       if (sessionExpiration) {
           var didShowLogoutAlert = api.getGlobalData({
               key: 'didShowLogoutAlert'
           });
           if (!didShowLogoutAlert) {
               api.setGlobalData({
                   key: 'didShowLogoutAlert',
                   value: true
               });

               UserCenter.setUserInfo('');
               api.confirm({
                   msg: '登陸已失效,請從新登陸',
                   buttons: ['取消', '從新登陸']
               }, (ret)=> {
                   api.setGlobalData({
                       key: 'didShowLogoutAlert',
                       value: false
                   });
                   if (ret.buttonIndex == 2) {
                       this.goLogin();
                   }
               });
           }
       }
   });
}

用戶登陸信息管理

在 script/user.js 中,對用戶登陸信息進行了封裝,作了統一管理,能夠方便地判斷是否登陸、保存和獲取用戶信息、以及判斷登陸是否過時的 accessToken 等。

const UserCenter = {
    isLogin(){
        let access_token = this.getAccessToken();
        return access_token?true:false;
    },
    setUserInfo(userInfo){
        delete userInfo.addtime;
        api.setPrefs({
            key: 'userInfo',
            value: userInfo
        });
        api.setPrefs({
            key: 'access_token',
            value: userInfo.access_token?userInfo.access_token:''
        });
    },
    getUserInfo(){
        let userInfo = api.getPrefs({
            sync: true,
            key: 'userInfo'
        });
        return userInfo?JSON.parse(userInfo):'';
    },
    getAccessToken(){
        return api.getPrefs({
            sync: true,
            key: 'access_token'
        });
    }
};

export default UserCenter;

TabBar 和導航欄的實現

首頁使用了 TabLayout 佈局來實現 TabBar 和導航欄,在 config.xml 裏面配置 content 字段,值爲 json 文件路徑,在 json 文件中配置 TabBar、導航欄和頁面信息。

// config.xml
<content src="config.json" />

config.json 文件內容以下,設置了 navigationBar 的背景色和標題文字顏色,設置了 tabBar 每項的 icon 和文字,以及每項對應的頁面。

{
  "name": "root",
  "hideNavigationBar": false,
  "navigationBar": {
    "background": "#fff",
    "color": "#000",
    "shadow": "#f1f1f1",
    "hideBackButton": true
  },
  "tabBar": {
    "scrollEnabled": false,
    "background": "#fff",
    "shadow": "#f1f1f1",
    "color": "#aaa",
    "selectedColor": "#339DFF",
    "preload": 0,
    "frames": [{
      "name": "page1",
      "url": "pages/main1/main1.stml",
      "title": "拼團商城"
    }, {
      "name": "page2",
      "url": "pages/main2/main2.stml",
      "title": "分類"
    }, {
      "name": "page4",
      "url": "pages/main4/main4.stml",
      "title": "個人"
    }],
    "list": [{
      "text": "首頁",
      "iconPath": "images/common/main1_1.png",
      "selectedIconPath": "images/common/main1.png"
    }, {
      "text": "分類",
      "iconPath": "images/common/main2_1.png",
      "selectedIconPath": "images/common/main2.png"
    }, {
      "text": "個人",
      "iconPath": "images/common/main4_1.png",
      "selectedIconPath": "images/common/main4.png"
    }]
  }
}

這裏」個人「頁面隱藏了導航欄,而其它頁面沒有隱藏。」個人「頁面路徑爲 pages/main4/main4.stml,咱們參照微信小程序的語法,在同目錄下放置了 main4.json 文件,在裏面配置 navigationStyle 字段爲 custom。

{
  "navigationStyle":"custom"
}

在首頁 main1.stml 的 apiready 方法裏面則監聽了 tabBar 每項的點擊事件,在 App 端,咱們須要在點擊事件裏面動態設置頁面顯示、隱藏導航欄。

// index.stml
api.addEventListener({
	name:'tabitembtn'
}, function(ret){
	var hideNavigationBar = ret.index == 2;
	api.setTabLayoutAttr({
		hideNavigationBar: hideNavigationBar,
		animated: false
	});
	api.setTabBarAttr({
		index: ret.index
	});
});

輪播圖實現

首頁和商品詳情頁面都使用了輪播圖,這裏以首頁爲例,首頁路徑爲 pages/main1/main1.stml,裏面輪播圖使用 swiper 組件實現,使用 v-for 指令循環 swiper-item,bannersList 爲定義的數組類型的屬性。這裏監聽了圖片的 click 事件,點擊後須要跳轉到對應的詳情頁面。這裏使用了自定義的指示器,經過設置指示器容器的 position 定位屬性爲 absolute,來讓指示器顯示到當前輪播圖的上面。

<view class="banner_box" style={'height:'+swiperHeight+'px;'}>
	<swiper class="banner_swiper" circular autoplay11 onchange="fnSwiperChange">
		<swiper-item v-for="(item_, index_) in bannerList">
			<image class="banner_img" src={item_.icon} mode="aspectFill" onclick="fnBannerPage" data-index={index_}></image>
		</swiper-item>
	</swiper>
	<view class="banner_dots">
		<view v-for="(item, index) in bannerList" class={current == index ? 'banner_dot-on' : 'banner_dot'}></view>
	</view>
</view>

爲保持不一樣分辨率設備上面圖片顯示比例不變,須要讓輪播圖的寬度跟隨屏幕寬度變化,高度則經過計算屬性 swiperHeight 來動態計算獲得。

computed:{
	swiperHeight(){
		return Math.floor((api.winWidth - 30)*0.4+20);
	}
}

rich-text 富文本的使用

在商品詳情頁中,商品詳情部分就是使用的 rich-text 來展現的,使用時若是沒爲 rich-text 設置高度,其高度就爲裏面內容的高度。

<rich-text nodes={html}></rich-text>

rich-text 用於展現 HTML String 片斷,在從服務器獲取到 HTML String 後,咱們調用 $util.fitRichText 方法處理了一下 HTML String,在 fitRichText 方法中爲 img 標籤加了最大寬度的限制,以防止圖片寬度過大致使顯示溢出。

// util.js
fitRichText(richtext, width){
   var str = `<img style="max-width:${width}px;"`;
   var result = richtext.replace(/\<img/gi, str);
   return result;
}

下拉刷新、滾動到底部加載更多

在」分類商品列表「頁面(pages/goodslist/goodslist.stml),經過 scroll-view 實現了商品列表展現,同時實現了下拉刷新、滾動到底部加載更多功能。

<scroll-view class="scroll-view" scroll-y enable-back-to-top refresher-enabled refresher-triggered={refresherTriggered} onrefresherrefresh="onrefresherrefresh" onscrolltolower="onscrolltolower">
	<list-item v-for="(item) in goodsList" item={item} showOriginalPrice onitemClick="fnOpenDetails"></list-item>
	<no-data v-if={showNoData} image="../../images/common/nolist.png" desc="暫無商品"></no-data>
</scroll-view>

下拉刷新使用了 scroll-view 默認的下拉刷新樣式,使用 refresher-enabled 字段來開啓下拉刷新,爲 refresher-triggered 字段綁定了 refresherTriggered 屬性來控制下拉刷新狀態,須要注意的是,在刷新的事件回調方法裏面,咱們須要主動設置 refresherTriggered 的值爲 true,在數據加載完成後再設置爲 false,這樣綁定的值有變化,刷新狀態才能通知到原生裏面。

onrefresherrefresh(){
	this.data.refresherTriggered = true;
	this.getData(false);
}

滾動到底部監聽了 scroll-view 的 scrolltolower 事件,在滾動到底部後自動加載更多數據,加載更多和下拉刷新都是調用 loadData 方法請求數據,經過 loadMore 參數來進行區分,作分頁請求處理。

 
getData(loadMore){
	let that = this;
	if (!loadMore) {
		that.data.page = 1;
	}
	this.data.loading = true;
	var url = "homes/getGoodsList?classid=" + that.data.classId + "&page=" + that.data.page;
	$util.ajax({
		url: url
	}, function(res, err){
		if (res && res.errcode == 0) {
			let list = res.data;
			that.data.haveMore = list.length > 0;
			if (loadMore) {
				that.data.goodsList = that.data.goodsList.concat(list);
			} else {
				that.data.goodsList = list;
			}
			if (list.length > 0) {
				that.data.page += 1;
			}
		}
		that.data.loading = false;
		that.data.refresherTriggered = false;
		that.data.showNoData = that.data.goodsList.length == 0;
		$util.hideProgress();
	});
}

自定義三級聯動城市選擇器組件

在填寫收貨地址頁面(pages/address_edit/address_edit.stml)裏面有一需求,爲選擇收貨地址城市區域,爲此咱們在 picker 組件的基礎上封裝了一個 region-picker 組件(components/region-picker.stml),使用時監聽該組件的 change 事件,就能夠獲取到選擇的城市區域的名稱和城市代碼。

// address_edit.stml

<region-picker region={qustr||''} onchange="fnChooseStr"></region-picker>

fnChooseStr(e){
	let code = e.detail.code;
	let val = e.detail.value;
	this.data.quid = code.join(",");
	this.data.qustr = val.join(",");

}

平臺差別化處理

在多端開發中,不免會遇到不一樣平臺差別化的地方,須要在運行期間作判斷處理,爲此在 utils/util.js 中封裝了 isApp、isMp 方法,裏面經過 api.platform 屬性判斷當前運行環境是 App 端仍是小程序端。

// util.js
isApp(){
   if (api.platform && api.platform == 'app') {
       return true;
   }
   return false;
},
isMp(){
   if (api.platform && api.platform == 'mp') {
       return true;
   }
   return false;
}
相關文章
相關標籤/搜索