# 组件

我们可以对一段要复用的js/uts逻辑代码进行封装,抽出function、module等形式。

那么涉及UI的复用时,该如何抽象?

这就是vue的组件机制,把视图template、script、style都封装到独立的uvue组件文件中,在其他需要的地方使用组件的名称进行引用。

每个组件,包括如下几个部分:以组件名称为标记的开始标签和结束标签、组件text内容、组件属性、组件属性值。

组件还可以封装方法、事件、插槽,提供了组件的生命周期,提供了组件和页面的互通信机制,满足了各种高级需求。

如果您还不了解这些概念,请务必先阅读 组件概述文档

# 组件内容构成

uni-app x 组件基于 vue 单文件组件规范,一个组件内,有 3 个根节点标签:

  • <template>:组件的模板内容
  • <script>:组件的脚本代码
  • <style>:组件的样式

# 和页面的区别

组件的内容构成和页面大体上一致,都符合vue的sfc规范。

事实上,一个在pages.json注册的页面uvue文件,也可以被当做一个组件引入到其他页面。

组件和页面的差别有:

  1. 组件中不支持页面相关的生命周期和API,比如 onLoadonShow页面生命周期,比如$setPageStyle等API。
  2. 组件中有一批组件独有的生命周期和API,比如 mountedunmounted组件生命周期,比如页面和组件通信的API。
  3. 组件文件不需要在pages.json中注册

# 创建及引用组件

# 创建组件

# easycom

  1. 项目根目录/components 目录上右键(如果没有,在根目录新建一个 components 目录即可),选择 新建组件,输入组件名称,选择一个模板;可勾选创建同名目录,将组件放在同名目录下。
  2. 项目根目录/uni_modules 目录上右键(如果没有,在根目录新建一个 uni_modules 目录即可),选择 新建uni_modules插件,输入插件ID,分类选择前端组件-通用组件;将组件放在和插件ID同名的目录下。

# 创建自定义组件

  1. 在项目 pages 目录 下的任意地方创建 .uvue/.vue 文件并编写组件代码

注意事项

uni-app x 项目支持使用 .vue.uvue 文件作为组件使用,但同文件名的两个文件同时存在,.uvue 文件会优先编译。

# 引用组件

# easycom

传统vue组件,需要安装、引用、注册,三个步骤后才能使用组件。easycom 将其精简为一步。

只要组件安装在项目的 components 目录下或 uni_modules/插件 id/components/插件 id/插件 id.uvue 目录下,并符合 组件名称/组件名称.(vue|uvue) 目录结构。就可以不用引用、注册,直接在页面中使用。

  • 比如 uni-loading,它导入到项目后,存放在了目录 /uni_modules/uni-loading/components/uni-loading/uni-loading.uvue

    同时它的组件名称也叫 uni-loading,所以这样的组件,不用在 script 里注册和引用。如下:

    <template>
        <view>
          <uni-loading></uni-loading><!-- 这里会显示一个loading -->
        </view>
      </template>
    <script>
      // 这里不用import引入,也不需要在components内注册组件。template里就可以直接用
      // ...
    </script>
    

这里出现了uni_module的概念,简单说下,它是uni-app的一种包管理方案。

uni_module其实不止服务于组件,它可以容纳组件、script库、页面、项目等所有DCloud插件市场所支持的种类。

在HBuilderX中点右键可方便的更新插件,插件作者也可以方便的上传插件。

uni_module有详细的专项文档,请另行查阅uni_module规范

如果你的组件不满足easycom标准的目录规范,还有一种办法是在pages.json里声明自己的目录规则,以便编译器查找到你的组件。自定义easycom路径规则的详细教程详见

# easycom组件的类型规范

组件标签名首字母大写,驼峰+ComponentPublicInstance,如:

<test/> 类型为:TestComponentPublicInstance <uni-data-checkbox/> 类型为:UniDataCheckboxComponentPublicInstance

# 手动引入组件

不符合 easycom 规范的组件,则需要手动引入:

<!-- 组件 child.vue -->
<template>
  <view>Child Component</view>
</template>

