危险

为之则易,不为则难

0%

01_小兔鲜

✔ 能够通过 HBuilderX 和命令行创建 uni-app 项目。

✔ 能够配置和使用 uni-ui 库。

✔ 掌握 http 请求函数的封装。

项目架构

项目架构图

创建 uni-app 项目

uni-app 支持两种方式创建项目,分别如下。

  1. 通过 HBuilderX 创建。

  2. 通过命令行创建(更推荐)。

通过 HBuilderX 创建

  1. 下载安装 HbuilderX 编辑器。

下载安装 HbuilderX 编辑器

  1. 通过 HbuilderX 创建 uni-app vue3 项目。

通过 HbuilderX 创建 uni-app vue3 项目

  1. 安装 uni-app vue3 编译器插件。

安装 uni-app vue3 编译器插件

  1. 编译成微信小程序端代码。

编译成微信小程序端代码

  1. 开启服务端口。

开启服务端口

😀 小技巧分享:模拟器窗口分离和置顶。

模拟器窗口分离和置顶

Hbuildex 和 微信开发者工具 关系

Hbuildex 和 微信开发者工具 关系

温馨提示:Hbuildexuni-app 都属于 DCloud 公司的产品。

pages.json 和 tabBar 案例

目录结构

我们先来认识 uni-app 项目的目录结构。

1
2
3
4
5
6
7
8
9
10
11
├─pages            业务页面文件存放的目录
│ └─index
│ └─index.vue index页面
├─static 存放应用引用的本地静态资源的目录(注意:静态资源只能存放于此)
├─unpackage 非工程代码,一般存放运行或发行的编译结果
├─index.html H5端页面
├─main.js Vue初始化入口文件
├─App.vue 配置App全局样式、监听应用生命周期
├─pages.json 配置页面路由、导航栏、tabBar等页面类信息
├─manifest.json 配置appid、应用名称、logo、版本等打包信息
└─uni.scss uni-app内置的常用样式变量

解读 pages.json

用于配置页面路由、导航栏、tabBar 等页面类信息。

image-20230504110053624

案例练习

案例练习

pages.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
{
// 页面路由
"pages": [
{
"path": "pages/index/index",
// 页面样式配置
"style": {
"navigationBarTitleText": "首页"
}
},
{
"path": "pages/my/my",
"style": {
"navigationBarTitleText": "我的"
}
}
],
// 全局样式配置
"globalStyle": {
"navigationBarTextStyle": "white",
"navigationBarTitleText": "uni-app",
"navigationBarBackgroundColor": "#27BA9B",
"backgroundColor": "#F8F8F8"
},
// tabBar 配置
"tabBar": {
"selectedColor": "#27BA9B",
"list": [
{
"pagePath": "pages/index/index",
"text": "首页",
"iconPath": "static/tabs/home_default.png",
"selectedIconPath": "static/tabs/home_selected.png"
},
{
"pagePath": "pages/my/my",
"text": "我的",
"iconPath": "static/tabs/user_default.png",
"selectedIconPath": "static/tabs/user_selected.png"
}
]
}
}

uni-app 和原生小程序区别

image-20230504110125720

主要区别

uni-app 项目每个页面是一个 .vue 文件,数据绑定及事件处理同 Vue.js 规范。

  1. 属性绑定 src="{ { url }}" 升级成 :src="url"

  2. 事件绑定 bindtap="eventName" 升级成 @tap="eventName"支持()传参

  3. 支持 Vue 常用指令 v-forv-ifv-showv-model 等。

其他区别补充

  1. 调用接口能力,建议前缀 wx 替换为 uni ,养成好习惯,这样支持多端开发。

  2. <style></style> 样式不需要写 scoped

案例练习

主要功能:滑动轮播图;点击大图预览。

案例练习

pages/index/index.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<template>
<swiper class="banner" indicator-dots circular :autoplay="false">
<swiper-item v-for="item in pictures" :key="item.id">
<image @tap="onPreviewImage(item.url)" :src="item.url"></image>
</swiper-item>
</swiper>
</template>

