day-102-one-hundred-and-two-20230630-QQ音乐-从零开始构建一个Vue2的项目-常见面试题-Vue组件间通信
QQ音乐
- 自适应适配。
从零开始构建一个Vue2的项目
-
基于@vue/cli脚手架创建项目。
vue create xxx
- 修改默认的配置项:
- 基础配置项:
- publicPath
- lintOnSave
- transpileDependencies
- productionSourceMap
- …
- dev-server
- host
- port
- open
- proxy
- …
- 进阶处理
- configWebpack
- chainWebpack
- 生产环境下去除console/debugger
- css
- 定义less中使用的公共样式变量。
- …
- babel.config.js
- UI组件库的按需导入
- …
- 基础配置项:
- 浏览器兼容的处理
- browserslist 浏览器的兼容列表。
- @babel/polyfill(或core-js)
- …
- 移动端的rem响应式布局开发
-
lib-flexible
- 帮助我们自动设置html的字体大小-也就是rem和px的换算比例,随着设备大小的切换,它也会跟着更改!
- 设置的值 = 设备的宽度 / 10
- 设计稿是375px的:1rem = 37.5px
- 设备宽度是414px:1rem = 41.4px
- 当设备宽度超过540px后,将不会继续放大。也就是最大的html字体大小是:54px。
- 具体步骤:
-
安装lib-flexible。
-
在入口文件中导入lib-flexible。
-
Vue2进阶/VueQQMusic/src/main.js
import 'lib-flexible'
-
-
解决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-pxtorem会把less中的样式代码中的px单位按照设置的长度改写为rem单位。
-
-
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
- …
-
- 修改默认的配置项:
-
配置一些项目中需要的基本骨架。
-
FastClick的处理。
-
API接口的统一管理-
/api文件夹
。import API from './api' Vue.prototype.$API = API
- 把接口放在Vue原型上,组件中this通过原型链可以取到。
-
准备一些静态资源 assets目录。
- 通用样式
- 静态资源图片。
- 公共的js方法库 utils.js
- 字体资源
- …
-
构建vuex的架子 store
-
构建vue-router的架子 router
- 分析路由
- 创建.vue单文件组件(页面
/views
) - 配置路由表(路由懒加载)
- …
-
global.js 全局需要处理的东西。
-
…
-
-
一个页面一个页面地开发
- 搭结构、写样式。
- 从服务器返回的原始数据,一般要进行冻结。
- 要获取DOM元素可通过ref来获取。
- 绑定的是一个循环出来的元素,得到的是元素列表。
- 在methods中的方法,实际上都是包了一层,每次执行时,都保证调用对应方法时this都为当前实例。
- 如果要定义一个只在该组件中用到的纯函数,可在
export default 组件
之前定义。
- 实现相应的需求功能。
- 最主要的是:注意组件的抽离和封装-通用的业务组件、UI组件库组件的二次封装、通用的功能组件…。
- EasyMock:创建假数据。
- 前后端联调-后端提供接口,把接口数据放到页面上。
- 搭结构、写样式。
-
优化与内测(自己测试几遍)
-
提测与Bug修复。
-
打包部署上线。
- 部署是放的是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:《我的梦》] [ar:张靓颖] [al:] [by:] [offset:0] [00:01.36]我的梦 (华为手机主题曲) - 张靓颖 [00:02.11]词:王海涛/张靓颖 [00:02.64]曲:Andy Love [00:03.48]编曲:崔迪 [00:04.49] [00:08.73]一直地一直地往前走 [00:11.65] [00:13.02]疯狂的世界 [00:14.58] [00:16.68]迎着痛把眼中所有梦 [00:20.52] [00:21.03]都交给时间 [00:22.71] [00:24.24]想飞就用心地去飞 [00:26.98]谁不经历狼狈 [00:30.68] [00:31.60]我想我会忽略失望的灰 [00:34.99]拥抱遗憾的美 [00:39.05]我的梦说别停留等待 [00:43.94]就让光芒折射泪湿的瞳孔 [00:47.74]映出心中最想拥有的彩虹 [00:51.78]带我奔向那片有你的天空 [00:55.74]因为你是我的梦 [01:01.06] [01:07.19]我的梦 [01:08.72] [01:16.75]执着地勇敢地不回头 [01:20.29] [01:21.05]穿过了黑夜踏过了边界 [01:24.87]路过雨路过风往前冲 [01:28.39] [01:28.96]总会有一天站在你身边 [01:32.52]泪就让它往下坠 [01:35.00]溅起伤口的美 [01:38.60] [01:39.55]哦别以为失去的最宝贵 [01:43.00]才让今天浪费 [01:47.04]我的梦说别停留等待 [01:51.93]就让光芒折射泪湿的瞳孔 [01:55.66]映出心中最想拥有的彩虹 [01:59.75]带我奔向那片有你的天空 [02:03.67]因为你是我的梦 [02:09.14] [02:11.72]我的梦 [02:13.09] [02:15.13]我的梦 [02:16.64] [02:19.60]我的梦 [02:21.39] [02:24.27]世界会怎么变化 [02:26.58]都不是意外 [02:28.33]记得用心去回答 [02:30.52]命运的精彩 [02:32.34]世界会怎么变化 [02:34.51]都离不开爱 [02:36.25]记得成长的对话 [02:38.28] [02:39.11]勇敢地说我不再等待 [02:45.63]就让光芒折射泪湿的瞳孔 [02:49.75]映出心中最想拥有的彩虹 [02:53.74]带我奔向那片有你的天空 [02:57.73]因为你是我的梦 [03:02.71] [03:05.51]我的梦 [03:07.32] [03:09.20]我的梦 [03:14.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+):(\d+).(?:\d+)\]([^&#?]+)(?: )?/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: ['*'] } } } } } } })
-
说明
地址
- 工程化的项目中,一般不要用相对地址了。
- 想拿到资源的绝对地址,建一个gitee仓库专门存放该文件,之后引用gitee仓库中的绝对路径。
- 拿到的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
-
属性
: 父组件基于属性
把父组件的方法传递给子组件,子组件内部可以接收到这个方法并执行。- 该父组件方法在执行时,内部的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')
-
- $root:获取根组件的实例-即直接挂载到dom元素上的那个组件。
-
发布订阅模式
:父组件
调用子组件的时候,基于v-on(@)向子组件的事件池
中注入父组件自定义事件和方法
。- 子组件内部,可以基于
$listeners
或者$emit
通知父组件自定义事件
执行并且传递相应的实参
。 - 给子组件绑定
v-model
,或者传递属性的时候,设置.sync修饰符
,都是基于此原理完成的。
-
- 父子通信方案,是非常非常重要的知识,总结下来就是四个主要途径:
属性
、插槽
、自定义事件
、组件实例
,我们使用的UI组件库就是基于这些知识来进行封装的,我们后期自己封装通用的组件
,也是基于这些内容来完成的!
- 父传子-父改子:基于属性、插槽、ref($children)。
- 父子组件通信(或者具备相同父亲的兄弟组件通信)
- 剩下的情况,基本上都是基于vuex/pina解决的。主要思路:
- 创建一个全局的公共容器,来存储需要
共享
或通信
的信息。 - 各组件都可以从指定的容器中获取公共信息,以及修改公共容器中的信息。
- 创建一个全局的公共容器,来存储需要
- 在Vue2项目中,因为每个组件都是Vue的实例,所以我也会在Vue.prototype上,挂载一些通用的信息,供各组件调用。
- 例如:
- 把
包含接口请求的API
挂载到Vue.prototype
上。 UI组件库
中,也经常把消息提示的方法
放在Vue.prototype
上。- …
- 把
- 例如:
- 除此之外,我还了解到一些其它的方案,只不过我之前开发的时候,很少用!
- EventBus事件总线:
- 创建一个全局通用的Vue实例或直接使用根组件实例上。
- 把需要共享的信息挂在根实例上。
- 某些组件可以基于$on向实例的事件池中注入
自定义事件
。 - 某些组件可以基于
$emit
通知自定义事件
执行。
- 基于上下文信息,实现祖先和后代组件的通信或具备相同祖先的平行组件通信。
- 在祖先组件中:
- 管理着需要共享的状态信息,以及修改状态的方法。
- 基于provide把这些信息和方法,放在上下文中。
- 后代组件中,想用什么信息或者方法,直接基于inject获取即可!
- 但是上下文方案我认为有个bug:上下文中的信息,默认是非响应式的,这样其实很不方便我们的操作,还需要我们特殊处理,我是感觉用起来有点麻烦!
- 特殊处理就是把需要传的值用一个对象包起来-直接的inject获取到的只是非响应式对象;但inject中内部的属性,是响应式的;
- 在祖先组件中:
- EventBus事件总线:
- 可能还有其它方案,我了解到的就这引起,只不过用到的就那几种:
- 父子通信基于
属性
、插槽
、自定义事件
、实例
; - 其余基本上都是基于
vuex
或pina
来管理; - 偶尔会用
Vue.prototype
。 - …
- 父子通信基于
- 在我之前的项目开发中,常见的组件通信方案,主要就是以下几种:
插槽
- 默认插槽
- 具名插槽
- 作用域插槽:把子组件间中的某些数据,直接放在父组件插槽位置上使用。
- 在子组件中:把
需要供父组件插槽位置使用的信息
基于属性
传递给<slot>内置组件
! - 在父组件中:把想要读取的信息放到
v-slot:插槽名='变量名'
把插槽名对应的插槽中返回的属性
存放到变量名对应的变量
中。
- 在子组件中:把