一、组件学习

1.1 view组件

视图容器

它类似于传统html中的div,用于包裹各种元素内容。

1.1.1 属性

属性名 类型 默认值 说明
hover-class String none 指定按下去的样式类。当 hover-class=”none” 时,没有点击态效果
hover-stop-propagation Boolean false 指定是否阻止本节点的祖先节点出现点击态,App、H5、支付宝小程序、百度小程序不支持(支付宝小程序、百度小程序文档中都有此属性,实测未支持)
hover-start-time Number 50 按住后多久出现点击态,单位毫秒
hover-stay-time Number 400 手指松开后点击态保留时间,单位毫秒

1.1.2 使用举例

1.2 text组件

文本组件。用于包裹文本内容。

1.2.1 属性

属性名 类型 默认值 说明 平台差异说明
selectable Boolean false 文本是否可选
user-select Boolean false 文本是否可选 微信小程序
space String 显示连续空格 钉钉小程序不支持
decode Boolean false 是否解码 百度、钉钉小程序不支持

1.3 scroll-view

可滚动视图区域。用于区域滚动。

需注意在webview渲染的页面中,区域滚动的性能不及页面滚动。

1.3.1 属性

1.3.2 水平方向的滑动布局

要实现水平方向的滑动布局

要实现三点:

1.4 swiper

滑块视图容器。

一般用于轮播图。

注意滑动切换和滚动区域的区别:

滑动切换是一屏一屏的切换。swiper下的每个swiper-item是一个滑动切换区域,不能停留在2个滑动区域之间。

1.4.1 属性

1.4.2 轮播图效果

1.5 image

图片组件。

1.5.1 属性

1.5.2 图片格式说明:

image | uni-app官网 (dcloud.net.cn)

  • 当使用浏览器/webview渲染时,支持哪些图片格式由webview决定,详见
  • 当使用uvue原生渲染时支持的格式如下
    • bmp
    • gif
    • ico
    • jpg
    • png
    • webp
    • heic(Android10+支持)
    • avif
    • tif
    • svg

1.5.3 mode有效值

1.6 navigator

页面跳转。

该组件类似HTML中的<a>组件,但只能跳转本地页面。目标页面必须在pages.json中注册。

1.6.1 属性

1.6.2 使用举例

1.6.3 open-type有效值

1.7 button

1.7.1 属性

1.8 input

单行输入框。

html规范中input不仅是输入框,还有radio、checkbox、时间、日期、文件选择功能。

在uni-app规范中,input仅仅是输入框。其他功能uni-app有单独的组件或API:

input | uni-app官网 (dcloud.net.cn)

1.8.1 属性

二、uniapp里的Vue3

2.1.事件处理对应表

2.2 v-for中key的重要性

key 这个特殊的 attribute 主要作为 Vue 的虚拟 DOM 算法提示,在比较新旧节点列表时用于识别 vnode。

  • 预期number | string | symbol

  • 详细信息

    在没有 key 的情况下,Vue 将使用一种最小化元素移动的算法,并尽可能地就地更新/复用相同类型的元素。如果传了 key,则将根据 key 的变化顺序来重新排列元素,并且将始终移除/销毁 key 已经不存在的元素。

    同一个父元素下的子元素必须具有唯一的 key。重复的 key 将会导致渲染异常。

    最常见的用例是与 v-for 结合:

1
2
3
<ul>
<li v-for="item in items" :key="item.id">...</li>
</ul>

2.2.1 例子

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
<template>
<view class="out">
<view class="item" v-for="(item,index) in goods" :key="item.id">
<checkbox></checkbox>
<text class="title">{{item.name}}</text>
<text class="del" @click="remove(index)">删除</text>
</view>
</view>
</template>

<script setup>
import {ref} from "vue";
const goods = ref([
{id:11,name:"小米"},
{id:22,name:"华为"},
{id:33,name:"oppo"},
{id:44,name:"苹果"},
])

function remove(index){
goods.value.splice(index,1)
}
</script>

<style lang="scss" scoped>
.out{
padding:10px;
.item{
padding: 10px 0;
.del{
color:#c00;
margin-left:30px;
}
}
}
</style>

没有加key的情况下

删除一个选中的元素

会将下一个元素也影响

加上key则不会

2.3 表单focus和blur事件用法

input常用事件

2.3.1 例子

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
<template>
<view class="out">
<input type="text" @focus="isActive=true" @blur="isActive=false"
v-model="iptValue"
@confirm="onConfirm"
/>
<image src="../../static/chicken.gif" mode="" class="pic"
:class="isActive?'active':''"></image>
</view>

<view>预览:{{iptValue}}</view>
</template>

<script setup>
import {ref} from "vue";
const iptValue = ref("");
const isActive = ref(false);

function onConfirm(e){
console.log(e);
}
</script>

<style lang="scss" scoped>
.out{
padding:0 20px;
margin-top:40px;
position: relative;
input{
border:1px solid #ccc;
height: 40px;
position: relative;
z-index: 2;
background: #fff;
padding:0 10px;
}
.pic{
width: 24px;
height: 24px;
z-index: 1;
position: absolute;
top:0px;
left:calc(50% - 12px);
transition: top 0.3s;
}
.pic.active{
top:-24px;
}
}
</style>

2.4 v-model双向绑定的实现原理

在前端处理表单时,我们常常需要将表单输入框的内容同步给 JavaScript 中相应的变量。手动连接值绑定和更改事件监听器可能会很麻烦:

1
2
3
<input
:value="text"
@input="event => text = event.target.value">

