# uni-app
> 基于 `vue` 语法的 `全端开发框架`
官网: https://uniapp.dcloud.io/
## 创建项目
https://uniapp.dcloud.net.cn/quickstart-cli.html
vue 3:
1. 创建项目
```sh
npx degit 'dcloudio/uni-preset-vue#vite-ts' uni-shop # zsh中#不用引号包起来会报错
# 直接下载模板的话在 https://github.com/dcloudio/uni-preset-vue/tree/vite-ts
pnpn i
pnpm add -D sass
```
2. 运行项目 `pnpm run dev:mp-weixin`
3. 使用微信小程序开发者工具来导入和预览项目 `dist/dev/mp-weixin`
==注意: 只能修改uni-app项目的文件, 不能修改编译后的小程序文件==
旧项目更新编译器主要依赖:
```sh
npx @dcloudio/uvm@latest
```
## 小程序的注意项
- 标签: div 和 ul 和 li 等改为 `view`、span 改为 `text`、a 改为 `navigator`、img 改为 `image`
- 不能使用浏览器对象: 比如document、xmlhttp、cookie、window、location、navigator、localstorage、websql、indexdb、webgl等对象
- style 不需要设置 soped, 多页面样式互相隔离
- css 不能使用 \* 选择器, body的元素选择器请改为page
- [微信小程序当前bug列表](https://developers.weixin.qq.com/community/develop/issueList?type=%E4%BF%AE%E5%A4%8D%E4%B8%AD&block=bug)
## 目录结构
```
├─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 页面路由
> 对 uni-app 进行全局配置,决定页面文件的路径、窗口样式、原生的导航栏、底部的原生tabbar 等
https://uniapp.dcloud.net.cn/collocation/pages.html
`src\pages.json`
```json
{
// https://uniapp.dcloud.io/collocation/pages
// 页面路由
// pages数组中第一项表示应用启动页
"pages": [
{
"path": "pages/index/index",
// 页面样式
"style": {
"navigationStyle": "custom", // 隐藏顶部默认导航
"navigationBarTitleText": "首页"
}
},
{
"path": "pages/my/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "我的"
}
}
],
// 全局样式
"globalStyle": {
"navigationBarTextStyle": "black",
"navigationBarTitleText": "uni-app",
"navigationBarBackgroundColor": "#F8F8F8",
"backgroundColor": "#F8F8F8"
}
}
```
### 导航栏
```json
"navigationStyle": "custom", // 隐藏顶部默认导航
```
https://ask.dcloud.net.cn/article/34921
### easycom 组件自动导入
```json
"easycom": {
// 是否开启自动扫描
"autoscan": true,
// 以正则方式自定义组件匹配规则
"custom": {
// uni-ui 规则如下配置
"^uni-(.*)": "@dcloudio/uni-ui/lib/uni-$1/uni-$1.vue",
// 以 Jbc 开头的组件,在 components 文件夹中查找引入(需要重启服务器)
"^Jbc(.*)": "@/components/Jbc$1.vue"
}
},
```
### 底部标签栏
https://uniapp.dcloud.net.cn/collocation/pages.html#tabbar
```json
"tabBar": {
"color": "#333",
"selectedColor": "#18c7ff",
"backgroundColor": "#fff",
"borderStyle": "white",
"spacing": "5px",
"list": [
{
"text": "首页",
"pagePath": "pages/index/index",
"iconPath": "static/tabs/home_default.png",
"selectedIconPath": "static/tabs/home_selected.png"
},
{
"text": "我的",
"pagePath": "pages/my/index",
"iconPath": "static/tabs/user_default.png",
"selectedIconPath": "static/tabs/user_selected.png"
}
]
},
```
## manifest.json 应用配置
> 例如修改 appid
https://uniapp.dcloud.net.cn/collocation/manifest.html
```
src\manifest.json
```
修改了项目的配置文件, 需要重启
最外面的appid, 是需要用到 Dcloud 云服务 (例如打包成 ios) 自动分配的, `mp-weixin => appid` 这里的才是微信小程序的
## uni-app 生命周期
- 应用生命周期:与 **小程序** 应用的生命周期一致(onLaunch、onShow、onHide 等) https://uniapp.dcloud.net.cn/collocation/App.html#applifecycle
- 页面生命周期:与 **小程序** 页面的生命周期一致(onLoad、onUnload、onShow 等) https://uniapp.dcloud.net.cn/tutorial/page.html#lifecycle
- 组件生命周期:与 **Vue.js** 组件的生命周期一致(created、mounted等) https://uniapp.dcloud.net.cn/tutorial/page.html#componentlifecycle
**页面组件优先使用小程序的生命周期钩子**,也就是 onShow、onHide 这些,普通组件就用 Vue 生命周期钩子
### 页面生命周期
https://uniapp.dcloud.net.cn/tutorial/page.html#lifecycle

1. 首先根据pages.json的配置,创建页面
2. 根据页面template里的组件,创建静态dom
3. 触发onLoad
1. 比较适合的操作是:接受上页的参数,联网取数据,更新data
2. 不能操作 dom
4. 转场动画开始
5. 页面onReady
6. 转场动画结束
#### onShow和onHide
a页面刚进入时,会触发a页面的onShow
当a跳转到b页面时,a会触发onHide,而b会触发onShow
但当b被关闭时,b会触发onUnload,此时a再次显示出现,会再次触发onShow
不同tab页面互相切换时,会触发各自的onShow和onHide
#### onBackPress
> 触发返回
```js
onBackPress(options) {
console.log('from:' + options.from)
}
// from:
// 'backbutton'——左上角导航栏按钮及安卓返回键;
// 'navigateBack'——uni.navigateBack() 方法
```
### 组件生命周期
> 和 vue 一样
## 页面和路由
https://uniapp.dcloud.net.cn/api/router.html#navigateto
- `uni.navigateTo`: ===保留当前页面, 跳转到新页面===, 旧页面并没有被销毁, 而是放入页面栈
- `uni.navigateBack` 后退时不会触发 onLoad 而是 onShow
- `uni.redirectTo` 销毁页面跳转到新页面
## 页面通讯
`uni.$emit(eventName,OBJECT)`
`uni.$on(eventName,callback)`
`uni.$once(eventName,callback)`
`uni.$off(eventName, callback)`
## ui 库
uview 不支持 vue 3, 使用 uni-app 自带组件
https://uniapp.dcloud.net.cn/component/
### 内置组件
#### 视图容器
| 组件名 | 说明 |
| ------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- |
| [view](https://uniapp.dcloud.net.cn/component/view.html) | 视图容器,类似于HTML中的div |
| [scroll-view](https://uniapp.dcloud.net.cn/component/scroll-view.html) | 可滚动视图容器 |
| [swiper](https://uniapp.dcloud.net.cn/component/swiper.html) | 滑块视图容器,比如用于轮播banner |
| [match-media](https://uniapp.dcloud.net.cn/component/match-media.html) | 屏幕动态适配组件,比如窄屏上不显示某些内容 |
| [movable-area](https://uniapp.dcloud.net.cn/component/movable-view.html#movable-area) | 可拖动区域 |
| [movable-view](https://uniapp.dcloud.net.cn/component/movable-view.html#movable-view) | 可移动的视图容器,在页面中可以拖拽滑动或双指缩放。movable-view必须在movable-area组件中 |
| [cover-view](https://uniapp.dcloud.net.cn/component/cover-view#cover-view) | 可覆盖在原生组件的上的文本组件 |
| [cover-image](https://uniapp.dcloud.net.cn/component/cover-view#cover-image) | 可覆盖在原生组件的上的图片组件 |
#### 基础内容
| 组件名 | 说明 |
| ------------------------------------------------------------------ | -------------- |
| [icon](https://uniapp.dcloud.net.cn/component/icon.html) | 图标 |
| [text](https://uniapp.dcloud.net.cn/component/text.html) | 文字 |
| [rich-text](https://uniapp.dcloud.net.cn/component/rich-text.html) | 富文本显示组件 |
| [progress](https://uniapp.dcloud.net.cn/component/progress.html) | 进度条 |
#### 表单组件
| 标签名 | 说明 |
| ---------------------------------------------------------------------- | -------------------- |
| [button](https://uniapp.dcloud.net.cn/component/button.html) | 按钮 |
| [checkbox](https://uniapp.dcloud.net.cn/component/checkbox.html) | 多项选择器 |
| [editor](https://uniapp.dcloud.net.cn/component/editor.html) | 富文本输入框 |
| [form](https://uniapp.dcloud.net.cn/component/form.html) | 表单 |
| [input](https://uniapp.dcloud.net.cn/component/input.html) | 输入框 |
| [label](https://uniapp.dcloud.net.cn/component/label.html) | 标签 |
| [picker](https://uniapp.dcloud.net.cn/component/picker.html) | 弹出式列表选择器 |
| [picker-view](https://uniapp.dcloud.net.cn/component/picker-view.html) | 窗体内嵌式列表选择器 |
| [radio](https://uniapp.dcloud.net.cn/component/radio.html) | 单项选择器 |
| [slider](https://uniapp.dcloud.net.cn/component/slider.html) | 滑动选择器 |
| [switch](https://uniapp.dcloud.net.cn/component/switch.html) | 开关选择器 |
| [textarea](https://uniapp.dcloud.net.cn/component/textarea.html) | 多行文本输入框 |
#### 路由与页面跳转
| 组件名 | 说明 |
| ------------------------------------------------------------------ | ----------------------------- |
| [navigator](https://uniapp.dcloud.net.cn/component/navigator.html) | 页面链接。类似于HTML中的a标签 |
#### 媒体组件
| 组件名 | 说明 |
| ---------------------------------------------------------------------- | ---------------------------- |
| [audio](https://uniapp.dcloud.net.cn/component/audio.html) | 音频 |
| [camera](https://uniapp.dcloud.net.cn/component/camera.html) | 相机 |
| [image](https://uniapp.dcloud.net.cn/component/image.html) | 图片 |
| [video](https://uniapp.dcloud.net.cn/component/video.html) | 视频 |
| [live-player](https://uniapp.dcloud.net.cn/component/live-player.html) | 直播播放 |
| [live-pusher](https://uniapp.dcloud.net.cn/component/live-pusher.html) | 实时音视频录制,也称直播推流 |
##### image
- `mode`:
- aspectFit: 保持纵横比缩放图片,使图片的长边能完全显示出来
- aspectFill : 保持纵横比缩放图片,只保证图片的短边能完全显示出来
- widthFix: 宽度不变,高度自动变化,保持原图宽高比不变
#### 地图
| 组件名 | 说明 |
| ------------------------------------------------------ | ---- |
| [map](https://uniapp.dcloud.net.cn/component/map.html) | 地图 |
#### 画布
| 组件名 | 说明 |
| ------------------------------------------------------------ | ---- |
| [canvas](https://uniapp.dcloud.net.cn/component/canvas.html) | 画布 |
#### webview
| 组件名 | 说明 |
| ---------------------------------------------------------------- | ------------- |
| [web-view](https://uniapp.dcloud.net.cn/component/web-view.html) | web浏览器组件 |
#### 广告
| 组件名 | 说明 |
| -------------------------------------------------------------- | ------------------ |
| [ad](https://uniapp.dcloud.net.cn/component/ad.html) | 广告组件 |
| [ad-draw](https://uniapp.dcloud.net.cn/component/ad-draw.html) | 沉浸视频流广告组件 |
#### 页面属性配置
| 组件名 | 说明 |
| ---------------------------------------------------------------------------- | -------------------- |
| [custom-tab-bar](https://uniapp.dcloud.net.cn/component/custom-tab-bar.html) | 底部tabbar自定义组件 |
| [navigation-bar](https://uniapp.dcloud.net.cn/component/navigation-bar.html) | 页面顶部导航 |
| [page-meta](https://uniapp.dcloud.net.cn/component/page-meta.html) | 页面属性配置节点 |
#### uniCloud
| 组件名 | 说明 |
| ----------------------------------------------------------------- | ---------------------------- |
| [unicloud-db组件](https://doc.dcloud.net.cn/uniCloud/unicloud-db) | uniCloud数据库访问和操作组件 |
### uni-ui
> 扩展组件, 符合 easycom 标准, 会自动导入
```sh
# 安装
pnpm add @dcloudio/uni-ui
```
| 组件名 | 组件说明 |
| --------------------- | ------------------------------------------------------------------------------- |
| uni-badge | [数字角标](https://ext.dcloud.net.cn/plugin?name=uni-badge) |
| uni-calendar | [日历](https://ext.dcloud.net.cn/plugin?name=uni-calendar) |
| uni-card | [卡片](https://ext.dcloud.net.cn/plugin?name=uni-card) |
| uni-collapse | [折叠面板](https://ext.dcloud.net.cn/plugin?name=uni-collapse) |
| uni-combox | [组合框](https://ext.dcloud.net.cn/plugin?name=uni-combox) |
| uni-countdown | [倒计时](https://ext.dcloud.net.cn/plugin?name=uni-countdown) |
| uni-data-checkbox | [数据选择器](https://ext.dcloud.net.cn/plugin?name=uni-data-checkbox) |
| uni-data-picker | [数据驱动的picker选择器](https://ext.dcloud.net.cn/plugin?name=uni-data-picker) |
| uni-dateformat | [日期格式化](https://ext.dcloud.net.cn/plugin?name=uni-dateformat) |
| uni-datetime-picker | [日期选择器](https://ext.dcloud.net.cn/plugin?name=uni-datetime-picker) |
| uni-drawer | [抽屉](https://ext.dcloud.net.cn/plugin?name=uni-drawer) |
| uni-easyinput | [增强输入框](https://ext.dcloud.net.cn/plugin?name=uni-easyinput) |
| uni-fab | [悬浮按钮](https://ext.dcloud.net.cn/plugin?name=uni-fab) |
| uni-fav | [收藏按钮](https://ext.dcloud.net.cn/plugin?name=uni-fav) |
| uni-file-picker | [文件选择上传](https://ext.dcloud.net.cn/plugin?name=uni-file-picker) |
| uni-forms | [表单](https://ext.dcloud.net.cn/plugin?name=uni-forms) |
| uni-goods-nav | [商品导航](https://ext.dcloud.net.cn/plugin?name=uni-goods-nav) |
| uni-grid | [宫格](https://ext.dcloud.net.cn/plugin?name=uni-grid) |
| uni-group | [分组](https://ext.dcloud.net.cn/plugin?name=uni-group) |
| uni-icons | [图标](https://ext.dcloud.net.cn/plugin?name=uni-icons) |
| uni-indexed-list | [索引列表](https://ext.dcloud.net.cn/plugin?name=uni-indexed-list) |
| uni-link | [超链接](https://ext.dcloud.net.cn/plugin?name=uni-link) |
| uni-list | [列表](https://ext.dcloud.net.cn/plugin?name=uni-list) |
| uni-load-more | [加载更多](https://ext.dcloud.net.cn/plugin?name=uni-load-more) |
| uni-nav-bar | [自定义导航栏](https://ext.dcloud.net.cn/plugin?name=uni-nav-bar) |
| uni-notice-bar | [通告栏](https://ext.dcloud.net.cn/plugin?name=uni-notice-bar) |
| uni-number-box | [数字输入框](https://ext.dcloud.net.cn/plugin?name=uni-number-box) |
| uni-pagination | [分页器](https://ext.dcloud.net.cn/plugin?name=uni-pagination) |
| uni-popup | [弹出层](https://ext.dcloud.net.cn/plugin?name=uni-popup) |
| uni-rate | [评分](https://ext.dcloud.net.cn/plugin?name=uni-rate) |
| uni-row | [布局-行](https://ext.dcloud.net.cn/plugin?name=uni-row) |
| uni-search-bar | [搜索栏](https://ext.dcloud.net.cn/plugin?name=uni-search-bar) |
| uni-segmented-control | [分段器](https://ext.dcloud.net.cn/plugin?name=uni-segmented-control) |
| uni-steps | [步骤条](https://ext.dcloud.net.cn/plugin?name=uni-steps) |
| uni-swipe-action | [滑动操作](https://ext.dcloud.net.cn/plugin?name=uni-swipe-action) |
| uni-swiper-dot | [轮播图指示点](https://ext.dcloud.net.cn/plugin?name=uni-swiper-dot) |
| uni-table | [表格](https://ext.dcloud.net.cn/plugin?name=uni-table) |
| uni-tag | [标签](https://ext.dcloud.net.cn/plugin?name=uni-tag) |
| uni-title | [章节标题](https://ext.dcloud.net.cn/plugin?name=uni-title) |
| uni-transition | [过渡动画](https://ext.dcloud.net.cn/plugin?name=uni-transition) |
## uni-api
- 出现目的
1. 很多的原生的小程序的api 不支持 promise `wx.request`
2. 想要做一个跨平台 应用 那么 发送请求 该用哪一代码来发送 3. axios 针对 网页的!!! 4. wx.request 只针对 微信小程序 5. 自己去封装一个api 可以去兼容适配所有的客户端
```
if (微信小程序) wx.request
if (web) axios
if (支付宝) ...
```
- 如何使用
```js
uni.request
```
## 语法
1. 标签优先使用 uni-app 的
2. js语法和vue一样
3. 特有的api: `uni.request`, `uni.showToast`
4. 生命周期参考上面的
### css 单位 rpx
- 以 750 px 宽的屏幕为基准,750rpx 恰好为屏幕宽度
- 设计师可以用 iPhone6 作为视觉稿的标准, 对应 750rpx
## 分包优化
https://uniapp.dcloud.net.cn/collocation/manifest.html#关于分包优化的说明
## 项目实战
视频: https://www.bilibili.com/video/BV1Bp4y1379L
笔记: https://gitee.com/Megasu/uni-app-shop-note
高星框架: https://github.com/codercup/unibest
### 框架搭建
#### vscode 插件
`.vscode/extensions.json`
```json
{
// 推荐的扩展插件
"recommendations": [
"mrmaoddxxaa.create-uniapp-view", // 创建 uni-app 页面
"uni-helper.uni-helper-vscode", // uni-app 代码提示
"evils.uniapp-vscode", // uni-app 文档
"vue.volar", // vue3 语法支持
"editorconfig.editorconfig", // editorconfig
"dbaeumer.vscode-eslint", // eslint
"esbenp.prettier-vscode" // prettier
]
}
```
#### create-uniapp-view 设置
设置: 创建同名文件夹、不使用 scoped、预编译器 scss、vue 3
#### 类型提示
> uni-app 类型, 由第三方 uni-helper 提供
```sh
pnpm add @uni-helper/uni-app-types @uni-helper/uni-ui-types -D
```
> 微信小程序的类型提示
```sh
pnpm add miniprogram-api-typings -D
```
> nodejs
```sh
pnpm add @types/node -D
```
在 `tsconfig.json` 的 types 中添加
```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"]
}
}
```
- TypeScript 默认设置 typeRoots 包含了 `node_modules/@types`, 以 `@types` 开头的包不需要手动添加
#### vscode 设置
`.vscode/settings.json`
```json
{
// 在保存时格式化文件
"editor.formatOnSave": true,
// 文件格式化配置
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
// 控制资源管理器是否应以紧凑形式呈现文件夹。单个子文件夹将被压缩在组合的树元素中。
"explorer.compactFolders": false,
// 配置 vue 文件的默认格式化工具为 prettier
"[vue]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
// 配置 html 文件的默认格式化工具为 prettier
"[html]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
// 配置 ts 文件的默认格式化工具为 prettier
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
// 配置语言的文件关联
"files.associations": {
"pages.json": "jsonc", // pages.json 可以写注释
"manifest.json": "jsonc" // manifest.json 可以写注释
}
}
```
#### prettierrc
```sh
pnpm add 'prettier@^3' '@vue/eslint-config-prettier@^9' -D
```
`.prettierrc.json`
```json
{
"singleQuote": true,
"semi": false,
"printWidth": 120,
"trailingComma": "all",
"endOfLine": "auto"
}
```
#### eslint
```sh
pnpm add 'eslint@^8' 'eslint-plugin-vue@^9' '@vue/eslint-config-typescript@^11' '@rushstack/eslint-patch@^1' -D
```
`.eslintrc.cjs`
```json
/* 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,
Page: true,
AnyObject: 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'],
},
}
```
#### editorconfig
`.editorconfig`
```ini
# editorconfig.org
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
```
#### husky
> 提交前格式校验
```sh
pnpm add husky lint-staged -D
```
`.husky/pre-commit`
```sh
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run lint-staged
```
`package.json`
```json
{
// ...
"scripts": {
// ...
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"prepare": "husky install",
"lint-staged": "lint-staged"
},
"lint-staged": {
"*.{js,ts,vue}": ["eslint --fix"]
}
//...
}
```
### 修改appid
微信公众号平台: https://mp.weixin.qq.com/
注册 --> 管理 --> 开发管理 --> AppID(小程序ID)
可以修改 `src/manifest.json` 中的 `mp-weixin => appid`, 或者直接在模拟器的详情中修改
### pinia 持久化
```sh
pnpm add 'pinia@^2' 'pinia-plugin-persistedstate@^3'
```
- `pinia-plugin-persistedstate` 默认使用 `localStorage`, 需要改为 uni app 的 `uni.setStorageSync()` 和 `uni.getStorageSync()`
- https://prazdevs.github.io/pinia-plugin-persistedstate/zh/guide/config.html#storage
- pinia 3 暂时不兼容: https://github.com/dcloudio/uni-app/issues/5326
`src/stores/index.ts`
```ts
import { createPinia } from 'pinia'
import persist from 'pinia-plugin-persistedstate'
// 创建 pinia 实例
const pinia = createPinia()
// 使用持久化存储插件
pinia.use(persist)
// 默认导出,给 main.ts 使用
export default pinia
```
`main.ts`
```ts
// 导入 pinia 实例
import pinia from './stores/'
// ...
// 在 return 前添加
// 使用 pinia
app.use(pinia)
```
`stores/modules/member.ts`
- 小程序没有 localStorage
```ts
export const useMemberStore = defineStore(
'member',
() => {
//…省略
},
{
// 配置持久化
persist: {
// 调整为兼容多端的API
storage: {
setItem(key, value) {
uni.setStorageSync(key, value) // [!code warning]
},
getItem(key) {
return uni.getStorageSync(key) // [!code warning]
},
},
},
},
)
```
### 拦截器
> 应用场景: 给请求 `request` 添加基础地址、超时时间、请求头标识、token
> 文件上传 `uploadFile` 同样添加
https://uniapp.dcloud.net.cn/api/interceptor.html#addinterceptor
- 添加拦截器: `uni.addInterceptor`
```js
uni.request({
url: 'request/login', //仅为示例,并非真实接口地址。
success: (res) => {
console.log(res.data)
// 打印: {code:1,...}
},
})
uni.addInterceptor('request', {
invoke(args) {
// 拦截前触发
args.url = 'https://www.example.com/' + args.url
},
success(args) {
// 成功回调拦截
args.data.code = 1
},
fail(err) {
// 失败回调拦截
console.log('interceptor-fail', err)
},
complete(res) {
// 完成回调拦截
console.log('interceptor-complete', res)
},
})
uni.addInterceptor({
returnValue(args) {
// 方法调用后触发,处理返回值
return args.data
},
})
```
- 删除拦截器 `uni.removeInterceptor`
```js
uni.removeInterceptor('request')
```
### 响应封装
- 成功:
- 提取 res. data
- 添加类型支持
- 失败
- 网络错误: 提示换网络
- 401: 清理用户信息, 跳转登录页
- 其他错误: 轻提示
注意项:
- uni-app 的 request 只有网络错误才会走 fail, 权限不足等后端有响应的, 都是 success. 需要通过 `res.statusCode` 状态码进一步控制
### 自定义导航栏
https://ask.dcloud.net.cn/article/34921
`src/pages/index/components/CustomNavbar.vue`
样式适配: 安全区域 (例如刘海屏)
```ts
// 获取屏幕边界到安全区域距离
const { safeAreaInsets } = uni.getSystemInfoSync()
```
用来设置 paddingTop
```vue
```
### 轮播图通用组件
#### 配置 easycom 自动导入
`page.json`
```json
"easycom": {
// 是否开启自动扫描
"autoscan": true,
// 以正则方式自定义组件匹配规则
"custom": {
// ...
// 以 Jbc 开头的组件,在 components 文件夹中查找引入(需要重启服务器)
"^Jbc(.*)": "@/components/Jbc$1.vue"
}
},
```
#### 组件
`src/components/JbcSwiper.vue`
使用: `swiper` 滑动视图容器
https://uniapp.dcloud.net.cn/component/swiper.html
在 `onload` 钩子中调用 api
- `swiper` 绑定 `change` 事件的类型: `UniHelper.SwiperOnChange`
```ts
const handleChange: UniHelper.SwiperOnChange = (e) => {
activeIndex.value = e.detail!.current
}
```
#### 类型提示
`src/types/components.d.ts`
```ts
import JbcSwiper from '@/components/JbcSwiper.vue'
declare module 'vue' {
export interface GlobalComponents {
JbcSwiper: typeof JbcSwiper
}
}
// 组件实例类型 (typeof xxx组件 获取到的是构造函数类型)
export type JbcSwiperInstance = InstanceType
```
- declare module 会与现有模块类型合并,而不是覆盖
#### api 封装
`src/services/home.ts`
- `uni.request()` : 当请求类型为 get 时, 把 data 里的键值对转化为 query 参数
### 首页分类组件
略
### 热门推荐
略
### 猜你喜欢通用模块
参考: `JbcSwiper.vue`
首页添加滚动组件:
- `scroll-view` 组件:
- @scrolltolower: 滚动到底触发
父组件调用子组件的方法:
- 子组件通过 `defineExpose({methodName})` 暴露方法
- 父组件给组件绑定 `ref="xxx"`, 通过 `xxx.value.methodName()`
- 子组件的类型为自定义组件实例类型 `JbcGuessInstance`
滚动到底时调用猜你喜欢模块获取下页数据插入数组
### 首页下拉刷新
- `scroll-view` 组件:
- refresher-enabled: 开启自定义下拉刷新
- @refresherrefresh: 自定义下拉刷新被触发
- refresher-triggered: 设置当前下拉刷新状态
- enable-back-to-top: iOS点击顶部状态栏、安卓双击标题栏时,滚动条返回顶部,只支持竖向
刷新时, 多个 api 用 `Promise.allSettled()`
bug 修复: 下拉刷新后轮播图指示器错乱
原因:
- 每次刷新, 获取的轮播图数据都会随机打乱, 用 id 作为 key 导致 dom 更新异常改为 `:key="item.id + index"`
- `watch` 监听 `props.list` 的变化, 复位 `activeIndex.value=0`
### 首页骨架屏
- 在微信开发者工具模拟器右下角三个小点的菜单里, 有生成骨架屏选项
- 复制 `index.skeleton.wxml` 到 template, 复制 `index.skeleton.wxss` 到 style
- 自定义导航栏不需要加载数据, 只保留滚动容器里面的部分
### 热门推荐详情页
- uni-app 获取路由参数
```ts
// uni-app获取路由参数
const query = defineProps<{
type: string
}>()
console.log(query)
```
- uni-app 动态设置标题
```ts
// 动态设置标题
uni.setNavigationBarTitle({ title: '标题名' })
```
- 判断当前是不是开发环境, 方便设置模拟数据
```ts
{
page: import.meta.env.DEV ? 33 : 1,
pageSize: 10,
}
```
### 商品分类页
- 小程序开发者工具直接显示分类页, 免去点击跳转:
点击'普通编译 -> 添加编译模式 -> 选择要启动页面为要编译的页面'
- 切换分类时, 滚动条不恢复到顶部:
```ts
// 切换分类时,滚动到顶部
const scrollTop = ref(0) // scrollTop绑定到 scroll-view 的 scroll-top 属性上
// 记录滚动位置, 保存到另一个变量
// 切换分页的时候,先还原scrollTop的旧值,然后在视图渲染后,再设置scrollTop等于0触发还原
const scroolTopOld = ref(0)
type ScrollEvent = {
detail: {
scrollTop: number
scrollLeft: number
scrollHeight: number
scrollWidth: number
deltaX: number
deltaY: number
}
[key: string]: any
}
const handlerScrool = (e: ScrollEvent) => {
scroolTopOld.value = e.detail.scrollTop
}
watch(activeIndex, () => {
scrollTop.value = scroolTopOld.value
nextTick(() => {
scrollTop.value = 0
})
})
```
### 商品详情页
- 图标: 使用阿里巴巴字体图标 https://www.iconfont.cn/
字体可以上传到 cdn, `iconfont.css` 需要修改 `font-face`
```css
@font-face {
font-family: 'iconfont'; /* Project id */
src: url('iconfont.ttf?t=1745547896290') format('truetype');
}
```
- 操作栏有按钮, 需要设置安全区域 (可能有小白条) 防止误操作
```vue
```
- 弹出层组件 `uni-popup`
https://uniapp.dcloud.net.cn/component/uniui/uni-popup.html
属性:
- type: 弹出方式, top center bottom left right message dialog share
- background-color: 主窗口背景色
- 先用 ref 绑定 `uni-popup`, 再调用 ref 的 open 和 close 方法控制是否显示
类型定义: `UniHelper.UniPopupInstance` 类型里的 `open()` 和 `close()` 是可选的, 会导致 `popup.value?.open()` 报错, 需要使用交叉类型
```ts
const popup = ref<
UniHelper.UniPopupInstance & {
open: (type?: UniHelper.UniPopupType) => void
close: () => void
}
>()
```
- `NonNullable`: 去除类型里的 null 和 undefined
### 登录模块
#### 快捷登录
- 登陆流程

- 获取用户凭证 `code`
===有效期五分钟===。开发者需要在开发者服务器后台调用 code2Session,使用 code 换取 openid、unionid、session_key 等信息
不能在 `getphonenumber` 里调用, 在 `onload` 里
```ts
wx.login(Object object)
```
https://developers.weixin.qq.com/miniprogram/dev/api/open-api/login/wx.login.html
- 获取手机号 (小程序有隐私保护, 需要用户同意, 且个人开发者没法调用)
`button` 需要设置 `open-type=getPhoneNumber`, 然后在 `@getphonenumber` 回调中拿到手机号
https://uniapp.dcloud.net.cn/component/button.html#button
https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/getPhoneNumber.html
### 个人中心
- 封装组合式函数 `src/composables/index.ts`
> 函数命名以 use 开头, 放 composables 文件夹下
```ts
import type { JbcGuessInstance } from '@/types/components'
import { ref } from 'vue'
export const useGuessList = () => {
const guessRef = ref()
const onScrollToLower = () => {
guessRef.value?.getMore()
}
return { guessRef, onScrollToLower }
}
```
调用:
```ts
import { useGuessList } from '@/composables'
const { guessRef, onScrollToLower } = useGuessList()
```
- 设置页分包
> 把不常用的页面拆成另外的包, 当某些步骤可能需要打开时, 提前预加载
存放目录: 和 `pages` 同级, 例如 `pagesMember/settings/settings.vue`
在 `pages.json` 中添加 `subPackages`, 和 `pages` 同级
```json
// 分包
"subPackages": [
{
// 子包的根目录
"root": "pagesMember",
// pages参数和主包一样
"pages": [
{
"path": "settings/settings",
"style": {
"navigationBarTitleText": "设置"
}
}
]
}
]
```
- 设置预下载 `page.json`
> key 是页面路径
https://uniapp.dcloud.net.cn/collocation/pages.html#preloadrule
```json
// 预下载
"preloadRule": {
"pages/my/my": {
"network": "all",
"packages": ["pagesMember"]
}
}
```
### 设置页面
- 显示模态框: `uni.showModal()`
- 路由后退: `uni.navigateBack()`
### 修改用户信息
- `Pick` : 从类型中选取指定属性
- `picker` 选择器
- 时间选择器: `mode = time`
- 省市区选择器: `mode = region`
https://uniapp.dcloud.net.cn/component/picker.html
- `radio` 选择器
https://uniapp.dcloud.net.cn/component/radio.html
- 修改头像: 调用拍照 api `uni.chooseMedia`
- 上传头像: `uni.uploadFile`
### 收货地址
#### 地址表单
- 切换开关 `switch` 组件
https://uniapp.dcloud.net.cn/component/switch.html
- `uni.navigateBack`, 触发的是 onShow 事件, 要把请求列表放到 onShow 而不是 onLoad
#### 数据校验
- 表单校验: 使用 `` 组件
- 表单绑定 `ref modelValue rules`
- 表单项设置 `label name`
- 原生 checkbox 或三方组件不支持 v-model 等, `onFieldChange`
- 提交前再调用 formRef 的 `.validate()`, 返回的是 Promise
https://uniapp.dcloud.net.cn/component/uniui/uni-forms.html
```ts
// 表单验证规则
const rules: UniFormsRules = {
receiver: {
rules: [
{ required: true, errorMessage: '请输入收货人姓名' },
{
minLength: 2,
maxLength: 5,
errorMessage: '姓名长度在 {minLength} 到 {maxLength} 个字符',
},
],
},
contact: {
rules: [
{ required: true, errorMessage: '请输入联系电话' },
{
pattern: /^1[3-9]\d{9}$/,
errorMessage: '手机号码为11位数字',
},
],
},
fullLocation: {
rules: [{ required: true, errorMessage: '请选择省市区' }],
},
address: {
rules: [
{ required: true, errorMessage: '请输入详细地址' },
{
minLength: 2,
maxLength: 20,
errorMessage: '详细地址长度在 {minLength} 到 {maxLength} 个字符',
},
],
},
}
```
#### 删除功能
- 删除地址: 侧划组件 `uni-swipe-action`
https://uniapp.dcloud.net.cn/component/uniui/uni-swipe-action.html
### SKU 模块 - 最小存货单位
- SKU 算法: https://juejin.cn/post/7002746459456176158
- 插件市场: https://ext.dcloud.net.cn/
- 插件下载: https://gitee.com/vk-uni/vk-u-goods-sku-popup
#### 类型定义
创建 `vk-data-goods-sku-popup.d.ts`, 放在 `vk-data-goods-sku-popup` 目录下
```ts
import { Component } from '@uni-helper/uni-app-types'
/** SKU 弹出层 */
export type SkuPopup = Component
/** SKU 弹出层实例 */
export type SkuPopupInstance = InstanceType
/** SKU 弹出层属性 */
export type SkuPopupProps = {
/** 双向绑定,true 为打开组件,false 为关闭组件 */
modelValue: boolean
/** 商品信息本地数据源 */
localdata: SkuPopupLocaldata
/** 按钮模式 1:都显示 2:只显示购物车 3:只显示立即购买 */
mode?: 1 | 2 | 3
/** 该商品已抢完时的按钮文字 */
noStockText?: string
/** 库存文字 */
stockText?: string
/** 点击遮罩是否关闭组件 */
maskCloseAble?: boolean
/** 顶部圆角值 */
borderRadius?: string | number
/** 最小购买数量 */
minBuyNum?: number
/** 最大购买数量 */
maxBuyNum?: number
/** 每次点击后的数量 */
stepBuyNum?: number
/** 是否只能输入 step 的倍数 */
stepStrictly?: boolean
/** 是否隐藏库存的显示 */
hideStock?: false
/** 主题风格 */
theme?: 'default' | 'red-black' | 'black-white' | 'coffee' | 'green'
/** 默认金额会除以100(即100=1元),若设置为0,则不会除以100(即1=1元) */
amountType?: 1 | 0
/** 自定义获取商品信息的函数(已知支付宝不支持,支付宝请改用localdata属性) */
customAction?: () => void
/** 是否显示右上角关闭按钮 */
showClose?: boolean
/** 关闭按钮的图片地址 */
closeImage?: string
/** 价格的字体颜色 */
priceColor?: string
/** 立即购买 - 按钮的文字 */
buyNowText?: string
/** 立即购买 - 按钮的字体颜色 */
buyNowColor?: string
/** 立即购买 - 按钮的背景颜色 */
buyNowBackgroundColor?: string
/** 加入购物车 - 按钮的文字 */
addCartText?: string
/** 加入购物车 - 按钮的字体颜色 */
addCartColor?: string
/** 加入购物车 - 按钮的背景颜色 */
addCartBackgroundColor?: string
/** 商品缩略图背景颜色 */
goodsThumbBackgroundColor?: string
/** 样式 - 不可点击时,按钮的样式 */
disableStyle?: object
/** 样式 - 按钮点击时的样式 */
activedStyle?: object
/** 样式 - 按钮常态的样式 */
btnStyle?: object
/** 字段名 - 商品表id的字段名 */
goodsIdName?: string
/** 字段名 - sku表id的字段名 */
skuIdName?: string
/** 字段名 - 商品对应的sku列表的字段名 */
skuListName?: string
/** 字段名 - 商品规格名称的字段名 */
specListName?: string
/** 字段名 - sku库存的字段名 */
stockName?: string
/** 字段名 - sku组合路径的字段名 */
skuArrName?: string
/** 字段名 - 商品缩略图字段名(未选择sku时) */
goodsThumbName?: string
/** 被选中的值 */
selectArr?: string[]
/** 打开弹出层 */
onOpen: () => void
/** 关闭弹出层 */
onClose: () => void
/** 点击加入购物车时(需选择完SKU才会触发)*/
onAddCart: (event: SkuPopupEvent) => void
/** 点击立即购买时(需选择完SKU才会触发)*/
onBuyNow: (event: SkuPopupEvent) => void
}
/** 商品信息本地数据源 */
export type SkuPopupLocaldata = {
/** 商品 ID */
_id: string
/** 商品名称 */
name: string
/** 商品图片 */
goods_thumb: string
/** 商品规格列表 */
spec_list: SkuPopupSpecItem[]
/** 商品SKU列表 */
sku_list: SkuPopupSkuItem[]
}
/** 商品规格名称的集合 */
export type SkuPopupSpecItem = {
/** 规格名称 */
name: string
/** 规格集合 */
list: { name: string }[]
}
/** 商品SKU列表 */
export type SkuPopupSkuItem = {
/** SKU ID */
_id: string
/** 商品 ID */
goods_id: string
/** 商品名称 */
goods_name: string
/** 商品图片 */
image: string
/** SKU 价格 * 100, 注意:需要乘以 100 */
price: number
/** SKU 规格组成, 注意:需要与 spec_list 数组顺序对应 */
sku_name_arr: string[]
/** SKU 库存 */
stock: number
}
/** 当前选择的sku数据 */
export type SkuPopupEvent = SkuPopupSkuItem & {
/** 商品购买数量 */
buy_num: number
}
/** 全局组件类型声明 */
declare module 'vue' {
export interface GlobalComponents {
'vk-data-goods-sku-popup': SkuPopup
}
}
```
### 购物车
- 步进器组件 `vk-data-input-number-box.vue`
- 步进器组件类型定义: `vk-data-input-number-box.d.ts`
```ts
import { Component } from '@uni-helper/uni-app-types'
/** 步进器 */
export type InputNumberBox = Component
/** 步进器实例 */
export type InputNumberBoxInstance = InstanceType
/** 步进器属性 */
export type InputNumberBoxProps = {
/** 输入框初始值(默认1) */
modelValue: number
/** 用户可输入的最小值(默认0) */
min: number
/** 用户可输入的最大值(默认99999) */
max: number
/** 步长,每次加或减的值(默认1) */
step: number
/** 是否禁用操作,包括输入框,加减按钮 */
disabled: boolean
/** 输入框宽度,单位rpx(默认80) */
inputWidth: string | number
/** 输入框和按钮的高度,单位rpx(默认50) */
inputHeight: string | number
/** 输入框和按钮的背景颜色(默认#F2F3F5) */
bgColor: string
/** 步进器标识符 */
index: string
/** 输入框内容发生变化时触发 */
change: (event: InputNumberBoxEvent) => void
/** 输入框失去焦点时触发 */
blur: (event: InputNumberBoxEvent) => void
/** 点击增加按钮时触发 */
plus: (event: InputNumberBoxEvent) => void
/** 点击减少按钮时触发 */
minus: (event: InputNumberBoxEvent) => void
}
/** 步进器事件对象 */
export type InputNumberBoxEvent = {
/** 输入框当前值 */
value: number
/** 步进器标识符 */
index: string
}
/** 全局组件类型声明 */
declare module 'vue' {
export interface GlobalComponents {
'vk-data-input-number-box': InputNumberBox
}
}
```
- 购物车有两个, 一个页底部导航栏的, 一个是商品页面按钮的
### 订单页
- 送货时间: `picker` 自定义选择列表
- 商品页跳转快速下单: 小程序开发者工具左下角可以看到页面参数
- 跳转订单详情页使用 `uni.redirectTo`
### 订单详情页
#### 自定义导航栏
- 自定义导航栏设置左上角按钮
- 获取当前页面栈 : https://developers.weixin.qq.com/miniprogram/dev/reference/api/getCurrentPages.html
- `getCurrentPages()` 最后一个就是当前页面
#### 滚动时切换 title
- 向下滚动时切换 title
- 滚动驱动的动画: https://developers.weixin.qq.com/miniprogram/dev/framework/view/animation.html#滚动驱动的动画
- 获取当前页面的实例
- 在页面渲染完毕 `onReady` 生命周期执行
```ts
onReady(() => {
// 动画效果: 导航栏背景色
pageInstance.animate(
'.navbar',
[
{
backgroundColor: 'transparent',
},
{
backgroundColor: '#f8f8f8',
},
],
1000,
{
scrollSource: '#scroller',
timeRange: 1000,
startScrollOffset: 0,
endScrollOffset: 50,
},
)
// 动画效果: 导航栏标题
pageInstance.animate(
'.navbar .title',
[
{
color: 'transparent',
},
{
color: '#000',
},
],
1000,
{
scrollSource: '#scroller',
timeRange: 1000,
startScrollOffset: 0,
endScrollOffset: 50,
},
)
// 动画效果: 返回按钮
pageInstance.animate(
'.navbar .back',
[
{
color: '#fff',
},
{
color: '#000',
},
],
1000,
{
scrollSource: '#scroller',
timeRange: 1000,
startScrollOffset: 0,
endScrollOffset: 50,
},
)
})
```
#### 支付
- 支付倒计时: `uni-countdown` https://uniapp.dcloud.net.cn/component/uniui/uni-countdown.html
- 去支付
- 生产环境:
1. 需要通过认证的应用对应的 appid https://uniapp.dcloud.net.cn/tutorial/app-payment-weixin.html#%E5%BC%80%E9%80%9A
2. 提交资料申请开通获得商户号 `PartnerID` 和密码, 置APIv2密钥
3. 服务器接入 https://pay.weixin.qq.com/doc/v3/merchant/4012791911
4. 流程: 
- 开发环境:
- 模拟支付
- 微信支付参数的返回类型: `WechatMiniprogram.RequestPaymentOption`, 给 `wx.requestPayment` 调用
- 复制剪切板: `uni.setClipboardData`
### 订单列表页
todo: 5 个不同类型的列表并发太慢
### 小程序打包
1. 编译
```sh
pnpm build:mp-weixin
```
2. 开发者工具: 点击上传
1. 或者使用 ci: https://developers.weixin.qq.com/miniprogram/dev/devtools/ci.html
3. 公众号平台提交审核 https://mp.weixin.qq.com
#### 条件编译
> 是通过注释实现
https://uniapp.dcloud.net.cn/tutorial/platform.html#preprocessor
- 以 `#ifdef` 或 `#ifndef` 加 `%PLATFORM%` 开头,以 `#endif` 结尾。
- `#ifdef`:if defined 仅在某平台存在
- `#ifndef`:if not defined 除了某平台均存在
- `%PLATFORM%`:平台名称
| 条件编译写法 | 说明 |
| ------------------------------------------------------------ | --------------------------------------------------------------------------------- |
| `#ifdef APP-PLUS`
需条件编译的代码
`#endif ` | 仅出现在 App 平台下的代码 |
| `#ifndef H5`
需条件编译的代码
`#endif` | 除了 H5 平台,其它平台均存在的代码(注意if后面有个n) |
| ` #ifdef H5 丨丨 MP-WEIXIN`
需条件编译的代码
`#endif` | 在 H5 平台或微信小程序平台存在的代码(这里只有 \|\|,不可能出现&&,因为没有交集) |
- 支持的文件:
- .vue/.nvue/.uvue
- .js/.uts
- .css
- pages.json
- 各预编译语言文件,如:.scss、.less、.stylus、.ts、.pug
**注意:**
1. 样式的条件编译,无论是 css 还是 sass/scss/less/stylus 等预编译语言中,必须使用 `/*注释*/` 的写法。
2. 通过 import 导入的 scss 中的条件编译似乎不生效
#### h5 预览
- 相对路径
`manifest.json`
```json
/* H5特有相关 */
"h5": {
"router": {
"base": "./"
}
},
```
#### 安卓打包
调试:
- 需要 `HBuilderX`
- 安装运行插件和基座
- 调试手机开启开发者工具
云打包:
- 注册 DCloud 账号
- 获取 DCloud AppID: 在 HBuilderX 中打开 `manifest.json`
- 设置应用名称图标
- 发行-云打包-使用云端证书
#### ios 打包
调试:
- 需要mac
- 安装 xcode
- 运行到模拟器
打包:
- 需要苹果开发者账号和年费
#### 样式兼容
- 小程序不支持 \* 选择器
- 页面视口差异
- 小程序: 分tabBar 页和普通页
- 在 tabBar 页中, 视口不包括顶部和底部
- 在普通页中, 视口不包括顶部
- h 5: 视口包括浏览器整个区域
- H 5 端默认开启 scoped
- uni-app 跨端注意
- uni-app css 支持
- uni-app 条件编译
实际遇到的问题:
- h5 中的顶栏和底栏都是 div 模拟的, 这样会有遮住的问题
uni-app 内置的 css 变量: https://uniapp.dcloud.net.cn/tutorial/syntax-css.html#css-%E5%8F%98%E9%87%8F
| CSS 变量 | 描述 | App | 小程序 | H5 |
| ------------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------- | ------ | -------------------- |
| --status-bar-height | 系统状态栏高度 | [系统状态栏高度](http://www.html5plus.org/doc/zh_cn/navigator.html#plus.navigator.getStatusbarHeight)、nvue 注意见下 | 25px | 0 |
| --window-top | 内容区域距离顶部的距离 | 0 | 0 | NavigationBar 的高度 |
| --window-bottom | 内容区域距离底部的距离 | 0 | 0 | TabBar 的高度 |
示例:
```scss
/* 吸底工具栏 */
.toolbar {
/* ... */
bottom: calc(var(--window-bottom));
/* ... */
}
```
- 骨架屏样式错位
原因: h5 样式隔离
具体点: 骨架屏是由小程序开发工具生成的, 自动生成的样式和 h5 样式隔离后的 class 对应不上
解决方法: 把骨架屏里对应的子组件样式单独拆分, 然后在骨架屏中条件编译导入, 搜索 `is=` 对应的组件, 就是需要处理的
- App flex 布局不生效: App 没有 page 元素, 只有 `#app`
#### 组件兼容
小程序特有的:
- `open-type=`, 例外: `navigate` `switchTab` `redirect`
- `wx.`
- `.animate` 动画效果
- `uni-navigator`: 在 h5 中实现, 实际在外层嵌套了一个 a 标签, 可能导致原来的样式失效
- 解决方法: 添加属性 `:render-link="false"`
#### API 兼容
- 选择头像的 `uni.chooseMedia` 只支持小程序, 可以使用停止维护的 api `uni.chooseImage`