<script>
export default {
data() {
return {
// 轮播图数据
pictures: [
{
id: '1',
url: 'https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/uploads/goods_preview_1.jpg',
},
{
id: '2',
url: 'https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/uploads/goods_preview_2.jpg',
},
{
id: '3',
url: 'https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/uploads/goods_preview_3.jpg',
},
{
id: '4',
url: 'https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/uploads/goods_preview_4.jpg',
},
{
id: '5',
url: 'https://pcapi-xiaotuxian-front-devtest.itheima.net/miniapp/uploads/goods_preview_5.jpg',
},
],
}
},
methods: {
onPreviewImage(url) {
// 大图预览
uni.previewImage({
urls: this.pictures.map((v) => v.url),
current: url,
})
},
},
}
</script>

<style>
.banner,
.banner image {
width: 750rpx;
height: 750rpx;
}
</style>

用命令行创建 uni-app 项目

image-20230504102640100

优势:通过命令行创建 uni-app 项目,不必依赖 HBuilderX,TypeScript 类型支持友好。

下面是 vue3 + ts 版,创建其他版本可查看:uni-app 官网

1
npx degit dcloudio/uni-preset-vue#vite-ts 项目名称
  1. 安装依赖 pnpm install

  2. 编译成微信小程序 pnpm dev:mp-weixin

  3. 导入微信开发者工具。

😚 温馨提示:在 manifest.json 文件添加小程序 appid 方便真机预览。

VSCode 开发 uni-app 项目

为什么选择 VSCode

  • 👍 VSCode 对 TS 类型支持友好,前端开发者熟悉的编辑器

  • 👀 HbuilderX 对 TS 类型支持暂不完善,期待官方完善。

image-20230504110206484

用 VSCode 开发配置

image-20230504110348681

  • 安装 uni-app 插件。

    • uni-create-view:快速创建 uni-app 页面。

    • uni-helper:代码提示。

    • uniapp 小程序扩展:鼠标悬停查文档。

  • TS 类型校验。

    • 安装类型声明文件 pnpm i -D @types/wechat-miniprogram @uni-helper/uni-app-types

    • 配置 tsconfig.json 中的 compilerOptions.types 选项。

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"extends": "@vue/tsconfig/tsconfig.json",
"compilerOptions": {
"sourceMap": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"lib": ["esnext", "dom"],
"types": ["@dcloudio/types", "@types/wechat-miniprogram", "@uni-helper/uni-app-types"]
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}
  • JSON 注释问题,搜索 Associations

    • 设置文件关联,把 manifest.jsonpages.json 设置为 jsonc

开发工具回顾

选择自己习惯的编辑器开发 uni-app 项目即可。

VSCode 和 微信开发者工具 关系。

VSCode 和 微信开发者工具 关系

HbuilderX 和 微信开发者工具 关系。

HbuilderX 和 微信开发者工具 关系

用 VSCode 开发课后练习

使用 VSCode 编辑器写代码,实现 tabBar 案例 + 轮播图案例。

温馨提示:VSCode 可通过快捷键 Ctrl + i 唤起代码提示。

拉取项目模板代码

项目模板包含:目录结构,项目素材,代码风格。

1
git clone http://git.itcast.cn/heimaqianduan/erabbit-uni-app-vue3-ts.git heima-shop

😚 提示:在 manifest.json 中添加微信小程序的 appid

引入 uni-ui 组件库

image-20230504110440804

  1. 安装 uni-ui 组件库
1
2
pnpm i @dcloudio/uni-ui
pnpm i sass -D
  1. 配置自动导入组件,pages.json
1
2
3
4
5
6
7
8
9
10
11
{
// ...
// 组件自动导入
"easycom": {
"autoscan": true,
"custom": {
// uni-ui 规则如下配置
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue"
}
},
}
  1. 测试,pages/index/index.vue
1
2
3
<uni-card title="基础卡片" sub-title="副标题" extra="额外信息" thumbnail="https://web-assets.dcloud.net.cn/unidoc/zh/unicloudlogo.png">
<text>这是一个带头像和双标题的基础卡片,此示例展示了一个完整的卡片。</text>
</uni-card>
  1. 鼠标悬停到 uni-card 显示 any,安装类型声明文件并配置。
1
pnpm i -D @uni-helper/uni-ui-types

tsconfig.json,在 types 中添加最后一个配置项。