v-model 指令帮我们简化了这一步骤:

1
<input v-model="text">

另外,v-model 还可以用于各种不同类型的输入,<textarea><select> 元素。它会根据所使用的元素自动使用对应的 DOM 属性和事件组合:

  • 文本类型的 <input><textarea> 元素会绑定 value property 并侦听 input 事件;
  • <input type="checkbox"><input type="radio"> 会绑定 checked property 并侦听 change 事件;
  • <select> 会绑定 value property 并侦听 change 事件。

2.5 计算属性用法及方法对比

2.5.1 计算属性缓存 vs 方法

你可能注意到我们在表达式中像这样调用一个函数也会获得和计算属性相同的结果:

1
<p>{{ calculateBooksMessage() }}</p>
1
2
3
4
// 组件中
function calculateBooksMessage() {
return author.books.length > 0 ? 'Yes' : 'No'
}

若我们将同样的函数定义为一个方法而不是计算属性,两种方式在结果上确实是完全相同的,然而,不同之处在于计算属性值会基于其响应式依赖被缓存。一个计算属性仅会在其响应式依赖更新时才重新计算。这意味着只要 author.books 不改变,无论多少次访问 publishedBooksMessage 都会立即返回先前的计算结果,而不用重复执行 getter 函数。

这也解释了为什么下面的计算属性永远不会更新,因为 Date.now() 并不是一个响应式依赖:

1
const now = computed(() => Date.now())

相比之下,方法调用总是会在重渲染发生时再次执行函数。

为什么需要缓存呢?想象一下我们有一个非常耗性能的计算属性 list,需要循环一个巨大的数组并做许多计算逻辑,并且可能也有其他计算属性依赖于 list。没有缓存的话,我们会重复执行非常多次 list 的 getter,然而这实际上没有必要!如果你确定不需要缓存,那么也可以使用方法调用。

2.5.2 可写计算属性

计算属性默认是只读的。当你尝试修改一个计算属性时,你会收到一个运行时警告。只在某些特殊场景中你可能才需要用到“可写”的属性,你可以通过同时提供 getter 和 setter 来创建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<script setup>
import { ref, computed } from 'vue'

const firstName = ref('John')
const lastName = ref('Doe')

const fullName = computed({
// getter
get() {
return firstName.value + ' ' + lastName.value
},
// setter
set(newValue) {
// 注意:我们这里使用的是解构赋值语法
[firstName.value, lastName.value] = newValue.split(' ')
}
})
</script>

现在当你再运行 fullName.value = 'John Doe' 时,setter 会被调用而 firstNamelastName 会随之更新。

2.6 watch知watchEffect监听的使用

2.6.1 基本示例

计算属性允许我们声明性地计算衍生值。然而在有些情况下,我们需要在状态变化时执行一些“副作用”:例如更改 DOM,或是根据异步操作的结果去修改另一处的状态。

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
<script setup>
import { ref, watch } from 'vue'

const question = ref('')
const answer = ref('Questions usually contain a question mark. ;-)')
const loading = ref(false)

// 可以直接侦听一个 ref
watch(question, async (newQuestion, oldQuestion) => {
if (newQuestion.includes('?')) {
loading.value = true
answer.value = 'Thinking...'
try {
const res = await fetch('https://yesno.wtf/api')
answer.value = (await res.json()).answer
} catch (error) {
answer.value = 'Error! Could not reach the API. ' + error
} finally {
loading.value = false
}
}
})
</script>

<template>
<p>
Ask a yes/no question:
<input v-model="question" :disabled="loading" />
</p>
<p>{{ answer }}</p>
</template>

2.6.2 侦听数据源类型

watch 的第一个参数可以是不同形式的“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const x = ref(0)
const y = ref(0)

// 单个 ref
watch(x, (newX) => {
console.log(`x is ${newX}`)
})

// getter 函数
watch(
() => x.value + y.value,
(sum) => {
console.log(`sum of x + y is: ${sum}`)
}
)

// 多个来源组成的数组
watch([x, () => y.value], ([newX, newY]) => {
console.log(`x is ${newX} and y is ${newY}`)
})

2.6.3 watchEffect()

侦听器的回调使用与源完全相同的响应式状态是很常见的。例如下面的代码,在每当 todoId 的引用发生变化时使用侦听器来加载一个远程资源:

1
2
3
4
5
6
7
8
9
10
11
12
13
const todoId = ref(1)
const data = ref(null)

watch(
todoId,
async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
},
{ immediate: true }
)

特别是注意侦听器是如何两次使用 todoId 的,一次是作为源,另一次是在回调中。

我们可以用 watchEffect 函数 来简化上面的代码。watchEffect() 允许我们自动跟踪回调的响应式依赖。上面的侦听器可以重写为:

1
2
3
4
5
6
watchEffect(async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
})

这个例子中,回调会立即执行,不需要指定 immediate: true。在执行期间,它会自动追踪 todoId.value 作为依赖(和计算属性类似)。每当 todoId.value 变化时,回调会再次执行。有了 watchEffect(),我们不再需要明确传递 todoId 作为源值。

2.6.4 watch vs. watchEffect

watchwatchEffect 都能响应式地执行有副作用的回调。它们之间的主要区别是追踪响应式依赖的方式:

  • watch 只追踪明确侦听的数据源。它不会追踪任何在回调中访问到的东西。另外,仅在数据源确实改变时才会触发回调。watch 会避免在发生副作用时追踪依赖,因此,我们能更加精确地控制回调函数的触发时机。
  • watchEffect,则会在副作用发生期间追踪依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。这更方便,而且代码往往更简洁,但有时其响应性依赖关系会不那么明确。

