vue.js 在实际开发过程需要的坑

268 阅读5分钟

vue请求方式

先封装axios

import axios from "axios";

const const setDefaultParams = params => {
	return params;
}

const resolve = function (response, success, error) {
  const status = response.statue || response.status
  if (String(status) === '0') {
    success(response)
  } else {
    // console.log('api error=====', response)
    if (error) {
      error(response)
    } else {}
  }
}

const reject = function (response, error) {
  console.error('API Error')
  if (error) {
    error(response)
  }
}

// create an axios instance
const service = axios.create({
  baseURL: process.env.VUE_APP_BASE_API, // url = base url + request url
  // withCredentials: true, // send cookies when cross-domain requests
  timeout: 5000 // request timeout
});

// request interceptor
service.interceptors.request.use(
  config => {
    // do something before request is sent

    if (store.getters.token) {
      // let each request carry token
      // ['X-Token'] is a custom headers key
      // please modify it according to the actual situation
      config.headers["X-Token"] = getToken();
    }
    return config;
  },
  error => {
    // do something with request error
    console.log(error); // for debug
    return Promise.reject(error);
  }
);

// response interceptor
service.interceptors.response.use(
  /**
   * If you want to get http information such as headers or status
   * Please return  response => response
   */

  /**
   * Determine the request status by custom code
   * Here is just an example
   * You can also judge the status by HTTP Status Code
   */
  response => {
    const res = response.data;

    // if the custom code is not 20000, it is judged as an error.
    if (res.code !== 20000) {
      Message({
        message: res.message || "Error",
        type: "error",
        duration: 5 * 1000
      });

      // 50008: Illegal token; 50012: Other clients logged in; 50014: Token expired;
      if (res.code === 50008 || res.code === 50012 || res.code === 50014) {
        // to re-login
        MessageBox.confirm(
          "You have been logged out, you can cancel to stay on this page, or log in again",
          "Confirm logout", {
            confirmButtonText: "Re-Login",
            cancelButtonText: "Cancel",
            type: "warning"
          }
        ).then(() => {
          store.dispatch("user/resetToken").then(() => {
            location.reload();
          });
        });
      }
      return Promise.reject(new Error(res.message || "Error"));
    } else {
      return res;
    }
  },
  error => {
    console.log("err" + error); // for debug
    Message({
      message: error.message,
      type: "error",
      duration: 5 * 1000
    });
    return Promise.reject(error);
  }
);


service.get = (path, params, success, error) => {
  const paramsData = Object.assign({
  	t:new Date().getTime()
  },params)
  params = setDefaultParams(paramsData)
  const res = service({
    url: path + '?' + qs.stringify(params),
    method: 'get',
    data: params,
    transformRequest: function (obj) {
      let ret = ''
      for (const it in obj) {
        ret += encodeURIComponent(it) + '=' + encodeURIComponent(obj[it]) + '&'
      }
      return ret
    },
    headers: {
      'Content-Type': 'application/json'
    }
  })
  res.then(
    response => {
      resolve(response, success, error)
    },
    response => {
      reject(response, error)
    }
  )
}

service.put = (path, params, success, error) => {
  const paramsData = Object.assign({
  	t:new Date().getTime()
  },params)
  params = setDefaultParams(paramsData)
  const res = service({
    url: path + '?' + qs.stringify(params),
    method: 'put',
    data: params,
    transformRequest: function (obj) {
      let ret = ''
      for (const it in obj) {
        ret += encodeURIComponent(it) + '=' + encodeURIComponent(obj[it]) + '&'
      }
      return ret
    },
    headers: {
      'Content-Type': 'application/json'
    }
  })
  res.then(
    response => {
      resolve(response, success, error)
    },
    response => {
      reject(response, error)
    }
  )
}

service.post = (path, params, success, error) => {
  params = setDefaultParams(params)
  const res = service({
    url: path,
    method: 'post',
    params: params
  })
  res.then(
    response => {
      resolve(response, success, error)
    },
    response => {
      reject(response, error)
    }
  )
}

