项目简介
我的书架是一个基于鸿蒙开发的图书管理应用,用户可以通过该应用实现图书的添加、删除、借阅、归还等功能。
开源地址:我的书架
本项目旨在练习鸿蒙开发中的网络请求模块,实现对后端数据的增删改查,同时熟悉V2版本的状态管理。
功能实现
本项目主要实现的功能有以下几项:
- 获取图书
- 新增图书
- 删除图书
- 全部删除
- 修改图书
获取图书
开启网络权限
在module.json5
中开启网络请求权限
1 2 3 4 5
| "requestPermissions": [ { "name": "ohos.permission.INTERNET" } ],
|
随后创建网络请求对象
1
| req: http.HttpRequest = http.createHttp()
|
用V2版本的状态管理搭建UI
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122
| import { router } from '@kit.ArkUI' import { http } from '@kit.NetworkKit'
export interface Book { id: number bookName: string author: string publisher: string }
export const creator: string = 'XBXyftx'
@Entry @ComponentV2 struct Day02_01_BookShelf { creator: string = creator @Local books: Book[] = [{ id: 366351, bookName: "《西游记》", author: "吴承恩", publisher: "人民文学出版社" }] @Local isLoading: boolean = true req: http.HttpRequest = http.createHttp()
build() { Column() { this.HeaderBuilder() LoadingProgress() .width(50)
List({ space: 15 }) { ForEach(this.books, (item: Book) => { ListItem() { bookItem({ data: item }) } .swipeAction({ end: () => { this.itemEnd(item) }, edgeEffect: SwipeEdgeEffect.Spring }) .onClick(() => {
}) }) } .padding(20) } .height('100%') .width('100%') }
@Builder HeaderBuilder() { Row() { Image($r('app.media.ic_public_drawer_filled')) .width(20);
Text('我的书架') .fontSize(25)
Image($r('app.media.ic_public_add')) .width(20) .onClick(() => { router.pushUrl({ url: 'pages/Day02_01_BookShelf_Add' }) }) } .width('100%') .justifyContent(FlexAlign.SpaceBetween) .height(60) .padding(10) .border({ width: { bottom: 2 }, color: '#f0f0f0' }) .backgroundColor(Color.White) }
@Builder itemEnd(item: Book) { Row() { Button('删除') .type(ButtonType.Normal) .backgroundColor('#da3231') .onClick(() => { AlertDialog.show({ message: '点了删除' }) }) .height('100%') }
} }
@ComponentV2 struct bookItem { @Param data: Partial<Book> = {}
build() { Row({ space: 10 }) { Image($r('app.media.ic_public_cover')) .width(108) .height(108) Column({ space: 5 }) {
Text('书名:' + this.data.bookName) .fontSize(20) Text('作者:' + this.data.author) .fontSize(14) .fontColor(Color.Gray) Blank() Text('出版社: ' + this.data.publisher) .fontSize(14) .fontColor(Color.Gray) } .padding({ top: 10, bottom: 10 }) .height(108) .alignItems(HorizontalAlign.Start) } } }
|

获取数据格式定义接口
1 2 3 4 5 6
| aboutToAppear(): void { this.req.request(`https://hmajax.itheima.net/api/books?creator=${this.creator}`) .then((res)=>{ AlertDialog.show({message:res.result.toString()}) }) }
|

由此可以定义接口
1 2 3 4 5 6 7 8 9 10
| export interface Book { id: number bookName: string author: string publisher: string } export interface BookRes{ message:string data:Book[] }
|
于是针对数据结构进行相应的数据处理如下
1 2 3 4 5 6 7
| @Local books: Book[] = [] aboutToAppear(): void { this.req.request(`https://hmajax.itheima.net/api/books?creator=${this.creator}`) .then((res)=>{ this.books = (JSON.parse(res.result.toString()) as BookRes).data }) }
|
显示效果如下

