用一周时间开发了一个微信小程序,我遇到了哪些问题?
功能截图
| |
|
| --- | --- |
|
|
|
|
|
|
|
|
|
|
|
|
特别说明:由于本项目是用于教学案例,并没有上线的二维码供大家体验。
开发版本
- 微信开发者工具版本:1.06
- 调试基础库:2.30
代码仓库
建议全文参考源代码观看效果更佳,代码可直接在微信开发者工具当中打开预览,appid需要替换成自己的。
获取用户信息变化
用户头像昵称获取规则已调整,现在微信小程序已经获取不到用户昵称和头像了,只能已通过用户回填(提供给用户一个修改昵称和头像的表单页面)的方式来实现。不过还是可以获取到code跟后端换取token的方式来进行登录。
vant weapp组件库的使用
1.需要使用npm构建的能力,用 npm 构建前,请先阅读微信官方的 npm 支持。初始化package.json
shell
npm init
2.安装@vant/weapp
```shell
通过 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"** 去除,小程序的[新版基础组件](http://developers.weixin.qq.com/miniprogram/dev/reference/configuration/app.html#style)强行加上了许多样式,难以覆盖,不关闭将造成部分组件样式混乱。
3.修改 project.config.json
开发者工具创建的项目,**miniprogramRoot** 默认为 **miniprogram**,**package.json** 在其外部,npm 构建无法正常工作。
需要手动在 **project.config.json** 内添加如下配置,使开发者工具可以正确索引到 npm 依赖的位置。
shell
{
...
"setting": {
...
"packNpmManually": true,
"packNpmRelationList": [
{
"packageJsonPath": "./package.json",
"miniprogramNpmDistDir": "./miniprogram/"
}
]
}
}
``
注意: 由于目前新版开发者工具创建的小程序目录文件结构问题,npm构建的文件目录为
miniprogram_npm,并且开发工具会默认在当前目录下创建
miniprogram_npm的文件名,所以新版本的
miniprogramNpmDistDir配置为
'./'`即可。
4.构建 npm 包
打开微信开发者工具,点击 工具 -> 构建 npm,并勾选 使用 npm 模块 选项,构建完成后,即可引入组件。
使用组件
引入组件 ```javascript // 通过 npm 安装 // app.json "usingComponents": { "van-button": "@vant/weapp/button/index" }
使用组件
shell
``
如果预览没有效果,从新构建一次npm,然后
重新打开此项目`。
自定义tabbar
这里我的购物车使用了徽标,所以需要自定义一个tabbar,这里自定义以后,会引发后面的一系列连锁反应(比如内容区域高度塌陷,导致tabbar遮挡内容区域),后面会讲如何计算。效果如下图:
1. 配置信息
- 在 app.json 中的 tabBar 项指定 custom 字段,同时其余 tabBar 相关配置也补充完整。
- 所有 tab 页的 json 里需声明 usingComponents 项,也可以在 app.json 全局开启。
示例: ```javascript { "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
目录。
.wxml
代码如下:
```javascript
``
注意这里的徽标控制我是通过
info字段来控制的,然后数量
cartCount单独第一个了一个字段,这个字段是通过
store来管理的,后面会讲为什么通过
stroe`来控制的。
3. 编写 tabBar 代码
用自定义组件的方式编写即可,该自定义组件完全接管 tabBar 的渲染。另外,自定义组件新增 getTabBar 接口,可获取当前页面下的自定义 tabBar 组件实例。 ```javascript 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.设置样式
css
.tab-bar {
position: fixed;
bottom: 0;
left: 0;
right: 0;
height: 48px;
background: white;
display: flex;
padding-bottom: env(safe-area-inset-bottom);
}
这里的样式单独贴出来说明一下:
shell
padding-bottom: env(safe-area-inset-bottom);
可以让出底部安全区域,不然的话tabbar会直接沉到底部
别忘了在
index.json
中设置component=true
shell
{
"component": true
}
5.tabbar页面设置index
上面的代码添加完毕以后,我们的tabbar就出来了,但是有个问题,就是在点击tab的时候,样式不会改变,必须再点击一次,这是因为当你切换页面或者刷新页面的时候,index的值会重置,为了解决这个问题,我们需要在每个tabbar的页面添加下面的代码:
javascript
/**
* 生命周期函数--监听页面显示
*/
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-bindings和mobx-miniprogram
这两个库来解决。真是帮了我的大忙了。
下面我们直接来使用。
先安装两个插件:
shell
npm install --save mobx-miniprogram mobx-miniprogram-bindings
方式跟安装vant weapp
一样,npm安装完成以后,在微信开发者工具当中构建npm
即可。
下面我们来通过如何实现一个tabbar徽标的场景来学习如何在微信小程序中使用store来管理全局数据。
tabbar徽标实现
1.定义store
```javascript 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
中,我们贴一下主要的代码:
```javascript
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
页面,下面是具体的逻辑:
```javascript
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中定义了一个一步方法: ```javascript 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](http://www.npmjs.com/package/mobx-miniprogram-bindings)`中的官方文档中没有做任何说明,我百度了好久才找到。
现在,我们有了这个方法,在哪里触发好合适呢?答案是`app.js`中的`onShow`生命周期函数中。也就是每次我们进入小程序,就会设置一下count的初始值了。下面是代码:
javascript
// 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的使用完美的融合了。 参考文章:http://blog.csdn.net/ice_stone_kai/article/details/126920723
如何获取tabbar的高度
当我们自定义tabbar以后,由于tabbar是使用的fixed定位,我们的内容区域如果不做任何限制,底部的内容就会被tabbar遮挡,所以我们需要给内容区域整体设置一个padding-bottom
,那这个值是多少呢?有的人可能会说,直接把tabbar的高度固定,然后padding-bottom
设置成这个高度的值不就可以了吗?你别忘了,现在五花八门的手机下面还有一个叫做安全区域的东西,如下图:
如果你没有把这个高度加上,那内容区域还是会被tabbar遮挡。下面我们就来看看这个高度具体如何计算呢?
我们以通过wx.getSystemInfoSync()
获取机型的各种信息。
其中screenHeight
是屏幕高度,safeArea
的bottom
属性会自动计算安全区域也就是去除tabBar下面的空白区域后有用区域的纵坐标。如此我们就可以就算出来tabber的高度:
```javascript
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`了。
我这里还使用了其他的一些属性,具体参考代码如下:
javascript
// 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`:
javascript
// components/layout/index.js
const app = getApp();
Component({
/*
* 组件的属性列表
/
properties: {
bottom: {
type: Number,
value: 48,
},
},
/*
* 组件的方法列表
/
methods: {},
});
javascript
分页版上拉加载更多
为什么我称作是分页版本的上拉加载更多呢,因为就是上拉然后多加载一页,没有做那种虚拟加载,感兴趣的可以参考这篇文章(我觉得写的非常到位了)。下面我以商品列表为例,代码在pages/goods/list
下,讲讲简单版本的实现:
```javascript
javascript
// 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
各种不同状态的处理。具体详情看看源码。
思考:如果加上个下拉刷新,跟上拉加载放在一起,如何实现呢?
如何分包
为什么要做小程序分包?先来看看小程序对文件包的大小限制
在不使用分包的时候,代码总包的大小限制为2M,如果使用了分包,总包大小可以达到20M,也就是我们能分10个包。
那么如何分包?非常的简单。代码如下:
javascript
{
"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"
]
}
],
}
目录结构如下:
解释一下:
我们subpackages下面的就是分包的内容,里面的每一个root就是一个包,pages里面的内容只能是这样的字符串路径,添加别的内容会报错。分包的逻辑:业务相关的页面放在一个包下,也就是一个目录下即可。
⚠️注意:
tabbar
的页面不能放在分包里面。
下面是分包以后的代码依赖分析截图:
后续更新计划
- 小程序如何自定义navbar
- 小程序如何添加typescript
- 在小程序中如何做表单校验的小技巧
- 微信支付流程
- 如何在小程序中mock数据
- 如何优化小程序
本文章可以随意转载。转给更多需要帮助的人。看了源码觉得有帮助的可以点个star。我会持续更新更多系列教程。