使用ArkWeb构建页面

ArkWeb简介

概念介绍

Web组件用于在应用程序中显示Web页面内容,为开发者提供页面加载、页面交互、页面调试等能力。可以用于实现移动端的混合式开发(HyBrid App):

  • 页面加载:Web组件提供基础的前端页面加载的能力,包括加载网络页面、本地页面、HTML格式文本数据
  • 页面交互:Web组件提供丰富的页面交互的方式,包括:设置前端页面深色模式,系窗口中加载页面,位置权限管理,Cookie管理,应用侧使用前端页面JavaScript等能力。
  • 页面调试:Web组件支持使用Devtools工具调试前端页面。

ArkWeb API参考

官网网址:ArkTS API-ArkWeb(方舟Web)
ArkWeb涉及到的API主要有以下两个:

  1. Web组件:提供具有网页显示能力的一种组件。
  2. Webview:提供web控制能力的相关接口。例如控制Web组件加载的内容,控制Web内容后退前进,以及异步执行JavaScript脚本等能力。

页面加载与显示

加载网络页面

开发者可以在Web组件创建时,指定默认加载的网络页面。在默认页面加载完成后,如果开发者需要变更此Web组件显示的网络页面,可以通过调用loadUrl()接口加载指定的网页。

....
struct WebComponent {
  webviewController: webview.WebviewController = new webview.WebviewController();

  build() {
    Column() {
      Button('loadUrl')
        .onClick(() => {
          try{
            this.webviewController.loadUrl('www.example1.com')
          } catch(error) {
            ...
          }
        })
      // 组件创建时,加载www.example.com
      Web({ src: 'www.example.com', controller: this.webviewController })
    }
  }
}

加载本地的页面

将本地页面文件放在应用的rawfile目录下,开发者可以在Web组件创建的时候指定默认加载的本地页面,并且加载完成后可通过调用loadUrl()接口变更当前Web组件的页面。
image.png

....
struct WebComponent {
  webviewController: webview.WebviewController = new webview.WebviewController();

  build() {
    Column() {
      Button('loadUrl')
        .onClick(() => {
          try{
            this.webviewController.loadUrl(${rawfile("local1.html1")})
          } catch(error) {
            ...
          }
        })
      // 组件创建时,加载本地页面
      Web({ src: $rawfile("local.html"), controller: this.webviewController })
    }
  }
}

加载HTML格式数据

Web组件可以通过loadDataO接口实现加载HTML格式的文本数据。当开发者不需要加载整个页面,只需要显示一些页面片段时,可通过此功能来快速加载页面。

....
struct WebComponent {
  webviewController: webview.WebviewController = new webview.WebviewController();

  build() {
    Column() {
      Button('loadUrl')
        .onClick(() => {
            this.webviewController.loadData(
              "<html><body bgcolor=\"white\">Source:<pre>source</pre></body></html>",
              "text/html",
              "UTF-8"
            ),
        })
      // 组件创建时,加载www.example.com
      Web({ src: 'www.example.com', controller: this.webviewController })
    }
  }
}

动态创建Web组件

支持命令式创建Web组件,这种方式创建的组件不会立即挂载到组件树,即不会对用户呈现(组件状态为Hidden和InActive),开发者可以在后续使用中按需动态挂载。后台启动的Web实例不建议超过200个。

// 载体Ability
// EntryAbility.ets
import { createNWeb} from "../pages/common"

onWindowStageCreate(windowStaeg: window.WindowStage): void {
  windowStage.loadContent('pages/Index', (err,data) => {
    // 创建Web动态组件(需传入UIContext),loadContent之后的任意时机均可创建
    createNWeb("https://www.example.com", windowStage.getMainWindowSync().getUIContext());
    if(err.code){
      return;
    }
  })
}
// 创建NodeController
// common.ets
import {webview} from '@kit.ArkWeb';
import { NodeController, BuilderNode, Size, FrameNode, UIContext} from '@kit.ArkUI'

// @Builder中为动态组件的具体组件内容
// Data为入参封装类
class Data {
  url: string = "https://www.example.com";
  controller: WebviewController = new webview.WebviewController();
}

