注册

用一周时间开发了一个微信小程序,我遇到了哪些问题?

功能截图

home.pic.jpginfo.pic.jpg
address-add.pic.jpgaddress-list.pic.jpg
cart-list.pic.jpgcategory.pic.jpg
goods-detail.pic.jpgorder-list.pic.jpg
goods-list.pic.jpgorder-detail.pic.jpg
特别说明:由于本项目是用于教学案例,并没有上线的二维码供大家体验。

开发版本

  • 微信开发者工具版本:1.06

  • 调试基础库:2.30

代码仓库

建议全文参考源代码观看效果更佳,代码可直接在微信开发者工具当中打开预览,appid需要替换成自己的。

获取用户信息变化

用户头像昵称获取规则已调整,现在微信小程序已经获取不到用户昵称和头像了,只能已通过用户回填(提供给用户一个修改昵称和头像的表单页面)的方式来实现。不过还是可以获取到code跟后端换取token的方式来进行登录。

具体参考 *用户信息接口调整说明*小程序用户头像昵称获取规则调整公告

vant weapp组件库的使用

1.需要使用npm构建的能力,用 npm 构建前,请先阅读微信官方的 npm 支持。初始化package.json

npm init

2.安装@vant/weapp

# 通过 npm 安装
npm i @vant/weapp -S --production

# 通过 yarn 安装
yarn add @vant/weapp --production

# 安装 0.x 版本
npm i vant-weapp -S --production

2.修改 app.json 将 app.json 中的 "style": "v2" 去除,小程序的新版基础组件强行加上了许多样式,难以覆盖,不关闭将造成部分组件样式混乱。 3.修改 project.config.json 开发者工具创建的项目,miniprogramRoot 默认为 miniprogrampackage.json 在其外部,npm 构建无法正常工作。 需要手动在 project.config.json 内添加如下配置,使开发者工具可以正确索引到 npm 依赖的位置。

{
...
 "setting": {
  ...
   "packNpmManually": true,
   "packNpmRelationList": [
    {
       "packageJsonPath": "./package.json",
       "miniprogramNpmDistDir": "./miniprogram/"
    }
  ]
}
}

注意: 由于目前新版开发者工具创建的小程序目录文件结构问题,npm构建的文件目录为miniprogram_npm,并且开发工具会默认在当前目录下创建miniprogram_npm的文件名,所以新版本的miniprogramNpmDistDir配置为'./'即可。 4.构建 npm 包 打开微信开发者工具,点击 工具 -> 构建 npm,并勾选 使用 npm 模块 选项,构建完成后,即可引入组件。 image.png

使用组件

引入组件

// 通过 npm 安装
// app.json
"usingComponents": {
 "van-button": "@vant/weapp/button/index"
}

使用组件

<van-button type="primary">按钮</van-button>

如果预览没有效果,从新构建一次npm,然后重新打开此项目

自定义tabbar

这里我的购物车使用了徽标,所以需要自定义一个tabbar,这里自定义以后,会引发后面的一系列连锁反应(比如内容区域高度塌陷,导致tabbar遮挡内容区域),后面会讲如何计算。效果如下图: image.png

1. 配置信息

  • 在 app.json 中的 tabBar 项指定 custom 字段,同时其余 tabBar 相关配置也补充完整。

  • 所有 tab 页的 json 里需声明 usingComponents 项,也可以在 app.json 全局开启。

示例:

{
 "tabBar": {
   "custom": true,
   "color": "#000000",
   "selectedColor": "#000000",
   "backgroundColor": "#000000",
   "list": [{
     "pagePath": "page/component/index",
     "text": "组件"
  }, {
     "pagePath": "page/API/index",
     "text": "接口"
  }]
},
 "usingComponents": {}
}

2. 添加 tabBar 代码文件

需要跟pages目录同级,创建一个custom-tab-bar目录。 image.png .wxml代码如下:

<!--miniprogram/custom-tab-bar/index.wxml-->
<cover-view class="tab-bar">
 <cover-view class="tab-bar-border"></cover-view>
 <cover-view wx:for="{{list}}"
   wx:key="index"
   class="tab-bar-item"
   data-path="{{item.pagePath}}"
   data-index="{{index}}"
   bindtap="switchTab">
   <cover-view class="tab-img-wrap">
     <cover-image src="{{selected === index ? item.selectedIconPath : item.iconPath}}"></cover-image>
     <cover-view wx-if="{{item.info && cartCount > 0}}"class="tab-badge">{{cartCount}}</cover-view>
   </cover-view>
   <cover-view style="color: {{selected === index ? selectedColor : color}}">{{item.text}}</cover-view>
 </cover-view>
