JS UI前端开发

内容纲要

@[toc]

基础能力

  • 声明式编程
  • 跨设备
  • 高性能

整体架构

  • JS UI框架包括应用层、前端框架层、引擎层、平台适配层
  • Application应用层表示使用JS UI框架开发的FA应用
  • Framework前端框架层主要完成前端页面解析,以及提供MVVM开发模式、页面路由机制和自定义组件等能力
  • Engine引擎层主要提供动画解析、DOM树构建、布局计算、渲染命令构建与绘制、事件管理等能力
  • Porting Layer适配层主要完成对平台层进行抽象,提供抽象接口,可对接到系统平台。比如事件对接、渲染管线对接和系统生命周期对接

AceAbility

  • AceAbility类是JS FA在HarmonyOS上运行环境的基类,继承自Ability。应用运行入口类应该从此类派生

    public class MainAbility extends AceAbility
    {
        @Override
        public void onStart(Intent intent)
        {
                super.onStart(intent);
        }
    
        @Override
        public void onStop()
        {
                super.onStop();
        }
    }

如何加载JS FA

  • JS FA生命周期事件分为应用生命周期页面生命周期,应用通过AceAbility类中setInstanceName()接口设置该Ability的实例资源,并通过AceAbility窗口进行显示以及全局应用生命周期管理
  • setInstanceName(String name)的参数"name"指实例名称,实例名称与config.json文件中module.js.name的值对应
  • 若开发者为修改实例名,而使用缺省值default,则无需调用此接口;若修改了实例名,则需在应用Ability实例的onStart()中调用此接口,并将参数"name"设置为修改后的实例名称

加载主页面

  • setInstanceName()接口使用方法:在MainAbility的onStart()中的super.onStart()前调用此接口
    public class MainAbility extends AceAbility
    {
        @Override
        public void onStart(Intent intent)
        {
                setInstanceName("JSComponentName");       // config.json配置文件中module.js.name的标签值
                super.onStart(intent);
        }
    }

组件介绍

  • 组件是构建页面的核心,每个组件通过对数据和方法的简单封装,实现独立的可视、可交互功能单元。各组件之间相互独立、随取随用,也可在需求相同的地方重复使用,可通过组件间合理的搭配定义满足业务需求的新组件。

组件分类

组件类型主要组件
基础组件text image progress rating span marquee image-animator divider search menu chart
容器组件div list list-item stack swiper tabs tab-bar tab-content list-item-group refresh dialog
媒体组件video
画布组件canvas

组件通用特性

组件通用属性

  • 通用属性包含常规属性渲染属性。常规属性指组件普遍支持的用来设置组件基本标识外观显示特征的属性。
名称类型默认值必填描述
idstring组件的唯一标识
stylestring组件的样式声明
classstring组件的样式类,用于引用样式表
refstring用来指定指向子元素或子组件的引用信息,该引用将注册到父组件的$ref属性对象上
disabledbooleanfalse当前组件是否被禁用,在禁用场景下,组件将无法响应用户交互
focusablebooleanfalse当前组件是否可与获取焦点。当设置为true时,组件可以响应焦点事件和按键事件,当组件额外设置了按键事件或点击事件时,框架会设置该属性为true

组件通用样式

  • 在前端设计中组件最关键的问题是如何将组件在屏幕上显示出来,就是要定义大小和位置关系

组件通用事件

  • 事件绑定在组件上,当组件达到事件触发条件时,会执行JS中对应的事件回调函数,实现页面UI视图和页面JS逻辑层的交互。对HarmonyOS来说,事件主要为手势事件按键事件。手势事件主要用于智能穿戴等具有触摸屏的设备,按键事件主要用于智慧屏设备。

布局说明

  • JS UI框架中手机和智慧屏以720px(px指逻辑像素)为基准宽度,根据实际屏幕宽度进行缩放,例如当width为100px时,在宽度为1440物理像素的屏幕上,实际显示的宽度为200物理像素。

定义文档结构

  • 实现标题和文本区域最常用的是基础组件text。text组件用于展示文本,可以设置不同的属性和样式,文本内容需要写在标签内容区,在页面中插入标题和文本区域的示例如下:
    <!-- xxx.hml -->
    <div class="container">
        <text class="title-text">{{headTitle}}</text>
        <text class="paragraph-text">{{paragraphFirst}}</text>
        <text class="paragraph-text">{{paragraphSecond}}</text>
    </div>
    /* xxx.css */
    .container
    {
        flex-direction: column;
        margin-top: 20px;
        margin-left: 30px;
    }
    .title-text
    {
        color: #1a1a1a;
        font-size: 50px;
        margin-top: 40px;
        margin-bottom: 20px;
    }
    .paragraph-text
    {
        color: #000000;
        font-size: 35px;
        line-height: 60px;
    }

