- Published on
初探 HarmonyOS
- Authors

- Name
- MissTree
HarmonyOS
HarmonyOS Next 开发语言是 ArkTS,类似于TypeScript,对于学习过前端开发人员来说,上手难度较低。 项目结构有点像 Java 、Kotlin ,但是 ArkTS 是基于 TypeScript 的,所以 TypeScript 的语法都可以使用。
DevEco Studio 安装
项目开发
页面的开发和前端的移动端开发很像,在单位使用尽量使用vp而不是px,使各个设备都适配UI px转vp的方式为:vp2px(100) 表示 100px转vp
@Entry
@Component
struct Index {
@State message: string = 'HarmonyOS';
@State search: string = '请输入搜索内容';
build() {
RelativeContainer() {
Stack({alignContent:Alignment.Bottom}){
// 顶部搜索
Stack({alignContent:Alignment.Top}){
Row(){
TextInput({
placeholder:this.search
})
.onFocus(()=>{
console.log('onFocus')
})
}
.height(80)
.width('100%')
.backgroundColor("blue")
}
.zIndex(1)
.position({
top:0,
left:0
})
// 滚动区域
Scroll(){
Column(){
Row(){
Text(this.message)
.id('HelloWorld')
.fontSize($r('app.float.page_text_font_size'))
.fontWeight(FontWeight.Bold)
.alignRules({
center: { anchor: '__container__', align: VerticalAlign.Center },
middle: { anchor: '__container__', align: HorizontalAlign.Center }
})
.onClick(() => {
this.message = this.message== 'Welcome'?'HarmonyOS':'Welcome';
})
}
.height('100%')
Row(){
Text('滚动内容1').height(500)
}
Row(){
Text('滚动内容2').height(500)
}
}
}
// 底部 tab
Row(){
Text('tab1')
.textAlign(TextAlign.Center)
.layoutWeight(1)
.fontWeight(FontWeight.Bold)
Text('tab2')
.layoutWeight(1)
.textAlign(TextAlign.Center)
Text('tab3')
.layoutWeight(1)
.textAlign(TextAlign.Center)
Text('tab4')
.layoutWeight(1)
.textAlign(TextAlign.Center)
.onClick(()=>{
console.log('点击了tab4')
})
}
.width('100%')
.height(70)
.position({
bottom:0,
left:0
})
}
}
.height('100%')
.width('100%')
}
}

