20230630----重返学习-QQ音乐-从零开始构建一个Vue2的项目-常见面试题-Vue组件间通信

45 篇文章 0 订阅
37 篇文章 0 订阅

day-102-one-hundred-and-two-20230630-QQ音乐-从零开始构建一个Vue2的项目-常见面试题-Vue组件间通信

QQ音乐

  1. 自适应适配。

从零开始构建一个Vue2的项目

  1. 基于@vue/cli脚手架创建项目。

    vue create xxx
    
    1. 修改默认的配置项:
      • 基础配置项:
        • publicPath
        • lintOnSave
        • transpileDependencies
        • productionSourceMap
      • dev-server
        • host
        • port
        • open
        • proxy
      • 进阶处理
        • configWebpack
        • chainWebpack
          • 生产环境下去除console/debugger
        • css
          • 定义less中使用的公共样式变量。
      • babel.config.js
        • UI组件库的按需导入
    2. 浏览器兼容的处理
      • browserslist 浏览器的兼容列表。
      • @babel/polyfill(或core-js)
    3. 移动端的rem响应式布局开发
      • lib-flexible

        • 帮助我们自动设置html的字体大小-也就是rem和px的换算比例,随着设备大小的切换,它也会跟着更改!
        • 设置的值 = 设备的宽度 / 10
          • 设计稿是375px的:1rem = 37.5px
          • 设备宽度是414px:1rem = 41.4px
        • 当设备宽度超过540px后,将不会继续放大。也就是最大的html字体大小是:54px。
        • 具体步骤:
          1. 安装lib-flexible。

          2. 在入口文件中导入lib-flexible。

            • Vue2进阶/VueQQMusic/src/main.js

              import 'lib-flexible'
              
          3. 解决lib-flexible中最大宽度为540px的问题:在根视图中设置最大宽度为540px。

            • 如果使用了postcss-pxtorem的插件,要使用内嵌样式。因为postcss-pxtorem会把less中的样式代码中的px单位按照设置的长度改写为rem单位。
              • /src/App.vue

                <template>
                  <div id="app" style="max-width: 540px;">
                    <music-page />
                  </div>
                </template>
                
      • postcss-pxtorem:postcss-loader的插件。

        • 在代码编译的时候,可以把我们写的px单位的值,按照指定的rem和px和换算比例,自动转换为rem的值!

          • 配置,以下两种方式,用一种就好了:
            • 可在/postcss.config.js中进行配置

              //导出postcss-loader的配置项。
              module.exports = {
                plugins: {
                  "postcss-pxtorem": {
                    rootValue: 37.5, //因为默认设计稿是375px,按照lib-flexible的计算规则,当前rem/px的换算比例是1rem=37.5px,即初始rem/px的换算比例是37.5。
                    propList: ["*"], //指定那些样式文件使用这个规则。
                  },
                },
              };
              
            • 可在/vue.config.js中进行配置

              const { defineConfig } = require('@vue/cli-service')
              
              module.exports = defineConfig({
                css: {
                  loaderOptions: {
                    postcss: {
                      postcssOptions: {
                        plugins: {
                          'postcss-pxtorem': {
                            rootValue: 37.5,
                            propList: ['*']
                          }
                        }
                      }
                    }
                  }
                }
              })
              
        • 好处:我们基于375px的设备稿,量出来多大,写样式的时候就写多少px,此插件会默认帮我们把样式转换为rem。

      • 安装一些后续需要使用的插件与组件库

        • axios
        • blueimp-md5
        • fastclick
        • hammerjs
        • less/less-loader
  2. 配置一些项目中需要的基本骨架。

    • FastClick的处理。

    • API接口的统一管理-/api文件夹

      import API from './api'
      Vue.prototype.$API = API
      
      • 把接口放在Vue原型上,组件中this通过原型链可以取到。
    • 准备一些静态资源 assets目录。

      • 通用样式
      • 静态资源图片。
      • 公共的js方法库 utils.js
      • 字体资源
    • 构建vuex的架子 store

    • 构建vue-router的架子 router

      1. 分析路由
      2. 创建.vue单文件组件(页面 /views)
      3. 配置路由表(路由懒加载)
    • global.js 全局需要处理的东西。

  3. 一个页面一个页面地开发

    1. 搭结构、写样式。
      1. 从服务器返回的原始数据,一般要进行冻结。
      2. 要获取DOM元素可通过ref来获取。
        • 绑定的是一个循环出来的元素,得到的是元素列表。
      3. 在methods中的方法,实际上都是包了一层,每次执行时,都保证调用对应方法时this都为当前实例。
      4. 如果要定义一个只在该组件中用到的纯函数,可在export default 组件之前定义。
    2. 实现相应的需求功能。
    3. 最主要的是:注意组件的抽离和封装-通用的业务组件、UI组件库组件的二次封装、通用的功能组件…。
      • EasyMock:创建假数据。
      • 前后端联调-后端提供接口,把接口数据放到页面上。
  4. 优化与内测(自己测试几遍)

  5. 提测与Bug修复。

  6. 打包部署上线。

    • 部署是放的是dist目录。

