用一週時間開發了一個微信小程式,我遇到了哪些問題?
功能截圖
| |
|
| --- | --- |
|
|
|
|
|
|
|
|
|
|
|
|
特別說明:由於本專案是用於教學案例,並沒有上線的二維碼供大家體驗。
開發版本
- 微信開發者工具版本: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"** 去除,小程式的[新版基礎元件](https://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](https://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的使用完美的融合了。 參考文章:https://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。我會持續更新更多系列教程。