鸿蒙ms参考

1.常用布局

Row/Column:线性布局

Stack:层叠布局

Flex:弹性布局

RelativeContainer:相对布局

List:列表

Grid

Swiper:轮播

Scroll搭配Column和Row一起使用实现滚动的效果。

2.常用的组件

Button:按钮

Progress:进度条

Text/Span:文本

TextInput/TextArea:输入框

CustomDialog:自定义弹框

Blank:类似于Flutter中的Space()和Expanded/安卓中的layout_weight权重。意为着占满Row或者Column的剩余空间。

3.路由跳转

1)页面栈的最大容量为32个页面,使用 router.clear() 方法可以清空页面栈,释放内存。

2)Router有两种页面跳转模式:

router.pushUrl():目标页不会替换当前页面,而是压入页面栈,因此可以用 router.back() 返回当前页面。

router.ReplaceUrl():目标页会替换当前页,当前页面被销毁并释放资源,无法返回当前页面。

3)Router的两种实例模式

Standard:标准实例模式,每次跳转都会新建一个目标页并压入栈顶,默认就是这种模式。

Single:单实例模式,如果目标页已经在栈中,则离栈顶最近的该页面会被移动到栈顶。

复制代码
router.pushUrl(
  {
    url:'pages/secondpage',
    params:{name:'张三'}//传递的参数
  },
  router.RouterMode.Standard,//跳转模式 Single/Stantard
  err=>{
    if(err){
      console.log('路由失败')
      console.log(err.code+'')
      console.log(err.message)
      console.log(err.name)
    }
  },
)
//在aboutToAppear中接收传递过来的参数
aboutToAppear(){
  const params = router.getParams();
  const id:number = params['id'];
  const name:string = params['name'];
  console.log(`接受到的参数: id=${id},name=${name}`)
}

返回上一个页面:

1)返回上一个页面

复制代码
router.back();

2)返回上一个页面并携带参数

复制代码
router.back({//使用back返回到上一个页面
  url:'pages/routerdemo/first_router',
  params:{'backParams':'secondRouter'}
})
上一个页面接收参数:
onPageShow(){//在onPageShow中接收下一个页面返回的参数
  const params = router.getParams();
  if(params){//判断参数是否为空
    const backParams:string = params['backParams']
    console.log(`接受不返回的参数为:${backParams}`)
  }
}

返回某个页面并销毁中间的其它页面:

使用场景:从页面1往下一直跳转,跳转到页面n后,从页面n返回到页面1并销毁中间的其它页面。

直接使用 router.back 回到页面1就可以了。

复制代码
router.back({
  url:'pages/routerdemo/first_router',
})

4.组件导航

1)Navigation

2)Tabs+TabContent(可以实现安卓中Fragment+ViewPager的页面切换的效果)

5.媒体查询:mediaquery

媒体查询可根据不同设备类型或同设备不同状态修改应用的样式,一般用于当屏幕发生动态改变时或者针对设备的属性信息设计出相匹配的布局。

6.动画

动画包含显示动画和属性动画,以及共享动画用于转场效果。

1)显示动画是通过animateTo闭包(一个函数在另外一个函数中,这个内部函数可以使用外部函数的局部变量)中的代码导致的状态变化插入过渡动效,闭包内的变化都会触发动画,包括由数据变化引起的组件的增删,组件属性的变化等等,可以进行比较复杂的动画。

显示动画除了直接改变布局方式外,还可以直接修改组件的宽,高,位置属性。

代码如下:

复制代码
@State mWidget:number = 70
@State mHeight:number = 40
@State flag:boolean = false
 
Button('改变宽高的动画')
  .onClick(() => {
    //animateTo除了可以改变组件属性如大小,也可以改变组件的颜色等等
    animateTo({ duration: 2000, curve: Curve.Ease }, () => {
      if (this.flag) {
        this.mWidget = 70
        this.mHeight = 40
        this.flag = false
      } else {
        this.mWidget = 140
        this.mHeight = 80
        this.flag = true
      }
    })
  })
  .fontSize(12)
  .width(this.mWidget)
  .height(this.mHeight)
  .margin({ top: 80 })
 

组件的插入,删除过程即为组件本身的转场过程,组件的插入,删除动画成为组件内转场动画。通过组件内转场动画,可以定义组件出现和消失的效果。组件内转场动画使用transition。

transition函数的入参就是组件内转场的效果,可以定义平移,透明度,旋转和缩放在这几种转场样式的单个或者组合的转场效果,必须和animateTo一起使用才能产生组件转场效果。

复制代码
Column() {
  Button(this.show)
    .onClick(() => {
      if (this.flagShow) {
        this.show = 'hide'
      } else {
        this.show = 'show'
      }
      animateTo({ duration: 3000 }, () => {
        //动画闭包内控制Image组件的出现和消失
        this.flagShow = !this.flagShow
      })
    })
  if(this.flagShow){
    Image($r('app.media.app_icon'))
      //显示/增加
      .transition({
        type: TransitionType.Insert,
        translate: { x: 200, y: -200 }
      })
      //隐藏/删除
      .transition({
        type: TransitionType.Delete,
        opacity: 0,
        scale: {
          x: 0,
          y: 0
        }
      })
  }
}

2)属性动画

显示动画把要执行动画的属性的修改放在闭包函数中触发动画,而属性动画则不需要使用闭包,把animation属性放在要做属性动画的组件的属性后面就可以了。

想要组件随某个属性值的变化而产生动画,这个属性需要加在animation属性之前。有的属性变化不希望通过animation产生属性动画,可以放在animation的后面。

Button('text')

.width(this.mWidget)//通过@State状态管理来动态设置宽高,并产生动画

.height(this.mHeight)

// animation只对其上面的width、height属性生效,时长为1000ms,曲线为Ease

.animation({duration:2000,curve:Curve.Ease})

// animation对下面的backgroundColor属性不生效

// backgroundColor会直接跳变,不会产生动画

.backgroundColor(this.mColor)

.margin({top:50})

3)共享元素转场动画(页面之间转场动画)

在不同的页面之间,有使用相同的元素的场景,可以使用共享元素转场动画衔接,为了突出不同页面之间相同元素的关联性,可以为他们添加共享元素转场动画,如果相同元素在不同页面之间的大小有明显差异,可以达到放大缩小的效果。

共享元素有两种:

1)Exchange类型的共享元素转场:需要在两个页面中存在通过sharedTransition配置为相同id的组件。