2.7 watch vs computed

三、组件基础

3.1 组件props

如果我们正在构建一个博客,我们可能需要一个表示博客文章的组件。我们希望所有的博客文章分享相同的视觉布局,但有不同的内容。要实现这样的效果自然必须向组件中传递数据,例如每篇文章标题和内容,这就会使用到 props。

Props 是一种特别的 attributes,你可以在组件上声明注册。要传递给博客文章组件一个标题,我们必须在组件的 props 列表上声明它。这里要用到 defineProps 宏:

1
2
3
4
5
6
7
8
<!-- BlogPost.vue -->
<script setup>
defineProps(['title'])
</script>

<template>
<h4>{{ title }}</h4>
</template>

defineProps 是一个仅 <script setup> 中可用的编译宏命令,并不需要显式地导入。声明的 props 会自动暴露给模板。defineProps 会返回一个对象,其中包含了可以传递给组件的所有 props:

1
2
const props = defineProps(['title'])
console.log(props.title)

TypeScript 用户请参考:为组件 props 标注类型

如果你没有使用 <script setup>,props 必须以 props 选项的方式声明,props 对象会作为 setup() 函数的第一个参数被传入:

1
2
3
4
5
6
export default {
props: ['title'],
setup(props) {
console.log(props.title)
}
}

一个组件可以有任意多的 props,默认情况下,所有 prop 都接受任意类型的值。

当一个 prop 被注册后,可以像这样以自定义 attribute 的形式传递数据给它:

1
2
3
<BlogPost title="My journey with Vue" />
<BlogPost title="Blogging with Vue" />
<BlogPost title="Why Vue is so fun" />

在实际应用中,我们可能在父组件中会有如下的一个博客文章数组:

1
2
3
4
5
const posts = ref([
{ id: 1, title: 'My journey with Vue' },
{ id: 2, title: 'Blogging with Vue' },
{ id: 3, title: 'Why Vue is so fun' }
])

这种情况下,我们可以使用 v-for 来渲染它们:

1
2
3
4
5
<BlogPost
v-for="post in posts"
:key="post.id"
:title="post.title"
/>

留意我们是如何使用 v-bind 来传递动态 prop 值的。当事先不知道要渲染的确切内容时,这一点特别有用。

3.2 props校验

Vue 组件可以更细致地声明对传入的 props 的校验要求。如果传入的值不满足类型要求,Vue 会在浏览器控制台中抛出警告来提醒使用者。这在开发给其他开发者使用的组件时非常有用。

要声明对 props 的校验,你可以向 defineProps() 宏提供一个带有 props 校验选项的对象,例如:

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
defineProps({
// 基础类型检查
// (给出 `null` 和 `undefined` 值则会跳过任何类型检查)
propA: Number,
// 多种可能的类型
propB: [String, Number],
// 必传,且为 String 类型
propC: {
type: String,
required: true
},
// Number 类型的默认值
propD: {
type: Number,
default: 100
},
// 对象类型的默认值
propE: {
type: Object,
// 对象或数组的默认值
// 必须从一个工厂函数返回。
// 该函数接收组件所接收到的原始 prop 作为参数。
default(rawProps) {
return { message: 'hello' }
}
},
// 自定义类型校验函数
// 在 3.4+ 中完整的 props 作为第二个参数传入
propF: {
validator(value, props) {
// The value must match one of these strings
return ['success', 'warning', 'danger'].includes(value)
}
},
// 函数类型的默认值
propG: {
type: Function,
// 不像对象或数组的默认,这不是一个工厂函数。这会是一个用来作为默认值的函数
default() {
return 'Default function'
}
}
})

defineProps() 宏中的参数不可以访问 <script setup> 中定义的其他变量,因为在编译时整个表达式都会被移到外部的函数中。

一些补充细节:

  • 所有 prop 默认都是可选的,除非声明了 required: true
  • Boolean 外的未传递的可选 prop 将会有一个默认值 undefined
  • Boolean 类型的未传递 prop 将被转换为 false。这可以通过为它设置 default 来更改——例如:设置为 default: undefined 将与非布尔类型的 prop 的行为保持一致。
  • 如果声明了 default 值,那么在 prop 的值被解析为 undefined 时,无论 prop 是未被传递还是显式指明的 undefined,都会改为 default 值。

当 prop 的校验失败后,Vue 会抛出一个控制台警告 (在开发模式下)。

3.2.1 使用举例

子组件

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
<template>
<view class="userinfo">
<image :src="obj.avatar" mode="" class="avatar"></image>
<view class="username">
{{obj.name}}
</view>


</view>
</template>

<script setup>
import {defineProps} from "vue"
defineProps({
obj:{
type:Object,
default(){
return {
name:"匿名",
avatar:"../../static/logo.png"
}
}
}
})
</script>

<style lang="scss" scoped>
.userinfo{
width: 100%;
height: 200px;
background: #ccc;
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
image{
width: 100px;
height: 100px;
border-radius: 50%;
}
.username{
padding:10px 0;
font-size: 20px;
}
}

</style>

父组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<template>
<view>
<UserInfo v-for="(item,index) in userinfo" :obj="item" :key="index">
</UserInfo>
</view>
</template>

<script setup>
import {ref,reactive} from "vue"

const userinfo = reactive([
{name:"张三",avatar:"../../static/pic1.jpg"},
{name:"李四",avatar:"../../static/pic2.jpg"},
{name:"王五",avatar:"../../static/pic3.jpg"},
])
</script>

