前言
學(xué)習(xí)了一段時(shí)間vue3的基礎(chǔ)知識(shí)學(xué)習(xí),百學(xué)不如一練,想著還是做出一個(gè)實(shí)際的demo項(xiàng)目(適配為移動(dòng)端),來實(shí)踐鞏固自己所學(xué)的知識(shí)點(diǎn)??。
項(xiàng)目簡(jiǎn)介
1.前端技術(shù)棧:
vue3.0全家桶(組合式API)vuex?vuex-persistedstate(持久化數(shù)據(jù)存儲(chǔ))開發(fā)與構(gòu)建工具
Vite(極速的開發(fā)服務(wù)器啟動(dòng))堅(jiān)守前端MVVM的設(shè)計(jì)理念,遵循組件化、模塊化的編程思想
2.后端:
- GitHub上開源的網(wǎng)易云音樂NodeJS版api接口NeteaseCloudMusicApi
成果

項(xiàng)目結(jié)構(gòu)
├─ src
├─api // 網(wǎng)路請(qǐng)求代碼
├─assets // 字體配置及全局樣式
├─style // 公共樣式
├─components // 可復(fù)用的 UI 組件
├─utils // 工具類函數(shù)和相關(guān)配置
├─views // 頁(yè)面
├─router // 路由配置文件
└─store // redux 相關(guān)文件
App.jsx // 根組件
main.jsx // 入口文件
項(xiàng)目?jī)?nèi)容
- Vue-Router4.0。
router/index.js代碼如下:
import {
createRouter,
createWebHistory,
RouteRecordRaw
} from 'vue-router'
const Login = () => import('../views/Login/Login.vue')
const Phone = () => import('../views/Login/Phone.vue')
const Vcode = () => import('../views/Login/VerCode.vue')
const Home = () => import('../views/Home.vue')
const View = () => import('../views/View.vue')
const Comment = () => import('../views/Comment.vue')
//類型校驗(yàn),規(guī)范化typescript,增加路由對(duì)象類型限制,好處:允許在基礎(chǔ)路由里增加開發(fā)自定義屬性
const routes: RouteRecordRaw[] = [
{
path: '/',
redirect: '/login',
},
{
path: '/login',
name: 'Login',
meta: {
type: 'Login'
},
component: Login,
},
{
path: '/login/phone',
name: 'Phone',
meta: {
type: 'Login'
},
component: Phone,
},
{
path: '/login/phone/vcode',
name: 'Vcode',
component: Vcode,
}
.......
- 路由跳轉(zhuǎn)實(shí)現(xiàn)
使用router-Link點(diǎn)擊導(dǎo)航、編程式導(dǎo)航,代碼片段如下:
<router-link
:to="{ path: '/view', query: { id: item.id } }"
class="bottom-bg"
v-for="item in state.musicList"
:key="item.id"
>
......
</router-link>
setup(props) {
const router = useRouter();
let Nickname = ref("");
let avatarUrl = ref("");
function jumpComment() {
router.push({
path: "comment",
query: { id: props.listID },
});
}
.....
}
- 組件通信
組件中通信,兄弟間組件可以使用:
1.事件總線EventBus(vue3中,取消了全局事件總線,如果想要使用全局事件總線,那么就需要使用一個(gè)插件mitt)
安裝mitt: npm i mitt -s
在utils下創(chuàng)建bus.js文件:
//注冊(cè)event bus事件
import mitt from 'mitt'
const emitter = mitt()
export default emitter
使用代碼片段:
//emitter.emit使用
setup(props) {
const store = useStore();
/* let musicObj = ref(""); */
let isListen = ref(Boolean);
let indexNumber = ref("");
function handleIcon(index) {
isListen = !isListen;
indexNumber.value = index;
//通過點(diǎn)擊傳遞指定列表數(shù)據(jù)
emitter.emit("event", store.state.musicObj[index]);
}
......
}
//emitter.on的使用
setup() {
.......
//獲取事件總線傳遞過來的數(shù)據(jù)
emitter.on("event", (e) => {
.......
});
//事件總線的卸載,否則會(huì)存粗之前的調(diào)用
onBeforeUnmount(() => {
emitter.off("event");
});
.......
},
2.消息發(fā)布與訂閱PubSub
安裝第三方庫(kù): npm i pubsub-js
消息訂閱:
pubsub.subscribe('event',(funname, data) => {
// funname為消息名稱 data為傳遞的參數(shù)
console.log(funname, data) })
消息發(fā)布:
pubsub.publish('event', 'Tom') //第一個(gè)參數(shù)為消息名稱,第二個(gè)參數(shù)為發(fā)布的數(shù)據(jù)
當(dāng)寫到底部的播放音樂的組件的時(shí)候,不同的歌單列表下進(jìn)行點(diǎn)擊會(huì)影響數(shù)據(jù)的改變,發(fā)現(xiàn)如果單一的傳遞數(shù)據(jù)會(huì)過于混雜,這時(shí)候就需要用到vuex來管理,并能夠?qū)崿F(xiàn)多組件共享存儲(chǔ)數(shù)據(jù)(集中式存儲(chǔ)管理應(yīng)用)
效果如圖:
[圖片上傳失敗...(image-439e7a-1662526794196)]
<img src="https://upload-images.jianshu.io/upload_images/28469657-95e8b9bc8c8c1fe1.png">