</cover-view>

注意这里的徽标控制我是通过info字段来控制的,然后数量cartCount单独第一个了一个字段,这个字段是通过store来管理的,后面会讲为什么通过stroe来控制的。

3. 编写 tabBar 代码

用自定义组件的方式编写即可,该自定义组件完全接管 tabBar 的渲染。另外,自定义组件新增 getTabBar 接口,可获取当前页面下的自定义 tabBar 组件实例。

import { storeBindingsBehavior } from 'mobx-miniprogram-bindings';
import { store } from '../store/index';

Component({
 behaviors: [storeBindingsBehavior],
 storeBindings: {
   store,
   fields: {
     count: 'count',
  },
   actions: [],
},
 observers: {
   count: function (val) {
     // 更新购物车的数量
     this.setData({ cartCount: val });
  },
},
 data: {
   selected: 0,
   color: '#252933',
   selectedColor: '#FF734C',
   cartCount: 0,
   list: [
    {
       pagePath: '/pages/index/index',
       text: '首页',
       iconPath: '/static/tabbar/home-icon1.png',
       selectedIconPath: '/static/tabbar/home-icon1-1.png',
    },
    {
       pagePath: '/pages/category/category',
       text: '分类',
       iconPath: '/static/tabbar/home-icon2.png',
       selectedIconPath: '/static/tabbar/home-icon2-2.png',
    },
    {
       pagePath: '/pages/cart/cart',
       text: '购物车',
       iconPath: '/static/tabbar/home-icon3.png',
       selectedIconPath: '/static/tabbar/home-icon3-3.png',
       info: true,
    },
    {
       pagePath: '/pages/info/info',
       text: '我的',
       iconPath: '/static/tabbar/home-icon4.png',
       selectedIconPath: '/static/tabbar/home-icon4-4.png',
    },
  ],
},

 lifetimes: {},
 methods: {
 // 改变tab的时候,记录index值
   switchTab(e) {
     const { path, index } = e.currentTarget.dataset;
     wx.switchTab({ url: path });
     this.setData({
       selected: index,
    });
  },
},
});

这里的store大家不用理会,只需要记住是设置徽标的值就可以了。

4.设置样式

.tab-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 48px;
background: white;
display: flex;
padding-bottom: env(safe-area-inset-bottom);
}

这里的样式单独贴出来说明一下:

padding-bottom: env(safe-area-inset-bottom);

可以让出底部安全区域,不然的话tabbar会直接沉到底部 image.png 别忘了在index.json中设置component=true

{
"component": true
}

5.tabbar页面设置index

上面的代码添加完毕以后,我们的tabbar就出来了,但是有个问题,就是在点击tab的时候,样式不会改变,必须再点击一次,这是因为当你切换页面或者刷新页面的时候,index的值会重置,为了解决这个问题,我们需要在每个tabbar的页面添加下面的代码:

/**
* 生命周期函数--监听页面显示
*/
onShow() {
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({
selected: 0,
});
}
},

当页面每次show的时候,设置一下selected的值,也就是选中的index就可以了。其他的tabbar页面同样也是如此设置即可。

添加store状态管理

接下来我们来讲讲微信小程序如何用store来管理我们的数据。 上面我们说了我们需要实现一个tabbar的徽标,起初我想的是直接用个缓存来解决就完事了,后来发现我太天真了,忘记了这个字段是一个响应式的,它是需要渲染到页面上的,它变了,页面中的数据也得跟着一起变。后来我想通过globalData来实现,也不行。后来我又又想到了把这个数据响应式监听一下不就行了?于是通过proxy,跟vue3的处理方式一样,监听一下这个字段的改变就可以了。在购物车这个页面触发的时候是挺好,可当我切换到其他tabbar页面的时候它就不见了。我忽略了一个问题,它是全局响应的啊。于是最后才想到了使用store的方式来实现。 我找到了一个针对微信小程序的解决方法,就是使用mobx-miniprogram-bindingsmobx-miniprogram这两个库来解决。真是帮了我的大忙了。 下面我们直接来使用。 先安装两个插件:

npm install --save mobx-miniprogram mobx-miniprogram-bindings

方式跟安装vant weapp一样,npm安装完成以后,在微信开发者工具当中构建npm即可。 下面我们来通过如何实现一个tabbar徽标的场景来学习如何在微信小程序中使用store来管理全局数据。

tabbar徽标实现

1.定义store

import { observable, action, runInAction } from 'mobx-miniprogram';
import { getCartList } from './cart';
// 获取购物车数量