动态内容和交互

// xxx.js
export default
{
        data:
        {
                headTitle: '...',
                paragraphFirst: '...',
                paragraphSecond: '...',
        },
}

添加图片区域

  • 实现图片区域通常用image组件来实现,使用方法与text组件类似
  • 图片资源放在与pages目录同级的common目录上
    <!-- xxx.hml -->
    <image class="img" src="{{middleImage}}"></image>
    /* xxx.css */
    .img
    {
        margin-top: 30px;
        margin-bottom: 30px;
        height: 385px;
    }
    // xxx.js
    export default
    {
        data:
        {
                middleImage: '/common/ice.png',
        },
    }

添加留言区域

  • 留言区域由div、text、input关联click事件实现。可使用input组件实现输入留言的部分,使用text组件实现留言显示部分,使用commentText的状态标记此时显示的组件(通过if属性控制)。在包含文本”完成“和“删除”的text组件中关联click事件,更新commentText状态和inputValue的内容

留言区实现

  • 页面结构

    <!-- xxx.hml -->
    <div class="container">
        <text class="conmment-title">Comment</text>
        <div if="{{!commentText}}">
        <input class="comment" value="{{inputValue}}" onchange="updateValue()"></input>
        <text class="comment-key" onclick="update" focusable="true">Done</text>
        </div>
        <div if="{{commentText}}">
                <text class="comment-text" focusable="true">{{inputValue}}</text>
                <text class="commnet-key" onclick="update" focusable="true">Delete</text>
        </div>
    </div>
  • 样式

    /* xxx.css */
    .container
    {
        margin-top: 24px;
        background-color: #ffffff;
    }
    .comment-title
    {
        font-size: 40px;
        color: #1a1a1a;
        font-weight: bold;
        margin-top: 40px;
        margin-bottom: 10px;
    }
    .comment
    {
        width: 550px;
        height: 100px;
        background-color: lightgrey;
    }
    .comment-key
    {
        width: 150px;
        height: 100px;
        margin-left: 20px;
        font-size: 32px;
        color: #1a1a1a;
        font-weight: bold;
    }
    .comment-key: focus
    {
        color: #007dff;
    }
    .comment-text
    {
        width: 550px;
        height: 100px;
        text-align: left;
        line-height: 35px;
        font-size: 30px;
        color: #000000;
        border-bottom-color: #bcbcbc;
        border-bottom-width: 0.5px;
    }
  • 交互

    export default
    {
        data:
        {
                inputValue: '',
                commentText: false,
        },
        update()
        {
                this.commentText = !this.commentText;
        },
        updateValue(e)
        {
                this.inputValue = e.text;
        },
    }

页面路由

  • 很多应用由多个页面组成,例如用户可以从音乐列表页面点击歌曲,跳转到该歌曲的播放界面。因此需要通过页面路由将这些页面串联起来,按需实现跳转。
  • 页面路由router根据页面的uri找到目标页面,从而实现跳转。以最基础的两个页面间的跳转为例:具体实现步骤如下:
    • 在“Project”窗口,打开“entry > src > main > js > default”,右键“pages”目录,选择“New > JS Page”,创建一个详情页
    • 调用router.push()路由到详情页
    • 调用router.back()回到首页

构建页面结构

  • index和detail两个页面均包含一个text组件和button组件:text组件指明当前页面,button组件实现两个页面之间的相互跳转
    <!-- index.hml -->
    <div class="container">
        <text class="title">This is the index page.</text>
        <button type="capsule" value="Go to the second page" class="button" onclick="launch"></button>
    </div>
    <!-- detail.hml -->
    <div class="container">
        <text class="title">This is the detail page.</text>
        <button type="capsule" value="Go back" class="button" onclick="launch"></button>
    </div>

构建页面样式

  • 构建index和detail的页面样式:text和button组件居中显示,两个组件之间间距50px:
    /* index.css detail.css */
    .container
    {
        flex-direction: column;
        justify-content: center;
        align-items: center;
    }
    .title
    {
        font-size: 50px;
        margin-bottom: 50px;
    }