源码示例

  • Vue2进阶/VueQQMusic/src/api/index.js

    // 模拟从服务器获取歌词等数据
    const queryLyric = () => {
      // 假设从服务器获取的数据
      const obj = {
        code: 0,
        message: "ok",
        data: {
          title: "我的梦 - 华为手机主题曲",
          author: "张靓颖",
          duration: "03:39",
          pic: "https://zxt_team.gitee.io/opensource/qqmusic/mydream.jpg",
          audio: "https://zxt_team.gitee.io/opensource/qqmusic/mydream.m4a",
          lyric:
            "[ti&#58;《我的梦》]&#10;[ar&#58;张靓颖]&#10;[al&#58;]&#10;[by&#58;]&#10;[offset&#58;0]&#10;[00&#58;01&#46;36]我的梦&#32;&#40;华为手机主题曲&#41;&#32;&#45;&#32;张靓颖&#10;[00&#58;02&#46;11]词:王海涛/张靓颖&#10;[00&#58;02&#46;64]曲:Andy&#32;Love&#10;[00&#58;03&#46;48]编曲:崔迪&#10;[00&#58;04&#46;49]&#10;[00&#58;08&#46;73]一直地一直地往前走&#10;[00&#58;11&#46;65]&#10;[00&#58;13&#46;02]疯狂的世界&#10;[00&#58;14&#46;58]&#10;[00&#58;16&#46;68]迎着痛把眼中所有梦&#10;[00&#58;20&#46;52]&#10;[00&#58;21&#46;03]都交给时间&#10;[00&#58;22&#46;71]&#10;[00&#58;24&#46;24]想飞就用心地去飞&#10;[00&#58;26&#46;98]谁不经历狼狈&#10;[00&#58;30&#46;68]&#10;[00&#58;31&#46;60]我想我会忽略失望的灰&#10;[00&#58;34&#46;99]拥抱遗憾的美&#10;[00&#58;39&#46;05]我的梦说别停留等待&#10;[00&#58;43&#46;94]就让光芒折射泪湿的瞳孔&#10;[00&#58;47&#46;74]映出心中最想拥有的彩虹&#10;[00&#58;51&#46;78]带我奔向那片有你的天空&#10;[00&#58;55&#46;74]因为你是我的梦&#10;[01&#58;01&#46;06]&#10;[01&#58;07&#46;19]我的梦&#10;[01&#58;08&#46;72]&#10;[01&#58;16&#46;75]执着地勇敢地不回头&#10;[01&#58;20&#46;29]&#10;[01&#58;21&#46;05]穿过了黑夜踏过了边界&#10;[01&#58;24&#46;87]路过雨路过风往前冲&#10;[01&#58;28&#46;39]&#10;[01&#58;28&#46;96]总会有一天站在你身边&#10;[01&#58;32&#46;52]泪就让它往下坠&#10;[01&#58;35&#46;00]溅起伤口的美&#10;[01&#58;38&#46;60]&#10;[01&#58;39&#46;55]哦别以为失去的最宝贵&#10;[01&#58;43&#46;00]才让今天浪费&#10;[01&#58;47&#46;04]我的梦说别停留等待&#10;[01&#58;51&#46;93]就让光芒折射泪湿的瞳孔&#10;[01&#58;55&#46;66]映出心中最想拥有的彩虹&#10;[01&#58;59&#46;75]带我奔向那片有你的天空&#10;[02&#58;03&#46;67]因为你是我的梦&#10;[02&#58;09&#46;14]&#10;[02&#58;11&#46;72]我的梦&#10;[02&#58;13&#46;09]&#10;[02&#58;15&#46;13]我的梦&#10;[02&#58;16&#46;64]&#10;[02&#58;19&#46;60]我的梦&#10;[02&#58;21&#46;39]&#10;[02&#58;24&#46;27]世界会怎么变化&#10;[02&#58;26&#46;58]都不是意外&#10;[02&#58;28&#46;33]记得用心去回答&#10;[02&#58;30&#46;52]命运的精彩&#10;[02&#58;32&#46;34]世界会怎么变化&#10;[02&#58;34&#46;51]都离不开爱&#10;[02&#58;36&#46;25]记得成长的对话&#10;[02&#58;38&#46;28]&#10;[02&#58;39&#46;11]勇敢地说我不再等待&#10;[02&#58;45&#46;63]就让光芒折射泪湿的瞳孔&#10;[02&#58;49&#46;75]映出心中最想拥有的彩虹&#10;[02&#58;53&#46;74]带我奔向那片有你的天空&#10;[02&#58;57&#46;73]因为你是我的梦&#10;[03&#58;02&#46;71]&#10;[03&#58;05&#46;51]我的梦&#10;[03&#58;07&#46;32]&#10;[03&#58;09&#46;20]我的梦&#10;[03&#58;14&#46;12]因为你是我的梦",
        },
      };
      return new Promise((resolve) => {
        setTimeout(() => {
          resolve(obj);
        }, Math.round(Math.random() * (2000 - 500) + 500));
      });
    };
    
    /* 暴露API */
    const API = {
      queryLyric,
    };
    export default API;
    
  • Vue2进阶/VueQQMusic/src/assets/images/loading.gif

  • Vue2进阶/VueQQMusic/src/assets/images/music.svg

  • Vue2进阶/VueQQMusic/src/assets/images/sprite_play.png

  • Vue2进阶/VueQQMusic/src/assets/reset.min.css

    body,h1,h2,h3,h4,h5,h6,hr,p,blockquote,dl,dt,dd,ul,ol,li,button,input,textarea,th,td{margin:0;padding:0}body{font-size:12px;font-style:normal;font-family:"\5FAE\8F6F\96C5\9ED1",Helvetica,sans-serif}small{font-size:12px}h1{font-size:18px}h2{font-size:16px}h3{font-size:14px}h4,h5,h6{font-size:100%}ul,ol{list-style:none}a{text-decoration:none;background-color:transparent}a:hover,a:active{outline-width:0;text-decoration:none}table{border-collapse:collapse;border-spacing:0}hr{border:0;height:1px}img{border-style:none}img:not([src]){display:none}svg:not(:root){overflow:hidden}html{-webkit-touch-callout:none;-webkit-text-size-adjust:100%}input,textarea,button,a{-webkit-tap-highlight-color:rgba(0,0,0,0)}article,aside,details,figcaption,figure,footer,header,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block}audio:not([controls]),video:not([controls]){display:none;height:0}progress{vertical-align:baseline}mark{background-color:#ff0;color:#000}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-0.25em}sup{top:-0.5em}button,input,select,textarea{font-size:100%;outline:0}button,input{overflow:visible}button,select{text-transform:none}textarea{overflow:auto}button,html [type="button"],[type="reset"],[type="submit"]{-webkit-appearance:button}button::-moz-focus-inner,[type="button"]::-moz-focus-inner,[type="reset"]::-moz-focus-inner,[type="submit"]::-moz-focus-inner{border-style:none;padding:0}button:-moz-focusring,[type="button"]:-moz-focusring,[type="reset"]:-moz-focusring,[type="submit"]:-moz-focusring{outline:1px dotted ButtonText}[type="checkbox"],[type="radio"]{box-sizing:border-box;padding:0}[type="number"]::-webkit-inner-spin-button,[type="number"]::-webkit-outer-spin-button{height:auto}[type="search"]{-webkit-appearance:textfield;outline-offset:-2px}[type="search"]::-webkit-search-cancel-button,[type="search"]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-input-placeholder{color:inherit;opacity:.54}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}.clearfix:after{display:block;height:0;content:"";clear:both}
    
  • Vue2进阶/VueQQMusic/src/views/MusicPage.vue

    <template>
      <div class="music-box" v-if="info">
        <!-- 头部 -->
        <header class="header-box">
          <div class="base">
            <div class="cover">
              <img :src="info.pic" alt="" />
            </div>
            <div class="info">
              <h2 class="title">{{ info.title }}</h2>
              <h3 class="author">{{ info.author }}</h3>
            </div>
          </div>
          <a
            href="javascript:;"
            @click="handle"
            :class="{
              'player-button': true,
              move: isPlay,
            }"
          ></a>
        </header>
    
        <!-- 歌词 -->
        <main class="main-box">
          <div
            class="wrapper"
            :style="{
              transform: `translateY(${y}px)`,
            }"
          >
            <p
              v-for="item in lyricList"
              :key="item.id"
              ref="para"
              :class="{
                active: item.active,
              }"
            >
              {{ item.text }}
            </p>
          </div>
        </main>
    
        <!-- 尾部 -->
        <footer class="footer-box">
          <div class="bar">
            <span class="time current">{{ time.current }}</span>
            <div class="progress">
              <div
                class="already"
                :style="{
                  width: already,
                }"
              ></div>
            </div>
            <span class="time duration">{{ time.duration }}</span>
          </div>
          <a href="#" class="download">下载这首音乐</a>
        </footer>
    
        <!-- 其它 -->
        <audio
          :src="info.audio"
          class="audio-box"
          preload="metadata"
          ref="myAudio"
        ></audio>
        <div
          class="mark-image"
          :style="{
            backgroundImage: `url(${info.pic})`,
          }"
        ></div>
        <div class="mark-overlay"></div>
      </div>
      <div class="loading-box" v-else>
        <div class="content">
          <img src="../assets/images/loading.gif" alt="" />
          <span>奴家正在努力加载中...</span>
        </div>
      </div>
    </template>
    
    <script>
    // 格式化时间的方法
    const format = function format(time) {
      let minutes = Math.floor(time / 60),
        seconds = Math.round(time - minutes * 60);
      minutes = minutes < 10 ? "0" + minutes : "" + minutes;
      seconds = seconds < 10 ? "0" + seconds : "" + seconds;
      return {
        minutes,
        seconds,
      };
    };
    
    export default {
      name: "MusicPage",
      data() {
        return {
          info: null,
          isPlay: false,
          lyricList: [],
          y: 0, //记录wrapper移动的距离
          num: 0, //记录累计匹配的数量
          already: "0%",
          time: {
            current: "00:00",
            duration: "00:00",
          },
        };
      },
      methods: {
        // 歌词解析
        formatLyric(lyric) {
          // 处理歌词部分的特殊符号
          lyric = lyric.replace(/&#(\d+);/g, (value, $1) => {
            let instead = value;
            switch (+$1) {
              case 32:
                instead = " ";
                break;
              case 40:
                instead = "(";
                break;
              case 41:
                instead = ")";
                break;
              case 45:
                instead = "-";
                break;
              default:
            }
            return instead;
          });
          // 解析歌词信息
          let arr = [],
            index = 0;
          lyric.replace(
            /\[(\d+)&#58;(\d+)&#46;(?:\d+)\]([^&#?]+)(?:&#10;)?/g,
            (_, $1, $2, $3) => {
              arr.push({
                id: ++index,
                minutes: $1,
                seconds: $2,
                text: $3,
                active: false,
              });
            }
          );
          this.lyricList = arr;
        },
        // 控制音乐的播放或暂停的
        handle() {
          const myAudio = this.$refs.myAudio;
          if (myAudio.paused) {
            // 当前是暂停状态:我们让其播放
            myAudio.play();
            this.isPlay = true;
            this.playing();
            if (!this.timer) {
              this.timer = setInterval(this.playing, 1000);
            }
            return;
          }
          // 当前是播放:我们让其暂停
          myAudio.pause();
          this.isPlay = false;
          clearInterval(this.timer);
          this.timer = null;
        },
        // 播放中要做的事情
        playing() {
          let myAudio = this.$refs.myAudio,
            { currentTime, duration } = myAudio;
          if (isNaN(currentTime) || isNaN(duration)) return;
          let { minutes: curM, seconds: curS } = format(currentTime),
            { minutes: durM, seconds: durS } = format(duration);
    
          // 已经播放完毕
          if (currentTime >= duration) {
            this.playend();
            return;
          }
    
          // 控制进度条变化
          this.time.current = `${curM}:${curS}`;
          this.time.duration = `${durM}:${durS}`;
          this.already = `${(currentTime / duration) * 100}%`;
    
          // 控制歌词的变化
          let matchs = this.lyricList.filter((item) => {
            return item.minutes === curM && item.seconds === curS;
          });
          if (matchs.length === 0) return;
          this.lyricList.forEach((item) => {
            item.active = matchs.includes(item);
          });
          this.num += matchs.length;
          if (this.num > 3) {
            this.y = -(this.num - 3) * this.$refs.para[0].offsetHeight;
          }
        },
        // 播放完毕要做的事情
        playend() {
          clearInterval(this.timer);
          this.timer = null;
    
          this.time.current = "00:00";
          this.already = "0%";
          this.y = 0;
          this.num = 0;
          this.isPlay = false;
          this.lyricList((item) => (item.active = false));
        },
      },
      async created() {
        // 第一次渲染之前:向服务器发送请求
        try {
          let { code, data } = await this.$API.queryLyric();
          if (+code === 0) {
            // 请求成功
            this.info = Object.freeze(data);
            this.time.duration = data.duration;
            this.formatLyric(data.lyric);
            return;
          }
        } catch (_) {}
        // 请求失败
        alert("获取数据失败!请稍后再试!");
      },
    };
    </script>
    
    <style lang="less" scoped>
    /* 基础样式 */
    .header-box,
    .footer-box,
    .main-box {
      box-sizing: border-box;
      height: 100px;
      overflow: hidden;
    }
    
    .main-box {
      height: calc(100vh - 200px);
    }
    
    .text-clip {
      text-overflow: ellipsis;
      white-space: nowrap;
      overflow: hidden;
    }
    
    /* Loading层 */
    .loading-box {
      position: fixed;
      top: 0;
      left: 0;
      z-index: 9999;
      box-sizing: border-box;
      width: 100vw;
      height: 100vh;
      background: #555;
      display: flex;
      justify-content: center;
      align-items: center;
    
      .content {
        img,
        span {
          display: block;
        }
    
        img {
          margin: 0 auto;
          width: 50px;
          height: 50px;
        }
    
        span {
          margin-top: 10px;
          color: rgb(25, 137, 250);
        }
      }
    }
    
    /* 背景层 */
    .mark-overlay,
    .mark-image {
      position: absolute;
      top: -10%;
      left: -10%;
      width: 120%;
      height: 120%;
    }
    
    .mark-image {
      z-index: -2;
      background-repeat: no-repeat;
      background-size: cover;
      filter: blur(6px);
    }
    
    .mark-overlay {
      z-index: -1;
      background: rgba(0, 0, 0, 0.5);
    }
    
    /* 头部区域样式 */
    .header-box {
      display: flex;
      justify-content: space-between;
      align-items: center;
      padding: 15px;
    
      .player-button {
        margin-left: 5px;
        width: 35px;
        height: 35px;
        background: url("../assets/images/music.svg") no-repeat;
        background-size: 100% 100%;
    
        &.move {
          animation: musicMove 1s linear 0s infinite both;
        }
      }
    
      .base {
        flex-grow: 1;
        display: flex;
    
        .cover {
          width: 70px;
          height: 70px;
          background: #aaa;
    
          img {
            display: block;
            width: 100%;
            height: 100%;
          }
    
          img[src=""] {
            display: none;
          }
        }
    
        .info {
          flex-grow: 1;
          margin-left: 5px;
          max-width: 230px;
    
          .title,
          .author {
            line-height: 35px;
            color: #fff;
            font-size: 17px;
            .text-clip;
          }
    
          .author {
            font-size: 15px;
          }
        }
      }
    }
    
    @keyframes musicMove {
      0% {
        transform: rotate(0deg);
      }
    
      100% {
        transform: rotate(360deg);
      }
    }
    
    /* 歌词区域样式 */
    .main-box {
      .wrapper {
        transform: translateY(0);
        transition: transform 0.3s;
    
        p {
          height: 50px;
          line-height: 50px;
          text-align: center;
          font-size: 15px;
          color: @comText;
    
          &.active {
            color: @primary;
            transition: color 0.3s;
          }
        }
      }
    }
    
    /* 尾部区域样式 */
    .footer-box {
      padding: 0 10px;
    
      .download {
        display: block;
        margin: 0 auto;
        width: 213px;
        height: 50px;
        line-height: 50px;
        text-align: center;
        font-size: 18px;
        color: #fff;
        text-indent: 20px;
        border-radius: 25px;
        background: url("../assets/images/sprite_play.png") no-repeat @primary;
        background-size: 40px 350px;
        background-position: 10px -291.5px;
      }
    
      .bar {
        display: flex;
        align-items: center;
    
        .time {
          width: 40px;
          line-height: 46px;
          text-align: center;
          font-size: 12px;
          color: @comText;
        }
    
        .progress {
          position: relative;
          flex-grow: 1;
          height: 2px;
          background: @comText;
    
          .already {
            position: absolute;
            top: 0;
            left: 0;
            width: 0;
            height: 100%;
            background: @primary;
          }
        }
      }
    }
    
    /* 音频 */
    .audio-box {
      display: none;
    }
    </style>
    
  • Vue2进阶/VueQQMusic/src/App.vue

    <template>
      <div id="app" style="max-width: 540px;">
        <music-page />
      </div>
    </template>
    
    <script>
    import MusicPage from './views/MusicPage.vue'
    export default {
      name: 'App',
      components: {
        MusicPage
      }
    }
    </script>
    
    <style lang="less">
    @import './assets/reset.min.css';
    
    html,
    body,
    #app {
      height: 100%;
      overflow: hidden;
    }
    
    #app {
      position: relative;
      margin: 0 auto;
      font-size: 14px;
    }
    </style>
    
  • Vue2进阶/VueQQMusic/src/main.js

    import Vue from 'vue'
    import App from './App.vue'
    import API from './api'
    /* REM */
    import 'lib-flexible'
    /* FastClick */
    import FastClick from 'fastclick'
    FastClick.attach(document.body)
    
    Vue.prototype.$API = API
    Vue.config.productionTip = false
    
    new Vue({
      render: h => h(App)
    }).$mount('#app')
    
  • Vue2进阶/VueQQMusic/public/index.html

    <!DOCTYPE html>
    <html>
    
    <head>
      <meta charset="utf-8">
      <meta http-equiv="X-UA-Compatible" content="IE=edge">
      <meta name="viewport" content="width=device-width,initial-scale=1.0">
      <link rel="icon" href="<%= BASE_URL %>favicon.ico">
      <title>QQ音乐播放器「Vue2版」</title>
    </head>
    
    <body>
      <div id="app"></div>
    </body>
    
    </html>
    
  • Vue2进阶/VueQQMusic/babel.config.js

    module.exports = {
      presets: [
        '@vue/cli-plugin-babel/preset'
      ]
    }
    
  • Vue2进阶/VueQQMusic/jsconfig.json

    {
      "compilerOptions": {
        "target": "es5",
        "module": "esnext",
        "baseUrl": "./",
        "moduleResolution": "node",
        "paths": {
          "@/*": [
            "src/*"
          ]
        },
        "lib": [
          "esnext",
          "dom",
          "dom.iterable",
          "scripthost"
        ]
      }
    }
    
  • Vue2进阶/VueQQMusic/package.json

    {
      "name": "vue_qq_music",
      "version": "0.1.0",
      "private": true,
      "scripts": {
        "serve": "vue-cli-service serve",
        "build": "vue-cli-service build",
        "lint": "vue-cli-service lint"
      },
      "dependencies": {
        "axios": "^1.4.0",
        "core-js": "^3.8.3",
        "fastclick": "^1.0.6",
        "hammerjs": "^2.0.8",
        "lib-flexible": "^0.3.2",
        "vue": "^2.6.14"
      },
      "devDependencies": {
        "@babel/core": "^7.12.16",
        "@babel/eslint-parser": "^7.12.16",
        "@vue/cli-plugin-babel": "~5.0.0",
        "@vue/cli-plugin-eslint": "~5.0.0",
        "@vue/cli-plugin-router": "~5.0.0",
        "@vue/cli-plugin-vuex": "~5.0.0",
        "@vue/cli-service": "~5.0.0",
        "eslint": "^7.32.0",
        "eslint-plugin-vue": "^8.0.3",
        "less": "^4.0.0",
        "less-loader": "^8.0.0",
        "postcss-pxtorem": "^6.0.0",
        "vue-template-compiler": "^2.6.14"
      },
      "eslintConfig": {
        "root": true,
        "env": {
          "node": true
        },
        "extends": [
          "plugin:vue/essential",
          "eslint:recommended"
        ],
        "parserOptions": {
          "parser": "@babel/eslint-parser"
        },
        "rules": {}
      },
      "browserslist": [
        "> 1%",
        "last 2 versions",
        "not dead"
      ]
    }
    
  • Vue2进阶/VueQQMusic/vue.config.js

    const { defineConfig } = require('@vue/cli-service')
    const env = process.env.NODE_ENV
    
    module.exports = defineConfig({
      /* 基础配置 */
      publicPath: './',
      lintOnSave: env !== 'production',
      transpileDependencies: [],
      productionSourceMap: false,
      /* DEV-SERVER */
      devServer: {
        host: '127.0.0.1',
        open: true,
        proxy: {
          '/api': {
            target: 'https://news-at.zhihu.com/api/4',
            changeOrigin: true,
            ws: true,
            pathRewrite: { "^/api": "" }
          }
        }
      },
      /* 进阶配置 */
      chainWebpack: config => {
        config.optimization.minimizer('terser')
          .tap(options => {
            let compress = options[0].terserOptions.compress
            compress.drop_console = true
            compress.drop_debugger = true
            return options
          })
      },
      css: {
        loaderOptions: {
          less: {
            lessOptions: {
              modifyVars: {
                primary: '#31C27C',
                comText: 'rgba(255, 255, 255, .5)'
              }
            }
          },
          postcss: {
            postcssOptions: {
              plugins: {
                'postcss-pxtorem': {
                  rootValue: 37.5,
                  propList: ['*']
                }
              }
            }
          }
        }
      }
    })
    
  • 说明

