Vue3 – Uni-App 小程序项目配置(Vite+TS+Vue3+Pinia)
简介
本文主要讲解使用 Uni-app 开发多端小程序之前,需要对项目进行配置,以及注意事项。
创建项目
使用 HBuildX 创建小程序项目
使用HBX创建非常简单,通过以下步骤就可以实现项目创建
目录结构说明:
|- pages 业务页面文件存放的目录
|---- index
|--------index.vue index页面
|- static 存放应用引用的本地静态资源的目录(注意:静态资源只能存放于此)
|- unpackage 非工程代码,一般存放运行或发行的编译结果
|- index.html H5端页面
|- main.js Vue初始化入口文件
|- App.vue 配置App全局样式、监听应用生命周期
|- page.json 配置页面路由、导航栏、tabBar等页面类信息
|- manifest.json 配置appid、应用名称、logo、版本等打包信息
|- uni.scss uni-app内置的常用样式变量
使用命令行创建小程序项目
使用命令行如下:
npx degit dcloudio/uni-preset-vue#vite-ts uni-app-vue3-ts
说明
npx包管理器
degit 往github中下载包文件,类似于 npm install 中的 install
dcloudio/uni-preset-vue 是指包名
#vite-ts 包名下的分支
uni-app-vue3-ts 把包下载到本地目录名
导入到任意IDE开发工具中
pnpm install
安装依赖
pnpm dev:mp-weixin
运行小程序调试
这时在根目录下会生成一个 dev 文件夹,该文件夹为编译后的文件夹,我们通过微信开发者工具导入文件夹中的内容,并在微信开发者工具中运行,既可完成调试。
uni-create-view 插件->用于创建页面
uni-helper 插件 -> 代码提示
uniapp小程序扩展 插件 -> 鼠标悬停查文档
安装插件
对于TypeScript 而言,我们还需要对TS类型进行配置,通过安装以下包安装ts类型
pnpm i @types/wechat-miniprogram @uni-helper/uni-app-types
安装完成后,在 tsconfig.json 中加入以下代码启用类型检查
{
"compilerOptions": {
"type":[
"@dcloudio/types",
"@types/wechat-miniprogram",
"@uni-helper/uni-app-types"
]
},
"vueCompilerOptions" : {
"experimentalRuntimeMode": "runtime-uni-app"
}
}
page.json 文件配置说明
pages
pages 属性用于定义需要显示在小程序的页面文件路径,默认排在第一个下标元素会被认为是小程序的首页。
"pages": [
{ 页面配置 }
],
页面配置:
path -> 定义页面路径地址
style -> 定义该页面的基本属性,如标题颜色、标题文字等
"style": {
"navigationBarTitleText": "首页",
"navigationBarShadow": {
导航栏阴影
},
"navigationBarBackgroundColor": "导航栏背景颜色",
"navigationStyle": 导航栏样式,可选 Custom和Default,当Custom时,则关闭导航栏
"navigationBarTextStyle": 导航栏标题颜色,仅支持 black/white ,
"enablePullDownRefresh": 是否开启下拉刷新 ,
"onReachBottomDistance": 页面上拉触底事件触发时距页面底部距离,
"transparentTitle": 导航栏透明设置。默认 none ,
"usingComponents": {
使用自定义组件
},
"topWindow": 当存在 topWindow时,当前页面是否显示 topWindow ,
"leftWindow": 当存在 leftWindow时,当前页面是否显示 leftWindow,
"rightWindow": 当存在 rightWindow时,当前页面是否显示 rightWindow,
titlePenetrate": 导航栏点击穿透,
"backgroundColor": "页面背景颜色 ",
"backgroundColorBottom": 底部窗口的背景色,
"backgroundColorTop": 顶部窗口的背景色,
"backgroundTextStyle": 下拉 loading 的样式 ,
"mp-weixin": {
微信小程序特有配置
},
}
globalStyle
globalStyle 是用于指定整个小程序的全局配置项,所配置的选项会影响整个小程序
tabbar
tabbar属性是用于定义小程序的下方选择栏的,必须至少存在两个选项值才可会显示
"tabBar": {
"redDotColor": "tabbar上红点颜色 ",
"selectedColor": "tabbar选中的颜色",
"color": "tab 上的文字默认颜色",
"iconWidth": "图标默认宽度(高度等比例缩放)",
"iconfontSrc": ".ttf文件目录 ",
"backgroundColor": "tab 的背景色",
"backgroundImage": "设置图片背景色优先级高于 backgroundColor",
"backgroundRepeat": "设置标题栏的背景图平铺方式 ",
"spacing": "图标和文字的间距",
"list": [
{
"pagePath": "页面路径",
"iconPath": "图标图片路径",
"iconfont": {
字体图标,优先级高于 iconPath
},
"selectedIconPath": "选中时的图片路径",
}
],
}
统一代码规范
安装ESLint与Prettier
pnpm i -D eslint prettier eslint-plugin-vue @vue/eslint-config-prettier @vue/eslint-config-typescript @rushstack/eslint-patch @vue/tsconfig
新建 .eslintrc.cjs 文件,添加以下 eslint 配置
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution')
module.exports = {
root: true,
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript',
'@vue/eslint-config-prettier',
],
// 小程序全局变量
globals: {
uni: true,
wx: true,
WechatMiniprogram: true,
getCurrentPages: true,
UniApp: true,
UniHelper: true,
},
parserOptions: {
ecmaVersion: 'latest',
},
rules: {
'prettier/prettier': [
'warn',
{
singleQuote: true,
semi: false,
printWidth: 100,
trailingComma: 'all',
endOfLine: 'auto',
},
],
'vue/multi-word-component-names': ['off'],
'vue/no-setup-props-destructure': ['off'],
'vue/no-deprecated-html-element-is': ['off'],
'@typescript-eslint/no-unused-vars': ['off'],
},
}
配置 package.json
{
"script": {
// ... 省略 ...
"lint": "eslint . --ext .vue,.js,.ts --fix --ignore-path .gitignore"
}
}
运行
pnpm lint
Git 工作流规范
安装并初始化 husky
pnpm dlx husky-init
安装 lint-staged
pnpm i lint-staged -D
配置 package.json
{
"script": {
// ... 省略 ...
},
"lint-staged": {
"*.{vue,ts,js}": ["eslint --fix"]
}
}
修改 .husky/pre-commit 文件
npm test // [!code --]
pnpm lint-staged // [!code ++]
完成 husky
+ lint-staged
的配置。
导入uni-ui组件
如果你希望使用 uni-ui 组件,可以根据下面的步骤导入 uni-ui 组件到项目中。
导入组件
pnpm i @dcloudio/uni-ui
配置自动导入组件
easycom 是 小程序 中独有的功能,可以实现组件自动导入
// pages.json
{
// 组件自动导入
"easycom": {
"autoscan": true,
"custom": {
// uni-ui 规则如下配置
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue"
}
},
"pages": [
// …省略
]
}
安装类型声明文件
因为 uni-ui 使用 js 开发,并没有 ts 的类型声名文件,我们可以通过安装第三方的类型声名组件包
pnpm i -D @uni-helper/uni-ui-types
配置类型声明文件
// tsconfig.json
{
"compilerOptions": {
"types": [
"@dcloudio/types",
"@uni-helper/uni-app-types",
"@uni-helper/uni-ui-types"
]
}
}
Pinia 状态管理持久化
安装Pinia状态管理插件
pnpm i pinia
安装Pinia持久化插件
pnpm i pinia-plugin-persistedstate
在小程序中,并没有 localStrage 的概念,但有
// 兼容多端API
uni.setStorageSync()
uni.getStorageSync()
的用法,我们可以使用插件进行自动化持久保存
配置 pinia-plugin-persistedstate 插件
因为小程序端没有 localStrage ,因此在配置自动存储时,需要对自动操作做一些调整
其它配置方式详情可参阅:https://megasu.gitee.io/uni-app-shop-note/rabbit-shop/
{
persist: {
storage: {
getItem(key) {
return uni.getStorageSync(key)
},
setItem(key, value) {
uni.setStorageSync(key, value)
},
},
},
},
设置请求相关拦截器
API拦截器
UniApp 带有拦截器功能,我们可以使用 uni.addInterceptor 对uniapp内的api增加拦截器,实现api方法加工
uni.addInterceptor("被监听的api名",拦截后调用的配置对象)
其中,invoke 方法则是表示在api执行之前,会触发拦截器
/**
* 小程序中设置请求拦截器,用于在小程序发生请求时对请求数据做一些加工
* 1. 增加基础URL
* 2. 修改超时时间
* 3. 增加小程序请求标识
* 4. 增加 token
*/
import { useMemberStore } from '@/stores'
const baseURL = ''
const httpInterceptor = {
// 执行前会被调用
invoke(options: UniApp.RequestOptions) {
// 1.增加基础URL
if (!options.url.startsWith('http')) {
// 如果请求头是以http开头的,说明不需要加基础链接,如果不是,则说明需要增加基础链接
options.url = baseURL + options.url
}
// 2.修改超时时间
options.timeout = 10000
// 3.增加小程序请求标识
options.header = {
...options.header,
'source-client': 'miniapp',
}
// 4.增加 token
const memberStore = useMemberStore()
const token = memberStore.profile?.token
if (token) {
options.header.Authorization = token
}
},
}
// 对两个API增加拦截器,也就是当执行 uni.request 和 uni.uploadFile 时会被先调用拦截器方法
uni.addInterceptor('request', httpInterceptor)
uni.addInterceptor('uploadFile', httpInterceptor)
详情配置可阅读:https://uniapp.dcloud.net.cn/api/interceptor.html#addinterceptor
封装泛型及Promise返回
我们平时在开发网页时,经常会使用到Axios,它的返回通常都是Promise
在小程序开发中,我们也可以把 uni.request 的api封装成类似 Promise 的返回值,并且我们还可以精确的声明请求后返回的数据类型
我们知道,通常我们的返回值格式都是以下这样的:
{
code:200,
msg:"成功"
data: ...
}
可以看到,除了 data 以外,code和msg都是固定的类型,那么我们就可以声明一个带有泛型的接口:
interface ResponseResult<T> {
code: string
msg: string
data: T
}
从上面声明的接口来看,这个接口还会接收一个泛型T,这个T类型将会在以后的请求调用时动态指定,这个类型T将是指定data的类型
接下来我们可以对 uni.request 进行 Promise 封装
Promise 封装
export const http = <T>(options: UniApp.RequestOptions) => {
return new Promise<ResponseResult<T>>((resolve, reject) => {
uni.request({
...options,
// success 的回调,不管是 200 还是 400 还是 500 都会被调用
success(res) {
// 当请求为正常响应时
if (res.statusCode >= 200 && res.statusCode < 300) {
resolve(res.data as ResponseResult<T>)
} else if (res.statusCode == 401) {
// 说明服务器的 token 已过期,要求清理用户信息,并跳到登陆页
const memberStore = useMemberStore()
memberStore.clearProfile()
uni.navigateTo({
url: '/pages/login/login',
})
reject(res)
} else {
// 其它的服务器错误
uni.showToast({
icon: 'error',
title: '服务器暂时开了小差,请稍后再试',
})
reject((res.data as ResponseResult<T>).msg)
}
},
// 对于完全请求不了的,才会走到错误
fail(error) {
uni.showToast({
icon: 'error',
title: '网络错误,请检查网络后再试',
})
reject(error)
},
})
})
}
上面的代码可以看出,调用这个http() 方法的时候,我们必须指定一个类型,即 http<T>(),而这个类型T将决定返回值中的 data 类型。
同时我们也可以使用 async - await 的方式来调用 http() 方法
然后我们再利用Promise封装请求成功,或失败后的返回处理。
注意:因为 res 是 uni.request 中返回的数据,本身带有自带类型,会出现类型错误,我们可以直接断言我们声明的类型
网络请求规范
页面加载时
当一个页面在打开时,通常我们都会预先加载一屏页面内容,小程序中提供了特有的生命周期 onLoad() 方法,当小程序动行加载后,会调用该生命周期方法,可以在其中加入一些网络加载请求工作。
示例:
// 页面加载时解发
onLoad(async () => {
getHomeBannerData()
getHomeCategoryData()
getHotData()
})
一般网络请求规范
前面章节我们讲解了小程序如何封装uni.request请求api,来达到封装Promise的目的,当我们需要向服务器请求数据时,我们应该要另外创建一个文件,用于保存请求url和暴露请求api接口,而不是直接在vue页面中调用网络请求api
示例:
import type { BannerItem, CategoryItem, HotItem } from '@/types/home'
import type { Guess } from '@/types/guess'
import type { PageParams } from '@/types/global'
// 定义一个用于存放请求url的枚举,方便管理
enum HomeApi {
// 首页广告区域接口
getHomeBannerURL = '/home/banner',
...
}
// 导入 uni.request 封装文件
import { http } from '@/utils/http'
// 暴露封装的请求api
// 首页广告区域请求
export const getHomeBannerApi = (distributionSite = 1) =>
http<BannerItem[]>({
method: 'GET',
url: HomeApi.getHomeBannerURL,
data: {
distributionSite,
},
})
...
在vue页面调用时,我们则可以通过引入该api接口文件,调用请求即可:
let bannerList = ref<BannerItem[]>([])
// 获取服务器Banner数据
const getHomeBannerData = async () => {
let res = await getHomeBannerApi()
bannerList.value = res.result
}
网络请求优化
在网络请求中,我们有大量的多个网络请求同时执行的情况,通常我们都会使用 async-await 来获取请求数据,如下示例:
await getHomeBanner()
await getHomeCategory()
await getHomeHot()
await getHomeLike()
...
但是如果使用 await 执行多个请求时,await 的执行是上一行代码执行完后,再去执行下一行代码,这样就会出现单线程请求的情况,效率低下,当多个请求互相不产生数据冲突的情况下,我们没有必要单线程的请求,可以使用 Promise.all 同时执行多个请求,提高效率,如下示例:
await Promise.all([
getHomeBanner(),
getHomeCategory(),
getHomeHot(),
getHomeLike()
...
])
这样的话,所有的请求都会在同时执行,最后一个请求完成后,才会释放,这样能有效的让多个请求同时执行,同时又不会产生跳级执行(即不使用await,不等待直接执行后面的代码)
TypeScript 数据类型规范*
关于数据类型,在ts中可做可不做,做了数据类型可以使项目更加规范。
组件数据类型*
每一个自定义组件,其实都有其类型,但如果我们直接调用,IDE是无法知道我们的组件类型的,我们可以通过声明组件类型:
import XtxSwiper from './XtxSwiper.vue'
import XtxGuess from './XtxGuess.vue'
// 通过声明自定义组件类型,当我们在使用组件时,IDE会清楚我们的组件是什么类型
declare module '@vue/runtime-core' {
export interface GlobalComponent {
XtxSwiper: typeof XtxSwiper
XtxGuess: typeof XtxGuess
}
}
ref子组件引用数据类型*
当我们在父组件通过 ref 标记子组件后,其ref也具有类型,其类型声明如何:
export type 子组件ref类型 = InstanceType<typeof 子组件>
提供 InstanceType 封装类型
事件处理数据类型*
对于组件的自定义事件处理,如 @click 、@change 等等的事件处理,也具有数据类型,通常如果是 uniapp 自带的组件的事件处理,都可以通过 UniHelper 获取,如下案例是Swiper轮播图组件的 @change 事件的数据类型:
<swiper @change="onChange">
</swiper>
// UniHelper 包含了基本的 uni 组件的数据类型
const onChange:UniHelper.SwiperOnChange = (ev) => {
...
}
自定义导航栏
我们平时所看到的小程序导航栏界面,都是小程序默认提供的,但我们可以通过配置,就可以关闭小程序给我们的默认导航栏
"pages": [
//pages数组中第一项表示应用启动页,参考:https://uniapp.dcloud.io/collocation/pages
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页",
"navigationStyle": "custom"
}
},
]
设置安全区
所谓的安全区,就是手机的状态栏到我们的小程序导航栏的距离,因为不同的手机,它的状态栏都不一样,有高有低,这使得小程序在不同的手机上所展示的导航栏位置会有所不同,容易出现重叠的情况,如下图:
为了解决这种还同手机的高度对导栏所处的位置问题,我们可以通过 uni.getSystemInfoSync() 获取不同手机的状态栏高度,即安全高度
const {safeAreaInsets} = uni.getSystemInfoSync()
console.log(safeAreaInsets)
==>
以 px 为单位
{
bottom: 20
left: 0
right: 0
top: 20
}
通过获取安全区域,则可以动态调整自定义导航栏的高度(或padding)
来实现精准布局
页面布局
一个小程序的主要页面布局分别为【导航栏】、【页面内容区】、【菜单栏】三个位置,如下图:
固定导航栏,实现页面内容滚动效果
因为导航栏是我们自定义的,因此,导航栏与页面内容是并列排列的,当我们内容变多时,向下滚动时,会使导航栏一并滚上去
uniapp提供了滚动组件,<scroll-view>,我们可以使用 scroll-view 包裹页面内容,而导航栏我们不使用 scroll-view 包裹,这样就可以使导航栏不出现被滚动上去的情况
scroll-view需要指定高度,否则scroll-view会按照内容大小被无限撑开,只有被设置了固定高度之后,scroll-view才会有滚动效果
设署Flex实现弹性布局
为了解决不同设备屏幕大小的匹配问题,我们不可以设置scroll-view的固定高度,因此我们可以利用Flex布局,把页面中的【导航栏】、【页面内容区】、【菜单栏】三组元素进行弹性布局,这样使得不管什么样的屏幕大小,都可以自动使scroll-view占满屏幕位置
// 设置index主页的整个页面布局为flex
page {
background-color: #f7f7f7;
display: flex;
flex-direction: column; // 使用列的方式排列布局,默认为横
height: 100%; // 使页面占有整个屏幕高度
}
// scroll-view 样式
.scroll {
flex: 1; // 使 scroll-view 占有 flex 中的大小比例
}
这样可使scroll-view动态占有不同屏中的固定位置和固定大小
下拉刷新
scroll-view 提供上拉刷新的功能,我们可以在 scroll-view 上加入属性 refresher-enabled 即可开启上拉刷新功能,而且 scroll-view 还能提供上拉刷新时的图标等功能。
refresher-enabled => 开启下拉刷新功能
@refresherrefresh="onRefresherRefresh" => 下拉刷新功能触发事件
:refresher-triggered="isTriggered" => 是否处理刷新加载状态
:refresher-threshold="45" => 下拉幅度多大触发刷新
refresher-default-style="black" => 设置下拉刷新时的默认样式
refresher-background => 下拉刷新时的背影图片
上拉触底
当我们上拉内容页时,到页尾时,希望触发新的数据请求,scroll-view 提供了触底事件处理:
<scroll-view
scroll-y
// 触底事件处理
@scrolltolower="onScrollToLower"
// 触顶事件处理
@scrolltoupper=""
>
</scroll-view>
骨架屏
所谓的骨架屏,则是刚小程序刚打开时,网络数据请求未完成时,为了给用户一种加载提示,而预先对内容的大致框架展示的一个组件:
骨架屏生成
小程序提供了骨架屏的生成功能,我们可以能过以下操作生成骨架屏:
生成后的骨架屏,会在小程序开发工具上多出wxml和wxss文件:
我们可以利用这两个文件,重新封装成vue组件,使用 v-if 来判断网络请求是否完成,如果未完成,则显示该骨架屏,否则显示真正的数据页面。
<template>
<!-- 导航栏 -->
<CustomNavBar />
<!-- 中间内容页面滚动框 -->
<scroll-view
class="scroll"
scroll-y
@scrolltolower="onScrollToLower"
refresher-enabled
@refresherrefresh="onRefresherRefresh"
:refresher-triggered="isTriggered"
>
<!-- 骨架屏 -->
<PageSkeleton v-if="isLoading"></PageSkeleton>
<!-- 中间内容页面滚动框 -->
<template v-else>
真正的内容区
</template>
</scroll-view>
</template>
小程序分包设置
当一个小程序的体积变得比较大时,我们可以对小程序中可能不会常用到的功能分到另一个包中
详情配置规则可查看:https://uniapp.dcloud.net.cn/collocation/pages.html
分包规则
通常我们可以把分包的页面存放在任意地方,但是尽量不要把分包页面存放在pages文件夹中,这样容易与非分包页面产生混乱,我们可以另外新建一个文件夹,用于保存分包页面文件
比如一个小程序,【设置】功能页面可能比较少人,那么我们就可以把【设置】的页面作为分包处理。
// pages.json 配置分包页面,这些页面不会在小程序启动时马上下载
"subPackages":[
// 子包一
{
"root":"memberPages",
"pages": [
{
"path": "settings/settings",
"style": {
"navigationBarTitleText": "设置",
}
}
],
},
// 子包二
// {...}
],
分包预下载
我们可以不设置分包预下载处理,但当我们的分包内容比较大时,在打开时才会下载分包,这样会使用户体验感不太好,因此我们可以设置,当打开什么页面时,系统自动预下载分包内容,这样就可以快速打开分包内容了
设置规则可在pages.json 中定义 preloadRule 属性
// 配置分包页面预下载规则
"preloadRule":{
// 进入 my 页面后,则自动启动预下载分包操作
"pages/my/my":{
// 下载分包名称
"packages":["memberPages"],
// 下载分包时的网络条件,all与wifi
"network":"all"
}
}
共有 0 条评论