1
2
3
4
5
6
7
{
// ...
"compilerOptions": {
// ...
"types": ["@dcloudio/types", "@types/wechat-miniprogram", "@uni-helper/uni-app-types", "@uni-helper/uni-ui-types"]
},
}

小程序 Pinia 持久化

说明:项目中 Pinia 用法平时完全一致,主要解决持久化插件兼容性问题。

image-20230504111029953

  1. 持久化存储插件: pinia-plugin-persistedstate
1
pnpm i pinia-plugin-persistedstate

stores/index.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
import { createPinia } from 'pinia'
// #1
import persist from 'pinia-plugin-persistedstate'

// 创建pinia实例
const pinia = createPinia()
// #2
pinia.use(persist)

// 导出pinia实例,给main使用
export default pinia
// #3
export * from './modules/member'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script setup lang="ts">
import { useMemberStore } from '@/stores'
const memberStore = useMemberStore()
</script>

<template>
<view class="my">
<view class="my">
<view>会员信息:{{ memberStore.profile }}</view>
<button @tap="memberStore.setProfile({ nickname: '黑马先锋' })" size="mini" plain type="primary">保存用户信息</button>
<button @tap="memberStore.clearProfile()" size="mini" plain type="warn">清理用户信息</button>
</view>
</view>
</template>

<style lang="scss">
//
</style>
  1. 插件默认使用 localStorage 实现持久化,小程序端不兼容,需要替换持久化 API,store/modules/member.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
export const useMemberStore = defineStore(
'member',
() => {
// ...
},
{
// 配置持久化
persist: {
// 调整为兼容多端的API
storage: {
setItem(key, value) {
uni.setStorageSync(key, value)
},
getItem(key) {
return uni.getStorageSync(key)
},
},
},
},
)
  1. 我的 .vscode/settings.json 参考。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"eslint.enable": true,
"eslint.run": "onType",
"eslint.options": {
"extensions": [
".js",
".vue",
".jsx",
".tsx"
]
},
"editor.codeActionsOnSave": {
"source.fixAll.eslint": true
},
// 配置语言的文件关联
"files.associations": {
"pages.json": "jsonc", // pages.json 可以写注释
"manifest.json": "jsonc" // manifest.json 可以写注释
}
}

uni.request 请求封装

image-20230504111254443

image-20230504112007172

添加请求和上传文件拦截器

  1. uni-app 拦截器:uni.addInterceptor

  2. 接口说明:接口文档

  3. 参考代码如下,src/utils/http.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { useMemberStore } from '@/stores'
const baseURL = 'https://pcapi-xiaotuxian-front-devtest.itheima.net'