@Builder
function WebBuilder(data: Data){
  Column() {
    Web({src: data.url, controller: data.controller})
      .width("100%")
      .height("100%")
  }
}

let wrap = wrapBuilder<Data[]>(WebBuilder);

// 用于控制和反馈对应的NodeContainer上的节点的行为,需要与NodeContainer一起使用
export class myNodeController extends NodeController {
  private rootnode: BuilderNode<Data[]> | null = null;
  makeNode(uiContext: UIContext): FrameNode | null {
    console.log("uicontext is undefined:" + (uiContext === undefined));

    if(this.rootnode != null ){
      /// 返回FrameNode节点
      return this.rootnode.getFrameNode();
    }

    // 返回null控制动态组件脱离绑定节点
    return null;
  }

  // 当布局大小发生变化时进行回调
  aboutToDisappear() {
    console.log("aboutToDisappear")
  }

  // 当controller对应的NodeContainer在Disappear的时候进行回调
  aboutToDisapper() {
    console.log("aboutToDisappear")
  }

  //此函数为自定义函数,可作为初始化函数使用
  // 通过UIContext初始化BuilderNode,再通过BuilderNode中的builder接口初始化@Builder中的内容
  initWeb(url: string, uiContext: UIContext: UIContext, controller: WebviewController){
    if(this.rootnode != null){
      return;
    }
    // 创建节点,需要uiContext
    this.rootnode = new BuilderNode(uiContext)
    // 创建动态Web组件
    this.rootnode.build(wrap, {url: url, controller: controller})
  }
}

export const createNWeb = (url: string, uiContext: UIContext) => {
  //创建NodeController
  let baseNode = new myNodeController();
  let controller = new webview.WebviewController();
  // 初始化自定义Web组件
  baseNode.initWeb(url, uiContext, controller);
  controllerMap.set(url, controller)
  NodeMap.set(url, baseNode);
}

export const getNWeb = (url: string): myNodeController | undefined => {
  return NodeMap.get(url);
}
// 使用NodeController的Page页
// Index.ets
import {getNWeb} from "./common"
@Entry
@Component
struct Index {
  build() {
    Row() {
      Column() {
        NodeContainer(getNWeb("https://www.example.com"))
          .height("90%")
          .width("100%")
      }
      .width('100%')
    }
    .height('100%')
  }
}

通过结构化数据构建页面

在快速入门案例中, 知识地图页左侧的导航栏以及右侧的知识地图详情页,都是通过结构化数据渲染而来。
image.png

数据结构设计

知识地图列表项数据结构设计

NavBar构建
image.png

@Component
export struct KnowledgeMap {
  // ... 
  @Builder
  NavBar(order: number, title: string) {
    Row() {
      Text(order < 10 ? '0' + order : '' + order)
        .margin({ right: $r('app.float.text_title_span') })
        .fontFamily($r("app.string.font_bold"))
        .fontSize($r('app.float.order_font_size'))
        .fontColor($r('app.color.order_font_color'))
        .textAlign(TextAlign.Start)
        .lineHeight($r('app.float.order_line_height'))
        .fontWeight(Constants.ORDER_FONT_WEIGHT)
      Text(title)
        .fontFamily($r('app.string.font_medium'))
        .fontSize($r('app.float.fs16'))
        .fontColor($r('app.color.order_font_color'))
        .textAlign(TextAlign.Start)
        .lineHeight(Constants.LINE_HEIGHT_22)
        .fontWeight(Constants.FONT_WEIGHT_500)
      Blank()
      Image($r('app.media.ic_arrow'))
        .width($r('app.float.right_arrow_width'))
        .height($r('app.float.right_arrow_height'))
    }
    .backgroundColor(this.currentNavBar === order - 1 ? $r('app.color.nav_row_back_color') : Color.Transparent)
    .borderRadius($r('app.float.br16'))
    .alignItems(VerticalAlign.Center)
    .width(Constants.FULL_WIDTH)
    .height($r('app.float.nav_bar_height'))
    .padding({ left: $r('app.float.nav_bar_span'), right: $r('app.float.nav_bar_span') })
  }
  build() {
  }
}

