前戲
某一天的夜里,敲完了代碼之后便直接倒在床上睡着了,醒來時只記得夢里的一句話:“想要成為高手,就必須要大量實踐,大量做項目,必須要把自己不會的東西全部吃透,不要得過且過。”,猛然想起是一位大神前輩對我說的,工作之后每天加班,回家之后就不想學習了,總想着一把錘子搞定所有釘子,這樣是不行的,於是我就下了幾百G的項目實戰視頻,有Vue、React、Node.js、Angular、Flutter、各個框架源碼分析。。。我計划今年把它們全部干完,每做一個項目我都會寫一篇博客來記錄開發過程和收獲,我想着通過大量的項目練習來讓自己變成熟練工種,然后再去看源碼就會比較輕松;接下來就讓我牽着同學們的小手帶你們開發一款基於uni-app的微信小程序項目,項目名字叫【懂你找圖】。
項目介紹
做這個項目之前,同學們最好寫過2-3個移動端的頁面,有一定的JS基礎,比如map,forEach函數的使用,Promise的使用,掌握Vue的基本語法,基本的生命周期,什么是Watch?怎么使用一個Component?子傳父 / 父傳子的實現方式。
沒有基礎的同學也不要擔心,可以跟着把項目寫完,然后把不理解的地方單獨抽出來,逐個學習,然后再把項目獨立做一遍就完事了。
這個是項目做完之后的效果:
這個項目我會帶領同學們寫完首頁的模塊,其他模塊由於API接口還沒有寫好,暫時不做,等以后寫好了,我會馬上更新,接下來我們就進入正片環節。
1.項目准備
1.1開發方式
uni-app為我們提供2種開發方式:
1.使用DCloud公司提供HBuilderX工具來快速開發;
2.使用腳手架來快速開發(我們這次項目使用此方式);
1.2腳手架搭建項目
1.全局安裝,如果你以前安裝過就不需要重復安裝了。
npm install -g @vue/cli
2.創建項目。
vue create -p dcloudio/uni-preset-vue dnpicture
3.啟動項目(微信小程序)。
npm run dev:mp-weixin
4.在微信小程序開發者工具導入項目。
注意導入項目的路徑。
1.3搭建過程中可能遇到的問題
容易出現 vue 和 vue-template-complier版本不一致的問題。
根據提示重新安裝對應的vue版本即可 npm install vue@2.6.10,然后再重新運行項目 npm run dev:mp-weixin。
1.4安裝sass依賴
npm install sass-loader node-sass
2.項目搭建
2.1新增tabbar頁面
頁面名稱 | 路徑 |
首頁 | home/index.vue |
橫屏 | horizontal/index.vue |
精美視頻 | video/index.vue |
搜索 | search/index.vue |
我的 | mine/index.vue |
新建完頁面之后,我們再去pages.json文件里面添加頁面路徑和tabbar對應的圖片和樣式。

{ "pages": [{ "path": "pages/home/index", "style": { "navigationBarTitleText": "首頁" } }, { "path": "pages/horizontal/index", "style": { "navigationBarTitleText": "橫屏" } }, { "path": "pages/video/index", "style": { "navigationBarTitleText": "精美視頻" } }, { "path": "pages/search/index", "style": { "navigationBarTitleText": "搜索" } }, { "path": "pages/mine/index", "style": { "navigationBarTitleText": "我的" } } ], "globalStyle": { "navigationBarTextStyle": "black", "navigationBarTitleText": "uni-app", "navigationBarBackgroundColor": "#F8F8F8", "backgroundColor": "#F8F8F8" }, "tabBar": { "color": "#8a8a8a", "selectedColor": "#d4237a", "backgroundColor": "#fff", "position": "bottom", "borderStyle": "black", "list": [{ "pagePath": "pages/home/index", "text": "首頁", "iconPath": "./static/icon/_home.png", "selectedIconPath": "./static/icon/home.png" }, { "pagePath": "pages/horizontal/index", "text": "橫屏", "iconPath": "./static/icon/_img.png", "selectedIconPath": "./static/icon/img.png" }, { "pagePath": "pages/video/index", "text": "精美視頻", "iconPath": "./static/icon/_videocamera.png", "selectedIconPath": "./static/icon/videocamera.png" }, { "pagePath": "pages/search/index", "text": "搜索", "iconPath": "./static/icon/_search.png", "selectedIconPath": "./static/icon/search.png" }, { "pagePath": "pages/mine/index", "text": "我的", "iconPath": "./static/icon/_my.png", "selectedIconPath": "./static/icon/my.png" } ] } }
接下來我們需要在App.vue中全局引入字體圖標文件。

