页面路由
实现不同页面之间的跳转和数据传递。
页面栈。上限是32个页面,使用router.clear()
可以清空页面栈释放内存。
两种跳转模式: 参数一致,都是三个参数
router.pushUrl()
,新页面压入页面栈,可以用router.back()
返回当前页router.replaceUrl()
目标页替换当前页,当前页被销毁,无法返回当前页
登录页基本只访问一次,无需返回。适合用第二种。
首页->搜索页应当使用第一种。
两种页面示例模式:
-Standard 默认,每次跳转都新建一个目标压入栈顶
- Single 单示例模式,如果目标页已在栈中,则离栈顶最近的URL页面会被移动到栈顶并重新加载
导入HarmonyOS提供的Router模块:
import router from 'ohos.router'
router.pushUrl(
{ //RouterOptions对象,含有url和params(可选)
url:'page/ImagePage',
params:{id:1} //可选
},
router.RouterMode.Single,//页面实例模式,枚举类型
err=>{ //异常回调函数,错误码,100001:内部错误,可能是渲染失败,100002:路由地址错误,100003:栈>32
if(err){
console.log('fail')
}
}
)
//获取传递来的参数
params:any=router.getParams()
//返回上一页
router.back()
//返回到指定页,并携带参数
router.back(
{
url:'pages/Index',
params:{id:10}
}
)
想要跳转页面,必须在resource-base-profile-main_pages.json里配置,在新建文件的时候直接新建page,编译器会自动在上面的文件里加入此页面,比较方便。
{
"src": [
"pages/Index",
"pages/ImagePage",
"pages/loopPractice",
"pages/stateManagement"
]
}
首页
import router from '@ohos.router'
class RouterInfo{
url:string
title:string
constructor(url:string,title:string) {
this.url=url
this.title=title
}
}
@Entry
@Component
struct Index {
private routers:RouterInfo[]=[
new RouterInfo('pages/ImagePage','图片查看样例'),
new RouterInfo('pages/loopPractice','商品列表案例'),
new RouterInfo('pages/stateManagement','任务列表案例')
]
build() {
Column(){
Text('页面列表')
List(){
ForEach(this.routers,
(router,index)=>{
ListItem(){
this.RouterItem(router,index+1)
}
}
)
}
.layoutWeight(1)
}
}
@Builder
RouterItem(r:RouterInfo,i:number){
Row(){
Text(i+'.')
Blank()
Text(r.title)
}
.onClick(()=>{
router.pushUrl(
{
url: r.url,
params: {id:i}
},
router.RouterMode.Single,
err=>{
if(err){
console.log(`路由失败,错误码:${err.code} errMsg:${err.message}`)
}
}
)
})
}
}
图片展示案例页
Header({title:"图片查看样例"})
.onClick(()=>{
//返回前的警告
router.showAlertBeforeBackPage({
message:'确定要返回吗?'
})
router.back()
})
效果
动画
可以提高用户交互时的体验
arkui会帮忙填充每一帧的画面,只需要写初始和结束的状态,以及播放时长等。
x从左到右,y从上到下
属性动画
animation
显式动画
组件转场动画
Stage模型
应用模型
概述
不同模块放到不同module里开发,具有通用的一些东西,把这些抽取出来,放到单独的依赖类型的模块里
可以new一个module,有empty ability,也有library module,核心是这两大类。
每一个模块都可以独立编译运行,HAP=harmony ability package,HSP=harmony shared package,HAP可以引用HSP包。
entry是项目的入口,是核心部分,作为入口的HAP只有一个,其他HAP是Feature类型的。
Bundle有一个Bundle name,是整个应用的唯一标识。
打包成app,就是安装包了。
Ability翻译成应用组件
UIAbility是主流的,ExtensionAbility是拓展的
好处:降低耦合;选择性安装feature模块,降低空间占用;跨设备
应用配置文件
AppScope/app.json5
{
"app": {
"bundleName": "com.example.hellloworld", //应用的唯一标识
"vendor": "example",
"versionCode": 1000000, //数字格式的版本
"versionName": "1.0.0", //字符串格式的版本
"icon": "$media:app_icon", //app图标
"label": "$string:app_name" //应用的描述 并没有直接指定,而是读取AppScope/resource/media下的文件和element/string.json
//这是在应用列表里展示的信息
}
}
entry/scr/main/module.json5
{
"module": {
"name": "entry", //模块的名字
"type": "entry", //模块的类型,ability细分为entry和feature;library但是叫做"shared"类型
"description": "$string:module_desc", //"$string:module_desc"
"mainElement": "EntryAbility", //当前模块的入口 打开以后会发现继承了UIAbility的东西
"deviceTypes": [ //设备类型 还有电视,手表等
"phone",
"tablet"
],
"deliveryWithInstall": true, //安装时是否必须,feature可以按需安装
"installationFree": false,
"pages": "$profile:main_pages", //entry/src/main/resources/base/profile/main_pages.json
"abilities": [ //ability数组,一个模块可以包含多个ability
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ts",
"description": "$string:EntryAbility_desc",
"icon": "$media:icon", //即桌面上的图标和名称,可以和应用列表里的不一样
"label": "$string:EntryAbility_label", //去改base/element/string.json,可以打开右上角的编辑器,一起改
"startWindowIcon": "$media:icon", //启动时展示的东西
"startWindowBackground": "$color:start_window_background",
"exported": true,
"skills": [
{
"entities": [
"entity.system.home"
],
"actions": [
"action.system.home"
]
}
]
}
],
"requestPermissions":[
{
"name": "ohos.permission.INTERNET"//name必填,请求网络
}
]
}
}
UIAbility 生命周期
创建-前台
每一个任务是一个独立的ability
应用可以切换到后台、前台,可以销毁
//hilog是一个导入的,可以带上日志级别,domain域(16进制),tag标记(方便日志查询),日志内容%s是占位符,后面是参数,public指可以公开(部署)
通过testTag过滤查看日志,发现其过程和理论一致
ability创建->windowStage创建->foreground前台,展示->background后台->windowStageDestroy销毁->Ability 销毁
页面及组件生命周期
页面被销毁,则组件被销毁
但页面存在,组件不一定存在,也可能被销毁
提供了一些钩子来帮助完成渲染。
页面的展示从入口组件开始,页面生命周期钩子只能在加了entry的组件里使用,不会在普通组件里被触发,另外两个属于组件的生命周期钩子,可以在任意自定义组件中使用。
这些钩子可以在ets里用,
aboutToAppear()在页面/组件创造时出现,可以做一下数据的初始化和准备
onPageShow() 在页面展示时出现
onBackPress() 页面返回(系统提供的)
onPageHide() 页面隐藏
aboutToDisappear() 页面/组件销毁,关键性数据可以去做保存,或资源释放
组件不存在隐藏,只会被销毁。
用foreach渲染的话,在每次元素变化时,会销毁所有元素,再创造所有元素。如果加了foreach的第三个参数(唯一标识)就不会这样。
页面路由のpush会使得页面hide,而replace会使得页面disappear
UIAbility的启动模式
单实例模式、标准模式(standard多个实例并存,multition旧实例会销毁)、指定模式(按key决定是否创建新实例,用途:文档编辑软件,同一个文档可以保留,不同文档可以多开。api较为复杂)
- entry/EntryAbility.ts 默认是Singleton,要用模拟器来看ability的日志,用onCreate等来验证。回到桌面:Ability onBackground,还存在,再次启动访问,发现:Ability onForeground,从后台切到了前台。
改变启动模式,要在module.json5修改,多写一个"launchType":standard
,发现其实有四种类型,并不是三种。
- 当模式修改为multition时,日志为:
可见再次创建了ability,这是一个新的ability,任务列表里只存在一个实例,旧的被干掉了
- 当模式修改为standard时,日志和multition类似,但任务列表中存在两个ability
- specified模式
startAbility来唤醒(拉起)
want(意图)是传入的key,成员包括:divice ID(不传指本设备,意味着可以跨设备),bundlename包名,abilityName实例名,moduleName模块名,parameters参数(UIAbility实例的Key)
取得参数,还可以拼接一些生成一个key
在json5文件中配置
例子:跨Ability的跳转,不能用router做,得用这个
new一个ability,和entryability是平行的
此ability一启动,加载的是文档编辑器模块。
在module.json5里修改launchType为specified
需要导包,
填写的want参数为:
其他都是本项目、本模块,只是跳转到另一ability。
接下来创建stage,需要自己新建目录:myabilitystage,新建ts文件MyAbilitystage.ts,实现生命周期钩子。
还需要在module.json5里配置srcEntry,指stage舞台的文件
这样去就做好了。返回的逻辑和去一致,修改一下abilityName,此时跳转不需要参数,因为它是单实例模式
必须在模拟机上测试,测试结果:
entry进入了background,文档onCreate,点击返回,document进入后台,entry进入前台。再创建一个文档,同时存在三个ability。
网络连接
http:客户端向服务端发起的单向的连接方式
webSocket:客户端和服务端的双向连接
socket:套接字,底层,实现UDP,TCP等通讯协议
http请求数据
底层是UDP,需要三次握手建立连接
- http的请求需要每次使用的时候创建,不能复用。
- 调用request方法,第一个参数是url路径(协议,主机名,端口号,也可以拼接请求参数),第二个参数是请求选项,字段有很多,可以选择。method,查询get,新增post,修改put,删除delete;extraData的string类型一般在get时用,Object复杂类型在提交、修改时使用,是键值对格式,多组用&拼接,也可以不在extraData里,而拼到url后面,Object要装在{}里;header放一些特殊信息,比如用户身份凭证,connectTimeout最长建立连接的时间,默认是60s,单位是毫秒,readTimeout读取超时,同上
- 处理响应结果,网络请求会有延迟,往往要通过异步任务处理。凡异步都会返回一个Promise,存放未来会完成的结果,先添加成功和失败的回调函数。
Promise提供了then成功和catch失败两个方法。返回的是HttpResponse,而非直接的数据。包含:responseCode状态码,200成功,400错误等,需要判断是否成功;header;cookies存放身份凭证;result我们要的响应的数据,有string类型(最常见),json字符串,还有Object数据流等较为少见;resultType响应结果的类型。
上课的例子是用nodejs模拟服务器
npm install
安装依赖
默认端口是3000,此处可以改
启动服务端npm start
pageNo=1&pageSize=2,分页查询,第一页,每页两个。查出来了:
图片的路径是相对服务器的路径
ShopPages.ets
import { Header } from '../common/components/CommonComponents'
import ShopInfo from '../viewmodel/ShopInfo'
import ShopItem from '../views/ShopItem'
import ShopModel from '../model/ShopModel'
@Entry
@Component
struct ShopPage {
@State shops: ShopInfo[] = []
isLoading: boolean = false //判断是否正在加载
isMore: boolean = true //判断是否还有更多数据
aboutToAppear() {
// 加载商品数据
this.loadShopInfo()
}
build() {
Column({ space: 10 }) {
Header({ title: '商铺列表' })
List({ space: 10 }) {
ForEach(this.shops, shop => {
ListItem() {
ShopItem({ shop: shop })
}
})
}
.width('100%')
.layoutWeight(1)
.onReachEnd(() => { //List刷到底了
console.log('触底了!')
if(!this.isLoading && this.isMore){
this.isLoading = true //因为回弹动画会导致触底两次,为了避免请求两次,增加isLoading的判断
// 翻页
ShopModel.pageNo++
this.loadShopInfo() //实现边划边加载
}
})
}
.width('100%')
.height('100%')
.padding(10)
.backgroundColor('#e1e2e3')
}
loadShopInfo(){
// 加载数据
ShopModel.getShopList() //返回的是Promise,这是一个将来的结果,那边日志已经打印过了,这边就不catch了
.then(shops => {
// 给图片加上服务器地址前缀
shops.forEach(s => {
s.images.forEach((src, i) => { //此处是为了修改端口号时方便因此拼接
s.images[i] = 'http://localhost:3000' + src
})
})
this.shops = this.shops.concat(shops) //把新数据拼接到到旧数据的后面,实现滑动时请求到新数据
this.isLoading = false //此时要把isLoading改回false
if(!shops || shops.length === 0){ //说明没有数据了
this.isMore = false //没有更多数据了
}
})
}
}
ShopModel.ts 数据模型类
import http from '@ohos.net.http'; //导入http模块
import ShopInfo from '../viewmodel/ShopInfo';
class ShopModel{
baseURL: string = 'http://localhost:3000'
pageNo: number = 1
getShopList(): Promise<ShopInfo[]>{
return new Promise((resolve, reject) => { //整个大函数的return,拿到这个Promise的人也去then和catch
// 1.创建http的请求对象
let httpRequest = http.createHttp() //这一对象不可复用
// 2.发送请求,异步调用,异步返回,要返回一个将来的结果Promise
httpRequest.request(
`${this.baseURL}/shops?pageNo=${this.pageNo}&pageSize=3`,
{
method: http.RequestMethod.GET //请求发送代码
}
)
.then(resp => { //加上回调,then指成功
if(resp.responseCode === 200){ //response是一个http的对象,结果在resp.result里,转换为string类型,json.parse可以把json字符串转成对象
// 查询成功
console.log('查询商铺成功!', resp.result)
resolve(JSON.parse(resp.result.toString()))//成功的通知
}else{
console.log('查询商铺信息失败!error:', JSON.stringify(resp))
reject('查询商铺失败') //失败的通知
}
})
.catch(error => { //catch代表出现异常,记录日志,error是一个对象,要转换成json的字符串
console.log('查询商铺信息失败!error:', JSON.stringify(error))
reject('查询商铺失败')
})
})
}
}
const shopModel = new ShopModel();
export default shopModel as ShopModel; //导出给别人使用
正确的日志:
第三方库axios
是最流行的发送请求的第三方库,不在鸿蒙中集成,需要用到ohpm来管理第三方库
ohpm=open harmony package manager
下载安装ohpm
按照提示配置了node.js的环境变量,进入命令行运行init.bat
表示成功。
此时还需要把ohpm目录配置到环境变量中去,环境变量中加入
此时可以在命令行的任意位置使用ohpm
下载安装axios
所有网络模块都需要配置这个权限。
其他第三方库:https://ohpm.openharmony.cn/#/cn/home 可以看到一些文档
需要回到开发工具去安装,项目目录下的oh-package.json5是依赖管理文件,下图是正确的
我的报错了:
解决方法,powershell安全策略
再次打开IDE,成功:
而且dependencies也出现了axios库:
在项目目录下可以看到oh_modules,里面就是下载的包
使用axios
get,put等各种方法,请求支持的参数非常多,params和data非常常见,params是get的参数,data是put等的参数,拿到Promise结果。axios会自动帮我们把json换成对象。
把之前的shopModel进行修改:不需要创建新对象,可见axios非常强大,而且简洁
import ShopInfo from '../viewmodel/ShopInfo';
import axios from '@ohos/axios'
class ShopModel{
baseURL: string = 'http://localhost:3000'
pageNo: number = 1
getShopList(): Promise<ShopInfo[]>{
return new Promise((resolve, reject) => {
axios.get(
`${this.baseURL}/shops`,
{
params: {pageNo: this.pageNo, pageSize: 3} //在这里,可以传参,不需要拼到url里
}
)
.then(resp => {
if(resp.status === 200){
// 查询成功
console.log('查询商铺成功!', JSON.stringify(resp.data))
resolve(resp.data) //不需要像http一样转换
}else{
console.log('查询商铺信息失败!error:', JSON.stringify(resp))
reject('查询商铺失败')
}
})
.catch(error => {
console.log('查询商铺信息失败!error:', JSON.stringify(error))
reject('查询商铺失败')
})
})
}
}
const shopModel = new ShopModel();
export default shopModel as ShopModel;
预览一下,和视频一模一样,太Nb了
数据持久化
之前的数据都在内存里,必须要存起来
用户首选项 preference
数据结构key-value,支持轻量级(如小说的页面设置,结构简单,数据少)。
首先要创建用户首选项的实例,可以创建多个,每个对应一个持久化文件(不需要关注底层,只需要调用接口)。
harmonyos提供了dataPreference不需要安装
参数:context是上下文,字符串是首选项的名称(需要唯一)。
读写磁盘上的持久化文件,是异步操作,会返回Promise对象,仍然是调用then,catch
key需要保持唯一,如果已存在,会覆盖。
then为什么要flush?因为只是写到了实例里,还没有刷到磁盘里。
get需要传一个默认值,以防找不到。
例子:
黑马写了一个字体面板,用来设置字体大小,并做用户首选项的持久化。
entry/src/main/ets/common/util/PreferencesUtil.ts工具类
异步写法:
改造成同步写法:加了异步标记,仍然会被异步包装。下面是同步写法。
没有写删除
import preferences from '@ohos.data.preferences'; //导入包
class PreferencesUtil{
prefMap: Map<string, preferences.Preferences> = new Map() //preference名字:实例 --map
async loadPreference(context, name: string){ //先定义一个方法,用来加载实例,async标记为异步
try { // 加载preferences
let pref = await preferences.getPreferences(context, name) //get得到的是将来的结果,await等待结果
this.prefMap.set(name, pref) //把key-value放进映射里
console.log('testTag', `加载Preferences[${name}]成功`)
} catch (e) { //同步也要捕获异常
console.log('testTag', `加载Preferences[${name}]失败`, JSON.stringify(e))
}
}
//新增操作,value的类型比较自由,有专门的一个类型:preferences.valueType
async putPreferenceValue(name: string, key: string, value: preferences.ValueType){
if (!this.prefMap.has(name)) { //判断map里有没有这个值
console.log('testTag', `Preferences[${name}]尚未初始化!`)
return
}
//map里有这个值,可以用map取到Preference对象
try {
let pref = this.prefMap.get(name)
// 写入数据
await pref.put(key, value)
// 刷盘
await pref.flush()
console.log('testTag', `保存Preferences[${name}.${key} = ${value}]成功`)
} catch (e) {
console.log('testTag', `保存Preferences[${name}.${key} = ${value}]失败`, JSON.stringify(e))
}
}
//读取
async getPreferenceValue(name: string, key: string, defaultValue: preferences.ValueType){
if (!this.prefMap.has(name)) {
console.log('testTag', `Preferences[${name}]尚未初始化!`)
return
}
try {
let pref = this.prefMap.get(name)
// 读数据,不需要刷盘
let value = await pref.get(key, defaultValue)
console.log('testTag', `读取Preferences[${name}.${key} = ${value}]成功`)
return value //返回结果
} catch (e) {
console.log('testTag', `读取Preferences[${name}.${key} ]失败`, JSON.stringify(e))
}
}
}
const preferencesUtil = new PreferencesUtil()
export default preferencesUtil as PreferencesUtil
工具类写好了,来使用它。load在何时调用?项目启动时调用(和生命周期有关),在entryAbility onCreate时加载preference:src/main/ets/entryability/EntryAbility.ets
import PreferencesUtil from '../common/util/PreferencesUtil'
export default class EntryAbility extends UIAbility {
async onCreate(want, launchParam) {
hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate running');
// 加载Preferences ,异步
await PreferencesUtil.loadPreference(this.context, 'MyPreferences')
// 初始化任务表
TaskModel.initTaskDB(this.context)
}
}
在首页indext.ets中使用,页面一加载就载入,用到页面的生命周期函数
@Provide fontSize: number = 16
async aboutToAppear(){
this.fontSize = await PreferencesUtil.getPreferenceValue('MyPreferences', 'IndexFontSize', 16) as number //value转换成number
}
修改时改变,src/main/ets/views/IndexFontSizePanel.ets中:
import PreferencesUtil from '../common/util/PreferencesUtil'
@Component
export default struct IndexFontSizePanel {
@Consume fontSize: number //父子组件双向绑定
fontSizLabel: object = {
14: '小',
16: '标准',
18: '大',
20: '特大',
}
build() {
Column() {
Text(this.fontSizLabel[this.fontSize]).fontSize(20)
Row({ space: 5 }) {
Text('A').fontSize(14).fontWeight(FontWeight.Bold)
Slider({
min: 14,
max: 20,
step: 2,
value: this.fontSize
})
.showSteps(true)
.trackThickness(6)
.layoutWeight(1)
.onChange(val => {
// 修改字体大小
this.fontSize = val
// 写入Preferences
PreferencesUtil.putPreferenceValue('MyPreferences', 'IndexFontSize', val)
})
Text('A').fontSize(20).fontWeight(FontWeight.Bold)
}.width('100%')
}
.width('100%')
.padding(15)
.backgroundColor('#fff1f0f0')
.borderRadius(20)
}
}
测试需要在模拟器里做
preferenceMap里包含多个preference实例,每个实例的数据结构是key-value。返回的都是异步,需要then,catch,可以做同步编程,加async,await,不需要写回调函数,用try-catch进行异常捕获。
关系型数据库
这种数据库不需要网络请求,同时有各种高级特性。
一个应用可以创建多个数据库。
安全级别有四级,S1是最低级别
异步
不需要手写sql语句,简化:
where部分由predicates构建,需要指定表名,where的详细条件可以用predicates的函数写,如equalTo()
query 先传入条件,再传入需要查询的字段,返回的是result结果集
控制指针循环读取(自带指针),最开始是-1。每列也有名字和编号,得到名字对应的编号,然后从编号里取数据,封装到结果数组里返回。
前端页面src/main/ets/pages/TaskManagePage.ets,其他都抽取为组件了,位置在view/task中
import { Header } from '../common/components/CommonComponents'
import TaskList from '../views/task/TaskList'
import TaskStatistics from '../views/task/TaskStatistics'
@Entry
@Component
struct TaskManagePage {
@State totalTask: number = 0
@State finishTask: number = 0
build() {
Column({space: 10}){
// 1.顶部导航
Header()
// 2.任务进度卡片
TaskStatistics({totalTask: this.totalTask, finishTask: this.finishTask})
// 3.任务列表
TaskList({totalTask: $totalTask, finishTask: $finishTask})
}
.width('100%')
.height('100%')
.backgroundColor('#F1F2F3')
}
}