List循环渲染NavBarItem:
image.png

...

@Component
export struct KnowledgeMap {
  // ...
  build() {
    Navigation() {
      Scroll() {
        Column() {
          // ...
          List({ space: 12 }) {
            ForEach(this.navBarList, (item: NavBarItemType, index: number) => {
              ListItem() {
                NavBarItem(item.order, item.title)
              }.width('100%')
            }, (item: NavBarItemType): string => item.title)
          }

          // ...
        }
      }

      // ...
    }

    // ...
  }
}

知识地图详情页

JSON数据
{
  "title": "准备与学习",
  "brief": "加入HarmonyOS生态,注册成为开发者,通过HarmonyOS课程了解基本概念和基础知识,轻松开启HarmonyOS的开发旅程。",
  "materials": [
    {
      "subtitle": "HarmonyOS简介",
      "knowledgeBase": [
        {
          "type": "准备",
          "title": "注册账号"
        },
        {
          "type": "准备",
          "title": "实名认证"
        },
        {
          "type": "学习与获取证书",
          "title": "HarmonyOS第一课"
        },
        {
          "type": "学习与获取证书",
          "title": "HarmonyOS应用开发者基础认证"
        }
      ]
    },
    {
      "subtitle": "赋能套件介绍",
      "knowledgeBase": [
        {
          "type": "指南",
          "title": "开发"
        },
        {
          "type": "指南",
          "title": "最佳实践"
        },
        {
          "type": "指南",
          "title": "API参考"
        }
      ]
    }
  ]
}
知识地图详情页:页面结构抽象
分析:

image.png
image.png
image.png
image.png

数据接口定义
interface Section {
  title: string,
  brief: string,
  materials: Material[]
}

interface Material {
  subtitle: string,
  knowledgeBase: KnowledgeBaseItem[]
}