service.postJson = (path, params, success, error) => {
  params = setDefaultParams(params)
  const res = service({
    url: path,
    method: 'post',
    data: params,
    transformRequest: function (obj) {
      return JSON.stringify(obj)
    },
    headers: {
      'Content-Type': 'application/json'
    }
  })
  res.then(
    response => {
      resolve(response, success, error)
    },
    response => {
      reject(response, error)
    }
  )
  return res
}

export default service;

api 管理接口

import request from '@/utils/request'

export function fetchArticle(id) {
  return request({
    url: '/vue-element-admin/article/detail',
    method: 'get',
    params: {
      id
    }
  })
}

组件内接口调用

import { fetchArticle } from '@/api/article'

fetchArticle(id).then(response => {
  this.postForm = response.data

  // just for test
  this.postForm.title += `   Article Id:${this.postForm.id}`
  this.postForm.content_short += `   Article Id:${this.postForm.id}`

  // set tagsview title
  this.setTagsViewTitle()

  // set page title
  this.setPageTitle()
}).catch(err => {
  console.log(err)
})

在实际开发过程中,发现get请求时,IE 跟 火狐 浏览器 get 请求会第一次请求后缓存。解决需要在url 后拼接时间搓,如果按现在方式请求的话,我们需要在每个请求去拼接时间搓的,这样就不太便利的,为了能够全局 管理,我们这边改变 请求方式。

使用前,需要全局挂载

utils/index.js

import axios from './axios'

