小兔鲜儿_第一周综合笔记
小兔鲜儿 UniApp 项目 - 第一周综合笔记
本笔记结合老师官方笔记重点理论 + 实践中遇到的疑问与解答,适合复习和查阅。
一、开发环境搭建
1.1 UniApp 项目创建方式
企业中最常见的三种方式:
| 方式 | 适用场景 |
|---|---|
| HBuilderX 可视化创建 | 快速原型、小团队 |
| Vue CLI 脚手架 | 中大型项目、需要工程化 |
| Vite 脚手架(官方推荐) | 新项目、追求构建速度 |
# 当前企业最推荐方式 npm create uni-app@latest1.2 HBuilderX 安装
- Windows:下载正式版
.zip - Mac Intel(2020年前):下载正式版
.dmg - Mac M1/M2/M3:下载 Mac Arm 正式版
- 正式版 vs Alpha版:日常开发选正式版,Alpha版有新功能但不稳定
1.3 开发工作流(重要)
用 VSCode 写代码时,必须先启动编译命令,否则微信开发者工具看到的是旧代码。
# 只需执行一次,保持运行 pnpm dev:mp-weixin完整流程:
1. VSCode 打开项目 2. 终端执行 pnpm dev:mp-weixin(保持运行,不要关闭) 3. 微信开发者工具打开 dist/dev/mp-weixin 目录 4. 在 VSCode 写代码、保存 5. 自动编译,微信开发者工具自动刷新实践疑问:为什么需要这个命令?因为写的是 UniApp + Vue3 代码,微信开发者工具看不懂.vue文件,需要先编译成微信小程序认识的.wxml、.js、.wxss格式。pnpm dev:mp-weixin是一个实时编译器,一直在后台把 Vue 代码翻译成小程序代码。
1.4 常用包管理器 pnpm
pnpm 比 npm 速度更快、占用磁盘空间更少,很多新项目使用它。
# 安装 pnpm(如未安装) npm install -g pnpm二、项目配置
2.1 引入 uni-ui 组件库
pnpm i @dcloudio/uni-ui配置自动导入:
// pages.json { "easycom": { "autoscan": true, "custom": { "^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue", "^Xtx(.*)": "@/components/Xtx$1.vue" } } }安装类型声明文件:
pnpm i -D @uni-helper/uni-app-types@latest @uni-helper/uni-ui-types@latest2.2 tsconfig.json 配置
{ "compilerOptions": { "types": [ "@dcloudio/types", "miniprogram-api-typings", "@uni-helper/uni-app-types", "@uni-helper/uni-ui-types" ] }, "vueCompilerOptions": { "plugins": ["@uni-helper/uni-app-types/volar-plugin"] } }实践疑问:vueCompilerOptions飘红提示"未知编译器选项",这是 VSCode 对非标准 tsconfig 字段的误报,不影响实际运行,忽略即可。确保安装了 Vue - Official 插件,并禁用旧版 Vetur 插件。
2.3 全局组件类型声明
// src/types/components.d.ts import XtxSwiper from '@/components/XtxSwiper.vue' import XtxGuess from '@/components/XtxGuess.vue' declare module 'vue' { export interface GlobalComponents { XtxSwiper: typeof XtxSwiper XtxGuess: typeof XtxGuess } } // 组件实例类型 export type XtxGuessInstance = InstanceType<typeof XtxGuess>实践疑问:配置全局自动导入后组件能正常运行,但编辑器类型提示不完整。原因是自动导入(编译时)和编辑器类型检查(实时)是独立的两套机制,需要在components.d.ts中手动声明类型才能获得完整提示。
两种类型的区别:
- 组件类型(GlobalComponents 里的):影响模板里使用组件标签时的提示,悬停能看到组件有哪些 props
- 组件实例类型(XtxGuessInstance):影响通过 ref 拿到实例后调用方法时的提示,能看到 defineExpose 暴露的方法和属性
2.4 Git 版本控制配置
.gitignore 推荐配置:
logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* pnpm-debug.log* node_modules dist .DS_Store *.local # 微信开发者工具私有配置(含个人 AppID,不应提交) project.private.config.json注意:project.config.json是公共配置可以提交,project.private.config.json含个人信息需要忽略。
拉取模板并推送到自己仓库:
# 克隆老师模板到当前目录 git clone 老师的仓库地址 . # 换成自己的远程仓库 git remote set-url origin 你的gitee仓库地址 # 改分支名 git branch -m master main # 推送 git push -u origin main三、Pinia 状态管理
3.1 基本概念
Pinia 是 Vue3 的状态管理库,把多个组件都需要用到的数据集中存放统一管理。
| 概念 | 说明 | 类比 |
|---|---|---|
state | 存放数据 | data |
getters | 计算数据 | computed |
actions | 修改数据/请求接口 | methods |
3.2 定义 Store
// stores/modules/member.ts import { defineStore } from 'pinia' import { ref } from 'vue' export const useMemberStore = defineStore( 'member', () => { const profile = ref<any>() const setProfile = (val: any) => { profile.value = val } const clearProfile = () => { profile.value = undefined } return { profile, setProfile, clearProfile } }, { persist: { // 小程序端需替换为兼容多端的 API storage: { setItem(key, value) { uni.setStorageSync(key, value) }, getItem(key) { return uni.getStorageSync(key) }, }, }, }, )实践疑问:为什么要替换持久化 API?因为默认的localStorage在小程序端不兼容,需要换成uni.setStorageSync/uni.getStorageSync来兼容多端。
四、网络请求封装
4.1 拦截器
拦截器在请求发出前或响应回来后自动做统一处理。
发请求 → [请求拦截器] → 服务器 服务器 → [响应拦截器] → 拿到数据请求拦截器能做什么:拼接 baseURL、设置超时、添加请求头、添加 token
响应拦截器能做什么:判断状态码、401 跳登录、提示错误、隐藏 loading
4.2 完整封装代码
// src/utils/http.ts 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 = 60000 // 3. 添加小程序端请求头标识 options.header = { 'source-client': 'miniapp', ...options.header, } // 4. 添加 token const memberStore = useMemberStore() const token = memberStore.profile?.token if (token) { options.header.Authorization = token } }, } uni.addInterceptor('request', httpInterceptor) uni.addInterceptor('uploadFile', httpInterceptor) type 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) { if (res.statusCode >= 200 && res.statusCode < 300) { resolve(res.data as Data<T>) } else if (res.statusCode === 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) { uni.showToast({ icon: 'none', title: '网络错误,换个网络试试' }) reject(err) }, }) }) }4.3 常见 HTTP 状态码
| 状态码 | 含义 | 场景 |
|---|---|---|
| 400 | 请求参数错误 | 传的参数格式不对 |
| 401 | 未授权 | 没登录或 token 过期 |
| 403 | 禁止访问 | 没有权限 |
| 404 | 找不到资源 | 接口地址不存在 |
| 500 | 服务器内部错误 | 后端代码报错 |
| 502 | 网关错误 | 服务器挂了或重启中 |
规律:4xx 是客户端的问题(请求有误、没权限),5xx 是服务器的问题(后端崩了)。
实践疑问:404 为什么还能有返回数据?uni.request的success回调只要服务器有响应就触发,不管状态码是什么。fail只在网络错误(超时、断网)时触发。所以需要手动在success里判断状态码。
实践疑问:source-client: miniapp是自定义请求头,告诉服务器这个请求来自小程序端,服务器可根据此字段返回适合小程序的数据。
五、TypeScript 关键知识点
5.1 import type
TS 里导入类型需要加type关键字:
// 导入值(函数、变量)→ 普通 import import { http } from '@/utils/http' import { ref, onMounted } from 'vue' // 导入类型(type、interface)→ 加 type import type { PageResult } from '@/types/global' import type { BannerItem } from '@/types/home'原因:类型只在编译阶段存在,编译成 JS 后会被完全删掉。import type明确告诉编译器这是纯类型导入,不打包进去。
5.2 泛型<T>
让函数在调用时可以指定返回数据的类型:
// 定义时加 <T> export const http = <T>(options: UniApp.RequestOptions) => { return new Promise<Data<T>>(...) } // 调用时传入具体类型 const res = await http<BannerItem[]>({ url: '/home/banner' }) // res.result 就是 BannerItem[] 类型,有完整提示5.3 非空断言!vs 可选链?.
// 非空断言:强硬告诉 TS 有值,如果实际没值运行时报错 activeIndex.value = ev.detail.current! // 可选链 + 默认值:更安全的写法(推荐) activeIndex.value = ev.detail?.current || 05.4 有无花括号的导入
// 默认导出(export default)→ 不需要花括号,名字可自定义 import XtxSwiper from '@/components/XtxSwiper.vue' // 命名导出(export const/type)→ 需要花括号,名字必须一致 import { ref, computed } from 'vue' import { http } from '@/utils/http'5.5 常用工具类型
| 工具类型 | 作用 | 场景 |
|---|---|---|
Required<T> | 把所有可选字段变成必填 | 组件内部分页参数,保证字段一定有值 |
Partial<T> | 把所有必填字段变成可选 | 接口传参时不想全部传 |
实践疑问:PageParams字段用?标记为可选,是为了方便外部调用接口时灵活传参。组件内部的分页变量用Required<PageParams>,是保证自己维护的分页状态一定有确定的值,防止页码累加时出现undefined++的问题。两个用途不一样。
5.6 属性简写(ES6)
// 完整写法 { data: data } // 简写(属性名和变量名一样时) { data }实践疑问:接口函数里写了data: ''导致参数传不进去。正确写法是直接写data,这是 ES6 的属性简写,等同于data: data。
六、Vue3 核心知识点
6.1 ref 响应式数据
// 定义响应式数据 const bannerList = ref<BannerItem[]>([]) // 泛型声明类型,初始值为空数组 // JS 中修改需要 .value bannerList.value = res.result // 模板中不需要 .value // <xtx-swiper :list="bannerList" />原生小程序对比:
// 原生:用 setData 修改 this.setData({ bannerList: [...] }) // Vue3:直接赋值,页面自动更新 bannerList.value = [...]ref 有两个用途:
// 用途1:响应式数据 const bannerList = ref<BannerItem[]>([]) // 用途2:获取组件/DOM实例 const guessRef = ref<XtxGuessInstance>() guessRef.value?.getMore()6.2 defineProps 组件类型声明
// 子组件中声明接收的 props(必须写,否则没有类型提示) defineProps<{ list: BannerItem[] }>()defineProps永远写在子组件里,用来接收父组件传来的数据。加了类型后,父组件传错类型会报错提醒。
6.3 页面生命周期钩子
UniApp 的页面钩子需要从@dcloudio/uni-app导入,不同于原生小程序的自动注入:
import { onLoad, onShow } from '@dcloudio/uni-app' onLoad(() => { getHomeBannerData() })| 来源 | 钩子 |
|---|---|
vue | onMounted、onUnmounted等 |
@dcloudio/uni-app | onLoad、onShow、onHide、onReady等 |
实践疑问:原生小程序不需要导入 onLoad,因为原生是配置式写法,钩子是对象的属性,框架自动识别。Vue3 是函数式写法,钩子是函数,需要先导入再调用。
6.4 defineExpose 暴露方法
子组件想让父组件调用自己的方法,需要用defineExpose暴露:
// 子组件暴露方法 defineExpose({ resetData, getMore: getHomeGoodsGuessLikeData, }) // 父组件获取实例并调用 const guessRef = ref<XtxGuessInstance>() guessRef.value?.getMore()父组件调用子组件方法的完整流程:
子组件 defineExpose 暴露方法 ↓ 父组件模板 ref="guessRef" 拿到子组件实例 ↓ guessRef.value 就是子组件实例 ↓ guessRef.value.getMore() 调用子组件方法6.5 动态绑定:的区别
<!-- 不加 :,传的是字符串 "false" --> <swiper autoplay="false"> <!-- 加 :,传的是布尔值 false --> <swiper :autoplay="false">规律:凡是要传数字、布尔值、变量、表达式,都要加:,传普通字符串才不加。
6.6 Promise.all 并发请求优化
// 串行写法(慢):三个请求一个接一个执行 onLoad(async () => { await getHomeBannerData() await getHomeCategoryData() await getHomeHotData() }) // 并发写法(快):三个请求同时发出 onLoad(async () => { await Promise.all([ getHomeBannerData(), getHomeCategoryData(), getHomeHotData(), ]) })适用场景:多个请求之间没有依赖关系时,用Promise.all并发请求可以显著提升页面加载速度。Promise.all会等所有请求都完成后才继续执行,任何一个失败则全部失败。
七、组件通信设计思路
| 模式 | 适用场景 | 例子 |
|---|---|---|
| 父传子(props) | 展示型组件,数据简单 | 导航栏、轮播图 |
| 组件内部自己请求 | 功能型组件,有独立业务逻辑、多处复用 | 猜你喜欢、评论列表 |
核心思想:数据和逻辑放在最合适的地方,哪里用哪里管,减少不必要的耦合。
八、首页模块各组件梳理
8.1 自定义导航栏(CustomNavbar)
- 属于首页的业务组件,存放在
pages/index/components/ - 需要在
pages.json中隐藏默认导航栏 - 通过
uni.getSystemInfoSync()获取安全区域适配不同机型
// pages.json { "path": "pages/index/index", "style": { "navigationStyle": "custom" } }8.2 轮播图组件(XtxSwiper)
- 通用组件,存放在
src/components/,首页和分类页都用 - 通过
defineProps接收list数据 - 使用
UniHelper.SwiperOnChange类型处理 swiper change 事件
8.3 热门推荐(HotPanel)
- 首页业务组件,存放在
pages/index/components/ - 父组件请求数据,通过
props传给子组件展示
8.4 猜你喜欢(XtxGuess)重难点
- 通用组件,存放在
src/components/,多页面复用 - 组件内部自己请求数据(而非父传子)
- 实现触底分页加载
- 通过
defineExpose暴露resetData和getMore方法给父组件调用
分页核心逻辑:
const pageParams: Required<PageParams> = { page: 1, pageSize: 10 } const guessList = ref<GuessItem[]>([]) const finish = ref(false) const getHomeGoodsGuessLikeData = async () => { if (finish.value === true) { return uni.showToast({ icon: 'none', title: '没有更多数据~' }) } const res = await getHomeGoodsGuessLikeAPI(pageParams) guessList.value.push(...res.result.items) // 数组追加,不是替换 if (pageParams.page < res.result.pages) { pageParams.page++ } else { finish.value = true } }8.5 骨架屏(PageSkeleton)
骨架屏在数据加载期间显示占位内容,提升用户体验。控制逻辑:
const isLoading = ref(false) onLoad(async () => { isLoading.value = true // 开始加载,显示骨架屏 await Promise.all([...]) isLoading.value = false // 加载完成,隐藏骨架屏 })<PageSkeleton v-if="isLoading" /> <template v-else> <!-- 真实内容 --> </template>实践疑问:本地开发网速快,骨架屏一闪而过看起来像空白。在微信开发者工具 Network 面板把网速调成 Slow 3G 或 2G 就能看到效果,代码本身没有问题。
8.6 下拉刷新
<scroll-view refresher-enabled @refresherrefresh="onRefresherrefresh" :refresher-triggered="isTriggered" scroll-y >const isTriggered = ref(false) const onRefresherrefresh = async () => { isTriggered.value = true // 开启动画 guessRef.value?.resetData() // 重置猜你喜欢数据 await Promise.all([ getHomeBannerData(), getHomeCategoryData(), getHomeHotData(), guessRef.value?.getMore(), ]) isTriggered.value = false // 关闭动画 }九、scroll-view 使用注意事项
scroll-view 不能滚动时排查以下三点:
1. 有没有加 scroll-y 属性?(没有这个属性默认不能纵向滚动) 2. 有没有固定高度?(高度自动撑开时不会触底) 3. 类名有没有写对?正确配置:
<scroll-view scroll-y class="scroll-view"> <style> page { height: 100%; display: flex; flex-direction: column; } .scroll-view { flex: 1; } </style>十、文件目录规范
src/ ├── components/ # 通用全局组件(多页面复用) ├── pages/ │ └── index/ │ └── components/ # 首页业务组件(仅首页使用) ├── services/ # 接口请求函数(按模块拆分) │ └── home.ts ├── stores/ # Pinia 状态管理 │ └── modules/ │ └── member.ts ├── types/ # TypeScript 类型声明 │ ├── global.d.ts # 通用类型(PageResult、PageParams) │ ├── home.d.ts # 首页相关类型 │ └── components.d.ts # 全局组件类型声明 └── utils/ # 工具函数 └── http.ts # 请求封装(拦截器等基础配置)utils/放工具函数,services/放接口请求函数,两者职责不同。http.ts放在utils/是因为它是基础工具性质的封装。
十一、常见报错速查
| 报错信息 | 原因 | 解决方法 |
|---|---|---|
pnpm 不是内部命令 | 未安装 pnpm | npm install -g pnpm |
找不到名称 onLoad | 未导入 | import { onLoad } from '@dcloudio/uni-app' |
import type报错 | 类型用普通 import | 改为import type { xxx } |
应有 1 个参数但获得 0 个 | 调用函数时漏传参数 | 按函数定义补充参数 |
不能将类型分配给类型 | Props 类型字段缺失或写成了字符串 | 检查defineProps中类型字段,去掉引号 |
Error: timeout | 请求超时 | 增大timeout值,检查域名校验设置 |
touristappid报错 | AppID 未配置 | 在manifest.json填入真实 AppID |
| 修改代码不生效 | 编译命令未运行 | 执行pnpm dev:mp-weixin |
| 页面不能滚动 | 缺少 scroll-y 或高度未设置 | 加上 scroll-y,给 scroll-view 设置高度 |
[object Object]报错 | 对象被当成字符串拼接 | console.log 用逗号分隔而不是加号 |
| 请求参数没有传到接口 | 接口函数里忘记写 data 字段 | 在 http 调用里加上data |
学习建议:跟课阶段不要纠结每个配置的具体含义,先把流程跑通。等做完整个项目再回头理解配置细节,性价比更高。