interface KnowledgeBaseItem {
  icon?: Resource | string,
  title: string,
  type: string
}
代码实现
@Builder
KnowledgeBlockLine(knowledgeBaseItem: KnowledgeBaseItem) {
  Row() {
    Image($r(TYPE_MAP_ICON[knowledgeBaseItem.type]))
      .width($r('app.float.type_icon_size'))
      .height($r('app.float.type_icon_size'))

    Column() {
      Text(knowledgeBaseItem.title)
        .fontFamily($r('app.string.font_medium'))
        .fontSize($r('app.float.fs16'))
        .fontWeight(CommonConstants.FONT_WEIGHT_500)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
        .maxLines(1)
      Text(knowledgeBaseItem.type)
        .fontFamily($r('app.string.font_normal'))
        .fontSize($r('app.float.fs14'))
        .fontWeight(CommonConstants.FONT_WEIGHT_400)
    }
    .layoutWeight(1)
    .alignItems(HorizontalAlign.Start)
    .margin({ left: $r('app.float.title_type_margin_left') })

    Image($r('app.media.ic_arrow'))
      .width($r('app.float.right_arrow_width'))
      .height($r('app.float.right_arrow_height'))
  }
  .width(CommonConstants.FULL_SCREEN)
  .height($r('app.float.content_line_height'))
  .alignItems(VerticalAlign.Center)
  .onClick(() => {
    const now = Date.now();
    if (now - this.lastCall < 3000) {
      return;
    }
    this.lastCall = now;
    promptAction.showToast({
      message: '当前知识地图仅供参考,具体内容请查阅官网',
      duration: 2000
    })
  })
}
@Builder
KnowledgeBlock(material: Material) {
  Column() {
    Text(material.subtitle)
      .fontFamily($r('app.string.font_medium'))
      .fontSize($r('app.float.fs14'))
      .fontWeight(CommonConstants.FONT_WEIGHT_500)
      .margin({ bottom: $r('app.float.back_icon_margin_right') })

    List() {
      ForEach(material.knowledgeBase, (item: KnowledgeBaseItem, index: number) => {
        this.KnowledgeBlockLine(item)
      }, (item: KnowledgeBaseItem, index: number) => item.title + index)
    }
    .lanes(new BreakpointType<number>({
      sm: Constants.LANES_ONE,
      md: Constants.LANES_ONE,
      lg: Constants.LANES_TWO,
      xl: Constants.LANES_TWO
    }).getValue(this.currentBreakpoint),
      $r('app.float.nav_bar_span'),
    )
    .backgroundColor(Color.White)
    .borderRadius($r('app.float.br16'))
    .padding({ left: $r('app.float.nav_bar_span'), right: $r('app.float.nav_bar_span') })
    .divider({
      strokeWidth: Constants.STROKE_WIDTH,
      startMargin: Constants.START_MARGIN,
      endMargin: Constants.END_MARGIN,
      color: $r('app.color.divider_color')
    })
  }
  .width(CommonConstants.FULL_SCREEN)
  .alignItems(HorizontalAlign.Start)
}
@Builder
KnowledgeBlock(material: Material) {
  Column() {
    Text(material.subtitle)
      .fontFamily($r('app.string.font_medium'))
      .fontSize($r('app.float.fs14'))
      .fontWeight(CommonConstants.FONT_WEIGHT_500)
      .margin({ bottom: $r('app.float.back_icon_margin_right') })

    List() {
      ForEach(material.knowledgeBase, (item: KnowledgeBaseItem, index: number) => {
        this.KnowledgeBlockLine(item)
      }, (item: KnowledgeBaseItem, index: number) => item.title + index)
    }
    .lanes(new BreakpointType<number>({
      sm: Constants.LANES_ONE,
      md: Constants.LANES_ONE,
      lg: Constants.LANES_TWO,
      xl: Constants.LANES_TWO
    }).getValue(this.currentBreakpoint),
      $r('app.float.nav_bar_span'),
    )
    .backgroundColor(Color.White)
    .borderRadius($r('app.float.br16'))
    .padding({ left: $r('app.float.nav_bar_span'), right: $r('app.float.nav_bar_span') })
    .divider({
      strokeWidth: Constants.STROKE_WIDTH,
      startMargin: Constants.START_MARGIN,
      endMargin: Constants.END_MARGIN,
      color: $r('app.color.divider_color')
    })
  }
  .width(CommonConstants.FULL_SCREEN)
  .alignItems(HorizontalAlign.Start)
}

设置组件导航

Navigation组件

基础概念

Navigation组件是路由导航的根视图容器,一般作为Page页面的根容器使用,可以实现路由进行切换。
知识地图首页切换到知识地图详情页由Navigation实现

Navigation组件的组成

单页面模式

image.png
首页 非首页

  • 首页页面结构
    • 标题栏:用于标识出整个页面的主题,支持两种类型的标题栏,也可以用Navigation的hideTitleBar属性隐藏标题栏
    • 菜单栏:支持将一些常用功能都放入菜单栏,开发者可以根据menus属性进行设置
    • 内容区:主要用于显示一些界面内容以及导航栏,点击导航栏会跳转到该导航栏所对应的内容区,实现页面的切换
    • 导航栏:最重要的部分
    • 工具栏:可以使用toolbarConfiguration属性进行设置
  • 非首页页面结构(NavDestination)
    • 标题栏:同样用于标识整内容区页面的主题,开发者可以使用NavDestination的title属性来控制标题栏的显示内容,也可以用Navigation的hideTitleBar属性控制标题栏的显示
    • 菜单栏:与首页的功能定义一致
    • 内容区:显示首页中当前导航栏对应的内容
分栏模式

使用Navigation组件时,若设置Navigation组件的mode属性为Navigation.Auto,那么当设备的宽度大于520vp时,Navigation组件会采用分栏模式。
image.png

标题栏

标题栏在界面顶部,用于呈现界面名称和操作入口,Navigation组件通过titleMode属性设置标题栏模式。支持两种显示模式:

  • Mini模式:普通型标题栏,用于一级页面不需要突出标题的场景
Naviagtion() {
  .....
}
.titleMode(NavigationTitleMode.Mini)
  • Full模式:强调型标题栏,用于一级页面需要突出标题的场景。
