简体中文
HBuilderX4.31 及以上版本支持
本文重点在于讲述如何在app-android和app-ios上,使用vue组件开发规范封装原生UI封装为 uni-app x 项目使用的UTS组件,供使用者在uvue页面template中以组件的方式调用。
主要思路是将app平台的原生view关联内置 native-view 组件,实现UTS组件的特定功能及UI展示。
继续阅读文档前,开发者需要了解以下前置条件:
标准模式
和 uni-app兼容模式
的差异,详情参考uts组件开发概述
┌─components // vue组件代码
| ├─xxx // vue组件名称文件夹 xxx代替组件名称
| ├─xxx.uvue // vue组件uts代码 xxx代替组件名称
├─static // 静态资源
├─utssdk
│ ├─app-android //Android平台目录
│ │ ├─assets //Android原生assets资源目录,可选
│ │ ├─libs //Android原生库目录,可选
│ │ ├─res //Android原生res资源目录,可选
│ │ ├─AndroidManifest.xml //Android原生应用清单文件,可选
│ │ ├─config.json //Android原生配置文件
│ │ ├─index.uts //Android原生插件能力实现,可选
│ ├─app-ios //iOS平台目录
│ │ ├─Frameworks //iOS原生依赖的第三方 framework 依赖库存放目录,可选
│ │ ├─Resources //iOS原生所依赖的资源文件存放目录,可选
│ │ ├─info.plist //iOS原生所需要添加到主 info.plist 文件中的配置文件,可选
│ │ ├─UTS.entitlements //iOS原生所需要添加到主工程 .entitlements 文件中的配置文件,可选
│ │ ├─config.json //iOS原生配置文件
│ │ ├─index.uts //iOS原生插件能力实现,可选
│ ├─web //web平台目录
│ │ └─index.uts
│ └─index.uts // 跨平台插件能力实现,可选
└─package.json // 插件清单文件
如上所示,UTS插件-标准模式组件目录结构与UTS插件基本相同,差别在于components目录,vue组件代码存放components目录下。
其他目录文件详情可参考UTS插件文档
也就是说:在components/xxx/xxx.uvue这个uvue组件文件中,定义组件的属性、事件、方法;然后这些属性、方法的具体实现,则调用在utssdk下的uts插件实现。
下面我们以一个例子来讲解标准模式组件的开发。
我们开发一个native-button
组件,目标是把Android和iOS的原生button封装成uvue组件。
Android的原生button会带有水波纹效果。uni-app自带的button组件并没有这个效果。
该组件计划提供一个 text属性,用于显示按钮的文字;有一个buttonTap事件用于处理点击。
let's go!
HBuilder X 选中你的项目,项目根目录选中uni_modules
目录,右键选择新建uni_modules
插件,弹窗后分类选择 “UTS插件-标准模式组件”,填写组件名称,以下均以 native-button
为例
创建完毕 HBuilder X 会自动创建一批模版文件,参考:前述目录结构
上一步创建标准模式组件(名称为native-button)后,HBuilder X 会自动创建components/native-button/native-button.uvue文件。
这个文件是一个标准的uvue组件,符合easycom规范,在这个文件编写组件的属性、事件。
首先在该文件的template区添加 native-view 组件
<template>
<native-view></native-view>
</template>
native-view 组件的用途就是提供一个占位view,并且可以和原生的view进行绑定。
native-view 组件初始化会触发 @init 事件,如下代码在init时创建原生NativeButton对象,在其内部实现了view的绑定。
NativeButton是在utssdk目录的app-android和app-ios目录下的index.uts中定义的原生对象。NativeButton对象内部处理原生view与native-view绑定关联业务。
native-button.uvue代码中用NativeButton对象调用插件相关的API。
native-view 组件在uts中对应着UniNativeViewElement对象,将 UniNativeViewElement 传递给NativeButton对象,进行关联绑定。
组合式 API
选项式 API
<template>
<native-view @init="onviewinit"></native-view>
</template>
<script setup lang="uts">
//引入 NativeButton 原生对象
import { NativeButton } from "@/uni_modules/native-button";
let button : NativeButton | null = null
//native-view初始化时触发此方法
function onviewinit(e : UniNativeViewInitEvent) {
//获取UniNativeViewElement 传递给NativeButton对象
button = new NativeButton(e.detail.element);
}
</script>
vue中,为组件定义方法很简单,但分选项式和组合式。
选项式中,在 methods 节点中添加updateText方法;组合式中,直接定义function updateText。参考vue文档页面调用vue组件方法
native-button组件使用者可调用该updateText方法中更新native-button文字。
但在vue组件的updateText方法中,需再次调用NativeButton的updateText方法,在原生插件中实现按钮文字更新。
组合式 API
选项式 API
<script setup lang="uts">
//引入 NativeButton 原生对象
import { NativeButton } from "@/uni_modules/native-button";
let button : NativeButton | null = null
//声明方法
function updateText(value : string) {
button?.updateText(value)
}
</script>
native-button 声明组件属性props,例如native-button的文案信息text属性,按vue组件规范监听到text属性更新,通过NativeButton对象驱动更新原生view属性,在components/native-button/native-button.uvue编写如下代码,具体参考vue文档vue组件Props规范
组合式 API
选项式 API
<script setup lang="uts">
//声明属性
const props = defineProps<{ text : string }>()
//声明方法
function updateText(value : string) {
button?.updateText(value)
}
//监听属性变化
watchEffect(() => {
// console.log("watchEffect "+props.text)
const text = props.text
updateText(text)
})
</script>
native-button 声明事件,例如原生组件触发点击事件@buttonTap, NativeButton对象通过 UniNativeViewElement 的 dispatchEvent 函数触发native-view的 @customClick 自定义事件。native-button.uvue监听native-view的 @customClick 自定义事件实现emit触发声明事件,具体参考vue组件事件规范
组合式 API
选项式 API
<template>
<native-view @customClick="ontap"></native-view>
</template>
<script setup lang="uts">
//声明事件
const emit = defineEmits<{
(e : "buttonTap", event : UniNativeViewEvent) : void
}>()
function ontap(e : UniNativeViewEvent) {
emit("buttonTap", e)
}
</script>
注意:
目前自定义事件参数仅支持UniNativeViewEvent
native-button/components/native-button/native-button.uvue 最终代码如下:
组合式 API
选项式 API
<template>
<native-view @init="onviewinit" @customClick="ontap"></native-view>
</template>
<script setup lang="uts">
import { NativeButton } from "@/uni_modules/native-button";
let button : NativeButton | null = null
//声明属性
const props = defineProps<{ text : string }>()
//声明事件
const emit = defineEmits<{
(e : "buttonTap", event : UniNativeViewEvent) : void
}>()
//声明方法
function updateText(value : string) {
button?.updateText(value)
}
//监听属性变化
watchEffect(() => {
const text = props.text
updateText(text)
})
//native-view初始化时触发此方法
function onviewinit(e : UniNativeViewInitEvent) {
//获取UniNativeViewElement 传递给NativeButton对象
button = new NativeButton(e.detail.element);
updateText(props.text)
}
function ontap(e : UniNativeViewEvent) {
emit("buttonTap", e)
}
function onUnmounted() {
// iOS平台需要主动释放 uts 实例
button?.destroy()
}
</script>
utssdk目录实现不同平台的原生NativeButton对象,构造参数获取UniNativeViewElement对象与原生view绑定,封装原生view功能关联的API。
Android
iOS
import { Button } from "android.widget"
export class NativeButton {
$element : UniNativeViewElement;
constructor(element : UniNativeViewElement) {
//接收传递过来的UniNativeViewElement
this.$element = element;
this.bindView();
}
button : Button | null = null;
bindView() {
//通过UniElement.getAndroidActivity()获取android平台activity 用于创建view的上下文
this.button = new Button(this.$element.getAndroidActivity()!); //构建原生view
//限制原生Button 文案描述不自动大写
this.button?.setAllCaps(false)
//监听原生Button点击事件
this.button?.setOnClickListener(_ => {
const detail = {}
//构建自定义UniNativeViewEvent返回对象
const event = new UniNativeViewEvent("customClick", detail)
//触发原生Button的点击事件
this.$element.dispatchEvent(event)
})
//UniNativeViewEvent 绑定 安卓原生view
this.$element.bindAndroidView(this.button!);
}
updateText(text: string) {
//更新原生Button 文案描述
this.button?.setText(text)
}
destroy(){
//数据回收
}
}
更多实现可参考 标准模式组件 native-button
此时一个简单的UTS插件-标准模式组件就完成了,
注意:
以 native-button 为例, 创建标准模式组件的项目页面可以直接使用 native-button 标签,也可将native-button插件包放置其他项目的uni-modules文件夹中。项目页面即可使用 native-button 标签
<template>
<view style="flex:1">
<native-button class="native-button" text="buttonText" @buttonTap="ontap"></native-button>
</view>
</template>
<script>
export default {
methods: {
ontap(e : UniNativeViewEvent) {
console.log("ontap----------"+e.type)
}
}
}
</script>
<style>
.native-button {
height: 100px;
width: 200px;
margin: 25px auto 25px auto;
}
</style>
在hello uni-app x中,有native-button的完整示例。集成native-button的页面在pages/component/native-view/native-view.uvue,native-button组件在uni_modules/native-button/components/native-button/中。
某些情况下,我们的组件需要对外提供能力,比如:
Video组件需要提供控制视频播放状态的能力
Map组件需要提供添加/查询标记点的能力
这类场景下,我们建议开发者以上下文(Context)的形式对外提供能力,方便调用者使用和管理
下面我们以 Native-Button
为例,进行说明:
我们会给 Native-Button
组件创建一个 NativeButtonContext
类型,组件的使用者可以通过使用createNativeButtonContext
得每个组件实例对应的上下文对象。后续开发者可以通过操作NativeButtonContext
实现对 Native-Button
原生组件的操作和管理
在 uni_modules\utssdk\interface.uts
文件中添加如下代码:
/**
* 原生组件的上下文对象
*/
export interface INativeButtonContext {
/**
* 更新文本示例
*/
updateText(text : string) : void
}
export type CreateNativeButtonContext = (
id : string,
component ?: ComponentPublicInstance | null
) => INativeButtonContext | null;
上面的代码中, 我们声明了一个INativeButtonContext
接口,提供了更新文本内容的方法 updateText
接下来,在 uni_modules/utssdk/app-android/index.uts
文件中添加如下代码:
import { INativeButtonContext } from "../interface.uts"
class NativeButtonContext implements INativeButtonContext {
private button : Button | null = null
constructor(button : Button) {
this.button = button
}
updateText(text : string) {
this.button?.setText(text)
}
}
在 uni_modules/utssdk/app-ios/index.uts
文件中添加如下代码:
import { INativeButtonContext, CreateNativeButtonContext } from "../interface.uts"
class NativeButtonContext implements INativeButtonContext {
btn: UIButton | null
constructor(element : UniNativeViewElement) {
let view = element.getIOSView()
if (view != null) {
this.btn = view as UIButton
}
}
updateText(title: string) : void {
this.btn?.setTitle(title, for = UIControl.State.normal)
}
}
上面的代码创建了对应的实现类 NativeButtonContext
,实现了更新文本内容的功能,需要注意在构建上下文对象时,要将具体待操作的原生对象作为入参传入。
需要特别注意: native-button.uvue
文件的 节点下,仅建议存在一个节点:即 如果你的native-view 不存在兄弟节点,则直接以native-view为根节点
<template>
<native-view style="height: 100px;" @init="onviewinit" @customClick="ontap"></native-view>
</template>
如果 native-view 存在兄弟节点,则需要在外层包裹一个view 作为根节点
<template>
<view >
<native-view style="height: 100px;" @init="onviewinit" @customClick="ontap"></native-view>
<view style="width: 50%;height: 100px;">
<button>测试按钮</button>
</view>
</view>
</template>
如果一定需要 native-view 存在兄弟节点 且不能包裹父容器,即 多个根节点的情况。 这种情况建议使用v-bind 标签 解决
在 uni_modules\utssdk\app-android\index.uts
文件中添加如下代码:
export function createNativeButtonContext(id : string, ins : ComponentPublicInstance | null = null) : INativeButtonContext | null {
if (ins == null) {
const pages = getCurrentPages()
if (pages.length > 0) {
const page = pages[pages.length - 1]
const rootViewElement = page.getElementById(id)
if (rootViewElement != null) {
/**
* 找到了root节点,递归检索目标 native-view
*/
const nativeViewElement = iterateElement(rootViewElement)
if (nativeViewElement != null) {
return new NativeButtonContext(nativeViewElement.getAndroidView()! as Button)
}
}
}
} else {
/**
* 尝试迭代遍历
*/
if (ins.$el != null) {
const nativeViewElement = iterateElement(ins.$el as UniElement)
if (nativeViewElement != null) {
return new NativeButtonContext(nativeViewElement.getAndroidView()! as Button)
}
}
}
return null
}
/**
* 递归查询
*/
function iterateElement(homeElement:UniElement):UniElement | null{
if("NATIVE-VIEW" == homeElement.nodeName){
return homeElement
}
for(perChildEle of homeElement.children){
let findEle = iterateElement(perChildEle)
if(findEle != null){
return findEle
}
}
return null
}
在 uni_modules\utssdk\app-ios\index.uts
文件中添加如下代码:
export const createNativeButtonContext: CreateNativeButtonContext = function (id : string, component ?: ComponentPublicInstance | null) : INativeButtonContext | null {
let element : UniNativeViewElement | null = null;
let e: UniElement | null = null;
if (component == null) {
e = uni.getElementById(id)
} else {
e = component?.$el as UniElement | null;
}
if (e instanceof UniNativeViewElement) {
element = e as UniNativeViewElement | null
}else {
element = getNativeViewElemet(e)
}
if (element == null) return null;
return new NativeButtonContext(element!);
}
/**
* 递归查询
*/
function getNativeViewElemet(element: UniElement | null): UniNativeViewElement | null {
if (element == null) {
return null;
}
if (element instanceof UniNativeViewElement) {
return element as UniNativeViewElement
}
for (item in element!.children) {
let tmp = getNativeViewElemet(item)
if (tmp != null) {
return tmp
}
}
return null
}
上面的代码中需要两个入参:
组件id 也就是 native-button 设置的id属性,用来实现在 页面中定位当前组件,必填
ComponentPublicInstance 可选,在自定义组件中使用标准组件能力时需要传入此对象
特别注意:
上述 iOS 平台代码中
component?.$el
的用法需要 HBuilder X 4.44及以上版本才能使用。
在uvue页面中使用组件上下文
<template>
<view>
<page-head :title="title"></page-head>
<view class="uni-btn-v uni-common-mt">
<button type="primary" @tap="testCallMethod">调用组件方法</button>
<native-button id="helloView" class="native-button" text="buttonText" @buttonTap="ontap"></native-button>
<Login></Login>
</view>
</view>
</template>
<script>
<template>
<view>
<page-head :title="title"></page-head>
<view class="uni-btn-v uni-common-mt">
<button type="primary" @tap="testCallMethod">调用组件方法</button>
<native-button id="helloView" class="native-button" text="buttonText" @buttonTap="ontap"></native-button>
</view>
</view>
</template>
<script>
import { createNativeButtonContext } from "../../uni_modules/native-button";
export default {
data() {
return {
title: '组件能力封装示例',
}
},
methods: {
ontap(e : UniNativeViewEvent) {
console.log("ontap----------" + e.type)
},
testCallMethod: function () {
let context = createNativeButtonContext("helloView")
context?.updateText("test code")
}
}
}
</script>
<style>
.native-button {
height: 100px;
width: 200px;
margin: 25px auto 25px auto;
}
</style>
在自定义组件中使用组件上下文:
<template>
<view>
<button type="primary" @tap="testCallMethod">调用组件方法</button>
<native-button id="helloView" class="native-button" text="自定义组件"></native-button>
</view>
</template>
<script>
import { createNativeButtonContext } from "../../uni_modules/native-button";
export default {
data() {
return {
title: '组件能力封装示例',
}
},
methods: {
ontap(e : UniNativeViewEvent) {
console.log("ontap----------" + e.type)
},
testCallMethod: function () {
let context = createNativeButtonContext("helloView", this)
context?.updateText("test code")
}
}
}
</script>
以上便是组件上下文相关的内容介绍
通过vue方式开发组件非常直观。它分为几个核心步骤: