利用UniApp制作一个小巧的图片浏览器
最近接了个需求,要求做一个轻量级的图片浏览工具,考虑到多端适配的问题,果断选择了UniApp作为开发框架。本文记录了我从0到1的开发过程,希望能给有类似需求的小伙伴一些参考。
前言
移动互联网时代,图片已成为人们日常生活中不可或缺的内容形式。无论是社交媒体还是工作沟通,我们每天都会接触大量图片。因此,一个好用的图片浏览器对用户体验至关重要。
UniApp作为一个使用Vue.js开发所有前端应用的框架,可以实现一套代码、多端运行(iOS、Android、H5、小程序等),是开发跨平台应用的理想选择。今天我就分享一下如何使用UniApp开发一个小巧但功能完善的图片浏览器。
开发环境准备
首先,我们需要搭建UniApp的开发环境:
- 安装HBuilderX(官方IDE)
- 创建UniApp项目
- 配置基础项目结构
bash
# 如果使用CLI方式创建项目
npx @vue/cli create -p dcloudio/uni-preset-vue my-image-browser
项目结构设计
为了保持代码的可维护性,我将项目结构设计如下:
├── components # 组件目录
│ ├── image-previewer # 图片预览组件
│ └── image-grid # 图片网格组件
├── pages # 页面
│ ├── index # 首页
│ └── detail # 图片详情页
├── static # 静态资源
├── utils # 工具函数
└── App.vue、main.js等 # 项目入口文件
核心功能实现
1. 图片网格列表
首先实现首页的图片网格列表,这是用户进入应用的第一个界面:
vue
<template>
<view class="container">
<view class="header">
<text class="title">图片浏览器</text>
</view>
<view class="image-grid">
<view
class="image-item"
v-for="(item, index) in imageList"
:key="index"
@tap="previewImage(index)"
>
<image
:src="item.thumb || item.url"
mode="aspectFill"
lazy-load
></image>
</view>
</view>
<view class="loading" v-if="loading">
<text>加载中...</text>
</view>
</view>
</template>
<script>
export default {
data() {
return {
imageList: [],
page: 1,
loading: false
}
},
onLoad() {
this.loadImages()
},
// 下拉刷新
onPullDownRefresh() {
this.page = 1
this.imageList = []
this.loadImages(() => {
uni.stopPullDownRefresh()
})
},
// 上拉加载更多
onReachBottom() {
this.loadImages()
},
methods: {
// 加载图片数据
loadImages(callback) {
if (this.loading) return
this.loading = true
// 这里可以替换为实际的API请求
setTimeout(() => {
// 模拟API返回数据
const newImages = Array(10).fill(0).map((_, i) => ({
id: this.imageList.length + i + 1,
url: `https://picsum.photos/id/${this.page * 10 + i}/500/500`,
thumb: `https://picsum.photos/id/${this.page * 10 + i}/200/200`
}))
this.imageList = [...this.imageList, ...newImages]
this.page++
this.loading = false
callback && callback()
}, 1000)
},
// 预览图片
previewImage(index) {
const urls = this.imageList.map(item => item.url)
uni.previewImage({
urls,
current: urls[index]
})
}
}
}
</script>
<style>
.container {
padding: 20rpx;
}
.header {
height: 80rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 20rpx;
}
.title {
font-size: 36rpx;
font-weight: bold;
}
.image-grid {
display: flex;
flex-wrap: wrap;
}
.image-item {
width: 33.33%;
padding: 5rpx;
box-sizing: border-box;
}
.image-item image {
width: 100%;
height: 220rpx;
border-radius: 8rpx;
}
.loading {
text-align: center;
margin: 30rpx 0;
color: #999;
}
</style>
这段代码实现了图片瀑布流展示、下拉刷新和上拉加载更多功能。我使用了懒加载技术来优化性能,同时使用缩略图先加载,提升用户体验。
2. 自定义图片预览组件
虽然UniApp内置了图片预览功能,但为了实现更丰富的交互和动画效果,我决定自己封装一个图片预览组件:
vue
<template>
<view
class="image-previewer"
v-if="visible"
@touchstart="handleTouchStart"
@touchmove="handleTouchMove"
@touchend="handleTouchEnd"
>
<view class="previewer-header">
<text class="counter">{{ current + 1 }}/{{ images.length }}</text>
<view class="close" @tap="close">×</view>
</view>
<swiper
class="swiper"
:current="current"
@change="handleChange"
:circular="true"
>
<swiper-item v-for="(item, index) in images" :key="index">
<movable-area class="movable-area">
<movable-view
class="movable-view"
:scale="item.scale"
:scale-min="1"
:scale-max="4"
:scale-value="item.scale"
direction="all"
@scale="handleScale($event, index)"
@change="handleMoveChange"
>
<image
:src="item.url"
mode="widthFix"
@load="imageLoaded(index)"
></image>
</movable-view>
</movable-area>
</swiper-item>
</swiper>
<view class="previewer-footer">
<view class="save-btn" @tap="saveImage">保存图片</view>
</view>
</view>
</template>
<script>
export default {
name: 'ImagePreviewer',
props: {
urls: {
type: Array,
default: () => []
},
current: {
type: Number,
default: 0
},
visible: {
type: Boolean,
default: false
}
},
data() {
return {
images: [],
startY: 0,
moveY: 0,
moving: false
}
},
watch: {
urls: {
handler(val) {
this.images = val.map(url => ({
url,
scale: 1,
loaded: false
}))
},
immediate: true
}
},
methods: {
handleChange(e) {
this.$emit('update:current', e.detail.current)
},
imageLoaded(index) {
this.$set(this.images[index], 'loaded', true)
},
handleScale(e, index) {
this.$set(this.images[index], 'scale', e.detail.scale)
},
handleMoveChange() {
// 处理图片拖动事件
},
handleTouchStart(e) {
this.startY = e.touches[0].clientY
},
handleTouchMove(e) {
if (this.images[this.current].scale > 1) return
this.moveY = e.touches[0].clientY - this.startY
if (this.moveY > 0) {
this.moving = true
}
},
handleTouchEnd() {
if (this.moving && this.moveY > 100) {
this.close()
}
this.moving = false
this.moveY = 0
},
close() {
this.$emit('close')
},
saveImage() {
const url = this.images[this.current].url
// 先下载图片到本地
uni.downloadFile({
url,
success: (res) => {
// 保存图片到相册
uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: () => {
uni.showToast({
title: '保存成功',
icon: 'success'
})
},
fail: () => {
uni.showToast({
title: '保存失败',
icon: 'none'
})
}
})
}
})
}
}
}
</script>
<style>
.image-previewer {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: #000;
z-index: 999;
display: flex;
flex-direction: column;
}
.previewer-header {
height: 88rpx;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 30rpx;
}
.counter {
color: #fff;
font-size: 28rpx;
}
.close {
color: #fff;
font-size: 60rpx;
line-height: 1;
}
.swiper {
flex: 1;
width: 100%;
}
.movable-area {
width: 100%;
height: 100%;
}
.movable-view {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.movable-view image {
width: 100%;
}
.previewer-footer {
height: 100rpx;
display: flex;
align-items: center;
justify-content: center;
}
.save-btn {
color: #fff;
font-size: 28rpx;
padding: 10rpx 30rpx;
border-radius: 30rpx;
background-color: rgba(255, 255, 255, 0.2);
}
</style>
这个组件实现了以下功能:
- 图片切换(使用swiper组件)
- 图片缩放(movable-view的scale属性)
- 向下滑动关闭预览
- 保存图片到本地相册
3. 添加图片相册功能
为了增强应用的实用性,我们来添加一个相册分类功能:
vue
<template>
<view class="album">
<view class="tabs">
<view
class="tab-item"
v-for="(item, index) in albums"
:key="index"
:class="{ active: currentAlbum === index }"
@tap="switchAlbum(index)"
>
<text>{{ item.name }}</text>
</view>
</view>
<view class="content">
<view class="album-info">
<text class="album-name">{{ albums[currentAlbum].name }}</text>
<text class="album-count">{{ imageList.length }}张照片</text>
</view>
<view class="image-grid">
<view
class="image-item"
v-for="(item, index) in imageList"
:key="index"
@tap="previewImage(index)"
>
<image
:src="item.thumb || item.url"
mode="aspectFill"
lazy-load
></image>
</view>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
albums: [
{ id: 1, name: '风景' },
{ id: 2, name: '人物' },
{ id: 3, name: '动物' },
{ id: 4, name: '植物' }
],
currentAlbum: 0,
imageList: []
}
},
onLoad() {
this.loadAlbumImages()
},
methods: {
switchAlbum(index) {
if (this.currentAlbum === index) return
this.currentAlbum = index
this.loadAlbumImages()
},
loadAlbumImages() {
const albumId = this.albums[this.currentAlbum].id
// 模拟加载不同相册的图片
uni.showLoading({ title: '加载中' })
setTimeout(() => {
// 模拟API返回数据
this.imageList = Array(15).fill(0).map((_, i) => ({
id: i + 1,
url: `https://picsum.photos/seed/${albumId * 100 + i}/500/500`,
thumb: `https://picsum.photos/seed/${albumId * 100 + i}/200/200`
}))
uni.hideLoading()
}, 800)
},
previewImage(index) {
const urls = this.imageList.map(item => item.url)
uni.previewImage({
urls,
current: urls[index]
})
}
}
}
</script>
<style>
.album {
display: flex;
flex-direction: column;
height: 100vh;
}
.tabs {
display: flex;
height: 80rpx;
border-bottom: 1rpx solid #eee;
}
.tab-item {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
position: relative;
}
.tab-item.active {
color: #007AFF;
}
.tab-item.active::after {
content: '';
position: absolute;
bottom: 0;
left: 25%;
width: 50%;
height: 4rpx;
background-color: #007AFF;
}
.content {
flex: 1;
padding: 20rpx;
}
.album-info {
margin-bottom: 20rpx;
}
.album-name {
font-size: 36rpx;
font-weight: bold;
}
.album-count {
font-size: 24rpx;
color: #999;
margin-left: 20rpx;
}
.image-grid {
display: flex;
flex-wrap: wrap;
}
.image-item {
width: 33.33%;
padding: 5rpx;
box-sizing: border-box;
}
.image-item image {
width: 100%;
height: 220rpx;
border-radius: 8rpx;
}
</style>
性能优化
开发过程中,我注意到一些性能问题,特别是图片加载较慢的情况,因此做了以下优化:
- 懒加载:使用lazy-load属性延迟加载图片
- 缩略图预加载:先加载小图,再加载大图
- 图片压缩:在上传和展示时进行适当压缩
js
// 图片压缩工具函数
export function compressImage(src, quality = 80) {
return new Promise((resolve, reject) => {
uni.compressImage({
src,
quality,
success: res => {
resolve(res.tempFilePath)
},
fail: err => {
reject(err)
}
})
})
}
- 虚拟列表:当图片数量很多时,考虑使用虚拟列表技术
踩坑记录
开发过程中遇到了一些坑,在此记录,希望能帮助到大家:
- 兼容性问题:H5和App表现一致,但在小程序中movable-view的缩放效果不太理想,需要针对不同平台做兼容处理
- 图片预览:小程序的图片预览API不支持长按保存,需要自己实现
- 内存问题:加载大量高清图片容易导致内存占用过高,需要做好图片资源管理
总结
通过这个项目,我实现了一个简单但功能完善的图片浏览器。UniApp的跨平台能力确实令人印象深刻,一套代码能够同时运行在多个平台上,大大提高了开发效率。
当然,这个应用还有很多可以改进的地方,比如添加图片滤镜、优化动画效果、增加云存储功能等。希望这篇文章对你有所帮助,有任何问题欢迎在评论区留言讨论!