文章目錄
- 一.項目介紹和前置內容
- 1.重要鏈接
- 2.技術棧
- 二.創建uniapp項目
- 1.使用HBuilderX創建
- 2.使用命令行創建
- 3.如何使用vscode開發uniapp項目?
- step1:把項目拉入vscode,開始下相關插件
- step2:ts類型校驗
- step3:設置json文件可以允許注釋
- 4.pages.json文件的作用是什么?
- 5.示例:在項目中實現一個tabbar功能
- 6.如何在手機上查看當前項目效果?
- 7.uniapp和原生小程序開發的區別?
- 三. 拉取項目代碼(直接從這兒開始)
- 如何拉取項目代碼?
- 拉取項目代碼后需要做什么?
- 四.小程序的基礎架構
- 1.構建頁面
- 1.1.安裝uni-ui組件庫
- 1.2.讓uni-ui組件庫中的組件實現自動按需導入
- 1.3.配置TS類型
- 2.狀態管理(以my頁的數據持久化為例)
- step0:安裝持久化存儲插件pinia-plugin-persistedstate
- step1:在`stores/index.ts`中準備好pinia相關代碼
- step2:在`main.ts`中準備好相關代碼
- step3:在`stores/modules/member.ts`中提前準備好文件用于管理會員信息
- step4:在`src/pages/my/my.vue`中提前準備好頁面和代碼
- step5【重點】:改`member.ts`中的 `persist: true`語句
- 3.數據交互
- 3.1.封裝攔截器
- uniapp中如何添加一個攔截器(語法)?
- 攔截器要做什么?
- 如何實現對request請求和上傳文件請求的攔截?(重點)
- 驗證:攔截器是否添加成功
- 3.2.封裝請求函數
- 3.2.1.請求成功的業務處理
- 3.2.2.請求失敗的業務處理
- 3.2.3.http.ts完整代碼
一.項目介紹和前置內容
該項目是一個B2C電商平臺項目
1.重要鏈接
項目演示: http://erabbit.itheima.net/#/
接口文檔:https://www.apifox.cn/apidoc/shared/0e6ee326-d646-41bd-9214-29dbf47648fa
項目代碼(從這里開始開發):git clone http://git.itcast.cn/heimaqianduan/erabbit-uni-app-vue3-ts.git
2.技術棧
- vue3.0組合式api
- vue-cli項目腳手架
- axios請求接口
- vue-router單頁路由
- vuex狀態管理
- vuex-persiststate數據持久化
- normalize.css初始化樣式
- @vueuse/core組合api常用工具庫
- 算法Power Set
- dayjs日期處理小組件
- validate表單校驗
二.創建uniapp項目
兩種方式創建uni-app項目:HBuilderX(都是DCloud公司旗下)和命令行
1.使用HBuilderX創建
- 創建項目:
HBuilderX:創建項目>uniapp>默認模板>vue3
- 下載插件:
工具>插件安裝>安裝新插件:uni-app(vue3)編譯器
-
在微信開發者工具中運行項目:
運行>微信開發者工具>選擇路徑
-
開啟服務端口:
前往微信開發者工具>設置>安全服務端口:開啟
-
分離窗口并固定:開發者工具只做效果展示,只留該效果窗口即可
2.使用命令行創建
vue create -p dcloudio/uni-preset-vue my-uniapp-project
3.如何使用vscode開發uniapp項目?
vscode的優勢是對ts類型的支持比較友好,本項目也將使用vscode進行uniapp開發
step1:把項目拉入vscode,開始下相關插件
uni-create-view
插件:快速創建uniapp頁面或組件
uni-helper
插件:uni-app代碼提示
uniapp小程序擴展插件
:鼠標懸停查文檔
step2:ts類型校驗
- 安裝類型聲明文件:
pnpm i -D @types/wechat-miniprogram @uri-helper/uni-app-types
- 配置
tsconfig.json
文件
{......"types": ["@dcloudio/types","miniprogram-api-typings","@types/wechat-miniprogram","@uni-helper/uni-app-types","@uni-helper/uni-ui-types"]},"vueCompilerOptions": {// experimentalRuntimeMode 已廢棄,請升級 Vue - Official 插件至最新版本"plugins": ["@uni-helper/uni-app-types/volar-plugin"]},
step3:設置json文件可以允許注釋
vscode設置>搜"文件關聯">添加:
項 值
manifest.json jsonc
pages.json jsonc
4.pages.json文件的作用是什么?
配置頁面的路由,導航欄,tabBar等頁面類信息
*有些類似routes數組
5.示例:在項目中實現一個tabbar功能
把圖標在pages/static作為靜態資源放在該目錄下
實現tabBar步驟:
1.pages右鍵>新建uniapp頁面:my
2.pages/pages.json中新增tabBar配置如下:
"tabBar":{"list":[{'pagePath':'pages/index/index','text':'首頁','iconPath':"xxx.png",'selectedIconPath':'xxx_selected.png'},{'pagePath':'pages/my/my','text':'我的'}]
}3.復制圖標文件到statics文件夾中,配置圖標路徑
6.如何在手機上查看當前項目效果?
manifest.json
配置AppID,從微信小程序配置復制
7.uniapp和原生小程序開發的區別?
每個頁面都是vue文件,數據綁定和事件處理使用vue規范
- 數據綁定:
屬性綁定不再需要寫成src='{{ url }}'
,直接寫成vue的動態綁定屬性:src='url'
- 事件綁定:
之前:bindtap='eventName'
現在:@tap='eventName'
,并支持傳參
- 支持vue常用指令
三. 拉取項目代碼(直接從這兒開始)
如何拉取項目代碼?
項目并非從零開發,直接拉取項目代碼:git clone http://git.itcast.cn/heimaqianduan/erabbit-uni-app-vue3-ts.git
拉取項目代碼后需要做什么?
- 在
manifest.json
中添加微信小程序的appid - 初始化:
pnpm install
- 生成
list/mp-weixin
文件:pnpm dev:mp-weixin
- 微信開發者工具中導入項目
mp-weixin
四.小程序的基礎架構
小程序的基礎架構具體實現分為:構建頁面,狀態管理和數據交互
1.構建頁面
構建界面分為:安裝uni-ui,自動引入組件,配置ts類型
1.1.安裝uni-ui組件庫
安裝命令:npm i @dcloudio/uni-ui
1.2.讓uni-ui組件庫中的組件實現自動按需導入
// pages.json
{// 組件自動導入"easycom": {"autoscan": true,"custom": {// uni-ui 規則如下配置 "^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue" }},"pages": [// …省略]
}
此時可以直接使用uni-ui的相關組件:
<uni-card>.....
</uni-card>
1.3.配置TS類型
- step1:安裝依賴
npm i -D @uni-helper/uni-ui-types
- step2:配置
tsconfig.js
確保compilerOptions.types
中含有@dcloudio/types
和@uni-helper/uni-ui-types
,且include包含了對應的vue文件
// tsconfig.json
{"compilerOptions": {// ..."types": ["@dcloudio/types", // uni-app API 類型"miniprogram-api-typings", // 原生微信小程序類型"@uni-helper/uni-app-types", // uni-app 組件類型"@uni-helper/uni-ui-types" // uni-ui 組件類型 ]},// vue 編譯器類型,校驗標簽類型"vueCompilerOptions": {"nativeTags": ["block", "component", "template", "slot"]}
}
- step3:重啟vscode,成功標志----
<uni-card>
有了類型聲明
2.狀態管理(以my頁的數據持久化為例)
最終目的是設置小程序端的Pinia持久化
Pinia持久化的方案與之前的寫法有些許不同:
以前的持久化主要針對的是網頁端的localStorage
而小程序端的持久化針對的是小程序端的api,即:uni.setStorageSync()
和getsetStorageSync()
step0:安裝持久化存儲插件pinia-plugin-persistedstate
安裝命令:pnpm i pinia-plugin-persistedstate
(Pinia 用法與 Vue3 項目完全一致,uni-app 項目僅需解決持久化插件兼容性問題。)
step1:在stores/index.ts
中準備好pinia相關代碼
import {createPinia} from "pinia"
import persist from 'pinia-plugin-persistedstate'//創建pinia實例
const pinia = createPinia()
//使用持久化存儲插件
pinia.use(persist)
//默認導出,給main.ts使用
export default pinia
//模塊統一導出
export * from "/modules/member"
step2:在main.ts
中準備好相關代碼
import {createSSRApp} from 'vue'
import App from './App.vue'//導入pinia實例
import pinia from './stores'export function createApp(){//創建vue實例const app=createSSRApp(App)//使用piniaapp.use(pinia)return {app}
}
*若pinia類型報錯,解決方法:npm i pinia
step3:在stores/modules/member.ts
中提前準備好文件用于管理會員信息
import { defineStore } from 'pinia'
import { ref } from 'vue'// 定義 Store
export const useMemberStore = defineStore('member',() => {// 會員信息const profile = ref<any>()// 保存會員信息,登錄時使用const setProfile = (val: any) => {profile.value = val//用戶保存的信息更新到空的profile中}// 清理會員信息,退出時使用const clearProfile = () => {profile.value = undefined}// 記得 returnreturn {profile,setProfile,clearProfile,}},// TODO: 持久化{persist: true,},
)
step4:在src/pages/my/my.vue
中提前準備好頁面和代碼
<script setup lang="ts">
import { useMemberStore } from '@/stores'const memberStore = useMemberStore()
</script><template><view class="my"><view>會員信息:{{ memberStore.profile }}</view><button@tap="memberStore.setProfile({nickname: '黑馬先鋒',})"size="mini"plaintype="primary">保存用戶信息</button><button @tap="memberStore.clearProfile()" size="mini" plain type="warn">清理用戶信息</button></view>
</template><style lang="scss">
//
</style>
當前的代碼可以實現數據的添加和清除,但是還沒有實現持久化
一刷新頁面,數據就會丟失
如何實現?
step5【重點】:改member.ts
中的 persist: true
語句
// TODO: 持久化{persist: true,},
該語句只能在網頁端實現持久化,要在小程序端實現持久化,要改正如下:
// TODO: 持久化{persist: {storage:{getItem(key){uni.getStorageSync(key)},setItem(key,value){uni.setStorageSync(key,value)}}}},
*注:此處遇到了persist的類型報錯問題,后續解決
3.數據交互
封裝請求工具:攔截器和請求函數
3.1.封裝攔截器
通過攔截器的封裝,實現request請求和uploadFile上傳文件的攔截
接口文檔:https://www.apifox.cn/apidoc/shared/0e6ee326-d646-41bd-9214-29dbf47648fa
uniapp中如何添加一個攔截器(語法)?
uni.addIntercepters(STRING,OBJECT),其中,STRING是攔截器的名稱,OBJECT的參數是傳入具體的配置(可以寫一個invoke函數,在攔截前觸發)
攔截器要做什么?
- 封裝請求基地址
- 超時時間
- 添加請求頭標識
- 添加token
如何實現對request請求和上傳文件請求的攔截?(重點)
//src/utils/http.tsimport { useMemberStore } from '@/stores'
/* 請求攔截器 */
/*
添加攔截器:攔截請求和上傳文件兩個接口
TODO:1.非http開頭的url,會自動拼接baseURL2.請求超時3.添加小程序端請求頭標識4.添加token請求頭標識
*///基地址
const baseURL = 'http://pcapi-xiaotuxian-front-devtest.itheima.net'
// 添加攔截器:攔截請求和上傳文件兩個接口
const httpInterceptor = {// 攔截器對象//攔截前觸發的函數,其中options就是請求的配置對象//它會拿到uni.request({methods:xxx,url:'xxx'})中的配置對象invoke(options: UniApp.RequestOptions) {//指定參數的類型:鼠標懸停在uni.request上獲取//1.對于非http開頭的url,會自動拼接baseURLif (!options.url.startsWith('http')) {options.url = baseURL + options.url}//2.請求超時:默認10soptions.timeout = 10000//3.添加小程序端請求頭標識options.header = {...options.header, //如果存在,就保留原有header'source-Client': 'miniapp',}//4.添加token請求頭標識:登錄成功后,拿到token,放到這里const memberStore = useMemberStore()const token = memberStore.profile?.token //此處的token從memberStore,profile中獲取if (token) {options.header.Authorization = token}return options},
}
// 攔截request請求
uni.addInterceptor('request', httpInterceptor)
// 攔截上傳文件請求
uni.addInterceptor('uploadFile', httpInterceptor)
驗證:攔截器是否添加成功
src/pages/my/my.vue<script setup lang="ts">
import { useMemberStore } from '@/stores'const memberStore = useMemberStore()
import { http } from '@/utils/http' //在src/utils/http.ts中配置了攔截器,里面有基地址
//測試接口按鈕
const getData = async () => {// uni.request({// method: 'GET',// url: '/home/banner',// })//為http添加類型成功后,嘗試傳入一個string格式的數組,測試泛型是否生效//此時res的類型是:Data<string[]>,會決定res.code等數據的類型const res = await http<string[]>({method: 'GET',url: '/home/banner',header: {},})console.log('請求成功:', res.code) //code的類型成功推斷為string
}
</script><template><view class="my"><view>會員信息:{{ memberStore.profile }}</view><!-- 模擬token --><button@tap="memberStore.setProfile({nickname: '黑馬先鋒',token: '123456',})"size="mini"plaintype="primary">保存用戶信息</button><button @tap="memberStore.clearProfile()" size="mini" plain type="warn">清理用戶信息</button><!-- 新增一個按鈕 --><button @tap="getData">點擊</button></view>
</template><style lang="scss">
//
</style>
3.2.封裝請求函數
封裝請求函數是為了更方便地發起請求
回顧:axios函數的返回值是一個Promise對象,配置async和await
可以更方便獲取成功的數據
為了方便使用,我們封裝的請求函數也要返回一個Promise對象
同時,由于uniapp的攔截器并不完善,因此響應攔截器的功能對類型支持并不友好,因此,前面的攔截器只完成了請求前的攔截,但響應后的攔截還沒設置
我們要通過自己封裝的請求函數實現之前axios響應攔截器的業務功能
對于響應攔截器,又分為成功和失敗兩種情況
成功時,提取核心數據(res.data)添加類型(支持泛型):uni.request函數不支持添加類型,因此要通過函數封裝,自己添加泛型,通過泛型來確定后續使用的具體類型
失敗時,處理網絡錯誤:提示用戶更換網絡401 錯誤:清理用戶信息,跳轉登錄頁其他錯誤:根據后端錯誤信息輕提示
3.2.1.請求成功的業務處理
- http.ts
/* TODO:
* 1.返回Promise對象
* 2.請求成功
* 2.1.獲取核心數據res.data
* 2.2.添加類型,支持泛型
* 3.請求失敗
* 3.1.網絡錯誤:提示用戶換網絡
* 3.2.401錯誤:清理用戶信息,跳轉登錄頁面
* 3.3.其他錯誤:根據后端返回的message提示用戶
*/
//定義一個后端返回值的類型
interface Data<T> {code: string //狀態碼:'1"msg: string //提示信息:'請求成功'result: T //核心數據類型:{{}.{},{},{},{}}
}
// 2.給http添加類型(其類型可變)--泛型:T代表任意類型
//T接收到的類型用來確定my.vue中res的類型
export const http = <T>(options: UniApp.RequestOptions) => {//給Promise(當前是Unknown)指定響應成功的類型return new Promise<Data<T>>((resolve, reject) => {uni.request({...options,// 請求成功success(res) {// resolve(res.data) //1.獲取核心數據res.data//res.data類型報錯,應為string|AnyObject|ArrayBuffer中的AnyObject,并準確指定為Data<T>//使用類型斷言,將res.data的類型斷言為Data<T>resolve(res.data as Data<T>)// 此時可以去my.vue中使用res進行測試},})})
}
- my.vue:使用res進行測試
//測試接口按鈕
const getData = async () => {// uni.request({// method: 'GET',// url: '/home/banner',// })//為http添加類型成功后,嘗試傳入一個string格式的數組,測試泛型是否生效//此時res的類型是:Data<string[]>,會決定res.code等數據的類型const res = await http<string[]>({method: 'GET',url: '/home/banner',header: {},})console.log('請求成功:', res.code) //code的類型成功推斷為string
}
- 步驟總結
首先內部返回一個Promise對象,方便通過async和await獲取到數據,為了使用的數據更加簡潔使用類型推斷提取出核心數據:res.data as Data<T>最后再為TS項目設置類型:(通過泛型實現)
3.2.2.請求失敗的業務處理
uni.request的success回調函數僅僅表示服務器響應成功,但未處理狀態碼,業務中使用不方便
uni.request({...options,//響應成功success(res){},//響應失敗fail(err){}
})
一有響應就走success,帶來的問題:
若服務器有響應,但是響應的結果是token獲取失敗,此時也會走success回調,這無疑在邏輯上不準確
而axios函數僅僅只有響應狀態碼為2xx時才調用resolve函數,表示獲取數據成功,業務中使用更準確
核心代碼如下:
if (res.statusCode >= 200 && res.statusCode < 300) {//請求成功:2xxresolve(res.data as Data<T>)} else if (res.statusCode === 401) {//請求失敗:401//清理用戶信息,跳轉登錄頁面uni.showToast({// title:'登錄過期,請重新登錄',icon: 'none',title: (res.data as Data<T>).msg || '登錄過期,請重新登錄',})} else {//其他錯誤:根據后端返回的message提示用戶uni.showToast({title: (res.data as Data<T>).msg || '請求錯誤',icon: 'none',})reject(new Error((res.data as Data<T>).msg))}
3.2.3.http.ts完整代碼
import { useMemberStore } from '@/stores'
/* 請求攔截器 */
/*
添加攔截器:攔截請求和上傳文件兩個接口
TODO:1.非http開頭的url,會自動拼接baseURL2.請求超時3.添加小程序端請求頭標識4.添加token請求頭標識
*///請求基地址
const baseURL = 'http://pcapi-xiaotuxian-front-devtest.itheima.net'
// 添加攔截器:攔截請求和上傳文件兩個接口
const httpInterceptor = {// 攔截器對象//攔截前觸發的函數,其中options就是請求的配置對象//它會拿到uni.request({methods:xxx,url:'xxx'})中的配置對象invoke(options: UniApp.RequestOptions) {//指定參數的類型:鼠標懸停在uni.request上獲取//1.對于非http開頭的url,會自動拼接baseURLif (!options.url.startsWith('http')) {options.url = baseURL + options.url}//2.請求超時:默認10soptions.timeout = 10000//3.添加小程序端請求頭標識options.header = {...options.header, //如果存在,就保留原有header'source-Client': 'miniapp',}//4.添加token請求頭標識:登錄成功后,拿到token,放到這里const memberStore = useMemberStore()const token = memberStore.profile?.token //此處的token從memberStore,profile中獲取if (token) {options.header.Authorization = token}return options},
}
// 攔截request請求
uni.addInterceptor('request', httpInterceptor)
// 攔截上傳文件請求
uni.addInterceptor('uploadFile', httpInterceptor)/* 響應攔截器 */
/*** 請求函數* @params UniApp.RequestOptions* @returns Promise<any>* TODO:* 1.返回Promise對象* 2.請求成功* 2.1.獲取核心數據res.data* 2.2.添加類型,支持泛型* 3.請求失敗* 3.1.網絡錯誤:提示用戶換網絡* 3.2.401錯誤:清理用戶信息,跳轉登錄頁面* 3.3.其他錯誤:根據后端返回的message提示用戶*/
//定義一個后端返回值的類型
interface Data<T> {code: string //狀態碼:'1"msg: string //提示信息:'請求成功'result: T //核心數據類型:{{}.{},{},{},{}}
}// 2.給http添加類型(其類型可變)--泛型:T代表任意類型
//T接收到的類型用來確定my.vue中res的類型
export const http = <T>(options: UniApp.RequestOptions) => {//給Promise(當前是Unknown)指定響應成功的類型return new Promise<Data<T>>((resolve, reject) => {uni.request({...options,// 請求成功success(res) {// resolve(res.data) //1.獲取核心數據res.data//res.data類型報錯,應為string|AnyObject|ArrayBuffer中的AnyObject,并準確指定為Data<T>//使用類型斷言,將res.data的類型斷言為Data<T>// resolve(res.data as Data<T>)// 此時可以去my.vue中使用res進行測試//優化:根據后端返回的狀態碼,判斷請求是否成功if (res.statusCode >= 200 && res.statusCode < 300) {//請求成功:2xxresolve(res.data as Data<T>)} else if (res.statusCode === 401) {//請求失敗:401//清理用戶信息,跳轉登錄頁面uni.showToast({// title:'登錄過期,請重新登錄',icon: 'none',title: (res.data as Data<T>).msg || '登錄過期,請重新登錄',})} else {//其他錯誤:根據后端返回的message提示用戶uni.showToast({title: (res.data as Data<T>).msg || '請求錯誤',icon: 'none',})reject(new Error((res.data as Data<T>).msg))}},// 請求失敗fail(err) {//提示用戶換網絡uni.showToast({title: '請求失敗,請檢查您的網絡',icon: 'none',})reject(err)},})})
}