export const store = observable({
/** 数据字段 */
count: 0,

/** 异步方法 */
getCartListCount: async function () {
const num = await getCartList();
runInAction(() => {
this.count = num;
});
},

/** 更新购物车的数量 */
updateCount: action(function (num) {
this.count = num;
}),
});

看起来是不是非常简单。这里我们定义了一个count,然后定义了两个方法,这两个方法有点区别:

  • updateCount用来更新count

  • getCartListCount用来异步更新count,因为这里我们在进入小程序的时候就需要获取count的初始值,这个值的计算又的依赖接口,所以需要使用异步的方式。

好了,现在我们字段有了,设置初始值的方法有了,更新字段的方法也有了。下面我们来看一下如何使用。

2.使用store

回到我们的tabbr组件,在custom-tab-bari/ndex.js中,我们贴一下主要的代码:

import { storeBindingsBehavior } from 'mobx-miniprogram-bindings';
import { store } from '../store/index';

Component({
behaviors: [storeBindingsBehavior],
storeBindings: {
store,
fields: {
count: 'count',
},
actions: [],
},
observers: {
count: function (val) {
// 更新购物车的数量
this.setData({ cartCount: val });
},
},
data: {
cartCount: 0,
},
});

解释一下,这里我们只是获取了count的值,然后通过observers的方式监听了一下count,然后赋值给了cartCount,这里你直接使用count渲染到页面上也是没有问题的。我这里只是为了演示一下observers的使用方式才这么写的。这样设置以后,tabbar上面的徽标数字已经可以正常展示了。 现在当我们的购物车数字改变以后,就要更新count的值了。

3.使用action

找到我们的cart页面,下面是具体的逻辑:

import {
findCartList,
deleteCart,
checkCart,
addToCart,
checkAllCart,
} from '../../utils/api';

import { createStoreBindings } from 'mobx-miniprogram-bindings';
import { store } from '../../store/index';
import { getCartTotalCount } from '../../store/cart';
const app = getApp();
Page({
data: {
list: [],
totalCount: 0,
},

/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
this.storeBindings = createStoreBindings(this, {
store,
fields: ['count'],
actions: ['updateCount'],
});
},

/**
* 声明周期函数--监听页面卸载
*/
onUnload() {
this.storeBindings.destroyStoreBindings();
},

/**
* 生命周期函数--监听页面显示
*/
onShow() {
if (typeof this.getTabBar === 'function' && this.getTabBar()) {
this.getTabBar().setData({
selected: 2,
});
}
this.getCartList();
},

/**
* 获取购物车列表
*/
async getCartList() {
const res = await findCartList();
this.setData({
list: res.data,
});
this.computedTotalCount(res.data);
},

/**
* 修改购物车数量
*/
async onChangeCount(event) {
const newCount = event.detail;
const goodsId = event.target.dataset.goodsid;
const originCount = event.target.dataset.count;
// 这里如果直接拿+以后的数量,接口的处理方式是直接在上次的基础累加的,
// 所以传给接口的购物车数量的计算方式如下:
// 购物车添加的数量=本次的数量-上次的数量
const count = newCount - originCount;
const res = await addToCart({
goodsId,
count,
});
if (res.code === 200) {
this.getCartList();
}
},

/**
* 计算购物车总数量
*/
computedTotalCount(list) {
// 获取购物车选中数量
const total = getCartTotalCount(list);
// 设置购物车徽标数量
this.updateCount(total);
},


});

上面的代码有所删减。在page和component中使用action方法有所区别,需要在onUnload的时候销毁一下我们的storeBindings。当修改购物车数量的时候,我这里会重新请求一次接口,然后计算一下totalCount的数量,通过updateCount来修改count的值。到了这里,我们的徽标就可以正常的使用了。不管是切换到哪一个tabbar页面,徽标都会保持状态。

4.使用异步action

现在还剩最后一个问题,就是如何设置count的初始值,这个值还得从接口获取过来。下面是实现思路。 首先我们在store中定义了一个一步方法:

import { observable, action, runInAction } from 'mobx-miniprogram';
import { getCartList } from './cart';
// 获取购物车数量

export const store = observable({
/** 数据字段 */
count: 0,

/** 异步方法 */
getCartListCount: async function () {
const num = await getCartList();
runInAction(() => {
this.count = num;
});
},

/** 更新购物车的数量 */
updateCount: action(function (num) {
this.count = num;
}),
});