2)Static类型的共享元素转场:只需要在一个页面中有Static的共享元素,不能在两个页面中出现相同id的Static类型的共享元素,一般用于标题逐渐出现和隐藏的场景。

复制代码
//第一个页面
Column(){
  Image($r('app.media.app_icon'))
    .sharedTransition('sharedImage1',{duration:2000,curve:Curve.Linear})
    .onClick(()=>{
      router.pushUrl({
        url:'pages/animate/shared_animate_secnod_page'
      })
    })
}
第二个页面:
Column() {
  Text('SharedTransition dest page')
    .sharedTransition('shanredText',{
      duration:500,
      curve:Curve.Linear,
      type:SharedTransitionEffectType.Static//Static类型
    })
  Image($r('app.media.app_icon'))
    Exchange类型
    .sharedTransition('sharedImage1',{
      duration:500,
      curve:Curve.Linear
    })
}

7.自定义组件

1)自定义组件

自定义组件的结构为 struct+组件名,不能有继承关系。@Component装饰器仅能修饰struct关键字声明的数据结构,struct被@Component装饰后就具备了组件化的能力,需要使用build方法描述UI,一个struct只能被一个@Component装饰。@Entry装饰的自定义组件可以作为页面的入口,在单个页面中最多可以使用@Entry装饰一个自定义组件,@Entry可以接受一个可选的LocalStorage的参数。

build函数:

@Entry装饰的自定义组件,build函数下的根节点必须为容器组件,其中ForEach禁止中作为根节点;

@Component装饰的自定义组件,build函数下的根节点可以为非容器组件,其中ForEach禁止作为根节点;

复制代码
@Component
struct ChildComponent {
  build() {
    // 根节点唯一且必要,可为非容器组件
    Image('test.jpg')
  }
}

不允许在build中声明本地变量;

复制代码
build() {
  // 反例:不允许声明本地变量
  let a: number = 1;
}

不允许直接使用console.log,可以允许在方法或者函数里面使用。

复制代码
build() {
  // 反例:不允许console.info
  console.info('print debug log');
}

不允许使用switch语法,如果需要使用条件判断可以使用if

不允许使用表达式:

复制代码
build() {
  Column() {
    // 反例:不允许使用表达式
    (this.aVar > 10) ? Text('...') : Image('...')
  }
}

如果在其它的文件中使用自定义组件,需要使用export关键字导出并在使用的页面import这个自定义组件。

页面和自定义组件的生命周期:

页面的生命周期,即被@Entry装饰的组件的生命周期:

aboutToAppear:组件即将出现时调用,在build()函数和onPageShow之前执行。

onPageShow:页面每次显示时触发一次,在aboutToAppear和build之后执行。

onPageHide:页面每次隐藏时触发一次(返回上一个页面和跳转到下一个页面时都会触发这个方法),在aboutToDisappear之前执行。

aboutToDisappear:在组件销毁之前调用,在onPageHide之后执行。

组件的生命周期,即一般用@Component装饰的自定义组件的生命周期,只有如下两个:

aboutToAppear

aboutToDisappear

@Entry装饰的组件的生命周期流程:

aboutToAppear->build->onPageShow->onPageHide->aboutToDisappear

1)@Builder:可以达到UI复用的效果,将重复使用的UI抽取成一个方法,使用@Builder可以修饰全局的自定义构建函数和局部的在自定义构建函数。

  • 全局的自定义构建函数使用 @Builder+function的方式来创建,需要在组件外面创建。
复制代码
@Builder function ListViewItem(item:string){
  Row() {
    Text(`元素为 ${item}`).fontSize(16)
  }.backgroundColor(Color.Red).height(30).width(100)
}
  • 局部的自定义构建函数:局部的自定义构建函数使用 @Builder 的格式来创建,没有function关键字,在组件内创建。

2)@Styles:实现自定义样式,自定义样式分为全局的自定义样式和局部的自定义样式。

全局的自定义样式,使用 @Styles function 的方式来创建,全局的自定义样式的属性只能设置组件的通用属性,如果是某个组件特有的属性则不支持,比如Text的fontSize则不支持。如果想在全局的自定义样式中设置某个组件特有的属性,可以使用 @Extend 关键字;

局部的自定义样式,没有function关键字:

复制代码
@Styles function fillScreen() {
  .backgroundColor(Color.Red)
  .height(30)
  .width(100)
}

3)@Extend:设置某个组件特有的样式,注意:@Extend只能写在全局中,不能写在局部中。

复制代码
@Extend(Text) function priceText() {
  .fontSize(16)
  .fontColor(Color.Blue)
}

4)stateStyles多态样式:使用stateStyles可以实现多态样式,可以设置组件的禁用样式,正常样式和按下样式。

5)@CustomDialog:自定义弹框

8.渲染控制:if/else ForEach

LazyForEach:从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。当在滚动容器中使用了LazyForEach,框架会根据滚动容器可视区域按需创建组件,当组件滑出可视区域外时,框架会进行组件销毁回收以降低内存占用。

9.Stage模型和FA模型,ArkTs,ArkUI,进程和线程,架构,AbilityStage,WindowStage

1)两种应用模型:Stage模型和FA模型

Stage模型:HarmonyOS API 9开始新增的模型,是目前主推且会长期演进的模型。在该模型中,由于提供了AbilityStage、WindowStage等类作为应用组件和Window窗口的"舞台",因此称这种应用模型为Stage模型。

Stage模型是HarmonyOS多端统一的应用开发框架中的一个核心概念,用于描述应用的界面层次结构和组件之间的关系。它主要包含UIAbility组件和ExtensionAbility组件、WindowStage、Context和AbilityStage。

在Stage模型中,

程序运行期:

AbilityStage持有UIAbility,UIAbility持有WindowStage,WindowStage持有Window,Window持有ArkUI Page。

编译器:将Module编译为HAP的文件

Stage模型中组件的分类包含UIAbility组件和ExtensionAbility组件,UIAbility组件包含UI界面,提供展示UI的能力,主要用于和用户交互。ExtensionAbility组件提供特定场景(如卡片、输入法)的扩展能力。

FA模型:不再主推。

Stage模型与FA模型最大的区别在于:Stage模型中,多个应用组件(鸿蒙中的Ability称为组件)共享同一个ArkTS引擎实例;而FA模型中,每个应用组件独享一个ArkTS引擎实例