<script> export default { onLaunch: function() { }, onShow: function() { }, onHide: function() { } } </script> <style> @import "./styles/iconfont.wxss"; @import "./styles/base.wxss"; </style>
引入成功之后,就可以看到如下效果啦。
注意:要記得把icon和styles文件夾放到項目中去哦。
styles文件加放到和App.vue同層級目錄下,icon文件夾放入static文件夾里面。
2.2 uni-ui介紹
文檔: https://uniapp.dcloud.io/component/README?id=uniui
uni-ui是DCloud提供的一個跨端ui庫,它是基於vue組件的、flex布局的、無dom的跨全端ui框架。
uni-ui不包括基礎組件,它是基礎組件的補充:
數字角標、日歷、卡片、折疊面板、倒計時、抽屜、懸浮按鈕、收藏按鈕、底部購物導航、宮格、圖標、索引列表、列表、加載更多、自定義導航欄、通告欄、數字輸入框、分頁器、彈出層、評分、搜索欄、分段器、步驟條、滑動操作、輪播圖指示點、標簽。
3.首頁模塊開發准備
3.1 功能分析
1.修改導航欄外觀
2.使用分段器組件搭建子頁面
3.封裝自己的異步請求
3.2 搭建子頁面
- 首頁模塊分為4個部分,分別是 推薦、分類、最新、專輯 新建自定義組件來代替上述的4個頁面
- home-recommend
- home-category
- home-new
- home-album
3.2.1 分段器介紹
分段器是指uni-ui中的一個組件,其實就是俗稱的標簽頁,tab欄(https://ext.dcloud.net.cn/plugin?id=54)
3.2.2 分段器使用

<template> <view> <view> <uni-segmented-control :current="current" :values="items.map(v=>v.title)" @clickItem="onClickItem" style-type="text" active-color="#d21974" ></uni-segmented-control> <view class="content"> <view v-if="current === 0"> </view> <view v-if="current === 1"> </view> <view v-if="current === 2"> </view> <view v-if="current === 3"> </view> </view> </view> </view> </template> <script> import { uniSegmentedControl } from "@dcloudio/uni-ui"; export default { components: { uniSegmentedControl }, data() { return { items: [ { title: "推薦" }, { title: "分類" }, { title: "最新" }, { title: "專輯" } ], current: 0 }; }, methods: { onClickItem(e) { if (this.current !== e.currentIndex) { this.current = e.currentIndex; } } } }; </script> <style lang="scss"> </style>
3.3 封裝自己的異步請求
為什么要封裝?
- 原生的請求不支持promise;
- uni-api的請求不能夠方便的添加請求中效果;
- uni-api的請求返回值是個數組,不方便取值;
封裝的思路
- 基於原生promise來封裝;
- 掛載到Vue的原型上;
- 通過this.request的方式來使用;

/* 基於原生promise封裝request 發請求之前顯示'加載中...' 請求完成之后隱藏'加載中...' */ export default (params) => { uni.showLoading({ title: '加載中' }); return new Promise((resolve, reject)=>{ wx.request({ ...params, success(res) { resolve(res.data); }, fail(err) { reject(err); }, complete(){ uni.hideLoading(); } }) }) }
在main.js里面將request函數掛載到Vue的原型上
4.首頁-推薦模塊開發
4.1 功能介紹
- 數據動態渲染;
- moment.js的使用;
- 基於scroll-view的分頁加載;
4.2 實現過程
首頁推薦這個頁面非常簡單,沒有任何技術含量。。。
首先我們把靜態頁面寫出來,然后發送請求獲取數據然后使用v-for指令循環渲染數據,渲染圖片的時候注意接口有沒有帶rule這個屬性,如果有需要把thumb屬性和rule進行拼接,這里約定好<Height>的值為300,然后注意一下圖像要使用widthFix還是aspectFill,這些都是非常基礎的知識,大家可以自行到微信小程序的官方開發文檔里面找到,如果你不懂,還不願意自己去找資料學習,那我也沒辦法啦。
日期部分使用的是moment.js庫,下面是他的文檔地址:
http://momentjs.cn/docs/#/displaying/
接下來說一說分頁,無非就是把最頂級的view 標簽改成scroll-view標簽,加上一個scroll-y屬性,再加上一個觸底事件@scrolltolower=“handleScrollToLower”即可,這些東西uni-app官網都有,由於這個項目是使用uni-app來開發,所以很多API和組件都需要在uni-app文檔和微信小程序的文檔穿插查找,搞技術嘛,就是要往上面砸時間,耐心點就完事了。
分頁部分的邏輯其實很簡單:
頁面觸底之后,發送請求獲取數據,skip的值等於自身加上limit的值,然后limit的值加上30條,需要注意的是在觸底之后發送請求之前要判斷是否還有新的limit數據,可以在data里面設置一個狀態,比如hasLimit:true,然后在請求函數里面判斷一下是否還有新數據返回,如果沒有的話就將hasLimit的值改為false並且提示用戶。

<template> <scroll-view scroll-y @scrolltolower="handleScrollToLower" class="container" v-if="recommentList.length!==0" > <!-- 推薦部分圖片 start --> <view class="recomment-wrap"> <view class="item" v-for="item in recommentList" :key="item.id"> <image :src="item.thumb" mode="widthFix" /> </view> </view> <!-- 推薦部分圖片 end --> <!-- 月份圖片部分 start --> <view class="month-wrap"> <view class="month-title"> <view class="month-info"> <view class="month-time"> <text class="day">{{monthList.day}} /</text> <text class="month">{{monthList.month}} 月</text> </view> <view class="month-text">{{monthList.title}}</view> </view> <view class="month-more">更多></view> </view> <view class="month-content"> <view class="item" v-for="item in monthList.items" :key="item.id"> <image :src="item.thumb + item.rule.replace('$<Height>',300)" mode="aspectFill" /> </view> </view> </view> <!-- 月份圖片部分 end --> <!-- 熱門部分 start --> <view class="hot-wrap"> <view class="hot-title"> <text class="title-text">熱門</text> </view> <view class="hot-content"> <view class="hot-item" v-for="item in hotList" :key="item.id"> <image :src="item.thumb" mode="widthFix" /> </view> </view> </view> <!-- 熱門部分 end --> </scroll-view> </template> <script> export default { data() { return { params: { limit: 30, order: "hot", skip: 0 }, recommentList: [], monthList: [], hotList: [], hasLimit: true }; }, mounted() { this.getList(); }, methods: { getList() { this.request({ url: "http://157.122.54.189:9088/image/v3/homepage/vertical", data: this.params }).then(data => { if (this.monthList.length === 0) { this.recommentList = data.res.homepage[1].items; this.monthList = data.res.homepage[2]; this.monthList.day = this.moment(this.monthList.stime).format("DD"); this.monthList.month = this.moment(this.monthList.stime).format("MM"); } if (data.res.vertical.length === 0) { this.hasLimit = false; uni.showToast({ title: "沒有更多數據啦", icon: "none" }); return; } this.hotList = [...this.hotList, ...data.res.vertical]; this.params.skip += this.params.limit; this.params.limit += 30; }); }, handleScrollToLower() { if (this.hasLimit) { this.getList(); } else { uni.showToast({ title: "沒有更多數據啦", icon: "none" }); } } } }; </script> <style lang="scss"> .container { height: calc(100vh - 35px); } /* 推薦圖片部分*/ .recomment-wrap { display: flex; flex-wrap: wrap; > .item { width: 50%; image { border: 3rpx solid #fff; } } } /* 月份圖片部分 */ .month-wrap { .month-title { display: flex; justify-content: space-between; padding: 20rpx 20rpx; .month-info { display: flex; font-weight: bold; .month-time { color: $color; .day { font-size: 32rpx; } .month { font-size: 26rpx; } } .month-text { margin-left: 20rpx; color: #666; font-size: 32rpx; } } .month-more { font-size: 28rpx; color: $color; } } .month-content { display: flex; flex-wrap: wrap; .item { width: 33.33%; image { border: 5rpx solid #fff; } } } } /* 熱門部分 */ .hot-wrap { .hot-title { padding: 20rpx; text.title-text { padding-left: 14rpx; color: $color; border-left: 10rpx solid $color; font-size: 28rpx; font-weight: bold; } } .hot-content { display: flex; flex-wrap: wrap; .hot-item { width: 33.33%; image { border: 5rpx solid #fff; } } } } </style>
后續還要加上跳轉功能,到時候會將跳轉抽離成一個公共組件,到時在下文補充。
5.首頁-專輯模塊開發
5.1 功能介紹
- swiper輪播圖部分
- 專輯列表部分
5.2 實現過程
輪播圖的部分直接使用微信小程序官方提供的swiper組件,注意swiper組件默認寬度100%,高度是150px,而且swiper必須和swiper-item配對出現,否則會出問題,下面是小程序基礎教程和官方文檔:
基礎教程:https://www.cnblogs.com/replaceroot/p/11262929.html
官方文檔:https://developers.weixin.qq.com/miniprogram/dev/component/swiper.html
搞定了輪播之后就很容易了,寫一下靜態頁面,發下請求然后渲染數據,注意對分頁數據的判斷就行啦,對你們來說絕對是小菜一碟,代碼如下:

<template> <scroll-view scroll-y="true" @scrolltolower="handleScrollToLower" class="album-wrap"> <!-- 輪播圖部分 start --> <swiper class="swiper" indicator-dots="true" autoplay="true" interval="3000" circular="true"> <swiper-item v-for="item in banner" :key="item.id"> <image :src="item.thumb" mode="widthFix" /> </swiper-item> </swiper> <!-- 輪播圖部分 end --> <!-- 專輯列表部分 start --> <view class="album-list"> <navigator :url="`/pages/album/index?id=${item.id}`" class="album-item" v-for="item in album" :key="item.id" > <view class="album-image"> <image :src="item.cover" mode="aspectFill" /> </view> <view class="album-info"> <view class="alubm-name ellipsis">{{item.name}}</view> <view class="alubm-desc ellipsis">{{item.desc}}</view> <view class="attention"> <view class="attention-btn">+關注</view> </view> </view> </navigator> </view> <!-- 專輯列表部分 end --> </scroll-view> </template> <script> export default { data() { return { banner: [], album: [], params: { limit: 30, skip: 0, order: "new" }, hasLimit: true }; }, methods: { getList() { this.request({ url: "http://157.122.54.189:9088/image/v1/wallpaper/album", data: this.params }).then(data => { if (this.album.length === 0) { this.banner = data.res.banner; } if (data.res.album.length === 0) { this.hasLimit = false; uni.showToast({ title: "沒有更多的數據啦!", icon: "none" }); return; } this.album = [...this.album, ...data.res.album]; this.params.skip += this.params.limit; this.params.limit += 30; }); }, handleScrollToLower() { if (this.hasLimit) { this.getList(); } else { uni.showToast({ title: "沒有更多數據啦!", icon: "none" }); } } }, mounted() { this.getList(); } }; </script> <style lang="scss"> /* 公共樣式 */ .ellipsis { text-overflow: ellipsis; overflow: hidden; white-space: nowrap; } /* 專輯輪播圖部分 */ .album-wrap { height: calc(100vh - 35px); .swiper { height: 320rpx; image { } } } /* 專輯列表部分 */ .album-list { padding: 10rpx; .album-item { display: flex; padding: 10rpx; border-bottom: 3rpx solid #d5d5d5; .album-image { flex: 1; image { width: 200rpx; height: 200rpx; } } .album-info { flex: 3; margin-left: 40rpx; overflow: hidden; .alubm-name { color: #000; } .alubm-desc { color: #666; } .attention { display: flex; justify-content: flex-end; margin-top: 10rpx; .attention-btn { padding: 0 5rpx; border: 3rpx solid $color; color: $color; } } } } } </style>
6.專輯詳情模塊開發
6.1 功能分析
- 頭部背景圖部分
- 專輯詳情列表圖片部分
6.2 實現過程
實現的過程也非常簡單,首先放一張image圖片當作背景圖片,圖片里面的文字都知道怎么做吧,直接用定位就完事了。
下面也是一樣套路,先寫靜態頁面,然后發請求,注意下圖片的寬高,和mode模式就行了,具體的代碼如下:

<template> <view class="album-detail-wrap"> <!-- 專輯詳情背景部分 start --> <view class="album-background"> <image :src="album.cover" mode="widthFix" /> <view class="album-info"> <view class="album-name">{{album.name}}</view> <view class="attention"> <view class="attention-btn">關注專輯</view> </view> </view> </view> <!-- 專輯詳情背景部分 end --> <!-- 列表部分 start --> <view class="album-list"> <view class="album-title"> <view class="author"> <image :src="album.user.avatar" mode="aspectFill" /> <text class="author-name">{{album.user.name}}</text> </view> <text class="album-desc">{{album.desc}}</text> </view> <view class="album-content"> <view class="alubm-item" v-for="item in wallpaper" :key="item.id"> <image :src="item.thumb + item.rule.replace('$<Height>',300)" mode="aspectFill" /> </view> </view> </view> <!-- 列表部分 end --> </view> </template> <script> export default { data() { return { params: { limit: 30, skip: 0, order: "new", first: "1" }, id: "", album: [], wallpaper: [], hasLimit: true }; }, methods: { getList() { if(this.album.length==0) { this.params.first = '1'; } this.request({ url: `http://157.122.54.189:9088/image/v1/wallpaper/album/${this.id}/wallpaper`, data: this.params }).then(data => { if(this.album.length === 0) { this.album = data.res.album; } if(data.res.wallpaper.length===0) { this.hasLimit = false; uni.showToast({ title: '沒有數據啦', icon: 'none' }); return; } this.params.first = 0; this.wallpaper = [...this.wallpaper,...data.res.wallpaper]; this.params.skip += this.params.limit; this.params.limit += 30; }); } }, onReachBottom() { if(this.hasLimit) { this.getList(); }else { uni.showToast({ title: '沒有數據啦!', icon: 'none' }); } }, onLoad(options) { this.id = options.id; this.getList(); } }; </script> <style lang="scss"> .album-detail-wrap { /* 專輯詳情背景圖部分 */ .album-background { position: relative; image { } .album-info { position: absolute; bottom: 5%; display: flex; align-items: center; justify-content: space-between; width: 100%; padding: 0 20rpx; .album-name { color: #fff; font-size: 32rpx; } .attention { display: flex; justify-content: center; align-items: center; .attention-btn { padding: 10rpx 15rpx; background-color: $color; color: #fff; border-radius: 10rpx; font-size: 26rpx; } } } } /* 專輯詳情列表部分 */ .album-list { .album-title { padding: 10rpx; .author { display: flex; image { width: 60rpx; height: 60rpx; } text.author-name { line-height: 60rpx; margin-left: 5rpx; color: #000; } } text.album-desc { color: #666; font-size: 26rpx; } } .album-content { display: flex; flex-wrap: wrap; .alubm-item { width: 33.33%; image { height: 200rpx; border: 3rpx solid #fff; } } } } } </style>
最后有個小坑需要注意下,小程序里面的view標簽不支持文本中的換行符,如果某些特殊場景中后台返回的文本里面包含換行符就直接使用text標簽就完事了。
7.圖片詳情模塊開發
7.1 功能分析
- 封裝超鏈接組件
- 發送請求獲取數據
- 使用moment.js處理特殊時間格式
- 封裝手勢滑動組件
- 調用API下載圖片
7.2 實現過程
在components組件文件夾下面新建一個goDetail.vue的自定義組件

<template> <view @click="handleClick"> <slot></slot> </view> </template> <script> export default { props: { list: Array, index: Number }, methods: { handleClick() { /* 1 將數據緩存下來 使用getApp()全局緩存方式 2 實現點擊跳轉頁面 */ getApp().globalData.imgList = this.list; getApp().globalData.imgIndex = this.index; uni.navigateTo({ url: "/pages/imgDetail/index" }); } } }; </script> <style> </style>
這個地方用到了微信小程序的全局緩存數據的方法,我們把數據緩存在App.vue文件中,使用的時候直接通過getApp().globalData.屬性的方法獲取數據即可。
具體發請求獲取數據渲染頁面的部分自行看代碼學習吧。
項目github地址:https://github.com/C4az6/dnpicture.git
項目API文檔:https://www.showdoc.cc/414855720281749?page_id=3678621017219602
這個項目教程就此結束。