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

语言: CN / TW / HK

功能截图

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

开发版本

  • 微信开发者工具版本: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 模块 选项,构建完成后,即可引入组件。 image.png

使用组件

引入组件 ```javascript // 通过 npm 安装 // app.json "usingComponents": { "van-button": "@vant/weapp/button/index" }

使用组件shell 按钮 `` 如果预览没有效果,从新构建一次npm,然后重新打开此项目`。

自定义tabbar

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

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目录。 image.png .wxml代码如下: ```javascript

{{item.text}}

`` 注意这里的徽标控制我是通过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会直接沉到底部 image.png 别忘了在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-bindingsmobx-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设置成这个高度的值不就可以了吗?你别忘了,现在五花八门的手机下面还有一个叫做安全区域的东西,如下图:

image.png

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

其中screenHeight是屏幕高度,safeAreabottom属性会自动计算安全区域也就是去除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 ``` 这里我简单粗暴的直接在外层套了一个组件,统一设置了padding-bottom。 除了自定义tabbar,还可以自定义navbar,这里我没这个功能,所以不展开讲了,这里放一个参考文章: 获取状态栏的高度。这个文章把如何自定义navbar,如何获取navbar的高度,讲的很通透,感兴趣的仔细拜读。

分页版上拉加载更多

为什么我称作是分页版本的上拉加载更多呢,因为就是上拉然后多加载一页,没有做那种虚拟加载,感兴趣的可以参考这篇文章(我觉得写的非常到位了)。下面我以商品列表为例,代码在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各种不同状态的处理。具体详情看看源码。 思考:如果加上个下拉刷新,跟上拉加载放在一起,如何实现呢?

如何分包

为什么要做小程序分包?先来看看小程序对文件包的大小限制 image.png 在不使用分包的时候,代码总包的大小限制为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" ] } ], } 目录结构如下: image.png 解释一下: 我们subpackages下面的就是分包的内容,里面的每一个root就是一个包,pages里面的内容只能是这样的字符串路径,添加别的内容会报错。分包的逻辑:业务相关的页面放在一个包下,也就是一个目录下即可。 ⚠️注意:tabbar的页面不能放在分包里面。 下面是分包以后的代码依赖分析截图: image.png

后续更新计划

  • 小程序如何自定义navbar
  • 小程序如何添加typescript
  • 在小程序中如何做表单校验的小技巧
  • 微信支付流程
  • 如何在小程序中mock数据
  • 如何优化小程序

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