。因此在Stage模型中,应用组件之间可以方便的共享对象和状态,同时减少复杂应用运行对内存的占用。

2)ArkTs

ArkTS是HarmonyOS优选的主力应用开发语言,它在TypeScript(简称TS)的基础上,匹配ArkUI框架,扩展了声明式UI(声明式UI的核心思想是将UI的描述与逻辑分离),状态管理等相应的能力。

JavaScript:是开发web的一种高级脚本语言,属于弱类型语言

TypeScript:是JavaScript的一个超集,它扩展了JavaScript的语法,它属于强类型语言,需要给它一个类型,也可以不给类型,系统会自动推导类型。

当前,ArkTS在TS的基础上主要扩展了声明式UI能力,让开发者能够以更简洁、更自然的方式开发高性能应用。推荐用ArKTS开发UI相关内容,TS可以用来开发业务逻辑相关内容。

3)ArkUI

HarmonyOS提供了一套UI开发框架,即方舟开发框架(ArkUI框架),使用的语言为ArkTS语言,UI更新方式为数据驱动更新。

多个应用组件共享同一个ArkTS引擎(运行ArkTS语言的虚拟机)实例,应用组件之间可以方便的共享对象和状态,同时减少复杂应用运行对内存的占用。

4)进程

应用中(同一包名)的所有UIAbility运行在同一个独立进程中。一个进程可以运行多个应用组件(Ability)实例,所有应用组件实例共享一个ArkTS引擎实例,ArkTS引擎实例在主线程上创建。

5)线程

HarmonyOS应用中每个进程都会有一个主线程,主线程有如下职责:

  1. 执行UI绘制;
  2. 管理主线程的ArkTS引擎实例,使多个UIAbility组件能够运行在其之上;
  3. 管理其他线程(例如Worker线程)的ArkTS引擎实例,例如启动和终止其他线程;
  4. 分发交互事件;
  5. 处理应用代码的回调,包括事件处理和生命周期管理;
  6. 接收Worker线程发送的消息;

除主线程外,还有一类与主线程并行的独立线程Worker,主要用于执行耗时操作,但不可以直接操作UI。Worker线程在主线程中创建,与主线程相互独立。最多可以创建8个Worker。

worker线程:

复制代码
let parent = worker.workerPort
// 处理来自主线程的消息
parent.onmessage =  function(message){
  console.info("onmessage: " + message.data)
  parent.postMessage("message from main thread.")
}

主线程:

复制代码
let wk = new worker.ThreadWorker("entry/ets/workers/worker.ts");
wk.postMessage("message from main thread.")


// 处理来自worker线程的消息
wk.onmessage = (message) =>{
  console.info("message from worker: " + message.data)
  // 根据业务按需停止worker线程
  wk.terminate()
}

主线程和worker线程发送消息都是使用postMessage发送消息。

线程间通信目前主要有Emitter和Worker两种方式,其中Emitter主要用于主线程和Worker线程,Worker线程和Worker线程之间的事件同步(线程之间有交互)。UIAbility组件与UI均在主线程中。 Worker主要用于新开一个线程执行耗时任务(线程间可能没有交互)。

注意:虽然EventHub和Emitter都可以实现事件订阅发布的功能,但是EventHub是在同一个线程内操作的,而Emitter的操作是在多线程进行的。

6)架构

应用软件涉及的芯片平台多种多样,有x86、ARM等,还有32位、64位之分,HarmonyOS为应用程序包屏蔽了芯片平台的差异,使应用程序包在不同的芯片平台都能够安装运行。

7)AbilityStage:

AbilityStage是一个Module级别的组件容器,应用的HAP在首次加载时会创建一个AbilityStage实例,可以对该Module进行初始化等操作。AbilityStage与Module一一对应,即一个Module拥有一个AbilityStage。

每个Entry类型或者Feature类型的HAP在运行期都有一个AbilityStage类实例,当HAP中的代码首次被加载到进程中的时候,系统会先创建AbilityStage实例。

如何创建一个AbilityStage?

1)在工程Module对应的ets目录下,右键选择"New > Directory",新建一个目录并命名为myabilitystage。

2)在myabilitystage目录,右键选择"New > TypeScript File",新建一个TypeScript文件并命名为MyAbilityStage.ts。

3)打开MyAbilityStage.ts文件,导入AbilityStage的依赖包,自定义类继承AbilityStage。

复制代码
export default class MyAbilityStage extends AbilityStage {
  onCreate() {
    // 应用的HAP在首次加载的时,为该Module初始化操作
  }
  onAcceptWant(want) {
    // 仅specified模式下触发
    return "MyAbilityStage";
  }
}

AbilityStage常用的方法:

onCreate()生命周期回调:在开始加载对应Module的第一个UIAbility实例之前会先创建AbilityStage,并在AbilityStage创建完成之后执行其onCreate()生命周期回调。AbilityStage模块提供在Module加载的时候,通知开发者,可以在此进行该Module的初始化(如资源预加载,线程创建等)能力。

onAcceptWant()事件回调:UIAbility指定实例模式(specified)启动时候触发的事件回调。

onConfigurationUpdated()事件回调:当系统全局配置发生变更时触发的事件,系统语言、深浅色等,配置项目前均定义在Configuration类中。

onMemoryLevel()事件回调:当系统调整内存时触发的事件。

8)WindowStage

每个UIAbility类实例都会与一个WindowStage类实例绑定,WindowStage类起到了应用进程内窗口管理器的作用,它包含一个主窗口。也就是说,UIAbility通过WindowStage持有了一个窗口Window,这个Window为ArkUI提供了绘制区域。在Stage模型下,应用主窗口由UIAbility创建并维护生命周期。在UIAbility的onWindowStageCreate回调中,通过WindowStage可以获取应用主窗口,即可对其进行属性设置等操作。

实际中用到的:

在Ability中的onWindowStageCreate方法中:

1)比如通过windowStage.getMainWindow接口获取应用主窗口然后实现沉浸式状态栏的效果。

2)通过windowStage.loadContent加载入口页面

复制代码
windowStage.loadContent('pages/Splash', storage,(err, data) => {
  if (err.code) {
    hilog.error(0x0000, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err) ?? '');
    return;
  }
  hilog.info(0x0000, 'testTag', 'Succeeded in loading the content. Data: %{public}s', JSON.stringify(data) ?? '');
});

10.Web

使用的组件为webview

11.应用数据持久化

1)Preferences

  1. SQLite

