HarmonyOS(二十三)——HTTP请求实战一个可切换的头条列表

news2024/11/26 14:35:38

在前一篇文章,我们已经知道如何实现一个http请求的完整流程,今天就用官方列子实战一个简单的新闻列表。进一步掌握ArkTS的声明式开发范式,数据请求,常用系统组件以及touch事件的使用。

主要包含以下功能:

  1. 数据请求。
  2. 列表下拉刷新。
  3. 列表上拉加载。

看一下最终的效果。
在这里插入图片描述

1.实战分析准备

既然是实现一个新闻列表请求,那么肯定少不了服务端搭建以及前端代码实战,下面就从这俩个方面一一实战讲解。

2.服务端搭建

服务端搭建,这里就用node.js提供服务接口支持,方便大家更好的理解和修改接口数据。

  1. 搭建nodejs环境:本篇Codelab的服务端是基于nodejs实现的,需要安装nodejs,如果您本地已有nodejs环境可以跳过此步骤。

    • 检查本地是否安装nodejs:打开命令行工具(如Windows系统的cmd和Mac电脑的Terminal,这里以Windows为例),输入node -v,如果可以看到版本信息,说明已经安装nodejs。
      在这里插入图片描述
    • 如果本地没有nodejs环境,您可以去nodejs官网上下载所需版本进行安装配置。
    • 配置完环境变量后,重新打开命令行工具,输入node -v,如果可以看到版本信息,说明已安装成功。
    • 运行服务端代码: 去下载华为官方提供的HttpServerOfNews服务端代码到本地,在项目的根目录,下打开命令行工具,输入npm install 安装服务端依赖包,安装成功后输入npm start点击回车。看到“服务器启动成功!”则表示服务端已经在正常运行。
    • 连接服务器地址:打开命令行工具,输入ipconfig命令查看本地ip,将本地ip地址复制到src/main/ets/common/constant/CommonConstants.ets文件下的23行,注意只替换ip地址部分,不要修改端口号,保存好ip之后即可运行代码进行测试。

3. 前端实战前准备分析工作

前端实现非常简单,可以按照以下几个步骤实现。

  1. 点击应用进入主页面,页面使用tabBar展示新闻分类,tabContent展示新闻列表,新闻分类和新闻列表通过请求nodejs服务端获取。
  2. 点击页签或左右滑动页面,切换标签并展示对应新闻类型的数据。
  3. 新闻列表页面,滑动到新闻列表首项数据,接着往下滑动会触发下拉刷新操作,页面更新初始4条新闻数据,滑动到新闻列表最后一项数据,往上拉会触发上拉加载操作,新闻列表会在后面加载4条新闻数据。

逻辑思路清晰了,下面我们开始实战前的准备配置。既然是实现网络请求,那么首先就需要配置网络权限。

  • 配置网络权限
    在进行网络请求前,您需要在module.json5文件中申明网络访问权限。
{
    "module" : {
        "requestPermissions":[
           {
             "name": "ohos.permission.INTERNET"
           }
        ]
    }
}

网络权限配置完成,就可以开始愉快的coding了

4.构建主界面

  1. 用tabBar展示新闻分类
    在TabBar.ets文件中的aboutToAppear()方法里获取新闻分类。代码如下:
import NewsList from '../view/newslist';
import { CommonConstant as Const } from '../common/constant/CommonConstant';
import NewsViewModel, { NewsTypeBean } from '../viewmodel/NewsViewModel';

/**
 * The tabBar component.
 */
@Component
export default struct TabBar {
  @State tabBarArray: NewsTypeBean[] = NewsViewModel.getDefaultTypeList();
  @State currentIndex: number = 0;
  @State currentPage: number = 1;

  @Builder TabBuilder(index: number) {
    Column() {
      Text(this.tabBarArray[index].name)
        .height(Const.FULL_HEIGHT)
        .padding({ left: Const.TabBars_HORIZONTAL_PADDING, right: Const.TabBars_HORIZONTAL_PADDING })
        .fontSize(this.currentIndex === index ? Const.TabBars_SELECT_TEXT_FONT_SIZE : Const.TabBars_UN_SELECT_TEXT_FONT_SIZE)
        .fontWeight(this.currentIndex === index ? Const.TabBars_SELECT_TEXT_FONT_WEIGHT : Const.TabBars_UN_SELECT_TEXT_FONT_WEIGHT)
        .fontColor($r('app.color.fontColor_text3'))
    }
  }

  aboutToAppear() {
    // Request news category.
    NewsViewModel.getNewsTypeList().then((typeList: NewsTypeBean[]) => {
      this.tabBarArray = typeList;
    }).catch((typeList: NewsTypeBean[]) => {
      this.tabBarArray = typeList;
    });
  }

  build() {
    Tabs() {
      ForEach(this.tabBarArray, (tabsItem: NewsTypeBean) => {
        TabContent() {
          Column() {
            NewsList({ currentIndex: $currentIndex })
          }
        }
        .tabBar(this.TabBuilder(tabsItem.id))
      }, (item: NewsTypeBean) => JSON.stringify(item));
    }
    .barHeight(Const.TabBars_BAR_HEIGHT)
    .barMode(BarMode.Scrollable)
    .barWidth(Const.TabBars_BAR_WIDTH)
    .onChange((index: number) => {
      this.currentIndex = index;
      this.currentPage = 1;
    })
    .vertical(false)
  }
}
  1. tabContent展示新闻列表
    在NewsList.ets文件中的aboutToAppear()方法里获取新闻数据,将数据加载到新闻列表页面ListLayout布局中。而数据列表是高度相似可以服用的一个item, 它由标题,描述信息,日期,以及若干图片组成, 因此,可以简单抽取并复用一个NewsItem, 完整代码如下所示:

NewsItem代码如下:

import { CommonConstant, CommonConstant as Const } from '../common/constant/CommonConstant';
import { NewsData, NewsFile } from '../viewmodel/NewsViewModel';

/**
 * The news list item component.
 */
@Component
export default struct NewsItem {
  private newsData: NewsData = new NewsData();

  build() {
    Column() {
      Row() {
        Image($r('app.media.news'))
          .width(Const.NewsTitle_IMAGE_WIDTH)
          .height($r('app.float.news_title_image_height'))
          .objectFit(ImageFit.Fill)
        Text(this.newsData.title)
          .fontSize(Const.NewsTitle_TEXT_FONT_SIZE)
          .fontColor($r('app.color.fontColor_text'))
          .width(Const.NewsTitle_TEXT_WIDTH)
          .maxLines(1)
          .margin({ left: Const.NewsTitle_TEXT_MARGIN_LEFT })
          .textOverflow({ overflow: TextOverflow.Ellipsis })
          .fontWeight(Const.NewsTitle_TEXT_FONT_WEIGHT)
      }
      .alignItems(VerticalAlign.Center)
      .height($r('app.float.news_title_row_height'))
      .margin({
        top: $r('app.float.news_title_row_margin_top'),
        left: Const.NewsTitle_IMAGE_MARGIN_LEFT
      })

      Text(this.newsData.content)
        .fontSize(Const.NewsContent_FONT_SIZE)
        .fontColor($r('app.color.fontColor_text'))
        .height(Const.NewsContent_HEIGHT)
        .width(Const.NewsContent_WIDTH)
        .maxLines(Const.NewsContent_MAX_LINES)
        .margin({ left: Const.NewsContent_MARGIN_LEFT, top: Const.NewsContent_MARGIN_TOP })
        .textOverflow({ overflow: TextOverflow.Ellipsis })

      Grid() {
        ForEach(this.newsData.imagesUrl, (itemImg: NewsFile) => {
          GridItem() {
            Image(Const.SERVER + itemImg.url)
              .objectFit(ImageFit.Cover)
              .borderRadius(Const.NewsGrid_IMAGE_BORDER_RADIUS)
          }
        }, (itemImg: NewsFile, index?: number) => JSON.stringify(itemImg) + index)
      }
      .columnsTemplate(CommonConstant.GRID_COLUMN_TEMPLATES.repeat(this.newsData.imagesUrl.length))
      .columnsGap(Const.NewsGrid_COLUMNS_GAP)
      .rowsTemplate(Const.NewsGrid_ROWS_TEMPLATE)
      .width(Const.NewsGrid_WIDTH)
      .height(Const.NewsGrid_HEIGHT)
      .margin({ left: Const.NewsGrid_MARGIN_LEFT, top: Const.NewsGrid_MARGIN_TOP,
        right: Const.NewsGrid_MARGIN_RIGHT })

      Text(this.newsData.source)
        .fontSize(Const.NewsSource_FONT_SIZE)
        .fontColor($r('app.color.fontColor_text2'))
        .height(Const.NewsSource_HEIGHT)
        .width(Const.NewsSource_WIDTH)
        .maxLines(Const.NewsSource_MAX_LINES)
        .margin({ left: Const.NewsSource_MARGIN_LEFT, top: Const.NewsSource_MARGIN_TOP })
        .textOverflow({ overflow: TextOverflow.None })
    }
    .alignItems(HorizontalAlign.Start)
  }
}

新闻列表NewsList代码如下:

import promptAction from '@ohos.promptAction';
import { CommonConstant, CommonConstant as Const, PageState } from '../common/constant/CommonConstant';
import NewsItem from './NewsItem';
import LoadMoreLayout from './LoadMoreLayout';
import RefreshLayout from './RefreshLayout';
import CustomRefreshLoadLayout from './CustomRefreshLoadLayout';
import { listTouchEvent } from '../common/utils/PullDownRefresh';
import NewsViewModel, { CustomRefreshLoadLayoutClass, NewsData } from '../viewmodel/NewsViewModel';
import NoMoreLayout from './NoMoreLayout';
import NewsModel from '../viewmodel/NewsModel';

/**
 * The news list component.
 */
@Component
export default struct NewsList {
  @State newsModel: NewsModel = new NewsModel();
  @Watch('changeCategory') @Link currentIndex: number;

  changeCategory() {
    this.newsModel.currentPage = 1;
    NewsViewModel.getNewsList(this.newsModel.currentPage, this.newsModel.pageSize, Const.GET_NEWS_LIST)
      .then((data: NewsData[]) => {
        this.newsModel.pageState = PageState.Success;
        if (data.length === this.newsModel.pageSize) {
          this.newsModel.currentPage++;
          this.newsModel.hasMore = true;
        } else {
          this.newsModel.hasMore = false;
        }
        this.newsModel.newsData = data;
      })
      .catch((err: string | Resource) => {
        promptAction.showToast({
          message: err,
          duration: Const.ANIMATION_DURATION
        });
        this.newsModel.pageState = PageState.Fail;
      });
  }

  aboutToAppear() {
    // Request news data.
    this.changeCategory();
  }

  build() {
    Column() {
      if (this.newsModel.pageState === PageState.Success) {
        this.ListLayout()
      } else if (this.newsModel.pageState === PageState.Loading) {
        this.LoadingLayout()
      } else {
        this.FailLayout()
      }
    }
    .width(Const.FULL_WIDTH)
    .height(Const.FULL_HEIGHT)
    .justifyContent(FlexAlign.Center)
    .onTouch((event: TouchEvent | undefined) => {
      if (event) {
        if (this.newsModel.pageState === PageState.Success) {
          listTouchEvent(this.newsModel, event);
        }
      }
    })
  }

  @Builder LoadingLayout() {
    CustomRefreshLoadLayout({ customRefreshLoadClass: new CustomRefreshLoadLayoutClass(true,
      $r('app.media.ic_pull_up_load'), $r('app.string.pull_up_load_text'), this.newsModel.pullDownRefreshHeight) })
  }

  @Builder ListLayout() {
    List() {
      ListItem() {
        RefreshLayout({
          refreshLayoutClass: new CustomRefreshLoadLayoutClass(this.newsModel.isVisiblePullDown, this.newsModel.pullDownRefreshImage,
            this.newsModel.pullDownRefreshText, this.newsModel.pullDownRefreshHeight)
        })
      }

      ForEach(this.newsModel.newsData, (item: NewsData) => {
        ListItem() {
          NewsItem({ newsData: item })
        }
        .height($r('app.float.news_list_height'))
        .backgroundColor($r('app.color.white'))
        .margin({ top: $r('app.float.news_list_margin_top') })
        .borderRadius(Const.NewsListConstant_ITEM_BORDER_RADIUS)
      }, (item: NewsData, index?: number) => JSON.stringify(item) + index)

      ListItem() {
        if (this.newsModel.hasMore) {
          LoadMoreLayout({
            loadMoreLayoutClass: new CustomRefreshLoadLayoutClass(this.newsModel.isVisiblePullUpLoad, this.newsModel.pullUpLoadImage,
              this.newsModel.pullUpLoadText, this.newsModel.pullUpLoadHeight)
          })
        } else {
          NoMoreLayout()
        }
      }
    }
    .width(Const.NewsListConstant_LIST_WIDTH)
    .height(Const.FULL_HEIGHT)
    .margin({ left: Const.NewsListConstant_LIST_MARGIN_LEFT, right: Const.NewsListConstant_LIST_MARGIN_RIGHT })
    .backgroundColor($r('app.color.listColor'))
    .divider({
      color: $r('app.color.dividerColor'),
      strokeWidth: Const.NewsListConstant_LIST_DIVIDER_STROKE_WIDTH,
      endMargin: Const.NewsListConstant_LIST_MARGIN_RIGHT
    })
    // Remove the rebound effect.
    .edgeEffect(EdgeEffect.None)
    .offset({ x: 0, y: `${this.newsModel.offsetY}${CommonConstant.LIST_OFFSET_UNIT}` })
    .onScrollIndex((start: number, end: number) => {
      // Listen to the first index of the current list.
      this.newsModel.startIndex = start;
      this.newsModel.endIndex = end;
    })
  }

  @Builder FailLayout() {
    Image($r('app.media.none'))
      .height(Const.NewsListConstant_NONE_IMAGE_SIZE)
      .width(Const.NewsListConstant_NONE_IMAGE_SIZE)
    Text($r('app.string.page_none_msg'))
      .opacity(Const.NewsListConstant_NONE_TEXT_opacity)
      .fontSize(Const.NewsListConstant_NONE_TEXT_size)
      .fontColor($r('app.color.fontColor_text3'))
      .margin({ top: Const.NewsListConstant_NONE_TEXT_margin })
  }
}
  1. 实现下拉刷新
    前面我们完成了一个新闻列表页面用于显示新闻信息所要用到的所有组件,但是,通常用户手机的大小是有限制的,为了用户能更好,更实时,更全面的获取新闻,我们通常还要实现下拉刷新和上拉加载功能。

创建一个下拉刷新布局CustomLayout,动态传入刷新图片和刷新文字描述。

// CustomRefreshLoadLayout.ets
build() {
  Row() {
    // 下拉刷新图片
    Image(this.customRefreshLoadClass.imageSrc)
      ...
    // 下拉刷新文字
    Text(this.customRefreshLoadClass.textValue)
      ...
  }
  ...
}

将下拉刷新的布局添加到NewsList.ets文件中新闻列表布局ListLayout里面,监听ListLayout组件的onTouch事件实现下拉刷新

// NewsList.ets
build() {
  Column() {
    if (this.newsModel.pageState === PageState.Success) {
      this.ListLayout()
    }
    ...
  }
  ...
  .onTouch((event: TouchEvent | undefined) => {
    if (event) {
      if (this.newsModel.pageState === PageState.Success) {
        listTouchEvent(this.newsModel, event);
      }
    }
  })
}
...
@Builder ListLayout() {
  List() {
    ListItem() {
      RefreshLayout({
        refreshLayoutClass: new CustomRefreshLoadLayoutClass(this.newsModel.isVisiblePullDown, this.newsModel.pullDownRefreshImage,
          this.newsModel.pullDownRefreshText, this.newsModel.pullDownRefreshHeight)
      })
      ...
    }
  }
  ...
}

完整下啦刷新代码如下:

import { CustomRefreshLoadLayoutClass } from '../viewmodel/NewsViewModel';
import CustomRefreshLoadLayout from './CustomRefreshLoadLayout';

/**
 * The refresh layout component.
 */
@Component
export default struct RefreshLayout {
  @ObjectLink refreshLayoutClass: CustomRefreshLoadLayoutClass;

  build() {
    Column() {
      if (this.refreshLayoutClass.isVisible) {
        CustomRefreshLoadLayout({ customRefreshLoadClass: new CustomRefreshLoadLayoutClass
        (this.refreshLayoutClass.isVisible, this.refreshLayoutClass.imageSrc, this.refreshLayoutClass.textValue,
          this.refreshLayoutClass.heightValue) })
      }
    }
  }
}
import { CommonConstant as Const } from '../common/constant/CommonConstant';
import { CustomRefreshLoadLayoutClass } from '../viewmodel/NewsViewModel';

/**
 * Custom layout to show refresh or load.
 */
@Component
export default struct CustomLayout {
  @ObjectLink customRefreshLoadClass: CustomRefreshLoadLayoutClass;

  build() {
    Row() {
      Image(this.customRefreshLoadClass.imageSrc)
        .width(Const.RefreshLayout_IMAGE_WIDTH)
        .height(Const.RefreshLayout_IMAGE_HEIGHT)

      Text(this.customRefreshLoadClass.textValue)
        .margin({
          left: Const.RefreshLayout_TEXT_MARGIN_LEFT,
          bottom: Const.RefreshLayout_TEXT_MARGIN_BOTTOM
        })
        .fontSize(Const.RefreshLayout_TEXT_FONT_SIZE)
        .textAlign(TextAlign.Center)
    }
    .clip(true)
    .width(Const.FULL_WIDTH)
    .justifyContent(FlexAlign.Center)
    .height(this.customRefreshLoadClass.heightValue)
  }
}
import promptAction from '@ohos.promptAction';
import { touchMoveLoadMore, touchUpLoadMore } from './PullUpLoadMore';
import {
  CommonConstant as Const,
  RefreshState
} from '../constant/CommonConstant';
import NewsViewModel, { NewsData } from '../../viewmodel/NewsViewModel';
import NewsModel from '../../viewmodel/NewsModel';

export function listTouchEvent(newsModel: NewsModel, event: TouchEvent) {
  switch (event.type) {
    case TouchType.Down:
      newsModel.downY = event.touches[0].y;
      newsModel.lastMoveY = event.touches[0].y;
      break;
    case TouchType.Move:
      if ((newsModel.isRefreshing === true) || (newsModel.isLoading === true)) {
        return;
      }
      let isDownPull = event.touches[0].y - newsModel.lastMoveY > 0;
      if (((isDownPull === true) || (newsModel.isPullRefreshOperation === true)) && (newsModel.isCanLoadMore === false))
      {
        // Finger movement, processing pull-down refresh.
        touchMovePullRefresh(newsModel, event);
      } else {
        // Finger movement, processing load more.
        touchMoveLoadMore(newsModel, event);
      }
      newsModel.lastMoveY = event.touches[0].y;
      break;
    case TouchType.Cancel:
      break;
    case TouchType.Up:
      if ((newsModel.isRefreshing === true) || (newsModel.isLoading === true)) {
        return;
      }
      if ((newsModel.isPullRefreshOperation === true)) {
        // Lift your finger and pull down to refresh.
        touchUpPullRefresh(newsModel);
      } else {
        // Fingers up, handle loading more.
        touchUpLoadMore(newsModel);
      }
      break;
    default:
      break;
  }
}

export function touchMovePullRefresh(newsModel: NewsModel, event: TouchEvent) {
  if (newsModel.startIndex === 0) {
    newsModel.isPullRefreshOperation = true;
    let height = vp2px(newsModel.pullDownRefreshHeight);
    newsModel.offsetY = event.touches[0].y - newsModel.downY;
    // The sliding offset is greater than the pull-down refresh layout height, and the refresh condition is met.
    if (newsModel.offsetY >= height) {
      pullRefreshState(newsModel, RefreshState.Release);
      newsModel.offsetY = height + newsModel.offsetY * Const.Y_OFF_SET_COEFFICIENT;
    } else {
      pullRefreshState(newsModel, RefreshState.DropDown);
    }
    if (newsModel.offsetY < 0) {
      newsModel.offsetY = 0;
      newsModel.isPullRefreshOperation = false;
    }
  }
}

export function touchUpPullRefresh(newsModel: NewsModel) {
  if (newsModel.isCanRefresh === true) {
    newsModel.offsetY = vp2px(newsModel.pullDownRefreshHeight);
    pullRefreshState(newsModel, RefreshState.Refreshing);
    newsModel.currentPage = 1;
    setTimeout(() => {
      let self: NewsModel = newsModel;
      NewsViewModel.getNewsList(newsModel.currentPage, newsModel.pageSize, Const.GET_NEWS_LIST).then((data:
        NewsData[]) => {
        if (data.length === newsModel.pageSize) {
          self.hasMore = true;
          self.currentPage++;
        } else {
          self.hasMore = false;
        }
        self.newsData = data;
        closeRefresh(self, true);
      }).catch((err: string | Resource) => {
        promptAction.showToast({ message: err });
        closeRefresh(self, false);
      });
    }, Const.DELAY_TIME);
  } else {
    closeRefresh(newsModel, false);
  }
}

export function pullRefreshState(newsModel: NewsModel, state: number) {
  switch (state) {
    case RefreshState.DropDown:
      newsModel.pullDownRefreshText = $r('app.string.pull_down_refresh_text');
      newsModel.pullDownRefreshImage = $r('app.media.ic_pull_down_refresh');
      newsModel.isCanRefresh = false;
      newsModel.isRefreshing = false;
      newsModel.isVisiblePullDown = true;
      break;
    case RefreshState.Release:
      newsModel.pullDownRefreshText = $r('app.string.release_refresh_text');
      newsModel.pullDownRefreshImage = $r('app.media.ic_pull_up_refresh');
      newsModel.isCanRefresh = true;
      newsModel.isRefreshing = false;
      break;
    case RefreshState.Refreshing:
      newsModel.offsetY = vp2px(newsModel.pullDownRefreshHeight);
      newsModel.pullDownRefreshText = $r('app.string.refreshing_text');
      newsModel.pullDownRefreshImage = $r('app.media.ic_pull_up_load');
      newsModel.isCanRefresh = true;
      newsModel.isRefreshing = true;
      break;
    case RefreshState.Success:
      newsModel.pullDownRefreshText = $r('app.string.refresh_success_text');
      newsModel.pullDownRefreshImage = $r('app.media.ic_succeed_refresh');
      newsModel.isCanRefresh = true;
      newsModel.isRefreshing = true;
      break;
    case RefreshState.Fail:
      newsModel.pullDownRefreshText = $r('app.string.refresh_fail_text');
      newsModel.pullDownRefreshImage = $r('app.media.ic_fail_refresh');
      newsModel.isCanRefresh = true;
      newsModel.isRefreshing = true;
      break;
    default:
      break;
  }
}

export function closeRefresh(newsModel: NewsModel, isRefreshSuccess: boolean) {
  let self = newsModel;
  setTimeout(() => {
    let delay = Const.RefreshConstant_DELAY_PULL_DOWN_REFRESH;
    if (self.isCanRefresh === true) {
      pullRefreshState(newsModel, isRefreshSuccess ? RefreshState.Success : RefreshState.Fail);
      delay = Const.RefreshConstant_DELAY_SHRINK_ANIMATION_TIME;
    }
    animateTo({
      duration: Const.RefreshConstant_CLOSE_PULL_DOWN_REFRESH_TIME,
      delay: delay,
      onFinish: () => {
        pullRefreshState(newsModel, RefreshState.DropDown);
        self.isVisiblePullDown = false;
        self.isPullRefreshOperation = false;
      }
    }, () => {
      self.offsetY = 0;
    })
  }, self.isCanRefresh ? Const.DELAY_ANIMATION_DURATION : 0);
}
  1. 实现上拉加载更多
import { CustomRefreshLoadLayoutClass } from '../viewmodel/NewsViewModel';
import CustomRefreshLoadLayout from './CustomRefreshLoadLayout';

/**
 * The load more layout component.
 */
@Component
export default struct LoadMoreLayout {
  @ObjectLink loadMoreLayoutClass: CustomRefreshLoadLayoutClass;

  build() {
    Column() {
      if (this.loadMoreLayoutClass.isVisible) {
        CustomRefreshLoadLayout({
          customRefreshLoadClass: new CustomRefreshLoadLayoutClass(this.loadMoreLayoutClass.isVisible,
            this.loadMoreLayoutClass.imageSrc, this.loadMoreLayoutClass.textValue, this.loadMoreLayoutClass.heightValue)
        })
      } else {
        CustomRefreshLoadLayout({
          customRefreshLoadClass: new CustomRefreshLoadLayoutClass(this.loadMoreLayoutClass.isVisible,
            this.loadMoreLayoutClass.imageSrc, this.loadMoreLayoutClass.textValue, 0)
        })
      }
    }
  }
}
import { CommonConstant as Const } from '../common/constant/CommonConstant';
import { CustomRefreshLoadLayoutClass } from '../viewmodel/NewsViewModel';

/**
 * Custom layout to show refresh or load.
 */
@Component
export default struct CustomLayout {
  @ObjectLink customRefreshLoadClass: CustomRefreshLoadLayoutClass;

  build() {
    Row() {
      Image(this.customRefreshLoadClass.imageSrc)
        .width(Const.RefreshLayout_IMAGE_WIDTH)
        .height(Const.RefreshLayout_IMAGE_HEIGHT)

      Text(this.customRefreshLoadClass.textValue)
        .margin({
          left: Const.RefreshLayout_TEXT_MARGIN_LEFT,
          bottom: Const.RefreshLayout_TEXT_MARGIN_BOTTOM
        })
        .fontSize(Const.RefreshLayout_TEXT_FONT_SIZE)
        .textAlign(TextAlign.Center)
    }
    .clip(true)
    .width(Const.FULL_WIDTH)
    .justifyContent(FlexAlign.Center)
    .height(this.customRefreshLoadClass.heightValue)
  }
}
import promptAction from '@ohos.promptAction';
import { touchMoveLoadMore, touchUpLoadMore } from './PullUpLoadMore';
import {
  CommonConstant as Const,
  RefreshState
} from '../constant/CommonConstant';
import NewsViewModel, { NewsData } from '../../viewmodel/NewsViewModel';
import NewsModel from '../../viewmodel/NewsModel';

export function listTouchEvent(newsModel: NewsModel, event: TouchEvent) {
  switch (event.type) {
    case TouchType.Down:
      newsModel.downY = event.touches[0].y;
      newsModel.lastMoveY = event.touches[0].y;
      break;
    case TouchType.Move:
      if ((newsModel.isRefreshing === true) || (newsModel.isLoading === true)) {
        return;
      }
      let isDownPull = event.touches[0].y - newsModel.lastMoveY > 0;
      if (((isDownPull === true) || (newsModel.isPullRefreshOperation === true)) && (newsModel.isCanLoadMore === false))
      {
        // Finger movement, processing pull-down refresh.
        touchMovePullRefresh(newsModel, event);
      } else {
        // Finger movement, processing load more.
        touchMoveLoadMore(newsModel, event);
      }
      newsModel.lastMoveY = event.touches[0].y;
      break;
    case TouchType.Cancel:
      break;
    case TouchType.Up:
      if ((newsModel.isRefreshing === true) || (newsModel.isLoading === true)) {
        return;
      }
      if ((newsModel.isPullRefreshOperation === true)) {
        // Lift your finger and pull down to refresh.
        touchUpPullRefresh(newsModel);
      } else {
        // Fingers up, handle loading more.
        touchUpLoadMore(newsModel);
      }
      break;
    default:
      break;
  }
}

export function touchMovePullRefresh(newsModel: NewsModel, event: TouchEvent) {
  if (newsModel.startIndex === 0) {
    newsModel.isPullRefreshOperation = true;
    let height = vp2px(newsModel.pullDownRefreshHeight);
    newsModel.offsetY = event.touches[0].y - newsModel.downY;
    // The sliding offset is greater than the pull-down refresh layout height, and the refresh condition is met.
    if (newsModel.offsetY >= height) {
      pullRefreshState(newsModel, RefreshState.Release);
      newsModel.offsetY = height + newsModel.offsetY * Const.Y_OFF_SET_COEFFICIENT;
    } else {
      pullRefreshState(newsModel, RefreshState.DropDown);
    }
    if (newsModel.offsetY < 0) {
      newsModel.offsetY = 0;
      newsModel.isPullRefreshOperation = false;
    }
  }
}

export function touchUpPullRefresh(newsModel: NewsModel) {
  if (newsModel.isCanRefresh === true) {
    newsModel.offsetY = vp2px(newsModel.pullDownRefreshHeight);
    pullRefreshState(newsModel, RefreshState.Refreshing);
    newsModel.currentPage = 1;
    setTimeout(() => {
      let self: NewsModel = newsModel;
      NewsViewModel.getNewsList(newsModel.currentPage, newsModel.pageSize, Const.GET_NEWS_LIST).then((data:
        NewsData[]) => {
        if (data.length === newsModel.pageSize) {
          self.hasMore = true;
          self.currentPage++;
        } else {
          self.hasMore = false;
        }
        self.newsData = data;
        closeRefresh(self, true);
      }).catch((err: string | Resource) => {
        promptAction.showToast({ message: err });
        closeRefresh(self, false);
      });
    }, Const.DELAY_TIME);
  } else {
    closeRefresh(newsModel, false);
  }
}

export function pullRefreshState(newsModel: NewsModel, state: number) {
  switch (state) {
    case RefreshState.DropDown:
      newsModel.pullDownRefreshText = $r('app.string.pull_down_refresh_text');
      newsModel.pullDownRefreshImage = $r('app.media.ic_pull_down_refresh');
      newsModel.isCanRefresh = false;
      newsModel.isRefreshing = false;
      newsModel.isVisiblePullDown = true;
      break;
    case RefreshState.Release:
      newsModel.pullDownRefreshText = $r('app.string.release_refresh_text');
      newsModel.pullDownRefreshImage = $r('app.media.ic_pull_up_refresh');
      newsModel.isCanRefresh = true;
      newsModel.isRefreshing = false;
      break;
    case RefreshState.Refreshing:
      newsModel.offsetY = vp2px(newsModel.pullDownRefreshHeight);
      newsModel.pullDownRefreshText = $r('app.string.refreshing_text');
      newsModel.pullDownRefreshImage = $r('app.media.ic_pull_up_load');
      newsModel.isCanRefresh = true;
      newsModel.isRefreshing = true;
      break;
    case RefreshState.Success:
      newsModel.pullDownRefreshText = $r('app.string.refresh_success_text');
      newsModel.pullDownRefreshImage = $r('app.media.ic_succeed_refresh');
      newsModel.isCanRefresh = true;
      newsModel.isRefreshing = true;
      break;
    case RefreshState.Fail:
      newsModel.pullDownRefreshText = $r('app.string.refresh_fail_text');
      newsModel.pullDownRefreshImage = $r('app.media.ic_fail_refresh');
      newsModel.isCanRefresh = true;
      newsModel.isRefreshing = true;
      break;
    default:
      break;
  }
}

export function closeRefresh(newsModel: NewsModel, isRefreshSuccess: boolean) {
  let self = newsModel;
  setTimeout(() => {
    let delay = Const.RefreshConstant_DELAY_PULL_DOWN_REFRESH;
    if (self.isCanRefresh === true) {
      pullRefreshState(newsModel, isRefreshSuccess ? RefreshState.Success : RefreshState.Fail);
      delay = Const.RefreshConstant_DELAY_SHRINK_ANIMATION_TIME;
    }
    animateTo({
      duration: Const.RefreshConstant_CLOSE_PULL_DOWN_REFRESH_TIME,
      delay: delay,
      onFinish: () => {
        pullRefreshState(newsModel, RefreshState.DropDown);
        self.isVisiblePullDown = false;
        self.isPullRefreshOperation = false;
      }
    }, () => {
      self.offsetY = 0;
    })
  }, self.isCanRefresh ? Const.DELAY_ANIMATION_DURATION : 0);
}
  1. 新闻数据请求
    前面俩步我们已经实现了一个新闻列表页面展示所需要的各个组件。下面就是来到真正的请求数据环节。由于前一篇认识HTTP请求之从网络获取数据已经详细介绍过,这里不做过多描述,直接封装一个通用http 请求工具类实现数据请求:

导入http模块,封装httpRequestGet方法,调用者传入url地址发起网络数据请求。

import http from '@ohos.net.http';
import { ResponseResult } from '../../viewmodel/NewsViewModel';
import { CommonConstant as Const, ContentType } from '../constant/CommonConstant';

/**
 * Initiates an HTTP request to a given URL.
 *
 * @param url URL for initiating an HTTP request.
 * @param params Params for initiating an HTTP request.
 */
export function httpRequestGet(url: string): Promise<ResponseResult> {
  let httpRequest = http.createHttp();
  let responseResult = httpRequest.request(url, {
    method: http.RequestMethod.GET,
    readTimeout: Const.HTTP_READ_TIMEOUT,
    header: {
      'Content-Type': ContentType.JSON
    },
    connectTimeout: Const.HTTP_READ_TIMEOUT,
    extraData: {}
  });
  let serverData: ResponseResult = new ResponseResult();
  // Processes the data and returns.
  return responseResult.then((value: http.HttpResponse) => {
    if (value.responseCode === Const.HTTP_CODE_200) {
      // Obtains the returned data.
      let result = `${value.result}`;
      let resultJson: ResponseResult = JSON.parse(result);
      if (resultJson.code === Const.SERVER_CODE_SUCCESS) {
        serverData.data = resultJson.data;
      }
      serverData.code = resultJson.code;
      serverData.msg = resultJson.msg;
    } else {
      serverData.msg = `${$r('app.string.http_error_message')}&${value.responseCode}`;
    }
    return serverData;
  }).catch(() => {
    serverData.msg = $r('app.string.http_error_message');
    return serverData;
  })
}
  1. NewsViewModel实现,获取服务端新闻数据列表
    在NewsViewModel.ets文件中封装getNewsList方法,调用httpRequestGet方法请求服务端,用Promise异步保存返回的新闻数据列表。
// NewsViewModel.ets
// 获取服务端新闻数据列表
getNewsList(currentPage: number, pageSize: number, path: string): Promise<NewsData[]> {
  return new Promise(async (resolve: Function, reject: Function) => {
    let url = `${Const.SERVER}/${path}`;
    url += '?currentPage=' + currentPage + '&pageSize=' + pageSize;
    httpRequestGet(url).then((data: ResponseResult) => {
      if (data.code === Const.SERVER_CODE_SUCCESS) {
        resolve(data.data);
      } else {
        Logger.error('getNewsList failed', JSON.stringify(data));
        reject($r('app.string.page_none_msg'));
      }
    }).catch((err: Error) => {
      Logger.error('getNewsList failed', JSON.stringify(err));
      reject($r('app.string.http_error_message'));
    });
  });
}

完整NewsViewModel.ets代码如下:

import { CommonConstant as Const } from '../common/constant/CommonConstant';
import { httpRequestGet } from '../common/utils/HttpUtil';
import Logger from '../common/utils/Logger';

class NewsViewModel {
  /**
   * Get news type list from server.
   *
   * @return NewsTypeBean[] newsTypeList
   */
  getNewsTypeList(): Promise<NewsTypeBean[]> {
    return new Promise((resolve: Function) => {
      let url = `${Const.SERVER}/${Const.GET_NEWS_TYPE}`;
      httpRequestGet(url).then((data: ResponseResult) => {
        if (data.code === Const.SERVER_CODE_SUCCESS) {
          resolve(data.data);
        } else {
          resolve(Const.TabBars_DEFAULT_NEWS_TYPES);
        }
      }).catch(() => {
        resolve(Const.TabBars_DEFAULT_NEWS_TYPES);
      });
    });
  }

  /**
   * Get default news type list.
   *
   * @return NewsTypeBean[] newsTypeList
   */
  getDefaultTypeList(): NewsTypeBean[] {
    return Const.TabBars_DEFAULT_NEWS_TYPES;
  }

  /**
   * Get news type list from server.
   *
   * @return NewsData[] newsDataList
   */
  getNewsList(currentPage: number, pageSize: number, path: string): Promise<NewsData[]> {
    return new Promise(async (resolve: Function, reject: Function) => {
      let url = `${Const.SERVER}/${path}`;
      url += '?currentPage=' + currentPage + '&pageSize=' + pageSize;
      httpRequestGet(url).then((data: ResponseResult) => {
        if (data.code === Const.SERVER_CODE_SUCCESS) {
          resolve(data.data);
        } else {
          Logger.error('getNewsList failed', JSON.stringify(data));
          reject($r('app.string.page_none_msg'));
        }
      }).catch((err: Error) => {
        Logger.error('getNewsList failed', JSON.stringify(err));
        reject($r('app.string.http_error_message'));
      });
    });
  }
}

let newsViewModel = new NewsViewModel();

export default newsViewModel as NewsViewModel;

/**
 * News list item info.
 */
export class NewsData {
  /**
   * News list item title.
   */
  title: string = '';

  /**
   * News list item content.
   */
  content: string = '';

  /**
   * News list item imagesUrl.
   */
  imagesUrl: Array<NewsFile> = [new NewsFile()];

  /**
   * News list item source.
   */
  source: string = '';
}

/**
 * News image list item info.
 */
export class NewsFile {
  /**
   * News image list item id.
   */
  id: number = 0;

  /**
   * News image list item url.
   */
  url: string = '';

  /**
   * News image list item type.
   */
  type: number = 0;

  /**
   * News image list item newsId.
   */
  newsId: number = 0;
}

/**
 * Custom refresh load layout data.
 */
@Observed
export class CustomRefreshLoadLayoutClass {
  /**
   * Custom refresh load layout isVisible.
   */
  isVisible: boolean;

  /**
   * Custom refresh load layout imageSrc.
   */
  imageSrc: Resource;

  /**
   * Custom refresh load layout textValue.
   */
  textValue: Resource;

  /**
   * Custom refresh load layout heightValue.
   */
  heightValue: number;

  constructor(isVisible: boolean, imageSrc: Resource, textValue: Resource, heightValue: number) {
    this.isVisible = isVisible;
    this.imageSrc = imageSrc;
    this.textValue = textValue;
    this.heightValue = heightValue;
  }
}

export class NewsTypeBean {
  id: number = 0;
  name: ResourceStr = '';
}

export class ResponseResult {
  /**
   * Code returned by the network request: success, fail.
   */
  code: string;

  /**
   * Message returned by the network request.
   */
  msg: string | Resource;

  /**
   * Data returned by the network request.
   */
  data: string | Object | ArrayBuffer;

  constructor() {
    this.code = '';
    this.msg = '';
    this.data = '';
  }
}

新闻列表数据详情数据结构以及页面分页刷行

import { CommonConstant as Const, PageState } from '../common/constant/CommonConstant';
import { NewsData } from './NewsViewModel';

export default class NewsModel {
  newsData: Array<NewsData> = [];
  currentPage: number = 1;
  pageSize: number = Const.PAGE_SIZE;
  pullDownRefreshText: Resource = $r('app.string.pull_down_refresh_text');
  pullDownRefreshImage: Resource = $r('app.media.ic_pull_down_refresh');
  pullDownRefreshHeight: number = Const.CUSTOM_LAYOUT_HEIGHT;
  isVisiblePullDown: boolean = false;
  pullUpLoadText: Resource = $r('app.string.pull_up_load_text');
  pullUpLoadImage: Resource = $r('app.media.ic_pull_up_load');
  pullUpLoadHeight: number = Const.CUSTOM_LAYOUT_HEIGHT;
  isVisiblePullUpLoad: boolean = false;
  offsetY: number = 0;
  pageState: number = PageState.Loading;
  hasMore: boolean = true;
  startIndex = 0;
  endIndex = 0;
  downY = 0;
  lastMoveY = 0;
  isRefreshing: boolean = false;
  isCanRefresh = false;
  isPullRefreshOperation = false;
  isLoading: boolean = false;
  isCanLoadMore: boolean = false;
}
  1. 难点以及注意事项

1. 在onTouch事件中,listTouchEvent方法判断触摸事件是否满足下拉条件。

2. 在touchMovePullRefresh方法中,我们将对下拉的偏移量与下拉刷新布局的高度进行对比,如 果大于布局高度并且在新闻列表的顶部,则表示达到刷新条件。

3. 在pullRefreshState方法中我们会对下拉刷新布局中的状态图片和描述进行改变,当手指松开,才执行刷新操作。

// PullDownRefresh.ets
export function listTouchEvent(newsModel: NewsModel, event: TouchEvent) {
  switch (event.type) {
    ...
    case TouchType.Move:
      if ((newsModel.isRefreshing === true) || (newsModel.isLoading === true)) {
        return;
      }
      let isDownPull = event.touches[0].y - newsModel.lastMoveY > 0;
      if (((isDownPull === true) || (newsModel.isPullRefreshOperation === true)) && (newsModel.isCanLoadMore === false))
      {
        // 手指移动,处理下拉刷新
        touchMovePullRefresh(newsModel, event);
      }
      ...
      break;
  }
}
export function touchMovePullRefresh(newsModel: NewsModel, event: TouchEvent) {
  if (newsModel.startIndex === 0) {
    newsModel.isPullRefreshOperation = true;
    let height = vp2px(newsModel.pullDownRefreshHeight);
    newsModel.offsetY = event.touches[0].y - newsModel.downY;
    // 滑动偏移量大于下拉刷新布局高度,满足刷新条件。
    if (newsModel.offsetY >= height) {
      pullRefreshState(newsModel, RefreshState.Release);
      newsModel.offsetY = height + newsModel.offsetY * Const.Y_OFF_SET_COEFFICIENT;
    } else {
      pullRefreshState(newsModel, RefreshState.DropDown);
    }
    if (newsModel.offsetY < 0) {
      newsModel.offsetY = 0;
      newsModel.isPullRefreshOperation = false;
    }
  }
}
export function pullRefreshState(newsModel: NewsModel, state: number) {
  switch (state) {
    ...
    case RefreshState.Release:
      newsModel.pullDownRefreshText = $r('app.string.release_refresh_text');
      newsModel.pullDownRefreshImage = $r('app.media.ic_pull_up_refresh');
      newsModel.isCanRefresh = true;
      newsModel.isRefreshing = false;
      break;
    case RefreshState.Refreshing:
      newsModel.offsetY = vp2px(newsModel.pullDownRefreshHeight);
      newsModel.pullDownRefreshText = $r('app.string.refreshing_text');
      newsModel.pullDownRefreshImage = $r('app.media.ic_pull_up_load');
      newsModel.isCanRefresh = true;
      newsModel.isRefreshing = true;
      break;
    case RefreshState.Success:
      newsModel.pullDownRefreshText = $r('app.string.refresh_success_text');
      newsModel.pullDownRefreshImage = $r('app.media.ic_succeed_refresh');
      newsModel.isCanRefresh = true;
      newsModel.isRefreshing = true;
      break;
    ...
    default:
      break;
  }
}

上拉加载也是通过touch事件来实现的,此处不再赘叙。

5.总结

  1. 实现网络请求,我们需要在module.json5文件中申明网络访问权限。
  2. 鸿蒙实现网络请求,需要导入@ohos.net.http,并且每一个httpRequest对应一个HTTP请求任务,不可复用。
  3. 刷新和上拉加载都是借助触摸事件onTouch来实现的,关于触摸事件onTouch的使用可以参考官方文档《触摸事件onTouch》,后期会专门出一篇文章讲解触摸事件的使用及其使用场景。
  4. 灵活掌握各个组件的特性和使用场景,可以帮助我们快速完成开发,比如此次新闻列表页,我们用到了如下三个系统组件:
    • List组件:列表包含一系列相同宽度的列表项,用于显示完整的新闻列表,是最大的容器。
    • Tabs:通过页签进行内容视图切换,可以切换不同类型的新闻。
    • TabContent:仅在Tabs中使用,对应一个切换页签的内容视图。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1803291.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

matplotlib 动态显示梯度下降过程

文章目录 简介曲线下降曲面下降 简介 梯度下降是一种优化算法&#xff0c;常用于寻找函数的最小值或最大值。它通过迭代更新参数的方式逐步减小&#xff08;或增大&#xff09;目标函数的值&#xff0c;直到达到某个停止条件为止。梯度下降的基本思想是沿着目标函数的负梯度方…

BeagleBone Black入门总结

文章目录 参考连接重要路径系统镜像下载访问 BeagleBone 参考连接 镜像下载启动系统制作&#xff1a;SD卡烧录工具入门书籍推荐&#xff1a;BeagleBone cookbookBeagleBone概况&#xff1f; 重要路径 官方例程及脚本路径&#xff1a;/var/lib/cloud9 系统镜像下载 疑问&am…

电子设计教学新篇章:SmartEDA引领学校教学升级风潮

在数字化时代的浪潮中&#xff0c;电子设计教学领域正迎来一场革命性的变革。SmartEDA&#xff0c;作为电子设计课程的新宠&#xff0c;以其高效、智能的特性&#xff0c;正成为学校教学升级的重要推手。它不仅极大地提升了电子设计的效率&#xff0c;还为学生们带来了更为深入…

TOGAF数字化转型的关键(文尾附在线TOGAF免费测试)

