入门级仿写网易云音乐🎵Demo项目(Vue3+Vite+Vuex+Vue-Router4.0)

前言

学习了一段时间vue3的基础知识学习,百学不如一练,想着还是做出一个实际的demo项目(适配为移动端),来实践巩固自己所学的知识点💪。

项目简介

1.前端技术栈:

  • vue3.0全家桶(组合式API)

  • vuexvuex-persistedstate(持久化数据存储)

  • 开发与构建工具Vite(极速的开发服务器启动)

  • 坚守前端MVVM的设计理念,遵循组件化、模块化的编程思想

2.后端:

成果

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

项目结构


├─ src

    ├─api                  // 网路请求代码

    ├─assets                // 字体配置及全局样式

    ├─style                // 公共样式

    ├─components            // 可复用的 UI 组件

    ├─utils                // 工具类函数和相关配置

    ├─views                // 页面

    ├─router                // 路由配置文件

    └─store                // redux 相关文件

      App.jsx              // 根组件

      main.jsx              // 入口文件

项目内容

  • 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')

//类型校验,规范化typescript,增加路由对象类型限制,好处:允许在基础路由里增加开发自定义属性

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,

    }

    .......

  • 路由跳转实现

使用router-Link点击导航、编程式导航,代码片段如下:


      <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中,取消了全局事件总线,如果想要使用全局事件总线,那么就需要使用一个插件mitt


安装mitt: npm i mitt -s

在utils下创建bus.js文件:

//注册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;

      //通过点击传递指定列表数据

      emitter.emit("event", store.state.musicObj[index]);

    }

    ......

  }



  //emitter.on的使用

  setup() {

    .......

    //获取事件总线传递过来的数据

    emitter.on("event", (e) => {

      .......

    });

    //事件总线的卸载,否则会存粗之前的调用

    onBeforeUnmount(() => {

      emitter.off("event");

    });

  .......

  },

2.消息发布与订阅PubSub


安装第三方库: npm i pubsub-js

消息订阅:

pubsub.subscribe('event',(funname, data) => {

// funname为消息名称 data为传递的参数

console.log(funname, data) })

消息发布:

pubsub.publish('event', 'Tom') //第一个参数为消息名称,第二个参数为发布的数据

具体代码点这里🙋

当写到底部的播放音乐的组件的时候,不同的歌单列表下进行点击会影响数据的改变,发现如果单一的传递数据会过于混杂,这时候就需要用到vuex来管理,并能够实现多组件共享存储数据(集中式存储管理应用)

效果如图:
[图片上传失败...(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 {

            //整个音乐列表的数据存储

            musicObj: {},

            ......

        }

    },

    mutations:

    {

        //保存发现好歌单信息

        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提交数据

......

import { useStore } from "vuex";

import ListBotton from "../components/listBotton.vue";

/* 消息的发布与订阅 */

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

  },

};

具体代码点这里🙋

主要页面编写

为了方便参考网易云的布局结构,参考的是ipad的网易云HD,所以页面有一些icon会看起来有点拥挤。

Phone页面

Phone页面是进行手机号的验证,并向手机号码发送实时验证码短信。

通过点击文件框获取焦点的时候灵活激活数字键盘,watch来监听手机号码的长度来判断是否可以点击“下一步”的按钮,并发起请求发送验证码。

  • 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("手机号不合格!");

      } 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页面和Comment页面

View页面是歌单列表详情页、Comment页面是评论列表详情页,都主要以处理数据并进行渲染为主,通过后端得到的数据进行分析,并用v-for进行循环排列歌单列表。

QQ20220905-183455-HD.gif

通过点击对应的歌曲,拿到相应的index存储起来,📢icon是通过判断当前点击的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;

      //通过点击传递指定列表数据

      emitter.emit("event", store.state.musicObj[index]);

    }

    ......

Comment页面效果:

QQ20220905-193157-HD.gif

通过后端数据返回来的评论时间的时间戳,并获取当前的时间戳,如果年份相同则在“精彩评论”中出现年份,否则不出现。

通过点击点赞事件来将点赞次数增加并且复制一份👍并播放动画,更改后端返回的某个数据或者增加属性,最后将刷新的数据交出去并进行页面渲染。

  • views/Comment.vue

/* 计算时间戳 */

......

    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";

    /* 判断是否第一次点击 */

    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 }));

    }

  ......

具体代码点这里🙋

优化

  • 路由懒加载:把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件。

  • Suspense:加载异步组件时,进行 Loading 的处理


  <Suspense>

    <!-- 懒加载页面 -->

    <template #fallback>

      <h1>Loading...</h1>

    </template>

  </Suspense>

  • 采用了“骨架屏”的方式去提升用户体验

项目遇到的坑

刷新页面后vuex的数据丢失了

在使用vuex进行存储列表歌曲数据时,在每一次页面刷新后所有的数据都丢失了,才知道vuex不能够持久化存储数据,一开始在尝试浏览器storage来实施本地存储,通过后来的学习,发现可以安装vuex-persistedstate插件来进行持久化本地数据存储。


安装: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和消息发布与订阅PubSub的时机

EventBus: 要先$on来接收频道信号,后$emit 发送频道信号(就是要先知道谁接收,才能发送)

PubSub: 先subscribe,后publish(就是要先知道谁订阅,才能发布)

两者都必须在得到数据的页面卸载之前把EventBusPubSub事件给注销,因此这两者都具有缓存性(也就说在下一次的接收会收到本次和上一次发送的数据)

总结

这个Demo是自己手把手撸出来的,算是比较粗糙,比如说代码规划还有代码风格可能不太好;功能也不是很全,还有比如Home页面中可以多个类似的组件可以做利用插槽进行代码优化,如果后面有时间的话可能会一点点去完善,毕竟学习不会止步。💪💪

源码

项目源码地址:GitHub,欢迎star😘

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容