3)文件存储

12.EventHub

EventHub可以实现UIAbility组件与UI之间的数据同步,它是基于发布订阅模式来实现的,事件需要先订阅后发布,订阅者收到消息后进行处理。

13.UIAbility相关

1)UIAbility组件

UIAbility组件是一种包含UI界面的应用组件,主要用于和用户交互。UIAbility组件是系统调度的基本单元,为应用提供绘制界面的窗口;一个UIAbility组件中可以通过多个页面来实现一个功能模块。每一个UIAbility组件实例,都对应于一个最近任务列表中的任务。

2)UIAbility的生命周期

UIAbility的生命周期包括onCreate、onForeground、onBackground、onDestroy四个状态。

  • onCreate:Create状态为在应用加载过程中,UIAbility实例创建完成时触发,系统会调用onCreate()回调。可以在该回调中进行页面初始化操作,例如变量定义资源加载等,用于后续的UI界面展示。
  • onForeground:Foreground状态在UIAbility实例切换至前台时触发,onForeground()回调在UIAbility的UI界面可见之前。可以在onForeground()回调中申请系统需要的资源。
  • onBackground:Background状态分在UIAbility实例切换至后台时触发。可以在onBackground()回调中释放UI界面不可见时无用的资源,或者在此回调中执行较为耗时的操作,例如状态保存等。
  • onDestroy:onDestroy状态在UIAbility实例销毁时触发。可以在onDestroy()回调中进行系统资源的释放、数据的保存等操作。

另外还有2个涉及到WindowStage的状态:

  • onWindowStageCreate:UIAbility实例创建完成之后,在进入Foreground之前,系统会创建一个WindowStage。WindowStage创建完成后会进入onWindowStageCreate()回调,通过loadContent()方法设置应用要加载的页面并根据需要订阅WindowStage的事件。

  • onWindowStageDestroy:对应于onWindowStageCreate()回调。在UIAbility实例销毁之前,则会先进入onWindowStageDestroy()回调,可以在该回调中释放UI界面资源。例如在onWindowStageDestroy()中注销WindowStage事件。

3)UIAbility的启动模式

UIAbility的启动模式是指UIAbility实例在启动时的不同呈现状态。

  • singleton启动模式:singleton启动模式为单实例模式,也是默认情况下的启动模式。每次调用startAbility()方法时,如果应用进程中该类型的UIAbility实例已经存在,则复用系统中的UIAbility实例。系统中只存在唯一一个该UIAbility实例,即在最近任务列表中只存在一个该类型的UIAbility实例。

应用的UIAbility实例已创建,该UIAbility配置为单实例模式,再次调用startAbility()方法启动该UIAbility实例,此时只会进入该UIAbility的onNewWant()回调,不会进入其onCreate()和onWindowStageCreate()生命周期回调。

如果需要使用singleton启动模式,在module.json5配置文件中的"launchType"字段配置为"singleton"或者不设置launchType。

  • multiton启动模式:

官网的解释:

multiton启动模式为多实例模式,每次调用startAbility()方法时,都会在应用进程中创建一个新的该类型UIAbility实例。即在最近任务列表中可以看到有多个该类型的UIAbility实例。这种情况下可以将UIAbility配置为multiton。

官网的这个解释是错误的,实际中启动的Ability只会有一个。

注意:这个模式虽然叫做多实例模式,但它在系统中也只存在唯一一个该UIAbility实例,但是它与singleton模式不同的是,再次调用startAbility方法时,它会替换原来的UIAbility实例,并且会进入onCreate和onWindowStateCreate生命周期。

  • specified启动模式:specified启动模式为指定实例模式,针对一些特殊场景使用(例如文档应用中每次新建文档希望都能新建一个文档实例,重复打开一个已保存的文档希望打开的都是同一个文档实例)。

在UIAbility实例创建之前,允许开发者为该实例创建一个唯一的字符串Key,创建的UIAbility实例绑定Key之后,后续每次调用startAbility()方法时,都会询问应用使用哪个Key对应的UIAbility实例来响应startAbility()请求。运行时由UIAbility内部业务决定是否创建多实例,如果匹配有该UIAbility实例的Key,则直接拉起与之绑定的UIAbility实例(此时再次启动该UIAbility时,只会进入该UIAbility的onNewWant()回调,不会进入其onCreate()和onWindowStageCreate()生命周期回调。),否则创建一个新的UIAbility实例。

复制代码
let want: Want = {
 ...
 //这个参数需要和MyAbilityStage中的instanceKey参数一致
 parameters: {
   instanceKey: '文档2'
 }
}
this.context.startAbility(want)

新建一个MyAbilityStage继承自AbilityStage,然后重写onAcceptWant。

复制代码
export default class MyAbilityStage extends AbilityStage {
  onAcceptWant(want: Want): string {
    if(want.abilityName === 'MultitonabilityEntryAbility'){
      //instanceKey需要和跳转Ability中的parameters的参数一致
      return want.parameters.instanceKey.toString()
    }
    return ''
  }
}
  • standard:standard启动模式为多实例模式,每次调用startAbility()方法时,都会在应用进程中创建一个新的该类型UIAbility实例。即在最近任务列表中可以看到有多个该类型的UIAbility实例。

这也就意味着,如果任务列表中存在该实例时,再次调用startAbility方法会进入onCreate和onWindowStateCreate生命周期。

注意:每次启动Ability都会启动一个新的,会有多个Ability。

4)UIAbility组件的基本用法

UIAbility组件的基本用法包括:指定UIAbility的启动页面以及获取UIAbility的上下文UIAbilityContext。

应用中的UIAbility在启动过程中,需要指定启动页面,否则应用启动后会因为没有默认加载页面而导致白屏。可以在UIAbility的onWindowStageCreate()生命周期回调中,通过WindowStage对象的 loadContent()方法设置启动页面。

5)获取UIAbility的上下文信息

UIAbility类拥有自身的上下文信息,该信息为 UIAbilityContext 类的实例,UIAbilityContext类拥有abilityInfo、currentHapModuleInfo等属性。通过UIAbilityContext可以获取UIAbility的相关配置信息,如包代码路径、Bundle名称、Ability名称和应用程序需要的环境状态等属性信息,以及可以获取操作UIAbility实例的方法(如startAbility()、connectServiceExtensionAbility()、terminateSelf()等)。

如何获取UIAbility的上下文信息?

在UIAbility中可以通过this.context获取UIAbility实例的上下文信息。