export default {
  install: (Vue) => {
    // 绑定到vue实例的原型链上,这样可以在组件中直接调用this.$axios.get(),而不需要用import引入axios模块
    Vue.prototype.$axios = axios
  }


组件内调用

  methods: {
    getList() {
      const uid = this.storage.get('yh-user').userId
      this.$axios.get(
        '/EipCountAction/getTaskCount.do',
        { userId: uid },
        res => {
          this.numb = res.dataContent
        }
      )
    }
  }

这样请求,不需要每个接口去拼接了,请求前 get方式统一 拼接 时间搓的

  const paramsData = Object.assign({
  	t:new Date().getTime()
  },params)
  params = setDefaultParams(paramsData)

image.png

请求后端的列表新增字段无法更新


下面是解决后的处理方式,使用 this.$set() 进行处理:

this.solutionList = r.data.data.solution;
for(let i=0;i<this.solutionList.length;i++){
	// 下面这种写法是没有get/set的,并且无法出发响应            
	// this.solutionList[i].is = true;
	this.$set(this.solutionList[i],'is',false);
}
  1. vue数组中添加新字段,改变字段后值没有比变化;
    • 提供了解决方案;
  2. vue2.0 给data对象新增属性,并触发视图更新;
    • 详细阐述了 this.$set()的正确使用方式;
  3. 给vue的数据添加新属性后实时响应失效问题;
    • 数据的更新,是需要 geter()/seter() 两个方法的;

正确的理解 attrsattrs 和 listeners


用简单的下图来描述Vue组件之间的数据通信:


事实上除了上图方式对数据进行通信之外,还有一些其他的方式,比如父组件获取子组件数据和事件可以通过:

  • 通过给子组件绑定ref属性来获取子组件实例
  • 通过this.$children获取子组件实例

对于子组件获取父组件数据和事件,可以通过:

  • 通过props传递父组件数据和事件,或者通过emitemit和on实现事件传递
  • 通过ref属性,调用子组件方法,传递数据;通过props传递父组件数据和事件,或者通过emitemit和on实现事件传递
  • 通过this.parent.parent.data或者this.parevent.data获取父组件数据,通过this.parevent._data获取父组件数据,通过this.parent执行父组件方法

对于兄弟组件之间数据通信和事件传递,可以通过:

  • 利用eventBus挂载全局事件
  • 利用parent进行数据传递,parent进行数据传递,parent.$children调用兄弟组件事件

另外,复杂一点的,可以通过Vuex完成Vue组件数据通信。特别是多级嵌套组件间的数据通信。但如果仅仅是数据之间传递,而不做中间处理,使用Vuex有点浪费。不过,自Vue 2.4版本开始提供了另一种方法:

使用v-bind="$attrs"将父组件中不被认为props特性绑定的属性传递给子组件。

通常该方法会配合interiAttrs一起使用。之所以这样使用是因为两者的出现使得组件之间跨组件的通信在不依赖Vuex和eventBus的情况下变得简洁,业务清晰。
其实这也就是我们今天要了解的另一个知识点。多级嵌套组件之间,我们如何借助attrsattrs和listeners来实现数据之间的通信。

业务场景

刚才提到过,我们接下来要聊的是多级嵌套组件之间的数据通信。为了让事情不变得太过于复杂(因为太复杂,对于初学者而言不易于理解和学习)。这里我们就拿三级组件之间的嵌套来举例。比如我们有三个组件ComponentA、ComponentB和ComponentC,而且它们之间的关系是ComponentA > ComponentB > ComponentC(>是包含关系),用下图来描述或许更易于明白他们之间的关系:


就三级嵌套的组件而言,他们的关系相对而言要简单一些:

  • ComponentA组件是ComponentB组件的父组件,他们的关系是父子关系
  • ComponentB组件是ComponentC组件的父组件,他们的关系也是父子关系
  • ComponentA组件是ComponentC组件的祖先组件,他们的关系是祖孙关系



对于这三个组件之间的数据通信,按照我们前面所掌握的知识,估计想到的是:

props向下,$emit向上。



也就是说,ComponentA向ComponentB可以通过props的方式向子组件传递,ComponentB向ComponentA通过在ComponentB组件中emit向上发送事件,然后在ComponentA组件中emit向上发送事件,然后在ComponentA组件中on的方式监听发送过来的事件。对于ComponentB和ComponentC两组件之间的通信也可以使用类似的方式。但对于ComponentA组件到ComponentC组件之间的通信,需要借助ComponentB组件做为中转站,当ComponentA组件需要把信息传递给ComponentC组件时,ComponentB接受ComponentA组件的信息,然后利用属性传递给ComponentC组件。
就此而言,这是一种解决方案,但如果我们嵌套的组件层级过多时将会导致代码繁琐,代码维护也较困难。

除了上述方式可以完成组件之间数据通信外,还有其他的方式,比如借助Vuex的全局状态共享;使用eventBus创建Vue的实例实现事件的监听和发布,从而实现组件之间的数据通信。但都过于太浪费,所以我们应该寻找其他更为简易的解决方案,其中文章开始提到的attrs以及attrs以及listeners。

简单地说,利用attrs实现祖孙组件间的数据传递,attrs实现祖孙组件间的数据传递,listeners实现祖孙组件间的事件监听。接下来看看怎么使用这两个特性来完成跨级嵌套组件之间的数据通信。

术语解释

在具体掌握attrsattrs和listeners是如何完成组件数据通信之前,先来简单地了解一下他们具体是什么?
Vue的官网对attrsattrs和listeners的描述分别是这样的:

$attrs的解释

包含了父作用域中不作为 props 被识别 (且获取) 的特性绑定 (class 和 style 除外)。当一个组件没有声明任何 props 时,这里会包含所有父作用域的绑定 (class 和 style 除外),并且可以通过 v-bind="$attrs" 传入内部组件 —— 在创建高级别的组件时非常有用。

$listeners的解释

包含了父作用域中的 (不含 .native 修饰器的) v-on 事件监听器。它可以通过 v-on="$listeners" 传入内部组件 —— 在创建更高层次的组件时非常有用。

官方解释的已经非常的清楚了。事实上,你可以把attrsattrs和listeners比作两个集合,其中attrs是一个属性集合,而attrs是一个属性集合,而listeners是一个事件集合,两者都是以对象的形式来保存数据。
更简单地说,利用attrs实现祖孙组件间的数据传递,attrs实现祖孙组件间的数据传递,listeners实现祖孙组件间的事件监听。而且attrs继承所有的父组件属性(除props传递的属性、classstyle),一般用在子组件的子元素上;attrs继承所有的父组件属性(除props传递的属性、class和style),一般用在子组件的子元素上;listeners是一个对象,里面包含了作用在这个组件上的所有监听器,配合v-on将所有事件监听器指向这个组件的某个特定的子元素(相当于子组件继承父组件的事件)。

为了更易于帮助大家理解这两个属性,我们还是通过一些简单的示例来演示吧。先来看一个简单的示例:

<!-- ChildComponent.vue -->
<template>
  <div class="child-component">
    <h1>我是一个 {{ professional }}</h1>
  </div>
</template>

<script>
export default {
  name: "ChildComponent",
  props: { professional: { type: String, default: "精神小伙" } },
  created() {
    console.log(this.$attrs, this.$listeners);
    // 调用父组件App.vue中的triggerTwo()方法 this.$listeners.two()
  },
};
</script>
<template>
  <div id="app">
    <ChildComponent :professional="professional"
                    :name="name"
                    @one.native="triggerOne"
                    @two="triggerTwo" />
  </div>
</template>

<script>
import ChildComponent from "./components/ChildComponent.vue";
export default {
  name: "app",
  data() {
    return { professional: "快乐小伙", name: "zhbb" };
  },
  components: { ChildComponent },
  methods: {
    triggerOne() {
      console.log("one");
    },
    triggerTwo() {
      console.log("two");
    },
  },
};
</script>

从上面的代码中我们可以看出来,在父组件App.vue中,调用子组件ChildComponent时有两个属性和两个方法,共别是其中有一个属性是props声明的(professional),事件一个是.native修饰器(监听组件根元素的原生事件)。
这个简单的示例告诉我们可以通过attrsattrs和listeners进行数据传递,在需要的地方进行调用和处理。比如上面子组件ChildComponent中通过this.listeners.two()访问了父组件App.vue中的triggerTwo()方法。当然,我们还可以通过von="listeners.two()访问了父组件App.vue中的triggerTwo()方法。当然,我们还可以通过v-on="listeners"一级级地往下传递,不管组件嵌套层级有多深。这个后面我们会详细介绍。
另外,上面的示例中,其中有一个属性是props,比如professional属性,另外还有一个非props属性,比如name。组件编译之后会把非props属性当成原始属性对待,从而添加到DOM元素(HTML标签上),比如上例中的name:
这样的结果或许并不是大家所想要的,如果想去掉HTML标签中name的属性,以至于该属性不暴露出来,我们可以借助inheritAttrs属性来完成。

inheritAttrs的默认值true,继承所有的父组件属性(除props的特定绑定)作为普通的HTML特性应用在子组件的根元素上,如果你不希望组件的根元素继承特性设置inheritAttrs: false,但是class属性会继承。简单的说,** inheritAttrs:true 继承除props之外的所有属性;inheritAttrs:false 只继承class属性**。

如果我们在子组件ChildComponent中添加inheritAttrs: false,重新编译出来的代码中name(非props)属性再不会暴露出来:

多级嵌套组件数据通信

前面花了很长的篇幅解释了attrsattrs和listeners以及它们是如何在组件中进行数据通信的。回到我们的示例中来,看看文章开头提以的三级嵌套组件之间的数据是如何借助attrsattrs和listeners实现数据通信。

<!-- ComponentA.vue --> 
<template>
  <div class="component-a">
    <ComponentB :name="name"
                :age="age"
                @on-test1="onTest1"
                @on-test2="onTest2" />
  </div>
</template>

<script>
import ComponentB from "./ComponentB";
export default {
  name: "ComponentA",
  components: { ComponentB },
  data() {
    return { name: "大zhbb", age: 23 };
  },
  methods: {
    onTest1() {
      console.log("test1 runing...");
    },
    onTest2() {
      console.log("test2 running...");
    },
  },
};
</script>
<!-- ComponentB.vue --> 
<template>
  <div class="component-b">
    <h3>组件B中的props: {{ age }}</h3>
    <p>组件B中的$attrs: {{ $attrs }}</p>
    <p>组件B中的$listeners: {{ $listeners }}</p>
    <hr />
    <ComponentC v-bind="$attrs"
                v-on="$listeners" />
  </div>
</template> <script>
import ComponentC from "./ComponentC";
export default {
  name: "ComponentB",
  props: { age: { type: Number, default: 30 } },
  inheritAttrs: false,
  components: { ComponentC },
  mounted() {
    this.$emit("test1");
    console.log("ComponentB", this.$attrs, this.$listeners);
  },
};
</script>
<!-- ComponentC.vue -->
<template>
  <div class="component-c">
    <h3>组件C中设置的props: {{ name }}</h3>
    <p>组件C中的$attrs: {{ $attrs }}</p>
    <p>组件C中的$listeners: {{ $listeners }}</p>
  </div>
</template>
 <script>
export default {
  name: "ComponentC",
  props: { name: { type: String, default: "zhbb" } },
  inheritAttrs: false,
  mounted() {
    this.$emit("test2");
    console.log("ComponentC", this.$attrs, this.$listeners);
  },
};
</script>
<template>
  <div id="app">
    <ComponentA />
  </div>
</template>

<script>
import ComponentA from "./components/ComponentA.vue";
export default {
  name: "app",
  data() {},
  components: {ComponentA },
  methods: {},
};
</script>

Vue $once('hook:beforeDestroy',e=>{})的使用 vue 如何销毁定时器

1、计时器Hook优化

在Vue中使用hook自动化清除定时器--程序化的事件侦听器

export default {
    mounted() {
        this.creatInterval('hello')
        this.creatInterval('world')
    },
    methods:{
		creatInterval(msg) {
	        let timer = setInterval(() => {
	            console.log(msg)
	        }, 1000)
	        this.$once('hook:beforeDestroy', function() {
	            clearInterval(timer)
	        })
	    }
	}
}

vue项目中使用$.once(‘hook:beforeDestory...

export default{
  methods:{
    fun1(){
      const timer = setInterval(()=>{
      	//需要做的事情
         console.log(11111);
      },1000);
      this.$once('hook:beforeDestroy',()=>{
        clearInterval(timer);
        timer = null;
      })
    }
  }
}

2、计时器Hook注入

Vue@hook那些事

<v-chart
    @hook:mounted="loading = false"
    @hook:beforeUpdated="loading = true"
    @hook:updated="loading = false"
    :data="data"
/>

Vue钩子函数之钩子事件hookEvent,监听组件

也许可以尝试hook子组件的自定义方法甚至数据的变更调用;

<custom-select @hook:updated="$_handleSelectUpdated" />

监听第三方组件数据的变化,但是组件又没有提供change事件,同事也没办法了,才想出来要去在外部监听组件的updated钩子函数。查看了一番资料,发现Vue支持在外部监听组件的生命周期钩子函数。

<template>
  <!--通过@hook:updated监听组件的updated生命钩子函数-->
  <!--组件的所有生命周期钩子都可以通过@hook:钩子函数名 来监听触发-->
  <custom-select @hook:updated="$_handleSelectUpdated" />
</template>
<script>
import CustomSelect from '../components/custom-select'
export default {
  components: {
    CustomSelect
  },
  methods: {
    $_handleSelectUpdated() {
      console.log('custom-select组件的updated钩子函数被触发')
    }
  }
}
</script>

Vue当中,hooks可以作为一种event,在Vue的源码当中,称之为hookEvent

在Vue组件中,可以用过on,on,once去监听所有的生命周期钩子函数,如监听组件的updated钩子函数可以写成 this.$on('hook:updated', () => {})