it编程 > 前端脚本 > 其它脚本

Web前端浅谈ArkTS组件开发

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的含义,照猫画虎即可。主要看前面的装饰器。

另外需要注意的是,在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两个装饰器实现。

style装饰器

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装饰器可以定义一些布局类的基础样式,比如背景,内外边距等等;如果定义在组件内部,有助于提升组件内聚;定义在外部,可以构建基础样式库。

而像fontsizefontcolor之类的仅在部分组件上具备的属性定义,在@styles中无法使用。所以这里就需要用到@extends装饰器。

extend装饰器

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)更新。

arkts中,组件(struct)成员有诸多修饰符可选。基于个人的开发经验和习惯,我推荐使用单向数据流方式,模型层面仅使用@prop@state来实现组件间交互。下面简单讲一下使用:

@state状态装饰器

在之前的代码中,可以看到一个用@state声明的状态值message

@state装饰的成员,可以对标reactusestate成员,或者vue组件中data()的某一个key

@prop属性装饰器

@state装饰的成员,可以对标reactusestate成员,或者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()
                    }
                })
        }
    }
}

这里声明了两个属性textcount,以及一个自定义事件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()
    })
}

图片

函数式组件

这里的函数式的命名,是纯字面的,并不是reactfunctional 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.

这错误提示给出两点要求:

看一个多插槽的例子,继续优化组件:

@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的方言、开发模式和基础库做简单了解,基本就能上手开发了,总体门槛不高。

最后吐几个槽点:

不过,上面的槽点只是有点烦,习惯就好。

关于 opentiny

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标签,一起参与开源贡献~

(0)
打赏 微信扫一扫 微信扫一扫

您想发表意见!!点此发布评论

推荐阅读

OpenTiny HUICharts 正式开源发布,一个简单、易上手的图表组件库

08-06

五款优质报表工具推荐,其中一款竟然免费!

08-06

“JVM” 上的AOP:Java Agent 实战

08-06

场景执行工具:Java

08-06

Vue Vine:带给你全新的 Vue 书写体验!

08-05

怎样判断CSS样式选择器的优先级?id优先还是Class优先?

08-04

猜你喜欢

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论