复制代码
export default class EntryAbility extends UIAbility {
  onCreate(want, launchParam) {
    // 获取UIAbility实例的上下文
    let context = this.context;
  }
}

在页面中获取UIAbility实例的上下文信息,包括导入依赖资源context模块和在组件中定义一个context变量两个部分。

复制代码
import common from '@ohos.app.ability.common';
private context = getContext(this) as common.UIAbilityContext
//启动Ability
this.context.startAbility(want)

6)UIAbility与UI的数据同步的方式有哪些

EventHub:EventHub可以用于UIAbility和UI以及UI和UI之间

EventHub提供了UIAbility组件/ExtensionAbility组件级别的事件机制,以UIAbility组件/ExtensionAbility组件为中心提供了订阅、取消订阅和触发事件和发布事件的数据通信能力。

globalThis:globalThis是ArkTS引擎实例内部的一个全局对象,引擎内部的UIAbility/ExtensionAbility/Page都可以使用,因此可以使用globalThis全局对象进行数据同步。

复制代码
//UIAbility中通过globalThis来存储数据
async onCreate(want, launchParam) {
  //这里接收跳转MultitonabilityEntryAbility传递过来的参数
  globalThis.entryAbilityWant = want;
}


aboutToAppear(){
  //这里接收的是在MultitonabilityEntryAbility中接收的want
  let want:Want =  globalThis.entryAbilityWant
  console.info("entryAbilityWant",want.parameters.instanceKey)
}
  • UIAbility使用globalThis传递参数到page

调用startAbility()方法启动一个UIAbility实例时,被启动的UIAbility创建完成后会进入onCreate()生命周期回调,且在onCreate()生命周期回调中能够接受到传递过来的want参数,可以将want参数绑定到globalThis上。

复制代码
export default class MultitonabilityEntryAbility extends UIAbility {
  async onCreate(want, launchParam) {
    //这里接收跳转MultitonabilityEntryAbility传递过来的参数
    globalThis.entryAbilityWant = want;
  }
}

在UI界面中即可通过globalThis获取到want参数信息。

复制代码
struct MultitonAbilityPage { 
  aboutToAppear(){
    //这里接收的是在MultitonabilityEntryAbility中接收的want
    let want:Want =  globalThis.entryAbilityWant
    console.info("entryAbilityWant",want.parameters.instanceKey)
  }
}
  • UiAbility和UIAbility之间使用globalThis

同一个应用中UIAbility和UIAbility之间的数据传递,可以通过将数据绑定到全局变量globalThis上进行同步,如在AbilityA中将数据保存在globalThis,然后跳转到AbilityB中取得该数据:

复制代码
export default class AbilityA extends UIAbility {
  onCreate(want, launch) {
    globalThis.entryAbilityStr = 'AbilityA'; // AbilityA存放字符串“AbilityA”到globalThis
    // ...
  }
}


export default class AbilityB extends UIAbility {
  onCreate(want, launch) {
    // AbilityB从globalThis读取name并输出
    console.info('name from entryAbilityStr: ' + globalThis.entryAbilityStr);
    // ...
  }
}

注意:Stage模型下进程内的UIAbility组件共享ArkTS引擎实例,使用globalThis时需要避免存放相同名称的对象。例如AbilityA和AbilityB可以使用globalThis共享数据,在存放相同名称的对象时,先存放的对象会被后存放的对象覆盖。FA模型因为每个UIAbility组件之间引擎隔离,不会存在该问题。

对于绑定在globalThis上的对象,其生命周期与ArkTS虚拟机实例相同,建议在使用完成之后将其赋值为null,以减少对应用内存的占用。

AppStorage/LocalStorage:

ArkUI提供了AppStorage和LocalStorage两种应用级别的状态管理方案,可用于实现应用级别和UIAbility级别的数据同步。使用这些方案可以方便地管理应用状态,提高应用性能和用户体验。其中,AppStorage是一个全局的状态管理器,适用于多个UIAbility共享同一状态数据的情况;而LocalStorage则是一个局部的状态管理器,适用于单个UIAbility内部使用的状态数据。通过这两种方案,开发者可以更加灵活地控制应用状态,提高应用的可维护性和可扩展性。

7)Ability组件之间的交互(设备内)

同一module中的Ability跳转:通过调用startAbility()方法启动UIAbility

复制代码
let want: Want = {
  deviceId: '',//空字符串代表当前设备
  bundleName: 'com.example.myapplication',
  moduleName: 'entry',
  abilityName: 'MultitonabilityEntryAbility’,
  parameters: {//参数
    instanceKey: '文档1'
  }
}
this.context.startAbility(want)
  .catch(err => {
  })

在Ability的生命周期回调函数中接收传递过来的参数:

复制代码
export default class FuncAbility extends UIAbility {
  onCreate(want, launchParam) {
    // 接收调用方UIAbility传过来的参数
    let info = want?. parameters?.instanceKey;
  }
}

在Ability业务完成之后,如需要停止当前UIAbility实例,在Ability中通过调用terminateSelf()方法实现:

复制代码
context为需要停止的UIAbility实例的AbilityContext
//无参数
this.context.terminateSelf((err) => {
  // ...
});

调用terminateSelfWithResult()返回参数:

复制代码
//跳转回上一个Ability
let abilityResult = {
  resultCode: 1001,
  want: {
    bundleName: 'com.example.myapplication',
    abilityName: 'EntryAbility',
    moduleName: 'entry',
    parameters: {
      backParams: '来自MultitonabilityEntryAbility',
    },
  },
}
//使用terminateSelfWithResult关闭Ability并返回参数
this.context.terminateSelfWithResult(abilityResult, (err) => {
});


//使用startAbilityForResult跳转并接收返回的参数
this.context.startAbilityForResult(want).then(data=>{
  if(data.resultCode == 1001){
    //取出Ability返回的信息
    let result = data.want.parameters.backParams;
    console.info(`Ability返回的result为:${result}`)
  }
})

启动其它应用的UIAbility