实现跳转

  • 为了使button组件的launch方法生效,需要在页面的js文件中实现跳转逻辑。调用router.push()接口将uri指定的页面添加到路由栈中,即跳转到uri指定的页面。在调用router方法之前,需要导入router模块:
    // index.js
    import router form '@system.router';
    export default
    {
        launch()
        {
                router.push
                ({
                        uri: 'pages/detail/detail',
                });
        },
    }
    // detail.js
    import router from '@system.router';
    export default
    {
        launch()
        {
                router.back();
        },
    }

动画

静态动画

  • 静态动画的核心是transform样式,主要可实现以下三种变换类型:
    • translate:沿水平或垂直方向将指定组件移动所需距离
    • scale:横向或纵向将指定组件缩放到所需比例
    • rotate:将指定组件沿横轴或纵轴或中心点旋转指定角度

静态动画示例

<!-- xxx.hml -->
<div class="container">
        <text class="translate">hello</text>
        <text class="rotate">hello</text>
        <text class="scale">hello</text>
</div>
.container
{
        flex-direction: column;
        align-items: center;
}
.translate
{
        height: 150px;
        width: 300px;
        font-size: 50px;
        background-color: #008000;
        transform: translate(200px);
}
.rotate
{
        height: 150px;
        width: 300px;
        font-size: 50px;
        background-color: #008000;
        transform-origin: 200px 100px;
        transform: rotateX(45deg);
}
.scale
{
        height: 150px;
        width: 300px;
        font-size: 50px;
        background-color: #008000;
        transform: scaleX(1.5);
}

连续动画

  • 静态动画只有开始和结束状态,无中间状态,如果需要设置中间过渡状态和转换效果,需要使用连续动画实现。
  • 连续动画的核心是animation样式,它定义了动画的开始状态、结束状态以及时间和速度的变化曲线。通过animation样式可实现的效果有:
    • animation-name:设置动画执行后应用到组件上的背景颜色、透明度、宽高和变换类型
    • animation-delay和animation-duration:分别设置动画执行后元素延迟和持续的时间
    • animation-timing-function:描述动画执行的速度曲线,事动画更加平滑
    • animation-iteration-count:定义动画播放的次数
    • animation-fill-mode:指定动画执行结束后是否恢复初始状态

连续动画页面结构

  • animation样式需要在css中先定义keyframe,在keyframe中设置动画的过渡效果,并通过一个样式类型在hml中调用。animation-name使用示例如下:
    <!-- xxx.hml -->
    <div class="item-container">
        <text class="header">animation-name</text>
        <div class="item {{colorParam}}">
                <text class="txt">color</text>
        </div>
        <div class="item {{opacityParam}}">
                <text class="txt">opacity</text>
        </div>
        <input class="button" type="button" name="" value="show" onclick="showAnimation"/>
    </div>

连续动画页面样式

/* xxx.css */
.item-container
{
        margin-right: 60px;
        margin-left: 6px;
        flex-direction: column;
}
.header
{
        margin-bottom: 20px;
}
.item
{
        background-color: #f76160;
}
.txt
{
        text-align: center;
        width: 200px;
        height: 100px;
}
.button
{
        width: 200px;
        font-size: 30px;
        background-color: #09ba07;
}
.color
{
        animation-name: Color;
        animation-duration: 8000ms;
}
.opacity
{
        animation-name: Opacity;
        animation-duration: 8000ms;
}
@keyframes Color
{
        from
        {
                background-color: #f76160;
        }
        to
        {
                background-color: #09ba07;
        }
}
@keyframes Opacity
{
        from
        {
                opacity: 0.9;
        }
        to
        {
                opacity: 0.1;
        }
}

连续动画交互逻辑

// xxx.js
export default
{
        data:
        {
                colorParam: '',
                opacityParam: '',
        },
        showAnimation: function()
        {
                this.colorParam = '';
                this.opacityParam = '';
                this.colorParam = 'color';
                this.opacityParam = 'opacity';
        },
}

手势事件

触摸

  • touchstart:触摸动作开始
  • touchmove:触摸后移动
  • touchcancel:触摸动作被打断,如来电提醒、弹窗
  • touchend:触摸动作结束
  • 点击-click:快速轻敲屏幕
  • 长按-longpress:在相同位置长时间保持与屏幕接触