页面组件
- Text:文本组件
- Image:图片组件
- 网络路径:Image('https://baidu.com/images/1.png') 表示图片的路径
- 本地路径:Image($('app.images.avatar')) 图片默认读取名称,不需要加后缀(不要设置相同名称的不同文件)
- fillColor:填充颜色
- Button:按钮组件
- Scroll:滚动组件
- Stack:层叠组件
- alignContent: Alignment.Center
- CheckBox:复选框组件
- Span:文本组件
- Column:列组件 不具备可滚动效果
- Row:行组件 不具备可滚动效果
- Divider:分割线组件
- AlertDialog:弹窗组件
- Navigator:导航组件
- Tab:标签组件
- List:列表组件
- Card:卡片组件
- Badge:角标组件
- Dialog:对话框组件
- Toast:提示框组件
- Loading:加载组件
- Grid:网格组件
- Picker:选择器组件
- DatePicker:日期选择器组件
- TimePicker:时间选择器组件
- Switch:开关组件
- Slider:滑块组件
Swiper- 轮播图组件
@Entry
@Component
struct SwiperSample {
build() {
Column() {
Swiper() {
Image('https://baidu.com/images/1.png')
Image('https://baidu.com/images/2.png')
Image('https://baidu.com/images/3.png')
}
.indicator(true) // 显示指示器
.indicatorSize(10) // 指示器大小
.indicatorPosition(IndicatorPosition.Bottom) // 指示器位置
.indicatorAlign(IndicatorAlign.Center) // 指示器对齐方式
.indicatorShape(IndicatorShape.Rect) // 指示器形状
.interval(3000) // 轮播间隔时间
.duration(500)
.loop(true) //开启循环
.height(300)
.autoplay(true) // 自动播放
}
}
}
Scroll- 滚动组件
@Entry
@Component
struct Index {
myScroll:Scroll = new Scroll()
build() {
// 滚动区域
Scroll(this.myScroll){
Column(){
Row(){
Text('滚动内容1').height(500)
}
}
}
.height(500)
.scrollable(ScrollDirection.Horizontal) // 水平滚动 默认纵向
.scrollBar(true) // 显示滚动条
.scrollBarColor('#ff2787d9') // 滚动条颜色
.scrollBarWidth(10) // 滚动条宽度
.scrollBarHeight(10) // 滚动条高度
.scrollBarRadius(5) // 滚动条圆角
.onScroll((x,y)=>{
// 可以监听滚动事件,设置回到顶部按钮的显示与隐藏
})
}
.Button('按钮')
.onClick(()=>{
const y= this.myScroll.currentOffset().yOffset // 获取当前滚动位置
this.myScroll.scrollEdge(100,0,0) // 滚动到指定位置
})
}
Tabs- 组件
@Entry
@Component
struct Index {
titles:string[]=['首页','关注',"热门',"财经",'美食','旅行']
private controller: TabsController = new TabsController();
build() {
Tabs({barPosition: BarPosition.Start, index: 1, controller: this.controller }){
ForEach(this.titles,(item:string,index)=>{
Tabcontent(){
Text(`${item}内容`)
}
.tabBar(item)
}
}
.vertical(true)
.scrollable(true) //
.onChange((x,y)=>{
// 切换事件,可以设置tabbar的样式
})
}
}
tabs属性
- barPosition :调整位置 开头 或 结尾 (参数)
- vertical:调整导航 水平 或 垂直
- scrollable:调整是否 手势滑动 切换
- animationDuration:点击滑动动画时间
页面属性设置
- layoutweight:类似css的弹性盒子的flex属性,设置权重,使页面元素可以自适应
- justifyContent:类似css的flex-start、flex-end、center、space-between、space-around,设置元素在主轴上的对齐方式
- margin:margin({top: 10, left: 10, right: 10, bottom: 10}) 设置外边距
- alignItems: alignItems({VerticalAlign.Center}) 设置元素在交叉轴上的对齐方式
- backgroundImage:backgroundImage('https://baidu.com/images/1.png') 设置背景图片
- backgroundImageSize:backgroundImageSize({ImageSize.Cover}) 设置背景图片大小
- Flex:
- direction:flexDirection({FlexDirection.Row}) 设置主轴方向
- justifyContent:FlexAlign.SpaceAround
- alignItems:AlignItems.Center
- Padding:
- flexDirection:flexDirection({FlexDirection.Column}) 设置主轴方向
多态样式
@Entry
@Component
struct StateStylesSample {
build() {
Column() {
Button('Button1')
.stateStyles({
focused: {
.backgroundColor('#ffffeef0')
},
pressed: {
.backgroundColor('#ff707070')
},
normal: {
.backgroundColor('#ff2787d9')
}
})
.margin(20)
Button('Button2')
.stateStyles({
focused: {
.backgroundColor('#ffffeef0')
},
pressed: {
.backgroundColor('#ff707070')
},
normal: {
.backgroundColor('#ff2787d9')
}
})
}.margin('30%')
}
}
页面变量
页面变量分为三种:全局变量(文件组件外变量)、组件变量、组件状态变量 全局变量和组件变量在页面初始化的时候只渲染一次,组件状态变量在页面渲染的时候会根据状态值进行渲染
// 全局变量
const globalVar = '全局变量'
@Entry
@Component
struct Index {
// 组件变量
const componentVar = '组件变量'
// 组件状态变量
@state componentStateVar = '组件状态变量'
build() {}
}
@state 的变量若是嵌套的太深是无法触发渲染的, 只有第一层属性赋值的变化,即Object.keys(observedObject)返回的属性才渲染
@state p:person={
name:'张三',
age:18,
attr:{
height:180,
weight:70
}
}
// 只修改一个属性
p.attr.height = 120 // 不会触发渲染
// 直接修改了指针方向
p.attr={
height:120,
weight:70
} // 会触发渲染
组件传值

@Link
父子组件双向传值
// 父组件
@Entry
@Component
struct Index {
@State count: string = '父组件数据'
build() {
Column() {
Text('父组件')
ChildComponent(
childData: this.count
)
}
}
}
// 子组件
@Component
struct ChildComponent {
@Link childData: string = ''
build() {
Column() {
Text('子组件')
Text(this.childData)
}
}
}
@Prvide|@Consume
父子组件单向传值
// 父组件
@Entry
@Component
struct Index {
@Provide provideData: number = 3
build() {
Column() {
Text('父组件')
ChildComponent()
}
}
}
// 子孙组件
@Component
struct ChildComponent {
@Consume provideData: string = ''
build() {
Column() {
Text('子组件')
Text(this.provideData)
}
}
}

ObjectLink 主要针对的是数组对象的更新,比如一个数组对象,当数组对象中的某个属性发生变化时,ObjectLink可以监听到这个变化,然后更新对象组件,但是不想更新整个对象组件,只想更新组件内关联的页面元素,这时候用ObjectLink就可以实现这个功能
页面装饰器
- @Entry:入口组件
- @Component:组件
- @Builder:构建函数
- @Extend:扩展组件
- @Styles:样式
- @Preview:预览组件
- @Prop: 父子组件属性传递
- @Link:父子组件双向传值
- @Provide:父组件向子组件传值
- @Consume:子组件向父组件传值
- @State:组件状态变量
- @ObjectLink:对象组件监听
- @Observed:监听对象组件
样式|结构重用
@Extend
扩展组件(样式、事件)
<!-- 定义 -->
@Extend(Text) // 继承组件类型:Text、Button、Image等
function TextFont(){
.fontSize(20)
.layoutWeight(1)
.textAlign(TextAlign.Center)
}
<!-- 使用 -->
Text('tab1')
.TextFont()
.fontWeight(FontWeight.Bold)
Text('tab2')
.TextFont()
<!-- 不仅支持样式,还支持在定义的继承传递参数渲染不同数据 -->
@Extend(Text)
function TextFont(index: number){
.fontSize(20)
.layoutWeight(1)
.textAlign(TextAlign.Center)
.onClick(()=>{
console.log('点击了tab1'+index)
})
}
@Styles
抽取通用属性、事件,定义的函数不可以传递参数
<!-- 全局定义 -->
@Styles CommonStyle(){
.fontSize(20)
.layoutWeight(1)
.textAlign(TextAlign.Center)
}
// 直接在UI组件链式调用即可
<!-- 组件定义 -->
@Styles UIStyle(){
.fontSize(20)
.onClick(()=>{
// 访问组件内的变量
console.log(this.message)
})
}
@Builder
自定义构建函数(结构、样式、事件)
<!-- 全局定义 -->
@Builder
function GlobalUI(msg:string){
Row(){
Text('tab1')
.TextFont()
.fontWeight(FontWeight.Bold)
Text('tab2')
.TextFont()
Text('tab3')
.TextFont()
Text('tab4')
.TextFont()
.onClick(()=>{
console.log(msg)
})
}
.width('100%')
.height(70)
.position({
bottom:0,
left:0
})
}
<!-- 使用 -->
GlobalUI('点击了tab4')
<!-- 在组件内定义 -->
@Entry
@Component
struct Index {
@Builder
GlobalUI(msg:string){
// code
}
build() {
RelativeContainer() {
// 底部 tab
GlobalUI('点击了tab4')
}
.height('100%')
.backgroundColor(Color.Orange)
.width('100%')
}
}
@Component
自定义组件,可以直接将页面的部分内容直接封装成一个组件,在页面引入使用。也可以直接在页面内定义自定义组件,然后直接使用,不需要export或者export default导出
// 组件文件夹文件定义 /components/GlobalUI.ets
@Component
export struct GlobalUI{
// 定义组件变量和函数
title:string //可以参数覆盖
@state name:string='' //可以参数覆盖
publicMethod = ()=>{} //可以参数覆盖
sayHi(){} //不可以参数覆盖
build(){
Row(){
}
}
}
// 或者直接导出单个组件
export default GlobalUI
// 然后在页面引入使用
import {GlobalUI} from '../components/GlobalUI.ets'
@Entry
@Component
struct Index {
build() {
GlobalUI(
publicMethod(){}
)
}
}
若是要预览自定义组件,需要在
@Component上添加@Preview
组件插槽 @Builder、@BuilderParam 可以让自定义组件 允许外部传递 UI。
@Component
export struct BuildUI{
// 定义组件变量和函数
title:string
@BuilderParam ContentBuilder:()=>void=this.defaultContentBuilder //可以参数覆盖
publicMethod = ()=>{}
@Builder
defaultContentBuilder(){
Text('默认内容')
}
build(){
Row(){
// 使用 @BuilderParam 装饰的成员变量
this.ContentBuilder()
}
}
}
// 然后在页面引入使用
import {GlobalUI} from '../components/GlobalUI.ets'
@Entry
@Component
struct Index {
build() {
BuildUI(
publicMethod(){}
){
Text('插槽内容')
}
}
}

页面渲染
ForEach-渲染控制
对于重复的元素,可以使用ForEach进行渲染控制
ForEach(this.list, (item,index) => {
Row(){
Text(item)
.fontSize(20)
})
页面路由
路由文档 创建路由页面的方式有两种:
- 直接在
pages文件夹下创建页面arkts文件,或者在pages文件夹下创建文件夹,然后在文件夹下创建页面arkts文件,这种方式需要手动在路由中注册。即在resources/base/proofile文件夹下的main_pages.json文件中添加路由配置。 - 直接在
pages文件夹下右键创建页面文件的时候选择New=>Page,或者在pages文件夹下创建文件夹,然后在文件夹下创建页面文件选择New=>Page,文件夹下创建的页面文件会自动在路由中注册。
// Navigation(推荐)
@Entry
@Component
struct Index {
// 创建一个页面栈对象并传入Navigation
pageStack: NavPathStack = new NavPathStack();
build() {
Navigation(this.pageStack) {
Text('首页').onClick(()=> {
this.pageStack.pushPath({
url: 'pages/second_page',
params: {
name: 'second_page'
}
})
}
.title('Main')
}
}
// 普通跳转
this.pageStack.pushPath({ name: "PageOne", param: "PageOne Param" });
this.pageStack.pushPathByName("PageOne", "PageOne Param");
// 带返回回调的跳转,跳转时添加onPop回调,能在页面出栈时获取返回信息,并进行处理。
this.pageStack.pushPathByName('PageOne', "PageOne Param", (popInfo) => {
console.log('Pop page name is: ' + popInfo.info.name + ', result: ' + JSON.stringify(popInfo.result));
});
// 带错误码的跳转,跳转结束会触发异步回调,返回错误码信息。
this.pageStack.pushDestination({name: "PageOne", param: "PageOne Param"})
.catch((error: BusinessError) => {
console.error(`Push destination failed, error code = ${error.code}, error.message = ${error.message}.`);
}).then(() => {
console.info('Push destination succeed.');
});
this.pageStack.pushDestinationByName("PageOne", "PageOne Param")
.catch((error: BusinessError) => {
console.error(`Push destination failed, error code = ${error.code}, error.message = ${error.message}.`);
}).then(() => {
console.info('Push destination succeed.');
});
// 删除栈中name为PageOne的所有页面
this.pageStack.removeByName("PageOne");
// 删除指定索引的页面
this.pageStack.removeByIndexes([1, 3, 5]);
// 删除指定id的页面
this.pageStack.removeByNavDestinationId("1");
// 移动栈中name为PageOne的页面到栈顶
this.pageStack.moveToTop("PageOne");
// 移动栈中索引为1的页面到栈顶
this.pageStack.moveIndexToTop(1);
// 获取栈中所有页面name集合
this.pageStack.getAllPathName();
// 获取索引为1的页面参数
this.pageStack.getParamByIndex(1);
// 获取PageOne页面的参数
this.pageStack.getParamByName("PageOne");
// 获取PageOne页面的索引集合
this.pageStack.getIndexByName("PageOne");
// router (不推荐)
// 导入 两种方式都可以
import router from@ohos .router';
import {router} from "@kit.ArkUI"
//1.调用方法-普通跳转(可以返回)
router.pushUrl({
url:页面地址
})
//2.调用方法-替换跳转(无法返回)
router.replaceUrl({
url:'页面地址
})
//3.调用方法-(返回)
router.back()
//获取页面栈长度
router.getLength()
// 清空页面栈
router.clear()
页面栈跳转最长 32 个页面记录
路由模式
standard:标准模式,页面栈跳转,跳转的路由不管之前是否存在都在页面栈中添加single:单任务模式,跳转时,如果栈中存在目标页面,则将目标页面以上的页面全部出栈,并跳转到目标页面
//1.调用方法-普通跳转(可以返回)
router.pushUrl({
url:页面地址
mode:'standard'
})
//2.调用方法-替换跳转(无法返回)
router.replaceUrl({
url:'页面地址
mode:'single'
})
获取路由参数
- aboutToAppear:在创建自定义组件后,执行其build()函数之前执行(NavDestination创建之前),允许在该方法中改变状态变量,更改将在后续执行build()函数中生效。
- onWillAppear:NavDestination创建后,挂载到组件树之前执行,在该方法中更改状态变量会在当前帧显示生效。
- onAppear:通用生命周期事件,NavDestination组件挂载到组件树时执行。
- onWillShow:NavDestination组件布局显示之前执行,此时页面不可见(应用切换到前台不会触发)。
- onShown:NavDestination组件布局显示之后执行,此时页面已完成布局。
- onActive:NavDestination处于激活态(处于栈顶可操作,且上层无特殊组件遮挡)触发。
- onWillHide:NavDestination组件触发隐藏之前执行(应用切换到后台不会触发)。
- onInactive:NavDestination组件处于非激活态(处于非栈顶不可操作,或处于栈顶时上层有特殊组件遮挡)触发。
- onHidden:NavDestination组件触发隐藏后执行(非栈顶页面push进栈,栈顶页面pop出栈或应用切换到后台)。
- onWillDisappear:NavDestination组件即将销毁之前执行,如果有转场动画,会在动画前触发(栈顶页面pop出栈)。
- onDisappear:通用生命周期事件,NavDestination组件从组件树上卸载销毁时执行。
- aboutToDisappear:自定义组件析构销毁之前执行,不允许在该方法中改变状态变量。
aboutToAppear():void{
console.log('aboutToAppear:路由传递参数',this.pageStack.params)
}
onActive():void{
console.log('onActive:路由传递参数',this.pageStack.params)
}
onShown():void{
console.log('onShown:页面组件渲染完成')
}
onDisappear():void{
console.log('onDisappear:页面销毁')
}

路由拦截
在willShow回调中通过修改路由栈来实现路由拦截重定向的能力。
this.pageStack.setInterception({
willShow: (from: NavDestinationContext | "navBar", to: NavDestinationContext | "navBar",
operation: NavigationOperation, animated: boolean) => {
if (typeof to === "string") {
console.log("target page is navigation home page.");
return;
}
// 将跳转到PageTwo的路由重定向到PageOne
let target: NavDestinationContext = to as NavDestinationContext;
if (target.pathInfo.name === 'PageTwo') {
target.pathStack.pop();
target.pathStack.pushPathByName('PageOne', null);
}
}
})
部署
项目解构
- entry:HarmonyOS工程模块,编译构建生成一个HAP包。
- src>main>ets:用于存放ArkTS源码,
- src>main>ets>entryability:应用/服务的入口,
- src>main>ets>pages:应用/服务包含的页面。
- src>main>resources:用于存放应用/服务所用到的资源文件。
- src>main>module.json5:模块应用配置文件。
- build-profile.json5:当前的模块信息、编译信息配置项,包括buildOption、targets配置等
- hvigorfile.ts:模块级编译构建任务脚本,开发者可以自定义相关任务和代码实现。
- obfuscation-rules.txt:混淆规则文件。。
- oh-modules:用于存放三方库依赖信息。
- build-profile.json5:应用级配置信息,包括签名signingConfigs、产品配置products等。。
- hvigorfile.ts:应用级编译构建任务脚本。
[项目名称]/entry/src/main/ets/entryability/EntryAbility.ets
在当前文件内配置了应用的各种生命配置
- onCreate:应用创建时调用
- onDestroy:应用销毁时调用
- onWindowStageCreate:应用窗口创建时调用
- 在windowStage.loadContent可以设置App默认启动页
- onWindowStageDestroy:应用窗口销毁时调用
- onForeground:应用进入前台时调用
- onBackground:应用进入后台时调用
AppScope/app.json5
{
"app": {
// 包名 不可省略
"bundleName": "com.example.myapplication",
// 应用开发厂商描述 不可省略
"vendor": "example",
// 应用版本号
"versionCode": 1000000,
// 应用版本名称
"versionName": "1.0.0",
// 应用图标
"icon": "$media:layered_image",
// 应用名称
"label": "$string:app_name"
}
}
src/main/module.json5
{
"module": {
"name": "entry",
"type": "entry",
"description": "$string:module_desc",
"mainElement": "EntryAbility",
"deviceTypes": [
"phone",
"tablet",
"2in1"
],
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:EntryAbility_desc",
"icon": "$media:layered_image", //应用图标
"label": "$string:EntryAbility_label",//应用名称
"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": [
"entity.system.home"
],
"actions": [
"action.system.home"
]
}
]
}
],
"extensionAbilities": [
{
"name": "EntryBackupAbility",
"srcEntry": "./ets/entrybackupability/EntryBackupAbility.ets",
"type": "backup",
"exported": false,
"metadata": [
{
"name": "ohos.extension.backup",
"resource": "$profile:backup_config"
}
],
}
]
}
}
Ability
一个手机可以同时运行多个应用,每个应用可以包含多个Ability,每个Ability负责完成一个特定的功能。Ability是应用的基本组成单元,一个应用可以包含一个或多个Ability。支持应用内的交互(微信和微信小程序或者说是启动了微信和打开微信的任务(视频语音聊天、小程序))、应用间的交互、数据共享、远程服务调用等。 更多介绍
创建Ability
每个项目都可以由多个Ability组成,每个Ability都对应一个页面,每个页面都对应一个Ability。在module.json5中配置了Ability,可以设置APP默认启动的Ability。在ets中创建对应的Ability文件,可以设置启动的Ability的默认启动页。 例如:打开微信,可以打开微信的首页,也可以打开微信的某个内置小程序。

{
"module": {
"name": "entry",
....
"deliveryWithInstall": true,
"installationFree": false,
"pages": "$profile:main_pages",
"abilities": [
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"description": "$string:EntryAbility_desc",
"icon": "$media:layered_image",
"label": "$string:EntryAbility_label",
"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": [
"entity.system.home"
],
"actions": [
"action.system.home"
]
}
]
},
{
"name": "twoAbility",
"srcEntry": "./ets/twoability/twoAbility.ets",
"description": "$string:twoAbility_desc",
"icon": "$media:layered_image",
"label": "$string:twoAbility_label",
"startWindowIcon": "$media:startIcon",
"startWindowBackground": "$color:start_window_background"
}
],
"extensionAbilities": []
}
}
在上面的 abilities 属性可以设置应用启动的默认Ability(直接将exported和skills属性剪切到另一个对象即可)。icon、label、startWindowIcon、startWindowBackground属性可以设置应用启动页面的图标、名称、启动页面的图标、启动页面的背景色。
Ability的交互
Ability 同模块的交互(即打开内置小程序)
Ability 应用间交互(淘宝或者京东打开微信和支付宝支付)