此我的项目为拼团商城类型,次要性能包含商品分类、商品详情、商品搜寻、拼团、订单治理等。
我的项目源码在 https://github.com/apicloudcom/group-ec 仓库的 widget 目录下。

我的项目中前端采纳 avm 多端开发技术进行开发,要点包含 TabLayout 布局、swiper 轮播图、rich-text 富文本、scroll-view 滚动视图、下拉刷新、组件封装等。应用 APICloud 多端技术进行开发,实现一套代码多端运行,反对编译成 Android & iOS App 以及微信小程序。

我的项目后端则是应用的 APICloud 数据云 3.0 自定义云函数来构建的。

应用步骤

  1. 应用 APICloud Studio 3 作为开发工具。
  2. 下载本我的项目源码。
  3. 在开发工具中新建我的项目,并将本源码导入新建的我的项目中,留神更新 config.xml 中的 appid 为你我的项目的 appid。
  4. 应用 AppLoader 进行真机同步调试预览。
  5. 或者提交我的项目源码,并为以后我的项目云编译自定义 Loader 进行真机同步调试预览。
  6. 云编译 生成 Android & iOS App 以及微信小程序源码包。

如果之前未接触过 APICloud 开发,倡议先理解一个简略我的项目的初始化、预览、调试和打包等操作,请参考 APICloud 多端开发疾速上手教程。

网络申请接口封装

在 script/util.js 中,封装了对立的网络申请接口 ajax 办法,对整个我的项目的申请进行对立治理,包含解决传入参数、拼装残缺申请 url、设置申请头等,最初调用 api.ajax 办法发动申请,在申请的回调办法外面还对 cookie 是否过期做了全局判断,过期后会革除本地用户登录信息,并提醒从新登录。

// util.jsajax(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.stmlapi.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.jsfitRichText(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.jsisApp(){   if (api.platform && api.platform == 'app') {       return true;   }   return false;},isMp(){   if (api.platform && api.platform == 'mp') {       return true;   }   return false;}