<style>

</style>

3.3 插槽slot

在之前的章节中,我们已经了解到组件能够接收任意类型的 JavaScript 值作为 props,但组件要如何接收模板内容呢?在某些场景中,我们可能想要为子组件传递一些模板片段,让子组件在它们的组件中渲染这些片段。

3.3.1 使用举例

举例来说,这里有一个 <FancyButton> 组件,可以像这样使用:

1
2
3
<FancyButton>
Click me! <!-- 插槽内容 -->
</FancyButton>

<FancyButton> 的模板是这样的:

1
2
3
<button class="fancy-btn">
<slot></slot> <!-- 插槽出口 -->
</button>

<slot> 元素是一个插槽出口 (slot outlet),标示了父元素提供的插槽内容 (slot content) 将在哪里被渲染。

通过使用插槽,<FancyButton> 仅负责渲染外层的 <button> (以及相应的样式),而其内部的内容由父组件提供。

理解插槽的另一种方式是和下面的 JavaScript 函数作类比,其概念是类似的:

1
2
3
4
5
6
7
8
9
// 父元素传入插槽内容
FancyButton('Click me!')

// FancyButton 在自己的模板中渲染插槽内容
function FancyButton(slotContent) {
return `<button class="fancy-btn">
${slotContent}
</button>`
}

插槽内容可以是任意合法的模板内容,不局限于文本。例如我们可以传入多个元素,甚至是组件:

1
2
3
4
<FancyButton>
<span style="color:red">Click me!</span>
<AwesomeIcon name="plus" />
</FancyButton>

3.3.2 默认内容

在外部没有提供任何内容的情况下,可以为插槽指定默认内容。比如有这样一个 <SubmitButton> 组件:

1
2
3
<button type="submit">
<slot></slot>
</button>

如果我们想在父组件没有提供任何插槽内容时在 <button> 内渲染“Submit”,只需要将“Submit”写在 <slot> 标签之间来作为默认内容:

1
2
3
4
5
<button type="submit">
<slot>
Submit <!-- 默认内容 -->
</slot>
</button>

现在,当我们在父组件中使用 <SubmitButton> 且没有提供任何插槽内容时:

1
<SubmitButton />

“Submit”将会被作为默认内容渲染:

1
<button type="submit">Submit</button>

但如果我们提供了插槽内容:

1
<SubmitButton>Save</SubmitButton>

那么被显式提供的内容会取代默认内容:

1
<button type="submit">Save</button>

3.3.3 具名插槽

有时在一个组件中包含多个插槽出口是很有用的。举例来说,在一个 <BaseLayout> 组件中,有如下模板:

1
2
3
4
5
6
7
8
9
10
11
<div class="container">
<header>
<!-- 标题内容放这里 -->
</header>
<main>
<!-- 主要内容放这里 -->
</main>
<footer>
<!-- 底部内容放这里 -->
</footer>
</div>

对于这种场景,<slot> 元素可以有一个特殊的 attribute name,用来给各个插槽分配唯一的 ID,以确定每一处要渲染的内容:

1
2
3
4
5
6
7
8
9
10
11
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>

这类带 name 的插槽被称为具名插槽 (named slots)。没有提供 name<slot> 出口会隐式地命名为“default”。

在父组件中使用 <BaseLayout> 时,我们需要一种方式将多个插槽内容传入到各自目标插槽的出口。此时就需要用到具名插槽了:

要为具名插槽传入内容,我们需要使用一个含 v-slot 指令的 <template> 元素,并将目标插槽的名字传给该指令:

1
2
3
4
5
<BaseLayout>
<template v-slot:header>
<!-- header 插槽的内容放这里 -->
</template>
</BaseLayout>

v-slot 有对应的简写 #,因此 <template v-slot:header> 可以简写为 <template #header>。其意思就是“将这部分模板片段传入子组件的 header 插槽中”。

3.4 emits

3.4.1 触发与监听事件

在组件的模板表达式中,可以直接使用 $emit 方法触发自定义事件 (例如:在 v-on 的处理函数中):

1
2
<!-- MyComponent -->
<button @click="$emit('someEvent')">click me</button>

父组件可以通过 v-on (缩写为 @) 来监听事件:

1
<MyComponent @some-event="callback" />

同样,组件的事件监听器也支持 .once 修饰符:

1
<MyComponent @some-event.once="callback" />

3.4.2 事件参数

有时候我们会需要在触发事件时附带一个特定的值。举例来说,我们想要 <BlogPost> 组件来管理文本会缩放得多大。在这个场景下,我们可以给 $emit 提供一个额外的参数:

1
2
3
<button @click="$emit('increaseBy', 1)">
Increase by 1
</button>

然后我们在父组件中监听事件,我们可以先简单写一个内联的箭头函数作为监听器,此函数会接收到事件附带的参数:

1
<MyButton @increase-by="(n) => count += n" />

或者,也可以用一个组件方法来作为事件处理函数:

1
<MyButton @increase-by="increaseCount" />

该方法也会接收到事件所传递的参数:

1
2
3
function increaseCount(n) {
count.value += n
}

3.4.3 声明触发的事件

组件可以显式地通过 defineEmits() 宏来声明它要触发的事件:

1
2
3
<script setup>
defineEmits(['inFocus', 'submit'])
</script>

我们在 <template> 中使用的 $emit 方法不能在组件的 <script setup> 部分中使用,但 defineEmits() 会返回一个相同作用的函数供我们使用:

1
2
3
4
5
6
7
<script setup>
const emit = defineEmits(['inFocus', 'submit'])

function buttonClick() {
emit('submit')
}
</script>

defineEmits()不能在子函数中使用。如上所示,它必须直接放置在 <script setup> 的顶级作用域下。

如果你显式地使用了 setup 函数而不是 <script setup>,则事件需要通过 emits 选项来定义,emit 函数也被暴露在 setup() 的上下文对象上:

1
2
3
4
5
6
export default {
emits: ['inFocus', 'submit'],
setup(props, ctx) {
ctx.emit('submit')
}
}

setup() 上下文对象中的其他属性一样,emit 可以安全地被解构:

1
2
3
4
5
6
export default {
emits: ['inFocus', 'submit'],
setup(props, { emit }) {
emit('submit')
}
}

这个 emits 选项和 defineEmits() 宏还支持对象语法。通过 TypeScript 为参数指定类型,它允许我们对触发事件的参数进行验证:

1
2
3
4
5
6
7
8
<script setup lang="ts">
const emit = defineEmits({
submit(payload: { email: string, password: string }) {
// 通过返回值为 `true` 还是为 `false` 来判断
// 验证是否通过
}
})
</script>

3.5 生命周期

生命周期有多重叫法,有叫生命周期函数的,也有叫生命周期钩子的,还有钩子函数的,其实都是代表,在 Vue 实例创建、更新和销毁的不同阶段触发的一组钩子函数,这些生命周期函数允许开发者在不同阶段对 Vue 实例进行操作,以便执行特定的逻辑或清理工作。

生命周期主要包含以下四个阶段:创建、挂载、更新、销毁。

3.5.1 Vue3中的生命周期函数

  • setup()是在beforeCreate和created之前运行的,所以可以用setup代替这两个钩子函数。

  • onBeforeMount() : 已经完成了模板的编译,但是组件还未挂载到DOM上的函数。

  • onMounted() : 组件挂载到DOM完成后执行的函数。

  • onBeforeUpdate(): 组件更新之前执行的函数。

  • onUpdated(): 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该函数。

  • onBeforeUnmount(): 在组件实例被卸载之前调用。

  • onUnmounted(): 组件卸载完成后执行的函数

  • onActivated(): 若组件实例是 缓存树的一部分,当组件被插入到 DOM 中时调用。

  • onDeactivated(): 若组件实例是 缓存树的一部分,当组件从 DOM 中被移除时调用。

  • onErrorCaptured(): 在捕获了后代组件传递的错误时调用。

注意:在uniapp组件中,onBeforeUpdate、onUpdated、onActivated、onDeactivated,H5支持,小程序无法使用。

3.5.2 Vue2与Vue3的对比

3.6 页面生命周期

3.6.1 uniapp中的页面生命周期函数

uniapp页面生命周期函数与 Vue.js 的生命周期函数有所不同,因为 uni-app 是基于 Vue.js 的跨平台应用框架,因此它具有自己特定的生命周期函数。
可以在这些生命周期函数中编写相应的逻辑代码,以便在不同阶段对页面进行初始化、展示、隐藏和卸载时执行特定的操作。

在开发uniapp Vue3版本的时候,不能像vue2的选项式API一样,可以直接使用onLoad、onShow等,在组合式API中需要先从“@dcloudio/uni-app”模块中导入才可以。

1
2
3
<script setup>
import {onLoad,onReady} from "@dcloudio/uni-app"
</script>
  • onLoad:页面加载时触发,可以在此生命周期函数中进行页面初始化操作。
  • onShow:页面显示时触发,可以在此生命周期函数中进行页面展示相关的操作。
  • onReady:页面初次渲染完成时触发,可以在此生命周期函数中进行页面渲染完成后的操作。
  • onHide:页面隐藏时触发,可以在此生命周期函数中进行页面隐藏相关的操作。
  • onUnload:页面卸载时触发,可以在此生命周期函数中进行页面卸载相关的操作。

3.6.2 uniapp中组件生命周期函数和页面生命周期函数的执行顺序

不包含组件的页面
onLoad > onShow > onReady

包含组件的页面
onLoad > onShow > onBeforeMount > onReady > onMounted

3.6.3 Vue3页面及组件生命周期流程图

3.6.4 页面加载的详细流程

接下来我们介绍onLoad、onReady、onShow的先后关系,页面加载的详细流程。

  1. uni-app框架,首先根据pages.json的配置,创建页面

所以原生导航栏是最快显示的。页面背景色也应该在这里配置。

  1. 根据页面template里的组件,创建dom。

这里的dom创建仅包含第一批处理的静态dom。对于通过js/uts更新data然后通过v-for再创建的列表数据,不在第一批处理。

要注意一个页面静态dom元素过多,会影响页面加载速度。在uni-app x Android版本上,可能会阻碍页面进入的转场动画。 因为此时,页面转场动画还没有启动。

  1. 触发onLoad

此时页面还未显示,没有开始进入的转场动画,页面dom还不存在。

所以这里不能直接操作dom(可以修改data,因为vue框架会等待dom准备后再更新界面);在 app-uvue 中获取当前的activity拿到的是老页面的activity,只能通过页面栈获取activity。

onLoad比较适合的操作是:接受上页的参数,联网取数据,更新data。

手机都是多核的,uni.request或云开发联网,在子线程运行,不会干扰UI线程的入场动画,并行处理可以更快的拿到数据、渲染界面。

