Search K
Appearance
Appearance
最近部门接到一个鸿蒙开发的项目,我们部门没有会鸿蒙开发的人员,公司又恰巧这时候降薪,别说招新人了,旧人都保不住。部门领导就想让我突击学一下鸿蒙顶上,没办法只能答应(极品牛马)。吐槽归吐槽,但是博主自己也计划学习一下鸿蒙开发,因为国家在大力推动鸿蒙开发的生态,提前储备一下也很好。最后花了两天时间入门了鸿蒙开发,达到了可以参与项目开发的程度。
不得不说一句鸿蒙开发对前端开发人员真的极其友好,所以想以前端开发的视角将鸿蒙开发中涉及到的要点记录下来,并非从0记录,主要是针对已有前端开发经验(例如Vue
、React
、Css
)的人员。
如果要让我来总结鸿蒙开发的语法,我理解的是鸿蒙融合了各家语言的一些特点,鸿蒙用的语法叫做ArkTs
,在Typescript
的基础上进行扩展,例如Java
中的注解,React
的状态管理、模板语法,Vue
的一些Api
特性,样式上完全采用Css
,所以对于前端开发人员来说学习鸿蒙真的So Easy。
React
模板渲染采用的是jsx
语法,我理解成都是一种声明式的语法,鸿蒙也可以理解成类似的语法,有React
开发经验的同学一眼就能看出。先看下整体结构:
@Component
export struct HelloHarmony {
@State count: number = 0
submit: () => void = () => {
this.count++;
}
build() {
Column(){
Text("学鸿蒙,就来黑马程序员").fontSize(25).fontWeight(FontWeight.Medium).textAlign(TextAlign.Start).width("100%")
Flex({direction: FlexDirection.Row,alignItems: ItemAlign.Center}){
Text("置顶").fontSize(15).fontWeight(FontWeight.Normal).fontColor("red")
Text("新华社").fontSize(15).fontColor('#666666').margin({left: 6})
Text(this.count + "评论").fontSize(15).fontColor("#666666").margin({left: 6})
}.margin({top: 10})
Row(){
Button("点赞",{type: ButtonType.Normal,stateEffect: true}).width(80).borderRadius(8).onClick(() => {
this.submit()
})
}.margin({top:70})
}.padding(20)
}
}
总结几个关键点:
@Component
注解标注该struct
结果提为一个组件,这个组件可以通过添加export
关键字导出,鸿蒙的组件引用是基于前端的ES
规范的,这里面定义的变量或者函数,在模板或其他函数中都需要用this
关键字访问。Css
盒子模型,不是标准盒子模型,是我们开发中常用的IE
或者叫怪异盒子模型,这点好评。组件的样式使用的是Css
,所以Css
中80%
的属性在鸿蒙中都可以使用,名字大部分是一致的。build
函数,等同于react
中的render
函数,这里是组件抛出的实际内容。@State
定义的变量可以理解为是组件中的内置变量,@State
修饰的变量值每次改变都会引发页面的重新渲染,所以需要在页面上渲染并且会改变的值可以用@State
定义,注意在struct
中定义的一切变量或函数在模板中都需要使用this
关键字访问。这里Row
和Column
是鸿蒙开发中常用的标签,推荐去官网好好看下,可以理解为是Flex
布局的增强版,鸿蒙开发中应该都是块级标签,没有行内标签和块级标签之分。
@Component
export struct LoginForm {
build() {
Column({space: 20}){
Row(){
Column(){
Image($r('app.media.app_icon')).width(60).height(60).borderRadius(30)
}.width('100%').margin({top: "15%"})
}.justifyContent(FlexAlign.Center)
Row(){
TextInput({placeholder: "请输入用户名"})
}.width("100%").margin({top:20})
Row(){
TextInput({placeholder: "请输入密码"}).type(InputType.Password)
}.width("100%")
Button("登录").width("100%")
Row(){
Text("前往注册")
Text("忘记密码").margin({left:15})
}.justifyContent(FlexAlign.Center)
}.padding(20)
}
}
$r('app.media.app_icon')
引入的是src/main/resources/base/media
路径下的静态资源
这一块是我觉得挺有意思的地方,除了单独创建封装子组件,鸿蒙提供了很多种封装样式和模板的方法,直接展示代码段:
/**
* @Extend:扩展组件,样式、事件,实现复用的效果
*/
@Extend(Text)
function textFn(color: ResourceColor,txt:string){
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor(color)
.padding(20)
.backgroundColor('#e1e1e1')
.width('100%')
.onClick(() => {
AlertDialog.show({
message: txt
})
})
}
// 注意@Styles不支持传参
/**
* @Styles: 抽取通用属性、事件
*/
// 1、全局定义
@Styles function commonStyles(){
.width(100)
.height(100)
.onClick(() => {
AlertDialog.show({
message: "点击"
})
})
}
/**
* 全局@Builde封装,自定义构建函数(结构、样式、事件)
*/
@Builder
function navItem(icon: ResourceStr, text: string){
Column({space:10}){
Image(icon).width(80)
Text(text)
}.width('100%').onClick(() => {
AlertDialog.show({
message: "点击" + text
})
})
}
@Component
export struct Study02 {
@State count: number = 0
color: string = 'blue'
// 组件内部定义@Styles 只有在组件内定义才可以访问状态
@Styles setBg() {
.backgroundColor(this.color)
}
submit: () => void = () => {
this.count++;
}
build() {
Column({space: 30}) {
Text('张三Exrend').textFn(Color.Blue,"点击张三")
Text('李四Exrend').textFn(Color.Blue,"点击李四")
Text('王五Exrend').textFn(Color.Blue,"点击王五")
Text("公共Styles").setBg().commonStyles().fontColor("#fff")
Column(){
navItem($r('app.media.app_icon'),"阿里拍卖")
navItem($r('app.media.app_icon'),"菜鸟")
}
}
}
}
@Extend
注解让开发者可以自由扩展原生组件的样式和属性,@Styles
不支持传参,用于抽取通用属性、事件,@Builder
注解用的也很多,可以理解为自定义构建函数(结构、样式、事件),可理解为颗粒度更小的组件,根据不同的场景使用不同的方法。
// 组件外的普通变量,改变不会引起视图变化
let msg3:string = "test3"
interface Person{
stuId: number
name: string
gerder: string
age: number
url: string
}
@Component
export struct StudyBase {
// 组件内的普通变量,改变不会引起视图变化
msg2:string = "test2"
// @State 标注的变量在变量改变时会触发UI的渲染刷新
@State mas: string = "hello world"
@State age: number = 15
@State stuArr: Person[] = [
{
stuId: 1,
name: "张三",
gerder: "男",
age: 18,
url: 'app.media.app_icon'
},
{
stuId: 2,
name: "李四",
gerder: "女",
age: 24,
url: 'app.media.app_icon'
},
{
stuId: 3,
name: "王五",
gerder: "男",
url: 'app.media.app_icon',
age: 26
},
{
stuId: 4,
name: "张三",
url: 'app.media.app_icon',
gerder: "男",
age: 18
},
]
// 复杂对象数组改变其中某一项只能替换整个item
changeTxt = ():void => {
this.stuArr[1] = {
stuId: 2,
name: "张翼德",
gerder: "女",
age: 24,
url: 'app.media.app_icon'
}
}
build() {
Column({space: 20}){
Row(){
Text(this.msg2)
Text(msg3)
}
Column(){
Text(String(this.age))
if(this.age < 18){
Text("18岁以下")
}
else if(this.age < 30){
Text("30岁以下")
}
else{
Text("30岁以上!")
}
Button("增加年龄").onClick(() => {
this.age += 5
})
}
// 循环渲染示例
Column({space: 20}){
Text("循环渲染示例").fontSize(24)
ForEach(this.stuArr, (item:Person,index) => {
Row(){
Text(item.name).fontSize(24).fontColor(Color.Orange).layoutWeight(1)
Image($r(item.url)).width(60).height(60).borderRadius(10)
}.justifyContent(FlexAlign.SpaceBetween).width('100%').padding(15).backgroundColor('#e1e1e1')
})
Button("改变第一项").onClick((event: ClickEvent) => {
this.changeTxt()
})
}
}.padding(20).backgroundColor('#fff')
}
}
这里对于学习过Vue
或React
的也比较简单,不做赘述
这个组件实测下来没有List
和ListItem组件好用
@Component
export struct Study03 {
myScroll: Scroller = new Scroller()
build() {
Column(){
Scroll(this.myScroll){
Column({space: 10}){
ForEach(Array.from({length: 10}) , (item:string,index) => {
Text("测试文本"+(index + 1)).fontColor('#fff').width('100%').borderRadius(10).backgroundColor(Color.Orange).textAlign(TextAlign.Center).height(60)
})
}
}.width('100%').height(400).scrollable(ScrollDirection.Vertical)
.scrollBar(BarState.Auto)
.scrollBarColor(Color.Blue)
.scrollBarWidth(5)
.edgeEffect(EdgeEffect.Fade)
.onScrollStop(() => {
AlertDialog.show({
message: `已经滚动的距离,${this.myScroll.currentOffset().yOffset}`
})
})
Button('控制滚动条位置').margin({top:50}).onClick(() => {
this.myScroll.scrollEdge(Edge.Top)
})
Button('获取已经滚动的距离').margin({ top:30 }).onClick(() => {
const y = this.myScroll.currentOffset().yOffset
AlertDialog.show({
message: `y: ${y}`
})
})
}.padding(20)
}
}
@Component
export struct SonCom {
@Builder
tDefaultBuilder(){
Text('第一个插槽默认内容')
}
@Builder
cDefaultBuilder(){
Text('第二个插槽默认内容')
}
@BuilderParam tBuilder: () => void = this.tDefaultBuilder
@BuilderParam cBuilder: () => void = this.cDefaultBuilder
build(){
Scroll(){
Column({space: 20}){
Text("插槽-tBuilder").width('100%').textAlign(TextAlign.Center)
// 熟悉的插槽。。。 理解为匿名插槽 <slot />
this.tBuilder()
Text("插槽-cBuilder").width('100%').textAlign(TextAlign.Center)
this.cBuilder()
}
}
}
}
在父组件中使用
@Component
struct Parent{
@Builder ftBuilder(){
Text("我是tBuilder内部");
}
@Builder fcBuilder(){
Text("我是cBuilder内部");
}
build(){
SonCom({
tBuilder: this.ftBuilder,
cBuilder: this.fcBuilder
})
}
}
组件通信一直都是前端开发中必不可少的部分,鸿蒙的父子组件传参跟react
的方式很像,通信方式有:单向通信@Prop
,双向通信@Link
,还有Provide/Inject
@Provide/@Consume
/**
* 插槽插槽
*/
@Component
struct SonChildCom {
@Consume themeColor: string
build() {
Column(){
Text("孙子组件")
Text(this.themeColor)
Button("孙子组件修改值").onClick(() => {
this.themeColor = "red"
})
}
}
}
@Component
export struct SonCom {
// 父子通信演示 单向通信
// 父向子通信
@Prop info : string
// 子向父通信,和react方式一样
changeInfo = (txt:string) => {}
// 双向绑定,相当于v-model
@Link count: number
// 相当于inject接收变量
@Consume themeColor: string
build(){
Scroll(){
Column({space: 20}){
Text("info")
Text(this.info)
Button("修改info").onClick(() => { this.changeInfo('新的值') })
Text("count")
Text(this.count.toString())
Button("内部修改count").onClick(() => { this.count++ })
Text("二级组件")
Text(this.themeColor)
Button("二级组件修改值").onClick(() => {
this.themeColor = "red"
})
// 孙子组件
SonChildCom()
}
}
}
}
最外层父组件
@Entry
@Component
struct Index {
// 要向子组件发送的值
@State info: string = "么么哒"
// 父组件传递给子组件使用的方法,子组件可以调用这个方法实现子组件向父组件通信
changeInfo = (txt:string) => {
this.info = txt
}
// 双向绑定值演示
@State count:number = 0
// 相当于Provide
@Provide themeColor: string = 'yellow'
build() {
Column(){
// 这里Count使用的是@Link,子组件或父组件改变均可以更新值
Button("外部修改count").onClick(() => { this.count++ }).margin({top:10})
// 组件通信
SonCom({
info: this.info,
changeInfo: this.changeInfo,
count: this.count
})
}
}
}
当更新复杂的对象数组时,如果直接更新其中某一项的值是不会触发页面渲染,例如this.list[index].age = 15
,这点跟Vue
很相似,这时候需要使用@ObjectLink
来解决这个问题,具体方法就是在子组件中需要更新的那一项使用@ObjectLink
注解标识。当然也可以整体更新那一项也可以触发页面刷新, 例如:this.list[index] = { age:14,... }
/**
* Observed用法
*/
interface IPerson {
id: number
name: string
age: number
}
@Observed
class Person implements IPerson{
id: number
name: string
age: number
constructor(config:IPerson) {
this.id = config.id
this.name = config.name
this.age = config.age
}
}
@Component
export struct ObservedAndLink {
@State personList: Person[] = [
new Person({
id: 1,
name: '张三',
age: 18
}),
new Person({
id: 2,
name: '张三',
age: 18
}),
new Person({
id: 3,
name: '张三',
age: 18
})
]
build() {
Column({space: 20}){
Text('父组件').fontSize(20)
// 属性更新逻辑: 当我们@ObjectLink装饰过的数据属性改变的时候,就会监听到
// 遍历依赖它的@ObjectLink 包装类,通知数据更新
Text(this.personList[0].age.toString()).fontSize(20)
Column({space:10}){
List({space: 10}){
ForEach(this.personList,(item: Person,index:number) => {
ItemCom({
info:item,
addAge: () => {
item.age++
AlertDialog.show({
message: JSON.stringify(this.personList)
})
}
})
})
}
}
}
}
}
@Component
struct ItemCom{
@ObjectLink info : Person
addAge = () => {}
build(){
Row(){
Text('姓名:'+this.info.name)
Text('年龄:'+this.info.age)
Blank()
Button('修改数据').onClick(() => {
// this.info.age++
this.addAge()
})
}
}
}
配置文件我目前了解到的主要关注三个主要配置文件
main_pages.json
配置所有页面路由,不注册进来就无法访问,顺便提一句@Entry
标注的struct
就代表一个页面,可以被跳转
src/main/resources/base/profile/main_pages.json
{
"src": [
"pages/Index"
]
}
app.json5
配置包名,版本信息,应用图标 AppScope/app.json5
{
"app": {
"bundleName": "com.example.myapplication",
"vendor": "example",
"versionCode": 1000000,
"versionName": "1.0.0",
"icon": "$media:app_icon",
"label": "$string:app_name"
}
}
module.json5
主要针对的是abilities
中的配置,这里解释下鸿蒙中认为一个Ability
就是一个任务,在使用安卓手机的时候有时候一个APP可以创建多个任务卡片,这里一个任务就是一个Ability
,这里可以配置启动的时候采用哪个任务。 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"
}
],
}
]
}
}
待补充