Naviagtion() {
  .....
}
.titleMode(NavigationTitleMode.Full)
菜单栏

菜单栏位于Navigation组件的右上角,开发者可以通过menus属性进行设置。
menus支持Array<NavigationMenuItem>CustonBuilder两种参数类型。
在使用Array<NavigationMenuItem>类型时,竖屏最多支持显示3个图标,横屏最多支持显示5个图标,多余的图标会被放入自动生成的更多图标。

let menuItem1: NavigationMenuItem = {'value': '', 'icon': './image/ic_search.svg', 'action': ()=> {} };
let menuItem1: NavigationMenuItem = {'value': '', 'icon': './image/ic_add.svg', 'action': ()=> {} };
Navigation(){
  ......
}
.menus([meunItem1, menuItem2, menuItem2])

image.png

工具栏

工具栏位于Navigation组件的底部开发者可以通过toolbarConfiguration属性进行设置。
该属性需要传入一个ToolbarItem构成的数组。

let toolItem: ToolbarItem = {'value': 'func', 'icon': './image/ic_public_highlights.svg', 'action': ()=> {} };
let toolBar: ToolbarItem[] = [toolItem, toolItem, toolItem]
Navigation() {
  ...
}
.toolbarConfiguration(toolbar)

image.png

模块内页面切换

在使用Navigation组件中,非常重要的一个点就是依靠Navigation组件提供的组件级路由能力实现更加自然流畅的转场体验,而这就需要依靠路由栈提供的系列方法。
官方文档:NavPathStack-Navigation路由栈

  • push系列方法:将指定的页面栈数据信息入栈
    • pushPath(info: NavPathInfo, animated?: boolean): void
    • pushPathByName(name: string, param: unknown, animated?: boolean): void
    • pushPathByName(name: string, param: Object,...)
    • pushDestination(info: NavPathInfo, animated?: boolean):Promise<void>
  • replace系列方法:将当前页面栈栈顶退出,再将指定的NavDestination页面信息入栈。
    • replacePath(info: NavPathInfo, animated?: boolean): void
    • replacePathByName(name: string, param: Object, animated?: boolean): void

pushPathByName为例

interface Param{
  name: string
}

const param5: Param = { name: 'test'};
this.pageInfos.pushPathByName('Page5', param5)

image.pngimage.png
replacePathByName为例

interface Param{
  name: string
}

const param1: Param = { name: 'test'};
this.pageInfos.replacePathByName('Page1', param5)

image.pngimage.png

路由传参与路由参数获取

image.png

  • getParamByIndex(index: number): unknown | undefined
    • 获取index指定的NavDestination页面的参数信息。
    • image.png
  • getParamByName(name: string): Array<unknown>
    • 获取全部名为name的NavDestination页面的参数信息
    • image.png

Tabs组件

简介

当页面信息较多时,为了让用户能够聚焦于当前显示的内容,需要对页面内容进行分类,提高页面空间利用率。Tabs组件可以在一个页面内快速实现视图内容的切换,一方面提升查找信息的效率,另一方面精简用户单次获取到的信息量。
Tabs(value?: {barPosition?: BarPosition, index?: number, controller?: TabsController})

  • barPosition:设置Tabs的页签位置。
  • index:设置当前显示页签的索引。
  • controller:设置Tabs控制器。

Tabs组件的使用

界面结构

TabBar按照不同的导航栏类型,可以分为底部导航、顶部导航、侧边导航。其导航栏分别位于底部、顶部和侧边。
image.png

controller

Tabs参数所传入的TabsController实例可以对Tabs显示的TabContent区域进行控制,控制方式是使用实例的changeIndex方法,该方法需要传入一个数值,能控制Tabs显示到对应的索引。

  • 关联controller与Tabs
struct Index {
  private tabsController: TabsController = new TabsController();

  // 关联controller
  Tabs({ controller: tabsController}) {
    ...
  }
}
  • 控制Tabs的TabContent区域显示的内容
this.tabsController.chanageIndex(1)
自定义TabBar