但onLoad里不适合进行大量同步耗时运算,因为此时转场动画还没开始。

尤其uni-app x 在 Android上,onLoad里的代码(除了联网和加载图片)默认是在UI线程运行的,大量同步耗时计算很容易卡住页面动画不启动。除非开发者显式指定在其他线程运行。

  1. 转场动画开始

新页面开始进入的转场动画,动画默认耗时300ms,可以在路由API中调节时长。

  1. 页面onReady

第2步创建dom是虚拟dom,dom创建后需要经历一段时间,UI层才能完成了页面上真实元素的创建,即触发了onReady。

onReady后,页面元素就可以自由操作了,比如ref获取节点。同时首批界面也渲染了。

注意:onReady和转场动画开始、结束之间,没有必然的先后顺序,完全取决于dom的数量和复杂度。

如果元素排版和渲染够快,转场动画刚开始就渲染好了;

大多情况下,转场动画走几格就看到了首批渲染内容;

如果元素排版和渲染过慢,转场动画结束都没有内容,就会造成白屏。

联网进程从onLoad起就在异步获取数据更新data,如果服务器速度够快,第二批数据也可能在转场动画结束前渲染。

  1. 转场动画结束

再次强调,5和6的先后顺序不一定,取决于首批dom渲染的速度。

3.6.5 使用举例

demo5.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
<template>
<view class="layout">
<demo-child ref="child"></demo-child>
<view>----</view>
<button @click="update">点击修改子值</button>
<navigator url="/pages/demo6/demo6?name=王五&age=20">跳转到demo6</navigator>
</view>
</template>

<script setup>
import {onMounted, ref} from "vue";

const child = ref(null);
const update = function(){
child.value.updateCount()
}


onMounted(()=>{
console.log(child.value);
})


</script>

<style lang="scss" scoped>

</style>

demo6.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
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
<template>
<view class="">
姓名:{{name}} - {{age}}
<scroll-view scroll-y="true" ref="scroll">
<view></view>
</scroll-view>
<navigator url="/pages/demo5/demo5">跳转demo5</navigator>
<view>----</view>
<view>计数:{{count}}</view>
<view>----</view>
<navigator open-type="reLaunch" url="/pages/demo4/demo4">demo4页面</navigator>
<view v-for="item in 50">{{item}}</view>

<view class="fixed" v-if="fixed">↑</view>

</view>
</template>

<script setup>
import {onBeforeMount, onMounted, ref} from "vue"
import {onLoad,onReady,onShow,onHide,onUnload,onPageScroll} from "@dcloudio/uni-app"
const name = ref("张三")
const age = ref(18)
const scroll = ref(null)
const count = ref(0)
const fixed = ref(false);

let time= setInterval(()=>{
count.value++
},50)


onLoad((e)=>{
console.log("onload函数");
console.log(e);
name.value = e.name
age.value = e.age
})

onShow(()=>{
console.log("onShow函数");
time= setInterval(()=>{
count.value++
},50)
})

onHide(()=>{
console.log("onHide函数");
clearInterval(time)
})


onReady((e)=>{
console.log("onReady函数");
})

onBeforeMount(()=>{
console.log("onBeforeMount函数");
})

onMounted(()=>{
console.log("onMounted函数");
})


onUnload(()=>{
console.log("onUnload卸载页面");
})


onPageScroll((e)=>{
console.log(e.scrollTop);
fixed.value = e.scrollTop>200

})


</script>

<style lang="scss" scoped>
.fixed{
width: 100px;
height: 100px;
background: orange;
position: fixed;
right:30px;
bottom:30px;
}
</style>

四、uniapp全局配置

4.1 响应式单位rpx及搭配使用UI产品工具

uni-app 支持的通用 css 单位包括 px、rpx

  • px 即屏幕像素
  • rpx 即响应式 px,一种根据屏幕宽度自适应的动态单位。以 750 宽的屏幕为基准,750rpx 恰好为屏幕宽度。屏幕变宽,rpx 实际显示效果会等比放大,但在 App(vue2 不含 nvue) 端和 H5(vue2) 端屏幕宽度达到 960px 时,默认将按照 375px 的屏幕宽度进行计算,具体配置参考:rpx 计算配置

vue 页面支持下面这些普通 H5 单位,但在 nvue 里不支持:

  • rem 根字体大小可以通过 page-meta 配置
  • vh viewpoint height,视窗高度,1vh 等于视窗高度的 1%
  • vw viewpoint width,视窗宽度,1vw 等于视窗宽度的 1%

下面对 rpx 详细说明:

设计师在提供设计图时,一般只提供一个分辨率的图。

严格按设计图标注的 px 做开发,在不同宽度的手机上界面很容易变形。

设计稿 1px 与框架样式 1rpx 转换公式如下:

1
设计稿 1px / 设计稿基准宽度 = 框架样式 1rpx / 750rpx

换言之,页面元素宽度在 uni-app 中的宽度计算公式:

1
750 * 元素在设计稿中的宽度 / 设计稿基准宽度

举例说明:

  1. 若设计稿宽度为 750px,元素 A 在设计稿上的宽度为 100px,那么元素 A 在 uni-app 里面的宽度应该设为:750 * 100 / 750,结果为:100rpx。
  2. 若设计稿宽度为 640px,元素 A 在设计稿上的宽度为 100px,那么元素 A 在 uni-app 里面的宽度应该设为:750 * 100 / 640,结果为:117rpx。
  3. 若设计稿宽度为 375px,元素 B 在设计稿上的宽度为 200px,那么元素 B 在 uni-app 里面的宽度应该设为:750 * 200 / 375,结果为:400rpx。