const httpInterceptor = {
// 拦截前触发
invoke(options: UniApp.RequestOptions) {
// 1. 非 http 开头需拼接地址
if (!options.url.startsWith('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
}
},
}

// 拦截 request 请求
uni.addInterceptor('request', httpInterceptor)
// 拦截 uploadFile 文件上传
uni.addInterceptor('uploadFile', httpInterceptor)
  1. 测试,pages/my/my.vue
1
2
3
4
5
6
7
8
9
10
11
12
import { useMemberStore } from '@/stores'
import '@/utils/http'
const memberStore = useMemberStore()

const getData = async () => {
const r = await uni.request({
method: 'GET',
url: '/home/banner'
})
console.log(r)
}
getData()

封装 Promise 请求函数

image-20230504112148210

  1. 封装基础的 http 函数,返回 Promise 对象,utils/http.ts
1
2
3
4
5
6
7
8
9
10
11
export const http = (options: UniApp.RequestOptions) => {
return new Promise((resolve, reject) => {
uni.request({
...options,
success(res) {
// 成功,resolve 并提取数据
resolve(res.data)
}
})
})
}

测试,pages/my/my.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { useMemberStore } from '@/stores'
// #1
import { http } from '@/utils/http'
const memberStore = useMemberStore()

const getData = async () => {
// #2
const r = await http({
method: 'GET',
url: '/home/banner'
})
console.log(r)
}
getData()
  1. 给 http 函数添加泛型。

image-20230504113140112

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
interface Data<T> {
code: string
msg: string
result: T
}
export const http = <T>(options: UniApp.RequestOptions) => {
return new Promise<Data<T>>((resolve, reject) => {
uni.request({
...options,
success(res) {
resolve(res.data as Data<T>)
}
})
})
}

测试,pages/my/my.vue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { useMemberStore } from '@/stores'
import { http } from '@/utils/http'
const memberStore = useMemberStore()

// #1
interface IBannerItem {
hrefUrl: string
id: string
imgUrl: string
type: string
}

const getData = async () => {
// #2
const r = await http<IBannerItem[]>({
method: 'GET',
url: '/home/banner'
})
console.log(r.result[0].hrefUrl)
}
getData()
  1. 处理失败的情况。

image-20230504113912491

image-20230504114702424

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
/**
* 请求函数
* @param UniApp.RequestOptions
* @returns Promise
* 1. 返回 Promise 对象
* 2. 获取数据成功
* 2.1 提取核心数据 res.data
* 2.2 添加类型,支持泛型
* 3. 获取数据失败
* 3.1 401错误 -> 清理用户信息,跳转到登录页
* 3.2 其他错误 -> 根据后端错误信息轻提示
* 3.3 网络错误 -> 提示用户换网络
*/
type Data<T> = {
code: string
msg: string
result: T
}
// 2.2 添加类型,支持泛型
export const http = <T>(options: UniApp.RequestOptions) => {
// 1. 返回 Promise 对象
return new Promise<Data<T>>((resolve, reject) => {
uni.request({
...options,
// 响应成功
success(res) {
// 状态码 2xx, axios 就是这样设计的
if (res.statusCode >= 200 && res.statusCode < 300) {
// 2.1 提取核心数据 res.data
resolve(res.data as Data<T>)
} else if (res.statusCode === 401) {
// 401错误 -> 清理用户信息,跳转到登录页
const memberStore = useMemberStore()
memberStore.clearProfile()
uni.navigateTo({ url: '/pages/login/login' })
reject(res)
} else {
// 其他错误 -> 根据后端错误信息轻提示
uni.showToast({
icon: 'none',
title: (res.data as Data<T>).msg || '请求错误',
})
reject(res)
}
},
// 响应失败
fail(err) {
// 测试:修改 options.timeout = 1000,并切换 network 为 Slow 3G
uni.showToast({
icon: 'none',
title: '网络错误,换个网络试试',
})
reject(err)
},
})
})
}
  1. 测试 401,pages/my/my.vue
1
2
3
<button @tap="getProfile" size="mini" plain type="primary">
获取个人信息
</button>
1
2
3
4
5
6
7
8
const getProfile = async () => {
const r = await http({
method: 'GET',
url: '/member/profile',
header: {}
})
console.log(r)
}

其他错误测试。

1
2
3
4
5
6
7
8
9
const getProfile = async () => {
const r = await http({
method: 'GET',
// 这儿把 url 清空了
url: '',
header: {}
})
console.log(r)
}

【拓展】了解代码规范

为什么需要代码规范?

如果没有统一代码风格,团队协作不便于查看代码提交时所做的修改。

diff

统一代码风格

  • 安装 eslint + prettier
1
pnpm i -D eslint prettier eslint-plugin-vue @vue/eslint-config-prettier @vue/eslint-config-typescript @rushstack/eslint-patch @vue/tsconfig
  • 新建 .eslintrc.cjs 文件,添加以下 eslint 配置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/* 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
1
2
3
4
5
6
{
"script": {
// ... 省略 ...
"lint": "eslint . --ext .vue,.js,.ts --fix --ignore-path .gitignore"
}
}
  • 运行。
1
pnpm lint

到此,你已完成 eslint + prettier 的配置。

Git 工作流规范

  • 安装并初始化 husky
1
pnpm dlx husky-init
  • 安装 lint-staged
1
pnpm i lint-staged -D
  • 配置 package.json
1
2
3
4
5
6
7
8
{
"script": {
// ... 省略 ...
},
"lint-staged": {
"*.{vue,ts,js}": ["eslint --fix"]
}
}
  • 修改 .husky/pre-commit 文件。
1
2
npm test   // [!code --]
pnpm lint-staged // [!code ++]

到此,你已完成 husky + lint-staged 的配置。