启动UIAbility有显式Want启动和隐式Want启动两种方式。

  • 显式Want启动:启动一个确定的UIAbility,在want参数中需要设置该应用bundleName和abilityName,当需要拉起某个明确的UIAbility时,通常使用显式Want启动方式。
  • 隐式Want启动:根据匹配条件由用户选择启动哪一个UIAbility,即不明确指出要启动哪一个UIAbility(abilityName参数未设置),在调用startAbility()方法时,其入参want中指定了一系列的entities字段(表示目标UIAbility额外的类别信息,如浏览器、视频播放器)和actions字段(表示要执行的通用操作,如查看、分享、应用详情等)等参数信息,然后由系统去分析want,并帮助找到合适的UIAbility来启动。当需要拉起其他应用的UIAbility时,开发者通常不知道用户设备中应用的安装情况,也无法确定目标应用的bundleName和abilityName,通常使用隐式Want启动方式。

启动其他应用的UIAbility,推荐使用隐式Want启动。系统会根据调用方的want参数来识别和启动匹配到的应用UIAbility。

不同应用之间的Ability传递参数和接受返回参数和在同应用间使用是一样的。

启动其它Ability的具体的某个页面(不同应用和同一应用都可以)

复制代码
let wantInfo = {
    deviceId: '', // deviceId为空表示本设备
    bundleName: 'com.example.myapplication2',
    abilityName: 'EntryAbility',
    moduleName: 'entry', // moduleName非必选
    parameters: { // 自定义参数传递页面信息,通过这个参数判断跳转哪个页面
      router: 'SecondPage',
    },
  }


  this.context.startAbility(wantInfo).then(() => {
    //then为回调,从其它Ability返回到当前Ability后的操作可以在这里处理
  }).catch((err) => {
    console.info(err)
  })

然后在其它Ability的onWindowStageCreate方法中通过windowStage.loadContent来加载具体的某个页面。

复制代码
onWindowStageCreate(windowStage: window.WindowStage) {
  let url = 'pages/Index';
  if (this.funcAbilityWant?.parameters?.router) {
    //打开指定页面
    if (this.funcAbilityWant.parameters.router === 'SecondPage') {
      url = 'pages/SecondPage';
    }
  }
  //加载页面
  windowStage.loadContent(url, (err, data) => {
    // ...
  });
}

14.信息传递载体Want

Want是对象间信息传递的载体,可以用于应用组件间的信息传递,除了用于通过startAbility在Ability之间传递参数,,还可以实现打开链接和分享中的参数配置。

18.应用上下文Context

  • BaseContext:最基础的Context
  • Context:继承自BaseContext
  • ApplicationContext:继承自Context,应用级别的Context,ApplicationContext在基类Context的基础上提供了订阅应用内Ability的生命周期的变化、订阅系统内存变化和订阅应用内系统环境的变化的能力,在UIAbility、ExtensionAbility、AbilityStage中均可以获取。
复制代码
import UIAbility from '@ohos.app.ability.UIAbility';
export default class EntryAbility extends UIAbility {
    onCreate(want, launchParam) {
        let applicationContext = this.context.getApplicationContext();
        // ...
    }
}
  • AbilityStageContext:Module级别的Context,继承自Context,和基类Context相比,额外提供HapModuleInfo、Configuration等信息。
复制代码
export default class MyAbilityStage extends AbilityStage {
    onCreate() {
        let abilityStageContext = this.context;
    }
}
  • UIAbilityContext:继承自UIAbilityContext,每个UIAbility中都包含了一个Context属性,提供操作Ability、获取Ability的配置信息、应用向用户申请授权等能力。
复制代码
import UIAbility from '@ohos.app.ability.UIAbility';
export default class EntryAbility extends UIAbility {
    onCreate(want, launchParam) {
        let uiAbilityContext = this.context;
    }
}
  • ExtensionContext:继承自Context
  • FormExtensionContext和ServiceExtensionContext继承自ExtensionContext

各类Context的持有关系:

UIAbility持有的是UIAbilityContext

AbilityStage持有的是AbilityStageContext

Application持有的是ApplicationContext

ServiceExtensionAbility持有的是ServiceExtensionContext

在页面中获取UIAbilityContext:

复制代码
private context = getContext(this) as common.UIAbilityContext
onPageShow(){
  //使用UIAbilityContext
  window.getLastWindow(this.context).then((windowClass) => {
  })
}
aboutToAppear(){
  //使用UIAbilityContext操作EventHub
  this.context.eventHub.on('EventHubonClick',()=>{
    prompt.showToast({message:'EventHubonClick'})
  })
}
//使用UIAbilityContext启动Ability
this.context.startAbility(want)
  .catch(err => {
  })

15.项目结构

16.3种形式的点击事件

第一种,箭头函数

复制代码
Button('click').onClick(()=>{
  console.log('Hongmeng Hello!')
})

第二种,使用其他的函数(匿名函数)

复制代码
Button('click2').onClick(function ():void{
  console.log('Hongmeng Hello!')
}.bind(this))

第三种,使用bind方式

复制代码
//注意:myClickHandler方法不能带()
Button('click2').onClick(this.myClickHandler.bind(this))


myClickHandler():void{
  console.log('HarmonyOS Hello!')
}

17.HAR,HSP和HAP

1)HAP

一个应用包含一个或者多个Module,可以在DevEco Studio工程中创建一个或者多个Module。Module是HarmonyOS应用/服务的基本功能单元,包含了源代码、资源文件、第三方库及应用/服务配置文件,每一个Module都可以独立进行编译和运行。Module分为"Ability"和"Library"两种类型, (Harmony Ability Package);"Library"类型的Module对应于HAR(Harmony Archive),或者HSP(Harmony Shared Package)。一个Module可以包含一个或多个UIAbility组件。

在项目的工程目录中有一个entry文件夹,它是系统默认生成的一个Module,通过DevEco Studio把应用程序编译为一个或者多个.hap后缀的文件,即HAP。HAP是HarmonyOS应用安装的基本单位,包含了编译后的代码、资源、三方库及配置文件。HAP可分为Entry和Feature两种类型。

Entry类型的HAP:是应用的主模块,在module.json5配置文件中的type标签配置为"entry"类型。在同一个应用中,同一设备类型只支持一个Entry类型的HAP,通常用于实现应用的入口界面、入口图标、主特性功能等。

Feature类型的HAP:是应用的动态特性模块,在module.json5配置文件中的type标签配置为"feature"类型。一个应用程序包可以包含一个或多个Feature类型的HAP,也可以不包含;Feature类型的HAP通常用于实现应用的特性功能,可以配置成按需下载安装,也可以配置成随Entry类型的HAP一起下载安装。

复制代码
{
  "module": {
    "type": "feature",
  }
}