可以看到,异步action的实现跟同步的区别很大,使用了runInAction这个方法,在它的回调函数中去修改count的值。很坑的是,这个方法在[mobx-miniprogram-bindings](https://www.npmjs.com/package/mobx-miniprogram-bindings)中的官方文档中没有做任何说明,我百度了好久才找到。 现在,我们有了这个方法,在哪里触发好合适呢?答案是app.js中的onShow生命周期函数中。也就是每次我们进入小程序,就会设置一下count的初始值了。下面是代码:

// app.js
import { createStoreBindings } from 'mobx-miniprogram-bindings';
import { store } from './store/index';
App({
onShow() {
this.storeBindings = createStoreBindings(this, {
store,
fields: [],
actions: ['getCartListCount'],
});
// 在页面初始化的时候,更新购物车徽标的数量
this.getCartListCount();
},
});

到此为止,整个完整的徽标响应式改变和store的使用完美的融合了。 参考文章:blog.csdn.net/ice_stone_k…

如何获取tabbar的高度

当我们自定义tabbar以后,由于tabbar是使用的fixed定位,我们的内容区域如果不做任何限制,底部的内容就会被tabbar遮挡,所以我们需要给内容区域整体设置一个padding-bottom,那这个值是多少呢?有的人可能会说,直接把tabbar的高度固定,然后padding-bottom设置成这个高度的值不就可以了吗?你别忘了,现在五花八门的手机下面还有一个叫做安全区域的东西,如下图:

image.png

如果你没有把这个高度加上,那内容区域还是会被tabbar遮挡。下面我们就来看看这个高度具体如何计算呢? 我们以通过wx.getSystemInfoSync()获取机型的各种信息。 image.png

其中screenHeight是屏幕高度,safeAreabottom属性会自动计算安全区域也就是去除tabBar下面的空白区域后有用区域的纵坐标。如此我们就可以就算出来tabber的高度:

const res = wx.getSystemInfoSync()
const { screenHeight, safeArea: { bottom } } = res

if (screenHeight && bottom){
let safeBottom = screenHeight - bottom
const tabbarHeight = 48 + safeBottom
}

这里48是tabbar的高度,我们固定是48px。拿到tabbarHeight以后,把它设置成一个globalData,我们就可以给其他页面设置padding-bottom了。 我这里还使用了其他的一些属性,具体参考代码如下:

// app.js

App({
onLaunch() {
// 获取高度
this.getHeight();
},
onShow() {
},
globalData: {
// tabber+安全区域高度
tabbarHeight: 0,
// 安全区域的高度
safeAreaHeight: 0,
// 内容区域高度
contentHeight: 0,
},
getHeight() {
const res = wx.getSystemInfoSync();
// 胶囊按钮位置信息
const menuButtonInfo = wx.getMenuButtonBoundingClientRect();
const {
screenHeight,
statusBarHeight,
safeArea: { bottom },
} = res;
// console.log('resHeight', res);

if (screenHeight && bottom) {
// 安全区域高度
const safeBottom = screenHeight - bottom;
// 导航栏高度 = 状态栏到胶囊的间距(胶囊距上距离-状态栏高度) * 2 + 胶囊高度 + 状态栏高度
const navBarHeight =
(menuButtonInfo.top - statusBarHeight) * 2 +
menuButtonInfo.height +
statusBarHeight;
// tabbar高度+安全区域高度
this.globalData.tabbarHeight = 48 + safeBottom;
this.globalData.safeAreaHeight = safeBottom;
// 内容区域高度,用来设置内容区域最小高度
this.globalData.contentHeight = screenHeight - navBarHeight;
}
},
});

假如我们需要给首页设置一个首页设置一个padding-bottom

// components/layout/index.js
const app = getApp();
Component({
/**
* 组件的属性列表
*/
properties: {
bottom: {
type: Number,
value: 48,
},
},

/**
* 组件的方法列表
*/
methods: {},
});
<view style="padding-bottom: {{bottom}}px">
<slot></slot>
</view>

这里我简单粗暴的直接在外层套了一个组件,统一设置了padding-bottom。 除了自定义tabbar,还可以自定义navbar,这里我没这个功能,所以不展开讲了,这里放一个参考文章: 获取状态栏的高度。这个文章把如何自定义navbar,如何获取navbar的高度,讲的很通透,感兴趣的仔细拜读。

分页版上拉加载更多

为什么我称作是分页版本的上拉加载更多呢,因为就是上拉然后多加载一页,没有做那种虚拟加载,感兴趣的可以参考这篇文章(我觉得写的非常到位了)。下面我以商品列表为例,代码在pages/goods/list下,讲讲简单版本的实现:

<!--pages/goods/list/index.wxml-->

<view style="min-height: {{contentHeight}}px; padding-bottom: {{safeAreaHeight}}px">

<view wx:if="{{list.length > 0}}">
<goods-card wx:for="{{list}}" wx:key="index" item="{{item}}"></goods-card>
<!-- 上拉加载更多 -->
<load-more
list-is-empty="{{!list.length}}"
status="{{loadStatus}}"
/>
</view>

<van-empty wx:else description="该分类下暂无商品,去看看其他的商品吧~">
<van-button
round
type="danger"

bindtap="gotoBack">
查看其他商品
</van-button>
</van-empty>

</view>
// pages/goods/list/index.js
import { findGoodsList } from '../../../utils/api';
const app = getApp();
Page({
/**
* 页面的初始数据
*/
data: {
page: 1,
limit: 10,
list: [],
options: {},
loadStatus: 0,
contentHeight: app.globalData.contentHeight,
safeAreaHeight: app.globalData.safeAreaHeight,
},

/**
* 生命周期函数--监听页面加载
*/
onLoad(options) {
this.setData({ options });
this.loadGoodsList(true);
},

/**
* 页面上拉触底事件的处理函数
*/
onReachBottom() {
// 还有数据,继续请求接口
if (this.data.loadStatus === 0) {
this.loadGoodsList();
}
},

/**
* 商品列表
*/
async loadGoodsList(fresh = false) {
// wx.stopPullDownRefresh();
this.setData({ loadStatus: 1 });
let page = fresh ? 1 : this.data.page + 1;
// 组装查询参数
const params = {
page,
limit: this.data.limit,
...this.data.options,
};
try {
// loadstatus说明: 0-加载完毕,隐藏加载状态 1-正在加载 2-全部加载 3-加载失败
const res = await findGoodsList(params);
const data = res.data.records;
if (data.length > 0) {
this.setData({
list: fresh ? data : this.data.list.concat(data),
loadStatus: data.length === this.data.limit ? 0 : 2,
page,
});
} else {
// 数据全部加载完毕
this.setData({
loadStatus: 2,
});
}
} catch {
// 错误请求
this.setData({
loadStatus: 3,
});
}
},
});

代码已经很详细了,我再展开说明一下。

  • onLoad的时候第一次请求商品列表数据loadGoodsList,这里我加了一个fresh字段,用来区分是不是第一次加载,从而且控制page是不是等于1

  • 触发onReachBottom的时候,先判断loadStatus === 0,表示接口数据还没加载完,继续请求loadGoodsList

  • loadGoodsList里面,先设置loadStatus = 1,表示状态为加载中。如果fresh为false,则表示要请求下一页的数据了,page+1。

  • 接口请求成功,给了list添加数据的时候要注意了,这里需要再上次list的基础上拼接数据,所以得用concat。同时修改loadStatus状态,如果当前请求回来的数据条数小与limit(每页数据大小),则表示没有更对的数据了,loadStatus = 2,反之为0。

  • 最后为了防止特殊情况出现,还有个loadStatus = 3,表示加载失败的情况。

这里我封装了一个load-more组件,里面就是对loadStatus各种不同状态的处理。具体详情看看源码。 思考:如果加上个下拉刷新,跟上拉加载放在一起,如何实现呢?

如何分包

为什么要做小程序分包?先来看看小程序对文件包的大小限制 image.png 在不使用分包的时候,代码总包的大小限制为2M,如果使用了分包,总包大小可以达到20M,也就是我们能分10个包。 那么如何分包?非常的简单。代码如下:

{
"pages": [
"pages/index/index",
"pages/category/category",
"pages/cart/cart",
"pages/info/info",
"pages/login/index"
],
"subpackages": [
{
"root": "pages/goods",
"pages": [
"list/index",
"detail/index"
]
},
{
"root": "pages/address",
"pages": [
"list/index",
"add/index"
]
},
{
"root": "pages/order",
"pages": [
"pay/index",
"list/index",
"result/index",
"detail/index"
]
}
],
}

目录结构如下: image.png 解释一下: 我们subpackages下面的就是分包的内容,里面的每一个root就是一个包,pages里面的内容只能是这样的字符串路径,添加别的内容会报错。分包的逻辑:业务相关的页面放在一个包下,也就是一个目录下即可。 ⚠️注意:tabbar的页面不能放在分包里面。 下面是分包以后的代码依赖分析截图: image.png

后续更新计划

  • 小程序如何自定义navbar

  • 小程序如何添加typescript

  • 在小程序中如何做表单校验的小技巧

  • 微信支付流程

  • 如何在小程序中mock数据

  • 如何优化小程序

本文章可以随意转载。转给更多需要帮助的人。看了源码觉得有帮助的可以点个star。我会持续更新更多系列教程。

作者:白哥学前端
来源:https://juejin.cn/post/7202495679397511227

0 个评论

要回复文章请先登录注册