167人参与 • 2024-08-06 • 其它脚本
本文由js老狗原创。
有幸参与本厂app的鸿蒙化改造,学习了arkts以及ide的相关知识,并有机会在issue上与鸿蒙各路大佬交流,获益颇丰。
本篇文章将从一个web前端的视角出发,浅谈arkts组件开发的基础问题,比如属性传递、插槽、条件渲染等。
这个过程简单过一下,不赘述。
创建好项目后,我们会自动跳到初始首页,代码如下:
@entry
@component
struct index {
@state message: string = 'hello world';
build() {
relativecontainer() {
text(this.message)
.id('helloworld')
.fontsize(50)
.fontweight(fontweight.bold)
.alignrules({
center: { anchor: '__container__', align: verticalalign.center },
middle: { anchor: '__container__', align: horizontalalign.center }
})
}
.height('100%')
.width('100%')
}
}
首先注意页面index
是按struct
定义。我们在这里不深入讨论struct
的含义,照猫画虎即可。主要看前面的装饰器。
entry
表示该页面为一个独立的page
,可通过router
进行跳转。component
对该对象封装之后,即可进行页面渲染,并构建数据->视图
的更新,可以看成是一个mvvm
结构的模版,类似对react.component
的集成,或者是vue
中definecomponent
的语法糖。build
渲染,可以对标react
组件中的render()
,或者vue
中的setup()
。当使用@component
装饰器之后,必须显式声明该方法,否则会有系统报错。另外需要注意的是,在build()
中仅可以使用声明式
的写法,也就是只能使用表达式
。可以看成是jsx
的一个变体:
// 请感受下面组件函数中 return 之后能写什么
export default () => {
return (
<h1>hello world</h1>
)
}
@component
export default struct somecomponent {
build() {
// console.log(123) // 这是不行的
text('hello world')
}
}
如果有条件可以打开ide实际操作体会一下。
上面组件的示例代码中,我们并没有使用@entry
装饰器。是的这就足够了,上面的代码就是一个完整组件的声明。
我们把组件单拎出来:
@component
export struct custombutton {
build() {
button('my button')
}
}
刚才的首页做一下改造,使用前端惯用的flex
布局:
import { custombutton } from './custombutton'
@entry
@component
struct index {
@state message: string = 'hello world';
build() {
flex({
direction: flexdirection.column,
justifycontent: flexalign.center,
alignitems: itemalign.center,
}) {
text(this.message)
.id('helloworld')
.fontsize(50)
.fontweight(fontweight.bold)
custombutton()
}
.height('100%')
.width('100%')
}
}
最基本的组件定义和使用,就是如此了。
与web
前端不同,arkts
没有css
,但arkts
通过链式写法,实现了常用的css
样式定义。只要有css
方案,基本都可以通过链式写法,把想要的样式点
出来。
这样散养
的样式并不常用,web前端会用class
来声明样式集。类似的功能,可以通过@extend
和@styles
两个装饰器实现。
import { custombutton } from './custombutton'
@entry
@component
struct index {
@state message: string = 'hello world';
// 声明style簇
@styles
helloworldstyle() {
.backgroundcolor(color.yellow)
.border({ width: { bottom: 5 }, color: '#ccc' })
.margin({ bottom: 10 })
}
build() {
flex({
direction: flexdirection.column,
justifycontent: flexalign.center,
alignitems: itemalign.center,
}) {
text(this.message)
.id('helloworld')
.fontsize(50)
.fontweight(fontweight.bold)
.helloworldstyle() // 注意这里调用样式簇
custombutton()
}
.height('100%')
.width('100%')
}
}
@styles
装饰器也可以单独修饰function
函数:
@styles
function helloworldstyle2() {
.backgroundcolor(color.yellow)
.border({ width: { bottom: 5 }, color: '#000' })
.margin({ bottom: 10 })
}
@entry
@component
struct index {
//...
}
使用@styles
装饰器可以定义一些布局类的基础样式,比如背景,内外边距等等;如果定义在组件内部,有助于提升组件内聚;定义在外部,可以构建基础样式库。
而像fontsize
、fontcolor
之类的仅在部分组件上具备的属性定义,在@styles
中无法使用。所以这里就需要用到@extends
装饰器。
import { custombutton } from './custombutton'
@extend(text)
function textstyle() {
.fontsize(50)
.fontweight(fontweight.bold)
.id('helloworld')
}
@entry
@component
struct index {
@state message: string = 'hello world';
@styles
helloworldstyle() {
.backgroundcolor(color.yellow)
.border({ width: { bottom: 5 }, color: '#ccc' })
.margin({ bottom: 10 })
}
build() {
flex({
direction: flexdirection.column,
justifycontent: flexalign.center,
alignitems: itemalign.center,
}) {
text(this.message)
.textstyle()
.helloworldstyle()
custombutton()
}
.height('100%')
.width('100%')
}
}
此外@extend
还可以带参数:
@extend(text)
function textstyle(fontsize: number = 50, fontcolor: resourcestr | color = '#f00') {
.fontsize(fontsize)
.fontcolor(fontcolor)
.fontweight(fontweight.bold)
.id('helloworld')
}
然后直接点
调用
text(this.message)
.textstyle(36, '#06c')
.helloworldstyle()
我们就得到了:
@extend
装饰器不能装饰struct
组件内部成员函数,这是与@styles
装饰器的一处不同。
各种事件也都可以点
出来:
import { promptaction } from '@kit.arkui'
@component
export struct custombutton {
build() {
column() {
button('my button')
.onclick(() => {
promptaction.showtoast({
message: '你点我!'
})
})
}
}
}
请注意这里使用了promptaction
组件来实现toast
效果:
对web开发者来说,首先要注意的是:没有事件传递 ------------没有冒泡
或捕获
过程,不需要处理子节点事件冒泡
到父节点的问题。
此外点击事件的回调参数提供了比较全面的详细信息ui信息,对实现一些弹框之类的ui展示比较有帮助。
比如event.target.area
可以获取触发组件本身的布局信息:
我们改一下上面的组件代码,在组件中声明一个成员函数onclickmybutton
,作为button
点击的回调:
@component
export struct custombutton {
onclickmybutton?: () => void
build() {
column() {
button('my button')
.onclick(() => {
if(typeof this.onclickmybutton === 'function') {
this.onclickmybutton()
}
})
}
}
}
然后改一下index
页面代码,定义onclickmybutton
回调:
build() {
flex({
direction: flexdirection.column,
justifycontent: flexalign.center,
alignitems: itemalign.center,
}) {
// ...
custombutton({
onclickmybutton: () => {
promptaction.showtoast({
message: '你又点我!'
})
}
})
}
.height('100%')
.width('100%')
}
在mv(x)
架构下,数据模型(model
)一般分为属性
和状态
两种概念,且都应当驱动视图(view
)更新。
property
),指外部(父级)传入值,自身只可读不可更改;如需要修改,则要通过回调通知父组件。state
),私有值,用于内部逻辑计算;一般来讲,状态的数据结构复杂度,与组件复杂度正相关。在arkts
中,组件(struct
)成员有诸多修饰符可选。基于个人的开发经验和习惯,我推荐使用单向数据流
方式,模型层面仅使用@prop
和@state
来实现组件间交互。下面简单讲一下使用:
在之前的代码中,可以看到一个用@state
声明的状态值message
。
被@state
装饰的成员,可以对标react
的usestate
成员,或者vue
组件中data()
的某一个key
。
被@state
装饰的成员,可以对标react
的usestate
成员,或者vue
组件中data()
的某一个key
。
@component
export struct custombutton {
onclickmybutton?: () => void
@prop text: string = 'my button'
build() {
column() {
button(this.text) // 使用该属性
.onclick(() => {
if(typeof this.onclickmybutton === 'function') {
this.onclickmybutton()
}
})
}
}
}
在父级调用
custombutton({
text: '我的按钮'
})
再完善一下组件:
@component
export struct custombutton {
onclickmybutton?: () => void
@prop text: string = 'my button'
@prop count: number = 0
build() {
column() {
// 这里展示计数
button(`${this.text}(${this.count})`)
.onclick(() => {
if(typeof this.onclickmybutton === 'function') {
this.onclickmybutton()
}
})
}
}
}
这里声明了两个属性text
和count
,以及一个自定义事件onclickmybutton
。
父级声明一个状态clickcount
,绑定子组件的count
属性,并在子组件的自定义事件中,增加clickcount
的值。预期页面的计数随clickcount
变化,按钮组件的计数随属性count
变化,两者应当同步。
@entry
@component
struct index {
@state message: string = 'hello world';
@state clickcount: number = 0
@styles
helloworldstyle() {
.backgroundcolor(color.yellow)
.border({ width: { bottom: 5 }, color: '#ccc' })
.margin({ bottom: 10 })
}
@builder
subtitle() {
// 这里展示计数
text(`the message is "${this.message}", count=${this.clickcount}`)
.margin({ bottom: 10 })
.fontsize(12)
.fontcolor('#999')
}
build() {
flex({
direction: flexdirection.column,
justifycontent: flexalign.center,
alignitems: itemalign.center,
}) {
text(this.message)
.textstyle(36, '#06c')
.helloworldstyle2()
this.subtitle()
italictext('italictext')
custombutton({
text: '点击次数',
count: this.clickcount,
onclickmybutton: () => {
this.clickcount += 1
}
})
}
.height('100%')
.width('100%')
}
}
实际效果:
符合预期。
使用@watch
装饰器,可以监听@prop
装饰对象的变化,并能指定监听方法:
@prop @watch('onchange') count: number = 0
private onchange(propname: string) {
console.log('>>>>>>', propname)
}
@watch
装饰器调用onchange
时,会把发生变化的属性名作为参数传递给onchange
;也就是说,我们可以只定义一个监听方法,通过入参propname
来区分如何操作。
@prop @watch('onchange') count: number = 0
@prop @watch('onchange') stock: number = 0
private onchange(propname: string) {
if(propname === 'count') {
//...
} else if(propname === 'stock') {
//...
}
}
我们下面用@watch
监听属性count
,实现属性更改驱动组件内部状态变化。首先改造组件:
@component
export struct custombutton {
onclickmybutton?: () => void
@prop text: string = 'my button'
@prop @watch('onchange') count: number = 0
// 内部状态
@state private double: number = 0
private onchange() {
this.double = this.count * 2
}
build() {
column() {
button(`${this.text}(${this.count} x 2 = ${this.double})`)
.onclick(() => {
if(typeof this.onclickmybutton === 'function') {
this.onclickmybutton()
}
})
}
}
}
效果可以对标react
中的useeffect
,或者vue
中的observer
或者watch
。
这里有一个隐含的问题:当@prop
被第一次赋值的时候,不会触发@watch
监听器。比如我们把页面状态clickcount
初始化为3
,这时候尬住了:
在web的解决方案中,这种问题自然是绑定组件生命周期。同样,artts
也是如此:
@component
export struct custombutton {
onclickmybutton?: () => void
@prop text: string = 'my button'
@prop @watch('onchange') count: number = 0
@state private double: number = 0
private onchange() {
this.double = this.count * 2
}
build() {
column() {
button(`${this.text}(${this.count} x 2 = ${this.double})`)
.onclick(() => {
if(typeof this.onclickmybutton === 'function') {
this.onclickmybutton()
}
})
}
// 这里绑定生命周期
.onattach(() => {
this.onchange()
})
}
}
本文为了简便,直接在onattach
中使用监听函数初始化。具体情况请自行斟酌。
用过react
的人都知道三目表达式的痛:
// 以下伪代码 未验证
export default mypage = (props: { haslogin: boolean; userinfo: tuserinfo }) => {
const { haslogin, userinfo } = props
return <div classname='my-wrapper'>{
haslogin ? <userinfo info={userinfo} /> : <login />
}</div>
}
前面提过,由于return
后面词法限制,只能使用纯表达式写法。或者,把return
包裹到if..else
中,总归不是那么优雅。
arkts
则直接支持在build()
中使用if...else
分支写法:
build() {
column() {
button(`${this.text}(${this.count} x 2 = ${this.double})`)
.onclick(() => {
if(typeof this.onclickmybutton === 'function') {
this.onclickmybutton()
}
})
if(this.count % 2 === 0) {
text('双数').fontcolor(color.red).margin({ top: 10 })
} else {
text('单数').fontcolor(color.blue).margin({ top: 10 })
}
}
.onattach(() => {
this.onchange()
})
}
这里的函数式
的命名,是纯字面的,并不是react
的functional component
的意思。
这类组件由@builder
装饰器声明,对象可以是一个单独的function
,抑或是struct
组件中的一个方法。
需要特别注意的是,这里的function
是指通过function
声明的函数,不包括 **箭头函数(arrow function)
**。
import { custombutton } from './custombutton'
@extend(text)
function textstyle(fontsize: number = 50, fontcolor: resourcestr | color = '#f00') {
.fontsize(fontsize)
.fontcolor(fontcolor)
.fontweight(fontweight.bold)
.id('helloworld')
}
@builder
function italictext(content: string) {
text(content).fontsize(14).fontstyle(fontstyle.italic).margin({ bottom: 10 })
}
@entry
@component
struct index {
@state message: string = 'hello world';
@styles
helloworldstyle() {
.backgroundcolor(color.yellow)
.border({ width: { bottom: 5 }, color: '#ccc' })
.margin({ bottom: 10 })
}
@builder
subtitle() {
text(`the message is "${this.message}"`)
.margin({ bottom: 10 })
.fontsize(12)
.fontcolor('#999')
}
build() {
flex({
direction: flexdirection.column,
justifycontent: flexalign.center,
alignitems: itemalign.center,
}) {
text(this.message)
.textstyle(36, '#06c')
.helloworldstyle()
this.subtitle()
italictext('italictext')
custombutton()
}
.height('100%')
.width('100%')
}
}
上面的代码中,声明了一个外部组件italictext
,一个内部组件this.subtitle
,可以在build()
中直接使用。
由于@builder
的装饰对象是一个function
函数,所以这个组件可以带参数动态渲染。
arkts
中提供了@builderparam
装饰器,可以让@builder
以参数的形式向其他组件传递。这为实现插槽提供了条件。
我们首先在组件中声明一个@builderparam
,然后植入到组件的build()
中。改造组件代码:
@component
export struct custombutton {
onclickmybutton?: () => void
@prop text: string = 'my button'
@prop @watch('onchange') count: number = 0
@state private double: number = 0
// 插槽
@builderparam slot: () => void
private onchange() {
this.double = this.count * 2
}
build() {
column() {
button(`${this.text}(${this.count} x 2 = ${this.double})`)
.onclick(() => {
if(typeof this.onclickmybutton === 'function') {
this.onclickmybutton()
}
})
if(this.count % 2 === 0) {
text('双数').fontcolor(color.red).margin({ top: 10 })
} else {
text('单数').fontcolor(color.blue).margin({ top: 10 })
}
// 植入插槽,位置自定
if(typeof this.slot === 'function') {
this.slot()
}
}
.onattach(() => {
this.onchange()
})
}
}
页面代码更改:
build() {
flex({
direction: flexdirection.column,
justifycontent: flexalign.center,
alignitems: itemalign.center,
}) {
text(this.message)
.textstyle(36, '#06c')
.helloworldstyle2()
this.subtitle()
italictext('italictext')
custombutton({
text: '点击次数',
count: this.clickcount,
onclickmybutton: () => {
this.clickcount += 1
},
// 定义插槽
slot: () => {
this.subtitle()
}
})
}
.height('100%')
.width('100%')
}
这种单一插槽的情况,可以有更优雅的写法:
请注意:单一插槽,也就是说组件中仅包含一个@builderparam成员,且与成员命名无关。
如果有多个@builderparam
成员,下面那种嵌套写法会在编译期报错:
[compile result] in the trailing lambda case, 'custombutton' must have one and only one property decorated with @builderparam, and its @builderparam expects no parameter.
这错误提示给出两点要求:
@builderparam
装饰成员看一个多插槽的例子,继续优化组件:
@component
export struct custombutton {
onclickmybutton?: () => void
@prop text: string = 'my button'
@prop @watch('onchange') count: number = 0
@state private double: number = 0
@builderparam slot: () => void
@builderparam slot2: () => void
private onchange() {
this.double = this.count * 2
}
build() {
column() {
button(`${this.text}(${this.count} x 2 = ${this.double})`)
.onclick(() => {
if(typeof this.onclickmybutton === 'function') {
this.onclickmybutton()
}
})
if(typeof this.slot === 'function') {
this.slot()
}
if(this.count % 2 === 0) {
text('双数').fontcolor(color.red).margin({ top: 10 })
} else {
text('单数').fontcolor(color.blue).margin({ top: 10 })
}
if(typeof this.slot2 === 'function') {
this.slot2()
}
}
.onattach(() => {
this.onchange()
})
}
}
请注意:在向@builderparam插槽传入@builder的时候,一定包一层箭头函数,否则会引起this指向问题。
从个人感受来讲,如果一个开发者对ts有充分的使用经验,进入arkts之后,只要对arkts的方言、开发模式和基础库做简单了解,基本就能上手开发了,总体门槛不高。
最后吐几个槽点:
ts
,但ts
的类型推断几乎没有;没有type
类型声明;不能一次性声明嵌套的复杂类型;没有any/unkown类型,object
有点类似unknown
,仅此而已;不支持解构取值。不过,上面的槽点只是有点烦,习惯就好。
opentiny 是一套企业级 web 前端开发解决方案,提供跨端、跨框架、跨版本的 tinyvue 组件库,包含基于 angular+typescript 的 tinyng 组件库,拥有灵活扩展的低代码引擎 tinyengine,具备主题配置系统tinytheme / 中后台模板 tinypro/ tinycli 命令行等丰富的效率提升工具,可帮助开发者高效开发 web 应用。
欢迎加入 opentiny 开源社区。添加微信小助手:opentiny-official 一起参与交流前端技术~
opentiny 官网:https://opentiny.design/
opentiny 代码仓库:https://github.com/opentiny/
tinyvue 源码:https://github.com/opentiny/tiny-vue
tinyengine 源码: https://github.com/opentiny/tiny-engine
欢迎进入代码仓库 startinyengine、tinyvue、tinyng、tinycli~ 如果你也想要共建,可以进入代码仓库,找到 good first issue标签,一起参与开源贡献~
您想发表意见!!点此发布评论
版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。
发表评论