加载效果开关
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| @Local isLoading: boolean = true aboutToAppear(): void { this.isLoading = true this.req.request(`https://hmajax.itheima.net/api/books?creator=${this.creator}`) .then((res)=>{ this.books = (JSON.parse(res.result.toString()) as BookRes).data this.isLoading = false }) .catch((err:string)=>{ AlertDialog.show({ message:err }) this.isLoading = false }) }
if (this.isLoading){ LoadingProgress() .width(50) }
|
新增图书
跳转页面
首先从主页跳转至新增页面
1 2 3 4 5 6 7
| Image($r('app.media.ic_public_add')) .width(20) .onClick(() => { router.pushUrl({ url: 'pages/AddBookPage' }) })
|
新增图书页面逻辑分析
UI框架如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67
| @Entry @ComponentV2 struct BookShelf_Add { @Local bookname: string = '' @Local author: string = '' @Local publisher: string = ''
build() { Navigation() { Column({ space: 10 }) { Row() { Text('图书姓名:') TextInput({ placeholder: '请输入图书名字', text: $$this.bookname }) .height(30) .backgroundColor(Color.Transparent) .layoutWeight(1) .padding({ left: 10, top: 0, bottom: 0 }) }
Divider() Row() { Text('图书作者:') TextInput({ placeholder: '请输入图书作者', text: $$this.author }) .height(30) .backgroundColor(Color.Transparent) .layoutWeight(1) .padding({ left: 10, top: 0, bottom: 0 }) }
Divider() Row() { Text('图书出版社:') TextInput({ placeholder: '请输入图书出版社', text: $$this.publisher }) .height(30) .backgroundColor(Color.Transparent) .layoutWeight(1) .padding({ left: 10, top: 0, bottom: 0 }) }
Divider()
Button('保存') .width('100%') .margin({ top: 20 }) .type(ButtonType.Normal) .borderRadius(10) .onClick(() => { })
} .padding(20) } .title('新增图书') .titleMode(NavigationTitleMode.Mini) .backButtonIcon($r('app.media.ic_public_arrow_left')) } }
|

在点击保存按钮之后,首先要去判断输入框是否为空,为空则提示用户输入,不为空则将数据发送至后端。
1 2 3 4 5 6 7 8
| .onClick(() => { if (this.bookname.trim() === '' || this.author.trim() === '' || this.publisher.trim() === '') { AlertDialog.show({ message:'新建图书信息均不能为空' }) return } })
|
在不为空的情况下就可去发送请求添加书籍,并返回数据。
请求方法为Post,数据格式为json。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| .onClick(() => { if (this.bookname.trim() === '' || this.author.trim() === '' || this.publisher.trim() === '') { AlertDialog.show({ message:'新建图书信息均不能为空' }) return } const req = http.createHttp() req.request('https://hmajax.itheima.net/api/books',{ method: http.RequestMethod.POST, header:{ contentType:'application/json' }, extraData:{ bookname:this.bookname, author:this.author, publisher:this.publisher, creator:creator } }) .then((res)=>{ const addRes = JSON.parse(res.result.toString()) as AddResponse router.back() AlertDialog.show({ message:addRes.message }) }) })
|
请求是成功的但是首页的图书列表并没有更新,需要刷新页面。
这是因为aboutToAppear
函数只在页面第一次出现时调用,而不是每次页面重新出现时调用。
所以换位onPageShow
函数即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| onPageShow(): void { this.isLoading = true this.req.request(`https://hmajax.itheima.net/api/books?creator=${this.creator}`) .then((res)=>{ this.books = (JSON.parse(res.result.toString()) as BookRes).data this.isLoading = false }) .catch((err:string)=>{ AlertDialog.show({ message:err }) this.isLoading = false }) }
|
删除图书
删除图书操作通过点击ListItem组件的左划删除按钮来触发。
但若点击删除事件直接触发删除函数的话这并不合理,因为用户很可能误触。
所以在点击删除按钮时先弹出一个确认框,确认用户是否要删除。
确认弹窗文档
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| Button('删除') .type(ButtonType.Normal) .backgroundColor('#da3231') .onClick(() => { promptAction.showDialog({ title: '删除确认', message: `您是否确认删除${item.bookname}`, buttons: [ { text: '确认', color: '#000000' }, { text: '取消', color: '#000000' } ], }) .then(data => { console.info('showDialog success, click button: ' + data.index); }) })
|
通过在日志筛选关键字showDialog success, click button:
来获取用户点击的按钮index
。

由此可知data.index
的索引值与buttons
对象数组中的索引值一致。
所以当用户点击确认时就可以去发送请求删除图书。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| Button('删除') .type(ButtonType.Normal) .backgroundColor('#da3231') .onClick(() => { promptAction.showDialog({ title: '删除确认', message: `您是否确认删除${item.bookname}`, buttons: [ { text: '确认', color: '#000000' }, { text: '取消', color: '#000000' } ], }) .then(data => { console.info('showDialog success, click button: ' + data.index); if (data.index === 0) { this.req.request(`https://hmajax.itheima.net/api/books/${item.id}`,{ method:http.RequestMethod.DELETE }) .then((rep)=>{ promptAction.showToast({ message:'删除成功' }) this.reloadBooks() }) } }) })
|
这样就完成了在提示用户确认删除后再删除对应图书的功能。
全部删除
全部删除功能是在删除图书功能的基础上实现的。
由于后端并没有给出删除全部图书的接口,所以需要在前端用删除图书的功能来模拟。
而UI界面上并没有预留全部删除按钮,所以我选择添加一个侧边栏。
- 首先重构整个页面结构,将最外层组件替换为
SideBarContainer
- 然后将侧边栏和主页面的默认内容重构为两个
Builder
。这一步主要为了方便管理代码,提高可读性可省略。
- 在侧边栏中添加一个删除全部的按钮。
- 在点击删除全部按钮时弹出确认框,确认用户是否要删除全部图书。
- 若用户确认删除,则发送请求删除全部图书。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252
| import { promptAction, router } from '@kit.ArkUI' import { http } from '@kit.NetworkKit'
export interface Book { id: number bookname: string author: string publisher: string }
export interface BookRes { message: string data: Book[] }
export const creator: string = 'XBXyftx'
@Entry @ComponentV2 struct Day02_01_BookShelf { creator: string = creator @Local books: Book[] = [] @Local isLoading: boolean = true @Local sideBarShow: boolean = false req: http.HttpRequest = http.createHttp()
onPageShow(): void { this.reloadBooks() }
private reloadBooks() { this.isLoading = true this.req.request(`https://hmajax.itheima.net/api/books?creator=${encodeURIComponent(this.creator)}`) .then((res) => { this.books = (JSON.parse(res.result.toString()) as BookRes).data this.isLoading = false }) .catch((err: string) => { AlertDialog.show({ message: err }) this.isLoading = false }) }
build() {
SideBarContainer(SideBarContainerType.Overlay) { this.SidebarContent_IndexPage()
this.MainPartOf_IndexPage() } .showControlButton(false) .showSideBar($$this.sideBarShow) .maxSideBarWidth('75%') .minSideBarWidth('50%')
}
@Builder SidebarContent_IndexPage() { Column({ space: 20 }) { Image($rawfile('删除全部.svg')) .width(50) .onClick(() => { this.deleteAllBooks() }) Text('删除全部') .fontSize(20) .fontWeight(500) } .justifyContent(FlexAlign.Center) .backgroundColor(Color.White) .height('100%') .width('100%') }
private deleteAllBooks() { const BooksIds: number[] = this.books.map(item => item.id) promptAction.showDialog({ title: '删除确认', message: `您是否确认删除全部书籍`, buttons: [ { text: '确认', color: '#000000' }, { text: '取消', color: '#000000' } ], }) .then(data => { console.info('showDialog success, click button: ' + data.index) if (data.index === 0) { for (let i = 0; i < BooksIds.length; i++) { this.req.request(`https://hmajax.itheima.net/api/books/${BooksIds[i]}`, { method: http.RequestMethod.DELETE }) .then((rep) => { promptAction.showToast({ message: (JSON.parse(rep.result.toString()) as BookRes).message }) this.reloadBooks() }) } } }) }
@Builder MainPartOf_IndexPage() { Column() { this.HeaderBuilder()
if (this.isLoading) { LoadingProgress() .width(50) }
List({ space: 15 }) { ForEach(this.books, (item: Book) => { ListItem() { bookItem({ data: item }) } .width('100%') .swipeAction({ end: () => { this.itemEnd(item) }, edgeEffect: SwipeEdgeEffect.Spring }) .onClick(() => { }) }) } .padding(20) } .height('100%') .width('100%') }
@Builder HeaderBuilder() { Row() { Image($r('app.media.ic_public_drawer_filled')) .width(20) .onClick(() => { animateTo({ duration: 500, curve: Curve.Ease }, () => { this.sideBarShow = true }) })
Text('我的书架') .fontSize(25)
Image($r('app.media.ic_public_add')) .width(20) .onClick(() => { router.pushUrl({ url: 'pages/AddBookPage' }) }) } .width('100%') .justifyContent(FlexAlign.SpaceBetween) .height(60) .padding(10) .border({ width: { bottom: 2 }, color: '#f0f0f0' }) .backgroundColor(Color.White) }
@Builder itemEnd(item: Book) { Row() { Button('删除') .type(ButtonType.Normal) .backgroundColor('#da3231') .onClick(() => { this.deleteOneBook(item) }) .height('100%') }
}
private deleteOneBook(item: Book) { promptAction.showDialog({ title: '删除确认', message: `您是否确认删除${item.bookname}`, buttons: [ { text: '确认', color: '#000000' }, { text: '取消', color: '#000000' } ], }) .then(data => { console.info('showDialog success, click button: ' + data.index) if (data.index === 0) { this.req.request(`https://hmajax.itheima.net/api/books/${item.id}`, { method: http.RequestMethod.DELETE }) .then((rep) => { promptAction.showToast({ message: '删除成功' }) this.reloadBooks() }) } }) } }
@ComponentV2 struct bookItem { @Param data: Partial<Book> = {}
build() { Row({ space: 10 }) { Image($r('app.media.ic_public_cover')) .width(108) .height(108) Column({ space: 5 }) {
Text('书名:' + this.data.bookname) .fontSize(20) Text('作者:' + this.data.author) .fontSize(14) .fontColor(Color.Gray) Blank() Text('出版社: ' + this.data.publisher) .fontSize(14) .fontColor(Color.Gray) } .padding({ top: 10, bottom: 10 }) .height(108) .alignItems(HorizontalAlign.Start) } .width('100%') } }
|
至此我们实现了删除全部的功能。
修改图书
修改图书的API接口为PUT
方法。同时拥有路径参数,也有请求体参数。
路径参数为id
,请求体参数为bookname
、author
、publisher
、creator
。
实现思路:
- 点击图书列表中的图书后携带图书ID跳转到修改页面。
- 根据ID发送请求获取图书详情。
- 在修改页面中显示图书详情。
- 在修改页面中修改图书详情。
- 点击保存按钮发送请求修改图书。
- 返回图书列表页面。
获取图书Id可以在aboutToAppear
函数中获取。
1 2 3 4
| bookId:number = 0 aboutToAppear(): void { this.bookId = router.getParams() as number }
|
在获取到ID后就需要发送请求获取图书详情。所以还需再aboutToAppear
函数中发送请求。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| aboutToAppear(): void { this.bookId = (router.getParams() as BookInfo).id
const req = http.createHttp() req.request(`https://hmajax.itheima.net/api/books/${this.bookId}`) .then(res => { const bookRes = JSON.parse(res.result.toString()) as GetBookResponse promptAction.showToast({ message: bookRes.message }) this.bookname = bookRes.data.bookname this.author = bookRes.data.author this.publisher = bookRes.data.publisher }) }
|
随后在保存按钮的onClick
函数中发送请求修改图书。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| Button('保存') .width('100%') .margin({ top: 20 }) .type(ButtonType.Normal) .borderRadius(10) .onClick(() => { const req = http.createHttp() req.request(`https://hmajax.itheima.net/api/books/${this.bookId}`,{ method:http.RequestMethod.PUT, header:{ ContentType:"application/json" }, extraData:{ bookname:this.bookname, author:this.author, publisher:this.publisher, creator:creator } }).then((req)=>{ promptAction.showToast({ message:(JSON.parse(req.result.toString()) as BookChangeRep).message }) router.back() }) })
|
这里要注意的是extraData
中的数据格式为json
。所以需要在请求头中添加ContentType
。
至此我们就完成了修改图书的功能。
总结
好啦,这个项目就到此为止了,在春节期间开始,中间拖拖拉拉到了14号才完成。
在技术上主要用到了以下几点:
- 网络请求:用鸿蒙的网络请求模块来进行增删改查图书信息。
- 路由:用鸿蒙的路由模块来进行页面跳转。
- 动画:用鸿蒙的动画模块来实现侧边栏的展开和收起。
- 弹窗:用鸿蒙的弹窗模块来实现删除确认框和提示框。
当然还有其他一些零碎的点就不一一列举了。
当然这个项目也算是打破了我之前一直不敢迈出的一步,也就是网络请求这一方面,在过去的一些活动中也尝试用过Java,Python,JS等语言的网络请求,我以前我总是认为要学的太多了,如果出bug了我就很难找到原因,它的Debug并不像一段算法代码或是UI一样,它还涉及到硬件、网络、后端等方面的问题。
但是在这次项目中我发现了鸿蒙的网络请求模块的强大之处,它的API非常简单,而且还支持异步请求,这使得我可以在不影响主线程的情况下进行网络请求,这是我之前一直不敢尝试的。
我也是得再次自嘲一下,以前的我也干过不少鼓励其他人走出舒适区的事,但真到自己身上时却又开始犯怵了,笑死了。
行了就先说这么多吧,下次见。