4.2 @import导入css样式及scss变星用法与static目录

4.2.1 样式导入

4.2.2 uni.scss

uni.scss是一个特殊文件,在代码中无需 import 这个文件即可在scss代码中使用这里的样式变量。uni-app的编译器在webpack配置中特殊处理了这个uni.scss,使得每个scss文件都被注入这个uni.scss,达到全局可用的效果。

uni.scss 是为了方便整体控制应用的风格。比如按钮颜色、边框风格,uni.scss 文件里预置了一批scss变量预置。

如果开发者想要less、stylus的全局使用,需要在vue.config.js中自行配置webpack策略。

注意:

  1. 如要使用这些常用变量,需要在 HBuilderX 里面安装 scss 插件;
  2. 使用时需要在 style 节点上加上 lang="scss"
1
2
<style lang="scss">
</style>

以下是 uni.scss 的相关变量:

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
58
59
60
61
62
63
/* 颜色变量 */

/* 行为相关颜色 */
$uni-color-primary: #007aff;
$uni-color-success: #4cd964;
$uni-color-warning: #f0ad4e;
$uni-color-error: #dd524d;

/* 文字基本颜色 */
$uni-text-color:#333;//基本色
$uni-text-color-inverse:#fff;//反色
$uni-text-color-grey:#999;//辅助灰色,如加载更多的提示信息
$uni-text-color-placeholder: #808080;
$uni-text-color-disable:#c0c0c0;

/* 背景颜色 */
$uni-bg-color:#ffffff;
$uni-bg-color-grey:#f8f8f8;
$uni-bg-color-hover:#f1f1f1;//点击状态颜色
$uni-bg-color-mask:rgba(0, 0, 0, 0.4);//遮罩颜色

/* 边框颜色 */
$uni-border-color:#c8c7cc;

/* 尺寸变量 */

/* 文字尺寸 */
$uni-font-size-sm:24rpx;
$uni-font-size-base:28rpx;
$uni-font-size-lg:32rpx;

/* 图片尺寸 */
$uni-img-size-sm:40rpx;
$uni-img-size-base:52rpx;
$uni-img-size-lg:80rpx;

/* Border Radius */
$uni-border-radius-sm: 4rpx;
$uni-border-radius-base: 6rpx;
$uni-border-radius-lg: 12rpx;
$uni-border-radius-circle: 50%;

/* 水平间距 */
$uni-spacing-row-sm: 10px;
$uni-spacing-row-base: 20rpx;
$uni-spacing-row-lg: 30rpx;

/* 垂直间距 */
$uni-spacing-col-sm: 8rpx;
$uni-spacing-col-base: 16rpx;
$uni-spacing-col-lg: 24rpx;

/* 透明度 */
$uni-opacity-disabled: 0.3; // 组件禁用态的透明度

/* 文章场景相关 */
$uni-color-title: #2C405A; // 文章标题颜色
$uni-font-size-title:40rpx;
$uni-color-subtitle: #555555; // 二级标题颜色
$uni-font-size-subtitle:36rpx;
$uni-color-paragraph: #3F536E; // 文章段落颜色
$uni-font-size-paragraph:30rpx;

4.3 pages.json全局配置页面路由globalStyle的属性

实现下拉刷新和上拉触底事件的页面生命周期:

4.4 pages可以独立设置页面路径及窗口表现

4.5 tabBar设置底部菜单选项及iconfont图标

在iconfont自己下载导入图标

代码示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
"tabBar": {
"color": "#7A7E83",
"selectedColor": "#3cc51f",
"borderStyle": "black",
"backgroundColor": "#ffffff",
"list": [{
"pagePath": "pages/component/index",
"iconPath": "static/image/icon_component.png",
"selectedIconPath": "static/image/icon_component_HL.png",
"text": "组件"
}, {
"pagePath": "pages/API/index",
"iconPath": "static/image/icon_API.png",
"selectedIconPath": "static/image/icon_API_HL.png",
"text": "接口"
}]
}

4.6 manifest.json是应用配置文件

4.7 注册微信小程序appid

4.8 自动引入插件

vite.config中安装插件unplugin–auto-import

开发uniapp使用Vue3组合式API版本,如何实现从vue模块中自动导入_hbuilder创建uniapp 如何自动导入unplugin-auto-import-CSDN博客

五、uniapp API

5.1 uni.showToast

代码示例:

1
2
3
4
uni.showToast({
title: '标题',
duration: 2000
});

5.2 uni.hideToast()

隐藏消息提示框。

1
uni.hideToast();

5.3 uni.showLoading加载

5.4 uni.showModalt模态框

5.5 uni.setNavigationBarTitle

5.6 uni.showNavigationBarLoading

5.7uni.setNavigationBarColor

5.8 uni.hideNavigationBarLoading

5.9 uni.hideHomeButton

5.10 uni.setTabBarItem

1
2
3
4
5
6
7
uni.setTabBarItem({
index: 0,
text: 'text',
iconPath: '/path/to/iconPath',
selectedIconPath: '/path/to/selectedIconPath'
})

5.11 uni.setTabBarStyle

5.12 uni.hideTabBar

5.13 uni.showTabBar

5.14 uni.setTabBarBadge

5.15 uni.removeTabBarBadge

5.16 uni.showTabBarRedDot

5.17 uni.hideTabBarRedDot

5.18 uni.navigateTo

5.19 uni.redirectTo

5.20 uni.reLaunch

