uni-app 开发实战:从入门到精通的一课一得

在移动应用开发的浪潮中,跨平台开发框架成为了开发者们的首选。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>
六、性能优化与用户体验提升

在开发过程中,我们还需要关注性能优化和用户体验提升。以下是一些常见的优化方法:

  1. 图片优化:使用图片懒加载,对图片进行压缩处理
  2. 数据缓存:对不经常变化的数据进行缓存,减少网络请求
  3. 骨架屏:在数据加载过程中显示骨架屏,提升用户体验
  4. 防抖和节流:对搜索等高频操作进行防抖和节流处理
  5. 页面预加载:对可能访问的页面进行预加载,提升页面切换速度
七、总结与展望

通过这篇文章,我们详细介绍了 uni-app 开发中的无数据视图、博客列表视图和分页加载等核心功能的实现方法。这些功能是大多数应用中都会用到的基础功能,掌握它们对于 uni-app 开发者来说非常重要。

当然,这只是 uni-app 开发的一部分内容。在实际项目中,我们还会遇到更多的挑战和问题,需要不断学习和探索。未来,我将继续分享更多关于 uni-app 开发的经验和技巧,希望能够帮助更多的开发者快速掌握这门技术。

希望这篇文章对大家有所帮助,谢谢阅读!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值