触摸事件示例

  • 页面结构

    <!-- xxx.hml -->
    <div class="container">
        <div class="text-container" onclick="click">
                <text class="text-style">{{onClick}}</text>
        </div>
        <div class="text-container" ontouchstart="touchStart">
                <text class="text-style">{{touchstart}}</text>
        </div>
        <div class="text-container" ontouchmove="touchMode">
                <text class="text-style">{{touchmove}}</text>
        </div>
        <div class="text-container" ontouchend="touchEnd">
                <text class="text-style">{{touchend}}</text>
        </div>
        <div class="text-container" ontouchcancel="touchCancel">
                <text class="text-style">{{touchcancel}}</text>
        <div class="text-container" onlongpress="longPress">
                <text class="text-style">{{onLongPress}}</text>
        </div>
        </div>
    </div>
  • 样式

    /* xxx.css */
    .container
    {
        flex-direction: column;
        justify-content: center;
        align-items: center;
    }
    .text-container
    {
        margin-top: 10px;
        flex-direction: column;
        width: 750px;
        height: 50px;
        background-color: #09ba07;
    }
    .text-style
    {
        width: 100%;
        line-height: 50px;
        text-align: center;
        font-size: 24px;
        color: #ffffff;
    }
  • 交互逻辑

    // xxx.js
    export default
    {
        data:
        {
                touchstart: 'touchstart',
                touchmove: 'touchmove',
                touchend: 'touchend',
                touchcancel: 'touchcancel',
                onClick: "onclick",
                onLongPress: "onlongpress",
        },
        touchCancel: function(event)
        {
                this.touchcancel = 'canceled';
        },
        touchEnd function(event)
        {
                this.touchend = "ended";
        },
        touchMove: function(event)
        {
                this.touchmove = 'moved';
        },
        touchStart: function(event)
        {
                this.touchstart = 'touchend';
        },
        longPress: function()
        {
                this.onLongPress = 'longpressed';
        },
        click: function()
        {
                this.onClick = 'clicked';
        },
    }

按键事件

  • 按键事件是智慧屏上特有的手势事件,当用户操作遥控器按键时触发。用户点击一个遥控器,通常会触发两次key事件:先触发action为0,再触发action为1,即先触发按下事件,再触发抬起事件。action为2的场景比较少见,一般为用户按下按键且不松开,此时repeatCount将返回次数。每个物理按键对应各自的键值实现不同功能。

按键事件示例

  • 页面结构及样式
    <!-- xxx.hml -->
    <div class="card-box">
        <div class="content-box">
                <text class="content-text" onkey="keyUp" onfocus="focusUp" onblur="blurUp">{{up}}</text>
        </div>
        <div class="content-box">
                <text class="content-text" onkey="keyDown" onfocus="focusDown" onblur="blurDown">{{down}}</text>
        </div>
    </div>
    /* xxx.css */
    .card-box
    {
        flex-direction: column;
        justify-content: center;
    }
    .content-box
    {
        align-items: center;
        height: 200px;
        flex-direction: column;
        margin-left: 200px;
        margin-right: 200px;
    }
    .content-text
    {
        font-size: 40px;
        text-align: center;
    }

自定义组件

  • JS UI框架支持自定义组件,用户可根据业务需求将已有组件进行扩展,增加自定义的私有属性和事件,封装成新的组件,方便在工程中多次调用,提高页面布局代码的可读性。

构建自定义组件

  • 子组件页面结构和样式
    <!-- comp.hml -->
    <div class="item">
        <text class="title-style">{{title}}</text>
        <text class="text-style" onclick="childClicked" focusable="true">点击这里查看隐藏文本</text>
        <text class="text-style" if="{{showObj}}">hello world</text>
    </div>
    .item
    {
        width: 700px;
        flex-direction: column;
        height: 300px;
        align-items: center;
        margin-top: 100px;
    }
    .text-style
    {
        width: 100%;
        text-align: center;
        font-weight: 500;
        font-family: Courier;
        font-size: 36px;
    }
    .title-style
    {
        font-weight: 500;
        font-family: Courier;
        font-size: 50px;
        color: #483d8b;
    }
    // comp.js
    export default
    {
        props:
        {
                title:
                {
                        default: 'title',
                },
                showObject: {},
        },
        data()
        {
                return
                {
                        showObj: this.showObject,
                };
        },
        childClicked()
        {
                this.$emit('eventType1', {text: '收到子组件参数'});
                this.showObj = !this.showObj;
        }
    }

JS FA调用PA

