UniApp 博客项目实战:从零到一搭建完整移动端博客应用【全流程详解】

UniApp 博客项目实战:从零到一搭建完整移动端博客应用【全流程详解】
摘要本文基于 UniApp 框架完整拆解 Blogs 博客移动端项目的开发全流程涵盖项目初始化、静态页面搭建、网络请求封装、图标资源引入、搜索功能实现、缓存优化、路由跳转、登录注册、个人中心、数据可视化、博客发布等核心模块。文章兼顾原理讲解与代码实现语言通俗易懂步骤清晰可复现适合前端入门开发者作为 UniApp 实战练手项目。关键词UniApp博客项目网络请求封装移动端开发前端实战1. 项目整体介绍本项目是一个适配多端的移动端博客应用基于 Vue.js UniApp 开发可编译为微信小程序、H5、App 等多端产物。项目包含博客浏览、搜索、详情查看、评论、用户登录注册、个人中心、数据统计、博客发布等完整功能覆盖移动端开发的绝大多数核心场景。技术栈核心框架UniApp Vue 2.x开发工具HBuilderX网络请求uni.request 二次封装图标资源Iconfont 阿里图标库数据可视化uCharts数据存储UniApp 本地缓存 Storage2. 项目开发全流程2.1 创建和初始化项目我们使用官方推荐的 HBuilderX 工具创建项目步骤清晰无门槛打开 HBuilderX点击「文件 → 新建 → 项目」选择「uni-app」项目类型模板选择「默认模板」项目名称填写blogs选择本地保存路径点击「创建」项目初始目录结构说明blogs ├── pages # 页面文件夹所有业务页面都放在这里 ├── static # 静态资源存放图片、字体、图标等文件 ├── App.vue # 全局配置文件编写全局样式、应用生命周期 ├── main.js # 项目入口文件挂载Vue实例、注册全局组件 ├── manifest.json # 应用配置文件设置appid、权限、各平台专属配置 └── pages.json # 页面路由配置管理页面路径、导航栏、底部Tabbar创建完成后点击工具栏「运行 → 运行到浏览器 → Chrome」能正常打开默认首页即说明项目初始化成功。2.2 Vue.js 简介及无数据视图在对接接口数据前我们先完成静态页面的搭建也就是「无数据视图」。这里先梳理本项目用到的 Vue.js 核心特性数据驱动视图通过修改 data 中的数据页面会自动更新无需手动操作 DOM组件化开发可将重复的 UI 模块封装成组件实现多处复用、统一维护指令系统通过v-for、v-if、v-bind、v-on等指令快速实现页面逻辑2.2.1 splash 页面Splash 页即启动欢迎页是用户打开应用时的全屏过渡页用于展示品牌标识、做初始化预加载。在pages文件夹下新建splash文件夹创建splash.vue文件在pages.json中将该页面放在 pages 数组第一位设为启动首页页面实现代码template view classsplash-container image classlogo src/static/logo.png modeaspectFit/image text classapp-nameBlogs 博客/text /view /template script export default { onLoad() { // 延时1.5秒后跳转到博客首页 setTimeout(() { uni.switchTab({ url: /pages/blogs/blogs }) }, 1500) } } /script style scoped .splash-container { width: 100vw; height: 100vh; display: flex; flex-direction: column; align-items: center; justify-content: center; background-color: #ffffff; } .logo { width: 120rpx; height: 120rpx; margin-bottom: 30rpx; } .app-name { font-size: 36rpx; font-weight: bold; color: #333; } /style2.2.2 无数据视图无数据视图也叫空状态页在网络请求未返回、列表无数据时展示避免页面空白降低用户体验。我们先在博客列表页实现基础空状态template view classblogs-page !-- 有数据时展示列表 -- view v-ifblogList.length 0 classblog-list !-- 列表项后续接入数据后开发 -- /view !-- 无数据空状态 -- view v-else classempty-box text classempty-icon/text text classempty-text暂无博客内容/text /view /view /template script export default { data() { return { blogList: [] // 初始为空数组模拟无数据状态 } } } /script style scoped .empty-box { padding-top: 200rpx; display: flex; flex-direction: column; align-items: center; color: #999; } .empty-icon { font-size: 80rpx; margin-bottom: 20rpx; } .empty-text { font-size: 28rpx; } /style2.3 blogs 列表视图显示及分页加载完成静态页面后我们接入真实接口数据实现博客列表渲染和上拉加载更多的分页功能。2.3.1 uni.request 网络请求封装原生uni.request每次调用都需要编写完整 URL、重复处理加载状态与错误提示开发效率低且维护成本高。我们对其进行二次封装统一管理请求配置。1.在项目根目录新建utils文件夹创建request.js文件2.封装代码如下// 接口基础地址开发环境和生产环境可区分配置 const BASE_URL https://你的接口域名/api // 封装请求方法返回Promise对象便于链式调用 export const request (options) { return new Promise((resolve, reject) { // 请求前显示加载框 uni.showLoading({ title: 加载中..., mask: true }) uni.request({ url: BASE_URL options.url, method: options.method || GET, data: options.data || {}, header: { Content-Type: application/json, // 从缓存读取token携带用户身份凭证 Authorization: uni.getStorageSync(token) || }, success: (res) { uni.hideLoading() // 统一处理业务状态码 if (res.data.code 200) { resolve(res.data.data) } else { uni.showToast({ title: res.data.msg || 请求失败, icon: none }) reject(res.data) } }, fail: (err) { uni.hideLoading() uni.showToast({ title: 网络异常请稍后重试, icon: none }) reject(err) } }) }) }3.新建api/blog.js统一管理博客相关接口实现接口与页面解耦import { request } from /utils/request.js // 获取博客列表 export const getBlogList (params) { return request({ url: /blog/list, method: GET, data: params }) }2.3.2 CommonJS 及 ES6 模块化规范上面的封装用到了模块化语法这里区分前端两种主流的模块化规范避免开发中混淆CommonJS 规范核心语法使用require()导入模块module.exports导出模块适用场景Node.js 环境、早期前端工程化项目代码示例// 导出 module.exports { request } // 导入 const { request } require(../../utils/request.js)ES6 模块化规范核心语法使用import导入模块export/export default导出模块适用场景现代前端项目Vue、React 等框架默认支持代码示例即上文request.js的写法UniApp 项目推荐使用 ES6 模块化规范语法更灵活且与 Vue 生态完美兼容。列表分页加载实现在博客列表页接入接口实现下拉刷新、上拉加载更多的完整分页逻辑script import { getBlogList } from /api/blog.js export default { data() { return { blogList: [], page: 1, pageSize: 10, hasMore: true // 标记是否还有下一页数据 } }, onLoad() { this.getList() }, // 下拉刷新生命周期 onPullDownRefresh() { this.page 1 this.hasMore true this.blogList [] this.getList(() { uni.stopPullDownRefresh() }) }, // 上拉触底生命周期 onReachBottom() { if (!this.hasMore) return this.page this.getList() }, methods: { getList(callback) { getBlogList({ page: this.page, pageSize: this.pageSize }).then(res { // 拼接新数据到列表末尾 this.blogList [...this.blogList, ...res.records] // 返回数据小于每页条数说明没有下一页 if (res.records.length this.pageSize) { this.hasMore false } callback callback() }).catch(() { callback callback() }) } } } /script2.4 iconfont 字体图标项目中会用到大量图标使用 Iconfont 字体图标比位图图片更节省体积、支持任意缩放不失真。接入步骤如下1.打开「阿里图标库」官网搜索需要的图标搜索、用户、发布、返回等加入自己的图标项目2.选择「Font class」使用方式生成在线 CSS 链接3.在项目App.vue的全局 style 中引入链接import url(https://at.alicdn.com/t/c/font_xxxxxx_xxxxxx.css);4.页面中直接通过类名使用图标text classiconfont icon-search/text text classiconfont icon-user/text如果需要离线使用可以将字体文件下载到本地static/font文件夹修改 CSS 中的字体路径后本地引入即可。2.5 uni 组件使用及 blogs 搜索功能UniApp 提供了丰富的内置组件我们可以基于原生组件快速实现博客搜索功能。搜索功能实现使用原生 input 组件实现搜索框监听输入内容调用搜索接口过滤列表数据template view classblogs-page !-- 搜索框 -- view classsearch-box input classsearch-input placeholder搜索博客内容 v-modelkeyword confirm-typesearch confirmhandleSearch / /view !-- 博客列表 -- view v-ifblogList.length 0 classblog-list view classblog-item v-foritem in blogList :keyitem.id text classtitle{{ item.title }}/text text classdesc{{ item.description }}/text /view /view !-- 无结果空状态 -- view v-else classempty-box text classempty-text暂无相关博客/text /view /view /template script import { getBlogList } from /api/blog.js export default { data() { return { keyword: , blogList: [], page: 1, pageSize: 10 } }, methods: { handleSearch() { this.page 1 this.blogList [] this.getList() }, getList() { getBlogList({ page: this.page, pageSize: this.pageSize, keyword: this.keyword }).then(res { this.blogList [...this.blogList, ...res.records] }) } } } /script2.5.1 项目检查过程开发到当前阶段建议做一次全面的项目自查避免后续功能堆积导致问题难定位页面跳转检查启动页能否正常跳转到首页页面切换是否流畅无卡顿样式适配检查切换不同尺寸的模拟器查看是否有样式错乱、内容溢出网络请求检查控制台查看接口是否正常返回、参数是否正确、错误提示是否正常触发边界场景检查搜索空关键词、搜索无结果、分页到底部等异常场景是否正常处理代码报错检查查看控制台是否有红色报错及时修复语法错误、变量未定义等基础问题2.6 使用缓存提升用户体验移动端网络环境不稳定合理使用本地缓存可以大幅减少白屏等待时间提升用户体验。缓存的常见使用场景列表数据缓存进入页面先读取缓存数据渲染同时请求最新数据请求成功后更新页面与缓存用户信息缓存登录后保存用户信息与 token避免每次打开应用都需要重新登录搜索历史缓存保存用户的搜索记录下次进入页面直接展示历史关键词列表缓存实现示例methods: { getList() { // 1. 先读取本地缓存快速渲染页面 const cacheData uni.getStorageSync(blog_list_cache) if (cacheData) { this.blogList cacheData } // 2. 请求最新接口数据 getBlogList({ page: 1, pageSize: 10 }).then(res { this.blogList res.records // 3. 更新本地缓存 uni.setStorageSync(blog_list_cache, res.records) }) } }2.6.1 移动端请求类型移动端开发中常见的 HTTP 请求类型与使用场景如下请求方法核心用途是否适合缓存GET获取数据博客列表、详情、用户信息查询✅ 适合做本地缓存POST提交数据登录、注册、发布博客❌ 不建议缓存PUT修改数据编辑博客、更新用户信息❌ 不建议缓存DELETE删除数据删除博客、删除评论❌ 不建议缓存我们的缓存策略仅针对 GET 查询类请求对于数据提交类请求必须实时与后端交互不能使用本地缓存。2.7 详细信息页面 uni 路由点击博客列表项进入详情页这里涉及到 UniApp 的路由跳转能力。路由跳转基础UniApp 常用的路由 API 有三种核心区别如下uni.navigateTo保留当前页面跳转到新页面左上角自带返回按钮适合详情页、二级页面uni.redirectTo关闭当前页面跳转到新页面无返回按钮适合登录页、结果页uni.switchTab跳转到 TabBar 底部导航页面只能用于底部标签页之间的切换详情页实现1.新建pages/blogDetail/blogDetail.vue在pages.json中配置页面路径2.列表页绑定点击事件跳转时携带博客 IDgoDetail(id) { uni.navigateTo({ url: /pages/blogDetail/blogDetail?id${id} }) }3.详情页接收参数请求详情数据并渲染export default { data() { return { blogId: , detail: {} } }, onLoad(options) { this.blogId options.id this.getDetail() }, methods: { getDetail() { request({ url: /blog/detail/${this.blogId}, method: GET }).then(res { this.detail res }) } } }2.7.1 实现页面通知的两种方式页面间传递信息、通知状态更新最常用的有两种实现方式覆盖不同场景方式一路由传参正向传值即上面详情页的实现方式通过 URL 拼接参数传递数据。适合从上级页面向下级页面传递数据语法简单直接是最常用的传值方式。方式二全局事件总线跨页面 / 反向传值使用 UniApp 内置的全局事件方法uni.$emit和uni.$on可实现任意页面间的通信。比如发布博客成功后通知列表页自动刷新数据// 发布页发送全局事件 uni.$emit(blogPublished) // 列表页监听全局事件 onLoad() { uni.$on(blogPublished, () { this.page 1 this.blogList [] this.getList() }) }, onUnload() { // 页面销毁时移除监听避免内存泄漏 uni.$off(blogPublished) }2.7.2 复杂列表 评论显示详情页底部通常包含评论列表评论可能嵌套二级回复属于典型的复杂嵌套列表。我们用双层v-for实现层级渲染template view classcomment-section view classcomment-title全部评论/view !-- 一级主评论 -- view classcomment-item v-forcomment in commentList :keycomment.id view classcomment-header image classavatar :srccomment.avatar/image text classusername{{ comment.username }}/text /view text classcomment-content{{ comment.content }}/text !-- 二级回复列表 -- view classreply-list v-ifcomment.replyList comment.replyList.length 0 view classreply-item v-forreply in comment.replyList :keyreply.id text classreply-name{{ reply.replyName }}/text text classreply-content{{ reply.content }}/text /view /view /view /view /template2.8 自定义组件和用户登录自定义组件封装项目中很多重复的 UI 模块可以封装成自定义组件提升代码复用性。以通用空状态组件为例1.新建components/empty-state/empty-state.vuetemplate view classempty-box text classempty-icon{{ icon }}/text text classempty-text{{ text }}/text /view /template script export default { props: { icon: { type: String, default: }, text: { type: String, default: 暂无数据 } } } /script2.页面中引入并使用template empty-state text暂无博客内容/empty-state /template script import EmptyState from /components/empty-state/empty-state.vue export default { components: { EmptyState } } /script用户登录功能新建登录页面实现表单验证与登录逻辑template view classlogin-page input classinput-item placeholder请输入用户名 v-modelform.username / input classinput-item placeholder请输入密码 password v-modelform.password / button classlogin-btn clickhandleLogin登录/button text classgo-register clickgoRegister没有账号去注册/text /view /template script import { request } from /utils/request.js export default { data() { return { form: { username: , password: } } }, methods: { handleLogin() { // 基础表单校验 if (!this.form.username || !this.form.password) { uni.showToast({ title: 请填写完整信息, icon: none }) return } request({ url: /user/login, method: POST, data: this.form }).then(res { // 保存token和用户信息到本地缓存 uni.setStorageSync(token, res.token) uni.setStorageSync(userInfo, res.userInfo) uni.showToast({ title: 登录成功, icon: success }) setTimeout(() { uni.navigateBack() }, 1000) }) }, goRegister() { uni.navigateTo({ url: /pages/register/register }) } } } /script2.9 注册页面注册页面逻辑与登录页类似额外增加确认密码校验handleRegister() { const { username, password, confirmPassword } this.form if (!username || !password || !confirmPassword) { uni.showToast({ title: 请填写完整信息, icon: none }) return } if (password ! confirmPassword) { uni.showToast({ title: 两次密码不一致, icon: none }) return } request({ url: /user/register, method: POST, data: { username, password } }).then(res { uni.showToast({ title: 注册成功, icon: success }) setTimeout(() { uni.navigateBack() }, 1000) }) }2.10 个人中心页面个人中心页展示用户信息与功能入口从本地缓存读取用户数据渲染template view classprofile-page !-- 用户信息卡片 -- view classuser-card image classavatar :srcuserInfo.avatar || /static/default-avatar.png/image text classusername{{ userInfo.username || 未登录 }}/text /view !-- 功能菜单 -- view classmenu-list view classmenu-item clickgoMyBlog text classiconfont icon-wode/text text我的博客/text /view view classmenu-item clickgoChart text classiconfont icon-tongji/text text数据统计/text /view view classmenu-item clicklogout v-ifisLogin text classiconfont icon-tuichu/text text退出登录/text /view /view /view /template script export default { data() { return { userInfo: {}, isLogin: false } }, onShow() { // 页面每次显示时刷新用户信息 const userInfo uni.getStorageSync(userInfo) || {} this.userInfo userInfo this.isLogin !!uni.getStorageSync(token) }, methods: { logout() { uni.showModal({ title: 提示, content: 确定要退出登录吗, success: (res) { if (res.confirm) { uni.removeStorageSync(token) uni.removeStorageSync(userInfo) this.isLogin false this.userInfo {} } } }) } } } /script2.11 图表显示分析数据数据统计页面我们使用 uCharts 图表库实现直观展示博客发布量、阅读量等统计数据。通过 HBuilderX 插件市场导入「qiun-data-charts」插件页面中使用柱状图展示周度数据template view classchart-page qiun-data-charts typecolumn :optschartOpts :chartDatachartData / /view /template script export default { data() { return { chartData: { categories: [周一, 周二, 周三, 周四, 周五, 周六, 周日], series: [ { name: 阅读量, data: [120, 200, 150, 80, 70, 110, 130] }, { name: 发布量, data: [5, 8, 3, 2, 4, 6, 7] } ] }, chartOpts: { color: [#1890ff, #52c41a] } } } } /script2.12 添加博客最后实现博客发布功能用户可提交标题和正文内容发布新博客template view classadd-blog-page input classtitle-input placeholder请输入博客标题 v-modelform.title / textarea classcontent-textarea placeholder请输入博客正文内容 v-modelform.content maxlength2000 /textarea button classsubmit-btn clickhandleSubmit发布博客/button /view /template script import { request } from /utils/request.js export default { data() { return { form: { title: , content: } } }, methods: { handleSubmit() { if (!this.form.title.trim()) { uni.showToast({ title: 请输入博客标题, icon: none }) return } if (!this.form.content.trim()) { uni.showToast({ title: 请输入博客内容, icon: none }) return } request({ url: /blog/add, method: POST, data: this.form }).then(res { uni.showToast({ title: 发布成功, icon: success }) // 发送全局事件通知博客列表页刷新数据 uni.$emit(blogPublished) setTimeout(() { uni.navigateBack() }, 1000) }) } } } /script3. 项目总结与拓展方向核心内容总结到这里我们就完成了 Blogs 博客项目全部核心功能的开发从项目初始化到各个业务模块落地完整覆盖了 UniApp 开发的核心知识点项目搭建规范与目录结构设计Vue 基础语法与静态页面开发网络请求封装与模块化规范图标资源引入与原生组件使用本地缓存策略与用户体验优化路由跳转与页面间通信方案自定义组件封装思想登录注册与用户体系设计数据可视化与表单提交逻辑