<!-- 页面(与 child.vue 组件在同级目录 -->
<template>
  <view>
    <child ref="component1"></child>
  </view>
</template>
<script setup lang="uts">
// 引入 child 组件
import child from './child.vue'

const component1 = ref<ComponentPublicInstance | null>(null) // 手动引入组件时的类型
</script>
# 手动引入组件的类型规范

类型为:ComponentPublicInstance

# 使用及通信

# 页面与页面通信

  1. 使用 navigateTourl 地址中携带参数
  2. 使用 event-bus

# 页面与组件通信

# 向组件传递 props

示例 详情

注意

  • 选项式 API:this.$propsMap 类型,需要使用 this.$props["propName"] 来访问
  • 组合式 API:可以使用 . 点操作符来访问
  • 默认情况下,父组件传递的,但没有被子组件解析为 props 的 attributes 绑定会被“透传”。这意味着当我们有一个单根节点的子组件时,这些绑定会被作为一个常规的 attribute 应用在子组件的根节点元素上,可以通过 inheritAttrs 选项来关闭该行为,详见

组合式 API

选项式 API

<template>
  <view class="page">
    <array-literal :str="str" :num="num" :bool="bool" :obj="obj" :arr="arr" />
    <object-type str="str" :num="num" :bool="bool" :obj="obj" :arr="arr" />
    <same-name-prop-default-value />
    <props-with-defaults />
    <!-- #ifdef APP-ANDROID -->
    <reference-types :list="[1,2,3]" />
    <!-- #endif -->
    <!-- #ifndef APP-ANDROID -->
    <reference-types :list="['a','b','c']" />
    <!-- #endif -->
  </view>
</template>

<script setup lang="uts">
  import ArrayLiteral from './array-literal-composition.uvue'
  import ObjectType from "./object-type-composition.uvue";
  import SameNamePropDefaultValue from "./same-name-prop-default-value-composition.uvue";
  import PropsWithDefaults from "./props-with-defaults.uvue";
  import ReferenceTypes from './reference-types-composition.uvue'

  const str = 'str'
  const num = 10
  const bool = true
  const obj = { age: 18 }
  const arr = ['a', 'b', 'c']
</script>

# 向组件传递回调函数

示例 详情

组合式 API

选项式 API

<template>
  <view class="page">
    <view class="row">
      <text>子组件传的参数</text>
      <text id="value">
        {{ value }}
      </text>
    </view>
    <child @callback="callback"></child>
  </view>
</template>

<script setup lang="uts">
import child from './child-composition.uvue'

const value = ref('')

const callback = (str: string) => {
  value.value = str
}
</script>

<style>
.row {
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  margin-bottom: 10px;
}
</style>

# 使用 provide/inject 来向下传递参数

示例 详情

组合式 API

选项式 API

<template>
  <view class="page">
    <inject-comp />
  </view>
</template>

<script setup lang="uts">
import InjectComp from '../inject/inject-composition.uvue';

provide('msg', 'hello');
provide('num', 0);
provide('obj', { a: 1 });
provide('arr', [1, 2, 3]);
provide('fn', () : string => 'hello');
</script>

# 使用 全局变量与状态管理

store/index.uts 文件详情

示例 详情

组合式 API

选项式 API

<template>
  <view class="page">
    <view class="flex-row">
      父组件:
      <text class="parent-msg">{{ msg }}</text>
    </view>
    <button class="parent-btn" @click="change">父组件改变数据</button>
    <child />
  </view>
</template>

<script setup lang="uts">
import child from './components/child.uvue'
import { setComponentMsg, state } from '@/store/index.uts'

const msg = computed<number>((): number => state.componentMsg)

const change = () => {
  setComponentMsg(state.componentMsg + 1)
}
</script>

#main.uts 中使用 app.config.globalProperties

如在 main.uts 中的 createApp 方法中使用:

app.config.globalProperties.globalPropertiesReactiveObj = reactive({
  str: 'default reactive string',
  num: 0,
  bool: false,
} as UTSJSONObject)

示例 详情

组合式 API

选项式 API

<template>
  <!-- #ifdef APP -->
  <scroll-view style="flex: 1;">
    <!-- #endif -->
    <view class="uni-padding-wrap">
      <text class="mt-10"
        >globalProperties string: {{ globalPropertiesStr }}</text
      >
      <text class="mt-10"
        >globalProperties number: {{ globalPropertiesNum }}</text
      >
      <text class="mt-10"
        >globalProperties boolean: {{ globalPropertiesBool }}</text
      >
      <text class="mt-10"
        >globalProperties object: {{ globalPropertiesObj }}</text
      >
      <text class="mt-10"
        >globalProperties null: {{ globalPropertiesNull }}</text
      >
      <text class="mt-10"
        >globalProperties array: {{ globalPropertiesArr }}</text
      >
      <text class="mt-10"
        >globalProperties set: {{ globalPropertiesSet }}</text
      >
      <text class="mt-10"
        >globalProperties map: {{ globalPropertiesMap }}</text
      >
      <text class="mt-10"
        >globalProperties reactiveObj.str:
        {{ globalPropertiesReactiveObj['str'] }}</text
      >
      <text class="mt-10"
        >globalProperties reactiveObj.num:
        {{ globalPropertiesReactiveObj['num'] }}</text
      >
      <text class="mt-10"
        >globalProperties reactiveObj.boolean:
        {{ globalPropertiesReactiveObj['bool'] }}</text
      >
      <text class="mt-10"
        >globalProperties fun 返回值: {{ globalPropertiesFn() }}</text
      >
      <button @click="updateGlobalProperties" class="mt-10">
        update globalProperties
      </button>
    </view>
    <!-- #ifdef APP -->
  </scroll-view>
  <!-- #endif -->
</template>

<script setup lang="uts">
type MyGlobalProperties = {
	str : string;
	num : number;
	bool : boolean;
	obj : UTSJSONObject;
	null : string | null;
	arr : number[];
	set : string[];
	map : UTSJSONObject;
	reactiveObj : UTSJSONObject;
  globalPropertiesFnRes: string;
}

const myGlobalProperties = reactive<MyGlobalProperties>({
	str: '',
	num: 0,
	bool: false,
	obj: {},
	null: null,
	arr: [] as number[],
	set: [] as string[],
	map: {},
	reactiveObj: {
		str: '',
		num: 0,
		bool: false,
	},
  globalPropertiesFnRes: '',
} as MyGlobalProperties)

const instance = getCurrentInstance()!.proxy!
const getGlobalProperties = () => {
	myGlobalProperties.str = instance.globalPropertiesStr
	myGlobalProperties.num = instance.globalPropertiesNum
	myGlobalProperties.bool = instance.globalPropertiesBool
	myGlobalProperties.obj = instance.globalPropertiesObj
	myGlobalProperties.null = instance.globalPropertiesNull
	myGlobalProperties.arr = instance.globalPropertiesArr
	myGlobalProperties.set = []
	instance.globalPropertiesSet.forEach(item => {
		myGlobalProperties.set.push(item)
	})
	myGlobalProperties.map = {}
	instance.globalPropertiesMap.forEach((value: number, key: string) => {
		myGlobalProperties.map[key] = value
	})
	myGlobalProperties.reactiveObj = instance.globalPropertiesReactiveObj
	myGlobalProperties.globalPropertiesFnRes = instance.globalPropertiesFn()
}

setTimeout(() => {
  // 等待 globalProperties-options resetGlobalProperties 完成
	getGlobalProperties()
}, 1000)

const updateGlobalProperties = () => {
	instance.globalPropertiesStr = 'new string'
	instance.globalPropertiesNum = 100
	instance.globalPropertiesBool = true
	instance.globalPropertiesObj = {
		str: 'new globalProperties obj string',
		num: 100,
		bool: true,
	}
	instance.globalPropertiesNull = 'not null'
	instance.globalPropertiesArr = [1, 2, 3]
	instance.globalPropertiesSet = new Set(['a', 'b', 'c'])
	instance.globalPropertiesMap = new Map([['a', 1], ['b', 2], ['c', 3]])
	instance.globalPropertiesReactiveObj['str'] = 'new reactive string'
	instance.globalPropertiesReactiveObj['num'] = 200
	instance.globalPropertiesReactiveObj['bool'] = true
	getGlobalProperties()
}

defineExpose({
  myGlobalProperties,
  updateGlobalProperties
})
</script>

<style>
.uni-padding-wrap {
  padding: 10px 10px 40px 10px;
}
</style>

# 父组件与子组件通信

上述 页面与组件通信 方法同样适用于父组件与子组件通信。

# 页面调用组件方法

# 调用 easycom 组件方法

在调用组件方法的时候如报错 error: Reference has a nullable type 则需要使用 ?. 操作符(如:a?.b?.())。

easycom组件,用法和内置组件一样。也是使用 this.$refs 获取组件并转换为组件的类型,通过 .操作符 调用组件方法或设置属性。

语法

(this.$refs['组件ref属性值'] as 驼峰ComponentPublicInstance)?.foo?.();

示例 详情

组合式 API

选项式 API

<template>
  <view>
    <call-easy-method ref="callEasyMethod1"></call-easy-method>
    <custom-call-easy-method ref="customCallEasyMethod1"></custom-call-easy-method>
    <test-getter-setter-composition ref="getterAndSetterComposition"></test-getter-setter-composition>
    <test-getter-setter-options ref="getterAndSetterOptions"></test-getter-setter-options>
    <view>
        <text>getter-setter: <text id="getterAndSetter">{{JSON.stringify(getterAndSetterResult)}}</text></text>
    </view>
  </view>
</template>

<script setup lang="uts">
const callEasyMethod1 = ref<CallEasyMethodComponentPublicInstance | null>(null)
const customCallEasyMethod1 = ref<CustomCallEasyMethodComponentPublicInstance | null>(null)

const callMethod = () => {
  // 调用组件的 foo 方法
  customCallEasyMethod1.value?.foo?.()
}

const callMethod1 = () => {
  // 调用组件的 foo1 方法
  callEasyMethod1.value?.foo1?.()
}

const callMethod2 = () => {
  // 调用组件的 foo2 方法并传递 1个参数
  callEasyMethod1.value?.foo2?.(Date.now())
}

const callMethod3 = () => {
  // 调用组件的 foo3 方法并传递 2个参数
  callEasyMethod1.value?.foo3?.(Date.now(), Date.now())
}

const callMethod4 = () => {
  // 调用组件的 foo4 方法并传递 callback
  callEasyMethod1.value?.foo4?.(() => {
    console.log('callback')
  })
}

const callMethod5 = () => {
  // 注意: 返回值可能为 null,当前例子一定不为空,所以加了 !
  const result = callEasyMethod1.value?.foo5?.('string1') as string
  console.log(result) // string1
}

const callMethodTest = (text: string): string | null => {
  const result = callEasyMethod1.value?.foo5?.(text) as string
  return result
}

const callCustomMethodTest = (): string | null => {
  const result = customCallEasyMethod1.value?.foo?.() as string
  return result
}

const getterAndSetterComposition = ref<TestGetterSetterCompositionComponentPublicInstance | null>(null)
const getterAndSetterOptions = ref<TestGetterSetterOptionsComponentPublicInstance | null>(null)
const getterAndSetterResult = ref<number[]>([])

const callGetterAndSetterCompositionGetCount = (): number => {
    return getterAndSetterComposition.value!.getCount()
}
const callGetterAndSetterCompositionGetCountWithCallMethod = (): number => {
    return getterAndSetterComposition.value!.$callMethod('getCount') as number
}
const callGetterAndSetterOptionsGetCount = (): number => {
    return getterAndSetterOptions.value!.getCount()
}
const callGetterAndSetterOptionsGetCountWithCallMethod = (): number => {
    return getterAndSetterOptions.value!.$callMethod('getCount') as number
}
const callGetterAndSetterCompositionSetCount = (count: number) => {
    getterAndSetterComposition.value!.setCount(count)
}
const callGetterAndSetterCompositionSetCountWithCallMethod = (count: number) => {
    getterAndSetterComposition.value!.$callMethod('setCount', count)
}
const callGetterAndSetterOptionsSetCount = (count: number) => {
    getterAndSetterOptions.value!.setCount(count)
}
const callGetterAndSetterOptionsSetCountWithCallMethod = (count: number) => {
    getterAndSetterOptions.value!.$callMethod('setCount', count)
}

const callGetterAndSetter = (): number[] => {
    const result: number[] = []
    callGetterAndSetterCompositionSetCount(1)
    result.push(callGetterAndSetterCompositionGetCount())
    callGetterAndSetterCompositionSetCountWithCallMethod(2)
    result.push(callGetterAndSetterCompositionGetCountWithCallMethod())
    callGetterAndSetterOptionsSetCount(3)
    result.push(callGetterAndSetterOptionsGetCount())
    callGetterAndSetterOptionsSetCountWithCallMethod(4)
    result.push(callGetterAndSetterOptionsGetCountWithCallMethod())
    getterAndSetterResult.value = result
    return result
}
const delay = (): Promise<string> =>
  new Promise((resolve, _) => {
    setTimeout(() => {
      resolve('')
    }, 1000)
  })

const call = async (): Promise<void> => {
  callGetterAndSetter()
  callMethod()
  callMethod1()
  await delay()
  callMethod2()
  await delay()
  callMethod3()
  await delay()
  callMethod4()
  await delay()
  callMethod5()
}

onReady(() => {
  call()
})

defineExpose({
  callMethodTest,
  callCustomMethodTest
})
</script>
# 调用 uni_modules easycom 组件方法 HBuilderX 3.97+

使用 ref 属性拿到组件实例,调用 easycom 组件方法时不需要使用 $callMethod 方法,直接使用点操作符即可 .

在调用组件方法的时候如报错 error: Reference has a nullable type 则需要使用 ?. 操作符(如:a?.b?.())。

示例 详情

组合式 API

选项式 API

<template>
  <view>
    <call-easy-method-uni-modules ref="callEasyMethod1"></call-easy-method-uni-modules>
    <!-- #ifdef APP-ANDROID || APP-IOS || APP-HARMONY -->
    <!-- 在兼容组件中 ios 返回非普通对象数据,比如 Map 数据时候会被 jscore 统一处理为 object,和安卓产生了差异 -->
    <!-- 测试用例用来验证返回数据特殊,安卓和其他平台无此限制 -->
    <view>---</view>
    <test-props id="btn1" :numList="numList" :objList='objList' @buttonclick='onButtonClick'
      @numListChange='numListChange' @objListChange='objListChange'
      style="width: 80px;height: 30px;background-color: lightblue"></test-props>
    <view style="flex-direction: row ;">
      <text>isNumListValid: </text>
      <text id='isNumListValid'>{{isNumListValid}}</text>
    </view>
    <view style="flex-direction: row ;">
      <text>isObjListValid: </text>
      <text id='isObjListValid'>{{isObjListValid}}</text>
    </view>
    <!-- #endif -->
  </view>
</template>

<script setup lang="uts">
  import { testInOtherFile } from './call-method-easycom-uni-modules'

  // #ifdef APP-ANDROID || APP-IOS
  import { PropsChangeEvent } from '@/uni_modules/test-props'
  // #endif

  const delay = () : Promise<string> =>
    new Promise((resolve, _) => {
      setTimeout(() => {
        resolve('')
      }, 1000)
    })

  const callEasyMethod1 = ref<CallEasyMethodUniModulesComponentPublicInstance | null>(null)

  const numList = ref<number[]>([1])   // 传递 props
  const objList = ref<any[]>([])
  const isNumListValid = ref(false)
  const isObjListValid = ref(false)


  const callMethod1 = () => {
    // 调用组件的 foo1 方法
    callEasyMethod1.value?.foo1?.()
  }
  const callMethod2 = () => {
    // 调用组件的 foo2 方法并传递 1个参数
    callEasyMethod1.value?.foo2?.(Date.now())
  }
  const callMethod3 = () => {
    // 调用组件的 foo3 方法并传递 2个参数
    callEasyMethod1.value?.foo3?.(Date.now(), Date.now())
  }
  const callMethod4 = () => {
    // 调用组件的 foo4 方法并传递 callback
    callEasyMethod1.value?.foo4?.(() => {
      console.log('callback')
    })
  }
  const callMethod5 = () => {
    // 注意: 返回值可能为 null,当前例子一定不为空,所以加了 !
    const result = callEasyMethod1.value?.foo5?.('string5') as string
    console.log(result) // string1
  }
  const callMethodTest = (text : string) : string | null => {
    const result = callEasyMethod1.value?.foo5?.(text) as string
    return result
  }
  const callMethodInOtherFile = (text : string) : string => {
    return testInOtherFile(callEasyMethod1.value!, text)
  }

  // #ifdef APP-ANDROID
  const numListChange = (res : Map<string, Map<string, any>>) => {
    const value = res['detail']!['value'] as number[]
    const isArray = Array.isArray(value)
    const isLengthGt0 = value.length > 0
    isNumListValid.value = isArray && isLengthGt0
  }
  // #endif

  // #ifdef APP-IOS || APP-HARMONY
  const numListChange = (res : any) => {
    const value = res['detail']!['value'] as number[]
    const isArray = Array.isArray(value)
    const isLengthGt0 = value.length > 0
    isNumListValid.value = isArray && isLengthGt0
  }
  // #endif


  // #ifdef APP-ANDROID
  const objListChange = (res : Map<string, Map<string, any>>) => {
    const value = res['detail']!['value'] as any[]
    const isArray = Array.isArray(value)
    const isLengthGt0 = value.length > 0
    isObjListValid.value = isArray && isLengthGt0
  }
  // #endif

  // #ifdef APP-IOS || APP-HARMONY
  const objListChange = (res : any) => {
    const value = res['detail']!['value'] as any[]
    const isArray = Array.isArray(value)
    const isLengthGt0 = value.length > 0
    isObjListValid.value = isArray && isLengthGt0
  }
  // #endif


  const onButtonClick = () => {
    // 改变 props: 观察 props 返回值为非响应式值
    numList.value = [3, 2, 1]
    objList.value = [{ id: '3' }, { id: '4' }]
  }


  const call = async () : Promise<void> => {
    callMethod1()
    await delay()
    callMethod2()
    await delay()
    callMethod3()
    await delay()
    callMethod4()
    await delay()
    callMethod5()
  }

  onReady(() => {
    call()
  })

  defineExpose({
    callMethodTest,
    callMethodInOtherFile,
    onButtonClick
  })
</script>

# 使用 ref 属性搭配 $callMethod 方法

如果不是内置组件,也不是easycom组件,那么无法使用.操作符了。

此时需使用 this.$refs 获取组件实例,然后通过 $callMethod 调用组件的方法。也就是把组件的方法名、参数,当做callMethod的参数来传递。此时也就没有.操作符那样的代码提示和校验了。

callMethod可用于所有自定义组件,包括easycom组件也可以使用,只不过easycom组件有更简单的用法。

语法

(this.$refs['组件ref属性值'] as ComponentPublicInstance)?.$callMethod('方法名', ...args)

组件类型

ComponentPublicInstance

示例 详情

组合式 API

选项式 API

<template>
  <view class="page">
    <child ref="childRef" />
  </view>
</template>

<script setup lang="uts">
  import Child from './child-composition.uvue'

  const childRef = ref<ComponentPublicInstance | null>(null)
  const str = ref('parent str')
  const num = ref(1)

  const getNum = () : number => { return num.value }

  const instance = getCurrentInstance()!.proxy!

  const callMethodByChild = () : number => {
    const childComponent = instance.$refs['childRef'] as ComponentPublicInstance
    return childComponent.$parent!.$callMethod('getNum') as number
  }

  defineExpose({
    str,
    getNum,
    callMethodByChild
  })
</script>

注意:

  • App-Android 平台 4.0 版本开始支持 $callMethod 调用 defineExpose 导出的方法
  • Web 平台、App-iOS 平台 4.13 版本开始支持 $callMethod 调用 defineExpose 导出的方法
  • 小程序 平台 支持 $callMethod 调用 defineExpose 导出的方法
  • <script setup>下的变量名不能和 easycom 组件的驼峰写法重复,比如:组件名为test-canvas,那么不能在<script setup>中定义const testCanvas变量名。
  • $callMethod 调用性能低于easycom组件的强类型调用,如果遇到高频调用场景,建议使用easycom组件的强类型调用方法。

# 内置组件的方法调用或设置属性 HBuilderX 3.93+

使用 this.$refs 获取组件并as转换为组件对应的element类型,通过 .操作符 调用组件方法或设置属性。

语法

(this.$refs['组件ref属性值'] as Uni[xxx]Element)?.foo?.();

内置组件的element类型规范

Uni组件名(驼峰)Element

如:

<button>: UniButtonElement <picker-view>: UniPickerViewElement

示例 详情

组合式 API

选项式 API

<template>
  <view>
    <slider :show-value="true" ref="sliderRef"></slider>
  </view>
</template>

<script setup lang="uts">
const sliderRef = ref<UniSliderElement | null>(null)

onReady(() => {
  sliderRef.value!.value = 80
})

const callMethodTest = (text: string): string | null => {
  sliderRef.value?.setAttribute('str', text)
  const result = sliderRef.value?.getAttribute('str') as string
  return result
}

defineExpose({
  callMethodTest
})
</script>

bug&tips

  • 目前uts组件,即封装原生ui给uni-app或uni-app x的页面中使用,类型与内置组件的 Uni组件名(驼峰)Element 方式相同。目前没有代码提示。

# 组件监听应用、页面生命周期

选项式 API 和 组合式 API 在监听页面生命周期时有所不同

比如选项式 API 中的 onShowonHide 监听页面生命周期在组合式 API 中分别对应 onPageShowonPageHide(在组合式 API 时会和 App 的生命周期冲突)

具体请查看 页面生命周期

组件中监听应用生命周期 Android HarmonyOS iOS Web 微信小程序
onAppHide 4.11 x x 4.11 x
onAppShow 4.11 x x 4.11 x

注意

onPageHideonPageShow 需要写在选项式的 setup 函数或者组合式 <script setup> 中才能生效

示例 详情

组合式 API

选项式 API

<script lang="uts" setup>
// #ifdef APP-ANDROID
onAppHide(() => {
	console.log('组件监听应用生命周期 => onAppHide')
})
onAppShow((onShowOptions: OnShowOptions) => {
	console.log('组件监听应用生命周期 => onAppShow => onShowOptions', onShowOptions)
})
// #endif

onPageShow(() => {
	console.log('组件监听页面生命周期 => onPageShow')
})
onPageHide(() => {
	console.log('组件监听页面生命周期 => onPageHide')
})
</script>

<template>组件监听页面、应用生命周期(组合式 API)</template>

# 组件的生命周期

# 组件生命周期(选项式 API)兼容性

Web 微信小程序 Android iOS HarmonyOS 描述
beforeCreate 4.0 4.41 3.9 4.11 4.61 在组件实例初始化完成之后立即调用。
在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。
created 4.0 4.41 3.9 4.11 4.61 在组件实例处理完所有与状态相关的选项后调用。
在这一步,实例已完成以下的配置:数据观测 (data observer),属性和方法的运算,watch/event 事件回调。
然而,此时挂载阶段还未开始,因此 $el 属性仍不可用。
beforeMount 4.0 4.41 3.9 4.11 4.61 在组件被挂载之前调用。
相关的 render 函数首次被调用。
当这个钩子被调用时,组件已经完成了其响应式状态的设置,但还没有创建 DOM 节点。
它即将首次执行 DOM 渲染过程。
mounted 4.0 4.41 3.9 4.11 4.61 在组件被挂载之后调用。
el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。
如果 root 实例挂载了一个文档内元素,当 mounted 被调用时 vm.$el 也在文档内。
beforeUpdate 4.0 4.41 3.9 4.11 4.61 在组件即将因为一个响应式状态变更而更新其 DOM 树之前调用。
数据更新时调用,发生在虚拟 DOM 打补丁之前。
这里适合在更新之前访问现有的 DOM,比如手动移除已添加的事件监听器。
updated 4.0 4.41 3.9 4.11 4.61 在组件因为一个响应式状态变更而更新其 DOM 树之后调用。
父组件的更新钩子将在其子组件的更新钩子之后调用。
这个钩子会在组件的任意 DOM 更新后被调用,这些更新可能是由不同的状态变更导致的。
如果你需要在某个特定的状态更改后访问更新后的 DOM,请使用 nextTick() 作为替代。
beforeUnmount 4.0 4.41 3.9 4.11 4.61 在一个组件实例被卸载之前调用。
当这个钩子被调用时,组件实例依然还保有全部的功能。
unmounted 4.0 4.41 3.9 4.11 4.61 在一个组件实例被卸载之后调用。
可以在这个钩子中手动清理一些副作用,例如计时器、DOM 事件监听器或者与服务器的连接。
errorCaptured 4.0 4.41 x x x 在捕获了后代组件传递的错误时调用。
这个钩子带有三个实参:错误对象、触发该错误的组件实例,以及一个说明错误来源类型的信息字符串。
这个钩子可以通过返回 false 来阻止错误继续向上传递。
renderTracked 4.0 4.41 x x x 在一个响应式依赖被组件的渲染作用追踪后调用。
跟踪虚拟 DOM 重新渲染时调用。钩子接收 debugger event 作为参数。
此事件告诉你哪个操作跟踪了组件以及该操作的目标对象和键。
renderTriggered 4.0 4.41 x x x 在一个响应式依赖被组件触发了重新渲染之后调用。
当虚拟 DOM 重新渲染为 triggered.Similarly 为renderTracked,接收 debugger event 作为参数。
此事件告诉你是什么操作触发了重新渲染,以及该操作的目标对象和键。
activated 4.0 x 4.0 4.11 4.61 若组件实例是 <KeepAlive> 缓存树的一部分,当组件被插入到 DOM 中时调用。
keep-alive 组件激活时调用。
deactivated 4.0 x 4.0 4.11 4.61 若组件实例是 <KeepAlive> 缓存树的一部分,当组件从 DOM 中被移除时调用。
keep-alive 组件停用时调用。
serverPrefetch x x x x x 当组件实例在服务器上被渲染之前要完成的异步函数。
如果这个钩子返回了一个 Promise,服务端渲染会在渲染该组件前等待该 Promise 完成。

# 组件生命周期(组合式 API)兼容性

Web 微信小程序 Android iOS HarmonyOS HarmonyOS(Vapor) 描述
onMounted() 4.0 4.41 4.0 4.11 4.61 - el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。
如果 root 实例挂载了一个文档内元素,当 mounted 被调用时 vm.$el 也在文档内。
onUpdated() 4.0 4.41 4.0 4.11 4.61 - 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。
onUnmounted() 4.0 4.41 4.0 4.11 4.61 - 在一个组件实例被卸载之后调用。
onBeforeMount() 4.0 4.41 4.0 4.11 4.61 - 在挂载开始之前被调用:相关的 render 函数首次被调用。
onBeforeUpdate() 4.0 4.41 4.0 4.11 4.61 - 数据更新时调用,发生在虚拟 DOM 打补丁之前。
这里适合在更新之前访问现有的 DOM,比如手动移除已添加的事件监听器。
onBeforeUnmount() 4.0 4.41 4.0 4.11 4.61 - 在一个组件实例被卸载之前调用。
onErrorCaptured() x - x x x - 注册一个钩子,在捕获了后代组件传递的错误时调用。
onRenderTracked() x - x x x - 注册一个调试钩子,当组件渲染过程中追踪到响应式依赖时调用。
onRenderTriggered() x - x x x - 注册一个调试钩子,当响应式依赖的变更触发了组件渲染时调用。
onActivated() 4.0 x x x x - keep-alive 组件激活时调用。
onDeactivated() 4.0 x x x x - keep-alive 组件停用时调用。
onServerPrefetch() x x x x x - 注册一个异步函数,在组件实例在服务器上被渲染之前调用。
如果这个钩子返回了一个 Promise,服务端渲染会在渲染该组件前等待该 Promise 完成。
这个钩子仅会在服务端渲染中执行,可以用于执行一些仅存在于服务端的数据抓取过程。
onRecycle() x x x x x 5.0 组件回收时的生命周期钩子
onReuse() x x x x x 5.0 组件复用时的生命周期钩子

示例 详情

组合式 API

选项式 API

<template>
  title: {{ title }}
  <button class="component-lifecycle-btn mt-10" @click="updateTitle">
    updateTitle
  </button>
</template>

<script setup lang='uts'>
import { state, setLifeCycleNum } from '@/store/index.uts'

const title = ref('component for composition API lifecycle test')

const emit = defineEmits<{
  (e : 'updateIsScroll', val : boolean) : void
}>()

onLoad((_ : OnLoadOptions) => {
  // 自动化测试
  setLifeCycleNum(state.lifeCycleNum + 100)
})
onPageShow(() => {
  // 自动化测试
  setLifeCycleNum(state.lifeCycleNum + 10)
})
onReady(() => {
  // 自动化测试
  // TODO: onReady 未触发
  setLifeCycleNum(state.lifeCycleNum + 10)
})

onPullDownRefresh(() => {
  // 自动化测试
  setLifeCycleNum(state.lifeCycleNum + 10)
})
onPageScroll((_) => {
  // 自动化测试
  emit('updateIsScroll', true)
})
onReachBottom(() => {
  // 自动化测试
  setLifeCycleNum(state.lifeCycleNum + 10)
})
onBackPress((_ : OnBackPressOptions) : boolean | null => {
  // 自动化测试
  setLifeCycleNum(state.lifeCycleNum - 10)
  return null
})
onPageHide(() => {
  // 自动化测试
  setLifeCycleNum(state.lifeCycleNum - 10)
})
onUnload(() => {
  // 自动化测试
  setLifeCycleNum(state.lifeCycleNum - 100)
})

onBeforeMount(() => {
  // 自动化测试
  setLifeCycleNum(state.lifeCycleNum + 1)
  console.log('component for lifecycle test onBeforeMount')
})

onMounted(() => {
  // 自动化测试
  setLifeCycleNum(state.lifeCycleNum + 1)
  console.log('component for lifecycle test mounted')
})

onBeforeUpdate(() => {
  // 自动化测试
  setLifeCycleNum(state.lifeCycleNum + 1)
  console.log('component for lifecycle test beforeUpdate')
})

onUpdated(() => {
  // 自动化测试
  setLifeCycleNum(state.lifeCycleNum + 1)
  console.log('component for lifecycle test updated')
})

onBeforeUnmount(() => {
  // 自动化测试
  setLifeCycleNum(state.lifeCycleNum - 1)
  console.log('component for lifecycle test beforeUnmount')
})

onUnmounted(() => {
  // 自动化测试
  setLifeCycleNum(state.lifeCycleNum - 1)
  console.log('component for lifecycle test unmounted')
})

onActivated(() => {
  // 自动化测试
  setLifeCycleNum(state.lifeCycleNum + 1)
  console.log('component for lifecycle test onActivated')
})
onDeactivated(() => {
  // 自动化测试
  setLifeCycleNum(state.lifeCycleNum - 1)
  console.log('component for lifecycle test onDeactivated')
})

const updateTitle = () => {
  title.value = 'component for lifecycle test updated'
}
</script>

# 全局组件

详情见 app.component

# props

  • 支持对象方式声明。从 4.0+ 支持字符串数组方式声明。使用字符串数组方式声明时,所有 prop 类型均为 any | null。
  • 仅支持直接在 export default 内部声明,不支持其他位置定义后,在 export default 中引用。
  • 复杂数据类型需要通过 PropType 标记类型,详见
  • type 不支持使用自定义的构造函数。

示例 详情

组合式 API

选项式 API

<template>
  <view class="page">
    <array-literal :str="str" :num="num" :bool="bool" :obj="obj" :arr="arr" />
    <object-type str="str" :num="num" :bool="bool" :obj="obj" :arr="arr" />
    <same-name-prop-default-value />
    <props-with-defaults />
    <!-- #ifdef APP-ANDROID -->
    <reference-types :list="[1,2,3]" />
    <!-- #endif -->
    <!-- #ifndef APP-ANDROID -->
    <reference-types :list="['a','b','c']" />
    <!-- #endif -->
  </view>
</template>

<script setup lang="uts">
  import ArrayLiteral from './array-literal-composition.uvue'
  import ObjectType from "./object-type-composition.uvue";
  import SameNamePropDefaultValue from "./same-name-prop-default-value-composition.uvue";
  import PropsWithDefaults from "./props-with-defaults.uvue";
  import ReferenceTypes from './reference-types-composition.uvue'

  const str = 'str'
  const num = 10
  const bool = true
  const obj = { age: 18 }
  const arr = ['a', 'b', 'c']
</script>

# 透传 Attributes​

# Attributes 继承​

“透传 attribute”指的是传递给一个组件,却没有被该组件声明为 propsemits 的 attribute 或者 v-on 事件监听器。最常见的例子就是 classstyleid

当一个组件以单个元素为根作渲染时,透传的 attribute 会自动被添加到根元素上。举例来说,假如我们有一个 <MyButton> 组件,它的模板长这样:

<!-- <MyButton> 的模板 -->
<button>Click Me</button>

一个父组件使用了这个组件,并且传入了 class

<MyButton class="large" />

最后渲染出的 DOM 结果是:

<button class="large">Click Me</button>

这里,<MyButton> 并没有将 class 声明为一个它所接受的 prop,所以 class 被视作透传 attribute,自动透传到了 <MyButton> 的根元素上。

# 对 class 和 style 的合并​

如果一个子组件的根元素已经有了 classstyle attribute,它会和从父组件上继承的值合并。如果我们将之前的 <MyButton> 组件的模板改成这样:

<!-- <MyButton> 的模板 -->
<button class="btn">Click Me</button>

则最后渲染出的 DOM 结果会变成:

<button class="btn large">Click Me</button>

# v-on 监听器继承​

同样的规则也适用于 v-on 事件监听器:

<MyButton @click="onClick" />

click 监听器会被添加到 <MyButton> 的根元素,即那个原生的 <button> 元素之上。当原生的 <button> 被点击,会触发父组件的 onClick 方法。同样的,如果原生 button 元素自身也通过 v-on 绑定了一个事件监听器,则这个监听器和从父组件继承的监听器都会被触发。

# 深层组件继承​

有些情况下一个组件会在根节点上渲染另一个组件。例如,我们重构一下 <MyButton>,让它在根节点上渲染 <BaseButton>

<!-- <MyButton/> 的模板,只是渲染另一个组件 -->
<BaseButton />

此时 <MyButton> 接收的透传 attribute 会直接继续传给 <BaseButton>

请注意:

  1. 透传的 attribute 不会包含 <MyButton> 上声明过的 props 或是针对 emits 声明事件的 v-on 侦听函数,换句话说,声明过的 props 和侦听函数被 <MyButton>“消费”了。
  2. 透传的 attribute 若符合声明,也可以作为 props 传入 <BaseButton>

# 禁用 Attributes 继承​

如果你不想要一个组件自动地继承 attribute,你可以在组件选项中设置 inheritAttrs: false

你也可以直接在 <script setup> 中使用 defineOptions

<script setup>
defineOptions({
  inheritAttrs: false
})
// ...setup 逻辑
</script>

最常见的需要禁用 attribute 继承的场景就是 attribute 需要应用在根节点以外的其他元素上。通过设置 inheritAttrs 选项为 false,你可以完全控制透传进来的 attribute 被如何使用。

这些透传进来的 attribute 可以在模板的表达式中直接用 $attrs 访问到。

<span>Fallthrough attribute: {{ $attrs }}</span>

这个 $attrs 对象包含了除组件所声明的 propsemits 之外的所有其他 attribute,例如 classstylev-on 监听器等等。

有几点需要注意:

  • props 有所不同,透传 attributes 在 JavaScript 中保留了它们原始的大小写,所以像 foo-bar 这样的一个 attribute 需要通过 $attrs['foo-bar'] 来访问。
  • @click 这样的一个 v-on 事件监听器将在此对象下被暴露为一个函数 $attrs.onClick

# 多根节点的 Attributes 继承​

和单根节点组件有所不同,有着多个根节点的组件没有自动 attribute 透传行为。如果 $attrs 没有被显式绑定,将会抛出一个运行时警告。

<CustomLayout id="custom-layout" @click="changeValue" />

如果 <CustomLayout> 有下面这样的多根节点模板,由于 Vue 不知道要将 attribute 透传到哪里,所以会抛出一个警告。

<header>...</header>
<main>...</main>
<footer>...</footer>

如果 $attrs 被显式绑定,则不会有警告:

<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>

# ref

uni-app js 引擎版中,非 Web端 只能用于获取自定义组件,不能用于获取内置组件实例(如:viewtext)。
uni-app x 中,内置组件会返回组件根节点的引用,自定义组件会返回组件实例。

注意事项:

  • 如果多个节点或自定义组件绑定相同 ref 属性,将获取到最后一个节点或组件实例的引用。
  • v-for 循环时,绑定 ref 属性会获取到节点或组件实例的集合。
  • uni-app x 中,要访问 $refs 中的属性,需要使用索引方式。

示例 详情

uni-app x(组合式)

uni-app x(选项式)

uni-app js 引擎版

<template>
  <view class="page">
    <view class="row">
      <text>NodeRef: </text>
      <text ref="nodeRef">{{ dataInfo.existRef }}</text>
    </view>
    <view class="row">
      <text>childRef:</text>
      <text>{{ dataInfo.existChildRef }}</text>
    </view>
    <view class="row">
      <text>existTextItems:</text>
      <text>{{ dataInfo.existTextItems }}</text>
    </view>
    <view>
      <text v-for="item in dataInfo.textItemsExpectNum" ref="textItems" :key="item">{{
        item
      }}</text>
    </view>
    <child class="mt-10" ref="childRef">ComponentRef</child>
  </view>
</template>

<script setup lang="uts">
import Child from './child-composition.uvue'

type DataInfo = {
  existRef: boolean
  existChildRef: boolean
  textItemsExpectNum: number
  existTextItems: boolean
}

const dataInfo = reactive<DataInfo>({
  existRef: false,
  existChildRef: false,
  textItemsExpectNum: 3,
  existTextItems: false
})

const nodeRef = ref<UniElement | null>(null)
const childRef = ref<UniElement | null>(null)
const textItems = ref<UniElement[] | null>(null)

onReady(() => {
  dataInfo.existRef = nodeRef.value !== null
  dataInfo.existChildRef = childRef.value !== null
  dataInfo.existTextItems = textItems.value?.length === dataInfo.textItemsExpectNum
})

defineExpose({
  dataInfo
})
</script>

<style>
.row {
  display: flex;
  flex-direction: row;
  justify-content: space-between;
  margin-bottom: 10px;
}
</style>

# 自定义组件 v-model 绑定复杂表达式

自定义组件 v-model 绑定复杂表达式时,需要通过 as 指定类型(仅App-Android 平台)。

组合式 API

选项式 API

<template>
  <input v-model="obj.str as string" />
</template>

<script setup lang="uts">
  type Obj = {
    str: string
  }
  const obj = reactive({
      str: "str"
    } as Obj)
</script>

# 作用域插槽的类型

示例 详情

作用域插槽需在组件中指定插槽数据类型

组合式 API

选项式 API

<template>
  <view class="container">
    <view>
      <slot name="header" msg="Here might be a page title"></slot>
    </view>
    <view>
      <slot msg="A paragraph for the main content."></slot>
    </view>
    <view>
      <slot name="footer" msg="Here's some contact info"></slot>
    </view>
  </view>
</template>

<script setup lang="uts">
defineOptions({
  name: 'child'
})

defineSlots<{
  default(props: { msg: string }): any
  header(props: { msg: string }): any
  footer(props: { msg: string }): any
}>()
</script>

# 递归组件

实现递归组件时不要使用组件 import 自身的写法,直接在模板内使用组件名即可。

<!-- component-a.uvue -->
<template>
  <view>
    <text>component-a::{{text}}</text>
    <component-a v-if="!end" :text="text" :limit="limit-1"></component-a>
  </view>
</template>

<script setup lang="uts">
  // import componentA from './component-a' // 错误用法
  defineOptions({
    name: "component-a"
  })
  const props = defineProps({
    text: {
      type: String,
      default: ''
    },
    limit: {
      type: Number,
      default: 2
    }
  })
  const end = computed(() : boolean => {
    return props.limit <= 0
  })
</script>