在移动应用开发的浪潮中,跨平台开发框架成为了开发者们的首选。uni-app 作为一款优秀的跨平台开发框架,凭借其 "一次开发,多端发布" 的特性,大大提高了开发效率。本文将分享我在使用 uni-app 进行项目开发过程中的一些心得体会和实战经验,特别是关于无数据视图、博客列表视图和分页加载等核心功能的实现。
一、uni-app 简介与环境搭建
uni-app 是 DCloud 推出的基于 Vue.js 的跨平台开发框架,支持发布到 iOS、Android、H5、小程序等多个平台。使用 uni-app 进行开发,开发者只需掌握 Vue.js 和相关前端技术,就能快速构建出多端兼容的应用。
环境搭建是开发的第一步,我们需要安装 HBuilderX 开发工具,它是 uni-app 官方推荐的 IDE,集成了丰富的插件和模板,能极大提升开发效率。安装完成后,我们可以通过 HBuilderX 快速创建 uni-app 项目,并进行编译、运行和调试。
二、无数据视图的设计与实现
在应用开发中,无数据视图是一个常见的场景,比如首次进入页面时数据还未加载完成,或者查询结果为空时,都需要展示无数据视图。一个好的无数据视图不仅能提升用户体验,还能引导用户进行下一步操作。
以下是一个无数据视图的实现示例:
vue
<!-- components/empty-view/index.vue -->
<template>
<view class="empty-view" :style="containerStyle">
<image :src="imageUrl" mode="aspectFit"></image>
<text class="empty-text">{{text}}</text>
<button v-if="showButton" class="empty-btn" @click="onButtonClick">{{buttonText}}</button>
</view>
</template>
<script>
export default {
props: {
// 图片地址
imageUrl: {
type: String,
default: '/static/images/empty.png'
},
// 提示文本
text: {
type: String,
default: '暂无数据'
},
// 是否显示按钮
showButton: {
type: Boolean,
default: false
},
// 按钮文本
buttonText: {
type: String,
default: '刷新'
},
// 容器样式
containerStyle: {
type: String,
default: ''
}
},
methods: {
onButtonClick() {
this.$emit('button-click');
}
}
}
</script>
<style>
.empty-view {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40px 20px;
text-align: center;
}
.empty-view image {
width: 120px;
height: 120px;
}
.empty-text {
margin-top: 20px;
color: #999;
font-size: 16px;
}
.empty-btn {
margin-top: 20px;
padding: 8px 20px;
background-color: #007AFF;
color: #fff;
border-radius: 4px;
font-size: 14px;
}
</style>
这个组件提供了以下功能:
- 可自定义显示的图片、文本和按钮
- 支持自定义容器样式
- 按钮点击事件通过 $emit 触发,方便外部监听
在页面中使用这个组件也非常简单:
vue
<!-- pages/blogs/index.vue -->
<template>
<view class="container">
<!-- 导航栏 -->
<view class="nav-bar">
<text class="title">博客列表</text>
</view>
<!-- 内容区域 -->
<view class="content">
<!-- 加载中 -->
<view v-if="loading" class="loading">
<text>加载中...</text>
</view>
<!-- 无数据 -->
<empty-view
v-else-if="!loading && blogs.length === 0"
text="暂无博客文章"
showButton="true"
buttonText="刷新"
@button-click="refresh"
></empty-view>
<!-- 博客列表 -->
<view v-else class="blogs-list">
<view class="blog-item" v-for="(blog, index) in blogs" :key="index">
<text class="blog-title">{{blog.title}}</text>
<text class="blog-date">{{blog.createTime}}</text>
<text class="blog-content">{{blog.content}}</text>
</view>
</view>
</view>
</view>
</template>
<script>
import EmptyView from '@/components/empty-view/index.vue';
export default {
components: {
EmptyView
},
data() {
return {
loading: true,
blogs: []
}
},
onLoad() {
this.loadBlogs();
},
methods: {
async loadBlogs() {
try {
// 模拟API请求
const res = await this.$api.getBlogList();
this.blogs = res.data;
} catch (error) {
console.error('加载博客失败', error);
uni.showToast({
title: '加载博客失败',
icon: 'none'
});
} finally {
this.loading = false;
}
},
refresh() {
this.loading = true;
this.loadBlogs();
}
}
}
</script>
三、博客列表视图的设计与实现
博客列表是应用中的核心功能之一,其设计直接影响用户体验。在设计博客列表时,我们需要考虑布局的美观性、信息的展示方式以及交互效果。
以下是一个博客列表视图的实现示例:
vue
<!-- components/blog-item/index.vue -->
<template>
<view class="blog-item" @click="goToDetail">
<view class="blog-header">
<image class="avatar" :src="blog.author.avatar" mode="aspectFill"></image>
<view class="author-info">
<text class="author-name">{{blog.author.name}}</text>
<text class="publish-time">{{blog.publishTime}}</text>
</view>
<view class="blog-actions">
<text class="like-count">{{blog.likeCount}}</text>
<image class="like-icon" src="/static/images/like.png"></image>
</view>
</view>
<view class="blog-content">
<text class="blog-title">{{blog.title}}</text>
<text class="blog-summary">{{blog.summary}}</text>
</view>
<view class="blog-footer">
<view class="blog-tags">
<text class="tag" v-for="(tag, index) in blog.tags" :key="index">#{{tag}}</text>
</view>
<view class="read-more">
<text>阅读更多</text>
<image src="/static/images/arrow_right.png"></image>
</view>
</view>
</view>
</template>
<script>
export default {
props: {
blog: {
type: Object,
required: true
}
},
methods: {
goToDetail() {
uni.navigateTo({
url: `/pages/blog/detail?id=${this.blog.id}`
});
}
}
}
</script>
<style>
.blog-item {
padding: 15px;
margin-bottom: 10px;
background-color: #fff;
border-radius: 8px;
}
.blog-header {
display: flex;
align-items: center;
}
.avatar {
width: 40px;
height: 40px;
border-radius: 20px;
}
.author-info {
flex: 1;
margin-left: 10px;
}
.author-name {
font-size: 16px;
font-weight: bold;
}
.publish-time {
font-size: 12px;
color: #999;
}
.blog-actions {
display: flex;
align-items: center;
}
.like-count {
margin-right: 5px;
font-size: 14px;
color: #999;
}
.like-icon {
width: 16px;
height: 16px;
}
.blog-content {
margin-top: 10px;
}
.blog-title {
display: block;
font-size: 18px;
font-weight: bold;
line-height: 1.5;
}
.blog-summary {
display: block;
margin-top: 5px;
font-size: 14px;
color: #666;
line-height: 1.5;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
}
.blog-footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 10px;
}
.blog-tags {
display: flex;
flex-wrap: wrap;
}
.tag {
margin-right: 10px;
font-size: 12px;
color: #007AFF;
}
.read-more {
display: flex;
align-items: center;
color: #007AFF;
font-size: 14px;
}
.read-more image {
width: 12px;
height: 12px;
margin-left: 5px;
}
</style>
这个博客列表组件具有以下特点:
- 采用卡片式布局,视觉效果清晰
- 展示博客的作者信息、发布时间、标题、摘要和标签
- 支持点赞数显示和阅读更多功能
- 点击博客可以跳转到详情页
四、分页加载的实现
当博客数量较多时,一次性加载所有数据会影响性能和用户体验。因此,我们需要实现分页加载功能,将数据分成多个页面进行加载。
以下是分页加载的实现示例:
vue
<!-- pages/blogs/index.vue -->
<template>
<view class="container">
<!-- 导航栏 -->
<view class="nav-bar">
<text class="title">博客列表</text>
</view>
<!-- 搜索框 -->
<view class="search-bar">
<input type="text" v-model="keyword" placeholder="搜索博客" @confirm="searchBlogs" />
<button @click="searchBlogs">搜索</button>
</view>
<!-- 内容区域 -->
<view class="content">
<!-- 加载中 -->
<view v-if="loading && blogs.length === 0" class="loading">
<text>加载中...</text>
</view>
<!-- 无数据 -->
<empty-view
v-else-if="!loading && blogs.length === 0"
text="暂无博客文章"
showButton="true"
buttonText="刷新"
@button-click="refresh"
></empty-view>
<!-- 博客列表 -->
<view v-else class="blogs-list">
<blog-item v-for="(blog, index) in blogs" :key="index" :blog="blog"></blog-item>
<!-- 加载更多提示 -->
<view class="load-more" v-if="hasMore">
<text>加载更多...</text>
</view>
<!-- 没有更多数据 -->
<view class="no-more" v-else>
<text>已经到底了</text>
</view>
</view>
</view>
</view>
</template>
<script>
import EmptyView from '@/components/empty-view/index.vue';
import BlogItem from '@/components/blog-item/index.vue';
export default {
components: {
EmptyView,
BlogItem
},
data() {
return {
loading: true,
blogs: [],
page: 1,
pageSize: 10,
hasMore: true,
keyword: ''
}
},
onLoad() {
this.loadBlogs();
},
onReachBottom() {
// 页面滚动到底部时触发加载更多
if (!this.loading && this.hasMore) {
this.loadMoreBlogs();
}
},
methods: {
async loadBlogs() {
try {
this.loading = true;
const res = await this.$api.getBlogList({
page: this.page,
pageSize: this.pageSize,
keyword: this.keyword
});
this.blogs = res.data.list;
this.hasMore = res.data.list.length === this.pageSize;
} catch (error) {
console.error('加载博客失败', error);
uni.showToast({
title: '加载博客失败',
icon: 'none'
});
} finally {
this.loading = false;
}
},
async loadMoreBlogs() {
try {
this.loading = true;
const res = await this.$api.getBlogList({
page: this.page + 1,
pageSize: this.pageSize,
keyword: this.keyword
});
this.blogs = [...this.blogs, ...res.data.list];
this.hasMore = res.data.list.length === this.pageSize;
if (this.hasMore) {
this.page++;
}
} catch (error) {
console.error('加载更多博客失败', error);
uni.showToast({
title: '加载更多博客失败',
icon: 'none'
});
} finally {
this.loading = false;
}
},
refresh() {
this.page = 1;
this.hasMore = true;
this.loadBlogs();
},
searchBlogs() {
this.page = 1;
this.hasMore = true;
this.loadBlogs();
}
}
}
</script>
<style>
.container {
background-color: #f5f5f5;
}
.nav-bar {
height: 44px;
line-height: 44px;
background-color: #007AFF;
color: #fff;
text-align: center;
}
.title {
font-size: 18px;
font-weight: bold;
}
.search-bar {
display: flex;
padding: 10px;
background-color: #fff;
}
.search-bar input {
flex: 1;
height: 34px;
padding: 0 10px;
background-color: #f5f5f5;
border-radius: 17px;
font-size: 14px;
}
.search-bar button {
width: 60px;
height: 34px;
margin-left: 10px;
background-color: #007AFF;
color: #fff;
border-radius: 17px;
font-size: 14px;
}
.content {
padding: 10px;
}
.loading {
padding: 20px;
text-align: center;
}
.blogs-list {
padding-bottom: 20px;
}
.load-more, .no-more {
padding: 10px;
text-align: center;
color: #999;
font-size: 14px;
}
</style>
分页加载的关键点:
- 使用
onReachBottom
生命周期函数监听页面滚动到底部事件 - 通过
loading
状态控制加载提示 - 使用
hasMore
判断是否还有更多数据 - 加载更多时保持原有数据,并追加新数据
五、其它核心功能实现
1. 博客详情页
博客详情页用于展示博客的完整内容,通常包括标题、作者信息、发布时间、正文内容等。以下是博客详情页的简单实现:
vue
<!-- pages/blog/detail.vue -->
<template>
<view class="container">
<!-- 导航栏 -->
<view class="nav-bar">
<view class="back" @click="goBack">
<image src="/static/images/back.png"></image>
</view>
<text class="title">博客详情</text>
</view>
<!-- 博客内容 -->
<view class="blog-detail">
<text class="blog-title">{{blog.title}}</text>
<view class="blog-meta">
<image class="avatar" :src="blog.author.avatar" mode="aspectFill"></image>
<view class="author-info">
<text class="author-name">{{blog.author.name}}</text>
<text class="publish-time">{{blog.publishTime}}</text>
</view>
<view class="blog-actions">
<view class="action-item">
<image src="/static/images/like.png"></image>
<text>{{blog.likeCount}}</text>
</view>
<view class="action-item">
<image src="/static/images/comment.png"></image>
<text>{{blog.commentCount}}</text>
</view>
</view>
</view>
<view class="blog-content" v-html="blog.content"></view>
<!-- 评论区 -->
<view class="comments-section">
<text class="section-title">评论 ({{comments.length}})</text>
<view class="comment-input">
<input type="text" v-model="commentContent" placeholder="发表评论..." />
<button @click="postComment">发布</button>
</view>
<view class="comments-list">
<view class="comment-item" v-for="(comment, index) in comments" :key="index">
<image class="avatar" :src="comment.user.avatar" mode="aspectFill"></image>
<view class="comment-content">
<text class="user-name">{{comment.user.name}}</text>
<text class="comment-text">{{comment.content}}</text>
<text class="comment-time">{{comment.createTime}}</text>
</view>
</view>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
blog: {},
comments: [],
commentContent: ''
}
},
onLoad(options) {
this.blogId = options.id;
this.loadBlogDetail();
this.loadComments();
},
methods: {
goBack() {
uni.navigateBack();
},
async loadBlogDetail() {
try {
const res = await this.$api.getBlogDetail(this.blogId);
this.blog = res.data;
} catch (error) {
console.error('加载博客详情失败', error);
uni.showToast({
title: '加载博客详情失败',
icon: 'none'
});
}
},
async loadComments() {
try {
const res = await this.$api.getBlogComments(this.blogId);
this.comments = res.data;
} catch (error) {
console.error('加载评论失败', error);
uni.showToast({
title: '加载评论失败',
icon: 'none'
});
}
},
async postComment() {
if (!this.commentContent.trim()) {
uni.showToast({
title: '评论内容不能为空',
icon: 'none'
});
return;
}
try {
await this.$api.postComment({
blogId: this.blogId,
content: this.commentContent
});
uni.showToast({
title: '评论成功',
icon: 'success'
});
this.commentContent = '';
this.loadComments();
} catch (error) {
console.error('发表评论失败', error);
uni.showToast({
title: '发表评论失败',
icon: 'none'
});
}
}
}
}
</script>
2. 分类与标签功能
分类与标签是博客系统中常用的内容组织方式,可以帮助用户快速找到感兴趣的内容。实现分类与标签功能的关键是设计合适的数据结构和路由。
javascript
// api/blog.js
export default {
// 获取博客分类列表
getCategories() {
return uni.request({
url: '/api/blog/categories',
method: 'GET'
});
},
// 根据分类获取博客列表
getBlogsByCategory(categoryId, page = 1, pageSize = 10) {
return uni.request({
url: `/api/blog/category/${categoryId}`,
method: 'GET',
data: {
page,
pageSize
}
});
},
// 根据标签获取博客列表
getBlogsByTag(tag, page = 1, pageSize = 10) {
return uni.request({
url: `/api/blog/tag/${tag}`,
method: 'GET',
data: {
page,
pageSize
}
});
}
}
3. 搜索功能
搜索功能可以帮助用户快速找到所需的博客内容。实现搜索功能的关键是处理搜索关键词和展示搜索结果。
vue
<!-- components/search-result/index.vue -->
<template>
<view class="search-result">
<view class="search-header">
<text class="result-count">搜索结果: {{totalCount}} 条</text>
<text class="keyword">关键词: <span>{{keyword}}</span></text>
</view>
<view class="result-list">
<view class="result-item" v-for="(result, index) in results" :key="index" @click="goToDetail(result.id)">
<text class="result-title" v-html="highlightKeyword(result.title)"></text>
<text class="result-content" v-html="highlightKeyword(result.summary)"></text>
<text class="result-time">{{result.publishTime}}</text>
</view>
</view>
<!-- 加载更多 -->
<view class="load-more" v-if="hasMore && !loading">
<button @click="loadMore">加载更多</button>
</view>
<!-- 加载中 -->
<view class="loading" v-if="loading">
<text>加载中...</text>
</view>
<!-- 无结果 -->
<empty-view v-else-if="results.length === 0" text="没有找到相关结果"></empty-view>
</view>
</template>
<script>
import EmptyView from '@/components/empty-view/index.vue';
export default {
components: {
EmptyView
},
props: {
keyword: {
type: String,
required: true
}
},
data() {
return {
results: [],
totalCount: 0,
page: 1,
pageSize: 10,
hasMore: true,
loading: false
}
},
created() {
this.search();
},
methods: {
async search() {
if (!this.keyword.trim()) return;
try {
this.loading = true;
const res = await this.$api.searchBlogs({
keyword: this.keyword,
page: this.page,
pageSize: this.pageSize
});
this.results = res.data.list;
this.totalCount = res.data.total;
this.hasMore = res.data.list.length === this.pageSize;
} catch (error) {
console.error('搜索失败', error);
uni.showToast({
title: '搜索失败',
icon: 'none'
});
} finally {
this.loading = false;
}
},
async loadMore() {
if (this.loading || !this.hasMore) return;
try {
this.loading = true;
const res = await this.$api.searchBlogs({
keyword: this.keyword,
page: this.page + 1,
pageSize: this.pageSize
});
this.results = [...this.results, ...res.data.list];
this.hasMore = res.data.list.length === this.pageSize;
if (this.hasMore) {
this.page++;
}
} catch (error) {
console.error('加载更多失败', error);
uni.showToast({
title: '加载更多失败',
icon: 'none'
});
} finally {
this.loading = false;
}
},
highlightKeyword(text) {
if (!text || !this.keyword) return text;
const regex = new RegExp(this.keyword, 'gi');
return text.replace(regex, match => `<span class="highlight">${match}</span>`);
},
goToDetail(id) {
uni.navigateTo({
url: `/pages/blog/detail?id=${id}`
});
}
}
}
</script>
<style>
.search-result {
padding: 15px;
}
.search-header {
margin-bottom: 15px;
}
.result-count {
font-size: 16px;
font-weight: bold;
}
.keyword {
display: block;
margin-top: 5px;
color: #666;
}
.keyword span {
color: #ff4500;
}
.result-list {
margin-bottom: 15px;
}
.result-item {
margin-bottom: 15px;
padding-bottom: 15px;
border-bottom: 1px solid #eee;
}
.result-title {
display: block;
font-size: 18px;
font-weight: bold;
color: #000;
margin-bottom: 5px;
}
.result-content {
display: block;
font-size: 14px;
color: #666;
line-height: 1.5;
margin-bottom: 5px;
}
.result-time {
font-size: 12px;
color: #999;
}
.highlight {
color: #ff4500;
font-weight: bold;
}
.load-more {
text-align: center;
margin: 15px 0;
}
.load-more button {
padding: 8px 20px;
background-color: #007AFF;
color: #fff;
border-radius: 4px;
}
.loading {
text-align: center;
padding: 15px 0;
}
</style>
六、性能优化与用户体验提升
在开发过程中,我们还需要关注性能优化和用户体验提升。以下是一些常见的优化方法:
- 图片优化:使用图片懒加载,对图片进行压缩处理
- 数据缓存:对不经常变化的数据进行缓存,减少网络请求
- 骨架屏:在数据加载过程中显示骨架屏,提升用户体验
- 防抖和节流:对搜索等高频操作进行防抖和节流处理
- 页面预加载:对可能访问的页面进行预加载,提升页面切换速度
七、总结与展望
通过这篇文章,我们详细介绍了 uni-app 开发中的无数据视图、博客列表视图和分页加载等核心功能的实现方法。这些功能是大多数应用中都会用到的基础功能,掌握它们对于 uni-app 开发者来说非常重要。
当然,这只是 uni-app 开发的一部分内容。在实际项目中,我们还会遇到更多的挑战和问题,需要不断学习和探索。未来,我将继续分享更多关于 uni-app 开发的经验和技巧,希望能够帮助更多的开发者快速掌握这门技术。
希望这篇文章对大家有所帮助,谢谢阅读!