B站视频直达,这个项目亮点在于所有 API 请求都并非在组件内编写,而是在组件内使用
this.$store.dispatch()
派发任务,再由 Vuex(actions、mutations、state三连操作) 获取后端数据后,渲染页面数据。
一、安装 Vue 脚手架
npm install -g @vue/cli
二、创建项目
vue create project(项目名称)
目录详解:
|- public // 静态页面目录,Webpack进行打包的时候会原封不动打包到dist文件夹中
|- index.html // 项目入口文件 (Webpack打包的js,css会自动注入到该页面中)
|- src // 源码目录 (程序员开发代码文件夹)
|- assets // 存放项目中用到的静态资源文件,例如:css 样式表、图片资源
|- components // 非路由组件、全局组件 (封装的、可复用的组件,都要放到 components 目录下)
|- views // 存放路由组件
|- App.vue // 唯一根组件
|- main.js // 项目的入口文件。项目的运行时最先执行的文件
|- babel.config.js // 配置文件(babel相关)
|- package.json // 项目的详细信息记录
|- package-lock.json // 缓存性文件(各种包的来源)
通过
main.js
把App.vue
渲染到index.html
的指定区域中。
vue-cli 打开项目流程
:
index.html ==> main.js ==> render(App.vue)
// 网页访问的是 index.html ,它引入了 main.js
// main.js 作为入口文件,它渲染了 App.vue。
package.json
"scripts": {
"serve": "vue-cli-service serve --open", // --open 运行项目时自动打开浏览器
"build": "vue-cli-service build",
"lint": "vue-cli-service lint"
},
Vue项目中的
vue.config.js
文件就是我们之前使用的webpack.config.js
。
根目录下vue.config.js
文件设置:
module.exports = {
css: {
sourceMap: true, // 开启 CSS source maps
},
lintOnSave: false //关闭eslint
}
vue.config.js加入:
module.exports = {
//关闭eslint
lintOnSave: false,
devServer: {
// 默认值为true开启热更新,false 则手动刷新
inline: true,
// 设置本地调试端口
port: 8001,
}
}
注意:修改完该配置文件后,要重启一下项目。
@/
代替src/
在根目录创建 jsconfig.json文件并写入:
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": [
"src/*"
]
}
},
"exclude": [
"node_modules",
"dist"
]
}
注意: 上面这个方法每次新建项目都要创建一个文件,如果你使用的是 vscode,使用下面这个方法可以一劳永逸:
1.在 vscode 扩展工具里安装 Path Autocomplete 插件(插件不唯一,也可以使用其他插件)
2.然后对其进行配置, 找到配置项Settings.json配置文件加入:
// 配置 @ 的路径提示
"path-autocomplete.pathMappings": {
"@": "${folder}/src"
},
我们可以需要修改public下的index.html文件:
或者在 main.js 引入
import '@/assets/css/reset.css';
* {
box-sizing: border-box;
font: inherit;
vertical-align: baseline;
/*
* 这个属性只用于iOS, 当你点击一个链接或者通过Javascript定义的可点击元素的时候
* 它就会出现一个半透明的灰色背景
*/
-webkit-tap-highlight-color: rgba(0,0,0,0);
}
html{
background-color:#fff;
color:#000;
font-size: 12px;
-webkit-text-size-adjust: 100%; /* 禁止字体变化 */
}
body {
-webkit-overflow-scrolling: touch; /* 设置滚动容器的滚动效果 */
-webkit-font-smoothing: antialiased; /* 字体抗锯齿渲染 */
}
html,body,ul,ol,dl,dd,h1,h2,h3,h4,h5,h6,figure,form,fieldset,legend,
input,textarea,button,p,blockquote,th,td,pre,xmp{
margin:0;
padding:0;
}
body,input,textarea,button,select,pre,xmp,tt,code,kbd,samp{
line-height: 1.5;
font-family: tahoma,arial,"Hiragino Sans GB",simsun,sans-serif;
}
h1,h2,h3,h4,h5,h6,small,big,input,textarea,button,select{
font-size:100%;
}
h1,h2,h3,h4,h5,h6{
font-family: tahoma,arial,"Hiragino Sans GB","微软雅黑",simsun,sans-serif;
}
h1,h2,h3,h4,h5,h6,b,strong{
font-weight:normal;
}
address,cite,dfn,em,i,optgroup,var{
font-style:normal;
}
table{
border-collapse:collapse;
border-spacing:0;
text-align:left;
}
caption,th{
text-align:inherit;
}
ul,ol,menu{
list-style:none;
}
fieldset,img{
border:0;
}
img,object,input,textarea,button,select{
vertical-align:middle;
}
article,aside,footer,header,section,nav,figure,figcaption,hgroup,details,menu{
display:block;
}
audio,canvas,video{
display:inline-block;
*display:inline;
*zoom:1;
}
blockquote:before,
blockquote:after,
q:before,
q:after{
content:"\0020";
}
textarea{
overflow:auto;
resize:vertical;
}
input,textarea,button,select,a{
outline:0 none;
border: none;
}
button::-moz-focus-inner,
input::-moz-focus-inner{
padding:0;
border:0;
}
mark{
background-color:transparent;
}
a,ins,s,u,del{
text-decoration:none;
}
sup,sub{
vertical-align:baseline;
}
a, a:active, a:hover {
/**
* 某些浏览器会给 a 设置默认颜色
*/
color: unset;
text-decoration: none;
}
ol, ul, li {
list-style: none;
}
input, textarea, select {
outline: none; /*去掉fouce时边框高亮效果*/
background: unset; /*去掉默认背景*/
-webkit-appearance: none; /* 去除ios输入框阴影 */
appearance: none;
}
非路由组件
放在components中,通过标签使用路由组件
放在pages 或views 中,通过配置路由使用$router
、 $route
属性
$router
: 进行路由跳转$route
: 获取路由信息(name、path、params等)声明式导航:通过
标签 ,(理解为一个 a 标签,可以添加 class )
编程式导航 :声明式导航能做的编程式都能做,而且还可以处理一些业务
我们在 App.vue 中导入了 Footer 组件,但是有些页面是不需要展示的,此时我们可以通过设置路由原信息 meta
和搭配 v-show 按需在页面展示 Footer 组件:
在路由中设置meta :
{
path: '/home',
component: Home,
meta: {
isHideFooter: true
}
},
v-show 与v-if 区别 :
1. 展示形式不同
v-if是 创建一个dom节点,通过元素上树与下树进行操作
v-show 是display:none 、 block,通过样式display控制
2. 使用场景不同
初次加载v-if要比v-show好,页面不会做加载盒子
频繁切换v-show要比v-if好,创建和删除的开销太大了,显示和隐藏开销较小
因为v-if会频繁的操作dom元素消耗性能,v-show只是通过样式将元素显示或隐藏。
query参数:不属于路径当中的一部分,路由不需要占位,写法类似于 ajax 当中query参数。
地址栏表现为:
/about?k1=v1&k2=v2
1. 显式 ==> 参数在url上
http://localhost:8080/about?a=1
传:this.$router.push({
path:'/about',
query:{
a:1
}
})
接:this.$route.query.a
params参数:属于路径当中的一部分,需要注意
配置路由的时候需要占位
。地址栏表现为/search/v1/v2
需要单独配置路由信息,如 :
{
name: 'search', // 当前路由的标识名称
path: '/search/:keyword?', // /:keyword就是一个params参数的占位符,?表示该参数可传可不传
component: Search,
},
{
name: 'detail',
path: '/detail/:skuId', // 配置路由跳转 params参数
component: Detail
},
注意: 因为使用
隐式传值跳转刷新界面后参数丢失问题
无法解决,所以路径传值一定是显示的。
1. Search路由项的path已经指定要传一个keyword的params参数:
path: "/search/:keyword",
2. 在Search组件中 执行下面进行路由跳转的代码:
this.$router.push({name:"Search",query:{keyword:this.keyword}})
当前跳转代码没有传递params参数
地址栏信息:http://localhost:8080/#/?keyword=asd
此时的地址信息少了/search
正常的地址栏信息: http://localhost:8080/#/search?keyword=asd
解决方法:可以通过改变path来指定params参数可传可不传
path: "/search/:keyword?", ?表示该参数可传可不传
参考链接
错误写法:
this.$router.push({name:"Search",query:{keyword:this.keyword},params:{keyword:''}})
出现的问题和 1 中的问题相同, 地址信息少了/search,解决方法: 加入|| undefined
,当我们传递的参数为空串时地址栏url 也可以保持正常:
this.$router.push({name:"Search",query:{keyword:this.keyword},params:{keyword:''||undefined}})
可以,但是只能传递params参数, 使用方法如下:
{
name: 'search', // 当前路由的标识名称
path: '/search/:keyword?', // /:keyword就是一个params参数的占位符,?表示该参数可传可不传
component: Search,
// 将params参数和query参数映射成 props 传入路由组件
props: route => ({ keyword3: route.params.keyword, keyword4: route.query.keyword2 })
},
this.$router.push("/search/"+this.params传参+" ? k= "+this.query传参)
this.$router.push(`/search/"+this.params 传参 +" ? k= "+this.query传参`)
this.$router.push({name:“路由名字”,params:{传参},query:{传参})
注意: 对象方式传参时,如果我们传参中使用了params,要跳转的路由只能使用name,不能使用path,query传参才可以使用 path。
多次执行相同的push问题,控制台会出现警告, 例如 :
let result = this.$router.push({name:"Search",query:{keyword:this.keyword}})
console.log(result) //返回了一个Promise
原因:因为 push返回的是一个Promise 对象,而Promise对象需要传递成功和失败两个参数,我们的push中并没有传递。
解决方法:
this.$router.push({name:‘Search’,params:{keyword: '3'||undefined}},()=>{},()=>{})
// 后面两项分别代表执行成功和失败的回调函数
这种写法治标不治本,将来在别的组件中使用编程式导航(push|replace)还是会有类似错误。
push是VueRouter.prototype的一个方法,在router中的index重写该方法即可:
//重写VueRouter.prototype原型对象身上的push|replace方法
//先把VueRouter.prototype身上的push|replace方法进行保存一份
let originPush = VueRouter.prototype.push;
let originReplace = VueRouter.prototype.replace;
VueRouter.prototype.push = function(location, resolve, reject) {
//第一个形参:路由跳转的配置对象(query|params)
//第二个参数:undefined|箭头函数(成功的回调)
//第三个参数:undefined|箭头函数(失败的回调)
if (resolve && reject) {
//push方法传递第二个参数|第三个参数(箭头函数)
//originPush:利用call修改上下文,变为(路由组件.$router)这个对象,第二参数:配置对象、第三、第四个参数:成功和失败回调函数
originPush.call(this, location, resolve, reject);
} else {
//push方法没有产地第二个参数|第三个参数
originPush.call(
this,
location,
() => {},
() => {}
);
}
};
VueRouter.prototype.replace = function(location, resolve, reject) {
if (resolve && reject) {
originReplace.call(this, location, resolve, reject);
} else {
originReplace.call(
this,
location,
() => {},
() => {}
);
}
};
第二种写法:
// 解决设置路由拦截时 跳转到登入页面时候的报错(router版本问题)
const originalPush = VueRouter.prototype.push
const originalReplace = VueRouter.prototype.replace
// push
VueRouter.prototype.push = function push(location, onResolve, onReject) {
if (onResolve || onReject) return originalPush.call(this, location, onResolve, onReject)
return originalPush.call(this, location).catch(err => err)
}
// replace
VueRouter.prototype.replace = function push(location, onResolve, onReject) {
if (onResolve || onReject) return originalReplace.call(this, location, onResolve, onReject)
return originalReplace.call(this, location).catch(err => err)
}
在 main.js 中注册:
//将三级联动组件注册为全局组件
import TypeNav from '@/pages/Home/TypeNav';
//第一个参数:全局组件名字,第二个参数:全局组件
Vue.component(TypeNav.name,TypeNav);
在组件中使用: 已经注册为全局组件,因此不需要引入:
在请求或响应被 then 或 catch 处理前拦截它们,减少服务器不必要的请求。
统一处理错误及配置请求信息
在请求拦截器中:
携带token令牌(设置在请求头中)、Loding效果开始
在响应拦截器中:
统一处理弹窗,结束Loding效果
在根目录下创建api/request.js
文件 :
import axios from "axios";
//1、对axios二次封装
const requests = axios.create({
//基础路径,requests发出的请求在端口号后面会跟改baseURl
baseURL:'/api',
timeout: 5000,
})
//2、配置请求拦截器 (可以携带请求头 token)
requests.interceptors.request.use(config => {
// console.log("请求拦截器----------------",config);
/* config内主要是对请求头Header配置
比如添加token(如果token在本地存储还在,就携带在请求头中)
--- config.headers.Authorization = token */
return config;
})
//3、配置响应拦截器
requests.interceptors.response.use((res) => {
// console.log("响应拦截器=------------------", res.data)
/*
let { code, msg } = res.data;
if ( msg) {
// 成功弹窗
if (code == 0) return Message({type: "success",message: msg})
Message.error(msg);
} */
return res.data; //成功的回调函数
},(error) => {
// console.log("响应失败回调--------------",error);
return Promise.reject(new Error('fail'))
})
//4、对外暴露一个axios实例
export default requests;
配置vue.config.js文件:
module.exports = {
lintOnSave: false,
devServer: {
inline: false,
port: 8001,
//代理服务器解决跨域
proxy: {
//会把请求路径中的/api换为后面的代理服务器
'/api': {
//提供数据的服务器地址
target: 'http://39.98.123.211',
}
},
}
}
注意:这里和以往配置跨域不同,因为上面封装axios的时候,
baseURL已经设置为了/api
,所以我们在写请求的时候都会携带/api
,在配置文件中我们就将/api进行了转换。在使用接口的时候就会拼接成http://39.98.123.211/api
。
将每个请求封装为一个函数,并暴露出去,组件只需要调用相应函数即可,方便统一修改接口。
在项目根目录下创建 api/index.js
文件
//当前模块,API进行统一管理,即对请求接口统一管理
import requests from "@/api/ajax";
//首页三级分类接口
export const reqCateGoryList = () => {
return requests({
url: '/product/getBaseCategoryList',
method: 'GET'
})
}
组件中按需使用 (不推荐):
import { reqCateGoryList } from './api'
reqCateGoryList(); //发起请求
每个页面都需要导入接口,是一件麻烦的事情,我们是否可以把所有接口挂载到vue原型上,在组件中按需导入,实现一劳永逸呢?
在main.js中导入所有接口:
import Vue from "vue";
import App from "./App.vue";
import router from "@/router";
import store from "@/store";
import * as API from '@/api'; //统一接口api文件夹里面全部请求函数
new Vue({
render: (h) => h(App),
beforeCreate() {
Vue.prototype.$API = API;
},
router,
store,
}).$mount("#app");
使用:
mounted() {
this.getPayInfo()
},
methods: {
async getPayInfo() {
let result = await this.$API.reqPayInfo(this.orderId)
}
如果项目中没有封装请求api,而是直接调用 axios ,就不需要使用async await,因为 axios 返回的就是一个Promise对象。
没有将函数封装前我们都会通过 then() 回调函数拿到服务器返回的数据,封装后依旧可以使用then获取数据:
categoryList(){
let result = reqCateGoryList().then(res=>{
console.log(res)
// return res
}
)
console.log(result) // 返回的是一个 Promise对象
}
上面我们已经把封装的所有接口都挂载在 vue原型中了,但是我们在vuex中也不想按需导入接口了,就想像组件中那样直接使用,使用方法如下:
// import { reqGetBannerList } from "@/api";
state:{
bannerList: [],
};
mutations : {
GETBANNERLIST(state, bannerList) {
state.bannerList = bannerList;
},
};
actions :{
async this._vm.$API.getBannerList({ commit }) {
let result = await reqGetBannerList();
if (result.code == 200) {
commit("GETBANNERLIST", result.data);
}
}
}
说明:当我们在 vuex中 打印 this 时可以看见 store 对象中有一个
_vm
就是vue 的实例对象,所以我们才可以直接通过this._vm.$API
使用。
nprogress 进度条插件链接
发起请求时页面上方会出现蓝色(默认)进度条。
使用: 在请求拦截器前开启进度条,在响应拦截器成功后关闭。
对应的ajax.js
设置:
import axios from "axios";
//引入进度条
import nprogress from 'nprogress';
//引入进度条样式
import "nprogress/nprogress.css";
//1、对axios二次封装
const requests = axios.create({
//基础路径,requests发出的请求在端口号后面会跟改baseURl
baseURL:'/api',
timeout: 5000,
})
//2、配置请求拦截器
requests.interceptors.request.use(config => {
//config内主要是对请求头Header配置
//比如添加token
//开启进度条
nprogress.start();
return config;
})
//3、配置相应拦截器
requests.interceptors.response.use((res) => {
//成功的回调函数
//响应成功,关闭进度条
nprogress.done()
return res.data;
},(error) => {
//失败的回调函数
console.log("响应失败"+error)
return Promise.reject(new Error('fail'))
})
//4、对外暴露
export default requests;
可以通过修改
node_modules/nprogress/nprogress.css
文件的background来修改进度条样式。
懒加载vue-lazyload插件官网
下载:
cnpm i vue-lazyload -S
1. 引入插件 import VueLazyload from "vue-lazyload";
2、注册插件 Vue.use(VueLazyload)
async addOrUpdateShopCart({commit},{skuId,skuNum}){
let result = await reqAddOrUpdateShopCart(skuId,skuNum)
}
注意:使用
action
时,函数的第一个参数,必须是{commit},即使不涉及到mutations
操作,也必须加上该参数,否则会报错。
详情见[Vuex] 官网: 和传值方法 跳转至参考链接→
如果不使用getters属性,我们在组件获取state中的数据表达式为:
this.$store.state.子模块.属性
,又长又不方便复用,Vuex 允许我们在 store 中定义 getters(可以认为是 store 的计算属性),就像计算属性一样,getter 的返回值会根据它的依赖被缓存起来,且只有当它的依赖值发生了改变才会被重新计算。
我们在Search模块中获取商品列表数据就是通过getters实现,需要注意的是当网络出现故障时应该将返回值设置为空,如果不设置返回值就变成了undefined:
store中search模块代码:
import {reqGetSearchInfo} from '@/api';
const state = {
searchList:{},
}
const mutations = {
SEARCHLIST(state,searchList){
state.searchList = searchList
}
}
const actions = {
//第二个参数data默认是一个空对象
async getSearchListr({commit},data={}){
let result = await reqGetSearchInfo(data)
if(result.code === 200){
commit("SEARCHLIST",result.data)
}
}
}
const getters = {
goodsList(state){
//网络出现故障时应该将返回值设置为空
return state.searchList.goodsList||[]
}
}
export default {
state,
mutations,
actions,
getters,
}
在Search组件中使用getters获取仓库数据:
<script>
//引入mapGetters
import {mapGetters} from 'vuex'
export default {
name: 'Search',
computed:{
//使用mapGetters,参数是一个数组,数组的元素对应getters中的函数名
...mapGetters(['goodsList'])
}
}
</script>
访问undefined的属性值会引起红色警告,网络正常时不会出错,一旦无网络或者网络问题就会gg。
下细节在于getters的返回值。如果getters按上面代码写为return state.goodInfo.categoryView
,页面可以正常运行,可以不处理,但是要明白红色警告警告的原因。
所以我们在写getters的时候要养成一个习惯在返回值后面加一个
||
条件。即当属性值undefined时,会返回 || 后面的数据,这样就不会报错。当然,如果返回值为对象加|| {}
,数组:|| [ ]
。
防抖:用户操作很频繁,但是只执行一次,减少业务负担。
节流:用户操作很频繁,但是把频繁的操作变为少量的操作,使浏览器有充分时间解析代码
[防抖和节流详情]https://www.jianshu.com/p/c8b86b09daf0
loadsh官网
– 防抖函数
– 节流函数
下面代码为设置节流,如果操作很频繁,限制50ms执行一次。这里函数定义采用的键值对形式。throttle的返回值就是一个函数,所以直接键值对赋值就可以,函数的参数在function中传入即可。
// throttle是节流函数
import {throttle} from 'lodash'
methods: {
//鼠标进入修改响应元素的背景颜色
//采用键值对形式创建函数,将changeIndex定义为节流函数,该函数触发很频繁时,设置50ms才会执行一次
changeIndex: throttle(function (index){
this.currentIndex = index
},50),
//鼠标移除触发时间
leaveIndex(){
this.currentIndex = -1
}
}
上面并没有通过 npm 下载 loadsh插件,因为vue脚手架自带,直接引入即可。
如图所示,三级标签每一个标签都是一个页面链接,要实现通过点击表现进行路由跳转。
使用导航式路由
:有多少个a标签就会生成多少个router-link
标签,router-link是vue提供的组件,这样当我们频繁操作时会出现卡顿现象。使用编程式路由
:通过触发点击事件实现路由跳转。同理有多少个a标签就会有多少个触发函数。虽然不会出现卡顿,但是也会影响性能。使用编程时导航+事件委派
的方式实现路由跳转 :
事件委派即把子节点的触发事件都委托给父节点,这样只需要一个回调函数 goSearch 就可以解决。
为三个等级的a标签添加自定义属性
date-categoryName
绑定商品标签名称来标识a标签(其余的标签是没有该属性的)。
为三个等级的 a 标签分别 添加自定义属性
data-category1Id
(一级)、data-category2Id
(二级)、data-category3Id
(三级) 并获取各级商品id,用于路由跳转。
然后再通过goSearch() 函数中传入event参数,获取当前的点击事件,通过event.target属性获取当前点击节点,再通过 dataset
属性获取节点的属性信息。
//函数使用
<div class="all-sort-list2" @click="goSearch" @mouseleave="leaveIndex">
//函数定义
goSearch(event){
console.log(event.target) // 点击后会输出当前元素的 dom节点
}
注意: event是系统属性,只需要在函数定义的时候作为参数传入,函数使用的时候不需要传参。
完整代码:
goSearch(event){
let element = event.target
//html中会把大写转为小写
// 获取和解构 4个自定义属性
let {categoryname,category1id,category2id,category3id} = element.dataset
//categoryname存在,表示为a标签
if(categoryname){
//整理路由跳转的参数
let location = {name:'Search'} //跳转路由name
let query = {categoryName:categoryname} //路由参数
// 判断属于哪一个等级的a标签
if(category1id){
query.category1Id = category1id // 一级
}else if(category2id){
query.category2Id = category2id // 二级
}else if(category3id){
query.category3Id = category3id // 三级
}
location.query = query //整理完参数
this.$router.push(location) //路由跳转
}
}
Vue 在路由切换的时候会销毁旧路由。
我们在三级列表全局组件中的mounted进行了请求一次商品分类列表数据,当我们在包含三级列表全局组件的不同组件之间进行切换时,都会进行一次信息请求。
由于信息都是一样的,处于性能考虑我们希望该数据只请求一次,所以我们把这次请求放在
App.vue
的mounted
中,根组件App.vue 的mounted只会执行一次。
mock.js官网
在开发中,有时候后端的接口还未完成,而前端开发者可以mock一些数据(模拟的一些假的接口),当后端接口完成时,再把mock数据变为后台给的接口数据替换。
下载和引入:
cnpm i mockjs -S
mock用来拦截前端ajax请求,返回我们自定义的数据用于测试前端接口。我们可以将不同的数据类型封装为不同的json文件,创建mock/mockServer.js
文件:
//先引入mockjs模块
import Mock from 'mockjs';
//把JSON数据格式引入进来[JSON数据格式根本没有对外暴露,但是可以引入]
//webpack默认对外暴露的:图片、JSON数据格式
import banner from './banner.json';
import floor from './floor.json';
//mock数据:第一个参数请求地址 第二个参数:请求数据
Mock.mock("/mock/banner",{code:200,data:banner});//模拟首页大的轮播图的数据
Mock.mock("/mock/floor",{code:200,data:floor});
mock 接口书写完毕后,mock当中 mockServer.js 需要执行一次,如果不执行,和你没有书写一样的。
回到入口文件,引入mockServer.js :
import "@/mock/mockServe"; //引入MockServer.js----mock数据
actions
中发起请把数据提交给mutations
mutations
中把数据存放在 state
中以我们的首页轮播图数据为例:
mounted() {
this.$store.dispatch("getBannerList")
},
actions:{
//获取首页轮播图数据
async getBannerList({commit}){
let result = await reqGetBannerList()
if(result.code === 200){
commit("BANNERLIST",result.data)
}
}
}
//唯一修改state的部分
state : {
bannerList: [],
};
mutations:{
BANNERLIST(state,bannerList){
state.bannerList = bannerList
}
}
由于数据是通过异步请求获得的,在轮播图组件使用时,我们要通过计算属性computed 获取vuex中的state。
<script>
import {mapState} from "vuex";
export default {
//主键挂载完毕,请求轮播图图片
mounted() {
this.$store.dispatch("getBannerList")
},
computed:{
...mapState({
bannerList: (state => state.home.bannerList)
})
}
}
</script>
1.安装swiper、高版本问题多。
cnpm i swiper@5 -S
2.在需要使用轮播图的组件内导入swpier和它的css样式
3.在组件中创建swiper需要的dom标签(html代码,参考官网代码)
4.创建swiper实例
我们在mounted中先去异步请求了轮播图数据,然后又创建的swiper实例。由于请求数据是异步的,所以浏览器不会等待该请求执行完再去创建swiper,而是先创建了swiper实例,但是此时我们的轮播图数据还没有获得,就导致了轮播图展示失败。
解决思路:
swiper对象生效的前提是dom结构已经渲染好了,假如watch先监听到bannerList数据变化,执行回调函数创建了swiper对象,之后v-for才执行,这样也是无法渲染轮播图图片。
<template>
<div class="swiper-container" ref="cur">
<div class="swiper-wrapper">
<div class="swiper-slide" v-for="carousel in list" :key="carousel.id">
<img :src="carousel.imgUrl" />
</div>
</div>
<!-- 如果需要分页器 -->
<div class="swiper-pagination"></div>
<!-- 如果需要导航按钮 -->
<div class="swiper-button-prev"></div>
<div class="swiper-button-next"></div>
</div>
</template>
<script>
//引入Swiper
import Swiper from 'swiper'
export default {
name: 'Carousel',
props: ['list'],
watch: {
list: {
//立即监听:不管你数据有没有变化,我上来立即监听一次
//为什么watch监听到list:因为这个数据从来没有发生变化(数据是父亲给的,父亲给的时候就是一个对象,对象里面该有的数据都是有的)
immediate: true,
handler() {
//只能监听到数据已经有了,但是v-for动态渲染结构我们还是没有办法确定的,因此还是需要用nextTick
this.$nextTick(() => {
var mySwiper = new Swiper(this.$refs.cur, {
autoplay: {
delay: 3000,
stopOnLastSlide: false,
disableOnInteraction: false
},
loop: true,
// 如果需要分页器
pagination: {
el: '.swiper-pagination',
//点击小球的时候也切换图片
clickable: true
},
// 如果需要前进后退按钮
navigation: {
nextEl: '.swiper-button-next',
prevEl: '.swiper-button-prev'
}
})
})
}
}
}
}
</script>
注意:之前我们在学习watch时,一般都是监听的定义在data中的属性,但是我们这里是监听的computed中的属性,这样也是完全可以的,并且如果你的业务数据也是从store中通过computed动态获取的,也需要watch监听数据变化执行相应回调函数。
// 在轮播图最外层DOM中添加ref属性
<div class="swiper-container" id="mySwiper" ref="cur">
// 通过ref属性值获取DOM
new Swiper(this.$refs.cur,{...})
最初想法:在每个三级列表和收缩按钮加一个点击触发事件,只要点击了就执行搜索函数。
但是这是一个很蠢的想法,如果这样就会生成很多回调函数,很耗性能。
最佳方法:我们每次进行新的搜索时,我们的query和params参数中的部分内容肯定会改变,而且这两个参数是路由的属性。我们可以通过监听路由信息的变化来动态发起搜索请求。
//数据监听:监听组件实例身上的属性的属性值变化
watch: {
//监听路由的信息是否发生变化,如果发生变化,再次发起请求
$route(newValue, oldValue) {
//每一次请求完毕,应该把相应的1、2、3级分类的id置空的,让他接受下一次的相应1、2、3
// 合并参数对象,再次发请求之前整理带给服务器参数
Object.assign(this.searchParams, this.$route.query, this.$route.params)
//再次发起ajax请求
this.getData()
//如果下一次搜索时只有params参数,拷贝后会发现searchParams会保留上一次的query参数
//分类名字与关键字不用清理:因为每一次路由发生变化的时候,都会给他赋予新的数据
//所以每次请求结束后将相应参数制空
this.searchParams.category1Id = undefined
// 使用 undefined是为了提示性能,路由将不会携带undefined参数,如果使用空字符串还是会被传入
this.searchParams.category2Id = undefined
this.searchParams.category3Id = undefined
}
}
本次项目的面包屑操作主要就是两个删除逻辑:
删除分类:
//删除分类的名字
removeCategoryName() {
//把带给服务器的参数置空了,还需要向服务器发请求
//带给服务器参数说明可有可无的:如果属性值为空的字符串还是会把相应的字段带给服务器
//但是你把相应的字段变为undefined,当前这个字段不会带给服务器
this.searchParams.categoryName = undefined
this.searchParams.category1Id = undefined
this.searchParams.category2Id = undefined
this.searchParams.category3Id = undefined
this.getData()
//地址栏也需要需改:进行路由跳转(现在的路由跳转只是跳转到自己这里)
this.$router.push({ name: 'search', params: this.$route.params })
}
删除搜索关键字:
//删除关键字
removeKeyword() {
//给服务器带的参数searchParams的keyword置空
this.searchParams.keyword = undefined
//通知兄弟组件Header清除关键字
this.$bus.$emit('clear')
//进行路由的跳转
if (this.$route.query) {
this.$router.push({ name: 'search', query: this.$route.query })
}
}
header组件接受$bus通信:
mounted() {
// 组件挂载时就监听clear事件,clear事件在search模块中定义
// 当删除关键字面包屑时,触发该事件,同时header的输入框绑定的keyword要删除
this.$bus.$on("clear",()=>{
this.keyword = ''
})
}
SearchSelector组件有两个属性也会生成面包屑,分别为品牌名、手机属性,原理与搜索页相同。唯一的区别是,这里删除面包屑时不需要修改地址栏url,因为url是由路由地址确定的,并且只有query、params两个参数变化会影响路由地址变化。
总结:面包屑由四个属性影响:parads、query、品牌、手机属性
面包屑生成逻辑:判断相关属性是否存在,存在即显示。
排序的逻辑比较简单,只是改变一下请求参数中的order字段,后端会根据order值返回不同的数据来实现升降序。
order属性值为字符串:
1:asc 1代表综合
2:desc 2代表价格
asc代表升序,desc代表降序
下载阿里图标并引入:
// 在public/index引入该 css
<link rel="stylesheet" href="https://at.alicdn.com/t/font_2994457_qqwrvmss9l9.css">
<script>
import SearchSelector from './SearchSelector.vue'
import { mapGetters, mapState } from 'vuex'
export default {
name: 'Search',
data() {
return {
searchParams: {
//产品相应的id
category1Id: '',
category2Id: '',
category3Id: '',
//产品的名字
categoryName: '',
//搜索的关键字
keyword: '',
//排序:初始状态应该是综合且降序
order: '1:desc',
//第几页
pageNo: 1,
//每一页展示条数
pageSize: 3,
//平台属性的操作
props: [],
//品牌
trademark: ''
}
}
},
components: {
SearchSelector
},
//在挂载之前调用一次|可以在发请求之前将带有参数进行修改
beforeMount() {
//在发请求之前,把接口需要传递参数,进行整理(在给服务器发请求之前,把参数整理好,服务器就会返回查询的数据)
Object.assign(this.searchParams, this.$route.query, this.$route.params)
},
mounted() {
//在发请求之前咱们需要将searchParams里面参数进行修改带给服务器
this.getData()
},
methods: {
//把发请求的这个action封装到一个函数里面
//将来需要再次发请求,你只需要在调用这个函数即可
getData() {
this.$store.dispatch('getSearchList', this.searchParams)
},
//删除分类的名字
removeCategoryName() {
//把带给服务器的参数置空了,还需要向服务器发请求
//带给服务器参数说明可有可无的:如果属性值为空的字符串还是会把相应的字段带给服务器
//但是你把相应的字段变为undefined,当前这个字段不会带给服务器
this.searchParams.categoryName = undefined
this.searchParams.category1Id = undefined
this.searchParams.category2Id = undefined
this.searchParams.category3Id = undefined
this.getData()
//地址栏也需要需改:进行路由跳转(现在的路由跳转只是跳转到自己这里)
//严谨:本意是删除query,如果路径当中出现params不应该删除,路由跳转的时候应该带着
/** 这里永远成立,即使是空对象会跳转 */
if (this.$route.params) {
this.$router.push({ name: 'search', params: this.$route.params })
}
},
//删除关键字
removeKeyword() {
//给服务器带的参数searchParams的keyword置空
this.searchParams.keyword = undefined
//再次发请求,(其实没必要,路由跳转后属性变化,watch内的函数会重新发起请求)
this.getData()
//通知兄弟组件Header清除关键字
// ps: 这里也可以通过检测路由里的 keyword是否为空,如果为空就修改header组件的值
this.$bus.$emit('clear')
//进行路由的跳转
if (this.$route.query) {
this.$router.push({ name: 'search', query: this.$route.query })
}
},
//自定义事件回调
trademarkInfo(trademark) {
//1:整理品牌字段的参数 "ID:品牌名称"
this.searchParams.trademark = `${trademark.tmId}:${trademark.tmName}`
//再次发请求获取search模块列表数据进行展示
this.getData()
},
//删除品牌的信息
removeTradeMark() {
//将品牌信息置空
this.searchParams.trademark = undefined
//再次发请求
this.getData()
},
//收集平台属性地方回调函数(自定义事件)
attrInfo(attr, attrValue) {
//["属性ID:属性值:属性名"]
console.log(attr, attrValue)
//参数格式整理好
let props = `${attr.attrId}:${attrValue}:${attr.attrName}`
//数组去重 splice(index,1) set include indexOf 都可以
if (this.searchParams.props.indexOf(props) == -1) {
this.searchParams.props.push(props)
//再次发请求
this.getData()
}
},
//removeAttr删除售卖的属性
removeAttr(index) {
//再次整理参数
this.searchParams.props.splice(index, 1)
//再次发请求
this.getData()
},
//排序的操作
changeOrder(flag) {
//flag:用户每一次点击li标签的时候,用于区分是综合(1)还是价格(2)
//现获取order初始状态【咱们需要通过初始状态去判断接下来做什么】
let originOrder = this.searchParams.order
let orginsFlag = originOrder.split(':')[0]
let originSort = originOrder.split(':')[1]
//新的排序方式
let newOrder = ''
// 获取flag取asc和desc是否存在 不存在取反
//判断的是多次点击的是不是同一个按钮
if (flag == orginsFlag) {
newOrder = `${orginsFlag}:${originSort == 'desc' ? 'asc' : 'desc'}`
} else {
//点击不是同一个按钮
newOrder = `${flag}:${'desc'}`
}
//需要给order重新赋值
this.searchParams.order = newOrder
//再次发请求
this.getData()
},
//自定义事件的回调函数---获取当前第几页
getPageNo(pageNo) {
//整理带给服务器参数
this.searchParams.pageNo = pageNo
//再次发请求
this.getData()
}
},
computed: {
//mapGetters里面的写法:传递的数组,因为getters计算是没有划分模块【home,search】
...mapGetters(['goodsList']),
isOne() {
return this.searchParams.order.indexOf('1') != -1
},
isTwo() {
return this.searchParams.order.indexOf('2') != -1
},
isAsc() {
return this.searchParams.order.indexOf('asc') != -1
},
isDesc() {
return this.searchParams.order.indexOf('desc') != -1
},
//获取search模块展示产品一共多少数据
...mapState({
total: (state) => state.search.searchList.total
})
},
//数据监听:监听组件实例身上的属性的属性值变化
watch: {
//监听路由的信息是否发生变化,如果发生变化,再次发起请求
$route(newValue, oldValue) {
//每一次请求完毕,应该把相应的1、2、3级分类的id置空的,让他接受下一次的相应1、2、3
//再次发请求之前整理带给服务器参数
Object.assign(this.searchParams, this.$route.query, this.$route.params)
//再次发起ajax请求
this.getData()
//分类名字与关键字不用清理:因为每一次路由发生变化的时候,都会给他赋予新的数据
this.searchParams.category1Id = undefined
this.searchParams.category2Id = undefined
this.searchParams.category3Id = undefined
}
}
}
</script>
props 父向子
$on、$emit 子向父
$bus 全局事件总线(通常用于兄弟组件传值)
Vuex 全局组件共享
slot插槽 适用于父子组件通信
开发分页器必须的核心属性:
pageNo 当前页码
pageSize 每一页展示多少条数据
total 总数据
continues 连续展示的页码 (连续页码数一般为5、7、9 奇数,对称好看)
totalPage 总页数 Math.ceil(总数/ 每一页多少条数据)
核心逻辑: 获取连续页码的起始页码和末尾页码,通过计算属性获得。
// 父组件传递子组件的数据: 当前页、每一页展示多少条数据、总数据、连续页码数
props: ['pageNo', 'pageSize', 'total', 'continues'],
computed: {
//总共多少页
totalPage() {
//向上取整(计算总页数)
return Math.ceil(this.total / this.pageSize)
},
//计算出连续的页码的起始数字与结束数字[连续页码的数字:至少是5]
startNumAndEndNum() {
// 解构出 连续页码数、当前页码、总页数
const { continues, pageNo, totalPage } = this
//先定义两个变量存储起始数字与结束数字
let start = 0,end = 0
//连续页码数字5【就是至少五页】,如果出现不正常的现象【就是不够五页】
//不正常现象【总页数小于连续页码数】
if (continues > totalPage) {
start = 1
end = totalPage
} else {
//正常现象【连续页码5,但是你的总页数一定是大于5的】 Math.floor:向下取整
//起始、结束数字
start = pageNo - Math.floor(continues/2)
end = pageNo + Math.floor(continues/2)
//把出现不正常的现象【start数字出现0|负数】纠正
if (start < 1) {
start = 1
end = continues
}
//把出现不正常的现象[当前页码数大于总页码]纠正
if (end > totalPage) {
end = totalPage
start = totalPage - continues + 1
}
}
return { start, end }
}
}
当点击页码会将pageNo传递给父组件,然后父组件发起请求,最后渲染。这里还是应用通过自定义事件实现子组件向父组件传递信息。
// 在js中使用
var a = n;
console.log(`a的值是:${a}`); //a的值是:n
// 在html中使用
router滚动行为详情
import Vue from 'vue'
import VueRouter from 'vue-router'
import routes from './routes.js'
Vue.use(VueRouter)
// 向外默认暴露路由器对象
let router = new VueRouter({
routes, // 注册所有路由
//router-link跳转时回到顶部
scrollBehavior(to, from, savedPosition) {
//返回的这个y=0,代表的滚动条在最上方
return { y: 0 }
}
})
豪哥的方法很巧妙:在轮播图组件中设置一个currendIndex,用来记录所点击图片的下标,并用currendIndex 实现点击图片边框高亮设置,当符合图片的下标满足currentIndex===index
时,该图片就会被标记为选中。
在轮播图组件中,点击图片触发全局事件changeImg,参数为图片所在数组的下标:
changeImg(index){
//将点击的图片标识位高亮
this.currentIndex = index
//通知兄弟组件修改大图图片
this.$bus.$emit("changeImg",index)
}
对应的放大镜组件,在mounted监听该全局事件
mounted() {
this.$bus.$on("changeImg",(index)=>{
//修改当前响应式图片
this.currentIndex = index;
})
},
放大镜组件中也会有一个currentIndex,他用表示大图中显示的图片的下标(因为放大镜组件只能显示一张图片),全局事件传递的 index 赋值给currentIndex ,通过computed计算属性改变放大镜组件展示的图片下标。
// 放大镜组件展示图片的html代码
<img :src="imgObj.imgUrl " />
computed:{
imgObj(){
return this.skuImageList[this.currentIndex] || {}
}
},
参考链接 : JavaScript 实现放大镜功能
blur 与 change: 输入结束后,离开输入框,会先后触发 change 与 blur
1. 没有进行任何输入时:
不会触发change,但是会触发 blur
2. 输入后值并没有发生变更时:
change依旧不会触发
keydown、input、keyup、blur都会触发
当我们想要实现路由跳转并传递数据时,首相想到的就是路由的query传递参数,但是query适合传递单个数值的简单参数,所以如果想要传递对象之类的
复杂信息
,就可以通过Web Storage实现。
共性: 都是已字符串形式存储:
sessionStorage 会话存储,当前窗口关闭后就会删除。
localStorage 本地存储,存储在浏览器中,再次打来还存在。
存储前: JSON.stringify()将对象转为字符串
取数据: JSON.parse()将字符串转为对象
问题点: 由于后台只提供了删除单个商品的接口,所以要删除多个商品时,只能多次调用actions中的函数。
解决思路: 我们可能最简单的方法是在method的方法中多次执行dispatch删除函数,当然这种做法也可行,但是为了深入了解actions,我们还是要将批量删除封装为actions函数。
// 官网的教程,一个标准的actions函数如下所示:
deleteAllCheckedById(context) {
console.log(context)
}
我们可以看一下context到底是什么:
可以看到 context中包含有:
commit、dispatch、getters、state
所以我们可以在actions函数中通过dispatch调用其他的actions函数,可以通过getters获取仓库的数据:
//删除选中的所有商品
deleteAllCheckedById({dispatch,getters}) {
getters.getCartList.cartInfoList.forEach(item => {
let result = [];
//将每一次返回值添加到数组中
result.push(item.isChecked === 1?dispatch('deleteCartById',item.skuId):'')
})
return Promise.all(result)
},
购物车组件method批量删除函数:
//删除选中的所有商品
async deleteAllCheckedById(){
try{
await this.$store.dispatch('deleteAllCheckedById')
//删除成功,刷新数据
this.$store.dispatch("getCartList")
}catch (error){
alert(error)
}
}
修改商品的全部状态和批量删除的原理相同:
// --------- vuex 中的 actions ----------------->
async updateAllChecked({dispatch,getters},flag){
let result = []
getters.getCartList.cartInfoList.forEach(item => {
result.push(dispatch('reqUpdateCheckedById',{skuId:item.skuId,isChecked:flag
}))
})
return Promise.all(result)
}
// --------- 组件中定义的 method ----------------->
async allChecked(event){
let flag = event.target.checked ? 1 : 0
console.log(flag)
try{
await this.$store.dispatch('updateAllChecked',flag)
this.$store.dispatch("getCartList")
}catch (error){
alert(error)
}
}
虽然getters中在获取 getCartList 时已经设置了 || {}
,但在组件中我们通过 computed 获取的是 getters 中的cartInfoList,它是一个数组,所以我们还需设置默认返回值。
问题原因: 组件的 computed 中的cartInfoList没有写 || []
返回值
cartInfoList(){
return this.getCartList.cartInfoList || [];
},
总结: 即使在getters设置了默认返回值,但是在组件中使用时还要使用计算属性筛选数据,必须再次设置默认返回值。
由于登录按钮的父节点是一个form表单,如果使用@click触发登录事件,form表单会执行默认事件action实现页面跳转。这里我们使用
@click.prevent
,它可以阻止自身默认事件的执行。
actions登陆函数:
//登录
async userLogin({commit},data){
let result = await reqPostLogin(data)
//服务器会返回token
if(result.code === 200){
//token存入vuex
commit("SETUSERTOKEN",result.data.token)
//持久化存储token
localStorage.setItem('TOKEN',result.data.token)
return 'ok'
}else{
return Promise.reject(new Error(result.message))
}
},
mutations设置用户token:
//设置用户token
SETUSERTOKEN(state,token){
state.token = token
}
登陆组件methods登陆函数:
async goLogin(){
try{
//会将this中的phone,password以对象的形式返回
const {phone,password} = this
phone && password && await this.$store.dispatch('userLogin',{phone,password})
//路由跳转到home首页
this.$router.push('/home')
}catch (error){
alert(error)
}
}
登陆成功后获取用户信息:
//-----------actions函数
async getUserInfo({commit}){
let result = await reqGetUserInfo();
//将用户信息存储到store中
if(result.code === 200){
//vuex存储用户信息
commit('SETUSERINFO',result.data)
return 'ok'
}else{
return Promise.reject(new Error(result.message))
}
},
// -----------mutations中 存储用户信息
SETUSERINFO(state,data){
state.userInfo = data
}
//--------------------router index.js全局前置守卫代码
router.beforeEach(async(to, from, next) => {
let token = store.state.user.token
let name = store.state.user.userInfo.name
//1、有token代表登录,全部页面放行
if(token){
//1.1登陆了,不允许前往登录页
if(to.path==='/login'){
next('/home')
} else{
//1.2、因为store中的token是通过localStorage获取的,token有存放在本地
// 当页面刷新时,token不会消失,但是store中的其他数据会清空,
// 所以不仅要判断token,还要判断用户信息
//1.2.1、判断仓库中是否有用户信息,有放行,没有派发actions获取信息
if(name)
next()
else{
//1.2.2、如果没有用户信息,则派发actions获取用户信息
try{
await store.dispatch('getUserInfo')
next()
}catch (error){
//1.2.3、获取用户信息失败,原因:token过期
//清除前后端token,跳转到登陆页面
await store.dispatch('logout')
next('/login')
}
}
}
}else{
//2、未登录,首页或者登录页可以正常访问
if(to.path === '/login' || to.path === '/home' || to.path==='/register')
next()
else{
alert('请先登录')
next('/login')
}
}
})
引出问题:
全局导航守卫已经帮助我们限制未登录的用户不可以访问相关页面。但是还会有一个问题
例如:
用户已经登陆,用户在home页直接通过地址栏访问trade结算页面,发现可以成功进入该页面,正常情况,用户只能通过在shopcart页面点击去结算按钮才可以到达trade页面。
通过路由独享守卫解决该问题:
// 在trade路由信息中加入路由独享守卫
//交易组件
{
name: 'Trade',
path: '/trade',
meta: {show:true},
component: () => import('@/pages/Trade'),
//路由独享首位
beforeEnter: (to, from, next) => {
if(from.path === '/shopcart' ){
next()
}else{
next(false) // 指回到from路由
}
}
}
此时又一个bug , 当我们在shopcart页面通过地址栏访问trade时还是会成功 !!!
解决办法:在shopcart 添加一个路由元信息 meta:{flag: false}
。
当点击去结算按钮后,将flag置为true。在trade的独享路由守卫中判断一下flag是否为true,当flag为true时,代表是通过点击去结算按钮跳转的,所以就放行。
//购物车
{
path: "/shopcart",
name: 'ShopCart',
component: ()=> import('../pages/ShopCart'),
meta:{show: true,flag: false},
},
// shopcart组件去结算按钮触发事件
toTrade(){
this.$route.meta.flag = true
this.$router.push('/trade')
}
// trade路由信息
{
name: 'Trade',
path: '/trade',
meta: {show:true},
component: () => import('@/pages/Trade'),
//路由独享首位
beforeEnter: (to, from, next) => {
if(from.path === '/shopcart' && from.meta.flag === true){
// 注意,判断通过后,在跳转之前一定要将flag置为false。
from.meta.flag = false
next()
}else{
next(false)
}
}
}
配置二级路由:
//个人中心
{
name: 'Center',
path: '/center',
component: () => import('@/pages/Center'),
children: [
{
//二级路由要么不写/,要么写全:'/center/myorder'
path: '/center/myorder',
component: () => import('@/pages/Center/MyOrder')
},
{
path: '/center/groupbuy',
component: () => import('@/pages/Center/GroupOrder'),
},
//默认显示
{
path: '',
redirect: 'myorder'
}
]
}
控制台警告问题:
总结警告缘由:当某个路由有子级路由时,父级路由必须要一个
默认的路由
,因此父级路由不能定义name属性,解决办法是去掉name:'Center'
就好了。
Object.asign() 浅拷贝
JSON.stringify() 深拷贝
every遍历某个数组,判断数组中的元素是否满足表达式,全部为满足返回true,否则返回false
【例】:判断底部勾选框是否全部勾选:
//判断底部勾选框是否全部勾选
isAllCheck() {
//every遍历某个数组,判断数组中的元素是否满足表达式,全部为满足返回true,否则返回false
return this.cartInfoList.every(item => item.isChecked === 1)
}
Promise.all可以将多个Promise实例包装成一个新的Promise实例。成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。
//删除选中的所有商品
deleteAllCheckedById({dispatch,getters}) {
getters.getCartList.cartInfoList.forEach(item => {
let result = [];
//将每一次返回值添加到数组中
result.push(item.isChecked === 1?dispatch('deleteCartById',item.skuId):'')
})
return Promise.all(result)
},
const {comment,index,deleteComment} = this
上面的这句话是一个简写,最终的含义相当于:
const comment = this.comment
const index = this.index
const deleteComment = this.deleteComment
. map
文件的作用:
可以理解为代码地图,因为代码是经过加密的,如果运行时报错,输出的错误信息无法准确得知时哪行的代码报错。而map文件就是用来代码定位,有了map才能准确的输出是哪一行那一列有错。
npm run build 打包项目
因为map文件较大,上线前需要删除map文件,
也可以通过在`vue.config.js`配置 `productionSourceMap: false`实现打包时不输出map文件。
为了告诉 Vue,传入的是 这是一个 JavaScript 表达式,如果不使用v-bind 将会被解析成一个字符串。 正确写法:
$refs:父组件访问子组件
如果在普通的DOM元素上使用,引用指向的是DOM元素;
如果用在子组件上,引用的是组件实例
$children:父组件访问子组件 (如果是多次的$refs操作,我们可以使用$children属性)
$parent: 子组件访问父组件
$root 根实例
$options 每一个Vue实例都有一个实例对象 options
$children 子组件实例 (是一个数组)
$parent 父组件实例
$emint 子组件抛出的自定义事件
$on 通过on接收自定义事件
$attrs和$props 加起来才是所有子组件的所有自定义属性:
$attrs 能获取到子组件所有未被props接收的属性 (排除了$props、class、style以外的其他属性,极端情况下使用,注意不能直接用模板字符串使用$attrs的数据)
$props 获取到子组件的所有props,父子传参时使用,正常情况下都用prpos传参,因为是响应式的数据也更安全。
$data 拿到组件完整的data对象
$refs 如果是HTML标签,拿到的是dom对象,如果是组件标签,拿到的是组件实例对象
$vnode 当前组件的虚拟节点
$router 拿到VueRouter实例
$route 获取组件的路由信息(name、meta、path、query等参数)
注意: App.vue 并不是根实例 ,是`$root根实例`的子组件。