在数字化浪潮席卷的当下,多端适配的需求愈发迫切。传统的原生开发方式,针对 iOS、Android、Web 等不同平台分别开发,不仅耗时耗力,还需要维护多套代码,成本极高。而 uni-app 作为一款强大的跨平台开发框架,凭借 “一套代码,多端发布” 的特性,成为开发者的福音。本文将在基础开发流程之上,进一步深入挖掘 uni-app 的开发技巧,带领大家打造一个功能更丰富、性能更优化的跨平台博客应用。
一、项目搭建与基础配置的深度拓展
1.1 环境优化与版本管理
在安装 Node.js 时,建议使用 nvm(Node Version Manager)进行版本管理,它可以方便地在不同 Node.js 版本间切换,避免因版本差异导致的兼容性问题。例如,在 Linux 或 macOS 系统中,通过以下命令安装 nvm:
TypeScript
取消自动换行复制
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh | bash
安装完成后,使用nvm install <version>安装指定版本的 Node.js,并通过nvm use <version>切换版本。
对于 HBuilderX,定期更新到最新版本,以获取新功能和性能优化。同时,在项目中使用 Git 进行版本控制,创建清晰的分支策略,如主分支用于稳定版本,开发分支用于日常开发,方便团队协作和代码回滚。
1.2 项目目录结构优化
除了基础的目录结构,还可以进一步细分目录,提高项目的可维护性。例如,创建store目录用于存放 Vuex 相关代码,管理应用的全局状态;utils目录下再细分request、filter等子目录,分别存放数据请求相关代码和数据过滤函数;styles目录用于管理全局样式和公共样式文件。优化后的目录结构如下:
TypeScript
取消自动换行复制
blog-app
├─ components // 组件目录
├─ pages // 页面目录
├─ static // 静态资源目录
├─ uni_modules // uni_modules插件目录
├─ store // Vuex状态管理目录
├─ utils
│ ├─ request // 数据请求目录
│ └─ filter // 数据过滤目录
├─ styles // 样式目录
├─ main.js // 入口文件
├─ App.vue // 应用根组件
└─ manifest.json // 应用配置文件
└─ pages.json // 页面路由配置文件
1.3 高级路由配置
在pages.json中,除了基础的页面路由配置,还可以配置路由守卫,实现权限控制等功能。例如,在进入文章编辑页时,检查用户是否登录:
TypeScript
取消自动换行复制
{
"pages": [
// 其他页面路由配置
{
"path": "pages/article/edit",
"style": {
"navigationBarTitleText": "编辑文章"
},
"meta": {
"requireLogin": true
}
}
]
}
在main.js中设置路由守卫:
TypeScript
取消自动换行复制
import Vue from 'vue';
import App from './App';
import router from './router'; // 假设已将路由配置提取到单独文件
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requireLogin)) {
const isLoggedIn = uni.getStorageSync('isLoggedIn'); // 假设登录状态存储在本地缓存
if (!isLoggedIn) {
next({
path: '/pages/login/login',
query: { redirect: to.fullPath }
});
} else {
next();
}
} else {
next();
}
});
new Vue({
router,
render: h => h(App)
}).$mount('#app');
二、数据请求与展示的进阶应用
2.1 数据请求拦截与响应处理
在utils/request.js中,添加请求拦截器和响应拦截器,统一处理请求头、错误提示等。例如,在请求头中添加 token:
TypeScript
取消自动换行复制
// utils/request.js
import uni from '@dcloudio/uni-app';
export function request(url, data = {}, method = 'GET') {
return new Promise((resolve, reject) => {
const token = uni.getStorageSync('token');
uni.request({
url: url,
data: data,
method: method,
header: {
'Content-Type': 'application/json',
'token': token
},
success: (res) => {
if (res.statusCode === 200) {
resolve(res.data);
} else {
uni.showToast({
title: '请求失败',
icon: 'none'
});
reject(res);
}
},
fail: (err) => {
uni.showToast({
title: '网络错误',
icon: 'none'
});
reject(err);
}
});
});
}
响应拦截器用于处理统一的错误码,如 401 表示未登录,自动跳转到登录页:
TypeScript
取消自动换行复制
// 在main.js中添加响应拦截器
import { request } from './utils/request.js';
request.interceptors.response.use(
response => {
return response;
},
error => {
if (error.statusCode === 401) {
uni.navigateTo({
url: '/pages/login/login'
});
}
return Promise.reject(error);
}
);
2.2 复杂数据展示与交互
在文章列表页,实现下拉刷新和上拉加载更多功能。在pages/home/home.vue中:
TypeScript
取消自动换行复制
<template>
<view class="home">
<scroll-view
:scroll-y="true"
@scrolltolower="loadMore"
:style="{ height: windowHeight + 'px' }"
>
<article-card v-for="article in articleList" :key="article.id" :article="article" @click="goToArticleDetail"></article-card>
<view v-if="loading" class="loading">加载中...</view>
</scroll-view>
<view v-if="!hasMore" class="no-more">没有更多内容了</view>
</view>
</template>
<script>
import articleCard from '@/components/article-card/article-card.vue';
import { request } from '@/utils/request.js';
export default {
components: {
articleCard
},
data() {
return {
articleList: [],
page: 1,
pageSize: 10,
loading: false,
hasMore: true,
windowHeight: 0
};
},
onLoad() {
this.getWindowHeight();
this.getArticleList();
},
methods: {
async getWindowHeight() {
const res = await uni.getSystemInfo();
this.windowHeight = res.windowHeight;
},
async getArticleList() {
this.loading = true;
try {
const res = await request(`/api/articles?page=${this.page}&pageSize=${this.pageSize}`);
if (res.data.length < this.pageSize) {
this.hasMore = false;
}
this.articleList = this.articleList.concat(res.data);
} catch (error) {
console.error('获取文章列表失败:', error);
} finally {
this.loading = false;
}
},
loadMore() {
if (!this.loading && this.hasMore) {
this.page++;
this.getArticleList();
}
},
goToArticleDetail(id) {
uni.navigateTo({
url: `/pages/article/article?id=${id}`
});
}
}
};
</script>
在文章详情页,实现点赞、收藏功能。通过调用后端接口更新数据,并实时展示状态:
TypeScript
取消自动换行复制
<template>
<view class="article">
<text class="article-title">{{ article.title }}</text>
<text class="article-author">作者:{{ article.author }}</text>
<view class="article-content" v-html="article.content"></view>
<view class="action-buttons">
<view class="action-button" @click="likeArticle">
<text :class="{ 'liked': article.isLiked }">{{ article.isLiked? '已点赞' : '点赞' }}</text>
<text class="like-count">{{ article.likeCount }}</text>
</view>
<view class="action-button" @click="collectArticle">
<text :class="{ 'collected': article.isCollected }">{{ article.isCollected? '已收藏' : '收藏' }}</text>
</view>
</view>
</view>
</template>
<script>
import { request } from '@/utils/request.js';
export default {
data() {
return {
article: {}
};
},
onLoad(options) {
const id = options.id;
this.getArticleDetail(id);
},
methods: {
async getArticleDetail(id) {
try {
const res = await request(`/api/articles/${id}`);
this.article = res.data;
} catch (error) {
console.error('获取文章详情失败:', error);
}
},
async likeArticle() {
if (this.article.isLiked) {
return;
}
try {
const res = await request(`/api/articles/${this.article.id}/like`, {}, 'POST');
this.article.isLiked = true;
this.article.likeCount = res.data.likeCount;
} catch (error) {
console.error('点赞失败:', error);
}
},
async collectArticle() {
if (this.article.isCollected) {
return;
}
try {
const res = await request(`/api/articles/${this.article.id}/collect`, {}, 'POST');
this.article.isCollected = true;
} catch (error) {
console.error('收藏失败:', error);
}
}
}
};
</script>
三、组件化开发的深入实践
3.1 复杂组件的封装与复用
创建一个评论组件(components/comment/comment.vue),支持多级评论展示和评论发布功能。组件接收文章 ID 作为参数,通过数据请求获取评论列表,并实现评论输入和提交:
TypeScript
取消自动换行复制
<template>
<view class="comment-section">
<view v-for="(comment, index) in commentList" :key="index" class="comment-item">
<text class="comment-author">{{ comment.nickname }}</text>
<text class="comment-time">{{ comment.createTime }}</text>
<view class="comment-content">{{ comment.content }}</view>
<view v-if="comment.replies && comment.replies.length > 0" class="reply-section">
<view v-for="(reply, replyIndex) in comment.replies" :key="replyIndex" class="reply-item">
<text class="reply-author">{{ reply.nickname }}</text>
<text class="reply-time">{{ reply.createTime }}</text>
<view class="reply-content">{{ reply.content }}</view>
</view>
</view>
<view class="reply-input" v-if="!replyToComment">
<input v-model="replyContent" placeholder="回复评论"></input>
<button @click="replyComment(comment.id)">回复</button>
</view>
<view v-else class="reply-input">
<input v-model="replyContent" placeholder="回复 {{ replyToComment.nickname }}"></input>
<button @click="submitReply(replyToComment.id)">发送</button>
</view>
</view>
<view class="comment-input">
<input v-model="newComment.content" placeholder="发表评论"></input>
<button @click="submitComment">提交</button>
</view>
</view>
</template>
<script>
import { request } from '@/utils/request.js';
export default {
props: {
articleId: {
type: String,
required: true
}
},
data() {
return {
commentList: [],
newComment: {
content: '',
parentId: null
},
replyContent: '',
replyToComment: null
};
},
onLoad() {
this.getCommentList();
},
methods: {
async getCommentList() {
try {
const res = await request(`/api/articles/${this.articleId}/comments`);
this.commentList = res.data;
} catch (error) {
console.error('获取评论列表失败:', error);
}
},
replyComment(commentId) {
this.replyToComment = this.commentList.find(c => c.id === commentId);
},
async submitReply(parentId) {
if (!this.replyContent) {
return;
}
try {
const res = await request(`/api/articles/${this.articleId}/comments/reply`, {
content: this.replyContent,
parentId: parentId
}, 'POST');
this.getCommentList();
this.replyContent = '';
this.replyToComment = null;
} catch (error) {
console.error('回复评论失败:', error);
}
},
async submitComment() {
if (!this.newComment.content) {
return;
}
try {
const res = await request(`/api/articles/${this.articleId}/comments`, this.newComment, 'POST');
this.getCommentList();
this.newComment.content = '';
} catch (error) {
console.error('提交评论失败:', error);
}
}
}
};
</script>
<style>
.comment-section {
padding: 15px;
}
.comment-item {
margin-bottom: 15px;
padding-bottom: 15px;
border-bottom: 1px solid #ccc;
}
.reply-section {
margin-left: 20px;
}
.reply-item {
margin-bottom: 5px;
}
.comment-input,
.reply-input {
margin-top: 15px;
}
</style>
在文章详情页引入评论组件:
TypeScript
取消自动换行复制
<template>
<view class="article">
<!-- 文章标题、作者、内容展示 -->
<comment :articleId="article.id"></comment>
</view>
</template>
<script>
import comment from '@/components/comment/comment.vue';
export default {
components: {
comment
},
// 其他代码不变
};
</script>
3.2 组件通信与状态管理
当组件之间存在复杂的通信需求时,使用 Vuex 进行状态管理。例如,在用户登录成功后,更新全局的用户登录状态,并在多个组件中使用。在store/index.js中定义用户状态:
TypeScript
取消自动换行复制
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export default new Vuex.Store({
state: {
user: null,
isLoggedIn: false
},
mutations: {
SET_USER(state, user) {
state.user = user;
state.isLoggedIn = true;
},
LOGOUT(state) {
state.user = null;
state.isLoggedIn = false;
}
},
actions: {
async login({ commit }, userData) {
try {
const res = await request('/api/login', userData, 'POST');
commit('SET_USER', res.data);
uni.setStorageSync('token', res.data.token);
uni.setStorageSync('user', res.data);
} catch (error) {
console.error('登录失败:', error);
}
},
logout({ commit }) {
commit('LOGOUT');
uni.removeStorageSync('token');
uni.removeStorageSync('user');
}
},
getters: {
getUser: state => state.user,
getIsLoggedIn: state => state.isLoggedIn
}
});
在登录页面调用login方法:
TypeScript
取消自动换行复制
<template>
<view class="login">
<input v-model="userData.username" placeholder="用户名"></input>
<input v-model="userData.password" placeholder="密码" type="password"></input>
<button @click="handleLogin">登录</button>
</view>
</template>
<script>
import { request</doubaocanvas>

被折叠的 条评论
为什么被折叠?