每个HarmonyOS应用可以包含多个.hap文件,一个应用中的.hap文件合在一起称为一个Bundle,而bundleName就是应用的唯一标识。需要特别说明的是:在应用上架到应用市场时,需要把应用包含的所有.hap文件(即Bundle)打包为一个.app后缀的文件用于上架,这个.app文件称为App Pack(Application Package),其中同时包含了描述App Pack属性的pack.info文件;在云端(服务器)分发和终端设备安装时,都是以HAP为单位进行分发和安装的。

从开发态到编译态,Module中的文件会发生如下变更:

  1. **ets目录:**ArkTS源码编译生成.abc文件。
  2. **resources目录:**AppScope目录下的资源文件会合入到Module下面资源目录中,如果两个目录下的存在重名文件,编译打包后只会保留AppScope目录下的资源文件。
  3. **module配置文件:**AppScope目录下的app.json5文件字段会合入到Module下面的module.json5文件之中,编译后生成HAP或HSP最终的module.json文件。
  4. 一个开发态的Module编译后生成一个部署态的HAP,Module和HAP一一对应。所有的HAP最终会编译到一个App Pack中(以.app为后缀的包文件),用于发布到应用市场。

HarmonyOS提供了两种共享包,HAR(Harmony Archive)静态共享包,和HSP(Harmony Shared Package)动态共享包。

HAR与HSP都是为了实现代码和资源的共享,都可以包含代码、C++库、资源和配置文件,最大的不同之处在于:HAR中的代码和资源跟随使用方编译,如果有多个使用方,它们的编译产物中会存在多份相同拷贝;而HSP中的代码和资源可以独立编译,运行时在一个进程中代码也只会存在一份。

2)HSP

HSP(Harmony Shared Package)是动态共享包,只能被该应用内部其他HAP/HSP使用,用于应用内部代码、资源的共享,可以包含代码、C++库、资源和配置文件,通过HSP可以实现应用内的代码和资源的共享。HSP不支持独立发布,而是跟随其宿主应用的APP包一起发布,与宿主应用同进程,具有相同的包名和生命周期。

HSP旨在解决HAR存在的几个问题

多个HAP引用相同的HAR,导致的APP包大小膨胀问题。

多个HAP引用相同的HAR,HAR中的一些状态变量无法共享的问题。

使用场景

  • 多个HAP/HSP共用的代码和资源放在同一个HSP中,可以提高代码、资源的可重用性和可维护性,同时编译打包时也只保留一份HSP代码和资源,能够有效控制应用包大小。
  • HSP在运行时按需加载,有助于提升应用性能。

HSP的一些约束

  • HSP不支持在设备上单独安装/运行,需要与依赖该HSP的HAP一起安装/运行。HSP的版本号必须与HAP版本号一致。
  • HSP不支持在配置文件中声明UIAbility组件与ExtensionAbility组件。
  • HSP可以依赖其他HAR或HSP,但不支持循环依赖,也不支持依赖传递。

HSP按照使用场景:可以分为应用内HSP和应用间HSP,应用间HSP暂不支持。

创建HSP模块

选中工程目录中任意文件,然后在菜单栏选择File > New > Module,开始创建新的Module。模板类型选择Shared Library,点击Next。

编译HSP模块

选中模块名,然后通过DevEco Studio菜单栏的Build > Make Module ${libraryName}进行编译构建,生成HSP。

引用HSP动态共享包

1)在使用方模块中引入HSP

在使用方entry/feature模块的oh-package.json5文件中添加HSP模块引用,以引用名为sharedlibrary的HSP为例:

复制代码
{
  ...
  "dependencies": {
    "sharedlibrary": "file:../sharedlibrary"
  }
}

添加引用后,dependencies字段内的片段将出现报错,将鼠标放置在报错处会出现提示,在提示框中点击Run 'ohpm install'。

HSP目录将映射到entry/feature的oh_modules目录下。

HSP和HAP页面互相跳转相关

如果想在entry模块中,添加一个按钮跳转至sharelibrary模块中的ShareLibraryPage页面(路径为:sharelibrary/src/main/ets/pages/ShareLibraryPage),那么可以在使用方的代码里这样使用:

复制代码
router.pushUrl({
  url: '@bundle:com.example.myapplication/sharelibrary/ets/pages/ShareLibraryPage'
})

url内容的模板为:

复制代码
'@bundle:包名(bundleName)/模块名(moduleName)/路径/页面所在的文件名(不加.ets后缀)'

HSP返回到HAP的页面

复制代码
router.back({
  url:'pages/Index'//路径为:`entry/src/main/ets/pages/Index.ets`
})

HSP页面跳转到HSP页面

复制代码
router.pushUrl({
  url:'@bundle:com.example.myapplication/sharelibrary/ets/pages/ShareLibrarySecondPage'
})

HSP返回到HSP的页面

复制代码
router.back({
  url:'@bundle:com.example.myapplication/sharelibrary/ets/pages/ShareLibraryPage'
})

也可以这样:

复制代码
router.back()

3)HAR

HAR(Harmony Archive)是静态共享包,可以包含代码、C++库、资源和配置文件。通过HAR可以实现多个模块或多个工程共享ArkUI组件、资源等相关代码。HAR不同于HAP,不能独立安装运行在设备上,只能作为应用模块的依赖项被引用。

如何创建HAR模块

鼠标移到工程目录顶部,单击右键,选择New > Module,在工程中添加模块。

在Choose Your Ability Template界面中,选择Static Library,并单击Next。

HAR如何导出入口文件

Index.ets文件是HAR导出声明文件的入口,HAR需要导出的接口,统一在Index.ets文件中导出。

在oh-package.json5中"main"字段定义导出文件入口。若不设置"main"字段,默认以当前目录下index.ets为入口文件,依据.ets>.ts>.js的顺序依次检索。以将ets/components/mainpage/MainPage.ets文件设置为入口文件为例:

复制代码
{
  "main": "./src/main/ets/components/mainpage/MainPage.ets",
}

HAR模块开启混淆

需要把HAR模块的build-profile.json5文件中的artifactType字段设置为obfuscation

复制代码
{
  "apiType": "stageMode",
  "buildOption": {
      "artifactType": "obfuscation"
  }
}

构建HAR共享包

选中HAR模块的根目录,点击Build > Make Module '<module-name>'启动构建。

构建完成后,build目录下生成闭源HAR包产物。