安裝:npm i vuex@next --save
store/inex.js代碼如下:
import { createStore } from 'vuex'
import { getPlayList } from '../api/index'
......
const store = createStore({
state() {
return {
//整個(gè)音樂列表的數(shù)據(jù)存儲(chǔ)
musicObj: {},
......
}
},
mutations:
{
//保存發(fā)現(xiàn)好歌單信息
saveMusic(state, obj) {
state.musicObj = obj
},
//通過acion異步獲取底部歌單播放詳情
getMusic(state, obj) {
state.bottomMusic = obj
},
......
},
......
})
export default store
代碼片段:
//獲取state
import { useStore } from "vuex";
import emitter from "../utils/bus";
import { getPlayList } from "../api/index";
export default {
setup() {
const store = useStore();
let musicPic = ref("");
let musicName = ref("");
let musicAhtuor = ref("");
let isActive = ref(store.state.isAcitve);
......
}
//mutations提交數(shù)據(jù)
......
import { useStore } from "vuex";
import ListBotton from "../components/listBotton.vue";
/* 消息的發(fā)布與訂閱 */
import pubsub from "pubsub-js";
export default {
components: { listTop, listMiddle, ListBotton },
setup() {
const route = useRoute();
const store = useStore();
let state = reactive({ playData: {} });
let listID = ref();
let isShow=ref(false)
onBeforeMount(async () => {
isShow.value=true
let res = await getPlayList(route.query.id);
state.playData = reactive(res.data.playlist);
store.commit("saveMusic", state.playData.tracks);
......
});
}
//actions異步的使用
import { useStore } from "vuex";
export default {
setup() {
const store = useStore();
store.dispatch("getMusicList");
},
};
主要頁(yè)面編寫
為了方便參考網(wǎng)易云的布局結(jié)構(gòu),參考的是ipad的網(wǎng)易云HD,所以頁(yè)面有一些icon會(huì)看起來有點(diǎn)擁擠。
Phone頁(yè)面

Phone頁(yè)面是進(jìn)行手機(jī)號(hào)的驗(yàn)證,并向手機(jī)號(hào)碼發(fā)送實(shí)時(shí)驗(yàn)證碼短信。
通過點(diǎn)擊文件框獲取焦點(diǎn)的時(shí)候靈活激活數(shù)字鍵盤,watch來監(jiān)聽手機(jī)號(hào)碼的長(zhǎng)度來判斷是否可以點(diǎn)擊“下一步”的按鈕,并發(fā)起請(qǐng)求發(fā)送驗(yàn)證碼。
- Login/Phone.vue代碼片段:
watch(number, (newNumber) => {
if (newNumber.length !== 0) {
isActive.value = false;
}
});
......
async function handleLogin(value) {
if (!isPhoneNumber(number.value) && value == false) {
return ElMessage.error("手機(jī)號(hào)不合格!");
} else if (value == false) {
let phoneInfo = number.value;
let res = await postNumber(phoneInfo);
sessionStorage.setItem("phone", phoneInfo);
console.log(res);
router.push({
path: "/login/phone/vcode",
name: "Vcode",
});
}
}
View頁(yè)面和Comment頁(yè)面
View頁(yè)面是歌單列表詳情頁(yè)、Comment頁(yè)面是評(píng)論列表詳情頁(yè),都主要以處理數(shù)據(jù)并進(jìn)行渲染為主,通過后端得到的數(shù)據(jù)進(jìn)行分析,并用v-for進(jìn)行循環(huán)排列歌單列表。

通過點(diǎn)擊對(duì)應(yīng)的歌曲,拿到相應(yīng)的index存儲(chǔ)起來,??icon是通過判斷當(dāng)前點(diǎn)擊的index和歌曲的index是否一致,一致則顯示??icon。
- viewList/listMiddle.vue代碼片段:
......
<div class="content-left">
<p v-show="index === indexNumber ? false : true">{{ index + 1 }}</p>
<i
class="iconfont icon-shengyin"
v-show="index === indexNumber ? true : false">
</i>
</div>
......
function handleIcon(index) {
isListen = !isListen;
indexNumber.value = index;
//通過點(diǎn)擊傳遞指定列表數(shù)據(jù)
emitter.emit("event", store.state.musicObj[index]);
}
......
Comment頁(yè)面效果:

通過后端數(shù)據(jù)返回來的評(píng)論時(shí)間的時(shí)間戳,并獲取當(dāng)前的時(shí)間戳,如果年份相同則在“精彩評(píng)論”中出現(xiàn)年份,否則不出現(xiàn)。
通過點(diǎn)擊點(diǎn)贊事件來將點(diǎn)贊次數(shù)增加并且復(fù)制一份??并播放動(dòng)畫,更改后端返回的某個(gè)數(shù)據(jù)或者增加屬性,最后將刷新的數(shù)據(jù)交出去并進(jìn)行頁(yè)面渲染。
- views/Comment.vue
/* 計(jì)算時(shí)間戳 */
......
function hotTime(params) {
let date = new Date(params);
let newDate = new Date();
let newY = newDate.getFullYear();
let Y = date.getFullYear();
let M =
date.getMonth() + 1 < 10
? "0" + (date.getMonth() + 1)
: date.getMonth() + 1;
let D = date.getDay() > 10 ? "0" + date.getDate() : date.getDate();
return newY === Y ? M + "月" + D + "日" : Y + "年" + M + "月" + D + "日";
}
const NewComment = "new";
const HotComment = "hot";
/* 判斷是否第一次點(diǎn)擊 */
function handleLike(index, value) {
let item =
value === NewComment ? newComment.news[index] : hotComment.hot[index];
item.liked = !item.liked;
item.liked === true ? item.likedCount++ : item.likedCount--;
value === NewComment
? (newComment = reactive({ news: newComment.news }))
: (hotComment = reactive({ hot: hotComment.hot }));
}
......
優(yōu)化
路由懶加載:把不同路由對(duì)應(yīng)的組件分割成不同的代碼塊,然后當(dāng)路由被訪問的時(shí)候才加載對(duì)應(yīng)組件。
Suspense:加載異步組件時(shí),進(jìn)行 Loading 的處理
<Suspense>
<!-- 懶加載頁(yè)面 -->
<template #fallback>
<h1>Loading...</h1>
</template>
</Suspense>
- 采用了“骨架屏”的方式去提升用戶體驗(yàn)
項(xiàng)目遇到的坑
刷新頁(yè)面后vuex的數(shù)據(jù)丟失了
在使用vuex進(jìn)行存儲(chǔ)列表歌曲數(shù)據(jù)時(shí),在每一次頁(yè)面刷新后所有的數(shù)據(jù)都丟失了,才知道vuex不能夠持久化存儲(chǔ)數(shù)據(jù),一開始在嘗試瀏覽器storage來實(shí)施本地存儲(chǔ),通過后來的學(xué)習(xí),發(fā)現(xiàn)可以安裝vuex-persistedstate插件來進(jìn)行持久化本地?cái)?shù)據(jù)存儲(chǔ)。
安裝:npm install vuex-persistedstate
//引入
import { createStore } from 'vuex'
import persistedState from 'vuex-persistedstate'
//
const store = createStore({
......
plugins: [persistedState(/* { storage: window.sessionStorage } */)]
})
export default store
使用事件總線EventBus和消息發(fā)布與訂閱PubSub的時(shí)機(jī)
EventBus: 要先$on來接收頻道信號(hào),后$emit 發(fā)送頻道信號(hào)(就是要先知道誰接收,才能發(fā)送)
PubSub: 先subscribe,后publish(就是要先知道誰訂閱,才能發(fā)布)
兩者都必須在得到數(shù)據(jù)的頁(yè)面卸載之前把EventBus和PubSub事件給注銷,因此這兩者都具有緩存性(也就說在下一次的接收會(huì)收到本次和上一次發(fā)送的數(shù)據(jù))
總結(jié)
這個(gè)Demo是自己手把手?jǐn)]出來的,算是比較粗糙,比如說代碼規(guī)劃還有代碼風(fēng)格可能不太好;功能也不是很全,還有比如Home頁(yè)面中可以多個(gè)類似的組件可以做利用插槽進(jìn)行代碼優(yōu)化,如果后面有時(shí)間的話可能會(huì)一點(diǎn)點(diǎn)去完善,畢竟學(xué)習(xí)不會(huì)止步。????
源碼
項(xiàng)目源碼地址:GitHub,歡迎star??