两种PA调用方式

  • JS UI框架提供了JS FA调用Java PA的机制,该机制提供了一种通道来传递方法调用、数据返回以及订阅事件上报
  • 当前提供Ability和Internal Ability两种调用方式:
    • Ability:拥有独立的Ability生命周期,FA使用远端进程通信拉起并请求PA服务,适用于基本服务供FA调用或服务在后台独立运行的场景
    • Internal Ability:与FA共进程,采用内部函数调用的方式和FA进行通信,适用于对服务响应时延要求较高的场景。该方式下PA不支持其他FA访问调用

交互流程

  • JS端与Java端通过bundleNameabilityName进行关联。在系统收到JS调用请求后,根据开发者在JS接口中设置的参数选择对应的处理方式,开发者在onRemoteRequest()中实现PA提供的业务逻辑

FA调用PA接口

FA端提供以下三个JS接口:

  • FeatureAbility.callAbility(OBJECT):调用PA能力
  • FeatureAbility.subscribeAbilityEvent(OBJECT, Function):订阅PA能力
    • FeatureAbility.unsubscribeAbilityEvent(OBJECT):取消订阅PA能力

两类接口

PA端提供以下两类接口:

  • IRemoteObject.onRemoteRequest(int, MessageParcel, MessageParcel, MessageOption):Ability调用方式,FA使用远程进程通信拉起并请求PA服务
  • AceInternalAbility.AceInternalAbilityHandler.onRemoteRequest(int, MessageParcel, MessageParcel, MessageOption):Internal Ability调用方式,采用内部函数调用的方式和FA进行通信

FA调用PA常见问题

callAbility返回报错:"Internal ability not register."返回该错误说明JS接口调用请求未在系统中找到对应的InternalAbilityHandler进行处理,需要检查以下几点是否执行:

  • 在AceAbility继承类中对AceInternalAbility继承类执行了register方法
  • JS侧填写的bundleName和abilityName与AceInternalAbility继承类构造函数中填写的名称保持相同
  • 检查JS端填写的abilityType,确保没有将abilityType缺省或误填写为Ability方式

Ability和Internal Ability区别

  • Ability和Internal Ability是两种不同的FA调用PA的方式:
差异项AbilityInternalAbility
JS端(abilityType)01
是否需要在config.json的abilities中为PA添加声明需要(有独立生命周期)不需要(和FA共生命周期)
是否需要在FA中注册不需要需要
继承的类ohos.aafwk.ability.Abilityohos.ace.ability.AceInternalAbility
是否允许被其他FA访问调用

同步参数

  • FeatureAbility.callAbility中syncOption参数说明:
    • 对于JS FA侧,返回结果都是Promise对象,无论该参数取何值,都采用异步方式等待FA侧响应
    • 对于JAVA PA侧,在Internal Ability方式下收到FA的请求后,根据该参数的取值来选择:通过同步方式获取结果后返回,或者异步执行PA逻辑,获取结果后使用remoteObject.sendRequest方式将结果返回FA
  • 使用await方式调用时IDE报错,需引入babel-runtime/regenerator

JS端

  • JS端调用FeatureAbility接口,传入两个Number参数,Java端接收后返回两个数的和
    var actionData = {};
    actionData.firstNum = 1234;
    actionData.secondNum = 2048;
    var action = {};
    action.bundleName = "com.huawei.hiaceservice";
    action.abilityName = "com.huawei.hiaceservice.ComputeServiceAbility";
    action.messageCode = ACTION_MESSAGE_CODE_PLUS;
    action.data = actionData;
    action.abilityType = ABILITY_TYPE_EXTERNAL;
    action.syncOption = ACTION_SYNC;
    var result = await FeatureAbility.callAbility(action);
    var ret = JSON.parse(result);
    if (ret.code == 0)
    {
        console.info('plus result is:' + JSON.stringify(ret.abilityResult));
    }

PA端(Ability方式)

public class ComputeServiceAbility extends Ability
{
        private static final String TAG = "ComputeServiceAbility";
        private MyRemote remote = new MyRemote();       // FA在请求PA服务时会调用AbilityconnectAbility连接PA,连接成功后,需要在onConnect返回一个remote对象,供FA向PA发送消息
        @Override
        protected IRemoteObject onConnect(Intent intent)
        {
                super.onConnect(intent);
                return remote.asObject();
        }
        class MyRemote extends RemoteObject implements IRemoteBroker
        {
                @Override
                public boolean onRemoteRequest(int code, MessageParcel data, MessageParcel reply, MessageOption option)
                {
                        @Override
                        public IRemoteObject asObject()
                        {
                                return this;
                        }
                }
        }
}

留下评论

您的电子邮箱地址不会被公开。