5.21 uni.navigateBack

5.22 uni.setStorage

5.23 uni.setStorageSync

5.24 uni.getStorage

5.25 uni.getStorageSync

5.26 uni.getStorageInfo

5.27 uni.getStorageInfoSync

5.28 uni.removeStorage

5.29 uni.removeStorageSync

5.30 uni.clearStorage

5.31 uni.clearStorageSync

5.32 uni.request

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
uni.request({
url: 'https://www.example.com/request', //仅为示例,并非真实接口地址。
data: {
text: 'uni.request'
},
header: {
'custom-header': 'hello' //自定义请求头信息
},
success: (res) => {
console.log(res.data);
this.text = 'request success';
}

});

一般不建议用success的方式回调,因为可能造成回调地狱,推荐使用then或者async和await方法实现:

六、萌宠案例练习

6.1 页面布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<template>
<view class="container">
<view class="layout">
<view class="box" v-for="(item,index) in pets" :key="item._id">
<view class="pic">
<image :src="item.url" mode="widthFix"></image>
</view>
<view class="text">
{{item.content}}
</view>
<view class="author">
--{{item.author}}
</view>
</view>
</view>
</view>
</template>

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
<style lang="scss" scoped>
.container{
.layout{
padding: 50rpx;
.box{
margin-bottom: 60rpx;
box-shadow: 0 10rpx 40rpx rgba(0, 0, 0, 0.08);
border-radius: 15rpx;
overflow: hidden;
.pic{
image{
width: 100%;
}
}
.text{
padding:30rpx;
color:#333;
font-size: 36rpx;
}
.author{
padding:0 30rpx 30rpx;
text-align: right;
color:#888;
font-size: 28rpx;
}
}
}
}
</style>

6.2 调用萌宠API接口渲染到页面中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function getpets(){
uni.request({
url:'https://tea.qingnian8.com/tools/petShow',
header:{
'access-key':"jackeylove"
},
data:{
size:10,
}
}).then((res)=>{
pets.value = res.data.data
})
}

getpets();

6.3 previewlmage图片预览和lazyload懒加载

1
2
3
4
<view class="pic">
<image lazy-load :src="item.url" mode="widthFix" @click="onPreview(index)"></image>
//图片懒加载 + 传图片索引参数到图片预览函数
</view>
1
2
3
4
5
6
7
8
const onPreview = function(index){
let urls = pets.value.map(item=>item.url);
//映射成路径列表
uni.previewImage({
current:index,
urls
})
}

6.4 对回调结果严格处理then catch finally

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
function getpets(){
uni.showNavigationBarLoading();
uni.request({
url:'https://tea.qingnian8.com/tools/petShow',
header:{
'access-key':"jackeylove"
},
data:{
size:10,
}
}).then((res)=>{

if(res.data.errCode === 0 ){
pets.value = [...pets.value,...res.data.data]
}
else if(res.data.errCode === 400){
uni.showToast({
title:res.data.errMsg,
icon:'none'
})
}

}).catch(err =>{
uni.showToast({
title:"请求有误,请重新刷新",
icon:"none"
})

}).finally(()=>{
uni.hideNavigationBarLoading();
})
}

6.5 完成下拉刷新和触底加载更多

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//导入
import {onReachBottom,onPullDownRefresh} from "@dcloudio/uni-app"

finally(()=>{
uni.hideNavigationBarLoading();
//停止下拉刷新
uni.stopPullDownRefresh();
})

//触底生命钩子
onReachBottom(()=>{
getpets();
})
//下拉刷新生命钩子
onPullDownRefresh(()=>{
pets.value = [];
getpets();
})

6.6 pageScrollTo滚动到]顶部和刷新

1
2
3
4
5
6
7
8
<view class="float">
<view class="item" @click="onRefresh">
<uni-icons type="refreshempty" size="26" color="#888"></uni-icons>
</view>
<view class="item" @click="onTop">
<uni-icons type="arrow-up" size="26" color="#888"></uni-icons>
</view>
</view>
1
2
3
4
5
6
7
8
9
10
11
12
//点击刷新
const onRefresh = function(){
uni.startPullDownRefresh();
}

//返回顶部
const onTop = ()=>{
uni.pageScrollTo({
scrollTop:0,
duration:100
})
}

6.7 使用uni-ui扩展组件

6.8 分段器组件实现点击切换萌宠类型

1
2
3
4
<view class="menu">
<uni-segmented-control :current="current" :values="values" @clickItem="onClickItem" styleType="button"
activeColor="#2B9939"></uni-segmented-control>
</view>
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
import {
ref,computed
} from 'vue'

const current = ref(0);
const classify = [{key:"all",value:"全部"},{key:"dog",value:"狗狗"},{key:"cat",value:"猫猫"}]
const values= computed(()=>classify.map(item=>item.value))


//点击菜单
const onClickItem=(e)=>{
current.value = e.currentIndex
pets.value = []
getpets();
}
//发送网络请求
function getpets() {
uni.showNavigationBarLoading();
uni.request({
url: 'https://tea.qingnian8.com/tools/petShow',
header: {
'access-key': "jackeylove"
},
data: {
size: 5,
type:classify[current.value].key
}
})

//下拉刷新
onPullDownRefresh(() => {
pets.value = [];
current.value = 0;
getpets();
})
1
2
3
.menu{
padding:50rpx 50rpx 0;
}

6.9 案例总结

案例效果:

感受:感觉uniapp开发十分舒爽,api很强大,与web端相似但又些许差别,不过整体思路没有太大变化。