HAR开发注意事项

  • HAR不支持在配置文件中声明abilities、extensionAbilities组件。
  • HAR不支持在配置文件中声明pages页面。
  • HAR不支持在build-profile.json5文件的buildOption中配置worker(HAP和HSP支持)。
  • Stage模型的HAR,不能引用AppScope内的内容。在编译构建时AppScope中的内容不会打包到HAR中,导致HAR资源引用失败。

HAR模块编译打包时会把资源打包到HAR中。在编译构建HAP时,DevEco Studio会从HAP模块及依赖的模块中收集资源文件。

18.引用依赖(HAR/HSP)的3种方式

包含引用HSP和HAR。

引用ohpm仓中的HAR

首先需要设置三方HAR的仓库信息。DevEco Studio默认仓库地址为OpenHarmony三方库中心仓,如果您需要设置自定义仓库,请在DevEco Studio的Terminal窗口执行如下命令:

复制代码
ohpm config set registry your_registry1,your_registry2

说明:ohpm支持多个仓库地址,采用英文逗号分隔。

然后通过如下两种方式设置三方包依赖信息:

  • 方式一:在Terminal窗口中,执行如下命令安装三方包,DevEco Studio会自动在工程的oh-package.json5中自动添加三方包依赖。
复制代码
ohpm install @ohos/lottie
  • 方式二:在工程的oh-package.json5中设置三方包依赖,配置示例如下:
复制代码
"dependencies": {
  "@ohos/lottie": "^2.0.0"
}

依赖设置完成后,需要执行ohpm install命令安装依赖包,依赖包会存储在工程的oh_modules目录下。

引用本地文件夹

引用本地文件夹,有两种方式:

  • 方式一:在Terminal窗口中,执行如下命令进行安装,并会在oh-package.json5中自动添加依赖。

方式二:在工程的oh-package.json5中设置三方包依赖,配置示例如下:

复制代码
"dependencies": {
  "folder": "file:../folder"
}

依赖设置完成后,需要执行ohpm install命令安装依赖包,依赖包会存储在工程的oh_modules目录下。

引用本地HAR/HSP包

引用本地HAR包,有如下两种方式:

  • 方式一:在Terminal窗口中,执行如下命令进行安装,并会在oh-package.json5中自动添加依赖。
复制代码
ohpm install ./package.har
  • 方式二:在工程的oh-package.json5中设置三方包依赖,配置示例如下:
复制代码
"dependencies": {
  "package": "file:./package.har"
}

依赖设置完成后,需要执行ohpm install命令安装依赖包,依赖包会存储在工程的oh_modules目录下。

在引用共享包时,请注意以下事项:

当前只支持在模块和工程下的oh-package.json5文件中声明dependencies依赖,才会被当做依赖使用,并在编译构建过程中进行相应的处理。

19.状态管理

状态管理机制:运行时状态变化所带来的UI的重新渲染,在ArkUI中被统称为状态管理机制。

1)实现应用与组件状态的同步:可以通过@StorageLink和LocalStorageLink实现应用和组件状态的双向同步,可以通过@StorageProp和LocalStorageProp实现应用和组件状态的单向同步。

处理组件状态的同步,有以下几种方式:

1)@State:当被标记为@State的变量改变时会重新渲染View,它可以作为子组件单向或者双向同步的数据源,@State标记的变量必须初始化,不能为空值,@State支持Object,class,string,number,boolean,enum类型以及这些类型的数组,如果是嵌套类型或者数组中的对象的属性则无法触发刷新。

2)@Prop:@Prop修饰的变量可以和父组件建立单向同步关系,@Prop修饰的变量不会同步回父组件。@Prop修饰的变量不能被初始化。除了@State,数据源也可以使用@Link或者@Prop修饰。

允许修饰的变量类型:string、number、boolean、enum类型。

限制:@Prop不能在@Entry修饰的自定义组件中使用。

3)@Link:@Link修饰的组件可以和父组件建立双向同步管理,@Link修饰的变量修改时会同步回父组件。@Link修饰的变量不能初始化。传递给@Link参数时,需要使用$。

允许修饰的变量类型:Object、class、string、number、boolean、enum类型,以及这些类型的数组。

限制:@Link不能在@Entry修饰的自定义组件中使用。

4)@Provider和@Consume:@Provider和@Consume可以用于多个组件之间变量的同步,它可以不通过命名参数而是通过别名来绑定参数。不用传递参数,直接使用@Provider和@Consume就可以。

@Provider必须初始化值,@Consume禁止初始化值。

5)@Observed:多层嵌套场景的class需要被@Observed修饰,需要和@ObjectLink搭配使用,@ObjectLink和数据源建立的是双向同步的关系。

上面这几个状态管理,除了@State和@Provider,其他的都禁止初始化值。

20.异步操作

promise

async/await

21.mvp和mvvm

MVP

原来的UI逻辑都抽象成一个View接口,业务逻辑抽象成precenter接口,model还是原来的model。model把数据返回给precenter,precenter再把数据返回给view,每个view都对应着一个precener,model可能是共用的。

presenter中的view层的引用使用弱引用,当系统内存不足时,回收引用,减少内存泄漏。

MVVM

view viewModel model

在view和model之间有个viewModel,这个viewModel是自动生成的。通过databinding把view和model绑定起来,双向绑定。

唯一的缺点就是内存消耗比较大

相关推荐
网络安全-杰克3 分钟前
《网络对抗》—— Web基础
前端·网络
m0_748250744 分钟前
2020数字中国创新大赛-虎符网络安全赛道丨Web Writeup
前端·安全·web安全
周伯通*5 分钟前
策略模式以及优化
java·前端·策略模式
艾斯特_19 分钟前
前端代码装饰器的介绍及应用
前端·javascript
Sokachlh23 分钟前
【elementplus】中文模式
前端·javascript
轻口味23 分钟前
【每日学点鸿蒙知识】hap安装报错、APP转移账号、import本地文件、远程包构建问题、访问前端页面方法
前端·华为·harmonyos
m0_7482453430 分钟前
BY组态-低代码web可视化组件
前端·低代码
Cshaosun1 小时前
js版本之ES6特性简述【Proxy、Reflect、Iterator、Generator】(五)
开发语言·javascript·es6
web182854825121 小时前
ctfshow-web 151-170-文件上传
前端·状态模式
轻口味1 小时前
【每日学点鸿蒙知识】Web请求支持Http、PDF展示、APP上架应用搜索问题、APP备案不通过问题、滚动列表问题
前端·http·harmonyos