用一週時間開發了一個微信小程序,我遇到了哪些問題?

語言: 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。我會持續更新更多系列教程。