手摸手探索原生微信小程序那些事~
前言
在前端开发中, 如今微信小程序开发占据着重要的一环。基于微信的生态, 一个成功的微信小程序可以为业务带来很大的帮助以及成功。如今, 微信小程序应用数不胜数, 开发方式也很多如uniapp、Taro、原生微信小程序等。目前随着大部分公司的业务推进, 跨端开发成为一种主流(有多个端小程序的需求, 比如微信小程序、支付宝小程序、京东小程序、抖音小程序等), 使其可以一套代码多端运行编译, 节省人力时间开发成本。一套代码多端编译运行, 这是一件不错的事情, 但是后续的一些开发者往往直接用跨端开发, 微信小程序原生似乎有点陌生, 特别是入职一家公司后, 一些开发者甚至是看到新公司的小程序是原生小程序开发后就产生一定的抵触心里, 这种现象可以说是不少。今天, 让我们一起来手摸手探索原生微信小程序那些事~
1、小程序项目搭建
此时候, 一个基本的小程序项目骨架出来了。
给小程序搭配一下组件库, 选择TDesign(个人选择)。安装如下:
TDesign官网微信小程序使用方式: TDesign (tencent.com)
至此, 一个原生小程序+TDesign的搭配骨架已出来。
{
"pages": [
"pages/index/index",
"pages/my/my"
],
"window": {
"navigationBarTextStyle": "white",
"navigationBarTitleText": "业务小程序",
"navigationBarBackgroundColor": "#0052d9",
"backgroundColor": "#f4f4f5"
},
"componentFramework": "glass-easel",
"sitemapLocation": "sitemap.json",
"lazyCodeLoading": "requiredComponents",
"tabBar": {
"color": "#000000",
"selectedColor": "#0052d9",
"backgroundColor": "#ffffff",
"list": [
{
"iconPath": "/assets/tarbar/dingdan.png",
"selectedIconPath": "/assets/tarbar/dingdan-select.png",
"pagePath": "pages/index/index",
"text": "我的订单"
},
{
"iconPath": "/assets/tarbar/wode.png",
"selectedIconPath": "/assets/tarbar/wode-select.png",
"pagePath": "pages/my/my",
"text": "我的"
}
]
}
}
2. 列表上拉加载、下拉刷新、回到顶部、动态刷新(整刷, 单刷, 删除)
2.1 列表整体、上拉加载、下拉刷新
{
"usingComponents": {
"t-tabs": "tdesign-miniprogram/tabs/tabs",
"t-tab-panel": "tdesign-miniprogram/tab-panel/tab-panel",
"t-empty": "tdesign-miniprogram/empty/empty",
"t-icon": "tdesign-miniprogram/icon/icon"
},
"backgroundColor": "#e5edf7",
"navigationBarTitleText": "我的订单",
"enablePullDownRefresh": true
}
<view class="wrapper">
<view class="tabs-box">
<t-tabs t-class-item="tabs-box-self" borderless sticky="{{isSticky}}" value="{{activeTab}}" bind:change="onTabsChange"
bind:click="onTabsClick">
<t-tab-panel wx:for="{{tabs}}" wx:key="index" label="{{item.label}}" value="{{item.value}}" />
</t-tabs>
</view>
<!-- Content -->
<view wx:if="{{tableList.length}}" class="main-content-list">
<view wx:for="{{tableList}}" wx:key="id" class="main-content-item">
<view bind:tap="handleDetail">
<view>-{{item.id}}-</view>
<view class="header-wrap">
<view class="header-left-wrap">
<view class="header-address-wrap">
<view class="address">{{item.addressList[0].province + ' ' + item.addressList[0].city }}</view>
<view class="address-link">
<t-icon name="swap-right" size="42rpx" />
</view>
<view class="address">{{item.addressList[1].province + ' ' + item.addressList[1].city }}</view>
</view>
</view>
<view class="header-status-wrap">
状态信息
</view>
</view>
<view class="address-detail-list">
<view class="address-detail-item">装货地址:
{{item.addressList[0].province + item.addressList[0].city + item.addressList[0].districtOrCounty + item.addressList[0].detailAddress }}
</view>
<view class="address-detail-item">卸货地址:
{{item.addressList[1].province + item.addressList[1].city + item.addressList[1].districtOrCounty + item.addressList[1].detailAddress }}
</view>
</view>
<view class="info-wrap">
<view class="info-left-wrap">
<view class="info-tab-list">
<view class="info-tab-item">
{{ item.discussingPricesType === 0 ? '一口价' : '电议' }}
</view>
<view class="info-tab-item">
{{ item.agreementId && item.agreementId.length ? '已投保' : '未投保' }}
</view>
</view>
</view>
<view class="info-right-wrap" catch:tap="handleLocationDetail"
data-item="{{item}}">
<view class="location-icon">
<t-icon name="location" size="34rpx" />
</view>
<view class="location-font">轨迹</view>
</view>
</view>
<view class="money-wrap">
<view class="money-item">
<text class="money-title">订金</text>
<text class="money-value">¥{{ item.deposit }}</text>
<text class="money-tip">(平台监管账户中)</text>
</view>
<view class="money-item">
<text class="money-title">运费</text>
<text class="money-value">¥{{ item.transportationPrice }}</text>
</view>
</view>
<!-- 司机信息 -->
<view class="drive-wrap">
<view class="drive-image-wrap">
<image style="width: 100%; height: 100%;" mode="aspectFit" catch:tap="handlePreviewAvatar"
data-src="{{item.driveObj.avatar }}"
src="{{ item.driveObj.avatar }}">
</image>
</view>
<view class="drive-basic">
<view class="drive-name">
{{ item.driveObj.userName }}
</view>
<view class="drive-basic-other">
{{ item.driveObj.licensePlate }} |
{{ item.driveObj.vehicleLong + '米' }}
</view>
</view>
<view class="drive-operator">
<view class="drive-item" catch:tap="handleConnection" data-type="{{1}}" data-item="{{item}}">
<t-icon name="call-1" size="40rpx" />
</view>
</view>
</view>
<view class="operator-list" wx:if="{{activeTab === 1}}">
<view class="operator-item operator-item-blue"
catch:tap="handleCancelOrder" data-item="{{item}}">删除订单
</view>
<view class="operator-item operator-item-blue"
catch:tap="handleCancelOrder" data-item="{{item}}">继续当前状态
</view>
<view class="operator-item operator-item-blue"
catch:tap="handleCancelOrder" data-item="{{item}}">流转状态2
</view>
</view>
</view>
</view>
<view wx:if="{{isShowBackTop}}">
<back-to-top></back-to-top>
</view>
</view>
<!-- Empty -->
<view wx:else>
<t-empty t-class="empty-box" icon="info-circle-filled" description="暂无数据" />
</view>
</view>
const orderObj = {
addressList: [
{
type: 0,
province: "广东省",
city: "广州市",
districtOrCounty: "天河区",
detailAddress: "新塘街道凌塘村下街1号",
},
{
type: 0,
province: "广东省",
city: "阳江市",
districtOrCounty: "阳西县",
detailAddress: "程村镇开发区1号",
},
],
discussingPricesType: 0,
agreementId: "123456",
deposit: 500,
transportationPrice: 1500,
driveObj: {
avatar:
"https://img0.baidu.com/it/u=3837622737,2619510196&fm=253&fmt=auto&app=138&f=JPEG?w=475&h=475",
userName: "阿祖",
licensePlate: "粤A123456",
vehicleLong: 5,
},
};
Page({
/**
* 页面的初始数据
*/
data: {
activeTab: 1,
isSticky: true,
tabs: [
{
label: "状态1",
value: 1,
},
{
label: "状态2",
value: 2,
},
{
label: "状态3",
value: 3,
},
{
label: "状态4",
value: 4,
},
{
label: "状态5",
value: 5,
},
{
label: "状态6",
value: 6,
},
],
tableList: [],
},
/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
// this.getList();
},
/**
* 生命周期函数--监听页面初次渲染完成
*/
onReady() {},
/**
* 生命周期函数--监听页面显示
*/
onShow(options) {},
/**
* 监听页面的滚动
*/
/** 模拟网络请求获取数据 */
requestUrl() {
let list = [];
const startIndex = this.data.tableList.length;
for (let i = startIndex; i < startIndex + 10; i++) {
list.push({ id: i + 1, ...orderObj });
}
return list;
},
/** 获取列表 */
async getList() {
wx.showLoading({
title: "加载中",
mask: true,
});
const list = await this.requestUrl();
// 模拟一个网络延时
setTimeout(() => {
wx.hideLoading();
console.log("获取数据", list);
this.setData({
isSticky: true,
tableList: [...this.data.tableList, ...list],
});
}, 1500);
},
/**
* 页面相关事件处理函数--监听用户下拉动作
*/
onPullDownRefresh() {
// 实现数据更新的逻辑
// 例如从服务器获取数据然后更新界面
// 数据更新完毕后,停止下拉刷新
this.setData({
isSticky: false,
tableList: [],
});
this.getList();
},
/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
if (this.data.tableList.length) {
this.getList();
}
},
/** tab 切换 */
onTabsClick(event) {
this.setData({
tableList: [],
activeTab: event.detail.value,
});
this.getList();
},
/** 预览头像 */
handlePreviewAvatar(e) {
// 拿到图片的地址url
const currentUrl = e.currentTarget.dataset.src;
if (currentUrl && currentUrl.length) {
// 微信预览图片的方法
wx.previewImage({
current: currentUrl,
urls: [currentUrl],
});
}
},
});
page {
height: 100%;
background: rgba(75, 156, 243, 0.1);
background: #e5edf7;
}
.tabs-box-self {
background: rgba(75, 156, 243, 0.1);
border: none;
overflow: scroll;
}
.main-content-list {
padding: 18rpx 20rpx 50rpx 20rpx;
.main-content-item {
padding: 16rpx 16rpx 16rpx 16rpx;
background: #FFFFFF;
border-radius: 12rpx 12rpx 12rpx 12rpx;
margin-bottom: 16rpx;
.header-wrap {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16rpx;
.header-left-wrap {
display: flex;
align-items: center;
flex: 1;
}
.header-avatar {
width: 76rpx;
height: 76rpx;
min-width: 76rpx;
min-height: 76rpx;
background: #E5E5EA;
border-radius: 50%;
overflow: hidden;
margin-right: 16rpx;
}
.header-address-wrap {
display: flex;
align-items: center;
.address {
font-size: 34rpx;
font-family: PingFang SC, PingFang SC;
font-weight: 500;
color: #333333;
}
.address-link {
margin: 0 16rpx;
color: #007AFF;
}
}
.header-status-wrap {
font-size: 28rpx;
font-family: PingFang SC, PingFang SC;
font-weight: 400;
color: #FAAD14;
&-red {
color: #F93D3D;
}
}
}
.address-detail-list {
margin-bottom: 18rpx;
.address-detail-item {
font-size: 24rpx;
font-family: PingFang SC, PingFang SC;
font-weight: 400;
color: #333333;
&:nth-child(2) {
margin: 12rpx 0;
}
.view-more-address {
color: #397EE2;
margin-left: 10rpx
}
}
}
.info-wrap {
display: flex;
justify-content: space-between;
align-items: flex-end;
margin-bottom: 36rpx;
.info-left-wrap {
.info-tab-list {
display: flex;
align-items: center;
margin-bottom: 16rpx;
.info-tab-item {
border-radius: 8rpx 8rpx 8rpx 8rpx;
border: 2rpx solid #397EE2;
margin-right: 20rpx;
font-size: 24rpx;
font-family: PingFang SC, PingFang SC;
font-weight: 400;
color: #397EE2;
padding: 2rpx 10rpx;
}
}
}
.info-right-wrap {
width: 60rpx;
height: 70rpx;
background: #FFFFFF;
box-shadow: 0px 2rpx 2rpx 2rpx #8ABAFE;
border-radius: 8rpx 8rpx 8rpx 8rpx;
color: #666666;
font-weight: 400;
.location-icon {}
.location-font {
margin-top: 4rpx;
text-align: center;
font-size: 20rpx;
font-family: PingFang SC, PingFang SC;
}
}
}
.money-wrap {
display: flex;
align-items: center;
justify-content: flex-end;
margin-bottom: 16rpx;
.money-item {
font-family: PingFang SC, PingFang SC;
&:nth-child(1) {
.money-tip {
margin-right: 40rpx;
}
}
.money-title {
font-size: 24rpx;
font-weight: 500;
color: #333333;
}
.money-value {
font-size: 28rpx;
font-weight: 400;
color: #397EE2;
margin: 0 4rpx;
}
.money-tip {
font-size: 20rpx;
font-weight: 400;
color: #666666;
}
}
}
.drive-wrap {
border-top: 2rpx solid #E5E5EA;
margin-bottom: 32rpx;
padding-top: 24rpx;
display: flex;
align-items: center;
.drive-image-wrap {
width: 76rpx;
height: 76rpx;
background: #E5E5EA;
border-radius: 50%;
overflow: hidden;
margin-right: 12rpx;
}
.drive-basic {
flex: 1;
font-family: PingFang SC, PingFang SC;
.drive-name {
font-size: 30rpx;
color: #333333;
margin-bottom: 6rpx;
font-weight: 500;
}
.drive-basic-other {
font-size: 24rpx;
font-weight: 400;
color: #666666;
}
}
.drive-operator {
display: flex;
align-items: center;
color: #666666;
.drive-item-1 {
margin-right: 36rpx;
}
}
}
.operator-list {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
margin-right: -16rpx;
.operator-item {
padding: 0 10rpx;
height: 60rpx;
text-align: center;
line-height: 60rpx;
border: 2rpx solid #CCCCCC;
font-size: 28rpx;
font-family: PingFang SC, PingFang SC;
font-weight: 400;
color: #666666;
border-radius: 10rpx;
margin: 0 16rpx 0rpx 0;
&-blue {
background: #007AFF;
color: #FFFFFF;
border: none;
}
&-share {
display: flex;
justify-content: center;
align-items: center;
border: none;
margin: -16rpx 14rpx 0 0;
&-btn {
border: none;
padding: 0;
width: 128rpx;
height: 60rpx;
text-align: center;
line-height: 60rpx;
font-size: 28rpx;
font-family: PingFang SC, PingFang SC;
font-weight: 400;
color: #666666;
border-radius: 10rpx;
background: #007AFF;
color: #FFFFFF;
border: none;
}
}
}
}
}
}
.empty-box {
padding-top: 100rpx;
}
页面效果:
2.2 回到顶部
当不断的上拉加载后, 列表的数据不断的增加, 此时如果我列表达到了很长的一个滚动, 那么我有没有一个快速的方法回到顶部呢? 那就增加一个回到顶部的功能, just do it~
将回到顶部设成一个组件, 创建组件 components/back-to-top, 具体代码如下:
<view>
<!--
1. theme: 主题 --- round/half-round/round-dark/half-round-dark
2. text 文案
3. scroll-top 页面滚动距离
4. fixed 是否绝对定位固定到屏幕右下方
5. visibility-height 滚动高度达到此参数值才出现
6. t-class 修改根节点样式
-->
<t-back-top theme="round" text="顶部" scroll-top="{{ { type: Number, value: 0 } }}" fixed="{{false}}" visibility-height="{{ 0 }}"
t-class="back-top-root"></t-back-top>
</view>
{
"component": true,
"usingComponents": {
"t-back-top": "tdesign-miniprogram/back-top/back-top"
}
}
.back-top-root {
position: fixed;
bottom: 40rpx;
right: 20rpx;
}
在订单列表页引进来:
{
"usingComponents": {
"back-to-top": "../../../components/back-to-top/back-to-top"
}
}
<view>
<back-to-top></back-to-top>
</view>
此时, 页面效果图如下:
此时, 页面从一开始就是显示回到顶部, 显示不太友好, 我们希望在滚动到一定的数值才显示, 低于这个值则不现实 。可以直接在父组件即订单列表页面直接处理, 通过 onPageScroll 控制一个布尔值来直接控制回到顶部子组件是否显示。
<view wx:if="{{isShowBackTop}}">
<back-to-top></back-to-top>
</view>
data: {
/** 是否显示回到顶部 */
isShowBackTop: false,
},
/**
* 监听页面的滚动
*/
onPageScroll(e) {
const isShowBackTop = e.scrollTop > 200 ? true : false;
this.setData({
isShowBackTop,
});
},
当滚动值到达200时才显示回到顶部子组件。
2.3 刷新问题(添加, 删除, 单刷, 整刷)
刷新问题, 有一部分人通常采用比较暴力的方法, 就是一把梭, 不管是删除还是更新还是新增, 都是操作完直接请求接口重新渲染一遍列表。这在交互体验上是比较不好的, 尤其是还涉及到页面的回退再刷新的问题。
2.3.1 添加
(1) 请求添加接口, 看后端是否支持添加接口返回创建订单的整个对象, 如支持, 则直接将整个对象塞进数组第一个; 如果没有则看是否有返回订单的id, 有的话则根据创建的订单id去查询列表接口, 得到对象信息再将整个对象塞进数组第一个
(2) 如第1点接口不支持, 那么只能重置列表数组, 再重新请求渲染
2.3.1 删除
请求删除接口, 请求成功后, 我们可以在订单列表数组里面过滤掉当前的删除订单id所属的那个对象信息, 而不用再请求一次订单列表接口再刷新列表
2.3.1 单刷与整刷
一般我们在tabbar页的关于列表的请求都放在onLoad里面, 其它页面的请求一般都放在onShow页面。假设当前有一个详情页 detail, 点击订单可跳转到 detail。从detail 页返回上一步如果涉及到业务逻辑操作我们一般用 wx.navigateBack({ delta: 1 })来处理, 没有的话一般也就点击左上角直接返回即可。那么我们就有一个疑问了, 假如detail页面有逻辑或者状态的流转关联到订单页的刷新问题, 那么我们该怎么处理呢, 订单页怎么知道详情页到底有没有做什么操作呢? 因此, 订单详情页需要传递参数给订单页。
假设订单详情页没有任何的点击任务逻辑操作, 单单是看一下详情然后点左上角返回到订单页, 那么我们可以不用管; 假如只要返回去都需要传递个状态给上一页, 那么我们可以传递一下状态值。
// 通过触发事件执行
handleStatus() {
const pages = getCurrentPages();
const prevPage = pages[pages.length - 2]; // 上一个页面
prevPage.options.refreshRuleObj = {
type: 3, // 1 整刷 2 单刷 3 移除
orderId: this.data.id, // 订单id
};
}
// 通过页面卸载生命周期传递
onUnload() {
const pages = getCurrentPages();
const prevPage = pages[pages.length - 2]; // 上一个页面
prevPage.options.refreshRuleObj = {
type: 2, // 1 整刷 2 单刷 3 移除
orderId: this.data.id, // 订单id
};
},
然后在订单页就可以获取到是否有值, 有就执行对应的业务逻辑, 没有就没有。
onShow(options) {
// 设置是否触发刷新规则机制
this.handleRefreshRule();
}
async handleRefreshRule() {
const pages = getCurrentPages();
const currentPage = pages[pages.length - 1];
const refreshRuleObj = currentPage.options.refreshRuleObj;
delete currentPage.options.refreshRuleObj;
if (refreshRuleObj) {
// type 1 整刷 2 单刷 3 移除
if (refreshRuleObj.type === 1) {
// 整刷逻辑
} else if (refreshRuleObj.type === 2) {
// 单刷逻辑
} else if (refreshRuleObj.type === 3) {
// 移除逻辑
}
}
},
3. 数据的传参方式、数据的处理
通过跳转路径传递, 通过全局传递, 通过自定义传递等等, 持续更新~