入門級(jí)仿寫網(wǎng)易云音樂??Demo項(xiàng)目(Vue3+Vite+Vuex+Vue-Router4.0)

前言

學(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.后端:

成果

屏幕錄制2022-09-05 13.45.37.gif

項(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.事件總線EventBusvue3中,取消了全局事件總線,如果想要使用全局事件總線,那么就需要使用一個(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ù)

具體代碼點(diǎn)這里??

當(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">


image.png

image.png

image.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");

  },

};

具體代碼點(diǎn)這里??

主要頁(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)排列歌單列表。

QQ20220905-183455-HD.gif

通過點(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è)面效果:

QQ20220905-193157-HD.gif

通過后端數(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 }));

    }

  ......

具體代碼點(diǎn)這里??

優(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è)面卸載之前把EventBusPubSub事件給注銷,因此這兩者都具有緩存性(也就說在下一次的接收會(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??

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容