地址

  1. 工程化的项目中,一般不要用相对地址了。
    • 想拿到资源的绝对地址,建一个gitee仓库专门存放该文件,之后引用gitee仓库中的绝对路径。
  2. 拿到的data数据通过Object.freeze(data),可以进行冻结,让数据不再进行响应式处理。

常见面试题

  • 面试题:Vue 组件间通信有哪几种方式?
  • 面试题:怎样理解 Vue 的单向数据流?
  • 面试题:父组件可以监听到子组件的生命周期吗?
  • 面试题:平时开发中,你有没有封装过公共组件?如果封装过,则简单说一下你当时是怎么考虑的!
  • 面试题:vue中组件和插件有什么区别?

Vue组件间通信

  • 面试题:Vue 组件间通信有哪几种方式?
    • 在我之前的项目开发中,常见的组件通信方案,主要就是以下几种:
      • 父子组件通信(或者具备相同父亲的兄弟组件通信)
        • 父传子-父改子:基于属性、插槽、ref($children)。
          • 如果需要传递的是一些值/方法:基于属性即可。子组件中基于props注册接收,或者基于$attrs获取-没有经过props注册的。

            • 代码示例:
              • fang/f20230630/day0630/src/views/Demo.vue

                <template>
                  <div class="demo-box">
                    x-{{ x }} y-{{ y }}
                    我是父组件
                    <Child :x="x" :y="y" :fn="parentHandle"></Child>
                  </div>
                </template>
                
                <script>
                import Child from "./Child.vue";
                export default {
                  components: {
                    Child,
                  },
                  data() {
                    return {
                      x: 10,
                      y: 20,
                    };
                  },
                  methods: {
                    parentHandle(n,m) {
                      console.log(`this-->`, this,n,m);
                    },
                  },
                };
                </script>
                
                <style lang="less" scoped>
                .demo-box {
                  box-sizing: border-box;
                }
                </style>
                
              • fang/f20230630/day0630/src/views/Child.vue

                <template>
                    <div class="child-box">
                        我是子组件
                    </div>
                </template>
                
                <script>
                export default {
                  inheritAttrs: false,//把没注册的属性在html结构上也隐藏。
                  props:['x','fn'],
                  created(){
                    console.log(`子组件:this-->`, this);
                    console.log(`子组件:this.x-->`, this.x);//10;//获取注册接收的属性值。
                    console.log(`子组件:this.$attrs.y-->`, this.$attrs.y);//20;//获取没有被props注册接收的,在$attrs中。
                
                    this.fn(100,200)
                    
                  }
                }
                </script>
                
          • 如果需要传递的是一些HTML结构:基于插槽处理。子组件中基于$slots/$scopedSlots获取-于jsx语法使用、或者基于<slot>组件来渲染-于template模版使用。

          • 如果不想传递东西只想操作实例:在父组件中,可以基于ref($children)获取子组件的实例,这样就可以很方便地去操作子组件实例上的数据和方法了,然后想做什么就做什么。

        • 子传父-也叫子改父:基于属性-依赖于属性传递的回调函数(react就是这样干的)发布订阅$parent
          • 属性: 父组件基于属性把父组件的方法传递给子组件,子组件内部可以接收到这个方法并执行。

            1. 该父组件方法在执行时,内部的this始终是父组件。
            • 如果是把方法执行,此时可以传递一些子组件的信息给父组件,实现了子传父
            • 父组件的方法执行,一般都是用来修改父组件信息的,实现了子改父
            • 代码示例:
              • fang/f20230630/day0630/src/views/Demo.vue

                <template>
                  <div class="demo-box">
                    x-{{ x }} y-{{ y }}
                    我是父组件
                    <Child :x="x" :y="y" :fn="parentHandle"></Child>
                  </div>
                </template>
                
                <script>
                import Child from "./Child.vue";
                export default {
                  components: {
                    Child,
                  },
                  data() {
                    return {
                      x: 10,
                      y: 20,
                    };
                  },
                  methods: {
                    parentHandle(n,m) {
                      console.log(`this-->`, this,n,m);
                    },
                  },
                };
                </script>
                
                <style lang="less" scoped>
                .demo-box {
                  box-sizing: border-box;
                }
                </style>
                
              • fang/f20230630/day0630/src/views/Child.vue

                <template>
                    <div class="child-box">
                        我是子组件
                    </div>
                </template>
                
                <script>
                export default {
                  props:['x','fn'],
                  created(){
                    console.log(`子组件:this-->`, this);
                    console.log(`子组件:this.x-->`, this.x);//10;//获取注册接收的属性值。
                    console.log(`子组件:this.$attrs.y-->`, this.$attrs.y);//20;//获取没有被props注册接收的,在$attrs中。
                
                    this.fn(100,200)
                    
                  }
                }
                </script>
                
          • $parent: 子组件实例上的$parent是获取其父组件的实例,然后想什么就做什么。

            • $root:获取根组件的实例-即直接挂载到dom元素上的那个组件。
              • Vue2进阶/day0630/src/main.js

                import Vue from 'vue'
                import './global'
                import App from './App.vue'
                
                new Vue({
                  render: h => h(App)
                }).$mount('#app')
                
          • 发布订阅模式

            • 父组件调用子组件的时候,基于v-on(@)向子组件的事件池中注入父组件自定义事件和方法
            • 子组件内部,可以基于$listeners或者$emit通知父组件自定义事件执行并且传递相应的实参
            • 给子组件绑定v-model,或者传递属性的时候,设置.sync修饰符,都是基于此原理完成的。
        • 父子通信方案,是非常非常重要的知识,总结下来就是四个主要途径:属性插槽自定义事件组件实例,我们使用的UI组件库就是基于这些知识来进行封装的,我们后期自己封装通用的组件,也是基于这些内容来完成的!
    • 剩下的情况,基本上都是基于vuex/pina解决的。主要思路:
      • 创建一个全局的公共容器,来存储需要共享通信的信息。
      • 各组件都可以从指定的容器中获取公共信息,以及修改公共容器中的信息。
    • 在Vue2项目中,因为每个组件都是Vue的实例,所以我也会在Vue.prototype上,挂载一些通用的信息,供各组件调用。
      • 例如:
        • 包含接口请求的API挂载到Vue.prototype上。
        • UI组件库中,也经常把消息提示的方法放在Vue.prototype上。
    • 除此之外,我还了解到一些其它的方案,只不过我之前开发的时候,很少用!
      • EventBus事件总线:
        1. 创建一个全局通用的Vue实例或直接使用根组件实例上。
        2. 把需要共享的信息挂在根实例上。
        3. 某些组件可以基于$on向实例的事件池中注入自定义事件
        4. 某些组件可以基于$emit通知自定义事件执行。
      • 基于上下文信息,实现祖先和后代组件的通信或具备相同祖先的平行组件通信。
        1. 在祖先组件中:
          1. 管理着需要共享的状态信息,以及修改状态的方法。
          2. 基于provide把这些信息和方法,放在上下文中。
        2. 后代组件中,想用什么信息或者方法,直接基于inject获取即可!
        • 但是上下文方案我认为有个bug:上下文中的信息,默认是非响应式的,这样其实很不方便我们的操作,还需要我们特殊处理,我是感觉用起来有点麻烦!
          • 特殊处理就是把需要传的值用一个对象包起来-直接的inject获取到的只是非响应式对象;但inject中内部的属性,是响应式的;
    • 可能还有其它方案,我了解到的就这引起,只不过用到的就那几种:
      • 父子通信基于属性插槽自定义事件实例
      • 其余基本上都是基于vuexpina来管理;
      • 偶尔会用Vue.prototype

插槽

  • 默认插槽
  • 具名插槽
  • 作用域插槽:把子组件间中的某些数据,直接放在父组件插槽位置上使用。
    1. 在子组件中:把需要供父组件插槽位置使用的信息基于属性传递给<slot>内置组件
    2. 在父组件中:把想要读取的信息放到v-slot:插槽名='变量名'插槽名对应的插槽中返回的属性存放到变量名对应的变量中。

事件总线

上下文

进阶参考

  1. Easy-Mock
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值