业务架构驱动数据架构和应用架构的设计&#xff0c;而应用架构又依赖于数据架构和技术架构的支持。技术架构则为整个架构提供了稳定的基础设施。 在数字化转型中&#xff0c;协调和整合这四种架构是至关重要的。通过确保它们之间的一致性和协同工作&#xff0c;可以实现企业业务…

使用OpenPCDet训练与测试Transformer模型:如何加载自己的数据集

引言 Transformer架构因其强大的序列处理能力和长距离依赖捕捉能力&#xff0c;在自然语言处理领域取得了巨大成功。近年来&#xff0c;这一架构也被引入3D物体检测领域&#xff0c;如Voxel Transformer等&#xff0c;显著提升了模型在复杂场景下的检测性能。OpenPCDet整合了多…

K8s速览

k8s的核心能力 ● 服务发现与负载均衡 ● 服务恢复 ● 服务伸缩 ● 自动发布与回滚 ● 批量执行 架构 server-client两层架构&#xff0c;Master作为中央管控节点&#xff0c;会和每一个Node进行一个连接&#xff1b; 所有UI层&#xff0c;client的操作&#xff0c;只会和Mat…

英伟达Docker 安装与GPu镜像拉取

获取nvidia_docker压缩包nvidia_docker.tgz将压缩包上传至服务器指定目录解压nvidia_docker.tgz压缩包 tar -zxvf 压缩包执行rpm安装命令&#xff1a; #查看指定rpm包安装情况 rpm -qa | grep libstdc #查看指定rpm包下的依赖包的版本情况 strings /lib64/libstdc |grep GLI…

酒店旅游API服务汇总

各大旅游平台常用API服务汇总&#xff1a; 实时房源服务【Airbnb】飞猪旅行开放服务途牛旅行开放平台API华为云数字差旅【差旅管理】动态信息接口【美团酒店】旅行商城商家管理API【马蜂窝】交易流程接口【美团酒店】电子导游【携程旅行】

STM32编程:实现LED灯闪烁(基于手写SDK的方式)

项目结构 stm32f10x.h 文件 //寄存器的值常常是芯片外设自动更改的&#xff0c;即使CPU没有执行程序&#xff0c;也有可能发生变化 //编译器有可能会对没有执行程序的变量进行优化//volatile表示易变的变量&#xff0c;防止编译器优化&#xff0c; #define __IO volati…

CSAPP Lab02——Bomb Lab完成思路详解

看见的看不见的 瞬间的永恒的 青草长啊大雪飘扬 ——月亮之上 完整代码见&#xff1a;CSAPP/bomb at main SnowLegend-star/CSAPP (github.com) 01 字符串比较 简单的把输入的字符串和地址“0x402400”内早已存储的字符串相比较。如果两个字符串相等则函数返回&#xff0c;否…

SpringBoot+Vue甘肃非物质文化网站(前后端分离)

技术栈 JavaSpringBootMavenMySQLMyBatisVueShiroElement-UI 系统角色对应功能 用户管理员 系统功能截图

SpringBoot Elasticsearch07-以黑马商场为例-黑马程序员学习笔记

06篇已经导入了大量数据到elasticsearch中&#xff0c;实现了商品数据的存储。不过查询商品数据时依然采用的是根据id查询&#xff0c;而非模糊搜索。 接下来研究下elasticsearch的数据搜索功能。Elasticsearch提供了基于JSON的DSL&#xff08;Domain Specific Language&#…

2024年06月数据库流行度最新排名

点击查看最新数据库流行度最新排名&#xff08;每月更新&#xff09; 2024年06月数据库流行度最新排名 TOP DB顶级数据库索引是通过分析在谷歌上搜索数据库名称的频率来创建的 一个数据库被搜索的次数越多&#xff0c;这个数据库就被认为越受欢迎。这是一个领先指标。原始数…

09-数组的含义以及零长数组变长数组与多维数组

09-数组的含义以及零长数组变长数组与多维数组 文章目录 09-数组的含义以及零长数组变长数组与多维数组一、数组名的含义1.1 表示整个数组的首地址1.2 表示整个数组首元素的首地址 二、数组下标字符串常量 三、零长数组3.1 示例 四、变长数组4.1 示例 五、多维数组5.1 定义与初…

UML实现图-部署图

概述 部署图(Deployent Diagram)描述了运行软件的系统中硬件和软件的物理结构。部署图中通常包含两种元素:节点和关联关系&#xff0c;部署图中每个配置必须存在于某些节点上。部署图也可以包含包或子系统。 节点是在运行时代表计算机资源的物理元素。节点名称有两种:简单名和…

APP开发技术的变迁史

随着移动互联网的迅猛发展&#xff0c;APP&#xff08;应用程序&#xff09;已经成为人们日常生活中不可或缺的一部分。从最初的简单工具到如今的智能平台&#xff0c;APP开发技术在这十年间经历了翻天覆地的变化。本文将从多个维度探讨近十年来APP开发技术的变迁史&#xff0c…

NVeloDocx一个基于NVelocity的word模版引擎

NVeloDocx是一个基于NVelocity的Word模版引擎&#xff0c;目前主要是用于E6低代码开发平台供用户轻松制作各种Word报告模版。 有以下优点&#xff1a; 1、完全的NVelocity语法&#xff1b; 2、直接在Word中写NVelocity脚本&#xff0c;使用非常非常方便&#xff1b; 3、完全兼…

阅读笔记:Life of a Pixel

PPT地址&#xff1a;​​​​​​​​​​​​​​https://docs.google.com/presentation/d/1boPxbgNrTU0ddsc144rcXayGA_WF53k96imRH8Mp34Y/edit?uspsharing 这份PPT讲述了Chromium浏览器内核中html文档渲染成像素的主要过程。网上有很多介绍和转载&#xff0c;内容非常硬核。…

数据结构及研究

**数据结构是计算机存储、组织数据的方式&#xff0c;它是相互之间存在一种或多种特定关系的数据元素的集合**Θic-1ΘΘic-2ΘΘic-3ΘΘic-4ΘΘic-5Θ。 数据结构这一概念在计算机科学领域扮演着至关重要的角色&#xff0c;它不仅决定了数据在计算机内部的存储方式&#xf…

lubuntu / ubuntu 配置静态ip

一、查看原始网络配置信息 1、获取网卡名称 ifconfig 2、查询网关IP route -n 二、编辑配置文件 去/etc/netplan目录找到配置文件&#xff0c;配置文件名一般为01-network-manager-all.yaml sudo vim /etc/netplan/01-network-manager-all.yaml文件打开后内容如下 # This …