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

6,667 阅读15分钟

功能截图

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 class="layout" style="padding-bottom: {{bottom}}px">
  <slot></slot>
</view>

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

分页版上拉加载更多

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

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

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

  <view class="goods-list" 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" 
      class="bottom-button" 
      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。我会持续更新更多系列教程。