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

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