对于底部导航栏,一般作为应用主要页面功能区分,为了更好的用户体验,会组合文字以及对应语义图标标识页签内容,这种情况下,需要自定义当行列页签的样式

@Builder
tabBarBuilder(
  title:string,
  targetIndex:number,
  selectedIcon: Resource,
  unselectIcon: Resource
) {  
  Column() {
    Image(this.currentIndexX === targetIndex? selectedIcon : unselectIcon)
      .width(24)
      .height(24)
    Text(title)
      .fontFamily('HarmonyHeiTi-Medium')
      .fontsize(10)
      .fontColor(this.currentIndex === targetIndex? '#0A59F7' : '#99000000')
      .textAlign(TextAlign.Center)
      .lineHeight(14)
      .fontWeight(500)
  }
}

实操案例

Navigation实操

创建相关数据
@Component
export struct KnowledgeMap {
  // 创建路由栈
  @Provide('pageInfos') pageInfos: NavPathStack = new NavPathStack();

  // 创建NavDestination
  @Builder
  PageMap(name: string, param: Section) {
    if (name === 'KnowledgeMapContent') {
      KnowledgeMapContent();
    }
  }
  build() {
  }
}
绑定到Navigation,并添加点击事件实现路由跳转
@Component
export struct KnowledgeMap {
  @Provide('pageInfos') pageInfos: NavPathStack = new NavPathStack();

  @Builder
  PageMap(name: string, param: Section) {
    if (name === 'KnowledgeMapContent') {
      KnowledgeMapContent();
    }
  }

  @Builder
  NavBarItem(order: string, title: string) {
    // ...
  }

  // 添加点击事件实现路由跳转
  .onClick(() => {
    const index = Number(order) - 1;
    this.pageInfos.pushPathByName('KnowledgeMapContent', this.sections[index]);
  })

  // ...
  build() {
    // 绑定路由栈
    Navigation(this.pageInfos) {
      //...
      List({ space: 12 }) {
        ForEach(this.navBarList, (item: NavBarItemType, index: number) => {
          ListItem() {
            this.NavBarItem(item.order, item.title)
          }
        }, (item: NavBarItemType): string => item.title)
      }.width('100%')

      // ...
    }
    // 绑定NavDestination
    .navDestination(this.PageMap)
  }
}
路由子组件获取数据
export struct KnowledgeMapContent {
  @Consume('pageInfos') pageInfos: NavPathStack;
  @State section: Section | null = null

  aboutToAppear(): void {
    const size = this.pageInfos.size();
    this.section = this.pageInfos.getParamByIndex(size - 1) as Section;
  }
}

Tabbs实操-使用自定义tabBar

@Entry
@Component
struct Index {
  private tabsController: TabsController = new TabsController();

  @Builder
  tabBarBuilder(
    title: string,
    targetIndex: number,
    selectedIcon: Resource,
    unselectIcon: Resource
  ) {
    Column() {
      Image(this.currentIndex === targetIndex ? selectedIcon : unselectIcon).width(24).height(24)
      Text(title)
        .fontFamily('HarmonyHeiTi-Medium')
        .fontSize(10)
        .fontColor(this.currentIndex === targetIndex ? '#0A59F7' : '#99000000')
        .textAlign(TextAlign.Center)
        .lineHeight(14)
        .fontWeight(500)
    }
    .onClick(() => {
      this.currentIndex = targetIndex;
      this.tabsController.changeIndex(targetIndex);
    })
  }

  build() {
    Tabs({ barPosition: BarPosition.End, controller: this.controller }) {
      TabContent() {
        QuickStartPage()
      }.tabBar(this.tabBarBuilder('快速入门', 0, $r('app.media.ic_01_on'), $r('app.media.ic_01_off')))

      TabContent() {
        CourseLearning()
      }.tabBar(this.tabBarBuilder('课程学习', 1, $r('app.media.ic_01_on'), $r('app.media.ic_01_off')))

      TabContent() {
        KnowledgeMap()
      }.tabBar(this.tabBarBuilder('知识地图', 2, $r('app.media.ic_01_on'), $r('app.media.ic_01_off')))
    }
  }
}
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